- 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
298 lines
10 KiB
TypeScript
298 lines
10 KiB
TypeScript
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);
|