|
|
@@ -0,0 +1,123 @@
|
|
|
+import {
|
|
|
+ WebSocketGateway,
|
|
|
+ WebSocketServer,
|
|
|
+ SubscribeMessage,
|
|
|
+ OnGatewayConnection,
|
|
|
+ OnGatewayDisconnect,
|
|
|
+ ConnectedSocket,
|
|
|
+} from '@nestjs/websockets'
|
|
|
+import { Server, Socket } from 'socket.io'
|
|
|
+import { JwtService } from '@nestjs/jwt'
|
|
|
+import { DeviceStatus } from '@shared/types'
|
|
|
+
|
|
|
+interface AuthenticatedSocket extends Socket {
|
|
|
+ userId?: string
|
|
|
+ orgId?: string
|
|
|
+}
|
|
|
+
|
|
|
+@WebSocketGateway({
|
|
|
+ cors: {
|
|
|
+ origin: process.env['CORS_ORIGIN'] || 'http://localhost:3000',
|
|
|
+ credentials: true,
|
|
|
+ },
|
|
|
+ namespace: '/realtime',
|
|
|
+})
|
|
|
+export class RealtimeGateway implements OnGatewayConnection, OnGatewayDisconnect {
|
|
|
+ @WebSocketServer()
|
|
|
+ server!: Server
|
|
|
+
|
|
|
+ // Track connected clients: socketId → { userId, orgId, subscribedDevices }
|
|
|
+ private clients = new Map<string, { userId: string; orgId?: string }>()
|
|
|
+
|
|
|
+ constructor(private readonly jwt: JwtService) {}
|
|
|
+
|
|
|
+ async handleConnection(client: AuthenticatedSocket) {
|
|
|
+ try {
|
|
|
+ const token = client.handshake.auth?.token ?? client.handshake.headers?.authorization?.replace('Bearer ', '')
|
|
|
+ if (!token) {
|
|
|
+ client.disconnect()
|
|
|
+ return
|
|
|
+ }
|
|
|
+
|
|
|
+ const payload = this.jwt.verify(token as string, {
|
|
|
+ secret: process.env['JWT_SECRET'],
|
|
|
+ })
|
|
|
+ client.userId = payload.sub
|
|
|
+
|
|
|
+ this.clients.set(client.id, { userId: payload.sub })
|
|
|
+ console.log(`[WS] Client connected: ${client.id} (user: ${payload.sub})`)
|
|
|
+ } catch {
|
|
|
+ client.disconnect()
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ handleDisconnect(client: AuthenticatedSocket) {
|
|
|
+ this.clients.delete(client.id)
|
|
|
+ console.log(`[WS] Client disconnected: ${client.id}`)
|
|
|
+ }
|
|
|
+
|
|
|
+ @SubscribeMessage('subscribe:project')
|
|
|
+ handleSubscribeProject(client: AuthenticatedSocket, projectId: string) {
|
|
|
+ client.join(`project:${projectId}`)
|
|
|
+ console.log(`[WS] Client ${client.id} joined project:${projectId}`)
|
|
|
+ }
|
|
|
+
|
|
|
+ @SubscribeMessage('unsubscribe:project')
|
|
|
+ handleUnsubscribeProject(client: AuthenticatedSocket, projectId: string) {
|
|
|
+ client.leave(`project:${projectId}`)
|
|
|
+ }
|
|
|
+
|
|
|
+ // ── Emit helpers (called from services) ────────────────────────
|
|
|
+
|
|
|
+ emitDeviceHeartbeat(deviceId: string, projectId: string, orgId: string, status: DeviceStatus, storageFreeGb: number) {
|
|
|
+ this.server.to(`project:${projectId}`).emit('device.heartbeat', {
|
|
|
+ deviceId,
|
|
|
+ status,
|
|
|
+ storageFreeGb,
|
|
|
+ ts: new Date().toISOString(),
|
|
|
+ })
|
|
|
+ // Also emit to org room
|
|
|
+ this.server.to(`org:${orgId}`).emit('device.heartbeat', {
|
|
|
+ deviceId,
|
|
|
+ status,
|
|
|
+ storageFreeGb,
|
|
|
+ ts: new Date().toISOString(),
|
|
|
+ })
|
|
|
+ }
|
|
|
+
|
|
|
+ emitDeviceStatusChanged(deviceId: string, projectId: string, status: DeviceStatus, previousStatus: DeviceStatus) {
|
|
|
+ this.server.to(`project:${projectId}`).emit('device.status.changed', {
|
|
|
+ deviceId,
|
|
|
+ status,
|
|
|
+ previousStatus,
|
|
|
+ ts: new Date().toISOString(),
|
|
|
+ })
|
|
|
+ }
|
|
|
+
|
|
|
+ emitCaptureUploaded(deviceId: string, projectId: string, captureId: string) {
|
|
|
+ this.server.to(`project:${projectId}`).emit('capture.uploaded', {
|
|
|
+ captureId,
|
|
|
+ deviceId,
|
|
|
+ ts: new Date().toISOString(),
|
|
|
+ })
|
|
|
+ }
|
|
|
+
|
|
|
+ emitAlert(deviceId: string | null, projectId: string | null, alertId: string, type: string, severity: string, message: string) {
|
|
|
+ if (projectId) {
|
|
|
+ this.server.to(`project:${projectId}`).emit('alert.opened', {
|
|
|
+ alertId,
|
|
|
+ deviceId,
|
|
|
+ type,
|
|
|
+ severity,
|
|
|
+ message,
|
|
|
+ ts: new Date().toISOString(),
|
|
|
+ })
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ joinOrg(client: AuthenticatedSocket, orgId: string) {
|
|
|
+ client.join(`org:${orgId}`)
|
|
|
+ const current = this.clients.get(client.id) ?? { userId: '' }
|
|
|
+ this.clients.set(client.id, { ...current, orgId })
|
|
|
+ }
|
|
|
+}
|