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
+39 -19
View File
@@ -29,17 +29,22 @@ import {
import { checkAccess, effectiveRole } from "./permission.js";
import type { GroupResolver } from "./groupResolver.js";
import type { VersionStore } from "./versionStore.js";
import { FILE_LIB_AUDIT_ACTIONS, writeFileLibAudit } from "./audit.js";
import { FILE_LIB_AUDIT_ACTIONS, nodeObjectType, writeFileLibAudit } from "./audit.js";
export interface FileLibActor {
readonly userId: string;
/** silo org OWNER/ADMIN(契约 C4 适配)。仅 root 创建/force_adjust 用,不给读旁路。 */
readonly isWebsiteAdmin: boolean;
/**
* 展示名(飞书昵称)。用于生成 commit message 的【用户名】部分;
* 权限判定一律用 userId。缺失时回退到 userId。
* 展示名(飞书昵称)。用于生成 commit message 的【用户名】部分,
* 以及审计的操作人姓名快照;权限判定一律用 userId。缺失时回退到 userId。
*/
readonly displayName?: string | undefined;
/**
* 客户端信息(IP / User-Agent),由 guard 从请求头采集。
* 只进审计,不参与任何判定;采不到即 undefined。
*/
readonly client?: { readonly ip?: string | undefined; readonly userAgent?: string | undefined } | undefined;
}
export interface TreeServiceDeps {
@@ -279,22 +284,27 @@ export async function createNode(
await writeFileLibAudit(tx, {
action: nodeAction(input.kind, "Create"),
actorUserId: actor.userId,
actor,
organizationId: deps.organizationId,
objectType: input.kind === "PROJECT" ? "project" : "folder",
objectType: nodeObjectType(input.kind),
objectId: id,
objectName: name,
objectPath: pathIds,
detail: { name, parentId: input.parentId, initialGrants: initialGrants.length },
// 创建:无前值。后值是落库的节点事实。
after: { name, kind: input.kind, parentId: input.parentId, description: input.description ?? null },
context: { initialGrants: initialGrants.length, isRootCreation: input.parentId === null },
});
for (const grant of initialGrants) {
await writeFileLibAudit(tx, {
action: FILE_LIB_AUDIT_ACTIONS.permissionGrant,
actorUserId: actor.userId,
actor,
organizationId: deps.organizationId,
objectType: "grant",
objectType: "GRANT",
objectId: id,
objectName: name,
objectPath: pathIds,
detail: { principalType: grant.principalType, principalId: grant.principalId, role: grant.role },
after: { principalType: grant.principalType, principalId: grant.principalId, role: grant.role },
context: { reason: "initial_grant_on_create" },
});
}
return node;
@@ -340,12 +350,14 @@ export async function renameNode(
}
await writeFileLibAudit(tx, {
action: nodeAction(node.kind, "Rename"),
actorUserId: actor.userId,
actor,
organizationId: deps.organizationId,
objectType: node.kind === "PROJECT" ? "project" : "folder",
objectType: nodeObjectType(node.kind),
objectId: node.id,
objectName: name,
objectPath: node.pathIds,
detail: { from: node.name, to: name },
before: { name: node.name },
after: { name },
});
return updated;
});
@@ -403,12 +415,16 @@ export async function moveNode(
}
await writeFileLibAudit(tx, {
action: nodeAction(node.kind, "Move"),
actorUserId: actor.userId,
actor,
organizationId: deps.organizationId,
objectType: node.kind === "PROJECT" ? "project" : "folder",
objectType: nodeObjectType(node.kind),
objectId: node.id,
objectName: node.name,
// 移动后的新路径 —— 按子树查询要能在新位置命中。
objectPath: newPathIds,
detail: { fromParentId: node.parentId, toParentId: newParentId },
before: { parentId: node.parentId, pathIds: node.pathIds },
after: { parentId: newParentId, pathIds: newPathIds },
context: { movedToRoot: newParentId === null },
});
return updated;
});
@@ -422,15 +438,19 @@ export async function softDeleteNode(
): Promise<void> {
await deps.prisma.$transaction(async (tx) => {
const { node } = await requireAccess(tx, deps, actor, nodeId, "MANAGE");
await tx.fileLibNode.update({ where: { id: node.id }, data: { deletedAt: new Date() } });
const deletedAt = new Date();
await tx.fileLibNode.update({ where: { id: node.id }, data: { deletedAt } });
await writeFileLibAudit(tx, {
action: nodeAction(node.kind, "Delete"),
actorUserId: actor.userId,
actor,
organizationId: deps.organizationId,
objectType: node.kind === "PROJECT" ? "project" : "folder",
objectType: nodeObjectType(node.kind),
objectId: node.id,
objectName: node.name,
objectPath: node.pathIds,
detail: { name: node.name },
before: { name: node.name, deletedAt: null },
// 软删(D15):后值是打标本身,不是消失 —— 回收站仍可恢复。
after: { deletedAt: deletedAt.toISOString() },
});
});
}