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}