Files
MikrotikManager/components/data-grids/users-expanded-detail.tsx
T
Denozordec 7755d77340
Docker images / prepare-release (push) Successful in 15s
Docker images / backend-test (push) Successful in 2m32s
Docker images / frontend-image (push) Successful in 4m19s
Docker images / updater-image (push) Successful in 50s
Docker images / backend-image (push) Successful in 2m40s
Docker images / notify-webhook (push) Skipped
Docker images / publish-release (push) Successful in 8s
feat(ipsec): управление IKEv2/IPsec VPN и клиентами из одного окна
- мастер инициализации сервера: CA и серверный сертификаты, peer/profile/proposal, пул, mode-config, policy-template, managed NAT masquerade
- клиенты по сертификату (RSA) и PSK: статический IP или из пула, онлайн-статус по active-peers
- скачивание .p12 и strongSwan .sswan с инструкцией, перекачка с новой passphrase
- история изменений (config_revisions, секция ipsec) и restore только managed-объектов
- привязка IPsec-клиентов к пользователям приложения по Common Name
- страница /ipsec с KPI и вкладками Клиенты/Сервер/CLI, сайдбар, command palette
2026-09-12 20:20:56 +07:00

135 lines
5.3 KiB
TypeScript

"use client"
import { Badge } from "@/components/reui/badge"
import { IconTile } from "@/components/reui/icon-tile"
import { Flag } from "@/components/flag"
import { cn } from "@/lib/utils"
import {
ALL_SECTIONS,
groupBindingsByServer,
IFACE_TYPE_LABEL,
summarizeUserAccess,
type AppUser,
type InterfaceType,
} from "@/lib/users"
import { CableIcon, KeyRoundIcon, LockIcon, NetworkIcon, ShieldIcon } from "lucide-react"
const TYPE_VARIANT: Record<InterfaceType, "outline" | "info-light" | "success-light" | "secondary"> = {
ether: "outline",
gre: "info-light",
wg: "success-light",
ipsec: "info-light",
other: "secondary",
}
const TYPE_ICON: Record<InterfaceType, { icon: typeof CableIcon; className: string }> = {
ether: { icon: CableIcon, className: "text-muted-foreground" },
gre: { icon: NetworkIcon, className: "text-info" },
wg: { icon: ShieldIcon, className: "text-success" },
ipsec: { icon: LockIcon, className: "text-info" },
other: { icon: CableIcon, className: "text-muted-foreground" },
}
interface UsersExpandedDetailProps {
user: AppUser
serversCount: number
allSectionsCount?: number
}
function UsersExpandedDetail({
user,
serversCount,
allSectionsCount = ALL_SECTIONS.length,
}: UsersExpandedDetailProps) {
const access = summarizeUserAccess(user, serversCount, allSectionsCount)
const groups = groupBindingsByServer(user.bindings)
return (
<div className="flex flex-col gap-5 px-5 py-5 bg-muted/20">
<div className="flex flex-wrap items-center gap-x-6 gap-y-2 text-xs">
<span className="text-muted-foreground">
Разделы:{" "}
<span className="font-mono text-foreground">
{access.sectionsGranted}/{access.sectionsTotal}
</span>
</span>
<span className="text-muted-foreground">
Серверы:{" "}
<span className="font-mono text-foreground">
{access.serversGranted}/{access.serversTotal}
</span>
</span>
{access.writeKind === "full" ? (
<Badge variant="success-light" size="sm">Полный доступ</Badge>
) : access.writeKind === "none" ? (
<Badge variant="outline" size="sm">Только просмотр</Badge>
) : (
<span className="inline-flex flex-wrap items-center gap-1">
{access.writeSections.map((section) => (
<Badge key={section} variant="info-light" size="sm">{section}</Badge>
))}
</span>
)}
</div>
{groups.length === 0 ? (
<p className="text-xs text-muted-foreground">Нет привязанных интерфейсов</p>
) : (
groups.map((g) => {
const items = g.types.flatMap((tg) => tg.items)
return (
<div key={g.serverId}>
<div className="flex items-center gap-3 mb-3">
<p className="text-[11px] font-semibold text-muted-foreground uppercase tracking-wider">
{g.serverName}
</p>
<Flag code={g.serverCountry} size={12} />
<span className="text-[11px] font-mono text-muted-foreground">{g.serverSite}</span>
</div>
<div className="grid grid-cols-2 sm:grid-cols-3 xl:grid-cols-5 gap-2">
{items.map((b) => {
const meta = TYPE_ICON[b.interfaceType]
const Icon = b.interfaceType === "wg" && b.peerPublicKey ? KeyRoundIcon : meta.icon
const iconClass = b.interfaceType === "wg" && b.peerPublicKey ? "text-success" : meta.className
return (
<div
key={b.id}
className="flex items-start gap-2 rounded-md border px-3 py-2.5"
>
<IconTile variant="elevated" className={cn("size-10.5 shrink-0", iconClass)}>
<Icon />
</IconTile>
<div className="min-w-0">
<p className="text-xs font-mono font-medium leading-tight truncate">
{b.interfaceType === "wg" && (b.peerName || b.peerPublicKey)
? `${b.peerName || "peer"} · ${b.interfaceName}`
: b.interfaceName}
</p>
<div className="flex flex-wrap items-center gap-1 mt-0.5">
<Badge variant={TYPE_VARIANT[b.interfaceType]} size="sm">
{IFACE_TYPE_LABEL[b.interfaceType]}
</Badge>
{b.interfaceType === "wg" && !(b.peerPublicKey ?? "") ? (
<Badge variant="warning-light" size="sm">весь интерфейс</Badge>
) : null}
{b.comment ? (
<span className="text-[10px] text-muted-foreground leading-tight truncate">
{b.comment}
</span>
) : null}
</div>
</div>
</div>
)
})}
</div>
</div>
)
})
)}
</div>
)
}
export { UsersExpandedDetail, TYPE_VARIANT, type UsersExpandedDetailProps }