Some checks failed
Deploy / deploy (push) Has been cancelled
Full-stack web application for Telegram management - Frontend: Vue 3 + Vben Admin - Backend: NestJS - Features: User management, group broadcast, statistics 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
304 lines
8.0 KiB
JavaScript
304 lines
8.0 KiB
JavaScript
import helmet from 'helmet';
|
|
import cors from 'cors';
|
|
import hpp from 'hpp';
|
|
import mongoSanitize from 'express-mongo-sanitize';
|
|
import { logger } from '../utils/logger.js';
|
|
import { config } from '../config/index.js';
|
|
import crypto from 'crypto';
|
|
|
|
/**
|
|
* Configure CORS
|
|
*/
|
|
export const corsOptions = {
|
|
origin: function (origin, callback) {
|
|
const allowedOrigins = config.cors?.allowedOrigins || [
|
|
'http://localhost:8080',
|
|
'http://localhost:3000',
|
|
'https://app.marketing-agent.com'
|
|
];
|
|
|
|
// Allow requests with no origin (like mobile apps or Postman)
|
|
if (!origin) return callback(null, true);
|
|
|
|
if (allowedOrigins.indexOf('*') !== -1 || allowedOrigins.indexOf(origin) !== -1) {
|
|
callback(null, true);
|
|
} else {
|
|
logger.warn('CORS blocked request', { origin, ip: origin });
|
|
callback(new Error('Not allowed by CORS'));
|
|
}
|
|
},
|
|
credentials: true,
|
|
methods: ['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'OPTIONS'],
|
|
allowedHeaders: ['Content-Type', 'Authorization', 'X-API-Key', 'X-Request-ID'],
|
|
exposedHeaders: ['X-Request-ID', 'X-RateLimit-Limit', 'X-RateLimit-Remaining', 'X-RateLimit-Reset'],
|
|
maxAge: 86400 // 24 hours
|
|
};
|
|
|
|
/**
|
|
* Configure Helmet security headers
|
|
*/
|
|
export const helmetConfig = helmet({
|
|
contentSecurityPolicy: {
|
|
directives: {
|
|
defaultSrc: ["'self'"],
|
|
scriptSrc: ["'self'", "'unsafe-inline'"],
|
|
styleSrc: ["'self'", "'unsafe-inline'"],
|
|
imgSrc: ["'self'", "data:", "https:"],
|
|
connectSrc: ["'self'"],
|
|
fontSrc: ["'self'"],
|
|
objectSrc: ["'none'"],
|
|
mediaSrc: ["'self'"],
|
|
frameSrc: ["'none'"],
|
|
upgradeInsecureRequests: config.environment === 'production' ? [] : null
|
|
}
|
|
},
|
|
hsts: {
|
|
maxAge: 31536000,
|
|
includeSubDomains: true,
|
|
preload: true
|
|
}
|
|
});
|
|
|
|
/**
|
|
* Request ID middleware
|
|
*/
|
|
export function requestId(req, res, next) {
|
|
const id = req.get('X-Request-ID') || crypto.randomUUID();
|
|
req.id = id;
|
|
res.setHeader('X-Request-ID', id);
|
|
next();
|
|
}
|
|
|
|
/**
|
|
* IP whitelist/blacklist middleware
|
|
*/
|
|
export function ipFilter(options = {}) {
|
|
const whitelist = options.whitelist || [];
|
|
const blacklist = options.blacklist || [];
|
|
|
|
return (req, res, next) => {
|
|
const clientIp = req.ip || req.connection.remoteAddress;
|
|
|
|
// Check blacklist first
|
|
if (blacklist.length > 0 && blacklist.includes(clientIp)) {
|
|
logger.warn('Blacklisted IP attempted access', { ip: clientIp });
|
|
return res.status(403).json({
|
|
success: false,
|
|
error: 'Access denied'
|
|
});
|
|
}
|
|
|
|
// Check whitelist if configured
|
|
if (whitelist.length > 0 && !whitelist.includes(clientIp)) {
|
|
logger.warn('Non-whitelisted IP attempted access', { ip: clientIp });
|
|
return res.status(403).json({
|
|
success: false,
|
|
error: 'Access denied'
|
|
});
|
|
}
|
|
|
|
next();
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Security headers middleware
|
|
*/
|
|
export function securityHeaders(req, res, next) {
|
|
// Additional security headers
|
|
res.setHeader('X-Frame-Options', 'DENY');
|
|
res.setHeader('X-Content-Type-Options', 'nosniff');
|
|
res.setHeader('X-XSS-Protection', '1; mode=block');
|
|
res.setHeader('Referrer-Policy', 'strict-origin-when-cross-origin');
|
|
res.setHeader('Permissions-Policy', 'geolocation=(), microphone=(), camera=()');
|
|
|
|
// Remove potentially sensitive headers
|
|
res.removeHeader('X-Powered-By');
|
|
res.removeHeader('Server');
|
|
|
|
next();
|
|
}
|
|
|
|
/**
|
|
* API version check middleware
|
|
*/
|
|
export function apiVersionCheck(supportedVersions = ['v1']) {
|
|
return (req, res, next) => {
|
|
const version = req.headers['api-version'] || req.query.apiVersion;
|
|
|
|
if (version && !supportedVersions.includes(version)) {
|
|
return res.status(400).json({
|
|
success: false,
|
|
error: 'Unsupported API version',
|
|
supportedVersions
|
|
});
|
|
}
|
|
|
|
req.apiVersion = version || supportedVersions[0];
|
|
next();
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Prevent parameter pollution
|
|
*/
|
|
export const preventParamPollution = hpp({
|
|
whitelist: ['sort', 'fields', 'filter', 'page', 'limit']
|
|
});
|
|
|
|
/**
|
|
* MongoDB injection prevention
|
|
*/
|
|
export const preventMongoInjection = mongoSanitize({
|
|
replaceWith: '_',
|
|
onSanitize: ({ req, key }) => {
|
|
logger.warn('MongoDB injection attempt prevented', {
|
|
ip: req.ip,
|
|
path: req.path,
|
|
key
|
|
});
|
|
}
|
|
});
|
|
|
|
/**
|
|
* Request logging middleware
|
|
*/
|
|
export function requestLogger(req, res, next) {
|
|
const start = Date.now();
|
|
|
|
res.on('finish', () => {
|
|
const duration = Date.now() - start;
|
|
|
|
logger.info('Request processed', {
|
|
requestId: req.id,
|
|
method: req.method,
|
|
path: req.path,
|
|
statusCode: res.statusCode,
|
|
duration,
|
|
ip: req.ip,
|
|
userAgent: req.get('user-agent'),
|
|
userId: req.user?.id
|
|
});
|
|
});
|
|
|
|
next();
|
|
}
|
|
|
|
/**
|
|
* Error logging middleware
|
|
*/
|
|
export function errorLogger(err, req, res, next) {
|
|
logger.error('Request error', {
|
|
requestId: req.id,
|
|
error: err.message,
|
|
stack: err.stack,
|
|
method: req.method,
|
|
path: req.path,
|
|
ip: req.ip,
|
|
userId: req.user?.id
|
|
});
|
|
|
|
next(err);
|
|
}
|
|
|
|
/**
|
|
* Security audit middleware
|
|
*/
|
|
export function securityAudit(eventType) {
|
|
return (req, res, next) => {
|
|
logger.info('Security event', {
|
|
eventType,
|
|
requestId: req.id,
|
|
userId: req.user?.id,
|
|
ip: req.ip,
|
|
path: req.path,
|
|
method: req.method,
|
|
timestamp: new Date().toISOString()
|
|
});
|
|
|
|
next();
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Trusted proxy configuration
|
|
*/
|
|
export function configureTrustedProxies(app) {
|
|
// Trust proxies for accurate IP detection
|
|
app.set('trust proxy', config.trustProxy || ['loopback', 'linklocal', 'uniquelocal']);
|
|
}
|
|
|
|
/**
|
|
* Session fixation prevention
|
|
*/
|
|
export function preventSessionFixation(req, res, next) {
|
|
if (req.session && req.user) {
|
|
// Regenerate session ID on login
|
|
if (req.path.includes('/login') && res.statusCode === 200) {
|
|
req.session.regenerate((err) => {
|
|
if (err) {
|
|
logger.error('Session regeneration failed', { error: err });
|
|
}
|
|
next();
|
|
});
|
|
return;
|
|
}
|
|
}
|
|
next();
|
|
}
|
|
|
|
/**
|
|
* CSRF protection for state-changing operations
|
|
*/
|
|
export function csrfProtection(options = {}) {
|
|
const excludePaths = options.exclude || ['/api/v1/auth/login', '/api/v1/auth/register'];
|
|
|
|
return (req, res, next) => {
|
|
// Skip for excluded paths
|
|
if (excludePaths.some(path => req.path.includes(path))) {
|
|
return next();
|
|
}
|
|
|
|
// Skip for safe methods
|
|
if (['GET', 'HEAD', 'OPTIONS'].includes(req.method)) {
|
|
return next();
|
|
}
|
|
|
|
const token = req.headers['x-csrf-token'] || req.body._csrf;
|
|
const sessionToken = req.session?.csrfToken;
|
|
|
|
if (!token || !sessionToken || token !== sessionToken) {
|
|
return res.status(403).json({
|
|
success: false,
|
|
error: 'Invalid CSRF token'
|
|
});
|
|
}
|
|
|
|
next();
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Apply all security middleware
|
|
*/
|
|
export function applySecurityMiddleware(app) {
|
|
// Basic security
|
|
app.use(helmetConfig);
|
|
app.use(cors(corsOptions));
|
|
app.use(requestId);
|
|
app.use(securityHeaders);
|
|
|
|
// Request processing
|
|
app.use(express.json({ limit: '10mb' }));
|
|
app.use(express.urlencoded({ extended: true, limit: '10mb' }));
|
|
|
|
// Injection prevention
|
|
app.use(preventMongoInjection);
|
|
app.use(preventParamPollution);
|
|
|
|
// Logging
|
|
app.use(requestLogger);
|
|
|
|
// Configure trusted proxies
|
|
configureTrustedProxies(app);
|
|
} |