Merge pull request #1684 from master3395/v2.5.5-dev

V2.5.5 dev
This commit is contained in:
Master3395
2026-02-14 23:08:15 +01:00
committed by GitHub
37 changed files with 3343 additions and 663 deletions

BIN
.DS_Store vendored

Binary file not shown.

View File

@@ -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

View File

@@ -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

View File

@@ -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[

View File

@@ -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 = [

View File

@@ -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).
---

View File

@@ -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;
}
}

View File

@@ -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;
}
}

View File

@@ -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;

View File

@@ -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);
}
}

View File

@@ -34,14 +34,13 @@
<script src="{% static 'baseTemplate/assets/bootstrap/js/bootstrap.min.js' %}?v={{ CP_VERSION }}"></script>
<script src="{% static 'baseTemplate/bootstrap-toggle.min.js' %}?v={{ CP_VERSION }}"></script>
<script src="{% static 'baseTemplate/custom-js/qrious.min.js' %}?v={{ CP_VERSION }}"></script>
<!-- Chart.js must load before system-status.js (dashboard charts depend on it) -->
<script src="{% static 'baseTemplate/custom-js/chart.umd.min.js' %}?v={{ CP_VERSION }}" data-cfasync="false"></script>
<script src="{% static 'baseTemplate/custom-js/system-status.js' %}?v={{ CP_VERSION }}" data-cfasync="false"></script>
<!-- Font Awesome -->
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
<!-- Chart.js -->
<script src="{% static 'baseTemplate/custom-js/chart.umd.min.js' %}?v={{ CP_VERSION }}"></script>
<!-- PNotify (data-cfasync=false ensures it loads before controllers that use it) -->
<link rel="stylesheet" type="text/css" href="{% static 'baseTemplate/custom-js/pnotify.custom.min.css' %}?v={{ CP_VERSION }}">
<script src="{% static 'baseTemplate/custom-js/pnotify.custom.min.js' %}?v={{ CP_VERSION }}" data-cfasync="false"></script>

View File

@@ -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

View File

@@ -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

View File

@@ -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}

View File

@@ -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'),
),
]

View File

@@ -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 5100)
$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)
}
}
});
});
(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);
})();

View File

@@ -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 @@
</div>
</div>
<!-- Tab Navigation -->
<div class="tab-navigation">
<button type="button"
ng-click="activeTab = 'rules'"
ng-class="{'tab-active': activeTab === 'rules'}"
class="tab-button">
<!-- Tab Navigation: buttons with native fallback so clicks always work -->
<div class="tab-navigation" role="tablist" id="firewall-tab-nav">
<button type="button" class="tab-button" role="tab" data-tab="rules" title="{% trans 'Firewall Rules' %}" ng-class="{'tab-active': activeTab === 'rules'}" ng-click="setFirewallTab('rules')">
<i class="fas fa-list-alt"></i>
{% trans "Firewall Rules" %}
</button>
<button type="button"
ng-click="activeTab = 'banned'; populateBannedIPs();"
ng-class="{'tab-active': activeTab === 'banned'}"
class="tab-button">
<button type="button" class="tab-button" role="tab" data-tab="banned" title="{% trans 'Banned IPs' %}" ng-class="{'tab-active': activeTab === 'banned'}" ng-click="setFirewallTab('banned')">
<i class="fas fa-ban"></i>
{% trans "Banned IPs" %}
</button>
</div>
<!-- Rules Panel -->
<div class="rules-panel" ng-show="activeTab === 'rules'">
<!-- Rules Panel (ng-if so second tab is not in DOM until needed; $watch loads data when switching) -->
<div class="rules-panel" ng-if="activeTab === 'rules'">
<div class="panel-header">
<div class="panel-title">
<div class="panel-icon">
@@ -1206,6 +1333,33 @@
</table>
</div>
<!-- Firewall Rules Pagination (show when there are rules or a total count) -->
<div class="pagination-bar" ng-if="(rules && rules.length > 0) || (rulesTotalCount > 0)">
<div class="pagination-info">
<span>{% trans "Showing" %} {$ rulesRangeStart() || 0 $} {$ rulesRangeEnd() || 0 $} {% trans "of" %} {$ rulesTotalCount || rules.length || 0 $}</span>
<span class="pagination-size">
<span class="pagination-goto-label">{% trans "Per page:" %}</span>
<span class="pagination-size-btns">
<button type="button" ng-repeat="n in rulesPageSizeOptions" class="pagination-size-btn" ng-class="{active: rulesPageSize === n}" ng-click="rulesPageSize = n; setRulesPageSize()">{$ n $}</button>
</span>
</span>
</div>
<div class="pagination-controls">
<button type="button" ng-click="goToRulesPage(rulesPage - 1)" ng-disabled="rulesPage <= 1" title="{% trans 'Previous' %}">
<i class="fas fa-chevron-left"></i>
</button>
<span class="page-num">{$ rulesPage $} / {$ rulesTotalPages() $}</span>
<button type="button" ng-click="goToRulesPage(rulesPage + 1)" ng-disabled="rulesPage >= rulesTotalPages()" title="{% trans 'Next' %}">
<i class="fas fa-chevron-right"></i>
</button>
<span class="pagination-goto">
<label class="pagination-goto-label">{% trans "Go to page" %}</label>
<input type="number" min="1" ng-attr-max="rulesTotalPages()" ng-model="rulesPageInput" class="pagination-goto-input" placeholder="{$ rulesPage $}">
<button type="button" class="pagination-goto-btn" ng-click="goToRulesPageByInput()" title="{% trans 'Go' %}">{% trans "Go" %}</button>
</span>
</div>
</div>
<!-- Empty State -->
<div ng-if="rules.length == 0" class="empty-state">
<i class="fas fa-shield-alt empty-icon"></i>
@@ -1234,7 +1388,7 @@
</div>
<!-- Banned IPs Panel -->
<div class="banned-ips-panel" ng-show="activeTab === 'banned'">
<div class="banned-ips-panel" ng-if="activeTab === 'banned'">
<div class="panel-header">
<div class="panel-title">
<div class="panel-icon">
@@ -1376,6 +1530,33 @@
</table>
</div>
<!-- Banned IPs Pagination (show when there are rows or a total count) -->
<div class="pagination-bar" ng-if="(bannedIPs && bannedIPs.length > 0) || (bannedTotalCount > 0)">
<div class="pagination-info">
<span>{% trans "Showing" %} {$ bannedRangeStart() || 0 $} {$ bannedRangeEnd() || 0 $} {% trans "of" %} {$ bannedTotalCount || bannedIPs.length || 0 $}</span>
<span class="pagination-size">
<span class="pagination-goto-label">{% trans "Per page:" %}</span>
<span class="pagination-size-btns">
<button type="button" ng-repeat="n in bannedPageSizeOptions" class="pagination-size-btn" ng-class="{active: bannedPageSize === n}" ng-click="bannedPageSize = n; setBannedPageSize()">{$ n $}</button>
</span>
</span>
</div>
<div class="pagination-controls">
<button type="button" ng-click="goToBannedPage(bannedPage - 1)" ng-disabled="bannedPage <= 1" title="{% trans 'Previous' %}">
<i class="fas fa-chevron-left"></i>
</button>
<span class="page-num">{$ bannedPage $} / {$ bannedTotalPages() $}</span>
<button type="button" ng-click="goToBannedPage(bannedPage + 1)" ng-disabled="bannedPage >= bannedTotalPages()" title="{% trans 'Next' %}">
<i class="fas fa-chevron-right"></i>
</button>
<span class="pagination-goto">
<label class="pagination-goto-label">{% trans "Go to page" %}</label>
<input type="number" min="1" ng-attr-max="bannedTotalPages()" ng-model="bannedPageInput" class="pagination-goto-input" placeholder="{$ bannedPage $}">
<button type="button" class="pagination-goto-btn" ng-click="goToBannedPageByInput()" title="{% trans 'Go' %}">{% trans "Go" %}</button>
</span>
</div>
</div>
<!-- Empty State: no banned IPs at all -->
<div ng-if="!bannedIPs || bannedIPs.length == 0" class="empty-state">
<i class="fas fa-shield-check empty-icon"></i>
@@ -1443,4 +1624,80 @@
</div>
</div>
<script>
(function(){
var nav = document.getElementById('firewall-tab-nav');
if (!nav) return;
function loadTabViaAngularScope(tab) {
if (!window.angular) return false;
var container = document.querySelector('.modern-container[ng-controller="firewallController"]') || document.querySelector('.modern-container');
if (!container) return false;
try {
var scope = window.angular.element(container).scope();
if (!scope) return false;
scope.$evalAsync(function() {
scope.activeTab = tab;
if (tab === 'banned' && scope.populateBannedIPs) scope.populateBannedIPs();
else if (tab === 'rules' && scope.populateCurrentRecords) scope.populateCurrentRecords();
});
return true;
} catch (e) {
return false;
}
}
function loadTab(tab) {
if (!tab || (tab !== 'rules' && tab !== 'banned')) return;
var done = false;
if (window.__firewallLoadTab) {
try { window.__firewallLoadTab(tab); done = true; } catch (e) {}
}
if (!done) {
done = loadTabViaAngularScope(tab);
}
if (!done) {
setTimeout(function() {
if (window.__firewallLoadTab) try { window.__firewallLoadTab(tab); } catch (e) {};
else loadTabViaAngularScope(tab);
}, 50);
setTimeout(function() {
if (window.__firewallLoadTab) try { window.__firewallLoadTab(tab); } catch (e) {};
else loadTabViaAngularScope(tab);
}, 200);
setTimeout(function() {
if (window.__firewallLoadTab) try { window.__firewallLoadTab(tab); } catch (e) {};
else loadTabViaAngularScope(tab);
}, 500);
}
}
function onTabButtonActivate(btn) {
var tab = btn && btn.getAttribute('data-tab');
if (!tab) return;
window.location.hash = (tab === 'banned') ? '#banned-ips' : '#rules';
loadTab(tab);
}
nav.addEventListener('click', function(e) {
var btn = (e.target && e.target.closest) ? e.target.closest('.tab-button[data-tab]') : null;
if (btn && nav.contains(btn)) onTabButtonActivate(btn);
}, false);
nav.addEventListener('mousedown', function(e) {
var btn = (e.target && e.target.closest) ? e.target.closest('.tab-button[data-tab]') : null;
if (btn && nav.contains(btn)) onTabButtonActivate(btn);
}, false);
function loadTabFromHash() {
var h = (window.location.hash || '').replace(/^#/, '');
var tab = (h === 'banned-ips') ? 'banned' : 'rules';
loadTab(tab);
}
var h = (window.location.hash || '').replace(/^#/, '');
if (h === 'banned-ips') loadTabFromHash();
window.addEventListener('hashchange', loadTabFromHash);
setTimeout(loadTabFromHash, 150);
setTimeout(loadTabFromHash, 500);
})();
</script>
{% endblock %}

View File

@@ -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'),

View File

@@ -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)

View File

@@ -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()

View File

@@ -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)

View File

@@ -1317,7 +1317,14 @@
<div class="plugin-footer">
<div class="plugin-actions">
{% if plugin.installed %}
{% if plugin.builtin %}
<span class="status-installed-small" style="margin-right: 8px;">{% trans "Built-in" %}</span>
{% if plugin.manage_url %}
<a href="{{ plugin.manage_url }}" class="btn-action btn-settings btn-small" title="{% trans 'Settings' %}">
<i class="fas fa-cog"></i> {% trans "Settings" %}
</a>
{% endif %}
{% elif plugin.installed %}
{% if plugin.manage_url %}
<a href="{{ plugin.manage_url }}" class="btn-action btn-settings btn-small" title="{% trans 'Plugin Settings' %}">
<i class="fas fa-cog"></i> {% trans "Settings" %}
@@ -1344,6 +1351,7 @@
</button>
{% endif %}
</div>
{% if not plugin.builtin %}
<div class="plugin-links">
<a href="/plugins/{{ plugin.plugin_dir }}/help/" class="btn-link btn-link-small" title="{% trans 'Plugin Help' %}">
<i class="fas fa-question-circle"></i> {% trans "Help" %}
@@ -1352,6 +1360,7 @@
<i class="fas fa-info-circle"></i> {% trans "About" %}
</a>
</div>
{% endif %}
</div>
</div>
{% endfor %}
@@ -1437,7 +1446,7 @@
</div>
</td>
<td class="active-column">
{% if plugin.installed %}
{% if plugin.builtin or plugin.installed %}
{% if plugin.enabled %}
<span class="active-status active-yes"><i class="fas fa-check-circle"></i></span>
{% 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');

View File

@@ -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,

View File

@@ -884,9 +884,23 @@ def fetchPackages(request):
locked = ProcessUtilities.outputExecutioner(command).split('\n')
if type == 'CyberPanel':
command = 'cat /usr/local/CyberCP/AllCPUbuntu.json'
packages = json.loads(ProcessUtilities.outputExecutioner(command))
# Prefer live data for Ubuntu 22/24, fall back to static JSON
packages = None
try:
cmd_out = ProcessUtilities.outputExecutioner('apt list --installed 2>/dev/null')
lines = [l for l in cmd_out.split('\n') if l and '/' in l][4:] # Skip header
packages = []
for line in lines:
parts = line.split(None, 2)
if len(parts) >= 2:
packages.append({'Package': parts[0], 'Version': parts[1]})
except Exception:
pass
if not packages and os.path.exists('/usr/local/CyberCP/AllCPUbuntu.json'):
command = 'cat /usr/local/CyberCP/AllCPUbuntu.json'
packages = json.loads(ProcessUtilities.outputExecutioner(command))
if not packages:
packages = []
else:
command = 'apt list --installed'
@@ -906,11 +920,16 @@ def fetchPackages(request):
elif ProcessUtilities.decideDistro() == ProcessUtilities.centos or ProcessUtilities.decideDistro() == ProcessUtilities.cent8:
### Check Package Lock status
if os.path.exists('/etc/yum.conf'):
# Prefer dnf.conf when dnf is present (AlmaLinux 9/10, RHEL 9, Rocky 9)
yum_dnf = 'dnf' if os.path.exists('/usr/bin/dnf') else 'yum'
if yum_dnf == 'dnf' and os.path.exists('/etc/dnf/dnf.conf'):
yumConf = '/etc/dnf/dnf.conf'
elif os.path.exists('/etc/yum.conf'):
yumConf = '/etc/yum.conf'
elif os.path.exists('/etc/yum/yum.conf'):
yumConf = '/etc/yum/yum.conf'
else:
yumConf = '/etc/dnf/dnf.conf' if os.path.exists('/etc/dnf/dnf.conf') else '/etc/yum.conf'
yumConfData = open(yumConf, 'r').read()
locked = []
@@ -930,7 +949,7 @@ def fetchPackages(request):
startForUpdate = 1
command = 'yum check-update'
command = '%s check-update 2>/dev/null || true' % yum_dnf
updates = ProcessUtilities.outputExecutioner(command).split('\n')
for items in updates:
@@ -948,7 +967,7 @@ def fetchPackages(request):
###
command = 'yum list installed'
command = '%s list installed' % yum_dnf
packages = ProcessUtilities.outputExecutioner(command).split('\n')
startFrom = 1
@@ -964,7 +983,7 @@ def fetchPackages(request):
startForUpdate = 1
command = 'yum check-update'
command = '%s check-update 2>/dev/null || true' % yum_dnf
packages = ProcessUtilities.outputExecutioner(command).split('\n')
for items in packages:
@@ -974,8 +993,26 @@ def fetchPackages(request):
else:
startForUpdate = startForUpdate + 1
elif type == 'CyberPanel':
command = 'cat /usr/local/CyberCP/CPCent7repo.json'
packages = json.loads(ProcessUtilities.outputExecutioner(command))
# Prefer live data for AlmaLinux 8/9/10, RHEL, Rocky; fall back to static JSON
packages = None
try:
dnf_cmd = 'dnf list installed' if os.path.exists('/usr/bin/dnf') else 'yum list installed'
cmd_out = ProcessUtilities.outputExecutioner(dnf_cmd)
lines = [l.strip() for l in cmd_out.split('\n') if l.strip()]
idx = next((i for i, l in enumerate(lines) if 'Installed Packages' in l or 'Installed' in l), 0)
lines = lines[idx + 1:] if idx < len(lines) else lines
packages = []
for line in lines:
parts = line.split()
if len(parts) >= 2:
packages.append({'Package': parts[0], 'Version': parts[1]})
except Exception:
pass
if not packages and os.path.exists('/usr/local/CyberCP/CPCent7repo.json'):
command = 'cat /usr/local/CyberCP/CPCent7repo.json'
packages = json.loads(ProcessUtilities.outputExecutioner(command))
if not packages:
packages = []
## make list of packages that need update
@@ -1131,7 +1168,8 @@ def fetchPackageDetails(request):
command = 'apt-cache show %s' % (package)
packageDetails = ProcessUtilities.outputExecutioner(command)
elif ProcessUtilities.decideDistro() == ProcessUtilities.centos or ProcessUtilities.decideDistro() == ProcessUtilities.cent8:
command = 'yum info %s' % (package)
pkg_cmd = 'dnf info' if os.path.exists('/usr/bin/dnf') else 'yum info'
command = '%s %s' % (pkg_cmd, package)
packageDetails = ProcessUtilities.outputExecutioner(command)
data_ret = {'status': 1, 'packageDetails': packageDetails}

View File

@@ -1,114 +0,0 @@
#!/bin/sh
# Simplified CyberPanel Installation Script
# Based on 2.4.4 approach with AlmaLinux 9 fixes
OUTPUT=$(cat /etc/*release)
# Detect OS and set appropriate variables
if echo $OUTPUT | grep -q "AlmaLinux 9" ; then
echo -e "\nDetecting AlmaLinux 9...\n"
SERVER_OS="AlmaLinux9"
PKG_MGR="dnf"
elif echo $OUTPUT | grep -q "AlmaLinux 8" ; then
echo -e "\nDetecting AlmaLinux 8...\n"
SERVER_OS="AlmaLinux8"
PKG_MGR="yum"
elif echo $OUTPUT | grep -q "Ubuntu 22.04" ; then
echo -e "\nDetecting Ubuntu 22.04...\n"
SERVER_OS="Ubuntu2204"
PKG_MGR="apt"
elif echo $OUTPUT | grep -q "Ubuntu 20.04" ; then
echo -e "\nDetecting Ubuntu 20.04...\n"
SERVER_OS="Ubuntu2004"
PKG_MGR="apt"
elif echo $OUTPUT | grep -q "CentOS Linux 8" ; then
echo -e "\nDetecting CentOS 8...\n"
SERVER_OS="CentOS8"
PKG_MGR="yum"
else
echo -e "\nUnsupported OS detected. This script supports:\n"
echo -e "AlmaLinux: 8, 9\n"
echo -e "Ubuntu: 20.04, 22.04\n"
echo -e "CentOS: 8\n"
exit 1
fi
echo "Installing basic dependencies..."
# Install basic packages
if [ "$PKG_MGR" = "dnf" ]; then
dnf update -y
dnf install -y epel-release
dnf install -y wget curl unzip zip rsync firewalld git python3 python3-pip
dnf install -y mariadb-server mariadb-client
dnf install -y ImageMagick gd libicu oniguruma aspell libc-client
elif [ "$PKG_MGR" = "yum" ]; then
yum update -y
yum install -y epel-release
yum install -y wget curl unzip zip rsync firewalld git python3 python3-pip
yum install -y mariadb-server mariadb-client
yum install -y ImageMagick gd libicu oniguruma aspell libc-client
elif [ "$PKG_MGR" = "apt" ]; then
apt update -y
apt install -y wget curl unzip zip rsync git python3 python3-pip
apt install -y mariadb-server mariadb-client
apt install -y imagemagick php-gd php-intl php-mbstring php-pspell
fi
# Start and enable MariaDB
echo "Starting MariaDB..."
systemctl enable mariadb
systemctl start mariadb
# Create MySQL password file
echo "Setting up MySQL..."
mkdir -p /etc/cyberpanel
echo "cyberpanel123" > /etc/cyberpanel/mysqlPassword
chmod 600 /etc/cyberpanel/mysqlPassword
# Secure MySQL installation
mysql -u root -e "ALTER USER 'root'@'localhost' IDENTIFIED BY 'cyberpanel123';" 2>/dev/null || true
mysql -u root -pcyberpanel123 -e "DELETE FROM mysql.user WHERE User='';" 2>/dev/null || true
mysql -u root -pcyberpanel123 -e "DELETE FROM mysql.user WHERE User='root' AND Host NOT IN ('localhost', '127.0.0.1', '::1');" 2>/dev/null || true
mysql -u root -pcyberpanel123 -e "DROP DATABASE IF EXISTS test;" 2>/dev/null || true
mysql -u root -pcyberpanel123 -e "DELETE FROM mysql.db WHERE Db='test' OR Db='test\\_%';" 2>/dev/null || true
mysql -u root -pcyberpanel123 -e "FLUSH PRIVILEGES;" 2>/dev/null || true
# Configure firewall
echo "Configuring firewall..."
if [ "$PKG_MGR" = "dnf" ] || [ "$PKG_MGR" = "yum" ]; then
systemctl enable firewalld
systemctl start firewalld
firewall-cmd --permanent --add-port=8090/tcp
firewall-cmd --permanent --add-port=7080/tcp
firewall-cmd --permanent --add-port=80/tcp
firewall-cmd --permanent --add-port=443/tcp
firewall-cmd --permanent --add-port=21/tcp
firewall-cmd --permanent --add-port=25/tcp
firewall-cmd --permanent --add-port=587/tcp
firewall-cmd --permanent --add-port=465/tcp
firewall-cmd --permanent --add-port=110/tcp
firewall-cmd --permanent --add-port=143/tcp
firewall-cmd --permanent --add-port=993/tcp
firewall-cmd --permanent --add-port=995/tcp
firewall-cmd --permanent --add-port=53/tcp
firewall-cmd --permanent --add-port=53/udp
firewall-cmd --reload
fi
# Download and install CyberPanel
echo "Downloading CyberPanel..."
rm -f cyberpanel.sh
curl --silent -o cyberpanel.sh "https://cyberpanel.sh/?dl&$SERVER_OS" 2>/dev/null
if [ -f "cyberpanel.sh" ]; then
echo "Installing CyberPanel..."
chmod +x cyberpanel.sh
./cyberpanel.sh
else
echo "Failed to download CyberPanel installer!"
exit 1
fi
echo "Installation completed!"

View File

@@ -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;
}
}

View File

@@ -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;
}
}

View File

@@ -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');

View File

@@ -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);
}
};

View File

@@ -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];

View File

@@ -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)
}
}
});
});
(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);
})();

View File

@@ -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();
});
}

View File

@@ -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' });
}
}

View File

@@ -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;
}
}
}
};

View File

@@ -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);
}
}

View File

@@ -35,7 +35,7 @@ These were only in the old fix and were copied into repo during the merge:
| `cyberpanel_standalone.sh` | Standalone install script |
| `fix_installation_issues.sh` | Installation fixes |
| `install_phpmyadmin.sh` | phpMyAdmin installer |
| `simple_install.sh` | Simple installer |
| ~~`simple_install.sh`~~ | Removed use official install.sh one-liner |
| `INSTALLER_SUMMARY.md` | Installer docs |
| `UNIVERSAL_OS_COMPATIBILITY.md` | OS compatibility docs |
| `to-do/MARIADB_INSTALLATION_FIXES.md` | MariaDB fixes |

View File

@@ -13,7 +13,7 @@
- `cyberpanel_standalone.sh`
- `fix_installation_issues.sh`
- `install_phpmyadmin.sh`
- `simple_install.sh`
- ~~`simple_install.sh`~~ (removed; use official install.sh)
- `INSTALLER_SUMMARY.md`
- `UNIVERSAL_OS_COMPATIBILITY.md`
- `to-do/MARIADB_INSTALLATION_FIXES.md`