Files
cmdb-insight/backend/src/index.ts
Bert Hausmans cdee0e8819 UI styling improvements: dashboard headers and navigation
- Restore blue PageHeader on Dashboard (/app-components)
- Update homepage (/) with subtle header design without blue bar
- Add uniform PageHeader styling to application edit page
- Fix Rapporten link on homepage to point to /reports overview
- Improve header descriptions spacing for better readability
2026-01-21 03:24:56 +01:00

298 lines
10 KiB
TypeScript
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import express from 'express';
import cors from 'cors';
import helmet from 'helmet';
import rateLimit from 'express-rate-limit';
import cookieParser from 'cookie-parser';
import { config, validateConfig } from './config/env.js';
import { logger } from './services/logger.js';
import { dataService } from './services/dataService.js';
import { cmdbService } from './services/cmdbService.js';
import applicationsRouter from './routes/applications.js';
import classificationsRouter from './routes/classifications.js';
import referenceDataRouter from './routes/referenceData.js';
import dashboardRouter from './routes/dashboard.js';
import configurationRouter from './routes/configuration.js';
import authRouter, { authMiddleware } from './routes/auth.js';
import usersRouter from './routes/users.js';
import rolesRouter from './routes/roles.js';
import userSettingsRouter from './routes/userSettings.js';
import profileRouter from './routes/profile.js';
import searchRouter from './routes/search.js';
import cacheRouter from './routes/cache.js';
import objectsRouter from './routes/objects.js';
import schemaRouter from './routes/schema.js';
import dataValidationRouter from './routes/dataValidation.js';
import schemaConfigurationRouter from './routes/schemaConfiguration.js';
import { runMigrations } from './services/database/migrations.js';
// Validate configuration
validateConfig();
const app = express();
// Security middleware
app.use(helmet());
app.use(cors({
origin: config.isDevelopment ? ['http://localhost:5173', 'http://localhost:3000'] : [config.frontendUrl],
credentials: true,
}));
// Cookie parser for session handling
app.use(cookieParser());
// Rate limiting
const limiter = rateLimit({
windowMs: 15 * 60 * 1000, // 15 minutes
max: 1000, // Limit each IP to 1000 requests per windowMs
message: 'Too many requests from this IP, please try again later.',
});
app.use(limiter);
// Body parsing
app.use(express.json({ limit: '10mb' }));
app.use(express.urlencoded({ extended: true }));
// Request logging
app.use((req, res, next) => {
logger.debug(`${req.method} ${req.path}`);
next();
});
// Auth middleware - extract session info for all requests
app.use(authMiddleware);
// Set user token and settings on services for each request
app.use(async (req, res, next) => {
// Set user's OAuth token if available (for OAuth sessions)
let userToken: string | null = null;
if (req.accessToken) {
userToken = req.accessToken;
}
// Set user's Jira PAT and AI keys if user is authenticated and has local account
if (req.user && 'id' in req.user) {
try {
const { userSettingsService } = await import('./services/userSettingsService.js');
const settings = await userSettingsService.getUserSettings(req.user.id);
if (settings?.jira_pat) {
// Use user's Jira PAT from profile settings (preferred for writes)
userToken = settings.jira_pat;
} else if (config.jiraServiceAccountToken) {
// Fallback to service account token if user doesn't have PAT configured
// This allows writes to work when JIRA_SERVICE_ACCOUNT_TOKEN is set in .env
userToken = config.jiraServiceAccountToken;
logger.debug('Using service account token as fallback (user PAT not configured)');
}
// Store user settings in request for services to access
(req as any).userSettings = settings;
} catch (error) {
// If user settings can't be loaded, try service account token as fallback
logger.debug('Failed to load user settings:', error);
if (config.jiraServiceAccountToken) {
userToken = config.jiraServiceAccountToken;
logger.debug('Using service account token as fallback (user settings load failed)');
}
}
}
// Set token on old services (for backward compatibility)
if (userToken) {
cmdbService.setUserToken(userToken);
} else {
cmdbService.setUserToken(null);
}
// Set token on new V2 infrastructure client (if feature flag enabled)
if (process.env.USE_V2_API === 'true') {
try {
const { jiraAssetsClient } = await import('./infrastructure/jira/JiraAssetsClient.js');
jiraAssetsClient.setRequestToken(userToken);
// Clear token after response
res.on('finish', () => {
jiraAssetsClient.clearRequestToken();
});
} catch (error) {
// V2 API not loaded - ignore
}
}
// Clear token after response is sent (for old services)
res.on('finish', () => {
cmdbService.clearUserToken();
});
next();
});
// Health check
app.get('/health', async (req, res) => {
const jiraConnected = await dataService.testConnection();
const cacheStatus = await dataService.getCacheStatus();
res.json({
status: 'ok',
timestamp: new Date().toISOString(),
dataSource: 'jira-assets-cached', // Always uses Jira Assets (mock data removed)
jiraConnected: jiraConnected,
aiConfigured: true, // AI is configured per-user in profile settings
cache: {
isWarm: cacheStatus.isWarm,
objectCount: cacheStatus.totalObjects,
lastSync: cacheStatus.lastIncrementalSync,
},
});
});
// Config endpoint
app.get('/api/config', (req, res) => {
res.json({
jiraHost: config.jiraHost,
});
});
// API routes
app.use('/api/auth', authRouter);
app.use('/api/users', usersRouter);
app.use('/api/roles', rolesRouter);
app.use('/api/user-settings', userSettingsRouter);
app.use('/api/profile', profileRouter);
app.use('/api/applications', applicationsRouter);
app.use('/api/classifications', classificationsRouter);
app.use('/api/reference-data', referenceDataRouter);
app.use('/api/dashboard', dashboardRouter);
app.use('/api/configuration', configurationRouter);
app.use('/api/search', searchRouter);
app.use('/api/cache', cacheRouter);
app.use('/api/objects', objectsRouter);
app.use('/api/schema', schemaRouter);
app.use('/api/data-validation', dataValidationRouter);
app.use('/api/schema-configuration', schemaConfigurationRouter);
// V2 API routes (new refactored architecture) - Feature flag: USE_V2_API
const useV2Api = process.env.USE_V2_API === 'true';
const useV2ApiEnv = process.env.USE_V2_API || 'not set';
logger.info(`V2 API feature flag: USE_V2_API=${useV2ApiEnv} (enabled: ${useV2Api})`);
if (useV2Api) {
try {
logger.debug('Loading V2 API routes from ./api/routes/v2.js...');
const v2Router = (await import('./api/routes/v2.js')).default;
if (!v2Router) {
logger.error('❌ V2 API router is undefined - route file did not export default router');
} else {
app.use('/api/v2', v2Router);
logger.info('✅ V2 API routes enabled and mounted at /api/v2');
logger.debug('V2 API router type:', typeof v2Router, 'is function:', typeof v2Router === 'function');
}
} catch (error) {
logger.error('❌ Failed to load V2 API routes', error);
if (error instanceof Error) {
logger.error('Error details:', {
message: error.message,
stack: error.stack,
name: error.name,
});
}
}
} else {
logger.info(` V2 API routes disabled (USE_V2_API=${useV2ApiEnv}, set USE_V2_API=true to enable)`);
}
// Error handling
app.use((err: Error, req: express.Request, res: express.Response, next: express.NextFunction) => {
logger.error('Unhandled error:', err);
res.status(500).json({
error: 'Internal server error',
message: config.isDevelopment ? err.message : undefined,
});
});
// 404 handler
app.use((req, res) => {
// Provide helpful error messages for V2 API routes
if (req.path.startsWith('/api/v2/')) {
const useV2Api = process.env.USE_V2_API === 'true';
if (!useV2Api) {
res.status(404).json({
error: 'V2 API routes are not enabled',
message: 'Please set USE_V2_API=true in environment variables and restart the server to use V2 API endpoints.',
path: req.path,
});
return;
}
}
res.status(404).json({ error: 'Not found', path: req.path });
});
// Start server
const PORT = config.port;
app.listen(PORT, async () => {
logger.info(`Server running on http://localhost:${PORT}`);
logger.info(`Environment: ${config.nodeEnv}`);
logger.info(`AI Classification: Configured per-user in profile settings`);
// Log V2 API feature flag status
const useV2ApiEnv = process.env.USE_V2_API || 'not set';
const useV2ApiEnabled = process.env.USE_V2_API === 'true';
logger.info(`V2 API Feature Flag: USE_V2_API=${useV2ApiEnv} (${useV2ApiEnabled ? '✅ ENABLED' : '❌ DISABLED'})`);
// Check if schemas exist in database
// Note: Schemas table may not exist yet if schema hasn't been initialized
let hasSchemas = false;
try {
const { normalizedCacheStore } = await import('./services/normalizedCacheStore.js');
const db = (normalizedCacheStore as any).db;
if (db) {
await db.ensureInitialized?.();
try {
const schemaRow = await db.queryOne<{ count: number }>(
`SELECT COUNT(*) as count FROM schemas`
);
hasSchemas = (schemaRow?.count || 0) > 0;
} catch (tableError: any) {
// If schemas table doesn't exist yet, that's okay - schema hasn't been initialized
if (tableError?.message?.includes('does not exist') ||
tableError?.message?.includes('relation') ||
tableError?.code === '42P01') { // PostgreSQL: undefined table
logger.debug('Schemas table does not exist yet (will be created by migrations)');
hasSchemas = false;
} else {
throw tableError; // Re-throw other errors
}
}
}
} catch (error) {
logger.debug('Failed to check if schemas exist in database (table may not exist yet)', error);
}
logger.info(`Jira Assets: ${hasSchemas ? 'Schemas configured in database - users configure PAT in profile' : 'No schemas configured - use Schema Configuration page to discover schemas'}`);
logger.info('Sync: All syncs must be triggered manually from the GUI (no auto-start)');
logger.info('Data: All data comes from Jira Assets API (mock data removed)');
// Run database migrations FIRST to create schemas table before other services try to use it
try {
logger.info('Running database migrations...');
await runMigrations();
logger.info('✅ Database migrations completed');
} catch (error) {
logger.error('❌ Failed to run database migrations', error);
}
});
// Graceful shutdown
const shutdown = () => {
logger.info('Shutdown signal received: stopping services...');
// Note: No sync engine to stop - syncs are only triggered from GUI
logger.info('Services stopped, exiting');
process.exit(0);
};
process.on('SIGTERM', shutdown);
process.on('SIGINT', shutdown);