Skip to main content

shared/
error.rs

1use chrono::{DateTime, Duration, Utc};
2
3pub type Result<T> = core::result::Result<T, CoreError>;
4
5// ── Top-level core Error ─────────────────────────────────────────────────────
6// NOTE: no actix_web, no HTTP status codes — that belongs in `api`
7
8#[derive(thiserror::Error, Debug)]
9pub enum CoreError {
10    #[error("bad request: {0}")]
11    BadRequest(String),
12    #[error(transparent)]
13    Validation(#[from] ValidationError),
14    #[error(transparent)]
15    Unauthenticated(#[from] AuthError),
16    #[error(transparent)]
17    Forbidden(#[from] ForbiddenReason),
18    #[error(transparent)]
19    NotFound(#[from] ResourceKind),
20    #[error(transparent)]
21    Conflict(#[from] ConflictReason),
22    #[error("unsupported media type: {0}")]
23    UnsupportedMediaType(String),
24    #[error("rate limit exceeded: {limit} requests allowed per {window:?}")]
25    RateLimitExceeded { limit: u32, window: Duration },
26    #[error(transparent)]
27    Internal(#[from] InternalError),
28}
29
30// ── Supporting types ────────────────────────────────────────────────────────
31
32#[derive(Debug, Clone)]
33pub enum CredentialField {
34    Username,
35    Email,
36    Password,
37    EmailOrPassword,
38    Token,
39    ApiKey,
40    ObjectId,
41}
42
43#[derive(Debug, Clone)]
44pub enum TokenErrorType {
45    Token,
46    AccessToken,
47    RefreshToken,
48    SessionToken,
49    PasswordResetToken,
50    EmailVerificationToken,
51}
52
53// ── Sub-errors ───────────────────────────────────────────────────────────────
54
55#[derive(thiserror::Error, Debug)]
56pub enum AuthError {
57    #[error("{token_type:?} expired at {expired_at}")]
58    TokenExpired {
59        token_type: TokenErrorType,
60        expired_at: DateTime<Utc>,
61    },
62    #[error("{token_type:?} token has an invalid signature")]
63    TokenInvalidSignature { token_type: TokenErrorType },
64    #[error("{token_type:?} token has an invalid audience")]
65    TokenInvalidAudience { token_type: TokenErrorType },
66    #[error("{token_type:?} token has an invalid issuer")]
67    TokenInvalidIssuer { token_type: TokenErrorType },
68    #[error("{token_type:?} token uses an invalid or missing algorithm")]
69    TokenInvalidAlgorithm { token_type: TokenErrorType },
70    #[error("{token_type:?} token is malformed")]
71    TokenMalformed { token_type: TokenErrorType },
72    #[error("{token_type:?} has already been used")]
73    TokenReplay { token_type: TokenErrorType },
74    #[error("{token_type:?} is invalid")]
75    TokenInvalid { token_type: TokenErrorType },
76    #[error("invalid credentials for {field:?}")]
77    InvalidCredentials { field: CredentialField },
78    #[error("account is not verified")]
79    AccountNotVerified,
80    #[error("jwt is not configured")]
81    JwtNotConfigured,
82}
83
84#[derive(thiserror::Error, Debug)]
85pub enum ForbiddenReason {
86    #[error("insufficient permissions to perform this action")]
87    InsufficientPermissions,
88    #[error("account has been suspended")]
89    AccountSuspended,
90}
91
92#[derive(thiserror::Error, Debug)]
93pub enum ResourceKind {
94    #[error("user not found (id: {id:?}, email: {email:?})")]
95    User {
96        id: Option<String>,
97        email: Option<String>,
98    },
99    #[error("role not found (id: {id:?})")]
100    Role { id: Option<String> },
101    #[error("{token_type:?} not found")]
102    Token { token_type: TokenErrorType },
103}
104
105#[derive(thiserror::Error, Debug)]
106pub enum ConflictReason {
107    #[error("{field:?} already exists")]
108    AlreadyExists { field: CredentialField },
109}
110
111#[derive(thiserror::Error, Debug)]
112pub enum ValidationError {
113    #[error("malformed {field:?}")]
114    Malformed { field: CredentialField },
115    #[error("missing required field: {field:?}")]
116    Missing { field: CredentialField },
117    #[error("invalid value for '{0}'")]
118    Invalid(String),
119}
120
121#[derive(thiserror::Error, Debug)]
122pub enum InternalError {
123    #[error("database error: {0}")]
124    Database(String),
125    #[error("hashing failure")]
126    Hashing,
127    #[error("failed to create {token_type:?}")]
128    TokenCreation { token_type: TokenErrorType },
129    #[error("failed to deliver email to {to}")]
130    EmailDelivery { to: String },
131    #[error("TLS configuration error at {path}")]
132    Tls { path: String },
133    #[error("I/O error: {0}")]
134    Io(String),
135    #[error("serialization error: {0}")]
136    Serialization(String),
137    #[error("cache error: {0}")]
138    Cache(String),
139    #[error("session error: {0}")]
140    Session(String),
141    #[error("JWT error: {0}")]
142    Jwt(String),
143    #[error("missing app data: {0}")]
144    MissingConfiguration(String),
145    #[error("missing configuration field: {field} - {reason}")]
146    MissingField { field: String, reason: String },
147    #[error("invalid config")]
148    InvalidConfig(Vec<CoreError>), // store messages, not Error — avoids recursive type
149}
150
151// ── ValidationError From impls ─────────────────────────────────────────────────
152impl From<validator::ValidationErrors> for ValidationError {
153    fn from(e: validator::ValidationErrors) -> Self {
154        ValidationError::Invalid(e.to_string())
155    }
156}
157
158impl From<validator::ValidationError> for ValidationError {
159    fn from(e: validator::ValidationError) -> Self {
160        ValidationError::Invalid(e.to_string())
161    }
162}
163
164// ── InternalError From impls ─────────────────────────────────────────────────
165impl From<std::io::Error> for InternalError {
166    fn from(e: std::io::Error) -> Self {
167        InternalError::Io(e.to_string())
168    }
169}
170impl From<std::env::VarError> for InternalError {
171    fn from(e: std::env::VarError) -> Self {
172        InternalError::Io(e.to_string())
173    }
174}
175
176impl From<sqlx::Error> for InternalError {
177    fn from(e: sqlx::Error) -> Self {
178        InternalError::Database(e.to_string())
179    }
180}
181impl From<sqlx::migrate::MigrateError> for InternalError {
182    fn from(e: sqlx::migrate::MigrateError) -> Self {
183        InternalError::Database(e.to_string())
184    }
185}
186
187impl From<mongodb::error::Error> for InternalError {
188    fn from(e: mongodb::error::Error) -> Self {
189        InternalError::Database(e.to_string())
190    }
191}
192impl From<mongodb::bson::ser::Error> for InternalError {
193    fn from(e: mongodb::bson::ser::Error) -> Self {
194        InternalError::Database(e.to_string())
195    }
196}
197
198impl From<redis::RedisError> for InternalError {
199    fn from(e: redis::RedisError) -> Self {
200        InternalError::Cache(e.to_string())
201    }
202}
203impl From<memcache::MemcacheError> for InternalError {
204    fn from(e: memcache::MemcacheError) -> Self {
205        InternalError::Cache(e.to_string())
206    }
207}
208impl From<serde_json::Error> for InternalError {
209    fn from(e: serde_json::Error) -> Self {
210        InternalError::Serialization(e.to_string())
211    }
212}
213impl From<serde_yaml::Error> for InternalError {
214    fn from(e: serde_yaml::Error) -> Self {
215        InternalError::Serialization(e.to_string())
216    }
217}
218impl From<config::ConfigError> for InternalError {
219    fn from(e: config::ConfigError) -> Self {
220        InternalError::Serialization(e.to_string())
221    }
222}
223
224impl From<jsonwebtoken::errors::Error> for InternalError {
225    fn from(e: jsonwebtoken::errors::Error) -> Self {
226        InternalError::Jwt(e.to_string())
227    }
228}
229
230// Convenience From impls so callers can use `?` without going through InternalError explicitly
231
232impl From<std::io::Error> for CoreError {
233    fn from(e: std::io::Error) -> Self {
234        CoreError::Internal(e.into())
235    }
236}
237impl From<std::env::VarError> for CoreError {
238    fn from(e: std::env::VarError) -> Self {
239        CoreError::Internal(e.into())
240    }
241}
242
243impl From<sqlx::Error> for CoreError {
244    fn from(e: sqlx::Error) -> Self {
245        CoreError::Internal(e.into())
246    }
247}
248impl From<sqlx::migrate::MigrateError> for CoreError {
249    fn from(e: sqlx::migrate::MigrateError) -> Self {
250        CoreError::Internal(e.into())
251    }
252}
253
254impl From<mongodb::error::Error> for CoreError {
255    fn from(e: mongodb::error::Error) -> Self {
256        CoreError::Internal(e.into())
257    }
258}
259impl From<mongodb::bson::ser::Error> for CoreError {
260    fn from(e: mongodb::bson::ser::Error) -> Self {
261        CoreError::Internal(e.into())
262    }
263}
264
265impl From<redis::RedisError> for CoreError {
266    fn from(e: redis::RedisError) -> Self {
267        CoreError::Internal(e.into())
268    }
269}
270impl From<memcache::MemcacheError> for CoreError {
271    fn from(e: memcache::MemcacheError) -> Self {
272        CoreError::Internal(e.into())
273    }
274}
275
276impl From<serde_json::Error> for CoreError {
277    fn from(e: serde_json::Error) -> Self {
278        CoreError::Internal(e.into())
279    }
280}
281impl From<serde_yaml::Error> for CoreError {
282    fn from(e: serde_yaml::Error) -> Self {
283        CoreError::Internal(e.into())
284    }
285}
286impl From<config::ConfigError> for CoreError {
287    fn from(e: config::ConfigError) -> Self {
288        CoreError::Internal(e.into())
289    }
290}
291
292// JWT maps to Unauthenticated, not Internal — keep this behaviour from your original
293impl From<jsonwebtoken::errors::Error> for CoreError {
294    fn from(_: jsonwebtoken::errors::Error) -> Self {
295        CoreError::Unauthenticated(AuthError::TokenInvalid {
296            token_type: TokenErrorType::AccessToken,
297        })
298    }
299}
300
301impl From<validator::ValidationErrors> for CoreError {
302    fn from(e: validator::ValidationErrors) -> Self {
303        CoreError::Validation(e.into())
304    }
305}
306impl From<validator::ValidationError> for CoreError {
307    fn from(e: validator::ValidationError) -> Self {
308        CoreError::Validation(e.into())
309    }
310}
311
312impl From<CoreError> for std::io::Error {
313    fn from(err: CoreError) -> Self {
314        std::io::Error::other(err.to_string())
315    }
316}