microsandbox_server/
error.rs

1//! Error handling module for the microsandbox server.
2//!
3//! This module provides comprehensive error handling functionality including:
4//! - Custom error types for server operations
5//! - Error codes and responses for API communication
6//! - Authentication and authorization error handling
7//! - Validation error handling
8//!
9//! The module implements:
10//! - Error types with detailed error messages
11//! - HTTP status code mapping
12//! - Serializable error responses for API clients
13//! - Structured error codes for frontend handling
14
15use axum::{
16    http::StatusCode,
17    response::{IntoResponse, Response},
18    Json,
19};
20use microsandbox_utils::MicrosandboxUtilsError;
21use serde::{Deserialize, Serialize};
22use thiserror::Error;
23use tracing::error;
24
25//--------------------------------------------------------------------------------------------------
26// Types
27//--------------------------------------------------------------------------------------------------
28
29/// The result of microsandbox-server operations in general.
30pub type MicrosandboxServerResult<T> = Result<T, MicrosandboxServerError>;
31
32/// The result of server-related operations.
33pub type ServerResult<T> = Result<T, ServerError>;
34
35/// Error returned when an unexpected internal error occurs
36#[derive(Error, Debug)]
37pub enum MicrosandboxServerError {
38    /// Error returned when the server fails to start
39    #[error("Server failed to start: {0}")]
40    StartError(String),
41
42    /// Error returned when the server fails to stop
43    #[error("Server failed to stop: {0}")]
44    StopError(String),
45
46    /// Error returned when the server key fails to generate
47    #[error("Server key failed to generate: {0}")]
48    KeyGenError(String),
49
50    /// Error returned when the server configuration fails
51    #[error("Server configuration failed: {0}")]
52    ConfigError(String),
53
54    /// Error returned when an I/O error occurs
55    #[error(transparent)]
56    IoError(#[from] std::io::Error),
57
58    /// Error returned from the microsandbox-utils crate
59    #[error(transparent)]
60    Utils(#[from] MicrosandboxUtilsError),
61}
62
63/// Represents all possible errors that can occur in the application
64#[derive(Error, Debug)]
65pub enum ServerError {
66    /// Error returned when authentication fails
67    #[error("Authentication failed: {0}")]
68    Authentication(AuthenticationError),
69
70    /// Error returned when a user doesn't have permission to access a resource
71    #[error("Authorization failed: {0}")]
72    AuthorizationError(AuthorizationError),
73
74    /// Error returned when a requested resource is not found
75    #[error("Resource not found: {0}")]
76    NotFound(String),
77
78    /// Error returned when a database operation fails
79    #[error("Database error: {0}")]
80    DatabaseError(String),
81
82    /// Error returned when request validation fails (e.g., invalid input format)
83    #[error("Validation error: {0}")]
84    ValidationError(ValidationError),
85
86    /// Error returned when an unexpected internal error occurs
87    #[error("Internal server error: {0}")]
88    InternalError(String),
89}
90
91/// Error code structure to be sent to frontend
92#[derive(Debug, Serialize, Deserialize, Clone, Copy, PartialEq, Eq)]
93pub enum ErrorCode {
94    // Authentication error codes
95    /// Error returned when credentials provided don't match our records
96    InvalidCredentials = 1001,
97    /// Error returned when a user attempts to login with an unconfirmed email
98    EmailNotConfirmed = 1002,
99    /// Error returned when there have been too many failed login attempts
100    TooManyLoginAttempts = 1003,
101    /// Error returned when an authentication token is invalid
102    InvalidToken = 1004,
103    /// Error returned when an authentication token has expired
104    ExpiredToken = 1005,
105    /// Error returned when a token is required but not provided
106    TokenRequired = 1006,
107    /// Error returned when attempting to register with an email that already exists
108    EmailAlreadyExists = 1007,
109    /// Error returned when a user tries to sign in with password but should use Google
110    UseGoogleLogin = 1008,
111    /// Error returned when a user tries to sign in with password but should use GitHub
112    UseGithubLogin = 1009,
113    /// Error returned when a user tries to use OAuth but should use email/password
114    UseEmailLogin = 1010,
115    /// Error returned when a user's email is not verified with their OAuth provider
116    EmailNotVerified = 1011,
117
118    // Validation error codes
119    /// Error returned when input fails validation rules
120    InvalidInput = 2001,
121    /// Error returned when password doesn't meet strength requirements
122    PasswordTooWeak = 2002,
123    /// Error returned when email format is invalid
124    EmailInvalid = 2003,
125    /// Error returned when a confirmation token is invalid or has expired
126    InvalidOrExpiredConfirmationToken = 2004,
127
128    // Authorization error codes
129    /// Error returned when a user is denied access to a resource
130    AccessDenied = 3001,
131    /// Error returned when a user doesn't have sufficient permissions for an action
132    InsufficientPermissions = 3002,
133
134    // Resource error codes
135    /// Error returned when a requested resource cannot be found
136    ResourceNotFound = 4001,
137
138    // Server error codes
139    /// Error returned when a database operation fails
140    DatabaseError = 5001,
141    /// Error returned when an unexpected server error occurs
142    InternalServerError = 5002,
143}
144
145/// Represents different types of authentication failures
146#[derive(Error, Debug)]
147pub enum AuthenticationError {
148    /// Security-sensitive authentication failures that shouldn't reveal details
149    #[error("Invalid credentials")]
150    InvalidCredentials(String),
151
152    /// User-facing authentication errors that can be shown directly
153    #[error("{0}")]
154    ClientError(String),
155
156    /// Email not confirmed
157    #[error("Email not confirmed")]
158    EmailNotConfirmed,
159
160    /// Too many login attempts
161    #[error("Too many login attempts")]
162    TooManyAttempts,
163
164    /// Invalid or expired token
165    #[error("Invalid or expired token")]
166    InvalidToken(String),
167
168    /// Email already registered
169    #[error("Email already registered")]
170    EmailAlreadyExists,
171
172    /// Should use Google login instead
173    #[error("Use Google login")]
174    UseGoogleLogin,
175
176    /// Should use GitHub login instead
177    #[error("Use GitHub login")]
178    UseGithubLogin,
179
180    /// Should use email/password login instead
181    #[error("Use email/password login")]
182    UseEmailLogin,
183
184    /// Email not verified with provider
185    #[error("Email not verified")]
186    EmailNotVerified,
187}
188
189/// Represents validation errors
190#[derive(Error, Debug)]
191pub enum ValidationError {
192    /// Generic validation error
193    #[error("{0}")]
194    InvalidInput(String),
195
196    /// Password not strong enough
197    #[error("Password is too weak")]
198    PasswordTooWeak(String),
199
200    /// Email format invalid
201    #[error("Email is invalid")]
202    EmailInvalid(String),
203
204    /// Invalid or expired confirmation token
205    #[error("Invalid or expired confirmation token")]
206    InvalidConfirmationToken,
207}
208
209/// Represents authorization errors
210#[derive(Error, Debug)]
211pub enum AuthorizationError {
212    /// Access denied
213    #[error("Access denied")]
214    AccessDenied(String),
215
216    /// Insufficient permissions
217    #[error("Insufficient permissions")]
218    InsufficientPermissions(String),
219}
220
221/// Response structure for errors
222#[derive(Serialize)]
223struct ErrorResponse {
224    error: String,
225    code: Option<u32>,
226}
227
228//--------------------------------------------------------------------------------------------------
229// Trait Implementations
230//--------------------------------------------------------------------------------------------------
231
232impl IntoResponse for ServerError {
233    /// Converts the ServerError into an HTTP response with appropriate status code
234    /// and JSON error message.
235    ///
236    /// ## Returns
237    ///
238    /// Returns an HTTP response containing:
239    /// - Appropriate HTTP status code based on the error type
240    /// - JSON body with an "error" field containing the error message
241    /// - And an optional "code" field with a numeric error code for the frontend
242    fn into_response(self) -> Response {
243        // Log the actual error with details
244        error!(error = ?self, "API error occurred");
245
246        let (status, error_message, error_code) = match self {
247            ServerError::Authentication(auth_error) => {
248                match auth_error {
249                    AuthenticationError::InvalidCredentials(_details) => {
250                        // Generic message for security-sensitive auth failures
251                        error!(details = ?_details, "Authentication error");
252                        (StatusCode::UNAUTHORIZED, "Invalid credentials".to_string(), Some(ErrorCode::InvalidCredentials as u32))
253                    }
254                    AuthenticationError::ClientError(details) => {
255                        // Safe to show these messages to users
256                        error!(details = ?details, "User-facing authentication error");
257                        (StatusCode::UNAUTHORIZED, details, None)
258                    }
259                    AuthenticationError::EmailNotConfirmed => {
260                        (StatusCode::UNAUTHORIZED, "Email not confirmed".to_string(), Some(ErrorCode::EmailNotConfirmed as u32))
261                    }
262                    AuthenticationError::TooManyAttempts => {
263                        (StatusCode::TOO_MANY_REQUESTS, "Too many login attempts, please try again later".to_string(), Some(ErrorCode::TooManyLoginAttempts as u32))
264                    }
265                    AuthenticationError::InvalidToken(details) => {
266                        error!(details = ?details, "Invalid token");
267                        (StatusCode::UNAUTHORIZED, "Invalid or expired token".to_string(), Some(ErrorCode::InvalidToken as u32))
268                    }
269                    AuthenticationError::EmailAlreadyExists => {
270                        (StatusCode::CONFLICT, "Email already registered".to_string(), Some(ErrorCode::EmailAlreadyExists as u32))
271                    }
272                    AuthenticationError::UseGoogleLogin => {
273                        (StatusCode::UNAUTHORIZED, "This email is registered with Google. Please use 'Sign in with Google' instead.".to_string(), Some(ErrorCode::UseGoogleLogin as u32))
274                    }
275                    AuthenticationError::UseGithubLogin => {
276                        (StatusCode::UNAUTHORIZED, "This email is registered with GitHub. Please use 'Sign in with GitHub' instead.".to_string(), Some(ErrorCode::UseGithubLogin as u32))
277                    }
278                    AuthenticationError::UseEmailLogin => {
279                        (StatusCode::UNAUTHORIZED, "This email is already registered. Please login with your password.".to_string(), Some(ErrorCode::UseEmailLogin as u32))
280                    }
281                    AuthenticationError::EmailNotVerified => {
282                        (StatusCode::UNAUTHORIZED, "Email not verified with the provider".to_string(), Some(ErrorCode::EmailNotVerified as u32))
283                    }
284                }
285            }
286            ServerError::AuthorizationError(auth_error) => match auth_error {
287                AuthorizationError::AccessDenied(details) => {
288                    error!(details = ?details, "Access denied");
289                    (
290                        StatusCode::FORBIDDEN,
291                        "Access denied".to_string(),
292                        Some(ErrorCode::AccessDenied as u32),
293                    )
294                }
295                AuthorizationError::InsufficientPermissions(details) => {
296                    error!(details = ?details, "Insufficient permissions");
297                    (
298                        StatusCode::FORBIDDEN,
299                        "Insufficient permissions".to_string(),
300                        Some(ErrorCode::InsufficientPermissions as u32),
301                    )
302                }
303            },
304            ServerError::NotFound(details) => (
305                StatusCode::NOT_FOUND,
306                details,
307                Some(ErrorCode::ResourceNotFound as u32),
308            ),
309            ServerError::DatabaseError(details) => {
310                error!(details = ?details, "Database error");
311                (
312                    StatusCode::INTERNAL_SERVER_ERROR,
313                    "Internal server error".to_string(),
314                    Some(ErrorCode::DatabaseError as u32),
315                )
316            }
317            ServerError::ValidationError(validation_error) => match validation_error {
318                ValidationError::InvalidInput(details) => (
319                    StatusCode::BAD_REQUEST,
320                    details,
321                    Some(ErrorCode::InvalidInput as u32),
322                ),
323                ValidationError::PasswordTooWeak(details) => (
324                    StatusCode::BAD_REQUEST,
325                    details,
326                    Some(ErrorCode::PasswordTooWeak as u32),
327                ),
328                ValidationError::EmailInvalid(details) => (
329                    StatusCode::BAD_REQUEST,
330                    details,
331                    Some(ErrorCode::EmailInvalid as u32),
332                ),
333                ValidationError::InvalidConfirmationToken => (
334                    StatusCode::BAD_REQUEST,
335                    "Invalid or expired confirmation token".to_string(),
336                    Some(ErrorCode::InvalidOrExpiredConfirmationToken as u32),
337                ),
338            },
339            ServerError::InternalError(details) => {
340                error!(details = ?details, "Internal error");
341                (
342                    StatusCode::INTERNAL_SERVER_ERROR,
343                    "Internal server error".to_string(),
344                    Some(ErrorCode::InternalServerError as u32),
345                )
346            }
347        };
348
349        let body = Json(ErrorResponse {
350            error: error_message,
351            code: error_code,
352        });
353
354        (status, body).into_response()
355    }
356}