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:
Jarrod Flesch
2024-07-31 14:55:08 -04:00
committed by GitHub
parent 3d89508ce3
commit 290ffd3287
26 changed files with 430 additions and 209 deletions

View File

@@ -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"

View File

@@ -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}
/>
)
}

View 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>
)
}

View 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
}

View File

@@ -220,6 +220,7 @@ export const useField = <T,>(options: Options): FieldType<T> => {
user,
validate,
field?.rows,
field?.valid,
],
)

View File

@@ -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,

View File

@@ -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
}
}