mirror of
https://github.com/usmannasir/cyberpanel.git
synced 2026-05-06 16:06:42 +02:00
Merge origin/v2.5.5-dev: Plugin Store sidebar, ?view=store, master3395 default, clone comment
This commit is contained in:
@@ -1,4 +1,7 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
import os
|
||||
import time
|
||||
|
||||
from .views import VERSION, BUILD
|
||||
|
||||
def version_context(request):
|
||||
@@ -49,4 +52,54 @@ def notification_preferences_context(request):
|
||||
return {
|
||||
'backup_notification_dismissed': False,
|
||||
'ai_scanner_notification_dismissed': False
|
||||
}
|
||||
|
||||
def firewall_static_context(request):
|
||||
"""Expose a cache-busting token for firewall static assets (bumps when firewall.js changes)."""
|
||||
try:
|
||||
from django.conf import settings
|
||||
base = settings.BASE_DIR
|
||||
# Check both app static and repo static so version updates when either is updated
|
||||
paths = [
|
||||
os.path.join(base, 'firewall', 'static', 'firewall', 'firewall.js'),
|
||||
os.path.join(base, 'static', 'firewall', 'firewall.js'),
|
||||
os.path.join(base, 'public', 'static', 'firewall', 'firewall.js'),
|
||||
]
|
||||
version = 0
|
||||
for p in paths:
|
||||
try:
|
||||
version = max(version, int(os.path.getmtime(p)))
|
||||
except (OSError, TypeError):
|
||||
pass
|
||||
if version <= 0:
|
||||
version = int(time.time())
|
||||
except (OSError, AttributeError):
|
||||
version = int(time.time())
|
||||
return {
|
||||
'FIREWALL_STATIC_VERSION': version
|
||||
}
|
||||
|
||||
|
||||
def dns_static_context(request):
|
||||
"""Cache-busting for DNS static assets (bumps when dns.js changes). Avoids stale JS/layout."""
|
||||
try:
|
||||
from django.conf import settings
|
||||
base = settings.BASE_DIR
|
||||
paths = [
|
||||
os.path.join(base, 'dns', 'static', 'dns', 'dns.js'),
|
||||
os.path.join(base, 'static', 'dns', 'dns.js'),
|
||||
os.path.join(base, 'public', 'static', 'dns', 'dns.js'),
|
||||
]
|
||||
version = 0
|
||||
for p in paths:
|
||||
try:
|
||||
version = max(version, int(os.path.getmtime(p)))
|
||||
except (OSError, TypeError):
|
||||
pass
|
||||
if version <= 0:
|
||||
version = int(time.time())
|
||||
except (OSError, AttributeError):
|
||||
version = int(time.time())
|
||||
return {
|
||||
'DNS_STATIC_VERSION': version
|
||||
}
|
||||
589
baseTemplate/static/baseTemplate/assets/mobile-responsive.css
Normal file
589
baseTemplate/static/baseTemplate/assets/mobile-responsive.css
Normal file
@@ -0,0 +1,589 @@
|
||||
/* CyberPanel Mobile Responsive & Readability Fixes */
|
||||
/* This file ensures all pages are mobile-friendly with proper font sizes and readable text */
|
||||
|
||||
/* Base font size and mobile-first approach */
|
||||
html {
|
||||
font-size: 16px; /* Base font size for better readability */
|
||||
-webkit-text-size-adjust: 100%;
|
||||
-ms-text-size-adjust: 100%;
|
||||
text-size-adjust: 100%;
|
||||
}
|
||||
|
||||
body {
|
||||
font-size: 16px;
|
||||
line-height: 1.6;
|
||||
color: #2f3640; /* Dark text for better readability on white backgrounds */
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
|
||||
}
|
||||
|
||||
/* Ensure all text is readable with proper contrast */
|
||||
* {
|
||||
color: inherit;
|
||||
}
|
||||
|
||||
/* Override any light text that might be hard to read */
|
||||
.text-muted, .text-secondary, .text-light {
|
||||
color: #2f3640 !important; /* Dark text for better readability on white backgrounds */
|
||||
}
|
||||
|
||||
/* Fix small font sizes that are hard to read */
|
||||
small, .small, .text-small {
|
||||
font-size: 14px !important; /* Minimum readable size */
|
||||
}
|
||||
|
||||
/* Table improvements for mobile */
|
||||
.table {
|
||||
font-size: 16px !important; /* Larger table text */
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.table th, .table td {
|
||||
padding: 12px 8px !important; /* More padding for touch targets */
|
||||
border: 1px solid #e8e9ff;
|
||||
text-align: left;
|
||||
vertical-align: middle;
|
||||
font-size: 14px !important;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.table th {
|
||||
background-color: #f8f9fa;
|
||||
font-weight: 600;
|
||||
color: #2f3640 !important;
|
||||
font-size: 15px !important;
|
||||
}
|
||||
|
||||
/* Button improvements for mobile */
|
||||
.btn {
|
||||
font-size: 16px !important;
|
||||
padding: 12px 20px !important;
|
||||
border-radius: 8px;
|
||||
min-height: 44px; /* Minimum touch target size */
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
text-decoration: none;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.btn-sm {
|
||||
font-size: 14px !important;
|
||||
padding: 8px 16px !important;
|
||||
min-height: 36px;
|
||||
}
|
||||
|
||||
.btn-xs {
|
||||
font-size: 13px !important;
|
||||
padding: 6px 12px !important;
|
||||
min-height: 32px;
|
||||
}
|
||||
|
||||
/* Form elements */
|
||||
.form-control, input, textarea, select {
|
||||
font-size: 16px !important; /* Prevents zoom on iOS */
|
||||
padding: 12px 16px !important;
|
||||
border: 2px solid #e8e9ff;
|
||||
border-radius: 8px;
|
||||
min-height: 44px;
|
||||
line-height: 1.4;
|
||||
color: #2f3640 !important;
|
||||
background-color: #ffffff;
|
||||
}
|
||||
|
||||
.form-control:focus, input:focus, textarea:focus, select:focus {
|
||||
border-color: #5856d6;
|
||||
box-shadow: 0 0 0 3px rgba(88, 86, 214, 0.1);
|
||||
outline: none;
|
||||
}
|
||||
|
||||
/* Labels and form text */
|
||||
label, .control-label {
|
||||
font-size: 16px !important;
|
||||
font-weight: 600;
|
||||
color: #2f3640 !important;
|
||||
margin-bottom: 8px;
|
||||
display: block;
|
||||
}
|
||||
|
||||
/* Headings with proper hierarchy */
|
||||
h1 {
|
||||
font-size: 2.5rem !important; /* 40px */
|
||||
font-weight: 700;
|
||||
color: #1e293b !important;
|
||||
line-height: 1.2;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
h2 {
|
||||
font-size: 2rem !important; /* 32px */
|
||||
font-weight: 600;
|
||||
color: #1e293b !important;
|
||||
line-height: 1.3;
|
||||
margin-bottom: 0.875rem;
|
||||
}
|
||||
|
||||
h3 {
|
||||
font-size: 1.5rem !important; /* 24px */
|
||||
font-weight: 600;
|
||||
color: #2f3640 !important;
|
||||
line-height: 1.4;
|
||||
margin-bottom: 0.75rem;
|
||||
}
|
||||
|
||||
h4 {
|
||||
font-size: 1.25rem !important; /* 20px */
|
||||
font-weight: 600;
|
||||
color: #2f3640 !important;
|
||||
line-height: 1.4;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
h5 {
|
||||
font-size: 1.125rem !important; /* 18px */
|
||||
font-weight: 600;
|
||||
color: #2f3640 !important;
|
||||
line-height: 1.4;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
h6 {
|
||||
font-size: 1rem !important; /* 16px */
|
||||
font-weight: 600;
|
||||
color: #2f3640 !important;
|
||||
line-height: 1.4;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
/* Paragraph and body text */
|
||||
p {
|
||||
font-size: 16px !important;
|
||||
line-height: 1.6;
|
||||
color: #2f3640 !important;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
/* Sidebar improvements */
|
||||
#page-sidebar {
|
||||
font-size: 16px !important;
|
||||
}
|
||||
|
||||
#page-sidebar ul li a {
|
||||
font-size: 16px !important;
|
||||
padding: 12px 20px !important;
|
||||
color: #2f3640 !important;
|
||||
min-height: 44px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
#page-sidebar ul li a:hover {
|
||||
background-color: #f8f9fa;
|
||||
color: #5856d6 !important;
|
||||
}
|
||||
|
||||
/* Content area improvements */
|
||||
.content-box, .panel, .card {
|
||||
font-size: 16px !important;
|
||||
color: #2f3640 !important;
|
||||
background-color: #ffffff;
|
||||
border: 1px solid #e8e9ff;
|
||||
border-radius: 12px;
|
||||
padding: 20px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
/* Modal improvements */
|
||||
.modal-content {
|
||||
font-size: 16px !important;
|
||||
color: #2f3640 !important;
|
||||
}
|
||||
|
||||
.modal-title {
|
||||
font-size: 1.5rem !important;
|
||||
font-weight: 600;
|
||||
color: #1e293b !important;
|
||||
}
|
||||
|
||||
/* Alert and notification improvements */
|
||||
.alert {
|
||||
font-size: 16px !important;
|
||||
padding: 16px 20px !important;
|
||||
border-radius: 8px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.alert-success {
|
||||
background-color: #f0fdf4;
|
||||
border-color: #bbf7d0;
|
||||
color: #166534 !important;
|
||||
}
|
||||
|
||||
.alert-danger {
|
||||
background-color: #fef2f2;
|
||||
border-color: #fecaca;
|
||||
color: #dc2626 !important;
|
||||
}
|
||||
|
||||
.alert-warning {
|
||||
background-color: #fffbeb;
|
||||
border-color: #fed7aa;
|
||||
color: #d97706 !important;
|
||||
}
|
||||
|
||||
.alert-info {
|
||||
background-color: #eff6ff;
|
||||
border-color: #bfdbfe;
|
||||
color: #2563eb !important;
|
||||
}
|
||||
|
||||
/* Navigation improvements */
|
||||
.navbar-nav .nav-link {
|
||||
font-size: 16px !important;
|
||||
padding: 12px 16px !important;
|
||||
color: #2f3640 !important;
|
||||
}
|
||||
|
||||
/* Breadcrumb improvements */
|
||||
.breadcrumb {
|
||||
font-size: 16px !important;
|
||||
background-color: transparent;
|
||||
padding: 0;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.breadcrumb-item {
|
||||
color: #64748b !important;
|
||||
}
|
||||
|
||||
.breadcrumb-item.active {
|
||||
color: #2f3640 !important;
|
||||
}
|
||||
|
||||
/* Mobile-first responsive breakpoints */
|
||||
@media (max-width: 1200px) {
|
||||
.container, .container-fluid {
|
||||
padding-left: 15px;
|
||||
padding-right: 15px;
|
||||
}
|
||||
|
||||
.table-responsive {
|
||||
border: none;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 992px) {
|
||||
/* Stack columns on tablets */
|
||||
.col-md-3, .col-md-4, .col-md-6, .col-md-8, .col-md-9 {
|
||||
flex: 0 0 100%;
|
||||
max-width: 100%;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
/* Adjust sidebar for tablets */
|
||||
#page-sidebar {
|
||||
width: 100%;
|
||||
position: static;
|
||||
height: auto;
|
||||
}
|
||||
|
||||
/* Make tables horizontally scrollable */
|
||||
.table-responsive {
|
||||
overflow-x: auto;
|
||||
}
|
||||
|
||||
.table {
|
||||
min-width: 600px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
/* Mobile-specific adjustments */
|
||||
html {
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
body {
|
||||
font-size: 14px;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.container, .container-fluid {
|
||||
padding-left: 10px;
|
||||
padding-right: 10px;
|
||||
}
|
||||
|
||||
/* Stack all columns on mobile */
|
||||
.col-sm-1, .col-sm-2, .col-sm-3, .col-sm-4, .col-sm-6, .col-sm-8, .col-sm-9, .col-sm-12,
|
||||
.col-md-1, .col-md-2, .col-md-3, .col-md-4, .col-md-6, .col-md-8, .col-md-9, .col-md-12 {
|
||||
flex: 0 0 100%;
|
||||
max-width: 100%;
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
/* Adjust headings for mobile */
|
||||
h1 {
|
||||
font-size: 2rem !important; /* 32px */
|
||||
}
|
||||
|
||||
h2 {
|
||||
font-size: 1.75rem !important; /* 28px */
|
||||
}
|
||||
|
||||
h3 {
|
||||
font-size: 1.5rem !important; /* 24px */
|
||||
}
|
||||
|
||||
h4 {
|
||||
font-size: 1.25rem !important; /* 20px */
|
||||
}
|
||||
|
||||
/* Button adjustments for mobile */
|
||||
.btn {
|
||||
font-size: 16px !important;
|
||||
padding: 14px 20px !important;
|
||||
width: 100%;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.btn-group .btn {
|
||||
width: auto;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
/* Form adjustments for mobile */
|
||||
.form-control, input, textarea, select {
|
||||
font-size: 16px !important; /* Prevents zoom on iOS */
|
||||
padding: 14px 16px !important;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
/* Table adjustments for mobile */
|
||||
.table {
|
||||
font-size: 14px !important;
|
||||
}
|
||||
|
||||
.table th, .table td {
|
||||
padding: 8px 6px !important;
|
||||
font-size: 13px !important;
|
||||
}
|
||||
|
||||
/* Hide less important columns on mobile */
|
||||
.table .d-none-mobile {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
/* Modal adjustments for mobile */
|
||||
.modal-dialog {
|
||||
margin: 10px;
|
||||
width: calc(100% - 20px);
|
||||
}
|
||||
|
||||
.modal-content {
|
||||
padding: 20px 15px;
|
||||
}
|
||||
|
||||
/* Content box adjustments */
|
||||
.content-box, .panel, .card {
|
||||
padding: 15px;
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
/* Sidebar adjustments for mobile */
|
||||
#page-sidebar {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: -100%;
|
||||
width: 280px;
|
||||
height: 100vh;
|
||||
z-index: 1000;
|
||||
transition: left 0.3s ease;
|
||||
background-color: #ffffff;
|
||||
box-shadow: 2px 0 10px rgba(0,0,0,0.1);
|
||||
}
|
||||
|
||||
#page-sidebar.show {
|
||||
left: 0;
|
||||
}
|
||||
|
||||
/* Main content adjustments when sidebar is open */
|
||||
#main-content {
|
||||
transition: margin-left 0.3s ease;
|
||||
}
|
||||
|
||||
#main-content.sidebar-open {
|
||||
margin-left: 280px;
|
||||
}
|
||||
|
||||
/* Mobile menu toggle */
|
||||
.mobile-menu-toggle {
|
||||
display: block;
|
||||
position: fixed;
|
||||
top: 20px;
|
||||
left: 20px;
|
||||
z-index: 1001;
|
||||
background-color: #5856d6;
|
||||
color: white;
|
||||
border: none;
|
||||
padding: 12px;
|
||||
border-radius: 8px;
|
||||
font-size: 18px;
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 576px) {
|
||||
/* Extra small devices */
|
||||
html {
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.container, .container-fluid {
|
||||
padding-left: 8px;
|
||||
padding-right: 8px;
|
||||
}
|
||||
|
||||
/* Even smaller buttons and forms for very small screens */
|
||||
.btn {
|
||||
font-size: 14px !important;
|
||||
padding: 12px 16px !important;
|
||||
}
|
||||
|
||||
.form-control, input, textarea, select {
|
||||
font-size: 16px !important; /* Still 16px to prevent zoom */
|
||||
padding: 12px 14px !important;
|
||||
}
|
||||
|
||||
/* Compact table for very small screens */
|
||||
.table th, .table td {
|
||||
padding: 6px 4px !important;
|
||||
font-size: 12px !important;
|
||||
}
|
||||
|
||||
/* Hide even more columns on very small screens */
|
||||
.table .d-none-mobile-sm {
|
||||
display: none !important;
|
||||
}
|
||||
}
|
||||
|
||||
/* Utility classes for mobile */
|
||||
.d-none-mobile {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.d-none-mobile-sm {
|
||||
display: block;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.d-none-mobile {
|
||||
display: none !important;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 576px) {
|
||||
.d-none-mobile-sm {
|
||||
display: none !important;
|
||||
}
|
||||
}
|
||||
|
||||
/* Ensure all text has proper contrast */
|
||||
.text-white {
|
||||
color: #ffffff !important;
|
||||
}
|
||||
|
||||
.text-dark {
|
||||
color: #2f3640 !important;
|
||||
}
|
||||
|
||||
.text-muted {
|
||||
color: #2f3640 !important; /* Dark text for better readability */
|
||||
}
|
||||
|
||||
/* Fix any light text on light backgrounds */
|
||||
.bg-light .text-muted,
|
||||
.bg-white .text-muted,
|
||||
.panel .text-muted {
|
||||
color: #2f3640 !important; /* Dark text for better readability */
|
||||
}
|
||||
|
||||
/* Ensure proper spacing for touch targets */
|
||||
a, button, input, select, textarea {
|
||||
min-height: 44px;
|
||||
min-width: 44px;
|
||||
}
|
||||
|
||||
/* Additional text readability improvements */
|
||||
/* Fix any green text issues */
|
||||
.ng-binding {
|
||||
color: #2f3640 !important; /* Normal dark text instead of green */
|
||||
}
|
||||
|
||||
/* Ensure all text elements have proper contrast */
|
||||
span, div, p, label, td, th {
|
||||
color: inherit;
|
||||
}
|
||||
|
||||
/* Fix specific text color issues */
|
||||
.text-success {
|
||||
color: #059669 !important; /* Darker green for better readability */
|
||||
}
|
||||
|
||||
.text-info {
|
||||
color: #0284c7 !important; /* Darker blue for better readability */
|
||||
}
|
||||
|
||||
.text-warning {
|
||||
color: #d97706 !important; /* Darker orange for better readability */
|
||||
}
|
||||
|
||||
/* Override Bootstrap's muted text */
|
||||
.text-muted {
|
||||
color: #2f3640 !important; /* Dark text instead of grey */
|
||||
}
|
||||
|
||||
/* Fix any remaining light text on light backgrounds */
|
||||
.bg-white .text-light,
|
||||
.bg-light .text-light,
|
||||
.panel .text-light,
|
||||
.card .text-light {
|
||||
color: #2f3640 !important;
|
||||
}
|
||||
|
||||
/* Fix for small clickable elements */
|
||||
.glyph-icon, .icon {
|
||||
min-width: 44px;
|
||||
min-height: 44px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
/* Loading and spinner improvements */
|
||||
.spinner, .loading {
|
||||
font-size: 16px !important;
|
||||
color: #5856d6 !important;
|
||||
}
|
||||
|
||||
/* Print styles */
|
||||
@media print {
|
||||
body {
|
||||
font-size: 12pt;
|
||||
color: #000000 !important;
|
||||
background: #ffffff !important;
|
||||
}
|
||||
|
||||
.table th, .table td {
|
||||
font-size: 10pt !important;
|
||||
color: #000000 !important;
|
||||
}
|
||||
|
||||
.btn, .alert, .modal {
|
||||
display: none !important;
|
||||
}
|
||||
}
|
||||
265
baseTemplate/static/baseTemplate/assets/readability-fixes.css
Normal file
265
baseTemplate/static/baseTemplate/assets/readability-fixes.css
Normal file
@@ -0,0 +1,265 @@
|
||||
/* CyberPanel Readability & Design Fixes */
|
||||
/* This file fixes the core design issues with grey text and color inconsistencies */
|
||||
|
||||
/* Override CSS Variables for Better Text Contrast */
|
||||
:root {
|
||||
/* Ensure all text uses proper dark colors for readability */
|
||||
--text-primary: #2f3640;
|
||||
--text-secondary: #2f3640; /* Changed from grey to dark for better readability */
|
||||
--text-heading: #1e293b;
|
||||
}
|
||||
|
||||
/* Dark theme also uses proper contrast */
|
||||
[data-theme="dark"] {
|
||||
--text-primary: #e4e4e7;
|
||||
--text-secondary: #e4e4e7; /* Changed from grey to light for better readability */
|
||||
--text-heading: #f3f4f6;
|
||||
}
|
||||
|
||||
/* Fix Green Text Issues */
|
||||
/* Override Angular binding colors that might be green */
|
||||
.ng-binding {
|
||||
color: var(--text-secondary) !important;
|
||||
}
|
||||
|
||||
/* Specific fix for uptime display */
|
||||
#sidebar .server-info .info-line span,
|
||||
#sidebar .server-info .info-line .ng-binding,
|
||||
.server-info .ng-binding {
|
||||
color: var(--text-secondary) !important;
|
||||
}
|
||||
|
||||
/* Fix Grey Text on White Background */
|
||||
/* Override all muted and secondary text classes */
|
||||
.text-muted,
|
||||
.text-secondary,
|
||||
.text-light,
|
||||
small,
|
||||
.small,
|
||||
.text-small {
|
||||
color: var(--text-secondary) !important;
|
||||
}
|
||||
|
||||
/* Fix specific Bootstrap classes */
|
||||
.text-muted {
|
||||
color: #2f3640 !important; /* Dark text for better readability */
|
||||
}
|
||||
|
||||
/* Fix text on white/light backgrounds */
|
||||
.bg-white .text-muted,
|
||||
.bg-light .text-muted,
|
||||
.panel .text-muted,
|
||||
.card .text-muted,
|
||||
.content-box .text-muted {
|
||||
color: #2f3640 !important;
|
||||
}
|
||||
|
||||
/* Fix menu items and navigation */
|
||||
#sidebar .menu-item,
|
||||
#sidebar .menu-item span,
|
||||
#sidebar .menu-item i,
|
||||
.sidebar .menu-item,
|
||||
.sidebar .menu-item span,
|
||||
.sidebar .menu-item i {
|
||||
color: var(--text-secondary) !important;
|
||||
}
|
||||
|
||||
#sidebar .menu-item:hover,
|
||||
.sidebar .menu-item:hover {
|
||||
color: var(--accent-color) !important;
|
||||
}
|
||||
|
||||
#sidebar .menu-item.active,
|
||||
.sidebar .menu-item.active {
|
||||
color: white !important;
|
||||
}
|
||||
|
||||
/* Fix server info and details */
|
||||
.server-info,
|
||||
.server-info *,
|
||||
.server-details,
|
||||
.server-details *,
|
||||
.info-line,
|
||||
.info-line span,
|
||||
.info-line strong,
|
||||
.tagline,
|
||||
.brand {
|
||||
color: inherit !important;
|
||||
}
|
||||
|
||||
/* Fix form elements */
|
||||
label,
|
||||
.control-label,
|
||||
.form-label {
|
||||
color: var(--text-primary) !important;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
/* Fix table text */
|
||||
.table th,
|
||||
.table td {
|
||||
color: var(--text-primary) !important;
|
||||
}
|
||||
|
||||
.table th {
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
/* Fix alert text */
|
||||
.alert {
|
||||
color: var(--text-primary) !important;
|
||||
}
|
||||
|
||||
.alert-success {
|
||||
color: #059669 !important; /* Darker green for better readability */
|
||||
}
|
||||
|
||||
.alert-info {
|
||||
color: #0284c7 !important; /* Darker blue for better readability */
|
||||
}
|
||||
|
||||
.alert-warning {
|
||||
color: #d97706 !important; /* Darker orange for better readability */
|
||||
}
|
||||
|
||||
.alert-danger {
|
||||
color: #dc2626 !important; /* Darker red for better readability */
|
||||
}
|
||||
|
||||
/* Fix breadcrumb text */
|
||||
.breadcrumb-item {
|
||||
color: var(--text-secondary) !important;
|
||||
}
|
||||
|
||||
.breadcrumb-item.active {
|
||||
color: var(--text-primary) !important;
|
||||
}
|
||||
|
||||
/* Fix modal text */
|
||||
.modal-content {
|
||||
color: var(--text-primary) !important;
|
||||
}
|
||||
|
||||
.modal-title {
|
||||
color: var(--text-heading) !important;
|
||||
}
|
||||
|
||||
/* Fix button text */
|
||||
.btn {
|
||||
color: inherit;
|
||||
}
|
||||
|
||||
/* Fix any remaining light text issues */
|
||||
.bg-light .text-light,
|
||||
.bg-white .text-light,
|
||||
.panel .text-light,
|
||||
.card .text-light {
|
||||
color: #2f3640 !important;
|
||||
}
|
||||
|
||||
/* Ensure proper contrast for all text elements */
|
||||
span, div, p, label, td, th, a, li {
|
||||
color: inherit;
|
||||
}
|
||||
|
||||
/* Fix specific color classes */
|
||||
.text-success {
|
||||
color: #059669 !important; /* Darker green for better readability */
|
||||
}
|
||||
|
||||
.text-info {
|
||||
color: #0284c7 !important; /* Darker blue for better readability */
|
||||
}
|
||||
|
||||
.text-warning {
|
||||
color: #d97706 !important; /* Darker orange for better readability */
|
||||
}
|
||||
|
||||
.text-danger {
|
||||
color: #dc2626 !important; /* Darker red for better readability */
|
||||
}
|
||||
|
||||
/* Fix any Angular-specific styling */
|
||||
[ng-controller] {
|
||||
color: inherit;
|
||||
}
|
||||
|
||||
[ng-show],
|
||||
[ng-hide],
|
||||
[ng-if] {
|
||||
color: inherit;
|
||||
}
|
||||
|
||||
/* Ensure all content areas have proper text color */
|
||||
.content-box,
|
||||
.panel,
|
||||
.card,
|
||||
.main-content,
|
||||
.page-content {
|
||||
color: var(--text-primary) !important;
|
||||
}
|
||||
|
||||
/* Fix any remaining Bootstrap classes */
|
||||
.text-dark {
|
||||
color: #2f3640 !important;
|
||||
}
|
||||
|
||||
.text-body {
|
||||
color: var(--text-primary) !important;
|
||||
}
|
||||
|
||||
/* Mobile-specific fixes */
|
||||
@media (max-width: 768px) {
|
||||
/* Ensure mobile text is also readable */
|
||||
body,
|
||||
.container,
|
||||
.container-fluid {
|
||||
color: var(--text-primary) !important;
|
||||
}
|
||||
|
||||
/* Fix mobile menu text */
|
||||
.mobile-menu .menu-item,
|
||||
.mobile-menu .menu-item span {
|
||||
color: var(--text-secondary) !important;
|
||||
}
|
||||
}
|
||||
|
||||
/* Print styles */
|
||||
@media print {
|
||||
body,
|
||||
.content-box,
|
||||
.panel,
|
||||
.card {
|
||||
color: #000000 !important;
|
||||
background: #ffffff !important;
|
||||
}
|
||||
|
||||
.text-muted,
|
||||
.text-secondary {
|
||||
color: #000000 !important;
|
||||
}
|
||||
}
|
||||
|
||||
/* High contrast mode support */
|
||||
@media (prefers-contrast: high) {
|
||||
:root {
|
||||
--text-primary: #000000;
|
||||
--text-secondary: #000000;
|
||||
--text-heading: #000000;
|
||||
}
|
||||
|
||||
[data-theme="dark"] {
|
||||
--text-primary: #ffffff;
|
||||
--text-secondary: #ffffff;
|
||||
--text-heading: #ffffff;
|
||||
}
|
||||
}
|
||||
|
||||
/* Reduced motion support */
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
* {
|
||||
animation-duration: 0.01ms !important;
|
||||
animation-iteration-count: 1 !important;
|
||||
transition-duration: 0.01ms !important;
|
||||
}
|
||||
}
|
||||
@@ -10,7 +10,7 @@ function getCookie(name) {
|
||||
if (document.cookie && document.cookie !== '') {
|
||||
var cookies = document.cookie.split(';');
|
||||
for (var i = 0; i < cookies.length; i++) {
|
||||
var cookie = jQuery.trim(cookies[i]);
|
||||
var cookie = (cookies[i] || '').replace(/^\s+|\s+$/g, '');
|
||||
// Does this cookie string begin with the name we want?
|
||||
if (cookie.substring(0, name.length + 1) === (name + '=')) {
|
||||
cookieValue = decodeURIComponent(cookie.substring(name.length + 1));
|
||||
@@ -39,6 +39,77 @@ function randomPassword(length) {
|
||||
window.app = angular.module('CyberCP', []);
|
||||
var app = window.app; // Local reference for this file
|
||||
|
||||
// MUST be first: register dashboard controller before any other setup (avoids ctrlreg when CDN/Tracking Prevention blocks scripts)
|
||||
app.controller('dashboardStatsController', ['$scope', '$http', '$timeout', function ($scope, $http, $timeout) {
|
||||
$scope.cpuUsage = 0; $scope.ramUsage = 0; $scope.diskUsage = 0; $scope.cpuCores = 0;
|
||||
$scope.ramTotalMB = 0; $scope.diskTotalGB = 0; $scope.diskFreeGB = 0;
|
||||
$scope.totalUsers = 0; $scope.totalSites = 0; $scope.totalWPSites = 0;
|
||||
$scope.totalDBs = 0; $scope.totalEmails = 0; $scope.totalFTPUsers = 0;
|
||||
$scope.topProcesses = []; $scope.sshLogins = []; $scope.sshLogs = [];
|
||||
$scope.loadingTopProcesses = true; $scope.loadingSSHLogins = true; $scope.loadingSSHLogs = true;
|
||||
$scope.blockedIPs = {}; $scope.blockingIP = null; $scope.securityAlerts = [];
|
||||
var opts = { headers: { 'X-CSRFToken': (typeof getCookie === 'function') ? getCookie('csrftoken') : '' } };
|
||||
try {
|
||||
$http.get('/base/getSystemStatus', opts).then(function (r) {
|
||||
if (r && r.data && r.data.status === 1) {
|
||||
$scope.cpuUsage = r.data.cpuUsage || 0; $scope.ramUsage = r.data.ramUsage || 0;
|
||||
$scope.diskUsage = r.data.diskUsage || 0; $scope.cpuCores = r.data.cpuCores || 0;
|
||||
$scope.ramTotalMB = r.data.ramTotalMB || 0; $scope.diskTotalGB = r.data.diskTotalGB || 0;
|
||||
$scope.diskFreeGB = r.data.diskFreeGB || 0;
|
||||
}
|
||||
});
|
||||
$http.get('/base/getDashboardStats', opts).then(function (r) {
|
||||
if (r && r.data && r.data.status === 1) {
|
||||
$scope.totalUsers = r.data.total_users || 0; $scope.totalSites = r.data.total_sites || 0;
|
||||
$scope.totalWPSites = r.data.total_wp_sites || 0; $scope.totalDBs = r.data.total_dbs || 0;
|
||||
$scope.totalEmails = r.data.total_emails || 0; $scope.totalFTPUsers = r.data.total_ftp_users || 0;
|
||||
}
|
||||
});
|
||||
$http.get('/base/getRecentSSHLogins', opts).then(function (r) {
|
||||
$scope.loadingSSHLogins = false;
|
||||
$scope.sshLogins = (r && r.data && r.data.logins) ? r.data.logins : [];
|
||||
}, function () { $scope.loadingSSHLogins = false; $scope.sshLogins = []; });
|
||||
$http.get('/base/getRecentSSHLogs', opts).then(function (r) {
|
||||
$scope.loadingSSHLogs = false;
|
||||
$scope.sshLogs = (r && r.data && r.data.logs) ? r.data.logs : [];
|
||||
}, function () { $scope.loadingSSHLogs = false; $scope.sshLogs = []; });
|
||||
$http.get('/base/getTopProcesses', opts).then(function (r) {
|
||||
$scope.loadingTopProcesses = false;
|
||||
$scope.topProcesses = (r && r.data && r.data.status === 1 && r.data.processes) ? r.data.processes : [];
|
||||
}, function () { $scope.loadingTopProcesses = false; $scope.topProcesses = []; });
|
||||
if (typeof $timeout === 'function') { $timeout(function() { /* refresh */ }, 10000); }
|
||||
} catch (e) { /* ignore */ }
|
||||
}]);
|
||||
|
||||
// Overview CPU/RAM/Disk cards use systemStatusInfo – register early so data loads even if later script fails
|
||||
app.controller('systemStatusInfo', ['$scope', '$http', '$timeout', function ($scope, $http, $timeout) {
|
||||
$scope.uptimeLoaded = false;
|
||||
$scope.uptime = 'Loading...';
|
||||
$scope.cpuUsage = 0; $scope.ramUsage = 0; $scope.diskUsage = 0;
|
||||
$scope.cpuCores = 0; $scope.ramTotalMB = 0; $scope.diskTotalGB = 0; $scope.diskFreeGB = 0;
|
||||
$scope.getSystemStatus = function() { fetchStatus(); };
|
||||
function fetchStatus() {
|
||||
try {
|
||||
var csrf = (typeof getCookie === 'function') ? getCookie('csrftoken') : '';
|
||||
$http.get('/base/getSystemStatus', { headers: { 'X-CSRFToken': csrf } }).then(function (r) {
|
||||
if (r && r.data && r.data.status === 1) {
|
||||
$scope.cpuUsage = r.data.cpuUsage != null ? r.data.cpuUsage : 0;
|
||||
$scope.ramUsage = r.data.ramUsage != null ? r.data.ramUsage : 0;
|
||||
$scope.diskUsage = r.data.diskUsage != null ? r.data.diskUsage : 0;
|
||||
$scope.cpuCores = r.data.cpuCores != null ? r.data.cpuCores : 0;
|
||||
$scope.ramTotalMB = r.data.ramTotalMB != null ? r.data.ramTotalMB : 0;
|
||||
$scope.diskTotalGB = r.data.diskTotalGB != null ? r.data.diskTotalGB : 0;
|
||||
$scope.diskFreeGB = r.data.diskFreeGB != null ? r.data.diskFreeGB : 0;
|
||||
$scope.uptime = r.data.uptime || 'N/A';
|
||||
}
|
||||
$scope.uptimeLoaded = true;
|
||||
}, function() { $scope.uptime = 'Unavailable'; $scope.uptimeLoaded = true; });
|
||||
if (typeof $timeout === 'function') { $timeout(fetchStatus, 60000); }
|
||||
} catch (e) { $scope.uptimeLoaded = true; }
|
||||
}
|
||||
fetchStatus();
|
||||
}]);
|
||||
|
||||
var globalScope;
|
||||
|
||||
function GlobalRespSuccess(response) {
|
||||
@@ -122,9 +193,17 @@ app.controller('systemStatusInfo', function ($scope, $http, $timeout) {
|
||||
|
||||
$scope.uptimeLoaded = false;
|
||||
$scope.uptime = 'Loading...';
|
||||
|
||||
// Defaults so template never shows undefined (avoids raw {$ cpuUsage $} when API is slow or fails)
|
||||
$scope.cpuUsage = 0;
|
||||
$scope.ramUsage = 0;
|
||||
$scope.diskUsage = 0;
|
||||
$scope.cpuCores = 0;
|
||||
$scope.ramTotalMB = 0;
|
||||
$scope.diskTotalGB = 0;
|
||||
$scope.diskFreeGB = 0;
|
||||
|
||||
getStuff();
|
||||
|
||||
|
||||
$scope.getSystemStatus = function() {
|
||||
getStuff();
|
||||
};
|
||||
@@ -138,17 +217,15 @@ app.controller('systemStatusInfo', function ($scope, $http, $timeout) {
|
||||
|
||||
function ListInitialData(response) {
|
||||
|
||||
$scope.cpuUsage = response.data.cpuUsage;
|
||||
$scope.ramUsage = response.data.ramUsage;
|
||||
$scope.diskUsage = response.data.diskUsage;
|
||||
|
||||
// Total system information
|
||||
$scope.cpuCores = response.data.cpuCores;
|
||||
$scope.ramTotalMB = response.data.ramTotalMB;
|
||||
$scope.diskTotalGB = response.data.diskTotalGB;
|
||||
$scope.diskFreeGB = response.data.diskFreeGB;
|
||||
|
||||
// Get uptime if available
|
||||
$scope.cpuUsage = response.data.cpuUsage != null ? response.data.cpuUsage : 0;
|
||||
$scope.ramUsage = response.data.ramUsage != null ? response.data.ramUsage : 0;
|
||||
$scope.diskUsage = response.data.diskUsage != null ? response.data.diskUsage : 0;
|
||||
|
||||
$scope.cpuCores = response.data.cpuCores != null ? response.data.cpuCores : 0;
|
||||
$scope.ramTotalMB = response.data.ramTotalMB != null ? response.data.ramTotalMB : 0;
|
||||
$scope.diskTotalGB = response.data.diskTotalGB != null ? response.data.diskTotalGB : 0;
|
||||
$scope.diskFreeGB = response.data.diskFreeGB != null ? response.data.diskFreeGB : 0;
|
||||
|
||||
if (response.data.uptime) {
|
||||
$scope.uptime = response.data.uptime;
|
||||
$scope.uptimeLoaded = true;
|
||||
@@ -162,6 +239,9 @@ app.controller('systemStatusInfo', function ($scope, $http, $timeout) {
|
||||
function cantLoadInitialData(response) {
|
||||
$scope.uptime = 'Unavailable';
|
||||
$scope.uptimeLoaded = true;
|
||||
$scope.cpuUsage = 0;
|
||||
$scope.ramUsage = 0;
|
||||
$scope.diskUsage = 0;
|
||||
}
|
||||
|
||||
$timeout(getStuff, 60000); // Update every minute
|
||||
@@ -273,11 +353,11 @@ app.controller('adminController', function ($scope, $http, $timeout) {
|
||||
}
|
||||
|
||||
if (!Boolean(response.data.deleteZone)) {
|
||||
$('.addDeleteRecords').hide();
|
||||
$('.deleteZone').hide();
|
||||
}
|
||||
|
||||
if (!Boolean(response.data.addDeleteRecords)) {
|
||||
$('.deleteDatabase').hide();
|
||||
$('.addDeleteRecords').hide();
|
||||
}
|
||||
|
||||
// Email Management
|
||||
@@ -557,15 +637,18 @@ app.controller('homePageStatus', function ($scope, $http, $timeout) {
|
||||
////////////
|
||||
|
||||
function increment() {
|
||||
$('.box').hide();
|
||||
var boxes = document.querySelectorAll ? document.querySelectorAll('.box') : [];
|
||||
for (var i = 0; i < boxes.length; i++) boxes[i].style.display = 'none';
|
||||
setTimeout(function () {
|
||||
$('.box').show();
|
||||
for (var j = 0; j < boxes.length; j++) boxes[j].style.display = '';
|
||||
}, 100);
|
||||
|
||||
|
||||
}
|
||||
|
||||
increment();
|
||||
if (document.readyState === 'loading') {
|
||||
document.addEventListener('DOMContentLoaded', increment);
|
||||
} else {
|
||||
increment();
|
||||
}
|
||||
|
||||
////////////
|
||||
|
||||
@@ -579,6 +662,7 @@ app.controller('versionManagment', function ($scope, $http, $timeout) {
|
||||
$scope.updateFinish = true;
|
||||
$scope.couldNotConnect = true;
|
||||
|
||||
var upgradeStatusTimer = null;
|
||||
|
||||
$scope.upgrade = function () {
|
||||
|
||||
@@ -660,7 +744,8 @@ app.controller('versionManagment', function ($scope, $http, $timeout) {
|
||||
if (response.data.upgradeStatus === 1) {
|
||||
|
||||
if (response.data.finished === 1) {
|
||||
$timeout.cancel();
|
||||
if (upgradeStatusTimer) $timeout.cancel(upgradeStatusTimer);
|
||||
upgradeStatusTimer = null;
|
||||
$scope.upgradelogBox = false;
|
||||
$scope.upgradeLog = response.data.upgradeLog;
|
||||
$scope.upgradeLoading = true;
|
||||
@@ -672,7 +757,7 @@ app.controller('versionManagment', function ($scope, $http, $timeout) {
|
||||
} else {
|
||||
$scope.upgradelogBox = false;
|
||||
$scope.upgradeLog = response.data.upgradeLog;
|
||||
timeout(getUpgradeStatus, 2000);
|
||||
upgradeStatusTimer = $timeout(getUpgradeStatus, 2000);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -900,7 +985,8 @@ app.controller('OnboardingCP', function ($scope, $http, $timeout, $window) {
|
||||
|
||||
});
|
||||
|
||||
app.controller('dashboardStatsController', function ($scope, $http, $timeout) {
|
||||
// Single implementation registered under both names for compatibility (some templates/caches use newDashboardStat)
|
||||
var dashboardStatsControllerFn = function ($scope, $http, $timeout) {
|
||||
console.log('dashboardStatsController initialized');
|
||||
|
||||
// Card values
|
||||
@@ -920,7 +1006,8 @@ app.controller('dashboardStatsController', function ($scope, $http, $timeout) {
|
||||
$scope.errorTopProcesses = '';
|
||||
$scope.refreshTopProcesses = function() {
|
||||
$scope.loadingTopProcesses = true;
|
||||
$http.get('/base/getTopProcesses').then(function (response) {
|
||||
var h = { headers: { 'X-CSRFToken': (typeof getCookie === 'function') ? getCookie('csrftoken') : '' } };
|
||||
$http.get('/base/getTopProcesses', h).then(function (response) {
|
||||
$scope.loadingTopProcesses = false;
|
||||
if (response.data && response.data.status === 1 && response.data.processes) {
|
||||
$scope.topProcesses = response.data.processes;
|
||||
@@ -939,7 +1026,8 @@ app.controller('dashboardStatsController', function ($scope, $http, $timeout) {
|
||||
$scope.errorSSHLogins = '';
|
||||
$scope.refreshSSHLogins = function() {
|
||||
$scope.loadingSSHLogins = true;
|
||||
$http.get('/base/getRecentSSHLogins').then(function (response) {
|
||||
var h = { headers: { 'X-CSRFToken': (typeof getCookie === 'function') ? getCookie('csrftoken') : '' } };
|
||||
$http.get('/base/getRecentSSHLogins', h).then(function (response) {
|
||||
$scope.loadingSSHLogins = false;
|
||||
if (response.data && response.data.logins) {
|
||||
$scope.sshLogins = response.data.logins;
|
||||
@@ -967,7 +1055,8 @@ app.controller('dashboardStatsController', function ($scope, $http, $timeout) {
|
||||
$scope.loadingSecurityAnalysis = false;
|
||||
$scope.refreshSSHLogs = function() {
|
||||
$scope.loadingSSHLogs = true;
|
||||
$http.get('/base/getRecentSSHLogs').then(function (response) {
|
||||
var h = { headers: { 'X-CSRFToken': (typeof getCookie === 'function') ? getCookie('csrftoken') : '' } };
|
||||
$http.get('/base/getRecentSSHLogs', h).then(function (response) {
|
||||
$scope.loadingSSHLogs = false;
|
||||
if (response.data && response.data.logs) {
|
||||
$scope.sshLogs = response.data.logs;
|
||||
@@ -1073,19 +1162,8 @@ app.controller('dashboardStatsController', function ($scope, $http, $timeout) {
|
||||
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;
|
||||
}
|
||||
// Do not early-return when IP is already in blockedIPs: still call the API so the
|
||||
// backend can close any active connections from this IP (already-banned path).
|
||||
|
||||
// Set blocking flag to prevent duplicate requests
|
||||
$scope.blockingIP = ipAddress;
|
||||
@@ -1130,19 +1208,12 @@ app.controller('dashboardStatsController', function ($scope, $http, $timeout) {
|
||||
console.log('Parsed responseData from string:', responseData);
|
||||
} catch (e) {
|
||||
console.error('Failed to parse response as JSON:', e);
|
||||
console.error('Raw response string:', responseData);
|
||||
// Try to extract error from string
|
||||
if (responseData.includes('error')) {
|
||||
if (typeof PNotify !== 'undefined') {
|
||||
new PNotify({
|
||||
title: 'Error',
|
||||
text: 'Failed to block IP address: ' + responseData,
|
||||
type: 'error',
|
||||
delay: 5000
|
||||
});
|
||||
}
|
||||
return;
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1159,11 +1230,12 @@ app.controller('dashboardStatsController', function ($scope, $http, $timeout) {
|
||||
}
|
||||
$scope.blockedIPs[ipAddress] = true;
|
||||
|
||||
// Show success notification
|
||||
// Show success notification (use server message when present, e.g. already-banned + connections closed)
|
||||
if (typeof PNotify !== 'undefined') {
|
||||
var successText = (responseData.message && responseData.message.length) ? responseData.message : `IP address ${ipAddress} has been permanently banned and added to the firewall. You can manage it in the Firewall > Banned IPs section.`;
|
||||
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.`,
|
||||
text: successText,
|
||||
type: 'success',
|
||||
delay: 5000
|
||||
});
|
||||
@@ -1217,28 +1289,20 @@ app.controller('dashboardStatsController', function ($scope, $http, $timeout) {
|
||||
$scope.lastErrorTime = Date.now();
|
||||
|
||||
var errorMessage = 'Failed to block IP address';
|
||||
if (err.data) {
|
||||
var errData = err.data;
|
||||
if (typeof errData === 'string') {
|
||||
try {
|
||||
errData = JSON.parse(errData);
|
||||
} catch (e) {
|
||||
errorMessage = errData || errorMessage;
|
||||
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') {
|
||||
if (errData.error_message) {
|
||||
errorMessage = errData.error_message;
|
||||
} else if (errData.error) {
|
||||
errorMessage = errData.error;
|
||||
} else if (errData.message) {
|
||||
errorMessage = errData.message;
|
||||
}
|
||||
}
|
||||
} else if (err.statusText) {
|
||||
errorMessage = err.statusText;
|
||||
}
|
||||
if (errData && typeof errData === 'object') {
|
||||
errorMessage = errData.error_message || errData.error || errData.message || errorMessage;
|
||||
} else if (err.status) {
|
||||
errorMessage = `HTTP ${err.status}: ${err.statusText || 'Unknown error'}`;
|
||||
errorMessage = 'HTTP ' + err.status + ': ' + (errorMessage);
|
||||
}
|
||||
|
||||
console.error('Final error message:', errorMessage);
|
||||
@@ -1287,14 +1351,9 @@ app.controller('dashboardStatsController', function ($scope, $http, $timeout) {
|
||||
return; // Already processing
|
||||
}
|
||||
|
||||
if ($scope.blockedIPs[ipAddress]) {
|
||||
new PNotify({
|
||||
title: 'Info',
|
||||
text: `IP address ${ipAddress} is already banned`,
|
||||
type: 'info',
|
||||
delay: 3000
|
||||
});
|
||||
return;
|
||||
// Still call API when already in blockedIPs so backend can close active connections
|
||||
if (!$scope.blockedIPs) {
|
||||
$scope.blockedIPs = {};
|
||||
}
|
||||
|
||||
$scope.blockingIP = ipAddress;
|
||||
@@ -1380,14 +1439,9 @@ app.controller('dashboardStatsController', function ($scope, $http, $timeout) {
|
||||
return; // Already processing
|
||||
}
|
||||
|
||||
if ($scope.blockedIPs[ipAddress]) {
|
||||
new PNotify({
|
||||
title: 'Info',
|
||||
text: `IP address ${ipAddress} is already banned`,
|
||||
type: 'info',
|
||||
delay: 3000
|
||||
});
|
||||
return;
|
||||
// Still call API when already in blockedIPs so backend can close active connections
|
||||
if (!$scope.blockedIPs) {
|
||||
$scope.blockedIPs = {};
|
||||
}
|
||||
|
||||
$scope.blockingIP = ipAddress;
|
||||
@@ -1474,6 +1528,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');
|
||||
@@ -1532,8 +1592,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;
|
||||
@@ -1581,13 +1641,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;
|
||||
@@ -1626,11 +1689,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;
|
||||
@@ -1669,13 +1735,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: [],
|
||||
@@ -1767,7 +1854,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: {
|
||||
@@ -1852,7 +1941,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: {
|
||||
@@ -1925,6 +2017,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) {
|
||||
@@ -1957,19 +2053,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();
|
||||
@@ -1979,7 +2076,7 @@ app.controller('dashboardStatsController', function ($scope, $http, $timeout) {
|
||||
$timeout(pollAll, pollInterval);
|
||||
}
|
||||
pollAll();
|
||||
}, 500);
|
||||
}, 800);
|
||||
|
||||
// SSH User Activity Modal
|
||||
$scope.showSSHActivityModal = false;
|
||||
@@ -2425,4 +2522,6 @@ app.controller('dashboardStatsController', function ($scope, $http, $timeout) {
|
||||
$scope.closeSSHActivityModal();
|
||||
}
|
||||
};
|
||||
});
|
||||
};
|
||||
app.controller('dashboardStatsController', dashboardStatsControllerFn);
|
||||
app.controller('newDashboardStat', dashboardStatsControllerFn);
|
||||
2
baseTemplate/static/baseTemplate/vendor/select2/select2.full.min.js
vendored
Normal file
2
baseTemplate/static/baseTemplate/vendor/select2/select2.full.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
1
baseTemplate/static/baseTemplate/vendor/select2/select2.min.css
vendored
Normal file
1
baseTemplate/static/baseTemplate/vendor/select2/select2.min.css
vendored
Normal file
File diff suppressed because one or more lines are too long
@@ -11,7 +11,7 @@
|
||||
<link rel="icon" type="image/png" href="{% static 'baseTemplate/assets/finalBase/favicon.png' %}">
|
||||
|
||||
<!-- Bootstrap CSS -->
|
||||
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0-beta.2/css/bootstrap.min.css" integrity="sha384-PsH8R72JQ3SOdhVi3uxftmaW6Vc51MKb0q5P2rRUpPvrszuE4W1povHYgTpBfshb" crossorigin="anonymous">
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/twitter-bootstrap/4.0.0-beta.2/css/bootstrap.min.css" integrity="sha384-PsH8R72JQ3SOdhVi3uxftmaW6Vc51MKb0q5P2rRUpPvrszuE4W1povHYgTpBfshb" crossorigin="anonymous">
|
||||
<link rel="stylesheet" href="{% static 'filemanager/images/fonts/css/font-awesome.min.css' %}">
|
||||
<link rel="stylesheet" href="{% static 'filemanager/css/fileManager.css' %}">
|
||||
|
||||
@@ -52,7 +52,7 @@
|
||||
</div-->
|
||||
<ul class="nav mr-10">
|
||||
<li class="nav-item">
|
||||
<a onclick="return false;" ng-click="showUploadBox()" class="nav-link point-events" href="#"><i class="fa fa-upload" aria-hidden="true"></i> {% trans "Upload" %}</a>
|
||||
<a id="uploadTriggerBtn" onclick="return false;" ng-click="showUploadBox()" class="nav-link point-events" href="#"><i class="fa fa-upload" aria-hidden="true"></i> {% trans "Upload" %}</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a onclick="return false;" ng-click="showCreateFileModal()" class="nav-link point-events" href="#"><i class="fa fa-plus-square" aria-hidden="true"></i> {% trans "New File" %}</a>
|
||||
@@ -608,7 +608,7 @@
|
||||
|
||||
<script src="https://code.jquery.com/jquery-3.2.1.slim.min.js" integrity="sha384-KJ3o2DKtIkvYIK3UENzmM7KCkRr/rE9/Qpg6aAZGJwFDMVNA/GpGFF93hXpG5KkN" crossorigin="anonymous"></script>
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/popper.js/1.12.3/umd/popper.min.js" integrity="sha384-vFJXuSJphROIrBnz7yo7oB41mKfc8JzQZiCq4NCceLEaO4IHwicKwpJf9c9IpFgh" crossorigin="anonymous"></script>
|
||||
<script src="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0-beta.2/js/bootstrap.min.js" integrity="sha384-alpBpkh1PFOepccYVYDB4do5UnbKysX5WZXm3XxPqe5iKTfUKjNkCk9SaVuEZflJ" crossorigin="anonymous"></script>
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/twitter-bootstrap/4.0.0-beta.2/js/bootstrap.min.js" integrity="sha384-alpBpkh1PFOepccYVYDB4do5UnbKysX5WZXm3XxPqe5iKTfUKjNkCk9SaVuEZflJ" crossorigin="anonymous"></script>
|
||||
|
||||
|
||||
<!-- HTML Editor Include -->
|
||||
|
||||
@@ -876,6 +876,29 @@
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<!-- SSH Logins Pagination -->
|
||||
<div ng-if="!loadingSSHLogins && sshLogins.length > 0" class="pagination-controls" style="display: flex; justify-content: space-between; align-items: center; flex-wrap: wrap; gap: 12px; margin-top: 16px; padding: 12px 0; border-top: 1px solid #e8e9ff;">
|
||||
<div style="display: flex; align-items: center; gap: 12px;">
|
||||
<span style="color: #64748b; font-size: 13px;">Show</span>
|
||||
<select ng-model="sshLoginsPerPage" ng-change="sshLoginsChangePerPage()" style="padding: 6px 10px; border: 1px solid #e2e8f0; border-radius: 6px; font-size: 13px; background: white;">
|
||||
<option value="10">10</option>
|
||||
<option value="20">20</option>
|
||||
<option value="50">50</option>
|
||||
<option value="100">100</option>
|
||||
</select>
|
||||
<span style="color: #64748b; font-size: 13px;">per page</span>
|
||||
</div>
|
||||
<div style="display: flex; align-items: center; gap: 8px;">
|
||||
<span style="color: #64748b; font-size: 13px;">{$ (sshLoginsPage - 1) * sshLoginsPerPage + 1 $}-{$ (sshLoginsPage * sshLoginsPerPage > sshLoginsTotal ? sshLoginsTotal : sshLoginsPage * sshLoginsPerPage) $} of {$ sshLoginsTotal $}</span>
|
||||
<button ng-click="sshLoginsGoToPage(sshLoginsPage - 1)" ng-disabled="sshLoginsPage <= 1" style="padding: 6px 12px; border: 1px solid #e2e8f0; border-radius: 6px; background: white; cursor: pointer; font-size: 13px;" title="Previous">
|
||||
<i class="fas fa-chevron-left"></i>
|
||||
</button>
|
||||
<button ng-click="sshLoginsGoToPage(sshLoginsPage + 1)" ng-disabled="sshLoginsPage >= sshLoginsTotalPages" style="padding: 6px 12px; border: 1px solid #e2e8f0; border-radius: 6px; background: white; cursor: pointer; font-size: 13px;" title="Next">
|
||||
<i class="fas fa-chevron-right"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Dummy data for demonstration -->
|
||||
<table class="activity-table records-table" ng-if="loadingSSHLogins">
|
||||
<thead>
|
||||
@@ -1065,6 +1088,29 @@
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<!-- SSH Logs Pagination -->
|
||||
<div ng-if="!loadingSSHLogs && sshLogs.length > 0" class="pagination-controls" style="display: flex; justify-content: space-between; align-items: center; flex-wrap: wrap; gap: 12px; margin-top: 16px; padding: 12px 0; border-top: 1px solid #e8e9ff;">
|
||||
<div style="display: flex; align-items: center; gap: 12px;">
|
||||
<span style="color: #64748b; font-size: 13px;">Show</span>
|
||||
<select ng-model="sshLogsPerPage" ng-change="sshLogsChangePerPage()" style="padding: 6px 10px; border: 1px solid #e2e8f0; border-radius: 6px; font-size: 13px; background: white;">
|
||||
<option value="10">10</option>
|
||||
<option value="25">25</option>
|
||||
<option value="50">50</option>
|
||||
<option value="100">100</option>
|
||||
</select>
|
||||
<span style="color: #64748b; font-size: 13px;">per page</span>
|
||||
</div>
|
||||
<div style="display: flex; align-items: center; gap: 8px;">
|
||||
<span style="color: #64748b; font-size: 13px;">{$ (sshLogsPage - 1) * sshLogsPerPage + 1 $}-{$ (sshLogsPage * sshLogsPerPage > sshLogsTotal ? sshLogsTotal : sshLogsPage * sshLogsPerPage) $} of {$ sshLogsTotal $}</span>
|
||||
<button ng-click="sshLogsGoToPage(sshLogsPage - 1)" ng-disabled="sshLogsPage <= 1" style="padding: 6px 12px; border: 1px solid #e2e8f0; border-radius: 6px; background: white; cursor: pointer; font-size: 13px;" title="Previous">
|
||||
<i class="fas fa-chevron-left"></i>
|
||||
</button>
|
||||
<button ng-click="sshLogsGoToPage(sshLogsPage + 1)" ng-disabled="sshLogsPage >= sshLogsTotalPages" style="padding: 6px 12px; border: 1px solid #e2e8f0; border-radius: 6px; background: white; cursor: pointer; font-size: 13px;" title="Next">
|
||||
<i class="fas fa-chevron-right"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Top Process Tab -->
|
||||
@@ -1311,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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1334,56 +1394,38 @@
|
||||
}
|
||||
|
||||
$.ajax({
|
||||
url: '/base/blockIPAddress',
|
||||
url: '/firewall/addBannedIP',
|
||||
type: 'POST',
|
||||
contentType: 'application/json',
|
||||
headers: {
|
||||
'X-CSRFToken': getCookie('csrftoken')
|
||||
},
|
||||
data: JSON.stringify({
|
||||
'ip_address': ipAddress,
|
||||
'reason': 'Security alert detected from dashboard'
|
||||
'ip': ipAddress,
|
||||
'reason': 'Brute force attack detected from SSH Security Analysis',
|
||||
'duration': 'permanent'
|
||||
}),
|
||||
success: function(data) {
|
||||
// Handle both success and error responses
|
||||
if (data.status === 1) {
|
||||
showNotification('success', data.message || 'IP address blocked successfully');
|
||||
if (data && data.status === 1) {
|
||||
showNotification('success', data.message || 'IP address blocked successfully. Manage in Firewall > Banned IPs.');
|
||||
// Refresh the page to update the blocked IPs list
|
||||
setTimeout(() => {
|
||||
location.reload();
|
||||
}, 1000);
|
||||
setTimeout(function() { location.reload(); }, 1000);
|
||||
} else {
|
||||
// Handle error response - check for both 'error' and 'error_message' fields
|
||||
var errorMsg = data.error || data.error_message || data.message || 'Failed to block IP address';
|
||||
var errorMsg = (data && (data.error_message || data.error || data.message)) || 'Failed to block IP address';
|
||||
showNotification('error', errorMsg);
|
||||
}
|
||||
},
|
||||
error: function(xhr, status, error) {
|
||||
// Handle network errors and parse JSON errors
|
||||
console.error('Ban IP error:', xhr, status, error);
|
||||
console.error('Ban IP error:', xhr.status, xhr.responseText);
|
||||
var errorMsg = 'Failed to block IP address. Please try again.';
|
||||
|
||||
// Log full response for debugging
|
||||
console.log('Response status:', xhr.status);
|
||||
console.log('Response text:', xhr.responseText);
|
||||
|
||||
if (xhr.responseJSON) {
|
||||
errorMsg = xhr.responseJSON.error || xhr.responseJSON.error_message || xhr.responseJSON.message || errorMsg;
|
||||
console.log('Parsed error from JSON:', errorMsg);
|
||||
} else if (xhr.responseText) {
|
||||
try {
|
||||
var errorData = JSON.parse(xhr.responseText);
|
||||
errorMsg = errorData.error || errorData.error_message || errorData.message || errorMsg;
|
||||
console.log('Parsed error from text:', errorMsg);
|
||||
} catch(e) {
|
||||
console.error('Failed to parse error response:', e);
|
||||
// If parsing fails, try to extract error from response text
|
||||
if (xhr.responseText.includes('error')) {
|
||||
errorMsg = xhr.responseText.substring(0, 200);
|
||||
}
|
||||
}
|
||||
var data = xhr.responseJSON;
|
||||
if (!data && xhr.responseText) {
|
||||
try { data = JSON.parse(xhr.responseText); } catch(e) {}
|
||||
}
|
||||
if (data && (data.error_message || data.error || data.message)) {
|
||||
errorMsg = data.error_message || data.error || data.message;
|
||||
}
|
||||
|
||||
showNotification('error', errorMsg);
|
||||
},
|
||||
complete: function() {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{% load i18n %}
|
||||
{% get_current_language as LANGUAGE_CODE %}
|
||||
{% with CP_VERSION="2.4.4.1" %}
|
||||
{% with CP_VERSION=CYBERPANEL_FULL_VERSION|default:"2.5.5.dev" %}
|
||||
<!DOCTYPE html>
|
||||
<html lang="en" ng-app="CyberCP">
|
||||
<head>
|
||||
@@ -26,29 +26,25 @@
|
||||
<!-- Readability Fixes CSS -->
|
||||
<link rel="stylesheet" type="text/css" href="{% static 'baseTemplate/assets/readability-fixes.css' %}?v={{ CP_VERSION }}">
|
||||
|
||||
<!-- Core Scripts (data-cfasync=false prevents Cloudflare Rocket Loader from breaking load order) -->
|
||||
<!-- Core Scripts: Angular + system-status FIRST so dashboard works even when CDN/Tracking Prevention blocks jQuery/Bootstrap -->
|
||||
<script src="{% static 'baseTemplate/angularjs.1.6.5.js' %}?v={{ CP_VERSION }}" data-cfasync="false"></script>
|
||||
<script src="{% static 'baseTemplate/custom-js/system-status.js' %}?v={{ CP_VERSION }}&dashboard=3" data-cfasync="false"></script>
|
||||
<script src="https://code.jquery.com/jquery-2.2.4.min.js" data-cfasync="false"></script>
|
||||
<!-- Bootstrap JavaScript -->
|
||||
|
||||
<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>
|
||||
<script src="{% static 'baseTemplate/custom-js/system-status.js' %}?v={{ CP_VERSION }}" data-cfasync="false"></script>
|
||||
<script src="{% static 'baseTemplate/custom-js/chart.umd.min.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>
|
||||
|
||||
<!-- Select2 (required by FTP create account; load after jQuery; exclude from Rocket Loader to preserve order) -->
|
||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/select2@4.0.13/dist/css/select2.min.css">
|
||||
<script src="https://cdn.jsdelivr.net/npm/select2@4.0.13/dist/js/select2.full.min.js" data-cfasync="false"></script>
|
||||
<!-- Select2 (required by FTP create account; local copy avoids CDN Tracking Prevention blocking) -->
|
||||
<link rel="stylesheet" href="{% static 'baseTemplate/vendor/select2/select2.min.css' %}?v={{ CP_VERSION }}">
|
||||
<script src="{% static 'baseTemplate/vendor/select2/select2.full.min.js' %}?v={{ CP_VERSION }}" data-cfasync="false"></script>
|
||||
|
||||
<!-- Modern Design System -->
|
||||
<style>
|
||||
@@ -297,38 +293,51 @@
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
/* Notification dropdown: always light panel with dark text so readable in both light and dark mode */
|
||||
.notification-center-dropdown {
|
||||
position: absolute;
|
||||
top: calc(100% + 10px);
|
||||
right: 0;
|
||||
background: white;
|
||||
border: 1px solid var(--border-color);
|
||||
background: #ffffff;
|
||||
border: 1px solid #e5e7eb;
|
||||
border-radius: 16px;
|
||||
box-shadow: 0 20px 40px rgba(0, 0, 0, 0.15);
|
||||
width: 520px;
|
||||
max-width: calc(100vw - 40px);
|
||||
max-height: 600px;
|
||||
overflow-y: auto;
|
||||
z-index: 10000;
|
||||
display: none;
|
||||
flex-direction: column;
|
||||
z-index: 10000;
|
||||
overflow: hidden;
|
||||
min-height: 0;
|
||||
}
|
||||
.notification-center-dropdown.show { display: block; }
|
||||
.notification-center-header {
|
||||
.notification-center-dropdown.show { display: flex; }
|
||||
.notification-center-dropdown .notification-center-header {
|
||||
padding: 1.25rem 1.5rem;
|
||||
border-bottom: 1px solid var(--border-color);
|
||||
border-bottom: 1px solid #e5e7eb;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.notification-center-header h3 { margin: 0; font-size: 1.25rem; font-weight: 700; }
|
||||
.notification-center-list { padding: 1rem; }
|
||||
.notification-center-empty { color: var(--text-secondary); padding: 1rem; }
|
||||
.notification-center-dropdown .notification-center-header h3 { margin: 0; font-size: 1.25rem; font-weight: 700; color: #111827; }
|
||||
.notification-center-list {
|
||||
padding: 1rem;
|
||||
flex: 1 1 0;
|
||||
min-height: 0;
|
||||
max-height: 480px;
|
||||
overflow-y: auto;
|
||||
overflow-x: hidden;
|
||||
-webkit-overflow-scrolling: touch;
|
||||
}
|
||||
#notification-center-dropdown .notification-center-empty { color: #4b5563; padding: 1rem; }
|
||||
.notification-center-item {
|
||||
padding: 1.5rem;
|
||||
border: 1px solid var(--border-color);
|
||||
border: 1px solid #e5e7eb;
|
||||
border-radius: 12px;
|
||||
margin-bottom: 1rem;
|
||||
background: white;
|
||||
background: #ffffff;
|
||||
overflow: visible;
|
||||
}
|
||||
.notification-center-item.dismissed { opacity: 0.7; background: #f9fafb; }
|
||||
.notification-center-item-title {
|
||||
@@ -337,36 +346,69 @@
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.75rem;
|
||||
color: #111827;
|
||||
}
|
||||
.notification-center-item-title .dismissed-badge {
|
||||
font-size: 0.75rem;
|
||||
color: #6b7280;
|
||||
color: #374151;
|
||||
margin-left: auto;
|
||||
padding: 0.25rem 0.5rem;
|
||||
background: #f3f4f6;
|
||||
background: #e5e7eb;
|
||||
border-radius: 6px;
|
||||
}
|
||||
.notification-center-item-text { color: var(--text-secondary); font-size: 0.95rem; margin-bottom: 1rem; line-height: 1.6; }
|
||||
.notification-center-item-actions { display: flex; gap: 0.75rem; flex-wrap: wrap; }
|
||||
.notification-center-item-link {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
color: white;
|
||||
background: linear-gradient(135deg, var(--accent-color) 0%, #6d6bd4 100%);
|
||||
text-decoration: none;
|
||||
font-size: 0.9rem;
|
||||
font-weight: 600;
|
||||
padding: 0.75rem 1.5rem;
|
||||
border-radius: 10px;
|
||||
#notification-center-dropdown .notification-center-item-text { color: #4b5563; font-size: 0.95rem; margin-bottom: 1rem; line-height: 1.6; }
|
||||
#notification-center-dropdown .notification-center-item-actions {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1.25rem;
|
||||
align-items: flex-start;
|
||||
margin-top: 0.75rem;
|
||||
width: 100%;
|
||||
}
|
||||
.notification-center-item-link-secondary {
|
||||
#notification-center-dropdown .notification-center-item-actions .notification-center-item-link {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
color: var(--accent-color);
|
||||
justify-content: center;
|
||||
gap: 0.4rem;
|
||||
color: #ffffff !important;
|
||||
background: linear-gradient(135deg, #4f46e5 0%, #4338ca 100%) !important;
|
||||
text-decoration: none;
|
||||
font-size: 0.9rem;
|
||||
font-size: 0.875rem;
|
||||
font-weight: 600;
|
||||
padding: 0.5rem 1.25rem;
|
||||
border-radius: 8px;
|
||||
white-space: nowrap;
|
||||
flex-shrink: 0;
|
||||
min-width: 160px;
|
||||
width: auto;
|
||||
min-height: 2.25rem;
|
||||
line-height: 1.3;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
#notification-center-dropdown .notification-center-item-actions .notification-center-item-link:hover {
|
||||
filter: brightness(1.1);
|
||||
color: #ffffff !important;
|
||||
}
|
||||
#notification-center-dropdown .notification-center-item-actions .notification-center-item-link-secondary {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 0.4rem;
|
||||
color: #4338ca !important;
|
||||
text-decoration: none;
|
||||
font-size: 0.875rem;
|
||||
font-weight: 500;
|
||||
padding: 0.4rem 0.75rem;
|
||||
white-space: nowrap;
|
||||
flex-shrink: 0;
|
||||
min-width: 120px;
|
||||
width: auto;
|
||||
min-height: 1.75rem;
|
||||
line-height: 1.3;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
#notification-center-dropdown .notification-center-item-actions .notification-center-item-link-secondary:hover {
|
||||
text-decoration: underline;
|
||||
color: #3730a3 !important;
|
||||
}
|
||||
|
||||
/* Sidebar */
|
||||
@@ -1547,9 +1589,6 @@
|
||||
<a href="{% url 'listChildDomains' %}" class="menu-item">
|
||||
<span>List Sub/Addon Domains</span>
|
||||
</a>
|
||||
<a href="{% url 'fixSubdomainLogs' %}" class="menu-item">
|
||||
<span>Fix Subdomain Logs</span>
|
||||
</a>
|
||||
{% if admin or modifyWebsite %}
|
||||
<a href="{% url 'modifyWebsite' %}" class="menu-item">
|
||||
<span>Modify Website</span>
|
||||
@@ -1651,22 +1690,22 @@
|
||||
</a>
|
||||
{% endif %}
|
||||
{% if admin or deleteZone %}
|
||||
<a href="{% url 'deleteDNSZone' %}" class="menu-item">
|
||||
<a href="{% url 'deleteDNSZone' %}" class="menu-item deleteZone">
|
||||
<span>Delete Zone</span>
|
||||
</a>
|
||||
{% endif %}
|
||||
{% if admin or addDeleteRecords %}
|
||||
<a href="{% url 'addDeleteDNSRecords' %}" class="menu-item">
|
||||
<a href="{% url 'addDeleteDNSRecords' %}" class="menu-item addDeleteRecords">
|
||||
<span>Add/Delete Records</span>
|
||||
</a>
|
||||
{% endif %}
|
||||
{% if admin or addDeleteRecords %}
|
||||
<a href="{% url 'addDeleteDNSRecordsCloudFlare' %}" class="menu-item">
|
||||
<a href="{% url 'addDeleteDNSRecordsCloudFlare' %}" class="menu-item addDeleteRecords">
|
||||
<span>CloudFlare</span>
|
||||
</a>
|
||||
{% endif %}
|
||||
{% if admin or addDeleteRecords %}
|
||||
<a href="{% url 'ResetDNSConfigurations' %}" class="menu-item">
|
||||
<a href="{% url 'ResetDNSConfigurations' %}" class="menu-item addDeleteRecords">
|
||||
<span>Reset DNS Configurations</span>
|
||||
</a>
|
||||
{% endif %}
|
||||
@@ -1835,9 +1874,7 @@
|
||||
<a href="{% url 'manageSSL' %}" class="menu-item">
|
||||
<span>Manage SSL</span>
|
||||
</a>
|
||||
<a href="{% url 'sslReconcile' %}" class="menu-item">
|
||||
<span>SSL Reconciliation</span>
|
||||
</a>
|
||||
{% comment %}SSL Reconciliation - hidden; URL /manageSSL/sslReconcile still works if needed{% endcomment %}
|
||||
{% endif %}
|
||||
{% if admin or hostnameSSL %}
|
||||
<a href="{% url 'sslForHostName' %}" class="menu-item">
|
||||
@@ -2186,14 +2223,14 @@
|
||||
<script src="{% static 'websiteFunctions/websiteFunctions.js' %}?v={{ CP_VERSION }}" data-cfasync="false"></script>
|
||||
<script src="{% static 'userManagment/userManagment.js' %}?v={{ CP_VERSION }}" data-cfasync="false"></script>
|
||||
<script src="{% static 'databases/databases.js' %}?v={{ CP_VERSION }}" data-cfasync="false"></script>
|
||||
<script src="{% static 'dns/dns.js' %}?v={{ CP_VERSION }}" data-cfasync="false"></script>
|
||||
<script src="{% static 'dns/dns.js' %}?v={{ CP_VERSION }}&dns={{ DNS_STATIC_VERSION }}" data-cfasync="false"></script>
|
||||
<script src="{% static 'mailServer/mailServer.js' %}?v={{ CP_VERSION }}" data-cfasync="false"></script>
|
||||
<script src="{% static 'ftp/ftp.js' %}?v={{ CP_VERSION }}" data-cfasync="false"></script>
|
||||
<script src="{% static 'backup/backup.js' %}?v={{ CP_VERSION }}" data-cfasync="false"></script>
|
||||
<script src="{% static 'managePHP/managePHP.js' %}?v={{ CP_VERSION }}" data-cfasync="false"></script>
|
||||
<script src="{% static 'serverLogs/serverLogs.js' %}?v={{ CP_VERSION }}" data-cfasync="false"></script>
|
||||
<script src="{% static 'serverStatus/serverStatus.js' %}?v={{ CP_VERSION }}" data-cfasync="false"></script>
|
||||
<script src="{% static 'firewall/firewall.js' %}?v={{ CP_VERSION }}" data-cfasync="false"></script>
|
||||
<script src="{% static 'firewall/firewall.js' %}?v={{ CP_VERSION }}&fw={{ FIREWALL_STATIC_VERSION|default:CP_VERSION }}&cb=4" data-cfasync="false"></script>
|
||||
<script src="{% static 'emailPremium/emailPremium.js' %}?v={{ CP_VERSION }}" data-cfasync="false"></script>
|
||||
<script src="{% static 'manageServices/manageServices.js' %}?v={{ CP_VERSION }}" data-cfasync="false"></script>
|
||||
<script src="{% static 'CLManager/CLManager.js' %}?v={{ CP_VERSION }}" data-cfasync="false"></script>
|
||||
@@ -2305,10 +2342,7 @@
|
||||
|
||||
// Check if user has backup configured (you'll need to implement this API)
|
||||
// For now, we'll show it by default unless they have a backup plan
|
||||
// This should be replaced with an actual check
|
||||
{% if not request.session.has_backup_configured %}
|
||||
showBackupNotification();
|
||||
{% endif %}
|
||||
// Session check omitted - has_backup_configured may be absent in some views
|
||||
|
||||
// For demonstration, let's show it if URL doesn't contain 'OneClickBackups'
|
||||
if (!window.location.href.includes('OneClickBackups')) {
|
||||
@@ -2400,12 +2434,13 @@
|
||||
// .htaccess Feature Notification Functions
|
||||
function checkHtaccessStatus() {
|
||||
// Check if user has dismissed the notification permanently (localStorage for longer persistence)
|
||||
if (localStorage.getItem('htaccessNotificationDismissed') === 'true') {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
if (localStorage.getItem('htaccessNotificationDismissed') === 'true') return;
|
||||
} catch (e) { return; }
|
||||
|
||||
// Check if notification has been shown today
|
||||
const lastShown = localStorage.getItem('htaccessNotificationLastShown');
|
||||
var lastShown;
|
||||
try { lastShown = localStorage.getItem('htaccessNotificationLastShown'); } catch (e) { return; }
|
||||
const today = new Date().toDateString();
|
||||
|
||||
if (lastShown === today) {
|
||||
@@ -2414,7 +2449,7 @@
|
||||
|
||||
// Show the notification
|
||||
showHtaccessNotification();
|
||||
localStorage.setItem('htaccessNotificationLastShown', today);
|
||||
try { localStorage.setItem('htaccessNotificationLastShown', today); } catch (e) {}
|
||||
}
|
||||
|
||||
function showHtaccessNotification() {
|
||||
@@ -2430,7 +2465,7 @@
|
||||
banner.classList.remove('show');
|
||||
body.classList.remove('htaccess-shown');
|
||||
// Remember dismissal permanently
|
||||
localStorage.setItem('htaccessNotificationDismissed', 'true');
|
||||
try { localStorage.setItem('htaccessNotificationDismissed', 'true'); } catch (e) {}
|
||||
}
|
||||
|
||||
function isNotificationDismissed(notificationKey) {
|
||||
@@ -2441,7 +2476,7 @@
|
||||
return {% if ai_scanner_notification_dismissed %}true{% else %}false{% endif %};
|
||||
}
|
||||
if (notificationKey === 'htaccess-notification') {
|
||||
return localStorage.getItem('htaccessNotificationDismissed') === 'true';
|
||||
try { return localStorage.getItem('htaccessNotificationDismissed') === 'true'; } catch (e) { return false; }
|
||||
}
|
||||
return false;
|
||||
}
|
||||
@@ -2516,6 +2551,10 @@
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
loadNotificationCenter();
|
||||
checkBackupStatus();
|
||||
// Optional: open notification dropdown for testing (e.g. ?showNotifications=1)
|
||||
if (window.location.search.indexOf('showNotifications=1') !== -1) {
|
||||
setTimeout(function() { document.getElementById('notification-center-dropdown').classList.add('show'); }, 400);
|
||||
}
|
||||
// Show AI Scanner notification with a slight delay for better UX
|
||||
setTimeout(checkAIScannerStatus, 1000);
|
||||
// Show .htaccess notification with additional delay for staggered effect
|
||||
@@ -2560,10 +2599,12 @@
|
||||
|
||||
<!-- Dark Mode Toggle Script -->
|
||||
<script>
|
||||
// Theme switching functionality
|
||||
// Theme switching functionality (wrapped in try-catch for Tracking Prevention / private browsing)
|
||||
(function() {
|
||||
// Get saved theme from localStorage or default to light
|
||||
const savedTheme = localStorage.getItem('cyberPanelTheme') || 'light';
|
||||
var savedTheme = 'light';
|
||||
try {
|
||||
savedTheme = localStorage.getItem('cyberPanelTheme') || 'light';
|
||||
} catch (e) { /* Tracking Prevention or storage disabled */ }
|
||||
document.documentElement.setAttribute('data-theme', savedTheme);
|
||||
|
||||
// Update icon based on current theme
|
||||
@@ -2586,7 +2627,7 @@
|
||||
|
||||
// Update theme
|
||||
document.documentElement.setAttribute('data-theme', newTheme);
|
||||
localStorage.setItem('cyberPanelTheme', newTheme);
|
||||
try { localStorage.setItem('cyberPanelTheme', newTheme); } catch (e) { /* Tracking Prevention */ }
|
||||
|
||||
// Update icon
|
||||
updateThemeIcon(newTheme);
|
||||
|
||||
@@ -199,9 +199,9 @@
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
/* Progress log */
|
||||
/* Progress log - dark background for terminal-style visibility */
|
||||
.log-container {
|
||||
background: var(--text-primary, #1e1e1e);
|
||||
background: #1e1e1e;
|
||||
border-radius: 8px;
|
||||
padding: 20px;
|
||||
margin-top: 20px;
|
||||
@@ -215,11 +215,12 @@
|
||||
min-height: 300px;
|
||||
background: transparent;
|
||||
border: none;
|
||||
color: var(--bg-secondary, #f0f0f0);
|
||||
color: #e8e8e8;
|
||||
font-family: 'SF Mono', Monaco, monospace;
|
||||
font-size: 13px;
|
||||
line-height: 1.6;
|
||||
resize: vertical;
|
||||
caret-color: #e8e8e8;
|
||||
}
|
||||
|
||||
.log-textarea:focus {
|
||||
@@ -337,30 +338,51 @@
|
||||
</div>
|
||||
|
||||
<script>
|
||||
// Function to populate the branch dropdown
|
||||
function populateBranches(branches) {
|
||||
var branchSelect = document.getElementById("branchSelect");
|
||||
for (let i = branches.length - 1; i >= 0; i--) {
|
||||
const branch = branches[i];
|
||||
var option = document.createElement("option");
|
||||
option.value = branch;
|
||||
option.text = branch;
|
||||
if (branch.startsWith("v") && branch.indexOf("dev") === -1 && branch.indexOf("version-counter") === -1) {
|
||||
branchSelect.appendChild(option);
|
||||
}
|
||||
}
|
||||
// Sort key for version branches: v2.5.5-dev > v2.5.5 > v2.4.4
|
||||
function versionSortKey(name) {
|
||||
var m = name.match(/^v(\d+)\.(\d+)(?:\.(\d+))?(?:-(.+))?$/);
|
||||
if (!m) return [0, 0, 0, 0];
|
||||
var major = parseInt(m[1], 10) || 0;
|
||||
var minor = parseInt(m[2], 10) || 0;
|
||||
var patch = parseInt(m[3], 10) || 0;
|
||||
var suffix = (m[4] === 'dev') ? 1 : 0;
|
||||
return [major, minor, patch, suffix];
|
||||
}
|
||||
|
||||
function compareBranches(a, b) {
|
||||
var ka = versionSortKey(a), kb = versionSortKey(b);
|
||||
for (var i = 0; i < 4; i++) {
|
||||
if (ka[i] !== kb[i]) return kb[i] - ka[i];
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
// Function to populate the branch dropdown (latest 10 only)
|
||||
function populateBranches(branches) {
|
||||
var filtered = branches.filter(function(b) {
|
||||
return b.startsWith("v") && b.indexOf("version-counter") === -1;
|
||||
});
|
||||
filtered.sort(compareBranches);
|
||||
var latest = filtered.slice(0, 10);
|
||||
|
||||
var branchSelect = document.getElementById("branchSelect");
|
||||
for (var i = 0; i < latest.length; i++) {
|
||||
var option = document.createElement("option");
|
||||
option.value = latest[i];
|
||||
option.text = latest[i];
|
||||
branchSelect.appendChild(option);
|
||||
}
|
||||
}
|
||||
|
||||
function getBranches(url, branches, page) {
|
||||
if (!page) page = 1;
|
||||
fetch(url + '?page=' + page)
|
||||
fetch(url + '?per_page=100&page=' + page)
|
||||
.then((response) => response.json())
|
||||
.then((data) => {
|
||||
if (data.length === 0) {
|
||||
populateBranches(branches);
|
||||
} else {
|
||||
const branchNames = data.map(branch => branch.name);
|
||||
var branchNames = data.map(function(b) { return b.name; });
|
||||
branches = branches.concat(branchNames);
|
||||
getBranches(url, branches, page + 1);
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@ urlpatterns = [
|
||||
re_path(r'^getAdminStatus$', views.getAdminStatus, name='getSystemInformation'),
|
||||
re_path(r'^getLoadAverage$', views.getLoadAverage, name='getLoadAverage'),
|
||||
re_path(r'^versionManagment$', views.versionManagment, name='versionManagment'),
|
||||
re_path(r'^versionManagement$', views.versionManagment, name='versionManagement'),
|
||||
re_path(r'^design$', views.design, name='design'),
|
||||
re_path(r'^getthemedata$', views.getthemedata, name='getthemedata'),
|
||||
re_path(r'^upgrade$', views.upgrade, name='upgrade'),
|
||||
|
||||
@@ -16,6 +16,7 @@ from plogical.acl import ACLManager
|
||||
from manageServices.models import PDNSStatus
|
||||
from django.views.decorators.csrf import ensure_csrf_cookie, csrf_exempt
|
||||
from plogical.processUtilities import ProcessUtilities
|
||||
from plogical.firewallUtilities import FirewallUtilities
|
||||
from plogical.httpProc import httpProc
|
||||
from websiteFunctions.models import Websites, WPSites
|
||||
from databases.models import Databases
|
||||
@@ -25,6 +26,7 @@ from loginSystem.models import Administrator
|
||||
from packages.models import Package
|
||||
from django.views.decorators.http import require_GET, require_POST
|
||||
import pwd
|
||||
import re
|
||||
|
||||
# Create your views here.
|
||||
|
||||
@@ -32,6 +34,27 @@ VERSION = '2.5.5'
|
||||
BUILD = 'dev'
|
||||
|
||||
|
||||
def _version_compare(a, b):
|
||||
"""Return 1 if a > b, -1 if a < b, 0 if equal."""
|
||||
def parse(v):
|
||||
parts = []
|
||||
for p in str(v).split('.'):
|
||||
try:
|
||||
parts.append(int(p))
|
||||
except ValueError:
|
||||
parts.append(0)
|
||||
return parts
|
||||
pa, pb = parse(a), parse(b)
|
||||
for i in range(max(len(pa), len(pb))):
|
||||
va = pa[i] if i < len(pa) else 0
|
||||
vb = pb[i] if i < len(pb) else 0
|
||||
if va > vb:
|
||||
return 1
|
||||
if va < vb:
|
||||
return -1
|
||||
return 0
|
||||
|
||||
|
||||
@ensure_csrf_cookie
|
||||
def renderBase(request):
|
||||
template = 'baseTemplate/homePage.html'
|
||||
@@ -44,27 +67,41 @@ def renderBase(request):
|
||||
|
||||
@ensure_csrf_cookie
|
||||
def versionManagement(request):
|
||||
currentVersion = VERSION
|
||||
currentBuild = str(BUILD)
|
||||
|
||||
getVersion = requests.get('https://cyberpanel.net/version.txt')
|
||||
latest = getVersion.json()
|
||||
latestVersion = latest['version']
|
||||
latestBuild = latest['build']
|
||||
branch_ref = 'v%s.%s' % (latestVersion, latestBuild)
|
||||
|
||||
currentVersion = VERSION
|
||||
currentBuild = str(BUILD)
|
||||
|
||||
u = "https://api.github.com/repos/usmannasir/cyberpanel/commits?sha=v%s.%s" % (latestVersion, latestBuild)
|
||||
logging.writeToFile(u)
|
||||
r = requests.get(u)
|
||||
latestcomit = r.json()[0]['sha']
|
||||
|
||||
command = "git -C /usr/local/CyberCP/ rev-parse HEAD"
|
||||
output = ProcessUtilities.outputExecutioner(command)
|
||||
|
||||
Currentcomt = output.rstrip("\n")
|
||||
notechk = True
|
||||
Currentcomt = ''
|
||||
latestcomit = ''
|
||||
|
||||
if Currentcomt == latestcomit:
|
||||
if _version_compare(currentVersion, latestVersion) > 0:
|
||||
notechk = False
|
||||
else:
|
||||
remote_cmd = 'git -C /usr/local/CyberCP remote get-url origin 2>/dev/null || true'
|
||||
remote_out = ProcessUtilities.outputExecutioner(remote_cmd)
|
||||
is_usmannasir = 'usmannasir/cyberpanel' in (remote_out or '')
|
||||
|
||||
if is_usmannasir:
|
||||
u = "https://api.github.com/repos/usmannasir/cyberpanel/commits?sha=%s" % branch_ref
|
||||
logging.CyberCPLogFileWriter.writeToFile(u)
|
||||
try:
|
||||
r = requests.get(u, timeout=10)
|
||||
r.raise_for_status()
|
||||
latestcomit = r.json()[0]['sha']
|
||||
except (requests.RequestException, IndexError, KeyError) as e:
|
||||
logging.CyberCPLogFileWriter.writeToFile('[versionManagement] GitHub API failed: %s' % str(e))
|
||||
head_cmd = 'git -C /usr/local/CyberCP rev-parse HEAD 2>/dev/null || true'
|
||||
Currentcomt = ProcessUtilities.outputExecutioner(head_cmd).rstrip('\n')
|
||||
if latestcomit and Currentcomt == latestcomit:
|
||||
notechk = False
|
||||
else:
|
||||
notechk = False
|
||||
|
||||
template = 'baseTemplate/versionManagment.html'
|
||||
finalData = {'build': currentBuild, 'currentVersion': currentVersion, 'latestVersion': latestVersion,
|
||||
@@ -129,6 +166,11 @@ def getAdminStatus(request):
|
||||
|
||||
|
||||
def getSystemStatus(request):
|
||||
default_fallback = {
|
||||
'cpuUsage': 0, 'ramUsage': 0, 'diskUsage': 0,
|
||||
'cpuCores': 2, 'ramTotalMB': 4096, 'diskTotalGB': 100,
|
||||
'diskFreeGB': 100, 'uptime': 'N/A'
|
||||
}
|
||||
try:
|
||||
val = request.session['userID']
|
||||
currentACL = ACLManager.loadedACL(val)
|
||||
@@ -215,31 +257,13 @@ def getSystemStatus(request):
|
||||
|
||||
except KeyError as e:
|
||||
logging.CyberCPLogFileWriter.writeToFile(f'[getSystemStatus] KeyError - No session userID: {str(e)}')
|
||||
# Return default values on error
|
||||
default_data = {
|
||||
'cpuUsage': 0,
|
||||
'ramUsage': 0,
|
||||
'diskUsage': 0,
|
||||
'cpuCores': 2,
|
||||
'ramTotalMB': 4096,
|
||||
'diskTotalGB': 100,
|
||||
'diskFreeGB': 100,
|
||||
'uptime': 'N/A'
|
||||
}
|
||||
return HttpResponse(json.dumps(default_data))
|
||||
return HttpResponse(json.dumps(default_fallback))
|
||||
except Exception as e:
|
||||
# Return default values on error
|
||||
default_data = {
|
||||
'cpuUsage': 0,
|
||||
'ramUsage': 0,
|
||||
'diskUsage': 0,
|
||||
'cpuCores': 2,
|
||||
'ramTotalMB': 4096,
|
||||
'diskTotalGB': 100,
|
||||
'diskFreeGB': 100,
|
||||
'uptime': 'N/A'
|
||||
}
|
||||
return HttpResponse(json.dumps(default_data))
|
||||
logging.CyberCPLogFileWriter.writeToFile(f'[getSystemStatus] Exception: {str(e)}')
|
||||
try:
|
||||
return HttpResponse(json.dumps(default_fallback))
|
||||
except Exception:
|
||||
return HttpResponse('{"cpuUsage":0,"ramUsage":0,"diskUsage":0,"cpuCores":2,"ramTotalMB":4096,"diskTotalGB":100,"diskFreeGB":100,"uptime":"N/A"}', content_type='application/json')
|
||||
|
||||
|
||||
def getLoadAverage(request):
|
||||
@@ -265,31 +289,75 @@ def getLoadAverage(request):
|
||||
|
||||
@ensure_csrf_cookie
|
||||
def versionManagment(request):
|
||||
## Get latest version
|
||||
|
||||
getVersion = requests.get('https://cyberpanel.net/version.txt')
|
||||
latest = getVersion.json()
|
||||
latestVersion = latest['version']
|
||||
latestBuild = latest['build']
|
||||
|
||||
## Get local version
|
||||
|
||||
currentVersion = VERSION
|
||||
currentBuild = str(BUILD)
|
||||
|
||||
u = "https://api.github.com/repos/usmannasir/cyberpanel/commits?sha=v%s.%s" % (latestVersion, latestBuild)
|
||||
logging.CyberCPLogFileWriter.writeToFile(u)
|
||||
r = requests.get(u)
|
||||
latestcomit = r.json()[0]['sha']
|
||||
|
||||
command = "git -C /usr/local/CyberCP/ rev-parse HEAD"
|
||||
output = ProcessUtilities.outputExecutioner(command)
|
||||
|
||||
Currentcomt = output.rstrip("\n")
|
||||
notechk = True
|
||||
Currentcomt = ''
|
||||
latestcomit = ''
|
||||
latestVersion = '0'
|
||||
latestBuild = '0'
|
||||
|
||||
if (Currentcomt == latestcomit):
|
||||
on_dev_branch = (currentVersion == '2.5.5' and currentBuild == 'dev')
|
||||
|
||||
try:
|
||||
getVersion = requests.get('https://cyberpanel.net/version.txt', timeout=10)
|
||||
getVersion.raise_for_status()
|
||||
latest = getVersion.json()
|
||||
latestVersion = str(latest.get('version', '0'))
|
||||
latestBuild = str(latest.get('build', '0'))
|
||||
except (requests.RequestException, ValueError, KeyError) as e:
|
||||
logging.CyberCPLogFileWriter.writeToFile('[versionManagment] cyberpanel.net/version.txt failed: %s' % str(e))
|
||||
if on_dev_branch:
|
||||
latestVersion, latestBuild = '2.5.5', 'dev'
|
||||
|
||||
# Dev branch: compare against v2.5.5-dev, show dev version info
|
||||
if on_dev_branch:
|
||||
branch_ref = 'v2.5.5-dev'
|
||||
latestVersion, latestBuild = '2.5.5', 'dev'
|
||||
else:
|
||||
branch_ref = 'v%s.%s' % (latestVersion, latestBuild)
|
||||
|
||||
# Always fetch local HEAD for display
|
||||
head_cmd = 'git -C /usr/local/CyberCP rev-parse HEAD 2>/dev/null || true'
|
||||
Currentcomt = (ProcessUtilities.outputExecutioner(head_cmd) or '').rstrip('\n')
|
||||
|
||||
remote_cmd = 'git -C /usr/local/CyberCP remote get-url origin 2>/dev/null || true'
|
||||
remote_out = ProcessUtilities.outputExecutioner(remote_cmd)
|
||||
is_usmannasir = 'usmannasir/cyberpanel' in (remote_out or '')
|
||||
|
||||
# Stable: newer than cyberpanel.net = up to date; dev: compare commits
|
||||
if not on_dev_branch and notechk and _version_compare(currentVersion, latestVersion) > 0:
|
||||
notechk = False
|
||||
elif notechk:
|
||||
# Dev branch: always use usmannasir v2.5.5-dev as canonical "latest"
|
||||
# Forks: use usmannasir for Latest Commit so all dev users compare to same upstream
|
||||
fetch_branch = branch_ref if (is_usmannasir or on_dev_branch) else None
|
||||
if fetch_branch:
|
||||
u = "https://api.github.com/repos/usmannasir/cyberpanel/commits?sha=%s" % fetch_branch
|
||||
logging.CyberCPLogFileWriter.writeToFile(u)
|
||||
try:
|
||||
r = requests.get(u, timeout=10)
|
||||
r.raise_for_status()
|
||||
latestcomit = r.json()[0]['sha']
|
||||
if Currentcomt and latestcomit and Currentcomt == latestcomit:
|
||||
notechk = False
|
||||
except (requests.RequestException, IndexError, KeyError) as e:
|
||||
logging.CyberCPLogFileWriter.writeToFile('[versionManagment] GitHub API failed: %s' % str(e))
|
||||
elif not on_dev_branch:
|
||||
# Stable fork: fetch from fork's branch
|
||||
m = re.search(r'github\.com[:/]([^/]+)/([^/]+?)(?:\.git)?$', (remote_out or '').strip())
|
||||
if m:
|
||||
owner, repo = m.group(1), m.group(2).rstrip('.git')
|
||||
try:
|
||||
u = "https://api.github.com/repos/%s/%s/commits?sha=%s" % (owner, repo, branch_ref)
|
||||
r = requests.get(u, timeout=10)
|
||||
r.raise_for_status()
|
||||
latestcomit = r.json()[0]['sha']
|
||||
if Currentcomt and latestcomit and Currentcomt == latestcomit:
|
||||
notechk = False
|
||||
except (requests.RequestException, IndexError, KeyError):
|
||||
pass
|
||||
|
||||
template = 'baseTemplate/versionManagment.html'
|
||||
finalData = {'build': currentBuild, 'currentVersion': currentVersion, 'latestVersion': latestVersion,
|
||||
@@ -729,9 +797,19 @@ def getRecentSSHLogins(request):
|
||||
import re, time
|
||||
from collections import OrderedDict
|
||||
|
||||
# Run 'last -n 20' to get recent SSH logins
|
||||
# Pagination params
|
||||
try:
|
||||
output = ProcessUtilities.outputExecutioner('last -n 20')
|
||||
page = max(1, int(request.GET.get('page', 1)))
|
||||
except (ValueError, TypeError):
|
||||
page = 1
|
||||
try:
|
||||
per_page = min(100, max(5, int(request.GET.get('per_page', 20))))
|
||||
except (ValueError, TypeError):
|
||||
per_page = 20
|
||||
|
||||
# Run 'last -n 500' to get enough entries for pagination
|
||||
try:
|
||||
output = ProcessUtilities.outputExecutioner('last -n 500')
|
||||
except Exception as e:
|
||||
return HttpResponse(json.dumps({'error': 'Failed to run last: %s' % str(e)}), content_type='application/json', status=500)
|
||||
|
||||
@@ -815,7 +893,19 @@ def getRecentSSHLogins(request):
|
||||
'is_active': is_active,
|
||||
'raw': line
|
||||
})
|
||||
return HttpResponse(json.dumps({'logins': logins}), content_type='application/json')
|
||||
total = len(logins)
|
||||
total_pages = (total + per_page - 1) // per_page if total > 0 else 1
|
||||
page = min(page, total_pages) if total_pages > 0 else 1
|
||||
start = (page - 1) * per_page
|
||||
end = start + per_page
|
||||
paginated_logins = logins[start:end]
|
||||
return HttpResponse(json.dumps({
|
||||
'logins': paginated_logins,
|
||||
'total': total,
|
||||
'page': page,
|
||||
'per_page': per_page,
|
||||
'total_pages': total_pages
|
||||
}), content_type='application/json')
|
||||
except Exception as e:
|
||||
return HttpResponse(json.dumps({'error': str(e)}), content_type='application/json', status=500)
|
||||
|
||||
@@ -829,6 +919,17 @@ def getRecentSSHLogs(request):
|
||||
currentACL = ACLManager.loadedACL(user_id)
|
||||
if not currentACL.get('admin', 0):
|
||||
return HttpResponse(json.dumps({'error': 'Admin only'}), content_type='application/json', status=403)
|
||||
|
||||
# Pagination params
|
||||
try:
|
||||
page = max(1, int(request.GET.get('page', 1)))
|
||||
except (ValueError, TypeError):
|
||||
page = 1
|
||||
try:
|
||||
per_page = min(100, max(5, int(request.GET.get('per_page', 25))))
|
||||
except (ValueError, TypeError):
|
||||
per_page = 25
|
||||
|
||||
from plogical.processUtilities import ProcessUtilities
|
||||
import re
|
||||
distro = ProcessUtilities.decideDistro()
|
||||
@@ -837,7 +938,7 @@ def getRecentSSHLogs(request):
|
||||
else:
|
||||
log_path = '/var/log/secure'
|
||||
try:
|
||||
output = ProcessUtilities.outputExecutioner(f'tail -n 100 {log_path}')
|
||||
output = ProcessUtilities.outputExecutioner(f'tail -n 500 {log_path}')
|
||||
except Exception as e:
|
||||
return HttpResponse(json.dumps({'error': f'Failed to read log: {str(e)}'}), content_type='application/json', status=500)
|
||||
lines = output.split('\n')
|
||||
@@ -875,7 +976,21 @@ def getRecentSSHLogs(request):
|
||||
'raw': line,
|
||||
'ip_address': ip_address
|
||||
})
|
||||
return HttpResponse(json.dumps({'logs': logs}), content_type='application/json')
|
||||
# Reverse so newest logs appear first (page 1 = most recent)
|
||||
logs.reverse()
|
||||
total = len(logs)
|
||||
total_pages = (total + per_page - 1) // per_page if total > 0 else 1
|
||||
page = min(page, total_pages) if total_pages > 0 else 1
|
||||
start = (page - 1) * per_page
|
||||
end = start + per_page
|
||||
paginated_logs = logs[start:end]
|
||||
return HttpResponse(json.dumps({
|
||||
'logs': paginated_logs,
|
||||
'total': total,
|
||||
'page': page,
|
||||
'per_page': per_page,
|
||||
'total_pages': total_pages
|
||||
}), content_type='application/json')
|
||||
except Exception as e:
|
||||
return HttpResponse(json.dumps({'error': str(e)}), content_type='application/json', status=500)
|
||||
|
||||
@@ -1284,61 +1399,23 @@ def blockIPAddress(request):
|
||||
'error': 'Invalid IP address'
|
||||
}), content_type='application/json', status=400)
|
||||
|
||||
# Use firewalld (CSF has been discontinued)
|
||||
# Use FirewallUtilities so firewall-cmd runs with proper privileges (root/lscpd)
|
||||
firewall_cmd = 'firewalld'
|
||||
reason = data.get('reason', 'Security alert detected from dashboard')
|
||||
try:
|
||||
# Verify firewalld is active using subprocess for better security
|
||||
import subprocess
|
||||
firewalld_check = subprocess.run(['systemctl', 'is-active', 'firewalld'],
|
||||
capture_output=True, text=True, timeout=10)
|
||||
if not (firewalld_check.returncode == 0 and 'active' in firewalld_check.stdout):
|
||||
return HttpResponse(json.dumps({
|
||||
'status': 0,
|
||||
'error': 'Firewalld is not active. Please enable firewalld service.'
|
||||
}), content_type='application/json', status=500)
|
||||
except subprocess.TimeoutExpired:
|
||||
return HttpResponse(json.dumps({
|
||||
'status': 0,
|
||||
'error': 'Timeout checking firewalld status'
|
||||
}), content_type='application/json', status=500)
|
||||
success, msg = FirewallUtilities.blockIP(ip_address, reason)
|
||||
except Exception as e:
|
||||
return HttpResponse(json.dumps({
|
||||
'status': 0,
|
||||
'error': f'Cannot check firewalld status: {str(e)}'
|
||||
}), content_type='application/json', status=500)
|
||||
|
||||
# Block the IP address using firewalld with subprocess for better security
|
||||
success = False
|
||||
error_message = ''
|
||||
|
||||
try:
|
||||
# Use subprocess with explicit argument lists to prevent injection
|
||||
rich_rule = f'rule family=ipv4 source address={ip_address} drop'
|
||||
add_rule_cmd = ['firewall-cmd', '--permanent', '--add-rich-rule', rich_rule]
|
||||
|
||||
# Execute the add rule command
|
||||
result = subprocess.run(add_rule_cmd, capture_output=True, text=True, timeout=30)
|
||||
if result.returncode == 0:
|
||||
# Reload firewall rules
|
||||
reload_cmd = ['firewall-cmd', '--reload']
|
||||
reload_result = subprocess.run(reload_cmd, capture_output=True, text=True, timeout=30)
|
||||
if reload_result.returncode == 0:
|
||||
success = True
|
||||
else:
|
||||
error_message = f'Failed to reload firewall rules: {reload_result.stderr}'
|
||||
else:
|
||||
error_message = f'Failed to add firewall rule: {result.stderr}'
|
||||
except subprocess.TimeoutExpired:
|
||||
error_message = 'Firewall command timed out'
|
||||
except Exception as e:
|
||||
error_message = f'Firewall command failed: {str(e)}'
|
||||
success = False
|
||||
msg = str(e)
|
||||
|
||||
if success:
|
||||
# Add to banned IPs JSON file for consistency with firewall page
|
||||
try:
|
||||
import os
|
||||
import time
|
||||
banned_ips_file = '/etc/cyberpanel/banned_ips.json'
|
||||
primary_file = '/usr/local/CyberCP/data/banned_ips.json'
|
||||
legacy_file = '/etc/cyberpanel/banned_ips.json'
|
||||
banned_ips_file = primary_file if os.path.exists(primary_file) else legacy_file if os.path.exists(legacy_file) else primary_file
|
||||
banned_ips = []
|
||||
|
||||
if os.path.exists(banned_ips_file):
|
||||
@@ -1372,10 +1449,10 @@ def blockIPAddress(request):
|
||||
banned_ips.append(new_banned_ip)
|
||||
|
||||
# Ensure directory exists
|
||||
os.makedirs(os.path.dirname(banned_ips_file), exist_ok=True)
|
||||
os.makedirs(os.path.dirname(primary_file), exist_ok=True)
|
||||
|
||||
# Save to file
|
||||
with open(banned_ips_file, 'w') as f:
|
||||
with open(primary_file, 'w') as f:
|
||||
json.dump(banned_ips, f, indent=2)
|
||||
except Exception as e:
|
||||
# Log but don't fail the request if JSON update fails
|
||||
@@ -1394,7 +1471,7 @@ def blockIPAddress(request):
|
||||
else:
|
||||
return HttpResponse(json.dumps({
|
||||
'status': 0,
|
||||
'error': error_message or 'Failed to block IP address'
|
||||
'error': msg or 'Failed to block IP address'
|
||||
}), content_type='application/json', status=500)
|
||||
|
||||
except json.JSONDecodeError as e:
|
||||
|
||||
Reference in New Issue
Block a user