mirror of
https://github.com/usmannasir/cyberpanel.git
synced 2026-02-17 03:56:48 +01:00
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:
@@ -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
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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')) {
|
||||
|
||||
@@ -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 = {
|
||||
|
||||
@@ -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.";
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
@@ -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
64
to-do/FIREWALL-LOAD-CHANGES.md
Normal file
64
to-do/FIREWALL-LOAD-CHANGES.md
Normal file
@@ -0,0 +1,64 @@
|
||||
# Firewall Rules & Banned IPs – Making Sure Changes Load
|
||||
|
||||
If Firewall Rules or Banned IPs don’t 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.
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user