@@ -42,15 +42,20 @@ app.get('/company-config.js', (req, res) => {
|
||||
|
||||
app.use(express.static(path.join(__dirname, 'public')));
|
||||
|
||||
// Extension-less page routes (/work-order, /admin, etc. below) always
|
||||
// revalidate with the server instead of the browser quietly reusing an
|
||||
// old cached copy — a server restart never clears the browser's own cache,
|
||||
// so an edited page could otherwise keep serving stale pre-fix HTML/JS
|
||||
// indefinitely until someone manually clears site data. Setting this BEFORE
|
||||
// res.sendFile() runs sticks: Express's send() only applies its own default
|
||||
// Cache-Control when one isn't already present on the response.
|
||||
// Extension-less page routes (/work-order, /admin, etc. below) never get
|
||||
// cached by the browser at all — a server restart never clears the
|
||||
// browser's own cache, so an edited page could otherwise keep serving stale
|
||||
// pre-fix HTML/JS indefinitely until someone manually clears site data.
|
||||
// 'no-cache' alone (revalidate-before-use) turned out not to be enough in
|
||||
// practice — repeatedly observed serving a stale page even right after a
|
||||
// fix landed and the server restarted, on pages with no version query
|
||||
// string to force a new cache entry (sidebar.js needed a manual ?v= bump
|
||||
// for the exact same reason). 'no-store' is unambiguous: never cache this
|
||||
// response at all, full stop. Setting this BEFORE res.sendFile() runs
|
||||
// sticks: Express's send() only applies its own default Cache-Control when
|
||||
// one isn't already present on the response.
|
||||
app.use((req, res, next) => {
|
||||
if (req.method === 'GET' && !path.extname(req.path)) res.set('Cache-Control', 'no-cache');
|
||||
if (req.method === 'GET' && !path.extname(req.path)) res.set('Cache-Control', 'no-store, no-cache, must-revalidate');
|
||||
next();
|
||||
});
|
||||
|
||||
@@ -116,6 +121,20 @@ app.get('/work-order-print', (req, res) => res.sendFile(path.join(__dirname, 'pu
|
||||
app.get('/audit-logs', (req, res) => res.sendFile(path.join(__dirname, 'public', 'audit-logs.html')));
|
||||
app.get('/ppc-report', (req, res) => res.sendFile(path.join(__dirname, 'public', 'ppc-report.html')));
|
||||
app.get('/pwo-source-audit', (req, res) => res.sendFile(path.join(__dirname, 'public', 'pwo-source-audit.html')));
|
||||
app.get('/inventory-status-report', (req, res) => res.sendFile(path.join(__dirname, 'public', 'inventory-status-report.html')));
|
||||
app.get('/batch-no-transaction', (req, res) => res.sendFile(path.join(__dirname, 'public', 'batch-no-transaction.html')));
|
||||
app.get('/inventory-transfer', (req, res) => res.sendFile(path.join(__dirname, 'public', 'inventory-transfer.html')));
|
||||
// Central auth appends a fixed "sso/login" suffix to this app's registered
|
||||
// app_url when redirecting back after login (confirmed against the QMS
|
||||
// portal's own working integration, which registers its app_url ending in
|
||||
// "/login/" and receives the browser at ".../login/sso/login") — so the
|
||||
// actual landing path is /sso/login, not /sso-login. Both are kept so a
|
||||
// manually-typed /sso-login link (e.g. for testing) still works.
|
||||
app.get('/sso/login', (req, res) => res.sendFile(path.join(__dirname, 'public', 'sso-login.html')));
|
||||
app.get('/sso-login', (req, res) => res.sendFile(path.join(__dirname, 'public', 'sso-login.html')));
|
||||
app.get('/inventory-posting-list', (req, res) => res.sendFile(path.join(__dirname, 'public', 'inventory-posting-list.html')));
|
||||
app.get('/downtime-analysis', (req, res) => res.sendFile(path.join(__dirname, 'public', 'downtime-analysis.html')));
|
||||
app.get('/deviations', (req, res) => res.sendFile(path.join(__dirname, 'public', 'deviations.html')));
|
||||
app.get('/mail-logs', (req, res) => res.sendFile(path.join(__dirname, 'public', 'mail-logs.html')));
|
||||
app.get('/guide', (req, res) => res.sendFile(path.join(__dirname, 'public', 'guide.html')));
|
||||
app.get('/verify-production',(req, res) => res.sendFile(path.join(__dirname, 'public', 'verify-production.html')));
|
||||
@@ -152,6 +171,13 @@ app.get('/admin', (req, res) => res.sendFile(path.join(__dirname, 'pub
|
||||
app.get('/item-group-classification', (req, res) => res.sendFile(path.join(__dirname, 'public', 'item-group-classification.html')));
|
||||
app.get('/business-master', (req, res) => res.sendFile(path.join(__dirname, 'public', 'business-master.html')));
|
||||
app.get('/profile', (req, res) => res.sendFile(path.join(__dirname, 'public', 'profile.html')));
|
||||
// Sales Order module (replaces the msale portal) — employee pages + the
|
||||
// separate customer portal (customers log in with their SAP customer code).
|
||||
app.get('/sales-orders', (req, res) => res.sendFile(path.join(__dirname, 'public', 'sales-orders.html')));
|
||||
app.get('/sales-samples', (req, res) => res.sendFile(path.join(__dirname, 'public', 'sales-samples.html')));
|
||||
app.get('/sales-reports', (req, res) => res.sendFile(path.join(__dirname, 'public', 'sales-reports.html')));
|
||||
app.get('/sales-admin', (req, res) => res.sendFile(path.join(__dirname, 'public', 'sales-admin.html')));
|
||||
app.get('/customer-portal', (req, res) => res.sendFile(path.join(__dirname, 'public', 'customer-portal.html')));
|
||||
app.get('/', (req, res) => res.sendFile(path.join(__dirname, 'public', 'index.html')));
|
||||
|
||||
// ════════════════════════════════════════════════════════════════
|
||||
@@ -283,6 +309,7 @@ app.get('/api/config', (req, res) => {
|
||||
woWeighingBalanceIds: appSettings.woWeighingBalanceIds(),
|
||||
woCustomQtyUnits: appSettings.woCustomQtyUnits(),
|
||||
woRawPotencyFactors: appSettings.woRawPotencyFactors(),
|
||||
woRawQtyIssuedSource: appSettings.woRawQtyIssuedSource(),
|
||||
woSolutionBatchMaxEditLtr: appSettings.woSolutionBatchMaxEditLtr(),
|
||||
woSolutionBatchMaxEditLtrPD: appSettings.woSolutionBatchMaxEditLtrPD(),
|
||||
woSolutionBatchRoundLtr: appSettings.woSolutionBatchRoundLtr(),
|
||||
@@ -420,6 +447,7 @@ authRouter.post('/login', async (req, res) => {
|
||||
if (!user) return res.status(401).json({ success: false, message: 'Invalid username or password' });
|
||||
const ok = await userDb.verifyPassword(password, user.passwordHash);
|
||||
if (!ok) return res.status(401).json({ success: false, message: 'Invalid username or password' });
|
||||
await userDb.upgradeLegacyPassword(user.id, password, user.passwordHash); // msale-migrated MD5 → bcrypt
|
||||
await userDb.touchLastLogin(user.id);
|
||||
// Per-user, per-company SAP logins (SAP password AES-encrypted) ride in
|
||||
// the JWT so downstream SAP calls act as this user — see the per-request
|
||||
@@ -435,6 +463,56 @@ authRouter.post('/login', async (req, res) => {
|
||||
}
|
||||
});
|
||||
|
||||
// ── POST /api/auth/sso-login — Mitra Industry central auth SSO ─────────────
|
||||
// Central auth (a separate Django service) is the entry point: a user logs
|
||||
// in there, picks this app from their dashboard, and central auth redirects
|
||||
// the browser to /sso/login on this app with a short-lived access token in
|
||||
// the URL. That token carries NO email itself (it's a bare SimpleJWT access
|
||||
// token — just token_type/exp/iat/jti/user_id), so it can't be trusted
|
||||
// locally; instead we call central auth's own API server-to-server to
|
||||
// resolve it — same proven pattern as the QMS portal's working integration
|
||||
// (D:\Claude_projects\qms\server\controllers\userController.js ssoLogin).
|
||||
// Central auth returns the authenticated user's email among other fields —
|
||||
// that's the ONLY identity taken from it. Unlike QMS, we do NOT auto-create
|
||||
// accounts: SAP per-user credentials/approval steps/module access must
|
||||
// already be deliberately provisioned by an admin, so an unmatched email is
|
||||
// rejected with a clear message instead of silently creating a blank user.
|
||||
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';
|
||||
|
||||
authRouter.post('/sso-login', async (req, res) => {
|
||||
const { token } = req.body || {};
|
||||
if (!token) return res.status(400).json({ success: false, message: 'Missing SSO token' });
|
||||
try {
|
||||
const userDb = getUsers();
|
||||
if (!userDb) return res.status(503).json({ success: false, message: 'Auth service not ready' });
|
||||
|
||||
const verifyUrl = `${CENTRAL_AUTH_API_URL}/accounts/auth-subapp/?app_name=${encodeURIComponent(CENTRAL_AUTH_APP_NAME)}`;
|
||||
const resp = await fetch(verifyUrl, { headers: { Authorization: `Bearer ${token}` } });
|
||||
if (!resp.ok) {
|
||||
const body = await resp.text().catch(() => '');
|
||||
console.warn('[AUTH] SSO verify failed:', resp.status, body.slice(0, 300));
|
||||
return res.status(401).json({ success: false, message: 'Central auth could not verify this login — please try logging in again from the central auth dashboard.' });
|
||||
}
|
||||
const data = await resp.json();
|
||||
const email = (data?.user?.email || '').trim();
|
||||
if (!email) return res.status(401).json({ success: false, message: 'Central auth did not return an email for this account.' });
|
||||
|
||||
const user = await userDb.findByEmail(email);
|
||||
if (!user) return res.status(403).json({ success: false, message: `No portal account found for ${email}. Ask your admin to create one with this exact email before using central auth login.` });
|
||||
|
||||
await userDb.touchLastLogin(user.id);
|
||||
const sapLogins = await userDb.getSapLoginMapRaw(user.id);
|
||||
const payload = buildAuthPayload(user, sapLogins);
|
||||
const jwtToken = jwt.sign(payload, SECRET, { expiresIn: '12h' });
|
||||
const { sapLogins: _sl, ...safeUser } = payload;
|
||||
res.json({ success: true, token: jwtToken, user: { ...safeUser, sapLogins: safeSapLogins(sapLogins) } });
|
||||
} catch (err) {
|
||||
console.error('[AUTH] SSO login error:', err.message);
|
||||
res.status(500).json({ success: false, message: 'Central auth login failed: ' + err.message });
|
||||
}
|
||||
});
|
||||
|
||||
authRouter.get('/me', (req, res) => {
|
||||
const token = (req.headers.authorization || '').replace('Bearer ', '');
|
||||
if (!token) return res.status(401).json({ success: false });
|
||||
@@ -1145,6 +1223,10 @@ app.use('/api/chat', require('./routes/chatbot'));
|
||||
app.use('/api/reports', require('./routes/reports'));
|
||||
app.use('/api/ppc', require('./routes/ppc'));
|
||||
app.use('/api/pwo-source-audit', require('./routes/pwoSourceAudit'));
|
||||
app.use('/api/inventory-status-report', require('./routes/inventoryStatusReport'));
|
||||
app.use('/api/batch-no-transaction', require('./routes/batchNoTransaction'));
|
||||
app.use('/api/inventory-transfer', require('./routes/inventoryTransfer'));
|
||||
app.use('/api/inventory-posting-list', require('./routes/inventoryPostingList'));
|
||||
app.use('/api/general-ledger', require('./routes/generalLedger'));
|
||||
|
||||
// ITEMS
|
||||
@@ -1172,10 +1254,14 @@ app.use('/api/production-planning', require('./routes/productionPlanning'));
|
||||
app.use('/api/password-reset', require('./routes/passwordReset'));
|
||||
app.use('/api/notifications', require('./routes/notifications'));
|
||||
app.use('/api/settings', require('./routes/appSettings'));
|
||||
app.use('/api/manpower', require('./routes/manpower'));
|
||||
app.use('/api/manpower', require('./routes/manPower'));
|
||||
app.use('/api/oee', require('./routes/oee'));
|
||||
app.use('/api/downtime-analysis', require('./routes/downtimeAnalysis'));
|
||||
app.use('/api/audit', require('./routes/audit'));
|
||||
app.use('/api/mail-logs', require('./routes/mailLogs'));
|
||||
app.use('/api/sales', require('./routes/sales'));
|
||||
app.use('/api/sales-portal', require('./routes/salesPortal'));
|
||||
app.use('/api/sales-ext', require('./routes/salesExt'));
|
||||
|
||||
// ── Warehouse → Transaction Type mapping (Receipt from Production) ──────────
|
||||
const whTranTypeStore = () => require('./services/warehouseTranTypeStore');
|
||||
@@ -1588,11 +1674,30 @@ async function runBootstrap() {
|
||||
await require('./services/oeeStore').bootstrap();
|
||||
await require('./services/auditStore').bootstrap();
|
||||
await require('./services/mailLogStore').bootstrap();
|
||||
await require('./services/shortStockAlertStore').bootstrap();
|
||||
await require('./services/sales/schema').bootstrap();
|
||||
await appSettings.bootstrap();
|
||||
console.log(require('./services/mailer').isConfigured()
|
||||
? '[MAILER] SMTP configured — stage-change emails enabled'
|
||||
: '[MAILER] SMTP not configured (.env SMTP_HOST) — stage-change emails will be skipped');
|
||||
|
||||
// Short in Stock — Auto Email Alerts: an independent background poll,
|
||||
// not tied to any user request. Runs once shortly after boot (so a
|
||||
// watch-list defined before a restart doesn't wait a full interval to
|
||||
// first fire), then every 15 minutes. checkShortStock() itself no-ops
|
||||
// instantly when the feature is off or has no rules defined, so this is
|
||||
// cheap to just always have running.
|
||||
setTimeout(() => require('./services/shortStockAlertStore').checkShortStock(), 30000);
|
||||
setInterval(() => require('./services/shortStockAlertStore').checkShortStock(), 15 * 60 * 1000);
|
||||
|
||||
// Sales Orders ↔ SAP: link orders already present in SAP (status 7 → 8)
|
||||
// and close fully-invoiced ones (8 → 9). Replaces msale's external sync job.
|
||||
const salesSync = () => require('./services/sales/orders').syncWithSap()
|
||||
.then(s => { if (s && (s.linked || s.closed || s.invoiceEmails)) console.log(`[SALES] SAP sync: ${s.linked} linked, ${s.closed} closed, ${s.invoiceEmails || 0} invoice email(s)`); })
|
||||
.catch(e => console.warn('[SALES] SAP sync failed:', e.message));
|
||||
setTimeout(salesSync, 60000);
|
||||
setInterval(salesSync, 15 * 60 * 1000); // also drives the "invoice raised" emails
|
||||
|
||||
global._hanaReady = true;
|
||||
console.log('Database ready — all features available\n');
|
||||
} catch (err) {
|
||||
|
||||
Reference in New Issue
Block a user