diff --git a/frontend/src/Pages/Authentication.tsx b/frontend/src/Pages/Authentication.tsx index eb117cfa..8c54c59e 100644 --- a/frontend/src/Pages/Authentication.tsx +++ b/frontend/src/Pages/Authentication.tsx @@ -1,9 +1,10 @@ -import React, { useState } from 'react'; +import React, { useState, useContext } from 'react'; import { Button } from '@/components/ui/button'; import { LoginForm, SignUpForm, OTPVerificationForm, ForgotPasswordForm, ResetPasswordForm } from './Authentication/forms.tsx'; import { Link, useLocation } from 'react-router-dom'; import DebateCover from '../assets/DebateCover4.svg'; import { ThemeToggle } from '@/components/ThemeToggle'; +import { AuthContext } from '../context/authContext'; const LeftSection = () => (
@@ -105,6 +106,7 @@ const RightSection: React.FC = ({ const Authentication = () => { const location = useLocation(); + const authContext = useContext(AuthContext); // Extend authMode to include 'resetPassword' const [authMode, setAuthMode] = useState< 'login' | 'signup' | 'otpVerification' | 'forgotPassword' | 'resetPassword' @@ -115,33 +117,41 @@ const Authentication = () => { const [infoMessage, setInfoMessage] = useState(''); const toggleAuthMode = () => { + authContext?.clearError(); + setInfoMessage(''); setAuthMode((prevMode) => (prevMode === 'login' ? 'signup' : 'login')); }; // Start OTP verification process const startOtpVerification = (email: string) => { + authContext?.clearError(); setEmailForOTP(email); setAuthMode('otpVerification'); }; // Handle successful OTP verification const handleOtpVerified = () => { + authContext?.clearError(); setAuthMode('login'); }; // Start forgot password process const startForgotPassword = () => { + authContext?.clearError(); + setInfoMessage(''); setAuthMode('forgotPassword'); }; // Start reset password process const startResetPassword = (email: string) => { + authContext?.clearError(); setEmailForPasswordReset(email); setAuthMode('resetPassword'); }; // Handle successful password reset const handlePasswordReset = () => { + authContext?.clearError(); setInfoMessage('Your password was successfully reset. You can now log in.'); setAuthMode('login'); }; diff --git a/frontend/src/Pages/Authentication/forms.tsx b/frontend/src/Pages/Authentication/forms.tsx index 221257f9..771bd75d 100644 --- a/frontend/src/Pages/Authentication/forms.tsx +++ b/frontend/src/Pages/Authentication/forms.tsx @@ -20,32 +20,41 @@ export const LoginForm: React.FC = ({ startForgotPassword, infoM throw new Error('LoginForm must be used within an AuthProvider'); } - const { login, googleLogin, error, loading } = authContext; + const { login, googleLogin, error, loading, clearError } = authContext; const [localError, setLocalError] = useState(null); + useEffect(() => { + clearError(); + }, []); -const MIN_PASSWORD_LENGTH = 8; -const handleSubmit = async (e: React.FormEvent) => { - e.preventDefault(); - if (password.length < MIN_PASSWORD_LENGTH) { - setLocalError(`Password must be at least ${MIN_PASSWORD_LENGTH} characters`); - return; - } - setLocalError(null); - await login(email, password); -}; - - - + const MIN_PASSWORD_LENGTH = 8; + const handleSubmit = async (e: React.FormEvent) => { + e.preventDefault(); + clearError(); + if (password.length < MIN_PASSWORD_LENGTH) { + setLocalError(`Password must be at least ${MIN_PASSWORD_LENGTH} characters`); + return; + } + setLocalError(null); + try { + await login(email, password); + } catch { + // Handled by authContext error state + } + }; -const handleGoogleLogin = useCallback( - (response: { credential: string; select_by: string }) => { - const idToken = response.credential; - googleLogin(idToken); - }, - [googleLogin] -); + const handleGoogleLogin = useCallback( + async (response: { credential: string; select_by: string }) => { + try { + const idToken = response.credential; + await googleLogin(idToken); + } catch { + // Handled by authContext error state + } + }, + [googleLogin] + ); useEffect(() => { const google = window.google; if (!google?.accounts) { @@ -79,21 +88,28 @@ const handleGoogleLogin = useCallback( type="email" placeholder="name@example.com" value={email} - onChange={(e) => setEmail(e.target.value)} + onChange={(e) => { + setEmail(e.target.value); + if (error) clearError(); + }} className="mb-2 dark:border-white" /> setPassword(e.target.value)} + onChange={(e) => { + setPassword(e.target.value); + if (error) clearError(); + if (localError) setLocalError(null); + }} className="mb-1 dark:border-white" /> {localError && (

{localError}

-)} + )}
= ({ startOtpVerification }) throw new Error('SignUpForm must be used within an AuthProvider'); } - const { signup, googleLogin, error, loading } = authContext; + const { signup, googleLogin, error, loading, clearError } = authContext; + + useEffect(() => { + clearError(); + }, []); const handleSubmit = async (e: React.FormEvent) => { e.preventDefault(); @@ -144,18 +164,26 @@ export const SignUpForm: React.FC = ({ startOtpVerification }) authContext.handleError('Passwords do not match'); return; } - - await signup(email, password); - startOtpVerification(email); + clearError(); + try { + await signup(email, password); + startOtpVerification(email); + } catch { + // Handled by authContext error state + } }; - const handleGoogleLogin = useCallback( - (response: { credential: string; select_by: string }) => { - const idToken = response.credential; - googleLogin(idToken); - }, - [googleLogin] -); + const handleGoogleLogin = useCallback( + async (response: { credential: string; select_by: string }) => { + try { + const idToken = response.credential; + await googleLogin(idToken); + } catch { + // Handled by authContext error state + } + }, + [googleLogin] + ); useEffect(() => { @@ -190,21 +218,30 @@ export const SignUpForm: React.FC = ({ startOtpVerification }) type="email" placeholder="name@example.com" value={email} - onChange={(e) => setEmail(e.target.value)} + onChange={(e) => { + setEmail(e.target.value); + if (error) clearError(); + }} className="mb-2 dark:border-white" /> setPassword(e.target.value)} + onChange={(e) => { + setPassword(e.target.value); + if (error) clearError(); + }} className="mb-2 dark:border-white" /> setConfirmPassword(e.target.value)} + onChange={(e) => { + setConfirmPassword(e.target.value); + if (error) clearError(); + }} className="mb-4 dark:border-white" />
@@ -240,12 +277,21 @@ export const OTPVerificationForm: React.FC = ({ email, throw new Error('OTPVerificationForm must be used within an AuthProvider'); } - const { verifyEmail, error, loading } = authContext; + const { verifyEmail, error, loading, clearError } = authContext; + + useEffect(() => { + clearError(); + }, []); const handleSubmit = async (e: React.FormEvent) => { e.preventDefault(); - await verifyEmail(email, otp); - handleOtpVerified(); + clearError(); + try { + await verifyEmail(email, otp); + handleOtpVerified(); + } catch { + // Handled by authContext error state + } }; return ( @@ -256,7 +302,10 @@ export const OTPVerificationForm: React.FC = ({ email, setOtp(e.target.value)} + onChange={(e) => { + setOtp(e.target.value); + if (error) clearError(); + }} placeholder="Enter OTP" className="w-full mb-4 dark:border-white" /> @@ -293,7 +342,8 @@ export const ForgotPasswordForm: React.FC = ({ }); if (!response.ok) { - setError('Failed to send reset password code. Please try again.'); + const data = await response.json().catch(() => ({})); + setError(data.error || data.message || 'Failed to send reset password code. Please try again.'); return; } @@ -311,7 +361,10 @@ export const ForgotPasswordForm: React.FC = ({ setEmail(e.target.value)} + onChange={(e) => { + setEmail(e.target.value); + if (error) setError(''); + }} placeholder="name@example.com" className="w-full mb-4 dark:border-white" /> @@ -340,7 +393,11 @@ export const ResetPasswordForm: React.FC = ({ email, han throw new Error('ResetPasswordForm must be used within an AuthProvider'); } - const { confirmForgotPassword, login, error, loading } = authContext; + const { confirmForgotPassword, login, error, loading, clearError } = authContext; + + useEffect(() => { + clearError(); + }, []); const handleSubmit = async (e: React.FormEvent) => { e.preventDefault(); @@ -350,9 +407,14 @@ export const ResetPasswordForm: React.FC = ({ email, han return; } - await confirmForgotPassword(email, code, newPassword); - await login(email, newPassword); - handlePasswordReset(); + try { + await confirmForgotPassword(email, code, newPassword); + clearError(); + await login(email, newPassword); + handlePasswordReset(); + } catch { + // Handled by authContext error state + } }; return ( @@ -362,21 +424,30 @@ export const ResetPasswordForm: React.FC = ({ email, han setCode(e.target.value)} + onChange={(e) => { + setCode(e.target.value); + if (error) clearError(); + }} placeholder="Enter Code" className="w-full mb-2 border dark:border-white" /> setNewPassword(e.target.value)} + onChange={(e) => { + setNewPassword(e.target.value); + if (error) clearError(); + }} placeholder="New Password" className="w-full mb-2 dark:border-white" /> setConfirmNewPassword(e.target.value)} + onChange={(e) => { + setConfirmNewPassword(e.target.value); + if (error) clearError(); + }} placeholder="Confirm New Password" className="w-full mb-4 dark:border-white" /> diff --git a/frontend/src/context/authContext.tsx b/frontend/src/context/authContext.tsx index 37ad105b..cf38c778 100644 --- a/frontend/src/context/authContext.tsx +++ b/frontend/src/context/authContext.tsx @@ -19,7 +19,8 @@ interface AuthContextType { isAuthenticated: boolean; loading: boolean; error: string | null; - handleError: (error: string) => void; + handleError: (error: unknown) => void; + clearError: () => void; login: (email: string, password: string) => Promise; logout: () => void; signup: (email: string, password: string) => Promise; @@ -46,12 +47,19 @@ export const AuthProvider = ({ children }: { children: ReactNode }) => { const navigate = useNavigate(); const setUser = useSetAtom(userAtom); - const handleError = (error: unknown) => { + const clearError = useCallback(() => { + setError(null); + }, []); + + const handleError = useCallback((error: unknown) => { const message = - error instanceof Error ? error.message : 'An unexpected error occurred'; + typeof error === 'string' + ? error + : error instanceof Error + ? error.message + : 'An unexpected error occurred'; setError(message); - throw error; - }; + }, []); let currentRequest = 0; const verifyToken = useCallback(async () => { @@ -136,6 +144,7 @@ const verifyToken = useCallback(async () => { const login = async (email: string, password: string) => { setLoading(true); + setError(null); try { const response = await fetch(`${baseURL}/login`, { method: 'POST', @@ -143,8 +152,8 @@ const verifyToken = useCallback(async () => { body: JSON.stringify({ email, password }), }); - const data = await response.json(); - if (!response.ok) throw new Error(data.message || 'Login failed'); + const data = await response.json().catch(() => ({})); + if (!response.ok) throw new Error(data.error || data.message || 'Login failed'); setToken(data.accessToken); localStorage.setItem('token', data.accessToken); @@ -177,6 +186,7 @@ const verifyToken = useCallback(async () => { navigate('/'); } catch (error) { handleError(error); + throw error; } finally { setLoading(false); } @@ -184,6 +194,7 @@ const verifyToken = useCallback(async () => { const signup = async (email: string, password: string) => { setLoading(true); + setError(null); try { const response = await fetch(`${baseURL}/signup`, { method: 'POST', @@ -192,11 +203,12 @@ const verifyToken = useCallback(async () => { }); if (!response.ok) { - const data = await response.json(); - throw new Error(data.message || 'Signup failed'); + const data = await response.json().catch(() => ({})); + throw new Error(data.error || data.message || 'Signup failed'); } } catch (error) { handleError(error); + throw error; } finally { setLoading(false); } @@ -204,6 +216,7 @@ const verifyToken = useCallback(async () => { const verifyEmail = async (email: string, code: string) => { setLoading(true); + setError(null); try { const response = await fetch(`${baseURL}/verifyEmail`, { method: 'POST', @@ -211,13 +224,11 @@ const verifyToken = useCallback(async () => { body: JSON.stringify({ email, confirmationCode: code }), }); + const data = await response.json().catch(() => ({})); if (!response.ok) { - const data = await response.json(); - throw new Error(data.error || 'Verification failed'); + throw new Error(data.error || data.message || 'Verification failed'); } - const data = await response.json(); - // User is now verified and logged in if (data.accessToken) { setToken(data.accessToken); @@ -251,6 +262,7 @@ const verifyToken = useCallback(async () => { } } catch (error) { handleError(error); + throw error; } finally { setLoading(false); } @@ -258,6 +270,7 @@ const verifyToken = useCallback(async () => { const forgotPassword = async (email: string) => { setLoading(true); + setError(null); try { const response = await fetch(`${baseURL}/forgotPassword`, { method: 'POST', @@ -266,11 +279,12 @@ const verifyToken = useCallback(async () => { }); if (!response.ok) { - const data = await response.json(); - throw new Error(data.message || 'Password reset failed'); + const data = await response.json().catch(() => ({})); + throw new Error(data.error || data.message || 'Password reset failed'); } } catch (error) { handleError(error); + throw error; } finally { setLoading(false); } @@ -282,6 +296,7 @@ const verifyToken = useCallback(async () => { newPassword: string ) => { setLoading(true); + setError(null); try { const response = await fetch(`${baseURL}/confirmForgotPassword`, { method: 'POST', @@ -290,11 +305,12 @@ const verifyToken = useCallback(async () => { }); if (!response.ok) { - const data = await response.json(); - throw new Error(data.message || 'Password update failed'); + const data = await response.json().catch(() => ({})); + throw new Error(data.error || data.message || 'Password update failed'); } } catch (error) { handleError(error); + throw error; } finally { setLoading(false); } @@ -302,6 +318,7 @@ const verifyToken = useCallback(async () => { const googleLogin = async (idToken: string) => { setLoading(true); + setError(null); try { const response = await fetch(`${baseURL}/googleLogin`, { method: 'POST', @@ -309,8 +326,8 @@ const verifyToken = useCallback(async () => { body: JSON.stringify({ idToken }), }); - const data = await response.json(); - if (!response.ok) throw new Error(data.message || 'Google login failed'); + const data = await response.json().catch(() => ({})); + if (!response.ok) throw new Error(data.error || data.message || 'Google login failed'); setToken(data.accessToken); localStorage.setItem('token', data.accessToken); @@ -344,12 +361,14 @@ const verifyToken = useCallback(async () => { navigate('/'); } catch (error) { handleError(error); + throw error; } finally { setLoading(false); } }; const logout = () => { + setError(null); setToken(null); localStorage.removeItem('token'); localStorage.removeItem(USER_CACHE_KEY); @@ -365,6 +384,7 @@ const verifyToken = useCallback(async () => { loading, error, handleError, + clearError, login, logout, signup,