From 9988a776604c9d7c36bf2270dcb2e02b590ca09a Mon Sep 17 00:00:00 2001 From: Sreenand P K Date: Thu, 9 Jul 2026 21:57:02 +0530 Subject: [PATCH 1/4] chore:configured frontend to backend connection --- src/App.tsx | 10 +++ src/components/Login.tsx | 58 +++++++++++- src/components/VerifyEmail.tsx | 144 ++++++++++++++++++++++++++++++ src/constants/routes.constants.ts | 1 + src/features/auth/auth.service.ts | 8 +- vite.config.ts | 5 ++ 6 files changed, 218 insertions(+), 8 deletions(-) create mode 100644 src/components/VerifyEmail.tsx diff --git a/src/App.tsx b/src/App.tsx index 4692ac8..8ef59f3 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -6,11 +6,17 @@ import { Register } from '@/components/Register'; import { ForgotPassword } from '@/components/ForgotPassword'; import { ResetPassword } from '@/components/ResetPassword'; import { SuccessPage } from '@/components/SuccessPage'; +import { VerifyEmail } from '@/components/VerifyEmail'; import { DashboardPage } from '@/features/dashboard/DashboardPage'; import { AuthGuard } from '@/features/auth/AuthGuard'; import { useUserStore } from '@/stores/userStore'; import { ROUTES } from '@/constants/routes.constants'; +if (window.location.pathname === '/verify-email') { + const search = window.location.search; + window.location.replace(window.location.origin + '/#/verify-email' + search); +} + function AppContent() { const navigate = useNavigate(); const { logout, initAuth, user } = useUserStore(); @@ -82,6 +88,10 @@ function AppContent() { /> } /> + } + /> navigate(ROUTES.LOGIN)} />} diff --git a/src/components/Login.tsx b/src/components/Login.tsx index 697a542..e8e7b04 100644 --- a/src/components/Login.tsx +++ b/src/components/Login.tsx @@ -25,6 +25,20 @@ export const Login: React.FC = ({ const { setAccessToken, setUser } = useUserStore(); + const [resendStatus, setResendStatus] = useState<'idle' | 'loading' | 'success' | 'error'>('idle'); + + const handleResendVerification = async () => { + if (!email) return; + try { + setResendStatus('loading'); + await authService.resendVerification(email); + setResendStatus('success'); + } catch (err) { + console.error(err); + setResendStatus('error'); + } + }; + const handleSubmit = async (e: React.FormEvent) => { e.preventDefault(); setErrorMsg(null); @@ -43,7 +57,9 @@ export const Login: React.FC = ({ onLoginSuccess(); } catch (err: any) { console.error('Login failed', err); - if (err.response?.status === 401 || err.response?.status === 400) { + if (err.response?.status === 403) { + setErrorMsg(err.response.data?.detail || 'Please verify your email before logging in.'); + } else if (err.response?.status === 401 || err.response?.status === 400) { setErrorMsg('Invalid email or password.'); } else { setErrorMsg('An unexpected error occurred. Please try again.'); @@ -66,7 +82,7 @@ export const Login: React.FC = ({
= ({ }} role="alert" > - - {errorMsg} +
+ + {errorMsg} +
+ {errorMsg.toLowerCase().includes('verify') && ( +
+ + {resendStatus === 'success' && ( + + Verification link resent successfully! + + )} + {resendStatus === 'error' && ( + + Failed to resend. Please try again. + + )} +
+ )}
)} diff --git a/src/components/VerifyEmail.tsx b/src/components/VerifyEmail.tsx new file mode 100644 index 0000000..5a4bf6f --- /dev/null +++ b/src/components/VerifyEmail.tsx @@ -0,0 +1,144 @@ +import React, { useEffect, useState } from 'react'; +import { useSearchParams, useNavigate } from 'react-router-dom'; +import { Loader2, CheckCircle2, XCircle, ArrowRight } from 'lucide-react'; +import { AuthLayout } from '@/components/ui/AuthLayout/AuthLayout'; +import { authService } from '@/features/auth/auth.service'; +import { useUserStore } from '@/stores/userStore'; +import { ROUTES } from '@/constants/routes.constants'; + +export const VerifyEmail: React.FC = () => { + const [searchParams] = useSearchParams(); + const navigate = useNavigate(); + const { setAccessToken, setUser } = useUserStore(); + + const [status, setStatus] = useState<'loading' | 'success' | 'error'>('loading'); + const [errorMessage, setErrorMessage] = useState(null); + + useEffect(() => { + const token = searchParams.get('token'); + + if (!token) { + setStatus('error'); + setErrorMessage('Missing verification token. Please verify the URL.'); + return; + } + + const performVerification = async () => { + try { + const response = await authService.verifyEmail(token); + + // If the backend returns access token on successful verification, auto-login + if (response.access_token) { + setAccessToken(response.access_token); + const userProfile = await authService.getMe(); + setUser(userProfile); + setStatus('success'); + + // Redirect to dashboard after a short delay + setTimeout(() => { + navigate(ROUTES.DASHBOARD); + }, 2000); + } else { + // Fallback if no token returned (require user to sign in manually) + setStatus('success'); + } + } catch (err: any) { + console.error('Verification failed', err); + setStatus('error'); + setErrorMessage( + err.response?.data?.detail || 'Invalid or expired email verification token.' + ); + } + }; + + performVerification(); + }, [searchParams, setAccessToken, setUser, navigate]); + + return ( + +
+ {status === 'loading' && ( + <> +
+ +
+
+

Verifying Email

+

Please wait while we secure your account...

+
+ + )} + + {status === 'success' && ( + <> +
+ +
+
+

Email Verified!

+

+ Your account is verified. Logging you in and redirecting to the dashboard... +

+
+ + )} + + {status === 'error' && ( + <> +
+ +
+
+

Verification Failed

+

{errorMessage}

+
+ +
+ +
+ + )} +
+
+ ); +}; diff --git a/src/constants/routes.constants.ts b/src/constants/routes.constants.ts index 852a100..02daac8 100644 --- a/src/constants/routes.constants.ts +++ b/src/constants/routes.constants.ts @@ -4,6 +4,7 @@ export const ROUTES = { FORGOT_PASSWORD: '/forgot-password', RESET_PASSWORD: '/reset-password/:token', REGISTER: '/register', + VERIFY_EMAIL: '/verify-email', SUCCESS: '/success', DASHBOARD: '/dashboard', } as const; diff --git a/src/features/auth/auth.service.ts b/src/features/auth/auth.service.ts index d7e37cd..1035f2f 100644 --- a/src/features/auth/auth.service.ts +++ b/src/features/auth/auth.service.ts @@ -76,16 +76,16 @@ export const authService = { /** * Verify a user's email using a verification token. */ - async verifyEmail(token: string): Promise<{ message: string }> { - const response = await apiClient.post('/auth/verify-email', { token }); + async verifyEmail(token: string): Promise<{ message: string; access_token?: string; token_type?: string }> { + const response = await apiClient.post<{ message: string; access_token?: string; token_type?: string }>('/auth/verify-email', { token }); return response.data; }, /** * Resend email verification link. */ - async resendVerification(email: string): Promise<{ message: string }> { - const response = await apiClient.post('/auth/resend-verification', { email }); + async resendVerification(username_or_email: string): Promise<{ message: string }> { + const response = await apiClient.post('/auth/resend-verification', { username_or_email }); return response.data; }, diff --git a/vite.config.ts b/vite.config.ts index 6319347..5e2f7e0 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -6,6 +6,11 @@ import path from 'path'; export default defineConfig({ plugins: [react()], + // ── Dev Server ─────────────────────────────────────────────────────────────── + server: { + port: 3000, + }, + // ── Path Aliases ──────────────────────────────────────────────────────────── // Enables: import { X } from '@/features/...' instead of '../../../features/...' resolve: { From 3dce7968c77714bab9d1c3a46183f49d88bdde7e Mon Sep 17 00:00:00 2001 From: Sreenand P K Date: Fri, 10 Jul 2026 11:39:25 +0530 Subject: [PATCH 2/4] feat: implement Google login button, AuthCallback route, and clean redirect flow --- src/App.tsx | 31 ++++++++++---- src/components/AuthCallback.tsx | 39 +++++++++++++++++ src/components/Login.tsx | 71 +++++++++++++++++++++++++++++-- src/components/VerifyEmail.tsx | 37 +++++++++++++--- src/constants/routes.constants.ts | 1 + src/features/auth/auth.service.ts | 10 ++++- 6 files changed, 168 insertions(+), 21 deletions(-) create mode 100644 src/components/AuthCallback.tsx diff --git a/src/App.tsx b/src/App.tsx index 8ef59f3..053f1ab 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -7,16 +7,12 @@ import { ForgotPassword } from '@/components/ForgotPassword'; import { ResetPassword } from '@/components/ResetPassword'; import { SuccessPage } from '@/components/SuccessPage'; import { VerifyEmail } from '@/components/VerifyEmail'; +import { AuthCallback } from '@/components/AuthCallback'; import { DashboardPage } from '@/features/dashboard/DashboardPage'; import { AuthGuard } from '@/features/auth/AuthGuard'; import { useUserStore } from '@/stores/userStore'; import { ROUTES } from '@/constants/routes.constants'; -if (window.location.pathname === '/verify-email') { - const search = window.location.search; - window.location.replace(window.location.origin + '/#/verify-email' + search); -} - function AppContent() { const navigate = useNavigate(); const { logout, initAuth, user } = useUserStore(); @@ -24,7 +20,26 @@ function AppContent() { // Run once on app load to restore session via HttpOnly cookie if present useEffect(() => { + if (window.location.pathname === '/verify-email') { + const search = window.location.search; + window.location.replace(window.location.origin + '/#/verify-email' + search); + return; + } + + if (window.location.pathname === '/auth/callback') { + const hash = window.location.hash; + window.location.replace(window.location.origin + '/#/auth/callback' + hash); + return; + } + const initializeSession = async () => { + // Extract Google OAuth token from URL hash if present + const hash = window.location.hash; + const tokenMatch = hash.match(/token=([^&]+)/); + if (tokenMatch) { + useUserStore.getState().setAccessToken(tokenMatch[1]); + } + await initAuth(); setIsInitializing(false); }; @@ -88,10 +103,8 @@ function AppContent() { /> } /> - } - /> + } /> + } /> navigate(ROUTES.LOGIN)} />} diff --git a/src/components/AuthCallback.tsx b/src/components/AuthCallback.tsx new file mode 100644 index 0000000..cc8b9e7 --- /dev/null +++ b/src/components/AuthCallback.tsx @@ -0,0 +1,39 @@ +import React, { useEffect } from 'react'; +import { useNavigate } from 'react-router-dom'; +import { Loader2 } from 'lucide-react'; +import { AuthLayout } from '@/components/ui/AuthLayout/AuthLayout'; +import { authService } from '@/features/auth/auth.service'; +import { useUserStore } from '@/stores/userStore'; +import { ROUTES } from '@/constants/routes.constants'; + +export const AuthCallback: React.FC = () => { + const navigate = useNavigate(); + const isAuthenticated = useUserStore((s) => s.isAuthenticated); + + useEffect(() => { + if (isAuthenticated) { + // Clean up fragment from url and navigate to dashboard + window.history.replaceState(null, '', '/#/dashboard'); + navigate(ROUTES.DASHBOARD); + } else { + navigate(ROUTES.LOGIN); + } + }, [isAuthenticated, navigate]); + + return ( + +
+
+ +
+
+

Authenticating

+

Loading your Google trading profile...

+
+
+
+ ); +}; diff --git a/src/components/Login.tsx b/src/components/Login.tsx index e8e7b04..bba427a 100644 --- a/src/components/Login.tsx +++ b/src/components/Login.tsx @@ -25,7 +25,9 @@ export const Login: React.FC = ({ const { setAccessToken, setUser } = useUserStore(); - const [resendStatus, setResendStatus] = useState<'idle' | 'loading' | 'success' | 'error'>('idle'); + const [resendStatus, setResendStatus] = useState<'idle' | 'loading' | 'success' | 'error'>( + 'idle', + ); const handleResendVerification = async () => { if (!email) return; @@ -99,7 +101,14 @@ export const Login: React.FC = ({ {errorMsg} {errorMsg.toLowerCase().includes('verify') && ( -
+
{resendStatus === 'success' && ( @@ -224,6 +235,60 @@ export const Login: React.FC = ({ OR
+ +
Don't have an account?
-

Verification Failed

+

+ Verification Failed +

{errorMessage}

diff --git a/src/constants/routes.constants.ts b/src/constants/routes.constants.ts index 02daac8..ff0a582 100644 --- a/src/constants/routes.constants.ts +++ b/src/constants/routes.constants.ts @@ -5,6 +5,7 @@ export const ROUTES = { RESET_PASSWORD: '/reset-password/:token', REGISTER: '/register', VERIFY_EMAIL: '/verify-email', + AUTH_CALLBACK: '/auth/callback', SUCCESS: '/success', DASHBOARD: '/dashboard', } as const; diff --git a/src/features/auth/auth.service.ts b/src/features/auth/auth.service.ts index 1035f2f..87ec7e2 100644 --- a/src/features/auth/auth.service.ts +++ b/src/features/auth/auth.service.ts @@ -76,8 +76,14 @@ export const authService = { /** * Verify a user's email using a verification token. */ - async verifyEmail(token: string): Promise<{ message: string; access_token?: string; token_type?: string }> { - const response = await apiClient.post<{ message: string; access_token?: string; token_type?: string }>('/auth/verify-email', { token }); + async verifyEmail( + token: string, + ): Promise<{ message: string; access_token?: string; token_type?: string }> { + const response = await apiClient.post<{ + message: string; + access_token?: string; + token_type?: string; + }>('/auth/verify-email', { token }); return response.data; }, From 2cd06afd1595223a13675e160ab45932e461288d Mon Sep 17 00:00:00 2001 From: Sreenand P K Date: Fri, 10 Jul 2026 11:55:34 +0530 Subject: [PATCH 3/4] refactor: resolve review feedback on verification timeouts, unused vars, and hash params --- src/App.tsx | 13 ++++++---- src/components/AuthCallback.tsx | 1 - src/components/VerifyEmail.tsx | 42 ++++++++++++++++++++++++--------- 3 files changed, 39 insertions(+), 17 deletions(-) diff --git a/src/App.tsx b/src/App.tsx index 053f1ab..6f6cac2 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -28,16 +28,19 @@ function AppContent() { if (window.location.pathname === '/auth/callback') { const hash = window.location.hash; - window.location.replace(window.location.origin + '/#/auth/callback' + hash); + const cleanHash = hash.startsWith('#') ? hash.substring(1) : hash; + window.location.replace(window.location.origin + '/#/auth/callback?' + cleanHash); return; } const initializeSession = async () => { - // Extract Google OAuth token from URL hash if present + // Extract Google OAuth token from URL hash if present on the callback route const hash = window.location.hash; - const tokenMatch = hash.match(/token=([^&]+)/); - if (tokenMatch) { - useUserStore.getState().setAccessToken(tokenMatch[1]); + if (hash.includes('/auth/callback') && hash.includes('token=')) { + const tokenMatch = hash.match(/token=([^&]+)/); + if (tokenMatch) { + useUserStore.getState().setAccessToken(tokenMatch[1]); + } } await initAuth(); diff --git a/src/components/AuthCallback.tsx b/src/components/AuthCallback.tsx index cc8b9e7..07521a3 100644 --- a/src/components/AuthCallback.tsx +++ b/src/components/AuthCallback.tsx @@ -2,7 +2,6 @@ import React, { useEffect } from 'react'; import { useNavigate } from 'react-router-dom'; import { Loader2 } from 'lucide-react'; import { AuthLayout } from '@/components/ui/AuthLayout/AuthLayout'; -import { authService } from '@/features/auth/auth.service'; import { useUserStore } from '@/stores/userStore'; import { ROUTES } from '@/constants/routes.constants'; diff --git a/src/components/VerifyEmail.tsx b/src/components/VerifyEmail.tsx index d61703c..2f8536f 100644 --- a/src/components/VerifyEmail.tsx +++ b/src/components/VerifyEmail.tsx @@ -11,7 +11,9 @@ export const VerifyEmail: React.FC = () => { const navigate = useNavigate(); const { setAccessToken, setUser } = useUserStore(); - const [status, setStatus] = useState<'loading' | 'success' | 'error'>('loading'); + const [status, setStatus] = useState<'loading' | 'success' | 'success-manual' | 'error'>( + 'loading', + ); const [errorMessage, setErrorMessage] = useState(null); useEffect(() => { @@ -25,10 +27,8 @@ export const VerifyEmail: React.FC = () => { } const performVerification = async () => { - let isVerified = false; try { const response = await authService.verifyEmail(token); - isVerified = true; // If the backend returns access token on successful verification, auto-login if (response.access_token) { @@ -39,11 +39,9 @@ export const VerifyEmail: React.FC = () => { setUser(userProfile); } catch (profileErr) { console.error('Failed to fetch user profile after verification', profileErr); - // Even if profile fetch fails, the email is verified. Let them log in manually. - setStatus('error'); - setErrorMessage( - 'Email verified successfully, but failed to load profile. Please try logging in manually.', - ); + // If profile fetch fails, user is still verified. Clear token and prompt manual sign in. + setAccessToken(null); + setStatus('success-manual'); return; } @@ -55,7 +53,7 @@ export const VerifyEmail: React.FC = () => { }, 2000); } else { // Fallback if no token returned (require user to sign in manually) - setStatus('success'); + setStatus('success-manual'); } } catch (err: any) { console.error('Verification failed', err); @@ -93,7 +91,7 @@ export const VerifyEmail: React.FC = () => { )} - {status === 'success' && ( + {(status === 'success' || status === 'success-manual') && ( <>
{

Email Verified!

- Your account is verified. Logging you in and redirecting to the dashboard... + {status === 'success' + ? 'Your account is verified. Logging you in and redirecting to the dashboard...' + : 'Your account is verified. Please sign in to access your dashboard.'}

+ {status === 'success-manual' && ( +
+ +
+ )} )} From 9f9b768983ffdf4590955a83f46350da1e0a2033 Mon Sep 17 00:00:00 2001 From: Sreenand P K Date: Fri, 10 Jul 2026 12:18:51 +0530 Subject: [PATCH 4/4] fix: resolve path redirect mapping for old query parameter reset links --- src/App.tsx | 17 ++++++++++++++++- src/test/Login.test.tsx | 4 ++-- 2 files changed, 18 insertions(+), 3 deletions(-) diff --git a/src/App.tsx b/src/App.tsx index 6f6cac2..e28278e 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -33,12 +33,27 @@ function AppContent() { return; } + if (window.location.pathname === '/reset-password') { + const params = new URLSearchParams(window.location.search); + const token = params.get('token'); + if (token) { + window.location.replace(window.location.origin + '/#/reset-password/' + token); + return; + } + } + + if (window.location.pathname.startsWith('/reset-password/')) { + const token = window.location.pathname.substring(16); // '/reset-password/'.length === 16 + window.location.replace(window.location.origin + '/#/reset-password/' + token); + return; + } + const initializeSession = async () => { // Extract Google OAuth token from URL hash if present on the callback route const hash = window.location.hash; if (hash.includes('/auth/callback') && hash.includes('token=')) { const tokenMatch = hash.match(/token=([^&]+)/); - if (tokenMatch) { + if (tokenMatch && tokenMatch[1]) { useUserStore.getState().setAccessToken(tokenMatch[1]); } } diff --git a/src/test/Login.test.tsx b/src/test/Login.test.tsx index 35e99da..5068e22 100644 --- a/src/test/Login.test.tsx +++ b/src/test/Login.test.tsx @@ -15,10 +15,10 @@ vi.mock('../features/auth/auth.service', () => ({ /** * Helper to render the Login component within a Router context. */ -const renderLogin = (onSuccess = vi.fn(), onBack = vi.fn()) => +const renderLogin = (onSuccess = vi.fn()) => render( - + , );