fix(login): link status ikut setting uptime_url + batas sesi per akun

- login.ejs: pakai settings.uptime_url (fallback status_url lama), sehingga tab Status tidak lagi hardcode status.skyfiber.qzz.io
- sessionLimitService: rotasi session id saat login (anti fixation), catat sesi di tabel login_sessions, batas admin/kasir/teknisi/kolektor 2 per akun dan pelanggan 3 per akun; sesi tertua diputus dari store saat melebihi batas
- hook login admin/kasir/teknisi/kolektor/pelanggan (direct & OTP) dan logout
This commit is contained in:
oxpoe
2026-11-05 01:58:29 +07:00
parent 4003149046
commit adc3437bd5
6 changed files with 186 additions and 5 deletions

View File

@@ -57,6 +57,7 @@ const backupSvc = require('../services/backupService');
const monitoringSvc = require('../services/monitoringService');
const inventorySvc = require('../services/inventoryService');
const securityWatchdog = require('../services/securityWatchdogService');
const sessionLimit = require('../services/sessionLimitService');
const auditSvc = require('../services/auditTrailService');
const whatsappService = require('../services/whatsappService');
const { parseMikhmonOnLogin } = require('../utils/mikhmonParser');
@@ -568,8 +569,13 @@ router.post('/login', loginRateLimiter, express.urlencoded({ extended: true }),
const { username, password } = req.body;
if (username === getSetting('admin_username', 'admin') && password === getSetting('admin_password', 'demo-isp123')) {
await securityWatchdog.recordAppLoginAttempt({ type: 'admin', username, ip, success: true });
await sessionLimit.rotateSession(req);
req.session.isAdmin = true;
req.session.adminUser = username;
sessionLimit.registerLoginSession(req, {
role: 'admin', accountId: username, limit: 2,
ip, userAgent: req.get('user-agent')
});
return res.redirect('/admin');
}
@@ -577,6 +583,11 @@ router.post('/login', loginRateLimiter, express.urlencoded({ extended: true }),
const cashier = adminSvc.authenticateCashier(username, password);
if (cashier) {
await securityWatchdog.recordAppLoginAttempt({ type: 'cashier', username, ip, success: true });
await sessionLimit.rotateSession(req);
sessionLimit.registerLoginSession(req, {
role: 'cashier', accountId: cashier.id, limit: 2,
ip, userAgent: req.get('user-agent')
});
req.session.isCashier = true;
req.session.cashierId = cashier.id;
req.session.cashierName = cashier.name;
@@ -594,7 +605,11 @@ router.post('/login', loginRateLimiter, express.urlencoded({ extended: true }),
res.render('admin/login', { title: 'Admin Login', company: company(), error: 'Username atau password salah' });
});
router.get('/logout', (req, res) => { req.session.destroy(); res.redirect('/admin/login'); });
router.get('/logout', (req, res) => {
sessionLimit.removeLoginSession(req);
req.session.destroy();
res.redirect('/admin/login');
});
// ─── OLT MANAGEMENT ────────────────────────────────────────────────────────
router.get('/olts', requireAdminSession, async (req, res) => {

View File

@@ -6,6 +6,7 @@ const db = require('../config/database');
const billingSvc = require('../services/billingService');
const customerSvc = require('../services/customerService');
const securityWatchdog = require('../services/securityWatchdogService');
const sessionLimit = require('../services/sessionLimitService');
const adminSvc = require('../services/adminService');
const attendanceSvc = require('../services/attendanceService');
const pdfSvc = require('../services/pdfInvoiceService');
@@ -65,7 +66,12 @@ router.post('/login', loginRateLimiter, express.urlencoded({ extended: true }),
const collector = adminSvc.authenticateCollector(username, password);
if (collector) {
await securityWatchdog.recordAppLoginAttempt({ type: 'collector', username, ip, success: true });
await sessionLimit.rotateSession(req);
req.session.isCollector = true;
sessionLimit.registerLoginSession(req, {
role: 'collector', accountId: collector.id, limit: 2,
ip, userAgent: req.get('user-agent')
});
req.session.collectorId = collector.id;
req.session.collectorName = collector.name;
req.session.collectorUsername = collector.username;
@@ -83,6 +89,7 @@ router.post('/login', loginRateLimiter, express.urlencoded({ extended: true }),
});
router.get('/logout', (req, res) => {
sessionLimit.removeLoginSession(req);
req.session.destroy();
res.redirect('/collector/login');
});

View File

@@ -1,4 +1,5 @@
const express = require('express');
const sessionLimit = require('../services/sessionLimitService');
const router = express.Router();
const customerDevice = require('../services/customerDeviceService');
const { getSettingsWithCache, getNowLocal, getCurrentTimeInfo, getNowLocalISO, formatDateLocal, formatTimeLocal } = require('../config/settingsManager');
@@ -1791,6 +1792,14 @@ router.post('/login', loginRateLimiter, async (req, res) => {
// --- DIRECT LOGIN ---
logger.info('[Login] Login direct berhasil.');
await sessionLimit.rotateSession(req);
sessionLimit.registerLoginSession(req, {
role: 'customer',
accountId: customer ? customer.id : String(customerPhone || '').replace(/\D/g, ''),
limit: 3,
ip: req.ip || req.connection?.remoteAddress || '',
userAgent: req.get('user-agent')
});
req.session.phone = customerPhone; // Nomor telepon untuk findCustomerByAny()
req.session.pppoe_username = pppoeUsername; // PPPoE username untuk GenieACS & MikroTik
if (customer && customer.status === 'suspended') {
@@ -1805,7 +1814,7 @@ router.get('/login-otp', (req, res) => {
res.render('login_otp', { error: null, settings, phone: req.session.pending_login.phone });
});
router.post('/login-otp', loginRateLimiter, (req, res) => {
router.post('/login-otp', loginRateLimiter, async (req, res) => {
const { otp } = req.body;
const settings = getSettingsWithCache();
const pending = req.session.pending_login;
@@ -1824,6 +1833,15 @@ router.post('/login-otp', loginRateLimiter, (req, res) => {
if (otp === pending.otp) {
logger.info('[Login] OTP berhasil diverifikasi.');
await sessionLimit.rotateSession(req);
const custForLimit = customerSvc.findCustomerByAny(pending.phone);
sessionLimit.registerLoginSession(req, {
role: 'customer',
accountId: custForLimit ? custForLimit.id : String(pending.phone || '').replace(/\D/g, ''),
limit: 3,
ip: req.ip || req.connection?.remoteAddress || '',
userAgent: req.get('user-agent')
});
req.session.phone = pending.phone; // Nomor telepon customer
req.session.pppoe_username = pending.pppoeUsername; // PPPoE username untuk GenieACS & MikroTik
delete req.session.pending_login;
@@ -2591,6 +2609,7 @@ router.post('/change-tag', async (req, res) => {
});
router.post('/logout', (req, res) => {
sessionLimit.removeLoginSession(req);
req.session.destroy(() => {
res.redirect('/customer/login');
});

View File

@@ -2,6 +2,7 @@ const express = require('express');
const router = express.Router();
const techSvc = require('../services/techService');
const securityWatchdog = require('../services/securityWatchdogService');
const sessionLimit = require('../services/sessionLimitService');
const customerSvc = require('../services/customerService');
const odpSvc = require('../services/odpService');
const { getSetting, getNowLocal, getCurrentDateInTimezone, getNowLocalISO, formatDateLocal, getSettings, formatTimeLocal, parseDateInTimezone } = require('../config/settingsManager');
@@ -155,7 +156,12 @@ router.post('/login', loginRateLimiter, express.urlencoded({ extended: true }),
const tech = techSvc.authenticate(username, password);
if (tech) {
await securityWatchdog.recordAppLoginAttempt({ type: 'technician', username, ip, success: true });
await sessionLimit.rotateSession(req);
req.session.isTechnician = true;
sessionLimit.registerLoginSession(req, {
role: 'technician', accountId: tech.id, limit: 2,
ip, userAgent: req.get('user-agent')
});
req.session.techId = tech.id;
req.session.techName = tech.name;
return res.redirect('/tech');
@@ -171,6 +177,7 @@ router.post('/login', loginRateLimiter, express.urlencoded({ extended: true }),
});
router.get('/logout', (req, res) => {
sessionLimit.removeLoginSession(req);
req.session.destroy();
res.redirect('/tech/login');
});