diff --git a/frontend/service/api/auth.ts b/frontend/service/api/auth.ts index 088600f..02ba395 100644 --- a/frontend/service/api/auth.ts +++ b/frontend/service/api/auth.ts @@ -5,6 +5,10 @@ import { LoginRequest, SignupRequest, UserResponse, + ForgotPasswordRequest, + ForgotPasswordResponse, + ResetPasswordRequest, + ResetPasswordResponse, } from '../../types/auth.entity'; const getApiUrl = (endpoint: string) => { @@ -68,4 +72,38 @@ export const authApi = { }, }); }, + + forgotPassword: async (data: ForgotPasswordRequest): Promise => { + const response = await fetch(getApiUrl('/auth/forgot-password'), { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify(data), + }); + + if (!response.ok) { + const error = await response.json(); + throw new Error(error.message || 'Failed to send reset email'); + } + + return await response.json(); + }, + + resetPassword: async (data: ResetPasswordRequest): Promise => { + const response = await fetch(getApiUrl('/auth/reset-password'), { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify(data), + }); + + if (!response.ok) { + const error = await response.json(); + throw new Error(error.message || 'Failed to reset password'); + } + + return await response.json(); + }, }; diff --git a/frontend/src/app/reset-password/page.tsx b/frontend/src/app/reset-password/page.tsx new file mode 100644 index 0000000..c8c8858 --- /dev/null +++ b/frontend/src/app/reset-password/page.tsx @@ -0,0 +1,190 @@ +'use client'; + +import { useEffect, useState, Suspense } from 'react'; +import { useSearchParams, useRouter } from 'next/navigation'; +import { authApi } from '../../../service/api/auth'; + +function ResetPasswordContent() { + const searchParams = useSearchParams(); + const router = useRouter(); + const token = searchParams.get('token'); + + const [formData, setFormData] = useState({ + newPassword: '', + confirmPassword: '', + }); + const [error, setError] = useState(''); + const [success, setSuccess] = useState(''); + const [isLoading, setIsLoading] = useState(false); + + useEffect(() => { + if (!token) { + setError('Invalid or missing reset token'); + } + }, [token]); + + const handleSubmit = async (e: React.FormEvent) => { + e.preventDefault(); + setError(''); + setSuccess(''); + + if (!token) { + setError('Invalid or missing reset token'); + return; + } + + if (formData.newPassword !== formData.confirmPassword) { + setError('Passwords do not match'); + return; + } + + if (formData.newPassword.length < 8) { + setError('Password must be at least 8 characters long'); + return; + } + + setIsLoading(true); + + try { + const response = await authApi.resetPassword({ + token, + newPassword: formData.newPassword, + }); + setSuccess(response.message); + setFormData({ newPassword: '', confirmPassword: '' }); + + // Redirect to home after 2 seconds with success message + setTimeout(() => { + router.push('/?reset=success'); + }, 2000); + } catch (err) { + setError(err instanceof Error ? err.message : 'An error occurred'); + } finally { + setIsLoading(false); + } + }; + + const handleBackToLogin = () => { + router.push('/'); + }; + + return ( +
+
+
+

+ Reset Password +

+ + {success && ( +
+ {success} +
Redirecting to login...
+
+ )} + + {error && ( +
+ {error} +
+ )} + + {!success && ( +
+
+ Enter your new password below. +
+ + {/* New Password input */} +
+ + setFormData({ ...formData, newPassword: e.target.value }) + } + required + minLength={8} + className="h-14 bg-white/5 border-white/10 text-white placeholder:text-gray-400 rounded-xl focus:border-secondary focus:ring-secondary/20 w-full px-2" + /> +
+ + {/* Confirm Password input */} +
+ + setFormData({ ...formData, confirmPassword: e.target.value }) + } + required + minLength={8} + className="h-14 bg-white/5 border-white/10 text-white placeholder:text-gray-400 rounded-xl focus:border-secondary focus:ring-secondary/20 w-full px-2" + /> +
+ + {/* Password requirements */} +
+ Password must be at least 8 characters long. +
+ + + + +
+ +
+
+ )} + + {success && ( +
+ +
+ )} +
+
+
+ ); +} + +export default function ResetPasswordPage() { + return ( + +
Loading...
+ + }> + +
+ ); +} diff --git a/frontend/src/components/LoginModal.tsx b/frontend/src/components/LoginModal.tsx deleted file mode 100644 index 945d075..0000000 --- a/frontend/src/components/LoginModal.tsx +++ /dev/null @@ -1,215 +0,0 @@ -'use client'; - -import type React from 'react'; -import { useState } from 'react'; -import { useAuth } from '../../context/AuthContext'; - -type ModalProps = { - closeModal: () => void; -}; -export function LoginForm({ closeModal }: ModalProps) { - const { login, signup } = useAuth(); - const [isSignup, setIsSignup] = useState(false); - const [formData, setFormData] = useState({ - email: '', - password: '', - username: '', - }); - const [rememberMe, setRememberMe] = useState(false); - const [error, setError] = useState(''); - const [isLoading, setIsLoading] = useState(false); - - const handleSubmit = async (e: React.FormEvent) => { - e.preventDefault(); - setError(''); - setIsLoading(true); - - try { - if (isSignup) { - await signup(formData.email, formData.password, formData.username); - } else { - await login(formData.email, formData.password); - closeModal(); - } - } catch (err) { - setError(err instanceof Error ? err.message : 'An error occurred'); - } finally { - setIsLoading(false); - } - }; - - return ( -
-
- {/* Title */} -
-

- {isSignup ? 'Sign Up' : 'Login'} -

- -
- {/* Error Message */} - {error && ( -
- {error} -
- )} - -
- {/* Email input */} -
- - setFormData({ ...formData, email: e.target.value }) - } - required - className="h-14 bg-white/5 border-white/10 text-white placeholder:text-gray-400 rounded-xl focus:border-secondary focus:ring-secondary/20 w-full px-2" - /> -
- - {/* Username input (only for signup) */} - {isSignup && ( -
- - setFormData({ ...formData, username: e.target.value }) - } - required - className="h-14 bg-white/5 border-white/10 text-white placeholder:text-gray-400 rounded-xl focus:border-secondary focus:ring-secondary/20 w-full px-2" - /> -
- )} - - {/* Password input */} -
- - setFormData({ ...formData, password: e.target.value }) - } - required - className="h-14 bg-white/5 border-white/10 text-white placeholder:text-gray-400 rounded-xl focus:border-secondary focus:ring-secondary/20 w-full px-2" - /> -
- - {/* Remember Me & Forgot Password (only for login) */} - {!isSignup && ( -
-
- setRememberMe(e.target.checked)} - className="border-white/30 data-[state=checked]:bg-secondary data-[state=checked]:border-secondary" - /> - -
- -
- )} - - {/* Login/Signup button */} - - - {/* Switch between Login/Signup */} -
- - {isSignup - ? 'Already have an account? ' - : 'Already have an account? '} - - -
-
- - {/* Divider */} -
-
-
-
-
-
- - Continue with - -
-
-
- - {/* Google Login button */} - -
-
- ); -} diff --git a/frontend/src/components/auth/AuthModal.tsx b/frontend/src/components/auth/AuthModal.tsx new file mode 100644 index 0000000..bfcb38b --- /dev/null +++ b/frontend/src/components/auth/AuthModal.tsx @@ -0,0 +1,88 @@ +'use client'; + +import type React from 'react'; +import { useState, useEffect } from 'react'; +import { LoginForm } from './LoginForm'; +import { SignupForm } from './SignupForm'; +import { ForgotPasswordForm } from './ForgotPasswordForm'; + +type AuthMode = 'login' | 'signup' | 'forgot-password'; + +type AuthModalProps = { + closeModal: () => void; + initialMode?: AuthMode; + onModeChange?: (mode: AuthMode) => void; + resetSuccess?: boolean; +}; + +export function AuthModal({ closeModal, initialMode = 'login', onModeChange, resetSuccess }: AuthModalProps) { + const [mode, setMode] = useState(initialMode); + + // Sync internal mode state when initialMode prop changes + useEffect(() => { + setMode(initialMode); + }, [initialMode]); + + const handleSwitchToSignup = () => { + setMode('signup'); + onModeChange?.('signup'); + }; + + const handleSwitchToLogin = () => { + setMode('login'); + onModeChange?.('login'); + }; + + const handleSwitchToForgotPassword = () => { + setMode('forgot-password'); + onModeChange?.('forgot-password'); + }; + + const handleSuccess = () => { + closeModal(); + }; + + return ( +
+
+ {/* Title */} +
+

+ {mode === 'signup' ? 'Sign Up' : mode === 'forgot-password' ? 'Forgot Password' : 'Login'} +

+ +
+ + {/* Conditional Form Rendering */} + {mode === 'login' ? ( + + ) : mode === 'signup' ? ( + + ) : ( + + )} +
+
+ ); +} diff --git a/frontend/src/components/auth/ForgotPasswordForm.tsx b/frontend/src/components/auth/ForgotPasswordForm.tsx new file mode 100644 index 0000000..7ecefa4 --- /dev/null +++ b/frontend/src/components/auth/ForgotPasswordForm.tsx @@ -0,0 +1,94 @@ +'use client'; + +import type React from 'react'; +import { useState } from 'react'; +import { authApi } from '../../../service/api/auth'; + +type ForgotPasswordFormProps = { + onSwitchToLogin: () => void; +}; + +export function ForgotPasswordForm({ onSwitchToLogin }: ForgotPasswordFormProps) { + const [email, setEmail] = useState(''); + const [error, setError] = useState(''); + const [success, setSuccess] = useState(''); + const [isLoading, setIsLoading] = useState(false); + + const handleSubmit = async (e: React.FormEvent) => { + e.preventDefault(); + setError(''); + setSuccess(''); + setIsLoading(true); + + try { + const response = await authApi.forgotPassword({ email }); + setSuccess(response.message); + setEmail(''); + } catch (err) { + setError(err instanceof Error ? err.message : 'An error occurred'); + } finally { + setIsLoading(false); + } + }; + + return ( +
+ {success && ( +
+ {success} +
+ )} + + {error && ( +
+ {error} +
+ )} + +
+
+ Enter your email address and we'll send you a link to reset your password. +
+ + {/* Email input */} +
+ setEmail(e.target.value)} + required + className="h-14 bg-white/5 border-white/10 text-white placeholder:text-gray-400 rounded-xl focus:border-secondary focus:ring-secondary/20 w-full px-2" + /> +
+ + {/* Send Reset Email button */} + + + {/* Back to Login */} +
+ + Remember your password? + + +
+
+
+ ); +} diff --git a/frontend/src/components/auth/LoginForm.tsx b/frontend/src/components/auth/LoginForm.tsx new file mode 100644 index 0000000..29d1445 --- /dev/null +++ b/frontend/src/components/auth/LoginForm.tsx @@ -0,0 +1,184 @@ +'use client'; + +import type React from 'react'; +import { useState } from 'react'; +import { useAuth } from '../../../context/AuthContext'; + +type LoginFormProps = { + onSwitchToSignup: () => void; + onSwitchToForgotPassword: () => void; + onSuccess: () => void; + resetSuccess?: boolean; +}; + +export function LoginForm({ onSwitchToSignup, onSwitchToForgotPassword, onSuccess, resetSuccess }: LoginFormProps) { + const { login } = useAuth(); + const [formData, setFormData] = useState({ + email: '', + password: '', + }); + const [rememberMe, setRememberMe] = useState(false); + const [error, setError] = useState(''); + const [isLoading, setIsLoading] = useState(false); + + const handleSubmit = async (e: React.FormEvent) => { + e.preventDefault(); + setError(''); + setIsLoading(true); + + try { + await login(formData.email, formData.password); + onSuccess(); + } catch (err) { + setError(err instanceof Error ? err.message : 'An error occurred'); + } finally { + setIsLoading(false); + } + }; + + const handleGoogleLogin = () => { + // Add Google OAuth logic here + console.log('Google login clicked'); + }; + + return ( +
+ {/* Success Message for Password Reset */} + {resetSuccess && ( +
+ Password has been reset successfully! You can now login with your new password. +
+ )} + + {/* Error Message */} + {error && ( +
+ {error} +
+ )} + +
+ {/* Email input */} +
+ + setFormData({ ...formData, email: e.target.value }) + } + required + className="h-14 bg-white/5 border-white/10 text-white placeholder:text-gray-400 rounded-xl focus:border-secondary focus:ring-secondary/20 w-full px-2" + /> +
+ + {/* Password input */} +
+ + setFormData({ ...formData, password: e.target.value }) + } + required + className="h-14 bg-white/5 border-white/10 text-white placeholder:text-gray-400 rounded-xl focus:border-secondary focus:ring-secondary/20 w-full px-2" + /> +
+ + {/* Remember Me & Forgot Password */} +
+
+ setRememberMe(e.target.checked)} + className="border-white/30 data-[state=checked]:bg-secondary data-[state=checked]:border-secondary" + /> + +
+ +
+ + {/* Login button */} + + + {/* Switch to Signup */} +
+ + Don't have an account? + + +
+
+ + {/* Divider */} +
+
+
+
+
+
+ + Continue with + +
+
+
+ + {/* Google Login button */} + +
+ ); +} diff --git a/frontend/src/components/auth/SignupForm.tsx b/frontend/src/components/auth/SignupForm.tsx new file mode 100644 index 0000000..e6dc6ad --- /dev/null +++ b/frontend/src/components/auth/SignupForm.tsx @@ -0,0 +1,165 @@ +'use client'; + +import type React from 'react'; +import { useState } from 'react'; +import { useAuth } from '../../../context/AuthContext'; + +type SignupFormProps = { + onSwitchToLogin: () => void; + onSuccess: () => void; +}; + +export function SignupForm({ onSwitchToLogin, onSuccess }: SignupFormProps) { + const { signup } = useAuth(); + const [formData, setFormData] = useState({ + email: '', + password: '', + username: '', + }); + const [error, setError] = useState(''); + const [isLoading, setIsLoading] = useState(false); + + const handleSubmit = async (e: React.FormEvent) => { + e.preventDefault(); + setError(''); + setIsLoading(true); + + try { + await signup(formData.email, formData.password, formData.username); + onSuccess(); + } catch (err) { + setError(err instanceof Error ? err.message : 'An error occurred'); + } finally { + setIsLoading(false); + } + }; + + const handleGoogleLogin = () => { + // Add Google OAuth logic here + console.log('Google signup clicked'); + }; + + return ( +
+ {/* Error Message */} + {error && ( +
+ {error} +
+ )} + +
+ {/* Email input */} +
+ + setFormData({ ...formData, email: e.target.value }) + } + required + className="h-14 bg-white/5 border-white/10 text-white placeholder:text-gray-400 rounded-xl focus:border-secondary focus:ring-secondary/20 w-full px-2" + /> +
+ + {/* Username input */} +
+ + setFormData({ ...formData, username: e.target.value }) + } + required + className="h-14 bg-white/5 border-white/10 text-white placeholder:text-gray-400 rounded-xl focus:border-secondary focus:ring-secondary/20 w-full px-2" + /> +
+ + {/* Password input */} +
+ + setFormData({ ...formData, password: e.target.value }) + } + required + className="h-14 bg-white/5 border-white/10 text-white placeholder:text-gray-400 rounded-xl focus:border-secondary focus:ring-secondary/20 w-full px-2" + /> +
+ + {/* Signup button */} + + + {/* Switch to Login */} +
+ + Already have an account? + + +
+
+ + {/* Divider */} +
+
+
+
+
+
+ + Continue with + +
+
+
+ + {/* Google Login button */} + +
+ ); +} diff --git a/frontend/src/components/header.tsx b/frontend/src/components/header.tsx index 95f3f58..7445d2d 100644 --- a/frontend/src/components/header.tsx +++ b/frontend/src/components/header.tsx @@ -1,7 +1,7 @@ 'use client'; -import { useMemo, useState } from 'react'; +import { useMemo, useState, useEffect } from 'react'; import Link from 'next/link'; -import { usePathname } from 'next/navigation'; +import { usePathname, useSearchParams } from 'next/navigation'; import { createAvatar } from '@dicebear/core'; import { adventurer } from '@dicebear/collection'; import Image from 'next/image'; @@ -13,7 +13,7 @@ import { PopoverTrigger, } from '@/components/ui/popover'; import { useAuth } from '../../context/AuthContext'; -import { LoginForm } from './LoginModal'; +import { AuthModal } from './auth/AuthModal'; const navItems = [ { label: 'Home', href: '/' }, @@ -24,8 +24,11 @@ const navItems = [ export default function Header() { const pathname = usePathname(); + const searchParams = useSearchParams(); const { isAuthenticated, user } = useAuth(); const [modal, setModal] = useState(false); + const [lastAuthMode, setLastAuthMode] = useState<'login' | 'signup' | 'forgot-password'>('login'); + const [resetSuccess, setResetSuccess] = useState(false); const isActive = (href: string) => pathname === href; const avatar = useMemo(() => { return createAvatar(adventurer, { @@ -34,6 +37,20 @@ export default function Header() { }).toDataUri(); }, [user]); + // Check for password reset success + useEffect(() => { + const resetParam = searchParams.get('reset'); + if (resetParam === 'success' && !isAuthenticated) { + setResetSuccess(true); + setModal(true); + setLastAuthMode('login'); + // Clear the URL parameter + const url = new URL(window.location.href); + url.searchParams.delete('reset'); + window.history.replaceState({}, '', url.toString()); + } + }, [searchParams, isAuthenticated]); + return (