106 lines
3.0 KiB
TypeScript
106 lines
3.0 KiB
TypeScript
import { getPayload as _getPayload } from "payload";
|
|
import type { SanitizedConfig } from "payload";
|
|
import type { AuditLogProtocol } from "@repo/core-shared/di";
|
|
import type { IDataRectify } from "./data-rectify.interface";
|
|
import type { DsrCollectionCustom } from "./dsr-collection-custom";
|
|
|
|
type PayloadDoc = Record<string, unknown>;
|
|
|
|
type PayloadAPI = {
|
|
find(args: {
|
|
collection: string;
|
|
where: Record<string, unknown>;
|
|
overrideAccess: true;
|
|
limit: number;
|
|
}): Promise<{ docs: PayloadDoc[] }>;
|
|
update(args: {
|
|
collection: string;
|
|
id: string;
|
|
data: Record<string, unknown>;
|
|
overrideAccess: true;
|
|
}): Promise<PayloadDoc>;
|
|
};
|
|
|
|
type GetPayload = (args: { config: SanitizedConfig }) => Promise<PayloadAPI>;
|
|
|
|
/**
|
|
* Payload-backed IDataRectify. Verifies the target field is PII-tagged before
|
|
* updating and emits a RESTRICT audit entry with `reason: "art-16-request"` as
|
|
* the tamper-evident record of the Art. 16 correction.
|
|
*/
|
|
export class PayloadDataRectify implements IDataRectify {
|
|
constructor(
|
|
private readonly config: SanitizedConfig,
|
|
private readonly auditLog: AuditLogProtocol,
|
|
private readonly getPayloadFn: GetPayload = _getPayload as unknown as GetPayload,
|
|
) {}
|
|
|
|
async updateSubjectField(
|
|
subjectId: string,
|
|
collectionSlug: string,
|
|
field: string,
|
|
value: unknown,
|
|
): Promise<void> {
|
|
const collectionCfg = this.config.collections.find(
|
|
(c) => c.slug === collectionSlug,
|
|
);
|
|
if (!collectionCfg) {
|
|
throw new Error(`Collection "${collectionSlug}" not found in config`);
|
|
}
|
|
|
|
const custom = (collectionCfg.custom ?? {}) as DsrCollectionCustom;
|
|
if (!custom.subject) {
|
|
throw new Error(
|
|
`Collection "${collectionSlug}" has no DSR subject linkage`,
|
|
);
|
|
}
|
|
if (!custom.pii?.[field]) {
|
|
throw new Error(
|
|
`Field "${field}" in "${collectionSlug}" is not tagged as PII`,
|
|
);
|
|
}
|
|
|
|
const { field: subjectField } = custom.subject;
|
|
const where =
|
|
subjectField === "id"
|
|
? { id: { equals: subjectId } }
|
|
: { [subjectField]: { equals: subjectId } };
|
|
|
|
const payload = await this.getPayloadFn({ config: this.config });
|
|
const result = await payload.find({
|
|
collection: collectionSlug,
|
|
where,
|
|
overrideAccess: true,
|
|
limit: 1000,
|
|
});
|
|
|
|
for (const doc of result.docs) {
|
|
await payload.update({
|
|
collection: collectionSlug,
|
|
id: String(doc["id"]),
|
|
data: { [field]: value },
|
|
overrideAccess: true,
|
|
});
|
|
}
|
|
|
|
await this.auditLog.record({
|
|
actorId: subjectId,
|
|
actorType: "user",
|
|
actorRoles: [],
|
|
action: "RESTRICT",
|
|
resource: { type: collectionSlug },
|
|
changedFields: [field],
|
|
at: new Date(),
|
|
scope: {
|
|
feature: "core-dsr",
|
|
environment: process.env["NODE_ENV"] ?? "development",
|
|
tenant: "default",
|
|
},
|
|
reason: "art-16-request",
|
|
from: { ipTruncated: "system", userAgent: "system" },
|
|
containsPii: false,
|
|
outcome: "success",
|
|
});
|
|
}
|
|
}
|