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, 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, key_bytes) = match session {
378 Ok(Some(row)) => (row.did, 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 // TODO: Send email or other notification
426 info!("Account deletion requested for user {}, token: {}", did, confirmation_token);
427
428 (StatusCode::OK, Json(json!({}))).into_response()
429}
430
431pub async fn refresh_session(
432 State(state): State<AppState>,
433 headers: axum::http::HeaderMap,
434) -> Response {
435 let auth_header = headers.get("Authorization");
436 if auth_header.is_none() {
437 return (
438 StatusCode::UNAUTHORIZED,
439 Json(json!({"error": "AuthenticationRequired"})),
440 )
441 .into_response();
442 }
443
444 let refresh_token = auth_header
445 .unwrap()
446 .to_str()
447 .unwrap_or("")
448 .replace("Bearer ", "");
449
450 let session = sqlx::query!(
451 "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",
452 refresh_token
453 )
454 .fetch_optional(&state.db)
455 .await;
456
457 match session {
458 Ok(Some(session_row)) => {
459 let did = &session_row.did;
460 let key_bytes = &session_row.key_bytes;
461
462 if let Err(_) = crate::auth::verify_token(&refresh_token, &key_bytes) {
463 return (StatusCode::UNAUTHORIZED, Json(json!({"error": "AuthenticationFailed", "message": "Invalid refresh token signature"}))).into_response();
464 }
465
466 let new_access_jwt = match crate::auth::create_access_token(&did, &key_bytes) {
467 Ok(t) => t,
468 Err(e) => {
469 error!("Failed to create access token: {:?}", e);
470 return (
471 StatusCode::INTERNAL_SERVER_ERROR,
472 Json(json!({"error": "InternalError"})),
473 )
474 .into_response();
475 }
476 };
477 let new_refresh_jwt = match crate::auth::create_refresh_token(&did, &key_bytes) {
478 Ok(t) => t,
479 Err(e) => {
480 error!("Failed to create refresh token: {:?}", e);
481 return (
482 StatusCode::INTERNAL_SERVER_ERROR,
483 Json(json!({"error": "InternalError"})),
484 )
485 .into_response();
486 }
487 };
488
489 let update = sqlx::query!(
490 "UPDATE sessions SET access_jwt = $1, refresh_jwt = $2 WHERE refresh_jwt = $3",
491 new_access_jwt,
492 new_refresh_jwt,
493 refresh_token
494 )
495 .execute(&state.db)
496 .await;
497
498 match update {
499 Ok(_) => {
500 let user = sqlx::query!("SELECT handle FROM users WHERE did = $1", did)
501 .fetch_optional(&state.db)
502 .await;
503
504 match user {
505 Ok(Some(u)) => {
506 return (
507 StatusCode::OK,
508 Json(json!({
509 "accessJwt": new_access_jwt,
510 "refreshJwt": new_refresh_jwt,
511 "handle": u.handle,
512 "did": did
513 })),
514 )
515 .into_response();
516 }
517 Ok(None) => {
518 error!("User not found for existing session: {}", did);
519 return (
520 StatusCode::INTERNAL_SERVER_ERROR,
521 Json(json!({"error": "InternalError"})),
522 )
523 .into_response();
524 }
525 Err(e) => {
526 error!("Database error fetching user: {:?}", e);
527 return (
528 StatusCode::INTERNAL_SERVER_ERROR,
529 Json(json!({"error": "InternalError"})),
530 )
531 .into_response();
532 }
533 }
534 }
535 Err(e) => {
536 error!("Database error updating session: {:?}", e);
537 return (
538 StatusCode::INTERNAL_SERVER_ERROR,
539 Json(json!({"error": "InternalError"})),
540 )
541 .into_response();
542 }
543 }
544 }
545 Ok(None) => {
546 return (
547 StatusCode::UNAUTHORIZED,
548 Json(json!({"error": "AuthenticationFailed", "message": "Invalid refresh token"})),
549 )
550 .into_response();
551 }
552 Err(e) => {
553 error!("Database error fetching session: {:?}", e);
554 return (
555 StatusCode::INTERNAL_SERVER_ERROR,
556 Json(json!({"error": "InternalError"})),
557 )
558 .into_response();
559 }
560 }
561}
562
563#[derive(Serialize)]
564#[serde(rename_all = "camelCase")]
565pub struct CheckAccountStatusOutput {
566 pub activated: bool,
567 pub valid_did: bool,
568 pub repo_commit: String,
569 pub repo_rev: String,
570 pub repo_blocks: i64,
571 pub indexed_records: i64,
572 pub private_state_values: i64,
573 pub expected_blobs: i64,
574 pub imported_blobs: i64,
575}
576
577pub async fn check_account_status(
578 State(state): State<AppState>,
579 headers: axum::http::HeaderMap,
580) -> Response {
581 let auth_header = headers.get("Authorization");
582 if auth_header.is_none() {
583 return (
584 StatusCode::UNAUTHORIZED,
585 Json(json!({"error": "AuthenticationRequired"})),
586 )
587 .into_response();
588 }
589
590 let token = auth_header
591 .unwrap()
592 .to_str()
593 .unwrap_or("")
594 .replace("Bearer ", "");
595
596 let session = sqlx::query!(
597 r#"
598 SELECT s.did, k.key_bytes, u.id as user_id
599 FROM sessions s
600 JOIN users u ON s.did = u.did
601 JOIN user_keys k ON u.id = k.user_id
602 WHERE s.access_jwt = $1
603 "#,
604 token
605 )
606 .fetch_optional(&state.db)
607 .await;
608
609 let (did, key_bytes, user_id) = match session {
610 Ok(Some(row)) => (row.did, row.key_bytes, row.user_id),
611 Ok(None) => {
612 return (
613 StatusCode::UNAUTHORIZED,
614 Json(json!({"error": "AuthenticationFailed"})),
615 )
616 .into_response();
617 }
618 Err(e) => {
619 error!("DB error in check_account_status: {:?}", e);
620 return (
621 StatusCode::INTERNAL_SERVER_ERROR,
622 Json(json!({"error": "InternalError"})),
623 )
624 .into_response();
625 }
626 };
627
628 if let Err(_) = crate::auth::verify_token(&token, &key_bytes) {
629 return (
630 StatusCode::UNAUTHORIZED,
631 Json(json!({"error": "AuthenticationFailed", "message": "Invalid token signature"})),
632 )
633 .into_response();
634 }
635
636 let user_status = sqlx::query!("SELECT deactivated_at FROM users WHERE did = $1", did)
637 .fetch_optional(&state.db)
638 .await;
639
640 let deactivated_at = match user_status {
641 Ok(Some(row)) => row.deactivated_at,
642 _ => None,
643 };
644
645 let repo_result = sqlx::query!("SELECT repo_root_cid FROM repos WHERE user_id = $1", user_id)
646 .fetch_optional(&state.db)
647 .await;
648
649 let repo_commit = match repo_result {
650 Ok(Some(row)) => row.repo_root_cid,
651 _ => String::new(),
652 };
653
654 let record_count: i64 = sqlx::query_scalar!("SELECT COUNT(*) FROM records WHERE repo_id = $1", user_id)
655 .fetch_one(&state.db)
656 .await
657 .unwrap_or(Some(0))
658 .unwrap_or(0);
659
660 let blob_count: i64 =
661 sqlx::query_scalar!("SELECT COUNT(*) FROM blobs WHERE created_by_user = $1", user_id)
662 .fetch_one(&state.db)
663 .await
664 .unwrap_or(Some(0))
665 .unwrap_or(0);
666
667 let valid_did = did.starts_with("did:");
668
669 (
670 StatusCode::OK,
671 Json(CheckAccountStatusOutput {
672 activated: deactivated_at.is_none(),
673 valid_did,
674 repo_commit: repo_commit.clone(),
675 repo_rev: chrono::Utc::now().timestamp_millis().to_string(),
676 repo_blocks: 0,
677 indexed_records: record_count,
678 private_state_values: 0,
679 expected_blobs: blob_count,
680 imported_blobs: blob_count,
681 }),
682 )
683 .into_response()
684}
685
686pub async fn activate_account(
687 State(state): State<AppState>,
688 headers: axum::http::HeaderMap,
689) -> Response {
690 let auth_header = headers.get("Authorization");
691 if auth_header.is_none() {
692 return (
693 StatusCode::UNAUTHORIZED,
694 Json(json!({"error": "AuthenticationRequired"})),
695 )
696 .into_response();
697 }
698
699 let token = auth_header
700 .unwrap()
701 .to_str()
702 .unwrap_or("")
703 .replace("Bearer ", "");
704
705 let session = sqlx::query!(
706 r#"
707 SELECT s.did, k.key_bytes
708 FROM sessions s
709 JOIN users u ON s.did = u.did
710 JOIN user_keys k ON u.id = k.user_id
711 WHERE s.access_jwt = $1
712 "#,
713 token
714 )
715 .fetch_optional(&state.db)
716 .await;
717
718 let (did, key_bytes) = match session {
719 Ok(Some(row)) => (row.did, row.key_bytes),
720 Ok(None) => {
721 return (
722 StatusCode::UNAUTHORIZED,
723 Json(json!({"error": "AuthenticationFailed"})),
724 )
725 .into_response();
726 }
727 Err(e) => {
728 error!("DB error in activate_account: {:?}", e);
729 return (
730 StatusCode::INTERNAL_SERVER_ERROR,
731 Json(json!({"error": "InternalError"})),
732 )
733 .into_response();
734 }
735 };
736
737 if let Err(_) = crate::auth::verify_token(&token, &key_bytes) {
738 return (
739 StatusCode::UNAUTHORIZED,
740 Json(json!({"error": "AuthenticationFailed", "message": "Invalid token signature"})),
741 )
742 .into_response();
743 }
744
745 let result = sqlx::query!("UPDATE users SET deactivated_at = NULL WHERE did = $1", did)
746 .execute(&state.db)
747 .await;
748
749 match result {
750 Ok(_) => (StatusCode::OK, Json(json!({}))).into_response(),
751 Err(e) => {
752 error!("DB error activating account: {:?}", e);
753 (
754 StatusCode::INTERNAL_SERVER_ERROR,
755 Json(json!({"error": "InternalError"})),
756 )
757 .into_response()
758 }
759 }
760}
761
762#[derive(Deserialize)]
763#[serde(rename_all = "camelCase")]
764pub struct DeactivateAccountInput {
765 pub delete_after: Option<String>,
766}
767
768pub async fn deactivate_account(
769 State(state): State<AppState>,
770 headers: axum::http::HeaderMap,
771 Json(_input): Json<DeactivateAccountInput>,
772) -> Response {
773 let auth_header = headers.get("Authorization");
774 if auth_header.is_none() {
775 return (
776 StatusCode::UNAUTHORIZED,
777 Json(json!({"error": "AuthenticationRequired"})),
778 )
779 .into_response();
780 }
781
782 let token = auth_header
783 .unwrap()
784 .to_str()
785 .unwrap_or("")
786 .replace("Bearer ", "");
787
788 let session = sqlx::query!(
789 r#"
790 SELECT s.did, k.key_bytes
791 FROM sessions s
792 JOIN users u ON s.did = u.did
793 JOIN user_keys k ON u.id = k.user_id
794 WHERE s.access_jwt = $1
795 "#,
796 token
797 )
798 .fetch_optional(&state.db)
799 .await;
800
801 let (did, key_bytes) = match session {
802 Ok(Some(row)) => (row.did, row.key_bytes),
803 Ok(None) => {
804 return (
805 StatusCode::UNAUTHORIZED,
806 Json(json!({"error": "AuthenticationFailed"})),
807 )
808 .into_response();
809 }
810 Err(e) => {
811 error!("DB error in deactivate_account: {:?}", e);
812 return (
813 StatusCode::INTERNAL_SERVER_ERROR,
814 Json(json!({"error": "InternalError"})),
815 )
816 .into_response();
817 }
818 };
819
820 if let Err(_) = crate::auth::verify_token(&token, &key_bytes) {
821 return (
822 StatusCode::UNAUTHORIZED,
823 Json(json!({"error": "AuthenticationFailed", "message": "Invalid token signature"})),
824 )
825 .into_response();
826 }
827
828 let result = sqlx::query!("UPDATE users SET deactivated_at = NOW() WHERE did = $1", did)
829 .execute(&state.db)
830 .await;
831
832 match result {
833 Ok(_) => (StatusCode::OK, Json(json!({}))).into_response(),
834 Err(e) => {
835 error!("DB error deactivating account: {:?}", e);
836 (
837 StatusCode::INTERNAL_SERVER_ERROR,
838 Json(json!({"error": "InternalError"})),
839 )
840 .into_response()
841 }
842 }
843}
844
845#[derive(Serialize)]
846#[serde(rename_all = "camelCase")]
847pub struct AppPassword {
848 pub name: String,
849 pub created_at: String,
850 pub privileged: bool,
851}
852
853#[derive(Serialize)]
854pub struct ListAppPasswordsOutput {
855 pub passwords: Vec<AppPassword>,
856}
857
858pub async fn list_app_passwords(
859 State(state): State<AppState>,
860 headers: axum::http::HeaderMap,
861) -> Response {
862 let auth_header = headers.get("Authorization");
863 if auth_header.is_none() {
864 return (
865 StatusCode::UNAUTHORIZED,
866 Json(json!({"error": "AuthenticationRequired"})),
867 )
868 .into_response();
869 }
870
871 let token = auth_header
872 .unwrap()
873 .to_str()
874 .unwrap_or("")
875 .replace("Bearer ", "");
876
877 let session = sqlx::query!(
878 r#"
879 SELECT s.did, k.key_bytes, u.id as user_id
880 FROM sessions s
881 JOIN users u ON s.did = u.did
882 JOIN user_keys k ON u.id = k.user_id
883 WHERE s.access_jwt = $1
884 "#,
885 token
886 )
887 .fetch_optional(&state.db)
888 .await;
889
890 let (_did, key_bytes, user_id) = match session {
891 Ok(Some(row)) => (row.did, row.key_bytes, row.user_id),
892 Ok(None) => {
893 return (
894 StatusCode::UNAUTHORIZED,
895 Json(json!({"error": "AuthenticationFailed"})),
896 )
897 .into_response();
898 }
899 Err(e) => {
900 error!("DB error in list_app_passwords: {:?}", e);
901 return (
902 StatusCode::INTERNAL_SERVER_ERROR,
903 Json(json!({"error": "InternalError"})),
904 )
905 .into_response();
906 }
907 };
908
909 if let Err(_) = crate::auth::verify_token(&token, &key_bytes) {
910 return (
911 StatusCode::UNAUTHORIZED,
912 Json(json!({"error": "AuthenticationFailed", "message": "Invalid token signature"})),
913 )
914 .into_response();
915 }
916
917 let result = sqlx::query!("SELECT name, created_at, privileged FROM app_passwords WHERE user_id = $1 ORDER BY created_at DESC", user_id)
918 .fetch_all(&state.db)
919 .await;
920
921 match result {
922 Ok(rows) => {
923 let passwords: Vec<AppPassword> = rows
924 .iter()
925 .map(|row| {
926 AppPassword {
927 name: row.name.clone(),
928 created_at: row.created_at.to_rfc3339(),
929 privileged: row.privileged,
930 }
931 })
932 .collect();
933
934 (StatusCode::OK, Json(ListAppPasswordsOutput { passwords })).into_response()
935 }
936 Err(e) => {
937 error!("DB error listing app passwords: {:?}", e);
938 (
939 StatusCode::INTERNAL_SERVER_ERROR,
940 Json(json!({"error": "InternalError"})),
941 )
942 .into_response()
943 }
944 }
945}
946
947#[derive(Deserialize)]
948pub struct CreateAppPasswordInput {
949 pub name: String,
950 pub privileged: Option<bool>,
951}
952
953#[derive(Serialize)]
954#[serde(rename_all = "camelCase")]
955pub struct CreateAppPasswordOutput {
956 pub name: String,
957 pub password: String,
958 pub created_at: String,
959 pub privileged: bool,
960}
961
962pub async fn create_app_password(
963 State(state): State<AppState>,
964 headers: axum::http::HeaderMap,
965 Json(input): Json<CreateAppPasswordInput>,
966) -> Response {
967 let auth_header = headers.get("Authorization");
968 if auth_header.is_none() {
969 return (
970 StatusCode::UNAUTHORIZED,
971 Json(json!({"error": "AuthenticationRequired"})),
972 )
973 .into_response();
974 }
975
976 let token = auth_header
977 .unwrap()
978 .to_str()
979 .unwrap_or("")
980 .replace("Bearer ", "");
981
982 let session = sqlx::query!(
983 r#"
984 SELECT s.did, k.key_bytes, u.id as user_id
985 FROM sessions s
986 JOIN users u ON s.did = u.did
987 JOIN user_keys k ON u.id = k.user_id
988 WHERE s.access_jwt = $1
989 "#,
990 token
991 )
992 .fetch_optional(&state.db)
993 .await;
994
995 let (_did, key_bytes, user_id) = match session {
996 Ok(Some(row)) => (row.did, row.key_bytes, row.user_id),
997 Ok(None) => {
998 return (
999 StatusCode::UNAUTHORIZED,
1000 Json(json!({"error": "AuthenticationFailed"})),
1001 )
1002 .into_response();
1003 }
1004 Err(e) => {
1005 error!("DB error in create_app_password: {:?}", e);
1006 return (
1007 StatusCode::INTERNAL_SERVER_ERROR,
1008 Json(json!({"error": "InternalError"})),
1009 )
1010 .into_response();
1011 }
1012 };
1013
1014 if let Err(_) = crate::auth::verify_token(&token, &key_bytes) {
1015 return (
1016 StatusCode::UNAUTHORIZED,
1017 Json(json!({"error": "AuthenticationFailed", "message": "Invalid token signature"})),
1018 )
1019 .into_response();
1020 }
1021
1022 let name = input.name.trim();
1023 if name.is_empty() {
1024 return (
1025 StatusCode::BAD_REQUEST,
1026 Json(json!({"error": "InvalidRequest", "message": "name is required"})),
1027 )
1028 .into_response();
1029 }
1030
1031 let existing = sqlx::query!("SELECT id FROM app_passwords WHERE user_id = $1 AND name = $2", user_id, name)
1032 .fetch_optional(&state.db)
1033 .await;
1034
1035 if let Ok(Some(_)) = existing {
1036 return (
1037 StatusCode::BAD_REQUEST,
1038 Json(json!({"error": "DuplicateAppPassword", "message": "App password with this name already exists"})),
1039 )
1040 .into_response();
1041 }
1042
1043 let password: String = (0..4)
1044 .map(|_| {
1045 use rand::Rng;
1046 let mut rng = rand::thread_rng();
1047 let chars: Vec<char> = "abcdefghijklmnopqrstuvwxyz234567".chars().collect();
1048 (0..4).map(|_| chars[rng.gen_range(0..chars.len())]).collect::<String>()
1049 })
1050 .collect::<Vec<String>>()
1051 .join("-");
1052
1053 let password_hash = match bcrypt::hash(&password, bcrypt::DEFAULT_COST) {
1054 Ok(h) => h,
1055 Err(e) => {
1056 error!("Failed to hash password: {:?}", e);
1057 return (
1058 StatusCode::INTERNAL_SERVER_ERROR,
1059 Json(json!({"error": "InternalError"})),
1060 )
1061 .into_response();
1062 }
1063 };
1064
1065 let privileged = input.privileged.unwrap_or(false);
1066 let created_at = chrono::Utc::now();
1067
1068 let result = sqlx::query!(
1069 "INSERT INTO app_passwords (user_id, name, password_hash, created_at, privileged) VALUES ($1, $2, $3, $4, $5)",
1070 user_id,
1071 name,
1072 password_hash,
1073 created_at,
1074 privileged
1075 )
1076 .execute(&state.db)
1077 .await;
1078
1079 match result {
1080 Ok(_) => (
1081 StatusCode::OK,
1082 Json(CreateAppPasswordOutput {
1083 name: name.to_string(),
1084 password,
1085 created_at: created_at.to_rfc3339(),
1086 privileged,
1087 }),
1088 )
1089 .into_response(),
1090 Err(e) => {
1091 error!("DB error creating app password: {:?}", e);
1092 (
1093 StatusCode::INTERNAL_SERVER_ERROR,
1094 Json(json!({"error": "InternalError"})),
1095 )
1096 .into_response()
1097 }
1098 }
1099}
1100
1101#[derive(Deserialize)]
1102pub struct RevokeAppPasswordInput {
1103 pub name: String,
1104}
1105
1106pub async fn revoke_app_password(
1107 State(state): State<AppState>,
1108 headers: axum::http::HeaderMap,
1109 Json(input): Json<RevokeAppPasswordInput>,
1110) -> Response {
1111 let auth_header = headers.get("Authorization");
1112 if auth_header.is_none() {
1113 return (
1114 StatusCode::UNAUTHORIZED,
1115 Json(json!({"error": "AuthenticationRequired"})),
1116 )
1117 .into_response();
1118 }
1119
1120 let token = auth_header
1121 .unwrap()
1122 .to_str()
1123 .unwrap_or("")
1124 .replace("Bearer ", "");
1125
1126 let session = sqlx::query!(
1127 r#"
1128 SELECT s.did, k.key_bytes, u.id as user_id
1129 FROM sessions s
1130 JOIN users u ON s.did = u.did
1131 JOIN user_keys k ON u.id = k.user_id
1132 WHERE s.access_jwt = $1
1133 "#,
1134 token
1135 )
1136 .fetch_optional(&state.db)
1137 .await;
1138
1139 let (_did, key_bytes, user_id) = match session {
1140 Ok(Some(row)) => (row.did, row.key_bytes, row.user_id),
1141 Ok(None) => {
1142 return (
1143 StatusCode::UNAUTHORIZED,
1144 Json(json!({"error": "AuthenticationFailed"})),
1145 )
1146 .into_response();
1147 }
1148 Err(e) => {
1149 error!("DB error in revoke_app_password: {:?}", e);
1150 return (
1151 StatusCode::INTERNAL_SERVER_ERROR,
1152 Json(json!({"error": "InternalError"})),
1153 )
1154 .into_response();
1155 }
1156 };
1157
1158 if let Err(_) = crate::auth::verify_token(&token, &key_bytes) {
1159 return (
1160 StatusCode::UNAUTHORIZED,
1161 Json(json!({"error": "AuthenticationFailed", "message": "Invalid token signature"})),
1162 )
1163 .into_response();
1164 }
1165
1166 let name = input.name.trim();
1167 if name.is_empty() {
1168 return (
1169 StatusCode::BAD_REQUEST,
1170 Json(json!({"error": "InvalidRequest", "message": "name is required"})),
1171 )
1172 .into_response();
1173 }
1174
1175 let result = sqlx::query!("DELETE FROM app_passwords WHERE user_id = $1 AND name = $2", user_id, name)
1176 .execute(&state.db)
1177 .await;
1178
1179 match result {
1180 Ok(r) => {
1181 if r.rows_affected() == 0 {
1182 return (
1183 StatusCode::NOT_FOUND,
1184 Json(json!({"error": "AppPasswordNotFound", "message": "App password not found"})),
1185 )
1186 .into_response();
1187 }
1188 (StatusCode::OK, Json(json!({}))).into_response()
1189 }
1190 Err(e) => {
1191 error!("DB error revoking app password: {:?}", e);
1192 (
1193 StatusCode::INTERNAL_SERVER_ERROR,
1194 Json(json!({"error": "InternalError"})),
1195 )
1196 .into_response()
1197 }
1198 }
1199}