import { useEffect, useState } from "react"; import { Link, Navigate } from "react-router-dom"; import { useAuth } from "../auth/AuthContext"; import { PageHeader } from "../components/layout/PageHeader"; import { Badge, Button, Card, EmptyState, Input, Pager, Select } from "../components/ui"; import { useData, useRepos } from "../data/DataContext"; import { KEYS } from "../data/mock/keys"; import type { Role } from "../domain/types"; import { useI18n } from "../i18n/I18nContext"; import { memberRoleSummary, roleLabel } from "../lib/memberRole"; import { readJson, writeJson } from "../lib/storage"; import type { MemberAdminView } from "../lib/tenantUsers"; import { useFormatApiError } from "../lib/apiErrors"; import { checkPasswordPolicy, isPasswordPolicyOk } from "../lib/passwordPolicy"; import { formatLocalDateTime, nowUnixNano } from "../lib/time"; import { PLANS, type PlanId, type UsageMemberPrefs } from "../lib/usageMeter"; const PAGE_SIZE_OPTIONS = [5, 10, 20]; /** 最近揭示的臨時密碼:可重整,需手動關閉 */ type RevealedTempPassword = { uid: string; display_name: string; email: string; password: string; created_at: number; reason?: "create" | "reset"; }; function loadRevealedTemp(): RevealedTempPassword | null { return readJson(KEYS.adminTempPassword, null); } function saveRevealedTemp(rec: RevealedTempPassword | null): void { writeJson(KEYS.adminTempPassword, rec); } /** * 管理員:島民列表(搜尋名稱/uid)+ 新增、停權、權限、驗證、重設密碼。 */ export function AdminUsersPage() { const repos = useRepos(); const { member, reload } = useAuth(); const { t } = useI18n(); const formatApiError = useFormatApiError(); const { tick, refresh, dataSource } = useData(); const isLive = dataSource === "live"; const [users, setUsers] = useState([]); const [page, setPage] = useState(1); const [pageSize, setPageSize] = useState(10); const [total, setTotal] = useState(0); const [query, setQuery] = useState(""); const [queryDraft, setQueryDraft] = useState(""); const [selected, setSelected] = useState(null); const [roleDraft, setRoleDraft] = useState(["member"]); const [busy, setBusy] = useState(""); const [message, setMessage] = useState(""); const [error, setError] = useState(""); const [revealed, setRevealed] = useState(() => loadRevealedTemp()); const [copyHint, setCopyHint] = useState(""); const [customPw, setCustomPw] = useState(""); const [showCreate, setShowCreate] = useState(false); const [createName, setCreateName] = useState(""); const [createEmail, setCreateEmail] = useState(""); const [createPassword, setCreatePassword] = useState(""); const [createVerified, setCreateVerified] = useState(true); const [createAsAdmin, setCreateAsAdmin] = useState(false); /** 選中島民的方案/不擋額度 */ const [usagePrefs, setUsagePrefs] = useState(null); const isAdmin = memberRoleSummary(member, t).isAdmin; useEffect(() => { if (!isAdmin) return; void (async () => { try { const res = await repos.adminUsers.listUsersPage({ page, pageSize, query, }); setUsers(res.list); setTotal(res.total); setPage(res.page); } catch (e) { setError(formatApiError(e, "admin.users.loadFail")); } })(); }, [repos.adminUsers, tick, isAdmin, page, pageSize, query]); const selectedUid = selected?.uid ?? ""; const selectedRolesKey = selected?.roles?.slice().sort().join(",") ?? ""; useEffect(() => { if (!selectedUid || !selected) return; setRoleDraft(selected.roles.includes("admin") ? ["member", "admin"] : ["member"]); // eslint-disable-next-line react-hooks/exhaustive-deps -- selectedRolesKey tracks role changes }, [selectedUid, selectedRolesKey]); useEffect(() => { if (!selectedUid || !isAdmin || isLive) { setUsagePrefs(null); return; } void repos.usage.getMemberPrefs(selectedUid).then(setUsagePrefs).catch(() => setUsagePrefs(null)); }, [selectedUid, isAdmin, isLive, repos.usage, tick]); // 路由已包 RequireAdmin;此處僅防守 if (!member) return ; if (!isAdmin) return ; async function reloadList() { const res = await repos.adminUsers.listUsersPage({ page, pageSize, query }); setUsers(res.list); setTotal(res.total); setPage(res.page); if (selected) { const u = await repos.adminUsers.getUser(selected.uid); setSelected(u); } refresh(); } async function createMember() { setBusy("create"); setMessage(""); setError(""); setCopyHint(""); const pw = createPassword.trim(); if (pw) { const policy = checkPasswordPolicy(pw); if (!policy.ok) { setError(t(policy.errorKey || "password.policy.hint")); setBusy(""); return; } } try { const res = await repos.adminUsers.createMember({ display_name: createName, email: createEmail, password: pw || undefined, roles: createAsAdmin ? ["member", "admin"] : ["member"], email_verified: createVerified, }); const rec: RevealedTempPassword = { uid: res.user.uid, display_name: res.user.display_name, email: res.user.email, password: res.temporary_password, created_at: nowUnixNano(), reason: "create", }; saveRevealedTemp(rec); setRevealed(rec); setCreateName(""); setCreateEmail(""); setCreatePassword(""); setCreateVerified(true); setCreateAsAdmin(false); setShowCreate(false); setSelected(res.user); setQuery(""); setQueryDraft(""); setPage(1); setMessage(t("admin.users.created", { name: res.user.display_name })); await reloadList(); } catch (e) { setError(formatApiError(e, "admin.users.createFail")); } finally { setBusy(""); } } async function saveUsageUnlimited(unlimited: boolean) { if (!selected) return; setBusy(`usage:${selected.uid}`); setMessage(""); setError(""); try { const next = await repos.usage.setMemberPrefs(selected.uid, { unlimited }); setUsagePrefs(next); setMessage( unlimited ? t("admin.users.unlimitedOn", { name: selected.display_name }) : t("admin.users.unlimitedOff", { name: selected.display_name }), ); } catch (e) { setError(formatApiError(e, "admin.users.updateFail")); } finally { setBusy(""); } } async function saveUsagePlan(plan_id: PlanId) { if (!selected) return; setBusy(`usage:${selected.uid}`); setMessage(""); setError(""); try { const next = await repos.usage.setMemberPrefs(selected.uid, { plan_id }); setUsagePrefs(next); setMessage(t("admin.users.planSet", { name: selected.display_name, plan: PLANS[plan_id].name })); } catch (e) { setError(formatApiError(e, "admin.users.updateFail")); } finally { setBusy(""); } } async function toggleSuspended(u: MemberAdminView) { const willSuspend = u.status !== "suspended"; if ( !window.confirm( willSuspend ? t("admin.users.confirmSuspend", { name: u.display_name }) : t("admin.users.confirmUnsuspend", { name: u.display_name }), ) ) { return; } setBusy(`suspend:${u.uid}`); setMessage(""); setError(""); try { const next = await repos.adminUsers.setSuspended(u.uid, willSuspend); setMessage( willSuspend ? t("admin.users.didSuspend", { name: next.display_name }) : t("admin.users.didUnsuspend", { name: next.display_name }), ); setSelected(next); await reloadList(); } catch (e) { setError( formatApiError( e, willSuspend ? "admin.users.suspendFail" : "admin.users.unsuspendFail", ), ); } finally { setBusy(""); } } async function toggleVerified(u: MemberAdminView) { setBusy(`verify:${u.uid}`); setMessage(""); setError(""); try { const next = await repos.adminUsers.setEmailVerified(u.uid, !u.email_verified); setMessage( next.email_verified ? t("admin.users.markedVerified", { name: next.display_name }) : t("admin.users.markedUnverified", { name: next.display_name }), ); if (member?.uid === u.uid) await reload(); await reloadList(); } catch (e) { setError(formatApiError(e, "admin.users.updateFail")); } finally { setBusy(""); } } async function saveRoles(u: MemberAdminView) { setBusy(`roles:${u.uid}`); setMessage(""); setError(""); try { const next = await repos.adminUsers.setRoles(u.uid, roleDraft); setMessage( t("admin.users.rolesUpdated", { name: next.display_name, roles: next.roles.map((r) => roleLabel(r, t)).join(t("common.listSep")), }), ); if (member?.uid === u.uid) await reload(); await reloadList(); } catch (e) { setError(formatApiError(e, "admin.users.rolesFail")); } finally { setBusy(""); } } async function doReset(u: MemberAdminView, useCustom: boolean) { if ( !window.confirm(t("admin.users.confirmReset", { name: u.display_name })) ) { return; } if (useCustom) { const policy = checkPasswordPolicy(customPw.trim()); if (!policy.ok) { setError(t(policy.errorKey || "password.policy.hint")); return; } } setBusy(`reset:${u.uid}`); setMessage(""); setError(""); setCopyHint(""); try { const res = await repos.adminUsers.resetPassword( u.uid, useCustom ? customPw.trim() || undefined : undefined, ); const rec: RevealedTempPassword = { uid: res.user.uid, display_name: res.user.display_name, email: res.user.email, password: res.temporary_password, created_at: nowUnixNano(), reason: "reset", }; saveRevealedTemp(rec); setRevealed(rec); setCustomPw(""); setMessage(t("admin.users.resetDone", { name: res.user.display_name })); await reloadList(); } catch (e) { setError(formatApiError(e, "admin.users.resetFail")); } finally { setBusy(""); } } async function copyTempPassword() { if (!revealed?.password) return; try { await navigator.clipboard.writeText(revealed.password); setCopyHint(t("admin.users.copied")); window.setTimeout(() => setCopyHint(""), 2500); } catch { try { const ta = document.createElement("textarea"); ta.value = revealed.password; ta.style.position = "fixed"; ta.style.left = "-9999px"; document.body.appendChild(ta); ta.select(); document.execCommand("copy"); document.body.removeChild(ta); setCopyHint(t("admin.users.copied")); window.setTimeout(() => setCopyHint(""), 2500); } catch { setCopyHint(t("admin.users.copyFail")); } } } function dismissTempPassword() { if ( !window.confirm(t("admin.users.confirmDismissTemp")) ) { return; } saveRevealedTemp(null); setRevealed(null); setCopyHint(""); } function applySearch() { setPage(1); setQuery(queryDraft.trim()); } const isSuspended = selected?.status === "suspended"; return ( <> {message ? (

{message}

) : null} {error ? (

{error}

) : null} {revealed ? (

{revealed.reason === "create" ? t("admin.users.tempPwNew") : t("admin.users.tempPw")} {t("admin.users.tempPwPersist")}

{revealed.display_name} · {revealed.email} {" · "} {formatLocalDateTime(revealed.created_at)}
{revealed.password}
{copyHint ? (

{copyHint}

) : null}
) : null}
{showCreate ? (
setCreateName(e.target.value)} placeholder={t("admin.users.displayNamePh")} autoComplete="off" /> setCreateEmail(e.target.value)} placeholder="name@example.com" autoComplete="off" /> setCreatePassword(e.target.value)} placeholder={t("admin.users.initPasswordPh")} autoComplete="new-password" />
) : null}
setQueryDraft(e.target.value)} placeholder={t("admin.users.searchPh")} onKeyDown={(e) => { if (e.key === "Enter") { e.preventDefault(); applySearch(); } }} /> {query ? ( ) : null}
{query ? (

{t("admin.users.searchActive", { query })}

) : null} {users.length === 0 ? ( ) : (
    {users.map((u) => { const role = memberRoleSummary(u, t); const active = selected?.uid === u.uid; const suspended = u.status === "suspended"; return (
  • ); })}
)}
{!selected ? (

{t("admin.users.pick")}

) : (

{selected.display_name}

{selected.email}

{t("invite.openFromAdmin")}

{selected.uid === member.uid ? {t("admin.users.you")} : null} {isSuspended ? ( {t("admin.users.suspended")} ) : ( {t("admin.users.active")} )}
{t("admin.users.status")}
{isSuspended ? t("admin.users.suspended") : t("admin.users.active")}
{t("admin.users.role")}
{selected.roles.map((r) => roleLabel(r, t)).join(t("common.listSep"))}
{t("invite.codeLabel")}
{selected.invite_code || t("common.dash")}
{t("invite.parentField")}
{selected.parent_uid ? ( {selected.parent_uid} ) : ( t("invite.root") )}
{t("admin.users.emailVerify")}
{selected.email_verified ? t("role.verified") : t("role.unverified")}
{t("admin.users.bio")}
{selected.bio?.trim() || t("common.dash")}
{t("admin.users.timezone")}
{selected.timezone || t("common.dash")}
{t("admin.users.notifyEmail")}
{selected.notify_email ? t("admin.users.on") : t("admin.users.off")}
uid
{selected.uid}
{t("admin.users.createdAt")}
{formatLocalDateTime(selected.created_at)}
{t("admin.users.updatedAt")}
{formatLocalDateTime(selected.updated_at)}
{/* 停權 */}

{t("admin.users.accountStatus")}

{/* 用量:方案 + 不擋額度(live 後端 M3 才有) */}

{t("admin.users.usageTitle")}

{isLive ? (

{t("admin.users.usageLiveSkip")}

) : usagePrefs ? ( <>
{usagePrefs.unlimited ? ( {t("admin.users.unlimited")} ) : ( {t("admin.users.byPlan")} )}

{t("admin.users.unlimitedHint")}

) : (

{t("admin.users.loadingUsage")}

)}
{/* 權限 */}

{t("admin.users.assignRoles")}

setCustomPw(e.target.value)} placeholder={t("admin.users.customPwPh")} disabled={isSuspended} />
)}
); }