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
358 lines
13 KiB
TypeScript
358 lines
13 KiB
TypeScript
import { servers } from "@/lib/data"
|
|
import type { InterfaceType, PermLevel, AppUserRole as Role } from "@mmapp/contracts/users"
|
|
|
|
export type { InterfaceType, PermLevel, Role }
|
|
|
|
export interface SectionPerm { section: string; level: PermLevel }
|
|
export interface ServerPerm { serverId: string; level: PermLevel }
|
|
|
|
export interface InterfaceBinding {
|
|
id: string
|
|
userId: string
|
|
serverId: string
|
|
serverName: string
|
|
serverSite: string
|
|
serverCountry: string
|
|
interfaceName: string
|
|
interfaceType: InterfaceType
|
|
peerPublicKey?: string
|
|
peerName?: string
|
|
comment: string
|
|
}
|
|
|
|
export interface CatalogPeer {
|
|
publicKey: string
|
|
name: string
|
|
comment: string
|
|
allowedIps: string[]
|
|
latestHandshake?: string
|
|
boundUserId: string | null
|
|
boundUserLogin: string | null
|
|
}
|
|
|
|
export interface CatalogIface {
|
|
name: string
|
|
type: InterfaceType
|
|
running: boolean
|
|
disabled: boolean
|
|
boundUserId: string | null
|
|
boundUserLogin: string | null
|
|
peers?: CatalogPeer[]
|
|
peersError?: string
|
|
}
|
|
|
|
export interface AppUserForm {
|
|
name: string
|
|
login: string
|
|
email: string
|
|
role: Role
|
|
active: boolean
|
|
sections: SectionPerm[]
|
|
servers: ServerPerm[]
|
|
bindings: InterfaceBinding[]
|
|
}
|
|
|
|
export interface AppUser {
|
|
id: string
|
|
name: string
|
|
login: string
|
|
email: string
|
|
role: Role
|
|
last: string
|
|
avatar: string
|
|
active: boolean
|
|
sections: SectionPerm[]
|
|
servers: ServerPerm[]
|
|
bindings: InterfaceBinding[]
|
|
}
|
|
|
|
export interface UserServerOption {
|
|
id: string
|
|
name: string
|
|
host: string
|
|
site: string
|
|
country: string
|
|
status?: "online" | "offline" | "degraded" | null
|
|
}
|
|
|
|
export const IFACE_TYPE_LABEL: Record<InterfaceType, string> = {
|
|
ether: "Ethernet",
|
|
gre: "GRE",
|
|
wg: "WireGuard",
|
|
ipsec: "IPsec",
|
|
other: "Прочие",
|
|
}
|
|
|
|
export const IFACE_TYPE_ORDER: InterfaceType[] = ["ether", "gre", "wg", "ipsec", "other"]
|
|
|
|
export const ROLE_LABEL: Record<Role, string> = {
|
|
admin: "Администратор",
|
|
operator: "Оператор",
|
|
viewer: "Наблюдатель",
|
|
}
|
|
|
|
export const ROLE_COLOR: Record<Role, string> = {
|
|
admin: "bg-violet-500/10 text-violet-600 dark:text-violet-400 border border-violet-500/20",
|
|
operator: "bg-sky-500/10 text-sky-600 dark:text-sky-400 border border-sky-500/20",
|
|
viewer: "bg-muted text-muted-foreground border border-border",
|
|
}
|
|
|
|
export const PERM_OPTS: { v: PermLevel; label: string }[] = [
|
|
{ v: "none", label: "Нет" },
|
|
{ v: "read", label: "Просмотр" },
|
|
{ v: "write", label: "Управление" },
|
|
]
|
|
|
|
export const PERM_COLOR: Record<PermLevel, string> = {
|
|
none: "bg-muted text-muted-foreground",
|
|
read: "bg-sky-500/10 text-sky-600 dark:text-sky-400",
|
|
write: "bg-success/10 text-success",
|
|
}
|
|
|
|
export const SECTION_GROUP_DEFS: { group: string; items: string[] }[] = [
|
|
{ group: "Обзор", items: ["Дашборд", "Трафик", "Карта сети", "Мониторинг"] },
|
|
{ group: "Данные", items: ["Домены", "IP-диапазоны", "ASN", "Communities"] },
|
|
{ group: "Управление", items: ["Серверы", "Фильтры", "Firewall", "GRE-туннели", "Бэкапы"] },
|
|
{ group: "Инструменты", items: ["Оптимизатор маршрутов", "OSPF", "Диагностика GRE", "Терминал"] },
|
|
{ group: "Система", items: ["Оповещения", "Сбор данных", "Пользователи", "Настройки"] },
|
|
]
|
|
|
|
export const ALL_SECTIONS = SECTION_GROUP_DEFS.flatMap((g) => g.items)
|
|
|
|
export function defaultSections(role: Role): SectionPerm[] {
|
|
return ALL_SECTIONS.map((section) => {
|
|
let level: PermLevel = "none"
|
|
if (role === "admin") level = "write"
|
|
else if (role === "operator") {
|
|
level = ["Серверы", "Пользователи", "Фильтры", "Firewall", "GRE-туннели", "Бэкапы", "Диагностика GRE", "Оптимизатор маршрутов", "OSPF"].includes(section)
|
|
? "write"
|
|
: "read"
|
|
} else if (role === "viewer") {
|
|
level = ["Настройки", "Терминал"].includes(section) ? "none" : "read"
|
|
}
|
|
return { section, level }
|
|
})
|
|
}
|
|
|
|
export function defaultServers(role: Role, serverList: { id: string }[] = servers): ServerPerm[] {
|
|
return serverList.map((s) => ({
|
|
serverId: s.id,
|
|
level: role === "admin" ? "write" : "read",
|
|
}))
|
|
}
|
|
|
|
export function userInitials(name: string): string {
|
|
return name.trim().split(/\s+/).map((p) => p[0] ?? "").slice(0, 2).join("").toUpperCase() || "??"
|
|
}
|
|
|
|
export const MOCK_IFACE_CATALOG: Record<string, CatalogIface[]> = {
|
|
srv1: [
|
|
{ name: "ether1", type: "ether", running: true, disabled: false, boundUserId: "u1", boundUserLogin: "a.korotaev@company.io" },
|
|
{ name: "ether2", type: "ether", running: true, disabled: false, boundUserId: null, boundUserLogin: null },
|
|
{ name: "gre-office-msk", type: "gre", running: true, disabled: false, boundUserId: "u1", boundUserLogin: "a.korotaev@company.io" },
|
|
{ name: "gre-datacenter", type: "gre", running: true, disabled: false, boundUserId: "u1", boundUserLogin: "a.korotaev@company.io" },
|
|
{ name: "gre-spb-branch", type: "gre", running: true, disabled: false, boundUserId: "u2", boundUserLogin: "d.fedorov@company.io" },
|
|
{ name: "gre-retail-01", type: "gre", running: false, disabled: false, boundUserId: "u4", boundUserLogin: "i.petrov@company.io" },
|
|
{
|
|
name: "wg-msk-spb", type: "wg", running: true, disabled: false, boundUserId: null, boundUserLogin: null,
|
|
peers: [
|
|
{ publicKey: "mockPeerKeyAAAA0123456789", name: "phone-ak", comment: "", allowedIps: ["10.8.0.2/32"], boundUserId: "u1", boundUserLogin: "a.korotaev@company.io" },
|
|
{ publicKey: "mockPeerKeyBBBB0123456789", name: "laptop-ak", comment: "", allowedIps: ["10.8.0.3/32"], boundUserId: null, boundUserLogin: null, latestHandshake: "12s" },
|
|
],
|
|
},
|
|
],
|
|
srv7: [
|
|
{ name: "ether1", type: "ether", running: true, disabled: false, boundUserId: null, boundUserLogin: null },
|
|
{ name: "gre-office-msk", type: "gre", running: true, disabled: false, boundUserId: "u1", boundUserLogin: "a.korotaev@company.io" },
|
|
{ name: "gre-warehouse", type: "gre", running: false, disabled: false, boundUserId: "u1", boundUserLogin: "a.korotaev@company.io" },
|
|
{ name: "gre-spb-branch", type: "gre", running: true, disabled: false, boundUserId: "u2", boundUserLogin: "d.fedorov@company.io" },
|
|
{
|
|
name: "wg-lab", type: "wg", running: true, disabled: false, boundUserId: null, boundUserLogin: null,
|
|
peers: [
|
|
{ publicKey: "mockPeerKeyLABB0123456789", name: "lab-peer", comment: "", allowedIps: ["10.9.0.2/32"], boundUserId: null, boundUserLogin: null },
|
|
],
|
|
},
|
|
],
|
|
}
|
|
|
|
export function bindingDiffKey(b: Pick<InterfaceBinding, "serverId" | "interfaceName" | "peerPublicKey">): string {
|
|
return `${b.serverId}::${b.interfaceName}::${b.peerPublicKey ?? ""}`
|
|
}
|
|
|
|
export function bindingTitle(b: Pick<InterfaceBinding, "interfaceName" | "interfaceType" | "peerName" | "peerPublicKey">): string {
|
|
if (b.interfaceType === "wg" && (b.peerName || b.peerPublicKey)) {
|
|
return `${b.peerName || "peer"} · ${b.interfaceName}`
|
|
}
|
|
return b.interfaceName
|
|
}
|
|
|
|
function bind(
|
|
id: string,
|
|
userId: string,
|
|
serverId: string,
|
|
interfaceName: string,
|
|
interfaceType: InterfaceType,
|
|
comment: string,
|
|
peer?: { publicKey: string; name: string },
|
|
): InterfaceBinding {
|
|
const srv = servers.find((s) => s.id === serverId)
|
|
return {
|
|
id,
|
|
userId,
|
|
serverId,
|
|
serverName: srv?.name ?? serverId,
|
|
serverSite: srv?.site ?? "—",
|
|
serverCountry: srv?.country ?? "UN",
|
|
interfaceName,
|
|
interfaceType,
|
|
peerPublicKey: peer?.publicKey,
|
|
peerName: peer?.name,
|
|
comment,
|
|
}
|
|
}
|
|
|
|
export const INIT_USERS: AppUser[] = [
|
|
{
|
|
id: "u1", name: "Александр Коротаев", login: "a.korotaev@company.io", email: "a.korotaev@company.io",
|
|
role: "admin", last: "сейчас", avatar: "АК", active: true,
|
|
sections: defaultSections("admin"), servers: defaultServers("admin"),
|
|
bindings: [
|
|
bind("b1", "u1", "srv1", "ether1", "ether", "Uplink MSK"),
|
|
bind("b2", "u1", "srv1", "gre-office-msk", "gre", "Офис MSK"),
|
|
bind("b3", "u1", "srv1", "gre-datacenter", "gre", "ЦОД"),
|
|
bind("b4", "u1", "srv1", "wg-msk-spb", "wg", "Overlay SPB", { publicKey: "mockPeerKeyAAAA0123456789", name: "phone-ak" }),
|
|
bind("b5", "u1", "srv7", "gre-office-msk", "gre", "Офис LAB"),
|
|
bind("b6", "u1", "srv7", "gre-warehouse", "gre", "Склад"),
|
|
],
|
|
},
|
|
{
|
|
id: "u2", name: "Дмитрий Фёдоров", login: "d.fedorov@company.io", email: "d.fedorov@company.io",
|
|
role: "operator", last: "2ч назад", avatar: "ДФ", active: true,
|
|
sections: defaultSections("operator"), servers: defaultServers("operator"),
|
|
bindings: [
|
|
bind("b7", "u2", "srv1", "gre-spb-branch", "gre", "Филиал SPB"),
|
|
bind("b8", "u2", "srv7", "gre-spb-branch", "gre", "Филиал LAB"),
|
|
],
|
|
},
|
|
{
|
|
id: "u3", name: "Мария Соколова", login: "m.sokolova@company.io", email: "m.sokolova@company.io",
|
|
role: "viewer", last: "вчера", avatar: "МС", active: true,
|
|
sections: defaultSections("viewer"), servers: defaultServers("viewer"),
|
|
bindings: [],
|
|
},
|
|
{
|
|
id: "u4", name: "Игорь Петров", login: "i.petrov@company.io", email: "i.petrov@company.io",
|
|
role: "operator", last: "3 дн назад", avatar: "ИП", active: false,
|
|
sections: defaultSections("operator"), servers: defaultServers("operator"),
|
|
bindings: [
|
|
bind("b9", "u4", "srv1", "gre-retail-01", "gre", "Магазин #1"),
|
|
],
|
|
},
|
|
]
|
|
|
|
export function catalogForServer(serverId: string, users: AppUser[]): CatalogIface[] {
|
|
const base = MOCK_IFACE_CATALOG[serverId] ?? []
|
|
return base.map((iface) => {
|
|
if (iface.type === "wg") {
|
|
const peers = (iface.peers ?? []).map((peer) => {
|
|
const owner = users.find((u) =>
|
|
u.bindings.some((b) =>
|
|
b.serverId === serverId
|
|
&& b.interfaceName === iface.name
|
|
&& (b.peerPublicKey ?? "") === peer.publicKey,
|
|
),
|
|
)
|
|
if (!owner) return { ...peer, boundUserId: null, boundUserLogin: null }
|
|
return { ...peer, boundUserId: owner.id, boundUserLogin: owner.email || owner.login }
|
|
})
|
|
const legacy = users.find((u) =>
|
|
u.bindings.some((b) =>
|
|
b.serverId === serverId
|
|
&& b.interfaceName === iface.name
|
|
&& !(b.peerPublicKey ?? ""),
|
|
),
|
|
)
|
|
return {
|
|
...iface,
|
|
boundUserId: legacy?.id ?? null,
|
|
boundUserLogin: legacy ? (legacy.email || legacy.login) : null,
|
|
peers,
|
|
}
|
|
}
|
|
const owner = users.find((u) =>
|
|
u.bindings.some((b) => b.serverId === serverId && b.interfaceName === iface.name && !(b.peerPublicKey ?? "")),
|
|
)
|
|
if (!owner) return { ...iface, boundUserId: null, boundUserLogin: null }
|
|
return { ...iface, boundUserId: owner.id, boundUserLogin: owner.email || owner.login }
|
|
})
|
|
}
|
|
|
|
export function groupBindingsByServer(bindings: InterfaceBinding[]): Array<{
|
|
serverId: string
|
|
serverName: string
|
|
serverSite: string
|
|
serverCountry: string
|
|
types: Array<{ type: InterfaceType; items: InterfaceBinding[] }>
|
|
}> {
|
|
const byServer = new Map<string, InterfaceBinding[]>()
|
|
for (const b of bindings) {
|
|
const arr = byServer.get(b.serverId) ?? []
|
|
arr.push(b)
|
|
byServer.set(b.serverId, arr)
|
|
}
|
|
return [...byServer.entries()].map(([serverId, items]) => {
|
|
const first = items[0]
|
|
const types = IFACE_TYPE_ORDER
|
|
.map((type) => ({ type, items: items.filter((i) => i.interfaceType === type) }))
|
|
.filter((g) => g.items.length > 0)
|
|
return {
|
|
serverId,
|
|
serverName: first?.serverName ?? serverId,
|
|
serverSite: first?.serverSite ?? "—",
|
|
serverCountry: first?.serverCountry ?? "UN",
|
|
types,
|
|
}
|
|
})
|
|
}
|
|
|
|
export type UserAccessWriteKind = "full" | "none" | "partial"
|
|
|
|
export interface UserAccessSummary {
|
|
sectionsGranted: number
|
|
sectionsTotal: number
|
|
serversGranted: number
|
|
serversTotal: number
|
|
writeKind: UserAccessWriteKind
|
|
writeSections: string[]
|
|
}
|
|
|
|
export function summarizeUserAccess(
|
|
user: Pick<AppUser, "role" | "sections" | "servers">,
|
|
serversTotal: number,
|
|
sectionsTotal = ALL_SECTIONS.length,
|
|
): UserAccessSummary {
|
|
const isAdmin = user.role === "admin"
|
|
const sectionsGranted = isAdmin
|
|
? sectionsTotal
|
|
: user.sections.filter((s) => s.level !== "none").length
|
|
const serversGranted = isAdmin
|
|
? serversTotal
|
|
: user.servers.filter((s) => s.level !== "none").length
|
|
const writeSections = isAdmin
|
|
? []
|
|
: user.sections.filter((s) => s.level === "write").map((s) => s.section)
|
|
const writeKind: UserAccessWriteKind = isAdmin
|
|
? "full"
|
|
: writeSections.length === 0
|
|
? "none"
|
|
: "partial"
|
|
return {
|
|
sectionsGranted,
|
|
sectionsTotal,
|
|
serversGranted,
|
|
serversTotal,
|
|
writeKind,
|
|
writeSections,
|
|
}
|
|
}
|