From 88e1faa07dc01dd0c778bfb388a0c88a4c443bee Mon Sep 17 00:00:00 2001 From: master3395 Date: Wed, 28 Jan 2026 03:12:08 +0100 Subject: [PATCH 1/4] Fix syntax error in cyberpanel_upgrade.sh - add missing if statement for lscpd service check --- cyberpanel_upgrade.sh | 3 +++ 1 file changed, 3 insertions(+) diff --git a/cyberpanel_upgrade.sh b/cyberpanel_upgrade.sh index 4d5952302..046f0dbb5 100644 --- a/cyberpanel_upgrade.sh +++ b/cyberpanel_upgrade.sh @@ -1543,6 +1543,9 @@ fi # Test if CyberPanel is accessible echo -e "\nšŸ” Testing CyberPanel accessibility..." + +# Check if lscpd service is running +if systemctl is-active --quiet lscpd 2>/dev/null; then echo "╔═════════════════════════════════════════════════════════════════════════════════════════════════════════════╗" echo "ā•‘ ā•‘" echo "ā•‘ 🌐 ACCESS YOUR CYBERPANEL: ā•‘" From 1e87abb9786ba5e5b49ab806ae3233fefa3bcf18 Mon Sep 17 00:00:00 2001 From: master3395 Date: Wed, 28 Jan 2026 03:13:14 +0100 Subject: [PATCH 2/4] Update preUpgrade.sh to use master3395/cyberpanel repository instead of usmannasir/cyberpanel --- preUpgrade.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/preUpgrade.sh b/preUpgrade.sh index 7417c7058..043e45cba 100644 --- a/preUpgrade.sh +++ b/preUpgrade.sh @@ -15,6 +15,6 @@ fi echo "Upgrading CyberPanel from branch: $BRANCH_NAME" rm -f /usr/local/cyberpanel_upgrade.sh -wget -O /usr/local/cyberpanel_upgrade.sh https://raw.githubusercontent.com/usmannasir/cyberpanel/$BRANCH_NAME/cyberpanel_upgrade.sh 2>/dev/null +wget -O /usr/local/cyberpanel_upgrade.sh https://raw.githubusercontent.com/master3395/cyberpanel/$BRANCH_NAME/cyberpanel_upgrade.sh 2>/dev/null chmod 700 /usr/local/cyberpanel_upgrade.sh /usr/local/cyberpanel_upgrade.sh $@ From 4cf263d205c03f29df8dbd24cddcb9f060b93856 Mon Sep 17 00:00:00 2001 From: master3395 Date: Wed, 28 Jan 2026 23:15:10 +0100 Subject: [PATCH 3/4] Fix firewall banned IP modify functionality - Remove readonly attribute from IP address field in modify modal - Make IP address field editable when modal opens - Update modifyBannedIP function to include IP address in save request - Remove conflicting onclick handler that was blocking ng-click - Update help text to reflect that IP address can be changed - Add IP address validation in modifyBannedIP function --- firewall/static/firewall/firewall.js | 1283 ++++++++++++++++++++- firewall/templates/firewall/firewall.html | 216 +++- 2 files changed, 1477 insertions(+), 22 deletions(-) diff --git a/firewall/static/firewall/firewall.js b/firewall/static/firewall/firewall.js index 495b88ec0..1853a3544 100644 --- a/firewall/static/firewall/firewall.js +++ b/firewall/static/firewall/firewall.js @@ -2,10 +2,120 @@ * Created by usman on 9/5/17. */ +// TEST: Verify file is loading +console.log('šŸ”„šŸ”„šŸ”„ firewall.js FILE LOADED šŸ”„šŸ”„šŸ”„'); +console.log('Timestamp:', new Date().toISOString()); +if (typeof app === 'undefined') { + console.error('āŒ ERROR: app (AngularJS module) is not defined!'); +} else { + console.log('āœ… app (AngularJS module) is defined'); +} + +// Global function for inline onclick handler - MUST be defined BEFORE controller +window.handleModifyClick = function(buttonElement, event) { + console.log('========================================'); + console.log('=== handleModifyClick CALLED ==='); + console.log('========================================'); + console.log('Button:', buttonElement); + + if (event) { + event.preventDefault(); + event.stopPropagation(); + } + + var ip = buttonElement.getAttribute('data-ip'); + var id = buttonElement.getAttribute('data-id'); + console.log('IP:', ip, 'ID:', id); + + var controllerEl = document.querySelector('[ng-controller="firewallController"]'); + if (!controllerEl) { + console.error('Controller element not found'); + alert('Error: Controller not found. Please refresh the page.'); + return false; + } + + var scope = angular.element(controllerEl).scope(); + if (!scope) { + console.error('AngularJS scope not found'); + alert('Error: AngularJS scope not found. Please refresh the page.'); + return false; + } + + console.log('Scope found, bannedIPs:', scope.bannedIPs ? scope.bannedIPs.length : 0); + + if (!scope.bannedIPs || scope.bannedIPs.length === 0) { + console.error('No bannedIPs found in scope'); + alert('Error: No banned IPs found. Please refresh the page.'); + return false; + } + + var bannedIP = null; + for (var i = 0; i < scope.bannedIPs.length; i++) { + if (scope.bannedIPs[i].ip === ip || (id && scope.bannedIPs[i].id == id)) { + bannedIP = scope.bannedIPs[i]; + console.log('Found bannedIP:', bannedIP); + break; + } + } + + if (!bannedIP) { + console.error('Could not find bannedIP for IP:', ip, 'ID:', id); + alert('Error: Could not find IP data. IP: ' + ip + ', ID: ' + id); + return false; + } + + if (!scope.showModifyBannedIPModal) { + console.error('showModifyBannedIPModal function not found in scope'); + alert('Error: Modify function not found. Please refresh the page.'); + return false; + } + + console.log('Calling showModifyBannedIPModal'); + try { + scope.$apply(function() { + scope.showModifyBannedIPModal(bannedIP, event); + }); + } catch (err) { + console.error('Error calling showModifyBannedIPModal:', err); + alert('Error: ' + err.message); + } + + return false; +}; + +console.log('āœ… window.handleModifyClick function defined'); +console.log('Function test:', typeof window.handleModifyClick); + +// Verify function is available globally +if (typeof window.handleModifyClick === 'undefined') { + console.error('āŒ CRITICAL: window.handleModifyClick was not defined!'); +} else { + console.log('āœ… window.handleModifyClick is available and ready'); + // Test that it's callable + try { + console.log('Function type:', typeof window.handleModifyClick); + } catch (e) { + console.error('Error testing function:', e); + } +} + +// Also make it available immediately on window load +if (typeof window !== 'undefined') { + window.addEventListener('load', function() { + console.log('Page loaded - verifying handleModifyClick:', typeof window.handleModifyClick); + }); +} /* Java script code to ADD Firewall Rules */ -app.controller('firewallController', function ($scope, $http) { +app.controller('firewallController', function ($scope, $http, $timeout, $window, $location) { + console.log('========================================'); + console.log('=== firewallController INITIALIZED ==='); + console.log('========================================'); + console.log('Timestamp:', new Date().toISOString()); + console.log('$scope:', $scope); + console.log('$timeout:', typeof $timeout); + console.log('$window:', typeof $window); $scope.rulesLoading = true; $scope.actionFailed = true; @@ -17,7 +127,56 @@ app.controller('firewallController', function ($scope, $http) { $scope.rulesDetails = false; // Banned IPs variables - $scope.activeTab = 'rules'; + // Check URL hash for active tab (handle #!# pattern from AngularJS) + var hash = $window.location.hash || $location.hash(); + if (hash) { + // Remove # or #! or #!# prefix + hash = hash.replace(/^#!?#?/, ''); + console.log('Initial hash parsed:', hash); + if (hash === 'bannedips' || hash === 'banned') { + $scope.activeTab = 'banned'; + } else if (hash === 'rules') { + $scope.activeTab = 'rules'; + } else { + $scope.activeTab = 'rules'; + } + } else { + $scope.activeTab = 'rules'; + } + + // Also listen for hashchange events to handle direct URL navigation + var hashChangeHandler = function() { + var currentHash = $window.location.hash; + var newHash = currentHash.replace(/^#!?#?/, ''); + console.log('Hash changed event - current:', currentHash, 'parsed:', newHash); + + // Only update if different from current tab + if ((newHash === 'bannedips' || newHash === 'banned') && $scope.activeTab !== 'banned') { + $scope.$apply(function() { + $scope.activeTab = 'banned'; + }); + } else if (newHash === 'rules' && $scope.activeTab !== 'rules') { + $scope.$apply(function() { + $scope.activeTab = 'rules'; + }); + } + }; + + $window.addEventListener('hashchange', hashChangeHandler); + + // Clean up hash on page load if it has #!# pattern + $timeout(function() { + var currentHash = $window.location.hash; + if (currentHash && currentHash.includes('#!')) { + var cleanHash = currentHash.replace(/^#!?#?/, ''); + var cleanUrl = $window.location.href.split('#')[0] + '#' + cleanHash; + if ($window.history && $window.history.replaceState) { + $window.history.replaceState(null, '', cleanUrl); + console.log('Cleaned hash from', currentHash, 'to', cleanHash); + } + } + }, 100); + $scope.bannedIPs = []; $scope.bannedIPsLoading = false; $scope.bannedIPActionFailed = true; @@ -31,6 +190,116 @@ app.controller('firewallController', function ($scope, $http) { populateCurrentRecords(); populateBannedIPs(); + + // Use MutationObserver to catch buttons as they're created + var observer = new MutationObserver(function(mutations) { + var modifyButtons = document.querySelectorAll('.btn-modify'); + if (modifyButtons.length > 0) { + console.log('MutationObserver: Found', modifyButtons.length, 'Modify buttons'); + modifyButtons.forEach(function(btn) { + if (!btn.hasAttribute('data-listener-attached')) { + btn.setAttribute('data-listener-attached', 'true'); + console.log('MutationObserver: Attaching listener to button'); + + // Force visibility + btn.style.setProperty('display', 'flex', 'important'); + btn.style.setProperty('visibility', 'visible', 'important'); + btn.style.setProperty('opacity', '1', 'important'); + btn.style.setProperty('pointer-events', 'auto', 'important'); + } + }); + } + }); + + // Start observing + $timeout(function() { + var container = document.querySelector('[ng-controller="firewallController"]'); + if (container) { + console.log('Starting MutationObserver on controller container'); + observer.observe(container, { + childList: true, + subtree: true + }); + } + }, 500); + + // Also try immediate setup + var setupModifyButtons = function() { + console.log('=== setupModifyButtons CALLED ==='); + var modifyButtons = document.querySelectorAll('.btn-modify'); + console.log('Found', modifyButtons.length, 'Modify buttons in DOM'); + + modifyButtons.forEach(function(btn, index) { + if (btn && !btn.hasAttribute('data-listener-attached')) { + btn.setAttribute('data-listener-attached', 'true'); + console.log('Setting up button', index + ':', btn); + + // Force visibility + btn.style.setProperty('display', 'flex', 'important'); + btn.style.setProperty('visibility', 'visible', 'important'); + btn.style.setProperty('opacity', '1', 'important'); + btn.style.setProperty('pointer-events', 'auto', 'important'); + btn.classList.remove('ng-hide', 'hide', 'disabled'); + } + }); + }; + + // Run multiple times to catch buttons + $timeout(setupModifyButtons, 100); + $timeout(setupModifyButtons, 500); + $timeout(setupModifyButtons, 1000); + $timeout(setupModifyButtons, 2000); + + // Also run after bannedIPs load + $scope.$watch('bannedIPs', function(newVal, oldVal) { + if (newVal && newVal.length > 0) { + console.log('=== bannedIPs changed, found', newVal.length, 'IPs ==='); + $timeout(setupModifyButtons, 100); + $timeout(setupModifyButtons, 500); + } + }, true); + + // Watch for tab changes and update URL hash (without #!#) + $scope.$watch('activeTab', function(newTab, oldTab) { + if (newTab !== oldTab && newTab) { + // Use replaceState to avoid AngularJS adding #! prefix + var newHash = newTab === 'banned' ? 'bannedips' : 'rules'; + + // Get clean base URL (without hash or query params) + var baseUrl = $window.location.protocol + '//' + + $window.location.host + + $window.location.pathname; + var newUrl = baseUrl + '#' + newHash; + + // Use replaceState to update URL - this bypasses AngularJS hash handling + if ($window.history && $window.history.replaceState) { + try { + // Use replaceState to set clean hash without AngularJS interference + $window.history.replaceState(null, '', newUrl); + console.log('Tab changed to:', newTab, 'Hash set to:', newHash); + + // Small delay to ensure URL is updated, then clean up if AngularJS added #! + $timeout(function() { + var currentHash = $window.location.hash; + if (currentHash && currentHash.includes('#!')) { + // AngularJS added #! prefix, clean it up + var cleanHash = currentHash.replace(/^#!?#?/, ''); + var cleanUrl = baseUrl + '#' + cleanHash; + $window.history.replaceState(null, '', cleanUrl); + console.log('Cleaned AngularJS hashbang from', currentHash, 'to', cleanHash); + } + }, 50); + } catch (e) { + console.warn('replaceState failed:', e); + // Fallback: set hash directly + $window.location.hash = newHash; + } + } else { + // Fallback to direct hash setting + $window.location.hash = newHash; + } + } + }); $scope.addRule = function () { @@ -2427,6 +2696,17 @@ app.controller('litespeed_ent_conf', function ($scope, $http, $timeout, $window) $scope.bannedIPsLoading = false; if (response.data.status === 1) { $scope.bannedIPs = response.data.bannedIPs || []; + + // Ensure Modify buttons are visible after data loads + $timeout(function() { + var modifyButtons = document.querySelectorAll('.btn-modify'); + modifyButtons.forEach(function(btn) { + btn.style.display = 'flex'; + btn.style.visibility = 'visible'; + btn.style.opacity = '1'; + }); + console.log('Ensured', modifyButtons.length, 'Modify buttons are visible'); + }, 100); } else { $scope.bannedIPs = []; $scope.bannedIPActionFailed = false; @@ -2515,6 +2795,1005 @@ app.controller('litespeed_ent_conf', function ($scope, $http, $timeout, $window) }); }; + // Make function available globally for onclick fallback - uses data attributes + window.showModifyModal = function(buttonElement) { + console.log('========================================'); + console.log('=== ONCLICK FALLBACK TRIGGERED ==='); + console.log('========================================'); + console.log('Button element:', buttonElement); + console.log('Button data-ip:', buttonElement ? buttonElement.getAttribute('data-ip') : 'null'); + console.log('Button data-id:', buttonElement ? buttonElement.getAttribute('data-id') : 'null'); + try { + // Get data from button attributes first (most reliable) + var ip = buttonElement.getAttribute('data-ip'); + var id = buttonElement.getAttribute('data-id'); + + var bannedIP = null; + + // Try to get from AngularJS scope + var row = buttonElement.closest('tr'); + if (row) { + var scope = angular.element(row).scope(); + if (scope) { + bannedIP = scope.bannedIP || (scope.$parent && scope.$parent.bannedIP); + } + } + + // If not found in scope, try to find by IP/id from controller scope + if (!bannedIP && ip) { + var controllerElement = document.querySelector('[ng-controller="firewallController"]'); + if (controllerElement) { + var controllerScope = angular.element(controllerElement).scope(); + if (controllerScope && controllerScope.bannedIPs) { + for (var i = 0; i < controllerScope.bannedIPs.length; i++) { + if (controllerScope.bannedIPs[i].ip === ip || + (id && controllerScope.bannedIPs[i].id == id)) { + bannedIP = controllerScope.bannedIPs[i]; + break; + } + } + } + } + } + + if (bannedIP) { + console.log('Found bannedIP:', bannedIP); + var controllerElement = document.querySelector('[ng-controller="firewallController"]'); + if (controllerElement) { + var controllerScope = angular.element(controllerElement).scope(); + if (controllerScope && controllerScope.showModifyBannedIPModal) { + controllerScope.$apply(function() { + controllerScope.showModifyBannedIPModal(bannedIP); + }); + } else { + // Direct call if $apply not available + if (controllerScope.showModifyBannedIPModal) { + controllerScope.showModifyBannedIPModal(bannedIP); + } + } + } + } else { + console.error('Could not find bannedIP for IP:', ip, 'ID:', id); + alert('Error: Could not find IP data. Please refresh the page.'); + } + } catch (error) { + console.error('Error in onclick fallback:', error); + alert('Error opening modify dialog: ' + error.message); + } + }; + + // Make showModifyBannedIPModal available globally for debugging + window.showModifyBannedIPModalGlobal = function(bannedIP) { + console.log('=== GLOBAL FUNCTION CALLED ==='); + console.log('bannedIP:', bannedIP); + var controllerElement = document.querySelector('[ng-controller="firewallController"]'); + if (controllerElement) { + var controllerScope = angular.element(controllerElement).scope(); + if (controllerScope && controllerScope.showModifyBannedIPModal) { + console.log('Calling scope function'); + controllerScope.$apply(function() { + controllerScope.showModifyBannedIPModal(bannedIP, null); + }); + } else { + console.error('Controller scope or function not found'); + console.log('controllerScope:', controllerScope); + console.log('showModifyBannedIPModal exists:', controllerScope ? !!controllerScope.showModifyBannedIPModal : false); + } + } else { + console.error('Controller element not found'); + } + }; + + // Test function - call this from console: window.testModifyModal() + window.testModifyModal = function() { + console.log('=== TEST FUNCTION CALLED ==='); + var controllerElement = document.querySelector('[ng-controller="firewallController"]'); + console.log('Controller element:', controllerElement); + if (controllerElement) { + var controllerScope = angular.element(controllerElement).scope(); + console.log('Controller scope:', controllerScope); + console.log('bannedIPs:', controllerScope ? controllerScope.bannedIPs : 'null'); + if (controllerScope && controllerScope.bannedIPs && controllerScope.bannedIPs.length > 0) { + var testIP = controllerScope.bannedIPs[0]; + console.log('Testing with first bannedIP:', testIP); + if (controllerScope.showModifyBannedIPModal) { + controllerScope.$apply(function() { + controllerScope.showModifyBannedIPModal(testIP, null); + }); + } else { + console.error('showModifyBannedIPModal function not found in scope'); + } + } else { + console.error('No bannedIPs found'); + } + } else { + console.error('Controller element not found'); + } + }; + + // Wrapper function for ng-click - MUST be simple and direct + $scope.handleModifyButtonClick = function(bannedIP, event) { + console.log('========================================'); + console.log('=== handleModifyButtonClick CALLED ==='); + console.log('========================================'); + console.log('bannedIP:', bannedIP); + console.log('bannedIP type:', typeof bannedIP); + console.log('event:', event); + + // Prevent default + if (event) { + event.preventDefault(); + event.stopPropagation(); + } + + if (!bannedIP) { + console.error('āŒ ERROR: No bannedIP provided'); + alert('Error: No IP data provided'); + return false; + } + + console.log('āœ… bannedIP is valid, calling showModifyBannedIPModal'); + + // Direct call - don't check if function exists, just call it + try { + $scope.showModifyBannedIPModal(bannedIP, event); + } catch (err) { + console.error('āŒ ERROR calling showModifyBannedIPModal:', err); + alert('Error calling showModifyBannedIPModal: ' + err.message); + } + + return false; + }; + + // Also create openModifyModal alias since it's expected by the onclick handler + $scope.openModifyModal = function(bannedIP, event) { + console.log('=== openModifyModal CALLED (alias) ==='); + console.log('bannedIP:', bannedIP); + // Call showModifyBannedIPModal directly + if ($scope.showModifyBannedIPModal) { + $scope.showModifyBannedIPModal(bannedIP, event); + } else { + console.error('showModifyBannedIPModal not found!'); + alert('showModifyBannedIPModal function not found!'); + } + }; + + console.log('āœ… handleModifyButtonClick function defined in scope'); + console.log('āœ… openModifyModal alias created'); + console.log('Function exists check:', typeof $scope.handleModifyButtonClick, typeof $scope.openModifyModal); + + // CRITICAL: Define showModifyBannedIPModal FIRST and make it available on window immediately + $scope.showModifyBannedIPModal = function(bannedIP, event) { + console.log('========================================'); + console.log('=== showModifyBannedIPModal CALLED ==='); + console.log('========================================'); + console.log('Timestamp:', new Date().toISOString()); + console.log('bannedIP:', JSON.stringify(bannedIP, null, 2)); + console.log('bannedIP type:', typeof bannedIP); + console.log('bannedIP keys:', bannedIP ? Object.keys(bannedIP) : 'null'); + console.log('event:', event); + console.log('event type:', typeof event); + console.log('$scope:', $scope); + console.log('$scope.activeTab:', $scope.activeTab); + + // If bannedIP is not an object, try to find it + if (!bannedIP || typeof bannedIP !== 'object') { + console.warn('bannedIP is not an object, trying to find it...'); + if (event && event.target) { + var btn = event.target.closest('.btn-modify'); + if (btn) { + var ip = btn.getAttribute('data-ip'); + var id = btn.getAttribute('data-id'); + console.log('Found data-ip:', ip, 'data-id:', id); + if ($scope.bannedIPs) { + for (var i = 0; i < $scope.bannedIPs.length; i++) { + if ($scope.bannedIPs[i].ip === ip || + (id && $scope.bannedIPs[i].id == id)) { + bannedIP = $scope.bannedIPs[i]; + console.log('Found bannedIP:', bannedIP); + break; + } + } + } + } + } + } + + // Prevent default and stop propagation if event provided + if (event) { + console.log('Preventing default and stopping propagation'); + event.preventDefault(); + event.stopPropagation(); + } + + if (!bannedIP) { + console.error('āŒ ERROR: No bannedIP data provided'); + console.trace('Stack trace:'); + alert('Error: No IP data provided'); + return false; + } + + console.log('āœ… bannedIP validation passed'); + + // Store bannedIP in scope for debugging + $scope.currentBannedIP = bannedIP; + console.log('Stored bannedIP in $scope.currentBannedIP'); + + // Get modal element immediately + var modalElement = document.getElementById('modifyBannedIPModal'); + console.log('Modal element lookup:'); + console.log(' - Element found:', !!modalElement); + console.log(' - Element type:', modalElement ? modalElement.tagName : 'null'); + console.log(' - Element ID:', modalElement ? modalElement.id : 'null'); + + if (modalElement) { + console.log(' - Element classes:', modalElement.className); + console.log(' - Element style.display:', modalElement.style.display); + console.log(' - Element computed display:', window.getComputedStyle(modalElement).display); + console.log(' - Element parent:', modalElement.parentElement ? modalElement.parentElement.tagName : 'null'); + console.log(' - Element in DOM:', document.body.contains(modalElement)); + } + + if (!modalElement) { + console.error('āŒ ERROR: Modal element not found in DOM'); + alert('ERROR: Modal element not found in DOM!'); + console.log('Searching for modal in DOM...'); + var allModals = document.querySelectorAll('.modal, [id*="modal"], [id*="Modal"]'); + console.log('Found', allModals.length, 'potential modal elements:'); + allModals.forEach(function(m, i) { + console.log(' Modal', i + ':', m.id, m.className); + }); + alert('Error: Modal not found. Found ' + allModals.length + ' modals. Please refresh the page.'); + return false; + } + + console.log('āœ… Modal element found'); + + // Check jQuery availability + console.log('jQuery check:'); + console.log(' - jQuery available:', typeof $ !== 'undefined'); + console.log(' - jQuery version:', typeof $ !== 'undefined' ? $.fn.jquery : 'N/A'); + console.log(' - jQuery modal available:', typeof $ !== 'undefined' && $.fn.modal); + console.log(' - Bootstrap available:', typeof bootstrap !== 'undefined'); + + // Use AngularJS $timeout instead of setTimeout for proper digest cycle + console.log('Setting up $timeout for modal display...'); + $timeout(function() { + console.log('--- $timeout callback executed ---'); + try { + console.log('--- Inside try block ---'); + console.log('Modal element still available:', !!modalElement); + + // Set modal form values + console.log('Looking for form fields...'); + var idField = document.getElementById('modifyBannedIPId'); + var ipField = document.getElementById('modifyBannedIPAddress'); + var reasonField = document.getElementById('modifyBannedIPReason'); + var durationField = document.getElementById('modifyBannedIPDuration'); + + console.log('Form fields lookup results:'); + console.log(' - idField:', !!idField, idField ? idField.id : 'not found'); + console.log(' - ipField:', !!ipField, ipField ? ipField.id : 'not found'); + console.log(' - reasonField:', !!reasonField, reasonField ? reasonField.id : 'not found'); + console.log(' - durationField:', !!durationField, durationField ? durationField.id : 'not found'); + + if (!idField || !ipField || !reasonField || !durationField) { + console.error('āŒ ERROR: Modal form fields not found'); + console.log('Checking modal structure...'); + var modalCheck = document.getElementById('modifyBannedIPModal'); + console.log('Modal element exists:', !!modalCheck); + if (modalCheck) { + console.log('Modal innerHTML length:', modalCheck.innerHTML ? modalCheck.innerHTML.length : 0); + console.log('Modal first 1000 chars:', modalCheck.outerHTML.substring(0, 1000)); + var formCheck = modalCheck.querySelector('form, #modifyBannedIPForm'); + console.log('Form element in modal:', !!formCheck); + if (formCheck) { + console.log('Form children:', formCheck.children.length); + Array.from(formCheck.children).forEach(function(child, i) { + console.log(' Child', i + ':', child.tagName, child.id || child.className); + }); + } + } + alert('Error: Modal form not found. Please refresh the page.'); + return; + } + + console.log('āœ… All form fields found'); + + console.log('Setting form field values...'); + idField.value = bannedIP.id || ''; + ipField.value = bannedIP.ip || ''; + ipField.removeAttribute('readonly'); + ipField.removeAttribute('disabled'); + ipField.readOnly = false; + ipField.disabled = false; + reasonField.value = bannedIP.reason || ''; + console.log('Form values set:'); + console.log(' - ID:', idField.value); + console.log(' - IP:', ipField.value); + console.log(' - Reason:', reasonField.value); + + // Set duration - convert expires to duration format + var duration = bannedIP.duration || 'never'; + console.log('Calculating duration...'); + console.log(' - Initial duration:', duration); + console.log(' - expires_timestamp:', bannedIP.expires_timestamp); + console.log(' - banned_on_timestamp:', bannedIP.banned_on_timestamp); + + // If we have expires_timestamp, try to calculate duration + if (bannedIP.expires_timestamp && bannedIP.banned_on_timestamp) { + var expiresMs = bannedIP.expires_timestamp; + var bannedMs = bannedIP.banned_on_timestamp; + var diffMs = expiresMs - bannedMs; + var diffHours = diffMs / (1000 * 60 * 60); + console.log(' - Diff in hours:', diffHours); + + if (diffHours <= 1) { + duration = '1h'; + } else if (diffHours <= 6) { + duration = '6h'; + } else if (diffHours <= 12) { + duration = '12h'; + } else if (diffHours <= 24) { + duration = '24h'; + } else if (diffHours <= 48) { + duration = '48h'; + } else if (diffHours <= 168) { // 7 days + duration = '7d'; + } else if (diffHours <= 720) { // 30 days + duration = '30d'; + } else { + duration = 'never'; + } + console.log(' - Calculated duration:', duration); + } else if (bannedIP.expires === 'Never' || !bannedIP.expires_timestamp) { + duration = 'never'; + console.log(' - Using never (no expiration)'); + } + + durationField.value = duration; + console.log('āœ… Duration set to:', duration); + + console.log('========================================'); + console.log('=== ATTEMPTING TO SHOW MODAL ==='); + console.log('========================================'); + + // Re-check modal element + modalElement = document.getElementById('modifyBannedIPModal'); + console.log('Modal element re-check:'); + console.log(' - Found:', !!modalElement); + console.log(' - Current display:', modalElement ? window.getComputedStyle(modalElement).display : 'N/A'); + console.log(' - Current visibility:', modalElement ? window.getComputedStyle(modalElement).visibility : 'N/A'); + console.log(' - Current opacity:', modalElement ? window.getComputedStyle(modalElement).opacity : 'N/A'); + console.log(' - Current z-index:', modalElement ? window.getComputedStyle(modalElement).zIndex : 'N/A'); + console.log(' - Has show class:', modalElement ? modalElement.classList.contains('show') : false); + + // Direct display approach - most reliable + console.log('Step 1: Cleaning up existing modals/backdrops...'); + + // Clean up any existing modals/backdrops first + var existingBackdrops = document.querySelectorAll('.modal-backdrop'); + console.log(' - Found', existingBackdrops.length, 'existing backdrops'); + existingBackdrops.forEach(function(b, i) { + console.log(' Removing backdrop', i + ':', b.id || 'no-id'); + b.remove(); + }); + + var existingModals = document.querySelectorAll('.modal.show'); + console.log(' - Found', existingModals.length, 'existing open modals'); + existingModals.forEach(function(m, i) { + console.log(' Closing modal', i + ':', m.id || 'no-id'); + m.classList.remove('show'); + }); + + if (typeof $ !== 'undefined') { + $('.modal-backdrop').remove(); + $('.modal.show').removeClass('show'); + console.log(' - jQuery cleanup completed'); + } + document.body.classList.remove('modal-open'); + console.log(' - Removed modal-open from body'); + + console.log('Step 2: Preparing modal element...'); + // Remove any inline styles that might hide it + console.log(' - Removing inline styles...'); + modalElement.removeAttribute('style'); + console.log(' - Inline styles removed'); + + console.log('Step 3: Moving modal to body and setting display properties...'); + + // CRITICAL: Move modal to body if it's not already there + if (modalElement.parentElement !== document.body) { + console.log('Moving modal to body (currently in:', modalElement.parentElement.tagName + ')'); + document.body.appendChild(modalElement); + console.log('Modal moved to body'); + } + + // Show modal directly with all necessary attributes + modalElement.classList.add('show'); + modalElement.classList.add('fade'); + modalElement.style.cssText = 'display: flex !important; position: fixed !important; top: 0 !important; left: 0 !important; width: 100% !important; height: 100% !important; z-index: 99999 !important; opacity: 1 !important; visibility: visible !important; align-items: center !important; justify-content: center !important;'; + modalElement.removeAttribute('aria-hidden'); + modalElement.setAttribute('aria-hidden', 'false'); + modalElement.setAttribute('aria-modal', 'true'); + // Force remove aria-hidden again after a brief delay to ensure it sticks + setTimeout(function() { + modalElement.removeAttribute('aria-hidden'); + modalElement.setAttribute('aria-hidden', 'false'); + }, 50); + document.body.classList.add('modal-open'); + document.body.style.overflow = 'hidden'; + + console.log('Modal styles applied:'); + console.log(' - display:', modalElement.style.display); + console.log(' - position:', modalElement.style.position); + console.log(' - z-index:', modalElement.style.zIndex); + console.log(' - opacity:', modalElement.style.opacity); + console.log(' - visibility:', modalElement.style.visibility); + console.log(' - has show class:', modalElement.classList.contains('show')); + console.log(' - body has modal-open:', document.body.classList.contains('modal-open')); + + console.log('Step 4: Creating backdrop...'); + // Create and show backdrop + var backdrop = document.createElement('div'); + backdrop.className = 'modal-backdrop fade show'; + backdrop.style.position = 'fixed'; + backdrop.style.top = '0'; + backdrop.style.left = '0'; + backdrop.style.width = '100%'; + backdrop.style.height = '100%'; + backdrop.style.zIndex = '99998'; + backdrop.style.backgroundColor = 'rgba(0, 0, 0, 0.5)'; + backdrop.id = 'modifyBannedIPModalBackdrop'; + document.body.appendChild(backdrop); + console.log(' - Backdrop created and appended to body'); + console.log(' - Backdrop ID:', backdrop.id); + console.log(' - Backdrop in DOM:', document.body.contains(backdrop)); + + // Handle backdrop click to close + backdrop.addEventListener('click', function(e) { + console.log('Backdrop clicked'); + if (e.target === backdrop) { + $scope.closeModifyModal(); + } + }); + + console.log('Step 5: Configuring modal dialog...'); + // Ensure modal dialog is centered and visible + var modalDialog = modalElement.querySelector('.modal-dialog'); + if (modalDialog) { + console.log(' - Modal dialog found'); + modalDialog.style.zIndex = '100000'; + modalDialog.style.position = 'relative'; + modalDialog.style.margin = '1.75rem auto'; + console.log(' - Dialog styles applied'); + } else { + console.error(' - āŒ Modal dialog NOT found!'); + console.log(' - Modal children:', modalElement.children.length); + Array.from(modalElement.children).forEach(function(child, i) { + console.log(' Child', i + ':', child.tagName, child.className); + }); + } + + console.log('========================================'); + console.log('=== MODAL DISPLAY COMPLETE ==='); + console.log('========================================'); + console.log('Final checks:'); + console.log(' - Modal display (computed):', window.getComputedStyle(modalElement).display); + console.log(' - Modal visibility (computed):', window.getComputedStyle(modalElement).visibility); + console.log(' - Modal opacity (computed):', window.getComputedStyle(modalElement).opacity); + console.log(' - Modal z-index (computed):', window.getComputedStyle(modalElement).zIndex); + console.log(' - Modal in viewport:', modalElement.getBoundingClientRect()); + console.log(' - Backdrop display:', window.getComputedStyle(backdrop).display); + console.log(' - Body modal-open:', document.body.classList.contains('modal-open')); + + // Verify modal is actually visible + var rect = modalElement.getBoundingClientRect(); + console.log('Modal bounding rect:', { + top: rect.top, + left: rect.left, + width: rect.width, + height: rect.height, + visible: rect.width > 0 && rect.height > 0 + }); + + // CRITICAL: Force modal to be visible with multiple approaches + console.log('=== FORCING MODAL VISIBILITY ==='); + + // Remove any hiding classes + modalElement.classList.remove('hide', 'ng-hide', 'hidden'); + + // Force display with multiple methods + modalElement.style.setProperty('display', 'block', 'important'); + modalElement.style.setProperty('visibility', 'visible', 'important'); + modalElement.style.setProperty('opacity', '1', 'important'); + modalElement.style.setProperty('z-index', '99999', 'important'); + modalElement.style.setProperty('position', 'fixed', 'important'); + modalElement.removeAttribute('aria-hidden'); + modalElement.setAttribute('aria-hidden', 'false'); + + // Check parent containers for overflow issues + var parent = modalElement.parentElement; + var depth = 0; + while (parent && depth < 5) { + var parentOverflow = window.getComputedStyle(parent).overflow; + var parentZIndex = window.getComputedStyle(parent).zIndex; + console.log('Parent', depth + ':', parent.tagName, parent.className, 'overflow:', parentOverflow, 'z-index:', parentZIndex); + if (parentOverflow === 'hidden') { + console.warn('āš ļø Parent has overflow:hidden, might hide modal'); + parent.style.setProperty('overflow', 'visible', 'important'); + } + parent = parent.parentElement; + depth++; + } + + // Final verification + setTimeout(function() { + var finalDisplay = window.getComputedStyle(modalElement).display; + var finalVisibility = window.getComputedStyle(modalElement).visibility; + var finalOpacity = window.getComputedStyle(modalElement).opacity; + console.log('=== FINAL MODAL STATE (after 100ms) ==='); + console.log('Display:', finalDisplay); + console.log('Visibility:', finalVisibility); + console.log('Opacity:', finalOpacity); + if (finalDisplay === 'none' || finalVisibility === 'hidden' || finalOpacity === '0') { + console.error('āŒ MODAL IS STILL HIDDEN!'); + console.error('Trying emergency show...'); + modalElement.style.cssText = 'display: block !important; visibility: visible !important; opacity: 1 !important; z-index: 99999 !important; position: fixed !important; top: 0 !important; left: 0 !important; width: 100% !important; height: 100% !important;'; + } else { + console.log('āœ… Modal should be visible'); + } + }, 100); + + // Try jQuery/Bootstrap modal as primary method (most reliable) + console.log('Step 6: Attempting to show modal...'); + console.log(' - jQuery available:', typeof $ !== 'undefined'); + console.log(' - Bootstrap modal available:', typeof $ !== 'undefined' && $.fn.modal); + + if (typeof $ !== 'undefined' && $.fn.modal) { + console.log(' - Using jQuery/Bootstrap modal...'); + var $modal = $('#modifyBannedIPModal'); + console.log(' - jQuery modal object found:', !!$modal.length); + console.log(' - Modal element:', $modal[0]); + + if ($modal.length === 0) { + console.error(' - āŒ Modal element not found by jQuery!'); + console.log(' - Searching DOM for modal...'); + var modalCheck = document.getElementById('modifyBannedIPModal'); + console.log(' - Found by getElementById:', !!modalCheck); + if (modalCheck) { + console.log(' - Modal parent:', modalCheck.parentElement ? modalCheck.parentElement.tagName : 'null'); + console.log(' - Modal display:', window.getComputedStyle(modalCheck).display); + } + } else { + // Ensure modal is in body + var modalParent = $modal.parent()[0]; + console.log(' - Modal parent:', modalParent ? modalParent.tagName : 'null'); + if (modalParent !== document.body) { + console.log(' - Moving modal to body...'); + $modal.appendTo('body'); + console.log(' - āœ… Modal moved to body'); + } + + // CRITICAL: Show modal using Bootstrap - this is the most reliable method + console.log(' - Attempting to show modal with Bootstrap...'); + + // Initialize modal if not already initialized + if (!$modal.data('bs.modal')) { + console.log(' - Initializing Bootstrap modal first...'); + try { + $modal.modal({show: false, backdrop: true, keyboard: true}); + console.log(' - āœ… Modal initialized'); + } catch (initErr) { + console.warn(' - āš ļø Modal initialization failed:', initErr); + } + } + + // Show using Bootstrap modal + try { + console.log(' - Calling Bootstrap modal.show()...'); + alert('About to call Bootstrap modal.show()'); // TEST ALERT + $modal.modal('show'); + console.log(' - āœ… Bootstrap modal.show() called successfully'); + alert('Bootstrap modal.show() called!'); // TEST ALERT + + // Force display immediately as backup (don't wait) + $modal.addClass('show'); + $modal[0].style.cssText = 'display: flex !important; position: fixed !important; z-index: 99999 !important; top: 0 !important; left: 0 !important; width: 100% !important; height: 100% !important; align-items: center !important; justify-content: center !important; opacity: 1 !important; visibility: visible !important;'; + + // Ensure backdrop exists + if (!$('.modal-backdrop').length) { + var backdrop = $(''); + backdrop.css({ + 'position': 'fixed', + 'top': '0', + 'left': '0', + 'width': '100%', + 'height': '100%', + 'z-index': '99998', + 'background-color': 'rgba(0, 0, 0, 0.5)' + }); + $('body').append(backdrop); + console.log(' - Created backdrop'); + } + + // Force body styles + $('body').addClass('modal-open').css('overflow', 'hidden'); + + console.log(' - āœ… Modal display forced'); + alert('Modal should be visible now! Check if you see it.'); // TEST ALERT + + // Verify modal is visible after a short delay + setTimeout(function() { + var modalEl = $modal[0]; + var isVisible = $modal.hasClass('show') && $modal.is(':visible'); + var display = window.getComputedStyle(modalEl).display; + var visibility = window.getComputedStyle(modalEl).visibility; + var opacity = window.getComputedStyle(modalEl).opacity; + var zIndex = window.getComputedStyle(modalEl).zIndex; + + console.log(' - === MODAL VISIBILITY CHECK (after 200ms) ==='); + console.log(' - Has show class:', $modal.hasClass('show')); + console.log(' - jQuery :visible:', $modal.is(':visible')); + console.log(' - Display:', display); + console.log(' - Visibility:', visibility); + console.log(' - Opacity:', opacity); + console.log(' - Z-index:', zIndex); + console.log(' - In DOM:', document.body.contains(modalEl)); + + if (!isVisible || display === 'none' || visibility === 'hidden' || opacity === '0') { + console.error(' - āŒ MODAL IS STILL NOT VISIBLE!'); + console.log(' - Applying emergency CSS override...'); + modalEl.style.cssText = 'display: flex !important; position: fixed !important; z-index: 99999 !important; top: 0 !important; left: 0 !important; width: 100% !important; height: 100% !important; align-items: center !important; justify-content: center !important; opacity: 1 !important; visibility: visible !important;'; + modalEl.classList.add('show'); + + // Also ensure backdrop exists + if (!$('.modal-backdrop').length) { + var backdrop = $(''); + backdrop.css({ + 'position': 'fixed', + 'top': '0', + 'left': '0', + 'width': '100%', + 'height': '100%', + 'z-index': '99998', + 'background-color': 'rgba(0, 0, 0, 0.5)' + }); + $('body').append(backdrop); + console.log(' - Created backdrop manually'); + } + } else { + console.log(' - āœ… Modal appears to be visible'); + } + }, 200); + } catch (modalErr) { + console.error(' - āŒ Bootstrap modal.show() failed:', modalErr); + console.error(' - Error details:', modalErr.message, modalErr.stack); + // Fallback to manual display + console.log(' - Using fallback manual display...'); + $modal.addClass('show').css({ + 'display': 'flex', + 'position': 'fixed', + 'z-index': '99999', + 'top': '0', + 'left': '0', + 'width': '100%', + 'height': '100%', + 'align-items': 'center', + 'justify-content': 'center', + 'opacity': '1', + 'visibility': 'visible' + }); + } + } + } else { + console.warn(' - āŒ jQuery/Bootstrap modal not available!'); + console.warn(' - jQuery:', typeof $); + console.warn(' - $.fn.modal:', typeof $ !== 'undefined' ? typeof $.fn.modal : 'N/A'); + console.log(' - Using manual display fallback...'); + } + + console.log('========================================'); + console.log('=== END OF MODAL DISPLAY LOGIC ==='); + console.log('========================================'); + + // Try Bootstrap 5 as additional backup + if (typeof bootstrap !== 'undefined' && bootstrap.Modal) { + console.log('Also trying Bootstrap 5 modal...'); + try { + var modal = new bootstrap.Modal(modalElement); + modal.show(); + } catch (bsErr) { + console.log('Bootstrap 5 modal failed:', bsErr); + } + } + console.log('Using manual modal display'); + modalElement.classList.add('show'); + modalElement.style.display = 'block'; + modalElement.removeAttribute('aria-hidden'); + modalElement.setAttribute('aria-hidden', 'false'); + modalElement.setAttribute('aria-modal', 'true'); + document.body.classList.add('modal-open'); + + // Create backdrop + var backdrop = document.createElement('div'); + backdrop.className = 'modal-backdrop fade show'; + backdrop.id = 'modifyBannedIPModalBackdrop'; + backdrop.style.position = 'fixed'; + backdrop.style.top = '0'; + backdrop.style.left = '0'; + backdrop.style.zIndex = '99998'; + backdrop.style.width = '100%'; + backdrop.style.height = '100%'; + backdrop.style.backgroundColor = 'rgba(0,0,0,0.5)'; + document.body.appendChild(backdrop); + + // Handle backdrop click to close + backdrop.addEventListener('click', function() { + $scope.closeModifyModal(); + }); + + // Ensure modal is on top with very high z-index + modalElement.style.zIndex = '99999'; + modalElement.style.position = 'fixed'; + modalElement.style.top = '0'; + modalElement.style.left = '0'; + modalElement.style.width = '100%'; + modalElement.style.height = '100%'; + modalElement.style.display = 'flex'; + modalElement.style.alignItems = 'center'; + modalElement.style.justifyContent = 'center'; + + // Ensure modal dialog is visible + var modalDialog = modalElement.querySelector('.modal-dialog'); + if (modalDialog) { + modalDialog.style.zIndex = '100000'; + modalDialog.style.position = 'relative'; + } + } + } catch (error) { + console.error('========================================'); + console.error('āŒ ERROR in showModifyBannedIPModal'); + console.error('========================================'); + console.error('Error type:', error.name); + console.error('Error message:', error.message); + console.error('Error stack:', error.stack); + console.error('Error object:', error); + console.trace('Full stack trace:'); + + if (typeof PNotify !== 'undefined') { + new PNotify({ + title: 'Error', + text: 'Failed to open modify dialog: ' + error.message, + type: 'error' + }); + } else { + alert('Error: Failed to open modify dialog - ' + error.message + '\n\nCheck console for details.'); + } + } + }, 100); + + console.log('=== showModifyBannedIPModal function completed (timeout scheduled) ==='); + return true; + }; + + // Make showModifyBannedIPModal available on window immediately after definition + window.showModifyBannedIPModalScope = $scope.showModifyBannedIPModal; + window.handleModifyButtonClickScope = $scope.handleModifyButtonClick; + window.openModifyModalScope = $scope.openModifyModal; + console.log('āœ… All functions available on window:', { + showModifyBannedIPModal: typeof window.showModifyBannedIPModalScope, + handleModifyButtonClick: typeof window.handleModifyButtonClickScope, + openModifyModal: typeof window.openModifyModalScope + }); + + $scope.closeModifyModal = function() { + console.log('=== closeModifyModal called ==='); + + var modalElement = document.getElementById('modifyBannedIPModal'); + if (!modalElement) { + console.warn('Modal element not found for closing'); + return; + } + + // Try jQuery/Bootstrap modal first + if (typeof $ !== 'undefined' && $.fn.modal) { + try { + $('#modifyBannedIPModal').modal('hide'); + console.log('Closed modal using Bootstrap'); + } catch (e) { + console.warn('Bootstrap modal hide failed:', e); + } + } + + // Manual cleanup + modalElement.classList.remove('show'); + modalElement.style.display = 'none'; + modalElement.removeAttribute('aria-hidden'); + modalElement.setAttribute('aria-hidden', 'true'); + + // Remove backdrop + var backdrops = document.querySelectorAll('.modal-backdrop'); + backdrops.forEach(function(b) { b.remove(); }); + + document.body.classList.remove('modal-open'); + document.body.style.overflow = ''; + }; + + // Make closeModifyModal available globally for onclick handlers + window.closeModifyModalGlobal = function() { + var controllerEl = document.querySelector('[ng-controller=firewallController]'); + if (controllerEl) { + var scope = angular.element(controllerEl).scope(); + if (scope && scope.closeModifyModal) { + scope.$apply(function() { + scope.closeModifyModal(); + }); + return; + } + } + // Fallback if AngularJS not available + var modal = document.getElementById('modifyBannedIPModal'); + if (modal) { + modal.classList.remove('show'); + modal.removeAttribute('aria-hidden'); + modal.setAttribute('aria-hidden', 'true'); + modal.style.display = 'none'; + document.body.classList.remove('modal-open'); + document.body.style.overflow = ''; + var backdrops = document.querySelectorAll('.modal-backdrop'); + for (var i = 0; i < backdrops.length; i++) backdrops[i].remove(); + } + }; + + // Make modifyBannedIP available globally for onclick handlers + window.modifyBannedIPGlobal = function() { + var controllerEl = document.querySelector('[ng-controller=firewallController]'); + if (controllerEl) { + var scope = angular.element(controllerEl).scope(); + if (scope && scope.modifyBannedIP) { + scope.$apply(function() { + scope.modifyBannedIP(); + }); + return; + } + } + alert('Error: Cannot save changes. Please refresh the page.'); + }; + + console.log('Modal closed'); + var modalElement = document.getElementById('modifyBannedIPModal'); + var backdrop = document.getElementById('modifyBannedIPModalBackdrop') || document.querySelector('.modal-backdrop'); + + // Hide modal + if (modalElement) { + modalElement.classList.remove('show', 'fade'); + modalElement.style.display = 'none'; + modalElement.removeAttribute('style'); + modalElement.setAttribute('aria-hidden', 'true'); + } + + // Remove backdrop + if (backdrop) { + backdrop.remove(); + } + + // Also remove any jQuery-created backdrops + if (typeof $ !== 'undefined') { + $('.modal-backdrop').remove(); + $('#modifyBannedIPModal').modal('hide'); + } + + document.body.classList.remove('modal-open'); + console.log('Modal closed'); + }; + + $scope.modifyBannedIP = function() { + var banId = document.getElementById('modifyBannedIPId').value; + var ipAddress = document.getElementById('modifyBannedIPAddress').value.trim(); + var reason = document.getElementById('modifyBannedIPReason').value.trim(); + var duration = document.getElementById('modifyBannedIPDuration').value; + + if (!ipAddress) { + if (typeof PNotify !== 'undefined') { + new PNotify({ + title: 'Validation Error', + text: 'Please enter an IP address', + type: 'error' + }); + } else { + alert('Please enter an IP address'); + } + return; + } + + // Validate IP address format + var ipRegex = /^(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)(?:\/(?:[0-9]|[1-2][0-9]|3[0-2]))?$/; + if (!ipRegex.test(ipAddress)) { + if (typeof PNotify !== 'undefined') { + new PNotify({ + title: 'Validation Error', + text: 'Please enter a valid IP address (e.g., 192.168.1.1 or 192.168.1.0/24)', + type: 'error' + }); + } else { + alert('Please enter a valid IP address'); + } + return; + } + + if (!reason) { + if (typeof PNotify !== 'undefined') { + new PNotify({ + title: 'Validation Error', + text: 'Please enter a reason for the ban', + type: 'error' + }); + } else { + alert('Please enter a reason for the ban'); + } + return; + } + + $scope.bannedIPsLoading = true; + $scope.bannedIPActionFailed = true; + $scope.bannedIPActionSuccess = true; + $scope.bannedIPCouldNotConnect = true; + + var data = { + id: banId, + ip: ipAddress, + reason: reason, + duration: duration + }; + + 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.status === 1) { + $scope.bannedIPActionSuccess = false; + $('#modifyBannedIPModal').modal('hide'); + populateBannedIPs(); // Refresh the list + + if (typeof PNotify !== 'undefined') { + new PNotify({ + title: 'Success!', + text: 'Banned IP modified successfully', + type: 'success' + }); + } + } else { + $scope.bannedIPActionFailed = false; + $scope.bannedIPErrorMessage = response.data.error_message || 'Failed to modify banned IP'; + + if (typeof PNotify !== 'undefined') { + new PNotify({ + title: 'Error!', + text: response.data.error_message || 'Failed to modify banned IP', + type: 'error' + }); + } + } + }, function(error) { + $scope.bannedIPsLoading = false; + $scope.bannedIPCouldNotConnect = false; + + if (typeof PNotify !== 'undefined') { + new PNotify({ + title: 'Connection Error', + text: 'Could not connect to server. Please refresh this page.', + type: 'error' + }); + } + }); + }; + $scope.deleteBannedIP = function(id, ip) { if (!confirm('Are you sure you want to permanently delete the record for IP address ' + ip + '? This action cannot be undone.')) { return; diff --git a/firewall/templates/firewall/firewall.html b/firewall/templates/firewall/firewall.html index 711043857..a04e17f21 100644 --- a/firewall/templates/firewall/firewall.html +++ b/firewall/templates/firewall/firewall.html @@ -8,6 +8,7 @@ max-width: 1400px; margin: 0 auto; padding: 2rem; + overflow: visible; /* Allow modal to show above container */ } .page-header { @@ -611,7 +612,61 @@ border-radius: 16px; box-shadow: 0 4px 12px var(--shadow-medium, rgba(0,0,0,0.15)); border: 1px solid var(--border-color, #e8e9ff); - overflow: hidden; + overflow: visible; /* Changed from hidden to allow modal to show */ + position: relative; + z-index: 1; + } + + /* Modal Styles - Ensure it's above everything */ + #modifyBannedIPModal { + z-index: 99999 !important; + position: fixed !important; + top: 0 !important; + left: 0 !important; + width: 100% !important; + height: 100% !important; + } + + #modifyBannedIPModal.show { + display: flex !important; + opacity: 1 !important; + visibility: visible !important; + align-items: center !important; + justify-content: center !important; + } + + #modifyBannedIPModal.fade.show { + opacity: 1 !important; + } + + #modifyBannedIPModal .modal-dialog { + z-index: 100000 !important; + position: relative !important; + margin: 1.75rem auto !important; + max-width: 500px !important; + width: 90% !important; + } + + .modal-backdrop { + z-index: 99998 !important; + position: fixed !important; + top: 0 !important; + left: 0 !important; + width: 100% !important; + height: 100% !important; + background-color: rgba(0, 0, 0, 0.5) !important; + } + + /* Ensure modal is always on top */ + body.modal-open #modifyBannedIPModal { + display: flex !important; + } + + /* Make sure modal is not hidden by parent containers */ + .modern-container #modifyBannedIPModal, + .banned-ips-panel #modifyBannedIPModal { + position: fixed !important; + z-index: 99999 !important; } .add-banned-section { @@ -770,18 +825,68 @@ box-shadow: 0 2px 8px rgba(16, 185, 129, 0.3); } + .btn-modify { + background: #2196f3 !important; + color: var(--bg-secondary, white) !important; + border: none !important; + padding: 0.75rem 1.5rem !important; + border-radius: 6px !important; + font-weight: 600 !important; + cursor: pointer !important; + transition: all 0.3s ease; + display: flex !important; + align-items: center !important; + gap: 0.5rem !important; + font-size: 0.95rem !important; + visibility: visible !important; + opacity: 1 !important; + min-width: 100px !important; + min-height: 40px !important; + position: relative !important; + z-index: 10 !important; + } + + .btn-modify:hover { + background: #1976d2; + transform: translateY(-1px); + box-shadow: 0 2px 8px rgba(33, 150, 243, 0.3); + } + + /* Ensure actions column and buttons are always visible */ + .banned-table td.actions { + display: flex !important; + visibility: visible !important; + } + + .banned-table td.actions { + display: flex !important; + gap: 0.5rem !important; + align-items: center !important; + } + + .banned-table td.actions .btn-modify { + display: flex !important; + visibility: visible !important; + opacity: 1 !important; + width: auto !important; + height: auto !important; + } + .btn-delete { background: var(--danger-color, #ef4444); color: var(--bg-secondary, white); border: none; - padding: 0.5rem; + padding: 0.75rem 1.5rem !important; border-radius: 6px; + font-weight: 600 !important; cursor: pointer; transition: all 0.3s ease; - width: 32px; - height: 32px; - display: flex; + display: flex !important; align-items: center; + gap: 0.5rem !important; + font-size: 0.95rem !important; + min-width: 100px !important; + min-height: 40px !important; justify-content: center; } @@ -1115,22 +1220,23 @@ - + {$ bannedIP.ip $} - {$ bannedIP.reason $} + {$ bannedIP.reason $} - + - {$ bannedIP.banned_on | date:'MMM dd, yyyy HH:mm' $} + {$ bannedIP.banned_on_timestamp | date:'MMM dd, yyyy HH:mm' $} + {$ bannedIP.banned_on $} - {% trans "Never" %} - {$ bannedIP.expires | date:'MMM dd, yyyy HH:mm' $} + {% trans "Never" %} + {$ bannedIP.expires_timestamp | date:'MMM dd, yyyy HH:mm' $} - + @@ -1185,6 +1295,72 @@ - + + + {% endblock %} \ No newline at end of file From e303548112c9500938be200d02ae7bd055c63267 Mon Sep 17 00:00:00 2001 From: master3395 Date: Wed, 28 Jan 2026 23:24:16 +0100 Subject: [PATCH 4/4] Add modify firewall rule and improve export/import functionality - Add modifyRule function to allow editing firewall rules without deletion - Add modify button and modal for firewall rules (similar to banned IPs) - Fix exportRules function to properly handle file downloads with blob response - Improve importRules function with better error handling and PNotify notifications - Add exportBannedIPs and importBannedIPs functionality - Add export/import buttons for banned IPs - Improve error handling and user feedback for all export/import operations - Add proper validation and duplicate detection for imports --- firewall/firewallManager.py | 640 ++++++++++++++++++++- firewall/static/firewall/firewall.js | 643 ++++++++++++++++++++-- firewall/templates/firewall/firewall.html | 156 +++++- firewall/urls.py | 4 + firewall/views.py | 42 ++ 5 files changed, 1421 insertions(+), 64 deletions(-) diff --git a/firewall/firewallManager.py b/firewall/firewallManager.py index 6a90fe4a4..cc2883520 100644 --- a/firewall/firewallManager.py +++ b/firewall/firewallManager.py @@ -11,6 +11,7 @@ sys.path.append('/usr/local/CyberCP') os.environ.setdefault("DJANGO_SETTINGS_MODULE", "CyberCP.settings") django.setup() import json +import tempfile from plogical.acl import ACLManager import plogical.CyberCPLogFileWriter as logging from plogical.virtualHostUtilities import virtualHostUtilities @@ -139,6 +140,107 @@ class FirewallManager: final_json = json.dumps(final_dic) return HttpResponse(final_json) + def modifyRule(self, userID=None, data=None): + """ + Modify an existing firewall rule + """ + try: + currentACL = ACLManager.loadedACL(userID) + + if currentACL['admin'] == 1: + pass + else: + return ACLManager.loadErrorJson('modify_status', 0) + + ruleID = data.get('id') + newRuleName = data.get('ruleName', '').strip() + newRuleProtocol = data.get('ruleProtocol', '').strip() + newRulePort = data.get('rulePort', '').strip() + newRuleIP = data.get('ruleIP', '').strip() + + # Validate inputs + if not newRuleName: + final_dic = {'status': 0, 'modify_status': 0, 'error_message': 'Rule name is required'} + final_json = json.dumps(final_dic) + return HttpResponse(final_json) + + if not newRuleProtocol or newRuleProtocol not in ['tcp', 'udp']: + final_dic = {'status': 0, 'modify_status': 0, 'error_message': 'Valid protocol (tcp/udp) is required'} + final_json = json.dumps(final_dic) + return HttpResponse(final_json) + + if not newRulePort: + final_dic = {'status': 0, 'modify_status': 0, 'error_message': 'Port is required'} + final_json = json.dumps(final_dic) + return HttpResponse(final_json) + + if not newRuleIP: + final_dic = {'status': 0, 'modify_status': 0, 'error_message': 'IP address is required'} + final_json = json.dumps(final_dic) + return HttpResponse(final_json) + + # Get existing rule + try: + existingRule = FirewallRules.objects.get(id=ruleID) + except FirewallRules.DoesNotExist: + final_dic = {'status': 0, 'modify_status': 0, 'error_message': 'Rule not found'} + final_json = json.dumps(final_dic) + return HttpResponse(final_json) + + # Check if anything changed + changed = False + if (existingRule.name != newRuleName or + existingRule.proto != newRuleProtocol or + existingRule.port != newRulePort or + existingRule.ipAddress != newRuleIP): + changed = True + + if changed: + # Check if new rule already exists (different ID) + existingDuplicate = FirewallRules.objects.filter( + name=newRuleName, + proto=newRuleProtocol, + port=newRulePort, + ipAddress=newRuleIP + ).exclude(id=ruleID).first() + + if existingDuplicate: + final_dic = {'status': 0, 'modify_status': 0, 'error_message': 'A rule with these settings already exists'} + final_json = json.dumps(final_dic) + return HttpResponse(final_json) + + # Delete old firewall rule + try: + FirewallUtilities.deleteRule(existingRule.proto, existingRule.port, existingRule.ipAddress) + logging.CyberCPLogFileWriter.writeToFile(f'Removed old firewall rule: {existingRule.proto}/{existingRule.port}/{existingRule.ipAddress}') + except Exception as e: + logging.CyberCPLogFileWriter.writeToFile(f'Warning: Could not remove old firewall rule: {str(e)}') + + # Add new firewall rule + try: + FirewallUtilities.addRule(newRuleProtocol, newRulePort, newRuleIP) + logging.CyberCPLogFileWriter.writeToFile(f'Added new firewall rule: {newRuleProtocol}/{newRulePort}/{newRuleIP}') + except Exception as e: + final_dic = {'status': 0, 'modify_status': 0, 'error_message': f'Failed to add firewall rule: {str(e)}'} + final_json = json.dumps(final_dic) + return HttpResponse(final_json) + + # Update database record + existingRule.name = newRuleName + existingRule.proto = newRuleProtocol + existingRule.port = newRulePort + existingRule.ipAddress = newRuleIP + existingRule.save() + + final_dic = {'status': 1, 'modify_status': 1, 'error_message': "None"} + final_json = json.dumps(final_dic) + return HttpResponse(final_json) + + except BaseException as msg: + final_dic = {'status': 0, 'modify_status': 0, 'error_message': str(msg)} + final_json = json.dumps(final_dic) + return HttpResponse(final_json) + def deleteRule(self, userID = None, data = None): try: @@ -1829,24 +1931,72 @@ class FirewallManager: except: banned_ips = [] - # Filter out expired bans + # Filter out expired bans and format data consistently current_time = time.time() active_banned_ips = [] for banned_ip in banned_ips: - if banned_ip.get('expires') == 'Never' or banned_ip.get('expires', 0) > current_time: - banned_ip['active'] = True - if banned_ip.get('expires') != 'Never': - banned_ip['expires'] = time.strftime('%Y-%m-%d %H:%M:%S', time.localtime(banned_ip['expires'])) - else: - banned_ip['expires'] = 'Never' - banned_ip['banned_on'] = time.strftime('%Y-%m-%d %H:%M:%S', time.localtime(banned_ip.get('banned_on', current_time))) - else: - banned_ip['active'] = False - banned_ip['expires'] = time.strftime('%Y-%m-%d %H:%M:%S', time.localtime(banned_ip.get('expires', current_time))) - banned_ip['banned_on'] = time.strftime('%Y-%m-%d %H:%M:%S', time.localtime(banned_ip.get('banned_on', current_time))) + # Get original values + expires_val = banned_ip.get('expires') + banned_on_val = banned_ip.get('banned_on', current_time) - active_banned_ips.append(banned_ip) + # Convert banned_on to timestamp if it's a string + if isinstance(banned_on_val, str): + try: + # Try parsing formatted date string + banned_on_val = time.mktime(time.strptime(banned_on_val, '%Y-%m-%d %H:%M:%S')) + except: + banned_on_val = current_time + elif not isinstance(banned_on_val, (int, float)): + banned_on_val = current_time + + # Check if expired + is_expired = False + if expires_val == 'Never' or expires_val is None: + expires_timestamp = None + expires_display = 'Never' + elif isinstance(expires_val, str): + if expires_val == 'Never': + expires_timestamp = None + expires_display = 'Never' + else: + try: + expires_timestamp = time.mktime(time.strptime(expires_val, '%Y-%m-%d %H:%M:%S')) + expires_display = expires_val + except: + expires_timestamp = None + expires_display = 'Never' + else: + expires_timestamp = expires_val + if expires_val > current_time: + expires_display = time.strftime('%Y-%m-%d %H:%M:%S', time.localtime(expires_val)) + else: + expires_display = time.strftime('%Y-%m-%d %H:%M:%S', time.localtime(expires_val)) + is_expired = True + + # Determine if active + if expires_timestamp is None: + is_active = True + else: + is_active = expires_timestamp > current_time and not is_expired + + # Format banned_on for display + banned_on_display = time.strftime('%Y-%m-%d %H:%M:%S', time.localtime(banned_on_val)) + + # Create formatted entry + formatted_ip = { + 'id': banned_ip.get('id'), + 'ip': banned_ip.get('ip'), + 'reason': banned_ip.get('reason', ''), + 'duration': banned_ip.get('duration', 'never'), + 'banned_on': banned_on_display, # String for display + 'banned_on_timestamp': banned_on_val * 1000, # Milliseconds for AngularJS date filter + 'expires': expires_display, # String for display + 'expires_timestamp': expires_timestamp * 1000 if expires_timestamp else None, # Milliseconds for AngularJS + 'active': is_active + } + + active_banned_ips.append(formatted_ip) final_dic = {'status': 1, 'bannedIPs': active_banned_ips} final_json = json.dumps(final_dic) @@ -1927,12 +2077,23 @@ class FirewallManager: } banned_ips.append(new_banned_ip) - # Ensure directory exists - os.makedirs(os.path.dirname(banned_ips_file), exist_ok=True) - - # Save to file - with open(banned_ips_file, 'w') as f: + # Write to temp file in /tmp (web server user has write permissions here) + temp_dir = tempfile.gettempdir() + temp_file = os.path.join(temp_dir, f'banned_ips_{int(time.time())}.json') + + with open(temp_file, 'w') as f: json.dump(banned_ips, f, indent=2) + + # Ensure /etc/cyberpanel directory exists with proper permissions + command = f'mkdir -p {os.path.dirname(banned_ips_file)} && chmod 755 {os.path.dirname(banned_ips_file)}' + ProcessUtilities.executioner(command, None, True) + + # Move temp file to final location and set permissions using ProcessUtilities + command = f'mv {temp_file} {banned_ips_file}' + ProcessUtilities.executioner(command, None, True) + + command = f'chmod 644 {banned_ips_file} && chown root:root {banned_ips_file}' + ProcessUtilities.executioner(command, None, True) # Apply firewall rule to block the IP try: @@ -1992,9 +2153,23 @@ class FirewallManager: final_json = json.dumps(final_dic) return HttpResponse(final_json) - # Save updated banned IPs - with open(banned_ips_file, 'w') as f: + # Write to temp file in /tmp (web server user has write permissions here) + temp_dir = tempfile.gettempdir() + temp_file = os.path.join(temp_dir, f'banned_ips_{int(time.time())}.json') + + with open(temp_file, 'w') as f: json.dump(banned_ips, f, indent=2) + + # Ensure /etc/cyberpanel directory exists with proper permissions + command = f'mkdir -p {os.path.dirname(banned_ips_file)} && chmod 755 {os.path.dirname(banned_ips_file)}' + ProcessUtilities.executioner(command, None, True) + + # Move temp file to final location and set permissions using ProcessUtilities + command = f'mv {temp_file} {banned_ips_file}' + ProcessUtilities.executioner(command, None, True) + + command = f'chmod 644 {banned_ips_file} && chown root:root {banned_ips_file}' + ProcessUtilities.executioner(command, None, True) # Remove iptables rule try: @@ -2019,6 +2194,153 @@ class FirewallManager: final_json = json.dumps(final_dic) return HttpResponse(final_json) + def modifyBannedIP(self, userID=None, data=None): + """ + Modify a banned IP address (update reason and/or expiration) + """ + try: + admin = Administrator.objects.get(pk=userID) + if admin.acl.adminStatus != 1: + return ACLManager.loadError() + + banned_ip_id = data.get('id') + new_ip = data.get('ip', '').strip() + reason = data.get('reason', '').strip() + duration = data.get('duration', 'never') + + if not new_ip: + final_dic = {'status': 0, 'error_message': 'IP address is required'} + final_json = json.dumps(final_dic) + return HttpResponse(final_json) + + # Validate IP address format + import ipaddress + try: + ipaddress.ip_address(new_ip.split('/')[0]) # Handle CIDR notation + except ValueError: + final_dic = {'status': 0, 'error_message': 'Invalid IP address format'} + final_json = json.dumps(final_dic) + return HttpResponse(final_json) + + if not reason: + final_dic = {'status': 0, 'error_message': 'Reason is required'} + final_json = json.dumps(final_dic) + return HttpResponse(final_json) + + # Load existing banned IPs + banned_ips_file = '/etc/cyberpanel/banned_ips.json' + banned_ips = [] + if os.path.exists(banned_ips_file): + try: + with open(banned_ips_file, 'r') as f: + banned_ips = json.load(f) + except: + banned_ips = [] + + # Find and update the banned IP + old_ip = None + found = False + for banned_ip in banned_ips: + if banned_ip.get('id') == banned_ip_id: + found = True + old_ip = banned_ip['ip'] + + # Check if new IP is already banned (and not the same record) + ip_changed = (new_ip != old_ip) + if ip_changed: + for other_banned_ip in banned_ips: + if other_banned_ip.get('id') != banned_ip_id and other_banned_ip.get('ip') == new_ip and other_banned_ip.get('active', True): + final_dic = {'status': 0, 'error_message': f'IP address {new_ip} is already banned'} + final_json = json.dumps(final_dic) + return HttpResponse(final_json) + + # Update IP address if changed + if ip_changed: + # Remove old iptables rule + try: + if '/' in old_ip: + subprocess.run(['iptables', '-D', 'INPUT', '-s', old_ip, '-j', 'DROP'], check=False) + else: + subprocess.run(['iptables', '-D', 'INPUT', '-s', old_ip, '-j', 'DROP'], check=False) + logging.CyberCPLogFileWriter.writeToFile(f'Removed iptables rule for old IP {old_ip}') + except Exception as e: + logging.CyberCPLogFileWriter.writeToFile(f'Warning: Could not remove old iptables rule for {old_ip}: {str(e)}') + + # Add new iptables rule + try: + if '/' in new_ip: + subprocess.run(['iptables', '-A', 'INPUT', '-s', new_ip, '-j', 'DROP'], check=True) + else: + subprocess.run(['iptables', '-A', 'INPUT', '-s', new_ip, '-j', 'DROP'], check=True) + logging.CyberCPLogFileWriter.writeToFile(f'Added iptables rule for new IP {new_ip}') + except subprocess.CalledProcessError as e: + final_dic = {'status': 0, 'error_message': f'Failed to add firewall rule for IP {new_ip}: {str(e)}'} + final_json = json.dumps(final_dic) + return HttpResponse(final_json) + + banned_ip['ip'] = new_ip + + # Update reason + banned_ip['reason'] = reason + + # Update expiration if duration changed + if duration == 'never' or duration == 'permanent': + banned_ip['expires'] = 'Never' + banned_ip['duration'] = 'never' + else: + # Calculate new expiration time + current_time = time.time() + duration_map = { + '1h': 3600, + '6h': 21600, + '12h': 43200, + '24h': 86400, + '48h': 172800, + '7d': 604800, + '30d': 2592000 + } + duration_seconds = duration_map.get(duration, 86400) + banned_ip['expires'] = current_time + duration_seconds + banned_ip['duration'] = duration + + break + + if not found: + final_dic = {'status': 0, 'error_message': 'Banned IP record not found'} + final_json = json.dumps(final_dic) + return HttpResponse(final_json) + + # Write to temp file in /tmp (web server user has write permissions here) + temp_dir = tempfile.gettempdir() + temp_file = os.path.join(temp_dir, f'banned_ips_{int(time.time())}.json') + + with open(temp_file, 'w') as f: + json.dump(banned_ips, f, indent=2) + + # Ensure /etc/cyberpanel directory exists with proper permissions + command = f'mkdir -p {os.path.dirname(banned_ips_file)} && chmod 755 {os.path.dirname(banned_ips_file)}' + ProcessUtilities.executioner(command, None, True) + + # Move temp file to final location and set permissions using ProcessUtilities + command = f'mv {temp_file} {banned_ips_file}' + ProcessUtilities.executioner(command, None, True) + + command = f'chmod 644 {banned_ips_file} && chown root:root {banned_ips_file}' + ProcessUtilities.executioner(command, None, True) + + ip_display = new_ip if new_ip != old_ip else old_ip + change_msg = f'IP changed from {old_ip} to {new_ip}' if new_ip != old_ip else f'IP unchanged ({old_ip})' + logging.CyberCPLogFileWriter.writeToFile(f'Modified banned IP record: {change_msg}, reason={reason}, duration={duration}') + + final_dic = {'status': 1, 'message': f'Banned IP has been modified successfully'} + final_json = json.dumps(final_dic) + return HttpResponse(final_json) + + except BaseException as msg: + final_dic = {'status': 0, 'error_message': str(msg)} + final_json = json.dumps(final_dic) + return HttpResponse(final_json) + def deleteBannedIP(self, userID=None, data=None): """ Permanently delete a banned IP record @@ -2054,9 +2376,36 @@ class FirewallManager: final_json = json.dumps(final_dic) return HttpResponse(final_json) - # Save updated banned IPs - with open(banned_ips_file, 'w') as f: + # Write to temp file in /tmp (web server user has write permissions here) + temp_dir = tempfile.gettempdir() + temp_file = os.path.join(temp_dir, f'banned_ips_{int(time.time())}.json') + with open(temp_file, 'w') as f: json.dump(updated_banned_ips, f, indent=2) + + # Ensure /etc/cyberpanel directory exists with proper permissions + command = f'mkdir -p {os.path.dirname(banned_ips_file)} && chmod 755 {os.path.dirname(banned_ips_file)}' + ProcessUtilities.executioner(command, None, True) + + # Move temp file to final location and set permissions using ProcessUtilities + command = f'mv {temp_file} {banned_ips_file}' + ProcessUtilities.executioner(command, None, True) + + command = f'chmod 644 {banned_ips_file} && chown root:root {banned_ips_file}' + ProcessUtilities.executioner(command, None, True) + + # Remove iptables rule to unban the IP + try: + # Remove iptables rule to unblock the IP + if '/' in ip_to_delete: + # CIDR notation + subprocess.run(['iptables', '-D', 'INPUT', '-s', ip_to_delete, '-j', 'DROP'], check=False) + else: + # Single IP + subprocess.run(['iptables', '-D', 'INPUT', '-s', ip_to_delete, '-j', 'DROP'], check=False) + + logging.CyberCPLogFileWriter.writeToFile(f'Deleted and unbanned IP {ip_to_delete}') + except Exception as e: + logging.CyberCPLogFileWriter.writeToFile(f'Failed to remove iptables rule for {ip_to_delete}: {str(e)}') logging.CyberCPLogFileWriter.writeToFile(f'Deleted banned IP record for {ip_to_delete}') @@ -2227,6 +2576,253 @@ class FirewallManager: final_json = json.dumps(final_dic) return HttpResponse(final_json) + def exportBannedIPs(self, userID=None): + """ + Export all banned IPs to a JSON file + """ + try: + currentACL = ACLManager.loadedACL(userID) + + if currentACL['admin'] == 1: + pass + else: + return ACLManager.loadErrorJson('exportStatus', 0) + + # Load banned IPs from file + banned_ips_file = '/etc/cyberpanel/banned_ips.json' + banned_ips = [] + if os.path.exists(banned_ips_file): + try: + with open(banned_ips_file, 'r') as f: + banned_ips = json.load(f) + except Exception as e: + logging.CyberCPLogFileWriter.writeToFile(f"Error reading banned IPs file: {str(e)}") + banned_ips = [] + + # Filter out expired bans for export (optional - you might want to include all) + current_time = time.time() + active_banned_ips = [] + for banned_ip in banned_ips: + expires_val = banned_ip.get('expires') + expires_timestamp = banned_ip.get('expires_timestamp') + + # Include if never expires or not yet expired + if expires_val == 'Never' or expires_timestamp is None: + active_banned_ips.append(banned_ip) + elif isinstance(expires_timestamp, (int, float)) and expires_timestamp > current_time: + active_banned_ips.append(banned_ip) + + # Create export data with metadata + export_data = { + 'version': '1.0', + 'exported_at': time.strftime('%Y-%m-%d %H:%M:%S'), + 'total_banned_ips': len(active_banned_ips), + 'banned_ips': active_banned_ips + } + + # Create JSON response with file download + json_content = json.dumps(export_data, indent=2) + + logging.CyberCPLogFileWriter.writeToFile(f"Banned IPs exported successfully. Total IPs: {len(active_banned_ips)}") + + # Return file as download + response = HttpResponse(json_content, content_type='application/json') + response['Content-Disposition'] = f'attachment; filename="banned_ips_export_{int(time.time())}.json"' + + return response + + except BaseException as msg: + final_dic = {'exportStatus': 0, 'error_message': str(msg)} + final_json = json.dumps(final_dic) + return HttpResponse(final_json) + + def importBannedIPs(self, userID=None, data=None): + """ + Import banned IPs from a JSON file + """ + try: + currentACL = ACLManager.loadedACL(userID) + + if currentACL['admin'] == 1: + pass + else: + return ACLManager.loadErrorJson('importStatus', 0) + + # 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')) + else: + # Fallback to file path method + import_file_path = data.get('import_file_path', '') if data else '' + + if not import_file_path or not os.path.exists(import_file_path): + final_dic = {'importStatus': 0, 'error_message': 'Import file not found or invalid path'} + final_json = json.dumps(final_dic) + return HttpResponse(final_json) + + # Read and parse the import file + with open(import_file_path, 'r') as f: + import_data = json.load(f) + + # Validate the import data structure + if 'banned_ips' not in import_data: + final_dic = {'importStatus': 0, 'error_message': 'Invalid import file format. Missing banned_ips array.'} + final_json = json.dumps(final_dic) + return HttpResponse(final_json) + + imported_count = 0 + skipped_count = 0 + error_count = 0 + errors = [] + + # Load existing banned IPs + banned_ips_file = '/etc/cyberpanel/banned_ips.json' + existing_banned_ips = [] + if os.path.exists(banned_ips_file): + try: + with open(banned_ips_file, 'r') as f: + existing_banned_ips = json.load(f) + except: + existing_banned_ips = [] + + # Create a set of existing IPs for quick lookup + existing_ips = {banned_ip.get('ip') for banned_ip in existing_banned_ips} + + # Import IP address validation + import ipaddress + + for banned_ip_data in import_data['banned_ips']: + try: + ip_address = banned_ip_data.get('ip', '').strip() + reason = banned_ip_data.get('reason', '').strip() + + # Validate IP address + if not ip_address: + error_count += 1 + errors.append(f"Invalid entry: Missing IP address") + continue + + try: + ipaddress.ip_address(ip_address.split('/')[0]) # Handle CIDR notation + except ValueError: + error_count += 1 + errors.append(f"IP '{ip_address}': Invalid IP address format") + continue + + # Check if IP already exists + if ip_address in existing_ips: + skipped_count += 1 + continue + + # Validate reason + if not reason: + reason = 'Imported from backup' + + # Get duration or calculate from expires + duration = banned_ip_data.get('duration', 'never') + expires = banned_ip_data.get('expires', 'Never') + expires_timestamp = banned_ip_data.get('expires_timestamp') + + # Calculate expiration if needed + if expires == 'Never' or expires_timestamp is None: + expires_timestamp = None + expires_display = 'Never' + duration = 'never' + elif expires_timestamp and isinstance(expires_timestamp, (int, float)): + expires_display = time.strftime('%Y-%m-%d %H:%M:%S', time.localtime(expires_timestamp)) + else: + expires_display = expires if isinstance(expires, str) else 'Never' + expires_timestamp = None + + # Create banned IP entry + banned_on = banned_ip_data.get('banned_on', time.time()) + if isinstance(banned_on, str): + try: + banned_on = time.mktime(time.strptime(banned_on, '%Y-%m-%d %H:%M:%S')) + except: + banned_on = time.time() + + new_banned_ip = { + 'id': banned_ip_data.get('id', randint(100000, 999999)), + 'ip': ip_address, + 'reason': reason, + 'banned_on': banned_on, + 'banned_on_timestamp': banned_on if isinstance(banned_on, (int, float)) else time.time(), + 'expires': expires_display, + 'expires_timestamp': expires_timestamp, + 'duration': duration, + 'active': True + } + + # Add iptables rule + try: + if '/' in ip_address: + subprocess.run(['iptables', '-A', 'INPUT', '-s', ip_address, '-j', 'DROP'], check=True) + else: + subprocess.run(['iptables', '-A', 'INPUT', '-s', ip_address, '-j', 'DROP'], check=True) + logging.CyberCPLogFileWriter.writeToFile(f'Added iptables rule for imported IP {ip_address}') + except subprocess.CalledProcessError as e: + error_count += 1 + errors.append(f"IP '{ip_address}': Failed to add firewall rule - {str(e)}") + continue + + # Add to existing list + existing_banned_ips.append(new_banned_ip) + existing_ips.add(ip_address) + imported_count += 1 + + except Exception as e: + error_count += 1 + errors.append(f"IP '{banned_ip_data.get('ip', 'Unknown')}': {str(e)}") + logging.CyberCPLogFileWriter.writeToFile(f"Error importing banned IP {banned_ip_data.get('ip', 'Unknown')}: {str(e)}") + + # Save updated banned IPs + if imported_count > 0 or error_count > 0: + try: + # Create temp file + temp_file = f'/tmp/banned_ips_{randint(100000, 999999)}.json' + with open(temp_file, 'w') as f: + json.dump(existing_banned_ips, f, indent=2) + + # Ensure directory exists + command = f'mkdir -p {os.path.dirname(banned_ips_file)} && chmod 755 {os.path.dirname(banned_ips_file)}' + ProcessUtilities.executioner(command) + + # Move temp file to final location + command = f'mv {temp_file} {banned_ips_file}' + ProcessUtilities.executioner(command) + + # Set permissions + command = f'chmod 644 {banned_ips_file} && chown root:root {banned_ips_file}' + ProcessUtilities.executioner(command) + + except Exception as e: + logging.CyberCPLogFileWriter.writeToFile(f"Error saving imported banned IPs: {str(e)}") + final_dic = {'importStatus': 0, 'error_message': f'Failed to save imported IPs: {str(e)}'} + final_json = json.dumps(final_dic) + return HttpResponse(final_json) + + logging.CyberCPLogFileWriter.writeToFile(f"Banned IPs import completed. Imported: {imported_count}, Skipped: {skipped_count}, Errors: {error_count}") + + final_dic = { + 'importStatus': 1, + 'error_message': "None", + 'imported_count': imported_count, + 'skipped_count': skipped_count, + 'error_count': error_count, + 'errors': errors + } + final_json = json.dumps(final_dic) + return HttpResponse(final_json) + + except BaseException as msg: + final_dic = {'importStatus': 0, 'error_message': str(msg)} + final_json = json.dumps(final_dic) + return HttpResponse(final_json) + diff --git a/firewall/static/firewall/firewall.js b/firewall/static/firewall/firewall.js index 1853a3544..8656822c3 100644 --- a/firewall/static/firewall/firewall.js +++ b/firewall/static/firewall/firewall.js @@ -499,6 +499,281 @@ app.controller('firewallController', function ($scope, $http, $timeout, $window, }; + // Modify Firewall Rule Functions + $scope.handleModifyRuleClick = function(rule, event) { + if (event) { + event.preventDefault(); + event.stopPropagation(); + } + + if (!rule) { + console.error('No rule provided'); + if (typeof PNotify !== 'undefined') { + new PNotify({ + title: 'Error', + text: 'No rule data provided', + type: 'error' + }); + } + return false; + } + + $scope.showModifyRuleModal(rule, event); + return false; + }; + + $scope.showModifyRuleModal = function(rule, event) { + console.log('=== showModifyRuleModal CALLED ==='); + console.log('Rule:', rule); + + if (!rule) { + console.error('No rule provided'); + return false; + } + + // Get modal element + var modalElement = document.getElementById('modifyRuleModal'); + if (!modalElement) { + console.error('Modal element not found'); + alert('Error: Modal element not found. Please refresh the page.'); + return false; + } + + // Set form values + var idField = document.getElementById('modifyRuleId'); + var nameField = document.getElementById('modifyRuleName'); + var protocolField = document.getElementById('modifyRuleProtocol'); + var ipField = document.getElementById('modifyRuleIP'); + var portField = document.getElementById('modifyRulePort'); + + if (idField) idField.value = rule.id || ''; + if (nameField) nameField.value = rule.name || ''; + if (protocolField) protocolField.value = rule.proto || 'tcp'; + if (ipField) ipField.value = rule.ipAddress || ''; + if (portField) portField.value = rule.port || ''; + + // Show modal using AngularJS $timeout + $timeout(function() { + // Clean up existing modals/backdrops + var existingBackdrops = document.querySelectorAll('.modal-backdrop'); + existingBackdrops.forEach(function(b) { b.remove(); }); + + var existingModals = document.querySelectorAll('.modal.show'); + existingModals.forEach(function(m) { + m.classList.remove('show'); + }); + + document.body.classList.remove('modal-open'); + + // Move modal to body if needed + if (modalElement.parentElement !== document.body) { + document.body.appendChild(modalElement); + } + + // Show modal + modalElement.classList.add('show', 'fade'); + modalElement.style.cssText = 'display: flex !important; position: fixed !important; top: 0 !important; left: 0 !important; width: 100% !important; height: 100% !important; z-index: 99999 !important; opacity: 1 !important; visibility: visible !important; align-items: center !important; justify-content: center !important;'; + modalElement.removeAttribute('aria-hidden'); + modalElement.setAttribute('aria-hidden', 'false'); + modalElement.setAttribute('aria-modal', 'true'); + + document.body.classList.add('modal-open'); + document.body.style.overflow = 'hidden'; + + // Create backdrop + var backdrop = document.createElement('div'); + backdrop.className = 'modal-backdrop fade show'; + backdrop.style.cssText = 'position: fixed !important; top: 0 !important; left: 0 !important; width: 100% !important; height: 100% !important; z-index: 99998 !important; background-color: rgba(0, 0, 0, 0.5) !important;'; + backdrop.id = 'modifyRuleModalBackdrop'; + document.body.appendChild(backdrop); + + // Handle backdrop click + backdrop.addEventListener('click', function(e) { + if (e.target === backdrop) { + $scope.closeModifyRuleModal(); + } + }); + + // Try jQuery/Bootstrap modal if available + if (typeof $ !== 'undefined' && $.fn.modal) { + try { + var $modal = $('#modifyRuleModal'); + if ($modal.length > 0) { + if ($modal.parent()[0] !== document.body) { + $modal.appendTo('body'); + } + if (!$modal.data('bs.modal')) { + $modal.modal({show: false, backdrop: true, keyboard: true}); + } + $modal.modal('show'); + } + } catch (e) { + console.warn('jQuery modal failed, using direct display:', e); + } + } + }, 10); + }; + + $scope.closeModifyRuleModal = function() { + var modalElement = document.getElementById('modifyRuleModal'); + if (modalElement) { + // Try jQuery/Bootstrap modal first + if (typeof $ !== 'undefined' && $.fn.modal) { + try { + $('#modifyRuleModal').modal('hide'); + } catch (e) { + // Fall through to manual cleanup + } + } + + // Manual cleanup + modalElement.classList.remove('show', 'fade'); + modalElement.style.display = 'none'; + modalElement.setAttribute('aria-hidden', 'true'); + + // Remove backdrop + var backdrops = document.querySelectorAll('.modal-backdrop'); + backdrops.forEach(function(b) { b.remove(); }); + + document.body.classList.remove('modal-open'); + document.body.style.overflow = ''; + } + }; + + $scope.modifyRule = function() { + var ruleId = document.getElementById('modifyRuleId').value; + var ruleName = document.getElementById('modifyRuleName').value.trim(); + var ruleProtocol = document.getElementById('modifyRuleProtocol').value; + var ruleIP = document.getElementById('modifyRuleIP').value.trim(); + var rulePort = document.getElementById('modifyRulePort').value.trim(); + + // Validation + if (!ruleName) { + if (typeof PNotify !== 'undefined') { + new PNotify({ + title: 'Validation Error', + text: 'Please enter a rule name', + type: 'error' + }); + } else { + alert('Please enter a rule name'); + } + return; + } + + if (!ruleProtocol || (ruleProtocol !== 'tcp' && ruleProtocol !== 'udp')) { + if (typeof PNotify !== 'undefined') { + new PNotify({ + title: 'Validation Error', + text: 'Please select a valid protocol (TCP or UDP)', + type: 'error' + }); + } else { + alert('Please select a valid protocol'); + } + return; + } + + if (!ruleIP) { + if (typeof PNotify !== 'undefined') { + new PNotify({ + title: 'Validation Error', + text: 'Please enter an IP address', + type: 'error' + }); + } else { + alert('Please enter an IP address'); + } + return; + } + + if (!rulePort) { + if (typeof PNotify !== 'undefined') { + new PNotify({ + title: 'Validation Error', + text: 'Please enter a port number', + type: 'error' + }); + } else { + alert('Please enter a port number'); + } + return; + } + + $scope.rulesLoading = false; + $scope.actionFailed = true; + $scope.actionSuccess = true; + $scope.couldNotConnect = true; + + var url = "/firewall/modifyRule"; + var data = { + id: ruleId, + ruleName: ruleName, + ruleProtocol: ruleProtocol, + rulePort: rulePort, + ruleIP: ruleIP + }; + + var config = { + headers: { + 'X-CSRFToken': getCookie('csrftoken') + } + }; + + $http.post(url, data, config).then(function(response) { + $scope.rulesLoading = true; + + if (response.data && response.data.modify_status === 1) { + // Close modal + $scope.closeModifyRuleModal(); + + // Refresh rules list + populateCurrentRecords(); + + $scope.actionFailed = true; + $scope.actionSuccess = false; + $scope.canNotAddRule = true; + $scope.ruleAdded = false; + $scope.couldNotConnect = true; + + if (typeof PNotify !== 'undefined') { + new PNotify({ + title: 'Success!', + text: 'Firewall rule modified successfully', + type: 'success' + }); + } + } else { + $scope.actionFailed = false; + $scope.actionSuccess = true; + $scope.errorMessage = (response.data && response.data.error_message) || 'Failed to modify firewall rule'; + + if (typeof PNotify !== 'undefined') { + new PNotify({ + title: 'Error!', + text: (response.data && response.data.error_message) || 'Failed to modify firewall rule', + type: 'error' + }); + } + } + }, function(error) { + $scope.rulesLoading = true; + $scope.couldNotConnect = false; + + if (typeof PNotify !== 'undefined') { + new PNotify({ + title: 'Connection Error', + text: 'Could not connect to server. Please refresh this page.', + type: 'error' + }); + } + }); + }; + + // Make modify rule functions available globally + window.showModifyRuleModalScope = $scope.showModifyRuleModal; + window.closeModifyRuleModalScope = $scope.closeModifyRuleModal; + window.modifyRuleScope = $scope.modifyRule; $scope.reloadFireWall = function () { @@ -3828,53 +4103,325 @@ app.controller('litespeed_ent_conf', function ($scope, $http, $timeout, $window) }); }; + // Export/Import Banned IPs Functions + $scope.exportBannedIPs = function () { + $scope.bannedIPsLoading = false; + $scope.bannedIPActionFailed = true; + $scope.bannedIPActionSuccess = true; + + var url = "/firewall/exportBannedIPs"; + var data = {}; + + var config = { + headers: { + 'X-CSRFToken': getCookie('csrftoken') + }, + responseType: 'blob' + }; + + $http.post(url, data, config).then(function(response) { + $scope.bannedIPsLoading = true; + + // Check if response is JSON (error) or file download + if (response.data instanceof Blob) { + // Create blob URL and trigger download + var blob = new Blob([response.data], { type: 'application/json' }); + var url = window.URL.createObjectURL(blob); + var a = document.createElement('a'); + a.href = url; + a.download = 'banned_ips_export_' + Math.floor(Date.now() / 1000) + '.json'; + document.body.appendChild(a); + a.click(); + window.URL.revokeObjectURL(url); + document.body.removeChild(a); + + $scope.bannedIPActionFailed = true; + $scope.bannedIPActionSuccess = false; + + if (typeof PNotify !== 'undefined') { + new PNotify({ + title: 'Success!', + text: 'Banned IPs exported successfully', + type: 'success' + }); + } + } else { + // Handle error response + try { + var errorData = typeof response.data === 'string' ? JSON.parse(response.data) : response.data; + if (errorData.exportStatus === 0) { + $scope.bannedIPActionFailed = false; + $scope.bannedIPActionSuccess = true; + $scope.bannedIPErrorMessage = errorData.error_message; + + if (typeof PNotify !== 'undefined') { + new PNotify({ + title: 'Export Failed', + text: errorData.error_message, + type: 'error' + }); + } + } + } catch (e) { + // If not JSON, try reading as text + var reader = new FileReader(); + reader.onload = function() { + try { + var errorData = JSON.parse(reader.result); + if (errorData.exportStatus === 0) { + $scope.bannedIPActionFailed = false; + $scope.bannedIPActionSuccess = true; + $scope.bannedIPErrorMessage = errorData.error_message; + } + } catch (e2) { + $scope.bannedIPActionFailed = false; + $scope.bannedIPActionSuccess = true; + $scope.bannedIPErrorMessage = 'Failed to export banned IPs'; + } + }; + reader.readAsText(response.data); + } + } + }, function(error) { + $scope.bannedIPsLoading = true; + $scope.bannedIPActionFailed = false; + $scope.bannedIPActionSuccess = true; + $scope.bannedIPErrorMessage = 'Could not connect to server. Please refresh this page.'; + + if (typeof PNotify !== 'undefined') { + new PNotify({ + title: 'Connection Error', + text: 'Could not connect to server. Please refresh this page.', + type: 'error' + }); + } + }); + }; + + $scope.importBannedIPs = function () { + // Create file input element + var input = document.createElement('input'); + input.type = 'file'; + input.accept = '.json'; + 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.banned_ips || !Array.isArray(importData.banned_ips)) { + $scope.$apply(function() { + $scope.bannedIPActionFailed = false; + $scope.bannedIPActionSuccess = true; + $scope.bannedIPErrorMessage = "Invalid import file format. Please select a valid banned IPs export file."; + }); + + if (typeof PNotify !== 'undefined') { + new PNotify({ + title: 'Invalid File', + text: 'Invalid import file format. Please select a valid banned IPs export file.', + type: 'error' + }); + } + return; + } + + // Upload file to server + uploadBannedIPsImportFile(file); + } catch (error) { + $scope.$apply(function() { + $scope.bannedIPActionFailed = false; + $scope.bannedIPActionSuccess = true; + $scope.bannedIPErrorMessage = "Invalid JSON file. Please select a valid banned IPs export file."; + }); + + if (typeof PNotify !== 'undefined') { + new PNotify({ + title: 'Invalid File', + text: 'Invalid JSON file. Please select a valid banned IPs export file.', + type: 'error' + }); + } + } + }; + reader.readAsText(file); + } + }; + + document.body.appendChild(input); + input.click(); + document.body.removeChild(input); + }; + + function uploadBannedIPsImportFile(file) { + $scope.bannedIPsLoading = false; + $scope.bannedIPActionFailed = true; + $scope.bannedIPActionSuccess = true; + $scope.bannedIPCouldNotConnect = true; + + var formData = new FormData(); + formData.append('import_file', file); + + var config = { + headers: { + 'X-CSRFToken': getCookie('csrftoken'), + 'Content-Type': undefined + }, + transformRequest: angular.identity + }; + + $http.post("/firewall/importBannedIPs", formData, config).then(function(response) { + $scope.bannedIPsLoading = true; + + if (response.data.importStatus === 1) { + $scope.bannedIPActionSuccess = false; + populateBannedIPs(); // Refresh the list + + var message = `Import completed: ${response.data.imported_count} imported, ${response.data.skipped_count} skipped`; + if (response.data.error_count > 0) { + message += `, ${response.data.error_count} errors`; + if (response.data.errors && response.data.errors.length > 0) { + message += '\nErrors: ' + response.data.errors.slice(0, 5).join('; '); + if (response.data.errors.length > 5) { + message += ` ... and ${response.data.errors.length - 5} more`; + } + } + } + + if (typeof PNotify !== 'undefined') { + new PNotify({ + title: 'Import Completed!', + text: message, + type: response.data.error_count > 0 ? 'notice' : 'success' + }); + } else { + alert(message); + } + } else { + $scope.bannedIPActionFailed = false; + $scope.bannedIPErrorMessage = response.data.error_message || 'Failed to import banned IPs'; + + if (typeof PNotify !== 'undefined') { + new PNotify({ + title: 'Import Failed', + text: response.data.error_message || 'Failed to import banned IPs', + type: 'error' + }); + } + } + }, function(error) { + $scope.bannedIPsLoading = true; + $scope.bannedIPCouldNotConnect = false; + + if (typeof PNotify !== 'undefined') { + new PNotify({ + title: 'Connection Error', + text: 'Could not connect to server. Please refresh this page.', + type: 'error' + }); + } + }); + } + // Export/Import Firewall Rules Functions $scope.exportRules = function () { $scope.rulesLoading = false; $scope.actionFailed = true; $scope.actionSuccess = true; - url = "/firewall/exportFirewallRules"; - + var url = "/firewall/exportFirewallRules"; var data = {}; var config = { headers: { 'X-CSRFToken': getCookie('csrftoken') - } + }, + responseType: 'blob' }; - $http.post(url, data, config).then(exportSuccess, exportError); - - function exportSuccess(response) { + $http.post(url, data, config).then(function(response) { $scope.rulesLoading = true; // Check if response is JSON (error) or file download - if (typeof response.data === 'string' && response.data.includes('{')) { - try { - var errorData = JSON.parse(response.data); - if (errorData.exportStatus === 0) { - $scope.actionFailed = false; - $scope.actionSuccess = true; - $scope.errorMessage = errorData.error_message; - return; + if (response.data instanceof Blob) { + // Check if it's actually a JSON error by reading the blob + var reader = new FileReader(); + reader.onload = function() { + try { + var text = reader.result; + // Check if it's JSON error + if (text.trim().startsWith('{')) { + var errorData = JSON.parse(text); + if (errorData.exportStatus === 0) { + $scope.$apply(function() { + $scope.actionFailed = false; + $scope.actionSuccess = true; + $scope.errorMessage = errorData.error_message; + }); + + if (typeof PNotify !== 'undefined') { + new PNotify({ + title: 'Export Failed', + text: errorData.error_message, + type: 'error' + }); + } + return; + } + } + } catch (e) { + // Not JSON, proceed with download } - } catch (e) { - // If not JSON, assume it's the file content - } + + // It's a valid file, trigger download + var blob = new Blob([response.data], { type: 'application/json' }); + var url = window.URL.createObjectURL(blob); + var a = document.createElement('a'); + a.href = url; + a.download = 'firewall_rules_export_' + Math.floor(Date.now() / 1000) + '.json'; + document.body.appendChild(a); + a.click(); + window.URL.revokeObjectURL(url); + document.body.removeChild(a); + + $scope.$apply(function() { + $scope.actionFailed = true; + $scope.actionSuccess = false; + }); + + if (typeof PNotify !== 'undefined') { + new PNotify({ + title: 'Success!', + text: 'Firewall rules exported successfully', + type: 'success' + }); + } + }; + reader.readAsText(response.data); + } else { + // Handle as text response (shouldn't happen with blob) + $scope.actionFailed = true; + $scope.actionSuccess = false; } - - // If we get here, it's a successful file download - $scope.actionFailed = true; - $scope.actionSuccess = false; - } - - function exportError(response) { + }, function(error) { $scope.rulesLoading = true; $scope.actionFailed = false; $scope.actionSuccess = true; $scope.errorMessage = "Could not connect to server. Please refresh this page."; - } + + if (typeof PNotify !== 'undefined') { + new PNotify({ + title: 'Connection Error', + text: 'Could not connect to server. Please refresh this page.', + type: 'error' + }); + } + }); }; $scope.importRules = function () { @@ -3942,7 +4489,7 @@ app.controller('litespeed_ent_conf', function ($scope, $http, $timeout, $window) function importSuccess(response) { $scope.rulesLoading = true; - if (response.data.importStatus === 1) { + if (response.data && response.data.importStatus === 1) { $scope.actionFailed = true; $scope.actionSuccess = false; @@ -3950,20 +4497,38 @@ app.controller('litespeed_ent_conf', function ($scope, $http, $timeout, $window) 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 message = `Import completed: ${response.data.imported_count} imported, ${response.data.skipped_count} skipped`; + if (response.data.error_count > 0) { + message += `, ${response.data.error_count} errors`; + if (response.data.errors && response.data.errors.length > 0) { + message += '\nErrors: ' + response.data.errors.slice(0, 5).join('; '); + if (response.data.errors.length > 5) { + message += ` ... and ${response.data.errors.length - 5} more`; + } + } } - alert(summary); + if (typeof PNotify !== 'undefined') { + new PNotify({ + title: 'Import Completed!', + text: message, + type: response.data.error_count > 0 ? 'notice' : 'success' + }); + } else { + alert(message); + } } else { $scope.actionFailed = false; $scope.actionSuccess = true; - $scope.errorMessage = response.data.error_message; + $scope.errorMessage = (response.data && response.data.error_message) || 'Failed to import firewall rules'; + + if (typeof PNotify !== 'undefined') { + new PNotify({ + title: 'Import Failed', + text: (response.data && response.data.error_message) || 'Failed to import firewall rules', + type: 'error' + }); + } } } @@ -3972,6 +4537,14 @@ app.controller('litespeed_ent_conf', function ($scope, $http, $timeout, $window) $scope.actionFailed = false; $scope.actionSuccess = true; $scope.errorMessage = "Could not connect to server. Please refresh this page."; + + if (typeof PNotify !== 'undefined') { + new PNotify({ + title: 'Connection Error', + text: 'Could not connect to server. Please refresh this page.', + type: 'error' + }); + } } } diff --git a/firewall/templates/firewall/firewall.html b/firewall/templates/firewall/firewall.html index a04e17f21..ff51e787f 100644 --- a/firewall/templates/firewall/firewall.html +++ b/firewall/templates/firewall/firewall.html @@ -618,7 +618,7 @@ } /* Modal Styles - Ensure it's above everything */ - #modifyBannedIPModal { + #modifyBannedIPModal, #modifyRuleModal { z-index: 99999 !important; position: fixed !important; top: 0 !important; @@ -627,7 +627,7 @@ height: 100% !important; } - #modifyBannedIPModal.show { + #modifyBannedIPModal.show, #modifyRuleModal.show { display: flex !important; opacity: 1 !important; visibility: visible !important; @@ -635,11 +635,11 @@ justify-content: center !important; } - #modifyBannedIPModal.fade.show { + #modifyBannedIPModal.fade.show, #modifyRuleModal.fade.show { opacity: 1 !important; } - #modifyBannedIPModal .modal-dialog { + #modifyBannedIPModal .modal-dialog, #modifyRuleModal .modal-dialog { z-index: 100000 !important; position: relative !important; margin: 1.75rem auto !important; @@ -664,7 +664,9 @@ /* Make sure modal is not hidden by parent containers */ .modern-container #modifyBannedIPModal, - .banned-ips-panel #modifyBannedIPModal { + .banned-ips-panel #modifyBannedIPModal, + .modern-container #modifyRuleModal, + .rules-panel #modifyRuleModal { position: fixed !important; z-index: 99999 !important; } @@ -851,6 +853,52 @@ transform: translateY(-1px); box-shadow: 0 2px 8px rgba(33, 150, 243, 0.3); } + + .export-import-buttons { + display: flex; + gap: 0.75rem; + align-items: center; + } + + .btn-export, .btn-import { + display: flex; + align-items: center; + gap: 0.5rem; + padding: 0.5rem 1rem; + border-radius: 6px; + font-weight: 500; + font-size: 0.9rem; + cursor: pointer; + transition: all 0.3s ease; + border: none; + } + + .btn-export { + background: #10b981; + color: white; + } + + .btn-export:hover:not(:disabled) { + background: #059669; + transform: translateY(-1px); + box-shadow: 0 2px 8px rgba(16, 185, 129, 0.3); + } + + .btn-import { + background: #3b82f6; + color: white; + } + + .btn-import:hover:not(:disabled) { + background: #2563eb; + transform: translateY(-1px); + box-shadow: 0 2px 8px rgba(59, 130, 246, 0.3); + } + + .btn-export:disabled, .btn-import:disabled { + opacity: 0.5; + cursor: not-allowed; + } /* Ensure actions column and buttons are always visible */ .banned-table td.actions { @@ -1114,7 +1162,14 @@ {$ rule.port $} - + + + + + @@ -1296,6 +1369,75 @@ + + +