feat(keamanan): security watchdog brute-force + soundbox TTS (port upstream)
- securityWatchdogService: deteksi brute force login admin/kasir/teknisi/kolektor & log MikroTik, blokir IP 15 menit, alert suara + WA/Telegram - soundboxService + voiceTtsService (TTS Google Translate, cache) - hook login admin/teknisi/kolektor, hook soundbox saat invoice lunas - route /admin/api/soundbox/poll dan /test
This commit is contained in:
@@ -56,6 +56,7 @@ const promoUpload = multer({
|
||||
const backupSvc = require('../services/backupService');
|
||||
const monitoringSvc = require('../services/monitoringService');
|
||||
const inventorySvc = require('../services/inventoryService');
|
||||
const securityWatchdog = require('../services/securityWatchdogService');
|
||||
const auditSvc = require('../services/auditTrailService');
|
||||
const whatsappService = require('../services/whatsappService');
|
||||
const { parseMikhmonOnLogin } = require('../utils/mikhmonParser');
|
||||
@@ -554,9 +555,19 @@ router.get('/login', (req, res) => {
|
||||
res.render('admin/login', { title: 'Admin Login', company: company(), error: null });
|
||||
});
|
||||
|
||||
router.post('/login', loginRateLimiter, express.urlencoded({ extended: true }), (req, res) => {
|
||||
router.post('/login', loginRateLimiter, express.urlencoded({ extended: true }), async (req, res) => {
|
||||
const ip = req.ip || req.connection?.remoteAddress || '127.0.0.1';
|
||||
if (securityWatchdog.isIpBlocked(ip)) {
|
||||
return res.render('admin/login', {
|
||||
title: 'Admin Login',
|
||||
company: company(),
|
||||
error: 'Akses login diblokir sementara karena terlalu banyak percobaan gagal. Coba lagi nanti.'
|
||||
});
|
||||
}
|
||||
|
||||
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 });
|
||||
req.session.isAdmin = true;
|
||||
req.session.adminUser = username;
|
||||
return res.redirect('/admin');
|
||||
@@ -565,6 +576,7 @@ router.post('/login', loginRateLimiter, express.urlencoded({ extended: true }),
|
||||
// Check Cashier
|
||||
const cashier = adminSvc.authenticateCashier(username, password);
|
||||
if (cashier) {
|
||||
await securityWatchdog.recordAppLoginAttempt({ type: 'cashier', username, ip, success: true });
|
||||
req.session.isCashier = true;
|
||||
req.session.cashierId = cashier.id;
|
||||
req.session.cashierName = cashier.name;
|
||||
@@ -572,6 +584,13 @@ router.post('/login', loginRateLimiter, express.urlencoded({ extended: true }),
|
||||
return res.redirect('/admin');
|
||||
}
|
||||
|
||||
await securityWatchdog.recordAppLoginAttempt({
|
||||
type: 'admin',
|
||||
username: username || '',
|
||||
ip,
|
||||
success: false,
|
||||
userAgent: req.get('user-agent')
|
||||
});
|
||||
res.render('admin/login', { title: 'Admin Login', company: company(), error: 'Username atau password salah' });
|
||||
});
|
||||
|
||||
@@ -5209,6 +5228,33 @@ router.get('/api/inventory/serials/:itemId', requireAdminSession, (req, res) =>
|
||||
}
|
||||
});
|
||||
|
||||
router.get('/api/soundbox/poll', requireAdminSession, (req, res) => {
|
||||
try {
|
||||
const soundboxSvc = require('../services/soundboxService');
|
||||
const events = soundboxSvc.getLatestEvents(Number(req.query.afterId || 0), 15);
|
||||
res.json({ success: true, events });
|
||||
} catch (err) {
|
||||
res.status(500).json({ success: false, message: err.message, events: [] });
|
||||
}
|
||||
});
|
||||
|
||||
router.post('/api/soundbox/test', requireAdminSession, express.urlencoded({ extended: true }), (req, res) => {
|
||||
try {
|
||||
const soundboxSvc = require('../services/soundboxService');
|
||||
const amount = Number(req.body?.amount || 50000);
|
||||
const id = soundboxSvc.queueSoundboxEvent({
|
||||
type: 'test',
|
||||
title: 'Uji Suara Kasir',
|
||||
speechText: req.body?.speechText || ('Uji coba speaker kasir berhasil. Pembayaran sebesar ' + soundboxSvc.formatRupiahSpoken(amount) + ' diterima.'),
|
||||
amount,
|
||||
customerName: 'Admin Soundbox'
|
||||
});
|
||||
res.json({ success: true, eventId: id });
|
||||
} catch (err) {
|
||||
res.status(500).json({ success: false, message: err.message });
|
||||
}
|
||||
});
|
||||
|
||||
router.get('/audit-logs', requireAdminSession, requireSidebarMenuAccess('audit_logs'), restrictToAdmin, (req, res) => {
|
||||
const filters = {
|
||||
action: req.query.action || null,
|
||||
|
||||
@@ -5,6 +5,7 @@ const { logger } = require('../config/logger');
|
||||
const db = require('../config/database');
|
||||
const billingSvc = require('../services/billingService');
|
||||
const customerSvc = require('../services/customerService');
|
||||
const securityWatchdog = require('../services/securityWatchdogService');
|
||||
const adminSvc = require('../services/adminService');
|
||||
const attendanceSvc = require('../services/attendanceService');
|
||||
const pdfSvc = require('../services/pdfInvoiceService');
|
||||
@@ -49,11 +50,21 @@ router.get('/login', (req, res) => {
|
||||
res.render('collector/login', { title: 'Login Kolektor', company: company(), error: null });
|
||||
});
|
||||
|
||||
router.post('/login', loginRateLimiter, express.urlencoded({ extended: true }), (req, res) => {
|
||||
router.post('/login', loginRateLimiter, express.urlencoded({ extended: true }), async (req, res) => {
|
||||
const ip = req.ip || req.connection?.remoteAddress || '127.0.0.1';
|
||||
if (securityWatchdog.isIpBlocked(ip)) {
|
||||
return res.render('collector/login', {
|
||||
title: 'Login Kolektor',
|
||||
company: company(),
|
||||
error: 'Akses login diblokir sementara karena terlalu banyak percobaan gagal. Coba lagi nanti.'
|
||||
});
|
||||
}
|
||||
|
||||
const username = String(req.body.username || '').trim();
|
||||
const password = String(req.body.password || '');
|
||||
const collector = adminSvc.authenticateCollector(username, password);
|
||||
if (collector) {
|
||||
await securityWatchdog.recordAppLoginAttempt({ type: 'collector', username, ip, success: true });
|
||||
req.session.isCollector = true;
|
||||
req.session.collectorId = collector.id;
|
||||
req.session.collectorName = collector.name;
|
||||
@@ -61,6 +72,13 @@ router.post('/login', loginRateLimiter, express.urlencoded({ extended: true }),
|
||||
req.session.collectorArea = collector.area || '';
|
||||
return res.redirect('/collector');
|
||||
}
|
||||
await securityWatchdog.recordAppLoginAttempt({
|
||||
type: 'collector',
|
||||
username,
|
||||
ip,
|
||||
success: false,
|
||||
userAgent: req.get('user-agent')
|
||||
});
|
||||
return res.render('collector/login', { title: 'Login Kolektor', company: company(), error: 'Username atau password salah!' });
|
||||
});
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
const express = require('express');
|
||||
const router = express.Router();
|
||||
const techSvc = require('../services/techService');
|
||||
const securityWatchdog = require('../services/securityWatchdogService');
|
||||
const customerSvc = require('../services/customerService');
|
||||
const odpSvc = require('../services/odpService');
|
||||
const { getSetting, getNowLocal, getCurrentDateInTimezone, getNowLocalISO, formatDateLocal, getSettings, formatTimeLocal, parseDateInTimezone } = require('../config/settingsManager');
|
||||
@@ -140,15 +141,32 @@ router.get('/login', (req, res) => {
|
||||
res.render('tech/login', { title: 'Teknisi Login', company: company(), error: null });
|
||||
});
|
||||
|
||||
router.post('/login', loginRateLimiter, express.urlencoded({ extended: true }), (req, res) => {
|
||||
router.post('/login', loginRateLimiter, express.urlencoded({ extended: true }), async (req, res) => {
|
||||
const ip = req.ip || req.connection?.remoteAddress || '127.0.0.1';
|
||||
if (securityWatchdog.isIpBlocked(ip)) {
|
||||
return res.render('tech/login', {
|
||||
title: 'Teknisi Login',
|
||||
company: company(),
|
||||
error: 'Akses login diblokir sementara karena terlalu banyak percobaan gagal. Coba lagi nanti.'
|
||||
});
|
||||
}
|
||||
|
||||
const { username, password } = req.body;
|
||||
const tech = techSvc.authenticate(username, password);
|
||||
if (tech) {
|
||||
await securityWatchdog.recordAppLoginAttempt({ type: 'technician', username, ip, success: true });
|
||||
req.session.isTechnician = true;
|
||||
req.session.techId = tech.id;
|
||||
req.session.techName = tech.name;
|
||||
return res.redirect('/tech');
|
||||
}
|
||||
await securityWatchdog.recordAppLoginAttempt({
|
||||
type: 'technician',
|
||||
username: username || '',
|
||||
ip,
|
||||
success: false,
|
||||
userAgent: req.get('user-agent')
|
||||
});
|
||||
res.render('tech/login', { title: 'Teknisi Login', company: company(), error: 'Username atau password salah!' });
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user