'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(); if (!accessToken) { setIsLoading(false); return; } // Set token in state setToken(accessToken); const response = await apiClient.get('/api/v1/auth/me'); // Check if response has expected structure if (response.data?.data) { setUser(response.data.data); } else if (response.data?.user) { // Handle alternative response structure setUser(response.data.user); } else { throw new Error('Invalid response structure'); } } catch (error: any) { console.error('Auth check failed:', error); // Only clear tokens if it's an actual auth error (401, 403) if (error?.response?.status === 401 || error?.response?.status === 403) { tokenStorage.clearTokens(); setUser(null); setToken(null); } } finally { setIsLoading(false); } }; const login = async (credentials: LoginCredentials) => { try { const deviceInfo = { deviceId: generateDeviceFingerprint(), 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; tokenStorage.setTokens(tokens.accessToken, tokens.refreshToken); 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 deviceInfo = { deviceId: generateDeviceFingerprint(), 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); 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); 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); }