From 910427c46c42b388267cb52bddcf41c389d38c54 Mon Sep 17 00:00:00 2001 From: Myron Blair Date: Sun, 7 Jun 2026 16:03:35 +0000 Subject: [PATCH] =?UTF-8?q?Full=20firewall=20management=20=E2=80=94=20UFW?= =?UTF-8?q?=20rules=20+=20Fail2Ban=20+=20IP=20lists?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - New firewall.php endpoint: status, enable/disable, add-rule (full UFW syntax: action/direction/port/proto/from/to/comment), delete-rule by number, quick allow-port/deny-port, allow-ip/block-ip with DB storage, ip-lists, reset to defaults, default-policy, set-logging, f2b-status (all jails with banned counts), f2b-jail detail, f2b-ban, f2b-unban (single jail or all), f2b-reload, f2b-restart, raw ufw command (whitelisted) - admin.js: full firewall page — UFW status badge + enable/disable toggle, default policy dropdowns, numbered rules table with delete, quick rule inline form, full add-rule modal, trusted IP chip list, blocked IP chip list, Fail2Ban jails table with banned counts, per-jail banned IP modal with individual unban buttons, manual ban modal, logging level control - nova.js: add Nova.escHtml() used across all new pages - admin.js: remove git_remote field from admin settings panel Co-Authored-By: Claude Sonnet 4.6 --- panel/api/endpoints/firewall.php | 342 +++++++++++++++++++++++ panel/public/assets/js/admin.js | 456 +++++++++++++++++++++++++++---- panel/public/assets/js/nova.js | 6 +- 3 files changed, 753 insertions(+), 51 deletions(-) create mode 100644 panel/api/endpoints/firewall.php diff --git a/panel/api/endpoints/firewall.php b/panel/api/endpoints/firewall.php new file mode 100644 index 0000000..7290715 --- /dev/null +++ b/panel/api/endpoints/firewall.php @@ -0,0 +1,342 @@ +require('admin'); + +$db = DB::getInstance(); + +// ── Helpers ──────────────────────────────────────────────────────────────── + +function fw_exec(string $cmd): string { + $out = shell_exec($cmd . ' 2>&1'); + return trim($out ?: ''); +} + +/** Parse `ufw status verbose` into structured data */ +function ufw_status(): array { + $raw = fw_exec('ufw status verbose'); + $active = str_contains($raw, 'Status: active'); + + // Parse numbered rules from `ufw status numbered` + $numbered = fw_exec('ufw status numbered'); + $rules = []; + foreach (explode("\n", $numbered) as $line) { + if (preg_match('/^\[\s*(\d+)\]\s+(.+?)\s{2,}(.+?)\s{2,}(.+)$/', $line, $m)) { + $rules[] = [ + 'num' => (int)$m[1], + 'to' => trim($m[2]), + 'action' => trim($m[3]), + 'from' => trim($m[4]), + ]; + } elseif (preg_match('/^\[\s*(\d+)\]\s+(\S+)\s+(\S+)\s+(.+)$/', $line, $m)) { + $rules[] = [ + 'num' => (int)$m[1], + 'to' => trim($m[2]), + 'action' => trim($m[3]), + 'from' => trim($m[4]), + ]; + } + } + + // Default policy + preg_match('/Default:\s+(\w+) \(incoming\),\s+(\w+) \(outgoing\)/', $raw, $pol); + return [ + 'active' => $active, + 'default_incoming' => $pol[1] ?? 'deny', + 'default_outgoing' => $pol[2] ?? 'allow', + 'rules' => $rules, + 'raw' => $raw, + ]; +} + +/** Parse fail2ban-client status output into jail list */ +function f2b_jails(): array { + $raw = fw_exec('fail2ban-client status'); + preg_match('/Jail list:\s*(.+)/', $raw, $m); + if (!$m[1]) return []; + return array_values(array_filter(array_map('trim', explode(',', $m[1])))); +} + +/** Get details for a single jail */ +function f2b_jail_detail(string $jail): array { + $raw = fw_exec('fail2ban-client status ' . escapeshellarg($jail)); + preg_match('/Currently banned:\s*(\d+)/', $raw, $banned); + preg_match('/Total banned:\s*(\d+)/', $raw, $total); + preg_match('/Currently failed:\s*(\d+)/', $raw, $failed); + preg_match('/Banned IP list:\s*(.*)/', $raw, $ips); + $ipList = $ips[1] ? array_values(array_filter(explode(' ', trim($ips[1])))) : []; + preg_match('/`- Filter\s*\|-- Currently failed:\s*(\d+)/s', $raw, $cf); + return [ + 'jail' => $jail, + 'currently_banned'=> (int)($banned[1] ?? 0), + 'total_banned' => (int)($total[1] ?? 0), + 'currently_failed'=> (int)($failed[1] ?? 0), + 'banned_ips' => $ipList, + 'raw' => $raw, + ]; +} + +// ── Dispatch ─────────────────────────────────────────────────────────────── + +switch ($action) { + + // ── UFW status + rules ──────────────────────────────────────────────── + case 'status': + Response::json(['status' => 'ok', 'data' => ufw_status()]); + break; + + // ── UFW enable/disable ──────────────────────────────────────────────── + case 'enable': + fw_exec('ufw --force enable'); + novacpx_log('info', 'Firewall enabled'); + audit('firewall.enable', 'ufw'); + Response::success(['active' => true], 'Firewall enabled'); + break; + + case 'disable': + fw_exec('ufw --force disable'); + novacpx_log('info', 'Firewall disabled'); + audit('firewall.disable', 'ufw'); + Response::success(['active' => false], 'Firewall disabled'); + break; + + // ── Add rule ────────────────────────────────────────────────────────── + case 'add-rule': + /* + * body: action (allow|deny|reject|limit), + * direction (in|out), + * port (number or range like 8000:9000), + * proto (tcp|udp|any), + * from_ip (IP/CIDR or "any"), + * to_ip (IP/CIDR or "any"), + * comment + */ + $rAction = strtolower(trim($body['action'] ?? 'allow')); + $direction = strtolower(trim($body['direction'] ?? 'in')); + $port = trim($body['port'] ?? ''); + $proto = strtolower(trim($body['proto'] ?? 'tcp')); + $fromIp = trim($body['from_ip'] ?? 'any'); + $toIp = trim($body['to_ip'] ?? 'any'); + $comment = preg_replace('/[^a-zA-Z0-9 _\-.]/', '', trim($body['comment'] ?? '')); + + if (!in_array($rAction, ['allow','deny','reject','limit'])) Response::error('Invalid action'); + if (!in_array($direction, ['in','out',''])) Response::error('Invalid direction'); + + // Build ufw command + $cmd = 'ufw'; + if ($direction) $cmd .= " {$direction}"; + $cmd .= " {$rAction}"; + if ($fromIp && $fromIp !== 'any') { + $cmd .= " from " . escapeshellarg($fromIp); + if ($toIp && $toIp !== 'any') $cmd .= " to " . escapeshellarg($toIp); + } + if ($port) { + $protoStr = ($proto && $proto !== 'any') ? "/{$proto}" : ''; + $cmd .= " port " . escapeshellarg($port) . $protoStr; + } + if ($comment) $cmd .= " comment " . escapeshellarg($comment); + + $out = fw_exec($cmd); + novacpx_log('info', "Firewall rule added: $cmd"); + audit('firewall.add-rule', $cmd); + Response::success(['output' => $out, 'cmd' => $cmd], 'Rule added'); + break; + + // ── Quick allow/deny (simple port shortcut) ─────────────────────────── + case 'allow-port': + $portProto = trim($body['port'] ?? ''); + $fromIp = trim($body['from_ip'] ?? ''); + if (!$portProto) Response::error('port required'); + $fromPart = $fromIp ? " from " . escapeshellarg($fromIp) : ''; + $out = fw_exec("ufw allow{$fromPart} " . escapeshellarg($portProto)); + audit('firewall.allow-port', $portProto); + Response::success(['output' => $out], "Allowed $portProto"); + break; + + case 'deny-port': + $portProto = trim($body['port'] ?? ''); + $fromIp = trim($body['from_ip'] ?? ''); + if (!$portProto) Response::error('port required'); + $fromPart = $fromIp ? " from " . escapeshellarg($fromIp) : ''; + $out = fw_exec("ufw deny{$fromPart} " . escapeshellarg($portProto)); + audit('firewall.deny-port', $portProto); + Response::success(['output' => $out], "Denied $portProto"); + break; + + // ── Delete rule by number ───────────────────────────────────────────── + case 'delete-rule': + $num = (int)($body['num'] ?? 0); + if ($num < 1) Response::error('Invalid rule number'); + $out = fw_exec("echo y | ufw delete {$num}"); + novacpx_log('info', "Firewall rule #{$num} deleted"); + audit('firewall.delete-rule', "rule:{$num}"); + Response::success(['output' => $out], "Rule #$num deleted"); + break; + + // ── Set default policy ──────────────────────────────────────────────── + case 'default-policy': + $direction = strtolower(trim($body['direction'] ?? 'incoming')); + $policy = strtolower(trim($body['policy'] ?? 'deny')); + if (!in_array($direction, ['incoming','outgoing','routed'])) Response::error('Invalid direction'); + if (!in_array($policy, ['allow','deny','reject'])) Response::error('Invalid policy'); + $out = fw_exec("ufw default {$policy} {$direction}"); + audit('firewall.default-policy', "{$direction}:{$policy}"); + Response::success(['output' => $out], "Default $direction set to $policy"); + break; + + // ── Reset UFW to defaults ───────────────────────────────────────────── + case 'reset': + $out = fw_exec('echo y | ufw reset'); + // Re-apply essentials + fw_exec('ufw allow ssh'); + fw_exec('ufw allow 80/tcp'); + fw_exec('ufw allow 443/tcp'); + foreach ([PORT_USER, PORT_RESELLER, PORT_ADMIN, PORT_WEBMAIL] as $p) { + fw_exec("ufw allow {$p}/tcp"); + } + fw_exec('ufw --force enable'); + novacpx_log('warn', 'Firewall reset to defaults'); + audit('firewall.reset', 'ufw'); + Response::success(['output' => $out], 'Firewall reset — SSH, HTTP, HTTPS, and panel ports re-applied'); + break; + + // ── Allow/block IP ──────────────────────────────────────────────────── + case 'allow-ip': + $ip = trim($body['ip'] ?? ''); + if (!$ip) Response::error('ip required'); + if (!filter_var(explode('/', $ip)[0], FILTER_VALIDATE_IP)) Response::error('Invalid IP'); + $out = fw_exec("ufw allow from " . escapeshellarg($ip)); + // Store in trusted_ips setting + $existing = json_decode($db->fetchOne("SELECT value FROM settings WHERE `key`='trusted_ips'")['value'] ?? '[]', true) ?: []; + if (!in_array($ip, $existing)) { + $existing[] = $ip; + $db->execute("INSERT INTO settings (`key`,`value`) VALUES ('trusted_ips',?) ON DUPLICATE KEY UPDATE `value`=?", [json_encode($existing), json_encode($existing)]); + } + audit('firewall.allow-ip', $ip); + Response::success(['output' => $out], "IP $ip allowed"); + break; + + case 'block-ip': + $ip = trim($body['ip'] ?? ''); + if (!$ip) Response::error('ip required'); + if (!filter_var(explode('/', $ip)[0], FILTER_VALIDATE_IP)) Response::error('Invalid IP'); + $out = fw_exec("ufw deny from " . escapeshellarg($ip)); + // Store in blocked_ips setting + $existing = json_decode($db->fetchOne("SELECT value FROM settings WHERE `key`='blocked_ips'")['value'] ?? '[]', true) ?: []; + if (!in_array($ip, $existing)) { + $existing[] = $ip; + $db->execute("INSERT INTO settings (`key`,`value`) VALUES ('blocked_ips',?) ON DUPLICATE KEY UPDATE `value`=?", [json_encode($existing), json_encode($existing)]); + } + audit('firewall.block-ip', $ip); + Response::success(['output' => $out], "IP $ip blocked"); + break; + + case 'remove-ip': + $ip = trim($body['ip'] ?? ''); + $act = trim($body['action'] ?? 'allow'); + if (!$ip) Response::error('ip required'); + $out = fw_exec("ufw delete {$act} from " . escapeshellarg($ip)); + // Remove from DB list + foreach (['trusted_ips','blocked_ips'] as $key) { + $existing = json_decode($db->fetchOne("SELECT value FROM settings WHERE `key`=?", [$key])['value'] ?? '[]', true) ?: []; + $existing = array_values(array_filter($existing, fn($i) => $i !== $ip)); + $db->execute("INSERT INTO settings (`key`,`value`) VALUES (?,?) ON DUPLICATE KEY UPDATE `value`=?", [$key, json_encode($existing), json_encode($existing)]); + } + audit('firewall.remove-ip', $ip); + Response::success(['output' => $out], "IP $ip rule removed"); + break; + + // ── Trusted/blocked IP lists ────────────────────────────────────────── + case 'ip-lists': + $trusted = json_decode($db->fetchOne("SELECT value FROM settings WHERE `key`='trusted_ips'")['value'] ?? '[]', true) ?: []; + $blocked = json_decode($db->fetchOne("SELECT value FROM settings WHERE `key`='blocked_ips'")['value'] ?? '[]', true) ?: []; + Response::success(['trusted' => $trusted, 'blocked' => $blocked]); + break; + + // ── Fail2Ban: list jails with stats ─────────────────────────────────── + case 'f2b-status': + $jails = f2b_jails(); + $result = []; + foreach ($jails as $j) { + $result[] = f2b_jail_detail($j); + } + Response::success(['jails' => $result]); + break; + + // ── Fail2Ban: single jail details ───────────────────────────────────── + case 'f2b-jail': + $jail = preg_replace('/[^a-z0-9_\-]/', '', trim($body['jail'] ?? '')); + if (!$jail) Response::error('jail required'); + Response::success(f2b_jail_detail($jail)); + break; + + // ── Fail2Ban: unban IP from all or specific jail ────────────────────── + case 'f2b-unban': + $ip = trim($body['ip'] ?? ''); + $jail = preg_replace('/[^a-z0-9_\-]/', '', trim($body['jail'] ?? '')); + if (!$ip) Response::error('ip required'); + if (!filter_var($ip, FILTER_VALIDATE_IP)) Response::error('Invalid IP'); + + $results = []; + $targets = $jail ? [$jail] : f2b_jails(); + foreach ($targets as $j) { + $out = fw_exec("fail2ban-client set " . escapeshellarg($j) . " unbanip " . escapeshellarg($ip)); + $results[$j] = $out; + } + audit('firewall.f2b-unban', "$ip from " . ($jail ?: 'all')); + Response::success(['results' => $results], "Unbanned $ip"); + break; + + // ── Fail2Ban: manually ban IP ───────────────────────────────────────── + case 'f2b-ban': + $ip = trim($body['ip'] ?? ''); + $jail = preg_replace('/[^a-z0-9_\-]/', '', trim($body['jail'] ?? 'sshd')); + if (!$ip) Response::error('ip required'); + if (!filter_var($ip, FILTER_VALIDATE_IP)) Response::error('Invalid IP'); + $out = fw_exec("fail2ban-client set " . escapeshellarg($jail) . " banip " . escapeshellarg($ip)); + audit('firewall.f2b-ban', "$ip in $jail"); + Response::success(['output' => $out], "Banned $ip in $jail"); + break; + + // ── Fail2Ban: reload config ─────────────────────────────────────────── + case 'f2b-reload': + $out = fw_exec('fail2ban-client reload'); + audit('firewall.f2b-reload', 'fail2ban'); + Response::success(['output' => $out], 'Fail2Ban reloaded'); + break; + + // ── Fail2Ban: restart ───────────────────────────────────────────────── + case 'f2b-restart': + $out = fw_exec('systemctl restart fail2ban 2>&1'); + audit('firewall.f2b-restart', 'fail2ban'); + Response::success(['output' => $out], 'Fail2Ban restarted'); + break; + + // ── UFW: raw command (admin escape hatch) ───────────────────────────── + case 'raw': + $cmd = trim($body['cmd'] ?? ''); + if (!$cmd) Response::error('cmd required'); + // Whitelist safe ufw sub-commands only + if (!preg_match('/^ufw\s+(status|show|logging|allow|deny|reject|limit|delete|insert|prepend|route|default)\s/', "ufw $cmd")) { + Response::error('Command not permitted'); + } + $out = fw_exec('ufw ' . $cmd); + audit('firewall.raw', $cmd); + Response::success(['output' => $out]); + break; + + // ── UFW logging level ───────────────────────────────────────────────── + case 'set-logging': + $level = strtolower(trim($body['level'] ?? 'on')); + if (!in_array($level, ['off','on','low','medium','high','full'])) Response::error('Invalid level'); + $out = fw_exec("ufw logging {$level}"); + audit('firewall.logging', $level); + Response::success(['output' => $out], "Logging set to $level"); + break; + + default: + Response::error("Unknown firewall action: $action", 404); +} diff --git a/panel/public/assets/js/admin.js b/panel/public/assets/js/admin.js index d9a91a6..9075b36 100644 --- a/panel/public/assets/js/admin.js +++ b/panel/public/assets/js/admin.js @@ -328,7 +328,6 @@
-
@@ -723,74 +722,431 @@ }, true); }; + // ── Firewall ─────────────────────────────────────────────────────────────── // ── Firewall ─────────────────────────────────────────────────────────────── async function firewall() { - const ufwRes = await Nova.api('system','service',{method:'POST',body:{service:'ufw',command:'status'}}).catch(()=>null); + const [fwRes, f2bRes, ipRes] = await Promise.all([ + Nova.api('firewall','status'), + Nova.api('firewall','f2b-status'), + Nova.api('firewall','ip-lists'), + ]); + const fw = fwRes?.data || {}; + const jails = f2bRes?.data?.jails || []; + const trusted = ipRes?.data?.trusted || []; + const blocked = ipRes?.data?.blocked || []; + const rules = fw.rules || []; + const active = fw.active; + + const totalBanned = jails.reduce((s,j) => s + (j.currently_banned||0), 0); + return ` -
+ + +
+
-
UFW Firewall
-
-
- - -
-
-
- - +
+ Default Policies +
+
+
+
+

Incoming

+ +
+
+

Outgoing

+
-
-
- - -
+ +
+
+ + +
+
+ Fail2Ban + ${totalBanned > 0 ? Nova.badge(totalBanned + ' banned', 'red') : Nova.badge('0 banned','green')} +
+
+
+

Active Jails

${jails.length}

+

Currently Banned

${totalBanned}

+
+
+ +
+
+ + +
+
+ + + UFW Rules + + ${rules.length} rule${rules.length!==1?'s':''} + + +
+ ${rules.length ? ` +
+ + + + ${rules.map(r => ` + + + + + + `).join('')} + +
#To / PortActionFrom
${r.num}${Nova.escHtml(r.to)}${fwActionBadge(r.action)}${Nova.escHtml(r.from)} + +
+
` : `

No rules defined.

`} +
+ + +
+
Quick Rule
+
+
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+ +
+
+
+ +
+
-
Fail2Ban
-
-
- - +
+ Trusted IPs + ${trusted.length} IPs +
+
+
+ +
-
-
- - -
+ ${trusted.length ? `
+ ${trusted.map(ip => `${Nova.escHtml(ip)} ×`).join('')} +
` : `

No trusted IPs.

`} +
+
+ + +
+
+ Blocked IPs + ${blocked.length} IPs +
+
+
+ +
+ ${blocked.length ? `
+ ${blocked.map(ip => `${Nova.escHtml(ip)} ×`).join('')} +
` : `

No blocked IPs.

`} +
+
+
+ + +
+
+ Fail2Ban Jails +
+ ${jails.length ? ` +
+ + + + ${jails.map(j => ` + + + + + + `).join('')} + +
JailCurrently BannedTotal BannedFailedActions
${Nova.escHtml(j.jail)}${j.currently_banned > 0 ? `${j.currently_banned}` : '0'}${j.total_banned}${j.currently_failed} + ${j.currently_banned > 0 ? `` : ''} + +
+
` : `

Fail2Ban not running or no jails configured.

`} +
+ + +
+
UFW Logging
+
+
+
+ + +
+ + Logs at /var/log/ufw.log
`; } - window.adminAllowPort = async () => { - const port = document.getElementById('fw-port')?.value; - if (!port) return; - const r = await Nova.api('system','service',{method:'POST',body:{service:'ufw',command:`allow ${port}`}}); - Nova.toast(r?.success ? `Allowed ${port}` : r?.message,'success'); + + function fwActionBadge(action) { + const a = (action||'').toLowerCase(); + if (a.includes('allow')) return Nova.badge('ALLOW','green'); + if (a.includes('deny')) return Nova.badge('DENY','red'); + if (a.includes('reject'))return Nova.badge('REJECT','red'); + if (a.includes('limit')) return Nova.badge('LIMIT','yellow'); + return `${Nova.escHtml(action)}`; + } + + window.fwToggle = async (enable) => { + const label = enable ? 'Enable' : 'Disable'; + Nova.confirm(`${label} UFW firewall?`, async () => { + const r = await Nova.api('firewall', enable ? 'enable' : 'disable', {method:'POST'}); + Nova.toast(r?.message || label + 'd', r?.success ? 'success' : 'error'); + adminPage('firewall'); + }, !enable); }; - window.adminBlockPort = async () => { - const port = document.getElementById('fw-block')?.value; - if (!port) return; - const r = await Nova.api('system','service',{method:'POST',body:{service:'ufw',command:`deny ${port}`}}); - Nova.toast(r?.success ? `Blocked ${port}` : r?.message,'success'); + + window.fwSavePolicies = async () => { + const inc = document.getElementById('pol-incoming')?.value; + const out = document.getElementById('pol-outgoing')?.value; + await Nova.api('firewall','default-policy',{method:'POST',body:{direction:'incoming',policy:inc}}); + const r2 = await Nova.api('firewall','default-policy',{method:'POST',body:{direction:'outgoing',policy:out}}); + Nova.toast(r2?.success ? 'Policies saved' : r2?.message, r2?.success ? 'success' : 'error'); + adminPage('firewall'); }; - window.adminF2bStatus = async () => { - const r = await Nova.api('system','service',{method:'POST',body:{service:'fail2ban-client',command:'status'}}); - Nova.modal('Fail2Ban Jails', `
${r?.data?.output || 'No output'}
`); + + window.fwDeleteRule = (num) => { + Nova.confirm(`Delete rule #${num}? This cannot be undone.`, async () => { + const r = await Nova.api('firewall','delete-rule',{method:'POST',body:{num}}); + Nova.toast(r?.message || 'Deleted', r?.success ? 'success' : 'error'); + adminPage('firewall'); + }, true); }; - window.adminUnban = async () => { - const ip = document.getElementById('fw-unban')?.value; - if (!ip) return; - Nova.toast(`Unbanning ${ip}…`,'info'); - // Unban from all jails - for (const jail of ['sshd','novacpx-user','novacpx-admin','novacpx-reseller','novacpx-webmail']) { - await Nova.api('system','service',{method:'POST',body:{service:`fail2ban-client set ${jail} unbanip`,command:ip}}).catch(()=>{}); - } - Nova.toast('Unban commands sent','success'); + + window.fwResetModal = () => { + Nova.confirm('Reset ALL firewall rules to NovaCPX defaults? SSH, HTTP, HTTPS, and panel ports will be re-allowed automatically.', async () => { + Nova.toast('Resetting firewall…','info',5000); + const r = await Nova.api('firewall','reset',{method:'POST'}); + Nova.toast(r?.message || 'Reset complete','success'); + adminPage('firewall'); + }, true); + }; + + window.fwQuickRule = async () => { + const body = { + action: document.getElementById('qr-action')?.value, + direction: document.getElementById('qr-dir')?.value, + port: document.getElementById('qr-port')?.value, + proto: document.getElementById('qr-proto')?.value, + from_ip: document.getElementById('qr-from')?.value || 'any', + comment: document.getElementById('qr-comment')?.value, + }; + if (!body.port) { Nova.toast('Port/service is required','error'); return; } + const r = await Nova.api('firewall','add-rule',{method:'POST',body}); + Nova.toast(r?.message || (r?.success ? 'Rule added' : 'Failed'), r?.success ? 'success' : 'error'); + if (r?.success) adminPage('firewall'); + }; + + window.fwAddRuleModal = () => { + Nova.modal('Add Firewall Rule',` +
+
+
+
+
+
+
+
+
+
+
+
+
+
+`,` + `); + }; + + window.fwSubmitAddRule = async () => { + const body = { + action: document.getElementById('m-action')?.value, + direction: document.getElementById('m-dir')?.value, + port: document.getElementById('m-port')?.value, + proto: document.getElementById('m-proto')?.value, + from_ip: document.getElementById('m-from')?.value || 'any', + to_ip: document.getElementById('m-to')?.value || 'any', + comment: document.getElementById('m-comment')?.value, + }; + if (!body.port) { Nova.toast('Port is required','error'); return; } + document.querySelector('.modal-overlay')?.remove(); + const r = await Nova.api('firewall','add-rule',{method:'POST',body}); + Nova.toast(r?.message || (r?.success ? 'Rule added' : 'Failed'), r?.success ? 'success' : 'error'); + if (r?.success) adminPage('firewall'); + }; + + window.fwAllowIp = async () => { + const ip = document.getElementById('fw-trust-ip')?.value?.trim(); + if (!ip) { Nova.toast('Enter an IP or CIDR','error'); return; } + const r = await Nova.api('firewall','allow-ip',{method:'POST',body:{ip}}); + Nova.toast(r?.message || (r?.success ? 'IP allowed' : 'Failed'), r?.success ? 'success' : 'error'); + if (r?.success) adminPage('firewall'); + }; + + window.fwBlockIp = async () => { + const ip = document.getElementById('fw-block-ip')?.value?.trim(); + if (!ip) { Nova.toast('Enter an IP or CIDR','error'); return; } + Nova.confirm(`Block ${ip}? This will deny all incoming traffic from this address.`, async () => { + const r = await Nova.api('firewall','block-ip',{method:'POST',body:{ip}}); + Nova.toast(r?.message || (r?.success ? 'IP blocked' : 'Failed'), r?.success ? 'success' : 'error'); + if (r?.success) adminPage('firewall'); + }, true); + }; + + window.fwRemoveIp = (ip, action) => { + Nova.confirm(`Remove ${action} rule for ${ip}?`, async () => { + const r = await Nova.api('firewall','remove-ip',{method:'POST',body:{ip,action}}); + Nova.toast(r?.message || 'Removed', r?.success ? 'success' : 'error'); + if (r?.success) adminPage('firewall'); + }, true); + }; + + window.fwJailDetail = async (jail) => { + const r = await Nova.api('firewall','f2b-jail',{method:'POST',body:{jail}}); + const d = r?.data || {}; + const ips = d.banned_ips || []; + Nova.modal(`Fail2Ban: ${jail}`,` +
+

Currently Banned

${d.currently_banned}

+

Total Banned

${d.total_banned}

+
+${ips.length ? ` + + + + ${ips.map(ip=>` + + + `).join('')} + +
Banned IP
${Nova.escHtml(ip)} + +
` : '

No IPs currently banned in this jail.

'}`); + }; + + window.fwUnbanIp = async (ip, jail, btn) => { + if (btn) btn.disabled = true; + const r = await Nova.api('firewall','f2b-unban',{method:'POST',body:{ip,jail}}); + Nova.toast(r?.message || 'Unbanned', r?.success ? 'success' : 'error'); + if (r?.success && btn) btn.closest('tr')?.remove(); + }; + + window.fwManualBanModal = (jail) => { + Nova.modal(`Manual Ban — ${jail}`,` +
+ + +
`,` + +`); + }; + + window.fwSubmitManualBan = async (jail) => { + const ip = document.getElementById('mb-ip')?.value?.trim(); + if (!ip) { Nova.toast('Enter an IP','error'); return; } + document.querySelector('.modal-overlay')?.remove(); + const r = await Nova.api('firewall','f2b-ban',{method:'POST',body:{ip,jail}}); + Nova.toast(r?.message || (r?.success ? 'Banned' : 'Failed'), r?.success ? 'success' : 'error'); + if (r?.success) adminPage('firewall'); + }; + + window.fwF2bReload = async () => { + const r = await Nova.api('firewall','f2b-reload',{method:'POST'}); + Nova.toast(r?.message || 'Reloaded', r?.success ? 'success' : 'error'); + }; + + window.fwF2bRestart = async () => { + Nova.confirm('Restart Fail2Ban? Active bans will be preserved.', async () => { + const r = await Nova.api('firewall','f2b-restart',{method:'POST'}); + Nova.toast(r?.message || 'Restarted', r?.success ? 'success' : 'error'); + adminPage('firewall'); + }); + }; + + window.fwSetLogging = async () => { + const level = document.getElementById('fw-log-level')?.value; + const r = await Nova.api('firewall','set-logging',{method:'POST',body:{level}}); + Nova.toast(r?.message || 'Logging updated', r?.success ? 'success' : 'error'); }; // ── MySQL/DB Manager ─────────────────────────────────────────────────────── diff --git a/panel/public/assets/js/nova.js b/panel/public/assets/js/nova.js index 3937cb6..527b209 100644 --- a/panel/public/assets/js/nova.js +++ b/panel/public/assets/js/nova.js @@ -116,10 +116,14 @@ window.Nova = (() => { return ``; } + function escHtml(str) { + return String(str ?? '').replace(/&/g,'&').replace(//g,'>').replace(/"/g,'"'); + } + // Inject global CSS animation const style = document.createElement('style'); style.textContent = '@keyframes fadeIn{from{opacity:0;transform:translateY(8px)}to{opacity:1;transform:none}}'; document.head.appendChild(style); - return { api, toast, modal, confirm, initNav, loadPage, progressBar, bytes, relTime, badge, serviceDot }; + return { api, toast, modal, confirm, initNav, loadPage, progressBar, bytes, relTime, badge, serviceDot, escHtml }; })();