this repo has no description
1use crate::api::proxy_client::{is_ssrf_safe, proxy_client, validate_did};
2use crate::api::ApiError;
3use crate::state::AppState;
4use axum::{
5 extract::State,
6 http::{HeaderMap, StatusCode},
7 response::{IntoResponse, Response},
8 Json,
9};
10use serde::Deserialize;
11use serde_json::json;
12use tracing::{error, info};
13#[derive(Deserialize)]
14#[serde(rename_all = "camelCase")]
15pub struct RegisterPushInput {
16 pub service_did: String,
17 pub token: String,
18 pub platform: String,
19 pub app_id: String,
20}
21const VALID_PLATFORMS: &[&str] = &["ios", "android", "web"];
22pub async fn register_push(
23 State(state): State<AppState>,
24 headers: HeaderMap,
25 Json(input): Json<RegisterPushInput>,
26) -> Response {
27 let token = match crate::auth::extract_bearer_token_from_header(
28 headers.get("Authorization").and_then(|h| h.to_str().ok()),
29 ) {
30 Some(t) => t,
31 None => return ApiError::AuthenticationRequired.into_response(),
32 };
33 let auth_user = match crate::auth::validate_bearer_token(&state.db, &token).await {
34 Ok(user) => user,
35 Err(e) => return ApiError::from(e).into_response(),
36 };
37 if let Err(e) = validate_did(&input.service_did) {
38 return ApiError::InvalidRequest(format!("Invalid serviceDid: {}", e)).into_response();
39 }
40 if input.token.is_empty() || input.token.len() > 4096 {
41 return ApiError::InvalidRequest("Invalid push token".to_string()).into_response();
42 }
43 if !VALID_PLATFORMS.contains(&input.platform.as_str()) {
44 return ApiError::InvalidRequest(format!(
45 "Invalid platform. Must be one of: {}",
46 VALID_PLATFORMS.join(", ")
47 ))
48 .into_response();
49 }
50 if input.app_id.is_empty() || input.app_id.len() > 256 {
51 return ApiError::InvalidRequest("Invalid appId".to_string()).into_response();
52 }
53 let appview_url = match std::env::var("APPVIEW_URL") {
54 Ok(url) => url,
55 Err(_) => {
56 return ApiError::UpstreamUnavailable("No upstream AppView configured".to_string())
57 .into_response();
58 }
59 };
60 if let Err(e) = is_ssrf_safe(&appview_url) {
61 error!("SSRF check failed for appview URL: {}", e);
62 return ApiError::UpstreamUnavailable(format!("Invalid upstream URL: {}", e))
63 .into_response();
64 }
65 let key_row = match sqlx::query!(
66 "SELECT key_bytes, encryption_version FROM user_keys k JOIN users u ON k.user_id = u.id WHERE u.did = $1",
67 auth_user.did
68 )
69 .fetch_optional(&state.db)
70 .await
71 {
72 Ok(Some(row)) => row,
73 Ok(None) => {
74 error!(did = %auth_user.did, "No signing key found for user");
75 return ApiError::InternalError.into_response();
76 }
77 Err(e) => {
78 error!(error = ?e, "Database error fetching signing key");
79 return ApiError::DatabaseError.into_response();
80 }
81 };
82 let decrypted_key =
83 match crate::config::decrypt_key(&key_row.key_bytes, key_row.encryption_version) {
84 Ok(k) => k,
85 Err(e) => {
86 error!(error = ?e, "Failed to decrypt signing key");
87 return ApiError::InternalError.into_response();
88 }
89 };
90 let service_token = match crate::auth::create_service_token(
91 &auth_user.did,
92 &input.service_did,
93 "app.bsky.notification.registerPush",
94 &decrypted_key,
95 ) {
96 Ok(t) => t,
97 Err(e) => {
98 error!(error = ?e, "Failed to create service token");
99 return ApiError::InternalError.into_response();
100 }
101 };
102 let target_url = format!("{}/xrpc/app.bsky.notification.registerPush", appview_url);
103 info!(
104 target = %target_url,
105 service_did = %input.service_did,
106 platform = %input.platform,
107 "Proxying registerPush request"
108 );
109 let client = proxy_client();
110 let request_body = json!({
111 "serviceDid": input.service_did,
112 "token": input.token,
113 "platform": input.platform,
114 "appId": input.app_id
115 });
116 match client
117 .post(&target_url)
118 .header("Authorization", format!("Bearer {}", service_token))
119 .header("Content-Type", "application/json")
120 .json(&request_body)
121 .send()
122 .await
123 {
124 Ok(resp) => {
125 let status =
126 StatusCode::from_u16(resp.status().as_u16()).unwrap_or(StatusCode::BAD_GATEWAY);
127 if status.is_success() {
128 StatusCode::OK.into_response()
129 } else {
130 let body = resp.bytes().await.unwrap_or_default();
131 error!(
132 status = %status,
133 "registerPush upstream error"
134 );
135 ApiError::from_upstream_response(status.as_u16(), &body).into_response()
136 }
137 }
138 Err(e) => {
139 error!(error = ?e, "Error proxying registerPush");
140 if e.is_timeout() {
141 ApiError::UpstreamTimeout.into_response()
142 } else if e.is_connect() {
143 ApiError::UpstreamUnavailable("Failed to connect to upstream".to_string())
144 .into_response()
145 } else {
146 ApiError::UpstreamFailure.into_response()
147 }
148 }
149 }
150}