feat: C-8/C-1/C-2/C-3/C-4/C-6/M-1/M-5/M-6 — WebSocket gateway, SENAE real, SP-API, Twilio SMS, WhatsApp Business, soporte portal, HMAC audit, reportes CSV
This commit is contained in:
@@ -0,0 +1,101 @@
|
||||
import {
|
||||
WebSocketGateway,
|
||||
WebSocketServer,
|
||||
OnGatewayConnection,
|
||||
OnGatewayDisconnect,
|
||||
SubscribeMessage,
|
||||
MessageBody,
|
||||
ConnectedSocket,
|
||||
} from "@nestjs/websockets";
|
||||
import { Server, Socket } from "socket.io";
|
||||
import { Logger } from "@nestjs/common";
|
||||
import { JwtService } from "@nestjs/jwt";
|
||||
|
||||
/**
|
||||
* NotificationsGateway — C-6
|
||||
* Real-time push of package status changes to connected portal clients.
|
||||
*
|
||||
* Connection flow:
|
||||
* 1. Client connects with `auth: { token: "<jwt>" }` in socket options.
|
||||
* 2. Gateway verifies JWT → places socket in room `user:<userId>`.
|
||||
* 3. On package status change, NotificationsService calls `emitStatusChange()`.
|
||||
* 4. All sockets in that user room receive `package:status` event.
|
||||
*
|
||||
* Client (Next.js portal):
|
||||
* const socket = io("http://localhost:3001", { auth: { token: localStorage.getItem("mw_access") } });
|
||||
* socket.on("package:status", (data) => { ... });
|
||||
*/
|
||||
@WebSocketGateway({
|
||||
cors: {
|
||||
origin: (process.env.CORS_ORIGINS ?? "http://localhost:3000").split(",").map(o => o.trim()),
|
||||
credentials: true,
|
||||
},
|
||||
namespace: "/ws",
|
||||
transports: ["websocket", "polling"],
|
||||
})
|
||||
export class NotificationsGateway implements OnGatewayConnection, OnGatewayDisconnect {
|
||||
@WebSocketServer() server!: Server;
|
||||
private readonly logger = new Logger(NotificationsGateway.name);
|
||||
|
||||
constructor(private jwtService: JwtService) {}
|
||||
|
||||
async handleConnection(client: Socket) {
|
||||
try {
|
||||
const token =
|
||||
(client.handshake.auth as any)?.token ??
|
||||
client.handshake.headers?.authorization?.replace("Bearer ", "");
|
||||
|
||||
if (!token) {
|
||||
this.logger.warn(`[WS] Client ${client.id} rejected — no token`);
|
||||
client.disconnect(true);
|
||||
return;
|
||||
}
|
||||
|
||||
const secret = process.env.JWT_SECRET ?? "changeme";
|
||||
const payload = this.jwtService.verify(token, { secret });
|
||||
const userId: string = payload.sub;
|
||||
|
||||
// Join personal room so we can target by userId
|
||||
await client.join(`user:${userId}`);
|
||||
client.data.userId = userId;
|
||||
client.data.tenantId = payload.tenantId;
|
||||
|
||||
this.logger.log(`[WS] Connected: ${client.id} → user:${userId}`);
|
||||
} catch {
|
||||
this.logger.warn(`[WS] Client ${client.id} rejected — invalid token`);
|
||||
client.disconnect(true);
|
||||
}
|
||||
}
|
||||
|
||||
handleDisconnect(client: Socket) {
|
||||
this.logger.log(`[WS] Disconnected: ${client.id}`);
|
||||
}
|
||||
|
||||
/** Emitted by NotificationsService on every package status change */
|
||||
emitStatusChange(userId: string, pkg: {
|
||||
id: string;
|
||||
trackingId: string;
|
||||
status: string;
|
||||
description?: string;
|
||||
}) {
|
||||
this.server.to(`user:${userId}`).emit("package:status", {
|
||||
packageId: pkg.id,
|
||||
trackingId: pkg.trackingId,
|
||||
status: pkg.status,
|
||||
description: pkg.description ?? null,
|
||||
at: new Date().toISOString(),
|
||||
});
|
||||
this.logger.log(`[WS] Emitted package:status to user:${userId} — ${pkg.trackingId} → ${pkg.status}`);
|
||||
}
|
||||
|
||||
/** Broadcast to all sockets in a tenant room */
|
||||
emitToTenant(tenantId: string, event: string, data: any) {
|
||||
this.server.to(`tenant:${tenantId}`).emit(event, data);
|
||||
}
|
||||
|
||||
/** Ping/pong — optional keep-alive */
|
||||
@SubscribeMessage("ping")
|
||||
handlePing(@ConnectedSocket() client: Socket, @MessageBody() _data: any) {
|
||||
client.emit("pong", { at: new Date().toISOString() });
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user