Files
negareh-api/src/modules/notification/guards/ws-admin-auth.guard.ts
T
2026-01-07 12:24:55 +03:30

126 lines
4.0 KiB
TypeScript

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<boolean> {
const client: Socket = context.switchToWs().getClient<Socket>();
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<string>('JWT_SECRET');
const payload = await this.jwtService.verifyAsync<IAdminTokenPayload>(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 <token>", 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;
// }
}