feat: admin password reset link UI + self-service reset page

This commit is contained in:
2026-01-28 19:13:36 +00:00
parent a20a5c5054
commit d9bcbd9701
5 changed files with 248 additions and 47 deletions

View File

@@ -71,21 +71,35 @@ export function AdminPage() {
};
const [resetUserId, setResetUserId] = useState<string | null>(null);
const [newPassword, setNewPassword] = useState('');
const [resetSuccess, setResetSuccess] = useState('');
const [resetUrl, setResetUrl] = useState('');
const [resetCopied, setResetCopied] = useState(false);
const [resetLoading, setResetLoading] = useState(false);
const handleResetPassword = async (userId: string) => {
if (!newPassword || newPassword.length < 8) return;
const handleGenerateResetLink = async (userId: string) => {
setResetLoading(true);
try {
await api.resetUserPassword(userId, newPassword);
setResetSuccess(userId);
setNewPassword('');
setTimeout(() => { setResetSuccess(''); setResetUserId(null); }, 2000);
const result = await api.createPasswordReset(userId);
setResetUserId(userId);
setResetUrl(result.resetUrl);
} catch (error) {
console.error('Failed to reset password:', error);
console.error('Failed to generate reset link:', error);
} finally {
setResetLoading(false);
}
};
const handleCopyResetUrl = () => {
navigator.clipboard.writeText(resetUrl);
setResetCopied(true);
setTimeout(() => setResetCopied(false), 2000);
};
const dismissReset = () => {
setResetUserId(null);
setResetUrl('');
setResetCopied(false);
};
const handleDeleteUser = async (userId: string) => {
if (!confirm('Are you sure you want to delete this user?')) return;
@@ -209,43 +223,27 @@ export function AdminPage() {
<>
{resetUserId === user.id ? (
<div className="flex items-center gap-1">
{resetSuccess === user.id ? (
<span className="text-xs text-green-600"> Reset!</span>
) : (
<>
<input
type="text"
value={newPassword}
onChange={(e) => setNewPassword(e.target.value)}
placeholder="New password (8+ chars)"
className="text-xs border border-gray-200 rounded px-2 py-1 w-40"
autoFocus
onKeyDown={(e) => {
if (e.key === 'Enter') handleResetPassword(user.id);
if (e.key === 'Escape') { setResetUserId(null); setNewPassword(''); }
}}
/>
<button
onClick={() => handleResetPassword(user.id)}
disabled={newPassword.length < 8}
className="text-xs px-2 py-1 bg-blue-600 text-white rounded disabled:opacity-50"
>
Set
</button>
<button
onClick={() => { setResetUserId(null); setNewPassword(''); }}
className="text-xs px-1 py-1 text-gray-400 hover:text-gray-600"
>
</button>
</>
)}
<span className="text-xs text-green-600"> Link generated!</span>
<button
onClick={handleCopyResetUrl}
className="text-xs px-2 py-1 bg-blue-600 text-white rounded flex items-center gap-1"
>
{resetCopied ? <Check className="w-3 h-3" /> : <Copy className="w-3 h-3" />}
{resetCopied ? 'Copied!' : 'Copy'}
</button>
<button
onClick={dismissReset}
className="text-xs px-1 py-1 text-gray-400 hover:text-gray-600"
>
</button>
</div>
) : (
<button
onClick={() => setResetUserId(user.id)}
onClick={() => handleGenerateResetLink(user.id)}
disabled={resetLoading}
className="p-1 text-gray-400 hover:text-blue-500"
title="Reset password"
title="Generate password reset link"
>
<KeyRound className="w-4 h-4" />
</button>