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) <noreply@anthropic.com>
This commit is contained in:
@@ -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:
|
||||
|
||||
@@ -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 (
|
||||
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'center', minHeight: '100vh' }}>
|
||||
<Spinner />
|
||||
|
||||
@@ -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<string, unknown>)?.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;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user