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:
usmannasir
2026-02-24 01:59:19 +05:00
parent 41a9d84974
commit 30243493d4
25 changed files with 3523 additions and 0 deletions

View File

@@ -75,6 +75,7 @@ INSTALLED_APPS = [
'CLManager',
'IncBackups',
'aiScanner',
'panelv2',
# 'WebTerminal'
]

View File

@@ -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
View File

View 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;
}
}

View 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();
}
};
}

View 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 %}

View 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 %}

View 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>

View 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 %}

View 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 %}

View 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 %}

View 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 %}

View 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 %}

View 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 %}

View 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 %}

View 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 %}

View 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 %}

View 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 %}

View 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 %}

View 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 %}

View 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 %}

View File

View 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
View 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
View 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')