mirror of
https://github.com/9technologygroup/patchmon.net.git
synced 2026-02-06 05:39:52 -06:00
- Replace HTTPS GitHub API calls with SSH git commands - Use existing deploy key for private repository access - Add proper error handling for SSH authentication issues - Support fetching latest tags via git ls-remote - Maintain compatibility with private repositories using deploy keys This allows the version checking system to work with private repositories that have SSH deploy keys configured, using the same authentication as the local git operations.
181 lines
6.0 KiB
JavaScript
181 lines
6.0 KiB
JavaScript
require('dotenv').config();
|
|
const express = require('express');
|
|
const cors = require('cors');
|
|
const helmet = require('helmet');
|
|
const rateLimit = require('express-rate-limit');
|
|
const { PrismaClient } = require('@prisma/client');
|
|
const winston = require('winston');
|
|
|
|
// Import routes
|
|
const authRoutes = require('./routes/authRoutes');
|
|
const hostRoutes = require('./routes/hostRoutes');
|
|
const hostGroupRoutes = require('./routes/hostGroupRoutes');
|
|
const packageRoutes = require('./routes/packageRoutes');
|
|
const dashboardRoutes = require('./routes/dashboardRoutes');
|
|
const permissionsRoutes = require('./routes/permissionsRoutes');
|
|
const settingsRoutes = require('./routes/settingsRoutes');
|
|
const dashboardPreferencesRoutes = require('./routes/dashboardPreferencesRoutes');
|
|
const repositoryRoutes = require('./routes/repositoryRoutes');
|
|
const versionRoutes = require('./routes/versionRoutes');
|
|
|
|
// Initialize Prisma client
|
|
const prisma = new PrismaClient();
|
|
|
|
// Initialize logger - only if logging is enabled
|
|
const logger = process.env.ENABLE_LOGGING === 'true' ? winston.createLogger({
|
|
level: process.env.LOG_LEVEL || 'info',
|
|
format: winston.format.combine(
|
|
winston.format.timestamp(),
|
|
winston.format.errors({ stack: true }),
|
|
winston.format.json()
|
|
),
|
|
transports: [
|
|
new winston.transports.File({ filename: 'logs/error.log', level: 'error' }),
|
|
new winston.transports.File({ filename: 'logs/combined.log' }),
|
|
],
|
|
}) : {
|
|
info: () => {},
|
|
error: () => {},
|
|
warn: () => {},
|
|
debug: () => {}
|
|
};
|
|
|
|
if (process.env.ENABLE_LOGGING === 'true' && process.env.NODE_ENV !== 'production') {
|
|
logger.add(new winston.transports.Console({
|
|
format: winston.format.simple()
|
|
}));
|
|
}
|
|
|
|
const app = express();
|
|
const PORT = process.env.PORT || 3001;
|
|
|
|
// Trust proxy (needed when behind reverse proxy) and remove X-Powered-By
|
|
if (process.env.TRUST_PROXY) {
|
|
app.set('trust proxy', process.env.TRUST_PROXY === 'true' ? 1 : parseInt(process.env.TRUST_PROXY, 10) || true);
|
|
} else {
|
|
app.set('trust proxy', 1);
|
|
}
|
|
app.disable('x-powered-by');
|
|
|
|
// Rate limiting
|
|
const limiter = rateLimit({
|
|
windowMs: parseInt(process.env.RATE_LIMIT_WINDOW_MS) || 15 * 60 * 1000,
|
|
max: parseInt(process.env.RATE_LIMIT_MAX) || 100,
|
|
message: 'Too many requests from this IP, please try again later.',
|
|
});
|
|
|
|
// Middleware
|
|
// Helmet with stricter defaults (CSP/HSTS only in production)
|
|
app.use(helmet({
|
|
contentSecurityPolicy: process.env.NODE_ENV === 'production' ? {
|
|
useDefaults: true,
|
|
directives: {
|
|
defaultSrc: ["'self'"],
|
|
scriptSrc: ["'self'"],
|
|
styleSrc: ["'self'", "'unsafe-inline'"],
|
|
imgSrc: ["'self'", 'data:'],
|
|
fontSrc: ["'self'", 'data:'],
|
|
connectSrc: ["'self'"],
|
|
frameAncestors: ["'none'"],
|
|
objectSrc: ["'none'"]
|
|
}
|
|
} : false,
|
|
hsts: process.env.ENABLE_HSTS === 'true' || process.env.NODE_ENV === 'production'
|
|
}));
|
|
|
|
// CORS allowlist from comma-separated env
|
|
const parseOrigins = (val) => (val || '').split(',').map(s => s.trim()).filter(Boolean);
|
|
const allowedOrigins = parseOrigins(process.env.CORS_ORIGINS || process.env.CORS_ORIGIN || 'http://localhost:3000');
|
|
app.use(cors({
|
|
origin: function(origin, callback) {
|
|
// Allow non-browser/SSR tools with no origin
|
|
if (!origin) return callback(null, true);
|
|
if (allowedOrigins.includes(origin)) return callback(null, true);
|
|
return callback(new Error('Not allowed by CORS'));
|
|
},
|
|
credentials: true
|
|
}));
|
|
app.use(limiter);
|
|
// Reduce body size limits to reasonable defaults
|
|
app.use(express.json({ limit: process.env.JSON_BODY_LIMIT || '5mb' }));
|
|
app.use(express.urlencoded({ extended: true, limit: process.env.JSON_BODY_LIMIT || '5mb' }));
|
|
|
|
// Request logging - only if logging is enabled
|
|
if (process.env.ENABLE_LOGGING === 'true') {
|
|
app.use((req, res, next) => {
|
|
logger.info(`${req.method} ${req.path} - ${req.ip}`);
|
|
next();
|
|
});
|
|
}
|
|
|
|
// Health check endpoint
|
|
app.get('/health', (req, res) => {
|
|
res.json({ status: 'ok', timestamp: new Date().toISOString() });
|
|
});
|
|
|
|
// API routes
|
|
const apiVersion = process.env.API_VERSION || 'v1';
|
|
|
|
// Per-route rate limits
|
|
const authLimiter = rateLimit({
|
|
windowMs: parseInt(process.env.AUTH_RATE_LIMIT_WINDOW_MS) || 10 * 60 * 1000,
|
|
max: parseInt(process.env.AUTH_RATE_LIMIT_MAX) || 20
|
|
});
|
|
const agentLimiter = rateLimit({
|
|
windowMs: parseInt(process.env.AGENT_RATE_LIMIT_WINDOW_MS) || 60 * 1000,
|
|
max: parseInt(process.env.AGENT_RATE_LIMIT_MAX) || 120
|
|
});
|
|
|
|
app.use(`/api/${apiVersion}/auth`, authLimiter, authRoutes);
|
|
app.use(`/api/${apiVersion}/hosts`, agentLimiter, hostRoutes);
|
|
app.use(`/api/${apiVersion}/host-groups`, hostGroupRoutes);
|
|
app.use(`/api/${apiVersion}/packages`, packageRoutes);
|
|
app.use(`/api/${apiVersion}/dashboard`, dashboardRoutes);
|
|
app.use(`/api/${apiVersion}/permissions`, permissionsRoutes);
|
|
app.use(`/api/${apiVersion}/settings`, settingsRoutes);
|
|
app.use(`/api/${apiVersion}/dashboard-preferences`, dashboardPreferencesRoutes);
|
|
app.use(`/api/${apiVersion}/repositories`, repositoryRoutes);
|
|
app.use(`/api/${apiVersion}/version`, versionRoutes);
|
|
|
|
// Error handling middleware
|
|
app.use((err, req, res, next) => {
|
|
if (process.env.ENABLE_LOGGING === 'true') {
|
|
logger.error(err.stack);
|
|
}
|
|
res.status(500).json({
|
|
error: 'Something went wrong!',
|
|
message: process.env.NODE_ENV === 'development' ? err.message : undefined
|
|
});
|
|
});
|
|
|
|
// 404 handler
|
|
app.use('*', (req, res) => {
|
|
res.status(404).json({ error: 'Route not found' });
|
|
});
|
|
|
|
// Graceful shutdown
|
|
process.on('SIGTERM', async () => {
|
|
if (process.env.ENABLE_LOGGING === 'true') {
|
|
logger.info('SIGTERM received, shutting down gracefully');
|
|
}
|
|
await prisma.$disconnect();
|
|
process.exit(0);
|
|
});
|
|
|
|
process.on('SIGINT', async () => {
|
|
if (process.env.ENABLE_LOGGING === 'true') {
|
|
logger.info('SIGINT received, shutting down gracefully');
|
|
}
|
|
await prisma.$disconnect();
|
|
process.exit(0);
|
|
});
|
|
|
|
// Start server
|
|
app.listen(PORT, () => {
|
|
if (process.env.ENABLE_LOGGING === 'true') {
|
|
logger.info(`Server running on port ${PORT}`);
|
|
logger.info(`Environment: ${process.env.NODE_ENV}`);
|
|
}
|
|
});
|
|
|
|
module.exports = app;
|