feat: migrate app state to bootstrap endpoint
app-ci-deploy / test-build-deploy (push) Has been cancelled

Use GET /me/bootstrap as the primary authenticated state source per ADR-0003. AuthContext now fetches and exposes typed bootstrapData, Header and class/detail admin checks use bootstrap permissions/roles, dashboard surfaces onboarding/calendar/timetable/graph status, and graph navigation derives school setup state from bootstrap instead of /school/status.

Refs: t_44353587
This commit is contained in:
2026-05-28 19:07:07 +01:00
parent 67e47fc47f
commit 65ce1bede8
6 changed files with 270 additions and 81 deletions
+23 -14
View File
@@ -5,6 +5,8 @@ import { CCUser, CCUserMetadata, authService } from '../services/auth/authServic
import { logger } from '../debugConfig';
import { supabase } from '../supabaseClient';
import { storageService, StorageKeys } from '../services/auth/localStorageService';
import { fetchBootstrapData, BootstrapResponse, invalidateBootstrapCache } from '../services/bootstrap/bootstrapService';
export interface AuthContextType {
user: CCUser | null;
@@ -15,6 +17,7 @@ export interface AuthContextType {
signIn: (email: string, password: string) => Promise<void>;
signOut: () => Promise<void>;
clearError: () => void;
bootstrapData: BootstrapResponse | null;
}
export const AuthContext = createContext<AuthContextType>({
@@ -25,7 +28,8 @@ export const AuthContext = createContext<AuthContextType>({
error: null,
signIn: async () => {},
signOut: async () => {},
clearError: () => {}
clearError: () => {},
bootstrapData: null
});
export function AuthProvider({ children }: { children: React.ReactNode }) {
@@ -35,8 +39,8 @@ export function AuthProvider({ children }: { children: React.ReactNode }) {
const [accessToken, setAccessToken] = useState<string | null>(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<Error | null>(null);
const [bootstrapData, setBootstrapData] = useState<BootstrapResponse | null>(null);
const apiBase = import.meta.env.VITE_API_BASE as string;
const persistSession = useCallback((session: Session | null) => {
if (session) {
@@ -71,15 +75,15 @@ export function AuthProvider({ children }: { children: React.ReactNode }) {
return { user: resolvedUser, role: resolvedRole };
}, []);
const triggerUserInit = useCallback((token: string) => {
fetch(`${apiBase}/user/init`, {
method: 'POST',
headers: { Authorization: `Bearer ${token}` },
})
.then(r => r.json())
.then(data => logger.debug('auth-context', '✅ User init', data))
.catch(err => logger.warn('auth-context', '⚠️ User init failed', { err }));
}, [apiBase]);
const loadBootstrap = useCallback(async (token: string | null | undefined) => {
if (!token) {
setBootstrapData(null);
return null;
}
const bootstrap = await fetchBootstrapData(token);
setBootstrapData(bootstrap);
return bootstrap;
}, []);
useEffect(() => {
const { data: { subscription } } = supabase.auth.onAuthStateChange(
@@ -94,7 +98,7 @@ export function AuthProvider({ children }: { children: React.ReactNode }) {
setUser(resolvedUser);
setUserRole(role);
setAccessToken(session.access_token ?? null);
triggerUserInit(session.access_token);
await loadBootstrap(session.access_token);
} catch (buildError) {
logger.error('auth-context', '❌ Failed to build user from session', { event, error: buildError });
setUser(null);
@@ -119,6 +123,7 @@ export function AuthProvider({ children }: { children: React.ReactNode }) {
setUser(resolvedUser);
setUserRole(role);
setAccessToken(session.access_token ?? null);
await loadBootstrap(session.access_token);
} catch (buildError) {
logger.error('auth-context', '❌ Failed to build user from session', { event, error: buildError });
setUser(null);
@@ -140,13 +145,15 @@ export function AuthProvider({ children }: { children: React.ReactNode }) {
setUser(null);
setUserRole(null);
setAccessToken(null);
setBootstrapData(null);
invalidateBootstrapCache();
setLoading(false);
}
}
);
return () => subscription.unsubscribe();
}, [buildUserFromSupabase, persistSession, triggerUserInit]);
}, [buildUserFromSupabase, persistSession, loadBootstrap]);
const signIn = async (email: string, password: string) => {
try {
@@ -167,6 +174,7 @@ export function AuthProvider({ children }: { children: React.ReactNode }) {
setUser(resolvedUser);
setUserRole(role);
setAccessToken(data.session?.access_token ?? null);
await loadBootstrap(data.session?.access_token ?? null);
}
} catch (error) {
logger.error('auth-context', '❌ Sign in failed', { error });
@@ -205,7 +213,8 @@ export function AuthProvider({ children }: { children: React.ReactNode }) {
error,
signIn,
signOut,
clearError
clearError,
bootstrapData
}}
>
{children}