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