this repo has no description
1use crate::state::AppState;
2use axum::{
3 Json,
4 extract::{Query, State},
5 http::StatusCode,
6 response::{IntoResponse, Response},
7};
8use bcrypt::verify;
9use chrono::{Duration, Utc};
10use uuid::Uuid;
11use serde::{Deserialize, Serialize};
12use serde_json::json;
13use tracing::{error, info, warn};
14
15#[derive(Deserialize)]
16pub struct GetServiceAuthParams {
17 pub aud: String,
18 pub lxm: Option<String>,
19 pub exp: Option<i64>,
20}
21
22#[derive(Serialize)]
23pub struct GetServiceAuthOutput {
24 pub token: String,
25}
26
27pub async fn get_service_auth(
28 State(state): State<AppState>,
29 headers: axum::http::HeaderMap,
30 Query(params): Query<GetServiceAuthParams>,
31) -> Response {
32 let auth_header = headers.get("Authorization");
33 if auth_header.is_none() {
34 return (
35 StatusCode::UNAUTHORIZED,
36 Json(json!({"error": "AuthenticationRequired"})),
37 )
38 .into_response();
39 }
40
41 let token = auth_header
42 .unwrap()
43 .to_str()
44 .unwrap_or("")
45 .replace("Bearer ", "");
46
47 let session = sqlx::query!(
48 r#"
49 SELECT s.did, k.key_bytes
50 FROM sessions s
51 JOIN users u ON s.did = u.did
52 JOIN user_keys k ON u.id = k.user_id
53 WHERE s.access_jwt = $1
54 "#,
55 token
56 )
57 .fetch_optional(&state.db)
58 .await;
59
60 let (did, key_bytes) = match session {
61 Ok(Some(row)) => (row.did, row.key_bytes),
62 Ok(None) => {
63 return (
64 StatusCode::UNAUTHORIZED,
65 Json(json!({"error": "AuthenticationFailed"})),
66 )
67 .into_response();
68 }
69 Err(e) => {
70 error!("DB error in get_service_auth: {:?}", e);
71 return (
72 StatusCode::INTERNAL_SERVER_ERROR,
73 Json(json!({"error": "InternalError"})),
74 )
75 .into_response();
76 }
77 };
78
79 if let Err(_) = crate::auth::verify_token(&token, &key_bytes) {
80 return (
81 StatusCode::UNAUTHORIZED,
82 Json(json!({"error": "AuthenticationFailed", "message": "Invalid token signature"})),
83 )
84 .into_response();
85 }
86
87 let lxm = params.lxm.as_deref().unwrap_or("*");
88
89 let service_token = match crate::auth::create_service_token(&did, ¶ms.aud, lxm, &key_bytes)
90 {
91 Ok(t) => t,
92 Err(e) => {
93 error!("Failed to create service token: {:?}", e);
94 return (
95 StatusCode::INTERNAL_SERVER_ERROR,
96 Json(json!({"error": "InternalError"})),
97 )
98 .into_response();
99 }
100 };
101
102 (StatusCode::OK, Json(GetServiceAuthOutput { token: service_token })).into_response()
103}
104
105#[derive(Deserialize)]
106pub struct CreateSessionInput {
107 pub identifier: String,
108 pub password: String,
109}
110
111#[derive(Serialize)]
112#[serde(rename_all = "camelCase")]
113pub struct CreateSessionOutput {
114 pub access_jwt: String,
115 pub refresh_jwt: String,
116 pub handle: String,
117 pub did: String,
118}
119
120pub async fn create_session(
121 State(state): State<AppState>,
122 Json(input): Json<CreateSessionInput>,
123) -> Response {
124 info!("create_session: identifier='{}'", input.identifier);
125
126 let user_row = sqlx::query!(
127 "SELECT u.id, 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",
128 input.identifier
129 )
130 .fetch_optional(&state.db)
131 .await;
132
133 match user_row {
134 Ok(Some(row)) => {
135 let user_id = row.id;
136 let stored_hash = &row.password_hash;
137 let did = &row.did;
138 let handle = &row.handle;
139 let key_bytes = &row.key_bytes;
140
141 let password_valid = if verify(&input.password, stored_hash).unwrap_or(false) {
142 true
143 } else {
144 let app_pass_rows = sqlx::query!("SELECT password_hash FROM app_passwords WHERE user_id = $1", user_id)
145 .fetch_all(&state.db)
146 .await
147 .unwrap_or_default();
148
149 app_pass_rows.iter().any(|row| {
150 verify(&input.password, &row.password_hash).unwrap_or(false)
151 })
152 };
153
154 if password_valid {
155 let access_jwt = match crate::auth::create_access_token(&did, &key_bytes) {
156 Ok(t) => t,
157 Err(e) => {
158 error!("Failed to create access token: {:?}", e);
159 return (
160 StatusCode::INTERNAL_SERVER_ERROR,
161 Json(json!({"error": "InternalError"})),
162 )
163 .into_response();
164 }
165 };
166
167 let refresh_jwt = match crate::auth::create_refresh_token(&did, &key_bytes) {
168 Ok(t) => t,
169 Err(e) => {
170 error!("Failed to create refresh token: {:?}", e);
171 return (
172 StatusCode::INTERNAL_SERVER_ERROR,
173 Json(json!({"error": "InternalError"})),
174 )
175 .into_response();
176 }
177 };
178
179 let session_insert = sqlx::query!(
180 "INSERT INTO sessions (access_jwt, refresh_jwt, did) VALUES ($1, $2, $3)",
181 access_jwt,
182 refresh_jwt,
183 did
184 )
185 .execute(&state.db)
186 .await;
187
188 match session_insert {
189 Ok(_) => {
190 return (
191 StatusCode::OK,
192 Json(CreateSessionOutput {
193 access_jwt,
194 refresh_jwt,
195 handle: handle.clone(),
196 did: did.clone(),
197 }),
198 )
199 .into_response();
200 }
201 Err(e) => {
202 error!("Failed to insert session: {:?}", e);
203 return (
204 StatusCode::INTERNAL_SERVER_ERROR,
205 Json(json!({"error": "InternalError"})),
206 )
207 .into_response();
208 }
209 }
210 } else {
211 warn!(
212 "Password verification failed for identifier: {}",
213 input.identifier
214 );
215 }
216 }
217 Ok(None) => {
218 warn!("User not found for identifier: {}", input.identifier);
219 }
220 Err(e) => {
221 error!("Database error fetching user: {:?}", e);
222 return (
223 StatusCode::INTERNAL_SERVER_ERROR,
224 Json(json!({"error": "InternalError"})),
225 )
226 .into_response();
227 }
228 }
229
230 (
231 StatusCode::UNAUTHORIZED,
232 Json(json!({"error": "AuthenticationFailed", "message": "Invalid identifier or password"})),
233 )
234 .into_response()
235}
236
237pub async fn get_session(
238 State(state): State<AppState>,
239 headers: axum::http::HeaderMap,
240) -> Response {
241 let auth_header = headers.get("Authorization");
242 if auth_header.is_none() {
243 return (
244 StatusCode::UNAUTHORIZED,
245 Json(json!({"error": "AuthenticationRequired"})),
246 )
247 .into_response();
248 }
249
250 let token = auth_header
251 .unwrap()
252 .to_str()
253 .unwrap_or("")
254 .replace("Bearer ", "");
255
256 let result = sqlx::query!(
257 r#"
258 SELECT u.handle, u.did, u.email, k.key_bytes
259 FROM sessions s
260 JOIN users u ON s.did = u.did
261 JOIN user_keys k ON u.id = k.user_id
262 WHERE s.access_jwt = $1
263 "#,
264 token
265 )
266 .fetch_optional(&state.db)
267 .await;
268
269 match result {
270 Ok(Some(row)) => {
271 if let Err(_) = crate::auth::verify_token(&token, &row.key_bytes) {
272 return (StatusCode::UNAUTHORIZED, Json(json!({"error": "AuthenticationFailed", "message": "Invalid token signature"}))).into_response();
273 }
274
275 return (
276 StatusCode::OK,
277 Json(json!({
278 "handle": row.handle,
279 "did": row.did,
280 "email": row.email,
281 "didDoc": {}
282 })),
283 )
284 .into_response();
285 }
286 Ok(None) => {
287 return (
288 StatusCode::UNAUTHORIZED,
289 Json(json!({"error": "AuthenticationFailed"})),
290 )
291 .into_response();
292 }
293 Err(e) => {
294 error!("Database error in get_session: {:?}", e);
295 return (
296 StatusCode::INTERNAL_SERVER_ERROR,
297 Json(json!({"error": "InternalError"})),
298 )
299 .into_response();
300 }
301 }
302}
303
304pub async fn delete_session(
305 State(state): State<AppState>,
306 headers: axum::http::HeaderMap,
307) -> Response {
308 let auth_header = headers.get("Authorization");
309 if auth_header.is_none() {
310 return (
311 StatusCode::UNAUTHORIZED,
312 Json(json!({"error": "AuthenticationRequired"})),
313 )
314 .into_response();
315 }
316
317 let token = auth_header
318 .unwrap()
319 .to_str()
320 .unwrap_or("")
321 .replace("Bearer ", "");
322
323 let result = sqlx::query!("DELETE FROM sessions WHERE access_jwt = $1", token)
324 .execute(&state.db)
325 .await;
326
327 match result {
328 Ok(res) => {
329 if res.rows_affected() > 0 {
330 return (StatusCode::OK, Json(json!({}))).into_response();
331 }
332 }
333 Err(e) => {
334 error!("Database error in delete_session: {:?}", e);
335 }
336 }
337
338 (
339 StatusCode::UNAUTHORIZED,
340 Json(json!({"error": "AuthenticationFailed"})),
341 )
342 .into_response()
343}
344
345pub async fn request_account_delete(
346 State(state): State<AppState>,
347 headers: axum::http::HeaderMap,
348) -> Response {
349 let auth_header = headers.get("Authorization");
350 if auth_header.is_none() {
351 return (
352 StatusCode::UNAUTHORIZED,
353 Json(json!({"error": "AuthenticationRequired"})),
354 )
355 .into_response();
356 }
357
358 let token = auth_header
359 .unwrap()
360 .to_str()
361 .unwrap_or("")
362 .replace("Bearer ", "");
363
364 let session = sqlx::query!(
365 r#"
366 SELECT s.did, u.id as user_id, u.email, u.handle, 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 token
373 )
374 .fetch_optional(&state.db)
375 .await;
376
377 let (did, user_id, email, handle, key_bytes) = match session {
378 Ok(Some(row)) => (row.did, row.user_id, row.email, row.handle, row.key_bytes),
379 Ok(None) => {
380 return (
381 StatusCode::UNAUTHORIZED,
382 Json(json!({"error": "AuthenticationFailed"})),
383 )
384 .into_response();
385 }
386 Err(e) => {
387 error!("DB error in request_account_delete: {:?}", e);
388 return (
389 StatusCode::INTERNAL_SERVER_ERROR,
390 Json(json!({"error": "InternalError"})),
391 )
392 .into_response();
393 }
394 };
395
396 if let Err(_) = crate::auth::verify_token(&token, &key_bytes) {
397 return (
398 StatusCode::UNAUTHORIZED,
399 Json(json!({"error": "AuthenticationFailed", "message": "Invalid token signature"})),
400 )
401 .into_response();
402 }
403
404 let confirmation_token = Uuid::new_v4().to_string();
405 let expires_at = Utc::now() + Duration::minutes(15);
406
407 let insert = sqlx::query!(
408 "INSERT INTO account_deletion_requests (token, did, expires_at) VALUES ($1, $2, $3)",
409 confirmation_token,
410 did,
411 expires_at
412 )
413 .execute(&state.db)
414 .await;
415
416 if let Err(e) = insert {
417 error!("DB error creating deletion token: {:?}", e);
418 return (
419 StatusCode::INTERNAL_SERVER_ERROR,
420 Json(json!({"error": "InternalError"})),
421 )
422 .into_response();
423 }
424
425 let hostname = std::env::var("PDS_HOSTNAME").unwrap_or_else(|_| "localhost".to_string());
426 if let Err(e) = crate::notifications::enqueue_account_deletion(
427 &state.db,
428 user_id,
429 &email,
430 &handle,
431 &confirmation_token,
432 &hostname,
433 )
434 .await
435 {
436 warn!("Failed to enqueue account deletion notification: {:?}", e);
437 }
438
439 info!("Account deletion requested for user {}", did);
440
441 (StatusCode::OK, Json(json!({}))).into_response()
442}
443
444pub async fn refresh_session(
445 State(state): State<AppState>,
446 headers: axum::http::HeaderMap,
447) -> Response {
448 let auth_header = headers.get("Authorization");
449 if auth_header.is_none() {
450 return (
451 StatusCode::UNAUTHORIZED,
452 Json(json!({"error": "AuthenticationRequired"})),
453 )
454 .into_response();
455 }
456
457 let refresh_token = auth_header
458 .unwrap()
459 .to_str()
460 .unwrap_or("")
461 .replace("Bearer ", "");
462
463 let session = sqlx::query!(
464 "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",
465 refresh_token
466 )
467 .fetch_optional(&state.db)
468 .await;
469
470 match session {
471 Ok(Some(session_row)) => {
472 let did = &session_row.did;
473 let key_bytes = &session_row.key_bytes;
474
475 if let Err(_) = crate::auth::verify_token(&refresh_token, &key_bytes) {
476 return (StatusCode::UNAUTHORIZED, Json(json!({"error": "AuthenticationFailed", "message": "Invalid refresh token signature"}))).into_response();
477 }
478
479 let new_access_jwt = match crate::auth::create_access_token(&did, &key_bytes) {
480 Ok(t) => t,
481 Err(e) => {
482 error!("Failed to create access token: {:?}", e);
483 return (
484 StatusCode::INTERNAL_SERVER_ERROR,
485 Json(json!({"error": "InternalError"})),
486 )
487 .into_response();
488 }
489 };
490 let new_refresh_jwt = match crate::auth::create_refresh_token(&did, &key_bytes) {
491 Ok(t) => t,
492 Err(e) => {
493 error!("Failed to create refresh token: {:?}", e);
494 return (
495 StatusCode::INTERNAL_SERVER_ERROR,
496 Json(json!({"error": "InternalError"})),
497 )
498 .into_response();
499 }
500 };
501
502 let update = sqlx::query!(
503 "UPDATE sessions SET access_jwt = $1, refresh_jwt = $2 WHERE refresh_jwt = $3",
504 new_access_jwt,
505 new_refresh_jwt,
506 refresh_token
507 )
508 .execute(&state.db)
509 .await;
510
511 match update {
512 Ok(_) => {
513 let user = sqlx::query!("SELECT handle FROM users WHERE did = $1", did)
514 .fetch_optional(&state.db)
515 .await;
516
517 match user {
518 Ok(Some(u)) => {
519 return (
520 StatusCode::OK,
521 Json(json!({
522 "accessJwt": new_access_jwt,
523 "refreshJwt": new_refresh_jwt,
524 "handle": u.handle,
525 "did": did
526 })),
527 )
528 .into_response();
529 }
530 Ok(None) => {
531 error!("User not found for existing session: {}", did);
532 return (
533 StatusCode::INTERNAL_SERVER_ERROR,
534 Json(json!({"error": "InternalError"})),
535 )
536 .into_response();
537 }
538 Err(e) => {
539 error!("Database error fetching user: {:?}", e);
540 return (
541 StatusCode::INTERNAL_SERVER_ERROR,
542 Json(json!({"error": "InternalError"})),
543 )
544 .into_response();
545 }
546 }
547 }
548 Err(e) => {
549 error!("Database error updating session: {:?}", e);
550 return (
551 StatusCode::INTERNAL_SERVER_ERROR,
552 Json(json!({"error": "InternalError"})),
553 )
554 .into_response();
555 }
556 }
557 }
558 Ok(None) => {
559 return (
560 StatusCode::UNAUTHORIZED,
561 Json(json!({"error": "AuthenticationFailed", "message": "Invalid refresh token"})),
562 )
563 .into_response();
564 }
565 Err(e) => {
566 error!("Database error fetching session: {:?}", e);
567 return (
568 StatusCode::INTERNAL_SERVER_ERROR,
569 Json(json!({"error": "InternalError"})),
570 )
571 .into_response();
572 }
573 }
574}
575
576#[derive(Serialize)]
577#[serde(rename_all = "camelCase")]
578pub struct CheckAccountStatusOutput {
579 pub activated: bool,
580 pub valid_did: bool,
581 pub repo_commit: String,
582 pub repo_rev: String,
583 pub repo_blocks: i64,
584 pub indexed_records: i64,
585 pub private_state_values: i64,
586 pub expected_blobs: i64,
587 pub imported_blobs: i64,
588}
589
590pub async fn check_account_status(
591 State(state): State<AppState>,
592 headers: axum::http::HeaderMap,
593) -> Response {
594 let auth_header = headers.get("Authorization");
595 if auth_header.is_none() {
596 return (
597 StatusCode::UNAUTHORIZED,
598 Json(json!({"error": "AuthenticationRequired"})),
599 )
600 .into_response();
601 }
602
603 let token = auth_header
604 .unwrap()
605 .to_str()
606 .unwrap_or("")
607 .replace("Bearer ", "");
608
609 let session = sqlx::query!(
610 r#"
611 SELECT s.did, k.key_bytes, u.id as user_id
612 FROM sessions s
613 JOIN users u ON s.did = u.did
614 JOIN user_keys k ON u.id = k.user_id
615 WHERE s.access_jwt = $1
616 "#,
617 token
618 )
619 .fetch_optional(&state.db)
620 .await;
621
622 let (did, key_bytes, user_id) = match session {
623 Ok(Some(row)) => (row.did, row.key_bytes, row.user_id),
624 Ok(None) => {
625 return (
626 StatusCode::UNAUTHORIZED,
627 Json(json!({"error": "AuthenticationFailed"})),
628 )
629 .into_response();
630 }
631 Err(e) => {
632 error!("DB error in check_account_status: {:?}", e);
633 return (
634 StatusCode::INTERNAL_SERVER_ERROR,
635 Json(json!({"error": "InternalError"})),
636 )
637 .into_response();
638 }
639 };
640
641 if let Err(_) = crate::auth::verify_token(&token, &key_bytes) {
642 return (
643 StatusCode::UNAUTHORIZED,
644 Json(json!({"error": "AuthenticationFailed", "message": "Invalid token signature"})),
645 )
646 .into_response();
647 }
648
649 let user_status = sqlx::query!("SELECT deactivated_at FROM users WHERE did = $1", did)
650 .fetch_optional(&state.db)
651 .await;
652
653 let deactivated_at = match user_status {
654 Ok(Some(row)) => row.deactivated_at,
655 _ => None,
656 };
657
658 let repo_result = sqlx::query!("SELECT repo_root_cid FROM repos WHERE user_id = $1", user_id)
659 .fetch_optional(&state.db)
660 .await;
661
662 let repo_commit = match repo_result {
663 Ok(Some(row)) => row.repo_root_cid,
664 _ => String::new(),
665 };
666
667 let record_count: i64 = sqlx::query_scalar!("SELECT COUNT(*) FROM records WHERE repo_id = $1", user_id)
668 .fetch_one(&state.db)
669 .await
670 .unwrap_or(Some(0))
671 .unwrap_or(0);
672
673 let blob_count: i64 =
674 sqlx::query_scalar!("SELECT COUNT(*) FROM blobs WHERE created_by_user = $1", user_id)
675 .fetch_one(&state.db)
676 .await
677 .unwrap_or(Some(0))
678 .unwrap_or(0);
679
680 let valid_did = did.starts_with("did:");
681
682 (
683 StatusCode::OK,
684 Json(CheckAccountStatusOutput {
685 activated: deactivated_at.is_none(),
686 valid_did,
687 repo_commit: repo_commit.clone(),
688 repo_rev: chrono::Utc::now().timestamp_millis().to_string(),
689 repo_blocks: 0,
690 indexed_records: record_count,
691 private_state_values: 0,
692 expected_blobs: blob_count,
693 imported_blobs: blob_count,
694 }),
695 )
696 .into_response()
697}
698
699pub async fn activate_account(
700 State(state): State<AppState>,
701 headers: axum::http::HeaderMap,
702) -> Response {
703 let auth_header = headers.get("Authorization");
704 if auth_header.is_none() {
705 return (
706 StatusCode::UNAUTHORIZED,
707 Json(json!({"error": "AuthenticationRequired"})),
708 )
709 .into_response();
710 }
711
712 let token = auth_header
713 .unwrap()
714 .to_str()
715 .unwrap_or("")
716 .replace("Bearer ", "");
717
718 let session = sqlx::query!(
719 r#"
720 SELECT s.did, k.key_bytes
721 FROM sessions s
722 JOIN users u ON s.did = u.did
723 JOIN user_keys k ON u.id = k.user_id
724 WHERE s.access_jwt = $1
725 "#,
726 token
727 )
728 .fetch_optional(&state.db)
729 .await;
730
731 let (did, key_bytes) = match session {
732 Ok(Some(row)) => (row.did, row.key_bytes),
733 Ok(None) => {
734 return (
735 StatusCode::UNAUTHORIZED,
736 Json(json!({"error": "AuthenticationFailed"})),
737 )
738 .into_response();
739 }
740 Err(e) => {
741 error!("DB error in activate_account: {:?}", e);
742 return (
743 StatusCode::INTERNAL_SERVER_ERROR,
744 Json(json!({"error": "InternalError"})),
745 )
746 .into_response();
747 }
748 };
749
750 if let Err(_) = crate::auth::verify_token(&token, &key_bytes) {
751 return (
752 StatusCode::UNAUTHORIZED,
753 Json(json!({"error": "AuthenticationFailed", "message": "Invalid token signature"})),
754 )
755 .into_response();
756 }
757
758 let result = sqlx::query!("UPDATE users SET deactivated_at = NULL WHERE did = $1", did)
759 .execute(&state.db)
760 .await;
761
762 match result {
763 Ok(_) => (StatusCode::OK, Json(json!({}))).into_response(),
764 Err(e) => {
765 error!("DB error activating account: {:?}", e);
766 (
767 StatusCode::INTERNAL_SERVER_ERROR,
768 Json(json!({"error": "InternalError"})),
769 )
770 .into_response()
771 }
772 }
773}
774
775#[derive(Deserialize)]
776#[serde(rename_all = "camelCase")]
777pub struct DeactivateAccountInput {
778 pub delete_after: Option<String>,
779}
780
781pub async fn deactivate_account(
782 State(state): State<AppState>,
783 headers: axum::http::HeaderMap,
784 Json(_input): Json<DeactivateAccountInput>,
785) -> Response {
786 let auth_header = headers.get("Authorization");
787 if auth_header.is_none() {
788 return (
789 StatusCode::UNAUTHORIZED,
790 Json(json!({"error": "AuthenticationRequired"})),
791 )
792 .into_response();
793 }
794
795 let token = auth_header
796 .unwrap()
797 .to_str()
798 .unwrap_or("")
799 .replace("Bearer ", "");
800
801 let session = sqlx::query!(
802 r#"
803 SELECT s.did, k.key_bytes
804 FROM sessions s
805 JOIN users u ON s.did = u.did
806 JOIN user_keys k ON u.id = k.user_id
807 WHERE s.access_jwt = $1
808 "#,
809 token
810 )
811 .fetch_optional(&state.db)
812 .await;
813
814 let (did, key_bytes) = match session {
815 Ok(Some(row)) => (row.did, row.key_bytes),
816 Ok(None) => {
817 return (
818 StatusCode::UNAUTHORIZED,
819 Json(json!({"error": "AuthenticationFailed"})),
820 )
821 .into_response();
822 }
823 Err(e) => {
824 error!("DB error in deactivate_account: {:?}", e);
825 return (
826 StatusCode::INTERNAL_SERVER_ERROR,
827 Json(json!({"error": "InternalError"})),
828 )
829 .into_response();
830 }
831 };
832
833 if let Err(_) = crate::auth::verify_token(&token, &key_bytes) {
834 return (
835 StatusCode::UNAUTHORIZED,
836 Json(json!({"error": "AuthenticationFailed", "message": "Invalid token signature"})),
837 )
838 .into_response();
839 }
840
841 let result = sqlx::query!("UPDATE users SET deactivated_at = NOW() WHERE did = $1", did)
842 .execute(&state.db)
843 .await;
844
845 match result {
846 Ok(_) => (StatusCode::OK, Json(json!({}))).into_response(),
847 Err(e) => {
848 error!("DB error deactivating account: {:?}", e);
849 (
850 StatusCode::INTERNAL_SERVER_ERROR,
851 Json(json!({"error": "InternalError"})),
852 )
853 .into_response()
854 }
855 }
856}
857
858#[derive(Serialize)]
859#[serde(rename_all = "camelCase")]
860pub struct AppPassword {
861 pub name: String,
862 pub created_at: String,
863 pub privileged: bool,
864}
865
866#[derive(Serialize)]
867pub struct ListAppPasswordsOutput {
868 pub passwords: Vec<AppPassword>,
869}
870
871pub async fn list_app_passwords(
872 State(state): State<AppState>,
873 headers: axum::http::HeaderMap,
874) -> Response {
875 let auth_header = headers.get("Authorization");
876 if auth_header.is_none() {
877 return (
878 StatusCode::UNAUTHORIZED,
879 Json(json!({"error": "AuthenticationRequired"})),
880 )
881 .into_response();
882 }
883
884 let token = auth_header
885 .unwrap()
886 .to_str()
887 .unwrap_or("")
888 .replace("Bearer ", "");
889
890 let session = sqlx::query!(
891 r#"
892 SELECT s.did, k.key_bytes, u.id as user_id
893 FROM sessions s
894 JOIN users u ON s.did = u.did
895 JOIN user_keys k ON u.id = k.user_id
896 WHERE s.access_jwt = $1
897 "#,
898 token
899 )
900 .fetch_optional(&state.db)
901 .await;
902
903 let (_did, key_bytes, user_id) = match session {
904 Ok(Some(row)) => (row.did, row.key_bytes, row.user_id),
905 Ok(None) => {
906 return (
907 StatusCode::UNAUTHORIZED,
908 Json(json!({"error": "AuthenticationFailed"})),
909 )
910 .into_response();
911 }
912 Err(e) => {
913 error!("DB error in list_app_passwords: {:?}", e);
914 return (
915 StatusCode::INTERNAL_SERVER_ERROR,
916 Json(json!({"error": "InternalError"})),
917 )
918 .into_response();
919 }
920 };
921
922 if let Err(_) = crate::auth::verify_token(&token, &key_bytes) {
923 return (
924 StatusCode::UNAUTHORIZED,
925 Json(json!({"error": "AuthenticationFailed", "message": "Invalid token signature"})),
926 )
927 .into_response();
928 }
929
930 let result = sqlx::query!("SELECT name, created_at, privileged FROM app_passwords WHERE user_id = $1 ORDER BY created_at DESC", user_id)
931 .fetch_all(&state.db)
932 .await;
933
934 match result {
935 Ok(rows) => {
936 let passwords: Vec<AppPassword> = rows
937 .iter()
938 .map(|row| {
939 AppPassword {
940 name: row.name.clone(),
941 created_at: row.created_at.to_rfc3339(),
942 privileged: row.privileged,
943 }
944 })
945 .collect();
946
947 (StatusCode::OK, Json(ListAppPasswordsOutput { passwords })).into_response()
948 }
949 Err(e) => {
950 error!("DB error listing app passwords: {:?}", e);
951 (
952 StatusCode::INTERNAL_SERVER_ERROR,
953 Json(json!({"error": "InternalError"})),
954 )
955 .into_response()
956 }
957 }
958}
959
960#[derive(Deserialize)]
961pub struct CreateAppPasswordInput {
962 pub name: String,
963 pub privileged: Option<bool>,
964}
965
966#[derive(Serialize)]
967#[serde(rename_all = "camelCase")]
968pub struct CreateAppPasswordOutput {
969 pub name: String,
970 pub password: String,
971 pub created_at: String,
972 pub privileged: bool,
973}
974
975pub async fn create_app_password(
976 State(state): State<AppState>,
977 headers: axum::http::HeaderMap,
978 Json(input): Json<CreateAppPasswordInput>,
979) -> Response {
980 let auth_header = headers.get("Authorization");
981 if auth_header.is_none() {
982 return (
983 StatusCode::UNAUTHORIZED,
984 Json(json!({"error": "AuthenticationRequired"})),
985 )
986 .into_response();
987 }
988
989 let token = auth_header
990 .unwrap()
991 .to_str()
992 .unwrap_or("")
993 .replace("Bearer ", "");
994
995 let session = sqlx::query!(
996 r#"
997 SELECT s.did, k.key_bytes, u.id as user_id
998 FROM sessions s
999 JOIN users u ON s.did = u.did
1000 JOIN user_keys k ON u.id = k.user_id
1001 WHERE s.access_jwt = $1
1002 "#,
1003 token
1004 )
1005 .fetch_optional(&state.db)
1006 .await;
1007
1008 let (_did, key_bytes, user_id) = match session {
1009 Ok(Some(row)) => (row.did, row.key_bytes, row.user_id),
1010 Ok(None) => {
1011 return (
1012 StatusCode::UNAUTHORIZED,
1013 Json(json!({"error": "AuthenticationFailed"})),
1014 )
1015 .into_response();
1016 }
1017 Err(e) => {
1018 error!("DB error in create_app_password: {:?}", e);
1019 return (
1020 StatusCode::INTERNAL_SERVER_ERROR,
1021 Json(json!({"error": "InternalError"})),
1022 )
1023 .into_response();
1024 }
1025 };
1026
1027 if let Err(_) = crate::auth::verify_token(&token, &key_bytes) {
1028 return (
1029 StatusCode::UNAUTHORIZED,
1030 Json(json!({"error": "AuthenticationFailed", "message": "Invalid token signature"})),
1031 )
1032 .into_response();
1033 }
1034
1035 let name = input.name.trim();
1036 if name.is_empty() {
1037 return (
1038 StatusCode::BAD_REQUEST,
1039 Json(json!({"error": "InvalidRequest", "message": "name is required"})),
1040 )
1041 .into_response();
1042 }
1043
1044 let existing = sqlx::query!("SELECT id FROM app_passwords WHERE user_id = $1 AND name = $2", user_id, name)
1045 .fetch_optional(&state.db)
1046 .await;
1047
1048 if let Ok(Some(_)) = existing {
1049 return (
1050 StatusCode::BAD_REQUEST,
1051 Json(json!({"error": "DuplicateAppPassword", "message": "App password with this name already exists"})),
1052 )
1053 .into_response();
1054 }
1055
1056 let password: String = (0..4)
1057 .map(|_| {
1058 use rand::Rng;
1059 let mut rng = rand::thread_rng();
1060 let chars: Vec<char> = "abcdefghijklmnopqrstuvwxyz234567".chars().collect();
1061 (0..4).map(|_| chars[rng.gen_range(0..chars.len())]).collect::<String>()
1062 })
1063 .collect::<Vec<String>>()
1064 .join("-");
1065
1066 let password_hash = match bcrypt::hash(&password, bcrypt::DEFAULT_COST) {
1067 Ok(h) => h,
1068 Err(e) => {
1069 error!("Failed to hash password: {:?}", e);
1070 return (
1071 StatusCode::INTERNAL_SERVER_ERROR,
1072 Json(json!({"error": "InternalError"})),
1073 )
1074 .into_response();
1075 }
1076 };
1077
1078 let privileged = input.privileged.unwrap_or(false);
1079 let created_at = chrono::Utc::now();
1080
1081 let result = sqlx::query!(
1082 "INSERT INTO app_passwords (user_id, name, password_hash, created_at, privileged) VALUES ($1, $2, $3, $4, $5)",
1083 user_id,
1084 name,
1085 password_hash,
1086 created_at,
1087 privileged
1088 )
1089 .execute(&state.db)
1090 .await;
1091
1092 match result {
1093 Ok(_) => (
1094 StatusCode::OK,
1095 Json(CreateAppPasswordOutput {
1096 name: name.to_string(),
1097 password,
1098 created_at: created_at.to_rfc3339(),
1099 privileged,
1100 }),
1101 )
1102 .into_response(),
1103 Err(e) => {
1104 error!("DB error creating app password: {:?}", e);
1105 (
1106 StatusCode::INTERNAL_SERVER_ERROR,
1107 Json(json!({"error": "InternalError"})),
1108 )
1109 .into_response()
1110 }
1111 }
1112}
1113
1114#[derive(Deserialize)]
1115pub struct RevokeAppPasswordInput {
1116 pub name: String,
1117}
1118
1119pub async fn revoke_app_password(
1120 State(state): State<AppState>,
1121 headers: axum::http::HeaderMap,
1122 Json(input): Json<RevokeAppPasswordInput>,
1123) -> Response {
1124 let auth_header = headers.get("Authorization");
1125 if auth_header.is_none() {
1126 return (
1127 StatusCode::UNAUTHORIZED,
1128 Json(json!({"error": "AuthenticationRequired"})),
1129 )
1130 .into_response();
1131 }
1132
1133 let token = auth_header
1134 .unwrap()
1135 .to_str()
1136 .unwrap_or("")
1137 .replace("Bearer ", "");
1138
1139 let session = sqlx::query!(
1140 r#"
1141 SELECT s.did, k.key_bytes, u.id as user_id
1142 FROM sessions s
1143 JOIN users u ON s.did = u.did
1144 JOIN user_keys k ON u.id = k.user_id
1145 WHERE s.access_jwt = $1
1146 "#,
1147 token
1148 )
1149 .fetch_optional(&state.db)
1150 .await;
1151
1152 let (_did, key_bytes, user_id) = match session {
1153 Ok(Some(row)) => (row.did, row.key_bytes, row.user_id),
1154 Ok(None) => {
1155 return (
1156 StatusCode::UNAUTHORIZED,
1157 Json(json!({"error": "AuthenticationFailed"})),
1158 )
1159 .into_response();
1160 }
1161 Err(e) => {
1162 error!("DB error in revoke_app_password: {:?}", e);
1163 return (
1164 StatusCode::INTERNAL_SERVER_ERROR,
1165 Json(json!({"error": "InternalError"})),
1166 )
1167 .into_response();
1168 }
1169 };
1170
1171 if let Err(_) = crate::auth::verify_token(&token, &key_bytes) {
1172 return (
1173 StatusCode::UNAUTHORIZED,
1174 Json(json!({"error": "AuthenticationFailed", "message": "Invalid token signature"})),
1175 )
1176 .into_response();
1177 }
1178
1179 let name = input.name.trim();
1180 if name.is_empty() {
1181 return (
1182 StatusCode::BAD_REQUEST,
1183 Json(json!({"error": "InvalidRequest", "message": "name is required"})),
1184 )
1185 .into_response();
1186 }
1187
1188 let result = sqlx::query!("DELETE FROM app_passwords WHERE user_id = $1 AND name = $2", user_id, name)
1189 .execute(&state.db)
1190 .await;
1191
1192 match result {
1193 Ok(r) => {
1194 if r.rows_affected() == 0 {
1195 return (
1196 StatusCode::NOT_FOUND,
1197 Json(json!({"error": "AppPasswordNotFound", "message": "App password not found"})),
1198 )
1199 .into_response();
1200 }
1201 (StatusCode::OK, Json(json!({}))).into_response()
1202 }
1203 Err(e) => {
1204 error!("DB error revoking app password: {:?}", e);
1205 (
1206 StatusCode::INTERNAL_SERVER_ERROR,
1207 Json(json!({"error": "InternalError"})),
1208 )
1209 .into_response()
1210 }
1211 }
1212}