use thiserror::Error;
#[derive(Error, Debug)]
pub enum AuthError {
#[error("{0}")]
BadRequest(String),
#[error("Invalid request: {0}")]
InvalidRequest(String),
#[error("Validation error: {0}")]
Validation(String),
#[error("Invalid credentials")]
InvalidCredentials,
#[error("Authentication required")]
Unauthenticated,
#[error("Session not found or expired")]
SessionNotFound,
#[error("{0}")]
Forbidden(String),
#[error("Insufficient permissions")]
Unauthorized,
#[error("User not found")]
UserNotFound,
#[error("{0}")]
NotFound(String),
#[error("{0}")]
Conflict(String),
#[error("Too many requests")]
RateLimited,
#[error("{0}")]
PayloadTooLarge(String),
#[error("{0}")]
NotImplemented(String),
#[error("Configuration error: {0}")]
Config(String),
#[error("Database error: {0}")]
Database(#[from] DatabaseError),
#[error("Serialization error: {0}")]
Serialization(#[from] serde_json::Error),
#[error("Plugin error: {plugin} - {message}")]
Plugin { plugin: String, message: String },
#[error("Internal server error: {0}")]
Internal(String),
#[error("Password hashing error: {0}")]
PasswordHash(String),
#[error("JWT error: {0}")]
Jwt(#[from] jsonwebtoken::errors::Error),
}
impl AuthError {
pub fn status_code(&self) -> u16 {
match self {
Self::BadRequest(_) | Self::InvalidRequest(_) | Self::Validation(_) => 400,
Self::InvalidCredentials | Self::Unauthenticated | Self::SessionNotFound => 401,
Self::Forbidden(_) | Self::Unauthorized => 403,
Self::UserNotFound | Self::NotFound(_) => 404,
Self::Conflict(_) => 409,
Self::PayloadTooLarge(_) => 413,
Self::RateLimited => 429,
Self::NotImplemented(_) => 501,
Self::Config(_)
| Self::Database(_)
| Self::Serialization(_)
| Self::Plugin { .. }
| Self::Internal(_)
| Self::PasswordHash(_)
| Self::Jwt(_) => 500,
}
}
pub fn into_response(self) -> crate::types::AuthResponse {
let status = self.status_code();
let message = match status {
500 => {
tracing::error!(error = %self, "Internal server error");
"Internal server error".to_string()
}
_ => self.to_string(),
};
crate::types::AuthResponse::json(
status,
&crate::types::ErrorMessageResponse {
message: message.clone(),
},
)
.unwrap_or_else(|_| crate::types::AuthResponse::text(status, &message))
}
pub fn bad_request(message: impl Into<String>) -> Self {
Self::BadRequest(message.into())
}
pub fn forbidden(message: impl Into<String>) -> Self {
Self::Forbidden(message.into())
}
pub fn not_found(message: impl Into<String>) -> Self {
Self::NotFound(message.into())
}
pub fn conflict(message: impl Into<String>) -> Self {
Self::Conflict(message.into())
}
pub fn not_implemented(message: impl Into<String>) -> Self {
Self::NotImplemented(message.into())
}
pub fn payload_too_large(message: impl Into<String>) -> Self {
Self::PayloadTooLarge(message.into())
}
pub fn plugin(plugin: &str, message: impl Into<String>) -> Self {
Self::Plugin {
plugin: plugin.to_string(),
message: message.into(),
}
}
pub fn config(message: impl Into<String>) -> Self {
Self::Config(message.into())
}
pub fn internal(message: impl Into<String>) -> Self {
Self::Internal(message.into())
}
pub fn validation(message: impl Into<String>) -> Self {
Self::Validation(message.into())
}
}
#[derive(Error, Debug)]
pub enum DatabaseError {
#[error("Connection error: {0}")]
Connection(String),
#[error("Query error: {0}")]
Query(String),
#[error("Migration error: {0}")]
Migration(String),
#[error("Constraint violation: {0}")]
Constraint(String),
#[error("Transaction error: {0}")]
Transaction(String),
}
#[cfg(feature = "sqlx-postgres")]
impl From<sqlx::Error> for DatabaseError {
fn from(err: sqlx::Error) -> Self {
match err {
sqlx::Error::Database(db_err) => {
if db_err.is_unique_violation() {
DatabaseError::Constraint(db_err.to_string())
} else {
DatabaseError::Query(db_err.to_string())
}
}
sqlx::Error::PoolClosed => DatabaseError::Connection("Pool closed".to_string()),
sqlx::Error::PoolTimedOut => DatabaseError::Connection("Pool timed out".to_string()),
_ => DatabaseError::Query(err.to_string()),
}
}
}
#[cfg(feature = "sqlx-postgres")]
impl From<sqlx::Error> for AuthError {
fn from(err: sqlx::Error) -> Self {
AuthError::Database(DatabaseError::from(err))
}
}
pub type AuthResult<T> = Result<T, AuthError>;
#[cfg(feature = "axum")]
impl axum::response::IntoResponse for AuthError {
fn into_response(self) -> axum::response::Response {
let status = axum::http::StatusCode::from_u16(self.status_code())
.unwrap_or(axum::http::StatusCode::INTERNAL_SERVER_ERROR);
let message = match self.status_code() {
500 => {
tracing::error!(error = %self, "Internal server error");
"Internal server error".to_string()
}
_ => self.to_string(),
};
(
status,
axum::Json(crate::types::ErrorMessageResponse { message }),
)
.into_response()
}
}
pub fn validation_error_response(
errors: &validator::ValidationErrors,
) -> crate::types::AuthResponse {
let field_errors: std::collections::HashMap<&str, Vec<String>> = errors
.field_errors()
.into_iter()
.map(|(field, errs)| {
let messages: Vec<String> = errs
.iter()
.map(|e| {
e.message
.as_ref()
.map(|m| m.to_string())
.unwrap_or_else(|| format!("Invalid value for {}", field))
})
.collect();
(field, messages)
})
.collect();
let body = crate::types::ValidationErrorResponse {
code: "VALIDATION_ERROR",
message: "Validation failed",
errors: field_errors,
};
crate::types::AuthResponse::json(422, &body)
.unwrap_or_else(|_| crate::types::AuthResponse::text(422, "Validation failed"))
}
pub fn validate_request_body<T>(
req: &crate::types::AuthRequest,
) -> Result<T, crate::types::AuthResponse>
where
T: serde::de::DeserializeOwned + validator::Validate,
{
let value: T = req.body_as_json().map_err(|e| {
crate::types::AuthResponse::json(
400,
&crate::types::ErrorMessageResponse {
message: format!("Invalid JSON: {}", e),
},
)
.unwrap_or_else(|_| crate::types::AuthResponse::text(400, "Invalid JSON"))
})?;
value
.validate()
.map_err(|e| validation_error_response(&e))?;
Ok(value)
}