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) <noreply@anthropic.com>
This commit is contained in:
@@ -12,6 +12,7 @@ interface OrgState {
|
|||||||
currentTenantId: string | null; // DB UUID — used for API calls like /api/tenants/{id}
|
currentTenantId: string | null; // DB UUID — used for API calls like /api/tenants/{id}
|
||||||
organizations: OrgInfo[];
|
organizations: OrgInfo[];
|
||||||
scopes: Set<string>;
|
scopes: Set<string>;
|
||||||
|
scopesReady: boolean; // true once OrgResolver has finished loading scopes
|
||||||
username: string | null;
|
username: string | null;
|
||||||
setCurrentOrg: (orgId: string | null) => void;
|
setCurrentOrg: (orgId: string | null) => void;
|
||||||
setOrganizations: (orgs: OrgInfo[]) => void;
|
setOrganizations: (orgs: OrgInfo[]) => void;
|
||||||
@@ -24,6 +25,7 @@ export const useOrgStore = create<OrgState>((set, get) => ({
|
|||||||
currentTenantId: null,
|
currentTenantId: null,
|
||||||
organizations: [],
|
organizations: [],
|
||||||
scopes: new Set(),
|
scopes: new Set(),
|
||||||
|
scopesReady: false,
|
||||||
username: null,
|
username: null,
|
||||||
setUsername: (name) => set({ username: name }),
|
setUsername: (name) => set({ username: name }),
|
||||||
setCurrentOrg: (orgId) => {
|
setCurrentOrg: (orgId) => {
|
||||||
@@ -39,5 +41,5 @@ export const useOrgStore = create<OrgState>((set, get) => ({
|
|||||||
currentTenantId: match?.tenantId ?? get().currentTenantId,
|
currentTenantId: match?.tenantId ?? get().currentTenantId,
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
setScopes: (scopes) => set({ scopes }),
|
setScopes: (scopes) => set({ scopes, scopesReady: true }),
|
||||||
}));
|
}));
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { useScopes } from '../auth/useScopes';
|
import { useScopes } from '../auth/useScopes';
|
||||||
|
import { useOrgStore } from '../auth/useOrganization';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
scope: string;
|
scope: string;
|
||||||
@@ -8,6 +9,8 @@ interface Props {
|
|||||||
|
|
||||||
export function RequireScope({ scope, children, fallback }: Props) {
|
export function RequireScope({ scope, children, fallback }: Props) {
|
||||||
const scopes = useScopes();
|
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;
|
if (!scopes.has(scope)) return fallback ? <>{fallback}</> : null;
|
||||||
return <>{children}</>;
|
return <>{children}</>;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -22,13 +22,12 @@ import { TenantAuditPage } from './pages/tenant/TenantAuditPage';
|
|||||||
|
|
||||||
function LandingRedirect() {
|
function LandingRedirect() {
|
||||||
const scopes = useScopes();
|
const scopes = useScopes();
|
||||||
const { organizations, currentOrgId } = useOrgStore();
|
const { organizations, currentOrgId, scopesReady } = useOrgStore();
|
||||||
const currentOrg = organizations.find((o) => o.id === currentOrgId);
|
const currentOrg = organizations.find((o) => o.id === currentOrgId);
|
||||||
|
|
||||||
// Wait for scopes to be resolved — they're loaded async by OrgResolver.
|
// Wait for scopes to be fully resolved by OrgResolver before redirecting.
|
||||||
// An empty set means "not yet loaded" (even viewer gets observe:read).
|
if (!scopesReady) {
|
||||||
if (scopes.size === 0) {
|
return null;
|
||||||
return null; // OrgResolver is still fetching tokens
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Vendor → vendor console
|
// Vendor → vendor console
|
||||||
|
|||||||
Reference in New Issue
Block a user