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
+43 -28
View File
@@ -129,7 +129,7 @@ export async function listGrants(
where: { organizationId: deps.organizationId, nodeId, revokedAt: null },
orderBy: [{ isCreatorGrant: "desc" }, { createdAt: "asc" }],
});
return withPrincipalNames(deps.prisma, grants.map(toDto));
return withPrincipalNames(deps.prisma, grants.map((g) => toDto(g)));
}
export interface PutGrantsResult {
@@ -179,7 +179,10 @@ export async function putGrants(
if (existing.role !== item.role) {
await tx.fileLibGrant.update({ where: { id: existing.id }, data: { role: item.role } });
updated += 1;
await audit(tx, deps, actor, FILE_LIB_AUDIT_ACTIONS.permissionUpdate, node.id, node.pathIds, { ...item });
await audit(tx, deps, actor, FILE_LIB_AUDIT_ACTIONS.permissionUpdate, node, {
before: { principalType: item.principalType, principalId: item.principalId, role: existing.role },
after: { principalType: item.principalType, principalId: item.principalId, role: item.role },
});
}
} else {
await tx.fileLibGrant.create({
@@ -193,14 +196,16 @@ export async function putGrants(
},
});
granted += 1;
await audit(tx, deps, actor, FILE_LIB_AUDIT_ACTIONS.permissionGrant, node.id, node.pathIds, { ...item });
await audit(tx, deps, actor, FILE_LIB_AUDIT_ACTIONS.permissionGrant, node, {
after: { principalType: item.principalType, principalId: item.principalId, role: item.role },
});
}
}
const grants = await tx.fileLibGrant.findMany({
where: { organizationId: deps.organizationId, nodeId: node.id, revokedAt: null },
orderBy: [{ isCreatorGrant: "desc" }, { createdAt: "asc" }],
});
return { granted, updated, grants: await withPrincipalNames(tx, grants.map(toDto)) };
return { granted, updated, grants: await withPrincipalNames(tx, grants.map((g) => toDto(g))) };
});
}
@@ -224,10 +229,14 @@ export async function revokeGrant(
throw new FileLibError(403, "only_creator_can_revoke_manage", "only the creator can revoke MANAGE");
}
await tx.fileLibGrant.update({ where: { id: grant.id }, data: { revokedAt: new Date() } });
await audit(tx, deps, actor, FILE_LIB_AUDIT_ACTIONS.permissionRevoke, node.id, node.pathIds, {
principalType: grant.principalType,
principalId: grant.principalId,
role: grant.role,
await audit(tx, deps, actor, FILE_LIB_AUDIT_ACTIONS.permissionRevoke, node, {
before: {
principalType: grant.principalType,
principalId: grant.principalId,
role: grant.role,
},
// 收回:无后值(授权不复存在)。
context: { grantId: grant.id },
});
});
}
@@ -267,12 +276,10 @@ export async function forceAdjustGrants(
if (existing.role !== item.role) {
await tx.fileLibGrant.update({ where: { id: existing.id }, data: { role: item.role } });
updated += 1;
await audit(tx, deps, actor, FILE_LIB_AUDIT_ACTIONS.adminForceAdjust, node.id, node.pathIds, {
change: "update",
principalType: item.principalType,
principalId: item.principalId,
from: existing.role,
to: item.role,
await audit(tx, deps, actor, FILE_LIB_AUDIT_ACTIONS.adminForceAdjust, node, {
before: { principalType: item.principalType, principalId: item.principalId, role: existing.role },
after: { principalType: item.principalType, principalId: item.principalId, role: item.role },
context: { change: "update", forced: true },
});
}
} else {
@@ -287,11 +294,9 @@ export async function forceAdjustGrants(
},
});
granted += 1;
await audit(tx, deps, actor, FILE_LIB_AUDIT_ACTIONS.adminForceAdjust, node.id, node.pathIds, {
change: "grant",
principalType: item.principalType,
principalId: item.principalId,
role: item.role,
await audit(tx, deps, actor, FILE_LIB_AUDIT_ACTIONS.adminForceAdjust, node, {
after: { principalType: item.principalType, principalId: item.principalId, role: item.role },
context: { change: "grant", forced: true },
});
}
}
@@ -299,7 +304,7 @@ export async function forceAdjustGrants(
where: { organizationId: deps.organizationId, nodeId: node.id, revokedAt: null },
orderBy: [{ isCreatorGrant: "desc" }, { createdAt: "asc" }],
});
return { granted, updated, grants: await withPrincipalNames(tx, grants.map(toDto)) };
return { granted, updated, grants: await withPrincipalNames(tx, grants.map((g) => toDto(g))) };
});
}
@@ -322,22 +327,32 @@ function validateGrantItems(items: readonly InitialGrant[]): void {
}
}
/**
* 授权审计:objectType 恒为 GRANT,objectId/objectPath 用被授权的节点 ——
* 「谁在哪个节点上动了谁的权限」是查询时的主索引。
*/
async function audit(
tx: Prisma.TransactionClient,
deps: Deps,
actor: FileLibActor,
action: string,
nodeId: string,
pathIds: string,
detail: Record<string, unknown>,
node: { readonly id: string; readonly name: string; readonly pathIds: string },
values: {
readonly before?: unknown;
readonly after?: unknown;
readonly context?: Record<string, unknown> | undefined;
},
): Promise<void> {
await writeFileLibAudit(tx, {
action,
actorUserId: actor.userId,
actor,
organizationId: deps.organizationId,
objectType: "grant",
objectId: nodeId,
objectPath: pathIds,
detail,
objectType: "GRANT",
objectId: node.id,
objectName: node.name,
objectPath: node.pathIds,
before: values.before,
after: values.after,
context: values.context,
});
}