diff --git a/frontend/src/context/authContext.tsx b/frontend/src/context/authContext.tsx index 46f51ab..c610698 100644 --- a/frontend/src/context/authContext.tsx +++ b/frontend/src/context/authContext.tsx @@ -10,6 +10,24 @@ import { useSetAtom } from 'jotai'; import { userAtom } from '@/state/userAtom'; import type { User } from '@/types/user'; +type ApiErrorPayload = { + message?: string; + error?: string; +}; + +type AuthPayload = { + accessToken?: string; + user?: User; +}; + +const safeJson = async (response: Response): Promise => { + try { + return await response.json(); + } catch { + return null; + } +}; + const baseURL = import.meta.env.VITE_BASE_URL; const USER_CACHE_KEY = 'userProfile'; @@ -18,23 +36,21 @@ interface AuthContextType { isAuthenticated: boolean; loading: boolean; error: string | null; - handleError: (error: string) => void; - login: (email: string, password: string) => Promise; - logout: () => void; - signup: (email: string, password: string) => Promise; - verifyEmail: (email: string, code: string) => Promise; - forgotPassword: (email: string) => Promise; + + login: (email: string, password: string) => Promise<{ success: boolean }>; + signup: (email: string, password: string) => Promise<{ success: boolean }>; + verifyEmail: (email: string, code: string) => Promise<{ success: boolean }>; + forgotPassword: (email: string) => Promise<{ success: boolean }>; confirmForgotPassword: ( email: string, code: string, newPassword: string - ) => Promise; - googleLogin: (idToken: string) => Promise; + ) => Promise<{ success: boolean }>; + googleLogin: (idToken: string) => Promise<{ success: boolean }>; + logout: () => void; } -export const AuthContext = createContext( - undefined -); +export const AuthContext = createContext(undefined); export const AuthProvider = ({ children }: { children: ReactNode }) => { const [token, setToken] = useState( @@ -42,74 +58,50 @@ export const AuthProvider = ({ children }: { children: ReactNode }) => { ); const [loading, setLoading] = useState(false); const [error, setError] = useState(null); + const navigate = useNavigate(); const setUser = useSetAtom(userAtom); - const handleError = (error: unknown) => { - const message = - error instanceof Error ? error.message : 'An unexpected error occurred'; - setError(message); - throw error; + const setApiError = (data: unknown, fallback: string): never => { + const payload = data as ApiErrorPayload | null; + throw new Error(payload?.message || payload?.error || fallback); }; const verifyToken = useCallback(async () => { const storedToken = localStorage.getItem('token'); if (!storedToken) return; + try { - const response = await fetch(`${baseURL}/verifyToken`, { + const res = await fetch(`${baseURL}/verifyToken`, { method: 'POST', headers: { Authorization: `Bearer ${storedToken}` }, }); - if (!response.ok) { - // Token is expired or invalid - clear it and redirect to login - localStorage.removeItem('token'); - setToken(null); - setUser(null); - navigate('/login'); - return; - } - setToken(storedToken); + const data = await safeJson(res); + if (!res.ok) setApiError(data, 'Token verification failed'); - // Fetch user data to populate userAtom - const userResponse = await fetch(`${baseURL}/user/fetchprofile`, { - method: 'GET', + const profileRes = await fetch(`${baseURL}/user/fetchprofile`, { headers: { Authorization: `Bearer ${storedToken}` }, }); - if (userResponse.ok) { - const userData = await userResponse.json(); - const normalizedUser: User = { - id: userData.id || userData._id, - email: userData.email, - displayName: userData.displayName || 'User', - bio: userData.bio || '', - rating: userData.rating || 1500, - rd: userData.rd || 350, - volatility: userData.volatility || 0.06, - lastRatingUpdate: - userData.lastRatingUpdate || new Date().toISOString(), - avatarUrl: - userData.avatarUrl || 'https://avatar.iran.liara.run/public/10', - twitter: userData.twitter, - instagram: userData.instagram, - linkedin: userData.linkedin, - password: '', - nickname: userData.nickname || 'User', - isVerified: userData.isVerified || false, - verificationCode: userData.verificationCode, - resetPasswordCode: userData.resetPasswordCode, - createdAt: userData.createdAt || new Date().toISOString(), - updatedAt: userData.updatedAt || new Date().toISOString(), - }; - setUser(normalizedUser); - localStorage.setItem(USER_CACHE_KEY, JSON.stringify(normalizedUser)); + const profileData = await safeJson(profileRes); + if (!profileRes.ok) setApiError(profileData, 'Failed to fetch profile'); + + const user = profileData as User; + if (!user?.email || (!user.id && !(user as any)._id)) { + throw new Error('Invalid user payload'); } - } catch (error) { - console.log('error', error); - logout(); + + setToken(storedToken); + setUser(user); + localStorage.setItem(USER_CACHE_KEY, JSON.stringify(user)); + } catch { + localStorage.removeItem('token'); + setToken(null); + setUser(null); + navigate('/login'); } - }, [setUser]); + }, [navigate, setUser]); useEffect(() => { verifyToken(); @@ -117,47 +109,33 @@ export const AuthProvider = ({ children }: { children: ReactNode }) => { const login = async (email: string, password: string) => { setLoading(true); + setError(null); + try { - const response = await fetch(`${baseURL}/login`, { + const res = await fetch(`${baseURL}/login`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ email, password }), }); - const data = await response.json(); - if (!response.ok) throw new Error(data.message || 'Login failed'); - - setToken(data.accessToken); - localStorage.setItem('token', data.accessToken); - // Set user details in userAtom based on the new User type - const normalizedUser: User = { - id: data.user?.id || data.user?._id || undefined, - email: data.user?.email || email, - displayName: data.user?.displayName || 'User', - bio: data.user?.bio || '', - rating: data.user?.rating || 1500, - rd: data.user?.rd || 350, // Default Glicko-2 RD value - volatility: data.user?.volatility || 0.06, // Default Glicko-2 volatility - lastRatingUpdate: - data.user?.lastRatingUpdate || new Date().toISOString(), - avatarUrl: - data.user?.avatarUrl || 'https://avatar.iran.liara.run/public/10', - twitter: data.user?.twitter || undefined, - instagram: data.user?.instagram || undefined, - linkedin: data.user?.linkedin || undefined, - password: '', // Password should not be stored in client-side state - nickname: data.user?.nickname || 'User', - isVerified: data.user?.isVerified || false, - verificationCode: data.user?.verificationCode || undefined, - resetPasswordCode: data.user?.resetPasswordCode || undefined, - createdAt: data.user?.createdAt || new Date().toISOString(), - updatedAt: data.user?.updatedAt || new Date().toISOString(), - }; - setUser(normalizedUser); - localStorage.setItem(USER_CACHE_KEY, JSON.stringify(normalizedUser)); + const data = await safeJson(res); + if (!res.ok) setApiError(data, 'Login failed'); + + const payload = data as AuthPayload; + if (!payload.accessToken || !payload.user?.email) { + throw new Error('Invalid auth payload'); + } + + setToken(payload.accessToken); + localStorage.setItem('token', payload.accessToken); + setUser(payload.user); + localStorage.setItem(USER_CACHE_KEY, JSON.stringify(payload.user)); navigate('/'); - } catch (error) { - handleError(error); + + return { success: true }; + } catch (err) { + setError(err instanceof Error ? err.message : 'Login failed'); + return { success: false }; } finally { setLoading(false); } @@ -166,18 +144,19 @@ export const AuthProvider = ({ children }: { children: ReactNode }) => { const signup = async (email: string, password: string) => { setLoading(true); try { - const response = await fetch(`${baseURL}/signup`, { + const res = await fetch(`${baseURL}/signup`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ email, password }), }); - if (!response.ok) { - const data = await response.json(); - throw new Error(data.message || 'Signup failed'); - } - } catch (error) { - handleError(error); + const data = await safeJson(res); + if (!res.ok) setApiError(data, 'Signup failed'); + + return { success: true }; + } catch (err) { + setError(err instanceof Error ? err.message : 'Signup failed'); + return { success: false }; } finally { setLoading(false); } @@ -186,52 +165,19 @@ export const AuthProvider = ({ children }: { children: ReactNode }) => { const verifyEmail = async (email: string, code: string) => { setLoading(true); try { - const response = await fetch(`${baseURL}/verifyEmail`, { + const res = await fetch(`${baseURL}/verifyEmail`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ email, confirmationCode: code }), }); - if (!response.ok) { - const data = await response.json(); - throw new Error(data.error || 'Verification failed'); - } + const data = await safeJson(res); + if (!res.ok) setApiError(data, 'Verification failed'); - const data = await response.json(); - - // User is now verified and logged in - if (data.accessToken) { - setToken(data.accessToken); - localStorage.setItem('token', data.accessToken); - - // Set user details - const normalizedUser: User = { - id: data.user?.id || data.user?._id || undefined, - email: data.user?.email || email, - displayName: data.user?.displayName || 'User', - bio: data.user?.bio || '', - rating: data.user?.rating || 1200, - rd: data.user?.rd || 350, - volatility: data.user?.volatility || 0.06, - lastRatingUpdate: data.user?.lastRatingUpdate || new Date().toISOString(), - avatarUrl: data.user?.avatarUrl || 'https://avatar.iran.liara.run/public/10', - twitter: data.user?.twitter || undefined, - instagram: data.user?.instagram || undefined, - linkedin: data.user?.linkedin || undefined, - password: '', - nickname: data.user?.nickname || 'User', - isVerified: true, - verificationCode: undefined, - resetPasswordCode: undefined, - createdAt: data.user?.createdAt || new Date().toISOString(), - updatedAt: data.user?.updatedAt || new Date().toISOString(), - }; - setUser(normalizedUser); - localStorage.setItem(USER_CACHE_KEY, JSON.stringify(normalizedUser)); - navigate('/'); - } - } catch (error) { - handleError(error); + return { success: true }; + } catch (err) { + setError(err instanceof Error ? err.message : 'Verification failed'); + return { success: false }; } finally { setLoading(false); } @@ -240,18 +186,19 @@ export const AuthProvider = ({ children }: { children: ReactNode }) => { const forgotPassword = async (email: string) => { setLoading(true); try { - const response = await fetch(`${baseURL}/forgotPassword`, { + const res = await fetch(`${baseURL}/forgotPassword`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ email }), }); - if (!response.ok) { - const data = await response.json(); - throw new Error(data.message || 'Password reset failed'); - } - } catch (error) { - handleError(error); + const data = await safeJson(res); + if (!res.ok) setApiError(data, 'Password reset failed'); + + return { success: true }; + } catch (err) { + setError(err instanceof Error ? err.message : 'Password reset failed'); + return { success: false }; } finally { setLoading(false); } @@ -264,18 +211,19 @@ export const AuthProvider = ({ children }: { children: ReactNode }) => { ) => { setLoading(true); try { - const response = await fetch(`${baseURL}/confirmForgotPassword`, { + const res = await fetch(`${baseURL}/confirmForgotPassword`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ email, code, newPassword }), }); - if (!response.ok) { - const data = await response.json(); - throw new Error(data.message || 'Password update failed'); - } - } catch (error) { - handleError(error); + const data = await safeJson(res); + if (!res.ok) setApiError(data, 'Password update failed'); + + return { success: true }; + } catch (err) { + setError(err instanceof Error ? err.message : 'Password update failed'); + return { success: false }; } finally { setLoading(false); } @@ -284,47 +232,30 @@ export const AuthProvider = ({ children }: { children: ReactNode }) => { const googleLogin = async (idToken: string) => { setLoading(true); try { - const response = await fetch(`${baseURL}/googleLogin`, { + const res = await fetch(`${baseURL}/googleLogin`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ idToken }), }); - const data = await response.json(); - if (!response.ok) throw new Error(data.message || 'Google login failed'); - - setToken(data.accessToken); - localStorage.setItem('token', data.accessToken); - // Set user details in userAtom based on the new User type - const normalizedUser: User = { - id: data.user?.id || data.user?._id || undefined, - email: data.user?.email || 'googleuser@example.com', - displayName: data.user?.displayName || 'Google User', - bio: data.user?.bio || '', - rating: data.user?.rating || 1500, - rd: data.user?.rd || 350, - volatility: data.user?.volatility || 0.06, - lastRatingUpdate: - data.user?.lastRatingUpdate || new Date().toISOString(), - avatarUrl: - data.user?.avatarUrl || 'https://avatar.iran.liara.run/public/10', - twitter: data.user?.twitter || undefined, - instagram: data.user?.instagram || undefined, - linkedin: data.user?.linkedin || undefined, - password: '', - nickname: data.user?.nickname || 'Google User', - isVerified: data.user?.isVerified || true, // Google login often implies verified - verificationCode: data.user?.verificationCode || undefined, - resetPasswordCode: data.user?.resetPasswordCode || undefined, - createdAt: data.user?.createdAt || new Date().toISOString(), - updatedAt: data.user?.updatedAt || new Date().toISOString(), - }; - setUser(normalizedUser); - localStorage.setItem(USER_CACHE_KEY, JSON.stringify(normalizedUser)); - console.log('User after Google login:', data.user); + const data = await safeJson(res); + if (!res.ok) setApiError(data, 'Google login failed'); + + const payload = data as AuthPayload; + if (!payload.accessToken || !payload.user?.email) { + throw new Error('Invalid auth payload'); + } + + setToken(payload.accessToken); + localStorage.setItem('token', payload.accessToken); + setUser(payload.user); + localStorage.setItem(USER_CACHE_KEY, JSON.stringify(payload.user)); navigate('/'); - } catch (error) { - handleError(error); + + return { success: true }; + } catch (err) { + setError(err instanceof Error ? err.message : 'Google login failed'); + return { success: false }; } finally { setLoading(false); } @@ -332,9 +263,9 @@ export const AuthProvider = ({ children }: { children: ReactNode }) => { const logout = () => { setToken(null); + setUser(null); localStorage.removeItem('token'); localStorage.removeItem(USER_CACHE_KEY); - setUser(null); // Clear userAtom on logout navigate('/auth'); }; @@ -345,14 +276,13 @@ export const AuthProvider = ({ children }: { children: ReactNode }) => { isAuthenticated: !!token, loading, error, - handleError, login, - logout, signup, verifyEmail, forgotPassword, confirmForgotPassword, googleLogin, + logout, }} > {children}