added validation for forms, sensitive text fields have been masked

This commit is contained in:
2026-08-21 17:15:02 +02:00
parent 5aa66ff37a
commit 2215766940
19 changed files with 325 additions and 4 deletions
+75
View File
@@ -0,0 +1,75 @@
import type { Field } from 'payload'
/**
* Fields for the Notifications global — localized user-facing texts for action
* results (form submission outcomes, and future contexts). Every text is
* localized: true so each language has its own value. Empty fields fall back to
* built-in English defaults (see modules/notifications/defaults).
*
* Grouped per context. `form` holds the outcomes of submitForm; more groups
* (e.g. `newsletter`, `system`) can be added the same way without touching
* consumers — getNotificationTexts resolves whatever exists, falling back
* per field.
*/
export const notificationsFields: Field[] = [
{
name: 'form',
type: 'group',
admin: {
description:
'Messages shown after a form is submitted. Leave a field empty to use the built-in default.',
},
fields: [
{
name: 'success',
type: 'text',
admin: { placeholder: 'Thank you — your message has been sent.' },
localized: true,
},
{
name: 'error',
type: 'text',
admin: { placeholder: 'Something went wrong. Please try again later.' },
localized: true,
},
{
name: 'rateLimited',
type: 'text',
admin: { placeholder: 'Too many attempts. Please wait a moment and try again.' },
localized: true,
},
{
name: 'turnstile',
type: 'text',
admin: { placeholder: 'Captcha verification failed. Please try again.' },
localized: true,
},
{
name: 'validation',
type: 'text',
admin: {
description:
'Shown on a validation error. Use {field} to insert the offending field name.',
placeholder: 'Please check the {field} field and try again.',
},
localized: true,
},
{
name: 'consent',
type: 'text',
admin: {
description: 'Shown when the GDPR consent checkbox is left unchecked.',
placeholder: 'Please accept the privacy policy to continue.',
},
localized: true,
},
{
name: 'notFound',
type: 'text',
admin: { placeholder: 'This form is no longer available.' },
localized: true,
},
],
label: 'Form messages',
},
]
+18
View File
@@ -0,0 +1,18 @@
import type { GlobalConfig } from 'payload'
import { notificationsFields } from './fields.js'
/**
* Builds the Notifications global — localized action-result texts. Readable by
* any authenticated panel user; server-side helpers read it with overrideAccess
* so the frontend can resolve texts without a session.
*/
export function buildNotifications(): GlobalConfig {
return {
slug: 'notifications',
label: 'Notifications',
access: {
read: () => true, // texts are public-facing (shown to end users)
},
fields: notificationsFields,
}
}
@@ -0,0 +1,30 @@
'use client'
import type { TextFieldClientComponent } from 'payload'
import { useField } from '@payloadcms/ui'
import { useState } from 'react'
export const MaskedField: TextFieldClientComponent = ({ field, path }) => {
const { setValue, value } = useField<string>({ path })
const [revealed, setRevealed] = useState(false)
const label = typeof field?.label === 'string' ? field.label : (field?.name ?? path)
return (
<div className="field-type text">
<label className="field-label">{label}</label>
<div style={{ display: 'flex', gap: '.5rem' }}>
<input
autoComplete="off"
onChange={(e) => setValue(e.target.value)}
style={{ flex: 1 }}
type={revealed ? 'text' : 'password'}
value={value ?? ''}
/>
<button onClick={() => setRevealed((r) => !r)} type="button">
{revealed ? 'Hide' : 'Reveal'}
</button>
</div>
</div>
)
}
export default MaskedField
@@ -36,6 +36,10 @@ export const smtpFields: Field[] = [
type: 'text',
admin: {
description: 'SMTP account password.',
// Masked in the UI (••••) — stored plaintext, readable for SMTP auth.
components: {
Field: '@intecion/ipal-kit/client#MaskedField',
},
},
},
{
@@ -35,6 +35,10 @@ export const storageFields: Field[] = [
type: 'text',
admin: {
description: 'R2 secret access key.',
// Masked in the UI (••••) — stored plaintext, readable for R2 auth.
components: {
Field: '@intecion/ipal-kit/client#MaskedField',
},
},
},
]
@@ -21,6 +21,10 @@ export const turnstileFields: Field[] = [
type: 'text',
admin: {
description: 'Secret key used for server-side verification.',
// Masked in the UI (••••) — stored plaintext, readable for verification.
components: {
Field: '@intecion/ipal-kit/client#MaskedField',
},
},
},
]