import type { ReactNode } from 'react'; import { createContext, useCallback, useContext, useEffect, useState } from 'react'; export type Role = 'executive' | 'admin' | 'cc-agent'; type User = { id?: string; name: string; initials: string; role: Role; email: string; avatarUrl?: string; platformRoles?: string[]; }; type AuthContextType = { user: User; isAdmin: boolean; isCCAgent: boolean; isAuthenticated: boolean; loading: boolean; loginWithUser: (userData: User) => void; login: () => void; logout: () => void; setRole: (role: Role) => void; }; const DEFAULT_USER: User = { name: '', initials: '', role: 'executive', email: '', }; const getInitials = (firstName: string, lastName: string): string => `${firstName[0] ?? ''}${lastName[0] ?? ''}`.toUpperCase(); const STORAGE_KEY = 'helix_user'; const loadPersistedUser = (): User | null => { try { const token = localStorage.getItem('helix_access_token'); const stored = localStorage.getItem(STORAGE_KEY); if (token && stored) { return JSON.parse(stored) as User; } } catch { // corrupt data } return null; }; const AuthContext = createContext(undefined); export const useAuth = (): AuthContextType => { const context = useContext(AuthContext); if (context === undefined) { throw new Error('useAuth must be used within an AuthProvider'); } return context; }; interface AuthProviderProps { children: ReactNode; } export const AuthProvider = ({ children }: AuthProviderProps) => { const persisted = loadPersistedUser(); const [user, setUser] = useState(persisted ?? DEFAULT_USER); const [isAuthenticated, setIsAuthenticated] = useState(!!persisted); const [loading, setLoading] = useState(!persisted && !!localStorage.getItem('helix_access_token')); // If we have a token but no persisted user, try to restore session useEffect(() => { if (!isAuthenticated && localStorage.getItem('helix_access_token') && !persisted) { // Token exists but no user data — could re-fetch profile here // For now, just clear stale token localStorage.removeItem('helix_access_token'); localStorage.removeItem('helix_refresh_token'); localStorage.removeItem(STORAGE_KEY); setLoading(false); } }, [isAuthenticated, persisted]); const isAdmin = user.role === 'admin'; const isCCAgent = user.role === 'cc-agent'; const loginWithUser = useCallback((userData: User) => { setUser(userData); setIsAuthenticated(true); localStorage.setItem(STORAGE_KEY, JSON.stringify(userData)); }, []); const login = useCallback(() => { setIsAuthenticated(true); }, []); const logout = useCallback(async () => { // Block logout during active call const { isOutboundPending, disconnectSip } = await import('@/state/sip-manager'); const activeUcid = localStorage.getItem('helix_active_ucid'); if (isOutboundPending() || activeUcid) { const confirmed = window.confirm( 'You have an active call. Logging out will disconnect the call. Are you sure?', ); if (!confirmed) return; } // Disconnect SIP before logout try { disconnectSip(true); } catch {} // Notify sidecar to unlock Redis session + Ozonetel logout — await before clearing tokens const token = localStorage.getItem('helix_access_token'); if (token) { const apiUrl = import.meta.env.VITE_API_URL ?? 'http://localhost:4100'; try { await fetch(`${apiUrl}/auth/logout`, { method: 'POST', headers: { Authorization: `Bearer ${token}` }, signal: AbortSignal.timeout(5000), }); } catch (err) { console.warn('Logout cleanup failed:', err); } } setUser(DEFAULT_USER); setIsAuthenticated(false); localStorage.removeItem('helix_access_token'); localStorage.removeItem('helix_refresh_token'); localStorage.removeItem('helix_agent_config'); localStorage.removeItem(STORAGE_KEY); }, []); const setRole = useCallback((role: Role) => { setUser(prev => { const updated = { ...prev, role }; localStorage.setItem(STORAGE_KEY, JSON.stringify(updated)); return updated; }); }, []); return ( {children} ); }; export { getInitials };