kagi-vault 0.1.2

Encrypted secrets and environment variable manager for teams — a secure, team-ready dotenv alternative with per-service isolation
use axum::Json;
use axum::http::StatusCode;
use axum::response::{IntoResponse, Response};
use serde_json::json;

#[derive(Debug)]
pub enum ServerError {
    BadRequest(String),
    BadEnvelope(String),
    DecryptFailed(String),
    AuthFailed,
    Forbidden,
    NotFound,
    Conflict {
        code: String,
        message: String,
        details: Option<serde_json::Value>,
    },
    InvalidPath(String),
    InvalidRevision,
    InvalidProjectState(String),
    ServerKeyMismatch,
    PayloadTooLarge,
    Internal(String),
}

impl IntoResponse for ServerError {
    fn into_response(self) -> Response {
        let (status, code, message, details): (
            StatusCode,
            &str,
            String,
            Option<serde_json::Value>,
        ) = match &self {
            ServerError::BadRequest(msg) => {
                (StatusCode::BAD_REQUEST, "bad_request", msg.clone(), None)
            }
            ServerError::BadEnvelope(msg) => {
                (StatusCode::BAD_REQUEST, "bad_envelope", msg.clone(), None)
            }
            ServerError::DecryptFailed(msg) => {
                (StatusCode::BAD_REQUEST, "decrypt_failed", msg.clone(), None)
            }
            ServerError::AuthFailed => (
                StatusCode::UNAUTHORIZED,
                "auth_failed",
                "authentication failed".into(),
                None,
            ),
            ServerError::Forbidden => (
                StatusCode::FORBIDDEN,
                "forbidden",
                "insufficient capabilities".into(),
                None,
            ),
            ServerError::NotFound => (
                StatusCode::NOT_FOUND,
                "not_found",
                "resource not found".into(),
                None,
            ),
            ServerError::Conflict {
                code,
                message,
                details,
            } => {
                return (StatusCode::CONFLICT, Json(json!({"ok": false, "error": {"code": code, "message": message, "details": details}}))).into_response();
            }
            ServerError::InvalidPath(msg) => {
                (StatusCode::BAD_REQUEST, "invalid_path", msg.clone(), None)
            }
            ServerError::InvalidRevision => (
                StatusCode::BAD_REQUEST,
                "invalid_revision",
                "revision mismatch".into(),
                None,
            ),
            ServerError::InvalidProjectState(msg) => (
                StatusCode::BAD_REQUEST,
                "invalid_project_state",
                msg.clone(),
                None,
            ),
            ServerError::ServerKeyMismatch => (
                StatusCode::BAD_REQUEST,
                "server_key_mismatch",
                "unknown server key".into(),
                None,
            ),
            ServerError::PayloadTooLarge => (
                StatusCode::PAYLOAD_TOO_LARGE,
                "payload_too_large",
                "project storage limit exceeded".into(),
                None,
            ),
            ServerError::Internal(msg) => (
                StatusCode::INTERNAL_SERVER_ERROR,
                "internal",
                msg.clone(),
                None,
            ),
        };

        let body = Json(json!({
            "ok": false,
            "error": { "code": code, "message": message, "details": details }
        }));
        (status, body).into_response()
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use axum::response::IntoResponse;

    fn status_of(err: ServerError) -> StatusCode {
        err.into_response().status()
    }

    #[test]
    fn test_bad_request_status() {
        assert_eq!(
            status_of(ServerError::BadRequest("x".into())),
            StatusCode::BAD_REQUEST
        );
    }

    #[test]
    fn test_auth_failed_status() {
        assert_eq!(status_of(ServerError::AuthFailed), StatusCode::UNAUTHORIZED);
    }

    #[test]
    fn test_forbidden_status() {
        assert_eq!(status_of(ServerError::Forbidden), StatusCode::FORBIDDEN);
    }

    #[test]
    fn test_not_found_status() {
        assert_eq!(status_of(ServerError::NotFound), StatusCode::NOT_FOUND);
    }

    #[test]
    fn test_conflict_status() {
        let err = ServerError::Conflict {
            code: "c".into(),
            message: "m".into(),
            details: None,
        };
        assert_eq!(status_of(err), StatusCode::CONFLICT);
    }

    #[test]
    fn test_internal_status() {
        assert_eq!(
            status_of(ServerError::Internal("x".into())),
            StatusCode::INTERNAL_SERVER_ERROR
        );
    }

    #[test]
    fn test_payload_too_large_status() {
        assert_eq!(
            status_of(ServerError::PayloadTooLarge),
            StatusCode::PAYLOAD_TOO_LARGE
        );
    }
}