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