Initial commit: MSP Monitor Dashboard
- Next.js 14 frontend with dark cyan/navy theme - tRPC API with Prisma ORM - MeshCentral, LibreNMS, Headwind MDM integrations - Multi-tenant architecture - Alert system with email/SMS/webhook notifications - Docker Compose deployment - Complete documentation
This commit is contained in:
305
src/server/trpc/routers/red.router.ts
Normal file
305
src/server/trpc/routers/red.router.ts
Normal file
@@ -0,0 +1,305 @@
|
||||
import { z } from 'zod'
|
||||
import { TRPCError } from '@trpc/server'
|
||||
import { router, protectedProcedure } from '../trpc'
|
||||
import { LibreNMSClient } from '@/server/services/librenms/client'
|
||||
|
||||
export const redRouter = router({
|
||||
// Listar dispositivos de red
|
||||
list: protectedProcedure
|
||||
.input(
|
||||
z.object({
|
||||
clienteId: z.string().optional(),
|
||||
tipo: z.enum(['ROUTER', 'SWITCH', 'FIREWALL', 'AP', 'IMPRESORA', 'OTRO']).optional(),
|
||||
estado: z.enum(['ONLINE', 'OFFLINE', 'ALERTA', 'MANTENIMIENTO', 'DESCONOCIDO']).optional(),
|
||||
search: z.string().optional(),
|
||||
page: z.number().default(1),
|
||||
limit: z.number().default(20),
|
||||
}).optional()
|
||||
)
|
||||
.query(async ({ ctx, input }) => {
|
||||
const { clienteId, tipo, estado, search, page = 1, limit = 20 } = input || {}
|
||||
|
||||
const tiposRed = ['ROUTER', 'SWITCH', 'FIREWALL', 'AP', 'IMPRESORA', 'OTRO'] as const
|
||||
|
||||
const where = {
|
||||
tipo: tipo ? { equals: tipo } : { in: tiposRed },
|
||||
...(ctx.user.clienteId ? { clienteId: ctx.user.clienteId } : {}),
|
||||
...(clienteId ? { clienteId } : {}),
|
||||
...(estado ? { estado } : {}),
|
||||
...(search ? {
|
||||
OR: [
|
||||
{ nombre: { contains: search, mode: 'insensitive' as const } },
|
||||
{ ip: { contains: search } },
|
||||
{ mac: { contains: search, mode: 'insensitive' as const } },
|
||||
],
|
||||
} : {}),
|
||||
}
|
||||
|
||||
const [dispositivos, total] = await Promise.all([
|
||||
ctx.prisma.dispositivo.findMany({
|
||||
where,
|
||||
include: {
|
||||
cliente: { select: { id: true, nombre: true, codigo: true } },
|
||||
ubicacion: { select: { id: true, nombre: true } },
|
||||
},
|
||||
orderBy: [{ estado: 'asc' }, { nombre: 'asc' }],
|
||||
skip: (page - 1) * limit,
|
||||
take: limit,
|
||||
}),
|
||||
ctx.prisma.dispositivo.count({ where }),
|
||||
])
|
||||
|
||||
return {
|
||||
dispositivos,
|
||||
pagination: {
|
||||
total,
|
||||
page,
|
||||
limit,
|
||||
totalPages: Math.ceil(total / limit),
|
||||
},
|
||||
}
|
||||
}),
|
||||
|
||||
// Obtener dispositivo de red por ID
|
||||
byId: protectedProcedure
|
||||
.input(z.object({ id: z.string() }))
|
||||
.query(async ({ ctx, input }) => {
|
||||
const dispositivo = await ctx.prisma.dispositivo.findUnique({
|
||||
where: { id: input.id },
|
||||
include: {
|
||||
cliente: true,
|
||||
ubicacion: true,
|
||||
alertas: {
|
||||
where: { estado: 'ACTIVA' },
|
||||
orderBy: { createdAt: 'desc' },
|
||||
take: 10,
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
if (!dispositivo) {
|
||||
throw new TRPCError({ code: 'NOT_FOUND', message: 'Dispositivo no encontrado' })
|
||||
}
|
||||
|
||||
if (ctx.user.clienteId && ctx.user.clienteId !== dispositivo.clienteId) {
|
||||
throw new TRPCError({ code: 'FORBIDDEN', message: 'Acceso denegado' })
|
||||
}
|
||||
|
||||
return dispositivo
|
||||
}),
|
||||
|
||||
// Obtener interfaces de un dispositivo
|
||||
interfaces: protectedProcedure
|
||||
.input(z.object({ dispositivoId: z.string() }))
|
||||
.query(async ({ ctx, input }) => {
|
||||
const dispositivo = await ctx.prisma.dispositivo.findUnique({
|
||||
where: { id: input.dispositivoId },
|
||||
})
|
||||
|
||||
if (!dispositivo || !dispositivo.librenmsId) {
|
||||
throw new TRPCError({
|
||||
code: 'BAD_REQUEST',
|
||||
message: 'Dispositivo no tiene LibreNMS',
|
||||
})
|
||||
}
|
||||
|
||||
if (ctx.user.clienteId && ctx.user.clienteId !== dispositivo.clienteId) {
|
||||
throw new TRPCError({ code: 'FORBIDDEN', message: 'Acceso denegado' })
|
||||
}
|
||||
|
||||
const librenms = new LibreNMSClient()
|
||||
return librenms.getDevicePorts(dispositivo.librenmsId)
|
||||
}),
|
||||
|
||||
// Obtener grafico de trafico de una interfaz
|
||||
trafico: protectedProcedure
|
||||
.input(
|
||||
z.object({
|
||||
dispositivoId: z.string(),
|
||||
portId: z.number(),
|
||||
periodo: z.enum(['1h', '6h', '24h', '7d', '30d']).default('24h'),
|
||||
})
|
||||
)
|
||||
.query(async ({ ctx, input }) => {
|
||||
const dispositivo = await ctx.prisma.dispositivo.findUnique({
|
||||
where: { id: input.dispositivoId },
|
||||
})
|
||||
|
||||
if (!dispositivo || !dispositivo.librenmsId) {
|
||||
throw new TRPCError({
|
||||
code: 'BAD_REQUEST',
|
||||
message: 'Dispositivo no tiene LibreNMS',
|
||||
})
|
||||
}
|
||||
|
||||
if (ctx.user.clienteId && ctx.user.clienteId !== dispositivo.clienteId) {
|
||||
throw new TRPCError({ code: 'FORBIDDEN', message: 'Acceso denegado' })
|
||||
}
|
||||
|
||||
const librenms = new LibreNMSClient()
|
||||
|
||||
// Calcular rango de tiempo
|
||||
const now = new Date()
|
||||
let desde: Date
|
||||
|
||||
switch (input.periodo) {
|
||||
case '1h':
|
||||
desde = new Date(now.getTime() - 60 * 60 * 1000)
|
||||
break
|
||||
case '6h':
|
||||
desde = new Date(now.getTime() - 6 * 60 * 60 * 1000)
|
||||
break
|
||||
case '24h':
|
||||
desde = new Date(now.getTime() - 24 * 60 * 60 * 1000)
|
||||
break
|
||||
case '7d':
|
||||
desde = new Date(now.getTime() - 7 * 24 * 60 * 60 * 1000)
|
||||
break
|
||||
case '30d':
|
||||
desde = new Date(now.getTime() - 30 * 24 * 60 * 60 * 1000)
|
||||
break
|
||||
}
|
||||
|
||||
return librenms.getPortStats(input.portId, desde, now)
|
||||
}),
|
||||
|
||||
// Obtener topologia de red
|
||||
topologia: protectedProcedure
|
||||
.input(z.object({ clienteId: z.string().optional() }))
|
||||
.query(async ({ ctx, input }) => {
|
||||
const clienteId = ctx.user.clienteId || input.clienteId
|
||||
|
||||
// Obtener dispositivos de red del cliente
|
||||
const dispositivos = await ctx.prisma.dispositivo.findMany({
|
||||
where: {
|
||||
...(clienteId ? { clienteId } : {}),
|
||||
tipo: { in: ['ROUTER', 'SWITCH', 'FIREWALL', 'AP'] },
|
||||
librenmsId: { not: null },
|
||||
},
|
||||
select: {
|
||||
id: true,
|
||||
nombre: true,
|
||||
ip: true,
|
||||
tipo: true,
|
||||
estado: true,
|
||||
librenmsId: true,
|
||||
},
|
||||
})
|
||||
|
||||
if (dispositivos.length === 0) {
|
||||
return { nodes: [], links: [] }
|
||||
}
|
||||
|
||||
// Obtener enlaces de LibreNMS
|
||||
const librenms = new LibreNMSClient()
|
||||
const links = await librenms.getLinks()
|
||||
|
||||
// Mapear a nodos y enlaces para visualizacion
|
||||
const librenmsIdToDevice = new Map(
|
||||
dispositivos
|
||||
.filter(d => d.librenmsId !== null)
|
||||
.map(d => [d.librenmsId!, d])
|
||||
)
|
||||
|
||||
const nodes = dispositivos.map(d => ({
|
||||
id: d.id,
|
||||
name: d.nombre,
|
||||
ip: d.ip,
|
||||
type: d.tipo,
|
||||
status: d.estado,
|
||||
}))
|
||||
|
||||
const edges = links
|
||||
.filter(
|
||||
(l: { local_device_id: number; remote_device_id: number }) =>
|
||||
librenmsIdToDevice.has(l.local_device_id) && librenmsIdToDevice.has(l.remote_device_id)
|
||||
)
|
||||
.map((l: { local_device_id: number; remote_device_id: number; local_port: string; remote_port: string }) => ({
|
||||
source: librenmsIdToDevice.get(l.local_device_id)!.id,
|
||||
target: librenmsIdToDevice.get(l.remote_device_id)!.id,
|
||||
localPort: l.local_port,
|
||||
remotePort: l.remote_port,
|
||||
}))
|
||||
|
||||
return { nodes, links: edges }
|
||||
}),
|
||||
|
||||
// Obtener alertas SNMP activas
|
||||
alertasSNMP: protectedProcedure
|
||||
.input(z.object({ dispositivoId: z.string().optional() }))
|
||||
.query(async ({ ctx, input }) => {
|
||||
const librenms = new LibreNMSClient()
|
||||
|
||||
if (input.dispositivoId) {
|
||||
const dispositivo = await ctx.prisma.dispositivo.findUnique({
|
||||
where: { id: input.dispositivoId },
|
||||
})
|
||||
|
||||
if (!dispositivo || !dispositivo.librenmsId) {
|
||||
return []
|
||||
}
|
||||
|
||||
if (ctx.user.clienteId && ctx.user.clienteId !== dispositivo.clienteId) {
|
||||
throw new TRPCError({ code: 'FORBIDDEN', message: 'Acceso denegado' })
|
||||
}
|
||||
|
||||
return librenms.getDeviceAlerts(dispositivo.librenmsId)
|
||||
}
|
||||
|
||||
return librenms.getAlerts()
|
||||
}),
|
||||
|
||||
// Obtener datos de NetFlow
|
||||
netflow: protectedProcedure
|
||||
.input(
|
||||
z.object({
|
||||
dispositivoId: z.string(),
|
||||
periodo: z.enum(['1h', '6h', '24h']).default('1h'),
|
||||
})
|
||||
)
|
||||
.query(async ({ ctx, input }) => {
|
||||
const dispositivo = await ctx.prisma.dispositivo.findUnique({
|
||||
where: { id: input.dispositivoId },
|
||||
})
|
||||
|
||||
if (!dispositivo || !dispositivo.librenmsId) {
|
||||
throw new TRPCError({
|
||||
code: 'BAD_REQUEST',
|
||||
message: 'Dispositivo no tiene LibreNMS',
|
||||
})
|
||||
}
|
||||
|
||||
if (ctx.user.clienteId && ctx.user.clienteId !== dispositivo.clienteId) {
|
||||
throw new TRPCError({ code: 'FORBIDDEN', message: 'Acceso denegado' })
|
||||
}
|
||||
|
||||
// LibreNMS puede tener integracion con nfsen para netflow
|
||||
// Por ahora retornamos datos de ejemplo
|
||||
return {
|
||||
topTalkers: [],
|
||||
topProtocols: [],
|
||||
topPorts: [],
|
||||
}
|
||||
}),
|
||||
|
||||
// Estadisticas de red por cliente
|
||||
stats: protectedProcedure
|
||||
.input(z.object({ clienteId: z.string().optional() }))
|
||||
.query(async ({ ctx, input }) => {
|
||||
const clienteId = ctx.user.clienteId || input.clienteId
|
||||
|
||||
const where = {
|
||||
tipo: { in: ['ROUTER', 'SWITCH', 'FIREWALL', 'AP', 'IMPRESORA', 'OTRO'] as const },
|
||||
...(clienteId ? { clienteId } : {}),
|
||||
}
|
||||
|
||||
const [total, online, offline, alertas] = await Promise.all([
|
||||
ctx.prisma.dispositivo.count({ where }),
|
||||
ctx.prisma.dispositivo.count({ where: { ...where, estado: 'ONLINE' } }),
|
||||
ctx.prisma.dispositivo.count({ where: { ...where, estado: 'OFFLINE' } }),
|
||||
ctx.prisma.dispositivo.count({ where: { ...where, estado: 'ALERTA' } }),
|
||||
])
|
||||
|
||||
return { total, online, offline, alertas }
|
||||
}),
|
||||
})
|
||||
Reference in New Issue
Block a user