import express from 'express'; import cors from 'cors'; import { ConfigWatcher } from './config'; import { GitHubService } from './github'; const app = express(); // Security middleware app.use((req, res, next) => { // Remove server information from headers res.removeHeader('X-Powered-By'); // Add security headers res.setHeader('X-Content-Type-Options', 'nosniff'); res.setHeader('X-Frame-Options', 'DENY'); res.setHeader('X-XSS-Protection', '1; mode=block'); next(); }); // Block access to sensitive files app.use((req, res, next) => { const sensitiveFiles = [ '/config.json', '/config.example.json', '/.env', '/package.json', '/package-lock.json', '/tsconfig.json', '/server/', '/.git/', '/node_modules/', '/dist/', '/build/', '/.vscode/', '/.idea/', '/README.md', '/CLAUDE.md' ]; const normalizedPath = req.path.toLowerCase(); // Check if the request is for a sensitive file or directory const isSensitiveFile = sensitiveFiles.some(sensitiveFile => normalizedPath === sensitiveFile.toLowerCase() || normalizedPath.startsWith(sensitiveFile.toLowerCase()) ); if (isSensitiveFile) { console.warn(`🚫 Blocked access to sensitive file: ${req.path} from ${req.ip}`); return res.status(403).json({ error: 'Access denied', message: 'This resource is not available' }); } next(); }); app.use(cors()); app.use(express.json()); let configWatcher: ConfigWatcher; let githubService: GitHubService; try { configWatcher = new ConfigWatcher(); const config = configWatcher.getConfig(); githubService = new GitHubService(config.github.token, config.cache?.timeoutSeconds || 300); console.log('✅ Configuration loaded successfully'); logConfigChange(config); // Handle config changes configWatcher.on('configChanged', (newConfig) => { const oldCache = githubService.getCacheStats(); const newCacheTimeout = newConfig.cache?.timeoutSeconds || 300; // If cache timeout changed, create new service, otherwise just update token const currentCacheTimeout = githubService.getCacheTimeout(); if (currentCacheTimeout !== newCacheTimeout) { githubService = new GitHubService(newConfig.github.token, newCacheTimeout); console.log(`🔄 GitHub service recreated with new cache timeout: ${newCacheTimeout} seconds`); } else { githubService.updateToken(newConfig.github.token); console.log('🔄 GitHub service updated with new token'); } console.log(`📊 Previous cache had ${oldCache.size} entries`); logConfigChange(newConfig); }); configWatcher.on('configError', (error) => { console.error('⚠️ Config reload failed, continuing with previous config:', error); }); } catch (error) { console.error('❌ Configuration error:', error); process.exit(1); } app.get('/api/health', (req, res) => { res.json({ status: 'ok', timestamp: new Date().toISOString() }); }); // Helper function to safely filter config for public consumption function getPublicConfig(config: any) { // Only expose safe, non-sensitive configuration data // Never expose tokens, API keys, or other sensitive data return { repositories: config.github.repositories.map((repo: any) => ({ owner: repo.owner, name: repo.name, full_name: `${repo.owner}/${repo.name}` })), // Add cache info without sensitive details cache: { timeoutSeconds: config.cache?.timeoutSeconds || 300 }, // Add repository count for UI repositoryCount: config.github.repositories.length }; } // Helper function to safely log config changes without exposing sensitive data function logConfigChange(config: any) { console.log(`📊 Configuration loaded: ${config.github.repositories.length} repositories`); console.log(`💾 Cache timeout: ${config.cache?.timeoutSeconds || 300} seconds`); // Never log tokens or other sensitive data } app.get('/api/config', (req, res) => { try { const config = configWatcher.getConfig(); const publicConfig = getPublicConfig(config); res.json(publicConfig); } catch (error) { console.error('Error fetching config:', error); res.status(500).json({ error: 'Failed to fetch configuration' }); } }); app.get('/api/workflow-runs', async (req, res) => { try { const config = configWatcher.getConfig(); const runs = await githubService.getLatestMainBranchRuns(config.github.repositories); res.json(runs); } catch (error) { console.error('Error fetching workflow runs:', error); res.status(500).json({ error: 'Failed to fetch workflow runs' }); } }); app.get('/api/repository/:owner/:repo/workflow-runs', async (req, res) => { try { const { owner, repo } = req.params; const limit = parseInt(req.query.limit as string) || 10; const repository = { owner, name: repo }; const runs = await githubService.getRepositoryWorkflowRuns(repository, limit); res.json(runs); } catch (error) { console.error(`Error fetching workflow runs for ${req.params.owner}/${req.params.repo}:`, error); res.status(500).json({ error: 'Failed to fetch repository workflow runs' }); } }); // New endpoints for rate limit and cache management app.get('/api/rate-limit', (req, res) => { try { const rateLimitInfo = githubService.getRateLimitInfo(); res.json({ ...rateLimitInfo, resetTimeFormatted: new Date(rateLimitInfo.resetTime).toISOString(), timeUntilReset: Math.max(0, rateLimitInfo.resetTime - Date.now()) }); } catch (error) { console.error('Error fetching rate limit info:', error); res.status(500).json({ error: 'Failed to fetch rate limit information' }); } }); app.get('/api/cache/stats', (req, res) => { try { const cacheStats = githubService.getCacheStats(); res.json(cacheStats); } catch (error) { console.error('Error fetching cache stats:', error); res.status(500).json({ error: 'Failed to fetch cache statistics' }); } }); app.delete('/api/cache', (req, res) => { try { githubService.clearCache(); res.json({ message: 'Cache cleared successfully' }); } catch (error) { console.error('Error clearing cache:', error); res.status(500).json({ error: 'Failed to clear cache' }); } }); const config = configWatcher.getConfig(); const port = config.server.port; const host = config.server.host; app.listen(port, host, () => { console.log(`🚀 Server running at http://${host}:${port}`); console.log(`📡 API endpoints:`); console.log(` GET /api/health - Health check`); console.log(` GET /api/config - Repository configuration`); console.log(` GET /api/workflow-runs - Latest workflow runs`); console.log(` GET /api/repository/:owner/:repo/workflow-runs - Repository workflow runs`); console.log(` GET /api/rate-limit - GitHub API rate limit status`); console.log(` GET /api/cache/stats - Cache statistics`); console.log(` DELETE /api/cache - Clear cache`); console.log(`👀 Watching config.json for changes...`); }); // Graceful shutdown process.on('SIGTERM', () => { console.log('🛑 Received SIGTERM, shutting down gracefully...'); configWatcher.close(); process.exit(0); }); process.on('SIGINT', () => { console.log('🛑 Received SIGINT, shutting down gracefully...'); configWatcher.close(); process.exit(0); });