From 30243493d45164a408166d97230ec2bbdf6614dc Mon Sep 17 00:00:00 2001 From: usmannasir Date: Tue, 24 Feb 2026 01:59:19 +0500 Subject: [PATCH] Add panelv2 site-centric panel redesign at /v2/ New Django app with Alpine.js frontend providing site-centric navigation (select site first, then manage features) coexisting with the classic panel. - 14 page views + 10 AJAX API endpoints under /v2/ - Dual-mode sidebar (site list vs site context) - Full feature pages: databases, email, FTP, DNS, SSL, backup, domains, files, logs, config, apps, security, server management - Dark mode support via CSS variables - Reuses existing ACL, httpProc, and all backend utilities --- CyberCP/settings.py | 1 + CyberCP/urls.py | 1 + panelv2/__init__.py | 0 panelv2/static/panelv2/css/v2.css | 824 ++++++++++++++++++ panelv2/static/panelv2/js/v2.js | 260 ++++++ panelv2/templates/panelv2/apps.html | 90 ++ panelv2/templates/panelv2/backup.html | 79 ++ panelv2/templates/panelv2/base.html | 146 ++++ panelv2/templates/panelv2/config.html | 104 +++ panelv2/templates/panelv2/databases.html | 100 +++ panelv2/templates/panelv2/dns.html | 123 +++ panelv2/templates/panelv2/domains.html | 117 +++ panelv2/templates/panelv2/email.html | 109 +++ panelv2/templates/panelv2/files.html | 49 ++ panelv2/templates/panelv2/ftp.html | 103 +++ panelv2/templates/panelv2/logs.html | 58 ++ panelv2/templates/panelv2/security.html | 114 +++ panelv2/templates/panelv2/server.html | 84 ++ panelv2/templates/panelv2/site_dashboard.html | 173 ++++ panelv2/templates/panelv2/site_list.html | 93 ++ panelv2/templates/panelv2/ssl.html | 78 ++ panelv2/templatetags/__init__.py | 0 panelv2/templatetags/v2_tags.py | 38 + panelv2/urls.py | 37 + panelv2/views.py | 742 ++++++++++++++++ 25 files changed, 3523 insertions(+) create mode 100644 panelv2/__init__.py create mode 100644 panelv2/static/panelv2/css/v2.css create mode 100644 panelv2/static/panelv2/js/v2.js create mode 100644 panelv2/templates/panelv2/apps.html create mode 100644 panelv2/templates/panelv2/backup.html create mode 100644 panelv2/templates/panelv2/base.html create mode 100644 panelv2/templates/panelv2/config.html create mode 100644 panelv2/templates/panelv2/databases.html create mode 100644 panelv2/templates/panelv2/dns.html create mode 100644 panelv2/templates/panelv2/domains.html create mode 100644 panelv2/templates/panelv2/email.html create mode 100644 panelv2/templates/panelv2/files.html create mode 100644 panelv2/templates/panelv2/ftp.html create mode 100644 panelv2/templates/panelv2/logs.html create mode 100644 panelv2/templates/panelv2/security.html create mode 100644 panelv2/templates/panelv2/server.html create mode 100644 panelv2/templates/panelv2/site_dashboard.html create mode 100644 panelv2/templates/panelv2/site_list.html create mode 100644 panelv2/templates/panelv2/ssl.html create mode 100644 panelv2/templatetags/__init__.py create mode 100644 panelv2/templatetags/v2_tags.py create mode 100644 panelv2/urls.py create mode 100644 panelv2/views.py diff --git a/CyberCP/settings.py b/CyberCP/settings.py index 4587e58eb..f15f8a64a 100644 --- a/CyberCP/settings.py +++ b/CyberCP/settings.py @@ -75,6 +75,7 @@ INSTALLED_APPS = [ 'CLManager', 'IncBackups', 'aiScanner', + 'panelv2', # 'WebTerminal' ] diff --git a/CyberCP/urls.py b/CyberCP/urls.py index da7ab903a..ccaf30592 100644 --- a/CyberCP/urls.py +++ b/CyberCP/urls.py @@ -45,5 +45,6 @@ urlpatterns = [ path('CloudLinux/', include('CLManager.urls')), path('IncrementalBackups/', include('IncBackups.urls')), path('aiscanner/', include('aiScanner.urls')), + path('v2/', include('panelv2.urls')), # path('Terminal/', include('WebTerminal.urls')), ] diff --git a/panelv2/__init__.py b/panelv2/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/panelv2/static/panelv2/css/v2.css b/panelv2/static/panelv2/css/v2.css new file mode 100644 index 000000000..602012841 --- /dev/null +++ b/panelv2/static/panelv2/css/v2.css @@ -0,0 +1,824 @@ +/* ========================================================================= + CyberPanel v2 – Site-Centric Panel Styles + Extends the existing CSS variable design system + ========================================================================= */ + +/* --- CSS Variables (inherit from existing, add v2-specific) ------------- */ +:root { + --sidebar-width: 250px; + --header-height: 60px; + --v2-radius: 10px; + --v2-radius-sm: 6px; + --v2-transition: 0.2s ease; +} + +/* --- Reset / Base ------------------------------------------------------- */ +.v2-app { + min-height: 100vh; + background: var(--bg-primary); + color: var(--text-primary); + font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif; +} + +.v2-app *, .v2-app *::before, .v2-app *::after { + box-sizing: border-box; +} + +/* --- Header ------------------------------------------------------------- */ +.v2-header { + position: fixed; + top: 0; + left: var(--sidebar-width); + right: 0; + height: var(--header-height); + background: var(--bg-secondary); + border-bottom: 1px solid var(--border-color); + display: flex; + align-items: center; + justify-content: space-between; + padding: 0 24px; + z-index: 100; + transition: left var(--v2-transition), background-color 0.3s ease; +} + +.v2-header-left { + display: flex; + align-items: center; + gap: 16px; +} + +.v2-header-left h1 { + font-size: 18px; + font-weight: 600; + color: var(--text-heading); + margin: 0; +} + +.v2-breadcrumb { + display: flex; + align-items: center; + gap: 8px; + font-size: 14px; + color: var(--text-secondary); +} + +.v2-breadcrumb a { + color: var(--accent-color); + text-decoration: none; +} + +.v2-breadcrumb a:hover { + text-decoration: underline; +} + +.v2-breadcrumb .sep { + color: var(--text-secondary); + opacity: 0.5; +} + +.v2-header-right { + display: flex; + align-items: center; + gap: 12px; +} + +.v2-btn-icon { + width: 36px; + height: 36px; + border-radius: var(--v2-radius-sm); + border: 1px solid var(--border-color); + background: var(--bg-primary); + color: var(--text-primary); + display: flex; + align-items: center; + justify-content: center; + cursor: pointer; + transition: all var(--v2-transition); +} + +.v2-btn-icon:hover { + background: var(--bg-hover); + border-color: var(--accent-color); + color: var(--accent-color); +} + +.v2-user-badge { + display: flex; + align-items: center; + gap: 8px; + padding: 6px 12px; + border-radius: var(--v2-radius-sm); + background: var(--bg-primary); + font-size: 13px; + color: var(--text-secondary); +} + +.v2-user-badge i { + color: var(--accent-color); +} + +/* --- Sidebar ------------------------------------------------------------ */ +.v2-sidebar { + position: fixed; + top: 0; + left: 0; + width: var(--sidebar-width); + height: 100vh; + background: var(--bg-sidebar); + border-right: 1px solid var(--border-color); + display: flex; + flex-direction: column; + z-index: 200; + transition: transform var(--v2-transition), background-color 0.3s ease; + overflow-y: auto; +} + +.v2-sidebar-brand { + height: var(--header-height); + display: flex; + align-items: center; + padding: 0 20px; + gap: 10px; + border-bottom: 1px solid var(--border-color); + flex-shrink: 0; +} + +.v2-sidebar-brand img { + width: 28px; + height: 28px; +} + +.v2-sidebar-brand span { + font-size: 16px; + font-weight: 700; + color: var(--text-heading); +} + +.v2-sidebar-brand .v2-version { + font-size: 11px; + font-weight: 400; + color: var(--text-secondary); + margin-left: auto; +} + +.v2-sidebar-section { + padding: 16px 12px 8px; +} + +.v2-sidebar-label { + font-size: 10px; + font-weight: 700; + text-transform: uppercase; + letter-spacing: 1px; + color: var(--text-secondary); + padding: 0 8px; + margin-bottom: 8px; +} + +.v2-sidebar-nav { + list-style: none; + padding: 0; + margin: 0; +} + +.v2-sidebar-nav li a, +.v2-sidebar-nav li button { + display: flex; + align-items: center; + gap: 10px; + padding: 9px 12px; + border-radius: var(--v2-radius-sm); + color: var(--text-primary); + text-decoration: none; + font-size: 13px; + font-weight: 500; + transition: all var(--v2-transition); + width: 100%; + border: none; + background: none; + cursor: pointer; + text-align: left; +} + +.v2-sidebar-nav li a:hover, +.v2-sidebar-nav li button:hover { + background: var(--bg-hover); + color: var(--accent-color); +} + +.v2-sidebar-nav li a.active, +.v2-sidebar-nav li button.active { + background: var(--accent-color); + color: white; +} + +.v2-sidebar-nav li a.active i, +.v2-sidebar-nav li button.active i { + color: white; +} + +.v2-sidebar-nav li a i, +.v2-sidebar-nav li button i { + width: 18px; + text-align: center; + font-size: 14px; + color: var(--text-secondary); + transition: color var(--v2-transition); +} + +.v2-sidebar-nav li a:hover i, +.v2-sidebar-nav li button:hover i { + color: var(--accent-color); +} + +.v2-sidebar-nav .nav-count { + margin-left: auto; + font-size: 11px; + background: var(--bg-primary); + color: var(--text-secondary); + padding: 2px 7px; + border-radius: 10px; + font-weight: 600; +} + +.v2-sidebar-nav li a.active .nav-count { + background: rgba(255,255,255,0.2); + color: white; +} + +/* Site context header in sidebar */ +.v2-site-header { + padding: 16px 20px; + border-bottom: 1px solid var(--border-color); +} + +.v2-site-header .back-link { + display: inline-flex; + align-items: center; + gap: 6px; + font-size: 12px; + color: var(--text-secondary); + text-decoration: none; + margin-bottom: 8px; +} + +.v2-site-header .back-link:hover { + color: var(--accent-color); +} + +.v2-site-header .site-domain { + font-size: 15px; + font-weight: 700; + color: var(--text-heading); + word-break: break-all; +} + +/* Sidebar footer */ +.v2-sidebar-footer { + margin-top: auto; + padding: 12px; + border-top: 1px solid var(--border-color); +} + +/* --- Main Content ------------------------------------------------------- */ +.v2-main { + margin-left: var(--sidebar-width); + padding-top: var(--header-height); + min-height: 100vh; + transition: margin-left var(--v2-transition); +} + +.v2-content { + padding: 24px; + max-width: 1400px; +} + +/* --- Cards -------------------------------------------------------------- */ +.v2-card { + background: var(--bg-secondary); + border: 1px solid var(--border-color); + border-radius: var(--v2-radius); + transition: box-shadow var(--v2-transition), background-color 0.3s ease; +} + +.v2-card:hover { + box-shadow: 0 4px 16px var(--shadow-color); +} + +.v2-card-header { + padding: 16px 20px; + border-bottom: 1px solid var(--border-color); + display: flex; + align-items: center; + justify-content: space-between; +} + +.v2-card-header h2 { + font-size: 15px; + font-weight: 600; + color: var(--text-heading); + margin: 0; +} + +.v2-card-body { + padding: 20px; +} + +.v2-card-body.no-pad { + padding: 0; +} + +/* --- Stat Cards (Dashboard) --------------------------------------------- */ +.v2-stats-grid { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(200px, 1fr)); + gap: 16px; + margin-bottom: 24px; +} + +.v2-stat-card { + background: var(--bg-secondary); + border: 1px solid var(--border-color); + border-radius: var(--v2-radius); + padding: 20px; + display: flex; + align-items: flex-start; + gap: 14px; + transition: all var(--v2-transition); +} + +.v2-stat-card:hover { + box-shadow: 0 4px 16px var(--shadow-color); + transform: translateY(-1px); +} + +.v2-stat-icon { + width: 42px; + height: 42px; + border-radius: var(--v2-radius-sm); + display: flex; + align-items: center; + justify-content: center; + font-size: 18px; + flex-shrink: 0; +} + +.v2-stat-icon.purple { background: rgba(88,86,214,0.1); color: var(--accent-color); } +.v2-stat-icon.green { background: rgba(16,185,129,0.1); color: var(--success-color); } +.v2-stat-icon.red { background: rgba(239,68,68,0.1); color: var(--danger-color); } +.v2-stat-icon.blue { background: rgba(59,130,246,0.1); color: #3b82f6; } +.v2-stat-icon.orange { background: rgba(245,158,11,0.1); color: #f59e0b; } + +.v2-stat-info .label { + font-size: 12px; + color: var(--text-secondary); + margin-bottom: 4px; +} + +.v2-stat-info .value { + font-size: 22px; + font-weight: 700; + color: var(--text-heading); +} + +/* --- Feature Grid (Dashboard) ------------------------------------------- */ +.v2-feature-grid { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(180px, 1fr)); + gap: 14px; +} + +.v2-feature-card { + display: flex; + flex-direction: column; + align-items: center; + gap: 10px; + padding: 24px 16px; + background: var(--bg-secondary); + border: 1px solid var(--border-color); + border-radius: var(--v2-radius); + text-decoration: none; + color: var(--text-primary); + transition: all var(--v2-transition); + text-align: center; +} + +.v2-feature-card:hover { + border-color: var(--accent-color); + box-shadow: 0 4px 16px var(--shadow-color); + transform: translateY(-2px); + color: var(--accent-color); +} + +.v2-feature-card i { + font-size: 24px; + color: var(--accent-color); +} + +.v2-feature-card .feature-name { + font-size: 13px; + font-weight: 600; +} + +.v2-feature-card .feature-meta { + font-size: 11px; + color: var(--text-secondary); +} + +/* --- Tables ------------------------------------------------------------- */ +.v2-table-wrap { + overflow-x: auto; +} + +.v2-table { + width: 100%; + border-collapse: collapse; +} + +.v2-table th { + padding: 12px 16px; + text-align: left; + font-size: 11px; + font-weight: 700; + text-transform: uppercase; + letter-spacing: 0.5px; + color: var(--text-secondary); + background: var(--bg-primary); + border-bottom: 1px solid var(--border-color); +} + +.v2-table td { + padding: 12px 16px; + font-size: 13px; + border-bottom: 1px solid var(--border-color); + color: var(--text-primary); +} + +.v2-table tr:last-child td { + border-bottom: none; +} + +.v2-table tr:hover td { + background: var(--bg-hover); +} + +.v2-table .clickable-row { + cursor: pointer; +} + +/* --- Badges ------------------------------------------------------------- */ +.v2-badge { + display: inline-flex; + align-items: center; + padding: 3px 10px; + border-radius: 12px; + font-size: 11px; + font-weight: 600; +} + +.v2-badge-success { + background: rgba(16,185,129,0.12); + color: var(--success-color); +} + +.v2-badge-danger { + background: rgba(239,68,68,0.12); + color: var(--danger-color); +} + +.v2-badge-warning { + background: rgba(245,158,11,0.12); + color: #f59e0b; +} + +.v2-badge-info { + background: rgba(59,130,246,0.12); + color: #3b82f6; +} + +/* --- Buttons ------------------------------------------------------------ */ +.v2-btn { + display: inline-flex; + align-items: center; + gap: 6px; + padding: 8px 16px; + border-radius: var(--v2-radius-sm); + font-size: 13px; + font-weight: 600; + cursor: pointer; + border: 1px solid transparent; + transition: all var(--v2-transition); + text-decoration: none; + line-height: 1.4; +} + +.v2-btn-primary { + background: var(--accent-color); + color: white; + border-color: var(--accent-color); +} + +.v2-btn-primary:hover { + background: var(--accent-hover); + border-color: var(--accent-hover); +} + +.v2-btn-outline { + background: transparent; + color: var(--text-primary); + border-color: var(--border-color); +} + +.v2-btn-outline:hover { + border-color: var(--accent-color); + color: var(--accent-color); +} + +.v2-btn-danger { + background: var(--danger-color); + color: white; + border-color: var(--danger-color); +} + +.v2-btn-danger:hover { + opacity: 0.9; +} + +.v2-btn-sm { + padding: 5px 10px; + font-size: 12px; +} + +.v2-btn:disabled { + opacity: 0.5; + cursor: not-allowed; +} + +/* --- Forms -------------------------------------------------------------- */ +.v2-form-group { + margin-bottom: 16px; +} + +.v2-form-group label { + display: block; + font-size: 12px; + font-weight: 600; + color: var(--text-secondary); + margin-bottom: 6px; + text-transform: uppercase; + letter-spacing: 0.5px; +} + +.v2-input { + width: 100%; + padding: 9px 12px; + border: 1px solid var(--border-color); + border-radius: var(--v2-radius-sm); + background: var(--bg-primary); + color: var(--text-primary); + font-size: 13px; + transition: border-color var(--v2-transition); +} + +.v2-input:focus { + outline: none; + border-color: var(--accent-color); + box-shadow: 0 0 0 3px rgba(88,86,214,0.1); +} + +.v2-select { + width: 100%; + padding: 9px 12px; + border: 1px solid var(--border-color); + border-radius: var(--v2-radius-sm); + background: var(--bg-primary); + color: var(--text-primary); + font-size: 13px; + cursor: pointer; +} + +textarea.v2-input { + min-height: 120px; + font-family: 'SFMono-Regular', Consolas, 'Liberation Mono', Menlo, monospace; + resize: vertical; +} + +/* --- Search Bar --------------------------------------------------------- */ +.v2-search-bar { + display: flex; + align-items: center; + gap: 8px; + margin-bottom: 20px; +} + +.v2-search-bar .v2-input { + max-width: 320px; +} + +/* --- Alerts / Toast ----------------------------------------------------- */ +.v2-alert { + padding: 12px 16px; + border-radius: var(--v2-radius-sm); + font-size: 13px; + margin-bottom: 16px; + display: flex; + align-items: center; + gap: 10px; +} + +.v2-alert-success { + background: rgba(16,185,129,0.1); + color: var(--success-color); + border: 1px solid rgba(16,185,129,0.2); +} + +.v2-alert-danger { + background: rgba(239,68,68,0.1); + color: var(--danger-color); + border: 1px solid rgba(239,68,68,0.2); +} + +.v2-alert-info { + background: rgba(59,130,246,0.1); + color: #3b82f6; + border: 1px solid rgba(59,130,246,0.2); +} + +/* --- Modal -------------------------------------------------------------- */ +.v2-modal-backdrop { + position: fixed; + inset: 0; + background: rgba(0,0,0,0.5); + z-index: 500; + display: flex; + align-items: center; + justify-content: center; + padding: 24px; +} + +.v2-modal { + background: var(--bg-secondary); + border-radius: var(--v2-radius); + width: 100%; + max-width: 520px; + max-height: 90vh; + overflow-y: auto; + box-shadow: 0 20px 60px rgba(0,0,0,0.2); +} + +.v2-modal-header { + padding: 16px 20px; + border-bottom: 1px solid var(--border-color); + display: flex; + align-items: center; + justify-content: space-between; +} + +.v2-modal-header h3 { + font-size: 15px; + font-weight: 600; + color: var(--text-heading); + margin: 0; +} + +.v2-modal-body { + padding: 20px; +} + +.v2-modal-footer { + padding: 12px 20px; + border-top: 1px solid var(--border-color); + display: flex; + justify-content: flex-end; + gap: 8px; +} + +/* --- Empty State -------------------------------------------------------- */ +.v2-empty { + text-align: center; + padding: 48px 24px; + color: var(--text-secondary); +} + +.v2-empty i { + font-size: 40px; + margin-bottom: 16px; + opacity: 0.4; +} + +.v2-empty p { + font-size: 14px; + margin-bottom: 16px; +} + +/* --- Loading Spinner ---------------------------------------------------- */ +.v2-spinner { + display: inline-block; + width: 18px; + height: 18px; + border: 2px solid var(--border-color); + border-top-color: var(--accent-color); + border-radius: 50%; + animation: v2spin 0.6s linear infinite; +} + +@keyframes v2spin { + to { transform: rotate(360deg); } +} + +/* --- Code Editor -------------------------------------------------------- */ +.v2-code-editor { + width: 100%; + min-height: 300px; + font-family: 'SFMono-Regular', Consolas, 'Liberation Mono', Menlo, monospace; + font-size: 13px; + line-height: 1.5; + padding: 16px; + border: 1px solid var(--border-color); + border-radius: var(--v2-radius-sm); + background: var(--bg-primary); + color: var(--text-primary); + resize: vertical; + tab-size: 4; +} + +/* --- Progress Bar ------------------------------------------------------- */ +.v2-progress { + height: 6px; + background: var(--bg-primary); + border-radius: 3px; + overflow: hidden; + margin-top: 8px; +} + +.v2-progress-bar { + height: 100%; + border-radius: 3px; + background: var(--accent-color); + transition: width 0.3s ease; +} + +.v2-progress-bar.green { background: var(--success-color); } +.v2-progress-bar.red { background: var(--danger-color); } +.v2-progress-bar.orange { background: #f59e0b; } + +/* --- Sidebar mobile toggle ---------------------------------------------- */ +.v2-menu-toggle { + display: none; + background: none; + border: none; + color: var(--text-primary); + font-size: 20px; + cursor: pointer; + padding: 4px; +} + +/* --- Responsive --------------------------------------------------------- */ +@media (max-width: 768px) { + .v2-sidebar { + transform: translateX(-100%); + } + + .v2-sidebar.open { + transform: translateX(0); + } + + .v2-header { + left: 0; + } + + .v2-main { + margin-left: 0; + } + + .v2-menu-toggle { + display: block; + } + + .v2-stats-grid { + grid-template-columns: repeat(2, 1fr); + } + + .v2-feature-grid { + grid-template-columns: repeat(2, 1fr); + } + + .v2-modal { + max-width: 100%; + margin: 12px; + } +} + +@media (max-width: 480px) { + .v2-stats-grid { + grid-template-columns: 1fr; + } + + .v2-feature-grid { + grid-template-columns: 1fr; + } + + .v2-content { + padding: 16px; + } +} diff --git a/panelv2/static/panelv2/js/v2.js b/panelv2/static/panelv2/js/v2.js new file mode 100644 index 000000000..459d8cc73 --- /dev/null +++ b/panelv2/static/panelv2/js/v2.js @@ -0,0 +1,260 @@ +/* ========================================================================= + CyberPanel v2 – Alpine.js Components & AJAX Helpers + ========================================================================= */ + +/** + * CSRF token helper – reads from cookie (Django default). + */ +function getCSRFToken() { + const name = 'csrftoken'; + const cookies = document.cookie.split(';'); + for (let c of cookies) { + c = c.trim(); + if (c.startsWith(name + '=')) { + return c.substring(name.length + 1); + } + } + return ''; +} + +/** + * Fetch wrapper for v2 API calls. + * @param {string} url + * @param {object} options - { method, body } + * @returns {Promise} parsed JSON response + */ +async function v2Fetch(url, options = {}) { + const method = options.method || 'GET'; + const headers = { + 'Content-Type': 'application/json', + 'X-CSRFToken': getCSRFToken(), + }; + const config = { method, headers, credentials: 'same-origin' }; + if (options.body && method !== 'GET') { + config.body = JSON.stringify(options.body); + } + const resp = await fetch(url, config); + return resp.json(); +} + +/* ========================================================================= + Alpine.js Store – Theme + ========================================================================= */ +document.addEventListener('alpine:init', () => { + + Alpine.store('theme', { + dark: localStorage.getItem('v2_theme') === 'dark', + + toggle() { + this.dark = !this.dark; + localStorage.setItem('v2_theme', this.dark ? 'dark' : 'light'); + document.documentElement.setAttribute('data-theme', this.dark ? 'dark' : ''); + }, + + init() { + if (this.dark) { + document.documentElement.setAttribute('data-theme', 'dark'); + } + } + }); + + Alpine.store('sidebar', { + open: false, + toggle() { this.open = !this.open; }, + close() { this.open = false; } + }); +}); + +/* ========================================================================= + Alpine.js Data Components + ========================================================================= */ + +/** + * Site list component with search & pagination. + * Expects `window.__v2_sites` to be set by the template. + */ +function siteListComponent() { + return { + sites: window.__v2_sites || [], + search: '', + page: 1, + perPage: 20, + + get filtered() { + const q = this.search.toLowerCase(); + if (!q) return this.sites; + return this.sites.filter(s => s.domain.toLowerCase().includes(q)); + }, + + get paged() { + const start = (this.page - 1) * this.perPage; + return this.filtered.slice(start, start + this.perPage); + }, + + get totalPages() { + return Math.max(1, Math.ceil(this.filtered.length / this.perPage)); + }, + + goToSite(id) { + window.location.href = '/v2/sites/' + id + '/'; + } + }; +} + +/** + * Generic CRUD component for feature pages (databases, email, ftp, etc.). + * @param {string} apiUrl - base API URL, e.g. /v2/api/sites/3/databases + * @param {string} itemsKey - JSON key for items list, e.g. 'databases' + */ +function crudComponent(apiUrl, itemsKey) { + return { + items: [], + loading: true, + showForm: false, + formData: {}, + error: '', + success: '', + + async init() { + await this.load(); + }, + + async load() { + this.loading = true; + this.error = ''; + try { + const data = await v2Fetch(apiUrl); + if (data.status === 1) { + this.items = data[itemsKey] || []; + } else { + this.error = data.error_message; + } + } catch (e) { + this.error = 'Failed to load data'; + } + this.loading = false; + }, + + async create(body) { + this.error = ''; + this.success = ''; + try { + const data = await v2Fetch(apiUrl, { method: 'POST', body }); + if (data.status === 1) { + this.success = 'Created successfully'; + this.showForm = false; + this.formData = {}; + await this.load(); + } else { + this.error = data.error_message; + } + } catch (e) { + this.error = 'Request failed'; + } + }, + + async remove(body) { + if (!confirm('Are you sure you want to delete this?')) return; + this.error = ''; + this.success = ''; + try { + const data = await v2Fetch(apiUrl, { method: 'DELETE', body }); + if (data.status === 1) { + this.success = 'Deleted successfully'; + await this.load(); + } else { + this.error = data.error_message; + } + } catch (e) { + this.error = 'Request failed'; + } + }, + + clearMessages() { + this.error = ''; + this.success = ''; + } + }; +} + +/** + * Log viewer component. + * @param {string} apiUrl - /v2/api/sites//logs + */ +function logViewerComponent(apiUrl) { + return { + logs: [], + logType: 'access', + lines: 100, + loading: false, + + async load() { + this.loading = true; + try { + const data = await v2Fetch(apiUrl + '?type=' + this.logType + '&lines=' + this.lines); + if (data.status === 1) { + this.logs = data.logs || []; + } + } catch (e) { + // silent + } + this.loading = false; + }, + + init() { + this.load(); + } + }; +} + +/** + * Config editor component. + * @param {string} apiUrl - /v2/api/sites//config + */ +function configEditorComponent(apiUrl) { + return { + content: '', + configType: 'vhost', + loading: false, + error: '', + success: '', + + async load() { + this.loading = true; + this.error = ''; + try { + const data = await v2Fetch(apiUrl + '?type=' + this.configType); + if (data.status === 1) { + this.content = data.content || ''; + } else { + this.error = data.error_message; + } + } catch (e) { + this.error = 'Failed to load config'; + } + this.loading = false; + }, + + async save() { + this.error = ''; + this.success = ''; + try { + const data = await v2Fetch(apiUrl, { + method: 'POST', + body: { type: this.configType, content: this.content } + }); + if (data.status === 1) { + this.success = 'Saved successfully'; + } else { + this.error = data.error_message; + } + } catch (e) { + this.error = 'Request failed'; + } + }, + + init() { + this.load(); + } + }; +} diff --git a/panelv2/templates/panelv2/apps.html b/panelv2/templates/panelv2/apps.html new file mode 100644 index 000000000..6dd643eb9 --- /dev/null +++ b/panelv2/templates/panelv2/apps.html @@ -0,0 +1,90 @@ +{% extends "panelv2/base.html" %} + +{% block title %}Apps - {{ site_context.domain }} - CyberPanel v2{% endblock %} +{% block nav_apps %}active{% endblock %} + +{% block breadcrumb %} +Sites +/ +{{ site_context.domain }} +/ +Apps +{% endblock %} + +{% block content %} +
+ +
+

Applications

+
+ + +
+
+

WordPress Sites

+
+
+ {% if wp_sites %} +
+ + + + + + + + + + + {% for wp in wp_sites %} + + + + + + + {% endfor %} + +
TitleURLPathAuto Updates
{{ wp.title }} + + {{ wp.FinalURL }} + + {{ wp.path }} + + {{ wp.AutoUpdates }} + +
+
+ {% else %} +
+ +

No WordPress installations found for this site.

+
+ {% endif %} +
+
+ + +
+
+

Install Applications

+
+ +
+
+{% endblock %} diff --git a/panelv2/templates/panelv2/backup.html b/panelv2/templates/panelv2/backup.html new file mode 100644 index 000000000..87b86cb50 --- /dev/null +++ b/panelv2/templates/panelv2/backup.html @@ -0,0 +1,79 @@ +{% extends "panelv2/base.html" %} + +{% block title %}Backup - {{ site_context.domain }} - CyberPanel v2{% endblock %} +{% block nav_backup %}active{% endblock %} + +{% block breadcrumb %} +Sites +/ +{{ site_context.domain }} +/ +Backup +{% endblock %} + +{% block content %} +
+ +
+

Backups

+ +
+ + + + + + +
+
+ + + +
+
+
+{% endblock %} diff --git a/panelv2/templates/panelv2/base.html b/panelv2/templates/panelv2/base.html new file mode 100644 index 000000000..a3834c823 --- /dev/null +++ b/panelv2/templates/panelv2/base.html @@ -0,0 +1,146 @@ +{% load static %} +{% load v2_tags %} + + + + + + {% block title %}CyberPanel v2{% endblock %} + + + + + {% block extra_css %}{% endblock %} + + + + + + + +
+
+ +
+ {% block breadcrumb %} + Sites + {% endblock %} +
+
+
+ + {% if fullName %} +
+ + {{ fullName }} +
+ {% endif %} +
+
+ + +
+
+ {% block content %}{% endblock %} +
+
+ + +
+ + + + +{% block extra_js %}{% endblock %} + + diff --git a/panelv2/templates/panelv2/config.html b/panelv2/templates/panelv2/config.html new file mode 100644 index 000000000..e67ccdf89 --- /dev/null +++ b/panelv2/templates/panelv2/config.html @@ -0,0 +1,104 @@ +{% extends "panelv2/base.html" %} + +{% block title %}Config - {{ site_context.domain }} - CyberPanel v2{% endblock %} +{% block nav_config %}active{% endblock %} + +{% block breadcrumb %} +Sites +/ +{{ site_context.domain }} +/ +Config +{% endblock %} + +{% block content %} +
+ +
+

Site Configuration

+
+ + + + + + +
+
+

PHP Version

+
+
+
+
Current PHP:
+ {{ website.phpSelection }} + + +
+ + +
+
+ + +
+
+

Configuration Files

+
+ +
+
+
+ + +
+
+
+{% endblock %} diff --git a/panelv2/templates/panelv2/databases.html b/panelv2/templates/panelv2/databases.html new file mode 100644 index 000000000..00258239a --- /dev/null +++ b/panelv2/templates/panelv2/databases.html @@ -0,0 +1,100 @@ +{% extends "panelv2/base.html" %} + +{% block title %}Databases - {{ site_context.domain }} - CyberPanel v2{% endblock %} +{% block nav_databases %}active{% endblock %} + +{% block breadcrumb %} +Sites +/ +{{ site_context.domain }} +/ +Databases +{% endblock %} + +{% block content %} +
+ +
+

Databases

+ +
+ + + + + + + + + +
+
+ + + +
+
+
+{% endblock %} diff --git a/panelv2/templates/panelv2/dns.html b/panelv2/templates/panelv2/dns.html new file mode 100644 index 000000000..ea57ecd83 --- /dev/null +++ b/panelv2/templates/panelv2/dns.html @@ -0,0 +1,123 @@ +{% extends "panelv2/base.html" %} + +{% block title %}DNS - {{ site_context.domain }} - CyberPanel v2{% endblock %} +{% block nav_dns %}active{% endblock %} + +{% block breadcrumb %} +Sites +/ +{{ site_context.domain }} +/ +DNS +{% endblock %} + +{% block content %} +
+ +
+

DNS Records

+ +
+ + + + + + + + + +
+
+ + + +
+
+
+{% endblock %} diff --git a/panelv2/templates/panelv2/domains.html b/panelv2/templates/panelv2/domains.html new file mode 100644 index 000000000..23c672640 --- /dev/null +++ b/panelv2/templates/panelv2/domains.html @@ -0,0 +1,117 @@ +{% extends "panelv2/base.html" %} + +{% block title %}Domains - {{ site_context.domain }} - CyberPanel v2{% endblock %} +{% block nav_domains %}active{% endblock %} + +{% block breadcrumb %} +Sites +/ +{{ site_context.domain }} +/ +Domains +{% endblock %} + +{% block content %} +
+ +
+

Child Domains & Aliases

+ +
+ + + + + + + + + +
+
+ + + +
+
+
+{% endblock %} diff --git a/panelv2/templates/panelv2/email.html b/panelv2/templates/panelv2/email.html new file mode 100644 index 000000000..37792b877 --- /dev/null +++ b/panelv2/templates/panelv2/email.html @@ -0,0 +1,109 @@ +{% extends "panelv2/base.html" %} + +{% block title %}Email - {{ site_context.domain }} - CyberPanel v2{% endblock %} +{% block nav_email %}active{% endblock %} + +{% block breadcrumb %} +Sites +/ +{{ site_context.domain }} +/ +Email +{% endblock %} + +{% block content %} +
+ +
+

Email Accounts

+ +
+ + + + + + + + + +
+
+ + + +
+
+
+{% endblock %} diff --git a/panelv2/templates/panelv2/files.html b/panelv2/templates/panelv2/files.html new file mode 100644 index 000000000..0a8a6867b --- /dev/null +++ b/panelv2/templates/panelv2/files.html @@ -0,0 +1,49 @@ +{% extends "panelv2/base.html" %} + +{% block title %}Files - {{ site_context.domain }} - CyberPanel v2{% endblock %} +{% block nav_files %}active{% endblock %} + +{% block breadcrumb %} +Sites +/ +{{ site_context.domain }} +/ +Files +{% endblock %} + +{% block content %} +
+ +
+

File Manager

+ + Open File Manager + +
+ + +
+
+
+
+ +
+
+
{{ website.domain }}
+
Document Root: /home/{{ website.domain }}/public_html
+
+
+
+
+ + +
+
+

File Manager

+
+
+ +
+
+
+{% endblock %} diff --git a/panelv2/templates/panelv2/ftp.html b/panelv2/templates/panelv2/ftp.html new file mode 100644 index 000000000..3fd69be61 --- /dev/null +++ b/panelv2/templates/panelv2/ftp.html @@ -0,0 +1,103 @@ +{% extends "panelv2/base.html" %} + +{% block title %}FTP - {{ site_context.domain }} - CyberPanel v2{% endblock %} +{% block nav_ftp %}active{% endblock %} + +{% block breadcrumb %} +Sites +/ +{{ site_context.domain }} +/ +FTP +{% endblock %} + +{% block content %} +
+ +
+

FTP Accounts

+ +
+ + + + + + + + + +
+
+ + + +
+
+
+{% endblock %} diff --git a/panelv2/templates/panelv2/logs.html b/panelv2/templates/panelv2/logs.html new file mode 100644 index 000000000..d81b4060e --- /dev/null +++ b/panelv2/templates/panelv2/logs.html @@ -0,0 +1,58 @@ +{% extends "panelv2/base.html" %} + +{% block title %}Logs - {{ site_context.domain }} - CyberPanel v2{% endblock %} +{% block nav_logs %}active{% endblock %} + +{% block breadcrumb %} +Sites +/ +{{ site_context.domain }} +/ +Logs +{% endblock %} + +{% block content %} +
+ +
+

Site Logs

+
+ + + +
+
+ + +
+
+

+ +
+
+ + + +
+
+
+{% endblock %} diff --git a/panelv2/templates/panelv2/security.html b/panelv2/templates/panelv2/security.html new file mode 100644 index 000000000..7f4b568b5 --- /dev/null +++ b/panelv2/templates/panelv2/security.html @@ -0,0 +1,114 @@ +{% extends "panelv2/base.html" %} + +{% block title %}Security - {{ site_context.domain }} - CyberPanel v2{% endblock %} +{% block nav_security %}active{% endblock %} + +{% block breadcrumb %} +Sites +/ +{{ site_context.domain }} +/ +Security +{% endblock %} + +{% block content %} +
+ +
+

Security Settings

+
+ + + + + + +
+
+

open_basedir Protection

+ +
+
+

+ open_basedir restricts PHP scripts to only access files within the site's directory. This prevents a compromised script from reading other sites' files on the server. +

+ + +
+
+ + +
+
+

SSH / SFTP Access

+
+
+
+
+
SFTP Host
+
{{ ipAddress }}
+
+
+
SFTP Port
+
22
+
+
+
Document Root
+
/home/{{ website.domain }}/public_html
+
+
+
+
+
+{% endblock %} diff --git a/panelv2/templates/panelv2/server.html b/panelv2/templates/panelv2/server.html new file mode 100644 index 000000000..1036a835e --- /dev/null +++ b/panelv2/templates/panelv2/server.html @@ -0,0 +1,84 @@ +{% extends "panelv2/base.html" %} + +{% block title %}Server Management - CyberPanel v2{% endblock %} +{% block nav_server %}active{% endblock %} + +{% block breadcrumb %} +Sites +/ +Server Management +{% endblock %} + +{% block content %} +
+ +
+

Server Management

+

Quick access to server-level management tools

+
+ + + +
+{% endblock %} diff --git a/panelv2/templates/panelv2/site_dashboard.html b/panelv2/templates/panelv2/site_dashboard.html new file mode 100644 index 000000000..c77b2b5e4 --- /dev/null +++ b/panelv2/templates/panelv2/site_dashboard.html @@ -0,0 +1,173 @@ +{% extends "panelv2/base.html" %} + +{% block title %}{{ site_context.domain }} - CyberPanel v2{% endblock %} +{% block nav_overview %}active{% endblock %} + +{% block breadcrumb %} +Sites +/ +{{ site_context.domain }} +{% endblock %} + +{% block content %} +
+ +
+

{{ site_context.domain }}

+

Site overview and quick navigation

+
+ + +
+
+
+
+
Databases
+
{{ databases_count }}
+
+
+
+
+
+
Email Accounts
+
{{ email_count }}
+
+
+
+
+
+
FTP Accounts
+
{{ ftp_count }}
+
+
+
+
+
+
Child Domains
+
{{ child_count }}
+
+
+
+
+ +
+
+
SSL
+
{% if website.ssl == 1 %}Active{% else %}None{% endif %}
+
+
+
+
+
+
WordPress Sites
+
{{ wp_count }}
+
+
+
+ + +
+
+

Site Details

+ + {% if website.state == 1 %}Active{% else %}Suspended{% endif %} + +
+
+
+
+
PHP Version
+
{{ website.phpSelection }}
+
+
+
Admin Email
+
{{ website.adminEmail }}
+
+
+
Backups
+
{{ backup_count }}
+
+
+
Document Root
+
/home/{{ website.domain }}/public_html
+
+
+
+
+ + + + + +

Manage

+ +
+{% endblock %} diff --git a/panelv2/templates/panelv2/site_list.html b/panelv2/templates/panelv2/site_list.html new file mode 100644 index 000000000..ccd4d592d --- /dev/null +++ b/panelv2/templates/panelv2/site_list.html @@ -0,0 +1,93 @@ +{% extends "panelv2/base.html" %} +{% load v2_tags %} + +{% block title %}Sites - CyberPanel v2{% endblock %} +{% block nav_site_list %}active{% endblock %} + +{% block breadcrumb %} +Sites +{% endblock %} + +{% block content %} +
+ +
+

Websites

+ {% if createWebsite %} + + Create Website + + {% endif %} +
+ + + + + +
+
+
+ + + + + + + + + + + + +
DomainPHPSSLState
+
+ + + +
+
+ + + +
+ + +{% endblock %} diff --git a/panelv2/templates/panelv2/ssl.html b/panelv2/templates/panelv2/ssl.html new file mode 100644 index 000000000..4f9ae8189 --- /dev/null +++ b/panelv2/templates/panelv2/ssl.html @@ -0,0 +1,78 @@ +{% extends "panelv2/base.html" %} + +{% block title %}SSL - {{ site_context.domain }} - CyberPanel v2{% endblock %} +{% block nav_ssl %}active{% endblock %} + +{% block breadcrumb %} +Sites +/ +{{ site_context.domain }} +/ +SSL +{% endblock %} + +{% block content %} +
+ +
+

SSL Certificate

+
+ + + + + + +
+
+

Current Status

+ + {% if website.ssl == 1 %}SSL Active{% else %}No SSL{% endif %} + +
+
+
+
+
Domain
+
{{ website.domain }}
+
+
+
SSL Status
+
{% if website.ssl == 1 %}Enabled{% else %}Disabled{% endif %}
+
+
+
+
+ + +
+
+

Issue SSL Certificate

+
+
+

+ Issue a free Let's Encrypt SSL certificate for {{ website.domain }}. Make sure the domain's DNS points to this server. +

+ +
+
+
+{% endblock %} diff --git a/panelv2/templatetags/__init__.py b/panelv2/templatetags/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/panelv2/templatetags/v2_tags.py b/panelv2/templatetags/v2_tags.py new file mode 100644 index 000000000..ba09c1178 --- /dev/null +++ b/panelv2/templatetags/v2_tags.py @@ -0,0 +1,38 @@ +from django import template +from plogical.acl import ACLManager + +register = template.Library() + + +@register.simple_tag +def has_permission(acl, permission): + """Check if user has a specific permission. + Usage: {% has_permission acl 'createDatabase' as can_create_db %} + """ + if acl.get('admin'): + return True + return bool(acl.get(permission)) + + +@register.filter +def ssl_badge(ssl_value): + """Return badge class based on SSL status.""" + if ssl_value == 1: + return 'badge-success' + return 'badge-warning' + + +@register.filter +def site_state(state_value): + """Return human-readable site state.""" + if state_value == 1: + return 'Active' + return 'Suspended' + + +@register.filter +def site_state_class(state_value): + """Return CSS class for site state.""" + if state_value == 1: + return 'text-success' + return 'text-danger' diff --git a/panelv2/urls.py b/panelv2/urls.py new file mode 100644 index 000000000..883b009fd --- /dev/null +++ b/panelv2/urls.py @@ -0,0 +1,37 @@ +from django.urls import path +from . import views + +urlpatterns = [ + # Site list (landing page) + path('', views.site_list, name='v2_site_list'), + + # Site-scoped pages + path('sites//', views.site_dashboard, name='v2_site_dashboard'), + path('sites//domains', views.site_domains, name='v2_site_domains'), + path('sites//databases', views.site_databases, name='v2_site_databases'), + path('sites//email', views.site_email, name='v2_site_email'), + path('sites//ftp', views.site_ftp, name='v2_site_ftp'), + path('sites//dns', views.site_dns, name='v2_site_dns'), + path('sites//ssl', views.site_ssl, name='v2_site_ssl'), + path('sites//backup', views.site_backup, name='v2_site_backup'), + path('sites//files', views.site_files, name='v2_site_files'), + path('sites//logs', views.site_logs, name='v2_site_logs'), + path('sites//config', views.site_config, name='v2_site_config'), + path('sites//apps', views.site_apps, name='v2_site_apps'), + path('sites//security', views.site_security, name='v2_site_security'), + + # AJAX API endpoints (site-scoped) + path('api/sites//databases', views.api_databases, name='v2_api_databases'), + path('api/sites//email', views.api_email, name='v2_api_email'), + path('api/sites//ftp', views.api_ftp, name='v2_api_ftp'), + path('api/sites//dns', views.api_dns, name='v2_api_dns'), + path('api/sites//ssl', views.api_ssl, name='v2_api_ssl'), + path('api/sites//backup', views.api_backup, name='v2_api_backup'), + path('api/sites//logs', views.api_logs, name='v2_api_logs'), + path('api/sites//config', views.api_config, name='v2_api_config'), + path('api/sites//domains', views.api_domains, name='v2_api_domains'), + path('api/sites//security', views.api_security, name='v2_api_security'), + + # Server management (admin only) + path('server/', views.server_management, name='v2_server'), +] diff --git a/panelv2/views.py b/panelv2/views.py new file mode 100644 index 000000000..ec4f0f898 --- /dev/null +++ b/panelv2/views.py @@ -0,0 +1,742 @@ +import json +from django.shortcuts import redirect, HttpResponse +from plogical.httpProc import httpProc +from plogical.acl import ACLManager +from loginSystem.models import Administrator +from websiteFunctions.models import Websites, ChildDomains, Backups, WPSites +from databases.models import Databases +from ftp.models import Users as FTPUsers +from mailServer.models import Domains as EmailDomains, EUsers +from dns.models import Domains as DNSDomains, Records as DNSRecords + + +# --------------------------------------------------------------------------- +# Helpers +# --------------------------------------------------------------------------- + +def _auth(request): + """Return (userID, admin, currentACL) or raise KeyError.""" + userID = request.session['userID'] + currentACL = ACLManager.loadedACL(userID) + admin = Administrator.objects.get(pk=userID) + return userID, admin, currentACL + + +def _get_site(site_id, admin, currentACL): + """Fetch website by pk and verify ownership. Returns website or None.""" + try: + website = Websites.objects.get(pk=site_id) + except Websites.DoesNotExist: + return None + if ACLManager.checkOwnership(website.domain, admin, currentACL) != 1: + return None + return website + + +def _site_context(website): + """Build the site_context dict passed to every site-scoped template.""" + return {'id': website.pk, 'domain': website.domain} + + +def _forbidden(): + return HttpResponse("Unauthorized", status=403) + + +def _not_found(): + return HttpResponse("Not Found", status=404) + + +def _login_redirect(): + from loginSystem.views import loadLoginPage + return redirect(loadLoginPage) + + +def _json(status, message, **extra): + data = {'status': status, 'error_message': message} + data.update(extra) + return HttpResponse(json.dumps(data), content_type='application/json') + + +# --------------------------------------------------------------------------- +# Page views +# --------------------------------------------------------------------------- + +def site_list(request): + try: + userID, admin, currentACL = _auth(request) + except KeyError: + return _login_redirect() + + websites = ACLManager.findWebsiteObjects(currentACL, userID) + data = { + 'site_context': None, + 'websites': websites, + } + proc = httpProc(request, 'panelv2/site_list.html', data, 'listWebsites') + return proc.render() + + +def site_dashboard(request, site_id): + try: + userID, admin, currentACL = _auth(request) + except KeyError: + return _login_redirect() + + website = _get_site(site_id, admin, currentACL) + if not website: + return _forbidden() + + databases_count = Databases.objects.filter(website=website).count() + ftp_count = FTPUsers.objects.filter(domain=website).count() + email_domains = EmailDomains.objects.filter(domainOwner=website) + email_count = EUsers.objects.filter(emailOwner__in=email_domains).count() + child_count = ChildDomains.objects.filter(master=website).count() + wp_count = WPSites.objects.filter(owner=website).count() + backup_count = Backups.objects.filter(website=website).count() + + data = { + 'site_context': _site_context(website), + 'website': website, + 'databases_count': databases_count, + 'ftp_count': ftp_count, + 'email_count': email_count, + 'child_count': child_count, + 'wp_count': wp_count, + 'backup_count': backup_count, + } + proc = httpProc(request, 'panelv2/site_dashboard.html', data) + return proc.render() + + +def site_databases(request, site_id): + try: + userID, admin, currentACL = _auth(request) + except KeyError: + return _login_redirect() + + website = _get_site(site_id, admin, currentACL) + if not website: + return _forbidden() + + databases = Databases.objects.filter(website=website) + data = { + 'site_context': _site_context(website), + 'website': website, + 'databases': databases, + } + proc = httpProc(request, 'panelv2/databases.html', data, 'createDatabase') + return proc.render() + + +def site_email(request, site_id): + try: + userID, admin, currentACL = _auth(request) + except KeyError: + return _login_redirect() + + website = _get_site(site_id, admin, currentACL) + if not website: + return _forbidden() + + email_domains = EmailDomains.objects.filter(domainOwner=website) + emails = EUsers.objects.filter(emailOwner__in=email_domains) + data = { + 'site_context': _site_context(website), + 'website': website, + 'email_domains': email_domains, + 'emails': emails, + } + proc = httpProc(request, 'panelv2/email.html', data, 'createEmail') + return proc.render() + + +def site_ftp(request, site_id): + try: + userID, admin, currentACL = _auth(request) + except KeyError: + return _login_redirect() + + website = _get_site(site_id, admin, currentACL) + if not website: + return _forbidden() + + ftp_accounts = FTPUsers.objects.filter(domain=website) + data = { + 'site_context': _site_context(website), + 'website': website, + 'ftp_accounts': ftp_accounts, + } + proc = httpProc(request, 'panelv2/ftp.html', data, 'createFTPAccount') + return proc.render() + + +def site_dns(request, site_id): + try: + userID, admin, currentACL = _auth(request) + except KeyError: + return _login_redirect() + + website = _get_site(site_id, admin, currentACL) + if not website: + return _forbidden() + + try: + dns_domain = DNSDomains.objects.get(name=website.domain) + records = DNSRecords.objects.filter(domainOwner=dns_domain) + except DNSDomains.DoesNotExist: + dns_domain = None + records = [] + + data = { + 'site_context': _site_context(website), + 'website': website, + 'dns_domain': dns_domain, + 'records': records, + } + proc = httpProc(request, 'panelv2/dns.html', data, 'addDeleteDNSRecords') + return proc.render() + + +def site_ssl(request, site_id): + try: + userID, admin, currentACL = _auth(request) + except KeyError: + return _login_redirect() + + website = _get_site(site_id, admin, currentACL) + if not website: + return _forbidden() + + data = { + 'site_context': _site_context(website), + 'website': website, + } + proc = httpProc(request, 'panelv2/ssl.html', data, 'issueSSL') + return proc.render() + + +def site_backup(request, site_id): + try: + userID, admin, currentACL = _auth(request) + except KeyError: + return _login_redirect() + + website = _get_site(site_id, admin, currentACL) + if not website: + return _forbidden() + + backups = Backups.objects.filter(website=website).order_by('-id') + data = { + 'site_context': _site_context(website), + 'website': website, + 'backups': backups, + } + proc = httpProc(request, 'panelv2/backup.html', data, 'createBackup') + return proc.render() + + +def site_domains(request, site_id): + try: + userID, admin, currentACL = _auth(request) + except KeyError: + return _login_redirect() + + website = _get_site(site_id, admin, currentACL) + if not website: + return _forbidden() + + child_domains = ChildDomains.objects.filter(master=website) + data = { + 'site_context': _site_context(website), + 'website': website, + 'child_domains': child_domains, + } + proc = httpProc(request, 'panelv2/domains.html', data, 'listWebsites') + return proc.render() + + +def site_files(request, site_id): + try: + userID, admin, currentACL = _auth(request) + except KeyError: + return _login_redirect() + + website = _get_site(site_id, admin, currentACL) + if not website: + return _forbidden() + + data = { + 'site_context': _site_context(website), + 'website': website, + } + proc = httpProc(request, 'panelv2/files.html', data) + return proc.render() + + +def site_logs(request, site_id): + try: + userID, admin, currentACL = _auth(request) + except KeyError: + return _login_redirect() + + website = _get_site(site_id, admin, currentACL) + if not website: + return _forbidden() + + data = { + 'site_context': _site_context(website), + 'website': website, + } + proc = httpProc(request, 'panelv2/logs.html', data, 'listWebsites') + return proc.render() + + +def site_config(request, site_id): + try: + userID, admin, currentACL = _auth(request) + except KeyError: + return _login_redirect() + + website = _get_site(site_id, admin, currentACL) + if not website: + return _forbidden() + + data = { + 'site_context': _site_context(website), + 'website': website, + } + proc = httpProc(request, 'panelv2/config.html', data, 'listWebsites') + return proc.render() + + +def site_apps(request, site_id): + try: + userID, admin, currentACL = _auth(request) + except KeyError: + return _login_redirect() + + website = _get_site(site_id, admin, currentACL) + if not website: + return _forbidden() + + wp_sites = WPSites.objects.filter(owner=website) + data = { + 'site_context': _site_context(website), + 'website': website, + 'wp_sites': wp_sites, + } + proc = httpProc(request, 'panelv2/apps.html', data, 'listWebsites') + return proc.render() + + +def site_security(request, site_id): + try: + userID, admin, currentACL = _auth(request) + except KeyError: + return _login_redirect() + + website = _get_site(site_id, admin, currentACL) + if not website: + return _forbidden() + + data = { + 'site_context': _site_context(website), + 'website': website, + } + proc = httpProc(request, 'panelv2/security.html', data, 'listWebsites') + return proc.render() + + +def server_management(request): + try: + userID, admin, currentACL = _auth(request) + except KeyError: + return _login_redirect() + + if not currentACL['admin']: + return _forbidden() + + data = { + 'site_context': None, + } + proc = httpProc(request, 'panelv2/server.html', data) + return proc.render() + + +# --------------------------------------------------------------------------- +# API endpoints (AJAX) +# --------------------------------------------------------------------------- + +def api_databases(request, site_id): + try: + userID, admin, currentACL = _auth(request) + except KeyError: + return _json(0, 'Not logged in') + + website = _get_site(site_id, admin, currentACL) + if not website: + return _json(0, 'Unauthorized or site not found') + + if request.method == 'GET': + dbs = Databases.objects.filter(website=website) + db_list = [{'name': db.dbName, 'user': db.dbUser, 'id': db.pk} for db in dbs] + return _json(1, 'None', databases=db_list) + + elif request.method == 'POST': + try: + data = json.loads(request.body) + data['databaseWebsite'] = website.domain + from plogical.mysqlUtilities import mysqlUtilities + mysqlUtilities.createDatabase(data['dbName'], data['dbUser'], data['dbPassword']) + newDB = Databases(website=website, dbName=data['dbName'], dbUser=data['dbUser']) + newDB.save() + return _json(1, 'None') + except BaseException as msg: + return _json(0, str(msg)) + + elif request.method == 'DELETE': + try: + data = json.loads(request.body) + db = Databases.objects.get(pk=data['id'], website=website) + from plogical.mysqlUtilities import mysqlUtilities + mysqlUtilities.deleteDatabase(db.dbName, db.dbUser) + db.delete() + return _json(1, 'None') + except BaseException as msg: + return _json(0, str(msg)) + + return _json(0, 'Invalid method') + + +def api_email(request, site_id): + try: + userID, admin, currentACL = _auth(request) + except KeyError: + return _json(0, 'Not logged in') + + website = _get_site(site_id, admin, currentACL) + if not website: + return _json(0, 'Unauthorized or site not found') + + if request.method == 'GET': + email_domains = EmailDomains.objects.filter(domainOwner=website) + emails = EUsers.objects.filter(emailOwner__in=email_domains) + email_list = [{'email': e.email, 'domain': e.emailOwner.domain, 'diskUsage': e.DiskUsage} for e in emails] + return _json(1, 'None', emails=email_list) + + elif request.method == 'POST': + try: + data = json.loads(request.body) + from plogical.mailUtilities import mailUtilities + mailUtilities.createEmailAccount(data['domain'], data['username'], data['password']) + return _json(1, 'None') + except BaseException as msg: + return _json(0, str(msg)) + + elif request.method == 'DELETE': + try: + data = json.loads(request.body) + from plogical.mailUtilities import mailUtilities + mailUtilities.deleteEmailAccount(data['email']) + return _json(1, 'None') + except BaseException as msg: + return _json(0, str(msg)) + + return _json(0, 'Invalid method') + + +def api_ftp(request, site_id): + try: + userID, admin, currentACL = _auth(request) + except KeyError: + return _json(0, 'Not logged in') + + website = _get_site(site_id, admin, currentACL) + if not website: + return _json(0, 'Unauthorized or site not found') + + if request.method == 'GET': + accounts = FTPUsers.objects.filter(domain=website) + ftp_list = [{'id': a.pk, 'user': a.user, 'dir': a.dir, 'quota': a.quotasize, 'status': a.status} for a in accounts] + return _json(1, 'None', ftp_accounts=ftp_list) + + elif request.method == 'POST': + try: + data = json.loads(request.body) + from plogical.ftpUtilities import ftpUtilities + ftpUtilities.createFTPAccount(data['ftpDomain'], data['ftpUser'], data['ftpPassword']) + return _json(1, 'None') + except BaseException as msg: + return _json(0, str(msg)) + + elif request.method == 'DELETE': + try: + data = json.loads(request.body) + ftp_user = FTPUsers.objects.get(pk=data['id'], domain=website) + from plogical.ftpUtilities import ftpUtilities + ftpUtilities.deleteFTPAccount(ftp_user.user) + ftp_user.delete() + return _json(1, 'None') + except BaseException as msg: + return _json(0, str(msg)) + + return _json(0, 'Invalid method') + + +def api_dns(request, site_id): + try: + userID, admin, currentACL = _auth(request) + except KeyError: + return _json(0, 'Not logged in') + + website = _get_site(site_id, admin, currentACL) + if not website: + return _json(0, 'Unauthorized or site not found') + + if request.method == 'GET': + try: + dns_domain = DNSDomains.objects.get(name=website.domain) + records = DNSRecords.objects.filter(domainOwner=dns_domain) + rec_list = [{'id': r.pk, 'name': r.name, 'type': r.type, 'content': r.content, 'ttl': r.ttl, 'prio': r.prio} for r in records] + return _json(1, 'None', records=rec_list) + except DNSDomains.DoesNotExist: + return _json(1, 'None', records=[]) + + elif request.method == 'POST': + try: + data = json.loads(request.body) + from plogical.dnsUtilities import dnsUtilities + dnsUtilities.createDNSRecord(website.domain, data['name'], data['type'], data['content'], data.get('prio', 0), data.get('ttl', 3600)) + return _json(1, 'None') + except BaseException as msg: + return _json(0, str(msg)) + + elif request.method == 'DELETE': + try: + data = json.loads(request.body) + record = DNSRecords.objects.get(pk=data['id']) + record.delete() + return _json(1, 'None') + except BaseException as msg: + return _json(0, str(msg)) + + return _json(0, 'Invalid method') + + +def api_ssl(request, site_id): + try: + userID, admin, currentACL = _auth(request) + except KeyError: + return _json(0, 'Not logged in') + + website = _get_site(site_id, admin, currentACL) + if not website: + return _json(0, 'Unauthorized or site not found') + + if request.method == 'POST': + try: + data = json.loads(request.body) + action = data.get('action', 'issue') + if action == 'issue': + from plogical.sslUtilities import sslUtilities + sslUtilities.issueSSL(website.domain) + return _json(1, 'None') + elif action == 'upload': + return _json(0, 'Upload SSL not yet implemented in v2 API') + return _json(0, 'Unknown action') + except BaseException as msg: + return _json(0, str(msg)) + + return _json(0, 'Invalid method') + + +def api_backup(request, site_id): + try: + userID, admin, currentACL = _auth(request) + except KeyError: + return _json(0, 'Not logged in') + + website = _get_site(site_id, admin, currentACL) + if not website: + return _json(0, 'Unauthorized or site not found') + + if request.method == 'GET': + backups = Backups.objects.filter(website=website).order_by('-id') + backup_list = [{'id': b.pk, 'fileName': b.fileName, 'date': b.date, 'size': b.size, 'status': b.status} for b in backups] + return _json(1, 'None', backups=backup_list) + + elif request.method == 'POST': + try: + from plogical.backupUtilities import backupUtilities + backupUtilities.createBackup(website.domain) + return _json(1, 'None') + except BaseException as msg: + return _json(0, str(msg)) + + elif request.method == 'DELETE': + try: + data = json.loads(request.body) + backup = Backups.objects.get(pk=data['id'], website=website) + from plogical.backupUtilities import backupUtilities + backupUtilities.deleteBackup(backup.fileName) + backup.delete() + return _json(1, 'None') + except BaseException as msg: + return _json(0, str(msg)) + + return _json(0, 'Invalid method') + + +def api_logs(request, site_id): + try: + userID, admin, currentACL = _auth(request) + except KeyError: + return _json(0, 'Not logged in') + + website = _get_site(site_id, admin, currentACL) + if not website: + return _json(0, 'Unauthorized or site not found') + + if request.method == 'GET': + import os + log_type = request.GET.get('type', 'access') + lines = int(request.GET.get('lines', 50)) + if lines > 1000: + lines = 1000 + + if log_type == 'access': + log_path = '/home/%s/logs/%s.access_log' % (website.domain, website.domain) + else: + log_path = '/home/%s/logs/%s.error_log' % (website.domain, website.domain) + + log_lines = [] + if os.path.exists(log_path): + try: + from plogical.processUtilities import ProcessUtilities + result = ProcessUtilities.outputExecutioner('tail -n %d %s' % (lines, log_path)) + log_lines = result.strip().split('\n') if result.strip() else [] + except: + pass + + return _json(1, 'None', logs=log_lines, log_type=log_type) + + return _json(0, 'Invalid method') + + +def api_config(request, site_id): + try: + userID, admin, currentACL = _auth(request) + except KeyError: + return _json(0, 'Not logged in') + + website = _get_site(site_id, admin, currentACL) + if not website: + return _json(0, 'Unauthorized or site not found') + + if request.method == 'GET': + config_type = request.GET.get('type', 'vhost') + import os + content = '' + if config_type == 'vhost': + vhost_path = '/usr/local/lsws/conf/vhosts/%s/vhconf.conf' % website.domain + if os.path.exists(vhost_path): + with open(vhost_path, 'r') as f: + content = f.read() + elif config_type == 'rewrite': + htaccess_path = '/home/%s/public_html/.htaccess' % website.domain + if os.path.exists(htaccess_path): + with open(htaccess_path, 'r') as f: + content = f.read() + return _json(1, 'None', content=content, config_type=config_type) + + elif request.method == 'POST': + try: + data = json.loads(request.body) + config_type = data.get('type', 'rewrite') + content = data.get('content', '') + if config_type == 'rewrite': + htaccess_path = '/home/%s/public_html/.htaccess' % website.domain + with open(htaccess_path, 'w') as f: + f.write(content) + return _json(1, 'None') + elif config_type == 'php': + from plogical.virtualHostUtilities import virtualHostUtilities + virtualHostUtilities.changePHP(website.domain, data['phpVersion']) + website.phpSelection = data['phpVersion'] + website.save() + return _json(1, 'None') + return _json(0, 'Unknown config type') + except BaseException as msg: + return _json(0, str(msg)) + + return _json(0, 'Invalid method') + + +def api_domains(request, site_id): + try: + userID, admin, currentACL = _auth(request) + except KeyError: + return _json(0, 'Not logged in') + + website = _get_site(site_id, admin, currentACL) + if not website: + return _json(0, 'Unauthorized or site not found') + + if request.method == 'GET': + children = ChildDomains.objects.filter(master=website) + domain_list = [{'id': c.pk, 'domain': c.domain, 'path': c.path, 'ssl': c.ssl, 'php': c.phpSelection, 'isAlias': c.alais} for c in children] + return _json(1, 'None', domains=domain_list) + + elif request.method == 'POST': + try: + data = json.loads(request.body) + from plogical.childDomain import childDomain + childDomain.createChildDomain(website.domain, data['domain'], data.get('path', ''), data.get('php', website.phpSelection), data.get('ssl', 0), data.get('isAlias', 0)) + return _json(1, 'None') + except BaseException as msg: + return _json(0, str(msg)) + + elif request.method == 'DELETE': + try: + data = json.loads(request.body) + child = ChildDomains.objects.get(pk=data['id'], master=website) + from plogical.childDomain import childDomain + childDomain.deleteChildDomain(child.domain) + return _json(1, 'None') + except BaseException as msg: + return _json(0, str(msg)) + + return _json(0, 'Invalid method') + + +def api_security(request, site_id): + try: + userID, admin, currentACL = _auth(request) + except KeyError: + return _json(0, 'Not logged in') + + website = _get_site(site_id, admin, currentACL) + if not website: + return _json(0, 'Unauthorized or site not found') + + if request.method == 'GET': + try: + config = json.loads(website.config) if website.config else {} + except: + config = {} + open_basedir = config.get('openBasedir', 1) + return _json(1, 'None', openBasedir=open_basedir) + + elif request.method == 'POST': + try: + data = json.loads(request.body) + action = data.get('action', '') + if action == 'toggleOpenBasedir': + from plogical.virtualHostUtilities import virtualHostUtilities + virtualHostUtilities.changeOpenBasedir(website.domain, data.get('value', 1)) + return _json(1, 'None') + return _json(0, 'Unknown action') + except BaseException as msg: + return _json(0, str(msg)) + + return _json(0, 'Invalid method')