Critical fixes: - helmet middleware (X-Frame-Options, HSTS, X-Content-Type, CSP, etc) - Remove /admin from PUBLIC_PREFIXES (double auth: session + admin role) - Session cookie: httpOnly, sameSite=lax, 1-day expiry (was 30 days) - Session regeneration on login (prevent session fixation) - Blocked user check on login Rate limiting: - /auth/login: 10 req / 15 min (brute force protection) - /auth/register: 10 req / 15 min (spam protection) - /parse, /metadata, /text, /preview: 20 req / min (DDoS via server) Input sanitization: - Strip HTML tags from display_name (stored XSS prevention)
139 lines
4.0 KiB
JavaScript
Executable File
139 lines
4.0 KiB
JavaScript
Executable File
require('dotenv').config();
|
|
|
|
const express = require('express');
|
|
const session = require('express-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: false, // Tailwind CDN needs inline scripts
|
|
crossOriginEmbedderPolicy: false,
|
|
}));
|
|
|
|
// Session
|
|
app.use(session({
|
|
secret: process.env.SESSION_SECRET || 'change-me-in-env',
|
|
resave: false,
|
|
saveUninitialized: false,
|
|
cookie: {
|
|
maxAge: 24 * 60 * 60 * 1000, // 1 day
|
|
httpOnly: true,
|
|
secure: process.env.NODE_ENV === 'production',
|
|
sameSite: 'lax',
|
|
},
|
|
}));
|
|
|
|
// Static assets (before auth — always public)
|
|
app.use(express.static('public'));
|
|
|
|
// 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 startTime = Date.now();
|
|
app.get('/health', (req, res) => {
|
|
const mem = process.memoryUsage();
|
|
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',
|
|
},
|
|
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(compressRouter.DOWNLOADS_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(require('./routes/pages'));
|
|
|
|
// Global error handler
|
|
app.use((err, req, res, next) => {
|
|
log.error('Unhandled error', { error: err.message, stack: err.stack });
|
|
res.status(500).json({ error: 'Внутренняя ошибка сервера' });
|
|
});
|
|
|
|
// 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 });
|
|
}
|
|
|
|
const server = app.listen(PORT, () => {
|
|
log.info(`Server started on http://localhost:${PORT}`);
|
|
});
|
|
|
|
// 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);
|
|
});
|