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",
"host": {
"id": "691a1db49ce913faf0e51284",
"authCode": "9uu3DC0si__-F9FnGWTKudle5z6yZasFMlKohnShElPekRYteh-LlZaksHOXFfOO"
"authCode": "BNj13heKj9zUZIJ0IodSU-V-OSZHNJGAA-nLa4nwFrDHa6N1wah7mpw_7pqbT9Il"
}
},
"production": {

View File

@ -1,7 +1,9 @@
import axios from "axios";
import { loadConfig } from "./config.js";
import log4js from "log4js";
import { createElectronWindow } from "./electron/window.js";
import { setupIPC } from "./electron/ipc.js";
import { LOCAL_SERVER_PORT, LocalServer } from "./localserver/localserver.js";
import { SocketClient } from "./socket/socketclient.js";
// Load configuration
@ -12,7 +14,89 @@ logger.level = config.logLevel;
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() {
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) {
// Create Electron window first
logger.info("Creating electron window...");
@ -31,13 +115,21 @@ export async function init() {
const socketClient = new SocketClient();
// Make socket client globally accessible for IPC handlers
global.socketClient = socketClient;
const localServer = new LocalServer(socketClient);
await localServer.start();
socketClient.connect();
process.on("SIGINT", () => {
logger.info("Shutting down...");
socketClient.disconnect();
localServer.stop();
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();
}
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) {
otp = await askOtp();
}
await this.authenticate({ otp: otp });
return;
await this.waitForConnection();
return this.authenticate({ otp }, { retryOnFailure });
}
async authenticate(authenticationData) {
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);
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;
async authenticate(authenticationData, options = {}) {
const { retryOnFailure = true } = options;
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();
await this.waitForConnection();
return new Promise((resolve, reject) => {
if (!this.socket?.connected) {
reject(new Error("Socket client is not connected"));
return;
}
);
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) {
@ -194,7 +254,7 @@ export class SocketClient {
this.documentPrinterManager.handleDocumentPrinterAction(
id,
action,
callback
callback,
);
}
}
@ -204,7 +264,7 @@ export class SocketClient {
"Got object update for type:",
data.objectType,
" id:",
data._id
data._id,
);
if (data.objectType == "host") {
this.handleHostAction(action, callback);
@ -213,7 +273,7 @@ export class SocketClient {
this.documentPrinterManager.handleDocumentPrinterUpdate(
data._id,
data,
callback
callback,
);
}
if (data.objectType == "printer") {
@ -315,7 +375,7 @@ export class SocketClient {
});
resolve(result);
}
}
},
);
});
}
@ -342,7 +402,7 @@ export class SocketClient {
});
resolve(result);
}
}
},
);
});
}
@ -374,7 +434,7 @@ export class SocketClient {
});
resolve(result);
}
}
},
);
});
}
@ -404,7 +464,7 @@ export class SocketClient {
});
resolve(result);
}
}
},
);
});
}
@ -537,7 +597,7 @@ export class SocketClient {
// Listen for found services
this.scanner.on("serviceFound", (data) => {
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);
});
@ -545,7 +605,7 @@ export class SocketClient {
// Listen for scan progress
this.scanner.on("scanProgress", ({ currentIP, progress }) => {
logger.info(
`Scanning ${currentIP} (${progress.toFixed(2)}% complete)`
`Scanning ${currentIP} (${progress.toFixed(2)}% complete)`,
);
this.socket.emit("notify_scan_network_progress", {
currentIP: currentIP,
@ -558,7 +618,7 @@ export class SocketClient {
"Scanning network for websocket services on port:",
data?.port || 7125,
"using protocol:",
data?.protocol || "ws"
data?.protocol || "ws",
);
this.scanner
.scanNetwork(data?.port || 7125, data?.protocol || "ws")
@ -641,7 +701,7 @@ export class SocketClient {
// Get the printer client
const printerClient = this.printerManager.getPrinterClient(
data.printerId
data.printerId,
);
if (!printerClient) {
throw new Error(`Printer with ID ${data.printerId} not found`);
@ -649,7 +709,7 @@ export class SocketClient {
// Load the filament stock
const result = await printerClient.loadFilamentStock(
data.filamentStockId
data.filamentStockId,
);
if (callback) {