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"), Internal(500, "internal server error"), } } impl From for ApiError { fn from(error: SpaceError) -> Self { match error { SpaceError::BadRequest(msg) => Self::bad_request(msg), SpaceError::Unauthorized => Self::unauthorized(), SpaceError::Forbidden => Self::forbidden(), SpaceError::JoinNotAllowed(msg) => Self::forbidden_with_code("join_not_allowed", msg), SpaceError::JoinRequirementNotMet(msg) => { Self::forbidden_with_code("join_requirement_not_met", msg) } SpaceError::JoinRequestCooldown(msg) => { Self::forbidden_with_code("join_request_cooldown", msg) } SpaceError::NotFound => Self::not_found("space resource not found"), SpaceError::Conflict(msg) => Self::conflict(msg), SpaceError::RateLimited(msg) => Self::rate_limited(msg), SpaceError::Internal(message) => Self::internal(message), SpaceError::MalformedRequest => Self::bad_request("request payload is malformed"), SpaceError::InvalidEmail => Self::bad_request("email is invalid"), SpaceError::InvalidPermission { permission } => { Self::bad_request(format!("permission `{permission}` is not recognized")) } SpaceError::SpaceNotFound => Self::not_found("space does not exist"), SpaceError::SpaceMemberNotFound => Self::not_found("space member does not exist"), SpaceError::SpaceRoleNotFound => Self::not_found("space role does not exist"), SpaceError::SpaceInviteNotFound => Self::not_found("space invite does not exist"), SpaceError::UserNotFound => Self::not_found("user does not exist"), SpaceError::SpaceSlugTaken => Self::conflict("space slug is already taken"), SpaceError::SpaceMemberExists => { Self::conflict("user is already a member of this space") } SpaceError::SpaceRoleKeyTaken => Self::conflict("space role key is already used"), SpaceError::SpaceHasMembers => Self::conflict("space still has active members"), SpaceError::VersionMismatch { .. } => Self::build( 409, Self::payload(409, "resource version mismatch").with_error_code("version_mismatch"), ), SpaceError::SpaceInactive => { Self::forbidden_with_code("space_inactive", "space is not active".to_string()) } SpaceError::SpaceMemberBanned => Self::forbidden_with_code( "space_member_banned", "space membership is banned".to_string(), ), SpaceError::ScopeInsufficient { required } => Self::forbidden_with_code( "scope_insufficient", format!("scope `{required}` is missing"), ), SpaceError::SpaceRoleInUse => Self::build( 412, Self::payload(412, "space role is still assigned to members") .with_error_code("space_role_in_use"), ), SpaceError::SpaceRoleSystem => Self::build( 412, Self::payload(412, "space role is built in and cannot be modified") .with_error_code("space_role_system"), ), SpaceError::SpaceInviteExhausted => Self::build( 412, Self::payload(412, "space invite is exhausted or revoked") .with_error_code("space_invite_exhausted"), ), SpaceError::SpaceInviteExpired => Self::build( 412, Self::payload(412, "space invite has expired") .with_error_code("space_invite_expired"), ), SpaceError::SpaceOwnerRequired => Self::build( 412, Self::payload(412, "space must have an owner; transfer ownership first") .with_error_code("space_owner_required"), ), } } } 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() } } pub fn body(&self) -> ErrorBody { ErrorBody { code: self.code().to_string(), message: self.message(), } } } #[derive(Debug, serde::Serialize)] pub struct ErrorBody { pub code: String, pub message: String, } impl actix_web::ResponseError for SpaceError { fn status_code(&self) -> StatusCode { self.status_code() } fn error_response(&self) -> actix_web::HttpResponse { actix_web::HttpResponse::build(self.status_code()).json(self.body()) } }