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