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>
559 lines
22 KiB
TypeScript
559 lines
22 KiB
TypeScript
import { pool } from '../config/database';
|
|
import bcrypt from 'bcryptjs';
|
|
|
|
const MIGRATION_SQL = `
|
|
-- Users
|
|
CREATE TABLE IF NOT EXISTS users (
|
|
id SERIAL PRIMARY KEY,
|
|
username VARCHAR(50) NOT NULL UNIQUE,
|
|
password_hash VARCHAR(255) NOT NULL,
|
|
role VARCHAR(20) NOT NULL DEFAULT 'admin',
|
|
created_at TIMESTAMPTZ DEFAULT NOW()
|
|
);
|
|
|
|
-- Devices
|
|
CREATE TABLE IF NOT EXISTS devices (
|
|
id SERIAL PRIMARY KEY,
|
|
name VARCHAR(100) NOT NULL,
|
|
ip_address VARCHAR(45) NOT NULL,
|
|
api_port INTEGER NOT NULL DEFAULT 8728,
|
|
ssh_port INTEGER NOT NULL DEFAULT 22,
|
|
api_username VARCHAR(50) NOT NULL,
|
|
api_password_encrypted TEXT NOT NULL,
|
|
ssh_username VARCHAR(50),
|
|
ssh_password_encrypted TEXT,
|
|
model VARCHAR(100),
|
|
serial_number VARCHAR(50),
|
|
firmware_version VARCHAR(50),
|
|
ros_version VARCHAR(20),
|
|
device_type VARCHAR(20) DEFAULT 'router',
|
|
status VARCHAR(20) DEFAULT 'unknown',
|
|
last_seen TIMESTAMPTZ,
|
|
notes TEXT,
|
|
created_at TIMESTAMPTZ DEFAULT NOW(),
|
|
updated_at TIMESTAMPTZ DEFAULT NOW()
|
|
);
|
|
|
|
-- Device full config snapshots
|
|
CREATE TABLE IF NOT EXISTS device_configs (
|
|
id SERIAL PRIMARY KEY,
|
|
device_id INTEGER NOT NULL REFERENCES devices(id) ON DELETE CASCADE,
|
|
config_json JSONB NOT NULL,
|
|
collected_at TIMESTAMPTZ DEFAULT NOW()
|
|
);
|
|
CREATE INDEX IF NOT EXISTS idx_device_configs_device ON device_configs(device_id, collected_at DESC);
|
|
|
|
-- Network interfaces
|
|
CREATE TABLE IF NOT EXISTS interfaces (
|
|
id SERIAL PRIMARY KEY,
|
|
device_id INTEGER NOT NULL REFERENCES devices(id) ON DELETE CASCADE,
|
|
name VARCHAR(50) NOT NULL,
|
|
type VARCHAR(30),
|
|
mac_address VARCHAR(17),
|
|
mtu INTEGER,
|
|
running BOOLEAN DEFAULT FALSE,
|
|
disabled BOOLEAN DEFAULT FALSE,
|
|
comment TEXT,
|
|
speed VARCHAR(20),
|
|
full_duplex BOOLEAN,
|
|
config_json JSONB,
|
|
updated_at TIMESTAMPTZ DEFAULT NOW(),
|
|
UNIQUE(device_id, name)
|
|
);
|
|
|
|
-- VLANs
|
|
CREATE TABLE IF NOT EXISTS vlans (
|
|
id SERIAL PRIMARY KEY,
|
|
device_id INTEGER NOT NULL REFERENCES devices(id) ON DELETE CASCADE,
|
|
vlan_id INTEGER NOT NULL,
|
|
name VARCHAR(100),
|
|
bridge VARCHAR(50),
|
|
tagged_ports TEXT[],
|
|
untagged_ports TEXT[],
|
|
config_json JSONB,
|
|
UNIQUE(device_id, vlan_id)
|
|
);
|
|
|
|
-- Bridge VLAN table entries (for switch port VLAN mapping)
|
|
CREATE TABLE IF NOT EXISTS bridge_vlan_entries (
|
|
id SERIAL PRIMARY KEY,
|
|
device_id INTEGER NOT NULL REFERENCES devices(id) ON DELETE CASCADE,
|
|
bridge VARCHAR(50) NOT NULL,
|
|
port VARCHAR(50) NOT NULL,
|
|
vlan_ids TEXT[],
|
|
pvid INTEGER,
|
|
tagged BOOLEAN DEFAULT FALSE,
|
|
config_json JSONB,
|
|
updated_at TIMESTAMPTZ DEFAULT NOW(),
|
|
UNIQUE(device_id, bridge, port)
|
|
);
|
|
|
|
-- Network clients (ARP/DHCP/wireless leases)
|
|
CREATE TABLE IF NOT EXISTS clients (
|
|
id SERIAL PRIMARY KEY,
|
|
device_id INTEGER NOT NULL REFERENCES devices(id) ON DELETE CASCADE,
|
|
mac_address VARCHAR(17) NOT NULL,
|
|
hostname VARCHAR(255),
|
|
ip_address VARCHAR(45),
|
|
interface_name VARCHAR(50),
|
|
tx_bytes BIGINT DEFAULT 0,
|
|
rx_bytes BIGINT DEFAULT 0,
|
|
signal_strength INTEGER,
|
|
comment TEXT,
|
|
client_type VARCHAR(20) DEFAULT 'wired',
|
|
active BOOLEAN DEFAULT FALSE,
|
|
last_seen TIMESTAMPTZ,
|
|
UNIQUE(device_id, mac_address)
|
|
);
|
|
CREATE INDEX IF NOT EXISTS idx_clients_active ON clients(active, last_seen DESC);
|
|
CREATE INDEX IF NOT EXISTS idx_clients_mac ON clients(mac_address);
|
|
|
|
-- Events and alerts
|
|
CREATE TABLE IF NOT EXISTS events (
|
|
id SERIAL PRIMARY KEY,
|
|
device_id INTEGER REFERENCES devices(id) ON DELETE CASCADE,
|
|
event_time TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
|
severity VARCHAR(20) NOT NULL DEFAULT 'info',
|
|
topic VARCHAR(100),
|
|
message TEXT NOT NULL,
|
|
raw_json JSONB
|
|
);
|
|
CREATE INDEX IF NOT EXISTS idx_events_device_time ON events(device_id, event_time DESC);
|
|
CREATE INDEX IF NOT EXISTS idx_events_time ON events(event_time DESC);
|
|
CREATE INDEX IF NOT EXISTS idx_events_severity ON events(severity, event_time DESC);
|
|
|
|
-- Backups
|
|
CREATE TABLE IF NOT EXISTS backups (
|
|
id SERIAL PRIMARY KEY,
|
|
device_id INTEGER NOT NULL REFERENCES devices(id) ON DELETE CASCADE,
|
|
filename VARCHAR(255) NOT NULL,
|
|
file_path VARCHAR(500) NOT NULL,
|
|
size_bytes INTEGER,
|
|
backup_type VARCHAR(20) DEFAULT 'manual',
|
|
notes TEXT,
|
|
created_at TIMESTAMPTZ DEFAULT NOW()
|
|
);
|
|
|
|
-- Topology links (discovered via LLDP/CDP/neighbor)
|
|
CREATE TABLE IF NOT EXISTS topology_links (
|
|
id SERIAL PRIMARY KEY,
|
|
from_device_id INTEGER REFERENCES devices(id) ON DELETE CASCADE,
|
|
from_interface VARCHAR(50),
|
|
to_device_id INTEGER REFERENCES devices(id) ON DELETE SET NULL,
|
|
to_interface VARCHAR(50),
|
|
neighbor_address VARCHAR(45),
|
|
neighbor_identity VARCHAR(255),
|
|
neighbor_platform VARCHAR(255),
|
|
link_type VARCHAR(20) DEFAULT 'lldp',
|
|
discovered_at TIMESTAMPTZ DEFAULT NOW(),
|
|
UNIQUE(from_device_id, from_interface)
|
|
);
|
|
|
|
-- Application settings
|
|
CREATE TABLE IF NOT EXISTS app_settings (
|
|
key VARCHAR(100) PRIMARY KEY,
|
|
value JSONB NOT NULL,
|
|
updated_at TIMESTAMPTZ DEFAULT NOW()
|
|
);
|
|
|
|
-- Incremental schema updates
|
|
ALTER TABLE clients ADD COLUMN IF NOT EXISTS vendor VARCHAR(255);
|
|
ALTER TABLE clients ADD COLUMN IF NOT EXISTS vlan_id INTEGER;
|
|
ALTER TABLE clients ADD COLUMN IF NOT EXISTS custom_name VARCHAR(255);
|
|
-- first_seen: when this client was first discovered ("connected since"); set once, preserved across polls
|
|
ALTER TABLE clients ADD COLUMN IF NOT EXISTS first_seen TIMESTAMPTZ;
|
|
UPDATE clients SET first_seen = last_seen WHERE first_seen IS NULL;
|
|
-- custom_category: user override of the fingerprinted device category
|
|
ALTER TABLE clients ADD COLUMN IF NOT EXISTS custom_category VARCHAR(30);
|
|
|
|
-- Firmware orchestration: staged fleet upgrades in waves
|
|
CREATE TABLE IF NOT EXISTS firmware_rollouts (
|
|
id SERIAL PRIMARY KEY,
|
|
name VARCHAR(100) NOT NULL,
|
|
status VARCHAR(20) NOT NULL DEFAULT 'pending', -- pending|running|completed|failed|cancelled
|
|
halt_on_failure BOOLEAN NOT NULL DEFAULT TRUE,
|
|
pre_backup BOOLEAN NOT NULL DEFAULT TRUE,
|
|
scheduled_at TIMESTAMPTZ,
|
|
started_at TIMESTAMPTZ,
|
|
finished_at TIMESTAMPTZ,
|
|
created_at TIMESTAMPTZ DEFAULT NOW()
|
|
);
|
|
CREATE TABLE IF NOT EXISTS firmware_rollout_devices (
|
|
id SERIAL PRIMARY KEY,
|
|
rollout_id INTEGER NOT NULL REFERENCES firmware_rollouts(id) ON DELETE CASCADE,
|
|
device_id INTEGER NOT NULL REFERENCES devices(id) ON DELETE CASCADE,
|
|
wave INTEGER NOT NULL DEFAULT 1,
|
|
status VARCHAR(20) NOT NULL DEFAULT 'pending', -- pending|backing_up|upgrading|rebooting|verifying|success|failed|skipped
|
|
from_version VARCHAR(30),
|
|
to_version VARCHAR(30),
|
|
error TEXT,
|
|
started_at TIMESTAMPTZ,
|
|
finished_at TIMESTAMPTZ
|
|
);
|
|
CREATE INDEX IF NOT EXISTS idx_fw_rollout_devices ON firmware_rollout_devices(rollout_id, wave, id);
|
|
|
|
-- Platform & automation: scoped API tokens, outbound webhooks, scheduled reports
|
|
CREATE TABLE IF NOT EXISTS api_tokens (
|
|
id SERIAL PRIMARY KEY,
|
|
name VARCHAR(100) NOT NULL,
|
|
token_hash VARCHAR(64) NOT NULL UNIQUE,
|
|
prefix VARCHAR(12) NOT NULL,
|
|
scope VARCHAR(10) NOT NULL DEFAULT 'read', -- read|write
|
|
created_by VARCHAR(50),
|
|
last_used_at TIMESTAMPTZ,
|
|
expires_at TIMESTAMPTZ,
|
|
created_at TIMESTAMPTZ DEFAULT NOW()
|
|
);
|
|
CREATE TABLE IF NOT EXISTS webhooks (
|
|
id SERIAL PRIMARY KEY,
|
|
name VARCHAR(100) NOT NULL,
|
|
url TEXT NOT NULL,
|
|
secret VARCHAR(128),
|
|
events TEXT[] NOT NULL DEFAULT '{}',
|
|
enabled BOOLEAN NOT NULL DEFAULT TRUE,
|
|
last_status INTEGER,
|
|
last_fired_at TIMESTAMPTZ,
|
|
created_at TIMESTAMPTZ DEFAULT NOW()
|
|
);
|
|
CREATE TABLE IF NOT EXISTS report_schedules (
|
|
id SERIAL PRIMARY KEY,
|
|
name VARCHAR(100) NOT NULL,
|
|
frequency VARCHAR(10) NOT NULL DEFAULT 'weekly', -- daily|weekly|monthly
|
|
recipients TEXT NOT NULL,
|
|
enabled BOOLEAN NOT NULL DEFAULT TRUE,
|
|
last_sent_at TIMESTAMPTZ,
|
|
next_run_at TIMESTAMPTZ NOT NULL,
|
|
created_at TIMESTAMPTZ DEFAULT NOW()
|
|
);
|
|
ALTER TABLE events ADD COLUMN IF NOT EXISTS log_id VARCHAR(20);
|
|
CREATE UNIQUE INDEX IF NOT EXISTS idx_events_device_log_id ON events(device_id, log_id);
|
|
ALTER TABLE topology_links ADD COLUMN IF NOT EXISTS neighbor_mac VARCHAR(17);
|
|
ALTER TABLE topology_links ADD COLUMN IF NOT EXISTS stp_role VARCHAR(20);
|
|
ALTER TABLE topology_links ADD COLUMN IF NOT EXISTS stp_state VARCHAR(20);
|
|
ALTER TABLE topology_links ADD COLUMN IF NOT EXISTS bridge_name VARCHAR(50);
|
|
ALTER TABLE devices ADD COLUMN IF NOT EXISTS location_address TEXT;
|
|
ALTER TABLE devices ADD COLUMN IF NOT EXISTS location_lat NUMERIC(10,7);
|
|
ALTER TABLE devices ADD COLUMN IF NOT EXISTS location_lng NUMERIC(10,7);
|
|
ALTER TABLE devices ADD COLUMN IF NOT EXISTS rack_name VARCHAR(100);
|
|
ALTER TABLE devices ADD COLUMN IF NOT EXISTS rack_slot VARCHAR(20);
|
|
ALTER TABLE topology_links ADD COLUMN IF NOT EXISTS neighbor_caps VARCHAR(255);
|
|
ALTER TABLE topology_links ADD COLUMN IF NOT EXISTS discovered_by VARCHAR(50);
|
|
-- RouterOS can return a long comma-separated discovered-by list; 50 chars was too small.
|
|
ALTER TABLE topology_links ALTER COLUMN discovered_by TYPE VARCHAR(512);
|
|
-- Interface names from neighbor discovery can exceed 50 chars (long bridge/bond names).
|
|
ALTER TABLE topology_links ALTER COLUMN from_interface TYPE VARCHAR(512);
|
|
ALTER TABLE topology_links ALTER COLUMN to_interface TYPE VARCHAR(512);
|
|
ALTER TABLE devices ADD COLUMN IF NOT EXISTS firmware_update_available BOOLEAN DEFAULT FALSE;
|
|
ALTER TABLE devices ADD COLUMN IF NOT EXISTS latest_ros_version VARCHAR(20);
|
|
ALTER TABLE devices ADD COLUMN IF NOT EXISTS routerboard_upgrade_available BOOLEAN DEFAULT FALSE;
|
|
ALTER TABLE devices ADD COLUMN IF NOT EXISTS upgrade_firmware_version VARCHAR(20);
|
|
|
|
-- Config history / drift detection: each snapshot stores the canonical /export
|
|
-- .rsc text (config_text), deduped by content hash, and links to the restorable
|
|
-- backup that holds the same .rsc. The snapshot and its backup are one artifact,
|
|
-- so deleting the backup cascades to remove the snapshot (kept consistent).
|
|
ALTER TABLE device_configs ADD COLUMN IF NOT EXISTS config_hash VARCHAR(64);
|
|
ALTER TABLE device_configs ADD COLUMN IF NOT EXISTS change_summary TEXT;
|
|
ALTER TABLE device_configs ADD COLUMN IF NOT EXISTS config_text TEXT;
|
|
ALTER TABLE device_configs ADD COLUMN IF NOT EXISTS backup_id INTEGER REFERENCES backups(id) ON DELETE SET NULL;
|
|
-- Upgrade the backup_id FK from SET NULL to CASCADE so a snapshot and its backup
|
|
-- stay in lockstep (deleting the backup removes the now-unrestorable snapshot).
|
|
ALTER TABLE device_configs DROP CONSTRAINT IF EXISTS device_configs_backup_id_fkey;
|
|
ALTER TABLE device_configs ADD CONSTRAINT device_configs_backup_id_fkey
|
|
FOREIGN KEY (backup_id) REFERENCES backups(id) ON DELETE CASCADE;
|
|
|
|
-- Cached IPv4/IPv6 addresses from /ip/address (per device) for topology resolution:
|
|
-- neighbors seen only by IP (CDP/MNDP) can be matched to managed devices even
|
|
-- when the address is not the device's management IP.
|
|
ALTER TABLE devices ADD COLUMN IF NOT EXISTS ip_addresses_jsonb JSONB;
|
|
|
|
-- Allow multiple neighbors per interface (one row per neighbor, not per port)
|
|
ALTER TABLE topology_links DROP CONSTRAINT IF EXISTS topology_links_from_device_id_from_interface_key;
|
|
|
|
-- Alert rules — one row per event type
|
|
CREATE TABLE IF NOT EXISTS alert_rules (
|
|
event_type VARCHAR(50) PRIMARY KEY,
|
|
enabled BOOLEAN NOT NULL DEFAULT false,
|
|
threshold INTEGER,
|
|
cooldown_min INTEGER NOT NULL DEFAULT 15,
|
|
updated_at TIMESTAMPTZ DEFAULT NOW()
|
|
);
|
|
|
|
-- Alert channels — email / Slack / Discord / Telegram
|
|
CREATE TABLE IF NOT EXISTS alert_channels (
|
|
id SERIAL PRIMARY KEY,
|
|
name VARCHAR(100) NOT NULL,
|
|
type VARCHAR(20) NOT NULL CHECK (type IN ('email','slack','discord','telegram')),
|
|
enabled BOOLEAN NOT NULL DEFAULT true,
|
|
config JSONB NOT NULL DEFAULT '{}',
|
|
created_at TIMESTAMPTZ DEFAULT NOW(),
|
|
updated_at TIMESTAMPTZ DEFAULT NOW()
|
|
);
|
|
|
|
-- Alert send history
|
|
CREATE TABLE IF NOT EXISTS alert_history (
|
|
id SERIAL PRIMARY KEY,
|
|
event_type VARCHAR(50) NOT NULL,
|
|
device_id INTEGER REFERENCES devices(id) ON DELETE SET NULL,
|
|
device_name VARCHAR(255),
|
|
message TEXT NOT NULL,
|
|
channels_notified JSONB,
|
|
sent_at TIMESTAMPTZ DEFAULT NOW()
|
|
);
|
|
CREATE INDEX IF NOT EXISTS idx_alert_history_sent ON alert_history(sent_at DESC);
|
|
|
|
-- Wireless interfaces (radio hardware config + SSID settings)
|
|
CREATE TABLE IF NOT EXISTS wireless_interfaces (
|
|
id SERIAL PRIMARY KEY,
|
|
device_id INTEGER NOT NULL REFERENCES devices(id) ON DELETE CASCADE,
|
|
name VARCHAR(50) NOT NULL,
|
|
ssid VARCHAR(100),
|
|
mode VARCHAR(30),
|
|
band VARCHAR(50),
|
|
frequency INTEGER,
|
|
channel_width VARCHAR(30),
|
|
tx_power INTEGER,
|
|
tx_power_mode VARCHAR(30),
|
|
antenna_gain INTEGER,
|
|
country VARCHAR(50),
|
|
installation VARCHAR(20) DEFAULT 'indoor',
|
|
disabled BOOLEAN DEFAULT FALSE,
|
|
running BOOLEAN DEFAULT FALSE,
|
|
mac_address VARCHAR(17),
|
|
security_profile VARCHAR(100),
|
|
noise_floor INTEGER,
|
|
registered_clients INTEGER DEFAULT 0,
|
|
config_json JSONB,
|
|
updated_at TIMESTAMPTZ DEFAULT NOW(),
|
|
UNIQUE(device_id, name)
|
|
);
|
|
|
|
-- Spectral scan snapshots
|
|
CREATE TABLE IF NOT EXISTS spectral_scan_data (
|
|
id SERIAL PRIMARY KEY,
|
|
device_id INTEGER NOT NULL REFERENCES devices(id) ON DELETE CASCADE,
|
|
interface_name TEXT NOT NULL,
|
|
scanned_at TIMESTAMPTZ DEFAULT NOW(),
|
|
data JSONB NOT NULL,
|
|
scan_type TEXT DEFAULT 'scheduled'
|
|
);
|
|
CREATE INDEX IF NOT EXISTS idx_spectral_scan_device
|
|
ON spectral_scan_data(device_id, interface_name, scanned_at DESC);
|
|
|
|
-- AP scan results (nearby access points discovered by wireless scan)
|
|
CREATE TABLE IF NOT EXISTS ap_scan_data (
|
|
id SERIAL PRIMARY KEY,
|
|
device_id INTEGER NOT NULL REFERENCES devices(id) ON DELETE CASCADE,
|
|
scanned_at TIMESTAMPTZ DEFAULT NOW(),
|
|
data JSONB NOT NULL,
|
|
scan_type TEXT DEFAULT 'scheduled'
|
|
);
|
|
CREATE INDEX IF NOT EXISTS idx_ap_scan_device
|
|
ON ap_scan_data(device_id, scanned_at DESC);
|
|
|
|
-- Device credential presets — reusable API/SSH credential sets, referenced
|
|
-- by name when adding or editing a managed device. Passwords are stored
|
|
-- encrypted at rest (same scheme as devices.api_password_encrypted) so the
|
|
-- plaintext never leaves the backend.
|
|
CREATE TABLE IF NOT EXISTS credential_presets (
|
|
id SERIAL PRIMARY KEY,
|
|
name VARCHAR(100) NOT NULL UNIQUE,
|
|
api_username VARCHAR(50) NOT NULL,
|
|
api_password_encrypted TEXT NOT NULL,
|
|
api_port INTEGER,
|
|
ssh_username VARCHAR(50),
|
|
ssh_password_encrypted TEXT,
|
|
ssh_port INTEGER,
|
|
notes TEXT,
|
|
created_at TIMESTAMPTZ DEFAULT NOW(),
|
|
updated_at TIMESTAMPTZ DEFAULT NOW()
|
|
);
|
|
ALTER TABLE credential_presets ADD COLUMN IF NOT EXISTS allow_operator_use BOOLEAN NOT NULL DEFAULT TRUE;
|
|
|
|
-- Maintenance windows — suppress alerts for planned downtime
|
|
CREATE TABLE IF NOT EXISTS maintenance_windows (
|
|
id SERIAL PRIMARY KEY,
|
|
name VARCHAR(100) NOT NULL,
|
|
device_ids INTEGER[] NOT NULL DEFAULT '{}',
|
|
start_at TIMESTAMPTZ NOT NULL,
|
|
end_at TIMESTAMPTZ NOT NULL,
|
|
recurring_cron VARCHAR(100),
|
|
active BOOLEAN NOT NULL DEFAULT true,
|
|
created_at TIMESTAMPTZ DEFAULT NOW()
|
|
);
|
|
CREATE INDEX IF NOT EXISTS idx_maintenance_windows_active ON maintenance_windows(active, start_at, end_at);
|
|
|
|
-- Device tags
|
|
CREATE TABLE IF NOT EXISTS tags (
|
|
id SERIAL PRIMARY KEY,
|
|
name VARCHAR(50) NOT NULL UNIQUE,
|
|
color VARCHAR(20) NOT NULL DEFAULT '#6366f1',
|
|
created_at TIMESTAMPTZ DEFAULT NOW()
|
|
);
|
|
|
|
CREATE TABLE IF NOT EXISTS device_tags (
|
|
device_id INTEGER NOT NULL REFERENCES devices(id) ON DELETE CASCADE,
|
|
tag_id INTEGER NOT NULL REFERENCES tags(id) ON DELETE CASCADE,
|
|
PRIMARY KEY (device_id, tag_id)
|
|
);
|
|
CREATE INDEX IF NOT EXISTS idx_device_tags_tag ON device_tags(tag_id);
|
|
|
|
-- Audit log — records all write operations performed by authenticated users
|
|
CREATE TABLE IF NOT EXISTS audit_log (
|
|
id SERIAL PRIMARY KEY,
|
|
user_id INTEGER,
|
|
username VARCHAR(50),
|
|
method VARCHAR(10) NOT NULL,
|
|
path TEXT NOT NULL,
|
|
entity_type VARCHAR(50),
|
|
entity_id INTEGER,
|
|
summary TEXT,
|
|
ip_address VARCHAR(45),
|
|
status_code INTEGER,
|
|
created_at TIMESTAMPTZ DEFAULT NOW()
|
|
);
|
|
CREATE INDEX IF NOT EXISTS idx_audit_log_created ON audit_log(created_at DESC);
|
|
CREATE INDEX IF NOT EXISTS idx_audit_log_user ON audit_log(user_id, created_at DESC);
|
|
CREATE INDEX IF NOT EXISTS idx_audit_log_entity ON audit_log(entity_type, entity_id, created_at DESC);
|
|
|
|
-- Device availability (offline/online outage tracking)
|
|
CREATE TABLE IF NOT EXISTS device_availability (
|
|
id SERIAL PRIMARY KEY,
|
|
device_id INTEGER NOT NULL REFERENCES devices(id) ON DELETE CASCADE,
|
|
went_offline_at TIMESTAMPTZ NOT NULL,
|
|
came_back_online_at TIMESTAMPTZ,
|
|
duration_seconds INTEGER
|
|
);
|
|
CREATE INDEX IF NOT EXISTS idx_device_availability_device ON device_availability(device_id, went_offline_at DESC);
|
|
|
|
-- Manual topology links — user-drawn connections for devices with no auto-discovery
|
|
CREATE TABLE IF NOT EXISTS manual_topology_links (
|
|
id SERIAL PRIMARY KEY,
|
|
from_device_id INTEGER NOT NULL REFERENCES devices(id) ON DELETE CASCADE,
|
|
to_device_id INTEGER NOT NULL REFERENCES devices(id) ON DELETE CASCADE,
|
|
label VARCHAR(100),
|
|
created_at TIMESTAMPTZ DEFAULT NOW(),
|
|
UNIQUE(from_device_id, to_device_id)
|
|
);
|
|
CREATE INDEX IF NOT EXISTS idx_manual_topology_links_from ON manual_topology_links(from_device_id);
|
|
CREATE INDEX IF NOT EXISTS idx_manual_topology_links_to ON manual_topology_links(to_device_id);
|
|
|
|
-- Configuration templates (reusable config sets pushed to devices or groups)
|
|
CREATE TABLE IF NOT EXISTS config_templates (
|
|
id SERIAL PRIMARY KEY,
|
|
name VARCHAR(100) NOT NULL UNIQUE,
|
|
description TEXT,
|
|
applies_to_type VARCHAR(20),
|
|
template_json JSONB NOT NULL DEFAULT '{}',
|
|
created_at TIMESTAMPTZ DEFAULT NOW(),
|
|
updated_at TIMESTAMPTZ DEFAULT NOW()
|
|
);
|
|
|
|
-- TOTP two-factor authentication
|
|
ALTER TABLE users ADD COLUMN IF NOT EXISTS totp_secret VARCHAR(64);
|
|
ALTER TABLE users ADD COLUMN IF NOT EXISTS totp_enabled BOOLEAN NOT NULL DEFAULT false;
|
|
|
|
-- Per-client daily traffic rollups from the NetFlow collector. mac_address
|
|
-- also holds the pseudo-clients 'unknown' (unmapped local IPs) and 'other'
|
|
-- (clients folded by the top-N cardinality cap).
|
|
CREATE TABLE IF NOT EXISTS client_traffic_daily (
|
|
mac_address VARCHAR(17) NOT NULL,
|
|
day DATE NOT NULL,
|
|
upload_bytes BIGINT NOT NULL DEFAULT 0,
|
|
download_bytes BIGINT NOT NULL DEFAULT 0,
|
|
app_breakdown JSONB NOT NULL DEFAULT '{}',
|
|
PRIMARY KEY (mac_address, day)
|
|
);
|
|
CREATE INDEX IF NOT EXISTS idx_client_traffic_daily_day ON client_traffic_daily(day DESC);
|
|
|
|
-- Wireless security profiles (WPA/WPA2/WPA3 config)
|
|
CREATE TABLE IF NOT EXISTS wireless_security_profiles (
|
|
id SERIAL PRIMARY KEY,
|
|
device_id INTEGER NOT NULL REFERENCES devices(id) ON DELETE CASCADE,
|
|
name VARCHAR(100) NOT NULL,
|
|
mode VARCHAR(30) DEFAULT 'none',
|
|
authentication_types TEXT[] DEFAULT '{}',
|
|
unicast_ciphers TEXT[] DEFAULT '{}',
|
|
group_ciphers TEXT[] DEFAULT '{}',
|
|
management_protection VARCHAR(20) DEFAULT 'disabled',
|
|
config_json JSONB,
|
|
updated_at TIMESTAMPTZ DEFAULT NOW(),
|
|
UNIQUE(device_id, name)
|
|
);
|
|
`;
|
|
|
|
const DEFAULT_SETTINGS = [
|
|
{ key: 'polling_fast_interval', value: 30 },
|
|
{ key: 'polling_slow_interval', value: 300 },
|
|
{ key: 'polling_logs_interval', value: 60 },
|
|
{ key: 'retention_events_days', value: 30 },
|
|
{ key: 'backup_schedule_enabled', value: false },
|
|
{ key: 'backup_schedule_cron', value: '0 2 * * *' },
|
|
{ key: 'mac_scan_enabled', value: true },
|
|
{ key: 'mac_scan_interval', value: 300 },
|
|
{ key: 'reverse_dns_enabled', value: false },
|
|
{ key: 'retention_clients_days', value: 7 },
|
|
{ key: 'spectral_scan_enabled', value: false },
|
|
{ key: 'spectral_scan_interval_hours', value: 24 },
|
|
{ key: 'ap_scan_enabled', value: false },
|
|
{ key: 'ap_scan_interval_hours', value: 24 },
|
|
{ key: 'login_rate_limit_window_sec', value: 60 },
|
|
{ key: 'login_rate_limit_max', value: 10 },
|
|
{ key: 'config_snapshot_enabled', value: true },
|
|
{ key: 'config_snapshot_interval_min', value: 60 },
|
|
{ key: 'config_snapshot_retention', value: 30 },
|
|
{ key: 'netflow_enabled', value: false },
|
|
{ key: 'netflow_collector_address', value: '' },
|
|
{ key: 'netflow_collector_port', value: 2055 },
|
|
{ key: 'netflow_version', value: '9' },
|
|
{ key: 'netflow_active_timeout', value: '1m' },
|
|
{ key: 'netflow_inactive_timeout', value: '15s' },
|
|
{ key: 'netflow_topn_clients', value: 50 },
|
|
{ key: 'netflow_accept_unknown', value: true },
|
|
{ key: 'netflow_retention_days', value: 30 },
|
|
{ key: 'netflow_daily_retention_days', value: 365 },
|
|
];
|
|
|
|
export async function runMigrations(): Promise<void> {
|
|
console.log('Running database migrations...');
|
|
const client = await pool.connect();
|
|
try {
|
|
await client.query(MIGRATION_SQL);
|
|
console.log('Schema created/verified');
|
|
|
|
// Insert default settings
|
|
for (const setting of DEFAULT_SETTINGS) {
|
|
await client.query(
|
|
`INSERT INTO app_settings (key, value) VALUES ($1, $2) ON CONFLICT (key) DO NOTHING`,
|
|
[setting.key, JSON.stringify(setting.value)]
|
|
);
|
|
}
|
|
|
|
// Create default admin user if no users exist
|
|
const userCount = await client.query('SELECT COUNT(*) FROM users');
|
|
if (parseInt(userCount.rows[0].count, 10) === 0) {
|
|
const hash = await bcrypt.hash('admin', 12);
|
|
await client.query(
|
|
`INSERT INTO users (username, password_hash, role) VALUES ($1, $2, $3)`,
|
|
['admin', hash, 'admin']
|
|
);
|
|
console.log('Default admin user created (username: admin, password: admin)');
|
|
console.log('⚠️ Please change the default password after first login!');
|
|
}
|
|
|
|
console.log('Database migrations completed successfully');
|
|
} finally {
|
|
client.release();
|
|
}
|
|
}
|
|
|
|
// Run directly if called as script
|
|
if (require.main === module) {
|
|
runMigrations()
|
|
.then(() => process.exit(0))
|
|
.catch((err) => {
|
|
console.error('Migration failed:', err);
|
|
process.exit(1);
|
|
});
|
|
}
|