'use client'; import React, { createContext, useCallback, useContext, useEffect, useMemo, useState } from 'react'; import { useTranslations } from 'next-intl'; import { useRouter } from '@/i18n/navigation'; import { authApi } from '@/lib/api/auth'; import { User, Organization } from '@/types/organization'; import { isAppLocale, getLocaleFromPathname } from '@/i18n/routing'; interface AuthContextType { user: User | null; organizations: Organization[]; currentOrganization: Organization | null; isLoading: boolean; isAuthReady: boolean; error: string | null; registerTrial: ( email: string, password: string, name: string, organizationName: string, organizationEmail: string, organizationType: 'CLINIC' | 'LAB' ) => Promise; login: (email: string, password: string) => Promise; logout: () => Promise; selectOrganization: (orgId: string) => Promise; createOrganization: ( organizationName: string, organizationEmail: string, organizationType: 'CLINIC' | 'LAB', planName?: string, ) => Promise; setUserLanguage: (language: string) => void; clearError: () => void; } const AuthContext = createContext(undefined); export function AuthProvider({ children }: { children: React.ReactNode }) { const t = useTranslations('auth'); const [user, setUser] = useState(null); const [organizations, setOrganizations] = useState([]); const [currentOrganization, setCurrentOrganization] = useState(null); const [isLoading, setIsLoading] = useState(false); const [isAuthReady, setIsAuthReady] = useState(false); // ✅ KEY FIX const [error, setError] = useState(null); const router = useRouter(); const normalizeProfilePayload = useCallback((payload: any): { user: User | null; organizations: Organization[] } => { const organizations = payload?.organizations || []; if (payload?.user) { return { user: { ...(payload.user as User), language: (payload.user as User).language ?? 'en', }, organizations, }; } if (payload?.id && payload?.email && payload?.name) { return { user: { id: payload.id, email: payload.email, name: payload.name, language: payload.language ?? 'en', }, organizations, }; } return { user: null, organizations }; }, []); const checkAuth = useCallback(async () => { try { setIsLoading(true); const response = await authApi.getProfile(); if (response.success) { const { user: userData, organizations: orgs } = normalizeProfilePayload(response.data); setUser(userData); setOrganizations(orgs); const storedOrgId = localStorage.getItem('currentOrganizationId'); if (storedOrgId && orgs.length > 0) { const org = orgs.find(o => o.id === storedOrgId); if (org) { setCurrentOrganization(org); // Ensure cookie token carries organizationId for org-scoped APIs. await authApi.selectOrganization(org.id); } else { setCurrentOrganization(null); } } else if (orgs.length === 1 && userData) { setCurrentOrganization(orgs[0]); localStorage.setItem('currentOrganizationId', orgs[0].id); // Keep JWT in sync with selected org even for single-org users. await authApi.selectOrganization(orgs[0].id); } else { setCurrentOrganization(null); } } } catch (err) { console.error('Auth check failed:', err); // Only clear state — DO NOT redirect here setUser(null); setOrganizations([]); setCurrentOrganization(null); } finally { setIsLoading(false); setIsAuthReady(true); } }, [normalizeProfilePayload]); useEffect(() => { void checkAuth(); }, [checkAuth]); const applyUrlLocaleToUser = useCallback(async (user: User): Promise => { if (typeof window === 'undefined') return user; const urlLocale = getLocaleFromPathname(window.location.pathname); if (!isAppLocale(urlLocale) || user.language === urlLocale) { return user; } try { await authApi.updateLanguage(urlLocale); } catch { /* keep URL locale in client state even if persistence fails */ } return { ...user, language: urlLocale }; }, []); // ✅ REGISTER const registerTrial = useCallback(async ( email: string, password: string, name: string, organizationName: string, organizationEmail: string, organizationType: 'CLINIC' | 'LAB' ) => { try { setIsLoading(true); setError(null); const response = await authApi.registerTrial({ email, password, name, organizationName, organizationEmail, organizationType, }); const userData = await applyUrlLocaleToUser(response.data.user); setUser(userData); setOrganizations(response.data.organizations); const orgs = response.data.organizations; if (orgs.length === 1) { const org = orgs[0]; await authApi.selectOrganization(org.id); setCurrentOrganization(org); localStorage.setItem('currentOrganizationId', org.id); router.push('/today'); } else { router.push('/select-organization'); } } catch (err: any) { setError(err.message || t('errorRegistrationFailed')); throw err; } finally { setIsLoading(false); } }, [applyUrlLocaleToUser, router, t]); // ✅ LOGIN const login = useCallback(async (email: string, password: string) => { try { setIsLoading(true); setError(null); const response = await authApi.login({ email, password }); const userData = await applyUrlLocaleToUser(response.data.user); setUser(userData); setOrganizations(response.data.organizations); const orgs = response.data.organizations; if (orgs.length === 1) { const org = orgs[0]; await authApi.selectOrganization(org.id); setCurrentOrganization(org); localStorage.setItem('currentOrganizationId', org.id); router.push('/today'); } else { router.push('/select-organization'); } } catch (err: any) { setError(err.message || t('errorLoginFailed')); throw err; } finally { setIsLoading(false); } }, [applyUrlLocaleToUser, router, t]); const logout = useCallback(async () => { try { // Important: clear auth cookies/session on the server first, // otherwise proxy may still treat the user as authenticated. await authApi.logout(); } catch (err) { console.error('Logout API failed:', err); } finally { localStorage.clear(); setUser(null); setOrganizations([]); setCurrentOrganization(null); setError(null); setIsAuthReady(true); router.replace('/'); router.refresh(); } }, [router]); const selectOrganization = useCallback(async (orgId: string) => { try { setIsLoading(true); const response = await authApi.selectOrganization(orgId); const { organization } = response.data; localStorage.setItem('currentOrganizationId', organization.id); setCurrentOrganization({ id: organization.id, name: organization.name, type: organization.type as Organization['type'], isOwner: Boolean((organization as { isOwner?: boolean }).isOwner), permissions: (organization as { permissions?: string[] }).permissions, plan: (organization as { plan?: Organization['plan'] }).plan, }); router.push('/today'); } catch (err: any) { setError(err.message); throw err; } finally { setIsLoading(false); } }, [router]); const createOrganization = useCallback(async ( organizationName: string, organizationEmail: string, organizationType: 'CLINIC' | 'LAB', planName?: string, ) => { try { setIsLoading(true); setError(null); const createResponse = await authApi.createOrganization({ organizationName, organizationEmail, organizationType, planName, }); const profileResponse = await authApi.getProfile(); if (profileResponse.success) { const { user: userData, organizations: orgs } = normalizeProfilePayload(profileResponse.data); setUser(userData); setOrganizations(orgs); } return createResponse.data.organization.id as string; } catch (err: any) { setError(err.message || t('errorCreateOrganization')); throw err; } finally { setIsLoading(false); } }, [normalizeProfilePayload, t]); const clearError = useCallback(() => setError(null), []); const setUserLanguage = useCallback((language: string) => { const normalized = isAppLocale(language) ? language : 'en'; setUser((current) => (current ? { ...current, language: normalized } : current)); }, []); const contextValue = useMemo( () => ({ user, organizations, currentOrganization, isLoading, isAuthReady, error, registerTrial, login, logout, selectOrganization, createOrganization, setUserLanguage, clearError, }), [ user, organizations, currentOrganization, isLoading, isAuthReady, error, registerTrial, login, logout, selectOrganization, createOrganization, setUserLanguage, clearError, ], ); return ( {children} ); } export const useAuth = () => { const context = useContext(AuthContext); if (!context) throw new Error('useAuth must be used within AuthProvider'); return context; };