Skip to content
Clone
1480255
260 lines 10.1 KB Rust
use actix_web::http::StatusCode;
use std::fmt;
pub use export::is_unique_violation;
#[derive(Debug)]
pub enum SpaceError {
BadRequest(String),
Unauthorized,
Forbidden,
JoinNotAllowed(String),
JoinRequirementNotMet(String),
JoinRequestCooldown(String),
NotFound,
Conflict(String),
RateLimited(String),
Internal(String),
MalformedRequest,
InvalidEmail,
InvalidPermission { permission: String },
SpaceNotFound,
SpaceMemberNotFound,
SpaceRoleNotFound,
SpaceInviteNotFound,
SpaceSlugTaken,
SpaceMemberExists,
SpaceRoleKeyTaken,
SpaceRoleInUse,
SpaceRoleSystem,
SpaceInviteExhausted,
SpaceInviteExpired,
SpaceOwnerRequired,
SpaceHasMembers,
SpaceInactive,
SpaceMemberBanned,
VersionMismatch { current: i32 },
UserNotFound,
ScopeInsufficient { required: String },
}
pub type SpaceResult<T> = Result<T, SpaceError>;
impl fmt::Display for SpaceError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::BadRequest(message) => write!(f, "{message}"),
Self::Unauthorized => write!(f, "unauthorized"),
Self::Forbidden => write!(f, "forbidden"),
Self::JoinNotAllowed(message) => write!(f, "{message}"),
Self::JoinRequirementNotMet(message) => write!(f, "{message}"),
Self::JoinRequestCooldown(message) => write!(f, "{message}"),
Self::NotFound => write!(f, "not found"),
Self::Conflict(message) => write!(f, "{message}"),
Self::RateLimited(message) => write!(f, "{message}"),
Self::Internal(message) => write!(f, "{message}"),
Self::MalformedRequest => write!(f, "request payload is malformed"),
Self::InvalidEmail => write!(f, "email is invalid"),
Self::InvalidPermission { permission } => {
write!(f, "permission `{permission}` is not recognized")
}
Self::SpaceNotFound => write!(f, "space does not exist"),
Self::SpaceMemberNotFound => write!(f, "space member does not exist"),
Self::SpaceRoleNotFound => write!(f, "space role does not exist"),
Self::SpaceInviteNotFound => write!(f, "space invite does not exist"),
Self::SpaceSlugTaken => write!(f, "space slug is already taken"),
Self::SpaceMemberExists => write!(f, "user is already a member of this space"),
Self::SpaceRoleKeyTaken => write!(f, "space role key is already used"),
Self::SpaceRoleInUse => write!(f, "space role is still assigned to members"),
Self::SpaceRoleSystem => write!(f, "space role is built in and cannot be modified"),
Self::SpaceInviteExhausted => write!(f, "space invite is exhausted or revoked"),
Self::SpaceInviteExpired => write!(f, "space invite has expired"),
Self::SpaceOwnerRequired => {
write!(f, "space must have an owner; transfer ownership first")
}
Self::SpaceHasMembers => write!(f, "space still has active members"),
Self::SpaceInactive => write!(f, "space is not active"),
Self::SpaceMemberBanned => write!(f, "space membership is banned"),
Self::VersionMismatch { current } => {
write!(f, "resource version mismatch, current version is {current}")
}
Self::UserNotFound => write!(f, "user does not exist"),
Self::ScopeInsufficient { required } => write!(f, "scope `{required}` is missing"),
}
}
}
impl std::error::Error for SpaceError {}
impl From<deletion::DeletionError> for SpaceError {
fn from(error: deletion::DeletionError) -> Self {
use deletion::DeletionError;
match error {
DeletionError::Database(error) => Self::from(error),
DeletionError::Conflict(message) => Self::Conflict(message),
DeletionError::NotFound(message) => {
tracing::debug!(%message, "deletion target not found");
Self::NotFound
}
DeletionError::KenAuth(message)
| DeletionError::KenShell(message)
| DeletionError::Config(message) => Self::Internal(message),
}
}
}
impl From<session::SessionError> for SpaceError {
fn from(_: session::SessionError) -> Self {
Self::Unauthorized
}
}
impl From<export::KenAuthError> for SpaceError {
fn from(error: export::KenAuthError) -> Self {
match error {
export::KenAuthError::NotFound => Self::NotFound,
export::KenAuthError::Unauthenticated => Self::Unauthorized,
export::KenAuthError::Unavailable(message) => {
Self::Internal(format!("kenoauth unavailable: {message}"))
}
export::KenAuthError::Rejected(message) => {
let lower = message.to_lowercase();
if lower.contains("already") || lower.contains("taken") {
Self::Conflict(message)
} else if lower.contains("forbidden") || lower.contains("permission") {
Self::Forbidden
} else {
Self::BadRequest(message)
}
}
export::KenAuthError::Config(message) | export::KenAuthError::Internal(message) => {
Self::Internal(message)
}
}
}
}
export::db_error_conversions!(SpaceError);
export::storage_error_conversion!(SpaceError);
export::export_error_conversion!(SpaceError);
export::json_payload_conversion!(SpaceError);
export::api_error! {
log: "space",
pub enum ApiError {
BadRequest(400, "request failed validation"),
Unauthorized(401, "authentication required"),
Forbidden(403, "operation not allowed"),
NotFound(404, "resource not found"),
Conflict(409, "resource conflict"),
TooManyRequests(429, "rate limit exceeded"),
PreconditionFailed(412, "precondition failed"),
Internal(500, "internal server error"),
}
}
impl From<SpaceError> for ApiError {
fn from(error: SpaceError) -> Self {
let status = error.status_code().as_u16();
if error.is_internal() {
tracing::warn!(%error, "space handler failed");
}
Self::build(
status,
export::rest::AppResponseData::err(i32::from(status), error.message())
.with_error_code(error.code()),
)
}
}
impl SpaceError {
pub fn internal(message: impl Into<String>) -> Self {
Self::Internal(message.into())
}
pub fn status_code(&self) -> StatusCode {
match self {
Self::Unauthorized => StatusCode::UNAUTHORIZED,
Self::Forbidden
| Self::JoinNotAllowed(_)
| Self::JoinRequirementNotMet(_)
| Self::JoinRequestCooldown(_)
| Self::SpaceInactive
| Self::SpaceMemberBanned
| Self::ScopeInsufficient { .. } => StatusCode::FORBIDDEN,
Self::NotFound
| Self::SpaceNotFound
| Self::SpaceMemberNotFound
| Self::SpaceRoleNotFound
| Self::SpaceInviteNotFound
| Self::UserNotFound => StatusCode::NOT_FOUND,
Self::Conflict(_)
| Self::SpaceSlugTaken
| Self::SpaceMemberExists
| Self::SpaceRoleKeyTaken
| Self::SpaceHasMembers
| Self::VersionMismatch { .. } => StatusCode::CONFLICT,
Self::SpaceRoleInUse
| Self::SpaceRoleSystem
| Self::SpaceInviteExhausted
| Self::SpaceInviteExpired
| Self::SpaceOwnerRequired => StatusCode::PRECONDITION_FAILED,
Self::RateLimited(_) => StatusCode::TOO_MANY_REQUESTS,
Self::Internal(_) => StatusCode::INTERNAL_SERVER_ERROR,
Self::BadRequest(_)
| Self::MalformedRequest
| Self::InvalidEmail
| Self::InvalidPermission { .. } => StatusCode::BAD_REQUEST,
}
}
pub fn code(&self) -> &'static str {
match self {
Self::BadRequest(_) => "bad_request",
Self::Unauthorized => "unauthorized",
Self::Forbidden => "forbidden",
Self::JoinNotAllowed(_) => "join_not_allowed",
Self::JoinRequirementNotMet(_) => "join_requirement_not_met",
Self::JoinRequestCooldown(_) => "join_request_cooldown",
Self::NotFound => "not_found",
Self::Conflict(_) => "conflict",
Self::RateLimited(_) => "rate_limited",
Self::Internal(_) => "internal",
Self::MalformedRequest => "malformed_request",
Self::InvalidEmail => "invalid_email",
Self::InvalidPermission { .. } => "invalid_permission",
Self::SpaceNotFound => "space_not_found",
Self::SpaceMemberNotFound => "space_member_not_found",
Self::SpaceRoleNotFound => "space_role_not_found",
Self::SpaceInviteNotFound => "space_invite_not_found",
Self::SpaceSlugTaken => "space_slug_taken",
Self::SpaceMemberExists => "space_member_exists",
Self::SpaceRoleKeyTaken => "space_role_key_taken",
Self::SpaceRoleInUse => "space_role_in_use",
Self::SpaceRoleSystem => "space_role_system",
Self::SpaceInviteExhausted => "space_invite_exhausted",
Self::SpaceInviteExpired => "space_invite_expired",
Self::SpaceOwnerRequired => "space_owner_required",
Self::SpaceHasMembers => "space_has_members",
Self::SpaceInactive => "space_inactive",
Self::SpaceMemberBanned => "space_member_banned",
Self::VersionMismatch { .. } => "version_mismatch",
Self::UserNotFound => "user_not_found",
Self::ScopeInsufficient { .. } => "scope_insufficient",
}
}
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()
}
}
}
export::access_error_conversion!(SpaceError, BadRequest);