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
- мастер инициализации сервера: 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
135 lines
5.3 KiB
TypeScript
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 }
|