//! # Gestion d'erreurs unifiée pour Veza Chat Server //! //! Ce module fournit un système d'erreurs cohérent et complet avec: //! - Catégorisation des erreurs par domaine //! - Codes d'erreur standardisés //! - Logging automatique selon la gravité //! - Sérialisation pour l'API use serde::{Deserialize, Serialize}; use std::fmt; use thiserror::Error; /// Type alias pour Result avec notre erreur personnalisée pub type Result = std::result::Result; /// Erreurs principales du système de chat #[derive(Error, Debug)] pub enum ChatError { // ═══════════════════════════════════════════════════════════════════════ // ERREURS D'AUTHENTIFICATION ET AUTORISATION // ═══════════════════════════════════════════════════════════════════════ /// Token JWT invalide ou expiré #[error("Token d'authentification invalide: {reason}")] InvalidToken { reason: String }, /// Utilisateur non autorisé pour cette action #[error("Accès refusé: {action}")] Unauthorized { action: String }, /// Utilisateur banni ou suspendu #[error("Compte suspendu: {reason}")] AccountSuspended { reason: String }, /// Tentative de connexion avec des identifiants invalides #[error("Identifiants invalides")] InvalidCredentials, /// Authentification à deux facteurs requise #[error("Authentification 2FA requise")] TwoFactorRequired, /// Code 2FA invalide #[error("Code d'authentification 2FA invalide")] InvalidTwoFactorCode, // ═══════════════════════════════════════════════════════════════════════ // ERREURS DE VALIDATION ET CONTENU // ═══════════════════════════════════════════════════════════════════════ /// Contenu de message trop long #[error("Message trop long: {actual} caractères (max: {max})")] MessageTooLong { actual: usize, max: usize }, /// Contenu inapproprié détecté #[error("Contenu inapproprié détecté: {reason}")] InappropriateContent { reason: String }, /// Spam détecté par les filtres #[error("Contenu identifié comme spam")] SpamDetected, /// Format de données invalide #[error("Format invalide pour {field}: {reason}")] InvalidFormat { field: String, reason: String }, /// Paramètre requis manquant #[error("Paramètre requis manquant: {param}")] MissingParameter { param: String }, /// Valeur hors limites acceptables #[error("{field} hors limites: {value} (min: {min}, max: {max})")] OutOfRange { field: String, value: i64, min: i64, max: i64, }, // ═══════════════════════════════════════════════════════════════════════ // ERREURS DE RATE LIMITING ET QUOTA // ═══════════════════════════════════════════════════════════════════════ /// Limite de taux dépassée #[error("Limite de taux dépassée pour {action}: {current}/{limit} dans {window}s")] RateLimitExceeded { action: String, current: u32, limit: u32, window: u64, }, /// Quota utilisateur dépassé #[error("Quota {quota_type} dépassé: {used}/{limit}")] QuotaExceeded { quota_type: String, used: u64, limit: u64, }, /// Trop de connexions simultanées #[error("Trop de connexions simultanées: {current}/{max}")] TooManyConnections { current: u32, max: u32 }, // ═══════════════════════════════════════════════════════════════════════ // ERREURS RÉSEAU ET WEBSOCKET // ═══════════════════════════════════════════════════════════════════════ /// Erreur de connexion WebSocket #[error("Erreur WebSocket: {source}")] WebSocket { #[source] source: tokio_tungstenite::tungstenite::Error, }, /// Connexion fermée de manière inattendue #[error("Connexion fermée: {reason}")] ConnectionClosed { reason: String }, /// Timeout de connexion #[error("Timeout de connexion après {seconds}s")] ConnectionTimeout { seconds: u64 }, /// Erreur réseau générale #[error("Erreur réseau: {message}")] NetworkError { message: String }, // ═══════════════════════════════════════════════════════════════════════ // ERREURS DE BASE DE DONNÉES // ═══════════════════════════════════════════════════════════════════════ /// Erreur de base de données #[error("Erreur base de données: {operation}")] Database { operation: String, #[source] source: sqlx::Error, }, /// Ressource non trouvée #[error("{resource} non trouvé(e): {id}")] NotFound { resource: String, id: String }, /// Conflit de données (ex: violation de contrainte unique) #[error("Conflit de données: {reason}")] Conflict { reason: String }, /// Transaction échouée #[error("Transaction échouée: {reason}")] TransactionFailed { reason: String }, // ═══════════════════════════════════════════════════════════════════════ // ERREURS DE CONVERSATIONS ET MESSAGES // ═══════════════════════════════════════════════════════════════════════ /// Conversation inexistante #[error("Conversation {id} inexistante")] ConversationNotFound { id: String }, /// Utilisateur pas membre de la conversation #[error("Utilisateur non membre de la conversation {conversation_id}")] NotMember { conversation_id: String }, /// Permissions insuffisantes dans la conversation #[error("Permissions insuffisantes pour {action} dans {conversation_id}")] InsufficientPermissions { action: String, conversation_id: String, }, /// Conversation archivée #[error("Conversation {id} archivée")] ConversationArchived { id: String }, /// Message non trouvé #[error("Message {id} non trouvé")] MessageNotFound { id: String }, /// Impossible d'éditer le message #[error("Edition impossible: {reason}")] EditForbidden { reason: String }, // ═══════════════════════════════════════════════════════════════════════ // ERREURS DE FICHIERS ET UPLOAD // ═══════════════════════════════════════════════════════════════════════ /// Fichier trop volumineux #[error("Fichier trop volumineux: {size} bytes (max: {max_size})")] FileTooLarge { size: u64, max_size: u64 }, /// Type de fichier non autorisé #[error("Type de fichier non autorisé: {mime_type}")] UnsupportedFileType { mime_type: String }, /// Fichier infecté détecté #[error("Fichier potentiellement dangereux détecté")] MaliciousFile, /// Erreur d'upload #[error("Erreur upload: {reason}")] UploadError { reason: String }, // ═══════════════════════════════════════════════════════════════════════ // ERREURS SYSTÈME ET CONFIGURATION // ═══════════════════════════════════════════════════════════════════════ /// Erreur de configuration #[error("Erreur configuration: {message}")] Configuration { message: String }, /// Service indisponible #[error("Service {service} indisponible: {reason}")] ServiceUnavailable { service: String, reason: String }, /// Erreur de cache #[error("Erreur cache: {operation}")] Cache { operation: String }, /// Timeout d'arrêt du serveur #[error("Timeout lors de l'arrêt du serveur")] ShutdownTimeout, /// Erreur interne non spécifiée #[error("Erreur interne: {message}")] Internal { message: String }, /// EventBus RabbitMQ indisponible #[error("EventBus indisponible: {source}")] EventBusUnavailable { #[from] source: crate::event_bus::EventBusUnavailableError, }, // ═══════════════════════════════════════════════════════════════════════ // ERREURS DE PERMISSIONS ET RÉACTIONS // ═══════════════════════════════════════════════════════════════════════ /// Permission refusée #[error("Permission refusée: {message}")] PermissionDenied { message: String }, /// Réaction déjà existante #[error("Réaction déjà existante pour ce message")] ReactionAlreadyExists, /// Réaction non trouvée #[error("Réaction non trouvée")] ReactionNotFound, // ═══════════════════════════════════════════════════════════════════════ // ERREURS DE SÉCURITÉ // ═══════════════════════════════════════════════════════════════════════ /// Activité suspecte détectée #[error("Activité suspecte détectée: {reason}")] SuspiciousActivity { reason: String }, /// IP bloquée #[error("Adresse IP {ip} bloquée: {reason}")] IpBlocked { ip: String, reason: String }, /// Tentative d'injection détectée #[error("Tentative d'injection détectée")] InjectionAttempt, /// Validation de sécurité échouée #[error("Validation sécurité échouée: {check}")] SecurityValidationFailed { check: String }, /// Erreur de sérialisation JSON #[error("Erreur JSON: {source}")] Json { #[source] source: serde_json::Error, }, /// Erreur de sérialisation générale #[error("Erreur de sérialisation {operation}: {message}")] Serialization { operation: String, message: String }, /// Fonctionnalité non disponible #[error("Fonctionnalité {feature} non disponible: {reason}")] FeatureNotAvailable { feature: String, reason: String }, /// Erreur de validation #[error("Erreur de validation pour {field}: {reason}")] ValidationError { field: String, reason: String }, /// Erreur de parsing #[error("Erreur de parsing: {reason}")] ParseError { reason: String }, /// Limite de connexions atteinte #[error("Limite de connexions simultanées atteinte")] ConnectionLimitReached, } impl ChatError { /// Retourne le code d'erreur HTTP approprié pub fn http_status(&self) -> u16 { match self { // 400 Bad Request Self::InvalidFormat { .. } | Self::MissingParameter { .. } | Self::OutOfRange { .. } | Self::MessageTooLong { .. } | Self::FileTooLarge { .. } | Self::UnsupportedFileType { .. } => 400, // 401 Unauthorized Self::InvalidToken { .. } | Self::InvalidCredentials | Self::TwoFactorRequired | Self::InvalidTwoFactorCode => 401, // 403 Forbidden Self::Unauthorized { .. } | Self::AccountSuspended { .. } | Self::InsufficientPermissions { .. } | Self::EditForbidden { .. } | Self::IpBlocked { .. } => 403, // 404 Not Found Self::NotFound { .. } | Self::ConversationNotFound { .. } | Self::MessageNotFound { .. } => 404, // 409 Conflict Self::Conflict { .. } => 409, // 413 Payload Too Large // 422 Unprocessable Entity Self::InappropriateContent { .. } | Self::SpamDetected | Self::MaliciousFile => 422, // 429 Too Many Requests Self::RateLimitExceeded { .. } | Self::QuotaExceeded { .. } | Self::TooManyConnections { .. } => 429, // 500 Internal Server Error Self::Database { .. } | Self::Internal { .. } | Self::Configuration { .. } | Self::TransactionFailed { .. } | Self::UploadError { .. } | Self::Cache { .. } => 500, // 503 Service Unavailable Self::ServiceUnavailable { .. } | Self::ShutdownTimeout | Self::EventBusUnavailable { .. } => 503, // Added EventBusUnavailable // 418 I'm a teapot (pour les tentatives d'injection) Self::InjectionAttempt => 418, // Autres erreurs -> 500 Self::Json { .. } | Self::Serialization { .. } | Self::FeatureNotAvailable { .. } | Self::ConnectionLimitReached | Self::SecurityValidationFailed { .. } | Self::SuspiciousActivity { .. } | Self::ConversationArchived { .. } | Self::NetworkError { .. } | Self::ConnectionClosed { .. } | Self::ConnectionTimeout { .. } | Self::WebSocket { .. } | Self::NotMember { .. } => 500, // Nouvelles erreurs Self::PermissionDenied { .. } => 403, Self::ReactionAlreadyExists => 409, Self::ReactionNotFound => 404, Self::ValidationError { .. } => 400, Self::ParseError { .. } => 400, } } /// Retourne la sévérité de l'erreur pour les logs pub fn severity(&self) -> ErrorSeverity { match self { // CRITICAL - Erreur système critique Self::Database { .. } | Self::ServiceUnavailable { .. } | Self::ShutdownTimeout | Self::SuspiciousActivity { .. } | Self::InjectionAttempt | Self::IpBlocked { .. } | Self::EventBusUnavailable { .. } => ErrorSeverity::High, // Added EventBusUnavailable // HIGH - Problème sérieux à traiter rapidement Self::InvalidToken { .. } | Self::AccountSuspended { .. } | Self::InvalidFormat { .. } | Self::MissingParameter { .. } | Self::OutOfRange { .. } | Self::MessageTooLong { .. } | Self::FileTooLarge { .. } | Self::UnsupportedFileType { .. } | Self::TransactionFailed { .. } | Self::UploadError { .. } | Self::InvalidCredentials | Self::InvalidTwoFactorCode | Self::InappropriateContent { .. } | Self::SpamDetected | Self::MaliciousFile | Self::ConversationNotFound { .. } | Self::InsufficientPermissions { .. } | Self::MessageNotFound { .. } | Self::EditForbidden { .. } | Self::Conflict { .. } | Self::ConnectionLimitReached | Self::SecurityValidationFailed { .. } => ErrorSeverity::Medium, // Gravité moyenne - Erreurs qui affectent l'utilisateur Self::RateLimitExceeded { .. } | Self::QuotaExceeded { .. } | Self::TooManyConnections { .. } | Self::Unauthorized { .. } | Self::NotFound { .. } => ErrorSeverity::Low, // INFO - Information Self::ConnectionClosed { .. } | Self::TwoFactorRequired | Self::NotMember { .. } | Self::Json { .. } | Self::Serialization { .. } | Self::FeatureNotAvailable { .. } | Self::ConversationArchived { .. } | Self::WebSocket { .. } | Self::NetworkError { .. } | Self::ConnectionTimeout { .. } | Self::Cache { .. } | Self::Internal { .. } | Self::Configuration { .. } => ErrorSeverity::Info, // Nouvelles erreurs Self::PermissionDenied { .. } => ErrorSeverity::Warning, Self::ReactionAlreadyExists => ErrorSeverity::Info, Self::ReactionNotFound => ErrorSeverity::Info, Self::ValidationError { .. } => ErrorSeverity::Low, Self::ParseError { .. } => ErrorSeverity::Low, } } /// Retourne un message d'erreur sécurisé pour le client pub fn public_message(&self) -> String { match self { // Messages détaillés OK pour le client Self::InvalidFormat { field, .. } => format!("Format invalide pour {}", field), Self::MissingParameter { param } => format!("Paramètre manquant: {}", param), Self::MessageTooLong { max, .. } => { format!("Message trop long (max: {} caractères)", max) } Self::RateLimitExceeded { action, window, .. } => { format!( "Trop de requêtes pour {}, veuillez patienter {}s", action, window ) } // Messages génériques pour éviter la divulgation d'informations Self::Database { .. } => "Erreur temporaire, veuillez réessayer".to_string(), Self::Internal { .. } => "Erreur interne du serveur".to_string(), Self::Configuration { .. } => "Service temporairement indisponible".to_string(), Self::InjectionAttempt => "Requête rejetée".to_string(), Self::SuspiciousActivity { .. } => "Activité inhabituelle détectée".to_string(), // Message par défaut _ => self.to_string(), } } /// Crée une erreur de base de données avec contexte pub fn database_error(operation: &str, source: sqlx::Error) -> Self { Self::Database { operation: operation.to_string(), source, } } /// Crée une erreur d'autorisation avec contexte pub fn unauthorized(action: &str) -> Self { Self::Unauthorized { action: action.to_string(), } } /// Crée une erreur de ressource non trouvée pub fn not_found(resource: &str, id: &str) -> Self { Self::NotFound { resource: resource.to_string(), id: id.to_string(), } } /// Helper pour les erreurs de configuration pub fn configuration_error(message: &str) -> Self { Self::Configuration { message: message.to_string(), } } /// Helper pour les erreurs de message trop long pub fn message_too_long(actual: usize, max: usize) -> Self { Self::MessageTooLong { actual, max } } /// Helper pour les erreurs de sérialisation pub fn serialization_error(type_name: &str, _data: &str, source: serde_json::Error) -> Self { Self::Serialization { operation: format!("serialize {}", type_name), message: source.to_string(), } } /// Helper pour les erreurs WebSocket pub fn websocket_error( _operation: &str, source: tokio_tungstenite::tungstenite::Error, ) -> Self { Self::WebSocket { source } } /// Helper pour les fonctionnalités non disponibles pub fn feature_not_available(feature: &str, reason: &str) -> Self { Self::FeatureNotAvailable { feature: feature.to_string(), reason: reason.to_string(), } } /// Helper pour convertir sqlx::Error avec une meilleure gestion pub fn from_sqlx_error(operation: &str, error: sqlx::Error) -> Self { Self::Database { operation: operation.to_string(), source: error, } } /// Helper pour les erreurs JSON pub fn from_json_error(error: serde_json::Error) -> Self { Self::Json { source: error } } /// Helper pour les erreurs de rate limiting avec des valeurs par défaut pub fn rate_limit_exceeded_simple(action: &str) -> Self { Self::RateLimitExceeded { action: action.to_string(), current: 0, limit: 0, window: 60, } } /// Helper pour les erreurs d'autorisation pub fn unauthorized_simple(action: &str) -> Self { Self::Unauthorized { action: action.to_string(), } } /// Helper pour les erreurs de contenu inapproprié pub fn inappropriate_content_simple(reason: &str) -> Self { Self::InappropriateContent { reason: reason.to_string(), } } /// Helper pour les erreurs de validation pub fn validation_error(reason: &str) -> Self { Self::ValidationError { field: "general".to_string(), reason: reason.to_string(), } } /// Helper pour les erreurs de permission pub fn permission_denied(message: &str) -> Self { Self::PermissionDenied { message: message.to_string(), } } /// Helper pour les erreurs internes pub fn internal_error(message: String) -> Self { // Changed to String Self::Internal { message, // Direct assignment } } /// Helper pour les erreurs not found avec un seul paramètre pub fn not_found_simple(message: &str) -> Self { Self::NotFound { resource: "resource".to_string(), id: message.to_string(), } } } /// Niveaux de sévérité des erreurs #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] pub enum ErrorSeverity { Info, Low, Medium, High, Critical, Warning, } impl fmt::Display for ErrorSeverity { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self { Self::Info => write!(f, "INFO"), Self::Low => write!(f, "LOW"), Self::Medium => write!(f, "MEDIUM"), Self::High => write!(f, "HIGH"), Self::Critical => write!(f, "CRITICAL"), Self::Warning => write!(f, "WARNING"), } } } /// Implémentations de conversion depuis des erreurs externes impl From for ChatError { fn from(err: sqlx::Error) -> Self { Self::database_error("query", err) } } impl From for ChatError { fn from(err: tokio_tungstenite::tungstenite::Error) -> Self { Self::WebSocket { source: err } } } impl From for ChatError { fn from(err: serde_json::Error) -> Self { Self::InvalidFormat { field: "json".to_string(), reason: err.to_string(), } } } impl From for ChatError { fn from(err: std::env::VarError) -> Self { Self::Configuration { message: format!("Variable d'environnement manquante: {}", err), } } } /// Macro pour simplifier la création d'erreurs #[macro_export] macro_rules! chat_error { ($variant:ident, $($field:ident = $value:expr),*) => { $crate::error::ChatError::$variant { $($field: $value.into()),* } }; ($variant:ident) => { $crate::error::ChatError::$variant }; } #[cfg(test)] mod tests { use super::*; #[test] fn test_error_http_status() { assert_eq!(ChatError::InvalidCredentials.http_status(), 401); assert_eq!(ChatError::not_found("user", "123").http_status(), 404); assert_eq!(ChatError::unauthorized("send_message").http_status(), 403); } #[test] fn test_error_severity() { assert_eq!(ChatError::InjectionAttempt.severity(), ErrorSeverity::High); assert_eq!( ChatError::InvalidCredentials.severity(), ErrorSeverity::Medium ); assert_eq!(ChatError::SpamDetected.severity(), ErrorSeverity::Medium); } #[test] fn test_public_message() { let error = ChatError::InvalidFormat { field: "email".to_string(), reason: "invalid format".to_string(), }; assert_eq!(error.public_message(), "Format invalide pour email"); let db_error = ChatError::database_error("insert", sqlx::Error::RowNotFound); assert_eq!( db_error.public_message(), "Erreur temporaire, veuillez réessayer" ); } #[test] fn test_error_creation_helpers() { let error = ChatError::not_found("conversation", "room_123"); match error { ChatError::NotFound { resource, id } => { assert_eq!(resource, "conversation"); assert_eq!(id, "room_123"); } _ => panic!("Wrong error type"), } } #[test] fn test_macro() { let error = chat_error!(MessageTooLong, actual = 5000_usize, max = 4000_usize); match error { ChatError::MessageTooLong { actual, max } => { assert_eq!(actual, 5000); assert_eq!(max, 4000); } _ => panic!("Wrong error type"), } } }