Loading portal-gui/src/app/guards/protected-route.tsx +15 −10 Changes for portal-gui/src/app/guards/protected-route.tsx: 15 added lines, 10 removed lines. Original line number Diff line number Diff line import { useEffect, useState } from 'react'; import { useEffect } from 'react'; import { Navigate, Outlet, useLocation } from 'react-router-dom'; import { useAuth } from '@/features/auth/auth-context'; import { LoadingState } from '@/components/common/loading-state'; Loading @@ -6,14 +6,22 @@ import { ROUTES } from '../config/route'; import { AuthPending } from './auth-pending'; const LAST_REDIRECT_KEY = 'portal:login-redirect-at'; // Coming back signed out sooner than this means the login did not stick, e.g. the // session cookie is refused, and redirecting again would loop through Keycloak. // A page loaded signed out this soon after our redirect is a login that did not // stick, e.g. the session cookie is refused; redirecting again would loop. const REDIRECT_COOLDOWN_MS = 10_000; function redirectedRecently(): boolean { function cameBackFromOwnRedirect(): boolean { const [arrival] = performance.getEntriesByType( 'navigation', ) as PerformanceNavigationTiming[]; // Back/Forward is the user's own navigation, whatever the timing. if (arrival?.type === 'back_forward') { return false; } try { const last = Number(sessionStorage.getItem(LAST_REDIRECT_KEY)); return Date.now() - last < REDIRECT_COOLDOWN_MS; const mark = Number(sessionStorage.getItem(LAST_REDIRECT_KEY)); const sinceMark = performance.timeOrigin - mark; return mark > 0 && sinceMark >= 0 && sinceMark < REDIRECT_COOLDOWN_MS; } catch { return false; } Loading @@ -34,13 +42,10 @@ function markRedirect(at: number | null) { export function ProtectedRoute() { const { status, isAuthenticated, login } = useAuth(); const location = useLocation(); // Read once, on arrival: a page load straight after our own redirect is the // browser coming back from Keycloak. const [justRedirected] = useState(redirectedRecently); // Set by the login callback when it failed; redirecting again would repeat it. const error = new URLSearchParams(location.search).get('error'); const signedOut = status === 'anonymous'; const stuck = signedOut && !error && justRedirected; const stuck = signedOut && !error && cameBackFromOwnRedirect(); const shouldRedirect = signedOut && !error && !stuck; const returnTo = location.pathname + location.search + location.hash; Loading portal-gui/vite.config.ts +6 −0 Changes for portal-gui/vite.config.ts: 6 added lines, 0 removed lines. Original line number Diff line number Diff line Loading @@ -14,6 +14,10 @@ export default defineConfig(({ mode }) => { }, }; // Keeps signed-in pages out of the back-forward cache: after a logout, Back would // otherwise show the old page's pixels before any script can hide them. const headers = { 'Cache-Control': 'no-store' }; return { plugins: [react(), tailwindcss()], Loading @@ -28,10 +32,12 @@ export default defineConfig(({ mode }) => { port: 3000, strictPort: true, proxy, headers, }, preview: { proxy, headers, }, }; }); Loading
portal-gui/src/app/guards/protected-route.tsx +15 −10 Changes for portal-gui/src/app/guards/protected-route.tsx: 15 added lines, 10 removed lines. Original line number Diff line number Diff line import { useEffect, useState } from 'react'; import { useEffect } from 'react'; import { Navigate, Outlet, useLocation } from 'react-router-dom'; import { useAuth } from '@/features/auth/auth-context'; import { LoadingState } from '@/components/common/loading-state'; Loading @@ -6,14 +6,22 @@ import { ROUTES } from '../config/route'; import { AuthPending } from './auth-pending'; const LAST_REDIRECT_KEY = 'portal:login-redirect-at'; // Coming back signed out sooner than this means the login did not stick, e.g. the // session cookie is refused, and redirecting again would loop through Keycloak. // A page loaded signed out this soon after our redirect is a login that did not // stick, e.g. the session cookie is refused; redirecting again would loop. const REDIRECT_COOLDOWN_MS = 10_000; function redirectedRecently(): boolean { function cameBackFromOwnRedirect(): boolean { const [arrival] = performance.getEntriesByType( 'navigation', ) as PerformanceNavigationTiming[]; // Back/Forward is the user's own navigation, whatever the timing. if (arrival?.type === 'back_forward') { return false; } try { const last = Number(sessionStorage.getItem(LAST_REDIRECT_KEY)); return Date.now() - last < REDIRECT_COOLDOWN_MS; const mark = Number(sessionStorage.getItem(LAST_REDIRECT_KEY)); const sinceMark = performance.timeOrigin - mark; return mark > 0 && sinceMark >= 0 && sinceMark < REDIRECT_COOLDOWN_MS; } catch { return false; } Loading @@ -34,13 +42,10 @@ function markRedirect(at: number | null) { export function ProtectedRoute() { const { status, isAuthenticated, login } = useAuth(); const location = useLocation(); // Read once, on arrival: a page load straight after our own redirect is the // browser coming back from Keycloak. const [justRedirected] = useState(redirectedRecently); // Set by the login callback when it failed; redirecting again would repeat it. const error = new URLSearchParams(location.search).get('error'); const signedOut = status === 'anonymous'; const stuck = signedOut && !error && justRedirected; const stuck = signedOut && !error && cameBackFromOwnRedirect(); const shouldRedirect = signedOut && !error && !stuck; const returnTo = location.pathname + location.search + location.hash; Loading
portal-gui/vite.config.ts +6 −0 Changes for portal-gui/vite.config.ts: 6 added lines, 0 removed lines. Original line number Diff line number Diff line Loading @@ -14,6 +14,10 @@ export default defineConfig(({ mode }) => { }, }; // Keeps signed-in pages out of the back-forward cache: after a logout, Back would // otherwise show the old page's pixels before any script can hide them. const headers = { 'Cache-Control': 'no-store' }; return { plugins: [react(), tailwindcss()], Loading @@ -28,10 +32,12 @@ export default defineConfig(({ mode }) => { port: 3000, strictPort: true, proxy, headers, }, preview: { proxy, headers, }, }; });