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