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 encrypted_key_bytes = match crate::config::encrypt_key(&secret_key_bytes) {
232 Ok(enc) => enc,
233 Err(e) => {
234 error!("Error encrypting user key: {:?}", e);
235 return (
236 StatusCode::INTERNAL_SERVER_ERROR,
237 Json(json!({"error": "InternalError"})),
238 )
239 .into_response();
240 }
241 };
242
243 let key_insert = sqlx::query!(
244 "INSERT INTO user_keys (user_id, key_bytes, encryption_version, encrypted_at) VALUES ($1, $2, $3, NOW())",
245 user_id,
246 &encrypted_key_bytes[..],
247 crate::config::ENCRYPTION_VERSION
248 )
249 .execute(&mut *tx)
250 .await;
251
252 if let Err(e) = key_insert {
253 error!("Error inserting user key: {:?}", e);
254 return (
255 StatusCode::INTERNAL_SERVER_ERROR,
256 Json(json!({"error": "InternalError"})),
257 )
258 .into_response();
259 }
260
261 if let Some(key_id) = reserved_key_id {
262 let mark_used = sqlx::query!(
263 "UPDATE reserved_signing_keys SET used_at = NOW() WHERE id = $1",
264 key_id
265 )
266 .execute(&mut *tx)
267 .await;
268
269 if let Err(e) = mark_used {
270 error!("Error marking reserved key as used: {:?}", e);
271 return (
272 StatusCode::INTERNAL_SERVER_ERROR,
273 Json(json!({"error": "InternalError"})),
274 )
275 .into_response();
276 }
277 }
278
279 let mst = Mst::new(Arc::new(state.block_store.clone()));
280 let mst_root = match mst.persist().await {
281 Ok(c) => c,
282 Err(e) => {
283 error!("Error persisting MST: {:?}", e);
284 return (
285 StatusCode::INTERNAL_SERVER_ERROR,
286 Json(json!({"error": "InternalError"})),
287 )
288 .into_response();
289 }
290 };
291
292 let did_obj = match Did::new(&did) {
293 Ok(d) => d,
294 Err(_) => {
295 return (
296 StatusCode::INTERNAL_SERVER_ERROR,
297 Json(json!({"error": "InternalError", "message": "Invalid DID"})),
298 )
299 .into_response();
300 }
301 };
302
303 let rev = Tid::now(LimitedU32::MIN);
304
305 let commit = Commit::new_unsigned(did_obj, mst_root, rev, None);
306
307 let commit_bytes = match commit.to_cbor() {
308 Ok(b) => b,
309 Err(e) => {
310 error!("Error serializing genesis commit: {:?}", e);
311 return (
312 StatusCode::INTERNAL_SERVER_ERROR,
313 Json(json!({"error": "InternalError"})),
314 )
315 .into_response();
316 }
317 };
318
319 let commit_cid = match state.block_store.put(&commit_bytes).await {
320 Ok(c) => c,
321 Err(e) => {
322 error!("Error saving genesis commit: {:?}", e);
323 return (
324 StatusCode::INTERNAL_SERVER_ERROR,
325 Json(json!({"error": "InternalError"})),
326 )
327 .into_response();
328 }
329 };
330
331 let commit_cid_str = commit_cid.to_string();
332 let repo_insert = sqlx::query!("INSERT INTO repos (user_id, repo_root_cid) VALUES ($1, $2)", user_id, commit_cid_str)
333 .execute(&mut *tx)
334 .await;
335
336 if let Err(e) = repo_insert {
337 error!("Error initializing repo: {:?}", e);
338 return (
339 StatusCode::INTERNAL_SERVER_ERROR,
340 Json(json!({"error": "InternalError"})),
341 )
342 .into_response();
343 }
344
345 if let Some(code) = &input.invite_code {
346 let use_insert =
347 sqlx::query!("INSERT INTO invite_code_uses (code, used_by_user) VALUES ($1, $2)", code, user_id)
348 .execute(&mut *tx)
349 .await;
350
351 if let Err(e) = use_insert {
352 error!("Error recording invite usage: {:?}", e);
353 return (
354 StatusCode::INTERNAL_SERVER_ERROR,
355 Json(json!({"error": "InternalError"})),
356 )
357 .into_response();
358 }
359 }
360
361 let access_meta = crate::auth::create_access_token_with_metadata(&did, &secret_key_bytes[..]).map_err(|e| {
362 error!("Error creating access token: {:?}", e);
363 (
364 StatusCode::INTERNAL_SERVER_ERROR,
365 Json(json!({"error": "InternalError"})),
366 )
367 .into_response()
368 });
369 let access_meta = match access_meta {
370 Ok(m) => m,
371 Err(r) => return r,
372 };
373
374 let refresh_meta = crate::auth::create_refresh_token_with_metadata(&did, &secret_key_bytes[..]).map_err(|e| {
375 error!("Error creating refresh token: {:?}", e);
376 (
377 StatusCode::INTERNAL_SERVER_ERROR,
378 Json(json!({"error": "InternalError"})),
379 )
380 .into_response()
381 });
382 let refresh_meta = match refresh_meta {
383 Ok(m) => m,
384 Err(r) => return r,
385 };
386
387 let session_insert =
388 sqlx::query!(
389 "INSERT INTO session_tokens (did, access_jti, refresh_jti, access_expires_at, refresh_expires_at) VALUES ($1, $2, $3, $4, $5)",
390 did,
391 access_meta.jti,
392 refresh_meta.jti,
393 access_meta.expires_at,
394 refresh_meta.expires_at
395 )
396 .execute(&mut *tx)
397 .await;
398
399 if let Err(e) = session_insert {
400 error!("Error inserting session: {:?}", e);
401 return (
402 StatusCode::INTERNAL_SERVER_ERROR,
403 Json(json!({"error": "InternalError"})),
404 )
405 .into_response();
406 }
407
408 if let Err(e) = tx.commit().await {
409 error!("Error committing transaction: {:?}", e);
410 return (
411 StatusCode::INTERNAL_SERVER_ERROR,
412 Json(json!({"error": "InternalError"})),
413 )
414 .into_response();
415 }
416
417 let hostname = std::env::var("PDS_HOSTNAME").unwrap_or_else(|_| "localhost".to_string());
418 if let Err(e) = crate::notifications::enqueue_welcome_email(
419 &state.db,
420 user_id,
421 &input.email,
422 &input.handle,
423 &hostname,
424 )
425 .await
426 {
427 warn!("Failed to enqueue welcome email: {:?}", e);
428 }
429
430 (
431 StatusCode::OK,
432 Json(CreateAccountOutput {
433 access_jwt: access_meta.token,
434 refresh_jwt: refresh_meta.token,
435 handle: input.handle,
436 did,
437 }),
438 )
439 .into_response()
440}