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}
} + +
+ +
+ + {emailIsInvalid && ( + + + + + )} + {emailIsValid && ( + + + + + )} +
+ {emailIsInvalid && ( +

Please enter a valid email address

+ )} +
+ + +
+ +
+ +
+ 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 = () => {
- +
+ + + 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}
} + +
+ + setPassword(e.target.value)} + required + disabled={isSubmitting} + /> +
+ +
+ + setConfirmPassword(e.target.value)} + required + disabled={isSubmitting} + /> +
+ + +
+
+
+ + ); +}; + +export default ResetPasswordPage; \ No newline at end of file