this repo has no description
1use crate::api::ApiError; 2use crate::plc::signing_key_to_did_key; 3use crate::state::AppState; 4use axum::{ 5 Json, 6 extract::{Path, Query, State}, 7 http::{HeaderMap, StatusCode}, 8 response::{IntoResponse, Response}, 9}; 10use base64::Engine; 11use k256::SecretKey; 12use k256::elliptic_curve::sec1::ToEncodedPoint; 13use reqwest; 14use serde::Deserialize; 15use serde_json::json; 16use tracing::{error, warn}; 17 18#[derive(Deserialize)] 19pub struct ResolveHandleParams { 20 pub handle: String, 21} 22 23pub async fn resolve_handle( 24 State(state): State<AppState>, 25 Query(params): Query<ResolveHandleParams>, 26) -> Response { 27 let handle = params.handle.trim(); 28 if handle.is_empty() { 29 return ( 30 StatusCode::BAD_REQUEST, 31 Json(json!({"error": "InvalidRequest", "message": "handle is required"})), 32 ) 33 .into_response(); 34 } 35 let cache_key = format!("handle:{}", handle); 36 if let Some(did) = state.cache.get(&cache_key).await { 37 return (StatusCode::OK, Json(json!({ "did": did }))).into_response(); 38 } 39 let hostname = std::env::var("PDS_HOSTNAME").unwrap_or_else(|_| "localhost".to_string()); 40 let suffix = format!(".{}", hostname); 41 let short_handle = if handle.ends_with(&suffix) { 42 handle.strip_suffix(&suffix).unwrap_or(handle) 43 } else { 44 handle 45 }; 46 let user = sqlx::query!("SELECT did FROM users WHERE handle = $1", short_handle) 47 .fetch_optional(&state.db) 48 .await; 49 match user { 50 Ok(Some(row)) => { 51 let _ = state 52 .cache 53 .set(&cache_key, &row.did, std::time::Duration::from_secs(300)) 54 .await; 55 (StatusCode::OK, Json(json!({ "did": row.did }))).into_response() 56 } 57 Ok(None) => match crate::handle::resolve_handle(handle).await { 58 Ok(did) => { 59 let _ = state 60 .cache 61 .set(&cache_key, &did, std::time::Duration::from_secs(300)) 62 .await; 63 (StatusCode::OK, Json(json!({ "did": did }))).into_response() 64 } 65 Err(_) => ( 66 StatusCode::NOT_FOUND, 67 Json(json!({"error": "HandleNotFound", "message": "Unable to resolve handle"})), 68 ) 69 .into_response(), 70 }, 71 Err(e) => { 72 error!("DB error resolving handle: {:?}", e); 73 ( 74 StatusCode::INTERNAL_SERVER_ERROR, 75 Json(json!({"error": "InternalError"})), 76 ) 77 .into_response() 78 } 79 } 80} 81 82pub fn get_jwk(key_bytes: &[u8]) -> Result<serde_json::Value, &'static str> { 83 let secret_key = SecretKey::from_slice(key_bytes).map_err(|_| "Invalid key length")?; 84 let public_key = secret_key.public_key(); 85 let encoded = public_key.to_encoded_point(false); 86 let x = encoded.x().ok_or("Missing x coordinate")?; 87 let y = encoded.y().ok_or("Missing y coordinate")?; 88 let x_b64 = base64::engine::general_purpose::URL_SAFE_NO_PAD.encode(x); 89 let y_b64 = base64::engine::general_purpose::URL_SAFE_NO_PAD.encode(y); 90 Ok(json!({ 91 "kty": "EC", 92 "crv": "secp256k1", 93 "x": x_b64, 94 "y": y_b64 95 })) 96} 97 98pub fn get_public_key_multibase(key_bytes: &[u8]) -> Result<String, &'static str> { 99 let secret_key = SecretKey::from_slice(key_bytes).map_err(|_| "Invalid key length")?; 100 let public_key = secret_key.public_key(); 101 let compressed = public_key.to_encoded_point(true); 102 let compressed_bytes = compressed.as_bytes(); 103 let mut multicodec_key = vec![0xe7, 0x01]; 104 multicodec_key.extend_from_slice(compressed_bytes); 105 Ok(format!("z{}", bs58::encode(&multicodec_key).into_string())) 106} 107 108pub async fn well_known_did(State(state): State<AppState>, headers: HeaderMap) -> Response { 109 let hostname = std::env::var("PDS_HOSTNAME").unwrap_or_else(|_| "localhost".to_string()); 110 let host_header = headers 111 .get("host") 112 .and_then(|h| h.to_str().ok()) 113 .unwrap_or(&hostname); 114 let host_without_port = host_header.split(':').next().unwrap_or(host_header); 115 let hostname_without_port = hostname.split(':').next().unwrap_or(&hostname); 116 if host_without_port != hostname_without_port 117 && host_without_port.ends_with(&format!(".{}", hostname_without_port)) 118 { 119 let handle = host_without_port 120 .strip_suffix(&format!(".{}", hostname_without_port)) 121 .unwrap_or(host_without_port); 122 return serve_subdomain_did_doc(&state, handle, &hostname).await; 123 } 124 let did = if hostname.contains(':') { 125 format!("did:web:{}", hostname.replace(':', "%3A")) 126 } else { 127 format!("did:web:{}", hostname) 128 }; 129 Json(json!({ 130 "@context": ["https://www.w3.org/ns/did/v1"], 131 "id": did, 132 "service": [{ 133 "id": "#atproto_pds", 134 "type": "AtprotoPersonalDataServer", 135 "serviceEndpoint": format!("https://{}", hostname) 136 }] 137 })) 138 .into_response() 139} 140 141async fn serve_subdomain_did_doc(state: &AppState, handle: &str, hostname: &str) -> Response { 142 let user = sqlx::query!( 143 "SELECT id, did, migrated_to_pds FROM users WHERE handle = $1", 144 handle 145 ) 146 .fetch_optional(&state.db) 147 .await; 148 let (user_id, did, migrated_to_pds) = match user { 149 Ok(Some(row)) => (row.id, row.did, row.migrated_to_pds), 150 Ok(None) => { 151 return (StatusCode::NOT_FOUND, Json(json!({"error": "NotFound"}))).into_response(); 152 } 153 Err(e) => { 154 error!("DB Error: {:?}", e); 155 return ( 156 StatusCode::INTERNAL_SERVER_ERROR, 157 Json(json!({"error": "InternalError"})), 158 ) 159 .into_response(); 160 } 161 }; 162 if !did.starts_with("did:web:") { 163 return ( 164 StatusCode::NOT_FOUND, 165 Json(json!({"error": "NotFound", "message": "User is not did:web"})), 166 ) 167 .into_response(); 168 } 169 let subdomain_host = format!("{}.{}", handle, hostname); 170 let encoded_subdomain = subdomain_host.replace(':', "%3A"); 171 let expected_self_hosted = format!("did:web:{}", encoded_subdomain); 172 if did != expected_self_hosted { 173 return ( 174 StatusCode::NOT_FOUND, 175 Json(json!({"error": "NotFound", "message": "External did:web - DID document hosted by user"})), 176 ) 177 .into_response(); 178 } 179 let key_row = sqlx::query!( 180 "SELECT key_bytes, encryption_version FROM user_keys WHERE user_id = $1", 181 user_id 182 ) 183 .fetch_optional(&state.db) 184 .await; 185 let key_bytes: Vec<u8> = match key_row { 186 Ok(Some(row)) => match crate::config::decrypt_key(&row.key_bytes, row.encryption_version) { 187 Ok(k) => k, 188 Err(_) => { 189 return ( 190 StatusCode::INTERNAL_SERVER_ERROR, 191 Json(json!({"error": "InternalError"})), 192 ) 193 .into_response(); 194 } 195 }, 196 _ => { 197 return ( 198 StatusCode::INTERNAL_SERVER_ERROR, 199 Json(json!({"error": "InternalError"})), 200 ) 201 .into_response(); 202 } 203 }; 204 let public_key_multibase = match get_public_key_multibase(&key_bytes) { 205 Ok(pk) => pk, 206 Err(e) => { 207 tracing::error!("Failed to generate public key multibase: {}", e); 208 return ( 209 StatusCode::INTERNAL_SERVER_ERROR, 210 Json(json!({"error": "InternalError"})), 211 ) 212 .into_response(); 213 } 214 }; 215 let full_handle = if handle.contains('.') { 216 handle.to_string() 217 } else { 218 format!("{}.{}", handle, hostname) 219 }; 220 let service_endpoint = migrated_to_pds.unwrap_or_else(|| format!("https://{}", hostname)); 221 Json(json!({ 222 "@context": [ 223 "https://www.w3.org/ns/did/v1", 224 "https://w3id.org/security/multikey/v1", 225 "https://w3id.org/security/suites/secp256k1-2019/v1" 226 ], 227 "id": did, 228 "alsoKnownAs": [format!("at://{}", full_handle)], 229 "verificationMethod": [{ 230 "id": format!("{}#atproto", did), 231 "type": "Multikey", 232 "controller": did, 233 "publicKeyMultibase": public_key_multibase 234 }], 235 "service": [{ 236 "id": "#atproto_pds", 237 "type": "AtprotoPersonalDataServer", 238 "serviceEndpoint": service_endpoint 239 }] 240 })) 241 .into_response() 242} 243 244pub async fn user_did_doc(State(state): State<AppState>, Path(handle): Path<String>) -> Response { 245 let hostname = std::env::var("PDS_HOSTNAME").unwrap_or_else(|_| "localhost".to_string()); 246 let user = sqlx::query!( 247 "SELECT id, did, migrated_to_pds FROM users WHERE handle = $1", 248 handle 249 ) 250 .fetch_optional(&state.db) 251 .await; 252 let (user_id, did, migrated_to_pds) = match user { 253 Ok(Some(row)) => (row.id, row.did, row.migrated_to_pds), 254 Ok(None) => { 255 return (StatusCode::NOT_FOUND, Json(json!({"error": "NotFound"}))).into_response(); 256 } 257 Err(e) => { 258 error!("DB Error: {:?}", e); 259 return ( 260 StatusCode::INTERNAL_SERVER_ERROR, 261 Json(json!({"error": "InternalError"})), 262 ) 263 .into_response(); 264 } 265 }; 266 if !did.starts_with("did:web:") { 267 return ( 268 StatusCode::NOT_FOUND, 269 Json(json!({"error": "NotFound", "message": "User is not did:web"})), 270 ) 271 .into_response(); 272 } 273 let encoded_hostname = hostname.replace(':', "%3A"); 274 let old_path_format = format!("did:web:{}:u:{}", encoded_hostname, handle); 275 let subdomain_host = format!("{}.{}", handle, hostname); 276 let encoded_subdomain = subdomain_host.replace(':', "%3A"); 277 let new_subdomain_format = format!("did:web:{}", encoded_subdomain); 278 if did != old_path_format && did != new_subdomain_format { 279 return ( 280 StatusCode::NOT_FOUND, 281 Json(json!({"error": "NotFound", "message": "External did:web - DID document hosted by user"})), 282 ) 283 .into_response(); 284 } 285 let key_row = sqlx::query!( 286 "SELECT key_bytes, encryption_version FROM user_keys WHERE user_id = $1", 287 user_id 288 ) 289 .fetch_optional(&state.db) 290 .await; 291 let key_bytes: Vec<u8> = match key_row { 292 Ok(Some(row)) => match crate::config::decrypt_key(&row.key_bytes, row.encryption_version) { 293 Ok(k) => k, 294 Err(_) => { 295 return ( 296 StatusCode::INTERNAL_SERVER_ERROR, 297 Json(json!({"error": "InternalError"})), 298 ) 299 .into_response(); 300 } 301 }, 302 _ => { 303 return ( 304 StatusCode::INTERNAL_SERVER_ERROR, 305 Json(json!({"error": "InternalError"})), 306 ) 307 .into_response(); 308 } 309 }; 310 let public_key_multibase = match get_public_key_multibase(&key_bytes) { 311 Ok(pk) => pk, 312 Err(e) => { 313 tracing::error!("Failed to generate public key multibase: {}", e); 314 return ( 315 StatusCode::INTERNAL_SERVER_ERROR, 316 Json(json!({"error": "InternalError"})), 317 ) 318 .into_response(); 319 } 320 }; 321 let full_handle = if handle.contains('.') { 322 handle.clone() 323 } else { 324 format!("{}.{}", handle, hostname) 325 }; 326 let service_endpoint = migrated_to_pds.unwrap_or_else(|| format!("https://{}", hostname)); 327 Json(json!({ 328 "@context": [ 329 "https://www.w3.org/ns/did/v1", 330 "https://w3id.org/security/multikey/v1", 331 "https://w3id.org/security/suites/secp256k1-2019/v1" 332 ], 333 "id": did, 334 "alsoKnownAs": [format!("at://{}", full_handle)], 335 "verificationMethod": [{ 336 "id": format!("{}#atproto", did), 337 "type": "Multikey", 338 "controller": did, 339 "publicKeyMultibase": public_key_multibase 340 }], 341 "service": [{ 342 "id": "#atproto_pds", 343 "type": "AtprotoPersonalDataServer", 344 "serviceEndpoint": service_endpoint 345 }] 346 })) 347 .into_response() 348} 349 350pub async fn verify_did_web(did: &str, hostname: &str, handle: &str) -> Result<(), String> { 351 let subdomain_host = format!("{}.{}", handle, hostname); 352 let encoded_subdomain = subdomain_host.replace(':', "%3A"); 353 let expected_subdomain_did = format!("did:web:{}", encoded_subdomain); 354 if did == expected_subdomain_did { 355 return Ok(()); 356 } 357 let expected_prefix = if hostname.contains(':') { 358 format!("did:web:{}", hostname.replace(':', "%3A")) 359 } else { 360 format!("did:web:{}", hostname) 361 }; 362 if did.starts_with(&expected_prefix) { 363 let suffix = &did[expected_prefix.len()..]; 364 let expected_suffix = format!(":u:{}", handle); 365 if suffix == expected_suffix { 366 return Ok(()); 367 } else { 368 return Err(format!( 369 "Invalid DID path for this PDS. Expected {}", 370 expected_suffix 371 )); 372 } 373 } 374 let parts: Vec<&str> = did.split(':').collect(); 375 if parts.len() < 3 || parts[0] != "did" || parts[1] != "web" { 376 return Err("Invalid did:web format".into()); 377 } 378 let domain_segment = parts[2]; 379 let domain = domain_segment.replace("%3A", ":"); 380 let scheme = if domain.starts_with("localhost") || domain.starts_with("127.0.0.1") { 381 "http" 382 } else { 383 "https" 384 }; 385 let url = if parts.len() == 3 { 386 format!("{}://{}/.well-known/did.json", scheme, domain) 387 } else { 388 let path = parts[3..].join("/"); 389 format!("{}://{}/{}/did.json", scheme, domain, path) 390 }; 391 let client = reqwest::Client::builder() 392 .timeout(std::time::Duration::from_secs(5)) 393 .build() 394 .map_err(|e| format!("Failed to create client: {}", e))?; 395 let resp = client 396 .get(&url) 397 .send() 398 .await 399 .map_err(|e| format!("Failed to fetch DID doc: {}", e))?; 400 if !resp.status().is_success() { 401 return Err(format!("Failed to fetch DID doc: HTTP {}", resp.status())); 402 } 403 let doc: serde_json::Value = resp 404 .json() 405 .await 406 .map_err(|e| format!("Failed to parse DID doc: {}", e))?; 407 let services = doc["service"] 408 .as_array() 409 .ok_or("No services found in DID doc")?; 410 let pds_endpoint = format!("https://{}", hostname); 411 let has_valid_service = services 412 .iter() 413 .any(|s| s["type"] == "AtprotoPersonalDataServer" && s["serviceEndpoint"] == pds_endpoint); 414 if has_valid_service { 415 Ok(()) 416 } else { 417 Err(format!( 418 "DID document does not list this PDS ({}) as AtprotoPersonalDataServer", 419 pds_endpoint 420 )) 421 } 422} 423 424#[derive(serde::Serialize)] 425#[serde(rename_all = "camelCase")] 426pub struct GetRecommendedDidCredentialsOutput { 427 pub rotation_keys: Vec<String>, 428 pub also_known_as: Vec<String>, 429 pub verification_methods: VerificationMethods, 430 pub services: Services, 431} 432 433#[derive(serde::Serialize)] 434#[serde(rename_all = "camelCase")] 435pub struct VerificationMethods { 436 pub atproto: String, 437} 438 439#[derive(serde::Serialize)] 440#[serde(rename_all = "camelCase")] 441pub struct Services { 442 pub atproto_pds: AtprotoPds, 443} 444 445#[derive(serde::Serialize)] 446#[serde(rename_all = "camelCase")] 447pub struct AtprotoPds { 448 #[serde(rename = "type")] 449 pub service_type: String, 450 pub endpoint: String, 451} 452 453pub async fn get_recommended_did_credentials( 454 State(state): State<AppState>, 455 headers: axum::http::HeaderMap, 456) -> Response { 457 let token = match crate::auth::extract_bearer_token_from_header( 458 headers.get("Authorization").and_then(|h| h.to_str().ok()), 459 ) { 460 Some(t) => t, 461 None => { 462 return ( 463 StatusCode::UNAUTHORIZED, 464 Json(json!({"error": "AuthenticationRequired"})), 465 ) 466 .into_response(); 467 } 468 }; 469 let auth_user = 470 match crate::auth::validate_bearer_token_allow_deactivated(&state.db, &token).await { 471 Ok(user) => user, 472 Err(e) => return ApiError::from(e).into_response(), 473 }; 474 let user = match sqlx::query!( 475 "SELECT handle FROM users u JOIN user_keys k ON u.id = k.user_id WHERE u.did = $1", 476 auth_user.did 477 ) 478 .fetch_optional(&state.db) 479 .await 480 { 481 Ok(Some(row)) => row, 482 _ => return ApiError::InternalError.into_response(), 483 }; 484 let key_bytes = match auth_user.key_bytes { 485 Some(kb) => kb, 486 None => { 487 return ApiError::AuthenticationFailedMsg( 488 "OAuth tokens cannot get DID credentials".into(), 489 ) 490 .into_response(); 491 } 492 }; 493 let hostname = std::env::var("PDS_HOSTNAME").unwrap_or_else(|_| "localhost".to_string()); 494 let pds_endpoint = format!("https://{}", hostname); 495 let full_handle = if user.handle.contains('.') { 496 user.handle.clone() 497 } else { 498 format!("{}.{}", user.handle, hostname) 499 }; 500 let signing_key = match k256::ecdsa::SigningKey::from_slice(&key_bytes) { 501 Ok(k) => k, 502 Err(_) => return ApiError::InternalError.into_response(), 503 }; 504 let did_key = signing_key_to_did_key(&signing_key); 505 let rotation_keys = if auth_user.did.starts_with("did:web:") { 506 vec![] 507 } else { 508 vec![did_key.clone()] 509 }; 510 ( 511 StatusCode::OK, 512 Json(GetRecommendedDidCredentialsOutput { 513 rotation_keys, 514 also_known_as: vec![format!("at://{}", full_handle)], 515 verification_methods: VerificationMethods { atproto: did_key }, 516 services: Services { 517 atproto_pds: AtprotoPds { 518 service_type: "AtprotoPersonalDataServer".to_string(), 519 endpoint: pds_endpoint, 520 }, 521 }, 522 }), 523 ) 524 .into_response() 525} 526 527#[derive(Deserialize)] 528pub struct UpdateHandleInput { 529 pub handle: String, 530} 531 532pub async fn update_handle( 533 State(state): State<AppState>, 534 headers: axum::http::HeaderMap, 535 Json(input): Json<UpdateHandleInput>, 536) -> Response { 537 let token = match crate::auth::extract_bearer_token_from_header( 538 headers.get("Authorization").and_then(|h| h.to_str().ok()), 539 ) { 540 Some(t) => t, 541 None => return ApiError::AuthenticationRequired.into_response(), 542 }; 543 let auth_user = 544 match crate::auth::validate_bearer_token_allow_deactivated(&state.db, &token).await { 545 Ok(user) => user, 546 Err(e) => return ApiError::from(e).into_response(), 547 }; 548 if let Err(e) = crate::auth::scope_check::check_identity_scope( 549 auth_user.is_oauth, 550 auth_user.scope.as_deref(), 551 crate::oauth::scopes::IdentityAttr::Handle, 552 ) { 553 return e; 554 } 555 let did = auth_user.did; 556 let user_id = match sqlx::query_scalar!("SELECT id FROM users WHERE did = $1", did) 557 .fetch_optional(&state.db) 558 .await 559 { 560 Ok(Some(id)) => id, 561 _ => return ApiError::InternalError.into_response(), 562 }; 563 let new_handle = input.handle.trim(); 564 if new_handle.is_empty() { 565 return ApiError::InvalidRequest("handle is required".into()).into_response(); 566 } 567 if !new_handle 568 .chars() 569 .all(|c| c.is_ascii_alphanumeric() || c == '.' || c == '-' || c == '_') 570 { 571 return ( 572 StatusCode::BAD_REQUEST, 573 Json( 574 json!({"error": "InvalidHandle", "message": "Handle contains invalid characters"}), 575 ), 576 ) 577 .into_response(); 578 } 579 let hostname = std::env::var("PDS_HOSTNAME").unwrap_or_else(|_| "localhost".to_string()); 580 let is_service_domain = crate::handle::is_service_domain_handle(new_handle, &hostname); 581 let (handle_to_store, full_handle) = if is_service_domain { 582 let suffix = format!(".{}", hostname); 583 let short_handle = if new_handle.ends_with(&suffix) { 584 new_handle.strip_suffix(&suffix).unwrap_or(new_handle) 585 } else { 586 new_handle 587 }; 588 ( 589 short_handle.to_string(), 590 format!("{}.{}", short_handle, hostname), 591 ) 592 } else { 593 match crate::handle::verify_handle_ownership(new_handle, &did).await { 594 Ok(()) => {} 595 Err(crate::handle::HandleResolutionError::NotFound) => { 596 return ( 597 StatusCode::BAD_REQUEST, 598 Json(json!({ 599 "error": "HandleNotAvailable", 600 "message": "Handle verification failed. Please set up DNS TXT record at _atproto.{} or serve your DID at https://{}/.well-known/atproto-did", 601 "handle": new_handle 602 })), 603 ) 604 .into_response(); 605 } 606 Err(crate::handle::HandleResolutionError::DidMismatch { expected, actual }) => { 607 return ( 608 StatusCode::BAD_REQUEST, 609 Json(json!({ 610 "error": "HandleNotAvailable", 611 "message": format!("Handle points to different DID. Expected {}, got {}", expected, actual) 612 })), 613 ) 614 .into_response(); 615 } 616 Err(e) => { 617 warn!("Handle verification failed: {}", e); 618 return ( 619 StatusCode::BAD_REQUEST, 620 Json(json!({ 621 "error": "HandleNotAvailable", 622 "message": format!("Handle verification failed: {}", e) 623 })), 624 ) 625 .into_response(); 626 } 627 } 628 (new_handle.to_string(), new_handle.to_string()) 629 }; 630 let old_handle = sqlx::query_scalar!("SELECT handle FROM users WHERE id = $1", user_id) 631 .fetch_optional(&state.db) 632 .await 633 .ok() 634 .flatten(); 635 let existing = sqlx::query!( 636 "SELECT id FROM users WHERE handle = $1 AND id != $2", 637 handle_to_store, 638 user_id 639 ) 640 .fetch_optional(&state.db) 641 .await; 642 if let Ok(Some(_)) = existing { 643 return ( 644 StatusCode::BAD_REQUEST, 645 Json(json!({"error": "HandleTaken", "message": "Handle is already in use"})), 646 ) 647 .into_response(); 648 } 649 let result = sqlx::query!( 650 "UPDATE users SET handle = $1 WHERE id = $2", 651 handle_to_store, 652 user_id 653 ) 654 .execute(&state.db) 655 .await; 656 match result { 657 Ok(_) => { 658 if let Some(old) = old_handle { 659 let _ = state.cache.delete(&format!("handle:{}", old)).await; 660 } 661 let _ = state 662 .cache 663 .delete(&format!("handle:{}", handle_to_store)) 664 .await; 665 let _ = state.cache.delete(&format!("handle:{}", full_handle)).await; 666 if let Err(e) = 667 crate::api::repo::record::sequence_identity_event(&state, &did, Some(&full_handle)) 668 .await 669 { 670 warn!("Failed to sequence identity event for handle update: {}", e); 671 } 672 if let Err(e) = update_plc_handle(&state, &did, &full_handle).await { 673 warn!("Failed to update PLC handle: {}", e); 674 } 675 (StatusCode::OK, Json(json!({}))).into_response() 676 } 677 Err(e) => { 678 error!("DB error updating handle: {:?}", e); 679 ( 680 StatusCode::INTERNAL_SERVER_ERROR, 681 Json(json!({"error": "InternalError"})), 682 ) 683 .into_response() 684 } 685 } 686} 687 688async fn update_plc_handle( 689 state: &AppState, 690 did: &str, 691 new_handle: &str, 692) -> Result<(), Box<dyn std::error::Error + Send + Sync>> { 693 if !did.starts_with("did:plc:") { 694 return Ok(()); 695 } 696 let user_row = sqlx::query!( 697 r#"SELECT u.id, uk.key_bytes, uk.encryption_version 698 FROM users u 699 JOIN user_keys uk ON u.id = uk.user_id 700 WHERE u.did = $1"#, 701 did 702 ) 703 .fetch_optional(&state.db) 704 .await?; 705 let user_row = match user_row { 706 Some(r) => r, 707 None => return Ok(()), 708 }; 709 let key_bytes = crate::config::decrypt_key(&user_row.key_bytes, user_row.encryption_version)?; 710 let signing_key = k256::ecdsa::SigningKey::from_slice(&key_bytes)?; 711 let plc_client = crate::plc::PlcClient::new(None); 712 let last_op = plc_client.get_last_op(did).await?; 713 let new_also_known_as = vec![format!("at://{}", new_handle)]; 714 let update_op = 715 crate::plc::create_update_op(&last_op, None, None, Some(new_also_known_as), None)?; 716 let signed_op = crate::plc::sign_operation(&update_op, &signing_key)?; 717 plc_client.send_operation(did, &signed_op).await?; 718 Ok(()) 719} 720 721pub async fn well_known_atproto_did(State(state): State<AppState>, headers: HeaderMap) -> Response { 722 let host = match headers.get("host").and_then(|h| h.to_str().ok()) { 723 Some(h) => h, 724 None => return (StatusCode::BAD_REQUEST, "Missing host header").into_response(), 725 }; 726 let hostname = std::env::var("PDS_HOSTNAME").unwrap_or_else(|_| "localhost".to_string()); 727 let suffix = format!(".{}", hostname); 728 let handle = host.split(':').next().unwrap_or(host); 729 let short_handle = if handle.ends_with(&suffix) { 730 handle.strip_suffix(&suffix).unwrap_or(handle) 731 } else { 732 return (StatusCode::NOT_FOUND, "Handle not found").into_response(); 733 }; 734 let user = sqlx::query!("SELECT did FROM users WHERE handle = $1", short_handle) 735 .fetch_optional(&state.db) 736 .await; 737 match user { 738 Ok(Some(row)) => row.did.into_response(), 739 Ok(None) => (StatusCode::NOT_FOUND, "Handle not found").into_response(), 740 Err(e) => { 741 error!("DB error in well-known atproto-did: {:?}", e); 742 (StatusCode::INTERNAL_SERVER_ERROR, "Internal error").into_response() 743 } 744 } 745}