mirror of
https://github.com/usmannasir/cyberpanel.git
synced 2026-02-24 23:40:45 +01:00
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
This commit is contained in:
@@ -75,6 +75,7 @@ INSTALLED_APPS = [
|
||||
'CLManager',
|
||||
'IncBackups',
|
||||
'aiScanner',
|
||||
'panelv2',
|
||||
# 'WebTerminal'
|
||||
]
|
||||
|
||||
|
||||
@@ -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')),
|
||||
]
|
||||
|
||||
0
panelv2/__init__.py
Normal file
0
panelv2/__init__.py
Normal file
824
panelv2/static/panelv2/css/v2.css
Normal file
824
panelv2/static/panelv2/css/v2.css
Normal file
@@ -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;
|
||||
}
|
||||
}
|
||||
260
panelv2/static/panelv2/js/v2.js
Normal file
260
panelv2/static/panelv2/js/v2.js
Normal file
@@ -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<object>} 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/<id>/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/<id>/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();
|
||||
}
|
||||
};
|
||||
}
|
||||
90
panelv2/templates/panelv2/apps.html
Normal file
90
panelv2/templates/panelv2/apps.html
Normal file
@@ -0,0 +1,90 @@
|
||||
{% extends "panelv2/base.html" %}
|
||||
|
||||
{% block title %}Apps - {{ site_context.domain }} - CyberPanel v2{% endblock %}
|
||||
{% block nav_apps %}active{% endblock %}
|
||||
|
||||
{% block breadcrumb %}
|
||||
<a href="/v2/">Sites</a>
|
||||
<span class="sep">/</span>
|
||||
<a href="/v2/sites/{{ site_context.id }}/">{{ site_context.domain }}</a>
|
||||
<span class="sep">/</span>
|
||||
<span>Apps</span>
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div>
|
||||
<!-- Header -->
|
||||
<div style="margin-bottom:20px;">
|
||||
<h1 style="font-size:22px; font-weight:700; color:var(--text-heading); margin:0;">Applications</h1>
|
||||
</div>
|
||||
|
||||
<!-- WordPress Sites -->
|
||||
<div class="v2-card" style="margin-bottom:20px;">
|
||||
<div class="v2-card-header">
|
||||
<h2><i class="fab fa-wordpress" style="margin-right:8px; color:var(--accent-color);"></i> WordPress Sites</h2>
|
||||
</div>
|
||||
<div class="v2-card-body no-pad">
|
||||
{% if wp_sites %}
|
||||
<div class="v2-table-wrap">
|
||||
<table class="v2-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Title</th>
|
||||
<th>URL</th>
|
||||
<th>Path</th>
|
||||
<th>Auto Updates</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for wp in wp_sites %}
|
||||
<tr>
|
||||
<td><span style="font-weight:600;">{{ wp.title }}</span></td>
|
||||
<td>
|
||||
<a href="{{ wp.FinalURL }}" target="_blank" rel="noopener" style="color:var(--accent-color); text-decoration:none;">
|
||||
{{ wp.FinalURL }} <i class="fas fa-external-link-alt" style="font-size:10px;"></i>
|
||||
</a>
|
||||
</td>
|
||||
<td style="font-size:12px;">{{ wp.path }}</td>
|
||||
<td>
|
||||
<span class="v2-badge {% if wp.AutoUpdates == 'Enabled' %}v2-badge-success{% else %}v2-badge-warning{% endif %}">
|
||||
{{ wp.AutoUpdates }}
|
||||
</span>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="v2-empty">
|
||||
<i class="fab fa-wordpress"></i>
|
||||
<p>No WordPress installations found for this site.</p>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- App Installers (links to existing v1 pages) -->
|
||||
<div class="v2-card">
|
||||
<div class="v2-card-header">
|
||||
<h2>Install Applications</h2>
|
||||
</div>
|
||||
<div class="v2-card-body">
|
||||
<div class="v2-feature-grid" style="grid-template-columns:repeat(auto-fill, minmax(160px, 1fr));">
|
||||
<a href="/websites/{{ website.domain }}/wordpress" class="v2-feature-card" style="padding:20px 14px;">
|
||||
<i class="fab fa-wordpress" style="font-size:28px; color:#21759b;"></i>
|
||||
<div class="feature-name">WordPress</div>
|
||||
</a>
|
||||
<a href="/websites/{{ website.domain }}/joomla" class="v2-feature-card" style="padding:20px 14px;">
|
||||
<i class="fab fa-joomla" style="font-size:28px; color:#5091cd;"></i>
|
||||
<div class="feature-name">Joomla</div>
|
||||
</a>
|
||||
<a href="/websites/{{ website.domain }}/git" class="v2-feature-card" style="padding:20px 14px;">
|
||||
<i class="fab fa-git-alt" style="font-size:28px; color:#f05032;"></i>
|
||||
<div class="feature-name">Git</div>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
79
panelv2/templates/panelv2/backup.html
Normal file
79
panelv2/templates/panelv2/backup.html
Normal file
@@ -0,0 +1,79 @@
|
||||
{% extends "panelv2/base.html" %}
|
||||
|
||||
{% block title %}Backup - {{ site_context.domain }} - CyberPanel v2{% endblock %}
|
||||
{% block nav_backup %}active{% endblock %}
|
||||
|
||||
{% block breadcrumb %}
|
||||
<a href="/v2/">Sites</a>
|
||||
<span class="sep">/</span>
|
||||
<a href="/v2/sites/{{ site_context.id }}/">{{ site_context.domain }}</a>
|
||||
<span class="sep">/</span>
|
||||
<span>Backup</span>
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div x-data="crudComponent('/v2/api/sites/{{ site_context.id }}/backup', 'backups')">
|
||||
<!-- Header -->
|
||||
<div style="display:flex; align-items:center; justify-content:space-between; margin-bottom:20px; flex-wrap:wrap; gap:12px;">
|
||||
<h1 style="font-size:22px; font-weight:700; color:var(--text-heading); margin:0;">Backups</h1>
|
||||
<button class="v2-btn v2-btn-primary" @click="create({})" :disabled="loading">
|
||||
<i class="fas fa-plus"></i> Create Backup
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Alerts -->
|
||||
<template x-if="error">
|
||||
<div class="v2-alert v2-alert-danger"><i class="fas fa-exclamation-circle"></i> <span x-text="error"></span></div>
|
||||
</template>
|
||||
<template x-if="success">
|
||||
<div class="v2-alert v2-alert-success"><i class="fas fa-check-circle"></i> <span x-text="success"></span></div>
|
||||
</template>
|
||||
|
||||
<!-- Table -->
|
||||
<div class="v2-card">
|
||||
<div class="v2-card-body no-pad">
|
||||
<template x-if="loading">
|
||||
<div style="padding:32px; text-align:center;"><div class="v2-spinner"></div></div>
|
||||
</template>
|
||||
<template x-if="!loading && items.length > 0">
|
||||
<div class="v2-table-wrap">
|
||||
<table class="v2-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>File Name</th>
|
||||
<th>Date</th>
|
||||
<th>Size</th>
|
||||
<th>Status</th>
|
||||
<th>Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<template x-for="b in items" :key="b.id">
|
||||
<tr>
|
||||
<td><span style="font-weight:600;" x-text="b.fileName"></span></td>
|
||||
<td x-text="b.date"></td>
|
||||
<td x-text="b.size"></td>
|
||||
<td>
|
||||
<span class="v2-badge" :class="b.status === 1 ? 'v2-badge-success' : 'v2-badge-warning'" x-text="b.status === 1 ? 'Complete' : 'Pending'"></span>
|
||||
</td>
|
||||
<td>
|
||||
<button class="v2-btn v2-btn-danger v2-btn-sm" @click="remove({id: b.id})">
|
||||
<i class="fas fa-trash"></i> Delete
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
</template>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</template>
|
||||
<template x-if="!loading && items.length === 0">
|
||||
<div class="v2-empty">
|
||||
<i class="fas fa-archive"></i>
|
||||
<p>No backups yet. Create one to protect your site.</p>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
146
panelv2/templates/panelv2/base.html
Normal file
146
panelv2/templates/panelv2/base.html
Normal file
@@ -0,0 +1,146 @@
|
||||
{% load static %}
|
||||
{% load v2_tags %}
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>{% block title %}CyberPanel v2{% endblock %}</title>
|
||||
<link rel="icon" type="image/x-icon" href="{% static 'baseTemplate/assets/finalBase/favicon.png' %}">
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
|
||||
<link rel="stylesheet" href="{% static 'panelv2/css/v2.css' %}">
|
||||
<style>
|
||||
:root {
|
||||
--bg-primary: #f0f0ff;
|
||||
--bg-secondary: white;
|
||||
--bg-sidebar: #f3f1ff;
|
||||
--bg-sidebar-item: white;
|
||||
--bg-hover: #e8e6ff;
|
||||
--text-primary: #2f3640;
|
||||
--text-secondary: #64748b;
|
||||
--text-heading: #1e293b;
|
||||
--border-color: #e8e9ff;
|
||||
--shadow-color: rgba(0,0,0,0.05);
|
||||
--accent-color: #5856d6;
|
||||
--accent-hover: #4644c0;
|
||||
--danger-color: #ef4444;
|
||||
--success-color: #10b981;
|
||||
}
|
||||
[data-theme="dark"] {
|
||||
--bg-primary: #0f0f23;
|
||||
--bg-secondary: #1a1a3e;
|
||||
--bg-sidebar: #16162e;
|
||||
--bg-sidebar-item: #1e1e42;
|
||||
--bg-hover: #252550;
|
||||
--text-primary: #e4e4e7;
|
||||
--text-secondary: #9ca3af;
|
||||
--text-heading: #f3f4f6;
|
||||
--border-color: #2a2a5e;
|
||||
--shadow-color: rgba(0,0,0,0.3);
|
||||
--accent-color: #7c7ff3;
|
||||
--accent-hover: #6b6ee8;
|
||||
--danger-color: #f87171;
|
||||
--success-color: #34d399;
|
||||
}
|
||||
</style>
|
||||
{% block extra_css %}{% endblock %}
|
||||
</head>
|
||||
<body class="v2-app" x-data x-init="$store.theme.init()">
|
||||
|
||||
<!-- Sidebar -->
|
||||
<aside class="v2-sidebar" :class="{ 'open': $store.sidebar.open }">
|
||||
<div class="v2-sidebar-brand">
|
||||
<img src="{% static 'baseTemplate/assets/finalBase/favicon.png' %}" alt="CP">
|
||||
<span>CyberPanel</span>
|
||||
<span class="v2-version">v2</span>
|
||||
</div>
|
||||
|
||||
{% if site_context %}
|
||||
<!-- Site context mode -->
|
||||
<div class="v2-site-header">
|
||||
<a href="/v2/" class="back-link"><i class="fas fa-arrow-left"></i> All Sites</a>
|
||||
<div class="site-domain">{{ site_context.domain }}</div>
|
||||
</div>
|
||||
<div class="v2-sidebar-section">
|
||||
<ul class="v2-sidebar-nav">
|
||||
<li><a href="/v2/sites/{{ site_context.id }}/" class="{% block nav_overview %}{% endblock %}"><i class="fas fa-tachometer-alt"></i> Overview</a></li>
|
||||
<li><a href="/v2/sites/{{ site_context.id }}/domains" class="{% block nav_domains %}{% endblock %}"><i class="fas fa-sitemap"></i> Domains</a></li>
|
||||
<li><a href="/v2/sites/{{ site_context.id }}/databases" class="{% block nav_databases %}{% endblock %}"><i class="fas fa-database"></i> Databases</a></li>
|
||||
<li><a href="/v2/sites/{{ site_context.id }}/email" class="{% block nav_email %}{% endblock %}"><i class="fas fa-envelope"></i> Email</a></li>
|
||||
<li><a href="/v2/sites/{{ site_context.id }}/ftp" class="{% block nav_ftp %}{% endblock %}"><i class="fas fa-upload"></i> FTP</a></li>
|
||||
<li><a href="/v2/sites/{{ site_context.id }}/dns" class="{% block nav_dns %}{% endblock %}"><i class="fas fa-globe"></i> DNS</a></li>
|
||||
<li><a href="/v2/sites/{{ site_context.id }}/ssl" class="{% block nav_ssl %}{% endblock %}"><i class="fas fa-lock"></i> SSL</a></li>
|
||||
<li><a href="/v2/sites/{{ site_context.id }}/backup" class="{% block nav_backup %}{% endblock %}"><i class="fas fa-archive"></i> Backup</a></li>
|
||||
<li><a href="/v2/sites/{{ site_context.id }}/files" class="{% block nav_files %}{% endblock %}"><i class="fas fa-folder-open"></i> Files</a></li>
|
||||
<li><a href="/v2/sites/{{ site_context.id }}/logs" class="{% block nav_logs %}{% endblock %}"><i class="fas fa-file-alt"></i> Logs</a></li>
|
||||
<li><a href="/v2/sites/{{ site_context.id }}/config" class="{% block nav_config %}{% endblock %}"><i class="fas fa-cog"></i> Config</a></li>
|
||||
<li><a href="/v2/sites/{{ site_context.id }}/apps" class="{% block nav_apps %}{% endblock %}"><i class="fas fa-th-large"></i> Apps</a></li>
|
||||
<li><a href="/v2/sites/{{ site_context.id }}/security" class="{% block nav_security %}{% endblock %}"><i class="fas fa-shield-alt"></i> Security</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
{% else %}
|
||||
<!-- Default mode (no site context) -->
|
||||
<div class="v2-sidebar-section">
|
||||
<div class="v2-sidebar-label">Sites</div>
|
||||
<ul class="v2-sidebar-nav">
|
||||
<li><a href="/v2/" class="{% block nav_site_list %}{% endblock %}"><i class="fas fa-list"></i> All Sites</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
{% if admin %}
|
||||
<div class="v2-sidebar-section">
|
||||
<div class="v2-sidebar-label">Server</div>
|
||||
<ul class="v2-sidebar-nav">
|
||||
<li><a href="/v2/server/" class="{% block nav_server %}{% endblock %}"><i class="fas fa-server"></i> Server Management</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
|
||||
<div class="v2-sidebar-footer">
|
||||
<ul class="v2-sidebar-nav">
|
||||
<li><a href="/"><i class="fas fa-arrow-left"></i> Classic Panel</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
<!-- Header -->
|
||||
<header class="v2-header">
|
||||
<div class="v2-header-left">
|
||||
<button class="v2-menu-toggle" @click="$store.sidebar.toggle()">
|
||||
<i class="fas fa-bars"></i>
|
||||
</button>
|
||||
<div class="v2-breadcrumb">
|
||||
{% block breadcrumb %}
|
||||
<a href="/v2/">Sites</a>
|
||||
{% endblock %}
|
||||
</div>
|
||||
</div>
|
||||
<div class="v2-header-right">
|
||||
<button class="v2-btn-icon" @click="$store.theme.toggle()" title="Toggle theme">
|
||||
<i class="fas" :class="$store.theme.dark ? 'fa-sun' : 'fa-moon'"></i>
|
||||
</button>
|
||||
{% if fullName %}
|
||||
<div class="v2-user-badge">
|
||||
<i class="fas fa-user-circle"></i>
|
||||
<span>{{ fullName }}</span>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<!-- Main Content -->
|
||||
<main class="v2-main">
|
||||
<div class="v2-content">
|
||||
{% block content %}{% endblock %}
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<!-- Overlay for mobile sidebar -->
|
||||
<div x-show="$store.sidebar.open" @click="$store.sidebar.close()" style="position:fixed;inset:0;background:rgba(0,0,0,0.3);z-index:150;display:none;" x-transition.opacity :style="$store.sidebar.open && 'display:block'"></div>
|
||||
|
||||
<!-- Alpine.js (CDN) -->
|
||||
<script defer src="https://cdn.jsdelivr.net/npm/alpinejs@3.x.x/dist/cdn.min.js"></script>
|
||||
<script src="{% static 'panelv2/js/v2.js' %}"></script>
|
||||
{% block extra_js %}{% endblock %}
|
||||
</body>
|
||||
</html>
|
||||
104
panelv2/templates/panelv2/config.html
Normal file
104
panelv2/templates/panelv2/config.html
Normal file
@@ -0,0 +1,104 @@
|
||||
{% extends "panelv2/base.html" %}
|
||||
|
||||
{% block title %}Config - {{ site_context.domain }} - CyberPanel v2{% endblock %}
|
||||
{% block nav_config %}active{% endblock %}
|
||||
|
||||
{% block breadcrumb %}
|
||||
<a href="/v2/">Sites</a>
|
||||
<span class="sep">/</span>
|
||||
<a href="/v2/sites/{{ site_context.id }}/">{{ site_context.domain }}</a>
|
||||
<span class="sep">/</span>
|
||||
<span>Config</span>
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div x-data="configEditorComponent('/v2/api/sites/{{ site_context.id }}/config')">
|
||||
<!-- Header -->
|
||||
<div style="display:flex; align-items:center; justify-content:space-between; margin-bottom:20px; flex-wrap:wrap; gap:12px;">
|
||||
<h1 style="font-size:22px; font-weight:700; color:var(--text-heading); margin:0;">Site Configuration</h1>
|
||||
</div>
|
||||
|
||||
<!-- Alerts -->
|
||||
<template x-if="error">
|
||||
<div class="v2-alert v2-alert-danger"><i class="fas fa-exclamation-circle"></i> <span x-text="error"></span></div>
|
||||
</template>
|
||||
<template x-if="success">
|
||||
<div class="v2-alert v2-alert-success"><i class="fas fa-check-circle"></i> <span x-text="success"></span></div>
|
||||
</template>
|
||||
|
||||
<!-- PHP Version Card -->
|
||||
<div class="v2-card" style="margin-bottom:20px;">
|
||||
<div class="v2-card-header">
|
||||
<h2>PHP Version</h2>
|
||||
</div>
|
||||
<div class="v2-card-body" x-data="{ phpVersion: '{{ website.phpSelection }}', changingPHP: false, phpError: '', phpSuccess: '' }">
|
||||
<div style="display:flex; align-items:center; gap:12px; flex-wrap:wrap;">
|
||||
<div style="font-size:13px; color:var(--text-secondary);">Current PHP:</div>
|
||||
<span class="v2-badge v2-badge-info">{{ website.phpSelection }}</span>
|
||||
<select class="v2-select" style="width:auto; min-width:150px;" x-model="phpVersion">
|
||||
<option value="PHP 7.0">PHP 7.0</option>
|
||||
<option value="PHP 7.1">PHP 7.1</option>
|
||||
<option value="PHP 7.2">PHP 7.2</option>
|
||||
<option value="PHP 7.3">PHP 7.3</option>
|
||||
<option value="PHP 7.4">PHP 7.4</option>
|
||||
<option value="PHP 8.0">PHP 8.0</option>
|
||||
<option value="PHP 8.1">PHP 8.1</option>
|
||||
<option value="PHP 8.2">PHP 8.2</option>
|
||||
<option value="PHP 8.3">PHP 8.3</option>
|
||||
<option value="PHP 8.4">PHP 8.4</option>
|
||||
</select>
|
||||
<button class="v2-btn v2-btn-primary v2-btn-sm" :disabled="changingPHP"
|
||||
@click="
|
||||
changingPHP = true; phpError = ''; phpSuccess = '';
|
||||
v2Fetch('/v2/api/sites/{{ site_context.id }}/config', {method: 'POST', body: {type: 'php', phpVersion: phpVersion}})
|
||||
.then(d => {
|
||||
if (d.status === 1) { phpSuccess = 'PHP version changed. Reloading...'; setTimeout(() => location.reload(), 1500); }
|
||||
else { phpError = d.error_message; }
|
||||
})
|
||||
.catch(() => { phpError = 'Request failed'; })
|
||||
.finally(() => { changingPHP = false; })
|
||||
">
|
||||
Change PHP
|
||||
</button>
|
||||
</div>
|
||||
<template x-if="phpError"><div class="v2-alert v2-alert-danger" style="margin-top:12px;"><span x-text="phpError"></span></div></template>
|
||||
<template x-if="phpSuccess"><div class="v2-alert v2-alert-success" style="margin-top:12px;"><span x-text="phpSuccess"></span></div></template>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Config Editor -->
|
||||
<div class="v2-card">
|
||||
<div class="v2-card-header">
|
||||
<h2>Configuration Files</h2>
|
||||
<div style="display:flex; gap:8px;">
|
||||
<select class="v2-select" style="width:auto;" x-model="configType" @change="load()">
|
||||
<option value="vhost">vHost Config</option>
|
||||
<option value="rewrite">Rewrite Rules (.htaccess)</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="v2-card-body">
|
||||
<template x-if="loading">
|
||||
<div style="padding:32px; text-align:center;"><div class="v2-spinner"></div></div>
|
||||
</template>
|
||||
<template x-if="!loading">
|
||||
<div>
|
||||
<textarea class="v2-code-editor" x-model="content" :readonly="configType === 'vhost'"></textarea>
|
||||
<div style="display:flex; gap:8px; margin-top:12px; align-items:center;">
|
||||
<template x-if="configType !== 'vhost'">
|
||||
<button class="v2-btn v2-btn-primary" @click="save()">
|
||||
<i class="fas fa-save"></i> Save Changes
|
||||
</button>
|
||||
</template>
|
||||
<template x-if="configType === 'vhost'">
|
||||
<div class="v2-alert v2-alert-info" style="margin:0;">
|
||||
<i class="fas fa-info-circle"></i> vHost config is read-only. Edit through the server configuration tools.
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
100
panelv2/templates/panelv2/databases.html
Normal file
100
panelv2/templates/panelv2/databases.html
Normal file
@@ -0,0 +1,100 @@
|
||||
{% extends "panelv2/base.html" %}
|
||||
|
||||
{% block title %}Databases - {{ site_context.domain }} - CyberPanel v2{% endblock %}
|
||||
{% block nav_databases %}active{% endblock %}
|
||||
|
||||
{% block breadcrumb %}
|
||||
<a href="/v2/">Sites</a>
|
||||
<span class="sep">/</span>
|
||||
<a href="/v2/sites/{{ site_context.id }}/">{{ site_context.domain }}</a>
|
||||
<span class="sep">/</span>
|
||||
<span>Databases</span>
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div x-data="crudComponent('/v2/api/sites/{{ site_context.id }}/databases', 'databases')">
|
||||
<!-- Header -->
|
||||
<div style="display:flex; align-items:center; justify-content:space-between; margin-bottom:20px; flex-wrap:wrap; gap:12px;">
|
||||
<h1 style="font-size:22px; font-weight:700; color:var(--text-heading); margin:0;">Databases</h1>
|
||||
<button class="v2-btn v2-btn-primary" @click="showForm = !showForm; clearMessages()">
|
||||
<i class="fas fa-plus"></i> Create Database
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Alerts -->
|
||||
<template x-if="error">
|
||||
<div class="v2-alert v2-alert-danger"><i class="fas fa-exclamation-circle"></i> <span x-text="error"></span></div>
|
||||
</template>
|
||||
<template x-if="success">
|
||||
<div class="v2-alert v2-alert-success"><i class="fas fa-check-circle"></i> <span x-text="success"></span></div>
|
||||
</template>
|
||||
|
||||
<!-- Create form -->
|
||||
<template x-if="showForm">
|
||||
<div class="v2-card" style="margin-bottom:20px;">
|
||||
<div class="v2-card-header"><h2>New Database</h2></div>
|
||||
<div class="v2-card-body">
|
||||
<div style="display:grid; grid-template-columns:repeat(auto-fill, minmax(220px, 1fr)); gap:16px;">
|
||||
<div class="v2-form-group">
|
||||
<label>Database Name</label>
|
||||
<input type="text" class="v2-input" x-model="formData.dbName" placeholder="mydb">
|
||||
</div>
|
||||
<div class="v2-form-group">
|
||||
<label>Database User</label>
|
||||
<input type="text" class="v2-input" x-model="formData.dbUser" placeholder="myuser">
|
||||
</div>
|
||||
<div class="v2-form-group">
|
||||
<label>Password</label>
|
||||
<input type="password" class="v2-input" x-model="formData.dbPassword" placeholder="Strong password">
|
||||
</div>
|
||||
</div>
|
||||
<div style="display:flex; gap:8px; margin-top:8px;">
|
||||
<button class="v2-btn v2-btn-primary" @click="create(formData)">Create</button>
|
||||
<button class="v2-btn v2-btn-outline" @click="showForm = false">Cancel</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- Table -->
|
||||
<div class="v2-card">
|
||||
<div class="v2-card-body no-pad">
|
||||
<template x-if="loading">
|
||||
<div style="padding:32px; text-align:center;"><div class="v2-spinner"></div></div>
|
||||
</template>
|
||||
<template x-if="!loading && items.length > 0">
|
||||
<div class="v2-table-wrap">
|
||||
<table class="v2-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Database Name</th>
|
||||
<th>Database User</th>
|
||||
<th>Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<template x-for="db in items" :key="db.id">
|
||||
<tr>
|
||||
<td><span style="font-weight:600;" x-text="db.name"></span></td>
|
||||
<td x-text="db.user"></td>
|
||||
<td>
|
||||
<button class="v2-btn v2-btn-danger v2-btn-sm" @click="remove({id: db.id})">
|
||||
<i class="fas fa-trash"></i> Delete
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
</template>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</template>
|
||||
<template x-if="!loading && items.length === 0">
|
||||
<div class="v2-empty">
|
||||
<i class="fas fa-database"></i>
|
||||
<p>No databases yet. Create one to get started.</p>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
123
panelv2/templates/panelv2/dns.html
Normal file
123
panelv2/templates/panelv2/dns.html
Normal file
@@ -0,0 +1,123 @@
|
||||
{% extends "panelv2/base.html" %}
|
||||
|
||||
{% block title %}DNS - {{ site_context.domain }} - CyberPanel v2{% endblock %}
|
||||
{% block nav_dns %}active{% endblock %}
|
||||
|
||||
{% block breadcrumb %}
|
||||
<a href="/v2/">Sites</a>
|
||||
<span class="sep">/</span>
|
||||
<a href="/v2/sites/{{ site_context.id }}/">{{ site_context.domain }}</a>
|
||||
<span class="sep">/</span>
|
||||
<span>DNS</span>
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div x-data="crudComponent('/v2/api/sites/{{ site_context.id }}/dns', 'records')">
|
||||
<!-- Header -->
|
||||
<div style="display:flex; align-items:center; justify-content:space-between; margin-bottom:20px; flex-wrap:wrap; gap:12px;">
|
||||
<h1 style="font-size:22px; font-weight:700; color:var(--text-heading); margin:0;">DNS Records</h1>
|
||||
<button class="v2-btn v2-btn-primary" @click="showForm = !showForm; clearMessages()">
|
||||
<i class="fas fa-plus"></i> Add Record
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Alerts -->
|
||||
<template x-if="error">
|
||||
<div class="v2-alert v2-alert-danger"><i class="fas fa-exclamation-circle"></i> <span x-text="error"></span></div>
|
||||
</template>
|
||||
<template x-if="success">
|
||||
<div class="v2-alert v2-alert-success"><i class="fas fa-check-circle"></i> <span x-text="success"></span></div>
|
||||
</template>
|
||||
|
||||
<!-- Create form -->
|
||||
<template x-if="showForm">
|
||||
<div class="v2-card" style="margin-bottom:20px;">
|
||||
<div class="v2-card-header"><h2>Add DNS Record</h2></div>
|
||||
<div class="v2-card-body">
|
||||
<div style="display:grid; grid-template-columns:repeat(auto-fill, minmax(180px, 1fr)); gap:16px;">
|
||||
<div class="v2-form-group">
|
||||
<label>Name</label>
|
||||
<input type="text" class="v2-input" x-model="formData.name" placeholder="{{ site_context.domain }}">
|
||||
</div>
|
||||
<div class="v2-form-group">
|
||||
<label>Type</label>
|
||||
<select class="v2-select" x-model="formData.type">
|
||||
<option value="A">A</option>
|
||||
<option value="AAAA">AAAA</option>
|
||||
<option value="CNAME">CNAME</option>
|
||||
<option value="MX">MX</option>
|
||||
<option value="TXT">TXT</option>
|
||||
<option value="NS">NS</option>
|
||||
<option value="SRV">SRV</option>
|
||||
<option value="CAA">CAA</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="v2-form-group">
|
||||
<label>Content</label>
|
||||
<input type="text" class="v2-input" x-model="formData.content" placeholder="Value">
|
||||
</div>
|
||||
<div class="v2-form-group">
|
||||
<label>Priority</label>
|
||||
<input type="number" class="v2-input" x-model="formData.prio" placeholder="0">
|
||||
</div>
|
||||
<div class="v2-form-group">
|
||||
<label>TTL</label>
|
||||
<input type="number" class="v2-input" x-model="formData.ttl" placeholder="3600" value="3600">
|
||||
</div>
|
||||
</div>
|
||||
<div style="display:flex; gap:8px; margin-top:8px;">
|
||||
<button class="v2-btn v2-btn-primary" @click="create(formData)">Add Record</button>
|
||||
<button class="v2-btn v2-btn-outline" @click="showForm = false">Cancel</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- Table -->
|
||||
<div class="v2-card">
|
||||
<div class="v2-card-body no-pad">
|
||||
<template x-if="loading">
|
||||
<div style="padding:32px; text-align:center;"><div class="v2-spinner"></div></div>
|
||||
</template>
|
||||
<template x-if="!loading && items.length > 0">
|
||||
<div class="v2-table-wrap">
|
||||
<table class="v2-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Name</th>
|
||||
<th>Type</th>
|
||||
<th>Content</th>
|
||||
<th>TTL</th>
|
||||
<th>Priority</th>
|
||||
<th>Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<template x-for="rec in items" :key="rec.id">
|
||||
<tr>
|
||||
<td style="font-weight:600; word-break:break-all;" x-text="rec.name"></td>
|
||||
<td><span class="v2-badge v2-badge-info" x-text="rec.type"></span></td>
|
||||
<td style="max-width:300px; word-break:break-all;" x-text="rec.content"></td>
|
||||
<td x-text="rec.ttl"></td>
|
||||
<td x-text="rec.prio || '-'"></td>
|
||||
<td>
|
||||
<button class="v2-btn v2-btn-danger v2-btn-sm" @click="remove({id: rec.id})">
|
||||
<i class="fas fa-trash"></i>
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
</template>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</template>
|
||||
<template x-if="!loading && items.length === 0">
|
||||
<div class="v2-empty">
|
||||
<i class="fas fa-globe"></i>
|
||||
<p>No DNS records found for this domain.</p>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
117
panelv2/templates/panelv2/domains.html
Normal file
117
panelv2/templates/panelv2/domains.html
Normal file
@@ -0,0 +1,117 @@
|
||||
{% extends "panelv2/base.html" %}
|
||||
|
||||
{% block title %}Domains - {{ site_context.domain }} - CyberPanel v2{% endblock %}
|
||||
{% block nav_domains %}active{% endblock %}
|
||||
|
||||
{% block breadcrumb %}
|
||||
<a href="/v2/">Sites</a>
|
||||
<span class="sep">/</span>
|
||||
<a href="/v2/sites/{{ site_context.id }}/">{{ site_context.domain }}</a>
|
||||
<span class="sep">/</span>
|
||||
<span>Domains</span>
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div x-data="crudComponent('/v2/api/sites/{{ site_context.id }}/domains', 'domains')">
|
||||
<!-- Header -->
|
||||
<div style="display:flex; align-items:center; justify-content:space-between; margin-bottom:20px; flex-wrap:wrap; gap:12px;">
|
||||
<h1 style="font-size:22px; font-weight:700; color:var(--text-heading); margin:0;">Child Domains & Aliases</h1>
|
||||
<button class="v2-btn v2-btn-primary" @click="showForm = !showForm; clearMessages()">
|
||||
<i class="fas fa-plus"></i> Add Domain
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Alerts -->
|
||||
<template x-if="error">
|
||||
<div class="v2-alert v2-alert-danger"><i class="fas fa-exclamation-circle"></i> <span x-text="error"></span></div>
|
||||
</template>
|
||||
<template x-if="success">
|
||||
<div class="v2-alert v2-alert-success"><i class="fas fa-check-circle"></i> <span x-text="success"></span></div>
|
||||
</template>
|
||||
|
||||
<!-- Create form -->
|
||||
<template x-if="showForm">
|
||||
<div class="v2-card" style="margin-bottom:20px;">
|
||||
<div class="v2-card-header"><h2>Add Child Domain / Alias</h2></div>
|
||||
<div class="v2-card-body">
|
||||
<div style="display:grid; grid-template-columns:repeat(auto-fill, minmax(220px, 1fr)); gap:16px;">
|
||||
<div class="v2-form-group">
|
||||
<label>Domain Name</label>
|
||||
<input type="text" class="v2-input" x-model="formData.domain" placeholder="sub.example.com">
|
||||
</div>
|
||||
<div class="v2-form-group">
|
||||
<label>Path</label>
|
||||
<input type="text" class="v2-input" x-model="formData.path" placeholder="subdirectory">
|
||||
</div>
|
||||
<div class="v2-form-group">
|
||||
<label>PHP Version</label>
|
||||
<input type="text" class="v2-input" x-model="formData.php" placeholder="{{ website.phpSelection }}" value="{{ website.phpSelection }}">
|
||||
</div>
|
||||
<div class="v2-form-group">
|
||||
<label>Type</label>
|
||||
<select class="v2-select" x-model="formData.isAlias">
|
||||
<option value="0">Child Domain</option>
|
||||
<option value="1">Alias Domain</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div style="display:flex; gap:8px; margin-top:8px;">
|
||||
<button class="v2-btn v2-btn-primary" @click="create(formData)">Add</button>
|
||||
<button class="v2-btn v2-btn-outline" @click="showForm = false">Cancel</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- Table -->
|
||||
<div class="v2-card">
|
||||
<div class="v2-card-body no-pad">
|
||||
<template x-if="loading">
|
||||
<div style="padding:32px; text-align:center;"><div class="v2-spinner"></div></div>
|
||||
</template>
|
||||
<template x-if="!loading && items.length > 0">
|
||||
<div class="v2-table-wrap">
|
||||
<table class="v2-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Domain</th>
|
||||
<th>Type</th>
|
||||
<th>Path</th>
|
||||
<th>PHP</th>
|
||||
<th>SSL</th>
|
||||
<th>Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<template x-for="d in items" :key="d.id">
|
||||
<tr>
|
||||
<td><span style="font-weight:600;" x-text="d.domain"></span></td>
|
||||
<td>
|
||||
<span class="v2-badge" :class="d.isAlias == 1 ? 'v2-badge-info' : 'v2-badge-warning'" x-text="d.isAlias == 1 ? 'Alias' : 'Child'"></span>
|
||||
</td>
|
||||
<td x-text="d.path || '/'"></td>
|
||||
<td x-text="d.php"></td>
|
||||
<td>
|
||||
<span class="v2-badge" :class="d.ssl == 1 ? 'v2-badge-success' : 'v2-badge-warning'" x-text="d.ssl == 1 ? 'Active' : 'None'"></span>
|
||||
</td>
|
||||
<td>
|
||||
<button class="v2-btn v2-btn-danger v2-btn-sm" @click="remove({id: d.id})">
|
||||
<i class="fas fa-trash"></i>
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
</template>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</template>
|
||||
<template x-if="!loading && items.length === 0">
|
||||
<div class="v2-empty">
|
||||
<i class="fas fa-sitemap"></i>
|
||||
<p>No child domains or aliases. Add one to extend your site.</p>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
109
panelv2/templates/panelv2/email.html
Normal file
109
panelv2/templates/panelv2/email.html
Normal file
@@ -0,0 +1,109 @@
|
||||
{% extends "panelv2/base.html" %}
|
||||
|
||||
{% block title %}Email - {{ site_context.domain }} - CyberPanel v2{% endblock %}
|
||||
{% block nav_email %}active{% endblock %}
|
||||
|
||||
{% block breadcrumb %}
|
||||
<a href="/v2/">Sites</a>
|
||||
<span class="sep">/</span>
|
||||
<a href="/v2/sites/{{ site_context.id }}/">{{ site_context.domain }}</a>
|
||||
<span class="sep">/</span>
|
||||
<span>Email</span>
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div x-data="crudComponent('/v2/api/sites/{{ site_context.id }}/email', 'emails')">
|
||||
<!-- Header -->
|
||||
<div style="display:flex; align-items:center; justify-content:space-between; margin-bottom:20px; flex-wrap:wrap; gap:12px;">
|
||||
<h1 style="font-size:22px; font-weight:700; color:var(--text-heading); margin:0;">Email Accounts</h1>
|
||||
<button class="v2-btn v2-btn-primary" @click="showForm = !showForm; clearMessages()">
|
||||
<i class="fas fa-plus"></i> Create Account
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Alerts -->
|
||||
<template x-if="error">
|
||||
<div class="v2-alert v2-alert-danger"><i class="fas fa-exclamation-circle"></i> <span x-text="error"></span></div>
|
||||
</template>
|
||||
<template x-if="success">
|
||||
<div class="v2-alert v2-alert-success"><i class="fas fa-check-circle"></i> <span x-text="success"></span></div>
|
||||
</template>
|
||||
|
||||
<!-- Create form -->
|
||||
<template x-if="showForm">
|
||||
<div class="v2-card" style="margin-bottom:20px;">
|
||||
<div class="v2-card-header"><h2>New Email Account</h2></div>
|
||||
<div class="v2-card-body">
|
||||
<div style="display:grid; grid-template-columns:repeat(auto-fill, minmax(220px, 1fr)); gap:16px;">
|
||||
<div class="v2-form-group">
|
||||
<label>Username</label>
|
||||
<input type="text" class="v2-input" x-model="formData.username" placeholder="user">
|
||||
</div>
|
||||
<div class="v2-form-group">
|
||||
<label>Domain</label>
|
||||
<select class="v2-select" x-model="formData.domain">
|
||||
{% for ed in email_domains %}
|
||||
<option value="{{ ed.domain }}">{{ ed.domain }}</option>
|
||||
{% endfor %}
|
||||
{% if not email_domains %}
|
||||
<option value="{{ website.domain }}">{{ website.domain }}</option>
|
||||
{% endif %}
|
||||
</select>
|
||||
</div>
|
||||
<div class="v2-form-group">
|
||||
<label>Password</label>
|
||||
<input type="password" class="v2-input" x-model="formData.password" placeholder="Strong password">
|
||||
</div>
|
||||
</div>
|
||||
<div style="display:flex; gap:8px; margin-top:8px;">
|
||||
<button class="v2-btn v2-btn-primary" @click="create(formData)">Create</button>
|
||||
<button class="v2-btn v2-btn-outline" @click="showForm = false">Cancel</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- Table -->
|
||||
<div class="v2-card">
|
||||
<div class="v2-card-body no-pad">
|
||||
<template x-if="loading">
|
||||
<div style="padding:32px; text-align:center;"><div class="v2-spinner"></div></div>
|
||||
</template>
|
||||
<template x-if="!loading && items.length > 0">
|
||||
<div class="v2-table-wrap">
|
||||
<table class="v2-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Email Address</th>
|
||||
<th>Domain</th>
|
||||
<th>Disk Usage</th>
|
||||
<th>Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<template x-for="em in items" :key="em.email">
|
||||
<tr>
|
||||
<td><span style="font-weight:600;" x-text="em.email"></span></td>
|
||||
<td x-text="em.domain"></td>
|
||||
<td x-text="em.diskUsage || '0'"></td>
|
||||
<td>
|
||||
<button class="v2-btn v2-btn-danger v2-btn-sm" @click="remove({email: em.email})">
|
||||
<i class="fas fa-trash"></i> Delete
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
</template>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</template>
|
||||
<template x-if="!loading && items.length === 0">
|
||||
<div class="v2-empty">
|
||||
<i class="fas fa-envelope"></i>
|
||||
<p>No email accounts yet. Create one to get started.</p>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
49
panelv2/templates/panelv2/files.html
Normal file
49
panelv2/templates/panelv2/files.html
Normal file
@@ -0,0 +1,49 @@
|
||||
{% extends "panelv2/base.html" %}
|
||||
|
||||
{% block title %}Files - {{ site_context.domain }} - CyberPanel v2{% endblock %}
|
||||
{% block nav_files %}active{% endblock %}
|
||||
|
||||
{% block breadcrumb %}
|
||||
<a href="/v2/">Sites</a>
|
||||
<span class="sep">/</span>
|
||||
<a href="/v2/sites/{{ site_context.id }}/">{{ site_context.domain }}</a>
|
||||
<span class="sep">/</span>
|
||||
<span>Files</span>
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div>
|
||||
<!-- Header -->
|
||||
<div style="display:flex; align-items:center; justify-content:space-between; margin-bottom:20px; flex-wrap:wrap; gap:12px;">
|
||||
<h1 style="font-size:22px; font-weight:700; color:var(--text-heading); margin:0;">File Manager</h1>
|
||||
<a href="/filemanager/{{ website.domain }}" class="v2-btn v2-btn-primary" target="_blank">
|
||||
<i class="fas fa-external-link-alt"></i> Open File Manager
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<!-- Info card -->
|
||||
<div class="v2-card">
|
||||
<div class="v2-card-body">
|
||||
<div style="display:flex; align-items:center; gap:16px; flex-wrap:wrap;">
|
||||
<div style="width:60px; height:60px; border-radius:12px; background:rgba(88,86,214,0.1); display:flex; align-items:center; justify-content:center;">
|
||||
<i class="fas fa-folder-open" style="font-size:24px; color:var(--accent-color);"></i>
|
||||
</div>
|
||||
<div>
|
||||
<div style="font-size:15px; font-weight:600; color:var(--text-heading); margin-bottom:4px;">{{ website.domain }}</div>
|
||||
<div style="font-size:13px; color:var(--text-secondary);">Document Root: <code>/home/{{ website.domain }}/public_html</code></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- File manager embed -->
|
||||
<div class="v2-card" style="margin-top:20px;">
|
||||
<div class="v2-card-header">
|
||||
<h2>File Manager</h2>
|
||||
</div>
|
||||
<div class="v2-card-body no-pad">
|
||||
<iframe src="/filemanager/{{ website.domain }}" style="width:100%; height:600px; border:none; border-radius:0 0 10px 10px;"></iframe>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
103
panelv2/templates/panelv2/ftp.html
Normal file
103
panelv2/templates/panelv2/ftp.html
Normal file
@@ -0,0 +1,103 @@
|
||||
{% extends "panelv2/base.html" %}
|
||||
|
||||
{% block title %}FTP - {{ site_context.domain }} - CyberPanel v2{% endblock %}
|
||||
{% block nav_ftp %}active{% endblock %}
|
||||
|
||||
{% block breadcrumb %}
|
||||
<a href="/v2/">Sites</a>
|
||||
<span class="sep">/</span>
|
||||
<a href="/v2/sites/{{ site_context.id }}/">{{ site_context.domain }}</a>
|
||||
<span class="sep">/</span>
|
||||
<span>FTP</span>
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div x-data="crudComponent('/v2/api/sites/{{ site_context.id }}/ftp', 'ftp_accounts')">
|
||||
<!-- Header -->
|
||||
<div style="display:flex; align-items:center; justify-content:space-between; margin-bottom:20px; flex-wrap:wrap; gap:12px;">
|
||||
<h1 style="font-size:22px; font-weight:700; color:var(--text-heading); margin:0;">FTP Accounts</h1>
|
||||
<button class="v2-btn v2-btn-primary" @click="showForm = !showForm; clearMessages()">
|
||||
<i class="fas fa-plus"></i> Create Account
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Alerts -->
|
||||
<template x-if="error">
|
||||
<div class="v2-alert v2-alert-danger"><i class="fas fa-exclamation-circle"></i> <span x-text="error"></span></div>
|
||||
</template>
|
||||
<template x-if="success">
|
||||
<div class="v2-alert v2-alert-success"><i class="fas fa-check-circle"></i> <span x-text="success"></span></div>
|
||||
</template>
|
||||
|
||||
<!-- Create form -->
|
||||
<template x-if="showForm">
|
||||
<div class="v2-card" style="margin-bottom:20px;">
|
||||
<div class="v2-card-header"><h2>New FTP Account</h2></div>
|
||||
<div class="v2-card-body">
|
||||
<div style="display:grid; grid-template-columns:repeat(auto-fill, minmax(220px, 1fr)); gap:16px;">
|
||||
<div class="v2-form-group">
|
||||
<label>FTP Username</label>
|
||||
<input type="text" class="v2-input" x-model="formData.ftpUser" placeholder="ftpuser">
|
||||
</div>
|
||||
<div class="v2-form-group">
|
||||
<label>Password</label>
|
||||
<input type="password" class="v2-input" x-model="formData.ftpPassword" placeholder="Strong password">
|
||||
</div>
|
||||
</div>
|
||||
<input type="hidden" x-model="formData.ftpDomain" value="{{ website.domain }}">
|
||||
<div style="display:flex; gap:8px; margin-top:8px;">
|
||||
<button class="v2-btn v2-btn-primary" @click="formData.ftpDomain = '{{ website.domain }}'; create(formData)">Create</button>
|
||||
<button class="v2-btn v2-btn-outline" @click="showForm = false">Cancel</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- Table -->
|
||||
<div class="v2-card">
|
||||
<div class="v2-card-body no-pad">
|
||||
<template x-if="loading">
|
||||
<div style="padding:32px; text-align:center;"><div class="v2-spinner"></div></div>
|
||||
</template>
|
||||
<template x-if="!loading && items.length > 0">
|
||||
<div class="v2-table-wrap">
|
||||
<table class="v2-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Username</th>
|
||||
<th>Directory</th>
|
||||
<th>Quota</th>
|
||||
<th>Status</th>
|
||||
<th>Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<template x-for="acc in items" :key="acc.id">
|
||||
<tr>
|
||||
<td><span style="font-weight:600;" x-text="acc.user"></span></td>
|
||||
<td style="font-size:12px; word-break:break-all;" x-text="acc.dir"></td>
|
||||
<td x-text="acc.quota + ' MB'"></td>
|
||||
<td>
|
||||
<span class="v2-badge" :class="acc.status === 'y' ? 'v2-badge-success' : 'v2-badge-danger'" x-text="acc.status === 'y' ? 'Active' : 'Inactive'"></span>
|
||||
</td>
|
||||
<td>
|
||||
<button class="v2-btn v2-btn-danger v2-btn-sm" @click="remove({id: acc.id})">
|
||||
<i class="fas fa-trash"></i> Delete
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
</template>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</template>
|
||||
<template x-if="!loading && items.length === 0">
|
||||
<div class="v2-empty">
|
||||
<i class="fas fa-upload"></i>
|
||||
<p>No FTP accounts yet. Create one to get started.</p>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
58
panelv2/templates/panelv2/logs.html
Normal file
58
panelv2/templates/panelv2/logs.html
Normal file
@@ -0,0 +1,58 @@
|
||||
{% extends "panelv2/base.html" %}
|
||||
|
||||
{% block title %}Logs - {{ site_context.domain }} - CyberPanel v2{% endblock %}
|
||||
{% block nav_logs %}active{% endblock %}
|
||||
|
||||
{% block breadcrumb %}
|
||||
<a href="/v2/">Sites</a>
|
||||
<span class="sep">/</span>
|
||||
<a href="/v2/sites/{{ site_context.id }}/">{{ site_context.domain }}</a>
|
||||
<span class="sep">/</span>
|
||||
<span>Logs</span>
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div x-data="logViewerComponent('/v2/api/sites/{{ site_context.id }}/logs')">
|
||||
<!-- Header -->
|
||||
<div style="display:flex; align-items:center; justify-content:space-between; margin-bottom:20px; flex-wrap:wrap; gap:12px;">
|
||||
<h1 style="font-size:22px; font-weight:700; color:var(--text-heading); margin:0;">Site Logs</h1>
|
||||
<div style="display:flex; gap:8px; align-items:center;">
|
||||
<select class="v2-select" style="width:auto;" x-model="logType" @change="load()">
|
||||
<option value="access">Access Log</option>
|
||||
<option value="error">Error Log</option>
|
||||
</select>
|
||||
<select class="v2-select" style="width:auto;" x-model="lines" @change="load()">
|
||||
<option value="50">50 lines</option>
|
||||
<option value="100">100 lines</option>
|
||||
<option value="500">500 lines</option>
|
||||
<option value="1000">1000 lines</option>
|
||||
</select>
|
||||
<button class="v2-btn v2-btn-outline" @click="load()" :disabled="loading">
|
||||
<i class="fas fa-sync-alt"></i> Refresh
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Log viewer -->
|
||||
<div class="v2-card">
|
||||
<div class="v2-card-header">
|
||||
<h2 x-text="logType === 'access' ? 'Access Log' : 'Error Log'"></h2>
|
||||
<span style="font-size:12px; color:var(--text-secondary);" x-text="logs.length + ' lines'"></span>
|
||||
</div>
|
||||
<div class="v2-card-body no-pad">
|
||||
<template x-if="loading">
|
||||
<div style="padding:32px; text-align:center;"><div class="v2-spinner"></div></div>
|
||||
</template>
|
||||
<template x-if="!loading && logs.length > 0">
|
||||
<pre style="margin:0; padding:16px; overflow-x:auto; font-size:12px; line-height:1.6; max-height:600px; overflow-y:auto; background:var(--bg-primary); color:var(--text-primary); font-family:'SFMono-Regular',Consolas,'Liberation Mono',Menlo,monospace;"><template x-for="(line, i) in logs" :key="i"><span x-text="line + '\n'"></span></template></pre>
|
||||
</template>
|
||||
<template x-if="!loading && logs.length === 0">
|
||||
<div class="v2-empty">
|
||||
<i class="fas fa-file-alt"></i>
|
||||
<p>No log entries found.</p>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
114
panelv2/templates/panelv2/security.html
Normal file
114
panelv2/templates/panelv2/security.html
Normal file
@@ -0,0 +1,114 @@
|
||||
{% extends "panelv2/base.html" %}
|
||||
|
||||
{% block title %}Security - {{ site_context.domain }} - CyberPanel v2{% endblock %}
|
||||
{% block nav_security %}active{% endblock %}
|
||||
|
||||
{% block breadcrumb %}
|
||||
<a href="/v2/">Sites</a>
|
||||
<span class="sep">/</span>
|
||||
<a href="/v2/sites/{{ site_context.id }}/">{{ site_context.domain }}</a>
|
||||
<span class="sep">/</span>
|
||||
<span>Security</span>
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div x-data="{
|
||||
openBasedir: 1,
|
||||
loading: true,
|
||||
error: '',
|
||||
success: '',
|
||||
toggling: false,
|
||||
|
||||
async init() {
|
||||
try {
|
||||
const data = await v2Fetch('/v2/api/sites/{{ site_context.id }}/security');
|
||||
if (data.status === 1) {
|
||||
this.openBasedir = data.openBasedir;
|
||||
}
|
||||
} catch(e) {}
|
||||
this.loading = false;
|
||||
},
|
||||
|
||||
async toggleOpenBasedir() {
|
||||
this.toggling = true;
|
||||
this.error = '';
|
||||
this.success = '';
|
||||
const newVal = this.openBasedir === 1 ? 0 : 1;
|
||||
try {
|
||||
const data = await v2Fetch('/v2/api/sites/{{ site_context.id }}/security', {
|
||||
method: 'POST',
|
||||
body: { action: 'toggleOpenBasedir', value: newVal }
|
||||
});
|
||||
if (data.status === 1) {
|
||||
this.openBasedir = newVal;
|
||||
this.success = 'open_basedir ' + (newVal === 1 ? 'enabled' : 'disabled') + ' successfully';
|
||||
} else {
|
||||
this.error = data.error_message;
|
||||
}
|
||||
} catch(e) {
|
||||
this.error = 'Request failed';
|
||||
}
|
||||
this.toggling = false;
|
||||
}
|
||||
}">
|
||||
<!-- Header -->
|
||||
<div style="margin-bottom:20px;">
|
||||
<h1 style="font-size:22px; font-weight:700; color:var(--text-heading); margin:0;">Security Settings</h1>
|
||||
</div>
|
||||
|
||||
<!-- Alerts -->
|
||||
<template x-if="error">
|
||||
<div class="v2-alert v2-alert-danger"><i class="fas fa-exclamation-circle"></i> <span x-text="error"></span></div>
|
||||
</template>
|
||||
<template x-if="success">
|
||||
<div class="v2-alert v2-alert-success"><i class="fas fa-check-circle"></i> <span x-text="success"></span></div>
|
||||
</template>
|
||||
|
||||
<!-- open_basedir -->
|
||||
<div class="v2-card" style="margin-bottom:20px;">
|
||||
<div class="v2-card-header">
|
||||
<h2>open_basedir Protection</h2>
|
||||
<template x-if="!loading">
|
||||
<span class="v2-badge" :class="openBasedir === 1 ? 'v2-badge-success' : 'v2-badge-warning'" x-text="openBasedir === 1 ? 'Enabled' : 'Disabled'"></span>
|
||||
</template>
|
||||
</div>
|
||||
<div class="v2-card-body">
|
||||
<p style="font-size:13px; color:var(--text-secondary); margin-bottom:16px;">
|
||||
<strong>open_basedir</strong> 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.
|
||||
</p>
|
||||
<template x-if="loading">
|
||||
<div><div class="v2-spinner"></div></div>
|
||||
</template>
|
||||
<template x-if="!loading">
|
||||
<button class="v2-btn" :class="openBasedir === 1 ? 'v2-btn-danger' : 'v2-btn-primary'" :disabled="toggling" @click="toggleOpenBasedir()">
|
||||
<i class="fas" :class="openBasedir === 1 ? 'fa-unlock' : 'fa-lock'"></i>
|
||||
<span x-text="openBasedir === 1 ? 'Disable open_basedir' : 'Enable open_basedir'"></span>
|
||||
</button>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- SSH / SFTP Access info -->
|
||||
<div class="v2-card">
|
||||
<div class="v2-card-header">
|
||||
<h2>SSH / SFTP Access</h2>
|
||||
</div>
|
||||
<div class="v2-card-body">
|
||||
<div style="display:grid; grid-template-columns:repeat(auto-fill, minmax(220px, 1fr)); gap:16px;">
|
||||
<div>
|
||||
<div style="font-size:11px; text-transform:uppercase; letter-spacing:0.5px; color:var(--text-secondary); margin-bottom:4px;">SFTP Host</div>
|
||||
<div style="font-weight:600;">{{ ipAddress }}</div>
|
||||
</div>
|
||||
<div>
|
||||
<div style="font-size:11px; text-transform:uppercase; letter-spacing:0.5px; color:var(--text-secondary); margin-bottom:4px;">SFTP Port</div>
|
||||
<div style="font-weight:600;">22</div>
|
||||
</div>
|
||||
<div>
|
||||
<div style="font-size:11px; text-transform:uppercase; letter-spacing:0.5px; color:var(--text-secondary); margin-bottom:4px;">Document Root</div>
|
||||
<div style="font-weight:600; word-break:break-all;">/home/{{ website.domain }}/public_html</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
84
panelv2/templates/panelv2/server.html
Normal file
84
panelv2/templates/panelv2/server.html
Normal file
@@ -0,0 +1,84 @@
|
||||
{% extends "panelv2/base.html" %}
|
||||
|
||||
{% block title %}Server Management - CyberPanel v2{% endblock %}
|
||||
{% block nav_server %}active{% endblock %}
|
||||
|
||||
{% block breadcrumb %}
|
||||
<a href="/v2/">Sites</a>
|
||||
<span class="sep">/</span>
|
||||
<span>Server Management</span>
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div>
|
||||
<!-- Header -->
|
||||
<div style="margin-bottom:24px;">
|
||||
<h1 style="font-size:22px; font-weight:700; color:var(--text-heading); margin:0 0 4px 0;">Server Management</h1>
|
||||
<p style="font-size:13px; color:var(--text-secondary); margin:0;">Quick access to server-level management tools</p>
|
||||
</div>
|
||||
|
||||
<!-- Server tools grid -->
|
||||
<div class="v2-feature-grid" style="grid-template-columns:repeat(auto-fill, minmax(200px, 1fr));">
|
||||
<a href="/tuning/" class="v2-feature-card">
|
||||
<i class="fas fa-sliders-h"></i>
|
||||
<div class="feature-name">Tuning</div>
|
||||
<div class="feature-meta">Server performance tuning</div>
|
||||
</a>
|
||||
<a href="/serverstatus/" class="v2-feature-card">
|
||||
<i class="fas fa-heartbeat"></i>
|
||||
<div class="feature-name">Server Status</div>
|
||||
<div class="feature-meta">Resource usage & monitoring</div>
|
||||
</a>
|
||||
<a href="/managephp/" class="v2-feature-card">
|
||||
<i class="fab fa-php"></i>
|
||||
<div class="feature-name">PHP</div>
|
||||
<div class="feature-meta">PHP versions & extensions</div>
|
||||
</a>
|
||||
<a href="/serverlogs/" class="v2-feature-card">
|
||||
<i class="fas fa-file-alt"></i>
|
||||
<div class="feature-name">Server Logs</div>
|
||||
<div class="feature-meta">System & access logs</div>
|
||||
</a>
|
||||
<a href="/firewall/" class="v2-feature-card">
|
||||
<i class="fas fa-shield-alt"></i>
|
||||
<div class="feature-name">Firewall</div>
|
||||
<div class="feature-meta">Firewall rules & security</div>
|
||||
</a>
|
||||
<a href="/email/" class="v2-feature-card">
|
||||
<i class="fas fa-envelope"></i>
|
||||
<div class="feature-name">Mail Settings</div>
|
||||
<div class="feature-meta">Email server configuration</div>
|
||||
</a>
|
||||
<a href="/manageservices/" class="v2-feature-card">
|
||||
<i class="fas fa-cogs"></i>
|
||||
<div class="feature-name">Services</div>
|
||||
<div class="feature-meta">Manage server services</div>
|
||||
</a>
|
||||
<a href="/docker/" class="v2-feature-card">
|
||||
<i class="fab fa-docker"></i>
|
||||
<div class="feature-name">Docker</div>
|
||||
<div class="feature-meta">Docker container manager</div>
|
||||
</a>
|
||||
<a href="/packages/" class="v2-feature-card">
|
||||
<i class="fas fa-box"></i>
|
||||
<div class="feature-name">Packages</div>
|
||||
<div class="feature-meta">Hosting packages</div>
|
||||
</a>
|
||||
<a href="/users/" class="v2-feature-card">
|
||||
<i class="fas fa-users"></i>
|
||||
<div class="feature-name">Users</div>
|
||||
<div class="feature-meta">User management</div>
|
||||
</a>
|
||||
<a href="/backup/" class="v2-feature-card">
|
||||
<i class="fas fa-archive"></i>
|
||||
<div class="feature-name">Backups</div>
|
||||
<div class="feature-meta">Server-level backups</div>
|
||||
</a>
|
||||
<a href="/CloudLinux/" class="v2-feature-card">
|
||||
<i class="fas fa-cloud"></i>
|
||||
<div class="feature-name">CloudLinux</div>
|
||||
<div class="feature-meta">CloudLinux manager</div>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
173
panelv2/templates/panelv2/site_dashboard.html
Normal file
173
panelv2/templates/panelv2/site_dashboard.html
Normal file
@@ -0,0 +1,173 @@
|
||||
{% extends "panelv2/base.html" %}
|
||||
|
||||
{% block title %}{{ site_context.domain }} - CyberPanel v2{% endblock %}
|
||||
{% block nav_overview %}active{% endblock %}
|
||||
|
||||
{% block breadcrumb %}
|
||||
<a href="/v2/">Sites</a>
|
||||
<span class="sep">/</span>
|
||||
<span>{{ site_context.domain }}</span>
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div>
|
||||
<!-- Page header -->
|
||||
<div style="margin-bottom:24px;">
|
||||
<h1 style="font-size:22px; font-weight:700; color:var(--text-heading); margin:0 0 4px 0;">{{ site_context.domain }}</h1>
|
||||
<p style="font-size:13px; color:var(--text-secondary); margin:0;">Site overview and quick navigation</p>
|
||||
</div>
|
||||
|
||||
<!-- Resource stats -->
|
||||
<div class="v2-stats-grid">
|
||||
<div class="v2-stat-card">
|
||||
<div class="v2-stat-icon purple"><i class="fas fa-database"></i></div>
|
||||
<div class="v2-stat-info">
|
||||
<div class="label">Databases</div>
|
||||
<div class="value">{{ databases_count }}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="v2-stat-card">
|
||||
<div class="v2-stat-icon green"><i class="fas fa-envelope"></i></div>
|
||||
<div class="v2-stat-info">
|
||||
<div class="label">Email Accounts</div>
|
||||
<div class="value">{{ email_count }}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="v2-stat-card">
|
||||
<div class="v2-stat-icon blue"><i class="fas fa-upload"></i></div>
|
||||
<div class="v2-stat-info">
|
||||
<div class="label">FTP Accounts</div>
|
||||
<div class="value">{{ ftp_count }}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="v2-stat-card">
|
||||
<div class="v2-stat-icon orange"><i class="fas fa-sitemap"></i></div>
|
||||
<div class="v2-stat-info">
|
||||
<div class="label">Child Domains</div>
|
||||
<div class="value">{{ child_count }}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="v2-stat-card">
|
||||
<div class="v2-stat-icon {% if website.ssl == 1 %}green{% else %}red{% endif %}">
|
||||
<i class="fas fa-lock"></i>
|
||||
</div>
|
||||
<div class="v2-stat-info">
|
||||
<div class="label">SSL</div>
|
||||
<div class="value">{% if website.ssl == 1 %}Active{% else %}None{% endif %}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="v2-stat-card">
|
||||
<div class="v2-stat-icon purple"><i class="fab fa-wordpress"></i></div>
|
||||
<div class="v2-stat-info">
|
||||
<div class="label">WordPress Sites</div>
|
||||
<div class="value">{{ wp_count }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Site info card -->
|
||||
<div class="v2-card" style="margin-bottom:24px;">
|
||||
<div class="v2-card-header">
|
||||
<h2>Site Details</h2>
|
||||
<span class="v2-badge {% if website.state == 1 %}v2-badge-success{% else %}v2-badge-danger{% endif %}">
|
||||
{% if website.state == 1 %}Active{% else %}Suspended{% endif %}
|
||||
</span>
|
||||
</div>
|
||||
<div class="v2-card-body">
|
||||
<div style="display:grid; grid-template-columns:repeat(auto-fill, minmax(200px, 1fr)); gap:16px;">
|
||||
<div>
|
||||
<div style="font-size:11px; text-transform:uppercase; letter-spacing:0.5px; color:var(--text-secondary); margin-bottom:4px;">PHP Version</div>
|
||||
<div style="font-weight:600;">{{ website.phpSelection }}</div>
|
||||
</div>
|
||||
<div>
|
||||
<div style="font-size:11px; text-transform:uppercase; letter-spacing:0.5px; color:var(--text-secondary); margin-bottom:4px;">Admin Email</div>
|
||||
<div style="font-weight:600;">{{ website.adminEmail }}</div>
|
||||
</div>
|
||||
<div>
|
||||
<div style="font-size:11px; text-transform:uppercase; letter-spacing:0.5px; color:var(--text-secondary); margin-bottom:4px;">Backups</div>
|
||||
<div style="font-weight:600;">{{ backup_count }}</div>
|
||||
</div>
|
||||
<div>
|
||||
<div style="font-size:11px; text-transform:uppercase; letter-spacing:0.5px; color:var(--text-secondary); margin-bottom:4px;">Document Root</div>
|
||||
<div style="font-weight:600; word-break:break-all;">/home/{{ website.domain }}/public_html</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Quick actions -->
|
||||
<div style="display:flex; gap:10px; flex-wrap:wrap; margin-bottom:24px;">
|
||||
<a href="/filemanager/{{ website.domain }}" class="v2-btn v2-btn-outline" target="_blank">
|
||||
<i class="fas fa-folder-open"></i> File Manager
|
||||
</a>
|
||||
<a href="https://{{ website.domain }}" class="v2-btn v2-btn-outline" target="_blank" rel="noopener">
|
||||
<i class="fas fa-external-link-alt"></i> Visit Site
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<!-- Feature navigation grid -->
|
||||
<h2 style="font-size:16px; font-weight:600; color:var(--text-heading); margin-bottom:16px;">Manage</h2>
|
||||
<div class="v2-feature-grid">
|
||||
<a href="/v2/sites/{{ site_context.id }}/domains" class="v2-feature-card">
|
||||
<i class="fas fa-sitemap"></i>
|
||||
<div class="feature-name">Domains</div>
|
||||
<div class="feature-meta">{{ child_count }} child domain{{ child_count|pluralize }}</div>
|
||||
</a>
|
||||
<a href="/v2/sites/{{ site_context.id }}/databases" class="v2-feature-card">
|
||||
<i class="fas fa-database"></i>
|
||||
<div class="feature-name">Databases</div>
|
||||
<div class="feature-meta">{{ databases_count }} database{{ databases_count|pluralize }}</div>
|
||||
</a>
|
||||
<a href="/v2/sites/{{ site_context.id }}/email" class="v2-feature-card">
|
||||
<i class="fas fa-envelope"></i>
|
||||
<div class="feature-name">Email</div>
|
||||
<div class="feature-meta">{{ email_count }} account{{ email_count|pluralize }}</div>
|
||||
</a>
|
||||
<a href="/v2/sites/{{ site_context.id }}/ftp" class="v2-feature-card">
|
||||
<i class="fas fa-upload"></i>
|
||||
<div class="feature-name">FTP</div>
|
||||
<div class="feature-meta">{{ ftp_count }} account{{ ftp_count|pluralize }}</div>
|
||||
</a>
|
||||
<a href="/v2/sites/{{ site_context.id }}/dns" class="v2-feature-card">
|
||||
<i class="fas fa-globe"></i>
|
||||
<div class="feature-name">DNS</div>
|
||||
<div class="feature-meta">Manage records</div>
|
||||
</a>
|
||||
<a href="/v2/sites/{{ site_context.id }}/ssl" class="v2-feature-card">
|
||||
<i class="fas fa-lock"></i>
|
||||
<div class="feature-name">SSL</div>
|
||||
<div class="feature-meta">{% if website.ssl == 1 %}Active{% else %}Not configured{% endif %}</div>
|
||||
</a>
|
||||
<a href="/v2/sites/{{ site_context.id }}/backup" class="v2-feature-card">
|
||||
<i class="fas fa-archive"></i>
|
||||
<div class="feature-name">Backup</div>
|
||||
<div class="feature-meta">{{ backup_count }} backup{{ backup_count|pluralize }}</div>
|
||||
</a>
|
||||
<a href="/v2/sites/{{ site_context.id }}/files" class="v2-feature-card">
|
||||
<i class="fas fa-folder-open"></i>
|
||||
<div class="feature-name">Files</div>
|
||||
<div class="feature-meta">File manager</div>
|
||||
</a>
|
||||
<a href="/v2/sites/{{ site_context.id }}/logs" class="v2-feature-card">
|
||||
<i class="fas fa-file-alt"></i>
|
||||
<div class="feature-name">Logs</div>
|
||||
<div class="feature-meta">Access & error logs</div>
|
||||
</a>
|
||||
<a href="/v2/sites/{{ site_context.id }}/config" class="v2-feature-card">
|
||||
<i class="fas fa-cog"></i>
|
||||
<div class="feature-name">Config</div>
|
||||
<div class="feature-meta">vHost, PHP, rewrites</div>
|
||||
</a>
|
||||
<a href="/v2/sites/{{ site_context.id }}/apps" class="v2-feature-card">
|
||||
<i class="fas fa-th-large"></i>
|
||||
<div class="feature-name">Apps</div>
|
||||
<div class="feature-meta">{{ wp_count }} WP site{{ wp_count|pluralize }}</div>
|
||||
</a>
|
||||
<a href="/v2/sites/{{ site_context.id }}/security" class="v2-feature-card">
|
||||
<i class="fas fa-shield-alt"></i>
|
||||
<div class="feature-name">Security</div>
|
||||
<div class="feature-meta">open_basedir, SSH</div>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
93
panelv2/templates/panelv2/site_list.html
Normal file
93
panelv2/templates/panelv2/site_list.html
Normal file
@@ -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 %}
|
||||
<a href="/v2/">Sites</a>
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div x-data="siteListComponent()">
|
||||
<!-- Page header -->
|
||||
<div style="display:flex; align-items:center; justify-content:space-between; margin-bottom:20px; flex-wrap:wrap; gap:12px;">
|
||||
<h1 style="font-size:22px; font-weight:700; color:var(--text-heading); margin:0;">Websites</h1>
|
||||
{% if createWebsite %}
|
||||
<a href="/websites/createWebsite" class="v2-btn v2-btn-primary">
|
||||
<i class="fas fa-plus"></i> Create Website
|
||||
</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<!-- Search bar -->
|
||||
<div class="v2-search-bar">
|
||||
<input type="text" class="v2-input" placeholder="Search sites..." x-model="search" @input="page = 1" style="max-width:320px;">
|
||||
<span style="font-size:13px; color:var(--text-secondary);" x-text="filtered.length + ' site(s)'"></span>
|
||||
</div>
|
||||
|
||||
<!-- Sites table -->
|
||||
<div class="v2-card">
|
||||
<div class="v2-card-body no-pad">
|
||||
<div class="v2-table-wrap">
|
||||
<table class="v2-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Domain</th>
|
||||
<th>PHP</th>
|
||||
<th>SSL</th>
|
||||
<th>State</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<template x-for="site in paged" :key="site.id">
|
||||
<tr class="clickable-row" @click="goToSite(site.id)" style="cursor:pointer;">
|
||||
<td>
|
||||
<div style="display:flex; align-items:center; gap:10px;">
|
||||
<div style="width:32px;height:32px;border-radius:6px;background:rgba(88,86,214,0.1);display:flex;align-items:center;justify-content:center;">
|
||||
<i class="fas fa-globe" style="color:var(--accent-color); font-size:14px;"></i>
|
||||
</div>
|
||||
<span style="font-weight:600;" x-text="site.domain"></span>
|
||||
</div>
|
||||
</td>
|
||||
<td><span class="v2-badge v2-badge-info" x-text="site.php"></span></td>
|
||||
<td>
|
||||
<span class="v2-badge" :class="site.ssl == 1 ? 'v2-badge-success' : 'v2-badge-warning'" x-text="site.ssl == 1 ? 'Active' : 'None'"></span>
|
||||
</td>
|
||||
<td>
|
||||
<span class="v2-badge" :class="site.state == 1 ? 'v2-badge-success' : 'v2-badge-danger'" x-text="site.state == 1 ? 'Active' : 'Suspended'"></span>
|
||||
</td>
|
||||
</tr>
|
||||
</template>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<!-- Empty state -->
|
||||
<template x-if="filtered.length === 0">
|
||||
<div class="v2-empty">
|
||||
<i class="fas fa-globe"></i>
|
||||
<p x-text="search ? 'No sites match your search.' : 'No websites found.'"></p>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Pagination -->
|
||||
<template x-if="totalPages > 1">
|
||||
<div style="display:flex; align-items:center; justify-content:center; gap:8px; margin-top:16px;">
|
||||
<button class="v2-btn v2-btn-outline v2-btn-sm" @click="page = Math.max(1, page - 1)" :disabled="page <= 1">Prev</button>
|
||||
<span style="font-size:13px; color:var(--text-secondary);" x-text="'Page ' + page + ' of ' + totalPages"></span>
|
||||
<button class="v2-btn v2-btn-outline v2-btn-sm" @click="page = Math.min(totalPages, page + 1)" :disabled="page >= totalPages">Next</button>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
window.__v2_sites = [
|
||||
{% for site in websites %}
|
||||
{ id: {{ site.pk }}, domain: "{{ site.domain|escapejs }}", php: "{{ site.phpSelection|escapejs }}", ssl: {{ site.ssl }}, state: {{ site.state }} }{% if not forloop.last %},{% endif %}
|
||||
{% endfor %}
|
||||
];
|
||||
</script>
|
||||
{% endblock %}
|
||||
78
panelv2/templates/panelv2/ssl.html
Normal file
78
panelv2/templates/panelv2/ssl.html
Normal file
@@ -0,0 +1,78 @@
|
||||
{% extends "panelv2/base.html" %}
|
||||
|
||||
{% block title %}SSL - {{ site_context.domain }} - CyberPanel v2{% endblock %}
|
||||
{% block nav_ssl %}active{% endblock %}
|
||||
|
||||
{% block breadcrumb %}
|
||||
<a href="/v2/">Sites</a>
|
||||
<span class="sep">/</span>
|
||||
<a href="/v2/sites/{{ site_context.id }}/">{{ site_context.domain }}</a>
|
||||
<span class="sep">/</span>
|
||||
<span>SSL</span>
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div x-data="{ issuing: false, error: '', success: '' }">
|
||||
<!-- Header -->
|
||||
<div style="margin-bottom:20px;">
|
||||
<h1 style="font-size:22px; font-weight:700; color:var(--text-heading); margin:0;">SSL Certificate</h1>
|
||||
</div>
|
||||
|
||||
<!-- Alerts -->
|
||||
<template x-if="error">
|
||||
<div class="v2-alert v2-alert-danger"><i class="fas fa-exclamation-circle"></i> <span x-text="error"></span></div>
|
||||
</template>
|
||||
<template x-if="success">
|
||||
<div class="v2-alert v2-alert-success"><i class="fas fa-check-circle"></i> <span x-text="success"></span></div>
|
||||
</template>
|
||||
|
||||
<!-- SSL Status -->
|
||||
<div class="v2-card" style="margin-bottom:20px;">
|
||||
<div class="v2-card-header">
|
||||
<h2>Current Status</h2>
|
||||
<span class="v2-badge {% if website.ssl == 1 %}v2-badge-success{% else %}v2-badge-warning{% endif %}">
|
||||
{% if website.ssl == 1 %}SSL Active{% else %}No SSL{% endif %}
|
||||
</span>
|
||||
</div>
|
||||
<div class="v2-card-body">
|
||||
<div style="display:grid; grid-template-columns:repeat(auto-fill, minmax(200px, 1fr)); gap:16px;">
|
||||
<div>
|
||||
<div style="font-size:11px; text-transform:uppercase; letter-spacing:0.5px; color:var(--text-secondary); margin-bottom:4px;">Domain</div>
|
||||
<div style="font-weight:600;">{{ website.domain }}</div>
|
||||
</div>
|
||||
<div>
|
||||
<div style="font-size:11px; text-transform:uppercase; letter-spacing:0.5px; color:var(--text-secondary); margin-bottom:4px;">SSL Status</div>
|
||||
<div style="font-weight:600;">{% if website.ssl == 1 %}Enabled{% else %}Disabled{% endif %}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Issue SSL -->
|
||||
<div class="v2-card">
|
||||
<div class="v2-card-header">
|
||||
<h2>Issue SSL Certificate</h2>
|
||||
</div>
|
||||
<div class="v2-card-body">
|
||||
<p style="font-size:13px; color:var(--text-secondary); margin-bottom:16px;">
|
||||
Issue a free Let's Encrypt SSL certificate for <strong>{{ website.domain }}</strong>. Make sure the domain's DNS points to this server.
|
||||
</p>
|
||||
<button class="v2-btn v2-btn-primary" :disabled="issuing"
|
||||
@click="
|
||||
issuing = true; error = ''; success = '';
|
||||
v2Fetch('/v2/api/sites/{{ site_context.id }}/ssl', {method: 'POST', body: {action: 'issue'}})
|
||||
.then(d => {
|
||||
if (d.status === 1) { success = 'SSL certificate issued successfully. Reloading...'; setTimeout(() => location.reload(), 2000); }
|
||||
else { error = d.error_message; }
|
||||
})
|
||||
.catch(() => { error = 'Request failed'; })
|
||||
.finally(() => { issuing = false; })
|
||||
">
|
||||
<template x-if="issuing"><span class="v2-spinner" style="width:14px;height:14px;border-width:2px;"></span></template>
|
||||
<template x-if="!issuing"><i class="fas fa-certificate"></i></template>
|
||||
Issue Let's Encrypt SSL
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
0
panelv2/templatetags/__init__.py
Normal file
0
panelv2/templatetags/__init__.py
Normal file
38
panelv2/templatetags/v2_tags.py
Normal file
38
panelv2/templatetags/v2_tags.py
Normal file
@@ -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'
|
||||
37
panelv2/urls.py
Normal file
37
panelv2/urls.py
Normal file
@@ -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/<int:site_id>/', views.site_dashboard, name='v2_site_dashboard'),
|
||||
path('sites/<int:site_id>/domains', views.site_domains, name='v2_site_domains'),
|
||||
path('sites/<int:site_id>/databases', views.site_databases, name='v2_site_databases'),
|
||||
path('sites/<int:site_id>/email', views.site_email, name='v2_site_email'),
|
||||
path('sites/<int:site_id>/ftp', views.site_ftp, name='v2_site_ftp'),
|
||||
path('sites/<int:site_id>/dns', views.site_dns, name='v2_site_dns'),
|
||||
path('sites/<int:site_id>/ssl', views.site_ssl, name='v2_site_ssl'),
|
||||
path('sites/<int:site_id>/backup', views.site_backup, name='v2_site_backup'),
|
||||
path('sites/<int:site_id>/files', views.site_files, name='v2_site_files'),
|
||||
path('sites/<int:site_id>/logs', views.site_logs, name='v2_site_logs'),
|
||||
path('sites/<int:site_id>/config', views.site_config, name='v2_site_config'),
|
||||
path('sites/<int:site_id>/apps', views.site_apps, name='v2_site_apps'),
|
||||
path('sites/<int:site_id>/security', views.site_security, name='v2_site_security'),
|
||||
|
||||
# AJAX API endpoints (site-scoped)
|
||||
path('api/sites/<int:site_id>/databases', views.api_databases, name='v2_api_databases'),
|
||||
path('api/sites/<int:site_id>/email', views.api_email, name='v2_api_email'),
|
||||
path('api/sites/<int:site_id>/ftp', views.api_ftp, name='v2_api_ftp'),
|
||||
path('api/sites/<int:site_id>/dns', views.api_dns, name='v2_api_dns'),
|
||||
path('api/sites/<int:site_id>/ssl', views.api_ssl, name='v2_api_ssl'),
|
||||
path('api/sites/<int:site_id>/backup', views.api_backup, name='v2_api_backup'),
|
||||
path('api/sites/<int:site_id>/logs', views.api_logs, name='v2_api_logs'),
|
||||
path('api/sites/<int:site_id>/config', views.api_config, name='v2_api_config'),
|
||||
path('api/sites/<int:site_id>/domains', views.api_domains, name='v2_api_domains'),
|
||||
path('api/sites/<int:site_id>/security', views.api_security, name='v2_api_security'),
|
||||
|
||||
# Server management (admin only)
|
||||
path('server/', views.server_management, name='v2_server'),
|
||||
]
|
||||
742
panelv2/views.py
Normal file
742
panelv2/views.py
Normal file
@@ -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')
|
||||
Reference in New Issue
Block a user