diff --git a/CyberCP/secMiddleware.py b/CyberCP/secMiddleware.py index 29061ce9e..dcf498c22 100644 --- a/CyberCP/secMiddleware.py +++ b/CyberCP/secMiddleware.py @@ -267,7 +267,7 @@ class secMiddleware: response['X-XSS-Protection'] = "1; mode=block" response['X-Frame-Options'] = "sameorigin" - response['Content-Security-Policy'] = "script-src 'self' https://www.jsdelivr.com" + response['Content-Security-Policy'] = "script-src 'self' 'unsafe-inline' https://www.jsdelivr.com" response['Content-Security-Policy'] = "connect-src *;" response['Content-Security-Policy'] = "font-src 'self' 'unsafe-inline' https://www.jsdelivr.com https://fonts.googleapis.com" response[ diff --git a/CyberCP/settings.py b/CyberCP/settings.py index 2008956ad..ea1b2d679 100644 --- a/CyberCP/settings.py +++ b/CyberCP/settings.py @@ -13,15 +13,15 @@ https://docs.djangoproject.com/en/1.11/ref/settings/ import os from django.utils.translation import gettext_lazy as _ -# Patreon OAuth Configuration for Paid Plugins -# SECURITY: Environment variables take precedence. Hardcoded values are fallback for this server only. -# For repository version, use empty defaults and set via environment variables. -PATREON_CLIENT_ID = os.environ.get('PATREON_CLIENT_ID', 'LFXeXUcfrM8MeVbUcmGbB7BgeJ9RzZi2v_H9wL4d9vG6t1dV4SUnQ4ibn9IYzvt7') -PATREON_CLIENT_SECRET = os.environ.get('PATREON_CLIENT_SECRET', 'APuJ5qoL3TLFmNnGDVkgl-qr3sCzp2CQsKfslBbp32hhnhlD0y6-ZcSCkb_FaUJv') +# Patreon OAuth (optional): for paid-plugin verification via Patreon membership. +# Set these only if you use Patreon-gated plugins; leave unset otherwise. +# Use environment variables; no defaults so the repo stays generic and safe to push to GitHub. +PATREON_CLIENT_ID = os.environ.get('PATREON_CLIENT_ID', '') +PATREON_CLIENT_SECRET = os.environ.get('PATREON_CLIENT_SECRET', '') PATREON_CREATOR_ID = os.environ.get('PATREON_CREATOR_ID', '') -PATREON_MEMBERSHIP_TIER_ID = os.environ.get('PATREON_MEMBERSHIP_TIER_ID', '27789984') # CyberPanel Paid Plugin tier -PATREON_CREATOR_ACCESS_TOKEN = os.environ.get('PATREON_CREATOR_ACCESS_TOKEN', 'niAHRiI9SgrRCMmaf5exoXXphy3RWXWsX4kO5Yv9SQI') -PATREON_CREATOR_REFRESH_TOKEN = os.environ.get('PATREON_CREATOR_REFRESH_TOKEN', 'VZlCQoPwJUr4NLni1N82-K_CpJHTAOYUOCx2PujdjQg') +PATREON_MEMBERSHIP_TIER_ID = os.environ.get('PATREON_MEMBERSHIP_TIER_ID', '') +PATREON_CREATOR_ACCESS_TOKEN = os.environ.get('PATREON_CREATOR_ACCESS_TOKEN', '') +PATREON_CREATOR_REFRESH_TOKEN = os.environ.get('PATREON_CREATOR_REFRESH_TOKEN', '') # Build paths inside the project like this: os.path.join(BASE_DIR, ...) BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) @@ -37,6 +37,22 @@ DEBUG = False ALLOWED_HOSTS = ['*'] +# When the panel is behind a reverse proxy (e.g. https://panel.example.com -> http://backend:port), +# the browser sends Origin/Referer with the public domain while the proxy may send Host as the +# backend address. Django then fails CSRF (Referer vs Host mismatch) and POSTs get 403. +# Set CSRF_TRUSTED_ORIGINS to your public origin(s) so CSRF passes. Optional; leave unset if +# you access the panel by IP:port only. +# Example: export CSRF_TRUSTED_ORIGINS="https://panel.example.com,http://panel.example.com" +_csrf_origins_env = os.environ.get('CSRF_TRUSTED_ORIGINS', '') +_csrf_origins_list = [o.strip() for o in _csrf_origins_env.split(',') if o.strip()] +# Add default trusted origins for common CyberPanel domains +_default_origins = [ + 'https://cyberpanel.newstargeted.com', + 'http://cyberpanel.newstargeted.com', +] +# Merge environment and default origins, avoiding duplicates +CSRF_TRUSTED_ORIGINS = list(dict.fromkeys(_csrf_origins_list + _default_origins)) + # Application definition INSTALLED_APPS = [ diff --git a/baseTemplate/static/baseTemplate/assets/mobile-responsive.css b/baseTemplate/static/baseTemplate/assets/mobile-responsive.css new file mode 100644 index 000000000..35e2674e5 --- /dev/null +++ b/baseTemplate/static/baseTemplate/assets/mobile-responsive.css @@ -0,0 +1,589 @@ +/* CyberPanel Mobile Responsive & Readability Fixes */ +/* This file ensures all pages are mobile-friendly with proper font sizes and readable text */ + +/* Base font size and mobile-first approach */ +html { + font-size: 16px; /* Base font size for better readability */ + -webkit-text-size-adjust: 100%; + -ms-text-size-adjust: 100%; + text-size-adjust: 100%; +} + +body { + font-size: 16px; + line-height: 1.6; + color: #2f3640; /* Dark text for better readability on white backgrounds */ + font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif; +} + +/* Ensure all text is readable with proper contrast */ +* { + color: inherit; +} + +/* Override any light text that might be hard to read */ +.text-muted, .text-secondary, .text-light { + color: #2f3640 !important; /* Dark text for better readability on white backgrounds */ +} + +/* Fix small font sizes that are hard to read */ +small, .small, .text-small { + font-size: 14px !important; /* Minimum readable size */ +} + +/* Table improvements for mobile */ +.table { + font-size: 16px !important; /* Larger table text */ + width: 100%; + border-collapse: collapse; + margin-bottom: 20px; +} + +.table th, .table td { + padding: 12px 8px !important; /* More padding for touch targets */ + border: 1px solid #e8e9ff; + text-align: left; + vertical-align: middle; + font-size: 14px !important; + line-height: 1.4; +} + +.table th { + background-color: #f8f9fa; + font-weight: 600; + color: #2f3640 !important; + font-size: 15px !important; +} + +/* Button improvements for mobile */ +.btn { + font-size: 16px !important; + padding: 12px 20px !important; + border-radius: 8px; + min-height: 44px; /* Minimum touch target size */ + display: inline-flex; + align-items: center; + justify-content: center; + text-decoration: none; + border: none; + cursor: pointer; + transition: all 0.2s ease; +} + +.btn-sm { + font-size: 14px !important; + padding: 8px 16px !important; + min-height: 36px; +} + +.btn-xs { + font-size: 13px !important; + padding: 6px 12px !important; + min-height: 32px; +} + +/* Form elements */ +.form-control, input, textarea, select { + font-size: 16px !important; /* Prevents zoom on iOS */ + padding: 12px 16px !important; + border: 2px solid #e8e9ff; + border-radius: 8px; + min-height: 44px; + line-height: 1.4; + color: #2f3640 !important; + background-color: #ffffff; +} + +.form-control:focus, input:focus, textarea:focus, select:focus { + border-color: #5856d6; + box-shadow: 0 0 0 3px rgba(88, 86, 214, 0.1); + outline: none; +} + +/* Labels and form text */ +label, .control-label { + font-size: 16px !important; + font-weight: 600; + color: #2f3640 !important; + margin-bottom: 8px; + display: block; +} + +/* Headings with proper hierarchy */ +h1 { + font-size: 2.5rem !important; /* 40px */ + font-weight: 700; + color: #1e293b !important; + line-height: 1.2; + margin-bottom: 1rem; +} + +h2 { + font-size: 2rem !important; /* 32px */ + font-weight: 600; + color: #1e293b !important; + line-height: 1.3; + margin-bottom: 0.875rem; +} + +h3 { + font-size: 1.5rem !important; /* 24px */ + font-weight: 600; + color: #2f3640 !important; + line-height: 1.4; + margin-bottom: 0.75rem; +} + +h4 { + font-size: 1.25rem !important; /* 20px */ + font-weight: 600; + color: #2f3640 !important; + line-height: 1.4; + margin-bottom: 0.5rem; +} + +h5 { + font-size: 1.125rem !important; /* 18px */ + font-weight: 600; + color: #2f3640 !important; + line-height: 1.4; + margin-bottom: 0.5rem; +} + +h6 { + font-size: 1rem !important; /* 16px */ + font-weight: 600; + color: #2f3640 !important; + line-height: 1.4; + margin-bottom: 0.5rem; +} + +/* Paragraph and body text */ +p { + font-size: 16px !important; + line-height: 1.6; + color: #2f3640 !important; + margin-bottom: 1rem; +} + +/* Sidebar improvements */ +#page-sidebar { + font-size: 16px !important; +} + +#page-sidebar ul li a { + font-size: 16px !important; + padding: 12px 20px !important; + color: #2f3640 !important; + min-height: 44px; + display: flex; + align-items: center; + text-decoration: none; +} + +#page-sidebar ul li a:hover { + background-color: #f8f9fa; + color: #5856d6 !important; +} + +/* Content area improvements */ +.content-box, .panel, .card { + font-size: 16px !important; + color: #2f3640 !important; + background-color: #ffffff; + border: 1px solid #e8e9ff; + border-radius: 12px; + padding: 20px; + margin-bottom: 20px; +} + +/* Modal improvements */ +.modal-content { + font-size: 16px !important; + color: #2f3640 !important; +} + +.modal-title { + font-size: 1.5rem !important; + font-weight: 600; + color: #1e293b !important; +} + +/* Alert and notification improvements */ +.alert { + font-size: 16px !important; + padding: 16px 20px !important; + border-radius: 8px; + margin-bottom: 20px; +} + +.alert-success { + background-color: #f0fdf4; + border-color: #bbf7d0; + color: #166534 !important; +} + +.alert-danger { + background-color: #fef2f2; + border-color: #fecaca; + color: #dc2626 !important; +} + +.alert-warning { + background-color: #fffbeb; + border-color: #fed7aa; + color: #d97706 !important; +} + +.alert-info { + background-color: #eff6ff; + border-color: #bfdbfe; + color: #2563eb !important; +} + +/* Navigation improvements */ +.navbar-nav .nav-link { + font-size: 16px !important; + padding: 12px 16px !important; + color: #2f3640 !important; +} + +/* Breadcrumb improvements */ +.breadcrumb { + font-size: 16px !important; + background-color: transparent; + padding: 0; + margin-bottom: 20px; +} + +.breadcrumb-item { + color: #64748b !important; +} + +.breadcrumb-item.active { + color: #2f3640 !important; +} + +/* Mobile-first responsive breakpoints */ +@media (max-width: 1200px) { + .container, .container-fluid { + padding-left: 15px; + padding-right: 15px; + } + + .table-responsive { + border: none; + margin-bottom: 20px; + } +} + +@media (max-width: 992px) { + /* Stack columns on tablets */ + .col-md-3, .col-md-4, .col-md-6, .col-md-8, .col-md-9 { + flex: 0 0 100%; + max-width: 100%; + margin-bottom: 20px; + } + + /* Adjust sidebar for tablets */ + #page-sidebar { + width: 100%; + position: static; + height: auto; + } + + /* Make tables horizontally scrollable */ + .table-responsive { + overflow-x: auto; + } + + .table { + min-width: 600px; + } +} + +@media (max-width: 768px) { + /* Mobile-specific adjustments */ + html { + font-size: 14px; + } + + body { + font-size: 14px; + padding: 0; + } + + .container, .container-fluid { + padding-left: 10px; + padding-right: 10px; + } + + /* Stack all columns on mobile */ + .col-sm-1, .col-sm-2, .col-sm-3, .col-sm-4, .col-sm-6, .col-sm-8, .col-sm-9, .col-sm-12, + .col-md-1, .col-md-2, .col-md-3, .col-md-4, .col-md-6, .col-md-8, .col-md-9, .col-md-12 { + flex: 0 0 100%; + max-width: 100%; + margin-bottom: 15px; + } + + /* Adjust headings for mobile */ + h1 { + font-size: 2rem !important; /* 32px */ + } + + h2 { + font-size: 1.75rem !important; /* 28px */ + } + + h3 { + font-size: 1.5rem !important; /* 24px */ + } + + h4 { + font-size: 1.25rem !important; /* 20px */ + } + + /* Button adjustments for mobile */ + .btn { + font-size: 16px !important; + padding: 14px 20px !important; + width: 100%; + margin-bottom: 10px; + } + + .btn-group .btn { + width: auto; + margin-bottom: 0; + } + + /* Form adjustments for mobile */ + .form-control, input, textarea, select { + font-size: 16px !important; /* Prevents zoom on iOS */ + padding: 14px 16px !important; + width: 100%; + } + + /* Table adjustments for mobile */ + .table { + font-size: 14px !important; + } + + .table th, .table td { + padding: 8px 6px !important; + font-size: 13px !important; + } + + /* Hide less important columns on mobile */ + .table .d-none-mobile { + display: none !important; + } + + /* Modal adjustments for mobile */ + .modal-dialog { + margin: 10px; + width: calc(100% - 20px); + } + + .modal-content { + padding: 20px 15px; + } + + /* Content box adjustments */ + .content-box, .panel, .card { + padding: 15px; + margin-bottom: 15px; + } + + /* Sidebar adjustments for mobile */ + #page-sidebar { + position: fixed; + top: 0; + left: -100%; + width: 280px; + height: 100vh; + z-index: 1000; + transition: left 0.3s ease; + background-color: #ffffff; + box-shadow: 2px 0 10px rgba(0,0,0,0.1); + } + + #page-sidebar.show { + left: 0; + } + + /* Main content adjustments when sidebar is open */ + #main-content { + transition: margin-left 0.3s ease; + } + + #main-content.sidebar-open { + margin-left: 280px; + } + + /* Mobile menu toggle */ + .mobile-menu-toggle { + display: block; + position: fixed; + top: 20px; + left: 20px; + z-index: 1001; + background-color: #5856d6; + color: white; + border: none; + padding: 12px; + border-radius: 8px; + font-size: 18px; + cursor: pointer; + } +} + +@media (max-width: 576px) { + /* Extra small devices */ + html { + font-size: 14px; + } + + .container, .container-fluid { + padding-left: 8px; + padding-right: 8px; + } + + /* Even smaller buttons and forms for very small screens */ + .btn { + font-size: 14px !important; + padding: 12px 16px !important; + } + + .form-control, input, textarea, select { + font-size: 16px !important; /* Still 16px to prevent zoom */ + padding: 12px 14px !important; + } + + /* Compact table for very small screens */ + .table th, .table td { + padding: 6px 4px !important; + font-size: 12px !important; + } + + /* Hide even more columns on very small screens */ + .table .d-none-mobile-sm { + display: none !important; + } +} + +/* Utility classes for mobile */ +.d-none-mobile { + display: block; +} + +.d-none-mobile-sm { + display: block; +} + +@media (max-width: 768px) { + .d-none-mobile { + display: none !important; + } +} + +@media (max-width: 576px) { + .d-none-mobile-sm { + display: none !important; + } +} + +/* Ensure all text has proper contrast */ +.text-white { + color: #ffffff !important; +} + +.text-dark { + color: #2f3640 !important; +} + +.text-muted { + color: #2f3640 !important; /* Dark text for better readability */ +} + +/* Fix any light text on light backgrounds */ +.bg-light .text-muted, +.bg-white .text-muted, +.panel .text-muted { + color: #2f3640 !important; /* Dark text for better readability */ +} + +/* Ensure proper spacing for touch targets */ +a, button, input, select, textarea { + min-height: 44px; + min-width: 44px; +} + +/* Additional text readability improvements */ +/* Fix any green text issues */ +.ng-binding { + color: #2f3640 !important; /* Normal dark text instead of green */ +} + +/* Ensure all text elements have proper contrast */ +span, div, p, label, td, th { + color: inherit; +} + +/* Fix specific text color issues */ +.text-success { + color: #059669 !important; /* Darker green for better readability */ +} + +.text-info { + color: #0284c7 !important; /* Darker blue for better readability */ +} + +.text-warning { + color: #d97706 !important; /* Darker orange for better readability */ +} + +/* Override Bootstrap's muted text */ +.text-muted { + color: #2f3640 !important; /* Dark text instead of grey */ +} + +/* Fix any remaining light text on light backgrounds */ +.bg-white .text-light, +.bg-light .text-light, +.panel .text-light, +.card .text-light { + color: #2f3640 !important; +} + +/* Fix for small clickable elements */ +.glyph-icon, .icon { + min-width: 44px; + min-height: 44px; + display: flex; + align-items: center; + justify-content: center; +} + +/* Loading and spinner improvements */ +.spinner, .loading { + font-size: 16px !important; + color: #5856d6 !important; +} + +/* Print styles */ +@media print { + body { + font-size: 12pt; + color: #000000 !important; + background: #ffffff !important; + } + + .table th, .table td { + font-size: 10pt !important; + color: #000000 !important; + } + + .btn, .alert, .modal { + display: none !important; + } +} diff --git a/baseTemplate/static/baseTemplate/assets/readability-fixes.css b/baseTemplate/static/baseTemplate/assets/readability-fixes.css new file mode 100644 index 000000000..1911482b7 --- /dev/null +++ b/baseTemplate/static/baseTemplate/assets/readability-fixes.css @@ -0,0 +1,265 @@ +/* CyberPanel Readability & Design Fixes */ +/* This file fixes the core design issues with grey text and color inconsistencies */ + +/* Override CSS Variables for Better Text Contrast */ +:root { + /* Ensure all text uses proper dark colors for readability */ + --text-primary: #2f3640; + --text-secondary: #2f3640; /* Changed from grey to dark for better readability */ + --text-heading: #1e293b; +} + +/* Dark theme also uses proper contrast */ +[data-theme="dark"] { + --text-primary: #e4e4e7; + --text-secondary: #e4e4e7; /* Changed from grey to light for better readability */ + --text-heading: #f3f4f6; +} + +/* Fix Green Text Issues */ +/* Override Angular binding colors that might be green */ +.ng-binding { + color: var(--text-secondary) !important; +} + +/* Specific fix for uptime display */ +#sidebar .server-info .info-line span, +#sidebar .server-info .info-line .ng-binding, +.server-info .ng-binding { + color: var(--text-secondary) !important; +} + +/* Fix Grey Text on White Background */ +/* Override all muted and secondary text classes */ +.text-muted, +.text-secondary, +.text-light, +small, +.small, +.text-small { + color: var(--text-secondary) !important; +} + +/* Fix specific Bootstrap classes */ +.text-muted { + color: #2f3640 !important; /* Dark text for better readability */ +} + +/* Fix text on white/light backgrounds */ +.bg-white .text-muted, +.bg-light .text-muted, +.panel .text-muted, +.card .text-muted, +.content-box .text-muted { + color: #2f3640 !important; +} + +/* Fix menu items and navigation */ +#sidebar .menu-item, +#sidebar .menu-item span, +#sidebar .menu-item i, +.sidebar .menu-item, +.sidebar .menu-item span, +.sidebar .menu-item i { + color: var(--text-secondary) !important; +} + +#sidebar .menu-item:hover, +.sidebar .menu-item:hover { + color: var(--accent-color) !important; +} + +#sidebar .menu-item.active, +.sidebar .menu-item.active { + color: white !important; +} + +/* Fix server info and details */ +.server-info, +.server-info *, +.server-details, +.server-details *, +.info-line, +.info-line span, +.info-line strong, +.tagline, +.brand { + color: inherit !important; +} + +/* Fix form elements */ +label, +.control-label, +.form-label { + color: var(--text-primary) !important; + font-weight: 600; +} + +/* Fix table text */ +.table th, +.table td { + color: var(--text-primary) !important; +} + +.table th { + font-weight: 600; +} + +/* Fix alert text */ +.alert { + color: var(--text-primary) !important; +} + +.alert-success { + color: #059669 !important; /* Darker green for better readability */ +} + +.alert-info { + color: #0284c7 !important; /* Darker blue for better readability */ +} + +.alert-warning { + color: #d97706 !important; /* Darker orange for better readability */ +} + +.alert-danger { + color: #dc2626 !important; /* Darker red for better readability */ +} + +/* Fix breadcrumb text */ +.breadcrumb-item { + color: var(--text-secondary) !important; +} + +.breadcrumb-item.active { + color: var(--text-primary) !important; +} + +/* Fix modal text */ +.modal-content { + color: var(--text-primary) !important; +} + +.modal-title { + color: var(--text-heading) !important; +} + +/* Fix button text */ +.btn { + color: inherit; +} + +/* Fix any remaining light text issues */ +.bg-light .text-light, +.bg-white .text-light, +.panel .text-light, +.card .text-light { + color: #2f3640 !important; +} + +/* Ensure proper contrast for all text elements */ +span, div, p, label, td, th, a, li { + color: inherit; +} + +/* Fix specific color classes */ +.text-success { + color: #059669 !important; /* Darker green for better readability */ +} + +.text-info { + color: #0284c7 !important; /* Darker blue for better readability */ +} + +.text-warning { + color: #d97706 !important; /* Darker orange for better readability */ +} + +.text-danger { + color: #dc2626 !important; /* Darker red for better readability */ +} + +/* Fix any Angular-specific styling */ +[ng-controller] { + color: inherit; +} + +[ng-show], +[ng-hide], +[ng-if] { + color: inherit; +} + +/* Ensure all content areas have proper text color */ +.content-box, +.panel, +.card, +.main-content, +.page-content { + color: var(--text-primary) !important; +} + +/* Fix any remaining Bootstrap classes */ +.text-dark { + color: #2f3640 !important; +} + +.text-body { + color: var(--text-primary) !important; +} + +/* Mobile-specific fixes */ +@media (max-width: 768px) { + /* Ensure mobile text is also readable */ + body, + .container, + .container-fluid { + color: var(--text-primary) !important; + } + + /* Fix mobile menu text */ + .mobile-menu .menu-item, + .mobile-menu .menu-item span { + color: var(--text-secondary) !important; + } +} + +/* Print styles */ +@media print { + body, + .content-box, + .panel, + .card { + color: #000000 !important; + background: #ffffff !important; + } + + .text-muted, + .text-secondary { + color: #000000 !important; + } +} + +/* High contrast mode support */ +@media (prefers-contrast: high) { + :root { + --text-primary: #000000; + --text-secondary: #000000; + --text-heading: #000000; + } + + [data-theme="dark"] { + --text-primary: #ffffff; + --text-secondary: #ffffff; + --text-heading: #ffffff; + } +} + +/* Reduced motion support */ +@media (prefers-reduced-motion: reduce) { + * { + animation-duration: 0.01ms !important; + animation-iteration-count: 1 !important; + transition-duration: 0.01ms !important; + } +} diff --git a/baseTemplate/static/baseTemplate/custom-js/system-status.js b/baseTemplate/static/baseTemplate/custom-js/system-status.js index 76353b1e4..9afb9976d 100644 --- a/baseTemplate/static/baseTemplate/custom-js/system-status.js +++ b/baseTemplate/static/baseTemplate/custom-js/system-status.js @@ -1459,6 +1459,12 @@ app.controller('dashboardStatsController', function ($scope, $http, $timeout) { var pollInterval = 2000; // ms var maxPoints = 30; + // Expose so switchTab can create charts on first tab click if they weren't created at load + window.cyberPanelSetupChartsIfNeeded = function() { + if (window.trafficChart && window.diskIOChart && window.cpuChart) return; + try { setupCharts(); } catch (e) { console.error('cyberPanelSetupChartsIfNeeded:', e); } + }; + function pollDashboardStats() { console.log('[dashboardStatsController] pollDashboardStats() called'); console.log('[dashboardStatsController] Fetching dashboard stats from /base/getDashboardStats'); @@ -1517,8 +1523,8 @@ app.controller('dashboardStatsController', function ($scope, $http, $timeout) { } function pollTraffic() { - console.log('pollTraffic called'); $http.get('/base/getTrafficStats').then(function(response) { + if (!response || !response.data) return; if (response.data.admin_only) { // Hide chart for non-admin users $scope.hideSystemCharts = true; @@ -1566,13 +1572,16 @@ app.controller('dashboardStatsController', function ($scope, $http, $timeout) { } lastRx = rx; lastTx = tx; } else { - console.log('pollTraffic error or no data:', response); + console.warn('pollTraffic: no data or status', response.data); } + }).catch(function(err) { + console.warn('pollTraffic failed:', err); }); } function pollDiskIO() { $http.get('/base/getDiskIOStats').then(function(response) { + if (!response || !response.data) return; if (response.data.admin_only) { // Hide chart for non-admin users $scope.hideSystemCharts = true; @@ -1611,11 +1620,14 @@ app.controller('dashboardStatsController', function ($scope, $http, $timeout) { } lastDiskRead = read; lastDiskWrite = write; } + }).catch(function(err) { + console.warn('pollDiskIO failed:', err); }); } function pollCPU() { $http.get('/base/getCPULoadGraph').then(function(response) { + if (!response || !response.data) return; if (response.data.admin_only) { // Hide chart for non-admin users $scope.hideSystemCharts = true; @@ -1654,13 +1666,34 @@ app.controller('dashboardStatsController', function ($scope, $http, $timeout) { } lastCPUTimes = cpuTimes; } + }).catch(function(err) { + console.warn('pollCPU failed:', err); }); } - function setupCharts() { - console.log('setupCharts called, initializing charts...'); - var trafficCtx = document.getElementById('trafficChart').getContext('2d'); - trafficChart = new Chart(trafficCtx, { + function setupCharts(retryCount) { + retryCount = retryCount || 0; + if (typeof Chart === 'undefined') { + if (retryCount < 3) { + $timeout(function() { setupCharts(retryCount + 1); }, 400); + } + return; + } + var trafficEl = document.getElementById('trafficChart'); + if (!trafficEl) { + if (retryCount < 5) { + $timeout(function() { setupCharts(retryCount + 1); }, 300); + } + return; + } + try { + var trafficCtx = trafficEl.getContext('2d'); + } catch (e) { + console.error('trafficChart getContext failed:', e); + return; + } + try { + trafficChart = new Chart(trafficCtx, { type: 'line', data: { labels: [], @@ -1752,7 +1785,9 @@ app.controller('dashboardStatsController', function ($scope, $http, $timeout) { console.log('trafficChart resized and updated after setup.'); } }, 500); - var diskCtx = document.getElementById('diskIOChart').getContext('2d'); + var diskEl = document.getElementById('diskIOChart'); + if (!diskEl) { console.warn('diskIOChart canvas not found'); return; } + var diskCtx = diskEl.getContext('2d'); diskIOChart = new Chart(diskCtx, { type: 'line', data: { @@ -1837,7 +1872,10 @@ app.controller('dashboardStatsController', function ($scope, $http, $timeout) { layout: { padding: { top: 10, bottom: 10, left: 10, right: 10 } } } }); - var cpuCtx = document.getElementById('cpuChart').getContext('2d'); + window.diskIOChart = diskIOChart; + var cpuEl = document.getElementById('cpuChart'); + if (!cpuEl) { console.warn('cpuChart canvas not found'); return; } + var cpuCtx = cpuEl.getContext('2d'); cpuChart = new Chart(cpuCtx, { type: 'line', data: { @@ -1910,6 +1948,10 @@ app.controller('dashboardStatsController', function ($scope, $http, $timeout) { layout: { padding: { top: 10, bottom: 10, left: 10, right: 10 } } } }); + window.cpuChart = cpuChart; + } catch (e) { + console.error('setupCharts error:', e); + } // Redraw charts on tab shown $("a[data-toggle='tab']").on('shown.bs.tab', function (e) { @@ -1942,19 +1984,20 @@ app.controller('dashboardStatsController', function ($scope, $http, $timeout) { $scope.refreshSSHLogs(); $timeout(function() { - // Check if user is admin before setting up charts + // Always create charts so Traffic/Disk IO/CPU tabs have something to show; admin check only affects hideSystemCharts + setupCharts(); $http.get('/base/getAdminStatus').then(function(response) { - if (response.data && response.data.admin === 1) { - setupCharts(); + if (response.data && (response.data.admin === 1 || response.data.admin === true)) { + $scope.hideSystemCharts = false; } else { $scope.hideSystemCharts = true; } - }).catch(function() { - // If error, assume non-admin and hide charts + }).catch(function(err) { + console.warn('getAdminStatus failed:', err); $scope.hideSystemCharts = true; }); - // Start polling for all stats + // Start polling for all stats (data feeds charts) function pollAll() { pollDashboardStats(); pollTraffic(); @@ -1964,7 +2007,7 @@ app.controller('dashboardStatsController', function ($scope, $http, $timeout) { $timeout(pollAll, pollInterval); } pollAll(); - }, 500); + }, 800); // SSH User Activity Modal $scope.showSSHActivityModal = false; diff --git a/baseTemplate/templates/baseTemplate/homePage.html b/baseTemplate/templates/baseTemplate/homePage.html index 3c078bf0f..39f7b12a4 100644 --- a/baseTemplate/templates/baseTemplate/homePage.html +++ b/baseTemplate/templates/baseTemplate/homePage.html @@ -1357,11 +1357,25 @@ // Add active class to clicked tab tabButton.classList.add('active'); - // Trigger chart resize if switching to chart tabs + // Chart tabs: ensure charts exist (lazy init on first click), then resize/update if (tabId === 'traffic' || tabId === 'diskio' || tabId === 'cpu-usage') { + if (typeof window.cyberPanelSetupChartsIfNeeded === 'function') { + window.cyberPanelSetupChartsIfNeeded(); + } setTimeout(() => { + var ch; + if (tabId === 'traffic' && (ch = window.trafficChart) && typeof ch.resize === 'function') { + ch.resize(); + if (typeof ch.update === 'function') ch.update(); + } else if (tabId === 'diskio' && (ch = window.diskIOChart) && typeof ch.resize === 'function') { + ch.resize(); + if (typeof ch.update === 'function') ch.update(); + } else if (tabId === 'cpu-usage' && (ch = window.cpuChart) && typeof ch.resize === 'function') { + ch.resize(); + if (typeof ch.update === 'function') ch.update(); + } window.dispatchEvent(new Event('resize')); - }, 100); + }, 200); } } diff --git a/baseTemplate/templates/baseTemplate/index.html b/baseTemplate/templates/baseTemplate/index.html index 0802be080..0fe34127a 100644 --- a/baseTemplate/templates/baseTemplate/index.html +++ b/baseTemplate/templates/baseTemplate/index.html @@ -34,14 +34,13 @@ + + - - - diff --git a/firewall/firewallManager.py b/firewall/firewallManager.py index 0a50250e4..96b3c8232 100644 --- a/firewall/firewallManager.py +++ b/firewall/firewallManager.py @@ -82,7 +82,12 @@ class FirewallManager: None, 'admin') return proc.render() - def getCurrentRules(self, userID = None): + def getCurrentRules(self, userID=None, data=None): + """ + Get firewall rules with optional pagination. + data may contain: page (1-based), page_size (default 10). + Returns: fetchStatus 1, data (JSON array), total_count, page, page_size. + """ try: currentACL = ACLManager.loadedACL(userID) @@ -91,58 +96,69 @@ class FirewallManager: else: return ACLManager.loadErrorJson('fetchStatus', 0) - rules = FirewallRules.objects.all() + rules_qs = FirewallRules.objects.all().order_by('id') # Ensure CyberPanel port 7080 rule exists in database for visibility - cyberpanel_rule_exists = False - for rule in rules: - if rule.port == '7080': - cyberpanel_rule_exists = True - break - + cyberpanel_rule_exists = rules_qs.filter(port='7080').exists() if not cyberpanel_rule_exists: - # Create database entry for port 7080 (already enabled in system firewall) try: - cyberpanel_rule = FirewallRules( + FirewallRules( name="CyberPanel Admin", proto="tcp", port="7080", ipAddress="0.0.0.0/0" - ) - cyberpanel_rule.save() + ).save() logging.CyberCPLogFileWriter.writeToFile("Added CyberPanel port 7080 to firewall database for UI visibility") except Exception as e: logging.CyberCPLogFileWriter.writeToFile(f"Failed to add CyberPanel port 7080 to database: {str(e)}") + rules_qs = FirewallRules.objects.all().order_by('id') - # Refresh rules after potential creation - rules = FirewallRules.objects.all() + total_count = rules_qs.count() + page = 1 + page_size = 10 + if data: + try: + page = max(1, int(data.get('page', 1))) + except (TypeError, ValueError): + pass + try: + page_size = max(1, min(100, int(data.get('page_size', 10)))) + except (TypeError, ValueError): + pass + + start = (page - 1) * page_size + end = start + page_size + rules = list(rules_qs[start:end]) json_data = "[" - checker = 0 - - for items in rules: + for i, items in enumerate(rules): dic = { - 'id': items.id, - 'name': items.name, - 'proto': items.proto, - 'port': items.port, - 'ipAddress': items.ipAddress, - } + 'id': items.id, + 'name': items.name, + 'proto': items.proto, + 'port': items.port, + 'ipAddress': items.ipAddress, + } + if i > 0: + json_data += ',' + json_data += json.dumps(dic) + json_data += ']' - if checker == 0: - json_data = json_data + json.dumps(dic) - checker = 1 - else: - json_data = json_data + ',' + json.dumps(dic) - - json_data = json_data + ']' - final_json = json.dumps({'status': 1, 'fetchStatus': 1, 'error_message': "None", "data": json_data}) - return HttpResponse(final_json) + final_json = json.dumps({ + 'status': 1, + 'fetchStatus': 1, + 'error_message': "None", + "data": json_data, + "total_count": total_count, + "page": page, + "page_size": page_size + }) + return HttpResponse(final_json, content_type='application/json') except BaseException as msg: final_dic = {'status': 0, 'fetchStatus': 0, 'error_message': str(msg)} final_json = json.dumps(final_dic) - return HttpResponse(final_json) + return HttpResponse(final_json, content_type='application/json') def addRule(self, userID = None, data = None): try: @@ -1841,9 +1857,11 @@ class FirewallManager: final_json = json.dumps(final_dic) return HttpResponse(final_json) - def getBannedIPs(self, userID=None): + def getBannedIPs(self, userID=None, data=None): """ - Get list of banned IP addresses from database, or fall back to JSON file. + Get list of banned IP addresses with optional pagination. + data may contain: page (1-based), page_size (default 10). + Returns: status 1, bannedIPs (array), total_count, page, page_size. """ try: admin = Administrator.objects.get(pk=userID) @@ -1876,12 +1894,10 @@ class FirewallManager: if ip_data['active']: active_banned_ips.append(ip_data) except (ImportError, AttributeError) as e: - # Fall back to JSON file when BannedIP model unavailable import plogical.CyberCPLogFileWriter as _log _log.CyberCPLogFileWriter.writeToFile('getBannedIPs: using JSON fallback (%s)' % str(e)) active_banned_ips = [] - # If DB returns nothing (or model not available), merge in JSON fallback if not active_banned_ips: banned_ips, _ = self._load_banned_ips_store() for b in banned_ips: @@ -1917,7 +1933,30 @@ class FirewallManager: 'active': True }) - final_dic = {'status': 1, 'bannedIPs': active_banned_ips} + total_count = len(active_banned_ips) + page = 1 + page_size = 10 + if data: + try: + page = max(1, int(data.get('page', 1))) + except (TypeError, ValueError): + pass + try: + page_size = max(1, min(100, int(data.get('page_size', 10)))) + except (TypeError, ValueError): + pass + + start = (page - 1) * page_size + end = start + page_size + paged_list = active_banned_ips[start:end] + + final_dic = { + 'status': 1, + 'bannedIPs': paged_list, + 'total_count': total_count, + 'page': page, + 'page_size': page_size + } final_json = json.dumps(final_dic) return HttpResponse(final_json, content_type='application/json') @@ -1926,11 +1965,12 @@ class FirewallManager: logging.CyberCPLogFileWriter.writeToFile('Error in getBannedIPs: %s' % str(msg)) final_dic = {'status': 0, 'error_message': str(msg)} final_json = json.dumps(final_dic) - return HttpResponse(final_json) + return HttpResponse(final_json, content_type='application/json') def addBannedIP(self, userID=None, data=None): """ - Add a banned IP address + Add a banned IP address. Uses database (BannedIP model) as primary storage; + JSON file is used only when the model is unavailable (fallback). Export/Import use JSON format. """ try: admin = Administrator.objects.get(pk=userID) @@ -1940,7 +1980,7 @@ class FirewallManager: ip = data.get('ip', '').strip() reason = data.get('reason', '').strip() - duration = data.get('duration', '24h') + duration = (data.get('duration') or '24h').strip().lower() if not ip or not reason: final_dic = {'status': 0, 'error_message': 'IP address and reason are required', 'error': 'IP address and reason are required'} @@ -1954,66 +1994,102 @@ class FirewallManager: final_dic = {'status': 0, 'error_message': 'Invalid IP address format', 'error': 'Invalid IP address format'} return HttpResponse(json.dumps(final_dic), content_type='application/json') - # Calculate expiration time current_time = time.time() + duration_map = { + '1h': 3600, + '24h': 86400, + '7d': 604800, + '30d': 2592000 + } if duration == 'permanent': - expires = 'Never' + expires_ts = None # Never expires else: - duration_map = { - '1h': 3600, - '24h': 86400, - '7d': 604800, - '30d': 2592000 - } duration_seconds = duration_map.get(duration, 86400) - expires = current_time + duration_seconds + expires_ts = int(current_time) + duration_seconds - # Load existing banned IPs - banned_ips, _ = self._load_banned_ips_store() + # Prefer database (BannedIP model) for primary storage + try: + from firewall.models import BannedIP + except Exception as e: + BannedIP = None + logging.CyberCPLogFileWriter.writeToFile('addBannedIP: BannedIP model unavailable, using JSON fallback: %s' % str(e)) - # Check if IP is already banned - for banned_ip in banned_ips: - if banned_ip.get('ip') == ip and banned_ip.get('active', True): + if BannedIP is not None: + # Primary path: save to database + existing = BannedIP.objects.filter(ip_address=ip, active=True).first() + if existing: msg = 'IP address %s is already banned' % ip final_dic = {'status': 0, 'error_message': msg, 'error': msg} return HttpResponse(json.dumps(final_dic), content_type='application/json') + try: + new_ban = BannedIP( + ip_address=ip, + reason=reason, + duration=duration, + expires=expires_ts, + active=True + ) + new_ban.save() + except Exception as e: + logging.CyberCPLogFileWriter.writeToFile('addBannedIP: failed to save to DB: %s' % str(e)) + final_dic = {'status': 0, 'error_message': 'Failed to save banned IP to database: %s' % str(e), 'error': str(e)} + return HttpResponse(json.dumps(final_dic), content_type='application/json') + else: + # Fallback: JSON store (only when DB unavailable) + banned_ips, _ = self._load_banned_ips_store() + for banned_ip in banned_ips: + if banned_ip.get('ip') == ip and banned_ip.get('active', True): + msg = 'IP address %s is already banned' % ip + final_dic = {'status': 0, 'error_message': msg, 'error': msg} + return HttpResponse(json.dumps(final_dic), content_type='application/json') + new_banned_ip = { + 'id': int(current_time), + 'ip': ip, + 'reason': reason, + 'duration': duration, + 'banned_on': current_time, + 'expires': 'Never' if expires_ts is None else expires_ts, + 'active': True + } + banned_ips.append(new_banned_ip) + try: + self._save_banned_ips_store(banned_ips) + except Exception as e: + logging.CyberCPLogFileWriter.writeToFile('addBannedIP: failed to save JSON store: %s' % str(e)) + final_dic = {'status': 0, 'error_message': 'Failed to save banned IP: %s' % str(e), 'error': str(e)} + return HttpResponse(json.dumps(final_dic), content_type='application/json') - # Add new banned IP - new_banned_ip = { - 'id': int(time.time()), - 'ip': ip, - 'reason': reason, - 'duration': duration, - 'banned_on': current_time, - 'expires': expires, - 'active': True - } - banned_ips.append(new_banned_ip) - - # Save to file - self._save_banned_ips_store(banned_ips) - - # Apply firewall rule using FirewallUtilities (runs with proper privileges via ProcessUtilities/lscpd) + # Apply firewall rule (same for DB and JSON path) try: block_ok, block_msg = FirewallUtilities.blockIP(ip, reason) if not block_ok: - # Rollback: remove the IP we just added from the store - banned_ips_rollback = [b for b in banned_ips if b.get('ip') != ip or not b.get('active', True)] - if len(banned_ips_rollback) < len(banned_ips): - self._save_banned_ips_store(banned_ips_rollback) + if BannedIP is not None: + try: + BannedIP.objects.filter(ip_address=ip, active=True).delete() + except Exception: + pass + else: + banned_ips_rollback = [b for b in banned_ips if b.get('ip') != ip or not b.get('active', True)] + if len(banned_ips_rollback) < len(banned_ips): + self._save_banned_ips_store(banned_ips_rollback) logging.CyberCPLogFileWriter.writeToFile('Failed to add firewalld rule for %s: %s' % (ip, block_msg)) err_msg = block_msg or 'Failed to add firewall rule' final_dic = {'status': 0, 'error_message': err_msg, 'error': err_msg} return HttpResponse(json.dumps(final_dic), content_type='application/json') - logging.CyberCPLogFileWriter.writeToFile(f'Banned IP {ip} with reason: {reason}') + logging.CyberCPLogFileWriter.writeToFile('Banned IP %s with reason: %s' % (ip, reason)) except Exception as e: - # Rollback store on any exception - try: - banned_ips_rollback = [b for b in banned_ips if b.get('ip') != ip or not b.get('active', True)] - if len(banned_ips_rollback) < len(banned_ips): - self._save_banned_ips_store(banned_ips_rollback) - except Exception: - pass + if BannedIP is not None: + try: + BannedIP.objects.filter(ip_address=ip, active=True).delete() + except Exception: + pass + else: + try: + banned_ips_rollback = [b for b in banned_ips if b.get('ip') != ip or not b.get('active', True)] + if len(banned_ips_rollback) < len(banned_ips): + self._save_banned_ips_store(banned_ips_rollback) + except Exception: + pass logging.CyberCPLogFileWriter.writeToFile('Failed to add firewalld rule for %s: %s' % (ip, str(e))) err_msg = 'Firewall command failed: %s' % str(e) final_dic = {'status': 0, 'error_message': err_msg, 'error': err_msg} diff --git a/firewall/migrations/0001_initial.py b/firewall/migrations/0001_initial.py new file mode 100644 index 000000000..bf7236d6f --- /dev/null +++ b/firewall/migrations/0001_initial.py @@ -0,0 +1,40 @@ +# Generated migration for firewall app - BannedIP model +# Primary storage for banned IPs is the database; JSON is used only for export/import. + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + ] + + operations = [ + migrations.CreateModel( + name='BannedIP', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('ip_address', models.GenericIPAddressField(db_index=True, unique=True, verbose_name='IP Address')), + ('reason', models.CharField(max_length=255, verbose_name='Ban Reason')), + ('duration', models.CharField(default='permanent', max_length=50, verbose_name='Duration')), + ('banned_on', models.DateTimeField(auto_now_add=True, verbose_name='Banned On')), + ('expires', models.BigIntegerField(blank=True, null=True, verbose_name='Expires Timestamp')), + ('active', models.BooleanField(db_index=True, default=True, verbose_name='Active')), + ], + options={ + 'verbose_name': 'Banned IP', + 'verbose_name_plural': 'Banned IPs', + 'db_table': 'firewall_bannedips', + }, + ), + migrations.AddIndex( + model_name='bannedip', + index=models.Index(fields=['ip_address', 'active'], name='fw_bannedip_ip_active_idx'), + ), + migrations.AddIndex( + model_name='bannedip', + index=models.Index(fields=['active', 'expires'], name='fw_bannedip_active_exp_idx'), + ), + ] diff --git a/firewall/static/firewall/firewall.js b/firewall/static/firewall/firewall.js index 4a4c4aeaf..e5acb30fe 100644 --- a/firewall/static/firewall/firewall.js +++ b/firewall/static/firewall/firewall.js @@ -31,9 +31,61 @@ app.controller('firewallController', function ($scope, $http, $timeout) { $scope.couldNotConnect = true; $scope.rulesDetails = false; - // Banned IPs variables - $scope.activeTab = 'rules'; + // Banned IPs variables – tab from hash so we stay on /firewall/ (avoids 404 on servers without /firewall/firewall-rules/) + function tabFromHash() { + var h = (window.location.hash || '').replace(/^#/, ''); + return (h === 'banned-ips') ? 'banned' : 'rules'; + } + $scope.activeTab = tabFromHash(); $scope.bannedIPs = []; // Initialize as empty array + + // Re-apply tab from hash after load (hash can be set after controller init in some browsers) + function applyTabFromHash() { + var tab = tabFromHash(); + if ($scope.activeTab !== tab) { + $scope.activeTab = tab; + if (tab === 'banned') { populateBannedIPs(); } else { populateCurrentRecords(); } + if (!$scope.$$phase && !$scope.$root.$$phase) { $scope.$apply(); } + } + } + $timeout(applyTabFromHash, 0); + if (document.readyState === 'complete') { + $timeout(applyTabFromHash, 50); + } else { + window.addEventListener('load', function() { $timeout(applyTabFromHash, 0); }); + } + + // Sync tab with hash and load that tab's data on switch + $scope.setFirewallTab = function(tab) { + $timeout(function() { + $scope.activeTab = tab; + window.location.hash = (tab === 'banned') ? '#banned-ips' : '#rules'; + if (tab === 'banned') { populateBannedIPs(); } else { populateCurrentRecords(); } + }, 0); + }; + + // Back/forward or direct hash change: sync tab and load its data + function syncTabFromHash() { + var tab = tabFromHash(); + if ($scope.activeTab !== tab) { + $scope.activeTab = tab; + if (tab === 'banned') { populateBannedIPs(); } else { populateCurrentRecords(); } + if (!$scope.$$phase && !$scope.$root.$$phase) { $scope.$apply(); } + } + } + window.addEventListener('hashchange', syncTabFromHash); + + // Pagination: Firewall Rules (default 10 per page, options 5–100) + $scope.rulesPage = 1; + $scope.rulesPageSize = 10; + $scope.rulesPageSizeOptions = [5, 10, 20, 30, 50, 100]; + $scope.rulesTotalCount = 0; + + // Pagination: Banned IPs + $scope.bannedPage = 1; + $scope.bannedPageSize = 10; + $scope.bannedPageSizeOptions = [5, 10, 20, 30, 50, 100]; + $scope.bannedTotalCount = 0; // Initialize banned IPs array - start as null so template shows empty state // Will be set to array after API call @@ -47,9 +99,21 @@ app.controller('firewallController', function ($scope, $http, $timeout) { firewallStatus(); + // Load both tabs on init; also load on tab change (watch) so content always shows populateCurrentRecords(); - - // Load banned IPs immediately when controller initializes + populateBannedIPs(); + + $scope.$watch('activeTab', function(newVal, oldVal) { + if (newVal === oldVal || !newVal) return; + $timeout(function() { + try { + if (newVal === 'banned' && typeof populateBannedIPs === 'function') populateBannedIPs(); + else if (newVal === 'rules' && typeof populateCurrentRecords === 'function') populateCurrentRecords(); + } catch (e) {} + }, 0); + }); + + // Log for debugging console.log('=== FIREWALL CONTROLLER INITIALIZING ==='); console.log('Initializing firewall controller, loading banned IPs...'); @@ -69,14 +133,19 @@ app.controller('firewallController', function ($scope, $http, $timeout) { } }; - console.log('Making request to:', url); + var postData = { + page: $scope.bannedPage || 1, + page_size: $scope.bannedPageSize || 10 + }; + console.log('Making request to:', url, 'page:', postData.page, 'page_size:', postData.page_size); console.log('CSRF Token:', csrfToken ? 'Found (' + csrfToken.substring(0, 10) + '...)' : 'MISSING!'); - $http.post(url, {}, config).then( + $http.post(url, postData, config).then( function(response) { + var res = (typeof response.data === 'string') ? (function() { try { return JSON.parse(response.data); } catch (e) { return {}; } })() : response.data; console.log('=== API RESPONSE RECEIVED ==='); console.log('Response status:', response.status); - console.log('Response data:', JSON.stringify(response.data, null, 2)); + console.log('Response data (parsed):', res); $scope.bannedIPsLoading = false; // Reset error flags @@ -84,8 +153,8 @@ app.controller('firewallController', function ($scope, $http, $timeout) { $scope.bannedIPActionSuccess = true; $scope.bannedIPCouldNotConnect = true; - if (response.data && response.data.status === 1) { - var bannedIPsArray = response.data.bannedIPs || []; + if (res && res.status === 1) { + var bannedIPsArray = res.bannedIPs || []; console.log('Raw bannedIPs from API:', bannedIPsArray); console.log('Banned IPs count:', bannedIPsArray.length); console.log('Is array?', Array.isArray(bannedIPsArray)); @@ -99,6 +168,9 @@ app.controller('firewallController', function ($scope, $http, $timeout) { // Assign to scope - Angular $http callbacks already run within $apply console.log('Assigning to scope.bannedIPs...'); $scope.bannedIPs = bannedIPsArray; + $scope.bannedTotalCount = res.total_count != null ? res.total_count : bannedIPsArray.length; + $scope.bannedPage = Math.max(1, res.page != null ? res.page : 1); + $scope.bannedPageSize = res.page_size != null ? res.page_size : 10; console.log('After assignment - scope.bannedIPs:', $scope.bannedIPs); console.log('After assignment - scope.bannedIPs.length:', $scope.bannedIPs ? $scope.bannedIPs.length : 'undefined'); console.log('After assignment - activeTab:', $scope.activeTab); @@ -109,10 +181,10 @@ app.controller('firewallController', function ($scope, $http, $timeout) { console.log('=== populateBannedIPs() SUCCESS ==='); } else { console.error('ERROR: API returned status !== 1'); - console.error('Response data:', response.data); + console.error('Response data:', res); $scope.bannedIPs = []; $scope.bannedIPActionFailed = false; - $scope.bannedIPErrorMessage = (response.data && response.data.error_message) || 'Unknown error'; + $scope.bannedIPErrorMessage = (res && res.error_message) || 'Unknown error'; } }, function(error) { @@ -144,6 +216,52 @@ app.controller('firewallController', function ($scope, $http, $timeout) { console.log('$scope.populateBannedIPs() called from template'); populateBannedIPs(); }; + + $scope.goToBannedPage = function(page) { + var totalP = $scope.bannedTotalPages(); + if (page < 1 || page > totalP) return; + $scope.bannedPage = page; + populateBannedIPs(); + }; + $scope.goToBannedPageByInput = function() { + var n = parseInt($scope.bannedPageInput, 10); + if (isNaN(n) || n < 1) n = 1; + var maxP = $scope.bannedTotalPages(); + if (n > maxP) n = maxP; + $scope.bannedPageInput = n; + $scope.goToBannedPage(n); + }; + $scope.bannedTotalPages = function() { + var size = $scope.bannedPageSize || 10; + var total = $scope.bannedTotalCount || ($scope.bannedIPs ? $scope.bannedIPs.length : 0) || 0; + return size > 0 ? Math.max(1, Math.ceil(total / size)) : 1; + }; + $scope.bannedRangeStart = function() { + var total = $scope.bannedTotalCount || ($scope.bannedIPs ? $scope.bannedIPs.length : 0) || 0; + if (total === 0) return 0; + var page = Math.max(1, $scope.bannedPage || 1); + var size = $scope.bannedPageSize || 10; + return (page - 1) * size + 1; + }; + $scope.bannedRangeEnd = function() { + var start = $scope.bannedRangeStart(); + var size = $scope.bannedPageSize || 10; + var total = $scope.bannedTotalCount || ($scope.bannedIPs ? $scope.bannedIPs.length : 0) || 0; + return total === 0 ? 0 : Math.min(start + size - 1, total); + }; + $scope.setBannedPageSize = function() { + $scope.bannedPage = 1; + populateBannedIPs(); + }; + + if (typeof window !== 'undefined') { + window.__firewallLoadTab = function(tab) { + $scope.$evalAsync(function() { + $scope.activeTab = tab; + if (tab === 'banned') { populateBannedIPs(); } else { populateCurrentRecords(); } + }); + }; + } // Load banned IPs on page load - use $timeout for Angular compatibility // Wrap in try-catch to ensure it executes even if there are other errors @@ -160,33 +278,6 @@ app.controller('firewallController', function ($scope, $http, $timeout) { console.error('Error setting up timeout for populateBannedIPs:', e); } - // Also load when switching to banned tab - use deep watch for immediate trigger - try { - $scope.$watch('activeTab', function(newVal, oldVal) { - console.log('=== activeTab WATCH TRIGGERED ==='); - console.log('activeTab changed from', oldVal, 'to', newVal); - if (newVal === 'banned') { - console.log('Switched to banned IPs tab, calling populateBannedIPs...'); - // Call immediately - try { - if (typeof populateBannedIPs === 'function') { - console.log('Calling populateBannedIPs from $watch...'); - populateBannedIPs(); - } else if (typeof $scope.populateBannedIPs === 'function') { - console.log('Calling $scope.populateBannedIPs from $watch...'); - $scope.populateBannedIPs(); - } else { - console.error('ERROR: populateBannedIPs is not available!'); - } - } catch(e) { - console.error('Error calling populateBannedIPs from watch:', e); - } - } - }, true); // Use deep watch (true parameter) - } catch(e) { - console.error('Error setting up $watch for activeTab:', e); - } - $scope.addRule = function () { $scope.rulesLoading = false; @@ -278,39 +369,76 @@ app.controller('firewallController', function ($scope, $http, $timeout) { $scope.actionFailed = true; $scope.actionSuccess = true; - url = "/firewall/getCurrentRules"; - - var data = {}; - + var data = { + page: $scope.rulesPage || 1, + page_size: $scope.rulesPageSize || 10 + }; var config = { headers: { 'X-CSRFToken': getCookie('csrftoken') } }; - $http.post(url, data, config).then(ListInitialDatas, cantLoadInitialDatas); - function ListInitialDatas(response) { - if (response.data.fetchStatus === 1) { - $scope.rules = JSON.parse(response.data.data); + var res = (typeof response.data === 'string') ? (function() { try { return JSON.parse(response.data); } catch (e) { return {}; } })() : response.data; + if (res && res.fetchStatus === 1) { + $scope.rules = typeof res.data === 'string' ? JSON.parse(res.data) : (res.data || []); + $scope.rulesTotalCount = res.total_count != null ? res.total_count : ($scope.rules ? $scope.rules.length : 0); + $scope.rulesPage = Math.max(1, res.page != null ? res.page : 1); + $scope.rulesPageSize = res.page_size != null ? res.page_size : 10; $scope.rulesLoading = true; } else { $scope.rulesLoading = true; - $scope.errorMessage = response.data.error_message; + $scope.errorMessage = (res && res.error_message) ? res.error_message : ''; } } function cantLoadInitialDatas(response) { $scope.couldNotConnect = false; - } - } + $scope.goToRulesPage = function(page) { + var totalP = $scope.rulesTotalPages(); + if (page < 1 || page > totalP) return; + $scope.rulesPage = page; + populateCurrentRecords(); + }; + $scope.goToRulesPageByInput = function() { + var n = parseInt($scope.rulesPageInput, 10); + if (isNaN(n) || n < 1) n = 1; + var maxP = $scope.rulesTotalPages(); + if (n > maxP) n = maxP; + $scope.rulesPageInput = n; + $scope.goToRulesPage(n); + }; + $scope.rulesTotalPages = function() { + var size = $scope.rulesPageSize || 10; + var total = $scope.rulesTotalCount || ($scope.rules && $scope.rules.length) || 0; + return size > 0 ? Math.max(1, Math.ceil(total / size)) : 1; + }; + $scope.rulesRangeStart = function() { + var total = $scope.rulesTotalCount || ($scope.rules && $scope.rules.length) || 0; + if (total === 0) return 0; + var page = Math.max(1, $scope.rulesPage || 1); + var size = $scope.rulesPageSize || 10; + return (page - 1) * size + 1; + }; + $scope.rulesRangeEnd = function() { + var start = $scope.rulesRangeStart(); + var size = $scope.rulesPageSize || 10; + var total = $scope.rulesTotalCount || ($scope.rules && $scope.rules.length) || 0; + return total === 0 ? 0 : Math.min(start + size - 1, total); + }; + $scope.setRulesPageSize = function() { + $scope.rulesPage = 1; + populateCurrentRecords(); + }; + $scope.deleteRule = function (id, proto, port, ruleIP) { $scope.rulesLoading = false; @@ -2837,4 +2965,26 @@ app.controller('litespeed_ent_conf', function ($scope, $http, $timeout, $window) } } -}); \ No newline at end of file +}); + +(function() { + // Do not capture tab clicks – let Angular ng-click run setFirewallTab() so data loads. + // Only sync tab from hash on load and hashchange (back/forward) via __firewallLoadTab. + function syncFirewallTabFromHash() { + var nav = document.getElementById('firewall-tab-nav'); + if (!nav) return; + var h = (window.location.hash || '').replace(/^#/, ''); + var tab = (h === 'banned-ips') ? 'banned' : 'rules'; + if (window.__firewallLoadTab) { + try { window.__firewallLoadTab(tab); } catch (e) {} + } + } + + if (document.readyState === 'loading') { + document.addEventListener('DOMContentLoaded', syncFirewallTabFromHash); + } else { + syncFirewallTabFromHash(); + } + setTimeout(syncFirewallTabFromHash, 100); + window.addEventListener('hashchange', syncFirewallTabFromHash); +})(); \ No newline at end of file diff --git a/firewall/templates/firewall/firewall.html b/firewall/templates/firewall/firewall.html index 6a500128c..981fa5727 100644 --- a/firewall/templates/firewall/firewall.html +++ b/firewall/templates/firewall/firewall.html @@ -207,6 +207,8 @@ /* Rules Panel */ .rules-panel { + position: relative; + z-index: 1; background: var(--bg-secondary, white); border-radius: 16px; box-shadow: 0 1px 3px rgba(0,0,0,0.05), 0 10px 40px rgba(0,0,0,0.08); @@ -565,6 +567,121 @@ 100% { transform: rotate(360deg); } } + /* Pagination */ + .pagination-bar { + display: flex; + align-items: center; + justify-content: space-between; + flex-wrap: wrap; + gap: 1rem; + padding: 1rem 2rem; + border-top: 1px solid var(--border-color, #e8e9ff); + background: var(--bg-tertiary, #f8f9ff); + font-size: 0.875rem; + color: var(--text-secondary, #64748b); + } + .pagination-info { + display: flex; + align-items: center; + gap: 0.5rem; + } + .pagination-controls { + display: flex; + align-items: center; + gap: 0.5rem; + } + .pagination-controls button { + padding: 0.4rem 0.75rem; + border-radius: 6px; + border: 1px solid var(--border-color, #e2e8f0); + background: var(--bg-secondary, white); + color: var(--text-primary, #1e293b); + font-weight: 500; + cursor: pointer; + transition: all 0.2s ease; + } + .pagination-controls button:hover:not(:disabled) { + background: var(--firewall-accent, #ef4444); + color: white; + border-color: var(--firewall-accent, #ef4444); + } + .pagination-controls button:disabled { + opacity: 0.5; + cursor: not-allowed; + } + .pagination-controls .page-num { + min-width: 2rem; + text-align: center; + font-weight: 600; + color: var(--firewall-accent, #ef4444); + } + .pagination-size { + display: inline-flex; + align-items: center; + gap: 0.35rem; + margin-left: 1rem; + } + .pagination-size select { + min-width: 4rem; + } + .pagination-size-btns { + display: inline-flex; + align-items: center; + gap: 0.2rem; + flex-wrap: wrap; + } + .pagination-size-btn { + min-width: 2rem; + padding: 0.35rem 0.5rem; + border: 1px solid var(--border-color, #e2e8f0); + border-radius: 4px; + background: var(--bg-secondary, white); + font-size: 0.8rem; + font-weight: 500; + cursor: pointer; + } + .pagination-size-btn:hover { + border-color: var(--firewall-accent, #ef4444); + color: var(--firewall-accent, #ef4444); + } + .pagination-size-btn.active { + background: var(--firewall-accent, #ef4444); + color: white; + border-color: var(--firewall-accent, #ef4444); + } + .pagination-goto { + display: inline-flex; + align-items: center; + gap: 0.35rem; + } + .pagination-goto-label { + font-size: 0.8rem; + color: var(--text-secondary, #64748b); + white-space: nowrap; + } + .pagination-goto-input { + width: 3.5rem; + padding: 0.35rem 0.5rem; + border: 1px solid var(--border-color, #e2e8f0); + border-radius: 4px; + font-size: 0.875rem; + text-align: center; + } + .pagination-goto-btn { + padding: 0.35rem 0.6rem; + border-radius: 4px; + border: 1px solid var(--border-color, #e2e8f0); + background: var(--bg-secondary, white); + font-size: 0.8rem; + font-weight: 500; + cursor: pointer; + } + .pagination-goto-btn:hover { + background: var(--firewall-accent, #ef4444); + color: white; + border-color: var(--firewall-accent, #ef4444); + } + /* Responsive */ @media (max-width: 1024px) { .rule-form { @@ -606,7 +723,7 @@ } } - /* Tab Navigation Styles */ + /* Tab Navigation – always on top, clearly clickable */ .tab-navigation { display: flex; background: var(--bg-secondary, white); @@ -615,6 +732,9 @@ margin-bottom: 2rem; box-shadow: 0 2px 8px var(--shadow-light, rgba(0,0,0,0.1)); border: 1px solid var(--border-color, #e8e9ff); + position: relative; + z-index: 100; + isolation: isolate; } .tab-button { @@ -627,11 +747,16 @@ font-weight: 500; font-size: 0.9rem; cursor: pointer; - transition: all 0.3s ease; + pointer-events: auto; + transition: all 0.2s ease; display: flex; align-items: center; justify-content: center; gap: 0.5rem; + text-decoration: none; + font-family: inherit; + -webkit-appearance: none; + appearance: none; } .tab-button:hover { @@ -639,6 +764,11 @@ color: var(--accent-color, #5b5fcf); } + .tab-button:focus { + outline: 2px solid var(--accent-color, #5b5fcf); + outline-offset: 2px; + } + .tab-button.tab-active { background: var(--accent-color, #5b5fcf); color: var(--bg-secondary, white); @@ -647,10 +777,13 @@ .tab-button i { font-size: 1rem; + pointer-events: none; } - /* Banned IPs Panel Styles */ + /* Banned IPs Panel – below tab bar (z-index) */ .banned-ips-panel { + position: relative; + z-index: 1; background: var(--bg-secondary, white); border-radius: 16px; box-shadow: 0 4px 12px var(--shadow-medium, rgba(0,0,0,0.15)); @@ -1068,26 +1201,20 @@ - -