Refactor HostManager and SocketClient for improved authentication and event handling
All checks were successful
farmcontrol/farmcontrol-server/pipeline/head This commit looks good

- Introduced a dedicated `HostManager` class to encapsulate host-related logic, including authentication and event subscription.
- Enhanced authentication methods in `HostManager` to handle OTP and standard authentication processes.
- Updated `SocketClient` to delegate host-related actions to `HostManager`, streamlining the connection and event handling workflow.
- Improved logging and configuration management during host authentication and updates.
- Removed redundant code from `SocketClient`, enhancing maintainability and clarity.
This commit is contained in:
Tom Butcher 2026-09-01 13:03:57 +01:00
parent d5fe978361
commit d04e722366
2 changed files with 223 additions and 173 deletions

View File

@ -1,5 +1,210 @@
import log4js from "log4js";
import { loadConfig, saveConfig } from "../config.js";
import { getDeviceInfo } from "../utils.js";
import { sendIPC } from "../desktop/notify.js";
const config = loadConfig();
const logger = log4js.getLogger("Host Manager");
logger.level = config.logLevel;
export class HostManager {
constructor(socketClient) {
this.socketClient = socketClient;
this.host = null;
this.id = null;
}
isOtpRequired() {
return (
config.host?.id == undefined ||
config.host?.id == "" ||
config.host?.authCode == undefined ||
config.host?.authCode == ""
);
}
subscribeToObjectEvents() {
const { id } = this;
this.socketClient.subscribeToObjectEvent({
objectType: "host",
_id: id,
eventType: "childUpdate",
});
this.socketClient.subscribeToObjectEvent({
objectType: "host",
_id: id,
eventType: "childDelete",
});
this.socketClient.subscribeToObjectEvent({
objectType: "host",
_id: id,
eventType: "childNew",
});
}
unsubscribeFromObjectEvents() {
if (!this.id) {
return;
}
const { id } = this;
this.socketClient.unsubscribeFromObjectEvent({
objectType: "host",
_id: id,
eventType: "childUpdate",
});
this.socketClient.unsubscribeFromObjectEvent({
objectType: "host",
_id: id,
eventType: "childDelete",
});
this.socketClient.unsubscribeFromObjectEvent({
objectType: "host",
_id: id,
eventType: "childNew",
});
}
async authenticateWithOtp(otp, options = {}) {
const { retryOnFailure = false, logs = true } = options;
if (!otp) {
return { valid: false, error: "OTP is required" };
}
await this.socketClient.waitForConnection();
return this.authenticate({ otp }, { retryOnFailure, logs });
}
async authenticate(authenticationData, options = {}) {
const { logs = true } = options;
await this.socketClient.waitForConnection();
return new Promise((resolve, reject) => {
const { socket } = this.socketClient;
if (!socket?.connected) {
reject(new Error("Socket client is not connected"));
return;
}
if (logs) {
logger.debug("Host authenticating...");
}
socket.emit(
"authenticate",
authenticationData,
async (verifyResult) => {
if (verifyResult.valid == false) {
this.socketClient.authenticated = false;
sendIPC("setAuthenticated", false);
if (logs) {
logger.error("Host not authenticated:", verifyResult.error);
}
resolve({ valid: false, error: verifyResult.error });
return;
}
if (logs) {
logger.info("Host authenticated.");
}
await this.onAuthenticated(verifyResult.host);
resolve({ valid: true, host: this.host });
},
);
});
}
async onAuthenticated(hostData) {
this.socketClient.authenticated = true;
sendIPC("setAuthenticated", true);
sendIPC("setLoading", false);
this.socketClient.loading = false;
if (this.host) {
sendIPC("setHost", {
...this.host,
online: false,
state: { type: "offline" },
});
}
this.host = {
...hostData,
state: { type: "online" },
online: true,
};
sendIPC("setHost", this.host);
this.id = this.host._id;
config.host = { id: this.id, authCode: this.host.authCode };
await saveConfig(config);
this.sendDeviceInfo();
this.subscribeToObjectEvents();
this.socketClient.subscribeToObjectUpdates({
objectType: "host",
_id: this.id,
});
this.socketClient.documentPrinterManager.reloadDocumentPrinters();
this.socketClient.printerManager.reloadPrinters();
await this.socketClient.fileManager.updateFiles();
}
onConnect() {
if (this.isOtpRequired()) {
logger.info("An OTP code is required to setup this host.");
this.socketClient.authenticated = false;
sendIPC("setAuthenticated", false);
return;
}
this.authenticate({
id: config.host.id,
authCode: config.host.authCode,
});
}
handleAction(action, callback) {
switch (action.type) {
case "reloadPrinters":
this.socketClient.printerManager.updatePrinters().catch((error) => {
logger.error("Failed to reload printers:", error);
});
return;
}
callback({ success: true });
}
async handleUpdate(data) {
if (data._id != this.id) {
return;
}
logger.debug("Handling host update for id:", data._id);
const { online, state } = this.host;
this.host = { ...this.host, ...data.object, online, state };
sendIPC("setHost", this.host);
if (data.object?.authCode) {
config.host = { id: this.id, authCode: data.object.authCode };
await saveConfig(config);
}
}
async handleChildEvent(data) {
if (data.parentType == "printer") {
this.socketClient.printerManager.reloadPrinters();
} else if (data.parentType == "documentPrinter") {
this.socketClient.documentPrinterManager.reloadDocumentPrinters();
}
}
sendDeviceInfo() {
logger.debug("Sending device info...");
const deviceInfo = getDeviceInfo();
this.socketClient.socket.emit("updateHost", {
host: { deviceInfo: deviceInfo },
});
}
}

View File

@ -3,8 +3,7 @@ import { WebSocketScanner } from "../network/websocketScanner.js";
import { io } from "socket.io-client";
import axios from "axios";
// Load configuration
import { loadConfig, saveConfig } from "../config.js";
import { getDeviceInfo } from "../utils.js";
import { loadConfig } from "../config.js";
import { sendIPC } from "../desktop/notify.js";
import { PrinterManager } from "../printer/printermanager.js";
import { HostManager } from "../host/hostmanager.js";
@ -23,8 +22,6 @@ export class SocketClient {
this.authenticated = false;
this.connected = false;
this.loading = false;
this.host = null;
this.id = null;
this.reconnectTimeout = null;
this.hostManager = new HostManager(this);
this.fileManager = new FileManager(this);
@ -38,6 +35,14 @@ export class SocketClient {
sendIPC("setLoading", false);
}
get id() {
return this.hostManager.id;
}
get host() {
return this.hostManager.host;
}
setupSocketEventHandlers() {
this.socket.on("connect", this.handleConnect.bind(this));
this.socket.on("connect_error", this.handleError.bind(this));
@ -61,42 +66,6 @@ export class SocketClient {
}, 3000);
}
subscribeToObjectEvents() {
this.subscribeToObjectEvent({
objectType: "host",
_id: this.id,
eventType: "childUpdate",
});
this.subscribeToObjectEvent({
objectType: "host",
_id: this.id,
eventType: "childDelete",
});
this.subscribeToObjectEvent({
objectType: "host",
_id: this.id,
eventType: "childNew",
});
}
unsubscribeFromObjectEvents() {
this.unsubscribeFromObjectEvent({
objectType: "host",
_id: this.id,
eventType: "childUpdate",
});
this.unsubscribeFromObjectEvent({
objectType: "host",
_id: this.id,
eventType: "childDelete",
});
this.unsubscribeFromObjectEvent({
objectType: "host",
_id: this.id,
eventType: "childNew",
});
}
async connect() {
try {
await stopWaiting();
@ -120,7 +89,7 @@ export class SocketClient {
}
disconnect() {
this.unsubscribeFromObjectEvents();
this.hostManager.unsubscribeFromObjectEvents();
this.socket.disconnect();
}
@ -154,109 +123,12 @@ export class SocketClient {
});
}
async authenticateWithOtp(otp, options = {}) {
const { retryOnFailure = false, logs = true } = options;
if (!otp) {
return { valid: false, error: "OTP is required" };
authenticateWithOtp(otp, options) {
return this.hostManager.authenticateWithOtp(otp, options);
}
await this.waitForConnection();
return this.authenticate({ otp }, { retryOnFailure, logs });
}
async authenticate(authenticationData, options = {}) {
const { retryOnFailure = true, logs = true } = options;
await this.waitForConnection();
return new Promise((resolve, reject) => {
if (!this.socket?.connected) {
reject(new Error("Socket client is not connected"));
return;
}
if (logs) {
logger.debug("Host authenticating...");
}
this.socket.emit(
"authenticate",
authenticationData,
async (verifyResult) => {
if (verifyResult.valid == false) {
this.authenticated = false;
sendIPC("setAuthenticated", false);
if (logs) {
logger.error("Host not authenticated:", verifyResult.error);
}
resolve({ valid: false, error: verifyResult.error });
return;
}
if (logs) {
logger.info("Host authenticated.");
}
this.authenticated = true;
sendIPC("setAuthenticated", true);
sendIPC("setLoading", false);
this.loading = false;
if (this.host) {
sendIPC("setHost", {
...this.host,
online: false,
state: { type: "offline" },
});
}
this.host = {
...verifyResult.host,
state: { type: "online" },
online: true,
};
sendIPC("setHost", this.host);
this.id = this.host._id;
config.host = { id: this.id, authCode: this.host.authCode };
await saveConfig(config);
this.sendDeviceInfo();
this.subscribeToObjectEvents();
this.subscribeToObjectUpdates({ objectType: "host", _id: this.id });
this.documentPrinterManager.reloadDocumentPrinters();
this.printerManager.reloadPrinters();
await this.fileManager.updateFiles();
resolve({ valid: true, host: this.host });
},
);
});
}
handleHostAction(action, callback) {
switch (action.type) {
case "reloadPrinters":
this.printerManager.updatePrinters().catch((error) => {
logger.error("Failed to reload printers:", error);
});
return;
}
callback({ success: true });
}
async handleHostUpdate(data) {
if (data._id != this.id) {
return;
}
logger.debug("Handling host update for id:", data._id);
const { online, state } = this.host;
this.host = { ...this.host, ...data.object, online, state };
sendIPC("setHost", this.host);
if (data.object?.authCode) {
config.host = { id: this.id, authCode: data.object.authCode };
await saveConfig(config);
}
authenticate(authenticationData, options) {
return this.hostManager.authenticate(authenticationData, options);
}
handleObjectAction(data, callback) {
@ -266,7 +138,7 @@ export class SocketClient {
const action = data.action;
if (id == this.id && objectType == "host") {
this.handleHostAction(action, callback);
this.hostManager.handleAction(action, callback);
}
if (objectType == "printer") {
@ -290,7 +162,7 @@ export class SocketClient {
data._id,
);
if (data.objectType == "host") {
this.handleHostUpdate(data);
this.hostManager.handleUpdate(data);
}
if (data.objectType == "documentPrinter") {
this.documentPrinterManager.handleDocumentPrinterUpdate(
@ -318,18 +190,7 @@ export class SocketClient {
this.reconnectTimeout = null;
}
const otpRequired =
config.host?.id == undefined ||
config.host?.id == "" ||
config.host?.authCode == undefined ||
config.host?.authCode == "";
if (otpRequired) {
logger.info("An OTP code is required to setup this host.");
this.authenticated = false;
sendIPC("setAuthenticated", false);
return;
}
this.authenticate({ id: config.host.id, authCode: config.host.authCode });
this.hostManager.onConnect();
}
async handleError(error) {
@ -345,14 +206,6 @@ export class SocketClient {
this.scheduleReconnect();
}
sendDeviceInfo() {
logger.debug("Sending device info...");
const deviceInfo = getDeviceInfo();
this.socket.emit("updateHost", {
host: { deviceInfo: deviceInfo },
});
}
async listObjects({
objectType,
populate,
@ -818,14 +671,6 @@ export class SocketClient {
emitUnsubscribe();
}
async handleHostChildUpdate(data) {
if (data.parentType == "printer") {
this.printerManager.reloadPrinters();
} else if (data.parentType == "documentPrinter") {
this.documentPrinterManager.reloadDocumentPrinters();
}
}
async handleObjectEvent({ objectType, _id, event }) {
logger.debug("Received object event...", {
objectType,
@ -853,7 +698,7 @@ export class SocketClient {
objectType == "host" &&
_id == this.id
) {
await this.handleHostChildUpdate(data);
await this.hostManager.handleChildEvent(data);
}
}