import * as React from "react" import DOMPurify from "dompurify" import { useSearchParams } from "react-router-dom" import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query" import { CheckCircle2, Copy, Globe2, Mailbox, MoreHorizontal, Plus, RefreshCcw, Search, ShieldCheck, Trash2, Users } from "lucide-react" import { api, AdminUser, Alias, DNSRecord, Domain, Mailbox as MailboxType, MailMessage, MailTemplate, SystemSettings } from "@/lib/api" import { cn, formatBytes, formatDate } from "@/lib/utils" import { Button } from "@/components/ui/button" import { Input } from "@/components/ui/input" import { Label } from "@/components/ui/label" import { Badge } from "@/components/ui/badge" import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card" import { Checkbox } from "@/components/ui/checkbox" import { Dialog, DialogContent, DialogFooter, DialogHeader, DialogTitle, DialogTrigger } from "@/components/ui/dialog" import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuSeparator, DropdownMenuTrigger } from "@/components/ui/dropdown-menu" import { ScrollArea } from "@/components/ui/scroll-area" import { Separator } from "@/components/ui/separator" import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select" import { Switch } from "@/components/ui/switch" import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table" import { Textarea } from "@/components/ui/textarea" import { useToast } from "@/hooks/use-toast" type Section = "overview" | "users" | "domains" | "mailboxes" | "aliases" | "messages" | "settings" const sectionLabels: Record = { overview: "概览", users: "用户", domains: "域名", mailboxes: "邮箱账号", aliases: "别名转发", messages: "全部邮件", settings: "系统设置", } const sectionKeys = Object.keys(sectionLabels) as Section[] export function AdminPage() { const overview = useQuery({ queryKey: ["admin", "overview"], queryFn: api.adminOverview }) const users = useQuery({ queryKey: ["admin", "users"], queryFn: api.users }) const domains = useQuery({ queryKey: ["admin", "domains"], queryFn: api.domains }) const mailboxes = useQuery({ queryKey: ["admin", "mailboxes"], queryFn: api.mailboxes }) const aliases = useQuery({ queryKey: ["admin", "aliases"], queryFn: api.aliases }) const settings = useQuery({ queryKey: ["admin", "settings"], queryFn: api.systemSettings }) const [params, setParams] = useSearchParams() const domainItems = domains.data?.items || [] const mailboxItems = mailboxes.data?.items || [] const aliasItems = aliases.data?.items || [] const userItems = users.data?.items || [] const rawSection = params.get("section") as Section | null const section: Section = rawSection && sectionKeys.includes(rawSection) ? rawSection : "overview" return (

{sectionLabels[section]}

{sectionKeys.map((key) => ( ))}
{section === "overview" && (
} label="用户" value={overview.data?.users || 0} /> } label="域名" value={overview.data?.domains || 0} /> } label="邮箱账号" value={overview.data?.mailboxes || 0} /> } label="存储" value={formatBytes(overview.data?.storageBytes || 0)} />
)} {section === "overview" && } {section === "users" && } {section === "domains" && } {section === "mailboxes" && } {section === "aliases" && } {section === "messages" && } {section === "settings" && }
) } function OverviewSection({ overview, domains }: { overview?: { activeUsers: number; activeMailboxes: number; aliases: number; messages: number; unreadMessages: number }; domains: Domain[] }) { return (
系统状态 DNS 状态 {domains.map((domain) => )} {domains.length === 0 && }
) } function UsersSection({ users }: { users: AdminUser[] }) { const qc = useQueryClient() const { toast } = useToast() const [query, setQuery] = React.useState("") const [roleFilter, setRoleFilter] = React.useState("all") const [statusFilter, setStatusFilter] = React.useState("all") const filteredUsers = users.filter((user) => { const keyword = query.trim().toLowerCase() const matchesKeyword = !keyword || [user.email, user.displayName, ...(user.mailboxes || [])].some((value) => value.toLowerCase().includes(keyword)) const matchesRole = roleFilter === "all" || user.role === roleFilter const matchesStatus = statusFilter === "all" || (statusFilter === "active" ? !user.disabled : user.disabled) return matchesKeyword && matchesRole && matchesStatus }) const remove = useMutation({ mutationFn: api.deleteUser, onSuccess: () => { invalidateAdmin(qc); toast({ title: "用户已删除" }) }, onError: (e) => toast({ title: "删除失败", description: e.message }) }) return (
用户管理
setQuery(event.target.value)} placeholder="搜索用户、邮箱、显示名称" className="pl-9" />
用户角色邮箱状态创建时间 {filteredUsers.map((user) => (
{user.displayName}
{user.email}
{user.role === "admin" ? "管理员" : "普通用户"} {user.disabled ? "停用" : "正常"} {new Date(user.createdAt).toLocaleDateString()} remove.mutate(user.id)} />
))}
{filteredUsers.length === 0 && }
) } function DomainsSection({ domains }: { domains: Domain[] }) { const qc = useQueryClient() const { toast } = useToast() const update = useMutation({ mutationFn: ({ id, status }: { id: string; status: string }) => api.updateDomain(id, { status }), onSuccess: () => { invalidateAdmin(qc); toast({ title: "域名已更新" }) }, onError: (e) => toast({ title: "更新失败", description: e.message }) }) const remove = useMutation({ mutationFn: api.deleteDomain, onSuccess: () => { invalidateAdmin(qc); toast({ title: "域名已删除" }) }, onError: (e) => toast({ title: "删除失败", description: e.message }) }) return ( 域名管理 {domains.map((domain) => (
{domain.name}
selector: {domain.dkimSelector}
{domain.status === "active" ? "启用" : "停用"} {domain.dnsStatus === "ok" ? "DNS 正常" : domain.dnsStatus}
))} {domains.length === 0 && }
) } function DomainDNSDialog({ domain }: { domain: Domain }) { return ( {domain.name} DNS ) } function MailboxesSection({ mailboxes, users }: { mailboxes: MailboxType[]; users: AdminUser[] }) { const qc = useQueryClient() const { toast } = useToast() const remove = useMutation({ mutationFn: api.deleteMailbox, onSuccess: () => { invalidateAdmin(qc); toast({ title: "邮箱已删除" }) }, onError: (e) => toast({ title: "删除失败", description: e.message }) }) return ( 邮箱账号管理 地址归属用户名称配额状态 {mailboxes.map((mailbox) => ( {mailbox.address} {mailbox.userEmail || mailbox.userId} {mailbox.displayName} {mailbox.quotaMb} MB {mailbox.status === "active" ? "启用" : "停用"} remove.mutate(mailbox.id)} /> ))}
) } function AliasesSection({ aliases, domains }: { aliases: Alias[]; domains: Domain[] }) { const qc = useQueryClient() const { toast } = useToast() const update = useMutation({ mutationFn: ({ id, payload }: { id: string; payload: { source: string; destination: string; enabled: boolean } }) => api.updateAlias(id, payload), onSuccess: () => { invalidateAdmin(qc); toast({ title: "别名已更新" }) }, onError: (e) => toast({ title: "更新失败", description: e.message }) }) const remove = useMutation({ mutationFn: api.deleteAlias, onSuccess: () => { invalidateAdmin(qc); toast({ title: "别名已删除" }) }, onError: (e) => toast({ title: "删除失败", description: e.message }) }) return ( 别名/转发管理 来源目标域名状态 {aliases.map((alias) => ( {alias.source} {alias.destination} {domains.find((d) => d.id === alias.domainId)?.name || alias.domainId} {alias.enabled ? "启用" : "停用"} update.mutate({ id: alias.id, payload: { source: alias.source, destination: alias.destination, enabled: !alias.enabled } })} onDelete={() => remove.mutate(alias.id)} /> ))}
) } function AdminMessagesSection({ mailboxes }: { mailboxes: MailboxType[] }) { const qc = useQueryClient() const [query, setQuery] = React.useState("") const [mailboxId, setMailboxId] = React.useState("all") const [folder, setFolder] = React.useState("all") const [selectedId, setSelectedId] = React.useState(null) const messages = useQuery({ queryKey: ["admin", "messages", mailboxId, folder, query], queryFn: () => api.adminMessages({ mailboxId: mailboxId === "all" ? "" : mailboxId, folder: folder === "all" ? "" : folder, q: query, }), }) const detail = useQuery({ queryKey: ["admin", "message", selectedId], queryFn: () => api.adminMessage(selectedId!), enabled: !!selectedId }) const items = messages.data?.items || [] return (
全部邮件
setQuery(event.target.value)} placeholder="搜索主题、发件人、收件人、邮箱" className="pl-9" />
邮件 邮箱 发件人 收件人 文件夹 时间 {items.map((message) => (
{message.subject}
{message.snippet}
{message.mailboxAddress || message.recipientAddress || "-"}
{message.ownerEmail &&
{message.ownerEmail}
}
{message.from} {message.recipientAddress || message.to.join(", ")} {folderName(message.folder)} {formatDate(message.receivedAt)}
))}
{messages.isLoading && } {!messages.isLoading && items.length === 0 && }
{ if (!open) setSelectedId(null) }} />
) } function SystemSettingsSection({ settings, domains }: { settings?: SystemSettings; domains: Domain[] }) { const qc = useQueryClient() const { toast } = useToast() const templates = useQuery({ queryKey: ["admin", "mail-templates"], queryFn: api.mailTemplates }) const [settingsTab, setSettingsTab] = React.useState<"base" | "smtp" | "storage" | "mail" | "templates" | "security">("base") const [smtpRequireTls, setSmtpRequireTls] = React.useState(false) const [allowInsecureHttp, setAllowInsecureHttp] = React.useState(true) const [openRegistration, setOpenRegistration] = React.useState(false) const [twoFactorEnabled, setTwoFactorEnabled] = React.useState(false) const [turnstileEnabled, setTurnstileEnabled] = React.useState(false) const [catchAllEnabled, setCatchAllEnabled] = React.useState(false) const [mailAutoRefresh, setMailAutoRefresh] = React.useState(true) const [userMailboxApplyEnabled, setUserMailboxApplyEnabled] = React.useState(false) const [userMailboxDomainIds, setUserMailboxDomainIds] = React.useState([]) React.useEffect(() => { if (!settings) return setSmtpRequireTls(settings.smtpRequireTls) setAllowInsecureHttp(settings.allowInsecureHttp) setOpenRegistration(settings.openRegistration) setTwoFactorEnabled(settings.twoFactorEnabled) setTurnstileEnabled(settings.turnstileEnabled) setCatchAllEnabled(settings.catchAllEnabled) setMailAutoRefresh(settings.mailAutoRefresh) setUserMailboxApplyEnabled(settings.userMailboxApplyEnabled) setUserMailboxDomainIds(settings.userMailboxDomainIds || []) }, [settings]) const save = useMutation({ mutationFn: (form: FormData) => api.updateSystemSettings({ publicHostname: fieldValue(form, "publicHostname", settings?.publicHostname || ""), publicBaseUrl: fieldValue(form, "publicBaseUrl", settings?.publicBaseUrl || ""), smtpHost: fieldValue(form, "smtpHost", settings?.smtpHost || ""), smtpPort: fieldValue(form, "smtpPort", settings?.smtpPort || "25"), smtpUsername: fieldValue(form, "smtpUsername", settings?.smtpUsername || ""), smtpPassword: fieldValue(form, "smtpPassword", ""), smtpRequireTls, maildirRoot: fieldValue(form, "maildirRoot", settings?.maildirRoot || ""), maildirScanSeconds: fieldNumber(form, "maildirScanSeconds", settings?.maildirScanSeconds || 30), sessionTtlHours: fieldNumber(form, "sessionTtlHours", settings?.sessionTtlHours || 168), allowInsecureHttp, openRegistration, twoFactorEnabled, turnstileEnabled, turnstileSiteKey: fieldValue(form, "turnstileSiteKey", settings?.turnstileSiteKey || ""), turnstileSecretKey: fieldValue(form, "turnstileSecretKey", ""), catchAllEnabled, mailAutoRefresh, mailRefreshSeconds: fieldNumber(form, "mailRefreshSeconds", settings?.mailRefreshSeconds || 30), userMailboxApplyEnabled, userMailboxDomainIds, reservedMailboxPrefixes: fieldValue(form, "reservedMailboxPrefixes", settings?.reservedMailboxPrefixes || ""), }), onSuccess: () => { qc.invalidateQueries({ queryKey: ["admin", "settings"] }) qc.invalidateQueries({ queryKey: ["dns-records"] }) qc.invalidateQueries({ queryKey: ["public-settings"] }) toast({ title: "系统设置已保存" }) }, onError: (e) => toast({ title: "保存失败", description: e.message }), }) const formKey = settings ? [ settings.publicHostname, settings.publicBaseUrl, settings.smtpHost, settings.smtpPort, settings.smtpUsername, settings.smtpPasswordSet, settings.smtpRequireTls, settings.maildirRoot, settings.maildirScanSeconds, settings.sessionTtlHours, settings.allowInsecureHttp, settings.openRegistration, settings.twoFactorEnabled, settings.turnstileEnabled, settings.turnstileSiteKey, settings.turnstileSecretSet, settings.catchAllEnabled, settings.mailAutoRefresh, settings.mailRefreshSeconds, settings.userMailboxApplyEnabled, (settings.userMailboxDomainIds || []).join(","), settings.reservedMailboxPrefixes, ].join("|") : "loading" const tabs: { key: typeof settingsTab; label: string }[] = [ { key: "base", label: "基础" }, { key: "smtp", label: "SMTP" }, { key: "storage", label: "存储" }, { key: "mail", label: "邮件" }, { key: "templates", label: "模板" }, { key: "security", label: "安全" }, ] return (
{ event.preventDefault(); save.mutate(new FormData(event.currentTarget)) }} className="space-y-6">
{tabs.map((tab) => ( ))}
{settingsTab === "base" && 基础设置 } {settingsTab === "smtp" &&
SMTP 设置
} {settingsTab === "storage" && 存储设置 } {settingsTab === "mail" && 邮件设置 {userMailboxApplyEnabled && (
{domains.map((domain) => { const checked = userMailboxDomainIds.includes(domain.id) const disabled = domain.status !== "active" return ( ) })}
{domains.length === 0 && }