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
This commit is contained in:
tigattack
2025-09-22 01:33:27 +01:00
parent 517b5cd7cb
commit 9cb5cd380b
3 changed files with 217 additions and 99 deletions

View File

@@ -1,9 +1,9 @@
const express = require('express'); const express = require('express');
const { body, validationResult } = require('express-validator'); const { body, validationResult } = require('express-validator');
const { PrismaClient } = require('@prisma/client'); const { PrismaClient } = require('@prisma/client');
const { v4: uuidv4 } = require('uuid');
const { authenticateToken } = require('../middleware/auth'); const { authenticateToken } = require('../middleware/auth');
const { requireManageSettings } = require('../middleware/permissions'); const { requireManageSettings } = require('../middleware/permissions');
const { getSettings, updateSettings } = require('../services/settingsService');
const router = express.Router(); const router = express.Router();
const prisma = new PrismaClient(); const prisma = new PrismaClient();
@@ -13,6 +13,10 @@ async function triggerCrontabUpdates() {
try { try {
console.log('Triggering crontab updates on all hosts with auto-update enabled...'); 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 // Get all hosts that have auto-update enabled
const hosts = await prisma.hosts.findMany({ const hosts = await prisma.hosts.findMany({
where: { where: {
@@ -40,10 +44,6 @@ async function triggerCrontabUpdates() {
const http = require('http'); const http = require('http');
const https = require('https'); 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 url = new URL(`${serverUrl}/api/v1/hosts/ping`);
const isHttps = url.protocol === 'https:'; const isHttps = url.protocol === 'https:';
const client = isHttps ? https : http; const client = isHttps ? https : http;
@@ -94,27 +94,8 @@ async function triggerCrontabUpdates() {
// Get current settings // Get current settings
router.get('/', authenticateToken, requireManageSettings, async (req, res) => { router.get('/', authenticateToken, requireManageSettings, async (req, res) => {
try { try {
let settings = await prisma.settings.findFirst({ const settings = await getSettings();
orderBy: { updated_at: 'desc' } console.log('Returning settings:', settings);
});
// 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()
}
});
}
res.json(settings); res.json(settings);
} catch (error) { } catch (error) {
console.error('Settings fetch error:', error); console.error('Settings fetch error:', error);
@@ -151,21 +132,12 @@ router.put('/', authenticateToken, requireManageSettings, [
const { serverProtocol, serverHost, serverPort, updateInterval, autoUpdate, signupEnabled, githubRepoUrl, repositoryType, sshKeyPath } = req.body; const { serverProtocol, serverHost, serverPort, updateInterval, autoUpdate, signupEnabled, githubRepoUrl, repositoryType, sshKeyPath } = req.body;
// Construct server URL from components // Get current settings to check for update interval changes
const serverUrl = `${serverProtocol}://${serverHost}:${serverPort}`; const currentSettings = await getSettings();
const oldUpdateInterval = currentSettings.update_interval;
let settings = await prisma.settings.findFirst({ // Update settings using the service
orderBy: { updated_at: 'desc' } const updatedSettings = await updateSettings(currentSettings.id, {
});
if (settings) {
// Update existing settings
const oldUpdateInterval = settings.update_interval;
settings = await prisma.settings.update({
where: { id: settings.id },
data: {
server_url: serverUrl,
server_protocol: serverProtocol, server_protocol: serverProtocol,
server_host: serverHost, server_host: serverHost,
server_port: serverPort, server_port: serverPort,
@@ -175,37 +147,19 @@ router.put('/', authenticateToken, requireManageSettings, [
github_repo_url: githubRepoUrl !== undefined ? githubRepoUrl : 'git@github.com:9technologygroup/patchmon.net.git', github_repo_url: githubRepoUrl !== undefined ? githubRepoUrl : 'git@github.com:9technologygroup/patchmon.net.git',
repository_type: repositoryType || 'public', repository_type: repositoryType || 'public',
ssh_key_path: sshKeyPath || null, ssh_key_path: sshKeyPath || null,
updated_at: new Date()
}
}); });
console.log('Settings updated successfully:', updatedSettings);
// If update interval changed, trigger crontab updates on all hosts with auto-update enabled // If update interval changed, trigger crontab updates on all hosts with auto-update enabled
if (oldUpdateInterval !== (updateInterval || 60)) { if (oldUpdateInterval !== (updateInterval || 60)) {
console.log(`Update interval changed from ${oldUpdateInterval} to ${updateInterval || 60} minutes. Triggering crontab updates...`); console.log(`Update interval changed from ${oldUpdateInterval} to ${updateInterval || 60} minutes. Triggering crontab updates...`);
await triggerCrontabUpdates(); 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()
}
});
}
res.json({ res.json({
message: 'Settings updated successfully', message: 'Settings updated successfully',
settings settings: updatedSettings
}); });
} catch (error) { } catch (error) {
console.error('Settings update error:', 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) // Get server URL for public use (used by installation scripts)
router.get('/server-url', async (req, res) => { router.get('/server-url', async (req, res) => {
try { try {
const settings = await prisma.settings.findFirst({ const settings = await getSettings();
orderBy: { updated_at: 'desc' } const serverUrl = settings.server_url;
}); res.json({ server_url: serverUrl });
if (!settings) {
return res.json({ server_url: 'http://localhost:3001' });
}
res.json({ server_url: settings.server_url });
} catch (error) { } catch (error) {
console.error('Server URL fetch error:', 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) // Get update interval policy for agents (public endpoint)
router.get('/update-interval', async (req, res) => { router.get('/update-interval', async (req, res) => {
try { try {
const settings = await prisma.settings.findFirst({ const settings = await getSettings();
orderBy: { updated_at: 'desc' }
});
if (!settings) {
return res.json({ updateInterval: 60 });
}
res.json({ res.json({
updateInterval: settings.update_interval, updateInterval: settings.update_interval,
cronExpression: `*/${settings.update_interval} * * * *` // Generate cron expression 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) // Get auto-update policy for agents (public endpoint)
router.get('/auto-update', async (req, res) => { router.get('/auto-update', async (req, res) => {
try { try {
const settings = await prisma.settings.findFirst({ const settings = await getSettings();
orderBy: { updated_at: 'desc' }
});
if (!settings) {
return res.json({ autoUpdate: false });
}
res.json({ res.json({
autoUpdate: settings.auto_update || false autoUpdate: settings.auto_update || false
}); });

View File

@@ -19,6 +19,7 @@ const repositoryRoutes = require('./routes/repositoryRoutes');
const versionRoutes = require('./routes/versionRoutes'); const versionRoutes = require('./routes/versionRoutes');
const tfaRoutes = require('./routes/tfaRoutes'); const tfaRoutes = require('./routes/tfaRoutes');
const updateScheduler = require('./services/updateScheduler'); const updateScheduler = require('./services/updateScheduler');
const { initSettings } = require('./services/settingsService');
// Initialize Prisma client with optimized connection pooling for multiple instances // Initialize Prisma client with optimized connection pooling for multiple instances
const prisma = createPrismaClient(); const prisma = createPrismaClient();
@@ -374,6 +375,19 @@ async function startServer() {
logger.info('✅ Database connection successful'); 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 // Check and import agent version on startup
await checkAndImportAgentVersion(); await checkAndImportAgentVersion();

View File

@@ -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
};