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