axum_gate/errors/
mod.rs

1//! Unified, category-based error types exposed by this crate.
2//!
3//! This module contains error types you mostly need when using this crate:
4//! - `Error`: root enum wrapping all category errors
5//! - `Result<T>`: convenience alias
6//! - `UserFriendlyError`: trait providing multiple message levels
7//! - Category enums: `AccountsError`, `AuthnError`, `AuthzError`, `PermissionsError`,
8//!   `CodecsError`, `JwtError`, `RepositoriesError`, `DatabaseError`, `HashingError`, `SecretError`
9//!
10//! # Error Message Levels
11//! Each error provides three message levels for different audiences:
12//! - **User Message**: Clear, actionable message for end users
13//! - **Developer Message**: Technical details for debugging
14//! - **Support Code**: Unique reference code for customer support
15//!
16//! # When to Use Each Variant
17//! - `Accounts` – Account operations (create/update/delete/query/workflows/validation)
18//! - `Authn` – Authentication flows (login/logout/session/MFA/rate-limits)
19//! - `Authz` – Authorization issues (permission format, collisions, hierarchy violations)
20//! - `Permissions` – Permission validation/collision concerns
21//! - `Codecs` – Codec/serialization problems (encode/decode/serialize/deserialize/validate)
22//! - `Jwt` – JWT processing (encode/decode/validate/refresh/revoke)
23//! - `Repositories` – Repository contract/operation failures by repository type
24//! - `Database` – Database driver/engine operation failures
25//! - `Hashing` – Hashing/verification problems (hash/verify/generate_salt/update_hash)
26//! - `Secrets` – Secret storage and verification (repo + hashing in secret flows)
27//!
28//! # Basic Example
29//! ```rust
30//! use axum_gate::errors::{Error, PermissionsError, Result, UserFriendlyError};
31//!
32//! fn do_permission_check(flag: bool) -> Result<()> {
33//!     if !flag {
34//!         let error = Error::Permissions(
35//!             PermissionsError::collision(42, vec!["read:alpha".into(), "read:beta".into()])
36//!         );
37//!         println!("User sees: {}", error.user_message());
38//!         println!("Developer sees: {}", error.developer_message());
39//!         return Err(error);
40//!     }
41//!     Ok(())
42//! }
43//! ```
44//!
45//! # Error Handling
46//! ```rust
47//! use axum_gate::errors::{Error, UserFriendlyError};
48//!
49//! fn handle_error(err: &Error) -> (String, String, String) {
50//!     (
51//!         err.user_message(),
52//!         err.developer_message(),
53//!         err.support_code()
54//!     )
55//! }
56//! ```
57
58use std::fmt;
59use thiserror::Error;
60
61// Category-based error re-exports for ergonomic imports.
62pub use crate::accounts::errors::{AccountOperation, AccountsError};
63pub use crate::authn::errors::{AuthenticationError, AuthnError};
64pub use crate::authz::errors::AuthzError;
65pub use crate::codecs::errors::{CodecOperation, CodecsError, JwtError, JwtOperation};
66pub use crate::hashing::errors::{HashingError, HashingOperation};
67pub use crate::permissions::errors::PermissionsError;
68pub use crate::repositories::errors::{
69    DatabaseError, DatabaseOperation, RepositoriesError, RepositoryOperation, RepositoryType,
70};
71pub use crate::secrets::errors::SecretError;
72
73/// Re-export of OAuth2Error for ergonomic imports.
74pub use crate::gate::oauth2::errors::OAuth2Error;
75/// Trait providing user-friendly error messaging at multiple levels.
76///
77/// This trait ensures all errors provide appropriate messages for different
78/// audiences while maintaining security and consistency.
79/// Provides a consistent, multi-level error messaging interface across the crate.
80///
81/// Implementors must supply user-safe text via `user_message` and richer context
82/// for logs and support via `developer_message` and `support_code`. The `severity`,
83/// `suggested_actions`, and `is_retryable` methods help downstream handling and UX.
84/// See each method for audience and usage guidance.
85pub trait UserFriendlyError: fmt::Display + fmt::Debug {
86    /// User-facing message that is clear, actionable, and non-technical.
87    ///
88    /// This message should:
89    /// - Use plain language that any user can understand
90    /// - Provide actionable guidance when possible
91    /// - Never leak sensitive information
92    /// - Be empathetic and helpful in tone
93    ///
94    /// # Examples
95    /// - "We're experiencing technical difficulties. Please try again in a moment."
96    /// - "Your session has expired. Please sign in again to continue."
97    /// - "There's an issue with your account. Please contact our support team."
98    fn user_message(&self) -> String;
99
100    /// Technical message with detailed information for developers and logs.
101    ///
102    /// This message should:
103    /// - Include precise technical details
104    /// - Provide context for debugging
105    /// - Include relevant identifiers and parameters
106    /// - Be structured for parsing by monitoring tools
107    fn developer_message(&self) -> String;
108
109    /// Unique support reference code for customer service and troubleshooting.
110    ///
111    /// This code should:
112    /// - Be unique and easily communicable
113    /// - Allow support teams to identify the exact error
114    /// - Not contain sensitive information
115    /// - Be consistent across error instances
116    fn support_code(&self) -> String;
117
118    /// Error severity level for proper handling and alerting.
119    fn severity(&self) -> ErrorSeverity;
120
121    /// Suggested user actions for resolving the error.
122    fn suggested_actions(&self) -> Vec<String> {
123        Vec::new()
124    }
125
126    /// Whether this error should be retryable by the user.
127    fn is_retryable(&self) -> bool {
128        false
129    }
130}
131
132/// Error severity levels for proper categorization and handling.
133#[derive(Debug, Clone, PartialEq, Eq)]
134pub enum ErrorSeverity {
135    /// Critical system error requiring immediate attention
136    Critical,
137    /// Error that prevents normal operation
138    Error,
139    /// Warning that may indicate a problem
140    Warning,
141    /// Informational message about an expected condition
142    Info,
143}
144
145/// Result type alias using our comprehensive Error type.
146///
147/// This provides a convenient way to return results from functions that can fail
148/// with any of the layer-specific errors defined in this module.
149///
150/// # Examples
151///
152/// ```rust
153/// use axum_gate::errors::{Result, Error, PermissionsError};
154///
155/// fn validate_account(user_id: &str) -> Result<()> {
156///     if user_id.is_empty() {
157///         return Err(Error::Permissions(PermissionsError::collision(
158///             12345,
159///             vec!["invalid".to_string()]
160///         )));
161///     }
162///     Ok(())
163/// }
164/// ```
165pub type Result<T> = std::result::Result<T, Error>;
166
167/// Root error type for the axum-gate library.
168///
169/// This enum represents all possible errors that can occur across different
170/// architectural layers, providing a unified error handling interface while
171/// maintaining clear separation of concerns.
172///
173/// Each error variant implements `UserFriendlyError` to provide appropriate
174/// messaging for different audiences while maintaining security and consistency.
175#[derive(Debug, Error)]
176pub enum Error {
177    /// Accounts category errors
178    #[error(transparent)]
179    Accounts(#[from] AccountsError),
180
181    /// Authentication category errors
182    #[error(transparent)]
183    Authn(#[from] AuthnError),
184
185    /// Authorization category errors
186    #[error(transparent)]
187    Authz(#[from] AuthzError),
188
189    /// Permissions category errors
190    #[error(transparent)]
191    Permissions(#[from] PermissionsError),
192
193    /// Codec/serialization category errors
194    #[error(transparent)]
195    Codecs(#[from] CodecsError),
196
197    /// JWT processing category errors
198    #[error(transparent)]
199    Jwt(#[from] JwtError),
200
201    /// Repository category errors
202    #[error(transparent)]
203    Repositories(#[from] RepositoriesError),
204
205    /// Database category errors
206    #[error(transparent)]
207    Database(#[from] DatabaseError),
208
209    /// Hashing/verification category errors
210    #[error(transparent)]
211    Hashing(#[from] HashingError),
212
213    /// Secret storage/category errors
214    #[error(transparent)]
215    Secrets(#[from] SecretError),
216
217    /// OAuth2 flow errors
218    #[error(transparent)]
219    OAuth2(#[from] crate::gate::oauth2::errors::OAuth2Error),
220}
221
222impl UserFriendlyError for Error {
223    fn user_message(&self) -> String {
224        match self {
225            Error::Accounts(err) => err.user_message(),
226            Error::Authn(err) => err.user_message(),
227            Error::Authz(err) => err.user_message(),
228            Error::Permissions(err) => err.user_message(),
229            Error::Codecs(err) => err.user_message(),
230            Error::Jwt(err) => err.user_message(),
231            Error::Repositories(err) => err.user_message(),
232            Error::Database(err) => err.user_message(),
233            Error::Hashing(err) => err.user_message(),
234            Error::Secrets(err) => err.user_message(),
235            Error::OAuth2(err) => err.user_message(),
236        }
237    }
238
239    fn developer_message(&self) -> String {
240        match self {
241            Error::Accounts(err) => err.developer_message(),
242            Error::Authn(err) => err.developer_message(),
243            Error::Authz(err) => err.developer_message(),
244            Error::Permissions(err) => err.developer_message(),
245            Error::Codecs(err) => err.developer_message(),
246            Error::Jwt(err) => err.developer_message(),
247            Error::Repositories(err) => err.developer_message(),
248            Error::Database(err) => err.developer_message(),
249            Error::Hashing(err) => err.developer_message(),
250            Error::Secrets(err) => err.developer_message(),
251            Error::OAuth2(err) => err.developer_message(),
252        }
253    }
254
255    fn support_code(&self) -> String {
256        match self {
257            Error::Accounts(err) => err.support_code(),
258            Error::Authn(err) => err.support_code(),
259            Error::Authz(err) => err.support_code(),
260            Error::Permissions(err) => err.support_code(),
261            Error::Codecs(err) => err.support_code(),
262            Error::Jwt(err) => err.support_code(),
263            Error::Repositories(err) => err.support_code(),
264            Error::Database(err) => err.support_code(),
265            Error::Hashing(err) => err.support_code(),
266            Error::Secrets(err) => err.support_code(),
267            Error::OAuth2(err) => err.support_code(),
268        }
269    }
270
271    fn severity(&self) -> ErrorSeverity {
272        match self {
273            Error::Accounts(err) => err.severity(),
274            Error::Authn(err) => err.severity(),
275            Error::Authz(err) => err.severity(),
276            Error::Permissions(err) => err.severity(),
277            Error::Codecs(err) => err.severity(),
278            Error::Jwt(err) => err.severity(),
279            Error::Repositories(err) => err.severity(),
280            Error::Database(err) => err.severity(),
281            Error::Hashing(err) => err.severity(),
282            Error::Secrets(err) => err.severity(),
283            Error::OAuth2(err) => err.severity(),
284        }
285    }
286
287    fn suggested_actions(&self) -> Vec<String> {
288        match self {
289            Error::Accounts(err) => err.suggested_actions(),
290            Error::Authn(err) => err.suggested_actions(),
291            Error::Authz(err) => err.suggested_actions(),
292            Error::Permissions(err) => err.suggested_actions(),
293            Error::Codecs(err) => err.suggested_actions(),
294            Error::Jwt(err) => err.suggested_actions(),
295            Error::Repositories(err) => err.suggested_actions(),
296            Error::Database(err) => err.suggested_actions(),
297            Error::Hashing(err) => err.suggested_actions(),
298            Error::Secrets(err) => err.suggested_actions(),
299            Error::OAuth2(err) => err.suggested_actions(),
300        }
301    }
302
303    fn is_retryable(&self) -> bool {
304        match self {
305            Error::Accounts(err) => err.is_retryable(),
306            Error::Authn(err) => err.is_retryable(),
307            Error::Authz(err) => err.is_retryable(),
308            Error::Permissions(err) => err.is_retryable(),
309            Error::Codecs(err) => err.is_retryable(),
310            Error::Jwt(err) => err.is_retryable(),
311            Error::Repositories(err) => err.is_retryable(),
312            Error::Database(err) => err.is_retryable(),
313            Error::Hashing(err) => err.is_retryable(),
314            Error::Secrets(err) => err.is_retryable(),
315            Error::OAuth2(err) => err.is_retryable(),
316        }
317    }
318}
319
320// External library error conversions
321#[cfg(feature = "storage-surrealdb")]
322impl From<surrealdb::Error> for Error {
323    fn from(err: surrealdb::Error) -> Self {
324        Error::Database(DatabaseError::with_context(
325            DatabaseOperation::Query,
326            format!("SurrealDB error: {}", err),
327            None,
328            None,
329        ))
330    }
331}
332
333// External library error conversions
334impl From<argon2::Error> for Error {
335    fn from(err: argon2::Error) -> Self {
336        Error::Hashing(HashingError::with_context(
337            HashingOperation::Hash,
338            format!("Argon2 error: {}", err),
339            Some("Argon2id".to_string()),
340            None,
341        ))
342    }
343}
344
345// Map cookie template builder validation errors into the crate-wide Error type.
346// We categorize these as codec/format issues since they reflect invalid configuration
347// for building a Cookie (shape/format contract violation).
348impl From<crate::cookie_template::CookieTemplateBuilderError> for Error {
349    fn from(err: crate::cookie_template::CookieTemplateBuilderError) -> Self {
350        Error::Codecs(CodecsError::codec_with_format(
351            CodecOperation::Encode,
352            format!("Invalid cookie template configuration: {}", err),
353            Some("cookie::CookieBuilder".to_string()),
354            Some("Invalid cookie settings".to_string()),
355        ))
356    }
357}
358
359#[cfg(test)]
360mod tests {
361    use crate::errors::{
362        AccountOperation, AccountsError, AuthenticationError, AuthnError, AuthzError,
363        CodecOperation, DatabaseError, DatabaseOperation, Error, ErrorSeverity, HashingOperation,
364        JwtOperation, RepositoriesError, RepositoryOperation, RepositoryType, UserFriendlyError,
365    };
366
367    #[test]
368    fn authz_error_permission_collision() {
369        let permissions = vec!["read:file".to_string(), "write:file".to_string()];
370        let error = Error::Authz(AuthzError::collision(123u64, permissions.clone()));
371
372        // Test error structure
373        match &error {
374            Error::Authz(AuthzError::PermissionCollision {
375                collision_count,
376                hash_id,
377                permissions: perms,
378            }) => {
379                assert_eq!(*collision_count, 2);
380                assert_eq!(*hash_id, 123u64);
381                assert_eq!(*perms, permissions);
382            }
383            _ => panic!("Expected PermissionCollision variant"),
384        }
385
386        // Test user-friendly messages
387        assert!(error.user_message().contains("technical issue"));
388        assert!(error.developer_message().contains("Permission collision"));
389        assert!(error.support_code().starts_with("AUTHZ-PERM-COLLISION-"));
390        assert_eq!(error.severity(), ErrorSeverity::Critical);
391        assert!(!error.suggested_actions().is_empty());
392    }
393
394    #[test]
395    fn authn_error_authentication() {
396        let auth_error = AuthenticationError::InvalidCredentials;
397        let error = Error::Authn(AuthnError::from_authentication(
398            auth_error,
399            Some("test context".to_string()),
400        ));
401
402        // Test error structure
403        match &error {
404            Error::Authn(AuthnError::Authentication { error, context }) => {
405                matches!(error, AuthenticationError::InvalidCredentials);
406                assert_eq!(*context, Some("test context".to_string()));
407            }
408            _ => panic!("Expected Authn::Authentication variant"),
409        }
410
411        // Test user-friendly messages
412        assert!(error.user_message().contains("username or password"));
413        assert!(error.developer_message().contains("Invalid credentials"));
414        assert_eq!(error.severity(), ErrorSeverity::Warning);
415        assert!(
416            error
417                .suggested_actions()
418                .iter()
419                .any(|action| action.contains("username") || action.contains("password"))
420        );
421    }
422
423    #[test]
424    fn database_error_query() {
425        let error = Error::Database(DatabaseError::new(
426            DatabaseOperation::Query,
427            "Connection failed",
428        ));
429
430        // Test error structure
431        match &error {
432            Error::Database(DatabaseError::Operation {
433                operation, message, ..
434            }) => {
435                matches!(operation, DatabaseOperation::Query);
436                assert_eq!(*message, "Connection failed");
437            }
438            _ => panic!("Expected Database::Operation variant"),
439        }
440
441        // Test user-friendly messages
442        assert!(error.user_message().contains("technical difficulties"));
443        assert!(error.developer_message().contains("Database"));
444        assert_eq!(error.severity(), ErrorSeverity::Error);
445        assert!(error.is_retryable());
446    }
447
448    #[test]
449    fn repositories_error_operation_failed() {
450        let error = Error::Repositories(RepositoriesError::operation_failed(
451            RepositoryType::Account,
452            RepositoryOperation::Insert,
453            "Insert failed",
454            Some("user-123".into()),
455            Some("insert_account".into()),
456        ));
457
458        // Test error structure
459        match &error {
460            Error::Repositories(RepositoriesError::OperationFailed {
461                repository,
462                operation,
463                message,
464                ..
465            }) => {
466                matches!(repository, RepositoryType::Account);
467                matches!(operation, RepositoryOperation::Insert);
468                assert_eq!(*message, "Insert failed");
469            }
470            _ => panic!("Expected Repositories::OperationFailed variant"),
471        }
472
473        // Test user-friendly messages
474        assert!(error.user_message().contains("account information"));
475        assert!(
476            error
477                .developer_message()
478                .contains("Repository operation failed")
479        );
480        assert!(
481            error.severity() == ErrorSeverity::Error || error.severity() == ErrorSeverity::Critical
482        );
483        assert!(error.is_retryable());
484    }
485
486    #[test]
487    fn error_display() {
488        let error = Error::Accounts(AccountsError::operation(
489            AccountOperation::Create,
490            "create failed",
491            Some("acc-1".into()),
492        ));
493        let display = format!("{}", error);
494        assert!(display.contains("Account operation"));
495
496        // Test all message levels
497        assert!(!error.user_message().is_empty());
498        assert!(!error.developer_message().is_empty());
499        assert!(!error.support_code().is_empty());
500        assert!(!matches!(error.severity(), ErrorSeverity::Info));
501    }
502
503    #[test]
504    fn operation_display() {
505        assert_eq!(format!("{}", AccountOperation::Create), "create");
506        assert_eq!(format!("{}", DatabaseOperation::Query), "query");
507        assert_eq!(format!("{}", JwtOperation::Encode), "encode");
508        assert_eq!(format!("{}", CodecOperation::Decode), "decode");
509        assert_eq!(format!("{}", HashingOperation::Verify), "verify");
510    }
511
512    #[test]
513    fn error_severity_levels() {
514        let authz_error = Error::Authz(AuthzError::collision(123, vec!["test".to_string()]));
515        assert_eq!(authz_error.severity(), ErrorSeverity::Critical);
516
517        // Test that all severity levels work
518        assert_ne!(ErrorSeverity::Critical, ErrorSeverity::Error);
519        assert_ne!(ErrorSeverity::Error, ErrorSeverity::Warning);
520        assert_ne!(ErrorSeverity::Warning, ErrorSeverity::Info);
521    }
522
523    #[test]
524    fn error_support_codes_are_unique() {
525        let authz_error = Error::Authz(AuthzError::collision(123, vec!["test".to_string()]));
526        let authn_error = Error::Authn(AuthnError::invalid_credentials(None));
527
528        assert_ne!(authz_error.support_code(), authn_error.support_code());
529        assert!(authz_error.support_code().starts_with("AUTHZ-"));
530        assert!(authn_error.support_code().starts_with("AUTHN-"));
531    }
532
533    #[test]
534    fn error_suggested_actions() {
535        let error = Error::Authn(AuthnError::invalid_credentials(None));
536        let actions = error.suggested_actions();
537        assert!(!actions.is_empty());
538        assert!(actions.iter().any(|action| action.contains("username")
539            || action.contains("password")
540            || action.contains("check")));
541    }
542}