Files
helix-engage/src/providers/auth-provider.tsx
saridsa2 c4b6f9a438 fix: 5 bug fixes — #533 #531 #529 #527 #547
#533: Remove redundant Call History top header (duplicate TopBar)
#531: Block logout during active call (confirm dialog + UCID check)
#529: Block outbound calls when agent is on Break/Training
#527: Remove updatePatient during appointment creation (was mutating
      shared Patient entity, affecting all past appointments)
#547: SLA rules seeded via API (config issue, not code)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-10 19:26:55 +05:30

153 lines
4.8 KiB
TypeScript

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<AuthContextType | undefined>(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<User>(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 (
<AuthContext.Provider value={{ user, isAdmin, isCCAgent, isAuthenticated, loading, loginWithUser, login, logout, setRole }}>
{children}
</AuthContext.Provider>
);
};
export { getInitials };