import { createContext, useCallback, useContext, useEffect, useState, } from "react"; import { flushSync } from "react-dom"; import { AUTH_PHASES, isAuthPhase } from "../constants/authPhases"; const AuthContext = createContext(); export const useAuth = () => { const context = useContext(AuthContext); if (!context) { throw new Error("useAuth must be used within an AuthProvider"); } return context; }; export const AuthProvider = ({ children }) => { const [user, setUser] = useState(null); const [token, setToken] = useState(null); const [permissions, setPermissions] = useState(null); const [needsFirstTimeSetup, setNeedsFirstTimeSetup] = useState(false); // Authentication state machine phases const [authPhase, setAuthPhase] = useState(AUTH_PHASES.INITIALISING); const [permissionsLoading, setPermissionsLoading] = useState(false); // Define functions first const fetchPermissions = useCallback(async (authToken) => { try { setPermissionsLoading(true); const response = await fetch("/api/v1/permissions/user-permissions", { headers: { Authorization: `Bearer ${authToken}`, }, }); if (response.ok) { const data = await response.json(); setPermissions(data); return data; } else { console.error("Failed to fetch permissions"); return null; } } catch (error) { console.error("Error fetching permissions:", error); return null; } finally { setPermissionsLoading(false); } }, []); const refreshPermissions = useCallback(async () => { if (token) { const updatedPermissions = await fetchPermissions(token); return updatedPermissions; } return null; }, [token, fetchPermissions]); // Initialize auth state from localStorage useEffect(() => { const storedToken = localStorage.getItem("token"); const storedUser = localStorage.getItem("user"); if (storedToken && storedUser) { try { setToken(storedToken); setUser(JSON.parse(storedUser)); // Fetch permissions from backend fetchPermissions(storedToken); // User is authenticated, skip setup check setAuthPhase(AUTH_PHASES.READY); } catch (error) { console.error("Error parsing stored user data:", error); localStorage.removeItem("token"); localStorage.removeItem("user"); // Move to setup check phase setAuthPhase(AUTH_PHASES.CHECKING_SETUP); } } else { // No stored auth, check if setup is needed setAuthPhase(AUTH_PHASES.CHECKING_SETUP); } }, [fetchPermissions]); const login = async (username, password) => { try { const response = await fetch("/api/v1/auth/login", { method: "POST", headers: { "Content-Type": "application/json", }, body: JSON.stringify({ username, password }), }); const data = await response.json(); if (response.ok) { // Check if TFA is required if (data.requiresTfa) { return { success: true, requiresTfa: true }; } // Regular successful login setToken(data.token); setUser(data.user); localStorage.setItem("token", data.token); localStorage.setItem("user", JSON.stringify(data.user)); // Fetch user permissions after successful login const userPermissions = await fetchPermissions(data.token); if (userPermissions) { setPermissions(userPermissions); } return { success: true }; } else { return { success: false, error: data.error || "Login failed" }; } } catch { return { success: false, error: "Network error occurred" }; } }; const logout = async () => { try { if (token) { await fetch("/api/v1/auth/logout", { method: "POST", headers: { Authorization: `Bearer ${token}`, "Content-Type": "application/json", }, }); } } catch (error) { console.error("Logout error:", error); } finally { setToken(null); setUser(null); setPermissions(null); localStorage.removeItem("token"); localStorage.removeItem("user"); } }; const updateProfile = async (profileData) => { try { const response = await fetch("/api/v1/auth/profile", { method: "PUT", headers: { Authorization: `Bearer ${token}`, "Content-Type": "application/json", }, body: JSON.stringify(profileData), }); const data = await response.json(); if (response.ok) { setUser(data.user); localStorage.setItem("user", JSON.stringify(data.user)); return { success: true, user: data.user }; } else { return { success: false, error: data.error || "Update failed" }; } } catch { return { success: false, error: "Network error occurred" }; } }; const changePassword = async (currentPassword, newPassword) => { try { const response = await fetch("/api/v1/auth/change-password", { method: "PUT", headers: { Authorization: `Bearer ${token}`, "Content-Type": "application/json", }, body: JSON.stringify({ currentPassword, newPassword }), }); const data = await response.json(); if (response.ok) { return { success: true }; } else { return { success: false, error: data.error || "Password change failed", }; } } catch { return { success: false, error: "Network error occurred" }; } }; const isAdmin = () => { return user?.role === "admin"; }; // Permission checking functions const hasPermission = (permission) => { // If permissions are still loading, return false to show loading state if (permissionsLoading) { return false; } return permissions?.[permission] === true; }; const canViewDashboard = () => hasPermission("can_view_dashboard"); const canViewHosts = () => hasPermission("can_view_hosts"); const canManageHosts = () => hasPermission("can_manage_hosts"); const canViewPackages = () => hasPermission("can_view_packages"); const canManagePackages = () => hasPermission("can_manage_packages"); const canViewUsers = () => hasPermission("can_view_users"); const canManageUsers = () => hasPermission("can_manage_users"); const canViewReports = () => hasPermission("can_view_reports"); const canExportData = () => hasPermission("can_export_data"); const canManageSettings = () => hasPermission("can_manage_settings"); // Check if any admin users exist (for first-time setup) const checkAdminUsersExist = useCallback(async () => { try { const response = await fetch("/api/v1/auth/check-admin-users", { method: "GET", headers: { "Content-Type": "application/json", }, }); if (response.ok) { const data = await response.json(); setNeedsFirstTimeSetup(!data.hasAdminUsers); setAuthPhase(AUTH_PHASES.READY); // Setup check complete, move to ready phase } else { // If endpoint doesn't exist or fails, assume setup is needed setNeedsFirstTimeSetup(true); setAuthPhase(AUTH_PHASES.READY); } } catch (error) { console.error("Error checking admin users:", error); // If there's an error, assume setup is needed setNeedsFirstTimeSetup(true); setAuthPhase(AUTH_PHASES.READY); } }, []); // Check for admin users ONLY when in CHECKING_SETUP phase useEffect(() => { if (isAuthPhase.checkingSetup(authPhase)) { checkAdminUsersExist(); } }, [authPhase, checkAdminUsersExist]); const setAuthState = (authToken, authUser) => { // Use flushSync to ensure all state updates are applied synchronously flushSync(() => { setToken(authToken); setUser(authUser); setNeedsFirstTimeSetup(false); setAuthPhase(AUTH_PHASES.READY); }); // Store in localStorage after state is updated localStorage.setItem("token", authToken); localStorage.setItem("user", JSON.stringify(authUser)); // Fetch permissions immediately for the new authenticated user fetchPermissions(authToken); }; // Computed loading state based on phase and permissions state const isLoading = !isAuthPhase.ready(authPhase) || permissionsLoading; // Function to check authentication status (maintains API compatibility) const isAuthenticated = () => { return !!(user && token && isAuthPhase.ready(authPhase)); }; const value = { user, token, permissions, isLoading, needsFirstTimeSetup, authPhase, login, logout, updateProfile, changePassword, refreshPermissions, setAuthState, isAuthenticated, isAdmin, hasPermission, canViewDashboard, canViewHosts, canManageHosts, canViewPackages, canManagePackages, canViewUsers, canManageUsers, canViewReports, canExportData, canManageSettings, }; return {children}; };