Aplikasi billing & manajemen ISP (fiber optic) — PWA pelanggan, portal teknisi & kolektor, admin panel lengkap, multi-vendor OLT, RADIUS, WhatsApp/Telegram, pembayaran QRIS. Kredit & lisensi (ISC): - Base kode & repo: alijayanet/billing-rtrw oleh Alijaya Net — terima kasih banyak atas base kode dan repo yang bermanfaat. - Add-on: GoWA, LibreSpeed/OpenSpeedTest, Uptime Kuma, notification-forwarder. - Dikembangkan menjadi OPEN-ISP oleh oxpoe — https://github.com/oxpoe/open-isp
18 KiB
18 KiB
IMPLEMENTATION GUIDE - SECURITY FIXES
Status: READY TO IMPLEMENT
Tanggal: 14 Agustus 2026
Target Completion: 2-3 minggu
🚀 QUICK START - CRITICAL FIXES
STEP 1: Install Required Packages
npm install bcrypt dotenv helmet express-rate-limit
npm install --save-dev @types/bcrypt
# Optional but recommended:
npm install express-validator joi
npm install better-sqlite3-session-store
STEP 2: Create .env File
# Copy template
cp .env.example .env
# Edit .env dengan credentials actual
# Edit dengan text editor
nano .env
File .env template:
# ======== SECURITY ========
NODE_ENV=development
SESSION_SECRET=GenerateRandomStringHere_$(openssl rand -hex 32)
ADMIN_PASSWORD_HASH=Will_Be_Set_By_Setup_Script
# ======== GENIEACS ========
GENIEACS_URL=http://192.168.8.189:7557
GENIEACS_USERNAME=admin
GENIEACS_PASSWORD=ChangeThisPassword
# ======== DATABASE ========
DATABASE_PATH=./database/billing.db
# ======== PAYMENT GATEWAYS ========
MIDTRANS_SERVER_KEY=Mid_server_xxxxx
MIDTRANS_MODE=sandbox
XENDIT_API_KEY=xnd_development_xxxxx
TRIPAY_API_KEY=xxxxx
TRIPAY_MERCHANT_CODE=xxxxx
TRIPAY_MODE=sandbox
DUITKU_API_KEY=xxxxx
DUITKU_MERCHANT_CODE=xxxxx
# ======== THIRD PARTY ========
DIGIFLAZZ_USERNAME=xxxxx
DIGIFLAZZ_API_KEY=xxxxx
DIGIFLAZZ_WEBHOOK_SECRET=xxxxx
# ======== WHATSAPP ========
WHATSAPP_ENABLED=true
# ======== TELEGRAM ========
TELEGRAM_ENABLED=false
TELEGRAM_BOT_TOKEN=xxxxx
# ======== RADIUS ========
RADIUS_SECRET=secret123
RADIUS_AUTH_PORT=1812
RADIUS_ACCT_PORT=1813
# ======== LOGGING ========
LOG_LEVEL=info
STEP 3: Update .gitignore
# Add these lines to .gitignore
.env
.env.local
.env.*.local
settings.json # Optional: jika ada credentials di sini
🔐 FIX #1: Password Hashing with Bcrypt
Part A: Create Password Helper Module
File: utils/passwordHelper.js
const bcrypt = require('bcrypt');
const SALT_ROUNDS = 10;
/**
* Hash password dengan bcrypt
* @param {string} plainPassword - Plain text password
* @returns {Promise<string>} Hashed password
*/
async function hashPassword(plainPassword) {
if (!plainPassword) throw new Error('Password cannot be empty');
return await bcrypt.hash(plainPassword, SALT_ROUNDS);
}
/**
* Verify password
* @param {string} plainPassword - Plain text password to verify
* @param {string} hashedPassword - Hashed password from database
* @returns {Promise<boolean>} True if password matches
*/
async function verifyPassword(plainPassword, hashedPassword) {
if (!plainPassword || !hashedPassword) return false;
return await bcrypt.compare(plainPassword, hashedPassword);
}
module.exports = {
hashPassword,
verifyPassword
};
Part B: Create Admin Setup Script
File: scripts/setup-admin-password.js
#!/usr/bin/env node
const readline = require('readline');
const db = require('../config/database');
const { hashPassword } = require('../utils/passwordHelper');
const { logger } = require('../config/logger');
const rl = readline.createInterface({
input: process.stdin,
output: process.stdout
});
const question = (prompt) => new Promise((resolve) => {
rl.question(prompt, resolve);
});
async function setupAdmin() {
console.log('\n🔐 SETUP ADMIN PASSWORD\n');
try {
// Check if admin exists
const admin = db.prepare('SELECT id FROM administrators LIMIT 1').get();
if (!admin) {
console.log('⚠️ No administrator found in database.');
console.log('Please run database migration first.\n');
rl.close();
process.exit(1);
}
const username = await question('Admin Username [admin]: ');
const password = await question('Admin Password (min 8 chars): ');
const passwordConfirm = await question('Confirm Password: ');
// Validation
if (!username.trim() || !password) {
console.log('❌ Username and password cannot be empty');
rl.close();
process.exit(1);
}
if (password !== passwordConfirm) {
console.log('❌ Passwords do not match');
rl.close();
process.exit(1);
}
if (password.length < 8) {
console.log('❌ Password must be at least 8 characters');
rl.close();
process.exit(1);
}
// Hash password
console.log('\n🔐 Hashing password...');
const hashedPassword = await hashPassword(password);
// Update database
const stmt = db.prepare('UPDATE administrators SET username = ?, password_hash = ? WHERE id = ?');
stmt.run(username.trim(), hashedPassword, admin.id);
console.log('✅ Admin password updated successfully!');
console.log(` Username: ${username.trim()}`);
console.log('\nℹ️ Please keep your password secure and do NOT share with anyone.\n');
rl.close();
process.exit(0);
} catch (err) {
logger.error('Setup failed:', err);
console.error('❌ Error:', err.message);
rl.close();
process.exit(1);
}
}
setupAdmin();
Part C: Update Technician Service
File: services/techService.js
const db = require('../config/database');
const { hashPassword, verifyPassword } = require('../utils/passwordHelper');
async function authenticate(username, password) {
const tech = db.prepare('SELECT * FROM technicians WHERE username = ? AND is_active = 1').get(username);
if (!tech) return null;
// Verify password using bcrypt
const isValid = await verifyPassword(password, tech.password_hash);
if (!isValid) return null;
// Return user data tanpa password
const { password_hash, ...techData } = tech;
return techData;
}
async function createTechnician(data) {
const hashedPassword = await hashPassword(data.password);
const stmt = db.prepare(
'INSERT INTO technicians (username, password_hash, name, phone, area) VALUES (?, ?, ?, ?, ?)'
);
return stmt.run(data.username, hashedPassword, data.name, data.phone || '', data.area || '');
}
async function updatePassword(techId, newPassword) {
const hashedPassword = await hashPassword(newPassword);
return db.prepare('UPDATE technicians SET password_hash = ? WHERE id = ?').run(hashedPassword, techId);
}
module.exports = {
authenticate,
createTechnician,
updatePassword,
// ... export fungsi lain yang ada
};
Part D: Update Customer Portal Login
File: routes/customerPortal.js (di bagian POST /login)
// Find in the file and replace the login logic:
// ❌ OLD CODE
const cust = db.prepare('SELECT * FROM customers WHERE ...')
.get(loginId, password);
// ✅ NEW CODE
const cust = db.prepare('SELECT * FROM customers WHERE login_id = ?')
.get(loginId);
if (cust) {
const { verifyPassword } = require('../utils/passwordHelper');
const isValid = await verifyPassword(password, cust.password_hash);
if (!isValid) {
return res.status(401).json({ error: 'Invalid credentials' });
}
// Regenerate session ID untuk security
req.session.regenerate((err) => {
if (err) return res.status(500).json({ error: 'Session error' });
req.session.customerId = cust.id;
res.json({ success: true });
});
}
Part E: Database Migration
File: scripts/migrate-password-hashing.js
#!/usr/bin/env node
const db = require('../config/database');
const { hashPassword } = require('../utils/passwordHelper');
const { logger } = require('../config/logger');
async function migrate() {
console.log('🔄 Starting password hashing migration...\n');
try {
// Migrate technicians
console.log('📋 Migrating technicians...');
const techs = db.prepare('SELECT id, password FROM technicians WHERE password_hash IS NULL').all();
for (const tech of techs) {
if (tech.password) {
const hashedPassword = await hashPassword(tech.password);
db.prepare('UPDATE technicians SET password_hash = ? WHERE id = ?').run(hashedPassword, tech.id);
console.log(` ✅ Tech ${tech.id}`);
}
}
// Migrate customers (if applicable)
console.log('\n📋 Migrating customers...');
const custs = db.prepare('SELECT id, password FROM customers WHERE password_hash IS NULL AND password IS NOT NULL').all();
for (const cust of custs) {
if (cust.password) {
const hashedPassword = await hashPassword(cust.password);
db.prepare('UPDATE customers SET password_hash = ? WHERE id = ?').run(hashedPassword, cust.id);
console.log(` ✅ Customer ${cust.id}`);
}
}
// Migrate administrators
console.log('\n📋 Migrating administrators...');
const admins = db.prepare('SELECT id, password FROM administrators WHERE password_hash IS NULL').all();
for (const admin of admins) {
if (admin.password) {
const hashedPassword = await hashPassword(admin.password);
db.prepare('UPDATE administrators SET password_hash = ? WHERE id = ?').run(hashedPassword, admin.id);
console.log(` ✅ Admin ${admin.id}`);
}
}
console.log('\n✅ Migration completed successfully!');
console.log('\n⚠️ IMPORTANT:');
console.log(' 1. Delete plain password columns: ALTER TABLE technicians DROP COLUMN password;');
console.log(' 2. Ensure all code uses password_hash column');
console.log(' 3. Update settings.json to not store admin password\n');
logger.info('Password hashing migration completed');
process.exit(0);
} catch (err) {
logger.error('Migration failed:', err);
console.error('❌ Error:', err.message);
process.exit(1);
}
}
migrate();
Part F: Update Database Schema
-- Add password_hash column to tables (if not exists)
ALTER TABLE technicians ADD COLUMN password_hash TEXT;
ALTER TABLE administrators ADD COLUMN password_hash TEXT;
ALTER TABLE customers ADD COLUMN password_hash TEXT;
-- After migration, drop old password columns:
-- ALTER TABLE technicians DROP COLUMN password;
-- ALTER TABLE administrators DROP COLUMN password;
-- ALTER TABLE customers DROP COLUMN password;
🗝️ FIX #2: Environment Variables & Config Management
Part A: Create .env.example
File: .env.example
# Copy this file to .env and fill in actual values
# DO NOT commit .env to git!
# ======== APPLICATION ========
NODE_ENV=development
SERVER_PORT=3001
SERVER_HOST=localhost
# ======== SECURITY ========
SESSION_SECRET=change-me-to-random-string-minimum-32-chars
COOKIE_SECURE=false # Set to true in production with HTTPS
# ======== ADMIN SETUP ========
# Set these during first-time setup, will be migrated to database
ADMIN_USERNAME=admin
ADMIN_PASSWORD=change-me-to-strong-password-min-8-chars
# ======== GENIEACS ========
GENIEACS_URL=http://192.168.8.189:7557
GENIEACS_USERNAME=admin
GENIEACS_PASSWORD=change-me
# ======== PAYMENT GATEWAYS ========
MIDTRANS_ENABLED=true
MIDTRANS_SERVER_KEY=Mid_server_xxxxx
MIDTRANS_MODE=sandbox
XENDIT_ENABLED=true
XENDIT_API_KEY=xnd_development_xxxxx
TRIPAY_ENABLED=false
TRIPAY_API_KEY=xxxxx
TRIPAY_PRIVATE_KEY=xxxxx
TRIPAY_MERCHANT_CODE=xxxxx
TRIPAY_MODE=sandbox
DUITKU_ENABLED=true
DUITKU_API_KEY=xxxxx
DUITKU_MERCHANT_CODE=xxxxx
DUITKU_MODE=sandbox
# ======== DIGIFLAZZ ========
DIGIFLAZZ_USERNAME=
DIGIFLAZZ_API_KEY=
DIGIFLAZZ_WEBHOOK_SECRET=
# ======== WHATSAPP ========
WHATSAPP_ENABLED=true
WHATSAPP_AUTH_FOLDER=auth_info_baileys
# ======== TELEGRAM ========
TELEGRAM_ENABLED=false
TELEGRAM_BOT_TOKEN=
# ======== RADIUS ========
RADIUS_ENABLED=0
RADIUS_SECRET=secret123
# ======== LOGGING ========
LOG_LEVEL=info
# ======== DATABASE ========
DATABASE_PATH=./database/billing.db
# ======== TIMEZONE ========
TIMEZONE=Asia/Jakarta
Part B: Update config/settingsManager.js
// Add at the top of settingsManager.js:
require('dotenv').config();
// Modify getSetting function to check .env first:
function getSetting(key, defaultValue = null) {
// 1. Check environment variable first (highest priority)
const envKey = key.toUpperCase().replace(/([A-Z])/g, '_$1');
if (process.env[envKey] !== undefined) {
return process.env[envKey];
}
// 2. Check settings.json
const settings = getSettings();
if (settings[key] !== undefined) {
return settings[key];
}
// 3. Return default value
return defaultValue;
}
Part C: Update .gitignore
# Add to .gitignore:
.env
.env.local
.env.*.local
.env.development.local
.env.staging.local
.env.production.local
# Optional: exclude settings.json if it contains credentials
# settings.json
# Instead: settings.json.example (commit this template)
🛡️ FIX #3: Enhanced CSRF Protection
Part A: Install CSURF
npm install csurf
Part B: Add CSRF to app-customer.js
const csrf = require('csurf');
// Create CSRF protection middleware
const csrfProtection = csrf({
cookie: false // Use session instead of cookie
});
// Apply to POST/PUT/DELETE routes
app.post('/admin/:path*', csrfProtection, adminRoutes);
app.post('/customer/:path*', csrfProtection, customerRoutes);
Part C: Add CSRF Token to Forms
<!-- In EJS templates, add hidden CSRF token field: -->
<form method="POST" action="/admin/billing/generate">
<input type="hidden" name="_csrf" value="<%= csrfToken %>">
<!-- Other form fields -->
<button type="submit">Generate Invoice</button>
</form>
<!-- For AJAX requests, include in headers: -->
<script>
const csrfToken = document.querySelector('meta[name="csrf-token"]').content;
fetch('/api/action', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRF-Token': csrfToken
},
body: JSON.stringify(data)
});
</script>
🚨 FIX #4: Rate Limiting
Part A: Install & Setup
npm install express-rate-limit
Part B: Create Rate Limiter Middleware
File: middleware/rateLimiter.js
const rateLimit = require('express-rate-limit');
const loginRateLimiter = rateLimit({
windowMs: 15 * 60 * 1000, // 15 minutes
max: 5, // Limit each IP to 5 requests per windowMs
message: 'Terlalu banyak percobaan login, coba lagi dalam beberapa menit',
standardHeaders: true,
legacyHeaders: false,
});
const apiRateLimiter = rateLimit({
windowMs: 1 * 60 * 1000, // 1 minute
max: 100, // 100 requests per minute
message: 'Terlalu banyak request ke API, coba lagi nanti'
});
const webhookRateLimiter = rateLimit({
windowMs: 1 * 60 * 1000,
max: 1000, // Higher limit for webhooks
skip: (req) => {
// Skip rate limiting if webhook signature is valid
return req.session?.webhookVerified;
}
});
module.exports = {
loginRateLimiter,
apiRateLimiter,
webhookRateLimiter
};
Part C: Apply Rate Limiters
// In routes
const { loginRateLimiter, apiRateLimiter } = require('../middleware/rateLimiter');
// Apply to login endpoints
router.post('/login', loginRateLimiter, loginController);
router.post('/customer/login', loginRateLimiter, customerLoginController);
// Apply to API endpoints
router.get('/api/data', apiRateLimiter, apiController);
🔒 FIX #5: Session Management Improvements
Part A: Setup Session Store
npm install better-sqlite3-session-store
Part B: Update app-customer.js
const SessionStore = require('better-sqlite3-session-store')(session);
const store = new SessionStore({
client: db,
expired: {
clear: true,
intervalMs: 15 * 60 * 1000 // Clear expired sessions every 15 minutes
}
});
app.use(session({
store: store, // Use database store instead of memory
secret: process.env.SESSION_SECRET || getSetting('session_secret'),
resave: false,
saveUninitialized: false,
cookie: {
secure: process.env.COOKIE_SECURE === 'true',
httpOnly: true,
sameSite: 'strict', // More restrictive
maxAge: 24 * 60 * 60 * 1000 // 24 hours
},
name: 'customer.sid'
}));
Part C: Session Regeneration on Login
// After successful login:
req.session.regenerate((err) => {
if (err) {
return res.status(500).json({ error: 'Session error' });
}
req.session.userId = user.id;
req.session.userRole = user.role;
req.session.loginTime = Date.now();
// Set inactivity timeout
req.session.inactivityTimeout = 30 * 60 * 1000; // 30 minutes
res.json({ success: true, redirect: '/dashboard' });
});
✅ VERIFICATION CHECKLIST
After implementing all fixes, verify:
- bcrypt password hashing implemented
- Old plain passwords migrated to hashed
- .env file created and populated
- .env added to .gitignore
- Settings.json reviewed for sensitive data
- CSRF protection implemented
- Rate limiting configured
- Session store using database
- Session regeneration on login working
- All tests passing
- Application running without errors
🧪 TESTING FIXES
Test Password Hashing
node -e "
const { hashPassword, verifyPassword } = require('./utils/passwordHelper');
(async () => {
const hash = await hashPassword('test123456');
const valid = await verifyPassword('test123456', hash);
console.log('Hash:', hash);
console.log('Valid:', valid);
})();
"
Test Environment Variables
node -e "
require('dotenv').config();
console.log('NODE_ENV:', process.env.NODE_ENV);
console.log('SESSION_SECRET:', process.env.SESSION_SECRET ? '✅ Set' : '❌ Not set');
console.log('GENIEACS_PASSWORD:', process.env.GENIEACS_PASSWORD ? '✅ Set' : '❌ Not set');
"
Test Login Flow
# Start application
npm start
# Open browser and test login with new credentials
# Check console logs for any errors
# Verify session is created in database
📞 TROUBLESHOOTING
Issue: "bcrypt is not defined"
Solution: npm install bcrypt
Issue: ".env file not loading"
Solution: Ensure dotenv is imported at the top of app-customer.js:
require('dotenv').config();
Issue: "CSRF token mismatch"
Solution: Ensure csrfToken is included in all forms and AJAX requests
Issue: "Session not persisting"
Solution: Verify database schema has session table:
sqlite3 database/billing.db ".schema sessions"
📝 NEXT STEPS
-
Immediate (Today):
- Review this guide with team
- Create .env file
- Test bcrypt locally
-
This Week:
- Implement bcrypt in all auth modules
- Run password migration script
- Deploy to staging for testing
-
Next Week:
- Implement CSRF protection
- Add rate limiting
- Full security testing
-
Following Week:
- Monitor production (if live)
- Collect logs and monitoring data
- Review and optimize
Questions? Review AUDIT_SECURITY_2026.md for detailed security analysis.