diff --git a/packages/graphql/src/resolvers/auth/login.ts b/packages/graphql/src/resolvers/auth/login.ts
index 241c204eb..fe588a563 100644
--- a/packages/graphql/src/resolvers/auth/login.ts
+++ b/packages/graphql/src/resolvers/auth/login.ts
@@ -19,8 +19,8 @@ export function login(collection: Collection): any {
const result = await loginOperation(options)
const cookie = generatePayloadCookie({
- collectionConfig: collection.config,
- payload: context.req.payload,
+ collectionAuthConfig: collection.config.auth,
+ cookiePrefix: context.req.payload.config.cookiePrefix,
token: result.token,
})
diff --git a/packages/graphql/src/resolvers/auth/logout.ts b/packages/graphql/src/resolvers/auth/logout.ts
index c81dbb799..7277ea5a2 100644
--- a/packages/graphql/src/resolvers/auth/logout.ts
+++ b/packages/graphql/src/resolvers/auth/logout.ts
@@ -13,8 +13,9 @@ export function logout(collection: Collection): any {
const result = await logoutOperation(options)
const expiredCookie = generateExpiredPayloadCookie({
- collectionConfig: collection.config,
- payload: context.req.payload,
+ collectionAuthConfig: collection.config.auth,
+ config: context.req.payload.config,
+ cookiePrefix: context.req.payload.config.cookiePrefix,
})
context.headers['Set-Cookie'] = expiredCookie
return result
diff --git a/packages/graphql/src/resolvers/auth/refresh.ts b/packages/graphql/src/resolvers/auth/refresh.ts
index ef25253e5..0f5dc8912 100644
--- a/packages/graphql/src/resolvers/auth/refresh.ts
+++ b/packages/graphql/src/resolvers/auth/refresh.ts
@@ -14,8 +14,8 @@ export function refresh(collection: Collection): any {
const result = await refreshOperation(options)
const cookie = generatePayloadCookie({
- collectionConfig: collection.config,
- payload: context.req.payload,
+ collectionAuthConfig: collection.config.auth,
+ cookiePrefix: context.req.payload.config.cookiePrefix,
token: result.refreshedToken,
})
context.headers['Set-Cookie'] = cookie
diff --git a/packages/graphql/src/resolvers/auth/resetPassword.ts b/packages/graphql/src/resolvers/auth/resetPassword.ts
index b17f4afa1..161e69322 100644
--- a/packages/graphql/src/resolvers/auth/resetPassword.ts
+++ b/packages/graphql/src/resolvers/auth/resetPassword.ts
@@ -23,8 +23,8 @@ export function resetPassword(collection: Collection): any {
const result = await resetPasswordOperation(options)
const cookie = generatePayloadCookie({
- collectionConfig: collection.config,
- payload: context.req.payload,
+ collectionAuthConfig: collection.config.auth,
+ cookiePrefix: context.req.payload.config.cookiePrefix,
token: result.token,
})
context.headers['Set-Cookie'] = cookie
diff --git a/packages/next/src/elements/FormHeader/index.scss b/packages/next/src/elements/FormHeader/index.scss
new file mode 100644
index 000000000..950597471
--- /dev/null
+++ b/packages/next/src/elements/FormHeader/index.scss
@@ -0,0 +1,6 @@
+.form-header {
+ display: flex;
+ flex-direction: column;
+ gap: calc(var(--base) * .5);
+ margin-bottom: var(--base);
+}
diff --git a/packages/next/src/elements/FormHeader/index.tsx b/packages/next/src/elements/FormHeader/index.tsx
new file mode 100644
index 000000000..22a07eb8f
--- /dev/null
+++ b/packages/next/src/elements/FormHeader/index.tsx
@@ -0,0 +1,22 @@
+import React from 'react'
+
+import './index.scss'
+
+const baseClass = 'form-header'
+
+type Props = {
+ description?: React.ReactNode | string
+ heading: string
+}
+export function FormHeader({ description, heading }: Props) {
+ if (!heading) {
+ return null
+ }
+
+ return (
+
+
{heading}
+ {Boolean(description) &&
{description}
}
+
+ )
+}
diff --git a/packages/next/src/routes/rest/auth/login.ts b/packages/next/src/routes/rest/auth/login.ts
index 5714c023d..f9f046c0d 100644
--- a/packages/next/src/routes/rest/auth/login.ts
+++ b/packages/next/src/routes/rest/auth/login.ts
@@ -29,8 +29,8 @@ export const login: CollectionRouteHandler = async ({ collection, req }) => {
})
const cookie = generatePayloadCookie({
- collectionConfig: collection.config,
- payload: req.payload,
+ collectionAuthConfig: collection.config.auth,
+ cookiePrefix: req.payload.config.cookiePrefix,
token: result.token,
})
diff --git a/packages/next/src/routes/rest/auth/logout.ts b/packages/next/src/routes/rest/auth/logout.ts
index e27c31990..cc49b48b2 100644
--- a/packages/next/src/routes/rest/auth/logout.ts
+++ b/packages/next/src/routes/rest/auth/logout.ts
@@ -30,8 +30,9 @@ export const logout: CollectionRouteHandler = async ({ collection, req }) => {
}
const expiredCookie = generateExpiredPayloadCookie({
- collectionConfig: collection.config,
- payload: req.payload,
+ collectionAuthConfig: collection.config.auth,
+ config: req.payload.config,
+ cookiePrefix: req.payload.config.cookiePrefix,
})
headers.set('Set-Cookie', expiredCookie)
diff --git a/packages/next/src/routes/rest/auth/refresh.ts b/packages/next/src/routes/rest/auth/refresh.ts
index 9c77ca90c..abf446cff 100644
--- a/packages/next/src/routes/rest/auth/refresh.ts
+++ b/packages/next/src/routes/rest/auth/refresh.ts
@@ -20,8 +20,8 @@ export const refresh: CollectionRouteHandler = async ({ collection, req }) => {
if (result.setCookie) {
const cookie = generatePayloadCookie({
- collectionConfig: collection.config,
- payload: req.payload,
+ collectionAuthConfig: collection.config.auth,
+ cookiePrefix: req.payload.config.cookiePrefix,
token: result.refreshedToken,
})
diff --git a/packages/next/src/routes/rest/auth/registerFirstUser.ts b/packages/next/src/routes/rest/auth/registerFirstUser.ts
index 7743ce79a..47995f825 100644
--- a/packages/next/src/routes/rest/auth/registerFirstUser.ts
+++ b/packages/next/src/routes/rest/auth/registerFirstUser.ts
@@ -28,8 +28,8 @@ export const registerFirstUser: CollectionRouteHandler = async ({ collection, re
})
const cookie = generatePayloadCookie({
- collectionConfig: collection.config,
- payload: req.payload,
+ collectionAuthConfig: collection.config.auth,
+ cookiePrefix: req.payload.config.cookiePrefix,
token: result.token,
})
diff --git a/packages/next/src/routes/rest/auth/resetPassword.ts b/packages/next/src/routes/rest/auth/resetPassword.ts
index b32ae23e5..4cf7bbe1c 100644
--- a/packages/next/src/routes/rest/auth/resetPassword.ts
+++ b/packages/next/src/routes/rest/auth/resetPassword.ts
@@ -20,8 +20,8 @@ export const resetPassword: CollectionRouteHandler = async ({ collection, req })
})
const cookie = generatePayloadCookie({
- collectionConfig: collection.config,
- payload: req.payload,
+ collectionAuthConfig: collection.config.auth,
+ cookiePrefix: req.payload.config.cookiePrefix,
token: result.token,
})
diff --git a/packages/next/src/utilities/initPage/handleAdminPage.ts b/packages/next/src/utilities/initPage/handleAdminPage.ts
index 24868a322..5ef40467f 100644
--- a/packages/next/src/utilities/initPage/handleAdminPage.ts
+++ b/packages/next/src/utilities/initPage/handleAdminPage.ts
@@ -1,25 +1,21 @@
-import type {
- Permissions,
- SanitizedCollectionConfig,
- SanitizedConfig,
- SanitizedGlobalConfig,
-} from 'payload'
+import type { SanitizedCollectionConfig, SanitizedConfig, SanitizedGlobalConfig } from 'payload'
-import { notFound } from 'next/navigation.js'
+import { getRouteWithoutAdmin, isAdminRoute } from './shared.js'
-import { getRouteWithoutAdmin, isAdminAuthRoute, isAdminRoute } from './shared.js'
-
-export const handleAdminPage = ({
- adminRoute,
- config,
- permissions,
- route,
-}: {
+type Args = {
adminRoute: string
config: SanitizedConfig
- permissions: Permissions
route: string
-}) => {
+}
+type RouteInfo = {
+ collectionConfig?: SanitizedCollectionConfig
+ collectionSlug?: string
+ docID?: string
+ globalConfig?: SanitizedGlobalConfig
+ globalSlug?: string
+}
+
+export function getRouteInfo({ adminRoute, config, route }: Args): RouteInfo {
if (isAdminRoute({ adminRoute, config, route })) {
const routeWithoutAdmin = getRouteWithoutAdmin({ adminRoute, route })
const routeSegments = routeWithoutAdmin.split('/').filter(Boolean)
@@ -33,28 +29,18 @@ export const handleAdminPage = ({
if (collectionSlug) {
collectionConfig = config.collections.find((collection) => collection.slug === collectionSlug)
-
- if (!collectionConfig) {
- notFound()
- }
}
if (globalSlug) {
globalConfig = config.globals.find((global) => global.slug === globalSlug)
-
- if (!globalConfig) {
- notFound()
- }
- }
-
- if (!permissions.canAccessAdmin && !isAdminAuthRoute({ adminRoute, config, route })) {
- notFound()
}
return {
collectionConfig,
+ collectionSlug,
docID,
globalConfig,
+ globalSlug,
}
}
diff --git a/packages/next/src/utilities/initPage/handleAuthRedirect.ts b/packages/next/src/utilities/initPage/handleAuthRedirect.ts
index 20393b698..bba32ae47 100644
--- a/packages/next/src/utilities/initPage/handleAuthRedirect.ts
+++ b/packages/next/src/utilities/initPage/handleAuthRedirect.ts
@@ -1,57 +1,46 @@
+import type { User } from 'payload'
+
import { formatAdminURL } from '@payloadcms/ui/shared'
-import { redirect } from 'next/navigation.js'
import * as qs from 'qs-esm'
-import { isAdminAuthRoute, isAdminRoute } from './shared.js'
-
-export const handleAuthRedirect = ({
- config,
- redirectUnauthenticatedUser,
- route,
- searchParams,
-}: {
+type Args = {
config
- redirectUnauthenticatedUser: boolean | string
route: string
searchParams: { [key: string]: string | string[] }
-}) => {
+ user?: User
+}
+export const handleAuthRedirect = ({ config, route, searchParams, user }: Args): string => {
const {
admin: {
- routes: { login: loginRouteFromConfig },
+ routes: { login: loginRouteFromConfig, unauthorized: unauthorizedRoute },
},
routes: { admin: adminRoute },
} = config
- if (!isAdminAuthRoute({ adminRoute, config, route })) {
- if (searchParams && 'redirect' in searchParams) {
- delete searchParams.redirect
- }
-
- const redirectRoute = encodeURIComponent(
- route + Object.keys(searchParams ?? {}).length
- ? `${qs.stringify(searchParams, { addQueryPrefix: true })}`
- : undefined,
- )
-
- const adminLoginRoute = formatAdminURL({ adminRoute, path: loginRouteFromConfig })
-
- const customLoginRoute =
- typeof redirectUnauthenticatedUser === 'string' ? redirectUnauthenticatedUser : undefined
-
- const loginRoute = isAdminRoute({ adminRoute, config, route })
- ? adminLoginRoute
- : customLoginRoute || loginRouteFromConfig
-
- const parsedLoginRouteSearchParams = qs.parse(loginRoute.split('?')[1] ?? '')
-
- const searchParamsWithRedirect = `${qs.stringify(
- {
- ...parsedLoginRouteSearchParams,
- ...(redirectRoute ? { redirect: redirectRoute } : {}),
- },
- { addQueryPrefix: true },
- )}`
-
- redirect(`${loginRoute.split('?')[0]}${searchParamsWithRedirect}`)
+ if (searchParams && 'redirect' in searchParams) {
+ delete searchParams.redirect
}
+
+ const redirectRoute = encodeURIComponent(
+ route + Object.keys(searchParams ?? {}).length
+ ? `${qs.stringify(searchParams, { addQueryPrefix: true })}`
+ : undefined,
+ )
+
+ const redirectTo = formatAdminURL({
+ adminRoute,
+ path: user ? unauthorizedRoute : loginRouteFromConfig,
+ })
+
+ const parsedLoginRouteSearchParams = qs.parse(redirectTo.split('?')[1] ?? '')
+
+ const searchParamsWithRedirect = `${qs.stringify(
+ {
+ ...parsedLoginRouteSearchParams,
+ ...(redirectRoute ? { redirect: redirectRoute } : {}),
+ },
+ { addQueryPrefix: true },
+ )}`
+
+ return `${redirectTo.split('?')[0]}${searchParamsWithRedirect}`
}
diff --git a/packages/next/src/utilities/initPage/index.ts b/packages/next/src/utilities/initPage/index.ts
index ff2ef34a2..fb402c3ab 100644
--- a/packages/next/src/utilities/initPage/index.ts
+++ b/packages/next/src/utilities/initPage/index.ts
@@ -2,6 +2,7 @@ import type { InitPageResult, Locale, PayloadRequest, VisibleEntities } from 'pa
import { findLocaleFromCode } from '@payloadcms/ui/shared'
import { headers as getHeaders } from 'next/headers.js'
+import { notFound } from 'next/navigation.js'
import { createLocalReq, isEntityHidden, parseCookies } from 'payload'
import * as qs from 'qs-esm'
@@ -9,13 +10,13 @@ import type { Args } from './types.js'
import { getPayloadHMR } from '../getPayloadHMR.js'
import { initReq } from '../initReq.js'
-import { handleAdminPage } from './handleAdminPage.js'
+import { getRouteInfo } from './handleAdminPage.js'
import { handleAuthRedirect } from './handleAuthRedirect.js'
+import { isPublicAdminRoute } from './shared.js'
export const initPage = async ({
config: configPromise,
importMap,
- redirectUnauthenticatedUser = false,
route,
searchParams,
}: Args): Promise => {
@@ -128,22 +129,30 @@ export const initPage = async ({
.filter(Boolean),
}
- if (redirectUnauthenticatedUser && !user) {
- handleAuthRedirect({
+ let redirectTo = null
+
+ if (
+ !permissions.canAccessAdmin &&
+ !isPublicAdminRoute({ adminRoute, config: payload.config, route })
+ ) {
+ redirectTo = handleAuthRedirect({
config: payload.config,
- redirectUnauthenticatedUser,
route,
searchParams,
+ user,
})
}
- const { collectionConfig, docID, globalConfig } = handleAdminPage({
+ const { collectionConfig, collectionSlug, docID, globalConfig, globalSlug } = getRouteInfo({
adminRoute,
config: payload.config,
- permissions,
route,
})
+ if ((collectionSlug && !collectionConfig) || (globalSlug && !globalConfig)) {
+ return notFound()
+ }
+
return {
collectionConfig,
cookies,
@@ -152,6 +161,7 @@ export const initPage = async ({
languageOptions,
locale,
permissions,
+ redirectTo,
req,
translations: i18n.translations,
visibleEntities,
diff --git a/packages/next/src/utilities/initPage/shared.ts b/packages/next/src/utilities/initPage/shared.ts
index 986f573cd..185d7ad7b 100644
--- a/packages/next/src/utilities/initPage/shared.ts
+++ b/packages/next/src/utilities/initPage/shared.ts
@@ -1,6 +1,10 @@
import type { SanitizedConfig } from 'payload'
-const authRouteKeys: (keyof SanitizedConfig['admin']['routes'])[] = [
+// Routes that require admin authentication
+const publicAdminRoutes: (keyof Pick<
+ SanitizedConfig['admin']['routes'],
+ 'createFirstUser' | 'forgot' | 'inactivity' | 'login' | 'logout' | 'reset' | 'unauthorized'
+>)[] = [
'createFirstUser',
'forgot',
'login',
@@ -13,17 +17,16 @@ const authRouteKeys: (keyof SanitizedConfig['admin']['routes'])[] = [
export const isAdminRoute = ({
adminRoute,
- config,
route,
}: {
adminRoute: string
config: SanitizedConfig
route: string
}): boolean => {
- return route.startsWith(adminRoute) && !isAdminAuthRoute({ adminRoute, config, route })
+ return route.startsWith(adminRoute)
}
-export const isAdminAuthRoute = ({
+export const isPublicAdminRoute = ({
adminRoute,
config,
route,
@@ -32,13 +35,17 @@ export const isAdminAuthRoute = ({
config: SanitizedConfig
route: string
}): boolean => {
- const authRoutes = config.admin?.routes
- ? Object.entries(config.admin.routes)
- .filter(([key]) => authRouteKeys.includes(key as keyof SanitizedConfig['admin']['routes']))
- .map(([_, value]) => value)
- : []
-
- return authRoutes.some((r) => getRouteWithoutAdmin({ adminRoute, route }).startsWith(r))
+ return publicAdminRoutes.some((routeSegment) => {
+ const segment = config.admin?.routes?.[routeSegment] || routeSegment
+ const routeWithoutAdmin = getRouteWithoutAdmin({ adminRoute, route })
+ if (routeWithoutAdmin.startsWith(segment)) {
+ return true
+ } else if (routeWithoutAdmin.includes('/verify/')) {
+ return true
+ } else {
+ return false
+ }
+ })
}
export const getRouteWithoutAdmin = ({
diff --git a/packages/next/src/views/ForgotPassword/ForgotPasswordForm/index.tsx b/packages/next/src/views/ForgotPassword/ForgotPasswordForm/index.tsx
index 8c1b15496..c5f4833fa 100644
--- a/packages/next/src/views/ForgotPassword/ForgotPasswordForm/index.tsx
+++ b/packages/next/src/views/ForgotPassword/ForgotPasswordForm/index.tsx
@@ -5,7 +5,9 @@ import type { FormState, PayloadRequest } from 'payload'
import { EmailField, Form, FormSubmit, TextField, useConfig, useTranslation } from '@payloadcms/ui'
import { email, text } from 'payload/shared'
-import React, { Fragment, useState } from 'react'
+import React, { useState } from 'react'
+
+import { FormHeader } from '../../../elements/FormHeader/index.js'
export const ForgotPasswordForm: React.FC = () => {
const { config } = useConfig()
@@ -54,10 +56,10 @@ export const ForgotPasswordForm: React.FC = () => {
if (hasSubmitted) {
return (
-
- {t('authentication:emailSent')}
- {t('authentication:checkYourEmailForPasswordReset')}
-
+
)
}
@@ -68,12 +70,14 @@ export const ForgotPasswordForm: React.FC = () => {
initialState={initialState}
method="POST"
>
- {t('authentication:forgotPassword')}
-
- {loginWithUsername
- ? t('authentication:forgotPasswordUsernameInstructions')
- : t('authentication:forgotPasswordEmailInstructions')}
-
+
{loginWithUsername ? (
{
}
/>
)}
- {t('general:submit')}
+ {t('general:submit')}
)
}
diff --git a/packages/next/src/views/ForgotPassword/index.tsx b/packages/next/src/views/ForgotPassword/index.tsx
index 108638df0..ec154248d 100644
--- a/packages/next/src/views/ForgotPassword/index.tsx
+++ b/packages/next/src/views/ForgotPassword/index.tsx
@@ -5,6 +5,7 @@ import { formatAdminURL, Translation } from '@payloadcms/ui/shared'
import LinkImport from 'next/link.js'
import React, { Fragment } from 'react'
+import { FormHeader } from '../../elements/FormHeader/index.js'
import { ForgotPasswordForm } from './ForgotPasswordForm/index.js'
export { generateForgotPasswordMetadata } from './meta.js'
@@ -31,26 +32,27 @@ export const ForgotPasswordView: React.FC = ({ initPageResult })
if (user) {
return (
- {i18n.t('authentication:alreadyLoggedIn')}
-
- (
-
- {children}
-
- ),
- }}
- i18nKey="authentication:loggedInChangePassword"
- t={i18n.t}
- />
-
-
+ (
+
+ {children}
+
+ ),
+ }}
+ i18nKey="authentication:loggedInChangePassword"
+ t={i18n.t}
+ />
+ }
+ heading={i18n.t('authentication:alreadyLoggedIn')}
+ />
diff --git a/packages/next/src/views/ResetPassword/index.client.tsx b/packages/next/src/views/ResetPassword/ResetPasswordForm/index.tsx
similarity index 71%
rename from packages/next/src/views/ResetPassword/index.client.tsx
rename to packages/next/src/views/ResetPassword/ResetPasswordForm/index.tsx
index 6128de4c9..c0af119d6 100644
--- a/packages/next/src/views/ResetPassword/index.client.tsx
+++ b/packages/next/src/views/ResetPassword/ResetPasswordForm/index.tsx
@@ -1,6 +1,4 @@
'use client'
-import type { FormState } from 'payload'
-
import {
ConfirmPasswordField,
Form,
@@ -13,8 +11,8 @@ import {
} from '@payloadcms/ui'
import { formatAdminURL } from '@payloadcms/ui/shared'
import { useRouter } from 'next/navigation.js'
+import { type FormState } from 'payload'
import React from 'react'
-import { toast } from 'sonner'
type Args = {
readonly token: string
@@ -33,7 +31,7 @@ const initialState: FormState = {
},
}
-export const ResetPasswordClient: React.FC = ({ token }) => {
+export const ResetPasswordForm: React.FC = ({ token }) => {
const i18n = useTranslation()
const {
config: {
@@ -47,26 +45,21 @@ export const ResetPasswordClient: React.FC = ({ token }) => {
} = useConfig()
const history = useRouter()
-
const { fetchFullUser } = useAuth()
- const onSuccess = React.useCallback(
- async (data) => {
- if (data.token) {
- await fetchFullUser()
- history.push(adminRoute)
- } else {
- history.push(
- formatAdminURL({
- adminRoute,
- path: loginRoute,
- }),
- )
- toast.success(i18n.t('general:updatedSuccessfully'))
- }
- },
- [adminRoute, fetchFullUser, history, i18n, loginRoute],
- )
+ const onSuccess = React.useCallback(async () => {
+ const user = await fetchFullUser()
+ if (user) {
+ history.push(adminRoute)
+ } else {
+ history.push(
+ formatAdminURL({
+ adminRoute,
+ path: loginRoute,
+ }),
+ )
+ }
+ }, [adminRoute, fetchFullUser, history, loginRoute])
return (