use axum::http::StatusCode; use axum::response::{IntoResponse, Response}; use serde::Serialize; /// Unified API error type #[derive(Debug, thiserror::Error)] pub enum ApiError { #[error("Not found: {0}")] NotFound(String), #[error("Bad request: {0}")] BadRequest(String), #[error("Unauthorized")] Unauthorized, #[error("Forbidden")] Forbidden, #[error("Conflict: {0}")] Conflict(String), #[error("License invalid or expired")] LicenseInvalid, #[error("Module not enabled: {0}")] ModuleNotEnabled(String), #[error("Rate limited")] RateLimited, #[error("Internal error: {0}")] Internal(String), #[error(transparent)] Database(#[from] sqlx::Error), #[error(transparent)] Anyhow(#[from] anyhow::Error), } #[derive(Serialize)] struct ErrorResponse { error: String, message: String, } impl IntoResponse for ApiError { fn into_response(self) -> Response { let (status, error_type, message) = match &self { ApiError::NotFound(msg) => (StatusCode::NOT_FOUND, "not_found", msg.clone()), ApiError::BadRequest(msg) => (StatusCode::BAD_REQUEST, "bad_request", msg.clone()), ApiError::Unauthorized => { (StatusCode::UNAUTHORIZED, "unauthorized", "Unauthorized".to_string()) } ApiError::Forbidden => { (StatusCode::FORBIDDEN, "forbidden", "Forbidden".to_string()) } ApiError::Conflict(msg) => (StatusCode::CONFLICT, "conflict", msg.clone()), ApiError::LicenseInvalid => ( StatusCode::PAYMENT_REQUIRED, "license_invalid", "License invalid or expired".to_string(), ), ApiError::ModuleNotEnabled(module) => ( StatusCode::PAYMENT_REQUIRED, "module_not_enabled", format!("Module not enabled: {module}"), ), ApiError::RateLimited => ( StatusCode::TOO_MANY_REQUESTS, "rate_limited", "Too many requests".to_string(), ), ApiError::Internal(msg) => { tracing::error!("Internal error: {msg}"); ( StatusCode::INTERNAL_SERVER_ERROR, "internal_error", "Internal server error".to_string(), ) } ApiError::Database(e) => { tracing::error!("Database error: {e}"); ( StatusCode::INTERNAL_SERVER_ERROR, "internal_error", "Internal server error".to_string(), ) } ApiError::Anyhow(e) => { tracing::error!("Unexpected error: {e}"); ( StatusCode::INTERNAL_SERVER_ERROR, "internal_error", "Internal server error".to_string(), ) } }; let body = ErrorResponse { error: error_type.to_string(), message, }; (status, axum::Json(body)).into_response() } } /// Result alias for API handlers pub type ApiResult = Result;