功能修复: 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 统一格式化
77 lines
3.0 KiB
Rust
77 lines
3.0 KiB
Rust
use axum::Extension;
|
||
use axum::Json;
|
||
use axum::extract::{FromRef, Path, State};
|
||
use sea_orm::{ConnectionTrait, DatabaseBackend, Statement};
|
||
use serde_json::{Value, json};
|
||
use uuid::Uuid;
|
||
|
||
use erp_core::error::AppError;
|
||
use erp_core::rbac::require_permission;
|
||
use erp_core::types::{ApiResponse, TenantContext};
|
||
|
||
use crate::state::AppState;
|
||
|
||
/// POST /api/v1/admin/tenants/:id/rotate-key
|
||
/// 密钥轮换 — 生成新 DEK,持久化到 tenant_crypto_keys,使缓存失效
|
||
pub async fn rotate_tenant_key<S>(
|
||
State(state): State<AppState>,
|
||
Extension(ctx): Extension<TenantContext>,
|
||
Path(tenant_id): Path<Uuid>,
|
||
) -> Result<Json<ApiResponse<Value>>, AppError>
|
||
where
|
||
AppState: FromRef<S>,
|
||
S: Clone + Send + Sync + 'static,
|
||
{
|
||
require_permission(&ctx, "tenant.manage")?;
|
||
|
||
// 读取当前最大版本号
|
||
let max_version: Option<i32> = {
|
||
let row = state.db.query_one(Statement::from_sql_and_values(
|
||
DatabaseBackend::Postgres,
|
||
"SELECT COALESCE(MAX(key_version), 0) as v FROM tenant_crypto_keys WHERE tenant_id = $1 AND deleted_at IS NULL",
|
||
[tenant_id.into()],
|
||
)).await.map_err(|e| AppError::Internal(format!("查询密钥版本失败: {}", e)))?;
|
||
row.and_then(|r| r.try_get_by_index::<i32>(0).ok())
|
||
};
|
||
let current_version = max_version.unwrap_or(0);
|
||
let new_version = current_version + 1;
|
||
|
||
// 将旧版本标记为不活跃
|
||
state.db.execute(Statement::from_sql_and_values(
|
||
DatabaseBackend::Postgres,
|
||
"UPDATE tenant_crypto_keys SET is_active = false, updated_at = now() WHERE tenant_id = $1 AND is_active = true AND deleted_at IS NULL",
|
||
[tenant_id.into()],
|
||
)).await.map_err(|e| AppError::Internal(format!("停用旧 DEK 失败: {}", e)))?;
|
||
|
||
// 生成新 DEK 并用 KEK 加密
|
||
let kek = state.pii_crypto.kek();
|
||
let (_new_dek, encrypted_dek) = erp_core::crypto::DekManager::generate_new_dek(kek)
|
||
.map_err(|e| AppError::Internal(format!("生成新 DEK 失败: {}", e)))?;
|
||
|
||
// 持久化新 DEK
|
||
let new_id = Uuid::now_v7();
|
||
state.db.execute(Statement::from_sql_and_values(
|
||
DatabaseBackend::Postgres,
|
||
"INSERT INTO tenant_crypto_keys (id, tenant_id, encrypted_dek, key_version, is_active, created_at, updated_at, version) VALUES ($1, $2, $3, $4, true, now(), now(), 1)",
|
||
[new_id.into(), tenant_id.into(), encrypted_dek.into(), new_version.into()],
|
||
)).await.map_err(|e| AppError::Internal(format!("存储新 DEK 失败: {}", e)))?;
|
||
|
||
// 使 DEK 缓存失效
|
||
state.pii_crypto.invalidate_dek(tenant_id);
|
||
|
||
tracing::info!(
|
||
tenant_id = %tenant_id,
|
||
old_version = current_version,
|
||
new_version = new_version,
|
||
"密钥轮换完成(新 DEK 已持久化,缓存已清除)"
|
||
);
|
||
|
||
Ok(Json(ApiResponse::ok(json!({
|
||
"message": "密钥轮换已完成",
|
||
"tenant_id": tenant_id,
|
||
"old_version": current_version,
|
||
"new_version": new_version,
|
||
"note": "后台重加密任务需要单独触发,旧数据仍可用旧 DEK 解密"
|
||
}))))
|
||
}
|