197 lines
5.4 KiB
Rust
197 lines
5.4 KiB
Rust
//! API error handling
|
|
//!
|
|
//! Maps domain errors to HTTP responses
|
|
|
|
use axum::{
|
|
Json,
|
|
http::StatusCode,
|
|
response::{IntoResponse, Response},
|
|
};
|
|
use serde::Serialize;
|
|
use thiserror::Error;
|
|
|
|
use domain::DomainError;
|
|
|
|
/// API-level errors
|
|
#[derive(Debug, Error)]
|
|
pub enum ApiError {
|
|
#[error("{0}")]
|
|
Domain(#[from] DomainError),
|
|
|
|
#[error("Validation error: {0}")]
|
|
Validation(String),
|
|
|
|
#[error("Internal server error")]
|
|
Internal(String),
|
|
|
|
#[error("Forbidden: {0}")]
|
|
Forbidden(String),
|
|
|
|
#[error("Unauthorized: {0}")]
|
|
Unauthorized(String),
|
|
|
|
#[error("password_required")]
|
|
PasswordRequired,
|
|
|
|
#[error("auth_required")]
|
|
AuthRequired,
|
|
|
|
#[allow(dead_code)]
|
|
#[error("Not found: {0}")]
|
|
NotFound(String),
|
|
|
|
#[error("Not implemented: {0}")]
|
|
NotImplemented(String),
|
|
|
|
#[error("Conflict: {0}")]
|
|
Conflict(String),
|
|
}
|
|
|
|
/// Error response body
|
|
#[derive(Debug, Serialize)]
|
|
pub struct ErrorResponse {
|
|
pub error: String,
|
|
#[serde(skip_serializing_if = "Option::is_none")]
|
|
pub details: Option<String>,
|
|
}
|
|
|
|
impl IntoResponse for ApiError {
|
|
fn into_response(self) -> Response {
|
|
let (status, error_response) = match &self {
|
|
ApiError::Domain(domain_error) => {
|
|
let status = match domain_error {
|
|
DomainError::UserNotFound(_) | DomainError::ChannelNotFound(_) | DomainError::NoActiveSchedule(_) => {
|
|
StatusCode::NOT_FOUND
|
|
}
|
|
|
|
DomainError::UserAlreadyExists(_) => StatusCode::CONFLICT,
|
|
|
|
DomainError::ValidationError(_) | DomainError::TimezoneError(_) => {
|
|
StatusCode::BAD_REQUEST
|
|
}
|
|
|
|
// Unauthenticated = not logged in → 401
|
|
DomainError::Unauthenticated(_) => StatusCode::UNAUTHORIZED,
|
|
|
|
// Forbidden = not allowed to perform action → 403
|
|
DomainError::Forbidden(_) => StatusCode::FORBIDDEN,
|
|
|
|
DomainError::RepositoryError(_) | DomainError::InfrastructureError(_) => {
|
|
StatusCode::INTERNAL_SERVER_ERROR
|
|
}
|
|
|
|
_ => StatusCode::INTERNAL_SERVER_ERROR,
|
|
};
|
|
|
|
(
|
|
status,
|
|
ErrorResponse {
|
|
error: domain_error.to_string(),
|
|
details: None,
|
|
},
|
|
)
|
|
}
|
|
|
|
ApiError::Validation(msg) => (
|
|
StatusCode::BAD_REQUEST,
|
|
ErrorResponse {
|
|
error: "Validation error".to_string(),
|
|
details: Some(msg.clone()),
|
|
},
|
|
),
|
|
|
|
ApiError::Internal(msg) => {
|
|
tracing::error!("Internal error: {}", msg);
|
|
(
|
|
StatusCode::INTERNAL_SERVER_ERROR,
|
|
ErrorResponse {
|
|
error: "Internal server error".to_string(),
|
|
details: None,
|
|
},
|
|
)
|
|
}
|
|
|
|
ApiError::Forbidden(msg) => (
|
|
StatusCode::FORBIDDEN,
|
|
ErrorResponse {
|
|
error: "Forbidden".to_string(),
|
|
details: Some(msg.clone()),
|
|
},
|
|
),
|
|
|
|
ApiError::Unauthorized(msg) => (
|
|
StatusCode::UNAUTHORIZED,
|
|
ErrorResponse {
|
|
error: "Unauthorized".to_string(),
|
|
details: Some(msg.clone()),
|
|
},
|
|
),
|
|
|
|
ApiError::PasswordRequired => (
|
|
StatusCode::UNAUTHORIZED,
|
|
ErrorResponse {
|
|
error: "password_required".to_string(),
|
|
details: None,
|
|
},
|
|
),
|
|
|
|
ApiError::AuthRequired => (
|
|
StatusCode::UNAUTHORIZED,
|
|
ErrorResponse {
|
|
error: "auth_required".to_string(),
|
|
details: None,
|
|
},
|
|
),
|
|
|
|
ApiError::NotFound(msg) => (
|
|
StatusCode::NOT_FOUND,
|
|
ErrorResponse {
|
|
error: "Not found".to_string(),
|
|
details: Some(msg.clone()),
|
|
},
|
|
),
|
|
|
|
ApiError::NotImplemented(msg) => (
|
|
StatusCode::NOT_IMPLEMENTED,
|
|
ErrorResponse {
|
|
error: "Not implemented".to_string(),
|
|
details: Some(msg.clone()),
|
|
},
|
|
),
|
|
|
|
ApiError::Conflict(msg) => (
|
|
StatusCode::CONFLICT,
|
|
ErrorResponse {
|
|
error: "Conflict".to_string(),
|
|
details: Some(msg.clone()),
|
|
},
|
|
),
|
|
};
|
|
|
|
(status, Json(error_response)).into_response()
|
|
}
|
|
}
|
|
|
|
impl ApiError {
|
|
pub fn validation(msg: impl Into<String>) -> Self {
|
|
Self::Validation(msg.into())
|
|
}
|
|
|
|
pub fn internal(msg: impl Into<String>) -> Self {
|
|
Self::Internal(msg.into())
|
|
}
|
|
|
|
pub fn not_found(msg: impl Into<String>) -> Self {
|
|
Self::NotFound(msg.into())
|
|
}
|
|
|
|
pub fn conflict(msg: impl Into<String>) -> Self {
|
|
Self::Conflict(msg.into())
|
|
}
|
|
|
|
pub fn not_implemented(msg: impl Into<String>) -> Self {
|
|
Self::NotImplemented(msg.into())
|
|
}
|
|
}
|
|
|