Files
cmdb-insight/backend/src/routes/configuration.ts
Bert Hausmans 1fa424efb9 Add authentication, user management, and database migration features
- Implement OAuth 2.0 and PAT authentication methods
- Add user management, roles, and profile functionality
- Add database migrations and admin user scripts
- Update services for authentication and user settings
- Add protected routes and permission hooks
- Update documentation for authentication and database access
2026-01-15 03:20:50 +01:00

274 lines
11 KiB
TypeScript

import { Router, Request, Response } from 'express';
import { readFile, writeFile } from 'fs/promises';
import { join, dirname } from 'path';
import { fileURLToPath } from 'url';
import { logger } from '../services/logger.js';
import { clearEffortCalculationConfigCache, getEffortCalculationConfigV25 } from '../services/effortCalculation.js';
import { requireAuth, requirePermission } from '../middleware/authorization.js';
import type { EffortCalculationConfig, EffortCalculationConfigV25 } from '../config/effortCalculation.js';
import type { DataCompletenessConfig } from '../types/index.js';
// Get __dirname equivalent for ES modules
const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
const router = Router();
// All routes require authentication and manage_settings permission
router.use(requireAuth);
router.use(requirePermission('manage_settings'));
// Path to the configuration files
const CONFIG_FILE_PATH = join(__dirname, '../../data/effort-calculation-config.json');
const CONFIG_FILE_PATH_V25 = join(__dirname, '../../data/effort-calculation-config.json');
const COMPLETENESS_CONFIG_FILE_PATH = join(__dirname, '../../data/data-completeness-config.json');
/**
* Get the current effort calculation configuration (legacy)
*/
router.get('/effort-calculation', async (req: Request, res: Response) => {
try {
// Try to read from JSON file, fallback to default config
try {
const fileContent = await readFile(CONFIG_FILE_PATH, 'utf-8');
const config = JSON.parse(fileContent) as EffortCalculationConfig;
res.json(config);
} catch (fileError) {
// If file doesn't exist, return default config from code
const { EFFORT_CALCULATION_CONFIG } = await import('../config/effortCalculation.js');
res.json(EFFORT_CALCULATION_CONFIG);
}
} catch (error) {
logger.error('Failed to get effort calculation configuration', error);
res.status(500).json({ error: 'Failed to get configuration' });
}
});
/**
* Update the effort calculation configuration (legacy)
*/
router.put('/effort-calculation', async (req: Request, res: Response) => {
try {
const config = req.body as EffortCalculationConfig;
// Validate the configuration structure
if (!config.governanceModelRules || !Array.isArray(config.governanceModelRules)) {
res.status(400).json({ error: 'Invalid configuration: governanceModelRules must be an array' });
return;
}
if (!config.default || typeof config.default.result !== 'number') {
res.status(400).json({ error: 'Invalid configuration: default.result must be a number' });
return;
}
// Write to JSON file
await writeFile(CONFIG_FILE_PATH, JSON.stringify(config, null, 2), 'utf-8');
// Clear the cache so the new config is loaded on next request
clearEffortCalculationConfigCache();
logger.info('Effort calculation configuration updated');
res.json({ success: true, message: 'Configuration saved successfully' });
} catch (error) {
logger.error('Failed to update effort calculation configuration', error);
res.status(500).json({ error: 'Failed to save configuration' });
}
});
/**
* Get the v25 effort calculation configuration
*/
router.get('/effort-calculation-v25', async (req: Request, res: Response) => {
try {
// Try to read from JSON file, fallback to default config
try {
const fileContent = await readFile(CONFIG_FILE_PATH_V25, 'utf-8');
const config = JSON.parse(fileContent) as EffortCalculationConfigV25;
res.json(config);
} catch (fileError) {
// If file doesn't exist, return default config from code
const config = getEffortCalculationConfigV25();
res.json(config);
}
} catch (error) {
logger.error('Failed to get effort calculation configuration v25', error);
res.status(500).json({ error: 'Failed to get configuration' });
}
});
/**
* Update the v25 effort calculation configuration
*/
router.put('/effort-calculation-v25', async (req: Request, res: Response) => {
try {
const config = req.body as EffortCalculationConfigV25;
// Validate the configuration structure
if (!config.regiemodellen || typeof config.regiemodellen !== 'object') {
res.status(400).json({ error: 'Invalid configuration: regiemodellen must be an object' });
return;
}
if (!config.validationRules || typeof config.validationRules !== 'object') {
res.status(400).json({ error: 'Invalid configuration: validationRules must be an object' });
return;
}
// Write to JSON file
await writeFile(CONFIG_FILE_PATH_V25, JSON.stringify(config, null, 2), 'utf-8');
// Clear the cache so the new config is loaded on next request
clearEffortCalculationConfigCache();
logger.info('Effort calculation configuration v25 updated');
res.json({ success: true, message: 'Configuration v25 saved successfully' });
} catch (error) {
logger.error('Failed to update effort calculation configuration v25', error);
res.status(500).json({ error: 'Failed to save configuration' });
}
});
/**
* Get the data completeness configuration
*/
router.get('/data-completeness', async (req: Request, res: Response) => {
try {
// Try to read from JSON file, fallback to default config
try {
const fileContent = await readFile(COMPLETENESS_CONFIG_FILE_PATH, 'utf-8');
const config = JSON.parse(fileContent) as DataCompletenessConfig;
res.json(config);
} catch (fileError) {
// If file doesn't exist, return default config
const defaultConfig: DataCompletenessConfig = {
metadata: {
version: '1.0.0',
description: 'Configuration for Data Completeness Score fields',
lastUpdated: new Date().toISOString(),
},
categories: [
{
id: 'general',
name: 'General',
description: 'General application information fields',
fields: [
{ id: 'organisation', name: 'Organisation', fieldPath: 'organisation', enabled: true },
{ id: 'applicationFunctions', name: 'ApplicationFunction', fieldPath: 'applicationFunctions', enabled: true },
{ id: 'status', name: 'Status', fieldPath: 'status', enabled: true },
{ id: 'businessImpactAnalyse', name: 'Business Impact Analyse', fieldPath: 'businessImpactAnalyse', enabled: true },
{ id: 'hostingType', name: 'Application Component Hosting Type', fieldPath: 'hostingType', enabled: true },
{ id: 'supplierProduct', name: 'Supplier Product', fieldPath: 'supplierProduct', enabled: true },
{ id: 'businessOwner', name: 'Business Owner', fieldPath: 'businessOwner', enabled: true },
{ id: 'systemOwner', name: 'System Owner', fieldPath: 'systemOwner', enabled: true },
{ id: 'functionalApplicationManagement', name: 'Functional Application Management', fieldPath: 'functionalApplicationManagement', enabled: true },
{ id: 'technicalApplicationManagement', name: 'Technical Application Management', fieldPath: 'technicalApplicationManagement', enabled: true },
],
},
{
id: 'applicationManagement',
name: 'Application Management',
description: 'Application management classification fields',
fields: [
{ id: 'governanceModel', name: 'ICT Governance Model', fieldPath: 'governanceModel', enabled: true },
{ id: 'applicationType', name: 'Application Management - Application Type', fieldPath: 'applicationType', enabled: true },
{ id: 'applicationManagementHosting', name: 'Application Management - Hosting', fieldPath: 'applicationManagementHosting', enabled: true },
{ id: 'applicationManagementTAM', name: 'Application Management - TAM', fieldPath: 'applicationManagementTAM', enabled: true },
{ id: 'dynamicsFactor', name: 'Application Management - Dynamics Factor', fieldPath: 'dynamicsFactor', enabled: true },
{ id: 'complexityFactor', name: 'Application Management - Complexity Factor', fieldPath: 'complexityFactor', enabled: true },
{ id: 'numberOfUsers', name: 'Application Management - Number of Users', fieldPath: 'numberOfUsers', enabled: true },
],
},
],
};
res.json(defaultConfig);
}
} catch (error) {
logger.error('Failed to get data completeness configuration', error);
res.status(500).json({ error: 'Failed to get configuration' });
}
});
/**
* Update the data completeness configuration
*/
router.put('/data-completeness', async (req: Request, res: Response) => {
try {
const config = req.body as DataCompletenessConfig;
// Validate the configuration structure
if (!config.categories || !Array.isArray(config.categories)) {
res.status(400).json({ error: 'Invalid configuration: categories must be an array' });
return;
}
if (config.categories.length === 0) {
res.status(400).json({ error: 'Invalid configuration: must have at least one category' });
return;
}
// Validate each category
for (const category of config.categories) {
if (!category.id || typeof category.id !== 'string') {
res.status(400).json({ error: 'Invalid configuration: each category must have an id' });
return;
}
if (!category.name || typeof category.name !== 'string') {
res.status(400).json({ error: 'Invalid configuration: each category must have a name' });
return;
}
if (!Array.isArray(category.fields)) {
res.status(400).json({ error: 'Invalid configuration: category fields must be arrays' });
return;
}
// Validate each field
for (const field of category.fields) {
if (!field.id || typeof field.id !== 'string') {
res.status(400).json({ error: 'Invalid configuration: each field must have an id' });
return;
}
if (!field.name || typeof field.name !== 'string') {
res.status(400).json({ error: 'Invalid configuration: each field must have a name' });
return;
}
if (!field.fieldPath || typeof field.fieldPath !== 'string') {
res.status(400).json({ error: 'Invalid configuration: each field must have a fieldPath' });
return;
}
if (typeof field.enabled !== 'boolean') {
res.status(400).json({ error: 'Invalid configuration: each field must have an enabled boolean' });
return;
}
}
}
// Update metadata
config.metadata = {
...config.metadata,
lastUpdated: new Date().toISOString(),
};
// Write to JSON file
await writeFile(COMPLETENESS_CONFIG_FILE_PATH, JSON.stringify(config, null, 2), 'utf-8');
// Clear the cache so the new config is loaded on next request
const { clearDataCompletenessConfigCache } = await import('../services/dataCompletenessConfig.js');
clearDataCompletenessConfigCache();
logger.info('Data completeness configuration updated');
res.json({ success: true, message: 'Configuration saved successfully' });
} catch (error) {
logger.error('Failed to update data completeness configuration', error);
res.status(500).json({ error: 'Failed to save configuration' });
}
});
export default router;