fix: misc issues with loginWithUsername (#7311)
- improves types - fixes create-first-user fields
This commit is contained in:
committed by
GitHub
parent
c405e5958f
commit
b2814eb67c
@@ -348,15 +348,19 @@ function initCollectionsGraphQL({ config, graphqlResult }: InitCollectionsGraphQ
|
||||
}
|
||||
|
||||
if (collectionConfig.auth) {
|
||||
const authFields: Field[] = collectionConfig.auth.disableLocalStrategy
|
||||
? []
|
||||
: [
|
||||
{
|
||||
name: 'email',
|
||||
type: 'email',
|
||||
required: true,
|
||||
},
|
||||
]
|
||||
const authFields: Field[] =
|
||||
collectionConfig.auth.disableLocalStrategy ||
|
||||
(collectionConfig.auth.loginWithUsername &&
|
||||
!collectionConfig.auth.loginWithUsername.allowEmailLogin &&
|
||||
!collectionConfig.auth.loginWithUsername.requireEmail)
|
||||
? []
|
||||
: [
|
||||
{
|
||||
name: 'email',
|
||||
type: 'email',
|
||||
required: true,
|
||||
},
|
||||
]
|
||||
collection.graphQL.JWT = buildObjectType({
|
||||
name: formatName(`${slug}JWT`),
|
||||
config,
|
||||
|
||||
@@ -9,16 +9,17 @@ import { headersWithCors } from '../../../utilities/headersWithCors.js'
|
||||
export const login: CollectionRouteHandler = async ({ collection, req }) => {
|
||||
const { searchParams, t } = req
|
||||
const depth = searchParams.get('depth')
|
||||
const authData = collection.config.auth?.loginWithUsername
|
||||
? {
|
||||
email: typeof req.data?.email === 'string' ? req.data.email : '',
|
||||
password: typeof req.data?.password === 'string' ? req.data.password : '',
|
||||
username: typeof req.data?.username === 'string' ? req.data.username : '',
|
||||
}
|
||||
: {
|
||||
email: typeof req.data?.email === 'string' ? req.data.email : '',
|
||||
password: typeof req.data?.password === 'string' ? req.data.password : '',
|
||||
}
|
||||
const authData =
|
||||
collection.config.auth?.loginWithUsername !== false
|
||||
? {
|
||||
email: typeof req.data?.email === 'string' ? req.data.email : '',
|
||||
password: typeof req.data?.password === 'string' ? req.data.password : '',
|
||||
username: typeof req.data?.username === 'string' ? req.data.username : '',
|
||||
}
|
||||
: {
|
||||
email: typeof req.data?.email === 'string' ? req.data.email : '',
|
||||
password: typeof req.data?.password === 'string' ? req.data.password : '',
|
||||
}
|
||||
|
||||
const result = await loginOperation({
|
||||
collection,
|
||||
|
||||
@@ -8,14 +8,15 @@ import { headersWithCors } from '../../../utilities/headersWithCors.js'
|
||||
export const unlock: CollectionRouteHandler = async ({ collection, req }) => {
|
||||
const { t } = req
|
||||
|
||||
const authData = collection.config.auth?.loginWithUsername
|
||||
? {
|
||||
email: typeof req.data?.email === 'string' ? req.data.email : '',
|
||||
username: typeof req.data?.username === 'string' ? req.data.username : '',
|
||||
}
|
||||
: {
|
||||
email: typeof req.data?.email === 'string' ? req.data.email : '',
|
||||
}
|
||||
const authData =
|
||||
collection.config.auth?.loginWithUsername !== false
|
||||
? {
|
||||
email: typeof req.data?.email === 'string' ? req.data.email : '',
|
||||
username: typeof req.data?.username === 'string' ? req.data.username : '',
|
||||
}
|
||||
: {
|
||||
email: typeof req.data?.email === 'string' ? req.data.email : '',
|
||||
}
|
||||
|
||||
await unlockOperation({
|
||||
collection,
|
||||
|
||||
@@ -3,7 +3,6 @@ import type { FormState } from 'payload'
|
||||
|
||||
import {
|
||||
ConfirmPasswordField,
|
||||
EmailField,
|
||||
Form,
|
||||
type FormProps,
|
||||
FormSubmit,
|
||||
@@ -16,10 +15,14 @@ import {
|
||||
import { getFormState } from '@payloadcms/ui/shared'
|
||||
import React from 'react'
|
||||
|
||||
import { LoginField } from '../Login/LoginField/index.js'
|
||||
|
||||
export const CreateFirstUserClient: React.FC<{
|
||||
initialState: FormState
|
||||
loginType: 'email' | 'emailOrUsername' | 'username'
|
||||
requireEmail?: boolean
|
||||
userSlug: string
|
||||
}> = ({ initialState, userSlug }) => {
|
||||
}> = ({ initialState, loginType, requireEmail = true, userSlug }) => {
|
||||
const { getFieldMap } = useComponentMap()
|
||||
|
||||
const {
|
||||
@@ -56,7 +59,10 @@ export const CreateFirstUserClient: React.FC<{
|
||||
redirect={admin}
|
||||
validationOperation="create"
|
||||
>
|
||||
<EmailField autoComplete="email" label={t('general:email')} name="email" required />
|
||||
{['emailOrUsername', 'username'].includes(loginType) && <LoginField type="username" />}
|
||||
{['email', 'emailOrUsername'].includes(loginType) && (
|
||||
<LoginField required={requireEmail} type="email" />
|
||||
)}
|
||||
<PasswordField
|
||||
autoComplete="off"
|
||||
label={t('authentication:newPassword')}
|
||||
|
||||
@@ -3,6 +3,8 @@ import type { AdminViewProps, Field } from 'payload'
|
||||
import { buildStateFromSchema } from '@payloadcms/ui/forms/buildStateFromSchema'
|
||||
import React from 'react'
|
||||
|
||||
import type { LoginFieldProps } from '../Login/LoginField/index.js'
|
||||
|
||||
import { CreateFirstUserClient } from './index.client.js'
|
||||
import './index.scss'
|
||||
|
||||
@@ -16,17 +18,39 @@ export const CreateFirstUserView: React.FC<AdminViewProps> = async ({ initPageRe
|
||||
config: {
|
||||
admin: { user: userSlug },
|
||||
},
|
||||
config,
|
||||
},
|
||||
},
|
||||
} = initPageResult
|
||||
|
||||
const fields: Field[] = [
|
||||
{
|
||||
name: 'email',
|
||||
type: 'email',
|
||||
label: req.t('general:emailAddress'),
|
||||
required: true,
|
||||
},
|
||||
const collectionConfig = config.collections?.find((collection) => collection?.slug === userSlug)
|
||||
const { auth: authOptions } = collectionConfig
|
||||
const loginWithUsername = authOptions.loginWithUsername
|
||||
const loginWithEmail = !loginWithUsername || loginWithUsername.allowEmailLogin
|
||||
const emailRequired = loginWithUsername && loginWithUsername.requireEmail
|
||||
|
||||
let loginType: LoginFieldProps['type'] = loginWithUsername ? 'username' : 'email'
|
||||
if (loginWithUsername && (loginWithUsername.allowEmailLogin || loginWithUsername.requireEmail)) {
|
||||
loginType = 'emailOrUsername'
|
||||
}
|
||||
|
||||
const emailField = {
|
||||
name: 'email',
|
||||
type: 'email',
|
||||
label: req.t('general:emailAddress'),
|
||||
required: emailRequired ? true : false,
|
||||
}
|
||||
|
||||
const usernameField = {
|
||||
name: 'username',
|
||||
type: 'text',
|
||||
label: req.t('authentication:username'),
|
||||
required: true,
|
||||
}
|
||||
|
||||
const fields = [
|
||||
...(loginWithUsername ? [usernameField] : []),
|
||||
...(emailRequired || loginWithEmail ? [emailField] : []),
|
||||
{
|
||||
name: 'password',
|
||||
type: 'text',
|
||||
@@ -42,7 +66,7 @@ export const CreateFirstUserView: React.FC<AdminViewProps> = async ({ initPageRe
|
||||
]
|
||||
|
||||
const formState = await buildStateFromSchema({
|
||||
fieldSchema: fields,
|
||||
fieldSchema: fields as Field[],
|
||||
operation: 'create',
|
||||
preferences: { fields: {} },
|
||||
req,
|
||||
@@ -52,7 +76,12 @@ export const CreateFirstUserView: React.FC<AdminViewProps> = async ({ initPageRe
|
||||
<div className="create-first-user">
|
||||
<h1>{req.t('general:welcome')}</h1>
|
||||
<p>{req.t('authentication:beginCreateFirstUser')}</p>
|
||||
<CreateFirstUserClient initialState={formState} userSlug={userSlug} />
|
||||
<CreateFirstUserClient
|
||||
initialState={formState}
|
||||
loginType={loginType}
|
||||
requireEmail={emailRequired}
|
||||
userSlug={userSlug}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -34,6 +34,7 @@ export const Auth: React.FC<Props> = (props) => {
|
||||
readOnly,
|
||||
requirePassword,
|
||||
useAPIKey,
|
||||
username,
|
||||
verify,
|
||||
} = props
|
||||
|
||||
@@ -64,9 +65,8 @@ export const Auth: React.FC<Props> = (props) => {
|
||||
const unlock = useCallback(async () => {
|
||||
const url = `${serverURL}${api}/${collectionSlug}/unlock`
|
||||
const response = await fetch(url, {
|
||||
body: JSON.stringify({
|
||||
email,
|
||||
}),
|
||||
body:
|
||||
loginWithUsername && username ? JSON.stringify({ username }) : JSON.stringify({ email }),
|
||||
credentials: 'include',
|
||||
headers: {
|
||||
'Accept-Language': i18n.language,
|
||||
@@ -80,7 +80,7 @@ export const Auth: React.FC<Props> = (props) => {
|
||||
} else {
|
||||
toast.error(t('authentication:failedToUnlock'))
|
||||
}
|
||||
}, [i18n, serverURL, api, collectionSlug, email, t])
|
||||
}, [i18n, serverURL, api, collectionSlug, email, username, t])
|
||||
|
||||
useEffect(() => {
|
||||
if (!modified) {
|
||||
@@ -98,6 +98,15 @@ export const Auth: React.FC<Props> = (props) => {
|
||||
<div className={[baseClass, className].filter(Boolean).join(' ')}>
|
||||
{!disableLocalStrategy && (
|
||||
<React.Fragment>
|
||||
{Boolean(loginWithUsername) && (
|
||||
<TextField
|
||||
disabled={disabled}
|
||||
label={t('authentication:username')}
|
||||
name="username"
|
||||
readOnly={readOnly}
|
||||
required
|
||||
/>
|
||||
)}
|
||||
{(!loginWithUsername ||
|
||||
loginWithUsername?.allowEmailLogin ||
|
||||
loginWithUsername?.requireEmail) && (
|
||||
@@ -110,15 +119,6 @@ export const Auth: React.FC<Props> = (props) => {
|
||||
required={!loginWithUsername || loginWithUsername?.requireEmail}
|
||||
/>
|
||||
)}
|
||||
{loginWithUsername && (
|
||||
<TextField
|
||||
disabled={disabled}
|
||||
label={t('authentication:username')}
|
||||
name="username"
|
||||
readOnly={readOnly}
|
||||
required
|
||||
/>
|
||||
)}
|
||||
{(changingPassword || requirePassword) && (
|
||||
<div className={`${baseClass}__changing-password`}>
|
||||
<PasswordField
|
||||
|
||||
@@ -10,5 +10,6 @@ export type Props = {
|
||||
readOnly: boolean
|
||||
requirePassword?: boolean
|
||||
useAPIKey?: boolean
|
||||
username: string
|
||||
verify?: VerifyConfig | boolean
|
||||
}
|
||||
|
||||
@@ -232,6 +232,7 @@ export const DefaultEditView: React.FC = () => {
|
||||
readOnly={!hasSavePermission}
|
||||
requirePassword={!id}
|
||||
useAPIKey={auth.useAPIKey}
|
||||
username={data?.username}
|
||||
verify={auth.verify}
|
||||
/>
|
||||
)}
|
||||
|
||||
@@ -5,9 +5,10 @@ import { EmailField, TextField, useConfig, useTranslation } from '@payloadcms/ui
|
||||
import { email, username } from 'payload/shared'
|
||||
import React from 'react'
|
||||
export type LoginFieldProps = {
|
||||
required?: boolean
|
||||
type: 'email' | 'emailOrUsername' | 'username'
|
||||
}
|
||||
export const LoginField: React.FC<LoginFieldProps> = ({ type }) => {
|
||||
export const LoginField: React.FC<LoginFieldProps> = ({ type, required = true }) => {
|
||||
const { t } = useTranslation()
|
||||
const config = useConfig()
|
||||
|
||||
@@ -17,7 +18,7 @@ export const LoginField: React.FC<LoginFieldProps> = ({ type }) => {
|
||||
autoComplete="email"
|
||||
label={t('general:email')}
|
||||
name="email"
|
||||
required
|
||||
required={required}
|
||||
validate={(value) =>
|
||||
email(value, {
|
||||
name: 'email',
|
||||
|
||||
@@ -7,7 +7,7 @@ export const emailField = ({ required = true }: { required?: boolean }): Field =
|
||||
type: 'email',
|
||||
admin: {
|
||||
components: {
|
||||
Field: required ? () => null : undefined,
|
||||
Field: () => null,
|
||||
},
|
||||
},
|
||||
hooks: {
|
||||
|
||||
@@ -612,65 +612,111 @@ const fieldType: JSONSchema4 = {
|
||||
type: 'string',
|
||||
required: false,
|
||||
}
|
||||
const generateAuthFieldTypes = (
|
||||
loginWithUsername: Auth['loginWithUsername'],
|
||||
withPassword = false,
|
||||
): JSONSchema4 => {
|
||||
const passwordField = {
|
||||
password: fieldType,
|
||||
const generateAuthFieldTypes = ({
|
||||
type,
|
||||
loginWithUsername,
|
||||
}: {
|
||||
loginWithUsername: Auth['loginWithUsername']
|
||||
type: 'forgotOrUnlock' | 'login' | 'register'
|
||||
}): JSONSchema4 => {
|
||||
const emailAuthFields = {
|
||||
additionalProperties: false,
|
||||
properties: { email: fieldType },
|
||||
required: ['email'],
|
||||
}
|
||||
const usernameAuthFields = {
|
||||
additionalProperties: false,
|
||||
properties: { username: fieldType },
|
||||
required: ['username'],
|
||||
}
|
||||
|
||||
if (['login', 'register'].includes(type)) {
|
||||
emailAuthFields.properties['password'] = fieldType
|
||||
emailAuthFields.required.push('password')
|
||||
usernameAuthFields.properties['password'] = fieldType
|
||||
usernameAuthFields.required.push('password')
|
||||
}
|
||||
|
||||
if (loginWithUsername) {
|
||||
if (loginWithUsername.allowEmailLogin) {
|
||||
return {
|
||||
additionalProperties: false,
|
||||
oneOf: [
|
||||
{
|
||||
switch (type) {
|
||||
case 'login': {
|
||||
if (loginWithUsername.allowEmailLogin) {
|
||||
// allow username or email and require password for login
|
||||
return {
|
||||
additionalProperties: false,
|
||||
properties: { email: fieldType, ...(withPassword ? { password: fieldType } : {}) },
|
||||
required: ['email', ...(withPassword ? ['password'] : [])],
|
||||
},
|
||||
{
|
||||
oneOf: [emailAuthFields, usernameAuthFields],
|
||||
}
|
||||
} else {
|
||||
// allow only username and password for login
|
||||
return usernameAuthFields
|
||||
}
|
||||
}
|
||||
|
||||
case 'register': {
|
||||
if (loginWithUsername.requireEmail) {
|
||||
// require username, email and password for registration
|
||||
return {
|
||||
additionalProperties: false,
|
||||
properties: { username: fieldType, ...(withPassword ? { password: fieldType } : {}) },
|
||||
required: ['username', ...(withPassword ? ['password'] : [])],
|
||||
},
|
||||
],
|
||||
properties: {
|
||||
...usernameAuthFields.properties,
|
||||
...emailAuthFields.properties,
|
||||
},
|
||||
required: [...usernameAuthFields.required, ...emailAuthFields.required],
|
||||
}
|
||||
} else if (loginWithUsername.allowEmailLogin) {
|
||||
// allow both but only require username for registration
|
||||
return {
|
||||
additionalProperties: false,
|
||||
properties: {
|
||||
...usernameAuthFields.properties,
|
||||
...emailAuthFields.properties,
|
||||
},
|
||||
required: usernameAuthFields.required,
|
||||
}
|
||||
} else {
|
||||
// require only username and password for registration
|
||||
return usernameAuthFields
|
||||
}
|
||||
}
|
||||
|
||||
case 'forgotOrUnlock': {
|
||||
if (loginWithUsername.allowEmailLogin) {
|
||||
// allow email or username for unlock/forgot-password
|
||||
return {
|
||||
additionalProperties: false,
|
||||
oneOf: [emailAuthFields, usernameAuthFields],
|
||||
}
|
||||
} else {
|
||||
// allow only username for unlock/forgot-password
|
||||
return usernameAuthFields
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
additionalProperties: false,
|
||||
properties: {
|
||||
username: fieldType,
|
||||
...(withPassword ? { password: fieldType } : {}),
|
||||
},
|
||||
required: ['username', ...(withPassword ? ['password'] : [])],
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
additionalProperties: false,
|
||||
properties: {
|
||||
email: fieldType,
|
||||
...(withPassword ? { password: fieldType } : {}),
|
||||
},
|
||||
required: ['email', ...(withPassword ? ['password'] : [])],
|
||||
}
|
||||
// default email (and password for login/register)
|
||||
return emailAuthFields
|
||||
}
|
||||
|
||||
export function authCollectionToOperationsJSONSchema(
|
||||
config: SanitizedCollectionConfig,
|
||||
): JSONSchema4 {
|
||||
const loginWithUsername = config.auth?.loginWithUsername
|
||||
const generatedFields: JSONSchema4 = generateAuthFieldTypes(loginWithUsername)
|
||||
const generatedFieldsWithPassword: JSONSchema4 = generateAuthFieldTypes(loginWithUsername, true)
|
||||
const loginUserFields: JSONSchema4 = generateAuthFieldTypes({ type: 'login', loginWithUsername })
|
||||
const forgotOrUnlockUserFields: JSONSchema4 = generateAuthFieldTypes({
|
||||
type: 'forgotOrUnlock',
|
||||
loginWithUsername,
|
||||
})
|
||||
const registerUserFields: JSONSchema4 = generateAuthFieldTypes({
|
||||
type: 'register',
|
||||
loginWithUsername,
|
||||
})
|
||||
|
||||
const properties: JSONSchema4['properties'] = {
|
||||
forgotPassword: generatedFields,
|
||||
login: generatedFieldsWithPassword,
|
||||
registerFirstUser: generatedFieldsWithPassword,
|
||||
unlock: generatedFields,
|
||||
forgotPassword: forgotOrUnlockUserFields,
|
||||
login: loginUserFields,
|
||||
registerFirstUser: registerUserFields,
|
||||
unlock: forgotOrUnlockUserFields,
|
||||
}
|
||||
|
||||
return {
|
||||
|
||||
@@ -39,55 +39,53 @@ export const DocumentFields: React.FC<Args> = ({
|
||||
const hasSidebarFields = sidebarFields && sidebarFields.length > 0
|
||||
|
||||
return (
|
||||
<React.Fragment>
|
||||
<div
|
||||
className={[
|
||||
baseClass,
|
||||
hasSidebarFields ? `${baseClass}--has-sidebar` : `${baseClass}--no-sidebar`,
|
||||
forceSidebarWrap && `${baseClass}--force-sidebar-wrap`,
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join(' ')}
|
||||
>
|
||||
<div className={`${baseClass}__main`}>
|
||||
<Gutter className={`${baseClass}__edit`}>
|
||||
<header className={`${baseClass}__header`}>
|
||||
{description && (
|
||||
<div className={`${baseClass}__sub-header`}>
|
||||
{/* <ViewDescription description={description} /> */}
|
||||
</div>
|
||||
)}
|
||||
</header>
|
||||
{BeforeFields}
|
||||
<RenderFields
|
||||
className={`${baseClass}__fields`}
|
||||
fieldMap={mainFields}
|
||||
forceRender={10}
|
||||
path=""
|
||||
permissions={docPermissions?.fields}
|
||||
readOnly={readOnly}
|
||||
schemaPath={schemaPath}
|
||||
/>
|
||||
{AfterFields}
|
||||
</Gutter>
|
||||
</div>
|
||||
{hasSidebarFields && (
|
||||
<div className={`${baseClass}__sidebar-wrap`}>
|
||||
<div className={`${baseClass}__sidebar`}>
|
||||
<div className={`${baseClass}__sidebar-fields`}>
|
||||
<RenderFields
|
||||
fieldMap={sidebarFields}
|
||||
forceRender={10}
|
||||
path=""
|
||||
permissions={docPermissions?.fields}
|
||||
readOnly={readOnly}
|
||||
schemaPath={schemaPath}
|
||||
/>
|
||||
<div
|
||||
className={[
|
||||
baseClass,
|
||||
hasSidebarFields ? `${baseClass}--has-sidebar` : `${baseClass}--no-sidebar`,
|
||||
forceSidebarWrap && `${baseClass}--force-sidebar-wrap`,
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join(' ')}
|
||||
>
|
||||
<div className={`${baseClass}__main`}>
|
||||
<Gutter className={`${baseClass}__edit`}>
|
||||
<header className={`${baseClass}__header`}>
|
||||
{description && (
|
||||
<div className={`${baseClass}__sub-header`}>
|
||||
{/* <ViewDescription description={description} /> */}
|
||||
</div>
|
||||
)}
|
||||
</header>
|
||||
{BeforeFields}
|
||||
<RenderFields
|
||||
className={`${baseClass}__fields`}
|
||||
fieldMap={mainFields}
|
||||
forceRender={10}
|
||||
path=""
|
||||
permissions={docPermissions?.fields}
|
||||
readOnly={readOnly}
|
||||
schemaPath={schemaPath}
|
||||
/>
|
||||
{AfterFields}
|
||||
</Gutter>
|
||||
</div>
|
||||
{hasSidebarFields && (
|
||||
<div className={`${baseClass}__sidebar-wrap`}>
|
||||
<div className={`${baseClass}__sidebar`}>
|
||||
<div className={`${baseClass}__sidebar-fields`}>
|
||||
<RenderFields
|
||||
fieldMap={sidebarFields}
|
||||
forceRender={10}
|
||||
path=""
|
||||
permissions={docPermissions?.fields}
|
||||
readOnly={readOnly}
|
||||
schemaPath={schemaPath}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</React.Fragment>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -193,6 +193,7 @@ export const buildFormState = async ({ req }: { req: PayloadRequest }): Promise<
|
||||
req.payload.collections[collectionSlug]?.config?.auth &&
|
||||
!req.payload.collections[collectionSlug].config.auth.disableLocalStrategy
|
||||
) {
|
||||
if (formState.username) result.username = formState.username
|
||||
if (formState.password) result.password = formState.password
|
||||
if (formState['confirm-password']) result['confirm-password'] = formState['confirm-password']
|
||||
if (formState.email) result.email = formState.email
|
||||
|
||||
Reference in New Issue
Block a user