this repo has no description
1use super::did::verify_did_web; 2use crate::plc::{create_genesis_operation, signing_key_to_did_key, PlcClient}; 3use crate::state::{AppState, RateLimitKind}; 4use axum::{ 5 Json, 6 extract::State, 7 http::{HeaderMap, StatusCode}, 8 response::{IntoResponse, Response}, 9}; 10use bcrypt::{DEFAULT_COST, hash}; 11use jacquard::types::{did::Did, integer::LimitedU32, string::Tid}; 12use jacquard_repo::{commit::Commit, mst::Mst, storage::BlockStore}; 13use k256::{ecdsa::SigningKey, SecretKey}; 14use rand::rngs::OsRng; 15use serde::{Deserialize, Serialize}; 16use serde_json::json; 17use std::sync::Arc; 18use tracing::{error, info, warn}; 19fn extract_client_ip(headers: &HeaderMap) -> String { 20 if let Some(forwarded) = headers.get("x-forwarded-for") { 21 if let Ok(value) = forwarded.to_str() { 22 if let Some(first_ip) = value.split(',').next() { 23 return first_ip.trim().to_string(); 24 } 25 } 26 } 27 if let Some(real_ip) = headers.get("x-real-ip") { 28 if let Ok(value) = real_ip.to_str() { 29 return value.trim().to_string(); 30 } 31 } 32 "unknown".to_string() 33} 34#[derive(Deserialize)] 35#[serde(rename_all = "camelCase")] 36pub struct CreateAccountInput { 37 pub handle: String, 38 pub email: Option<String>, 39 pub password: String, 40 pub invite_code: Option<String>, 41 pub did: Option<String>, 42 pub signing_key: Option<String>, 43 pub verification_channel: Option<String>, 44 pub discord_id: Option<String>, 45 pub telegram_username: Option<String>, 46 pub signal_number: Option<String>, 47} 48#[derive(Serialize)] 49#[serde(rename_all = "camelCase")] 50pub struct CreateAccountOutput { 51 pub handle: String, 52 pub did: String, 53 pub verification_required: bool, 54 pub verification_channel: String, 55} 56pub async fn create_account( 57 State(state): State<AppState>, 58 headers: HeaderMap, 59 Json(input): Json<CreateAccountInput>, 60) -> Response { 61 info!("create_account called"); 62 let client_ip = extract_client_ip(&headers); 63 if !state.check_rate_limit(RateLimitKind::AccountCreation, &client_ip).await { 64 warn!(ip = %client_ip, "Account creation rate limit exceeded"); 65 return ( 66 StatusCode::TOO_MANY_REQUESTS, 67 Json(json!({ 68 "error": "RateLimitExceeded", 69 "message": "Too many account creation attempts. Please try again later." 70 })), 71 ) 72 .into_response(); 73 } 74 if input.handle.contains('!') || input.handle.contains('@') { 75 return ( 76 StatusCode::BAD_REQUEST, 77 Json( 78 json!({"error": "InvalidHandle", "message": "Handle contains invalid characters"}), 79 ), 80 ) 81 .into_response(); 82 } 83 let email: Option<String> = input.email.as_ref() 84 .map(|e| e.trim().to_string()) 85 .filter(|e| !e.is_empty()); 86 if let Some(ref email) = email { 87 if !crate::api::validation::is_valid_email(email) { 88 return ( 89 StatusCode::BAD_REQUEST, 90 Json(json!({"error": "InvalidEmail", "message": "Invalid email format"})), 91 ) 92 .into_response(); 93 } 94 } 95 let verification_channel = input.verification_channel.as_deref().unwrap_or("email"); 96 let valid_channels = ["email", "discord", "telegram", "signal"]; 97 if !valid_channels.contains(&verification_channel) { 98 return ( 99 StatusCode::BAD_REQUEST, 100 Json(json!({"error": "InvalidVerificationChannel", "message": "Invalid verification channel. Must be one of: email, discord, telegram, signal"})), 101 ) 102 .into_response(); 103 } 104 let verification_recipient = match verification_channel { 105 "email" => match &input.email { 106 Some(email) if !email.trim().is_empty() => email.trim().to_string(), 107 _ => return ( 108 StatusCode::BAD_REQUEST, 109 Json(json!({"error": "MissingEmail", "message": "Email is required when using email verification"})), 110 ).into_response(), 111 }, 112 "discord" => match &input.discord_id { 113 Some(id) if !id.trim().is_empty() => id.trim().to_string(), 114 _ => return ( 115 StatusCode::BAD_REQUEST, 116 Json(json!({"error": "MissingDiscordId", "message": "Discord ID is required when using Discord verification"})), 117 ).into_response(), 118 }, 119 "telegram" => match &input.telegram_username { 120 Some(username) if !username.trim().is_empty() => username.trim().to_string(), 121 _ => return ( 122 StatusCode::BAD_REQUEST, 123 Json(json!({"error": "MissingTelegramUsername", "message": "Telegram username is required when using Telegram verification"})), 124 ).into_response(), 125 }, 126 "signal" => match &input.signal_number { 127 Some(number) if !number.trim().is_empty() => number.trim().to_string(), 128 _ => return ( 129 StatusCode::BAD_REQUEST, 130 Json(json!({"error": "MissingSignalNumber", "message": "Signal phone number is required when using Signal verification"})), 131 ).into_response(), 132 }, 133 _ => return ( 134 StatusCode::BAD_REQUEST, 135 Json(json!({"error": "InvalidVerificationChannel", "message": "Invalid verification channel"})), 136 ).into_response(), 137 }; 138 let hostname = std::env::var("PDS_HOSTNAME").unwrap_or_else(|_| "localhost".to_string()); 139 let pds_endpoint = format!("https://{}", hostname); 140 let suffix = format!(".{}", hostname); 141 let short_handle = if input.handle.ends_with(&suffix) { 142 input.handle.strip_suffix(&suffix).unwrap_or(&input.handle) 143 } else { 144 &input.handle 145 }; 146 let full_handle = format!("{}.{}", short_handle, hostname); 147 let (secret_key_bytes, reserved_key_id): (Vec<u8>, Option<uuid::Uuid>) = 148 if let Some(signing_key_did) = &input.signing_key { 149 let reserved = sqlx::query!( 150 r#" 151 SELECT id, private_key_bytes 152 FROM reserved_signing_keys 153 WHERE public_key_did_key = $1 154 AND used_at IS NULL 155 AND expires_at > NOW() 156 FOR UPDATE 157 "#, 158 signing_key_did 159 ) 160 .fetch_optional(&state.db) 161 .await; 162 match reserved { 163 Ok(Some(row)) => (row.private_key_bytes, Some(row.id)), 164 Ok(None) => { 165 return ( 166 StatusCode::BAD_REQUEST, 167 Json(json!({ 168 "error": "InvalidSigningKey", 169 "message": "Signing key not found, already used, or expired" 170 })), 171 ) 172 .into_response(); 173 } 174 Err(e) => { 175 error!("Error looking up reserved signing key: {:?}", e); 176 return ( 177 StatusCode::INTERNAL_SERVER_ERROR, 178 Json(json!({"error": "InternalError"})), 179 ) 180 .into_response(); 181 } 182 } 183 } else { 184 let secret_key = SecretKey::random(&mut OsRng); 185 (secret_key.to_bytes().to_vec(), None) 186 }; 187 let signing_key = match SigningKey::from_slice(&secret_key_bytes) { 188 Ok(k) => k, 189 Err(e) => { 190 error!("Error creating signing key: {:?}", e); 191 return ( 192 StatusCode::INTERNAL_SERVER_ERROR, 193 Json(json!({"error": "InternalError"})), 194 ) 195 .into_response(); 196 } 197 }; 198 let did = if let Some(d) = &input.did { 199 if d.trim().is_empty() { 200 let rotation_key = std::env::var("PLC_ROTATION_KEY") 201 .unwrap_or_else(|_| signing_key_to_did_key(&signing_key)); 202 let genesis_result = match create_genesis_operation( 203 &signing_key, 204 &rotation_key, 205 &full_handle, 206 &pds_endpoint, 207 ) { 208 Ok(r) => r, 209 Err(e) => { 210 error!("Error creating PLC genesis operation: {:?}", e); 211 return ( 212 StatusCode::INTERNAL_SERVER_ERROR, 213 Json(json!({"error": "InternalError", "message": "Failed to create PLC operation"})), 214 ) 215 .into_response(); 216 } 217 }; 218 let plc_client = PlcClient::new(None); 219 if let Err(e) = plc_client.send_operation(&genesis_result.did, &genesis_result.signed_operation).await { 220 error!("Failed to submit PLC genesis operation: {:?}", e); 221 return ( 222 StatusCode::BAD_GATEWAY, 223 Json(json!({ 224 "error": "UpstreamError", 225 "message": format!("Failed to register DID with PLC directory: {}", e) 226 })), 227 ) 228 .into_response(); 229 } 230 info!(did = %genesis_result.did, "Successfully registered DID with PLC directory"); 231 genesis_result.did 232 } else if d.starts_with("did:web:") { 233 if let Err(e) = verify_did_web(d, &hostname, &input.handle).await { 234 return ( 235 StatusCode::BAD_REQUEST, 236 Json(json!({"error": "InvalidDid", "message": e})), 237 ) 238 .into_response(); 239 } 240 d.clone() 241 } else { 242 return ( 243 StatusCode::BAD_REQUEST, 244 Json(json!({"error": "InvalidDid", "message": "Only did:web DIDs can be provided; leave empty for did:plc"})), 245 ) 246 .into_response(); 247 } 248 } else { 249 let rotation_key = std::env::var("PLC_ROTATION_KEY") 250 .unwrap_or_else(|_| signing_key_to_did_key(&signing_key)); 251 let genesis_result = match create_genesis_operation( 252 &signing_key, 253 &rotation_key, 254 &full_handle, 255 &pds_endpoint, 256 ) { 257 Ok(r) => r, 258 Err(e) => { 259 error!("Error creating PLC genesis operation: {:?}", e); 260 return ( 261 StatusCode::INTERNAL_SERVER_ERROR, 262 Json(json!({"error": "InternalError", "message": "Failed to create PLC operation"})), 263 ) 264 .into_response(); 265 } 266 }; 267 let plc_client = PlcClient::new(None); 268 if let Err(e) = plc_client.send_operation(&genesis_result.did, &genesis_result.signed_operation).await { 269 error!("Failed to submit PLC genesis operation: {:?}", e); 270 return ( 271 StatusCode::BAD_GATEWAY, 272 Json(json!({ 273 "error": "UpstreamError", 274 "message": format!("Failed to register DID with PLC directory: {}", e) 275 })), 276 ) 277 .into_response(); 278 } 279 info!(did = %genesis_result.did, "Successfully registered DID with PLC directory"); 280 genesis_result.did 281 }; 282 let mut tx = match state.db.begin().await { 283 Ok(tx) => tx, 284 Err(e) => { 285 error!("Error starting transaction: {:?}", e); 286 return ( 287 StatusCode::INTERNAL_SERVER_ERROR, 288 Json(json!({"error": "InternalError"})), 289 ) 290 .into_response(); 291 } 292 }; 293 let exists_query = sqlx::query!("SELECT 1 as one FROM users WHERE handle = $1", short_handle) 294 .fetch_optional(&mut *tx) 295 .await; 296 match exists_query { 297 Ok(Some(_)) => { 298 return ( 299 StatusCode::BAD_REQUEST, 300 Json(json!({"error": "HandleTaken", "message": "Handle already taken"})), 301 ) 302 .into_response(); 303 } 304 Err(e) => { 305 error!("Error checking handle: {:?}", e); 306 return ( 307 StatusCode::INTERNAL_SERVER_ERROR, 308 Json(json!({"error": "InternalError"})), 309 ) 310 .into_response(); 311 } 312 Ok(None) => {} 313 } 314 if let Some(code) = &input.invite_code { 315 let invite_query = 316 sqlx::query!("SELECT available_uses FROM invite_codes WHERE code = $1 FOR UPDATE", code) 317 .fetch_optional(&mut *tx) 318 .await; 319 match invite_query { 320 Ok(Some(row)) => { 321 if row.available_uses <= 0 { 322 return (StatusCode::BAD_REQUEST, Json(json!({"error": "InvalidInviteCode", "message": "Invite code exhausted"}))).into_response(); 323 } 324 let update_invite = sqlx::query!( 325 "UPDATE invite_codes SET available_uses = available_uses - 1 WHERE code = $1", 326 code 327 ) 328 .execute(&mut *tx) 329 .await; 330 if let Err(e) = update_invite { 331 error!("Error updating invite code: {:?}", e); 332 return ( 333 StatusCode::INTERNAL_SERVER_ERROR, 334 Json(json!({"error": "InternalError"})), 335 ) 336 .into_response(); 337 } 338 } 339 Ok(None) => { 340 return ( 341 StatusCode::BAD_REQUEST, 342 Json(json!({"error": "InvalidInviteCode", "message": "Invite code not found"})), 343 ) 344 .into_response(); 345 } 346 Err(e) => { 347 error!("Error checking invite code: {:?}", e); 348 return ( 349 StatusCode::INTERNAL_SERVER_ERROR, 350 Json(json!({"error": "InternalError"})), 351 ) 352 .into_response(); 353 } 354 } 355 } 356 let password_hash = match hash(&input.password, DEFAULT_COST) { 357 Ok(h) => h, 358 Err(e) => { 359 error!("Error hashing password: {:?}", e); 360 return ( 361 StatusCode::INTERNAL_SERVER_ERROR, 362 Json(json!({"error": "InternalError"})), 363 ) 364 .into_response(); 365 } 366 }; 367 let verification_code = format!("{:06}", rand::random::<u32>() % 1_000_000); 368 let code_expires_at = chrono::Utc::now() + chrono::Duration::minutes(30); 369 let user_insert: Result<(uuid::Uuid,), _> = sqlx::query_as( 370 r#"INSERT INTO users ( 371 handle, email, did, password_hash, 372 email_confirmation_code, email_confirmation_code_expires_at, 373 preferred_notification_channel, 374 discord_id, telegram_username, signal_number 375 ) VALUES ($1, $2, $3, $4, $5, $6, $7::notification_channel, $8, $9, $10) RETURNING id"#, 376 ) 377 .bind(short_handle) 378 .bind(&email) 379 .bind(&did) 380 .bind(&password_hash) 381 .bind(&verification_code) 382 .bind(&code_expires_at) 383 .bind(verification_channel) 384 .bind(input.discord_id.as_deref().map(|s| s.trim()).filter(|s| !s.is_empty())) 385 .bind(input.telegram_username.as_deref().map(|s| s.trim()).filter(|s| !s.is_empty())) 386 .bind(input.signal_number.as_deref().map(|s| s.trim()).filter(|s| !s.is_empty())) 387 .fetch_one(&mut *tx) 388 .await; 389 let user_id = match user_insert { 390 Ok((id,)) => id, 391 Err(e) => { 392 if let Some(db_err) = e.as_database_error() { 393 if db_err.code().as_deref() == Some("23505") { 394 let constraint = db_err.constraint().unwrap_or(""); 395 if constraint.contains("handle") || constraint.contains("users_handle") { 396 return ( 397 StatusCode::BAD_REQUEST, 398 Json(json!({ 399 "error": "HandleNotAvailable", 400 "message": "Handle already taken" 401 })), 402 ) 403 .into_response(); 404 } else if constraint.contains("email") || constraint.contains("users_email") { 405 return ( 406 StatusCode::BAD_REQUEST, 407 Json(json!({ 408 "error": "InvalidEmail", 409 "message": "Email already registered" 410 })), 411 ) 412 .into_response(); 413 } else if constraint.contains("did") || constraint.contains("users_did") { 414 return ( 415 StatusCode::BAD_REQUEST, 416 Json(json!({ 417 "error": "AccountAlreadyExists", 418 "message": "An account with this DID already exists" 419 })), 420 ) 421 .into_response(); 422 } 423 } 424 } 425 error!("Error inserting user: {:?}", e); 426 return ( 427 StatusCode::INTERNAL_SERVER_ERROR, 428 Json(json!({"error": "InternalError"})), 429 ) 430 .into_response(); 431 } 432 }; 433 let encrypted_key_bytes = match crate::config::encrypt_key(&secret_key_bytes) { 434 Ok(enc) => enc, 435 Err(e) => { 436 error!("Error encrypting user key: {:?}", e); 437 return ( 438 StatusCode::INTERNAL_SERVER_ERROR, 439 Json(json!({"error": "InternalError"})), 440 ) 441 .into_response(); 442 } 443 }; 444 let key_insert = sqlx::query!( 445 "INSERT INTO user_keys (user_id, key_bytes, encryption_version, encrypted_at) VALUES ($1, $2, $3, NOW())", 446 user_id, 447 &encrypted_key_bytes[..], 448 crate::config::ENCRYPTION_VERSION 449 ) 450 .execute(&mut *tx) 451 .await; 452 if let Err(e) = key_insert { 453 error!("Error inserting user key: {:?}", e); 454 return ( 455 StatusCode::INTERNAL_SERVER_ERROR, 456 Json(json!({"error": "InternalError"})), 457 ) 458 .into_response(); 459 } 460 if let Some(key_id) = reserved_key_id { 461 let mark_used = sqlx::query!( 462 "UPDATE reserved_signing_keys SET used_at = NOW() WHERE id = $1", 463 key_id 464 ) 465 .execute(&mut *tx) 466 .await; 467 if let Err(e) = mark_used { 468 error!("Error marking reserved key as used: {:?}", e); 469 return ( 470 StatusCode::INTERNAL_SERVER_ERROR, 471 Json(json!({"error": "InternalError"})), 472 ) 473 .into_response(); 474 } 475 } 476 let mst = Mst::new(Arc::new(state.block_store.clone())); 477 let mst_root = match mst.persist().await { 478 Ok(c) => c, 479 Err(e) => { 480 error!("Error persisting MST: {:?}", e); 481 return ( 482 StatusCode::INTERNAL_SERVER_ERROR, 483 Json(json!({"error": "InternalError"})), 484 ) 485 .into_response(); 486 } 487 }; 488 let did_obj = match Did::new(&did) { 489 Ok(d) => d, 490 Err(_) => { 491 return ( 492 StatusCode::INTERNAL_SERVER_ERROR, 493 Json(json!({"error": "InternalError", "message": "Invalid DID"})), 494 ) 495 .into_response(); 496 } 497 }; 498 let rev = Tid::now(LimitedU32::MIN); 499 let unsigned_commit = Commit::new_unsigned(did_obj, mst_root, rev, None); 500 let signed_commit = match unsigned_commit.sign(&signing_key) { 501 Ok(c) => c, 502 Err(e) => { 503 error!("Error signing genesis commit: {:?}", e); 504 return ( 505 StatusCode::INTERNAL_SERVER_ERROR, 506 Json(json!({"error": "InternalError"})), 507 ) 508 .into_response(); 509 } 510 }; 511 let commit_bytes = match signed_commit.to_cbor() { 512 Ok(b) => b, 513 Err(e) => { 514 error!("Error serializing genesis commit: {:?}", e); 515 return ( 516 StatusCode::INTERNAL_SERVER_ERROR, 517 Json(json!({"error": "InternalError"})), 518 ) 519 .into_response(); 520 } 521 }; 522 let commit_cid = match state.block_store.put(&commit_bytes).await { 523 Ok(c) => c, 524 Err(e) => { 525 error!("Error saving genesis commit: {:?}", e); 526 return ( 527 StatusCode::INTERNAL_SERVER_ERROR, 528 Json(json!({"error": "InternalError"})), 529 ) 530 .into_response(); 531 } 532 }; 533 let commit_cid_str = commit_cid.to_string(); 534 let repo_insert = sqlx::query!("INSERT INTO repos (user_id, repo_root_cid) VALUES ($1, $2)", user_id, commit_cid_str) 535 .execute(&mut *tx) 536 .await; 537 if let Err(e) = repo_insert { 538 error!("Error initializing repo: {:?}", e); 539 return ( 540 StatusCode::INTERNAL_SERVER_ERROR, 541 Json(json!({"error": "InternalError"})), 542 ) 543 .into_response(); 544 } 545 if let Some(code) = &input.invite_code { 546 let use_insert = 547 sqlx::query!("INSERT INTO invite_code_uses (code, used_by_user) VALUES ($1, $2)", code, user_id) 548 .execute(&mut *tx) 549 .await; 550 if let Err(e) = use_insert { 551 error!("Error recording invite usage: {:?}", e); 552 return ( 553 StatusCode::INTERNAL_SERVER_ERROR, 554 Json(json!({"error": "InternalError"})), 555 ) 556 .into_response(); 557 } 558 } 559 if let Err(e) = tx.commit().await { 560 error!("Error committing transaction: {:?}", e); 561 return ( 562 StatusCode::INTERNAL_SERVER_ERROR, 563 Json(json!({"error": "InternalError"})), 564 ) 565 .into_response(); 566 } 567 if let Err(e) = crate::api::repo::record::sequence_identity_event(&state, &did, Some(&full_handle)).await { 568 warn!("Failed to sequence identity event for {}: {}", did, e); 569 } 570 if let Err(e) = crate::api::repo::record::sequence_account_event(&state, &did, true, None).await { 571 warn!("Failed to sequence account event for {}: {}", did, e); 572 } 573 let profile_record = json!({ 574 "$type": "app.bsky.actor.profile", 575 "displayName": input.handle 576 }); 577 if let Err(e) = crate::api::repo::record::create_record_internal( 578 &state, 579 &did, 580 "app.bsky.actor.profile", 581 "self", 582 &profile_record, 583 ).await { 584 warn!("Failed to create default profile for {}: {}", did, e); 585 } 586 if let Err(e) = crate::notifications::enqueue_signup_verification( 587 &state.db, 588 user_id, 589 verification_channel, 590 &verification_recipient, 591 &verification_code, 592 ).await { 593 warn!("Failed to enqueue signup verification notification: {:?}", e); 594 } 595 ( 596 StatusCode::OK, 597 Json(CreateAccountOutput { 598 handle: short_handle.to_string(), 599 did, 600 verification_required: true, 601 verification_channel: verification_channel.to_string(), 602 }), 603 ) 604 .into_response() 605}