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