Files
agentic-dev/packages/auth/src/integrations/cms/collections/users.ts
Danijel Martinek 7b0c2ea590 fix(auth): declare users email/username/displayName in DSR pii map
The DSR walkers read the COLLECTION-level custom.pii map, which the
users collection never declared — Art. 15 export returned bare ids and
Art. 17 soft delete redacted nothing; the auth-injected email field in
particular was invisible (audit finding A5). Declares email (auto-added
by Payload auth: true), username and displayName as exportable +
restrictable; walker tests pin a users-shaped collection end to end.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-10 18:25:46 +02:00

104 lines
2.8 KiB
TypeScript

import type { CollectionConfig } from "payload";
export const users: CollectionConfig = {
slug: "users",
auth: true,
admin: {
useAsTitle: "email",
},
custom: {
retention: {
purgeSchedule: "daily",
postDeletion: {
duration: "P30D",
trigger: "after-deletion",
action: "hard-delete",
},
},
subject: { kind: "self", field: "id" },
// Collection-level PII map consumed by the DSR walkers (audit finding
// A5): export includes fields marked exportable; the soft-delete path
// redacts them. `email` is auto-added by Payload's `auth: true` and has
// no explicit field entry below, so it MUST be declared here or Art. 15
// export misses it and Art. 17 soft delete leaves it behind.
pii: {
email: {
category: "contact-email",
purpose: ["account-authentication", "transactional-notifications"],
exportable: true,
restrictable: true,
},
username: {
category: "identification-username",
purpose: ["service-delivery"],
exportable: true,
restrictable: true,
},
displayName: {
category: "identification-username",
purpose: ["service-delivery"],
exportable: true,
restrictable: true,
},
},
},
fields: [
{
// Read/written by the production UsersRepository (getUserByUsername,
// createUser). Pinned by collections/users.test.ts against
// USERS_REPOSITORY_FIELDS so repo <-> collection drift fails fast.
name: "username",
type: "text",
required: true,
unique: true,
index: true,
custom: {
pii: {
category: "identification-username",
purpose: ["service-delivery"],
exportable: true,
restrictable: true,
},
},
},
{
// Credential material — must never leave the server. `access.read`
// returns false unconditionally so the field is stripped from every
// REST/GraphQL/admin API response; the auth repository still reads it
// through the local API with `overrideAccess: true`.
name: "passwordHash",
type: "text",
required: true,
admin: { hidden: true },
access: { read: () => false },
},
{
name: "displayName",
type: "text",
custom: {
pii: {
category: "identification-username",
purpose: ["service-delivery"],
exportable: true,
restrictable: true,
},
},
},
{
name: "role",
type: "select",
options: [
{ label: "Admin", value: "admin" },
{ label: "Editor", value: "editor" },
{ label: "Author", value: "author" },
],
defaultValue: "author",
required: true,
},
{
name: "consentState",
type: "json",
},
],
};