use axum::{
http::StatusCode,
response::{IntoResponse, Response},
Json,
};
use microsandbox_utils::MicrosandboxUtilsError;
use serde::{Deserialize, Serialize};
use thiserror::Error;
use tracing::error;
pub type MicrosandboxServerResult<T> = Result<T, MicrosandboxServerError>;
pub type ServerResult<T> = Result<T, ServerError>;
#[derive(Error, Debug)]
pub enum MicrosandboxServerError {
#[error("Server failed to start: {0}")]
StartError(String),
#[error("Server failed to stop: {0}")]
StopError(String),
#[error("Server key failed to generate: {0}")]
KeyGenError(String),
#[error("Server configuration failed: {0}")]
ConfigError(String),
#[error(transparent)]
IoError(#[from] std::io::Error),
#[error(transparent)]
Utils(#[from] MicrosandboxUtilsError),
}
#[derive(Error, Debug)]
pub enum ServerError {
#[error("Authentication failed: {0}")]
Authentication(AuthenticationError),
#[error("Authorization failed: {0}")]
AuthorizationError(AuthorizationError),
#[error("Resource not found: {0}")]
NotFound(String),
#[error("Database error: {0}")]
DatabaseError(String),
#[error("Validation error: {0}")]
ValidationError(ValidationError),
#[error("Internal server error: {0}")]
InternalError(String),
}
#[derive(Debug, Serialize, Deserialize, Clone, Copy, PartialEq, Eq)]
pub enum ErrorCode {
InvalidCredentials = 1001,
EmailNotConfirmed = 1002,
TooManyLoginAttempts = 1003,
InvalidToken = 1004,
ExpiredToken = 1005,
TokenRequired = 1006,
EmailAlreadyExists = 1007,
UseGoogleLogin = 1008,
UseGithubLogin = 1009,
UseEmailLogin = 1010,
EmailNotVerified = 1011,
InvalidInput = 2001,
PasswordTooWeak = 2002,
EmailInvalid = 2003,
InvalidOrExpiredConfirmationToken = 2004,
AccessDenied = 3001,
InsufficientPermissions = 3002,
ResourceNotFound = 4001,
DatabaseError = 5001,
InternalServerError = 5002,
}
#[derive(Error, Debug)]
pub enum AuthenticationError {
#[error("Invalid credentials")]
InvalidCredentials(String),
#[error("{0}")]
ClientError(String),
#[error("Email not confirmed")]
EmailNotConfirmed,
#[error("Too many login attempts")]
TooManyAttempts,
#[error("Invalid or expired token")]
InvalidToken(String),
#[error("Email already registered")]
EmailAlreadyExists,
#[error("Use Google login")]
UseGoogleLogin,
#[error("Use GitHub login")]
UseGithubLogin,
#[error("Use email/password login")]
UseEmailLogin,
#[error("Email not verified")]
EmailNotVerified,
}
#[derive(Error, Debug)]
pub enum ValidationError {
#[error("{0}")]
InvalidInput(String),
#[error("Password is too weak")]
PasswordTooWeak(String),
#[error("Email is invalid")]
EmailInvalid(String),
#[error("Invalid or expired confirmation token")]
InvalidConfirmationToken,
}
#[derive(Error, Debug)]
pub enum AuthorizationError {
#[error("Access denied")]
AccessDenied(String),
#[error("Insufficient permissions")]
InsufficientPermissions(String),
}
#[derive(Serialize)]
struct ErrorResponse {
error: String,
code: Option<u32>,
}
impl IntoResponse for ServerError {
fn into_response(self) -> Response {
error!(error = ?self, "API error occurred");
let (status, error_message, error_code) = match self {
ServerError::Authentication(auth_error) => {
match auth_error {
AuthenticationError::InvalidCredentials(_details) => {
error!(details = ?_details, "Authentication error");
(StatusCode::UNAUTHORIZED, "Invalid credentials".to_string(), Some(ErrorCode::InvalidCredentials as u32))
}
AuthenticationError::ClientError(details) => {
error!(details = ?details, "User-facing authentication error");
(StatusCode::UNAUTHORIZED, details, None)
}
AuthenticationError::EmailNotConfirmed => {
(StatusCode::UNAUTHORIZED, "Email not confirmed".to_string(), Some(ErrorCode::EmailNotConfirmed as u32))
}
AuthenticationError::TooManyAttempts => {
(StatusCode::TOO_MANY_REQUESTS, "Too many login attempts, please try again later".to_string(), Some(ErrorCode::TooManyLoginAttempts as u32))
}
AuthenticationError::InvalidToken(details) => {
error!(details = ?details, "Invalid token");
(StatusCode::UNAUTHORIZED, "Invalid or expired token".to_string(), Some(ErrorCode::InvalidToken as u32))
}
AuthenticationError::EmailAlreadyExists => {
(StatusCode::CONFLICT, "Email already registered".to_string(), Some(ErrorCode::EmailAlreadyExists as u32))
}
AuthenticationError::UseGoogleLogin => {
(StatusCode::UNAUTHORIZED, "This email is registered with Google. Please use 'Sign in with Google' instead.".to_string(), Some(ErrorCode::UseGoogleLogin as u32))
}
AuthenticationError::UseGithubLogin => {
(StatusCode::UNAUTHORIZED, "This email is registered with GitHub. Please use 'Sign in with GitHub' instead.".to_string(), Some(ErrorCode::UseGithubLogin as u32))
}
AuthenticationError::UseEmailLogin => {
(StatusCode::UNAUTHORIZED, "This email is already registered. Please login with your password.".to_string(), Some(ErrorCode::UseEmailLogin as u32))
}
AuthenticationError::EmailNotVerified => {
(StatusCode::UNAUTHORIZED, "Email not verified with the provider".to_string(), Some(ErrorCode::EmailNotVerified as u32))
}
}
}
ServerError::AuthorizationError(auth_error) => match auth_error {
AuthorizationError::AccessDenied(details) => {
error!(details = ?details, "Access denied");
(
StatusCode::FORBIDDEN,
"Access denied".to_string(),
Some(ErrorCode::AccessDenied as u32),
)
}
AuthorizationError::InsufficientPermissions(details) => {
error!(details = ?details, "Insufficient permissions");
(
StatusCode::FORBIDDEN,
"Insufficient permissions".to_string(),
Some(ErrorCode::InsufficientPermissions as u32),
)
}
},
ServerError::NotFound(details) => (
StatusCode::NOT_FOUND,
details,
Some(ErrorCode::ResourceNotFound as u32),
),
ServerError::DatabaseError(details) => {
error!(details = ?details, "Database error");
(
StatusCode::INTERNAL_SERVER_ERROR,
"Internal server error".to_string(),
Some(ErrorCode::DatabaseError as u32),
)
}
ServerError::ValidationError(validation_error) => match validation_error {
ValidationError::InvalidInput(details) => (
StatusCode::BAD_REQUEST,
details,
Some(ErrorCode::InvalidInput as u32),
),
ValidationError::PasswordTooWeak(details) => (
StatusCode::BAD_REQUEST,
details,
Some(ErrorCode::PasswordTooWeak as u32),
),
ValidationError::EmailInvalid(details) => (
StatusCode::BAD_REQUEST,
details,
Some(ErrorCode::EmailInvalid as u32),
),
ValidationError::InvalidConfirmationToken => (
StatusCode::BAD_REQUEST,
"Invalid or expired confirmation token".to_string(),
Some(ErrorCode::InvalidOrExpiredConfirmationToken as u32),
),
},
ServerError::InternalError(details) => {
error!(details = ?details, "Internal error");
(
StatusCode::INTERNAL_SERVER_ERROR,
"Internal server error".to_string(),
Some(ErrorCode::InternalServerError as u32),
)
}
};
let body = Json(ErrorResponse {
error: error_message,
code: error_code,
});
(status, body).into_response()
}
}