e891f19
ZhenYi
refactor: enhance event payload structure with actor and room details for improved clarity
e891f19
use actix_web::http::StatusCode;
use actix_web::http::header::{HeaderValue, RETRY_AFTER};
use actix_web::{HttpResponse, ResponseError};
use serde::{Deserialize, Serialize};
use utoipa::ToSchema;
pub type AuthResult<T> = std::result::Result<T, AuthError>;
#[derive(Debug, thiserror::Error)]
#[non_exhaustive]
pub enum AuthError {
#[error("challenge expired")]
Expired,
#[error("proof payload does not match the request")]
InvalidPayload,
#[error("proof does not meet the required difficulty")]
InvalidProof,
#[error("request payload is malformed")]
MalformedRequest,
#[error("username is invalid")]
InvalidUsername,
#[error("display name is invalid")]
InvalidDisplayname,
#[error("email is invalid")]
InvalidEmail,
#[error("email is unchanged")]
EmailUnchanged,
#[error("birth date is invalid")]
InvalidBirthDate,
#[error("you must be at least {min_age} years old")]
UnderMinimumAge { min_age: u32 },
#[error("password does not satisfy the password policy")]
InvalidPassword,
#[error("password hashing failed: {0}")]
PasswordHash(String),
#[error("username is already taken")]
UsernameTaken,
#[error("email is already taken")]
EmailTaken,
#[error("email verification code is invalid")]
InvalidEmailCode,
#[error("too many invalid email verification attempts")]
EmailCodeAttemptsExceeded,
#[error("password reset token is invalid")]
InvalidResetToken,
#[error("authentication required")]
Unauthenticated,
#[error("forbidden")]
Forbidden,
#[error("username or password is incorrect")]
InvalidCredentials,
#[error("login is forbidden{}", reason.as_deref().map(|reason| format!(": {reason}")).unwrap_or_default())]
LoginForbidden { reason: Option<String> },
#[error("account is unavailable: {reason}")]
AccountUnavailable { reason: String },
#[error("two-factor verification is required")]
MfaRequired,
#[error("totp is not configured")]
TotpNotConfigured,
#[error("totp is already enabled")]
TotpAlreadyEnabled,
#[error("totp code is invalid")]
TotpCodeInvalid,
#[error("totp code was already used")]
TotpStepReused,
#[error("recovery code is invalid")]
RecoveryCodeInvalid,
#[error("recovery codes are exhausted")]
RecoveryCodesExhausted,
#[error("access token is invalid")]
InvalidAccessToken,
#[error("access token does not exist")]
AccessTokenNotFound,
#[error("access key name is invalid")]
InvalidAccessKeyName,
#[error("access token scope is invalid")]
InvalidScope,
#[error("expires_at is invalid")]
InvalidExpiry,
#[error("too many access keys")]
TooManyAccessKeys,
#[error("public key is invalid")]
SshKeyInvalid,
#[error("permission `{permission}` is not recognized")]
InvalidPermission { permission: String },
#[error("organization does not exist")]
OrganizationNotFound,
#[error("organization member does not exist")]
OrganizationMemberNotFound,
#[error("organization role does not exist")]
OrganizationRoleNotFound,
#[error("organization invite does not exist")]
OrganizationInviteNotFound,
#[error("organization slug is already taken")]
OrganizationSlugTaken,
#[error("user is already a member of this organization")]
OrganizationMemberExists,
#[error("organization role key is already used")]
OrganizationRoleKeyTaken,
#[error("organization role is still assigned to members")]
OrganizationRoleInUse,
#[error("organization role is built in and cannot be modified")]
OrganizationRoleSystem,
#[error("organization invite is exhausted or revoked")]
OrganizationInviteExhausted,
#[error("organization invite has expired")]
OrganizationInviteExpired,
#[error("organization must have an owner; transfer ownership first")]
OrganizationOwnerRequired,
#[error("organization still has active members")]
OrganizationHasMembers,
#[error("organization is not active")]
OrganizationInactive,
#[error("organization membership is banned")]
OrganizationMemberBanned,
#[error("resource version mismatch, current version is {current}")]
VersionMismatch { current: i32 },
#[error("public key is already registered")]
SshKeyTaken,
#[error("ssh key name is invalid")]
SshKeyNameInvalid,
#[error("ssh key does not exist")]
SshKeyNotFound,
#[error("agreement `{key}` does not exist")]
AgreementNotFound { key: String },
#[error("agreement `{key}` must be accepted")]
AgreementNotAccepted { key: String },
#[error("account deletion is already requested")]
DeletionAlreadyRequested,
#[error("account deletion is not requested")]
DeletionNotRequested,
#[error("too many requests, retry after {retry_after_secs}s")]
RateLimited { retry_after_secs: u64 },
#[error("user does not exist")]
UserNotFound,
#[error("app does not exist")]
AppNotFound,
#[error("session does not exist")]
SessionNotFound,
#[error("token does not exist")]
TokenNotFound,
#[error("token has expired")]
TokenExpired,
#[error("token has been revoked")]
TokenRevoked,
#[error("credentials are stale, security version is {current}")]
SecurityVersionStale { current: i32 },
#[error("scope `{required}` is missing")]
ScopeInsufficient { required: String },
#[error("dependency is unavailable: {0}")]
Unavailable(String),
#[error("internal error: {0}")]
Internal(String),
#[error("database error: {0}")]
Database(#[from] crate::db::DbError),
#[error("cache error: {0}")]
Cache(#[from] crate::cache::CacheError),
#[error("session error: {0}")]
Session(#[from] crate::session::SessionError),
#[error("email error: {0}")]
Email(#[from] crate::email::EmailError),
#[error("serialization error: {0}")]
Serialization(#[from] serde_json::Error),
}
impl From<sea_orm::DbErr> for AuthError {
fn from(error: sea_orm::DbErr) -> Self {
Self::Database(crate::db::DbError::from(error))
}
}
impl AuthError {
pub fn internal(message: impl Into<String>) -> Self {
Self::Internal(message.into())
}
pub fn status_code(&self) -> StatusCode {
match self {
Self::Unauthenticated
| Self::InvalidCredentials
| Self::InvalidAccessToken
| Self::AccessTokenNotFound
| Self::TokenExpired
| Self::TokenRevoked
| Self::SecurityVersionStale { .. }
| Self::MfaRequired => StatusCode::UNAUTHORIZED,
Self::Forbidden
| Self::LoginForbidden { .. }
| Self::UnderMinimumAge { .. }
| Self::AccountUnavailable { .. }
| Self::ScopeInsufficient { .. }
| Self::OrganizationInactive
| Self::OrganizationMemberBanned
| Self::AgreementNotAccepted { .. } => StatusCode::FORBIDDEN,
Self::UserNotFound
| Self::SessionNotFound
| Self::TokenNotFound
| Self::AppNotFound
| Self::SshKeyNotFound
| Self::OrganizationNotFound
| Self::OrganizationMemberNotFound
| Self::OrganizationRoleNotFound
| Self::OrganizationInviteNotFound
| Self::AgreementNotFound { .. } => StatusCode::NOT_FOUND,
Self::Unavailable(_) => StatusCode::SERVICE_UNAVAILABLE,
Self::UsernameTaken
| Self::EmailTaken
| Self::SshKeyTaken
| Self::OrganizationSlugTaken
| Self::OrganizationMemberExists
| Self::OrganizationRoleKeyTaken
| Self::OrganizationHasMembers
| Self::TotpAlreadyEnabled
| Self::TotpNotConfigured
| Self::DeletionAlreadyRequested => StatusCode::CONFLICT,
Self::OrganizationRoleInUse
| Self::OrganizationRoleSystem
| Self::OrganizationInviteExhausted
| Self::OrganizationInviteExpired
| Self::OrganizationOwnerRequired => StatusCode::PRECONDITION_FAILED,
Self::VersionMismatch { .. } => StatusCode::CONFLICT,
Self::RateLimited { .. }
| Self::EmailCodeAttemptsExceeded
| Self::TooManyAccessKeys
| Self::RecoveryCodesExhausted => StatusCode::TOO_MANY_REQUESTS,
Self::PasswordHash(_)
| Self::Internal(_)
| Self::Database(_)
| Self::Cache(_)
| Self::Session(_)
| Self::Email(_)
| Self::Serialization(_) => StatusCode::INTERNAL_SERVER_ERROR,
_ => StatusCode::BAD_REQUEST,
}
}
pub fn code(&self) -> &'static str {
match self {
Self::Expired => "expired",
Self::InvalidPayload => "invalid_payload",
Self::InvalidProof => "invalid_proof",
Self::MalformedRequest => "malformed_request",
Self::InvalidUsername => "invalid_username",
Self::InvalidDisplayname => "invalid_displayname",
Self::InvalidEmail => "invalid_email",
Self::EmailUnchanged => "email_unchanged",
Self::InvalidBirthDate => "invalid_birth_date",
Self::UnderMinimumAge { .. } => "under_minimum_age",
Self::InvalidPassword => "invalid_password",
Self::PasswordHash(_) => "password_hash",
Self::UsernameTaken => "username_taken",
Self::EmailTaken => "email_taken",
Self::InvalidEmailCode => "invalid_email_code",
Self::EmailCodeAttemptsExceeded => "email_code_attempts_exceeded",
Self::InvalidResetToken => "invalid_reset_token",
Self::Unauthenticated => "unauthenticated",
Self::Forbidden => "forbidden",
Self::InvalidCredentials => "invalid_credentials",
Self::LoginForbidden { .. } => "login_forbidden",
Self::AccountUnavailable { .. } => "account_unavailable",
Self::MfaRequired => "mfa_required",
Self::TotpNotConfigured => "totp_not_configured",
Self::TotpAlreadyEnabled => "totp_already_enabled",
Self::TotpCodeInvalid => "totp_code_invalid",
Self::TotpStepReused => "totp_step_reused",
Self::RecoveryCodeInvalid => "recovery_code_invalid",
Self::RecoveryCodesExhausted => "recovery_codes_exhausted",
Self::InvalidAccessToken => "invalid_access_token",
Self::AccessTokenNotFound => "access_token_not_found",
Self::InvalidAccessKeyName => "invalid_access_key_name",
Self::InvalidScope => "invalid_scope",
Self::InvalidExpiry => "invalid_expiry",
Self::TooManyAccessKeys => "too_many_access_keys",
Self::SshKeyInvalid => "ssh_key_invalid",
Self::InvalidPermission { .. } => "invalid_permission",
Self::OrganizationNotFound => "organization_not_found",
Self::OrganizationMemberNotFound => "organization_member_not_found",
Self::OrganizationRoleNotFound => "organization_role_not_found",
Self::OrganizationInviteNotFound => "organization_invite_not_found",
Self::OrganizationSlugTaken => "organization_slug_taken",
Self::OrganizationMemberExists => "organization_member_exists",
Self::OrganizationRoleKeyTaken => "organization_role_key_taken",
Self::OrganizationRoleInUse => "organization_role_in_use",
Self::OrganizationRoleSystem => "organization_role_system",
Self::OrganizationInviteExhausted => "organization_invite_exhausted",
Self::OrganizationInviteExpired => "organization_invite_expired",
Self::OrganizationOwnerRequired => "organization_owner_required",
Self::OrganizationHasMembers => "organization_has_members",
Self::OrganizationInactive => "organization_inactive",
Self::OrganizationMemberBanned => "organization_member_banned",
Self::VersionMismatch { .. } => "version_mismatch",
Self::SshKeyTaken => "ssh_key_taken",
Self::SshKeyNameInvalid => "ssh_key_name_invalid",
Self::SshKeyNotFound => "ssh_key_not_found",
Self::AgreementNotFound { .. } => "agreement_not_found",
Self::AgreementNotAccepted { .. } => "agreement_not_accepted",
Self::DeletionAlreadyRequested => "deletion_already_requested",
Self::DeletionNotRequested => "deletion_not_requested",
Self::RateLimited { .. } => "rate_limited",
Self::UserNotFound => "user_not_found",
Self::AppNotFound => "app_not_found",
Self::SessionNotFound => "session_not_found",
Self::TokenNotFound => "token_not_found",
Self::TokenExpired => "token_expired",
Self::TokenRevoked => "token_revoked",
Self::SecurityVersionStale { .. } => "security_version_stale",
Self::ScopeInsufficient { .. } => "scope_insufficient",
Self::Unavailable(_) => "unavailable",
Self::Internal(_) => "internal",
Self::Database(_) => "database",
Self::Cache(_) => "cache",
Self::Session(_) => "session",
Self::Email(_) => "email",
Self::Serialization(_) => "serialization",
}
}
pub fn is_internal(&self) -> bool {
self.status_code().is_server_error()
}
pub fn message(&self) -> String {
if self.is_internal() {
"internal server error".to_string()
} else {
self.to_string()
}
}
pub fn body(&self) -> ErrorBody {
ErrorBody {
code: self.code().to_string(),
message: self.message(),
}
}
}
impl ResponseError for AuthError {
fn status_code(&self) -> StatusCode {
AuthError::status_code(self)
}
fn error_response(&self) -> HttpResponse {
let body = self.body();
let mut response = crate::envelope::Envelope::<crate::envelope::NoData>::err(
self.status_code().as_u16() as i32,
body.message,
)
.with_error_code(body.code)
.into_http_response();
if let Self::RateLimited { retry_after_secs } = self
&& let Ok(value) = HeaderValue::from_str(&retry_after_secs.to_string())
{
response.headers_mut().insert(RETRY_AFTER, value);
}
response
}
}
#[derive(Clone, Debug, Deserialize, Serialize, ToSchema)]
pub struct ErrorBody {
pub code: String,
pub message: String,
}