"use client"; import { useEffect, useState } from "react"; import { nip19 } from "nostr-tools"; import { api } from "@/lib/api"; import { useAuth } from "@/hooks/useAuth"; import { useNostrProfile } from "@/hooks/useNostrProfile"; import { NostrAvatar } from "@/components/nostr/NostrAvatar"; import { formatDate } from "@/lib/utils"; import { Lock } from "lucide-react"; const ROLE_RANK: Record = { superadmin: 4, admin: 3, moderator: 2, writer: 1, guest: 0, }; const ROLE_OPTIONS: { value: string; label: string }[] = [ { value: "admin", label: "Admin" }, { value: "moderator", label: "Moderator" }, { value: "writer", label: "Writer" }, { value: "guest", label: "Guest (no role)" }, ]; function normalizeRole(role: string | null | undefined): string { return role && ROLE_RANK[role] !== undefined ? role : "guest"; } function hexToNpub(pubkey: string): string { if (!pubkey) return ""; // Already an npub (some users are stored in npub form). if (pubkey.startsWith("npub1")) return pubkey; try { return nip19.npubEncode(pubkey); } catch { return ""; } } function shortenNpub(npub: string): string { if (npub.length <= 26) return npub; return `${npub.slice(0, 14)}...${npub.slice(-10)}`; } interface UserRowProps { user: any; draft: string; onDraftChange: (pubkey: string, value: string) => void; savingPubkey: string | null; onSaveUsername: (pubkey: string, currentUsername: string | null | undefined) => void; onCancelUsername: (pubkey: string, stored: string | null | undefined) => void; hostname: string; copiedPubkey: string | null; onCopyNpub: (pubkey: string) => void; callerRank: number; isSuperAdmin: boolean; savingRole: string | null; onRoleChange: (pubkey: string, role: string) => void; } function UserRow({ user, draft, onDraftChange, savingPubkey, onSaveUsername, onCancelUsername, hostname, copiedPubkey, onCopyNpub, callerRank, isSuperAdmin, savingRole, onRoleChange, }: UserRowProps) { const { profile, loading: profileLoading } = useNostrProfile(user.pubkey); const stored = user.username ?? ""; const usernameDirty = draft.trim().toLowerCase() !== stored.toLowerCase(); const fullNpub = hexToNpub(user.pubkey); const nostrDisplay = profile?.name || profile?.displayName; return (

{profileLoading ? ( ) : nostrDisplay ? ( nostrDisplay ) : ( No Nostr name )}

NIP-05 username

Reserved names from the site blocklist can be assigned here (users cannot claim them on the dashboard).

onDraftChange(user.pubkey, e.target.value)} disabled={savingPubkey === user.pubkey} placeholder="local-part" className="bg-surface-container-highest text-on-surface rounded-lg px-3 py-2 text-sm font-mono min-w-[8rem] max-w-full flex-1 focus:outline-none focus:ring-1 focus:ring-primary/40 disabled:opacity-50" /> @{hostname || "…"}
{draft.trim() && (

{draft.trim().toLowerCase()}@{hostname || "…"}

)}
{user.isSuperAdmin ? ( SuperAdmin ) : ( (() => { const targetRole = normalizeRole(user.role); const targetRank = ROLE_RANK[targetRole]; const canModify = callerRank > targetRank; return ( ); })() )} {user.createdAt && ( Joined {formatDate(user.createdAt)} )}
); } export default function UsersPage() { const { user: currentUser, isSuperAdmin } = useAuth(); const [users, setUsers] = useState([]); const [loading, setLoading] = useState(true); const [error, setError] = useState(""); const [savingRole, setSavingRole] = useState(null); const [hostname, setHostname] = useState(""); const [usernameDrafts, setUsernameDrafts] = useState>({}); const [savingPubkey, setSavingPubkey] = useState(null); const [copiedPubkey, setCopiedPubkey] = useState(null); const loadUsers = async () => { try { const data = await api.getUsers(); setUsers(data); setUsernameDrafts( Object.fromEntries( data.map((u: { pubkey: string; username?: string | null }) => [u.pubkey, u.username ?? ""]) ) ); } catch (err: any) { setError(err.message); } finally { setLoading(false); } }; useEffect(() => { loadUsers(); }, []); useEffect(() => { setHostname(typeof window !== "undefined" ? window.location.hostname : ""); }, []); const handleCopyNpub = async (pubkey: string) => { const full = hexToNpub(pubkey); if (!full) return; try { await navigator.clipboard.writeText(full); setCopiedPubkey(pubkey); window.setTimeout(() => setCopiedPubkey((p) => (p === pubkey ? null : p)), 2000); } catch { // ignore } }; const callerRank = isSuperAdmin ? ROLE_RANK.superadmin : ROLE_RANK[normalizeRole(currentUser?.role)]; const handleRoleChange = async (pubkey: string, role: string) => { setSavingRole(pubkey); setError(""); try { await api.setUserRole(pubkey, role === "guest" ? null : role); await loadUsers(); } catch (err: any) { setError(err.message); } finally { setSavingRole(null); } }; const handleSaveUsername = async (pubkey: string, currentUsername: string | null | undefined) => { const trimmed = (usernameDrafts[pubkey] ?? "").trim().toLowerCase(); if (!trimmed) return; setSavingPubkey(pubkey); setError(""); try { await api.updateUserUsername(pubkey, trimmed); await loadUsers(); } catch (err: any) { setError(err.message); setUsernameDrafts((prev) => ({ ...prev, [pubkey]: currentUsername ?? "" })); } finally { setSavingPubkey(null); } }; const handleCancelUsername = (pubkey: string, stored: string | null | undefined) => { setUsernameDrafts((prev) => ({ ...prev, [pubkey]: stored ?? "" })); }; const handleDraftChange = (pubkey: string, value: string) => { setUsernameDrafts((prev) => ({ ...prev, [pubkey]: value })); }; if (loading) { return (
Loading users...
); } return (

User Management

{error &&

{error}

}
{users.length === 0 ? (

No users found.

) : ( users.map((user) => ( )) )}
); }