diff --git a/src/App.tsx b/src/App.tsx index 64e2de9..6231a5b 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -5,6 +5,8 @@ import Home from './pages/Home'; import PluginDetail from './pages/PluginDetail'; import LoginPage from './pages/auth/LoginPage'; import RegisterPage from './pages/auth/RegisterPage'; +import ForgotPasswordPage from './pages/auth/ForgotPasswordPage'; +import ResetPasswordPage from './pages/auth/ResetPasswordPage'; import VerifyEmailPage from './pages/auth/VerifyEmailPage'; import CheckEmailPage from './pages/auth/CheckEmailPage'; import ProfilePage from './pages/ProfilePage'; @@ -120,6 +122,8 @@ const App = () => ( } /> } /> } /> + } /> + } /> } /> } /> }> diff --git a/src/pages/auth/ForgotPasswordPage.tsx b/src/pages/auth/ForgotPasswordPage.tsx new file mode 100644 index 0000000..d8551e0 --- /dev/null +++ b/src/pages/auth/ForgotPasswordPage.tsx @@ -0,0 +1,291 @@ +import React, { useState } from 'react'; +import { Link } from 'react-router-dom'; +import { useTranslation } from 'react-i18next'; +import api from '../../api'; + +const ForgotPasswordPage = () => { + const { t } = useTranslation(); + const [email, setEmail] = useState(''); + const [error, setError] = useState(''); + const [message, setMessage] = useState(''); + const [isSubmitting, setIsSubmitting] = useState(false); + const [emailTouched, setEmailTouched] = useState(false); + const [emailValid, setEmailValid] = useState(null); + + const validateEmail = (email: string) => + Boolean(String(email).toLowerCase().match(/^[^\s@]+@[^\s@]+\.[^\s@]+$/)); + + const handleEmailChange = (e: React.ChangeEvent) => { + const val = e.target.value; + setEmail(val); + if (emailTouched) { + setEmailValid(val === '' ? null : validateEmail(val)); + } + }; + + const handleEmailBlur = () => { + setEmailTouched(true); + setEmailValid(email === '' ? null : validateEmail(email)); + }; + + const handleSubmit = async (e: React.FormEvent) => { + e.preventDefault(); + + setEmailTouched(true); + const isEmailValid = validateEmail(email); + setEmailValid(isEmailValid); + + if (!isEmailValid) { + setError('Please enter a valid email address.'); + return; + } + + setIsSubmitting(true); + setError(''); + setMessage(''); + try { + await api.post('/auth/forgot-password', { email }); + setMessage('If an account exists with that email, a reset link has been sent.'); + } catch (err: any) { + setError(err.response?.data?.error || 'Something went wrong. Please try again.'); + } finally { + setIsSubmitting(false); + } + }; + + const emailIsInvalid = emailTouched && emailValid === false; + const emailIsValid = emailTouched && emailValid === true; + + return ( + <> + + + + + + Reset your password + Enter your email and we'll send you a link to reset your password. + + + + {error && {error}} + {message && {message}} + + + {t('auth.email')} + + + {emailIsInvalid && ( + + + + + )} + {emailIsValid && ( + + + + + )} + + {emailIsInvalid && ( + Please enter a valid email address + )} + + + + {isSubmitting ? <>Sending...> : 'Send reset link'} + + + + + + + Remembered your password? Sign in + + + + > + ); +}; + +export default ForgotPasswordPage; \ No newline at end of file diff --git a/src/pages/auth/LoginPage.tsx b/src/pages/auth/LoginPage.tsx index ea94dee..8b05527 100644 --- a/src/pages/auth/LoginPage.tsx +++ b/src/pages/auth/LoginPage.tsx @@ -352,7 +352,12 @@ const LoginPage = () => { - {t('auth.password')} + + {t('auth.password')} + + Forgot password? + + { + const [searchParams] = useSearchParams(); + const token = searchParams.get('token'); + const navigate = useNavigate(); + + const [password, setPassword] = useState(''); + const [confirmPassword, setConfirmPassword] = useState(''); + const [error, setError] = useState(''); + const [isSubmitting, setIsSubmitting] = useState(false); + + const handleSubmit = async (e: React.FormEvent) => { + e.preventDefault(); + + if (!token) { + setError('Invalid reset link. Please request a new one.'); + return; + } + + if (password.length < 8) { + setError('Password must be at least 8 characters long.'); + return; + } + + if (password !== confirmPassword) { + setError('Passwords do not match.'); + return; + } + + setIsSubmitting(true); + setError(''); + try { + await api.post('/auth/reset-password', { token, new_password: password }); + alert('Password has been successfully reset. You can now log in with your new password.'); + navigate('/login'); + } catch (err: any) { + setError(err.response?.data?.error || 'Something went wrong. Please try again.'); + } finally { + setIsSubmitting(false); + } + }; + + if (!token) { + return ( + + + + Invalid Link + This password reset link is invalid or has expired. + + + Request a new link + + + + ); + } + + return ( + <> + + + + + + Set new password + Choose a new, strong password for your account. + + + + {error && {error}} + + + New Password + setPassword(e.target.value)} + required + disabled={isSubmitting} + /> + + + + Confirm New Password + setConfirmPassword(e.target.value)} + required + disabled={isSubmitting} + /> + + + + {isSubmitting ? <>Updating...> : 'Update password'} + + + + + > + ); +}; + +export default ResetPasswordPage; \ No newline at end of file
Enter your email and we'll send you a link to reset your password.
Please enter a valid email address
This password reset link is invalid or has expired.
Choose a new, strong password for your account.