bb-inspired
Version:
Core library for BB-inspired NestJS backend
488 lines • 19.3 kB
JavaScript
"use strict";
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
return c > 3 && r && Object.defineProperty(target, key, r), r;
};
var __metadata = (this && this.__metadata) || function (k, v) {
if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
};
var __param = (this && this.__param) || function (paramIndex, decorator) {
return function (target, key) { decorator(target, key, paramIndex); }
};
var WebsocketGateway_1;
Object.defineProperty(exports, "__esModule", { value: true });
exports.WebsocketGateway = exports.ChannelPermission = void 0;
const websockets_1 = require("@nestjs/websockets");
const socket_io_1 = require("socket.io");
const jwt_service_1 = require("../auth/jwt.service");
const logger_1 = require("../../utils/logger");
const common_1 = require("@nestjs/common");
const websocket_rate_limiter_1 = require("./websocket.rate-limiter");
var ChannelPermission;
(function (ChannelPermission) {
ChannelPermission["READ"] = "read";
ChannelPermission["WRITE"] = "write";
ChannelPermission["ADMIN"] = "admin";
})(ChannelPermission || (exports.ChannelPermission = ChannelPermission = {}));
let WebsocketGateway = WebsocketGateway_1 = class WebsocketGateway {
constructor(jwtService, rateLimiter) {
this.jwtService = jwtService;
this.rateLimiter = rateLimiter;
this.logger = new logger_1.AppLogger(WebsocketGateway_1.name);
this.connectedClients = new Map();
this.channels = new Map();
this.channelAcls = new Map();
this.lastPingTimestamps = new Map();
this.serverStartTime = Date.now();
this.registerChannel({
name: 'system',
description: 'System notifications channel',
allowPublicSubscribe: false,
allowPublicPublish: false,
allowAnonymous: false,
requiredRoles: ['admin'],
});
this.registerChannel({
name: 'notifications',
description: 'General notifications channel',
allowPublicSubscribe: true,
allowPublicPublish: false,
allowAnonymous: false,
});
}
afterInit(server) {
this.logger.log('WebSocket Gateway initialized');
}
async handleConnection(client, ...args) {
var _a;
try {
if (this.rateLimiter && !this.rateLimiter.handleConnection(client)) {
client.disconnect();
return;
}
const token = client.handshake.auth.token || ((_a = client.handshake.headers.authorization) === null || _a === void 0 ? void 0 : _a.split(' ')[1]);
if (!token) {
this.logger.warn('Client attempted to connect without a token');
client.disconnect();
return;
}
const decoded = await this.jwtService.verifyWsToken(token);
client.data.user = {
id: decoded.sub,
connectionId: decoded.connectionId || client.id,
roles: decoded.roles || [],
permissions: decoded.permissions || [],
};
this.connectedClients.set(client.id, client);
this.logger.verbose(`Client connected: ${client.id} (User: ${decoded.sub})`);
client.emit('connection_established', {
connectionId: client.id,
message: 'Successfully connected to WebSocket server'
});
this.autoJoinAuthorizedChannels(client);
}
catch (error) {
this.logger.error(`WebSocket authentication failed: ${error.message}`);
client.disconnect();
}
}
handleDisconnect(client) {
if (this.rateLimiter) {
this.rateLimiter.handleDisconnect(client);
}
this.connectedClients.delete(client.id);
this.logger.verbose(`Client disconnected: ${client.id}`);
}
async handleAuthenticate(client, data) {
try {
if (!data.token) {
throw new Error('Authentication token is required');
}
const payload = await this.jwtService.verifyWsToken(data.token);
client.data.user = {
id: payload.sub,
connectionId: payload.connectionId || client.id,
roles: payload.roles || [],
permissions: payload.permissions || [],
};
this.autoJoinAuthorizedChannels(client);
return {
event: 'authenticated',
data: { success: true, userId: payload.sub }
};
}
catch (error) {
this.logger.error(`Authentication failed: ${error.message}`);
return {
event: 'authenticated',
data: { success: false, error: 'Authentication failed' }
};
}
}
handleSubscribe(client, data) {
if (this.rateLimiter && !this.rateLimiter.handleMessage(client)) {
return {
event: 'error',
data: {
message: 'Rate limit exceeded',
code: 'RATE_LIMIT_EXCEEDED'
}
};
}
if (this.rateLimiter && !this.rateLimiter.handleSubscription(client, data.channel)) {
return {
event: 'error',
data: {
message: 'Subscription limit exceeded',
channel: data.channel,
code: 'SUBSCRIPTION_LIMIT_EXCEEDED'
}
};
}
if (!client.data.user) {
return { event: 'error', data: { message: 'Not authenticated' } };
}
const channelName = data.channel;
if (!this.canSubscribeToChannel(client, channelName)) {
return {
event: 'error',
data: {
message: `Unauthorized to subscribe to channel: ${channelName}`,
channel: channelName
}
};
}
client.join(channelName);
this.logger.verbose(`Client ${client.id} subscribed to ${channelName}`);
client.emit(`${channelName}:welcome`, {
message: `Welcome to channel ${channelName}`,
timestamp: new Date().toISOString()
});
return {
event: 'subscribed',
data: { channel: channelName, success: true }
};
}
handleUnsubscribe(client, data) {
if (this.rateLimiter && !this.rateLimiter.handleMessage(client)) {
return {
event: 'error',
data: {
message: 'Rate limit exceeded',
code: 'RATE_LIMIT_EXCEEDED'
}
};
}
if (this.rateLimiter) {
this.rateLimiter.handleUnsubscription(client, data.channel);
}
client.leave(data.channel);
this.logger.verbose(`Client ${client.id} unsubscribed from ${data.channel}`);
return {
event: 'unsubscribed',
data: { channel: data.channel, success: true }
};
}
handlePublish(client, data) {
if (this.rateLimiter && !this.rateLimiter.handleMessage(client)) {
return {
event: 'error',
data: {
message: 'Rate limit exceeded',
code: 'RATE_LIMIT_EXCEEDED'
}
};
}
if (!client.data.user) {
return { event: 'error', data: { message: 'Not authenticated' } };
}
const { channel, event, payload } = data;
if (!this.canPublishToChannel(client, channel)) {
return {
event: 'error',
data: {
message: `Unauthorized to publish to channel: ${channel}`,
channel
}
};
}
this.server.to(channel).emit(event, {
...payload,
timestamp: new Date().toISOString(),
publisher: client.data.user.id
});
this.logger.verbose(`Client ${client.id} published to ${channel}: ${event}`);
return {
event: 'published',
data: { channel, event, success: true }
};
}
registerChannel(channelConfig) {
if (this.channels.has(channelConfig.name)) {
this.logger.warn(`Channel ${channelConfig.name} already exists`);
return false;
}
this.channels.set(channelConfig.name, channelConfig);
this.channelAcls.set(channelConfig.name, new Map());
this.logger.verbose(`Registered channel: ${channelConfig.name}`);
return true;
}
unregisterChannel(channelName) {
if (!this.channels.has(channelName)) {
return false;
}
this.server.in(channelName).socketsLeave(channelName);
this.channels.delete(channelName);
this.channelAcls.delete(channelName);
this.logger.verbose(`Unregistered channel: ${channelName}`);
return true;
}
grantChannelPermission(channelName, userId, permission) {
if (!this.channels.has(channelName)) {
this.logger.warn(`Cannot grant permission for non-existent channel: ${channelName}`);
return false;
}
const channelAcl = this.channelAcls.get(channelName);
channelAcl.set(String(userId), permission);
this.logger.verbose(`Granted ${permission} permission to user ${userId} for channel ${channelName}`);
this.notifyUserOfPermissionChange(userId, channelName, permission);
return true;
}
revokeChannelAccess(channelName, userId) {
if (!this.channels.has(channelName)) {
return false;
}
const channelAcl = this.channelAcls.get(channelName);
const userIdStr = String(userId);
if (!channelAcl.has(userIdStr)) {
return false;
}
channelAcl.delete(userIdStr);
this.disconnectUserFromChannel(userId, channelName);
this.logger.verbose(`Revoked access for user ${userId} from channel ${channelName}`);
return true;
}
broadcastToChannel(channel, event, data) {
this.server.to(channel).emit(event, {
...data,
timestamp: new Date().toISOString(),
channel
});
this.logger.verbose(`Broadcast to channel ${channel}: ${event}`);
}
sendToClient(clientId, event, data) {
const client = this.connectedClients.get(clientId);
if (!client) {
this.logger.warn(`Client not found: ${clientId}`);
return false;
}
client.emit(event, {
...data,
timestamp: new Date().toISOString()
});
this.logger.verbose(`Message sent to client ${clientId}: ${event}`);
return true;
}
autoJoinAuthorizedChannels(client) {
if (!client.data.user) {
return;
}
for (const [channelName, config] of this.channels.entries()) {
if (this.canSubscribeToChannel(client, channelName)) {
client.join(channelName);
client.emit('auto_subscribed', { channel: channelName });
this.logger.verbose(`Auto-subscribed client ${client.id} to channel ${channelName}`);
}
}
}
canSubscribeToChannel(client, channelName) {
const user = client.data.user;
if (!user) {
return false;
}
const channel = this.channels.get(channelName);
if (!channel) {
return false;
}
const channelAcl = this.channelAcls.get(channelName);
if (channelAcl.has(String(user.id))) {
return true;
}
if (channel.allowAnonymous) {
return true;
}
if (channel.allowPublicSubscribe) {
return true;
}
if (channel.requiredRoles && channel.requiredRoles.length > 0) {
const userRoles = user.roles || [];
if (channel.requiredRoles.some(role => userRoles.includes(role))) {
return true;
}
}
if (channel.requiredPermissions && channel.requiredPermissions.length > 0) {
const userPermissions = user.permissions || [];
if (channel.requiredPermissions.some(perm => userPermissions.includes(perm))) {
return true;
}
}
return false;
}
canPublishToChannel(client, channelName) {
const user = client.data.user;
if (!user) {
return false;
}
const channel = this.channels.get(channelName);
if (!channel) {
return false;
}
const channelAcl = this.channelAcls.get(channelName);
const userPermission = channelAcl.get(String(user.id));
if (userPermission === ChannelPermission.WRITE || userPermission === ChannelPermission.ADMIN) {
return true;
}
if (channel.allowPublicPublish) {
return true;
}
if (channel.requiredRoles && channel.requiredRoles.length > 0) {
const userRoles = user.roles || [];
if (channel.requiredRoles.some(role => userRoles.includes(role))) {
return true;
}
}
return false;
}
notifyUserOfPermissionChange(userId, channelName, permission) {
for (const [clientId, client] of this.connectedClients.entries()) {
if (client.data.user && String(client.data.user.id) === String(userId)) {
client.emit('permission_changed', {
channel: channelName,
permission,
timestamp: new Date().toISOString()
});
if (permission !== null) {
client.join(channelName);
client.emit(`${channelName}:welcome`, {
message: `Welcome to channel ${channelName}`,
timestamp: new Date().toISOString()
});
}
}
}
}
disconnectUserFromChannel(userId, channelName) {
for (const [clientId, client] of this.connectedClients.entries()) {
if (client.data.user && String(client.data.user.id) === String(userId)) {
client.leave(channelName);
client.emit('channel_access_revoked', {
channel: channelName,
timestamp: new Date().toISOString()
});
}
}
}
getServerStatus() {
return {
running: !!this.server,
uptime: Date.now() - this.serverStartTime
};
}
getActiveConnectionsCount() {
return this.connectedClients.size;
}
getActiveChannelsCount() {
return this.channels.size;
}
pingAllClients() {
const timestamp = Date.now();
let count = 0;
for (const [clientId, client] of this.connectedClients.entries()) {
this.lastPingTimestamps.set(clientId, timestamp);
client.emit('ping', { timestamp });
count++;
}
return count;
}
handlePong(client, timestamp) {
const originalTimestamp = this.lastPingTimestamps.get(client.id);
if (originalTimestamp && timestamp === originalTimestamp) {
const latency = Date.now() - originalTimestamp;
this.server.emit('ws:metrics:latency', { clientId: client.id, latency });
this.lastPingTimestamps.delete(client.id);
}
}
getChannelStats() {
var _a, _b;
const result = new Map();
for (const [channelName, _] of this.channels.entries()) {
const roomSize = ((_a = this.server.sockets.adapter.rooms.get(channelName)) === null || _a === void 0 ? void 0 : _a.size) || 0;
result.set(channelName, {
subscribers: roomSize,
acls: ((_b = this.channelAcls.get(channelName)) === null || _b === void 0 ? void 0 : _b.size) || 0
});
}
return result;
}
handleClientPong(client, data) {
this.handlePong(client, data.timestamp);
}
};
exports.WebsocketGateway = WebsocketGateway;
__decorate([
(0, websockets_1.WebSocketServer)(),
__metadata("design:type", socket_io_1.Server)
], WebsocketGateway.prototype, "server", void 0);
__decorate([
(0, websockets_1.SubscribeMessage)('authenticate'),
__param(0, (0, websockets_1.ConnectedSocket)()),
__param(1, (0, websockets_1.MessageBody)()),
__metadata("design:type", Function),
__metadata("design:paramtypes", [socket_io_1.Socket, Object]),
__metadata("design:returntype", Promise)
], WebsocketGateway.prototype, "handleAuthenticate", null);
__decorate([
(0, websockets_1.SubscribeMessage)('subscribe'),
__param(0, (0, websockets_1.ConnectedSocket)()),
__param(1, (0, websockets_1.MessageBody)()),
__metadata("design:type", Function),
__metadata("design:paramtypes", [socket_io_1.Socket, Object]),
__metadata("design:returntype", Object)
], WebsocketGateway.prototype, "handleSubscribe", null);
__decorate([
(0, websockets_1.SubscribeMessage)('unsubscribe'),
__param(0, (0, websockets_1.ConnectedSocket)()),
__param(1, (0, websockets_1.MessageBody)()),
__metadata("design:type", Function),
__metadata("design:paramtypes", [socket_io_1.Socket, Object]),
__metadata("design:returntype", Object)
], WebsocketGateway.prototype, "handleUnsubscribe", null);
__decorate([
(0, websockets_1.SubscribeMessage)('publish'),
__param(0, (0, websockets_1.ConnectedSocket)()),
__param(1, (0, websockets_1.MessageBody)()),
__metadata("design:type", Function),
__metadata("design:paramtypes", [socket_io_1.Socket, Object]),
__metadata("design:returntype", Object)
], WebsocketGateway.prototype, "handlePublish", null);
__decorate([
(0, websockets_1.SubscribeMessage)('pong'),
__param(0, (0, websockets_1.ConnectedSocket)()),
__param(1, (0, websockets_1.MessageBody)()),
__metadata("design:type", Function),
__metadata("design:paramtypes", [socket_io_1.Socket, Object]),
__metadata("design:returntype", void 0)
], WebsocketGateway.prototype, "handleClientPong", null);
exports.WebsocketGateway = WebsocketGateway = WebsocketGateway_1 = __decorate([
(0, websockets_1.WebSocketGateway)({
cors: {
origin: '*',
},
}),
__param(1, (0, common_1.Optional)()),
__metadata("design:paramtypes", [jwt_service_1.JwtService,
websocket_rate_limiter_1.WebsocketRateLimiter])
], WebsocketGateway);
//# sourceMappingURL=websocket.gateway.js.map