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}