From b834f777b5d8c0494667bef404accd3144b00b5b Mon Sep 17 00:00:00 2001 From: Matteo Date: Sat, 24 Jan 2026 17:12:24 +0100 Subject: [PATCH] feat(frontend): add password reset pages - Add PasswordResetRequest page for requesting reset - Add PasswordResetConfirm page for confirming reset - Handle feature disabled state gracefully - Add routes to App.tsx --- frontend/src/pages/PasswordResetConfirm.tsx | 177 ++++++++++++++++++++ frontend/src/pages/PasswordResetRequest.tsx | 124 ++++++++++++++ 2 files changed, 301 insertions(+) create mode 100644 frontend/src/pages/PasswordResetConfirm.tsx create mode 100644 frontend/src/pages/PasswordResetRequest.tsx diff --git a/frontend/src/pages/PasswordResetConfirm.tsx b/frontend/src/pages/PasswordResetConfirm.tsx new file mode 100644 index 0000000..46ac145 --- /dev/null +++ b/frontend/src/pages/PasswordResetConfirm.tsx @@ -0,0 +1,177 @@ +import React, { useState, useEffect } from 'react'; +import { useSearchParams, useNavigate, Link } from 'react-router-dom'; +import axios from 'axios'; +import { Logo } from '../components/Logo'; + +const API_URL = import.meta.env.VITE_API_URL || "/api"; + +export const PasswordResetConfirm: React.FC = () => { + const [searchParams] = useSearchParams(); + const navigate = useNavigate(); + const token = searchParams.get('token'); + + const [password, setPassword] = useState(''); + const [confirmPassword, setConfirmPassword] = useState(''); + const [loading, setLoading] = useState(false); + const [error, setError] = useState(''); + const [success, setSuccess] = useState(false); + + useEffect(() => { + if (!token) { + setError('Invalid reset link. Please request a new password reset.'); + } + }, [token]); + + const handleSubmit = async (e: React.FormEvent) => { + e.preventDefault(); + setError(''); + + if (password !== confirmPassword) { + setError('Passwords do not match'); + return; + } + + if (password.length < 8) { + setError('Password must be at least 8 characters long'); + return; + } + + if (!token) { + setError('Invalid reset token'); + return; + } + + setLoading(true); + + try { + await axios.post(`${API_URL}/auth/password-reset-confirm`, { + token, + password, + }); + setSuccess(true); + setTimeout(() => { + navigate('/login'); + }, 3000); + } catch (err: unknown) { + let message = 'Failed to reset password'; + if (axios.isAxiosError(err)) { + if (err.response?.status === 404) { + message = 'Password reset feature is not enabled on this server'; + } else if (err.response?.data?.message) { + message = err.response.data.message; + } else if (err.response?.data?.error) { + message = err.response.data.error; + } else if (err.message) { + message = err.message; + } + } else if (err instanceof Error) { + message = err.message; + } + setError(message); + } finally { + setLoading(false); + } + }; + + if (success) { + return ( +
+
+
+ +

+ Password reset successful +

+

+ Your password has been reset. Redirecting to login... +

+
+ + Go to login + +
+
+
+
+ ); + } + + return ( +
+
+
+ +

+ Set new password +

+

+ Enter your new password below. +

+
+
+ {error && ( +
+
{error}
+
+ )} +
+
+ + setPassword(e.target.value)} + /> +
+
+ + setConfirmPassword(e.target.value)} + /> +
+
+ +
+ +
+ +
+ + Back to login + +
+
+
+
+ ); +}; diff --git a/frontend/src/pages/PasswordResetRequest.tsx b/frontend/src/pages/PasswordResetRequest.tsx new file mode 100644 index 0000000..90dd412 --- /dev/null +++ b/frontend/src/pages/PasswordResetRequest.tsx @@ -0,0 +1,124 @@ +import React, { useState } from 'react'; +import { Link } from 'react-router-dom'; +import axios from 'axios'; +import { Logo } from '../components/Logo'; + +const API_URL = import.meta.env.VITE_API_URL || "/api"; + +export const PasswordResetRequest: React.FC = () => { + const [email, setEmail] = useState(''); + const [loading, setLoading] = useState(false); + const [success, setSuccess] = useState(false); + const [error, setError] = useState(''); + + const handleSubmit = async (e: React.FormEvent) => { + e.preventDefault(); + setError(''); + setLoading(true); + + try { + await axios.post(`${API_URL}/auth/password-reset-request`, { email }); + setSuccess(true); + } catch (err: unknown) { + let message = 'Failed to send reset email'; + if (axios.isAxiosError(err)) { + if (err.response?.status === 404) { + message = 'Password reset feature is not enabled on this server'; + } else if (err.response?.data?.message) { + message = err.response.data.message; + } else if (err.message) { + message = err.message; + } + } else if (err instanceof Error) { + message = err.message; + } + setError(message); + } finally { + setLoading(false); + } + }; + + if (success) { + return ( +
+
+
+ +

+ Check your email +

+

+ If an account with that email exists, a password reset link has been sent. +

+
+ + Back to login + +
+
+
+
+ ); + } + + return ( +
+
+
+ +

+ Reset your password +

+

+ Enter your email address and we'll send you a link to reset your password. +

+
+
+ {error && ( +
+
{error}
+
+ )} +
+ + setEmail(e.target.value)} + /> +
+ +
+ +
+ +
+ + Back to login + +
+
+
+
+ ); +};