this repo has no description
1use axum::{
2 extract::State,
3 Json,
4 response::{IntoResponse, Response},
5 http::StatusCode,
6};
7use serde::{Deserialize, Serialize};
8use serde_json::json;
9use crate::state::AppState;
10use sqlx::Row;
11use bcrypt::{hash, verify, DEFAULT_COST};
12use tracing::{info, error, warn};
13use jacquard_repo::{mst::Mst, commit::Commit, storage::BlockStore};
14use jacquard::types::{string::Tid, did::Did, integer::LimitedU32};
15use std::sync::Arc;
16use k256::SecretKey;
17use rand::rngs::OsRng;
18
19pub async fn describe_server() -> impl IntoResponse {
20 let domains_str = std::env::var("AVAILABLE_USER_DOMAINS").unwrap_or_else(|_| "example.com".to_string());
21 let domains: Vec<&str> = domains_str.split(',').map(|s| s.trim()).collect();
22
23 Json(json!({
24 "availableUserDomains": domains
25 }))
26}
27
28pub async fn health(State(state): State<AppState>) -> impl IntoResponse {
29 match sqlx::query("SELECT 1").execute(&state.db).await {
30 Ok(_) => (StatusCode::OK, "OK"),
31 Err(e) => {
32 error!("Health check failed: {:?}", e);
33 (StatusCode::SERVICE_UNAVAILABLE, "Service Unavailable")
34 }
35 }
36}
37
38#[derive(Deserialize)]
39pub struct CreateAccountInput {
40 pub handle: String,
41 pub email: String,
42 pub password: String,
43 #[serde(rename = "inviteCode")]
44 pub invite_code: Option<String>,
45}
46
47#[derive(Serialize)]
48#[serde(rename_all = "camelCase")]
49pub struct CreateAccountOutput {
50 pub access_jwt: String,
51 pub refresh_jwt: String,
52 pub handle: String,
53 pub did: String,
54}
55
56pub async fn create_account(
57 State(state): State<AppState>,
58 Json(input): Json<CreateAccountInput>,
59) -> Response {
60 info!("create_account hit: {}", input.handle);
61 if input.handle.contains('!') || input.handle.contains('@') {
62 return (StatusCode::BAD_REQUEST, Json(json!({"error": "InvalidHandle", "message": "Handle contains invalid characters"}))).into_response();
63 }
64
65 let mut tx = match state.db.begin().await {
66 Ok(tx) => tx,
67 Err(e) => {
68 error!("Error starting transaction: {:?}", e);
69 return (StatusCode::INTERNAL_SERVER_ERROR, Json(json!({"error": "InternalError"}))).into_response();
70 }
71 };
72
73 let exists_query = sqlx::query("SELECT 1 FROM users WHERE handle = $1")
74 .bind(&input.handle)
75 .fetch_optional(&mut *tx)
76 .await;
77
78 match exists_query {
79 Ok(Some(_)) => return (StatusCode::BAD_REQUEST, Json(json!({"error": "HandleTaken", "message": "Handle already taken"}))).into_response(),
80 Err(e) => {
81 error!("Error checking handle: {:?}", e);
82 return (StatusCode::INTERNAL_SERVER_ERROR, Json(json!({"error": "InternalError"}))).into_response();
83 }
84 Ok(None) => {}
85 }
86
87 if let Some(code) = &input.invite_code {
88 let invite_query = sqlx::query("SELECT available_uses FROM invite_codes WHERE code = $1 FOR UPDATE")
89 .bind(code)
90 .fetch_optional(&mut *tx)
91 .await;
92
93 match invite_query {
94 Ok(Some(row)) => {
95 let uses: i32 = row.get("available_uses");
96 if uses <= 0 {
97 return (StatusCode::BAD_REQUEST, Json(json!({"error": "InvalidInviteCode", "message": "Invite code exhausted"}))).into_response();
98 }
99
100 let update_invite = sqlx::query("UPDATE invite_codes SET available_uses = available_uses - 1 WHERE code = $1")
101 .bind(code)
102 .execute(&mut *tx)
103 .await;
104
105 if let Err(e) = update_invite {
106 error!("Error updating invite code: {:?}", e);
107 return (StatusCode::INTERNAL_SERVER_ERROR, Json(json!({"error": "InternalError"}))).into_response();
108 }
109 },
110 Ok(None) => return (StatusCode::BAD_REQUEST, Json(json!({"error": "InvalidInviteCode", "message": "Invite code not found"}))).into_response(),
111 Err(e) => {
112 error!("Error checking invite code: {:?}", e);
113 return (StatusCode::INTERNAL_SERVER_ERROR, Json(json!({"error": "InternalError"}))).into_response();
114 }
115 }
116 }
117
118 let did = format!("did:plc:{}", uuid::Uuid::new_v4());
119
120 let password_hash = match hash(&input.password, DEFAULT_COST) {
121 Ok(h) => h,
122 Err(e) => {
123 error!("Error hashing password: {:?}", e);
124 return (StatusCode::INTERNAL_SERVER_ERROR, Json(json!({"error": "InternalError"}))).into_response();
125 }
126 };
127
128 let user_insert = sqlx::query("INSERT INTO users (handle, email, did, password_hash) VALUES ($1, $2, $3, $4) RETURNING id")
129 .bind(&input.handle)
130 .bind(&input.email)
131 .bind(&did)
132 .bind(&password_hash)
133 .fetch_one(&mut *tx)
134 .await;
135
136 let user_id: uuid::Uuid = match user_insert {
137 Ok(row) => row.get("id"),
138 Err(e) => {
139 error!("Error inserting user: {:?}", e);
140 return (StatusCode::INTERNAL_SERVER_ERROR, Json(json!({"error": "InternalError"}))).into_response();
141 }
142 };
143
144 let secret_key = SecretKey::random(&mut OsRng);
145 let secret_key_bytes = secret_key.to_bytes();
146
147 let key_insert = sqlx::query("INSERT INTO user_keys (user_id, key_bytes) VALUES ($1, $2)")
148 .bind(user_id)
149 .bind(&secret_key_bytes[..])
150 .execute(&mut *tx)
151 .await;
152
153 if let Err(e) = key_insert {
154 error!("Error inserting user key: {:?}", e);
155 return (StatusCode::INTERNAL_SERVER_ERROR, Json(json!({"error": "InternalError"}))).into_response();
156 }
157
158 let store = Arc::new(state.block_store.clone());
159 let mst = Mst::new(store.clone());
160 let mst_root = match mst.root().await {
161 Ok(c) => c,
162 Err(e) => {
163 error!("Error creating MST root: {:?}", e);
164 return (StatusCode::INTERNAL_SERVER_ERROR, Json(json!({"error": "InternalError"}))).into_response();
165 }
166 };
167
168 let did_obj = match Did::new(&did) {
169 Ok(d) => d,
170 Err(_) => return (StatusCode::INTERNAL_SERVER_ERROR, Json(json!({"error": "InternalError", "message": "Invalid DID"}))).into_response(),
171 };
172
173 let rev = Tid::now(LimitedU32::MIN);
174
175 let commit = Commit::new_unsigned(
176 did_obj,
177 mst_root,
178 rev,
179 None
180 );
181
182 let commit_bytes = match commit.to_cbor() {
183 Ok(b) => b,
184 Err(e) => {
185 error!("Error serializing genesis commit: {:?}", e);
186 return (StatusCode::INTERNAL_SERVER_ERROR, Json(json!({"error": "InternalError"}))).into_response();
187 }
188 };
189
190 let commit_cid = match state.block_store.put(&commit_bytes).await {
191 Ok(c) => c,
192 Err(e) => {
193 error!("Error saving genesis commit: {:?}", e);
194 return (StatusCode::INTERNAL_SERVER_ERROR, Json(json!({"error": "InternalError"}))).into_response();
195 }
196 };
197
198 let repo_insert = sqlx::query("INSERT INTO repos (user_id, repo_root_cid) VALUES ($1, $2)")
199 .bind(user_id)
200 .bind(commit_cid.to_string())
201 .execute(&mut *tx)
202 .await;
203
204 if let Err(e) = repo_insert {
205 error!("Error initializing repo: {:?}", e);
206 return (StatusCode::INTERNAL_SERVER_ERROR, Json(json!({"error": "InternalError"}))).into_response();
207 }
208
209 if let Some(code) = &input.invite_code {
210 let use_insert = sqlx::query("INSERT INTO invite_code_uses (code, used_by_user) VALUES ($1, $2)")
211 .bind(code)
212 .bind(user_id)
213 .execute(&mut *tx)
214 .await;
215
216 if let Err(e) = use_insert {
217 error!("Error recording invite usage: {:?}", e);
218 return (StatusCode::INTERNAL_SERVER_ERROR, Json(json!({"error": "InternalError"}))).into_response();
219 }
220 }
221
222 let access_jwt = crate::auth::create_access_token(&did, &secret_key_bytes[..]).map_err(|e| {
223 error!("Error creating access token: {:?}", e);
224 (StatusCode::INTERNAL_SERVER_ERROR, Json(json!({"error": "InternalError"}))).into_response()
225 });
226 let access_jwt = match access_jwt {
227 Ok(t) => t,
228 Err(r) => return r,
229 };
230
231 let refresh_jwt = crate::auth::create_refresh_token(&did, &secret_key_bytes[..]).map_err(|e| {
232 error!("Error creating refresh token: {:?}", e);
233 (StatusCode::INTERNAL_SERVER_ERROR, Json(json!({"error": "InternalError"}))).into_response()
234 });
235 let refresh_jwt = match refresh_jwt {
236 Ok(t) => t,
237 Err(r) => return r,
238 };
239
240 let session_insert = sqlx::query("INSERT INTO sessions (access_jwt, refresh_jwt, did) VALUES ($1, $2, $3)")
241 .bind(&access_jwt)
242 .bind(&refresh_jwt)
243 .bind(&did)
244 .execute(&mut *tx)
245 .await;
246
247 if let Err(e) = session_insert {
248 error!("Error inserting session: {:?}", e);
249 return (StatusCode::INTERNAL_SERVER_ERROR, Json(json!({"error": "InternalError"}))).into_response();
250 }
251
252 if let Err(e) = tx.commit().await {
253 error!("Error committing transaction: {:?}", e);
254 return (StatusCode::INTERNAL_SERVER_ERROR, Json(json!({"error": "InternalError"}))).into_response();
255 }
256
257 (StatusCode::OK, Json(CreateAccountOutput {
258 access_jwt,
259 refresh_jwt,
260 handle: input.handle,
261 did,
262 })).into_response()
263}
264
265#[derive(Deserialize)]
266pub struct CreateSessionInput {
267 pub identifier: String,
268 pub password: String,
269}
270
271#[derive(Serialize)]
272#[serde(rename_all = "camelCase")]
273pub struct CreateSessionOutput {
274 pub access_jwt: String,
275 pub refresh_jwt: String,
276 pub handle: String,
277 pub did: String,
278}
279
280pub async fn create_session(
281 State(state): State<AppState>,
282 Json(input): Json<CreateSessionInput>,
283) -> Response {
284 info!("create_session: identifier='{}'", input.identifier);
285
286 let user_row = sqlx::query("SELECT u.did, u.handle, u.password_hash, k.key_bytes FROM users u JOIN user_keys k ON u.id = k.user_id WHERE u.handle = $1 OR u.email = $1")
287 .bind(&input.identifier)
288 .fetch_optional(&state.db)
289 .await;
290
291 match user_row {
292 Ok(Some(row)) => {
293 let stored_hash: String = row.get("password_hash");
294
295 if verify(&input.password, &stored_hash).unwrap_or(false) {
296 let did: String = row.get("did");
297 let handle: String = row.get("handle");
298 let key_bytes: Vec<u8> = row.get("key_bytes");
299
300 let access_jwt = match crate::auth::create_access_token(&did, &key_bytes) {
301 Ok(t) => t,
302 Err(e) => {
303 error!("Failed to create access token: {:?}", e);
304 return (StatusCode::INTERNAL_SERVER_ERROR, Json(json!({"error": "InternalError"}))).into_response();
305 }
306 };
307
308 let refresh_jwt = match crate::auth::create_refresh_token(&did, &key_bytes) {
309 Ok(t) => t,
310 Err(e) => {
311 error!("Failed to create refresh token: {:?}", e);
312 return (StatusCode::INTERNAL_SERVER_ERROR, Json(json!({"error": "InternalError"}))).into_response();
313 }
314 };
315
316 let session_insert = sqlx::query("INSERT INTO sessions (access_jwt, refresh_jwt, did) VALUES ($1, $2, $3)")
317 .bind(&access_jwt)
318 .bind(&refresh_jwt)
319 .bind(&did)
320 .execute(&state.db)
321 .await;
322
323 match session_insert {
324 Ok(_) => {
325 return (StatusCode::OK, Json(CreateSessionOutput {
326 access_jwt,
327 refresh_jwt,
328 handle,
329 did,
330 })).into_response();
331 },
332 Err(e) => {
333 error!("Failed to insert session: {:?}", e);
334 return (StatusCode::INTERNAL_SERVER_ERROR, Json(json!({"error": "InternalError"}))).into_response();
335 }
336 }
337 } else {
338 warn!("Password verification failed for identifier: {}", input.identifier);
339 }
340 },
341 Ok(None) => {
342 warn!("User not found for identifier: {}", input.identifier);
343 },
344 Err(e) => {
345 error!("Database error fetching user: {:?}", e);
346 return (StatusCode::INTERNAL_SERVER_ERROR, Json(json!({"error": "InternalError"}))).into_response();
347 }
348 }
349
350 (StatusCode::UNAUTHORIZED, Json(json!({"error": "AuthenticationFailed", "message": "Invalid identifier or password"}))).into_response()
351}
352
353pub async fn get_session(
354 State(state): State<AppState>,
355 headers: axum::http::HeaderMap,
356) -> Response {
357 let auth_header = headers.get("Authorization");
358 if auth_header.is_none() {
359 return (StatusCode::UNAUTHORIZED, Json(json!({"error": "AuthenticationRequired"}))).into_response();
360 }
361
362 let token = auth_header.unwrap().to_str().unwrap_or("").replace("Bearer ", "");
363
364 let result = sqlx::query(
365 r#"
366 SELECT u.handle, u.did, u.email, k.key_bytes
367 FROM sessions s
368 JOIN users u ON s.did = u.did
369 JOIN user_keys k ON u.id = k.user_id
370 WHERE s.access_jwt = $1
371 "#
372 )
373 .bind(&token)
374 .fetch_optional(&state.db)
375 .await;
376
377 match result {
378 Ok(Some(row)) => {
379 let handle: String = row.get("handle");
380 let did: String = row.get("did");
381 let email: String = row.get("email");
382 let key_bytes: Vec<u8> = row.get("key_bytes");
383
384 if let Err(_) = crate::auth::verify_token(&token, &key_bytes) {
385 return (StatusCode::UNAUTHORIZED, Json(json!({"error": "AuthenticationFailed", "message": "Invalid token signature"}))).into_response();
386 }
387
388 return (StatusCode::OK, Json(json!({
389 "handle": handle,
390 "did": did,
391 "email": email,
392 "didDoc": {}
393 }))).into_response();
394 },
395 Ok(None) => {
396 return (StatusCode::UNAUTHORIZED, Json(json!({"error": "AuthenticationFailed"}))).into_response();
397 },
398 Err(e) => {
399 error!("Database error in get_session: {:?}", e);
400 return (StatusCode::INTERNAL_SERVER_ERROR, Json(json!({"error": "InternalError"}))).into_response();
401 }
402 }
403}
404
405pub async fn delete_session(
406 State(state): State<AppState>,
407 headers: axum::http::HeaderMap,
408) -> Response {
409 let auth_header = headers.get("Authorization");
410 if auth_header.is_none() {
411 return (StatusCode::UNAUTHORIZED, Json(json!({"error": "AuthenticationRequired"}))).into_response();
412 }
413
414 let token = auth_header.unwrap().to_str().unwrap_or("").replace("Bearer ", "");
415
416 let result = sqlx::query("DELETE FROM sessions WHERE access_jwt = $1")
417 .bind(token)
418 .execute(&state.db)
419 .await;
420
421 match result {
422 Ok(res) => {
423 if res.rows_affected() > 0 {
424 return (StatusCode::OK, Json(json!({}))).into_response();
425 }
426 },
427 Err(e) => {
428 error!("Database error in delete_session: {:?}", e);
429 }
430 }
431
432 (StatusCode::UNAUTHORIZED, Json(json!({"error": "AuthenticationFailed"}))).into_response()
433}
434
435pub async fn refresh_session(
436 State(state): State<AppState>,
437 headers: axum::http::HeaderMap,
438) -> Response {
439 let auth_header = headers.get("Authorization");
440 if auth_header.is_none() {
441 return (StatusCode::UNAUTHORIZED, Json(json!({"error": "AuthenticationRequired"}))).into_response();
442 }
443
444 let refresh_token = auth_header.unwrap().to_str().unwrap_or("").replace("Bearer ", "");
445
446 let session = sqlx::query(
447 "SELECT s.did, k.key_bytes FROM sessions s JOIN users u ON s.did = u.did JOIN user_keys k ON u.id = k.user_id WHERE s.refresh_jwt = $1"
448 )
449 .bind(&refresh_token)
450 .fetch_optional(&state.db)
451 .await;
452
453 match session {
454 Ok(Some(session_row)) => {
455 let did: String = session_row.get("did");
456 let key_bytes: Vec<u8> = session_row.get("key_bytes");
457
458 if let Err(_) = crate::auth::verify_token(&refresh_token, &key_bytes) {
459 return (StatusCode::UNAUTHORIZED, Json(json!({"error": "AuthenticationFailed", "message": "Invalid refresh token signature"}))).into_response();
460 }
461
462 let new_access_jwt = match crate::auth::create_access_token(&did, &key_bytes) {
463 Ok(t) => t,
464 Err(e) => {
465 error!("Failed to create access token: {:?}", e);
466 return (StatusCode::INTERNAL_SERVER_ERROR, Json(json!({"error": "InternalError"}))).into_response();
467 }
468 };
469 let new_refresh_jwt = match crate::auth::create_refresh_token(&did, &key_bytes) {
470 Ok(t) => t,
471 Err(e) => {
472 error!("Failed to create refresh token: {:?}", e);
473 return (StatusCode::INTERNAL_SERVER_ERROR, Json(json!({"error": "InternalError"}))).into_response();
474 }
475 };
476
477 let update = sqlx::query("UPDATE sessions SET access_jwt = $1, refresh_jwt = $2 WHERE refresh_jwt = $3")
478 .bind(&new_access_jwt)
479 .bind(&new_refresh_jwt)
480 .bind(&refresh_token)
481 .execute(&state.db)
482 .await;
483
484 match update {
485 Ok(_) => {
486 let user = sqlx::query("SELECT handle FROM users WHERE did = $1")
487 .bind(&did)
488 .fetch_optional(&state.db)
489 .await;
490
491 match user {
492 Ok(Some(u)) => {
493 let handle: String = u.get("handle");
494 return (StatusCode::OK, Json(json!({
495 "accessJwt": new_access_jwt,
496 "refreshJwt": new_refresh_jwt,
497 "handle": handle,
498 "did": did
499 }))).into_response();
500 },
501 Ok(None) => {
502 error!("User not found for existing session: {}", did);
503 return (StatusCode::INTERNAL_SERVER_ERROR, Json(json!({"error": "InternalError"}))).into_response();
504 },
505 Err(e) => {
506 error!("Database error fetching user: {:?}", e);
507 return (StatusCode::INTERNAL_SERVER_ERROR, Json(json!({"error": "InternalError"}))).into_response();
508 }
509 }
510 },
511 Err(e) => {
512 error!("Database error updating session: {:?}", e);
513 return (StatusCode::INTERNAL_SERVER_ERROR, Json(json!({"error": "InternalError"}))).into_response();
514 }
515 }
516 },
517 Ok(None) => {
518 return (StatusCode::UNAUTHORIZED, Json(json!({"error": "AuthenticationFailed", "message": "Invalid refresh token"}))).into_response();
519 },
520 Err(e) => {
521 error!("Database error fetching session: {:?}", e);
522 return (StatusCode::INTERNAL_SERVER_ERROR, Json(json!({"error": "InternalError"}))).into_response();
523 }
524 }
525}