security headers, notifications, GDPR consent, masked fields

This commit is contained in:
2026-08-21 19:03:14 +02:00
parent 81fa409513
commit 05b3dc8cb1
37 changed files with 569 additions and 14 deletions
+17
View File
@@ -0,0 +1,17 @@
/**
* Built-in English fallbacks, used per field when the Notifications global
* leaves a text empty. Same philosophy as consent FALLBACK: the site works out
* of the box, editors override per locale as needed.
*/ export const NOTIFICATION_FALLBACK = {
form: {
success: 'Thank you — your message has been sent.',
error: 'Something went wrong. Please try again later.',
rateLimited: 'Too many attempts. Please wait a moment and try again.',
turnstile: 'Captcha verification failed. Please try again.',
validation: 'Please check the {field} field and try again.',
consent: 'Please accept the privacy policy to continue.',
notFound: 'This form is no longer available.'
}
};
//# sourceMappingURL=defaults.js.map
+1
View File
@@ -0,0 +1 @@
{"version":3,"sources":["../../../src/modules/notifications/defaults.ts"],"sourcesContent":["import type { NotificationTexts } from './types.js'\n\n/**\n * Built-in English fallbacks, used per field when the Notifications global\n * leaves a text empty. Same philosophy as consent FALLBACK: the site works out\n * of the box, editors override per locale as needed.\n */\nexport const NOTIFICATION_FALLBACK: NotificationTexts = {\n form: {\n success: 'Thank you — your message has been sent.',\n error: 'Something went wrong. Please try again later.',\n rateLimited: 'Too many attempts. Please wait a moment and try again.',\n turnstile: 'Captcha verification failed. Please try again.',\n validation: 'Please check the {field} field and try again.',\n consent: 'Please accept the privacy policy to continue.',\n notFound: 'This form is no longer available.',\n },\n}\n"],"names":["NOTIFICATION_FALLBACK","form","success","error","rateLimited","turnstile","validation","consent","notFound"],"mappings":"AAEA;;;;CAIC,GACD,OAAO,MAAMA,wBAA2C;IACtDC,MAAM;QACJC,SAAS;QACTC,OAAO;QACPC,aAAa;QACbC,WAAW;QACXC,YAAY;QACZC,SAAS;QACTC,UAAU;IACZ;AACF,EAAC"}
+27
View File
@@ -0,0 +1,27 @@
import { getGlobal } from '../payload/index.js';
import { NOTIFICATION_FALLBACK } from './defaults.js';
/**
* Resolves notification texts from the Notifications global, falling back to
* English defaults per field. Mirrors getConsentTexts: one read, per-field
* fallback, locale-aware. The frontend maps a submitForm result code to the
* matching text and styles it however it likes (toast, inline, banner).
*/ export async function getNotificationTexts({ locale, payload }) {
const g = await getGlobal(payload, 'notifications', {
locale
});
const f = g.form ?? {};
const fb = NOTIFICATION_FALLBACK.form;
return {
form: {
consent: f.consent || fb.consent,
error: f.error || fb.error,
notFound: f.notFound || fb.notFound,
rateLimited: f.rateLimited || fb.rateLimited,
success: f.success || fb.success,
turnstile: f.turnstile || fb.turnstile,
validation: f.validation || fb.validation
}
};
}
//# sourceMappingURL=getNotificationTexts.js.map
@@ -0,0 +1 @@
{"version":3,"sources":["../../../src/modules/notifications/getNotificationTexts.ts"],"sourcesContent":["import type { BasePayload } from 'payload'\n\nimport type { NotificationsData, NotificationTexts } from './types.js'\n\nimport { getGlobal } from '../payload/index.js'\nimport { NOTIFICATION_FALLBACK } from './defaults.js'\n\ntype GetNotificationTextsArgs = {\n /** Active locale — selects the language variant of each text. */\n locale?: string\n payload: BasePayload\n}\n\n/**\n * Resolves notification texts from the Notifications global, falling back to\n * English defaults per field. Mirrors getConsentTexts: one read, per-field\n * fallback, locale-aware. The frontend maps a submitForm result code to the\n * matching text and styles it however it likes (toast, inline, banner).\n */\nexport async function getNotificationTexts({\n locale,\n payload,\n}: GetNotificationTextsArgs): Promise<NotificationTexts> {\n const g = await getGlobal<NotificationsData>(payload, 'notifications', { locale })\n\n const f = g.form ?? {}\n const fb = NOTIFICATION_FALLBACK.form\n\n return {\n form: {\n consent: f.consent || fb.consent,\n error: f.error || fb.error,\n notFound: f.notFound || fb.notFound,\n rateLimited: f.rateLimited || fb.rateLimited,\n success: f.success || fb.success,\n turnstile: f.turnstile || fb.turnstile,\n validation: f.validation || fb.validation,\n },\n }\n}\n"],"names":["getGlobal","NOTIFICATION_FALLBACK","getNotificationTexts","locale","payload","g","f","form","fb","consent","error","notFound","rateLimited","success","turnstile","validation"],"mappings":"AAIA,SAASA,SAAS,QAAQ,sBAAqB;AAC/C,SAASC,qBAAqB,QAAQ,gBAAe;AAQrD;;;;;CAKC,GACD,OAAO,eAAeC,qBAAqB,EACzCC,MAAM,EACNC,OAAO,EACkB;IACzB,MAAMC,IAAI,MAAML,UAA6BI,SAAS,iBAAiB;QAAED;IAAO;IAEhF,MAAMG,IAAID,EAAEE,IAAI,IAAI,CAAC;IACrB,MAAMC,KAAKP,sBAAsBM,IAAI;IAErC,OAAO;QACLA,MAAM;YACJE,SAASH,EAAEG,OAAO,IAAID,GAAGC,OAAO;YAChCC,OAAOJ,EAAEI,KAAK,IAAIF,GAAGE,KAAK;YAC1BC,UAAUL,EAAEK,QAAQ,IAAIH,GAAGG,QAAQ;YACnCC,aAAaN,EAAEM,WAAW,IAAIJ,GAAGI,WAAW;YAC5CC,SAASP,EAAEO,OAAO,IAAIL,GAAGK,OAAO;YAChCC,WAAWR,EAAEQ,SAAS,IAAIN,GAAGM,SAAS;YACtCC,YAAYT,EAAES,UAAU,IAAIP,GAAGO,UAAU;QAC3C;IACF;AACF"}
+5
View File
@@ -0,0 +1,5 @@
export { NOTIFICATION_FALLBACK } from './defaults.js';
export { getNotificationTexts } from './getNotificationTexts.js';
export { resolveFormMessage } from './resolveFormMessage.js';
//# sourceMappingURL=index.js.map
+1
View File
@@ -0,0 +1 @@
{"version":3,"sources":["../../../src/modules/notifications/index.ts"],"sourcesContent":["export { NOTIFICATION_FALLBACK } from './defaults.js'\nexport { getNotificationTexts } from './getNotificationTexts.js'\nexport { resolveFormMessage } from './resolveFormMessage.js'\nexport type { FormNotificationTexts, NotificationsData, NotificationTexts } from './types.js'\n"],"names":["NOTIFICATION_FALLBACK","getNotificationTexts","resolveFormMessage"],"mappings":"AAAA,SAASA,qBAAqB,QAAQ,gBAAe;AACrD,SAASC,oBAAoB,QAAQ,4BAA2B;AAChE,SAASC,kBAAkB,QAAQ,0BAAyB"}
+35
View File
@@ -0,0 +1,35 @@
/**
* Maps a submitForm result to the user-facing message, interpolating {field}
* for validation errors. This is the bridge the frontend uses: it gets a result
* code from submitForm and the resolved texts from getNotificationTexts, and
* this turns them into one string to display. Keeping the mapping here means the
* frontend never hard-codes messages or knows about result codes.
*
* Never surfaces raw backend/exception detail — 'error' maps to a friendly
* generic message, not the thrown error's text (which could leak internals).
*/ export function resolveFormMessage(result, texts) {
if (result.success) {
return texts.success;
}
switch(result.reason){
case 'consent':
return texts.consent;
case 'not_found':
return texts.notFound;
case 'rate_limited':
return texts.rateLimited;
case 'turnstile':
return texts.turnstile;
case 'validation':
{
// Interpolate {field} with the offending field name when present.
const field = 'field' in result && result.field ? result.field : '';
return texts.validation.replace('{field}', field);
}
case 'error':
default:
return texts.error;
}
}
//# sourceMappingURL=resolveFormMessage.js.map
+1
View File
@@ -0,0 +1 @@
{"version":3,"sources":["../../../src/modules/notifications/resolveFormMessage.ts"],"sourcesContent":["import type { SubmitFormResult } from '../forms/index.js'\nimport type { FormNotificationTexts } from './types.js'\n\n/**\n * Maps a submitForm result to the user-facing message, interpolating {field}\n * for validation errors. This is the bridge the frontend uses: it gets a result\n * code from submitForm and the resolved texts from getNotificationTexts, and\n * this turns them into one string to display. Keeping the mapping here means the\n * frontend never hard-codes messages or knows about result codes.\n *\n * Never surfaces raw backend/exception detail — 'error' maps to a friendly\n * generic message, not the thrown error's text (which could leak internals).\n */\nexport function resolveFormMessage(result: SubmitFormResult, texts: FormNotificationTexts): string {\n if (result.success) {return texts.success}\n\n switch (result.reason) {\n case 'consent':\n return texts.consent\n case 'not_found':\n return texts.notFound\n case 'rate_limited':\n return texts.rateLimited\n case 'turnstile':\n return texts.turnstile\n case 'validation': {\n // Interpolate {field} with the offending field name when present.\n const field = 'field' in result && result.field ? result.field : ''\n return texts.validation.replace('{field}', field)\n }\n case 'error':\n default:\n return texts.error\n }\n}\n"],"names":["resolveFormMessage","result","texts","success","reason","consent","notFound","rateLimited","turnstile","field","validation","replace","error"],"mappings":"AAGA;;;;;;;;;CASC,GACD,OAAO,SAASA,mBAAmBC,MAAwB,EAAEC,KAA4B;IACvF,IAAID,OAAOE,OAAO,EAAE;QAAC,OAAOD,MAAMC,OAAO;IAAA;IAEzC,OAAQF,OAAOG,MAAM;QACnB,KAAK;YACH,OAAOF,MAAMG,OAAO;QACtB,KAAK;YACH,OAAOH,MAAMI,QAAQ;QACvB,KAAK;YACH,OAAOJ,MAAMK,WAAW;QAC1B,KAAK;YACH,OAAOL,MAAMM,SAAS;QACxB,KAAK;YAAc;gBACjB,kEAAkE;gBAClE,MAAMC,QAAQ,WAAWR,UAAUA,OAAOQ,KAAK,GAAGR,OAAOQ,KAAK,GAAG;gBACjE,OAAOP,MAAMQ,UAAU,CAACC,OAAO,CAAC,WAAWF;YAC7C;QACA,KAAK;QACL;YACE,OAAOP,MAAMU,KAAK;IACtB;AACF"}
+6
View File
@@ -0,0 +1,6 @@
/**
* Resolved notification texts, ready for the frontend. Grouped per context;
* `form` maps submitForm result codes to user-facing messages.
*/ /** Raw shape read from the Notifications global (all fields optional). */ export { };
//# sourceMappingURL=types.js.map
+1
View File
@@ -0,0 +1 @@
{"version":3,"sources":["../../../src/modules/notifications/types.ts"],"sourcesContent":["/**\n * Resolved notification texts, ready for the frontend. Grouped per context;\n * `form` maps submitForm result codes to user-facing messages.\n */\nexport type FormNotificationTexts = {\n success: string\n error: string\n rateLimited: string\n turnstile: string\n /** May contain the {field} placeholder — resolve with resolveValidationText. */\n validation: string\n /** Shown when a required GDPR consent checkbox was left unchecked. */\n consent: string\n notFound: string\n}\n\nexport type NotificationTexts = {\n form: FormNotificationTexts\n}\n\n/** Raw shape read from the Notifications global (all fields optional). */\nexport type NotificationsData = {\n form?: Partial<FormNotificationTexts>\n}\n"],"names":[],"mappings":"AAAA;;;CAGC,GAiBD,wEAAwE,GACxE,WAEC"}