From 14c82939df7251b179a20609123117c4ea5deee4 Mon Sep 17 00:00:00 2001 From: Soorej S Date: Wed, 28 Jan 2026 13:53:14 +0530 Subject: [PATCH 1/3] fix(frontend): safely handle empty or invalid auth JSON responses --- frontend/src/context/authContext.tsx | 316 +++++++++++---------------- 1 file changed, 123 insertions(+), 193 deletions(-) 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} From a241c2ff030147f433559c4affd983c76e42dbd0 Mon Sep 17 00:00:00 2001 From: Soorej S Date: Sat, 31 Jan 2026 17:38:37 +0530 Subject: [PATCH 2/3] fix(frontend): gate OTP flow on successful signup and verification --- frontend/src/Pages/Authentication/forms.tsx | 93 ++++++++++++++++----- 1 file changed, 74 insertions(+), 19 deletions(-) diff --git a/frontend/src/Pages/Authentication/forms.tsx b/frontend/src/Pages/Authentication/forms.tsx index 221257f..b29bb5c 100644 --- a/frontend/src/Pages/Authentication/forms.tsx +++ b/frontend/src/Pages/Authentication/forms.tsx @@ -129,6 +129,8 @@ export const SignUpForm: React.FC = ({ startOtpVerification }) const [password, setPassword] = useState(''); const [passwordVisible, setPasswordVisible] = useState(false); const [confirmPassword, setConfirmPassword] = useState(''); + const [localError, setLocalError] = useState(null); + const [isSubmitting, setIsSubmitting] = useState(false); const authContext = useContext(AuthContext); if (!authContext) { @@ -139,14 +141,45 @@ export const SignUpForm: React.FC = ({ startOtpVerification }) const handleSubmit = async (e: React.FormEvent) => { e.preventDefault(); + setLocalError(null); + setIsSubmitting(true); + + // Basic validation + if (!email || !password || !confirmPassword) { + setLocalError('All fields are required'); + setIsSubmitting(false); + return; + } + + if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email)) { + setLocalError('Please enter a valid email address'); + setIsSubmitting(false); + return; + } + + if (password.length < 8) { + setLocalError('Password must be at least 8 characters long'); + setIsSubmitting(false); + return; + } if (password !== confirmPassword) { - authContext.handleError('Passwords do not match'); + setLocalError('Passwords do not match'); + setIsSubmitting(false); return; } - await signup(email, password); - startOtpVerification(email); + try { + const { success } = await signup(email, password); + if (success) { + startOtpVerification(email); + } + } catch (err) { + console.error('Signup error:', err); + setLocalError('Failed to create account. Please try again.'); + } finally { + setIsSubmitting(false); + } }; const handleGoogleLogin = useCallback( @@ -156,8 +189,6 @@ export const SignUpForm: React.FC = ({ startOtpVerification }) }, [googleLogin] ); - - useEffect(() => { const google = window.google; if (!google?.accounts) { @@ -186,41 +217,55 @@ export const SignUpForm: React.FC = ({ startOtpVerification }) return (
+ {localError &&

{localError}

} + {error &&

{error}

} + setEmail(e.target.value)} className="mb-2 dark:border-white" + disabled={isSubmitting} /> + setPassword(e.target.value)} className="mb-2 dark:border-white" + disabled={isSubmitting} /> + setConfirmPassword(e.target.value)} - className="mb-4 dark:border-white" + className="mb-2 dark:border-white" + disabled={isSubmitting} /> -
+ +
setPasswordVisible(e.target.checked)} + disabled={isSubmitting} />
-
show password
+
Show password
- {error &&

{error}

} -
@@ -244,8 +289,10 @@ export const OTPVerificationForm: React.FC = ({ email, const handleSubmit = async (e: React.FormEvent) => { e.preventDefault(); - await verifyEmail(email, otp); - handleOtpVerified(); + const { success } = await verifyEmail(email, otp); + if (success) { + handleOtpVerified(); + } }; return ( @@ -334,6 +381,7 @@ export const ResetPasswordForm: React.FC = ({ email, han const [newPassword, setNewPassword] = useState(''); const [confirmNewPassword, setConfirmNewPassword] = useState(''); const [passwordVisible, setPasswordVisible] = useState(false); + const [localError, setLocalError] = useState(null); const authContext = useContext(AuthContext); if (!authContext) { @@ -344,15 +392,21 @@ export const ResetPasswordForm: React.FC = ({ email, han const handleSubmit = async (e: React.FormEvent) => { e.preventDefault(); + setLocalError(null); if (newPassword !== confirmNewPassword) { - authContext.handleError('Passwords do not match'); + setLocalError('Passwords do not match'); return; } - await confirmForgotPassword(email, code, newPassword); - await login(email, newPassword); - handlePasswordReset(); + try { + await confirmForgotPassword(email, code, newPassword); + await login(email, newPassword); + handlePasswordReset(); + } catch (err) { + console.error('Password reset error:', err); + setLocalError('Failed to reset password. Please try again.'); + } }; return ( @@ -391,6 +445,7 @@ export const ResetPasswordForm: React.FC = ({ email, han
show password
+ {localError &&

{localError}

} {error &&

{error}

}
@@ -289,10 +244,8 @@ export const OTPVerificationForm: React.FC = ({ email, const handleSubmit = async (e: React.FormEvent) => { e.preventDefault(); - const { success } = await verifyEmail(email, otp); - if (success) { - handleOtpVerified(); - } + await verifyEmail(email, otp); + handleOtpVerified(); }; return ( @@ -381,7 +334,6 @@ export const ResetPasswordForm: React.FC = ({ email, han const [newPassword, setNewPassword] = useState(''); const [confirmNewPassword, setConfirmNewPassword] = useState(''); const [passwordVisible, setPasswordVisible] = useState(false); - const [localError, setLocalError] = useState(null); const authContext = useContext(AuthContext); if (!authContext) { @@ -392,21 +344,15 @@ export const ResetPasswordForm: React.FC = ({ email, han const handleSubmit = async (e: React.FormEvent) => { e.preventDefault(); - setLocalError(null); if (newPassword !== confirmNewPassword) { - setLocalError('Passwords do not match'); + authContext.handleError('Passwords do not match'); return; } - try { - await confirmForgotPassword(email, code, newPassword); - await login(email, newPassword); - handlePasswordReset(); - } catch (err) { - console.error('Password reset error:', err); - setLocalError('Failed to reset password. Please try again.'); - } + await confirmForgotPassword(email, code, newPassword); + await login(email, newPassword); + handlePasswordReset(); }; return ( @@ -445,7 +391,6 @@ export const ResetPasswordForm: React.FC = ({ email, han
show password
- {localError &&

{localError}

} {error &&

{error}

}