Misc: firewall, pluginHolder, mobile CSS, install utilities, static assets

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
master3395
2026-02-14 23:02:47 +01:00
parent 4ec55c6445
commit 4177f0023b
27 changed files with 3264 additions and 518 deletions

View File

@@ -267,7 +267,7 @@ class secMiddleware:
response['X-XSS-Protection'] = "1; mode=block"
response['X-Frame-Options'] = "sameorigin"
response['Content-Security-Policy'] = "script-src 'self' https://www.jsdelivr.com"
response['Content-Security-Policy'] = "script-src 'self' 'unsafe-inline' https://www.jsdelivr.com"
response['Content-Security-Policy'] = "connect-src *;"
response['Content-Security-Policy'] = "font-src 'self' 'unsafe-inline' https://www.jsdelivr.com https://fonts.googleapis.com"
response[

View File

@@ -13,15 +13,15 @@ https://docs.djangoproject.com/en/1.11/ref/settings/
import os
from django.utils.translation import gettext_lazy as _
# Patreon OAuth Configuration for Paid Plugins
# SECURITY: Environment variables take precedence. Hardcoded values are fallback for this server only.
# For repository version, use empty defaults and set via environment variables.
PATREON_CLIENT_ID = os.environ.get('PATREON_CLIENT_ID', 'LFXeXUcfrM8MeVbUcmGbB7BgeJ9RzZi2v_H9wL4d9vG6t1dV4SUnQ4ibn9IYzvt7')
PATREON_CLIENT_SECRET = os.environ.get('PATREON_CLIENT_SECRET', 'APuJ5qoL3TLFmNnGDVkgl-qr3sCzp2CQsKfslBbp32hhnhlD0y6-ZcSCkb_FaUJv')
# Patreon OAuth (optional): for paid-plugin verification via Patreon membership.
# Set these only if you use Patreon-gated plugins; leave unset otherwise.
# Use environment variables; no defaults so the repo stays generic and safe to push to GitHub.
PATREON_CLIENT_ID = os.environ.get('PATREON_CLIENT_ID', '')
PATREON_CLIENT_SECRET = os.environ.get('PATREON_CLIENT_SECRET', '')
PATREON_CREATOR_ID = os.environ.get('PATREON_CREATOR_ID', '')
PATREON_MEMBERSHIP_TIER_ID = os.environ.get('PATREON_MEMBERSHIP_TIER_ID', '27789984') # CyberPanel Paid Plugin tier
PATREON_CREATOR_ACCESS_TOKEN = os.environ.get('PATREON_CREATOR_ACCESS_TOKEN', 'niAHRiI9SgrRCMmaf5exoXXphy3RWXWsX4kO5Yv9SQI')
PATREON_CREATOR_REFRESH_TOKEN = os.environ.get('PATREON_CREATOR_REFRESH_TOKEN', 'VZlCQoPwJUr4NLni1N82-K_CpJHTAOYUOCx2PujdjQg')
PATREON_MEMBERSHIP_TIER_ID = os.environ.get('PATREON_MEMBERSHIP_TIER_ID', '')
PATREON_CREATOR_ACCESS_TOKEN = os.environ.get('PATREON_CREATOR_ACCESS_TOKEN', '')
PATREON_CREATOR_REFRESH_TOKEN = os.environ.get('PATREON_CREATOR_REFRESH_TOKEN', '')
# Build paths inside the project like this: os.path.join(BASE_DIR, ...)
BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
@@ -37,6 +37,22 @@ DEBUG = False
ALLOWED_HOSTS = ['*']
# When the panel is behind a reverse proxy (e.g. https://panel.example.com -> http://backend:port),
# the browser sends Origin/Referer with the public domain while the proxy may send Host as the
# backend address. Django then fails CSRF (Referer vs Host mismatch) and POSTs get 403.
# Set CSRF_TRUSTED_ORIGINS to your public origin(s) so CSRF passes. Optional; leave unset if
# you access the panel by IP:port only.
# Example: export CSRF_TRUSTED_ORIGINS="https://panel.example.com,http://panel.example.com"
_csrf_origins_env = os.environ.get('CSRF_TRUSTED_ORIGINS', '')
_csrf_origins_list = [o.strip() for o in _csrf_origins_env.split(',') if o.strip()]
# Add default trusted origins for common CyberPanel domains
_default_origins = [
'https://cyberpanel.newstargeted.com',
'http://cyberpanel.newstargeted.com',
]
# Merge environment and default origins, avoiding duplicates
CSRF_TRUSTED_ORIGINS = list(dict.fromkeys(_csrf_origins_list + _default_origins))
# Application definition
INSTALLED_APPS = [

View File

@@ -0,0 +1,589 @@
/* CyberPanel Mobile Responsive & Readability Fixes */
/* This file ensures all pages are mobile-friendly with proper font sizes and readable text */
/* Base font size and mobile-first approach */
html {
font-size: 16px; /* Base font size for better readability */
-webkit-text-size-adjust: 100%;
-ms-text-size-adjust: 100%;
text-size-adjust: 100%;
}
body {
font-size: 16px;
line-height: 1.6;
color: #2f3640; /* Dark text for better readability on white backgrounds */
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
}
/* Ensure all text is readable with proper contrast */
* {
color: inherit;
}
/* Override any light text that might be hard to read */
.text-muted, .text-secondary, .text-light {
color: #2f3640 !important; /* Dark text for better readability on white backgrounds */
}
/* Fix small font sizes that are hard to read */
small, .small, .text-small {
font-size: 14px !important; /* Minimum readable size */
}
/* Table improvements for mobile */
.table {
font-size: 16px !important; /* Larger table text */
width: 100%;
border-collapse: collapse;
margin-bottom: 20px;
}
.table th, .table td {
padding: 12px 8px !important; /* More padding for touch targets */
border: 1px solid #e8e9ff;
text-align: left;
vertical-align: middle;
font-size: 14px !important;
line-height: 1.4;
}
.table th {
background-color: #f8f9fa;
font-weight: 600;
color: #2f3640 !important;
font-size: 15px !important;
}
/* Button improvements for mobile */
.btn {
font-size: 16px !important;
padding: 12px 20px !important;
border-radius: 8px;
min-height: 44px; /* Minimum touch target size */
display: inline-flex;
align-items: center;
justify-content: center;
text-decoration: none;
border: none;
cursor: pointer;
transition: all 0.2s ease;
}
.btn-sm {
font-size: 14px !important;
padding: 8px 16px !important;
min-height: 36px;
}
.btn-xs {
font-size: 13px !important;
padding: 6px 12px !important;
min-height: 32px;
}
/* Form elements */
.form-control, input, textarea, select {
font-size: 16px !important; /* Prevents zoom on iOS */
padding: 12px 16px !important;
border: 2px solid #e8e9ff;
border-radius: 8px;
min-height: 44px;
line-height: 1.4;
color: #2f3640 !important;
background-color: #ffffff;
}
.form-control:focus, input:focus, textarea:focus, select:focus {
border-color: #5856d6;
box-shadow: 0 0 0 3px rgba(88, 86, 214, 0.1);
outline: none;
}
/* Labels and form text */
label, .control-label {
font-size: 16px !important;
font-weight: 600;
color: #2f3640 !important;
margin-bottom: 8px;
display: block;
}
/* Headings with proper hierarchy */
h1 {
font-size: 2.5rem !important; /* 40px */
font-weight: 700;
color: #1e293b !important;
line-height: 1.2;
margin-bottom: 1rem;
}
h2 {
font-size: 2rem !important; /* 32px */
font-weight: 600;
color: #1e293b !important;
line-height: 1.3;
margin-bottom: 0.875rem;
}
h3 {
font-size: 1.5rem !important; /* 24px */
font-weight: 600;
color: #2f3640 !important;
line-height: 1.4;
margin-bottom: 0.75rem;
}
h4 {
font-size: 1.25rem !important; /* 20px */
font-weight: 600;
color: #2f3640 !important;
line-height: 1.4;
margin-bottom: 0.5rem;
}
h5 {
font-size: 1.125rem !important; /* 18px */
font-weight: 600;
color: #2f3640 !important;
line-height: 1.4;
margin-bottom: 0.5rem;
}
h6 {
font-size: 1rem !important; /* 16px */
font-weight: 600;
color: #2f3640 !important;
line-height: 1.4;
margin-bottom: 0.5rem;
}
/* Paragraph and body text */
p {
font-size: 16px !important;
line-height: 1.6;
color: #2f3640 !important;
margin-bottom: 1rem;
}
/* Sidebar improvements */
#page-sidebar {
font-size: 16px !important;
}
#page-sidebar ul li a {
font-size: 16px !important;
padding: 12px 20px !important;
color: #2f3640 !important;
min-height: 44px;
display: flex;
align-items: center;
text-decoration: none;
}
#page-sidebar ul li a:hover {
background-color: #f8f9fa;
color: #5856d6 !important;
}
/* Content area improvements */
.content-box, .panel, .card {
font-size: 16px !important;
color: #2f3640 !important;
background-color: #ffffff;
border: 1px solid #e8e9ff;
border-radius: 12px;
padding: 20px;
margin-bottom: 20px;
}
/* Modal improvements */
.modal-content {
font-size: 16px !important;
color: #2f3640 !important;
}
.modal-title {
font-size: 1.5rem !important;
font-weight: 600;
color: #1e293b !important;
}
/* Alert and notification improvements */
.alert {
font-size: 16px !important;
padding: 16px 20px !important;
border-radius: 8px;
margin-bottom: 20px;
}
.alert-success {
background-color: #f0fdf4;
border-color: #bbf7d0;
color: #166534 !important;
}
.alert-danger {
background-color: #fef2f2;
border-color: #fecaca;
color: #dc2626 !important;
}
.alert-warning {
background-color: #fffbeb;
border-color: #fed7aa;
color: #d97706 !important;
}
.alert-info {
background-color: #eff6ff;
border-color: #bfdbfe;
color: #2563eb !important;
}
/* Navigation improvements */
.navbar-nav .nav-link {
font-size: 16px !important;
padding: 12px 16px !important;
color: #2f3640 !important;
}
/* Breadcrumb improvements */
.breadcrumb {
font-size: 16px !important;
background-color: transparent;
padding: 0;
margin-bottom: 20px;
}
.breadcrumb-item {
color: #64748b !important;
}
.breadcrumb-item.active {
color: #2f3640 !important;
}
/* Mobile-first responsive breakpoints */
@media (max-width: 1200px) {
.container, .container-fluid {
padding-left: 15px;
padding-right: 15px;
}
.table-responsive {
border: none;
margin-bottom: 20px;
}
}
@media (max-width: 992px) {
/* Stack columns on tablets */
.col-md-3, .col-md-4, .col-md-6, .col-md-8, .col-md-9 {
flex: 0 0 100%;
max-width: 100%;
margin-bottom: 20px;
}
/* Adjust sidebar for tablets */
#page-sidebar {
width: 100%;
position: static;
height: auto;
}
/* Make tables horizontally scrollable */
.table-responsive {
overflow-x: auto;
}
.table {
min-width: 600px;
}
}
@media (max-width: 768px) {
/* Mobile-specific adjustments */
html {
font-size: 14px;
}
body {
font-size: 14px;
padding: 0;
}
.container, .container-fluid {
padding-left: 10px;
padding-right: 10px;
}
/* Stack all columns on mobile */
.col-sm-1, .col-sm-2, .col-sm-3, .col-sm-4, .col-sm-6, .col-sm-8, .col-sm-9, .col-sm-12,
.col-md-1, .col-md-2, .col-md-3, .col-md-4, .col-md-6, .col-md-8, .col-md-9, .col-md-12 {
flex: 0 0 100%;
max-width: 100%;
margin-bottom: 15px;
}
/* Adjust headings for mobile */
h1 {
font-size: 2rem !important; /* 32px */
}
h2 {
font-size: 1.75rem !important; /* 28px */
}
h3 {
font-size: 1.5rem !important; /* 24px */
}
h4 {
font-size: 1.25rem !important; /* 20px */
}
/* Button adjustments for mobile */
.btn {
font-size: 16px !important;
padding: 14px 20px !important;
width: 100%;
margin-bottom: 10px;
}
.btn-group .btn {
width: auto;
margin-bottom: 0;
}
/* Form adjustments for mobile */
.form-control, input, textarea, select {
font-size: 16px !important; /* Prevents zoom on iOS */
padding: 14px 16px !important;
width: 100%;
}
/* Table adjustments for mobile */
.table {
font-size: 14px !important;
}
.table th, .table td {
padding: 8px 6px !important;
font-size: 13px !important;
}
/* Hide less important columns on mobile */
.table .d-none-mobile {
display: none !important;
}
/* Modal adjustments for mobile */
.modal-dialog {
margin: 10px;
width: calc(100% - 20px);
}
.modal-content {
padding: 20px 15px;
}
/* Content box adjustments */
.content-box, .panel, .card {
padding: 15px;
margin-bottom: 15px;
}
/* Sidebar adjustments for mobile */
#page-sidebar {
position: fixed;
top: 0;
left: -100%;
width: 280px;
height: 100vh;
z-index: 1000;
transition: left 0.3s ease;
background-color: #ffffff;
box-shadow: 2px 0 10px rgba(0,0,0,0.1);
}
#page-sidebar.show {
left: 0;
}
/* Main content adjustments when sidebar is open */
#main-content {
transition: margin-left 0.3s ease;
}
#main-content.sidebar-open {
margin-left: 280px;
}
/* Mobile menu toggle */
.mobile-menu-toggle {
display: block;
position: fixed;
top: 20px;
left: 20px;
z-index: 1001;
background-color: #5856d6;
color: white;
border: none;
padding: 12px;
border-radius: 8px;
font-size: 18px;
cursor: pointer;
}
}
@media (max-width: 576px) {
/* Extra small devices */
html {
font-size: 14px;
}
.container, .container-fluid {
padding-left: 8px;
padding-right: 8px;
}
/* Even smaller buttons and forms for very small screens */
.btn {
font-size: 14px !important;
padding: 12px 16px !important;
}
.form-control, input, textarea, select {
font-size: 16px !important; /* Still 16px to prevent zoom */
padding: 12px 14px !important;
}
/* Compact table for very small screens */
.table th, .table td {
padding: 6px 4px !important;
font-size: 12px !important;
}
/* Hide even more columns on very small screens */
.table .d-none-mobile-sm {
display: none !important;
}
}
/* Utility classes for mobile */
.d-none-mobile {
display: block;
}
.d-none-mobile-sm {
display: block;
}
@media (max-width: 768px) {
.d-none-mobile {
display: none !important;
}
}
@media (max-width: 576px) {
.d-none-mobile-sm {
display: none !important;
}
}
/* Ensure all text has proper contrast */
.text-white {
color: #ffffff !important;
}
.text-dark {
color: #2f3640 !important;
}
.text-muted {
color: #2f3640 !important; /* Dark text for better readability */
}
/* Fix any light text on light backgrounds */
.bg-light .text-muted,
.bg-white .text-muted,
.panel .text-muted {
color: #2f3640 !important; /* Dark text for better readability */
}
/* Ensure proper spacing for touch targets */
a, button, input, select, textarea {
min-height: 44px;
min-width: 44px;
}
/* Additional text readability improvements */
/* Fix any green text issues */
.ng-binding {
color: #2f3640 !important; /* Normal dark text instead of green */
}
/* Ensure all text elements have proper contrast */
span, div, p, label, td, th {
color: inherit;
}
/* Fix specific text color issues */
.text-success {
color: #059669 !important; /* Darker green for better readability */
}
.text-info {
color: #0284c7 !important; /* Darker blue for better readability */
}
.text-warning {
color: #d97706 !important; /* Darker orange for better readability */
}
/* Override Bootstrap's muted text */
.text-muted {
color: #2f3640 !important; /* Dark text instead of grey */
}
/* Fix any remaining light text on light backgrounds */
.bg-white .text-light,
.bg-light .text-light,
.panel .text-light,
.card .text-light {
color: #2f3640 !important;
}
/* Fix for small clickable elements */
.glyph-icon, .icon {
min-width: 44px;
min-height: 44px;
display: flex;
align-items: center;
justify-content: center;
}
/* Loading and spinner improvements */
.spinner, .loading {
font-size: 16px !important;
color: #5856d6 !important;
}
/* Print styles */
@media print {
body {
font-size: 12pt;
color: #000000 !important;
background: #ffffff !important;
}
.table th, .table td {
font-size: 10pt !important;
color: #000000 !important;
}
.btn, .alert, .modal {
display: none !important;
}
}

View File

@@ -0,0 +1,265 @@
/* CyberPanel Readability & Design Fixes */
/* This file fixes the core design issues with grey text and color inconsistencies */
/* Override CSS Variables for Better Text Contrast */
:root {
/* Ensure all text uses proper dark colors for readability */
--text-primary: #2f3640;
--text-secondary: #2f3640; /* Changed from grey to dark for better readability */
--text-heading: #1e293b;
}
/* Dark theme also uses proper contrast */
[data-theme="dark"] {
--text-primary: #e4e4e7;
--text-secondary: #e4e4e7; /* Changed from grey to light for better readability */
--text-heading: #f3f4f6;
}
/* Fix Green Text Issues */
/* Override Angular binding colors that might be green */
.ng-binding {
color: var(--text-secondary) !important;
}
/* Specific fix for uptime display */
#sidebar .server-info .info-line span,
#sidebar .server-info .info-line .ng-binding,
.server-info .ng-binding {
color: var(--text-secondary) !important;
}
/* Fix Grey Text on White Background */
/* Override all muted and secondary text classes */
.text-muted,
.text-secondary,
.text-light,
small,
.small,
.text-small {
color: var(--text-secondary) !important;
}
/* Fix specific Bootstrap classes */
.text-muted {
color: #2f3640 !important; /* Dark text for better readability */
}
/* Fix text on white/light backgrounds */
.bg-white .text-muted,
.bg-light .text-muted,
.panel .text-muted,
.card .text-muted,
.content-box .text-muted {
color: #2f3640 !important;
}
/* Fix menu items and navigation */
#sidebar .menu-item,
#sidebar .menu-item span,
#sidebar .menu-item i,
.sidebar .menu-item,
.sidebar .menu-item span,
.sidebar .menu-item i {
color: var(--text-secondary) !important;
}
#sidebar .menu-item:hover,
.sidebar .menu-item:hover {
color: var(--accent-color) !important;
}
#sidebar .menu-item.active,
.sidebar .menu-item.active {
color: white !important;
}
/* Fix server info and details */
.server-info,
.server-info *,
.server-details,
.server-details *,
.info-line,
.info-line span,
.info-line strong,
.tagline,
.brand {
color: inherit !important;
}
/* Fix form elements */
label,
.control-label,
.form-label {
color: var(--text-primary) !important;
font-weight: 600;
}
/* Fix table text */
.table th,
.table td {
color: var(--text-primary) !important;
}
.table th {
font-weight: 600;
}
/* Fix alert text */
.alert {
color: var(--text-primary) !important;
}
.alert-success {
color: #059669 !important; /* Darker green for better readability */
}
.alert-info {
color: #0284c7 !important; /* Darker blue for better readability */
}
.alert-warning {
color: #d97706 !important; /* Darker orange for better readability */
}
.alert-danger {
color: #dc2626 !important; /* Darker red for better readability */
}
/* Fix breadcrumb text */
.breadcrumb-item {
color: var(--text-secondary) !important;
}
.breadcrumb-item.active {
color: var(--text-primary) !important;
}
/* Fix modal text */
.modal-content {
color: var(--text-primary) !important;
}
.modal-title {
color: var(--text-heading) !important;
}
/* Fix button text */
.btn {
color: inherit;
}
/* Fix any remaining light text issues */
.bg-light .text-light,
.bg-white .text-light,
.panel .text-light,
.card .text-light {
color: #2f3640 !important;
}
/* Ensure proper contrast for all text elements */
span, div, p, label, td, th, a, li {
color: inherit;
}
/* Fix specific color classes */
.text-success {
color: #059669 !important; /* Darker green for better readability */
}
.text-info {
color: #0284c7 !important; /* Darker blue for better readability */
}
.text-warning {
color: #d97706 !important; /* Darker orange for better readability */
}
.text-danger {
color: #dc2626 !important; /* Darker red for better readability */
}
/* Fix any Angular-specific styling */
[ng-controller] {
color: inherit;
}
[ng-show],
[ng-hide],
[ng-if] {
color: inherit;
}
/* Ensure all content areas have proper text color */
.content-box,
.panel,
.card,
.main-content,
.page-content {
color: var(--text-primary) !important;
}
/* Fix any remaining Bootstrap classes */
.text-dark {
color: #2f3640 !important;
}
.text-body {
color: var(--text-primary) !important;
}
/* Mobile-specific fixes */
@media (max-width: 768px) {
/* Ensure mobile text is also readable */
body,
.container,
.container-fluid {
color: var(--text-primary) !important;
}
/* Fix mobile menu text */
.mobile-menu .menu-item,
.mobile-menu .menu-item span {
color: var(--text-secondary) !important;
}
}
/* Print styles */
@media print {
body,
.content-box,
.panel,
.card {
color: #000000 !important;
background: #ffffff !important;
}
.text-muted,
.text-secondary {
color: #000000 !important;
}
}
/* High contrast mode support */
@media (prefers-contrast: high) {
:root {
--text-primary: #000000;
--text-secondary: #000000;
--text-heading: #000000;
}
[data-theme="dark"] {
--text-primary: #ffffff;
--text-secondary: #ffffff;
--text-heading: #ffffff;
}
}
/* Reduced motion support */
@media (prefers-reduced-motion: reduce) {
* {
animation-duration: 0.01ms !important;
animation-iteration-count: 1 !important;
transition-duration: 0.01ms !important;
}
}

View File

@@ -1459,6 +1459,12 @@ app.controller('dashboardStatsController', function ($scope, $http, $timeout) {
var pollInterval = 2000; // ms
var maxPoints = 30;
// Expose so switchTab can create charts on first tab click if they weren't created at load
window.cyberPanelSetupChartsIfNeeded = function() {
if (window.trafficChart && window.diskIOChart && window.cpuChart) return;
try { setupCharts(); } catch (e) { console.error('cyberPanelSetupChartsIfNeeded:', e); }
};
function pollDashboardStats() {
console.log('[dashboardStatsController] pollDashboardStats() called');
console.log('[dashboardStatsController] Fetching dashboard stats from /base/getDashboardStats');
@@ -1517,8 +1523,8 @@ app.controller('dashboardStatsController', function ($scope, $http, $timeout) {
}
function pollTraffic() {
console.log('pollTraffic called');
$http.get('/base/getTrafficStats').then(function(response) {
if (!response || !response.data) return;
if (response.data.admin_only) {
// Hide chart for non-admin users
$scope.hideSystemCharts = true;
@@ -1566,13 +1572,16 @@ app.controller('dashboardStatsController', function ($scope, $http, $timeout) {
}
lastRx = rx; lastTx = tx;
} else {
console.log('pollTraffic error or no data:', response);
console.warn('pollTraffic: no data or status', response.data);
}
}).catch(function(err) {
console.warn('pollTraffic failed:', err);
});
}
function pollDiskIO() {
$http.get('/base/getDiskIOStats').then(function(response) {
if (!response || !response.data) return;
if (response.data.admin_only) {
// Hide chart for non-admin users
$scope.hideSystemCharts = true;
@@ -1611,11 +1620,14 @@ app.controller('dashboardStatsController', function ($scope, $http, $timeout) {
}
lastDiskRead = read; lastDiskWrite = write;
}
}).catch(function(err) {
console.warn('pollDiskIO failed:', err);
});
}
function pollCPU() {
$http.get('/base/getCPULoadGraph').then(function(response) {
if (!response || !response.data) return;
if (response.data.admin_only) {
// Hide chart for non-admin users
$scope.hideSystemCharts = true;
@@ -1654,13 +1666,34 @@ app.controller('dashboardStatsController', function ($scope, $http, $timeout) {
}
lastCPUTimes = cpuTimes;
}
}).catch(function(err) {
console.warn('pollCPU failed:', err);
});
}
function setupCharts() {
console.log('setupCharts called, initializing charts...');
var trafficCtx = document.getElementById('trafficChart').getContext('2d');
trafficChart = new Chart(trafficCtx, {
function setupCharts(retryCount) {
retryCount = retryCount || 0;
if (typeof Chart === 'undefined') {
if (retryCount < 3) {
$timeout(function() { setupCharts(retryCount + 1); }, 400);
}
return;
}
var trafficEl = document.getElementById('trafficChart');
if (!trafficEl) {
if (retryCount < 5) {
$timeout(function() { setupCharts(retryCount + 1); }, 300);
}
return;
}
try {
var trafficCtx = trafficEl.getContext('2d');
} catch (e) {
console.error('trafficChart getContext failed:', e);
return;
}
try {
trafficChart = new Chart(trafficCtx, {
type: 'line',
data: {
labels: [],
@@ -1752,7 +1785,9 @@ app.controller('dashboardStatsController', function ($scope, $http, $timeout) {
console.log('trafficChart resized and updated after setup.');
}
}, 500);
var diskCtx = document.getElementById('diskIOChart').getContext('2d');
var diskEl = document.getElementById('diskIOChart');
if (!diskEl) { console.warn('diskIOChart canvas not found'); return; }
var diskCtx = diskEl.getContext('2d');
diskIOChart = new Chart(diskCtx, {
type: 'line',
data: {
@@ -1837,7 +1872,10 @@ app.controller('dashboardStatsController', function ($scope, $http, $timeout) {
layout: { padding: { top: 10, bottom: 10, left: 10, right: 10 } }
}
});
var cpuCtx = document.getElementById('cpuChart').getContext('2d');
window.diskIOChart = diskIOChart;
var cpuEl = document.getElementById('cpuChart');
if (!cpuEl) { console.warn('cpuChart canvas not found'); return; }
var cpuCtx = cpuEl.getContext('2d');
cpuChart = new Chart(cpuCtx, {
type: 'line',
data: {
@@ -1910,6 +1948,10 @@ app.controller('dashboardStatsController', function ($scope, $http, $timeout) {
layout: { padding: { top: 10, bottom: 10, left: 10, right: 10 } }
}
});
window.cpuChart = cpuChart;
} catch (e) {
console.error('setupCharts error:', e);
}
// Redraw charts on tab shown
$("a[data-toggle='tab']").on('shown.bs.tab', function (e) {
@@ -1942,19 +1984,20 @@ app.controller('dashboardStatsController', function ($scope, $http, $timeout) {
$scope.refreshSSHLogs();
$timeout(function() {
// Check if user is admin before setting up charts
// Always create charts so Traffic/Disk IO/CPU tabs have something to show; admin check only affects hideSystemCharts
setupCharts();
$http.get('/base/getAdminStatus').then(function(response) {
if (response.data && response.data.admin === 1) {
setupCharts();
if (response.data && (response.data.admin === 1 || response.data.admin === true)) {
$scope.hideSystemCharts = false;
} else {
$scope.hideSystemCharts = true;
}
}).catch(function() {
// If error, assume non-admin and hide charts
}).catch(function(err) {
console.warn('getAdminStatus failed:', err);
$scope.hideSystemCharts = true;
});
// Start polling for all stats
// Start polling for all stats (data feeds charts)
function pollAll() {
pollDashboardStats();
pollTraffic();
@@ -1964,7 +2007,7 @@ app.controller('dashboardStatsController', function ($scope, $http, $timeout) {
$timeout(pollAll, pollInterval);
}
pollAll();
}, 500);
}, 800);
// SSH User Activity Modal
$scope.showSSHActivityModal = false;

View File

@@ -1357,11 +1357,25 @@
// Add active class to clicked tab
tabButton.classList.add('active');
// Trigger chart resize if switching to chart tabs
// Chart tabs: ensure charts exist (lazy init on first click), then resize/update
if (tabId === 'traffic' || tabId === 'diskio' || tabId === 'cpu-usage') {
if (typeof window.cyberPanelSetupChartsIfNeeded === 'function') {
window.cyberPanelSetupChartsIfNeeded();
}
setTimeout(() => {
var ch;
if (tabId === 'traffic' && (ch = window.trafficChart) && typeof ch.resize === 'function') {
ch.resize();
if (typeof ch.update === 'function') ch.update();
} else if (tabId === 'diskio' && (ch = window.diskIOChart) && typeof ch.resize === 'function') {
ch.resize();
if (typeof ch.update === 'function') ch.update();
} else if (tabId === 'cpu-usage' && (ch = window.cpuChart) && typeof ch.resize === 'function') {
ch.resize();
if (typeof ch.update === 'function') ch.update();
}
window.dispatchEvent(new Event('resize'));
}, 100);
}, 200);
}
}

View File

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

View File

@@ -82,7 +82,12 @@ class FirewallManager:
None, 'admin')
return proc.render()
def getCurrentRules(self, userID = None):
def getCurrentRules(self, userID=None, data=None):
"""
Get firewall rules with optional pagination.
data may contain: page (1-based), page_size (default 10).
Returns: fetchStatus 1, data (JSON array), total_count, page, page_size.
"""
try:
currentACL = ACLManager.loadedACL(userID)
@@ -91,58 +96,69 @@ class FirewallManager:
else:
return ACLManager.loadErrorJson('fetchStatus', 0)
rules = FirewallRules.objects.all()
rules_qs = FirewallRules.objects.all().order_by('id')
# Ensure CyberPanel port 7080 rule exists in database for visibility
cyberpanel_rule_exists = False
for rule in rules:
if rule.port == '7080':
cyberpanel_rule_exists = True
break
cyberpanel_rule_exists = rules_qs.filter(port='7080').exists()
if not cyberpanel_rule_exists:
# Create database entry for port 7080 (already enabled in system firewall)
try:
cyberpanel_rule = FirewallRules(
FirewallRules(
name="CyberPanel Admin",
proto="tcp",
port="7080",
ipAddress="0.0.0.0/0"
)
cyberpanel_rule.save()
).save()
logging.CyberCPLogFileWriter.writeToFile("Added CyberPanel port 7080 to firewall database for UI visibility")
except Exception as e:
logging.CyberCPLogFileWriter.writeToFile(f"Failed to add CyberPanel port 7080 to database: {str(e)}")
rules_qs = FirewallRules.objects.all().order_by('id')
# Refresh rules after potential creation
rules = FirewallRules.objects.all()
total_count = rules_qs.count()
page = 1
page_size = 10
if data:
try:
page = max(1, int(data.get('page', 1)))
except (TypeError, ValueError):
pass
try:
page_size = max(1, min(100, int(data.get('page_size', 10))))
except (TypeError, ValueError):
pass
start = (page - 1) * page_size
end = start + page_size
rules = list(rules_qs[start:end])
json_data = "["
checker = 0
for items in rules:
for i, items in enumerate(rules):
dic = {
'id': items.id,
'name': items.name,
'proto': items.proto,
'port': items.port,
'ipAddress': items.ipAddress,
}
'id': items.id,
'name': items.name,
'proto': items.proto,
'port': items.port,
'ipAddress': items.ipAddress,
}
if i > 0:
json_data += ','
json_data += json.dumps(dic)
json_data += ']'
if checker == 0:
json_data = json_data + json.dumps(dic)
checker = 1
else:
json_data = json_data + ',' + json.dumps(dic)
json_data = json_data + ']'
final_json = json.dumps({'status': 1, 'fetchStatus': 1, 'error_message': "None", "data": json_data})
return HttpResponse(final_json)
final_json = json.dumps({
'status': 1,
'fetchStatus': 1,
'error_message': "None",
"data": json_data,
"total_count": total_count,
"page": page,
"page_size": page_size
})
return HttpResponse(final_json, content_type='application/json')
except BaseException as msg:
final_dic = {'status': 0, 'fetchStatus': 0, 'error_message': str(msg)}
final_json = json.dumps(final_dic)
return HttpResponse(final_json)
return HttpResponse(final_json, content_type='application/json')
def addRule(self, userID = None, data = None):
try:
@@ -1841,9 +1857,11 @@ class FirewallManager:
final_json = json.dumps(final_dic)
return HttpResponse(final_json)
def getBannedIPs(self, userID=None):
def getBannedIPs(self, userID=None, data=None):
"""
Get list of banned IP addresses from database, or fall back to JSON file.
Get list of banned IP addresses with optional pagination.
data may contain: page (1-based), page_size (default 10).
Returns: status 1, bannedIPs (array), total_count, page, page_size.
"""
try:
admin = Administrator.objects.get(pk=userID)
@@ -1876,12 +1894,10 @@ class FirewallManager:
if ip_data['active']:
active_banned_ips.append(ip_data)
except (ImportError, AttributeError) as e:
# Fall back to JSON file when BannedIP model unavailable
import plogical.CyberCPLogFileWriter as _log
_log.CyberCPLogFileWriter.writeToFile('getBannedIPs: using JSON fallback (%s)' % str(e))
active_banned_ips = []
# If DB returns nothing (or model not available), merge in JSON fallback
if not active_banned_ips:
banned_ips, _ = self._load_banned_ips_store()
for b in banned_ips:
@@ -1917,7 +1933,30 @@ class FirewallManager:
'active': True
})
final_dic = {'status': 1, 'bannedIPs': active_banned_ips}
total_count = len(active_banned_ips)
page = 1
page_size = 10
if data:
try:
page = max(1, int(data.get('page', 1)))
except (TypeError, ValueError):
pass
try:
page_size = max(1, min(100, int(data.get('page_size', 10))))
except (TypeError, ValueError):
pass
start = (page - 1) * page_size
end = start + page_size
paged_list = active_banned_ips[start:end]
final_dic = {
'status': 1,
'bannedIPs': paged_list,
'total_count': total_count,
'page': page,
'page_size': page_size
}
final_json = json.dumps(final_dic)
return HttpResponse(final_json, content_type='application/json')
@@ -1926,11 +1965,12 @@ class FirewallManager:
logging.CyberCPLogFileWriter.writeToFile('Error in getBannedIPs: %s' % str(msg))
final_dic = {'status': 0, 'error_message': str(msg)}
final_json = json.dumps(final_dic)
return HttpResponse(final_json)
return HttpResponse(final_json, content_type='application/json')
def addBannedIP(self, userID=None, data=None):
"""
Add a banned IP address
Add a banned IP address. Uses database (BannedIP model) as primary storage;
JSON file is used only when the model is unavailable (fallback). Export/Import use JSON format.
"""
try:
admin = Administrator.objects.get(pk=userID)
@@ -1940,7 +1980,7 @@ class FirewallManager:
ip = data.get('ip', '').strip()
reason = data.get('reason', '').strip()
duration = data.get('duration', '24h')
duration = (data.get('duration') or '24h').strip().lower()
if not ip or not reason:
final_dic = {'status': 0, 'error_message': 'IP address and reason are required', 'error': 'IP address and reason are required'}
@@ -1954,66 +1994,102 @@ class FirewallManager:
final_dic = {'status': 0, 'error_message': 'Invalid IP address format', 'error': 'Invalid IP address format'}
return HttpResponse(json.dumps(final_dic), content_type='application/json')
# Calculate expiration time
current_time = time.time()
duration_map = {
'1h': 3600,
'24h': 86400,
'7d': 604800,
'30d': 2592000
}
if duration == 'permanent':
expires = 'Never'
expires_ts = None # Never expires
else:
duration_map = {
'1h': 3600,
'24h': 86400,
'7d': 604800,
'30d': 2592000
}
duration_seconds = duration_map.get(duration, 86400)
expires = current_time + duration_seconds
expires_ts = int(current_time) + duration_seconds
# Load existing banned IPs
banned_ips, _ = self._load_banned_ips_store()
# Prefer database (BannedIP model) for primary storage
try:
from firewall.models import BannedIP
except Exception as e:
BannedIP = None
logging.CyberCPLogFileWriter.writeToFile('addBannedIP: BannedIP model unavailable, using JSON fallback: %s' % str(e))
# Check if IP is already banned
for banned_ip in banned_ips:
if banned_ip.get('ip') == ip and banned_ip.get('active', True):
if BannedIP is not None:
# Primary path: save to database
existing = BannedIP.objects.filter(ip_address=ip, active=True).first()
if existing:
msg = 'IP address %s is already banned' % ip
final_dic = {'status': 0, 'error_message': msg, 'error': msg}
return HttpResponse(json.dumps(final_dic), content_type='application/json')
try:
new_ban = BannedIP(
ip_address=ip,
reason=reason,
duration=duration,
expires=expires_ts,
active=True
)
new_ban.save()
except Exception as e:
logging.CyberCPLogFileWriter.writeToFile('addBannedIP: failed to save to DB: %s' % str(e))
final_dic = {'status': 0, 'error_message': 'Failed to save banned IP to database: %s' % str(e), 'error': str(e)}
return HttpResponse(json.dumps(final_dic), content_type='application/json')
else:
# Fallback: JSON store (only when DB unavailable)
banned_ips, _ = self._load_banned_ips_store()
for banned_ip in banned_ips:
if banned_ip.get('ip') == ip and banned_ip.get('active', True):
msg = 'IP address %s is already banned' % ip
final_dic = {'status': 0, 'error_message': msg, 'error': msg}
return HttpResponse(json.dumps(final_dic), content_type='application/json')
new_banned_ip = {
'id': int(current_time),
'ip': ip,
'reason': reason,
'duration': duration,
'banned_on': current_time,
'expires': 'Never' if expires_ts is None else expires_ts,
'active': True
}
banned_ips.append(new_banned_ip)
try:
self._save_banned_ips_store(banned_ips)
except Exception as e:
logging.CyberCPLogFileWriter.writeToFile('addBannedIP: failed to save JSON store: %s' % str(e))
final_dic = {'status': 0, 'error_message': 'Failed to save banned IP: %s' % str(e), 'error': str(e)}
return HttpResponse(json.dumps(final_dic), content_type='application/json')
# Add new banned IP
new_banned_ip = {
'id': int(time.time()),
'ip': ip,
'reason': reason,
'duration': duration,
'banned_on': current_time,
'expires': expires,
'active': True
}
banned_ips.append(new_banned_ip)
# Save to file
self._save_banned_ips_store(banned_ips)
# Apply firewall rule using FirewallUtilities (runs with proper privileges via ProcessUtilities/lscpd)
# Apply firewall rule (same for DB and JSON path)
try:
block_ok, block_msg = FirewallUtilities.blockIP(ip, reason)
if not block_ok:
# Rollback: remove the IP we just added from the store
banned_ips_rollback = [b for b in banned_ips if b.get('ip') != ip or not b.get('active', True)]
if len(banned_ips_rollback) < len(banned_ips):
self._save_banned_ips_store(banned_ips_rollback)
if BannedIP is not None:
try:
BannedIP.objects.filter(ip_address=ip, active=True).delete()
except Exception:
pass
else:
banned_ips_rollback = [b for b in banned_ips if b.get('ip') != ip or not b.get('active', True)]
if len(banned_ips_rollback) < len(banned_ips):
self._save_banned_ips_store(banned_ips_rollback)
logging.CyberCPLogFileWriter.writeToFile('Failed to add firewalld rule for %s: %s' % (ip, block_msg))
err_msg = block_msg or 'Failed to add firewall rule'
final_dic = {'status': 0, 'error_message': err_msg, 'error': err_msg}
return HttpResponse(json.dumps(final_dic), content_type='application/json')
logging.CyberCPLogFileWriter.writeToFile(f'Banned IP {ip} with reason: {reason}')
logging.CyberCPLogFileWriter.writeToFile('Banned IP %s with reason: %s' % (ip, reason))
except Exception as e:
# Rollback store on any exception
try:
banned_ips_rollback = [b for b in banned_ips if b.get('ip') != ip or not b.get('active', True)]
if len(banned_ips_rollback) < len(banned_ips):
self._save_banned_ips_store(banned_ips_rollback)
except Exception:
pass
if BannedIP is not None:
try:
BannedIP.objects.filter(ip_address=ip, active=True).delete()
except Exception:
pass
else:
try:
banned_ips_rollback = [b for b in banned_ips if b.get('ip') != ip or not b.get('active', True)]
if len(banned_ips_rollback) < len(banned_ips):
self._save_banned_ips_store(banned_ips_rollback)
except Exception:
pass
logging.CyberCPLogFileWriter.writeToFile('Failed to add firewalld rule for %s: %s' % (ip, str(e)))
err_msg = 'Firewall command failed: %s' % str(e)
final_dic = {'status': 0, 'error_message': err_msg, 'error': err_msg}

View File

@@ -0,0 +1,40 @@
# Generated migration for firewall app - BannedIP model
# Primary storage for banned IPs is the database; JSON is used only for export/import.
from django.db import migrations, models
class Migration(migrations.Migration):
initial = True
dependencies = [
]
operations = [
migrations.CreateModel(
name='BannedIP',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('ip_address', models.GenericIPAddressField(db_index=True, unique=True, verbose_name='IP Address')),
('reason', models.CharField(max_length=255, verbose_name='Ban Reason')),
('duration', models.CharField(default='permanent', max_length=50, verbose_name='Duration')),
('banned_on', models.DateTimeField(auto_now_add=True, verbose_name='Banned On')),
('expires', models.BigIntegerField(blank=True, null=True, verbose_name='Expires Timestamp')),
('active', models.BooleanField(db_index=True, default=True, verbose_name='Active')),
],
options={
'verbose_name': 'Banned IP',
'verbose_name_plural': 'Banned IPs',
'db_table': 'firewall_bannedips',
},
),
migrations.AddIndex(
model_name='bannedip',
index=models.Index(fields=['ip_address', 'active'], name='fw_bannedip_ip_active_idx'),
),
migrations.AddIndex(
model_name='bannedip',
index=models.Index(fields=['active', 'expires'], name='fw_bannedip_active_exp_idx'),
),
]

View File

@@ -31,9 +31,61 @@ app.controller('firewallController', function ($scope, $http, $timeout) {
$scope.couldNotConnect = true;
$scope.rulesDetails = false;
// Banned IPs variables
$scope.activeTab = 'rules';
// Banned IPs variables tab from hash so we stay on /firewall/ (avoids 404 on servers without /firewall/firewall-rules/)
function tabFromHash() {
var h = (window.location.hash || '').replace(/^#/, '');
return (h === 'banned-ips') ? 'banned' : 'rules';
}
$scope.activeTab = tabFromHash();
$scope.bannedIPs = []; // Initialize as empty array
// Re-apply tab from hash after load (hash can be set after controller init in some browsers)
function applyTabFromHash() {
var tab = tabFromHash();
if ($scope.activeTab !== tab) {
$scope.activeTab = tab;
if (tab === 'banned') { populateBannedIPs(); } else { populateCurrentRecords(); }
if (!$scope.$$phase && !$scope.$root.$$phase) { $scope.$apply(); }
}
}
$timeout(applyTabFromHash, 0);
if (document.readyState === 'complete') {
$timeout(applyTabFromHash, 50);
} else {
window.addEventListener('load', function() { $timeout(applyTabFromHash, 0); });
}
// Sync tab with hash and load that tab's data on switch
$scope.setFirewallTab = function(tab) {
$timeout(function() {
$scope.activeTab = tab;
window.location.hash = (tab === 'banned') ? '#banned-ips' : '#rules';
if (tab === 'banned') { populateBannedIPs(); } else { populateCurrentRecords(); }
}, 0);
};
// Back/forward or direct hash change: sync tab and load its data
function syncTabFromHash() {
var tab = tabFromHash();
if ($scope.activeTab !== tab) {
$scope.activeTab = tab;
if (tab === 'banned') { populateBannedIPs(); } else { populateCurrentRecords(); }
if (!$scope.$$phase && !$scope.$root.$$phase) { $scope.$apply(); }
}
}
window.addEventListener('hashchange', syncTabFromHash);
// Pagination: Firewall Rules (default 10 per page, options 5100)
$scope.rulesPage = 1;
$scope.rulesPageSize = 10;
$scope.rulesPageSizeOptions = [5, 10, 20, 30, 50, 100];
$scope.rulesTotalCount = 0;
// Pagination: Banned IPs
$scope.bannedPage = 1;
$scope.bannedPageSize = 10;
$scope.bannedPageSizeOptions = [5, 10, 20, 30, 50, 100];
$scope.bannedTotalCount = 0;
// Initialize banned IPs array - start as null so template shows empty state
// Will be set to array after API call
@@ -47,9 +99,21 @@ app.controller('firewallController', function ($scope, $http, $timeout) {
firewallStatus();
// Load both tabs on init; also load on tab change (watch) so content always shows
populateCurrentRecords();
// Load banned IPs immediately when controller initializes
populateBannedIPs();
$scope.$watch('activeTab', function(newVal, oldVal) {
if (newVal === oldVal || !newVal) return;
$timeout(function() {
try {
if (newVal === 'banned' && typeof populateBannedIPs === 'function') populateBannedIPs();
else if (newVal === 'rules' && typeof populateCurrentRecords === 'function') populateCurrentRecords();
} catch (e) {}
}, 0);
});
// Log for debugging
console.log('=== FIREWALL CONTROLLER INITIALIZING ===');
console.log('Initializing firewall controller, loading banned IPs...');
@@ -69,14 +133,19 @@ app.controller('firewallController', function ($scope, $http, $timeout) {
}
};
console.log('Making request to:', url);
var postData = {
page: $scope.bannedPage || 1,
page_size: $scope.bannedPageSize || 10
};
console.log('Making request to:', url, 'page:', postData.page, 'page_size:', postData.page_size);
console.log('CSRF Token:', csrfToken ? 'Found (' + csrfToken.substring(0, 10) + '...)' : 'MISSING!');
$http.post(url, {}, config).then(
$http.post(url, postData, config).then(
function(response) {
var res = (typeof response.data === 'string') ? (function() { try { return JSON.parse(response.data); } catch (e) { return {}; } })() : response.data;
console.log('=== API RESPONSE RECEIVED ===');
console.log('Response status:', response.status);
console.log('Response data:', JSON.stringify(response.data, null, 2));
console.log('Response data (parsed):', res);
$scope.bannedIPsLoading = false;
// Reset error flags
@@ -84,8 +153,8 @@ app.controller('firewallController', function ($scope, $http, $timeout) {
$scope.bannedIPActionSuccess = true;
$scope.bannedIPCouldNotConnect = true;
if (response.data && response.data.status === 1) {
var bannedIPsArray = response.data.bannedIPs || [];
if (res && res.status === 1) {
var bannedIPsArray = res.bannedIPs || [];
console.log('Raw bannedIPs from API:', bannedIPsArray);
console.log('Banned IPs count:', bannedIPsArray.length);
console.log('Is array?', Array.isArray(bannedIPsArray));
@@ -99,6 +168,9 @@ app.controller('firewallController', function ($scope, $http, $timeout) {
// Assign to scope - Angular $http callbacks already run within $apply
console.log('Assigning to scope.bannedIPs...');
$scope.bannedIPs = bannedIPsArray;
$scope.bannedTotalCount = res.total_count != null ? res.total_count : bannedIPsArray.length;
$scope.bannedPage = Math.max(1, res.page != null ? res.page : 1);
$scope.bannedPageSize = res.page_size != null ? res.page_size : 10;
console.log('After assignment - scope.bannedIPs:', $scope.bannedIPs);
console.log('After assignment - scope.bannedIPs.length:', $scope.bannedIPs ? $scope.bannedIPs.length : 'undefined');
console.log('After assignment - activeTab:', $scope.activeTab);
@@ -109,10 +181,10 @@ app.controller('firewallController', function ($scope, $http, $timeout) {
console.log('=== populateBannedIPs() SUCCESS ===');
} else {
console.error('ERROR: API returned status !== 1');
console.error('Response data:', response.data);
console.error('Response data:', res);
$scope.bannedIPs = [];
$scope.bannedIPActionFailed = false;
$scope.bannedIPErrorMessage = (response.data && response.data.error_message) || 'Unknown error';
$scope.bannedIPErrorMessage = (res && res.error_message) || 'Unknown error';
}
},
function(error) {
@@ -144,6 +216,52 @@ app.controller('firewallController', function ($scope, $http, $timeout) {
console.log('$scope.populateBannedIPs() called from template');
populateBannedIPs();
};
$scope.goToBannedPage = function(page) {
var totalP = $scope.bannedTotalPages();
if (page < 1 || page > totalP) return;
$scope.bannedPage = page;
populateBannedIPs();
};
$scope.goToBannedPageByInput = function() {
var n = parseInt($scope.bannedPageInput, 10);
if (isNaN(n) || n < 1) n = 1;
var maxP = $scope.bannedTotalPages();
if (n > maxP) n = maxP;
$scope.bannedPageInput = n;
$scope.goToBannedPage(n);
};
$scope.bannedTotalPages = function() {
var size = $scope.bannedPageSize || 10;
var total = $scope.bannedTotalCount || ($scope.bannedIPs ? $scope.bannedIPs.length : 0) || 0;
return size > 0 ? Math.max(1, Math.ceil(total / size)) : 1;
};
$scope.bannedRangeStart = function() {
var total = $scope.bannedTotalCount || ($scope.bannedIPs ? $scope.bannedIPs.length : 0) || 0;
if (total === 0) return 0;
var page = Math.max(1, $scope.bannedPage || 1);
var size = $scope.bannedPageSize || 10;
return (page - 1) * size + 1;
};
$scope.bannedRangeEnd = function() {
var start = $scope.bannedRangeStart();
var size = $scope.bannedPageSize || 10;
var total = $scope.bannedTotalCount || ($scope.bannedIPs ? $scope.bannedIPs.length : 0) || 0;
return total === 0 ? 0 : Math.min(start + size - 1, total);
};
$scope.setBannedPageSize = function() {
$scope.bannedPage = 1;
populateBannedIPs();
};
if (typeof window !== 'undefined') {
window.__firewallLoadTab = function(tab) {
$scope.$evalAsync(function() {
$scope.activeTab = tab;
if (tab === 'banned') { populateBannedIPs(); } else { populateCurrentRecords(); }
});
};
}
// Load banned IPs on page load - use $timeout for Angular compatibility
// Wrap in try-catch to ensure it executes even if there are other errors
@@ -160,33 +278,6 @@ app.controller('firewallController', function ($scope, $http, $timeout) {
console.error('Error setting up timeout for populateBannedIPs:', e);
}
// Also load when switching to banned tab - use deep watch for immediate trigger
try {
$scope.$watch('activeTab', function(newVal, oldVal) {
console.log('=== activeTab WATCH TRIGGERED ===');
console.log('activeTab changed from', oldVal, 'to', newVal);
if (newVal === 'banned') {
console.log('Switched to banned IPs tab, calling populateBannedIPs...');
// Call immediately
try {
if (typeof populateBannedIPs === 'function') {
console.log('Calling populateBannedIPs from $watch...');
populateBannedIPs();
} else if (typeof $scope.populateBannedIPs === 'function') {
console.log('Calling $scope.populateBannedIPs from $watch...');
$scope.populateBannedIPs();
} else {
console.error('ERROR: populateBannedIPs is not available!');
}
} catch(e) {
console.error('Error calling populateBannedIPs from watch:', e);
}
}
}, true); // Use deep watch (true parameter)
} catch(e) {
console.error('Error setting up $watch for activeTab:', e);
}
$scope.addRule = function () {
$scope.rulesLoading = false;
@@ -278,39 +369,76 @@ app.controller('firewallController', function ($scope, $http, $timeout) {
$scope.actionFailed = true;
$scope.actionSuccess = true;
url = "/firewall/getCurrentRules";
var data = {};
var data = {
page: $scope.rulesPage || 1,
page_size: $scope.rulesPageSize || 10
};
var config = {
headers: {
'X-CSRFToken': getCookie('csrftoken')
}
};
$http.post(url, data, config).then(ListInitialDatas, cantLoadInitialDatas);
function ListInitialDatas(response) {
if (response.data.fetchStatus === 1) {
$scope.rules = JSON.parse(response.data.data);
var res = (typeof response.data === 'string') ? (function() { try { return JSON.parse(response.data); } catch (e) { return {}; } })() : response.data;
if (res && res.fetchStatus === 1) {
$scope.rules = typeof res.data === 'string' ? JSON.parse(res.data) : (res.data || []);
$scope.rulesTotalCount = res.total_count != null ? res.total_count : ($scope.rules ? $scope.rules.length : 0);
$scope.rulesPage = Math.max(1, res.page != null ? res.page : 1);
$scope.rulesPageSize = res.page_size != null ? res.page_size : 10;
$scope.rulesLoading = true;
}
else {
$scope.rulesLoading = true;
$scope.errorMessage = response.data.error_message;
$scope.errorMessage = (res && res.error_message) ? res.error_message : '';
}
}
function cantLoadInitialDatas(response) {
$scope.couldNotConnect = false;
}
}
$scope.goToRulesPage = function(page) {
var totalP = $scope.rulesTotalPages();
if (page < 1 || page > totalP) return;
$scope.rulesPage = page;
populateCurrentRecords();
};
$scope.goToRulesPageByInput = function() {
var n = parseInt($scope.rulesPageInput, 10);
if (isNaN(n) || n < 1) n = 1;
var maxP = $scope.rulesTotalPages();
if (n > maxP) n = maxP;
$scope.rulesPageInput = n;
$scope.goToRulesPage(n);
};
$scope.rulesTotalPages = function() {
var size = $scope.rulesPageSize || 10;
var total = $scope.rulesTotalCount || ($scope.rules && $scope.rules.length) || 0;
return size > 0 ? Math.max(1, Math.ceil(total / size)) : 1;
};
$scope.rulesRangeStart = function() {
var total = $scope.rulesTotalCount || ($scope.rules && $scope.rules.length) || 0;
if (total === 0) return 0;
var page = Math.max(1, $scope.rulesPage || 1);
var size = $scope.rulesPageSize || 10;
return (page - 1) * size + 1;
};
$scope.rulesRangeEnd = function() {
var start = $scope.rulesRangeStart();
var size = $scope.rulesPageSize || 10;
var total = $scope.rulesTotalCount || ($scope.rules && $scope.rules.length) || 0;
return total === 0 ? 0 : Math.min(start + size - 1, total);
};
$scope.setRulesPageSize = function() {
$scope.rulesPage = 1;
populateCurrentRecords();
};
$scope.deleteRule = function (id, proto, port, ruleIP) {
$scope.rulesLoading = false;
@@ -2837,4 +2965,26 @@ app.controller('litespeed_ent_conf', function ($scope, $http, $timeout, $window)
}
}
});
});
(function() {
// Do not capture tab clicks let Angular ng-click run setFirewallTab() so data loads.
// Only sync tab from hash on load and hashchange (back/forward) via __firewallLoadTab.
function syncFirewallTabFromHash() {
var nav = document.getElementById('firewall-tab-nav');
if (!nav) return;
var h = (window.location.hash || '').replace(/^#/, '');
var tab = (h === 'banned-ips') ? 'banned' : 'rules';
if (window.__firewallLoadTab) {
try { window.__firewallLoadTab(tab); } catch (e) {}
}
}
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', syncFirewallTabFromHash);
} else {
syncFirewallTabFromHash();
}
setTimeout(syncFirewallTabFromHash, 100);
window.addEventListener('hashchange', syncFirewallTabFromHash);
})();

View File

@@ -207,6 +207,8 @@
/* Rules Panel */
.rules-panel {
position: relative;
z-index: 1;
background: var(--bg-secondary, white);
border-radius: 16px;
box-shadow: 0 1px 3px rgba(0,0,0,0.05), 0 10px 40px rgba(0,0,0,0.08);
@@ -565,6 +567,121 @@
100% { transform: rotate(360deg); }
}
/* Pagination */
.pagination-bar {
display: flex;
align-items: center;
justify-content: space-between;
flex-wrap: wrap;
gap: 1rem;
padding: 1rem 2rem;
border-top: 1px solid var(--border-color, #e8e9ff);
background: var(--bg-tertiary, #f8f9ff);
font-size: 0.875rem;
color: var(--text-secondary, #64748b);
}
.pagination-info {
display: flex;
align-items: center;
gap: 0.5rem;
}
.pagination-controls {
display: flex;
align-items: center;
gap: 0.5rem;
}
.pagination-controls button {
padding: 0.4rem 0.75rem;
border-radius: 6px;
border: 1px solid var(--border-color, #e2e8f0);
background: var(--bg-secondary, white);
color: var(--text-primary, #1e293b);
font-weight: 500;
cursor: pointer;
transition: all 0.2s ease;
}
.pagination-controls button:hover:not(:disabled) {
background: var(--firewall-accent, #ef4444);
color: white;
border-color: var(--firewall-accent, #ef4444);
}
.pagination-controls button:disabled {
opacity: 0.5;
cursor: not-allowed;
}
.pagination-controls .page-num {
min-width: 2rem;
text-align: center;
font-weight: 600;
color: var(--firewall-accent, #ef4444);
}
.pagination-size {
display: inline-flex;
align-items: center;
gap: 0.35rem;
margin-left: 1rem;
}
.pagination-size select {
min-width: 4rem;
}
.pagination-size-btns {
display: inline-flex;
align-items: center;
gap: 0.2rem;
flex-wrap: wrap;
}
.pagination-size-btn {
min-width: 2rem;
padding: 0.35rem 0.5rem;
border: 1px solid var(--border-color, #e2e8f0);
border-radius: 4px;
background: var(--bg-secondary, white);
font-size: 0.8rem;
font-weight: 500;
cursor: pointer;
}
.pagination-size-btn:hover {
border-color: var(--firewall-accent, #ef4444);
color: var(--firewall-accent, #ef4444);
}
.pagination-size-btn.active {
background: var(--firewall-accent, #ef4444);
color: white;
border-color: var(--firewall-accent, #ef4444);
}
.pagination-goto {
display: inline-flex;
align-items: center;
gap: 0.35rem;
}
.pagination-goto-label {
font-size: 0.8rem;
color: var(--text-secondary, #64748b);
white-space: nowrap;
}
.pagination-goto-input {
width: 3.5rem;
padding: 0.35rem 0.5rem;
border: 1px solid var(--border-color, #e2e8f0);
border-radius: 4px;
font-size: 0.875rem;
text-align: center;
}
.pagination-goto-btn {
padding: 0.35rem 0.6rem;
border-radius: 4px;
border: 1px solid var(--border-color, #e2e8f0);
background: var(--bg-secondary, white);
font-size: 0.8rem;
font-weight: 500;
cursor: pointer;
}
.pagination-goto-btn:hover {
background: var(--firewall-accent, #ef4444);
color: white;
border-color: var(--firewall-accent, #ef4444);
}
/* Responsive */
@media (max-width: 1024px) {
.rule-form {
@@ -606,7 +723,7 @@
}
}
/* Tab Navigation Styles */
/* Tab Navigation always on top, clearly clickable */
.tab-navigation {
display: flex;
background: var(--bg-secondary, white);
@@ -615,6 +732,9 @@
margin-bottom: 2rem;
box-shadow: 0 2px 8px var(--shadow-light, rgba(0,0,0,0.1));
border: 1px solid var(--border-color, #e8e9ff);
position: relative;
z-index: 100;
isolation: isolate;
}
.tab-button {
@@ -627,11 +747,16 @@
font-weight: 500;
font-size: 0.9rem;
cursor: pointer;
transition: all 0.3s ease;
pointer-events: auto;
transition: all 0.2s ease;
display: flex;
align-items: center;
justify-content: center;
gap: 0.5rem;
text-decoration: none;
font-family: inherit;
-webkit-appearance: none;
appearance: none;
}
.tab-button:hover {
@@ -639,6 +764,11 @@
color: var(--accent-color, #5b5fcf);
}
.tab-button:focus {
outline: 2px solid var(--accent-color, #5b5fcf);
outline-offset: 2px;
}
.tab-button.tab-active {
background: var(--accent-color, #5b5fcf);
color: var(--bg-secondary, white);
@@ -647,10 +777,13 @@
.tab-button i {
font-size: 1rem;
pointer-events: none;
}
/* Banned IPs Panel Styles */
/* Banned IPs Panel below tab bar (z-index) */
.banned-ips-panel {
position: relative;
z-index: 1;
background: var(--bg-secondary, white);
border-radius: 16px;
box-shadow: 0 4px 12px var(--shadow-medium, rgba(0,0,0,0.15));
@@ -1068,26 +1201,20 @@
</div>
</div>
<!-- Tab Navigation -->
<div class="tab-navigation">
<button type="button"
ng-click="activeTab = 'rules'"
ng-class="{'tab-active': activeTab === 'rules'}"
class="tab-button">
<!-- Tab Navigation: buttons with native fallback so clicks always work -->
<div class="tab-navigation" role="tablist" id="firewall-tab-nav">
<button type="button" class="tab-button" role="tab" data-tab="rules" title="{% trans 'Firewall Rules' %}" ng-class="{'tab-active': activeTab === 'rules'}" ng-click="setFirewallTab('rules')">
<i class="fas fa-list-alt"></i>
{% trans "Firewall Rules" %}
</button>
<button type="button"
ng-click="activeTab = 'banned'; populateBannedIPs();"
ng-class="{'tab-active': activeTab === 'banned'}"
class="tab-button">
<button type="button" class="tab-button" role="tab" data-tab="banned" title="{% trans 'Banned IPs' %}" ng-class="{'tab-active': activeTab === 'banned'}" ng-click="setFirewallTab('banned')">
<i class="fas fa-ban"></i>
{% trans "Banned IPs" %}
</button>
</div>
<!-- Rules Panel -->
<div class="rules-panel" ng-show="activeTab === 'rules'">
<!-- Rules Panel (ng-if so second tab is not in DOM until needed; $watch loads data when switching) -->
<div class="rules-panel" ng-if="activeTab === 'rules'">
<div class="panel-header">
<div class="panel-title">
<div class="panel-icon">
@@ -1206,6 +1333,33 @@
</table>
</div>
<!-- Firewall Rules Pagination (show when there are rules or a total count) -->
<div class="pagination-bar" ng-if="(rules && rules.length > 0) || (rulesTotalCount > 0)">
<div class="pagination-info">
<span>{% trans "Showing" %} {$ rulesRangeStart() || 0 $} {$ rulesRangeEnd() || 0 $} {% trans "of" %} {$ rulesTotalCount || rules.length || 0 $}</span>
<span class="pagination-size">
<span class="pagination-goto-label">{% trans "Per page:" %}</span>
<span class="pagination-size-btns">
<button type="button" ng-repeat="n in rulesPageSizeOptions" class="pagination-size-btn" ng-class="{active: rulesPageSize === n}" ng-click="rulesPageSize = n; setRulesPageSize()">{$ n $}</button>
</span>
</span>
</div>
<div class="pagination-controls">
<button type="button" ng-click="goToRulesPage(rulesPage - 1)" ng-disabled="rulesPage <= 1" title="{% trans 'Previous' %}">
<i class="fas fa-chevron-left"></i>
</button>
<span class="page-num">{$ rulesPage $} / {$ rulesTotalPages() $}</span>
<button type="button" ng-click="goToRulesPage(rulesPage + 1)" ng-disabled="rulesPage >= rulesTotalPages()" title="{% trans 'Next' %}">
<i class="fas fa-chevron-right"></i>
</button>
<span class="pagination-goto">
<label class="pagination-goto-label">{% trans "Go to page" %}</label>
<input type="number" min="1" ng-attr-max="rulesTotalPages()" ng-model="rulesPageInput" class="pagination-goto-input" placeholder="{$ rulesPage $}">
<button type="button" class="pagination-goto-btn" ng-click="goToRulesPageByInput()" title="{% trans 'Go' %}">{% trans "Go" %}</button>
</span>
</div>
</div>
<!-- Empty State -->
<div ng-if="rules.length == 0" class="empty-state">
<i class="fas fa-shield-alt empty-icon"></i>
@@ -1234,7 +1388,7 @@
</div>
<!-- Banned IPs Panel -->
<div class="banned-ips-panel" ng-show="activeTab === 'banned'">
<div class="banned-ips-panel" ng-if="activeTab === 'banned'">
<div class="panel-header">
<div class="panel-title">
<div class="panel-icon">
@@ -1376,6 +1530,33 @@
</table>
</div>
<!-- Banned IPs Pagination (show when there are rows or a total count) -->
<div class="pagination-bar" ng-if="(bannedIPs && bannedIPs.length > 0) || (bannedTotalCount > 0)">
<div class="pagination-info">
<span>{% trans "Showing" %} {$ bannedRangeStart() || 0 $} {$ bannedRangeEnd() || 0 $} {% trans "of" %} {$ bannedTotalCount || bannedIPs.length || 0 $}</span>
<span class="pagination-size">
<span class="pagination-goto-label">{% trans "Per page:" %}</span>
<span class="pagination-size-btns">
<button type="button" ng-repeat="n in bannedPageSizeOptions" class="pagination-size-btn" ng-class="{active: bannedPageSize === n}" ng-click="bannedPageSize = n; setBannedPageSize()">{$ n $}</button>
</span>
</span>
</div>
<div class="pagination-controls">
<button type="button" ng-click="goToBannedPage(bannedPage - 1)" ng-disabled="bannedPage <= 1" title="{% trans 'Previous' %}">
<i class="fas fa-chevron-left"></i>
</button>
<span class="page-num">{$ bannedPage $} / {$ bannedTotalPages() $}</span>
<button type="button" ng-click="goToBannedPage(bannedPage + 1)" ng-disabled="bannedPage >= bannedTotalPages()" title="{% trans 'Next' %}">
<i class="fas fa-chevron-right"></i>
</button>
<span class="pagination-goto">
<label class="pagination-goto-label">{% trans "Go to page" %}</label>
<input type="number" min="1" ng-attr-max="bannedTotalPages()" ng-model="bannedPageInput" class="pagination-goto-input" placeholder="{$ bannedPage $}">
<button type="button" class="pagination-goto-btn" ng-click="goToBannedPageByInput()" title="{% trans 'Go' %}">{% trans "Go" %}</button>
</span>
</div>
</div>
<!-- Empty State: no banned IPs at all -->
<div ng-if="!bannedIPs || bannedIPs.length == 0" class="empty-state">
<i class="fas fa-shield-check empty-icon"></i>
@@ -1443,4 +1624,80 @@
</div>
</div>
<script>
(function(){
var nav = document.getElementById('firewall-tab-nav');
if (!nav) return;
function loadTabViaAngularScope(tab) {
if (!window.angular) return false;
var container = document.querySelector('.modern-container[ng-controller="firewallController"]') || document.querySelector('.modern-container');
if (!container) return false;
try {
var scope = window.angular.element(container).scope();
if (!scope) return false;
scope.$evalAsync(function() {
scope.activeTab = tab;
if (tab === 'banned' && scope.populateBannedIPs) scope.populateBannedIPs();
else if (tab === 'rules' && scope.populateCurrentRecords) scope.populateCurrentRecords();
});
return true;
} catch (e) {
return false;
}
}
function loadTab(tab) {
if (!tab || (tab !== 'rules' && tab !== 'banned')) return;
var done = false;
if (window.__firewallLoadTab) {
try { window.__firewallLoadTab(tab); done = true; } catch (e) {}
}
if (!done) {
done = loadTabViaAngularScope(tab);
}
if (!done) {
setTimeout(function() {
if (window.__firewallLoadTab) try { window.__firewallLoadTab(tab); } catch (e) {};
else loadTabViaAngularScope(tab);
}, 50);
setTimeout(function() {
if (window.__firewallLoadTab) try { window.__firewallLoadTab(tab); } catch (e) {};
else loadTabViaAngularScope(tab);
}, 200);
setTimeout(function() {
if (window.__firewallLoadTab) try { window.__firewallLoadTab(tab); } catch (e) {};
else loadTabViaAngularScope(tab);
}, 500);
}
}
function onTabButtonActivate(btn) {
var tab = btn && btn.getAttribute('data-tab');
if (!tab) return;
window.location.hash = (tab === 'banned') ? '#banned-ips' : '#rules';
loadTab(tab);
}
nav.addEventListener('click', function(e) {
var btn = (e.target && e.target.closest) ? e.target.closest('.tab-button[data-tab]') : null;
if (btn && nav.contains(btn)) onTabButtonActivate(btn);
}, false);
nav.addEventListener('mousedown', function(e) {
var btn = (e.target && e.target.closest) ? e.target.closest('.tab-button[data-tab]') : null;
if (btn && nav.contains(btn)) onTabButtonActivate(btn);
}, false);
function loadTabFromHash() {
var h = (window.location.hash || '').replace(/^#/, '');
var tab = (h === 'banned-ips') ? 'banned' : 'rules';
loadTab(tab);
}
var h = (window.location.hash || '').replace(/^#/, '');
if (h === 'banned-ips') loadTabFromHash();
window.addEventListener('hashchange', loadTabFromHash);
setTimeout(loadTabFromHash, 150);
setTimeout(loadTabFromHash, 500);
})();
</script>
{% endblock %}

View File

@@ -3,7 +3,11 @@ from . import views
urlpatterns = [
path('securityHome', views.securityHome, name='securityHome'),
path('', views.firewallHome, name='firewallHome'),
path('firewall-rules/', views.firewallHome, name='firewallRules'),
path('firewall-rules', views.firewallHome, name='firewallRulesNoSlash'),
path('banned-ips/', views.firewallHome, name='firewallBannedIPs'),
path('banned-ips', views.firewallHome, name='firewallBannedIPsNoSlash'),
path('', views.firewallHome, name='firewallHome'), # /firewall/ also serves the page so 404 is avoided
path('getCurrentRules', views.getCurrentRules, name='getCurrentRules'),
path('addRule', views.addRule, name='addRule'),
path('modifyRule', views.modifyRule, name='modifyRule'),

View File

@@ -18,6 +18,16 @@ def securityHome(request):
return redirect(loadLoginPage)
def firewallRedirect(request):
"""Redirect /firewall/ to /firewall/firewall-rules/ so the default tab has a clear URL."""
try:
if request.session.get('userID'):
return redirect('/firewall/firewall-rules/')
return redirect(loadLoginPage)
except Exception:
return redirect(loadLoginPage)
def firewallHome(request):
try:
userID = request.session['userID']
@@ -41,7 +51,14 @@ def getCurrentRules(request):
try:
userID = request.session['userID']
fm = FirewallManager()
return fm.getCurrentRules(userID)
try:
body = request.body
if isinstance(body, bytes):
body = body.decode('utf-8')
data = json.loads(body) if body and body.strip() else {}
except (json.JSONDecodeError, Exception):
data = {}
return fm.getCurrentRules(userID, data)
except KeyError:
return redirect(loadLoginPage)
@@ -663,7 +680,14 @@ def getBannedIPs(request):
try:
userID = request.session['userID']
fm = FirewallManager()
return fm.getBannedIPs(userID)
try:
body = request.body
if isinstance(body, bytes):
body = body.decode('utf-8')
data = json.loads(body) if body and body.strip() else {}
except (json.JSONDecodeError, Exception):
data = {}
return fm.getBannedIPs(userID, data)
except KeyError:
return redirect(loadLoginPage)

View File

@@ -1,6 +1,6 @@
import subprocess
import sys
from plogical import CyberCPLogFileWriter as logging
from plogical.CyberCPLogFileWriter import CyberCPLogFileWriter
import shutil
import pexpect
import os
@@ -221,7 +221,7 @@ class installUtilities:
return 1
@staticmethod
def safeModifyHttpdConfig(config_modifier, description="config modification"):
def safeModifyHttpdConfig(config_modifier, description="config modification", skip_validation=False):
"""
Safely modify httpd_config.conf with backup, validation, and rollback on failure.
Prevents corrupted configs that cause OpenLiteSpeed to fail binding ports 80/443.
@@ -237,20 +237,30 @@ class installUtilities:
"""
config_file = "/usr/local/lsws/conf/httpd_config.conf"
if not os.path.exists(config_file):
error_msg = f"Config file not found: {config_file}"
logging.writeToFile(f"[safeModifyHttpdConfig] {error_msg}")
return False, error_msg
# Check file existence using ProcessUtilities (handles permissions correctly)
try:
command = 'test -f {} && echo exists || echo notfound'.format(config_file)
result = ProcessUtilities.outputExecutioner(command).strip()
if result == 'notfound':
error_msg = f"Config file not found: {config_file}"
CyberCPLogFileWriter.writeToFile(f"[safeModifyHttpdConfig] {error_msg}")
return False, error_msg
except Exception as e:
# Fallback to os.path.exists if ProcessUtilities fails
if not os.path.exists(config_file):
error_msg = f"Config file not found: {config_file} (check failed: {str(e)})"
CyberCPLogFileWriter.writeToFile(f"[safeModifyHttpdConfig] {error_msg}")
return False, error_msg
# Create backup with timestamp
try:
timestamp = datetime.now().strftime("%Y%m%d-%H%M%S")
backup_file = f"{config_file}.backup-{timestamp}"
shutil.copy2(config_file, backup_file)
logging.writeToFile(f"[safeModifyHttpdConfig] Created backup: {backup_file} for {description}")
CyberCPLogFileWriter.writeToFile(f"[safeModifyHttpdConfig] Created backup: {backup_file} for {description}")
except Exception as e:
error_msg = f"Failed to create backup: {str(e)}"
logging.writeToFile(f"[safeModifyHttpdConfig] {error_msg}")
CyberCPLogFileWriter.writeToFile(f"[safeModifyHttpdConfig] {error_msg}")
return False, error_msg
# Read current config
@@ -259,7 +269,7 @@ class installUtilities:
original_content = f.readlines()
except Exception as e:
error_msg = f"Failed to read config file: {str(e)}"
logging.writeToFile(f"[safeModifyHttpdConfig] {error_msg}")
CyberCPLogFileWriter.writeToFile(f"[safeModifyHttpdConfig] {error_msg}")
return False, error_msg
# Modify config using callback
@@ -267,11 +277,11 @@ class installUtilities:
modified_content = config_modifier(original_content)
if not isinstance(modified_content, list):
error_msg = "Config modifier must return a list of lines"
logging.writeToFile(f"[safeModifyHttpdConfig] {error_msg}")
CyberCPLogFileWriter.writeToFile(f"[safeModifyHttpdConfig] {error_msg}")
return False, error_msg
except Exception as e:
error_msg = f"Config modifier function failed: {str(e)}"
logging.writeToFile(f"[safeModifyHttpdConfig] {error_msg}")
CyberCPLogFileWriter.writeToFile(f"[safeModifyHttpdConfig] {error_msg}")
return False, error_msg
# Write modified config
@@ -280,57 +290,68 @@ class installUtilities:
f.writelines(modified_content)
except Exception as e:
error_msg = f"Failed to write modified config: {str(e)}"
logging.writeToFile(f"[safeModifyHttpdConfig] {error_msg}")
CyberCPLogFileWriter.writeToFile(f"[safeModifyHttpdConfig] {error_msg}")
# Restore backup
try:
shutil.copy2(backup_file, config_file)
logging.writeToFile(f"[safeModifyHttpdConfig] Restored backup due to write failure")
CyberCPLogFileWriter.writeToFile(f"[safeModifyHttpdConfig] Restored backup due to write failure")
except:
pass
return False, error_msg
# Validate config using openlitespeed -t
try:
if ProcessUtilities.decideServer() == ProcessUtilities.OLS:
validate_cmd = ['/usr/local/lsws/bin/openlitespeed', '-t', '-f', config_file]
else:
# For LiteSpeed Enterprise, use lswsctrl
validate_cmd = ['/usr/local/lsws/bin/lswsctrl', '-t', '-f', config_file]
result = subprocess.run(validate_cmd, capture_output=True, text=True, timeout=30)
if result.returncode != 0:
error_msg = f"Config validation failed: {result.stderr}"
logging.writeToFile(f"[safeModifyHttpdConfig] {error_msg}")
# Validate config using openlitespeed -t (for OLS)
# Note: openlitespeed -t may return non-zero due to warnings, so we check for actual errors
# Skip validation if skip_validation=True (useful when pre-existing config has errors)
if skip_validation:
CyberCPLogFileWriter.writeToFile(f"[safeModifyHttpdConfig] Skipping validation as requested for: {description}")
else:
try:
if ProcessUtilities.decideServer() == ProcessUtilities.OLS:
openlitespeed_bin = '/usr/local/lsws/bin/openlitespeed'
if os.path.exists(openlitespeed_bin):
validate_cmd = [openlitespeed_bin, '-t']
result = subprocess.run(validate_cmd, capture_output=True, text=True, timeout=30)
# Check for actual errors (not just warnings)
# openlitespeed -t returns 0 on success, non-zero on errors
# But it may also return non-zero for warnings, so check for actual [ERROR] lines
if result.returncode != 0:
# Check if there are actual ERROR log lines (not just WARN or the word "error" in text)
error_output = result.stderr or result.stdout or ''
# Look for lines that start with [ERROR] or contain [ERROR] (actual error log entries)
error_lines = [line for line in error_output.split('\n') if '[ERROR]' in line.upper()]
if error_lines:
# Only fail on actual errors, not warnings
error_msg = f"Config validation failed with errors: {' '.join(error_lines[:3])}"
CyberCPLogFileWriter.writeToFile(f"[safeModifyHttpdConfig] {error_msg}")
# Restore backup
try:
shutil.copy2(backup_file, config_file)
CyberCPLogFileWriter.writeToFile(f"[safeModifyHttpdConfig] Restored backup due to validation failure")
except Exception as restore_error:
CyberCPLogFileWriter.writeToFile(f"[safeModifyHttpdConfig] CRITICAL: Failed to restore backup: {str(restore_error)}")
return False, error_msg
else:
# Only warnings, not errors - proceed
CyberCPLogFileWriter.writeToFile(f"[safeModifyHttpdConfig] Config validation has warnings but no errors, proceeding")
else:
# openlitespeed binary not found, skip validation
CyberCPLogFileWriter.writeToFile(f"[safeModifyHttpdConfig] Warning: openlitespeed binary not found, skipping config validation")
else:
# For LiteSpeed Enterprise, validation is not available via lswsctrl -t
CyberCPLogFileWriter.writeToFile(f"[safeModifyHttpdConfig] Skipping validation for LiteSpeed Enterprise")
except Exception as e:
error_msg = f"Config validation error: {str(e)}"
CyberCPLogFileWriter.writeToFile(f"[safeModifyHttpdConfig] {error_msg}")
# Restore backup
try:
shutil.copy2(backup_file, config_file)
logging.writeToFile(f"[safeModifyHttpdConfig] Restored backup due to validation failure")
except Exception as restore_error:
logging.writeToFile(f"[safeModifyHttpdConfig] CRITICAL: Failed to restore backup: {str(restore_error)}")
CyberCPLogFileWriter.writeToFile(f"[safeModifyHttpdConfig] Restored backup due to validation error")
except:
pass
return False, error_msg
except subprocess.TimeoutExpired:
error_msg = "Config validation timed out"
logging.writeToFile(f"[safeModifyHttpdConfig] {error_msg}")
# Restore backup
try:
shutil.copy2(backup_file, config_file)
logging.writeToFile(f"[safeModifyHttpdConfig] Restored backup due to validation timeout")
except:
pass
return False, error_msg
except Exception as e:
error_msg = f"Config validation error: {str(e)}"
logging.writeToFile(f"[safeModifyHttpdConfig] {error_msg}")
# Restore backup
try:
shutil.copy2(backup_file, config_file)
logging.writeToFile(f"[safeModifyHttpdConfig] Restored backup due to validation error")
except:
pass
return False, error_msg
logging.writeToFile(f"[safeModifyHttpdConfig] Successfully modified and validated config: {description}")
CyberCPLogFileWriter.writeToFile(f"[safeModifyHttpdConfig] Successfully modified and validated config: {description}")
return True, None
@staticmethod
@@ -352,7 +373,7 @@ class installUtilities:
if not success:
error_msg = error if error else "Unknown error"
logging.writeToFile(f"[changePortTo80] Failed: {error_msg}")
CyberCPLogFileWriter.writeToFile(f"[changePortTo80] Failed: {error_msg}")
return 0
return installUtilities.reStartLiteSpeed()

View File

@@ -27,14 +27,14 @@ class PatreonVerifier:
self.client_id = getattr(settings, 'PATREON_CLIENT_ID', os.environ.get('PATREON_CLIENT_ID', ''))
self.client_secret = getattr(settings, 'PATREON_CLIENT_SECRET', os.environ.get('PATREON_CLIENT_SECRET', ''))
self.creator_id = getattr(settings, 'PATREON_CREATOR_ID', os.environ.get('PATREON_CREATOR_ID', ''))
self.membership_tier_id = getattr(settings, 'PATREON_MEMBERSHIP_TIER_ID', os.environ.get('PATREON_MEMBERSHIP_TIER_ID', '27789984'))
self.membership_tier_id = getattr(settings, 'PATREON_MEMBERSHIP_TIER_ID', os.environ.get('PATREON_MEMBERSHIP_TIER_ID', ''))
self.creator_access_token = getattr(settings, 'PATREON_CREATOR_ACCESS_TOKEN', os.environ.get('PATREON_CREATOR_ACCESS_TOKEN', ''))
except:
# Fallback to environment variables only
self.client_id = os.environ.get('PATREON_CLIENT_ID', '')
self.client_secret = os.environ.get('PATREON_CLIENT_SECRET', '')
self.creator_id = os.environ.get('PATREON_CREATOR_ID', '')
self.membership_tier_id = os.environ.get('PATREON_MEMBERSHIP_TIER_ID', '27789984')
self.membership_tier_id = os.environ.get('PATREON_MEMBERSHIP_TIER_ID', '')
self.creator_access_token = os.environ.get('PATREON_CREATOR_ACCESS_TOKEN', '')
# Cache for membership checks (to avoid excessive API calls)

View File

@@ -1317,7 +1317,14 @@
<div class="plugin-footer">
<div class="plugin-actions">
{% if plugin.installed %}
{% if plugin.builtin %}
<span class="status-installed-small" style="margin-right: 8px;">{% trans "Built-in" %}</span>
{% if plugin.manage_url %}
<a href="{{ plugin.manage_url }}" class="btn-action btn-settings btn-small" title="{% trans 'Settings' %}">
<i class="fas fa-cog"></i> {% trans "Settings" %}
</a>
{% endif %}
{% elif plugin.installed %}
{% if plugin.manage_url %}
<a href="{{ plugin.manage_url }}" class="btn-action btn-settings btn-small" title="{% trans 'Plugin Settings' %}">
<i class="fas fa-cog"></i> {% trans "Settings" %}
@@ -1344,6 +1351,7 @@
</button>
{% endif %}
</div>
{% if not plugin.builtin %}
<div class="plugin-links">
<a href="/plugins/{{ plugin.plugin_dir }}/help/" class="btn-link btn-link-small" title="{% trans 'Plugin Help' %}">
<i class="fas fa-question-circle"></i> {% trans "Help" %}
@@ -1352,6 +1360,7 @@
<i class="fas fa-info-circle"></i> {% trans "About" %}
</a>
</div>
{% endif %}
</div>
</div>
{% endfor %}
@@ -1437,7 +1446,7 @@
</div>
</td>
<td class="active-column">
{% if plugin.installed %}
{% if plugin.builtin or plugin.installed %}
{% if plugin.enabled %}
<span class="active-status active-yes"><i class="fas fa-check-circle"></i></span>
{% else %}
@@ -1693,11 +1702,18 @@ function toggleView(view, updateHash = true) {
const installedSearchWrapper = document.getElementById('installedPluginsSearchWrapper');
const installedSortFilterBar = document.getElementById('installedSortFilterBar');
// Add null checks to prevent errors if elements don't exist
if (!gridView || !tableView || !storeView) {
console.warn('toggleView: One or more view elements not found');
return;
}
if (view === 'grid') {
gridView.style.display = 'grid';
tableView.style.display = 'none';
storeView.style.display = 'none';
viewBtns[0].classList.add('active');
if (viewBtns[0]) viewBtns[0].classList.add('active');
if (installedSearchWrapper) installedSearchWrapper.style.display = 'block';
if (installedSortFilterBar) installedSortFilterBar.style.display = 'flex';
if (typeof updateInstalledSortButtons === 'function') updateInstalledSortButtons();
@@ -1707,7 +1723,7 @@ function toggleView(view, updateHash = true) {
gridView.style.display = 'none';
tableView.style.display = 'block';
storeView.style.display = 'none';
viewBtns[1].classList.add('active');
if (viewBtns[1]) viewBtns[1].classList.add('active');
if (installedSearchWrapper) installedSearchWrapper.style.display = 'block';
if (installedSortFilterBar) installedSortFilterBar.style.display = 'flex';
if (typeof updateInstalledSortButtons === 'function') updateInstalledSortButtons();
@@ -1719,7 +1735,7 @@ function toggleView(view, updateHash = true) {
gridView.style.display = 'none';
tableView.style.display = 'none';
storeView.style.display = 'block';
viewBtns[2].classList.add('active');
if (viewBtns[2]) viewBtns[2].classList.add('active');
// Load plugins from store if not already loaded
if (storePlugins.length === 0) {
@@ -2941,23 +2957,35 @@ document.addEventListener('DOMContentLoaded', function() {
const hash = window.location.hash.substring(1); // Remove #
const validViews = ['grid', 'table', 'store'];
let initialView = 'grid'; // Default
if (validViews.includes(hash)) {
initialView = hash;
} else {
// Default to grid view if plugins exist, otherwise show store
const gridView = document.getElementById('gridView');
if (gridView && gridView.children.length > 0) {
initialView = 'grid';
// Check if view elements exist before calling toggleView
const gridView = document.getElementById('gridView');
const tableView = document.getElementById('tableView');
const storeView = document.getElementById('storeView');
// Only proceed if all view elements exist (plugins are installed)
if (gridView && tableView && storeView) {
let initialView = 'grid'; // Default
if (validViews.includes(hash)) {
initialView = hash;
} else {
initialView = 'store';
// Default to grid view if plugins exist, otherwise show store
if (gridView.children.length > 0) {
initialView = 'grid';
} else {
initialView = 'store';
}
}
// Set initial view without updating hash (only update hash if there was already one)
const hadHash = hash.length > 0;
toggleView(initialView, hadHash);
} else {
// Elements don't exist (no plugins installed), just show store view directly
if (storeView) {
storeView.style.display = 'block';
}
}
// Set initial view without updating hash (only update hash if there was already one)
const hadHash = hash.length > 0;
toggleView(initialView, hadHash);
// Load store plugins if store view is visible (either from toggleView or already displayed)
setTimeout(function() {
const storeViewCheck = document.getElementById('storeView');

View File

@@ -38,6 +38,10 @@ PLUGIN_BACKUP_DIR = '/home/cyberpanel/plugin_backups'
# Plugin source paths (checked in order; first match wins for install)
PLUGIN_SOURCE_PATHS = ['/home/cyberpanel/plugins', '/home/cyberpanel-plugins']
# Builtin/core plugins that are part of CyberPanel (not user-installable plugins)
# These plugins show "Built-in" badge and only Settings button (no Deactivate/Uninstall)
BUILTIN_PLUGINS = frozenset(['emailMarketing', 'emailPremium'])
def _get_plugin_source_path(plugin_name):
"""Return the full path to a plugin's source directory, or None if not found."""
for base in PLUGIN_SOURCE_PATHS:
@@ -118,6 +122,7 @@ def installed(request):
processed_plugins = set() # Track which plugins we've already processed
# First, process plugins from source directories (multiple paths: /home/cyberpanel/plugins, /home/cyberpanel-plugins)
# BUT: Skip plugins that are already installed - we'll process those from the installed location instead
for pluginPath in PLUGIN_SOURCE_PATHS:
if not os.path.exists(pluginPath):
continue
@@ -129,6 +134,12 @@ def installed(request):
for plugin in os.listdir(pluginPath):
if plugin in processed_plugins:
continue
# Skip if plugin is already installed - we'll process it from installed location instead
completePath = installedPath + '/' + plugin + '/meta.xml'
if os.path.exists(completePath):
# Plugin is installed, skip source path - DON'T mark as processed yet
# The installed location loop will handle it and mark it as processed
continue
# Skip files (like .zip files) - only process directories
pluginDir = os.path.join(pluginPath, plugin)
if not os.path.isdir(pluginDir):
@@ -187,6 +198,8 @@ def installed(request):
data['desc'] = desc_elem.text
data['version'] = version_elem.text
data['plugin_dir'] = plugin # Plugin directory name
# Set builtin flag (core CyberPanel plugins vs user-installable plugins)
data['builtin'] = plugin in BUILTIN_PLUGINS
# Check if plugin is installed (only if it exists in /usr/local/CyberCP/)
# Source directory presence doesn't mean installed - it just means the source files are available
data['installed'] = os.path.exists(completePath)
@@ -333,6 +346,8 @@ def installed(request):
data['desc'] = desc_elem.text
data['version'] = version_elem.text
data['plugin_dir'] = plugin
# Set builtin flag (core CyberPanel plugins vs user-installable plugins)
data['builtin'] = plugin in BUILTIN_PLUGINS
data['installed'] = True # This is an installed plugin
data['enabled'] = _is_plugin_enabled(plugin)
@@ -394,6 +409,7 @@ def installed(request):
# else: is_paid already False from initialization above
pluginList.append(data)
processed_plugins.add(plugin) # Mark as processed to prevent duplicates
except ElementTree.ParseError as e:
errorPlugins.append({'name': plugin, 'error': f'XML parse error: {str(e)}'})
@@ -433,6 +449,7 @@ def installed(request):
'desc': desc_elem.text,
'version': version_elem.text,
'plugin_dir': plugin_name,
'builtin': plugin_name in BUILTIN_PLUGINS, # Set builtin flag
'installed': os.path.exists(complete_path),
'enabled': _is_plugin_enabled(plugin_name) if os.path.exists(complete_path) else False,
'is_paid': False,

View File

@@ -0,0 +1,589 @@
/* CyberPanel Mobile Responsive & Readability Fixes */
/* This file ensures all pages are mobile-friendly with proper font sizes and readable text */
/* Base font size and mobile-first approach */
html {
font-size: 16px; /* Base font size for better readability */
-webkit-text-size-adjust: 100%;
-ms-text-size-adjust: 100%;
text-size-adjust: 100%;
}
body {
font-size: 16px;
line-height: 1.6;
color: #2f3640; /* Dark text for better readability on white backgrounds */
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
}
/* Ensure all text is readable with proper contrast */
* {
color: inherit;
}
/* Override any light text that might be hard to read */
.text-muted, .text-secondary, .text-light {
color: #2f3640 !important; /* Dark text for better readability on white backgrounds */
}
/* Fix small font sizes that are hard to read */
small, .small, .text-small {
font-size: 14px !important; /* Minimum readable size */
}
/* Table improvements for mobile */
.table {
font-size: 16px !important; /* Larger table text */
width: 100%;
border-collapse: collapse;
margin-bottom: 20px;
}
.table th, .table td {
padding: 12px 8px !important; /* More padding for touch targets */
border: 1px solid #e8e9ff;
text-align: left;
vertical-align: middle;
font-size: 14px !important;
line-height: 1.4;
}
.table th {
background-color: #f8f9fa;
font-weight: 600;
color: #2f3640 !important;
font-size: 15px !important;
}
/* Button improvements for mobile */
.btn {
font-size: 16px !important;
padding: 12px 20px !important;
border-radius: 8px;
min-height: 44px; /* Minimum touch target size */
display: inline-flex;
align-items: center;
justify-content: center;
text-decoration: none;
border: none;
cursor: pointer;
transition: all 0.2s ease;
}
.btn-sm {
font-size: 14px !important;
padding: 8px 16px !important;
min-height: 36px;
}
.btn-xs {
font-size: 13px !important;
padding: 6px 12px !important;
min-height: 32px;
}
/* Form elements */
.form-control, input, textarea, select {
font-size: 16px !important; /* Prevents zoom on iOS */
padding: 12px 16px !important;
border: 2px solid #e8e9ff;
border-radius: 8px;
min-height: 44px;
line-height: 1.4;
color: #2f3640 !important;
background-color: #ffffff;
}
.form-control:focus, input:focus, textarea:focus, select:focus {
border-color: #5856d6;
box-shadow: 0 0 0 3px rgba(88, 86, 214, 0.1);
outline: none;
}
/* Labels and form text */
label, .control-label {
font-size: 16px !important;
font-weight: 600;
color: #2f3640 !important;
margin-bottom: 8px;
display: block;
}
/* Headings with proper hierarchy */
h1 {
font-size: 2.5rem !important; /* 40px */
font-weight: 700;
color: #1e293b !important;
line-height: 1.2;
margin-bottom: 1rem;
}
h2 {
font-size: 2rem !important; /* 32px */
font-weight: 600;
color: #1e293b !important;
line-height: 1.3;
margin-bottom: 0.875rem;
}
h3 {
font-size: 1.5rem !important; /* 24px */
font-weight: 600;
color: #2f3640 !important;
line-height: 1.4;
margin-bottom: 0.75rem;
}
h4 {
font-size: 1.25rem !important; /* 20px */
font-weight: 600;
color: #2f3640 !important;
line-height: 1.4;
margin-bottom: 0.5rem;
}
h5 {
font-size: 1.125rem !important; /* 18px */
font-weight: 600;
color: #2f3640 !important;
line-height: 1.4;
margin-bottom: 0.5rem;
}
h6 {
font-size: 1rem !important; /* 16px */
font-weight: 600;
color: #2f3640 !important;
line-height: 1.4;
margin-bottom: 0.5rem;
}
/* Paragraph and body text */
p {
font-size: 16px !important;
line-height: 1.6;
color: #2f3640 !important;
margin-bottom: 1rem;
}
/* Sidebar improvements */
#page-sidebar {
font-size: 16px !important;
}
#page-sidebar ul li a {
font-size: 16px !important;
padding: 12px 20px !important;
color: #2f3640 !important;
min-height: 44px;
display: flex;
align-items: center;
text-decoration: none;
}
#page-sidebar ul li a:hover {
background-color: #f8f9fa;
color: #5856d6 !important;
}
/* Content area improvements */
.content-box, .panel, .card {
font-size: 16px !important;
color: #2f3640 !important;
background-color: #ffffff;
border: 1px solid #e8e9ff;
border-radius: 12px;
padding: 20px;
margin-bottom: 20px;
}
/* Modal improvements */
.modal-content {
font-size: 16px !important;
color: #2f3640 !important;
}
.modal-title {
font-size: 1.5rem !important;
font-weight: 600;
color: #1e293b !important;
}
/* Alert and notification improvements */
.alert {
font-size: 16px !important;
padding: 16px 20px !important;
border-radius: 8px;
margin-bottom: 20px;
}
.alert-success {
background-color: #f0fdf4;
border-color: #bbf7d0;
color: #166534 !important;
}
.alert-danger {
background-color: #fef2f2;
border-color: #fecaca;
color: #dc2626 !important;
}
.alert-warning {
background-color: #fffbeb;
border-color: #fed7aa;
color: #d97706 !important;
}
.alert-info {
background-color: #eff6ff;
border-color: #bfdbfe;
color: #2563eb !important;
}
/* Navigation improvements */
.navbar-nav .nav-link {
font-size: 16px !important;
padding: 12px 16px !important;
color: #2f3640 !important;
}
/* Breadcrumb improvements */
.breadcrumb {
font-size: 16px !important;
background-color: transparent;
padding: 0;
margin-bottom: 20px;
}
.breadcrumb-item {
color: #64748b !important;
}
.breadcrumb-item.active {
color: #2f3640 !important;
}
/* Mobile-first responsive breakpoints */
@media (max-width: 1200px) {
.container, .container-fluid {
padding-left: 15px;
padding-right: 15px;
}
.table-responsive {
border: none;
margin-bottom: 20px;
}
}
@media (max-width: 992px) {
/* Stack columns on tablets */
.col-md-3, .col-md-4, .col-md-6, .col-md-8, .col-md-9 {
flex: 0 0 100%;
max-width: 100%;
margin-bottom: 20px;
}
/* Adjust sidebar for tablets */
#page-sidebar {
width: 100%;
position: static;
height: auto;
}
/* Make tables horizontally scrollable */
.table-responsive {
overflow-x: auto;
}
.table {
min-width: 600px;
}
}
@media (max-width: 768px) {
/* Mobile-specific adjustments */
html {
font-size: 14px;
}
body {
font-size: 14px;
padding: 0;
}
.container, .container-fluid {
padding-left: 10px;
padding-right: 10px;
}
/* Stack all columns on mobile */
.col-sm-1, .col-sm-2, .col-sm-3, .col-sm-4, .col-sm-6, .col-sm-8, .col-sm-9, .col-sm-12,
.col-md-1, .col-md-2, .col-md-3, .col-md-4, .col-md-6, .col-md-8, .col-md-9, .col-md-12 {
flex: 0 0 100%;
max-width: 100%;
margin-bottom: 15px;
}
/* Adjust headings for mobile */
h1 {
font-size: 2rem !important; /* 32px */
}
h2 {
font-size: 1.75rem !important; /* 28px */
}
h3 {
font-size: 1.5rem !important; /* 24px */
}
h4 {
font-size: 1.25rem !important; /* 20px */
}
/* Button adjustments for mobile */
.btn {
font-size: 16px !important;
padding: 14px 20px !important;
width: 100%;
margin-bottom: 10px;
}
.btn-group .btn {
width: auto;
margin-bottom: 0;
}
/* Form adjustments for mobile */
.form-control, input, textarea, select {
font-size: 16px !important; /* Prevents zoom on iOS */
padding: 14px 16px !important;
width: 100%;
}
/* Table adjustments for mobile */
.table {
font-size: 14px !important;
}
.table th, .table td {
padding: 8px 6px !important;
font-size: 13px !important;
}
/* Hide less important columns on mobile */
.table .d-none-mobile {
display: none !important;
}
/* Modal adjustments for mobile */
.modal-dialog {
margin: 10px;
width: calc(100% - 20px);
}
.modal-content {
padding: 20px 15px;
}
/* Content box adjustments */
.content-box, .panel, .card {
padding: 15px;
margin-bottom: 15px;
}
/* Sidebar adjustments for mobile */
#page-sidebar {
position: fixed;
top: 0;
left: -100%;
width: 280px;
height: 100vh;
z-index: 1000;
transition: left 0.3s ease;
background-color: #ffffff;
box-shadow: 2px 0 10px rgba(0,0,0,0.1);
}
#page-sidebar.show {
left: 0;
}
/* Main content adjustments when sidebar is open */
#main-content {
transition: margin-left 0.3s ease;
}
#main-content.sidebar-open {
margin-left: 280px;
}
/* Mobile menu toggle */
.mobile-menu-toggle {
display: block;
position: fixed;
top: 20px;
left: 20px;
z-index: 1001;
background-color: #5856d6;
color: white;
border: none;
padding: 12px;
border-radius: 8px;
font-size: 18px;
cursor: pointer;
}
}
@media (max-width: 576px) {
/* Extra small devices */
html {
font-size: 14px;
}
.container, .container-fluid {
padding-left: 8px;
padding-right: 8px;
}
/* Even smaller buttons and forms for very small screens */
.btn {
font-size: 14px !important;
padding: 12px 16px !important;
}
.form-control, input, textarea, select {
font-size: 16px !important; /* Still 16px to prevent zoom */
padding: 12px 14px !important;
}
/* Compact table for very small screens */
.table th, .table td {
padding: 6px 4px !important;
font-size: 12px !important;
}
/* Hide even more columns on very small screens */
.table .d-none-mobile-sm {
display: none !important;
}
}
/* Utility classes for mobile */
.d-none-mobile {
display: block;
}
.d-none-mobile-sm {
display: block;
}
@media (max-width: 768px) {
.d-none-mobile {
display: none !important;
}
}
@media (max-width: 576px) {
.d-none-mobile-sm {
display: none !important;
}
}
/* Ensure all text has proper contrast */
.text-white {
color: #ffffff !important;
}
.text-dark {
color: #2f3640 !important;
}
.text-muted {
color: #2f3640 !important; /* Dark text for better readability */
}
/* Fix any light text on light backgrounds */
.bg-light .text-muted,
.bg-white .text-muted,
.panel .text-muted {
color: #2f3640 !important; /* Dark text for better readability */
}
/* Ensure proper spacing for touch targets */
a, button, input, select, textarea {
min-height: 44px;
min-width: 44px;
}
/* Additional text readability improvements */
/* Fix any green text issues */
.ng-binding {
color: #2f3640 !important; /* Normal dark text instead of green */
}
/* Ensure all text elements have proper contrast */
span, div, p, label, td, th {
color: inherit;
}
/* Fix specific text color issues */
.text-success {
color: #059669 !important; /* Darker green for better readability */
}
.text-info {
color: #0284c7 !important; /* Darker blue for better readability */
}
.text-warning {
color: #d97706 !important; /* Darker orange for better readability */
}
/* Override Bootstrap's muted text */
.text-muted {
color: #2f3640 !important; /* Dark text instead of grey */
}
/* Fix any remaining light text on light backgrounds */
.bg-white .text-light,
.bg-light .text-light,
.panel .text-light,
.card .text-light {
color: #2f3640 !important;
}
/* Fix for small clickable elements */
.glyph-icon, .icon {
min-width: 44px;
min-height: 44px;
display: flex;
align-items: center;
justify-content: center;
}
/* Loading and spinner improvements */
.spinner, .loading {
font-size: 16px !important;
color: #5856d6 !important;
}
/* Print styles */
@media print {
body {
font-size: 12pt;
color: #000000 !important;
background: #ffffff !important;
}
.table th, .table td {
font-size: 10pt !important;
color: #000000 !important;
}
.btn, .alert, .modal {
display: none !important;
}
}

View File

@@ -0,0 +1,265 @@
/* CyberPanel Readability & Design Fixes */
/* This file fixes the core design issues with grey text and color inconsistencies */
/* Override CSS Variables for Better Text Contrast */
:root {
/* Ensure all text uses proper dark colors for readability */
--text-primary: #2f3640;
--text-secondary: #2f3640; /* Changed from grey to dark for better readability */
--text-heading: #1e293b;
}
/* Dark theme also uses proper contrast */
[data-theme="dark"] {
--text-primary: #e4e4e7;
--text-secondary: #e4e4e7; /* Changed from grey to light for better readability */
--text-heading: #f3f4f6;
}
/* Fix Green Text Issues */
/* Override Angular binding colors that might be green */
.ng-binding {
color: var(--text-secondary) !important;
}
/* Specific fix for uptime display */
#sidebar .server-info .info-line span,
#sidebar .server-info .info-line .ng-binding,
.server-info .ng-binding {
color: var(--text-secondary) !important;
}
/* Fix Grey Text on White Background */
/* Override all muted and secondary text classes */
.text-muted,
.text-secondary,
.text-light,
small,
.small,
.text-small {
color: var(--text-secondary) !important;
}
/* Fix specific Bootstrap classes */
.text-muted {
color: #2f3640 !important; /* Dark text for better readability */
}
/* Fix text on white/light backgrounds */
.bg-white .text-muted,
.bg-light .text-muted,
.panel .text-muted,
.card .text-muted,
.content-box .text-muted {
color: #2f3640 !important;
}
/* Fix menu items and navigation */
#sidebar .menu-item,
#sidebar .menu-item span,
#sidebar .menu-item i,
.sidebar .menu-item,
.sidebar .menu-item span,
.sidebar .menu-item i {
color: var(--text-secondary) !important;
}
#sidebar .menu-item:hover,
.sidebar .menu-item:hover {
color: var(--accent-color) !important;
}
#sidebar .menu-item.active,
.sidebar .menu-item.active {
color: white !important;
}
/* Fix server info and details */
.server-info,
.server-info *,
.server-details,
.server-details *,
.info-line,
.info-line span,
.info-line strong,
.tagline,
.brand {
color: inherit !important;
}
/* Fix form elements */
label,
.control-label,
.form-label {
color: var(--text-primary) !important;
font-weight: 600;
}
/* Fix table text */
.table th,
.table td {
color: var(--text-primary) !important;
}
.table th {
font-weight: 600;
}
/* Fix alert text */
.alert {
color: var(--text-primary) !important;
}
.alert-success {
color: #059669 !important; /* Darker green for better readability */
}
.alert-info {
color: #0284c7 !important; /* Darker blue for better readability */
}
.alert-warning {
color: #d97706 !important; /* Darker orange for better readability */
}
.alert-danger {
color: #dc2626 !important; /* Darker red for better readability */
}
/* Fix breadcrumb text */
.breadcrumb-item {
color: var(--text-secondary) !important;
}
.breadcrumb-item.active {
color: var(--text-primary) !important;
}
/* Fix modal text */
.modal-content {
color: var(--text-primary) !important;
}
.modal-title {
color: var(--text-heading) !important;
}
/* Fix button text */
.btn {
color: inherit;
}
/* Fix any remaining light text issues */
.bg-light .text-light,
.bg-white .text-light,
.panel .text-light,
.card .text-light {
color: #2f3640 !important;
}
/* Ensure proper contrast for all text elements */
span, div, p, label, td, th, a, li {
color: inherit;
}
/* Fix specific color classes */
.text-success {
color: #059669 !important; /* Darker green for better readability */
}
.text-info {
color: #0284c7 !important; /* Darker blue for better readability */
}
.text-warning {
color: #d97706 !important; /* Darker orange for better readability */
}
.text-danger {
color: #dc2626 !important; /* Darker red for better readability */
}
/* Fix any Angular-specific styling */
[ng-controller] {
color: inherit;
}
[ng-show],
[ng-hide],
[ng-if] {
color: inherit;
}
/* Ensure all content areas have proper text color */
.content-box,
.panel,
.card,
.main-content,
.page-content {
color: var(--text-primary) !important;
}
/* Fix any remaining Bootstrap classes */
.text-dark {
color: #2f3640 !important;
}
.text-body {
color: var(--text-primary) !important;
}
/* Mobile-specific fixes */
@media (max-width: 768px) {
/* Ensure mobile text is also readable */
body,
.container,
.container-fluid {
color: var(--text-primary) !important;
}
/* Fix mobile menu text */
.mobile-menu .menu-item,
.mobile-menu .menu-item span {
color: var(--text-secondary) !important;
}
}
/* Print styles */
@media print {
body,
.content-box,
.panel,
.card {
color: #000000 !important;
background: #ffffff !important;
}
.text-muted,
.text-secondary {
color: #000000 !important;
}
}
/* High contrast mode support */
@media (prefers-contrast: high) {
:root {
--text-primary: #000000;
--text-secondary: #000000;
--text-heading: #000000;
}
[data-theme="dark"] {
--text-primary: #ffffff;
--text-secondary: #ffffff;
--text-heading: #ffffff;
}
}
/* Reduced motion support */
@media (prefers-reduced-motion: reduce) {
* {
animation-duration: 0.01ms !important;
animation-iteration-count: 1 !important;
transition-duration: 0.01ms !important;
}
}

View File

@@ -914,126 +914,6 @@ app.controller('dashboardStatsController', function ($scope, $http, $timeout) {
// Hide system charts for non-admin users
$scope.hideSystemCharts = false;
// Pagination settings - 10 entries per page
var ITEMS_PER_PAGE = 10;
// Pagination state for each section
$scope.pagination = {
sshLogins: { currentPage: 1, itemsPerPage: ITEMS_PER_PAGE },
sshLogs: { currentPage: 1, itemsPerPage: ITEMS_PER_PAGE },
topProcesses: { currentPage: 1, itemsPerPage: ITEMS_PER_PAGE },
traffic: { currentPage: 1, itemsPerPage: ITEMS_PER_PAGE },
diskIO: { currentPage: 1, itemsPerPage: ITEMS_PER_PAGE },
cpuUsage: { currentPage: 1, itemsPerPage: ITEMS_PER_PAGE }
};
// Input fields for "go to page"
$scope.gotoPageInput = {
sshLogins: 1,
sshLogs: 1,
topProcesses: 1,
traffic: 1,
diskIO: 1,
cpuUsage: 1
};
// Expose Math to template
$scope.Math = Math;
// Pagination helper functions
$scope.getTotalPages = function(section) {
var items = [];
if (section === 'sshLogins') items = $scope.sshLogins || [];
else if (section === 'sshLogs') items = $scope.sshLogs || [];
else if (section === 'topProcesses') items = $scope.topProcesses || [];
else if (section === 'traffic') items = $scope.trafficLabels || [];
else if (section === 'diskIO') items = $scope.diskLabels || [];
else if (section === 'cpuUsage') items = $scope.cpuLabels || [];
return Math.max(1, Math.ceil((items.length || 0) / ITEMS_PER_PAGE));
};
$scope.getPaginatedItems = function(section) {
// Initialize pagination if it doesn't exist
if (!$scope.pagination) {
$scope.pagination = {};
}
if (!$scope.pagination[section]) {
$scope.pagination[section] = { currentPage: 1, itemsPerPage: ITEMS_PER_PAGE };
console.log('[getPaginatedItems] Initialized pagination for section:', section);
}
var items = [];
if (section === 'sshLogins') items = $scope.sshLogins || [];
else if (section === 'sshLogs') items = $scope.sshLogs || [];
else if (section === 'topProcesses') items = $scope.topProcesses || [];
else if (section === 'traffic') items = $scope.trafficLabels || [];
else if (section === 'diskIO') items = $scope.diskLabels || [];
else if (section === 'cpuUsage') items = $scope.cpuLabels || [];
// Ensure currentPage is a valid number
var currentPage = parseInt($scope.pagination[section].currentPage) || 1;
if (currentPage < 1 || isNaN(currentPage)) currentPage = 1;
var start = (currentPage - 1) * ITEMS_PER_PAGE;
var end = start + ITEMS_PER_PAGE;
var result = items.slice(start, end);
console.log('[getPaginatedItems] Section:', section, 'Total items:', items.length, 'Page:', currentPage, 'Start:', start, 'End:', end, 'Paginated count:', result.length);
if (result.length > 0) {
console.log('[getPaginatedItems] First item:', result[0]);
} else if (items.length > 0) {
console.warn('[getPaginatedItems] No items returned but total items > 0. Items:', items.length, 'Page:', currentPage, 'Start:', start, 'End:', end);
}
return result;
};
$scope.goToPage = function(section, page) {
var totalPages = $scope.getTotalPages(section);
if (page >= 1 && page <= totalPages) {
$scope.pagination[section].currentPage = parseInt(page);
$scope.gotoPageInput[section] = parseInt(page);
}
};
$scope.nextPage = function(section) {
var totalPages = $scope.getTotalPages(section);
if ($scope.pagination[section].currentPage < totalPages) {
$scope.pagination[section].currentPage++;
$scope.gotoPageInput[section] = $scope.pagination[section].currentPage;
}
};
$scope.prevPage = function(section) {
if ($scope.pagination[section].currentPage > 1) {
$scope.pagination[section].currentPage--;
$scope.gotoPageInput[section] = $scope.pagination[section].currentPage;
}
};
$scope.getPageNumbers = function(section) {
var totalPages = $scope.getTotalPages(section);
var current = $scope.pagination[section].currentPage;
var pages = [];
var maxVisible = 5; // Show max 5 page numbers
if (totalPages <= maxVisible) {
for (var i = 1; i <= totalPages; i++) {
pages.push(i);
}
} else {
if (current <= 3) {
for (var i = 1; i <= 5; i++) pages.push(i);
} else if (current >= totalPages - 2) {
for (var i = totalPages - 4; i <= totalPages; i++) pages.push(i);
} else {
for (var i = current - 2; i <= current + 2; i++) pages.push(i);
}
}
return pages;
};
// Top Processes
$scope.topProcesses = [];
$scope.loadingTopProcesses = true;
@@ -1044,9 +924,6 @@ app.controller('dashboardStatsController', function ($scope, $http, $timeout) {
$scope.loadingTopProcesses = false;
if (response.data && response.data.status === 1 && response.data.processes) {
$scope.topProcesses = response.data.processes;
// Reset to first page when data refreshes
$scope.pagination.topProcesses.currentPage = 1;
$scope.gotoPageInput.topProcesses = 1;
} else {
$scope.topProcesses = [];
}
@@ -1066,34 +943,14 @@ app.controller('dashboardStatsController', function ($scope, $http, $timeout) {
$scope.loadingSSHLogins = false;
if (response.data && response.data.logins) {
$scope.sshLogins = response.data.logins;
console.log('[refreshSSHLogins] Loaded', $scope.sshLogins.length, 'SSH logins');
// Ensure pagination is initialized
if (!$scope.pagination) {
$scope.pagination = {};
}
if (!$scope.pagination.sshLogins) {
$scope.pagination.sshLogins = { currentPage: 1, itemsPerPage: ITEMS_PER_PAGE };
}
// Reset to first page when data refreshes
$scope.pagination.sshLogins.currentPage = 1;
if (!$scope.gotoPageInput) {
$scope.gotoPageInput = {};
}
$scope.gotoPageInput.sshLogins = 1;
// Debug: Log paginated items
var paginated = $scope.getPaginatedItems('sshLogins');
console.log('[refreshSSHLogins] Paginated items count:', paginated.length, 'Items:', paginated);
// Debug: Log first login to see structure
if ($scope.sshLogins.length > 0) {
console.log('[refreshSSHLogins] First SSH login object:', $scope.sshLogins[0]);
console.log('[refreshSSHLogins] IP field:', $scope.sshLogins[0].ip);
console.log('[refreshSSHLogins] All keys:', Object.keys($scope.sshLogins[0]));
console.log('First SSH login object:', $scope.sshLogins[0]);
console.log('IP field:', $scope.sshLogins[0].ip);
console.log('All keys:', Object.keys($scope.sshLogins[0]));
}
} else {
$scope.sshLogins = [];
console.log('[refreshSSHLogins] No logins found in response');
}
}, function (err) {
$scope.loadingSSHLogins = false;
@@ -1114,9 +971,6 @@ app.controller('dashboardStatsController', function ($scope, $http, $timeout) {
$scope.loadingSSHLogs = false;
if (response.data && response.data.logs) {
$scope.sshLogs = response.data.logs;
// Reset to first page when data refreshes
$scope.pagination.sshLogs.currentPage = 1;
$scope.gotoPageInput.sshLogs = 1;
// Analyze logs for security issues
$scope.analyzeSSHSecurity();
} else {
@@ -1157,8 +1011,84 @@ app.controller('dashboardStatsController', function ($scope, $http, $timeout) {
};
$scope.blockIPAddress = function(ipAddress) {
if (!$scope.blockingIP) {
$scope.blockingIP = ipAddress;
try {
console.log('========================================');
console.log('=== blockIPAddress CALLED ===');
console.log('========================================');
console.log('blockIPAddress called with:', ipAddress);
console.log('ipAddress type:', typeof ipAddress);
console.log('ipAddress value:', ipAddress);
console.log('$scope:', $scope);
console.log('$scope.blockIPAddress:', typeof $scope.blockIPAddress);
// Validate IP address parameter
if (!ipAddress) {
console.error('No IP address provided:', ipAddress);
if (typeof PNotify !== 'undefined') {
new PNotify({
title: 'Error',
text: 'No IP address provided',
type: 'error',
delay: 5000
});
}
return;
}
// Ensure it's a string and trim it
ipAddress = String(ipAddress).trim();
// Validate after trimming
if (!ipAddress || ipAddress === '' || ipAddress === 'undefined' || ipAddress === 'null') {
console.error('IP address is empty or invalid after trim:', ipAddress);
if (typeof PNotify !== 'undefined') {
new PNotify({
title: 'Error',
text: 'Invalid IP address provided: ' + ipAddress,
type: 'error',
delay: 5000
});
}
return;
}
// Basic IP format validation
var ipPattern = /^(\d{1,3}\.){3}\d{1,3}(\/\d{1,2})?$/;
if (!ipPattern.test(ipAddress)) {
console.error('IP address format is invalid:', ipAddress);
if (typeof PNotify !== 'undefined') {
new PNotify({
title: 'Error',
text: 'Invalid IP address format: ' + ipAddress,
type: 'error',
delay: 5000
});
}
return;
}
// Prevent duplicate requests
if ($scope.blockingIP === ipAddress) {
console.log('Already processing IP:', ipAddress);
return; // Already processing this IP
}
// Check if already blocked
if ($scope.blockedIPs && $scope.blockedIPs[ipAddress]) {
console.log('IP already blocked:', ipAddress);
if (typeof PNotify !== 'undefined') {
new PNotify({
title: 'Info',
text: `IP address ${ipAddress} is already banned`,
type: 'info',
delay: 3000
});
}
return;
}
// Set blocking flag to prevent duplicate requests
$scope.blockingIP = ipAddress;
// Use the new Banned IPs system instead of the old blockIPAddress
var data = {
@@ -1173,48 +1103,343 @@ app.controller('dashboardStatsController', function ($scope, $http, $timeout) {
}
};
console.log('Sending ban IP request:', data);
console.log('CSRF Token:', getCookie('csrftoken'));
console.log('Config:', config);
$http.post('/firewall/addBannedIP', data, config).then(function (response) {
console.log('=== addBannedIP SUCCESS ===');
console.log('Full response:', response);
console.log('response.data:', response.data);
console.log('response.data type:', typeof response.data);
console.log('response.status:', response.status);
// Reset blocking flag
$scope.blockingIP = null;
if (response.data && response.data.status === 1) {
// Apply scope changes
if (!$scope.$$phase && !$scope.$root.$$phase) {
$scope.$apply();
}
// Handle both JSON string and object responses
var responseData = response.data;
if (typeof responseData === 'string') {
try {
responseData = JSON.parse(responseData);
console.log('Parsed responseData from string:', responseData);
} catch (e) {
console.error('Failed to parse response as JSON:', e);
var errorMsg = responseData && responseData.length ? responseData : 'Failed to block IP address';
if (typeof PNotify !== 'undefined') {
new PNotify({ title: 'Error', text: errorMsg, type: 'error', delay: 5000 });
}
$scope.blockingIP = null;
return;
}
}
console.log('Final responseData:', responseData);
console.log('responseData.status:', responseData ? responseData.status : 'undefined');
console.log('responseData.message:', responseData ? responseData.message : 'undefined');
console.log('responseData.error_message:', responseData ? responseData.error_message : 'undefined');
// Check for success (status === 1 or status === '1')
if (responseData && (responseData.status === 1 || responseData.status === '1')) {
// Mark IP as blocked
if (!$scope.blockedIPs) {
$scope.blockedIPs = {};
}
$scope.blockedIPs[ipAddress] = true;
// Show success notification
new PNotify({
title: 'IP Address Banned',
text: `IP address ${ipAddress} has been permanently banned and added to the firewall. You can manage it in the Firewall > Banned IPs section.`,
type: 'success',
delay: 5000
});
if (typeof PNotify !== 'undefined') {
new PNotify({
title: 'IP Address Banned',
text: `IP address ${ipAddress} has been permanently banned and added to the firewall. You can manage it in the Firewall > Banned IPs section.`,
type: 'success',
delay: 5000
});
}
// Refresh security analysis to update alerts
$scope.analyzeSSHSecurity();
if ($scope.analyzeSSHSecurity) {
$scope.analyzeSSHSecurity();
}
// Apply scope changes
if (!$scope.$$phase && !$scope.$root.$$phase) {
$scope.$apply();
}
} else {
// Show error notification
var errorMsg = 'Failed to block IP address';
if (responseData && responseData.error_message) {
errorMsg = responseData.error_message;
} else if (responseData && responseData.error) {
errorMsg = responseData.error;
} else if (responseData && responseData.message) {
errorMsg = responseData.message;
} else if (responseData) {
errorMsg = JSON.stringify(responseData);
}
console.error('Ban IP failed:', errorMsg);
if (typeof PNotify !== 'undefined') {
new PNotify({
title: 'Error',
text: errorMsg,
type: 'error',
delay: 5000
});
}
}
}, function (err) {
$scope.blockingIP = null;
console.error('addBannedIP error:', err);
console.error('Error status:', err.status);
console.error('Error statusText:', err.statusText);
console.error('Error data:', err.data);
// Prevent showing duplicate error notifications
if ($scope.lastErrorIP === ipAddress && $scope.lastErrorTime && (Date.now() - $scope.lastErrorTime) < 2000) {
console.log('Skipping duplicate error notification for IP:', ipAddress);
return;
}
$scope.lastErrorIP = ipAddress;
$scope.lastErrorTime = Date.now();
var errorMessage = 'Failed to block IP address';
var errData = err.data;
if (typeof errData === 'string') {
try {
errData = JSON.parse(errData);
} catch (e) {
if (errData && errData.length) {
errorMessage = errData.length > 200 ? errData.substring(0, 200) + '...' : errData;
}
}
}
if (errData && typeof errData === 'object') {
errorMessage = errData.error_message || errData.error || errData.message || errorMessage;
} else if (err.status) {
errorMessage = 'HTTP ' + err.status + ': ' + (errorMessage);
}
console.error('Final error message:', errorMessage);
if (typeof PNotify !== 'undefined') {
new PNotify({
title: 'Error',
text: response.data && response.data.error ? response.data.error : 'Failed to block IP address',
text: errorMessage,
type: 'error',
delay: 5000
});
}
}, function (err) {
$scope.blockingIP = null;
var errorMessage = 'Failed to block IP address';
if (err.data && err.data.error) {
errorMessage = err.data.error;
} else if (err.data && err.data.message) {
errorMessage = err.data.message;
});
} catch (e) {
console.error('========================================');
console.error('=== ERROR in blockIPAddress ===');
console.error('========================================');
console.error('Error:', e);
console.error('Error message:', e.message);
console.error('Error stack:', e.stack);
$scope.blockingIP = null;
if (typeof PNotify !== 'undefined') {
new PNotify({
title: 'Error',
text: 'An error occurred while trying to ban the IP address: ' + (e.message || String(e)),
type: 'error',
delay: 5000
});
}
}
};
// Ban IP from SSH Logs
$scope.banIPFromSSHLog = function(ipAddress) {
if (!ipAddress) {
new PNotify({
title: 'Error',
text: 'No IP address provided',
type: 'error',
delay: 5000
});
return;
}
if ($scope.blockingIP === ipAddress) {
return; // Already processing
}
if ($scope.blockedIPs[ipAddress]) {
new PNotify({
title: 'Info',
text: `IP address ${ipAddress} is already banned`,
type: 'info',
delay: 3000
});
return;
}
$scope.blockingIP = ipAddress;
// Use the Banned IPs system
var data = {
ip: ipAddress,
reason: 'Suspicious activity detected from SSH logs',
duration: 'permanent'
};
var config = {
headers: {
'X-CSRFToken': getCookie('csrftoken')
}
};
$http.post('/firewall/addBannedIP', data, config).then(function (response) {
$scope.blockingIP = null;
if (response.data && response.data.status === 1) {
// Mark IP as blocked
$scope.blockedIPs[ipAddress] = true;
// Show success notification
new PNotify({
title: 'IP Address Banned',
text: `IP address ${ipAddress} has been permanently banned and added to the firewall. You can manage it in the Firewall > Banned IPs section.`,
type: 'success',
delay: 5000
});
// Refresh SSH logs to update the UI
$scope.refreshSSHLogs();
} else {
// Show error notification
var errorMsg = 'Failed to ban IP address';
if (response.data && response.data.error_message) {
errorMsg = response.data.error_message;
} else if (response.data && response.data.error) {
errorMsg = response.data.error;
}
new PNotify({
title: 'Error',
text: errorMessage,
text: errorMsg,
type: 'error',
delay: 5000
});
}
}, function (err) {
$scope.blockingIP = null;
var errorMessage = 'Failed to ban IP address';
if (err.data && err.data.error_message) {
errorMessage = err.data.error_message;
} else if (err.data && err.data.error) {
errorMessage = err.data.error;
} else if (err.data && err.data.message) {
errorMessage = err.data.message;
}
new PNotify({
title: 'Error',
text: errorMessage,
type: 'error',
delay: 5000
});
});
};
// Ban IP from SSH Logs
$scope.banIPFromSSHLog = function(ipAddress) {
if (!ipAddress) {
new PNotify({
title: 'Error',
text: 'No IP address provided',
type: 'error',
delay: 5000
});
return;
}
if ($scope.blockingIP === ipAddress) {
return; // Already processing
}
if ($scope.blockedIPs[ipAddress]) {
new PNotify({
title: 'Info',
text: `IP address ${ipAddress} is already banned`,
type: 'info',
delay: 3000
});
return;
}
$scope.blockingIP = ipAddress;
// Use the Banned IPs system
var data = {
ip: ipAddress,
reason: 'Suspicious activity detected from SSH logs',
duration: 'permanent'
};
var config = {
headers: {
'X-CSRFToken': getCookie('csrftoken')
}
};
$http.post('/firewall/addBannedIP', data, config).then(function (response) {
$scope.blockingIP = null;
if (response.data && response.data.status === 1) {
// Mark IP as blocked
$scope.blockedIPs[ipAddress] = true;
// Show success notification
new PNotify({
title: 'IP Address Banned',
text: `IP address ${ipAddress} has been permanently banned and added to the firewall. You can manage it in the Firewall > Banned IPs section.`,
type: 'success',
delay: 5000
});
// Refresh SSH logs to update the UI
$scope.refreshSSHLogs();
} else {
// Show error notification
var errorMsg = 'Failed to ban IP address';
if (response.data && response.data.error_message) {
errorMsg = response.data.error_message;
} else if (response.data && response.data.error) {
errorMsg = response.data.error;
}
new PNotify({
title: 'Error',
text: errorMsg,
type: 'error',
delay: 5000
});
}
}, function (err) {
$scope.blockingIP = null;
var errorMessage = 'Failed to ban IP address';
if (err.data && err.data.error_message) {
errorMessage = err.data.error_message;
} else if (err.data && err.data.error) {
errorMessage = err.data.error;
} else if (err.data && err.data.message) {
errorMessage = err.data.message;
}
new PNotify({
title: 'Error',
text: errorMessage,
type: 'error',
delay: 5000
});
});
};
// Initial fetch
@@ -1224,72 +1449,15 @@ app.controller('dashboardStatsController', function ($scope, $http, $timeout) {
// Chart.js chart objects
var trafficChart, diskIOChart, cpuChart;
// Data arrays for live graphs - expose to scope for pagination
$scope.trafficLabels = [];
$scope.rxData = [];
$scope.txData = [];
$scope.diskLabels = [];
$scope.readData = [];
$scope.writeData = [];
$scope.cpuLabels = [];
$scope.cpuUsageData = [];
// Internal references for backward compatibility
var trafficLabels = $scope.trafficLabels;
var rxData = $scope.rxData;
var txData = $scope.txData;
var diskLabels = $scope.diskLabels;
var readData = $scope.readData;
var writeData = $scope.writeData;
var cpuLabels = $scope.cpuLabels;
var cpuUsageData = $scope.cpuUsageData;
// Data arrays for live graphs
var trafficLabels = [], rxData = [], txData = [];
var diskLabels = [], readData = [], writeData = [];
var cpuLabels = [], cpuUsageData = [];
// For rate calculation
var lastRx = null, lastTx = null, lastDiskRead = null, lastDiskWrite = null, lastCPU = null;
var lastCPUTimes = null;
var pollInterval = 2000; // ms
var maxPoints = 30;
// Watch pagination changes and update charts accordingly
$scope.$watch('pagination.traffic.currentPage', function() {
updateTrafficChartData();
});
$scope.$watch('pagination.diskIO.currentPage', function() {
updateDiskIOChartData();
});
$scope.$watch('pagination.cpuUsage.currentPage', function() {
updateCPUChartData();
});
function updateTrafficChartData() {
if (!trafficChart || !$scope.trafficLabels || $scope.trafficLabels.length === 0) return;
var startIdx = ($scope.pagination.traffic.currentPage - 1) * ITEMS_PER_PAGE;
var endIdx = startIdx + ITEMS_PER_PAGE;
trafficChart.data.labels = $scope.trafficLabels.slice(startIdx, endIdx);
trafficChart.data.datasets[0].data = $scope.rxData.slice(startIdx, endIdx);
trafficChart.data.datasets[1].data = $scope.txData.slice(startIdx, endIdx);
trafficChart.update();
}
function updateDiskIOChartData() {
if (!diskIOChart || !$scope.diskLabels || $scope.diskLabels.length === 0) return;
var startIdx = ($scope.pagination.diskIO.currentPage - 1) * ITEMS_PER_PAGE;
var endIdx = startIdx + ITEMS_PER_PAGE;
diskIOChart.data.labels = $scope.diskLabels.slice(startIdx, endIdx);
diskIOChart.data.datasets[0].data = $scope.readData.slice(startIdx, endIdx);
diskIOChart.data.datasets[1].data = $scope.writeData.slice(startIdx, endIdx);
diskIOChart.update();
}
function updateCPUChartData() {
if (!cpuChart || !$scope.cpuLabels || $scope.cpuLabels.length === 0) return;
var startIdx = ($scope.pagination.cpuUsage.currentPage - 1) * ITEMS_PER_PAGE;
var endIdx = startIdx + ITEMS_PER_PAGE;
cpuChart.data.labels = $scope.cpuLabels.slice(startIdx, endIdx);
cpuChart.data.datasets[0].data = $scope.cpuUsageData.slice(startIdx, endIdx);
cpuChart.update();
}
function pollDashboardStats() {
console.log('[dashboardStatsController] pollDashboardStats() called');

View File

@@ -82,6 +82,15 @@ fileManager.controller('fileManagerCtrl', function ($scope, $http, FileUploader,
$scope.showUploadBox = function () {
$('#uploadBox').modal('show');
};
// Fix aria-hidden a11y: move focus out of modal before hide so no focused descendant retains focus
$(document).on('hide.bs.modal', '.modal', function () {
var modal = this;
if (document.activeElement && modal.contains(document.activeElement)) {
var trigger = document.getElementById('uploadTriggerBtn');
if (trigger && modal.id === 'uploadBox') { trigger.focus(); }
else { document.activeElement.blur(); }
}
});
$scope.showHTMLEditorModal = function (MainFM= 0) {
$scope.htmlEditorLoading = false;
@@ -1147,7 +1156,8 @@ fileManager.controller('fileManagerCtrl', function ($scope, $http, FileUploader,
});
$scope.fetchForTableSecondary(null, 'refresh');
} else {
var notification = alertify.notify('Files/Folders can not be deleted', 'error', 5, function () {
var errMsg = (response.data && response.data.error_message) ? response.data.error_message : 'Files/Folders can not be deleted';
var notification = alertify.notify(errMsg, 'error', 8, function () {
console.log('dismissed');
});
}
@@ -1155,6 +1165,10 @@ fileManager.controller('fileManagerCtrl', function ($scope, $http, FileUploader,
}
function cantLoadInitialDatas(response) {
var err = (response && response.data && (response.data.error_message || response.data.message)) ||
(response && response.statusText) || 'Request failed';
if (response && response.status === 0) err = 'Network error';
alertify.notify(err, 'error', 8);
}
};

View File

@@ -156,6 +156,14 @@ function findFileExtension(fileName) {
$scope.showUploadBox = function () {
$("#uploadBox").modal();
};
$(document).on("hide.bs.modal", ".modal", function () {
var modal = this;
if (document.activeElement && modal.contains(document.activeElement)) {
var trigger = document.getElementById("uploadTriggerBtn");
if (trigger && modal.id === "uploadBox") { trigger.focus(); }
else { document.activeElement.blur(); }
}
});
$scope.showHTMLEditorModal = function (MainFM = 0) {
$scope.fileInEditor = allFilesAndFolders[0];

View File

@@ -5,7 +5,7 @@
/* Java script code to ADD Firewall Rules */
app.controller('firewallController', function ($scope, $http) {
app.controller('firewallController', function ($scope, $http, $timeout) {
$scope.rulesLoading = true;
$scope.actionFailed = true;
@@ -16,9 +16,51 @@ app.controller('firewallController', function ($scope, $http) {
$scope.couldNotConnect = true;
$scope.rulesDetails = false;
// Banned IPs variables
$scope.activeTab = 'rules';
// Tab from hash so we stay on /firewall/ (avoids 404 on servers without /firewall/firewall-rules/)
function tabFromHash() {
var h = (window.location.hash || '').replace(/^#/, '');
return (h === 'banned-ips') ? 'banned' : 'rules';
}
$scope.activeTab = tabFromHash();
$scope.bannedIPs = [];
// Re-apply tab from hash after load (hash can be set after controller init in some browsers)
function applyTabFromHash() {
var tab = tabFromHash();
if ($scope.activeTab !== tab) {
$scope.activeTab = tab;
if (tab === 'banned') { populateBannedIPs(); } else { populateCurrentRecords(); }
if (!$scope.$$phase && !$scope.$root.$$phase) { $scope.$apply(); }
}
}
$timeout(applyTabFromHash, 0);
if (document.readyState === 'complete') {
$timeout(applyTabFromHash, 50);
} else {
window.addEventListener('load', function() { $timeout(applyTabFromHash, 0); });
}
$scope.setFirewallTab = function(tab) {
$timeout(function() {
$scope.activeTab = tab;
window.location.hash = (tab === 'banned') ? '#banned-ips' : '#rules';
if (tab === 'banned') { populateBannedIPs(); } else { populateCurrentRecords(); }
}, 0);
};
window.addEventListener('hashchange', function() {
var tab = tabFromHash();
if ($scope.activeTab !== tab) {
$scope.activeTab = tab;
if (tab === 'banned') { populateBannedIPs(); } else { populateCurrentRecords(); }
if (!$scope.$$phase && !$scope.$root.$$phase) { $scope.$apply(); }
}
});
$scope.rulesPage = 1;
$scope.rulesPageSize = 10;
$scope.rulesPageSizeOptions = [5, 10, 20, 30, 50, 100];
$scope.rulesTotalCount = 0;
$scope.bannedPage = 1;
$scope.bannedPageSize = 10;
$scope.bannedPageSizeOptions = [5, 10, 20, 30, 50, 100];
$scope.bannedTotalCount = 0;
$scope.bannedIPsLoading = false;
$scope.bannedIPActionFailed = true;
$scope.bannedIPActionSuccess = true;
@@ -29,9 +71,18 @@ app.controller('firewallController', function ($scope, $http) {
firewallStatus();
// Load both tabs on init
populateCurrentRecords();
populateBannedIPs();
// Whenever activeTab changes, load that tab's data (ensures second tab loads even if click/apply failed)
$scope.$watch('activeTab', function(newVal, oldVal) {
if (newVal === oldVal || !newVal) return;
$timeout(function() {
if (newVal === 'banned') { populateBannedIPs(); } else if (newVal === 'rules') { populateCurrentRecords(); }
}, 0);
});
$scope.addRule = function () {
$scope.rulesLoading = false;
@@ -123,37 +174,67 @@ app.controller('firewallController', function ($scope, $http) {
$scope.actionFailed = true;
$scope.actionSuccess = true;
url = "/firewall/getCurrentRules";
var data = {};
var config = {
headers: {
'X-CSRFToken': getCookie('csrftoken')
}
};
var data = { page: $scope.rulesPage || 1, page_size: $scope.rulesPageSize || 10 };
var config = { headers: { 'X-CSRFToken': getCookie('csrftoken') } };
$http.post(url, data, config).then(ListInitialDatas, cantLoadInitialDatas);
function ListInitialDatas(response) {
if (response.data.fetchStatus === 1) {
$scope.rules = JSON.parse(response.data.data);
var res = (typeof response.data === 'string') ? (function() { try { return JSON.parse(response.data); } catch (e) { return {}; } })() : response.data;
if (res && res.fetchStatus === 1) {
$scope.rules = typeof res.data === 'string' ? JSON.parse(res.data) : (res.data || []);
$scope.rulesTotalCount = res.total_count != null ? res.total_count : ($scope.rules ? $scope.rules.length : 0);
$scope.rulesPage = Math.max(1, res.page != null ? res.page : 1);
$scope.rulesPageSize = res.page_size != null ? res.page_size : 10;
$scope.rulesLoading = true;
}
else {
$scope.rulesLoading = true;
$scope.errorMessage = response.data.error_message;
$scope.errorMessage = (res && res.error_message) ? res.error_message : '';
}
}
function cantLoadInitialDatas(response) {
$scope.couldNotConnect = false;
}
}
$scope.goToRulesPage = function(page) {
var totalP = $scope.rulesTotalPages();
if (page < 1 || page > totalP) return;
$scope.rulesPage = page;
populateCurrentRecords();
};
$scope.goToRulesPageByInput = function() {
var n = parseInt($scope.rulesPageInput, 10);
if (isNaN(n) || n < 1) n = 1;
var maxP = $scope.rulesTotalPages();
if (n > maxP) n = maxP;
$scope.rulesPageInput = n;
$scope.goToRulesPage(n);
};
$scope.rulesTotalPages = function() {
var size = $scope.rulesPageSize || 10;
var total = $scope.rulesTotalCount || ($scope.rules && $scope.rules.length) || 0;
return size > 0 ? Math.max(1, Math.ceil(total / size)) : 1;
};
$scope.rulesRangeStart = function() {
var total = $scope.rulesTotalCount || ($scope.rules && $scope.rules.length) || 0;
if (total === 0) return 0;
var page = Math.max(1, $scope.rulesPage || 1);
var size = $scope.rulesPageSize || 10;
return (page - 1) * size + 1;
};
$scope.rulesRangeEnd = function() {
var start = $scope.rulesRangeStart();
var size = $scope.rulesPageSize || 10;
var total = $scope.rulesTotalCount || ($scope.rules && $scope.rules.length) || 0;
return total === 0 ? 0 : Math.min(start + size - 1, total);
};
$scope.setRulesPageSize = function() {
$scope.rulesPage = 1;
populateCurrentRecords();
};
$scope.deleteRule = function (id, proto, port, ruleIP) {
@@ -2417,20 +2498,25 @@ app.controller('litespeed_ent_conf', function ($scope, $http, $timeout, $window)
function populateBannedIPs() {
$scope.bannedIPsLoading = true;
var url = "/firewall/getBannedIPs";
var postData = { page: $scope.bannedPage || 1, page_size: $scope.bannedPageSize || 10 };
var config = {
headers: {
'X-CSRFToken': getCookie('csrftoken')
}
};
$http.post(url, {}, config).then(function(response) {
$http.post(url, postData, config).then(function(response) {
$scope.bannedIPsLoading = false;
if (response.data.status === 1) {
$scope.bannedIPs = response.data.bannedIPs || [];
var res = (typeof response.data === 'string') ? (function() { try { return JSON.parse(response.data); } catch (e) { return {}; } })() : response.data;
if (res && res.status === 1) {
$scope.bannedIPs = res.bannedIPs || [];
$scope.bannedTotalCount = res.total_count != null ? res.total_count : ($scope.bannedIPs ? $scope.bannedIPs.length : 0);
$scope.bannedPage = Math.max(1, res.page != null ? res.page : 1);
$scope.bannedPageSize = res.page_size != null ? res.page_size : 10;
} else {
$scope.bannedIPs = [];
$scope.bannedIPActionFailed = false;
$scope.bannedIPErrorMessage = response.data.error_message;
$scope.bannedIPErrorMessage = (res && res.error_message) ? res.error_message : '';
}
}, function(error) {
$scope.bannedIPsLoading = false;
@@ -2438,6 +2524,53 @@ app.controller('litespeed_ent_conf', function ($scope, $http, $timeout, $window)
});
}
$scope.goToBannedPage = function(page) {
var totalP = $scope.bannedTotalPages();
if (page < 1 || page > totalP) return;
$scope.bannedPage = page;
populateBannedIPs();
};
$scope.goToBannedPageByInput = function() {
var n = parseInt($scope.bannedPageInput, 10);
if (isNaN(n) || n < 1) n = 1;
var maxP = $scope.bannedTotalPages();
if (n > maxP) n = maxP;
$scope.bannedPageInput = n;
$scope.goToBannedPage(n);
};
$scope.bannedTotalPages = function() {
var size = $scope.bannedPageSize || 10;
var total = $scope.bannedTotalCount || ($scope.bannedIPs ? $scope.bannedIPs.length : 0) || 0;
return size > 0 ? Math.max(1, Math.ceil(total / size)) : 1;
};
$scope.bannedRangeStart = function() {
var total = $scope.bannedTotalCount || ($scope.bannedIPs ? $scope.bannedIPs.length : 0) || 0;
if (total === 0) return 0;
var page = Math.max(1, $scope.bannedPage || 1);
var size = $scope.bannedPageSize || 10;
return (page - 1) * size + 1;
};
$scope.bannedRangeEnd = function() {
var start = $scope.bannedRangeStart();
var size = $scope.bannedPageSize || 10;
var total = $scope.bannedTotalCount || ($scope.bannedIPs ? $scope.bannedIPs.length : 0) || 0;
return total === 0 ? 0 : Math.min(start + size - 1, total);
};
$scope.setBannedPageSize = function() {
$scope.bannedPage = 1;
populateBannedIPs();
};
$scope.populateBannedIPs = populateBannedIPs;
if (typeof window !== 'undefined') {
window.__firewallLoadTab = function(tab) {
$scope.$evalAsync(function() {
$scope.activeTab = tab;
if (tab === 'banned') { populateBannedIPs(); } else { populateCurrentRecords(); }
});
};
}
$scope.addBannedIP = function() {
if (!$scope.banIP || !$scope.banReason) {
$scope.bannedIPActionFailed = false;
@@ -2696,4 +2829,26 @@ app.controller('litespeed_ent_conf', function ($scope, $http, $timeout, $window)
}
}
});
});
(function() {
// Do not capture tab clicks let Angular ng-click run setFirewallTab() so data loads.
// Only sync tab from hash on load and hashchange (back/forward) via __firewallLoadTab.
function syncFirewallTabFromHash() {
var nav = document.getElementById('firewall-tab-nav');
if (!nav) return;
var h = (window.location.hash || '').replace(/^#/, '');
var tab = (h === 'banned-ips') ? 'banned' : 'rules';
if (window.__firewallLoadTab) {
try { window.__firewallLoadTab(tab); } catch (e) {}
}
}
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', syncFirewallTabFromHash);
} else {
syncFirewallTabFromHash();
}
setTimeout(syncFirewallTabFromHash, 100);
window.addEventListener('hashchange', syncFirewallTabFromHash);
})();

View File

@@ -26,9 +26,10 @@ app.controller('createFTPAccount', function ($scope, $http) {
$sel.select2();
$sel.on('select2:select', function (e) {
var data = e.params.data;
$scope.ftpDomain = data.text;
$scope.ftpDetails = false;
$scope.$apply();
$scope.$evalAsync(function () {
$scope.ftpDomain = data.text;
$scope.ftpDetails = false;
});
$(".ftpDetails, .account-details").show();
});
} else {
@@ -42,9 +43,11 @@ app.controller('createFTPAccount', function ($scope, $http) {
}
function initNativeSelect() {
$('.create-ftp-acct-select').off('select2:select').on('change', function () {
$scope.ftpDomain = $(this).val();
$scope.ftpDetails = ($scope.ftpDomain && $scope.ftpDomain !== '') ? false : true;
$scope.$apply();
var val = $(this).val();
$scope.$evalAsync(function () {
$scope.ftpDomain = val;
$scope.ftpDetails = (val && val !== '') ? false : true;
});
$(".ftpDetails, .account-details").show();
});
}

View File

@@ -114,7 +114,7 @@
$scope.forwardSuccess = true;
$scope.couldNotConnect = true;
$scope.notifyBox = true;
if (typeof new PNotify === 'function') {
if (typeof PNotify === 'function') {
new PNotify({ title: 'Success!', text: 'Changes applied.', type: 'success' });
}
$scope.showEmailDetails();
@@ -126,7 +126,7 @@
$scope.forwardSuccess = true;
$scope.couldNotConnect = true;
$scope.notifyBox = false;
if (typeof new PNotify === 'function') {
if (typeof PNotify === 'function') {
new PNotify({ title: 'Error!', text: response.data.error_message || 'Error', type: 'error' });
}
}

View File

@@ -1514,6 +1514,7 @@ app.controller('EmailLimitsNew', function ($scope, $http) {
// Given email to search for
var givenEmail = $scope.selectedEmail;
if ($scope.emails) {
for (var i = 0; i < $scope.emails.length; i++) {
if ($scope.emails[i].email === givenEmail) {
// Extract numberofEmails and duration
@@ -1523,14 +1524,11 @@ app.controller('EmailLimitsNew', function ($scope, $http) {
$scope.numberofEmails = numberofEmails;
$scope.duration = duration;
// Use numberofEmails and duration as needed
console.log("Number of emails:", numberofEmails);
console.log("Duration:", duration);
// Break out of the loop since the email is found
break;
}
}
}
};

View File

@@ -641,11 +641,15 @@ app.controller('servicesManager', function ($scope, $http) {
getServiceStatus();
$scope.ActionSuccessfull = true;
$scope.ActionFailed = false;
$scope.actionErrorMsg = '';
$scope.couldNotConnect = false;
$scope.actionLoader = false;
$scope.btnDisable = false;
}, 3000);
} else {
var errMsg = (response.data && response.data.error_message) ? response.data.error_message : 'Action failed';
if (errMsg === 0) errMsg = 'Action failed';
$scope.actionErrorMsg = errMsg;
setTimeout(function () {
getServiceStatus();
$scope.ActionSuccessfull = false;
@@ -654,7 +658,6 @@ app.controller('servicesManager', function ($scope, $http) {
$scope.actionLoader = false;
$scope.btnDisable = false;
}, 5000);
}
}