From 3424c17ab3379eaf0b2ab3fb4dddc520a4ffe95a Mon Sep 17 00:00:00 2001 From: Tom Butcher Date: Mon, 18 Aug 2025 01:07:14 +0100 Subject: [PATCH] Refactor socket management: replace SocketClient with SocketUser and SocketHost classes - Removed SocketClient class and its associated functionality. - Introduced SocketUser and SocketHost classes to handle user and host socket connections respectively. - Updated SocketManager to manage user and host connections, including authentication and event handling. - Enhanced event handling for user actions such as locking, unlocking, and subscribing to updates. --- src/socket/socketclient.js | 128 --------------------------- src/socket/sockethost.js | 117 +++++++++++++++++++++++++ src/socket/socketmanager.js | 107 +++++++++++++++-------- src/socket/socketuser.js | 168 ++++++++++++++++++++++++++++++++++++ 4 files changed, 357 insertions(+), 163 deletions(-) delete mode 100644 src/socket/socketclient.js create mode 100644 src/socket/sockethost.js create mode 100644 src/socket/socketuser.js diff --git a/src/socket/socketclient.js b/src/socket/socketclient.js deleted file mode 100644 index 9cf695f..0000000 --- a/src/socket/socketclient.js +++ /dev/null @@ -1,128 +0,0 @@ -import log4js from 'log4js'; -// Load configuration -import { loadConfig } from '../config.js'; -import { userModel } from '../database/user.schema.js'; - -const config = loadConfig(); - -const logger = log4js.getLogger('Socket Client'); -logger.level = config.server.logLevel; - -export class SocketClient { - constructor(socket, socketManager) { - this.socket = socket; - this.user = null; - this.socketManager = socketManager; - this.lockManager = socketManager.lockManager; - this.updateManager = socketManager.updateManager; - } - - async initUser() { - if (this.socket?.user?.username) { - try { - const userDoc = await userModel - .findOne({ username: this.socket.user.username }) - .lean(); - this.user = userDoc; - logger.debug('ID:', this.user._id.toString()); - logger.debug('Name:', this.user.name); - logger.debug('Username:', this.user.username); - logger.debug('Email:', this.user.email); - this.setupSocketEventHandlers(); - } catch (err) { - logger.error('Error looking up user by username:', err); - this.user = null; - } - } - } - - setupSocketEventHandlers() { - this.socket.on('lock', this.handleLockEvent.bind(this)); - this.socket.on('unlock', this.handleUnlockEvent.bind(this)); - this.socket.on('getLock', this.handleGetLockEvent.bind(this)); - this.socket.on('update', this.handleUpdateEvent.bind(this)); - } - - async handleLockEvent(data) { - // data: { _id: string, params?: object } - if (!data || !data._id) { - this.socket.emit('lock_result', { - success: false, - error: 'Invalid lock event data' - }); - return; - } - data = { ...data, user: this.user._id.toString() }; - try { - await this.lockManager.lockObject(data); - } catch (err) { - logger.error('Lock event error:', err); - this.socket.emit('lock_result', { success: false, error: err.message }); - } - } - - async handleUnlockEvent(data) { - // data: { _id: string } - if (!data || !data._id) { - this.socket.emit('unlock_result', { - success: false, - error: 'Invalid unlock event data' - }); - return; - } - data = { ...data, user: this.user._id.toString() }; - try { - await this.lockManager.unlockObject(data); - } catch (err) { - logger.error('Unlock event error:', err); - this.socket.emit('unlock_result', { success: false, error: err.message }); - } - } - - async handleGetLockEvent(data, callback) { - // data: { _id: string } - if (!data || !data._id) { - callback({ - error: 'Invalid getLock event data' - }); - return; - } - try { - const lockEvent = await this.lockManager.getObjectLock(data); - callback(lockEvent); - } catch (err) { - logger.error('GetLock event error:', err); - callback({ - error: err.message - }); - } - } - - async handleUpdateEvent(data) { - // data: { _id: string, type: string, ...otherProperties } - if (!data || !data._id || !data.type) { - return; - } - - try { - // Add user information to the update data - const updateData = { - ...data, - updatedAt: new Date() - }; - - // Use the updateManager to handle the update - if (this.updateManager) { - await this.updateManager.updateObject(updateData); - } else { - throw new Error('UpdateManager not available'); - } - } catch (err) { - logger.error('Update event error:', err); - } - } - - handleDisconnect() { - logger.info('External client disconnected:', this.socket.user?.username); - } -} diff --git a/src/socket/sockethost.js b/src/socket/sockethost.js new file mode 100644 index 0000000..a3b1a37 --- /dev/null +++ b/src/socket/sockethost.js @@ -0,0 +1,117 @@ +import log4js from 'log4js'; +// Load configuration +import { loadConfig } from '../config.js'; +import { CodeAuth, createAuthMiddleware } from '../auth/auth.js'; +import { editObject, getObject } from '../database/database.js'; +import { hostModel } from '../database/schemas/management/host.schema.js'; +import { UpdateManager } from '../updates/updatemanager.js'; +import { ActionManager } from '../actions/actionmanager.js'; +import { getModelByName } from '../utils.js'; + +const config = loadConfig(); + +const logger = log4js.getLogger('Socket Host'); +logger.level = config.server.logLevel; + +export class SocketHost { + constructor(socket, socketManager) { + this.socket = socket; + this.authenticated = false; + this.socketId = socket.id; + this.id = null; + this.host = null; + this.socketManager = socketManager; + this.updateManager = new UpdateManager(this); + this.actionManager = new ActionManager(this); + this.codeAuth = new CodeAuth(); + this.setupSocketEventHandlers(); + } + + setupSocketEventHandlers() { + this.socket.use(createAuthMiddleware(this)); + this.socket.on('authenticate', this.handleAuthenticate.bind(this)); + this.socket.on('updateHost', this.handleUpdateHost.bind(this)); + this.socket.on('getObject', this.handleGetObject.bind(this)); + this.socket.on('disconnect', this.handleDisconnect.bind(this)); + } + + async initializeHost() { + this.actionManager.subscribeToObjectActions(this.id, 'host'); + } + + async handleAuthenticate(data, callback) { + logger.trace('handleAuthenticateEvent'); + const id = data.id || undefined; + const authCode = data.authCode || undefined; + const otp = data.otp || undefined; + + if (id && authCode) { + logger.info('Authenticating host with id + authCode...'); + const verifyResult = await this.codeAuth.verifyCode(id, authCode); + if (verifyResult.valid == true) { + logger.info('Host authenticated and valid.'); + this.host = verifyResult.host; + this.id = this.host._id.toString(); + this.authenticated = true; + await editObject({ + model: hostModel, + id: this.host._id, + updateData: { online: true, state: { type: 'online' } }, + owner: this.host, + ownerType: 'host' + }); + await this.initializeHost(); + } + callback(verifyResult); + return; + } + + if (otp) { + logger.info('Authenticating host otp...'); + const verifyResult = await this.codeAuth.verifyOtp(otp); + if (verifyResult.valid == true) { + logger.info('Host authenticated and valid.'); + this.host = verifyResult.host; + this.authenticated = true; + } + callback(verifyResult); + return; + } + + callback({ valid: false, error: 'Missing params.' }); + } + + async handleUpdateHost(data) { + await editObject({ + model: hostModel, + id: this.host._id, + updateData: { ...data.host }, + owner: this.host, + ownerType: 'host' + }); + } + + async handleGetObject(data, callback) { + const object = await getObject({ + model: getModelByName(data.objectType), + id: data._id, + cached: true, + populate: data.populate + }); + callback(object); + } + + async handleDisconnect() { + if (this.authenticated) { + await editObject({ + model: hostModel, + id: this.host._id, + updateData: { online: false, state: { type: 'offline' } }, + owner: this.host, + ownerType: 'host' + }); + this.authenticated = false; + } + logger.info('External host disconnected. Socket ID:', this.id); + } +} diff --git a/src/socket/socketmanager.js b/src/socket/socketmanager.js index 82258a4..f83e1c5 100644 --- a/src/socket/socketmanager.js +++ b/src/socket/socketmanager.js @@ -1,12 +1,13 @@ // server.js - HTTP and Socket.IO server setup import { Server } from 'socket.io'; -import { createAuthMiddleware } from '../auth/auth.js'; import log4js from 'log4js'; // Load configuration import { loadConfig } from '../config.js'; -import { SocketClient } from './socketclient.js'; +import { SocketUser } from './socketuser.js'; import { LockManager } from '../lock/lockmanager.js'; import { UpdateManager } from '../updates/updatemanager.js'; +import { TemplateManager } from '../templates/templatemanager.js'; +import { SocketHost } from './sockethost.js'; const config = loadConfig(); @@ -14,10 +15,10 @@ const logger = log4js.getLogger('Socket Manager'); logger.level = config.server.logLevel; export class SocketManager { - constructor(auth, server) { - this.socketClientConnections = new Map(); - this.lockManager = new LockManager(this); - this.updateManager = new UpdateManager(this); + constructor(server) { + this.socketUsers = new Map(); + this.socketHosts = new Map(); + this.templateManager = new TemplateManager(this); // Use the provided HTTP server // Create Socket.IO server @@ -28,55 +29,91 @@ export class SocketManager { } }); - // Apply authentication middleware - io.use(createAuthMiddleware(auth)); - - // Handle client connections + // Handle user connections io.on('connection', async socket => { - logger.info('External client connected:', socket.user?.username); - await this.addClient(socket); + const authType = socket.handshake?.auth?.type; + if (authType == 'user') { + await this.addUser(socket); + } else if (authType == 'host') { + await this.addHost(socket); + } }); this.io = io; this.server = server; } - async addClient(socket) { - const client = new SocketClient(socket, this, this.lockManager); - await client.initUser(); - this.socketClientConnections.set(socket.id, client); - logger.info('External client connected:', socket.user?.username); + async addUser(socket) { + const socketUser = new SocketUser(socket, this, this.lockManager); + this.socketUsers.set(socketUser.id, socketUser); + logger.info('External user connected. Socket ID:', socket.id); // Handle disconnection socket.on('disconnect', () => { - logger.info('External client disconnected:', socket.user?.username); - this.removeClient(socket.id); + logger.info('External user disconnected. Socket ID:', socket.id); + this.removeUser(socket.id); }); } - removeClient(socketClientId) { - const socketClient = this.socketClientConnections.get(socketClientId); - if (socketClient) { - this.socketClientConnections.delete(socketClientId); - logger.info( - 'External client disconnected:', - socketClient.socket.user?.username - ); + async addHost(socket) { + const socketHost = new SocketHost(socket, this, this.lockManager); + this.socketHosts.set(socketHost.id, socketHost); + logger.info('External host connected. Socket ID:', socket.id); + // Handle disconnection + socket.on('disconnect', () => { + logger.info('External host disconnected. Socket ID:', socket.id); + this.removeHost(socket.id); + }); + } + + removeUser(id) { + const socketUser = this.socketUsers.get(id); + if (socketUser) { + this.socketUsers.delete(id); + logger.info('External user disconnected. Socket ID:', id); } } - getSocketClient(clientId) { - return this.socketClientConnections.get(clientId); + removeHost(id) { + const socketHost = this.socketHosts.get(id); + if (socketHost) { + this.socketHosts.delete(id); + logger.info('External host disconnected. Socket ID:', id); + } } - getAllSocketClients() { - return Array.from(this.socketClientConnections.values()); + getSocketUser(userId) { + return this.socketUserConnections.get(userId); } - broadcast(event, data, excludeClientId = null) { - for (const [clientId, socketClient] of this.socketClientConnections) { - if (excludeClientId !== clientId) { - socketClient.socket.emit(event, data); + getAllSocketUsers() { + return Array.from(this.socketUserConnections.values()); + } + + broadcast(event, data, excludeUserId = null) { + for (const [userId, socketUser] of this.socketUserConnections) { + if (excludeUserId !== userId) { + socketUser.socket.emit(event, data); } } } + + /** + * Send a message to a specific user by their user ID + * @param {string} userId - The user ID to send the message to + * @param {string} event - The event name + * @param {any} data - The data to send + */ + sendToUser(id, event, data) { + let sentCount = 0; + for (const [, socketUser] of this.socketUsers) { + if (socketUser.user && socketUser.user._id.toString() === id) { + socketUser.socket.emit(event, data); + sentCount += 1; + logger.debug( + `Sent ${event} to user: ${id}, connection: ${socketUser.socket.id}` + ); + } + } + logger.debug(`Sent to ${sentCount} active connection(s).`); + } } diff --git a/src/socket/socketuser.js b/src/socket/socketuser.js new file mode 100644 index 0000000..0187882 --- /dev/null +++ b/src/socket/socketuser.js @@ -0,0 +1,168 @@ +import log4js from 'log4js'; +// Load configuration +import { loadConfig } from '../config.js'; +import { createAuthMiddleware, KeycloakAuth } from '../auth/auth.js'; +import { generateHostOTP } from '../utils.js'; +import { LockManager } from '../lock/lockmanager.js'; +import { UpdateManager } from '../updates/updatemanager.js'; +import { ActionManager } from '../actions/actionmanager.js'; + +const config = loadConfig(); + +const logger = log4js.getLogger('Socket User'); +logger.level = config.server.logLevel; + +export class SocketUser { + constructor(socket, socketManager) { + this.socket = socket; + this.authenticated = false; + this.socketId = socket.id; + this.id = null; + this.user = null; + this.socketManager = socketManager; + this.lockManager = new LockManager(this); + this.updateManager = new UpdateManager(this); + this.actionManager = new ActionManager(this); + this.templateManager = socketManager.templateManager; + this.keycloakAuth = new KeycloakAuth(); + this.setupSocketEventHandlers(); + } + + setupSocketEventHandlers() { + this.socket.use(createAuthMiddleware(this)); + this.socket.on('authenticate', this.handleAuthenticateEvent.bind(this)); + this.socket.on('lock', this.handleLockEvent.bind(this)); + this.socket.on('unlock', this.handleUnlockEvent.bind(this)); + this.socket.on('getLock', this.handleGetLockEvent.bind(this)); + this.socket.on( + 'subscribeToObjectTypeUpdate', + this.handleSubscribeToObjectTypeUpdateEvent.bind(this) + ); + this.socket.on( + 'subscribeToObjectUpdate', + this.handleSubscribeToObjectUpdateEvent.bind(this) + ); + this.socket.on( + 'previewTemplate', + this.handlePreviewTemplateEvent.bind(this) + ); + this.socket.on( + 'generateHostOtp', + this.handleGenerateHostOtpEvent.bind(this) + ); + this.socket.on('objectAction', this.handleObjectActionEvent.bind(this)); + } + + async handleAuthenticateEvent(data, callback) { + const token = data.token || undefined; + logger.info('Authenticating user with token...'); + if (token) { + const result = await this.keycloakAuth.verifyToken(token); + if (result.valid == true) { + logger.info('User authenticated and valid.'); + this.user = result.user; + this.id = this.user._id.toString(); + this.authenticated = true; + } else { + logger.warn('User is not authenticated.'); + } + callback(result); + } + } + + async handleLockEvent(data) { + // data: { _id: string, params?: object } + if (!data || !data._id) { + this.socket.emit('lock_result', { + success: false, + error: 'Invalid lock event data' + }); + return; + } + data = { ...data, user: this.user._id.toString() }; + try { + await this.lockManager.lockObject(data); + } catch (err) { + logger.error('Lock event error:', err); + this.socket.emit('lock_result', { success: false, error: err.message }); + } + } + + async handleUnlockEvent(data) { + // data: { _id: string } + if (!data || !data._id) { + this.socket.emit('unlock_result', { + success: false, + error: 'Invalid unlock event data' + }); + return; + } + data = { ...data, user: this.user._id.toString() }; + try { + await this.lockManager.unlockObject(data); + } catch (err) { + logger.error('Unlock event error:', err); + this.socket.emit('unlock_result', { success: false, error: err.message }); + } + } + + async handleGetLockEvent(data, callback) { + // data: { _id: string } + if (!data || !data._id) { + callback({ + error: 'Invalid getLock event data' + }); + return; + } + try { + const lockEvent = await this.lockManager.getObjectLock(data); + callback(lockEvent); + } catch (err) { + logger.error('GetLock event error:', err); + callback({ + error: err.message + }); + } + } + + async handleSubscribeToObjectTypeUpdateEvent(data, callback) { + const result = this.updateManager.subscribeToObjectNew(data.objectType); + callback(result); + } + + async handleSubscribeToObjectUpdateEvent(data, callback) { + const result = this.updateManager.subscribeToObjectUpdate( + data._id, + data.objectType + ); + callback(result); + } + + async handlePreviewTemplateEvent(data, callback) { + const result = await this.templateManager.renderTemplate( + data._id, + data.content, + data.testObject, + data.scale + ); + callback(result); + } + + async handleGenerateHostOtpEvent(data, callback) { + const result = await generateHostOTP(data._id); + callback(result); + } + + async handleObjectActionEvent(data, callback) { + await this.actionManager.sendObjectAction( + data._id, + data.objectType, + data.action, + callback + ); + } + + handleDisconnect() { + logger.info('External user disconnected:', this.socket.user?.username); + } +}