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):
self.path = '/usr/local/CyberCP/version.txt'
#versionInfo = json.loads(open(self.path, 'r').read())
self.version = '2.3'
self.build = '9'
self.version = '2.4'
self.build = '0'
ipFile = "/etc/cyberpanel/machineIP"
f = open(ipFile)

View File

@@ -34,6 +34,7 @@ import googleapiclient.discovery
from googleapiclient.discovery import build
from websiteFunctions.models import NormalBackupDests, NormalBackupJobs, NormalBackupSites
from plogical.IncScheduler import IncScheduler
from django.http import JsonResponse
class BackupManager:
localBackupPath = '/home/cyberpanel/localBackupPath'
@@ -2338,4 +2339,76 @@ class BackupManager:
json_data = json.dumps(data_ret)
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.
*/
// 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 ****//
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) {
$scope.restoreLoading = true;

View File

@@ -5,59 +5,409 @@
{% load static %}
{% get_current_language as 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 id="page-title">
<h2>{% trans "One-click Backups" %} - <a target="_blank"
href="https://youtu.be/mLjMg8Anq70"
style="height: 23px;line-height: 21px;"
class="btn btn-border btn-alt border-red btn-link font-red"
title=""><span>{% trans "One-Click Backup Docs" %}</span></a>
</h2>
<p>{% trans "On this page you purchase and manage one-click backups." %}</p>
<!-- Page header -->
<div class="page-header">
<h1 class="page-title">One-click Backups</h1>
<a href="https://youtu.be/mLjMg8Anq70" target="_blank" class="cp-btn cp-btn-outline">
Watch Tutorial
</a>
</div>
<div ng-controller="backupPlanNowOneClick" class="panel">
<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">
<p class="page-description">On this page you purchase and manage one-click backups.</p>
<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 %}
<div class="alert alert-info">
<p>You have successfully purchased a backup plan.</p>
<div class="info-box">
<p class="mb-0">You have successfully purchased a backup plan.</p>
</div>
{% elif status == 0 %}
<div class="alert alert-danger">
<p>Your purchase was not successful.</p> {{ message }}
<div class="info-box" style="background-color: #f8d7da; border-color: #f5c6cb;">
<p class="mb-0">Your purchase was not successful. {{ message }}</p>
</div>
{% elif status == 4 %}
<div class="alert alert-danger">
{{ message }}
<div class="info-box" style="background-color: #f8d7da; border-color: #f5c6cb;">
<p class="mb-0">{{ message }}</p>
</div>
{% endif %}
<form action="/" class="form-horizontal bordered-row">
<p style="font-size: 15px;margin: 1%;">With CyberPanel's one-click backups, you can easily back
up your website to our secure
servers in just 60 seconds. It's simple, fast, and reliable.</p>
<!------ List of Purchased backup plans --------------->
<div class="form-group">
<div class="col-sm-12">
<table class="table">
<!-- Your Backup Plans Section -->
<div class="cp-card mb-4">
<div class="cp-card-header">Your Backup Plans</div>
<div class="cp-card-body p-0">
<table class="cp-table">
<thead>
<tr>
<th>{% trans "Account" %}</th>
@@ -74,108 +424,182 @@
<td>{{ plan.sftpUser }}</td>
<td>{{ plan.planName }}</td>
<td>{{ plan.subscription }}</td>
<td>
<span class="billing-cycle">
{% if plan.months == '1' %}
<td>${{ plan.price }}/month</td>
${{ plan.price }}/month
{% else %}
<td>${{ plan.price }}/year</td>
${{ plan.price }}/year
{% endif %}
</span>
</td>
<td>{{ plan.date }}</td>
<td>
<div class="action-btns">
{% if plan.state == 1 %}
<a
href="{% url 'ManageOCBackups' %}?id={{ plan.id }}">
<button style="margin-bottom: 1%" type="button"
class="btn btn-primary btn-lg btn-block">{% trans "Schedule Backups" %}</button>
<a href="{% url 'ManageOCBackups' %}?id={{ plan.id }}"
class="cp-btn cp-btn-primary">
{% trans "Schedule Backups" %}
</a>
<a href="{% url 'RestoreOCBackups' %}?id={{ plan.id }}">
<button type="button"
class="btn btn-primary btn-lg btn-block">{% trans "Restore Backups" %}</button>
<a href="{% url 'RestoreOCBackups' %}?id={{ plan.id }}"
class="cp-btn cp-btn-outline">
{% trans "Restore Backups" %}
</a>
{% else %}
<button type="button"
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 %}
</td>
</div>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
<!------ List of Purchased backup plans --------------->
<!------ List of Backup plans --------------->
<h3 class="title-hero">
{% trans "Subscribe to one-click backup plans." %} <img ng-hide="cyberpanelLoading"
src="{% static 'images/loading.gif' %}">
</h3>
<div class="form-group">
<div class="col-sm-12">
<table class="table">
<thead>
<tr>
<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>
<!-- Available Backup Plans Section -->
<h2 class="section-title">Available Backup Plans</h2>
<div class="plans-container">
<div class="plan-grid">
<!-- 100GB Plan -->
<div class="cp-card">
<div class="cp-card-header">100GB</div>
<div class="cp-card-body">
<div class="price-box mb-3">
<div class="price-amount">${{ plans.0.monthlyPrice }}</div>
<div class="price-period">/month</div>
</div>
<div class="price-box mb-4">
<div class="price-amount">${{ plans.0.yearlyPrice }}</div>
<div class="price-period">/year</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>
<!------ List of backup plans --------------->
<!--- AWS End --->
</form>
<!-- 500GB Plan -->
<div class="cp-card">
<div class="cp-card-header">500GB</div>
<div class="cp-card-body">
<div class="price-box mb-3">
<div class="price-amount">${{ plans.1.monthlyPrice }}</div>
<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>
<!-- 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 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 %}

View File

@@ -10,6 +10,7 @@ urlpatterns = [
re_path(r'^fetchOCSites$', views.fetchOCSites, name='fetchOCSites'),
re_path(r'^StartOCRestore$', views.StartOCRestore, name='StartOCRestore'),
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'^restoreSite$', views.restoreSite, name='restoreSite'),

View File

@@ -5,6 +5,7 @@
import json
from django.shortcuts import redirect
from django.http import HttpResponse
from backup.backupManager import BackupManager
from backup.pluginManager import pluginManager
@@ -12,6 +13,8 @@ from loginSystem.views import loadLoginPage
import os
from plogical.CyberCPLogFileWriter import CyberCPLogFileWriter as logging
from django.views.decorators.csrf import csrf_exempt
from django.contrib.auth.models import User
from loginSystem.models import Administrator
def loadBackupHome(request):
try:
@@ -539,3 +542,14 @@ def DeployAccount(request):
return bm.DeployAccount(request, userID)
except KeyError:
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 -->
{% 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' %}">
@@ -87,7 +87,7 @@
<link rel="stylesheet" type="text/css"
href="/static/baseTemplate/assets/themes/admin/color-schemes/default.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 'websiteFunctions/websiteFunctions.css' %}">
<link rel="icon" type="image/x-icon" href="{% static 'baseTemplate/assets/finalBase/favicon.png' %}">
@@ -248,7 +248,7 @@
title="{% trans 'Server IP Address' %}">
<i class="glyph-icon tooltip-button icon-laptop" title="{% trans 'Server IP Address' %}"
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 id="sidebar-menu-item-dashboard" href="{% url 'index' %}"
title="{% trans 'Dashboard' %}">
@@ -376,7 +376,6 @@
<a href="#" title="{% trans 'Dockersite' %}">
<div class="glyph-icon icon-globe" title="{% trans 'Docker Apps' %}"></div>
<span>{% trans "Docker Apps" %}</span>
<span class="bs-label badge-yellow">{% trans "Beta" %}</span>
</a>
<div class="sidebar-submenu">
@@ -1198,6 +1197,7 @@
<script src="{% static 'baseTemplate/custom-js/pnotify.custom.min.js' %}"></script>
<script src="{% static 'packages/packages.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 'serverStatus/serverStatus.js' %}?ver={{ version }}"></script>
<script src="{% static 'dns/dns.js' %}?ver={{ version }}"></script>
@@ -1228,5 +1228,27 @@
</div>
{% block footer_scripts %}
{% 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>
</html>

View File

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

View File

@@ -765,7 +765,7 @@ else
Check_Return
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
cd wsgi-lsapi-2.1 || exit
/usr/local/CyberPanel/bin/python ./configure.py

View File

@@ -3,6 +3,7 @@
import os.path
import sys
import django
from datetime import datetime
from plogical.DockerSites import Docker_Sites
@@ -1116,27 +1117,68 @@ class ContainerManager(multi.Thread):
if admin.acl.adminStatus != 1:
return ACLManager.loadError()
name = data['name']
containerID = data['id']
passdata = {}
passdata["JobID"] = None
passdata['name'] = name
passdata['containerID'] = containerID
da = Docker_Sites(None, passdata)
retdata = da.ContainerInfo()
# Create a Docker client
client = docker.from_env()
container = client.containers.get(containerID)
# 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)
return HttpResponse(json_data)
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)
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):
try:
admin = Administrator.objects.get(pk=userID)

View File

@@ -1,7 +1,7 @@
from django.urls import path, re_path
from . import views
from websiteFunctions.views import Dockersitehome
from websiteFunctions.views import Dockersitehome, startContainer, stopContainer, restartContainer
urlpatterns = [
re_path(r'^$', views.loadDockerHome, name='dockerHome'),
@@ -27,7 +27,7 @@ urlpatterns = [
re_path(r'^recreateContainer$', views.recreateContainer, name='recreateContainer'),
re_path(r'^installDocker$', views.installDocker, name='installDocker'),
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('getDockersiteList', views.getDockersiteList, name='getDockersiteList'),
@@ -36,4 +36,9 @@ urlpatterns = [
path('recreateappcontainer', views.recreateappcontainer, name='recreateappcontainer'),
path('RestartContainerAPP', views.RestartContainerAPP, name='RestartContainerAPP'),
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 *
import stat
VERSION = '2.3'
BUILD = 9
VERSION = '2.4'
BUILD = 0
char_set = {'small': 'abcdefghijklmnopqrstuvwxyz', 'nums': '0123456789', 'big': 'ABCDEFGHIJKLMNOPQRSTUVWXYZ'}

View File

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

View File

@@ -4,6 +4,9 @@ import os
import sys
import time
from random import randint
import socket
import shutil
import docker
sys.path.append('/usr/local/CyberCP')
@@ -24,11 +27,25 @@ from plogical.CyberCPLogFileWriter import CyberCPLogFileWriter as logging
import argparse
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):
Wordpress = 1
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):
multi.Thread.__init__(self)
self.function_run = function_run
@@ -165,15 +182,54 @@ class Docker_Sites(multi.Thread):
return 0, ReturnCode
else:
command = 'apt install docker-compose -y'
ReturnCode = ProcessUtilities.executioner(command)
if ReturnCode:
return 1, None
else:
# 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, 'root', True)
if not 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
def SetupProxy(port):
import xml.etree.ElementTree as ET
@@ -614,8 +670,6 @@ services:
### forcefully delete containers
import docker
# Create a Docker client
client = docker.from_env()
@@ -651,30 +705,54 @@ services:
## This function need site name which was passed while creating the app
def ListContainers(self):
try:
import docker
# Create a Docker client
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
label_filter = {'name': FilerValue}
# List all containers without filtering first
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
containers = client.containers.list(filters=label_filter)
# Now filter containers - handle both CentOS and Ubuntu naming
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 = "["
checker = 0
for container in containers:
try:
dic = {
'id': container.short_id,
'name': container.name,
'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 [],
'logs_50': container.logs(tail=50).decode('utf-8'),
'ports': container.attrs['HostConfig']['PortBindings'] if 'HostConfig' in container.attrs else {}
@@ -685,9 +763,15 @@ services:
checker = 1
else:
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 + ']'
if os.path.exists(ProcessUtilities.debugPath):
logging.writeToFile(f'Final JSON data: {json_data}')
return 1, json_data
except BaseException as msg:
@@ -697,7 +781,6 @@ services:
### pass container id and number of lines to fetch from logs
def ContainerLogs(self):
try:
import docker
# Create a Docker client
client = docker.from_env()
@@ -716,7 +799,6 @@ services:
def ContainerInfo(self):
try:
import docker
# Create a Docker client
client = docker.from_env()
@@ -748,7 +830,6 @@ services:
def RestartContainer(self):
try:
import docker
# Create a Docker client
client = docker.from_env()
@@ -764,7 +845,6 @@ services:
def StopContainer(self):
try:
import docker
# Create a Docker client
client = docker.from_env()
@@ -780,102 +860,367 @@ services:
##### 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:
# 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'
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 os.path.exists(ProcessUtilities.debugPath):
logging.writeToFile(f'About to run docker install function...')
execPath = "/usr/local/CyberCP/bin/python /usr/local/CyberCP/dockerManager/dockerInstall.py"
ProcessUtilities.executioner(execPath)
logging.statusWriter(self.JobID, 'Docker is ready to use..,10')
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]')
# Call InstallDocker to install Docker
install_result, error = self.InstallDocker()
if not install_result:
logging.statusWriter(self.JobID, f'Failed to install Docker: {error} [404]')
return 0
TempCompose = f'/home/cyberpanel/{self.data["finalURL"]}-docker-compose.yml'
logging.statusWriter(self.JobID, 'Docker installation verified...,20')
WriteToFile = open(TempCompose, 'w')
WriteToFile.write(WPSite)
WriteToFile.close()
# Verify system resources
self.verify_system_resources()
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']}"
result, message = ProcessUtilities.outputExecutioner(command, None, None, None, 1)
if result == 0:
logging.statusWriter(self.JobID, f'Error {str(message)} . [404]')
return 0
raise DockerDeploymentError(f"Failed to move compose file: {message}")
command = f"chmod 600 {self.data['ComposePath']} && chown root:root {self.data['ComposePath']}"
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:
dockerCommand = 'docker compose'
else:
@@ -883,69 +1228,182 @@ services:
command = f"{dockerCommand} -f {self.data['ComposePath']} -p '{self.data['SiteName']}' up -d"
result, message = ProcessUtilities.outputExecutioner(command, None, None, None, 1)
if result == 0:
logging.statusWriter(self.JobID, f'Error {str(message)} . [404]')
return 0
logging.statusWriter(self.JobID, 'Bringing containers online..,50')
raise DockerDeploymentError(f"Failed to deploy containers: {message}")
logging.statusWriter(self.JobID, 'Containers deployed...,60')
# Wait for containers to be healthy
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')
### 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
# Setup proxy
execPath = "/usr/local/CyberCP/bin/python /usr/local/CyberCP/plogical/DockerSites.py"
execPath = execPath + f" SetupProxy --port {self.data['port']}"
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 = execPath + f" SetupHTAccess --port {self.data['port']} --htaccess {self.data['htaccessPath']}"
ProcessUtilities.executioner(execPath, self.data['externalApp'])
logging.statusWriter(self.JobID, 'HTAccess configured...,90')
# if ProcessUtilities.decideDistro() == ProcessUtilities.centos or ProcessUtilities.decideDistro() == ProcessUtilities.cent8:
# 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
# Restart web server
from plogical.installUtilities import installUtilities
installUtilities.reStartLiteSpeedSocket()
logging.statusWriter(self.JobID, 'Completed. [200]')
# Monitor deployment
metrics = self.monitor_deployment()
self.log_deployment_metrics(metrics)
except BaseException as msg:
logging.writeToFile(f'{str(msg)}. [DeployN8NContainer]')
logging.statusWriter(self.JobID, f'Error {str(msg)} . [404]')
print(str(msg))
pass
logging.statusWriter(self.JobID, 'Deployment completed successfully. [200]')
return True
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():
try:

View File

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

View File

@@ -53,8 +53,8 @@ try:
except:
pass
VERSION = '2.3'
BUILD = 9
VERSION = '2.4'
BUILD = 0
## I am not the monster that you think I am..
@@ -739,7 +739,7 @@ class backupUtilities:
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..')
databaseUsers = database.findall('databaseUsers')
@@ -1073,7 +1073,7 @@ class backupUtilities:
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..')

View File

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

View File

@@ -25,7 +25,7 @@ from managePHP.phpManager import PHPManager
from plogical.vhostConfs import vhostConfs
from ApachController.ApacheVhosts import ApacheVhost
try:
from websiteFunctions.models import Websites, ChildDomains, aliasDomains
from websiteFunctions.models import Websites, ChildDomains, aliasDomains, DockerSites
from databases.models import Databases
except:
pass
@@ -404,6 +404,23 @@ class vhost:
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:
mysqlUtilities.deleteDatabase(items.dbName, items.dbUser)

View File

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

View File

@@ -237,7 +237,7 @@ app.controller('createWebsite', function ($scope, $http, $timeout, $window) {
$("#listFail").hide();
app.controller('listWebsites', function ($scope, $http) {
app.controller('listWebsites', function ($scope, $http, $window) {
$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();
}, 3000);
} else {
$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">
<h6 style="font-weight: bold">Search Engine Indexing</h6>
<div class="custom-control custom-switch">
<input ng-click="UpdateWPSettings('searchIndex')"
type="checkbox"
class="custom-control-input ng-pristine ng-untouched ng-valid ng-not-empty"
id="searchIndex">
<input type="checkbox"
class="custom-control-input"
id="searchIndex"
ng-click="UpdateWPSettings('searchIndex')"
ng-checked="searchIndex == 1">
<label class="custom-control-label"
for="searchIndex"></label>
</div>

View File

@@ -23,6 +23,31 @@
$scope.wpSitesCount = $scope.debug.wp_sites_count;
$scope.currentPage = 1;
$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() {
var filteredSites = $scope.wpSites;
@@ -66,12 +91,12 @@
var settingMap = {
'search-indexing': 'searchIndex',
'debugging': 'debugging',
'password-protection': 'passwordprotection',
'password-protection': 'passwordProtection',
'maintenance-mode': 'maintenanceMode'
};
var data = {
siteId: site.id,
WPid: site.id,
setting: setting,
value: site[settingMap[setting]] ? 1 : 0
};
@@ -110,13 +135,28 @@
GLobalAjaxCall($http, "{% url 'GetCurrentPlugins' %}", data,
function(response) {
if (response.data.status === 1) {
try {
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;
} 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) {
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.totalThemes = themes.length;
}
site.loadingTheme = false;
},
function(response) {
site.activeTheme = 'Error';
site.loadingTheme = false;
}
);
}
@@ -154,23 +196,135 @@
site.debugging = data.debugging === 1;
site.passwordProtection = data.passwordprotection === 1;
site.maintenanceMode = data.maintenanceMode === 1;
site.loading = false;
fetchPluginData(site);
fetchThemeData(site);
} else {
site.phpVersion = 'PHP 7.4'; // Default value on error
site.loading = false;
console.log('Failed to fetch site data:', response.data.error_message);
}
},
function(response) {
site.phpVersion = 'PHP 7.4'; // Default value on error
site.loading = false;
console.log('Failed to fetch site data');
}
);
}
if ($scope.wpSites) {
$scope.wpSites.forEach(fetchSiteData);
if ($scope.wpSites && $scope.wpSites.length > 0) {
// 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
@@ -231,7 +385,16 @@
<div class="wp-site-header">
<div class="row">
<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 class="col-sm-4 text-right">
<a ng-href="{% url 'WPHome' %}?ID={$ site.id $}" class="btn btn-primary btn-sm">Manage</a>
@@ -239,7 +402,7 @@
</div>
</div>
</div>
<div class="wp-site-content">
<div class="wp-site-content" ng-if="isExpanded(site.id)">
<div class="row">
<div class="col-sm-3">
<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="info-box">
<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 class="col-sm-3">
<div class="info-box">
<label>PHP Version</label>
<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 class="col-sm-3">
<div class="info-box">
<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 class="col-sm-3">
<div class="info-box">
<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>
@@ -301,7 +469,9 @@
<div class="col-sm-6">
<div class="checkbox">
<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
</label>
</div>
@@ -339,6 +509,36 @@
</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>
<style>
@@ -389,6 +589,18 @@
.text-center .btn {
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>
{% endblock content %}

View File

@@ -7,6 +7,9 @@
{% get_current_language as 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>
$(document).ready(function () {
$('[data-toggle="tooltip"]').tooltip();
@@ -14,6 +17,45 @@
</script>
<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">
<h2 id="domainNamePage">{% trans "List Websites" %}
@@ -84,97 +126,146 @@
<div class="col-md-12">
<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"
title="Disk Usage">&emsp;</i>
<span ng-bind="web.diskUsed" style="text-transform: none"></span>
</div>
<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"
title="Packages">&emsp;</i>
<span ng-bind="web.package" style="text-transform: none"></span>
</div>
<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>
<span ng-bind="web.admin" style="text-transform: none"></span>
</div>
<div class="col-md-3 content-box-header">
<i class="p fa fa-wordpress btn-icon text-muted" ng-click="showWPSites(web.domain)"
data-toggle="tooltip" data-placement="right" title="Show WordPress Sites">&emsp;</i>
<span ng-if="web.wp_sites && web.wp_sites.length > 0" style="text-transform: none">
{$ web.wp_sites.length $} WordPress Sites
<a href="javascript:void(0);" ng-click="showWPSites(web.domain)" class="wp-sites-link">
<i class="fa-brands fa-wordpress btn-icon text-muted" data-toggle="tooltip"
data-placement="right" title="Show WordPress Sites"></i>
<span ng-if="!web.loadingWPSites" class="wp-sites-count">
{$ (web.wp_sites && web.wp_sites.length) || 0 $} WordPress Sites
</span>
<span ng-if="web.loadingWPSites" class="loading-indicator">
Loading <i class="fa fa-spinner fa-spin"></i>
</span>
</a>
</div>
</div>
<!-- WordPress Sites Section -->
<div ng-if="web.showWPSites && web.wp_sites && web.wp_sites.length > 0" class="card mt-3">
<div class="card-header">
<h5 class="mb-0">WordPress Sites</h5>
</div>
<div class="card-body">
<div class="col-md-12" ng-if="web.showWPSites && web.wp_sites && web.wp_sites.length > 0" style="padding: 15px 30px;">
<div ng-repeat="wp in web.wp_sites" class="wp-site-item">
<div class="row">
<div class="col-md-6 mb-4" ng-repeat="site in web.wp_sites">
<div class="card h-100">
<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="col-sm-12">
<div class="wp-site-header">
<div class="row">
<div class="col-md-6">
<p><strong>WordPress Version:</strong> {{site.version}}</p>
<p><strong>PHP Version:</strong> {{site.phpVersion}}</p>
<p><strong>Active Theme:</strong> {{site.theme}}</p>
<p><strong>Active Plugins:</strong> {{site.activePlugins}}</p>
<div class="col-sm-8">
<h4>
<i class="fa-brands fa-wordpress" style="color: #00749C; margin-right: 8px;"></i>
{$ wp.title $}
<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 class="col-md-6">
<div class="form-group">
<div class="custom-control custom-switch">
<input type="checkbox" class="custom-control-input"
id="searchIndex{{site.id}}"
ng-model="site.searchIndex"
ng-change="updateSetting(site.id, 'search-indexing', site.searchIndex ? 'enable' : 'disable')">
<label class="custom-control-label" for="searchIndex{{site.id}}">Search Indexing</label>
<div class="col-sm-4 text-right">
<button class="btn btn-outline-primary btn-sm wp-action-btn" ng-click="manageWP(wp.id)">
<i class="fa-solid fa-cog"></i> Manage
</button>
<button class="btn btn-outline-danger btn-sm wp-action-btn" ng-click="deleteWPSite(wp)">
<i class="fa-solid fa-trash"></i> Delete
</button>
</div>
</div>
<div class="form-group">
<div class="custom-control custom-switch">
<input type="checkbox" class="custom-control-input"
id="debugging{{site.id}}"
ng-model="site.debugging"
ng-change="updateSetting(site.id, 'debugging', site.debugging ? 'enable' : 'disable')">
<label class="custom-control-label" for="debugging{{site.id}}">Debugging</label>
</div>
<div class="wp-site-content">
<div class="row">
<div class="col-sm-3">
<img ng-src="{$ wp.screenshot $}"
alt="{$ wp.title $}"
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 class="form-group">
<div class="custom-control custom-switch">
<input type="checkbox" class="custom-control-input"
id="passwordProtection{{site.id}}"
ng-model="site.passwordProtection"
ng-change="updateSetting(site.id, 'password-protection', site.passwordProtection ? 'enable' : 'disable')">
<label class="custom-control-label" for="passwordProtection{{site.id}}">Password Protection</label>
<div class="col-sm-9">
<div class="row">
<div class="col-sm-3">
<div class="info-box">
<label>WordPress</label>
<span>{$ wp.version || 'Loading...' $}</span>
<i ng-if="wp.loading" class="fa fa-spinner fa-spin" style="margin-left: 5px; font-size: 12px;"></i>
</div>
</div>
<div class="form-group">
<div class="custom-control custom-switch">
<input type="checkbox" class="custom-control-input"
id="maintenanceMode{{site.id}}"
ng-model="site.maintenanceMode"
ng-change="updateSetting(site.id, 'maintenance-mode', site.maintenanceMode ? 'enable' : 'disable')">
<label class="custom-control-label" for="maintenanceMode{{site.id}}">Maintenance Mode</label>
<div class="col-sm-3">
<div class="info-box">
<label>PHP Version</label>
<span>{$ wp.phpVersion || 'Loading...' $}</span>
<i ng-if="wp.loading" 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>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>
@@ -186,6 +277,141 @@
</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">
<p>{% trans "Cannot list websites. Error message:" %} {$ errorMessage $}</p>
</div>
@@ -209,5 +435,6 @@
</div>
</div>
</div>
</div>
{% endblock %}

View File

@@ -51,6 +51,7 @@ urlpatterns = [
path('AddWPsiteforRemoteBackup', views.AddWPsiteforRemoteBackup, name='AddWPsiteforRemoteBackup'),
path('UpdateRemoteschedules', views.UpdateRemoteschedules, name='UpdateRemoteschedules'),
path('ScanWordpressSite', views.ScanWordpressSite, name='ScanWordpressSite'),
path('fetchWPDetails', views.fetchWPDetails, name='fetchWPDetails'),
# AddPlugin
path('ConfigurePlugins', views.ConfigurePlugins, name='ConfigurePlugins'),
@@ -178,6 +179,11 @@ urlpatterns = [
path('ListDockerSites', views.ListDockerSites, name='ListDockerSites'),
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
path('getSSHConfigs', views.getSSHConfigs, name='getSSHConfigs'),
path('deleteSSHKey', views.deleteSSHKey, name='deleteSSHKey'),
@@ -194,6 +200,4 @@ urlpatterns = [
# Catch all for domains
path('<domain>/<childDomain>', views.launchChild, name='launchChild'),
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.pluginManager import pluginManager
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):
val = request.session['userID']
@@ -1843,19 +1846,40 @@ def Dockersitehome(request, dockerapp):
except KeyError:
return redirect(loadLoginPage)
def GetWPSitesByDomain(request):
def fetchWPDetails(request):
try:
userID = request.session['userID']
data = json.loads(request.body)
domain = data['domain']
data = {
'domain': request.POST.get('domain')
}
wm = WebsiteManager()
response = wm.GetWPSitesByDomain(userID, data)
return response
return wm.fetchWPSitesForDomain(userID, data)
except KeyError:
return redirect(reverse('login'))
except BaseException as msg:
data_ret = {'status': 0, 'error_message': str(msg)}
json_data = json.dumps(data_ret)
return HttpResponse(json_data)
return redirect(loadLoginPage)
@csrf_exempt
def startContainer(request):
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