Firewall: fix search bar, Search button, Modify centering, system-status $ error

- Search input: add firewall-search-input class, blue focus instead of red (avoids read-only/error look)
- Search button: use btn-search with round futuristic style (match Ban IP/Overview)
- Actions column: center Modify/Unban/Delete in Firewall Rules and Banned IPs tables
- system-status.js: increment() uses document.querySelectorAll (no jQuery), fixes $ is not defined
- upgrade_modules/09_sync.sh: sync firewall static to public/static during upgrade
- to-do/FIREWALL-LOAD-CHANGES.md: doc on file locations and deploy steps
This commit is contained in:
master3395
2026-02-16 03:16:18 +01:00
parent cf7518b015
commit 9390551ebd
13 changed files with 2851 additions and 852 deletions

View File

@@ -55,11 +55,25 @@ def notification_preferences_context(request):
}
def firewall_static_context(request):
"""Expose a cache-busting token for firewall static assets."""
firewall_js_path = '/usr/local/CyberCP/static/firewall/firewall.js'
"""Expose a cache-busting token for firewall static assets (bumps when firewall.js changes)."""
try:
version = int(os.path.getmtime(firewall_js_path))
except OSError:
from django.conf import settings
base = settings.BASE_DIR
# Check both app static and repo static so version updates when either is updated
paths = [
os.path.join(base, 'firewall', 'static', 'firewall', 'firewall.js'),
os.path.join(base, 'static', 'firewall', 'firewall.js'),
os.path.join(base, 'public', 'static', 'firewall', 'firewall.js'),
]
version = 0
for p in paths:
try:
version = max(version, int(os.path.getmtime(p)))
except (OSError, TypeError):
pass
if version <= 0:
version = int(time.time())
except (OSError, AttributeError):
version = int(time.time())
return {
'FIREWALL_STATIC_VERSION': version

View File

@@ -10,7 +10,7 @@ function getCookie(name) {
if (document.cookie && document.cookie !== '') {
var cookies = document.cookie.split(';');
for (var i = 0; i < cookies.length; i++) {
var cookie = jQuery.trim(cookies[i]);
var cookie = (cookies[i] || '').replace(/^\s+|\s+$/g, '');
// Does this cookie string begin with the name we want?
if (cookie.substring(0, name.length + 1) === (name + '=')) {
cookieValue = decodeURIComponent(cookie.substring(name.length + 1));
@@ -39,6 +39,77 @@ function randomPassword(length) {
window.app = angular.module('CyberCP', []);
var app = window.app; // Local reference for this file
// MUST be first: register dashboard controller before any other setup (avoids ctrlreg when CDN/Tracking Prevention blocks scripts)
app.controller('dashboardStatsController', ['$scope', '$http', '$timeout', function ($scope, $http, $timeout) {
$scope.cpuUsage = 0; $scope.ramUsage = 0; $scope.diskUsage = 0; $scope.cpuCores = 0;
$scope.ramTotalMB = 0; $scope.diskTotalGB = 0; $scope.diskFreeGB = 0;
$scope.totalUsers = 0; $scope.totalSites = 0; $scope.totalWPSites = 0;
$scope.totalDBs = 0; $scope.totalEmails = 0; $scope.totalFTPUsers = 0;
$scope.topProcesses = []; $scope.sshLogins = []; $scope.sshLogs = [];
$scope.loadingTopProcesses = true; $scope.loadingSSHLogins = true; $scope.loadingSSHLogs = true;
$scope.blockedIPs = {}; $scope.blockingIP = null; $scope.securityAlerts = [];
var opts = { headers: { 'X-CSRFToken': (typeof getCookie === 'function') ? getCookie('csrftoken') : '' } };
try {
$http.get('/base/getSystemStatus', opts).then(function (r) {
if (r && r.data && r.data.status === 1) {
$scope.cpuUsage = r.data.cpuUsage || 0; $scope.ramUsage = r.data.ramUsage || 0;
$scope.diskUsage = r.data.diskUsage || 0; $scope.cpuCores = r.data.cpuCores || 0;
$scope.ramTotalMB = r.data.ramTotalMB || 0; $scope.diskTotalGB = r.data.diskTotalGB || 0;
$scope.diskFreeGB = r.data.diskFreeGB || 0;
}
});
$http.get('/base/getDashboardStats', opts).then(function (r) {
if (r && r.data && r.data.status === 1) {
$scope.totalUsers = r.data.total_users || 0; $scope.totalSites = r.data.total_sites || 0;
$scope.totalWPSites = r.data.total_wp_sites || 0; $scope.totalDBs = r.data.total_dbs || 0;
$scope.totalEmails = r.data.total_emails || 0; $scope.totalFTPUsers = r.data.total_ftp_users || 0;
}
});
$http.get('/base/getRecentSSHLogins', opts).then(function (r) {
$scope.loadingSSHLogins = false;
$scope.sshLogins = (r && r.data && r.data.logins) ? r.data.logins : [];
}, function () { $scope.loadingSSHLogins = false; $scope.sshLogins = []; });
$http.get('/base/getRecentSSHLogs', opts).then(function (r) {
$scope.loadingSSHLogs = false;
$scope.sshLogs = (r && r.data && r.data.logs) ? r.data.logs : [];
}, function () { $scope.loadingSSHLogs = false; $scope.sshLogs = []; });
$http.get('/base/getTopProcesses', opts).then(function (r) {
$scope.loadingTopProcesses = false;
$scope.topProcesses = (r && r.data && r.data.status === 1 && r.data.processes) ? r.data.processes : [];
}, function () { $scope.loadingTopProcesses = false; $scope.topProcesses = []; });
if (typeof $timeout === 'function') { $timeout(function() { /* refresh */ }, 10000); }
} catch (e) { /* ignore */ }
}]);
// Overview CPU/RAM/Disk cards use systemStatusInfo register early so data loads even if later script fails
app.controller('systemStatusInfo', ['$scope', '$http', '$timeout', function ($scope, $http, $timeout) {
$scope.uptimeLoaded = false;
$scope.uptime = 'Loading...';
$scope.cpuUsage = 0; $scope.ramUsage = 0; $scope.diskUsage = 0;
$scope.cpuCores = 0; $scope.ramTotalMB = 0; $scope.diskTotalGB = 0; $scope.diskFreeGB = 0;
$scope.getSystemStatus = function() { fetchStatus(); };
function fetchStatus() {
try {
var csrf = (typeof getCookie === 'function') ? getCookie('csrftoken') : '';
$http.get('/base/getSystemStatus', { headers: { 'X-CSRFToken': csrf } }).then(function (r) {
if (r && r.data && r.data.status === 1) {
$scope.cpuUsage = r.data.cpuUsage != null ? r.data.cpuUsage : 0;
$scope.ramUsage = r.data.ramUsage != null ? r.data.ramUsage : 0;
$scope.diskUsage = r.data.diskUsage != null ? r.data.diskUsage : 0;
$scope.cpuCores = r.data.cpuCores != null ? r.data.cpuCores : 0;
$scope.ramTotalMB = r.data.ramTotalMB != null ? r.data.ramTotalMB : 0;
$scope.diskTotalGB = r.data.diskTotalGB != null ? r.data.diskTotalGB : 0;
$scope.diskFreeGB = r.data.diskFreeGB != null ? r.data.diskFreeGB : 0;
$scope.uptime = r.data.uptime || 'N/A';
}
$scope.uptimeLoaded = true;
}, function() { $scope.uptime = 'Unavailable'; $scope.uptimeLoaded = true; });
if (typeof $timeout === 'function') { $timeout(fetchStatus, 60000); }
} catch (e) { $scope.uptimeLoaded = true; }
}
fetchStatus();
}]);
var globalScope;
function GlobalRespSuccess(response) {
@@ -566,15 +637,18 @@ app.controller('homePageStatus', function ($scope, $http, $timeout) {
////////////
function increment() {
$('.box').hide();
var boxes = document.querySelectorAll ? document.querySelectorAll('.box') : [];
for (var i = 0; i < boxes.length; i++) boxes[i].style.display = 'none';
setTimeout(function () {
$('.box').show();
for (var j = 0; j < boxes.length; j++) boxes[j].style.display = '';
}, 100);
}
increment();
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', increment);
} else {
increment();
}
////////////
@@ -932,7 +1006,8 @@ var dashboardStatsControllerFn = function ($scope, $http, $timeout) {
$scope.errorTopProcesses = '';
$scope.refreshTopProcesses = function() {
$scope.loadingTopProcesses = true;
$http.get('/base/getTopProcesses').then(function (response) {
var h = { headers: { 'X-CSRFToken': (typeof getCookie === 'function') ? getCookie('csrftoken') : '' } };
$http.get('/base/getTopProcesses', h).then(function (response) {
$scope.loadingTopProcesses = false;
if (response.data && response.data.status === 1 && response.data.processes) {
$scope.topProcesses = response.data.processes;
@@ -951,7 +1026,8 @@ var dashboardStatsControllerFn = function ($scope, $http, $timeout) {
$scope.errorSSHLogins = '';
$scope.refreshSSHLogins = function() {
$scope.loadingSSHLogins = true;
$http.get('/base/getRecentSSHLogins').then(function (response) {
var h = { headers: { 'X-CSRFToken': (typeof getCookie === 'function') ? getCookie('csrftoken') : '' } };
$http.get('/base/getRecentSSHLogins', h).then(function (response) {
$scope.loadingSSHLogins = false;
if (response.data && response.data.logins) {
$scope.sshLogins = response.data.logins;
@@ -979,7 +1055,8 @@ var dashboardStatsControllerFn = function ($scope, $http, $timeout) {
$scope.loadingSecurityAnalysis = false;
$scope.refreshSSHLogs = function() {
$scope.loadingSSHLogs = true;
$http.get('/base/getRecentSSHLogs').then(function (response) {
var h = { headers: { 'X-CSRFToken': (typeof getCookie === 'function') ? getCookie('csrftoken') : '' } };
$http.get('/base/getRecentSSHLogs', h).then(function (response) {
$scope.loadingSSHLogs = false;
if (response.data && response.data.logs) {
$scope.sshLogs = response.data.logs;

View File

@@ -26,17 +26,14 @@
<!-- Readability Fixes CSS -->
<link rel="stylesheet" type="text/css" href="{% static 'baseTemplate/assets/readability-fixes.css' %}?v={{ CP_VERSION }}">
<!-- Core Scripts (data-cfasync=false prevents Cloudflare Rocket Loader from breaking load order) -->
<!-- Core Scripts: Angular + system-status FIRST so dashboard works even when CDN/Tracking Prevention blocks jQuery/Bootstrap -->
<script src="{% static 'baseTemplate/angularjs.1.6.5.js' %}?v={{ CP_VERSION }}" data-cfasync="false"></script>
<script src="{% static 'baseTemplate/custom-js/system-status.js' %}?v={{ CP_VERSION }}&dashboard=3" data-cfasync="false"></script>
<script src="https://code.jquery.com/jquery-2.2.4.min.js" data-cfasync="false"></script>
<!-- Bootstrap JavaScript -->
<script src="{% static 'baseTemplate/assets/bootstrap/js/bootstrap.min.js' %}?v={{ CP_VERSION }}"></script>
<script src="{% static 'baseTemplate/bootstrap-toggle.min.js' %}?v={{ CP_VERSION }}"></script>
<script src="{% static 'baseTemplate/custom-js/qrious.min.js' %}?v={{ CP_VERSION }}"></script>
<!-- Chart.js must load before system-status.js (dashboard charts depend on it) -->
<script src="{% static 'baseTemplate/custom-js/chart.umd.min.js' %}?v={{ CP_VERSION }}" data-cfasync="false"></script>
<script src="{% static 'baseTemplate/custom-js/system-status.js' %}?v={{ CP_VERSION }}" data-cfasync="false"></script>
<!-- Font Awesome -->
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
@@ -45,9 +42,9 @@
<link rel="stylesheet" type="text/css" href="{% static 'baseTemplate/custom-js/pnotify.custom.min.css' %}?v={{ CP_VERSION }}">
<script src="{% static 'baseTemplate/custom-js/pnotify.custom.min.js' %}?v={{ CP_VERSION }}" data-cfasync="false"></script>
<!-- Select2 (required by FTP create account; load after jQuery; exclude from Rocket Loader to preserve order) -->
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/select2@4.0.13/dist/css/select2.min.css">
<script src="https://cdn.jsdelivr.net/npm/select2@4.0.13/dist/js/select2.full.min.js" data-cfasync="false"></script>
<!-- Select2 (required by FTP create account; local copy avoids CDN Tracking Prevention blocking) -->
<link rel="stylesheet" href="{% static 'baseTemplate/vendor/select2/select2.min.css' %}?v={{ CP_VERSION }}">
<script src="{% static 'baseTemplate/vendor/select2/select2.full.min.js' %}?v={{ CP_VERSION }}" data-cfasync="false"></script>
<!-- Modern Design System -->
<style>
@@ -2233,7 +2230,7 @@
<script src="{% static 'managePHP/managePHP.js' %}?v={{ CP_VERSION }}" data-cfasync="false"></script>
<script src="{% static 'serverLogs/serverLogs.js' %}?v={{ CP_VERSION }}" data-cfasync="false"></script>
<script src="{% static 'serverStatus/serverStatus.js' %}?v={{ CP_VERSION }}" data-cfasync="false"></script>
<script src="{% static 'firewall/firewall.js' %}?v={{ CP_VERSION }}&fw={{ FIREWALL_STATIC_VERSION }}" data-cfasync="false"></script>
<script src="{% static 'firewall/firewall.js' %}?v={{ CP_VERSION }}&fw={{ FIREWALL_STATIC_VERSION|default:CP_VERSION }}&cb=4" data-cfasync="false"></script>
<script src="{% static 'emailPremium/emailPremium.js' %}?v={{ CP_VERSION }}" data-cfasync="false"></script>
<script src="{% static 'manageServices/manageServices.js' %}?v={{ CP_VERSION }}" data-cfasync="false"></script>
<script src="{% static 'CLManager/CLManager.js' %}?v={{ CP_VERSION }}" data-cfasync="false"></script>
@@ -2345,10 +2342,7 @@
// Check if user has backup configured (you'll need to implement this API)
// For now, we'll show it by default unless they have a backup plan
// This should be replaced with an actual check
{% if not request.session.has_backup_configured %}
showBackupNotification();
{% endif %}
// Session check omitted - has_backup_configured may be absent in some views
// For demonstration, let's show it if URL doesn't contain 'OneClickBackups'
if (!window.location.href.includes('OneClickBackups')) {

View File

@@ -217,6 +217,59 @@ class FirewallManager:
final_json = json.dumps(final_dic)
return HttpResponse(final_json)
def modifyRule(self, userID=None, data=None):
"""
Update an existing firewall rule: remove old rule from firewalld and DB, add new rule.
data: id, name, proto, port, ruleIP (or ipAddress).
"""
try:
currentACL = ACLManager.loadedACL(userID)
if currentACL['admin'] != 1:
return ACLManager.loadErrorJson('status', 0)
rule_id = data.get('id')
new_name = (data.get('name') or data.get('ruleName') or '').strip()
new_proto = (data.get('proto') or data.get('ruleProtocol') or 'tcp').strip().lower()
new_port = (data.get('port') or data.get('rulePort') or '').strip()
new_ip = (data.get('ruleIP') or data.get('ipAddress') or '0.0.0.0/0').strip()
if not rule_id:
final_dic = {'status': 0, 'error_message': 'Rule ID is required'}
return HttpResponse(json.dumps(final_dic), content_type='application/json')
if not new_name:
final_dic = {'status': 0, 'error_message': 'Rule name is required'}
return HttpResponse(json.dumps(final_dic), content_type='application/json')
if new_proto not in ('tcp', 'udp'):
final_dic = {'status': 0, 'error_message': 'Protocol must be tcp or udp'}
return HttpResponse(json.dumps(final_dic), content_type='application/json')
if not new_port:
final_dic = {'status': 0, 'error_message': 'Port is required'}
return HttpResponse(json.dumps(final_dic), content_type='application/json')
existing = FirewallRules.objects.get(id=rule_id)
old_proto = existing.proto
old_port = existing.port
old_ip = existing.ipAddress
FirewallUtilities.deleteRule(old_proto, old_port, old_ip)
FirewallUtilities.addRule(new_proto, new_port, new_ip)
existing.name = new_name
existing.proto = new_proto
existing.port = new_port
existing.ipAddress = new_ip
existing.save()
final_dic = {'status': 1, 'modify_status': 1, 'error_message': 'None'}
return HttpResponse(json.dumps(final_dic), content_type='application/json')
except FirewallRules.DoesNotExist:
final_dic = {'status': 0, 'error_message': 'Rule not found'}
return HttpResponse(json.dumps(final_dic), content_type='application/json')
except BaseException as msg:
final_dic = {'status': 0, 'error_message': str(msg)}
return HttpResponse(json.dumps(final_dic), content_type='application/json')
def reloadFirewall(self, userID = None, data = None):
try:
@@ -1866,15 +1919,16 @@ class FirewallManager:
try:
admin = Administrator.objects.get(pk=userID)
if admin.acl.adminStatus != 1:
return ACLManager.loadError()
return ACLManager.loadErrorJson('status', 0)
active_banned_ips = []
db_ips = set() # IPs already added from DB (for merge with JSON)
current_time = int(time.time())
try:
from firewall.models import BannedIP
from django.db.models import Q
current_time = int(time.time())
banned_ips_queryset = BannedIP.objects.filter(
active=True
).filter(
@@ -1882,56 +1936,116 @@ class FirewallManager:
).order_by('-banned_on')
for banned_ip in banned_ips_queryset:
ip_data = {
'id': banned_ip.id,
'ip': banned_ip.ip_address,
'reason': banned_ip.reason,
'duration': banned_ip.duration,
'banned_on': banned_ip.get_banned_on_display(),
'expires': banned_ip.get_expires_display(),
'active': not banned_ip.is_expired() and banned_ip.active
}
if ip_data['active']:
active_banned_ips.append(ip_data)
except (ImportError, AttributeError) as e:
try:
ip_data = {
'id': banned_ip.id,
'ip': banned_ip.ip_address,
'reason': banned_ip.reason,
'duration': banned_ip.duration,
'banned_on': banned_ip.get_banned_on_display(),
'expires': banned_ip.get_expires_display(),
'active': not banned_ip.is_expired() and banned_ip.active
}
if ip_data['active']:
active_banned_ips.append(ip_data)
db_ips.add(banned_ip.ip_address)
except Exception as row_e:
import plogical.CyberCPLogFileWriter as _log
_log.CyberCPLogFileWriter.writeToFile('getBannedIPs: skip row %s: %s' % (getattr(banned_ip, 'ip_address', '?'), str(row_e)))
except Exception as e:
import plogical.CyberCPLogFileWriter as _log
_log.CyberCPLogFileWriter.writeToFile('getBannedIPs: using JSON fallback (%s)' % str(e))
active_banned_ips = []
_log.CyberCPLogFileWriter.writeToFile('getBannedIPs: DB read failed, merging with JSON (%s)' % str(e))
# If ORM returned nothing but we have the table, try raw SQL as fallback
if not active_banned_ips:
banned_ips, _ = self._load_banned_ips_store()
for b in banned_ips:
if not b.get('active', True):
continue
exp = b.get('expires')
if exp == 'Never' or exp is None:
try:
from django.db import connection
with connection.cursor() as cursor:
cursor.execute(
"""SELECT id, ip_address, reason, duration, banned_on, expires
FROM firewall_bannedips WHERE active = 1
AND (expires IS NULL OR expires > %s)
ORDER BY banned_on DESC""",
[current_time]
)
for row in cursor.fetchall():
bid, ip_addr, reason_val, duration_val, banned_on_val, expires_val = row
if ip_addr in db_ips:
continue
from datetime import datetime
if banned_on_val:
banned_on_str = banned_on_val.strftime('%Y-%m-%d %H:%M:%S') if hasattr(banned_on_val, 'strftime') else str(banned_on_val)
else:
banned_on_str = 'N/A'
if expires_val is None:
expires_str = 'Never'
else:
try:
expires_str = datetime.fromtimestamp(expires_val).strftime('%Y-%m-%d %H:%M:%S')
except Exception:
expires_str = 'Never'
active_banned_ips.append({
'id': bid,
'ip': ip_addr or '',
'reason': reason_val or '',
'duration': duration_val or 'permanent',
'banned_on': banned_on_str,
'expires': expires_str,
'active': True
})
db_ips.add(ip_addr)
except Exception as raw_e:
import plogical.CyberCPLogFileWriter as _log
_log.CyberCPLogFileWriter.writeToFile('getBannedIPs: raw fallback failed: %s' % str(raw_e))
# Always load JSON store and merge: show bans from both DB and JSON (e.g. bans from base/SSH logs)
banned_ips, _ = self._load_banned_ips_store()
for b in banned_ips:
if not b.get('active', True):
continue
ip_val = (b.get('ip') or '').strip()
if ip_val in db_ips:
continue
exp = b.get('expires')
if exp == 'Never' or exp is None:
expires_display = 'Never'
elif isinstance(exp, (int, float)):
from datetime import datetime
try:
expires_display = datetime.fromtimestamp(exp).strftime('%Y-%m-%d %H:%M:%S')
except Exception:
expires_display = 'Never'
elif isinstance(exp, (int, float)):
from datetime import datetime
try:
expires_display = datetime.fromtimestamp(exp).strftime('%Y-%m-%d %H:%M:%S')
except Exception:
expires_display = 'Never'
else:
expires_display = str(exp)
banned_on = b.get('banned_on')
if isinstance(banned_on, (int, float)):
from datetime import datetime
try:
banned_on = datetime.fromtimestamp(banned_on).strftime('%Y-%m-%d %H:%M:%S')
except Exception:
banned_on = 'N/A'
else:
banned_on = str(banned_on) if banned_on else 'N/A'
active_banned_ips.append({
'id': b.get('id'),
'ip': b.get('ip', ''),
'reason': b.get('reason', ''),
'duration': b.get('duration', 'permanent'),
'banned_on': banned_on,
'expires': expires_display,
'active': True
})
else:
expires_display = str(exp)
banned_on = b.get('banned_on')
if isinstance(banned_on, (int, float)):
from datetime import datetime
try:
banned_on = datetime.fromtimestamp(banned_on).strftime('%Y-%m-%d %H:%M:%S')
except Exception:
banned_on = 'N/A'
else:
banned_on = str(banned_on) if banned_on else 'N/A'
active_banned_ips.append({
'id': b.get('id'),
'ip': ip_val,
'reason': b.get('reason', ''),
'duration': b.get('duration', 'permanent'),
'banned_on': banned_on,
'expires': expires_display,
'active': True
})
# Optional server-side search: filter by IP or reason (case-insensitive substring)
# Normalize: strip query and compare against stripped IP/reason so "1.2.3.4" matches stored " 1.2.3.4 "
search_q = (data.get('search') or data.get('q') or '').strip() if data else ''
if search_q:
search_lower = search_q.lower()
def matches(b):
ip = (b.get('ip') or '').strip().lower()
reason = (b.get('reason') or '').strip().lower()
return search_lower in ip or search_lower in reason
active_banned_ips = [b for b in active_banned_ips if matches(b)]
total_count = len(active_banned_ips)
page = 1
@@ -2404,7 +2518,8 @@ class FirewallManager:
def exportFirewallRules(self, userID=None):
"""
Export all custom firewall rules to a JSON file, excluding default CyberPanel rules
Export all custom firewall rules. Format from POST body: { "format": "json" | "excel" }.
JSON: application/json file. Excel: text/csv (opens in Excel).
"""
try:
currentACL = ACLManager.loadedACL(userID)
@@ -2414,13 +2529,23 @@ class FirewallManager:
else:
return ACLManager.loadErrorJson('exportStatus', 0)
# Optional format from request body
export_format = 'json'
if getattr(self, 'request', None) and self.request.method == 'POST' and self.request.body:
try:
body = self.request.body
if isinstance(body, bytes):
body = body.decode('utf-8')
data = json.loads(body) if body.strip() else {}
export_format = (data.get('format') or 'json').strip().lower()
if export_format not in ('json', 'excel'):
export_format = 'json'
except Exception:
pass
# Get all firewall rules
rules = FirewallRules.objects.all()
# Default CyberPanel rules to exclude
default_rules = ['CyberPanel Admin', 'SSHCustom']
# Filter out default rules
custom_rules = []
for rule in rules:
if rule.name not in default_rules:
@@ -2430,24 +2555,31 @@ class FirewallManager:
'port': rule.port,
'ipAddress': rule.ipAddress
})
# Create export data with metadata
logging.CyberCPLogFileWriter.writeToFile(f"Firewall rules exported successfully. Total rules: {len(custom_rules)}")
if export_format == 'excel':
import csv
import io
buf = io.StringIO()
writer = csv.writer(buf)
writer.writerow(['name', 'proto', 'port', 'ipAddress'])
for r in custom_rules:
writer.writerow([r.get('name', ''), r.get('proto', ''), r.get('port', ''), r.get('ipAddress', '')])
content = buf.getvalue()
response = HttpResponse(content, content_type='text/csv; charset=utf-8')
response['Content-Disposition'] = f'attachment; filename="firewall_rules_export_{int(time.time())}.csv"'
return response
export_data = {
'version': '1.0',
'exported_at': time.strftime('%Y-%m-%d %H:%M:%S'),
'total_rules': len(custom_rules),
'rules': custom_rules
}
# Create JSON response with file download
json_content = json.dumps(export_data, indent=2)
logging.CyberCPLogFileWriter.writeToFile(f"Firewall rules exported successfully. Total rules: {len(custom_rules)}")
# Return file as download
response = HttpResponse(json_content, content_type='application/json')
response['Content-Disposition'] = f'attachment; filename="firewall_rules_export_{int(time.time())}.json"'
return response
except BaseException as msg:
@@ -2470,9 +2602,23 @@ class FirewallManager:
# Handle file upload
if hasattr(self.request, 'FILES') and 'import_file' in self.request.FILES:
import_file = self.request.FILES['import_file']
# Read file content
import_data = json.loads(import_file.read().decode('utf-8'))
raw = import_file.read().decode('utf-8')
name = (import_file.name or '').lower()
if name.endswith('.csv'):
import csv
import io
reader = csv.DictReader(io.StringIO(raw))
rules_list = []
for row in reader:
rules_list.append({
'name': str(row.get('name', '')).strip(),
'proto': str(row.get('proto', 'tcp')).strip().lower() or 'tcp',
'port': str(row.get('port', '')).strip(),
'ipAddress': str(row.get('ipAddress', '0.0.0.0/0')).strip() or '0.0.0.0/0'
})
import_data = {'rules': rules_list}
else:
import_data = json.loads(raw)
else:
# Fallback to file path method
import_file_path = data.get('import_file_path', '')
@@ -2582,13 +2728,13 @@ class FirewallManager:
try:
for banned_ip in BannedIP.objects.all().order_by('-banned_on'):
banned_records.append({
'id': banned_ip.id,
'ip': banned_ip.ip_address,
'reason': banned_ip.reason,
'duration': banned_ip.duration,
'banned_on': banned_ip.get_banned_on_display(),
'expires': banned_ip.get_expires_display(),
'active': banned_ip.active and not banned_ip.is_expired()
'id': int(banned_ip.id),
'ip': str(banned_ip.ip_address or ''),
'reason': str(banned_ip.reason or ''),
'duration': str(banned_ip.duration or 'permanent'),
'banned_on': str(banned_ip.get_banned_on_display() or 'N/A'),
'expires': str(banned_ip.get_expires_display() or 'Never'),
'active': bool(banned_ip.active and not banned_ip.is_expired())
})
except Exception as e:
logging.CyberCPLogFileWriter.writeToFile(f'Error exporting banned IPs from DB: {str(e)}')
@@ -2600,12 +2746,12 @@ class FirewallManager:
for b in banned_ips:
banned_records.append({
'id': b.get('id'),
'ip': b.get('ip', ''),
'reason': b.get('reason', ''),
'duration': b.get('duration', 'permanent'),
'banned_on': b.get('banned_on', 'N/A'),
'expires': b.get('expires', 'Never'),
'active': b.get('active', True)
'ip': str(b.get('ip') or ''),
'reason': str(b.get('reason') or ''),
'duration': str(b.get('duration') or 'permanent'),
'banned_on': str(b.get('banned_on') if b.get('banned_on') is not None else 'N/A'),
'expires': str(b.get('expires') if b.get('expires') is not None else 'Never'),
'active': bool(b.get('active', True))
})
export_data = {

View File

@@ -25,6 +25,12 @@ app.controller('firewallController', function ($scope, $http, $timeout) {
$scope.rulesLoading = true;
$scope.actionFailed = true;
$scope.actionSuccess = true;
$scope.showExportFormatModal = false;
$scope.showImportFormatModal = false;
$scope.exportRulesFormat = 'json';
$scope.importRulesFormat = 'json';
$scope.showModifyRuleModal = false;
$scope.modifyRuleData = { id: null, name: '', proto: 'tcp', port: '', ruleIP: '0.0.0.0/0' };
$scope.canNotAddRule = true;
$scope.ruleAdded = true;
@@ -88,6 +94,10 @@ app.controller('firewallController', function ($scope, $http, $timeout) {
$scope.bannedPageSize = 10;
$scope.bannedPageSizeOptions = [5, 10, 20, 30, 50, 100];
$scope.bannedTotalCount = 0;
// Modify Banned IP modal state
$scope.showModifyModal = false;
$scope.modifyBannedIPData = { ip: '', id: null, reason: '', duration: '24h' };
// Initialize banned IPs array - start as null so template shows empty state
// Will be set to array after API call
@@ -99,12 +109,6 @@ app.controller('firewallController', function ($scope, $http, $timeout) {
$scope.banReason = '';
$scope.banDuration = '24h';
$scope.bannedIPSearch = '';
// Modify banned IP modal
$scope.showModifyModal = false;
$scope.modifyBannedIPData = { ip: '', reason: '', duration: '24h', id: null };
// Search filter for banned IPs (by IP, reason, or status)
$scope.searchBannedIPFilter = function(item) {
var q = ($scope.bannedIPSearch || '').toLowerCase().trim();
if (!q) return true;
@@ -114,42 +118,17 @@ app.controller('firewallController', function ($scope, $http, $timeout) {
return ip.indexOf(q) !== -1 || reason.indexOf(q) !== -1 || status.indexOf(q) !== -1;
};
// Modify banned IP modal functions
$scope.openModifyModal = function(bannedIP) {
if (!bannedIP) return;
$scope.modifyBannedIPData = {
id: bannedIP.id,
ip: bannedIP.ip,
reason: bannedIP.reason || '',
duration: bannedIP.duration || '24h'
};
$scope.showModifyModal = true;
$scope.onBannedSearchChange = function() {
if ($scope.bannedSearchTimeout) $timeout.cancel($scope.bannedSearchTimeout);
$scope.bannedSearchTimeout = $timeout(function() {
$scope.bannedPage = 1;
if (typeof populateBannedIPs === 'function') populateBannedIPs();
}, 350);
};
$scope.closeModifyModal = function() { $scope.showModifyModal = false; };
$scope.saveModifyBannedIP = function() {
if (!$scope.modifyBannedIPData || !$scope.modifyBannedIPData.id) return;
$scope.bannedIPsLoading = true;
$http.post('/firewall/modifyBannedIP', {
id: $scope.modifyBannedIPData.id,
reason: $scope.modifyBannedIPData.reason,
duration: $scope.modifyBannedIPData.duration
}, { headers: { 'X-CSRFToken': getCookie('csrftoken') } }).then(
function(response) {
$scope.bannedIPsLoading = false;
if (response.data && response.data.status === 1) {
$scope.bannedIPActionSuccess = false;
$scope.closeModifyModal();
populateBannedIPs();
} else {
$scope.bannedIPActionFailed = false;
$scope.bannedIPErrorMessage = (response.data && response.data.error_message) || 'Unknown error';
}
},
function() {
$scope.bannedIPsLoading = false;
$scope.bannedIPCouldNotConnect = false;
}
);
$scope.runBannedSearch = function() {
$scope.bannedPage = 1;
if (typeof populateBannedIPs === 'function') populateBannedIPs();
};
firewallStatus();
@@ -189,10 +168,11 @@ app.controller('firewallController', function ($scope, $http, $timeout) {
};
var postData = {
page: $scope.bannedPage || 1,
page_size: $scope.bannedPageSize || 10
page: Math.max(1, parseInt($scope.bannedPage, 10) || 1),
page_size: Math.max(5, Math.min(100, parseInt($scope.bannedPageSize, 10) || 10)),
search: ($scope.bannedIPSearch || '').trim()
};
console.log('Making request to:', url, 'page:', postData.page, 'page_size:', postData.page_size);
console.log('Making request to:', url, 'page:', postData.page, 'page_size:', postData.page_size, 'search:', postData.search);
console.log('CSRF Token:', csrfToken ? 'Found (' + csrfToken.substring(0, 10) + '...)' : 'MISSING!');
$http.post(url, postData, config).then(
@@ -226,6 +206,7 @@ app.controller('firewallController', function ($scope, $http, $timeout) {
$scope.bannedTotalCount = res.total_count != null ? res.total_count : bannedIPsArray.length;
$scope.bannedPage = Math.max(1, res.page != null ? res.page : 1);
$scope.bannedPageSize = res.page_size != null ? res.page_size : 10;
$scope.bannedPageInput = $scope.bannedPage;
console.log('After assignment - scope.bannedIPs:', $scope.bannedIPs);
console.log('After assignment - scope.bannedIPs.length:', $scope.bannedIPs ? $scope.bannedIPs.length : 'undefined');
console.log('After assignment - activeTab:', $scope.activeTab);
@@ -273,18 +254,23 @@ app.controller('firewallController', function ($scope, $http, $timeout) {
};
$scope.goToBannedPage = function(page) {
var totalP = $scope.bannedTotalPages();
if (page < 1 || page > totalP) return;
$scope.bannedPage = page;
var totalP = Math.max(1, $scope.bannedTotalPages());
var p = parseInt(page, 10);
if (isNaN(p) || p < 1 || p > totalP) return;
$scope.bannedPage = p;
populateBannedIPs();
};
$scope.goToBannedPageByInput = function() {
var n = parseInt($scope.bannedPageInput, 10);
if (isNaN(n) || n < 1) n = 1;
var maxP = $scope.bannedTotalPages();
if (n > maxP) n = maxP;
$scope.bannedPageInput = n;
$scope.goToBannedPage(n);
var self = $scope;
$timeout(function() {
var n = parseInt(self.bannedPageInput, 10);
if (isNaN(n) || n < 1) n = self.bannedPage || 1;
var maxP = Math.max(1, self.bannedTotalPages());
n = Math.min(Math.max(1, n), maxP);
self.bannedPageInput = n;
self.bannedPage = n;
populateBannedIPs();
}, 0);
};
$scope.bannedTotalPages = function() {
var size = $scope.bannedPageSize || 10;
@@ -305,6 +291,8 @@ app.controller('firewallController', function ($scope, $http, $timeout) {
return total === 0 ? 0 : Math.min(start + size - 1, total);
};
$scope.setBannedPageSize = function() {
var size = parseInt($scope.bannedPageSize, 10);
$scope.bannedPageSize = (size >= 5 && size <= 100) ? size : 10;
$scope.bannedPage = 1;
populateBannedIPs();
};
@@ -426,8 +414,8 @@ app.controller('firewallController', function ($scope, $http, $timeout) {
url = "/firewall/getCurrentRules";
var data = {
page: $scope.rulesPage || 1,
page_size: $scope.rulesPageSize || 10
page: Math.max(1, parseInt($scope.rulesPage, 10) || 1),
page_size: Math.max(5, Math.min(100, parseInt($scope.rulesPageSize, 10) || 10))
};
var config = {
headers: {
@@ -458,18 +446,22 @@ app.controller('firewallController', function ($scope, $http, $timeout) {
}
$scope.goToRulesPage = function(page) {
var totalP = $scope.rulesTotalPages();
if (page < 1 || page > totalP) return;
$scope.rulesPage = page;
var totalP = Math.max(1, $scope.rulesTotalPages());
var p = parseInt(page, 10);
if (isNaN(p) || p < 1 || p > totalP) return;
$scope.rulesPage = p;
populateCurrentRecords();
};
$scope.goToRulesPageByInput = function() {
var n = parseInt($scope.rulesPageInput, 10);
if (isNaN(n) || n < 1) n = 1;
var maxP = $scope.rulesTotalPages();
if (n > maxP) n = maxP;
$scope.rulesPageInput = n;
$scope.goToRulesPage(n);
$timeout(function() {
var n = parseInt($scope.rulesPageInput, 10);
if (isNaN(n) || n < 1) n = $scope.rulesPage || 1;
var maxP = Math.max(1, $scope.rulesTotalPages());
n = Math.min(Math.max(1, n), maxP);
$scope.rulesPageInput = n;
$scope.rulesPage = n;
populateCurrentRecords();
}, 0);
};
$scope.rulesTotalPages = function() {
var size = $scope.rulesPageSize || 10;
@@ -490,10 +482,73 @@ app.controller('firewallController', function ($scope, $http, $timeout) {
return total === 0 ? 0 : Math.min(start + size - 1, total);
};
$scope.setRulesPageSize = function() {
var size = parseInt($scope.rulesPageSize, 10);
$scope.rulesPageSize = (size >= 5 && size <= 100) ? size : 10;
$scope.rulesPage = 1;
populateCurrentRecords();
};
$scope.openModifyRuleModal = function(rule) {
if (!rule) return;
$scope.modifyRuleData = {
id: rule.id,
name: rule.name || '',
proto: rule.proto || 'tcp',
port: String(rule.port || ''),
ruleIP: rule.ipAddress || rule.ruleIP || '0.0.0.0/0'
};
$scope.showModifyRuleModal = true;
};
$scope.closeModifyRuleModal = function() {
$scope.showModifyRuleModal = false;
$scope.modifyRuleData = { id: null, name: '', proto: 'tcp', port: '', ruleIP: '0.0.0.0/0' };
};
$scope.saveModifyRule = function() {
var d = $scope.modifyRuleData;
if (!d.name || !d.name.trim()) {
$scope.actionFailed = false;
$scope.actionSuccess = true;
$scope.errorMessage = 'Rule name is required';
return;
}
if (!d.port || !String(d.port).trim()) {
$scope.actionFailed = false;
$scope.actionSuccess = true;
$scope.errorMessage = 'Port is required';
return;
}
$scope.rulesLoading = false;
var url = '/firewall/modifyRule';
var data = {
id: d.id,
name: d.name.trim(),
proto: d.proto || 'tcp',
port: String(d.port).trim(),
ruleIP: (d.ruleIP || '0.0.0.0/0').trim()
};
var config = { headers: { 'X-CSRFToken': getCookie('csrftoken') } };
$http.post(url, data, config).then(function(response) {
$scope.rulesLoading = true;
if (response.data && response.data.status === 1) {
$scope.closeModifyRuleModal();
$scope.actionFailed = true;
$scope.actionSuccess = false;
populateCurrentRecords();
} else {
$scope.actionFailed = false;
$scope.actionSuccess = true;
$scope.errorMessage = (response.data && response.data.error_message) || 'Modify failed';
}
}, function() {
$scope.rulesLoading = true;
$scope.actionFailed = false;
$scope.actionSuccess = true;
$scope.errorMessage = 'Could not connect to server. Please refresh this page.';
});
};
$scope.deleteRule = function (id, proto, port, ruleIP) {
$scope.rulesLoading = false;
@@ -975,40 +1030,102 @@ app.controller('firewallController', function ($scope, $http, $timeout) {
});
};
// Export banned IPs (triggers file download)
$scope.exportBannedIPs = function() {
var url = '/firewall/exportBannedIPs';
var form = document.createElement('form');
form.method = 'POST';
form.action = url;
form.style.display = 'none';
var csrfInput = document.createElement('input');
csrfInput.type = 'hidden';
csrfInput.name = 'csrfmiddlewaretoken';
csrfInput.value = getCookie('csrftoken');
form.appendChild(csrfInput);
document.body.appendChild(form);
form.submit();
document.body.removeChild(form);
$scope.openModifyModal = function(bannedIP) {
if (!bannedIP) return;
$scope.modifyBannedIPData = {
id: bannedIP.id,
ip: bannedIP.ip || bannedIP.ip_address || '',
reason: bannedIP.reason || '',
duration: bannedIP.duration || '24h'
};
$scope.showModifyModal = true;
};
$scope.closeModifyModal = function() {
$scope.showModifyModal = false;
$scope.modifyBannedIPData = { ip: '', id: null, reason: '', duration: '24h' };
};
$scope.saveModifyBannedIP = function() {
var d = $scope.modifyBannedIPData;
if (!d.reason || !d.reason.trim()) {
$scope.bannedIPActionFailed = false;
$scope.bannedIPErrorMessage = 'Reason is required';
return;
}
$scope.bannedIPsLoading = true;
$scope.bannedIPActionFailed = true;
$scope.bannedIPActionSuccess = true;
$scope.bannedIPCouldNotConnect = true;
var data = {
id: d.id,
ip: d.ip,
reason: d.reason.trim(),
duration: d.duration || '24h'
};
var url = '/firewall/modifyBannedIP';
var config = { headers: { 'X-CSRFToken': getCookie('csrftoken') } };
$http.post(url, data, config).then(function(response) {
$scope.bannedIPsLoading = false;
if (response.data && response.data.status === 1) {
$scope.bannedIPActionSuccess = false;
$scope.closeModifyModal();
populateBannedIPs();
} else {
$scope.bannedIPActionFailed = false;
$scope.bannedIPErrorMessage = (response.data && response.data.error_message) || 'Modify failed';
}
}, function(error) {
$scope.bannedIPsLoading = false;
$scope.bannedIPCouldNotConnect = false;
});
};
$scope.exportBannedIPs = function() {
$scope.bannedIPsLoading = false;
var url = "/firewall/exportBannedIPs";
var config = { headers: { 'X-CSRFToken': getCookie('csrftoken') }, responseType: 'text' };
$http.post(url, {}, config).then(function(response) {
$scope.bannedIPsLoading = true;
var raw = response.data;
try {
var data = typeof raw === 'string' ? JSON.parse(raw) : raw;
if (data && data.exportStatus === 0) {
$scope.bannedIPActionFailed = false;
$scope.bannedIPErrorMessage = data.error_message || 'Export failed';
return;
}
} catch (e) {}
var content = typeof raw === 'string' ? raw : JSON.stringify(raw, null, 2);
var blob = new Blob([content], { type: 'application/json' });
var a = document.createElement('a');
a.href = window.URL.createObjectURL(blob);
a.download = 'banned_ips_export_' + (Date.now() / 1000 | 0) + '.json';
a.click();
window.URL.revokeObjectURL(a.href);
$scope.bannedIPActionSuccess = false;
}, function() {
$scope.bannedIPsLoading = true;
$scope.bannedIPCouldNotConnect = false;
});
};
// Import banned IPs (file picker -> POST)
$scope.importBannedIPs = function() {
var input = document.createElement('input');
input.type = 'file';
input.accept = '.json';
input.style.display = 'none';
input.onchange = function(ev) {
var file = ev.target && ev.target.files[0];
input.onchange = function(event) {
var file = event.target.files[0];
if (!file) return;
$scope.bannedIPsLoading = false;
var formData = new FormData();
formData.append('import_file', file);
formData.append('csrfmiddlewaretoken', getCookie('csrftoken'));
$scope.bannedIPsLoading = false;
$http.post('/firewall/importBannedIPs', formData, {
var config = {
headers: { 'X-CSRFToken': getCookie('csrftoken'), 'Content-Type': undefined },
transformRequest: angular.identity
}).then(function(response) {
};
$http.post("/firewall/importBannedIPs", formData, config).then(function(response) {
$scope.bannedIPsLoading = true;
if (response.data && response.data.importStatus === 1) {
$scope.bannedIPActionSuccess = false;
@@ -1027,101 +1144,125 @@ app.controller('firewallController', function ($scope, $http, $timeout) {
document.body.removeChild(input);
};
// Export/Import Firewall Rules Functions
// Export/Import Firewall Rules: format modals and actions
$scope.exportRules = function () {
$scope.rulesLoading = false;
$scope.showExportFormatModal = true;
$scope.exportRulesFormat = $scope.exportRulesFormat || 'json';
};
$scope.closeExportFormatModal = function () {
$scope.showExportFormatModal = false;
};
$scope.confirmExportRules = function () {
$scope.showExportFormatModal = false;
var format = $scope.exportRulesFormat || 'json';
doExportRules(format);
};
function doExportRules(format) {
$scope.rulesLoading = true;
$scope.actionFailed = true;
$scope.actionSuccess = true;
url = "/firewall/exportFirewallRules";
var data = {};
var url = "/firewall/exportFirewallRules";
var data = { format: format };
var config = {
headers: {
'X-CSRFToken': getCookie('csrftoken')
}
headers: { 'X-CSRFToken': getCookie('csrftoken') },
responseType: 'text'
};
$http.post(url, data, config).then(exportSuccess, exportError);
function exportSuccess(response) {
$scope.rulesLoading = true;
// Check if response is JSON (error) or file download
if (typeof response.data === 'string' && response.data.includes('{')) {
$scope.rulesLoading = false;
var raw = response.data;
if (typeof raw === 'string' && raw.indexOf('{') === 0) {
try {
var errorData = JSON.parse(response.data);
if (errorData.exportStatus === 0) {
var parsed = JSON.parse(raw);
if (parsed.exportStatus === 0) {
$scope.actionFailed = false;
$scope.actionSuccess = true;
$scope.errorMessage = errorData.error_message;
$scope.errorMessage = parsed.error_message || 'Export failed';
return;
}
} catch (e) {
// If not JSON, assume it's the file content
}
} catch (e) {}
}
// If we get here, it's a successful file download
var contentType = format === 'excel' ? 'text/csv' : 'application/json';
var ext = format === 'excel' ? 'csv' : 'json';
var blob = new Blob([typeof raw === 'string' ? raw : JSON.stringify(raw)], { type: contentType });
var a = document.createElement('a');
a.href = window.URL.createObjectURL(blob);
a.download = 'firewall_rules_export_' + (Date.now() / 1000 | 0) + '.' + ext;
a.click();
window.URL.revokeObjectURL(a.href);
$scope.actionFailed = true;
$scope.actionSuccess = false;
}
function exportError(response) {
$scope.rulesLoading = true;
function exportError() {
$scope.rulesLoading = false;
$scope.actionFailed = false;
$scope.actionSuccess = true;
$scope.errorMessage = "Could not connect to server. Please refresh this page.";
}
};
}
$scope.importRules = function () {
// Create file input element
$scope.showImportFormatModal = true;
$scope.importRulesFormat = $scope.importRulesFormat || 'json';
};
$scope.closeImportFormatModal = function () {
$scope.showImportFormatModal = false;
};
$scope.confirmImportRules = function () {
$scope.showImportFormatModal = false;
var format = $scope.importRulesFormat || 'json';
var accept = format === 'excel' ? '.csv' : '.json';
var input = document.createElement('input');
input.type = 'file';
input.accept = '.json';
input.accept = accept;
input.style.display = 'none';
input.onchange = function(event) {
var file = event.target.files[0];
if (file) {
var reader = new FileReader();
reader.onload = function(e) {
try {
var importData = JSON.parse(e.target.result);
// Validate file format
if (!importData.rules || !Array.isArray(importData.rules)) {
if (format === 'json') {
var reader = new FileReader();
reader.onload = function(e) {
try {
var importData = JSON.parse(e.target.result);
if (!importData.rules || !Array.isArray(importData.rules)) {
$scope.$apply(function() {
$scope.actionFailed = false;
$scope.actionSuccess = true;
$scope.errorMessage = "Invalid import file format. Please select a valid firewall rules export file.";
});
return;
}
uploadImportFile(file);
} catch (err) {
$scope.$apply(function() {
$scope.actionFailed = false;
$scope.actionSuccess = true;
$scope.errorMessage = "Invalid import file format. Please select a valid firewall rules export file.";
$scope.errorMessage = "Invalid JSON file. Please select a valid firewall rules export file.";
});
return;
}
// Upload file to server
uploadImportFile(file);
} catch (error) {
$scope.$apply(function() {
$scope.actionFailed = false;
$scope.actionSuccess = true;
$scope.errorMessage = "Invalid JSON file. Please select a valid firewall rules export file.";
});
}
};
reader.readAsText(file);
};
reader.readAsText(file);
} else {
uploadImportFile(file);
}
}
};
document.body.appendChild(input);
input.click();
document.body.removeChild(input);
};
function uploadImportFile(file) {
$scope.rulesLoading = false;
$scope.rulesLoading = true;
$scope.actionFailed = true;
$scope.actionSuccess = true;
@@ -1139,35 +1280,29 @@ app.controller('firewallController', function ($scope, $http, $timeout) {
$http.post("/firewall/importFirewallRules", formData, config).then(importSuccess, importError);
function importSuccess(response) {
$scope.rulesLoading = true;
if (response.data.importStatus === 1) {
$scope.rulesLoading = false;
var res = response.data;
if (typeof res === 'string') {
try { res = JSON.parse(res); } catch (e) { res = {}; }
}
if (res && res.importStatus === 1) {
$scope.actionFailed = true;
$scope.actionSuccess = false;
// Refresh rules list
populateCurrentRecords();
// Show import summary
var summary = `Import completed successfully!\n` +
`Imported: ${response.data.imported_count} rules\n` +
`Skipped: ${response.data.skipped_count} rules\n` +
`Errors: ${response.data.error_count} rules`;
if (response.data.errors && response.data.errors.length > 0) {
summary += `\n\nErrors:\n${response.data.errors.join('\n')}`;
var summary = "Import completed successfully!\nImported: " + (res.imported_count || 0) + " rules\nSkipped: " + (res.skipped_count || 0) + " rules\nErrors: " + (res.error_count || 0) + " rules";
if (res.errors && res.errors.length > 0) {
summary += "\n\nErrors:\n" + res.errors.join("\n");
}
alert(summary);
} else {
$scope.actionFailed = false;
$scope.actionSuccess = true;
$scope.errorMessage = response.data.error_message;
$scope.errorMessage = (res && res.error_message) ? res.error_message : "Import failed.";
}
}
function importError(response) {
$scope.rulesLoading = true;
function importError() {
$scope.rulesLoading = false;
$scope.actionFailed = false;
$scope.actionSuccess = true;
$scope.errorMessage = "Could not connect to server. Please refresh this page.";

View File

@@ -316,6 +316,15 @@
box-shadow: 0 0 0 3px var(--firewall-focus-shadow, rgba(239, 68, 68, 0.1));
}
/* Search input: neutral blue focus (not red) so it doesn't look read-only/error */
.firewall-search-input {
border-color: var(--border-color, #e2e8f0);
}
.firewall-search-input:focus {
border-color: var(--accent-color, #5b5fcf);
box-shadow: 0 0 0 3px rgba(91, 95, 207, 0.15);
}
.select-control {
appearance: none;
background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' fill='none' viewBox='0 0 20 20'%3e%3cpath stroke='%236b7280' stroke-linecap='round' stroke-linejoin='round' stroke-width='1.5' d='M6 8l4 4 4-4'/%3e%3c/svg%3e");
@@ -381,6 +390,16 @@
border-bottom: 1px solid var(--border-color, #e8e9ff);
}
.rules-table th:last-child,
.banned-table th:last-child {
text-align: center;
}
.rules-table td:last-child,
.banned-table td:last-child {
text-align: center;
}
.rules-table td {
padding: 1rem;
border-bottom: 1px solid var(--border-light, #f1f5f9);
@@ -400,6 +419,7 @@
display: flex;
flex-wrap: wrap;
gap: 0.5rem;
justify-content: center;
}
.rule-id {
@@ -621,8 +641,21 @@
gap: 0.35rem;
margin-left: 1rem;
}
.pagination-size select {
min-width: 4rem;
.pagination-size select,
.pagination-size-select {
min-width: 4.5rem;
padding: 0.35rem 0.5rem;
border: 1px solid var(--border-color, #e2e8f0);
border-radius: 4px;
font-size: 0.875rem;
background: var(--bg-secondary, white);
cursor: pointer;
appearance: menulist;
display: inline-block;
}
.banned-ips-per-page select.pagination-size-select {
appearance: menulist;
display: inline-block;
}
.pagination-size-btns {
display: inline-flex;
@@ -825,6 +858,27 @@
box-shadow: 0 4px 12px rgba(239, 68, 68, 0.4);
}
.btn-search {
background: var(--accent-color, #5b5fcf);
color: var(--bg-secondary, white);
border: none;
padding: 0.75rem 1.25rem;
border-radius: 10px;
font-weight: 600;
font-size: 0.9rem;
cursor: pointer;
transition: all 0.3s ease;
display: inline-flex;
align-items: center;
gap: 0.5rem;
}
.btn-search:hover {
background: var(--accent-hover, #4f46e5);
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(91, 95, 207, 0.4);
}
.banned-list-section {
padding: 2rem;
}
@@ -924,6 +978,7 @@
.actions {
display: flex;
gap: 0.5rem;
justify-content: center;
}
.btn-unban {
@@ -1144,7 +1199,7 @@
{% get_current_language as LANGUAGE_CODE %}
<!-- Current language: {{ LANGUAGE_CODE }} -->
<div class="modern-container" ng-controller="firewallController" ng-cloak>
<div class="modern-container" ng-controller="firewallController">
<!-- Page Header -->
<div class="page-header">
<div class="header-content">
@@ -1213,8 +1268,8 @@
</button>
</div>
<!-- Rules Panel (ng-if so second tab is not in DOM until needed; $watch loads data when switching) -->
<div class="rules-panel" ng-if="activeTab === 'rules'">
<!-- Rules Panel (ng-show so panel is always in DOM; visibility toggled by tab) -->
<div class="rules-panel" ng-show="activeTab === 'rules'">
<div class="panel-header">
<div class="panel-title">
<div class="panel-icon">
@@ -1242,6 +1297,50 @@
</div>
</div>
</div>
<!-- Export Rules Format Modal -->
<div class="modify-modal-overlay" ng-class="{'show': showExportFormatModal}" ng-click="closeExportFormatModal()">
<div class="modify-modal" ng-click="$event.stopPropagation()">
<h3>{% trans "Export Firewall Rules" %}</h3>
<p style="margin: 0 0 1rem 0; color: var(--text-secondary, #64748b);">{% trans "Choose export format:" %}</p>
<div class="form-group">
<label class="form-label">
<input type="radio" ng-model="exportRulesFormat" value="json"> {% trans "JSON" %}
</label>
<label class="form-label">
<input type="radio" ng-model="exportRulesFormat" value="excel"> {% trans "Excel (CSV)" %}
</label>
</div>
<div class="modify-modal-actions">
<button type="button" class="btn-cancel" ng-click="closeExportFormatModal()">{% trans "Cancel" %}</button>
<button type="button" class="btn-save" ng-click="confirmExportRules()">
<i class="fas fa-download"></i> {% trans "Export" %}
</button>
</div>
</div>
</div>
<!-- Import Rules Format Modal -->
<div class="modify-modal-overlay" ng-class="{'show': showImportFormatModal}" ng-click="closeImportFormatModal()">
<div class="modify-modal" ng-click="$event.stopPropagation()">
<h3>{% trans "Import Firewall Rules" %}</h3>
<p style="margin: 0 0 1rem 0; color: var(--text-secondary, #64748b);">{% trans "Choose file format:" %}</p>
<div class="form-group">
<label class="form-label">
<input type="radio" ng-model="importRulesFormat" value="json"> {% trans "JSON" %}
</label>
<label class="form-label">
<input type="radio" ng-model="importRulesFormat" value="excel"> {% trans "Excel (CSV)" %}
</label>
</div>
<div class="modify-modal-actions">
<button type="button" class="btn-cancel" ng-click="closeImportFormatModal()">{% trans "Cancel" %}</button>
<button type="button" class="btn-save" ng-click="confirmImportRules()">
<i class="fas fa-upload"></i> {% trans "Choose file" %}
</button>
</div>
</div>
</div>
<!-- Add Rule Section -->
<div ng-hide="rulesDetails" class="add-rule-section">
@@ -1321,6 +1420,13 @@
<span class="port-number">{$ rule.port $}</span>
</td>
<td>
<button type="button"
ng-click="$parent.openModifyRuleModal(rule); $event.stopPropagation()"
class="btn-modify"
title="{% trans 'Modify Rule' %}">
<i class="fas fa-edit"></i>
{% trans "Modify" %}
</button>
<button type="button"
ng-click="deleteRule(rule.id, rule.proto, rule.port, rule.ipAddress)"
class="btn-delete"
@@ -1332,16 +1438,48 @@
</tbody>
</table>
</div>
<!-- Modify Firewall Rule Modal -->
<div class="modify-modal-overlay" ng-class="{'show': showModifyRuleModal}" ng-click="closeModifyRuleModal()">
<div class="modify-modal" ng-click="$event.stopPropagation()">
<h3>{% trans "Modify Firewall Rule" %}</h3>
<div class="form-group">
<label class="form-label">{% trans "Rule Name" %}</label>
<input type="text" class="form-control" ng-model="modifyRuleData.name" placeholder="{% trans 'e.g., Allow SSH' %}">
</div>
<div class="form-group">
<label class="form-label">{% trans "Protocol" %}</label>
<select ng-model="modifyRuleData.proto" class="form-control">
<option value="tcp">TCP</option>
<option value="udp">UDP</option>
</select>
</div>
<div class="form-group">
<label class="form-label">{% trans "IP Address" %}</label>
<input type="text" class="form-control" ng-model="modifyRuleData.ruleIP" placeholder="0.0.0.0/0">
</div>
<div class="form-group">
<label class="form-label">{% trans "Port" %}</label>
<input type="text" class="form-control" ng-model="modifyRuleData.port" placeholder="e.g., 80">
</div>
<div class="modify-modal-actions">
<button type="button" class="btn-cancel" ng-click="closeModifyRuleModal()">{% trans "Cancel" %}</button>
<button type="button" class="btn-save" ng-click="saveModifyRule()">
<i class="fas fa-save"></i> {% trans "Save" %}
</button>
</div>
</div>
</div>
<!-- Firewall Rules Pagination (show when there are rules or a total count) -->
<div class="pagination-bar" ng-if="(rules && rules.length > 0) || (rulesTotalCount > 0)">
<div class="pagination-info">
<span>{% trans "Showing" %} {$ rulesRangeStart() || 0 $} {$ rulesRangeEnd() || 0 $} {% trans "of" %} {$ rulesTotalCount || rules.length || 0 $}</span>
<span class="pagination-size">
<span class="pagination-goto-label">{% trans "Per page:" %}</span>
<span class="pagination-size-btns">
<button type="button" ng-repeat="n in rulesPageSizeOptions" class="pagination-size-btn" ng-class="{active: rulesPageSize === n}" ng-click="rulesPageSize = n; setRulesPageSize()">{$ n $}</button>
</span>
<label class="pagination-goto-label">{% trans "Per page:" %}</label>
<select ng-model="rulesPageSize" ng-change="setRulesPageSize()" class="pagination-size-select" title="{% trans 'Items per page' %}">
<option ng-repeat="n in rulesPageSizeOptions" ng-value="n">{$ n $}</option>
</select>
</span>
</div>
<div class="pagination-controls">
@@ -1354,7 +1492,7 @@
</button>
<span class="pagination-goto">
<label class="pagination-goto-label">{% trans "Go to page" %}</label>
<input type="number" min="1" ng-attr-max="rulesTotalPages()" ng-model="rulesPageInput" class="pagination-goto-input" placeholder="{$ rulesPage $}">
<input type="number" min="1" ng-attr-max="rulesTotalPages()" ng-model="rulesPageInput" class="pagination-goto-input" placeholder="{$ rulesPage $}" ng-keypress="$event.keyCode === 13 && goToRulesPageByInput()">
<button type="button" class="pagination-goto-btn" ng-click="goToRulesPageByInput()" title="{% trans 'Go' %}">{% trans "Go" %}</button>
</span>
</div>
@@ -1388,7 +1526,7 @@
</div>
<!-- Banned IPs Panel -->
<div class="banned-ips-panel" ng-if="activeTab === 'banned'">
<div class="banned-ips-panel" ng-show="activeTab === 'banned'">
<div class="panel-header">
<div class="panel-title">
<div class="panel-icon">
@@ -1455,11 +1593,19 @@
<label class="form-label" style="display: block; margin-bottom: 0.5rem; font-weight: 600;">
<i class="fas fa-search"></i> {% trans "Search banned IPs" %}
</label>
<input type="text"
class="form-control"
ng-model="bannedIPSearch"
placeholder="{% trans 'Search by IP address, reason or status (Active/Expired)...' %}"
style="max-width: 400px;">
<div style="display: inline-flex; align-items: center; gap: 0.5rem; flex-wrap: wrap;">
<input type="text"
class="form-control firewall-search-input"
ng-model="bannedIPSearch"
ng-change="onBannedSearchChange()"
ng-keypress="$event.keyCode === 13 && runBannedSearch(); $event.preventDefault()"
placeholder="{% trans 'Search by IP address, reason or status (Active/Expired)...' %}"
autocomplete="off"
style="max-width: 400px;">
<button type="button" class="btn-search" ng-click="runBannedSearch()" title="{% trans 'Run search' %}">
<i class="fas fa-search"></i> {% trans "Search" %}
</button>
</div>
</div>
<!-- Banned IPs List -->
@@ -1477,7 +1623,7 @@
</tr>
</thead>
<tbody>
<tr ng-repeat="bannedIP in bannedIPs | filter:searchBannedIPFilter as filteredBannedList">
<tr ng-repeat="bannedIP in bannedIPs | filter:searchBannedIPFilter">
<td class="ip-address">
<i class="fas fa-globe"></i>
{$ bannedIP.ip $}
@@ -1534,11 +1680,11 @@
<div class="pagination-bar" ng-if="(bannedIPs && bannedIPs.length > 0) || (bannedTotalCount > 0)">
<div class="pagination-info">
<span>{% trans "Showing" %} {$ bannedRangeStart() || 0 $} {$ bannedRangeEnd() || 0 $} {% trans "of" %} {$ bannedTotalCount || bannedIPs.length || 0 $}</span>
<span class="pagination-size">
<span class="pagination-goto-label">{% trans "Per page:" %}</span>
<span class="pagination-size-btns">
<button type="button" ng-repeat="n in bannedPageSizeOptions" class="pagination-size-btn" ng-class="{active: bannedPageSize === n}" ng-click="bannedPageSize = n; setBannedPageSize()">{$ n $}</button>
</span>
<span class="pagination-size banned-ips-per-page">
<label class="pagination-goto-label">{% trans "Per page:" %}</label>
<select ng-model="bannedPageSize" ng-change="setBannedPageSize()" class="pagination-size-select" title="{% trans 'Items per page' %}" aria-label="{% trans 'Items per page' %}">
<option ng-repeat="n in bannedPageSizeOptions" ng-value="n">{$ n $}</option>
</select>
</span>
</div>
<div class="pagination-controls">
@@ -1551,20 +1697,20 @@
</button>
<span class="pagination-goto">
<label class="pagination-goto-label">{% trans "Go to page" %}</label>
<input type="number" min="1" ng-attr-max="bannedTotalPages()" ng-model="bannedPageInput" class="pagination-goto-input" placeholder="{$ bannedPage $}">
<input type="number" min="1" ng-attr-max="bannedTotalPages()" ng-model="bannedPageInput" class="pagination-goto-input" placeholder="{$ bannedPage $}" ng-keypress="$event.keyCode === 13 && goToBannedPageByInput(); $event.preventDefault()" aria-label="{% trans 'Go to page' %}">
<button type="button" class="pagination-goto-btn" ng-click="goToBannedPageByInput()" title="{% trans 'Go' %}">{% trans "Go" %}</button>
</span>
</div>
</div>
<!-- Empty State: no banned IPs at all -->
<div ng-if="!bannedIPs || bannedIPs.length == 0" class="empty-state">
<!-- Empty State: no banned IPs at all (only when not searching) -->
<div ng-if="(!bannedIPs || bannedIPs.length == 0) && (bannedIPSearch || '').length === 0" class="empty-state">
<i class="fas fa-shield-check empty-icon"></i>
<h3 class="empty-title">{% trans "No Banned IPs" %}</h3>
<p class="empty-text">{% trans "All IP addresses are currently allowed. Add banned IPs to block suspicious or malicious traffic." %}</p>
</div>
<!-- Empty State: search returned no matches -->
<div ng-if="bannedIPs && bannedIPs.length > 0 && (bannedIPSearch || '').length > 0 && ((bannedIPs | filter:searchBannedIPFilter).length === 0)" class="empty-state">
<!-- Empty State: search returned no matches (server or client filter) -->
<div ng-if="(bannedIPSearch || '').length > 0 && ((!bannedIPs || bannedIPs.length == 0) || (bannedIPs | filter:searchBannedIPFilter).length === 0)" class="empty-state">
<i class="fas fa-search empty-icon"></i>
<h3 class="empty-title">{% trans "No matching banned IPs" %}</h3>
<p class="empty-text">{% trans "No banned IPs match your search. Try a different IP, reason or status (Active/Expired)." %}</p>
@@ -1626,24 +1772,77 @@
<script>
(function(){
// Sync tab from hash on load and hashchange (back/forward). Tab clicks are handled by ng-click only.
var nav = document.getElementById('firewall-tab-nav');
if (!nav) return;
function loadTabViaAngularScope(tab) {
if (!window.angular) return false;
var container = document.querySelector('.modern-container[ng-controller="firewallController"]') || document.querySelector('.modern-container');
if (!container) return false;
try {
var scope = window.angular.element(container).scope();
if (!scope) return false;
scope.$evalAsync(function() {
scope.activeTab = tab;
if (tab === 'banned' && scope.populateBannedIPs) scope.populateBannedIPs();
else if (tab === 'rules' && scope.populateCurrentRecords) scope.populateCurrentRecords();
});
return true;
} catch (e) {
return false;
}
}
function loadTab(tab) {
if (!tab || (tab !== 'rules' && tab !== 'banned')) return;
var done = false;
if (window.__firewallLoadTab) {
try { window.__firewallLoadTab(tab); done = true; } catch (e) {}
}
if (!done) {
done = loadTabViaAngularScope(tab);
}
if (!done) {
setTimeout(function() {
if (window.__firewallLoadTab) { try { window.__firewallLoadTab(tab); } catch (e) {} }
else { loadTabViaAngularScope(tab); }
}, 50);
setTimeout(function() {
if (window.__firewallLoadTab) { try { window.__firewallLoadTab(tab); } catch (e) {} }
else { loadTabViaAngularScope(tab); }
}, 200);
setTimeout(function() {
if (window.__firewallLoadTab) { try { window.__firewallLoadTab(tab); } catch (e) {} }
else { loadTabViaAngularScope(tab); }
}, 500);
}
}
function onTabButtonClick(btn) {
var tab = btn && btn.getAttribute('data-tab');
if (!tab) return;
window.location.hash = (tab === 'banned') ? '#banned-ips' : '#rules';
loadTab(tab);
}
nav.addEventListener('click', function(e) {
var btn = (e.target && e.target.closest) ? e.target.closest('.tab-button[data-tab]') : null;
if (btn && nav.contains(btn)) onTabButtonClick(btn);
}, false);
nav.addEventListener('mousedown', function(e) {
var btn = (e.target && e.target.closest) ? e.target.closest('.tab-button[data-tab]') : null;
if (btn && nav.contains(btn)) onTabButtonClick(btn);
}, false);
function loadTabFromHash() {
var h = (window.location.hash || '').replace(/^#/, '');
var tab = (h === 'banned-ips') ? 'banned' : 'rules';
if (window.__firewallLoadTab) {
try { window.__firewallLoadTab(tab); } catch (e) {}
}
loadTab(tab);
}
var h = (window.location.hash || '').replace(/^#/, '');
if (h === 'banned-ips') loadTabFromHash();
window.addEventListener('hashchange', loadTabFromHash);
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', function() {
setTimeout(loadTabFromHash, 100);
setTimeout(loadTabFromHash, 400);
});
} else {
setTimeout(loadTabFromHash, 100);
setTimeout(loadTabFromHash, 400);
}
setTimeout(loadTabFromHash, 150);
setTimeout(loadTabFromHash, 500);
})();
</script>

View File

@@ -1,4 +1,5 @@
from django.shortcuts import redirect
from django.http import HttpResponse
import json
from loginSystem.views import loadLoginPage
from plogical.processUtilities import ProcessUtilities
@@ -680,16 +681,22 @@ def getBannedIPs(request):
try:
userID = request.session['userID']
fm = FirewallManager()
try:
body = request.body
if isinstance(body, bytes):
body = body.decode('utf-8')
data = json.loads(body) if body and body.strip() else {}
except (json.JSONDecodeError, Exception):
data = {}
return fm.getBannedIPs(userID, data)
data = {}
if request.method == 'POST':
try:
body = request.body
if isinstance(body, bytes):
body = body.decode('utf-8')
data = json.loads(body) if body and body.strip() else {}
except (json.JSONDecodeError, Exception):
pass
# GET also supported (no body); pagination uses defaults
result = fm.getBannedIPs(userID, data)
# Ensure we return JSON (FirewallManager may return HttpResponse)
return result
except KeyError:
return redirect(loadLoginPage)
final_dic = {'status': 0, 'error_message': 'Session expired. Please log in again.', 'bannedIPs': [], 'total_count': 0}
return HttpResponse(json.dumps(final_dic), content_type='application/json', status=403)
def addBannedIP(request):
try:
@@ -804,7 +811,7 @@ def deleteBannedIP(request):
def exportFirewallRules(request):
try:
userID = request.session['userID']
fm = FirewallManager()
fm = FirewallManager(request)
return fm.exportFirewallRules(userID)
except KeyError:
return redirect(loadLoginPage)

View File

@@ -531,15 +531,18 @@ app.controller('homePageStatus', function ($scope, $http, $timeout) {
////////////
function increment() {
$('.box').hide();
var boxes = document.querySelectorAll ? document.querySelectorAll('.box') : [];
for (var i = 0; i < boxes.length; i++) boxes[i].style.display = 'none';
setTimeout(function () {
$('.box').show();
for (var j = 0; j < boxes.length; j++) boxes[j].style.display = '';
}, 100);
}
increment();
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', increment);
} else {
increment();
}
////////////

File diff suppressed because it is too large Load Diff

View File

@@ -10,7 +10,7 @@ function getCookie(name) {
if (document.cookie && document.cookie !== '') {
var cookies = document.cookie.split(';');
for (var i = 0; i < cookies.length; i++) {
var cookie = jQuery.trim(cookies[i]);
var cookie = (cookies[i] || '').replace(/^\s+|\s+$/g, '');
// Does this cookie string begin with the name we want?
if (cookie.substring(0, name.length + 1) === (name + '=')) {
cookieValue = decodeURIComponent(cookie.substring(name.length + 1));
@@ -39,6 +39,77 @@ function randomPassword(length) {
window.app = angular.module('CyberCP', []);
var app = window.app; // Local reference for this file
// MUST be first: register dashboard controller before any other setup (avoids ctrlreg when CDN/Tracking Prevention blocks scripts)
app.controller('dashboardStatsController', ['$scope', '$http', '$timeout', function ($scope, $http, $timeout) {
$scope.cpuUsage = 0; $scope.ramUsage = 0; $scope.diskUsage = 0; $scope.cpuCores = 0;
$scope.ramTotalMB = 0; $scope.diskTotalGB = 0; $scope.diskFreeGB = 0;
$scope.totalUsers = 0; $scope.totalSites = 0; $scope.totalWPSites = 0;
$scope.totalDBs = 0; $scope.totalEmails = 0; $scope.totalFTPUsers = 0;
$scope.topProcesses = []; $scope.sshLogins = []; $scope.sshLogs = [];
$scope.loadingTopProcesses = true; $scope.loadingSSHLogins = true; $scope.loadingSSHLogs = true;
$scope.blockedIPs = {}; $scope.blockingIP = null; $scope.securityAlerts = [];
var opts = { headers: { 'X-CSRFToken': (typeof getCookie === 'function') ? getCookie('csrftoken') : '' } };
try {
$http.get('/base/getSystemStatus', opts).then(function (r) {
if (r && r.data && r.data.status === 1) {
$scope.cpuUsage = r.data.cpuUsage || 0; $scope.ramUsage = r.data.ramUsage || 0;
$scope.diskUsage = r.data.diskUsage || 0; $scope.cpuCores = r.data.cpuCores || 0;
$scope.ramTotalMB = r.data.ramTotalMB || 0; $scope.diskTotalGB = r.data.diskTotalGB || 0;
$scope.diskFreeGB = r.data.diskFreeGB || 0;
}
});
$http.get('/base/getDashboardStats', opts).then(function (r) {
if (r && r.data && r.data.status === 1) {
$scope.totalUsers = r.data.total_users || 0; $scope.totalSites = r.data.total_sites || 0;
$scope.totalWPSites = r.data.total_wp_sites || 0; $scope.totalDBs = r.data.total_dbs || 0;
$scope.totalEmails = r.data.total_emails || 0; $scope.totalFTPUsers = r.data.total_ftp_users || 0;
}
});
$http.get('/base/getRecentSSHLogins', opts).then(function (r) {
$scope.loadingSSHLogins = false;
$scope.sshLogins = (r && r.data && r.data.logins) ? r.data.logins : [];
}, function () { $scope.loadingSSHLogins = false; $scope.sshLogins = []; });
$http.get('/base/getRecentSSHLogs', opts).then(function (r) {
$scope.loadingSSHLogs = false;
$scope.sshLogs = (r && r.data && r.data.logs) ? r.data.logs : [];
}, function () { $scope.loadingSSHLogs = false; $scope.sshLogs = []; });
$http.get('/base/getTopProcesses', opts).then(function (r) {
$scope.loadingTopProcesses = false;
$scope.topProcesses = (r && r.data && r.data.status === 1 && r.data.processes) ? r.data.processes : [];
}, function () { $scope.loadingTopProcesses = false; $scope.topProcesses = []; });
if (typeof $timeout === 'function') { $timeout(function() { /* refresh */ }, 10000); }
} catch (e) { /* ignore */ }
}]);
// Overview CPU/RAM/Disk cards use systemStatusInfo register early so data loads even if later script fails
app.controller('systemStatusInfo', ['$scope', '$http', '$timeout', function ($scope, $http, $timeout) {
$scope.uptimeLoaded = false;
$scope.uptime = 'Loading...';
$scope.cpuUsage = 0; $scope.ramUsage = 0; $scope.diskUsage = 0;
$scope.cpuCores = 0; $scope.ramTotalMB = 0; $scope.diskTotalGB = 0; $scope.diskFreeGB = 0;
$scope.getSystemStatus = function() { fetchStatus(); };
function fetchStatus() {
try {
var csrf = (typeof getCookie === 'function') ? getCookie('csrftoken') : '';
$http.get('/base/getSystemStatus', { headers: { 'X-CSRFToken': csrf } }).then(function (r) {
if (r && r.data && r.data.status === 1) {
$scope.cpuUsage = r.data.cpuUsage != null ? r.data.cpuUsage : 0;
$scope.ramUsage = r.data.ramUsage != null ? r.data.ramUsage : 0;
$scope.diskUsage = r.data.diskUsage != null ? r.data.diskUsage : 0;
$scope.cpuCores = r.data.cpuCores != null ? r.data.cpuCores : 0;
$scope.ramTotalMB = r.data.ramTotalMB != null ? r.data.ramTotalMB : 0;
$scope.diskTotalGB = r.data.diskTotalGB != null ? r.data.diskTotalGB : 0;
$scope.diskFreeGB = r.data.diskFreeGB != null ? r.data.diskFreeGB : 0;
$scope.uptime = r.data.uptime || 'N/A';
}
$scope.uptimeLoaded = true;
}, function() { $scope.uptime = 'Unavailable'; $scope.uptimeLoaded = true; });
if (typeof $timeout === 'function') { $timeout(fetchStatus, 60000); }
} catch (e) { $scope.uptimeLoaded = true; }
}
fetchStatus();
}]);
var globalScope;
function GlobalRespSuccess(response) {
@@ -122,9 +193,17 @@ app.controller('systemStatusInfo', function ($scope, $http, $timeout) {
$scope.uptimeLoaded = false;
$scope.uptime = 'Loading...';
// Defaults so template never shows undefined (avoids raw {$ cpuUsage $} when API is slow or fails)
$scope.cpuUsage = 0;
$scope.ramUsage = 0;
$scope.diskUsage = 0;
$scope.cpuCores = 0;
$scope.ramTotalMB = 0;
$scope.diskTotalGB = 0;
$scope.diskFreeGB = 0;
getStuff();
$scope.getSystemStatus = function() {
getStuff();
};
@@ -138,17 +217,15 @@ app.controller('systemStatusInfo', function ($scope, $http, $timeout) {
function ListInitialData(response) {
$scope.cpuUsage = response.data.cpuUsage;
$scope.ramUsage = response.data.ramUsage;
$scope.diskUsage = response.data.diskUsage;
// Total system information
$scope.cpuCores = response.data.cpuCores;
$scope.ramTotalMB = response.data.ramTotalMB;
$scope.diskTotalGB = response.data.diskTotalGB;
$scope.diskFreeGB = response.data.diskFreeGB;
// Get uptime if available
$scope.cpuUsage = response.data.cpuUsage != null ? response.data.cpuUsage : 0;
$scope.ramUsage = response.data.ramUsage != null ? response.data.ramUsage : 0;
$scope.diskUsage = response.data.diskUsage != null ? response.data.diskUsage : 0;
$scope.cpuCores = response.data.cpuCores != null ? response.data.cpuCores : 0;
$scope.ramTotalMB = response.data.ramTotalMB != null ? response.data.ramTotalMB : 0;
$scope.diskTotalGB = response.data.diskTotalGB != null ? response.data.diskTotalGB : 0;
$scope.diskFreeGB = response.data.diskFreeGB != null ? response.data.diskFreeGB : 0;
if (response.data.uptime) {
$scope.uptime = response.data.uptime;
$scope.uptimeLoaded = true;
@@ -162,6 +239,9 @@ app.controller('systemStatusInfo', function ($scope, $http, $timeout) {
function cantLoadInitialData(response) {
$scope.uptime = 'Unavailable';
$scope.uptimeLoaded = true;
$scope.cpuUsage = 0;
$scope.ramUsage = 0;
$scope.diskUsage = 0;
}
$timeout(getStuff, 60000); // Update every minute
@@ -273,11 +353,11 @@ app.controller('adminController', function ($scope, $http, $timeout) {
}
if (!Boolean(response.data.deleteZone)) {
$('.addDeleteRecords').hide();
$('.deleteZone').hide();
}
if (!Boolean(response.data.addDeleteRecords)) {
$('.deleteDatabase').hide();
$('.addDeleteRecords').hide();
}
// Email Management
@@ -557,15 +637,18 @@ app.controller('homePageStatus', function ($scope, $http, $timeout) {
////////////
function increment() {
$('.box').hide();
var boxes = document.querySelectorAll ? document.querySelectorAll('.box') : [];
for (var i = 0; i < boxes.length; i++) boxes[i].style.display = 'none';
setTimeout(function () {
$('.box').show();
for (var j = 0; j < boxes.length; j++) boxes[j].style.display = '';
}, 100);
}
increment();
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', increment);
} else {
increment();
}
////////////
@@ -902,7 +985,8 @@ app.controller('OnboardingCP', function ($scope, $http, $timeout, $window) {
});
app.controller('dashboardStatsController', function ($scope, $http, $timeout) {
// Single implementation registered under both names for compatibility (some templates/caches use newDashboardStat)
var dashboardStatsControllerFn = function ($scope, $http, $timeout) {
console.log('dashboardStatsController initialized');
// Card values
@@ -922,7 +1006,8 @@ app.controller('dashboardStatsController', function ($scope, $http, $timeout) {
$scope.errorTopProcesses = '';
$scope.refreshTopProcesses = function() {
$scope.loadingTopProcesses = true;
$http.get('/base/getTopProcesses').then(function (response) {
var h = { headers: { 'X-CSRFToken': (typeof getCookie === 'function') ? getCookie('csrftoken') : '' } };
$http.get('/base/getTopProcesses', h).then(function (response) {
$scope.loadingTopProcesses = false;
if (response.data && response.data.status === 1 && response.data.processes) {
$scope.topProcesses = response.data.processes;
@@ -941,7 +1026,8 @@ app.controller('dashboardStatsController', function ($scope, $http, $timeout) {
$scope.errorSSHLogins = '';
$scope.refreshSSHLogins = function() {
$scope.loadingSSHLogins = true;
$http.get('/base/getRecentSSHLogins').then(function (response) {
var h = { headers: { 'X-CSRFToken': (typeof getCookie === 'function') ? getCookie('csrftoken') : '' } };
$http.get('/base/getRecentSSHLogins', h).then(function (response) {
$scope.loadingSSHLogins = false;
if (response.data && response.data.logins) {
$scope.sshLogins = response.data.logins;
@@ -969,7 +1055,8 @@ app.controller('dashboardStatsController', function ($scope, $http, $timeout) {
$scope.loadingSecurityAnalysis = false;
$scope.refreshSSHLogs = function() {
$scope.loadingSSHLogs = true;
$http.get('/base/getRecentSSHLogs').then(function (response) {
var h = { headers: { 'X-CSRFToken': (typeof getCookie === 'function') ? getCookie('csrftoken') : '' } };
$http.get('/base/getRecentSSHLogs', h).then(function (response) {
$scope.loadingSSHLogs = false;
if (response.data && response.data.logs) {
$scope.sshLogs = response.data.logs;
@@ -1461,6 +1548,12 @@ app.controller('dashboardStatsController', function ($scope, $http, $timeout) {
var pollInterval = 2000; // ms
var maxPoints = 30;
// Expose so switchTab can create charts on first tab click if they weren't created at load
window.cyberPanelSetupChartsIfNeeded = function() {
if (window.trafficChart && window.diskIOChart && window.cpuChart) return;
try { setupCharts(); } catch (e) { console.error('cyberPanelSetupChartsIfNeeded:', e); }
};
function pollDashboardStats() {
console.log('[dashboardStatsController] pollDashboardStats() called');
console.log('[dashboardStatsController] Fetching dashboard stats from /base/getDashboardStats');
@@ -1519,8 +1612,8 @@ app.controller('dashboardStatsController', function ($scope, $http, $timeout) {
}
function pollTraffic() {
console.log('pollTraffic called');
$http.get('/base/getTrafficStats').then(function(response) {
if (!response || !response.data) return;
if (response.data.admin_only) {
// Hide chart for non-admin users
$scope.hideSystemCharts = true;
@@ -1568,13 +1661,16 @@ app.controller('dashboardStatsController', function ($scope, $http, $timeout) {
}
lastRx = rx; lastTx = tx;
} else {
console.log('pollTraffic error or no data:', response);
console.warn('pollTraffic: no data or status', response.data);
}
}).catch(function(err) {
console.warn('pollTraffic failed:', err);
});
}
function pollDiskIO() {
$http.get('/base/getDiskIOStats').then(function(response) {
if (!response || !response.data) return;
if (response.data.admin_only) {
// Hide chart for non-admin users
$scope.hideSystemCharts = true;
@@ -1613,11 +1709,14 @@ app.controller('dashboardStatsController', function ($scope, $http, $timeout) {
}
lastDiskRead = read; lastDiskWrite = write;
}
}).catch(function(err) {
console.warn('pollDiskIO failed:', err);
});
}
function pollCPU() {
$http.get('/base/getCPULoadGraph').then(function(response) {
if (!response || !response.data) return;
if (response.data.admin_only) {
// Hide chart for non-admin users
$scope.hideSystemCharts = true;
@@ -1656,13 +1755,34 @@ app.controller('dashboardStatsController', function ($scope, $http, $timeout) {
}
lastCPUTimes = cpuTimes;
}
}).catch(function(err) {
console.warn('pollCPU failed:', err);
});
}
function setupCharts() {
console.log('setupCharts called, initializing charts...');
var trafficCtx = document.getElementById('trafficChart').getContext('2d');
trafficChart = new Chart(trafficCtx, {
function setupCharts(retryCount) {
retryCount = retryCount || 0;
if (typeof Chart === 'undefined') {
if (retryCount < 3) {
$timeout(function() { setupCharts(retryCount + 1); }, 400);
}
return;
}
var trafficEl = document.getElementById('trafficChart');
if (!trafficEl) {
if (retryCount < 5) {
$timeout(function() { setupCharts(retryCount + 1); }, 300);
}
return;
}
try {
var trafficCtx = trafficEl.getContext('2d');
} catch (e) {
console.error('trafficChart getContext failed:', e);
return;
}
try {
trafficChart = new Chart(trafficCtx, {
type: 'line',
data: {
labels: [],
@@ -1754,7 +1874,9 @@ app.controller('dashboardStatsController', function ($scope, $http, $timeout) {
console.log('trafficChart resized and updated after setup.');
}
}, 500);
var diskCtx = document.getElementById('diskIOChart').getContext('2d');
var diskEl = document.getElementById('diskIOChart');
if (!diskEl) { console.warn('diskIOChart canvas not found'); return; }
var diskCtx = diskEl.getContext('2d');
diskIOChart = new Chart(diskCtx, {
type: 'line',
data: {
@@ -1839,7 +1961,10 @@ app.controller('dashboardStatsController', function ($scope, $http, $timeout) {
layout: { padding: { top: 10, bottom: 10, left: 10, right: 10 } }
}
});
var cpuCtx = document.getElementById('cpuChart').getContext('2d');
window.diskIOChart = diskIOChart;
var cpuEl = document.getElementById('cpuChart');
if (!cpuEl) { console.warn('cpuChart canvas not found'); return; }
var cpuCtx = cpuEl.getContext('2d');
cpuChart = new Chart(cpuCtx, {
type: 'line',
data: {
@@ -1912,6 +2037,10 @@ app.controller('dashboardStatsController', function ($scope, $http, $timeout) {
layout: { padding: { top: 10, bottom: 10, left: 10, right: 10 } }
}
});
window.cpuChart = cpuChart;
} catch (e) {
console.error('setupCharts error:', e);
}
// Redraw charts on tab shown
$("a[data-toggle='tab']").on('shown.bs.tab', function (e) {
@@ -1944,19 +2073,20 @@ app.controller('dashboardStatsController', function ($scope, $http, $timeout) {
$scope.refreshSSHLogs();
$timeout(function() {
// Check if user is admin before setting up charts
// Always create charts so Traffic/Disk IO/CPU tabs have something to show; admin check only affects hideSystemCharts
setupCharts();
$http.get('/base/getAdminStatus').then(function(response) {
if (response.data && response.data.admin === 1) {
setupCharts();
if (response.data && (response.data.admin === 1 || response.data.admin === true)) {
$scope.hideSystemCharts = false;
} else {
$scope.hideSystemCharts = true;
}
}).catch(function() {
// If error, assume non-admin and hide charts
}).catch(function(err) {
console.warn('getAdminStatus failed:', err);
$scope.hideSystemCharts = true;
});
// Start polling for all stats
// Start polling for all stats (data feeds charts)
function pollAll() {
pollDashboardStats();
pollTraffic();
@@ -1966,7 +2096,7 @@ app.controller('dashboardStatsController', function ($scope, $http, $timeout) {
$timeout(pollAll, pollInterval);
}
pollAll();
}, 500);
}, 800);
// SSH User Activity Modal
$scope.showSSHActivityModal = false;
@@ -2412,4 +2542,6 @@ app.controller('dashboardStatsController', function ($scope, $http, $timeout) {
$scope.closeSSHActivityModal();
}
};
});
};
app.controller('dashboardStatsController', dashboardStatsControllerFn);
app.controller('newDashboardStat', dashboardStatsControllerFn);

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,64 @@
# Firewall Rules & Banned IPs Making Sure Changes Load
If Firewall Rules or Banned IPs dont show the latest UI (Modify buttons, Per-page dropdown, Search, etc.), do the following.
## 1. Sync firewall JavaScript (when you change firewall JS)
The panel can serve `firewall/firewall.js` from the **firewall app** (`firewall/static/firewall/firewall.js`) or from **static/** after collectstatic. The cache-buster uses the newest mtime from:
- `firewall/static/firewall/firewall.js`
- `static/firewall/firewall.js`
- `public/static/firewall/firewall.js`
So that the query param updates when any of these change.
**After editing `firewall/static/firewall/firewall.js`, sync copies so all paths are up to date:**
```bash
# From repo root
mkdir -p static/firewall public/static/firewall
cp firewall/static/firewall/firewall.js static/firewall/
cp firewall/static/firewall/firewall.js public/static/firewall/
```
## 2. Templates
The firewall **HTML** comes from the **firewall app** template:
- `firewall/templates/firewall/firewall.html`
Django loads it when you open the firewall page. There is no separate copy under `static/` or `baseTemplate/` for that page. So any change in `firewall/templates/firewall/firewall.html` is used as long as the running app is your repo (or a deploy that includes this file).
## 3. Where CyberPanel stores files (production)
- **Production root:** `/usr/local/CyberCP` the full repo (including `firewall/`, `baseTemplate/`, etc.) lives here after install/upgrade.
- **Upgrade sync:** `upgrade_modules/09_sync.sh` runs from that directory (`git fetch` / checkout / pull). After sync, it copies **baseTemplate** static and **firewall** static into `public/static/` so LiteSpeed serves the latest dashboard and firewall JS.
- **Firewall code:** `firewall/templates/firewall/firewall.html` and `firewall/static/firewall/firewall.js` under `/usr/local/CyberCP`. LiteSpeed serves `/static/firewall/firewall.js` from `public/static/firewall/firewall.js`, which is updated by the upgrade script.
## 4. Production (e.g. `/usr/local/CyberCP`) manual deploy
If the panel runs from an **installed** path (e.g. `/usr/local/CyberCP`), that directory is often a copy of the repo. Then:
- Replace or update the firewall app there with your repo version:
- `firewall/templates/firewall/firewall.html`
- `firewall/static/firewall/firewall.js`
- If the installer or deploy uses `static/` or `public/static/`, copy the same `firewall.js` there too (as in step 1).
- Restart the app server (e.g. Gunicorn/LiteSpeed) so Django and static file serving use the new files.
## 5. Browser cache
The script tag uses a cache-buster:
`?v={{ CP_VERSION }}&fw={{ FIREWALL_STATIC_VERSION }}&cb=4`
- Do a **hard refresh**: Ctrl+Shift+R (Windows/Linux) or Cmd+Shift+R (Mac).
- Or clear cache for the panel site and reload.
## 6. Quick checklist
- [ ] `firewall/static/firewall/firewall.js` has the latest code.
- [ ] Synced to `static/firewall/firewall.js` and `public/static/firewall/firewall.js` (see step 1).
- [ ] `firewall/templates/firewall/firewall.html` has the latest markup (Modify buttons, modals, Per page dropdown).
- [ ] If using an installed path, copy updated firewall app (and static copies) there and restart the server.
- [ ] Hard refresh (or clear cache) in the browser.
After this, Firewall Rules and Banned IPs should load the correct layout and Modify buttons.

View File

@@ -34,6 +34,12 @@ Sync_CyberCP_To_Latest() {
cp -r /usr/local/CyberCP/baseTemplate/static/baseTemplate/* /usr/local/CyberCP/public/static/baseTemplate/ 2>/dev/null || true
echo -e "[$(date +"%Y-%m-%d %H:%M:%S")] Synced baseTemplate static to public/static" | tee -a /var/log/cyberpanel_upgrade_debug.log
fi
# Firewall UI (firewall.js) so Firewall Rules and Banned IPs load correct layout and Modify buttons
if [[ -d /usr/local/CyberCP/public/static ]] && [[ -f /usr/local/CyberCP/firewall/static/firewall/firewall.js ]]; then
mkdir -p /usr/local/CyberCP/public/static/firewall
cp -f /usr/local/CyberCP/firewall/static/firewall/firewall.js /usr/local/CyberCP/public/static/firewall/ 2>/dev/null && \
echo -e "[$(date +"%Y-%m-%d %H:%M:%S")] Synced firewall static to public/static" | tee -a /var/log/cyberpanel_upgrade_debug.log || true
fi
if [[ $sync_code -eq 0 ]]; then
echo -e "[$(date +"%Y-%m-%d %H:%M:%S")] Sync completed. Current HEAD: $(git -C /usr/local/CyberCP rev-parse HEAD 2>/dev/null || echo 'unknown')" | tee -a /var/log/cyberpanel_upgrade_debug.log
else