Readur/src/errors/user.rs

224 lines
9.6 KiB
Rust

use axum::http::StatusCode;
use thiserror::Error;
use uuid::Uuid;
use super::{AppError, ErrorCategory, ErrorSeverity, impl_into_response};
/// Errors related to user management operations
#[derive(Error, Debug)]
pub enum UserError {
#[error("User not found")]
NotFound,
#[error("User with ID {id} not found")]
NotFoundById { id: Uuid },
#[error("Username '{username}' already exists")]
DuplicateUsername { username: String },
#[error("Email '{email}' already exists")]
DuplicateEmail { email: String },
#[error("Invalid role '{role}'. Valid roles are: admin, user")]
InvalidRole { role: String },
#[error("Permission denied: {reason}")]
PermissionDenied { reason: String },
#[error("Invalid credentials")]
InvalidCredentials,
#[error("Account is disabled")]
AccountDisabled,
#[error("Password does not meet requirements: {requirements}")]
InvalidPassword { requirements: String },
#[error("Username '{username}' is invalid: {reason}")]
InvalidUsername { username: String, reason: String },
#[error("Email '{email}' is invalid")]
InvalidEmail { email: String },
#[error("Cannot delete user with ID {id}: {reason}")]
DeleteRestricted { id: Uuid, reason: String },
#[error("OIDC authentication failed: {details}")]
OidcAuthenticationFailed { details: String },
#[error("Authentication provider '{provider}' is not configured")]
AuthProviderNotConfigured { provider: String },
#[error("Token has expired")]
TokenExpired,
#[error("Invalid token format")]
InvalidToken,
#[error("User session has expired, please login again")]
SessionExpired,
#[error("Internal server error: {message}")]
InternalServerError { message: String },
}
impl AppError for UserError {
fn status_code(&self) -> StatusCode {
match self {
UserError::NotFound | UserError::NotFoundById { .. } => StatusCode::NOT_FOUND,
UserError::DuplicateUsername { .. } | UserError::DuplicateEmail { .. } => StatusCode::CONFLICT,
UserError::InvalidRole { .. } => StatusCode::BAD_REQUEST,
UserError::PermissionDenied { .. } => StatusCode::FORBIDDEN,
UserError::InvalidCredentials => StatusCode::UNAUTHORIZED,
UserError::AccountDisabled => StatusCode::FORBIDDEN,
UserError::InvalidPassword { .. } => StatusCode::BAD_REQUEST,
UserError::InvalidUsername { .. } => StatusCode::BAD_REQUEST,
UserError::InvalidEmail { .. } => StatusCode::BAD_REQUEST,
UserError::DeleteRestricted { .. } => StatusCode::FORBIDDEN,
UserError::OidcAuthenticationFailed { .. } => StatusCode::UNAUTHORIZED,
UserError::AuthProviderNotConfigured { .. } => StatusCode::BAD_REQUEST,
UserError::TokenExpired => StatusCode::UNAUTHORIZED,
UserError::InvalidToken => StatusCode::UNAUTHORIZED,
UserError::SessionExpired => StatusCode::UNAUTHORIZED,
UserError::InternalServerError { .. } => StatusCode::INTERNAL_SERVER_ERROR,
}
}
fn user_message(&self) -> String {
match self {
UserError::NotFound | UserError::NotFoundById { .. } => "User not found".to_string(),
UserError::DuplicateUsername { .. } => "Username already exists".to_string(),
UserError::DuplicateEmail { .. } => "Email already exists".to_string(),
UserError::InvalidRole { .. } => "Invalid user role specified".to_string(),
UserError::PermissionDenied { reason } => format!("Permission denied: {}", reason),
UserError::InvalidCredentials => "Invalid username or password".to_string(),
UserError::AccountDisabled => "Account is disabled".to_string(),
UserError::InvalidPassword { requirements } => format!("Password does not meet requirements: {}", requirements),
UserError::InvalidUsername { reason, .. } => format!("Invalid username: {}", reason),
UserError::InvalidEmail { .. } => "Invalid email address".to_string(),
UserError::DeleteRestricted { reason, .. } => format!("Cannot delete user: {}", reason),
UserError::OidcAuthenticationFailed { .. } => "OIDC authentication failed".to_string(),
UserError::AuthProviderNotConfigured { .. } => "Authentication provider not configured".to_string(),
UserError::TokenExpired => "Token has expired".to_string(),
UserError::InvalidToken => "Invalid token".to_string(),
UserError::SessionExpired => "Session has expired, please login again".to_string(),
UserError::InternalServerError { .. } => "An internal error occurred".to_string(),
}
}
fn error_code(&self) -> &'static str {
match self {
UserError::NotFound => "USER_NOT_FOUND",
UserError::NotFoundById { .. } => "USER_NOT_FOUND_BY_ID",
UserError::DuplicateUsername { .. } => "USER_DUPLICATE_USERNAME",
UserError::DuplicateEmail { .. } => "USER_DUPLICATE_EMAIL",
UserError::InvalidRole { .. } => "USER_INVALID_ROLE",
UserError::PermissionDenied { .. } => "USER_PERMISSION_DENIED",
UserError::InvalidCredentials => "USER_INVALID_CREDENTIALS",
UserError::AccountDisabled => "USER_ACCOUNT_DISABLED",
UserError::InvalidPassword { .. } => "USER_INVALID_PASSWORD",
UserError::InvalidUsername { .. } => "USER_INVALID_USERNAME",
UserError::InvalidEmail { .. } => "USER_INVALID_EMAIL",
UserError::DeleteRestricted { .. } => "USER_DELETE_RESTRICTED",
UserError::OidcAuthenticationFailed { .. } => "USER_OIDC_AUTH_FAILED",
UserError::AuthProviderNotConfigured { .. } => "USER_AUTH_PROVIDER_NOT_CONFIGURED",
UserError::TokenExpired => "USER_TOKEN_EXPIRED",
UserError::InvalidToken => "USER_INVALID_TOKEN",
UserError::SessionExpired => "USER_SESSION_EXPIRED",
UserError::InternalServerError { .. } => "USER_INTERNAL_SERVER_ERROR",
}
}
fn error_category(&self) -> ErrorCategory {
ErrorCategory::Auth
}
fn error_severity(&self) -> ErrorSeverity {
match self {
UserError::PermissionDenied { .. } | UserError::DeleteRestricted { .. } => ErrorSeverity::Important,
UserError::OidcAuthenticationFailed { .. } | UserError::AuthProviderNotConfigured { .. } => ErrorSeverity::Critical,
UserError::InvalidCredentials | UserError::AccountDisabled => ErrorSeverity::Expected,
UserError::InternalServerError { .. } => ErrorSeverity::Critical,
_ => ErrorSeverity::Minor,
}
}
fn suppression_key(&self) -> Option<String> {
match self {
UserError::InvalidCredentials => Some("user_invalid_credentials".to_string()),
UserError::NotFound => Some("user_not_found".to_string()),
_ => None,
}
}
fn suggested_action(&self) -> Option<String> {
match self {
UserError::DuplicateUsername { .. } => Some("Please choose a different username".to_string()),
UserError::DuplicateEmail { .. } => Some("Please use a different email address".to_string()),
UserError::InvalidPassword { .. } => Some("Password must be at least 8 characters long and contain uppercase, lowercase, and numbers".to_string()),
UserError::InvalidCredentials => Some("Please check your username and password".to_string()),
UserError::SessionExpired | UserError::TokenExpired => Some("Please login again".to_string()),
UserError::AccountDisabled => Some("Please contact an administrator".to_string()),
_ => None,
}
}
}
impl_into_response!(UserError);
/// Convenience methods for creating common user errors
impl UserError {
pub fn not_found_by_id(id: Uuid) -> Self {
Self::NotFoundById { id }
}
pub fn duplicate_username<S: Into<String>>(username: S) -> Self {
Self::DuplicateUsername { username: username.into() }
}
pub fn duplicate_email<S: Into<String>>(email: S) -> Self {
Self::DuplicateEmail { email: email.into() }
}
pub fn invalid_role<S: Into<String>>(role: S) -> Self {
Self::InvalidRole { role: role.into() }
}
pub fn permission_denied<S: Into<String>>(reason: S) -> Self {
Self::PermissionDenied { reason: reason.into() }
}
pub fn invalid_password<S: Into<String>>(requirements: S) -> Self {
Self::InvalidPassword { requirements: requirements.into() }
}
pub fn invalid_username<S: Into<String>>(username: S, reason: S) -> Self {
Self::InvalidUsername {
username: username.into(),
reason: reason.into()
}
}
pub fn invalid_email<S: Into<String>>(email: S) -> Self {
Self::InvalidEmail { email: email.into() }
}
pub fn delete_restricted<S: Into<String>>(id: Uuid, reason: S) -> Self {
Self::DeleteRestricted {
id,
reason: reason.into()
}
}
pub fn oidc_authentication_failed<S: Into<String>>(details: S) -> Self {
Self::OidcAuthenticationFailed { details: details.into() }
}
pub fn auth_provider_not_configured<S: Into<String>>(provider: S) -> Self {
Self::AuthProviderNotConfigured { provider: provider.into() }
}
pub fn internal_server_error<S: Into<String>>(message: S) -> Self {
Self::InternalServerError { message: message.into() }
}
}