194 lines
6.1 KiB
JavaScript
194 lines
6.1 KiB
JavaScript
const express = require('express');
|
|
const bcrypt = require('bcryptjs');
|
|
const jwt = require('jsonwebtoken');
|
|
const { getUserByUsername, updateUserPassword, getDB } = require('../database');
|
|
const router = express.Router();
|
|
|
|
const JWT_SECRET = process.env.JWT_SECRET || 'super-secret-backone-key';
|
|
|
|
router.post('/login', (req, res) => {
|
|
const { username, password } = req.body;
|
|
if (!username || !password) {
|
|
return res.status(400).json({ error: 'Username and password are required' });
|
|
}
|
|
|
|
const user = getUserByUsername(username);
|
|
if (!user) {
|
|
return res.status(401).json({ error: 'Invalid credentials' });
|
|
}
|
|
|
|
const isValid = bcrypt.compareSync(password, user.password_hash);
|
|
if (!isValid) {
|
|
return res.status(401).json({ error: 'Invalid credentials' });
|
|
}
|
|
|
|
const token = jwt.sign(
|
|
{ id: user.id, username: user.username, role: user.role, site_uuid: user.site_uuid, agent_uuid: user.agent_uuid },
|
|
JWT_SECRET,
|
|
{ expiresIn: '1d' }
|
|
);
|
|
|
|
// Set HttpOnly cookie
|
|
res.cookie('token', token, {
|
|
httpOnly: true,
|
|
secure: process.env.NODE_ENV === 'production',
|
|
sameSite: 'strict',
|
|
maxAge: 24 * 60 * 60 * 1000 // 1 day
|
|
});
|
|
|
|
res.json({
|
|
message: 'Login successful',
|
|
user: { id: user.id, username: user.username, role: user.role, site_uuid: user.site_uuid, agent_uuid: user.agent_uuid }
|
|
});
|
|
});
|
|
|
|
router.get('/me', (req, res) => {
|
|
const token = req.cookies?.token;
|
|
if (!token) {
|
|
return res.status(401).json({ error: 'Not authenticated' });
|
|
}
|
|
|
|
try {
|
|
const decoded = jwt.verify(token, JWT_SECRET);
|
|
res.json({ user: decoded });
|
|
} catch (err) {
|
|
res.status(401).json({ error: 'Invalid token' });
|
|
}
|
|
});
|
|
|
|
router.post('/change-password', (req, res) => {
|
|
const token = req.cookies?.token;
|
|
if (!token) {
|
|
return res.status(401).json({ error: 'Not authenticated' });
|
|
}
|
|
|
|
try {
|
|
const decoded = jwt.verify(token, JWT_SECRET);
|
|
const { currentPassword, newPassword } = req.body;
|
|
|
|
if (!currentPassword || !newPassword) {
|
|
return res.status(400).json({ error: 'Current password and new password are required' });
|
|
}
|
|
|
|
// 1. Get user details from database
|
|
const user = getUserByUsername(decoded.username);
|
|
if (!user) {
|
|
return res.status(404).json({ error: 'User not found' });
|
|
}
|
|
|
|
// 2. Verify current password
|
|
const isCurrentValid = bcrypt.compareSync(currentPassword, user.password_hash);
|
|
if (!isCurrentValid) {
|
|
return res.status(400).json({ error: 'Password saat ini salah' });
|
|
}
|
|
|
|
// 3. Validate new password strength
|
|
const passwordRegex = /^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[!@#$%^&*(),.?":{}|<>]).{8,}$/;
|
|
if (!passwordRegex.test(newPassword)) {
|
|
return res.status(400).json({
|
|
error: 'Password baru tidak memenuhi kriteria: minimal 8 karakter, serta mengandung huruf besar, huruf kecil, angka, dan karakter spesial.'
|
|
});
|
|
}
|
|
|
|
// 4. Hash new password and save to DB
|
|
const newHash = bcrypt.hashSync(newPassword, 10);
|
|
updateUserPassword(user.id, newHash);
|
|
|
|
return res.json({ ok: true, message: 'Password berhasil diubah!' });
|
|
} catch (err) {
|
|
return res.status(401).json({ error: 'Invalid token' });
|
|
}
|
|
});
|
|
|
|
router.post('/logout', (req, res) => {
|
|
res.clearCookie('token');
|
|
res.json({ message: 'Logged out successfully' });
|
|
});
|
|
|
|
router.get('/geoip', async (req, res) => {
|
|
const ip = req.query.ip;
|
|
if (!ip) {
|
|
return res.status(400).json({ error: 'IP is required' });
|
|
}
|
|
|
|
const d = getDB();
|
|
try {
|
|
// 1. Check local cache
|
|
let cached = d.prepare("SELECT * FROM geoip_cache WHERE ip_address = ?").get(ip);
|
|
if (cached) {
|
|
return res.json(cached);
|
|
}
|
|
|
|
// 2. Check if private IP (IPv4 and IPv6 link local)
|
|
const parts = ip.split('.');
|
|
let isPrivate = false;
|
|
if (parts.length === 4) {
|
|
const o1 = parseInt(parts[0], 10);
|
|
const o2 = parseInt(parts[1], 10);
|
|
if (o1 === 10) isPrivate = true;
|
|
else if (o1 === 192 && o2 === 168) isPrivate = true;
|
|
else if (o1 === 172 && (o2 >= 16 && o2 <= 31)) isPrivate = true;
|
|
else if (o1 === 127) isPrivate = true;
|
|
else if (o1 === 169 && o2 === 254) isPrivate = true;
|
|
} else if (ip.startsWith('fe80:') || ip === '::1' || ip.startsWith('fd')) {
|
|
isPrivate = true;
|
|
}
|
|
|
|
if (isPrivate) {
|
|
const privateInfo = {
|
|
ip_address: ip,
|
|
isp: 'Intranet / Private Network',
|
|
country: 'Local',
|
|
city: 'Local',
|
|
as_org: 'RFC 1918 Private Range'
|
|
};
|
|
d.prepare("INSERT OR IGNORE INTO geoip_cache (ip_address, isp, country, city, as_org) VALUES (?, ?, ?, ?, ?)").run(
|
|
privateInfo.ip_address, privateInfo.isp, privateInfo.country, privateInfo.city, privateInfo.as_org
|
|
);
|
|
return res.json(privateInfo);
|
|
}
|
|
|
|
// 3. Query public GeoIP API (ip-api.com) with timeout
|
|
const controller = new AbortController();
|
|
const timeoutId = setTimeout(() => controller.abort(), 3000); // 3-second timeout
|
|
|
|
const response = await fetch(`http://ip-api.com/json/${ip}`, { signal: controller.signal });
|
|
clearTimeout(timeoutId);
|
|
|
|
const geo = await response.json();
|
|
if (geo && geo.status === 'success') {
|
|
const publicInfo = {
|
|
ip_address: ip,
|
|
isp: geo.isp || 'Unknown ISP',
|
|
country: geo.country || 'Unknown Country',
|
|
city: geo.city || 'Unknown City',
|
|
as_org: geo.as || geo.org || 'Data Center'
|
|
};
|
|
|
|
d.prepare("INSERT OR IGNORE INTO geoip_cache (ip_address, isp, country, city, as_org) VALUES (?, ?, ?, ?, ?)").run(
|
|
publicInfo.ip_address, publicInfo.isp, publicInfo.country, publicInfo.city, publicInfo.as_org
|
|
);
|
|
return res.json(publicInfo);
|
|
} else {
|
|
// Return temporary/fallback details for lookup failures without caching
|
|
return res.json({
|
|
ip_address: ip,
|
|
isp: 'Public IP',
|
|
country: 'Remote',
|
|
city: 'Remote',
|
|
as_org: 'Public Network'
|
|
});
|
|
}
|
|
} catch (err) {
|
|
return res.json({
|
|
ip_address: ip,
|
|
isp: 'Public IP',
|
|
country: 'Remote',
|
|
city: 'Remote',
|
|
as_org: 'Public Network'
|
|
});
|
|
}
|
|
});
|
|
|
|
module.exports = router;
|