// src/context/UserAuthContext.tsx import React, { createContext, useContext, useState, useEffect, ReactNode } from 'react'; import { useNavigate } from 'react-router-dom'; import axios, { AxiosInstance, AxiosResponse, AxiosError } from 'axios'; interface User { id: string; accountId: string; name: string; email: string; balance: number; activityScore: number; activityUnits: number; badges: string[]; position: number | null; ranking: string | null; isAdmin: boolean; joinedAt?: string; createdAt?: string; updatedAt?: string; } interface Competition { id: string; name: string; description: string; registrationStart: string; registrationEnd: string; competitionStart: string; competitionEnd: string; isActive: boolean; participants?: string[]; winners?: Record; } interface CompetitionStatusResponse { isActive: boolean; competition?: Competition; phase?: 'registration' | 'active' | 'ended' | 'none'; timeRemaining?: { days: number; hours: number; minutes: number }; } interface JoinCompetitionResponse { success: boolean; message: string; competition?: Competition; } interface RegisterData { accountId: string; name: string; email: string; password: string; } interface UserAuthContextType { user: User | null; token: string | null; loading: boolean; error: string | null; // Authentication functions register: (userData: RegisterData) => Promise; login: (accountId: string, password: string) => Promise; logout: () => void; clearError: () => void; // Competition functions joinCompetition: () => Promise; checkCompetitionStatus: () => Promise; getActiveCompetition: (force?: boolean) => Promise; getCompetitionById: (id: string) => Promise; // User-specific functions getUserProfile: () => Promise; getUserTransactions: (page?: number, limit?: number) => Promise<{ transactions: any[], pagination: { page: number, limit: number } }>; updateUserActivity: (score: number, units: number) => Promise; // Leaderboard functions getLeaderboard: () => Promise; getTopPerformers: (limit?: number) => Promise; getUserRanking: () => Promise<{ userId: string, position: number, score: number, outOf: number }>; // Enhanced competition features competitionCache: { activeCompetition?: Competition | null; status?: CompetitionStatusResponse; lastFetch?: number; }; refreshCompetitionData: (force?: boolean) => Promise; startCompetitionPolling: (intervalMs?: number) => void; stopCompetitionPolling: () => void; invalidateCompetitionCache: () => void; validateCompetitionDates: (competitionData: { registrationStart: string; registrationEnd: string; competitionStart: string; competitionEnd: string; }) => { isValid: boolean; errors: string[] }; getCompetitionParticipants: (competitionId: string) => Promise; // Admin functions (if user is admin) bulkUpdateCompetitions?: (operations: Array<{ id: string; action: 'activate' | 'deactivate' | 'end'; data?: any; }>) => Promise<{ success: number; failed: number; errors: string[] }>; } interface UserAuthProviderProps { children: ReactNode; } const API_URL = 'https://blue-backend-production.up.railway.app'; const CACHE_DURATION = 5 * 60 * 1000; // 5 minutes const REQUEST_TIMEOUT = 15000; // 15 seconds const UserAuthContext = createContext(undefined); export const UserAuthProvider: React.FC = ({ children }) => { const [user, setUser] = useState(null); const [token, setToken] = useState(null); const [loading, setLoading] = useState(false); const [error, setError] = useState(null); const [competitionCache, setCompetitionCache] = useState<{ activeCompetition?: Competition | null; status?: CompetitionStatusResponse; lastFetch?: number; }>({}); const [competitionPolling, setCompetitionPolling] = useState(null); const navigate = useNavigate(); // Create Axios instance with optimized configuration const apiClient: AxiosInstance = axios.create({ baseURL: API_URL, timeout: REQUEST_TIMEOUT, headers: { 'Content-Type': 'application/json', }, }); // Request interceptor to add auth token apiClient.interceptors.request.use( (config) => { if (token) { config.headers.Authorization = `Bearer ${token}`; } return config; }, (error) => Promise.reject(error) ); // Response interceptor for error handling apiClient.interceptors.response.use( (response: AxiosResponse) => response, (error: AxiosError) => { if (error.response?.status === 401) { // Token expired or invalid logout(); } return Promise.reject(error); } ); // Load user data from localStorage on initial render useEffect(() => { const storedToken = localStorage.getItem('token'); const storedUser = localStorage.getItem('user'); if (storedToken && storedUser) { setToken(storedToken); setUser(JSON.parse(storedUser)); } }, []); // Utility function for enhanced error handling const handleApiError = (error: any): string => { if (axios.isAxiosError(error)) { if (error.response?.data?.message) { return error.response.data.message; } if (error.code === 'ECONNABORTED') { return 'Request timeout. Please check your internet connection.'; } if (error.code === 'ERR_NETWORK') { return 'Network error. Please check your internet connection.'; } return error.message || 'Network error occurred'; } return error.message || 'An unexpected error occurred'; }; // Cache management functions const invalidateCompetitionCache = () => { setCompetitionCache({}); }; const refreshCompetitionData = async (force: boolean = false) => { const now = Date.now(); if (!force && competitionCache.lastFetch && (now - competitionCache.lastFetch) < CACHE_DURATION) { return competitionCache; } try { const [statusResponse, activeCompResponse] = await Promise.allSettled([ apiClient.get('/api/competition/status'), token ? apiClient.get('/api/competition/active') : Promise.resolve({ data: null }) ]); const status = statusResponse.status === 'fulfilled' ? statusResponse.value.data : { isActive: false, phase: 'none' }; const activeComp = activeCompResponse.status === 'fulfilled' ? activeCompResponse.value.data : null; const newCache = { activeCompetition: activeComp, status, lastFetch: now }; setCompetitionCache(newCache); return newCache; } catch (error) { console.error('Failed to refresh competition data:', error); return competitionCache; } }; // Polling management const startCompetitionPolling = (intervalMs: number = 60000) => { if (competitionPolling) { clearInterval(competitionPolling); } const interval = setInterval(() => { refreshCompetitionData(true); }, intervalMs); setCompetitionPolling(interval); }; const stopCompetitionPolling = () => { if (competitionPolling) { clearInterval(competitionPolling); setCompetitionPolling(null); } }; // Competition date validation const validateCompetitionDates = (competitionData: { registrationStart: string; registrationEnd: string; competitionStart: string; competitionEnd: string; }): { isValid: boolean; errors: string[] } => { const errors: string[] = []; const now = new Date(); const regStart = new Date(competitionData.registrationStart); const regEnd = new Date(competitionData.registrationEnd); const compStart = new Date(competitionData.competitionStart); const compEnd = new Date(competitionData.competitionEnd); if (isNaN(regStart.getTime())) errors.push('Invalid registration start date'); if (isNaN(regEnd.getTime())) errors.push('Invalid registration end date'); if (isNaN(compStart.getTime())) errors.push('Invalid competition start date'); if (isNaN(compEnd.getTime())) errors.push('Invalid competition end date'); if (errors.length === 0) { if (regStart < now) errors.push('Registration start date cannot be in the past'); if (regStart >= regEnd) errors.push('Registration end must be after registration start'); if (regEnd > compStart) errors.push('Competition start must be after registration end'); if (compStart >= compEnd) errors.push('Competition end must be after competition start'); const minDuration = 24 * 60 * 60 * 1000; // 1 day if ((regEnd.getTime() - regStart.getTime()) < minDuration) { errors.push('Registration period must be at least 1 day'); } if ((compEnd.getTime() - compStart.getTime()) < minDuration) { errors.push('Competition period must be at least 1 day'); } } return { isValid: errors.length === 0, errors }; }; // Authentication functions const register = async (userData: RegisterData): Promise => { setLoading(true); setError(null); try { const response = await apiClient.post('/api/auth/register', userData); setUser(response.data.user); // After successful registration, proceed with login await login(userData.accountId, userData.password); } catch (error) { const errorMsg = handleApiError(error); setError(errorMsg); throw new Error(errorMsg); } finally { setLoading(false); } }; const login = async (accountId: string, password: string): Promise => { setLoading(true); setError(null); try { console.log('Attempting login for accountId:', accountId); const response = await apiClient.post('/api/auth/login', { accountId, password }); if (!response.data.user || !response.data.token) { throw new Error('Invalid login response - missing user or token'); } console.log('Login successful, setting user and token'); setUser(response.data.user); setToken(response.data.token); // Store in localStorage localStorage.setItem('token', response.data.token); localStorage.setItem('user', JSON.stringify(response.data.user)); // Redirect to user dashboard navigate('/userdashboard'); } catch (error) { console.error('Login error:', error); const errorMsg = handleApiError(error); setError(errorMsg); throw new Error(errorMsg); } finally { setLoading(false); } }; const logout = (): void => { stopCompetitionPolling(); invalidateCompetitionCache(); setUser(null); setToken(null); localStorage.removeItem('token'); localStorage.removeItem('user'); navigate('/login'); }; const clearError = (): void => { setError(null); }; // Competition functions const joinCompetition = async (): Promise => { if (!token) { throw new Error('Authentication required - no token found'); } setLoading(true); setError(null); try { console.log('Making request to join competition...'); const response = await apiClient.post('/api/competition/join'); // Invalidate cache to refresh competition data invalidateCompetitionCache(); console.log('Successfully joined competition:', response.data); return response.data; } catch (error) { console.error('Join competition error:', error); const errorMsg = handleApiError(error); setError(errorMsg); throw new Error(errorMsg); } finally { setLoading(false); } }; const checkCompetitionStatus = async (): Promise => { try { const response = await apiClient.get('/api/competition/status'); return response.data; } catch (error) { console.error('Competition status check error:', error); const errorMsg = handleApiError(error); setError(errorMsg); return { isActive: false, phase: 'none' }; } }; const getActiveCompetition = async (force: boolean = false): Promise => { if (!token) { throw new Error('Authentication token missing'); } // Check cache first if not forcing refresh if (!force && competitionCache.activeCompetition && competitionCache.lastFetch) { const now = Date.now(); if ((now - competitionCache.lastFetch) < CACHE_DURATION) { return competitionCache.activeCompetition; } } try { const response = await apiClient.get('/api/competition/active'); // Update cache setCompetitionCache(prev => ({ ...prev, activeCompetition: response.data, lastFetch: Date.now() })); return response.data; } catch (error) { const errorMsg = handleApiError(error); setError(errorMsg); throw new Error(errorMsg); } }; const getCompetitionById = async (id: string): Promise => { try { const response = await apiClient.get(`/api/competition/${id}`); return response.data; } catch (error) { const errorMsg = handleApiError(error); setError(errorMsg); throw new Error(errorMsg); } }; const getCompetitionParticipants = async (competitionId: string): Promise => { if (!token || !user?.isAdmin) { throw new Error('Admin authentication required'); } try { const competition = await getCompetitionById(competitionId); if (!competition || !competition.participants) { return []; } // Fetch participant details using Promise.allSettled for better error handling const participantPromises = competition.participants.map(async (participantId: string) => { try { const response = await apiClient.get(`/api/users/${participantId}`); return response.data; } catch { return null; } }); const results = await Promise.allSettled(participantPromises); return results .filter(result => result.status === 'fulfilled' && result.value !== null) .map(result => (result as PromiseFulfilledResult).value); } catch (error) { const errorMsg = handleApiError(error); setError(errorMsg); throw new Error(errorMsg); } }; // User functions const getUserProfile = async (): Promise => { if (!token) { throw new Error('Authentication required'); } try { const response = await apiClient.get('/api/users/profile'); return response.data.user; } catch (error) { const errorMsg = handleApiError(error); setError(errorMsg); throw new Error(errorMsg); } }; const getUserTransactions = async (page: number = 1, limit: number = 10): Promise<{ transactions: any[], pagination: { page: number, limit: number } }> => { if (!token) { throw new Error('Authentication required'); } try { const response = await apiClient.get(`/api/users/transactions?page=${page}&limit=${limit}`); return response.data; } catch (error) { const errorMsg = handleApiError(error); setError(errorMsg); throw new Error(errorMsg); } }; const updateUserActivity = async (score: number, units: number): Promise => { if (!token) { throw new Error('Authentication required'); } try { const response = await apiClient.post('/api/users/activity', { score, units }); // Update local user state with new data if (user) { setUser(response.data.user); localStorage.setItem('user', JSON.stringify(response.data.user)); } return response.data.user; } catch (error) { const errorMsg = handleApiError(error); setError(errorMsg); throw new Error(errorMsg); } }; // Leaderboard functions const getLeaderboard = async (): Promise => { try { const response = await apiClient.get('/api/leaderboard'); return response.data; } catch (error) { const errorMsg = handleApiError(error); setError(errorMsg); throw new Error(errorMsg); } }; const getTopPerformers = async (limit: number = 5): Promise => { try { const response = await apiClient.get(`/api/leaderboard/top?limit=${limit}`); return response.data; } catch (error) { const errorMsg = handleApiError(error); setError(errorMsg); throw new Error(errorMsg); } }; const getUserRanking = async (): Promise<{ userId: string, position: number, score: number, outOf: number }> => { if (!token) { throw new Error('Authentication required'); } try { const response = await apiClient.get('/api/leaderboard/user'); return response.data; } catch (error) { const errorMsg = handleApiError(error); setError(errorMsg); throw new Error(errorMsg); } }; // Admin functions const bulkUpdateCompetitions = async (operations: Array<{ id: string; action: 'activate' | 'deactivate' | 'end'; data?: any; }>): Promise<{ success: number; failed: number; errors: string[] }> => { if (!token || !user?.isAdmin) { throw new Error('Admin authentication required'); } const results = { success: 0, failed: 0, errors: [] as string[] }; // Use Promise.allSettled for better parallel processing const operationPromises = operations.map(async (operation) => { try { await apiClient.put(`/api/admin/competitions/${operation.id}/${operation.action}`, operation.data || {}); return { success: true, operation }; } catch (error) { return { success: false, operation, error: handleApiError(error) }; } }); const operationResults = await Promise.allSettled(operationPromises); operationResults.forEach((result) => { if (result.status === 'fulfilled') { if (result.value.success) { results.success++; } else { results.failed++; results.errors.push(`Failed to ${result.value.operation.action} competition ${result.value.operation.id}: ${result.value.error}`); } } else { results.failed++; results.errors.push(`Unexpected error during bulk operation: ${result.reason}`); } }); // Invalidate cache after bulk operations invalidateCompetitionCache(); return results; }; const contextValue: UserAuthContextType = { user, token, loading, error, // Authentication functions register, login, logout, clearError, // Competition functions joinCompetition, checkCompetitionStatus, getActiveCompetition, getCompetitionById, // User functions getUserProfile, getUserTransactions, updateUserActivity, // Leaderboard functions getLeaderboard, getTopPerformers, getUserRanking, // Enhanced competition features competitionCache, refreshCompetitionData, startCompetitionPolling, stopCompetitionPolling, invalidateCompetitionCache, validateCompetitionDates, getCompetitionParticipants, // Admin functions (conditionally available) ...(user?.isAdmin && { bulkUpdateCompetitions }), }; return ( {children} ); }; export const useUserAuth = (): UserAuthContextType => { const context = useContext(UserAuthContext); if (context === undefined) { throw new Error('useUserAuth must be used within a UserAuthProvider'); } return context; };