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