forked from EduCraft/curriculum-project-hub
feat(filelib): 操作日志模块——防篡改哈希链、组合查询与 CSV 导出
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -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,
|
||||
});
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user