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:
382
src/server/trpc/routers/celulares.router.ts
Normal file
382
src/server/trpc/routers/celulares.router.ts
Normal file
@@ -0,0 +1,382 @@
|
||||
import { z } from 'zod'
|
||||
import { TRPCError } from '@trpc/server'
|
||||
import { router, protectedProcedure, adminProcedure } from '../trpc'
|
||||
import { HeadwindClient } from '@/server/services/headwind/client'
|
||||
|
||||
export const celularesRouter = router({
|
||||
// Listar celulares y tablets
|
||||
list: protectedProcedure
|
||||
.input(
|
||||
z.object({
|
||||
clienteId: z.string().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, estado, search, page = 1, limit = 20 } = input || {}
|
||||
|
||||
const where = {
|
||||
tipo: { in: ['CELULAR', 'TABLET'] as const },
|
||||
...(ctx.user.clienteId ? { clienteId: ctx.user.clienteId } : {}),
|
||||
...(clienteId ? { clienteId } : {}),
|
||||
...(estado ? { estado } : {}),
|
||||
...(search ? {
|
||||
OR: [
|
||||
{ nombre: { contains: search, mode: 'insensitive' as const } },
|
||||
{ imei: { contains: search } },
|
||||
{ numeroTelefono: { contains: search } },
|
||||
],
|
||||
} : {}),
|
||||
}
|
||||
|
||||
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 celular 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 ubicacion actual
|
||||
ubicacion: protectedProcedure
|
||||
.input(z.object({ dispositivoId: z.string() }))
|
||||
.query(async ({ ctx, input }) => {
|
||||
const dispositivo = await ctx.prisma.dispositivo.findUnique({
|
||||
where: { id: input.dispositivoId },
|
||||
select: {
|
||||
id: true,
|
||||
nombre: true,
|
||||
latitud: true,
|
||||
longitud: true,
|
||||
gpsUpdatedAt: true,
|
||||
clienteId: true,
|
||||
},
|
||||
})
|
||||
|
||||
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 {
|
||||
lat: dispositivo.latitud,
|
||||
lng: dispositivo.longitud,
|
||||
updatedAt: dispositivo.gpsUpdatedAt,
|
||||
}
|
||||
}),
|
||||
|
||||
// Solicitar actualizacion de ubicacion
|
||||
solicitarUbicacion: protectedProcedure
|
||||
.input(z.object({ dispositivoId: z.string() }))
|
||||
.mutation(async ({ ctx, input }) => {
|
||||
const dispositivo = await ctx.prisma.dispositivo.findUnique({
|
||||
where: { id: input.dispositivoId },
|
||||
})
|
||||
|
||||
if (!dispositivo || !dispositivo.headwindId) {
|
||||
throw new TRPCError({
|
||||
code: 'BAD_REQUEST',
|
||||
message: 'Dispositivo no tiene Headwind MDM',
|
||||
})
|
||||
}
|
||||
|
||||
if (ctx.user.clienteId && ctx.user.clienteId !== dispositivo.clienteId) {
|
||||
throw new TRPCError({ code: 'FORBIDDEN', message: 'Acceso denegado' })
|
||||
}
|
||||
|
||||
const headwindClient = new HeadwindClient()
|
||||
await headwindClient.requestLocation(dispositivo.headwindId)
|
||||
|
||||
return { success: true, message: 'Solicitud de ubicacion enviada' }
|
||||
}),
|
||||
|
||||
// Bloquear dispositivo
|
||||
bloquear: protectedProcedure
|
||||
.input(
|
||||
z.object({
|
||||
dispositivoId: z.string(),
|
||||
mensaje: z.string().optional(),
|
||||
})
|
||||
)
|
||||
.mutation(async ({ ctx, input }) => {
|
||||
const dispositivo = await ctx.prisma.dispositivo.findUnique({
|
||||
where: { id: input.dispositivoId },
|
||||
})
|
||||
|
||||
if (!dispositivo || !dispositivo.headwindId) {
|
||||
throw new TRPCError({
|
||||
code: 'BAD_REQUEST',
|
||||
message: 'Dispositivo no tiene Headwind MDM',
|
||||
})
|
||||
}
|
||||
|
||||
if (ctx.user.clienteId && ctx.user.clienteId !== dispositivo.clienteId) {
|
||||
throw new TRPCError({ code: 'FORBIDDEN', message: 'Acceso denegado' })
|
||||
}
|
||||
|
||||
const headwindClient = new HeadwindClient()
|
||||
await headwindClient.lockDevice(dispositivo.headwindId, input.mensaje)
|
||||
|
||||
await ctx.prisma.auditLog.create({
|
||||
data: {
|
||||
usuarioId: ctx.user.id,
|
||||
dispositivoId: input.dispositivoId,
|
||||
accion: 'bloquear',
|
||||
recurso: 'celular',
|
||||
detalles: { mensaje: input.mensaje },
|
||||
},
|
||||
})
|
||||
|
||||
return { success: true }
|
||||
}),
|
||||
|
||||
// Desbloquear dispositivo
|
||||
desbloquear: protectedProcedure
|
||||
.input(z.object({ dispositivoId: z.string() }))
|
||||
.mutation(async ({ ctx, input }) => {
|
||||
const dispositivo = await ctx.prisma.dispositivo.findUnique({
|
||||
where: { id: input.dispositivoId },
|
||||
})
|
||||
|
||||
if (!dispositivo || !dispositivo.headwindId) {
|
||||
throw new TRPCError({
|
||||
code: 'BAD_REQUEST',
|
||||
message: 'Dispositivo no tiene Headwind MDM',
|
||||
})
|
||||
}
|
||||
|
||||
if (ctx.user.clienteId && ctx.user.clienteId !== dispositivo.clienteId) {
|
||||
throw new TRPCError({ code: 'FORBIDDEN', message: 'Acceso denegado' })
|
||||
}
|
||||
|
||||
const headwindClient = new HeadwindClient()
|
||||
await headwindClient.unlockDevice(dispositivo.headwindId)
|
||||
|
||||
await ctx.prisma.auditLog.create({
|
||||
data: {
|
||||
usuarioId: ctx.user.id,
|
||||
dispositivoId: input.dispositivoId,
|
||||
accion: 'desbloquear',
|
||||
recurso: 'celular',
|
||||
},
|
||||
})
|
||||
|
||||
return { success: true }
|
||||
}),
|
||||
|
||||
// Hacer sonar dispositivo
|
||||
sonar: protectedProcedure
|
||||
.input(z.object({ dispositivoId: z.string() }))
|
||||
.mutation(async ({ ctx, input }) => {
|
||||
const dispositivo = await ctx.prisma.dispositivo.findUnique({
|
||||
where: { id: input.dispositivoId },
|
||||
})
|
||||
|
||||
if (!dispositivo || !dispositivo.headwindId) {
|
||||
throw new TRPCError({
|
||||
code: 'BAD_REQUEST',
|
||||
message: 'Dispositivo no tiene Headwind MDM',
|
||||
})
|
||||
}
|
||||
|
||||
if (ctx.user.clienteId && ctx.user.clienteId !== dispositivo.clienteId) {
|
||||
throw new TRPCError({ code: 'FORBIDDEN', message: 'Acceso denegado' })
|
||||
}
|
||||
|
||||
const headwindClient = new HeadwindClient()
|
||||
await headwindClient.ringDevice(dispositivo.headwindId)
|
||||
|
||||
return { success: true }
|
||||
}),
|
||||
|
||||
// Enviar mensaje al dispositivo
|
||||
enviarMensaje: protectedProcedure
|
||||
.input(
|
||||
z.object({
|
||||
dispositivoId: z.string(),
|
||||
mensaje: z.string().min(1),
|
||||
})
|
||||
)
|
||||
.mutation(async ({ ctx, input }) => {
|
||||
const dispositivo = await ctx.prisma.dispositivo.findUnique({
|
||||
where: { id: input.dispositivoId },
|
||||
})
|
||||
|
||||
if (!dispositivo || !dispositivo.headwindId) {
|
||||
throw new TRPCError({
|
||||
code: 'BAD_REQUEST',
|
||||
message: 'Dispositivo no tiene Headwind MDM',
|
||||
})
|
||||
}
|
||||
|
||||
if (ctx.user.clienteId && ctx.user.clienteId !== dispositivo.clienteId) {
|
||||
throw new TRPCError({ code: 'FORBIDDEN', message: 'Acceso denegado' })
|
||||
}
|
||||
|
||||
const headwindClient = new HeadwindClient()
|
||||
await headwindClient.sendMessage(dispositivo.headwindId, input.mensaje)
|
||||
|
||||
return { success: true }
|
||||
}),
|
||||
|
||||
// Borrar datos (factory reset)
|
||||
borrarDatos: adminProcedure
|
||||
.input(z.object({ dispositivoId: z.string() }))
|
||||
.mutation(async ({ ctx, input }) => {
|
||||
const dispositivo = await ctx.prisma.dispositivo.findUnique({
|
||||
where: { id: input.dispositivoId },
|
||||
})
|
||||
|
||||
if (!dispositivo || !dispositivo.headwindId) {
|
||||
throw new TRPCError({
|
||||
code: 'BAD_REQUEST',
|
||||
message: 'Dispositivo no tiene Headwind MDM',
|
||||
})
|
||||
}
|
||||
|
||||
const headwindClient = new HeadwindClient()
|
||||
await headwindClient.wipeDevice(dispositivo.headwindId)
|
||||
|
||||
await ctx.prisma.auditLog.create({
|
||||
data: {
|
||||
usuarioId: ctx.user.id,
|
||||
dispositivoId: input.dispositivoId,
|
||||
accion: 'borrar_datos',
|
||||
recurso: 'celular',
|
||||
},
|
||||
})
|
||||
|
||||
return { success: true }
|
||||
}),
|
||||
|
||||
// Instalar aplicacion
|
||||
instalarApp: protectedProcedure
|
||||
.input(
|
||||
z.object({
|
||||
dispositivoId: z.string(),
|
||||
packageName: z.string(),
|
||||
})
|
||||
)
|
||||
.mutation(async ({ ctx, input }) => {
|
||||
const dispositivo = await ctx.prisma.dispositivo.findUnique({
|
||||
where: { id: input.dispositivoId },
|
||||
})
|
||||
|
||||
if (!dispositivo || !dispositivo.headwindId) {
|
||||
throw new TRPCError({
|
||||
code: 'BAD_REQUEST',
|
||||
message: 'Dispositivo no tiene Headwind MDM',
|
||||
})
|
||||
}
|
||||
|
||||
if (ctx.user.clienteId && ctx.user.clienteId !== dispositivo.clienteId) {
|
||||
throw new TRPCError({ code: 'FORBIDDEN', message: 'Acceso denegado' })
|
||||
}
|
||||
|
||||
const headwindClient = new HeadwindClient()
|
||||
await headwindClient.installApp(dispositivo.headwindId, input.packageName)
|
||||
|
||||
await ctx.prisma.auditLog.create({
|
||||
data: {
|
||||
usuarioId: ctx.user.id,
|
||||
dispositivoId: input.dispositivoId,
|
||||
accion: 'instalar_app',
|
||||
recurso: 'celular',
|
||||
detalles: { packageName: input.packageName },
|
||||
},
|
||||
})
|
||||
|
||||
return { success: true }
|
||||
}),
|
||||
|
||||
// Desinstalar aplicacion
|
||||
desinstalarApp: protectedProcedure
|
||||
.input(
|
||||
z.object({
|
||||
dispositivoId: z.string(),
|
||||
packageName: z.string(),
|
||||
})
|
||||
)
|
||||
.mutation(async ({ ctx, input }) => {
|
||||
const dispositivo = await ctx.prisma.dispositivo.findUnique({
|
||||
where: { id: input.dispositivoId },
|
||||
})
|
||||
|
||||
if (!dispositivo || !dispositivo.headwindId) {
|
||||
throw new TRPCError({
|
||||
code: 'BAD_REQUEST',
|
||||
message: 'Dispositivo no tiene Headwind MDM',
|
||||
})
|
||||
}
|
||||
|
||||
if (ctx.user.clienteId && ctx.user.clienteId !== dispositivo.clienteId) {
|
||||
throw new TRPCError({ code: 'FORBIDDEN', message: 'Acceso denegado' })
|
||||
}
|
||||
|
||||
const headwindClient = new HeadwindClient()
|
||||
await headwindClient.removeApp(dispositivo.headwindId, input.packageName)
|
||||
|
||||
await ctx.prisma.auditLog.create({
|
||||
data: {
|
||||
usuarioId: ctx.user.id,
|
||||
dispositivoId: input.dispositivoId,
|
||||
accion: 'desinstalar_app',
|
||||
recurso: 'celular',
|
||||
detalles: { packageName: input.packageName },
|
||||
},
|
||||
})
|
||||
|
||||
return { success: true }
|
||||
}),
|
||||
})
|
||||
Reference in New Issue
Block a user