mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-08-29 19:58:11 +00:00
fix: added chat to the freight api
This commit is contained in:
@@ -0,0 +1,122 @@
|
||||
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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user