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 repo_result = sqlx::query("SELECT repo_root_cid FROM repos WHERE user_id = $1")
565 .bind(user_id)
566 .fetch_optional(&state.db)
567 .await;
568
569 let repo_commit = match repo_result {
570 Ok(Some(row)) => row.get::<String, _>("repo_root_cid"),
571 _ => String::new(),
572 };
573
574 let record_count: i64 = sqlx::query_scalar("SELECT COUNT(*) FROM records WHERE repo_id = $1")
575 .bind(user_id)
576 .fetch_one(&state.db)
577 .await
578 .unwrap_or(0);
579
580 let blob_count: i64 =
581 sqlx::query_scalar("SELECT COUNT(*) FROM blobs WHERE created_by_user = $1")
582 .bind(user_id)
583 .fetch_one(&state.db)
584 .await
585 .unwrap_or(0);
586
587 let valid_did = did.starts_with("did:");
588
589 (
590 StatusCode::OK,
591 Json(CheckAccountStatusOutput {
592 activated: true,
593 valid_did,
594 repo_commit: repo_commit.clone(),
595 repo_rev: chrono::Utc::now().timestamp_millis().to_string(),
596 repo_blocks: 0,
597 indexed_records: record_count,
598 private_state_values: 0,
599 expected_blobs: blob_count,
600 imported_blobs: blob_count,
601 }),
602 )
603 .into_response()
604}
605
606pub async fn activate_account(
607 State(_state): State<AppState>,
608 headers: axum::http::HeaderMap,
609) -> Response {
610 let auth_header = headers.get("Authorization");
611 if auth_header.is_none() {
612 return (
613 StatusCode::UNAUTHORIZED,
614 Json(json!({"error": "AuthenticationRequired"})),
615 )
616 .into_response();
617 }
618
619 (StatusCode::OK, Json(json!({}))).into_response()
620}
621
622#[derive(Deserialize)]
623#[serde(rename_all = "camelCase")]
624pub struct DeactivateAccountInput {
625 pub delete_after: Option<String>,
626}
627
628pub async fn deactivate_account(
629 State(_state): State<AppState>,
630 headers: axum::http::HeaderMap,
631 Json(_input): Json<DeactivateAccountInput>,
632) -> Response {
633 let auth_header = headers.get("Authorization");
634 if auth_header.is_none() {
635 return (
636 StatusCode::UNAUTHORIZED,
637 Json(json!({"error": "AuthenticationRequired"})),
638 )
639 .into_response();
640 }
641
642 (StatusCode::OK, Json(json!({}))).into_response()
643}
644
645#[derive(Serialize)]
646#[serde(rename_all = "camelCase")]
647pub struct AppPassword {
648 pub name: String,
649 pub created_at: String,
650 pub privileged: bool,
651}
652
653#[derive(Serialize)]
654pub struct ListAppPasswordsOutput {
655 pub passwords: Vec<AppPassword>,
656}
657
658pub async fn list_app_passwords(
659 State(state): State<AppState>,
660 headers: axum::http::HeaderMap,
661) -> Response {
662 let auth_header = headers.get("Authorization");
663 if auth_header.is_none() {
664 return (
665 StatusCode::UNAUTHORIZED,
666 Json(json!({"error": "AuthenticationRequired"})),
667 )
668 .into_response();
669 }
670
671 let token = auth_header
672 .unwrap()
673 .to_str()
674 .unwrap_or("")
675 .replace("Bearer ", "");
676
677 let session = sqlx::query(
678 r#"
679 SELECT s.did, k.key_bytes, u.id as user_id
680 FROM sessions s
681 JOIN users u ON s.did = u.did
682 JOIN user_keys k ON u.id = k.user_id
683 WHERE s.access_jwt = $1
684 "#,
685 )
686 .bind(&token)
687 .fetch_optional(&state.db)
688 .await;
689
690 let (_did, key_bytes, user_id) = match session {
691 Ok(Some(row)) => (
692 row.get::<String, _>("did"),
693 row.get::<Vec<u8>, _>("key_bytes"),
694 row.get::<uuid::Uuid, _>("user_id"),
695 ),
696 Ok(None) => {
697 return (
698 StatusCode::UNAUTHORIZED,
699 Json(json!({"error": "AuthenticationFailed"})),
700 )
701 .into_response();
702 }
703 Err(e) => {
704 error!("DB error in list_app_passwords: {:?}", e);
705 return (
706 StatusCode::INTERNAL_SERVER_ERROR,
707 Json(json!({"error": "InternalError"})),
708 )
709 .into_response();
710 }
711 };
712
713 if let Err(_) = crate::auth::verify_token(&token, &key_bytes) {
714 return (
715 StatusCode::UNAUTHORIZED,
716 Json(json!({"error": "AuthenticationFailed", "message": "Invalid token signature"})),
717 )
718 .into_response();
719 }
720
721 let result = sqlx::query("SELECT name, created_at, privileged FROM app_passwords WHERE user_id = $1 ORDER BY created_at DESC")
722 .bind(user_id)
723 .fetch_all(&state.db)
724 .await;
725
726 match result {
727 Ok(rows) => {
728 let passwords: Vec<AppPassword> = rows
729 .iter()
730 .map(|row| {
731 let name: String = row.get("name");
732 let created_at: chrono::DateTime<chrono::Utc> = row.get("created_at");
733 let privileged: bool = row.get("privileged");
734 AppPassword {
735 name,
736 created_at: created_at.to_rfc3339(),
737 privileged,
738 }
739 })
740 .collect();
741
742 (StatusCode::OK, Json(ListAppPasswordsOutput { passwords })).into_response()
743 }
744 Err(e) => {
745 error!("DB error listing app passwords: {:?}", e);
746 (
747 StatusCode::INTERNAL_SERVER_ERROR,
748 Json(json!({"error": "InternalError"})),
749 )
750 .into_response()
751 }
752 }
753}
754
755#[derive(Deserialize)]
756pub struct CreateAppPasswordInput {
757 pub name: String,
758 pub privileged: Option<bool>,
759}
760
761#[derive(Serialize)]
762#[serde(rename_all = "camelCase")]
763pub struct CreateAppPasswordOutput {
764 pub name: String,
765 pub password: String,
766 pub created_at: String,
767 pub privileged: bool,
768}
769
770pub async fn create_app_password(
771 State(state): State<AppState>,
772 headers: axum::http::HeaderMap,
773 Json(input): Json<CreateAppPasswordInput>,
774) -> Response {
775 let auth_header = headers.get("Authorization");
776 if auth_header.is_none() {
777 return (
778 StatusCode::UNAUTHORIZED,
779 Json(json!({"error": "AuthenticationRequired"})),
780 )
781 .into_response();
782 }
783
784 let token = auth_header
785 .unwrap()
786 .to_str()
787 .unwrap_or("")
788 .replace("Bearer ", "");
789
790 let session = sqlx::query(
791 r#"
792 SELECT s.did, k.key_bytes, u.id as user_id
793 FROM sessions s
794 JOIN users u ON s.did = u.did
795 JOIN user_keys k ON u.id = k.user_id
796 WHERE s.access_jwt = $1
797 "#,
798 )
799 .bind(&token)
800 .fetch_optional(&state.db)
801 .await;
802
803 let (_did, key_bytes, user_id) = match session {
804 Ok(Some(row)) => (
805 row.get::<String, _>("did"),
806 row.get::<Vec<u8>, _>("key_bytes"),
807 row.get::<uuid::Uuid, _>("user_id"),
808 ),
809 Ok(None) => {
810 return (
811 StatusCode::UNAUTHORIZED,
812 Json(json!({"error": "AuthenticationFailed"})),
813 )
814 .into_response();
815 }
816 Err(e) => {
817 error!("DB error in create_app_password: {:?}", e);
818 return (
819 StatusCode::INTERNAL_SERVER_ERROR,
820 Json(json!({"error": "InternalError"})),
821 )
822 .into_response();
823 }
824 };
825
826 if let Err(_) = crate::auth::verify_token(&token, &key_bytes) {
827 return (
828 StatusCode::UNAUTHORIZED,
829 Json(json!({"error": "AuthenticationFailed", "message": "Invalid token signature"})),
830 )
831 .into_response();
832 }
833
834 let name = input.name.trim();
835 if name.is_empty() {
836 return (
837 StatusCode::BAD_REQUEST,
838 Json(json!({"error": "InvalidRequest", "message": "name is required"})),
839 )
840 .into_response();
841 }
842
843 let existing = sqlx::query("SELECT id FROM app_passwords WHERE user_id = $1 AND name = $2")
844 .bind(user_id)
845 .bind(name)
846 .fetch_optional(&state.db)
847 .await;
848
849 if let Ok(Some(_)) = existing {
850 return (
851 StatusCode::BAD_REQUEST,
852 Json(json!({"error": "DuplicateAppPassword", "message": "App password with this name already exists"})),
853 )
854 .into_response();
855 }
856
857 let password: String = (0..4)
858 .map(|_| {
859 use rand::Rng;
860 let mut rng = rand::thread_rng();
861 let chars: Vec<char> = "abcdefghijklmnopqrstuvwxyz234567".chars().collect();
862 (0..4).map(|_| chars[rng.gen_range(0..chars.len())]).collect::<String>()
863 })
864 .collect::<Vec<String>>()
865 .join("-");
866
867 let password_hash = match bcrypt::hash(&password, bcrypt::DEFAULT_COST) {
868 Ok(h) => h,
869 Err(e) => {
870 error!("Failed to hash password: {:?}", e);
871 return (
872 StatusCode::INTERNAL_SERVER_ERROR,
873 Json(json!({"error": "InternalError"})),
874 )
875 .into_response();
876 }
877 };
878
879 let privileged = input.privileged.unwrap_or(false);
880 let created_at = chrono::Utc::now();
881
882 let result = sqlx::query(
883 "INSERT INTO app_passwords (user_id, name, password_hash, created_at, privileged) VALUES ($1, $2, $3, $4, $5)"
884 )
885 .bind(user_id)
886 .bind(name)
887 .bind(&password_hash)
888 .bind(created_at)
889 .bind(privileged)
890 .execute(&state.db)
891 .await;
892
893 match result {
894 Ok(_) => (
895 StatusCode::OK,
896 Json(CreateAppPasswordOutput {
897 name: name.to_string(),
898 password,
899 created_at: created_at.to_rfc3339(),
900 privileged,
901 }),
902 )
903 .into_response(),
904 Err(e) => {
905 error!("DB error creating app password: {:?}", e);
906 (
907 StatusCode::INTERNAL_SERVER_ERROR,
908 Json(json!({"error": "InternalError"})),
909 )
910 .into_response()
911 }
912 }
913}
914
915#[derive(Deserialize)]
916pub struct RevokeAppPasswordInput {
917 pub name: String,
918}
919
920pub async fn revoke_app_password(
921 State(state): State<AppState>,
922 headers: axum::http::HeaderMap,
923 Json(input): Json<RevokeAppPasswordInput>,
924) -> Response {
925 let auth_header = headers.get("Authorization");
926 if auth_header.is_none() {
927 return (
928 StatusCode::UNAUTHORIZED,
929 Json(json!({"error": "AuthenticationRequired"})),
930 )
931 .into_response();
932 }
933
934 let token = auth_header
935 .unwrap()
936 .to_str()
937 .unwrap_or("")
938 .replace("Bearer ", "");
939
940 let session = sqlx::query(
941 r#"
942 SELECT s.did, k.key_bytes, u.id as user_id
943 FROM sessions s
944 JOIN users u ON s.did = u.did
945 JOIN user_keys k ON u.id = k.user_id
946 WHERE s.access_jwt = $1
947 "#,
948 )
949 .bind(&token)
950 .fetch_optional(&state.db)
951 .await;
952
953 let (_did, key_bytes, user_id) = match session {
954 Ok(Some(row)) => (
955 row.get::<String, _>("did"),
956 row.get::<Vec<u8>, _>("key_bytes"),
957 row.get::<uuid::Uuid, _>("user_id"),
958 ),
959 Ok(None) => {
960 return (
961 StatusCode::UNAUTHORIZED,
962 Json(json!({"error": "AuthenticationFailed"})),
963 )
964 .into_response();
965 }
966 Err(e) => {
967 error!("DB error in revoke_app_password: {:?}", e);
968 return (
969 StatusCode::INTERNAL_SERVER_ERROR,
970 Json(json!({"error": "InternalError"})),
971 )
972 .into_response();
973 }
974 };
975
976 if let Err(_) = crate::auth::verify_token(&token, &key_bytes) {
977 return (
978 StatusCode::UNAUTHORIZED,
979 Json(json!({"error": "AuthenticationFailed", "message": "Invalid token signature"})),
980 )
981 .into_response();
982 }
983
984 let name = input.name.trim();
985 if name.is_empty() {
986 return (
987 StatusCode::BAD_REQUEST,
988 Json(json!({"error": "InvalidRequest", "message": "name is required"})),
989 )
990 .into_response();
991 }
992
993 let result = sqlx::query("DELETE FROM app_passwords WHERE user_id = $1 AND name = $2")
994 .bind(user_id)
995 .bind(name)
996 .execute(&state.db)
997 .await;
998
999 match result {
1000 Ok(r) => {
1001 if r.rows_affected() == 0 {
1002 return (
1003 StatusCode::NOT_FOUND,
1004 Json(json!({"error": "AppPasswordNotFound", "message": "App password not found"})),
1005 )
1006 .into_response();
1007 }
1008 (StatusCode::OK, Json(json!({}))).into_response()
1009 }
1010 Err(e) => {
1011 error!("DB error revoking app password: {:?}", e);
1012 (
1013 StatusCode::INTERNAL_SERVER_ERROR,
1014 Json(json!({"error": "InternalError"})),
1015 )
1016 .into_response()
1017 }
1018 }
1019}