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
        ));
    }
}