fix(ui): public users unable to log out (#10188)

Fixes #10180. When logged in as an unauthorized user who cannot access
the admin panel, the user is unable to log out through the prompted
`/admin/logout` page. This was because that page was using an incorrect
API endpoint, reading from `admin.user` instead of `user.collection`
when formatting the route. This page was also able to get stuck in an
infinite loading state when attempting to log out without any user at
all. Now, public users can properly log out and then back in with
another user who might have access. The messaging around this was also
misleading. Instead of displaying the "Unauthorized, you must be logged
in to make this request" message, we now display a new "Unauthorized,
this user does not have access to the admin panel" message for added
clarity.
This commit is contained in:
Jacob Fletcher
2024-12-26 22:52:00 -05:00
committed by GitHub
parent 5613a7ebe1
commit f3aebe3263
52 changed files with 825 additions and 739 deletions

View File

@@ -9,6 +9,7 @@ type Args = {
searchParams: { [key: string]: string | string[] } searchParams: { [key: string]: string | string[] }
user?: User user?: User
} }
export const handleAuthRedirect = ({ config, route, searchParams, user }: Args): string => { export const handleAuthRedirect = ({ config, route, searchParams, user }: Args): string => {
const { const {
admin: { admin: {

View File

@@ -110,7 +110,7 @@ export const initPage = async ({
}, },
}) })
?.then((res) => res.docs?.[0]?.value as string) ?.then((res) => res.docs?.[0]?.value as string)
} catch (error) {} // eslint-disable-line no-empty } catch (_err) {} // eslint-disable-line no-empty
} }
locale = findLocaleFromCode(localization, localeCode) locale = findLocaleFromCode(localization, localeCode)

View File

@@ -19,8 +19,11 @@ export const LogoutClient: React.FC<{
const { adminRoute, inactivity, redirect } = props const { adminRoute, inactivity, redirect } = props
const { logOut, user } = useAuth() const { logOut, user } = useAuth()
const [isLoggedOut, setIsLoggedOut] = React.useState<boolean>(!user) const [isLoggedOut, setIsLoggedOut] = React.useState<boolean>(!user)
const logOutSuccessRef = React.useRef(false) const logOutSuccessRef = React.useRef(false)
const [loginRoute] = React.useState(() => const [loginRoute] = React.useState(() =>
formatAdminURL({ formatAdminURL({
adminRoute, adminRoute,
@@ -49,8 +52,10 @@ export const LogoutClient: React.FC<{
useEffect(() => { useEffect(() => {
if (!isLoggedOut) { if (!isLoggedOut) {
void handleLogOut() void handleLogOut()
} else {
router.push(loginRoute)
} }
}, [handleLogOut, isLoggedOut]) }, [handleLogOut, isLoggedOut, loginRoute, router])
if (isLoggedOut && inactivity) { if (isLoggedOut && inactivity) {
return ( return (

View File

@@ -2,8 +2,9 @@
@layer payload-default { @layer payload-default {
.unauthorized { .unauthorized {
&__button { &__button.btn {
margin: 0; margin: 0;
margin-block: 0;
} }
} }
} }

View File

@@ -18,6 +18,7 @@ export const UnauthorizedView: PayloadServerReactComponent<AdminViewComponent> =
initPageResult, initPageResult,
}) => { }) => {
const { const {
permissions,
req: { req: {
i18n, i18n,
payload: { payload: {
@@ -28,6 +29,7 @@ export const UnauthorizedView: PayloadServerReactComponent<AdminViewComponent> =
routes: { admin: adminRoute }, routes: { admin: adminRoute },
}, },
}, },
user,
}, },
} = initPageResult } = initPageResult
@@ -35,9 +37,10 @@ export const UnauthorizedView: PayloadServerReactComponent<AdminViewComponent> =
<div className={baseClass}> <div className={baseClass}>
<FormHeader <FormHeader
description={i18n.t('error:notAllowedToAccessPage')} description={i18n.t('error:notAllowedToAccessPage')}
heading={i18n.t('error:unauthorized')} heading={i18n.t(
user && !permissions.canAccessAdmin ? 'error:unauthorizedAdmin' : 'error:unauthorized',
)}
/> />
<Button <Button
className={`${baseClass}__button`} className={`${baseClass}__button`}
el="link" el="link"

View File

@@ -78,6 +78,7 @@ export const clientTranslationKeys = createClientTranslationKeys([
'error:unableToReindexCollection', 'error:unableToReindexCollection',
'error:unableToUpdateCount', 'error:unableToUpdateCount',
'error:unauthorized', 'error:unauthorized',
'error:unauthorizedAdmin',
'error:unknown', 'error:unknown',
'error:unspecific', 'error:unspecific',
'error:userEmailAlreadyRegistered', 'error:userEmailAlreadyRegistered',

View File

@@ -115,6 +115,7 @@ export const arTranslations: DefaultTranslationsObject = {
unableToReindexCollection: 'خطأ في إعادة فهرسة المجموعة {{collection}}. تم إيقاف العملية.', unableToReindexCollection: 'خطأ في إعادة فهرسة المجموعة {{collection}}. تم إيقاف العملية.',
unableToUpdateCount: 'يتعذّر تحديث {{count}} من {{total}} {{label}}.', unableToUpdateCount: 'يتعذّر تحديث {{count}} من {{total}} {{label}}.',
unauthorized: 'غير مصرّح لك ، عليك أن تقوم بتسجيل الدّخول لتتمكّن من تقديم هذا الطّلب.', unauthorized: 'غير مصرّح لك ، عليك أن تقوم بتسجيل الدّخول لتتمكّن من تقديم هذا الطّلب.',
unauthorizedAdmin: 'غير مصرّح لك بالوصول إلى لوحة التحكّم.',
unknown: 'حدث خطأ غير معروف.', unknown: 'حدث خطأ غير معروف.',
unPublishingDocument: 'حدث خطأ أثناء إلغاء نشر هذا المستند.', unPublishingDocument: 'حدث خطأ أثناء إلغاء نشر هذا المستند.',
unspecific: 'حدث خطأ.', unspecific: 'حدث خطأ.',

View File

@@ -116,6 +116,7 @@ export const azTranslations: DefaultTranslationsObject = {
'{{collection}} kolleksiyasının yenidən indekslənməsi zamanı səhv baş verdi. Əməliyyat dayandırıldı.', '{{collection}} kolleksiyasının yenidən indekslənməsi zamanı səhv baş verdi. Əməliyyat dayandırıldı.',
unableToUpdateCount: '{{count}} dən {{total}} {{label}} yenilənə bilmir.', unableToUpdateCount: '{{count}} dən {{total}} {{label}} yenilənə bilmir.',
unauthorized: 'İcazəniz yoxdur, bu tələbi yerinə yetirmək üçün daxil olmalısınız.', unauthorized: 'İcazəniz yoxdur, bu tələbi yerinə yetirmək üçün daxil olmalısınız.',
unauthorizedAdmin: 'Bu əməliyyatı yerinə yetirmək üçün admin olmalısınız.',
unknown: 'Naməlum bir xəta baş verdi.', unknown: 'Naməlum bir xəta baş verdi.',
unPublishingDocument: 'Bu sənədin nəşrini ləğv etmək zamanı problem baş verdi.', unPublishingDocument: 'Bu sənədin nəşrini ləğv etmək zamanı problem baş verdi.',
unspecific: 'Xəta baş verdi.', unspecific: 'Xəta baş verdi.',

View File

@@ -116,6 +116,7 @@ export const bgTranslations: DefaultTranslationsObject = {
'Грешка при преиндексиране на колекцията {{collection}}. Операцията е прекратена.', 'Грешка при преиндексиране на колекцията {{collection}}. Операцията е прекратена.',
unableToUpdateCount: 'Не беше възможно да се обновят {{count}} от {{total}} {{label}}.', unableToUpdateCount: 'Не беше възможно да се обновят {{count}} от {{total}} {{label}}.',
unauthorized: 'Неоторизиран, трябва да влезеш, за да извършиш тази заявка.', unauthorized: 'Неоторизиран, трябва да влезеш, за да извършиш тази заявка.',
unauthorizedAdmin: 'Неоторизиран, трябва да си администратор, за да извършиш тази заявка.',
unknown: 'Неизвестна грешка.', unknown: 'Неизвестна грешка.',
unPublishingDocument: 'Имаше проблем при скриването на този документ.', unPublishingDocument: 'Имаше проблем при скриването на този документ.',
unspecific: 'Грешка.', unspecific: 'Грешка.',

View File

@@ -116,6 +116,7 @@ export const csTranslations: DefaultTranslationsObject = {
'Chyba při přeindexování kolekce {{collection}}. Operace byla přerušena.', 'Chyba při přeindexování kolekce {{collection}}. Operace byla přerušena.',
unableToUpdateCount: 'Nelze aktualizovat {{count}} z {{total}} {{label}}.', unableToUpdateCount: 'Nelze aktualizovat {{count}} z {{total}} {{label}}.',
unauthorized: 'Neautorizováno, pro zadání tohoto požadavku musíte být přihlášeni.', unauthorized: 'Neautorizováno, pro zadání tohoto požadavku musíte být přihlášeni.',
unauthorizedAdmin: 'Neautorizováno, tento uživatel nemá přístup k administraci.',
unknown: 'Došlo k neznámé chybě.', unknown: 'Došlo k neznámé chybě.',
unPublishingDocument: 'Při zrušení publikování tohoto dokumentu došlo k chybě.', unPublishingDocument: 'Při zrušení publikování tohoto dokumentu došlo k chybě.',
unspecific: 'Došlo k chybě.', unspecific: 'Došlo k chybě.',

View File

@@ -115,6 +115,7 @@ export const daTranslations: DefaultTranslationsObject = {
'Fejl ved genindeksering af samling {{collection}}. Operationen blev afbrudt.', 'Fejl ved genindeksering af samling {{collection}}. Operationen blev afbrudt.',
unableToUpdateCount: 'Kunne ikke slette {{count}} mangler {{total}} {{label}}.', unableToUpdateCount: 'Kunne ikke slette {{count}} mangler {{total}} {{label}}.',
unauthorized: 'Uautoriseret, log in for at gennemføre handlingen.', unauthorized: 'Uautoriseret, log in for at gennemføre handlingen.',
unauthorizedAdmin: 'Uautoriseret, denne bruger har ikke adgang til adminpanelet.',
unknown: 'En ukendt fejl er opstået.', unknown: 'En ukendt fejl er opstået.',
unPublishingDocument: 'Der opstod et problem med at ophæve udgivelsen af dette dokument.', unPublishingDocument: 'Der opstod et problem med at ophæve udgivelsen af dette dokument.',
unspecific: 'En fejl er opstået.', unspecific: 'En fejl er opstået.',

View File

@@ -118,6 +118,7 @@ export const deTranslations: DefaultTranslationsObject = {
'Fehler beim Neuindizieren der Sammlung {{collection}}. Vorgang abgebrochen.', 'Fehler beim Neuindizieren der Sammlung {{collection}}. Vorgang abgebrochen.',
unableToUpdateCount: '{{count}} von {{total}} {{label}} konnte nicht aktualisiert werden.', unableToUpdateCount: '{{count}} von {{total}} {{label}} konnte nicht aktualisiert werden.',
unauthorized: 'Nicht autorisiert - du musst angemeldet sein, um diese Anfrage zu stellen.', unauthorized: 'Nicht autorisiert - du musst angemeldet sein, um diese Anfrage zu stellen.',
unauthorizedAdmin: 'Nicht autorisiert, dieser Benutzer hat keinen Zugriff auf das Admin-Panel.',
unknown: 'Ein unbekannter Fehler ist aufgetreten.', unknown: 'Ein unbekannter Fehler ist aufgetreten.',
unPublishingDocument: 'Es gab ein Problem, dieses Dokument auf Entwurf zu setzen.', unPublishingDocument: 'Es gab ein Problem, dieses Dokument auf Entwurf zu setzen.',
unspecific: 'Ein Fehler ist aufgetreten.', unspecific: 'Ein Fehler ist aufgetreten.',

View File

@@ -116,6 +116,7 @@ export const enTranslations = {
unableToReindexCollection: 'Error reindexing collection {{collection}}. Operation aborted.', unableToReindexCollection: 'Error reindexing collection {{collection}}. Operation aborted.',
unableToUpdateCount: 'Unable to update {{count}} out of {{total}} {{label}}.', unableToUpdateCount: 'Unable to update {{count}} out of {{total}} {{label}}.',
unauthorized: 'Unauthorized, you must be logged in to make this request.', unauthorized: 'Unauthorized, you must be logged in to make this request.',
unauthorizedAdmin: 'Unauthorized, this user does not have access to the admin panel.',
unknown: 'An unknown error has occurred.', unknown: 'An unknown error has occurred.',
unPublishingDocument: 'There was a problem while un-publishing this document.', unPublishingDocument: 'There was a problem while un-publishing this document.',
unspecific: 'An error has occurred.', unspecific: 'An error has occurred.',

View File

@@ -116,6 +116,7 @@ export const esTranslations: DefaultTranslationsObject = {
'Error al reindexar la colección {{collection}}. Operación abortada.', 'Error al reindexar la colección {{collection}}. Operación abortada.',
unableToUpdateCount: 'No se puede actualizar {{count}} de {{total}} {{label}}.', unableToUpdateCount: 'No se puede actualizar {{count}} de {{total}} {{label}}.',
unauthorized: 'No autorizado, debes iniciar sesión para realizar esta solicitud.', unauthorized: 'No autorizado, debes iniciar sesión para realizar esta solicitud.',
unauthorizedAdmin: 'No autorizado, este usuario no tiene acceso al panel de administración.',
unknown: 'Ocurrió un error desconocido.', unknown: 'Ocurrió un error desconocido.',
unPublishingDocument: 'Ocurrió un error al despublicar este documento.', unPublishingDocument: 'Ocurrió un error al despublicar este documento.',
unspecific: 'Ocurrió un error.', unspecific: 'Ocurrió un error.',

View File

@@ -114,6 +114,7 @@ export const faTranslations: DefaultTranslationsObject = {
unableToReindexCollection: 'خطا در بازنمایه‌سازی مجموعه {{collection}}. عملیات متوقف شد.', unableToReindexCollection: 'خطا در بازنمایه‌سازی مجموعه {{collection}}. عملیات متوقف شد.',
unableToUpdateCount: 'امکان به روز رسانی {{count}} خارج از {{total}} {{label}} وجود ندارد.', unableToUpdateCount: 'امکان به روز رسانی {{count}} خارج از {{total}} {{label}} وجود ندارد.',
unauthorized: 'درخواست نامعتبر، جهت فرستادن این درخواست باید وارد شوید.', unauthorized: 'درخواست نامعتبر، جهت فرستادن این درخواست باید وارد شوید.',
unauthorizedAdmin: 'دسترسی به پیشخوان برای این کاربر مجاز نیست.',
unknown: 'یک خطای ناشناخته رخ داد.', unknown: 'یک خطای ناشناخته رخ داد.',
unPublishingDocument: 'هنگام لغو انتشار این سند خطایی رخ داد.', unPublishingDocument: 'هنگام لغو انتشار این سند خطایی رخ داد.',
unspecific: 'خطایی رخ داد.', unspecific: 'خطایی رخ داد.',

View File

@@ -119,6 +119,7 @@ export const frTranslations: DefaultTranslationsObject = {
'Erreur lors de la réindexation de la collection {{collection}}. Opération annulée.', 'Erreur lors de la réindexation de la collection {{collection}}. Opération annulée.',
unableToUpdateCount: 'Impossible de mettre à jour {{count}} sur {{total}} {{label}}.', unableToUpdateCount: 'Impossible de mettre à jour {{count}} sur {{total}} {{label}}.',
unauthorized: 'Non autorisé, vous devez être connecté pour effectuer cette demande.', unauthorized: 'Non autorisé, vous devez être connecté pour effectuer cette demande.',
unauthorizedAdmin: 'Non autorisé, cet utilisateur na pas accès au panneau dadministration.',
unknown: 'Une erreur inconnue sest produite.', unknown: 'Une erreur inconnue sest produite.',
unPublishingDocument: unPublishingDocument:
'Un problème est survenu lors de lannulation de la publication de ce document.', 'Un problème est survenu lors de lannulation de la publication de ce document.',

View File

@@ -113,6 +113,7 @@ export const heTranslations: DefaultTranslationsObject = {
unableToReindexCollection: 'שגיאה בהחזרת אינדקס של אוסף {{collection}}. הפעולה בוטלה.', unableToReindexCollection: 'שגיאה בהחזרת אינדקס של אוסף {{collection}}. הפעולה בוטלה.',
unableToUpdateCount: 'לא ניתן לעדכן {{count}} מתוך {{total}} {{label}}.', unableToUpdateCount: 'לא ניתן לעדכן {{count}} מתוך {{total}} {{label}}.',
unauthorized: 'אין הרשאה, עליך להתחבר כדי לבצע בקשה זו.', unauthorized: 'אין הרשאה, עליך להתחבר כדי לבצע בקשה זו.',
unauthorizedAdmin: 'אין הרשאה, משתמש זה אינו יכול לגשת לפאנל הניהול.',
unknown: 'אירעה שגיאה לא ידועה.', unknown: 'אירעה שגיאה לא ידועה.',
unPublishingDocument: 'אירעה בעיה בביטול הפרסום של מסמך זה.', unPublishingDocument: 'אירעה בעיה בביטול הפרסום של מסמך זה.',
unspecific: 'אירעה שגיאה.', unspecific: 'אירעה שגיאה.',

View File

@@ -117,6 +117,7 @@ export const hrTranslations: DefaultTranslationsObject = {
'Pogreška pri ponovnom indeksiranju kolekcije {{collection}}. Operacija je prekinuta.', 'Pogreška pri ponovnom indeksiranju kolekcije {{collection}}. Operacija je prekinuta.',
unableToUpdateCount: 'Nije moguće ažurirati {{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.', unauthorized: 'Neovlašteno, morate biti prijavljeni da biste uputili ovaj zahtjev.',
unauthorizedAdmin: 'Neovlašteno, ovaj korisnik nema pristup administratorskom panelu.',
unknown: 'Došlo je do nepoznate pogreške.', unknown: 'Došlo je do nepoznate pogreške.',
unPublishingDocument: 'Došlo je do problema pri poništavanju objave ovog dokumenta.', unPublishingDocument: 'Došlo je do problema pri poništavanju objave ovog dokumenta.',
unspecific: 'Došlo je do pogreške.', unspecific: 'Došlo je do pogreške.',

View File

@@ -118,6 +118,7 @@ export const huTranslations: DefaultTranslationsObject = {
'Hiba a(z) {{collection}} gyűjtemény újraindexelésekor. A művelet megszakítva.', 'Hiba a(z) {{collection}} gyűjtemény újraindexelésekor. A művelet megszakítva.',
unableToUpdateCount: 'Nem sikerült frissíteni {{count}}/{{total}} {{label}}.', unableToUpdateCount: 'Nem sikerült frissíteni {{count}}/{{total}} {{label}}.',
unauthorized: 'Jogosulatlan, a kéréshez be kell jelentkeznie.', unauthorized: 'Jogosulatlan, a kéréshez be kell jelentkeznie.',
unauthorizedAdmin: 'Jogosulatlan, ez a felhasználó nem fér hozzá az admin panelhez.',
unknown: 'Ismeretlen hiba történt.', unknown: 'Ismeretlen hiba történt.',
unPublishingDocument: 'Hiba történt a dokumentum közzétételének visszavonása közben.', unPublishingDocument: 'Hiba történt a dokumentum közzétételének visszavonása közben.',
unspecific: 'Hiba történt.', unspecific: 'Hiba történt.',

View File

@@ -118,6 +118,8 @@ export const itTranslations: DefaultTranslationsObject = {
'Errore durante la reindicizzazione della collezione {{collection}}. Operazione annullata.', 'Errore durante la reindicizzazione della collezione {{collection}}. Operazione annullata.',
unableToUpdateCount: 'Impossibile aggiornare {{count}} su {{total}} {{label}}.', unableToUpdateCount: 'Impossibile aggiornare {{count}} su {{total}} {{label}}.',
unauthorized: 'Non autorizzato, devi essere loggato per effettuare questa richiesta.', unauthorized: 'Non autorizzato, devi essere loggato per effettuare questa richiesta.',
unauthorizedAdmin:
'Non autorizzato, questo utente non ha accesso al pannello di amministrazione.',
unknown: 'Si è verificato un errore sconosciuto.', unknown: 'Si è verificato un errore sconosciuto.',
unPublishingDocument: unPublishingDocument:
"Si è verificato un problema durante l'annullamento della pubblicazione di questo documento.", "Si è verificato un problema durante l'annullamento della pubblicazione di questo documento.",

View File

@@ -117,6 +117,7 @@ export const jaTranslations: DefaultTranslationsObject = {
'コレクション {{collection}} の再インデックス中にエラーが発生しました。操作は中止されました。', 'コレクション {{collection}} の再インデックス中にエラーが発生しました。操作は中止されました。',
unableToUpdateCount: '{{total}} {{label}} のうち {{count}} 個を更新できません。', unableToUpdateCount: '{{total}} {{label}} のうち {{count}} 個を更新できません。',
unauthorized: '認証されていません。このリクエストを行うにはログインが必要です。', unauthorized: '認証されていません。このリクエストを行うにはログインが必要です。',
unauthorizedAdmin: '管理画面へのアクセス権がないため、認証されていません。',
unknown: '不明なエラーが発生しました。', unknown: '不明なエラーが発生しました。',
unPublishingDocument: 'このデータを非公開する際に問題が発生しました。', unPublishingDocument: 'このデータを非公開する際に問題が発生しました。',
unspecific: 'エラーが発生しました。', unspecific: 'エラーが発生しました。',

View File

@@ -116,6 +116,7 @@ export const koTranslations: DefaultTranslationsObject = {
'{{collection}} 컬렉션의 재인덱싱 중 오류가 발생했습니다. 작업이 중단되었습니다.', '{{collection}} 컬렉션의 재인덱싱 중 오류가 발생했습니다. 작업이 중단되었습니다.',
unableToUpdateCount: '총 {{total}}개 중 {{count}}개의 {{label}}을(를) 업데이트할 수 없습니다.', unableToUpdateCount: '총 {{total}}개 중 {{count}}개의 {{label}}을(를) 업데이트할 수 없습니다.',
unauthorized: '권한 없음, 이 요청을 수행하려면 로그인해야 합니다.', unauthorized: '권한 없음, 이 요청을 수행하려면 로그인해야 합니다.',
unauthorizedAdmin: '관리자 패널에 액세스할 수 없습니다.',
unknown: '알 수 없는 오류가 발생했습니다.', unknown: '알 수 없는 오류가 발생했습니다.',
unPublishingDocument: '이 문서의 게시 취소 중에 문제가 발생했습니다.', unPublishingDocument: '이 문서의 게시 취소 중에 문제가 발생했습니다.',
unspecific: '오류가 발생했습니다.', unspecific: '오류가 발생했습니다.',

View File

@@ -116,6 +116,7 @@ export const myTranslations: DefaultTranslationsObject = {
'{{collection}} စုစည်းမှုကို ပြန်လည်အညွှန်းပြုလုပ်ခြင်း အမှားရှိနေသည်။ လုပ်ဆောင်မှုကို ဖျက်သိမ်းခဲ့သည်။', '{{collection}} စုစည်းမှုကို ပြန်လည်အညွှန်းပြုလုပ်ခြင်း အမှားရှိနေသည်။ လုပ်ဆောင်မှုကို ဖျက်သိမ်းခဲ့သည်။',
unableToUpdateCount: '{{total}} {{label}} မှ {{count}} ကို အပ်ဒိတ်လုပ်၍မရပါ။', unableToUpdateCount: '{{total}} {{label}} မှ {{count}} ကို အပ်ဒိတ်လုပ်၍မရပါ။',
unauthorized: 'အခွင့်မရှိပါ။ ဤတောင်းဆိုချက်ကို လုပ်ဆောင်နိုင်ရန် သင်သည် လော့ဂ်အင်ဝင်ရပါမည်။', unauthorized: 'အခွင့်မရှိပါ။ ဤတောင်းဆိုချက်ကို လုပ်ဆောင်နိုင်ရန် သင်သည် လော့ဂ်အင်ဝင်ရပါမည်။',
unauthorizedAdmin: 'အခွင့်မရှိပါ။ ဤအကောင့်အသုံးပြုသူသည် အဆင့်မပြုပါနိုင်ပါ။',
unknown: 'ဘာမှန်းမသိသော error တက်သွားပါသည်။', unknown: 'ဘာမှန်းမသိသော error တက်သွားပါသည်။',
unPublishingDocument: 'ဖိုင်ကို ပြန်လည့် သိမ်းဆည်းခြင်းမှာ ပြဿနာရှိနေသည်။', unPublishingDocument: 'ဖိုင်ကို ပြန်လည့် သိမ်းဆည်းခြင်းမှာ ပြဿနာရှိနေသည်။',
unspecific: 'Error တက်နေပါသည်။', unspecific: 'Error တက်နေပါသည်။',

View File

@@ -116,6 +116,7 @@ export const nbTranslations: DefaultTranslationsObject = {
'Feil ved reindeksering av samlingen {{collection}}. Operasjonen ble avbrutt.', 'Feil ved reindeksering av samlingen {{collection}}. Operasjonen ble avbrutt.',
unableToUpdateCount: 'Kan ikke oppdatere {{count}} av {{total}} {{label}}.', unableToUpdateCount: 'Kan ikke oppdatere {{count}} av {{total}} {{label}}.',
unauthorized: 'Uautorisert, du må være innlogget for å gjøre denne forespørselen.', unauthorized: 'Uautorisert, du må være innlogget for å gjøre denne forespørselen.',
unauthorizedAdmin: 'Uautorisert, denne brukeren har ikke tilgang til kontrollpanelet.',
unknown: 'En ukjent feil har oppstått.', unknown: 'En ukjent feil har oppstått.',
unPublishingDocument: 'Det oppstod et problem under avpublisering av dokumentet.', unPublishingDocument: 'Det oppstod et problem under avpublisering av dokumentet.',
unspecific: 'En feil har oppstått.', unspecific: 'En feil har oppstått.',

View File

@@ -117,6 +117,8 @@ export const nlTranslations: DefaultTranslationsObject = {
'Fout bij het herindexeren van de collectie {{collection}}. De operatie is afgebroken.', 'Fout bij het herindexeren van de collectie {{collection}}. De operatie is afgebroken.',
unableToUpdateCount: 'Kan {{count}} van {{total}} {{label}} niet updaten.', unableToUpdateCount: 'Kan {{count}} van {{total}} {{label}} niet updaten.',
unauthorized: 'Ongeautoriseerd, u moet ingelogd zijn om dit verzoek te doen.', unauthorized: 'Ongeautoriseerd, u moet ingelogd zijn om dit verzoek te doen.',
unauthorizedAdmin:
'Ongeautoriseerd, deze gebruiker heeft geen toegang tot het beheerderspaneel.',
unknown: 'Er is een onbekende fout opgetreden.', unknown: 'Er is een onbekende fout opgetreden.',
unPublishingDocument: 'Er was een probleem met het depubliceren van dit document.', unPublishingDocument: 'Er was een probleem met het depubliceren van dit document.',
unspecific: 'Er is een fout opgetreden.', unspecific: 'Er is een fout opgetreden.',

View File

@@ -116,6 +116,7 @@ export const plTranslations: DefaultTranslationsObject = {
'Błąd podczas ponownego indeksowania kolekcji {{collection}}. Operacja została przerwana.', 'Błąd podczas ponownego indeksowania kolekcji {{collection}}. Operacja została przerwana.',
unableToUpdateCount: 'Nie można zaktualizować {{count}} z {{total}} {{label}}.', unableToUpdateCount: 'Nie można zaktualizować {{count}} z {{total}} {{label}}.',
unauthorized: 'Brak dostępu, musisz być zalogowany.', unauthorized: 'Brak dostępu, musisz być zalogowany.',
unauthorizedAdmin: 'Brak dostępu, ten użytkownik nie ma dostępu do panelu administracyjnego.',
unknown: 'Wystąpił nieznany błąd.', unknown: 'Wystąpił nieznany błąd.',
unPublishingDocument: 'Wystąpił problem podczas cofania publikacji tego dokumentu.', unPublishingDocument: 'Wystąpił problem podczas cofania publikacji tego dokumentu.',
unspecific: 'Wystąpił błąd', unspecific: 'Wystąpił błąd',

View File

@@ -116,6 +116,7 @@ export const ptTranslations: DefaultTranslationsObject = {
unableToReindexCollection: 'Erro ao reindexar a coleção {{collection}}. Operação abortada.', unableToReindexCollection: 'Erro ao reindexar a coleção {{collection}}. Operação abortada.',
unableToUpdateCount: 'Não foi possível atualizar {{count}} de {{total}} {{label}}.', unableToUpdateCount: 'Não foi possível atualizar {{count}} de {{total}} {{label}}.',
unauthorized: 'Não autorizado. Você deve estar logado para fazer essa requisição', unauthorized: 'Não autorizado. Você deve estar logado para fazer essa requisição',
unauthorizedAdmin: 'Não autorizado, esse usuário não tem acesso ao painel de administração.',
unknown: 'Ocorreu um erro desconhecido.', unknown: 'Ocorreu um erro desconhecido.',
unPublishingDocument: 'Ocorreu um problema ao despublicar esse documento', unPublishingDocument: 'Ocorreu um problema ao despublicar esse documento',
unspecific: 'Ocorreu um erro.', unspecific: 'Ocorreu um erro.',

View File

@@ -117,7 +117,8 @@ export const roTranslations: DefaultTranslationsObject = {
unableToReindexCollection: unableToReindexCollection:
'Eroare la reindexarea colecției {{collection}}. Operațiune anulată.', 'Eroare la reindexarea colecției {{collection}}. Operațiune anulată.',
unableToUpdateCount: 'Nu se poate șterge {{count}} din {{total}} {{label}}.', unableToUpdateCount: 'Nu se poate șterge {{count}} din {{total}} {{label}}.',
unauthorized: 'neautorizat, trebuie să vă conectați pentru a face această cerere.', unauthorized: 'Neautorizat, trebuie să vă conectați pentru a face această cerere.',
unauthorizedAdmin: 'Neautorizat, acest utilizator nu are acces la panoul de administrare.',
unknown: 'S-a produs o eroare necunoscută.', unknown: 'S-a produs o eroare necunoscută.',
unPublishingDocument: 'A existat o problemă în timpul nepublicării acestui document.', unPublishingDocument: 'A existat o problemă în timpul nepublicării acestui document.',
unspecific: 'S-a produs o eroare.', unspecific: 'S-a produs o eroare.',

View File

@@ -117,6 +117,7 @@ export const rsTranslations: DefaultTranslationsObject = {
'Грешка при реиндексирању колекције {{collection}}. Операција је прекинута.', 'Грешка при реиндексирању колекције {{collection}}. Операција је прекинута.',
unableToUpdateCount: 'Није могуће ажурирати {{count}} од {{total}} {{label}}.', unableToUpdateCount: 'Није могуће ажурирати {{count}} од {{total}} {{label}}.',
unauthorized: 'Нисте ауторизовани да бисте упутили овај захтев.', unauthorized: 'Нисте ауторизовани да бисте упутили овај захтев.',
unauthorizedAdmin: 'Немате приступ администраторском панелу.',
unknown: 'Дошло је до непознате грешке.', unknown: 'Дошло је до непознате грешке.',
unPublishingDocument: 'Постоји проблем при поништавању објаве овог документа.', unPublishingDocument: 'Постоји проблем при поништавању објаве овог документа.',
unspecific: 'Дошло је до грешке.', unspecific: 'Дошло је до грешке.',

View File

@@ -117,6 +117,7 @@ export const rsLatinTranslations: DefaultTranslationsObject = {
'Greška pri reindeksiranju kolekcije {{collection}}. Operacija je prekinuta.', 'Greška pri reindeksiranju kolekcije {{collection}}. Operacija je prekinuta.',
unableToUpdateCount: 'Nije moguće ažurirati {{count}} od {{total}} {{label}}.', unableToUpdateCount: 'Nije moguće ažurirati {{count}} od {{total}} {{label}}.',
unauthorized: 'Niste autorizovani da biste uputili ovaj zahtev.', unauthorized: 'Niste autorizovani da biste uputili ovaj zahtev.',
unauthorizedAdmin: 'Nemate pristup administratorskom panelu.',
unknown: 'Došlo je do nepoznate greške.', unknown: 'Došlo je do nepoznate greške.',
unPublishingDocument: 'Postoji problem pri poništavanju objave ovog dokumenta.', unPublishingDocument: 'Postoji problem pri poništavanju objave ovog dokumenta.',
unspecific: 'Došlo je do greške.', unspecific: 'Došlo je do greške.',

View File

@@ -117,6 +117,7 @@ export const ruTranslations: DefaultTranslationsObject = {
'Ошибка при переиндексации коллекции {{collection}}. Операция прервана.', 'Ошибка при переиндексации коллекции {{collection}}. Операция прервана.',
unableToUpdateCount: 'Не удалось обновить {{count}} из {{total}} {{label}}.', unableToUpdateCount: 'Не удалось обновить {{count}} из {{total}} {{label}}.',
unauthorized: 'Нет доступа, вы должны войти, чтобы сделать этот запрос.', unauthorized: 'Нет доступа, вы должны войти, чтобы сделать этот запрос.',
unauthorizedAdmin: 'Нет доступа, этот пользователь не имеет доступа к панели администратора.',
unknown: 'Произошла неизвестная ошибка.', unknown: 'Произошла неизвестная ошибка.',
unPublishingDocument: 'При отмене публикации этого документа возникла проблема.', unPublishingDocument: 'При отмене публикации этого документа возникла проблема.',
unspecific: 'Произошла ошибка.', unspecific: 'Произошла ошибка.',

View File

@@ -117,6 +117,8 @@ export const skTranslations: DefaultTranslationsObject = {
'Chyba pri reindexácii kolekcie {{collection}}. Operácia bola prerušená.', 'Chyba pri reindexácii kolekcie {{collection}}. Operácia bola prerušená.',
unableToUpdateCount: 'Nie je možné aktualizovať {{count}} z {{total}} {{label}}.', unableToUpdateCount: 'Nie je možné aktualizovať {{count}} z {{total}} {{label}}.',
unauthorized: 'Neautorizováno, pro zadání tohoto požadavku musíte být přihlášeni.', unauthorized: 'Neautorizováno, pro zadání tohoto požadavku musíte být přihlášeni.',
unauthorizedAdmin:
'Neoprávnený prístup, tento používateľ nemá prístup k administrátorskému panelu.',
unknown: 'Došlo k neznámej chybe.', unknown: 'Došlo k neznámej chybe.',
unPublishingDocument: 'Pri zrušení publikovania tohto dokumentu došlo k chybe.', unPublishingDocument: 'Pri zrušení publikovania tohto dokumentu došlo k chybe.',
unspecific: 'Došlo k chybe.', unspecific: 'Došlo k chybe.',

View File

@@ -116,6 +116,7 @@ export const slTranslations: DefaultTranslationsObject = {
'Napaka pri reindeksiranju zbirke {{collection}}. Operacija je bila prekinjena.', 'Napaka pri reindeksiranju zbirke {{collection}}. Operacija je bila prekinjena.',
unableToUpdateCount: 'Ni bilo mogoče posodobiti {{count}} od {{total}} {{label}}.', unableToUpdateCount: 'Ni bilo mogoče posodobiti {{count}} od {{total}} {{label}}.',
unauthorized: 'Neavtorizirano, za to zahtevo morate biti prijavljeni.', unauthorized: 'Neavtorizirano, za to zahtevo morate biti prijavljeni.',
unauthorizedAdmin: 'Neavtorizirano, ta uporabnik nima dostopa do skrbniškega vmesnika.',
unknown: 'Prišlo je do neznane napake.', unknown: 'Prišlo je do neznane napake.',
unPublishingDocument: 'Pri umiku objave tega dokumenta je prišlo do težave.', unPublishingDocument: 'Pri umiku objave tega dokumenta je prišlo do težave.',
unspecific: 'Prišlo je do napake.', unspecific: 'Prišlo je do napake.',

View File

@@ -116,6 +116,7 @@ export const svTranslations: DefaultTranslationsObject = {
'Fel vid omindexering av samlingen {{collection}}. Operationen avbröts.', 'Fel vid omindexering av samlingen {{collection}}. Operationen avbröts.',
unableToUpdateCount: 'Det gick inte att uppdatera {{count}} av {{total}} {{label}}.', unableToUpdateCount: 'Det gick inte att uppdatera {{count}} av {{total}} {{label}}.',
unauthorized: 'Obehörig, du måste vara inloggad för att göra denna begäran.', unauthorized: 'Obehörig, du måste vara inloggad för att göra denna begäran.',
unauthorizedAdmin: 'Obehörig, denna användare har inte åtkomst till adminpanelen.',
unknown: 'Ett okänt fel har uppstått.', unknown: 'Ett okänt fel har uppstått.',
unPublishingDocument: 'Det uppstod ett problem när det här dokumentet skulle avpubliceras.', unPublishingDocument: 'Det uppstod ett problem när det här dokumentet skulle avpubliceras.',
unspecific: 'Ett fel har uppstått.', unspecific: 'Ett fel har uppstått.',

View File

@@ -114,6 +114,7 @@ export const thTranslations: DefaultTranslationsObject = {
'เกิดข้อผิดพลาดในการจัดทำดัชนีใหม่ของคอลเลกชัน {{collection}}. การดำเนินการถูกยกเลิก', 'เกิดข้อผิดพลาดในการจัดทำดัชนีใหม่ของคอลเลกชัน {{collection}}. การดำเนินการถูกยกเลิก',
unableToUpdateCount: 'ไม่สามารถอัปเดต {{count}} จาก {{total}} {{label}}', unableToUpdateCount: 'ไม่สามารถอัปเดต {{count}} จาก {{total}} {{label}}',
unauthorized: 'คุณไม่ได้รับอนุญาต กรุณาเข้าสู่ระบบเพื่อทำคำขอนี้', unauthorized: 'คุณไม่ได้รับอนุญาต กรุณาเข้าสู่ระบบเพื่อทำคำขอนี้',
unauthorizedAdmin: 'คุณไม่ได้รับอนุญาตให้เข้าถึงแผงผู้ดูแล',
unknown: 'เกิดปัญหาบางอย่างที่ไม่ทราบสาเหตุ', unknown: 'เกิดปัญหาบางอย่างที่ไม่ทราบสาเหตุ',
unPublishingDocument: 'เกิดปัญหาระหว่างการยกเลิกการเผยแพร่เอกสารนี้', unPublishingDocument: 'เกิดปัญหาระหว่างการยกเลิกการเผยแพร่เอกสารนี้',
unspecific: 'เกิดปัญหาบางอย่าง', unspecific: 'เกิดปัญหาบางอย่าง',

View File

@@ -117,6 +117,7 @@ export const trTranslations: DefaultTranslationsObject = {
'{{collection}} koleksiyonunun yeniden indekslenmesinde hata oluştu. İşlem durduruldu.', '{{collection}} koleksiyonunun yeniden indekslenmesinde hata oluştu. İşlem durduruldu.',
unableToUpdateCount: '{{total}} {{label}} içinden {{count}} güncellenemiyor.', unableToUpdateCount: '{{total}} {{label}} içinden {{count}} güncellenemiyor.',
unauthorized: 'Bu işlemi gerçekleştirmek için lütfen giriş yapın.', unauthorized: 'Bu işlemi gerçekleştirmek için lütfen giriş yapın.',
unauthorizedAdmin: 'Bu kullanıcı yönetici paneline erişim iznine sahip değil.',
unknown: 'Bilinmeyen bir hata oluştu.', unknown: 'Bilinmeyen bir hata oluştu.',
unPublishingDocument: 'Geçerli döküman yayından kaldırılırken bir sorun oluştu.', unPublishingDocument: 'Geçerli döküman yayından kaldırılırken bir sorun oluştu.',
unspecific: 'Bir hata oluştu.', unspecific: 'Bir hata oluştu.',

View File

@@ -117,6 +117,7 @@ export const ukTranslations: DefaultTranslationsObject = {
'Помилка при повторному індексуванні колекції {{collection}}. Операцію скасовано.', 'Помилка при повторному індексуванні колекції {{collection}}. Операцію скасовано.',
unableToUpdateCount: 'Не вдалося оновити {{count}} із {{total}} {{label}}.', unableToUpdateCount: 'Не вдалося оновити {{count}} із {{total}} {{label}}.',
unauthorized: 'Немає доступу, ви повинні увійти, щоб виконати цей запит.', unauthorized: 'Немає доступу, ви повинні увійти, щоб виконати цей запит.',
unauthorizedAdmin: 'Немає доступу, цей користувач не має доступу до панелі адміністратора.',
unknown: 'Виникла невідома помилка.', unknown: 'Виникла невідома помилка.',
unPublishingDocument: 'Під час скасування публікації даного документа виникла помилка.', unPublishingDocument: 'Під час скасування публікації даного документа виникла помилка.',
unspecific: 'Виникла помилка.', unspecific: 'Виникла помилка.',

View File

@@ -116,6 +116,7 @@ export const viTranslations: DefaultTranslationsObject = {
'Lỗi khi tái lập chỉ mục bộ sưu tập {{collection}}. Quá trình bị hủy.', 'Lỗi khi tái lập chỉ mục bộ sưu tập {{collection}}. Quá trình bị hủy.',
unableToUpdateCount: 'Không thể cập nhật {{count}} trên {{total}} {{label}}.', unableToUpdateCount: 'Không thể cập nhật {{count}} trên {{total}} {{label}}.',
unauthorized: 'Lỗi - Bạn cần phải đăng nhập trước khi gửi request sau.', unauthorized: 'Lỗi - Bạn cần phải đăng nhập trước khi gửi request sau.',
unauthorizedAdmin: 'Lỗi - Người dùng không có quyền truy cập vào bảng điều khiển.',
unknown: 'Lỗi - Không xác định (unknown error).', unknown: 'Lỗi - Không xác định (unknown error).',
unPublishingDocument: 'Lỗi - Đã xảy ra vấn để khi ẩn bản tài liệu.', unPublishingDocument: 'Lỗi - Đã xảy ra vấn để khi ẩn bản tài liệu.',
unspecific: 'Lỗi - Đã xảy ra (unspecific error).', unspecific: 'Lỗi - Đã xảy ra (unspecific error).',

View File

@@ -111,6 +111,7 @@ export const zhTranslations: DefaultTranslationsObject = {
unableToReindexCollection: '重新索引集合 {{collection}} 时出错。操作已中止。', unableToReindexCollection: '重新索引集合 {{collection}} 时出错。操作已中止。',
unableToUpdateCount: '无法更新 {{count}} 个,共 {{total}} 个 {{label}}。', unableToUpdateCount: '无法更新 {{count}} 个,共 {{total}} 个 {{label}}。',
unauthorized: '未经授权,您必须登录才能提出这个请求。', unauthorized: '未经授权,您必须登录才能提出这个请求。',
unauthorizedAdmin: '未经授权,此用户无权访问管理面板。',
unknown: '发生了一个未知的错误。', unknown: '发生了一个未知的错误。',
unPublishingDocument: '取消发布此文件时出现了问题。', unPublishingDocument: '取消发布此文件时出现了问题。',
unspecific: '发生了一个错误。', unspecific: '发生了一个错误。',

View File

@@ -111,6 +111,7 @@ export const zhTwTranslations: DefaultTranslationsObject = {
unableToReindexCollection: '重新索引集合 {{collection}} 時出現錯誤。操作已中止。', unableToReindexCollection: '重新索引集合 {{collection}} 時出現錯誤。操作已中止。',
unableToUpdateCount: '無法從 {{total}} 個中更新 {{count}} 個 {{label}}。', unableToUpdateCount: '無法從 {{total}} 個中更新 {{count}} 個 {{label}}。',
unauthorized: '未經授權,您必須登錄才能提出這個請求。', unauthorized: '未經授權,您必須登錄才能提出這個請求。',
unauthorizedAdmin: '未經授權,此使用者無法訪問管理面板。',
unknown: '發生了一個未知的錯誤。', unknown: '發生了一個未知的錯誤。',
unPublishingDocument: '取消發布此文件時出現了問題。', unPublishingDocument: '取消發布此文件時出現了問題。',
unspecific: '發生了一個錯誤。', unspecific: '發生了一個錯誤。',

View File

@@ -196,7 +196,7 @@ export function AuthProvider({
const logOut = useCallback(async () => { const logOut = useCallback(async () => {
try { try {
await requests.post(`${serverURL}${apiRoute}/${userSlug}/logout`) await requests.post(`${serverURL}${apiRoute}/${user.collection}/logout`)
setNewUser(null) setNewUser(null)
revokeTokenAndExpire() revokeTokenAndExpire()
return true return true
@@ -204,7 +204,7 @@ export function AuthProvider({
toast.error(`Logging out failed: ${e.message}`) toast.error(`Logging out failed: ${e.message}`)
return false return false
} }
}, [apiRoute, revokeTokenAndExpire, serverURL, setNewUser, userSlug]) }, [apiRoute, revokeTokenAndExpire, serverURL, setNewUser, user])
const refreshPermissions = useCallback( const refreshPermissions = useCallback(
async ({ locale }: { locale?: string } = {}) => { async ({ locale }: { locale?: string } = {}) => {
@@ -309,7 +309,7 @@ export function AuthProvider({
clearTimeout(forceLogOut) clearTimeout(forceLogOut)
} }
} }
}, [tokenExpiration, openModal, i18n, setNewUser, user]) }, [tokenExpiration, openModal, i18n, setNewUser, user, redirectToInactivityRoute])
return ( return (
<Context.Provider <Context.Provider

File diff suppressed because it is too large Load Diff

View File

@@ -9,12 +9,7 @@ import { wait } from 'payload/shared'
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 { import type { Config, ReadOnlyCollection, RestrictedVersion } from './payload-types.js'
Config,
NonAdminUser,
ReadOnlyCollection,
RestrictedVersion,
} from './payload-types.js'
import { import {
closeNav, closeNav,
@@ -34,9 +29,9 @@ import {
disabledSlug, disabledSlug,
docLevelAccessSlug, docLevelAccessSlug,
fullyRestrictedSlug, fullyRestrictedSlug,
noAdminAccessEmail, nonAdminEmail,
nonAdminUserEmail, publicUserEmail,
nonAdminUserSlug, publicUsersSlug,
readNotUpdateGlobalSlug, readNotUpdateGlobalSlug,
readOnlyGlobalSlug, readOnlyGlobalSlug,
readOnlySlug, readOnlySlug,
@@ -610,56 +605,56 @@ describe('access control', () => {
}) })
describe('admin access', () => { describe('admin access', () => {
test('should block admin access to admin user', async () => { test('unauthenticated users should not have access to the admin panel', async () => {
const adminURL = `${serverURL}/admin` await page.goto(url.logout)
await page.goto(adminURL) await page.waitForURL(url.logout)
await page.waitForURL(adminURL)
await expect(page.locator('.dashboard')).toBeVisible() await expect(page.locator('.payload-toast-container')).toContainText(
'You have been logged out successfully.',
)
await page.goto(logoutURL) await expect(page.locator('form.login__form')).toBeVisible()
await page.waitForURL(logoutURL)
await page.goto(url.admin)
await page.waitForURL(url.login)
expect(page.url()).toEqual(url.login)
})
test('non-admin users should not have access to the admin panel', async () => {
await page.goto(url.logout)
await page.waitForURL(url.logout)
await login({ await login({
data: { data: {
email: noAdminAccessEmail, email: nonAdminEmail,
password: 'test', password: 'test',
}, },
page, page,
serverURL, serverURL,
}) })
await expect(page.locator('.unauthorized')).toBeVisible() await expect(page.locator('.unauthorized .form-header h1')).toHaveText(
'Unauthorized, this user does not have access to the admin panel.',
)
// Log back in for the next test await page.goto(url.logout)
await page.goto(logoutURL) await page.waitForURL(url.logout)
await login({
data: { await expect(page.locator('.payload-toast-container')).toContainText(
email: devUser.email, 'You have been logged out successfully.',
password: devUser.password, )
},
page, await expect(page.locator('form.login__form')).toBeVisible()
serverURL,
})
}) })
test('should block admin access to non-admin user', async () => { test('public users should not have access to access admin', async () => {
const adminURL = `${serverURL}/admin` await page.goto(url.logout)
const unauthorizedURL = `${serverURL}/admin/unauthorized` await page.waitForURL(url.logout)
await page.goto(adminURL)
await page.waitForURL(adminURL)
await expect(page.locator('.dashboard')).toBeVisible() const user = await payload.login({
collection: publicUsersSlug,
await page.goto(logoutURL)
await page.waitForURL(logoutURL)
const nonAdminUser: {
token?: string
} & NonAdminUser = await payload.login({
collection: nonAdminUserSlug,
data: { data: {
email: nonAdminUserEmail, email: publicUserEmail,
password: devUser.password, password: devUser.password,
}, },
}) })
@@ -667,20 +662,36 @@ describe('access control', () => {
await context.addCookies([ await context.addCookies([
{ {
name: 'payload-token', name: 'payload-token',
url: serverURL, value: user.token,
value: nonAdminUser.token, domain: 'localhost',
path: '/',
httpOnly: true,
secure: true,
}, },
]) ])
await page.goto(adminURL) await page.reload()
await page.waitForURL(unauthorizedURL)
await expect(page.locator('.unauthorized')).toBeVisible() await page.goto(url.admin)
await page.waitForURL(/unauthorized$/)
// Log back in for the next test await expect(page.locator('.unauthorized .form-header h1')).toHaveText(
await context.clearCookies() 'Unauthorized, this user does not have access to the admin panel.',
await page.goto(logoutURL) )
await page.waitForURL(logoutURL)
await page.goto(url.logout)
await page.waitForURL(url.logout)
await expect(page.locator('.payload-toast-container')).toContainText(
'You have been logged out successfully.',
)
await expect(page.locator('form.login__form')).toBeVisible()
})
})
describe('read-only from access control', () => {
beforeAll(async () => {
await login({ await login({
data: { data: {
email: devUser.email, email: devUser.email,
@@ -690,9 +701,7 @@ describe('access control', () => {
serverURL, serverURL,
}) })
}) })
})
describe('read-only from access control', () => {
test('should be read-only when update returns false', async () => { test('should be read-only when update returns false', async () => {
await page.goto(disabledFields.create) await page.goto(disabledFields.create)

View File

@@ -9,11 +9,11 @@
export interface Config { export interface Config {
auth: { auth: {
users: UserAuthOperations; users: UserAuthOperations;
'non-admin-user': NonAdminUserAuthOperations; 'public-users': PublicUserAuthOperations;
}; };
collections: { collections: {
users: User; users: User;
'non-admin-user': NonAdminUser; 'public-users': PublicUser;
posts: Post; posts: Post;
unrestricted: Unrestricted; unrestricted: Unrestricted;
'relation-restricted': RelationRestricted; 'relation-restricted': RelationRestricted;
@@ -40,7 +40,7 @@ export interface Config {
collectionsJoins: {}; collectionsJoins: {};
collectionsSelect: { collectionsSelect: {
users: UsersSelect<false> | UsersSelect<true>; users: UsersSelect<false> | UsersSelect<true>;
'non-admin-user': NonAdminUserSelect<false> | NonAdminUserSelect<true>; 'public-users': PublicUsersSelect<false> | PublicUsersSelect<true>;
posts: PostsSelect<false> | PostsSelect<true>; posts: PostsSelect<false> | PostsSelect<true>;
unrestricted: UnrestrictedSelect<false> | UnrestrictedSelect<true>; unrestricted: UnrestrictedSelect<false> | UnrestrictedSelect<true>;
'relation-restricted': RelationRestrictedSelect<false> | RelationRestrictedSelect<true>; 'relation-restricted': RelationRestrictedSelect<false> | RelationRestrictedSelect<true>;
@@ -86,8 +86,8 @@ export interface Config {
| (User & { | (User & {
collection: 'users'; collection: 'users';
}) })
| (NonAdminUser & { | (PublicUser & {
collection: 'non-admin-user'; collection: 'public-users';
}); });
jobs: { jobs: {
tasks: unknown; tasks: unknown;
@@ -112,7 +112,7 @@ export interface UserAuthOperations {
password: string; password: string;
}; };
} }
export interface NonAdminUserAuthOperations { export interface PublicUserAuthOperations {
forgotPassword: { forgotPassword: {
email: string; email: string;
password: string; password: string;
@@ -150,9 +150,9 @@ export interface User {
} }
/** /**
* This interface was referenced by `Config`'s JSON-Schema * This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "non-admin-user". * via the `definition` "public-users".
*/ */
export interface NonAdminUser { export interface PublicUser {
id: string; id: string;
updatedAt: string; updatedAt: string;
createdAt: string; createdAt: string;
@@ -624,8 +624,8 @@ export interface PayloadLockedDocument {
value: string | User; value: string | User;
} | null) } | null)
| ({ | ({
relationTo: 'non-admin-user'; relationTo: 'public-users';
value: string | NonAdminUser; value: string | PublicUser;
} | null) } | null)
| ({ | ({
relationTo: 'posts'; relationTo: 'posts';
@@ -710,8 +710,8 @@ export interface PayloadLockedDocument {
value: string | User; value: string | User;
} }
| { | {
relationTo: 'non-admin-user'; relationTo: 'public-users';
value: string | NonAdminUser; value: string | PublicUser;
}; };
updatedAt: string; updatedAt: string;
createdAt: string; createdAt: string;
@@ -728,8 +728,8 @@ export interface PayloadPreference {
value: string | User; value: string | User;
} }
| { | {
relationTo: 'non-admin-user'; relationTo: 'public-users';
value: string | NonAdminUser; value: string | PublicUser;
}; };
key?: string | null; key?: string | null;
value?: value?:
@@ -773,9 +773,9 @@ export interface UsersSelect<T extends boolean = true> {
} }
/** /**
* This interface was referenced by `Config`'s JSON-Schema * This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "non-admin-user_select". * via the `definition` "public-users_select".
*/ */
export interface NonAdminUserSelect<T extends boolean = true> { export interface PublicUsersSelect<T extends boolean = true> {
updatedAt?: T; updatedAt?: T;
createdAt?: T; createdAt?: T;
email?: T; email?: T;

View File

@@ -18,11 +18,8 @@ export const docLevelAccessSlug = 'doc-level-access'
export const hiddenFieldsSlug = 'hidden-fields' export const hiddenFieldsSlug = 'hidden-fields'
export const hiddenAccessSlug = 'hidden-access' export const hiddenAccessSlug = 'hidden-access'
export const hiddenAccessCountSlug = 'hidden-access-count' export const hiddenAccessCountSlug = 'hidden-access-count'
export const noAdminAccessEmail = 'no-admin-access@payloadcms.com'
export const nonAdminUserEmail = 'non-admin-user@payloadcms.com'
export const nonAdminUserSlug = 'non-admin-user'
export const disabledSlug = 'disabled' export const disabledSlug = 'disabled'
export const nonAdminEmail = 'no-admin-access@payloadcms.com'
export const publicUserEmail = 'public-user@payloadcms.com'
export const publicUsersSlug = 'public-users'

View File

@@ -10,6 +10,7 @@ import {
apiKeysSlug, apiKeysSlug,
namedSaveToJWTValue, namedSaveToJWTValue,
partialDisableLocaleStrategiesSlug, partialDisableLocaleStrategiesSlug,
publicUsersSlug,
saveToJWTKey, saveToJWTKey,
slug, slug,
} from './shared.js' } from './shared.js'
@@ -209,7 +210,7 @@ export default buildConfigWithDefaults({
if (!user) { if (!user) {
return false return false
} }
if (user?.collection === 'api-keys') { if (user?.collection === apiKeysSlug) {
return { return {
id: { id: {
equals: user.id, equals: user.id,
@@ -230,7 +231,7 @@ export default buildConfigWithDefaults({
}, },
}, },
{ {
slug: 'public-users', slug: publicUsersSlug,
auth: { auth: {
verify: true, verify: true,
}, },
@@ -263,7 +264,7 @@ export default buildConfigWithDefaults({
}) })
await payload.create({ await payload.create({
collection: 'api-keys', collection: apiKeysSlug,
data: { data: {
apiKey: uuid(), apiKey: uuid(),
enableAPIKey: true, enableAPIKey: true,
@@ -271,7 +272,7 @@ export default buildConfigWithDefaults({
}) })
await payload.create({ await payload.create({
collection: 'api-keys', collection: apiKeysSlug,
data: { data: {
apiKey: uuid(), apiKey: uuid(),
enableAPIKey: true, enableAPIKey: true,

View File

@@ -1,9 +1,10 @@
import type { Page } from '@playwright/test' import type { BrowserContext, Page } from '@playwright/test'
import type { SanitizedConfig } from 'payload' import type { SanitizedConfig } from 'payload'
import { expect, test } from '@playwright/test' import { expect, test } from '@playwright/test'
import { devUser } from 'credentials.js' import { devUser } from 'credentials.js'
import path from 'path' import path from 'path'
import { wait } from 'payload/shared'
import { fileURLToPath } from 'url' import { fileURLToPath } from 'url'
import { v4 as uuid } from 'uuid' import { v4 as uuid } from 'uuid'
@@ -83,6 +84,7 @@ const createFirstUser = async ({
describe('auth', () => { describe('auth', () => {
let page: Page let page: Page
let context: BrowserContext
let url: AdminUrlUtil let url: AdminUrlUtil
let serverURL: string let serverURL: string
let apiURL: string let apiURL: string
@@ -93,7 +95,7 @@ describe('auth', () => {
apiURL = `${serverURL}/api` apiURL = `${serverURL}/api`
url = new AdminUrlUtil(serverURL, slug) url = new AdminUrlUtil(serverURL, slug)
const context = await browser.newContext() context = await browser.newContext()
page = await context.newPage() page = await context.newPage()
initPageConsoleErrorCatch(page) initPageConsoleErrorCatch(page)
@@ -107,7 +109,7 @@ describe('auth', () => {
}) })
await payload.create({ await payload.create({
collection: 'api-keys', collection: apiKeysSlug,
data: { data: {
apiKey: uuid(), apiKey: uuid(),
enableAPIKey: true, enableAPIKey: true,
@@ -115,7 +117,7 @@ describe('auth', () => {
}) })
await payload.create({ await payload.create({
collection: 'api-keys', collection: apiKeysSlug,
data: { data: {
apiKey: uuid(), apiKey: uuid(),
enableAPIKey: true, enableAPIKey: true,

View File

@@ -13,6 +13,7 @@ import {
apiKeysSlug, apiKeysSlug,
namedSaveToJWTValue, namedSaveToJWTValue,
partialDisableLocaleStrategiesSlug, partialDisableLocaleStrategiesSlug,
publicUsersSlug,
saveToJWTKey, saveToJWTKey,
slug, slug,
} from './shared.js' } from './shared.js'
@@ -276,7 +277,7 @@ describe('Auth', () => {
it('should allow verification of a user', async () => { it('should allow verification of a user', async () => {
const emailToVerify = 'verify@me.com' const emailToVerify = 'verify@me.com'
const response = await restClient.POST(`/public-users`, { const response = await restClient.POST(`/${publicUsersSlug}`, {
body: JSON.stringify({ body: JSON.stringify({
email: emailToVerify, email: emailToVerify,
password, password,
@@ -290,7 +291,7 @@ describe('Auth', () => {
expect(response.status).toBe(201) expect(response.status).toBe(201)
const userResult = await payload.find({ const userResult = await payload.find({
collection: 'public-users', collection: publicUsersSlug,
limit: 1, limit: 1,
showHiddenFields: true, showHiddenFields: true,
where: { where: {
@@ -306,13 +307,13 @@ describe('Auth', () => {
expect(_verificationToken).toBeDefined() expect(_verificationToken).toBeDefined()
const verificationResponse = await restClient.POST( const verificationResponse = await restClient.POST(
`/public-users/verify/${_verificationToken}`, `/${publicUsersSlug}/verify/${_verificationToken}`,
) )
expect(verificationResponse.status).toBe(200) expect(verificationResponse.status).toBe(200)
const afterVerifyResult = await payload.find({ const afterVerifyResult = await payload.find({
collection: 'public-users', collection: publicUsersSlug,
limit: 1, limit: 1,
showHiddenFields: true, showHiddenFields: true,
where: { where: {
@@ -782,24 +783,24 @@ describe('Auth', () => {
describe('API Key', () => { describe('API Key', () => {
it('should authenticate via the correct API key user', async () => { it('should authenticate via the correct API key user', async () => {
const usersQuery = await payload.find({ const usersQuery = await payload.find({
collection: 'api-keys', collection: apiKeysSlug,
}) })
const [user1, user2] = usersQuery.docs const [user1, user2] = usersQuery.docs
const success = await restClient const success = await restClient
.GET(`/api-keys/${user2.id}`, { .GET(`/${apiKeysSlug}/${user2.id}`, {
headers: { headers: {
Authorization: `api-keys API-Key ${user2.apiKey}`, Authorization: `${apiKeysSlug} API-Key ${user2.apiKey}`,
}, },
}) })
.then((res) => res.json()) .then((res) => res.json())
expect(success.apiKey).toStrictEqual(user2.apiKey) expect(success.apiKey).toStrictEqual(user2.apiKey)
const fail = await restClient.GET(`/api-keys/${user1.id}`, { const fail = await restClient.GET(`/${apiKeysSlug}/${user1.id}`, {
headers: { headers: {
Authorization: `api-keys API-Key ${user2.apiKey}`, Authorization: `${apiKeysSlug} API-Key ${user2.apiKey}`,
}, },
}) })
@@ -809,7 +810,7 @@ describe('Auth', () => {
it('should not remove an API key from a user when updating other fields', async () => { it('should not remove an API key from a user when updating other fields', async () => {
const apiKey = uuid() const apiKey = uuid()
const user = await payload.create({ const user = await payload.create({
collection: 'api-keys', collection: apiKeysSlug,
data: { data: {
apiKey, apiKey,
enableAPIKey: true, enableAPIKey: true,
@@ -818,14 +819,14 @@ describe('Auth', () => {
const updatedUser = await payload.update({ const updatedUser = await payload.update({
id: user.id, id: user.id,
collection: 'api-keys', collection: apiKeysSlug,
data: { data: {
enableAPIKey: true, enableAPIKey: true,
}, },
}) })
const userResult = await payload.find({ const userResult = await payload.find({
collection: 'api-keys', collection: apiKeysSlug,
where: { where: {
id: { id: {
equals: user.id, equals: user.id,
@@ -857,7 +858,7 @@ describe('Auth', () => {
// use the api key in a fetch to assert that it is disabled // use the api key in a fetch to assert that it is disabled
const response = await restClient const response = await restClient
.GET(`/api-keys/me`, { .GET(`/${apiKeysSlug}/me`, {
headers: { headers: {
Authorization: `${apiKeysSlug} API-Key ${apiKey}`, Authorization: `${apiKeysSlug} API-Key ${apiKey}`,
}, },
@@ -888,7 +889,7 @@ describe('Auth', () => {
// use the api key in a fetch to assert that it is disabled // use the api key in a fetch to assert that it is disabled
const response = await restClient const response = await restClient
.GET(`/api-keys/me`, { .GET(`/${apiKeysSlug}/me`, {
headers: { headers: {
Authorization: `${apiKeysSlug} API-Key ${apiKey}`, Authorization: `${apiKeysSlug} API-Key ${apiKey}`,
}, },

View File

@@ -1,4 +1,7 @@
export const slug = 'users' export const slug = 'users'
export const publicUsersSlug = 'public-users'
export const apiKeysSlug = 'api-keys' export const apiKeysSlug = 'api-keys'
export const partialDisableLocaleStrategiesSlug = 'partial-disable-locale-strategies' export const partialDisableLocaleStrategiesSlug = 'partial-disable-locale-strategies'

View File

@@ -28,6 +28,7 @@ type LoginArgs = {
page: Page page: Page
serverURL: string serverURL: string
} }
const random = (min: number, max: number) => Math.floor(Math.random() * (max - min + 1)) + min const random = (min: number, max: number) => Math.floor(Math.random() * (max - min + 1)) + min
const networkConditions = { const networkConditions = {

View File

@@ -1,9 +1,9 @@
import type { Config } from 'payload'
// IMPORTANT: ensure that imports do not contain React components, etc. as this breaks Playwright tests // IMPORTANT: ensure that imports do not contain React components, etc. as this breaks Playwright tests
// Instead of pointing to the bundled code, which will include React components, use direct import paths // Instead of pointing to the bundled code, which will include React components, use direct import paths
import { formatAdminURL } from '../../packages/ui/src/utilities/formatAdminURL.js' // eslint-disable-line payload/no-relative-monorepo-imports import { formatAdminURL } from '../../packages/ui/src/utilities/formatAdminURL.js' // eslint-disable-line payload/no-relative-monorepo-imports
import type { Config } from 'payload'
export class AdminUrlUtil { export class AdminUrlUtil {
account: string account: string
@@ -15,9 +15,14 @@ export class AdminUrlUtil {
list: string list: string
login: string
logout: string
routes: Config['routes'] routes: Config['routes']
serverURL: string serverURL: string
constructor(serverURL: string, slug: string, routes?: Config['routes']) { constructor(serverURL: string, slug: string, routes?: Config['routes']) {
this.routes = { this.routes = {
admin: routes?.admin || '/admin', admin: routes?.admin || '/admin',
@@ -39,6 +44,18 @@ export class AdminUrlUtil {
serverURL: this.serverURL, serverURL: this.serverURL,
}) })
this.login = formatAdminURL({
adminRoute: this.routes.admin,
path: '/login',
serverURL: this.serverURL,
})
this.logout = formatAdminURL({
adminRoute: this.routes.admin,
path: '/logout',
serverURL: this.serverURL,
})
this.list = formatAdminURL({ this.list = formatAdminURL({
adminRoute: this.routes.admin, adminRoute: this.routes.admin,
path: `/collections/${this.entitySlug}`, path: `/collections/${this.entitySlug}`,

View File

@@ -28,7 +28,7 @@
} }
], ],
"paths": { "paths": {
"@payload-config": ["./test/live-preview/config.ts"], "@payload-config": ["./test/access-control/config.ts"],
"@payloadcms/live-preview": ["./packages/live-preview/src"], "@payloadcms/live-preview": ["./packages/live-preview/src"],
"@payloadcms/live-preview-react": ["./packages/live-preview-react/src/index.ts"], "@payloadcms/live-preview-react": ["./packages/live-preview-react/src/index.ts"],
"@payloadcms/live-preview-vue": ["./packages/live-preview-vue/src/index.ts"], "@payloadcms/live-preview-vue": ["./packages/live-preview-vue/src/index.ts"],