From 9cb5cd380bcf2f77565edd85fde679a0a628120f Mon Sep 17 00:00:00 2001 From: tigattack <10629864+tigattack@users.noreply.github.com> Date: Mon, 22 Sep 2025 01:33:27 +0100 Subject: [PATCH] feat(backend): add settings service with env var handling New features: * Settings initialised on startup rather than first request * Settings are cached in memory for performance * Reduction of code duplication/defensive coding practices --- backend/src/routes/settingsRoutes.js | 132 +++++------------- backend/src/server.js | 14 ++ backend/src/services/settingsService.js | 170 ++++++++++++++++++++++++ 3 files changed, 217 insertions(+), 99 deletions(-) create mode 100644 backend/src/services/settingsService.js diff --git a/backend/src/routes/settingsRoutes.js b/backend/src/routes/settingsRoutes.js index 89674c5..a34eee4 100644 --- a/backend/src/routes/settingsRoutes.js +++ b/backend/src/routes/settingsRoutes.js @@ -1,9 +1,9 @@ const express = require('express'); const { body, validationResult } = require('express-validator'); const { PrismaClient } = require('@prisma/client'); -const { v4: uuidv4 } = require('uuid'); const { authenticateToken } = require('../middleware/auth'); const { requireManageSettings } = require('../middleware/permissions'); +const { getSettings, updateSettings } = require('../services/settingsService'); const router = express.Router(); const prisma = new PrismaClient(); @@ -13,6 +13,10 @@ async function triggerCrontabUpdates() { try { console.log('Triggering crontab updates on all hosts with auto-update enabled...'); + // Get current settings for server URL + const settings = await getSettings(); + const serverUrl = settings.server_url; + // Get all hosts that have auto-update enabled const hosts = await prisma.hosts.findMany({ where: { @@ -40,10 +44,6 @@ async function triggerCrontabUpdates() { const http = require('http'); const https = require('https'); - const settings = await prisma.settings.findFirst({ - orderBy: { updated_at: 'desc' } - }); - const serverUrl = settings?.server_url || process.env.SERVER_URL || 'http://localhost:3001'; const url = new URL(`${serverUrl}/api/v1/hosts/ping`); const isHttps = url.protocol === 'https:'; const client = isHttps ? https : http; @@ -94,27 +94,8 @@ async function triggerCrontabUpdates() { // Get current settings router.get('/', authenticateToken, requireManageSettings, async (req, res) => { try { - let settings = await prisma.settings.findFirst({ - orderBy: { updated_at: 'desc' } - }); - - // If no settings exist, create default settings - if (!settings) { - settings = await prisma.settings.create({ - data: { - id: uuidv4(), - server_url: 'http://localhost:3001', - server_protocol: 'http', - server_host: 'localhost', - server_port: 3001, - update_interval: 60, - auto_update: false, - signup_enabled: false, - updated_at: new Date() - } - }); - } - + const settings = await getSettings(); + console.log('Returning settings:', settings); res.json(settings); } catch (error) { console.error('Settings fetch error:', error); @@ -151,61 +132,34 @@ router.put('/', authenticateToken, requireManageSettings, [ const { serverProtocol, serverHost, serverPort, updateInterval, autoUpdate, signupEnabled, githubRepoUrl, repositoryType, sshKeyPath } = req.body; - // Construct server URL from components - const serverUrl = `${serverProtocol}://${serverHost}:${serverPort}`; + // Get current settings to check for update interval changes + const currentSettings = await getSettings(); + const oldUpdateInterval = currentSettings.update_interval; - let settings = await prisma.settings.findFirst({ - orderBy: { updated_at: 'desc' } + // Update settings using the service + const updatedSettings = await updateSettings(currentSettings.id, { + server_protocol: serverProtocol, + server_host: serverHost, + server_port: serverPort, + update_interval: updateInterval || 60, + auto_update: autoUpdate || false, + signup_enabled: signupEnabled || false, + github_repo_url: githubRepoUrl !== undefined ? githubRepoUrl : 'git@github.com:9technologygroup/patchmon.net.git', + repository_type: repositoryType || 'public', + ssh_key_path: sshKeyPath || null, }); - if (settings) { - // Update existing settings - const oldUpdateInterval = settings.update_interval; + console.log('Settings updated successfully:', updatedSettings); - settings = await prisma.settings.update({ - where: { id: settings.id }, - data: { - server_url: serverUrl, - server_protocol: serverProtocol, - server_host: serverHost, - server_port: serverPort, - update_interval: updateInterval || 60, - auto_update: autoUpdate || false, - signup_enabled: signupEnabled || false, - github_repo_url: githubRepoUrl !== undefined ? githubRepoUrl : 'git@github.com:9technologygroup/patchmon.net.git', - repository_type: repositoryType || 'public', - ssh_key_path: sshKeyPath || null, - updated_at: new Date() - } - }); - // If update interval changed, trigger crontab updates on all hosts with auto-update enabled - if (oldUpdateInterval !== (updateInterval || 60)) { - console.log(`Update interval changed from ${oldUpdateInterval} to ${updateInterval || 60} minutes. Triggering crontab updates...`); - await triggerCrontabUpdates(); - } - } else { - // Create new settings - settings = await prisma.settings.create({ - data: { - id: uuidv4(), - server_url: serverUrl, - server_protocol: serverProtocol, - server_host: serverHost, - server_port: serverPort, - update_interval: updateInterval || 60, - auto_update: autoUpdate || false, - signup_enabled: signupEnabled || false, - github_repo_url: githubRepoUrl !== undefined ? githubRepoUrl : 'git@github.com:9technologygroup/patchmon.net.git', - repository_type: repositoryType || 'public', - ssh_key_path: sshKeyPath || null, - updated_at: new Date() - } - }); + // If update interval changed, trigger crontab updates on all hosts with auto-update enabled + if (oldUpdateInterval !== (updateInterval || 60)) { + console.log(`Update interval changed from ${oldUpdateInterval} to ${updateInterval || 60} minutes. Triggering crontab updates...`); + await triggerCrontabUpdates(); } res.json({ message: 'Settings updated successfully', - settings + settings: updatedSettings }); } catch (error) { console.error('Settings update error:', error); @@ -216,32 +170,19 @@ router.put('/', authenticateToken, requireManageSettings, [ // Get server URL for public use (used by installation scripts) router.get('/server-url', async (req, res) => { try { - const settings = await prisma.settings.findFirst({ - orderBy: { updated_at: 'desc' } - }); - - if (!settings) { - return res.json({ server_url: 'http://localhost:3001' }); - } - - res.json({ server_url: settings.server_url }); + const settings = await getSettings(); + const serverUrl = settings.server_url; + res.json({ server_url: serverUrl }); } catch (error) { console.error('Server URL fetch error:', error); - res.json({ server_url: 'http://localhost:3001' }); + res.status(500).json({ error: 'Failed to fetch server URL' }); } }); // Get update interval policy for agents (public endpoint) router.get('/update-interval', async (req, res) => { try { - const settings = await prisma.settings.findFirst({ - orderBy: { updated_at: 'desc' } - }); - - if (!settings) { - return res.json({ updateInterval: 60 }); - } - + const settings = await getSettings(); res.json({ updateInterval: settings.update_interval, cronExpression: `*/${settings.update_interval} * * * *` // Generate cron expression @@ -255,14 +196,7 @@ router.get('/update-interval', async (req, res) => { // Get auto-update policy for agents (public endpoint) router.get('/auto-update', async (req, res) => { try { - const settings = await prisma.settings.findFirst({ - orderBy: { updated_at: 'desc' } - }); - - if (!settings) { - return res.json({ autoUpdate: false }); - } - + const settings = await getSettings(); res.json({ autoUpdate: settings.auto_update || false }); diff --git a/backend/src/server.js b/backend/src/server.js index 5beb873..94d7643 100644 --- a/backend/src/server.js +++ b/backend/src/server.js @@ -19,6 +19,7 @@ const repositoryRoutes = require('./routes/repositoryRoutes'); const versionRoutes = require('./routes/versionRoutes'); const tfaRoutes = require('./routes/tfaRoutes'); const updateScheduler = require('./services/updateScheduler'); +const { initSettings } = require('./services/settingsService'); // Initialize Prisma client with optimized connection pooling for multiple instances const prisma = createPrismaClient(); @@ -374,6 +375,19 @@ async function startServer() { logger.info('✅ Database connection successful'); } + // Initialise settings from environment variables on startup + try { + await initSettings(); + if (process.env.ENABLE_LOGGING === 'true') { + logger.info('✅ Settings initialised from environment variables'); + } + } catch (initError) { + if (process.env.ENABLE_LOGGING === 'true') { + logger.error('❌ Failed to initialise settings:', initError.message); + } + throw initError; // Fail startup if settings can't be initialised + } + // Check and import agent version on startup await checkAndImportAgentVersion(); diff --git a/backend/src/services/settingsService.js b/backend/src/services/settingsService.js new file mode 100644 index 0000000..0460628 --- /dev/null +++ b/backend/src/services/settingsService.js @@ -0,0 +1,170 @@ +const { PrismaClient } = require('@prisma/client'); +const { v4: uuidv4 } = require('uuid'); + +const prisma = new PrismaClient(); + +// Cached settings instance +let cachedSettings = null; + +// Environment variable to settings field mapping +const ENV_TO_SETTINGS_MAP = { + 'SERVER_PROTOCOL': 'server_protocol', + 'SERVER_HOST': 'server_host', + 'SERVER_PORT': 'server_port', +}; + +// Create settings from environment variables and/or defaults +async function createSettingsFromEnvironment() { + const protocol = process.env.SERVER_PROTOCOL || 'http'; + const host = process.env.SERVER_HOST || 'localhost'; + const port = parseInt(process.env.SERVER_PORT, 10) || 3001; + const serverUrl = `${protocol}://${host}:${port}`.toLowerCase(); + + const settings = await prisma.settings.create({ + data: { + id: uuidv4(), + server_url: serverUrl, + server_protocol: protocol, + server_host: host, + server_port: port, + update_interval: 60, + auto_update: false, + signup_enabled: false, + updated_at: new Date() + } + }); + + console.log('Created settings'); + return settings; +} + +// Sync environment variables with existing settings +async function syncEnvironmentToSettings(currentSettings) { + const updates = {}; + let hasChanges = false; + + // Check each environment variable mapping + for (const [envVar, settingsField] of Object.entries(ENV_TO_SETTINGS_MAP)) { + if (process.env[envVar]) { + const envValue = process.env[envVar]; + const currentValue = currentSettings[settingsField]; + + // Convert environment value to appropriate type + let convertedValue = envValue; + if (settingsField === 'server_port') { + convertedValue = parseInt(envValue, 10); + } + + // Only update if values differ + if (currentValue !== convertedValue) { + updates[settingsField] = convertedValue; + hasChanges = true; + console.log(`Environment variable ${envVar} (${envValue}) differs from settings ${settingsField} (${currentValue}), updating...`); + } + } + } + + // Construct server_url from components if any components were updated + const protocol = updates.server_protocol || currentSettings.server_protocol; + const host = updates.server_host || currentSettings.server_host; + const port = updates.server_port || currentSettings.server_port; + const constructedServerUrl = `${protocol}://${host}:${port}`.toLowerCase(); + + // Update server_url if it differs from the constructed value + if (currentSettings.server_url !== constructedServerUrl) { + updates.server_url = constructedServerUrl; + hasChanges = true; + console.log(`Updating server_url to: ${constructedServerUrl}`); + } + + // Update settings if there are changes + if (hasChanges) { + const updatedSettings = await prisma.settings.update({ + where: { id: currentSettings.id }, + data: { + ...updates, + updated_at: new Date() + } + }); + console.log(`Synced ${Object.keys(updates).length} environment variables to settings`); + return updatedSettings; + } + + return currentSettings; +} + +// Initialise settings - create from environment or sync existing +async function initSettings() { + if (cachedSettings) { + return cachedSettings; + } + + try { + let settings = await prisma.settings.findFirst({ + orderBy: { updated_at: 'desc' } + }); + + if (!settings) { + // No settings exist, create from environment variables and defaults + settings = await createSettingsFromEnvironment(); + } else { + // Settings exist, sync with environment variables + settings = await syncEnvironmentToSettings(settings); + } + + // Cache the initialised settings + cachedSettings = settings; + return settings; + } catch (error) { + console.error('Failed to initialise settings:', error); + throw error; + } +} + +// Get current settings (returns cached if available) +async function getSettings() { + return cachedSettings || await initSettings(); +} + +// Update settings and refresh cache +async function updateSettings(id, updateData) { + try { + const updatedSettings = await prisma.settings.update({ + where: { id }, + data: { + ...updateData, + updated_at: new Date() + } + }); + + // Reconstruct server_url from components + const serverUrl = `${updatedSettings.server_protocol}://${updatedSettings.server_host}:${updatedSettings.server_port}`.toLowerCase(); + if (updatedSettings.server_url !== serverUrl) { + updatedSettings.server_url = serverUrl; + await prisma.settings.update({ + where: { id }, + data: { server_url: serverUrl } + }); + } + + // Update cache + cachedSettings = updatedSettings; + return updatedSettings; + } catch (error) { + console.error('Failed to update settings:', error); + throw error; + } +} + +// Invalidate cache (useful for testing or manual refresh) +function invalidateCache() { + cachedSettings = null; +} + +module.exports = { + initSettings, + getSettings, + updateSettings, + invalidateCache, + syncEnvironmentToSettings // Export for startup use +};