// src/context/UserAuthContext.tsx import React, { createContext, useContext, useState, useEffect, ReactNode } from 'react'; import { useNavigate } from 'react-router-dom'; 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: () => 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 }>; // Competition utility functions 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 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 navigate = useNavigate(); // 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 retry logic async function withRetry( operation: () => Promise, maxRetries: number = 3, delay: number = 1000 ): Promise { let lastError: Error; for (let i = 0; i < maxRetries; i++) { try { return await operation(); } catch (error) { lastError = error instanceof Error ? error : new Error('Unknown error'); if (i < maxRetries - 1) { await new Promise(resolve => setTimeout(resolve, delay * (i + 1))); } } } throw lastError!; } // 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'); // Add minimum duration checks const minRegDuration = 24 * 60 * 60 * 1000; // 1 day const minCompDuration = 24 * 60 * 60 * 1000; // 1 day if ((regEnd.getTime() - regStart.getTime()) < minRegDuration) { errors.push('Registration period must be at least 1 day'); } if ((compEnd.getTime() - compStart.getTime()) < minCompDuration) { 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 withRetry(async () => { return await fetch(`${API_URL}/api/auth/register`, { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify(userData), }); }); const data = await response.json(); if (!response.ok) { throw new Error(data.message || 'Registration failed'); } setUser(data.user); // After successful registration, proceed with login await login(userData.accountId, userData.password); } catch (err) { setError(err instanceof Error ? err.message : 'An unexpected error occurred'); throw err; } finally { setLoading(false); } }; // Enhanced login function to ensure proper token storage const login = async (accountId: string, password: string): Promise => { setLoading(true); setError(null); try { console.log('Attempting login for accountId:', accountId); const response = await withRetry(async () => { return await fetch(`${API_URL}/api/auth/login`, { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({ accountId, password }), }); }); const data = await response.json(); console.log('Login response:', { success: response.ok, status: response.status }); if (!response.ok) { throw new Error(data.message || 'Login failed'); } // Ensure we have both user and token if (!data.user || !data.token) { throw new Error('Invalid login response - missing user or token'); } console.log('Login successful, setting user and token'); setUser(data.user); setToken(data.token); // Store in localStorage localStorage.setItem('token', data.token); localStorage.setItem('user', JSON.stringify(data.user)); console.log('Token stored in localStorage:', !!localStorage.getItem('token')); // Redirect to user dashboard navigate('/userdashboard'); } catch (err) { console.error('Login error:', err); setError(err instanceof Error ? err.message : 'An unexpected error occurred'); throw err; } finally { setLoading(false); } }; const logout = (): void => { setUser(null); setToken(null); localStorage.removeItem('token'); localStorage.removeItem('user'); navigate('/login'); }; const clearError = (): void => { setError(null); }; // Competition functions const joinCompetition = async (): Promise => { console.log('Token exists:', !!token); console.log('Token value:', token ? `${token.substring(0, 20)}...` : 'null'); if (!token) { const error = 'Authentication required - no token found'; console.error(error); throw new Error(error); } console.log('User exists:', !!user); console.log('User data:', user ? { id: user.id, accountId: user.accountId } : 'null'); setLoading(true); setError(null); try { console.log('Making request to join competition...'); const response = await withRetry(async () => { const url = `${API_URL}/api/competition/join`; console.log('Request URL:', url); const headers = { 'Authorization': `Bearer ${token}`, 'Content-Type': 'application/json', }; console.log('Request headers:', headers); return await fetch(url, { method: 'POST', headers: headers, }); }); console.log('Response status:', response.status); console.log('Response ok:', response.ok); const data = await response.json(); console.log('Response data:', data); if (!response.ok) { if (response.status === 401) { console.error('401 Unauthorized - Token may be invalid or expired'); throw new Error('Authentication failed. Please log in again.'); } throw new Error(data.message || `Failed to join competition (${response.status})`); } console.log('Successfully joined competition:', data); return data; } catch (err) { console.error('Join competition error:', err); const errorMsg = err instanceof Error ? err.message : 'An unexpected error occurred'; setError(errorMsg); throw new Error(errorMsg); } finally { setLoading(false); } }; const checkCompetitionStatus = async (): Promise => { try { const response = await withRetry(async () => { return await fetch(`${API_URL}/api/competition/status`, { method: 'GET', headers: { 'Content-Type': 'application/json', } }); }); const data = await response.json(); if (!response.ok) { throw new Error(data.message || 'Failed to check competition status'); } return data; } catch (err) { console.error('Competition status check error:', err); const errorMsg = err instanceof Error ? err.message : 'An unexpected error occurred'; setError(errorMsg); return { isActive: false, phase: 'none' }; } }; const getActiveCompetition = async (): Promise => { if (!token) { throw new Error('Authentication token missing'); } try { const response = await withRetry(async () => { return await fetch(`${API_URL}/api/competition/active`, { method: 'GET', headers: { 'Authorization': `Bearer ${token}`, 'Content-Type': 'application/json', }, }); }); const data = await response.json(); if (!response.ok) { throw new Error(data.message || 'Failed to get active competition'); } return data; } catch (err) { const errorMsg = err instanceof Error ? err.message : 'An unexpected error occurred'; setError(errorMsg); throw new Error(errorMsg); } }; const getCompetitionById = async (id: string): Promise => { try { const response = await withRetry(async () => { return await fetch(`${API_URL}/api/competition/${id}`, { method: 'GET', }); }); const data = await response.json(); if (!response.ok) { throw new Error(data.message || 'Failed to get competition by ID'); } return data; } catch (err) { const errorMsg = err instanceof Error ? err.message : 'An unexpected error occurred'; 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 const participants = await Promise.all( competition.participants.map(async (participantId: string) => { try { const response = await fetch(`${API_URL}/api/users/${participantId}`, { headers: { 'Authorization': `Bearer ${token}` } }); return response.ok ? await response.json() : null; } catch { return null; } }) ); return participants.filter(Boolean); } catch (err) { const errorMsg = err instanceof Error ? err.message : 'Failed to get competition participants'; setError(errorMsg); throw new Error(errorMsg); } }; // User functions const getUserProfile = async (): Promise => { if (!token) { throw new Error('Authentication required'); } try { const response = await withRetry(async () => { return await fetch(`${API_URL}/api/users/profile`, { method: 'GET', headers: { 'Authorization': `Bearer ${token}`, }, }); }); const data = await response.json(); if (!response.ok) { throw new Error(data.message || 'Failed to get user profile'); } return data.user; } catch (err) { const errorMsg = err instanceof Error ? err.message : 'An unexpected error occurred'; 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 withRetry(async () => { return await fetch(`${API_URL}/api/users/transactions?page=${page}&limit=${limit}`, { method: 'GET', headers: { 'Authorization': `Bearer ${token}`, }, }); }); const data = await response.json(); if (!response.ok) { throw new Error(data.message || 'Failed to get user transactions'); } return data; } catch (err) { const errorMsg = err instanceof Error ? err.message : 'An unexpected error occurred'; 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 withRetry(async () => { return await fetch(`${API_URL}/api/users/activity`, { method: 'POST', headers: { 'Authorization': `Bearer ${token}`, 'Content-Type': 'application/json', }, body: JSON.stringify({ score, units }), }); }); const data = await response.json(); if (!response.ok) { throw new Error(data.message || 'Failed to update user activity'); } // Update local user state with new data if (user) { setUser(data.user); localStorage.setItem('user', JSON.stringify(data.user)); } return data.user; } catch (err) { const errorMsg = err instanceof Error ? err.message : 'An unexpected error occurred'; setError(errorMsg); throw new Error(errorMsg); } }; // Leaderboard functions const getLeaderboard = async (): Promise => { try { const response = await withRetry(async () => { return await fetch(`${API_URL}/api/leaderboard`, { method: 'GET', }); }); const data = await response.json(); if (!response.ok) { throw new Error(data.message || 'Failed to get leaderboard'); } return data; } catch (err) { const errorMsg = err instanceof Error ? err.message : 'An unexpected error occurred'; setError(errorMsg); throw new Error(errorMsg); } }; const getTopPerformers = async (limit: number = 5): Promise => { try { const response = await withRetry(async () => { return await fetch(`${API_URL}/api/leaderboard/top?limit=${limit}`, { method: 'GET', }); }); const data = await response.json(); if (!response.ok) { throw new Error(data.message || 'Failed to get top performers'); } return data; } catch (err) { const errorMsg = err instanceof Error ? err.message : 'An unexpected error occurred'; 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 withRetry(async () => { return await fetch(`${API_URL}/api/leaderboard/user`, { method: 'GET', headers: { 'Authorization': `Bearer ${token}`, }, }); }); const data = await response.json(); if (!response.ok) { throw new Error(data.message || 'Failed to get user ranking'); } return data; } catch (err) { const errorMsg = err instanceof Error ? err.message : 'An unexpected error occurred'; setError(errorMsg); throw new Error(errorMsg); } }; // Admin functions (only available if user is admin) 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[] }; for (const operation of operations) { try { const response = await fetch(`${API_URL}/api/admin/competitions/${operation.id}/${operation.action}`, { method: 'PUT', headers: { 'Authorization': `Bearer ${token}`, 'Content-Type': 'application/json', }, body: JSON.stringify(operation.data || {}), }); if (!response.ok) { const errorData = await response.json(); throw new Error(errorData.message || `Failed to ${operation.action} competition`); } results.success++; } catch (error) { results.failed++; results.errors.push(`Failed to ${operation.action} competition ${operation.id}: ${error instanceof Error ? error.message : 'Unknown error'}`); } } 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, // Competition utility functions 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; };