use crate::models::{CreateUserRequest, LoginRequest, LoginResponse, User}; use crate::utils::authz::filter_permissions_by_enabled_apps; 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 { 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 { // 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?; let (enabled_apps, apps_version) = sqlx::query_as::<_, (Vec, i32)>( r#" SELECT enabled_apps, version FROM tenant_entitlements WHERE tenant_id = $1 "#, ) .bind(user.tenant_id) .fetch_optional(&self.pool) .await? .unwrap_or_else(|| (vec![], 0)); let permissions = filter_permissions_by_enabled_apps(permissions, &enabled_apps); // 3. 签发 Access Token let access_token = sign( user.id, user.tenant_id, roles, permissions, enabled_apps, apps_version, )?; // 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 WHERE ($2::uuid = '00000000-0000-0000-0000-000000000001' OR p.code NOT LIKE 'iam:%') ON CONFLICT DO NOTHING "#, ) .bind(role_id) .bind(tenant_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(()) } }