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 = Result; 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 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 for SpaceError { fn from(_: session::SessionError) -> Self { Self::Unauthorized } } impl From 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 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) -> 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);