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.
This commit is contained in:
Tom Butcher 2025-08-18 01:07:14 +01:00
parent 03eb0a61c1
commit 3424c17ab3
4 changed files with 357 additions and 163 deletions

View File

@ -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);
}
}

117
src/socket/sockethost.js Normal file
View File

@ -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);
}
}

View File

@ -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);
}
}
removeHost(id) {
const socketHost = this.socketHosts.get(id);
if (socketHost) {
this.socketHosts.delete(id);
logger.info('External host disconnected. Socket ID:', id);
}
}
getSocketUser(userId) {
return this.socketUserConnections.get(userId);
}
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}`
);
}
}
getSocketClient(clientId) {
return this.socketClientConnections.get(clientId);
}
getAllSocketClients() {
return Array.from(this.socketClientConnections.values());
}
broadcast(event, data, excludeClientId = null) {
for (const [clientId, socketClient] of this.socketClientConnections) {
if (excludeClientId !== clientId) {
socketClient.socket.emit(event, data);
}
}
logger.debug(`Sent to ${sentCount} active connection(s).`);
}
}

168
src/socket/socketuser.js Normal file
View File

@ -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);
}
}