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
+35 -7
View File
@@ -163,16 +163,44 @@ describe("treeService · D17 breadcrumb", () => {
});
});
describe("treeService · 审计落库(C3)", () => {
it("创建/改名/移动/删除均写 AuditEntry", async () => {
describe("treeService · 审计落库(ADR-0039)", () => {
it("创建/改名/移动/删除均写 FileLibAuditLog", async () => {
const root = await createNode(deps(), ADMIN, { parentId: null, kind: "FOLDER", name: "物理" });
await renameNode(deps(), ADMIN, root.id, "物理学");
await softDeleteNode(deps(), ADMIN, root.id);
const actions = (await prisma.auditEntry.findMany({
const rows = await prisma.fileLibAuditLog.findMany({
where: { organizationId: DEFAULT_ORG_ID },
select: { action: true },
orderBy: { createdAt: "asc" },
})).map((e) => e.action);
expect(actions).toEqual(["folder.create", "folder.rename", "folder.delete"]);
orderBy: { seq: "asc" },
});
expect(rows.map((e) => e.action)).toEqual(["folder.create", "folder.rename", "folder.delete"]);
// 改名要留下前后值 —— 这正是旧 metadata blob 做不到的事。
const rename = rows[1]!;
expect(rename.beforeValue).toMatchObject({ name: "物理" });
expect(rename.afterValue).toMatchObject({ name: "物理学" });
expect(rename.result).toBe("SUCCESS");
expect(rename.actorUserId).toBe(ADMIN.userId);
// 姓名快照与对象名称直接落列,查询无需回查 User/Node。
expect(rename.actorName).not.toBe("");
expect(rename.objectName).toBe("物理学");
// 哈希链:seq 连续,prevHash 串上一条。
expect(rows.map((e) => e.seq)).toEqual([1n, 2n, 3n]);
expect(rows[0]!.prevHash).toBeNull();
expect(rows[1]!.prevHash).toBe(rows[0]!.entryHash);
expect(rows[2]!.prevHash).toBe(rows[1]!.entryHash);
});
it("日志只追加:UPDATE 与 DELETE 被数据库触发器拒绝", async () => {
const node = await createNode(deps(), ADMIN, { parentId: null, kind: "FOLDER", name: "不可篡改" });
const row = await prisma.fileLibAuditLog.findFirstOrThrow({
where: { organizationId: DEFAULT_ORG_ID, objectId: node.id },
});
await expect(
prisma.$executeRawUnsafe(`UPDATE "FileLibAuditLog" SET "action" = 'forged' WHERE "id" = $1`, row.id),
).rejects.toThrow(/immutable/);
await expect(
prisma.$executeRawUnsafe(`DELETE FROM "FileLibAuditLog" WHERE "id" = $1`, row.id),
).rejects.toThrow(/append-only/);
});
});