Files
flipstone-radar/server/index.ts

226 lines
7.2 KiB
TypeScript

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