"use client"; import { useState, useEffect, useRef, useCallback } from "react"; import SideNav from "../components/SideNav"; import HelpTooltip from "../components/HelpTooltip"; import { IconSearch, IconX, IconKey, IconTrash, IconPlus, } from "@tabler/icons-react"; function b64uToBuf(b64u: string): ArrayBuffer { const b64 = b64u.replace(/-/g, "+").replace(/_/g, "/"); const bin = atob(b64); const buf = new Uint8Array(bin.length); for (let i = 0; i < bin.length; i++) buf[i] = bin.charCodeAt(i); return buf.buffer; } function bufToB64u(buf: ArrayBuffer): string { const bytes = new Uint8Array(buf); let bin = ""; for (const b of bytes) bin += String.fromCharCode(b); return btoa(bin).replace(/\+/g, "-").replace(/\//g, "_").replace(/=/g, ""); } interface Credential { id: string; created_at?: string; } interface User { username: string; credentials: Credential[]; } type EnrollStatus = "idle" | "starting" | "waiting_yubikey" | "saving" | "done" | "error"; const truncId = (id: string) => id.length > 20 ? id.slice(0, 10) + "…" + id.slice(-6) : id; const fmtDate = (iso?: string) => { if (!iso) return null; try { return new Date(iso).toLocaleDateString(undefined, { month: "short", day: "numeric", year: "2-digit", }); } catch { return null; } }; export default function UsersPage() { const [users, setUsers] = useState(null); const [selected, setSelected] = useState(null); const [search, setSearch] = useState(""); const [enrollPassword, setEnrollPassword] = useState(""); const [enrollStatus, setEnrollStatus] = useState("idle"); const [enrollMessage, setEnrollMessage] = useState(""); const [deletingId, setDeletingId] = useState(null); const [leftPct, setLeftPct] = useState(35); const isDragging = useRef(false); const containerRef = useRef(null); const fetchUsers = useCallback(async () => { const res = await fetch("/api/users"); if (!res.ok) return; const data = await res.json(); const list: User[] = data.users ?? data; setUsers(list); setSelected((prev) => { const match = prev ? list.find((u) => u.username === prev.username) : null; return match ?? list[0] ?? null; }); }, []); useEffect(() => { fetchUsers(); }, [fetchUsers]); useEffect(() => { const onMove = (e: MouseEvent) => { if (!isDragging.current || !containerRef.current) return; const rect = containerRef.current.getBoundingClientRect(); const pct = ((e.clientX - rect.left) / rect.width) * 100; setLeftPct(Math.min(Math.max(pct, 20), 60)); }; const onUp = () => { isDragging.current = false; }; window.addEventListener("mousemove", onMove); window.addEventListener("mouseup", onUp); return () => { window.removeEventListener("mousemove", onMove); window.removeEventListener("mouseup", onUp); }; }, []); const selectUser = (user: User) => { setSelected(user); setEnrollPassword(""); setEnrollStatus("idle"); setEnrollMessage(""); }; const deleteCredential = async (credId: string) => { if (!selected) return; setDeletingId(credId); await fetch(`/api/users/${selected.username}/credentials/${credId}`, { method: "DELETE", }); setDeletingId(null); await fetchUsers(); }; const enrollKey = async (e: React.FormEvent) => { e.preventDefault(); if (!selected) return; setEnrollStatus("starting"); setEnrollMessage(""); try { const startRes = await fetch( `/api/users/${selected.username}/enroll/start`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ password: enrollPassword }), }, ); if (!startRes.ok) { setEnrollMessage("Invalid password."); setEnrollStatus("error"); return; } const { session_id, challenge } = await startRes.json(); setEnrollStatus("waiting_yubikey"); const opts = challenge.publicKey; opts.challenge = b64uToBuf(opts.challenge); opts.user.id = b64uToBuf(opts.user.id); opts.authenticatorSelection = { authenticatorAttachment: "cross-platform", residentKey: "discouraged", requireResidentKey: false, userVerification: "discouraged", }; (opts as Record).hints = ["security-key"]; if (opts.excludeCredentials) { opts.excludeCredentials = opts.excludeCredentials.map( (c: { id: string; type: string; transports?: string[] }) => ({ ...c, id: b64uToBuf(c.id), }), ); } let cred: PublicKeyCredential; try { cred = (await navigator.credentials.create({ publicKey: opts, })) as PublicKeyCredential; } catch (err) { setEnrollMessage( "YubiKey error: " + (err instanceof Error ? err.message : "cancelled"), ); setEnrollStatus("error"); return; } setEnrollStatus("saving"); const attestation = cred.response as AuthenticatorAttestationResponse; const finishRes = await fetch( `/api/users/${selected.username}/enroll/finish`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ session_id, credential: { id: cred.id, rawId: bufToB64u(cred.rawId), type: cred.type, response: { attestationObject: bufToB64u(attestation.attestationObject), clientDataJSON: bufToB64u(attestation.clientDataJSON), transports: ["usb", "nfc", "ble", "hybrid"], }, extensions: {}, }, }), }, ); if (!finishRes.ok) { setEnrollMessage("Registration failed. Check server logs."); setEnrollStatus("error"); return; } setEnrollStatus("done"); setEnrollMessage("YubiKey enrolled successfully."); setEnrollPassword(""); await fetchUsers(); } catch { setEnrollMessage("Something went wrong. Try again."); setEnrollStatus("error"); } }; const filtered = (users ?? []).filter((u) => u.username.toLowerCase().includes(search.toLowerCase()), ); const enrollBusy = enrollStatus !== "idle" && enrollStatus !== "error"; return (
{}} isAuthed={true} /> {/* Desktop split pane */}
{/* Left panel — user list */}
setSearch(e.target.value)} placeholder="Search users…" className="flex-1 bg-transparent text-[12px] text-foreground placeholder:text-foreground-sec outline-none" /> {search && ( )}
{users === null ? (
{[...Array(4)].map((_, i) => (
))}
) : filtered.length === 0 ? (

No users found.

) : (
{filtered.map((user) => { const active = selected?.username === user.username; return (
selectUser(user)} className={ "flex items-center justify-between gap-[10px] px-[10px] py-[9px] rounded-xl cursor-pointer transition-colors m-[2px] " + (active ? "bg-blue/20 shadow-sm shadow-blue/10" : "hover:bg-secondary/50") } >
{user.username[0]?.toUpperCase()}
{user.username}
{user.credentials.length}{" "} {user.credentials.length === 1 ? "key" : "keys"}
); })}
)}
{/* Drag handle */}
{ isDragging.current = true; e.preventDefault(); }} className="w-[10px] shrink-0 flex items-center justify-center cursor-col-resize group" >
{/* Right panel — credential management */}
{selected ? (
{/* User header */}
{selected.username[0]?.toUpperCase()}

{selected.username}

{selected.credentials.length} security{" "} {selected.credentials.length === 1 ? "key" : "keys"} enrolled

{/* Enrolled keys */}
Enrolled Keys
{selected.credentials.length}
{selected.credentials.length === 0 ? (
No keys enrolled
) : (
{selected.credentials.map((cred) => (
{truncId(cred.id)} {fmtDate(cred.created_at) && ( {fmtDate(cred.created_at)} )}
))}
)}
{/* Enroll new key */}
Enroll New Key
{enrollStatus === "done" ? (
{enrollMessage}
) : (
setEnrollPassword(e.target.value)} required disabled={enrollBusy} placeholder="Enter user password" autoComplete="current-password" className="w-full px-[12px] py-[8px] bg-secondary/50 border border-secondary rounded-xl text-[13px] text-foreground outline-none focus:border-blue/50 transition-colors disabled:opacity-50" />
{enrollStatus === "waiting_yubikey" && (

Touch your YubiKey…

)} {enrollStatus === "error" && enrollMessage && (

{enrollMessage}

)}
)}
) : (
Select a user
)}
{/* Mobile — stacked */}
{/* User list */}
setSearch(e.target.value)} placeholder="Search users…" className="flex-1 bg-transparent text-[12px] text-foreground placeholder:text-foreground-sec outline-none" />
{users === null ? (
{[...Array(3)].map((_, i) => (
))}
) : ( filtered.map((user) => { const open = selected?.username === user.username; return (
selectUser(open ? { ...user } : user)} className={ "flex items-center justify-between gap-[10px] px-[10px] py-[9px] rounded-xl cursor-pointer transition-colors " + (open ? "bg-blue/20" : "hover:bg-secondary/50") } >
{user.username[0]?.toUpperCase()}
{user.username}
{user.credentials.length} {user.credentials.length === 1 ? "key" : "keys"}
{open && (
{/* Keys */}

Enrolled Keys

{user.credentials.length === 0 ? (

No keys enrolled.

) : (
{user.credentials.map((cred) => (
{truncId(cred.id)}
))}
)}
{/* Enroll */}

Enroll New Key

{enrollStatus === "done" ? (

{enrollMessage}

) : (
setEnrollPassword(e.target.value)} required disabled={enrollBusy} placeholder={`Password for ${user.username}`} className="w-full px-[10px] py-[7px] bg-secondary/50 border border-secondary rounded-xl text-[12px] text-foreground outline-none" /> {enrollStatus === "waiting_yubikey" && (

Touch your YubiKey…

)} {enrollStatus === "error" && enrollMessage && (

{enrollMessage}

)}
)}
)}
); }) )}
); }