mirror of
https://github.com/9technologygroup/patchmon.net.git
synced 2025-11-09 16:37:29 +00:00
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:
@@ -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
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -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();
|
||||||
|
|
||||||
|
|||||||
170
backend/src/services/settingsService.js
Normal file
170
backend/src/services/settingsService.js
Normal 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
|
||||||
|
};
|
||||||
Reference in New Issue
Block a user