fix(handlers): add handlers
This commit is contained in:
222
src/services/auth.rs
Normal file
222
src/services/auth.rs
Normal 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(())
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user