Skip to content

Commit b2814eb

Browse files
authored
fix: misc issues with loginWithUsername (#7311)
- improves types - fixes create-first-user fields
1 parent c405e59 commit b2814eb

File tree

13 files changed

+232
-143
lines changed

13 files changed

+232
-143
lines changed

packages/graphql/src/schema/initCollections.ts

Lines changed: 13 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -348,15 +348,19 @@ function initCollectionsGraphQL({ config, graphqlResult }: InitCollectionsGraphQ
348348
}
349349

350350
if (collectionConfig.auth) {
351-
const authFields: Field[] = collectionConfig.auth.disableLocalStrategy
352-
? []
353-
: [
354-
{
355-
name: 'email',
356-
type: 'email',
357-
required: true,
358-
},
359-
]
351+
const authFields: Field[] =
352+
collectionConfig.auth.disableLocalStrategy ||
353+
(collectionConfig.auth.loginWithUsername &&
354+
!collectionConfig.auth.loginWithUsername.allowEmailLogin &&
355+
!collectionConfig.auth.loginWithUsername.requireEmail)
356+
? []
357+
: [
358+
{
359+
name: 'email',
360+
type: 'email',
361+
required: true,
362+
},
363+
]
360364
collection.graphQL.JWT = buildObjectType({
361365
name: formatName(`${slug}JWT`),
362366
config,

packages/next/src/routes/rest/auth/login.ts

Lines changed: 11 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -9,16 +9,17 @@ import { headersWithCors } from '../../../utilities/headersWithCors.js'
99
export const login: CollectionRouteHandler = async ({ collection, req }) => {
1010
const { searchParams, t } = req
1111
const depth = searchParams.get('depth')
12-
const authData = collection.config.auth?.loginWithUsername
13-
? {
14-
email: typeof req.data?.email === 'string' ? req.data.email : '',
15-
password: typeof req.data?.password === 'string' ? req.data.password : '',
16-
username: typeof req.data?.username === 'string' ? req.data.username : '',
17-
}
18-
: {
19-
email: typeof req.data?.email === 'string' ? req.data.email : '',
20-
password: typeof req.data?.password === 'string' ? req.data.password : '',
21-
}
12+
const authData =
13+
collection.config.auth?.loginWithUsername !== false
14+
? {
15+
email: typeof req.data?.email === 'string' ? req.data.email : '',
16+
password: typeof req.data?.password === 'string' ? req.data.password : '',
17+
username: typeof req.data?.username === 'string' ? req.data.username : '',
18+
}
19+
: {
20+
email: typeof req.data?.email === 'string' ? req.data.email : '',
21+
password: typeof req.data?.password === 'string' ? req.data.password : '',
22+
}
2223

2324
const result = await loginOperation({
2425
collection,

packages/next/src/routes/rest/auth/unlock.ts

Lines changed: 9 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -8,14 +8,15 @@ import { headersWithCors } from '../../../utilities/headersWithCors.js'
88
export const unlock: CollectionRouteHandler = async ({ collection, req }) => {
99
const { t } = req
1010

11-
const authData = collection.config.auth?.loginWithUsername
12-
? {
13-
email: typeof req.data?.email === 'string' ? req.data.email : '',
14-
username: typeof req.data?.username === 'string' ? req.data.username : '',
15-
}
16-
: {
17-
email: typeof req.data?.email === 'string' ? req.data.email : '',
18-
}
11+
const authData =
12+
collection.config.auth?.loginWithUsername !== false
13+
? {
14+
email: typeof req.data?.email === 'string' ? req.data.email : '',
15+
username: typeof req.data?.username === 'string' ? req.data.username : '',
16+
}
17+
: {
18+
email: typeof req.data?.email === 'string' ? req.data.email : '',
19+
}
1920

2021
await unlockOperation({
2122
collection,

packages/next/src/views/CreateFirstUser/index.client.tsx

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@ import type { FormState } from 'payload'
33

44
import {
55
ConfirmPasswordField,
6-
EmailField,
76
Form,
87
type FormProps,
98
FormSubmit,
@@ -16,10 +15,14 @@ import {
1615
import { getFormState } from '@payloadcms/ui/shared'
1716
import React from 'react'
1817

18+
import { LoginField } from '../Login/LoginField/index.js'
19+
1920
export const CreateFirstUserClient: React.FC<{
2021
initialState: FormState
22+
loginType: 'email' | 'emailOrUsername' | 'username'
23+
requireEmail?: boolean
2124
userSlug: string
22-
}> = ({ initialState, userSlug }) => {
25+
}> = ({ initialState, loginType, requireEmail = true, userSlug }) => {
2326
const { getFieldMap } = useComponentMap()
2427

2528
const {
@@ -56,7 +59,10 @@ export const CreateFirstUserClient: React.FC<{
5659
redirect={admin}
5760
validationOperation="create"
5861
>
59-
<EmailField autoComplete="email" label={t('general:email')} name="email" required />
62+
{['emailOrUsername', 'username'].includes(loginType) && <LoginField type="username" />}
63+
{['email', 'emailOrUsername'].includes(loginType) && (
64+
<LoginField required={requireEmail} type="email" />
65+
)}
6066
<PasswordField
6167
autoComplete="off"
6268
label={t('authentication:newPassword')}

packages/next/src/views/CreateFirstUser/index.tsx

Lines changed: 38 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@ import type { AdminViewProps, Field } from 'payload'
33
import { buildStateFromSchema } from '@payloadcms/ui/forms/buildStateFromSchema'
44
import React from 'react'
55

6+
import type { LoginFieldProps } from '../Login/LoginField/index.js'
7+
68
import { CreateFirstUserClient } from './index.client.js'
79
import './index.scss'
810

@@ -16,17 +18,39 @@ export const CreateFirstUserView: React.FC<AdminViewProps> = async ({ initPageRe
1618
config: {
1719
admin: { user: userSlug },
1820
},
21+
config,
1922
},
2023
},
2124
} = initPageResult
2225

23-
const fields: Field[] = [
24-
{
25-
name: 'email',
26-
type: 'email',
27-
label: req.t('general:emailAddress'),
28-
required: true,
29-
},
26+
const collectionConfig = config.collections?.find((collection) => collection?.slug === userSlug)
27+
const { auth: authOptions } = collectionConfig
28+
const loginWithUsername = authOptions.loginWithUsername
29+
const loginWithEmail = !loginWithUsername || loginWithUsername.allowEmailLogin
30+
const emailRequired = loginWithUsername && loginWithUsername.requireEmail
31+
32+
let loginType: LoginFieldProps['type'] = loginWithUsername ? 'username' : 'email'
33+
if (loginWithUsername && (loginWithUsername.allowEmailLogin || loginWithUsername.requireEmail)) {
34+
loginType = 'emailOrUsername'
35+
}
36+
37+
const emailField = {
38+
name: 'email',
39+
type: 'email',
40+
label: req.t('general:emailAddress'),
41+
required: emailRequired ? true : false,
42+
}
43+
44+
const usernameField = {
45+
name: 'username',
46+
type: 'text',
47+
label: req.t('authentication:username'),
48+
required: true,
49+
}
50+
51+
const fields = [
52+
...(loginWithUsername ? [usernameField] : []),
53+
...(emailRequired || loginWithEmail ? [emailField] : []),
3054
{
3155
name: 'password',
3256
type: 'text',
@@ -42,7 +66,7 @@ export const CreateFirstUserView: React.FC<AdminViewProps> = async ({ initPageRe
4266
]
4367

4468
const formState = await buildStateFromSchema({
45-
fieldSchema: fields,
69+
fieldSchema: fields as Field[],
4670
operation: 'create',
4771
preferences: { fields: {} },
4872
req,
@@ -52,7 +76,12 @@ export const CreateFirstUserView: React.FC<AdminViewProps> = async ({ initPageRe
5276
<div className="create-first-user">
5377
<h1>{req.t('general:welcome')}</h1>
5478
<p>{req.t('authentication:beginCreateFirstUser')}</p>
55-
<CreateFirstUserClient initialState={formState} userSlug={userSlug} />
79+
<CreateFirstUserClient
80+
initialState={formState}
81+
loginType={loginType}
82+
requireEmail={emailRequired}
83+
userSlug={userSlug}
84+
/>
5685
</div>
5786
)
5887
}

packages/next/src/views/Edit/Default/Auth/index.tsx

Lines changed: 13 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ export const Auth: React.FC<Props> = (props) => {
3434
readOnly,
3535
requirePassword,
3636
useAPIKey,
37+
username,
3738
verify,
3839
} = props
3940

@@ -64,9 +65,8 @@ export const Auth: React.FC<Props> = (props) => {
6465
const unlock = useCallback(async () => {
6566
const url = `${serverURL}${api}/${collectionSlug}/unlock`
6667
const response = await fetch(url, {
67-
body: JSON.stringify({
68-
email,
69-
}),
68+
body:
69+
loginWithUsername && username ? JSON.stringify({ username }) : JSON.stringify({ email }),
7070
credentials: 'include',
7171
headers: {
7272
'Accept-Language': i18n.language,
@@ -80,7 +80,7 @@ export const Auth: React.FC<Props> = (props) => {
8080
} else {
8181
toast.error(t('authentication:failedToUnlock'))
8282
}
83-
}, [i18n, serverURL, api, collectionSlug, email, t])
83+
}, [i18n, serverURL, api, collectionSlug, email, username, t])
8484

8585
useEffect(() => {
8686
if (!modified) {
@@ -98,6 +98,15 @@ export const Auth: React.FC<Props> = (props) => {
9898
<div className={[baseClass, className].filter(Boolean).join(' ')}>
9999
{!disableLocalStrategy && (
100100
<React.Fragment>
101+
{Boolean(loginWithUsername) && (
102+
<TextField
103+
disabled={disabled}
104+
label={t('authentication:username')}
105+
name="username"
106+
readOnly={readOnly}
107+
required
108+
/>
109+
)}
101110
{(!loginWithUsername ||
102111
loginWithUsername?.allowEmailLogin ||
103112
loginWithUsername?.requireEmail) && (
@@ -110,15 +119,6 @@ export const Auth: React.FC<Props> = (props) => {
110119
required={!loginWithUsername || loginWithUsername?.requireEmail}
111120
/>
112121
)}
113-
{loginWithUsername && (
114-
<TextField
115-
disabled={disabled}
116-
label={t('authentication:username')}
117-
name="username"
118-
readOnly={readOnly}
119-
required
120-
/>
121-
)}
122122
{(changingPassword || requirePassword) && (
123123
<div className={`${baseClass}__changing-password`}>
124124
<PasswordField

packages/next/src/views/Edit/Default/Auth/types.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,5 +10,6 @@ export type Props = {
1010
readOnly: boolean
1111
requirePassword?: boolean
1212
useAPIKey?: boolean
13+
username: string
1314
verify?: VerifyConfig | boolean
1415
}

packages/next/src/views/Edit/Default/index.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -232,6 +232,7 @@ export const DefaultEditView: React.FC = () => {
232232
readOnly={!hasSavePermission}
233233
requirePassword={!id}
234234
useAPIKey={auth.useAPIKey}
235+
username={data?.username}
235236
verify={auth.verify}
236237
/>
237238
)}

packages/next/src/views/Login/LoginField/index.tsx

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,10 @@ import { EmailField, TextField, useConfig, useTranslation } from '@payloadcms/ui
55
import { email, username } from 'payload/shared'
66
import React from 'react'
77
export type LoginFieldProps = {
8+
required?: boolean
89
type: 'email' | 'emailOrUsername' | 'username'
910
}
10-
export const LoginField: React.FC<LoginFieldProps> = ({ type }) => {
11+
export const LoginField: React.FC<LoginFieldProps> = ({ type, required = true }) => {
1112
const { t } = useTranslation()
1213
const config = useConfig()
1314

@@ -17,7 +18,7 @@ export const LoginField: React.FC<LoginFieldProps> = ({ type }) => {
1718
autoComplete="email"
1819
label={t('general:email')}
1920
name="email"
20-
required
21+
required={required}
2122
validate={(value) =>
2223
email(value, {
2324
name: 'email',

packages/payload/src/auth/baseFields/email.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ export const emailField = ({ required = true }: { required?: boolean }): Field =
77
type: 'email',
88
admin: {
99
components: {
10-
Field: required ? () => null : undefined,
10+
Field: () => null,
1111
},
1212
},
1313
hooks: {

0 commit comments

Comments
 (0)