fix(next): infinite loop when logging into root admin (#7412)
This commit is contained in:
@@ -7,7 +7,7 @@ import type {
|
|||||||
|
|
||||||
import { notFound } from 'next/navigation.js'
|
import { notFound } from 'next/navigation.js'
|
||||||
|
|
||||||
import { isAdminAuthRoute, isAdminRoute } from './shared.js'
|
import { getRouteWithoutAdmin, isAdminAuthRoute, isAdminRoute } from './shared.js'
|
||||||
|
|
||||||
export const handleAdminPage = ({
|
export const handleAdminPage = ({
|
||||||
adminRoute,
|
adminRoute,
|
||||||
@@ -20,9 +20,9 @@ export const handleAdminPage = ({
|
|||||||
permissions: Permissions
|
permissions: Permissions
|
||||||
route: string
|
route: string
|
||||||
}) => {
|
}) => {
|
||||||
if (isAdminRoute(route, adminRoute)) {
|
if (isAdminRoute({ adminRoute, config, route })) {
|
||||||
const baseAdminRoute = adminRoute && adminRoute !== '/' ? route.replace(adminRoute, '') : route
|
const routeWithoutAdmin = getRouteWithoutAdmin({ adminRoute, route })
|
||||||
const routeSegments = baseAdminRoute.split('/').filter(Boolean)
|
const routeSegments = routeWithoutAdmin.split('/').filter(Boolean)
|
||||||
const [entityType, entitySlug, createOrID] = routeSegments
|
const [entityType, entitySlug, createOrID] = routeSegments
|
||||||
const collectionSlug = entityType === 'collections' ? entitySlug : undefined
|
const collectionSlug = entityType === 'collections' ? entitySlug : undefined
|
||||||
const globalSlug = entityType === 'globals' ? entitySlug : undefined
|
const globalSlug = entityType === 'globals' ? entitySlug : undefined
|
||||||
@@ -47,7 +47,7 @@ export const handleAdminPage = ({
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!permissions.canAccessAdmin && !isAdminAuthRoute(config, route, adminRoute)) {
|
if (!permissions.canAccessAdmin && !isAdminAuthRoute({ adminRoute, config, route })) {
|
||||||
notFound()
|
notFound()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ export const handleAuthRedirect = ({
|
|||||||
routes: { admin: adminRoute },
|
routes: { admin: adminRoute },
|
||||||
} = config
|
} = config
|
||||||
|
|
||||||
if (!isAdminAuthRoute(config, route, adminRoute)) {
|
if (!isAdminAuthRoute({ adminRoute, config, route })) {
|
||||||
if (searchParams && 'redirect' in searchParams) delete searchParams.redirect
|
if (searchParams && 'redirect' in searchParams) delete searchParams.redirect
|
||||||
|
|
||||||
const redirectRoute = encodeURIComponent(
|
const redirectRoute = encodeURIComponent(
|
||||||
@@ -36,7 +36,7 @@ export const handleAuthRedirect = ({
|
|||||||
const customLoginRoute =
|
const customLoginRoute =
|
||||||
typeof redirectUnauthenticatedUser === 'string' ? redirectUnauthenticatedUser : undefined
|
typeof redirectUnauthenticatedUser === 'string' ? redirectUnauthenticatedUser : undefined
|
||||||
|
|
||||||
const loginRoute = isAdminRoute(route, adminRoute)
|
const loginRoute = isAdminRoute({ adminRoute, config, route })
|
||||||
? adminLoginRoute
|
? adminLoginRoute
|
||||||
: customLoginRoute || loginRouteFromConfig
|
: customLoginRoute || loginRouteFromConfig
|
||||||
|
|
||||||
|
|||||||
@@ -11,16 +11,42 @@ const authRouteKeys: (keyof SanitizedConfig['admin']['routes'])[] = [
|
|||||||
'reset',
|
'reset',
|
||||||
]
|
]
|
||||||
|
|
||||||
export const isAdminRoute = (route: string, adminRoute: string) => {
|
export const isAdminRoute = ({
|
||||||
return route.startsWith(adminRoute)
|
adminRoute,
|
||||||
|
config,
|
||||||
|
route,
|
||||||
|
}: {
|
||||||
|
adminRoute: string
|
||||||
|
config: SanitizedConfig
|
||||||
|
route: string
|
||||||
|
}): boolean => {
|
||||||
|
return route.startsWith(adminRoute) && !isAdminAuthRoute({ adminRoute, config, route })
|
||||||
}
|
}
|
||||||
|
|
||||||
export const isAdminAuthRoute = (config: SanitizedConfig, route: string, adminRoute: string) => {
|
export const isAdminAuthRoute = ({
|
||||||
|
adminRoute,
|
||||||
|
config,
|
||||||
|
route,
|
||||||
|
}: {
|
||||||
|
adminRoute: string
|
||||||
|
config: SanitizedConfig
|
||||||
|
route: string
|
||||||
|
}): boolean => {
|
||||||
const authRoutes = config.admin?.routes
|
const authRoutes = config.admin?.routes
|
||||||
? Object.entries(config.admin.routes)
|
? Object.entries(config.admin.routes)
|
||||||
.filter(([key]) => authRouteKeys.includes(key as keyof SanitizedConfig['admin']['routes']))
|
.filter(([key]) => authRouteKeys.includes(key as keyof SanitizedConfig['admin']['routes']))
|
||||||
.map(([_, value]) => value)
|
.map(([_, value]) => value)
|
||||||
: []
|
: []
|
||||||
|
|
||||||
return authRoutes.some((r) => route.replace(adminRoute, '').startsWith(r))
|
return authRoutes.some((r) => getRouteWithoutAdmin({ adminRoute, route }).startsWith(r))
|
||||||
|
}
|
||||||
|
|
||||||
|
export const getRouteWithoutAdmin = ({
|
||||||
|
adminRoute,
|
||||||
|
route,
|
||||||
|
}: {
|
||||||
|
adminRoute: string
|
||||||
|
route: string
|
||||||
|
}): string => {
|
||||||
|
return adminRoute && adminRoute !== '/' ? route.replace(adminRoute, '') : route
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ import {
|
|||||||
closeNav,
|
closeNav,
|
||||||
ensureCompilationIsDone,
|
ensureCompilationIsDone,
|
||||||
exactText,
|
exactText,
|
||||||
getAdminRoutes,
|
getRoutes,
|
||||||
initPageConsoleErrorCatch,
|
initPageConsoleErrorCatch,
|
||||||
login,
|
login,
|
||||||
openDocControls,
|
openDocControls,
|
||||||
@@ -99,7 +99,7 @@ describe('access control', () => {
|
|||||||
routes: { logout: logoutRoute },
|
routes: { logout: logoutRoute },
|
||||||
},
|
},
|
||||||
routes: { admin: adminRoute },
|
routes: { admin: adminRoute },
|
||||||
} = getAdminRoutes({})
|
} = getRoutes({})
|
||||||
|
|
||||||
logoutURL = `${serverURL}${adminRoute}${logoutRoute}`
|
logoutURL = `${serverURL}${adminRoute}${logoutRoute}`
|
||||||
})
|
})
|
||||||
|
|||||||
BIN
test/admin-root/app/favicon.ico
Normal file
BIN
test/admin-root/app/favicon.ico
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 15 KiB |
@@ -12,6 +12,13 @@ const dirname = path.dirname(filename)
|
|||||||
|
|
||||||
export default buildConfigWithDefaults({
|
export default buildConfigWithDefaults({
|
||||||
collections: [PostsCollection],
|
collections: [PostsCollection],
|
||||||
|
admin: {
|
||||||
|
autoLogin: {
|
||||||
|
email: devUser.email,
|
||||||
|
password: devUser.password,
|
||||||
|
prefillOnly: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
cors: ['http://localhost:3000', 'http://localhost:3001'],
|
cors: ['http://localhost:3000', 'http://localhost:3001'],
|
||||||
globals: [MenuGlobal],
|
globals: [MenuGlobal],
|
||||||
routes: {
|
routes: {
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import * as path from 'path'
|
|||||||
import { adminRoute } from 'shared.js'
|
import { adminRoute } from 'shared.js'
|
||||||
import { fileURLToPath } from 'url'
|
import { fileURLToPath } from 'url'
|
||||||
|
|
||||||
import { ensureCompilationIsDone, initPageConsoleErrorCatch } from '../helpers.js'
|
import { ensureCompilationIsDone, initPageConsoleErrorCatch, login } from '../helpers.js'
|
||||||
import { AdminUrlUtil } from '../helpers/adminUrlUtil.js'
|
import { AdminUrlUtil } from '../helpers/adminUrlUtil.js'
|
||||||
import { initPayloadE2ENoConfig } from '../helpers/initPayloadE2ENoConfig.js'
|
import { initPayloadE2ENoConfig } from '../helpers/initPayloadE2ENoConfig.js'
|
||||||
import { TEST_TIMEOUT_LONG } from '../playwright.config.js'
|
import { TEST_TIMEOUT_LONG } from '../playwright.config.js'
|
||||||
@@ -29,6 +29,8 @@ test.describe('Admin Panel (Root)', () => {
|
|||||||
page = await context.newPage()
|
page = await context.newPage()
|
||||||
initPageConsoleErrorCatch(page)
|
initPageConsoleErrorCatch(page)
|
||||||
|
|
||||||
|
await login({ page, serverURL, customRoutes: { admin: adminRoute } })
|
||||||
|
|
||||||
await ensureCompilationIsDone({
|
await ensureCompilationIsDone({
|
||||||
customRoutes: {
|
customRoutes: {
|
||||||
admin: adminRoute,
|
admin: adminRoute,
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ import {
|
|||||||
checkPageTitle,
|
checkPageTitle,
|
||||||
ensureCompilationIsDone,
|
ensureCompilationIsDone,
|
||||||
exactText,
|
exactText,
|
||||||
getAdminRoutes,
|
getRoutes,
|
||||||
initPageConsoleErrorCatch,
|
initPageConsoleErrorCatch,
|
||||||
openDocControls,
|
openDocControls,
|
||||||
openNav,
|
openNav,
|
||||||
@@ -76,7 +76,7 @@ describe('admin1', () => {
|
|||||||
let customFieldsURL: AdminUrlUtil
|
let customFieldsURL: AdminUrlUtil
|
||||||
let disableDuplicateURL: AdminUrlUtil
|
let disableDuplicateURL: AdminUrlUtil
|
||||||
let serverURL: string
|
let serverURL: string
|
||||||
let adminRoutes: ReturnType<typeof getAdminRoutes>
|
let adminRoutes: ReturnType<typeof getRoutes>
|
||||||
let loginURL: string
|
let loginURL: string
|
||||||
|
|
||||||
beforeAll(async ({ browser }, testInfo) => {
|
beforeAll(async ({ browser }, testInfo) => {
|
||||||
@@ -106,7 +106,7 @@ describe('admin1', () => {
|
|||||||
|
|
||||||
await ensureCompilationIsDone({ customAdminRoutes, page, serverURL })
|
await ensureCompilationIsDone({ customAdminRoutes, page, serverURL })
|
||||||
|
|
||||||
adminRoutes = getAdminRoutes({ customAdminRoutes })
|
adminRoutes = getRoutes({ customAdminRoutes })
|
||||||
|
|
||||||
loginURL = `${serverURL}${adminRoutes.routes.admin}${adminRoutes.admin.routes.login}`
|
loginURL = `${serverURL}${adminRoutes.routes.admin}${adminRoutes.admin.routes.login}`
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ import type { Config, Geo, Post } from '../../payload-types.js'
|
|||||||
import {
|
import {
|
||||||
ensureCompilationIsDone,
|
ensureCompilationIsDone,
|
||||||
exactText,
|
exactText,
|
||||||
getAdminRoutes,
|
getRoutes,
|
||||||
initPageConsoleErrorCatch,
|
initPageConsoleErrorCatch,
|
||||||
openDocDrawer,
|
openDocDrawer,
|
||||||
openNav,
|
openNav,
|
||||||
@@ -44,7 +44,7 @@ describe('admin2', () => {
|
|||||||
let postsUrl: AdminUrlUtil
|
let postsUrl: AdminUrlUtil
|
||||||
|
|
||||||
let serverURL: string
|
let serverURL: string
|
||||||
let adminRoutes: ReturnType<typeof getAdminRoutes>
|
let adminRoutes: ReturnType<typeof getRoutes>
|
||||||
|
|
||||||
beforeAll(async ({ browser }, testInfo) => {
|
beforeAll(async ({ browser }, testInfo) => {
|
||||||
const prebuild = Boolean(process.env.CI)
|
const prebuild = Boolean(process.env.CI)
|
||||||
@@ -69,7 +69,7 @@ describe('admin2', () => {
|
|||||||
|
|
||||||
await ensureCompilationIsDone({ customAdminRoutes, page, serverURL })
|
await ensureCompilationIsDone({ customAdminRoutes, page, serverURL })
|
||||||
|
|
||||||
adminRoutes = getAdminRoutes({ customAdminRoutes })
|
adminRoutes = getRoutes({ customAdminRoutes })
|
||||||
})
|
})
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
await reInitializeDB({
|
await reInitializeDB({
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ import type { Config } from './payload-types.js'
|
|||||||
|
|
||||||
import {
|
import {
|
||||||
ensureCompilationIsDone,
|
ensureCompilationIsDone,
|
||||||
getAdminRoutes,
|
getRoutes,
|
||||||
initPageConsoleErrorCatch,
|
initPageConsoleErrorCatch,
|
||||||
saveDocAndAssert,
|
saveDocAndAssert,
|
||||||
} from '../helpers.js'
|
} from '../helpers.js'
|
||||||
@@ -49,7 +49,7 @@ const createFirstUser = async ({
|
|||||||
routes: { createFirstUser: createFirstUserRoute },
|
routes: { createFirstUser: createFirstUserRoute },
|
||||||
},
|
},
|
||||||
routes: { admin: adminRoute },
|
routes: { admin: adminRoute },
|
||||||
} = getAdminRoutes({
|
} = getRoutes({
|
||||||
customAdminRoutes,
|
customAdminRoutes,
|
||||||
customRoutes,
|
customRoutes,
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import type { BrowserContext, ChromiumBrowserContext, Locator, Page } from '@playwright/test'
|
import type { BrowserContext, ChromiumBrowserContext, Locator, Page } from '@playwright/test'
|
||||||
import type { Config } from 'payload'
|
import type { Config } from 'payload'
|
||||||
|
|
||||||
|
import { formatAdminURL } from '@payloadcms/ui/shared'
|
||||||
import { expect } from '@playwright/test'
|
import { expect } from '@playwright/test'
|
||||||
import { defaults } from 'payload'
|
import { defaults } from 'payload'
|
||||||
import { wait } from 'payload/shared'
|
import { wait } from 'payload/shared'
|
||||||
@@ -65,7 +66,7 @@ export async function ensureCompilationIsDone({
|
|||||||
}): Promise<void> {
|
}): Promise<void> {
|
||||||
const {
|
const {
|
||||||
routes: { admin: adminRoute },
|
routes: { admin: adminRoute },
|
||||||
} = getAdminRoutes({ customAdminRoutes, customRoutes })
|
} = getRoutes({ customAdminRoutes, customRoutes })
|
||||||
|
|
||||||
const adminURL = `${serverURL}${adminRoute}`
|
const adminURL = `${serverURL}${adminRoute}`
|
||||||
|
|
||||||
@@ -114,7 +115,7 @@ export async function firstRegister(args: FirstRegisterArgs): Promise<void> {
|
|||||||
|
|
||||||
const {
|
const {
|
||||||
routes: { admin: adminRoute },
|
routes: { admin: adminRoute },
|
||||||
} = getAdminRoutes({ customAdminRoutes, customRoutes })
|
} = getRoutes({ customAdminRoutes, customRoutes })
|
||||||
|
|
||||||
await page.goto(`${serverURL}${adminRoute}`)
|
await page.goto(`${serverURL}${adminRoute}`)
|
||||||
await page.fill('#field-email', devUser.email)
|
await page.fill('#field-email', devUser.email)
|
||||||
@@ -130,27 +131,37 @@ export async function login(args: LoginArgs): Promise<void> {
|
|||||||
|
|
||||||
const {
|
const {
|
||||||
admin: {
|
admin: {
|
||||||
routes: { createFirstUser: createFirstUserRoute, login: loginRoute },
|
routes: { createFirstUser, login: incomingLoginRoute },
|
||||||
},
|
},
|
||||||
routes: { admin: adminRoute },
|
routes: { admin: incomingAdminRoute },
|
||||||
} = getAdminRoutes({ customAdminRoutes, customRoutes })
|
} = getRoutes({ customAdminRoutes, customRoutes })
|
||||||
|
|
||||||
await page.goto(`${serverURL}${adminRoute}${loginRoute}`)
|
const adminRoute = formatAdminURL({ serverURL, adminRoute: incomingAdminRoute, path: '' })
|
||||||
await page.waitForURL(`${serverURL}${adminRoute}${loginRoute}`)
|
const loginRoute = formatAdminURL({
|
||||||
|
serverURL,
|
||||||
|
adminRoute: incomingAdminRoute,
|
||||||
|
path: incomingLoginRoute,
|
||||||
|
})
|
||||||
|
const createFirstUserRoute = formatAdminURL({
|
||||||
|
serverURL,
|
||||||
|
adminRoute: incomingAdminRoute,
|
||||||
|
path: createFirstUser,
|
||||||
|
})
|
||||||
|
|
||||||
|
await page.goto(loginRoute)
|
||||||
|
await page.waitForURL(loginRoute)
|
||||||
await wait(500)
|
await wait(500)
|
||||||
await page.fill('#field-email', data.email)
|
await page.fill('#field-email', data.email)
|
||||||
await page.fill('#field-password', data.password)
|
await page.fill('#field-password', data.password)
|
||||||
await wait(500)
|
await wait(500)
|
||||||
await page.click('[type=submit]')
|
await page.click('[type=submit]')
|
||||||
await page.waitForURL(`${serverURL}${adminRoute}`)
|
await page.waitForURL(adminRoute)
|
||||||
|
|
||||||
await expect(() => expect(page.url()).not.toContain(`${adminRoute}${loginRoute}`)).toPass({
|
await expect(() => expect(page.url()).not.toContain(loginRoute)).toPass({
|
||||||
timeout: POLL_TOPASS_TIMEOUT,
|
timeout: POLL_TOPASS_TIMEOUT,
|
||||||
})
|
})
|
||||||
|
|
||||||
await expect(() =>
|
await expect(() => expect(page.url()).not.toContain(createFirstUserRoute)).toPass({
|
||||||
expect(page.url()).not.toContain(`${adminRoute}${createFirstUserRoute}`),
|
|
||||||
).toPass({
|
|
||||||
timeout: POLL_TOPASS_TIMEOUT,
|
timeout: POLL_TOPASS_TIMEOUT,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -328,7 +339,7 @@ export function describeIfInCIOrHasLocalstack(): jest.Describe {
|
|||||||
|
|
||||||
type AdminRoutes = Config['admin']['routes']
|
type AdminRoutes = Config['admin']['routes']
|
||||||
|
|
||||||
export function getAdminRoutes({
|
export function getRoutes({
|
||||||
customAdminRoutes,
|
customAdminRoutes,
|
||||||
customRoutes,
|
customRoutes,
|
||||||
}: {
|
}: {
|
||||||
|
|||||||
@@ -37,7 +37,7 @@
|
|||||||
],
|
],
|
||||||
"paths": {
|
"paths": {
|
||||||
"@payload-config": [
|
"@payload-config": [
|
||||||
"./test/admin/config.ts"
|
"./test/_community/config.ts"
|
||||||
],
|
],
|
||||||
"@payloadcms/live-preview": [
|
"@payloadcms/live-preview": [
|
||||||
"./packages/live-preview/src"
|
"./packages/live-preview/src"
|
||||||
|
|||||||
Reference in New Issue
Block a user