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}