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 sqlx::Row; 17use std::sync::Arc; 18use tracing::{error, info}; 19 20#[derive(Deserialize)] 21pub struct CreateAccountInput { 22 pub handle: String, 23 pub email: String, 24 pub password: String, 25 #[serde(rename = "inviteCode")] 26 pub invite_code: Option<String>, 27 pub did: 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 FROM users WHERE handle = $1") 86 .bind(&input.handle) 87 .fetch_optional(&mut *tx) 88 .await; 89 90 match exists_query { 91 Ok(Some(_)) => { 92 return ( 93 StatusCode::BAD_REQUEST, 94 Json(json!({"error": "HandleTaken", "message": "Handle already taken"})), 95 ) 96 .into_response(); 97 } 98 Err(e) => { 99 error!("Error checking handle: {:?}", e); 100 return ( 101 StatusCode::INTERNAL_SERVER_ERROR, 102 Json(json!({"error": "InternalError"})), 103 ) 104 .into_response(); 105 } 106 Ok(None) => {} 107 } 108 109 if let Some(code) = &input.invite_code { 110 let invite_query = 111 sqlx::query("SELECT available_uses FROM invite_codes WHERE code = $1 FOR UPDATE") 112 .bind(code) 113 .fetch_optional(&mut *tx) 114 .await; 115 116 match invite_query { 117 Ok(Some(row)) => { 118 let uses: i32 = row.get("available_uses"); 119 if uses <= 0 { 120 return (StatusCode::BAD_REQUEST, Json(json!({"error": "InvalidInviteCode", "message": "Invite code exhausted"}))).into_response(); 121 } 122 123 let update_invite = sqlx::query( 124 "UPDATE invite_codes SET available_uses = available_uses - 1 WHERE code = $1", 125 ) 126 .bind(code) 127 .execute(&mut *tx) 128 .await; 129 130 if let Err(e) = update_invite { 131 error!("Error updating invite code: {:?}", e); 132 return ( 133 StatusCode::INTERNAL_SERVER_ERROR, 134 Json(json!({"error": "InternalError"})), 135 ) 136 .into_response(); 137 } 138 } 139 Ok(None) => { 140 return ( 141 StatusCode::BAD_REQUEST, 142 Json(json!({"error": "InvalidInviteCode", "message": "Invite code not found"})), 143 ) 144 .into_response(); 145 } 146 Err(e) => { 147 error!("Error checking invite code: {:?}", e); 148 return ( 149 StatusCode::INTERNAL_SERVER_ERROR, 150 Json(json!({"error": "InternalError"})), 151 ) 152 .into_response(); 153 } 154 } 155 } 156 157 let password_hash = match hash(&input.password, DEFAULT_COST) { 158 Ok(h) => h, 159 Err(e) => { 160 error!("Error hashing password: {:?}", e); 161 return ( 162 StatusCode::INTERNAL_SERVER_ERROR, 163 Json(json!({"error": "InternalError"})), 164 ) 165 .into_response(); 166 } 167 }; 168 169 let user_insert = sqlx::query("INSERT INTO users (handle, email, did, password_hash) VALUES ($1, $2, $3, $4) RETURNING id") 170 .bind(&input.handle) 171 .bind(&input.email) 172 .bind(&did) 173 .bind(&password_hash) 174 .fetch_one(&mut *tx) 175 .await; 176 177 let user_id: uuid::Uuid = match user_insert { 178 Ok(row) => row.get("id"), 179 Err(e) => { 180 error!("Error inserting user: {:?}", e); 181 // TODO: Check for unique constraint violation on email/did specifically 182 return ( 183 StatusCode::INTERNAL_SERVER_ERROR, 184 Json(json!({"error": "InternalError"})), 185 ) 186 .into_response(); 187 } 188 }; 189 190 let secret_key = SecretKey::random(&mut OsRng); 191 let secret_key_bytes = secret_key.to_bytes(); 192 193 let key_insert = sqlx::query("INSERT INTO user_keys (user_id, key_bytes) VALUES ($1, $2)") 194 .bind(user_id) 195 .bind(&secret_key_bytes[..]) 196 .execute(&mut *tx) 197 .await; 198 199 if let Err(e) = key_insert { 200 error!("Error inserting user key: {:?}", e); 201 return ( 202 StatusCode::INTERNAL_SERVER_ERROR, 203 Json(json!({"error": "InternalError"})), 204 ) 205 .into_response(); 206 } 207 208 let mst = Mst::new(Arc::new(state.block_store.clone())); 209 let mst_root = match mst.root().await { 210 Ok(c) => c, 211 Err(e) => { 212 error!("Error creating MST root: {:?}", e); 213 return ( 214 StatusCode::INTERNAL_SERVER_ERROR, 215 Json(json!({"error": "InternalError"})), 216 ) 217 .into_response(); 218 } 219 }; 220 221 let did_obj = match Did::new(&did) { 222 Ok(d) => d, 223 Err(_) => { 224 return ( 225 StatusCode::INTERNAL_SERVER_ERROR, 226 Json(json!({"error": "InternalError", "message": "Invalid DID"})), 227 ) 228 .into_response(); 229 } 230 }; 231 232 let rev = Tid::now(LimitedU32::MIN); 233 234 let commit = Commit::new_unsigned(did_obj, mst_root, rev, None); 235 236 let commit_bytes = match commit.to_cbor() { 237 Ok(b) => b, 238 Err(e) => { 239 error!("Error serializing genesis commit: {:?}", e); 240 return ( 241 StatusCode::INTERNAL_SERVER_ERROR, 242 Json(json!({"error": "InternalError"})), 243 ) 244 .into_response(); 245 } 246 }; 247 248 let commit_cid = match state.block_store.put(&commit_bytes).await { 249 Ok(c) => c, 250 Err(e) => { 251 error!("Error saving genesis commit: {:?}", e); 252 return ( 253 StatusCode::INTERNAL_SERVER_ERROR, 254 Json(json!({"error": "InternalError"})), 255 ) 256 .into_response(); 257 } 258 }; 259 260 let repo_insert = sqlx::query("INSERT INTO repos (user_id, repo_root_cid) VALUES ($1, $2)") 261 .bind(user_id) 262 .bind(commit_cid.to_string()) 263 .execute(&mut *tx) 264 .await; 265 266 if let Err(e) = repo_insert { 267 error!("Error initializing repo: {:?}", e); 268 return ( 269 StatusCode::INTERNAL_SERVER_ERROR, 270 Json(json!({"error": "InternalError"})), 271 ) 272 .into_response(); 273 } 274 275 if let Some(code) = &input.invite_code { 276 let use_insert = 277 sqlx::query("INSERT INTO invite_code_uses (code, used_by_user) VALUES ($1, $2)") 278 .bind(code) 279 .bind(user_id) 280 .execute(&mut *tx) 281 .await; 282 283 if let Err(e) = use_insert { 284 error!("Error recording invite usage: {:?}", e); 285 return ( 286 StatusCode::INTERNAL_SERVER_ERROR, 287 Json(json!({"error": "InternalError"})), 288 ) 289 .into_response(); 290 } 291 } 292 293 let access_jwt = crate::auth::create_access_token(&did, &secret_key_bytes[..]).map_err(|e| { 294 error!("Error creating access token: {:?}", e); 295 ( 296 StatusCode::INTERNAL_SERVER_ERROR, 297 Json(json!({"error": "InternalError"})), 298 ) 299 .into_response() 300 }); 301 let access_jwt = match access_jwt { 302 Ok(t) => t, 303 Err(r) => return r, 304 }; 305 306 let refresh_jwt = crate::auth::create_refresh_token(&did, &secret_key_bytes[..]).map_err(|e| { 307 error!("Error creating refresh token: {:?}", e); 308 ( 309 StatusCode::INTERNAL_SERVER_ERROR, 310 Json(json!({"error": "InternalError"})), 311 ) 312 .into_response() 313 }); 314 let refresh_jwt = match refresh_jwt { 315 Ok(t) => t, 316 Err(r) => return r, 317 }; 318 319 let session_insert = 320 sqlx::query("INSERT INTO sessions (access_jwt, refresh_jwt, did) VALUES ($1, $2, $3)") 321 .bind(&access_jwt) 322 .bind(&refresh_jwt) 323 .bind(&did) 324 .execute(&mut *tx) 325 .await; 326 327 if let Err(e) = session_insert { 328 error!("Error inserting session: {:?}", e); 329 return ( 330 StatusCode::INTERNAL_SERVER_ERROR, 331 Json(json!({"error": "InternalError"})), 332 ) 333 .into_response(); 334 } 335 336 if let Err(e) = tx.commit().await { 337 error!("Error committing transaction: {:?}", e); 338 return ( 339 StatusCode::INTERNAL_SERVER_ERROR, 340 Json(json!({"error": "InternalError"})), 341 ) 342 .into_response(); 343 } 344 345 ( 346 StatusCode::OK, 347 Json(CreateAccountOutput { 348 access_jwt, 349 refresh_jwt, 350 handle: input.handle, 351 did, 352 }), 353 ) 354 .into_response() 355}