fix(handlers): add handlers

This commit is contained in:
2026-01-30 16:31:53 +08:00
parent bb82c75834
commit ce12b997f4
38 changed files with 3746 additions and 317 deletions

222
src/services/auth.rs Normal file
View File

@@ -0,0 +1,222 @@
use crate::models::{CreateUserRequest, LoginRequest, LoginResponse, User};
use crate::utils::{hash_password, sign, verify_password};
use common_telemetry::AppError;
use rand::RngCore;
use sqlx::PgPool;
use tracing::instrument;
use uuid::Uuid;
#[derive(Clone)]
pub struct AuthService {
pool: PgPool,
// jwt_secret removed, using RS256 keys
}
impl AuthService {
/// 创建认证服务实例。
///
/// 说明:
/// - 当前实现使用 RS256 密钥对进行 JWT 签发与校验,因此 `_jwt_secret` 参数仅为兼容保留。
pub fn new(pool: PgPool, _jwt_secret: String) -> Self {
Self { pool }
}
// 注册业务
#[instrument(skip(self, req))]
/// 在指定租户下注册新用户,并在首次注册时自动引导初始化租户管理员权限。
///
/// 业务规则:
/// - 用户必须绑定到 `tenant_id`,禁止跨租户注册写入。
/// - 密码以安全哈希形式存储,不回传明文。
/// - 若该租户用户数为 1首个用户自动创建/获取 `Admin` 系统角色并授予全量权限,同时绑定到该用户。
///
/// 输入:
/// - `tenant_id`:目标租户
/// - `req.email` / `req.password`:注册信息
///
/// 输出:
/// - 返回创建后的 `User` 记录(包含 `id/tenant_id/email` 等字段)
///
/// 异常:
/// - 数据库写入失败(如唯一约束冲突、连接错误等)
/// - 密码哈希失败
pub async fn register(
&self,
tenant_id: Uuid,
req: CreateUserRequest,
) -> Result<User, AppError> {
let mut tx = self.pool.begin().await?;
// 1. 哈希密码
let hashed =
hash_password(&req.password).map_err(|e| AppError::AnyhowError(anyhow::anyhow!(e)))?;
// 2. 存入数据库 (带上 tenant_id)
let query = r#"
INSERT INTO users (tenant_id, email, password_hash)
VALUES ($1, $2, $3)
RETURNING id, tenant_id, email, password_hash
"#;
let user = sqlx::query_as::<_, User>(query)
.bind(tenant_id)
.bind(&req.email)
.bind(&hashed)
.fetch_one(&mut *tx)
.await?;
let user_count: i64 = sqlx::query_scalar("SELECT COUNT(1) FROM users WHERE tenant_id = $1")
.bind(tenant_id)
.fetch_one(&mut *tx)
.await?;
if user_count == 1 {
self.bootstrap_tenant_admin(&mut tx, tenant_id, user.id)
.await?;
}
tx.commit().await?;
Ok(user)
}
// 登录业务
#[instrument(skip(self, req))]
/// 在指定租户内完成用户认证并签发访问令牌与刷新令牌。
///
/// 业务规则:
/// - 仅在当前租户内按 `email` 查找用户,防止跨租户登录。
/// - 密码校验失败返回 `InvalidCredentials`。
/// - 登录成功后生成:
/// - `access_token`JWT包含租户、用户、角色与权限
/// - `refresh_token`:随机生成并哈希后入库(默认 30 天过期)
///
/// 输出:
/// - `LoginResponse`token_type 固定为 `Bearer``expires_in` 当前为 15 分钟)
///
/// 异常:
/// - 用户不存在404
/// - 密码错误401
/// - Token 签发失败或数据库写入失败
pub async fn login(
&self,
tenant_id: Uuid,
req: LoginRequest,
) -> Result<LoginResponse, AppError> {
// 1. 查找用户 (带 tenant_id 防止跨租户登录)
let query = "SELECT * FROM users WHERE tenant_id = $1 AND email = $2";
let user = sqlx::query_as::<_, User>(query)
.bind(tenant_id)
.bind(&req.email)
.fetch_optional(&self.pool)
.await?
.ok_or(AppError::NotFound("User not found".into()))?;
// 2. 验证密码
if !verify_password(&req.password, &user.password_hash) {
return Err(AppError::InvalidCredentials);
}
let roles = sqlx::query_scalar::<_, String>(
r#"
SELECT r.name
FROM roles r
JOIN user_roles ur ON ur.role_id = r.id
WHERE r.tenant_id = $1 AND ur.user_id = $2
"#,
)
.bind(user.tenant_id)
.bind(user.id)
.fetch_all(&self.pool)
.await?;
let permissions = sqlx::query_scalar::<_, String>(
r#"
SELECT DISTINCT p.code
FROM permissions p
JOIN role_permissions rp ON rp.permission_id = p.id
JOIN user_roles ur ON ur.role_id = rp.role_id
JOIN roles r ON r.id = ur.role_id
WHERE r.tenant_id = $1 AND ur.user_id = $2
"#,
)
.bind(user.tenant_id)
.bind(user.id)
.fetch_all(&self.pool)
.await?;
// 3. 签发 Access Token
let access_token = sign(user.id, user.tenant_id, roles, permissions)?;
// 4. 生成 Refresh Token
let mut refresh_bytes = [0u8; 32];
rand::rng().fill_bytes(&mut refresh_bytes);
let refresh_token = hex::encode(refresh_bytes);
// Hash refresh token for storage
let refresh_token_hash =
hash_password(&refresh_token).map_err(|e| AppError::AnyhowError(anyhow::anyhow!(e)))?;
// 5. 存储 Refresh Token (30天过期)
let expires_at = chrono::Utc::now() + chrono::Duration::days(30);
sqlx::query(
"INSERT INTO refresh_tokens (user_id, token_hash, expires_at) VALUES ($1, $2, $3)",
)
.bind(user.id)
.bind(refresh_token_hash)
.bind(expires_at)
.execute(&self.pool)
.await?;
Ok(LoginResponse {
access_token,
refresh_token,
token_type: "Bearer".to_string(),
expires_in: 15 * 60, // 15 mins
})
}
async fn bootstrap_tenant_admin(
&self,
tx: &mut sqlx::Transaction<'_, sqlx::Postgres>,
tenant_id: Uuid,
user_id: Uuid,
) -> Result<(), AppError> {
let role_id: Uuid = sqlx::query_scalar(
r#"
INSERT INTO roles (tenant_id, name, description, is_system)
VALUES ($1, 'Admin', 'Tenant administrator', TRUE)
ON CONFLICT (tenant_id, name)
DO UPDATE SET name = EXCLUDED.name
RETURNING id
"#,
)
.bind(tenant_id)
.fetch_one(&mut **tx)
.await?;
sqlx::query(
r#"
INSERT INTO role_permissions (role_id, permission_id)
SELECT $1, p.id FROM permissions p
ON CONFLICT DO NOTHING
"#,
)
.bind(role_id)
.execute(&mut **tx)
.await?;
sqlx::query(
r#"
INSERT INTO user_roles (user_id, role_id)
VALUES ($1, $2)
ON CONFLICT DO NOTHING
"#,
)
.bind(user_id)
.bind(role_id)
.execute(&mut **tx)
.await?;
Ok(())
}
}