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>
71 lines
2.4 KiB
TypeScript
71 lines
2.4 KiB
TypeScript
import { buildConfig } from "payload";
|
|
import { postgresAdapter } from "@payloadcms/db-postgres";
|
|
import { lexicalEditor } from "@payloadcms/richtext-lexical";
|
|
import path from "node:path";
|
|
import { fileURLToPath } from "node:url";
|
|
|
|
import {
|
|
withRetentionTombstone,
|
|
buildRetentionPurgeTask,
|
|
} from "@repo/core-shared/payload";
|
|
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";
|
|
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),
|
|
// Local audit sink (A6) — required for PayloadAuditLog.record() to work.
|
|
auditLogsCollection,
|
|
];
|
|
|
|
export default buildConfig({
|
|
editor: lexicalEditor(),
|
|
collections,
|
|
globals: [siteSettings, header],
|
|
secret: process.env.PAYLOAD_SECRET || "default-secret-change-me",
|
|
db: postgresAdapter({
|
|
pool: {
|
|
connectionString:
|
|
process.env.DATABASE_URL ||
|
|
"postgresql://postgres:postgres@localhost:5433/template",
|
|
},
|
|
}),
|
|
jobs: {
|
|
// Task definitions for the retention purge (audit finding A3):
|
|
// registerRetentionPurgeJobs (called from bindAllProduction) enqueues
|
|
// `retention-purge--<slug>` jobs; these definitions let Payload run them.
|
|
tasks: collections
|
|
.filter((c) => Boolean(c.custom?.retention?.purgeSchedule))
|
|
.map((c) => buildRetentionPurgeTask(c.slug)) as never,
|
|
},
|
|
typescript: {
|
|
outputFile: path.resolve(dirname, "generated-types.ts"),
|
|
},
|
|
});
|