Docker images / prepare-release (push) Successful in 7s
Docker images / backend-image (push) Successful in 1m37s
Docker images / frontend-image (push) Successful in 2m44s
Docker images / notify-webhook (push) Skipped
Docker images / updater-image (push) Successful in 40s
Docker images / publish-release (push) Successful in 10s
Added support for managing peer information in interface bindings, including new fields for peerPublicKey and peerName in the BoundIfaceTraffic interface. Updated the database schema to include these fields in user_interface_bindings and traffic_samples tables. Enhanced the UI components to display peer details alongside interface names, improving user experience and clarity in the traffic management system. Updated relevant functions and services to handle peer-specific logic, ensuring robust integration across the application.
357 lines
13 KiB
TypeScript
357 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",
|
|
other: "Прочие",
|
|
}
|
|
|
|
export const IFACE_TYPE_ORDER: InterfaceType[] = ["ether", "gre", "wg", "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,
|
|
}
|
|
}
|