mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-08-29 19:58:11 +00:00
feat: add the support feature to the passenger api
This commit is contained in:
134
apps/edr-passenger-api/src/modules/support/support.gateway.ts
Normal file
134
apps/edr-passenger-api/src/modules/support/support.gateway.ts
Normal file
@@ -0,0 +1,134 @@
|
||||
import { Logger } from '@nestjs/common';
|
||||
import {
|
||||
OnGatewayConnection,
|
||||
WebSocketGateway,
|
||||
WebSocketServer,
|
||||
} from '@nestjs/websockets';
|
||||
import { Server, Socket } from 'socket.io';
|
||||
import { Passenger as PassengerTypes } from '@edr/types';
|
||||
|
||||
import { PrismaService } from '../../common/prisma.service';
|
||||
import { WsAuthService } from './ws-auth.service';
|
||||
|
||||
/**
|
||||
* Server → client push for passenger support chat. Clients only *listen* (no
|
||||
* `@SubscribeMessage`); the handshake is authenticated in `handleConnection`.
|
||||
* Each socket joins a room based on its side:
|
||||
* - backoffice staff → the shared `backoffice` room (see every conversation).
|
||||
* - passengers → their `user:<iamUserId>` room (their own tickets only).
|
||||
*
|
||||
* Side is decided by the presence of a `Passenger` row for the IAM user id
|
||||
* (staff have none). A message is emitted to BOTH the owner's room and the
|
||||
* backoffice room so the customer thread, the sender's echo, and every agent's
|
||||
* inbox update live.
|
||||
*/
|
||||
@WebSocketGateway({
|
||||
namespace: PassengerTypes.PASSENGER_SUPPORT_WS_NAMESPACE,
|
||||
cors: { origin: true, credentials: true },
|
||||
})
|
||||
export class SupportGateway implements OnGatewayConnection {
|
||||
private readonly logger = new Logger(SupportGateway.name);
|
||||
|
||||
private static readonly BACKOFFICE_ROOM = 'backoffice';
|
||||
|
||||
@WebSocketServer()
|
||||
private readonly server!: Server;
|
||||
|
||||
constructor(
|
||||
private readonly wsAuth: WsAuthService,
|
||||
private readonly prisma: PrismaService,
|
||||
) {}
|
||||
|
||||
async handleConnection(socket: Socket): Promise<void> {
|
||||
const userId = await this.wsAuth.resolveUserId(this.extractToken(socket));
|
||||
|
||||
// Authenticated: passenger (own room) or backoffice staff (shared room).
|
||||
if (userId) {
|
||||
socket.data.userId = userId;
|
||||
const passenger = await this.prisma.passenger.findUnique({
|
||||
where: { iamUserId: userId },
|
||||
});
|
||||
if (passenger) {
|
||||
await socket.join(`user:${userId}`);
|
||||
socket.data.side = 'USER';
|
||||
} else {
|
||||
await socket.join(SupportGateway.BACKOFFICE_ROOM);
|
||||
socket.data.side = 'AGENT';
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Guest: no valid token, but a client-generated guestId scopes the room.
|
||||
// Anyone holding the guestId can see that thread (no account = weaker
|
||||
// ownership) — an accepted MVP trade-off for guest support.
|
||||
const guestId = this.extractGuestId(socket);
|
||||
if (guestId) {
|
||||
socket.data.guestId = guestId;
|
||||
socket.data.side = 'USER';
|
||||
await socket.join(`guest:${guestId}`);
|
||||
return;
|
||||
}
|
||||
|
||||
this.logger.debug(`Rejected passenger-support handshake ${socket.id}`);
|
||||
socket.disconnect(true);
|
||||
}
|
||||
|
||||
/** Push a new message + updated conversation to the owner + backoffice rooms. */
|
||||
emitMessage(
|
||||
ownerRoom: string | null,
|
||||
conversation: PassengerTypes.PassengerSupportConversationDto,
|
||||
message: PassengerTypes.PassengerSupportMessageDto,
|
||||
): void {
|
||||
const payload = { conversation, message };
|
||||
for (const room of this.targetRooms(ownerRoom)) {
|
||||
const to = this.server.to(room);
|
||||
to.emit(PassengerTypes.PASSENGER_SUPPORT_WS_EVENTS.MESSAGE_NEW, payload);
|
||||
to.emit(
|
||||
PassengerTypes.PASSENGER_SUPPORT_WS_EVENTS.CONVERSATION_UPDATED,
|
||||
conversation,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/** Push a conversation metadata change (e.g. status) to both rooms. */
|
||||
emitConversationUpdated(
|
||||
ownerRoom: string | null,
|
||||
conversation: PassengerTypes.PassengerSupportConversationDto,
|
||||
): void {
|
||||
for (const room of this.targetRooms(ownerRoom)) {
|
||||
this.server
|
||||
.to(room)
|
||||
.emit(
|
||||
PassengerTypes.PASSENGER_SUPPORT_WS_EVENTS.CONVERSATION_UPDATED,
|
||||
conversation,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
private targetRooms(ownerRoom: string | null): string[] {
|
||||
const rooms = [SupportGateway.BACKOFFICE_ROOM];
|
||||
if (ownerRoom) rooms.push(ownerRoom);
|
||||
return rooms;
|
||||
}
|
||||
|
||||
private extractGuestId(socket: Socket): string | undefined {
|
||||
const authGuest = socket.handshake.auth?.guestId as string | undefined;
|
||||
if (authGuest) return authGuest;
|
||||
const queryGuest = socket.handshake.query?.guestId;
|
||||
if (typeof queryGuest === 'string') return queryGuest;
|
||||
return undefined;
|
||||
}
|
||||
|
||||
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