fix(next): infinite loop when logging into root admin (#7412)

This commit is contained in:
Jacob Fletcher
2024-07-29 12:57:57 -04:00
committed by GitHub
parent 7ed6634bc5
commit 874279c530
12 changed files with 82 additions and 36 deletions

View File

@@ -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()
} }

View File

@@ -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

View File

@@ -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
} }

View File

@@ -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}`
}) })

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

View File

@@ -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: {

View File

@@ -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,

View File

@@ -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}`
}) })

View File

@@ -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({

View File

@@ -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,
}) })

View File

@@ -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,
}: { }: {

View File

@@ -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"