Files
hms/crates/erp-auth/src/module.rs
iven bf8bcdbd5d fix: E2E 测试发现的后端 BUG 修复 — 限流拆分 + 积分查询 + 错误码修正
- 拆分 refresh token 限流为独立中间件(30次/分 vs 登录5次/分)
- 修复积分 recent-activity 500:JOIN 通过 points_account 中间表
- 修复患者/医生不存在返回 400 → 正确的 404 NotFound
2026-05-15 22:58:02 +08:00

373 lines
13 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
use axum::Router;
use uuid::Uuid;
use erp_core::error::AppResult;
use erp_core::events::EventBus;
use erp_core::module::{ErpModule, PermissionDescriptor};
use crate::handler::{auth_handler, org_handler, role_handler, user_handler, wechat_handler};
/// Auth module implementing the `ErpModule` trait.
///
/// Manages identity, authentication, and user CRUD within the ERP platform.
/// This module has no dependencies on other business modules.
pub struct AuthModule;
impl AuthModule {
pub fn new() -> Self {
Self
}
/// Build public (unauthenticated) routes for the auth module.
///
/// These routes do not require a valid JWT token.
/// The caller wraps this into whatever state type the application uses.
pub fn public_routes<S>() -> Router<S>
where
crate::auth_state::AuthState: axum::extract::FromRef<S>,
S: Clone + Send + Sync + 'static,
{
Router::new()
.route("/auth/login", axum::routing::post(auth_handler::login))
.route(
"/auth/wechat/login",
axum::routing::post(wechat_handler::wechat_login),
)
.route(
"/auth/wechat/bind-phone",
axum::routing::post(wechat_handler::wechat_bind_phone),
)
}
/// Refresh token routes — public but with higher rate limit (30/min vs 5/min for login).
pub fn refresh_routes<S>() -> Router<S>
where
crate::auth_state::AuthState: axum::extract::FromRef<S>,
S: Clone + Send + Sync + 'static,
{
Router::new().route("/auth/refresh", axum::routing::post(auth_handler::refresh))
}
/// Build protected (authenticated) routes for the auth module.
///
/// These routes require a valid JWT token, verified by the middleware layer.
/// The caller wraps this into whatever state type the application uses.
pub fn protected_routes<S>() -> Router<S>
where
crate::auth_state::AuthState: axum::extract::FromRef<S>,
S: Clone + Send + Sync + 'static,
{
Router::new()
.route("/auth/logout", axum::routing::post(auth_handler::logout))
.route(
"/auth/change-password",
axum::routing::post(auth_handler::change_password),
)
.route(
"/users",
axum::routing::get(user_handler::list_users).post(user_handler::create_user),
)
.route(
"/users/{id}",
axum::routing::get(user_handler::get_user)
.put(user_handler::update_user)
.delete(user_handler::delete_user),
)
.route(
"/users/{id}/roles",
axum::routing::post(user_handler::assign_roles),
)
.route(
"/users/{id}/reset-password",
axum::routing::post(user_handler::reset_password),
)
.route(
"/roles",
axum::routing::get(role_handler::list_roles).post(role_handler::create_role),
)
// 精确匹配 /roles/permissions必须在 /roles/{id} 之前注册
.route(
"/roles/permissions",
axum::routing::get(role_handler::list_permissions),
)
.route(
"/roles/{id}",
axum::routing::get(role_handler::get_role)
.put(role_handler::update_role)
.delete(role_handler::delete_role),
)
.route(
"/roles/{id}/permissions",
axum::routing::get(role_handler::get_role_permissions)
.post(role_handler::assign_permissions),
)
.route(
"/permissions",
axum::routing::get(role_handler::list_permissions),
)
// Organization routes
.route(
"/organizations",
axum::routing::get(org_handler::list_organizations)
.post(org_handler::create_organization),
)
.route(
"/organizations/{id}",
axum::routing::put(org_handler::update_organization)
.delete(org_handler::delete_organization),
)
// Department routes (nested under organization)
.route(
"/organizations/{org_id}/departments",
axum::routing::get(org_handler::list_departments)
.post(org_handler::create_department),
)
.route(
"/departments/{id}",
axum::routing::put(org_handler::update_department)
.delete(org_handler::delete_department),
)
// Position routes (nested under department)
.route(
"/departments/{dept_id}/positions",
axum::routing::get(org_handler::list_positions).post(org_handler::create_position),
)
.route(
"/positions/{id}",
axum::routing::put(org_handler::update_position)
.delete(org_handler::delete_position),
)
}
}
impl Default for AuthModule {
fn default() -> Self {
Self::new()
}
}
#[async_trait::async_trait]
impl ErpModule for AuthModule {
fn name(&self) -> &str {
"auth"
}
fn version(&self) -> &str {
env!("CARGO_PKG_VERSION")
}
fn dependencies(&self) -> Vec<&str> {
// Auth is a foundational module with no business-module dependencies.
vec![]
}
fn register_event_handlers(&self, _bus: &EventBus) {
// Auth 模块暂无跨模块事件订阅需求
}
async fn on_tenant_created(
&self,
tenant_id: Uuid,
db: &sea_orm::DatabaseConnection,
_event_bus: &EventBus,
) -> AppResult<()> {
let password = std::env::var("ERP__SUPER_ADMIN_PASSWORD").map_err(|_| {
tracing::error!("环境变量 ERP__SUPER_ADMIN_PASSWORD 未设置,无法初始化租户认证");
erp_core::error::AppError::Internal("ERP__SUPER_ADMIN_PASSWORD 未设置".to_string())
})?;
crate::service::seed::seed_tenant_auth(db, tenant_id, &password)
.await
.map_err(|e| erp_core::error::AppError::Internal(e.to_string()))?;
tracing::info!(tenant_id = %tenant_id, "Tenant auth initialized");
Ok(())
}
async fn on_tenant_deleted(
&self,
tenant_id: Uuid,
db: &sea_orm::DatabaseConnection,
) -> AppResult<()> {
use chrono::Utc;
use sea_orm::{ActiveModelTrait, ColumnTrait, EntityTrait, QueryFilter, Set};
let now = Utc::now();
// 软删除该租户下所有用户
let users = crate::entity::user::Entity::find()
.filter(crate::entity::user::Column::TenantId.eq(tenant_id))
.filter(crate::entity::user::Column::DeletedAt.is_null())
.all(db)
.await
.map_err(|e| erp_core::error::AppError::Internal(e.to_string()))?;
for user_model in users {
let current_version = user_model.version;
let active: crate::entity::user::ActiveModel = user_model.into();
let mut to_update: crate::entity::user::ActiveModel = active;
to_update.deleted_at = Set(Some(now));
to_update.updated_at = Set(now);
to_update.version = Set(current_version + 1);
let _ = to_update
.update(db)
.await
.map_err(|e| erp_core::error::AppError::Internal(e.to_string()))?;
}
tracing::info!(tenant_id = %tenant_id, "Tenant users soft-deleted");
Ok(())
}
fn permissions(&self) -> Vec<PermissionDescriptor> {
vec![
PermissionDescriptor {
code: "user.list".into(),
name: "查看用户列表".into(),
description: "查看用户列表".into(),
module: "auth".into(),
},
PermissionDescriptor {
code: "user.create".into(),
name: "创建用户".into(),
description: "创建新用户".into(),
module: "auth".into(),
},
PermissionDescriptor {
code: "user.read".into(),
name: "查看用户详情".into(),
description: "查看用户信息".into(),
module: "auth".into(),
},
PermissionDescriptor {
code: "user.update".into(),
name: "编辑用户".into(),
description: "编辑用户信息".into(),
module: "auth".into(),
},
PermissionDescriptor {
code: "user.delete".into(),
name: "删除用户".into(),
description: "软删除用户".into(),
module: "auth".into(),
},
PermissionDescriptor {
code: "user.reset-password".into(),
name: "重置用户密码".into(),
description: "管理员重置指定用户密码".into(),
module: "auth".into(),
},
PermissionDescriptor {
code: "role.list".into(),
name: "查看角色列表".into(),
description: "查看角色列表".into(),
module: "auth".into(),
},
PermissionDescriptor {
code: "role.create".into(),
name: "创建角色".into(),
description: "创建新角色".into(),
module: "auth".into(),
},
PermissionDescriptor {
code: "role.read".into(),
name: "查看角色详情".into(),
description: "查看角色信息".into(),
module: "auth".into(),
},
PermissionDescriptor {
code: "role.update".into(),
name: "编辑角色".into(),
description: "编辑角色".into(),
module: "auth".into(),
},
PermissionDescriptor {
code: "role.delete".into(),
name: "删除角色".into(),
description: "删除角色".into(),
module: "auth".into(),
},
PermissionDescriptor {
code: "permission.list".into(),
name: "查看权限".into(),
description: "查看权限列表".into(),
module: "auth".into(),
},
PermissionDescriptor {
code: "organization.list".into(),
name: "查看组织列表".into(),
description: "查看组织列表".into(),
module: "auth".into(),
},
PermissionDescriptor {
code: "organization.create".into(),
name: "创建组织".into(),
description: "创建组织".into(),
module: "auth".into(),
},
PermissionDescriptor {
code: "organization.update".into(),
name: "编辑组织".into(),
description: "编辑组织".into(),
module: "auth".into(),
},
PermissionDescriptor {
code: "organization.delete".into(),
name: "删除组织".into(),
description: "删除组织".into(),
module: "auth".into(),
},
PermissionDescriptor {
code: "department.list".into(),
name: "查看部门列表".into(),
description: "查看部门列表".into(),
module: "auth".into(),
},
PermissionDescriptor {
code: "department.create".into(),
name: "创建部门".into(),
description: "创建部门".into(),
module: "auth".into(),
},
PermissionDescriptor {
code: "department.update".into(),
name: "编辑部门".into(),
description: "编辑部门".into(),
module: "auth".into(),
},
PermissionDescriptor {
code: "department.delete".into(),
name: "删除部门".into(),
description: "删除部门".into(),
module: "auth".into(),
},
PermissionDescriptor {
code: "position.list".into(),
name: "查看岗位列表".into(),
description: "查看岗位列表".into(),
module: "auth".into(),
},
PermissionDescriptor {
code: "position.create".into(),
name: "创建岗位".into(),
description: "创建岗位".into(),
module: "auth".into(),
},
PermissionDescriptor {
code: "position.update".into(),
name: "编辑岗位".into(),
description: "编辑岗位".into(),
module: "auth".into(),
},
PermissionDescriptor {
code: "position.delete".into(),
name: "删除岗位".into(),
description: "删除岗位".into(),
module: "auth".into(),
},
]
}
fn as_any(&self) -> &dyn std::any::Any {
self
}
}