385 lines
15 KiB
JavaScript
385 lines
15 KiB
JavaScript
|
|
"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
|