import { useState } from "react"; import { useTranslation } from "react-i18next"; import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; import { Check, Clock, Copy, Globe, KeyRound, Mail, Send, ShieldAlert } from "lucide-react"; import Modal from "./Modal"; import { LoadingState, StateMessage } from "./QueryState"; import { api } from "../lib/api"; import { qk } from "../lib/queryKeys"; import { notify } from "../lib/notifications"; import type { AdminUserAccess, AdminUserRow } from "../lib/api/admin"; // The admin's answer to "why can this person not get in?", and the place every recovery action for // one account lives (E12 · S5, design option C). // // It exists because nothing in the admin UI showed an account's GATE STATE: an admin could see a // "Pending approval" chip but not whether the address was confirmed, whether the password had been // cleared by SEC-10, or which Google account was attached. Every support conversation started by // guessing. The recovery actions sit under that readout rather than in the row, so the admin picks // one having read what is actually wrong. /** One line of the Access readout: an icon, a label, and the state in the tone that state deserves. */ function AccessRow({ icon: Icon, label, value, tone = "muted", }: { icon: typeof Mail; label: string; value: string; tone?: "muted" | "warn" | "ok"; }) { const toneCls = tone === "warn" ? "text-warn" : tone === "ok" ? "text-success" : "text-muted"; return (
{label} {value}
); } export default function AccountPanel({ user, onClose, }: { user: AdminUserRow; onClose: () => void; }) { const { t } = useTranslation(); const qc = useQueryClient(); const [link, setLink] = useState<{ url: string; expires_at: string | null } | null>(null); const [copied, setCopied] = useState(false); const q = useQuery({ queryKey: qk.adminUserAccess(user.id), queryFn: () => api.adminUserAccess(user.id), }); const issue = useMutation({ mutationFn: () => api.createSetPasswordLink(user.id), onSuccess: (r) => setLink({ url: r.url, expires_at: r.expires_at }), }); const resend = useMutation({ mutationFn: () => api.resendUserVerification(user.id), onSuccess: () => notify({ level: "success", message: t("users.account.resendSent") }), }); const activate = useMutation({ mutationFn: () => api.activateUser(user.id), onSuccess: () => { qc.invalidateQueries({ queryKey: qk.adminUsers() }); qc.invalidateQueries({ queryKey: qk.adminUserAccess(user.id) }); }, }); const copy = async () => { if (!link) return; try { await navigator.clipboard.writeText(link.url); setCopied(true); window.setTimeout(() => setCopied(false), 2000); } catch { // Clipboard access can be refused (permissions, an insecure origin). The link is on screen and // selectable, so this is a convenience that failed — not the action failing. notify({ level: "warning", message: t("users.account.copyFailed") }); } }; const a: AdminUserAccess | undefined = q.data; return (
{user.email}
{q.isError && !a ? ( q.refetch()}> {t("common.loadError")} ) : !a ? ( ) : ( <>
{t("users.account.access")}
{/* The gate that explains a Google sign-in silently becoming a fresh access request. */} {!a.is_allowed && ( )}
{t("users.account.recovery")}
{a.can_activate && ( )}
{link && (
{link.url}

{t("users.account.linkNote")}

)} )}
); }