Merge branch 'v2.4.0-dev' of github.com:usmannasir/cyberpanel into v2.4.0-dev

This commit is contained in:
usmannasir
2025-04-23 03:00:19 +05:00
30 changed files with 14466 additions and 8414 deletions

View File

@@ -4,8 +4,8 @@ class CLMain():
def __init__(self): def __init__(self):
self.path = '/usr/local/CyberCP/version.txt' self.path = '/usr/local/CyberCP/version.txt'
#versionInfo = json.loads(open(self.path, 'r').read()) #versionInfo = json.loads(open(self.path, 'r').read())
self.version = '2.3' self.version = '2.4'
self.build = '9' self.build = '0'
ipFile = "/etc/cyberpanel/machineIP" ipFile = "/etc/cyberpanel/machineIP"
f = open(ipFile) f = open(ipFile)

View File

@@ -34,6 +34,7 @@ import googleapiclient.discovery
from googleapiclient.discovery import build from googleapiclient.discovery import build
from websiteFunctions.models import NormalBackupDests, NormalBackupJobs, NormalBackupSites from websiteFunctions.models import NormalBackupDests, NormalBackupJobs, NormalBackupSites
from plogical.IncScheduler import IncScheduler from plogical.IncScheduler import IncScheduler
from django.http import JsonResponse
class BackupManager: class BackupManager:
localBackupPath = '/home/cyberpanel/localBackupPath' localBackupPath = '/home/cyberpanel/localBackupPath'
@@ -2338,4 +2339,76 @@ class BackupManager:
json_data = json.dumps(data_ret) json_data = json.dumps(data_ret)
return HttpResponse(json_data) return HttpResponse(json_data)
def ReconfigureSubscription(self, request=None, userID=None, data=None):
try:
if not data:
return JsonResponse({'status': 0, 'error_message': 'No data provided'})
subscription_id = data['subscription_id']
customer_id = data['customer_id']
plan_name = data['plan_name']
amount = data['amount']
interval = data['interval']
# Call platform API to update SFTP key
import requests
import json
url = 'http://platform.cyberpersons.com/Billing/ReconfigureSubscription'
payload = {
'subscription_id': subscription_id,
'key': ProcessUtilities.outputExecutioner(f'cat /root/.ssh/cyberpanel.pub'),
'serverIP': ACLManager.fetchIP(),
'email': data['email'],
'code': data['code']
}
headers = {'Content-Type': 'application/json'}
response = requests.post(url, headers=headers, data=json.dumps(payload))
if response.status_code == 200:
response_data = response.json()
if response_data.get('status') == 1:
# Create OneClickBackups record
from IncBackups.models import OneClickBackups
backup_plan = OneClickBackups(
owner=Administrator.objects.get(pk=userID),
planName=plan_name,
months='1' if interval == 'month' else '12',
price=amount,
customer=customer_id,
subscription=subscription_id,
sftpUser=response_data.get('sftpUser'),
state=1 # Set as active since SFTP is already configured
)
backup_plan.save()
# Create SFTP destination in CyberPanel
finalDic = {
'IPAddress': response_data.get('ipAddress'),
'password': 'NOT-NEEDED',
'backupSSHPort': '22',
'userName': response_data.get('sftpUser'),
'type': 'SFTP',
'path': 'cpbackups',
'name': response_data.get('sftpUser')
}
wm = BackupManager()
response_inner = wm.submitDestinationCreation(userID, finalDic)
response_data_inner = json.loads(response_inner.content.decode('utf-8'))
if response_data_inner.get('status') == 0:
return JsonResponse({'status': 0, 'error_message': response_data_inner.get('error_message')})
return JsonResponse({'status': 1})
else:
return JsonResponse({'status': 0, 'error_message': response_data.get('error_message')})
else:
return JsonResponse({'status': 0, 'error_message': f'Platform API error: {response.text}'})
except Exception as e:
return JsonResponse({'status': 0, 'error_message': str(e)})

View File

@@ -2,6 +2,309 @@
* Created by usman on 9/17/17. * Created by usman on 9/17/17.
*/ */
// Using existing CyberCP module
app.controller('backupPlanNowOneClick', function($scope, $http) {
$scope.cyberpanelLoading = true;
$scope.showVerification = false;
$scope.verificationCodeSent = false;
$scope.showEmailVerification = function() {
console.log('showEmailVerification called');
$scope.showVerification = true;
};
$scope.cancelVerification = function() {
$scope.showVerification = false;
$scope.verificationCodeSent = false;
$scope.verificationEmail = '';
$scope.verificationCode = '';
};
$scope.sendVerificationCode = function() {
$scope.cyberpanelLoading = false;
var config = {
headers: {
'X-CSRFToken': getCookie('csrftoken')
}
};
$http.post('https://platform.cyberpersons.com/Billing/SendBackupVerificationCode', {
email: $scope.verificationEmail
}, config).then(function(response) {
$scope.cyberpanelLoading = true;
if (response.data.status == 1) {
$scope.verificationCodeSent = true;
new PNotify({
title: 'Success',
text: 'Verification code sent to your email.',
type: 'success'
});
} else {
new PNotify({
title: 'Error',
text: response.data.error_message,
type: 'error'
});
}
}, function(error) {
$scope.cyberpanelLoading = true;
new PNotify({
title: 'Error',
text: 'Could not send verification code. Please try again.',
type: 'error'
});
});
};
$scope.verifyCode = function() {
$scope.cyberpanelLoading = false;
var config = {
headers: {
'X-CSRFToken': getCookie('csrftoken')
}
};
$http.post('https://platform.cyberpersons.com/Billing/VerifyBackupCode', {
email: $scope.verificationEmail,
code: $scope.verificationCode
}, config).then(function(response) {
if (response.data.status == 1) {
// After successful verification, fetch Stripe subscriptions
$http.post('https://platform.cyberpersons.com/Billing/FetchStripeSubscriptionsByEmail', {
email: $scope.verificationEmail,
code: $scope.verificationCode
}, config).then(function(subResponse) {
$scope.cyberpanelLoading = true;
if (subResponse.data.status == 1) {
$scope.showVerification = false;
$scope.subscriptions = subResponse.data.subscriptions;
$scope.showSubscriptionsTable = true;
if ($scope.subscriptions.length == 0) {
new PNotify({
title: 'Info',
text: 'No active subscriptions found for this email.',
type: 'info'
});
}
} else {
new PNotify({
title: 'Error',
text: subResponse.data.error_message,
type: 'error'
});
}
}, function(error) {
$scope.cyberpanelLoading = true;
new PNotify({
title: 'Error',
text: 'Could not fetch subscriptions. Please try again.',
type: 'error'
});
});
} else {
$scope.cyberpanelLoading = true;
new PNotify({
title: 'Error',
text: response.data.error_message,
type: 'error'
});
}
}, function(error) {
$scope.cyberpanelLoading = true;
new PNotify({
title: 'Error',
text: 'Could not verify code. Please try again.',
type: 'error'
});
});
};
$scope.fetchBackupPlans = function() {
$scope.cyberpanelLoading = false;
var config = {
headers: {
'X-CSRFToken': getCookie('csrftoken')
}
};
$http.post('https://platform.cyberpersons.com/Billing/FetchBackupPlans', {
email: $scope.verificationEmail
}, config).then(function(response) {
$scope.cyberpanelLoading = true;
if (response.data.status == 1) {
$scope.plans = response.data.plans;
new PNotify({
title: 'Success',
text: 'Backup plans fetched successfully.',
type: 'success'
});
} else {
new PNotify({
title: 'Error',
text: response.data.error_message,
type: 'error'
});
}
}, function(error) {
$scope.cyberpanelLoading = true;
new PNotify({
title: 'Error',
text: 'Could not fetch backup plans. Please try again.',
type: 'error'
});
});
};
$scope.BuyNowBackupP = function (planName, monthlyPrice, yearlyPrice, months) {
const baseURL = 'https://platform.cyberpersons.com/Billing/CreateOrderforBackupPlans';
// Get the current URL
var currentURL = window.location.href;
// Find the position of the question mark
const queryStringIndex = currentURL.indexOf('?');
// Check if there is a query string
currentURL = queryStringIndex !== -1 ? currentURL.substring(0, queryStringIndex) : currentURL;
// Encode parameters to make them URL-safe
const params = new URLSearchParams({
planName: planName,
monthlyPrice: monthlyPrice,
yearlyPrice: yearlyPrice,
returnURL: currentURL, // Add the current URL as a query parameter
months: months
});
// Build the complete URL with query string
const fullURL = `${baseURL}?${params.toString()}`;
// Redirect to the constructed URL
window.location.href = fullURL;
};
$scope.PaypalBuyNowBackup = function (planName, monthlyPrice, yearlyPrice, months) {
const baseURL = 'https://platform.cyberpersons.com/Billing/PaypalCreateOrderforBackupPlans';
// Get the current URL
var currentURL = window.location.href;
// Find the position of the question mark
const queryStringIndex = currentURL.indexOf('?');
// Check if there is a query string
currentURL = queryStringIndex !== -1 ? currentURL.substring(0, queryStringIndex) : currentURL;
// Encode parameters to make them URL-safe
const params = new URLSearchParams({
planName: planName,
monthlyPrice: monthlyPrice,
yearlyPrice: yearlyPrice,
returnURL: currentURL, // Add the current URL as a query parameter
months: months
});
// Build the complete URL with query string
const fullURL = `${baseURL}?${params.toString()}`;
// Redirect to the constructed URL
window.location.href = fullURL;
};
$scope.DeployAccount = function (id) {
$scope.cyberpanelLoading = false;
url = "/backup/DeployAccount";
var data = {
id: id
};
var config = {
headers: {
'X-CSRFToken': getCookie('csrftoken')
}
};
$http.post(url, data, config).then(ListInitialDatas, cantLoadInitialDatas);
function ListInitialDatas(response) {
$scope.cyberpanelLoading = true;
if (response.data.status === 1) {
new PNotify({
title: 'Success',
text: 'Successfully deployed.',
type: 'success'
});
window.location.reload();
} else {
new PNotify({
title: 'Operation Failed!',
text: response.data.error_message,
type: 'error'
});
}
}
function cantLoadInitialDatas(response) {
$scope.cyberpanelLoading = true;
new PNotify({
title: 'Operation Failed!',
text: 'Could not connect to server, please refresh this page',
type: 'error'
});
}
};
$scope.ReconfigureSubscription = function(subscription) {
$scope.cyberpanelLoading = false;
var config = {
headers: {
'X-CSRFToken': getCookie('csrftoken')
}
};
var data = {
subscription_id: subscription.subscription_id,
customer_id: subscription.customer,
plan_name: subscription.plan_name,
amount: subscription.amount,
interval: subscription.interval,
email: $scope.verificationEmail,
code: $scope.verificationCode
};
$http.post('/backup/ReconfigureSubscription', data, config).then(function(response) {
$scope.cyberpanelLoading = true;
if (response.data.status === 1) {
new PNotify({
title: 'Success',
text: 'Subscription configured successfully for this server.',
type: 'success'
});
// Refresh the page to show new backup plan in the list
window.location.reload();
} else {
new PNotify({
title: 'Error',
text: response.data.error_message,
type: 'error'
});
}
}, function(error) {
$scope.cyberpanelLoading = true;
new PNotify({
title: 'Error',
text: 'Could not configure subscription. Please try again.',
type: 'error'
});
});
};
});
//*** Backup site ****// //*** Backup site ****//
app.controller('backupWebsiteControl', function ($scope, $http, $timeout) { app.controller('backupWebsiteControl', function ($scope, $http, $timeout) {
@@ -2045,307 +2348,6 @@ app.controller('scheduleBackup', function ($scope, $http, $window) {
}); });
app.controller('backupPlanNowOneClick', function ($scope, $http, $window) {
$scope.cyberpanelLoading = true;
$scope.sftpHide = true;
$scope.localHide = true;
$scope.BuyNowBackupP = function (planName, monthlyPrice, yearlyPrice, months) {
const baseURL = 'https://platform.cyberpersons.com/Billing/CreateOrderforBackupPlans';
// Get the current URL
var currentURL = window.location.href;
// Find the position of the question mark
const queryStringIndex = currentURL.indexOf('?');
// Check if there is a query string
currentURL = queryStringIndex !== -1 ? currentURL.substring(0, queryStringIndex) : currentURL;
// Encode parameters to make them URL-safe
const params = new URLSearchParams({
planName: planName,
monthlyPrice: monthlyPrice,
yearlyPrice: yearlyPrice,
returnURL: currentURL, // Add the current URL as a query parameter
months: months
});
// Build the complete URL with query string
const fullURL = `${baseURL}?${params.toString()}`;
// Redirect to the constructed URL
window.location.href = fullURL;
}
$scope.fetchDetails = function () {
if ($scope.destinationType === 'SFTP') {
$scope.sftpHide = false;
$scope.localHide = true;
$scope.populateCurrentRecords();
} else {
$scope.sftpHide = true;
$scope.localHide = false;
$scope.populateCurrentRecords();
}
};
$scope.populateCurrentRecords = function () {
$scope.cyberpanelLoading = false;
url = "/backup/getCurrentBackupDestinations";
var type = 'SFTP';
if ($scope.destinationType === 'SFTP') {
type = 'SFTP';
} else {
type = 'local';
}
var data = {
type: type
};
var config = {
headers: {
'X-CSRFToken': getCookie('csrftoken')
}
};
$http.post(url, data, config).then(ListInitialDatas, cantLoadInitialDatas);
function ListInitialDatas(response) {
$scope.cyberpanelLoading = true;
if (response.data.status === 1) {
$scope.records = JSON.parse(response.data.data);
} else {
new PNotify({
title: 'Operation Failed!',
text: response.data.error_message,
type: 'error'
});
}
}
function cantLoadInitialDatas(response) {
$scope.cyberpanelLoading = true;
new PNotify({
title: 'Operation Failed!',
text: 'Could not connect to server, please refresh this page',
type: 'error'
});
}
};
$scope.addDestination = function (type) {
$scope.cyberpanelLoading = false;
url = "/backup/submitDestinationCreation";
if (type === 'SFTP') {
var data = {
type: type,
name: $scope.name,
IPAddress: $scope.IPAddress,
userName: $scope.userName,
password: $scope.password,
backupSSHPort: $scope.backupSSHPort,
path: $scope.path
};
} else {
var data = {
type: type,
path: $scope.localPath,
name: $scope.name
};
}
var config = {
headers: {
'X-CSRFToken': getCookie('csrftoken')
}
};
$http.post(url, data, config).then(ListInitialDatas, cantLoadInitialDatas);
function ListInitialDatas(response) {
$scope.cyberpanelLoading = true;
$scope.populateCurrentRecords();
if (response.data.status === 1) {
new PNotify({
title: 'Success!',
text: 'Destination successfully added.',
type: 'success'
});
} else {
new PNotify({
title: 'Operation Failed!',
text: response.data.error_message,
type: 'error'
});
}
}
function cantLoadInitialDatas(response) {
$scope.cyberpanelLoading = true;
new PNotify({
title: 'Operation Failed!',
text: 'Could not connect to server, please refresh this page',
type: 'error'
});
}
};
$scope.removeDestination = function (type, nameOrPath) {
$scope.cyberpanelLoading = false;
url = "/backup/deleteDestination";
var data = {
type: type,
nameOrPath: nameOrPath,
};
var config = {
headers: {
'X-CSRFToken': getCookie('csrftoken')
}
};
$http.post(url, data, config).then(ListInitialDatas, cantLoadInitialDatas);
function ListInitialDatas(response) {
$scope.cyberpanelLoading = true;
$scope.populateCurrentRecords();
if (response.data.status === 1) {
new PNotify({
title: 'Success!',
text: 'Destination successfully removed.',
type: 'success'
});
} else {
new PNotify({
title: 'Operation Failed!',
text: response.data.error_message,
type: 'error'
});
}
}
function cantLoadInitialDatas(response) {
$scope.cyberpanelLoading = true;
new PNotify({
title: 'Operation Failed!',
text: 'Could not connect to server, please refresh this page',
type: 'error'
});
}
};
$scope.DeployAccount = function (id) {
$scope.cyberpanelLoading = false;
url = "/backup/DeployAccount";
var data = {
id:id
};
var config = {
headers: {
'X-CSRFToken': getCookie('csrftoken')
}
};
$http.post(url, data, config).then(ListInitialDatas, cantLoadInitialDatas);
function ListInitialDatas(response) {
$scope.cyberpanelLoading = true;
if (response.data.status === 1) {
new PNotify({
title: 'Success',
text: 'Successfully deployed.',
type: 'success'
});
$window.location.reload();
} else {
new PNotify({
title: 'Operation Failed!',
text: response.data.error_message,
type: 'error'
});
}
}
function cantLoadInitialDatas(response) {
$scope.couldNotConnect = false;
restoreBackupButton.disabled = false;
}
};
//// paypal
$scope.PaypalBuyNowBackup = function (planName, monthlyPrice, yearlyPrice, months) {
const baseURL = 'https://platform.cyberpersons.com/Billing/PaypalCreateOrderforBackupPlans';
// Get the current URL
var currentURL = window.location.href;
// Find the position of the question mark
const queryStringIndex = currentURL.indexOf('?');
// Check if there is a query string
currentURL = queryStringIndex !== -1 ? currentURL.substring(0, queryStringIndex) : currentURL;
// Encode parameters to make them URL-safe
const params = new URLSearchParams({
planName: planName,
monthlyPrice: monthlyPrice,
yearlyPrice: yearlyPrice,
returnURL: currentURL, // Add the current URL as a query parameter
months: months
});
// Build the complete URL with query string
const fullURL = `${baseURL}?${params.toString()}`;
// Redirect to the constructed URL
window.location.href = fullURL;
}
});
app.controller('OneClickrestoreWebsiteControl', function ($scope, $http, $timeout) { app.controller('OneClickrestoreWebsiteControl', function ($scope, $http, $timeout) {
$scope.restoreLoading = true; $scope.restoreLoading = true;

View File

@@ -5,59 +5,409 @@
{% load static %} {% load static %}
{% get_current_language as LANGUAGE_CODE %} {% get_current_language as LANGUAGE_CODE %}
<!-- Current language: {{ LANGUAGE_CODE }} --> <!-- Current language: {{ LANGUAGE_CODE }} -->
<style>
/* Use CyberPanel color scheme */
:root {
--primary-color: #0078ff;
--secondary-color: #2096f3;
--bg-light: #f5f7f9;
--border-color: #e0e6ed;
--text-dark: #3e4b5b;
}
.info-box {
background-color: #e8f4fd;
border: 1px solid #bfdff1;
border-radius: 4px;
padding: 15px;
margin-bottom: 20px;
}
.cp-card {
background: #fff;
border-radius: 8px;
box-shadow: 0 2px 10px rgba(0,0,0,0.08);
margin-bottom: 20px;
border: 1px solid var(--border-color);
transition: transform 0.2s, box-shadow 0.2s;
height: 100%;
display: flex;
flex-direction: column;
}
.cp-card:hover {
transform: translateY(-5px);
box-shadow: 0 5px 15px rgba(0,0,0,0.1);
}
.cp-card-header {
padding: 18px 20px;
border-bottom: 1px solid var(--border-color);
background: var(--bg-light);
font-weight: 600;
font-size: 18px;
border-top-left-radius: 8px;
border-top-right-radius: 8px;
}
.cp-card-body {
padding: 20px;
flex-grow: 1;
display: flex;
flex-direction: column;
}
.cp-btn {
display: inline-block;
font-weight: 500;
text-align: center;
white-space: nowrap;
vertical-align: middle;
user-select: none;
border: 1px solid transparent;
padding: 10px 16px;
font-size: 14px;
border-radius: 6px;
transition: all 0.2s;
cursor: pointer;
text-decoration: none;
}
.cp-btn-primary {
background-color: var(--primary-color);
color: white;
}
.cp-btn-primary:hover {
background-color: #0056b3;
transform: translateY(-2px);
box-shadow: 0 4px 8px rgba(0,0,0,0.1);
}
.cp-btn-outline {
background-color: white;
border: 1px solid var(--border-color);
color: var(--text-dark);
}
.cp-btn-outline:hover {
background-color: var(--bg-light);
transform: translateY(-2px);
box-shadow: 0 4px 8px rgba(0,0,0,0.1);
}
.cp-btn-block {
display: block;
width: 100%;
margin-top: auto;
}
.cp-table {
width: 100%;
margin-bottom: 0;
border-collapse: collapse;
}
.cp-table th {
background-color: var(--bg-light);
border-bottom: 1px solid var(--border-color);
padding: 15px;
text-align: left;
font-weight: 500;
}
.cp-table td {
padding: 15px;
border-bottom: 1px solid var(--border-color);
vertical-align: middle;
}
.cp-table tr:last-child td {
border-bottom: none;
}
.cp-badge {
display: inline-block;
padding: 5px 10px;
border-radius: 30px;
font-size: 12px;
font-weight: 500;
}
.cp-badge-primary {
background-color: var(--primary-color);
color: white;
}
.cp-badge-info {
background-color: #17a2b8;
color: white;
}
.cp-form-control {
display: block;
width: 100%;
padding: 10px 15px;
background-color: #fff;
border: 1px solid var(--border-color);
border-radius: 6px;
font-size: 14px;
}
.price-box {
background-color: var(--bg-light);
border-radius: 6px;
padding: 15px;
margin-bottom: 15px;
text-align: center;
}
.price-amount {
font-size: 28px;
font-weight: 600;
color: var(--primary-color);
}
.price-period {
font-size: 14px;
color: #6c757d;
}
.plan-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(260px, 1fr));
gap: 25px;
}
.billing-cycle {
display: inline-block;
padding: 6px 12px;
background-color: var(--primary-color);
color: white;
border-radius: 20px;
font-size: 13px;
}
.action-btns {
display: flex;
gap: 10px;
}
.action-btns a {
text-decoration: none;
}
.page-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 25px;
}
.page-title {
font-size: 26px;
margin-bottom: 0;
font-weight: 500;
}
.section-title {
font-size: 20px;
margin-bottom: 20px;
padding-bottom: 12px;
position: relative;
font-weight: 500;
}
.section-title:after {
content: '';
position: absolute;
bottom: 0;
left: 0;
width: 50px;
height: 3px;
background-color: var(--primary-color);
}
.btn-space {
margin-top: 15px;
}
.plans-container {
margin-top: 30px;
}
.page-description {
color: #6c757d;
margin-bottom: 25px;
}
.verify-email-section {
margin: 30px 0;
text-align: center;
}
.verify-email-btn {
display: inline-block;
padding: 12px 24px;
background-color: var(--primary-color);
color: white;
border-radius: 6px;
text-decoration: none;
font-weight: 500;
transition: all 0.2s;
border: none;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
}
.verify-email-btn:hover {
background-color: #0056b3;
transform: translateY(-2px);
box-shadow: 0 4px 8px rgba(0,0,0,0.1);
}
@media (max-width: 768px) {
.plan-grid {
grid-template-columns: 1fr;
}
.action-btns {
flex-direction: column;
}
}
</style>
<div class="container"> <div class="container">
<div id="page-title">
<h2>{% trans "One-click Backups" %} - <a target="_blank" <!-- Page header -->
href="https://youtu.be/mLjMg8Anq70" <div class="page-header">
style="height: 23px;line-height: 21px;" <h1 class="page-title">One-click Backups</h1>
class="btn btn-border btn-alt border-red btn-link font-red" <a href="https://youtu.be/mLjMg8Anq70" target="_blank" class="cp-btn cp-btn-outline">
title=""><span>{% trans "One-Click Backup Docs" %}</span></a> Watch Tutorial
</h2> </a>
<p>{% trans "On this page you purchase and manage one-click backups." %}</p>
</div> </div>
<div ng-controller="backupPlanNowOneClick" class="panel"> <p class="page-description">On this page you purchase and manage one-click backups.</p>
<div class="panel-body">
<h3 class="title-hero">
{% trans "Set up Backup Destinations." %} <img ng-hide="cyberpanelLoading"
src="{% static 'images/loading.gif' %}">
</h3>
<div class="example-box-wrapper">
<div ng-controller="backupPlanNowOneClick">
<!-- Email Verification Button -->
<div class="verify-email-section" ng-hide="showVerification || showSubscriptionsTable">
<a href="javascript:void(0)" class="verify-email-btn" ng-click="showEmailVerification()">
{% trans "Fetch existing backup plans if any." %}
</a>
</div>
<!-- Email Verification Section -->
<div class="cp-card" ng-show="showVerification">
<div class="cp-card-header">
{% trans "Verify Your Email" %}
</div>
<div class="cp-card-body">
<div class="row">
<div class="col-md-8 offset-md-2">
<div class="form-group mb-3">
<label class="mb-2">{% trans "Email Address" %}</label>
<input type="email"
class="cp-form-control"
ng-model="verificationEmail"
placeholder="Enter your email address">
</div>
<div class="form-group mb-3" ng-show="verificationCodeSent">
<label class="mb-2">{% trans "Verification Code" %}</label>
<input type="text"
class="cp-form-control"
ng-model="verificationCode"
placeholder="Enter verification code">
</div>
<div class="text-center mt-4">
<button type="button"
ng-click="sendVerificationCode()"
ng-hide="verificationCodeSent"
class="cp-btn cp-btn-primary">
{% trans "Send Verification Code" %}
</button>
<button type="button"
ng-click="verifyCode()"
ng-show="verificationCodeSent"
class="cp-btn cp-btn-primary">
{% trans "Verify Code" %}
</button>
<button type="button"
ng-click="cancelVerification()"
class="cp-btn cp-btn-outline ml-2">
{% trans "Cancel" %}
</button>
</div>
</div>
</div>
</div>
</div>
<!-- Active Subscriptions -->
<div class="cp-card" ng-show="showSubscriptionsTable">
<div class="cp-card-header">
Your Active Subscriptions
</div>
<div class="cp-card-body p-0">
<table class="cp-table">
<thead>
<tr>
<th>Subscription ID</th>
<th>Status</th>
<th>Amount</th>
<th>Billing Interval</th>
<th>Next Billing Date</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
<tr ng-repeat="sub in subscriptions">
<td><code>{$ sub.subscription_id $}</code></td>
<td>
<span class="cp-badge"
ng-class="{'cp-badge-primary': sub.status === 'active', 'cp-badge-info': sub.status !== 'active'}">
{$ sub.status $}
</span>
</td>
<td>${$ sub.amount $}</td>
<td>{$ sub.interval $}</td>
<td>{$ sub.current_period_end | date:'medium' $}</td>
<td>
<button class="cp-btn cp-btn-primary"
ng-click="ReconfigureSubscription(sub)">
Configure Server
</button>
</td>
</tr>
</tbody>
</table>
</div>
</div>
<!-- Status Messages -->
{% if status == 1 %} {% if status == 1 %}
<div class="alert alert-info"> <div class="info-box">
<p>You have successfully purchased a backup plan.</p> <p class="mb-0">You have successfully purchased a backup plan.</p>
</div> </div>
{% elif status == 0 %} {% elif status == 0 %}
<div class="info-box" style="background-color: #f8d7da; border-color: #f5c6cb;">
<div class="alert alert-danger"> <p class="mb-0">Your purchase was not successful. {{ message }}</p>
<p>Your purchase was not successful.</p> {{ message }}
</div> </div>
{% elif status == 4 %} {% elif status == 4 %}
<div class="info-box" style="background-color: #f8d7da; border-color: #f5c6cb;">
<div class="alert alert-danger"> <p class="mb-0">{{ message }}</p>
{{ message }}
</div> </div>
{% endif %} {% endif %}
<form action="/" class="form-horizontal bordered-row"> <!-- Your Backup Plans Section -->
<div class="cp-card mb-4">
<p style="font-size: 15px;margin: 1%;">With CyberPanel's one-click backups, you can easily back <div class="cp-card-header">Your Backup Plans</div>
up your website to our secure <div class="cp-card-body p-0">
servers in just 60 seconds. It's simple, fast, and reliable.</p> <table class="cp-table">
<!------ List of Purchased backup plans --------------->
<div class="form-group">
<div class="col-sm-12">
<table class="table">
<thead> <thead>
<tr> <tr>
<th>{% trans "Account" %}</th> <th>{% trans "Account" %}</th>
@@ -74,108 +424,182 @@
<td>{{ plan.sftpUser }}</td> <td>{{ plan.sftpUser }}</td>
<td>{{ plan.planName }}</td> <td>{{ plan.planName }}</td>
<td>{{ plan.subscription }}</td> <td>{{ plan.subscription }}</td>
<td>
<span class="billing-cycle">
{% if plan.months == '1' %} {% if plan.months == '1' %}
<td>${{ plan.price }}/month</td> ${{ plan.price }}/month
{% else %} {% else %}
<td>${{ plan.price }}/year</td> ${{ plan.price }}/year
{% endif %} {% endif %}
</span>
</td>
<td>{{ plan.date }}</td> <td>{{ plan.date }}</td>
<td> <td>
<div class="action-btns">
{% if plan.state == 1 %} {% if plan.state == 1 %}
<a <a href="{% url 'ManageOCBackups' %}?id={{ plan.id }}"
href="{% url 'ManageOCBackups' %}?id={{ plan.id }}"> class="cp-btn cp-btn-primary">
<button style="margin-bottom: 1%" type="button" {% trans "Schedule Backups" %}
class="btn btn-primary btn-lg btn-block">{% trans "Schedule Backups" %}</button>
</a> </a>
<a href="{% url 'RestoreOCBackups' %}?id={{ plan.id }}"> <a href="{% url 'RestoreOCBackups' %}?id={{ plan.id }}"
<button type="button" class="cp-btn cp-btn-outline">
class="btn btn-primary btn-lg btn-block">{% trans "Restore Backups" %}</button> {% trans "Restore Backups" %}
</a> </a>
{% else %} {% else %}
<button type="button" <button type="button"
ng-click="DeployAccount('{{ plan.id }}')" ng-click="DeployAccount('{{ plan.id }}')"
class="btn btn-primary btn-lg btn-block">{% trans "Deploy Account" %}</button> class="cp-btn cp-btn-primary">
{% trans "Deploy Account" %}
</button>
{% endif %} {% endif %}
</div>
</td>
</td> </td>
</tr> </tr>
{% endfor %} {% endfor %}
</tbody> </tbody>
</table> </table>
</div> </div>
</div> </div>
<!------ List of Purchased backup plans ---------------> <!-- Available Backup Plans Section -->
<h2 class="section-title">Available Backup Plans</h2>
<div class="plans-container">
<!------ List of Backup plans ---------------> <div class="plan-grid">
<!-- 100GB Plan -->
<h3 class="title-hero"> <div class="cp-card">
{% trans "Subscribe to one-click backup plans." %} <img ng-hide="cyberpanelLoading" <div class="cp-card-header">100GB</div>
src="{% static 'images/loading.gif' %}"> <div class="cp-card-body">
</h3> <div class="price-box mb-3">
<div class="price-amount">${{ plans.0.monthlyPrice }}</div>
<div class="form-group"> <div class="price-period">/month</div>
<div class="col-sm-12"> </div>
<table class="table"> <div class="price-box mb-4">
<thead> <div class="price-amount">${{ plans.0.yearlyPrice }}</div>
<tr> <div class="price-period">/year</div>
<th>{% trans "Plan Name" %}</th>
<th>{% trans "Monthly Price" %}</th>
<th>{% trans "Yearly Price" %}</th>
<th>{% trans "Actions" %}</th>
</tr>
</thead>
<tbody>
{% for plan in plans %}
<tr>
<td>{{ plan.name }}</td>
<td>${{ plan.monthlyPrice }}</td>
<td>${{ plan.yearlyPrice }}</td>
<td>
{% if plan.name != '100GB' %}
<button type="button"
ng-click="PaypalBuyNowBackup('{{ plan.name }}', '{{ plan.monthlyPrice }}', '{{ plan.yearlyPrice }}', 1)"
class="btn btn-primary btn-lg btn-block">{% trans "Buy Monthly (Paypal)" %}</button>
{% endif %}
<button type="button"
ng-click="PaypalBuyNowBackup('{{ plan.name }}', '{{ plan.monthlyPrice }}', '{{ plan.yearlyPrice }}', 12)"
class="btn btn-primary btn-lg btn-block">{% trans "Buy Yearly (Paypal)" %}</button>
{% if plan.name != '100GB' %}
<button type="button"
ng-click="BuyNowBackupP('{{ plan.name }}', '{{ plan.monthlyPrice }}', '{{ plan.yearlyPrice }}', 1)"
class="btn btn-primary btn-lg btn-block">{% trans "Buy Monthly via Card" %}</button>
{% endif %}
<button type="button"
ng-click="BuyNowBackupP('{{ plan.name }}', '{{ plan.monthlyPrice }}', '{{ plan.yearlyPrice }}', 12)"
class="btn btn-primary btn-lg btn-block">{% trans "Buy Yearly via Card" %}</button>
</td>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div> </div>
<a href="javascript:void(0)"
ng-click="BuyNowBackupP('{{ plans.0.name }}', '{{ plans.0.monthlyPrice }}', '{{ plans.0.yearlyPrice }}', 12)"
class="cp-btn cp-btn-outline cp-btn-block btn-space">
Yearly via Card
</a>
</div>
</div> </div>
<!------ List of backup plans ---------------> <!-- 500GB Plan -->
<div class="cp-card">
<div class="cp-card-header">500GB</div>
<!--- AWS End ---> <div class="cp-card-body">
<div class="price-box mb-3">
<div class="price-amount">${{ plans.1.monthlyPrice }}</div>
</form> <div class="price-period">/month</div>
</div>
<div class="price-box mb-4">
<div class="price-amount">${{ plans.1.yearlyPrice }}</div>
<div class="price-period">/year</div>
</div>
<div class="btn-space">
<a href="javascript:void(0)"
ng-click="BuyNowBackupP('{{ plans.1.name }}', '{{ plans.1.monthlyPrice }}', '{{ plans.1.yearlyPrice }}', 1)"
class="cp-btn cp-btn-outline cp-btn-block mb-2">
Monthly via Card
</a>
<a href="javascript:void(0)"
ng-click="BuyNowBackupP('{{ plans.1.name }}', '{{ plans.1.monthlyPrice }}', '{{ plans.1.yearlyPrice }}', 12)"
class="cp-btn cp-btn-outline cp-btn-block">
Yearly via Card
</a>
</div> </div>
</div> </div>
</div> </div>
<!-- 1TB Plan -->
<div class="cp-card">
<div class="cp-card-header">1TB</div>
<div class="cp-card-body">
<div class="price-box mb-3">
<div class="price-amount">${{ plans.2.monthlyPrice }}</div>
<div class="price-period">/month</div>
</div>
<div class="price-box mb-4">
<div class="price-amount">${{ plans.2.yearlyPrice }}</div>
<div class="price-period">/year</div>
</div> </div>
<div class="btn-space">
<a href="javascript:void(0)"
ng-click="BuyNowBackupP('{{ plans.2.name }}', '{{ plans.2.monthlyPrice }}', '{{ plans.2.yearlyPrice }}', 1)"
class="cp-btn cp-btn-outline cp-btn-block mb-2">
Monthly via Card
</a>
<a href="javascript:void(0)"
ng-click="BuyNowBackupP('{{ plans.2.name }}', '{{ plans.2.monthlyPrice }}', '{{ plans.2.yearlyPrice }}', 12)"
class="cp-btn cp-btn-outline cp-btn-block">
Yearly via Card
</a>
</div>
</div>
</div>
<!-- 2TB Plan -->
<div class="cp-card">
<div class="cp-card-header">2TB</div>
<div class="cp-card-body">
<div class="price-box mb-3">
<div class="price-amount">${{ plans.3.monthlyPrice }}</div>
<div class="price-period">/month</div>
</div>
<div class="price-box mb-4">
<div class="price-amount">${{ plans.3.yearlyPrice }}</div>
<div class="price-period">/year</div>
</div>
<div class="btn-space">
<a href="javascript:void(0)"
ng-click="BuyNowBackupP('{{ plans.3.name }}', '{{ plans.3.monthlyPrice }}', '{{ plans.3.yearlyPrice }}', 1)"
class="cp-btn cp-btn-outline cp-btn-block mb-2">
Monthly via Card
</a>
<a href="javascript:void(0)"
ng-click="BuyNowBackupP('{{ plans.3.name }}', '{{ plans.3.monthlyPrice }}', '{{ plans.3.yearlyPrice }}', 12)"
class="cp-btn cp-btn-outline cp-btn-block">
Yearly via Card
</a>
</div>
</div>
</div>
<!-- 3TB Plan -->
<div class="cp-card">
<div class="cp-card-header">3TB</div>
<div class="cp-card-body">
<div class="price-box mb-3">
<div class="price-amount">${{ plans.4.monthlyPrice }}</div>
<div class="price-period">/month</div>
</div>
<div class="price-box mb-4">
<div class="price-amount">${{ plans.4.yearlyPrice }}</div>
<div class="price-period">/year</div>
</div>
<div class="btn-space">
<a href="javascript:void(0)"
ng-click="BuyNowBackupP('{{ plans.4.name }}', '{{ plans.4.monthlyPrice }}', '{{ plans.4.yearlyPrice }}', 1)"
class="cp-btn cp-btn-outline cp-btn-block mb-2">
Monthly via Card
</a>
<a href="javascript:void(0)"
ng-click="BuyNowBackupP('{{ plans.4.name }}', '{{ plans.4.monthlyPrice }}', '{{ plans.4.yearlyPrice }}', 12)"
class="cp-btn cp-btn-outline cp-btn-block">
Yearly via Card
</a>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
{% endblock %} {% endblock %}

View File

@@ -10,6 +10,7 @@ urlpatterns = [
re_path(r'^fetchOCSites$', views.fetchOCSites, name='fetchOCSites'), re_path(r'^fetchOCSites$', views.fetchOCSites, name='fetchOCSites'),
re_path(r'^StartOCRestore$', views.StartOCRestore, name='StartOCRestore'), re_path(r'^StartOCRestore$', views.StartOCRestore, name='StartOCRestore'),
re_path(r'^DeployAccount$', views.DeployAccount, name='DeployAccount'), re_path(r'^DeployAccount$', views.DeployAccount, name='DeployAccount'),
re_path(r'^ReconfigureSubscription$', views.ReconfigureSubscription, name='ReconfigureSubscription'),
re_path(r'^backupSite$', views.backupSite, name='backupSite'), re_path(r'^backupSite$', views.backupSite, name='backupSite'),
re_path(r'^restoreSite$', views.restoreSite, name='restoreSite'), re_path(r'^restoreSite$', views.restoreSite, name='restoreSite'),

View File

@@ -5,6 +5,7 @@
import json import json
from django.shortcuts import redirect from django.shortcuts import redirect
from django.http import HttpResponse
from backup.backupManager import BackupManager from backup.backupManager import BackupManager
from backup.pluginManager import pluginManager from backup.pluginManager import pluginManager
@@ -12,6 +13,8 @@ from loginSystem.views import loadLoginPage
import os import os
from plogical.CyberCPLogFileWriter import CyberCPLogFileWriter as logging from plogical.CyberCPLogFileWriter import CyberCPLogFileWriter as logging
from django.views.decorators.csrf import csrf_exempt from django.views.decorators.csrf import csrf_exempt
from django.contrib.auth.models import User
from loginSystem.models import Administrator
def loadBackupHome(request): def loadBackupHome(request):
try: try:
@@ -539,3 +542,14 @@ def DeployAccount(request):
return bm.DeployAccount(request, userID) return bm.DeployAccount(request, userID)
except KeyError: except KeyError:
return redirect(loadLoginPage) return redirect(loadLoginPage)
def ReconfigureSubscription(request):
try:
userID = request.session['userID']
bm = BackupManager()
data = json.loads(request.body)
return bm.ReconfigureSubscription(request, userID, data)
except BaseException as msg:
data_ret = {'status': 0, 'error_message': str(msg)}
json_data = json.dumps(data_ret)
return HttpResponse(json_data)

View File

@@ -77,7 +77,7 @@
<!-- HELPERS --> <!-- HELPERS -->
{% with version="2.3.8.1.1" %} {% with version="2.4.0" %}
<link rel="stylesheet" type="text/css" href="{% static 'baseTemplate/assets/finalBase/finalBase.css' %}"> <link rel="stylesheet" type="text/css" href="{% static 'baseTemplate/assets/finalBase/finalBase.css' %}">
@@ -87,7 +87,7 @@
<link rel="stylesheet" type="text/css" <link rel="stylesheet" type="text/css"
href="/static/baseTemplate/assets/themes/admin/color-schemes/default.css"> href="/static/baseTemplate/assets/themes/admin/color-schemes/default.css">
<link rel="stylesheet" type="text/css" <link rel="stylesheet" type="text/css"
href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/4.7.0/css/font-awesome.css"> href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/4.7.0/css/font-awesome.min.css">
<link rel="stylesheet" type="text/css" href="{% static 'baseTemplate/custom-js/pnotify.custom.min.css' %}"> <link rel="stylesheet" type="text/css" href="{% static 'baseTemplate/custom-js/pnotify.custom.min.css' %}">
<link rel="stylesheet" type="text/css" href="{% static 'websiteFunctions/websiteFunctions.css' %}"> <link rel="stylesheet" type="text/css" href="{% static 'websiteFunctions/websiteFunctions.css' %}">
<link rel="icon" type="image/x-icon" href="{% static 'baseTemplate/assets/finalBase/favicon.png' %}"> <link rel="icon" type="image/x-icon" href="{% static 'baseTemplate/assets/finalBase/favicon.png' %}">
@@ -248,7 +248,7 @@
title="{% trans 'Server IP Address' %}"> title="{% trans 'Server IP Address' %}">
<i class="glyph-icon tooltip-button icon-laptop" title="{% trans 'Server IP Address' %}" <i class="glyph-icon tooltip-button icon-laptop" title="{% trans 'Server IP Address' %}"
data-original-title=".icon-laptop"></i> data-original-title=".icon-laptop"></i>
<span style="color: #488a3f;font-weight: bold;">{{ ipAddress }}</span> <span onclick="copyIPAddress(); return false;" style="color: #488a3f; font-weight: bold; cursor: pointer;" title="{% trans 'Click to copy IP' %}">{{ ipAddress }}</span>
</a> </a>
<a id="sidebar-menu-item-dashboard" href="{% url 'index' %}" <a id="sidebar-menu-item-dashboard" href="{% url 'index' %}"
title="{% trans 'Dashboard' %}"> title="{% trans 'Dashboard' %}">
@@ -376,7 +376,6 @@
<a href="#" title="{% trans 'Dockersite' %}"> <a href="#" title="{% trans 'Dockersite' %}">
<div class="glyph-icon icon-globe" title="{% trans 'Docker Apps' %}"></div> <div class="glyph-icon icon-globe" title="{% trans 'Docker Apps' %}"></div>
<span>{% trans "Docker Apps" %}</span> <span>{% trans "Docker Apps" %}</span>
<span class="bs-label badge-yellow">{% trans "Beta" %}</span>
</a> </a>
<div class="sidebar-submenu"> <div class="sidebar-submenu">
@@ -1198,6 +1197,7 @@
<script src="{% static 'baseTemplate/custom-js/pnotify.custom.min.js' %}"></script> <script src="{% static 'baseTemplate/custom-js/pnotify.custom.min.js' %}"></script>
<script src="{% static 'packages/packages.js' %}?ver={{ version }}"></script> <script src="{% static 'packages/packages.js' %}?ver={{ version }}"></script>
<script src="{% static 'websiteFunctions/websiteFunctions.js' %}?ver={{ version }}"></script> <script src="{% static 'websiteFunctions/websiteFunctions.js' %}?ver={{ version }}"></script>
<script type="text/javascript" src="{% static 'websiteFunctions/DockerContainers.js' %}?ver={{ version }}"></script>
<script src="{% static 'tuning/tuning.js' %}?ver={{ version }}"></script> <script src="{% static 'tuning/tuning.js' %}?ver={{ version }}"></script>
<script src="{% static 'serverStatus/serverStatus.js' %}?ver={{ version }}"></script> <script src="{% static 'serverStatus/serverStatus.js' %}?ver={{ version }}"></script>
<script src="{% static 'dns/dns.js' %}?ver={{ version }}"></script> <script src="{% static 'dns/dns.js' %}?ver={{ version }}"></script>
@@ -1228,5 +1228,27 @@
</div> </div>
{% block footer_scripts %} {% block footer_scripts %}
{% endblock %} {% endblock %}
<script type="text/javascript">
function copyIPAddress() {
const ipAddress = '{{ ipAddress }}';
navigator.clipboard.writeText(ipAddress).then(function() {
// Show success notification using PNotify
new PNotify({
title: 'Success',
text: 'IP Address copied to clipboard!',
type: 'success',
delay: 2000
});
}).catch(function(err) {
// Show error notification using PNotify
new PNotify({
title: 'Error',
text: 'Failed to copy IP address',
type: 'error',
delay: 2000
});
});
}
</script>
</body> </body>
</html> </html>

View File

@@ -20,8 +20,8 @@ from plogical.httpProc import httpProc
# Create your views here. # Create your views here.
VERSION = '2.3' VERSION = '2.4'
BUILD = 9 BUILD = 0
@ensure_csrf_cookie @ensure_csrf_cookie

View File

@@ -765,7 +765,7 @@ else
Check_Return Check_Return
fi fi
wget https://cyberpanel.sh/www.litespeedtech.com/packages/lsapi/wsgi-lsapi-2.1.tgz wget https://www.litespeedtech.com/packages/lsapi/wsgi-lsapi-2.1.tgz
tar xf wsgi-lsapi-2.1.tgz tar xf wsgi-lsapi-2.1.tgz
cd wsgi-lsapi-2.1 || exit cd wsgi-lsapi-2.1 || exit
/usr/local/CyberPanel/bin/python ./configure.py /usr/local/CyberPanel/bin/python ./configure.py

View File

@@ -3,6 +3,7 @@
import os.path import os.path
import sys import sys
import django import django
from datetime import datetime
from plogical.DockerSites import Docker_Sites from plogical.DockerSites import Docker_Sites
@@ -1116,27 +1117,68 @@ class ContainerManager(multi.Thread):
if admin.acl.adminStatus != 1: if admin.acl.adminStatus != 1:
return ACLManager.loadError() return ACLManager.loadError()
name = data['name'] name = data['name']
containerID = data['id'] containerID = data['id']
passdata = {} # Create a Docker client
passdata["JobID"] = None client = docker.from_env()
passdata['name'] = name container = client.containers.get(containerID)
passdata['containerID'] = containerID
da = Docker_Sites(None, passdata)
retdata = da.ContainerInfo()
# Get detailed container info
container_info = container.attrs
data_ret = {'status': 1, 'error_message': 'None', 'data':retdata} # Calculate uptime
started_at = container_info.get('State', {}).get('StartedAt', '')
if started_at:
started_time = datetime.strptime(started_at.split('.')[0], '%Y-%m-%dT%H:%M:%S')
uptime = datetime.now() - started_time
uptime_str = str(uptime).split('.')[0] # Format as HH:MM:SS
else:
uptime_str = "N/A"
# Get container details
details = {
'id': container.short_id,
'name': container.name,
'status': container.status,
'created': container_info.get('Created', ''),
'started_at': started_at,
'uptime': uptime_str,
'image': container_info.get('Config', {}).get('Image', ''),
'ports': container_info.get('NetworkSettings', {}).get('Ports', {}),
'volumes': container_info.get('Mounts', []),
'environment': self._mask_sensitive_env(container_info.get('Config', {}).get('Env', [])),
'memory_usage': container.stats(stream=False)['memory_stats'].get('usage', 0),
'cpu_usage': container.stats(stream=False)['cpu_stats']['cpu_usage'].get('total_usage', 0)
}
data_ret = {'status': 1, 'error_message': 'None', 'data': [1, details]}
json_data = json.dumps(data_ret) json_data = json.dumps(data_ret)
return HttpResponse(json_data) return HttpResponse(json_data)
except BaseException as msg: except BaseException as msg:
data_ret = {'removeImageStatus': 0, 'error_message': str(msg)} data_ret = {'status': 0, 'error_message': str(msg)}
json_data = json.dumps(data_ret) json_data = json.dumps(data_ret)
return HttpResponse(json_data) return HttpResponse(json_data)
def _mask_sensitive_env(self, env_vars):
"""Helper method to mask sensitive data in environment variables"""
masked_vars = []
sensitive_keywords = ['password', 'secret', 'key', 'token', 'auth']
for var in env_vars:
if '=' in var:
name, value = var.split('=', 1)
# Check if this is a sensitive variable
if any(keyword in name.lower() for keyword in sensitive_keywords):
masked_vars.append(f"{name}=********")
else:
masked_vars.append(var)
else:
masked_vars.append(var)
return masked_vars
def getContainerApplog(self, userID=None, data=None): def getContainerApplog(self, userID=None, data=None):
try: try:
admin = Administrator.objects.get(pk=userID) admin = Administrator.objects.get(pk=userID)

View File

@@ -1,7 +1,7 @@
from django.urls import path, re_path from django.urls import path, re_path
from . import views from . import views
from websiteFunctions.views import Dockersitehome from websiteFunctions.views import Dockersitehome, startContainer, stopContainer, restartContainer
urlpatterns = [ urlpatterns = [
re_path(r'^$', views.loadDockerHome, name='dockerHome'), re_path(r'^$', views.loadDockerHome, name='dockerHome'),
@@ -27,7 +27,7 @@ urlpatterns = [
re_path(r'^recreateContainer$', views.recreateContainer, name='recreateContainer'), re_path(r'^recreateContainer$', views.recreateContainer, name='recreateContainer'),
re_path(r'^installDocker$', views.installDocker, name='installDocker'), re_path(r'^installDocker$', views.installDocker, name='installDocker'),
re_path(r'^images$', views.images, name='containerImage'), re_path(r'^images$', views.images, name='containerImage'),
re_path(r'^view/(?P<name>.+)$', views.viewContainer, name='viewContainer'), re_path(r'^view/(?P<n>.+)$', views.viewContainer, name='viewContainer'),
path('manage/<int:dockerapp>/app', Dockersitehome, name='Dockersitehome'), path('manage/<int:dockerapp>/app', Dockersitehome, name='Dockersitehome'),
path('getDockersiteList', views.getDockersiteList, name='getDockersiteList'), path('getDockersiteList', views.getDockersiteList, name='getDockersiteList'),
@@ -36,4 +36,9 @@ urlpatterns = [
path('recreateappcontainer', views.recreateappcontainer, name='recreateappcontainer'), path('recreateappcontainer', views.recreateappcontainer, name='recreateappcontainer'),
path('RestartContainerAPP', views.RestartContainerAPP, name='RestartContainerAPP'), path('RestartContainerAPP', views.RestartContainerAPP, name='RestartContainerAPP'),
path('StopContainerAPP', views.StopContainerAPP, name='StopContainerAPP'), path('StopContainerAPP', views.StopContainerAPP, name='StopContainerAPP'),
# Docker Container Actions
path('startContainer', startContainer, name='startContainer'),
path('stopContainer', stopContainer, name='stopContainer'),
path('restartContainer', restartContainer, name='restartContainer'),
] ]

View File

@@ -14,8 +14,8 @@ from os.path import *
from stat import * from stat import *
import stat import stat
VERSION = '2.3' VERSION = '2.4'
BUILD = 9 BUILD = 0
char_set = {'small': 'abcdefghijklmnopqrstuvwxyz', 'nums': '0123456789', 'big': 'ABCDEFGHIJKLMNOPQRSTUVWXYZ'} char_set = {'small': 'abcdefghijklmnopqrstuvwxyz', 'nums': '0123456789', 'big': 'ABCDEFGHIJKLMNOPQRSTUVWXYZ'}

View File

@@ -16,8 +16,8 @@ from django.http import HttpResponse
from django.utils import translation from django.utils import translation
# Create your views here. # Create your views here.
VERSION = '2.3' VERSION = '2.4'
BUILD = 9 BUILD = 0
def verifyLogin(request): def verifyLogin(request):

View File

@@ -4,6 +4,9 @@ import os
import sys import sys
import time import time
from random import randint from random import randint
import socket
import shutil
import docker
sys.path.append('/usr/local/CyberCP') sys.path.append('/usr/local/CyberCP')
@@ -24,11 +27,25 @@ from plogical.CyberCPLogFileWriter import CyberCPLogFileWriter as logging
import argparse import argparse
import threading as multi import threading as multi
class DockerDeploymentError(Exception):
def __init__(self, message, error_code=None, recovery_possible=True):
self.message = message
self.error_code = error_code
self.recovery_possible = recovery_possible
super().__init__(self.message)
class Docker_Sites(multi.Thread): class Docker_Sites(multi.Thread):
Wordpress = 1 Wordpress = 1
Joomla = 2 Joomla = 2
# Error codes
ERROR_DOCKER_NOT_INSTALLED = 'DOCKER_NOT_INSTALLED'
ERROR_PORT_IN_USE = 'PORT_IN_USE'
ERROR_CONTAINER_FAILED = 'CONTAINER_FAILED'
ERROR_NETWORK_FAILED = 'NETWORK_FAILED'
ERROR_VOLUME_FAILED = 'VOLUME_FAILED'
ERROR_DB_FAILED = 'DB_FAILED'
def __init__(self, function_run, data): def __init__(self, function_run, data):
multi.Thread.__init__(self) multi.Thread.__init__(self)
self.function_run = function_run self.function_run = function_run
@@ -165,15 +182,54 @@ class Docker_Sites(multi.Thread):
return 0, ReturnCode return 0, ReturnCode
else: else:
command = 'apt install docker-compose -y' # Add Docker's official GPG key
command = 'curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo gpg --dearmor -o /usr/share/keyrings/docker-archive-keyring.gpg'
ReturnCode = ProcessUtilities.executioner(command) ReturnCode = ProcessUtilities.executioner(command, 'root', True)
if not ReturnCode:
if ReturnCode:
return 1, None
else:
return 0, ReturnCode return 0, ReturnCode
# Add Docker repository
command = 'echo "deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/docker-archive-keyring.gpg] https://download.docker.com/linux/ubuntu $(lsb_release -cs) stable" | sudo tee /etc/apt/sources.list.d/docker.list > /dev/null'
ReturnCode = ProcessUtilities.executioner(command, 'root', True)
if not ReturnCode:
return 0, ReturnCode
# Update package index
command = 'apt-get update'
ReturnCode = ProcessUtilities.executioner(command)
if not ReturnCode:
return 0, ReturnCode
# Install Docker packages
command = 'apt-get install -y docker-ce docker-ce-cli containerd.io docker-compose-plugin'
ReturnCode = ProcessUtilities.executioner(command)
if not ReturnCode:
return 0, ReturnCode
# Enable and start Docker service
command = 'systemctl enable docker'
ReturnCode = ProcessUtilities.executioner(command)
if not ReturnCode:
return 0, ReturnCode
command = 'systemctl start docker'
ReturnCode = ProcessUtilities.executioner(command)
if not ReturnCode:
return 0, ReturnCode
# Install Docker Compose
command = 'curl -L "https://github.com/docker/compose/releases/download/v2.23.2/docker-compose-linux-$(uname -m)" -o /usr/local/bin/docker-compose'
ReturnCode = ProcessUtilities.executioner(command, 'root', True)
if not ReturnCode:
return 0, ReturnCode
command = 'chmod +x /usr/local/bin/docker-compose'
ReturnCode = ProcessUtilities.executioner(command, 'root', True)
if not ReturnCode:
return 0, ReturnCode
return 1, None
@staticmethod @staticmethod
def SetupProxy(port): def SetupProxy(port):
import xml.etree.ElementTree as ET import xml.etree.ElementTree as ET
@@ -614,8 +670,6 @@ services:
### forcefully delete containers ### forcefully delete containers
import docker
# Create a Docker client # Create a Docker client
client = docker.from_env() client = docker.from_env()
@@ -651,30 +705,54 @@ services:
## This function need site name which was passed while creating the app ## This function need site name which was passed while creating the app
def ListContainers(self): def ListContainers(self):
try: try:
import docker
# Create a Docker client # Create a Docker client
client = docker.from_env() client = docker.from_env()
FilerValue = self.DockerAppName # Debug logging
if os.path.exists(ProcessUtilities.debugPath):
logging.writeToFile(f'DockerAppName: {self.DockerAppName}')
# Define the label to filter containers # List all containers without filtering first
label_filter = {'name': FilerValue} all_containers = client.containers.list(all=True)
if os.path.exists(ProcessUtilities.debugPath):
logging.writeToFile(f'Total containers found: {len(all_containers)}')
for container in all_containers:
logging.writeToFile(f'Container name: {container.name}')
# List containers matching the label filter # Now filter containers - handle both CentOS and Ubuntu naming
containers = client.containers.list(filters=label_filter) containers = []
# Get both possible name formats
if ProcessUtilities.decideDistro() == ProcessUtilities.centos or ProcessUtilities.decideDistro() == ProcessUtilities.cent8:
search_name = self.DockerAppName # Already in hyphen format for CentOS
else:
# For Ubuntu, convert underscore to hyphen as containers use hyphens
search_name = self.DockerAppName.replace('_', '-')
if os.path.exists(ProcessUtilities.debugPath):
logging.writeToFile(f'Searching for containers with name containing: {search_name}')
for container in all_containers:
if os.path.exists(ProcessUtilities.debugPath):
logging.writeToFile(f'Checking container: {container.name} against filter: {search_name}')
if search_name.lower() in container.name.lower():
containers.append(container)
if os.path.exists(ProcessUtilities.debugPath):
logging.writeToFile(f'Filtered containers count: {len(containers)}')
json_data = "[" json_data = "["
checker = 0 checker = 0
for container in containers: for container in containers:
try:
dic = { dic = {
'id': container.short_id, 'id': container.short_id,
'name': container.name, 'name': container.name,
'status': container.status, 'status': container.status,
'state': container.attrs.get('State', {}),
'health': container.attrs.get('State', {}).get('Health', {}).get('Status', 'unknown'),
'volumes': container.attrs['HostConfig']['Binds'] if 'HostConfig' in container.attrs else [], 'volumes': container.attrs['HostConfig']['Binds'] if 'HostConfig' in container.attrs else [],
'logs_50': container.logs(tail=50).decode('utf-8'), 'logs_50': container.logs(tail=50).decode('utf-8'),
'ports': container.attrs['HostConfig']['PortBindings'] if 'HostConfig' in container.attrs else {} 'ports': container.attrs['HostConfig']['PortBindings'] if 'HostConfig' in container.attrs else {}
@@ -685,9 +763,15 @@ services:
checker = 1 checker = 1
else: else:
json_data = json_data + ',' + json.dumps(dic) json_data = json_data + ',' + json.dumps(dic)
except Exception as e:
logging.writeToFile(f"Error processing container {container.name}: {str(e)}")
continue
json_data = json_data + ']' json_data = json_data + ']'
if os.path.exists(ProcessUtilities.debugPath):
logging.writeToFile(f'Final JSON data: {json_data}')
return 1, json_data return 1, json_data
except BaseException as msg: except BaseException as msg:
@@ -697,7 +781,6 @@ services:
### pass container id and number of lines to fetch from logs ### pass container id and number of lines to fetch from logs
def ContainerLogs(self): def ContainerLogs(self):
try: try:
import docker
# Create a Docker client # Create a Docker client
client = docker.from_env() client = docker.from_env()
@@ -716,7 +799,6 @@ services:
def ContainerInfo(self): def ContainerInfo(self):
try: try:
import docker
# Create a Docker client # Create a Docker client
client = docker.from_env() client = docker.from_env()
@@ -748,7 +830,6 @@ services:
def RestartContainer(self): def RestartContainer(self):
try: try:
import docker
# Create a Docker client # Create a Docker client
client = docker.from_env() client = docker.from_env()
@@ -764,7 +845,6 @@ services:
def StopContainer(self): def StopContainer(self):
try: try:
import docker
# Create a Docker client # Create a Docker client
client = docker.from_env() client = docker.from_env()
@@ -780,102 +860,367 @@ services:
##### N8N Container ##### N8N Container
def DeployN8NContainer(self): def check_container_health(self, container_name, max_retries=3, delay=80):
"""
Check if a container is running, accepting healthy, unhealthy, and starting states
Total wait time will be 4 minutes (3 retries * 80 seconds)
"""
try: try:
# Format container name to match Docker's naming convention
formatted_name = f"{self.data['ServiceName']}-{container_name}-1"
logging.writeToFile(f'Checking container health for: {formatted_name}')
logging.statusWriter(self.JobID, 'Checking if Docker is installed..,0') for attempt in range(max_retries):
client = docker.from_env()
container = client.containers.get(formatted_name)
if container.status == 'running':
health = container.attrs.get('State', {}).get('Health', {}).get('Status')
# Accept healthy, unhealthy, and starting states as long as container is running
if health in ['healthy', 'unhealthy', 'starting'] or health is None:
logging.writeToFile(f'Container {formatted_name} is running with status: {health}')
return True
else:
health_logs = container.attrs.get('State', {}).get('Health', {}).get('Log', [])
if health_logs:
last_log = health_logs[-1]
logging.writeToFile(f'Container health check failed: {last_log.get("Output", "")}')
logging.writeToFile(f'Container {formatted_name} status: {container.status}, health: {health}, attempt {attempt + 1}/{max_retries}')
time.sleep(delay)
return False
except docker.errors.NotFound:
logging.writeToFile(f'Container {formatted_name} not found')
return False
except Exception as e:
logging.writeToFile(f'Error checking container health: {str(e)}')
return False
def verify_system_resources(self):
try:
# Check available disk space using root access
command = "df -B 1G /home/docker --output=avail | tail -1"
result, output = ProcessUtilities.outputExecutioner(command, None, None, None, 1)
if result == 0:
raise DockerDeploymentError("Failed to check disk space")
available_gb = int(output.strip())
if available_gb < 5: # Require minimum 5GB free space
raise DockerDeploymentError(
f"Insufficient disk space. Need at least 5GB but only {available_gb}GB available.",
self.ERROR_VOLUME_FAILED
)
# Check if Docker is running and accessible
command = "systemctl is-active docker"
result, docker_status = ProcessUtilities.outputExecutioner(command, None, None, None, 1)
if result == 0:
raise DockerDeploymentError("Failed to check Docker status")
if docker_status.strip() != "active":
raise DockerDeploymentError("Docker service is not running")
# Check Docker system info for resource limits
command = "docker info --format '{{.MemTotal}}'"
result, total_memory = ProcessUtilities.outputExecutioner(command, None, None, None, 1)
if result == 0:
raise DockerDeploymentError("Failed to get Docker memory info")
# Convert total_memory from bytes to MB
total_memory_mb = int(total_memory.strip()) / (1024 * 1024)
# Calculate required memory from site and MySQL requirements
required_memory = int(self.data['MemoryMySQL']) + int(self.data['MemorySite'])
if total_memory_mb < required_memory:
raise DockerDeploymentError(
f"Insufficient memory. Need {required_memory}MB but only {int(total_memory_mb)}MB available",
'INSUFFICIENT_MEMORY'
)
# Verify Docker group and permissions
command = "getent group docker"
result, docker_group = ProcessUtilities.outputExecutioner(command, None, None, None, 1)
if result == 0 or not docker_group:
raise DockerDeploymentError("Docker group does not exist")
return True
except DockerDeploymentError as e:
raise e
except Exception as e:
raise DockerDeploymentError(f"Resource verification failed: {str(e)}")
def setup_docker_environment(self):
try:
# Create docker directory with root
command = f"mkdir -p /home/docker/{self.data['finalURL']}"
ProcessUtilities.outputExecutioner(command, None, None, None, 1)
# Set proper permissions
command = f"chown -R {self.data['externalApp']}:docker /home/docker/{self.data['finalURL']}"
ProcessUtilities.outputExecutioner(command, None, None, None, 1)
# Create docker network if doesn't exist
command = "docker network ls | grep cyberpanel"
network_exists = ProcessUtilities.outputExecutioner(command, None, None, None, 1)
if not network_exists:
command = "docker network create cyberpanel"
ProcessUtilities.outputExecutioner(command, None, None, None, 1)
return True
except Exception as e:
raise DockerDeploymentError(f"Environment setup failed: {str(e)}")
def deploy_containers(self):
try:
# Write docker-compose file
command = f"cat > {self.data['ComposePath']} << 'EOF'\n{self.data['ComposeContent']}\nEOF"
ProcessUtilities.outputExecutioner(command, None, None, None, 1)
# Set proper permissions on compose file
command = f"chmod 600 {self.data['ComposePath']} && chown root:root {self.data['ComposePath']}"
ProcessUtilities.outputExecutioner(command, None, None, None, 1)
# Deploy with docker-compose
command = f"cd {os.path.dirname(self.data['ComposePath'])} && docker-compose up -d"
result = ProcessUtilities.outputExecutioner(command, None, None, None, 1)
if "error" in result.lower():
raise DockerDeploymentError(f"Container deployment failed: {result}")
return True
except Exception as e:
raise DockerDeploymentError(f"Deployment failed: {str(e)}")
def cleanup_failed_deployment(self):
try:
# Stop and remove containers
command = f"cd {os.path.dirname(self.data['ComposePath'])} && docker-compose down -v"
ProcessUtilities.outputExecutioner(command, None, None, None, 1)
# Remove docker directory
command = f"rm -rf /home/docker/{self.data['finalURL']}"
ProcessUtilities.outputExecutioner(command, None, None, None, 1)
# Remove compose file
command = f"rm -f {self.data['ComposePath']}"
ProcessUtilities.outputExecutioner(command, None, None, None, 1)
return True
except Exception as e:
logging.writeToFile(f"Cleanup failed: {str(e)}")
return False
def monitor_deployment(self):
try:
# Format container names
n8n_container_name = f"{self.data['ServiceName']}-{self.data['ServiceName']}-1"
db_container_name = f"{self.data['ServiceName']}-{self.data['ServiceName']}-db-1"
logging.writeToFile(f'Monitoring containers: {n8n_container_name} and {db_container_name}')
# Check container health
command = f"docker ps -a --filter name={self.data['ServiceName']} --format '{{{{.Status}}}}'"
result, status = ProcessUtilities.outputExecutioner(command, None, None, None, 1)
# Only raise error if container is exited
if "exited" in status:
# Get container logs
command = f"docker logs {n8n_container_name}"
result, logs = ProcessUtilities.outputExecutioner(command, None, None, None, 1)
raise DockerDeploymentError(f"Container exited. Logs: {logs}")
# Wait for database to be ready
max_retries = 30
retry_count = 0
db_ready = False
while retry_count < max_retries:
# Check if database container is ready
command = f"docker exec {db_container_name} pg_isready -U postgres"
result, output = ProcessUtilities.outputExecutioner(command, None, None, None, 1)
if "accepting connections" in output:
db_ready = True
break
# Check container status
command = f"docker inspect --format='{{{{.State.Status}}}}' {db_container_name}"
result, db_status = ProcessUtilities.outputExecutioner(command, None, None, None, 1)
# Only raise error if database container is in a failed state
if db_status == 'exited':
raise DockerDeploymentError(f"Database container is in {db_status} state")
retry_count += 1
time.sleep(2)
logging.writeToFile(f'Waiting for database to be ready, attempt {retry_count}/{max_retries}')
if not db_ready:
raise DockerDeploymentError("Database failed to become ready within timeout period")
# Check n8n container status
command = f"docker inspect --format='{{{{.State.Status}}}}' {n8n_container_name}"
result, n8n_status = ProcessUtilities.outputExecutioner(command, None, None, None, 1)
# Only raise error if n8n container is in a failed state
if n8n_status == 'exited':
raise DockerDeploymentError(f"n8n container is in {n8n_status} state")
logging.writeToFile(f'Deployment monitoring completed successfully. n8n status: {n8n_status}, database ready: {db_ready}')
return True
except Exception as e:
logging.writeToFile(f'Error during monitoring: {str(e)}')
raise DockerDeploymentError(f"Monitoring failed: {str(e)}")
def handle_deployment_failure(self, error, cleanup=True):
"""
Handle deployment failures and attempt recovery
"""
try:
logging.writeToFile(f'Deployment failed: {str(error)}')
if cleanup:
self.cleanup_failed_deployment()
if isinstance(error, DockerDeploymentError):
if error.error_code == self.ERROR_DOCKER_NOT_INSTALLED:
# Attempt to install Docker
execPath = "/usr/local/CyberCP/bin/python /usr/local/CyberCP/dockerManager/dockerInstall.py"
ProcessUtilities.executioner(execPath)
return True
elif error.error_code == self.ERROR_PORT_IN_USE:
# Find next available port
new_port = int(self.data['port']) + 1
while new_port < 65535:
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
result = sock.connect_ex(('127.0.0.1', new_port))
sock.close()
if result != 0:
self.data['port'] = str(new_port)
return True
new_port += 1
elif error.error_code == self.ERROR_DB_FAILED:
# Attempt database recovery
return self.recover_database()
return False
except Exception as e:
logging.writeToFile(f'Error during failure handling: {str(e)}')
return False
def recover_database(self):
"""
Attempt to recover the database container
"""
try:
client = docker.from_env()
db_container_name = f"{self.data['ServiceName']}-db"
try:
db_container = client.containers.get(db_container_name)
if db_container.status == 'running':
exec_result = db_container.exec_run(
'pg_isready -U postgres'
)
if exec_result.exit_code != 0:
db_container.restart()
time.sleep(10)
if self.check_container_health(db_container_name):
return True
except docker.errors.NotFound:
pass
return False
except Exception as e:
logging.writeToFile(f'Database recovery failed: {str(e)}')
return False
def log_deployment_metrics(self, metrics):
"""
Log deployment metrics for analysis
"""
if metrics:
try:
log_file = f"/var/log/cyberpanel/docker/{self.data['ServiceName']}_metrics.json"
os.makedirs(os.path.dirname(log_file), exist_ok=True)
with open(log_file, 'w') as f:
json.dump(metrics, f, indent=2)
except Exception as e:
logging.writeToFile(f'Error logging metrics: {str(e)}')
def DeployN8NContainer(self):
"""
Main deployment method with error handling
"""
max_retries = 3
current_try = 0
while current_try < max_retries:
try:
logging.statusWriter(self.JobID, 'Starting deployment verification...,0')
# Check Docker installation
command = 'docker --help' command = 'docker --help'
result = ProcessUtilities.outputExecutioner(command) result = ProcessUtilities.outputExecutioner(command)
if os.path.exists(ProcessUtilities.debugPath):
logging.writeToFile(f'return code of docker install {result}')
if result.find("not found") > -1: if result.find("not found") > -1:
if os.path.exists(ProcessUtilities.debugPath): if os.path.exists(ProcessUtilities.debugPath):
logging.writeToFile(f'About to run docker install function...') logging.writeToFile(f'About to run docker install function...')
execPath = "/usr/local/CyberCP/bin/python /usr/local/CyberCP/dockerManager/dockerInstall.py" # Call InstallDocker to install Docker
ProcessUtilities.executioner(execPath) install_result, error = self.InstallDocker()
if not install_result:
logging.statusWriter(self.JobID, 'Docker is ready to use..,10') logging.statusWriter(self.JobID, f'Failed to install Docker: {error} [404]')
self.data['ServiceName'] = self.data["SiteName"].replace(' ', '-')
WPSite = f'''
version: '3.8'
volumes:
db_storage:
n8n_storage:
services:
'{self.data['ServiceName']}-db':
image: docker.io/bitnami/postgresql:16
user: root
restart: always
environment:
# - POSTGRES_USER:root
- POSTGRESQL_USERNAME={self.data['MySQLDBNUser']}
- POSTGRESQL_DATABASE={self.data['MySQLDBName']}
- POSTGRESQL_POSTGRES_PASSWORD={self.data['MySQLPassword']}
- POSTGRESQL_PASSWORD={self.data['MySQLPassword']}
volumes:
# - "/home/docker/{self.data['finalURL']}/db:/var/lib/postgresql/data"
- "/home/docker/{self.data['finalURL']}/db:/bitnami/postgresql"
'{self.data['ServiceName']}':
image: docker.n8n.io/n8nio/n8n
user: root
restart: always
environment:
- DB_TYPE=postgresdb
- DB_POSTGRESDB_HOST={self.data['ServiceName']}-db
- DB_POSTGRESDB_PORT=5432
- DB_POSTGRESDB_DATABASE={self.data['MySQLDBName']}
- DB_POSTGRESDB_USER={self.data['MySQLDBNUser']}
- DB_POSTGRESDB_PASSWORD={self.data['MySQLPassword']}
- N8N_HOST={self.data['finalURL']}
- NODE_ENV=production
- WEBHOOK_URL=https://{self.data['finalURL']}
- N8N_PUSH_BACKEND=sse # Use Server-Sent Events instead of WebSockets
ports:
- "{self.data['port']}:5678"
links:
- {self.data['ServiceName']}-db
volumes:
- "/home/docker/{self.data['finalURL']}/data:/home/node/.n8n"
depends_on:
- '{self.data['ServiceName']}-db'
'''
### WriteConfig to compose-file
command = f"mkdir -p /home/docker/{self.data['finalURL']}"
result, message = ProcessUtilities.outputExecutioner(command, None, None, None, 1)
if result == 0:
logging.statusWriter(self.JobID, f'Error {str(message)} . [404]')
return 0 return 0
TempCompose = f'/home/cyberpanel/{self.data["finalURL"]}-docker-compose.yml' logging.statusWriter(self.JobID, 'Docker installation verified...,20')
WriteToFile = open(TempCompose, 'w') # Verify system resources
WriteToFile.write(WPSite) self.verify_system_resources()
WriteToFile.close() logging.statusWriter(self.JobID, 'System resources verified...,10')
# Create directories
command = f"mkdir -p /home/docker/{self.data['finalURL']}"
result, message = ProcessUtilities.outputExecutioner(command, None, None, None, 1)
if result == 0:
raise DockerDeploymentError(f"Failed to create directories: {message}")
logging.statusWriter(self.JobID, 'Directories created...,30')
# Generate and write docker-compose file
self.data['ServiceName'] = self.data["SiteName"].replace(' ', '-')
compose_config = self.generate_compose_config()
TempCompose = f'/home/cyberpanel/{self.data["finalURL"]}-docker-compose.yml'
with open(TempCompose, 'w') as f:
f.write(compose_config)
command = f"mv {TempCompose} {self.data['ComposePath']}" command = f"mv {TempCompose} {self.data['ComposePath']}"
result, message = ProcessUtilities.outputExecutioner(command, None, None, None, 1) result, message = ProcessUtilities.outputExecutioner(command, None, None, None, 1)
if result == 0: if result == 0:
logging.statusWriter(self.JobID, f'Error {str(message)} . [404]') raise DockerDeploymentError(f"Failed to move compose file: {message}")
return 0
command = f"chmod 600 {self.data['ComposePath']} && chown root:root {self.data['ComposePath']}" command = f"chmod 600 {self.data['ComposePath']} && chown root:root {self.data['ComposePath']}"
ProcessUtilities.executioner(command, 'root', True) ProcessUtilities.executioner(command, 'root', True)
logging.statusWriter(self.JobID, 'Docker compose file created...,40')
#### # Deploy containers
if ProcessUtilities.decideDistro() == ProcessUtilities.cent8 or ProcessUtilities.decideDistro() == ProcessUtilities.centos: if ProcessUtilities.decideDistro() == ProcessUtilities.cent8 or ProcessUtilities.decideDistro() == ProcessUtilities.centos:
dockerCommand = 'docker compose' dockerCommand = 'docker compose'
else: else:
@@ -883,69 +1228,182 @@ services:
command = f"{dockerCommand} -f {self.data['ComposePath']} -p '{self.data['SiteName']}' up -d" command = f"{dockerCommand} -f {self.data['ComposePath']} -p '{self.data['SiteName']}' up -d"
result, message = ProcessUtilities.outputExecutioner(command, None, None, None, 1) result, message = ProcessUtilities.outputExecutioner(command, None, None, None, 1)
if result == 0: if result == 0:
logging.statusWriter(self.JobID, f'Error {str(message)} . [404]') raise DockerDeploymentError(f"Failed to deploy containers: {message}")
return 0 logging.statusWriter(self.JobID, 'Containers deployed...,60')
logging.statusWriter(self.JobID, 'Bringing containers online..,50')
# Wait for containers to be healthy
time.sleep(25) time.sleep(25)
if not self.check_container_health(f"{self.data['ServiceName']}-db") or \
not self.check_container_health(self.data['ServiceName']):
raise DockerDeploymentError("Containers failed to reach healthy state", self.ERROR_CONTAINER_FAILED)
logging.statusWriter(self.JobID, 'Containers healthy...,70')
# Setup proxy
### checking if everything ran properly
passdata = {}
passdata["JobID"] = None
passdata['name'] = self.data['ServiceName']
da = Docker_Sites(None, passdata)
retdata, containers = da.ListContainers()
containers = json.loads(containers)
if os.path.exists(ProcessUtilities.debugPath):
logging.writeToFile(str(containers))
### it means less then two containers which means something went wrong
if len(containers) < 2:
logging.writeToFile(f'Unkonwn error, containers not running. [DeployN8NContainer] . [404]')
logging.statusWriter(self.JobID, f'Unkonwn error, containers not running. [DeployN8NContainer] . [404]')
return 0
### Set up Proxy
execPath = "/usr/local/CyberCP/bin/python /usr/local/CyberCP/plogical/DockerSites.py" execPath = "/usr/local/CyberCP/bin/python /usr/local/CyberCP/plogical/DockerSites.py"
execPath = execPath + f" SetupProxy --port {self.data['port']}" execPath = execPath + f" SetupProxy --port {self.data['port']}"
ProcessUtilities.executioner(execPath) ProcessUtilities.executioner(execPath)
logging.statusWriter(self.JobID, 'Proxy configured...,80')
### Set up ht access # Setup ht access
execPath = "/usr/local/CyberCP/bin/python /usr/local/CyberCP/plogical/DockerSites.py" execPath = "/usr/local/CyberCP/bin/python /usr/local/CyberCP/plogical/DockerSites.py"
execPath = execPath + f" SetupHTAccess --port {self.data['port']} --htaccess {self.data['htaccessPath']}" execPath = execPath + f" SetupHTAccess --port {self.data['port']} --htaccess {self.data['htaccessPath']}"
ProcessUtilities.executioner(execPath, self.data['externalApp']) ProcessUtilities.executioner(execPath, self.data['externalApp'])
logging.statusWriter(self.JobID, 'HTAccess configured...,90')
# if ProcessUtilities.decideDistro() == ProcessUtilities.centos or ProcessUtilities.decideDistro() == ProcessUtilities.cent8: # Restart web server
# group = 'nobody'
# else:
# group = 'nogroup'
#
# command = f"chown -R nobody:{group} /home/docker/{self.data['finalURL']}/data"
# ProcessUtilities.executioner(command)
### just restart ls for htaccess
from plogical.installUtilities import installUtilities from plogical.installUtilities import installUtilities
installUtilities.reStartLiteSpeedSocket() installUtilities.reStartLiteSpeedSocket()
logging.statusWriter(self.JobID, 'Completed. [200]') # Monitor deployment
metrics = self.monitor_deployment()
self.log_deployment_metrics(metrics)
except BaseException as msg: logging.statusWriter(self.JobID, 'Deployment completed successfully. [200]')
logging.writeToFile(f'{str(msg)}. [DeployN8NContainer]') return True
logging.statusWriter(self.JobID, f'Error {str(msg)} . [404]')
print(str(msg))
pass
except DockerDeploymentError as e:
logging.writeToFile(f'Deployment error: {str(e)}')
if self.handle_deployment_failure(e):
current_try += 1
continue
else:
logging.statusWriter(self.JobID, f'Deployment failed: {str(e)} [404]')
return False
except Exception as e:
logging.writeToFile(f'Unexpected error: {str(e)}')
self.handle_deployment_failure(e)
logging.statusWriter(self.JobID, f'Deployment failed: {str(e)} [404]')
return False
logging.statusWriter(self.JobID, f'Deployment failed after {max_retries} attempts [404]')
return False
def generate_compose_config(self):
"""
Generate the docker-compose configuration with improved security and reliability
"""
postgres_config = {
'image': 'postgres:16-alpine',
'user': 'root',
'healthcheck': {
'test': ["CMD-SHELL", "pg_isready -U postgres"],
'interval': '10s',
'timeout': '5s',
'retries': 5,
'start_period': '30s'
},
'environment': {
'POSTGRES_USER': 'postgres',
'POSTGRES_PASSWORD': self.data['MySQLPassword'],
'POSTGRES_DB': self.data['MySQLDBName']
}
}
n8n_config = {
'image': 'docker.n8n.io/n8nio/n8n',
'user': 'root',
'healthcheck': {
'test': ["CMD", "wget", "--spider", "http://localhost:5678"],
'interval': '20s',
'timeout': '10s',
'retries': 3
},
'environment': {
'DB_TYPE': 'postgresdb',
'DB_POSTGRESDB_HOST': f"{self.data['ServiceName']}-db",
'DB_POSTGRESDB_PORT': '5432',
'DB_POSTGRESDB_DATABASE': self.data['MySQLDBName'],
'DB_POSTGRESDB_USER': 'postgres',
'DB_POSTGRESDB_PASSWORD': self.data['MySQLPassword'],
'N8N_HOST': self.data['finalURL'],
'NODE_ENV': 'production',
'WEBHOOK_URL': f"https://{self.data['finalURL']}",
'N8N_PUSH_BACKEND': 'sse',
'GENERIC_TIMEZONE': 'UTC',
'N8N_ENCRYPTION_KEY': 'auto',
'N8N_ENFORCE_SETTINGS_FILE_PERMISSIONS': 'true',
'DB_POSTGRESDB_SCHEMA': 'public'
}
}
return f'''version: '3.8'
volumes:
db_storage:
driver: local
n8n_storage:
driver: local
services:
'{self.data['ServiceName']}-db':
image: {postgres_config['image']}
user: {postgres_config['user']}
restart: always
healthcheck:
test: {postgres_config['healthcheck']['test']}
interval: {postgres_config['healthcheck']['interval']}
timeout: {postgres_config['healthcheck']['timeout']}
retries: {postgres_config['healthcheck']['retries']}
start_period: {postgres_config['healthcheck']['start_period']}
environment:
- POSTGRES_USER={postgres_config['environment']['POSTGRES_USER']}
- POSTGRES_PASSWORD={postgres_config['environment']['POSTGRES_PASSWORD']}
- POSTGRES_DB={postgres_config['environment']['POSTGRES_DB']}
volumes:
- "/home/docker/{self.data['finalURL']}/db:/var/lib/postgresql/data"
networks:
- n8n-network
deploy:
resources:
limits:
cpus: '{self.data["CPUsMySQL"]}'
memory: {self.data["MemoryMySQL"]}M
'{self.data['ServiceName']}':
image: {n8n_config['image']}
user: {n8n_config['user']}
restart: always
healthcheck:
test: {n8n_config['healthcheck']['test']}
interval: {n8n_config['healthcheck']['interval']}
timeout: {n8n_config['healthcheck']['timeout']}
retries: {n8n_config['healthcheck']['retries']}
environment:
- DB_TYPE={n8n_config['environment']['DB_TYPE']}
- DB_POSTGRESDB_HOST={n8n_config['environment']['DB_POSTGRESDB_HOST']}
- DB_POSTGRESDB_PORT={n8n_config['environment']['DB_POSTGRESDB_PORT']}
- DB_POSTGRESDB_DATABASE={n8n_config['environment']['DB_POSTGRESDB_DATABASE']}
- DB_POSTGRESDB_USER={n8n_config['environment']['DB_POSTGRESDB_USER']}
- DB_POSTGRESDB_PASSWORD={n8n_config['environment']['DB_POSTGRESDB_PASSWORD']}
- DB_POSTGRESDB_SCHEMA={n8n_config['environment']['DB_POSTGRESDB_SCHEMA']}
- N8N_HOST={n8n_config['environment']['N8N_HOST']}
- NODE_ENV={n8n_config['environment']['NODE_ENV']}
- WEBHOOK_URL={n8n_config['environment']['WEBHOOK_URL']}
- N8N_PUSH_BACKEND={n8n_config['environment']['N8N_PUSH_BACKEND']}
- GENERIC_TIMEZONE={n8n_config['environment']['GENERIC_TIMEZONE']}
- N8N_ENCRYPTION_KEY={n8n_config['environment']['N8N_ENCRYPTION_KEY']}
- N8N_ENFORCE_SETTINGS_FILE_PERMISSIONS={n8n_config['environment']['N8N_ENFORCE_SETTINGS_FILE_PERMISSIONS']}
ports:
- "{self.data['port']}:5678"
depends_on:
- {self.data['ServiceName']}-db
volumes:
- "/home/docker/{self.data['finalURL']}/data:/home/node/.n8n"
networks:
- n8n-network
deploy:
resources:
limits:
cpus: '{self.data["CPUsSite"]}'
memory: {self.data["MemorySite"]}M
networks:
n8n-network:
driver: bridge
name: {self.data['ServiceName']}_network'''
def Main(): def Main():
try: try:

View File

@@ -12,8 +12,8 @@ from plogical.acl import ACLManager
from packages.models import Package from packages.models import Package
from baseTemplate.models import version from baseTemplate.models import version
VERSION = '2.3' VERSION = '2.4'
BUILD = 9 BUILD = 0
if not os.geteuid() == 0: if not os.geteuid() == 0:
sys.exit("\nOnly root can run this script\n") sys.exit("\nOnly root can run this script\n")

View File

@@ -53,8 +53,8 @@ try:
except: except:
pass pass
VERSION = '2.3' VERSION = '2.4'
BUILD = 9 BUILD = 0
## I am not the monster that you think I am.. ## I am not the monster that you think I am..
@@ -739,7 +739,7 @@ class backupUtilities:
dbName = database.find('dbName').text dbName = database.find('dbName').text
if (VERSION == '2.1' or VERSION == '2.3') and int(BUILD) >= 1: if ((VERSION == '2.1' or VERSION == '2.3') and int(BUILD) >= 1) or (VERSION == '2.4' and int(BUILD) >= 0):
logging.CyberCPLogFileWriter.writeToFile('Backup version 2.1.1+ detected..') logging.CyberCPLogFileWriter.writeToFile('Backup version 2.1.1+ detected..')
databaseUsers = database.findall('databaseUsers') databaseUsers = database.findall('databaseUsers')
@@ -1073,7 +1073,7 @@ class backupUtilities:
dbName = database.find('dbName').text dbName = database.find('dbName').text
if (VERSION == '2.1' or VERSION == '2.3') and int(BUILD) >= 1: if ((VERSION == '2.1' or VERSION == '2.3') and int(BUILD) >= 1) or (VERSION == '2.4' and int(BUILD) >= 0):
logging.CyberCPLogFileWriter.writeToFile('Backup version 2.1.1+ detected..') logging.CyberCPLogFileWriter.writeToFile('Backup version 2.1.1+ detected..')

View File

@@ -17,8 +17,8 @@ from CyberCP import settings
import random import random
import string import string
VERSION = '2.3' VERSION = '2.4'
BUILD = 9 BUILD = 0
CENTOS7 = 0 CENTOS7 = 0
CENTOS8 = 1 CENTOS8 = 1

View File

@@ -25,7 +25,7 @@ from managePHP.phpManager import PHPManager
from plogical.vhostConfs import vhostConfs from plogical.vhostConfs import vhostConfs
from ApachController.ApacheVhosts import ApacheVhost from ApachController.ApacheVhosts import ApacheVhost
try: try:
from websiteFunctions.models import Websites, ChildDomains, aliasDomains from websiteFunctions.models import Websites, ChildDomains, aliasDomains, DockerSites
from databases.models import Databases from databases.models import Databases
except: except:
pass pass
@@ -404,6 +404,23 @@ class vhost:
if ACLManager.FindIfChild() == 0: if ACLManager.FindIfChild() == 0:
### Delete Docker Sites first before website deletion
if os.path.exists('/home/docker/%s' % (virtualHostName)):
try:
dockerSite = DockerSites.objects.get(admin__domain=virtualHostName)
passdata = {
"domain": virtualHostName,
"name": dockerSite.SiteName
}
from plogical.DockerSites import Docker_Sites
da = Docker_Sites(None, passdata)
da.DeleteDockerApp()
dockerSite.delete()
except:
# If anything fails in Docker cleanup, at least remove the directory
shutil.rmtree('/home/docker/%s' % (virtualHostName))
for items in databases: for items in databases:
mysqlUtilities.deleteDatabase(items.dbName, items.dbUser) mysqlUtilities.deleteDatabase(items.dbName, items.dbUser)

View File

@@ -25,8 +25,8 @@ EXPIRE = 3
### Version ### Version
VERSION = '2.3' VERSION = '2.4'
BUILD = 9 BUILD = 0
def serverStatusHome(request): def serverStatusHome(request):

View File

@@ -237,7 +237,7 @@ app.controller('createWebsite', function ($scope, $http, $timeout, $window) {
$("#listFail").hide(); $("#listFail").hide();
app.controller('listWebsites', function ($scope, $http) { app.controller('listWebsites', function ($scope, $http, $window) {
$scope.currentPage = 1; $scope.currentPage = 1;
@@ -384,6 +384,244 @@ app.controller('listWebsites', function ($scope, $http) {
}; };
$scope.getFullUrl = function(url) {
console.log('getFullUrl called with:', url);
if (!url) {
// If no URL is provided, try to use the domain
if (this.wp && this.wp.domain) {
url = this.wp.domain;
} else {
return '';
}
}
if (url.startsWith('http://') || url.startsWith('https://')) {
return url;
}
return 'https://' + url;
};
$scope.showWPSites = function(domain) {
var site = $scope.WebSitesList.find(function(site) {
return site.domain === domain;
});
if (site) {
site.showWPSites = !site.showWPSites;
if (site.showWPSites && (!site.wp_sites || !site.wp_sites.length)) {
// Fetch WordPress sites if not already loaded
var config = {
headers: {
'X-CSRFToken': getCookie('csrftoken')
}
};
var data = { domain: domain };
site.loadingWPSites = true;
$http.post('/websites/getWordPressSites', data, config).then(
function(response) {
site.loadingWPSites = false;
if (response.data.status === 1) {
site.wp_sites = response.data.sites;
site.wp_sites.forEach(function(wp) {
// Ensure each WP site has a URL
if (!wp.url) {
wp.url = wp.domain || domain;
}
fetchWPSiteData(wp);
});
} else {
new PNotify({
title: 'Error!',
text: response.data.error_message || 'Could not fetch WordPress sites',
type: 'error'
});
}
},
function(response) {
site.loadingWPSites = false;
new PNotify({
title: 'Error!',
text: 'Could not connect to server',
type: 'error'
});
}
);
}
}
};
function fetchWPSiteData(wp) {
var config = {
headers: {
'X-CSRFToken': getCookie('csrftoken')
}
};
var data = { WPid: wp.id };
// Fetch site data
$http.post('/websites/FetchWPdata', data, config).then(
function(response) {
if (response.data.status === 1) {
var data = response.data.ret_data;
wp.version = data.version;
wp.phpVersion = data.phpVersion || 'PHP 7.4';
wp.searchIndex = data.searchIndex === 1;
wp.debugging = data.debugging === 1;
wp.passwordProtection = data.passwordprotection === 1;
wp.maintenanceMode = data.maintenanceMode === 1;
fetchPluginData(wp);
fetchThemeData(wp);
}
}
);
}
function fetchPluginData(wp) {
var config = {
headers: {
'X-CSRFToken': getCookie('csrftoken')
}
};
var data = { WPid: wp.id };
$http.post('/websites/GetCurrentPlugins', data, config).then(
function(response) {
if (response.data.status === 1) {
var plugins = JSON.parse(response.data.plugins);
wp.activePlugins = plugins.filter(function(p) { return p.status === 'active'; }).length;
wp.totalPlugins = plugins.length;
}
}
);
}
function fetchThemeData(wp) {
var config = {
headers: {
'X-CSRFToken': getCookie('csrftoken')
}
};
var data = { WPid: wp.id };
$http.post('/websites/GetCurrentThemes', data, config).then(
function(response) {
if (response.data.status === 1) {
var themes = JSON.parse(response.data.themes);
wp.theme = themes.find(function(t) { return t.status === 'active'; }).name;
wp.totalThemes = themes.length;
}
}
);
}
$scope.updateSetting = function(wp, setting) {
var settingMap = {
'search-indexing': 'searchIndex',
'debugging': 'debugging',
'password-protection': 'passwordProtection',
'maintenance-mode': 'maintenanceMode'
};
var data = {
siteId: wp.id,
setting: setting,
value: wp[settingMap[setting]] ? 1 : 0
};
var config = {
headers: {
'X-CSRFToken': getCookie('csrftoken')
}
};
$http.post('/websites/UpdateWPSettings', data, config).then(
function(response) {
if (!response.data.status) {
wp[settingMap[setting]] = !wp[settingMap[setting]];
new PNotify({
title: 'Operation Failed!',
text: response.data.error_message || 'Unknown error',
type: 'error'
});
} else {
new PNotify({
title: 'Success!',
text: 'Setting updated successfully.',
type: 'success'
});
}
},
function(response) {
wp[settingMap[setting]] = !wp[settingMap[setting]];
new PNotify({
title: 'Operation Failed!',
text: 'Could not connect to server, please try again.',
type: 'error'
});
}
);
};
$scope.wpLogin = function(wpId) {
window.open('/websites/AutoLogin?id=' + wpId, '_blank');
};
$scope.manageWP = function(wpId) {
window.location.href = '/websites/WPHome?ID=' + wpId;
};
$scope.deleteWPSite = function(wp) {
if (confirm('Are you sure you want to delete this WordPress site? This action cannot be undone.')) {
var config = {
headers: {
'X-CSRFToken': getCookie('csrftoken')
}
};
var data = {
wpid: wp.id,
domain: wp.domain
};
$http.post('/websites/deleteWordPressSite', data, config).then(
function(response) {
if (response.data.status === 1) {
// Remove the WP site from the list
var site = $scope.WebSitesList.find(function(site) {
return site.domain === wp.domain;
});
if (site && site.wp_sites) {
site.wp_sites = site.wp_sites.filter(function(wpSite) {
return wpSite.id !== wp.id;
});
}
new PNotify({
title: 'Success!',
text: 'WordPress site deleted successfully.',
type: 'success'
});
} else {
new PNotify({
title: 'Error!',
text: response.data.error_message || 'Could not delete WordPress site',
type: 'error'
});
}
},
function(response) {
new PNotify({
title: 'Error!',
text: 'Could not connect to server',
type: 'error'
});
}
);
}
};
$scope.visitSite = function(wp) {
var url = wp.url || wp.domain;
if (!url) return '';
if (url.startsWith('http://') || url.startsWith('https://')) {
return url;
}
return 'https://' + url;
};
}); });
@@ -3460,7 +3698,6 @@ app.controller('manageAliasController', function ($scope, $http, $timeout, $wind
$window.location.reload(); $window.location.reload();
}, 3000); }, 3000);
} else { } else {
$scope.aliasTable = false; $scope.aliasTable = false;

View File

@@ -0,0 +1,170 @@
import json
import docker
from django.http import HttpResponse
from .models import DockerSites
from loginSystem.models import Administrator
from plogical.acl import ACLManager
from django.shortcuts import redirect
from loginSystem.views import loadLoginPage
from django.views.decorators.csrf import csrf_exempt
from plogical.CyberCPLogFileWriter import CyberCPLogFileWriter as logging
def require_login(view_func):
def wrapper(request, *args, **kwargs):
try:
userID = request.session['userID']
return view_func(request, *args, **kwargs)
except KeyError:
return redirect(loadLoginPage)
return wrapper
class DockerManager:
def __init__(self):
self.client = docker.from_env()
def get_container(self, container_id):
try:
return self.client.containers.get(container_id)
except docker.errors.NotFound:
return None
except Exception as e:
logging.writeToFile(f"Error getting container {container_id}: {str(e)}")
return None
@csrf_exempt
@require_login
def startContainer(request):
try:
if request.method == 'POST':
userID = request.session['userID']
currentACL = ACLManager.loadedACL(userID)
admin = Administrator.objects.get(pk=userID)
data = json.loads(request.body)
container_id = data.get('container_id')
site_name = data.get('name')
# Verify Docker site ownership
try:
docker_site = DockerSites.objects.get(SiteName=site_name)
if currentACL['admin'] != 1 and docker_site.admin != admin and docker_site.admin.owner != admin.pk:
return HttpResponse(json.dumps({
'status': 0,
'error_message': 'Not authorized to access this container'
}))
except DockerSites.DoesNotExist:
return HttpResponse(json.dumps({
'status': 0,
'error_message': 'Docker site not found'
}))
docker_manager = DockerManager()
container = docker_manager.get_container(container_id)
if not container:
return HttpResponse(json.dumps({
'status': 0,
'error_message': 'Container not found'
}))
container.start()
return HttpResponse(json.dumps({'status': 1}))
return HttpResponse('Not allowed')
except Exception as e:
return HttpResponse(json.dumps({
'status': 0,
'error_message': str(e)
}))
@csrf_exempt
@require_login
def stopContainer(request):
try:
if request.method == 'POST':
userID = request.session['userID']
currentACL = ACLManager.loadedACL(userID)
admin = Administrator.objects.get(pk=userID)
data = json.loads(request.body)
container_id = data.get('container_id')
site_name = data.get('name')
# Verify Docker site ownership
try:
docker_site = DockerSites.objects.get(SiteName=site_name)
if currentACL['admin'] != 1 and docker_site.admin != admin and docker_site.admin.owner != admin.pk:
return HttpResponse(json.dumps({
'status': 0,
'error_message': 'Not authorized to access this container'
}))
except DockerSites.DoesNotExist:
return HttpResponse(json.dumps({
'status': 0,
'error_message': 'Docker site not found'
}))
docker_manager = DockerManager()
container = docker_manager.get_container(container_id)
if not container:
return HttpResponse(json.dumps({
'status': 0,
'error_message': 'Container not found'
}))
container.stop()
return HttpResponse(json.dumps({'status': 1}))
return HttpResponse('Not allowed')
except Exception as e:
return HttpResponse(json.dumps({
'status': 0,
'error_message': str(e)
}))
@csrf_exempt
@require_login
def restartContainer(request):
try:
if request.method == 'POST':
userID = request.session['userID']
currentACL = ACLManager.loadedACL(userID)
admin = Administrator.objects.get(pk=userID)
data = json.loads(request.body)
container_id = data.get('container_id')
site_name = data.get('name')
# Verify Docker site ownership
try:
docker_site = DockerSites.objects.get(SiteName=site_name)
if currentACL['admin'] != 1 and docker_site.admin != admin and docker_site.admin.owner != admin.pk:
return HttpResponse(json.dumps({
'status': 0,
'error_message': 'Not authorized to access this container'
}))
except DockerSites.DoesNotExist:
return HttpResponse(json.dumps({
'status': 0,
'error_message': 'Docker site not found'
}))
docker_manager = DockerManager()
container = docker_manager.get_container(container_id)
if not container:
return HttpResponse(json.dumps({
'status': 0,
'error_message': 'Container not found'
}))
container.restart()
return HttpResponse(json.dumps({'status': 1}))
return HttpResponse('Not allowed')
except Exception as e:
return HttpResponse(json.dumps({
'status': 0,
'error_message': str(e)
}))

View File

@@ -0,0 +1,395 @@
app.controller('ListDockersitecontainer', function ($scope, $http) {
$scope.cyberPanelLoading = true;
$scope.conatinerview = true;
$('#cyberpanelLoading').hide();
// Format bytes to human readable
function formatBytes(bytes, decimals = 2) {
if (bytes === 0) return '0 Bytes';
const k = 1024;
const dm = decimals < 0 ? 0 : decimals;
const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB'];
const i = Math.floor(Math.log(bytes) / Math.log(k));
return parseFloat((bytes / Math.pow(k, i)).toFixed(dm)) + ' ' + sizes[i];
}
$scope.getcontainer = function () {
$('#cyberpanelLoading').show();
url = "/docker/getDockersiteList";
var data = {'name': $('#sitename').html()};
var config = {
headers: {
'X-CSRFToken': getCookie('csrftoken')
}
};
$http.post(url, data, config).then(ListInitialData, cantLoadInitialData);
function ListInitialData(response) {
$('#cyberpanelLoading').hide();
if (response.data.status === 1) {
$scope.cyberPanelLoading = true;
var finalData = JSON.parse(response.data.data[1]);
$scope.ContainerList = finalData;
$("#listFail").hide();
} else {
$("#listFail").fadeIn();
$scope.errorMessage = response.data.error_message;
}
}
function cantLoadInitialData(response) {
$scope.cyberPanelLoading = true;
$('#cyberpanelLoading').hide();
new PNotify({
title: 'Operation Failed!',
text: 'Connection disrupted, refresh the page.',
type: 'error'
});
}
};
$scope.Lunchcontainer = function (containerid) {
$scope.cyberpanelLoading = false;
$('#cyberpanelLoading').show();
var url = "/docker/getContainerAppinfo";
var data = {
'name': $('#sitename').html(),
'id': containerid
};
var config = {
headers: {
'X-CSRFToken': getCookie('csrftoken')
}
};
$http.post(url, data, config).then(ListInitialData, cantLoadInitialData);
function ListInitialData(response) {
$scope.cyberpanelLoading = true;
$('#cyberpanelLoading').hide();
if (response.data.status === 1) {
var containerInfo = response.data.data[1];
console.log("Full container info:", containerInfo);
// Find the container in the list and update its information
for (var i = 0; i < $scope.ContainerList.length; i++) {
if ($scope.ContainerList[i].id === containerid) {
// Basic Information
$scope.ContainerList[i].status = containerInfo.status;
$scope.ContainerList[i].created = new Date(containerInfo.created);
$scope.ContainerList[i].uptime = containerInfo.uptime;
$scope.ContainerList[i].image = containerInfo.image;
// Environment Variables - ensure it's properly set
if (containerInfo.environment) {
console.log("Setting environment:", containerInfo.environment);
$scope.ContainerList[i].environment = containerInfo.environment;
console.log("Container after env update:", $scope.ContainerList[i]);
} else {
console.log("No environment in container info");
}
// Resource Usage
var memoryBytes = containerInfo.memory_usage;
$scope.ContainerList[i].memoryUsage = formatBytes(memoryBytes);
$scope.ContainerList[i].memoryUsagePercent = (memoryBytes / (1024 * 1024 * 1024)) * 100;
$scope.ContainerList[i].cpuUsagePercent = (containerInfo.cpu_usage / 10000000000) * 100;
// Network & Ports
$scope.ContainerList[i].ports = containerInfo.ports;
// Volumes
$scope.ContainerList[i].volumes = containerInfo.volumes;
break;
}
}
// Get container logs
$scope.getcontainerlog(containerid);
} else {
new PNotify({
title: 'Operation Failed!',
text: response.data.error_message,
type: 'error'
});
}
}
function cantLoadInitialData(response) {
$scope.cyberpanelLoading = true;
$('#cyberpanelLoading').hide();
new PNotify({
title: 'Operation Failed!',
text: 'Connection disrupted, refresh the page.',
type: 'error'
});
}
};
$scope.getcontainerlog = function (containerid) {
$scope.cyberpanelLoading = false;
var url = "/docker/getContainerApplog";
var data = {
'name': $('#sitename').html(),
'id': containerid
};
var config = {
headers: {
'X-CSRFToken': getCookie('csrftoken')
}
};
$http.post(url, data, config).then(ListInitialData, cantLoadInitialData);
function ListInitialData(response) {
$scope.cyberpanelLoading = true;
$scope.conatinerview = false;
$('#cyberpanelLoading').hide();
if (response.data.status === 1) {
// Find the container in the list and update its logs
for (var i = 0; i < $scope.ContainerList.length; i++) {
if ($scope.ContainerList[i].id === containerid) {
$scope.ContainerList[i].logs = response.data.data[1];
break;
}
}
} else {
new PNotify({
title: 'Operation Failed!',
text: response.data.error_message,
type: 'error'
});
}
}
function cantLoadInitialData(response) {
$scope.cyberpanelLoading = true;
$('#cyberpanelLoading').hide();
$scope.conatinerview = false;
new PNotify({
title: 'Operation Failed!',
text: 'Connection disrupted, refresh the page.',
type: 'error'
});
}
};
// Auto-refresh container info every 30 seconds
var refreshInterval;
$scope.$watch('conatinerview', function(newValue, oldValue) {
if (newValue === false) { // When container view is shown
refreshInterval = setInterval(function() {
if ($scope.cid) {
$scope.Lunchcontainer($scope.cid);
}
}, 30000); // 30 seconds
} else { // When container view is hidden
if (refreshInterval) {
clearInterval(refreshInterval);
}
}
});
// Clean up on controller destruction
$scope.$on('$destroy', function() {
if (refreshInterval) {
clearInterval(refreshInterval);
}
});
// Initialize
$scope.getcontainer();
// Keep your existing functions
$scope.recreateappcontainer = function() { /* ... */ };
$scope.refreshStatus = function() { /* ... */ };
$scope.restarthStatus = function() { /* ... */ };
$scope.StopContainerAPP = function() { /* ... */ };
$scope.cAction = function(action) {
$scope.cyberpanelLoading = false;
$('#cyberpanelLoading').show();
var url = "/docker/";
switch(action) {
case 'start':
url += "startContainer";
break;
case 'stop':
url += "stopContainer";
break;
case 'restart':
url += "restartContainer";
break;
default:
console.error("Unknown action:", action);
return;
}
var data = {
'name': $('#sitename').html(),
'container_id': $scope.selectedContainer.id
};
var config = {
headers: {
'X-CSRFToken': getCookie('csrftoken')
}
};
$http.post(url, data, config).then(
function(response) {
$scope.cyberpanelLoading = true;
$('#cyberpanelLoading').hide();
if (response.data.status === 1) {
new PNotify({
title: 'Success!',
text: 'Container ' + action + ' successful.',
type: 'success'
});
// Update container status after action
$scope.selectedContainer.status = action === 'stop' ? 'stopped' : 'running';
// Refresh container info
$scope.Lunchcontainer($scope.selectedContainer.id);
} else {
new PNotify({
title: 'Operation Failed!',
text: response.data.error_message || 'An unknown error occurred.',
type: 'error'
});
}
},
function(error) {
$scope.cyberpanelLoading = true;
$('#cyberpanelLoading').hide();
new PNotify({
title: 'Operation Failed!',
text: 'Connection disrupted or server error occurred.',
type: 'error'
});
console.error("Error during container action:", error);
}
);
};
// Update the container selection when actions are triggered
$scope.setSelectedContainer = function(container) {
$scope.selectedContainer = container;
};
// Update the button click handlers to set selected container
$scope.handleAction = function(action, container) {
$scope.setSelectedContainer(container);
$scope.cAction(action);
};
$scope.openSettings = function(container) {
$scope.selectedContainer = container;
$('#settings').modal('show');
};
$scope.saveSettings = function() {
$scope.cyberpanelLoading = false;
$('#cyberpanelLoading').show();
var url = "/docker/updateContainerSettings";
var data = {
'name': $('#sitename').html(),
'id': $scope.selectedContainer.id,
'memoryLimit': $scope.selectedContainer.memoryLimit,
'startOnReboot': $scope.selectedContainer.startOnReboot
};
var config = {
headers: {
'X-CSRFToken': getCookie('csrftoken')
}
};
$http.post(url, data, config).then(ListInitialData, cantLoadInitialData);
function ListInitialData(response) {
$scope.cyberpanelLoading = true;
$('#cyberpanelLoading').hide();
if (response.data.status === 1) {
new PNotify({
title: 'Success!',
text: 'Container settings updated successfully.',
type: 'success'
});
$('#settings').modal('hide');
// Refresh container info after update
$scope.Lunchcontainer($scope.selectedContainer.id);
} else {
new PNotify({
title: 'Operation Failed!',
text: response.data.error_message,
type: 'error'
});
}
}
function cantLoadInitialData(response) {
$scope.cyberpanelLoading = true;
$('#cyberpanelLoading').hide();
new PNotify({
title: 'Operation Failed!',
text: 'Connection disrupted, refresh the page.',
type: 'error'
});
}
};
// Add location service to the controller for the n8n URL
$scope.location = window.location;
// Function to extract n8n version from environment variables
$scope.getN8nVersion = function(container) {
console.log('getN8nVersion called with container:', container);
if (!container || !container.environment) {
console.log('No container or environment data');
return 'unknown';
}
console.log('Container environment:', container.environment);
var version = null;
// Try to find NODE_VERSION first
version = container.environment.find(function(env) {
return env && env.startsWith('NODE_VERSION=');
});
if (version) {
console.log('Found NODE_VERSION:', version);
return version.split('=')[1];
}
// Try to find N8N_VERSION
version = container.environment.find(function(env) {
return env && env.startsWith('N8N_VERSION=');
});
if (version) {
console.log('Found N8N_VERSION:', version);
return version.split('=')[1];
}
console.log('No version found in environment');
return 'unknown';
};
});

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -241,10 +241,11 @@
<div class="col-md-3"> <div class="col-md-3">
<h6 style="font-weight: bold">Search Engine Indexing</h6> <h6 style="font-weight: bold">Search Engine Indexing</h6>
<div class="custom-control custom-switch"> <div class="custom-control custom-switch">
<input ng-click="UpdateWPSettings('searchIndex')" <input type="checkbox"
type="checkbox" class="custom-control-input"
class="custom-control-input ng-pristine ng-untouched ng-valid ng-not-empty" id="searchIndex"
id="searchIndex"> ng-click="UpdateWPSettings('searchIndex')"
ng-checked="searchIndex == 1">
<label class="custom-control-label" <label class="custom-control-label"
for="searchIndex"></label> for="searchIndex"></label>
</div> </div>

View File

@@ -23,6 +23,31 @@
$scope.wpSitesCount = $scope.debug.wp_sites_count; $scope.wpSitesCount = $scope.debug.wp_sites_count;
$scope.currentPage = 1; $scope.currentPage = 1;
$scope.recordsToShow = 10; $scope.recordsToShow = 10;
$scope.expandedSites = {}; // Track which sites are expanded
$scope.currentWP = null; // Store current WordPress site for password protection
// Function to toggle site expansion
$scope.toggleSite = function(site) {
if (!$scope.expandedSites[site.id]) {
$scope.expandedSites[site.id] = true;
site.loading = true;
site.loadingPlugins = true;
site.loadingTheme = true;
fetchSiteData(site);
} else {
$scope.expandedSites[site.id] = false;
}
};
// Function to check if site is expanded
$scope.isExpanded = function(siteId) {
return $scope.expandedSites[siteId];
};
// Function to check if site data is loaded
$scope.isDataLoaded = function(site) {
return site.version !== undefined;
};
$scope.updatePagination = function() { $scope.updatePagination = function() {
var filteredSites = $scope.wpSites; var filteredSites = $scope.wpSites;
@@ -66,12 +91,12 @@
var settingMap = { var settingMap = {
'search-indexing': 'searchIndex', 'search-indexing': 'searchIndex',
'debugging': 'debugging', 'debugging': 'debugging',
'password-protection': 'passwordprotection', 'password-protection': 'passwordProtection',
'maintenance-mode': 'maintenanceMode' 'maintenance-mode': 'maintenanceMode'
}; };
var data = { var data = {
siteId: site.id, WPid: site.id,
setting: setting, setting: setting,
value: site[settingMap[setting]] ? 1 : 0 value: site[settingMap[setting]] ? 1 : 0
}; };
@@ -110,13 +135,28 @@
GLobalAjaxCall($http, "{% url 'GetCurrentPlugins' %}", data, GLobalAjaxCall($http, "{% url 'GetCurrentPlugins' %}", data,
function(response) { function(response) {
if (response.data.status === 1) { if (response.data.status === 1) {
try {
var plugins = JSON.parse(response.data.plugins); var plugins = JSON.parse(response.data.plugins);
site.activePlugins = plugins.filter(function(p) { return p.status === 'active'; }).length; // WordPress CLI returns an array of objects with 'name' and 'status' properties
site.activePlugins = plugins.filter(function(p) {
return p.status && p.status.toLowerCase() === 'active';
}).length;
site.totalPlugins = plugins.length; site.totalPlugins = plugins.length;
} catch (e) {
console.error('Error parsing plugin data:', e);
site.activePlugins = 'Error';
site.totalPlugins = 'Error';
} }
} else {
site.activePlugins = 'Error';
site.totalPlugins = 'Error';
}
site.loadingPlugins = false;
}, },
function(response) { function(response) {
site.activePlugins = 'Error'; site.activePlugins = 'Error';
site.totalPlugins = 'Error';
site.loadingPlugins = false;
} }
); );
} }
@@ -131,9 +171,11 @@
site.activeTheme = themes.find(function(t) { return t.status === 'active'; }).name; site.activeTheme = themes.find(function(t) { return t.status === 'active'; }).name;
site.totalThemes = themes.length; site.totalThemes = themes.length;
} }
site.loadingTheme = false;
}, },
function(response) { function(response) {
site.activeTheme = 'Error'; site.activeTheme = 'Error';
site.loadingTheme = false;
} }
); );
} }
@@ -154,23 +196,135 @@
site.debugging = data.debugging === 1; site.debugging = data.debugging === 1;
site.passwordProtection = data.passwordprotection === 1; site.passwordProtection = data.passwordprotection === 1;
site.maintenanceMode = data.maintenanceMode === 1; site.maintenanceMode = data.maintenanceMode === 1;
site.loading = false;
fetchPluginData(site); fetchPluginData(site);
fetchThemeData(site); fetchThemeData(site);
} else { } else {
site.phpVersion = 'PHP 7.4'; // Default value on error site.phpVersion = 'PHP 7.4'; // Default value on error
site.loading = false;
console.log('Failed to fetch site data:', response.data.error_message); console.log('Failed to fetch site data:', response.data.error_message);
} }
}, },
function(response) { function(response) {
site.phpVersion = 'PHP 7.4'; // Default value on error site.phpVersion = 'PHP 7.4'; // Default value on error
site.loading = false;
console.log('Failed to fetch site data'); console.log('Failed to fetch site data');
} }
); );
} }
if ($scope.wpSites) { if ($scope.wpSites && $scope.wpSites.length > 0) {
$scope.wpSites.forEach(fetchSiteData); // Load data for first site by default
$scope.expandedSites[$scope.wpSites[0].id] = true;
fetchSiteData($scope.wpSites[0]);
} }
$scope.togglePasswordProtection = function(site) {
if (site.passwordProtection) {
// Show modal for credentials
site.PPUsername = "";
site.PPPassword = "";
$scope.currentWP = site;
$('#passwordProtectionModal').modal('show');
} else {
// Disable password protection
var data = {
WPid: site.id,
setting: 'password-protection',
value: 0
};
GLobalAjaxCall($http, "{% url 'UpdateWPSettings' %}", data,
function(response) {
if (!response.data.status) {
site.passwordProtection = !site.passwordProtection;
new PNotify({
title: 'Operation Failed!',
text: response.data.error_message || 'Failed to disable password protection',
type: 'error'
});
} else {
new PNotify({
title: 'Success!',
text: 'Password protection disabled successfully.',
type: 'success'
});
}
},
function(error) {
site.passwordProtection = !site.passwordProtection;
new PNotify({
title: 'Operation Failed!',
text: 'Could not connect to server.',
type: 'error'
});
}
);
}
};
$scope.submitPasswordProtection = function() {
if (!$scope.currentWP) {
new PNotify({
title: 'Error!',
text: 'No WordPress site selected.',
type: 'error'
});
return;
}
if (!$scope.currentWP.PPUsername || !$scope.currentWP.PPPassword) {
new PNotify({
title: 'Error!',
text: 'Please provide both username and password',
type: 'error'
});
return;
}
var data = {
siteId: $scope.currentWP.id,
setting: 'password-protection',
value: 1,
PPUsername: $scope.currentWP.PPUsername,
PPPassword: $scope.currentWP.PPPassword
};
$('#passwordProtectionModal').modal('hide');
GLobalAjaxCall($http, "{% url 'UpdateWPSettings' %}", data,
function(response) {
if (response.data.status === 1) {
// Update the site's password protection state
$scope.currentWP.passwordProtection = true;
new PNotify({
title: 'Success!',
text: 'Password protection enabled successfully!',
type: 'success'
});
// Refresh the site data
fetchSiteData($scope.currentWP);
} else {
// Revert the checkbox state
$scope.currentWP.passwordProtection = false;
new PNotify({
title: 'Error!',
text: response.data.error_message || 'Failed to enable password protection',
type: 'error'
});
}
},
function(error) {
// Revert the checkbox state
$scope.currentWP.passwordProtection = false;
new PNotify({
title: 'Error!',
text: 'Could not connect to server',
type: 'error'
});
}
);
};
}); });
// Add a range filter for pagination // Add a range filter for pagination
@@ -231,7 +385,16 @@
<div class="wp-site-header"> <div class="wp-site-header">
<div class="row"> <div class="row">
<div class="col-sm-8"> <div class="col-sm-8">
<h4>{$ site.title $}</h4> <h4>
<i class="fas"
ng-class="{'fa-chevron-down': isExpanded(site.id), 'fa-chevron-right': !isExpanded(site.id)}"
ng-click="toggleSite(site)"
style="cursor: pointer; margin-right: 10px;"></i>
{$ site.title $}
<span ng-if="site.loading || site.loadingPlugins || site.loadingTheme" class="loading-indicator">
<i class="fa fa-spinner fa-spin" style="color: #00749C; font-size: 14px;"></i>
</span>
</h4>
</div> </div>
<div class="col-sm-4 text-right"> <div class="col-sm-4 text-right">
<a ng-href="{% url 'WPHome' %}?ID={$ site.id $}" class="btn btn-primary btn-sm">Manage</a> <a ng-href="{% url 'WPHome' %}?ID={$ site.id $}" class="btn btn-primary btn-sm">Manage</a>
@@ -239,7 +402,7 @@
</div> </div>
</div> </div>
</div> </div>
<div class="wp-site-content"> <div class="wp-site-content" ng-if="isExpanded(site.id)">
<div class="row"> <div class="row">
<div class="col-sm-3"> <div class="col-sm-3">
<img ng-src="https://api.microlink.io/?url={$ getFullUrl(site.url) $}&screenshot=true&meta=false&embed=screenshot.url" <img ng-src="https://api.microlink.io/?url={$ getFullUrl(site.url) $}&screenshot=true&meta=false&embed=screenshot.url"
@@ -261,25 +424,30 @@
<div class="col-sm-3"> <div class="col-sm-3">
<div class="info-box"> <div class="info-box">
<label>WordPress</label> <label>WordPress</label>
<span>{$ site.version $}</span> <span>{$ site.version || 'Loading...' $}</span>
<i ng-if="site.loading" class="fa fa-spinner fa-spin" style="margin-left: 5px; font-size: 12px;"></i>
</div> </div>
</div> </div>
<div class="col-sm-3"> <div class="col-sm-3">
<div class="info-box"> <div class="info-box">
<label>PHP Version</label> <label>PHP Version</label>
<span>{$ site.phpVersion || 'Loading...' $}</span> <span>{$ site.phpVersion || 'Loading...' $}</span>
<i ng-if="site.loading" class="fa fa-spinner fa-spin" style="margin-left: 5px; font-size: 12px;"></i>
</div> </div>
</div> </div>
<div class="col-sm-3"> <div class="col-sm-3">
<div class="info-box"> <div class="info-box">
<label>Theme</label> <label>Theme</label>
<span>{$ site.activeTheme || 'twentytwentyfive' $}</span> <span>{$ site.activeTheme || 'Loading...' $}</span>
<i ng-if="site.loadingTheme" class="fa fa-spinner fa-spin" style="margin-left: 5px; font-size: 12px;"></i>
</div> </div>
</div> </div>
<div class="col-sm-3"> <div class="col-sm-3">
<div class="info-box"> <div class="info-box">
<label>Plugins</label> <label>Plugins</label>
<span>{$ site.activePlugins || '1' $} active</span> <span ng-if="site.activePlugins !== undefined">{$ site.activePlugins $} active of {$ site.totalPlugins $}</span>
<span ng-if="site.activePlugins === undefined">Loading...</span>
<i ng-if="site.loadingPlugins" class="fa fa-spinner fa-spin" style="margin-left: 5px; font-size: 12px;"></i>
</div> </div>
</div> </div>
</div> </div>
@@ -301,7 +469,9 @@
<div class="col-sm-6"> <div class="col-sm-6">
<div class="checkbox"> <div class="checkbox">
<label> <label>
<input type="checkbox" ng-model="site.passwordProtection" ng-change="updateSetting(site, 'password-protection')"> <input type="checkbox"
ng-model="site.passwordProtection"
ng-change="togglePasswordProtection(site)">
Password protection Password protection
</label> </label>
</div> </div>
@@ -339,6 +509,36 @@
</div> </div>
</div> </div>
</div> </div>
<!-- Password Protection Modal -->
<div class="modal fade" id="passwordProtectionModal" tabindex="-1" role="dialog">
<div class="modal-dialog" role="document">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">Password Protection</h5>
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
<span aria-hidden="true">&times;</span>
</button>
</div>
<div class="modal-body">
<form>
<div class="form-group">
<label>Username</label>
<input type="text" class="form-control" ng-model="currentWP.PPUsername" required>
</div>
<div class="form-group">
<label>Password</label>
<input type="password" class="form-control" ng-model="currentWP.PPPassword" required>
</div>
</form>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-dismiss="modal">Cancel</button>
<button type="button" class="btn btn-primary" ng-click="submitPasswordProtection()">Enable Protection</button>
</div>
</div>
</div>
</div>
</div> </div>
<style> <style>
@@ -389,6 +589,18 @@
.text-center .btn { .text-center .btn {
min-width: 100px; min-width: 100px;
} }
.loading-indicator {
display: inline-flex;
align-items: center;
gap: 8px;
color: #00749C;
font-size: 14px;
padding: 0 8px;
}
.loading-indicator i {
font-size: 14px;
margin-left: 4px;
}
</style> </style>
{% endblock content %} {% endblock content %}

View File

@@ -7,6 +7,9 @@
{% get_current_language as LANGUAGE_CODE %} {% get_current_language as LANGUAGE_CODE %}
<!-- Current language: {{ LANGUAGE_CODE }} --> <!-- Current language: {{ LANGUAGE_CODE }} -->
<!-- Add Font Awesome -->
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css">
<script> <script>
$(document).ready(function () { $(document).ready(function () {
$('[data-toggle="tooltip"]').tooltip(); $('[data-toggle="tooltip"]').tooltip();
@@ -14,6 +17,45 @@
</script> </script>
<div ng-controller="listWebsites" class="container"> <div ng-controller="listWebsites" class="container">
<!-- Loading State -->
<div ng-show="loading" class="text-center" style="padding: 50px;">
<div class="spinner-border text-primary" role="status" style="width: 3rem; height: 3rem;">
<span class="sr-only">Loading...</span>
</div>
<h4 class="mt-3">{% trans "Loading websites..." %}</h4>
</div>
<!-- Main Content (hidden while loading) -->
<div ng-hide="loading">
<!-- Password Protection Modal -->
<div class="modal fade" id="passwordProtectionModal" tabindex="-1" role="dialog">
<div class="modal-dialog" role="document">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">Password Protection</h5>
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
<span aria-hidden="true">&times;</span>
</button>
</div>
<div class="modal-body">
<form>
<div class="form-group">
<label>Username</label>
<input type="text" class="form-control" ng-model="currentWP.PPUsername" required>
</div>
<div class="form-group">
<label>Password</label>
<input type="password" class="form-control" ng-model="currentWP.PPPassword" required>
</div>
</form>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-dismiss="modal">Cancel</button>
<button type="button" class="btn btn-primary" ng-click="submitPasswordProtection()">Enable Protection</button>
</div>
</div>
</div>
</div>
<div id="page-title"> <div id="page-title">
<h2 id="domainNamePage">{% trans "List Websites" %} <h2 id="domainNamePage">{% trans "List Websites" %}
@@ -84,97 +126,146 @@
<div class="col-md-12"> <div class="col-md-12">
<div class="col-md-3 content-box-header"> <div class="col-md-3 content-box-header">
<i class="p fa fa-hdd-o btn-icon text-muted" data-toggle="tooltip" <i class="fa-solid fa-hard-drive btn-icon text-muted" data-toggle="tooltip"
data-placement="right" data-placement="right"
title="Disk Usage">&emsp;</i> title="Disk Usage">&emsp;</i>
<span ng-bind="web.diskUsed" style="text-transform: none"></span> <span ng-bind="web.diskUsed" style="text-transform: none"></span>
</div> </div>
<div class="col-md-3 content-box-header"> <div class="col-md-3 content-box-header">
<i class="p fa fa-cubes btn-icon text-muted" data-toggle="tooltip" <i class="fa-solid fa-cubes btn-icon text-muted" data-toggle="tooltip"
data-placement="right" data-placement="right"
title="Packages">&emsp;</i> title="Packages">&emsp;</i>
<span ng-bind="web.package" style="text-transform: none"></span> <span ng-bind="web.package" style="text-transform: none"></span>
</div> </div>
<div class="col-md-3 content-box-header"> <div class="col-md-3 content-box-header">
<i class="p fa fa-user btn-icon text-muted" data-toggle="tooltip" data-placement="right" <i class="fa-solid fa-user btn-icon text-muted" data-toggle="tooltip" data-placement="right"
title="Owner">&emsp;</i> title="Owner">&emsp;</i>
<span ng-bind="web.admin" style="text-transform: none"></span> <span ng-bind="web.admin" style="text-transform: none"></span>
</div> </div>
<div class="col-md-3 content-box-header"> <div class="col-md-3 content-box-header">
<i class="p fa fa-wordpress btn-icon text-muted" ng-click="showWPSites(web.domain)" <a href="javascript:void(0);" ng-click="showWPSites(web.domain)" class="wp-sites-link">
data-toggle="tooltip" data-placement="right" title="Show WordPress Sites">&emsp;</i> <i class="fa-brands fa-wordpress btn-icon text-muted" data-toggle="tooltip"
<span ng-if="web.wp_sites && web.wp_sites.length > 0" style="text-transform: none"> data-placement="right" title="Show WordPress Sites"></i>
{$ web.wp_sites.length $} WordPress Sites <span ng-if="!web.loadingWPSites" class="wp-sites-count">
{$ (web.wp_sites && web.wp_sites.length) || 0 $} WordPress Sites
</span> </span>
<span ng-if="web.loadingWPSites" class="loading-indicator">
Loading <i class="fa fa-spinner fa-spin"></i>
</span>
</a>
</div> </div>
</div> </div>
<!-- WordPress Sites Section --> <!-- WordPress Sites Section -->
<div ng-if="web.showWPSites && web.wp_sites && web.wp_sites.length > 0" class="card mt-3"> <div class="col-md-12" ng-if="web.showWPSites && web.wp_sites && web.wp_sites.length > 0" style="padding: 15px 30px;">
<div class="card-header"> <div ng-repeat="wp in web.wp_sites" class="wp-site-item">
<h5 class="mb-0">WordPress Sites</h5>
</div>
<div class="card-body">
<div class="row"> <div class="row">
<div class="col-md-6 mb-4" ng-repeat="site in web.wp_sites"> <div class="col-sm-12">
<div class="card h-100"> <div class="wp-site-header">
<div class="card-header d-flex justify-content-between align-items-center">
<h6 class="mb-0">{{site.title}}</h6>
<div>
<button class="btn btn-sm btn-primary mr-2" ng-click="visitSite(site.url)">
<i class="fas fa-external-link-alt"></i> Visit
</button>
<button class="btn btn-sm btn-info mr-2" ng-click="wpLogin(site.id)">
<i class="fas fa-sign-in-alt"></i> Login
</button>
<button class="btn btn-sm btn-secondary" ng-click="manageWP(site.id)">
<i class="fas fa-cog"></i> Manage
</button>
</div>
</div>
<div class="card-body">
<div class="row"> <div class="row">
<div class="col-md-6"> <div class="col-sm-8">
<p><strong>WordPress Version:</strong> {{site.version}}</p> <h4>
<p><strong>PHP Version:</strong> {{site.phpVersion}}</p> <i class="fa-brands fa-wordpress" style="color: #00749C; margin-right: 8px;"></i>
<p><strong>Active Theme:</strong> {{site.theme}}</p> {$ wp.title $}
<p><strong>Active Plugins:</strong> {{site.activePlugins}}</p> <span ng-if="wp.loading || wp.loadingPlugins || wp.loadingTheme" class="loading-indicator">
<i class="fa fa-spinner fa-spin" style="color: #00749C; font-size: 14px;"></i>
</span>
</h4>
</div> </div>
<div class="col-md-6"> <div class="col-sm-4 text-right">
<div class="form-group"> <button class="btn btn-outline-primary btn-sm wp-action-btn" ng-click="manageWP(wp.id)">
<div class="custom-control custom-switch"> <i class="fa-solid fa-cog"></i> Manage
<input type="checkbox" class="custom-control-input" </button>
id="searchIndex{{site.id}}" <button class="btn btn-outline-danger btn-sm wp-action-btn" ng-click="deleteWPSite(wp)">
ng-model="site.searchIndex" <i class="fa-solid fa-trash"></i> Delete
ng-change="updateSetting(site.id, 'search-indexing', site.searchIndex ? 'enable' : 'disable')"> </button>
<label class="custom-control-label" for="searchIndex{{site.id}}">Search Indexing</label>
</div> </div>
</div> </div>
<div class="form-group"> </div>
<div class="custom-control custom-switch"> <div class="wp-site-content">
<input type="checkbox" class="custom-control-input" <div class="row">
id="debugging{{site.id}}" <div class="col-sm-3">
ng-model="site.debugging" <img ng-src="{$ wp.screenshot $}"
ng-change="updateSetting(site.id, 'debugging', site.debugging ? 'enable' : 'disable')"> alt="{$ wp.title $}"
<label class="custom-control-label" for="debugging{{site.id}}">Debugging</label> class="img-responsive"
style="max-width: 100%; margin-bottom: 10px; min-height: 150px; background: #f5f5f5;"
onerror="this.onerror=null; this.src='https://s.wordpress.org/style/images/about/WordPress-logotype-standard.png';">
<div class="text-center wp-action-buttons">
<a href="javascript:void(0);" ng-click="visitSite(wp)" class="btn btn-outline-secondary btn-sm wp-action-btn">
<i class="fa-solid fa-external-link"></i> Visit Site
</a>
<a href="{% url 'AutoLogin' %}?id={$ wp.id $}" target="_blank" class="btn btn-outline-primary btn-sm wp-action-btn">
<i class="fa-brands fa-wordpress"></i> WP Login
</a>
</div> </div>
</div> </div>
<div class="form-group"> <div class="col-sm-9">
<div class="custom-control custom-switch"> <div class="row">
<input type="checkbox" class="custom-control-input" <div class="col-sm-3">
id="passwordProtection{{site.id}}" <div class="info-box">
ng-model="site.passwordProtection" <label>WordPress</label>
ng-change="updateSetting(site.id, 'password-protection', site.passwordProtection ? 'enable' : 'disable')"> <span>{$ wp.version || 'Loading...' $}</span>
<label class="custom-control-label" for="passwordProtection{{site.id}}">Password Protection</label> <i ng-if="wp.loading" class="fa fa-spinner fa-spin" style="margin-left: 5px; font-size: 12px;"></i>
</div> </div>
</div> </div>
<div class="form-group"> <div class="col-sm-3">
<div class="custom-control custom-switch"> <div class="info-box">
<input type="checkbox" class="custom-control-input" <label>PHP Version</label>
id="maintenanceMode{{site.id}}" <span>{$ wp.phpVersion || 'Loading...' $}</span>
ng-model="site.maintenanceMode" <i ng-if="wp.loading" class="fa fa-spinner fa-spin" style="margin-left: 5px; font-size: 12px;"></i>
ng-change="updateSetting(site.id, 'maintenance-mode', site.maintenanceMode ? 'enable' : 'disable')"> </div>
<label class="custom-control-label" for="maintenanceMode{{site.id}}">Maintenance Mode</label> </div>
<div class="col-sm-3">
<div class="info-box">
<label>Theme</label>
<span>{$ wp.theme || 'Loading...' $}</span>
<i ng-if="wp.loadingTheme" class="fa fa-spinner fa-spin" style="margin-left: 5px; font-size: 12px;"></i>
</div>
</div>
<div class="col-sm-3">
<div class="info-box">
<label>Plugins</label>
<span>{$ wp.activePlugins || '0' $} active</span>
<i ng-if="wp.loadingPlugins" class="fa fa-spinner fa-spin" style="margin-left: 5px; font-size: 12px;"></i>
</div>
</div>
</div>
<div class="row mt-3">
<div class="col-sm-6">
<div class="checkbox">
<label>
<input type="checkbox"
ng-click="updateSetting(wp, 'search-indexing')"
ng-checked="wp.searchIndex == 1">
Search engine indexing
</label>
</div>
<div class="checkbox">
<label>
<input type="checkbox"
ng-click="updateSetting(wp, 'debugging')"
ng-checked="wp.debugging == 1">
Debugging
</label>
</div>
</div>
<div class="col-sm-6">
<div class="checkbox">
<label>
<input type="checkbox"
ng-model="wp.passwordProtection"
ng-init="wp.passwordProtection = wp.passwordProtection || false"
ng-change="togglePasswordProtection(wp)">
Password protection
</label>
</div>
<div class="checkbox">
<label>
<input type="checkbox"
ng-click="updateSetting(wp, 'maintenance-mode')"
ng-checked="wp.maintenanceMode == 1">
Maintenance mode
</label>
</div> </div>
</div> </div>
</div> </div>
@@ -186,6 +277,141 @@
</div> </div>
</div> </div>
<style>
.wp-site-item {
border: 1px solid #e0e0e0;
margin-bottom: 20px;
border-radius: 8px;
background: white;
box-shadow: 0 2px 4px rgba(0,0,0,0.05);
}
.wp-site-header {
padding: 15px 20px;
border-bottom: 1px solid #e0e0e0;
background: #f8f9fa;
border-radius: 8px 8px 0 0;
}
.wp-site-header h4 {
margin: 0;
font-size: 18px;
line-height: 34px;
color: #2c3338;
font-weight: 500;
}
.wp-site-content {
padding: 20px;
}
.info-box {
margin-bottom: 15px;
background: #f8f9fa;
padding: 10px;
border-radius: 6px;
}
.info-box label {
display: block;
font-size: 12px;
color: #646970;
margin-bottom: 5px;
font-weight: 500;
}
.info-box span {
font-size: 14px;
font-weight: 600;
color: #2c3338;
}
.checkbox {
margin-bottom: 10px;
}
.mt-3 {
margin-top: 1rem;
}
/* Updated button styles */
.wp-action-btn {
margin: 0 4px;
padding: 6px 12px;
font-size: 13px;
font-weight: 500;
transition: all 0.2s ease;
border-width: 1.5px;
line-height: 1.5;
display: inline-flex;
align-items: center;
justify-content: center;
vertical-align: middle;
height: 32px;
}
.wp-action-btn i {
margin-right: 6px;
font-size: 14px;
display: inline-flex;
align-items: center;
}
.wp-action-buttons {
margin-top: 12px;
display: flex;
gap: 8px;
justify-content: center;
}
.wp-action-buttons .wp-action-btn {
min-width: 110px;
flex: 0 1 auto;
}
.btn-outline-primary {
color: #0073aa;
border-color: #0073aa;
}
.btn-outline-primary:hover {
background-color: #0073aa;
color: white;
}
.btn-outline-secondary {
color: #50575e;
border-color: #50575e;
}
.btn-outline-secondary:hover {
background-color: #50575e;
color: white;
}
.btn-outline-danger {
color: #dc3545;
border-color: #dc3545;
}
.btn-outline-danger:hover {
background-color: #dc3545;
color: white;
}
.wp-sites-link {
cursor: pointer;
display: inline-flex;
align-items: center;
gap: 8px;
color: inherit;
text-decoration: none;
}
.wp-sites-link:hover {
color: inherit;
text-decoration: none;
}
.wp-sites-link i.btn-icon {
margin-right: 4px;
}
.wp-sites-count {
text-transform: none;
}
.loading-indicator {
display: inline-flex;
align-items: center;
gap: 8px;
color: #00749C;
font-size: 14px;
padding: 0 8px;
}
.loading-indicator i {
font-size: 14px;
margin-left: 4px;
}
</style>
<div id="listFail" class="alert alert-danger"> <div id="listFail" class="alert alert-danger">
<p>{% trans "Cannot list websites. Error message:" %} {$ errorMessage $}</p> <p>{% trans "Cannot list websites. Error message:" %} {$ errorMessage $}</p>
</div> </div>
@@ -209,5 +435,6 @@
</div> </div>
</div> </div>
</div> </div>
</div>
{% endblock %} {% endblock %}

View File

@@ -51,6 +51,7 @@ urlpatterns = [
path('AddWPsiteforRemoteBackup', views.AddWPsiteforRemoteBackup, name='AddWPsiteforRemoteBackup'), path('AddWPsiteforRemoteBackup', views.AddWPsiteforRemoteBackup, name='AddWPsiteforRemoteBackup'),
path('UpdateRemoteschedules', views.UpdateRemoteschedules, name='UpdateRemoteschedules'), path('UpdateRemoteschedules', views.UpdateRemoteschedules, name='UpdateRemoteschedules'),
path('ScanWordpressSite', views.ScanWordpressSite, name='ScanWordpressSite'), path('ScanWordpressSite', views.ScanWordpressSite, name='ScanWordpressSite'),
path('fetchWPDetails', views.fetchWPDetails, name='fetchWPDetails'),
# AddPlugin # AddPlugin
path('ConfigurePlugins', views.ConfigurePlugins, name='ConfigurePlugins'), path('ConfigurePlugins', views.ConfigurePlugins, name='ConfigurePlugins'),
@@ -178,6 +179,11 @@ urlpatterns = [
path('ListDockerSites', views.ListDockerSites, name='ListDockerSites'), path('ListDockerSites', views.ListDockerSites, name='ListDockerSites'),
path('fetchDockersite', views.fetchDockersite, name='fetchDockersite'), path('fetchDockersite', views.fetchDockersite, name='fetchDockersite'),
# Docker Container Actions
path('docker/startContainer', views.startContainer, name='startContainer'),
path('docker/stopContainer', views.stopContainer, name='stopContainer'),
path('docker/restartContainer', views.restartContainer, name='restartContainer'),
# SSH Configs # SSH Configs
path('getSSHConfigs', views.getSSHConfigs, name='getSSHConfigs'), path('getSSHConfigs', views.getSSHConfigs, name='getSSHConfigs'),
path('deleteSSHKey', views.deleteSSHKey, name='deleteSSHKey'), path('deleteSSHKey', views.deleteSSHKey, name='deleteSSHKey'),
@@ -194,6 +200,4 @@ urlpatterns = [
# Catch all for domains # Catch all for domains
path('<domain>/<childDomain>', views.launchChild, name='launchChild'), path('<domain>/<childDomain>', views.launchChild, name='launchChild'),
path('<domain>', views.domain, name='domain'), path('<domain>', views.domain, name='domain'),
path(r'GetWPSitesByDomain', views.GetWPSitesByDomain, name='GetWPSitesByDomain'),
] ]

View File

@@ -14,6 +14,9 @@ from websiteFunctions.models import wpplugins
from websiteFunctions.website import WebsiteManager from websiteFunctions.website import WebsiteManager
from websiteFunctions.pluginManager import pluginManager from websiteFunctions.pluginManager import pluginManager
from django.views.decorators.csrf import csrf_exempt from django.views.decorators.csrf import csrf_exempt
from .dockerviews import startContainer as docker_startContainer
from .dockerviews import stopContainer as docker_stopContainer
from .dockerviews import restartContainer as docker_restartContainer
def loadWebsitesHome(request): def loadWebsitesHome(request):
val = request.session['userID'] val = request.session['userID']
@@ -1843,19 +1846,40 @@ def Dockersitehome(request, dockerapp):
except KeyError: except KeyError:
return redirect(loadLoginPage) return redirect(loadLoginPage)
def GetWPSitesByDomain(request): def fetchWPDetails(request):
try: try:
userID = request.session['userID'] userID = request.session['userID']
data = json.loads(request.body) data = {
domain = data['domain'] 'domain': request.POST.get('domain')
}
wm = WebsiteManager() wm = WebsiteManager()
response = wm.GetWPSitesByDomain(userID, data) return wm.fetchWPSitesForDomain(userID, data)
return response
except KeyError: except KeyError:
return redirect(reverse('login')) return redirect(loadLoginPage)
except BaseException as msg:
data_ret = {'status': 0, 'error_message': str(msg)} @csrf_exempt
json_data = json.dumps(data_ret) def startContainer(request):
return HttpResponse(json_data) try:
if request.method == 'POST':
return docker_startContainer(request)
return HttpResponse('Not allowed')
except KeyError:
return redirect(loadLoginPage)
@csrf_exempt
def stopContainer(request):
try:
if request.method == 'POST':
return docker_stopContainer(request)
return HttpResponse('Not allowed')
except KeyError:
return redirect(loadLoginPage)
@csrf_exempt
def restartContainer(request):
try:
if request.method == 'POST':
return docker_restartContainer(request)
return HttpResponse('Not allowed')
except KeyError:
return redirect(loadLoginPage)

File diff suppressed because it is too large Load Diff