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 "./ui/Modal"; import { LoadingState, StateMessage } from "./ui/QueryState"; import { api } from "../lib/api"; import { qk } from "../lib/queryKeys"; import { notify } from "../lib/notifications"; import { copyText } from "../lib/clipboard"; import { invalidateUserAccess, isEmailOff } from "../lib/adminUsers"; 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), }); // Every action here goes through a `quiet: true` endpoint, which suppresses the GLOBAL error // dialog on the promise that the caller reports the failure inline. Without an `onError` that // promise is broken and a refusal renders as nothing at all — on the broken-SMTP instance this // panel exists for, the admin would read silence as "the mail went out". const issue = useMutation({ mutationFn: () => api.createSetPasswordLink(user.id), onSuccess: (r) => setLink({ url: r.url, expires_at: r.expires_at }), onError: () => notify({ level: "error", message: t("users.account.linkFailed") }), }); const resend = useMutation({ mutationFn: () => api.resendUserVerification(user.id), onSuccess: () => notify({ level: "success", message: t("users.account.resendSent") }), onError: (err) => notify({ level: "error", message: isEmailOff(err) ? t("users.pendingAccess.resendEmailOff") : t("users.pendingAccess.resendFailed"), }), }); const activate = useMutation({ mutationFn: () => api.activateUser(user.id), onSuccess: () => invalidateUserAccess(qc, user.id), onError: () => notify({ level: "error", message: t("users.pendingAccess.activateFailed") }), }); const copy = async () => { if (!link) return; // Via the shared helper, whose synchronous `execCommand` path is what makes copying work on a // plain-http LAN origin — where `navigator.clipboard` is undefined, and which is exactly the // minimal self-host this no-email link was built for. if (await copyText(link.url)) { setCopied(true); window.setTimeout(() => setCopied(false), 2000); return; } // The link is on screen and selectable, so a failed copy is a convenience that failed — not the // action failing. Say so rather than leaving the button looking inert. 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")}

)} )}
); }