feat(mcp): подготовить сервер и доступ к приёмке
This commit is contained in:
commit
bf3d50e1ff
3835 files changed
+537122
No files matched your search
+360
@@ -0,0 +1,360 @@
|
||||
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 | null
|
||||
disabled: boolean | null
|
||||
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",
|
||||
bridge: "Bridge",
|
||||
vlan: "VLAN",
|
||||
vxlan: "VXLAN",
|
||||
gre: "GRE",
|
||||
wg: "WireGuard",
|
||||
ipsec: "IPsec",
|
||||
other: "Прочие",
|
||||
}
|
||||
|
||||
export const IFACE_TYPE_ORDER: InterfaceType[] = ["ether", "bridge", "vlan", "vxlan", "gre", "wg", "ipsec", "other"]
|
||||
|
||||
export const ROLE_LABEL: Record<Role, string> = {
|
||||
admin: "Администратор",
|
||||
operator: "Оператор",
|
||||
viewer: "Наблюдатель",
|
||||
}
|
||||
|
||||
export const ROLE_COLOR: Record<Role, string> = {
|
||||
admin: "bg-primary/10 text-primary border border-primary/20",
|
||||
operator: "bg-info/10 text-info border border-info/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-info/10 text-info",
|
||||
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,
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user