feat: Guest WiFi, network intelligence, firmware orchestration, platform automation, api-ssl (v0.16.3-beta)
Guest WiFi (Hotspot): one-click captive-portal wizard that creates the guest SSID (virtual AP per radio), VLAN/bridge segregation + L3 VLAN interface, pool, DHCP, portal, rate-limited guest profile, and masquerade; vouchers with limits + printable sheets; live guest sessions; walled garden. Network intelligence: per-client device fingerprinting (vendor/hostname → category) with a persisted per-client override on the detail page; rogue/neighbor AP detection from stored AP scans (evil-twin flagging); Mist-style anomaly insights (client-count/CPU vs same-hour 14-day baseline + error bursts) in the Operations engine. Firmware orchestration: staged wave-based RouterOS rollouts (canary first) with pre-upgrade backup, reboot-ride-out, version verification, halt-on-failure, and scheduling; live per-device progress + cancel. Platform & automation: scoped API tokens (mtm_, read/write, hashed at rest, Bearer auth in requireAuth); outbound webhooks (12 events, HMAC-signed) fired from the alert pipeline + firmware orchestrator; scheduled email reports. api-ssl: RouterOSClient negotiates TLS on port 8729 and accepts RouterOS's default self-signed certs (rejectUnauthorized:false), so SSL-secured API devices connect without manual cert trust. Fixes #57. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
efb430d934
commit
9dfb8a5333
@@ -846,4 +846,41 @@ router.get('/rf/connectivity', async (req: Request, res: Response) => {
|
||||
});
|
||||
});
|
||||
|
||||
// ─── Rogue / neighbor AP detection ────────────────────────────────────────────
|
||||
|
||||
// GET /api/wireless/rf/rogue — classify latest AP scans against our own SSIDs/BSSIDs
|
||||
router.get('/rf/rogue', async (_req: Request, res: Response) => {
|
||||
const { classifyScans } = await import('../utils/rogueAp');
|
||||
|
||||
const [scans, radios, ifaceMacs] = await Promise.all([
|
||||
query<{ device_id: number; device_name: string; scanned_at: string; data: unknown }>(`
|
||||
SELECT DISTINCT ON (a.device_id) a.device_id, d.name AS device_name, a.scanned_at, a.data
|
||||
FROM ap_scan_data a JOIN devices d ON d.id = a.device_id
|
||||
ORDER BY a.device_id, a.scanned_at DESC`),
|
||||
query<{ ssid: string | null; mac_address: string | null }>(
|
||||
`SELECT ssid, mac_address FROM wireless_interfaces`),
|
||||
query<{ mac_address: string }>(
|
||||
`SELECT mac_address FROM interfaces WHERE mac_address IS NOT NULL`),
|
||||
]);
|
||||
|
||||
const ownSsids = new Set(radios.map(r => r.ssid || '').filter(Boolean));
|
||||
const ownBssids = new Set([
|
||||
...radios.map(r => (r.mac_address || '').toLowerCase()).filter(Boolean),
|
||||
...ifaceMacs.map(r => r.mac_address.toLowerCase()),
|
||||
]);
|
||||
|
||||
const records = scans.map(s => ({
|
||||
deviceName: s.device_name,
|
||||
scannedAt: String(s.scanned_at),
|
||||
networks: Array.isArray(s.data) ? s.data as import('../utils/rogueAp').ScannedNetwork[] : [],
|
||||
}));
|
||||
|
||||
const { rogues, neighbors } = classifyScans(records, ownSsids, ownBssids);
|
||||
res.json({
|
||||
rogues, neighbors,
|
||||
lastScanAt: scans.length ? scans.map(s => String(s.scanned_at)).sort().pop() : null,
|
||||
scannedDevices: scans.length,
|
||||
});
|
||||
});
|
||||
|
||||
export default router;
|
||||
|
||||
Reference in New Issue
Block a user