116 lines
3.8 KiB
JavaScript
116 lines
3.8 KiB
JavaScript
import React, { useState } from 'react';
|
|
import { useParams, useNavigate, Link } from 'react-router-dom';
|
|
import { Button } from './ui/button.js';
|
|
import { validatePassword, passwordHelp } from '../utils/passwordRules.ts';
|
|
|
|
export default function ResetPassword() {
|
|
const { token } = useParams();
|
|
const navigate = useNavigate();
|
|
const [pw, setPw] = useState('');
|
|
const [pw2, setPw2] = useState('');
|
|
const [submitting, setSubmitting] = useState(false);
|
|
const [error, setError] = useState('');
|
|
const [ok, setOk] = useState(false);
|
|
|
|
async function onSubmit(e) {
|
|
e.preventDefault();
|
|
setError('');
|
|
const pwErr = validatePassword(pw);
|
|
if (pwErr) { setError(pwErr); return; }
|
|
if (pw !== pw2) {
|
|
setError('Passwords do not match.');
|
|
return;
|
|
}
|
|
setSubmitting(true);
|
|
try {
|
|
const r = await fetch('/api/auth/password-reset/confirm', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({ token, password: pw })
|
|
});
|
|
if (!r.ok) {
|
|
if (r.status === 429) throw new Error('Too many attempts. Please wait ~30 seconds and try again.');
|
|
const j = await r.json().catch(() => ({}));
|
|
throw new Error(j.error || 'Reset failed');
|
|
}
|
|
setOk(true);
|
|
} catch (err) {
|
|
setError(err.message || 'Reset failed');
|
|
} finally {
|
|
setSubmitting(false);
|
|
}
|
|
}
|
|
|
|
if (ok) {
|
|
return (
|
|
<div className="max-w-md mx-auto bg-white p-6 rounded shadow">
|
|
<h2 className="text-xl font-semibold mb-2">Password updated</h2>
|
|
<p className="text-sm text-gray-700">You can now sign in with your new password.</p>
|
|
<div className="mt-4">
|
|
<Button
|
|
onClick={() => {
|
|
try {
|
|
localStorage.removeItem('token');
|
|
localStorage.removeItem('id');
|
|
} catch {}
|
|
window.location.replace('/signin');
|
|
}}
|
|
>
|
|
Go to Sign In
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
if (!token) {
|
|
return (
|
|
<div className="max-w-md mx-auto bg-white p-6 rounded shadow">
|
|
<h2 className="text-xl font-semibold mb-2">Invalid link</h2>
|
|
<p className="text-sm text-gray-700">This reset link is missing or malformed.</p>
|
|
<Link className="text-blue-600 underline text-sm" to="/forgot-password">Request a new link</Link>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<div className="max-w-md mx-auto bg-white p-6 rounded shadow">
|
|
<h2 className="text-xl font-semibold mb-3">Set a new password</h2>
|
|
<form onSubmit={onSubmit} className="space-y-3">
|
|
<div>
|
|
<label className="block text-sm mb-1">New password</label>
|
|
<input
|
|
type="password"
|
|
className="w-full border rounded px-3 py-2"
|
|
value={pw}
|
|
onChange={(e) => setPw(e.target.value)}
|
|
autoComplete="new-password"
|
|
required
|
|
minLength={8}
|
|
/>
|
|
<p className="mt-1 text-xs text-gray-500">{passwordHelp}</p>
|
|
</div>
|
|
<div>
|
|
<label className="block text-sm mb-1">Confirm password</label>
|
|
<input
|
|
type="password"
|
|
className="w-full border rounded px-3 py-2"
|
|
value={pw2}
|
|
onChange={(e) => setPw2(e.target.value)}
|
|
autoComplete="new-password"
|
|
required
|
|
minLength={8}
|
|
/>
|
|
</div>
|
|
{error && <p className="text-red-600 text-xs">{error}</p>}
|
|
<Button type="submit" disabled={submitting}>
|
|
{submitting ? 'Saving…' : 'Update password'}
|
|
</Button>
|
|
</form>
|
|
<div className="mt-3">
|
|
<Link className="text-blue-600 underline text-sm" to="/forgot-password">Need a new link?</Link>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|