error_handler.rs
178 lignes · 6297 octets
/// Centralized Error Handling Middleware /// Feature 009: Admin & Settings /// /// Middleware to intercept errors and transform them into standardized responses /// with structured error codes. Provides consistent error handling across the application. use axum::{extract::Request, middleware::Next, response::Response}; use crate::error::AppError; /// Structured error codes for different error types /// /// Error codes follow the pattern: `{CATEGORY}_{NUMBER}` /// Categories: /// - AUTH: Authentication errors /// - CSRF: CSRF protection errors /// - RATE: Rate limiting errors /// - VALID: Validation errors /// - RBAC: Authorization errors /// - DB: Database errors /// - CONFIG: Configuration errors #[derive(Debug, Clone)] pub enum ErrorCode { // Authentication errors /// Invalid credentials supplied (wrong username or password). Auth001, /// Token is malformed or unrecognised. Auth002, /// Token has expired and must be refreshed. Auth003, /// Token was already used; possible token-theft detected. Auth004, /// Request requires authentication but no credentials were provided. Auth005, /// User must change their password before proceeding. Auth006, // CSRF errors /// CSRF token is absent from the request. Csrf001, /// CSRF token does not match the expected value. Csrf002, /// CSRF token has expired. Csrf003, // Rate limiting errors /// The client has exceeded the allowed request rate. Rate001, // Validation errors /// Input data failed schema or business-rule validation. Valid001, /// Submitted password does not meet complexity requirements. Valid002, /// Supplied email address is not in a valid format. Valid003, /// Supplied username is not in a valid format. Valid004, // Authorization errors /// The authenticated user lacks the required permission. Rbac001, /// The operation requires administrator role. Rbac002, // Database errors /// A database operation failed unexpectedly. Db001, /// The requested database record could not be found. Db002, // Configuration errors /// A configuration value is missing or invalid. Config001, } impl ErrorCode { /// Get error code as string pub fn as_str(&self) -> &'static str { match self { ErrorCode::Auth001 => "AUTH_001", ErrorCode::Auth002 => "AUTH_002", ErrorCode::Auth003 => "AUTH_003", ErrorCode::Auth004 => "AUTH_004", ErrorCode::Auth005 => "AUTH_005", ErrorCode::Auth006 => "AUTH_006", ErrorCode::Csrf001 => "CSRF_001", ErrorCode::Csrf002 => "CSRF_002", ErrorCode::Csrf003 => "CSRF_003", ErrorCode::Rate001 => "RATE_001", ErrorCode::Valid001 => "VALID_001", ErrorCode::Valid002 => "VALID_002", ErrorCode::Valid003 => "VALID_003", ErrorCode::Valid004 => "VALID_004", ErrorCode::Rbac001 => "RBAC_001", ErrorCode::Rbac002 => "RBAC_002", ErrorCode::Db001 => "DB_001", ErrorCode::Db002 => "DB_002", ErrorCode::Config001 => "CONFIG_001", } } /// Get error code from AppError pub fn from_app_error(error: &AppError) -> Self { match error { AppError::InvalidCredentials => ErrorCode::Auth001, AppError::InvalidToken => ErrorCode::Auth002, AppError::TokenExpired => ErrorCode::Auth003, AppError::TokenReused => ErrorCode::Auth004, AppError::Unauthorized => ErrorCode::Auth005, AppError::MustChangePassword => ErrorCode::Auth006, AppError::Validation(_) => ErrorCode::Valid001, AppError::Forbidden => ErrorCode::Rbac001, AppError::Database(_) => ErrorCode::Db001, AppError::NotFound(_) => ErrorCode::Db002, AppError::Config(_) => ErrorCode::Config001, AppError::Internal(_) => ErrorCode::Db001, AppError::Template(_) => ErrorCode::Db001, // Template errors treated as internal AppError::TotpFailed => ErrorCode::Auth001, AppError::TotpChallengeInvalid => ErrorCode::Auth002, AppError::TotpMaxAttemptsExceeded => ErrorCode::Rate001, AppError::TotpRequired => ErrorCode::Auth006, AppError::TotpAlreadyEnabled => ErrorCode::Valid001, AppError::TotpNotEnabled => ErrorCode::Valid001, AppError::TooManyRequests { .. } => ErrorCode::Rate001, } } } /// Middleware that intercepts errors and transforms them into standardized responses /// /// This middleware catches errors from handlers and converts them to structured /// error responses with error codes. It works in conjunction with AppError's /// IntoResponse implementation. /// /// # Arguments /// /// * `request` - HTTP request /// * `next` - Next middleware or handler /// /// # Returns /// /// Standardized error response with error code and message pub async fn error_handler(request: Request, next: Next) -> Response { // Errors are currently handled by `AppError::IntoResponse`. This middleware // is the extension point if we ever want to enrich error responses (codes, // headers, telemetry) without modifying every handler. next.run(request).await } #[cfg(test)] mod tests { use super::*; #[test] fn test_error_code_strings() { assert_eq!(ErrorCode::Auth001.as_str(), "AUTH_001"); assert_eq!(ErrorCode::Csrf001.as_str(), "CSRF_001"); assert_eq!(ErrorCode::Rate001.as_str(), "RATE_001"); assert_eq!(ErrorCode::Valid001.as_str(), "VALID_001"); assert_eq!(ErrorCode::Rbac001.as_str(), "RBAC_001"); } #[test] fn test_error_code_from_app_error() { assert!(matches!( ErrorCode::from_app_error(&AppError::InvalidCredentials), ErrorCode::Auth001 )); assert!(matches!( ErrorCode::from_app_error(&AppError::Validation("test".to_string())), ErrorCode::Valid001 )); assert!(matches!( ErrorCode::from_app_error(&AppError::Forbidden), ErrorCode::Rbac001 )); } }
GitRust