180 lines
5.3 KiB
TypeScript
180 lines
5.3 KiB
TypeScript
import { createContext, useContext, useState, useEffect, useCallback, useMemo } from 'react';
|
|
import type { ReactNode } from 'react';
|
|
import { useNavigate } from 'react-router-dom';
|
|
import { authService, type AppUser, type UserRole } from '@/api';
|
|
|
|
interface AuthSession {
|
|
user: AppUser;
|
|
access_token: string;
|
|
}
|
|
|
|
interface AuthContextType {
|
|
user: AppUser | null;
|
|
session: AuthSession | null;
|
|
userRole: UserRole;
|
|
loading: boolean;
|
|
signIn: (username: string, password: string) => Promise<{ error: { message: string } | null }>;
|
|
signOut: () => Promise<void>;
|
|
}
|
|
|
|
const AuthContext = createContext<AuthContextType | undefined>(undefined);
|
|
|
|
/**
|
|
* Maps the API user_id to the application's UserRole.
|
|
* 1 -> helpdesk
|
|
* 2 -> bank_customer
|
|
* 3 -> manufacturing_customer
|
|
*/
|
|
const mapUserIdToRole = (userId: string): UserRole => {
|
|
switch (userId) {
|
|
case '1':
|
|
return 'helpdesk';
|
|
case '2':
|
|
return 'bank_customer';
|
|
case '3':
|
|
return 'manufacturing_customer';
|
|
default:
|
|
console.warn(`Unknown user_id: ${userId}. Defaulting to null role.`);
|
|
return null;
|
|
}
|
|
};
|
|
|
|
export const AuthProvider = ({ children }: { children: ReactNode }) => {
|
|
// OPTIMIZATION: Initialize from localStorage synchronously to avoid blocking render
|
|
const getInitialSession = (): AuthSession | null => {
|
|
try {
|
|
const storedSession = localStorage.getItem('dealer360_session');
|
|
if (storedSession) {
|
|
return JSON.parse(storedSession) as AuthSession;
|
|
}
|
|
} catch (e) {
|
|
localStorage.removeItem('dealer360_session');
|
|
}
|
|
return null;
|
|
};
|
|
|
|
const initialSession = getInitialSession();
|
|
const [user, setUser] = useState<AppUser | null>(initialSession?.user || null);
|
|
const [session, setSession] = useState<AuthSession | null>(initialSession);
|
|
const [userRole, setUserRole] = useState<UserRole>(initialSession?.user?.role || null);
|
|
const [loading, setLoading] = useState(false); // Start as false since we sync synchronously
|
|
const navigate = useNavigate();
|
|
|
|
// Helper to sync state from localStorage
|
|
const syncStateFromStorage = useCallback(() => {
|
|
const storedSession = localStorage.getItem('dealer360_session');
|
|
if (storedSession) {
|
|
try {
|
|
const parsedSession = JSON.parse(storedSession) as AuthSession;
|
|
setSession(parsedSession);
|
|
setUser(parsedSession.user);
|
|
setUserRole(parsedSession.user.role);
|
|
} catch (e) {
|
|
localStorage.removeItem('dealer360_session');
|
|
setSession(null);
|
|
setUser(null);
|
|
setUserRole(null);
|
|
}
|
|
} else {
|
|
setSession(null);
|
|
setUser(null);
|
|
setUserRole(null);
|
|
}
|
|
}, []);
|
|
|
|
useEffect(() => {
|
|
// Initial sync (already done synchronously, but keep for consistency)
|
|
// This is mainly for storage event listeners
|
|
|
|
// Listen for storage changes (multi-tab support)
|
|
const handleStorageChange = (e: StorageEvent) => {
|
|
if (e.key === 'dealer360_session') {
|
|
syncStateFromStorage();
|
|
// If logged out in another tab, redirect to login
|
|
if (!e.newValue && !window.location.pathname.includes('/login')) {
|
|
navigate('/login');
|
|
}
|
|
}
|
|
};
|
|
|
|
window.addEventListener('storage', handleStorageChange);
|
|
return () => window.removeEventListener('storage', handleStorageChange);
|
|
}, [syncStateFromStorage, navigate]);
|
|
|
|
const signIn = async (username: string, password: string): Promise<{ error: { message: string } | null }> => {
|
|
try {
|
|
const response = await authService.login({ username, password });
|
|
|
|
if (response.success && response.data) {
|
|
const { user_id, email, username: apiUsername, company_name, token } = response.data;
|
|
|
|
const role = mapUserIdToRole(user_id);
|
|
|
|
const appUser: AppUser = {
|
|
id: user_id,
|
|
username: apiUsername,
|
|
email: email,
|
|
role: role,
|
|
company_name: company_name,
|
|
};
|
|
|
|
const authSession: AuthSession = {
|
|
user: appUser,
|
|
access_token: token,
|
|
};
|
|
|
|
// Store session in localStorage for persistence
|
|
localStorage.setItem('dealer360_session', JSON.stringify(authSession));
|
|
|
|
setUser(appUser);
|
|
setSession(authSession);
|
|
setUserRole(role);
|
|
|
|
return { error: null };
|
|
}
|
|
|
|
return { error: { message: response.message || 'Login failed' } };
|
|
} catch (error: any) {
|
|
const errorMessage = error.response?.data?.message || error.message || 'An unexpected error occurred during login';
|
|
return { error: { message: errorMessage } };
|
|
}
|
|
};
|
|
|
|
const signOut = async () => {
|
|
// Clear state
|
|
localStorage.removeItem('dealer360_session');
|
|
setUser(null);
|
|
setSession(null);
|
|
setUserRole(null);
|
|
|
|
// Attempt to notify backend
|
|
try {
|
|
await authService.logout();
|
|
} catch (e) {
|
|
// Silent fail
|
|
}
|
|
|
|
navigate('/login');
|
|
};
|
|
|
|
// Memoize context value to prevent unnecessary re-renders
|
|
const contextValue = useMemo(
|
|
() => ({ user, session, userRole, loading, signIn, signOut }),
|
|
[user, session, userRole, loading, signIn, signOut]
|
|
);
|
|
|
|
return (
|
|
<AuthContext.Provider value={contextValue}>
|
|
{children}
|
|
</AuthContext.Provider>
|
|
);
|
|
};
|
|
|
|
export const useAuth = () => {
|
|
const context = useContext(AuthContext);
|
|
if (context === undefined) {
|
|
throw new Error('useAuth must be used within an AuthProvider');
|
|
}
|
|
return context;
|
|
};
|