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}