fix: surface app-config load failure so SSO button is never silently hidden

When /api/auth/app-config fails (ZT redirect, network blip) the login page
now shows a warning banner with a Refresh button instead of silently omitting
the SSO sign-in button. The SW also now applies authRedirectPlugin to this
endpoint so ZT opaque redirects are converted to 401 AUTH_REQUIRED rather
than causing a JSON parse failure that went undetected.

Translations added for all 15 supported languages.
This commit is contained in:
jubnl
2026-05-05 16:27:53 +02:00
parent a1f4643b90
commit 81a59edf03
17 changed files with 59 additions and 5 deletions
+2
View File
@@ -464,6 +464,8 @@ const ar: Record<string, string | { name: string; category: string }[]> = {
'login.mfaVerify': 'تحقق',
'login.invalidInviteLink': 'رابط الدعوة غير صالح أو منتهي الصلاحية',
'login.oidcFailed': 'فشل تسجيل الدخول عبر OIDC',
'login.configLoadError': 'تعذّر تحميل خيارات تسجيل الدخول.',
'login.configLoadRetry': 'تحديث',
'login.usernameRequired': 'اسم المستخدم مطلوب',
'login.passwordMinLength': 'يجب أن تكون كلمة المرور 8 أحرف على الأقل',
'login.forgotPassword': 'نسيت كلمة المرور؟',
+2
View File
@@ -459,6 +459,8 @@ const br: Record<string, string | { name: string; category: string }[]> = {
'login.mfaVerify': 'Verificar',
'login.invalidInviteLink': 'Link de convite inválido ou expirado',
'login.oidcFailed': 'Falha no login OIDC',
'login.configLoadError': 'Não foi possível carregar as opções de login.',
'login.configLoadRetry': 'Atualizar',
'login.usernameRequired': 'Nome de usuário é obrigatório',
'login.passwordMinLength': 'A senha deve ter pelo menos 8 caracteres',
'login.forgotPassword': 'Esqueceu a senha?',
+2
View File
@@ -459,6 +459,8 @@ const cs: Record<string, string | { name: string; category: string }[]> = {
'login.mfaVerify': 'Ověřit',
'login.invalidInviteLink': 'Neplatný nebo vypršelý odkaz s pozvánkou',
'login.oidcFailed': 'Přihlášení přes OIDC se nezdařilo',
'login.configLoadError': 'Nepodařilo se načíst možnosti přihlášení.',
'login.configLoadRetry': 'Obnovit',
'login.usernameRequired': 'Uživatelské jméno je povinné',
'login.passwordMinLength': 'Heslo musí mít alespoň 8 znaků',
'login.forgotPassword': 'Zapomenuté heslo?',
+2
View File
@@ -464,6 +464,8 @@ const de: Record<string, string | { name: string; category: string }[]> = {
'login.mfaVerify': 'Bestätigen',
'login.invalidInviteLink': 'Ungültiger oder abgelaufener Einladungslink',
'login.oidcFailed': 'OIDC-Anmeldung fehlgeschlagen',
'login.configLoadError': 'Anmeldeoptionen konnten nicht geladen werden.',
'login.configLoadRetry': 'Aktualisieren',
'login.usernameRequired': 'Benutzername ist erforderlich',
'login.passwordMinLength': 'Das Passwort muss mindestens 8 Zeichen lang sein',
'login.forgotPassword': 'Passwort vergessen?',
+2
View File
@@ -537,6 +537,8 @@ const en: Record<string, string | { name: string; category: string }[]> = {
'login.mfaVerify': 'Verify',
'login.invalidInviteLink': 'Invalid or expired invite link',
'login.oidcFailed': 'OIDC login failed',
'login.configLoadError': 'Could not load login options.',
'login.configLoadRetry': 'Refresh',
'login.usernameRequired': 'Username is required',
'login.passwordMinLength': 'Password must be at least 8 characters',
'login.forgotPassword': 'Forgot password?',
+2
View File
@@ -451,6 +451,8 @@ const es: Record<string, string> = {
'login.mfaVerify': 'Verificar',
'login.invalidInviteLink': 'Enlace de invitación inválido o expirado',
'login.oidcFailed': 'Error de inicio de sesión OIDC',
'login.configLoadError': 'No se pudieron cargar las opciones de inicio de sesión.',
'login.configLoadRetry': 'Actualizar',
'login.usernameRequired': 'El nombre de usuario es obligatorio',
'login.passwordMinLength': 'La contraseña debe tener al menos 8 caracteres',
'login.forgotPassword': '¿Olvidaste tu contraseña?',
+2
View File
@@ -452,6 +452,8 @@ const fr: Record<string, string> = {
'login.mfaVerify': 'Vérifier',
'login.invalidInviteLink': 'Lien d\'invitation invalide ou expiré',
'login.oidcFailed': 'Échec de connexion OIDC',
'login.configLoadError': 'Impossible de charger les options de connexion.',
'login.configLoadRetry': 'Actualiser',
'login.usernameRequired': 'Le nom d\'utilisateur est obligatoire',
'login.passwordMinLength': 'Le mot de passe doit comporter au moins 8 caractères',
'login.forgotPassword': 'Mot de passe oublié ?',
+2
View File
@@ -459,6 +459,8 @@ const hu: Record<string, string | { name: string; category: string }[]> = {
'login.mfaVerify': 'Ellenőrzés',
'login.invalidInviteLink': 'Érvénytelen vagy lejárt meghívólink',
'login.oidcFailed': 'OIDC bejelentkezés sikertelen',
'login.configLoadError': 'A bejelentkezési lehetőségek betöltése nem sikerült.',
'login.configLoadRetry': 'Frissítés',
'login.usernameRequired': 'A felhasználónév kötelező',
'login.passwordMinLength': 'A jelszónak legalább 8 karakter hosszúnak kell lennie',
'login.forgotPassword': 'Elfelejtetted a jelszavad?',
+2
View File
@@ -521,6 +521,8 @@ const id: Record<string, string | { name: string; category: string }[]> = {
'login.mfaVerify': 'Verifikasi',
'login.invalidInviteLink': 'Tautan undangan tidak valid atau sudah kedaluwarsa',
'login.oidcFailed': 'Login OIDC gagal',
'login.configLoadError': 'Gagal memuat opsi login.',
'login.configLoadRetry': 'Segarkan',
'login.usernameRequired': 'Nama pengguna wajib diisi',
'login.passwordMinLength': 'Kata sandi minimal 8 karakter',
'login.forgotPassword': 'Lupa kata sandi?',
+2
View File
@@ -459,6 +459,8 @@ const it: Record<string, string | { name: string; category: string }[]> = {
'login.mfaVerify': 'Verifica',
'login.invalidInviteLink': 'Link di invito non valido o scaduto',
'login.oidcFailed': 'Accesso OIDC non riuscito',
'login.configLoadError': 'Impossibile caricare le opzioni di accesso.',
'login.configLoadRetry': 'Aggiorna',
'login.usernameRequired': 'Il nome utente è obbligatorio',
'login.passwordMinLength': 'La password deve contenere almeno 8 caratteri',
'login.forgotPassword': 'Password dimenticata?',
+2
View File
@@ -452,6 +452,8 @@ const nl: Record<string, string> = {
'login.mfaVerify': 'Verifiëren',
'login.invalidInviteLink': 'Ongeldige of verlopen uitnodigingslink',
'login.oidcFailed': 'OIDC-aanmelding mislukt',
'login.configLoadError': 'Kan aanmeldingsopties niet laden.',
'login.configLoadRetry': 'Vernieuwen',
'login.usernameRequired': 'Gebruikersnaam is vereist',
'login.passwordMinLength': 'Wachtwoord moet minimaal 8 tekens bevatten',
'login.forgotPassword': 'Wachtwoord vergeten?',
+2
View File
@@ -426,6 +426,8 @@ const pl: Record<string, string | { name: string; category: string }[]> = {
'login.mfaVerify': 'Weryfikuj',
'login.invalidInviteLink': 'Nieprawidłowy lub wygasły link zaproszenia',
'login.oidcFailed': 'Logowanie OIDC nie powiodło się',
'login.configLoadError': 'Nie można załadować opcji logowania.',
'login.configLoadRetry': 'Odśwież',
'login.usernameRequired': 'Nazwa użytkownika jest wymagana',
'login.passwordMinLength': 'Hasło musi mieć co najmniej 8 znaków',
'login.forgotPassword': 'Nie pamiętasz hasła?',
+2
View File
@@ -452,6 +452,8 @@ const ru: Record<string, string> = {
'login.mfaVerify': 'Подтвердить',
'login.invalidInviteLink': 'Недействительная или истёкшая ссылка-приглашение',
'login.oidcFailed': 'Ошибка входа через OIDC',
'login.configLoadError': 'Не удалось загрузить параметры входа.',
'login.configLoadRetry': 'Обновить',
'login.usernameRequired': 'Имя пользователя обязательно',
'login.passwordMinLength': 'Пароль должен содержать не менее 8 символов',
'login.forgotPassword': 'Забыли пароль?',
+2
View File
@@ -452,6 +452,8 @@ const zh: Record<string, string> = {
'login.mfaVerify': '验证',
'login.invalidInviteLink': '邀请链接无效或已过期',
'login.oidcFailed': 'OIDC 登录失败',
'login.configLoadError': '无法加载登录选项。',
'login.configLoadRetry': '刷新',
'login.usernameRequired': '用户名为必填项',
'login.passwordMinLength': '密码至少需要8个字符',
'login.forgotPassword': '忘记密码?',
+2
View File
@@ -511,6 +511,8 @@ const zhTw: Record<string, string> = {
'login.mfaVerify': '驗證',
'login.invalidInviteLink': '邀請連結無效或已過期',
'login.oidcFailed': 'OIDC 登入失敗',
'login.configLoadError': '無法載入登入選項。',
'login.configLoadRetry': '重新整理',
'login.usernameRequired': '使用者名稱為必填',
'login.passwordMinLength': '密碼至少需要8個字元',
'login.forgotPassword': '忘記密碼?',
+19 -4
View File
@@ -33,6 +33,7 @@ export default function LoginPage(): React.ReactElement {
const [isLoading, setIsLoading] = useState<boolean>(false)
const [error, setError] = useState<string>('')
const [appConfig, setAppConfig] = useState<AppConfig | null>(null)
const [configError, setConfigError] = useState<boolean>(false)
const [inviteToken, setInviteToken] = useState<string>('')
const [inviteValid, setInviteValid] = useState<boolean>(false)
const exchangeInitiated = useRef(false)
@@ -117,15 +118,15 @@ export default function LoginPage(): React.ReactElement {
return
}
authApi.getAppConfig?.().catch(() => null).then((config: AppConfig | null) => {
if (config) {
authApi.getAppConfig?.()
.then((config: AppConfig) => {
setAppConfig(config)
if (!config.has_users) setMode('register')
if (!config.password_login && config.oidc_login && config.oidc_configured && config.has_users && !invite && !noRedirect) {
window.location.href = '/api/auth/oidc/login'
}
}
})
})
.catch(() => setConfigError(true))
}, [navigate, t, noRedirect])
// Language detection chain (runs once on mount, only if user has no saved preference):
@@ -860,6 +861,20 @@ export default function LoginPage(): React.ReactElement {
</>
)}
{/* Config load error shown when /api/auth/app-config fails (e.g. ZT redirect,
network blip). Hides the SSO button; prompt user to refresh. */}
{configError && !appConfig && (
<div style={{ marginTop: 16, padding: '10px 14px', background: '#fef3c7', border: '1px solid #fde68a', borderRadius: 12, fontSize: 13, color: '#92400e', display: 'flex', alignItems: 'center', justifyContent: 'space-between', gap: 8 }}>
<span>{t('login.configLoadError')}</span>
<button
onClick={() => window.location.reload()}
style={{ background: 'none', border: '1px solid #d97706', borderRadius: 8, padding: '4px 10px', fontSize: 12, fontWeight: 600, color: '#92400e', cursor: 'pointer', fontFamily: 'inherit', whiteSpace: 'nowrap' }}
>
{t('login.configLoadRetry')}
</button>
</div>
)}
{/* Demo login button */}
{appConfig?.demo_mode && (
<button onClick={handleDemoLogin} disabled={isLoading}
+10 -1
View File
@@ -7,7 +7,7 @@ import {
matchPrecache,
} from 'workbox-precaching';
import { registerRoute, NavigationRoute } from 'workbox-routing';
import { NetworkFirst, CacheFirst } from 'workbox-strategies';
import { NetworkFirst, CacheFirst, NetworkOnly } from 'workbox-strategies';
import { ExpirationPlugin } from 'workbox-expiration';
import { CacheableResponsePlugin } from 'workbox-cacheable-response';
import {
@@ -135,6 +135,15 @@ function applyConfig(cfg: SwCacheConfig): void {
osmStrategy = buildTilesStrategy(cfg);
}
// Apply authRedirectPlugin to the public app-config endpoint so a ZT redirect
// surfaces as AUTH_REQUIRED (401) instead of causing a silent JSON parse failure
// on the login page, which would hide the SSO button.
registerRoute(
/\/api\/auth\/app-config$/i,
new NetworkOnly({ plugins: [authRedirectPlugin] }),
'GET',
);
// eslint-disable-next-line @typescript-eslint/no-explicit-any
registerRoute(/\/api\/(?!auth|admin|backup|settings).*/i, { handle: (o: any) => apiStrategy.handle(o) }, 'GET');
// eslint-disable-next-line @typescript-eslint/no-explicit-any