fix(oidc): resolve login/logout loop in OIDC-only mode
Three distinct bugs caused infinite OIDC redirect loops:
1. After logout, navigating to /login with no signal to suppress the
auto-redirect caused the login page to immediately re-trigger the
OIDC flow. Fixed by passing `{ state: { noRedirect: true } }` via
React Router's navigation state (not URL params, which were fragile
due to async cleanup timing) from all logout call sites.
2. On the OIDC callback page (/login?oidc_code=...), App.tsx's
mount-level loadUser() fired concurrently with the LoginPage's
exchange fetch. The App-level call had no cookie yet and got a 401,
which (if it resolved after the successful exchange loadUser()) would
overwrite isAuthenticated back to false. Fixed by skipping loadUser()
in App.tsx when the initial path is /login.
3. React 18 StrictMode double-invokes useEffect. The first run called
window.history.replaceState to clean the oidc_code from the URL
before starting the async exchange, so the second run saw no
oidc_code and fell through to the getAppConfig auto-redirect, firing
window.location.href = '/api/auth/oidc/login' before the exchange
could complete. Fixed by adding a useRef guard to prevent
double-execution and moving replaceState into the fetch callbacks so
the URL is only cleaned after the exchange resolves.
Also adds login.oidcLoggedOut translation key in all 14 languages to
show "You have been logged out" instead of the generic OIDC-only
message when landing on /login after an intentional logout.
Closes #491
This commit is contained in:
@@ -82,7 +82,7 @@ export default function App() {
|
|||||||
const { loadSettings } = useSettingsStore()
|
const { loadSettings } = useSettingsStore()
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!location.pathname.startsWith('/shared/')) {
|
if (!location.pathname.startsWith('/shared/') && !location.pathname.startsWith('/login')) {
|
||||||
loadUser()
|
loadUser()
|
||||||
}
|
}
|
||||||
authApi.getAppConfig().then(async (config: { demo_mode?: boolean; dev_mode?: boolean; has_maps_key?: boolean; version?: string; timezone?: string; require_mfa?: boolean; trip_reminders_enabled?: boolean; permissions?: Record<string, PermissionLevel> }) => {
|
authApi.getAppConfig().then(async (config: { demo_mode?: boolean; dev_mode?: boolean; has_maps_key?: boolean; version?: string; timezone?: string; require_mfa?: boolean; trip_reminders_enabled?: boolean; permissions?: Record<string, PermissionLevel> }) => {
|
||||||
|
|||||||
@@ -53,7 +53,7 @@ export default function Navbar({ tripTitle, tripId, onBack, showBack, onShare }:
|
|||||||
|
|
||||||
const handleLogout = () => {
|
const handleLogout = () => {
|
||||||
logout()
|
logout()
|
||||||
navigate('/login')
|
navigate('/login', { state: { noRedirect: true } })
|
||||||
}
|
}
|
||||||
|
|
||||||
const toggleDarkMode = () => {
|
const toggleDarkMode = () => {
|
||||||
|
|||||||
@@ -575,7 +575,7 @@ export default function AccountTab(): React.ReactElement {
|
|||||||
try {
|
try {
|
||||||
await authApi.deleteOwnAccount()
|
await authApi.deleteOwnAccount()
|
||||||
logout()
|
logout()
|
||||||
navigate('/login')
|
navigate('/login', { state: { noRedirect: true } })
|
||||||
} catch (err: unknown) {
|
} catch (err: unknown) {
|
||||||
toast.error(getApiErrorMessage(err, t('common.error')))
|
toast.error(getApiErrorMessage(err, t('common.error')))
|
||||||
setShowDeleteConfirm(false)
|
setShowDeleteConfirm(false)
|
||||||
|
|||||||
@@ -367,6 +367,7 @@ const ar: Record<string, string | { name: string; category: string }[]> = {
|
|||||||
'login.demoFailed': 'فشل الدخول إلى العرض التجريبي',
|
'login.demoFailed': 'فشل الدخول إلى العرض التجريبي',
|
||||||
'login.oidcSignIn': 'تسجيل الدخول عبر {name}',
|
'login.oidcSignIn': 'تسجيل الدخول عبر {name}',
|
||||||
'login.oidcOnly': 'تم تعطيل المصادقة بكلمة المرور. يرجى تسجيل الدخول عبر مزود SSO.',
|
'login.oidcOnly': 'تم تعطيل المصادقة بكلمة المرور. يرجى تسجيل الدخول عبر مزود SSO.',
|
||||||
|
'login.oidcLoggedOut': 'تم تسجيل خروجك. سجّل الدخول مجدداً عبر مزود SSO.',
|
||||||
'login.demoHint': 'جرّب العرض التجريبي دون الحاجة للتسجيل',
|
'login.demoHint': 'جرّب العرض التجريبي دون الحاجة للتسجيل',
|
||||||
'login.mfaTitle': 'المصادقة الثنائية',
|
'login.mfaTitle': 'المصادقة الثنائية',
|
||||||
'login.mfaSubtitle': 'أدخل الرمز المكون من 6 أرقام من تطبيق المصادقة.',
|
'login.mfaSubtitle': 'أدخل الرمز المكون من 6 أرقام من تطبيق المصادقة.',
|
||||||
|
|||||||
@@ -362,6 +362,7 @@ const br: Record<string, string | { name: string; category: string }[]> = {
|
|||||||
'login.demoFailed': 'Falha no login de demonstração',
|
'login.demoFailed': 'Falha no login de demonstração',
|
||||||
'login.oidcSignIn': 'Entrar com {name}',
|
'login.oidcSignIn': 'Entrar com {name}',
|
||||||
'login.oidcOnly': 'Login por senha desativado. Use o provedor SSO.',
|
'login.oidcOnly': 'Login por senha desativado. Use o provedor SSO.',
|
||||||
|
'login.oidcLoggedOut': 'Você foi desconectado. Entre novamente usando o provedor SSO.',
|
||||||
'login.demoHint': 'Experimente a demonstração — sem cadastro',
|
'login.demoHint': 'Experimente a demonstração — sem cadastro',
|
||||||
'login.mfaTitle': 'Autenticação em duas etapas',
|
'login.mfaTitle': 'Autenticação em duas etapas',
|
||||||
'login.mfaSubtitle': 'Digite o código de 6 dígitos do seu app autenticador.',
|
'login.mfaSubtitle': 'Digite o código de 6 dígitos do seu app autenticador.',
|
||||||
|
|||||||
@@ -362,6 +362,7 @@ const cs: Record<string, string | { name: string; category: string }[]> = {
|
|||||||
'login.demoFailed': 'Přihlášení do dema se nezdařilo',
|
'login.demoFailed': 'Přihlášení do dema se nezdařilo',
|
||||||
'login.oidcSignIn': 'Přihlásit se přes {name}',
|
'login.oidcSignIn': 'Přihlásit se přes {name}',
|
||||||
'login.oidcOnly': 'Ověřování heslem je zakázáno. Přihlaste se prosím přes SSO poskytovatele.',
|
'login.oidcOnly': 'Ověřování heslem je zakázáno. Přihlaste se prosím přes SSO poskytovatele.',
|
||||||
|
'login.oidcLoggedOut': 'Byl jste odhlášen. Přihlaste se znovu přes SSO poskytovatele.',
|
||||||
'login.demoHint': 'Vyzkoušejte demo – registrace není nutná',
|
'login.demoHint': 'Vyzkoušejte demo – registrace není nutná',
|
||||||
'login.mfaTitle': 'Dvoufaktorové ověření',
|
'login.mfaTitle': 'Dvoufaktorové ověření',
|
||||||
'login.mfaSubtitle': 'Zadejte 6místný kód z vaší autentizační aplikace.',
|
'login.mfaSubtitle': 'Zadejte 6místný kód z vaší autentizační aplikace.',
|
||||||
|
|||||||
@@ -362,6 +362,7 @@ const de: Record<string, string | { name: string; category: string }[]> = {
|
|||||||
'login.demoFailed': 'Demo-Login fehlgeschlagen',
|
'login.demoFailed': 'Demo-Login fehlgeschlagen',
|
||||||
'login.oidcSignIn': 'Anmelden mit {name}',
|
'login.oidcSignIn': 'Anmelden mit {name}',
|
||||||
'login.oidcOnly': 'Passwort-Authentifizierung ist deaktiviert. Bitte melde dich über deinen SSO-Anbieter an.',
|
'login.oidcOnly': 'Passwort-Authentifizierung ist deaktiviert. Bitte melde dich über deinen SSO-Anbieter an.',
|
||||||
|
'login.oidcLoggedOut': 'Du wurdest abgemeldet. Melde dich erneut über deinen SSO-Anbieter an.',
|
||||||
'login.demoHint': 'Demo ausprobieren — ohne Registrierung',
|
'login.demoHint': 'Demo ausprobieren — ohne Registrierung',
|
||||||
'login.mfaTitle': 'Zwei-Faktor-Authentifizierung',
|
'login.mfaTitle': 'Zwei-Faktor-Authentifizierung',
|
||||||
'login.mfaSubtitle': 'Gib den 6-stelligen Code aus deiner Authenticator-App ein.',
|
'login.mfaSubtitle': 'Gib den 6-stelligen Code aus deiner Authenticator-App ein.',
|
||||||
|
|||||||
@@ -383,6 +383,7 @@ const en: Record<string, string | { name: string; category: string }[]> = {
|
|||||||
'login.demoFailed': 'Demo login failed',
|
'login.demoFailed': 'Demo login failed',
|
||||||
'login.oidcSignIn': 'Sign in with {name}',
|
'login.oidcSignIn': 'Sign in with {name}',
|
||||||
'login.oidcOnly': 'Password authentication is disabled. Please sign in using your SSO provider.',
|
'login.oidcOnly': 'Password authentication is disabled. Please sign in using your SSO provider.',
|
||||||
|
'login.oidcLoggedOut': 'You have been logged out. Sign in again using your SSO provider.',
|
||||||
'login.demoHint': 'Try the demo — no registration needed',
|
'login.demoHint': 'Try the demo — no registration needed',
|
||||||
'login.mfaTitle': 'Two-factor authentication',
|
'login.mfaTitle': 'Two-factor authentication',
|
||||||
'login.mfaSubtitle': 'Enter the 6-digit code from your authenticator app.',
|
'login.mfaSubtitle': 'Enter the 6-digit code from your authenticator app.',
|
||||||
|
|||||||
@@ -1490,6 +1490,7 @@ const es: Record<string, string> = {
|
|||||||
'admin.oidcOnlyMode': 'Desactivar autenticación por contraseña',
|
'admin.oidcOnlyMode': 'Desactivar autenticación por contraseña',
|
||||||
'admin.oidcOnlyModeHint': 'Si está activado, solo se permite el inicio de sesión con SSO. El inicio de sesión y registro con contraseña se bloquean.',
|
'admin.oidcOnlyModeHint': 'Si está activado, solo se permite el inicio de sesión con SSO. El inicio de sesión y registro con contraseña se bloquean.',
|
||||||
'login.oidcOnly': 'La autenticación por contraseña está desactivada. Por favor, inicia sesión con tu proveedor SSO.',
|
'login.oidcOnly': 'La autenticación por contraseña está desactivada. Por favor, inicia sesión con tu proveedor SSO.',
|
||||||
|
'login.oidcLoggedOut': 'Has cerrado sesión. Vuelve a iniciar sesión con tu proveedor SSO.',
|
||||||
|
|
||||||
// Settings (2.6.2)
|
// Settings (2.6.2)
|
||||||
'settings.currentPasswordRequired': 'La contraseña actual es obligatoria',
|
'settings.currentPasswordRequired': 'La contraseña actual es obligatoria',
|
||||||
|
|||||||
@@ -369,6 +369,7 @@ const fr: Record<string, string> = {
|
|||||||
'login.demoFailed': 'Échec de la connexion démo',
|
'login.demoFailed': 'Échec de la connexion démo',
|
||||||
'login.oidcSignIn': 'Se connecter avec {name}',
|
'login.oidcSignIn': 'Se connecter avec {name}',
|
||||||
'login.oidcOnly': 'L\'authentification par mot de passe est désactivée. Veuillez vous connecter via votre fournisseur SSO.',
|
'login.oidcOnly': 'L\'authentification par mot de passe est désactivée. Veuillez vous connecter via votre fournisseur SSO.',
|
||||||
|
'login.oidcLoggedOut': 'Vous avez été déconnecté. Reconnectez-vous via votre fournisseur SSO.',
|
||||||
'login.demoHint': 'Essayez la démo — aucune inscription nécessaire',
|
'login.demoHint': 'Essayez la démo — aucune inscription nécessaire',
|
||||||
|
|
||||||
// Register
|
// Register
|
||||||
|
|||||||
@@ -362,6 +362,7 @@ const hu: Record<string, string | { name: string; category: string }[]> = {
|
|||||||
'login.demoFailed': 'Demo bejelentkezés sikertelen',
|
'login.demoFailed': 'Demo bejelentkezés sikertelen',
|
||||||
'login.oidcSignIn': 'Bejelentkezés ezzel: {name}',
|
'login.oidcSignIn': 'Bejelentkezés ezzel: {name}',
|
||||||
'login.oidcOnly': 'A jelszavas hitelesítés le van tiltva. Kérjük, jelentkezz be az SSO szolgáltatódon keresztül.',
|
'login.oidcOnly': 'A jelszavas hitelesítés le van tiltva. Kérjük, jelentkezz be az SSO szolgáltatódon keresztül.',
|
||||||
|
'login.oidcLoggedOut': 'Kijelentkeztél. Jelentkezz be újra az SSO szolgáltatódon keresztül.',
|
||||||
'login.demoHint': 'Próbáld ki a demót — regisztráció nélkül',
|
'login.demoHint': 'Próbáld ki a demót — regisztráció nélkül',
|
||||||
'login.mfaTitle': 'Kétfaktoros hitelesítés',
|
'login.mfaTitle': 'Kétfaktoros hitelesítés',
|
||||||
'login.mfaSubtitle': 'Add meg a 6 jegyű kódot a hitelesítő alkalmazásból.',
|
'login.mfaSubtitle': 'Add meg a 6 jegyű kódot a hitelesítő alkalmazásból.',
|
||||||
|
|||||||
@@ -362,6 +362,7 @@ const it: Record<string, string | { name: string; category: string }[]> = {
|
|||||||
'login.demoFailed': 'Accesso demo fallito',
|
'login.demoFailed': 'Accesso demo fallito',
|
||||||
'login.oidcSignIn': 'Accedi con {name}',
|
'login.oidcSignIn': 'Accedi con {name}',
|
||||||
'login.oidcOnly': 'L\'autenticazione tramite password è disabilitata. Accedi utilizzando il tuo provider SSO.',
|
'login.oidcOnly': 'L\'autenticazione tramite password è disabilitata. Accedi utilizzando il tuo provider SSO.',
|
||||||
|
'login.oidcLoggedOut': 'Sei stato disconnesso. Accedi nuovamente tramite il tuo provider SSO.',
|
||||||
'login.demoHint': 'Prova la demo — nessuna registrazione necessaria',
|
'login.demoHint': 'Prova la demo — nessuna registrazione necessaria',
|
||||||
'login.mfaTitle': 'Autenticazione a due fattori',
|
'login.mfaTitle': 'Autenticazione a due fattori',
|
||||||
'login.mfaSubtitle': 'Inserisci il codice a 6 cifre dalla tua app authenticator.',
|
'login.mfaSubtitle': 'Inserisci il codice a 6 cifre dalla tua app authenticator.',
|
||||||
|
|||||||
@@ -369,6 +369,7 @@ const nl: Record<string, string> = {
|
|||||||
'login.demoFailed': 'Demo-login mislukt',
|
'login.demoFailed': 'Demo-login mislukt',
|
||||||
'login.oidcSignIn': 'Inloggen met {name}',
|
'login.oidcSignIn': 'Inloggen met {name}',
|
||||||
'login.oidcOnly': 'Wachtwoordauthenticatie is uitgeschakeld. Log in via je SSO-provider.',
|
'login.oidcOnly': 'Wachtwoordauthenticatie is uitgeschakeld. Log in via je SSO-provider.',
|
||||||
|
'login.oidcLoggedOut': 'Je bent uitgelogd. Log opnieuw in via je SSO-provider.',
|
||||||
'login.demoHint': 'Probeer de demo — geen registratie nodig',
|
'login.demoHint': 'Probeer de demo — geen registratie nodig',
|
||||||
|
|
||||||
// Register
|
// Register
|
||||||
|
|||||||
@@ -329,6 +329,7 @@ const pl: Record<string, string | { name: string; category: string }[]> = {
|
|||||||
'login.demoFailed': 'Nie udało się zalogować do wersji demonstracyjnej',
|
'login.demoFailed': 'Nie udało się zalogować do wersji demonstracyjnej',
|
||||||
'login.oidcSignIn': 'Zaloguj się z {name}',
|
'login.oidcSignIn': 'Zaloguj się z {name}',
|
||||||
'login.oidcOnly': 'Uwierzytelnianie hasłem jest wyłączone. Zaloguj się za pomocą swojego dostawcy SSO.',
|
'login.oidcOnly': 'Uwierzytelnianie hasłem jest wyłączone. Zaloguj się za pomocą swojego dostawcy SSO.',
|
||||||
|
'login.oidcLoggedOut': 'Zostałeś wylogowany. Zaloguj się ponownie za pomocą swojego dostawcy SSO.',
|
||||||
'login.demoHint': 'Wypróbuj demo — nie wymaga rejestracji',
|
'login.demoHint': 'Wypróbuj demo — nie wymaga rejestracji',
|
||||||
'login.mfaTitle': 'Uwierzytelnianie dwuskładnikowe',
|
'login.mfaTitle': 'Uwierzytelnianie dwuskładnikowe',
|
||||||
'login.mfaSubtitle': 'Wprowadź 6-cyfrowy kod z aplikacji uwierzytelniającej.',
|
'login.mfaSubtitle': 'Wprowadź 6-cyfrowy kod z aplikacji uwierzytelniającej.',
|
||||||
|
|||||||
@@ -369,6 +369,7 @@ const ru: Record<string, string> = {
|
|||||||
'login.demoFailed': 'Ошибка демо-входа',
|
'login.demoFailed': 'Ошибка демо-входа',
|
||||||
'login.oidcSignIn': 'Войти через {name}',
|
'login.oidcSignIn': 'Войти через {name}',
|
||||||
'login.oidcOnly': 'Вход по паролю отключён. Используйте вашего провайдера SSO для входа.',
|
'login.oidcOnly': 'Вход по паролю отключён. Используйте вашего провайдера SSO для входа.',
|
||||||
|
'login.oidcLoggedOut': 'Вы вышли из системы. Войдите снова через вашего провайдера SSO.',
|
||||||
'login.demoHint': 'Попробуйте демо — регистрация не требуется',
|
'login.demoHint': 'Попробуйте демо — регистрация не требуется',
|
||||||
|
|
||||||
// Register
|
// Register
|
||||||
|
|||||||
@@ -369,6 +369,7 @@ const zh: Record<string, string> = {
|
|||||||
'login.demoFailed': '演示登录失败',
|
'login.demoFailed': '演示登录失败',
|
||||||
'login.oidcSignIn': '通过 {name} 登录',
|
'login.oidcSignIn': '通过 {name} 登录',
|
||||||
'login.oidcOnly': '密码登录已关闭。请通过 SSO 提供商登录。',
|
'login.oidcOnly': '密码登录已关闭。请通过 SSO 提供商登录。',
|
||||||
|
'login.oidcLoggedOut': '您已退出登录。请重新通过 SSO 提供商登录。',
|
||||||
'login.demoHint': '试用演示——无需注册',
|
'login.demoHint': '试用演示——无需注册',
|
||||||
|
|
||||||
// Register
|
// Register
|
||||||
|
|||||||
@@ -353,6 +353,7 @@ const zhTw: Record<string, string> = {
|
|||||||
'login.demoFailed': '演示登入失敗',
|
'login.demoFailed': '演示登入失敗',
|
||||||
'login.oidcSignIn': '透過 {name} 登入',
|
'login.oidcSignIn': '透過 {name} 登入',
|
||||||
'login.oidcOnly': '密碼登入已關閉。請透過 SSO 提供商登入。',
|
'login.oidcOnly': '密碼登入已關閉。請透過 SSO 提供商登入。',
|
||||||
|
'login.oidcLoggedOut': '您已登出。請重新透過 SSO 提供商登入。',
|
||||||
'login.demoHint': '試用演示——無需註冊',
|
'login.demoHint': '試用演示——無需註冊',
|
||||||
|
|
||||||
// Register
|
// Register
|
||||||
|
|||||||
@@ -1551,7 +1551,7 @@ docker run -d --name trek \\
|
|||||||
await adminApi.rotateJwtSecret()
|
await adminApi.rotateJwtSecret()
|
||||||
setShowRotateJwtModal(false)
|
setShowRotateJwtModal(false)
|
||||||
logout()
|
logout()
|
||||||
navigate('/login')
|
navigate('/login', { state: { noRedirect: true } })
|
||||||
} catch {
|
} catch {
|
||||||
toast.error(t('common.error'))
|
toast.error(t('common.error'))
|
||||||
setRotatingJwt(false)
|
setRotatingJwt(false)
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import React, { useState, useEffect, useMemo } from 'react'
|
import React, { useState, useEffect, useMemo, useRef } from 'react'
|
||||||
import { useNavigate } from 'react-router-dom'
|
import { useNavigate, useLocation } from 'react-router-dom'
|
||||||
import { useAuthStore } from '../store/authStore'
|
import { useAuthStore } from '../store/authStore'
|
||||||
import { useSettingsStore } from '../store/settingsStore'
|
import { useSettingsStore } from '../store/settingsStore'
|
||||||
import { SUPPORTED_LANGUAGES, useTranslation } from '../i18n'
|
import { SUPPORTED_LANGUAGES, useTranslation } from '../i18n'
|
||||||
@@ -29,10 +29,13 @@ export default function LoginPage(): React.ReactElement {
|
|||||||
const [appConfig, setAppConfig] = useState<AppConfig | null>(null)
|
const [appConfig, setAppConfig] = useState<AppConfig | null>(null)
|
||||||
const [inviteToken, setInviteToken] = useState<string>('')
|
const [inviteToken, setInviteToken] = useState<string>('')
|
||||||
const [inviteValid, setInviteValid] = useState<boolean>(false)
|
const [inviteValid, setInviteValid] = useState<boolean>(false)
|
||||||
|
const exchangeInitiated = useRef(false)
|
||||||
|
|
||||||
const { login, register, demoLogin, completeMfaLogin, loadUser } = useAuthStore()
|
const { login, register, demoLogin, completeMfaLogin, loadUser } = useAuthStore()
|
||||||
const { setLanguageLocal } = useSettingsStore()
|
const { setLanguageLocal } = useSettingsStore()
|
||||||
const navigate = useNavigate()
|
const navigate = useNavigate()
|
||||||
|
const location = useLocation()
|
||||||
|
const noRedirect = !!(location.state as { noRedirect?: boolean } | null)?.noRedirect
|
||||||
|
|
||||||
const redirectTarget = useMemo(() => {
|
const redirectTarget = useMemo(() => {
|
||||||
const params = new URLSearchParams(window.location.search)
|
const params = new URLSearchParams(window.location.search)
|
||||||
@@ -63,11 +66,13 @@ export default function LoginPage(): React.ReactElement {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (oidcCode) {
|
if (oidcCode) {
|
||||||
|
if (exchangeInitiated.current) return
|
||||||
|
exchangeInitiated.current = true
|
||||||
setIsLoading(true)
|
setIsLoading(true)
|
||||||
window.history.replaceState({}, '', '/login')
|
|
||||||
fetch('/api/auth/oidc/exchange?code=' + encodeURIComponent(oidcCode), { credentials: 'include' })
|
fetch('/api/auth/oidc/exchange?code=' + encodeURIComponent(oidcCode), { credentials: 'include' })
|
||||||
.then(r => r.json())
|
.then(r => r.json())
|
||||||
.then(async data => {
|
.then(async data => {
|
||||||
|
window.history.replaceState({}, '', '/login')
|
||||||
if (data.token) {
|
if (data.token) {
|
||||||
await loadUser()
|
await loadUser()
|
||||||
navigate('/dashboard', { replace: true })
|
navigate('/dashboard', { replace: true })
|
||||||
@@ -75,7 +80,10 @@ export default function LoginPage(): React.ReactElement {
|
|||||||
setError(data.error || 'OIDC login failed')
|
setError(data.error || 'OIDC login failed')
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.catch(() => setError('OIDC login failed'))
|
.catch(() => {
|
||||||
|
window.history.replaceState({}, '', '/login')
|
||||||
|
setError('OIDC login failed')
|
||||||
|
})
|
||||||
.finally(() => setIsLoading(false))
|
.finally(() => setIsLoading(false))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -96,12 +104,12 @@ export default function LoginPage(): React.ReactElement {
|
|||||||
if (config) {
|
if (config) {
|
||||||
setAppConfig(config)
|
setAppConfig(config)
|
||||||
if (!config.has_users) setMode('register')
|
if (!config.has_users) setMode('register')
|
||||||
if (config.oidc_only_mode && config.oidc_configured && config.has_users && !invite) {
|
if (config.oidc_only_mode && config.oidc_configured && config.has_users && !invite && !noRedirect) {
|
||||||
window.location.href = '/api/auth/oidc/login'
|
window.location.href = '/api/auth/oidc/login'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}, [navigate, t])
|
}, [navigate, t, noRedirect])
|
||||||
|
|
||||||
const handleDemoLogin = async (): Promise<void> => {
|
const handleDemoLogin = async (): Promise<void> => {
|
||||||
setError('')
|
setError('')
|
||||||
@@ -527,7 +535,7 @@ export default function LoginPage(): React.ReactElement {
|
|||||||
{oidcOnly ? (
|
{oidcOnly ? (
|
||||||
<>
|
<>
|
||||||
<h2 style={{ margin: '0 0 4px', fontSize: 22, fontWeight: 800, color: '#111827' }}>{t('login.title')}</h2>
|
<h2 style={{ margin: '0 0 4px', fontSize: 22, fontWeight: 800, color: '#111827' }}>{t('login.title')}</h2>
|
||||||
<p style={{ margin: '0 0 24px', fontSize: 13.5, color: '#9ca3af' }}>{t('login.oidcOnly')}</p>
|
<p style={{ margin: '0 0 24px', fontSize: 13.5, color: '#9ca3af' }}>{noRedirect ? t('login.oidcLoggedOut') : t('login.oidcOnly')}</p>
|
||||||
{error && (
|
{error && (
|
||||||
<div style={{ padding: '10px 14px', background: '#fef2f2', border: '1px solid #fecaca', borderRadius: 10, fontSize: 13, color: '#dc2626', marginBottom: 16 }}>
|
<div style={{ padding: '10px 14px', background: '#fef2f2', border: '1px solid #fecaca', borderRadius: 10, fontSize: 13, color: '#dc2626', marginBottom: 16 }}>
|
||||||
{error}
|
{error}
|
||||||
|
|||||||
Reference in New Issue
Block a user