use axum::http::StatusCode; use axum::response::{IntoResponse, Response}; use export::data::{AppResponseData, NoData}; use thiserror::Error; use cache::redis::RedisError; use email::EmailError; use jwt::JwtError; use crate::pow::PowError; /// Result alias used by every auth use case. pub type AuthResult = Result; #[derive(Debug, Error)] pub enum AuthError { #[error("bad request: {0}")] BadRequest(String), #[error("invalid credentials")] InvalidCredentials, #[error("unauthorized: {0}")] Unauthorized(String), #[error("forbidden: {0}")] Forbidden(String), #[error("not found: {0}")] NotFound(String), #[error("conflict: {0}")] Conflict(String), #[error("rate limited: {0}")] RateLimited(String), #[error("pow failed: {0}")] PowFailed(String), #[error("internal error: {0}")] Internal(String), #[error("database error: {0}")] Sqlx(#[from] db::conn::SqlxError), #[error("redis error: {0}")] Redis(#[from] RedisError), #[error("pow error: {0}")] Pow(#[from] PowError), #[error("jwt error: {0}")] Jwt(#[from] JwtError), #[error("email error: {0}")] Email(#[from] EmailError), #[error("uuid: {0}")] Uuid(#[from] uuid::Error), } impl From for AuthError { fn from(re: axum::extract::rejection::JsonRejection) -> Self { AuthError::BadRequest(re.body_text()) } } impl From for AuthError { fn from(re: axum::extract::rejection::QueryRejection) -> Self { AuthError::BadRequest(re.body_text()) } } impl AuthError { fn status(&self) -> StatusCode { match self { AuthError::BadRequest(_) | AuthError::InvalidCredentials | AuthError::PowFailed(_) => { StatusCode::BAD_REQUEST } AuthError::Forbidden(_) => StatusCode::FORBIDDEN, AuthError::Conflict(_) => StatusCode::CONFLICT, AuthError::Unauthorized(_) => StatusCode::UNAUTHORIZED, AuthError::NotFound(_) => StatusCode::NOT_FOUND, AuthError::RateLimited(_) => StatusCode::TOO_MANY_REQUESTS, AuthError::Internal(_) => StatusCode::INTERNAL_SERVER_ERROR, AuthError::Sqlx(_) => StatusCode::INTERNAL_SERVER_ERROR, AuthError::Redis(_) => StatusCode::INTERNAL_SERVER_ERROR, AuthError::Pow(_) => StatusCode::BAD_REQUEST, AuthError::Jwt(_) => StatusCode::UNAUTHORIZED, AuthError::Email(_) => StatusCode::INTERNAL_SERVER_ERROR, AuthError::Uuid(_) => StatusCode::BAD_REQUEST, } } fn is_user_facing(&self) -> bool { matches!( self, AuthError::BadRequest(_) | AuthError::InvalidCredentials | AuthError::Unauthorized(_) | AuthError::Forbidden(_) | AuthError::NotFound(_) | AuthError::Conflict(_) | AuthError::RateLimited(_) | AuthError::PowFailed(_) ) } pub fn error_kind(&self) -> &'static str { match self { Self::BadRequest(_) => "bad_request", Self::InvalidCredentials => "invalid_credentials", Self::Unauthorized(_) => "unauthorized", Self::Forbidden(_) => "forbidden", Self::NotFound(_) => "not_found", Self::Conflict(_) => "conflict", Self::RateLimited(_) => "rate_limited", Self::PowFailed(_) => "pow_failed", Self::Internal(_) => "internal_other", Self::Sqlx(_) => "internal_db", Self::Redis(_) => "internal_redis", Self::Pow(_) => "internal_pow", Self::Jwt(_) => "internal_jwt", Self::Email(_) => "internal_email", Self::Uuid(_) => "internal_uuid", } } } impl IntoResponse for AuthError { fn into_response(self) -> Response { let status = self.status(); let msg = if self.is_user_facing() { self.to_string() } else { tracing::error!(error = %self, "auth internal error"); "internal error".to_string() }; AppResponseData::::err_plain(status.as_u16() as i32, msg).into_response() } }