this repo has no description
1use crate::api::ApiError;
2use crate::state::AppState;
3use axum::{
4 Json,
5 extract::State,
6 http::StatusCode,
7 response::{IntoResponse, Response},
8};
9use chrono::{DateTime, Utc};
10use serde::{Deserialize, Serialize};
11use serde_json::json;
12
13#[derive(Serialize)]
14#[serde(rename_all = "camelCase")]
15pub struct GetMigrationStatusOutput {
16 pub did: String,
17 pub did_type: String,
18 pub migrated: bool,
19 #[serde(skip_serializing_if = "Option::is_none")]
20 pub migrated_to_pds: Option<String>,
21 #[serde(skip_serializing_if = "Option::is_none")]
22 pub migrated_at: Option<DateTime<Utc>>,
23}
24
25pub async fn get_migration_status(
26 State(state): State<AppState>,
27 headers: axum::http::HeaderMap,
28) -> Response {
29 let extracted = match crate::auth::extract_auth_token_from_header(
30 headers.get("Authorization").and_then(|h| h.to_str().ok()),
31 ) {
32 Some(t) => t,
33 None => return ApiError::AuthenticationRequired.into_response(),
34 };
35 let dpop_proof = headers.get("DPoP").and_then(|h| h.to_str().ok());
36 let http_uri = format!(
37 "https://{}/xrpc/com.tranquil.account.getMigrationStatus",
38 std::env::var("PDS_HOSTNAME").unwrap_or_else(|_| "localhost".to_string())
39 );
40 let auth_user = match crate::auth::validate_token_with_dpop(
41 &state.db,
42 &extracted.token,
43 extracted.is_dpop,
44 dpop_proof,
45 "GET",
46 &http_uri,
47 true,
48 )
49 .await
50 {
51 Ok(user) => user,
52 Err(e) => return ApiError::from(e).into_response(),
53 };
54 let user = match sqlx::query!(
55 "SELECT did, migrated_to_pds, migrated_at FROM users WHERE did = $1",
56 auth_user.did
57 )
58 .fetch_optional(&state.db)
59 .await
60 {
61 Ok(Some(row)) => row,
62 Ok(None) => return ApiError::AccountNotFound.into_response(),
63 Err(e) => {
64 tracing::error!("DB error getting migration status: {:?}", e);
65 return ApiError::InternalError.into_response();
66 }
67 };
68 let did_type = if user.did.starts_with("did:plc:") {
69 "plc"
70 } else if user.did.starts_with("did:web:") {
71 "web"
72 } else {
73 "unknown"
74 };
75 let migrated = user.migrated_to_pds.is_some();
76 (
77 StatusCode::OK,
78 Json(GetMigrationStatusOutput {
79 did: user.did,
80 did_type: did_type.to_string(),
81 migrated,
82 migrated_to_pds: user.migrated_to_pds,
83 migrated_at: user.migrated_at,
84 }),
85 )
86 .into_response()
87}
88
89#[derive(Deserialize)]
90#[serde(rename_all = "camelCase")]
91pub struct UpdateMigrationForwardingInput {
92 pub pds_url: String,
93}
94
95#[derive(Serialize)]
96#[serde(rename_all = "camelCase")]
97pub struct UpdateMigrationForwardingOutput {
98 pub success: bool,
99 pub migrated_to_pds: String,
100 pub migrated_at: DateTime<Utc>,
101}
102
103pub async fn update_migration_forwarding(
104 State(state): State<AppState>,
105 headers: axum::http::HeaderMap,
106 Json(input): Json<UpdateMigrationForwardingInput>,
107) -> Response {
108 let extracted = match crate::auth::extract_auth_token_from_header(
109 headers.get("Authorization").and_then(|h| h.to_str().ok()),
110 ) {
111 Some(t) => t,
112 None => return ApiError::AuthenticationRequired.into_response(),
113 };
114 let dpop_proof = headers.get("DPoP").and_then(|h| h.to_str().ok());
115 let http_uri = format!(
116 "https://{}/xrpc/com.tranquil.account.updateMigrationForwarding",
117 std::env::var("PDS_HOSTNAME").unwrap_or_else(|_| "localhost".to_string())
118 );
119 let auth_user = match crate::auth::validate_token_with_dpop(
120 &state.db,
121 &extracted.token,
122 extracted.is_dpop,
123 dpop_proof,
124 "POST",
125 &http_uri,
126 true,
127 )
128 .await
129 {
130 Ok(user) => user,
131 Err(e) => return ApiError::from(e).into_response(),
132 };
133 if !auth_user.did.starts_with("did:web:") {
134 return (
135 StatusCode::BAD_REQUEST,
136 Json(json!({
137 "error": "InvalidRequest",
138 "message": "Migration forwarding is only available for did:web accounts. did:plc accounts use PLC directory for identity updates."
139 })),
140 )
141 .into_response();
142 }
143 let pds_url = input.pds_url.trim();
144 if pds_url.is_empty() {
145 return ApiError::InvalidRequest("pds_url is required".into()).into_response();
146 }
147 if !pds_url.starts_with("https://") {
148 return ApiError::InvalidRequest("pds_url must start with https://".into()).into_response();
149 }
150 let pds_url_clean = pds_url.trim_end_matches('/');
151 let now = Utc::now();
152 let result = sqlx::query!(
153 "UPDATE users SET migrated_to_pds = $1, migrated_at = $2 WHERE did = $3",
154 pds_url_clean,
155 now,
156 auth_user.did
157 )
158 .execute(&state.db)
159 .await;
160 match result {
161 Ok(_) => {
162 tracing::info!(
163 "Updated migration forwarding for {} to {}",
164 auth_user.did,
165 pds_url_clean
166 );
167 (
168 StatusCode::OK,
169 Json(UpdateMigrationForwardingOutput {
170 success: true,
171 migrated_to_pds: pds_url_clean.to_string(),
172 migrated_at: now,
173 }),
174 )
175 .into_response()
176 }
177 Err(e) => {
178 tracing::error!("DB error updating migration forwarding: {:?}", e);
179 ApiError::InternalError.into_response()
180 }
181 }
182}
183
184pub async fn clear_migration_forwarding(
185 State(state): State<AppState>,
186 headers: axum::http::HeaderMap,
187) -> Response {
188 let extracted = match crate::auth::extract_auth_token_from_header(
189 headers.get("Authorization").and_then(|h| h.to_str().ok()),
190 ) {
191 Some(t) => t,
192 None => return ApiError::AuthenticationRequired.into_response(),
193 };
194 let dpop_proof = headers.get("DPoP").and_then(|h| h.to_str().ok());
195 let http_uri = format!(
196 "https://{}/xrpc/com.tranquil.account.clearMigrationForwarding",
197 std::env::var("PDS_HOSTNAME").unwrap_or_else(|_| "localhost".to_string())
198 );
199 let auth_user = match crate::auth::validate_token_with_dpop(
200 &state.db,
201 &extracted.token,
202 extracted.is_dpop,
203 dpop_proof,
204 "POST",
205 &http_uri,
206 true,
207 )
208 .await
209 {
210 Ok(user) => user,
211 Err(e) => return ApiError::from(e).into_response(),
212 };
213 if !auth_user.did.starts_with("did:web:") {
214 return (
215 StatusCode::BAD_REQUEST,
216 Json(json!({
217 "error": "InvalidRequest",
218 "message": "Migration forwarding is only available for did:web accounts"
219 })),
220 )
221 .into_response();
222 }
223 let result = sqlx::query!(
224 "UPDATE users SET migrated_to_pds = NULL, migrated_at = NULL WHERE did = $1",
225 auth_user.did
226 )
227 .execute(&state.db)
228 .await;
229 match result {
230 Ok(_) => {
231 tracing::info!("Cleared migration forwarding for {}", auth_user.did);
232 (StatusCode::OK, Json(json!({ "success": true }))).into_response()
233 }
234 Err(e) => {
235 tracing::error!("DB error clearing migration forwarding: {:?}", e);
236 ApiError::InternalError.into_response()
237 }
238 }
239}