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