Fix admin seed, open username/email changes, invite refresh & revocation

- Seed checks for any admin role instead of username='admin'
- Username change open to all registered users (was admin-only)
- New change-email endpoint requiring password confirmation
- Settings page: inline editing for username and email
- Invitations: await refresh after generate so list updates visibly
- Invitations: revoke button to delete unused invites (admin only)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-04-27 22:22:42 -04:00
parent c1cc70eeb9
commit 2912d760cb
6 changed files with 235 additions and 24 deletions
+32 -9
View File
@@ -1,5 +1,5 @@
import { useState, useEffect, useCallback } from 'react';
import { Copy, Check, Plus, RefreshCw } from 'lucide-react';
import { Copy, Check, Plus, RefreshCw, Trash2 } from 'lucide-react';
import { api } from '@/lib/api';
interface Invitation {
@@ -31,6 +31,7 @@ export function InvitationsPage() {
const [loading, setLoading] = useState(true);
const [generating, setGenerating] = useState(false);
const [copiedCode, setCopiedCode] = useState<string | null>(null);
const [revoking, setRevoking] = useState<string | null>(null);
const fetchInvitations = useCallback(async () => {
try {
@@ -53,7 +54,7 @@ export function InvitationsPage() {
await navigator.clipboard.writeText(url);
setCopiedCode(result.code);
setTimeout(() => setCopiedCode(null), 2000);
fetchInvitations();
await fetchInvitations();
} catch {
// silent
} finally {
@@ -61,6 +62,18 @@ export function InvitationsPage() {
}
}
async function handleRevoke(id: string) {
setRevoking(id);
try {
await api.invites.revoke(id);
await fetchInvitations();
} catch {
// silent
} finally {
setRevoking(null);
}
}
async function handleCopyCode(code: string) {
const url = `${window.location.origin}?invite=${code}`;
await navigator.clipboard.writeText(url);
@@ -132,13 +145,23 @@ export function InvitationsPage() {
</td>
<td className="px-4 py-3">
{!inv.used && (
<button
onClick={() => handleCopyCode(inv.code)}
className="text-surface-400 hover:text-surface-200 transition-colors"
title="Copy invite link"
>
{copiedCode === inv.code ? <Check size={14} className="text-accent" /> : <Copy size={14} />}
</button>
<div className="flex items-center gap-2">
<button
onClick={() => handleCopyCode(inv.code)}
className="text-surface-400 hover:text-surface-200 transition-colors"
title="Copy invite link"
>
{copiedCode === inv.code ? <Check size={14} className="text-accent" /> : <Copy size={14} />}
</button>
<button
onClick={() => handleRevoke(inv.id)}
disabled={revoking === inv.id}
className="text-surface-400 hover:text-danger transition-colors disabled:opacity-50"
title="Revoke invitation"
>
<Trash2 size={14} />
</button>
</div>
)}
</td>
</tr>