fix: verify view is inaccessible (#8557)

Fixes https://github.com/payloadcms/payload/issues/8470

Cleans up the way we redirect and where it happens.

## Improvements
- When you verify, the admin panel will display a toast when it
redirects you to the login route. This is contextually helpful as to
what is happening.
- Removes dead code path, as we always set the _verifiedToken to null
after it is used.

## `handleAdminPage` renamed to `getRouteInfo`
This function no longer handles routing. It kicks that responsibility
back up to the initPage function.

## `isAdminAuthRoute` renamed to `isPublicAdminRoute`
This was inversely named as it determines if a given route is public.
Also simplifies deterministic logic here.

## `redirectUnauthenticatedUser` argument
This is no longer used or needed. We can determine these things by using
the `isPublicAdminRoute` function.

## View Style fixes
- Reset Password
- Forgot Password
- Unauthorized
This commit is contained in:
Jarrod Flesch
2024-10-07 14:20:07 -04:00
committed by GitHub
parent 2a1321c813
commit 1b63ad4cb3
66 changed files with 417 additions and 377 deletions

View File

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,6 @@
.form-header {
display: flex;
flex-direction: column;
gap: calc(var(--base) * .5);
margin-bottom: var(--base);
}

View File

@@ -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 (
<div className={baseClass}>
<h1>{heading}</h1>
{Boolean(description) && <p>{description}</p>}
</div>
)
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,28 +1,22 @@
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
}
@@ -33,16 +27,12 @@ export const handleAuthRedirect = ({
: undefined,
)
const adminLoginRoute = formatAdminURL({ adminRoute, path: loginRouteFromConfig })
const redirectTo = formatAdminURL({
adminRoute,
path: user ? unauthorizedRoute : 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 parsedLoginRouteSearchParams = qs.parse(redirectTo.split('?')[1] ?? '')
const searchParamsWithRedirect = `${qs.stringify(
{
@@ -52,6 +42,5 @@ export const handleAuthRedirect = ({
{ addQueryPrefix: true },
)}`
redirect(`${loginRoute.split('?')[0]}${searchParamsWithRedirect}`)
}
return `${redirectTo.split('?')[0]}${searchParamsWithRedirect}`
}

View File

@@ -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<InitPageResult> => {
@@ -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,

View File

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

View File

@@ -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 (
<Fragment>
<h1>{t('authentication:emailSent')}</h1>
<p>{t('authentication:checkYourEmailForPasswordReset')}</p>
</Fragment>
<FormHeader
description={t('authentication:checkYourEmailForPasswordReset')}
heading={t('authentication:emailSent')}
/>
)
}
@@ -68,12 +70,14 @@ export const ForgotPasswordForm: React.FC = () => {
initialState={initialState}
method="POST"
>
<h1>{t('authentication:forgotPassword')}</h1>
<p>
{loginWithUsername
<FormHeader
description={
loginWithUsername
? t('authentication:forgotPasswordUsernameInstructions')
: t('authentication:forgotPasswordEmailInstructions')}
</p>
: t('authentication:forgotPasswordEmailInstructions')
}
heading={t('authentication:forgotPassword')}
/>
{loginWithUsername ? (
<TextField
@@ -120,7 +124,7 @@ export const ForgotPasswordForm: React.FC = () => {
}
/>
)}
<FormSubmit>{t('general:submit')}</FormSubmit>
<FormSubmit size="large">{t('general:submit')}</FormSubmit>
</Form>
)
}

View File

@@ -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,8 +32,8 @@ export const ForgotPasswordView: React.FC<AdminViewProps> = ({ initPageResult })
if (user) {
return (
<Fragment>
<h1>{i18n.t('authentication:alreadyLoggedIn')}</h1>
<p>
<FormHeader
description={
<Translation
elements={{
'0': ({ children }) => (
@@ -49,8 +50,9 @@ export const ForgotPasswordView: React.FC<AdminViewProps> = ({ initPageResult })
i18nKey="authentication:loggedInChangePassword"
t={i18n.t}
/>
</p>
<br />
}
heading={i18n.t('authentication:alreadyLoggedIn')}
/>
<Button buttonStyle="secondary" el="link" Link={Link} size="large" to={adminRoute}>
{i18n.t('general:backToDashboard')}
</Button>

View File

@@ -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<Args> = ({ token }) => {
export const ResetPasswordForm: React.FC<Args> = ({ token }) => {
const i18n = useTranslation()
const {
config: {
@@ -47,13 +45,11 @@ export const ResetPasswordClient: React.FC<Args> = ({ token }) => {
} = useConfig()
const history = useRouter()
const { fetchFullUser } = useAuth()
const onSuccess = React.useCallback(
async (data) => {
if (data.token) {
await fetchFullUser()
const onSuccess = React.useCallback(async () => {
const user = await fetchFullUser()
if (user) {
history.push(adminRoute)
} else {
history.push(
@@ -62,11 +58,8 @@ export const ResetPasswordClient: React.FC<Args> = ({ token }) => {
path: loginRoute,
}),
)
toast.success(i18n.t('general:updatedSuccessfully'))
}
},
[adminRoute, fetchFullUser, history, i18n, loginRoute],
)
}, [adminRoute, fetchFullUser, history, loginRoute])
return (
<Form
@@ -75,7 +68,7 @@ export const ResetPasswordClient: React.FC<Args> = ({ token }) => {
method="POST"
onSuccess={onSuccess}
>
<div className={'inputWrap'}>
<div className="inputWrap">
<PasswordField
field={{
name: 'password',

View File

@@ -1,33 +1,11 @@
@import '../../scss/styles.scss';
@layer payload-default {
.reset-password {
&__wrap {
z-index: 1;
position: relative;
display: flex;
flex-direction: column;
align-items: flex-start;
gap: base(0.8);
max-width: base(36);
& > form {
width: 100%;
& > .inputWrap {
.reset-password__wrap {
.inputWrap {
display: flex;
flex-direction: column;
gap: base(0.8);
> * {
margin: 0;
}
}
}
& > .btn {
margin: 0;
}
}
}
}

View File

@@ -5,8 +5,9 @@ import { formatAdminURL, Translation } from '@payloadcms/ui/shared'
import LinkImport from 'next/link.js'
import React from 'react'
import { ResetPasswordClient } from './index.client.js'
import { FormHeader } from '../../elements/FormHeader/index.js'
import './index.scss'
import { ResetPasswordForm } from './ResetPasswordForm/index.js'
export const resetPasswordBaseClass = 'reset-password'
@@ -29,7 +30,7 @@ export const ResetPassword: React.FC<AdminViewProps> = ({ initPageResult, params
const {
admin: {
routes: { account: accountRoute },
routes: { account: accountRoute, login: loginRoute },
},
routes: { admin: adminRoute },
} = config
@@ -37,8 +38,8 @@ export const ResetPassword: React.FC<AdminViewProps> = ({ initPageResult, params
if (user) {
return (
<div className={`${resetPasswordBaseClass}__wrap`}>
<h1>{i18n.t('authentication:alreadyLoggedIn')}</h1>
<p>
<FormHeader
description={
<Translation
elements={{
'0': ({ children }) => (
@@ -55,7 +56,9 @@ export const ResetPassword: React.FC<AdminViewProps> = ({ initPageResult, params
i18nKey="authentication:loggedInChangePassword"
t={i18n.t}
/>
</p>
}
heading={i18n.t('authentication:alreadyLoggedIn')}
/>
<Button buttonStyle="secondary" el="link" Link={Link} size="large" to={adminRoute}>
{i18n.t('general:backToDashboard')}
</Button>
@@ -65,8 +68,16 @@ export const ResetPassword: React.FC<AdminViewProps> = ({ initPageResult, params
return (
<div className={`${resetPasswordBaseClass}__wrap`}>
<h1>{i18n.t('authentication:resetPassword')}</h1>
<ResetPasswordClient token={token} />
<FormHeader heading={i18n.t('authentication:resetPassword')} />
<ResetPasswordForm token={token} />
<Link
href={formatAdminURL({
adminRoute,
path: loginRoute,
})}
>
{i18n.t('authentication:backToLogin')}
</Link>
</div>
)
}

View File

@@ -92,7 +92,6 @@ export const getViewFromConfig = ({
}
templateClassName = 'dashboard'
templateType = 'default'
initPageOptions.redirectUnauthenticatedUser = true
}
break
}
@@ -132,7 +131,6 @@ export const getViewFromConfig = ({
templateType = 'minimal'
if (viewKey === 'account') {
initPageOptions.redirectUnauthenticatedUser = true
templateType = 'default'
}
}
@@ -150,7 +148,6 @@ export const getViewFromConfig = ({
if (isCollection) {
// --> /collections/:collectionSlug
initPageOptions.redirectUnauthenticatedUser = true
ViewToRender = {
Component: ListView,
@@ -160,7 +157,6 @@ export const getViewFromConfig = ({
templateType = 'default'
} else if (isGlobal) {
// --> /globals/:globalSlug
initPageOptions.redirectUnauthenticatedUser = true
ViewToRender = {
Component: DocumentView,
@@ -187,7 +183,6 @@ export const getViewFromConfig = ({
// --> /collections/:collectionSlug/:id/versions
// --> /collections/:collectionSlug/:id/versions/:versionId
// --> /collections/:collectionSlug/:id/api
initPageOptions.redirectUnauthenticatedUser = true
ViewToRender = {
Component: DocumentView,
@@ -201,7 +196,6 @@ export const getViewFromConfig = ({
// --> /globals/:globalSlug/preview
// --> /globals/:globalSlug/versions/:versionId
// --> /globals/:globalSlug/api
initPageOptions.redirectUnauthenticatedUser = true
ViewToRender = {
Component: DocumentView,

View File

@@ -72,6 +72,10 @@ export const RootPage = async ({
const initPageResult = await initPage(initPageOptions)
if (typeof initPageResult?.redirectTo === 'string') {
redirect(initPageResult.redirectTo)
}
if (initPageResult) {
dbHasUser = await initPageResult?.req.payload.db
.findOne({
@@ -137,8 +141,8 @@ export const RootPage = async ({
visibleEntities={{
// The reason we are not passing in initPageResult.visibleEntities directly is due to a "Cannot assign to read only property of object '#<Object>" error introduced in React 19
// which this caused as soon as initPageResult.visibleEntities is passed in
collections: initPageResult.visibleEntities?.collections,
globals: initPageResult.visibleEntities?.globals,
collections: initPageResult?.visibleEntities?.collections,
globals: initPageResult?.visibleEntities?.globals,
}}
>
{RenderedView}

View File

@@ -2,37 +2,8 @@
@layer payload-default {
.unauthorized {
margin-top: var(--base);
& > * {
&:first-child {
margin-top: 0;
}
&:last-child {
margin-bottom: 0;
}
}
&__button {
margin: 0;
}
&--margin-top-large {
margin-top: calc(var(--base) * 2);
}
@include large-break {
&--margin-top-large {
margin-top: var(--base);
}
}
@include small-break {
margin-top: calc(var(--base) / 2);
&--margin-top-large {
margin-top: calc(var(--base) / 2);
}
}
}
}

View File

@@ -1,9 +1,11 @@
import type { AdminViewComponent, PayloadServerReactComponent } from 'payload'
import { Button, Gutter } from '@payloadcms/ui'
import { Button } from '@payloadcms/ui'
import { formatAdminURL } from '@payloadcms/ui/shared'
import LinkImport from 'next/link.js'
import React from 'react'
import { FormHeader } from '../../elements/FormHeader/index.js'
import './index.scss'
const Link = (LinkImport.default || LinkImport) as unknown as typeof LinkImport.default
@@ -23,24 +25,31 @@ export const UnauthorizedView: PayloadServerReactComponent<AdminViewComponent> =
admin: {
routes: { logout: logoutRoute },
},
routes: { admin: adminRoute },
},
},
},
} = initPageResult
return (
<Gutter className={baseClass}>
<h2>{i18n.t('error:unauthorized')}</h2>
<p>{i18n.t('error:notAllowedToAccessPage')}</p>
<div className={baseClass}>
<FormHeader
description={i18n.t('error:notAllowedToAccessPage')}
heading={i18n.t('error:unauthorized')}
/>
<Button
className={`${baseClass}__button`}
el="link"
Link={Link}
size="large"
to={logoutRoute}
to={formatAdminURL({
adminRoute,
path: logoutRoute,
})}
>
{i18n.t('authentication:logOut')}
</Button>
</Gutter>
</div>
)
}

View File

@@ -0,0 +1,32 @@
'use client'
import { toast } from '@payloadcms/ui'
import { useRouter } from 'next/navigation.js'
import React, { useEffect } from 'react'
type Props = {
message: string
redirectTo: string
}
export function ToastAndRedirect({ message, redirectTo }: Props) {
const router = useRouter()
const hasToastedRef = React.useRef(false)
useEffect(() => {
let timeoutID
if (toast) {
timeoutID = setTimeout(() => {
toast.success(message)
hasToastedRef.current = true
router.push(redirectTo)
}, 100)
}
return () => {
if (timeoutID) {
clearTimeout(timeoutID)
}
}
}, [router, redirectTo, message])
return null
}

View File

@@ -1,10 +1,10 @@
import type { AdminViewProps } from 'payload'
import { formatAdminURL } from '@payloadcms/ui/shared'
import { redirect } from 'next/navigation.js'
import React from 'react'
import { Logo } from '../../elements/Logo/index.js'
import { ToastAndRedirect } from './index.client.js'
import './index.scss'
export const verifyBaseClass = 'verify'
@@ -33,6 +33,7 @@ export const Verify: React.FC<AdminViewProps> = async ({
} = config
let textToRender
let isVerified = false
try {
await req.payload.verifyEmail({
@@ -40,15 +41,21 @@ export const Verify: React.FC<AdminViewProps> = async ({
token,
})
return redirect(formatAdminURL({ adminRoute, path: '/login' }))
isVerified = true
textToRender = req.t('authentication:emailVerified')
} catch (e) {
// already verified
if (e?.status === 202) {
redirect(formatAdminURL({ adminRoute, path: '/login' }))
}
textToRender = req.t('authentication:unableToVerify')
}
if (isVerified) {
return (
<ToastAndRedirect
message={req.t('authentication:emailVerified')}
redirectTo={formatAdminURL({ adminRoute, path: '/login' })}
/>
)
}
return (
<React.Fragment>
<div className={`${verifyBaseClass}__brand`}>

View File

@@ -53,6 +53,7 @@ export type InitPageResult = {
languageOptions: LanguageOptions
locale?: Locale
permissions: Permissions
redirectTo?: string
req: PayloadRequest
translations: ClientTranslationsObject
visibleEntities: VisibleEntities

View File

@@ -1,4 +1,3 @@
import type { Payload } from '../index.js'
import type { SanitizedCollectionConfig } from './../collections/config/types.js'
type CookieOptions = {
@@ -125,63 +124,63 @@ export const getCookieExpiration = ({ seconds = 7200 }: GetCookieExpirationArgs)
type GeneratePayloadCookieArgs = {
/* The auth collection config */
collectionConfig: SanitizedCollectionConfig
/* An instance of payload */
payload: Payload
collectionAuthConfig: SanitizedCollectionConfig['auth']
/* Prefix to scope the cookie */
cookiePrefix: string
/* The returnAs value */
returnCookieAsObject?: boolean
/* The token to be stored in the cookie */
token: string
}
export const generatePayloadCookie = <T extends GeneratePayloadCookieArgs>({
collectionConfig,
payload,
collectionAuthConfig,
cookiePrefix,
returnCookieAsObject = false,
token,
}: T): T['returnCookieAsObject'] extends true ? CookieObject : string => {
const sameSite =
typeof collectionConfig.auth.cookies.sameSite === 'string'
? collectionConfig.auth.cookies.sameSite
: collectionConfig.auth.cookies.sameSite
typeof collectionAuthConfig.cookies.sameSite === 'string'
? collectionAuthConfig.cookies.sameSite
: collectionAuthConfig.cookies.sameSite
? 'Strict'
: undefined
return generateCookie<T['returnCookieAsObject']>({
name: `${payload.config.cookiePrefix}-token`,
domain: collectionConfig.auth.cookies.domain ?? undefined,
expires: getCookieExpiration({ seconds: collectionConfig.auth.tokenExpiration }),
name: `${cookiePrefix}-token`,
domain: collectionAuthConfig.cookies.domain ?? undefined,
expires: getCookieExpiration({ seconds: collectionAuthConfig.tokenExpiration }),
httpOnly: true,
path: '/',
returnCookieAsObject,
sameSite,
secure: collectionConfig.auth.cookies.secure,
secure: collectionAuthConfig.cookies.secure,
value: token,
})
}
export const generateExpiredPayloadCookie = <T extends Omit<GeneratePayloadCookieArgs, 'token'>>({
collectionConfig,
payload,
collectionAuthConfig,
cookiePrefix,
returnCookieAsObject = false,
}: T): T['returnCookieAsObject'] extends true ? CookieObject : string => {
const sameSite =
typeof collectionConfig.auth.cookies.sameSite === 'string'
? collectionConfig.auth.cookies.sameSite
: collectionConfig.auth.cookies.sameSite
typeof collectionAuthConfig.cookies.sameSite === 'string'
? collectionAuthConfig.cookies.sameSite
: collectionAuthConfig.cookies.sameSite
? 'Strict'
: undefined
const expires = new Date(Date.now() - 1000)
return generateCookie<T['returnCookieAsObject']>({
name: `${payload.config.cookiePrefix}-token`,
domain: collectionConfig.auth.cookies.domain ?? undefined,
name: `${cookiePrefix}-token`,
domain: collectionAuthConfig.cookies.domain ?? undefined,
expires,
httpOnly: true,
path: '/',
returnCookieAsObject,
sameSite,
secure: collectionConfig.auth.cookies.secure,
secure: collectionAuthConfig.cookies.secure,
})
}

View File

@@ -81,7 +81,7 @@ export const resetPasswordOperation = async (args: Arguments): Promise<Result> =
user.resetPasswordExpiration = new Date().toISOString()
if (collectionConfig.auth.verify) {
user._verified = true
user._verified = Boolean(user._verified)
}
const doc = await payload.db.updateOne({

View File

@@ -34,9 +34,6 @@ export const verifyEmailOperation = async (args: Args): Promise<boolean> => {
if (!user) {
throw new APIError('Verification token is invalid.', httpStatus.FORBIDDEN)
}
if (user && user._verified === true) {
throw new APIError('This account has already been activated.', httpStatus.ACCEPTED)
}
await req.payload.db.updateOne({
id: user.id,

View File

@@ -1,5 +1,13 @@
export {
generateCookie,
generateExpiredPayloadCookie,
generatePayloadCookie,
getCookieExpiration,
parseCookies,
} from '../auth/cookies.js'
export { parsePayloadComponent } from '../bin/generateImportMap/parsePayloadComponent.js'
export { defaults as collectionDefaults } from '../collections/config/defaults.js'
export { serverProps } from '../config/types.js'
export {
@@ -20,19 +28,19 @@ export {
tabHasName,
valueIsValueWithRelation,
} from '../fields/config/types.js'
export * from '../fields/validations.js'
export { validOperators } from '../types/constants.js'
export { formatFilesize } from '../uploads/formatFilesize.js'
export { isImage } from '../uploads/isImage.js'
export {
deepCopyObject,
deepCopyObjectComplex,
deepCopyObjectSimple,
} from '../utilities/deepCopyObject.js'
export {
deepMerge,
deepMergeWithCombinedArrays,
@@ -41,8 +49,8 @@ export {
} from '../utilities/deepMerge.js'
export { fieldSchemaToJSON } from '../utilities/fieldSchemaToJSON.js'
export { getDataByPath } from '../utilities/getDataByPath.js'
export { getSiblingData } from '../utilities/getSiblingData.js'
export { getUniqueListBy } from '../utilities/getUniqueListBy.js'
@@ -66,6 +74,5 @@ export { unflatten } from '../utilities/unflatten.js'
export { wait } from '../utilities/wait.js'
export { default as wordBoundariesRegex } from '../utilities/wordBoundariesRegex.js'
export { versionDefaults } from '../versions/defaults.js'
export { deepMergeSimple } from '@payloadcms/translations/utilities'

View File

@@ -184,7 +184,7 @@ export const arTranslations: DefaultTranslationsObject = {
backToDashboard: 'العودة للوحة التّحكّم',
cancel: 'إلغاء',
changesNotSaved: 'لم يتمّ حفظ التّغييرات. إن غادرت الآن ، ستفقد تغييراتك.',
clearAll: undefined,
clearAll: 'امسح الكل',
close: 'إغلاق',
collapse: 'طيّ',
collections: 'المجموعات',
@@ -407,7 +407,7 @@ export const arTranslations: DefaultTranslationsObject = {
lastSavedAgo: 'تم الحفظ آخر مرة قبل {{distance}}',
noFurtherVersionsFound: 'لم يتمّ العثور على نسخات أخرى',
noRowsFound: 'لم يتمّ العثور على {{label}}',
noRowsSelected: undefined,
noRowsSelected: 'لم يتم اختيار {{label}}',
preview: 'معاينة',
previouslyPublished: 'نشر سابقا',
problemRestoringVersion: 'حدث خطأ في استعادة هذه النّسخة',

View File

@@ -186,7 +186,7 @@ export const azTranslations: DefaultTranslationsObject = {
cancel: 'Ləğv et',
changesNotSaved:
'Dəyişiklikləriniz saxlanılmayıb. İndi çıxsanız, dəyişikliklərinizi itirəcəksiniz.',
clearAll: undefined,
clearAll: 'Hamısını təmizlə',
close: 'Bağla',
collapse: 'Bağla',
collections: 'Kolleksiyalar',
@@ -414,7 +414,7 @@ export const azTranslations: DefaultTranslationsObject = {
lastSavedAgo: '{{distance}} əvvəl son yadda saxlanıldı',
noFurtherVersionsFound: 'Başqa versiyalar tapılmadı',
noRowsFound: 'Heç bir {{label}} tapılmadı',
noRowsSelected: undefined,
noRowsSelected: 'Heç bir {{label}} seçilməyib',
preview: 'Öncədən baxış',
previouslyPublished: 'Daha əvvəl nəşr olunmuş',
problemRestoringVersion: 'Bu versiyanın bərpasında problem yaşandı',

View File

@@ -185,7 +185,7 @@ export const bgTranslations: DefaultTranslationsObject = {
backToDashboard: 'Обратно към таблото',
cancel: 'Отмени',
changesNotSaved: 'Промените ти не са запазени. Ако напуснеш сега, ще ги загубиш.',
clearAll: undefined,
clearAll: 'Изчисти всичко',
close: 'Затвори',
collapse: 'Свий',
collections: 'Колекции',
@@ -413,7 +413,7 @@ export const bgTranslations: DefaultTranslationsObject = {
lastSavedAgo: 'последно запазено преди {{distance}}',
noFurtherVersionsFound: 'Не са открити повече версии',
noRowsFound: 'Не е открит {{label}}',
noRowsSelected: undefined,
noRowsSelected: 'Не е избран {{label}}',
preview: 'Предварителен преглед',
previouslyPublished: 'Предишно публикувано',
problemRestoringVersion: 'Имаше проблем при възстановяването на тази версия',

View File

@@ -185,7 +185,7 @@ export const csTranslations: DefaultTranslationsObject = {
backToDashboard: 'Zpět na nástěnku',
cancel: 'Zrušit',
changesNotSaved: 'Vaše změny nebyly uloženy. Pokud teď odejdete, ztratíte své změny.',
clearAll: undefined,
clearAll: 'Vymazat vše',
close: 'Zavřít',
collapse: 'Sbalit',
collections: 'Kolekce',
@@ -412,7 +412,7 @@ export const csTranslations: DefaultTranslationsObject = {
lastSavedAgo: 'Naposledy uloženo před {{distance}}',
noFurtherVersionsFound: 'Nenalezeny další verze',
noRowsFound: 'Nenalezen {{label}}',
noRowsSelected: undefined,
noRowsSelected: 'Nebyl vybrán žádný {{label}}',
preview: 'Náhled',
previouslyPublished: 'Dříve publikováno',
problemRestoringVersion: 'Při obnovování této verze došlo k problému',

View File

@@ -190,7 +190,7 @@ export const deTranslations: DefaultTranslationsObject = {
cancel: 'Abbrechen',
changesNotSaved:
'Deine Änderungen wurden nicht gespeichert. Wenn du diese Seite verlässt, gehen deine Änderungen verloren.',
clearAll: undefined,
clearAll: 'Alles löschen',
close: 'Schließen',
collapse: 'Einklappen',
collections: 'Sammlungen',
@@ -418,7 +418,7 @@ export const deTranslations: DefaultTranslationsObject = {
lastSavedAgo: 'Zuletzt vor {{distance}} gespeichert',
noFurtherVersionsFound: 'Keine weiteren Versionen vorhanden',
noRowsFound: 'Kein {{label}} gefunden',
noRowsSelected: undefined,
noRowsSelected: 'Kein {{label}} ausgewählt',
preview: 'Vorschau',
previouslyPublished: 'Zuvor Veröffentlicht',
problemRestoringVersion: 'Es gab ein Problem bei der Wiederherstellung dieser Version',

View File

@@ -66,9 +66,8 @@ export const enTranslations = {
successfullyRegisteredFirstUser: 'Successfully registered first user.',
successfullyUnlocked: 'Successfully unlocked',
tokenRefreshSuccessful: 'Token refresh successful.',
username: 'Username',
unableToVerify: 'Unable to Verify',
username: 'Username',
verified: 'Verified',
verifiedSuccessfully: 'Verified Successfully',
verify: 'Verify',

View File

@@ -190,7 +190,7 @@ export const esTranslations: DefaultTranslationsObject = {
cancel: 'Cancelar',
changesNotSaved:
'Tus cambios no han sido guardados. Si te sales ahora, se perderán tus cambios.',
clearAll: undefined,
clearAll: 'Borrar todo',
close: 'Cerrar',
collapse: 'Colapsar',
collections: 'Colecciones',
@@ -418,7 +418,7 @@ export const esTranslations: DefaultTranslationsObject = {
lastSavedAgo: 'Guardado por última vez hace {{distance}}',
noFurtherVersionsFound: 'No se encontraron más versiones',
noRowsFound: 'No encontramos {{label}}',
noRowsSelected: undefined,
noRowsSelected: 'No se ha seleccionado ninguna {{etiqueta}}',
preview: 'Previsualizar',
previouslyPublished: 'Publicado Anteriormente',
problemRestoringVersion: 'Ocurrió un problema al restaurar esta versión',

View File

@@ -185,7 +185,7 @@ export const faTranslations: DefaultTranslationsObject = {
cancel: 'لغو',
changesNotSaved:
'تغییرات شما ذخیره نشده، اگر این برگه را ترک کنید. تمام تغییرات از دست خواهد رفت.',
clearAll: undefined,
clearAll: 'همه را پاک کنید',
close: 'بستن',
collapse: 'بستن',
collections: 'مجموعه‌ها',
@@ -411,7 +411,7 @@ export const faTranslations: DefaultTranslationsObject = {
lastSavedAgo: 'آخرین بار {{distance}} پیش ذخیره شد',
noFurtherVersionsFound: 'نگارش دیگری یافت نشد',
noRowsFound: 'هیچ {{label}} یافت نشد',
noRowsSelected: undefined,
noRowsSelected: 'هیچ {{label}} ای انتخاب نشده است',
preview: 'پیش‌نمایش',
previouslyPublished: 'قبلا منتشر شده',
problemRestoringVersion: 'مشکلی در بازیابی این نگارش وجود دارد',

View File

@@ -193,7 +193,7 @@ export const frTranslations: DefaultTranslationsObject = {
cancel: 'Annuler',
changesNotSaved:
'Vos modifications nont pas été enregistrées. Vous perdrez vos modifications si vous quittez maintenant.',
clearAll: undefined,
clearAll: 'Tout effacer',
close: 'Fermer',
collapse: 'Réduire',
collections: 'Collections',
@@ -425,7 +425,7 @@ export const frTranslations: DefaultTranslationsObject = {
lastSavedAgo: 'Dernière sauvegarde il y a {{distance}}',
noFurtherVersionsFound: 'Aucune autre version trouvée',
noRowsFound: 'Aucun(e) {{label}} trouvé(e)',
noRowsSelected: undefined,
noRowsSelected: 'Aucune {{étiquette}} sélectionnée',
preview: 'Aperçu',
previouslyPublished: 'Précédemment publié',
problemRestoringVersion: 'Un problème est survenu lors de la restauration de cette version',

View File

@@ -181,7 +181,7 @@ export const heTranslations: DefaultTranslationsObject = {
backToDashboard: 'חזרה ללוח המחוונים',
cancel: 'ביטול',
changesNotSaved: 'השינויים שלך לא נשמרו. אם תצא כעת, תאבד את השינויים שלך.',
clearAll: undefined,
clearAll: 'נקה הכל',
close: 'סגור',
collapse: 'כווץ',
collections: 'אוספים',
@@ -260,7 +260,7 @@ export const heTranslations: DefaultTranslationsObject = {
nothingFound: 'לא נמצא כלום',
noValue: 'אין ערך',
of: 'מתוך',
only: undefined,
only: 'רק',
open: 'פתח',
or: 'או',
order: 'סדר',
@@ -401,7 +401,7 @@ export const heTranslations: DefaultTranslationsObject = {
lastSavedAgo: 'נשמר לאחרונה לפני {{distance}}',
noFurtherVersionsFound: 'לא נמצאו עוד גרסאות',
noRowsFound: 'לא נמצאו {{label}}',
noRowsSelected: undefined,
noRowsSelected: 'לא נבחר {{תווית}}',
preview: 'תצוגה מקדימה',
previouslyPublished: 'פורסם בעבר',
problemRestoringVersion: 'הייתה בעיה בשחזור הגרסה הזו',

View File

@@ -34,7 +34,6 @@ export const hrTranslations: DefaultTranslationsObject = {
generateNewAPIKey: 'Generiraj novi API ključ',
generatingNewAPIKeyWillInvalidate:
'Generiranje novog API ključa će <1>poništiti</1> prethodni ključ. Jeste li sigurni da želite nastaviti?',
newAPIKeyGenerated: 'New API ključ generiran.',
lockUntil: 'Zaključaj dok',
logBackIn: 'Ponovno se prijavite',
loggedIn: 'Za prijavu s drugim korisničkim računom potrebno je prvo <0>odjaviti se</0>',
@@ -54,6 +53,7 @@ export const hrTranslations: DefaultTranslationsObject = {
logoutUser: 'Odjava korisnika',
newAccountCreated:
'Novi račun je izrađen. Pristupite računu klikom na: <a href="{{serverURL}}">{{serverURL}}</a>. Molimo kliknite na sljedeću poveznicu ili zalijepite URL, koji se nalazi ispod, u preglednik da biste potvrdili svoju e-mail adresu: <a href="{{verificationURL}}">{{verificationURL}}</a><br> Nakon što potvrdite e-mail adresu, moći ćete se prijaviti.',
newAPIKeyGenerated: 'New API ključ generiran.',
newPassword: 'Nova lozinka',
passed: 'Autentifikacija je prošla',
passwordResetSuccessfully: 'Lozinka uspješno resetirana.',
@@ -111,11 +111,11 @@ export const hrTranslations: DefaultTranslationsObject = {
problemUploadingFile: 'Došlo je do problema pri učitavanju datoteke.',
tokenInvalidOrExpired: 'Token je neispravan ili je istekao.',
tokenNotProvided: 'Token nije pružen.',
unPublishingDocument: 'Došlo je do problema pri poništavanju objave ovog dokumenta.',
unableToDeleteCount: 'Nije moguće izbrisati {{count}} od {{total}} {{label}}.',
unableToUpdateCount: 'Nije moguće ažurirati {{count}} od {{total}} {{label}}.',
unauthorized: 'Neovlašteno, morate biti prijavljeni da biste uputili ovaj zahtjev.',
unknown: 'Došlo je do nepoznate pogreške.',
unPublishingDocument: 'Došlo je do problema pri poništavanju objave ovog dokumenta.',
unspecific: 'Došlo je do pogreške.',
userEmailAlreadyRegistered: 'Korisnik s navedenom e-mail adresom je već registriran.',
userLocked: 'Ovaj korisnik je zaključan zbog previše neuspješnih pokušaja prijave.',
@@ -186,7 +186,7 @@ export const hrTranslations: DefaultTranslationsObject = {
backToDashboard: 'Natrag na nadzornu ploču',
cancel: 'Otkaži',
changesNotSaved: 'Vaše promjene nisu spremljene. Ako izađete sada, izgubit ćete promjene.',
clearAll: undefined,
clearAll: 'Očisti sve',
close: 'Zatvori',
collapse: 'Sažmi',
collections: 'Kolekcije',
@@ -258,11 +258,12 @@ export const hrTranslations: DefaultTranslationsObject = {
next: 'Sljedeće',
noFiltersSet: 'Nema postavljenih filtera',
noLabel: '<Nema {{label}}>',
notFound: 'Nije pronađeno',
nothingFound: 'Ništa nije pronađeno',
none: 'Nijedan',
noOptions: 'Nema opcija',
noResults: 'Nije pronađen nijedan {{label}}. Ili {{label}} još uvijek ne postoji ili nijedan od odgovara postavljenim filterima.',
noResults:
'Nije pronađen nijedan {{label}}. Ili {{label}} još uvijek ne postoji ili nijedan od odgovara postavljenim filterima.',
notFound: 'Nije pronađeno',
nothingFound: 'Ništa nije pronađeno',
noValue: 'Bez vrijednosti',
of: 'od',
only: 'Samo',
@@ -410,14 +411,14 @@ export const hrTranslations: DefaultTranslationsObject = {
lastSavedAgo: 'Zadnji put spremljeno prije {{distance}',
noFurtherVersionsFound: 'Nisu pronađene daljnje verzije',
noRowsFound: '{{label}} nije pronađeno',
noRowsSelected: undefined,
noRowsSelected: 'Nije odabrana {{oznaka}}',
preview: 'Pregled',
previouslyPublished: 'Prethodno objavljeno',
problemRestoringVersion: 'Nastao je problem pri vraćanju ove verzije',
publish: 'Objaviti',
publishChanges: 'Objavi promjene',
published: 'Objavljeno',
publishIn: undefined,
publishIn: 'Objavi na {{locale}}',
publishing: 'Objavljivanje',
restoreAsDraft: 'Vrati kao skicu',
restoredSuccessfully: 'Uspješno vraćeno.',

View File

@@ -188,7 +188,7 @@ export const huTranslations: DefaultTranslationsObject = {
cancel: 'Mégsem',
changesNotSaved:
'A módosítások nem lettek mentve. Ha most távozik, elveszíti a változtatásokat.',
clearAll: undefined,
clearAll: 'Törölj mindent',
close: 'Bezárás',
collapse: 'Összecsukás',
collections: 'Gyűjtemények',
@@ -418,7 +418,7 @@ export const huTranslations: DefaultTranslationsObject = {
lastSavedAgo: 'Utoljára mentve {{distance}} órája',
noFurtherVersionsFound: 'További verziók nem találhatók',
noRowsFound: 'Nem található {{label}}',
noRowsSelected: undefined,
noRowsSelected: 'Nincs {{címke}} kiválasztva',
preview: 'Előnézet',
previouslyPublished: 'Korábban Közzétéve',
problemRestoringVersion: 'Hiba történt a verzió visszaállításakor',

View File

@@ -189,7 +189,7 @@ export const itTranslations: DefaultTranslationsObject = {
backToDashboard: 'Torna alla Dashboard',
cancel: 'Cancella',
changesNotSaved: 'Le tue modifiche non sono state salvate. Se esci ora, verranno perse.',
clearAll: undefined,
clearAll: 'Cancella Tutto',
close: 'Chiudere',
collapse: 'Comprimi',
collections: 'Collezioni',
@@ -418,7 +418,7 @@ export const itTranslations: DefaultTranslationsObject = {
lastSavedAgo: 'Ultimo salvataggio {{distance}} fa',
noFurtherVersionsFound: 'Non sono state trovate ulteriori versioni',
noRowsFound: 'Nessun {{label}} trovato',
noRowsSelected: undefined,
noRowsSelected: 'Nessuna {{etichetta}} selezionata',
preview: 'Anteprima',
previouslyPublished: 'Precedentemente Pubblicato',
problemRestoringVersion: 'Si è verificato un problema durante il ripristino di questa versione',

View File

@@ -186,7 +186,7 @@ export const jaTranslations: DefaultTranslationsObject = {
backToDashboard: 'ダッシュボードに戻る',
cancel: 'キャンセル',
changesNotSaved: '未保存の変更があります。このまま画面を離れると内容が失われます。',
clearAll: undefined,
clearAll: 'すべてクリア',
close: '閉じる',
collapse: '閉じる',
collections: 'コレクション',
@@ -412,7 +412,7 @@ export const jaTranslations: DefaultTranslationsObject = {
lastSavedAgo: '{{distance}}前に最後に保存されました',
noFurtherVersionsFound: 'その他のバージョンは見つかりませんでした。',
noRowsFound: '{{label}} は未設定です',
noRowsSelected: undefined,
noRowsSelected: '選択された{{label}}はありません',
preview: 'プレビュー',
previouslyPublished: '以前に公開された',
problemRestoringVersion: 'このバージョンの復元に問題がありました。',

View File

@@ -185,7 +185,7 @@ export const koTranslations: DefaultTranslationsObject = {
backToDashboard: '대시보드로 돌아가기',
cancel: '취소',
changesNotSaved: '변경 사항이 저장되지 않았습니다. 지금 떠나면 변경 사항을 잃게 됩니다.',
clearAll: undefined,
clearAll: '모두 지우기',
close: '닫기',
collapse: '접기',
collections: '컬렉션',
@@ -408,7 +408,7 @@ export const koTranslations: DefaultTranslationsObject = {
lastSavedAgo: '마지막으로 저장한지 {{distance}} 전',
noFurtherVersionsFound: '더 이상의 버전을 찾을 수 없습니다.',
noRowsFound: '{{label}}을(를) 찾을 수 없음',
noRowsSelected: undefined,
noRowsSelected: '선택된 {{label}} 없음',
preview: '미리보기',
previouslyPublished: '이전에 발표된',
problemRestoringVersion: '이 버전을 복원하는 중 문제가 발생했습니다.',

View File

@@ -188,7 +188,7 @@ export const myTranslations: DefaultTranslationsObject = {
cancel: 'မလုပ်တော့ပါ။',
changesNotSaved:
'သင်၏ပြောင်းလဲမှုများကို မသိမ်းဆည်းရသေးပါ။ ယခု စာမျက်နှာက ထွက်လိုက်ပါက သင်၏ပြောင်းလဲမှုများ အကုန် ဆုံးရှုံးသွားပါမည်။ အကုန်နော်။',
clearAll: undefined,
clearAll: 'အားလုံးကိုရှင်းလင်းပါ',
close: 'ပိတ်',
collapse: 'ခေါက်သိမ်းပါ။',
collections: 'စုစည်းမှူများ',
@@ -420,7 +420,7 @@ export const myTranslations: DefaultTranslationsObject = {
lastSavedAgo: 'နောက်ဆုံး သိမ်းချက် {{distance}} ကြာပြီး',
noFurtherVersionsFound: 'နောက်ထပ်ဗားရှင်းများ မတွေ့ပါ။',
noRowsFound: '{{label}} အားမတွေ့ပါ။',
noRowsSelected: undefined,
noRowsSelected: 'Tiada {{label}} yang dipilih',
preview: 'နမူနာပြရန်',
previouslyPublished: 'တိုင်းရင်းသားထုတ်ဝေခဲ့',
problemRestoringVersion: 'ဤဗားရှင်းကို ပြန်လည်ရယူရာတွင် ပြဿနာရှိနေသည်။',

View File

@@ -186,7 +186,7 @@ export const nbTranslations: DefaultTranslationsObject = {
cancel: 'Avbryt',
changesNotSaved:
'Endringene dine er ikke lagret. Hvis du forlater nå, vil du miste endringene dine.',
clearAll: undefined,
clearAll: 'Tøm alt',
close: 'Lukk',
collapse: 'Skjul',
collections: 'Samlinger',
@@ -266,7 +266,7 @@ export const nbTranslations: DefaultTranslationsObject = {
nothingFound: 'Ingenting funnet',
noValue: 'Ingen verdi',
of: 'av',
only: undefined,
only: 'Bare',
open: 'Åpne',
or: 'Eller',
order: 'Rekkefølge',
@@ -414,7 +414,7 @@ export const nbTranslations: DefaultTranslationsObject = {
lastSavedAgo: 'Sist lagret {{distance}} siden',
noFurtherVersionsFound: 'Ingen flere versjoner funnet',
noRowsFound: 'Ingen {{label}} funnet',
noRowsSelected: undefined,
noRowsSelected: 'Ingen {{label}} valgt',
preview: 'Forhåndsvisning',
previouslyPublished: 'Tidligere Publisert',
problemRestoringVersion: 'Det oppstod et problem med gjenoppretting av denne versjonen',

View File

@@ -188,7 +188,7 @@ export const nlTranslations: DefaultTranslationsObject = {
cancel: 'Annuleren',
changesNotSaved:
'Uw wijzigingen zijn niet bewaard. Als u weggaat zullen de wijzigingen verloren gaan.',
clearAll: undefined,
clearAll: 'Alles wissen',
close: 'Dichtbij',
collapse: 'Samenvouwen',
collections: 'Collecties',
@@ -417,7 +417,7 @@ export const nlTranslations: DefaultTranslationsObject = {
lastSavedAgo: 'Laatst opgeslagen {{distance}} geleden',
noFurtherVersionsFound: 'Geen verdere versies gevonden',
noRowsFound: 'Geen {{label}} gevonden',
noRowsSelected: undefined,
noRowsSelected: 'Geen {{label}} geselecteerd',
preview: 'Voorbeeld',
previouslyPublished: 'Eerder gepubliceerd',
problemRestoringVersion: 'Er was een probleem bij het herstellen van deze versie',

View File

@@ -186,7 +186,7 @@ export const plTranslations: DefaultTranslationsObject = {
cancel: 'Anuluj',
changesNotSaved:
'Twoje zmiany nie zostały zapisane. Jeśli teraz wyjdziesz, stracisz swoje zmiany.',
clearAll: undefined,
clearAll: 'Wyczyść wszystko',
close: 'Zamknij',
collapse: 'Zwiń',
collections: 'Kolekcje',
@@ -414,14 +414,14 @@ export const plTranslations: DefaultTranslationsObject = {
lastSavedAgo: 'Ostatnio zapisane {{distance}} temu',
noFurtherVersionsFound: 'Nie znaleziono dalszych wersji',
noRowsFound: 'Nie znaleziono {{label}}',
noRowsSelected: undefined,
noRowsSelected: 'Nie wybrano {{etykieta}}',
preview: 'Podgląd',
previouslyPublished: 'Wcześniej opublikowane',
problemRestoringVersion: 'Wystąpił problem podczas przywracania tej wersji',
publish: 'Publikuj',
publishChanges: 'Opublikuj zmiany',
published: 'Opublikowano',
publishIn: undefined,
publishIn: 'Opublikuj w {{locale}}',
publishing: 'Publikacja',
restoreAsDraft: 'Przywróć jako szkic',
restoredSuccessfully: 'Przywrócono pomyślnie.',

View File

@@ -187,7 +187,7 @@ export const ptTranslations: DefaultTranslationsObject = {
cancel: 'Cancelar',
changesNotSaved:
'Suas alterações não foram salvas. Se você sair agora, essas alterações serão perdidas.',
clearAll: undefined,
clearAll: 'Limpar Tudo',
close: 'Fechar',
collapse: 'Recolher',
collections: 'Coleções',
@@ -267,7 +267,7 @@ export const ptTranslations: DefaultTranslationsObject = {
nothingFound: 'Nada encontrado',
noValue: 'Nenhum valor',
of: 'de',
only: undefined,
only: 'Apenas',
open: 'Abrir',
or: 'Ou',
order: 'Ordem',
@@ -415,14 +415,14 @@ export const ptTranslations: DefaultTranslationsObject = {
lastSavedAgo: 'Última gravação há {{distance}}',
noFurtherVersionsFound: 'Nenhuma outra versão encontrada',
noRowsFound: 'Nenhum(a) {{label}} encontrado(a)',
noRowsSelected: undefined,
noRowsSelected: 'Nenhum {{rótulo}} selecionado',
preview: 'Pré-visualização',
previouslyPublished: 'Publicado Anteriormente',
problemRestoringVersion: 'Ocorreu um problema ao restaurar essa versão',
publish: 'Publicar',
publishChanges: 'Publicar alterações',
published: 'Publicado',
publishIn: undefined,
publishIn: 'Publicar em {{locale}}',
publishing: 'Publicação',
restoreAsDraft: 'Restaurar como rascunho',
restoredSuccessfully: 'Restaurado com sucesso.',

View File

@@ -190,7 +190,7 @@ export const roTranslations: DefaultTranslationsObject = {
cancel: 'Anulați',
changesNotSaved:
'Modificările dvs. nu au fost salvate. Dacă plecați acum, vă veți pierde modificările.',
clearAll: undefined,
clearAll: 'Șterge tot',
close: 'Închide',
collapse: 'Colaps',
collections: 'Colecții',
@@ -422,7 +422,7 @@ export const roTranslations: DefaultTranslationsObject = {
lastSavedAgo: 'Ultima salvare acum {{distance}}',
noFurtherVersionsFound: 'Nu s-au găsit alte versiuni',
noRowsFound: 'Nu s-a găsit niciun {{label}}',
noRowsSelected: undefined,
noRowsSelected: 'Niciun {{etichetă}} selectat',
preview: 'Previzualizare',
previouslyPublished: 'Publicat anterior',
problemRestoringVersion: 'A existat o problemă la restaurarea acestei versiuni',

View File

@@ -185,7 +185,7 @@ export const rsTranslations: DefaultTranslationsObject = {
backToDashboard: 'Назад на контролни панел',
cancel: 'Откажи',
changesNotSaved: 'Ваше промене нису сачуване. Ако изађете сада, изгубићете промене.',
clearAll: undefined,
clearAll: 'Obriši sve',
close: 'Затвори',
collapse: 'Скупи',
collections: 'Колекције',
@@ -265,7 +265,7 @@ export const rsTranslations: DefaultTranslationsObject = {
nothingFound: 'Ништа није пронађено',
noValue: 'Без вредности',
of: 'Од',
only: undefined,
only: 'Samo',
open: 'Отвори',
or: 'Или',
order: 'Редослед',
@@ -409,14 +409,14 @@ export const rsTranslations: DefaultTranslationsObject = {
lastSavedAgo: 'Задњи пут сачувано пре {{distance}',
noFurtherVersionsFound: 'Нису пронађене наредне верзије',
noRowsFound: '{{label}} није пронађено',
noRowsSelected: undefined,
noRowsSelected: 'Nije odabrana {{label}}',
preview: 'Преглед',
previouslyPublished: 'Prethodno objavljeno',
problemRestoringVersion: 'Настао је проблем при враћању ове верзије',
publish: 'Објавити',
publishChanges: 'Објави промене',
published: 'Објављено',
publishIn: undefined,
publishIn: 'Objavi na {{locale}}',
publishing: 'Objavljivanje',
restoreAsDraft: 'Vrati kao nacrt',
restoredSuccessfully: 'Успешно враћено.',

View File

@@ -185,7 +185,7 @@ export const rsLatinTranslations: DefaultTranslationsObject = {
backToDashboard: 'Nazad na kontrolni panel',
cancel: 'Otkaži',
changesNotSaved: 'Vaše promene nisu sačuvane. Ako izađete sada, izgubićete promene.',
clearAll: undefined,
clearAll: 'Očisti sve',
close: 'Zatvori',
collapse: 'Skupi',
collections: 'Kolekcije',
@@ -265,7 +265,7 @@ export const rsLatinTranslations: DefaultTranslationsObject = {
nothingFound: 'Ništa nije pronađeno',
noValue: 'Bez vrednosti',
of: 'Od',
only: undefined,
only: 'Samo',
open: 'Otvori',
or: 'Ili',
order: 'Redosled',
@@ -410,7 +410,7 @@ export const rsLatinTranslations: DefaultTranslationsObject = {
lastSavedAgo: 'Zadnji put sačuvano pre {{distance}',
noFurtherVersionsFound: 'Nisu pronađene naredne verzije',
noRowsFound: '{{label}} nije pronađeno',
noRowsSelected: undefined,
noRowsSelected: 'Nije odabrana {{label}}',
preview: 'Pregled',
previouslyPublished: 'Prethodno objavljeno',
problemRestoringVersion: 'Nastao je problem pri vraćanju ove verzije',

View File

@@ -188,7 +188,7 @@ export const ruTranslations: DefaultTranslationsObject = {
cancel: 'Отмена',
changesNotSaved:
'Ваши изменения не были сохранены. Если вы сейчас уйдете, то потеряете свои изменения.',
clearAll: undefined,
clearAll: 'Очистить все',
close: 'Закрыть',
collapse: 'Свернуть',
collections: 'Коллекции',
@@ -416,7 +416,7 @@ export const ruTranslations: DefaultTranslationsObject = {
lastSavedAgo: 'Последний раз сохранено {{distance}} назад',
noFurtherVersionsFound: 'Другие версии не найдены',
noRowsFound: 'Не найдено {{label}}',
noRowsSelected: undefined,
noRowsSelected: 'Не выбран {{label}}',
preview: 'Предпросмотр',
previouslyPublished: 'Ранее опубликовано',
problemRestoringVersion: 'Возникла проблема с восстановлением этой версии',

View File

@@ -187,7 +187,7 @@ export const skTranslations: DefaultTranslationsObject = {
backToDashboard: 'Späť na nástenku',
cancel: 'Zrušiť',
changesNotSaved: 'Vaše zmeny neboli uložené. Ak teraz odídete, stratíte svoje zmeny.',
clearAll: undefined,
clearAll: 'Vymazať všetko',
close: 'Zavrieť',
collapse: 'Zbaliť',
collections: 'Kolekcia',
@@ -267,7 +267,7 @@ export const skTranslations: DefaultTranslationsObject = {
nothingFound: 'Nič nenájdené',
noValue: 'Žiadna hodnota',
of: 'z',
only: undefined,
only: 'Iba',
open: 'Otvoriť',
or: 'Alebo',
order: 'Poradie',
@@ -414,7 +414,7 @@ export const skTranslations: DefaultTranslationsObject = {
lastSavedAgo: 'Naposledy uložené pred {{distance}}',
noFurtherVersionsFound: 'Nenájdené ďalšie verzie',
noRowsFound: 'Nenájdené {{label}}',
noRowsSelected: undefined,
noRowsSelected: 'Nie je vybraté žiadne {{označenie}}',
preview: 'Náhľad',
previouslyPublished: 'Predtým publikované',
problemRestoringVersion: 'Pri obnovovaní tejto verzie došlo k problému',

View File

@@ -186,7 +186,7 @@ export const svTranslations: DefaultTranslationsObject = {
cancel: 'Avbryt',
changesNotSaved:
'Dina ändringar har inte sparats. Om du lämnar nu kommer du att förlora dina ändringar.',
clearAll: undefined,
clearAll: 'Rensa alla',
close: 'Stänga',
collapse: 'Kollapsa',
collections: 'Samlingar',
@@ -413,7 +413,7 @@ export const svTranslations: DefaultTranslationsObject = {
lastSavedAgo: 'Senast sparad för {{distance}} sedan',
noFurtherVersionsFound: 'Inga fler versioner hittades',
noRowsFound: 'Inga {{label}} hittades',
noRowsSelected: undefined,
noRowsSelected: 'Inget {{etikett}} valt',
preview: 'Förhandsvisa',
previouslyPublished: 'Tidigare publicerad',
problemRestoringVersion: 'Det uppstod ett problem när den här versionen skulle återställas',

View File

@@ -182,7 +182,7 @@ export const thTranslations: DefaultTranslationsObject = {
backToDashboard: 'กลับไปหน้าแดชบอร์ด',
cancel: 'ยกเลิก',
changesNotSaved: 'การเปลี่ยนแปลงยังไม่ได้ถูกบันทึก ถ้าคุณออกตอนนี้ สิ่งที่แก้ไขไว้จะหายไป',
clearAll: undefined,
clearAll: 'ล้างทั้งหมด',
close: 'ปิด',
collapse: 'ยุบ',
collections: 'Collections',
@@ -405,7 +405,7 @@ export const thTranslations: DefaultTranslationsObject = {
lastSavedAgo: 'บันทึกครั้งล่าสุด {{distance}} ที่ผ่านมา',
noFurtherVersionsFound: 'ไม่พบเวอร์ชันอื่น ๆ',
noRowsFound: 'ไม่พบ {{label}}',
noRowsSelected: undefined,
noRowsSelected: 'ไม่มี {{label}} ที่ถูกเลือก',
preview: 'ตัวอย่าง',
previouslyPublished: 'เผยแพร่ก่อนหน้านี้',
problemRestoringVersion: 'เกิดปัญหาระหว่างการกู้คืนเวอร์ชันนี้',

View File

@@ -189,7 +189,7 @@ export const trTranslations: DefaultTranslationsObject = {
cancel: 'İptal',
changesNotSaved:
'Değişiklikleriniz henüz kaydedilmedi. Eğer bu sayfayı terk ederseniz değişiklikleri kaybedeceksiniz.',
clearAll: undefined,
clearAll: 'Hepsini Temizle',
close: 'Kapat',
collapse: 'Daralt',
collections: 'Koleksiyonlar',
@@ -415,7 +415,7 @@ export const trTranslations: DefaultTranslationsObject = {
lastSavedAgo: 'Son kaydedildi {{distance}} önce',
noFurtherVersionsFound: 'Başka sürüm bulunamadı.',
noRowsFound: '{{label}} bulunamadı',
noRowsSelected: undefined,
noRowsSelected: 'Seçilen {{label}} yok',
preview: 'Önizleme',
previouslyPublished: 'Daha Önce Yayınlanmış',
problemRestoringVersion: 'Bu sürüme geri döndürürken bir hatayla karşılaşıldı.',

View File

@@ -186,7 +186,7 @@ export const ukTranslations: DefaultTranslationsObject = {
backToDashboard: 'Повернутись до головної сторінки',
cancel: 'Скасувати',
changesNotSaved: 'Ваши зміни не були збережені. Якщо ви вийдете зараз, то втратите свої зміни.',
clearAll: undefined,
clearAll: 'Очистити все',
close: 'Закрити',
collapse: 'Згорнути',
collections: 'Колекції',
@@ -413,7 +413,7 @@ export const ukTranslations: DefaultTranslationsObject = {
lastSavedAgo: 'Востаннє збережено {{distance}} тому',
noFurtherVersionsFound: 'Інших версій не знайдено',
noRowsFound: 'Не знайдено {{label}}',
noRowsSelected: undefined,
noRowsSelected: 'Не вибрано {{label}}',
preview: 'Попередній перегляд',
previouslyPublished: 'Раніше опубліковано',
problemRestoringVersion: 'Виникла проблема з відновленням цієї версії',

View File

@@ -184,7 +184,7 @@ export const viTranslations: DefaultTranslationsObject = {
backToDashboard: 'Quay lại bảng điều khiển',
cancel: 'Hủy',
changesNotSaved: 'Thay đổi chưa được lưu lại. Bạn sẽ mất bản chỉnh sửa nếu thoát bây giờ.',
clearAll: undefined,
clearAll: 'Xóa tất cả',
close: 'Gần',
collapse: 'Thu gọn',
collections: 'Collections',
@@ -264,7 +264,7 @@ export const viTranslations: DefaultTranslationsObject = {
nothingFound: 'Không tìm thấy',
noValue: 'Không có giá trị',
of: 'trong số',
only: undefined,
only: 'Chỉ',
open: 'Mở',
or: 'hoặc',
order: 'Thứ tự',
@@ -408,7 +408,7 @@ export const viTranslations: DefaultTranslationsObject = {
lastSavedAgo: 'Lần lưu cuối cùng {{distance}} trước đây',
noFurtherVersionsFound: 'Không tìm thấy phiên bản cũ hơn',
noRowsFound: 'Không tìm thấy: {{label}}',
noRowsSelected: undefined,
noRowsSelected: 'Không có {{label}} được chọn',
preview: 'Bản xem trước',
previouslyPublished: 'Đã xuất bản trước đây',
problemRestoringVersion: 'Đã xảy ra vấn đề khi khôi phục phiên bản này',

View File

@@ -179,7 +179,7 @@ export const zhTranslations: DefaultTranslationsObject = {
backToDashboard: '返回到仪表板',
cancel: '取消',
changesNotSaved: '您的更改尚未保存。您确定要离开吗?',
clearAll: undefined,
clearAll: '清除全部',
close: '关闭',
collapse: '折叠',
collections: '集合',
@@ -258,7 +258,7 @@ export const zhTranslations: DefaultTranslationsObject = {
nothingFound: '没有找到任何东西',
noValue: '没有值',
of: '的',
only: undefined,
only: '仅',
open: '打开',
or: '或',
order: '排序',
@@ -398,14 +398,14 @@ export const zhTranslations: DefaultTranslationsObject = {
lastSavedAgo: '上次保存{{distance}}之前',
noFurtherVersionsFound: '没有发现其他版本',
noRowsFound: '没有发现{{label}}',
noRowsSelected: undefined,
noRowsSelected: '未选择{{label}}',
preview: '预览',
previouslyPublished: '先前发布过的',
problemRestoringVersion: '恢复这个版本时发生了问题',
publish: '发布',
publishChanges: '发布修改',
published: '已发布',
publishIn: undefined,
publishIn: '在{{locale}}发布',
publishing: '发布',
restoreAsDraft: '恢复为草稿',
restoredSuccessfully: '恢复成功。',

View File

@@ -179,7 +179,7 @@ export const zhTwTranslations: DefaultTranslationsObject = {
backToDashboard: '返回到控制面板',
cancel: '取消',
changesNotSaved: '您還有尚未儲存的變更。您確定要離開嗎?',
clearAll: undefined,
clearAll: '清除全部',
close: '關閉',
collapse: '折疊',
collections: '集合',
@@ -398,14 +398,14 @@ export const zhTwTranslations: DefaultTranslationsObject = {
lastSavedAgo: '上次儲存在{{distance}}之前',
noFurtherVersionsFound: '沒有發現其他版本',
noRowsFound: '沒有發現{{label}}',
noRowsSelected: undefined,
noRowsSelected: '未選擇 {{label}}',
preview: '預覽',
previouslyPublished: '先前出版過的',
problemRestoringVersion: '回復這個版本時發生了問題',
publish: '發佈',
publishChanges: '發佈修改',
published: '已發佈',
publishIn: undefined,
publishIn: '在 {{locale}} 發佈',
publishing: '發布',
restoreAsDraft: '恢復為草稿',
restoredSuccessfully: '回復成功。',

View File

@@ -1,5 +1,5 @@
'use client'
import type { ClientUser, MeOperationResult, Permissions } from 'payload'
import type { ClientUser, MeOperationResult, Permissions, User } from 'payload'
import { useModal } from '@faceless-ui/modal'
import { usePathname, useRouter } from 'next/navigation.js'
@@ -15,7 +15,7 @@ import { formatAdminURL } from '../../utilities/formatAdminURL.js'
import { useConfig } from '../Config/index.js'
export type AuthContext<T = ClientUser> = {
fetchFullUser: () => Promise<void>
fetchFullUser: () => Promise<null | User>
logOut: () => Promise<void>
permissions?: Permissions
refreshCookie: (forceRefresh?: boolean) => void
@@ -227,6 +227,7 @@ export function AuthProvider({
const fetchFullUser = React.useCallback(async () => {
try {
const request = await requests.get(`${serverURL}${apiRoute}/${userSlug}/me`, {
credentials: 'include',
headers: {
'Accept-Language': i18n.language,
},
@@ -234,9 +235,11 @@ export function AuthProvider({
if (request.status === 200) {
const json: MeOperationResult = await request.json()
let user = null
if (json?.user) {
setUser(json.user)
user = json.user
if (json?.token) {
setTokenAndExpiration(json)
@@ -245,10 +248,14 @@ export function AuthProvider({
setUser(null)
revokeTokenAndExpire()
}
return user
}
} catch (e) {
toast.error(`Fetching user failed: ${e.message}`)
}
return null
}, [serverURL, apiRoute, userSlug, i18n.language, setTokenAndExpiration, revokeTokenAndExpire])
// On mount, get user and set

View File

@@ -482,7 +482,7 @@ describe('access control', () => {
serverURL,
})
await expect(page.locator('.next-error-h1')).toBeVisible()
await expect(page.locator('.unauthorized')).toBeVisible()
await page.goto(logoutURL)
await page.waitForURL(logoutURL)
@@ -500,6 +500,7 @@ describe('access control', () => {
test('should block admin access to non-admin user', async () => {
const adminURL = `${serverURL}/admin`
const unauthorizedURL = `${serverURL}/admin/unauthorized`
await page.goto(adminURL)
await page.waitForURL(adminURL)
@@ -527,9 +528,9 @@ describe('access control', () => {
])
await page.goto(adminURL)
await page.waitForURL(adminURL)
await page.waitForURL(unauthorizedURL)
await expect(page.locator('.next-error-h1')).toBeVisible()
await expect(page.locator('.unauthorized')).toBeVisible()
})
})

View File

@@ -310,6 +310,7 @@ export function initPageConsoleErrorCatch(page: Page) {
!msg.text().includes('the server responded with a status of') &&
!msg.text().includes('Failed to fetch RSC payload for') &&
!msg.text().includes('Error: NEXT_NOT_FOUND') &&
!msg.text().includes('Error: NEXT_REDIRECT') &&
!msg.text().includes('Error getting document data')
) {
// "Failed to fetch RSC payload for" happens seemingly randomly. There are lots of issues in the next.js repository for this. Causes e2e tests to fail and flake. Will ignore for now