From a3a6f99958c65b4d5c13d2bd19f59fd357bf7fae Mon Sep 17 00:00:00 2001 From: hsiegeln <37154749+hsiegeln@users.noreply.github.com> Date: Fri, 10 Apr 2026 19:16:46 +0200 Subject: [PATCH] fix: prevent vendor redirect to /tenant on hard refresh RequireScope and LandingRedirect now wait for scopesReady flag before evaluating, preventing the race where org-scoped tokens load before global tokens and the vendor gets incorrectly redirected. Co-Authored-By: Claude Opus 4.6 (1M context) --- ui/src/auth/useOrganization.ts | 4 +++- ui/src/components/RequireScope.tsx | 3 +++ ui/src/router.tsx | 9 ++++----- 3 files changed, 10 insertions(+), 6 deletions(-) diff --git a/ui/src/auth/useOrganization.ts b/ui/src/auth/useOrganization.ts index 8768e88..30aee9b 100644 --- a/ui/src/auth/useOrganization.ts +++ b/ui/src/auth/useOrganization.ts @@ -12,6 +12,7 @@ interface OrgState { currentTenantId: string | null; // DB UUID — used for API calls like /api/tenants/{id} organizations: OrgInfo[]; scopes: Set; + scopesReady: boolean; // true once OrgResolver has finished loading scopes username: string | null; setCurrentOrg: (orgId: string | null) => void; setOrganizations: (orgs: OrgInfo[]) => void; @@ -24,6 +25,7 @@ export const useOrgStore = create((set, get) => ({ currentTenantId: null, organizations: [], scopes: new Set(), + scopesReady: false, username: null, setUsername: (name) => set({ username: name }), setCurrentOrg: (orgId) => { @@ -39,5 +41,5 @@ export const useOrgStore = create((set, get) => ({ currentTenantId: match?.tenantId ?? get().currentTenantId, }); }, - setScopes: (scopes) => set({ scopes }), + setScopes: (scopes) => set({ scopes, scopesReady: true }), })); diff --git a/ui/src/components/RequireScope.tsx b/ui/src/components/RequireScope.tsx index 6e87ea5..63f3714 100644 --- a/ui/src/components/RequireScope.tsx +++ b/ui/src/components/RequireScope.tsx @@ -1,4 +1,5 @@ import { useScopes } from '../auth/useScopes'; +import { useOrgStore } from '../auth/useOrganization'; interface Props { scope: string; @@ -8,6 +9,8 @@ interface Props { export function RequireScope({ scope, children, fallback }: Props) { const scopes = useScopes(); + const scopesReady = useOrgStore((s) => s.scopesReady); + if (!scopesReady) return null; // Still loading — don't redirect yet if (!scopes.has(scope)) return fallback ? <>{fallback} : null; return <>{children}; } diff --git a/ui/src/router.tsx b/ui/src/router.tsx index b127728..24260dc 100644 --- a/ui/src/router.tsx +++ b/ui/src/router.tsx @@ -22,13 +22,12 @@ import { TenantAuditPage } from './pages/tenant/TenantAuditPage'; function LandingRedirect() { const scopes = useScopes(); - const { organizations, currentOrgId } = useOrgStore(); + const { organizations, currentOrgId, scopesReady } = useOrgStore(); const currentOrg = organizations.find((o) => o.id === currentOrgId); - // Wait for scopes to be resolved — they're loaded async by OrgResolver. - // An empty set means "not yet loaded" (even viewer gets observe:read). - if (scopes.size === 0) { - return null; // OrgResolver is still fetching tokens + // Wait for scopes to be fully resolved by OrgResolver before redirecting. + if (!scopesReady) { + return null; } // Vendor → vendor console