fix: hide reset token, add password confirmation field

Token is now read from URL silently. User enters password twice
with client-side match validation. Shows error for missing/invalid tokens.

v1.23.1
This commit is contained in:
mARTin 2026-03-10 16:43:27 +01:00
parent 59cfcc7be4
commit 627f4c0005
2 changed files with 61 additions and 47 deletions

View File

@ -1,6 +1,6 @@
{ {
"name": "my-weekly-todo-list", "name": "my-weekly-todo-list",
"version": "1.23.0", "version": "1.23.1",
"description": "A web-based weekly task management application that organizes to-dos and calendar events in a single, intuitive weekly view", "description": "A web-based weekly task management application that organizes to-dos and calendar events in a single, intuitive weekly view",
"main": "index.js", "main": "index.js",
"scripts": { "scripts": {

View File

@ -5,49 +5,53 @@ import React, { useState, useEffect } from 'react';
const ResetPasswordPage = () => { const ResetPasswordPage = () => {
const [token, setToken] = useState(''); const [token, setToken] = useState('');
const [newPassword, setNewPassword] = useState(''); const [newPassword, setNewPassword] = useState('');
const [confirmPassword, setConfirmPassword] = useState('');
const [isLoading, setIsLoading] = useState(false); const [isLoading, setIsLoading] = useState(false);
const [message, setMessage] = useState<string | null>(null);
const [error, setError] = useState<string | null>(null); const [error, setError] = useState<string | null>(null);
const [showSuccess, setShowSuccess] = useState(false); const [showSuccess, setShowSuccess] = useState(false);
const [hasToken, setHasToken] = useState(false);
// Get token from URL query parameter
useEffect(() => { useEffect(() => {
const urlParams = new URLSearchParams(window.location.search); const urlParams = new URLSearchParams(window.location.search);
const tokenParam = urlParams.get('token'); const tokenParam = urlParams.get('token');
if (tokenParam) { if (tokenParam) {
setToken(tokenParam); setToken(tokenParam);
setHasToken(true);
} }
}, []); }, []);
const handleSubmit = async (e: React.FormEvent) => { const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault(); e.preventDefault();
setIsLoading(true);
setMessage(null);
setError(null); setError(null);
if (newPassword.length < 6) {
setError('Password must be at least 6 characters');
return;
}
if (newPassword !== confirmPassword) {
setError('Passwords do not match');
return;
}
setIsLoading(true);
try { try {
// Simulate API call
const response = await fetch('/api/auth/reset-password', { const response = await fetch('/api/auth/reset-password', {
method: 'POST', method: 'POST',
headers: { headers: { 'Content-Type': 'application/json' },
'Content-Type': 'application/json',
},
body: JSON.stringify({ token, newPassword }), body: JSON.stringify({ token, newPassword }),
}); });
const result = await response.json(); const result = await response.json();
if (!response.ok) { if (!response.ok) {
throw new Error(result.error || 'Failed to reset password'); throw new Error(result.error || 'Failed to reset password');
} }
setMessage('Password reset successfully!');
setShowSuccess(true); setShowSuccess(true);
setToken('');
setNewPassword('');
} catch (err) { } catch (err) {
setError(err instanceof Error ? err.message : 'An unknown error occurred'); setError(err instanceof Error ? err.message : 'An unknown error occurred');
console.error('Reset password error:', err);
} finally { } finally {
setIsLoading(false); setIsLoading(false);
} }
@ -60,7 +64,25 @@ const ResetPasswordPage = () => {
<div className="text-center"> <div className="text-center">
<h2 className="text-2xl font-bold text-gray-900">Password Reset Successful!</h2> <h2 className="text-2xl font-bold text-gray-900">Password Reset Successful!</h2>
<p className="mt-2 text-gray-600"> <p className="mt-2 text-gray-600">
Your password has been reset successfully. You can now <a href="/auth/login" className="font-medium text-indigo-600 hover:text-indigo-500">sign in</a> with your new password. Your password has been reset successfully. You can now{' '}
<a href="/auth/login" className="font-medium text-indigo-600 hover:text-indigo-500">sign in</a>{' '}
with your new password.
</p>
</div>
</div>
</div>
);
}
if (!hasToken) {
return (
<div className="min-h-screen bg-gray-50 flex items-center justify-center">
<div className="max-w-md w-full space-y-8 p-8 bg-white rounded-lg shadow">
<div className="text-center">
<h2 className="text-2xl font-bold text-gray-900">Invalid Reset Link</h2>
<p className="mt-2 text-gray-600">
This reset link is invalid or has expired. Please{' '}
<a href="/auth/forgot-password" className="font-medium text-indigo-600 hover:text-indigo-500">request a new one</a>.
</p> </p>
</div> </div>
</div> </div>
@ -74,36 +96,19 @@ const ResetPasswordPage = () => {
<div className="w-full max-w-md"> <div className="w-full max-w-md">
<div className="text-center mb-8"> <div className="text-center mb-8">
<h1 className="text-3xl font-bold text-gray-900">Weekly To Do List</h1> <h1 className="text-3xl font-bold text-gray-900">Weekly To Do List</h1>
<p className="mt-2 text-gray-600">Reset your password</p> <p className="mt-2 text-gray-600">Choose a new password</p>
</div> </div>
<form className="mt-8 space-y-6" onSubmit={handleSubmit}> <form className="mt-8 space-y-6" onSubmit={handleSubmit}>
{error && ( {error && (
<div className="rounded-md bg-red-50 p-4"> <div className="rounded-md bg-red-50 p-4">
<div className="text-sm text-red-700">{error}</div> <div className="text-sm text-red-700">{error}</div>
</div> </div>
)} )}
<div className="rounded-md shadow-sm -space-y-px"> <div className="rounded-md shadow-sm -space-y-px">
<div> <div>
<label htmlFor="token" className="sr-only"> <label htmlFor="new-password" className="sr-only">New Password</label>
Reset Token
</label>
<input
id="token"
name="token"
type="text"
required
value={token}
onChange={(e) => setToken(e.target.value)}
className="appearance-none rounded-none relative block w-full px-3 py-2 border border-gray-300 placeholder-gray-500 text-gray-900 rounded-t-md focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 focus:z-10 sm:text-sm"
placeholder="Reset Token"
/>
</div>
<div>
<label htmlFor="new-password" className="sr-only">
New Password
</label>
<input <input
id="new-password" id="new-password"
name="new-password" name="new-password"
@ -111,10 +116,23 @@ const ResetPasswordPage = () => {
required required
value={newPassword} value={newPassword}
onChange={(e) => setNewPassword(e.target.value)} onChange={(e) => setNewPassword(e.target.value)}
className="appearance-none rounded-none relative block w-full px-3 py-2 border border-gray-300 placeholder-gray-500 text-gray-900 rounded-b-md focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 focus:z-10 sm:text-sm" className="appearance-none rounded-none relative block w-full px-3 py-2 border border-gray-300 placeholder-gray-500 text-gray-900 rounded-t-md focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 focus:z-10 sm:text-sm"
placeholder="New Password" placeholder="New Password"
/> />
</div> </div>
<div>
<label htmlFor="confirm-password" className="sr-only">Confirm Password</label>
<input
id="confirm-password"
name="confirm-password"
type="password"
required
value={confirmPassword}
onChange={(e) => setConfirmPassword(e.target.value)}
className="appearance-none rounded-none relative block w-full px-3 py-2 border border-gray-300 placeholder-gray-500 text-gray-900 rounded-b-md focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 focus:z-10 sm:text-sm"
placeholder="Confirm Password"
/>
</div>
</div> </div>
<div> <div>
@ -123,15 +141,11 @@ const ResetPasswordPage = () => {
disabled={isLoading} disabled={isLoading}
className="group relative w-full flex justify-center py-2 px-4 border border-transparent text-sm font-medium rounded-md text-white bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500 disabled:opacity-50" className="group relative w-full flex justify-center py-2 px-4 border border-transparent text-sm font-medium rounded-md text-white bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500 disabled:opacity-50"
> >
{isLoading ? ( {isLoading ? 'Resetting...' : 'Reset Password'}
<span>Resetting...</span>
) : (
<span>Reset Password</span>
)}
</button> </button>
</div> </div>
</form> </form>
<div className="mt-6 text-center"> <div className="mt-6 text-center">
<p className="text-sm text-gray-600"> <p className="text-sm text-gray-600">
<a href="/auth/login" className="font-medium text-indigo-600 hover:text-indigo-500"> <a href="/auth/login" className="font-medium text-indigo-600 hover:text-indigo-500">
@ -145,4 +159,4 @@ const ResetPasswordPage = () => {
); );
}; };
export default ResetPasswordPage; export default ResetPasswordPage;