Refactor code structure for improved readability and maintainability

This commit is contained in:
jhartworks
2026-09-16 12:41:24 +02:00
parent bfce688f6a
commit 4d8b07fc68
22 changed files with 2670 additions and 96 deletions
+2 -2
View File
@@ -1,9 +1,9 @@
FROM node:20-alpine
FROM node:22-alpine
WORKDIR /app
COPY package*.json ./
RUN npm install
RUN npm ci
COPY src ./src
+3 -2
View File
@@ -5,7 +5,8 @@
"type": "module",
"scripts": {
"dev": "node --watch src/index.js",
"start": "node src/index.js"
"start": "node src/index.js",
"check": "node --check src/index.js && node --check src/db.js"
},
"dependencies": {
"bcryptjs": "^2.4.3",
@@ -16,4 +17,4 @@
"jsonwebtoken": "^9.0.2",
"mysql2": "^3.12.0"
}
}
}
+29 -10
View File
@@ -1,4 +1,4 @@
import mysql from "mysql2/promise";
import mysql from "mysql2/promise";
import { config } from "./config.js";
function createPool(password) {
@@ -24,7 +24,15 @@ function isAccessDenied(error) {
return error?.code === "ER_ACCESS_DENIED_ERROR";
}
async function query(sql, values) {
function isTransientConnectionError(error) {
return ["ECONNREFUSED", "ECONNRESET", "ETIMEDOUT", "PROTOCOL_CONNECTION_LOST"].includes(error?.code);
}
function delay(milliseconds) {
return new Promise((resolve) => setTimeout(resolve, milliseconds));
}
async function queryWithCredentials(sql, values) {
try {
return await activePool.query(sql, values);
} catch (error) {
@@ -33,21 +41,18 @@ async function query(sql, values) {
}
for (const password of candidatePasswords) {
if (password === activePassword) {
continue;
}
if (password === activePassword) continue;
const fallbackPool = createPool(password);
try {
const result = await fallbackPool.query(sql, values);
const previousPool = activePool;
activePool = fallbackPool;
activePassword = password;
previousPool.end().catch(() => undefined);
return result;
} catch (fallbackError) {
await fallbackPool.end().catch(() => undefined);
if (!isAccessDenied(fallbackError)) {
throw fallbackError;
}
if (!isAccessDenied(fallbackError)) throw fallbackError;
}
}
@@ -55,4 +60,18 @@ async function query(sql, values) {
}
}
export const pool = { query };
async function query(sql, values) {
let lastError;
for (let attempt = 0; attempt < 5; attempt += 1) {
try {
return await queryWithCredentials(sql, values);
} catch (error) {
lastError = error;
if (!isTransientConnectionError(error) || attempt === 4) throw error;
await delay(250 * 2 ** attempt);
}
}
throw lastError;
}
export const pool = { query };
+26 -11
View File
@@ -202,18 +202,33 @@ async function ensureRolesSchema() {
}
function sanitizeApiSettings(body = {}) {
function updateSecret(body, existing, field, clearField) {
if (body[clearField] === true) return "";
const replacement = String(body[field] ?? "").trim();
return replacement || String(existing[field] || "").trim();
}
function sanitizeApiSettings(body = {}, existing = {}) {
return {
mailUrl: String(body.mailUrl || "https://mailapi.se-inno.de").trim() || "https://mailapi.se-inno.de",
mailApiKey: String(body.mailApiKey || "").trim(),
whatsappUrl: String(body.whatsappUrl || "https://whatsappapi.se-inno.de").trim() || "https://whatsappapi.se-inno.de",
whatsappApiKey: String(body.whatsappApiKey || "").trim(),
voipUrl: String(body.voipUrl || "https://voipapi.se-inno.de").trim() || "https://voipapi.se-inno.de",
voipApiKey: String(body.voipApiKey || "").trim(),
mailUrl: String(body.mailUrl ?? existing.mailUrl ?? "https://mailapi.se-inno.de").trim() || "https://mailapi.se-inno.de",
mailApiKey: updateSecret(body, existing, "mailApiKey", "clearMailApiKey"),
whatsappUrl: String(body.whatsappUrl ?? existing.whatsappUrl ?? "https://whatsappapi.se-inno.de").trim() || "https://whatsappapi.se-inno.de",
whatsappApiKey: updateSecret(body, existing, "whatsappApiKey", "clearWhatsappApiKey"),
voipUrl: String(body.voipUrl ?? existing.voipUrl ?? "https://voipapi.se-inno.de").trim() || "https://voipapi.se-inno.de",
voipApiKey: updateSecret(body, existing, "voipApiKey", "clearVoipApiKey"),
updatedAt: new Date().toISOString(),
};
}
function publicApiSettings(settings = {}) {
return {
mailUrl: settings.mailUrl || "https://mailapi.se-inno.de",
whatsappUrl: settings.whatsappUrl || "https://whatsappapi.se-inno.de",
voipUrl: settings.voipUrl || "https://voipapi.se-inno.de",
updatedAt: settings.updatedAt || null,
};
}
function normalizeRunnerTime(value) {
const raw = String(value ?? "").trim().replace(/\s*uhr$/i, "").replace(".", ":");
const match = raw.match(/^(\d{1,2}):(\d{2})$/);
@@ -834,9 +849,8 @@ app.post("/api/dashboard/data", authenticateTokenOptional, wrap(async (req, res)
app.get("/api/integrations", authenticateToken, requirePermission("manage_integrations"), wrap(async (_req, res) => {
const current = JSON.parse(await redis.get("integration:api") || "{}");
const settings = sanitizeApiSettings(current);
res.json({
settings,
settings: publicApiSettings(current),
mailConfigured: Boolean(current.mailApiKey),
whatsappConfigured: Boolean(current.whatsappApiKey),
voipConfigured: Boolean(current.voipApiKey),
@@ -844,10 +858,11 @@ app.get("/api/integrations", authenticateToken, requirePermission("manage_integr
}));
app.put("/api/integrations", authenticateToken, requirePermission("manage_integrations"), wrap(async (req, res) => {
const payload = sanitizeApiSettings(req.body || {});
const current = JSON.parse(await redis.get("integration:api") || "{}");
const payload = sanitizeApiSettings(req.body || {}, current);
await redis.set("integration:api", JSON.stringify(payload));
res.json({
settings: payload,
settings: publicApiSettings(payload),
mailConfigured: Boolean(payload.mailApiKey),
whatsappConfigured: Boolean(payload.whatsappApiKey),
voipConfigured: Boolean(payload.voipApiKey),