From 290ffd3287dee398b9908ffeacf36ffcfe948980 Mon Sep 17 00:00:00 2001 From: Jarrod Flesch <30633324+JarrodMFlesch@users.noreply.github.com> Date: Wed, 31 Jul 2024 14:55:08 -0400 Subject: [PATCH] fix: validates password and confirm password on the server (#7410) Fixes https://github.com/payloadcms/payload/issues/7380 Adjusts how the password/confirm-password fields are validated. Moves validation to the server, adds them to a custom schema under the schema path `${collectionSlug}.auth` for auth enabled collections. --- .../views/CreateFirstUser/index.client.tsx | 12 +- .../next/src/views/CreateFirstUser/index.tsx | 45 ++----- .../src/views/Document/getDocumentData.tsx | 7 +- .../src/views/Edit/Default/Auth/APIKey.tsx | 3 +- .../src/views/Edit/Default/Auth/index.tsx | 52 ++++++-- .../next/src/views/Edit/Default/Auth/types.ts | 2 + .../next/src/views/Edit/Default/index.tsx | 16 ++- .../next/src/views/Login/LoginField/index.tsx | 3 + .../next/src/views/Login/LoginForm/index.tsx | 26 +--- .../src/views/ResetPassword/index.client.tsx | 42 ++----- .../src/auth/operations/resetPassword.ts | 1 + .../local/generatePasswordSaltHash.ts | 25 ++-- .../src/auth/strategies/local/register.ts | 2 +- .../src/collections/operations/updateByID.ts | 1 + packages/payload/src/fields/validations.ts | 25 +++- .../ui/src/fields/ConfirmPassword/index.tsx | 7 +- packages/ui/src/fields/Password/index.tsx | 113 ++++++++++++------ packages/ui/src/fields/Password/input.tsx | 86 +++++++++++++ packages/ui/src/fields/Password/types.ts | 26 ++++ packages/ui/src/forms/useField/index.tsx | 1 + .../utilities/buildFieldSchemaMap/index.ts | 26 +++- packages/ui/src/utilities/buildFormState.ts | 2 - test/access-control/payload-types.ts | 13 +- test/auth/e2e.spec.ts | 57 +++++++-- test/auth/payload-types.ts | 22 +++- test/collections-graphql/int.spec.ts | 24 ++-- 26 files changed, 430 insertions(+), 209 deletions(-) create mode 100644 packages/ui/src/fields/Password/input.tsx create mode 100644 packages/ui/src/fields/Password/types.ts diff --git a/packages/next/src/views/CreateFirstUser/index.client.tsx b/packages/next/src/views/CreateFirstUser/index.client.tsx index 5a8b3c3f80..9501281dca 100644 --- a/packages/next/src/views/CreateFirstUser/index.client.tsx +++ b/packages/next/src/views/CreateFirstUser/index.client.tsx @@ -35,18 +35,17 @@ export const CreateFirstUserClient: React.FC<{ const fieldMap = getFieldMap({ collectionSlug: userSlug }) const onChange: FormProps['onChange'][0] = React.useCallback( - async ({ formState: prevFormState }) => { - return getFormState({ + async ({ formState: prevFormState }) => + getFormState({ apiRoute, body: { collectionSlug: userSlug, formState: prevFormState, operation: 'create', - schemaPath: userSlug, + schemaPath: `_${userSlug}.auth`, }, serverURL, - }) - }, + }), [apiRoute, userSlug, serverURL], ) @@ -64,14 +63,15 @@ export const CreateFirstUserClient: React.FC<{ )} = async ({ initPageResult }) => { const { + locale, req, req: { payload: { @@ -26,7 +27,6 @@ export const CreateFirstUserView: React.FC = async ({ initPageRe 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' @@ -34,42 +34,11 @@ export const CreateFirstUserView: React.FC = async ({ initPageRe 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', - label: req.t('general:password'), - required: true, - }, - { - name: 'confirm-password', - type: 'text', - label: req.t('authentication:confirmPassword'), - required: true, - }, - ] - - const formState = await buildStateFromSchema({ - fieldSchema: fields as Field[], - operation: 'create', - preferences: { fields: {} }, + const { formState } = await getDocumentData({ + collectionConfig, + locale, req, + schemaPath: `_${collectionConfig.slug}.auth`, }) return ( diff --git a/packages/next/src/views/Document/getDocumentData.tsx b/packages/next/src/views/Document/getDocumentData.tsx index 5bfb9f5f74..dce5548306 100644 --- a/packages/next/src/views/Document/getDocumentData.tsx +++ b/packages/next/src/views/Document/getDocumentData.tsx @@ -15,8 +15,11 @@ export const getDocumentData = async (args: { id?: number | string locale: Locale req: PayloadRequest + schemaPath?: string }): Promise => { - const { id, collectionConfig, globalConfig, locale, req } = args + const { id, collectionConfig, globalConfig, locale, req, schemaPath: schemaPathFromProps } = args + + const schemaPath = schemaPathFromProps || collectionConfig?.slug || globalConfig?.slug try { const formState = await buildFormState({ @@ -28,7 +31,7 @@ export const getDocumentData = async (args: { globalSlug: globalConfig?.slug, locale: locale?.code, operation: (collectionConfig && id) || globalConfig ? 'update' : 'create', - schemaPath: collectionConfig?.slug || globalConfig?.slug, + schemaPath, }, }, }) diff --git a/packages/next/src/views/Edit/Default/Auth/APIKey.tsx b/packages/next/src/views/Edit/Default/Auth/APIKey.tsx index f914d4546d..cf2e8d38b9 100644 --- a/packages/next/src/views/Edit/Default/Auth/APIKey.tsx +++ b/packages/next/src/views/Edit/Default/Auth/APIKey.tsx @@ -78,7 +78,7 @@ export const APIKey: React.FC<{ enabled: boolean; readOnly?: boolean }> = ({ if (!apiKeyValue && enabled) { setValue(initialAPIKey) } - if (!enabled) { + if (!enabled && apiKeyValue) { setValue(null) } }, [apiKeyValue, enabled, setValue, initialAPIKey]) @@ -100,6 +100,7 @@ export const APIKey: React.FC<{ enabled: boolean; readOnly?: boolean }> = ({
= (props) => { operation, readOnly, requirePassword, + setSchemaPath, + setValidateBeforeSubmit, useAPIKey, username, verify, @@ -42,6 +45,7 @@ export const Auth: React.FC = (props) => { const { permissions } = useAuth() const [changingPassword, setChangingPassword] = useState(requirePassword) const enableAPIKey = useFormFields(([fields]) => (fields && fields?.enableAPIKey) || null) + const forceOpenChangePassword = useFormFields(([fields]) => (fields && fields?.password) || null) const dispatchFields = useFormFields((reducer) => reducer[1]) const modified = useFormModified() const { i18n, t } = useTranslation() @@ -70,15 +74,32 @@ export const Auth: React.FC = (props) => { }, [permissions, collectionSlug]) const handleChangePassword = useCallback( - (state: boolean) => { - if (!state) { + (showPasswordFields: boolean) => { + if (showPasswordFields) { + setValidateBeforeSubmit(true) + setSchemaPath(`_${collectionSlug}.auth`) + dispatchFields({ + type: 'UPDATE', + errorMessage: t('validation:required'), + path: 'password', + valid: false, + }) + dispatchFields({ + type: 'UPDATE', + errorMessage: t('validation:required'), + path: 'confirm-password', + valid: false, + }) + } else { + setValidateBeforeSubmit(false) + setSchemaPath(collectionSlug) dispatchFields({ type: 'REMOVE', path: 'password' }) dispatchFields({ type: 'REMOVE', path: 'confirm-password' }) } - setChangingPassword(state) + setChangingPassword(showPasswordFields) }, - [dispatchFields], + [dispatchFields, t, collectionSlug, setSchemaPath, setValidateBeforeSubmit], ) const unlock = useCallback(async () => { @@ -99,7 +120,7 @@ export const Auth: React.FC = (props) => { } else { toast.error(t('authentication:failedToUnlock')) } - }, [i18n, serverURL, api, collectionSlug, email, username, t]) + }, [i18n, serverURL, api, collectionSlug, email, username, t, loginWithUsername]) useEffect(() => { if (!modified) { @@ -113,6 +134,8 @@ export const Auth: React.FC = (props) => { const disabled = readOnly || isInitializing + const showPasswordFields = changingPassword || forceOpenChangePassword + return (
{!disableLocalStrategy && ( @@ -136,22 +159,33 @@ export const Auth: React.FC = (props) => { 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: {}, + }) + } /> )} - {(changingPassword || requirePassword) && ( + {(showPasswordFields || requirePassword) && (
)}
- {changingPassword && !requirePassword && ( + {showPasswordFields && !requirePassword && ( )} - {!changingPassword && !requirePassword && ( + {!showPasswordFields && !requirePassword && (