- Base platform from base.git (ERP base: auth, core, config, message, workflow, plugin) - Created erp-diary module skeleton (lib.rs, dto.rs, error.rs, event.rs, state.rs) - Integrated erp-diary into workspace and erp-server - Added DiaryModule registration in main.rs - Added DiaryState FromRef in state.rs - Diary routes mounted (empty routes, ready for implementation) - Product design spec v1.2 preserved in docs/ - Implementation plan preserved in plans/ Cargo check: OK Cargo test: OK (78+ base tests passing)
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 解密"
|
||
}))))
|
||
}
|