feat: password reset UI (forgot password, reset page, admin reset button)

This commit is contained in:
2026-01-28 21:44:22 +00:00
parent 6e451e0795
commit 696e2187f4
6 changed files with 420 additions and 10 deletions

View File

@@ -1,5 +1,5 @@
import { useState, useEffect } from 'react';
import { Users, Mail, Plus, Trash2, Copy, Check } from 'lucide-react';
import { Users, Mail, Plus, Trash2, Copy, Check, KeyRound } from 'lucide-react';
import { api } from '@/lib/api';
import { useAuthStore } from '@/stores/auth';
import type { User, Invite } from '@/types';
@@ -21,6 +21,12 @@ export default function AdminPage() {
const [inviteUrl, setInviteUrl] = useState('');
const [copied, setCopied] = useState(false);
// Password reset
const [resetUserId, setResetUserId] = useState<string | null>(null);
const [resetUrl, setResetUrl] = useState('');
const [resetCopied, setResetCopied] = useState(false);
const [resetLoading, setResetLoading] = useState(false);
useEffect(() => {
loadData();
}, []);
@@ -69,6 +75,31 @@ export default function AdminPage() {
}
};
const handleGenerateResetLink = async (userId: string) => {
setResetLoading(true);
try {
const result = await api.createPasswordReset(userId);
setResetUserId(userId);
setResetUrl(result.resetUrl);
} catch (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? All their data will be lost.')) return;
try {
@@ -180,13 +211,44 @@ export default function AdminPage() {
</td>
<td className="px-4 py-3">
{user.id !== currentUser?.id && (
<button
onClick={() => handleDeleteUser(user.id)}
className="p-1 text-slate-400 hover:text-red-500 transition-colors"
title="Delete user"
>
<Trash2 className="w-4 h-4" />
</button>
<div className="flex items-center gap-1">
{resetUserId === user.id ? (
<div className="flex items-center gap-1">
<span className="text-xs text-green-600 whitespace-nowrap"> Link ready</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-slate-400 hover:text-slate-600"
>
</button>
</div>
) : (
<>
<button
onClick={() => handleGenerateResetLink(user.id)}
disabled={resetLoading}
className="p-1 text-slate-400 hover:text-blue-500 transition-colors"
title="Generate password reset link"
>
<KeyRound className="w-4 h-4" />
</button>
<button
onClick={() => handleDeleteUser(user.id)}
className="p-1 text-slate-400 hover:text-red-500 transition-colors"
title="Delete user"
>
<Trash2 className="w-4 h-4" />
</button>
</>
)}
</div>
)}
</td>
</tr>