176 lines
5.4 KiB
JavaScript
Executable File
176 lines
5.4 KiB
JavaScript
Executable File
require('dotenv').config();
|
|
|
|
const express = require('express');
|
|
const sessionMiddleware = require('./lib/session');
|
|
const log = require('./lib/logger');
|
|
const authMiddleware = require('./lib/auth');
|
|
|
|
const app = express();
|
|
app.set('trust proxy', 1);
|
|
|
|
// Security headers
|
|
const helmet = require('helmet');
|
|
app.use(helmet({
|
|
contentSecurityPolicy: {
|
|
directives: {
|
|
defaultSrc: ["'self'"],
|
|
scriptSrc: ["'self'", "'unsafe-inline'", "https://mc.yandex.ru"],
|
|
styleSrc: ["'self'", "'unsafe-inline'"],
|
|
imgSrc: ["'self'", "data:", "https://mc.yandex.ru"],
|
|
connectSrc: ["'self'", "wss:", "ws:"],
|
|
fontSrc: ["'self'"],
|
|
objectSrc: ["'none'"],
|
|
frameAncestors: ["'none'"],
|
|
baseUri: ["'self'"],
|
|
formAction: ["'self'"],
|
|
scriptSrcAttr: ["'unsafe-inline'"],
|
|
},
|
|
},
|
|
crossOriginEmbedderPolicy: false,
|
|
}));
|
|
|
|
// Session
|
|
app.use(sessionMiddleware);
|
|
|
|
// Static assets (before auth — always public)
|
|
app.use(express.static('public'));
|
|
// Works portfolio — static HTML projects
|
|
app.use('/works', express.static('works'));
|
|
|
|
// Auth routes (before auth middleware — public)
|
|
app.use('/auth', require('./routes/auth'));
|
|
|
|
// Public API (only specific endpoints for landing page)
|
|
const apiRouter = require('./routes/api');
|
|
app.get('/api/settings', apiRouter);
|
|
app.get('/api/tools', apiRouter);
|
|
app.get('/api/content/advantages', apiRouter);
|
|
app.get('/api/content/dashboard', apiRouter);
|
|
|
|
// Public status page (before auth — no login required)
|
|
app.use('/status', require('./routes/status'));
|
|
|
|
// Auth middleware (protects everything below)
|
|
app.use(authMiddleware);
|
|
|
|
// Protected API (all other /api/* routes)
|
|
app.use('/api', apiRouter);
|
|
|
|
// Health check (extended)
|
|
const os = require('os');
|
|
const startTime = Date.now();
|
|
app.get('/health', (req, res) => {
|
|
// Public callers get a minimal response; detailed metrics only for authenticated admins.
|
|
const isAdmin = req.session && req.session.user && req.session.user.role === 'admin';
|
|
if (!isAdmin) return res.json({ status: 'ok' });
|
|
|
|
const mem = process.memoryUsage();
|
|
const queue = require('./lib/queue');
|
|
const wsInfo = require('./lib/ws');
|
|
const loadavg = os.loadavg();
|
|
|
|
res.json({
|
|
status: 'ok',
|
|
uptime: Math.floor((Date.now() - startTime) / 1000),
|
|
memory: {
|
|
rss: Math.round(mem.rss / 1024 / 1024) + 'MB',
|
|
heap: Math.round(mem.heapUsed / 1024 / 1024) + '/' + Math.round(mem.heapTotal / 1024 / 1024) + 'MB',
|
|
system: Math.round((1 - os.freemem() / os.totalmem()) * 100) + '%',
|
|
},
|
|
cpu: {
|
|
load1m: loadavg[0].toFixed(2),
|
|
load5m: loadavg[1].toFixed(2),
|
|
load15m: loadavg[2].toFixed(2),
|
|
cores: os.cpus().length,
|
|
},
|
|
queue: {
|
|
stats: queue.getStats(),
|
|
active: queue.getActiveCount(),
|
|
pending: queue.getQueueDepth(),
|
|
},
|
|
ws: {
|
|
connections: wsInfo.getConnectionCount(),
|
|
},
|
|
node: process.version,
|
|
pid: process.pid,
|
|
});
|
|
});
|
|
|
|
// Routes
|
|
const compressRouter = require('./routes/compress');
|
|
app.use('/compress', compressRouter);
|
|
|
|
// Download endpoint
|
|
const fs = require('fs');
|
|
const path = require('path');
|
|
app.get('/download/:filename', (req, res) => {
|
|
const filename = path.basename(req.params.filename);
|
|
const filePath = path.join(require('./lib/storage').RESULTS_DIR, filename);
|
|
if (!fs.existsSync(filePath)) return res.status(404).json({ error: 'Файл не найден' });
|
|
res.download(filePath, 'compressed.zip');
|
|
});
|
|
|
|
app.use(require('./routes/parser'));
|
|
app.use('/placeholder', require('./routes/placeholder'));
|
|
app.use('/placeholder-img', require('./routes/placeholder'));
|
|
app.use(require('./routes/httpclient'));
|
|
app.use(require('./routes/redirects'));
|
|
app.use(require('./routes/svgeditor'));
|
|
app.use('/video', require('./routes/video'));
|
|
app.use('/pdf', require('./routes/pdf'));
|
|
app.use('/favicon', require('./routes/favicon'));
|
|
app.use(require('./routes/pages'));
|
|
|
|
// Start server (async for AdminJS setup)
|
|
const PORT = parseInt(process.env.PORT) || 3000;
|
|
|
|
(async () => {
|
|
// AdminJS — loaded async (ESM modules)
|
|
try {
|
|
const { setupAdmin } = require('./lib/admin');
|
|
await setupAdmin(app);
|
|
log.info('AdminJS mounted at /admin');
|
|
} catch (err) {
|
|
log.error('AdminJS setup failed', { error: err.message });
|
|
}
|
|
|
|
// Global error handler — registered AFTER AdminJS so errors thrown inside /admin are caught too.
|
|
app.use((err, req, res, next) => {
|
|
log.error('Unhandled error', { error: err.message, stack: err.stack });
|
|
if (res.headersSent) return next(err);
|
|
res.status(500).json({ error: 'Внутренняя ошибка сервера' });
|
|
});
|
|
|
|
const server = app.listen(PORT, () => {
|
|
log.info(`Server started on http://localhost:${PORT}`);
|
|
});
|
|
|
|
// WebSocket
|
|
const wsServer = require('./lib/ws');
|
|
wsServer.attach(server);
|
|
|
|
// Graceful shutdown
|
|
function shutdown(signal) {
|
|
log.info(`${signal} received, shutting down gracefully...`);
|
|
server.close(() => {
|
|
log.info('Server closed');
|
|
process.exit(0);
|
|
});
|
|
setTimeout(() => {
|
|
log.warn('Forced shutdown after timeout');
|
|
process.exit(1);
|
|
}, 10000);
|
|
}
|
|
|
|
process.on('SIGTERM', () => shutdown('SIGTERM'));
|
|
process.on('SIGINT', () => shutdown('SIGINT'));
|
|
})();
|
|
|
|
process.on('unhandledRejection', (err) => {
|
|
log.error('Unhandled rejection', { error: err?.message || String(err) });
|
|
});
|
|
process.on('uncaughtException', (err) => {
|
|
log.error('Uncaught exception', { error: err.message, stack: err.stack });
|
|
process.exit(1);
|
|
});
|