From 4177f0023bc94e7c2eae8af99e782a5a34c24dd6 Mon Sep 17 00:00:00 2001 From: master3395 Date: Sat, 14 Feb 2026 23:02:47 +0100 Subject: [PATCH] Misc: firewall, pluginHolder, mobile CSS, install utilities, static assets Co-authored-by: Cursor --- CyberCP/secMiddleware.py | 2 +- CyberCP/settings.py | 32 +- .../baseTemplate/assets/mobile-responsive.css | 589 ++++++++++++++++ .../baseTemplate/assets/readability-fixes.css | 265 ++++++++ .../baseTemplate/custom-js/system-status.js | 73 +- .../templates/baseTemplate/homePage.html | 18 +- .../templates/baseTemplate/index.html | 5 +- firewall/firewallManager.py | 242 ++++--- firewall/migrations/0001_initial.py | 40 ++ firewall/static/firewall/firewall.js | 250 +++++-- firewall/templates/firewall/firewall.html | 289 +++++++- firewall/urls.py | 6 +- firewall/views.py | 28 +- plogical/installUtilities.py | 123 ++-- pluginHolder/patreon_verifier.py | 4 +- .../templates/pluginHolder/plugins.html | 64 +- pluginHolder/views.py | 17 + .../baseTemplate/assets/mobile-responsive.css | 589 ++++++++++++++++ .../baseTemplate/assets/readability-fixes.css | 265 ++++++++ .../baseTemplate/custom-js/system-status.js | 626 +++++++++++------- static/filemanager/js/fileManager.js | 16 +- static/filemanager/js/newFileManager.js | 8 + static/firewall/firewall.js | 201 +++++- static/ftp/ftp.js | 15 +- static/mailServer/emailLimitsController.js | 4 +- static/mailServer/mailServer.js | 6 +- static/serverStatus/serverStatus.js | 5 +- 27 files changed, 3264 insertions(+), 518 deletions(-) create mode 100644 baseTemplate/static/baseTemplate/assets/mobile-responsive.css create mode 100644 baseTemplate/static/baseTemplate/assets/readability-fixes.css create mode 100644 firewall/migrations/0001_initial.py create mode 100644 static/baseTemplate/assets/mobile-responsive.css create mode 100644 static/baseTemplate/assets/readability-fixes.css 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 @@ - -
- -
- -
+ +
@@ -1206,6 +1333,33 @@
+ +
+
+ {% trans "Showing" %} {$ rulesRangeStart() || 0 $} – {$ rulesRangeEnd() || 0 $} {% trans "of" %} {$ rulesTotalCount || rules.length || 0 $} + + {% trans "Per page:" %} + + + + +
+
+ + {$ rulesPage $} / {$ rulesTotalPages() $} + + + + + + +
+
+
@@ -1234,7 +1388,7 @@
-
+
@@ -1376,6 +1530,33 @@
+ +
+
+ {% trans "Showing" %} {$ bannedRangeStart() || 0 $} – {$ bannedRangeEnd() || 0 $} {% trans "of" %} {$ bannedTotalCount || bannedIPs.length || 0 $} + + {% trans "Per page:" %} + + + + +
+
+ + {$ bannedPage $} / {$ bannedTotalPages() $} + + + + + + +
+
+
@@ -1443,4 +1624,80 @@
+ + {% endblock %} \ No newline at end of file diff --git a/firewall/urls.py b/firewall/urls.py index e60e00369..91d53103f 100644 --- a/firewall/urls.py +++ b/firewall/urls.py @@ -3,7 +3,11 @@ from . import views urlpatterns = [ path('securityHome', views.securityHome, name='securityHome'), - path('', views.firewallHome, name='firewallHome'), + path('firewall-rules/', views.firewallHome, name='firewallRules'), + path('firewall-rules', views.firewallHome, name='firewallRulesNoSlash'), + path('banned-ips/', views.firewallHome, name='firewallBannedIPs'), + path('banned-ips', views.firewallHome, name='firewallBannedIPsNoSlash'), + path('', views.firewallHome, name='firewallHome'), # /firewall/ also serves the page so 404 is avoided path('getCurrentRules', views.getCurrentRules, name='getCurrentRules'), path('addRule', views.addRule, name='addRule'), path('modifyRule', views.modifyRule, name='modifyRule'), diff --git a/firewall/views.py b/firewall/views.py index 5e42bcf96..20ddd8b7f 100644 --- a/firewall/views.py +++ b/firewall/views.py @@ -18,6 +18,16 @@ def securityHome(request): return redirect(loadLoginPage) +def firewallRedirect(request): + """Redirect /firewall/ to /firewall/firewall-rules/ so the default tab has a clear URL.""" + try: + if request.session.get('userID'): + return redirect('/firewall/firewall-rules/') + return redirect(loadLoginPage) + except Exception: + return redirect(loadLoginPage) + + def firewallHome(request): try: userID = request.session['userID'] @@ -41,7 +51,14 @@ def getCurrentRules(request): try: userID = request.session['userID'] fm = FirewallManager() - return fm.getCurrentRules(userID) + try: + body = request.body + if isinstance(body, bytes): + body = body.decode('utf-8') + data = json.loads(body) if body and body.strip() else {} + except (json.JSONDecodeError, Exception): + data = {} + return fm.getCurrentRules(userID, data) except KeyError: return redirect(loadLoginPage) @@ -663,7 +680,14 @@ def getBannedIPs(request): try: userID = request.session['userID'] fm = FirewallManager() - return fm.getBannedIPs(userID) + try: + body = request.body + if isinstance(body, bytes): + body = body.decode('utf-8') + data = json.loads(body) if body and body.strip() else {} + except (json.JSONDecodeError, Exception): + data = {} + return fm.getBannedIPs(userID, data) except KeyError: return redirect(loadLoginPage) diff --git a/plogical/installUtilities.py b/plogical/installUtilities.py index fb3aaeba4..c5486296d 100644 --- a/plogical/installUtilities.py +++ b/plogical/installUtilities.py @@ -1,6 +1,6 @@ import subprocess import sys -from plogical import CyberCPLogFileWriter as logging +from plogical.CyberCPLogFileWriter import CyberCPLogFileWriter import shutil import pexpect import os @@ -221,7 +221,7 @@ class installUtilities: return 1 @staticmethod - def safeModifyHttpdConfig(config_modifier, description="config modification"): + def safeModifyHttpdConfig(config_modifier, description="config modification", skip_validation=False): """ Safely modify httpd_config.conf with backup, validation, and rollback on failure. Prevents corrupted configs that cause OpenLiteSpeed to fail binding ports 80/443. @@ -237,20 +237,30 @@ class installUtilities: """ config_file = "/usr/local/lsws/conf/httpd_config.conf" - if not os.path.exists(config_file): - error_msg = f"Config file not found: {config_file}" - logging.writeToFile(f"[safeModifyHttpdConfig] {error_msg}") - return False, error_msg + # Check file existence using ProcessUtilities (handles permissions correctly) + try: + command = 'test -f {} && echo exists || echo notfound'.format(config_file) + result = ProcessUtilities.outputExecutioner(command).strip() + if result == 'notfound': + error_msg = f"Config file not found: {config_file}" + CyberCPLogFileWriter.writeToFile(f"[safeModifyHttpdConfig] {error_msg}") + return False, error_msg + except Exception as e: + # Fallback to os.path.exists if ProcessUtilities fails + if not os.path.exists(config_file): + error_msg = f"Config file not found: {config_file} (check failed: {str(e)})" + CyberCPLogFileWriter.writeToFile(f"[safeModifyHttpdConfig] {error_msg}") + return False, error_msg # Create backup with timestamp try: timestamp = datetime.now().strftime("%Y%m%d-%H%M%S") backup_file = f"{config_file}.backup-{timestamp}" shutil.copy2(config_file, backup_file) - logging.writeToFile(f"[safeModifyHttpdConfig] Created backup: {backup_file} for {description}") + CyberCPLogFileWriter.writeToFile(f"[safeModifyHttpdConfig] Created backup: {backup_file} for {description}") except Exception as e: error_msg = f"Failed to create backup: {str(e)}" - logging.writeToFile(f"[safeModifyHttpdConfig] {error_msg}") + CyberCPLogFileWriter.writeToFile(f"[safeModifyHttpdConfig] {error_msg}") return False, error_msg # Read current config @@ -259,7 +269,7 @@ class installUtilities: original_content = f.readlines() except Exception as e: error_msg = f"Failed to read config file: {str(e)}" - logging.writeToFile(f"[safeModifyHttpdConfig] {error_msg}") + CyberCPLogFileWriter.writeToFile(f"[safeModifyHttpdConfig] {error_msg}") return False, error_msg # Modify config using callback @@ -267,11 +277,11 @@ class installUtilities: modified_content = config_modifier(original_content) if not isinstance(modified_content, list): error_msg = "Config modifier must return a list of lines" - logging.writeToFile(f"[safeModifyHttpdConfig] {error_msg}") + CyberCPLogFileWriter.writeToFile(f"[safeModifyHttpdConfig] {error_msg}") return False, error_msg except Exception as e: error_msg = f"Config modifier function failed: {str(e)}" - logging.writeToFile(f"[safeModifyHttpdConfig] {error_msg}") + CyberCPLogFileWriter.writeToFile(f"[safeModifyHttpdConfig] {error_msg}") return False, error_msg # Write modified config @@ -280,57 +290,68 @@ class installUtilities: f.writelines(modified_content) except Exception as e: error_msg = f"Failed to write modified config: {str(e)}" - logging.writeToFile(f"[safeModifyHttpdConfig] {error_msg}") + CyberCPLogFileWriter.writeToFile(f"[safeModifyHttpdConfig] {error_msg}") # Restore backup try: shutil.copy2(backup_file, config_file) - logging.writeToFile(f"[safeModifyHttpdConfig] Restored backup due to write failure") + CyberCPLogFileWriter.writeToFile(f"[safeModifyHttpdConfig] Restored backup due to write failure") except: pass return False, error_msg - # Validate config using openlitespeed -t - try: - if ProcessUtilities.decideServer() == ProcessUtilities.OLS: - validate_cmd = ['/usr/local/lsws/bin/openlitespeed', '-t', '-f', config_file] - else: - # For LiteSpeed Enterprise, use lswsctrl - validate_cmd = ['/usr/local/lsws/bin/lswsctrl', '-t', '-f', config_file] - - result = subprocess.run(validate_cmd, capture_output=True, text=True, timeout=30) - - if result.returncode != 0: - error_msg = f"Config validation failed: {result.stderr}" - logging.writeToFile(f"[safeModifyHttpdConfig] {error_msg}") + # Validate config using openlitespeed -t (for OLS) + # Note: openlitespeed -t may return non-zero due to warnings, so we check for actual errors + # Skip validation if skip_validation=True (useful when pre-existing config has errors) + if skip_validation: + CyberCPLogFileWriter.writeToFile(f"[safeModifyHttpdConfig] Skipping validation as requested for: {description}") + else: + try: + if ProcessUtilities.decideServer() == ProcessUtilities.OLS: + openlitespeed_bin = '/usr/local/lsws/bin/openlitespeed' + if os.path.exists(openlitespeed_bin): + validate_cmd = [openlitespeed_bin, '-t'] + result = subprocess.run(validate_cmd, capture_output=True, text=True, timeout=30) + + # Check for actual errors (not just warnings) + # openlitespeed -t returns 0 on success, non-zero on errors + # But it may also return non-zero for warnings, so check for actual [ERROR] lines + if result.returncode != 0: + # Check if there are actual ERROR log lines (not just WARN or the word "error" in text) + error_output = result.stderr or result.stdout or '' + # Look for lines that start with [ERROR] or contain [ERROR] (actual error log entries) + error_lines = [line for line in error_output.split('\n') if '[ERROR]' in line.upper()] + if error_lines: + # Only fail on actual errors, not warnings + error_msg = f"Config validation failed with errors: {' '.join(error_lines[:3])}" + CyberCPLogFileWriter.writeToFile(f"[safeModifyHttpdConfig] {error_msg}") + # Restore backup + try: + shutil.copy2(backup_file, config_file) + CyberCPLogFileWriter.writeToFile(f"[safeModifyHttpdConfig] Restored backup due to validation failure") + except Exception as restore_error: + CyberCPLogFileWriter.writeToFile(f"[safeModifyHttpdConfig] CRITICAL: Failed to restore backup: {str(restore_error)}") + return False, error_msg + else: + # Only warnings, not errors - proceed + CyberCPLogFileWriter.writeToFile(f"[safeModifyHttpdConfig] Config validation has warnings but no errors, proceeding") + else: + # openlitespeed binary not found, skip validation + CyberCPLogFileWriter.writeToFile(f"[safeModifyHttpdConfig] Warning: openlitespeed binary not found, skipping config validation") + else: + # For LiteSpeed Enterprise, validation is not available via lswsctrl -t + CyberCPLogFileWriter.writeToFile(f"[safeModifyHttpdConfig] Skipping validation for LiteSpeed Enterprise") + except Exception as e: + error_msg = f"Config validation error: {str(e)}" + CyberCPLogFileWriter.writeToFile(f"[safeModifyHttpdConfig] {error_msg}") # Restore backup try: shutil.copy2(backup_file, config_file) - logging.writeToFile(f"[safeModifyHttpdConfig] Restored backup due to validation failure") - except Exception as restore_error: - logging.writeToFile(f"[safeModifyHttpdConfig] CRITICAL: Failed to restore backup: {str(restore_error)}") + CyberCPLogFileWriter.writeToFile(f"[safeModifyHttpdConfig] Restored backup due to validation error") + except: + pass return False, error_msg - except subprocess.TimeoutExpired: - error_msg = "Config validation timed out" - logging.writeToFile(f"[safeModifyHttpdConfig] {error_msg}") - # Restore backup - try: - shutil.copy2(backup_file, config_file) - logging.writeToFile(f"[safeModifyHttpdConfig] Restored backup due to validation timeout") - except: - pass - return False, error_msg - except Exception as e: - error_msg = f"Config validation error: {str(e)}" - logging.writeToFile(f"[safeModifyHttpdConfig] {error_msg}") - # Restore backup - try: - shutil.copy2(backup_file, config_file) - logging.writeToFile(f"[safeModifyHttpdConfig] Restored backup due to validation error") - except: - pass - return False, error_msg - logging.writeToFile(f"[safeModifyHttpdConfig] Successfully modified and validated config: {description}") + CyberCPLogFileWriter.writeToFile(f"[safeModifyHttpdConfig] Successfully modified and validated config: {description}") return True, None @staticmethod @@ -352,7 +373,7 @@ class installUtilities: if not success: error_msg = error if error else "Unknown error" - logging.writeToFile(f"[changePortTo80] Failed: {error_msg}") + CyberCPLogFileWriter.writeToFile(f"[changePortTo80] Failed: {error_msg}") return 0 return installUtilities.reStartLiteSpeed() diff --git a/pluginHolder/patreon_verifier.py b/pluginHolder/patreon_verifier.py index 42fd5cfed..6566f2651 100644 --- a/pluginHolder/patreon_verifier.py +++ b/pluginHolder/patreon_verifier.py @@ -27,14 +27,14 @@ class PatreonVerifier: self.client_id = getattr(settings, 'PATREON_CLIENT_ID', os.environ.get('PATREON_CLIENT_ID', '')) self.client_secret = getattr(settings, 'PATREON_CLIENT_SECRET', os.environ.get('PATREON_CLIENT_SECRET', '')) self.creator_id = getattr(settings, 'PATREON_CREATOR_ID', os.environ.get('PATREON_CREATOR_ID', '')) - self.membership_tier_id = getattr(settings, 'PATREON_MEMBERSHIP_TIER_ID', os.environ.get('PATREON_MEMBERSHIP_TIER_ID', '27789984')) + self.membership_tier_id = getattr(settings, 'PATREON_MEMBERSHIP_TIER_ID', os.environ.get('PATREON_MEMBERSHIP_TIER_ID', '')) self.creator_access_token = getattr(settings, 'PATREON_CREATOR_ACCESS_TOKEN', os.environ.get('PATREON_CREATOR_ACCESS_TOKEN', '')) except: # Fallback to environment variables only self.client_id = os.environ.get('PATREON_CLIENT_ID', '') self.client_secret = os.environ.get('PATREON_CLIENT_SECRET', '') self.creator_id = os.environ.get('PATREON_CREATOR_ID', '') - self.membership_tier_id = os.environ.get('PATREON_MEMBERSHIP_TIER_ID', '27789984') + self.membership_tier_id = os.environ.get('PATREON_MEMBERSHIP_TIER_ID', '') self.creator_access_token = os.environ.get('PATREON_CREATOR_ACCESS_TOKEN', '') # Cache for membership checks (to avoid excessive API calls) diff --git a/pluginHolder/templates/pluginHolder/plugins.html b/pluginHolder/templates/pluginHolder/plugins.html index c8c35de44..c4ad4998e 100644 --- a/pluginHolder/templates/pluginHolder/plugins.html +++ b/pluginHolder/templates/pluginHolder/plugins.html @@ -1317,7 +1317,14 @@
{% endfor %} @@ -1437,7 +1446,7 @@
- {% if plugin.installed %} + {% if plugin.builtin or plugin.installed %} {% if plugin.enabled %} {% else %} @@ -1693,11 +1702,18 @@ function toggleView(view, updateHash = true) { const installedSearchWrapper = document.getElementById('installedPluginsSearchWrapper'); const installedSortFilterBar = document.getElementById('installedSortFilterBar'); + + // Add null checks to prevent errors if elements don't exist + if (!gridView || !tableView || !storeView) { + console.warn('toggleView: One or more view elements not found'); + return; + } + if (view === 'grid') { gridView.style.display = 'grid'; tableView.style.display = 'none'; storeView.style.display = 'none'; - viewBtns[0].classList.add('active'); + if (viewBtns[0]) viewBtns[0].classList.add('active'); if (installedSearchWrapper) installedSearchWrapper.style.display = 'block'; if (installedSortFilterBar) installedSortFilterBar.style.display = 'flex'; if (typeof updateInstalledSortButtons === 'function') updateInstalledSortButtons(); @@ -1707,7 +1723,7 @@ function toggleView(view, updateHash = true) { gridView.style.display = 'none'; tableView.style.display = 'block'; storeView.style.display = 'none'; - viewBtns[1].classList.add('active'); + if (viewBtns[1]) viewBtns[1].classList.add('active'); if (installedSearchWrapper) installedSearchWrapper.style.display = 'block'; if (installedSortFilterBar) installedSortFilterBar.style.display = 'flex'; if (typeof updateInstalledSortButtons === 'function') updateInstalledSortButtons(); @@ -1719,7 +1735,7 @@ function toggleView(view, updateHash = true) { gridView.style.display = 'none'; tableView.style.display = 'none'; storeView.style.display = 'block'; - viewBtns[2].classList.add('active'); + if (viewBtns[2]) viewBtns[2].classList.add('active'); // Load plugins from store if not already loaded if (storePlugins.length === 0) { @@ -2941,23 +2957,35 @@ document.addEventListener('DOMContentLoaded', function() { const hash = window.location.hash.substring(1); // Remove # const validViews = ['grid', 'table', 'store']; - let initialView = 'grid'; // Default - if (validViews.includes(hash)) { - initialView = hash; - } else { - // Default to grid view if plugins exist, otherwise show store - const gridView = document.getElementById('gridView'); - if (gridView && gridView.children.length > 0) { - initialView = 'grid'; + // Check if view elements exist before calling toggleView + const gridView = document.getElementById('gridView'); + const tableView = document.getElementById('tableView'); + const storeView = document.getElementById('storeView'); + + // Only proceed if all view elements exist (plugins are installed) + if (gridView && tableView && storeView) { + let initialView = 'grid'; // Default + if (validViews.includes(hash)) { + initialView = hash; } else { - initialView = 'store'; + // Default to grid view if plugins exist, otherwise show store + if (gridView.children.length > 0) { + initialView = 'grid'; + } else { + initialView = 'store'; + } + } + + // Set initial view without updating hash (only update hash if there was already one) + const hadHash = hash.length > 0; + toggleView(initialView, hadHash); + } else { + // Elements don't exist (no plugins installed), just show store view directly + if (storeView) { + storeView.style.display = 'block'; } } - // Set initial view without updating hash (only update hash if there was already one) - const hadHash = hash.length > 0; - toggleView(initialView, hadHash); - // Load store plugins if store view is visible (either from toggleView or already displayed) setTimeout(function() { const storeViewCheck = document.getElementById('storeView'); diff --git a/pluginHolder/views.py b/pluginHolder/views.py index 0bda088bc..be0419456 100644 --- a/pluginHolder/views.py +++ b/pluginHolder/views.py @@ -38,6 +38,10 @@ PLUGIN_BACKUP_DIR = '/home/cyberpanel/plugin_backups' # Plugin source paths (checked in order; first match wins for install) PLUGIN_SOURCE_PATHS = ['/home/cyberpanel/plugins', '/home/cyberpanel-plugins'] +# Builtin/core plugins that are part of CyberPanel (not user-installable plugins) +# These plugins show "Built-in" badge and only Settings button (no Deactivate/Uninstall) +BUILTIN_PLUGINS = frozenset(['emailMarketing', 'emailPremium']) + def _get_plugin_source_path(plugin_name): """Return the full path to a plugin's source directory, or None if not found.""" for base in PLUGIN_SOURCE_PATHS: @@ -118,6 +122,7 @@ def installed(request): processed_plugins = set() # Track which plugins we've already processed # First, process plugins from source directories (multiple paths: /home/cyberpanel/plugins, /home/cyberpanel-plugins) + # BUT: Skip plugins that are already installed - we'll process those from the installed location instead for pluginPath in PLUGIN_SOURCE_PATHS: if not os.path.exists(pluginPath): continue @@ -129,6 +134,12 @@ def installed(request): for plugin in os.listdir(pluginPath): if plugin in processed_plugins: continue + # Skip if plugin is already installed - we'll process it from installed location instead + completePath = installedPath + '/' + plugin + '/meta.xml' + if os.path.exists(completePath): + # Plugin is installed, skip source path - DON'T mark as processed yet + # The installed location loop will handle it and mark it as processed + continue # Skip files (like .zip files) - only process directories pluginDir = os.path.join(pluginPath, plugin) if not os.path.isdir(pluginDir): @@ -187,6 +198,8 @@ def installed(request): data['desc'] = desc_elem.text data['version'] = version_elem.text data['plugin_dir'] = plugin # Plugin directory name + # Set builtin flag (core CyberPanel plugins vs user-installable plugins) + data['builtin'] = plugin in BUILTIN_PLUGINS # Check if plugin is installed (only if it exists in /usr/local/CyberCP/) # Source directory presence doesn't mean installed - it just means the source files are available data['installed'] = os.path.exists(completePath) @@ -333,6 +346,8 @@ def installed(request): data['desc'] = desc_elem.text data['version'] = version_elem.text data['plugin_dir'] = plugin + # Set builtin flag (core CyberPanel plugins vs user-installable plugins) + data['builtin'] = plugin in BUILTIN_PLUGINS data['installed'] = True # This is an installed plugin data['enabled'] = _is_plugin_enabled(plugin) @@ -394,6 +409,7 @@ def installed(request): # else: is_paid already False from initialization above pluginList.append(data) + processed_plugins.add(plugin) # Mark as processed to prevent duplicates except ElementTree.ParseError as e: errorPlugins.append({'name': plugin, 'error': f'XML parse error: {str(e)}'}) @@ -433,6 +449,7 @@ def installed(request): 'desc': desc_elem.text, 'version': version_elem.text, 'plugin_dir': plugin_name, + 'builtin': plugin_name in BUILTIN_PLUGINS, # Set builtin flag 'installed': os.path.exists(complete_path), 'enabled': _is_plugin_enabled(plugin_name) if os.path.exists(complete_path) else False, 'is_paid': False, diff --git a/static/baseTemplate/assets/mobile-responsive.css b/static/baseTemplate/assets/mobile-responsive.css new file mode 100644 index 000000000..35e2674e5 --- /dev/null +++ b/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/static/baseTemplate/assets/readability-fixes.css b/static/baseTemplate/assets/readability-fixes.css new file mode 100644 index 000000000..1911482b7 --- /dev/null +++ b/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/static/baseTemplate/custom-js/system-status.js b/static/baseTemplate/custom-js/system-status.js index 1817b58ce..76353b1e4 100644 --- a/static/baseTemplate/custom-js/system-status.js +++ b/static/baseTemplate/custom-js/system-status.js @@ -914,126 +914,6 @@ app.controller('dashboardStatsController', function ($scope, $http, $timeout) { // Hide system charts for non-admin users $scope.hideSystemCharts = false; - // Pagination settings - 10 entries per page - var ITEMS_PER_PAGE = 10; - - // Pagination state for each section - $scope.pagination = { - sshLogins: { currentPage: 1, itemsPerPage: ITEMS_PER_PAGE }, - sshLogs: { currentPage: 1, itemsPerPage: ITEMS_PER_PAGE }, - topProcesses: { currentPage: 1, itemsPerPage: ITEMS_PER_PAGE }, - traffic: { currentPage: 1, itemsPerPage: ITEMS_PER_PAGE }, - diskIO: { currentPage: 1, itemsPerPage: ITEMS_PER_PAGE }, - cpuUsage: { currentPage: 1, itemsPerPage: ITEMS_PER_PAGE } - }; - - // Input fields for "go to page" - $scope.gotoPageInput = { - sshLogins: 1, - sshLogs: 1, - topProcesses: 1, - traffic: 1, - diskIO: 1, - cpuUsage: 1 - }; - - // Expose Math to template - $scope.Math = Math; - - // Pagination helper functions - $scope.getTotalPages = function(section) { - var items = []; - if (section === 'sshLogins') items = $scope.sshLogins || []; - else if (section === 'sshLogs') items = $scope.sshLogs || []; - else if (section === 'topProcesses') items = $scope.topProcesses || []; - else if (section === 'traffic') items = $scope.trafficLabels || []; - else if (section === 'diskIO') items = $scope.diskLabels || []; - else if (section === 'cpuUsage') items = $scope.cpuLabels || []; - return Math.max(1, Math.ceil((items.length || 0) / ITEMS_PER_PAGE)); - }; - - $scope.getPaginatedItems = function(section) { - // Initialize pagination if it doesn't exist - if (!$scope.pagination) { - $scope.pagination = {}; - } - if (!$scope.pagination[section]) { - $scope.pagination[section] = { currentPage: 1, itemsPerPage: ITEMS_PER_PAGE }; - console.log('[getPaginatedItems] Initialized pagination for section:', section); - } - - var items = []; - if (section === 'sshLogins') items = $scope.sshLogins || []; - else if (section === 'sshLogs') items = $scope.sshLogs || []; - else if (section === 'topProcesses') items = $scope.topProcesses || []; - else if (section === 'traffic') items = $scope.trafficLabels || []; - else if (section === 'diskIO') items = $scope.diskLabels || []; - else if (section === 'cpuUsage') items = $scope.cpuLabels || []; - - // Ensure currentPage is a valid number - var currentPage = parseInt($scope.pagination[section].currentPage) || 1; - if (currentPage < 1 || isNaN(currentPage)) currentPage = 1; - - var start = (currentPage - 1) * ITEMS_PER_PAGE; - var end = start + ITEMS_PER_PAGE; - - var result = items.slice(start, end); - console.log('[getPaginatedItems] Section:', section, 'Total items:', items.length, 'Page:', currentPage, 'Start:', start, 'End:', end, 'Paginated count:', result.length); - - if (result.length > 0) { - console.log('[getPaginatedItems] First item:', result[0]); - } else if (items.length > 0) { - console.warn('[getPaginatedItems] No items returned but total items > 0. Items:', items.length, 'Page:', currentPage, 'Start:', start, 'End:', end); - } - - return result; - }; - - $scope.goToPage = function(section, page) { - var totalPages = $scope.getTotalPages(section); - if (page >= 1 && page <= totalPages) { - $scope.pagination[section].currentPage = parseInt(page); - $scope.gotoPageInput[section] = parseInt(page); - } - }; - - $scope.nextPage = function(section) { - var totalPages = $scope.getTotalPages(section); - if ($scope.pagination[section].currentPage < totalPages) { - $scope.pagination[section].currentPage++; - $scope.gotoPageInput[section] = $scope.pagination[section].currentPage; - } - }; - - $scope.prevPage = function(section) { - if ($scope.pagination[section].currentPage > 1) { - $scope.pagination[section].currentPage--; - $scope.gotoPageInput[section] = $scope.pagination[section].currentPage; - } - }; - - $scope.getPageNumbers = function(section) { - var totalPages = $scope.getTotalPages(section); - var current = $scope.pagination[section].currentPage; - var pages = []; - var maxVisible = 5; // Show max 5 page numbers - - if (totalPages <= maxVisible) { - for (var i = 1; i <= totalPages; i++) { - pages.push(i); - } - } else { - if (current <= 3) { - for (var i = 1; i <= 5; i++) pages.push(i); - } else if (current >= totalPages - 2) { - for (var i = totalPages - 4; i <= totalPages; i++) pages.push(i); - } else { - for (var i = current - 2; i <= current + 2; i++) pages.push(i); - } - } - return pages; - }; - // Top Processes $scope.topProcesses = []; $scope.loadingTopProcesses = true; @@ -1044,9 +924,6 @@ app.controller('dashboardStatsController', function ($scope, $http, $timeout) { $scope.loadingTopProcesses = false; if (response.data && response.data.status === 1 && response.data.processes) { $scope.topProcesses = response.data.processes; - // Reset to first page when data refreshes - $scope.pagination.topProcesses.currentPage = 1; - $scope.gotoPageInput.topProcesses = 1; } else { $scope.topProcesses = []; } @@ -1066,34 +943,14 @@ app.controller('dashboardStatsController', function ($scope, $http, $timeout) { $scope.loadingSSHLogins = false; if (response.data && response.data.logins) { $scope.sshLogins = response.data.logins; - console.log('[refreshSSHLogins] Loaded', $scope.sshLogins.length, 'SSH logins'); - // Ensure pagination is initialized - if (!$scope.pagination) { - $scope.pagination = {}; - } - if (!$scope.pagination.sshLogins) { - $scope.pagination.sshLogins = { currentPage: 1, itemsPerPage: ITEMS_PER_PAGE }; - } - // Reset to first page when data refreshes - $scope.pagination.sshLogins.currentPage = 1; - if (!$scope.gotoPageInput) { - $scope.gotoPageInput = {}; - } - $scope.gotoPageInput.sshLogins = 1; - - // Debug: Log paginated items - var paginated = $scope.getPaginatedItems('sshLogins'); - console.log('[refreshSSHLogins] Paginated items count:', paginated.length, 'Items:', paginated); - // Debug: Log first login to see structure if ($scope.sshLogins.length > 0) { - console.log('[refreshSSHLogins] First SSH login object:', $scope.sshLogins[0]); - console.log('[refreshSSHLogins] IP field:', $scope.sshLogins[0].ip); - console.log('[refreshSSHLogins] All keys:', Object.keys($scope.sshLogins[0])); + console.log('First SSH login object:', $scope.sshLogins[0]); + console.log('IP field:', $scope.sshLogins[0].ip); + console.log('All keys:', Object.keys($scope.sshLogins[0])); } } else { $scope.sshLogins = []; - console.log('[refreshSSHLogins] No logins found in response'); } }, function (err) { $scope.loadingSSHLogins = false; @@ -1114,9 +971,6 @@ app.controller('dashboardStatsController', function ($scope, $http, $timeout) { $scope.loadingSSHLogs = false; if (response.data && response.data.logs) { $scope.sshLogs = response.data.logs; - // Reset to first page when data refreshes - $scope.pagination.sshLogs.currentPage = 1; - $scope.gotoPageInput.sshLogs = 1; // Analyze logs for security issues $scope.analyzeSSHSecurity(); } else { @@ -1157,8 +1011,84 @@ app.controller('dashboardStatsController', function ($scope, $http, $timeout) { }; $scope.blockIPAddress = function(ipAddress) { - if (!$scope.blockingIP) { - $scope.blockingIP = ipAddress; + try { + console.log('========================================'); + console.log('=== blockIPAddress CALLED ==='); + console.log('========================================'); + console.log('blockIPAddress called with:', ipAddress); + console.log('ipAddress type:', typeof ipAddress); + console.log('ipAddress value:', ipAddress); + console.log('$scope:', $scope); + console.log('$scope.blockIPAddress:', typeof $scope.blockIPAddress); + + // Validate IP address parameter + if (!ipAddress) { + console.error('No IP address provided:', ipAddress); + if (typeof PNotify !== 'undefined') { + new PNotify({ + title: 'Error', + text: 'No IP address provided', + type: 'error', + delay: 5000 + }); + } + return; + } + + // Ensure it's a string and trim it + ipAddress = String(ipAddress).trim(); + + // Validate after trimming + if (!ipAddress || ipAddress === '' || ipAddress === 'undefined' || ipAddress === 'null') { + console.error('IP address is empty or invalid after trim:', ipAddress); + if (typeof PNotify !== 'undefined') { + new PNotify({ + title: 'Error', + text: 'Invalid IP address provided: ' + ipAddress, + type: 'error', + delay: 5000 + }); + } + return; + } + + // Basic IP format validation + var ipPattern = /^(\d{1,3}\.){3}\d{1,3}(\/\d{1,2})?$/; + if (!ipPattern.test(ipAddress)) { + console.error('IP address format is invalid:', ipAddress); + if (typeof PNotify !== 'undefined') { + new PNotify({ + title: 'Error', + text: 'Invalid IP address format: ' + ipAddress, + type: 'error', + delay: 5000 + }); + } + return; + } + + // Prevent duplicate requests + if ($scope.blockingIP === ipAddress) { + console.log('Already processing IP:', ipAddress); + return; // Already processing this IP + } + + // Check if already blocked + if ($scope.blockedIPs && $scope.blockedIPs[ipAddress]) { + console.log('IP already blocked:', ipAddress); + if (typeof PNotify !== 'undefined') { + new PNotify({ + title: 'Info', + text: `IP address ${ipAddress} is already banned`, + type: 'info', + delay: 3000 + }); + } + return; + } + + // Set blocking flag to prevent duplicate requests + $scope.blockingIP = ipAddress; // Use the new Banned IPs system instead of the old blockIPAddress var data = { @@ -1173,48 +1103,343 @@ app.controller('dashboardStatsController', function ($scope, $http, $timeout) { } }; + console.log('Sending ban IP request:', data); + console.log('CSRF Token:', getCookie('csrftoken')); + console.log('Config:', config); + $http.post('/firewall/addBannedIP', data, config).then(function (response) { + console.log('=== addBannedIP SUCCESS ==='); + console.log('Full response:', response); + console.log('response.data:', response.data); + console.log('response.data type:', typeof response.data); + console.log('response.status:', response.status); + + // Reset blocking flag $scope.blockingIP = null; - if (response.data && response.data.status === 1) { + + // Apply scope changes + if (!$scope.$$phase && !$scope.$root.$$phase) { + $scope.$apply(); + } + + // Handle both JSON string and object responses + var responseData = response.data; + if (typeof responseData === 'string') { + try { + responseData = JSON.parse(responseData); + console.log('Parsed responseData from string:', responseData); + } catch (e) { + console.error('Failed to parse response as JSON:', e); + var errorMsg = responseData && responseData.length ? responseData : 'Failed to block IP address'; + if (typeof PNotify !== 'undefined') { + new PNotify({ title: 'Error', text: errorMsg, type: 'error', delay: 5000 }); + } + $scope.blockingIP = null; + return; + } + } + + console.log('Final responseData:', responseData); + console.log('responseData.status:', responseData ? responseData.status : 'undefined'); + console.log('responseData.message:', responseData ? responseData.message : 'undefined'); + console.log('responseData.error_message:', responseData ? responseData.error_message : 'undefined'); + + // Check for success (status === 1 or status === '1') + if (responseData && (responseData.status === 1 || responseData.status === '1')) { // Mark IP as blocked + if (!$scope.blockedIPs) { + $scope.blockedIPs = {}; + } $scope.blockedIPs[ipAddress] = true; // Show success notification - new PNotify({ - title: 'IP Address Banned', - text: `IP address ${ipAddress} has been permanently banned and added to the firewall. You can manage it in the Firewall > Banned IPs section.`, - type: 'success', - delay: 5000 - }); + if (typeof PNotify !== 'undefined') { + new PNotify({ + title: 'IP Address Banned', + text: `IP address ${ipAddress} has been permanently banned and added to the firewall. You can manage it in the Firewall > Banned IPs section.`, + type: 'success', + delay: 5000 + }); + } // Refresh security analysis to update alerts - $scope.analyzeSSHSecurity(); + if ($scope.analyzeSSHSecurity) { + $scope.analyzeSSHSecurity(); + } + + // Apply scope changes + if (!$scope.$$phase && !$scope.$root.$$phase) { + $scope.$apply(); + } } else { // Show error notification + var errorMsg = 'Failed to block IP address'; + if (responseData && responseData.error_message) { + errorMsg = responseData.error_message; + } else if (responseData && responseData.error) { + errorMsg = responseData.error; + } else if (responseData && responseData.message) { + errorMsg = responseData.message; + } else if (responseData) { + errorMsg = JSON.stringify(responseData); + } + console.error('Ban IP failed:', errorMsg); + if (typeof PNotify !== 'undefined') { + new PNotify({ + title: 'Error', + text: errorMsg, + type: 'error', + delay: 5000 + }); + } + } + }, function (err) { + $scope.blockingIP = null; + console.error('addBannedIP error:', err); + console.error('Error status:', err.status); + console.error('Error statusText:', err.statusText); + console.error('Error data:', err.data); + + // Prevent showing duplicate error notifications + if ($scope.lastErrorIP === ipAddress && $scope.lastErrorTime && (Date.now() - $scope.lastErrorTime) < 2000) { + console.log('Skipping duplicate error notification for IP:', ipAddress); + return; + } + + $scope.lastErrorIP = ipAddress; + $scope.lastErrorTime = Date.now(); + + var errorMessage = 'Failed to block IP address'; + var errData = err.data; + if (typeof errData === 'string') { + try { + errData = JSON.parse(errData); + } catch (e) { + if (errData && errData.length) { + errorMessage = errData.length > 200 ? errData.substring(0, 200) + '...' : errData; + } + } + } + if (errData && typeof errData === 'object') { + errorMessage = errData.error_message || errData.error || errData.message || errorMessage; + } else if (err.status) { + errorMessage = 'HTTP ' + err.status + ': ' + (errorMessage); + } + + console.error('Final error message:', errorMessage); + + if (typeof PNotify !== 'undefined') { new PNotify({ title: 'Error', - text: response.data && response.data.error ? response.data.error : 'Failed to block IP address', + text: errorMessage, type: 'error', delay: 5000 }); } - }, function (err) { - $scope.blockingIP = null; - var errorMessage = 'Failed to block IP address'; - if (err.data && err.data.error) { - errorMessage = err.data.error; - } else if (err.data && err.data.message) { - errorMessage = err.data.message; + }); + } catch (e) { + console.error('========================================'); + console.error('=== ERROR in blockIPAddress ==='); + console.error('========================================'); + console.error('Error:', e); + console.error('Error message:', e.message); + console.error('Error stack:', e.stack); + $scope.blockingIP = null; + if (typeof PNotify !== 'undefined') { + new PNotify({ + title: 'Error', + text: 'An error occurred while trying to ban the IP address: ' + (e.message || String(e)), + type: 'error', + delay: 5000 + }); + } + } + }; + + // Ban IP from SSH Logs + $scope.banIPFromSSHLog = function(ipAddress) { + if (!ipAddress) { + new PNotify({ + title: 'Error', + text: 'No IP address provided', + type: 'error', + delay: 5000 + }); + return; + } + + if ($scope.blockingIP === ipAddress) { + return; // Already processing + } + + if ($scope.blockedIPs[ipAddress]) { + new PNotify({ + title: 'Info', + text: `IP address ${ipAddress} is already banned`, + type: 'info', + delay: 3000 + }); + return; + } + + $scope.blockingIP = ipAddress; + + // Use the Banned IPs system + var data = { + ip: ipAddress, + reason: 'Suspicious activity detected from SSH logs', + duration: 'permanent' + }; + + var config = { + headers: { + 'X-CSRFToken': getCookie('csrftoken') + } + }; + + $http.post('/firewall/addBannedIP', data, config).then(function (response) { + $scope.blockingIP = null; + if (response.data && response.data.status === 1) { + // Mark IP as blocked + $scope.blockedIPs[ipAddress] = true; + + // Show success notification + new PNotify({ + title: 'IP Address Banned', + text: `IP address ${ipAddress} has been permanently banned and added to the firewall. You can manage it in the Firewall > Banned IPs section.`, + type: 'success', + delay: 5000 + }); + + // Refresh SSH logs to update the UI + $scope.refreshSSHLogs(); + } else { + // Show error notification + var errorMsg = 'Failed to ban IP address'; + if (response.data && response.data.error_message) { + errorMsg = response.data.error_message; + } else if (response.data && response.data.error) { + errorMsg = response.data.error; } new PNotify({ title: 'Error', - text: errorMessage, + text: errorMsg, type: 'error', delay: 5000 }); + } + }, function (err) { + $scope.blockingIP = null; + var errorMessage = 'Failed to ban IP address'; + if (err.data && err.data.error_message) { + errorMessage = err.data.error_message; + } else if (err.data && err.data.error) { + errorMessage = err.data.error; + } else if (err.data && err.data.message) { + errorMessage = err.data.message; + } + + new PNotify({ + title: 'Error', + text: errorMessage, + type: 'error', + delay: 5000 }); + }); + }; + + // Ban IP from SSH Logs + $scope.banIPFromSSHLog = function(ipAddress) { + if (!ipAddress) { + new PNotify({ + title: 'Error', + text: 'No IP address provided', + type: 'error', + delay: 5000 + }); + return; } + + if ($scope.blockingIP === ipAddress) { + return; // Already processing + } + + if ($scope.blockedIPs[ipAddress]) { + new PNotify({ + title: 'Info', + text: `IP address ${ipAddress} is already banned`, + type: 'info', + delay: 3000 + }); + return; + } + + $scope.blockingIP = ipAddress; + + // Use the Banned IPs system + var data = { + ip: ipAddress, + reason: 'Suspicious activity detected from SSH logs', + duration: 'permanent' + }; + + var config = { + headers: { + 'X-CSRFToken': getCookie('csrftoken') + } + }; + + $http.post('/firewall/addBannedIP', data, config).then(function (response) { + $scope.blockingIP = null; + if (response.data && response.data.status === 1) { + // Mark IP as blocked + $scope.blockedIPs[ipAddress] = true; + + // Show success notification + new PNotify({ + title: 'IP Address Banned', + text: `IP address ${ipAddress} has been permanently banned and added to the firewall. You can manage it in the Firewall > Banned IPs section.`, + type: 'success', + delay: 5000 + }); + + // Refresh SSH logs to update the UI + $scope.refreshSSHLogs(); + } else { + // Show error notification + var errorMsg = 'Failed to ban IP address'; + if (response.data && response.data.error_message) { + errorMsg = response.data.error_message; + } else if (response.data && response.data.error) { + errorMsg = response.data.error; + } + + new PNotify({ + title: 'Error', + text: errorMsg, + type: 'error', + delay: 5000 + }); + } + }, function (err) { + $scope.blockingIP = null; + var errorMessage = 'Failed to ban IP address'; + if (err.data && err.data.error_message) { + errorMessage = err.data.error_message; + } else if (err.data && err.data.error) { + errorMessage = err.data.error; + } else if (err.data && err.data.message) { + errorMessage = err.data.message; + } + + new PNotify({ + title: 'Error', + text: errorMessage, + type: 'error', + delay: 5000 + }); + }); }; // Initial fetch @@ -1224,72 +1449,15 @@ app.controller('dashboardStatsController', function ($scope, $http, $timeout) { // Chart.js chart objects var trafficChart, diskIOChart, cpuChart; - // Data arrays for live graphs - expose to scope for pagination - $scope.trafficLabels = []; - $scope.rxData = []; - $scope.txData = []; - $scope.diskLabels = []; - $scope.readData = []; - $scope.writeData = []; - $scope.cpuLabels = []; - $scope.cpuUsageData = []; - // Internal references for backward compatibility - var trafficLabels = $scope.trafficLabels; - var rxData = $scope.rxData; - var txData = $scope.txData; - var diskLabels = $scope.diskLabels; - var readData = $scope.readData; - var writeData = $scope.writeData; - var cpuLabels = $scope.cpuLabels; - var cpuUsageData = $scope.cpuUsageData; + // Data arrays for live graphs + var trafficLabels = [], rxData = [], txData = []; + var diskLabels = [], readData = [], writeData = []; + var cpuLabels = [], cpuUsageData = []; // For rate calculation var lastRx = null, lastTx = null, lastDiskRead = null, lastDiskWrite = null, lastCPU = null; var lastCPUTimes = null; var pollInterval = 2000; // ms var maxPoints = 30; - - // Watch pagination changes and update charts accordingly - $scope.$watch('pagination.traffic.currentPage', function() { - updateTrafficChartData(); - }); - $scope.$watch('pagination.diskIO.currentPage', function() { - updateDiskIOChartData(); - }); - $scope.$watch('pagination.cpuUsage.currentPage', function() { - updateCPUChartData(); - }); - - function updateTrafficChartData() { - if (!trafficChart || !$scope.trafficLabels || $scope.trafficLabels.length === 0) return; - var startIdx = ($scope.pagination.traffic.currentPage - 1) * ITEMS_PER_PAGE; - var endIdx = startIdx + ITEMS_PER_PAGE; - - trafficChart.data.labels = $scope.trafficLabels.slice(startIdx, endIdx); - trafficChart.data.datasets[0].data = $scope.rxData.slice(startIdx, endIdx); - trafficChart.data.datasets[1].data = $scope.txData.slice(startIdx, endIdx); - trafficChart.update(); - } - - function updateDiskIOChartData() { - if (!diskIOChart || !$scope.diskLabels || $scope.diskLabels.length === 0) return; - var startIdx = ($scope.pagination.diskIO.currentPage - 1) * ITEMS_PER_PAGE; - var endIdx = startIdx + ITEMS_PER_PAGE; - - diskIOChart.data.labels = $scope.diskLabels.slice(startIdx, endIdx); - diskIOChart.data.datasets[0].data = $scope.readData.slice(startIdx, endIdx); - diskIOChart.data.datasets[1].data = $scope.writeData.slice(startIdx, endIdx); - diskIOChart.update(); - } - - function updateCPUChartData() { - if (!cpuChart || !$scope.cpuLabels || $scope.cpuLabels.length === 0) return; - var startIdx = ($scope.pagination.cpuUsage.currentPage - 1) * ITEMS_PER_PAGE; - var endIdx = startIdx + ITEMS_PER_PAGE; - - cpuChart.data.labels = $scope.cpuLabels.slice(startIdx, endIdx); - cpuChart.data.datasets[0].data = $scope.cpuUsageData.slice(startIdx, endIdx); - cpuChart.update(); - } function pollDashboardStats() { console.log('[dashboardStatsController] pollDashboardStats() called'); diff --git a/static/filemanager/js/fileManager.js b/static/filemanager/js/fileManager.js index 7ea1bc575..78ad4dfca 100644 --- a/static/filemanager/js/fileManager.js +++ b/static/filemanager/js/fileManager.js @@ -82,6 +82,15 @@ fileManager.controller('fileManagerCtrl', function ($scope, $http, FileUploader, $scope.showUploadBox = function () { $('#uploadBox').modal('show'); }; + // Fix aria-hidden a11y: move focus out of modal before hide so no focused descendant retains focus + $(document).on('hide.bs.modal', '.modal', function () { + var modal = this; + if (document.activeElement && modal.contains(document.activeElement)) { + var trigger = document.getElementById('uploadTriggerBtn'); + if (trigger && modal.id === 'uploadBox') { trigger.focus(); } + else { document.activeElement.blur(); } + } + }); $scope.showHTMLEditorModal = function (MainFM= 0) { $scope.htmlEditorLoading = false; @@ -1147,7 +1156,8 @@ fileManager.controller('fileManagerCtrl', function ($scope, $http, FileUploader, }); $scope.fetchForTableSecondary(null, 'refresh'); } else { - var notification = alertify.notify('Files/Folders can not be deleted', 'error', 5, function () { + var errMsg = (response.data && response.data.error_message) ? response.data.error_message : 'Files/Folders can not be deleted'; + var notification = alertify.notify(errMsg, 'error', 8, function () { console.log('dismissed'); }); } @@ -1155,6 +1165,10 @@ fileManager.controller('fileManagerCtrl', function ($scope, $http, FileUploader, } function cantLoadInitialDatas(response) { + var err = (response && response.data && (response.data.error_message || response.data.message)) || + (response && response.statusText) || 'Request failed'; + if (response && response.status === 0) err = 'Network error'; + alertify.notify(err, 'error', 8); } }; diff --git a/static/filemanager/js/newFileManager.js b/static/filemanager/js/newFileManager.js index bfa8bed5b..b10512b57 100644 --- a/static/filemanager/js/newFileManager.js +++ b/static/filemanager/js/newFileManager.js @@ -156,6 +156,14 @@ function findFileExtension(fileName) { $scope.showUploadBox = function () { $("#uploadBox").modal(); }; + $(document).on("hide.bs.modal", ".modal", function () { + var modal = this; + if (document.activeElement && modal.contains(document.activeElement)) { + var trigger = document.getElementById("uploadTriggerBtn"); + if (trigger && modal.id === "uploadBox") { trigger.focus(); } + else { document.activeElement.blur(); } + } + }); $scope.showHTMLEditorModal = function (MainFM = 0) { $scope.fileInEditor = allFilesAndFolders[0]; diff --git a/static/firewall/firewall.js b/static/firewall/firewall.js index 495b88ec0..cbcc04bbc 100644 --- a/static/firewall/firewall.js +++ b/static/firewall/firewall.js @@ -5,7 +5,7 @@ /* Java script code to ADD Firewall Rules */ -app.controller('firewallController', function ($scope, $http) { +app.controller('firewallController', function ($scope, $http, $timeout) { $scope.rulesLoading = true; $scope.actionFailed = true; @@ -16,9 +16,51 @@ app.controller('firewallController', function ($scope, $http) { $scope.couldNotConnect = true; $scope.rulesDetails = false; - // Banned IPs variables - $scope.activeTab = 'rules'; + // 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 = []; + // 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); }); + } + $scope.setFirewallTab = function(tab) { + $timeout(function() { + $scope.activeTab = tab; + window.location.hash = (tab === 'banned') ? '#banned-ips' : '#rules'; + if (tab === 'banned') { populateBannedIPs(); } else { populateCurrentRecords(); } + }, 0); + }; + window.addEventListener('hashchange', function() { + var tab = tabFromHash(); + if ($scope.activeTab !== tab) { + $scope.activeTab = tab; + if (tab === 'banned') { populateBannedIPs(); } else { populateCurrentRecords(); } + if (!$scope.$$phase && !$scope.$root.$$phase) { $scope.$apply(); } + } + }); + $scope.rulesPage = 1; + $scope.rulesPageSize = 10; + $scope.rulesPageSizeOptions = [5, 10, 20, 30, 50, 100]; + $scope.rulesTotalCount = 0; + $scope.bannedPage = 1; + $scope.bannedPageSize = 10; + $scope.bannedPageSizeOptions = [5, 10, 20, 30, 50, 100]; + $scope.bannedTotalCount = 0; $scope.bannedIPsLoading = false; $scope.bannedIPActionFailed = true; $scope.bannedIPActionSuccess = true; @@ -29,9 +71,18 @@ app.controller('firewallController', function ($scope, $http) { firewallStatus(); + // Load both tabs on init populateCurrentRecords(); populateBannedIPs(); + // Whenever activeTab changes, load that tab's data (ensures second tab loads even if click/apply failed) + $scope.$watch('activeTab', function(newVal, oldVal) { + if (newVal === oldVal || !newVal) return; + $timeout(function() { + if (newVal === 'banned') { populateBannedIPs(); } else if (newVal === 'rules') { populateCurrentRecords(); } + }, 0); + }); + $scope.addRule = function () { $scope.rulesLoading = false; @@ -123,37 +174,67 @@ app.controller('firewallController', function ($scope, $http) { $scope.actionFailed = true; $scope.actionSuccess = true; - url = "/firewall/getCurrentRules"; - - var data = {}; - - var config = { - headers: { - 'X-CSRFToken': getCookie('csrftoken') - } - }; - + 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) { @@ -2417,20 +2498,25 @@ app.controller('litespeed_ent_conf', function ($scope, $http, $timeout, $window) function populateBannedIPs() { $scope.bannedIPsLoading = true; var url = "/firewall/getBannedIPs"; + var postData = { page: $scope.bannedPage || 1, page_size: $scope.bannedPageSize || 10 }; var config = { headers: { 'X-CSRFToken': getCookie('csrftoken') } }; - $http.post(url, {}, config).then(function(response) { + $http.post(url, postData, config).then(function(response) { $scope.bannedIPsLoading = false; - if (response.data.status === 1) { - $scope.bannedIPs = response.data.bannedIPs || []; + var res = (typeof response.data === 'string') ? (function() { try { return JSON.parse(response.data); } catch (e) { return {}; } })() : response.data; + if (res && res.status === 1) { + $scope.bannedIPs = res.bannedIPs || []; + $scope.bannedTotalCount = res.total_count != null ? res.total_count : ($scope.bannedIPs ? $scope.bannedIPs.length : 0); + $scope.bannedPage = Math.max(1, res.page != null ? res.page : 1); + $scope.bannedPageSize = res.page_size != null ? res.page_size : 10; } else { $scope.bannedIPs = []; $scope.bannedIPActionFailed = false; - $scope.bannedIPErrorMessage = response.data.error_message; + $scope.bannedIPErrorMessage = (res && res.error_message) ? res.error_message : ''; } }, function(error) { $scope.bannedIPsLoading = false; @@ -2438,6 +2524,53 @@ app.controller('litespeed_ent_conf', function ($scope, $http, $timeout, $window) }); } + $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(); + }; + $scope.populateBannedIPs = populateBannedIPs; + + if (typeof window !== 'undefined') { + window.__firewallLoadTab = function(tab) { + $scope.$evalAsync(function() { + $scope.activeTab = tab; + if (tab === 'banned') { populateBannedIPs(); } else { populateCurrentRecords(); } + }); + }; + } + $scope.addBannedIP = function() { if (!$scope.banIP || !$scope.banReason) { $scope.bannedIPActionFailed = false; @@ -2696,4 +2829,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/static/ftp/ftp.js b/static/ftp/ftp.js index 3035a8c7c..ef6cd4a4e 100644 --- a/static/ftp/ftp.js +++ b/static/ftp/ftp.js @@ -26,9 +26,10 @@ app.controller('createFTPAccount', function ($scope, $http) { $sel.select2(); $sel.on('select2:select', function (e) { var data = e.params.data; - $scope.ftpDomain = data.text; - $scope.ftpDetails = false; - $scope.$apply(); + $scope.$evalAsync(function () { + $scope.ftpDomain = data.text; + $scope.ftpDetails = false; + }); $(".ftpDetails, .account-details").show(); }); } else { @@ -42,9 +43,11 @@ app.controller('createFTPAccount', function ($scope, $http) { } function initNativeSelect() { $('.create-ftp-acct-select').off('select2:select').on('change', function () { - $scope.ftpDomain = $(this).val(); - $scope.ftpDetails = ($scope.ftpDomain && $scope.ftpDomain !== '') ? false : true; - $scope.$apply(); + var val = $(this).val(); + $scope.$evalAsync(function () { + $scope.ftpDomain = val; + $scope.ftpDetails = (val && val !== '') ? false : true; + }); $(".ftpDetails, .account-details").show(); }); } diff --git a/static/mailServer/emailLimitsController.js b/static/mailServer/emailLimitsController.js index 45f6bc77b..21d504cc7 100644 --- a/static/mailServer/emailLimitsController.js +++ b/static/mailServer/emailLimitsController.js @@ -114,7 +114,7 @@ $scope.forwardSuccess = true; $scope.couldNotConnect = true; $scope.notifyBox = true; - if (typeof new PNotify === 'function') { + if (typeof PNotify === 'function') { new PNotify({ title: 'Success!', text: 'Changes applied.', type: 'success' }); } $scope.showEmailDetails(); @@ -126,7 +126,7 @@ $scope.forwardSuccess = true; $scope.couldNotConnect = true; $scope.notifyBox = false; - if (typeof new PNotify === 'function') { + if (typeof PNotify === 'function') { new PNotify({ title: 'Error!', text: response.data.error_message || 'Error', type: 'error' }); } } diff --git a/static/mailServer/mailServer.js b/static/mailServer/mailServer.js index 62be0aefe..a546a8cec 100644 --- a/static/mailServer/mailServer.js +++ b/static/mailServer/mailServer.js @@ -1514,6 +1514,7 @@ app.controller('EmailLimitsNew', function ($scope, $http) { // Given email to search for var givenEmail = $scope.selectedEmail; + if ($scope.emails) { for (var i = 0; i < $scope.emails.length; i++) { if ($scope.emails[i].email === givenEmail) { // Extract numberofEmails and duration @@ -1523,14 +1524,11 @@ app.controller('EmailLimitsNew', function ($scope, $http) { $scope.numberofEmails = numberofEmails; $scope.duration = duration; - // Use numberofEmails and duration as needed - console.log("Number of emails:", numberofEmails); - console.log("Duration:", duration); - // Break out of the loop since the email is found break; } } + } }; diff --git a/static/serverStatus/serverStatus.js b/static/serverStatus/serverStatus.js index f16d66701..733d26a96 100644 --- a/static/serverStatus/serverStatus.js +++ b/static/serverStatus/serverStatus.js @@ -641,11 +641,15 @@ app.controller('servicesManager', function ($scope, $http) { getServiceStatus(); $scope.ActionSuccessfull = true; $scope.ActionFailed = false; + $scope.actionErrorMsg = ''; $scope.couldNotConnect = false; $scope.actionLoader = false; $scope.btnDisable = false; }, 3000); } else { + var errMsg = (response.data && response.data.error_message) ? response.data.error_message : 'Action failed'; + if (errMsg === 0) errMsg = 'Action failed'; + $scope.actionErrorMsg = errMsg; setTimeout(function () { getServiceStatus(); $scope.ActionSuccessfull = false; @@ -654,7 +658,6 @@ app.controller('servicesManager', function ($scope, $http) { $scope.actionLoader = false; $scope.btnDisable = false; }, 5000); - } }