mirror of
https://github.com/usmannasir/cyberpanel.git
synced 2026-02-16 19:46:48 +01:00
Misc: firewall, pluginHolder, mobile CSS, install utilities, static assets
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -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 = [
|
||||
|
||||
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>
|
||||
|
||||
@@ -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,
|
||||
|
||||
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);
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user