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.
This commit is contained in:
Jarrod Flesch
2024-07-31 14:55:08 -04:00
committed by GitHub
parent 3d89508ce3
commit 290ffd3287
26 changed files with 430 additions and 209 deletions

View File

@@ -31,6 +31,9 @@ export interface Config {
'payload-preferences': PayloadPreference;
'payload-migrations': PayloadMigration;
};
db: {
defaultIDType: string;
};
globals: {
settings: Setting;
test: Test;
@@ -52,26 +55,32 @@ export interface UserAuthOperations {
email: string;
};
login: {
password: string;
email: string;
password: string;
};
registerFirstUser: {
email: string;
password: string;
};
unlock: {
email: string;
};
}
export interface NonAdminUserAuthOperations {
forgotPassword: {
email: string;
};
login: {
password: string;
email: string;
password: string;
};
registerFirstUser: {
email: string;
password: string;
};
unlock: {
email: string;
};
}
/**
* This interface was referenced by `Config`'s JSON-Schema

View File

@@ -13,6 +13,7 @@ import type { Config } from './payload-types.js'
import {
ensureCompilationIsDone,
exactText,
getRoutes,
initPageConsoleErrorCatch,
saveDocAndAssert,
@@ -34,8 +35,6 @@ const headers = {
}
const createFirstUser = async ({
customAdminRoutes,
customRoutes,
page,
serverURL,
}: {
@@ -49,19 +48,35 @@ const createFirstUser = async ({
routes: { createFirstUser: createFirstUserRoute },
},
routes: { admin: adminRoute },
} = getRoutes({
customAdminRoutes,
customRoutes,
})
} = getRoutes({})
// wait for create first user route
await page.goto(serverURL + `${adminRoute}${createFirstUserRoute}`)
// forget to fill out confirm password
await page.locator('#field-email').fill(devUser.email)
await page.locator('#field-password').fill(devUser.password)
await wait(500)
await page.locator('.form-submit > button').click()
await expect(page.locator('.field-type.confirm-password .field-error')).toHaveText(
'This field is required.',
)
// make them match, but does not pass password validation
await page.locator('#field-email').fill(devUser.email)
await page.locator('#field-password').fill('12')
await page.locator('#field-confirm-password').fill('12')
await wait(500)
await page.locator('.form-submit > button').click()
await expect(page.locator('.field-type.password .field-error')).toHaveText(
'This value must be longer than the minimum length of 3 characters.',
)
await page.locator('#field-email').fill(devUser.email)
await page.locator('#field-password').fill(devUser.password)
await page.locator('#field-confirm-password').fill(devUser.password)
await page.locator('#field-custom').fill('Hello, world!')
await wait(500)
await page.locator('.form-submit > button').click()
await expect
@@ -109,7 +124,7 @@ describe('auth', () => {
})
describe('authenticated users', () => {
beforeAll(({ browser }) => {
beforeAll(() => {
url = new AdminUrlUtil(serverURL, slug)
})
@@ -118,15 +133,35 @@ describe('auth', () => {
const emailBeforeSave = await page.locator('#field-email').inputValue()
await page.locator('#change-password').click()
await page.locator('#field-password').fill('password')
// should fail to save without confirm password
await page.locator('#action-save').click()
await expect(
page.locator('.field-type.confirm-password .tooltip--show', {
hasText: exactText('This field is required.'),
}),
).toBeVisible()
// should fail to save with incorrect confirm password
await page.locator('#field-confirm-password').fill('wrong password')
await page.locator('#action-save').click()
await expect(
page.locator('.field-type.confirm-password .tooltip--show', {
hasText: exactText('Passwords do not match.'),
}),
).toBeVisible()
// should succeed with matching confirm password
await page.locator('#field-confirm-password').fill('password')
await saveDocAndAssert(page)
await saveDocAndAssert(page, '#action-save')
// should still have the same email
await expect(page.locator('#field-email')).toHaveValue(emailBeforeSave)
})
test('should have up-to-date user in `useAuth` hook', async () => {
await page.goto(url.account)
await page.waitForURL(url.account)
await expect(page.locator('#users-api-result')).toHaveText('Hello, world!')
await expect(page.locator('#users-api-result')).toHaveText('')
await expect(page.locator('#use-auth-result')).toHaveText('Hello, world!')
const field = page.locator('#field-custom')
await field.fill('Goodbye, world!')

View File

@@ -20,6 +20,9 @@ export interface Config {
'payload-preferences': PayloadPreference;
'payload-migrations': PayloadMigration;
};
db: {
defaultIDType: string;
};
globals: {};
locale: null;
user:
@@ -38,39 +41,48 @@ export interface UserAuthOperations {
email: string;
};
login: {
password: string;
email: string;
password: string;
};
registerFirstUser: {
email: string;
password: string;
};
unlock: {
email: string;
};
}
export interface ApiKeyAuthOperations {
forgotPassword: {
email: string;
};
login: {
password: string;
email: string;
password: string;
};
registerFirstUser: {
email: string;
password: string;
};
unlock: {
email: string;
};
}
export interface PublicUserAuthOperations {
forgotPassword: {
email: string;
};
login: {
password: string;
email: string;
password: string;
};
registerFirstUser: {
email: string;
password: string;
};
unlock: {
email: string;
};
}
/**
* This interface was referenced by `Config`'s JSON-Schema
@@ -207,6 +219,6 @@ export interface Auth {
declare module 'payload' {
// @ts-ignore
// @ts-ignore
export interface GeneratedTypes extends Config {}
}
}

View File

@@ -3,6 +3,7 @@ import type { Payload } from 'payload'
import { fileURLToPath } from 'node:url'
import path from 'path'
import { getFileByPath, mapAsync } from 'payload'
import { wait } from 'payload/shared'
import type { NextRESTClient } from '../helpers/NextRESTClient.js'
import type { Post } from './payload-types.js'
@@ -634,17 +635,16 @@ describe('collections-graphql', () => {
it('should sort find results by nearest distance', async () => {
// creating twice as many records as we are querying to get a random sample
await mapAsync([...Array(10)], () => {
// setTimeout used to randomize the creation timestamp
setTimeout(async () => {
await payload.create({
collection: pointSlug,
data: {
// only randomize longitude to make distance comparison easy
point: [Math.random(), 0],
},
})
}, Math.random())
await mapAsync([...Array(10)], async () => {
// randomize the creation timestamp
await wait(Math.random())
await payload.create({
collection: pointSlug,
data: {
// only randomize longitude to make distance comparison easy
point: [Math.random(), 0],
},
})
})
const nearQuery = `
@@ -1185,7 +1185,7 @@ describe('collections-graphql', () => {
expect(errors[0].message).toEqual('The following field is invalid: password')
expect(errors[0].path[0]).toEqual('test2')
expect(errors[0].extensions.name).toEqual('ValidationError')
expect(errors[0].extensions.data.errors[0].message).toEqual('No password was given')
expect(errors[0].extensions.data.errors[0].message).toEqual('This field is required.')
expect(errors[0].extensions.data.errors[0].field).toEqual('password')
expect(Array.isArray(errors[1].locations)).toEqual(true)