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
124 lines
3.0 KiB
TypeScript
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;
|
|
}
|