2ae9196
ZhenYi
feat(lib/git): add HTTP handler tree with routes and errors across analysis, archives, blobs, branches, commits, conflicts, content, diffs, hooks, lfs, logs, maintenance, packs, patches, pipelines, refs, remotes, repos, signatures, submodules, tags, trees
2ae9196
use std::fmt;
use axum::extract::rejection::{PathRejection, QueryRejection};
use axum::http::StatusCode;
use axum::response::{IntoResponse, Response};
use axum::Json;
use export::rest::{AppResponseData, NoData};
use utoipa::IntoResponses;
use crate::pow::PowError;
#[derive(Debug)]
pub enum AuthError {
BadRequest(String),
InvalidCredentials,
Unauthorized,
Forbidden,
NotFound,
Conflict(String),
RateLimited,
PowFailed,
Internal(String),
}
impl fmt::Display for AuthError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::BadRequest(message) => write!(f, "{message}"),
Self::InvalidCredentials => write!(f, "invalid credentials"),
Self::Unauthorized => write!(f, "unauthorized"),
Self::Forbidden => write!(f, "forbidden"),
Self::NotFound => write!(f, "not found"),
Self::Conflict(message) => write!(f, "{message}"),
Self::RateLimited => write!(f, "rate limit exceeded"),
Self::PowFailed => write!(f, "proof of work failed"),
Self::Internal(message) => write!(f, "{message}"),
}
}
}
impl std::error::Error for AuthError {}
impl From<PowError> for AuthError {
fn from(error: PowError) -> Self {
match error {
PowError::NotConfigured | PowError::InvalidKey => {
Self::Internal("pow service is not configured".to_string())
}
PowError::Missing => Self::BadRequest("pow challenge missing".to_string()),
PowError::Expired => Self::BadRequest("pow challenge expired".to_string()),
PowError::Malformed | PowError::InvalidSolution => {
Self::BadRequest("invalid pow solution".to_string())
}
PowError::InvalidSignature => Self::BadRequest("invalid pow challenge".to_string()),
}
}
}
impl From<sea_orm::DbErr> for AuthError {
fn from(error: sea_orm::DbErr) -> Self {
if is_unique_violation(&error) {
return Self::Conflict("resource already exists".to_string());
}
Self::Internal(format!("database error: {error}"))
}
}
impl From<sea_orm::TransactionError<sea_orm::DbErr>> for AuthError {
fn from(error: sea_orm::TransactionError<sea_orm::DbErr>) -> Self {
match &error {
sea_orm::TransactionError::Transaction(db_err) if is_unique_violation(db_err) => {
Self::Conflict("resource already exists".to_string())
}
_ => Self::Internal(format!("database transaction error: {error}")),
}
}
}
fn is_unique_violation(err: &sea_orm::DbErr) -> bool {
let msg = err.to_string().to_lowercase();
msg.contains("unique") || msg.contains("duplicate") || msg.contains("23505")
}
impl From<redis::RedisError> for AuthError {
fn from(error: redis::RedisError) -> Self {
Self::Internal(format!("redis error: {error}"))
}
}
impl From<export::ExportError> for AuthError {
fn from(error: export::ExportError) -> Self {
Self::Internal(format!("infrastructure error: {error}"))
}
}
impl From<serde_json::Error> for AuthError {
fn from(error: serde_json::Error) -> Self {
Self::BadRequest(format!("invalid payload: {error}"))
}
}
#[derive(Debug, IntoResponses)]
pub enum ApiError {
#[response(status = 400, description = "request failed validation")]
BadRequest(#[to_schema] AppResponseData<NoData>),
#[response(status = 400, description = "credentials rejected")]
InvalidCredentials(#[to_schema] AppResponseData<NoData>),
#[response(status = 401, description = "authentication required")]
Unauthorized(#[to_schema] AppResponseData<NoData>),
#[response(status = 403, description = "operation not allowed")]
Forbidden(#[to_schema] AppResponseData<NoData>),
#[response(status = 404, description = "resource not found")]
NotFound(#[to_schema] AppResponseData<NoData>),
#[response(status = 409, description = "resource conflict")]
Conflict(#[to_schema] AppResponseData<NoData>),
#[response(status = 429, description = "rate limit exceeded")]
RateLimited(#[to_schema] AppResponseData<NoData>),
#[response(status = 500, description = "internal server error")]
Internal(#[to_schema] AppResponseData<NoData>),
}
impl ApiError {
fn payload(code: i32, message: &'static str) -> AppResponseData<NoData> {
AppResponseData::err(code, message)
}
fn decompose(self) -> (u16, AppResponseData<NoData>) {
match self {
Self::BadRequest(envelope) => (400, envelope),
Self::InvalidCredentials(envelope) => (400, envelope),
Self::Unauthorized(envelope) => (401, envelope),
Self::Forbidden(envelope) => (403, envelope),
Self::NotFound(envelope) => (404, envelope),
Self::Conflict(envelope) => (409, envelope),
Self::RateLimited(envelope) => (429, envelope),
Self::Internal(envelope) => (500, envelope),
}
}
}
impl IntoResponse for ApiError {
fn into_response(self) -> Response {
let (code, envelope) = self.decompose();
let status = StatusCode::from_u16(code).unwrap_or_else(|_| StatusCode::INTERNAL_SERVER_ERROR);
(status, Json(envelope)).into_response()
}
}
impl From<AuthError> for ApiError {
fn from(error: AuthError) -> Self {
match error {
AuthError::BadRequest(msg) => {
Self::BadRequest(AppResponseData::err(400, msg))
}
AuthError::PowFailed => {
Self::BadRequest(Self::payload(400, "proof of work failed"))
}
AuthError::InvalidCredentials => {
Self::InvalidCredentials(Self::payload(400, "invalid credentials"))
}
AuthError::Unauthorized => Self::Unauthorized(Self::payload(401, "unauthorized")),
AuthError::Forbidden => Self::Forbidden(Self::payload(403, "forbidden")),
AuthError::NotFound => Self::NotFound(Self::payload(404, "not found")),
AuthError::Conflict(msg) => {
Self::Conflict(AppResponseData::err(409, msg))
}
AuthError::RateLimited => Self::RateLimited(Self::payload(429, "rate limit exceeded")),
AuthError::Internal(message) => {
tracing::warn!(error = %message, "auth handler failed");
Self::Internal(Self::payload(500, "internal server error"))
}
}
}
}
impl From<Box<ApiError>> for ApiError {
fn from(error: Box<ApiError>) -> Self {
*error
}
}
impl From<AppResponseData<()>> for ApiError {
fn from(_error: AppResponseData<()>) -> Self {
Self::BadRequest(Self::payload(400, "bad request"))
}
}
impl From<PathRejection> for ApiError {
fn from(_error: PathRejection) -> Self {
Self::BadRequest(Self::payload(400, "bad request"))
}
}
impl From<QueryRejection> for ApiError {
fn from(_error: QueryRejection) -> Self {
Self::BadRequest(Self::payload(400, "bad request"))
}
}
impl From<(StatusCode, &'static str)> for ApiError {
fn from(_error: (StatusCode, &'static str)) -> Self {
Self::Internal(Self::payload(500, "internal server error"))
}
}