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