added validation for forms, sensitive text fields have been masked
This commit is contained in:
@@ -0,0 +1,18 @@
|
||||
import type { NotificationTexts } from './types.js'
|
||||
|
||||
/**
|
||||
* 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: NotificationTexts = {
|
||||
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.',
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
import type { BasePayload } from 'payload'
|
||||
|
||||
import type { NotificationsData, NotificationTexts } from './types.js'
|
||||
|
||||
import { getGlobal } from '../payload/index.js'
|
||||
import { NOTIFICATION_FALLBACK } from './defaults.js'
|
||||
|
||||
type GetNotificationTextsArgs = {
|
||||
/** Active locale — selects the language variant of each text. */
|
||||
locale?: string
|
||||
payload: BasePayload
|
||||
}
|
||||
|
||||
/**
|
||||
* 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,
|
||||
}: GetNotificationTextsArgs): Promise<NotificationTexts> {
|
||||
const g = await getGlobal<NotificationsData>(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,
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
export { NOTIFICATION_FALLBACK } from './defaults.js'
|
||||
export { getNotificationTexts } from './getNotificationTexts.js'
|
||||
export { resolveFormMessage } from './resolveFormMessage.js'
|
||||
export type { FormNotificationTexts, NotificationsData, NotificationTexts } from './types.js'
|
||||
@@ -0,0 +1,35 @@
|
||||
import type { SubmitFormResult } from '../forms/index.js'
|
||||
import type { FormNotificationTexts } from './types.js'
|
||||
|
||||
/**
|
||||
* 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: SubmitFormResult, texts: FormNotificationTexts): string {
|
||||
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
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
/**
|
||||
* Resolved notification texts, ready for the frontend. Grouped per context;
|
||||
* `form` maps submitForm result codes to user-facing messages.
|
||||
*/
|
||||
export type FormNotificationTexts = {
|
||||
success: string
|
||||
error: string
|
||||
rateLimited: string
|
||||
turnstile: string
|
||||
/** May contain the {field} placeholder — resolve with resolveValidationText. */
|
||||
validation: string
|
||||
/** Shown when a required GDPR consent checkbox was left unchecked. */
|
||||
consent: string
|
||||
notFound: string
|
||||
}
|
||||
|
||||
export type NotificationTexts = {
|
||||
form: FormNotificationTexts
|
||||
}
|
||||
|
||||
/** Raw shape read from the Notifications global (all fields optional). */
|
||||
export type NotificationsData = {
|
||||
form?: Partial<FormNotificationTexts>
|
||||
}
|
||||
Reference in New Issue
Block a user