From f26b2c877773cfab936f538aabf2ddbd1377f738 Mon Sep 17 00:00:00 2001 From: Denozordec Date: Sat, 18 Jul 2026 14:09:21 +0700 Subject: [PATCH] =?UTF-8?q?fix(auth):=20=D1=83=D0=B1=D1=80=D0=B0=D1=82?= =?UTF-8?q?=D1=8C=20SSO-=D1=86=D0=B8=D0=BA=D0=BB=20=D0=BD=D0=B0=20/auth/ca?= =?UTF-8?q?llback=20=D0=BF=D0=BE=D1=81=D0=BB=D0=B5=20replaceState?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Cursor --- apps/web/src/lib/api-client.ts | 6 +++++- apps/web/src/routes/auth.callback.tsx | 21 ++++++++++++++------- 2 files changed, 19 insertions(+), 8 deletions(-) diff --git a/apps/web/src/lib/api-client.ts b/apps/web/src/lib/api-client.ts index 01b37c0..c46c49e 100644 --- a/apps/web/src/lib/api-client.ts +++ b/apps/web/src/lib/api-client.ts @@ -38,9 +38,13 @@ async function fetchApi(path: string, options: RequestInit = {}): Promise }) if (!res.ok) { if (res.status === 401) { + // Avoid redirect storms: only hand off once per page load + const handoffKey = 'vps_auth_401_handoff' + const already = sessionStorage.getItem(handoffKey) clearToken() const cfg = await ensureAuthConfig() - if (cfg.required || isAuthEnabled()) { + if ((cfg.required || isAuthEnabled()) && !already) { + sessionStorage.setItem(handoffKey, '1') redirectToPortalLogin(`${window.location.origin}/auth/callback`) } } diff --git a/apps/web/src/routes/auth.callback.tsx b/apps/web/src/routes/auth.callback.tsx index 520bccc..3bfff02 100644 --- a/apps/web/src/routes/auth.callback.tsx +++ b/apps/web/src/routes/auth.callback.tsx @@ -2,6 +2,8 @@ import { createFileRoute, redirect } from '@tanstack/react-router' import { ensureAuthConfig, firstAllowedPath, + getClaims, + getToken, parseHashToken, redirectToPortalLogin, setToken, @@ -11,14 +13,19 @@ export const Route = createFileRoute('/auth/callback')({ beforeLoad: async () => { await ensureAuthConfig() const { accessToken } = parseHashToken(window.location.hash) - if (!accessToken) { - redirectToPortalLogin(`${window.location.origin}/auth/callback`) - await new Promise(() => {}) - return + if (accessToken) { + setToken(accessToken) + // Do not replaceState to strip the hash here — that re-triggers beforeLoad + // with an empty hash and sends the user back to the portal (SSO loop). + sessionStorage.removeItem('vps_auth_401_handoff') + throw redirect({ to: firstAllowedPath() }) } - setToken(accessToken) - window.history.replaceState(null, '', '/auth/callback') - throw redirect({ to: firstAllowedPath() }) + // Already stored from a previous parse (e.g. remount) — finish handoff. + if (getToken() && getClaims()) { + throw redirect({ to: firstAllowedPath() }) + } + redirectToPortalLogin(`${window.location.origin}/auth/callback`) + await new Promise(() => {}) }, component: () => null, })