111
This commit is contained in:
385
dist/managers/WebClientManager.js
vendored
Normal file
385
dist/managers/WebClientManager.js
vendored
Normal file
@@ -0,0 +1,385 @@
|
||||
"use strict";
|
||||
var __importDefault = (this && this.__importDefault) || function (mod) {
|
||||
return (mod && mod.__esModule) ? mod : { "default": mod };
|
||||
};
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
const Logger_1 = __importDefault(require("../utils/Logger"));
|
||||
/**
|
||||
* Web客户端管理器
|
||||
*/
|
||||
class WebClientManager {
|
||||
constructor(databaseService) {
|
||||
this.clients = new Map();
|
||||
this.socketToClient = new Map();
|
||||
this.deviceControllers = new Map(); // deviceId -> clientId
|
||||
// 🔧 添加请求速率限制 - 防止频繁重复请求
|
||||
this.requestTimestamps = new Map(); // "clientId:deviceId" -> timestamp
|
||||
this.REQUEST_COOLDOWN = 2000; // 2秒内不允许重复请求(增加冷却时间)
|
||||
this.logger = new Logger_1.default('WebClientManager');
|
||||
this.databaseService = databaseService;
|
||||
}
|
||||
/**
|
||||
* ✅ 清理所有客户端记录(服务器重启时调用)
|
||||
*/
|
||||
clearAllClients() {
|
||||
const clientCount = this.clients.size;
|
||||
this.clients.clear();
|
||||
this.socketToClient.clear();
|
||||
this.deviceControllers.clear();
|
||||
this.requestTimestamps.clear();
|
||||
this.logger.info(`🧹 已清理所有客户端记录: ${clientCount} 个客户端`);
|
||||
}
|
||||
/**
|
||||
* 设置Socket.IO实例
|
||||
*/
|
||||
setSocketIO(io) {
|
||||
this.io = io;
|
||||
}
|
||||
/**
|
||||
* 添加Web客户端
|
||||
*/
|
||||
addClient(clientInfo) {
|
||||
// 🔧 检查是否已有相同Socket ID的客户端记录
|
||||
const existingClientId = this.socketToClient.get(clientInfo.socketId);
|
||||
if (existingClientId) {
|
||||
this.logger.warn(`⚠️ Socket ${clientInfo.socketId} 已有客户端记录 ${existingClientId},清理旧记录`);
|
||||
this.removeClient(existingClientId);
|
||||
}
|
||||
this.clients.set(clientInfo.id, clientInfo);
|
||||
this.socketToClient.set(clientInfo.socketId, clientInfo.id);
|
||||
this.logger.info(`Web客户端已添加: ${clientInfo.id} from ${clientInfo.ip}`);
|
||||
}
|
||||
/**
|
||||
* 移除Web客户端
|
||||
*/
|
||||
removeClient(clientId) {
|
||||
const client = this.clients.get(clientId);
|
||||
if (client) {
|
||||
this.clients.delete(clientId);
|
||||
this.socketToClient.delete(client.socketId);
|
||||
// 如果客户端正在控制设备,释放控制权
|
||||
if (client.controllingDeviceId) {
|
||||
this.logger.info(`🔓 客户端断开连接,自动释放设备控制权: ${clientId} -> ${client.controllingDeviceId}`);
|
||||
this.releaseDeviceControl(client.controllingDeviceId);
|
||||
}
|
||||
// 清理请求时间戳记录
|
||||
const keysToDelete = Array.from(this.requestTimestamps.keys()).filter(key => key.startsWith(clientId + ':'));
|
||||
keysToDelete.forEach(key => this.requestTimestamps.delete(key));
|
||||
this.logger.info(`Web客户端已移除: ${clientId}`);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
/**
|
||||
* 通过Socket ID移除客户端
|
||||
*/
|
||||
removeClientBySocketId(socketId) {
|
||||
const clientId = this.socketToClient.get(socketId);
|
||||
if (clientId) {
|
||||
return this.removeClient(clientId);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
/**
|
||||
* 获取客户端信息
|
||||
*/
|
||||
getClient(clientId) {
|
||||
return this.clients.get(clientId);
|
||||
}
|
||||
/**
|
||||
* 通过Socket ID获取客户端
|
||||
*/
|
||||
getClientBySocketId(socketId) {
|
||||
const clientId = this.socketToClient.get(socketId);
|
||||
return clientId ? this.clients.get(clientId) : undefined;
|
||||
}
|
||||
/**
|
||||
* 获取所有客户端
|
||||
*/
|
||||
getAllClients() {
|
||||
return Array.from(this.clients.values());
|
||||
}
|
||||
/**
|
||||
* 获取客户端数量
|
||||
*/
|
||||
getClientCount() {
|
||||
return this.clients.size;
|
||||
}
|
||||
/**
|
||||
* 获取客户端Socket
|
||||
*/
|
||||
getClientSocket(clientId) {
|
||||
const client = this.clients.get(clientId);
|
||||
if (client && this.io) {
|
||||
return this.io.sockets.sockets.get(client.socketId);
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
/**
|
||||
* 请求控制设备
|
||||
*/
|
||||
requestDeviceControl(clientId, deviceId) {
|
||||
// 🔧 防止频繁重复请求
|
||||
const requestKey = `${clientId}:${deviceId}`;
|
||||
const now = Date.now();
|
||||
const lastRequestTime = this.requestTimestamps.get(requestKey) || 0;
|
||||
if (now - lastRequestTime < this.REQUEST_COOLDOWN) {
|
||||
this.logger.debug(`🚫 请求过于频繁: ${clientId} -> ${deviceId} (间隔${now - lastRequestTime}ms < ${this.REQUEST_COOLDOWN}ms)`);
|
||||
return {
|
||||
success: false,
|
||||
message: '请求过于频繁,请稍后再试'
|
||||
};
|
||||
}
|
||||
// 获取客户端信息
|
||||
const client = this.clients.get(clientId);
|
||||
if (!client) {
|
||||
this.logger.error(`❌ 客户端不存在: ${clientId}`);
|
||||
return {
|
||||
success: false,
|
||||
message: '客户端不存在'
|
||||
};
|
||||
}
|
||||
// ✅ 优化:先检查是否是重复请求(已经在控制此设备)
|
||||
const currentController = this.deviceControllers.get(deviceId);
|
||||
if (currentController === clientId) {
|
||||
this.logger.debug(`🔄 客户端 ${clientId} 重复请求控制设备 ${deviceId},已在控制中`);
|
||||
client.lastSeen = new Date();
|
||||
// 更新请求时间戳,但返回成功(避免频繁日志)
|
||||
this.requestTimestamps.set(requestKey, now);
|
||||
return {
|
||||
success: true,
|
||||
message: '已在控制此设备'
|
||||
};
|
||||
}
|
||||
// 记录请求时间戳(在检查重复控制后记录)
|
||||
this.requestTimestamps.set(requestKey, now);
|
||||
// 检查设备是否被其他客户端控制
|
||||
if (currentController && currentController !== clientId) {
|
||||
const controllerClient = this.clients.get(currentController);
|
||||
this.logger.warn(`🚫 设备 ${deviceId} 控制权冲突: 当前控制者 ${currentController}, 请求者 ${clientId}`);
|
||||
return {
|
||||
success: false,
|
||||
message: `设备正在被其他客户端控制 (${controllerClient?.ip || 'unknown'})`,
|
||||
currentController
|
||||
};
|
||||
}
|
||||
// 如果客户端已在控制其他设备,先释放
|
||||
if (client.controllingDeviceId && client.controllingDeviceId !== deviceId) {
|
||||
this.logger.info(`🔄 客户端 ${clientId} 切换控制设备: ${client.controllingDeviceId} -> ${deviceId}`);
|
||||
this.releaseDeviceControl(client.controllingDeviceId);
|
||||
}
|
||||
// 建立控制关系
|
||||
this.deviceControllers.set(deviceId, clientId);
|
||||
client.controllingDeviceId = deviceId;
|
||||
client.lastSeen = new Date();
|
||||
// 🔐 如果客户端有用户ID,将权限持久化到数据库
|
||||
if (client.userId && this.databaseService) {
|
||||
this.databaseService.grantUserDevicePermission(client.userId, deviceId, 'control');
|
||||
this.logger.info(`🔐 用户 ${client.userId} 的设备 ${deviceId} 控制权限已持久化`);
|
||||
}
|
||||
this.logger.info(`🎮 客户端 ${clientId} 开始控制设备 ${deviceId}`);
|
||||
return {
|
||||
success: true,
|
||||
message: '控制权获取成功'
|
||||
};
|
||||
}
|
||||
/**
|
||||
* 释放设备控制权
|
||||
*/
|
||||
releaseDeviceControl(deviceId) {
|
||||
const controllerId = this.deviceControllers.get(deviceId);
|
||||
if (controllerId) {
|
||||
const client = this.clients.get(controllerId);
|
||||
if (client) {
|
||||
const previousDevice = client.controllingDeviceId;
|
||||
client.controllingDeviceId = undefined;
|
||||
this.logger.debug(`🔓 客户端 ${controllerId} 释放设备控制权: ${previousDevice}`);
|
||||
}
|
||||
else {
|
||||
this.logger.warn(`⚠️ 控制设备 ${deviceId} 的客户端 ${controllerId} 不存在,可能已断开`);
|
||||
}
|
||||
this.deviceControllers.delete(deviceId);
|
||||
this.logger.info(`🔓 设备 ${deviceId} 的控制权已释放 (之前控制者: ${controllerId})`);
|
||||
return true;
|
||||
}
|
||||
else {
|
||||
this.logger.debug(`🤷 设备 ${deviceId} 没有被控制,无需释放`);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
/**
|
||||
* 获取设备控制者
|
||||
*/
|
||||
getDeviceController(deviceId) {
|
||||
return this.deviceControllers.get(deviceId);
|
||||
}
|
||||
/**
|
||||
* 检查客户端是否有设备控制权
|
||||
*/
|
||||
hasDeviceControl(clientId, deviceId) {
|
||||
// 🛡️ 记录权限检查审计日志
|
||||
this.logPermissionOperation(clientId, deviceId, '权限检查');
|
||||
// 🔐 获取客户端信息
|
||||
const client = this.clients.get(clientId);
|
||||
// 🆕 超级管理员绕过权限检查
|
||||
if (client?.username) {
|
||||
const superAdminUsername = process.env.SUPERADMIN_USERNAME || 'superadmin';
|
||||
if (client.username === superAdminUsername) {
|
||||
this.logger.info(`🔐 超级管理员 ${client.username} 绕过设备控制权限检查`);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
// 🔐 首先检查内存中的控制权
|
||||
const memoryControl = this.deviceControllers.get(deviceId) === clientId;
|
||||
if (memoryControl) {
|
||||
return true;
|
||||
}
|
||||
// 🔐 如果内存中没有控制权,检查数据库中的用户权限
|
||||
if (client?.userId && this.databaseService) {
|
||||
const hasPermission = this.databaseService.hasUserDevicePermission(client.userId, deviceId, 'control');
|
||||
if (hasPermission) {
|
||||
// 🔐 如果用户有权限,自动建立控制关系(允许权限恢复)
|
||||
this.deviceControllers.set(deviceId, clientId);
|
||||
client.controllingDeviceId = deviceId;
|
||||
this.logger.info(`🔐 用户 ${client.userId} 基于数据库权限获得设备 ${deviceId} 控制权`);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
/**
|
||||
* 向指定客户端发送消息
|
||||
*/
|
||||
sendToClient(clientId, event, data) {
|
||||
const socket = this.getClientSocket(clientId);
|
||||
if (socket) {
|
||||
socket.emit(event, data);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
/**
|
||||
* 向所有客户端广播消息
|
||||
*/
|
||||
broadcastToAll(event, data) {
|
||||
if (this.io) {
|
||||
let activeClients = 0;
|
||||
// 只向Web客户端广播,且过滤掉已断开的连接
|
||||
for (const [socketId, clientId] of this.socketToClient.entries()) {
|
||||
const socket = this.io.sockets.sockets.get(socketId);
|
||||
if (socket && socket.connected) {
|
||||
socket.emit(event, data);
|
||||
activeClients++;
|
||||
}
|
||||
}
|
||||
this.logger.debug(`📡 广播消息到 ${activeClients} 个活跃Web客户端: ${event}`);
|
||||
}
|
||||
}
|
||||
/**
|
||||
* 向控制指定设备的客户端发送消息
|
||||
*/
|
||||
sendToDeviceController(deviceId, event, data) {
|
||||
const controllerId = this.deviceControllers.get(deviceId);
|
||||
if (controllerId) {
|
||||
return this.sendToClient(controllerId, event, data);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
/**
|
||||
* 更新客户端活跃时间
|
||||
*/
|
||||
updateClientActivity(socketId) {
|
||||
const clientId = this.socketToClient.get(socketId);
|
||||
if (clientId) {
|
||||
const client = this.clients.get(clientId);
|
||||
if (client) {
|
||||
client.lastSeen = new Date();
|
||||
}
|
||||
}
|
||||
}
|
||||
/**
|
||||
* 清理不活跃的客户端
|
||||
*/
|
||||
cleanupInactiveClients(timeoutMs = 600000) {
|
||||
const now = Date.now();
|
||||
const clientsToRemove = [];
|
||||
for (const [clientId, client] of this.clients.entries()) {
|
||||
if (now - client.lastSeen.getTime() > timeoutMs) {
|
||||
clientsToRemove.push(clientId);
|
||||
}
|
||||
}
|
||||
clientsToRemove.forEach(clientId => {
|
||||
this.removeClient(clientId);
|
||||
});
|
||||
if (clientsToRemove.length > 0) {
|
||||
this.logger.info(`已清理 ${clientsToRemove.length} 个不活跃的Web客户端`);
|
||||
}
|
||||
}
|
||||
/**
|
||||
* 获取客户端统计信息
|
||||
*/
|
||||
getClientStats() {
|
||||
const clients = Array.from(this.clients.values());
|
||||
return {
|
||||
total: clients.length,
|
||||
controlling: clients.filter(c => c.controllingDeviceId).length,
|
||||
idle: clients.filter(c => !c.controllingDeviceId).length,
|
||||
};
|
||||
}
|
||||
/**
|
||||
* 🔐 恢复用户的设备权限
|
||||
*/
|
||||
restoreUserPermissions(userId, clientId) {
|
||||
if (!this.databaseService) {
|
||||
this.logger.warn('数据库服务未初始化,无法恢复用户权限');
|
||||
return;
|
||||
}
|
||||
try {
|
||||
// 获取用户的所有设备权限
|
||||
const permissions = this.databaseService.getUserDevicePermissions(userId);
|
||||
if (permissions.length > 0) {
|
||||
this.logger.info(`🔐 为用户 ${userId} 恢复 ${permissions.length} 个设备权限`);
|
||||
// 恢复第一个设备的控制权(优先恢复用户之前的权限)
|
||||
for (const permission of permissions) {
|
||||
if (permission.permissionType === 'control') {
|
||||
// 直接恢复权限,不检查冲突(因为这是用户自己的权限恢复)
|
||||
this.deviceControllers.set(permission.deviceId, clientId);
|
||||
const client = this.clients.get(clientId);
|
||||
if (client) {
|
||||
client.controllingDeviceId = permission.deviceId;
|
||||
this.logger.info(`🔐 用户 ${userId} 的设备 ${permission.deviceId} 控制权已恢复`);
|
||||
break; // 只恢复第一个设备
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (error) {
|
||||
this.logger.error('恢复用户权限失败:', error);
|
||||
}
|
||||
}
|
||||
/**
|
||||
* 🔐 设置客户端用户信息
|
||||
*/
|
||||
setClientUserInfo(clientId, userId, username) {
|
||||
const client = this.clients.get(clientId);
|
||||
if (client) {
|
||||
client.userId = userId;
|
||||
client.username = username;
|
||||
this.logger.info(`🔐 客户端 ${clientId} 用户信息已设置: ${username} (${userId})`);
|
||||
// 🛡️ 记录安全审计日志
|
||||
this.logger.info(`🛡️ 安全审计: 客户端 ${clientId} (IP: ${client.ip}) 绑定用户 ${username} (${userId})`);
|
||||
}
|
||||
}
|
||||
/**
|
||||
* 🛡️ 记录权限操作审计日志
|
||||
*/
|
||||
logPermissionOperation(clientId, deviceId, operation) {
|
||||
const client = this.clients.get(clientId);
|
||||
if (client) {
|
||||
this.logger.info(`🛡️ 权限审计: 客户端 ${clientId} (用户: ${client.username || 'unknown'}, IP: ${client.ip}) 执行 ${operation} 操作,目标设备: ${deviceId}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
exports.default = WebClientManager;
|
||||
//# sourceMappingURL=WebClientManager.js.map
|
||||
Reference in New Issue
Block a user