1use 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
25pub type MicrosandboxServerResult<T> = Result<T, MicrosandboxServerError>;
31
32pub type ServerResult<T> = Result<T, ServerError>;
34
35#[derive(Error, Debug)]
37pub enum MicrosandboxServerError {
38 #[error("Server failed to start: {0}")]
40 StartError(String),
41
42 #[error("Server failed to stop: {0}")]
44 StopError(String),
45
46 #[error("Server key failed to generate: {0}")]
48 KeyGenError(String),
49
50 #[error("Server configuration failed: {0}")]
52 ConfigError(String),
53
54 #[error(transparent)]
56 IoError(#[from] std::io::Error),
57
58 #[error(transparent)]
60 Utils(#[from] MicrosandboxUtilsError),
61}
62
63#[derive(Error, Debug)]
65pub enum ServerError {
66 #[error("Authentication failed: {0}")]
68 Authentication(AuthenticationError),
69
70 #[error("Authorization failed: {0}")]
72 AuthorizationError(AuthorizationError),
73
74 #[error("Resource not found: {0}")]
76 NotFound(String),
77
78 #[error("Database error: {0}")]
80 DatabaseError(String),
81
82 #[error("Validation error: {0}")]
84 ValidationError(ValidationError),
85
86 #[error("Internal server error: {0}")]
88 InternalError(String),
89}
90
91#[derive(Debug, Serialize, Deserialize, Clone, Copy, PartialEq, Eq)]
93pub enum ErrorCode {
94 InvalidCredentials = 1001,
97 EmailNotConfirmed = 1002,
99 TooManyLoginAttempts = 1003,
101 InvalidToken = 1004,
103 ExpiredToken = 1005,
105 TokenRequired = 1006,
107 EmailAlreadyExists = 1007,
109 UseGoogleLogin = 1008,
111 UseGithubLogin = 1009,
113 UseEmailLogin = 1010,
115 EmailNotVerified = 1011,
117
118 InvalidInput = 2001,
121 PasswordTooWeak = 2002,
123 EmailInvalid = 2003,
125 InvalidOrExpiredConfirmationToken = 2004,
127
128 AccessDenied = 3001,
131 InsufficientPermissions = 3002,
133
134 ResourceNotFound = 4001,
137
138 DatabaseError = 5001,
141 InternalServerError = 5002,
143}
144
145#[derive(Error, Debug)]
147pub enum AuthenticationError {
148 #[error("Invalid credentials")]
150 InvalidCredentials(String),
151
152 #[error("{0}")]
154 ClientError(String),
155
156 #[error("Email not confirmed")]
158 EmailNotConfirmed,
159
160 #[error("Too many login attempts")]
162 TooManyAttempts,
163
164 #[error("Invalid or expired token")]
166 InvalidToken(String),
167
168 #[error("Email already registered")]
170 EmailAlreadyExists,
171
172 #[error("Use Google login")]
174 UseGoogleLogin,
175
176 #[error("Use GitHub login")]
178 UseGithubLogin,
179
180 #[error("Use email/password login")]
182 UseEmailLogin,
183
184 #[error("Email not verified")]
186 EmailNotVerified,
187}
188
189#[derive(Error, Debug)]
191pub enum ValidationError {
192 #[error("{0}")]
194 InvalidInput(String),
195
196 #[error("Password is too weak")]
198 PasswordTooWeak(String),
199
200 #[error("Email is invalid")]
202 EmailInvalid(String),
203
204 #[error("Invalid or expired confirmation token")]
206 InvalidConfirmationToken,
207}
208
209#[derive(Error, Debug)]
211pub enum AuthorizationError {
212 #[error("Access denied")]
214 AccessDenied(String),
215
216 #[error("Insufficient permissions")]
218 InsufficientPermissions(String),
219}
220
221#[derive(Serialize)]
223struct ErrorResponse {
224 error: String,
225 code: Option<u32>,
226}
227
228impl IntoResponse for ServerError {
233 fn into_response(self) -> Response {
243 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 error!(details = ?_details, "Authentication error");
252 (StatusCode::UNAUTHORIZED, "Invalid credentials".to_string(), Some(ErrorCode::InvalidCredentials as u32))
253 }
254 AuthenticationError::ClientError(details) => {
255 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}