Skip to content

Commit afcc970

Browse files
authored
fix(next): ensures req.locale is populated before running access control (#10533)
Fixes #10529. The `req.locale` property within collection and global access control functions does not reflect the current locale. This was because we were attaching the locale to the req only _after_ running `payload.auth`, which attempts to get access control without a fully-formed req. The fix is to first authenticate the user using the `executeAuthStrategies` operation directly, then determine the request locale with that user, and finally get access results with the proper locale.
1 parent 6b051bd commit afcc970

File tree

7 files changed

+90
-35
lines changed

7 files changed

+90
-35
lines changed

packages/next/src/utilities/initPage/index.ts

Lines changed: 1 addition & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,6 @@ import * as qs from 'qs-esm'
77

88
import type { Args } from './types.js'
99

10-
import { getRequestLocale } from '../getRequestLocale.js'
1110
import { initReq } from '../initReq.js'
1211
import { getRouteInfo } from './handleAdminPage.js'
1312
import { handleAuthRedirect } from './handleAuthRedirect.js'
@@ -32,7 +31,7 @@ export const initPage = async ({
3231

3332
const cookies = parseCookies(headers)
3433

35-
const { permissions, req } = await initReq(payload.config, {
34+
const { locale, permissions, req } = await initReq(payload.config, {
3635
fallbackLocale: false,
3736
req: {
3837
headers,
@@ -58,12 +57,6 @@ export const initPage = async ({
5857
[],
5958
)
6059

61-
const locale = await getRequestLocale({
62-
req,
63-
})
64-
65-
req.locale = locale?.code
66-
6760
const visibleEntities: VisibleEntities = {
6861
collections: collections
6962
.map(({ slug, admin: { hidden } }) =>

packages/next/src/utilities/initReq.ts

Lines changed: 32 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,22 @@
11
import type { I18n, I18nClient } from '@payloadcms/translations'
2-
import type { PayloadRequest, SanitizedConfig, SanitizedPermissions } from 'payload'
2+
import type { Locale, PayloadRequest, SanitizedConfig, SanitizedPermissions } from 'payload'
33

44
import { initI18n } from '@payloadcms/translations'
55
import { headers as getHeaders } from 'next/headers.js'
6-
import { createLocalReq, getPayload, getRequestLanguage, parseCookies } from 'payload'
6+
import {
7+
createLocalReq,
8+
executeAuthStrategies,
9+
getAccessResults,
10+
getPayload,
11+
getRequestLanguage,
12+
parseCookies,
13+
} from 'payload'
714
import { cache } from 'react'
815

16+
import { getRequestLocale } from './getRequestLocale.js'
17+
918
type Result = {
19+
locale?: Locale
1020
permissions: SanitizedPermissions
1121
req: PayloadRequest
1222
}
@@ -33,7 +43,14 @@ export const initReq = cache(async function (
3343
language: languageCode,
3444
})
3545

36-
const { permissions, user } = await payload.auth({ headers })
46+
/**
47+
* Cannot simply call `payload.auth` here, as we need the user to get the locale, and we need the locale to get the access results
48+
* I.e. the `payload.auth` function would call `getAccessResults` without a fully-formed `req` object
49+
*/
50+
const { responseHeaders, user } = await executeAuthStrategies({
51+
headers,
52+
payload,
53+
})
3754

3855
const { req: reqOverrides, ...optionsOverrides } = overrides || {}
3956

@@ -43,6 +60,7 @@ export const initReq = cache(async function (
4360
headers,
4461
host: headers.get('host'),
4562
i18n: i18n as I18n,
63+
responseHeaders,
4664
url: `${payload.config.serverURL}`,
4765
user,
4866
...(reqOverrides || {}),
@@ -52,7 +70,18 @@ export const initReq = cache(async function (
5270
payload,
5371
)
5472

73+
const locale = await getRequestLocale({
74+
req,
75+
})
76+
77+
req.locale = locale?.code
78+
79+
const permissions = await getAccessResults({
80+
req,
81+
})
82+
5583
return {
84+
locale,
5685
permissions,
5786
req,
5887
}

test/localization/config.ts

Lines changed: 13 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@ import { fileURLToPath } from 'node:url'
22
import path from 'path'
33
const filename = fileURLToPath(import.meta.url)
44
const dirname = path.dirname(filename)
5+
import type { CollectionConfig } from 'payload'
6+
57
import type { LocalizedPost } from './payload-types.js'
68

79
import { buildConfigWithDefaults } from '../buildConfigWithDefaults.js'
@@ -18,6 +20,7 @@ import { RichTextCollection } from './collections/RichText/index.js'
1820
import { Tab } from './collections/Tab/index.js'
1921
import {
2022
blocksWithLocalizedSameName,
23+
cannotCreateDefaultLocale,
2124
defaultLocale,
2225
englishTitle,
2326
hungarianLocale,
@@ -42,7 +45,7 @@ export type LocalizedPostAllLocale = {
4245
}
4346
} & LocalizedPost
4447

45-
const openAccess = {
48+
const openAccess: CollectionConfig['access'] = {
4649
create: () => true,
4750
delete: () => true,
4851
read: () => true,
@@ -258,14 +261,14 @@ export default buildConfigWithDefaults({
258261
// Relation multiple relationTo
259262
{
260263
name: 'localizedRelationMultiRelationTo',
261-
relationTo: [localizedPostsSlug, 'dummy'],
264+
relationTo: [localizedPostsSlug, cannotCreateDefaultLocale],
262265
type: 'relationship',
263266
},
264267
// Relation multiple relationTo hasMany
265268
{
266269
name: 'localizedRelationMultiRelationToHasMany',
267270
hasMany: true,
268-
relationTo: [localizedPostsSlug, 'dummy'],
271+
relationTo: [localizedPostsSlug, cannotCreateDefaultLocale],
269272
type: 'relationship',
270273
},
271274
],
@@ -289,14 +292,14 @@ export default buildConfigWithDefaults({
289292
{
290293
name: 'relationMultiRelationTo',
291294
localized: true,
292-
relationTo: [localizedPostsSlug, 'dummy'],
295+
relationTo: [localizedPostsSlug, cannotCreateDefaultLocale],
293296
type: 'relationship',
294297
},
295298
{
296299
name: 'relationMultiRelationToHasMany',
297300
hasMany: true,
298301
localized: true,
299-
relationTo: [localizedPostsSlug, 'dummy'],
302+
relationTo: [localizedPostsSlug, cannotCreateDefaultLocale],
300303
type: 'relationship',
301304
},
302305
{
@@ -317,14 +320,17 @@ export default buildConfigWithDefaults({
317320
slug: relationshipLocalizedSlug,
318321
},
319322
{
320-
access: openAccess,
323+
access: {
324+
...openAccess,
325+
create: ({ req }) => req.locale !== defaultLocale,
326+
},
321327
fields: [
322328
{
323329
name: 'name',
324330
type: 'text',
325331
},
326332
],
327-
slug: 'dummy',
333+
slug: cannotCreateDefaultLocale,
328334
},
329335
NestedToArrayAndBlock,
330336
Group,

test/localization/e2e.spec.ts

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ import { navigateToDoc } from 'helpers/e2e/navigateToDoc.js'
3636
import { upsertPrefs } from 'helpers/e2e/upsertPrefs.js'
3737
import { RESTClient } from 'helpers/rest.js'
3838
import { GeneratedTypes } from 'helpers/sdk/types.js'
39+
import { wait } from 'payload/shared'
3940
const filename = fileURLToPath(import.meta.url)
4041
const dirname = path.dirname(filename)
4142

@@ -52,6 +53,7 @@ const { beforeAll, beforeEach, describe, afterEach } = test
5253
let url: AdminUrlUtil
5354
let urlWithRequiredLocalizedFields: AdminUrlUtil
5455
let urlRelationshipLocalized: AdminUrlUtil
56+
let urlCannotCreateDefaultLocale: AdminUrlUtil
5557

5658
const title = 'english title'
5759
const spanishTitle = 'spanish title'
@@ -74,6 +76,7 @@ describe('Localization', () => {
7476
urlRelationshipLocalized = new AdminUrlUtil(serverURL, relationshipLocalizedSlug)
7577
richTextURL = new AdminUrlUtil(serverURL, richTextSlug)
7678
urlWithRequiredLocalizedFields = new AdminUrlUtil(serverURL, withRequiredLocalizedFields)
79+
urlCannotCreateDefaultLocale = new AdminUrlUtil(serverURL, 'cannot-create-default-locale')
7780

7881
context = await browser.newContext()
7982
page = await context.newPage()
@@ -122,6 +125,29 @@ describe('Localization', () => {
122125
})
123126
})
124127

128+
describe('access control', () => {
129+
test('should have req.locale within access control', async () => {
130+
await changeLocale(page, defaultLocale)
131+
await page.goto(urlCannotCreateDefaultLocale.list)
132+
133+
const createNewButtonLocator =
134+
'.collection-list a[href="/admin/collections/cannot-create-default-locale/create"]'
135+
136+
await expect(page.locator(createNewButtonLocator)).not.toBeVisible()
137+
await changeLocale(page, spanishLocale)
138+
await expect(page.locator(createNewButtonLocator).first()).toBeVisible()
139+
await page.goto(urlCannotCreateDefaultLocale.create)
140+
await expect(page.locator('#field-name')).toBeVisible()
141+
await changeLocale(page, defaultLocale)
142+
143+
await expect(
144+
page.locator('h1', {
145+
hasText: 'Unauthorized',
146+
}),
147+
).toBeVisible()
148+
})
149+
})
150+
125151
describe('localized text', () => {
126152
test('create english post, switch to spanish', async () => {
127153
await changeLocale(page, defaultLocale)

test/localization/payload-types.ts

Lines changed: 16 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ export interface Config {
2222
'localized-required': LocalizedRequired;
2323
'with-localized-relationship': WithLocalizedRelationship;
2424
'relationship-localized': RelationshipLocalized;
25-
dummy: Dummy;
25+
'cannot-create-default-locale': CannotCreateDefaultLocale;
2626
nested: Nested;
2727
groups: Group;
2828
tabs: Tab;
@@ -46,7 +46,7 @@ export interface Config {
4646
'localized-required': LocalizedRequiredSelect<false> | LocalizedRequiredSelect<true>;
4747
'with-localized-relationship': WithLocalizedRelationshipSelect<false> | WithLocalizedRelationshipSelect<true>;
4848
'relationship-localized': RelationshipLocalizedSelect<false> | RelationshipLocalizedSelect<true>;
49-
dummy: DummySelect<false> | DummySelect<true>;
49+
'cannot-create-default-locale': CannotCreateDefaultLocaleSelect<false> | CannotCreateDefaultLocaleSelect<true>;
5050
nested: NestedSelect<false> | NestedSelect<true>;
5151
groups: GroupsSelect<false> | GroupsSelect<true>;
5252
tabs: TabsSelect<false> | TabsSelect<true>;
@@ -378,8 +378,8 @@ export interface WithLocalizedRelationship {
378378
value: string | LocalizedPost;
379379
} | null)
380380
| ({
381-
relationTo: 'dummy';
382-
value: string | Dummy;
381+
relationTo: 'cannot-create-default-locale';
382+
value: string | CannotCreateDefaultLocale;
383383
} | null);
384384
localizedRelationMultiRelationToHasMany?:
385385
| (
@@ -388,8 +388,8 @@ export interface WithLocalizedRelationship {
388388
value: string | LocalizedPost;
389389
}
390390
| {
391-
relationTo: 'dummy';
392-
value: string | Dummy;
391+
relationTo: 'cannot-create-default-locale';
392+
value: string | CannotCreateDefaultLocale;
393393
}
394394
)[]
395395
| null;
@@ -398,9 +398,9 @@ export interface WithLocalizedRelationship {
398398
}
399399
/**
400400
* This interface was referenced by `Config`'s JSON-Schema
401-
* via the `definition` "dummy".
401+
* via the `definition` "cannot-create-default-locale".
402402
*/
403-
export interface Dummy {
403+
export interface CannotCreateDefaultLocale {
404404
id: string;
405405
name?: string | null;
406406
updatedAt: string;
@@ -420,8 +420,8 @@ export interface RelationshipLocalized {
420420
value: string | LocalizedPost;
421421
} | null)
422422
| ({
423-
relationTo: 'dummy';
424-
value: string | Dummy;
423+
relationTo: 'cannot-create-default-locale';
424+
value: string | CannotCreateDefaultLocale;
425425
} | null);
426426
relationMultiRelationToHasMany?:
427427
| (
@@ -430,8 +430,8 @@ export interface RelationshipLocalized {
430430
value: string | LocalizedPost;
431431
}
432432
| {
433-
relationTo: 'dummy';
434-
value: string | Dummy;
433+
relationTo: 'cannot-create-default-locale';
434+
value: string | CannotCreateDefaultLocale;
435435
}
436436
)[]
437437
| null;
@@ -668,8 +668,8 @@ export interface PayloadLockedDocument {
668668
value: string | RelationshipLocalized;
669669
} | null)
670670
| ({
671-
relationTo: 'dummy';
672-
value: string | Dummy;
671+
relationTo: 'cannot-create-default-locale';
672+
value: string | CannotCreateDefaultLocale;
673673
} | null)
674674
| ({
675675
relationTo: 'nested';
@@ -1027,9 +1027,9 @@ export interface RelationshipLocalizedSelect<T extends boolean = true> {
10271027
}
10281028
/**
10291029
* This interface was referenced by `Config`'s JSON-Schema
1030-
* via the `definition` "dummy_select".
1030+
* via the `definition` "cannot-create-default-locale_select".
10311031
*/
1032-
export interface DummySelect<T extends boolean = true> {
1032+
export interface CannotCreateDefaultLocaleSelect<T extends boolean = true> {
10331033
name?: T;
10341034
updatedAt?: T;
10351035
createdAt?: T;

test/localization/shared.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,3 +18,4 @@ export const withRequiredLocalizedFields = 'localized-required'
1818
export const localizedSortSlug = 'localized-sort'
1919
export const usersSlug = 'users'
2020
export const blocksWithLocalizedSameName = 'blocks-same-name'
21+
export const cannotCreateDefaultLocale = 'cannot-create-default-locale'

tsconfig.base.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@
2828
}
2929
],
3030
"paths": {
31-
"@payload-config": ["./test/_community/config.ts"],
31+
"@payload-config": ["./test/localization/config.ts"],
3232
"@payloadcms/live-preview": ["./packages/live-preview/src"],
3333
"@payloadcms/live-preview-react": ["./packages/live-preview-react/src/index.ts"],
3434
"@payloadcms/live-preview-vue": ["./packages/live-preview-vue/src/index.ts"],

0 commit comments

Comments
 (0)