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::Utc; 10use serde::{Deserialize, Serialize}; 11use serde_json::json; 12 13#[derive(Debug, Clone, Serialize, Deserialize)] 14#[serde(rename_all = "camelCase")] 15pub struct VerificationMethod { 16 pub id: String, 17 #[serde(rename = "type")] 18 pub method_type: String, 19 pub public_key_multibase: String, 20} 21 22#[derive(Deserialize)] 23#[serde(rename_all = "camelCase")] 24pub struct UpdateDidDocumentInput { 25 pub verification_methods: Option<Vec<VerificationMethod>>, 26 pub also_known_as: Option<Vec<String>>, 27 pub service_endpoint: Option<String>, 28} 29 30#[derive(Serialize)] 31#[serde(rename_all = "camelCase")] 32pub struct UpdateDidDocumentOutput { 33 pub success: bool, 34 pub did_document: serde_json::Value, 35} 36 37pub async fn update_did_document( 38 State(state): State<AppState>, 39 headers: axum::http::HeaderMap, 40 Json(input): Json<UpdateDidDocumentInput>, 41) -> Response { 42 let extracted = match crate::auth::extract_auth_token_from_header( 43 headers.get("Authorization").and_then(|h| h.to_str().ok()), 44 ) { 45 Some(t) => t, 46 None => return ApiError::AuthenticationRequired.into_response(), 47 }; 48 let dpop_proof = headers.get("DPoP").and_then(|h| h.to_str().ok()); 49 let http_uri = format!( 50 "https://{}/xrpc/_account.updateDidDocument", 51 std::env::var("PDS_HOSTNAME").unwrap_or_else(|_| "localhost".to_string()) 52 ); 53 let auth_user = match crate::auth::validate_token_with_dpop( 54 &state.db, 55 &extracted.token, 56 extracted.is_dpop, 57 dpop_proof, 58 "POST", 59 &http_uri, 60 true, 61 ) 62 .await 63 { 64 Ok(user) => user, 65 Err(e) => return ApiError::from(e).into_response(), 66 }; 67 68 if !auth_user.did.starts_with("did:web:") { 69 return ApiError::InvalidRequest( 70 "DID document updates are only available for did:web accounts".into(), 71 ) 72 .into_response(); 73 } 74 75 let user = match sqlx::query!( 76 "SELECT id, handle, deactivated_at FROM users WHERE did = $1", 77 &auth_user.did 78 ) 79 .fetch_optional(&state.db) 80 .await 81 { 82 Ok(Some(row)) => row, 83 Ok(None) => return ApiError::AccountNotFound.into_response(), 84 Err(e) => { 85 tracing::error!("DB error getting user: {:?}", e); 86 return ApiError::InternalError(None).into_response(); 87 } 88 }; 89 90 if user.deactivated_at.is_some() { 91 return ApiError::AccountDeactivated.into_response(); 92 } 93 94 if let Some(ref methods) = input.verification_methods { 95 if methods.is_empty() { 96 return ApiError::InvalidRequest("verification_methods cannot be empty".into()) 97 .into_response(); 98 } 99 for method in methods { 100 if method.id.is_empty() { 101 return ApiError::InvalidRequest("verification method id is required".into()) 102 .into_response(); 103 } 104 if method.method_type != "Multikey" { 105 return ApiError::InvalidRequest( 106 "verification method type must be 'Multikey'".into(), 107 ) 108 .into_response(); 109 } 110 if !method.public_key_multibase.starts_with('z') { 111 return ApiError::InvalidRequest( 112 "publicKeyMultibase must start with 'z' (base58btc)".into(), 113 ) 114 .into_response(); 115 } 116 if method.public_key_multibase.len() < 40 { 117 return ApiError::InvalidRequest( 118 "publicKeyMultibase appears too short for a valid key".into(), 119 ) 120 .into_response(); 121 } 122 } 123 } 124 125 if let Some(ref handles) = input.also_known_as { 126 for handle in handles { 127 if !handle.starts_with("at://") { 128 return ApiError::InvalidRequest("alsoKnownAs entries must be at:// URIs".into()) 129 .into_response(); 130 } 131 } 132 } 133 134 if let Some(ref endpoint) = input.service_endpoint { 135 let endpoint = endpoint.trim(); 136 if !endpoint.starts_with("https://") { 137 return ApiError::InvalidRequest("serviceEndpoint must start with https://".into()) 138 .into_response(); 139 } 140 } 141 142 let verification_methods_json = input 143 .verification_methods 144 .as_ref() 145 .map(|v| serde_json::to_value(v).unwrap_or_default()); 146 147 let also_known_as: Option<Vec<String>> = input.also_known_as.clone(); 148 149 let now = Utc::now(); 150 151 let upsert_result = sqlx::query!( 152 r#" 153 INSERT INTO did_web_overrides (user_id, verification_methods, also_known_as, updated_at) 154 VALUES ($1, COALESCE($2, '[]'::jsonb), COALESCE($3, '{}'::text[]), $4) 155 ON CONFLICT (user_id) DO UPDATE SET 156 verification_methods = CASE WHEN $2 IS NOT NULL THEN $2 ELSE did_web_overrides.verification_methods END, 157 also_known_as = CASE WHEN $3 IS NOT NULL THEN $3 ELSE did_web_overrides.also_known_as END, 158 updated_at = $4 159 "#, 160 user.id, 161 verification_methods_json, 162 also_known_as.as_deref(), 163 now 164 ) 165 .execute(&state.db) 166 .await; 167 168 if let Err(e) = upsert_result { 169 tracing::error!("DB error upserting did_web_overrides: {:?}", e); 170 return ApiError::InternalError(None).into_response(); 171 } 172 173 if let Some(ref endpoint) = input.service_endpoint { 174 let endpoint_clean = endpoint.trim().trim_end_matches('/'); 175 let update_result = sqlx::query!( 176 "UPDATE users SET migrated_to_pds = $1, migrated_at = $2 WHERE did = $3", 177 endpoint_clean, 178 now, 179 &auth_user.did 180 ) 181 .execute(&state.db) 182 .await; 183 184 if let Err(e) = update_result { 185 tracing::error!("DB error updating service endpoint: {:?}", e); 186 return ApiError::InternalError(None).into_response(); 187 } 188 } 189 190 let did_doc = build_did_document(&state.db, &auth_user.did).await; 191 192 tracing::info!("Updated DID document for {}", &auth_user.did); 193 194 ( 195 StatusCode::OK, 196 Json(UpdateDidDocumentOutput { 197 success: true, 198 did_document: did_doc, 199 }), 200 ) 201 .into_response() 202} 203 204pub async fn get_did_document( 205 State(state): State<AppState>, 206 headers: axum::http::HeaderMap, 207) -> Response { 208 let extracted = match crate::auth::extract_auth_token_from_header( 209 headers.get("Authorization").and_then(|h| h.to_str().ok()), 210 ) { 211 Some(t) => t, 212 None => return ApiError::AuthenticationRequired.into_response(), 213 }; 214 let dpop_proof = headers.get("DPoP").and_then(|h| h.to_str().ok()); 215 let http_uri = format!( 216 "https://{}/xrpc/_account.getDidDocument", 217 std::env::var("PDS_HOSTNAME").unwrap_or_else(|_| "localhost".to_string()) 218 ); 219 let auth_user = match crate::auth::validate_token_with_dpop( 220 &state.db, 221 &extracted.token, 222 extracted.is_dpop, 223 dpop_proof, 224 "GET", 225 &http_uri, 226 true, 227 ) 228 .await 229 { 230 Ok(user) => user, 231 Err(e) => return ApiError::from(e).into_response(), 232 }; 233 234 if !auth_user.did.starts_with("did:web:") { 235 return ApiError::InvalidRequest( 236 "This endpoint is only available for did:web accounts".into(), 237 ) 238 .into_response(); 239 } 240 241 let did_doc = build_did_document(&state.db, &auth_user.did).await; 242 243 (StatusCode::OK, Json(json!({ "didDocument": did_doc }))).into_response() 244} 245 246async fn build_did_document(db: &sqlx::PgPool, did: &str) -> serde_json::Value { 247 let hostname = std::env::var("PDS_HOSTNAME").unwrap_or_else(|_| "localhost".to_string()); 248 249 let user = match sqlx::query!( 250 "SELECT id, handle, migrated_to_pds FROM users WHERE did = $1", 251 did 252 ) 253 .fetch_optional(db) 254 .await 255 { 256 Ok(Some(row)) => row, 257 _ => { 258 return json!({ 259 "error": "User not found" 260 }); 261 } 262 }; 263 264 let overrides = sqlx::query!( 265 "SELECT verification_methods, also_known_as FROM did_web_overrides WHERE user_id = $1", 266 user.id 267 ) 268 .fetch_optional(db) 269 .await 270 .ok() 271 .flatten(); 272 273 let service_endpoint = user 274 .migrated_to_pds 275 .unwrap_or_else(|| format!("https://{}", hostname)); 276 277 if let Some((ovr, parsed)) = overrides.as_ref().and_then(|ovr| { 278 serde_json::from_value::<Vec<VerificationMethod>>(ovr.verification_methods.clone()) 279 .ok() 280 .filter(|p| !p.is_empty()) 281 .map(|p| (ovr, p)) 282 }) { 283 let also_known_as = if !ovr.also_known_as.is_empty() { 284 ovr.also_known_as.clone() 285 } else { 286 vec![format!("at://{}", user.handle)] 287 }; 288 return json!({ 289 "@context": [ 290 "https://www.w3.org/ns/did/v1", 291 "https://w3id.org/security/multikey/v1", 292 "https://w3id.org/security/suites/secp256k1-2019/v1" 293 ], 294 "id": did, 295 "alsoKnownAs": also_known_as, 296 "verificationMethod": parsed.iter().map(|m| json!({ 297 "id": format!("{}{}", did, if m.id.starts_with('#') { m.id.clone() } else { format!("#{}", m.id) }), 298 "type": m.method_type, 299 "controller": did, 300 "publicKeyMultibase": m.public_key_multibase 301 })).collect::<Vec<_>>(), 302 "service": [{ 303 "id": "#atproto_pds", 304 "type": "AtprotoPersonalDataServer", 305 "serviceEndpoint": service_endpoint 306 }] 307 }); 308 } 309 310 let key_row = sqlx::query!( 311 "SELECT key_bytes, encryption_version FROM user_keys WHERE user_id = $1", 312 user.id 313 ) 314 .fetch_optional(db) 315 .await; 316 317 let public_key_multibase = match key_row { 318 Ok(Some(row)) => match crate::config::decrypt_key(&row.key_bytes, row.encryption_version) { 319 Ok(key_bytes) => crate::api::identity::did::get_public_key_multibase(&key_bytes) 320 .unwrap_or_else(|_| "error".to_string()), 321 Err(_) => "error".to_string(), 322 }, 323 _ => "error".to_string(), 324 }; 325 326 let also_known_as = if let Some(ref ovr) = overrides { 327 if !ovr.also_known_as.is_empty() { 328 ovr.also_known_as.clone() 329 } else { 330 vec![format!("at://{}", user.handle)] 331 } 332 } else { 333 vec![format!("at://{}", user.handle)] 334 }; 335 336 json!({ 337 "@context": [ 338 "https://www.w3.org/ns/did/v1", 339 "https://w3id.org/security/multikey/v1", 340 "https://w3id.org/security/suites/secp256k1-2019/v1" 341 ], 342 "id": did, 343 "alsoKnownAs": also_known_as, 344 "verificationMethod": [{ 345 "id": format!("{}#atproto", did), 346 "type": "Multikey", 347 "controller": did, 348 "publicKeyMultibase": public_key_multibase 349 }], 350 "service": [{ 351 "id": "#atproto_pds", 352 "type": "AtprotoPersonalDataServer", 353 "serviceEndpoint": service_endpoint 354 }] 355 }) 356}