feat!: consolidates admin.logoutRoute and admin.inactivityRoute into admin.routes (#6354)
This commit is contained in:
@@ -45,8 +45,7 @@ All options for the Admin panel are defined in your base Payload config file.
|
|||||||
| `components` | Component overrides that affect the entirety of the Admin panel. [More](/docs/admin/components) |
|
| `components` | Component overrides that affect the entirety of the Admin panel. [More](/docs/admin/components) |
|
||||||
| `webpack` | Customize the Webpack config that's used to generate the Admin panel. [More](/docs/admin/webpack) |
|
| `webpack` | Customize the Webpack config that's used to generate the Admin panel. [More](/docs/admin/webpack) |
|
||||||
| `vite` | Customize the Vite config that's used to generate the Admin panel. [More](/docs/admin/vite) |
|
| `vite` | Customize the Vite config that's used to generate the Admin panel. [More](/docs/admin/vite) |
|
||||||
| `logoutRoute` | The route for the `logout` page. |
|
| `routes` | Replace built-in Admin Panel routes with your own custom routes. I.e. `{ logout: '/custom-logout', inactivity: 'custom-inactivity' }` |
|
||||||
| `inactivityRoute` | The route for the `logout` inactivity page. |
|
|
||||||
|
|
||||||
### The Admin User Collection
|
### The Admin User Collection
|
||||||
|
|
||||||
|
|||||||
@@ -46,7 +46,7 @@ export const handleAdminPage = ({
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!permissions.canAccessAdmin && !isAdminAuthRoute(route, adminRoute)) {
|
if (!permissions.canAccessAdmin && !isAdminAuthRoute(config, route, adminRoute)) {
|
||||||
notFound()
|
notFound()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -4,17 +4,24 @@ import QueryString from 'qs'
|
|||||||
import { isAdminAuthRoute, isAdminRoute } from './shared.js'
|
import { isAdminAuthRoute, isAdminRoute } from './shared.js'
|
||||||
|
|
||||||
export const handleAuthRedirect = ({
|
export const handleAuthRedirect = ({
|
||||||
adminRoute,
|
config,
|
||||||
redirectUnauthenticatedUser,
|
redirectUnauthenticatedUser,
|
||||||
route,
|
route,
|
||||||
searchParams,
|
searchParams,
|
||||||
}: {
|
}: {
|
||||||
adminRoute: string
|
config
|
||||||
redirectUnauthenticatedUser: boolean | string
|
redirectUnauthenticatedUser: boolean | string
|
||||||
route: string
|
route: string
|
||||||
searchParams: { [key: string]: string | string[] }
|
searchParams: { [key: string]: string | string[] }
|
||||||
}) => {
|
}) => {
|
||||||
if (!isAdminAuthRoute(route, adminRoute)) {
|
const {
|
||||||
|
admin: {
|
||||||
|
routes: { login: loginRouteFromConfig },
|
||||||
|
},
|
||||||
|
routes: { admin: adminRoute },
|
||||||
|
} = config
|
||||||
|
|
||||||
|
if (!isAdminAuthRoute(config, route, adminRoute)) {
|
||||||
if (searchParams && 'redirect' in searchParams) delete searchParams.redirect
|
if (searchParams && 'redirect' in searchParams) delete searchParams.redirect
|
||||||
|
|
||||||
const redirectRoute = encodeURIComponent(
|
const redirectRoute = encodeURIComponent(
|
||||||
@@ -23,14 +30,14 @@ export const handleAuthRedirect = ({
|
|||||||
: undefined,
|
: undefined,
|
||||||
)
|
)
|
||||||
|
|
||||||
const adminLoginRoute = `${adminRoute}/login`
|
const adminLoginRoute = `${adminRoute}${loginRouteFromConfig}`
|
||||||
|
|
||||||
const customLoginRoute =
|
const customLoginRoute =
|
||||||
typeof redirectUnauthenticatedUser === 'string' ? redirectUnauthenticatedUser : undefined
|
typeof redirectUnauthenticatedUser === 'string' ? redirectUnauthenticatedUser : undefined
|
||||||
|
|
||||||
const loginRoute = isAdminRoute(route, adminRoute)
|
const loginRoute = isAdminRoute(route, adminRoute)
|
||||||
? adminLoginRoute
|
? adminLoginRoute
|
||||||
: customLoginRoute || '/login'
|
: customLoginRoute || loginRouteFromConfig
|
||||||
|
|
||||||
const parsedLoginRouteSearchParams = QueryString.parse(loginRoute.split('?')[1] ?? '')
|
const parsedLoginRouteSearchParams = QueryString.parse(loginRoute.split('?')[1] ?? '')
|
||||||
|
|
||||||
|
|||||||
@@ -78,7 +78,7 @@ export const initPage = async ({
|
|||||||
|
|
||||||
if (redirectUnauthenticatedUser && !user) {
|
if (redirectUnauthenticatedUser && !user) {
|
||||||
handleAuthRedirect({
|
handleAuthRedirect({
|
||||||
adminRoute,
|
config: payload.config,
|
||||||
redirectUnauthenticatedUser,
|
redirectUnauthenticatedUser,
|
||||||
route,
|
route,
|
||||||
searchParams,
|
searchParams,
|
||||||
|
|||||||
@@ -1,17 +1,26 @@
|
|||||||
export const authRoutes = [
|
import type { SanitizedConfig } from 'payload/types'
|
||||||
'/login',
|
|
||||||
'/logout',
|
const authRouteKeys: (keyof SanitizedConfig['admin']['routes'])[] = [
|
||||||
'/create-first-user',
|
'account',
|
||||||
'/forgot',
|
'createFirstUser',
|
||||||
'/reset',
|
'forgot',
|
||||||
'/verify',
|
'login',
|
||||||
'/logout-inactivity',
|
'logout',
|
||||||
|
'forgot',
|
||||||
|
'inactivity',
|
||||||
|
'unauthorized',
|
||||||
]
|
]
|
||||||
|
|
||||||
export const isAdminRoute = (route: string, adminRoute: string) => {
|
export const isAdminRoute = (route: string, adminRoute: string) => {
|
||||||
return route.startsWith(adminRoute)
|
return route.startsWith(adminRoute)
|
||||||
}
|
}
|
||||||
|
|
||||||
export const isAdminAuthRoute = (route: string, adminRoute: string) => {
|
export const isAdminAuthRoute = (config: SanitizedConfig, route: string, adminRoute: string) => {
|
||||||
|
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) => route.replace(adminRoute, '').startsWith(r))
|
return authRoutes.some((r) => route.replace(adminRoute, '').startsWith(r))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -22,6 +22,9 @@ export const ForgotPasswordView: React.FC<AdminViewProps> = ({ initPageResult })
|
|||||||
} = initPageResult
|
} = initPageResult
|
||||||
|
|
||||||
const {
|
const {
|
||||||
|
admin: {
|
||||||
|
routes: { account: accountRoute },
|
||||||
|
},
|
||||||
routes: { admin },
|
routes: { admin },
|
||||||
} = config
|
} = config
|
||||||
|
|
||||||
@@ -32,7 +35,7 @@ export const ForgotPasswordView: React.FC<AdminViewProps> = ({ initPageResult })
|
|||||||
<p>
|
<p>
|
||||||
<Translation
|
<Translation
|
||||||
elements={{
|
elements={{
|
||||||
'0': ({ children }) => <Link href={`${admin}/account`}>{children}</Link>,
|
'0': ({ children }) => <Link href={`${admin}${accountRoute}`}>{children}</Link>,
|
||||||
}}
|
}}
|
||||||
i18nKey="authentication:loggedInChangePassword"
|
i18nKey="authentication:loggedInChangePassword"
|
||||||
t={i18n.t}
|
t={i18n.t}
|
||||||
|
|||||||
@@ -25,7 +25,11 @@ export const LoginForm: React.FC<{
|
|||||||
const config = useConfig()
|
const config = useConfig()
|
||||||
|
|
||||||
const {
|
const {
|
||||||
admin: { autoLogin, user: userSlug },
|
admin: {
|
||||||
|
autoLogin,
|
||||||
|
routes: { forgot: forgotRoute },
|
||||||
|
user: userSlug,
|
||||||
|
},
|
||||||
routes: { admin, api },
|
routes: { admin, api },
|
||||||
} = config
|
} = config
|
||||||
|
|
||||||
@@ -98,7 +102,7 @@ export const LoginForm: React.FC<{
|
|||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<Link href={`${admin}/forgot`}>{t('authentication:forgotPasswordQuestion')}</Link>
|
<Link href={`${admin}${forgotRoute}`}>{t('authentication:forgotPasswordQuestion')}</Link>
|
||||||
<FormSubmit>{t('authentication:login')}</FormSubmit>
|
<FormSubmit>{t('authentication:login')}</FormSubmit>
|
||||||
</Form>
|
</Form>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -29,6 +29,9 @@ export const ResetPassword: React.FC<AdminViewProps> = ({ initPageResult, params
|
|||||||
} = req
|
} = req
|
||||||
|
|
||||||
const {
|
const {
|
||||||
|
admin: {
|
||||||
|
routes: { account: accountRoute },
|
||||||
|
},
|
||||||
routes: { admin },
|
routes: { admin },
|
||||||
} = config
|
} = config
|
||||||
|
|
||||||
@@ -40,7 +43,7 @@ export const ResetPassword: React.FC<AdminViewProps> = ({ initPageResult, params
|
|||||||
<p>
|
<p>
|
||||||
<Translation
|
<Translation
|
||||||
elements={{
|
elements={{
|
||||||
'0': ({ children }) => <Link href={`${admin}/account`}>{children}</Link>,
|
'0': ({ children }) => <Link href={`${admin}${accountRoute}`}>{children}</Link>,
|
||||||
}}
|
}}
|
||||||
i18nKey="authentication:loggedInChangePassword"
|
i18nKey="authentication:loggedInChangePassword"
|
||||||
t={i18n.t}
|
t={i18n.t}
|
||||||
|
|||||||
@@ -15,20 +15,27 @@ import { ResetPassword, resetPasswordBaseClass } from '../ResetPassword/index.js
|
|||||||
import { UnauthorizedView } from '../Unauthorized/index.js'
|
import { UnauthorizedView } from '../Unauthorized/index.js'
|
||||||
import { Verify, verifyBaseClass } from '../Verify/index.js'
|
import { Verify, verifyBaseClass } from '../Verify/index.js'
|
||||||
import { getCustomViewByRoute } from './getCustomViewByRoute.js'
|
import { getCustomViewByRoute } from './getCustomViewByRoute.js'
|
||||||
|
import { isPathMatchingRoute } from './isPathMatchingRoute.js'
|
||||||
|
|
||||||
const baseClasses = {
|
const baseClasses = {
|
||||||
|
account: 'account',
|
||||||
forgot: forgotPasswordBaseClass,
|
forgot: forgotPasswordBaseClass,
|
||||||
login: loginBaseClass,
|
login: loginBaseClass,
|
||||||
reset: resetPasswordBaseClass,
|
reset: resetPasswordBaseClass,
|
||||||
verify: verifyBaseClass,
|
verify: verifyBaseClass,
|
||||||
}
|
}
|
||||||
|
|
||||||
const oneSegmentViews = {
|
type OneSegmentViews = {
|
||||||
'create-first-user': CreateFirstUserView,
|
[K in keyof SanitizedConfig['admin']['routes']]: AdminViewComponent
|
||||||
|
}
|
||||||
|
|
||||||
|
const oneSegmentViews: OneSegmentViews = {
|
||||||
|
account: Account,
|
||||||
|
createFirstUser: CreateFirstUserView,
|
||||||
forgot: ForgotPasswordView,
|
forgot: ForgotPasswordView,
|
||||||
|
inactivity: LogoutInactivity,
|
||||||
login: LoginView,
|
login: LoginView,
|
||||||
logout: LogoutView,
|
logout: LogoutView,
|
||||||
'logout-inactivity': LogoutInactivity,
|
|
||||||
unauthorized: UnauthorizedView,
|
unauthorized: UnauthorizedView,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -78,22 +85,41 @@ export const getViewFromConfig = ({
|
|||||||
break
|
break
|
||||||
}
|
}
|
||||||
case 1: {
|
case 1: {
|
||||||
if (oneSegmentViews[segmentOne] && segmentOne !== 'account') {
|
// users can override the default routes via `admin.routes` config
|
||||||
|
// i.e.{ admin: { routes: { logout: '/sign-out', inactivity: '/idle' }}}
|
||||||
|
let viewToRender: keyof typeof oneSegmentViews
|
||||||
|
|
||||||
|
if (config.admin.routes) {
|
||||||
|
const matchedRoute = Object.entries(config.admin.routes).find(([, route]) => {
|
||||||
|
return isPathMatchingRoute({
|
||||||
|
currentRoute,
|
||||||
|
exact: true,
|
||||||
|
path: `${adminRoute}${route}`,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
if (matchedRoute) {
|
||||||
|
viewToRender = matchedRoute[0] as keyof typeof oneSegmentViews
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (oneSegmentViews[viewToRender]) {
|
||||||
|
// --> /account
|
||||||
// --> /create-first-user
|
// --> /create-first-user
|
||||||
// --> /forgot
|
// --> /forgot
|
||||||
// --> /login
|
// --> /login
|
||||||
// --> /logout
|
// --> /logout
|
||||||
// --> /logout-inactivity
|
// --> /logout-inactivity
|
||||||
// --> /unauthorized
|
// --> /unauthorized
|
||||||
ViewToRender = oneSegmentViews[segmentOne]
|
|
||||||
templateClassName = baseClasses[segmentOne]
|
ViewToRender = oneSegmentViews[viewToRender]
|
||||||
|
templateClassName = baseClasses[viewToRender]
|
||||||
templateType = 'minimal'
|
templateType = 'minimal'
|
||||||
} else if (segmentOne === 'account') {
|
|
||||||
// --> /account
|
if (viewToRender === 'account') {
|
||||||
initPageOptions.redirectUnauthenticatedUser = true
|
initPageOptions.redirectUnauthenticatedUser = true
|
||||||
ViewToRender = Account
|
templateType = 'default'
|
||||||
templateClassName = 'account'
|
}
|
||||||
templateType = 'default'
|
|
||||||
}
|
}
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -34,7 +34,10 @@ export const RootPage = async ({
|
|||||||
const config = await configPromise
|
const config = await configPromise
|
||||||
|
|
||||||
const {
|
const {
|
||||||
admin: { user: userSlug },
|
admin: {
|
||||||
|
routes: { createFirstUser: createFirstUserRoute },
|
||||||
|
user: userSlug,
|
||||||
|
},
|
||||||
routes: { admin: adminRoute },
|
routes: { admin: adminRoute },
|
||||||
} = config
|
} = config
|
||||||
|
|
||||||
@@ -66,13 +69,13 @@ export const RootPage = async ({
|
|||||||
})
|
})
|
||||||
?.then((doc) => !!doc)
|
?.then((doc) => !!doc)
|
||||||
|
|
||||||
const createFirstUserRoute = `${adminRoute}/create-first-user`
|
const routeWithAdmin = `${adminRoute}${createFirstUserRoute}`
|
||||||
|
|
||||||
if (!dbHasUser && currentRoute !== createFirstUserRoute) {
|
if (!dbHasUser && currentRoute !== routeWithAdmin) {
|
||||||
redirect(createFirstUserRoute)
|
redirect(routeWithAdmin)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (dbHasUser && currentRoute === createFirstUserRoute) {
|
if (dbHasUser && currentRoute === routeWithAdmin) {
|
||||||
redirect(adminRoute)
|
redirect(adminRoute)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,7 +19,9 @@ export const UnauthorizedView: AdminViewComponent = ({ initPageResult }) => {
|
|||||||
i18n,
|
i18n,
|
||||||
payload: {
|
payload: {
|
||||||
config: {
|
config: {
|
||||||
admin: { logoutRoute },
|
admin: {
|
||||||
|
routes: { logout: logoutRoute },
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -7,11 +7,18 @@ export const defaults: Omit<Config, 'db' | 'editor' | 'secret'> = {
|
|||||||
custom: {},
|
custom: {},
|
||||||
dateFormat: 'MMMM do yyyy, h:mm a',
|
dateFormat: 'MMMM do yyyy, h:mm a',
|
||||||
disable: false,
|
disable: false,
|
||||||
inactivityRoute: '/logout-inactivity',
|
|
||||||
logoutRoute: '/logout',
|
|
||||||
meta: {
|
meta: {
|
||||||
titleSuffix: '- Payload',
|
titleSuffix: '- Payload',
|
||||||
},
|
},
|
||||||
|
routes: {
|
||||||
|
account: '/account',
|
||||||
|
createFirstUser: '/create-first-user',
|
||||||
|
forgot: '/forgot',
|
||||||
|
inactivity: '/logout-inactivity',
|
||||||
|
login: '/login',
|
||||||
|
logout: '/logout',
|
||||||
|
unauthorized: '/unauthorized',
|
||||||
|
},
|
||||||
},
|
},
|
||||||
bin: [],
|
bin: [],
|
||||||
collections: [],
|
collections: [],
|
||||||
|
|||||||
@@ -60,13 +60,11 @@ export default joi.object({
|
|||||||
custom: joi.object().pattern(joi.string(), joi.any()),
|
custom: joi.object().pattern(joi.string(), joi.any()),
|
||||||
dateFormat: joi.string(),
|
dateFormat: joi.string(),
|
||||||
disable: joi.bool(),
|
disable: joi.bool(),
|
||||||
inactivityRoute: joi.string(),
|
|
||||||
livePreview: joi.object({
|
livePreview: joi.object({
|
||||||
...livePreviewSchema,
|
...livePreviewSchema,
|
||||||
collections: joi.array().items(joi.string()),
|
collections: joi.array().items(joi.string()),
|
||||||
globals: joi.array().items(joi.string()),
|
globals: joi.array().items(joi.string()),
|
||||||
}),
|
}),
|
||||||
logoutRoute: joi.string(),
|
|
||||||
meta: joi.object().keys({
|
meta: joi.object().keys({
|
||||||
icons: joi
|
icons: joi
|
||||||
.alternatives()
|
.alternatives()
|
||||||
@@ -78,6 +76,15 @@ export default joi.object({
|
|||||||
ogImage: joi.string(),
|
ogImage: joi.string(),
|
||||||
titleSuffix: joi.string(),
|
titleSuffix: joi.string(),
|
||||||
}),
|
}),
|
||||||
|
routes: joi.object({
|
||||||
|
account: joi.string(),
|
||||||
|
createFirstUser: joi.string(),
|
||||||
|
forgot: joi.string(),
|
||||||
|
inactivity: joi.string(),
|
||||||
|
login: joi.string(),
|
||||||
|
logout: joi.string(),
|
||||||
|
unauthorized: joi.string(),
|
||||||
|
}),
|
||||||
user: joi.string(),
|
user: joi.string(),
|
||||||
}),
|
}),
|
||||||
bin: joi.array().items(
|
bin: joi.array().items(
|
||||||
|
|||||||
@@ -453,14 +453,10 @@ export type Config = {
|
|||||||
dateFormat?: string
|
dateFormat?: string
|
||||||
/** If set to true, the entire Admin panel will be disabled. */
|
/** If set to true, the entire Admin panel will be disabled. */
|
||||||
disable?: boolean
|
disable?: boolean
|
||||||
/** The route the user will be redirected to after being inactive for too long. */
|
|
||||||
inactivityRoute?: string
|
|
||||||
livePreview?: LivePreviewConfig & {
|
livePreview?: LivePreviewConfig & {
|
||||||
collections?: string[]
|
collections?: string[]
|
||||||
globals?: string[]
|
globals?: string[]
|
||||||
}
|
}
|
||||||
/** The route for the logout page. */
|
|
||||||
logoutRoute?: string
|
|
||||||
/** Base meta data to use for the Admin Panel. Included properties are titleSuffix, ogImage, and favicon. */
|
/** Base meta data to use for the Admin Panel. Included properties are titleSuffix, ogImage, and favicon. */
|
||||||
meta?: {
|
meta?: {
|
||||||
/**
|
/**
|
||||||
@@ -482,6 +478,22 @@ export type Config = {
|
|||||||
*/
|
*/
|
||||||
titleSuffix?: string
|
titleSuffix?: string
|
||||||
}
|
}
|
||||||
|
routes?: {
|
||||||
|
/** The route for the account page. */
|
||||||
|
account?: string
|
||||||
|
/** The route for the create first user page. */
|
||||||
|
createFirstUser?: string
|
||||||
|
/** The route for the forgot password page. */
|
||||||
|
forgot?: string
|
||||||
|
/** The route the user will be redirected to after being inactive for too long. */
|
||||||
|
inactivity?: string
|
||||||
|
/** The route for the login page. */
|
||||||
|
login?: string
|
||||||
|
/** The route for the logout page. */
|
||||||
|
logout?: string
|
||||||
|
/** The route for the unauthorized page. */
|
||||||
|
unauthorized?: string
|
||||||
|
}
|
||||||
/** The slug of a Collection that you want be used to log in to the Admin dashboard. */
|
/** The slug of a Collection that you want be used to log in to the Admin dashboard. */
|
||||||
user?: string
|
user?: string
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,6 +20,9 @@ export const AppHeader: React.FC = () => {
|
|||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
|
|
||||||
const {
|
const {
|
||||||
|
admin: {
|
||||||
|
routes: { account: accountRoute },
|
||||||
|
},
|
||||||
localization,
|
localization,
|
||||||
routes: { admin: adminRoute },
|
routes: { admin: adminRoute },
|
||||||
} = useConfig()
|
} = useConfig()
|
||||||
@@ -86,7 +89,7 @@ export const AppHeader: React.FC = () => {
|
|||||||
<LinkElement
|
<LinkElement
|
||||||
aria-label={t('authentication:account')}
|
aria-label={t('authentication:account')}
|
||||||
className={`${baseClass}__account`}
|
className={`${baseClass}__account`}
|
||||||
href={`${adminRoute}/account`}
|
href={`${adminRoute}${accountRoute}`}
|
||||||
tabIndex={0}
|
tabIndex={0}
|
||||||
>
|
>
|
||||||
<Account />
|
<Account />
|
||||||
|
|||||||
@@ -16,7 +16,9 @@ const DefaultLogout: React.FC<{
|
|||||||
const config = useConfig()
|
const config = useConfig()
|
||||||
|
|
||||||
const {
|
const {
|
||||||
admin: { logoutRoute },
|
admin: {
|
||||||
|
routes: { logout: logoutRoute },
|
||||||
|
},
|
||||||
routes: { admin },
|
routes: { admin },
|
||||||
} = config
|
} = config
|
||||||
|
|
||||||
|
|||||||
@@ -22,7 +22,9 @@ export const StayLoggedInModal: React.FC = () => {
|
|||||||
const config = useConfig()
|
const config = useConfig()
|
||||||
|
|
||||||
const {
|
const {
|
||||||
admin: { logoutRoute },
|
admin: {
|
||||||
|
routes: { logout: logoutRoute },
|
||||||
|
},
|
||||||
routes: { admin },
|
routes: { admin },
|
||||||
} = config
|
} = config
|
||||||
const { toggleModal } = useModal()
|
const { toggleModal } = useModal()
|
||||||
|
|||||||
@@ -5,22 +5,21 @@ import React from 'react'
|
|||||||
import { useAuth } from '../../providers/Auth/index.js'
|
import { useAuth } from '../../providers/Auth/index.js'
|
||||||
import { useConfig } from '../../providers/Config/index.js'
|
import { useConfig } from '../../providers/Config/index.js'
|
||||||
import { DefaultAccountIcon } from './Default/index.js'
|
import { DefaultAccountIcon } from './Default/index.js'
|
||||||
export { DefaultAccountIcon } from './Default/index.js'
|
|
||||||
import { GravatarAccountIcon } from './Gravatar/index.js'
|
|
||||||
export { GravatarAccountIcon } from './Gravatar/index.js'
|
export { GravatarAccountIcon } from './Gravatar/index.js'
|
||||||
|
|
||||||
export const Account = () => {
|
export const Account = () => {
|
||||||
const {
|
const {
|
||||||
admin: { avatar: Avatar },
|
admin: { avatar: Avatar },
|
||||||
|
admin: {
|
||||||
|
routes: { account: accountRoute },
|
||||||
|
},
|
||||||
routes: { admin: adminRoute },
|
routes: { admin: adminRoute },
|
||||||
} = useConfig()
|
} = useConfig()
|
||||||
|
|
||||||
const { user } = useAuth()
|
const { user } = useAuth()
|
||||||
const pathname = usePathname()
|
const pathname = usePathname()
|
||||||
|
|
||||||
const isOnAccountPage = pathname === `${adminRoute}/account`
|
const isOnAccountPage = pathname === `${adminRoute}${accountRoute}`
|
||||||
|
|
||||||
if (!user?.email || Avatar === 'default') return <DefaultAccountIcon active={isOnAccountPage} />
|
if (!user?.email || Avatar === 'default') return <DefaultAccountIcon active={isOnAccountPage} />
|
||||||
if (Avatar === 'gravatar') return <GravatarAccountIcon />
|
|
||||||
if (Avatar) return <Avatar active={isOnAccountPage} />
|
|
||||||
return <DefaultAccountIcon active={isOnAccountPage} />
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -47,7 +47,12 @@ export const AuthProvider: React.FC<{ children: React.ReactNode }> = ({ children
|
|||||||
const config = useConfig()
|
const config = useConfig()
|
||||||
|
|
||||||
const {
|
const {
|
||||||
admin: { autoLogin, inactivityRoute: logoutInactivityRoute, user: userSlug },
|
admin: {
|
||||||
|
autoLogin,
|
||||||
|
routes: { inactivity: logoutInactivityRoute },
|
||||||
|
routes: { login: loginRoute },
|
||||||
|
user: userSlug,
|
||||||
|
},
|
||||||
routes: { admin, api },
|
routes: { admin, api },
|
||||||
serverURL,
|
serverURL,
|
||||||
} = config
|
} = config
|
||||||
@@ -211,16 +216,19 @@ export const AuthProvider: React.FC<{ children: React.ReactNode }> = ({ children
|
|||||||
} else if (autoLogin && autoLogin.prefillOnly !== true) {
|
} else if (autoLogin && autoLogin.prefillOnly !== true) {
|
||||||
// auto log-in with the provided autoLogin credentials. This is used in dev mode
|
// auto log-in with the provided autoLogin credentials. This is used in dev mode
|
||||||
// so you don't have to log in over and over again
|
// so you don't have to log in over and over again
|
||||||
const autoLoginResult = await requests.post(`${serverURL}${api}/${userSlug}/login`, {
|
const autoLoginResult = await requests.post(
|
||||||
body: JSON.stringify({
|
`${serverURL}${api}/${userSlug}${loginRoute}`,
|
||||||
email: autoLogin.email,
|
{
|
||||||
password: autoLogin.password,
|
body: JSON.stringify({
|
||||||
}),
|
email: autoLogin.email,
|
||||||
headers: {
|
password: autoLogin.password,
|
||||||
'Accept-Language': i18n.language,
|
}),
|
||||||
'Content-Type': 'application/json',
|
headers: {
|
||||||
|
'Accept-Language': i18n.language,
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
},
|
},
|
||||||
})
|
)
|
||||||
if (autoLoginResult.status === 200) {
|
if (autoLoginResult.status === 200) {
|
||||||
const autoLoginJson = await autoLoginResult.json()
|
const autoLoginJson = await autoLoginResult.json()
|
||||||
setUser(autoLoginJson.user)
|
setUser(autoLoginJson.user)
|
||||||
@@ -253,6 +261,7 @@ export const AuthProvider: React.FC<{ children: React.ReactNode }> = ({ children
|
|||||||
searchParams,
|
searchParams,
|
||||||
admin,
|
admin,
|
||||||
revokeTokenAndExpire,
|
revokeTokenAndExpire,
|
||||||
|
loginRoute,
|
||||||
])
|
])
|
||||||
|
|
||||||
// On mount, get user and set
|
// On mount, get user and set
|
||||||
|
|||||||
@@ -8,12 +8,18 @@ import { wait } from 'payload/utilities'
|
|||||||
import { fileURLToPath } from 'url'
|
import { fileURLToPath } from 'url'
|
||||||
|
|
||||||
import type { PayloadTestSDK } from '../helpers/sdk/index.js'
|
import type { PayloadTestSDK } from '../helpers/sdk/index.js'
|
||||||
import type { Config, ReadOnlyCollection, RestrictedVersion } from './payload-types.js'
|
import type {
|
||||||
|
Config,
|
||||||
|
NonAdminUser,
|
||||||
|
ReadOnlyCollection,
|
||||||
|
RestrictedVersion,
|
||||||
|
} from './payload-types.js'
|
||||||
|
|
||||||
import {
|
import {
|
||||||
closeNav,
|
closeNav,
|
||||||
ensureAutoLoginAndCompilationIsDone,
|
ensureAutoLoginAndCompilationIsDone,
|
||||||
exactText,
|
exactText,
|
||||||
|
getAdminRoutes,
|
||||||
initPageConsoleErrorCatch,
|
initPageConsoleErrorCatch,
|
||||||
login,
|
login,
|
||||||
openDocControls,
|
openDocControls,
|
||||||
@@ -58,6 +64,7 @@ describe('access control', () => {
|
|||||||
let restrictedVersionsUrl: AdminUrlUtil
|
let restrictedVersionsUrl: AdminUrlUtil
|
||||||
let serverURL: string
|
let serverURL: string
|
||||||
let context: BrowserContext
|
let context: BrowserContext
|
||||||
|
let logoutURL: string
|
||||||
|
|
||||||
beforeAll(async ({ browser }, testInfo) => {
|
beforeAll(async ({ browser }, testInfo) => {
|
||||||
testInfo.setTimeout(TEST_TIMEOUT_LONG)
|
testInfo.setTimeout(TEST_TIMEOUT_LONG)
|
||||||
@@ -75,6 +82,15 @@ describe('access control', () => {
|
|||||||
|
|
||||||
await login({ page, serverURL })
|
await login({ page, serverURL })
|
||||||
await ensureAutoLoginAndCompilationIsDone({ page, serverURL })
|
await ensureAutoLoginAndCompilationIsDone({ page, serverURL })
|
||||||
|
|
||||||
|
const {
|
||||||
|
admin: {
|
||||||
|
routes: { logout: logoutRoute },
|
||||||
|
},
|
||||||
|
routes: { admin: adminRoute },
|
||||||
|
} = getAdminRoutes({})
|
||||||
|
|
||||||
|
logoutURL = `${serverURL}${adminRoute}${logoutRoute}`
|
||||||
})
|
})
|
||||||
|
|
||||||
test('field without read access should not show', async () => {
|
test('field without read access should not show', async () => {
|
||||||
@@ -352,8 +368,8 @@ describe('access control', () => {
|
|||||||
|
|
||||||
await expect(page.locator('.dashboard')).toBeVisible()
|
await expect(page.locator('.dashboard')).toBeVisible()
|
||||||
|
|
||||||
await page.goto(`${serverURL}/admin/logout`)
|
await page.goto(logoutURL)
|
||||||
await page.waitForURL(`${serverURL}/admin/logout`)
|
await page.waitForURL(logoutURL)
|
||||||
|
|
||||||
await login({
|
await login({
|
||||||
page,
|
page,
|
||||||
@@ -366,8 +382,8 @@ describe('access control', () => {
|
|||||||
|
|
||||||
await expect(page.locator('.next-error-h1')).toBeVisible()
|
await expect(page.locator('.next-error-h1')).toBeVisible()
|
||||||
|
|
||||||
await page.goto(`${serverURL}/admin/logout`)
|
await page.goto(logoutURL)
|
||||||
await page.waitForURL(`${serverURL}/admin/logout`)
|
await page.waitForURL(logoutURL)
|
||||||
|
|
||||||
// Log back in for the next test
|
// Log back in for the next test
|
||||||
await login({
|
await login({
|
||||||
@@ -387,10 +403,12 @@ describe('access control', () => {
|
|||||||
|
|
||||||
await expect(page.locator('.dashboard')).toBeVisible()
|
await expect(page.locator('.dashboard')).toBeVisible()
|
||||||
|
|
||||||
await page.goto(`${serverURL}/admin/logout`)
|
await page.goto(logoutURL)
|
||||||
await page.waitForURL(`${serverURL}/admin/logout`)
|
await page.waitForURL(logoutURL)
|
||||||
|
|
||||||
const nonAdminUser = await payload.login({
|
const nonAdminUser: NonAdminUser & {
|
||||||
|
token?: string
|
||||||
|
} = await payload.login({
|
||||||
collection: nonAdminUserSlug,
|
collection: nonAdminUserSlug,
|
||||||
data: {
|
data: {
|
||||||
email: nonAdminUserEmail,
|
email: nonAdminUserEmail,
|
||||||
@@ -398,7 +416,7 @@ describe('access control', () => {
|
|||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
context.addCookies([
|
await context.addCookies([
|
||||||
{
|
{
|
||||||
name: 'payload-token',
|
name: 'payload-token',
|
||||||
value: nonAdminUser.token,
|
value: nonAdminUser.token,
|
||||||
@@ -418,5 +436,5 @@ async function createDoc(data: any): Promise<TypeWithID & Record<string, unknown
|
|||||||
return payload.create({
|
return payload.create({
|
||||||
collection: slug,
|
collection: slug,
|
||||||
data,
|
data,
|
||||||
})
|
}) as any as Promise<TypeWithID & Record<string, unknown>>
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,9 +7,12 @@ import React from 'react'
|
|||||||
export const Logout: React.FC = () => {
|
export const Logout: React.FC = () => {
|
||||||
const config = useConfig()
|
const config = useConfig()
|
||||||
const {
|
const {
|
||||||
admin: { logoutRoute },
|
admin: {
|
||||||
|
routes: { logout: logoutRoute },
|
||||||
|
},
|
||||||
routes: { admin },
|
routes: { admin },
|
||||||
} = config
|
} = config
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<a href={`${admin}${logoutRoute}#custom`}>
|
<a href={`${admin}${logoutRoute}#custom`}>
|
||||||
<LogOut />
|
<LogOut />
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { buildConfigWithDefaults } from '../buildConfigWithDefaults.js'
|
import { buildConfigWithDefaults } from '../buildConfigWithDefaults.js'
|
||||||
|
import { devUser } from '../credentials.js'
|
||||||
import { CustomIdRow } from './collections/CustomIdRow.js'
|
import { CustomIdRow } from './collections/CustomIdRow.js'
|
||||||
import { CustomIdTab } from './collections/CustomIdTab.js'
|
import { CustomIdTab } from './collections/CustomIdTab.js'
|
||||||
import { CustomViews1 } from './collections/CustomViews1.js'
|
import { CustomViews1 } from './collections/CustomViews1.js'
|
||||||
@@ -35,7 +36,12 @@ import { GlobalGroup1B } from './globals/Group1B.js'
|
|||||||
import { GlobalHidden } from './globals/Hidden.js'
|
import { GlobalHidden } from './globals/Hidden.js'
|
||||||
import { GlobalNoApiView } from './globals/NoApiView.js'
|
import { GlobalNoApiView } from './globals/NoApiView.js'
|
||||||
import { seed } from './seed.js'
|
import { seed } from './seed.js'
|
||||||
import { customNestedViewPath, customParamViewPath, customViewPath } from './shared.js'
|
import {
|
||||||
|
customAdminRoutes,
|
||||||
|
customNestedViewPath,
|
||||||
|
customParamViewPath,
|
||||||
|
customViewPath,
|
||||||
|
} from './shared.js'
|
||||||
export default buildConfigWithDefaults({
|
export default buildConfigWithDefaults({
|
||||||
admin: {
|
admin: {
|
||||||
components: {
|
components: {
|
||||||
@@ -75,6 +81,7 @@ export default buildConfigWithDefaults({
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
routes: customAdminRoutes,
|
||||||
meta: {
|
meta: {
|
||||||
icons: [
|
icons: [
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -13,7 +13,9 @@ import {
|
|||||||
checkPageTitle,
|
checkPageTitle,
|
||||||
ensureAutoLoginAndCompilationIsDone,
|
ensureAutoLoginAndCompilationIsDone,
|
||||||
exactText,
|
exactText,
|
||||||
|
getAdminRoutes,
|
||||||
initPageConsoleErrorCatch,
|
initPageConsoleErrorCatch,
|
||||||
|
login,
|
||||||
openDocControls,
|
openDocControls,
|
||||||
openDocDrawer,
|
openDocDrawer,
|
||||||
openNav,
|
openNav,
|
||||||
@@ -23,6 +25,7 @@ import {
|
|||||||
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 {
|
import {
|
||||||
|
customAdminRoutes,
|
||||||
customEditLabel,
|
customEditLabel,
|
||||||
customNestedTabViewPath,
|
customNestedTabViewPath,
|
||||||
customNestedTabViewTitle,
|
customNestedTabViewTitle,
|
||||||
@@ -61,7 +64,7 @@ import { fileURLToPath } from 'url'
|
|||||||
import type { PayloadTestSDK } from '../helpers/sdk/index.js'
|
import type { PayloadTestSDK } from '../helpers/sdk/index.js'
|
||||||
|
|
||||||
import { reInitializeDB } from '../helpers/reInitializeDB.js'
|
import { reInitializeDB } from '../helpers/reInitializeDB.js'
|
||||||
import { POLL_TOPASS_TIMEOUT, TEST_TIMEOUT, TEST_TIMEOUT_LONG } from '../playwright.config.js'
|
import { POLL_TOPASS_TIMEOUT, TEST_TIMEOUT_LONG } from '../playwright.config.js'
|
||||||
const filename = fileURLToPath(import.meta.url)
|
const filename = fileURLToPath(import.meta.url)
|
||||||
const dirname = path.dirname(filename)
|
const dirname = path.dirname(filename)
|
||||||
|
|
||||||
@@ -72,6 +75,8 @@ describe('admin', () => {
|
|||||||
let customViewsURL: AdminUrlUtil
|
let customViewsURL: AdminUrlUtil
|
||||||
let disableDuplicateURL: AdminUrlUtil
|
let disableDuplicateURL: AdminUrlUtil
|
||||||
let serverURL: string
|
let serverURL: string
|
||||||
|
let adminRoutes: ReturnType<typeof getAdminRoutes>
|
||||||
|
let loginURL: string
|
||||||
|
|
||||||
beforeAll(async ({ browser }, testInfo) => {
|
beforeAll(async ({ browser }, testInfo) => {
|
||||||
const prebuild = Boolean(process.env.CI)
|
const prebuild = Boolean(process.env.CI)
|
||||||
@@ -95,7 +100,12 @@ describe('admin', () => {
|
|||||||
serverURL,
|
serverURL,
|
||||||
snapshotKey: 'adminTests',
|
snapshotKey: 'adminTests',
|
||||||
})
|
})
|
||||||
await ensureAutoLoginAndCompilationIsDone({ page, serverURL })
|
|
||||||
|
await ensureAutoLoginAndCompilationIsDone({ page, serverURL, customAdminRoutes })
|
||||||
|
|
||||||
|
adminRoutes = getAdminRoutes({ customAdminRoutes })
|
||||||
|
|
||||||
|
loginURL = `${serverURL}${adminRoutes.routes.admin}${adminRoutes.admin.routes.login}`
|
||||||
})
|
})
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
await reInitializeDB({
|
await reInitializeDB({
|
||||||
@@ -103,7 +113,7 @@ describe('admin', () => {
|
|||||||
snapshotKey: 'adminTests',
|
snapshotKey: 'adminTests',
|
||||||
})
|
})
|
||||||
|
|
||||||
await ensureAutoLoginAndCompilationIsDone({ page, serverURL })
|
await ensureAutoLoginAndCompilationIsDone({ page, serverURL, customAdminRoutes })
|
||||||
})
|
})
|
||||||
|
|
||||||
describe('metadata', () => {
|
describe('metadata', () => {
|
||||||
@@ -130,6 +140,28 @@ describe('admin', () => {
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
describe('routing', () => {
|
||||||
|
test('should use custom logout route', async () => {
|
||||||
|
await page.goto(`${serverURL}${adminRoutes.routes.admin}${adminRoutes.admin.routes.logout}`)
|
||||||
|
|
||||||
|
await page.waitForURL(
|
||||||
|
`${serverURL}${adminRoutes.routes.admin}${adminRoutes.admin.routes.logout}`,
|
||||||
|
)
|
||||||
|
|
||||||
|
await expect(() => expect(page.url()).not.toContain(loginURL)).toPass({
|
||||||
|
timeout: POLL_TOPASS_TIMEOUT,
|
||||||
|
})
|
||||||
|
|
||||||
|
// Ensure auto-login logged the user back in
|
||||||
|
|
||||||
|
await expect(() => expect(page.url()).toBe(`${serverURL}${adminRoutes.routes.admin}`)).toPass(
|
||||||
|
{
|
||||||
|
timeout: POLL_TOPASS_TIMEOUT,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
describe('navigation', () => {
|
describe('navigation', () => {
|
||||||
test('nav — should navigate to collection', async () => {
|
test('nav — should navigate to collection', async () => {
|
||||||
await page.goto(postsUrl.admin)
|
await page.goto(postsUrl.admin)
|
||||||
@@ -206,7 +238,7 @@ describe('admin', () => {
|
|||||||
|
|
||||||
test('breadcrumbs — should navigate from list to dashboard', async () => {
|
test('breadcrumbs — should navigate from list to dashboard', async () => {
|
||||||
await page.goto(postsUrl.list)
|
await page.goto(postsUrl.list)
|
||||||
await page.locator('.step-nav a[href="/admin"]').click()
|
await page.locator(`.step-nav a[href="${adminRoutes.routes.admin}"]`).click()
|
||||||
expect(page.url()).toContain(postsUrl.admin)
|
expect(page.url()).toContain(postsUrl.admin)
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -214,7 +246,7 @@ describe('admin', () => {
|
|||||||
const { id } = await createPost()
|
const { id } = await createPost()
|
||||||
await page.goto(postsUrl.edit(id))
|
await page.goto(postsUrl.edit(id))
|
||||||
const collectionBreadcrumb = page.locator(
|
const collectionBreadcrumb = page.locator(
|
||||||
`.step-nav a[href="/admin/collections/${postsCollectionSlug}"]`,
|
`.step-nav a[href="${adminRoutes.routes.admin}/collections/${postsCollectionSlug}"]`,
|
||||||
)
|
)
|
||||||
await expect(collectionBreadcrumb).toBeVisible()
|
await expect(collectionBreadcrumb).toBeVisible()
|
||||||
await expect(collectionBreadcrumb).toHaveText(slugPluralLabel)
|
await expect(collectionBreadcrumb).toHaveText(slugPluralLabel)
|
||||||
@@ -248,16 +280,16 @@ describe('admin', () => {
|
|||||||
|
|
||||||
describe('custom views', () => {
|
describe('custom views', () => {
|
||||||
test('root — should render custom view', async () => {
|
test('root — should render custom view', async () => {
|
||||||
await page.goto(`${serverURL}/admin${customViewPath}`)
|
await page.goto(`${serverURL}${adminRoutes.routes.admin}${customViewPath}`)
|
||||||
await page.waitForURL(`**/admin${customViewPath}`)
|
await page.waitForURL(`**${adminRoutes.routes.admin}${customViewPath}`)
|
||||||
await expect(page.locator('h1#custom-view-title')).toContainText(customViewTitle)
|
await expect(page.locator('h1#custom-view-title')).toContainText(customViewTitle)
|
||||||
})
|
})
|
||||||
|
|
||||||
test('root — should render custom nested view', async () => {
|
test('root — should render custom nested view', async () => {
|
||||||
await page.goto(`${serverURL}/admin${customNestedViewPath}`)
|
await page.goto(`${serverURL}${adminRoutes.routes.admin}${customNestedViewPath}`)
|
||||||
const pageURL = page.url()
|
const pageURL = page.url()
|
||||||
const pathname = new URL(pageURL).pathname
|
const pathname = new URL(pageURL).pathname
|
||||||
expect(pathname).toEqual(`/admin${customNestedViewPath}`)
|
expect(pathname).toEqual(`${adminRoutes.routes.admin}${customNestedViewPath}`)
|
||||||
await expect(page.locator('h1#custom-view-title')).toContainText(customNestedViewTitle)
|
await expect(page.locator('h1#custom-view-title')).toContainText(customNestedViewTitle)
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -831,7 +863,10 @@ describe('admin', () => {
|
|||||||
const { id } = await createPost()
|
const { id } = await createPost()
|
||||||
await page.reload()
|
await page.reload()
|
||||||
const linkCell = page.locator(`${tableRowLocator} td`).nth(1).locator('a')
|
const linkCell = page.locator(`${tableRowLocator} td`).nth(1).locator('a')
|
||||||
await expect(linkCell).toHaveAttribute('href', `/admin/collections/posts/${id}`)
|
await expect(linkCell).toHaveAttribute(
|
||||||
|
'href',
|
||||||
|
`${adminRoutes.routes.admin}/collections/posts/${id}`,
|
||||||
|
)
|
||||||
|
|
||||||
// open the column controls
|
// open the column controls
|
||||||
await page.locator('.list-controls__toggle-columns').click()
|
await page.locator('.list-controls__toggle-columns').click()
|
||||||
@@ -849,7 +884,10 @@ describe('admin', () => {
|
|||||||
await page.locator('.cell-id').waitFor({ state: 'detached' })
|
await page.locator('.cell-id').waitFor({ state: 'detached' })
|
||||||
|
|
||||||
// recheck that the 2nd cell is still a link
|
// recheck that the 2nd cell is still a link
|
||||||
await expect(linkCell).toHaveAttribute('href', `/admin/collections/posts/${id}`)
|
await expect(linkCell).toHaveAttribute(
|
||||||
|
'href',
|
||||||
|
`${adminRoutes.routes.admin}/collections/posts/${id}`,
|
||||||
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
test('should filter rows', async () => {
|
test('should filter rows', async () => {
|
||||||
|
|||||||
@@ -34,3 +34,8 @@ export const customNestedTabViewTitle = 'Custom Nested Tab View'
|
|||||||
export const customCollectionParamViewPathBase = '/custom-param'
|
export const customCollectionParamViewPathBase = '/custom-param'
|
||||||
|
|
||||||
export const customCollectionParamViewPath = `${customCollectionParamViewPathBase}/:slug`
|
export const customCollectionParamViewPath = `${customCollectionParamViewPathBase}/:slug`
|
||||||
|
|
||||||
|
export const customAdminRoutes = {
|
||||||
|
logout: '/custom-logout',
|
||||||
|
inactivity: '/custom-inactivity',
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import type { Page } from '@playwright/test'
|
import type { Page } from '@playwright/test'
|
||||||
|
import type { SanitizedConfig } from 'payload/types'
|
||||||
|
|
||||||
import { expect, test } from '@playwright/test'
|
import { expect, test } from '@playwright/test'
|
||||||
import { devUser } from 'credentials.js'
|
import { devUser } from 'credentials.js'
|
||||||
@@ -12,6 +13,7 @@ import type { Config } from './payload-types.js'
|
|||||||
|
|
||||||
import {
|
import {
|
||||||
ensureAutoLoginAndCompilationIsDone,
|
ensureAutoLoginAndCompilationIsDone,
|
||||||
|
getAdminRoutes,
|
||||||
initPageConsoleErrorCatch,
|
initPageConsoleErrorCatch,
|
||||||
saveDocAndAssert,
|
saveDocAndAssert,
|
||||||
} from '../helpers.js'
|
} from '../helpers.js'
|
||||||
@@ -31,8 +33,28 @@ const headers = {
|
|||||||
'Content-Type': 'application/json',
|
'Content-Type': 'application/json',
|
||||||
}
|
}
|
||||||
|
|
||||||
const createFirstUser = async ({ page, serverURL }: { page: Page; serverURL: string }) => {
|
const createFirstUser = async ({
|
||||||
await page.goto(serverURL + '/admin/create-first-user')
|
page,
|
||||||
|
serverURL,
|
||||||
|
customAdminRoutes,
|
||||||
|
customRoutes,
|
||||||
|
}: {
|
||||||
|
customAdminRoutes?: SanitizedConfig['admin']['routes']
|
||||||
|
customRoutes?: SanitizedConfig['routes']
|
||||||
|
page: Page
|
||||||
|
serverURL: string
|
||||||
|
}) => {
|
||||||
|
const {
|
||||||
|
admin: {
|
||||||
|
routes: { createFirstUser: createFirstUserRoute },
|
||||||
|
},
|
||||||
|
routes: { admin: adminRoute },
|
||||||
|
} = getAdminRoutes({
|
||||||
|
customAdminRoutes,
|
||||||
|
customRoutes,
|
||||||
|
})
|
||||||
|
|
||||||
|
await page.goto(serverURL + `${adminRoute}${createFirstUserRoute}`)
|
||||||
await page.locator('#field-email').fill(devUser.email)
|
await page.locator('#field-email').fill(devUser.email)
|
||||||
await page.locator('#field-password').fill(devUser.password)
|
await page.locator('#field-password').fill(devUser.password)
|
||||||
await page.locator('#field-confirm-password').fill(devUser.password)
|
await page.locator('#field-confirm-password').fill(devUser.password)
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
import type { BrowserContext, ChromiumBrowserContext, Locator, Page } from '@playwright/test'
|
import type { BrowserContext, ChromiumBrowserContext, Locator, Page } from '@playwright/test'
|
||||||
|
import type { Config } from 'payload/config'
|
||||||
|
|
||||||
import { expect } from '@playwright/test'
|
import { expect } from '@playwright/test'
|
||||||
|
import { defaults } from 'payload/config'
|
||||||
import { wait } from 'payload/utilities'
|
import { wait } from 'payload/utilities'
|
||||||
import shelljs from 'shelljs'
|
import shelljs from 'shelljs'
|
||||||
import { setTimeout } from 'timers/promises'
|
import { setTimeout } from 'timers/promises'
|
||||||
@@ -9,11 +11,15 @@ import { devUser } from './credentials.js'
|
|||||||
import { POLL_TOPASS_TIMEOUT } from './playwright.config.js'
|
import { POLL_TOPASS_TIMEOUT } from './playwright.config.js'
|
||||||
|
|
||||||
type FirstRegisterArgs = {
|
type FirstRegisterArgs = {
|
||||||
|
customAdminRoutes?: Config['admin']['routes']
|
||||||
|
customRoutes?: Config['routes']
|
||||||
page: Page
|
page: Page
|
||||||
serverURL: string
|
serverURL: string
|
||||||
}
|
}
|
||||||
|
|
||||||
type LoginArgs = {
|
type LoginArgs = {
|
||||||
|
customAdminRoutes?: Config['admin']['routes']
|
||||||
|
customRoutes?: Config['routes']
|
||||||
data?: {
|
data?: {
|
||||||
email: string
|
email: string
|
||||||
password: string
|
password: string
|
||||||
@@ -49,20 +55,36 @@ const networkConditions = {
|
|||||||
export async function ensureAutoLoginAndCompilationIsDone({
|
export async function ensureAutoLoginAndCompilationIsDone({
|
||||||
page,
|
page,
|
||||||
serverURL,
|
serverURL,
|
||||||
|
customAdminRoutes,
|
||||||
|
customRoutes,
|
||||||
}: {
|
}: {
|
||||||
|
customAdminRoutes?: Config['admin']['routes']
|
||||||
|
customRoutes?: Config['routes']
|
||||||
page: Page
|
page: Page
|
||||||
serverURL: string
|
serverURL: string
|
||||||
}): Promise<void> {
|
}): Promise<void> {
|
||||||
const adminURL = `${serverURL}/admin`
|
const {
|
||||||
|
admin: {
|
||||||
|
routes: { login: loginRoute, createFirstUser: createFirstUserRoute },
|
||||||
|
},
|
||||||
|
routes: { admin: adminRoute },
|
||||||
|
} = getAdminRoutes({ customAdminRoutes, customRoutes })
|
||||||
|
|
||||||
|
const adminURL = `${serverURL}${adminRoute}`
|
||||||
|
|
||||||
await page.goto(adminURL)
|
await page.goto(adminURL)
|
||||||
await page.waitForURL(adminURL)
|
await page.waitForURL(adminURL)
|
||||||
await expect(() => expect(page.url()).not.toContain(`/admin/login`)).toPass({
|
|
||||||
|
await expect(() => expect(page.url()).not.toContain(`${adminRoute}${loginRoute}`)).toPass({
|
||||||
timeout: POLL_TOPASS_TIMEOUT,
|
timeout: POLL_TOPASS_TIMEOUT,
|
||||||
})
|
})
|
||||||
await expect(() => expect(page.url()).not.toContain(`/admin/create-first-user`)).toPass({
|
|
||||||
|
await expect(() =>
|
||||||
|
expect(page.url()).not.toContain(`${adminRoute}${createFirstUserRoute}`),
|
||||||
|
).toPass({
|
||||||
timeout: POLL_TOPASS_TIMEOUT,
|
timeout: POLL_TOPASS_TIMEOUT,
|
||||||
})
|
})
|
||||||
|
|
||||||
// Check if hero is there
|
// Check if hero is there
|
||||||
await expect(page.locator('.dashboard__label').first()).toBeVisible()
|
await expect(page.locator('.dashboard__label').first()).toBeVisible()
|
||||||
}
|
}
|
||||||
@@ -98,34 +120,47 @@ export async function throttleTest({
|
|||||||
}
|
}
|
||||||
|
|
||||||
export async function firstRegister(args: FirstRegisterArgs): Promise<void> {
|
export async function firstRegister(args: FirstRegisterArgs): Promise<void> {
|
||||||
const { page, serverURL } = args
|
const { page, serverURL, customAdminRoutes, customRoutes } = args
|
||||||
|
|
||||||
await page.goto(`${serverURL}/admin`)
|
const {
|
||||||
|
routes: { admin: adminRoute },
|
||||||
|
} = getAdminRoutes({ customAdminRoutes, customRoutes })
|
||||||
|
|
||||||
|
await page.goto(`${serverURL}${adminRoute}`)
|
||||||
await page.fill('#field-email', devUser.email)
|
await page.fill('#field-email', devUser.email)
|
||||||
await page.fill('#field-password', devUser.password)
|
await page.fill('#field-password', devUser.password)
|
||||||
await page.fill('#field-confirm-password', devUser.password)
|
await page.fill('#field-confirm-password', devUser.password)
|
||||||
await wait(500)
|
await wait(500)
|
||||||
await page.click('[type=submit]')
|
await page.click('[type=submit]')
|
||||||
await page.waitForURL(`${serverURL}/admin`)
|
await page.waitForURL(`${serverURL}${adminRoute}`)
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function login(args: LoginArgs): Promise<void> {
|
export async function login(args: LoginArgs): Promise<void> {
|
||||||
const { page, serverURL, data = devUser } = args
|
const { page, serverURL, data = devUser, customAdminRoutes, customRoutes } = args
|
||||||
|
|
||||||
await page.goto(`${serverURL}/admin/login`)
|
const {
|
||||||
await page.waitForURL(`${serverURL}/admin/login`)
|
admin: {
|
||||||
|
routes: { login: loginRoute, createFirstUser: createFirstUserRoute },
|
||||||
|
},
|
||||||
|
routes: { admin: adminRoute },
|
||||||
|
} = getAdminRoutes({ customAdminRoutes, customRoutes })
|
||||||
|
|
||||||
|
await page.goto(`${serverURL}${adminRoute}${loginRoute}`)
|
||||||
|
await page.waitForURL(`${serverURL}${adminRoute}${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}/admin`)
|
await page.waitForURL(`${serverURL}${adminRoute}`)
|
||||||
|
|
||||||
await expect(() => expect(page.url()).not.toContain(`/admin/login`)).toPass({
|
await expect(() => expect(page.url()).not.toContain(`${adminRoute}${loginRoute}`)).toPass({
|
||||||
timeout: POLL_TOPASS_TIMEOUT,
|
timeout: POLL_TOPASS_TIMEOUT,
|
||||||
})
|
})
|
||||||
|
|
||||||
await expect(() => expect(page.url()).not.toContain(`/admin/create-first-user`)).toPass({
|
await expect(() =>
|
||||||
|
expect(page.url()).not.toContain(`${adminRoute}${createFirstUserRoute}`),
|
||||||
|
).toPass({
|
||||||
timeout: POLL_TOPASS_TIMEOUT,
|
timeout: POLL_TOPASS_TIMEOUT,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -288,3 +323,42 @@ export function describeIfInCIOrHasLocalstack(): jest.Describe {
|
|||||||
|
|
||||||
return describe
|
return describe
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type AdminRoutes = Config['admin']['routes']
|
||||||
|
|
||||||
|
export function getAdminRoutes({
|
||||||
|
customRoutes,
|
||||||
|
customAdminRoutes,
|
||||||
|
}: {
|
||||||
|
customAdminRoutes?: AdminRoutes
|
||||||
|
customRoutes?: Config['routes']
|
||||||
|
}): {
|
||||||
|
admin: {
|
||||||
|
routes: AdminRoutes
|
||||||
|
}
|
||||||
|
routes: Config['routes']
|
||||||
|
} {
|
||||||
|
let routes = defaults.routes
|
||||||
|
let adminRoutes = defaults.admin.routes
|
||||||
|
|
||||||
|
if (customAdminRoutes) {
|
||||||
|
adminRoutes = {
|
||||||
|
...adminRoutes,
|
||||||
|
...customAdminRoutes,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (customRoutes) {
|
||||||
|
routes = {
|
||||||
|
...routes,
|
||||||
|
...customRoutes,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
admin: {
|
||||||
|
routes: adminRoutes,
|
||||||
|
},
|
||||||
|
routes,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user