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(())
}
}

View File

@@ -0,0 +1,79 @@
use common_telemetry::AppError;
use sqlx::PgPool;
use tracing::instrument;
use uuid::Uuid;
#[derive(Clone)]
pub struct AuthorizationService {
pool: PgPool,
}
impl AuthorizationService {
/// 创建权限服务实例。
pub fn new(pool: PgPool) -> Self {
Self { pool }
}
#[instrument(skip(self))]
/// 获取用户在指定租户下的权限编码集合(去重)。
///
/// 说明:
/// - 权限来源于用户所属角色user_roles → roles及角色绑定权限role_permissions → permissions
///
/// 输入:
/// - `tenant_id`:租户 ID
/// - `user_id`:用户 ID
///
/// 输出:
/// - 权限编码数组(如 `tenant:read` / `user:write`
///
/// 异常:
/// - 数据库查询失败
pub async fn list_permissions_for_user(
&self,
tenant_id: Uuid,
user_id: Uuid,
) -> Result<Vec<String>, AppError> {
let query = 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
"#;
let rows = sqlx::query_scalar::<_, String>(query)
.bind(tenant_id)
.bind(user_id)
.fetch_all(&self.pool)
.await?;
Ok(rows)
}
#[instrument(skip(self))]
/// 校验用户是否具备指定权限,不满足则直接返回权限拒绝错误。
///
/// 业务规则:
/// - 若用户权限集合中不包含 `permission_code`,返回 `PermissionDenied(permission_code)`。
///
/// 输入:
/// - `tenant_id`:租户 ID
/// - `user_id`:用户 ID
/// - `permission_code`:权限编码
///
/// 输出:
/// - 成功返回 `()`;失败返回权限拒绝错误
pub async fn require_permission(
&self,
tenant_id: Uuid,
user_id: Uuid,
permission_code: &str,
) -> Result<(), AppError> {
let permissions = self.list_permissions_for_user(tenant_id, user_id).await?;
if permissions.iter().any(|p| p == permission_code) {
Ok(())
} else {
Err(AppError::PermissionDenied(permission_code.to_string()))
}
}
}

View File

@@ -1,69 +1,11 @@
use crate::models::{CreateUserRequest, User}; // 假设你在 models 定义了这些
use crate::utils::{create_jwt, hash_password, verify_password};
use axum::Json;
use sqlx::PgPool;
use uuid::Uuid;
pub mod auth;
pub mod authorization;
pub mod role;
pub mod tenant;
pub mod user;
#[derive(Clone)]
pub struct AuthService {
pool: PgPool,
jwt_secret: String,
}
impl AuthService {
pub fn new(pool: PgPool, jwt_secret: String) -> Self {
Self { pool, jwt_secret }
}
// 注册业务
pub async fn register(
&self,
tenant_id: Uuid,
req: CreateUserRequest,
) -> Result<Json<User>, String> {
// 1. 哈希密码
let hashed = hash_password(&req.password)?;
// 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, created_at
"#;
let user = sqlx::query_as::<_, User>(query)
.bind(tenant_id)
.bind(&req.email)
.bind(&hashed)
.fetch_one(&self.pool)
.await
.map_err(|e| e.to_string())?;
Ok(Json(user))
}
// 登录业务
pub async fn login(
&self,
tenant_id: Uuid,
email: &str,
password: &str,
) -> Result<String, String> {
// 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(email)
.fetch_optional(&self.pool)
.await
.map_err(|e| e.to_string())?
.ok_or("User not found")?;
// 2. 验证密码
if !verify_password(password, &user.password_hash) {
return Err("Invalid password".to_string());
}
// 3. 签发 Token
create_jwt(user.id, user.tenant_id, &self.jwt_secret)
}
}
pub use auth::AuthService;
pub use authorization::AuthorizationService;
pub use role::RoleService;
pub use tenant::TenantService;
pub use user::UserService;

59
src/services/role.rs Normal file
View File

@@ -0,0 +1,59 @@
use crate::models::{CreateRoleRequest, Role};
use common_telemetry::AppError;
use sqlx::PgPool;
use tracing::instrument;
use uuid::Uuid;
#[derive(Clone)]
pub struct RoleService {
pool: PgPool,
}
impl RoleService {
/// 创建角色服务实例。
pub fn new(pool: PgPool) -> Self {
Self { pool }
}
#[instrument(skip(self))]
/// 在指定租户下创建角色记录。
///
/// 业务规则:
/// - 角色与租户强绑定(写入时携带 `tenant_id`)。
///
/// 异常:
/// - 数据库写入失败(如约束冲突、连接错误等)
pub async fn create_role(
&self,
tenant_id: Uuid,
req: CreateRoleRequest,
) -> Result<Role, AppError> {
let query = r#"
INSERT INTO roles (tenant_id, name, description)
VALUES ($1, $2, $3)
RETURNING id, tenant_id, name, description
"#;
// Note: 'roles' table needs to be created in DB
sqlx::query_as::<_, Role>(query)
.bind(tenant_id)
.bind(req.name)
.bind(req.description)
.fetch_one(&self.pool)
.await
.map_err(|e| AppError::DbError(e))
}
#[instrument(skip(self))]
/// 查询指定租户下的角色列表。
///
/// 异常:
/// - 数据库查询失败
pub async fn list_roles(&self, tenant_id: Uuid) -> Result<Vec<Role>, AppError> {
let query = "SELECT * FROM roles WHERE tenant_id = $1";
sqlx::query_as::<_, Role>(query)
.bind(tenant_id)
.fetch_all(&self.pool)
.await
.map_err(|e| AppError::DbError(e))
}
}

134
src/services/tenant.rs Normal file
View File

@@ -0,0 +1,134 @@
use crate::models::{
CreateTenantRequest, Tenant, UpdateTenantRequest, UpdateTenantStatusRequest,
};
use common_telemetry::AppError;
use serde_json::Value;
use sqlx::PgPool;
use tracing::instrument;
use uuid::Uuid;
#[derive(Clone)]
pub struct TenantService {
pool: PgPool,
}
impl TenantService {
/// 创建租户服务实例。
pub fn new(pool: PgPool) -> Self {
Self { pool }
}
#[instrument(skip(self, req))]
/// 创建新租户并初始化默认状态与配置。
///
/// 业务规则:
/// - 默认 `status=active`。
/// - `config` 未提供时默认 `{}`。
///
/// 输出:
/// - 返回新建租户记录(含 `id`
///
/// 异常:
/// - 数据库写入失败(如连接异常、约束失败等)
pub async fn create_tenant(&self, req: CreateTenantRequest) -> Result<Tenant, AppError> {
let config = req.config.unwrap_or_else(|| Value::Object(Default::default()));
let query = r#"
INSERT INTO tenants (name, status, config)
VALUES ($1, 'active', $2)
RETURNING id, name, status, config
"#;
let tenant = sqlx::query_as::<_, Tenant>(query)
.bind(req.name)
.bind(config)
.fetch_one(&self.pool)
.await?;
Ok(tenant)
}
#[instrument(skip(self))]
/// 根据租户 ID 查询租户信息。
///
/// 异常:
/// - 若租户不存在,返回 `NotFound("Tenant not found")`。
pub async fn get_tenant(&self, tenant_id: Uuid) -> Result<Tenant, AppError> {
let query = "SELECT id, name, status, config FROM tenants WHERE id = $1";
sqlx::query_as::<_, Tenant>(query)
.bind(tenant_id)
.fetch_optional(&self.pool)
.await?
.ok_or_else(|| AppError::NotFound("Tenant not found".into()))
}
#[instrument(skip(self, req))]
/// 更新租户基础信息(名称 / 配置)。
///
/// 说明:
/// - 仅更新 `UpdateTenantRequest` 中提供的字段,未提供字段保持不变。
///
/// 异常:
/// - 若租户不存在,返回 `NotFound("Tenant not found")`。
pub async fn update_tenant(
&self,
tenant_id: Uuid,
req: UpdateTenantRequest,
) -> Result<Tenant, AppError> {
let query = r#"
UPDATE tenants
SET
name = COALESCE($1, name),
config = COALESCE($2, config),
updated_at = NOW()
WHERE id = $3
RETURNING id, name, status, config
"#;
sqlx::query_as::<_, Tenant>(query)
.bind(req.name)
.bind(req.config)
.bind(tenant_id)
.fetch_optional(&self.pool)
.await?
.ok_or_else(|| AppError::NotFound("Tenant not found".into()))
}
#[instrument(skip(self, req))]
/// 更新租户状态字段(如 active / disabled
///
/// 异常:
/// - 若租户不存在,返回 `NotFound("Tenant not found")`。
pub async fn update_tenant_status(
&self,
tenant_id: Uuid,
req: UpdateTenantStatusRequest,
) -> Result<Tenant, AppError> {
let query = r#"
UPDATE tenants
SET
status = $1,
updated_at = NOW()
WHERE id = $2
RETURNING id, name, status, config
"#;
sqlx::query_as::<_, Tenant>(query)
.bind(req.status)
.bind(tenant_id)
.fetch_optional(&self.pool)
.await?
.ok_or_else(|| AppError::NotFound("Tenant not found".into()))
}
#[instrument(skip(self))]
/// 删除指定租户。
///
/// 异常:
/// - 若租户不存在,返回 `NotFound("Tenant not found")`。
pub async fn delete_tenant(&self, tenant_id: Uuid) -> Result<(), AppError> {
let result = sqlx::query("DELETE FROM tenants WHERE id = $1")
.bind(tenant_id)
.execute(&self.pool)
.await?;
if result.rows_affected() == 0 {
return Err(AppError::NotFound("Tenant not found".into()));
}
Ok(())
}
}

109
src/services/user.rs Normal file
View File

@@ -0,0 +1,109 @@
use crate::models::{UpdateUserRequest, User};
use common_telemetry::AppError;
use sqlx::PgPool;
use tracing::instrument;
use uuid::Uuid;
#[derive(Clone)]
pub struct UserService {
pool: PgPool,
}
impl UserService {
/// 创建用户服务实例。
pub fn new(pool: PgPool) -> Self {
Self { pool }
}
#[instrument(skip(self))]
/// 根据用户 ID 查询用户记录(限定在指定租户内)。
///
/// 业务规则:
/// - 查询条件同时包含 `tenant_id` 与 `user_id`,避免跨租户读取。
///
/// 异常:
/// - 用户不存在返回 `NotFound("User not found")`
pub async fn get_user_by_id(&self, tenant_id: Uuid, user_id: Uuid) -> Result<User, AppError> {
let query = "SELECT * FROM users WHERE tenant_id = $1 AND id = $2";
sqlx::query_as::<_, User>(query)
.bind(tenant_id)
.bind(user_id)
.fetch_optional(&self.pool)
.await
.map_err(|e| AppError::DbError(e))?
.ok_or_else(|| AppError::NotFound("User not found".into()))
}
#[instrument(skip(self))]
/// 分页查询租户下用户列表。
///
/// 说明:
/// - `offset = (page - 1) * page_size`,由上层负责保证 `page>=1`。
///
/// 异常:
/// - 数据库查询失败
pub async fn list_users(
&self,
tenant_id: Uuid,
page: u32,
page_size: u32,
) -> Result<Vec<User>, AppError> {
let offset = (page - 1) * page_size;
let query = "SELECT * FROM users WHERE tenant_id = $1 LIMIT $2 OFFSET $3";
sqlx::query_as::<_, User>(query)
.bind(tenant_id)
.bind(page_size as i64)
.bind(offset as i64)
.fetch_all(&self.pool)
.await
.map_err(|e| AppError::DbError(e))
}
#[instrument(skip(self))]
/// 更新指定用户信息(目前仅支持邮箱字段)。
///
/// 业务规则:
/// - 查询条件同时包含 `tenant_id` 与 `user_id`,避免跨租户更新。
/// - `UpdateUserRequest` 中未提供字段保持不变。
///
/// 异常:
/// - 用户不存在返回 `NotFound("User not found")`
pub async fn update_user(
&self,
tenant_id: Uuid,
user_id: Uuid,
req: UpdateUserRequest,
) -> Result<User, AppError> {
// Simple update implementation
// In a real app, you'd build the query dynamically based on Option fields
let query = "UPDATE users SET email = COALESCE($1, email) WHERE tenant_id = $2 AND id = $3 RETURNING *";
sqlx::query_as::<_, User>(query)
.bind(req.email)
.bind(tenant_id)
.bind(user_id)
.fetch_optional(&self.pool)
.await
.map_err(|e| AppError::DbError(e))?
.ok_or_else(|| AppError::NotFound("User not found".into()))
}
#[instrument(skip(self))]
/// 删除指定用户(限定在指定租户内)。
///
/// 异常:
/// - 用户不存在返回 `NotFound("User not found")`
pub async fn delete_user(&self, tenant_id: Uuid, user_id: Uuid) -> Result<(), AppError> {
let query = "DELETE FROM users WHERE tenant_id = $1 AND id = $2";
let result = sqlx::query(query)
.bind(tenant_id)
.bind(user_id)
.execute(&self.pool)
.await
.map_err(|e| AppError::DbError(e))?;
if result.rows_affected() == 0 {
return Err(AppError::NotFound("User not found".into()));
}
Ok(())
}
}