mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-08-26 18:42:49 +00:00
123 lines
4.0 KiB
TypeScript
123 lines
4.0 KiB
TypeScript
import {
|
|
SUPPORT_CHAT_WS_EVENTS,
|
|
SUPPORT_CHAT_WS_NAMESPACE,
|
|
SupportConversationDto,
|
|
SupportMessageDto,
|
|
} from "@edr/types";
|
|
import { Logger } from "@nestjs/common";
|
|
import {
|
|
OnGatewayConnection,
|
|
WebSocketGateway,
|
|
WebSocketServer,
|
|
} from "@nestjs/websockets";
|
|
import { Server, Socket } from "socket.io";
|
|
|
|
import { BackofficeService } from "../backoffice/backoffice.service";
|
|
import { ExternalProfileRepository } from "../companies/external-profile.repository";
|
|
import { WsAuthService } from "../notification-inbox/ws-auth.service";
|
|
|
|
/**
|
|
* Server → client push for support chat. Clients only *listen* (no
|
|
* `@SubscribeMessage`); the handshake is authenticated in `handleConnection`
|
|
* (reusing the notification module's {@link WsAuthService}). Each socket joins a
|
|
* room based on its side:
|
|
* - backoffice staff → the shared `backoffice` room (see every conversation).
|
|
* - portal users → their `company:<companyId>` room (their tickets only).
|
|
*
|
|
* A message is emitted to *both* the company room and the backoffice room so the
|
|
* customer thread, the sender's echo, and every other agent's inbox update live.
|
|
*/
|
|
@WebSocketGateway({
|
|
namespace: SUPPORT_CHAT_WS_NAMESPACE,
|
|
cors: { origin: true, credentials: true },
|
|
})
|
|
export class SupportChatGateway implements OnGatewayConnection {
|
|
private readonly logger = new Logger(SupportChatGateway.name);
|
|
|
|
private static readonly BACKOFFICE_ROOM = "backoffice";
|
|
|
|
@WebSocketServer()
|
|
private readonly server!: Server;
|
|
|
|
constructor(
|
|
private readonly wsAuth: WsAuthService,
|
|
private readonly backoffice: BackofficeService,
|
|
private readonly externalProfiles: ExternalProfileRepository,
|
|
) {}
|
|
|
|
async handleConnection(socket: Socket): Promise<void> {
|
|
const userId = await this.wsAuth.resolveUserId(this.extractToken(socket));
|
|
if (!userId) {
|
|
this.logger.debug(`Rejected support-chat handshake ${socket.id}`);
|
|
socket.disconnect(true);
|
|
return;
|
|
}
|
|
socket.data.userId = userId;
|
|
|
|
try {
|
|
const staffIds = await this.backoffice.getAllCurrentEmployeeUserIds();
|
|
if (staffIds.includes(userId)) {
|
|
await socket.join(SupportChatGateway.BACKOFFICE_ROOM);
|
|
socket.data.side = "AGENT";
|
|
return;
|
|
}
|
|
} catch (err) {
|
|
this.logger.warn(`Staff lookup failed: ${(err as Error).message}`);
|
|
}
|
|
|
|
const profile = await this.externalProfiles.findByUserId(userId);
|
|
if (profile?.companyId) {
|
|
await socket.join(this.companyRoom(profile.companyId));
|
|
socket.data.side = "CUSTOMER";
|
|
socket.data.companyId = profile.companyId;
|
|
}
|
|
}
|
|
|
|
/** Push a new message + updated conversation to the company and backoffice rooms. */
|
|
emitMessage(
|
|
companyId: string,
|
|
conversation: SupportConversationDto,
|
|
message: SupportMessageDto,
|
|
): void {
|
|
const payload = { conversation, message };
|
|
for (const room of this.targetRooms(companyId)) {
|
|
const to = this.server.to(room);
|
|
to.emit(SUPPORT_CHAT_WS_EVENTS.MESSAGE_NEW, payload);
|
|
to.emit(SUPPORT_CHAT_WS_EVENTS.CONVERSATION_UPDATED, conversation);
|
|
}
|
|
}
|
|
|
|
/** Push a conversation metadata change (e.g. status) to both rooms. */
|
|
emitConversationUpdated(
|
|
companyId: string,
|
|
conversation: SupportConversationDto,
|
|
): void {
|
|
for (const room of this.targetRooms(companyId)) {
|
|
this.server
|
|
.to(room)
|
|
.emit(SUPPORT_CHAT_WS_EVENTS.CONVERSATION_UPDATED, conversation);
|
|
}
|
|
}
|
|
|
|
private targetRooms(companyId: string): string[] {
|
|
return [this.companyRoom(companyId), SupportChatGateway.BACKOFFICE_ROOM];
|
|
}
|
|
|
|
private companyRoom(companyId: string): string {
|
|
return `company:${companyId}`;
|
|
}
|
|
|
|
private extractToken(socket: Socket): string | undefined {
|
|
const authToken = socket.handshake.auth?.token as string | undefined;
|
|
if (authToken) return authToken;
|
|
|
|
const queryToken = socket.handshake.query?.token;
|
|
if (typeof queryToken === "string") return queryToken;
|
|
|
|
const header = socket.handshake.headers?.authorization;
|
|
if (header?.startsWith("Bearer ")) return header.slice(7);
|
|
|
|
return undefined;
|
|
}
|
|
}
|