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::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 hit: {}", input.handle); 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 let did = if let Some(d) = &input.did { 55 if d.trim().is_empty() { 56 format!("did:plc:{}", uuid::Uuid::new_v4()) 57 } else { 58 let hostname = 59 std::env::var("PDS_HOSTNAME").unwrap_or_else(|_| "localhost".to_string()); 60 if let Err(e) = verify_did_web(d, &hostname, &input.handle).await { 61 return ( 62 StatusCode::BAD_REQUEST, 63 Json(json!({"error": "InvalidDid", "message": e})), 64 ) 65 .into_response(); 66 } 67 d.clone() 68 } 69 } else { 70 format!("did:plc:{}", uuid::Uuid::new_v4()) 71 }; 72 73 let mut tx = match state.db.begin().await { 74 Ok(tx) => tx, 75 Err(e) => { 76 error!("Error starting transaction: {:?}", e); 77 return ( 78 StatusCode::INTERNAL_SERVER_ERROR, 79 Json(json!({"error": "InternalError"})), 80 ) 81 .into_response(); 82 } 83 }; 84 85 let exists_query = sqlx::query!("SELECT 1 as one FROM users WHERE handle = $1", input.handle) 86 .fetch_optional(&mut *tx) 87 .await; 88 89 match exists_query { 90 Ok(Some(_)) => { 91 return ( 92 StatusCode::BAD_REQUEST, 93 Json(json!({"error": "HandleTaken", "message": "Handle already taken"})), 94 ) 95 .into_response(); 96 } 97 Err(e) => { 98 error!("Error checking handle: {:?}", e); 99 return ( 100 StatusCode::INTERNAL_SERVER_ERROR, 101 Json(json!({"error": "InternalError"})), 102 ) 103 .into_response(); 104 } 105 Ok(None) => {} 106 } 107 108 if let Some(code) = &input.invite_code { 109 let invite_query = 110 sqlx::query!("SELECT available_uses FROM invite_codes WHERE code = $1 FOR UPDATE", code) 111 .fetch_optional(&mut *tx) 112 .await; 113 114 match invite_query { 115 Ok(Some(row)) => { 116 if row.available_uses <= 0 { 117 return (StatusCode::BAD_REQUEST, Json(json!({"error": "InvalidInviteCode", "message": "Invite code exhausted"}))).into_response(); 118 } 119 120 let update_invite = sqlx::query!( 121 "UPDATE invite_codes SET available_uses = available_uses - 1 WHERE code = $1", 122 code 123 ) 124 .execute(&mut *tx) 125 .await; 126 127 if let Err(e) = update_invite { 128 error!("Error updating invite code: {:?}", e); 129 return ( 130 StatusCode::INTERNAL_SERVER_ERROR, 131 Json(json!({"error": "InternalError"})), 132 ) 133 .into_response(); 134 } 135 } 136 Ok(None) => { 137 return ( 138 StatusCode::BAD_REQUEST, 139 Json(json!({"error": "InvalidInviteCode", "message": "Invite code not found"})), 140 ) 141 .into_response(); 142 } 143 Err(e) => { 144 error!("Error checking invite code: {:?}", e); 145 return ( 146 StatusCode::INTERNAL_SERVER_ERROR, 147 Json(json!({"error": "InternalError"})), 148 ) 149 .into_response(); 150 } 151 } 152 } 153 154 let password_hash = match hash(&input.password, DEFAULT_COST) { 155 Ok(h) => h, 156 Err(e) => { 157 error!("Error hashing password: {:?}", e); 158 return ( 159 StatusCode::INTERNAL_SERVER_ERROR, 160 Json(json!({"error": "InternalError"})), 161 ) 162 .into_response(); 163 } 164 }; 165 166 let user_insert = sqlx::query!( 167 "INSERT INTO users (handle, email, did, password_hash) VALUES ($1, $2, $3, $4) RETURNING id", 168 input.handle, 169 input.email, 170 did, 171 password_hash 172 ) 173 .fetch_one(&mut *tx) 174 .await; 175 176 let user_id = match user_insert { 177 Ok(row) => row.id, 178 Err(e) => { 179 error!("Error inserting user: {:?}", e); 180 // TODO: Check for unique constraint violation on email/did specifically 181 return ( 182 StatusCode::INTERNAL_SERVER_ERROR, 183 Json(json!({"error": "InternalError"})), 184 ) 185 .into_response(); 186 } 187 }; 188 189 let (secret_key_bytes, reserved_key_id): (Vec<u8>, Option<uuid::Uuid>) = 190 if let Some(signing_key_did) = &input.signing_key { 191 let reserved = sqlx::query!( 192 r#" 193 SELECT id, private_key_bytes 194 FROM reserved_signing_keys 195 WHERE public_key_did_key = $1 196 AND used_at IS NULL 197 AND expires_at > NOW() 198 FOR UPDATE 199 "#, 200 signing_key_did 201 ) 202 .fetch_optional(&mut *tx) 203 .await; 204 205 match reserved { 206 Ok(Some(row)) => (row.private_key_bytes, Some(row.id)), 207 Ok(None) => { 208 return ( 209 StatusCode::BAD_REQUEST, 210 Json(json!({ 211 "error": "InvalidSigningKey", 212 "message": "Signing key not found, already used, or expired" 213 })), 214 ) 215 .into_response(); 216 } 217 Err(e) => { 218 error!("Error looking up reserved signing key: {:?}", e); 219 return ( 220 StatusCode::INTERNAL_SERVER_ERROR, 221 Json(json!({"error": "InternalError"})), 222 ) 223 .into_response(); 224 } 225 } 226 } else { 227 let secret_key = SecretKey::random(&mut OsRng); 228 (secret_key.to_bytes().to_vec(), None) 229 }; 230 231 let key_insert = sqlx::query!( 232 "INSERT INTO user_keys (user_id, key_bytes) VALUES ($1, $2)", 233 user_id, 234 &secret_key_bytes[..] 235 ) 236 .execute(&mut *tx) 237 .await; 238 239 if let Err(e) = key_insert { 240 error!("Error inserting user key: {:?}", e); 241 return ( 242 StatusCode::INTERNAL_SERVER_ERROR, 243 Json(json!({"error": "InternalError"})), 244 ) 245 .into_response(); 246 } 247 248 if let Some(key_id) = reserved_key_id { 249 let mark_used = sqlx::query!( 250 "UPDATE reserved_signing_keys SET used_at = NOW() WHERE id = $1", 251 key_id 252 ) 253 .execute(&mut *tx) 254 .await; 255 256 if let Err(e) = mark_used { 257 error!("Error marking reserved key as used: {:?}", e); 258 return ( 259 StatusCode::INTERNAL_SERVER_ERROR, 260 Json(json!({"error": "InternalError"})), 261 ) 262 .into_response(); 263 } 264 } 265 266 let mst = Mst::new(Arc::new(state.block_store.clone())); 267 let mst_root = match mst.persist().await { 268 Ok(c) => c, 269 Err(e) => { 270 error!("Error persisting MST: {:?}", e); 271 return ( 272 StatusCode::INTERNAL_SERVER_ERROR, 273 Json(json!({"error": "InternalError"})), 274 ) 275 .into_response(); 276 } 277 }; 278 279 let did_obj = match Did::new(&did) { 280 Ok(d) => d, 281 Err(_) => { 282 return ( 283 StatusCode::INTERNAL_SERVER_ERROR, 284 Json(json!({"error": "InternalError", "message": "Invalid DID"})), 285 ) 286 .into_response(); 287 } 288 }; 289 290 let rev = Tid::now(LimitedU32::MIN); 291 292 let commit = Commit::new_unsigned(did_obj, mst_root, rev, None); 293 294 let commit_bytes = match commit.to_cbor() { 295 Ok(b) => b, 296 Err(e) => { 297 error!("Error serializing genesis commit: {:?}", e); 298 return ( 299 StatusCode::INTERNAL_SERVER_ERROR, 300 Json(json!({"error": "InternalError"})), 301 ) 302 .into_response(); 303 } 304 }; 305 306 let commit_cid = match state.block_store.put(&commit_bytes).await { 307 Ok(c) => c, 308 Err(e) => { 309 error!("Error saving genesis commit: {:?}", e); 310 return ( 311 StatusCode::INTERNAL_SERVER_ERROR, 312 Json(json!({"error": "InternalError"})), 313 ) 314 .into_response(); 315 } 316 }; 317 318 let commit_cid_str = commit_cid.to_string(); 319 let repo_insert = sqlx::query!("INSERT INTO repos (user_id, repo_root_cid) VALUES ($1, $2)", user_id, commit_cid_str) 320 .execute(&mut *tx) 321 .await; 322 323 if let Err(e) = repo_insert { 324 error!("Error initializing repo: {:?}", e); 325 return ( 326 StatusCode::INTERNAL_SERVER_ERROR, 327 Json(json!({"error": "InternalError"})), 328 ) 329 .into_response(); 330 } 331 332 if let Some(code) = &input.invite_code { 333 let use_insert = 334 sqlx::query!("INSERT INTO invite_code_uses (code, used_by_user) VALUES ($1, $2)", code, user_id) 335 .execute(&mut *tx) 336 .await; 337 338 if let Err(e) = use_insert { 339 error!("Error recording invite usage: {:?}", e); 340 return ( 341 StatusCode::INTERNAL_SERVER_ERROR, 342 Json(json!({"error": "InternalError"})), 343 ) 344 .into_response(); 345 } 346 } 347 348 let access_jwt = crate::auth::create_access_token(&did, &secret_key_bytes[..]).map_err(|e| { 349 error!("Error creating access token: {:?}", e); 350 ( 351 StatusCode::INTERNAL_SERVER_ERROR, 352 Json(json!({"error": "InternalError"})), 353 ) 354 .into_response() 355 }); 356 let access_jwt = match access_jwt { 357 Ok(t) => t, 358 Err(r) => return r, 359 }; 360 361 let refresh_jwt = crate::auth::create_refresh_token(&did, &secret_key_bytes[..]).map_err(|e| { 362 error!("Error creating refresh token: {:?}", e); 363 ( 364 StatusCode::INTERNAL_SERVER_ERROR, 365 Json(json!({"error": "InternalError"})), 366 ) 367 .into_response() 368 }); 369 let refresh_jwt = match refresh_jwt { 370 Ok(t) => t, 371 Err(r) => return r, 372 }; 373 374 let session_insert = 375 sqlx::query!("INSERT INTO sessions (access_jwt, refresh_jwt, did) VALUES ($1, $2, $3)", access_jwt, refresh_jwt, did) 376 .execute(&mut *tx) 377 .await; 378 379 if let Err(e) = session_insert { 380 error!("Error inserting session: {:?}", e); 381 return ( 382 StatusCode::INTERNAL_SERVER_ERROR, 383 Json(json!({"error": "InternalError"})), 384 ) 385 .into_response(); 386 } 387 388 if let Err(e) = tx.commit().await { 389 error!("Error committing transaction: {:?}", e); 390 return ( 391 StatusCode::INTERNAL_SERVER_ERROR, 392 Json(json!({"error": "InternalError"})), 393 ) 394 .into_response(); 395 } 396 397 let hostname = std::env::var("PDS_HOSTNAME").unwrap_or_else(|_| "localhost".to_string()); 398 if let Err(e) = crate::notifications::enqueue_welcome_email( 399 &state.db, 400 user_id, 401 &input.email, 402 &input.handle, 403 &hostname, 404 ) 405 .await 406 { 407 warn!("Failed to enqueue welcome email: {:?}", e); 408 } 409 410 ( 411 StatusCode::OK, 412 Json(CreateAccountOutput { 413 access_jwt, 414 refresh_jwt, 415 handle: input.handle, 416 did, 417 }), 418 ) 419 .into_response() 420}