All files / src/common/guards ws-jwt.guard.ts

65% Statements 26/40
60% Branches 18/30
100% Functions 2/2
63.15% Lines 24/38

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 9613x 13x 13x 13x 13x     13x     13x 14x     14x 14x 14x       3x 3x   3x     3x 1x 1x       2x 2x 2x           2x                                                                                               1x 1x 1x          
import { Injectable, CanActivate, ExecutionContext, Logger, Optional } from '@nestjs/common';
import { JwtService } from '@nestjs/jwt';
import { ConfigService } from '@nestjs/config';
import { WsException } from '@nestjs/websockets';
import { PrismaService } from '@app/modules/prisma/prisma.service';
import { Algorithm } from 'jsonwebtoken';
 
const IS_TEST = process.env.NODE_ENV === 'test' || process.env.TEST_MODE === '1';
 
@Injectable()
export class WsJwtGuard implements CanActivate {
  private readonly logger = new Logger(WsJwtGuard.name);
 
  constructor(
    private jwtService: JwtService,
    private config: ConfigService,
    @Optional() private prisma?: PrismaService,
  ) {}
 
  async canActivate(context: ExecutionContext): Promise<boolean> {
    try {
      const client = context.switchToWs().getClient();
      const token =
        client.handshake.auth.token ||
        client.handshake.headers.authorization?.replace('Bearer ', '');
 
      if (!token) {
        this.logger.warn('WsJwtGuard: No token provided in WebSocket connection');
        throw new WsException('Unauthorized: No token provided');
      }
 
      // In test mode, use simplified verification
      if (IS_TEST || !this.prisma) {
        const payload = await this.jwtService.verifyAsync(token);
        client.data.user = {
          id: payload.sub,
          sub: payload.sub,
          email: payload.email,
          role: payload.role || 'USER',
        };
        return true;
      }
 
      // Production mode: Verify with fixed public key
      const publicKey = this.config.get<string>('JWT_PUBLIC_KEY')?.replace(/\\n/g, '\n');
 
      Iif (!publicKey) {
        this.logger.error('WsJwtGuard: JWT_PUBLIC_KEY not configured');
        throw new WsException('Unauthorized: Configuration error');
      }
 
      // Verify token with public key and enforce issuer/audience
      const algorithm = (this.config.get<string>('JWT_ALGORITHM') || 'RS256') as Algorithm;
      const issuer = this.config.get<string>('JWT_ISSUER');
      const audience = this.config.get<string>('JWT_AUDIENCE');
      const payload = await this.jwtService.verifyAsync(token, {
        publicKey,
        algorithms: [algorithm],
        issuer,
        audience,
      });
 
      // Validate user exists and token version matches
      const user = await this.prisma.client.user.findUnique({
        where: { id: payload.sub },
        select: {
          id: true,
          email: true,
          role: true,
          tokenVersion: true,
        }
      });
 
      Iif (!user || user.tokenVersion !== payload.tokenVersion) {
        this.logger.warn(`WsJwtGuard: Invalid user or token version mismatch for user ${payload.sub}`);
        throw new WsException('Unauthorized: Invalid or expired token');
      }
 
      // Attach complete user context to socket
      client.data.user = {
        id: user.id,
        sub: user.id,
        email: user.email,
        role: user.role,
      };
 
      return true;
    } catch (error) {
      const msg = (error as any)?.message ?? String(error);
      this.logger.error(`WsJwtGuard: Token verification failed: ${msg}`);
      throw new WsException('Unauthorized: Invalid token');
    }
  }
}