feat: allows loginWithUsername to not require username (#7480)
Allows username to be optional when using the new loginWithUsername
feature. This can be done by the following:
```ts
auth: {
loginWithUsername: {
requireUsername: false, // <-- new property, default true
requireEmail: false, // default: false
allowEmailLogin: true, // default false
},
},
```
This commit is contained in:
45
packages/next/src/elements/EmailAndUsername/index.tsx
Normal file
45
packages/next/src/elements/EmailAndUsername/index.tsx
Normal file
@@ -0,0 +1,45 @@
|
||||
'use client'
|
||||
|
||||
import type { LoginWithUsernameOptions } from 'payload'
|
||||
|
||||
import { EmailField, TextField, useTranslation } from '@payloadcms/ui'
|
||||
import { email, username } from 'payload/shared'
|
||||
import React from 'react'
|
||||
|
||||
type Props = {
|
||||
loginWithUsername?: LoginWithUsernameOptions | false
|
||||
}
|
||||
export const EmailAndUsernameFields: React.FC<Props> = ({ loginWithUsername }) => {
|
||||
const { t } = useTranslation()
|
||||
|
||||
const requireEmail = !loginWithUsername || (loginWithUsername && loginWithUsername.requireEmail)
|
||||
const requireUsername = loginWithUsername && loginWithUsername.requireUsername
|
||||
const showEmailField =
|
||||
!loginWithUsername || loginWithUsername?.requireEmail || loginWithUsername?.allowEmailLogin
|
||||
const showUsernameField = Boolean(loginWithUsername)
|
||||
|
||||
return (
|
||||
<React.Fragment>
|
||||
{showEmailField && (
|
||||
<EmailField
|
||||
autoComplete="email"
|
||||
label={t('general:email')}
|
||||
name="email"
|
||||
path="email"
|
||||
required={requireEmail}
|
||||
validate={email}
|
||||
/>
|
||||
)}
|
||||
|
||||
{showUsernameField && (
|
||||
<TextField
|
||||
label={t('authentication:username')}
|
||||
name="username"
|
||||
path="username"
|
||||
required={requireUsername}
|
||||
validate={username}
|
||||
/>
|
||||
)}
|
||||
</React.Fragment>
|
||||
)
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
'use client'
|
||||
import type { FormState } from 'payload'
|
||||
import type { FormState, LoginWithUsernameOptions } from 'payload'
|
||||
|
||||
import {
|
||||
ConfirmPasswordField,
|
||||
@@ -15,14 +15,13 @@ import {
|
||||
import { getFormState } from '@payloadcms/ui/shared'
|
||||
import React from 'react'
|
||||
|
||||
import { LoginField } from '../Login/LoginField/index.js'
|
||||
import { EmailAndUsernameFields } from '../../elements/EmailAndUsername/index.js'
|
||||
|
||||
export const CreateFirstUserClient: React.FC<{
|
||||
initialState: FormState
|
||||
loginType: 'email' | 'emailOrUsername' | 'username'
|
||||
requireEmail?: boolean
|
||||
loginWithUsername?: LoginWithUsernameOptions | false
|
||||
userSlug: string
|
||||
}> = ({ initialState, loginType, requireEmail = true, userSlug }) => {
|
||||
}> = ({ initialState, loginWithUsername, userSlug }) => {
|
||||
const { getFieldMap } = useComponentMap()
|
||||
|
||||
const {
|
||||
@@ -58,10 +57,7 @@ export const CreateFirstUserClient: React.FC<{
|
||||
redirect={admin}
|
||||
validationOperation="create"
|
||||
>
|
||||
{['emailOrUsername', 'username'].includes(loginType) && <LoginField type="username" />}
|
||||
{['email', 'emailOrUsername'].includes(loginType) && (
|
||||
<LoginField required={requireEmail} type="email" />
|
||||
)}
|
||||
<EmailAndUsernameFields loginWithUsername={loginWithUsername} />
|
||||
<PasswordField
|
||||
label={t('authentication:newPassword')}
|
||||
name="password"
|
||||
|
||||
@@ -27,12 +27,6 @@ export const CreateFirstUserView: React.FC<AdminViewProps> = async ({ initPageRe
|
||||
const collectionConfig = config.collections?.find((collection) => collection?.slug === userSlug)
|
||||
const { auth: authOptions } = collectionConfig
|
||||
const loginWithUsername = authOptions.loginWithUsername
|
||||
const emailRequired = loginWithUsername && loginWithUsername.requireEmail
|
||||
|
||||
let loginType: LoginFieldProps['type'] = loginWithUsername ? 'username' : 'email'
|
||||
if (loginWithUsername && (loginWithUsername.allowEmailLogin || loginWithUsername.requireEmail)) {
|
||||
loginType = 'emailOrUsername'
|
||||
}
|
||||
|
||||
const { formState } = await getDocumentData({
|
||||
collectionConfig,
|
||||
@@ -47,8 +41,7 @@ export const CreateFirstUserView: React.FC<AdminViewProps> = async ({ initPageRe
|
||||
<p>{req.t('authentication:beginCreateFirstUser')}</p>
|
||||
<CreateFirstUserClient
|
||||
initialState={formState}
|
||||
loginType={loginType}
|
||||
requireEmail={emailRequired}
|
||||
loginWithUsername={loginWithUsername}
|
||||
userSlug={userSlug}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -4,9 +4,7 @@ import {
|
||||
Button,
|
||||
CheckboxField,
|
||||
ConfirmPasswordField,
|
||||
EmailField,
|
||||
PasswordField,
|
||||
TextField,
|
||||
useAuth,
|
||||
useConfig,
|
||||
useDocumentInfo,
|
||||
@@ -14,12 +12,12 @@ import {
|
||||
useFormModified,
|
||||
useTranslation,
|
||||
} from '@payloadcms/ui'
|
||||
import { email as emailValidation } from 'payload/shared'
|
||||
import React, { useCallback, useEffect, useMemo, useState } from 'react'
|
||||
import { toast } from 'sonner'
|
||||
|
||||
import type { Props } from './types.js'
|
||||
|
||||
import { EmailAndUsernameFields } from '../../../../elements/EmailAndUsername/index.js'
|
||||
import { APIKey } from './APIKey.js'
|
||||
import './index.scss'
|
||||
|
||||
@@ -140,38 +138,7 @@ export const Auth: React.FC<Props> = (props) => {
|
||||
<div className={[baseClass, className].filter(Boolean).join(' ')}>
|
||||
{!disableLocalStrategy && (
|
||||
<React.Fragment>
|
||||
{Boolean(loginWithUsername) && (
|
||||
<TextField
|
||||
disabled={disabled}
|
||||
label={t('authentication:username')}
|
||||
name="username"
|
||||
readOnly={readOnly}
|
||||
required
|
||||
/>
|
||||
)}
|
||||
{(!loginWithUsername ||
|
||||
loginWithUsername?.allowEmailLogin ||
|
||||
loginWithUsername?.requireEmail) && (
|
||||
<EmailField
|
||||
autoComplete="email"
|
||||
disabled={disabled}
|
||||
label={t('general:email')}
|
||||
name="email"
|
||||
readOnly={readOnly}
|
||||
required={!loginWithUsername || loginWithUsername?.requireEmail}
|
||||
validate={(value) =>
|
||||
emailValidation(value, {
|
||||
name: 'email',
|
||||
type: 'email',
|
||||
data: {},
|
||||
preferences: { fields: {} },
|
||||
req: { t } as any,
|
||||
required: true,
|
||||
siblingData: {},
|
||||
})
|
||||
}
|
||||
/>
|
||||
)}
|
||||
<EmailAndUsernameFields loginWithUsername={loginWithUsername} />
|
||||
{(showPasswordFields || requirePassword) && (
|
||||
<div className={`${baseClass}__changing-password`}>
|
||||
<PasswordField
|
||||
|
||||
@@ -1,16 +1,16 @@
|
||||
'use client'
|
||||
import type { PayloadRequest } from 'payload'
|
||||
import type { Validate, ValidateOptions } from 'payload'
|
||||
|
||||
import { EmailField, TextField, useConfig, useTranslation } from '@payloadcms/ui'
|
||||
import { EmailField, TextField, useTranslation } from '@payloadcms/ui'
|
||||
import { email, username } from 'payload/shared'
|
||||
import React from 'react'
|
||||
export type LoginFieldProps = {
|
||||
required?: boolean
|
||||
type: 'email' | 'emailOrUsername' | 'username'
|
||||
validate?: Validate
|
||||
}
|
||||
export const LoginField: React.FC<LoginFieldProps> = ({ type, required = true }) => {
|
||||
const { t } = useTranslation()
|
||||
const config = useConfig()
|
||||
|
||||
if (type === 'email') {
|
||||
return (
|
||||
@@ -20,17 +20,7 @@ export const LoginField: React.FC<LoginFieldProps> = ({ type, required = true })
|
||||
name="email"
|
||||
path="email"
|
||||
required={required}
|
||||
validate={(value) =>
|
||||
email(value, {
|
||||
name: 'email',
|
||||
type: 'email',
|
||||
data: {},
|
||||
preferences: { fields: {} },
|
||||
req: { t } as PayloadRequest,
|
||||
required: true,
|
||||
siblingData: {},
|
||||
})
|
||||
}
|
||||
validate={email}
|
||||
/>
|
||||
)
|
||||
}
|
||||
@@ -41,23 +31,8 @@ export const LoginField: React.FC<LoginFieldProps> = ({ type, required = true })
|
||||
label={t('authentication:username')}
|
||||
name="username"
|
||||
path="username"
|
||||
required
|
||||
validate={(value) =>
|
||||
username(value, {
|
||||
name: 'username',
|
||||
type: 'text',
|
||||
data: {},
|
||||
preferences: { fields: {} },
|
||||
req: {
|
||||
payload: {
|
||||
config,
|
||||
},
|
||||
t,
|
||||
} as PayloadRequest,
|
||||
required: true,
|
||||
siblingData: {},
|
||||
})
|
||||
}
|
||||
required={required}
|
||||
validate={username}
|
||||
/>
|
||||
)
|
||||
}
|
||||
@@ -68,36 +43,16 @@ export const LoginField: React.FC<LoginFieldProps> = ({ type, required = true })
|
||||
label={t('authentication:emailOrUsername')}
|
||||
name="username"
|
||||
path="username"
|
||||
required
|
||||
validate={(value) => {
|
||||
const passesUsername = username(value, {
|
||||
name: 'username',
|
||||
type: 'text',
|
||||
data: {},
|
||||
preferences: { fields: {} },
|
||||
req: {
|
||||
payload: {
|
||||
config,
|
||||
},
|
||||
t,
|
||||
} as PayloadRequest,
|
||||
required: true,
|
||||
siblingData: {},
|
||||
})
|
||||
const passesEmail = email(value, {
|
||||
name: 'username',
|
||||
type: 'email',
|
||||
data: {},
|
||||
preferences: { fields: {} },
|
||||
req: {
|
||||
payload: {
|
||||
config,
|
||||
},
|
||||
t,
|
||||
} as PayloadRequest,
|
||||
required: true,
|
||||
siblingData: {},
|
||||
})
|
||||
required={required}
|
||||
validate={(value, options) => {
|
||||
const passesUsername = username(
|
||||
value,
|
||||
options as ValidateOptions<any, { email?: string }, any, string>,
|
||||
)
|
||||
const passesEmail = email(
|
||||
value,
|
||||
options as ValidateOptions<any, { username?: string }, any, string>,
|
||||
)
|
||||
|
||||
if (!passesEmail && !passesUsername) {
|
||||
return `${t('general:email')}: ${passesEmail} ${t('general:username')}: ${passesUsername}`
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import type { ArrayField } from '../../fields/config/types.js'
|
||||
import type { ArrayFieldValidation } from '../../fields/validations.js'
|
||||
import type { ErrorComponent } from '../forms/Error.js'
|
||||
import type { FieldMap } from '../forms/FieldMap.js'
|
||||
import type { DescriptionComponent, FormFieldBase, LabelComponent } from '../types.js'
|
||||
@@ -12,6 +13,7 @@ export type ArrayFieldProps = {
|
||||
maxRows?: ArrayField['maxRows']
|
||||
minRows?: ArrayField['minRows']
|
||||
name?: string
|
||||
validate?: ArrayFieldValidation
|
||||
width?: string
|
||||
} & FormFieldBase
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import type { Block, BlockField } from '../../fields/config/types.js'
|
||||
import type { BlockFieldValidation } from '../../fields/validations.js'
|
||||
import type { ErrorComponent } from '../forms/Error.js'
|
||||
import type { FieldMap } from '../forms/FieldMap.js'
|
||||
import type { DescriptionComponent, FormFieldBase, LabelComponent } from '../types.js'
|
||||
@@ -12,6 +13,7 @@ export type BlocksFieldProps = {
|
||||
minRows?: number
|
||||
name?: string
|
||||
slug?: string
|
||||
validate?: BlockFieldValidation
|
||||
width?: string
|
||||
} & FormFieldBase
|
||||
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import type { CheckboxFieldValidation } from '../../fields/validations.js'
|
||||
import type { ErrorComponent } from '../forms/Error.js'
|
||||
import type { DescriptionComponent, FormFieldBase, LabelComponent } from '../types.js'
|
||||
|
||||
@@ -9,6 +10,7 @@ export type CheckboxFieldProps = {
|
||||
onChange?: (val: boolean) => void
|
||||
partialChecked?: boolean
|
||||
path?: string
|
||||
validate?: CheckboxFieldValidation
|
||||
width?: string
|
||||
} & FormFieldBase
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import type { CodeField } from '../../fields/config/types.js'
|
||||
import type { CodeFieldValidation } from '../../fields/validations.js'
|
||||
import type { ErrorComponent } from '../forms/Error.js'
|
||||
import type { DescriptionComponent, FormFieldBase, LabelComponent } from '../types.js'
|
||||
|
||||
@@ -7,6 +8,7 @@ export type CodeFieldProps = {
|
||||
language?: CodeField['admin']['language']
|
||||
name?: string
|
||||
path?: string
|
||||
validate?: CodeFieldValidation
|
||||
width: string
|
||||
} & FormFieldBase
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import type { DateField } from '../../fields/config/types.js'
|
||||
import type { DateFieldValidation } from '../../fields/validations.js'
|
||||
import type { ErrorComponent } from '../forms/Error.js'
|
||||
import type { DescriptionComponent, FormFieldBase, LabelComponent } from '../types.js'
|
||||
|
||||
@@ -7,6 +8,7 @@ export type DateFieldProps = {
|
||||
name?: string
|
||||
path?: string
|
||||
placeholder?: DateField['admin']['placeholder'] | string
|
||||
validate?: DateFieldValidation
|
||||
width?: string
|
||||
} & FormFieldBase
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import type { EmailField } from '../../fields/config/types.js'
|
||||
import type { EmailFieldValidation } from '../../fields/validations.js'
|
||||
import type { ErrorComponent } from '../forms/Error.js'
|
||||
import type { DescriptionComponent, FormFieldBase, LabelComponent } from '../types.js'
|
||||
|
||||
@@ -7,6 +8,7 @@ export type EmailFieldProps = {
|
||||
name?: string
|
||||
path?: string
|
||||
placeholder?: EmailField['admin']['placeholder']
|
||||
validate?: EmailFieldValidation
|
||||
width?: string
|
||||
} & FormFieldBase
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import type { JSONField } from '../../fields/config/types.js'
|
||||
import type { JSONFieldValidation } from '../../fields/validations.js'
|
||||
import type { ErrorComponent } from '../forms/Error.js'
|
||||
import type { DescriptionComponent, FormFieldBase, LabelComponent } from '../types.js'
|
||||
|
||||
@@ -7,6 +8,7 @@ export type JSONFieldProps = {
|
||||
jsonSchema?: Record<string, unknown>
|
||||
name?: string
|
||||
path?: string
|
||||
validate?: JSONFieldValidation
|
||||
width?: string
|
||||
} & FormFieldBase
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import type { NumberField } from '../../fields/config/types.js'
|
||||
import type { NumberFieldValidation } from '../../fields/validations.js'
|
||||
import type { ErrorComponent } from '../forms/Error.js'
|
||||
import type { DescriptionComponent, FormFieldBase, LabelComponent } from '../types.js'
|
||||
|
||||
@@ -12,6 +13,7 @@ export type NumberFieldProps = {
|
||||
path?: string
|
||||
placeholder?: NumberField['admin']['placeholder']
|
||||
step?: number
|
||||
validate?: NumberFieldValidation
|
||||
width?: string
|
||||
} & FormFieldBase
|
||||
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import type { PointFieldValidation } from '../../fields/validations.js'
|
||||
import type { ErrorComponent } from '../forms/Error.js'
|
||||
import type { DescriptionComponent, FormFieldBase, LabelComponent } from '../types.js'
|
||||
|
||||
@@ -6,6 +7,7 @@ export type PointFieldProps = {
|
||||
path?: string
|
||||
placeholder?: string
|
||||
step?: number
|
||||
validate?: PointFieldValidation
|
||||
width?: string
|
||||
} & FormFieldBase
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import type { Option } from '../../fields/config/types.js'
|
||||
import type { RadioFieldValidation } from '../../fields/validations.js'
|
||||
import type { ErrorComponent } from '../forms/Error.js'
|
||||
import type { DescriptionComponent, FormFieldBase, LabelComponent } from '../types.js'
|
||||
|
||||
@@ -8,6 +9,7 @@ export type RadioFieldProps = {
|
||||
onChange?: OnChange
|
||||
options?: Option[]
|
||||
path?: string
|
||||
validate?: RadioFieldValidation
|
||||
value?: string
|
||||
width?: string
|
||||
} & FormFieldBase
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import type { RelationshipField } from '../../fields/config/types.js'
|
||||
import type { RelationshipFieldValidation } from '../../fields/validations.js'
|
||||
import type { ErrorComponent } from '../forms/Error.js'
|
||||
import type { DescriptionComponent, FormFieldBase, LabelComponent } from '../types.js'
|
||||
|
||||
@@ -9,6 +10,7 @@ export type RelationshipFieldProps = {
|
||||
name: string
|
||||
relationTo?: RelationshipField['relationTo']
|
||||
sortOptions?: RelationshipField['admin']['sortOptions']
|
||||
validate?: RelationshipFieldValidation
|
||||
width?: string
|
||||
} & FormFieldBase
|
||||
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import type { RichTextFieldValidation } from '../../fields/validations.js'
|
||||
import type { ErrorComponent } from '../forms/Error.js'
|
||||
import type { MappedField } from '../forms/FieldMap.js'
|
||||
import type { DescriptionComponent, FormFieldBase, LabelComponent } from '../types.js'
|
||||
@@ -5,6 +6,7 @@ import type { DescriptionComponent, FormFieldBase, LabelComponent } from '../typ
|
||||
export type RichTextComponentProps = {
|
||||
name: string
|
||||
richTextComponentMap?: Map<string, MappedField[] | React.ReactNode>
|
||||
validate?: RichTextFieldValidation
|
||||
width?: string
|
||||
} & FormFieldBase
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import type { Option } from '../../fields/config/types.js'
|
||||
import type { SelectFieldValidation } from '../../fields/validations.js'
|
||||
import type { ErrorComponent } from '../forms/Error.js'
|
||||
import type { DescriptionComponent, FormFieldBase, LabelComponent } from '../types.js'
|
||||
|
||||
@@ -10,6 +11,7 @@ export type SelectFieldProps = {
|
||||
onChange?: (e: string | string[]) => void
|
||||
options?: Option[]
|
||||
path?: string
|
||||
validate?: SelectFieldValidation
|
||||
value?: string
|
||||
width?: string
|
||||
} & FormFieldBase
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import type { TextField } from '../../fields/config/types.js'
|
||||
import type { TextFieldValidation } from '../../fields/validations.js'
|
||||
import type { ErrorComponent } from '../forms/Error.js'
|
||||
import type { DescriptionComponent, FormFieldBase, LabelComponent } from '../types.js'
|
||||
|
||||
@@ -13,6 +14,7 @@ export type TextFieldProps = {
|
||||
onKeyDown?: React.KeyboardEventHandler<HTMLInputElement>
|
||||
path?: string
|
||||
placeholder?: TextField['admin']['placeholder']
|
||||
validate?: TextFieldValidation
|
||||
width?: string
|
||||
} & FormFieldBase
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import type { TextareaField } from '../../fields/config/types.js'
|
||||
import type { TextareaFieldValidation } from '../../fields/validations.js'
|
||||
import type { ErrorComponent } from '../forms/Error.js'
|
||||
import type { DescriptionComponent, FormFieldBase, LabelComponent } from '../types.js'
|
||||
|
||||
@@ -9,6 +10,7 @@ export type TextareaFieldProps = {
|
||||
path?: string
|
||||
placeholder?: TextareaField['admin']['placeholder']
|
||||
rows?: number
|
||||
validate?: TextareaFieldValidation
|
||||
width?: string
|
||||
} & FormFieldBase
|
||||
|
||||
|
||||
@@ -1,4 +1,10 @@
|
||||
import type { DescriptionComponent, FormFieldBase, LabelComponent, UploadField } from 'payload'
|
||||
import type {
|
||||
DescriptionComponent,
|
||||
FormFieldBase,
|
||||
LabelComponent,
|
||||
UploadField,
|
||||
UploadFieldValidation,
|
||||
} from 'payload'
|
||||
|
||||
import type { ErrorComponent } from '../forms/Error.js'
|
||||
|
||||
@@ -7,6 +13,7 @@ export type UploadFieldProps = {
|
||||
name?: string
|
||||
path?: string
|
||||
relationTo?: UploadField['relationTo']
|
||||
validate?: UploadFieldValidation
|
||||
width?: string
|
||||
} & FormFieldBase
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { ClientValidate, Field } from '../../fields/config/types.js'
|
||||
import type { Field, Validate } from '../../fields/config/types.js'
|
||||
import type { Where } from '../../types/index.js'
|
||||
|
||||
export type Data = {
|
||||
@@ -25,7 +25,7 @@ export type FormField = {
|
||||
passesCondition?: boolean
|
||||
rows?: Row[]
|
||||
valid: boolean
|
||||
validate?: ClientValidate
|
||||
validate?: Validate
|
||||
value: unknown
|
||||
}
|
||||
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import type { Field } from '../../fields/config/types.js'
|
||||
import type { EmailField } from '../../fields/config/types.js'
|
||||
|
||||
import { email } from '../../fields/validations.js'
|
||||
|
||||
export const emailField = ({ required = true }: { required?: boolean }): Field => ({
|
||||
export const emailFieldConfig: EmailField = {
|
||||
name: 'email',
|
||||
type: 'email',
|
||||
admin: {
|
||||
@@ -20,7 +20,7 @@ export const emailField = ({ required = true }: { required?: boolean }): Field =
|
||||
],
|
||||
},
|
||||
label: ({ t }) => t('general:email'),
|
||||
required,
|
||||
required: true,
|
||||
unique: true,
|
||||
validate: email,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import type { Field } from '../../fields/config/types.js'
|
||||
import type { TextField } from '../../fields/config/types.js'
|
||||
|
||||
import { username } from '../../fields/validations.js'
|
||||
|
||||
export const usernameField: Field = {
|
||||
export const usernameFieldConfig: TextField = {
|
||||
name: 'username',
|
||||
type: 'text',
|
||||
admin: {
|
||||
|
||||
77
packages/payload/src/auth/ensureUsernameOrEmail.ts
Normal file
77
packages/payload/src/auth/ensureUsernameOrEmail.ts
Normal file
@@ -0,0 +1,77 @@
|
||||
import type { RequiredDataFromCollectionSlug } from '../collections/config/types.js'
|
||||
import type { AuthCollection, CollectionSlug, PayloadRequest } from '../index.js'
|
||||
|
||||
import { ValidationError } from '../errors/index.js'
|
||||
|
||||
type ValidateUsernameOrEmailArgs<TSlug extends CollectionSlug> = {
|
||||
authOptions: AuthCollection['config']['auth']
|
||||
collectionSlug: string
|
||||
data: RequiredDataFromCollectionSlug<TSlug>
|
||||
req: PayloadRequest
|
||||
} & (
|
||||
| {
|
||||
operation: 'create'
|
||||
originalDoc?: never
|
||||
}
|
||||
| {
|
||||
operation: 'update'
|
||||
originalDoc: RequiredDataFromCollectionSlug<TSlug>
|
||||
}
|
||||
)
|
||||
export const ensureUsernameOrEmail = <TSlug extends CollectionSlug>({
|
||||
authOptions: { disableLocalStrategy, loginWithUsername },
|
||||
collectionSlug,
|
||||
data,
|
||||
operation,
|
||||
originalDoc,
|
||||
req,
|
||||
}: ValidateUsernameOrEmailArgs<TSlug>) => {
|
||||
// neither username or email are required
|
||||
// and neither are provided
|
||||
// so we need to manually validate
|
||||
if (
|
||||
!disableLocalStrategy &&
|
||||
loginWithUsername &&
|
||||
!loginWithUsername.requireEmail &&
|
||||
!loginWithUsername.requireUsername
|
||||
) {
|
||||
let missingFields = false
|
||||
if (operation === 'create' && !data.email && !data.username) {
|
||||
missingFields = true
|
||||
} else if (operation === 'update') {
|
||||
// prevent clearing both email and username
|
||||
if ('email' in data && !data.email && 'username' in data && !data.username) {
|
||||
missingFields = true
|
||||
}
|
||||
// prevent clearing email if no username
|
||||
if ('email' in data && !data.email && !originalDoc.username) {
|
||||
missingFields = true
|
||||
}
|
||||
// prevent clearing username if no email
|
||||
if ('username' in data && !data.username && !originalDoc.email) {
|
||||
missingFields = true
|
||||
}
|
||||
}
|
||||
|
||||
if (missingFields) {
|
||||
throw new ValidationError(
|
||||
{
|
||||
collection: collectionSlug,
|
||||
errors: [
|
||||
{
|
||||
field: 'username',
|
||||
message: 'Username or email is required',
|
||||
},
|
||||
{
|
||||
field: 'email',
|
||||
message: 'Username or email is required',
|
||||
},
|
||||
],
|
||||
},
|
||||
req.t,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
@@ -1,11 +1,11 @@
|
||||
import type { Field } from '../fields/config/types.js'
|
||||
import type { Field, TextField } from '../fields/config/types.js'
|
||||
import type { IncomingAuthType } from './types.js'
|
||||
|
||||
import { accountLockFields } from './baseFields/accountLock.js'
|
||||
import { apiKeyFields } from './baseFields/apiKey.js'
|
||||
import { baseAuthFields } from './baseFields/auth.js'
|
||||
import { emailField } from './baseFields/email.js'
|
||||
import { usernameField } from './baseFields/username.js'
|
||||
import { emailFieldConfig } from './baseFields/email.js'
|
||||
import { usernameFieldConfig } from './baseFields/username.js'
|
||||
import { verificationFields } from './baseFields/verification.js'
|
||||
|
||||
export const getBaseAuthFields = (authConfig: IncomingAuthType): Field[] => {
|
||||
@@ -16,18 +16,23 @@ export const getBaseAuthFields = (authConfig: IncomingAuthType): Field[] => {
|
||||
}
|
||||
|
||||
if (!authConfig.disableLocalStrategy) {
|
||||
const emailFieldIndex = authFields.push(emailField({ required: true })) - 1
|
||||
const emailField = { ...emailFieldConfig }
|
||||
let usernameField: TextField | undefined
|
||||
|
||||
if (authConfig.loginWithUsername) {
|
||||
if (
|
||||
typeof authConfig.loginWithUsername === 'object' &&
|
||||
authConfig.loginWithUsername.requireEmail === false
|
||||
) {
|
||||
authFields[emailFieldIndex] = emailField({ required: false })
|
||||
usernameField = { ...usernameFieldConfig }
|
||||
if (typeof authConfig.loginWithUsername === 'object') {
|
||||
if (authConfig.loginWithUsername.requireEmail === false) {
|
||||
emailField.required = false
|
||||
}
|
||||
if (authConfig.loginWithUsername.requireUsername === false) {
|
||||
usernameField.required = false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
authFields.push(usernameField)
|
||||
}
|
||||
authFields.push(emailField)
|
||||
if (usernameField) authFields.push(usernameField)
|
||||
|
||||
authFields.push(...baseAuthFields)
|
||||
|
||||
|
||||
@@ -11,6 +11,7 @@ import { Forbidden } from '../../errors/index.js'
|
||||
import { commitTransaction } from '../../utilities/commitTransaction.js'
|
||||
import { initTransaction } from '../../utilities/initTransaction.js'
|
||||
import { killTransaction } from '../../utilities/killTransaction.js'
|
||||
import { ensureUsernameOrEmail } from '../ensureUsernameOrEmail.js'
|
||||
|
||||
export type Arguments<TSlug extends CollectionSlug> = {
|
||||
collection: Collection
|
||||
@@ -44,6 +45,14 @@ export const registerFirstUserOperation = async <TSlug extends CollectionSlug>(
|
||||
try {
|
||||
const shouldCommit = await initTransaction(req)
|
||||
|
||||
ensureUsernameOrEmail<TSlug>({
|
||||
authOptions: config.auth,
|
||||
collectionSlug: slug,
|
||||
data,
|
||||
operation: 'create',
|
||||
req,
|
||||
})
|
||||
|
||||
const doc = await payload.db.findOne({
|
||||
collection: config.slug,
|
||||
req,
|
||||
|
||||
@@ -118,10 +118,18 @@ export type AuthStrategy = {
|
||||
name: string
|
||||
}
|
||||
|
||||
export type LoginWithUsernameOptions = {
|
||||
allowEmailLogin?: boolean
|
||||
export type LoginWithUsernameOptions =
|
||||
| {
|
||||
allowEmailLogin?: false
|
||||
requireEmail?: boolean
|
||||
}
|
||||
// If `allowEmailLogin` is false, `requireUsername` must be true (default: true)
|
||||
requireUsername?: true
|
||||
}
|
||||
| {
|
||||
allowEmailLogin?: true
|
||||
requireEmail?: boolean
|
||||
requireUsername?: boolean
|
||||
}
|
||||
|
||||
export interface IncomingAuthType {
|
||||
cookies?: {
|
||||
|
||||
@@ -66,4 +66,5 @@ export const authDefaults: IncomingAuthType = {
|
||||
export const loginWithUsernameDefaults: LoginWithUsernameOptions = {
|
||||
allowEmailLogin: false,
|
||||
requireEmail: false,
|
||||
requireUsername: true,
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import type { LoginWithUsernameOptions } from '../../auth/types.js'
|
||||
import type { Config, SanitizedConfig } from '../../config/types.js'
|
||||
import type { CollectionConfig, SanitizedCollectionConfig } from './types.js'
|
||||
|
||||
@@ -153,14 +154,24 @@ export const sanitizeCollection = async (
|
||||
sanitized.auth.strategies = []
|
||||
}
|
||||
|
||||
sanitized.auth.loginWithUsername = sanitized.auth.loginWithUsername
|
||||
? {
|
||||
if (sanitized.auth.loginWithUsername) {
|
||||
if (sanitized.auth.loginWithUsername === true) {
|
||||
sanitized.auth.loginWithUsername = loginWithUsernameDefaults
|
||||
} else {
|
||||
const loginWithUsernameWithDefaults = {
|
||||
...loginWithUsernameDefaults,
|
||||
...(typeof sanitized.auth.loginWithUsername === 'boolean'
|
||||
? {}
|
||||
: sanitized.auth.loginWithUsername),
|
||||
...sanitized.auth.loginWithUsername,
|
||||
} as LoginWithUsernameOptions
|
||||
|
||||
// if allowEmailLogin is false, requireUsername must be true
|
||||
if (loginWithUsernameWithDefaults.allowEmailLogin === false) {
|
||||
loginWithUsernameWithDefaults.requireUsername = true
|
||||
}
|
||||
sanitized.auth.loginWithUsername = loginWithUsernameWithDefaults
|
||||
}
|
||||
} else {
|
||||
sanitized.auth.loginWithUsername = false
|
||||
}
|
||||
: false
|
||||
|
||||
sanitized.fields = mergeBaseFields(sanitized.fields, getBaseAuthFields(sanitized.auth))
|
||||
}
|
||||
|
||||
@@ -11,6 +11,7 @@ import type {
|
||||
RequiredDataFromCollectionSlug,
|
||||
} from '../config/types.js'
|
||||
|
||||
import { ensureUsernameOrEmail } from '../../auth/ensureUsernameOrEmail.js'
|
||||
import executeAccess from '../../auth/executeAccess.js'
|
||||
import { sendVerificationEmail } from '../../auth/sendVerificationEmail.js'
|
||||
import { registerLocalStrategy } from '../../auth/strategies/local/register.js'
|
||||
@@ -49,6 +50,14 @@ export const createOperation = async <TSlug extends CollectionSlug>(
|
||||
try {
|
||||
const shouldCommit = await initTransaction(args.req)
|
||||
|
||||
ensureUsernameOrEmail<TSlug>({
|
||||
authOptions: args.collection.config.auth,
|
||||
collectionSlug: args.collection.config.slug,
|
||||
data: args.data,
|
||||
operation: 'create',
|
||||
req: args.req,
|
||||
})
|
||||
|
||||
// /////////////////////////////////////
|
||||
// beforeOperation - Collection
|
||||
// /////////////////////////////////////
|
||||
|
||||
@@ -12,6 +12,7 @@ import type {
|
||||
RequiredDataFromCollectionSlug,
|
||||
} from '../config/types.js'
|
||||
|
||||
import { ensureUsernameOrEmail } from '../../auth/ensureUsernameOrEmail.js'
|
||||
import executeAccess from '../../auth/executeAccess.js'
|
||||
import { combineQueries } from '../../database/combineQueries.js'
|
||||
import { validateQueryPaths } from '../../database/queryValidation/validateQueryPaths.js'
|
||||
@@ -199,6 +200,17 @@ export const updateOperation = async <TSlug extends CollectionSlug>(
|
||||
req,
|
||||
})
|
||||
|
||||
if (args.collection.config.auth) {
|
||||
ensureUsernameOrEmail<TSlug>({
|
||||
authOptions: args.collection.config.auth,
|
||||
collectionSlug: args.collection.config.slug,
|
||||
data: args.data,
|
||||
operation: 'update',
|
||||
originalDoc,
|
||||
req: args.req,
|
||||
})
|
||||
}
|
||||
|
||||
// /////////////////////////////////////
|
||||
// beforeValidate - Fields
|
||||
// /////////////////////////////////////
|
||||
|
||||
@@ -11,6 +11,7 @@ import type {
|
||||
RequiredDataFromCollectionSlug,
|
||||
} from '../config/types.js'
|
||||
|
||||
import { ensureUsernameOrEmail } from '../../auth/ensureUsernameOrEmail.js'
|
||||
import executeAccess from '../../auth/executeAccess.js'
|
||||
import { generatePasswordSaltHash } from '../../auth/strategies/local/generatePasswordSaltHash.js'
|
||||
import { hasWhereAccessResult } from '../../auth/types.js'
|
||||
@@ -143,6 +144,17 @@ export const updateByIDOperation = async <TSlug extends CollectionSlug>(
|
||||
showHiddenFields: true,
|
||||
})
|
||||
|
||||
if (args.collection.config.auth) {
|
||||
ensureUsernameOrEmail<TSlug>({
|
||||
authOptions: args.collection.config.auth,
|
||||
collectionSlug: args.collection.config.slug,
|
||||
data: args.data,
|
||||
operation: 'update',
|
||||
originalDoc,
|
||||
req: args.req,
|
||||
})
|
||||
}
|
||||
|
||||
// /////////////////////////////////////
|
||||
// Generate data for all files and sizes
|
||||
// /////////////////////////////////////
|
||||
|
||||
@@ -8,13 +8,20 @@ import { APIError } from './APIError.js'
|
||||
// This gets dynamically reassigned during compilation
|
||||
export let ValidationErrorName = 'ValidationError'
|
||||
|
||||
export type ValidationFieldError = {
|
||||
// The field path, i.e. "textField", "groupField.subTextField", etc.
|
||||
field: string
|
||||
// The error message to display for this field
|
||||
message: string
|
||||
}
|
||||
|
||||
export class ValidationError extends APIError<{
|
||||
collection?: string
|
||||
errors: { field: string; message: string }[]
|
||||
errors: ValidationFieldError[]
|
||||
global?: string
|
||||
}> {
|
||||
constructor(
|
||||
results: { collection?: string; errors: { field: string; message: string }[]; global?: string },
|
||||
results: { collection?: string; errors: ValidationFieldError[]; global?: string },
|
||||
t?: TFunction,
|
||||
) {
|
||||
const message = t
|
||||
|
||||
@@ -20,3 +20,4 @@ export { NotFound } from './NotFound.js'
|
||||
export { QueryError } from './QueryError.js'
|
||||
export { ReservedFieldName } from './ReservedFieldName.js'
|
||||
export { ValidationError, ValidationErrorName } from './ValidationError.js'
|
||||
export type { ValidationFieldError } from './ValidationError.js'
|
||||
|
||||
@@ -7,6 +7,7 @@ import type { CSSProperties } from 'react'
|
||||
import monacoeditor from 'monaco-editor' // IMPORTANT - DO NOT REMOVE: This is required for pnpm's default isolated mode to work - even though the import is not used. This is due to a typescript bug: https://github.com/microsoft/TypeScript/issues/47663#issuecomment-1519138189. (tsbugisolatedmode)
|
||||
import type { JSONSchema4 } from 'json-schema'
|
||||
import type React from 'react'
|
||||
import type { DeepPartial } from 'ts-essentials'
|
||||
|
||||
import type { RichTextAdapter, RichTextAdapterProvider } from '../../admin/RichText.js'
|
||||
import type { ErrorComponent } from '../../admin/forms/Error.js'
|
||||
@@ -176,12 +177,14 @@ export type Labels = {
|
||||
}
|
||||
|
||||
export type BaseValidateOptions<TData, TSiblingData, TValue> = {
|
||||
collectionSlug?: string
|
||||
data: Partial<TData>
|
||||
id?: number | string
|
||||
operation?: Operation
|
||||
preferences: DocumentPreferences
|
||||
previousValue?: TValue
|
||||
req: PayloadRequest
|
||||
required?: boolean
|
||||
siblingData: Partial<TSiblingData>
|
||||
}
|
||||
|
||||
@@ -202,8 +205,6 @@ export type Validate<
|
||||
options: ValidateOptions<TData, TSiblingData, TFieldConfig, TValue>,
|
||||
) => Promise<string | true> | string | true
|
||||
|
||||
export type ClientValidate = Omit<Validate, 'req'>
|
||||
|
||||
export type OptionObject = {
|
||||
label: LabelFunction | LabelStatic
|
||||
value: string
|
||||
|
||||
@@ -136,6 +136,7 @@ export const promise = async ({
|
||||
const validationResult = await field.validate(valueToValidate, {
|
||||
...field,
|
||||
id,
|
||||
collectionSlug: collection?.slug,
|
||||
data: deepMergeWithSourceArrays(doc, data),
|
||||
jsonError,
|
||||
operation,
|
||||
|
||||
@@ -31,7 +31,8 @@ import type {
|
||||
import { isNumber } from '../utilities/isNumber.js'
|
||||
import { isValidID } from '../utilities/isValidID.js'
|
||||
|
||||
export const text: Validate<string | string[], unknown, unknown, TextField> = (
|
||||
export type TextFieldValidation = Validate<string, unknown, unknown, TextField>
|
||||
export const text: TextFieldValidation = (
|
||||
value,
|
||||
{
|
||||
hasMany,
|
||||
@@ -83,7 +84,8 @@ export const text: Validate<string | string[], unknown, unknown, TextField> = (
|
||||
return true
|
||||
}
|
||||
|
||||
export const password: Validate<string, unknown, unknown, TextField> = (
|
||||
export type PasswordFieldValidation = Validate<string, unknown, unknown, TextField>
|
||||
export const password: PasswordFieldValidation = (
|
||||
value,
|
||||
{
|
||||
maxLength: fieldMaxLength,
|
||||
@@ -115,32 +117,54 @@ export const password: Validate<string, unknown, unknown, TextField> = (
|
||||
return true
|
||||
}
|
||||
|
||||
export const confirmPassword: Validate<string, unknown, unknown, TextField> = (
|
||||
export type ConfirmPasswordFieldValidation = Validate<
|
||||
string,
|
||||
unknown,
|
||||
{ password: string },
|
||||
TextField
|
||||
>
|
||||
export const confirmPassword: ConfirmPasswordFieldValidation = (
|
||||
value,
|
||||
{ req: { data, t }, required },
|
||||
{ req: { t }, required, siblingData },
|
||||
) => {
|
||||
if (required && !value) {
|
||||
return t('validation:required')
|
||||
}
|
||||
|
||||
if (
|
||||
value &&
|
||||
typeof data.formState === 'object' &&
|
||||
'password' in data.formState &&
|
||||
typeof data.formState.password === 'object' &&
|
||||
'value' in data.formState.password &&
|
||||
value !== data.formState.password.value
|
||||
) {
|
||||
if (value && value !== siblingData.password) {
|
||||
return t('fields:passwordsDoNotMatch')
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
export const email: Validate<string, unknown, unknown, EmailField> = (
|
||||
export type EmailFieldValidation = Validate<string, unknown, { username?: string }, EmailField>
|
||||
export const email: EmailFieldValidation = (
|
||||
value,
|
||||
{ req: { t }, required },
|
||||
{
|
||||
collectionSlug,
|
||||
req: {
|
||||
payload: { config },
|
||||
t,
|
||||
},
|
||||
required,
|
||||
siblingData,
|
||||
},
|
||||
) => {
|
||||
if (collectionSlug) {
|
||||
const collection = config.collections.find(({ slug }) => slug === collectionSlug)
|
||||
|
||||
if (
|
||||
collection.auth.loginWithUsername &&
|
||||
!collection.auth.loginWithUsername?.requireUsername &&
|
||||
!collection.auth.loginWithUsername?.requireEmail
|
||||
) {
|
||||
if (!value && !siblingData?.username) {
|
||||
return t('validation:required')
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if ((value && !/\S[^\s@]*@\S+\.\S+/.test(value)) || (!value && required)) {
|
||||
return t('validation:emailAddress')
|
||||
}
|
||||
@@ -148,18 +172,35 @@ export const email: Validate<string, unknown, unknown, EmailField> = (
|
||||
return true
|
||||
}
|
||||
|
||||
export const username: Validate<string, unknown, unknown, TextField> = (
|
||||
export type UsernameFieldValidation = Validate<string, unknown, { email?: string }, TextField>
|
||||
export const username: UsernameFieldValidation = (
|
||||
value,
|
||||
{
|
||||
collectionSlug,
|
||||
req: {
|
||||
payload: { config },
|
||||
t,
|
||||
},
|
||||
required,
|
||||
siblingData,
|
||||
},
|
||||
) => {
|
||||
let maxLength: number
|
||||
|
||||
if (collectionSlug) {
|
||||
const collection = config.collections.find(({ slug }) => slug === collectionSlug)
|
||||
|
||||
if (
|
||||
collection.auth.loginWithUsername &&
|
||||
!collection.auth.loginWithUsername?.requireUsername &&
|
||||
!collection.auth.loginWithUsername?.requireEmail
|
||||
) {
|
||||
if (!value && !siblingData?.email) {
|
||||
return t('validation:required')
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (typeof config?.defaultMaxTextLength === 'number') maxLength = config.defaultMaxTextLength
|
||||
|
||||
if (value && maxLength && value.length > maxLength) {
|
||||
@@ -173,7 +214,8 @@ export const username: Validate<string, unknown, unknown, TextField> = (
|
||||
return true
|
||||
}
|
||||
|
||||
export const textarea: Validate<string, unknown, unknown, TextareaField> = (
|
||||
export type TextareaFieldValidation = Validate<string, unknown, unknown, TextareaField>
|
||||
export const textarea: TextareaFieldValidation = (
|
||||
value,
|
||||
{
|
||||
maxLength: fieldMaxLength,
|
||||
@@ -204,10 +246,8 @@ export const textarea: Validate<string, unknown, unknown, TextareaField> = (
|
||||
return true
|
||||
}
|
||||
|
||||
export const code: Validate<string, unknown, unknown, CodeField> = (
|
||||
value,
|
||||
{ req: { t }, required },
|
||||
) => {
|
||||
export type CodeFieldValidation = Validate<string, unknown, unknown, CodeField>
|
||||
export const code: CodeFieldValidation = (value, { req: { t }, required }) => {
|
||||
if (required && value === undefined) {
|
||||
return t('validation:required')
|
||||
}
|
||||
@@ -215,7 +255,13 @@ export const code: Validate<string, unknown, unknown, CodeField> = (
|
||||
return true
|
||||
}
|
||||
|
||||
export const json: Validate<string, unknown, unknown, { jsonError?: string } & JSONField> = async (
|
||||
export type JSONFieldValidation = Validate<
|
||||
string,
|
||||
unknown,
|
||||
unknown,
|
||||
{ jsonError?: string } & JSONField
|
||||
>
|
||||
export const json: JSONFieldValidation = async (
|
||||
value,
|
||||
{ jsonError, jsonSchema, req: { t }, required },
|
||||
) => {
|
||||
@@ -281,10 +327,8 @@ export const json: Validate<string, unknown, unknown, { jsonError?: string } & J
|
||||
return true
|
||||
}
|
||||
|
||||
export const checkbox: Validate<boolean, unknown, unknown, CheckboxField> = (
|
||||
value,
|
||||
{ req: { t }, required },
|
||||
) => {
|
||||
export type CheckboxFieldValidation = Validate<boolean, unknown, unknown, CheckboxField>
|
||||
export const checkbox: CheckboxFieldValidation = (value, { req: { t }, required }) => {
|
||||
if ((value && typeof value !== 'boolean') || (required && typeof value !== 'boolean')) {
|
||||
return t('validation:trueOrFalse')
|
||||
}
|
||||
@@ -292,10 +336,8 @@ export const checkbox: Validate<boolean, unknown, unknown, CheckboxField> = (
|
||||
return true
|
||||
}
|
||||
|
||||
export const date: Validate<Date, unknown, unknown, DateField> = (
|
||||
value,
|
||||
{ req: { t }, required },
|
||||
) => {
|
||||
export type DateFieldValidation = Validate<Date, unknown, unknown, DateField>
|
||||
export const date: DateFieldValidation = (value, { req: { t }, required }) => {
|
||||
if (value && !isNaN(Date.parse(value.toString()))) {
|
||||
return true
|
||||
}
|
||||
@@ -311,10 +353,8 @@ export const date: Validate<Date, unknown, unknown, DateField> = (
|
||||
return true
|
||||
}
|
||||
|
||||
export const richText: Validate<object, unknown, unknown, RichTextField> = async (
|
||||
value,
|
||||
options,
|
||||
) => {
|
||||
export type RichTextFieldValidation = Validate<object, unknown, unknown, RichTextField>
|
||||
export const richText: RichTextFieldValidation = async (value, options) => {
|
||||
if (!options?.editor) {
|
||||
throw new Error('richText field has no editor property.')
|
||||
}
|
||||
@@ -357,7 +397,8 @@ const validateArrayLength = (
|
||||
return true
|
||||
}
|
||||
|
||||
export const number: Validate<number | number[], unknown, unknown, NumberField> = (
|
||||
export type NumberFieldValidation = Validate<number | number[], unknown, unknown, NumberField>
|
||||
export const number: NumberFieldValidation = (
|
||||
value,
|
||||
{ hasMany, max, maxRows, min, minRows, req: { t }, required },
|
||||
) => {
|
||||
@@ -391,17 +432,13 @@ export const number: Validate<number | number[], unknown, unknown, NumberField>
|
||||
return true
|
||||
}
|
||||
|
||||
export const array: Validate<unknown[], unknown, unknown, ArrayField> = (
|
||||
value,
|
||||
{ maxRows, minRows, req: { t }, required },
|
||||
) => {
|
||||
export type ArrayFieldValidation = Validate<unknown[], unknown, unknown, ArrayField>
|
||||
export const array: ArrayFieldValidation = (value, { maxRows, minRows, req: { t }, required }) => {
|
||||
return validateArrayLength(value, { maxRows, minRows, required, t })
|
||||
}
|
||||
|
||||
export const blocks: Validate<unknown, unknown, unknown, BlockField> = (
|
||||
value,
|
||||
{ maxRows, minRows, req: { t }, required },
|
||||
) => {
|
||||
export type BlockFieldValidation = Validate<unknown, unknown, unknown, BlockField>
|
||||
export const blocks: BlockFieldValidation = (value, { maxRows, minRows, req: { t }, required }) => {
|
||||
return validateArrayLength(value, { maxRows, minRows, required, t })
|
||||
}
|
||||
|
||||
@@ -533,10 +570,8 @@ const validateFilterOptions: Validate<
|
||||
return true
|
||||
}
|
||||
|
||||
export const upload: Validate<unknown, unknown, unknown, UploadField> = (
|
||||
value: string,
|
||||
options,
|
||||
) => {
|
||||
export type UploadFieldValidation = Validate<unknown, unknown, unknown, UploadField>
|
||||
export const upload: UploadFieldValidation = (value: string, options) => {
|
||||
if (!value && options.required) {
|
||||
return options?.req?.t('validation:required')
|
||||
}
|
||||
@@ -554,12 +589,13 @@ export const upload: Validate<unknown, unknown, unknown, UploadField> = (
|
||||
return validateFilterOptions(value, options)
|
||||
}
|
||||
|
||||
export const relationship: Validate<
|
||||
export type RelationshipFieldValidation = Validate<
|
||||
RelationshipValue,
|
||||
unknown,
|
||||
unknown,
|
||||
RelationshipField
|
||||
> = async (value, options) => {
|
||||
>
|
||||
export const relationship: RelationshipFieldValidation = async (value, options) => {
|
||||
const {
|
||||
maxRows,
|
||||
minRows,
|
||||
@@ -634,7 +670,8 @@ export const relationship: Validate<
|
||||
return validateFilterOptions(value, options)
|
||||
}
|
||||
|
||||
export const select: Validate<unknown, unknown, unknown, SelectField> = (
|
||||
export type SelectFieldValidation = Validate<string | string[], unknown, unknown, SelectField>
|
||||
export const select: SelectFieldValidation = (
|
||||
value,
|
||||
{ hasMany, options, req: { t }, required },
|
||||
) => {
|
||||
@@ -671,10 +708,8 @@ export const select: Validate<unknown, unknown, unknown, SelectField> = (
|
||||
return true
|
||||
}
|
||||
|
||||
export const radio: Validate<unknown, unknown, unknown, RadioField> = (
|
||||
value,
|
||||
{ options, req: { t }, required },
|
||||
) => {
|
||||
export type RadioFieldValidation = Validate<unknown, unknown, unknown, RadioField>
|
||||
export const radio: RadioFieldValidation = (value, { options, req: { t }, required }) => {
|
||||
if (value) {
|
||||
const valueMatchesOption = options.some(
|
||||
(option) => option === value || (typeof option !== 'string' && option.value === value),
|
||||
@@ -685,10 +720,13 @@ export const radio: Validate<unknown, unknown, unknown, RadioField> = (
|
||||
return required ? t('validation:required') : true
|
||||
}
|
||||
|
||||
export const point: Validate<[number | string, number | string], unknown, unknown, PointField> = (
|
||||
value = ['', ''],
|
||||
{ req: { t }, required },
|
||||
) => {
|
||||
export type PointFieldValidation = Validate<
|
||||
[number | string, number | string],
|
||||
unknown,
|
||||
unknown,
|
||||
PointField
|
||||
>
|
||||
export const point: PointFieldValidation = (value = ['', ''], { req: { t }, required }) => {
|
||||
const lng = parseFloat(String(value[0]))
|
||||
const lat = parseFloat(String(value[1]))
|
||||
if (
|
||||
|
||||
@@ -870,7 +870,6 @@ export type {
|
||||
Block,
|
||||
BlockField,
|
||||
CheckboxField,
|
||||
ClientValidate,
|
||||
CodeField,
|
||||
CollapsibleField,
|
||||
Condition,
|
||||
@@ -928,6 +927,27 @@ export { traverseFields as afterReadTraverseFields } from './fields/hooks/afterR
|
||||
export { traverseFields as beforeChangeTraverseFields } from './fields/hooks/beforeChange/traverseFields.js'
|
||||
export { traverseFields as beforeValidateTraverseFields } from './fields/hooks/beforeValidate/traverseFields.js'
|
||||
export { default as sortableFieldTypes } from './fields/sortableFieldTypes.js'
|
||||
export type {
|
||||
ArrayFieldValidation,
|
||||
BlockFieldValidation,
|
||||
CheckboxFieldValidation,
|
||||
CodeFieldValidation,
|
||||
ConfirmPasswordFieldValidation,
|
||||
DateFieldValidation,
|
||||
EmailFieldValidation,
|
||||
JSONFieldValidation,
|
||||
NumberFieldValidation,
|
||||
PasswordFieldValidation,
|
||||
PointFieldValidation,
|
||||
RadioFieldValidation,
|
||||
RelationshipFieldValidation,
|
||||
RichTextFieldValidation,
|
||||
SelectFieldValidation,
|
||||
TextFieldValidation,
|
||||
TextareaFieldValidation,
|
||||
UploadFieldValidation,
|
||||
UsernameFieldValidation,
|
||||
} from './fields/validations.js'
|
||||
export type { ClientGlobalConfig } from './globals/config/client.js'
|
||||
export { createClientGlobalConfig } from './globals/config/client.js'
|
||||
export type {
|
||||
@@ -999,7 +1019,8 @@ export { deleteCollectionVersions } from './versions/deleteCollectionVersions.js
|
||||
export { enforceMaxVersions } from './versions/enforceMaxVersions.js'
|
||||
export { getLatestCollectionVersion } from './versions/getLatestCollectionVersion.js'
|
||||
export { getLatestGlobalVersion } from './versions/getLatestGlobalVersion.js'
|
||||
export { saveVersion } from './versions/saveVersion.js'
|
||||
export { getDependencies }
|
||||
export { saveVersion } from './versions/saveVersion.js'
|
||||
export type { TypeWithVersion } from './versions/types.js'
|
||||
|
||||
export { deepMergeSimple } from '@payloadcms/translations/utilities'
|
||||
|
||||
@@ -619,24 +619,6 @@ const generateAuthFieldTypes = ({
|
||||
loginWithUsername: Auth['loginWithUsername']
|
||||
type: 'forgotOrUnlock' | 'login' | 'register'
|
||||
}): JSONSchema4 => {
|
||||
const emailAuthFields = {
|
||||
additionalProperties: false,
|
||||
properties: { email: fieldType },
|
||||
required: ['email'],
|
||||
}
|
||||
const usernameAuthFields = {
|
||||
additionalProperties: false,
|
||||
properties: { username: fieldType },
|
||||
required: ['username'],
|
||||
}
|
||||
|
||||
if (['login', 'register'].includes(type)) {
|
||||
emailAuthFields.properties['password'] = fieldType
|
||||
emailAuthFields.required.push('password')
|
||||
usernameAuthFields.properties['password'] = fieldType
|
||||
usernameAuthFields.required.push('password')
|
||||
}
|
||||
|
||||
if (loginWithUsername) {
|
||||
switch (type) {
|
||||
case 'login': {
|
||||
@@ -644,38 +626,57 @@ const generateAuthFieldTypes = ({
|
||||
// allow username or email and require password for login
|
||||
return {
|
||||
additionalProperties: false,
|
||||
oneOf: [emailAuthFields, usernameAuthFields],
|
||||
oneOf: [
|
||||
{
|
||||
additionalProperties: false,
|
||||
properties: { email: fieldType, password: fieldType },
|
||||
required: ['email', 'password'],
|
||||
},
|
||||
{
|
||||
additionalProperties: false,
|
||||
properties: { password: fieldType, username: fieldType },
|
||||
required: ['username', 'password'],
|
||||
},
|
||||
],
|
||||
}
|
||||
} else {
|
||||
// allow only username and password for login
|
||||
return usernameAuthFields
|
||||
return {
|
||||
additionalProperties: false,
|
||||
properties: {
|
||||
password: fieldType,
|
||||
username: fieldType,
|
||||
},
|
||||
required: ['username', 'password'],
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
case 'register': {
|
||||
const requiredFields: ('email' | 'password' | 'username')[] = ['password']
|
||||
const properties: {
|
||||
email?: JSONSchema4['properties']
|
||||
password?: JSONSchema4['properties']
|
||||
username?: JSONSchema4['properties']
|
||||
} = {
|
||||
password: fieldType,
|
||||
username: fieldType,
|
||||
}
|
||||
|
||||
if (loginWithUsername.requireEmail) {
|
||||
// require username, email and password for registration
|
||||
requiredFields.push('email')
|
||||
}
|
||||
if (loginWithUsername.requireUsername) {
|
||||
requiredFields.push('username')
|
||||
}
|
||||
if (loginWithUsername.requireEmail || loginWithUsername.allowEmailLogin) {
|
||||
properties.email = fieldType
|
||||
}
|
||||
|
||||
return {
|
||||
additionalProperties: false,
|
||||
properties: {
|
||||
...usernameAuthFields.properties,
|
||||
...emailAuthFields.properties,
|
||||
},
|
||||
required: [...usernameAuthFields.required, ...emailAuthFields.required],
|
||||
}
|
||||
} else if (loginWithUsername.allowEmailLogin) {
|
||||
// allow both but only require username for registration
|
||||
return {
|
||||
additionalProperties: false,
|
||||
properties: {
|
||||
...usernameAuthFields.properties,
|
||||
...emailAuthFields.properties,
|
||||
},
|
||||
required: usernameAuthFields.required,
|
||||
}
|
||||
} else {
|
||||
// require only username and password for registration
|
||||
return usernameAuthFields
|
||||
properties,
|
||||
required: requiredFields,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -684,18 +685,37 @@ const generateAuthFieldTypes = ({
|
||||
// allow email or username for unlock/forgot-password
|
||||
return {
|
||||
additionalProperties: false,
|
||||
oneOf: [emailAuthFields, usernameAuthFields],
|
||||
oneOf: [
|
||||
{
|
||||
additionalProperties: false,
|
||||
properties: { email: fieldType },
|
||||
required: ['email'],
|
||||
},
|
||||
{
|
||||
additionalProperties: false,
|
||||
properties: { username: fieldType },
|
||||
required: ['username'],
|
||||
},
|
||||
],
|
||||
}
|
||||
} else {
|
||||
// allow only username for unlock/forgot-password
|
||||
return usernameAuthFields
|
||||
return {
|
||||
additionalProperties: false,
|
||||
properties: { username: fieldType },
|
||||
required: ['username'],
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// default email (and password for login/register)
|
||||
return emailAuthFields
|
||||
return {
|
||||
additionalProperties: false,
|
||||
properties: { email: fieldType, password: fieldType },
|
||||
required: ['email', 'password'],
|
||||
}
|
||||
}
|
||||
|
||||
export function authCollectionToOperationsJSONSchema(
|
||||
|
||||
@@ -13,7 +13,7 @@ export const blockValidationHOC = (
|
||||
const blockFieldData = node.fields ?? ({} as BlockFields)
|
||||
|
||||
const {
|
||||
options: { id, operation, preferences, req },
|
||||
options: { id, collectionSlug, operation, preferences, req },
|
||||
} = validation
|
||||
|
||||
// find block
|
||||
@@ -30,6 +30,7 @@ export const blockValidationHOC = (
|
||||
|
||||
const result = await buildStateFromSchema({
|
||||
id,
|
||||
collectionSlug,
|
||||
data: blockFieldData,
|
||||
fieldSchema: block.fields,
|
||||
operation: operation === 'create' || operation === 'update' ? operation : 'update',
|
||||
|
||||
@@ -13,7 +13,7 @@ export const linkValidation = (
|
||||
return async ({
|
||||
node,
|
||||
validation: {
|
||||
options: { id, operation, preferences, req },
|
||||
options: { id, collectionSlug, operation, preferences, req },
|
||||
},
|
||||
}) => {
|
||||
/**
|
||||
@@ -22,6 +22,7 @@ export const linkValidation = (
|
||||
|
||||
const result = await buildStateFromSchema({
|
||||
id,
|
||||
collectionSlug,
|
||||
data: node.fields,
|
||||
fieldSchema: sanitizedFieldsWithoutText, // Sanitized in feature.server.ts
|
||||
operation: operation === 'create' || operation === 'update' ? operation : 'update',
|
||||
|
||||
@@ -44,6 +44,7 @@ export const uploadValidation = (
|
||||
|
||||
const result = await buildStateFromSchema({
|
||||
id,
|
||||
collectionSlug: node.relationTo,
|
||||
data: node?.fields ?? {},
|
||||
fieldSchema: collection.fields,
|
||||
operation: operation === 'create' || operation === 'update' ? operation : 'update',
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
'use client'
|
||||
|
||||
import type { ClientValidate, FormFieldBase } from 'payload'
|
||||
import type { FormFieldBase, PayloadRequest, RichTextFieldValidation } from 'payload'
|
||||
import type { BaseEditor, BaseOperation } from 'slate'
|
||||
import type { HistoryEditor } from 'slate-history'
|
||||
import type { ReactEditor } from 'slate-react'
|
||||
@@ -56,6 +56,7 @@ const RichTextField: React.FC<
|
||||
placeholder?: string
|
||||
plugins: RichTextPlugin[]
|
||||
richTextComponentMap: Map<string, React.ReactNode>
|
||||
validate?: RichTextFieldValidation
|
||||
width?: string
|
||||
} & FormFieldBase
|
||||
> = (props) => {
|
||||
@@ -88,14 +89,14 @@ const RichTextField: React.FC<
|
||||
const drawerDepth = useEditDepth()
|
||||
const drawerIsOpen = drawerDepth > 1
|
||||
|
||||
const memoizedValidate: ClientValidate = useCallback(
|
||||
const memoizedValidate = useCallback(
|
||||
(value, validationOptions) => {
|
||||
if (typeof validate === 'function') {
|
||||
return validate(value, {
|
||||
...validationOptions,
|
||||
req: {
|
||||
t: i18n.t,
|
||||
},
|
||||
} as PayloadRequest,
|
||||
required,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
'use client'
|
||||
import type { CheckboxFieldProps, ClientValidate } from 'payload'
|
||||
import type { CheckboxFieldProps, CheckboxFieldValidation } from 'payload'
|
||||
|
||||
import React, { useCallback } from 'react'
|
||||
|
||||
@@ -51,7 +51,7 @@ const CheckboxFieldComponent: React.FC<CheckboxFieldProps> = (props) => {
|
||||
|
||||
const editDepth = useEditDepth()
|
||||
|
||||
const memoizedValidate: ClientValidate = useCallback(
|
||||
const memoizedValidate: CheckboxFieldValidation = useCallback(
|
||||
(value, options) => {
|
||||
if (typeof validate === 'function') {
|
||||
return validate(value, { ...options, required })
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
'use client'
|
||||
import type { FormField } from 'payload'
|
||||
|
||||
import React, { useCallback } from 'react'
|
||||
import { confirmPassword } from 'payload/shared'
|
||||
import React from 'react'
|
||||
|
||||
import { useFormFields } from '../../forms/Form/context.js'
|
||||
import { useField } from '../../forms/useField/index.js'
|
||||
import { useTranslation } from '../../providers/Translation/index.js'
|
||||
import { FieldError } from '../FieldError/index.js'
|
||||
@@ -18,28 +17,18 @@ export type ConfirmPasswordFieldProps = {
|
||||
|
||||
export const ConfirmPasswordField: React.FC<ConfirmPasswordFieldProps> = (props) => {
|
||||
const { disabled, path = 'confirm-password' } = props
|
||||
|
||||
const password = useFormFields<FormField>(([fields]) => fields?.password)
|
||||
const { t } = useTranslation()
|
||||
|
||||
const validate = useCallback(
|
||||
(value: string) => {
|
||||
if (!value) {
|
||||
return t('validation:required')
|
||||
}
|
||||
|
||||
if (value === password?.value) {
|
||||
return true
|
||||
}
|
||||
|
||||
return t('fields:passwordsDoNotMatch')
|
||||
},
|
||||
[password, t],
|
||||
)
|
||||
|
||||
const { setValue, showError, value } = useField({
|
||||
path,
|
||||
validate,
|
||||
validate: (value, options) => {
|
||||
return confirmPassword(value, {
|
||||
name: 'confirm-password',
|
||||
type: 'text',
|
||||
required: true,
|
||||
...options,
|
||||
})
|
||||
},
|
||||
})
|
||||
|
||||
return (
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
'use client'
|
||||
import type { ClientValidate, DateField, DateFieldProps } from 'payload'
|
||||
import type { DateFieldProps, DateFieldValidation } from 'payload'
|
||||
|
||||
import { getTranslation } from '@payloadcms/translations'
|
||||
import React, { useCallback } from 'react'
|
||||
@@ -43,7 +43,7 @@ const DateTimeFieldComponent: React.FC<DateFieldProps> = (props) => {
|
||||
|
||||
const { i18n } = useTranslation()
|
||||
|
||||
const memoizedValidate: ClientValidate = useCallback(
|
||||
const memoizedValidate: DateFieldValidation = useCallback(
|
||||
(value, options) => {
|
||||
if (typeof validate === 'function') {
|
||||
return validate(value, { ...options, required })
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
'use client'
|
||||
import type { ClientValidate, EmailFieldProps } from 'payload'
|
||||
import type { EmailFieldProps, EmailFieldValidation } from 'payload'
|
||||
|
||||
import { getTranslation } from '@payloadcms/translations'
|
||||
import React, { useCallback } from 'react'
|
||||
@@ -39,7 +39,7 @@ const EmailFieldComponent: React.FC<EmailFieldProps> = (props) => {
|
||||
|
||||
const { i18n } = useTranslation()
|
||||
|
||||
const memoizedValidate: ClientValidate = useCallback(
|
||||
const memoizedValidate: EmailFieldValidation = useCallback(
|
||||
(value, options) => {
|
||||
if (typeof validate === 'function') {
|
||||
return validate(value, { ...options, required })
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
'use client'
|
||||
import type { ClientValidate, JSONFieldProps } from 'payload'
|
||||
import type { JSONFieldProps } from 'payload'
|
||||
|
||||
import React, { useCallback, useEffect, useState } from 'react'
|
||||
|
||||
@@ -43,7 +43,7 @@ const JSONFieldComponent: React.FC<JSONFieldProps> = (props) => {
|
||||
const [jsonError, setJsonError] = useState<string>()
|
||||
const [hasLoadedValue, setHasLoadedValue] = useState(false)
|
||||
|
||||
const memoizedValidate: ClientValidate = useCallback(
|
||||
const memoizedValidate = useCallback(
|
||||
(value, options) => {
|
||||
if (typeof validate === 'function')
|
||||
return validate(value, { ...options, jsonError, required })
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
'use client'
|
||||
import type { ClientValidate, Description, PayloadRequest, Validate } from 'payload'
|
||||
import type { Description, PasswordFieldValidation, PayloadRequest, Validate } from 'payload'
|
||||
|
||||
import { useConfig, useLocale, useTranslation } from '@payloadcms/ui'
|
||||
import { password } from 'payload/shared'
|
||||
@@ -31,7 +31,7 @@ export type PasswordFieldProps = {
|
||||
required?: boolean
|
||||
rtl?: boolean
|
||||
style?: React.CSSProperties
|
||||
validate?: Validate
|
||||
validate?: PasswordFieldValidation
|
||||
width?: string
|
||||
}
|
||||
|
||||
@@ -63,7 +63,7 @@ const PasswordFieldComponent: React.FC<PasswordFieldProps> = (props) => {
|
||||
const locale = useLocale()
|
||||
const config = useConfig()
|
||||
|
||||
const memoizedValidate: ClientValidate = useCallback(
|
||||
const memoizedValidate: PasswordFieldValidation = useCallback(
|
||||
(value, options) => {
|
||||
if (typeof validate === 'function') {
|
||||
return validate(value, { ...options, required })
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
'use client'
|
||||
import type { ClientValidate, PointFieldProps } from 'payload'
|
||||
import type { PointFieldProps, PointFieldValidation } from 'payload'
|
||||
|
||||
import { getTranslation } from '@payloadcms/translations'
|
||||
import React, { useCallback } from 'react'
|
||||
@@ -42,7 +42,7 @@ export const PointFieldComponent: React.FC<PointFieldProps> = (props) => {
|
||||
|
||||
const { i18n, t } = useTranslation()
|
||||
|
||||
const memoizedValidate: ClientValidate = useCallback(
|
||||
const memoizedValidate: PointFieldValidation = useCallback(
|
||||
(value, options) => {
|
||||
if (typeof validate === 'function') {
|
||||
return validate(value, { ...options, required })
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
'use client'
|
||||
import type { ClientValidate, Option, OptionObject, SelectFieldProps } from 'payload'
|
||||
import type { Option, OptionObject, SelectFieldProps } from 'payload'
|
||||
|
||||
import React, { useCallback } from 'react'
|
||||
|
||||
@@ -51,7 +51,7 @@ const SelectFieldComponent: React.FC<SelectFieldProps> = (props) => {
|
||||
|
||||
const options = React.useMemo(() => formatOptions(optionsFromProps), [optionsFromProps])
|
||||
|
||||
const memoizedValidate: ClientValidate = useCallback(
|
||||
const memoizedValidate = useCallback(
|
||||
(value, validationOptions) => {
|
||||
if (typeof validate === 'function')
|
||||
return validate(value, { ...validationOptions, hasMany, options, required })
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
'use client'
|
||||
import type { ClientValidate, TextFieldProps } from 'payload'
|
||||
import type { TextFieldProps } from 'payload'
|
||||
|
||||
import React, { useCallback, useEffect, useState } from 'react'
|
||||
|
||||
@@ -51,7 +51,7 @@ const TextFieldComponent: React.FC<TextFieldProps> = (props) => {
|
||||
|
||||
const { localization: localizationConfig } = useConfig()
|
||||
|
||||
const memoizedValidate: ClientValidate = useCallback(
|
||||
const memoizedValidate = useCallback(
|
||||
(value, options) => {
|
||||
if (typeof validate === 'function')
|
||||
return validate(value, { ...options, maxLength, minLength, required })
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
'use client'
|
||||
import type { ClientValidate, TextareaFieldProps } from 'payload'
|
||||
import type { TextareaFieldProps, TextareaFieldValidation } from 'payload'
|
||||
|
||||
import { getTranslation } from '@payloadcms/translations'
|
||||
import React, { useCallback } from 'react'
|
||||
@@ -56,7 +56,7 @@ const TextareaFieldComponent: React.FC<TextareaFieldProps> = (props) => {
|
||||
localizationConfig: localization || undefined,
|
||||
})
|
||||
|
||||
const memoizedValidate: ClientValidate = useCallback(
|
||||
const memoizedValidate: TextareaFieldValidation = useCallback(
|
||||
(value, options) => {
|
||||
if (typeof validate === 'function')
|
||||
return validate(value, { ...options, maxLength, minLength, required })
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
'use client'
|
||||
import type { FormState } from 'payload'
|
||||
import type { FormState, PayloadRequest } from 'payload'
|
||||
|
||||
import { dequal } from 'dequal/lite' // lite: no need for Map and Set support
|
||||
import { useRouter } from 'next/navigation.js'
|
||||
@@ -126,13 +126,20 @@ export const Form: React.FC<FormProps> = (props) => {
|
||||
}
|
||||
|
||||
validationResult = await field.validate(valueToValidate, {
|
||||
...field,
|
||||
id,
|
||||
config,
|
||||
collectionSlug,
|
||||
data,
|
||||
operation,
|
||||
siblingData: contextRef.current.getSiblingData(path),
|
||||
preferences: {} as any,
|
||||
req: {
|
||||
payload: {
|
||||
config,
|
||||
},
|
||||
t,
|
||||
user,
|
||||
} as PayloadRequest,
|
||||
siblingData: contextRef.current.getSiblingData(path),
|
||||
})
|
||||
|
||||
if (typeof validationResult === 'string') {
|
||||
@@ -160,7 +167,7 @@ export const Form: React.FC<FormProps> = (props) => {
|
||||
}
|
||||
|
||||
return isValid
|
||||
}, [id, user, operation, t, dispatchFields, config])
|
||||
}, [collectionSlug, config, dispatchFields, id, operation, t, user])
|
||||
|
||||
const submit = useCallback(
|
||||
async (options: SubmitOptions = {}, e): Promise<void> => {
|
||||
@@ -621,11 +628,11 @@ export const Form: React.FC<FormProps> = (props) => {
|
||||
|
||||
return (
|
||||
<form
|
||||
action={action}
|
||||
action={typeof action === 'function' ? void action : action}
|
||||
className={classes}
|
||||
method={method}
|
||||
noValidate
|
||||
onSubmit={(e) => contextRef.current.submit({}, e)}
|
||||
onSubmit={(e) => void contextRef.current.submit({}, e)}
|
||||
ref={formRef}
|
||||
>
|
||||
<FormContext.Provider value={contextRef.current}>
|
||||
|
||||
@@ -23,6 +23,7 @@ export type AddFieldStatePromiseArgs = {
|
||||
* if all parents are localized, then the field is localized
|
||||
*/
|
||||
anyParentLocalized?: boolean
|
||||
collectionSlug?: string
|
||||
data: Data
|
||||
field: Field
|
||||
fieldIndex: number
|
||||
@@ -47,8 +48,8 @@ export type AddFieldStatePromiseArgs = {
|
||||
operation: 'create' | 'update'
|
||||
passesCondition: boolean
|
||||
path: string
|
||||
preferences: DocumentPreferences
|
||||
|
||||
preferences: DocumentPreferences
|
||||
/**
|
||||
* Req is used for validation and defaultValue calculation. If you don't need validation,
|
||||
* just create your own req and pass in the locale and the user
|
||||
@@ -74,6 +75,7 @@ export const addFieldStatePromise = async (args: AddFieldStatePromiseArgs): Prom
|
||||
id,
|
||||
addErrorPathToParent: addErrorPathToParentArg,
|
||||
anyParentLocalized = false,
|
||||
collectionSlug,
|
||||
data,
|
||||
field,
|
||||
fieldIndex,
|
||||
@@ -120,6 +122,7 @@ export const addFieldStatePromise = async (args: AddFieldStatePromiseArgs): Prom
|
||||
validationResult = await validate(data?.[field.name], {
|
||||
...field,
|
||||
id,
|
||||
collectionSlug,
|
||||
data: fullData,
|
||||
operation,
|
||||
req,
|
||||
|
||||
@@ -10,6 +10,7 @@ import { calculateDefaultValues } from './calculateDefaultValues/index.js'
|
||||
import { iterateFields } from './iterateFields.js'
|
||||
|
||||
type Args = {
|
||||
collectionSlug?: string
|
||||
data?: Data
|
||||
fieldSchema: FieldSchema[] | undefined
|
||||
id?: number | string
|
||||
@@ -32,7 +33,7 @@ export type BuildFormStateArgs = {
|
||||
}
|
||||
|
||||
export const buildStateFromSchema = async (args: Args): Promise<FormState> => {
|
||||
const { id, data: fullData = {}, fieldSchema, operation, preferences, req } = args
|
||||
const { id, collectionSlug, data: fullData = {}, fieldSchema, operation, preferences, req } = args
|
||||
|
||||
if (fieldSchema) {
|
||||
const state: FormState = {}
|
||||
@@ -49,6 +50,7 @@ export const buildStateFromSchema = async (args: Args): Promise<FormState> => {
|
||||
await iterateFields({
|
||||
id,
|
||||
addErrorPathToParent: null,
|
||||
collectionSlug,
|
||||
data: dataWithDefaultValues,
|
||||
fields: fieldSchema,
|
||||
fullData,
|
||||
|
||||
@@ -18,6 +18,7 @@ type Args = {
|
||||
* if any parents is localized, then the field is localized. @default false
|
||||
*/
|
||||
anyParentLocalized?: boolean
|
||||
collectionSlug?: string
|
||||
data: Data
|
||||
fields: FieldSchema[]
|
||||
filter?: (args: AddFieldStatePromiseArgs) => boolean
|
||||
@@ -64,6 +65,7 @@ export const iterateFields = async ({
|
||||
id,
|
||||
addErrorPathToParent: addErrorPathToParentArg,
|
||||
anyParentLocalized = false,
|
||||
collectionSlug,
|
||||
data,
|
||||
fields,
|
||||
filter,
|
||||
@@ -98,6 +100,7 @@ export const iterateFields = async ({
|
||||
id,
|
||||
addErrorPathToParent: addErrorPathToParentArg,
|
||||
anyParentLocalized,
|
||||
collectionSlug,
|
||||
data,
|
||||
field,
|
||||
fieldIndex,
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
'use client'
|
||||
import type { PayloadRequest } from 'payload'
|
||||
|
||||
import { useCallback, useMemo, useRef } from 'react'
|
||||
|
||||
import type { UPDATE } from '../Form/types.js'
|
||||
@@ -39,7 +41,7 @@ export const useField = <T,>(options: Options): FieldType<T> => {
|
||||
const processing = useFormProcessing()
|
||||
const initializing = useFormInitializing()
|
||||
const { user } = useAuth()
|
||||
const { id } = useDocumentInfo()
|
||||
const { id, collectionSlug } = useDocumentInfo()
|
||||
const operation = useOperation()
|
||||
|
||||
const { dispatchField, field } = useFormFields(([fields, dispatch]) => ({
|
||||
@@ -161,12 +163,18 @@ export const useField = <T,>(options: Options): FieldType<T> => {
|
||||
typeof validate === 'function'
|
||||
? await validate(valueToValidate, {
|
||||
id,
|
||||
config,
|
||||
collectionSlug,
|
||||
data: getData(),
|
||||
operation,
|
||||
siblingData: getSiblingData(path),
|
||||
preferences: {} as any,
|
||||
req: {
|
||||
payload: {
|
||||
config,
|
||||
},
|
||||
t,
|
||||
user,
|
||||
} as PayloadRequest,
|
||||
siblingData: getSiblingData(path),
|
||||
})
|
||||
: true
|
||||
|
||||
@@ -190,7 +198,7 @@ export const useField = <T,>(options: Options): FieldType<T> => {
|
||||
path,
|
||||
rows: field?.rows,
|
||||
valid,
|
||||
// validate,
|
||||
validate,
|
||||
value,
|
||||
}
|
||||
|
||||
@@ -220,7 +228,7 @@ export const useField = <T,>(options: Options): FieldType<T> => {
|
||||
user,
|
||||
validate,
|
||||
field?.rows,
|
||||
field?.valid,
|
||||
collectionSlug,
|
||||
],
|
||||
)
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { ClientValidate, FieldPermissions, FilterOptionsResult, Row } from 'payload'
|
||||
import type { FieldPermissions, FilterOptionsResult, Row, Validate } from 'payload'
|
||||
|
||||
export type Options = {
|
||||
disableFormData?: boolean
|
||||
@@ -7,7 +7,7 @@ export type Options = {
|
||||
* If you do not provide a `path` or a `name`, this hook will look for one using the `useFieldPath` hook.
|
||||
**/
|
||||
path?: string
|
||||
validate?: ClientValidate
|
||||
validate?: Validate
|
||||
}
|
||||
|
||||
export type FieldType<T> = {
|
||||
|
||||
@@ -176,6 +176,7 @@ export const buildFormState = async ({ req }: { req: PayloadRequest }): Promise<
|
||||
|
||||
const result = await buildStateFromSchema({
|
||||
id,
|
||||
collectionSlug,
|
||||
data,
|
||||
fieldSchema,
|
||||
operation,
|
||||
@@ -188,14 +189,6 @@ export const buildFormState = async ({ req }: { req: PayloadRequest }): Promise<
|
||||
if (req.payload.collections[collectionSlug]?.config?.upload && formState.file) {
|
||||
result.file = formState.file
|
||||
}
|
||||
|
||||
if (
|
||||
req.payload.collections[collectionSlug]?.config?.auth &&
|
||||
!req.payload.collections[collectionSlug].config.auth.disableLocalStrategy
|
||||
) {
|
||||
if (formState.username) result.username = formState.username
|
||||
if (formState.email) result.email = formState.email
|
||||
}
|
||||
}
|
||||
|
||||
return result
|
||||
|
||||
@@ -15,7 +15,9 @@ export const AuthDebug: React.FC<UIField> = () => {
|
||||
setState(userRes)
|
||||
}
|
||||
|
||||
if (user?.id) {
|
||||
void fetchUser()
|
||||
}
|
||||
}, [user])
|
||||
|
||||
return (
|
||||
|
||||
@@ -56,7 +56,6 @@ const createFirstUser = async ({
|
||||
// forget to fill out confirm password
|
||||
await page.locator('#field-email').fill(devUser.email)
|
||||
await page.locator('#field-password').fill(devUser.password)
|
||||
await wait(500)
|
||||
await page.locator('.form-submit > button').click()
|
||||
await expect(page.locator('.field-type.confirm-password .field-error')).toHaveText(
|
||||
'This field is required.',
|
||||
@@ -66,7 +65,6 @@ const createFirstUser = async ({
|
||||
await page.locator('#field-email').fill(devUser.email)
|
||||
await page.locator('#field-password').fill('12')
|
||||
await page.locator('#field-confirm-password').fill('12')
|
||||
await wait(500)
|
||||
await page.locator('.form-submit > button').click()
|
||||
await expect(page.locator('.field-type.password .field-error')).toHaveText(
|
||||
'This value must be longer than the minimum length of 3 characters.',
|
||||
@@ -76,7 +74,6 @@ const createFirstUser = async ({
|
||||
await page.locator('#field-password').fill(devUser.password)
|
||||
await page.locator('#field-confirm-password').fill(devUser.password)
|
||||
await page.locator('#field-custom').fill('Hello, world!')
|
||||
await wait(500)
|
||||
await page.locator('.form-submit > button').click()
|
||||
|
||||
await expect
|
||||
|
||||
38
test/login-with-username/config.ts
Normal file
38
test/login-with-username/config.ts
Normal file
@@ -0,0 +1,38 @@
|
||||
import path from 'path'
|
||||
import { fileURLToPath } from 'url'
|
||||
|
||||
import { buildConfigWithDefaults } from '../buildConfigWithDefaults.js'
|
||||
|
||||
const filename = fileURLToPath(import.meta.url)
|
||||
const dirname = path.dirname(filename)
|
||||
|
||||
export const LoginWithUsernameConfig = buildConfigWithDefaults({
|
||||
typescript: {
|
||||
outputFile: path.resolve(dirname, 'payload-types.ts'),
|
||||
},
|
||||
collections: [
|
||||
{
|
||||
slug: 'users',
|
||||
auth: {
|
||||
loginWithUsername: {
|
||||
requireEmail: false,
|
||||
allowEmailLogin: false,
|
||||
},
|
||||
},
|
||||
fields: [],
|
||||
},
|
||||
{
|
||||
slug: 'login-with-either',
|
||||
auth: {
|
||||
loginWithUsername: {
|
||||
requireEmail: false,
|
||||
allowEmailLogin: true,
|
||||
requireUsername: false,
|
||||
},
|
||||
},
|
||||
fields: [],
|
||||
},
|
||||
],
|
||||
})
|
||||
|
||||
export default LoginWithUsernameConfig
|
||||
118
test/login-with-username/int.spec.ts
Normal file
118
test/login-with-username/int.spec.ts
Normal file
@@ -0,0 +1,118 @@
|
||||
import type { Payload } from 'payload'
|
||||
|
||||
import { devUser } from '../credentials.js'
|
||||
import { initPayloadInt } from '../helpers/initPayloadInt.js'
|
||||
import configPromise from './config.js'
|
||||
|
||||
let payload: Payload
|
||||
|
||||
describe('Login With Username Feature', () => {
|
||||
beforeAll(async () => {
|
||||
;({ payload } = await initPayloadInt(configPromise))
|
||||
})
|
||||
|
||||
afterAll(async () => {
|
||||
if (typeof payload.db.destroy === 'function') {
|
||||
await payload.db.destroy()
|
||||
}
|
||||
})
|
||||
|
||||
describe('hook execution', () => {
|
||||
it('should not allow creation with neither email nor username', async () => {
|
||||
let errors = []
|
||||
try {
|
||||
await payload.create({
|
||||
collection: 'login-with-either',
|
||||
data: {
|
||||
email: null,
|
||||
username: null,
|
||||
},
|
||||
})
|
||||
} catch (error) {
|
||||
errors = error.data.errors
|
||||
}
|
||||
expect(errors).toHaveLength(2)
|
||||
})
|
||||
})
|
||||
|
||||
it('should not allow removing both username and email fields', async () => {
|
||||
const emailToUse = 'example@email.com'
|
||||
const usernameToUse = 'exampleUser'
|
||||
|
||||
const exampleUser = await payload.create({
|
||||
collection: 'login-with-either',
|
||||
data: {
|
||||
email: emailToUse,
|
||||
username: usernameToUse,
|
||||
password: 'test',
|
||||
},
|
||||
})
|
||||
|
||||
let errors = []
|
||||
try {
|
||||
await payload.update({
|
||||
collection: 'login-with-either',
|
||||
id: exampleUser.id,
|
||||
data: {
|
||||
email: null,
|
||||
username: null,
|
||||
},
|
||||
})
|
||||
} catch (error) {
|
||||
errors = error.data.errors
|
||||
}
|
||||
expect(errors).toHaveLength(2)
|
||||
|
||||
errors = []
|
||||
await payload.update({
|
||||
collection: 'login-with-either',
|
||||
id: exampleUser.id,
|
||||
data: {
|
||||
username: null,
|
||||
},
|
||||
})
|
||||
expect(errors).toHaveLength(0)
|
||||
|
||||
try {
|
||||
await payload.update({
|
||||
collection: 'login-with-either',
|
||||
id: exampleUser.id,
|
||||
data: {
|
||||
email: null,
|
||||
},
|
||||
})
|
||||
} catch (error) {
|
||||
errors = error.data.errors
|
||||
}
|
||||
expect(errors).toHaveLength(2)
|
||||
})
|
||||
|
||||
it('should allow login with either username or email', async () => {
|
||||
await payload.create({
|
||||
collection: 'login-with-either',
|
||||
data: {
|
||||
email: devUser.email,
|
||||
username: 'dev',
|
||||
password: devUser.password,
|
||||
},
|
||||
})
|
||||
|
||||
const loginWithEmail = await payload.login({
|
||||
collection: 'login-with-either',
|
||||
data: {
|
||||
email: devUser.email,
|
||||
password: devUser.password,
|
||||
},
|
||||
})
|
||||
expect(loginWithEmail).toHaveProperty('token')
|
||||
|
||||
const loginWithUsername = await payload.login({
|
||||
collection: 'login-with-either',
|
||||
data: {
|
||||
username: 'dev',
|
||||
password: devUser.password,
|
||||
},
|
||||
})
|
||||
expect(loginWithUsername).toHaveProperty('token')
|
||||
})
|
||||
})
|
||||
166
test/login-with-username/payload-types.ts
Normal file
166
test/login-with-username/payload-types.ts
Normal file
@@ -0,0 +1,166 @@
|
||||
/* tslint:disable */
|
||||
/* eslint-disable */
|
||||
/**
|
||||
* This file was automatically generated by Payload.
|
||||
* DO NOT MODIFY IT BY HAND. Instead, modify your source Payload config,
|
||||
* and re-run `payload generate:types` to regenerate this file.
|
||||
*/
|
||||
|
||||
export interface Config {
|
||||
auth: {
|
||||
users: UserAuthOperations;
|
||||
'login-with-either': LoginWithEitherAuthOperations;
|
||||
};
|
||||
collections: {
|
||||
users: User;
|
||||
'login-with-either': LoginWithEither;
|
||||
'payload-preferences': PayloadPreference;
|
||||
'payload-migrations': PayloadMigration;
|
||||
};
|
||||
db: {
|
||||
defaultIDType: string;
|
||||
};
|
||||
globals: {};
|
||||
locale: null;
|
||||
user:
|
||||
| (User & {
|
||||
collection: 'users';
|
||||
})
|
||||
| (LoginWithEither & {
|
||||
collection: 'login-with-either';
|
||||
});
|
||||
}
|
||||
export interface UserAuthOperations {
|
||||
forgotPassword: {
|
||||
username: string;
|
||||
};
|
||||
login: {
|
||||
password: string;
|
||||
username: string;
|
||||
};
|
||||
registerFirstUser: {
|
||||
password: string;
|
||||
username: string;
|
||||
};
|
||||
unlock: {
|
||||
username: string;
|
||||
};
|
||||
}
|
||||
export interface LoginWithEitherAuthOperations {
|
||||
forgotPassword:
|
||||
| {
|
||||
email: string;
|
||||
}
|
||||
| {
|
||||
username: string;
|
||||
};
|
||||
login:
|
||||
| {
|
||||
email: string;
|
||||
password: string;
|
||||
}
|
||||
| {
|
||||
password: string;
|
||||
username: string;
|
||||
};
|
||||
registerFirstUser: {
|
||||
password: string;
|
||||
username?: string;
|
||||
email?: string;
|
||||
};
|
||||
unlock:
|
||||
| {
|
||||
email: string;
|
||||
}
|
||||
| {
|
||||
username: string;
|
||||
};
|
||||
}
|
||||
/**
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
* via the `definition` "users".
|
||||
*/
|
||||
export interface User {
|
||||
id: string;
|
||||
updatedAt: string;
|
||||
createdAt: string;
|
||||
email?: string | null;
|
||||
username: string;
|
||||
resetPasswordToken?: string | null;
|
||||
resetPasswordExpiration?: string | null;
|
||||
salt?: string | null;
|
||||
hash?: string | null;
|
||||
loginAttempts?: number | null;
|
||||
lockUntil?: string | null;
|
||||
password?: string | null;
|
||||
}
|
||||
/**
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
* via the `definition` "login-with-either".
|
||||
*/
|
||||
export interface LoginWithEither {
|
||||
id: string;
|
||||
updatedAt: string;
|
||||
createdAt: string;
|
||||
email?: string | null;
|
||||
username?: string | null;
|
||||
resetPasswordToken?: string | null;
|
||||
resetPasswordExpiration?: string | null;
|
||||
salt?: string | null;
|
||||
hash?: string | null;
|
||||
loginAttempts?: number | null;
|
||||
lockUntil?: string | null;
|
||||
password?: string | null;
|
||||
}
|
||||
/**
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
* via the `definition` "payload-preferences".
|
||||
*/
|
||||
export interface PayloadPreference {
|
||||
id: string;
|
||||
user:
|
||||
| {
|
||||
relationTo: 'users';
|
||||
value: string | User;
|
||||
}
|
||||
| {
|
||||
relationTo: 'login-with-either';
|
||||
value: string | LoginWithEither;
|
||||
};
|
||||
key?: string | null;
|
||||
value?:
|
||||
| {
|
||||
[k: string]: unknown;
|
||||
}
|
||||
| unknown[]
|
||||
| string
|
||||
| number
|
||||
| boolean
|
||||
| null;
|
||||
updatedAt: string;
|
||||
createdAt: string;
|
||||
}
|
||||
/**
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
* via the `definition` "payload-migrations".
|
||||
*/
|
||||
export interface PayloadMigration {
|
||||
id: string;
|
||||
name?: string | null;
|
||||
batch?: number | null;
|
||||
updatedAt: string;
|
||||
createdAt: string;
|
||||
}
|
||||
/**
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
* via the `definition` "auth".
|
||||
*/
|
||||
export interface Auth {
|
||||
[k: string]: unknown;
|
||||
}
|
||||
|
||||
|
||||
declare module 'payload' {
|
||||
// @ts-ignore
|
||||
export interface GeneratedTypes extends Config {}
|
||||
}
|
||||
3
test/login-with-username/tsconfig.json
Normal file
3
test/login-with-username/tsconfig.json
Normal file
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"extends": "../tsconfig.json"
|
||||
}
|
||||
Reference in New Issue
Block a user