My-Weekly-ToDo-List/src/app/auth/reset-password/page.tsx
mARTin 627f4c0005 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
2026-03-10 16:43:27 +01:00

163 lines
5.9 KiB
TypeScript

'use client';
import React, { useState, useEffect } from 'react';
const ResetPasswordPage = () => {
const [token, setToken] = useState('');
const [newPassword, setNewPassword] = useState('');
const [confirmPassword, setConfirmPassword] = useState('');
const [isLoading, setIsLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
const [showSuccess, setShowSuccess] = useState(false);
const [hasToken, setHasToken] = useState(false);
useEffect(() => {
const urlParams = new URLSearchParams(window.location.search);
const tokenParam = urlParams.get('token');
if (tokenParam) {
setToken(tokenParam);
setHasToken(true);
}
}, []);
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
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 {
const response = await fetch('/api/auth/reset-password', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ token, newPassword }),
});
const result = await response.json();
if (!response.ok) {
throw new Error(result.error || 'Failed to reset password');
}
setShowSuccess(true);
} catch (err) {
setError(err instanceof Error ? err.message : 'An unknown error occurred');
} finally {
setIsLoading(false);
}
};
if (showSuccess) {
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">Password Reset Successful!</h2>
<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.
</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>
</div>
</div>
</div>
);
}
return (
<div className="min-h-screen bg-gray-50">
<div className="flex flex-col items-center justify-center min-h-screen py-12 px-4 sm:px-6 lg:px-8">
<div className="w-full max-w-md">
<div className="text-center mb-8">
<h1 className="text-3xl font-bold text-gray-900">Weekly To Do List</h1>
<p className="mt-2 text-gray-600">Choose a new password</p>
</div>
<form className="mt-8 space-y-6" onSubmit={handleSubmit}>
{error && (
<div className="rounded-md bg-red-50 p-4">
<div className="text-sm text-red-700">{error}</div>
</div>
)}
<div className="rounded-md shadow-sm -space-y-px">
<div>
<label htmlFor="new-password" className="sr-only">New Password</label>
<input
id="new-password"
name="new-password"
type="password"
required
value={newPassword}
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-t-md focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 focus:z-10 sm:text-sm"
placeholder="New Password"
/>
</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>
<button
type="submit"
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"
>
{isLoading ? 'Resetting...' : 'Reset Password'}
</button>
</div>
</form>
<div className="mt-6 text-center">
<p className="text-sm text-gray-600">
<a href="/auth/login" className="font-medium text-indigo-600 hover:text-indigo-500">
Back to login
</a>
</p>
</div>
</div>
</div>
</div>
);
};
export default ResetPasswordPage;