mirror of
https://github.com/usmannasir/cyberpanel.git
synced 2026-02-16 19:46:48 +01:00
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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[
|
||||
|
||||
@@ -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 = [
|
||||
|
||||
22
README.md
22
README.md
@@ -70,25 +70,25 @@ Fast • Secure • Scalable — Simplify hosting management with style.
|
||||
|
||||
| OS family | Recommended / Supported |
|
||||
| -------------------------- | ----------------------: |
|
||||
| Ubuntu 24.04, 22.04, 20.04 | ✅ Recommended |
|
||||
| Debian 13, 12, 11 | ✅ Supported |
|
||||
| AlmaLinux 10, 9, 8 | ✅ Supported |
|
||||
| RockyLinux 9, 8 | ✅ Supported |
|
||||
| RHEL 9, 8 | ✅ Supported |
|
||||
| CloudLinux 9, 8 | ✅ Supported |
|
||||
| AlmaLinux 10, 9, 8 | ✅ Recommended |
|
||||
| CentOS 7 | ⚠️ Legacy — EOL |
|
||||
| CloudLinux 9, 8 | ✅ Supported |
|
||||
| Debian 13, 12, 11 | ✅ Supported |
|
||||
| RHEL 9, 8 | ✅ Supported |
|
||||
| RockyLinux 9, 8 | ✅ Supported |
|
||||
| Ubuntu 24.04, 22.04, 20.04 | ✅ Recommended |
|
||||
|
||||
> CyberPanel targets x86\_64 only. Test the unsupported OS in staging first.
|
||||
> **Architectures:** x86_64 (primary), aarch64/ARM64 (supported). AlmaLinux is the recommended RHEL-compatible distribution. Test unsupported OS in staging first.
|
||||
|
||||
---
|
||||
|
||||
## PHP support (short)
|
||||
|
||||
* ✅ **Recommended**: PHP 8.5 (beta), 8.4, 8.3, 8.2, 8.1
|
||||
* ⚠️ **Legacy**: PHP 8.0, PHP 7.4 (security-only)
|
||||
* ❌ **Deprecated**: PHP 7.1, 7.2, 7.3 (no longer installed)
|
||||
* ✅ **Recommended**: PHP 8.5, 8.4
|
||||
* ⚠️ **Security fixes only**: PHP 8.3, 8.2, 8.1
|
||||
* ❌ **EOL / Deprecated**: PHP 8.0, 7.4, 7.1, 7.2, 7.3 (no longer supported)
|
||||
|
||||
Third-party repositories (Remi, Ondrej) may provide older or niche versions; verify compatibility before use.
|
||||
Third-party repositories may provide older or niche versions; verify compatibility before use. RHEL/Alma/Rocky: [Remi RPM](https://rpms.remirepo.net/). Ubuntu/Debian: [Ondrej PPA](https://launchpad.net/~ondrej/+archive/ubuntu/php). See [php.net/supported-versions](https://www.php.net/supported-versions.php).
|
||||
|
||||
---
|
||||
|
||||
|
||||
589
baseTemplate/static/baseTemplate/assets/mobile-responsive.css
Normal file
589
baseTemplate/static/baseTemplate/assets/mobile-responsive.css
Normal 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;
|
||||
}
|
||||
}
|
||||
265
baseTemplate/static/baseTemplate/assets/readability-fixes.css
Normal file
265
baseTemplate/static/baseTemplate/assets/readability-fixes.css
Normal 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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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}
|
||||
|
||||
40
firewall/migrations/0001_initial.py
Normal file
40
firewall/migrations/0001_initial.py
Normal 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'),
|
||||
),
|
||||
]
|
||||
@@ -31,9 +31,61 @@ app.controller('firewallController', function ($scope, $http, $timeout) {
|
||||
$scope.couldNotConnect = true;
|
||||
$scope.rulesDetails = false;
|
||||
|
||||
// Banned IPs variables
|
||||
$scope.activeTab = 'rules';
|
||||
// Banned IPs variables – tab from hash so we stay on /firewall/ (avoids 404 on servers without /firewall/firewall-rules/)
|
||||
function tabFromHash() {
|
||||
var h = (window.location.hash || '').replace(/^#/, '');
|
||||
return (h === 'banned-ips') ? 'banned' : 'rules';
|
||||
}
|
||||
$scope.activeTab = tabFromHash();
|
||||
$scope.bannedIPs = []; // Initialize as empty array
|
||||
|
||||
// Re-apply tab from hash after load (hash can be set after controller init in some browsers)
|
||||
function applyTabFromHash() {
|
||||
var tab = tabFromHash();
|
||||
if ($scope.activeTab !== tab) {
|
||||
$scope.activeTab = tab;
|
||||
if (tab === 'banned') { populateBannedIPs(); } else { populateCurrentRecords(); }
|
||||
if (!$scope.$$phase && !$scope.$root.$$phase) { $scope.$apply(); }
|
||||
}
|
||||
}
|
||||
$timeout(applyTabFromHash, 0);
|
||||
if (document.readyState === 'complete') {
|
||||
$timeout(applyTabFromHash, 50);
|
||||
} else {
|
||||
window.addEventListener('load', function() { $timeout(applyTabFromHash, 0); });
|
||||
}
|
||||
|
||||
// Sync tab with hash and load that tab's data on switch
|
||||
$scope.setFirewallTab = function(tab) {
|
||||
$timeout(function() {
|
||||
$scope.activeTab = tab;
|
||||
window.location.hash = (tab === 'banned') ? '#banned-ips' : '#rules';
|
||||
if (tab === 'banned') { populateBannedIPs(); } else { populateCurrentRecords(); }
|
||||
}, 0);
|
||||
};
|
||||
|
||||
// Back/forward or direct hash change: sync tab and load its data
|
||||
function syncTabFromHash() {
|
||||
var tab = tabFromHash();
|
||||
if ($scope.activeTab !== tab) {
|
||||
$scope.activeTab = tab;
|
||||
if (tab === 'banned') { populateBannedIPs(); } else { populateCurrentRecords(); }
|
||||
if (!$scope.$$phase && !$scope.$root.$$phase) { $scope.$apply(); }
|
||||
}
|
||||
}
|
||||
window.addEventListener('hashchange', syncTabFromHash);
|
||||
|
||||
// Pagination: Firewall Rules (default 10 per page, options 5–100)
|
||||
$scope.rulesPage = 1;
|
||||
$scope.rulesPageSize = 10;
|
||||
$scope.rulesPageSizeOptions = [5, 10, 20, 30, 50, 100];
|
||||
$scope.rulesTotalCount = 0;
|
||||
|
||||
// Pagination: Banned IPs
|
||||
$scope.bannedPage = 1;
|
||||
$scope.bannedPageSize = 10;
|
||||
$scope.bannedPageSizeOptions = [5, 10, 20, 30, 50, 100];
|
||||
$scope.bannedTotalCount = 0;
|
||||
|
||||
// Initialize banned IPs array - start as null so template shows empty state
|
||||
// Will be set to array after API call
|
||||
@@ -47,9 +99,21 @@ app.controller('firewallController', function ($scope, $http, $timeout) {
|
||||
|
||||
firewallStatus();
|
||||
|
||||
// Load both tabs on init; also load on tab change (watch) so content always shows
|
||||
populateCurrentRecords();
|
||||
|
||||
// Load banned IPs immediately when controller initializes
|
||||
populateBannedIPs();
|
||||
|
||||
$scope.$watch('activeTab', function(newVal, oldVal) {
|
||||
if (newVal === oldVal || !newVal) return;
|
||||
$timeout(function() {
|
||||
try {
|
||||
if (newVal === 'banned' && typeof populateBannedIPs === 'function') populateBannedIPs();
|
||||
else if (newVal === 'rules' && typeof populateCurrentRecords === 'function') populateCurrentRecords();
|
||||
} catch (e) {}
|
||||
}, 0);
|
||||
});
|
||||
|
||||
// Log for debugging
|
||||
console.log('=== FIREWALL CONTROLLER INITIALIZING ===');
|
||||
console.log('Initializing firewall controller, loading banned IPs...');
|
||||
|
||||
@@ -69,14 +133,19 @@ app.controller('firewallController', function ($scope, $http, $timeout) {
|
||||
}
|
||||
};
|
||||
|
||||
console.log('Making request to:', url);
|
||||
var postData = {
|
||||
page: $scope.bannedPage || 1,
|
||||
page_size: $scope.bannedPageSize || 10
|
||||
};
|
||||
console.log('Making request to:', url, 'page:', postData.page, 'page_size:', postData.page_size);
|
||||
console.log('CSRF Token:', csrfToken ? 'Found (' + csrfToken.substring(0, 10) + '...)' : 'MISSING!');
|
||||
|
||||
$http.post(url, {}, config).then(
|
||||
$http.post(url, postData, config).then(
|
||||
function(response) {
|
||||
var res = (typeof response.data === 'string') ? (function() { try { return JSON.parse(response.data); } catch (e) { return {}; } })() : response.data;
|
||||
console.log('=== API RESPONSE RECEIVED ===');
|
||||
console.log('Response status:', response.status);
|
||||
console.log('Response data:', JSON.stringify(response.data, null, 2));
|
||||
console.log('Response data (parsed):', res);
|
||||
|
||||
$scope.bannedIPsLoading = false;
|
||||
// Reset error flags
|
||||
@@ -84,8 +153,8 @@ app.controller('firewallController', function ($scope, $http, $timeout) {
|
||||
$scope.bannedIPActionSuccess = true;
|
||||
$scope.bannedIPCouldNotConnect = true;
|
||||
|
||||
if (response.data && response.data.status === 1) {
|
||||
var bannedIPsArray = response.data.bannedIPs || [];
|
||||
if (res && res.status === 1) {
|
||||
var bannedIPsArray = res.bannedIPs || [];
|
||||
console.log('Raw bannedIPs from API:', bannedIPsArray);
|
||||
console.log('Banned IPs count:', bannedIPsArray.length);
|
||||
console.log('Is array?', Array.isArray(bannedIPsArray));
|
||||
@@ -99,6 +168,9 @@ app.controller('firewallController', function ($scope, $http, $timeout) {
|
||||
// Assign to scope - Angular $http callbacks already run within $apply
|
||||
console.log('Assigning to scope.bannedIPs...');
|
||||
$scope.bannedIPs = bannedIPsArray;
|
||||
$scope.bannedTotalCount = res.total_count != null ? res.total_count : bannedIPsArray.length;
|
||||
$scope.bannedPage = Math.max(1, res.page != null ? res.page : 1);
|
||||
$scope.bannedPageSize = res.page_size != null ? res.page_size : 10;
|
||||
console.log('After assignment - scope.bannedIPs:', $scope.bannedIPs);
|
||||
console.log('After assignment - scope.bannedIPs.length:', $scope.bannedIPs ? $scope.bannedIPs.length : 'undefined');
|
||||
console.log('After assignment - activeTab:', $scope.activeTab);
|
||||
@@ -109,10 +181,10 @@ app.controller('firewallController', function ($scope, $http, $timeout) {
|
||||
console.log('=== populateBannedIPs() SUCCESS ===');
|
||||
} else {
|
||||
console.error('ERROR: API returned status !== 1');
|
||||
console.error('Response data:', response.data);
|
||||
console.error('Response data:', res);
|
||||
$scope.bannedIPs = [];
|
||||
$scope.bannedIPActionFailed = false;
|
||||
$scope.bannedIPErrorMessage = (response.data && response.data.error_message) || 'Unknown error';
|
||||
$scope.bannedIPErrorMessage = (res && res.error_message) || 'Unknown error';
|
||||
}
|
||||
},
|
||||
function(error) {
|
||||
@@ -144,6 +216,52 @@ app.controller('firewallController', function ($scope, $http, $timeout) {
|
||||
console.log('$scope.populateBannedIPs() called from template');
|
||||
populateBannedIPs();
|
||||
};
|
||||
|
||||
$scope.goToBannedPage = function(page) {
|
||||
var totalP = $scope.bannedTotalPages();
|
||||
if (page < 1 || page > totalP) return;
|
||||
$scope.bannedPage = page;
|
||||
populateBannedIPs();
|
||||
};
|
||||
$scope.goToBannedPageByInput = function() {
|
||||
var n = parseInt($scope.bannedPageInput, 10);
|
||||
if (isNaN(n) || n < 1) n = 1;
|
||||
var maxP = $scope.bannedTotalPages();
|
||||
if (n > maxP) n = maxP;
|
||||
$scope.bannedPageInput = n;
|
||||
$scope.goToBannedPage(n);
|
||||
};
|
||||
$scope.bannedTotalPages = function() {
|
||||
var size = $scope.bannedPageSize || 10;
|
||||
var total = $scope.bannedTotalCount || ($scope.bannedIPs ? $scope.bannedIPs.length : 0) || 0;
|
||||
return size > 0 ? Math.max(1, Math.ceil(total / size)) : 1;
|
||||
};
|
||||
$scope.bannedRangeStart = function() {
|
||||
var total = $scope.bannedTotalCount || ($scope.bannedIPs ? $scope.bannedIPs.length : 0) || 0;
|
||||
if (total === 0) return 0;
|
||||
var page = Math.max(1, $scope.bannedPage || 1);
|
||||
var size = $scope.bannedPageSize || 10;
|
||||
return (page - 1) * size + 1;
|
||||
};
|
||||
$scope.bannedRangeEnd = function() {
|
||||
var start = $scope.bannedRangeStart();
|
||||
var size = $scope.bannedPageSize || 10;
|
||||
var total = $scope.bannedTotalCount || ($scope.bannedIPs ? $scope.bannedIPs.length : 0) || 0;
|
||||
return total === 0 ? 0 : Math.min(start + size - 1, total);
|
||||
};
|
||||
$scope.setBannedPageSize = function() {
|
||||
$scope.bannedPage = 1;
|
||||
populateBannedIPs();
|
||||
};
|
||||
|
||||
if (typeof window !== 'undefined') {
|
||||
window.__firewallLoadTab = function(tab) {
|
||||
$scope.$evalAsync(function() {
|
||||
$scope.activeTab = tab;
|
||||
if (tab === 'banned') { populateBannedIPs(); } else { populateCurrentRecords(); }
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
// Load banned IPs on page load - use $timeout for Angular compatibility
|
||||
// Wrap in try-catch to ensure it executes even if there are other errors
|
||||
@@ -160,33 +278,6 @@ app.controller('firewallController', function ($scope, $http, $timeout) {
|
||||
console.error('Error setting up timeout for populateBannedIPs:', e);
|
||||
}
|
||||
|
||||
// Also load when switching to banned tab - use deep watch for immediate trigger
|
||||
try {
|
||||
$scope.$watch('activeTab', function(newVal, oldVal) {
|
||||
console.log('=== activeTab WATCH TRIGGERED ===');
|
||||
console.log('activeTab changed from', oldVal, 'to', newVal);
|
||||
if (newVal === 'banned') {
|
||||
console.log('Switched to banned IPs tab, calling populateBannedIPs...');
|
||||
// Call immediately
|
||||
try {
|
||||
if (typeof populateBannedIPs === 'function') {
|
||||
console.log('Calling populateBannedIPs from $watch...');
|
||||
populateBannedIPs();
|
||||
} else if (typeof $scope.populateBannedIPs === 'function') {
|
||||
console.log('Calling $scope.populateBannedIPs from $watch...');
|
||||
$scope.populateBannedIPs();
|
||||
} else {
|
||||
console.error('ERROR: populateBannedIPs is not available!');
|
||||
}
|
||||
} catch(e) {
|
||||
console.error('Error calling populateBannedIPs from watch:', e);
|
||||
}
|
||||
}
|
||||
}, true); // Use deep watch (true parameter)
|
||||
} catch(e) {
|
||||
console.error('Error setting up $watch for activeTab:', e);
|
||||
}
|
||||
|
||||
$scope.addRule = function () {
|
||||
|
||||
$scope.rulesLoading = false;
|
||||
@@ -278,39 +369,76 @@ app.controller('firewallController', function ($scope, $http, $timeout) {
|
||||
$scope.actionFailed = true;
|
||||
$scope.actionSuccess = true;
|
||||
|
||||
|
||||
url = "/firewall/getCurrentRules";
|
||||
|
||||
var data = {};
|
||||
|
||||
var data = {
|
||||
page: $scope.rulesPage || 1,
|
||||
page_size: $scope.rulesPageSize || 10
|
||||
};
|
||||
var config = {
|
||||
headers: {
|
||||
'X-CSRFToken': getCookie('csrftoken')
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
$http.post(url, data, config).then(ListInitialDatas, cantLoadInitialDatas);
|
||||
|
||||
|
||||
function ListInitialDatas(response) {
|
||||
if (response.data.fetchStatus === 1) {
|
||||
$scope.rules = JSON.parse(response.data.data);
|
||||
var res = (typeof response.data === 'string') ? (function() { try { return JSON.parse(response.data); } catch (e) { return {}; } })() : response.data;
|
||||
if (res && res.fetchStatus === 1) {
|
||||
$scope.rules = typeof res.data === 'string' ? JSON.parse(res.data) : (res.data || []);
|
||||
$scope.rulesTotalCount = res.total_count != null ? res.total_count : ($scope.rules ? $scope.rules.length : 0);
|
||||
$scope.rulesPage = Math.max(1, res.page != null ? res.page : 1);
|
||||
$scope.rulesPageSize = res.page_size != null ? res.page_size : 10;
|
||||
$scope.rulesLoading = true;
|
||||
}
|
||||
else {
|
||||
$scope.rulesLoading = true;
|
||||
$scope.errorMessage = response.data.error_message;
|
||||
$scope.errorMessage = (res && res.error_message) ? res.error_message : '';
|
||||
}
|
||||
}
|
||||
|
||||
function cantLoadInitialDatas(response) {
|
||||
$scope.couldNotConnect = false;
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
$scope.goToRulesPage = function(page) {
|
||||
var totalP = $scope.rulesTotalPages();
|
||||
if (page < 1 || page > totalP) return;
|
||||
$scope.rulesPage = page;
|
||||
populateCurrentRecords();
|
||||
};
|
||||
$scope.goToRulesPageByInput = function() {
|
||||
var n = parseInt($scope.rulesPageInput, 10);
|
||||
if (isNaN(n) || n < 1) n = 1;
|
||||
var maxP = $scope.rulesTotalPages();
|
||||
if (n > maxP) n = maxP;
|
||||
$scope.rulesPageInput = n;
|
||||
$scope.goToRulesPage(n);
|
||||
};
|
||||
$scope.rulesTotalPages = function() {
|
||||
var size = $scope.rulesPageSize || 10;
|
||||
var total = $scope.rulesTotalCount || ($scope.rules && $scope.rules.length) || 0;
|
||||
return size > 0 ? Math.max(1, Math.ceil(total / size)) : 1;
|
||||
};
|
||||
$scope.rulesRangeStart = function() {
|
||||
var total = $scope.rulesTotalCount || ($scope.rules && $scope.rules.length) || 0;
|
||||
if (total === 0) return 0;
|
||||
var page = Math.max(1, $scope.rulesPage || 1);
|
||||
var size = $scope.rulesPageSize || 10;
|
||||
return (page - 1) * size + 1;
|
||||
};
|
||||
$scope.rulesRangeEnd = function() {
|
||||
var start = $scope.rulesRangeStart();
|
||||
var size = $scope.rulesPageSize || 10;
|
||||
var total = $scope.rulesTotalCount || ($scope.rules && $scope.rules.length) || 0;
|
||||
return total === 0 ? 0 : Math.min(start + size - 1, total);
|
||||
};
|
||||
$scope.setRulesPageSize = function() {
|
||||
$scope.rulesPage = 1;
|
||||
populateCurrentRecords();
|
||||
};
|
||||
|
||||
$scope.deleteRule = function (id, proto, port, ruleIP) {
|
||||
|
||||
$scope.rulesLoading = false;
|
||||
@@ -2837,4 +2965,26 @@ app.controller('litespeed_ent_conf', function ($scope, $http, $timeout, $window)
|
||||
}
|
||||
}
|
||||
|
||||
});
|
||||
});
|
||||
|
||||
(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);
|
||||
})();
|
||||
@@ -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 %}
|
||||
@@ -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'),
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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');
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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!"
|
||||
589
static/baseTemplate/assets/mobile-responsive.css
Normal file
589
static/baseTemplate/assets/mobile-responsive.css
Normal 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;
|
||||
}
|
||||
}
|
||||
265
static/baseTemplate/assets/readability-fixes.css
Normal file
265
static/baseTemplate/assets/readability-fixes.css
Normal 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;
|
||||
}
|
||||
}
|
||||
@@ -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');
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
@@ -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];
|
||||
|
||||
@@ -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);
|
||||
})();
|
||||
@@ -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();
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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' });
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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 |
|
||||
|
||||
@@ -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`
|
||||
|
||||
Reference in New Issue
Block a user