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);