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