- audit_log handler: 添加 require_permission("audit.log.list") 守卫
- upload handler: 添加 require_permission("file.upload") 守卫
- 种子数据: 新增 audit.log.list / file.upload / diary.comment.delete 权限定义
- 角色种子: admin 获得 audit.log.list + file.upload + diary.comment.delete 权限
- diary.comment.delete 已在 teacher 列表中(种子定义之前缺失)
审计 ID: 5b-C01, 5b-C02, 4a-C02
161 lines
4.5 KiB
Rust
161 lines
4.5 KiB
Rust
use axum::Router;
|
|
use axum::extract::{Extension, FromRef, Query, State};
|
|
use axum::response::Json;
|
|
use axum::routing::get;
|
|
use sea_orm::{ColumnTrait, EntityTrait, PaginatorTrait, QueryFilter, QueryOrder};
|
|
use serde::{Deserialize, Serialize};
|
|
|
|
use erp_core::entity::audit_log;
|
|
use erp_core::error::AppError;
|
|
use erp_core::rbac::require_permission;
|
|
use erp_core::types::{ApiResponse, PaginatedResponse, TenantContext};
|
|
|
|
#[derive(Debug, Deserialize)]
|
|
pub struct AuditLogQuery {
|
|
pub resource_type: Option<String>,
|
|
pub user_id: Option<uuid::Uuid>,
|
|
pub page: Option<u64>,
|
|
pub page_size: Option<u64>,
|
|
}
|
|
|
|
#[derive(Debug, Serialize)]
|
|
pub struct AuditLogResp {
|
|
pub id: uuid::Uuid,
|
|
pub tenant_id: uuid::Uuid,
|
|
pub user_id: Option<uuid::Uuid>,
|
|
pub user_name: Option<String>,
|
|
pub action: String,
|
|
pub resource_type: String,
|
|
pub resource_id: Option<uuid::Uuid>,
|
|
pub old_value: Option<serde_json::Value>,
|
|
pub new_value: Option<serde_json::Value>,
|
|
pub ip_address: Option<String>,
|
|
pub user_agent: Option<String>,
|
|
pub created_at: chrono::DateTime<chrono::Utc>,
|
|
}
|
|
|
|
impl From<audit_log::Model> for AuditLogResp {
|
|
fn from(m: audit_log::Model) -> Self {
|
|
Self {
|
|
id: m.id,
|
|
tenant_id: m.tenant_id,
|
|
user_id: m.user_id,
|
|
user_name: None,
|
|
action: m.action,
|
|
resource_type: m.resource_type,
|
|
resource_id: m.resource_id,
|
|
old_value: m.old_value,
|
|
new_value: m.new_value,
|
|
ip_address: m.ip_address,
|
|
user_agent: m.user_agent,
|
|
created_at: m.created_at,
|
|
}
|
|
}
|
|
}
|
|
|
|
async fn resolve_user_names(
|
|
db: &sea_orm::DatabaseConnection,
|
|
items: &[audit_log::Model],
|
|
) -> std::collections::HashMap<uuid::Uuid, String> {
|
|
use erp_auth::entity::user;
|
|
|
|
let user_ids: Vec<uuid::Uuid> = items
|
|
.iter()
|
|
.filter_map(|i| i.user_id)
|
|
.collect::<std::collections::HashSet<_>>()
|
|
.into_iter()
|
|
.collect();
|
|
|
|
if user_ids.is_empty() {
|
|
return std::collections::HashMap::new();
|
|
}
|
|
|
|
let users = user::Entity::find()
|
|
.filter(user::Column::Id.is_in(user_ids))
|
|
.all(db)
|
|
.await
|
|
.unwrap_or_default();
|
|
|
|
users
|
|
.into_iter()
|
|
.map(|u| {
|
|
let name = u
|
|
.display_name
|
|
.filter(|n| !n.is_empty())
|
|
.unwrap_or(u.username);
|
|
(u.id, name)
|
|
})
|
|
.collect()
|
|
}
|
|
|
|
/// GET /audit-logs
|
|
pub async fn list_audit_logs<S>(
|
|
State(db): State<sea_orm::DatabaseConnection>,
|
|
Extension(ctx): Extension<TenantContext>,
|
|
Query(params): Query<AuditLogQuery>,
|
|
) -> Result<Json<ApiResponse<PaginatedResponse<AuditLogResp>>>, AppError>
|
|
where
|
|
sea_orm::DatabaseConnection: FromRef<S>,
|
|
S: Clone + Send + Sync + 'static,
|
|
{
|
|
// 权限守卫:只有拥有 audit.log.list 权限的用户可查看审计日志
|
|
require_permission(&ctx, "audit.log.list")?;
|
|
|
|
let page = params.page.unwrap_or(1).max(1);
|
|
let page_size = params.page_size.unwrap_or(20).min(100);
|
|
let tenant_id = ctx.tenant_id;
|
|
|
|
let mut q = audit_log::Entity::find().filter(audit_log::Column::TenantId.eq(tenant_id));
|
|
|
|
if let Some(rt) = ¶ms.resource_type {
|
|
q = q.filter(audit_log::Column::ResourceType.eq(rt.clone()));
|
|
}
|
|
if let Some(uid) = ¶ms.user_id {
|
|
q = q.filter(audit_log::Column::UserId.eq(*uid));
|
|
}
|
|
|
|
let paginator = q
|
|
.order_by_desc(audit_log::Column::CreatedAt)
|
|
.paginate(&db, page_size);
|
|
|
|
let total = paginator
|
|
.num_items()
|
|
.await
|
|
.map_err(|e| AppError::Internal(format!("查询审计日志失败: {e}")))?;
|
|
|
|
let items = paginator
|
|
.fetch_page(page - 1)
|
|
.await
|
|
.map_err(|e| AppError::Internal(format!("查询审计日志失败: {e}")))?;
|
|
|
|
let user_map = resolve_user_names(&db, &items).await;
|
|
|
|
let resp_items: Vec<AuditLogResp> = items
|
|
.into_iter()
|
|
.map(|m| {
|
|
let user_name = m.user_id.and_then(|uid| user_map.get(&uid).cloned());
|
|
let mut resp = AuditLogResp::from(m);
|
|
resp.user_name = user_name;
|
|
resp
|
|
})
|
|
.collect();
|
|
|
|
let total_pages = total.div_ceil(page_size);
|
|
|
|
Ok(Json(ApiResponse::ok(PaginatedResponse {
|
|
data: resp_items,
|
|
total,
|
|
page,
|
|
page_size,
|
|
total_pages,
|
|
})))
|
|
}
|
|
|
|
pub fn audit_log_router<S>() -> Router<S>
|
|
where
|
|
sea_orm::DatabaseConnection: FromRef<S>,
|
|
S: Clone + Send + Sync + 'static,
|
|
{
|
|
Router::new().route("/audit-logs", get(list_audit_logs))
|
|
}
|