feat: admin password reset link UI + self-service reset page
This commit is contained in:
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user