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.
This commit is contained in:
@@ -7,7 +7,6 @@ import * as qs from 'qs-esm'
|
||||
|
||||
import type { Args } from './types.js'
|
||||
|
||||
import { getRequestLocale } from '../getRequestLocale.js'
|
||||
import { initReq } from '../initReq.js'
|
||||
import { getRouteInfo } from './handleAdminPage.js'
|
||||
import { handleAuthRedirect } from './handleAuthRedirect.js'
|
||||
@@ -32,7 +31,7 @@ export const initPage = async ({
|
||||
|
||||
const cookies = parseCookies(headers)
|
||||
|
||||
const { permissions, req } = await initReq(payload.config, {
|
||||
const { locale, permissions, req } = await initReq(payload.config, {
|
||||
fallbackLocale: false,
|
||||
req: {
|
||||
headers,
|
||||
@@ -58,12 +57,6 @@ export const initPage = async ({
|
||||
[],
|
||||
)
|
||||
|
||||
const locale = await getRequestLocale({
|
||||
req,
|
||||
})
|
||||
|
||||
req.locale = locale?.code
|
||||
|
||||
const visibleEntities: VisibleEntities = {
|
||||
collections: collections
|
||||
.map(({ slug, admin: { hidden } }) =>
|
||||
|
||||
@@ -1,12 +1,22 @@
|
||||
import type { I18n, I18nClient } from '@payloadcms/translations'
|
||||
import type { PayloadRequest, SanitizedConfig, SanitizedPermissions } from 'payload'
|
||||
import type { Locale, PayloadRequest, SanitizedConfig, SanitizedPermissions } from 'payload'
|
||||
|
||||
import { initI18n } from '@payloadcms/translations'
|
||||
import { headers as getHeaders } from 'next/headers.js'
|
||||
import { createLocalReq, getPayload, getRequestLanguage, parseCookies } from 'payload'
|
||||
import {
|
||||
createLocalReq,
|
||||
executeAuthStrategies,
|
||||
getAccessResults,
|
||||
getPayload,
|
||||
getRequestLanguage,
|
||||
parseCookies,
|
||||
} from 'payload'
|
||||
import { cache } from 'react'
|
||||
|
||||
import { getRequestLocale } from './getRequestLocale.js'
|
||||
|
||||
type Result = {
|
||||
locale?: Locale
|
||||
permissions: SanitizedPermissions
|
||||
req: PayloadRequest
|
||||
}
|
||||
@@ -33,7 +43,14 @@ export const initReq = cache(async function (
|
||||
language: languageCode,
|
||||
})
|
||||
|
||||
const { permissions, user } = await payload.auth({ headers })
|
||||
/**
|
||||
* 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
|
||||
* I.e. the `payload.auth` function would call `getAccessResults` without a fully-formed `req` object
|
||||
*/
|
||||
const { responseHeaders, user } = await executeAuthStrategies({
|
||||
headers,
|
||||
payload,
|
||||
})
|
||||
|
||||
const { req: reqOverrides, ...optionsOverrides } = overrides || {}
|
||||
|
||||
@@ -43,6 +60,7 @@ export const initReq = cache(async function (
|
||||
headers,
|
||||
host: headers.get('host'),
|
||||
i18n: i18n as I18n,
|
||||
responseHeaders,
|
||||
url: `${payload.config.serverURL}`,
|
||||
user,
|
||||
...(reqOverrides || {}),
|
||||
@@ -52,7 +70,18 @@ export const initReq = cache(async function (
|
||||
payload,
|
||||
)
|
||||
|
||||
const locale = await getRequestLocale({
|
||||
req,
|
||||
})
|
||||
|
||||
req.locale = locale?.code
|
||||
|
||||
const permissions = await getAccessResults({
|
||||
req,
|
||||
})
|
||||
|
||||
return {
|
||||
locale,
|
||||
permissions,
|
||||
req,
|
||||
}
|
||||
|
||||
@@ -2,6 +2,8 @@ import { fileURLToPath } from 'node:url'
|
||||
import path from 'path'
|
||||
const filename = fileURLToPath(import.meta.url)
|
||||
const dirname = path.dirname(filename)
|
||||
import type { CollectionConfig } from 'payload'
|
||||
|
||||
import type { LocalizedPost } from './payload-types.js'
|
||||
|
||||
import { buildConfigWithDefaults } from '../buildConfigWithDefaults.js'
|
||||
@@ -18,6 +20,7 @@ import { RichTextCollection } from './collections/RichText/index.js'
|
||||
import { Tab } from './collections/Tab/index.js'
|
||||
import {
|
||||
blocksWithLocalizedSameName,
|
||||
cannotCreateDefaultLocale,
|
||||
defaultLocale,
|
||||
englishTitle,
|
||||
hungarianLocale,
|
||||
@@ -42,7 +45,7 @@ export type LocalizedPostAllLocale = {
|
||||
}
|
||||
} & LocalizedPost
|
||||
|
||||
const openAccess = {
|
||||
const openAccess: CollectionConfig['access'] = {
|
||||
create: () => true,
|
||||
delete: () => true,
|
||||
read: () => true,
|
||||
@@ -258,14 +261,14 @@ export default buildConfigWithDefaults({
|
||||
// Relation multiple relationTo
|
||||
{
|
||||
name: 'localizedRelationMultiRelationTo',
|
||||
relationTo: [localizedPostsSlug, 'dummy'],
|
||||
relationTo: [localizedPostsSlug, cannotCreateDefaultLocale],
|
||||
type: 'relationship',
|
||||
},
|
||||
// Relation multiple relationTo hasMany
|
||||
{
|
||||
name: 'localizedRelationMultiRelationToHasMany',
|
||||
hasMany: true,
|
||||
relationTo: [localizedPostsSlug, 'dummy'],
|
||||
relationTo: [localizedPostsSlug, cannotCreateDefaultLocale],
|
||||
type: 'relationship',
|
||||
},
|
||||
],
|
||||
@@ -289,14 +292,14 @@ export default buildConfigWithDefaults({
|
||||
{
|
||||
name: 'relationMultiRelationTo',
|
||||
localized: true,
|
||||
relationTo: [localizedPostsSlug, 'dummy'],
|
||||
relationTo: [localizedPostsSlug, cannotCreateDefaultLocale],
|
||||
type: 'relationship',
|
||||
},
|
||||
{
|
||||
name: 'relationMultiRelationToHasMany',
|
||||
hasMany: true,
|
||||
localized: true,
|
||||
relationTo: [localizedPostsSlug, 'dummy'],
|
||||
relationTo: [localizedPostsSlug, cannotCreateDefaultLocale],
|
||||
type: 'relationship',
|
||||
},
|
||||
{
|
||||
@@ -317,14 +320,17 @@ export default buildConfigWithDefaults({
|
||||
slug: relationshipLocalizedSlug,
|
||||
},
|
||||
{
|
||||
access: openAccess,
|
||||
access: {
|
||||
...openAccess,
|
||||
create: ({ req }) => req.locale !== defaultLocale,
|
||||
},
|
||||
fields: [
|
||||
{
|
||||
name: 'name',
|
||||
type: 'text',
|
||||
},
|
||||
],
|
||||
slug: 'dummy',
|
||||
slug: cannotCreateDefaultLocale,
|
||||
},
|
||||
NestedToArrayAndBlock,
|
||||
Group,
|
||||
|
||||
@@ -36,6 +36,7 @@ import { navigateToDoc } from 'helpers/e2e/navigateToDoc.js'
|
||||
import { upsertPrefs } from 'helpers/e2e/upsertPrefs.js'
|
||||
import { RESTClient } from 'helpers/rest.js'
|
||||
import { GeneratedTypes } from 'helpers/sdk/types.js'
|
||||
import { wait } from 'payload/shared'
|
||||
const filename = fileURLToPath(import.meta.url)
|
||||
const dirname = path.dirname(filename)
|
||||
|
||||
@@ -52,6 +53,7 @@ const { beforeAll, beforeEach, describe, afterEach } = test
|
||||
let url: AdminUrlUtil
|
||||
let urlWithRequiredLocalizedFields: AdminUrlUtil
|
||||
let urlRelationshipLocalized: AdminUrlUtil
|
||||
let urlCannotCreateDefaultLocale: AdminUrlUtil
|
||||
|
||||
const title = 'english title'
|
||||
const spanishTitle = 'spanish title'
|
||||
@@ -74,6 +76,7 @@ describe('Localization', () => {
|
||||
urlRelationshipLocalized = new AdminUrlUtil(serverURL, relationshipLocalizedSlug)
|
||||
richTextURL = new AdminUrlUtil(serverURL, richTextSlug)
|
||||
urlWithRequiredLocalizedFields = new AdminUrlUtil(serverURL, withRequiredLocalizedFields)
|
||||
urlCannotCreateDefaultLocale = new AdminUrlUtil(serverURL, 'cannot-create-default-locale')
|
||||
|
||||
context = await browser.newContext()
|
||||
page = await context.newPage()
|
||||
@@ -122,6 +125,29 @@ describe('Localization', () => {
|
||||
})
|
||||
})
|
||||
|
||||
describe('access control', () => {
|
||||
test('should have req.locale within access control', async () => {
|
||||
await changeLocale(page, defaultLocale)
|
||||
await page.goto(urlCannotCreateDefaultLocale.list)
|
||||
|
||||
const createNewButtonLocator =
|
||||
'.collection-list a[href="/admin/collections/cannot-create-default-locale/create"]'
|
||||
|
||||
await expect(page.locator(createNewButtonLocator)).not.toBeVisible()
|
||||
await changeLocale(page, spanishLocale)
|
||||
await expect(page.locator(createNewButtonLocator).first()).toBeVisible()
|
||||
await page.goto(urlCannotCreateDefaultLocale.create)
|
||||
await expect(page.locator('#field-name')).toBeVisible()
|
||||
await changeLocale(page, defaultLocale)
|
||||
|
||||
await expect(
|
||||
page.locator('h1', {
|
||||
hasText: 'Unauthorized',
|
||||
}),
|
||||
).toBeVisible()
|
||||
})
|
||||
})
|
||||
|
||||
describe('localized text', () => {
|
||||
test('create english post, switch to spanish', async () => {
|
||||
await changeLocale(page, defaultLocale)
|
||||
|
||||
@@ -22,7 +22,7 @@ export interface Config {
|
||||
'localized-required': LocalizedRequired;
|
||||
'with-localized-relationship': WithLocalizedRelationship;
|
||||
'relationship-localized': RelationshipLocalized;
|
||||
dummy: Dummy;
|
||||
'cannot-create-default-locale': CannotCreateDefaultLocale;
|
||||
nested: Nested;
|
||||
groups: Group;
|
||||
tabs: Tab;
|
||||
@@ -46,7 +46,7 @@ export interface Config {
|
||||
'localized-required': LocalizedRequiredSelect<false> | LocalizedRequiredSelect<true>;
|
||||
'with-localized-relationship': WithLocalizedRelationshipSelect<false> | WithLocalizedRelationshipSelect<true>;
|
||||
'relationship-localized': RelationshipLocalizedSelect<false> | RelationshipLocalizedSelect<true>;
|
||||
dummy: DummySelect<false> | DummySelect<true>;
|
||||
'cannot-create-default-locale': CannotCreateDefaultLocaleSelect<false> | CannotCreateDefaultLocaleSelect<true>;
|
||||
nested: NestedSelect<false> | NestedSelect<true>;
|
||||
groups: GroupsSelect<false> | GroupsSelect<true>;
|
||||
tabs: TabsSelect<false> | TabsSelect<true>;
|
||||
@@ -378,8 +378,8 @@ export interface WithLocalizedRelationship {
|
||||
value: string | LocalizedPost;
|
||||
} | null)
|
||||
| ({
|
||||
relationTo: 'dummy';
|
||||
value: string | Dummy;
|
||||
relationTo: 'cannot-create-default-locale';
|
||||
value: string | CannotCreateDefaultLocale;
|
||||
} | null);
|
||||
localizedRelationMultiRelationToHasMany?:
|
||||
| (
|
||||
@@ -388,8 +388,8 @@ export interface WithLocalizedRelationship {
|
||||
value: string | LocalizedPost;
|
||||
}
|
||||
| {
|
||||
relationTo: 'dummy';
|
||||
value: string | Dummy;
|
||||
relationTo: 'cannot-create-default-locale';
|
||||
value: string | CannotCreateDefaultLocale;
|
||||
}
|
||||
)[]
|
||||
| null;
|
||||
@@ -398,9 +398,9 @@ export interface WithLocalizedRelationship {
|
||||
}
|
||||
/**
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
* via the `definition` "dummy".
|
||||
* via the `definition` "cannot-create-default-locale".
|
||||
*/
|
||||
export interface Dummy {
|
||||
export interface CannotCreateDefaultLocale {
|
||||
id: string;
|
||||
name?: string | null;
|
||||
updatedAt: string;
|
||||
@@ -420,8 +420,8 @@ export interface RelationshipLocalized {
|
||||
value: string | LocalizedPost;
|
||||
} | null)
|
||||
| ({
|
||||
relationTo: 'dummy';
|
||||
value: string | Dummy;
|
||||
relationTo: 'cannot-create-default-locale';
|
||||
value: string | CannotCreateDefaultLocale;
|
||||
} | null);
|
||||
relationMultiRelationToHasMany?:
|
||||
| (
|
||||
@@ -430,8 +430,8 @@ export interface RelationshipLocalized {
|
||||
value: string | LocalizedPost;
|
||||
}
|
||||
| {
|
||||
relationTo: 'dummy';
|
||||
value: string | Dummy;
|
||||
relationTo: 'cannot-create-default-locale';
|
||||
value: string | CannotCreateDefaultLocale;
|
||||
}
|
||||
)[]
|
||||
| null;
|
||||
@@ -668,8 +668,8 @@ export interface PayloadLockedDocument {
|
||||
value: string | RelationshipLocalized;
|
||||
} | null)
|
||||
| ({
|
||||
relationTo: 'dummy';
|
||||
value: string | Dummy;
|
||||
relationTo: 'cannot-create-default-locale';
|
||||
value: string | CannotCreateDefaultLocale;
|
||||
} | null)
|
||||
| ({
|
||||
relationTo: 'nested';
|
||||
@@ -1027,9 +1027,9 @@ export interface RelationshipLocalizedSelect<T extends boolean = true> {
|
||||
}
|
||||
/**
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
* via the `definition` "dummy_select".
|
||||
* via the `definition` "cannot-create-default-locale_select".
|
||||
*/
|
||||
export interface DummySelect<T extends boolean = true> {
|
||||
export interface CannotCreateDefaultLocaleSelect<T extends boolean = true> {
|
||||
name?: T;
|
||||
updatedAt?: T;
|
||||
createdAt?: T;
|
||||
|
||||
@@ -18,3 +18,4 @@ export const withRequiredLocalizedFields = 'localized-required'
|
||||
export const localizedSortSlug = 'localized-sort'
|
||||
export const usersSlug = 'users'
|
||||
export const blocksWithLocalizedSameName = 'blocks-same-name'
|
||||
export const cannotCreateDefaultLocale = 'cannot-create-default-locale'
|
||||
|
||||
@@ -28,7 +28,7 @@
|
||||
}
|
||||
],
|
||||
"paths": {
|
||||
"@payload-config": ["./test/_community/config.ts"],
|
||||
"@payload-config": ["./test/localization/config.ts"],
|
||||
"@payloadcms/live-preview": ["./packages/live-preview/src"],
|
||||
"@payloadcms/live-preview-react": ["./packages/live-preview-react/src/index.ts"],
|
||||
"@payloadcms/live-preview-vue": ["./packages/live-preview-vue/src/index.ts"],
|
||||
|
||||
Reference in New Issue
Block a user