diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 54eebb9..8bf4162 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -20,6 +20,8 @@ const Compare = lazy(() => import('./pages/Compare').then(m => ({ default: m.Com const Reports = lazy(() => import('./pages/Reports').then(m => ({ default: m.Reports }))); const Login = lazy(() => import('./pages/Login').then(m => ({ default: m.Login }))); const Register = lazy(() => import('./pages/Register').then(m => ({ default: m.Register }))); +const ForgotPassword = lazy(() => import('./pages/ForgotPassword').then(m => ({ default: m.ForgotPassword }))); +const ResetPassword = lazy(() => import('./pages/ResetPassword').then(m => ({ default: m.ResetPassword }))); const ApiKeys = lazy(() => import('./pages/ApiKeys').then(m => ({ default: m.ApiKeys }))); const AnalyticsDashboard = lazy(() => import('./pages/AnalyticsDashboard').then(m => ({ default: m.AnalyticsDashboard }))); const NotFound = lazy(() => import('./pages/NotFound').then(m => ({ default: m.NotFound }))); @@ -70,6 +72,8 @@ function App() { {/* Public routes */} } /> } /> + } /> + } /> {/* Protected routes with layout */} }> diff --git a/frontend/src/contexts/AuthContext.tsx b/frontend/src/contexts/AuthContext.tsx index 05d99d3..ce7a5fa 100644 --- a/frontend/src/contexts/AuthContext.tsx +++ b/frontend/src/contexts/AuthContext.tsx @@ -23,6 +23,8 @@ interface AuthContextType { login: (email: string, password: string) => Promise; logout: () => void; register: (email: string, password: string, fullName: string) => Promise; + requestPasswordReset: (email: string) => Promise; + resetPassword: (token: string, newPassword: string) => Promise; } const AuthContext = createContext(undefined); @@ -151,13 +153,58 @@ export function AuthProvider({ children }: { children: React.ReactNode }) { localStorage.removeItem(REFRESH_TOKEN_KEY); localStorage.removeItem(USER_KEY); delete api.defaults.headers.common['Authorization']; - - showToast({ - title: 'Logged out', - description: 'See you soon!' + + showToast({ + title: 'Logged out', + description: 'See you soon!' }); }, []); + const requestPasswordReset = useCallback(async (email: string): Promise => { + try { + await api.post('/auth/reset-password-request', { email }); + + showToast({ + title: 'Reset email sent', + description: 'Check your email for password reset instructions' + }); + + return true; + } catch (error: any) { + const message = error.response?.data?.detail || 'Failed to send reset email'; + showToast({ + title: 'Request failed', + description: message, + variant: 'destructive' + }); + return false; + } + }, []); + + const resetPassword = useCallback(async (token: string, newPassword: string): Promise => { + try { + await api.post('/auth/reset-password', { + token, + new_password: newPassword + }); + + showToast({ + title: 'Password reset successful', + description: 'You can now log in with your new password' + }); + + return true; + } catch (error: any) { + const message = error.response?.data?.detail || 'Failed to reset password'; + showToast({ + title: 'Reset failed', + description: message, + variant: 'destructive' + }); + return false; + } + }, []); + return ( {children} diff --git a/frontend/src/pages/ForgotPassword.tsx b/frontend/src/pages/ForgotPassword.tsx new file mode 100644 index 0000000..61bc550 --- /dev/null +++ b/frontend/src/pages/ForgotPassword.tsx @@ -0,0 +1,131 @@ +import { useState } from 'react'; +import { Link } from 'react-router-dom'; +import { useAuth } from '@/contexts/AuthContext'; +import { Button } from '@/components/ui/button'; +import { Input } from '@/components/ui/input'; +import { Label } from '@/components/ui/label'; +import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from '@/components/ui/card'; +import { Cloud, Loader2, CheckCircle } from 'lucide-react'; + +export function ForgotPassword() { + const [email, setEmail] = useState(''); + const [isSubmitting, setIsSubmitting] = useState(false); + const [isSuccess, setIsSuccess] = useState(false); + const { requestPasswordReset } = useAuth(); + + const handleSubmit = async (e: React.FormEvent) => { + e.preventDefault(); + setIsSubmitting(true); + + const success = await requestPasswordReset(email); + if (success) { + setIsSuccess(true); + } + + setIsSubmitting(false); + }; + + if (isSuccess) { + return ( +
+
+
+ + mockupAWS +
+ + + +
+ +
+ Check your email + + We've sent password reset instructions to {email} + +
+ +

+ If you don't see the email, check your spam folder or make sure the email address is correct. +

+
+ + + + Back to sign in + + +
+
+
+ ); + } + + return ( +
+
+
+ + mockupAWS +
+ + + + Reset password + + Enter your email address and we'll send you instructions to reset your password + + +
+ +
+ + setEmail(e.target.value)} + required + autoComplete="email" + autoFocus + /> +
+
+ + +

+ Remember your password?{' '} + + Sign in + +

+
+
+
+
+
+ ); +} diff --git a/frontend/src/pages/Login.tsx b/frontend/src/pages/Login.tsx index bd50aaa..a4a08fb 100644 --- a/frontend/src/pages/Login.tsx +++ b/frontend/src/pages/Login.tsx @@ -58,14 +58,9 @@ export function Login() {
- { - e.preventDefault(); - // TODO: Implement forgot password - alert('Forgot password - Coming soon'); - }} > Forgot password? diff --git a/frontend/src/pages/ResetPassword.tsx b/frontend/src/pages/ResetPassword.tsx new file mode 100644 index 0000000..d491454 --- /dev/null +++ b/frontend/src/pages/ResetPassword.tsx @@ -0,0 +1,205 @@ +import { useState, useEffect } from 'react'; +import { Link, useSearchParams, useNavigate } from 'react-router-dom'; +import { useAuth } from '@/contexts/AuthContext'; +import { Button } from '@/components/ui/button'; +import { Input } from '@/components/ui/input'; +import { Label } from '@/components/ui/label'; +import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from '@/components/ui/card'; +import { Cloud, Loader2, CheckCircle, AlertCircle } from 'lucide-react'; + +export function ResetPassword() { + const [searchParams] = useSearchParams(); + const navigate = useNavigate(); + const token = searchParams.get('token'); + + const [password, setPassword] = useState(''); + const [confirmPassword, setConfirmPassword] = useState(''); + const [isSubmitting, setIsSubmitting] = useState(false); + const [isSuccess, setIsSuccess] = useState(false); + const [error, setError] = useState(null); + const { resetPassword } = useAuth(); + + // Redirect if no token + useEffect(() => { + if (!token) { + setError('Invalid or missing reset token. Please request a new password reset.'); + } + }, [token]); + + const handleSubmit = async (e: React.FormEvent) => { + e.preventDefault(); + setError(null); + + if (password !== confirmPassword) { + setError('Passwords do not match'); + return; + } + + if (password.length < 8) { + setError('Password must be at least 8 characters long'); + return; + } + + setIsSubmitting(true); + + const success = await resetPassword(token!, password); + if (success) { + setIsSuccess(true); + // Redirect to login after 3 seconds + setTimeout(() => { + navigate('/login'); + }, 3000); + } + + setIsSubmitting(false); + }; + + if (!token) { + return ( +
+
+
+ + mockupAWS +
+ + + +
+ +
+ Invalid Link + + {error} + +
+ + + + + + Back to sign in + + +
+
+
+ ); + } + + if (isSuccess) { + return ( +
+
+
+ + mockupAWS +
+ + + +
+ +
+ Password reset successful + + Your password has been reset successfully. You will be redirected to the login page in a few seconds. + +
+ + + + + +
+
+
+ ); + } + + return ( +
+
+
+ + mockupAWS +
+ + + + Set new password + + Enter your new password below + + +
+ + {error && ( +
+ {error} +
+ )} +
+ + setPassword(e.target.value)} + required + autoComplete="new-password" + autoFocus + /> +

+ Must be at least 8 characters long +

+
+
+ + setConfirmPassword(e.target.value)} + required + autoComplete="new-password" + /> +
+
+ + + + Back to sign in + + +
+
+
+
+ ); +}