Files
zesdex/apps/interfaces/api/src/handlers/auth.rs
T
asepharyana da2ed6da25 feat(tui): add usage overlay and sidebar for displaying usage statistics and tasks
feat(tui): implement status bar with connection and turn state indicators
feat(tui): create workflow panel for agent status and progress visualization
feat(web): introduce web frontend interface with static file serving
feat(ws): add WebSocket interface for real-time communication and session management
2026-07-20 09:04:57 +07:00

222 lines
7.4 KiB
Rust

//! 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<Arc<ApiState>> {
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<Arc<ApiState>>,
Json(req): Json<LoginRequest>,
) -> Result<Json<AuthResponse>, 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<String, String> = 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<Arc<ApiState>>,
Json(req): Json<RegisterRequest>,
) -> Result<Json<AuthResponse>, 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<String, String> = 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<Arc<ApiState>>,
Json(req): Json<RefreshRequest>,
) -> Result<Json<AuthResponse>, 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,
}))
}