forked from EduCraft/curriculum-project-hub
feat(filelib): 操作日志模块——防篡改哈希链、组合查询与 CSV 导出
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -35,7 +35,7 @@ import { resolveMaxFileBytes } from "../filelib/fileService.js";
|
||||
import { createMemberGroupResolver } from "../filelib/memberGroupResolver.js";
|
||||
import { createHttpGroupResolver } from "../filelib/groupResolverHttp.js";
|
||||
import { createCphPdfAdapter, createManifestStubAdapter } from "../filelib/exportService.js";
|
||||
import { FILE_LIB_AUDIT_ACTIONS } from "../filelib/audit.js";
|
||||
import { registerAuditRoutes } from "../audit/index.js";
|
||||
import { actorOrNull, sendRouteError } from "../filelib/routeShared.js";
|
||||
import type { FileLibRouteDeps } from "../filelib/routeShared.js";
|
||||
|
||||
@@ -168,6 +168,14 @@ export async function registerDatabaseRoutes(
|
||||
await registerFileRoutes(app, filelibDeps);
|
||||
await registerMemberGroupRoutes(app, filelibDeps);
|
||||
await registerBinRoutes(app, filelibDeps);
|
||||
// 审计日志模块(横切):自带 /database/api/audit/*。它只认 prisma + org +
|
||||
// 组解析口 + 一个 actor 门禁函数,不依赖任何 filelib service。
|
||||
await registerAuditRoutes(app, {
|
||||
prisma: config.prisma,
|
||||
organizationId: siloOrg.id,
|
||||
resolveMemberGroupIds: (userId) => filelibDeps.groupResolver.resolveMemberGroupIds(userId),
|
||||
actorOrNull: async (request, reply) => actorOrNull(request, reply, filelibDeps),
|
||||
});
|
||||
await registerTeacherApp(app, {
|
||||
prisma: config.prisma,
|
||||
sessionSecret: config.sessionSecret,
|
||||
@@ -196,6 +204,7 @@ interface DashboardStats {
|
||||
readonly actor: string;
|
||||
readonly label: string;
|
||||
readonly when: Date;
|
||||
readonly result: string;
|
||||
}>;
|
||||
}
|
||||
|
||||
@@ -225,29 +234,20 @@ async function loadDashboardStats(
|
||||
files += (await deps.versionStore.list(project.storageDir)).length;
|
||||
} catch { /* repo 缺失(如重启未恢复)不计 */ }
|
||||
}
|
||||
const entries = await prisma.auditEntry.findMany({
|
||||
where: { organizationId, action: { in: Object.values(FILE_LIB_AUDIT_ACTIONS) } },
|
||||
orderBy: { createdAt: "desc" },
|
||||
// 最近活动读审计日志模块的表(FileLibAuditLog)。它自带操作人姓名快照与
|
||||
// 对象名称,不需要再回查 User —— 这也是审计字段结构化后的直接收益。
|
||||
const entries = await prisma.fileLibAuditLog.findMany({
|
||||
where: { organizationId, archivedAt: null },
|
||||
orderBy: [{ occurredAt: "desc" }, { seq: "desc" }],
|
||||
take: 8,
|
||||
select: { action: true, actorName: true, objectName: true, occurredAt: true, result: true },
|
||||
});
|
||||
const actorIds = [...new Set(entries.map((e) => e.actorUserId).filter((x): x is string => x !== null))];
|
||||
const users = actorIds.length === 0
|
||||
? []
|
||||
: await prisma.user.findMany({ where: { id: { in: actorIds } }, select: { id: true, displayName: true } });
|
||||
const nameById = new Map(users.map((u) => [u.id, u.displayName]));
|
||||
const recent = entries.map((entry) => {
|
||||
const meta = (entry.metadata ?? {}) as Record<string, unknown>;
|
||||
const label =
|
||||
(typeof meta["name"] === "string" ? meta["name"] : undefined) ??
|
||||
(typeof meta["to"] === "string" ? meta["to"] : undefined) ??
|
||||
(typeof meta["path"] === "string" ? meta["path"] : undefined) ??
|
||||
(typeof meta["objectId"] === "string" ? meta["objectId"].slice(0, 8) : "");
|
||||
return {
|
||||
action: entry.action,
|
||||
actor: nameById.get(entry.actorUserId ?? "") ?? entry.actorUserId ?? "unknown",
|
||||
label,
|
||||
when: entry.createdAt,
|
||||
};
|
||||
});
|
||||
const recent = entries.map((entry) => ({
|
||||
action: entry.action,
|
||||
actor: entry.actorName,
|
||||
label: entry.objectName,
|
||||
when: entry.occurredAt,
|
||||
result: entry.result,
|
||||
}));
|
||||
return { folders, projects, files, grants, recent };
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user