fix: validates password and confirm password on the server (#7410)
Fixes https://github.com/payloadcms/payload/issues/7380 Adjusts how the password/confirm-password fields are validated. Moves validation to the server, adds them to a custom schema under the schema path `${collectionSlug}.auth` for auth enabled collections.
This commit is contained in:
@@ -13,10 +13,11 @@ import './index.scss'
|
||||
|
||||
export type ConfirmPasswordFieldProps = {
|
||||
disabled?: boolean
|
||||
path?: string
|
||||
}
|
||||
|
||||
export const ConfirmPasswordField: React.FC<ConfirmPasswordFieldProps> = (props) => {
|
||||
const { disabled } = props
|
||||
const { disabled, path = 'confirm-password' } = props
|
||||
|
||||
const password = useFormFields<FormField>(([fields]) => fields?.password)
|
||||
const { t } = useTranslation()
|
||||
@@ -36,10 +37,7 @@ export const ConfirmPasswordField: React.FC<ConfirmPasswordFieldProps> = (props)
|
||||
[password, t],
|
||||
)
|
||||
|
||||
const path = 'confirm-password'
|
||||
|
||||
const { setValue, showError, value } = useField({
|
||||
disableFormData: true,
|
||||
path,
|
||||
validate,
|
||||
})
|
||||
@@ -58,6 +56,7 @@ export const ConfirmPasswordField: React.FC<ConfirmPasswordFieldProps> = (props)
|
||||
<div className={`${fieldBaseClass}__wrap`}>
|
||||
<FieldError path={path} />
|
||||
<input
|
||||
aria-label={t('authentication:confirmPassword')}
|
||||
autoComplete="off"
|
||||
disabled={!!disabled}
|
||||
id="field-confirm-password"
|
||||
|
||||
@@ -1,53 +1,90 @@
|
||||
'use client'
|
||||
import type { ClientValidate, Description, FormFieldBase , Validate } from 'payload'
|
||||
import type { ClientValidate, Description, PayloadRequest, Validate } from 'payload'
|
||||
|
||||
import { useConfig, useLocale, useTranslation } from '@payloadcms/ui'
|
||||
import { password } from 'payload/shared'
|
||||
import React, { useCallback } from 'react'
|
||||
|
||||
import { useField } from '../../forms/useField/index.js'
|
||||
import { withCondition } from '../../forms/withCondition/index.js'
|
||||
import { FieldError } from '../FieldError/index.js'
|
||||
import { FieldLabel } from '../FieldLabel/index.js'
|
||||
import { fieldBaseClass } from '../shared/index.js'
|
||||
import { isFieldRTL } from '../shared/index.js'
|
||||
import './index.scss'
|
||||
import { PasswordInput } from './input.js'
|
||||
|
||||
export type PasswordFieldProps = {
|
||||
AfterInput?: React.ReactElement
|
||||
BeforeInput?: React.ReactElement
|
||||
CustomDescription?: React.ReactElement
|
||||
CustomError?: React.ReactElement
|
||||
CustomLabel?: React.ReactElement
|
||||
autoComplete?: string
|
||||
className?: string
|
||||
description?: Description
|
||||
disabled?: boolean
|
||||
errorProps?: any // unknown type
|
||||
inputRef?: React.RefObject<HTMLInputElement>
|
||||
label?: string
|
||||
labelProps?: any // unknown type
|
||||
name: string
|
||||
path?: string
|
||||
placeholder?: string
|
||||
required?: boolean
|
||||
rtl?: boolean
|
||||
style?: React.CSSProperties
|
||||
validate?: Validate
|
||||
width?: string
|
||||
} & FormFieldBase
|
||||
}
|
||||
|
||||
const PasswordFieldComponent: React.FC<PasswordFieldProps> = (props) => {
|
||||
const {
|
||||
name,
|
||||
AfterInput,
|
||||
BeforeInput,
|
||||
CustomDescription,
|
||||
CustomError,
|
||||
CustomLabel,
|
||||
autoComplete,
|
||||
className,
|
||||
disabled: disabledFromProps,
|
||||
errorProps,
|
||||
inputRef,
|
||||
label,
|
||||
labelProps,
|
||||
path: pathFromProps,
|
||||
placeholder,
|
||||
required,
|
||||
rtl,
|
||||
style,
|
||||
validate,
|
||||
width,
|
||||
} = props
|
||||
|
||||
const { t } = useTranslation()
|
||||
const locale = useLocale()
|
||||
const config = useConfig()
|
||||
|
||||
const memoizedValidate: ClientValidate = useCallback(
|
||||
(value, options) => {
|
||||
if (typeof validate === 'function') {
|
||||
return validate(value, { ...options, required })
|
||||
}
|
||||
|
||||
return password(value, {
|
||||
name: 'password',
|
||||
type: 'text',
|
||||
data: {},
|
||||
preferences: { fields: {} },
|
||||
req: {
|
||||
payload: {
|
||||
config,
|
||||
},
|
||||
t,
|
||||
} as PayloadRequest,
|
||||
required: true,
|
||||
siblingData: {},
|
||||
})
|
||||
},
|
||||
[validate, required],
|
||||
[validate, config, t, required],
|
||||
)
|
||||
|
||||
const { formInitializing, formProcessing, path, setValue, showError, value } = useField({
|
||||
@@ -57,41 +94,39 @@ const PasswordFieldComponent: React.FC<PasswordFieldProps> = (props) => {
|
||||
|
||||
const disabled = disabledFromProps || formInitializing || formProcessing
|
||||
|
||||
const renderRTL = isFieldRTL({
|
||||
fieldLocalized: false,
|
||||
fieldRTL: rtl,
|
||||
locale,
|
||||
localizationConfig: config.localization || undefined,
|
||||
})
|
||||
|
||||
return (
|
||||
<div
|
||||
className={[
|
||||
fieldBaseClass,
|
||||
'password',
|
||||
className,
|
||||
showError && 'error',
|
||||
disabled && 'read-only',
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join(' ')}
|
||||
style={{
|
||||
...style,
|
||||
width,
|
||||
<PasswordInput
|
||||
AfterInput={AfterInput}
|
||||
BeforeInput={BeforeInput}
|
||||
CustomDescription={CustomDescription}
|
||||
CustomError={CustomError}
|
||||
CustomLabel={CustomLabel}
|
||||
autoComplete={autoComplete}
|
||||
className={className}
|
||||
errorProps={errorProps}
|
||||
inputRef={inputRef}
|
||||
label={label}
|
||||
labelProps={labelProps}
|
||||
onChange={(e) => {
|
||||
setValue(e.target.value)
|
||||
}}
|
||||
>
|
||||
<FieldLabel
|
||||
CustomLabel={CustomLabel}
|
||||
label={label}
|
||||
required={required}
|
||||
{...(labelProps || {})}
|
||||
/>
|
||||
<div className={`${fieldBaseClass}__wrap`}>
|
||||
<FieldError CustomError={CustomError} path={path} {...(errorProps || {})} />
|
||||
<input
|
||||
autoComplete={autoComplete}
|
||||
disabled={disabled}
|
||||
id={`field-${path.replace(/\./g, '__')}`}
|
||||
name={path}
|
||||
onChange={setValue}
|
||||
type="password"
|
||||
value={(value as string) || ''}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
path={path}
|
||||
placeholder={placeholder}
|
||||
readOnly={disabled}
|
||||
required={required}
|
||||
rtl={renderRTL}
|
||||
showError={showError}
|
||||
style={style}
|
||||
value={(value as string) || ''}
|
||||
width={width}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
86
packages/ui/src/fields/Password/input.tsx
Normal file
86
packages/ui/src/fields/Password/input.tsx
Normal file
@@ -0,0 +1,86 @@
|
||||
'use client'
|
||||
import type { ChangeEvent } from 'react'
|
||||
|
||||
import React from 'react'
|
||||
|
||||
import type { PasswordInputProps } from './types.js'
|
||||
|
||||
import { FieldError } from '../FieldError/index.js'
|
||||
import { FieldLabel } from '../FieldLabel/index.js'
|
||||
import { fieldBaseClass } from '../shared/index.js'
|
||||
import './index.scss'
|
||||
|
||||
export const PasswordInput: React.FC<PasswordInputProps> = (props) => {
|
||||
const {
|
||||
AfterInput,
|
||||
BeforeInput,
|
||||
CustomDescription,
|
||||
CustomError,
|
||||
CustomLabel,
|
||||
autoComplete = 'off',
|
||||
className,
|
||||
errorProps,
|
||||
inputRef,
|
||||
label,
|
||||
labelProps,
|
||||
onChange,
|
||||
onKeyDown,
|
||||
path,
|
||||
placeholder,
|
||||
readOnly,
|
||||
required,
|
||||
rtl,
|
||||
showError,
|
||||
style,
|
||||
value,
|
||||
width,
|
||||
} = props
|
||||
|
||||
return (
|
||||
<div
|
||||
className={[
|
||||
fieldBaseClass,
|
||||
'password',
|
||||
className,
|
||||
showError && 'error',
|
||||
readOnly && 'read-only',
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join(' ')}
|
||||
style={{
|
||||
...style,
|
||||
width,
|
||||
}}
|
||||
>
|
||||
<FieldLabel
|
||||
CustomLabel={CustomLabel}
|
||||
htmlFor={`field-${path.replace(/\./g, '__')}`}
|
||||
label={label}
|
||||
required={required}
|
||||
{...(labelProps || {})}
|
||||
/>
|
||||
<div className={`${fieldBaseClass}__wrap`}>
|
||||
<FieldError CustomError={CustomError} path={path} {...(errorProps || {})} />
|
||||
<div>
|
||||
{BeforeInput !== undefined && BeforeInput}
|
||||
<input
|
||||
aria-label={label}
|
||||
autoComplete={autoComplete}
|
||||
data-rtl={rtl}
|
||||
disabled={readOnly}
|
||||
id={`field-${path.replace(/\./g, '__')}`}
|
||||
name={path}
|
||||
onChange={onChange as (e: ChangeEvent<HTMLInputElement>) => void}
|
||||
onKeyDown={onKeyDown}
|
||||
placeholder={placeholder}
|
||||
ref={inputRef}
|
||||
type="password"
|
||||
value={value || ''}
|
||||
/>
|
||||
{AfterInput !== undefined && AfterInput}
|
||||
</div>
|
||||
{CustomDescription !== undefined && CustomDescription}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
26
packages/ui/src/fields/Password/types.ts
Normal file
26
packages/ui/src/fields/Password/types.ts
Normal file
@@ -0,0 +1,26 @@
|
||||
import type { ErrorProps, LabelProps } from 'payload'
|
||||
import type { ChangeEvent } from 'react'
|
||||
export type PasswordInputProps = {
|
||||
AfterInput?: React.ReactElement
|
||||
BeforeInput?: React.ReactElement
|
||||
CustomDescription?: React.ReactElement
|
||||
CustomError?: React.ReactElement
|
||||
CustomLabel?: React.ReactElement
|
||||
autoComplete?: string
|
||||
className?: string
|
||||
errorProps: ErrorProps
|
||||
inputRef?: React.RefObject<HTMLInputElement>
|
||||
label: string
|
||||
labelProps: LabelProps
|
||||
onChange?: (e: ChangeEvent<HTMLInputElement>) => void
|
||||
onKeyDown?: React.KeyboardEventHandler<HTMLInputElement>
|
||||
path: string
|
||||
placeholder?: string
|
||||
readOnly?: boolean
|
||||
required?: boolean
|
||||
rtl?: boolean
|
||||
showError?: boolean
|
||||
style?: React.CSSProperties
|
||||
value?: string
|
||||
width?: string
|
||||
}
|
||||
@@ -220,6 +220,7 @@ export const useField = <T,>(options: Options): FieldType<T> => {
|
||||
user,
|
||||
validate,
|
||||
field?.rows,
|
||||
field?.valid,
|
||||
],
|
||||
)
|
||||
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import type { I18n } from '@payloadcms/translations'
|
||||
import type { SanitizedConfig } from 'payload'
|
||||
import type { Field, SanitizedConfig } from 'payload'
|
||||
|
||||
import { confirmPassword, password } from 'payload/shared'
|
||||
|
||||
import type { FieldSchemaMap } from './types.js'
|
||||
|
||||
@@ -14,6 +16,28 @@ export const buildFieldSchemaMap = (args: {
|
||||
const result: FieldSchemaMap = new Map()
|
||||
|
||||
config.collections.forEach((collection) => {
|
||||
if (collection.auth && !collection.auth.disableLocalStrategy) {
|
||||
// register schema with auth schemaPath
|
||||
const baseAuthFields: Field[] = [
|
||||
{
|
||||
name: 'password',
|
||||
type: 'text',
|
||||
label: i18n.t('general:password'),
|
||||
required: true,
|
||||
validate: password,
|
||||
},
|
||||
{
|
||||
name: 'confirm-password',
|
||||
type: 'text',
|
||||
label: i18n.t('authentication:confirmPassword'),
|
||||
required: true,
|
||||
validate: confirmPassword,
|
||||
},
|
||||
]
|
||||
|
||||
result.set(`_${collection.slug}.auth`, [...collection.fields, ...baseAuthFields])
|
||||
}
|
||||
|
||||
traverseFields({
|
||||
config,
|
||||
fields: collection.fields,
|
||||
|
||||
@@ -194,8 +194,6 @@ export const buildFormState = async ({ req }: { req: PayloadRequest }): Promise<
|
||||
!req.payload.collections[collectionSlug].config.auth.disableLocalStrategy
|
||||
) {
|
||||
if (formState.username) result.username = formState.username
|
||||
if (formState.password) result.password = formState.password
|
||||
if (formState['confirm-password']) result['confirm-password'] = formState['confirm-password']
|
||||
if (formState.email) result.email = formState.email
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user