1use crate::api::{ApiResponse, ApiState, extract_bearer_token};
6use axum::{Json, extract::State, http::HeaderMap};
7use serde::{Deserialize, Serialize};
8
9#[derive(Debug, Deserialize)]
16pub struct LoginRequest {
17 pub username: String,
18 pub password: String,
19 #[serde(default)]
21 pub challenge_id: Option<String>,
22 #[serde(default)]
24 pub mfa_code: Option<String>,
25 #[serde(default)]
27 pub remember_me: bool,
28}
29
30#[derive(Debug, Serialize)]
36pub struct LoginResponse {
37 pub access_token: String,
38 pub refresh_token: String,
39 pub token_type: String,
40 pub expires_in: u64,
41 pub user: LoginUserInfo,
42 pub login_risk_level: String,
45 pub security_warnings: Vec<String>,
48}
49
50#[derive(Debug, Serialize)]
52pub struct LoginUserInfo {
53 pub id: String,
54 pub username: String,
55 pub roles: Vec<String>,
56 pub permissions: Vec<String>,
57}
58
59async fn build_login_response(
60 state: &ApiState,
61 user_id: &str,
62 username: String,
63 permissions: Vec<String>,
64) -> ApiResponse<LoginResponse> {
65 let user_key = format!("user:{}", user_id);
66 let roles: Vec<String> = match state.auth_framework.storage().get_kv(&user_key).await {
67 Ok(Some(bytes)) => {
68 let json: serde_json::Value = serde_json::from_slice(&bytes).unwrap_or_default();
69 json["roles"]
70 .as_array()
71 .map(|arr| {
72 arr.iter()
73 .filter_map(|value| value.as_str())
74 .map(|value| value.to_string())
75 .collect()
76 })
77 .unwrap_or_default()
78 }
79 _ => vec![],
80 };
81
82 let user_info = LoginUserInfo {
83 id: user_id.to_string(),
84 username,
85 roles: roles.clone(),
86 permissions,
87 };
88
89 let token_lifetime = state.auth_framework.config().token_lifetime;
90 let access_token = match state.auth_framework.token_manager().create_jwt_token(
91 user_id,
92 roles,
93 Some(token_lifetime),
94 ) {
95 Ok(jwt) => jwt,
96 Err(e) => {
97 tracing::error!("Failed to create JWT token: {}", e);
98 return ApiResponse::error_typed(
99 "TOKEN_CREATION_FAILED",
100 "Failed to create access token",
101 );
102 }
103 };
104
105 let refresh_token_lifetime = state.auth_framework.config().refresh_token_lifetime;
106 let refresh_token = match state.auth_framework.token_manager().create_jwt_token(
107 user_id,
108 vec!["refresh".to_string()],
109 Some(refresh_token_lifetime),
110 ) {
111 Ok(jwt) => jwt,
112 Err(e) => {
113 tracing::error!("Failed to create refresh token: {}", e);
114 return ApiResponse::error_typed(
115 "TOKEN_CREATION_FAILED",
116 "Failed to create refresh token",
117 );
118 }
119 };
120
121 ApiResponse::success(LoginResponse {
122 access_token,
123 refresh_token,
124 token_type: "Bearer".to_string(),
125 expires_in: token_lifetime.as_secs(),
126 user: user_info,
127 login_risk_level: "low".to_string(), security_warnings: Vec::new(), })
130}
131
132#[derive(Debug, Deserialize)]
134pub struct RefreshRequest {
135 pub refresh_token: String,
137}
138
139#[derive(Debug, Serialize)]
141pub struct RefreshResponse {
142 pub access_token: String,
144 pub token_type: String,
146 pub expires_in: u64,
148}
149
150#[derive(Debug, Deserialize)]
152pub struct LogoutRequest {
153 #[serde(default)]
155 pub refresh_token: Option<String>,
156}
157
158pub(crate) fn login_risk_level(headers: &HeaderMap) -> (&'static str, Vec<String>) {
165 let user_agent = headers
166 .get("user-agent")
167 .and_then(|v| v.to_str().ok())
168 .unwrap_or("");
169
170 let forwarded_for = headers
171 .get("x-forwarded-for")
172 .and_then(|v| v.to_str().ok())
173 .unwrap_or("");
174
175 let mut risk_points: u8 = 0;
176 let mut warnings: Vec<String> = Vec::new();
177
178 if user_agent.is_empty() {
179 risk_points = risk_points.saturating_add(30);
180 warnings.push(
181 "No browser User-Agent detected; this request may originate from an automated script."
182 .to_string(),
183 );
184 }
185
186 if user_agent.to_lowercase().contains("tor browser") {
189 risk_points = risk_points.saturating_add(40);
190 warnings.push("Login originated from the Tor Browser.".to_string());
191 }
192
193 let hop_count = forwarded_for.split(',').count();
195 if hop_count >= 2 {
196 risk_points = risk_points.saturating_add(15);
197 warnings.push(format!(
198 "Request passed through {} proxy hops (X-Forwarded-For).",
199 hop_count
200 ));
201 }
202
203 let level = match risk_points {
204 0..=9 => "low",
205 10..=29 => "medium",
206 30..=59 => "high",
207 _ => "critical",
208 };
209 (level, warnings)
210}
211
212async fn increment_login_failure(state: &ApiState, lockout_key: &str, window_secs: u64) {
214 let current: u64 = match state.auth_framework.storage().get_kv(lockout_key).await {
215 Ok(Some(bytes)) => std::str::from_utf8(&bytes)
216 .ok()
217 .and_then(|s| s.parse().ok())
218 .unwrap_or(0),
219 _ => 0,
220 };
221 let new_count = current.saturating_add(1);
222 let _ = state
223 .auth_framework
224 .storage()
225 .store_kv(
226 lockout_key,
227 new_count.to_string().as_bytes(),
228 Some(std::time::Duration::from_secs(window_secs)),
229 )
230 .await;
231}
232
233pub async fn login(
239 State(state): State<ApiState>,
240 headers: HeaderMap,
241 Json(req): Json<LoginRequest>,
242) -> ApiResponse<LoginResponse> {
243 if req.username.is_empty() || req.password.is_empty() {
245 return ApiResponse::validation_error_typed("Username and password are required");
246 }
247
248 if req.challenge_id.is_some() ^ req.mfa_code.is_some() {
249 return ApiResponse::validation_error_typed(
250 "challenge_id and mfa_code must be provided together",
251 );
252 }
253
254 if let (Some(challenge_id), Some(mfa_code)) =
255 (req.challenge_id.clone(), req.mfa_code.as_deref())
256 {
257 return match state
258 .auth_framework
259 .complete_mfa_by_id(&challenge_id, mfa_code)
260 .await
261 {
262 Ok(token) => {
263 let mut response = build_login_response(
264 &state,
265 &token.user_id,
266 req.username,
267 token.permissions.to_vec(),
268 )
269 .await;
270 if let Some(data) = response.data.as_mut() {
272 data.login_risk_level = "low".to_string();
273 }
274 response
275 }
276 Err(e) => {
277 tracing::debug!("MFA completion failed during login: {}", e);
278 ApiResponse::error_typed(
279 "MFA_INVALID_CODE",
280 "Invalid or expired MFA challenge or code",
281 )
282 }
283 };
284 }
285
286 let (risk_level, mut risk_warnings) = login_risk_level(&headers);
289
290 let lockout_key = format!("login_failures:{}", req.username);
294 const MAX_FAILED_ATTEMPTS: u64 = 5;
295 const LOCKOUT_WINDOW_SECS: u64 = 900; if let Ok(Some(count_bytes)) = state.auth_framework.storage().get_kv(&lockout_key).await {
297 if let Ok(count_str) = std::str::from_utf8(&count_bytes) {
298 if let Ok(count) = count_str.parse::<u64>() {
299 if count >= MAX_FAILED_ATTEMPTS {
300 tracing::warn!(
301 username = %req.username,
302 failed_attempts = count,
303 "Login rejected — account temporarily locked due to repeated failures"
304 );
305 return ApiResponse::error_typed(
306 "ACCOUNT_LOCKED",
307 "Too many failed login attempts. Please try again later.",
308 );
309 }
310 }
311 }
312 }
313
314 let credential = crate::authentication::credentials::Credential::Password {
316 username: req.username.clone(),
317 password: req.password.clone(),
318 };
319
320 match state
322 .auth_framework
323 .authenticate("password", credential)
324 .await
325 {
326 Ok(auth_result) => match auth_result {
327 crate::auth::AuthResult::Success(token) => {
328 let mfa_enrolled =
331 crate::api::mfa::check_user_mfa_status(&state.auth_framework, &token.user_id)
332 .await;
333
334 if !mfa_enrolled && matches!(risk_level, "high" | "critical") {
335 risk_warnings.push(
336 "Your account does not have multi-factor authentication enabled. \
337 Enable MFA to protect this account from high-risk login contexts."
338 .to_string(),
339 );
340 tracing::warn!(
341 user_id = %token.user_id,
342 risk_level = %risk_level,
343 "High-risk login without MFA enrolled"
344 );
345 } else {
346 tracing::info!(
347 user_id = %token.user_id,
348 risk_level = %risk_level,
349 mfa_enrolled = %mfa_enrolled,
350 "Successful login"
351 );
352 }
353
354 let mut response = build_login_response(
355 &state,
356 &token.user_id,
357 req.username,
358 token.permissions.to_vec(),
359 )
360 .await;
361
362 let _ = state.auth_framework.storage().delete_kv(&lockout_key).await;
364
365 if let Some(data) = response.data.as_mut() {
366 data.login_risk_level = risk_level.to_string();
367 data.security_warnings = risk_warnings;
368 }
369 response
370 }
371 crate::auth::AuthResult::MfaRequired(challenge) => {
372 let mfa_type_str = match &challenge.mfa_type {
376 crate::methods::MfaType::Totp => "totp",
377 crate::methods::MfaType::Sms { .. } => "sms",
378 crate::methods::MfaType::Email { .. } => "email",
379 crate::methods::MfaType::Push { .. } => "push",
380 crate::methods::MfaType::SecurityKey => "security_key",
381 crate::methods::MfaType::BackupCode => "backup_code",
382 crate::methods::MfaType::MultiMethod => "totp_or_backup_code",
383 };
384 ApiResponse::<()>::error_with_details(
385 "MFA_REQUIRED",
386 "Multi-factor authentication required",
387 serde_json::json!({
388 "challenge_id": challenge.id,
389 "mfa_type": mfa_type_str,
390 "expires_at": challenge.expires_at.to_rfc3339(),
391 "message": challenge.message,
392 }),
393 )
394 .cast()
395 }
396 crate::auth::AuthResult::Failure(reason) => {
397 increment_login_failure(&state, &lockout_key, LOCKOUT_WINDOW_SECS).await;
399 ApiResponse::error_typed("AUTHENTICATION_FAILED", reason)
400 }
401 },
402 Err(e) => {
403 increment_login_failure(&state, &lockout_key, LOCKOUT_WINDOW_SECS).await;
405 tracing::debug!(
409 "Authentication error (reported as INVALID_CREDENTIALS): {}",
410 e
411 );
412 ApiResponse::error_typed("INVALID_CREDENTIALS", "Invalid username or password")
413 }
414 }
415}
416
417pub async fn refresh_token(
419 State(state): State<ApiState>,
420 Json(req): Json<RefreshRequest>,
421) -> ApiResponse<RefreshResponse> {
422 if req.refresh_token.is_empty() {
423 return ApiResponse::validation_error_typed("Invalid request");
424 }
425
426 match state
428 .auth_framework
429 .token_manager()
430 .validate_jwt_token(&req.refresh_token)
431 {
432 Ok(claims) => {
433 if !claims.scope.contains("refresh") {
435 return ApiResponse::error_typed(
436 "INVALID_TOKEN",
437 "Expected a refresh token, but received an access token",
438 );
439 }
440
441 let revocation_key = format!("revoked_token:{}", claims.jti);
443 match state.auth_framework.storage().get_kv(&revocation_key).await {
444 Ok(Some(_)) => {
445 return ApiResponse::error_typed(
446 "INVALID_TOKEN",
447 "Refresh token has been revoked",
448 );
449 }
450 Ok(None) => {} Err(e) => {
452 tracing::error!("Refresh token revocation check failed: {}", e);
453 return ApiResponse::error_typed(
454 "INTERNAL_ERROR",
455 "Unable to verify token status",
456 );
457 }
458 }
459
460 {
467 let user_key = format!("user:{}", claims.sub);
468 if let Ok(Some(user_bytes)) = state.auth_framework.storage().get_kv(&user_key).await
469 {
470 let user_json: serde_json::Value =
471 serde_json::from_slice(&user_bytes).unwrap_or_default();
472 let active = user_json["active"].as_bool().unwrap_or(true);
473 if !active {
474 return ApiResponse::error_typed(
475 "ACCOUNT_DEACTIVATED",
476 "Account has been deactivated",
477 );
478 }
479 }
480 }
481
482 let permissions: Vec<String> = match state
483 .auth_framework
484 .storage()
485 .get_kv(&format!("user_permissions:{}", claims.sub))
486 .await
487 {
488 Ok(Some(data)) => serde_json::from_slice(&data).unwrap_or_default(),
489 _ => vec![],
490 };
491
492 let token_lifetime = state.auth_framework.config().token_lifetime;
493 let new_access_token = match state.auth_framework.token_manager().create_jwt_token(
494 &claims.sub,
495 permissions,
496 Some(token_lifetime),
497 ) {
498 Ok(jwt) => jwt,
499 Err(e) => {
500 tracing::error!("Failed to create new access token: {}", e);
501 return ApiResponse::error_typed(
502 "TOKEN_CREATION_FAILED",
503 "Failed to create new access token",
504 );
505 }
506 };
507
508 let response = RefreshResponse {
509 access_token: new_access_token,
510 token_type: "Bearer".to_string(),
511 expires_in: token_lifetime.as_secs(),
512 };
513
514 ApiResponse::success(response)
515 }
516 Err(e) => {
517 tracing::warn!("Invalid refresh token: {}", e);
518 ApiResponse::error_typed("INVALID_TOKEN", "Invalid or expired refresh token")
519 }
520 }
521}
522
523pub async fn logout(
525 State(state): State<ApiState>,
526 headers: HeaderMap,
527 Json(req): Json<LogoutRequest>,
528) -> ApiResponse<()> {
529 if let Some(token) = extract_bearer_token(&headers) {
531 match state
532 .auth_framework
533 .token_manager()
534 .validate_jwt_token(&token)
535 {
536 Ok(claims) => {
537 let revocation_key = format!("revoked_token:{}", claims.jti);
538 let ttl = std::time::Duration::from_secs(7 * 86400);
540 if let Err(e) = state
541 .auth_framework
542 .storage()
543 .store_kv(revocation_key.as_str(), b"revoked", Some(ttl))
544 .await
545 {
546 tracing::error!("Failed to revoke access token JTI {}: {}", claims.jti, e);
547 } else {
548 tracing::info!("Access token revoked (JTI: {})", claims.jti);
549 }
550 }
551 Err(_) => {
552 tracing::debug!("Logout called with invalid/expired access token");
554 }
555 }
556 }
557
558 if let Some(ref refresh_token) = req.refresh_token {
560 match state
561 .auth_framework
562 .token_manager()
563 .validate_jwt_token(refresh_token)
564 {
565 Ok(claims) => {
566 let revocation_key = format!("revoked_token:{}", claims.jti);
567 let ttl = std::time::Duration::from_secs(7 * 86400);
568 if let Err(e) = state
569 .auth_framework
570 .storage()
571 .store_kv(revocation_key.as_str(), b"revoked", Some(ttl))
572 .await
573 {
574 tracing::error!("Failed to revoke refresh token JTI {}: {}", claims.jti, e);
575 } else {
576 tracing::info!("Refresh token revoked (JTI: {})", claims.jti);
577 }
578 }
579 Err(_) => {
580 tracing::debug!("Logout called with invalid/expired refresh token");
581 }
582 }
583 }
584
585 ApiResponse::<()>::ok_with_message("Successfully logged out")
586}
587
588pub async fn validate_token(
591 State(state): State<ApiState>,
592 headers: HeaderMap,
593) -> ApiResponse<LoginUserInfo> {
594 match extract_bearer_token(&headers) {
595 Some(token) => {
596 match crate::api::validate_api_token(&state.auth_framework, &token).await {
597 Ok(auth_token) => {
598 let username = match state
600 .auth_framework
601 .get_user_profile(&auth_token.user_id)
602 .await
603 {
604 Ok(profile) => profile
605 .username
606 .unwrap_or_else(|| format!("user_{}", auth_token.user_id)),
607 Err(_) => format!("user_{}", auth_token.user_id), };
609
610 let user_info = LoginUserInfo {
611 id: auth_token.user_id,
612 username,
613 roles: auth_token.roles.to_vec(),
614 permissions: auth_token.permissions.to_vec(),
615 };
616 ApiResponse::success(user_info)
617 }
618 Err(_e) => ApiResponse::error_typed("AUTH_ERROR", "Token validation failed"),
619 }
620 }
621 None => ApiResponse::unauthorized_typed(),
622 }
623}
624
625pub async fn list_providers(State(_state): State<ApiState>) -> ApiResponse<Vec<ProviderInfo>> {
627 let providers = vec![
628 ProviderInfo {
629 name: "google".to_string(),
630 display_name: "Google".to_string(),
631 auth_url: "/oauth/google".to_string(),
632 },
633 ProviderInfo {
634 name: "github".to_string(),
635 display_name: "GitHub".to_string(),
636 auth_url: "/oauth/github".to_string(),
637 },
638 ProviderInfo {
639 name: "microsoft".to_string(),
640 display_name: "Microsoft".to_string(),
641 auth_url: "/oauth/microsoft".to_string(),
642 },
643 ];
644
645 ApiResponse::success(providers)
646}
647
648#[derive(Debug, Serialize)]
650pub struct ProviderInfo {
651 pub name: String,
653 pub display_name: String,
655 pub auth_url: String,
657}
658
659#[derive(Debug, Deserialize)]
661pub struct RegisterRequest {
662 pub username: String,
664 pub email: String,
666 pub password: String,
668}
669
670#[derive(Debug, Serialize)]
672pub struct RegisterResponse {
673 pub user_id: String,
675 pub username: String,
677 pub email: String,
679}
680
681pub async fn register(
686 State(state): State<ApiState>,
687 Json(req): Json<RegisterRequest>,
688) -> ApiResponse<RegisterResponse> {
689 if req.username.is_empty() || req.password.is_empty() || req.email.is_empty() {
691 return ApiResponse::validation_error_typed("Username, password, and email are required");
692 }
693
694 if let Err(e) = crate::utils::validation::validate_username(&req.username) {
696 return ApiResponse::validation_error_typed(format!("{e}"));
697 }
698
699 if let Err(e) = crate::utils::validation::validate_password(&req.password) {
701 return ApiResponse::validation_error_typed(format!("{e}"));
702 }
703
704 match crate::utils::breach_check::is_password_breached(&req.password).await {
706 Ok(true) => {
707 return ApiResponse::validation_error_typed(
708 "This password has appeared in a known data breach. Please choose a different password.",
709 );
710 }
711 Ok(false) => {} Err(_) => {} }
714
715 if let Err(e) = crate::utils::validation::validate_email(&req.email) {
717 return ApiResponse::validation_error_typed(format!("{e}"));
718 }
719
720 let username_key = format!("user:credentials:{}", req.username);
722 match state.auth_framework.storage().get_kv(&username_key).await {
723 Ok(Some(_)) => {
724 return ApiResponse::error_typed(
727 "CONFLICT",
728 "An account with the provided details already exists",
729 );
730 }
731 Err(e) => {
732 tracing::error!("Storage error checking username: {}", e);
733 return ApiResponse::internal_error_typed();
734 }
735 Ok(None) => {}
736 }
737
738 let email_key = format!("user:email:{}", req.email);
740 match state.auth_framework.storage().get_kv(&email_key).await {
741 Ok(Some(_)) => {
742 return ApiResponse::error_typed(
743 "CONFLICT",
744 "An account with the provided details already exists",
745 );
746 }
747 Err(e) => {
748 tracing::error!("Storage error checking email: {}", e);
749 return ApiResponse::internal_error_typed();
750 }
751 Ok(None) => {}
752 }
753
754 let password_hash = match crate::utils::password::hash_password(&req.password) {
756 Ok(hash) => hash,
757 Err(e) => {
758 tracing::error!("Password hashing failed: {:?}", e);
759 return ApiResponse::error_typed("REGISTRATION_FAILED", "Failed to process password");
760 }
761 };
762
763 let user_id = format!("user_{}", uuid::Uuid::new_v4().as_simple());
765 let created_at = chrono::Utc::now().to_rfc3339();
766
767 let user_data = serde_json::json!({
769 "user_id": user_id,
770 "username": req.username,
771 "email": req.email,
772 "password_hash": password_hash,
773 "created_at": created_at,
774 });
775 let user_data_bytes = user_data.to_string().into_bytes();
776
777 if let Err(e) = state
779 .auth_framework
780 .storage()
781 .store_kv(&username_key, &user_data_bytes, None)
782 .await
783 {
784 tracing::error!("User registration storage failed: {:?}", e);
785 return ApiResponse::error_typed("REGISTRATION_FAILED", "Failed to create user account");
786 }
787
788 if let Err(e) = state
790 .auth_framework
791 .storage()
792 .store_kv(&email_key, user_id.as_bytes(), None)
793 .await
794 {
795 tracing::error!("Email mapping storage failed: {:?}", e);
796 let _ = state
798 .auth_framework
799 .storage()
800 .delete_kv(&username_key)
801 .await;
802 return ApiResponse::error_typed("REGISTRATION_FAILED", "Failed to create user account");
803 }
804
805 let canonical_user_data = serde_json::json!({
808 "user_id": user_id,
809 "username": req.username,
810 "email": req.email,
811 "password_hash": password_hash,
812 "roles": ["user"],
813 "active": true,
814 "created_at": created_at,
815 });
816 let canonical_key = format!("user:{}", user_id);
817 if let Err(e) = state
818 .auth_framework
819 .storage()
820 .store_kv(
821 &canonical_key,
822 canonical_user_data.to_string().as_bytes(),
823 None,
824 )
825 .await
826 {
827 tracing::error!("Canonical user record storage failed: {:?}", e);
828 let _ = state
829 .auth_framework
830 .storage()
831 .delete_kv(&username_key)
832 .await;
833 let _ = state.auth_framework.storage().delete_kv(&email_key).await;
834 return ApiResponse::error_typed("REGISTRATION_FAILED", "Failed to create user account");
835 }
836
837 let username_id_key = format!("user:username:{}", req.username);
839 if let Err(e) = state
840 .auth_framework
841 .storage()
842 .store_kv(&username_id_key, user_id.as_bytes(), None)
843 .await
844 {
845 tracing::error!("Username-id mapping storage failed: {:?}", e);
846 let _ = state
847 .auth_framework
848 .storage()
849 .delete_kv(&username_key)
850 .await;
851 let _ = state.auth_framework.storage().delete_kv(&email_key).await;
852 let _ = state
853 .auth_framework
854 .storage()
855 .delete_kv(&canonical_key)
856 .await;
857 return ApiResponse::error_typed("REGISTRATION_FAILED", "Failed to create user account");
858 }
859
860 let index_key = "users:index";
862 let mut ids: Vec<String> = match state.auth_framework.storage().get_kv(index_key).await {
863 Ok(Some(bytes)) => serde_json::from_slice(&bytes).unwrap_or_default(),
864 _ => vec![],
865 };
866 ids.push(user_id.clone());
867 if let Ok(idx_json) = serde_json::to_vec(&ids) {
868 if let Err(e) = state
869 .auth_framework
870 .storage()
871 .store_kv(index_key, &idx_json, None)
872 .await
873 {
874 tracing::warn!("Failed to update user index after registration: {}", e);
875 }
876 }
877
878 tracing::info!("New user registered: {} ({})", req.username, user_id);
879
880 ApiResponse::success(RegisterResponse {
881 user_id,
882 username: req.username,
883 email: req.email,
884 })
885}
886
887#[derive(Debug, Serialize)]
889pub struct CreateApiKeyResponse {
890 pub api_key: String,
892 pub token_type: String,
894}
895
896pub async fn create_api_key(
898 State(state): State<ApiState>,
899 headers: HeaderMap,
900) -> ApiResponse<CreateApiKeyResponse> {
901 let token = match crate::api::extract_bearer_token(&headers) {
903 Some(t) => t,
904 None => return ApiResponse::unauthorized_typed(),
905 };
906
907 let auth_token = match crate::api::validate_api_token(&state.auth_framework, &token).await {
909 Ok(t) => t,
910 Err(_) => return ApiResponse::unauthorized_typed(),
911 };
912
913 match state
915 .auth_framework
916 .create_api_key(&auth_token.user_id, None)
917 .await
918 {
919 Ok(api_key) => ApiResponse::success(CreateApiKeyResponse {
920 api_key,
921 token_type: "ApiKey".to_string(),
922 }),
923 Err(e) => {
924 tracing::error!(
925 "Failed to create API key for user {}: {}",
926 auth_token.user_id,
927 e
928 );
929 ApiResponse::internal_error_typed()
930 }
931 }
932}
933
934#[cfg(test)]
935mod tests {
936 use super::*;
937 use axum::http::HeaderMap;
938
939 #[test]
940 fn test_login_risk_low_normal_request() {
941 let mut headers = HeaderMap::new();
942 headers.insert(
943 "user-agent",
944 "Mozilla/5.0 (Windows NT 10.0; Win64; x64)".parse().unwrap(),
945 );
946 let (level, warnings) = login_risk_level(&headers);
947 assert_eq!(level, "low");
948 assert!(warnings.is_empty());
949 }
950
951 #[test]
952 fn test_login_risk_high_no_user_agent() {
953 let headers = HeaderMap::new();
954 let (level, warnings) = login_risk_level(&headers);
955 assert_eq!(level, "high");
956 assert!(!warnings.is_empty());
957 }
958
959 #[test]
960 fn test_login_risk_tor_browser() {
961 let mut headers = HeaderMap::new();
962 headers.insert("user-agent", "Mozilla/5.0 (Tor Browser)".parse().unwrap());
963 let (level, warnings) = login_risk_level(&headers);
964 assert!(level == "high" || level == "critical");
966 assert!(warnings.iter().any(|w| w.contains("Tor")));
967 }
968
969 #[test]
970 fn test_login_risk_proxy_hops() {
971 let mut headers = HeaderMap::new();
972 headers.insert("user-agent", "Mozilla/5.0".parse().unwrap());
973 headers.insert("x-forwarded-for", "1.2.3.4, 5.6.7.8".parse().unwrap());
974 let (level, warnings) = login_risk_level(&headers);
975 assert_eq!(level, "medium");
976 assert!(warnings.iter().any(|w| w.contains("proxy")));
977 }
978
979 #[test]
980 fn test_login_request_deserialization() {
981 let json = r#"{"username":"alice","password":"secret"}"#;
982 let req: LoginRequest = serde_json::from_str(json).unwrap();
983 assert_eq!(req.username, "alice");
984 assert_eq!(req.password, "secret");
985 assert!(!req.remember_me);
986 assert!(req.challenge_id.is_none());
987 assert!(req.mfa_code.is_none());
988 }
989
990 #[test]
991 fn test_login_response_serialization() {
992 let resp = LoginResponse {
993 access_token: "at".into(),
994 refresh_token: "rt".into(),
995 token_type: "Bearer".into(),
996 expires_in: 3600,
997 user: LoginUserInfo {
998 id: "uid".into(),
999 username: "alice".into(),
1000 roles: vec!["user".into()],
1001 permissions: vec![],
1002 },
1003 login_risk_level: "low".into(),
1004 security_warnings: vec![],
1005 };
1006 let json = serde_json::to_value(&resp).unwrap();
1007 assert_eq!(json["token_type"], "Bearer");
1008 assert_eq!(json["expires_in"], 3600);
1009 assert_eq!(json["user"]["username"], "alice");
1010 }
1011
1012 #[test]
1013 fn test_register_request_deserialization() {
1014 let json = r#"{"username":"bob","password":"StrongP@ss1","email":"bob@example.com"}"#;
1015 let req: RegisterRequest = serde_json::from_str(json).unwrap();
1016 assert_eq!(req.username, "bob");
1017 assert_eq!(req.email, "bob@example.com");
1018 }
1019
1020 #[test]
1021 fn test_refresh_request_deserialization() {
1022 let json = r#"{"refresh_token":"some_token"}"#;
1023 let req: RefreshRequest = serde_json::from_str(json).unwrap();
1024 assert_eq!(req.refresh_token, "some_token");
1025 }
1026}