'use client'; import { createContext, useContext, useEffect, useState, ReactNode } from 'react'; import { useRouter } from 'next/navigation'; import apiClient from '@/lib/api/client'; import { tokenStorage } from '@/lib/utils/tokenStorage'; export interface User { id: string; email: string; name: string; photoUrl?: string; role: string; families?: Array<{ id: string; familyId: string; role: string; }>; eulaAcceptedAt?: string | null; eulaVersion?: string | null; } export interface LoginCredentials { email: string; password: string; deviceFingerprint?: string; } export interface RegisterData { email: string; password: string; name: string; role?: string; dateOfBirth: string; // COPPA compliance - required parentalEmail?: string; // For users 13-17 coppaConsentGiven?: boolean; // For users 13-17 } interface AuthContextType { user: User | null; token: string | null; isLoading: boolean; isAuthenticated: boolean; login: (credentials: LoginCredentials) => Promise; register: (data: RegisterData) => Promise; logout: () => Promise; refreshUser: () => Promise; } const AuthContext = createContext(undefined); export const AuthProvider = ({ children }: { children: ReactNode }) => { const [user, setUser] = useState(null); const [token, setToken] = useState(null); const [isLoading, setIsLoading] = useState(true); const router = useRouter(); const isAuthenticated = !!user; // Check authentication status on mount useEffect(() => { // Only run on client side if (typeof window !== 'undefined') { checkAuth(); } else { setIsLoading(false); } }, []); const checkAuth = async () => { // Ensure we're on client side if (typeof window === 'undefined') { setIsLoading(false); return; } try { const accessToken = tokenStorage.getAccessToken(); const refreshToken = tokenStorage.getRefreshToken(); console.log('[AuthContext] checkAuth - tokens present:', { hasAccess: !!accessToken, hasRefresh: !!refreshToken }); if (!accessToken && !refreshToken) { console.log('[AuthContext] No tokens found, user not authenticated'); setUser(null); setToken(null); setIsLoading(false); return; } // If we only have refresh token but no access token, don't make /me call // The axios interceptor will handle getting a new access token when needed if (!accessToken && refreshToken) { console.log('[AuthContext] Only refresh token present, skipping /me call'); setUser(null); setToken(null); setIsLoading(false); return; } // If we only have access token but no refresh token, it might be a timing issue // during login (React Strict Mode). Give it a moment and check again. if (accessToken && !refreshToken) { console.log('[AuthContext] Only access token present, checking if this is temporary...'); // Wait a tiny bit and check again (for React Strict Mode race condition) await new Promise(resolve => setTimeout(resolve, 10)); const refreshTokenRetry = tokenStorage.getRefreshToken(); if (!refreshTokenRetry) { console.log('[AuthContext] Still no refresh token after retry, clearing invalid state'); tokenStorage.clearTokens(); setUser(null); setToken(null); setIsLoading(false); return; } else { console.log('[AuthContext] Refresh token found on retry, proceeding with auth check'); // Update local variable for the rest of the function } } // At this point we have both tokens - proceed with auth check // Set token in state if we have one if (accessToken) { setToken(accessToken); } const response = await apiClient.get('/api/v1/auth/me'); console.log('[AuthContext] /me response:', response.data); // Check if response has expected structure if (response.data?.data) { console.log('[AuthContext] Setting user from response.data.data:', response.data.data); console.log('[AuthContext] EULA fields from /me:', { eulaAcceptedAt: response.data.data.eulaAcceptedAt, eulaVersion: response.data.data.eulaVersion, }); setUser(response.data.data); } else if (response.data?.user) { // Handle alternative response structure console.log('[AuthContext] Setting user from response.data.user:', response.data.user); setUser(response.data.user); } else { throw new Error('Invalid response structure'); } } catch (error: any) { console.error('[AuthContext] Auth check failed:', error); // Don't clear tokens on 401 during initial auth check // The axios interceptor in client.ts will handle token refresh automatically // Only clear tokens if the error is NOT a 401 (e.g., network error, 403, etc.) // Or if there's no refresh token available (meaning refresh already failed) const hasRefreshToken = tokenStorage.getRefreshToken(); if (!hasRefreshToken) { // No refresh token means we can't recover - clear everything console.log('[AuthContext] No refresh token available, clearing auth state'); tokenStorage.clearTokens(); setUser(null); setToken(null); } else if (error?.response?.status === 401) { // 401 with refresh token - let axios interceptor handle refresh console.log('[AuthContext] 401 error but refresh token exists, letting axios interceptor handle refresh'); // Don't clear tokens - the axios interceptor will attempt refresh setUser(null); setToken(null); } else if (error?.response?.status === 403) { // 403 means forbidden - clear tokens console.log('[AuthContext] 403 Forbidden, clearing auth state'); tokenStorage.clearTokens(); setUser(null); setToken(null); } } finally { setIsLoading(false); } }; const login = async (credentials: LoginCredentials) => { try { const deviceId = generateDeviceFingerprint(); const deviceInfo = { deviceId, platform: 'web', model: navigator.userAgent, osVersion: navigator.platform, }; const response = await apiClient.post('/api/v1/auth/login', { email: credentials.email, password: credentials.password, deviceInfo, }); // Backend returns { success, data: { user, tokens } } const { data: responseData } = response.data; const { tokens, user: userData } = responseData; console.log('[AuthContext] Login response user data:', userData); console.log('[AuthContext] EULA fields:', { eulaAcceptedAt: userData.eulaAcceptedAt, eulaVersion: userData.eulaVersion, }); // Store tokens and deviceId tokenStorage.setTokens(tokens.accessToken, tokens.refreshToken, deviceId); setToken(tokens.accessToken); setUser(userData); router.push('/'); } catch (error: any) { console.error('Login failed:', error); throw new Error(error.response?.data?.message || 'Login failed'); } }; const register = async (data: RegisterData) => { try { const deviceId = generateDeviceFingerprint(); const deviceInfo = { deviceId, platform: 'web', model: navigator.userAgent, osVersion: navigator.platform, }; // Auto-detect timezone from user's device const detectedTimezone = Intl.DateTimeFormat().resolvedOptions().timeZone; const payload: any = { email: data.email, password: data.password, name: data.name, timezone: detectedTimezone || 'UTC', dateOfBirth: data.dateOfBirth, deviceInfo, }; // Add optional COPPA fields if provided if (data.parentalEmail) { payload.parentalEmail = data.parentalEmail; } if (data.coppaConsentGiven !== undefined) { payload.coppaConsentGiven = data.coppaConsentGiven; } console.log('[Auth] Registration payload:', JSON.stringify(payload, null, 2)); const response = await apiClient.post('/api/v1/auth/register', payload); // Backend returns { success, data: { user, family, tokens } } const { data: responseData } = response.data; const { tokens, user: userData, family: familyData } = responseData; if (!tokens?.accessToken || !tokens?.refreshToken) { throw new Error('Invalid response from server'); } const { accessToken, refreshToken } = tokens; // Add family data to user object (registration returns family separately) const userWithFamily = { ...userData, families: familyData ? [{ id: familyData.id, familyId: familyData.id, role: familyData.role || 'parent', }] : [], }; tokenStorage.setTokens(accessToken, refreshToken, deviceId); setToken(accessToken); setUser(userWithFamily); // Redirect to onboarding router.push('/onboarding'); } catch (error: any) { console.error('Registration failed:', error); throw new Error(error.response?.data?.message || error.message || 'Registration failed'); } }; const logout = async () => { try { await apiClient.post('/api/v1/auth/logout'); } catch (error) { console.error('Logout failed:', error); } finally { tokenStorage.clearTokens(); setUser(null); setToken(null); // Clear all localStorage and sessionStorage to remove cached data // This ensures a fresh start on next login if (typeof window !== 'undefined') { localStorage.clear(); sessionStorage.clear(); console.log('[AuthContext] Cleared all browser storage on logout'); } router.push('/login'); } }; const refreshUser = async () => { try { const response = await apiClient.get('/api/v1/auth/me'); setUser(response.data.data); } catch (error) { console.error('Failed to refresh user:', error); } }; return ( {children} ); }; export const useAuth = () => { const context = useContext(AuthContext); if (context === undefined) { throw new Error('useAuth must be used within an AuthProvider'); } return context; }; // Helper function to generate a simple device fingerprint function generateDeviceFingerprint(): string { const navigator = window.navigator; const screen = window.screen; const data = [ navigator.userAgent, navigator.language, screen.colorDepth, screen.width, screen.height, new Date().getTimezoneOffset(), ].join('|'); // Simple hash function let hash = 0; for (let i = 0; i < data.length; i++) { const char = data.charCodeAt(i); hash = ((hash << 5) - hash) + char; hash = hash & hash; } return hash.toString(36); }