功能修复: 1. 患者创建空名称验证:后端添加 name.trim().is_empty() 检查 2. 仪表盘统计容错:单个查询失败返回零值而非 500 3. FHIR 路由修复:从 /fhir 移到 /api/v1/fhir 保持一致 4. 冻结模块后端中间件:新增 frozen_module_middleware 拦截冻结路径 5. 积分端点权限码:health.health-data.list → health.points.list 6. 角色权限迁移:护士补充 devices.list,运营补充 points.list/manage 7. 测试结果文档:R01-R05 角色测试 + T00/T10 结果归档 Clippy 全 workspace 清零(14→0 errors): - erp-core: 修复 empty doc line、collapsible if、redundant closure 等 9 处 - erp-health: 修复 too_many_arguments、unused var、unnecessary parens 等 58 处 - erp-ai: 修复 dead_code、unused import 等 11 处 - erp-plugin: 修复 too_many_arguments、wildcard pattern 等 11 处 - erp-server-migration: 修复 enum_variant_names 5 处 - erp-auth/config/workflow/message: 各 1-3 处 工程改进: - lint-staged 配置迁移到 .lintstagedrc.js(函数式避免文件列表传给 clippy) - cargo fmt 统一格式化
355 lines
13 KiB
Rust
355 lines
13 KiB
Rust
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/refresh", axum::routing::post(auth_handler::refresh))
|
||
.route(
|
||
"/auth/wechat/login",
|
||
axum::routing::post(wechat_handler::wechat_login),
|
||
)
|
||
.route(
|
||
"/auth/wechat/bind-phone",
|
||
axum::routing::post(wechat_handler::wechat_bind_phone),
|
||
)
|
||
}
|
||
|
||
/// 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(
|
||
"/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: "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
|
||
}
|
||
}
|