diff --git a/.DS_Store b/.DS_Store deleted file mode 100644 index 497829761..000000000 Binary files a/.DS_Store and /dev/null differ diff --git a/CPScripts/mailscannerinstaller.sh b/CPScripts/mailscannerinstaller.sh index 76a20e02d..63925c14d 100644 --- a/CPScripts/mailscannerinstaller.sh +++ b/CPScripts/mailscannerinstaller.sh @@ -55,13 +55,13 @@ elif grep -q -E "CloudLinux 7|CloudLinux 8" /etc/os-release ; then Server_OS="CloudLinux" elif grep -q -E "Rocky Linux" /etc/os-release ; then Server_OS="RockyLinux" -elif grep -q -E "Ubuntu 18.04|Ubuntu 20.04|Ubuntu 20.10|Ubuntu 22.04" /etc/os-release ; then +elif grep -q -E "Ubuntu 18.04|Ubuntu 20.04|Ubuntu 20.10|Ubuntu 22.04|Ubuntu 24.04" /etc/os-release ; then Server_OS="Ubuntu" elif grep -q -E "openEuler 20.03|openEuler 22.03" /etc/os-release ; then Server_OS="openEuler" else echo -e "Unable to detect your system..." - echo -e "\nCyberPanel is supported on x86_64 based Ubuntu 18.04, Ubuntu 20.04, Ubuntu 20.10, Ubuntu 22.04, CentOS 7, CentOS 8, AlmaLinux 8, RockyLinux 8, CloudLinux 7, CloudLinux 8, openEuler 20.03, openEuler 22.03...\n" + echo -e "\nCyberPanel is supported on x86_64 based Ubuntu 18.04, Ubuntu 20.04, Ubuntu 20.10, Ubuntu 22.04, Ubuntu 24.04, CentOS 7, CentOS 8, AlmaLinux 8, AlmaLinux 9, AlmaLinux 10, RockyLinux 8, CloudLinux 7, CloudLinux 8, openEuler 20.03, openEuler 22.03...\n" exit fi diff --git a/CPScripts/mailscanneruninstaller.sh b/CPScripts/mailscanneruninstaller.sh index 9eebad670..347deae60 100644 --- a/CPScripts/mailscanneruninstaller.sh +++ b/CPScripts/mailscanneruninstaller.sh @@ -12,13 +12,13 @@ elif grep -q -E "CloudLinux 7|CloudLinux 8" /etc/os-release ; then Server_OS="CloudLinux" elif grep -q -E "Rocky Linux" /etc/os-release ; then Server_OS="RockyLinux" -elif grep -q -E "Ubuntu 18.04|Ubuntu 20.04|Ubuntu 20.10|Ubuntu 22.04" /etc/os-release ; then +elif grep -q -E "Ubuntu 18.04|Ubuntu 20.04|Ubuntu 20.10|Ubuntu 22.04|Ubuntu 24.04" /etc/os-release ; then Server_OS="Ubuntu" elif grep -q -E "openEuler 20.03|openEuler 22.03" /etc/os-release ; then Server_OS="openEuler" else echo -e "Unable to detect your system..." - echo -e "\nCyberPanel is supported on x86_64 based Ubuntu 18.04, Ubuntu 20.04, Ubuntu 20.10, Ubuntu 22.04, CentOS 7, CentOS 8, AlmaLinux 8, RockyLinux 8, CloudLinux 7, CloudLinux 8, openEuler 20.03, openEuler 22.03...\n" + echo -e "\nCyberPanel is supported on x86_64 based Ubuntu 18.04, Ubuntu 20.04, Ubuntu 20.10, Ubuntu 22.04, Ubuntu 24.04, CentOS 7, CentOS 8, AlmaLinux 8, AlmaLinux 9, AlmaLinux 10, RockyLinux 8, CloudLinux 7, CloudLinux 8, openEuler 20.03, openEuler 22.03...\n" exit fi 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/README.md b/README.md index 9568e658c..1f1111bde 100644 --- a/README.md +++ b/README.md @@ -70,25 +70,25 @@ Fast • Secure • Scalable — Simplify hosting management with style. | OS family | Recommended / Supported | | -------------------------- | ----------------------: | -| Ubuntu 24.04, 22.04, 20.04 | ✅ Recommended | -| Debian 13, 12, 11 | ✅ Supported | -| AlmaLinux 10, 9, 8 | ✅ Supported | -| RockyLinux 9, 8 | ✅ Supported | -| RHEL 9, 8 | ✅ Supported | -| CloudLinux 9, 8 | ✅ Supported | +| AlmaLinux 10, 9, 8 | ✅ Recommended | | CentOS 7 | ⚠️ Legacy — EOL | +| CloudLinux 9, 8 | ✅ Supported | +| Debian 13, 12, 11 | ✅ Supported | +| RHEL 9, 8 | ✅ Supported | +| RockyLinux 9, 8 | ✅ Supported | +| Ubuntu 24.04, 22.04, 20.04 | ✅ Recommended | -> CyberPanel targets x86\_64 only. Test the unsupported OS in staging first. +> **Architectures:** x86_64 (primary), aarch64/ARM64 (supported). AlmaLinux is the recommended RHEL-compatible distribution. Test unsupported OS in staging first. --- ## PHP support (short) -* ✅ **Recommended**: PHP 8.5 (beta), 8.4, 8.3, 8.2, 8.1 -* ⚠️ **Legacy**: PHP 8.0, PHP 7.4 (security-only) -* ❌ **Deprecated**: PHP 7.1, 7.2, 7.3 (no longer installed) +* ✅ **Recommended**: PHP 8.5, 8.4 +* ⚠️ **Security fixes only**: PHP 8.3, 8.2, 8.1 +* ❌ **EOL / Deprecated**: PHP 8.0, 7.4, 7.1, 7.2, 7.3 (no longer supported) -Third-party repositories (Remi, Ondrej) may provide older or niche versions; verify compatibility before use. +Third-party repositories may provide older or niche versions; verify compatibility before use. RHEL/Alma/Rocky: [Remi RPM](https://rpms.remirepo.net/). Ubuntu/Debian: [Ondrej PPA](https://launchpad.net/~ondrej/+archive/ubuntu/php). See [php.net/supported-versions](https://www.php.net/supported-versions.php). --- 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/cyberpanel.sh b/cyberpanel.sh index 52713fb5c..feba43655 100644 --- a/cyberpanel.sh +++ b/cyberpanel.sh @@ -117,6 +117,11 @@ detect_os() { OS_FAMILY="rhel" PACKAGE_MANAGER="yum" print_status "Detected: Rocky Linux 8" + elif echo $OUTPUT | grep -q "Ubuntu 24.04" ; then + SERVER_OS="Ubuntu2404" + OS_FAMILY="debian" + PACKAGE_MANAGER="apt" + print_status "Detected: Ubuntu 24.04" elif echo $OUTPUT | grep -q "Ubuntu 22.04" ; then SERVER_OS="Ubuntu2204" OS_FAMILY="debian" @@ -127,6 +132,11 @@ detect_os() { OS_FAMILY="debian" PACKAGE_MANAGER="apt" print_status "Detected: Ubuntu 20.04" + elif echo $OUTPUT | grep -q "Debian GNU/Linux 13" ; then + SERVER_OS="Debian13" + OS_FAMILY="debian" + PACKAGE_MANAGER="apt" + print_status "Detected: Debian GNU/Linux 13" elif echo $OUTPUT | grep -q "Debian GNU/Linux 12" ; then SERVER_OS="Debian12" OS_FAMILY="debian" @@ -139,7 +149,7 @@ detect_os() { print_status "Detected: Debian GNU/Linux 11" else print_status "ERROR: Unsupported OS detected" - print_status "Supported OS: AlmaLinux 8/9/10, CentOS 8/9, Rocky Linux 8/9, Ubuntu 20.04/22.04, Debian 11/12" + print_status "Supported OS: AlmaLinux 8/9/10, CentOS 8/9, Rocky Linux 8/9, Ubuntu 20.04/22.04/24.04, Debian 11/12/13" return 1 fi diff --git a/cyberpanel_utility.sh b/cyberpanel_utility.sh index 26c89743a..fad47d35c 100644 --- a/cyberpanel_utility.sh +++ b/cyberpanel_utility.sh @@ -21,7 +21,7 @@ check_OS() { Server_OS="AlmaLinux" elif grep -q -E "CloudLinux 7|CloudLinux 8" /etc/os-release ; then Server_OS="CloudLinux" - elif grep -q -E "Ubuntu 18.04|Ubuntu 20.04|Ubuntu 20.10|Ubuntu 22.04" /etc/os-release ; then + elif grep -q -E "Ubuntu 18.04|Ubuntu 20.04|Ubuntu 20.10|Ubuntu 22.04|Ubuntu 24.04" /etc/os-release ; then Server_OS="Ubuntu" elif grep -q -E "Rocky Linux" /etc/os-release ; then Server_OS="RockyLinux" @@ -29,7 +29,7 @@ check_OS() { Server_OS="openEuler" else echo -e "Unable to detect your system..." - echo -e "\nCyberPanel is supported on x86_64 based Ubuntu 18.04, Ubuntu 20.04, Ubuntu 20.10, Ubuntu 22.04, CentOS 7, CentOS 8, AlmaLinux 8, AlmaLinux 9, AlmaLinux 10, RockyLinux 8, CloudLinux 7, CloudLinux 8, openEuler 20.03, openEuler 22.03...\n" + echo -e "\nCyberPanel is supported on x86_64 based Ubuntu 18.04, Ubuntu 20.04, Ubuntu 20.10, Ubuntu 22.04, Ubuntu 24.04, CentOS 7, CentOS 8, AlmaLinux 8, AlmaLinux 9, AlmaLinux 10, RockyLinux 8, CloudLinux 7, CloudLinux 8, openEuler 20.03, openEuler 22.03...\n" exit fi 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 @@ - -