226 lines
7.2 KiB
TypeScript
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);
|
|
}); |