fix: allows for emails to be non unique when allowEmailLogin is false (#9541)

### What?
When you prevent users from authenticating with their email, we should
not enforce uniqueness on the email field.

### Why?
We never set the unique property to false.

### How?
Set the unique property to false if `loginWithUsername.allowEmailLogin`
is `false`.
This commit is contained in:
Jarrod Flesch
2024-11-26 16:18:10 -05:00
committed by GitHub
parent f19053e049
commit 67a9d669b6
9 changed files with 36 additions and 17 deletions

View File

@@ -14,7 +14,7 @@ import {
GraphQLString, GraphQLString,
} from 'graphql' } from 'graphql'
import { buildVersionCollectionFields, flattenTopLevelFields, formatNames, toWords } from 'payload' import { buildVersionCollectionFields, flattenTopLevelFields, formatNames, toWords } from 'payload'
import { fieldAffectsData } from 'payload/shared' import { fieldAffectsData, getLoginOptions } from 'payload/shared'
import type { ObjectTypeConfig } from './buildObjectType.js' import type { ObjectTypeConfig } from './buildObjectType.js'
@@ -442,10 +442,9 @@ export function initCollections({ config, graphqlResult }: InitCollectionsGraphQ
if (!collectionConfig.auth.disableLocalStrategy) { if (!collectionConfig.auth.disableLocalStrategy) {
const authArgs = {} const authArgs = {}
const canLoginWithEmail = const { canLoginWithEmail, canLoginWithUsername } = getLoginOptions(
!collectionConfig.auth.loginWithUsername || collectionConfig.auth.loginWithUsername,
collectionConfig.auth.loginWithUsername?.allowEmailLogin )
const canLoginWithUsername = collectionConfig.auth.loginWithUsername
if (canLoginWithEmail) { if (canLoginWithEmail) {
authArgs['email'] = { type: new GraphQLNonNull(GraphQLString) } authArgs['email'] = { type: new GraphQLNonNull(GraphQLString) }

View File

@@ -11,6 +11,7 @@ import type { FormState } from 'payload'
import { Form, FormSubmit, PasswordField, useAuth, useConfig, useTranslation } from '@payloadcms/ui' import { Form, FormSubmit, PasswordField, useAuth, useConfig, useTranslation } from '@payloadcms/ui'
import { formatAdminURL } from '@payloadcms/ui/shared' import { formatAdminURL } from '@payloadcms/ui/shared'
import { getLoginOptions } from 'payload/shared'
import type { LoginFieldProps } from '../LoginField/index.js' import type { LoginFieldProps } from '../LoginField/index.js'
@@ -36,9 +37,7 @@ export const LoginForm: React.FC<{
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 canLoginWithEmail = const { canLoginWithEmail, canLoginWithUsername } = getLoginOptions(loginWithUsername)
!authOptions.loginWithUsername || authOptions.loginWithUsername.allowEmailLogin
const canLoginWithUsername = authOptions.loginWithUsername
const [loginType] = React.useState<LoginFieldProps['type']>(() => { const [loginType] = React.useState<LoginFieldProps['type']>(() => {
if (canLoginWithEmail && canLoginWithUsername) { if (canLoginWithEmail && canLoginWithUsername) {

View File

@@ -28,6 +28,9 @@ export const getBaseAuthFields = (authConfig: IncomingAuthType): Field[] => {
if (authConfig.loginWithUsername.requireUsername === false) { if (authConfig.loginWithUsername.requireUsername === false) {
usernameField.required = false usernameField.required = false
} }
if (authConfig.loginWithUsername.allowEmailLogin === false) {
emailField.unique = false
}
} }
} }

View File

@@ -0,0 +1,13 @@
import type { Auth } from './types.js'
export const getLoginOptions = (
loginWithUsername: Auth['loginWithUsername'],
): {
canLoginWithEmail: boolean
canLoginWithUsername: boolean
} => {
return {
canLoginWithEmail: !loginWithUsername || loginWithUsername.allowEmailLogin,
canLoginWithUsername: Boolean(loginWithUsername),
}
}

View File

@@ -14,6 +14,7 @@ import { APIError } 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 { getLoginOptions } from '../getLoginOptions.js'
export type Arguments<TSlug extends CollectionSlug> = { export type Arguments<TSlug extends CollectionSlug> = {
collection: Collection collection: Collection
@@ -33,8 +34,7 @@ export const forgotPasswordOperation = async <TSlug extends CollectionSlug>(
const loginWithUsername = incomingArgs.collection.config.auth.loginWithUsername const loginWithUsername = incomingArgs.collection.config.auth.loginWithUsername
const { data } = incomingArgs const { data } = incomingArgs
const canLoginWithUsername = Boolean(loginWithUsername) const { canLoginWithEmail, canLoginWithUsername } = getLoginOptions(loginWithUsername)
const canLoginWithEmail = !loginWithUsername || loginWithUsername.allowEmailLogin
const sanitizedEmail = const sanitizedEmail =
(canLoginWithEmail && (incomingArgs.data.email || '').toLowerCase().trim()) || null (canLoginWithEmail && (incomingArgs.data.email || '').toLowerCase().trim()) || null

View File

@@ -13,6 +13,7 @@ import { afterRead } from '../../fields/hooks/afterRead/index.js'
import { killTransaction } from '../../utilities/killTransaction.js' import { killTransaction } from '../../utilities/killTransaction.js'
import sanitizeInternalFields from '../../utilities/sanitizeInternalFields.js' import sanitizeInternalFields from '../../utilities/sanitizeInternalFields.js'
import { getFieldsToSign } from '../getFieldsToSign.js' import { getFieldsToSign } from '../getFieldsToSign.js'
import { getLoginOptions } from '../getLoginOptions.js'
import isLocked from '../isLocked.js' import isLocked from '../isLocked.js'
import { jwtSign } from '../jwt.js' import { jwtSign } from '../jwt.js'
import { authenticateLocalStrategy } from '../strategies/local/authenticate.js' import { authenticateLocalStrategy } from '../strategies/local/authenticate.js'
@@ -87,8 +88,7 @@ export const loginOperation = async <TSlug extends CollectionSlug>(
? data.username.toLowerCase().trim() ? data.username.toLowerCase().trim()
: null : null
const canLoginWithUsername = Boolean(loginWithUsername) const { canLoginWithEmail, canLoginWithUsername } = getLoginOptions(loginWithUsername)
const canLoginWithEmail = !loginWithUsername || loginWithUsername.allowEmailLogin
// cannot login with email, did not provide username // cannot login with email, did not provide username
if (!canLoginWithEmail && !sanitizedUsername) { if (!canLoginWithEmail && !sanitizedUsername) {

View File

@@ -12,6 +12,7 @@ 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 executeAccess from '../executeAccess.js' import executeAccess from '../executeAccess.js'
import { getLoginOptions } from '../getLoginOptions.js'
import { resetLoginAttempts } from '../strategies/local/resetLoginAttempts.js' import { resetLoginAttempts } from '../strategies/local/resetLoginAttempts.js'
export type Arguments<TSlug extends CollectionSlug> = { export type Arguments<TSlug extends CollectionSlug> = {
@@ -32,8 +33,8 @@ export const unlockOperation = async <TSlug extends CollectionSlug>(
} = args } = args
const loginWithUsername = collectionConfig.auth.loginWithUsername const loginWithUsername = collectionConfig.auth.loginWithUsername
const canLoginWithUsername = Boolean(loginWithUsername)
const canLoginWithEmail = !loginWithUsername || loginWithUsername.allowEmailLogin const { canLoginWithEmail, canLoginWithUsername } = getLoginOptions(loginWithUsername)
const sanitizedEmail = canLoginWithEmail && (args.data?.email || '').toLowerCase().trim() const sanitizedEmail = canLoginWithEmail && (args.data?.email || '').toLowerCase().trim()
const sanitizedUsername = const sanitizedUsername =

View File

@@ -3,6 +3,7 @@ import type { JsonObject, Payload } from '../../../index.js'
import type { PayloadRequest, SelectType, Where } from '../../../types/index.js' import type { PayloadRequest, SelectType, Where } from '../../../types/index.js'
import { ValidationError } from '../../../errors/index.js' import { ValidationError } from '../../../errors/index.js'
import { getLoginOptions } from '../../getLoginOptions.js'
import { generatePasswordSaltHash } from './generatePasswordSaltHash.js' import { generatePasswordSaltHash } from './generatePasswordSaltHash.js'
type Args = { type Args = {
@@ -24,9 +25,11 @@ export const registerLocalStrategy = async ({
}: Args): Promise<Record<string, unknown>> => { }: Args): Promise<Record<string, unknown>> => {
const loginWithUsername = collection?.auth?.loginWithUsername const loginWithUsername = collection?.auth?.loginWithUsername
const { canLoginWithEmail, canLoginWithUsername } = getLoginOptions(loginWithUsername)
let whereConstraint: Where let whereConstraint: Where
if (!loginWithUsername) { if (!canLoginWithUsername) {
whereConstraint = { whereConstraint = {
email: { email: {
equals: doc.email, equals: doc.email,
@@ -37,7 +40,7 @@ export const registerLocalStrategy = async ({
or: [], or: [],
} }
if (doc.email) { if (canLoginWithEmail && doc.email) {
whereConstraint.or?.push({ whereConstraint.or?.push({
email: { email: {
equals: doc.email, equals: doc.email,
@@ -67,7 +70,7 @@ export const registerLocalStrategy = async ({
throw new ValidationError({ throw new ValidationError({
collection: collection.slug, collection: collection.slug,
errors: [ errors: [
loginWithUsername canLoginWithUsername
? { ? {
message: req.t('error:usernameAlreadyRegistered'), message: req.t('error:usernameAlreadyRegistered'),
path: 'username', path: 'username',

View File

@@ -5,6 +5,7 @@ export {
getCookieExpiration, getCookieExpiration,
parseCookies, parseCookies,
} from '../auth/cookies.js' } from '../auth/cookies.js'
export { getLoginOptions } from '../auth/getLoginOptions.js'
export { getFromImportMap } from '../bin/generateImportMap/getFromImportMap.js' export { getFromImportMap } from '../bin/generateImportMap/getFromImportMap.js'
export { parsePayloadComponent } from '../bin/generateImportMap/parsePayloadComponent.js' export { parsePayloadComponent } from '../bin/generateImportMap/parsePayloadComponent.js'
export { defaults as collectionDefaults } from '../collections/config/defaults.js' export { defaults as collectionDefaults } from '../collections/config/defaults.js'