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