fix: user validation error inside the forgotPassword operation in the cases where user had localised fields (#12034)
### What?
So, while resetting the password using the Local API, I encountered a
validation error for localized fields. I jumped into the Payload
repository, and saw that `payload.update` is being used in the process,
with no locale specified/supported. This causes errors if the user has
localized fields, but specifying a locale for the password reset
operation would be silly, so I suggest turning this into a db operation,
just like the user fetching operation before.
### How?
I replaced this:
```TS
user = await payload.update({
id: user.id,
collection: collectionConfig.slug,
data: user,
req,
})
```
With this:
```TS
user = await payload.db.updateOne({
id: user.id,
collection: collectionConfig.slug,
data: user,
req,
})
```
So the validation of other fields would be skipped in this operation.
### Why?
This is the error I encountered while trying to reset password, it
blocks my project to go further :)
```bash
Error [ValidationError]: The following field is invalid: Data > Name
at async sendOfferEmail (src/collections/Offers/components/SendEmailButton/index.tsx:18:20)
16 | try {
17 | const payload = await getPayload({ config });
> 18 | const token = await payload.forgotPassword({
| ^
19 | collection: "offers",
20 | data: {
{
data: [Object],
isOperational: true,
isPublic: false,
status: 400,
[cause]: [Object]
}
cause:
{
id: '67f4c1df8aa60189df9bdf5c',
collection: 'offers',
errors: [
{
label: 'Data > Name',
message: 'This field is required.',
path: 'name'
}
],
global: undefined
}
```
P.S The name field is totally fine, it is required and filled with
values in both locales I use, in admin panel I can edit and save
everything without any issues.
<!--
Thank you for the PR! Please go through the checklist below and make
sure you've completed all the steps.
Please review the
[CONTRIBUTING.md](https://github.com/payloadcms/payload/blob/main/CONTRIBUTING.md)
document in this repository if you haven't already.
The following items will ensure that your PR is handled as smoothly as
possible:
- PR Title must follow conventional commits format. For example, `feat:
my new feature`, `fix(plugin-seo): my fix`.
- Minimal description explained as if explained to someone not
immediately familiar with the code.
- Provide before/after screenshots or code diffs if applicable.
- Link any related issues/discussions from GitHub or Discord.
- Add review comments if necessary to explain to the reviewer the logic
behind a change
### What?
### Why?
### How?
Fixes #
-->
This commit is contained in:
51
test/auth/forgot-password-localized/config.ts
Normal file
51
test/auth/forgot-password-localized/config.ts
Normal file
@@ -0,0 +1,51 @@
|
||||
import { fileURLToPath } from 'node:url'
|
||||
import path from 'path'
|
||||
|
||||
import { buildConfigWithDefaults } from '../../buildConfigWithDefaults.js'
|
||||
|
||||
export const collectionSlug = 'users'
|
||||
|
||||
const filename = fileURLToPath(import.meta.url)
|
||||
const dirname = path.dirname(filename)
|
||||
|
||||
export default buildConfigWithDefaults({
|
||||
admin: {
|
||||
user: collectionSlug,
|
||||
importMap: {
|
||||
baseDir: path.resolve(dirname),
|
||||
},
|
||||
},
|
||||
localization: {
|
||||
locales: ['en', 'pl'],
|
||||
defaultLocale: 'en',
|
||||
},
|
||||
collections: [
|
||||
{
|
||||
slug: collectionSlug,
|
||||
auth: {
|
||||
forgotPassword: {
|
||||
// Default options
|
||||
},
|
||||
},
|
||||
fields: [
|
||||
{
|
||||
name: 'localizedField',
|
||||
type: 'text',
|
||||
localized: true, // This field is localized and will require locale during validation
|
||||
required: true,
|
||||
},
|
||||
{
|
||||
name: 'roles',
|
||||
type: 'select',
|
||||
defaultValue: ['user'],
|
||||
hasMany: true,
|
||||
label: 'Role',
|
||||
options: ['admin', 'editor', 'moderator', 'user', 'viewer'],
|
||||
required: true,
|
||||
saveToJWT: true,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
debug: true,
|
||||
})
|
||||
78
test/auth/forgot-password-localized/int.spec.ts
Normal file
78
test/auth/forgot-password-localized/int.spec.ts
Normal file
@@ -0,0 +1,78 @@
|
||||
import type { Payload } from 'payload'
|
||||
|
||||
import path from 'path'
|
||||
import { fileURLToPath } from 'url'
|
||||
|
||||
import type { NextRESTClient } from '../../helpers/NextRESTClient.js'
|
||||
|
||||
import { devUser } from '../../credentials.js'
|
||||
import { initPayloadInt } from '../../helpers/initPayloadInt.js'
|
||||
import { collectionSlug } from './config.js'
|
||||
|
||||
let restClient: NextRESTClient | undefined
|
||||
let payload: Payload | undefined
|
||||
|
||||
const filename = fileURLToPath(import.meta.url)
|
||||
const dirname = path.dirname(filename)
|
||||
|
||||
describe('Forgot password operation with localized fields', () => {
|
||||
beforeAll(async () => {
|
||||
;({ payload, restClient } = await initPayloadInt(dirname, 'auth/forgot-password-localized'))
|
||||
|
||||
// Register a user with additional localized field
|
||||
const res = await restClient?.POST(`/${collectionSlug}/first-register?locale=en`, {
|
||||
body: JSON.stringify({
|
||||
...devUser,
|
||||
'confirm-password': devUser.password,
|
||||
localizedField: 'English content',
|
||||
}),
|
||||
})
|
||||
|
||||
if (!res) {
|
||||
throw new Error('Failed to register user')
|
||||
}
|
||||
|
||||
const { user } = await res.json()
|
||||
|
||||
// @ts-expect-error - Localized field is not in the general Payload type, but it is in mocked collection in this case.
|
||||
await payload?.update({
|
||||
collection: collectionSlug,
|
||||
id: user.id as string,
|
||||
locale: 'pl',
|
||||
data: {
|
||||
localizedField: 'Polish content',
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
afterAll(async () => {
|
||||
if (typeof payload?.db.destroy === 'function') {
|
||||
await payload?.db.destroy()
|
||||
}
|
||||
})
|
||||
|
||||
it('should successfully process forgotPassword operation with localized fields', async () => {
|
||||
// Attempt to trigger forgotPassword operation
|
||||
const token = await payload?.forgotPassword({
|
||||
collection: collectionSlug,
|
||||
data: { email: devUser.email },
|
||||
disableEmail: true,
|
||||
})
|
||||
|
||||
// Verify token was generated successfully
|
||||
expect(token).toBeDefined()
|
||||
expect(typeof token).toBe('string')
|
||||
expect(token?.length).toBeGreaterThan(0)
|
||||
})
|
||||
|
||||
it('should not throw validation errors for localized fields', async () => {
|
||||
// We expect this not to throw an error
|
||||
await expect(
|
||||
payload?.forgotPassword({
|
||||
collection: collectionSlug,
|
||||
data: { email: devUser.email },
|
||||
disableEmail: true,
|
||||
}),
|
||||
).resolves.not.toThrow()
|
||||
})
|
||||
})
|
||||
246
test/auth/forgot-password-localized/payload-types.ts
Normal file
246
test/auth/forgot-password-localized/payload-types.ts
Normal file
@@ -0,0 +1,246 @@
|
||||
/* tslint:disable */
|
||||
/* eslint-disable */
|
||||
/**
|
||||
* This file was automatically generated by Payload.
|
||||
* DO NOT MODIFY IT BY HAND. Instead, modify your source Payload config,
|
||||
* and re-run `payload generate:types` to regenerate this file.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Supported timezones in IANA format.
|
||||
*
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
* via the `definition` "supportedTimezones".
|
||||
*/
|
||||
export type SupportedTimezones =
|
||||
| 'Pacific/Midway'
|
||||
| 'Pacific/Niue'
|
||||
| 'Pacific/Honolulu'
|
||||
| 'Pacific/Rarotonga'
|
||||
| 'America/Anchorage'
|
||||
| 'Pacific/Gambier'
|
||||
| 'America/Los_Angeles'
|
||||
| 'America/Tijuana'
|
||||
| 'America/Denver'
|
||||
| 'America/Phoenix'
|
||||
| 'America/Chicago'
|
||||
| 'America/Guatemala'
|
||||
| 'America/New_York'
|
||||
| 'America/Bogota'
|
||||
| 'America/Caracas'
|
||||
| 'America/Santiago'
|
||||
| 'America/Buenos_Aires'
|
||||
| 'America/Sao_Paulo'
|
||||
| 'Atlantic/South_Georgia'
|
||||
| 'Atlantic/Azores'
|
||||
| 'Atlantic/Cape_Verde'
|
||||
| 'Europe/London'
|
||||
| 'Europe/Berlin'
|
||||
| 'Africa/Lagos'
|
||||
| 'Europe/Athens'
|
||||
| 'Africa/Cairo'
|
||||
| 'Europe/Moscow'
|
||||
| 'Asia/Riyadh'
|
||||
| 'Asia/Dubai'
|
||||
| 'Asia/Baku'
|
||||
| 'Asia/Karachi'
|
||||
| 'Asia/Tashkent'
|
||||
| 'Asia/Calcutta'
|
||||
| 'Asia/Dhaka'
|
||||
| 'Asia/Almaty'
|
||||
| 'Asia/Jakarta'
|
||||
| 'Asia/Bangkok'
|
||||
| 'Asia/Shanghai'
|
||||
| 'Asia/Singapore'
|
||||
| 'Asia/Tokyo'
|
||||
| 'Asia/Seoul'
|
||||
| 'Australia/Brisbane'
|
||||
| 'Australia/Sydney'
|
||||
| 'Pacific/Guam'
|
||||
| 'Pacific/Noumea'
|
||||
| 'Pacific/Auckland'
|
||||
| 'Pacific/Fiji';
|
||||
|
||||
export interface Config {
|
||||
auth: {
|
||||
users: UserAuthOperations;
|
||||
};
|
||||
blocks: {};
|
||||
collections: {
|
||||
users: User;
|
||||
'payload-locked-documents': PayloadLockedDocument;
|
||||
'payload-preferences': PayloadPreference;
|
||||
'payload-migrations': PayloadMigration;
|
||||
};
|
||||
collectionsJoins: {};
|
||||
collectionsSelect: {
|
||||
users: UsersSelect<false> | UsersSelect<true>;
|
||||
'payload-locked-documents': PayloadLockedDocumentsSelect<false> | PayloadLockedDocumentsSelect<true>;
|
||||
'payload-preferences': PayloadPreferencesSelect<false> | PayloadPreferencesSelect<true>;
|
||||
'payload-migrations': PayloadMigrationsSelect<false> | PayloadMigrationsSelect<true>;
|
||||
};
|
||||
db: {
|
||||
defaultIDType: string;
|
||||
};
|
||||
globals: {};
|
||||
globalsSelect: {};
|
||||
locale: 'en' | 'pl';
|
||||
user: User & {
|
||||
collection: 'users';
|
||||
};
|
||||
jobs: {
|
||||
tasks: unknown;
|
||||
workflows: unknown;
|
||||
};
|
||||
}
|
||||
export interface UserAuthOperations {
|
||||
forgotPassword: {
|
||||
email: string;
|
||||
password: string;
|
||||
};
|
||||
login: {
|
||||
email: string;
|
||||
password: string;
|
||||
};
|
||||
registerFirstUser: {
|
||||
email: string;
|
||||
password: string;
|
||||
};
|
||||
unlock: {
|
||||
email: string;
|
||||
password: string;
|
||||
};
|
||||
}
|
||||
/**
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
* via the `definition` "users".
|
||||
*/
|
||||
export interface User {
|
||||
id: string;
|
||||
localizedField: string;
|
||||
roles: ('admin' | 'editor' | 'moderator' | 'user' | 'viewer')[];
|
||||
updatedAt: string;
|
||||
createdAt: string;
|
||||
email: string;
|
||||
resetPasswordToken?: string | null;
|
||||
resetPasswordExpiration?: string | null;
|
||||
salt?: string | null;
|
||||
hash?: string | null;
|
||||
loginAttempts?: number | null;
|
||||
lockUntil?: string | null;
|
||||
password?: string | null;
|
||||
}
|
||||
/**
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
* via the `definition` "payload-locked-documents".
|
||||
*/
|
||||
export interface PayloadLockedDocument {
|
||||
id: string;
|
||||
document?: {
|
||||
relationTo: 'users';
|
||||
value: string | User;
|
||||
} | null;
|
||||
globalSlug?: string | null;
|
||||
user: {
|
||||
relationTo: 'users';
|
||||
value: string | User;
|
||||
};
|
||||
updatedAt: string;
|
||||
createdAt: string;
|
||||
}
|
||||
/**
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
* via the `definition` "payload-preferences".
|
||||
*/
|
||||
export interface PayloadPreference {
|
||||
id: string;
|
||||
user: {
|
||||
relationTo: 'users';
|
||||
value: string | User;
|
||||
};
|
||||
key?: string | null;
|
||||
value?:
|
||||
| {
|
||||
[k: string]: unknown;
|
||||
}
|
||||
| unknown[]
|
||||
| string
|
||||
| number
|
||||
| boolean
|
||||
| null;
|
||||
updatedAt: string;
|
||||
createdAt: string;
|
||||
}
|
||||
/**
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
* via the `definition` "payload-migrations".
|
||||
*/
|
||||
export interface PayloadMigration {
|
||||
id: string;
|
||||
name?: string | null;
|
||||
batch?: number | null;
|
||||
updatedAt: string;
|
||||
createdAt: string;
|
||||
}
|
||||
/**
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
* via the `definition` "users_select".
|
||||
*/
|
||||
export interface UsersSelect<T extends boolean = true> {
|
||||
localizedField?: T;
|
||||
roles?: T;
|
||||
updatedAt?: T;
|
||||
createdAt?: T;
|
||||
email?: T;
|
||||
resetPasswordToken?: T;
|
||||
resetPasswordExpiration?: T;
|
||||
salt?: T;
|
||||
hash?: T;
|
||||
loginAttempts?: T;
|
||||
lockUntil?: T;
|
||||
}
|
||||
/**
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
* via the `definition` "payload-locked-documents_select".
|
||||
*/
|
||||
export interface PayloadLockedDocumentsSelect<T extends boolean = true> {
|
||||
document?: T;
|
||||
globalSlug?: T;
|
||||
user?: T;
|
||||
updatedAt?: T;
|
||||
createdAt?: T;
|
||||
}
|
||||
/**
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
* via the `definition` "payload-preferences_select".
|
||||
*/
|
||||
export interface PayloadPreferencesSelect<T extends boolean = true> {
|
||||
user?: T;
|
||||
key?: T;
|
||||
value?: T;
|
||||
updatedAt?: T;
|
||||
createdAt?: T;
|
||||
}
|
||||
/**
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
* via the `definition` "payload-migrations_select".
|
||||
*/
|
||||
export interface PayloadMigrationsSelect<T extends boolean = true> {
|
||||
name?: T;
|
||||
batch?: T;
|
||||
updatedAt?: T;
|
||||
createdAt?: T;
|
||||
}
|
||||
/**
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
* via the `definition` "auth".
|
||||
*/
|
||||
export interface Auth {
|
||||
[k: string]: unknown;
|
||||
}
|
||||
|
||||
|
||||
declare module 'payload' {
|
||||
// @ts-ignore
|
||||
export interface GeneratedTypes extends Config {}
|
||||
}
|
||||
Reference in New Issue
Block a user