import React, { createContext, useContext, useEffect, useMemo, useRef, useState } from "react"; import { API_URL } from "./api"; const AuthCtx = createContext(null); const UI_MODE_KEY = "nexapg_ui_mode"; function loadStorage() { try { return JSON.parse(localStorage.getItem("nexapg_auth") || "null"); } catch { return null; } } function loadUiMode() { try { const value = localStorage.getItem(UI_MODE_KEY); if (value === "easy" || value === "dba") return value; } catch { // ignore storage errors } return "dba"; } export function AuthProvider({ children }) { const initial = loadStorage(); const [tokens, setTokens] = useState(initial?.tokens || null); const [me, setMe] = useState(initial?.me || null); const [uiMode, setUiModeState] = useState(loadUiMode); const [alertStatus, setAlertStatus] = useState({ warnings: [], alerts: [], warning_count: 0, alert_count: 0 }); const [alertToasts, setAlertToasts] = useState([]); const knownAlertKeysRef = useRef(new Set()); const hasAlertSnapshotRef = useRef(false); const persist = (nextTokens, nextMe) => { if (nextTokens && nextMe) { localStorage.setItem("nexapg_auth", JSON.stringify({ tokens: nextTokens, me: nextMe })); } else { localStorage.removeItem("nexapg_auth"); } }; const refresh = async () => { if (!tokens?.refreshToken) return null; const res = await fetch(`${API_URL}/auth/refresh`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ refresh_token: tokens.refreshToken }), }); if (!res.ok) { setTokens(null); setMe(null); persist(null, null); return null; } const data = await res.json(); const nextTokens = { accessToken: data.access_token, refreshToken: data.refresh_token }; setTokens(nextTokens); persist(nextTokens, me); return nextTokens; }; const login = async (email, password) => { const res = await fetch(`${API_URL}/auth/login`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ email, password }), }); if (!res.ok) throw new Error("Login failed"); const data = await res.json(); const nextTokens = { accessToken: data.access_token, refreshToken: data.refresh_token }; const meRes = await fetch(`${API_URL}/me`, { headers: { Authorization: `Bearer ${nextTokens.accessToken}` }, }); if (!meRes.ok) throw new Error("Could not load user profile"); const profile = await meRes.json(); setTokens(nextTokens); setMe(profile); persist(nextTokens, profile); }; const logout = async () => { try { if (tokens?.accessToken) { await fetch(`${API_URL}/auth/logout`, { method: "POST", headers: { Authorization: `Bearer ${tokens.accessToken}` }, }); } } finally { setTokens(null); setMe(null); persist(null, null); } }; const dismissAlertToast = (toastId) => { setAlertToasts((prev) => prev.map((t) => (t.id === toastId ? { ...t, closing: true } : t))); setTimeout(() => { setAlertToasts((prev) => prev.filter((t) => t.id !== toastId)); }, 220); }; useEffect(() => { if (!tokens?.accessToken) { setAlertStatus({ warnings: [], alerts: [], warning_count: 0, alert_count: 0 }); setAlertToasts([]); knownAlertKeysRef.current = new Set(); hasAlertSnapshotRef.current = false; return; } let mounted = true; const pushToastsForNewItems = (items) => { if (!items.length) return; const createdAt = Date.now(); const nextToasts = items.slice(0, 4).map((item, idx) => ({ id: `${createdAt}-${idx}-${item.alert_key}`, alertKey: item.alert_key, severity: item.severity, title: item.name, target: item.target_name, message: item.message, closing: false, })); setAlertToasts((prev) => [...nextToasts, ...prev].slice(0, 6)); for (const toast of nextToasts) { setTimeout(() => { if (!mounted) return; dismissAlertToast(toast.id); }, 8000); } }; const loadAlertStatus = async () => { const request = async (accessToken) => fetch(`${API_URL}/alerts/status`, { headers: { Authorization: `Bearer ${accessToken}` }, }); let res = await request(tokens.accessToken); if (res.status === 401 && tokens.refreshToken) { const refreshed = await refresh(); if (refreshed?.accessToken) { res = await request(refreshed.accessToken); } } if (!res.ok) return; const payload = await res.json(); if (!mounted) return; setAlertStatus(payload); const currentItems = [...(payload.warnings || []), ...(payload.alerts || [])]; const currentKeys = new Set(currentItems.map((item) => item.alert_key)); if (!hasAlertSnapshotRef.current) { knownAlertKeysRef.current = currentKeys; hasAlertSnapshotRef.current = true; return; } const newItems = currentItems.filter((item) => !knownAlertKeysRef.current.has(item.alert_key)); knownAlertKeysRef.current = currentKeys; pushToastsForNewItems(newItems); }; loadAlertStatus().catch(() => {}); const timer = setInterval(() => { loadAlertStatus().catch(() => {}); }, 8000); return () => { mounted = false; clearInterval(timer); }; }, [tokens?.accessToken, tokens?.refreshToken]); const setUiMode = (nextMode) => { const mode = nextMode === "easy" ? "easy" : "dba"; setUiModeState(mode); localStorage.setItem(UI_MODE_KEY, mode); }; const value = useMemo( () => ({ tokens, me, login, logout, refresh, uiMode, setUiMode, alertStatus, alertToasts, dismissAlertToast, }), [tokens, me, uiMode, alertStatus, alertToasts] ); return {children}; } export function useAuth() { const ctx = useContext(AuthCtx); if (!ctx) throw new Error("useAuth must be used inside AuthProvider"); return ctx; }