feat(core-cms): register audit-logs + wire GDPR audit erasure
The audit-logs collection was never registered (record() would throw), bindAudit/createAuditErasureHook were unused, and DSR cascade-hard never touched the audit trail (audit finding A6). core-cms now registers the collection and wires a req-scoped afterDelete erasure hook on users; bindAllProduction binds the audit log into consent/DSR; cascade-hard pseudonymizes the subject's audit entries; the action select accepts the full AuditAction enum so consent/DSR entries pass validation. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -35,6 +35,18 @@ describe("payloadConfig composition", () => {
|
||||
);
|
||||
});
|
||||
|
||||
it("registers the audit-logs collection (A6)", async () => {
|
||||
const resolved = await config;
|
||||
const slugs = resolved.collections?.map((c) => c.slug) ?? [];
|
||||
expect(slugs).toContain("audit-logs");
|
||||
});
|
||||
|
||||
it("wires the audit erasure afterDelete hook on users (A6)", async () => {
|
||||
const resolved = await config;
|
||||
const users = resolved.collections?.find((c) => c.slug === "users");
|
||||
expect(users?.hooks?.afterDelete?.length ?? 0).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
it("registers all feature globals", async () => {
|
||||
const resolved = await config;
|
||||
const slugs = resolved.globals?.map((g) => g.slug) ?? [];
|
||||
|
||||
@@ -8,7 +8,11 @@ import {
|
||||
withRetentionTombstone,
|
||||
buildRetentionPurgeTask,
|
||||
} from "@repo/core-shared/payload";
|
||||
import { users } from "@repo/auth/cms";
|
||||
import {
|
||||
auditLogsCollection,
|
||||
createReqScopedAuditErasureHook,
|
||||
} from "@repo/core-audit";
|
||||
import { users as usersBase } from "@repo/auth/cms";
|
||||
import { articles } from "@repo/blog/cms";
|
||||
import { media } from "@repo/media/cms";
|
||||
import { pages, siteSettings } from "@repo/marketing-pages/cms";
|
||||
@@ -17,10 +21,28 @@ import { header } from "@repo/navigation/cms";
|
||||
const filename = fileURLToPath(import.meta.url);
|
||||
const dirname = path.dirname(filename);
|
||||
|
||||
// GDPR audit erasure (audit finding A6): when a users row is hard-deleted
|
||||
// (admin expunge, DSR cascade-hard, retention purge), pseudonymize that
|
||||
// subject's audit-log entries so the trail keeps its shape without PII linkage.
|
||||
const users = {
|
||||
...usersBase,
|
||||
hooks: {
|
||||
...usersBase.hooks,
|
||||
afterDelete: [
|
||||
...(usersBase.hooks?.afterDelete ?? []),
|
||||
createReqScopedAuditErasureHook(),
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
// Collections declaring custom.retention.postDeletion get the soft-delete
|
||||
// tombstone field (`deletedAt`) so the DSR soft delete can stamp rows and the
|
||||
// retention purge job can grace-purge them (audit finding A2).
|
||||
const collections = [users, articles, pages, media].map(withRetentionTombstone);
|
||||
const collections = [
|
||||
...[users, articles, pages, media].map(withRetentionTombstone),
|
||||
// Local audit sink (A6) — required for PayloadAuditLog.record() to work.
|
||||
auditLogsCollection,
|
||||
];
|
||||
|
||||
export default buildConfig({
|
||||
editor: lexicalEditor(),
|
||||
|
||||
Reference in New Issue
Block a user