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