feat: password reset UI (forgot password, reset page, admin reset button)
This commit is contained in:
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user