Implement local server for OTP authentication and enhance socket client functionality
All checks were successful
farmcontrol/farmcontrol-server/pipeline/head This commit looks good

Add a LocalServer class to handle OTP authentication requests and server status checks. Update index.js to integrate local server functionality, including checks for server status and OTP authentication flow. Enhance SocketClient with connection waiting and improved OTP authentication handling. Update config.json with a new authCode for the host.
This commit is contained in:
Tom Butcher 2026-07-26 22:48:02 +01:00
parent 55b4c1a42e
commit 2e95075d3e
4 changed files with 278 additions and 50 deletions

View File

@ -5,7 +5,7 @@
"apiUrl": "https://dev.tombutcher.work/api", "apiUrl": "https://dev.tombutcher.work/api",
"host": { "host": {
"id": "691a1db49ce913faf0e51284", "id": "691a1db49ce913faf0e51284",
"authCode": "9uu3DC0si__-F9FnGWTKudle5z6yZasFMlKohnShElPekRYteh-LlZaksHOXFfOO" "authCode": "BNj13heKj9zUZIJ0IodSU-V-OSZHNJGAA-nLa4nwFrDHa6N1wah7mpw_7pqbT9Il"
} }
}, },
"production": { "production": {

View File

@ -1,7 +1,9 @@
import axios from "axios";
import { loadConfig } from "./config.js"; import { loadConfig } from "./config.js";
import log4js from "log4js"; import log4js from "log4js";
import { createElectronWindow } from "./electron/window.js"; import { createElectronWindow } from "./electron/window.js";
import { setupIPC } from "./electron/ipc.js"; import { setupIPC } from "./electron/ipc.js";
import { LOCAL_SERVER_PORT, LocalServer } from "./localserver/localserver.js";
import { SocketClient } from "./socket/socketclient.js"; import { SocketClient } from "./socket/socketclient.js";
// Load configuration // Load configuration
@ -12,7 +14,89 @@ logger.level = config.logLevel;
const isHeadless = process.argv.includes("--headless"); const isHeadless = process.argv.includes("--headless");
function getArgValue(flag) {
const withEquals = process.argv.find((arg) => arg.startsWith(`${flag}=`));
if (withEquals) {
return withEquals.slice(flag.length + 1);
}
const index = process.argv.indexOf(flag);
if (index !== -1 && index + 1 < process.argv.length) {
return process.argv[index + 1];
}
return undefined;
}
const otpCode = getArgValue("--otp") || undefined;
async function checkRunning() {
try {
const response = await axios.get(
`http://127.0.0.1:${LOCAL_SERVER_PORT}/info`,
{ timeout: 1000 },
);
if (response.data?.running === true) {
return response.data;
}
} catch (err) {
return false;
}
}
async function authenticateWithOtp(otpCode) {
try {
const response = await axios.post(
`http://127.0.0.1:${LOCAL_SERVER_PORT}/otpAuth`,
{ otp: otpCode },
{ timeout: 30000 },
);
return response.data;
} catch (err) {
if (err.response?.data) {
return err.response.data;
}
throw new Error("Failed to authenticate with OTP");
}
}
export async function init() { export async function init() {
logger.info("⌛ Checking if Farm Control Server is running...");
const runningServer = await checkRunning();
if (otpCode != undefined) {
logger.info("✔ Farm Control Server is running.");
}
if (otpCode != undefined) {
logger.info("🔒 Authenticating with OTP...");
const authenticatedServer = await authenticateWithOtp(otpCode);
if (authenticatedServer.valid == false) {
logger.error(
"✘ Failed to authenticate with OTP -",
authenticatedServer.error,
);
} else {
logger.info("✔ Authenticated with OTP.");
logger.info("----------Host----------");
logger.info("Name:", authenticatedServer.host?.name || "Unknown");
logger.info("Tags: -", authenticatedServer.host?.tags?.[0] || "n/a");
for (var i = 0; i < authenticatedServer.host?.tags?.length - 1; i++) {
const tag = authenticatedServer.host?.tags[i + 1];
logger.info(" -", tag);
}
logger.info("State:", "Online");
logger.info("------------------------");
return authenticatedServer;
}
return authenticatedServer;
}
if (runningServer != false) {
logger.warn("✘ Farm Control Server is already running.");
return runningServer;
}
if (!isHeadless) { if (!isHeadless) {
// Create Electron window first // Create Electron window first
logger.info("Creating electron window..."); logger.info("Creating electron window...");
@ -31,13 +115,21 @@ export async function init() {
const socketClient = new SocketClient(); const socketClient = new SocketClient();
// Make socket client globally accessible for IPC handlers // Make socket client globally accessible for IPC handlers
global.socketClient = socketClient; global.socketClient = socketClient;
const localServer = new LocalServer(socketClient);
await localServer.start();
socketClient.connect(); socketClient.connect();
process.on("SIGINT", () => { process.on("SIGINT", () => {
logger.info("Shutting down..."); logger.info("Shutting down...");
socketClient.disconnect(); socketClient.disconnect();
localServer.stop();
process.exit(0); process.exit(0);
}); });
} }
init(); init().catch((err) => {
logger.error(err.message);
process.exit(1);
});

View File

@ -0,0 +1,76 @@
import express from "express";
import log4js from "log4js";
import { notPrompting } from "../utils.js";
import { loadConfig } from "../config.js";
export const LOCAL_SERVER_PORT = 47291;
const config = loadConfig();
const logger = log4js.getLogger("Local Server");
logger.level = config.logLevel;
export class LocalServer {
constructor(socketClient) {
this.socketClient = socketClient;
this.server = null;
this.app = express();
this.app.use(express.json());
this.setupRoutes();
}
setupRoutes() {
this.app.get("/info", (_req, res) => {
logger.debug("Received request to check if server is running");
res.json({ running: true });
});
this.app.post("/otpAuth", async (req, res) => {
logger.debug("Received request to authenticate with OTP");
const { otp } = req.body;
if (!otp) {
res.status(400).json({ error: "otp is required" });
return;
}
try {
logger.info("Authenticating with OTP via local server...");
notPrompting();
const result = await this.socketClient.authenticateWithOtp(otp, {
retryOnFailure: false,
});
if (!result.valid) {
res.status(401).json(result);
return;
}
res.json(result);
} catch (err) {
logger.error("OTP authentication failed:", err);
res.status(500).json({ error: err.message });
}
});
}
start() {
return new Promise((resolve, reject) => {
this.server = this.app.listen(LOCAL_SERVER_PORT, "127.0.0.1", () => {
logger.info(
`Local server listening on http://127.0.0.1:${LOCAL_SERVER_PORT}`,
);
resolve(this.server);
});
this.server.on("error", reject);
});
}
stop() {
if (this.server) {
this.server.close();
this.server = null;
}
}
}

View File

@ -119,49 +119,109 @@ export class SocketClient {
this.socket.disconnect(); this.socket.disconnect();
} }
async authenticateWithOtp(otp = undefined) { async waitForConnection(timeoutMs = 10000) {
if (this.socket?.connected) {
return;
}
if (!this.socket) {
throw new Error("Socket client is not connected");
}
return new Promise((resolve, reject) => {
const timeout = setTimeout(() => {
this.socket.off("connect", onConnect);
reject(new Error("Socket connection timed out"));
}, timeoutMs);
const onConnect = () => {
clearTimeout(timeout);
resolve();
};
if (this.socket.connected) {
clearTimeout(timeout);
resolve();
return;
}
this.socket.once("connect", onConnect);
});
}
async authenticateWithOtp(otp = undefined, options = {}) {
const { retryOnFailure = otp == undefined } = options;
if (otp == undefined) { if (otp == undefined) {
otp = await askOtp(); otp = await askOtp();
} }
await this.authenticate({ otp: otp });
return; await this.waitForConnection();
return this.authenticate({ otp }, { retryOnFailure });
} }
async authenticate(authenticationData) { async authenticate(authenticationData, options = {}) {
logger.debug("Host authenticating..."); const { retryOnFailure = true } = options;
this.socket.emit(
"authenticate",
authenticationData,
async (verifyResult) => {
if (verifyResult.valid == false) {
this.authenticated = false;
sendIPC("setAuthenticated", false);
logger.error("Host not authenticated:", verifyResult.error);
await this.authenticateWithOtp();
return;
}
logger.info("Host authenticated.");
this.authenticated = true;
sendIPC("setAuthenticated", true);
sendIPC("setLoading", false);
this.loading = false;
this.host = verifyResult.host;
sendIPC("setHost", {
...this.host,
online: true,
state: { type: "online" },
});
this.id = this.host._id;
config.host = { id: this.id, authCode: this.host.authCode }; await this.waitForConnection();
saveConfig(config);
this.sendDeviceInfo(); return new Promise((resolve, reject) => {
this.subscribeToObjectEvents(); if (!this.socket?.connected) {
this.documentPrinterManager.reloadDocumentPrinters(); reject(new Error("Socket client is not connected"));
this.printerManager.reloadPrinters(); return;
await this.fileManager.updateFiles();
} }
);
logger.debug("Host authenticating...");
this.socket.emit(
"authenticate",
authenticationData,
async (verifyResult) => {
if (verifyResult.valid == false) {
this.authenticated = false;
sendIPC("setAuthenticated", false);
logger.error("Host not authenticated:", verifyResult.error);
if (retryOnFailure) {
const retryResult = await this.authenticateWithOtp();
resolve(retryResult);
return;
}
resolve({ valid: false, error: verifyResult.error });
return;
}
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;
sendIPC("setHost", {
...this.host,
online: true,
state: { type: "online" },
});
this.id = this.host._id;
config.host = { id: this.id, authCode: this.host.authCode };
saveConfig(config);
this.sendDeviceInfo();
this.subscribeToObjectEvents();
this.documentPrinterManager.reloadDocumentPrinters();
this.printerManager.reloadPrinters();
await this.fileManager.updateFiles();
resolve({ valid: true, host: this.host });
},
);
});
} }
handleHostAction(action, callback) { handleHostAction(action, callback) {
@ -194,7 +254,7 @@ export class SocketClient {
this.documentPrinterManager.handleDocumentPrinterAction( this.documentPrinterManager.handleDocumentPrinterAction(
id, id,
action, action,
callback callback,
); );
} }
} }
@ -204,7 +264,7 @@ export class SocketClient {
"Got object update for type:", "Got object update for type:",
data.objectType, data.objectType,
" id:", " id:",
data._id data._id,
); );
if (data.objectType == "host") { if (data.objectType == "host") {
this.handleHostAction(action, callback); this.handleHostAction(action, callback);
@ -213,7 +273,7 @@ export class SocketClient {
this.documentPrinterManager.handleDocumentPrinterUpdate( this.documentPrinterManager.handleDocumentPrinterUpdate(
data._id, data._id,
data, data,
callback callback,
); );
} }
if (data.objectType == "printer") { if (data.objectType == "printer") {
@ -315,7 +375,7 @@ export class SocketClient {
}); });
resolve(result); resolve(result);
} }
} },
); );
}); });
} }
@ -342,7 +402,7 @@ export class SocketClient {
}); });
resolve(result); resolve(result);
} }
} },
); );
}); });
} }
@ -374,7 +434,7 @@ export class SocketClient {
}); });
resolve(result); resolve(result);
} }
} },
); );
}); });
} }
@ -404,7 +464,7 @@ export class SocketClient {
}); });
resolve(result); resolve(result);
} }
} },
); );
}); });
} }
@ -537,7 +597,7 @@ export class SocketClient {
// Listen for found services // Listen for found services
this.scanner.on("serviceFound", (data) => { this.scanner.on("serviceFound", (data) => {
logger.info( logger.info(
`Found websocket service at ${data.hostname} (${data.ip})` `Found websocket service at ${data.hostname} (${data.ip})`,
); );
this.socket.emit("notify_scan_network_found", data); this.socket.emit("notify_scan_network_found", data);
}); });
@ -545,7 +605,7 @@ export class SocketClient {
// Listen for scan progress // Listen for scan progress
this.scanner.on("scanProgress", ({ currentIP, progress }) => { this.scanner.on("scanProgress", ({ currentIP, progress }) => {
logger.info( logger.info(
`Scanning ${currentIP} (${progress.toFixed(2)}% complete)` `Scanning ${currentIP} (${progress.toFixed(2)}% complete)`,
); );
this.socket.emit("notify_scan_network_progress", { this.socket.emit("notify_scan_network_progress", {
currentIP: currentIP, currentIP: currentIP,
@ -558,7 +618,7 @@ export class SocketClient {
"Scanning network for websocket services on port:", "Scanning network for websocket services on port:",
data?.port || 7125, data?.port || 7125,
"using protocol:", "using protocol:",
data?.protocol || "ws" data?.protocol || "ws",
); );
this.scanner this.scanner
.scanNetwork(data?.port || 7125, data?.protocol || "ws") .scanNetwork(data?.port || 7125, data?.protocol || "ws")
@ -641,7 +701,7 @@ export class SocketClient {
// Get the printer client // Get the printer client
const printerClient = this.printerManager.getPrinterClient( const printerClient = this.printerManager.getPrinterClient(
data.printerId data.printerId,
); );
if (!printerClient) { if (!printerClient) {
throw new Error(`Printer with ID ${data.printerId} not found`); throw new Error(`Printer with ID ${data.printerId} not found`);
@ -649,7 +709,7 @@ export class SocketClient {
// Load the filament stock // Load the filament stock
const result = await printerClient.loadFilamentStock( const result = await printerClient.loadFilamentStock(
data.filamentStockId data.filamentStockId,
); );
if (callback) { if (callback) {