import { CanActivate, ExecutionContext, Injectable, Logger, Inject } from '@nestjs/common'; import { WsException } from '@nestjs/websockets'; import { Socket } from 'socket.io'; import { JwtService } from '@nestjs/jwt'; import { ConfigService } from '@nestjs/config'; import { IAdminTokenPayload } from '../../auth/interfaces/IToken-payload'; export interface AuthenticatedSocket extends Socket { adminId?: string; restId?: string; } @Injectable() export class WsAdminAuthGuard implements CanActivate { private readonly logger = new Logger(WsAdminAuthGuard.name); constructor( @Inject(JwtService) private readonly jwtService: JwtService, @Inject(ConfigService) private readonly configService: ConfigService, ) {} async canActivate(context: ExecutionContext): Promise { const client: Socket = context.switchToWs().getClient(); try { const token = this.extractToken(client); if (!token) { this.logger.warn('No token provided in WebSocket connection', { socketId: client.id, auth: client.handshake.auth, query: client.handshake.query, }); throw new WsException('Authentication required'); } const secret = this.configService.getOrThrow('JWT_SECRET'); const payload = await this.jwtService.verifyAsync(token, { secret, }); if (!payload.adminId || !payload.restId) { this.logger.error('Invalid token payload structure', payload); throw new WsException('Invalid token payload'); } // Attach admin info to socket (client as AuthenticatedSocket).adminId = payload.adminId; (client as AuthenticatedSocket).restId = payload.restId; this.logger.log(`Admin authenticated via WebSocket: ${payload.adminId}, restId: ${payload.restId}`); return true; } catch (error) { if (error instanceof WsException) { throw error; } this.logger.error('WebSocket authentication error', { error: error instanceof Error ? error.message : 'Unknown error', socketId: client.id, }); throw new WsException('Authentication failed'); } } private extractToken(client: Socket): string | undefined { // Try to get token from handshake auth (recommended for socket.io) const auth = client.handshake.auth; const authToken = (auth?.token as string | undefined) || (auth?.authorization as string | undefined); if (authToken) { // If it's "Bearer ", extract just the token if (typeof authToken === 'string' && authToken.startsWith('Bearer ')) { return authToken.substring(7); } return authToken; } // Try to get from query parameters (fallback) const queryToken = client.handshake.query?.token || client.handshake.query?.authorization; if (queryToken) { if (typeof queryToken === 'string' && queryToken.startsWith('Bearer ')) { return queryToken.substring(7); } return queryToken as string; } // Try to get from headers (if available) const headers = client.handshake.headers; const authHeader = headers.authorization || headers['authorization']; if (authHeader && typeof authHeader === 'string') { const [type, token] = authHeader.split(' '); if (type?.toLowerCase() === 'bearer' && token) { return token; } } return undefined; } // private extractToken(client: Socket): string | undefined { // const tryGet = (...values: any[]) => values.find(v => typeof v === 'string' && v.length > 0); // let token = tryGet( // client.handshake.auth?.token, // client.handshake.auth?.authorization, // client.handshake.query?.token, // client.handshake.query?.authorization, // client.handshake.headers.authorization, // client.handshake.headers['authorization'], // ); // if (!token) return undefined; // if (token.startsWith('Bearer ')) { // token = token.slice(7); // } // return token; // } }