rate-limit,cors
SAP-ERP Portal CI/CD / build (push) Successful in 2m58s

This commit is contained in:
John
2026-10-05 19:12:24 +05:30
parent eead8f5ffd
commit 1a596217b7
5 changed files with 175 additions and 3 deletions
+7
View File
@@ -1,5 +1,12 @@
PORT=5000 PORT=5000
NODE_ENV=production NODE_ENV=production
# ─── CORS ─────────────────────────────────────────────────
# Comma-separated list of EXACT origins (scheme+host+port) allowed to make
# credentialed (cookie-carrying) requests to this API. Leave blank to reflect
# any origin back (fine on a private LAN — this is today's default). Once
# this app has a real public domain, set it, e.g.:
# CORS_ALLOWED_ORIGINS=https://portal.mitraindustries.com
CORS_ALLOWED_ORIGINS=https://erp.miplapp.in
# NOTE: The following app settings moved OUT of .env and are now edited from # NOTE: The following app settings moved OUT of .env and are now edited from
# the Admin panel (System Settings tab), persisted in the app DB table # the Admin panel (System Settings tab), persisted in the app DB table
# ZAPP_SETTINGS (see services/appSettingsStore.js): BOM approval levels, skip # ZAPP_SETTINGS (see services/appSettingsStore.js): BOM approval levels, skip
+63
View File
@@ -1833,6 +1833,48 @@
"url": "https://opencollective.com/express" "url": "https://opencollective.com/express"
} }
}, },
"node_modules/express-rate-limit": {
"version": "8.7.0",
"resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-8.7.0.tgz",
"integrity": "sha512-hOwV7WOxXfjRpAM1DSJWZDXx3GhplwD8IfwuwvogD8i1Qnkgosw/H45s4ZnFAUHDAhPjlY9hLBvJhKmGMyY26g==",
"license": "MIT",
"dependencies": {
"debug": "^4.4.3",
"ip-address": "^10.2.0"
},
"engines": {
"node": ">= 16"
},
"funding": {
"url": "https://github.com/sponsors/express-rate-limit"
},
"peerDependencies": {
"express": ">= 4.11"
}
},
"node_modules/express-rate-limit/node_modules/debug": {
"version": "4.4.3",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
"integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==",
"license": "MIT",
"dependencies": {
"ms": "^2.1.3"
},
"engines": {
"node": ">=6.0"
},
"peerDependenciesMeta": {
"supports-color": {
"optional": true
}
}
},
"node_modules/express-rate-limit/node_modules/ms": {
"version": "2.1.3",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
"license": "MIT"
},
"node_modules/express-validator": { "node_modules/express-validator": {
"version": "7.3.1", "version": "7.3.1",
"resolved": "https://registry.npmjs.org/express-validator/-/express-validator-7.3.1.tgz", "resolved": "https://registry.npmjs.org/express-validator/-/express-validator-7.3.1.tgz",
@@ -2288,6 +2330,18 @@
"node": ">= 0.4" "node": ">= 0.4"
} }
}, },
"node_modules/helmet": {
"version": "8.3.0",
"resolved": "https://registry.npmjs.org/helmet/-/helmet-8.3.0.tgz",
"integrity": "sha512-Qgpiaws3Sm30Av8Eah6sjMCZZwjlBu+E68rhpCWBshY1lb09HtLwj5GviX0OyQIn+ulUS0iX0AxN5n3tLZzz1w==",
"license": "MIT",
"engines": {
"node": ">=18.0.0"
},
"funding": {
"url": "https://github.com/sponsors/EvanHahn"
}
},
"node_modules/http-errors": { "node_modules/http-errors": {
"version": "2.0.1", "version": "2.0.1",
"resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz", "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz",
@@ -2456,6 +2510,15 @@
"node": ">= 0.4" "node": ">= 0.4"
} }
}, },
"node_modules/ip-address": {
"version": "10.7.3",
"resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.7.3.tgz",
"integrity": "sha512-A1kdq/tSb5QjvKvAMgIoEvDBIgL7qaqVP/jkvSwYYRZ9iEzvPpopxp2wQfu3SuZRHtpHNxMn8Fs0bS+gf5Xmwg==",
"license": "MIT",
"engines": {
"node": ">= 12"
}
},
"node_modules/ipaddr.js": { "node_modules/ipaddr.js": {
"version": "1.9.1", "version": "1.9.1",
"resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz",
+65
View File
@@ -16,8 +16,10 @@
"dotenv": "^16.6.1", "dotenv": "^16.6.1",
"exceljs": "^4.4.0", "exceljs": "^4.4.0",
"express": "^4.18.2", "express": "^4.18.2",
"express-rate-limit": "^8.7.0",
"express-validator": "^7.0.1", "express-validator": "^7.0.1",
"form-data": "^4.0.5", "form-data": "^4.0.5",
"helmet": "^8.3.0",
"jsonwebtoken": "^9.0.2", "jsonwebtoken": "^9.0.2",
"morgan": "^1.10.0", "morgan": "^1.10.0",
"mssql": "^9.0.0", "mssql": "^9.0.0",
@@ -2285,6 +2287,48 @@
"url": "https://opencollective.com/express" "url": "https://opencollective.com/express"
} }
}, },
"node_modules/express-rate-limit": {
"version": "8.7.0",
"resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-8.7.0.tgz",
"integrity": "sha512-hOwV7WOxXfjRpAM1DSJWZDXx3GhplwD8IfwuwvogD8i1Qnkgosw/H45s4ZnFAUHDAhPjlY9hLBvJhKmGMyY26g==",
"license": "MIT",
"dependencies": {
"debug": "^4.4.3",
"ip-address": "^10.2.0"
},
"engines": {
"node": ">= 16"
},
"funding": {
"url": "https://github.com/sponsors/express-rate-limit"
},
"peerDependencies": {
"express": ">= 4.11"
}
},
"node_modules/express-rate-limit/node_modules/debug": {
"version": "4.4.3",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
"integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==",
"license": "MIT",
"dependencies": {
"ms": "^2.1.3"
},
"engines": {
"node": ">=6.0"
},
"peerDependenciesMeta": {
"supports-color": {
"optional": true
}
}
},
"node_modules/express-rate-limit/node_modules/ms": {
"version": "2.1.3",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
"license": "MIT"
},
"node_modules/express-validator": { "node_modules/express-validator": {
"version": "7.3.1", "version": "7.3.1",
"resolved": "https://registry.npmjs.org/express-validator/-/express-validator-7.3.1.tgz", "resolved": "https://registry.npmjs.org/express-validator/-/express-validator-7.3.1.tgz",
@@ -2755,6 +2799,18 @@
"node": ">= 0.4" "node": ">= 0.4"
} }
}, },
"node_modules/helmet": {
"version": "8.3.0",
"resolved": "https://registry.npmjs.org/helmet/-/helmet-8.3.0.tgz",
"integrity": "sha512-Qgpiaws3Sm30Av8Eah6sjMCZZwjlBu+E68rhpCWBshY1lb09HtLwj5GviX0OyQIn+ulUS0iX0AxN5n3tLZzz1w==",
"license": "MIT",
"engines": {
"node": ">=18.0.0"
},
"funding": {
"url": "https://github.com/sponsors/EvanHahn"
}
},
"node_modules/http-errors": { "node_modules/http-errors": {
"version": "2.0.1", "version": "2.0.1",
"resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz", "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz",
@@ -2923,6 +2979,15 @@
"node": ">= 0.4" "node": ">= 0.4"
} }
}, },
"node_modules/ip-address": {
"version": "10.7.3",
"resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.7.3.tgz",
"integrity": "sha512-A1kdq/tSb5QjvKvAMgIoEvDBIgL7qaqVP/jkvSwYYRZ9iEzvPpopxp2wQfu3SuZRHtpHNxMn8Fs0bS+gf5Xmwg==",
"license": "MIT",
"engines": {
"node": ">= 12"
}
},
"node_modules/ipaddr.js": { "node_modules/ipaddr.js": {
"version": "1.9.1", "version": "1.9.1",
"resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz",
+2
View File
@@ -16,8 +16,10 @@
"dotenv": "^16.6.1", "dotenv": "^16.6.1",
"exceljs": "^4.4.0", "exceljs": "^4.4.0",
"express": "^4.18.2", "express": "^4.18.2",
"express-rate-limit": "^8.7.0",
"express-validator": "^7.0.1", "express-validator": "^7.0.1",
"form-data": "^4.0.5", "form-data": "^4.0.5",
"helmet": "^8.3.0",
"jsonwebtoken": "^9.0.2", "jsonwebtoken": "^9.0.2",
"morgan": "^1.10.0", "morgan": "^1.10.0",
"mssql": "^9.0.0", "mssql": "^9.0.0",
+38 -3
View File
@@ -4,6 +4,8 @@
require('dotenv').config(); require('dotenv').config();
const express = require('express'); const express = require('express');
const cors = require('cors'); const cors = require('cors');
const helmet = require('helmet');
const rateLimit = require('express-rate-limit');
const path = require('path'); const path = require('path');
const jwt = require('jsonwebtoken'); const jwt = require('jsonwebtoken');
const { body, validationResult } = require('express-validator'); const { body, validationResult } = require('express-validator');
@@ -18,7 +20,40 @@ app.use(express.json({ limit: '50mb' }));
app.use(express.urlencoded({ limit: '50mb', extended: true })); app.use(express.urlencoded({ limit: '50mb', extended: true }));
// Audit trail — records every mutating /api request (must be after body parsers) // Audit trail — records every mutating /api request (must be after body parsers)
app.use(require('./middleware/auditLogger')); app.use(require('./middleware/auditLogger'));
app.use(cors({ origin: true, credentials: true })); // Baseline security headers (X-Content-Type-Options, X-Frame-Options, HSTS,
// etc.). CSP is left off — this app serves lots of inline <script> across
// ~60 hand-written pages with no nonce/hash setup, so a default CSP would
// break every page; revisit only if/when those pages are restructured.
app.use(helmet({ contentSecurityPolicy: false }));
// ── CORS allowlist ────────────────────────────────────────────────
// CORS_ALLOWED_ORIGINS in .env: comma-separated list of exact origins
// (e.g. "https://portal.mitraindustries.com,https://192.9.201.210:8001").
// Unset/empty = reflect any origin (today's behavior) — fine while this
// only runs on a private LAN. Once this has a real public domain, set
// CORS_ALLOWED_ORIGINS so credentialed requests (cookies) are only ever
// accepted from pages this app itself serves, not from any arbitrary site
// a logged-in user's browser happens to have open.
const corsAllowedOrigins = (process.env.CORS_ALLOWED_ORIGINS || '')
.split(',').map(s => s.trim()).filter(Boolean);
app.use(cors({
origin: corsAllowedOrigins.length
? (origin, cb) => cb(null, !origin || corsAllowedOrigins.includes(origin))
: true,
credentials: true,
}));
// ── Login rate limiting ───────────────────────────────────────────
// Applied to every authentication entry point (password login, SSO token
// exchange) — not the rest of the API, which already requires a valid JWT.
// 10 attempts per 15 minutes per IP is generous for a real user mistyping a
// password a few times, but shuts down automated credential-stuffing/
// brute-force against a now-internet-reachable login endpoint.
const loginLimiter = rateLimit({
windowMs: 15 * 60 * 1000,
max: 10,
standardHeaders: true,
legacyHeaders: false,
message: { success: false, message: 'Too many login attempts — please wait a few minutes and try again.' },
});
// auth-guard.js is the first <script> on every protected page. Serve it with // auth-guard.js is the first <script> on every protected page. Serve it with
// the default SAP company (from .env, via companyConfig) injected as a global // the default SAP company (from .env, via companyConfig) injected as a global
@@ -436,7 +471,7 @@ function safeSapLogins(sapLogins) {
return out; return out;
} }
authRouter.post('/login', async (req, res) => { authRouter.post('/login', loginLimiter, async (req, res) => {
const { username, password } = req.body; const { username, password } = req.body;
if (!username || !password) if (!username || !password)
return res.status(400).json({ success: false, message: 'Username and password required' }); return res.status(400).json({ success: false, message: 'Username and password required' });
@@ -480,7 +515,7 @@ authRouter.post('/login', async (req, res) => {
const CENTRAL_AUTH_API_URL = process.env.CENTRAL_AUTH_API_URL || 'http://14.99.235.114:8016'; const CENTRAL_AUTH_API_URL = process.env.CENTRAL_AUTH_API_URL || 'http://14.99.235.114:8016';
const CENTRAL_AUTH_APP_NAME = process.env.CENTRAL_AUTH_APP_NAME || 'SAP ERP'; const CENTRAL_AUTH_APP_NAME = process.env.CENTRAL_AUTH_APP_NAME || 'SAP ERP';
authRouter.post('/sso-login', async (req, res) => { authRouter.post('/sso-login', loginLimiter, async (req, res) => {
const { token } = req.body || {}; const { token } = req.body || {};
if (!token) return res.status(400).json({ success: false, message: 'Missing SSO token' }); if (!token) return res.status(400).json({ success: false, message: 'Missing SSO token' });
try { try {