From 6ccf7f3fcbb69ae7e9e5cb6a1fcbc135f9cd678c Mon Sep 17 00:00:00 2001 From: hsiegeln <37154749+hsiegeln@users.noreply.github.com> Date: Sun, 5 Apr 2026 13:11:44 +0200 Subject: [PATCH] fix: ProtectedRoute spinner fix, TokenSync cleanup, dev hot-reload - ProtectedRoute: only gate on initial auth load, not every async op - TokenSync: OrgResolver is sole source of org data, remove fetchUserInfo - docker-compose.dev: mount ui/dist for hot-reload Co-Authored-By: Claude Opus 4.6 (1M context) --- docker-compose.dev.yml | 3 +++ ui/src/auth/ProtectedRoute.tsx | 11 ++++++++++- ui/src/main.tsx | 34 +++++++--------------------------- 3 files changed, 20 insertions(+), 28 deletions(-) diff --git a/docker-compose.dev.yml b/docker-compose.dev.yml index 979ddae..6e6c69b 100644 --- a/docker-compose.dev.yml +++ b/docker-compose.dev.yml @@ -13,8 +13,11 @@ services: cameleer-saas: ports: - "8080:8080" + volumes: + - ./ui/dist:/app/static environment: SPRING_PROFILES_ACTIVE: dev + SPRING_WEB_RESOURCES_STATIC_LOCATIONS: file:/app/static/,classpath:/static/ cameleer3-server: ports: diff --git a/ui/src/auth/ProtectedRoute.tsx b/ui/src/auth/ProtectedRoute.tsx index 6433b20..0c08c06 100644 --- a/ui/src/auth/ProtectedRoute.tsx +++ b/ui/src/auth/ProtectedRoute.tsx @@ -1,11 +1,20 @@ +import { useRef } from 'react'; import { Navigate } from 'react-router'; import { useLogto } from '@logto/react'; import { Spinner } from '@cameleer/design-system'; export function ProtectedRoute({ children }: { children: React.ReactNode }) { const { isAuthenticated, isLoading } = useLogto(); + // The Logto SDK sets isLoading=true for EVERY async method (getAccessToken, etc.), + // not just initial auth. Only gate on the initial load — once isLoading is false + // for the first time, never show the spinner again. + const initialLoadDone = useRef(false); - if (isLoading) { + if (!isLoading) { + initialLoadDone.current = true; + } + + if (!initialLoadDone.current) { return (
diff --git a/ui/src/main.tsx b/ui/src/main.tsx index 480d4b0..45f7f2c 100644 --- a/ui/src/main.tsx +++ b/ui/src/main.tsx @@ -10,6 +10,7 @@ import { fetchConfig } from './config'; import { setTokenProvider } from './api/client'; import { useOrgStore } from './auth/useOrganization'; + const queryClient = new QueryClient({ defaultOptions: { queries: { @@ -20,33 +21,12 @@ const queryClient = new QueryClient({ }); function TokenSync({ resource }: { resource: string }) { - const { getAccessToken, isAuthenticated, fetchUserInfo } = useLogto(); - const { currentOrgId, setCurrentOrg, setOrganizations } = useOrgStore(); + const { getAccessToken, isAuthenticated } = useLogto(); + const { currentOrgId } = useOrgStore(); - // After auth, resolve user's organizations from Logto - useEffect(() => { - if (!isAuthenticated) { - setOrganizations([]); - setCurrentOrg(null); - return; - } - - fetchUserInfo().then((info) => { - const orgData = (info as Record)?.organization_data as - Array<{ id: string; name: string }> | undefined; - const orgs = orgData ?? []; - setOrganizations(orgs.map((o) => ({ id: o.id, name: o.name }))); - - // Auto-select if user has exactly one org - if (orgs.length === 1 && !currentOrgId) { - setCurrentOrg(orgs[0].id); - } - }).catch(() => { - // fetchUserInfo may fail if token is being refreshed - }); - }, [isAuthenticated]); - - // Set token provider — org-scoped if org selected, plain otherwise + // Set token provider — org-scoped if org selected, plain otherwise. + // OrgResolver is the sole source of org data (via /api/me). + // eslint-disable-next-line react-hooks/exhaustive-deps — getAccessToken is unstable (new ref each render) useEffect(() => { if (isAuthenticated && resource) { if (currentOrgId) { @@ -57,7 +37,7 @@ function TokenSync({ resource }: { resource: string }) { } else { setTokenProvider(null); } - }, [isAuthenticated, getAccessToken, resource, currentOrgId]); + }, [isAuthenticated, resource, currentOrgId]); return null; }