diff --git a/client/src/App.tsx b/client/src/App.tsx index 621201e..0ca00b6 100644 --- a/client/src/App.tsx +++ b/client/src/App.tsx @@ -82,7 +82,7 @@ export default function App() { const { loadSettings } = useSettingsStore() useEffect(() => { - if (!location.pathname.startsWith('/shared/')) { + if (!location.pathname.startsWith('/shared/') && !location.pathname.startsWith('/login')) { 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 }) => { diff --git a/client/src/components/Layout/Navbar.tsx b/client/src/components/Layout/Navbar.tsx index e4e1dc9..cee59b8 100644 --- a/client/src/components/Layout/Navbar.tsx +++ b/client/src/components/Layout/Navbar.tsx @@ -53,7 +53,7 @@ export default function Navbar({ tripTitle, tripId, onBack, showBack, onShare }: const handleLogout = () => { logout() - navigate('/login') + navigate('/login', { state: { noRedirect: true } }) } const toggleDarkMode = () => { diff --git a/client/src/components/Settings/AccountTab.tsx b/client/src/components/Settings/AccountTab.tsx index 81bf491..1c9abfd 100644 --- a/client/src/components/Settings/AccountTab.tsx +++ b/client/src/components/Settings/AccountTab.tsx @@ -575,7 +575,7 @@ export default function AccountTab(): React.ReactElement { try { await authApi.deleteOwnAccount() logout() - navigate('/login') + navigate('/login', { state: { noRedirect: true } }) } catch (err: unknown) { toast.error(getApiErrorMessage(err, t('common.error'))) setShowDeleteConfirm(false) diff --git a/client/src/i18n/translations/ar.ts b/client/src/i18n/translations/ar.ts index 43f29ee..cdcac56 100644 --- a/client/src/i18n/translations/ar.ts +++ b/client/src/i18n/translations/ar.ts @@ -367,6 +367,7 @@ const ar: Record = { 'login.demoFailed': 'فشل الدخول إلى العرض التجريبي', 'login.oidcSignIn': 'تسجيل الدخول عبر {name}', 'login.oidcOnly': 'تم تعطيل المصادقة بكلمة المرور. يرجى تسجيل الدخول عبر مزود SSO.', + 'login.oidcLoggedOut': 'تم تسجيل خروجك. سجّل الدخول مجدداً عبر مزود SSO.', 'login.demoHint': 'جرّب العرض التجريبي دون الحاجة للتسجيل', 'login.mfaTitle': 'المصادقة الثنائية', 'login.mfaSubtitle': 'أدخل الرمز المكون من 6 أرقام من تطبيق المصادقة.', diff --git a/client/src/i18n/translations/br.ts b/client/src/i18n/translations/br.ts index 12612da..d3b11e6 100644 --- a/client/src/i18n/translations/br.ts +++ b/client/src/i18n/translations/br.ts @@ -362,6 +362,7 @@ const br: Record = { 'login.demoFailed': 'Falha no login de demonstração', 'login.oidcSignIn': 'Entrar com {name}', '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.mfaTitle': 'Autenticação em duas etapas', 'login.mfaSubtitle': 'Digite o código de 6 dígitos do seu app autenticador.', diff --git a/client/src/i18n/translations/cs.ts b/client/src/i18n/translations/cs.ts index defebfb..130f962 100644 --- a/client/src/i18n/translations/cs.ts +++ b/client/src/i18n/translations/cs.ts @@ -362,6 +362,7 @@ const cs: Record = { 'login.demoFailed': 'Přihlášení do dema se nezdařilo', '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.oidcLoggedOut': 'Byl jste odhlášen. Přihlaste se znovu přes SSO poskytovatele.', 'login.demoHint': 'Vyzkoušejte demo – registrace není nutná', 'login.mfaTitle': 'Dvoufaktorové ověření', 'login.mfaSubtitle': 'Zadejte 6místný kód z vaší autentizační aplikace.', diff --git a/client/src/i18n/translations/de.ts b/client/src/i18n/translations/de.ts index 1c76a6c..c9ebf45 100644 --- a/client/src/i18n/translations/de.ts +++ b/client/src/i18n/translations/de.ts @@ -362,6 +362,7 @@ const de: Record = { 'login.demoFailed': 'Demo-Login fehlgeschlagen', 'login.oidcSignIn': 'Anmelden mit {name}', '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.mfaTitle': 'Zwei-Faktor-Authentifizierung', 'login.mfaSubtitle': 'Gib den 6-stelligen Code aus deiner Authenticator-App ein.', diff --git a/client/src/i18n/translations/en.ts b/client/src/i18n/translations/en.ts index 6e6cc0b..2cb895a 100644 --- a/client/src/i18n/translations/en.ts +++ b/client/src/i18n/translations/en.ts @@ -383,6 +383,7 @@ const en: Record = { 'login.demoFailed': 'Demo login failed', 'login.oidcSignIn': 'Sign in with {name}', '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.mfaTitle': 'Two-factor authentication', 'login.mfaSubtitle': 'Enter the 6-digit code from your authenticator app.', diff --git a/client/src/i18n/translations/es.ts b/client/src/i18n/translations/es.ts index c487b25..4121943 100644 --- a/client/src/i18n/translations/es.ts +++ b/client/src/i18n/translations/es.ts @@ -1490,6 +1490,7 @@ const es: Record = { '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.', '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.currentPasswordRequired': 'La contraseña actual es obligatoria', diff --git a/client/src/i18n/translations/fr.ts b/client/src/i18n/translations/fr.ts index b1615c9..cbc2e09 100644 --- a/client/src/i18n/translations/fr.ts +++ b/client/src/i18n/translations/fr.ts @@ -369,6 +369,7 @@ const fr: Record = { 'login.demoFailed': 'Échec de la connexion démo', '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.oidcLoggedOut': 'Vous avez été déconnecté. Reconnectez-vous via votre fournisseur SSO.', 'login.demoHint': 'Essayez la démo — aucune inscription nécessaire', // Register diff --git a/client/src/i18n/translations/hu.ts b/client/src/i18n/translations/hu.ts index 3d6c660..40ce49a 100644 --- a/client/src/i18n/translations/hu.ts +++ b/client/src/i18n/translations/hu.ts @@ -362,6 +362,7 @@ const hu: Record = { 'login.demoFailed': 'Demo bejelentkezés sikertelen', '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.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.mfaTitle': 'Kétfaktoros hitelesítés', 'login.mfaSubtitle': 'Add meg a 6 jegyű kódot a hitelesítő alkalmazásból.', diff --git a/client/src/i18n/translations/it.ts b/client/src/i18n/translations/it.ts index 0a504f9..d5449ff 100644 --- a/client/src/i18n/translations/it.ts +++ b/client/src/i18n/translations/it.ts @@ -362,6 +362,7 @@ const it: Record = { 'login.demoFailed': 'Accesso demo fallito', 'login.oidcSignIn': 'Accedi con {name}', '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.mfaTitle': 'Autenticazione a due fattori', 'login.mfaSubtitle': 'Inserisci il codice a 6 cifre dalla tua app authenticator.', diff --git a/client/src/i18n/translations/nl.ts b/client/src/i18n/translations/nl.ts index 93c4e78..2e7495d 100644 --- a/client/src/i18n/translations/nl.ts +++ b/client/src/i18n/translations/nl.ts @@ -369,6 +369,7 @@ const nl: Record = { 'login.demoFailed': 'Demo-login mislukt', 'login.oidcSignIn': 'Inloggen met {name}', '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', // Register diff --git a/client/src/i18n/translations/pl.ts b/client/src/i18n/translations/pl.ts index b020286..4f60b98 100644 --- a/client/src/i18n/translations/pl.ts +++ b/client/src/i18n/translations/pl.ts @@ -329,6 +329,7 @@ const pl: Record = { 'login.demoFailed': 'Nie udało się zalogować do wersji demonstracyjnej', 'login.oidcSignIn': 'Zaloguj się z {name}', '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.mfaTitle': 'Uwierzytelnianie dwuskładnikowe', 'login.mfaSubtitle': 'Wprowadź 6-cyfrowy kod z aplikacji uwierzytelniającej.', diff --git a/client/src/i18n/translations/ru.ts b/client/src/i18n/translations/ru.ts index 3cf4cc7..18001fc 100644 --- a/client/src/i18n/translations/ru.ts +++ b/client/src/i18n/translations/ru.ts @@ -369,6 +369,7 @@ const ru: Record = { 'login.demoFailed': 'Ошибка демо-входа', 'login.oidcSignIn': 'Войти через {name}', 'login.oidcOnly': 'Вход по паролю отключён. Используйте вашего провайдера SSO для входа.', + 'login.oidcLoggedOut': 'Вы вышли из системы. Войдите снова через вашего провайдера SSO.', 'login.demoHint': 'Попробуйте демо — регистрация не требуется', // Register diff --git a/client/src/i18n/translations/zh.ts b/client/src/i18n/translations/zh.ts index 5dc7421..d0af81d 100644 --- a/client/src/i18n/translations/zh.ts +++ b/client/src/i18n/translations/zh.ts @@ -369,6 +369,7 @@ const zh: Record = { 'login.demoFailed': '演示登录失败', 'login.oidcSignIn': '通过 {name} 登录', 'login.oidcOnly': '密码登录已关闭。请通过 SSO 提供商登录。', + 'login.oidcLoggedOut': '您已退出登录。请重新通过 SSO 提供商登录。', 'login.demoHint': '试用演示——无需注册', // Register diff --git a/client/src/i18n/translations/zhTw.ts b/client/src/i18n/translations/zhTw.ts index fc35e1a..86fa141 100644 --- a/client/src/i18n/translations/zhTw.ts +++ b/client/src/i18n/translations/zhTw.ts @@ -353,6 +353,7 @@ const zhTw: Record = { 'login.demoFailed': '演示登入失敗', 'login.oidcSignIn': '透過 {name} 登入', 'login.oidcOnly': '密碼登入已關閉。請透過 SSO 提供商登入。', + 'login.oidcLoggedOut': '您已登出。請重新透過 SSO 提供商登入。', 'login.demoHint': '試用演示——無需註冊', // Register diff --git a/client/src/pages/AdminPage.tsx b/client/src/pages/AdminPage.tsx index 92f3b98..c6f5d51 100644 --- a/client/src/pages/AdminPage.tsx +++ b/client/src/pages/AdminPage.tsx @@ -1551,7 +1551,7 @@ docker run -d --name trek \\ await adminApi.rotateJwtSecret() setShowRotateJwtModal(false) logout() - navigate('/login') + navigate('/login', { state: { noRedirect: true } }) } catch { toast.error(t('common.error')) setRotatingJwt(false) diff --git a/client/src/pages/LoginPage.tsx b/client/src/pages/LoginPage.tsx index 44da863..6f76aee 100644 --- a/client/src/pages/LoginPage.tsx +++ b/client/src/pages/LoginPage.tsx @@ -1,5 +1,5 @@ -import React, { useState, useEffect, useMemo } from 'react' -import { useNavigate } from 'react-router-dom' +import React, { useState, useEffect, useMemo, useRef } from 'react' +import { useNavigate, useLocation } from 'react-router-dom' import { useAuthStore } from '../store/authStore' import { useSettingsStore } from '../store/settingsStore' import { SUPPORTED_LANGUAGES, useTranslation } from '../i18n' @@ -29,10 +29,13 @@ export default function LoginPage(): React.ReactElement { const [appConfig, setAppConfig] = useState(null) const [inviteToken, setInviteToken] = useState('') const [inviteValid, setInviteValid] = useState(false) + const exchangeInitiated = useRef(false) const { login, register, demoLogin, completeMfaLogin, loadUser } = useAuthStore() const { setLanguageLocal } = useSettingsStore() const navigate = useNavigate() + const location = useLocation() + const noRedirect = !!(location.state as { noRedirect?: boolean } | null)?.noRedirect const redirectTarget = useMemo(() => { const params = new URLSearchParams(window.location.search) @@ -63,11 +66,13 @@ export default function LoginPage(): React.ReactElement { } if (oidcCode) { + if (exchangeInitiated.current) return + exchangeInitiated.current = true setIsLoading(true) - window.history.replaceState({}, '', '/login') fetch('/api/auth/oidc/exchange?code=' + encodeURIComponent(oidcCode), { credentials: 'include' }) .then(r => r.json()) .then(async data => { + window.history.replaceState({}, '', '/login') if (data.token) { await loadUser() navigate('/dashboard', { replace: true }) @@ -75,7 +80,10 @@ export default function LoginPage(): React.ReactElement { setError(data.error || 'OIDC login failed') } }) - .catch(() => setError('OIDC login failed')) + .catch(() => { + window.history.replaceState({}, '', '/login') + setError('OIDC login failed') + }) .finally(() => setIsLoading(false)) return } @@ -96,12 +104,12 @@ export default function LoginPage(): React.ReactElement { if (config) { setAppConfig(config) 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' } } }) - }, [navigate, t]) + }, [navigate, t, noRedirect]) const handleDemoLogin = async (): Promise => { setError('') @@ -527,7 +535,7 @@ export default function LoginPage(): React.ReactElement { {oidcOnly ? ( <>

{t('login.title')}

-

{t('login.oidcOnly')}

+

{noRedirect ? t('login.oidcLoggedOut') : t('login.oidcOnly')}

{error && (
{error}