//! Authentication handlers — login, register, and token refresh. //! //! # Endpoints //! //! - `POST /auth/login` — authenticate with username/password, returns JWT //! - `POST /auth/register` — create a new user account //! - `POST /auth/refresh` — exchange a refresh token for a new access token //! //! # Flow //! //! Login: validate input → verify password → generate token pair → return. //! Register: validate input → check uniqueness → hash password → persist → login. //! Refresh: decode refresh token → verify → generate new token pair. use std::sync::Arc; use axum::extract::State; use axum::routing::post; use axum::{Json, Router}; use zesdex_application::ports::{PasswordService, TokenService}; use crate::dto::auth::{AuthResponse, LoginRequest, RefreshRequest, RegisterRequest}; use crate::error::ApiError; use crate::state::ApiState; /// Build the auth sub-router (`/auth/*`). pub fn router() -> Router> { Router::new() .route("/login", post(login_handler)) .route("/register", post(register_handler)) .route("/refresh", post(refresh_handler)) } /// POST /auth/login — authenticate and issue JWT tokens. /// /// ## Flow /// /// 1. Deserialize `LoginRequest`. /// 2. Load the stored user credentials from the users store. /// 3. Verify the password against the stored hash. /// 4. Generate an access + refresh token pair. /// 5. Return `AuthResponse`. /// /// ## Errors /// /// - `400 Bad Request` — missing or empty fields. /// - `401 Unauthorized` — invalid username or password. /// - `500 Internal Server Error` — unexpected failure. #[tracing::instrument(skip(state))] pub async fn login_handler( State(state): State>, Json(req): Json, ) -> Result, ApiError> { // Validate input if req.username.is_empty() || req.password.is_empty() { return Err(ApiError::BadRequest( "Username and password are required".into(), )); } // Load the users database from the store let users_path = state.store_base_dir.join("users.json"); let users: std::collections::HashMap = if users_path.exists() { let content = std::fs::read_to_string(&users_path) .map_err(|e| ApiError::Internal(format!("Failed to read users: {e}")))?; serde_json::from_str(&content) .map_err(|e| ApiError::Internal(format!("Failed to parse users: {e}")))? } else { return Err(ApiError::Unauthorized("Invalid username or password".into())); }; // Look up the user let stored_hash = users .get(&req.username) .ok_or_else(|| ApiError::Unauthorized("Invalid username or password".into()))?; // Verify password let valid = state .password_service .verify(&req.password, stored_hash) .await .map_err(|e| ApiError::Internal(format!("Password verification failed: {e}")))?; if !valid { return Err(ApiError::Unauthorized("Invalid username or password".into())); } // Generate tokens let (access_token, refresh_token) = state .token_service .generate_tokens(&req.username) .map_err(|e| ApiError::Internal(format!("Token generation failed: {e}")))?; Ok(Json(AuthResponse { access_token, refresh_token, token_type: "Bearer".to_string(), expires_in: state.token_service.access_token_expiry_secs, })) } /// POST /auth/register — create a new user account. /// /// ## Flow /// /// 1. Deserialize `RegisterRequest`. /// 2. Check username availability (load users, reject if exists). /// 3. Hash the password using Argon2id. /// 4. Persist the new username + hash. /// 5. Generate an access + refresh token pair. /// 6. Return `AuthResponse`. /// /// ## Errors /// /// - `400 Bad Request` — missing or invalid fields. /// - `409 Conflict` — username already taken. /// - `500 Internal Server Error` — unexpected failure. #[tracing::instrument(skip(state))] pub async fn register_handler( State(state): State>, Json(req): Json, ) -> Result, ApiError> { // Validate input if req.username.is_empty() { return Err(ApiError::BadRequest("Username is required".into())); } if req.password.len() < 6 { return Err(ApiError::BadRequest( "Password must be at least 6 characters".into(), )); } // Load existing users let users_path = state.store_base_dir.join("users.json"); let mut users: std::collections::HashMap = if users_path.exists() { let content = std::fs::read_to_string(&users_path) .map_err(|e| ApiError::Internal(format!("Failed to read users: {e}")))?; serde_json::from_str(&content) .map_err(|e| ApiError::Internal(format!("Failed to parse users: {e}")))? } else { std::collections::HashMap::new() }; // Check uniqueness if users.contains_key(&req.username) { return Err(ApiError::Conflict( "Username already exists".into(), )); } // Hash the password let hash = state .password_service .hash(&req.password) .await .map_err(|e| ApiError::Internal(format!("Password hashing failed: {e}")))?; // Persist users.insert(req.username.clone(), hash); let content = serde_json::to_string_pretty(&users) .map_err(|e| ApiError::Internal(format!("Failed to serialize users: {e}")))?; std::fs::write(&users_path, &content) .map_err(|e| ApiError::Internal(format!("Failed to write users: {e}")))?; // Generate tokens let (access_token, refresh_token) = state .token_service .generate_tokens(&req.username) .map_err(|e| ApiError::Internal(format!("Token generation failed: {e}")))?; Ok(Json(AuthResponse { access_token, refresh_token, token_type: "Bearer".to_string(), expires_in: state.token_service.access_token_expiry_secs, })) } /// POST /auth/refresh — exchange a refresh token for a new access token. /// /// ## Flow /// /// 1. Deserialize `RefreshRequest`. /// 2. Verify the refresh token's signature and extract the subject. /// 3. Generate a fresh access + refresh token pair. /// 4. Return `AuthResponse`. /// /// ## Errors /// /// - `400 Bad Request` — missing refresh token. /// - `401 Unauthorized` — invalid or expired refresh token. /// - `500 Internal Server Error` — unexpected failure. #[tracing::instrument(skip(state))] pub async fn refresh_handler( State(state): State>, Json(req): Json, ) -> Result, ApiError> { if req.refresh_token.is_empty() { return Err(ApiError::BadRequest("Refresh token is required".into())); } // Verify the refresh token and extract the subject let sub = state .token_service .verify_access_token(&req.refresh_token) .map_err(|_| ApiError::Unauthorized("Invalid or expired refresh token".into()))?; // Generate a fresh token pair let (access_token, new_refresh_token) = state .token_service .generate_tokens(&sub) .map_err(|e| ApiError::Internal(format!("Token generation failed: {e}")))?; Ok(Json(AuthResponse { access_token, refresh_token: new_refresh_token, token_type: "Bearer".to_string(), expires_in: state.token_service.access_token_expiry_secs, })) }