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[] }
user?: User
}
export const handleAuthRedirect = ({ config, route, searchParams, user }: Args): string => {
const {
admin: {

View File

@@ -110,7 +110,7 @@ export const initPage = async ({
},
})
?.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)

View File

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

View File

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

View File

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

View File

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

View File

@@ -115,6 +115,7 @@ export const arTranslations: DefaultTranslationsObject = {
unableToReindexCollection: 'خطأ في إعادة فهرسة المجموعة {{collection}}. تم إيقاف العملية.',
unableToUpdateCount: 'يتعذّر تحديث {{count}} من {{total}} {{label}}.',
unauthorized: 'غير مصرّح لك ، عليك أن تقوم بتسجيل الدّخول لتتمكّن من تقديم هذا الطّلب.',
unauthorizedAdmin: 'غير مصرّح لك بالوصول إلى لوحة التحكّم.',
unknown: 'حدث خطأ غير معروف.',
unPublishingDocument: 'حدث خطأ أثناء إلغاء نشر هذا المستند.',
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ı.',
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.',
unauthorizedAdmin: 'Bu əməliyyatı yerinə yetirmək üçün admin olmalısınız.',
unknown: 'Naməlum bir xəta baş verdi.',
unPublishingDocument: 'Bu sənədin nəşrini ləğv etmək zamanı problem baş verdi.',
unspecific: 'Xəta baş verdi.',

View File

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

View File

@@ -116,6 +116,7 @@ export const csTranslations: DefaultTranslationsObject = {
'Chyba při přeindexování kolekce {{collection}}. Operace byla přerušena.',
unableToUpdateCount: 'Nelze aktualizovat {{count}} z {{total}} {{label}}.',
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ě.',
unPublishingDocument: 'Při zrušení publikování tohoto dokumentu 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.',
unableToUpdateCount: 'Kunne ikke slette {{count}} mangler {{total}} {{label}}.',
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.',
unPublishingDocument: 'Der opstod et problem med at ophæve udgivelsen af dette dokument.',
unspecific: 'En fejl er opstået.',

View File

@@ -118,6 +118,7 @@ export const deTranslations: DefaultTranslationsObject = {
'Fehler beim Neuindizieren der Sammlung {{collection}}. Vorgang abgebrochen.',
unableToUpdateCount: '{{count}} von {{total}} {{label}} konnte nicht aktualisiert werden.',
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.',
unPublishingDocument: 'Es gab ein Problem, dieses Dokument auf Entwurf zu setzen.',
unspecific: 'Ein Fehler ist aufgetreten.',

View File

@@ -116,6 +116,7 @@ export const enTranslations = {
unableToReindexCollection: 'Error reindexing collection {{collection}}. Operation aborted.',
unableToUpdateCount: 'Unable to update {{count}} out of {{total}} {{label}}.',
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.',
unPublishingDocument: 'There was a problem while un-publishing this document.',
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.',
unableToUpdateCount: 'No se puede actualizar {{count}} de {{total}} {{label}}.',
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.',
unPublishingDocument: 'Ocurrió un error al despublicar este documento.',
unspecific: 'Ocurrió un error.',

View File

@@ -114,6 +114,7 @@ export const faTranslations: DefaultTranslationsObject = {
unableToReindexCollection: 'خطا در بازنمایه‌سازی مجموعه {{collection}}. عملیات متوقف شد.',
unableToUpdateCount: 'امکان به روز رسانی {{count}} خارج از {{total}} {{label}} وجود ندارد.',
unauthorized: 'درخواست نامعتبر، جهت فرستادن این درخواست باید وارد شوید.',
unauthorizedAdmin: 'دسترسی به پیشخوان برای این کاربر مجاز نیست.',
unknown: 'یک خطای ناشناخته رخ داد.',
unPublishingDocument: 'هنگام لغو انتشار این سند خطایی رخ داد.',
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.',
unableToUpdateCount: 'Impossible de mettre à jour {{count}} sur {{total}} {{label}}.',
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.',
unPublishingDocument:
'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}}. הפעולה בוטלה.',
unableToUpdateCount: 'לא ניתן לעדכן {{count}} מתוך {{total}} {{label}}.',
unauthorized: 'אין הרשאה, עליך להתחבר כדי לבצע בקשה זו.',
unauthorizedAdmin: 'אין הרשאה, משתמש זה אינו יכול לגשת לפאנל הניהול.',
unknown: 'אירעה שגיאה לא ידועה.',
unPublishingDocument: 'אירעה בעיה בביטול הפרסום של מסמך זה.',
unspecific: 'אירעה שגיאה.',

View File

@@ -117,6 +117,7 @@ export const hrTranslations: DefaultTranslationsObject = {
'Pogreška pri ponovnom indeksiranju kolekcije {{collection}}. Operacija je prekinuta.',
unableToUpdateCount: 'Nije moguće ažurirati {{count}} od {{total}} {{label}}.',
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.',
unPublishingDocument: 'Došlo je do problema pri poništavanju objave ovog dokumenta.',
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.',
unableToUpdateCount: 'Nem sikerült frissíteni {{count}}/{{total}} {{label}}.',
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.',
unPublishingDocument: 'Hiba történt a dokumentum közzétételének visszavonása közben.',
unspecific: 'Hiba történt.',

View File

@@ -118,6 +118,8 @@ export const itTranslations: DefaultTranslationsObject = {
'Errore durante la reindicizzazione della collezione {{collection}}. Operazione annullata.',
unableToUpdateCount: 'Impossibile aggiornare {{count}} su {{total}} {{label}}.',
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.',
unPublishingDocument:
"Si è verificato un problema durante l'annullamento della pubblicazione di questo documento.",

View File

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

View File

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

View File

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

View File

@@ -116,6 +116,7 @@ export const nbTranslations: DefaultTranslationsObject = {
'Feil ved reindeksering av samlingen {{collection}}. Operasjonen ble avbrutt.',
unableToUpdateCount: 'Kan ikke oppdatere {{count}} av {{total}} {{label}}.',
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.',
unPublishingDocument: 'Det oppstod et problem under avpublisering av dokumentet.',
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.',
unableToUpdateCount: 'Kan {{count}} van {{total}} {{label}} niet updaten.',
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.',
unPublishingDocument: 'Er was een probleem met het depubliceren van dit document.',
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.',
unableToUpdateCount: 'Nie można zaktualizować {{count}} z {{total}} {{label}}.',
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.',
unPublishingDocument: 'Wystąpił problem podczas cofania publikacji tego dokumentu.',
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.',
unableToUpdateCount: 'Não foi possível atualizar {{count}} de {{total}} {{label}}.',
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.',
unPublishingDocument: 'Ocorreu um problema ao despublicar esse documento',
unspecific: 'Ocorreu um erro.',

View File

@@ -117,7 +117,8 @@ export const roTranslations: DefaultTranslationsObject = {
unableToReindexCollection:
'Eroare la reindexarea colecției {{collection}}. Operațiune anulată.',
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ă.',
unPublishingDocument: 'A existat o problemă în timpul nepublicării acestui document.',
unspecific: 'S-a produs o eroare.',

View File

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

View File

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

View File

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

View File

@@ -117,6 +117,8 @@ export const skTranslations: DefaultTranslationsObject = {
'Chyba pri reindexácii kolekcie {{collection}}. Operácia bola prerušená.',
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.',
unauthorizedAdmin:
'Neoprávnený prístup, tento používateľ nemá prístup k administrátorskému panelu.',
unknown: 'Došlo k neznámej chybe.',
unPublishingDocument: 'Pri zrušení publikovania tohto dokumentu 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.',
unableToUpdateCount: 'Ni bilo mogoče posodobiti {{count}} od {{total}} {{label}}.',
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.',
unPublishingDocument: 'Pri umiku objave tega dokumenta je prišlo do težave.',
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.',
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.',
unauthorizedAdmin: 'Obehörig, denna användare har inte åtkomst till adminpanelen.',
unknown: 'Ett okänt fel har uppstått.',
unPublishingDocument: 'Det uppstod ett problem när det här dokumentet skulle avpubliceras.',
unspecific: 'Ett fel har uppstått.',

View File

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

View File

@@ -117,6 +117,7 @@ export const trTranslations: DefaultTranslationsObject = {
'{{collection}} koleksiyonunun yeniden indekslenmesinde hata oluştu. İşlem durduruldu.',
unableToUpdateCount: '{{total}} {{label}} içinden {{count}} güncellenemiyor.',
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.',
unPublishingDocument: 'Geçerli döküman yayından kaldırılırken bir sorun oluştu.',
unspecific: 'Bir hata oluştu.',

View File

@@ -117,6 +117,7 @@ export const ukTranslations: DefaultTranslationsObject = {
'Помилка при повторному індексуванні колекції {{collection}}. Операцію скасовано.',
unableToUpdateCount: 'Не вдалося оновити {{count}} із {{total}} {{label}}.',
unauthorized: 'Немає доступу, ви повинні увійти, щоб виконати цей запит.',
unauthorizedAdmin: 'Немає доступу, цей користувач не має доступу до панелі адміністратора.',
unknown: 'Виникла невідома помилка.',
unPublishingDocument: 'Під час скасування публікації даного документа виникла помилка.',
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.',
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.',
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).',
unPublishingDocument: 'Lỗi - Đã xảy ra vấn để khi ẩn bản tài liệu.',
unspecific: 'Lỗi - Đã xảy ra (unspecific error).',

View File

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

View File

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

View File

@@ -196,7 +196,7 @@ export function AuthProvider({
const logOut = useCallback(async () => {
try {
await requests.post(`${serverURL}${apiRoute}/${userSlug}/logout`)
await requests.post(`${serverURL}${apiRoute}/${user.collection}/logout`)
setNewUser(null)
revokeTokenAndExpire()
return true
@@ -204,7 +204,7 @@ export function AuthProvider({
toast.error(`Logging out failed: ${e.message}`)
return false
}
}, [apiRoute, revokeTokenAndExpire, serverURL, setNewUser, userSlug])
}, [apiRoute, revokeTokenAndExpire, serverURL, setNewUser, user])
const refreshPermissions = useCallback(
async ({ locale }: { locale?: string } = {}) => {
@@ -309,7 +309,7 @@ export function AuthProvider({
clearTimeout(forceLogOut)
}
}
}, [tokenExpiration, openModal, i18n, setNewUser, user])
}, [tokenExpiration, openModal, i18n, setNewUser, user, redirectToInactivityRoute])
return (
<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 type { PayloadTestSDK } from '../helpers/sdk/index.js'
import type {
Config,
NonAdminUser,
ReadOnlyCollection,
RestrictedVersion,
} from './payload-types.js'
import type { Config, ReadOnlyCollection, RestrictedVersion } from './payload-types.js'
import {
closeNav,
@@ -34,9 +29,9 @@ import {
disabledSlug,
docLevelAccessSlug,
fullyRestrictedSlug,
noAdminAccessEmail,
nonAdminUserEmail,
nonAdminUserSlug,
nonAdminEmail,
publicUserEmail,
publicUsersSlug,
readNotUpdateGlobalSlug,
readOnlyGlobalSlug,
readOnlySlug,
@@ -610,56 +605,56 @@ describe('access control', () => {
})
describe('admin access', () => {
test('should block admin access to admin user', async () => {
const adminURL = `${serverURL}/admin`
await page.goto(adminURL)
await page.waitForURL(adminURL)
test('unauthenticated users should not have access to the admin panel', async () => {
await page.goto(url.logout)
await page.waitForURL(url.logout)
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 page.waitForURL(logoutURL)
await expect(page.locator('form.login__form')).toBeVisible()
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({
data: {
email: noAdminAccessEmail,
email: nonAdminEmail,
password: 'test',
},
page,
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(logoutURL)
await login({
data: {
email: devUser.email,
password: devUser.password,
},
page,
serverURL,
})
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()
})
test('should block admin access to non-admin user', async () => {
const adminURL = `${serverURL}/admin`
const unauthorizedURL = `${serverURL}/admin/unauthorized`
await page.goto(adminURL)
await page.waitForURL(adminURL)
test('public users should not have access to access admin', async () => {
await page.goto(url.logout)
await page.waitForURL(url.logout)
await expect(page.locator('.dashboard')).toBeVisible()
await page.goto(logoutURL)
await page.waitForURL(logoutURL)
const nonAdminUser: {
token?: string
} & NonAdminUser = await payload.login({
collection: nonAdminUserSlug,
const user = await payload.login({
collection: publicUsersSlug,
data: {
email: nonAdminUserEmail,
email: publicUserEmail,
password: devUser.password,
},
})
@@ -667,20 +662,36 @@ describe('access control', () => {
await context.addCookies([
{
name: 'payload-token',
url: serverURL,
value: nonAdminUser.token,
value: user.token,
domain: 'localhost',
path: '/',
httpOnly: true,
secure: true,
},
])
await page.goto(adminURL)
await page.waitForURL(unauthorizedURL)
await page.reload()
await expect(page.locator('.unauthorized')).toBeVisible()
await page.goto(url.admin)
await page.waitForURL(/unauthorized$/)
// Log back in for the next test
await context.clearCookies()
await page.goto(logoutURL)
await page.waitForURL(logoutURL)
await expect(page.locator('.unauthorized .form-header h1')).toHaveText(
'Unauthorized, this user does not have access to the admin panel.',
)
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({
data: {
email: devUser.email,
@@ -690,9 +701,7 @@ describe('access control', () => {
serverURL,
})
})
})
describe('read-only from access control', () => {
test('should be read-only when update returns false', async () => {
await page.goto(disabledFields.create)

View File

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

View File

@@ -18,11 +18,8 @@ export const docLevelAccessSlug = 'doc-level-access'
export const hiddenFieldsSlug = 'hidden-fields'
export const hiddenAccessSlug = 'hidden-access'
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 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,
namedSaveToJWTValue,
partialDisableLocaleStrategiesSlug,
publicUsersSlug,
saveToJWTKey,
slug,
} from './shared.js'
@@ -209,7 +210,7 @@ export default buildConfigWithDefaults({
if (!user) {
return false
}
if (user?.collection === 'api-keys') {
if (user?.collection === apiKeysSlug) {
return {
id: {
equals: user.id,
@@ -230,7 +231,7 @@ export default buildConfigWithDefaults({
},
},
{
slug: 'public-users',
slug: publicUsersSlug,
auth: {
verify: true,
},
@@ -263,7 +264,7 @@ export default buildConfigWithDefaults({
})
await payload.create({
collection: 'api-keys',
collection: apiKeysSlug,
data: {
apiKey: uuid(),
enableAPIKey: true,
@@ -271,7 +272,7 @@ export default buildConfigWithDefaults({
})
await payload.create({
collection: 'api-keys',
collection: apiKeysSlug,
data: {
apiKey: uuid(),
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 { expect, test } from '@playwright/test'
import { devUser } from 'credentials.js'
import path from 'path'
import { wait } from 'payload/shared'
import { fileURLToPath } from 'url'
import { v4 as uuid } from 'uuid'
@@ -83,6 +84,7 @@ const createFirstUser = async ({
describe('auth', () => {
let page: Page
let context: BrowserContext
let url: AdminUrlUtil
let serverURL: string
let apiURL: string
@@ -93,7 +95,7 @@ describe('auth', () => {
apiURL = `${serverURL}/api`
url = new AdminUrlUtil(serverURL, slug)
const context = await browser.newContext()
context = await browser.newContext()
page = await context.newPage()
initPageConsoleErrorCatch(page)
@@ -107,7 +109,7 @@ describe('auth', () => {
})
await payload.create({
collection: 'api-keys',
collection: apiKeysSlug,
data: {
apiKey: uuid(),
enableAPIKey: true,
@@ -115,7 +117,7 @@ describe('auth', () => {
})
await payload.create({
collection: 'api-keys',
collection: apiKeysSlug,
data: {
apiKey: uuid(),
enableAPIKey: true,

View File

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

View File

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

View File

@@ -28,6 +28,7 @@ type LoginArgs = {
page: Page
serverURL: string
}
const random = (min: number, max: number) => Math.floor(Math.random() * (max - min + 1)) + min
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
// 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 type { Config } from 'payload'
export class AdminUrlUtil {
account: string
@@ -15,9 +15,14 @@ export class AdminUrlUtil {
list: string
login: string
logout: string
routes: Config['routes']
serverURL: string
constructor(serverURL: string, slug: string, routes?: Config['routes']) {
this.routes = {
admin: routes?.admin || '/admin',
@@ -39,6 +44,18 @@ export class AdminUrlUtil {
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({
adminRoute: this.routes.admin,
path: `/collections/${this.entitySlug}`,

View File

@@ -28,7 +28,7 @@
}
],
"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-react": ["./packages/live-preview-react/src/index.ts"],
"@payloadcms/live-preview-vue": ["./packages/live-preview-vue/src/index.ts"],