feat(saas): Phase 1 — 基础框架与账号管理模块
- 新增 zclaw-saas crate 作为 workspace 成员 - 配置系统 (TOML + 环境变量覆盖) - 错误类型体系 (SaasError 16 变体, IntoResponse) - SQLite 数据库 (12 表 schema, 内存/文件双模式, 3 系统角色种子数据) - JWT 认证 (签发/验证/刷新) - Argon2id 密码哈希 - 认证中间件 (公开/受保护路由分层) - 账号管理 CRUD + API Token 管理 + 操作日志 - 7 单元测试 + 5 集成测试全部通过
This commit is contained in:
119
crates/zclaw-saas/src/error.rs
Normal file
119
crates/zclaw-saas/src/error.rs
Normal file
@@ -0,0 +1,119 @@
|
||||
//! SaaS 错误类型
|
||||
|
||||
use axum::http::StatusCode;
|
||||
use axum::response::{IntoResponse, Response};
|
||||
use serde_json::json;
|
||||
|
||||
/// SaaS 服务错误类型
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
pub enum SaasError {
|
||||
#[error("未找到: {0}")]
|
||||
NotFound(String),
|
||||
|
||||
#[error("权限不足: {0}")]
|
||||
Forbidden(String),
|
||||
|
||||
#[error("未认证")]
|
||||
Unauthorized,
|
||||
|
||||
#[error("无效输入: {0}")]
|
||||
InvalidInput(String),
|
||||
|
||||
#[error("认证失败: {0}")]
|
||||
AuthError(String),
|
||||
|
||||
#[error("用户已存在: {0}")]
|
||||
AlreadyExists(String),
|
||||
|
||||
#[error("序列化错误: {0}")]
|
||||
Serialization(#[from] serde_json::Error),
|
||||
|
||||
#[error("IO 错误: {0}")]
|
||||
Io(#[from] std::io::Error),
|
||||
|
||||
#[error("数据库错误: {0}")]
|
||||
Database(#[from] sqlx::Error),
|
||||
|
||||
#[error("配置错误: {0}")]
|
||||
Config(#[from] toml::de::Error),
|
||||
|
||||
#[error("JWT 错误: {0}")]
|
||||
Jwt(#[from] jsonwebtoken::errors::Error),
|
||||
|
||||
#[error("密码哈希错误: {0}")]
|
||||
PasswordHash(String),
|
||||
|
||||
#[error("TOTP 错误: {0}")]
|
||||
Totp(String),
|
||||
|
||||
#[error("加密错误: {0}")]
|
||||
Encryption(String),
|
||||
|
||||
#[error("中转错误: {0}")]
|
||||
Relay(String),
|
||||
|
||||
#[error("速率限制: {0}")]
|
||||
RateLimited(String),
|
||||
|
||||
#[error("内部错误: {0}")]
|
||||
Internal(String),
|
||||
}
|
||||
|
||||
impl SaasError {
|
||||
/// 获取 HTTP 状态码
|
||||
pub fn status_code(&self) -> StatusCode {
|
||||
match self {
|
||||
Self::NotFound(_) => StatusCode::NOT_FOUND,
|
||||
Self::Forbidden(_) => StatusCode::FORBIDDEN,
|
||||
Self::Unauthorized => StatusCode::UNAUTHORIZED,
|
||||
Self::InvalidInput(_) => StatusCode::BAD_REQUEST,
|
||||
Self::AlreadyExists(_) => StatusCode::CONFLICT,
|
||||
Self::RateLimited(_) => StatusCode::TOO_MANY_REQUESTS,
|
||||
Self::Database(_) | Self::Internal(_) | Self::Io(_) | Self::Serialization(_) => StatusCode::INTERNAL_SERVER_ERROR,
|
||||
Self::AuthError(_) => StatusCode::UNAUTHORIZED,
|
||||
Self::Jwt(_) | Self::PasswordHash(_) | Self::Totp(_) | Self::Encryption(_) => {
|
||||
StatusCode::INTERNAL_SERVER_ERROR
|
||||
}
|
||||
Self::Config(_) => StatusCode::INTERNAL_SERVER_ERROR,
|
||||
Self::Relay(_) => StatusCode::BAD_GATEWAY,
|
||||
}
|
||||
}
|
||||
|
||||
/// 获取错误代码
|
||||
pub fn error_code(&self) -> &str {
|
||||
match self {
|
||||
Self::NotFound(_) => "NOT_FOUND",
|
||||
Self::Forbidden(_) => "FORBIDDEN",
|
||||
Self::Unauthorized => "UNAUTHORIZED",
|
||||
Self::InvalidInput(_) => "INVALID_INPUT",
|
||||
Self::AlreadyExists(_) => "ALREADY_EXISTS",
|
||||
Self::RateLimited(_) => "RATE_LIMITED",
|
||||
Self::Database(_) => "DATABASE_ERROR",
|
||||
Self::Io(_) => "IO_ERROR",
|
||||
Self::Serialization(_) => "SERIALIZATION_ERROR",
|
||||
Self::Internal(_) => "INTERNAL_ERROR",
|
||||
Self::AuthError(_) => "AUTH_ERROR",
|
||||
Self::Jwt(_) => "JWT_ERROR",
|
||||
Self::PasswordHash(_) => "PASSWORD_HASH_ERROR",
|
||||
Self::Totp(_) => "TOTP_ERROR",
|
||||
Self::Encryption(_) => "ENCRYPTION_ERROR",
|
||||
Self::Config(_) => "CONFIG_ERROR",
|
||||
Self::Relay(_) => "RELAY_ERROR",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 实现 Axum 响应
|
||||
impl IntoResponse for SaasError {
|
||||
fn into_response(self) -> Response {
|
||||
let status = self.status_code();
|
||||
let body = json!({
|
||||
"error": self.error_code(),
|
||||
"message": self.to_string(),
|
||||
});
|
||||
(status, axum::Json(body)).into_response()
|
||||
}
|
||||
}
|
||||
|
||||
/// Result 类型别名
|
||||
pub type SaasResult<T> = std::result::Result<T, SaasError>;
|
||||
Reference in New Issue
Block a user