Skip to content

Commit ea917dd

Browse files
authored
feat(next): supports custom login redirects in initPage (#6186)
2 parents e25814e + 070d8e1 commit ea917dd

File tree

10 files changed

+254
-153
lines changed

10 files changed

+254
-153
lines changed

packages/next/src/exports/utilities.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,4 +5,4 @@ export { createPayloadRequest } from '../utilities/createPayloadRequest.js'
55
export { getNextRequestI18n } from '../utilities/getNextRequestI18n.js'
66
export { getPayloadHMR, reload } from '../utilities/getPayloadHMR.js'
77
export { headersWithCors } from '../utilities/headersWithCors.js'
8-
export { initPage } from '../utilities/initPage.js'
8+
export { initPage } from '../utilities/initPage/index.js'

packages/next/src/utilities/initPage.ts

Lines changed: 0 additions & 149 deletions
This file was deleted.
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
import type { Permissions } from 'payload/auth'
2+
import type {
3+
SanitizedCollectionConfig,
4+
SanitizedConfig,
5+
SanitizedGlobalConfig,
6+
} from 'payload/types'
7+
8+
import { notFound } from 'next/navigation.js'
9+
10+
import { isAdminAuthRoute, isAdminRoute } from './shared.js'
11+
12+
export const handleAdminPage = ({
13+
adminRoute,
14+
config,
15+
permissions,
16+
route,
17+
}: {
18+
adminRoute: string
19+
config: SanitizedConfig
20+
permissions: Permissions
21+
route: string
22+
}) => {
23+
if (isAdminRoute(route, adminRoute)) {
24+
const routeSegments = route.replace(adminRoute, '').split('/').filter(Boolean)
25+
const [entityType, entitySlug, createOrID] = routeSegments
26+
const collectionSlug = entityType === 'collections' ? entitySlug : undefined
27+
const globalSlug = entityType === 'globals' ? entitySlug : undefined
28+
const docID = collectionSlug && createOrID !== 'create' ? createOrID : undefined
29+
30+
let collectionConfig: SanitizedCollectionConfig | undefined
31+
let globalConfig: SanitizedGlobalConfig | undefined
32+
33+
if (collectionSlug) {
34+
collectionConfig = config.collections.find((collection) => collection.slug === collectionSlug)
35+
36+
if (!collectionConfig) {
37+
notFound()
38+
}
39+
}
40+
41+
if (globalSlug) {
42+
globalConfig = config.globals.find((global) => global.slug === globalSlug)
43+
44+
if (!globalConfig) {
45+
notFound()
46+
}
47+
}
48+
49+
if (!permissions.canAccessAdmin && !isAdminAuthRoute(route, adminRoute)) {
50+
notFound()
51+
}
52+
53+
return {
54+
collectionConfig,
55+
docID,
56+
globalConfig,
57+
}
58+
}
59+
}
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
import { redirect } from 'next/navigation.js'
2+
import QueryString from 'qs'
3+
4+
import { isAdminAuthRoute, isAdminRoute } from './shared.js'
5+
6+
export const handleAuthRedirect = ({
7+
adminRoute,
8+
redirectUnauthenticatedUser,
9+
route,
10+
searchParams,
11+
}: {
12+
adminRoute: string
13+
redirectUnauthenticatedUser: boolean | string
14+
route: string
15+
searchParams: { [key: string]: string | string[] }
16+
}) => {
17+
if (!isAdminAuthRoute(route, adminRoute)) {
18+
if (searchParams && 'redirect' in searchParams) delete searchParams.redirect
19+
20+
const redirectRoute = encodeURIComponent(
21+
route + Object.keys(searchParams ?? {}).length
22+
? `${QueryString.stringify(searchParams, { addQueryPrefix: true })}`
23+
: undefined,
24+
)
25+
26+
const adminLoginRoute = `${adminRoute}/login`
27+
28+
const customLoginRoute =
29+
typeof redirectUnauthenticatedUser === 'string' ? redirectUnauthenticatedUser : undefined
30+
31+
const loginRoute = isAdminRoute(route, adminRoute)
32+
? adminLoginRoute
33+
: customLoginRoute || '/login'
34+
35+
const parsedLoginRouteSearchParams = QueryString.parse(loginRoute.split('?')[1] ?? '')
36+
37+
const searchParamsWithRedirect = `${QueryString.stringify(
38+
{
39+
...parsedLoginRouteSearchParams,
40+
...(redirectRoute ? { redirect: redirectRoute } : {}),
41+
},
42+
{ addQueryPrefix: true },
43+
)}`
44+
45+
redirect(`${loginRoute.split('?')[0]}${searchParamsWithRedirect}`)
46+
}
47+
}
Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
import type { InitPageResult, PayloadRequestWithData, VisibleEntities } from 'payload/types'
2+
3+
import { initI18n } from '@payloadcms/translations'
4+
import { findLocaleFromCode } from '@payloadcms/ui/utilities/findLocaleFromCode'
5+
import { headers as getHeaders } from 'next/headers.js'
6+
import { parseCookies } from 'payload/auth'
7+
import { createLocalReq, isEntityHidden } from 'payload/utilities'
8+
import qs from 'qs'
9+
10+
import type { Args } from './types.js'
11+
12+
import { getPayloadHMR } from '../getPayloadHMR.js'
13+
import { getRequestLanguage } from '../getRequestLanguage.js'
14+
import { handleAdminPage } from './handleAdminPage.js'
15+
import { handleAuthRedirect } from './handleAuthRedirect.js'
16+
17+
export const initPage = async ({
18+
config: configPromise,
19+
redirectUnauthenticatedUser = false,
20+
route,
21+
searchParams,
22+
}: Args): Promise<InitPageResult> => {
23+
const headers = getHeaders()
24+
const localeParam = searchParams?.locale as string
25+
const payload = await getPayloadHMR({ config: configPromise })
26+
27+
const {
28+
collections,
29+
globals,
30+
i18n: i18nConfig,
31+
localization,
32+
routes: { admin: adminRoute },
33+
} = payload.config
34+
35+
const queryString = `${qs.stringify(searchParams ?? {}, { addQueryPrefix: true })}`
36+
const defaultLocale =
37+
localization && localization.defaultLocale ? localization.defaultLocale : 'en'
38+
const localeCode = localeParam || defaultLocale
39+
const locale = localization && findLocaleFromCode(localization, localeCode)
40+
const cookies = parseCookies(headers)
41+
const language = getRequestLanguage({ config: payload.config, cookies, headers })
42+
43+
const i18n = await initI18n({
44+
config: i18nConfig,
45+
context: 'client',
46+
language,
47+
})
48+
49+
const req = await createLocalReq(
50+
{
51+
fallbackLocale: null,
52+
locale: locale.code,
53+
req: {
54+
i18n,
55+
query: qs.parse(queryString, {
56+
depth: 10,
57+
ignoreQueryPrefix: true,
58+
}),
59+
url: `${payload.config.serverURL}${route}${searchParams ? queryString : ''}`,
60+
} as PayloadRequestWithData,
61+
},
62+
payload,
63+
)
64+
65+
const { permissions, user } = await payload.auth({ headers, req })
66+
67+
req.user = user
68+
69+
const visibleEntities: VisibleEntities = {
70+
collections: collections
71+
.map(({ slug, admin: { hidden } }) => (!isEntityHidden({ hidden, user }) ? slug : null))
72+
.filter(Boolean),
73+
globals: globals
74+
.map(({ slug, admin: { hidden } }) => (!isEntityHidden({ hidden, user }) ? slug : null))
75+
.filter(Boolean),
76+
}
77+
78+
if (redirectUnauthenticatedUser && !user) {
79+
handleAuthRedirect({
80+
adminRoute,
81+
redirectUnauthenticatedUser,
82+
route,
83+
searchParams,
84+
})
85+
}
86+
87+
const { collectionConfig, docID, globalConfig } = handleAdminPage({
88+
adminRoute,
89+
config: payload.config,
90+
permissions,
91+
route,
92+
})
93+
94+
return {
95+
collectionConfig,
96+
cookies,
97+
docID,
98+
globalConfig,
99+
locale,
100+
permissions,
101+
req,
102+
translations: i18n.translations,
103+
visibleEntities,
104+
}
105+
}
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
export const authRoutes = [
2+
'/login',
3+
'/logout',
4+
'/create-first-user',
5+
'/forgot',
6+
'/reset',
7+
'/verify',
8+
'/logout-inactivity',
9+
]
10+
11+
export const isAdminRoute = (route: string, adminRoute: string) => {
12+
return route.startsWith(adminRoute)
13+
}
14+
15+
export const isAdminAuthRoute = (route: string, adminRoute: string) => {
16+
return authRoutes.some((r) => route.replace(adminRoute, '').startsWith(r))
17+
}

0 commit comments

Comments
 (0)