diff --git a/backend/package-lock.json b/backend/package-lock.json index dbbea24..3df3137 100644 --- a/backend/package-lock.json +++ b/backend/package-lock.json @@ -13,6 +13,8 @@ "cors": "^2.8.5", "dotenv": "^16.3.1", "express": "^4.18.2", + "express-rate-limit": "^8.2.1", + "helmet": "^8.1.0", "jsonwebtoken": "^9.0.2", "multer": "^1.4.5-lts.1", "pg": "^8.16.3", @@ -571,6 +573,24 @@ "url": "https://opencollective.com/express" } }, + "node_modules/express-rate-limit": { + "version": "8.2.1", + "resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-8.2.1.tgz", + "integrity": "sha512-PCZEIEIxqwhzw4KF0n7QF4QqruVTcF73O5kFKUnGOyjbCCgizBBiFaYpd/fnBLUMPw/BWw9OsiN7GgrNYr7j6g==", + "license": "MIT", + "dependencies": { + "ip-address": "10.0.1" + }, + "engines": { + "node": ">= 16" + }, + "funding": { + "url": "https://github.com/sponsors/express-rate-limit" + }, + "peerDependencies": { + "express": ">= 4.11" + } + }, "node_modules/fill-range": { "version": "7.1.1", "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", @@ -740,6 +760,15 @@ "node": ">= 0.4" } }, + "node_modules/helmet": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/helmet/-/helmet-8.1.0.tgz", + "integrity": "sha512-jOiHyAZsmnr8LqoPGmCjYAaiuWwjAPLgY8ZX2XrmHawt99/u1y6RgrZMTeoPfpUbV96HOalYgz1qzkRbw54Pmg==", + "license": "MIT", + "engines": { + "node": ">=18.0.0" + } + }, "node_modules/http-errors": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz", @@ -794,6 +823,15 @@ "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", "license": "ISC" }, + "node_modules/ip-address": { + "version": "10.0.1", + "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.0.1.tgz", + "integrity": "sha512-NWv9YLW4PoW2B7xtzaS3NCot75m6nK7Icdv0o3lfMceJVRfSoQwqD4wEH5rLwoKJwUiZ/rfpiVBhnaF0FK4HoA==", + "license": "MIT", + "engines": { + "node": ">= 12" + } + }, "node_modules/ipaddr.js": { "version": "1.9.1", "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", diff --git a/backend/package.json b/backend/package.json index 32e709c..bcf5008 100644 --- a/backend/package.json +++ b/backend/package.json @@ -13,6 +13,8 @@ "cors": "^2.8.5", "dotenv": "^16.3.1", "express": "^4.18.2", + "express-rate-limit": "^8.2.1", + "helmet": "^8.1.0", "jsonwebtoken": "^9.0.2", "multer": "^1.4.5-lts.1", "pg": "^8.16.3", diff --git a/backend/src/index.js b/backend/src/index.js index 5c8fa1b..53f1df5 100644 --- a/backend/src/index.js +++ b/backend/src/index.js @@ -1,6 +1,8 @@ require('dotenv').config(); const express = require('express'); const cors = require('cors'); +const helmet = require('helmet'); +const rateLimit = require('express-rate-limit'); const path = require('path'); const apiRouter = require('./routes/api'); const stripeRouter = require('./routes/stripe'); @@ -10,8 +12,62 @@ const adminRouter = require('./routes/admin'); const app = express(); const PORT = process.env.PORT || 3000; -// CORS -app.use(cors()); +// ============ SECURITY ============ + +// Helmet - добавляет security headers +app.use(helmet({ + contentSecurityPolicy: false // отключаем для admin panel +})); + +// CORS - разрешаем только наши домены +const allowedOrigins = [ + 'https://wellnuo.smartlaunchhub.com', + 'https://wellnuo.com', + 'http://localhost:3000', + 'http://localhost:8081', // Expo dev + 'exp://192.168.1.*' // Expo local +]; + +app.use(cors({ + origin: function(origin, callback) { + // Разрешаем запросы без origin (mobile apps, Postman) + if (!origin) return callback(null, true); + + // Проверяем разрешённые домены + if (allowedOrigins.some(allowed => { + if (allowed.includes('*')) { + const regex = new RegExp(allowed.replace('*', '.*')); + return regex.test(origin); + } + return allowed === origin; + })) { + return callback(null, true); + } + + callback(new Error('Not allowed by CORS')); + }, + credentials: true +})); + +// Rate Limiting - защита от DDoS +const limiter = rateLimit({ + windowMs: 15 * 60 * 1000, // 15 минут + max: 100, // максимум 100 запросов за 15 минут + message: { error: 'Too many requests, please try again later' }, + standardHeaders: true, + legacyHeaders: false +}); + +// Более строгий лимит для auth endpoints +const authLimiter = rateLimit({ + windowMs: 15 * 60 * 1000, + max: 5, // только 5 попыток логина за 15 минут + message: { error: 'Too many login attempts, please try again later' } +}); + +// Применяем rate limiting +app.use(limiter); +app.use('/function/well-api/api', authLimiter); // Legacy API с auth // Stripe webhooks need raw body for signature verification // Must be before express.json()