Docker images / prepare-release (push) Successful in 10s
Docker images / backend-image (push) Successful in 1m57s
Docker images / frontend-image (push) Successful in 4m8s
Docker images / notify-webhook (push) Skipped
Docker images / updater-image (push) Successful in 44s
Docker images / publish-release (push) Successful in 11s
Added user management functionality, including the creation of app_users and user_interface_bindings tables in the database. Implemented API routes for user data retrieval and permissions handling. Enhanced the traffic monitoring system to include user traffic statistics and interface bindings. Updated relevant components and services to support the new user features, improving overall application functionality and user experience.
292 lines
10 KiB
TypeScript
292 lines
10 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
|
|
comment: 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 CatalogIface {
|
|
name: string
|
|
type: InterfaceType
|
|
running: boolean
|
|
disabled: boolean
|
|
boundUserId: string | null
|
|
boundUserLogin: string | null
|
|
}
|
|
|
|
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: "[email protected]" },
|
|
{ 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: "[email protected]" },
|
|
{ name: "gre-datacenter", type: "gre", running: true, disabled: false, boundUserId: "u1", boundUserLogin: "[email protected]" },
|
|
{ name: "gre-spb-branch", type: "gre", running: true, disabled: false, boundUserId: "u2", boundUserLogin: "[email protected]" },
|
|
{ name: "gre-retail-01", type: "gre", running: false, disabled: false, boundUserId: "u4", boundUserLogin: "[email protected]" },
|
|
{ name: "wg-msk-spb", type: "wg", running: true, disabled: false, boundUserId: "u1", boundUserLogin: "[email protected]" },
|
|
],
|
|
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: "[email protected]" },
|
|
{ name: "gre-warehouse", type: "gre", running: false, disabled: false, boundUserId: "u1", boundUserLogin: "[email protected]" },
|
|
{ name: "gre-spb-branch", type: "gre", running: true, disabled: false, boundUserId: "u2", boundUserLogin: "[email protected]" },
|
|
{ name: "wg-lab", type: "wg", running: true, disabled: false, boundUserId: null, boundUserLogin: null },
|
|
],
|
|
}
|
|
|
|
function bind(
|
|
id: string,
|
|
userId: string,
|
|
serverId: string,
|
|
interfaceName: string,
|
|
interfaceType: InterfaceType,
|
|
comment: 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,
|
|
comment,
|
|
}
|
|
}
|
|
|
|
export const INIT_USERS: AppUser[] = [
|
|
{
|
|
id: "u1", name: "Александр Коротаев", login: "[email protected]", email: "[email protected]",
|
|
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"),
|
|
bind("b5", "u1", "srv7", "gre-office-msk", "gre", "Офис LAB"),
|
|
bind("b6", "u1", "srv7", "gre-warehouse", "gre", "Склад"),
|
|
],
|
|
},
|
|
{
|
|
id: "u2", name: "Дмитрий Фёдоров", login: "[email protected]", email: "[email protected]",
|
|
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: "[email protected]", email: "[email protected]",
|
|
role: "viewer", last: "вчера", avatar: "МС", active: true,
|
|
sections: defaultSections("viewer"), servers: defaultServers("viewer"),
|
|
bindings: [],
|
|
},
|
|
{
|
|
id: "u4", name: "Игорь Петров", login: "[email protected]", email: "[email protected]",
|
|
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) => {
|
|
const owner = users.find((u) =>
|
|
u.bindings.some((b) => b.serverId === serverId && b.interfaceName === iface.name),
|
|
)
|
|
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,
|
|
}
|
|
}
|