Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
41 changes: 41 additions & 0 deletions src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ 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 { AuthCallback } from '@/components/AuthCallback';
import { DashboardPage } from '@/features/dashboard/DashboardPage';
import { AuthGuard } from '@/features/auth/AuthGuard';
import { useUserStore } from '@/stores/userStore';
Expand All @@ -18,7 +20,44 @@ 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;
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.

if (window.location.pathname === '/auth/callback') {
const hash = window.location.hash;
const cleanHash = hash.startsWith('#') ? hash.substring(1) : hash;
window.location.replace(window.location.origin + '/#/auth/callback?' + cleanHash);
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;
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.

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 && tokenMatch[1]) {
useUserStore.getState().setAccessToken(tokenMatch[1]);
}
}

await initAuth();
setIsInitializing(false);
};
Expand Down Expand Up @@ -82,6 +121,8 @@ function AppContent() {
/>
}
/>
<Route path={ROUTES.VERIFY_EMAIL} element={<VerifyEmail />} />
<Route path={ROUTES.AUTH_CALLBACK} element={<AuthCallback />} />
<Route
path={ROUTES.FORGOT_PASSWORD}
element={<ForgotPassword onBackToLogin={() => navigate(ROUTES.LOGIN)} />}
Expand Down
38 changes: 38 additions & 0 deletions src/components/AuthCallback.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import React, { useEffect } from 'react';
import { useNavigate } from 'react-router-dom';
import { Loader2 } from 'lucide-react';
import { AuthLayout } from '@/components/ui/AuthLayout/AuthLayout';
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 (
<AuthLayout>
<div
className="auth-form-card no-card"
style={{ alignItems: 'center', textAlign: 'center', maxWidth: '380px' }}
>
<div style={{ marginBottom: '16px', color: 'var(--color-brand-teal)' }}>
<Loader2 size={48} className="animate-spin" />
</div>
<div className="form-header" style={{ alignItems: 'center' }}>
<h2 className="form-title">Authenticating</h2>
<p className="form-subtitle">Loading your Google trading profile...</p>
</div>
</div>
</AuthLayout>
);
};
123 changes: 119 additions & 4 deletions src/components/Login.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,22 @@ export const Login: React.FC<LoginProps> = ({

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);
Expand All @@ -43,7 +59,9 @@ export const Login: React.FC<LoginProps> = ({
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.');
Expand All @@ -66,7 +84,7 @@ export const Login: React.FC<LoginProps> = ({
<div
style={{
display: 'flex',
alignItems: 'center',
flexDirection: 'column',
gap: '8px',
padding: '12px',
backgroundColor: 'rgba(239, 68, 68, 0.1)',
Expand All @@ -78,8 +96,51 @@ export const Login: React.FC<LoginProps> = ({
}}
role="alert"
>
<AlertCircle size={16} />
<span>{errorMsg}</span>
<div style={{ display: 'flex', alignItems: 'center', gap: '8px' }}>
<AlertCircle size={16} style={{ flexShrink: 0 }} />
<span>{errorMsg}</span>
</div>
{errorMsg.toLowerCase().includes('verify') && (
<div
style={{
display: 'flex',
flexDirection: 'column',
gap: '4px',
paddingLeft: '24px',
}}
>
<button
type="button"
onClick={handleResendVerification}
disabled={resendStatus === 'loading'}
style={{
alignSelf: 'flex-start',
fontSize: '0.75rem',
color: 'var(--color-brand-teal)',
background: 'none',
border: 'none',
cursor: 'pointer',
padding: 0,
textDecoration: 'underline',
fontWeight: 600,
}}
>
{resendStatus === 'loading'
? 'Sending new link...'
: 'Resend verification email'}
</button>
{resendStatus === 'success' && (
<span style={{ fontSize: '0.75rem', color: '#10b981' }}>
Verification link resent successfully!
</span>
)}
{resendStatus === 'error' && (
<span style={{ fontSize: '0.75rem', color: '#ef4444' }}>
Failed to resend. Please try again.
</span>
)}
</div>
)}
</div>
)}

Expand Down Expand Up @@ -174,6 +235,60 @@ export const Login: React.FC<LoginProps> = ({
<span className="divider-text">OR</span>
</div>

<button
type="button"
onClick={() => {
const API_URL = import.meta.env.VITE_API_BASE_URL || 'http://localhost:8000';
window.location.href = `${API_URL}/auth/oauth2/google/login`;
}}
style={{
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
gap: '12px',
width: '100%',
padding: '12px',
backgroundColor: '#ffffff',
border: '1px solid #e2e8f0',
borderRadius: '6px',
color: '#1e293b',
fontSize: '0.9rem',
fontWeight: 600,
cursor: 'pointer',
marginTop: '16px',
marginBottom: '16px',
transition: 'all 0.2s',
}}
onMouseEnter={(e) => {
e.currentTarget.style.backgroundColor = '#f8fafc';
e.currentTarget.style.borderColor = '#cbd5e1';
}}
onMouseLeave={(e) => {
e.currentTarget.style.backgroundColor = '#ffffff';
e.currentTarget.style.borderColor = '#e2e8f0';
}}
>
<svg width="18" height="18" viewBox="0 0 24 24">
<path
fill="#4285F4"
d="M22.56 12.25c0-.78-.07-1.53-.2-2.25H12v4.26h5.92c-.26 1.37-1.04 2.53-2.21 3.31v2.77h3.57c2.08-1.92 3.28-4.74 3.28-8.09z"
/>
<path
fill="#34A853"
d="M12 23c2.97 0 5.46-.98 7.28-2.66l-3.57-2.77c-.98.66-2.23 1.06-3.71 1.06-2.86 0-5.29-1.93-6.16-4.53H2.18v2.84C3.99 20.53 7.7 23 12 23z"
/>
<path
fill="#FBBC05"
d="M5.84 14.09c-.22-.66-.35-1.36-.35-2.09s.13-1.43.35-2.09V7.06H2.18C1.43 8.55 1 10.22 1 12s.43 3.45 1.18 4.94l2.85-2.22.81-.63z"
/>
<path
fill="#EA4335"
d="M12 5.38c1.62 0 3.06.56 4.21 1.64l3.15-3.15C17.45 2.09 14.97 1 12 1 7.7 1 3.99 3.47 2.18 7.06l3.66 2.84c.87-2.6 3.3-4.52 6.16-4.52z"
/>
</svg>
<span>Continue with Google</span>
</button>

<div className="login-footer">
<span className="footer-grey-text">Don&apos;t have an account? </span>
<button
Expand Down
Loading
Loading