feat(filelib): 操作日志模块——防篡改哈希链、组合查询与 CSV 导出

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-06 22:54:45 +08:00
parent c96ea60482
commit 26523d1b54
29 changed files with 2881 additions and 209 deletions
+23 -23
View File
@@ -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 };
}