Files
app/src/services/bootstrap/bootstrapService.ts
T
kcar 65ce1bede8
app-ci-deploy / test-build-deploy (push) Has been cancelled
feat: migrate app state to bootstrap endpoint
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
2026-05-28 19:07:07 +01:00

124 lines
3.0 KiB
TypeScript

import { logger } from '../../debugConfig';
const apiBase = (import.meta.env.VITE_API_BASE as string) || '';
export interface BootstrapProfile {
id: string;
email: string;
display_name: string;
user_type: string;
school_id: string | null;
}
export interface BootstrapMembership {
institute_id: string;
role: string;
status: string;
is_active: boolean;
institute: {
id: string;
name: string;
urn: string | null;
};
}
export interface BootstrapActiveInstitute {
id: string | null;
source: string;
membership_role: string | null;
}
export interface BootstrapPermissions {
platform_admin: boolean;
platform_super_admin: boolean;
can_create_school: boolean;
can_manage_school: boolean;
can_manage_calendar: boolean;
can_manage_timetable: boolean;
can_invite_staff: boolean;
can_manage_classes: boolean;
can_view_student_data: boolean;
can_use_canvas: boolean;
}
export interface BootstrapOnboarding {
next_step: string;
required: string[];
optional: string[];
message: string;
}
export interface BootstrapCalendarStatus {
available: boolean;
academic_year_count: number;
term_count: number;
current_academic_year_id: string | null;
needs_setup: boolean;
}
export interface BootstrapTimetableStatus {
available: boolean;
teacher_timetable_id: string | null;
slot_count: number;
needs_setup: boolean;
}
export interface BootstrapGraphStatus {
available: boolean;
user_db: string | null;
institute_db: string | null;
projection_state: string;
needs_rebuild: boolean;
last_checked_at: string | null;
error_code: string | null;
}
export interface BootstrapResponse {
profile: BootstrapProfile;
memberships: BootstrapMembership[];
active_institute: BootstrapActiveInstitute;
permissions: BootstrapPermissions;
school_status: string;
onboarding: BootstrapOnboarding;
calendar_status: BootstrapCalendarStatus;
timetable_status: BootstrapTimetableStatus;
graph_status: BootstrapGraphStatus;
}
let cachedBootstrap: BootstrapResponse | null = null;
let cacheTimestamp = 0;
const CACHE_TTL_MS = 60_000; // 60 second cache
export async function fetchBootstrapData(accessToken: string): Promise<BootstrapResponse | null> {
if (!accessToken) return null;
// Return cached data if fresh
if (cachedBootstrap && Date.now() - cacheTimestamp < CACHE_TTL_MS) {
return cachedBootstrap;
}
try {
const res = await fetch(`${apiBase}/me/bootstrap`, {
headers: { Authorization: `Bearer ${accessToken}` },
});
if (!res.ok) {
logger.warn('bootstrap-service', `Bootstrap fetch failed: ${res.status}`);
return cachedBootstrap; // Return stale cache on error
}
const data: BootstrapResponse = await res.json();
cachedBootstrap = data;
cacheTimestamp = Date.now();
return data;
} catch (err) {
logger.error('bootstrap-service', 'Bootstrap fetch error', { err });
return cachedBootstrap; // Return stale cache on error
}
}
export function invalidateBootstrapCache(): void {
cachedBootstrap = null;
cacheTimestamp = 0;
}