use std::fmt; use axum::Json; use axum::extract::rejection::{JsonRejection, PathRejection, QueryRejection}; use axum::http::StatusCode; use axum::response::{IntoResponse, Response}; use export::rest::{AppResponseData, NoData}; use utoipa::IntoResponses; use crate::pow::PowError; #[derive(Debug)] pub enum AuthError { BadRequest(String), InvalidCredentials, Unauthorized, Forbidden, NotFound, Conflict(String), RateLimited, PowFailed, Internal(String), } impl fmt::Display for AuthError { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self { Self::BadRequest(message) => write!(f, "{message}"), Self::InvalidCredentials => write!(f, "invalid credentials"), Self::Unauthorized => write!(f, "unauthorized"), Self::Forbidden => write!(f, "forbidden"), Self::NotFound => write!(f, "not found"), Self::Conflict(message) => write!(f, "{message}"), Self::RateLimited => write!(f, "rate limit exceeded"), Self::PowFailed => write!(f, "proof of work failed"), Self::Internal(message) => write!(f, "{message}"), } } } impl std::error::Error for AuthError {} impl From for AuthError { fn from(error: PowError) -> Self { match error { PowError::NotConfigured | PowError::InvalidKey => { Self::Internal("pow service is not configured".to_string()) } PowError::Missing => Self::BadRequest("pow challenge missing".to_string()), PowError::Expired => Self::BadRequest("pow challenge expired".to_string()), PowError::Malformed | PowError::InvalidSolution => { Self::BadRequest("invalid pow solution".to_string()) } PowError::InvalidSignature => Self::BadRequest("invalid pow challenge".to_string()), } } } impl From for AuthError { fn from(error: sea_orm::DbErr) -> Self { if is_unique_violation(&error) { return Self::Conflict("resource already exists".to_string()); } Self::Internal(format!("database error: {error}")) } } impl From> for AuthError { fn from(error: sea_orm::TransactionError) -> Self { match &error { sea_orm::TransactionError::Transaction(db_err) if is_unique_violation(db_err) => { Self::Conflict("resource already exists".to_string()) } _ => Self::Internal(format!("database transaction error: {error}")), } } } fn is_unique_violation(err: &sea_orm::DbErr) -> bool { let msg = err.to_string().to_lowercase(); msg.contains("unique") || msg.contains("duplicate") || msg.contains("23505") } impl From for AuthError { fn from(error: redis::RedisError) -> Self { Self::Internal(format!("redis error: {error}")) } } impl From for AuthError { fn from(error: export::ExportError) -> Self { Self::Internal(format!("infrastructure error: {error}")) } } impl From for AuthError { fn from(error: serde_json::Error) -> Self { Self::BadRequest(format!("invalid payload: {error}")) } } #[derive(Debug, IntoResponses)] pub enum ApiError { #[response(status = 400, description = "request failed validation")] BadRequest(#[to_schema] AppResponseData), #[response(status = 400, description = "credentials rejected")] InvalidCredentials(#[to_schema] AppResponseData), #[response(status = 401, description = "authentication required")] Unauthorized(#[to_schema] AppResponseData), #[response(status = 403, description = "operation not allowed")] Forbidden(#[to_schema] AppResponseData), #[response(status = 404, description = "resource not found")] NotFound(#[to_schema] AppResponseData), #[response(status = 409, description = "resource conflict")] Conflict(#[to_schema] AppResponseData), #[response(status = 429, description = "rate limit exceeded")] RateLimited(#[to_schema] AppResponseData), #[response(status = 500, description = "internal server error")] Internal(#[to_schema] AppResponseData), } impl ApiError { fn payload(code: i32, message: &'static str) -> AppResponseData { AppResponseData::err(code, message) } fn decompose(self) -> (u16, AppResponseData) { match self { Self::BadRequest(envelope) => (400, envelope), Self::InvalidCredentials(envelope) => (400, envelope), Self::Unauthorized(envelope) => (401, envelope), Self::Forbidden(envelope) => (403, envelope), Self::NotFound(envelope) => (404, envelope), Self::Conflict(envelope) => (409, envelope), Self::RateLimited(envelope) => (429, envelope), Self::Internal(envelope) => (500, envelope), } } } impl IntoResponse for ApiError { fn into_response(self) -> Response { let (code, envelope) = self.decompose(); let status = StatusCode::from_u16(code).unwrap_or_else(|_| StatusCode::INTERNAL_SERVER_ERROR); (status, Json(envelope)).into_response() } } impl From for ApiError { fn from(error: AuthError) -> Self { match error { AuthError::BadRequest(msg) => { Self::BadRequest(AppResponseData::err(400, msg).with_error_code("bad_request")) } AuthError::PowFailed => Self::BadRequest( Self::payload(400, "proof of work failed").with_error_code("pow_failed"), ), AuthError::InvalidCredentials => { Self::InvalidCredentials(Self::payload(400, "invalid credentials")) } AuthError::Unauthorized => Self::Unauthorized( Self::payload(401, "unauthorized").with_error_code("unauthorized"), ), AuthError::Forbidden => { Self::Forbidden(Self::payload(403, "forbidden").with_error_code("forbidden")) } AuthError::NotFound => Self::NotFound( Self::payload(404, "auth resource not found").with_error_code("not_found"), ), AuthError::Conflict(msg) => { Self::Conflict(AppResponseData::err(409, msg).with_error_code("conflict")) } AuthError::RateLimited => Self::RateLimited( Self::payload(429, "rate limit exceeded").with_error_code("rate_limited"), ), AuthError::Internal(message) => { tracing::warn!(error = %message, "auth handler failed"); Self::Internal(Self::payload(500, "internal server error")) } } } } impl From> for ApiError { fn from(error: Box) -> Self { *error } } impl From for ApiError { fn from(error: session::SessionError) -> Self { match error { session::SessionError::Unauthorized => { Self::Unauthorized(Self::payload(401, "unauthorized")) } session::SessionError::Database(message) => { tracing::warn!(error = %message, "auth handler failed"); Self::Internal(Self::payload(500, "internal server error")) } } } } impl From> for ApiError { fn from(_error: AppResponseData<()>) -> Self { Self::BadRequest(Self::payload(400, "bad request")) } } impl From for ApiError { fn from(error: PathRejection) -> Self { Self::BadRequest( AppResponseData::err(400, "invalid request path").with_error(error.body_text()), ) } } impl From for ApiError { fn from(error: JsonRejection) -> Self { Self::BadRequest( AppResponseData::err(400, "invalid request body") .with_error(error.body_text()) .with_error_code("invalid_request_body"), ) } } impl From for ApiError { fn from(error: QueryRejection) -> Self { Self::BadRequest( AppResponseData::err(400, "invalid query parameters").with_error(error.body_text()), ) } } impl From<(StatusCode, &'static str)> for ApiError { fn from((status, message): (StatusCode, &'static str)) -> Self { tracing::error!(%status, %message, "unhandled (status, message) converted to 500"); Self::Internal(Self::payload(500, "internal server error")) } }