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