This commit is contained in:
@@ -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
@@ -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",
|
||||||
|
|||||||
Generated
+65
@@ -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",
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
Reference in New Issue
Block a user