mirror of
https://github.com/usmannasir/cyberpanel.git
synced 2025-11-10 15:26:13 +01:00
Merge branch 'v2.4.0-dev' of github.com:usmannasir/cyberpanel into v2.4.0-dev
This commit is contained in:
@@ -4,8 +4,8 @@ class CLMain():
|
|||||||
def __init__(self):
|
def __init__(self):
|
||||||
self.path = '/usr/local/CyberCP/version.txt'
|
self.path = '/usr/local/CyberCP/version.txt'
|
||||||
#versionInfo = json.loads(open(self.path, 'r').read())
|
#versionInfo = json.loads(open(self.path, 'r').read())
|
||||||
self.version = '2.3'
|
self.version = '2.4'
|
||||||
self.build = '9'
|
self.build = '0'
|
||||||
|
|
||||||
ipFile = "/etc/cyberpanel/machineIP"
|
ipFile = "/etc/cyberpanel/machineIP"
|
||||||
f = open(ipFile)
|
f = open(ipFile)
|
||||||
|
|||||||
@@ -34,6 +34,7 @@ import googleapiclient.discovery
|
|||||||
from googleapiclient.discovery import build
|
from googleapiclient.discovery import build
|
||||||
from websiteFunctions.models import NormalBackupDests, NormalBackupJobs, NormalBackupSites
|
from websiteFunctions.models import NormalBackupDests, NormalBackupJobs, NormalBackupSites
|
||||||
from plogical.IncScheduler import IncScheduler
|
from plogical.IncScheduler import IncScheduler
|
||||||
|
from django.http import JsonResponse
|
||||||
|
|
||||||
class BackupManager:
|
class BackupManager:
|
||||||
localBackupPath = '/home/cyberpanel/localBackupPath'
|
localBackupPath = '/home/cyberpanel/localBackupPath'
|
||||||
@@ -2338,4 +2339,76 @@ class BackupManager:
|
|||||||
json_data = json.dumps(data_ret)
|
json_data = json.dumps(data_ret)
|
||||||
return HttpResponse(json_data)
|
return HttpResponse(json_data)
|
||||||
|
|
||||||
|
def ReconfigureSubscription(self, request=None, userID=None, data=None):
|
||||||
|
try:
|
||||||
|
if not data:
|
||||||
|
return JsonResponse({'status': 0, 'error_message': 'No data provided'})
|
||||||
|
|
||||||
|
subscription_id = data['subscription_id']
|
||||||
|
customer_id = data['customer_id']
|
||||||
|
plan_name = data['plan_name']
|
||||||
|
amount = data['amount']
|
||||||
|
interval = data['interval']
|
||||||
|
|
||||||
|
# Call platform API to update SFTP key
|
||||||
|
import requests
|
||||||
|
import json
|
||||||
|
|
||||||
|
url = 'http://platform.cyberpersons.com/Billing/ReconfigureSubscription'
|
||||||
|
|
||||||
|
payload = {
|
||||||
|
'subscription_id': subscription_id,
|
||||||
|
'key': ProcessUtilities.outputExecutioner(f'cat /root/.ssh/cyberpanel.pub'),
|
||||||
|
'serverIP': ACLManager.fetchIP(),
|
||||||
|
'email': data['email'],
|
||||||
|
'code': data['code']
|
||||||
|
}
|
||||||
|
|
||||||
|
headers = {'Content-Type': 'application/json'}
|
||||||
|
response = requests.post(url, headers=headers, data=json.dumps(payload))
|
||||||
|
|
||||||
|
if response.status_code == 200:
|
||||||
|
response_data = response.json()
|
||||||
|
if response_data.get('status') == 1:
|
||||||
|
# Create OneClickBackups record
|
||||||
|
from IncBackups.models import OneClickBackups
|
||||||
|
backup_plan = OneClickBackups(
|
||||||
|
owner=Administrator.objects.get(pk=userID),
|
||||||
|
planName=plan_name,
|
||||||
|
months='1' if interval == 'month' else '12',
|
||||||
|
price=amount,
|
||||||
|
customer=customer_id,
|
||||||
|
subscription=subscription_id,
|
||||||
|
sftpUser=response_data.get('sftpUser'),
|
||||||
|
state=1 # Set as active since SFTP is already configured
|
||||||
|
)
|
||||||
|
backup_plan.save()
|
||||||
|
|
||||||
|
# Create SFTP destination in CyberPanel
|
||||||
|
finalDic = {
|
||||||
|
'IPAddress': response_data.get('ipAddress'),
|
||||||
|
'password': 'NOT-NEEDED',
|
||||||
|
'backupSSHPort': '22',
|
||||||
|
'userName': response_data.get('sftpUser'),
|
||||||
|
'type': 'SFTP',
|
||||||
|
'path': 'cpbackups',
|
||||||
|
'name': response_data.get('sftpUser')
|
||||||
|
}
|
||||||
|
|
||||||
|
wm = BackupManager()
|
||||||
|
response_inner = wm.submitDestinationCreation(userID, finalDic)
|
||||||
|
response_data_inner = json.loads(response_inner.content.decode('utf-8'))
|
||||||
|
|
||||||
|
if response_data_inner.get('status') == 0:
|
||||||
|
return JsonResponse({'status': 0, 'error_message': response_data_inner.get('error_message')})
|
||||||
|
|
||||||
|
return JsonResponse({'status': 1})
|
||||||
|
else:
|
||||||
|
return JsonResponse({'status': 0, 'error_message': response_data.get('error_message')})
|
||||||
|
else:
|
||||||
|
return JsonResponse({'status': 0, 'error_message': f'Platform API error: {response.text}'})
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
return JsonResponse({'status': 0, 'error_message': str(e)})
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -2,6 +2,309 @@
|
|||||||
* Created by usman on 9/17/17.
|
* Created by usman on 9/17/17.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
// Using existing CyberCP module
|
||||||
|
app.controller('backupPlanNowOneClick', function($scope, $http) {
|
||||||
|
$scope.cyberpanelLoading = true;
|
||||||
|
$scope.showVerification = false;
|
||||||
|
$scope.verificationCodeSent = false;
|
||||||
|
|
||||||
|
$scope.showEmailVerification = function() {
|
||||||
|
console.log('showEmailVerification called');
|
||||||
|
$scope.showVerification = true;
|
||||||
|
};
|
||||||
|
|
||||||
|
$scope.cancelVerification = function() {
|
||||||
|
$scope.showVerification = false;
|
||||||
|
$scope.verificationCodeSent = false;
|
||||||
|
$scope.verificationEmail = '';
|
||||||
|
$scope.verificationCode = '';
|
||||||
|
};
|
||||||
|
|
||||||
|
$scope.sendVerificationCode = function() {
|
||||||
|
$scope.cyberpanelLoading = false;
|
||||||
|
|
||||||
|
var config = {
|
||||||
|
headers: {
|
||||||
|
'X-CSRFToken': getCookie('csrftoken')
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
$http.post('https://platform.cyberpersons.com/Billing/SendBackupVerificationCode', {
|
||||||
|
email: $scope.verificationEmail
|
||||||
|
}, config).then(function(response) {
|
||||||
|
$scope.cyberpanelLoading = true;
|
||||||
|
if (response.data.status == 1) {
|
||||||
|
$scope.verificationCodeSent = true;
|
||||||
|
new PNotify({
|
||||||
|
title: 'Success',
|
||||||
|
text: 'Verification code sent to your email.',
|
||||||
|
type: 'success'
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
new PNotify({
|
||||||
|
title: 'Error',
|
||||||
|
text: response.data.error_message,
|
||||||
|
type: 'error'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, function(error) {
|
||||||
|
$scope.cyberpanelLoading = true;
|
||||||
|
new PNotify({
|
||||||
|
title: 'Error',
|
||||||
|
text: 'Could not send verification code. Please try again.',
|
||||||
|
type: 'error'
|
||||||
|
});
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
$scope.verifyCode = function() {
|
||||||
|
$scope.cyberpanelLoading = false;
|
||||||
|
|
||||||
|
var config = {
|
||||||
|
headers: {
|
||||||
|
'X-CSRFToken': getCookie('csrftoken')
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
$http.post('https://platform.cyberpersons.com/Billing/VerifyBackupCode', {
|
||||||
|
email: $scope.verificationEmail,
|
||||||
|
code: $scope.verificationCode
|
||||||
|
}, config).then(function(response) {
|
||||||
|
if (response.data.status == 1) {
|
||||||
|
// After successful verification, fetch Stripe subscriptions
|
||||||
|
$http.post('https://platform.cyberpersons.com/Billing/FetchStripeSubscriptionsByEmail', {
|
||||||
|
email: $scope.verificationEmail,
|
||||||
|
code: $scope.verificationCode
|
||||||
|
}, config).then(function(subResponse) {
|
||||||
|
$scope.cyberpanelLoading = true;
|
||||||
|
if (subResponse.data.status == 1) {
|
||||||
|
$scope.showVerification = false;
|
||||||
|
$scope.subscriptions = subResponse.data.subscriptions;
|
||||||
|
$scope.showSubscriptionsTable = true;
|
||||||
|
|
||||||
|
if ($scope.subscriptions.length == 0) {
|
||||||
|
new PNotify({
|
||||||
|
title: 'Info',
|
||||||
|
text: 'No active subscriptions found for this email.',
|
||||||
|
type: 'info'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
new PNotify({
|
||||||
|
title: 'Error',
|
||||||
|
text: subResponse.data.error_message,
|
||||||
|
type: 'error'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, function(error) {
|
||||||
|
$scope.cyberpanelLoading = true;
|
||||||
|
new PNotify({
|
||||||
|
title: 'Error',
|
||||||
|
text: 'Could not fetch subscriptions. Please try again.',
|
||||||
|
type: 'error'
|
||||||
|
});
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
$scope.cyberpanelLoading = true;
|
||||||
|
new PNotify({
|
||||||
|
title: 'Error',
|
||||||
|
text: response.data.error_message,
|
||||||
|
type: 'error'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, function(error) {
|
||||||
|
$scope.cyberpanelLoading = true;
|
||||||
|
new PNotify({
|
||||||
|
title: 'Error',
|
||||||
|
text: 'Could not verify code. Please try again.',
|
||||||
|
type: 'error'
|
||||||
|
});
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
$scope.fetchBackupPlans = function() {
|
||||||
|
$scope.cyberpanelLoading = false;
|
||||||
|
|
||||||
|
var config = {
|
||||||
|
headers: {
|
||||||
|
'X-CSRFToken': getCookie('csrftoken')
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
$http.post('https://platform.cyberpersons.com/Billing/FetchBackupPlans', {
|
||||||
|
email: $scope.verificationEmail
|
||||||
|
}, config).then(function(response) {
|
||||||
|
$scope.cyberpanelLoading = true;
|
||||||
|
if (response.data.status == 1) {
|
||||||
|
$scope.plans = response.data.plans;
|
||||||
|
new PNotify({
|
||||||
|
title: 'Success',
|
||||||
|
text: 'Backup plans fetched successfully.',
|
||||||
|
type: 'success'
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
new PNotify({
|
||||||
|
title: 'Error',
|
||||||
|
text: response.data.error_message,
|
||||||
|
type: 'error'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, function(error) {
|
||||||
|
$scope.cyberpanelLoading = true;
|
||||||
|
new PNotify({
|
||||||
|
title: 'Error',
|
||||||
|
text: 'Could not fetch backup plans. Please try again.',
|
||||||
|
type: 'error'
|
||||||
|
});
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
$scope.BuyNowBackupP = function (planName, monthlyPrice, yearlyPrice, months) {
|
||||||
|
const baseURL = 'https://platform.cyberpersons.com/Billing/CreateOrderforBackupPlans';
|
||||||
|
// Get the current URL
|
||||||
|
var currentURL = window.location.href;
|
||||||
|
|
||||||
|
// Find the position of the question mark
|
||||||
|
const queryStringIndex = currentURL.indexOf('?');
|
||||||
|
|
||||||
|
// Check if there is a query string
|
||||||
|
currentURL = queryStringIndex !== -1 ? currentURL.substring(0, queryStringIndex) : currentURL;
|
||||||
|
|
||||||
|
// Encode parameters to make them URL-safe
|
||||||
|
const params = new URLSearchParams({
|
||||||
|
planName: planName,
|
||||||
|
monthlyPrice: monthlyPrice,
|
||||||
|
yearlyPrice: yearlyPrice,
|
||||||
|
returnURL: currentURL, // Add the current URL as a query parameter
|
||||||
|
months: months
|
||||||
|
});
|
||||||
|
|
||||||
|
// Build the complete URL with query string
|
||||||
|
const fullURL = `${baseURL}?${params.toString()}`;
|
||||||
|
|
||||||
|
// Redirect to the constructed URL
|
||||||
|
window.location.href = fullURL;
|
||||||
|
};
|
||||||
|
|
||||||
|
$scope.PaypalBuyNowBackup = function (planName, monthlyPrice, yearlyPrice, months) {
|
||||||
|
const baseURL = 'https://platform.cyberpersons.com/Billing/PaypalCreateOrderforBackupPlans';
|
||||||
|
// Get the current URL
|
||||||
|
var currentURL = window.location.href;
|
||||||
|
|
||||||
|
// Find the position of the question mark
|
||||||
|
const queryStringIndex = currentURL.indexOf('?');
|
||||||
|
|
||||||
|
// Check if there is a query string
|
||||||
|
currentURL = queryStringIndex !== -1 ? currentURL.substring(0, queryStringIndex) : currentURL;
|
||||||
|
|
||||||
|
// Encode parameters to make them URL-safe
|
||||||
|
const params = new URLSearchParams({
|
||||||
|
planName: planName,
|
||||||
|
monthlyPrice: monthlyPrice,
|
||||||
|
yearlyPrice: yearlyPrice,
|
||||||
|
returnURL: currentURL, // Add the current URL as a query parameter
|
||||||
|
months: months
|
||||||
|
});
|
||||||
|
|
||||||
|
// Build the complete URL with query string
|
||||||
|
const fullURL = `${baseURL}?${params.toString()}`;
|
||||||
|
|
||||||
|
// Redirect to the constructed URL
|
||||||
|
window.location.href = fullURL;
|
||||||
|
};
|
||||||
|
|
||||||
|
$scope.DeployAccount = function (id) {
|
||||||
|
$scope.cyberpanelLoading = false;
|
||||||
|
|
||||||
|
url = "/backup/DeployAccount";
|
||||||
|
|
||||||
|
var data = {
|
||||||
|
id: id
|
||||||
|
};
|
||||||
|
|
||||||
|
var config = {
|
||||||
|
headers: {
|
||||||
|
'X-CSRFToken': getCookie('csrftoken')
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
$http.post(url, data, config).then(ListInitialDatas, cantLoadInitialDatas);
|
||||||
|
|
||||||
|
function ListInitialDatas(response) {
|
||||||
|
$scope.cyberpanelLoading = true;
|
||||||
|
if (response.data.status === 1) {
|
||||||
|
new PNotify({
|
||||||
|
title: 'Success',
|
||||||
|
text: 'Successfully deployed.',
|
||||||
|
type: 'success'
|
||||||
|
});
|
||||||
|
window.location.reload();
|
||||||
|
} else {
|
||||||
|
new PNotify({
|
||||||
|
title: 'Operation Failed!',
|
||||||
|
text: response.data.error_message,
|
||||||
|
type: 'error'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function cantLoadInitialDatas(response) {
|
||||||
|
$scope.cyberpanelLoading = true;
|
||||||
|
new PNotify({
|
||||||
|
title: 'Operation Failed!',
|
||||||
|
text: 'Could not connect to server, please refresh this page',
|
||||||
|
type: 'error'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
$scope.ReconfigureSubscription = function(subscription) {
|
||||||
|
$scope.cyberpanelLoading = false;
|
||||||
|
|
||||||
|
var config = {
|
||||||
|
headers: {
|
||||||
|
'X-CSRFToken': getCookie('csrftoken')
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
var data = {
|
||||||
|
subscription_id: subscription.subscription_id,
|
||||||
|
customer_id: subscription.customer,
|
||||||
|
plan_name: subscription.plan_name,
|
||||||
|
amount: subscription.amount,
|
||||||
|
interval: subscription.interval,
|
||||||
|
email: $scope.verificationEmail,
|
||||||
|
code: $scope.verificationCode
|
||||||
|
};
|
||||||
|
|
||||||
|
$http.post('/backup/ReconfigureSubscription', data, config).then(function(response) {
|
||||||
|
$scope.cyberpanelLoading = true;
|
||||||
|
if (response.data.status === 1) {
|
||||||
|
new PNotify({
|
||||||
|
title: 'Success',
|
||||||
|
text: 'Subscription configured successfully for this server.',
|
||||||
|
type: 'success'
|
||||||
|
});
|
||||||
|
// Refresh the page to show new backup plan in the list
|
||||||
|
window.location.reload();
|
||||||
|
} else {
|
||||||
|
new PNotify({
|
||||||
|
title: 'Error',
|
||||||
|
text: response.data.error_message,
|
||||||
|
type: 'error'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, function(error) {
|
||||||
|
$scope.cyberpanelLoading = true;
|
||||||
|
new PNotify({
|
||||||
|
title: 'Error',
|
||||||
|
text: 'Could not configure subscription. Please try again.',
|
||||||
|
type: 'error'
|
||||||
|
});
|
||||||
|
});
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
//*** Backup site ****//
|
//*** Backup site ****//
|
||||||
|
|
||||||
app.controller('backupWebsiteControl', function ($scope, $http, $timeout) {
|
app.controller('backupWebsiteControl', function ($scope, $http, $timeout) {
|
||||||
@@ -2045,307 +2348,6 @@ app.controller('scheduleBackup', function ($scope, $http, $window) {
|
|||||||
|
|
||||||
});
|
});
|
||||||
|
|
||||||
app.controller('backupPlanNowOneClick', function ($scope, $http, $window) {
|
|
||||||
$scope.cyberpanelLoading = true;
|
|
||||||
$scope.sftpHide = true;
|
|
||||||
$scope.localHide = true;
|
|
||||||
|
|
||||||
$scope.BuyNowBackupP = function (planName, monthlyPrice, yearlyPrice, months) {
|
|
||||||
|
|
||||||
const baseURL = 'https://platform.cyberpersons.com/Billing/CreateOrderforBackupPlans';
|
|
||||||
// Get the current URL
|
|
||||||
var currentURL = window.location.href;
|
|
||||||
|
|
||||||
// Find the position of the question mark
|
|
||||||
const queryStringIndex = currentURL.indexOf('?');
|
|
||||||
|
|
||||||
// Check if there is a query string
|
|
||||||
currentURL = queryStringIndex !== -1 ? currentURL.substring(0, queryStringIndex) : currentURL;
|
|
||||||
|
|
||||||
|
|
||||||
// Encode parameters to make them URL-safe
|
|
||||||
const params = new URLSearchParams({
|
|
||||||
planName: planName,
|
|
||||||
monthlyPrice: monthlyPrice,
|
|
||||||
yearlyPrice: yearlyPrice,
|
|
||||||
returnURL: currentURL, // Add the current URL as a query parameter
|
|
||||||
months: months
|
|
||||||
});
|
|
||||||
|
|
||||||
|
|
||||||
// Build the complete URL with query string
|
|
||||||
const fullURL = `${baseURL}?${params.toString()}`;
|
|
||||||
|
|
||||||
// Redirect to the constructed URL
|
|
||||||
|
|
||||||
window.location.href = fullURL;
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
$scope.fetchDetails = function () {
|
|
||||||
|
|
||||||
if ($scope.destinationType === 'SFTP') {
|
|
||||||
$scope.sftpHide = false;
|
|
||||||
$scope.localHide = true;
|
|
||||||
$scope.populateCurrentRecords();
|
|
||||||
} else {
|
|
||||||
$scope.sftpHide = true;
|
|
||||||
$scope.localHide = false;
|
|
||||||
$scope.populateCurrentRecords();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
$scope.populateCurrentRecords = function () {
|
|
||||||
|
|
||||||
$scope.cyberpanelLoading = false;
|
|
||||||
|
|
||||||
url = "/backup/getCurrentBackupDestinations";
|
|
||||||
|
|
||||||
var type = 'SFTP';
|
|
||||||
if ($scope.destinationType === 'SFTP') {
|
|
||||||
type = 'SFTP';
|
|
||||||
} else {
|
|
||||||
type = 'local';
|
|
||||||
}
|
|
||||||
|
|
||||||
var data = {
|
|
||||||
type: type
|
|
||||||
};
|
|
||||||
|
|
||||||
var config = {
|
|
||||||
headers: {
|
|
||||||
'X-CSRFToken': getCookie('csrftoken')
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
|
|
||||||
$http.post(url, data, config).then(ListInitialDatas, cantLoadInitialDatas);
|
|
||||||
|
|
||||||
|
|
||||||
function ListInitialDatas(response) {
|
|
||||||
$scope.cyberpanelLoading = true;
|
|
||||||
if (response.data.status === 1) {
|
|
||||||
$scope.records = JSON.parse(response.data.data);
|
|
||||||
} else {
|
|
||||||
new PNotify({
|
|
||||||
title: 'Operation Failed!',
|
|
||||||
text: response.data.error_message,
|
|
||||||
type: 'error'
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
function cantLoadInitialDatas(response) {
|
|
||||||
$scope.cyberpanelLoading = true;
|
|
||||||
new PNotify({
|
|
||||||
title: 'Operation Failed!',
|
|
||||||
text: 'Could not connect to server, please refresh this page',
|
|
||||||
type: 'error'
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
};
|
|
||||||
|
|
||||||
$scope.addDestination = function (type) {
|
|
||||||
$scope.cyberpanelLoading = false;
|
|
||||||
|
|
||||||
url = "/backup/submitDestinationCreation";
|
|
||||||
|
|
||||||
if (type === 'SFTP') {
|
|
||||||
var data = {
|
|
||||||
type: type,
|
|
||||||
name: $scope.name,
|
|
||||||
IPAddress: $scope.IPAddress,
|
|
||||||
userName: $scope.userName,
|
|
||||||
password: $scope.password,
|
|
||||||
backupSSHPort: $scope.backupSSHPort,
|
|
||||||
path: $scope.path
|
|
||||||
};
|
|
||||||
} else {
|
|
||||||
var data = {
|
|
||||||
type: type,
|
|
||||||
path: $scope.localPath,
|
|
||||||
name: $scope.name
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
var config = {
|
|
||||||
headers: {
|
|
||||||
'X-CSRFToken': getCookie('csrftoken')
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
|
|
||||||
$http.post(url, data, config).then(ListInitialDatas, cantLoadInitialDatas);
|
|
||||||
|
|
||||||
|
|
||||||
function ListInitialDatas(response) {
|
|
||||||
$scope.cyberpanelLoading = true;
|
|
||||||
$scope.populateCurrentRecords();
|
|
||||||
if (response.data.status === 1) {
|
|
||||||
new PNotify({
|
|
||||||
title: 'Success!',
|
|
||||||
text: 'Destination successfully added.',
|
|
||||||
type: 'success'
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
new PNotify({
|
|
||||||
title: 'Operation Failed!',
|
|
||||||
text: response.data.error_message,
|
|
||||||
type: 'error'
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
function cantLoadInitialDatas(response) {
|
|
||||||
$scope.cyberpanelLoading = true;
|
|
||||||
new PNotify({
|
|
||||||
title: 'Operation Failed!',
|
|
||||||
text: 'Could not connect to server, please refresh this page',
|
|
||||||
type: 'error'
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
};
|
|
||||||
|
|
||||||
$scope.removeDestination = function (type, nameOrPath) {
|
|
||||||
$scope.cyberpanelLoading = false;
|
|
||||||
|
|
||||||
|
|
||||||
url = "/backup/deleteDestination";
|
|
||||||
|
|
||||||
var data = {
|
|
||||||
type: type,
|
|
||||||
nameOrPath: nameOrPath,
|
|
||||||
};
|
|
||||||
|
|
||||||
var config = {
|
|
||||||
headers: {
|
|
||||||
'X-CSRFToken': getCookie('csrftoken')
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
|
|
||||||
$http.post(url, data, config).then(ListInitialDatas, cantLoadInitialDatas);
|
|
||||||
|
|
||||||
|
|
||||||
function ListInitialDatas(response) {
|
|
||||||
$scope.cyberpanelLoading = true;
|
|
||||||
$scope.populateCurrentRecords();
|
|
||||||
if (response.data.status === 1) {
|
|
||||||
new PNotify({
|
|
||||||
title: 'Success!',
|
|
||||||
text: 'Destination successfully removed.',
|
|
||||||
type: 'success'
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
new PNotify({
|
|
||||||
title: 'Operation Failed!',
|
|
||||||
text: response.data.error_message,
|
|
||||||
type: 'error'
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
function cantLoadInitialDatas(response) {
|
|
||||||
$scope.cyberpanelLoading = true;
|
|
||||||
new PNotify({
|
|
||||||
title: 'Operation Failed!',
|
|
||||||
text: 'Could not connect to server, please refresh this page',
|
|
||||||
type: 'error'
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
};
|
|
||||||
|
|
||||||
$scope.DeployAccount = function (id) {
|
|
||||||
$scope.cyberpanelLoading = false;
|
|
||||||
|
|
||||||
url = "/backup/DeployAccount";
|
|
||||||
|
|
||||||
var data = {
|
|
||||||
id:id
|
|
||||||
|
|
||||||
};
|
|
||||||
|
|
||||||
var config = {
|
|
||||||
headers: {
|
|
||||||
'X-CSRFToken': getCookie('csrftoken')
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
$http.post(url, data, config).then(ListInitialDatas, cantLoadInitialDatas);
|
|
||||||
|
|
||||||
function ListInitialDatas(response) {
|
|
||||||
|
|
||||||
$scope.cyberpanelLoading = true;
|
|
||||||
if (response.data.status === 1) {
|
|
||||||
new PNotify({
|
|
||||||
title: 'Success',
|
|
||||||
text: 'Successfully deployed.',
|
|
||||||
type: 'success'
|
|
||||||
});
|
|
||||||
$window.location.reload();
|
|
||||||
|
|
||||||
|
|
||||||
} else {
|
|
||||||
new PNotify({
|
|
||||||
title: 'Operation Failed!',
|
|
||||||
text: response.data.error_message,
|
|
||||||
type: 'error'
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
function cantLoadInitialDatas(response) {
|
|
||||||
$scope.couldNotConnect = false;
|
|
||||||
restoreBackupButton.disabled = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
};
|
|
||||||
|
|
||||||
//// paypal
|
|
||||||
|
|
||||||
$scope.PaypalBuyNowBackup = function (planName, monthlyPrice, yearlyPrice, months) {
|
|
||||||
|
|
||||||
const baseURL = 'https://platform.cyberpersons.com/Billing/PaypalCreateOrderforBackupPlans';
|
|
||||||
// Get the current URL
|
|
||||||
var currentURL = window.location.href;
|
|
||||||
|
|
||||||
// Find the position of the question mark
|
|
||||||
const queryStringIndex = currentURL.indexOf('?');
|
|
||||||
|
|
||||||
// Check if there is a query string
|
|
||||||
currentURL = queryStringIndex !== -1 ? currentURL.substring(0, queryStringIndex) : currentURL;
|
|
||||||
|
|
||||||
// Encode parameters to make them URL-safe
|
|
||||||
const params = new URLSearchParams({
|
|
||||||
planName: planName,
|
|
||||||
monthlyPrice: monthlyPrice,
|
|
||||||
yearlyPrice: yearlyPrice,
|
|
||||||
returnURL: currentURL, // Add the current URL as a query parameter
|
|
||||||
months: months
|
|
||||||
});
|
|
||||||
|
|
||||||
|
|
||||||
// Build the complete URL with query string
|
|
||||||
const fullURL = `${baseURL}?${params.toString()}`;
|
|
||||||
|
|
||||||
// Redirect to the constructed URL
|
|
||||||
|
|
||||||
window.location.href = fullURL;
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
});
|
|
||||||
|
|
||||||
|
|
||||||
app.controller('OneClickrestoreWebsiteControl', function ($scope, $http, $timeout) {
|
app.controller('OneClickrestoreWebsiteControl', function ($scope, $http, $timeout) {
|
||||||
|
|
||||||
$scope.restoreLoading = true;
|
$scope.restoreLoading = true;
|
||||||
|
|||||||
@@ -5,177 +5,601 @@
|
|||||||
|
|
||||||
{% load static %}
|
{% load static %}
|
||||||
|
|
||||||
|
|
||||||
{% get_current_language as LANGUAGE_CODE %}
|
{% get_current_language as LANGUAGE_CODE %}
|
||||||
<!-- Current language: {{ LANGUAGE_CODE }} -->
|
<!-- Current language: {{ LANGUAGE_CODE }} -->
|
||||||
|
|
||||||
|
<style>
|
||||||
|
/* Use CyberPanel color scheme */
|
||||||
|
:root {
|
||||||
|
--primary-color: #0078ff;
|
||||||
|
--secondary-color: #2096f3;
|
||||||
|
--bg-light: #f5f7f9;
|
||||||
|
--border-color: #e0e6ed;
|
||||||
|
--text-dark: #3e4b5b;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-box {
|
||||||
|
background-color: #e8f4fd;
|
||||||
|
border: 1px solid #bfdff1;
|
||||||
|
border-radius: 4px;
|
||||||
|
padding: 15px;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cp-card {
|
||||||
|
background: #fff;
|
||||||
|
border-radius: 8px;
|
||||||
|
box-shadow: 0 2px 10px rgba(0,0,0,0.08);
|
||||||
|
margin-bottom: 20px;
|
||||||
|
border: 1px solid var(--border-color);
|
||||||
|
transition: transform 0.2s, box-shadow 0.2s;
|
||||||
|
height: 100%;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cp-card:hover {
|
||||||
|
transform: translateY(-5px);
|
||||||
|
box-shadow: 0 5px 15px rgba(0,0,0,0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.cp-card-header {
|
||||||
|
padding: 18px 20px;
|
||||||
|
border-bottom: 1px solid var(--border-color);
|
||||||
|
background: var(--bg-light);
|
||||||
|
font-weight: 600;
|
||||||
|
font-size: 18px;
|
||||||
|
border-top-left-radius: 8px;
|
||||||
|
border-top-right-radius: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cp-card-body {
|
||||||
|
padding: 20px;
|
||||||
|
flex-grow: 1;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cp-btn {
|
||||||
|
display: inline-block;
|
||||||
|
font-weight: 500;
|
||||||
|
text-align: center;
|
||||||
|
white-space: nowrap;
|
||||||
|
vertical-align: middle;
|
||||||
|
user-select: none;
|
||||||
|
border: 1px solid transparent;
|
||||||
|
padding: 10px 16px;
|
||||||
|
font-size: 14px;
|
||||||
|
border-radius: 6px;
|
||||||
|
transition: all 0.2s;
|
||||||
|
cursor: pointer;
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cp-btn-primary {
|
||||||
|
background-color: var(--primary-color);
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cp-btn-primary:hover {
|
||||||
|
background-color: #0056b3;
|
||||||
|
transform: translateY(-2px);
|
||||||
|
box-shadow: 0 4px 8px rgba(0,0,0,0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.cp-btn-outline {
|
||||||
|
background-color: white;
|
||||||
|
border: 1px solid var(--border-color);
|
||||||
|
color: var(--text-dark);
|
||||||
|
}
|
||||||
|
|
||||||
|
.cp-btn-outline:hover {
|
||||||
|
background-color: var(--bg-light);
|
||||||
|
transform: translateY(-2px);
|
||||||
|
box-shadow: 0 4px 8px rgba(0,0,0,0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.cp-btn-block {
|
||||||
|
display: block;
|
||||||
|
width: 100%;
|
||||||
|
margin-top: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cp-table {
|
||||||
|
width: 100%;
|
||||||
|
margin-bottom: 0;
|
||||||
|
border-collapse: collapse;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cp-table th {
|
||||||
|
background-color: var(--bg-light);
|
||||||
|
border-bottom: 1px solid var(--border-color);
|
||||||
|
padding: 15px;
|
||||||
|
text-align: left;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cp-table td {
|
||||||
|
padding: 15px;
|
||||||
|
border-bottom: 1px solid var(--border-color);
|
||||||
|
vertical-align: middle;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cp-table tr:last-child td {
|
||||||
|
border-bottom: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cp-badge {
|
||||||
|
display: inline-block;
|
||||||
|
padding: 5px 10px;
|
||||||
|
border-radius: 30px;
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cp-badge-primary {
|
||||||
|
background-color: var(--primary-color);
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cp-badge-info {
|
||||||
|
background-color: #17a2b8;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cp-form-control {
|
||||||
|
display: block;
|
||||||
|
width: 100%;
|
||||||
|
padding: 10px 15px;
|
||||||
|
background-color: #fff;
|
||||||
|
border: 1px solid var(--border-color);
|
||||||
|
border-radius: 6px;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.price-box {
|
||||||
|
background-color: var(--bg-light);
|
||||||
|
border-radius: 6px;
|
||||||
|
padding: 15px;
|
||||||
|
margin-bottom: 15px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.price-amount {
|
||||||
|
font-size: 28px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--primary-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.price-period {
|
||||||
|
font-size: 14px;
|
||||||
|
color: #6c757d;
|
||||||
|
}
|
||||||
|
|
||||||
|
.plan-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fill, minmax(260px, 1fr));
|
||||||
|
gap: 25px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.billing-cycle {
|
||||||
|
display: inline-block;
|
||||||
|
padding: 6px 12px;
|
||||||
|
background-color: var(--primary-color);
|
||||||
|
color: white;
|
||||||
|
border-radius: 20px;
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.action-btns {
|
||||||
|
display: flex;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.action-btns a {
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 25px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-title {
|
||||||
|
font-size: 26px;
|
||||||
|
margin-bottom: 0;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.section-title {
|
||||||
|
font-size: 20px;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
padding-bottom: 12px;
|
||||||
|
position: relative;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.section-title:after {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
bottom: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 50px;
|
||||||
|
height: 3px;
|
||||||
|
background-color: var(--primary-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-space {
|
||||||
|
margin-top: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.plans-container {
|
||||||
|
margin-top: 30px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-description {
|
||||||
|
color: #6c757d;
|
||||||
|
margin-bottom: 25px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.verify-email-section {
|
||||||
|
margin: 30px 0;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.verify-email-btn {
|
||||||
|
display: inline-block;
|
||||||
|
padding: 12px 24px;
|
||||||
|
background-color: var(--primary-color);
|
||||||
|
color: white;
|
||||||
|
border-radius: 6px;
|
||||||
|
text-decoration: none;
|
||||||
|
font-weight: 500;
|
||||||
|
transition: all 0.2s;
|
||||||
|
border: none;
|
||||||
|
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.verify-email-btn:hover {
|
||||||
|
background-color: #0056b3;
|
||||||
|
transform: translateY(-2px);
|
||||||
|
box-shadow: 0 4px 8px rgba(0,0,0,0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.plan-grid {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
|
||||||
|
.action-btns {
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
<div class="container">
|
<div class="container">
|
||||||
<div id="page-title">
|
|
||||||
<h2>{% trans "One-click Backups" %} - <a target="_blank"
|
<!-- Page header -->
|
||||||
href="https://youtu.be/mLjMg8Anq70"
|
<div class="page-header">
|
||||||
style="height: 23px;line-height: 21px;"
|
<h1 class="page-title">One-click Backups</h1>
|
||||||
class="btn btn-border btn-alt border-red btn-link font-red"
|
<a href="https://youtu.be/mLjMg8Anq70" target="_blank" class="cp-btn cp-btn-outline">
|
||||||
title=""><span>{% trans "One-Click Backup Docs" %}</span></a>
|
Watch Tutorial
|
||||||
</h2>
|
</a>
|
||||||
<p>{% trans "On this page you purchase and manage one-click backups." %}</p>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<p class="page-description">On this page you purchase and manage one-click backups.</p>
|
||||||
|
|
||||||
<div ng-controller="backupPlanNowOneClick" class="panel">
|
<div ng-controller="backupPlanNowOneClick">
|
||||||
<div class="panel-body">
|
<!-- Email Verification Button -->
|
||||||
<h3 class="title-hero">
|
<div class="verify-email-section" ng-hide="showVerification || showSubscriptionsTable">
|
||||||
{% trans "Set up Backup Destinations." %} <img ng-hide="cyberpanelLoading"
|
<a href="javascript:void(0)" class="verify-email-btn" ng-click="showEmailVerification()">
|
||||||
src="{% static 'images/loading.gif' %}">
|
{% trans "Fetch existing backup plans if any." %}
|
||||||
</h3>
|
</a>
|
||||||
<div class="example-box-wrapper">
|
</div>
|
||||||
|
|
||||||
{% if status == 1 %}
|
<!-- Email Verification Section -->
|
||||||
<div class="alert alert-info">
|
<div class="cp-card" ng-show="showVerification">
|
||||||
<p>You have successfully purchased a backup plan.</p>
|
<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>
|
||||||
{% elif status == 0 %}
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="alert alert-danger">
|
<!-- Active Subscriptions -->
|
||||||
<p>Your purchase was not successful.</p> {{ message }}
|
<div class="cp-card" ng-show="showSubscriptionsTable">
|
||||||
</div>
|
<div class="cp-card-header">
|
||||||
{% elif status == 4 %}
|
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>
|
||||||
|
|
||||||
<div class="alert alert-danger">
|
<!-- Status Messages -->
|
||||||
{{ message }}
|
{% if status == 1 %}
|
||||||
</div>
|
<div class="info-box">
|
||||||
{% endif %}
|
<p class="mb-0">You have successfully purchased a backup plan.</p>
|
||||||
|
</div>
|
||||||
|
{% elif status == 0 %}
|
||||||
|
<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="info-box" style="background-color: #f8d7da; border-color: #f5c6cb;">
|
||||||
|
<p class="mb-0">{{ message }}</p>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
<form action="/" class="form-horizontal bordered-row">
|
<!-- Your Backup Plans Section -->
|
||||||
|
<div class="cp-card mb-4">
|
||||||
<p style="font-size: 15px;margin: 1%;">With CyberPanel's one-click backups, you can easily back
|
<div class="cp-card-header">Your Backup Plans</div>
|
||||||
up your website to our secure
|
<div class="cp-card-body p-0">
|
||||||
servers in just 60 seconds. It's simple, fast, and reliable.</p>
|
<table class="cp-table">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
<!------ List of Purchased backup plans --------------->
|
<th>{% trans "Account" %}</th>
|
||||||
|
<th>{% trans "Plan Name" %}</th>
|
||||||
<div class="form-group">
|
<th>{% trans "Subscription" %}</th>
|
||||||
|
<th>{% trans "Billing Cycle" %}</th>
|
||||||
<div class="col-sm-12">
|
<th>{% trans "Purchase Date" %}</th>
|
||||||
|
<th>{% trans "Actions" %}</th>
|
||||||
<table class="table">
|
</tr>
|
||||||
<thead>
|
</thead>
|
||||||
<tr>
|
<tbody>
|
||||||
<th>{% trans "Account" %}</th>
|
{% for plan in bPlans %}
|
||||||
<th>{% trans "Plan Name" %}</th>
|
<tr>
|
||||||
<th>{% trans "Subscription" %}</th>
|
<td>{{ plan.sftpUser }}</td>
|
||||||
<th>{% trans "Billing Cycle" %}</th>
|
<td>{{ plan.planName }}</td>
|
||||||
<th>{% trans "Purchase Date" %}</th>
|
<td>{{ plan.subscription }}</td>
|
||||||
<th>{% trans "Actions" %}</th>
|
<td>
|
||||||
</tr>
|
<span class="billing-cycle">
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
{% for plan in bPlans %}
|
|
||||||
<tr>
|
|
||||||
<td>{{ plan.sftpUser }}</td>
|
|
||||||
<td>{{ plan.planName }}</td>
|
|
||||||
<td>{{ plan.subscription }}</td>
|
|
||||||
{% if plan.months == '1' %}
|
{% if plan.months == '1' %}
|
||||||
<td>${{ plan.price }}/month</td>
|
${{ plan.price }}/month
|
||||||
{% else %}
|
{% else %}
|
||||||
<td>${{ plan.price }}/year</td>
|
${{ plan.price }}/year
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<td>{{ plan.date }}</td>
|
</span>
|
||||||
<td>
|
</td>
|
||||||
{% if plan.state == 1 %}
|
<td>{{ plan.date }}</td>
|
||||||
<a
|
<td>
|
||||||
href="{% url 'ManageOCBackups' %}?id={{ plan.id }}">
|
<div class="action-btns">
|
||||||
<button style="margin-bottom: 1%" type="button"
|
{% if plan.state == 1 %}
|
||||||
class="btn btn-primary btn-lg btn-block">{% trans "Schedule Backups" %}</button>
|
<a href="{% url 'ManageOCBackups' %}?id={{ plan.id }}"
|
||||||
</a>
|
class="cp-btn cp-btn-primary">
|
||||||
<a href="{% url 'RestoreOCBackups' %}?id={{ plan.id }}">
|
{% trans "Schedule Backups" %}
|
||||||
<button type="button"
|
</a>
|
||||||
class="btn btn-primary btn-lg btn-block">{% trans "Restore Backups" %}</button>
|
<a href="{% url 'RestoreOCBackups' %}?id={{ plan.id }}"
|
||||||
</a>
|
class="cp-btn cp-btn-outline">
|
||||||
{% else %}
|
{% trans "Restore Backups" %}
|
||||||
<button type="button"
|
</a>
|
||||||
ng-click="DeployAccount('{{ plan.id }}')"
|
{% else %}
|
||||||
class="btn btn-primary btn-lg btn-block">{% trans "Deploy Account" %}</button>
|
|
||||||
{% endif %}
|
|
||||||
|
|
||||||
</td>
|
|
||||||
</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"
|
<button type="button"
|
||||||
ng-click="PaypalBuyNowBackup('{{ plan.name }}', '{{ plan.monthlyPrice }}', '{{ plan.yearlyPrice }}', 12)"
|
ng-click="DeployAccount('{{ plan.id }}')"
|
||||||
class="btn btn-primary btn-lg btn-block">{% trans "Buy Yearly (Paypal)" %}</button>
|
class="cp-btn cp-btn-primary">
|
||||||
{% if plan.name != '100GB' %}
|
{% trans "Deploy Account" %}
|
||||||
<button type="button"
|
</button>
|
||||||
ng-click="BuyNowBackupP('{{ plan.name }}', '{{ plan.monthlyPrice }}', '{{ plan.yearlyPrice }}', 1)"
|
{% endif %}
|
||||||
class="btn btn-primary btn-lg btn-block">{% trans "Buy Monthly via Card" %}</button>
|
</div>
|
||||||
{% endif %}
|
</td>
|
||||||
<button type="button"
|
</tr>
|
||||||
ng-click="BuyNowBackupP('{{ plan.name }}', '{{ plan.monthlyPrice }}', '{{ plan.yearlyPrice }}', 12)"
|
{% endfor %}
|
||||||
class="btn btn-primary btn-lg btn-block">{% trans "Buy Yearly via Card" %}</button>
|
</tbody>
|
||||||
</td>
|
</table>
|
||||||
</td>
|
</div>
|
||||||
</tr>
|
</div>
|
||||||
{% endfor %}
|
|
||||||
</tbody>
|
<!-- Available Backup Plans Section -->
|
||||||
</table>
|
<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>
|
||||||
|
<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>
|
||||||
|
</div>
|
||||||
<!------ List of backup plans --------------->
|
|
||||||
|
<!-- 500GB Plan -->
|
||||||
|
<div class="cp-card">
|
||||||
<!--- AWS End --->
|
<div class="cp-card-header">500GB</div>
|
||||||
|
<div class="cp-card-body">
|
||||||
|
<div class="price-box mb-3">
|
||||||
</form>
|
<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>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
@@ -10,6 +10,7 @@ urlpatterns = [
|
|||||||
re_path(r'^fetchOCSites$', views.fetchOCSites, name='fetchOCSites'),
|
re_path(r'^fetchOCSites$', views.fetchOCSites, name='fetchOCSites'),
|
||||||
re_path(r'^StartOCRestore$', views.StartOCRestore, name='StartOCRestore'),
|
re_path(r'^StartOCRestore$', views.StartOCRestore, name='StartOCRestore'),
|
||||||
re_path(r'^DeployAccount$', views.DeployAccount, name='DeployAccount'),
|
re_path(r'^DeployAccount$', views.DeployAccount, name='DeployAccount'),
|
||||||
|
re_path(r'^ReconfigureSubscription$', views.ReconfigureSubscription, name='ReconfigureSubscription'),
|
||||||
|
|
||||||
re_path(r'^backupSite$', views.backupSite, name='backupSite'),
|
re_path(r'^backupSite$', views.backupSite, name='backupSite'),
|
||||||
re_path(r'^restoreSite$', views.restoreSite, name='restoreSite'),
|
re_path(r'^restoreSite$', views.restoreSite, name='restoreSite'),
|
||||||
|
|||||||
@@ -5,6 +5,7 @@
|
|||||||
import json
|
import json
|
||||||
|
|
||||||
from django.shortcuts import redirect
|
from django.shortcuts import redirect
|
||||||
|
from django.http import HttpResponse
|
||||||
|
|
||||||
from backup.backupManager import BackupManager
|
from backup.backupManager import BackupManager
|
||||||
from backup.pluginManager import pluginManager
|
from backup.pluginManager import pluginManager
|
||||||
@@ -12,6 +13,8 @@ from loginSystem.views import loadLoginPage
|
|||||||
import os
|
import os
|
||||||
from plogical.CyberCPLogFileWriter import CyberCPLogFileWriter as logging
|
from plogical.CyberCPLogFileWriter import CyberCPLogFileWriter as logging
|
||||||
from django.views.decorators.csrf import csrf_exempt
|
from django.views.decorators.csrf import csrf_exempt
|
||||||
|
from django.contrib.auth.models import User
|
||||||
|
from loginSystem.models import Administrator
|
||||||
|
|
||||||
def loadBackupHome(request):
|
def loadBackupHome(request):
|
||||||
try:
|
try:
|
||||||
@@ -538,4 +541,15 @@ def DeployAccount(request):
|
|||||||
bm = BackupManager()
|
bm = BackupManager()
|
||||||
return bm.DeployAccount(request, userID)
|
return bm.DeployAccount(request, userID)
|
||||||
except KeyError:
|
except KeyError:
|
||||||
return redirect(loadLoginPage)
|
return redirect(loadLoginPage)
|
||||||
|
|
||||||
|
def ReconfigureSubscription(request):
|
||||||
|
try:
|
||||||
|
userID = request.session['userID']
|
||||||
|
bm = BackupManager()
|
||||||
|
data = json.loads(request.body)
|
||||||
|
return bm.ReconfigureSubscription(request, userID, data)
|
||||||
|
except BaseException as msg:
|
||||||
|
data_ret = {'status': 0, 'error_message': str(msg)}
|
||||||
|
json_data = json.dumps(data_ret)
|
||||||
|
return HttpResponse(json_data)
|
||||||
@@ -77,7 +77,7 @@
|
|||||||
|
|
||||||
<!-- HELPERS -->
|
<!-- HELPERS -->
|
||||||
|
|
||||||
{% with version="2.3.8.1.1" %}
|
{% with version="2.4.0" %}
|
||||||
|
|
||||||
<link rel="stylesheet" type="text/css" href="{% static 'baseTemplate/assets/finalBase/finalBase.css' %}">
|
<link rel="stylesheet" type="text/css" href="{% static 'baseTemplate/assets/finalBase/finalBase.css' %}">
|
||||||
|
|
||||||
@@ -87,7 +87,7 @@
|
|||||||
<link rel="stylesheet" type="text/css"
|
<link rel="stylesheet" type="text/css"
|
||||||
href="/static/baseTemplate/assets/themes/admin/color-schemes/default.css">
|
href="/static/baseTemplate/assets/themes/admin/color-schemes/default.css">
|
||||||
<link rel="stylesheet" type="text/css"
|
<link rel="stylesheet" type="text/css"
|
||||||
href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/4.7.0/css/font-awesome.css">
|
href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/4.7.0/css/font-awesome.min.css">
|
||||||
<link rel="stylesheet" type="text/css" href="{% static 'baseTemplate/custom-js/pnotify.custom.min.css' %}">
|
<link rel="stylesheet" type="text/css" href="{% static 'baseTemplate/custom-js/pnotify.custom.min.css' %}">
|
||||||
<link rel="stylesheet" type="text/css" href="{% static 'websiteFunctions/websiteFunctions.css' %}">
|
<link rel="stylesheet" type="text/css" href="{% static 'websiteFunctions/websiteFunctions.css' %}">
|
||||||
<link rel="icon" type="image/x-icon" href="{% static 'baseTemplate/assets/finalBase/favicon.png' %}">
|
<link rel="icon" type="image/x-icon" href="{% static 'baseTemplate/assets/finalBase/favicon.png' %}">
|
||||||
@@ -248,7 +248,7 @@
|
|||||||
title="{% trans 'Server IP Address' %}">
|
title="{% trans 'Server IP Address' %}">
|
||||||
<i class="glyph-icon tooltip-button icon-laptop" title="{% trans 'Server IP Address' %}"
|
<i class="glyph-icon tooltip-button icon-laptop" title="{% trans 'Server IP Address' %}"
|
||||||
data-original-title=".icon-laptop"></i>
|
data-original-title=".icon-laptop"></i>
|
||||||
<span style="color: #488a3f;font-weight: bold;">{{ ipAddress }}</span>
|
<span onclick="copyIPAddress(); return false;" style="color: #488a3f; font-weight: bold; cursor: pointer;" title="{% trans 'Click to copy IP' %}">{{ ipAddress }}</span>
|
||||||
</a>
|
</a>
|
||||||
<a id="sidebar-menu-item-dashboard" href="{% url 'index' %}"
|
<a id="sidebar-menu-item-dashboard" href="{% url 'index' %}"
|
||||||
title="{% trans 'Dashboard' %}">
|
title="{% trans 'Dashboard' %}">
|
||||||
@@ -376,7 +376,6 @@
|
|||||||
<a href="#" title="{% trans 'Dockersite' %}">
|
<a href="#" title="{% trans 'Dockersite' %}">
|
||||||
<div class="glyph-icon icon-globe" title="{% trans 'Docker Apps' %}"></div>
|
<div class="glyph-icon icon-globe" title="{% trans 'Docker Apps' %}"></div>
|
||||||
<span>{% trans "Docker Apps" %}</span>
|
<span>{% trans "Docker Apps" %}</span>
|
||||||
<span class="bs-label badge-yellow">{% trans "Beta" %}</span>
|
|
||||||
</a>
|
</a>
|
||||||
<div class="sidebar-submenu">
|
<div class="sidebar-submenu">
|
||||||
|
|
||||||
@@ -1198,6 +1197,7 @@
|
|||||||
<script src="{% static 'baseTemplate/custom-js/pnotify.custom.min.js' %}"></script>
|
<script src="{% static 'baseTemplate/custom-js/pnotify.custom.min.js' %}"></script>
|
||||||
<script src="{% static 'packages/packages.js' %}?ver={{ version }}"></script>
|
<script src="{% static 'packages/packages.js' %}?ver={{ version }}"></script>
|
||||||
<script src="{% static 'websiteFunctions/websiteFunctions.js' %}?ver={{ version }}"></script>
|
<script src="{% static 'websiteFunctions/websiteFunctions.js' %}?ver={{ version }}"></script>
|
||||||
|
<script type="text/javascript" src="{% static 'websiteFunctions/DockerContainers.js' %}?ver={{ version }}"></script>
|
||||||
<script src="{% static 'tuning/tuning.js' %}?ver={{ version }}"></script>
|
<script src="{% static 'tuning/tuning.js' %}?ver={{ version }}"></script>
|
||||||
<script src="{% static 'serverStatus/serverStatus.js' %}?ver={{ version }}"></script>
|
<script src="{% static 'serverStatus/serverStatus.js' %}?ver={{ version }}"></script>
|
||||||
<script src="{% static 'dns/dns.js' %}?ver={{ version }}"></script>
|
<script src="{% static 'dns/dns.js' %}?ver={{ version }}"></script>
|
||||||
@@ -1228,5 +1228,27 @@
|
|||||||
</div>
|
</div>
|
||||||
{% block footer_scripts %}
|
{% block footer_scripts %}
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
<script type="text/javascript">
|
||||||
|
function copyIPAddress() {
|
||||||
|
const ipAddress = '{{ ipAddress }}';
|
||||||
|
navigator.clipboard.writeText(ipAddress).then(function() {
|
||||||
|
// Show success notification using PNotify
|
||||||
|
new PNotify({
|
||||||
|
title: 'Success',
|
||||||
|
text: 'IP Address copied to clipboard!',
|
||||||
|
type: 'success',
|
||||||
|
delay: 2000
|
||||||
|
});
|
||||||
|
}).catch(function(err) {
|
||||||
|
// Show error notification using PNotify
|
||||||
|
new PNotify({
|
||||||
|
title: 'Error',
|
||||||
|
text: 'Failed to copy IP address',
|
||||||
|
type: 'error',
|
||||||
|
delay: 2000
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
</script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@@ -20,8 +20,8 @@ from plogical.httpProc import httpProc
|
|||||||
|
|
||||||
# Create your views here.
|
# Create your views here.
|
||||||
|
|
||||||
VERSION = '2.3'
|
VERSION = '2.4'
|
||||||
BUILD = 9
|
BUILD = 0
|
||||||
|
|
||||||
|
|
||||||
@ensure_csrf_cookie
|
@ensure_csrf_cookie
|
||||||
|
|||||||
@@ -765,7 +765,7 @@ else
|
|||||||
Check_Return
|
Check_Return
|
||||||
fi
|
fi
|
||||||
|
|
||||||
wget https://cyberpanel.sh/www.litespeedtech.com/packages/lsapi/wsgi-lsapi-2.1.tgz
|
wget https://www.litespeedtech.com/packages/lsapi/wsgi-lsapi-2.1.tgz
|
||||||
tar xf wsgi-lsapi-2.1.tgz
|
tar xf wsgi-lsapi-2.1.tgz
|
||||||
cd wsgi-lsapi-2.1 || exit
|
cd wsgi-lsapi-2.1 || exit
|
||||||
/usr/local/CyberPanel/bin/python ./configure.py
|
/usr/local/CyberPanel/bin/python ./configure.py
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
import os.path
|
import os.path
|
||||||
import sys
|
import sys
|
||||||
import django
|
import django
|
||||||
|
from datetime import datetime
|
||||||
|
|
||||||
from plogical.DockerSites import Docker_Sites
|
from plogical.DockerSites import Docker_Sites
|
||||||
|
|
||||||
@@ -1116,27 +1117,68 @@ class ContainerManager(multi.Thread):
|
|||||||
if admin.acl.adminStatus != 1:
|
if admin.acl.adminStatus != 1:
|
||||||
return ACLManager.loadError()
|
return ACLManager.loadError()
|
||||||
|
|
||||||
|
|
||||||
name = data['name']
|
name = data['name']
|
||||||
containerID = data['id']
|
containerID = data['id']
|
||||||
|
|
||||||
passdata = {}
|
# Create a Docker client
|
||||||
passdata["JobID"] = None
|
client = docker.from_env()
|
||||||
passdata['name'] = name
|
container = client.containers.get(containerID)
|
||||||
passdata['containerID'] = containerID
|
|
||||||
da = Docker_Sites(None, passdata)
|
|
||||||
retdata = da.ContainerInfo()
|
|
||||||
|
|
||||||
|
# Get detailed container info
|
||||||
|
container_info = container.attrs
|
||||||
|
|
||||||
data_ret = {'status': 1, 'error_message': 'None', 'data':retdata}
|
# Calculate uptime
|
||||||
|
started_at = container_info.get('State', {}).get('StartedAt', '')
|
||||||
|
if started_at:
|
||||||
|
started_time = datetime.strptime(started_at.split('.')[0], '%Y-%m-%dT%H:%M:%S')
|
||||||
|
uptime = datetime.now() - started_time
|
||||||
|
uptime_str = str(uptime).split('.')[0] # Format as HH:MM:SS
|
||||||
|
else:
|
||||||
|
uptime_str = "N/A"
|
||||||
|
|
||||||
|
# Get container details
|
||||||
|
details = {
|
||||||
|
'id': container.short_id,
|
||||||
|
'name': container.name,
|
||||||
|
'status': container.status,
|
||||||
|
'created': container_info.get('Created', ''),
|
||||||
|
'started_at': started_at,
|
||||||
|
'uptime': uptime_str,
|
||||||
|
'image': container_info.get('Config', {}).get('Image', ''),
|
||||||
|
'ports': container_info.get('NetworkSettings', {}).get('Ports', {}),
|
||||||
|
'volumes': container_info.get('Mounts', []),
|
||||||
|
'environment': self._mask_sensitive_env(container_info.get('Config', {}).get('Env', [])),
|
||||||
|
'memory_usage': container.stats(stream=False)['memory_stats'].get('usage', 0),
|
||||||
|
'cpu_usage': container.stats(stream=False)['cpu_stats']['cpu_usage'].get('total_usage', 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
data_ret = {'status': 1, 'error_message': 'None', 'data': [1, details]}
|
||||||
json_data = json.dumps(data_ret)
|
json_data = json.dumps(data_ret)
|
||||||
return HttpResponse(json_data)
|
return HttpResponse(json_data)
|
||||||
|
|
||||||
except BaseException as msg:
|
except BaseException as msg:
|
||||||
data_ret = {'removeImageStatus': 0, 'error_message': str(msg)}
|
data_ret = {'status': 0, 'error_message': str(msg)}
|
||||||
json_data = json.dumps(data_ret)
|
json_data = json.dumps(data_ret)
|
||||||
return HttpResponse(json_data)
|
return HttpResponse(json_data)
|
||||||
|
|
||||||
|
def _mask_sensitive_env(self, env_vars):
|
||||||
|
"""Helper method to mask sensitive data in environment variables"""
|
||||||
|
masked_vars = []
|
||||||
|
sensitive_keywords = ['password', 'secret', 'key', 'token', 'auth']
|
||||||
|
|
||||||
|
for var in env_vars:
|
||||||
|
if '=' in var:
|
||||||
|
name, value = var.split('=', 1)
|
||||||
|
# Check if this is a sensitive variable
|
||||||
|
if any(keyword in name.lower() for keyword in sensitive_keywords):
|
||||||
|
masked_vars.append(f"{name}=********")
|
||||||
|
else:
|
||||||
|
masked_vars.append(var)
|
||||||
|
else:
|
||||||
|
masked_vars.append(var)
|
||||||
|
|
||||||
|
return masked_vars
|
||||||
|
|
||||||
def getContainerApplog(self, userID=None, data=None):
|
def getContainerApplog(self, userID=None, data=None):
|
||||||
try:
|
try:
|
||||||
admin = Administrator.objects.get(pk=userID)
|
admin = Administrator.objects.get(pk=userID)
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
from django.urls import path, re_path
|
from django.urls import path, re_path
|
||||||
|
|
||||||
from . import views
|
from . import views
|
||||||
from websiteFunctions.views import Dockersitehome
|
from websiteFunctions.views import Dockersitehome, startContainer, stopContainer, restartContainer
|
||||||
|
|
||||||
urlpatterns = [
|
urlpatterns = [
|
||||||
re_path(r'^$', views.loadDockerHome, name='dockerHome'),
|
re_path(r'^$', views.loadDockerHome, name='dockerHome'),
|
||||||
@@ -27,7 +27,7 @@ urlpatterns = [
|
|||||||
re_path(r'^recreateContainer$', views.recreateContainer, name='recreateContainer'),
|
re_path(r'^recreateContainer$', views.recreateContainer, name='recreateContainer'),
|
||||||
re_path(r'^installDocker$', views.installDocker, name='installDocker'),
|
re_path(r'^installDocker$', views.installDocker, name='installDocker'),
|
||||||
re_path(r'^images$', views.images, name='containerImage'),
|
re_path(r'^images$', views.images, name='containerImage'),
|
||||||
re_path(r'^view/(?P<name>.+)$', views.viewContainer, name='viewContainer'),
|
re_path(r'^view/(?P<n>.+)$', views.viewContainer, name='viewContainer'),
|
||||||
|
|
||||||
path('manage/<int:dockerapp>/app', Dockersitehome, name='Dockersitehome'),
|
path('manage/<int:dockerapp>/app', Dockersitehome, name='Dockersitehome'),
|
||||||
path('getDockersiteList', views.getDockersiteList, name='getDockersiteList'),
|
path('getDockersiteList', views.getDockersiteList, name='getDockersiteList'),
|
||||||
@@ -36,4 +36,9 @@ urlpatterns = [
|
|||||||
path('recreateappcontainer', views.recreateappcontainer, name='recreateappcontainer'),
|
path('recreateappcontainer', views.recreateappcontainer, name='recreateappcontainer'),
|
||||||
path('RestartContainerAPP', views.RestartContainerAPP, name='RestartContainerAPP'),
|
path('RestartContainerAPP', views.RestartContainerAPP, name='RestartContainerAPP'),
|
||||||
path('StopContainerAPP', views.StopContainerAPP, name='StopContainerAPP'),
|
path('StopContainerAPP', views.StopContainerAPP, name='StopContainerAPP'),
|
||||||
|
|
||||||
|
# Docker Container Actions
|
||||||
|
path('startContainer', startContainer, name='startContainer'),
|
||||||
|
path('stopContainer', stopContainer, name='stopContainer'),
|
||||||
|
path('restartContainer', restartContainer, name='restartContainer'),
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -14,8 +14,8 @@ from os.path import *
|
|||||||
from stat import *
|
from stat import *
|
||||||
import stat
|
import stat
|
||||||
|
|
||||||
VERSION = '2.3'
|
VERSION = '2.4'
|
||||||
BUILD = 9
|
BUILD = 0
|
||||||
|
|
||||||
char_set = {'small': 'abcdefghijklmnopqrstuvwxyz', 'nums': '0123456789', 'big': 'ABCDEFGHIJKLMNOPQRSTUVWXYZ'}
|
char_set = {'small': 'abcdefghijklmnopqrstuvwxyz', 'nums': '0123456789', 'big': 'ABCDEFGHIJKLMNOPQRSTUVWXYZ'}
|
||||||
|
|
||||||
|
|||||||
@@ -16,8 +16,8 @@ from django.http import HttpResponse
|
|||||||
from django.utils import translation
|
from django.utils import translation
|
||||||
# Create your views here.
|
# Create your views here.
|
||||||
|
|
||||||
VERSION = '2.3'
|
VERSION = '2.4'
|
||||||
BUILD = 9
|
BUILD = 0
|
||||||
|
|
||||||
|
|
||||||
def verifyLogin(request):
|
def verifyLogin(request):
|
||||||
|
|||||||
@@ -4,6 +4,9 @@ import os
|
|||||||
import sys
|
import sys
|
||||||
import time
|
import time
|
||||||
from random import randint
|
from random import randint
|
||||||
|
import socket
|
||||||
|
import shutil
|
||||||
|
import docker
|
||||||
|
|
||||||
sys.path.append('/usr/local/CyberCP')
|
sys.path.append('/usr/local/CyberCP')
|
||||||
|
|
||||||
@@ -24,11 +27,25 @@ from plogical.CyberCPLogFileWriter import CyberCPLogFileWriter as logging
|
|||||||
import argparse
|
import argparse
|
||||||
import threading as multi
|
import threading as multi
|
||||||
|
|
||||||
|
class DockerDeploymentError(Exception):
|
||||||
|
def __init__(self, message, error_code=None, recovery_possible=True):
|
||||||
|
self.message = message
|
||||||
|
self.error_code = error_code
|
||||||
|
self.recovery_possible = recovery_possible
|
||||||
|
super().__init__(self.message)
|
||||||
|
|
||||||
class Docker_Sites(multi.Thread):
|
class Docker_Sites(multi.Thread):
|
||||||
Wordpress = 1
|
Wordpress = 1
|
||||||
Joomla = 2
|
Joomla = 2
|
||||||
|
|
||||||
|
# Error codes
|
||||||
|
ERROR_DOCKER_NOT_INSTALLED = 'DOCKER_NOT_INSTALLED'
|
||||||
|
ERROR_PORT_IN_USE = 'PORT_IN_USE'
|
||||||
|
ERROR_CONTAINER_FAILED = 'CONTAINER_FAILED'
|
||||||
|
ERROR_NETWORK_FAILED = 'NETWORK_FAILED'
|
||||||
|
ERROR_VOLUME_FAILED = 'VOLUME_FAILED'
|
||||||
|
ERROR_DB_FAILED = 'DB_FAILED'
|
||||||
|
|
||||||
def __init__(self, function_run, data):
|
def __init__(self, function_run, data):
|
||||||
multi.Thread.__init__(self)
|
multi.Thread.__init__(self)
|
||||||
self.function_run = function_run
|
self.function_run = function_run
|
||||||
@@ -165,15 +182,54 @@ class Docker_Sites(multi.Thread):
|
|||||||
return 0, ReturnCode
|
return 0, ReturnCode
|
||||||
|
|
||||||
else:
|
else:
|
||||||
command = 'apt install docker-compose -y'
|
# Add Docker's official GPG key
|
||||||
|
command = 'curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo gpg --dearmor -o /usr/share/keyrings/docker-archive-keyring.gpg'
|
||||||
ReturnCode = ProcessUtilities.executioner(command)
|
ReturnCode = ProcessUtilities.executioner(command, 'root', True)
|
||||||
|
if not ReturnCode:
|
||||||
if ReturnCode:
|
|
||||||
return 1, None
|
|
||||||
else:
|
|
||||||
return 0, ReturnCode
|
return 0, ReturnCode
|
||||||
|
|
||||||
|
# Add Docker repository
|
||||||
|
command = 'echo "deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/docker-archive-keyring.gpg] https://download.docker.com/linux/ubuntu $(lsb_release -cs) stable" | sudo tee /etc/apt/sources.list.d/docker.list > /dev/null'
|
||||||
|
ReturnCode = ProcessUtilities.executioner(command, 'root', True)
|
||||||
|
if not ReturnCode:
|
||||||
|
return 0, ReturnCode
|
||||||
|
|
||||||
|
# Update package index
|
||||||
|
command = 'apt-get update'
|
||||||
|
ReturnCode = ProcessUtilities.executioner(command)
|
||||||
|
if not ReturnCode:
|
||||||
|
return 0, ReturnCode
|
||||||
|
|
||||||
|
# Install Docker packages
|
||||||
|
command = 'apt-get install -y docker-ce docker-ce-cli containerd.io docker-compose-plugin'
|
||||||
|
ReturnCode = ProcessUtilities.executioner(command)
|
||||||
|
if not ReturnCode:
|
||||||
|
return 0, ReturnCode
|
||||||
|
|
||||||
|
# Enable and start Docker service
|
||||||
|
command = 'systemctl enable docker'
|
||||||
|
ReturnCode = ProcessUtilities.executioner(command)
|
||||||
|
if not ReturnCode:
|
||||||
|
return 0, ReturnCode
|
||||||
|
|
||||||
|
command = 'systemctl start docker'
|
||||||
|
ReturnCode = ProcessUtilities.executioner(command)
|
||||||
|
if not ReturnCode:
|
||||||
|
return 0, ReturnCode
|
||||||
|
|
||||||
|
# Install Docker Compose
|
||||||
|
command = 'curl -L "https://github.com/docker/compose/releases/download/v2.23.2/docker-compose-linux-$(uname -m)" -o /usr/local/bin/docker-compose'
|
||||||
|
ReturnCode = ProcessUtilities.executioner(command, 'root', True)
|
||||||
|
if not ReturnCode:
|
||||||
|
return 0, ReturnCode
|
||||||
|
|
||||||
|
command = 'chmod +x /usr/local/bin/docker-compose'
|
||||||
|
ReturnCode = ProcessUtilities.executioner(command, 'root', True)
|
||||||
|
if not ReturnCode:
|
||||||
|
return 0, ReturnCode
|
||||||
|
|
||||||
|
return 1, None
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def SetupProxy(port):
|
def SetupProxy(port):
|
||||||
import xml.etree.ElementTree as ET
|
import xml.etree.ElementTree as ET
|
||||||
@@ -614,8 +670,6 @@ services:
|
|||||||
|
|
||||||
### forcefully delete containers
|
### forcefully delete containers
|
||||||
|
|
||||||
import docker
|
|
||||||
|
|
||||||
# Create a Docker client
|
# Create a Docker client
|
||||||
client = docker.from_env()
|
client = docker.from_env()
|
||||||
|
|
||||||
@@ -651,43 +705,73 @@ services:
|
|||||||
## This function need site name which was passed while creating the app
|
## This function need site name which was passed while creating the app
|
||||||
def ListContainers(self):
|
def ListContainers(self):
|
||||||
try:
|
try:
|
||||||
|
|
||||||
import docker
|
|
||||||
|
|
||||||
# Create a Docker client
|
# Create a Docker client
|
||||||
client = docker.from_env()
|
client = docker.from_env()
|
||||||
|
|
||||||
FilerValue = self.DockerAppName
|
# Debug logging
|
||||||
|
if os.path.exists(ProcessUtilities.debugPath):
|
||||||
|
logging.writeToFile(f'DockerAppName: {self.DockerAppName}')
|
||||||
|
|
||||||
# Define the label to filter containers
|
# List all containers without filtering first
|
||||||
label_filter = {'name': FilerValue}
|
all_containers = client.containers.list(all=True)
|
||||||
|
|
||||||
|
if os.path.exists(ProcessUtilities.debugPath):
|
||||||
|
logging.writeToFile(f'Total containers found: {len(all_containers)}')
|
||||||
|
for container in all_containers:
|
||||||
|
logging.writeToFile(f'Container name: {container.name}')
|
||||||
|
|
||||||
|
# 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}')
|
||||||
|
|
||||||
# List containers matching the label filter
|
for container in all_containers:
|
||||||
containers = client.containers.list(filters=label_filter)
|
if os.path.exists(ProcessUtilities.debugPath):
|
||||||
|
logging.writeToFile(f'Checking container: {container.name} against filter: {search_name}')
|
||||||
|
if search_name.lower() in container.name.lower():
|
||||||
|
containers.append(container)
|
||||||
|
|
||||||
|
if os.path.exists(ProcessUtilities.debugPath):
|
||||||
|
logging.writeToFile(f'Filtered containers count: {len(containers)}')
|
||||||
|
|
||||||
json_data = "["
|
json_data = "["
|
||||||
checker = 0
|
checker = 0
|
||||||
|
|
||||||
for container in containers:
|
for container in containers:
|
||||||
|
try:
|
||||||
|
dic = {
|
||||||
|
'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 {}
|
||||||
|
}
|
||||||
|
|
||||||
dic = {
|
if checker == 0:
|
||||||
'id': container.short_id,
|
json_data = json_data + json.dumps(dic)
|
||||||
'name': container.name,
|
checker = 1
|
||||||
'status': container.status,
|
else:
|
||||||
'volumes': container.attrs['HostConfig']['Binds'] if 'HostConfig' in container.attrs else [],
|
json_data = json_data + ',' + json.dumps(dic)
|
||||||
'logs_50': container.logs(tail=50).decode('utf-8'),
|
except Exception as e:
|
||||||
'ports': container.attrs['HostConfig']['PortBindings'] if 'HostConfig' in container.attrs else {}
|
logging.writeToFile(f"Error processing container {container.name}: {str(e)}")
|
||||||
}
|
continue
|
||||||
|
|
||||||
if checker == 0:
|
|
||||||
json_data = json_data + json.dumps(dic)
|
|
||||||
checker = 1
|
|
||||||
else:
|
|
||||||
json_data = json_data + ',' + json.dumps(dic)
|
|
||||||
|
|
||||||
json_data = json_data + ']'
|
json_data = json_data + ']'
|
||||||
|
|
||||||
|
if os.path.exists(ProcessUtilities.debugPath):
|
||||||
|
logging.writeToFile(f'Final JSON data: {json_data}')
|
||||||
|
|
||||||
return 1, json_data
|
return 1, json_data
|
||||||
|
|
||||||
except BaseException as msg:
|
except BaseException as msg:
|
||||||
@@ -697,7 +781,6 @@ services:
|
|||||||
### pass container id and number of lines to fetch from logs
|
### pass container id and number of lines to fetch from logs
|
||||||
def ContainerLogs(self):
|
def ContainerLogs(self):
|
||||||
try:
|
try:
|
||||||
import docker
|
|
||||||
# Create a Docker client
|
# Create a Docker client
|
||||||
client = docker.from_env()
|
client = docker.from_env()
|
||||||
|
|
||||||
@@ -716,7 +799,6 @@ services:
|
|||||||
|
|
||||||
def ContainerInfo(self):
|
def ContainerInfo(self):
|
||||||
try:
|
try:
|
||||||
import docker
|
|
||||||
# Create a Docker client
|
# Create a Docker client
|
||||||
client = docker.from_env()
|
client = docker.from_env()
|
||||||
|
|
||||||
@@ -748,7 +830,6 @@ services:
|
|||||||
|
|
||||||
def RestartContainer(self):
|
def RestartContainer(self):
|
||||||
try:
|
try:
|
||||||
import docker
|
|
||||||
# Create a Docker client
|
# Create a Docker client
|
||||||
client = docker.from_env()
|
client = docker.from_env()
|
||||||
|
|
||||||
@@ -764,7 +845,6 @@ services:
|
|||||||
|
|
||||||
def StopContainer(self):
|
def StopContainer(self):
|
||||||
try:
|
try:
|
||||||
import docker
|
|
||||||
# Create a Docker client
|
# Create a Docker client
|
||||||
client = docker.from_env()
|
client = docker.from_env()
|
||||||
|
|
||||||
@@ -780,172 +860,550 @@ services:
|
|||||||
|
|
||||||
##### N8N Container
|
##### N8N Container
|
||||||
|
|
||||||
def DeployN8NContainer(self):
|
def check_container_health(self, container_name, max_retries=3, delay=80):
|
||||||
|
"""
|
||||||
|
Check if a container is running, accepting healthy, unhealthy, and starting states
|
||||||
|
Total wait time will be 4 minutes (3 retries * 80 seconds)
|
||||||
|
"""
|
||||||
try:
|
try:
|
||||||
|
# Format container name to match Docker's naming convention
|
||||||
|
formatted_name = f"{self.data['ServiceName']}-{container_name}-1"
|
||||||
|
logging.writeToFile(f'Checking container health for: {formatted_name}')
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
logging.statusWriter(self.JobID, 'Checking if Docker is installed..,0')
|
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())
|
||||||
|
|
||||||
command = 'docker --help'
|
if available_gb < 5: # Require minimum 5GB free space
|
||||||
result = ProcessUtilities.outputExecutioner(command)
|
raise DockerDeploymentError(
|
||||||
|
f"Insufficient disk space. Need at least 5GB but only {available_gb}GB available.",
|
||||||
|
self.ERROR_VOLUME_FAILED
|
||||||
|
)
|
||||||
|
|
||||||
if os.path.exists(ProcessUtilities.debugPath):
|
# Check if Docker is running and accessible
|
||||||
logging.writeToFile(f'return code of docker install {result}')
|
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")
|
||||||
|
|
||||||
if result.find("not found") > -1:
|
# Check Docker system info for resource limits
|
||||||
if os.path.exists(ProcessUtilities.debugPath):
|
command = "docker info --format '{{.MemTotal}}'"
|
||||||
logging.writeToFile(f'About to run docker install function...')
|
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'
|
||||||
|
)
|
||||||
|
|
||||||
execPath = "/usr/local/CyberCP/bin/python /usr/local/CyberCP/dockerManager/dockerInstall.py"
|
# 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 result.find("not found") > -1:
|
||||||
|
if os.path.exists(ProcessUtilities.debugPath):
|
||||||
|
logging.writeToFile(f'About to run docker install function...')
|
||||||
|
|
||||||
|
# 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
|
||||||
|
|
||||||
|
logging.statusWriter(self.JobID, 'Docker installation verified...,20')
|
||||||
|
|
||||||
|
# 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:
|
||||||
|
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:
|
||||||
|
dockerCommand = 'docker-compose'
|
||||||
|
|
||||||
|
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:
|
||||||
|
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')
|
||||||
|
|
||||||
|
# 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)
|
ProcessUtilities.executioner(execPath)
|
||||||
|
logging.statusWriter(self.JobID, 'Proxy configured...,80')
|
||||||
|
|
||||||
logging.statusWriter(self.JobID, 'Docker is ready to use..,10')
|
# 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')
|
||||||
|
|
||||||
self.data['ServiceName'] = self.data["SiteName"].replace(' ', '-')
|
# Restart web server
|
||||||
|
from plogical.installUtilities import installUtilities
|
||||||
|
installUtilities.reStartLiteSpeedSocket()
|
||||||
|
|
||||||
WPSite = f'''
|
# Monitor deployment
|
||||||
version: '3.8'
|
metrics = self.monitor_deployment()
|
||||||
|
self.log_deployment_metrics(metrics)
|
||||||
|
|
||||||
|
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:
|
volumes:
|
||||||
db_storage:
|
db_storage:
|
||||||
|
driver: local
|
||||||
n8n_storage:
|
n8n_storage:
|
||||||
|
driver: local
|
||||||
|
|
||||||
services:
|
services:
|
||||||
'{self.data['ServiceName']}-db':
|
'{self.data['ServiceName']}-db':
|
||||||
image: docker.io/bitnami/postgresql:16
|
image: {postgres_config['image']}
|
||||||
user: root
|
user: {postgres_config['user']}
|
||||||
restart: always
|
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:
|
environment:
|
||||||
# - POSTGRES_USER:root
|
- POSTGRES_USER={postgres_config['environment']['POSTGRES_USER']}
|
||||||
- POSTGRESQL_USERNAME={self.data['MySQLDBNUser']}
|
- POSTGRES_PASSWORD={postgres_config['environment']['POSTGRES_PASSWORD']}
|
||||||
- POSTGRESQL_DATABASE={self.data['MySQLDBName']}
|
- POSTGRES_DB={postgres_config['environment']['POSTGRES_DB']}
|
||||||
- POSTGRESQL_POSTGRES_PASSWORD={self.data['MySQLPassword']}
|
|
||||||
- POSTGRESQL_PASSWORD={self.data['MySQLPassword']}
|
|
||||||
volumes:
|
volumes:
|
||||||
# - "/home/docker/{self.data['finalURL']}/db:/var/lib/postgresql/data"
|
- "/home/docker/{self.data['finalURL']}/db:/var/lib/postgresql/data"
|
||||||
- "/home/docker/{self.data['finalURL']}/db:/bitnami/postgresql"
|
networks:
|
||||||
|
- n8n-network
|
||||||
|
deploy:
|
||||||
|
resources:
|
||||||
|
limits:
|
||||||
|
cpus: '{self.data["CPUsMySQL"]}'
|
||||||
|
memory: {self.data["MemoryMySQL"]}M
|
||||||
|
|
||||||
'{self.data['ServiceName']}':
|
'{self.data['ServiceName']}':
|
||||||
image: docker.n8n.io/n8nio/n8n
|
image: {n8n_config['image']}
|
||||||
user: root
|
user: {n8n_config['user']}
|
||||||
restart: always
|
restart: always
|
||||||
|
healthcheck:
|
||||||
|
test: {n8n_config['healthcheck']['test']}
|
||||||
|
interval: {n8n_config['healthcheck']['interval']}
|
||||||
|
timeout: {n8n_config['healthcheck']['timeout']}
|
||||||
|
retries: {n8n_config['healthcheck']['retries']}
|
||||||
environment:
|
environment:
|
||||||
- DB_TYPE=postgresdb
|
- DB_TYPE={n8n_config['environment']['DB_TYPE']}
|
||||||
- DB_POSTGRESDB_HOST={self.data['ServiceName']}-db
|
- DB_POSTGRESDB_HOST={n8n_config['environment']['DB_POSTGRESDB_HOST']}
|
||||||
- DB_POSTGRESDB_PORT=5432
|
- DB_POSTGRESDB_PORT={n8n_config['environment']['DB_POSTGRESDB_PORT']}
|
||||||
- DB_POSTGRESDB_DATABASE={self.data['MySQLDBName']}
|
- DB_POSTGRESDB_DATABASE={n8n_config['environment']['DB_POSTGRESDB_DATABASE']}
|
||||||
- DB_POSTGRESDB_USER={self.data['MySQLDBNUser']}
|
- DB_POSTGRESDB_USER={n8n_config['environment']['DB_POSTGRESDB_USER']}
|
||||||
- DB_POSTGRESDB_PASSWORD={self.data['MySQLPassword']}
|
- DB_POSTGRESDB_PASSWORD={n8n_config['environment']['DB_POSTGRESDB_PASSWORD']}
|
||||||
- N8N_HOST={self.data['finalURL']}
|
- DB_POSTGRESDB_SCHEMA={n8n_config['environment']['DB_POSTGRESDB_SCHEMA']}
|
||||||
- NODE_ENV=production
|
- N8N_HOST={n8n_config['environment']['N8N_HOST']}
|
||||||
- WEBHOOK_URL=https://{self.data['finalURL']}
|
- NODE_ENV={n8n_config['environment']['NODE_ENV']}
|
||||||
- N8N_PUSH_BACKEND=sse # Use Server-Sent Events instead of WebSockets
|
- 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:
|
ports:
|
||||||
- "{self.data['port']}:5678"
|
- "{self.data['port']}:5678"
|
||||||
links:
|
depends_on:
|
||||||
- {self.data['ServiceName']}-db
|
- {self.data['ServiceName']}-db
|
||||||
volumes:
|
volumes:
|
||||||
- "/home/docker/{self.data['finalURL']}/data:/home/node/.n8n"
|
- "/home/docker/{self.data['finalURL']}/data:/home/node/.n8n"
|
||||||
depends_on:
|
networks:
|
||||||
- '{self.data['ServiceName']}-db'
|
- n8n-network
|
||||||
'''
|
deploy:
|
||||||
|
resources:
|
||||||
### WriteConfig to compose-file
|
limits:
|
||||||
|
cpus: '{self.data["CPUsSite"]}'
|
||||||
command = f"mkdir -p /home/docker/{self.data['finalURL']}"
|
memory: {self.data["MemorySite"]}M
|
||||||
result, message = ProcessUtilities.outputExecutioner(command, None, None, None, 1)
|
|
||||||
|
|
||||||
if result == 0:
|
|
||||||
logging.statusWriter(self.JobID, f'Error {str(message)} . [404]')
|
|
||||||
return 0
|
|
||||||
|
|
||||||
TempCompose = f'/home/cyberpanel/{self.data["finalURL"]}-docker-compose.yml'
|
|
||||||
|
|
||||||
WriteToFile = open(TempCompose, 'w')
|
|
||||||
WriteToFile.write(WPSite)
|
|
||||||
WriteToFile.close()
|
|
||||||
|
|
||||||
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
|
|
||||||
|
|
||||||
command = f"chmod 600 {self.data['ComposePath']} && chown root:root {self.data['ComposePath']}"
|
|
||||||
ProcessUtilities.executioner(command, 'root', True)
|
|
||||||
|
|
||||||
####
|
|
||||||
|
|
||||||
if ProcessUtilities.decideDistro() == ProcessUtilities.cent8 or ProcessUtilities.decideDistro() == ProcessUtilities.centos:
|
|
||||||
dockerCommand = 'docker compose'
|
|
||||||
else:
|
|
||||||
dockerCommand = 'docker-compose'
|
|
||||||
|
|
||||||
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')
|
|
||||||
|
|
||||||
time.sleep(25)
|
|
||||||
|
|
||||||
|
|
||||||
### checking if everything ran properly
|
|
||||||
|
|
||||||
passdata = {}
|
|
||||||
passdata["JobID"] = None
|
|
||||||
passdata['name'] = self.data['ServiceName']
|
|
||||||
da = Docker_Sites(None, passdata)
|
|
||||||
retdata, containers = da.ListContainers()
|
|
||||||
|
|
||||||
containers = json.loads(containers)
|
|
||||||
|
|
||||||
if os.path.exists(ProcessUtilities.debugPath):
|
|
||||||
logging.writeToFile(str(containers))
|
|
||||||
|
|
||||||
### it means less then two containers which means something went wrong
|
|
||||||
if len(containers) < 2:
|
|
||||||
logging.writeToFile(f'Unkonwn error, containers not running. [DeployN8NContainer] . [404]')
|
|
||||||
logging.statusWriter(self.JobID, f'Unkonwn error, containers not running. [DeployN8NContainer] . [404]')
|
|
||||||
return 0
|
|
||||||
|
|
||||||
### Set up Proxy
|
|
||||||
|
|
||||||
execPath = "/usr/local/CyberCP/bin/python /usr/local/CyberCP/plogical/DockerSites.py"
|
|
||||||
execPath = execPath + f" SetupProxy --port {self.data['port']}"
|
|
||||||
ProcessUtilities.executioner(execPath)
|
|
||||||
|
|
||||||
### Set up 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'])
|
|
||||||
|
|
||||||
# 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
|
|
||||||
|
|
||||||
from plogical.installUtilities import installUtilities
|
|
||||||
installUtilities.reStartLiteSpeedSocket()
|
|
||||||
|
|
||||||
logging.statusWriter(self.JobID, 'Completed. [200]')
|
|
||||||
|
|
||||||
except BaseException as msg:
|
|
||||||
logging.writeToFile(f'{str(msg)}. [DeployN8NContainer]')
|
|
||||||
logging.statusWriter(self.JobID, f'Error {str(msg)} . [404]')
|
|
||||||
print(str(msg))
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
networks:
|
||||||
|
n8n-network:
|
||||||
|
driver: bridge
|
||||||
|
name: {self.data['ServiceName']}_network'''
|
||||||
|
|
||||||
def Main():
|
def Main():
|
||||||
try:
|
try:
|
||||||
|
|||||||
@@ -12,8 +12,8 @@ from plogical.acl import ACLManager
|
|||||||
from packages.models import Package
|
from packages.models import Package
|
||||||
from baseTemplate.models import version
|
from baseTemplate.models import version
|
||||||
|
|
||||||
VERSION = '2.3'
|
VERSION = '2.4'
|
||||||
BUILD = 9
|
BUILD = 0
|
||||||
|
|
||||||
if not os.geteuid() == 0:
|
if not os.geteuid() == 0:
|
||||||
sys.exit("\nOnly root can run this script\n")
|
sys.exit("\nOnly root can run this script\n")
|
||||||
|
|||||||
@@ -53,8 +53,8 @@ try:
|
|||||||
except:
|
except:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
VERSION = '2.3'
|
VERSION = '2.4'
|
||||||
BUILD = 9
|
BUILD = 0
|
||||||
|
|
||||||
|
|
||||||
## I am not the monster that you think I am..
|
## I am not the monster that you think I am..
|
||||||
@@ -739,7 +739,7 @@ class backupUtilities:
|
|||||||
|
|
||||||
dbName = database.find('dbName').text
|
dbName = database.find('dbName').text
|
||||||
|
|
||||||
if (VERSION == '2.1' or VERSION == '2.3') and int(BUILD) >= 1:
|
if ((VERSION == '2.1' or VERSION == '2.3') and int(BUILD) >= 1) or (VERSION == '2.4' and int(BUILD) >= 0):
|
||||||
|
|
||||||
logging.CyberCPLogFileWriter.writeToFile('Backup version 2.1.1+ detected..')
|
logging.CyberCPLogFileWriter.writeToFile('Backup version 2.1.1+ detected..')
|
||||||
databaseUsers = database.findall('databaseUsers')
|
databaseUsers = database.findall('databaseUsers')
|
||||||
@@ -1073,7 +1073,7 @@ class backupUtilities:
|
|||||||
|
|
||||||
dbName = database.find('dbName').text
|
dbName = database.find('dbName').text
|
||||||
|
|
||||||
if (VERSION == '2.1' or VERSION == '2.3') and int(BUILD) >= 1:
|
if ((VERSION == '2.1' or VERSION == '2.3') and int(BUILD) >= 1) or (VERSION == '2.4' and int(BUILD) >= 0):
|
||||||
|
|
||||||
logging.CyberCPLogFileWriter.writeToFile('Backup version 2.1.1+ detected..')
|
logging.CyberCPLogFileWriter.writeToFile('Backup version 2.1.1+ detected..')
|
||||||
|
|
||||||
|
|||||||
@@ -17,8 +17,8 @@ from CyberCP import settings
|
|||||||
import random
|
import random
|
||||||
import string
|
import string
|
||||||
|
|
||||||
VERSION = '2.3'
|
VERSION = '2.4'
|
||||||
BUILD = 9
|
BUILD = 0
|
||||||
|
|
||||||
CENTOS7 = 0
|
CENTOS7 = 0
|
||||||
CENTOS8 = 1
|
CENTOS8 = 1
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ from managePHP.phpManager import PHPManager
|
|||||||
from plogical.vhostConfs import vhostConfs
|
from plogical.vhostConfs import vhostConfs
|
||||||
from ApachController.ApacheVhosts import ApacheVhost
|
from ApachController.ApacheVhosts import ApacheVhost
|
||||||
try:
|
try:
|
||||||
from websiteFunctions.models import Websites, ChildDomains, aliasDomains
|
from websiteFunctions.models import Websites, ChildDomains, aliasDomains, DockerSites
|
||||||
from databases.models import Databases
|
from databases.models import Databases
|
||||||
except:
|
except:
|
||||||
pass
|
pass
|
||||||
@@ -404,6 +404,23 @@ class vhost:
|
|||||||
|
|
||||||
if ACLManager.FindIfChild() == 0:
|
if ACLManager.FindIfChild() == 0:
|
||||||
|
|
||||||
|
### Delete Docker Sites first before website deletion
|
||||||
|
|
||||||
|
if os.path.exists('/home/docker/%s' % (virtualHostName)):
|
||||||
|
try:
|
||||||
|
dockerSite = DockerSites.objects.get(admin__domain=virtualHostName)
|
||||||
|
passdata = {
|
||||||
|
"domain": virtualHostName,
|
||||||
|
"name": dockerSite.SiteName
|
||||||
|
}
|
||||||
|
from plogical.DockerSites import Docker_Sites
|
||||||
|
da = Docker_Sites(None, passdata)
|
||||||
|
da.DeleteDockerApp()
|
||||||
|
dockerSite.delete()
|
||||||
|
except:
|
||||||
|
# If anything fails in Docker cleanup, at least remove the directory
|
||||||
|
shutil.rmtree('/home/docker/%s' % (virtualHostName))
|
||||||
|
|
||||||
for items in databases:
|
for items in databases:
|
||||||
mysqlUtilities.deleteDatabase(items.dbName, items.dbUser)
|
mysqlUtilities.deleteDatabase(items.dbName, items.dbUser)
|
||||||
|
|
||||||
|
|||||||
@@ -25,8 +25,8 @@ EXPIRE = 3
|
|||||||
|
|
||||||
### Version
|
### Version
|
||||||
|
|
||||||
VERSION = '2.3'
|
VERSION = '2.4'
|
||||||
BUILD = 9
|
BUILD = 0
|
||||||
|
|
||||||
|
|
||||||
def serverStatusHome(request):
|
def serverStatusHome(request):
|
||||||
|
|||||||
@@ -237,7 +237,7 @@ app.controller('createWebsite', function ($scope, $http, $timeout, $window) {
|
|||||||
$("#listFail").hide();
|
$("#listFail").hide();
|
||||||
|
|
||||||
|
|
||||||
app.controller('listWebsites', function ($scope, $http) {
|
app.controller('listWebsites', function ($scope, $http, $window) {
|
||||||
|
|
||||||
|
|
||||||
$scope.currentPage = 1;
|
$scope.currentPage = 1;
|
||||||
@@ -384,6 +384,244 @@ app.controller('listWebsites', function ($scope, $http) {
|
|||||||
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
$scope.getFullUrl = function(url) {
|
||||||
|
console.log('getFullUrl called with:', url);
|
||||||
|
if (!url) {
|
||||||
|
// If no URL is provided, try to use the domain
|
||||||
|
if (this.wp && this.wp.domain) {
|
||||||
|
url = this.wp.domain;
|
||||||
|
} else {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (url.startsWith('http://') || url.startsWith('https://')) {
|
||||||
|
return url;
|
||||||
|
}
|
||||||
|
return 'https://' + url;
|
||||||
|
};
|
||||||
|
|
||||||
|
$scope.showWPSites = function(domain) {
|
||||||
|
var site = $scope.WebSitesList.find(function(site) {
|
||||||
|
return site.domain === domain;
|
||||||
|
});
|
||||||
|
if (site) {
|
||||||
|
site.showWPSites = !site.showWPSites;
|
||||||
|
if (site.showWPSites && (!site.wp_sites || !site.wp_sites.length)) {
|
||||||
|
// Fetch WordPress sites if not already loaded
|
||||||
|
var config = {
|
||||||
|
headers: {
|
||||||
|
'X-CSRFToken': getCookie('csrftoken')
|
||||||
|
}
|
||||||
|
};
|
||||||
|
var data = { domain: domain };
|
||||||
|
site.loadingWPSites = true;
|
||||||
|
$http.post('/websites/getWordPressSites', data, config).then(
|
||||||
|
function(response) {
|
||||||
|
site.loadingWPSites = false;
|
||||||
|
if (response.data.status === 1) {
|
||||||
|
site.wp_sites = response.data.sites;
|
||||||
|
site.wp_sites.forEach(function(wp) {
|
||||||
|
// Ensure each WP site has a URL
|
||||||
|
if (!wp.url) {
|
||||||
|
wp.url = wp.domain || domain;
|
||||||
|
}
|
||||||
|
fetchWPSiteData(wp);
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
new PNotify({
|
||||||
|
title: 'Error!',
|
||||||
|
text: response.data.error_message || 'Could not fetch WordPress sites',
|
||||||
|
type: 'error'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
function(response) {
|
||||||
|
site.loadingWPSites = false;
|
||||||
|
new PNotify({
|
||||||
|
title: 'Error!',
|
||||||
|
text: 'Could not connect to server',
|
||||||
|
type: 'error'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
function fetchWPSiteData(wp) {
|
||||||
|
var config = {
|
||||||
|
headers: {
|
||||||
|
'X-CSRFToken': getCookie('csrftoken')
|
||||||
|
}
|
||||||
|
};
|
||||||
|
var data = { WPid: wp.id };
|
||||||
|
|
||||||
|
// Fetch site data
|
||||||
|
$http.post('/websites/FetchWPdata', data, config).then(
|
||||||
|
function(response) {
|
||||||
|
if (response.data.status === 1) {
|
||||||
|
var data = response.data.ret_data;
|
||||||
|
wp.version = data.version;
|
||||||
|
wp.phpVersion = data.phpVersion || 'PHP 7.4';
|
||||||
|
wp.searchIndex = data.searchIndex === 1;
|
||||||
|
wp.debugging = data.debugging === 1;
|
||||||
|
wp.passwordProtection = data.passwordprotection === 1;
|
||||||
|
wp.maintenanceMode = data.maintenanceMode === 1;
|
||||||
|
fetchPluginData(wp);
|
||||||
|
fetchThemeData(wp);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function fetchPluginData(wp) {
|
||||||
|
var config = {
|
||||||
|
headers: {
|
||||||
|
'X-CSRFToken': getCookie('csrftoken')
|
||||||
|
}
|
||||||
|
};
|
||||||
|
var data = { WPid: wp.id };
|
||||||
|
$http.post('/websites/GetCurrentPlugins', data, config).then(
|
||||||
|
function(response) {
|
||||||
|
if (response.data.status === 1) {
|
||||||
|
var plugins = JSON.parse(response.data.plugins);
|
||||||
|
wp.activePlugins = plugins.filter(function(p) { return p.status === 'active'; }).length;
|
||||||
|
wp.totalPlugins = plugins.length;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function fetchThemeData(wp) {
|
||||||
|
var config = {
|
||||||
|
headers: {
|
||||||
|
'X-CSRFToken': getCookie('csrftoken')
|
||||||
|
}
|
||||||
|
};
|
||||||
|
var data = { WPid: wp.id };
|
||||||
|
$http.post('/websites/GetCurrentThemes', data, config).then(
|
||||||
|
function(response) {
|
||||||
|
if (response.data.status === 1) {
|
||||||
|
var themes = JSON.parse(response.data.themes);
|
||||||
|
wp.theme = themes.find(function(t) { return t.status === 'active'; }).name;
|
||||||
|
wp.totalThemes = themes.length;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
$scope.updateSetting = function(wp, setting) {
|
||||||
|
var settingMap = {
|
||||||
|
'search-indexing': 'searchIndex',
|
||||||
|
'debugging': 'debugging',
|
||||||
|
'password-protection': 'passwordProtection',
|
||||||
|
'maintenance-mode': 'maintenanceMode'
|
||||||
|
};
|
||||||
|
|
||||||
|
var data = {
|
||||||
|
siteId: wp.id,
|
||||||
|
setting: setting,
|
||||||
|
value: wp[settingMap[setting]] ? 1 : 0
|
||||||
|
};
|
||||||
|
|
||||||
|
var config = {
|
||||||
|
headers: {
|
||||||
|
'X-CSRFToken': getCookie('csrftoken')
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
$http.post('/websites/UpdateWPSettings', data, config).then(
|
||||||
|
function(response) {
|
||||||
|
if (!response.data.status) {
|
||||||
|
wp[settingMap[setting]] = !wp[settingMap[setting]];
|
||||||
|
new PNotify({
|
||||||
|
title: 'Operation Failed!',
|
||||||
|
text: response.data.error_message || 'Unknown error',
|
||||||
|
type: 'error'
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
new PNotify({
|
||||||
|
title: 'Success!',
|
||||||
|
text: 'Setting updated successfully.',
|
||||||
|
type: 'success'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
function(response) {
|
||||||
|
wp[settingMap[setting]] = !wp[settingMap[setting]];
|
||||||
|
new PNotify({
|
||||||
|
title: 'Operation Failed!',
|
||||||
|
text: 'Could not connect to server, please try again.',
|
||||||
|
type: 'error'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
$scope.wpLogin = function(wpId) {
|
||||||
|
window.open('/websites/AutoLogin?id=' + wpId, '_blank');
|
||||||
|
};
|
||||||
|
|
||||||
|
$scope.manageWP = function(wpId) {
|
||||||
|
window.location.href = '/websites/WPHome?ID=' + wpId;
|
||||||
|
};
|
||||||
|
|
||||||
|
$scope.deleteWPSite = function(wp) {
|
||||||
|
if (confirm('Are you sure you want to delete this WordPress site? This action cannot be undone.')) {
|
||||||
|
var config = {
|
||||||
|
headers: {
|
||||||
|
'X-CSRFToken': getCookie('csrftoken')
|
||||||
|
}
|
||||||
|
};
|
||||||
|
var data = {
|
||||||
|
wpid: wp.id,
|
||||||
|
domain: wp.domain
|
||||||
|
};
|
||||||
|
|
||||||
|
$http.post('/websites/deleteWordPressSite', data, config).then(
|
||||||
|
function(response) {
|
||||||
|
if (response.data.status === 1) {
|
||||||
|
// Remove the WP site from the list
|
||||||
|
var site = $scope.WebSitesList.find(function(site) {
|
||||||
|
return site.domain === wp.domain;
|
||||||
|
});
|
||||||
|
if (site && site.wp_sites) {
|
||||||
|
site.wp_sites = site.wp_sites.filter(function(wpSite) {
|
||||||
|
return wpSite.id !== wp.id;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
new PNotify({
|
||||||
|
title: 'Success!',
|
||||||
|
text: 'WordPress site deleted successfully.',
|
||||||
|
type: 'success'
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
new PNotify({
|
||||||
|
title: 'Error!',
|
||||||
|
text: response.data.error_message || 'Could not delete WordPress site',
|
||||||
|
type: 'error'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
function(response) {
|
||||||
|
new PNotify({
|
||||||
|
title: 'Error!',
|
||||||
|
text: 'Could not connect to server',
|
||||||
|
type: 'error'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
$scope.visitSite = function(wp) {
|
||||||
|
var url = wp.url || wp.domain;
|
||||||
|
if (!url) return '';
|
||||||
|
if (url.startsWith('http://') || url.startsWith('https://')) {
|
||||||
|
return url;
|
||||||
|
}
|
||||||
|
return 'https://' + url;
|
||||||
|
};
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -3460,7 +3698,6 @@ app.controller('manageAliasController', function ($scope, $http, $timeout, $wind
|
|||||||
$window.location.reload();
|
$window.location.reload();
|
||||||
}, 3000);
|
}, 3000);
|
||||||
|
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
|
|
||||||
$scope.aliasTable = false;
|
$scope.aliasTable = false;
|
||||||
|
|||||||
170
websiteFunctions/dockerviews.py
Normal file
170
websiteFunctions/dockerviews.py
Normal 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)
|
||||||
|
}))
|
||||||
395
websiteFunctions/static/websiteFunctions/DockerContainers.js
Normal file
395
websiteFunctions/static/websiteFunctions/DockerContainers.js
Normal 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
@@ -241,10 +241,11 @@
|
|||||||
<div class="col-md-3">
|
<div class="col-md-3">
|
||||||
<h6 style="font-weight: bold">Search Engine Indexing</h6>
|
<h6 style="font-weight: bold">Search Engine Indexing</h6>
|
||||||
<div class="custom-control custom-switch">
|
<div class="custom-control custom-switch">
|
||||||
<input ng-click="UpdateWPSettings('searchIndex')"
|
<input type="checkbox"
|
||||||
type="checkbox"
|
class="custom-control-input"
|
||||||
class="custom-control-input ng-pristine ng-untouched ng-valid ng-not-empty"
|
id="searchIndex"
|
||||||
id="searchIndex">
|
ng-click="UpdateWPSettings('searchIndex')"
|
||||||
|
ng-checked="searchIndex == 1">
|
||||||
<label class="custom-control-label"
|
<label class="custom-control-label"
|
||||||
for="searchIndex"></label>
|
for="searchIndex"></label>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -23,6 +23,31 @@
|
|||||||
$scope.wpSitesCount = $scope.debug.wp_sites_count;
|
$scope.wpSitesCount = $scope.debug.wp_sites_count;
|
||||||
$scope.currentPage = 1;
|
$scope.currentPage = 1;
|
||||||
$scope.recordsToShow = 10;
|
$scope.recordsToShow = 10;
|
||||||
|
$scope.expandedSites = {}; // Track which sites are expanded
|
||||||
|
$scope.currentWP = null; // Store current WordPress site for password protection
|
||||||
|
|
||||||
|
// Function to toggle site expansion
|
||||||
|
$scope.toggleSite = function(site) {
|
||||||
|
if (!$scope.expandedSites[site.id]) {
|
||||||
|
$scope.expandedSites[site.id] = true;
|
||||||
|
site.loading = true;
|
||||||
|
site.loadingPlugins = true;
|
||||||
|
site.loadingTheme = true;
|
||||||
|
fetchSiteData(site);
|
||||||
|
} else {
|
||||||
|
$scope.expandedSites[site.id] = false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Function to check if site is expanded
|
||||||
|
$scope.isExpanded = function(siteId) {
|
||||||
|
return $scope.expandedSites[siteId];
|
||||||
|
};
|
||||||
|
|
||||||
|
// Function to check if site data is loaded
|
||||||
|
$scope.isDataLoaded = function(site) {
|
||||||
|
return site.version !== undefined;
|
||||||
|
};
|
||||||
|
|
||||||
$scope.updatePagination = function() {
|
$scope.updatePagination = function() {
|
||||||
var filteredSites = $scope.wpSites;
|
var filteredSites = $scope.wpSites;
|
||||||
@@ -66,12 +91,12 @@
|
|||||||
var settingMap = {
|
var settingMap = {
|
||||||
'search-indexing': 'searchIndex',
|
'search-indexing': 'searchIndex',
|
||||||
'debugging': 'debugging',
|
'debugging': 'debugging',
|
||||||
'password-protection': 'passwordprotection',
|
'password-protection': 'passwordProtection',
|
||||||
'maintenance-mode': 'maintenanceMode'
|
'maintenance-mode': 'maintenanceMode'
|
||||||
};
|
};
|
||||||
|
|
||||||
var data = {
|
var data = {
|
||||||
siteId: site.id,
|
WPid: site.id,
|
||||||
setting: setting,
|
setting: setting,
|
||||||
value: site[settingMap[setting]] ? 1 : 0
|
value: site[settingMap[setting]] ? 1 : 0
|
||||||
};
|
};
|
||||||
@@ -110,13 +135,28 @@
|
|||||||
GLobalAjaxCall($http, "{% url 'GetCurrentPlugins' %}", data,
|
GLobalAjaxCall($http, "{% url 'GetCurrentPlugins' %}", data,
|
||||||
function(response) {
|
function(response) {
|
||||||
if (response.data.status === 1) {
|
if (response.data.status === 1) {
|
||||||
var plugins = JSON.parse(response.data.plugins);
|
try {
|
||||||
site.activePlugins = plugins.filter(function(p) { return p.status === 'active'; }).length;
|
var plugins = JSON.parse(response.data.plugins);
|
||||||
site.totalPlugins = plugins.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) {
|
function(response) {
|
||||||
site.activePlugins = 'Error';
|
site.activePlugins = 'Error';
|
||||||
|
site.totalPlugins = 'Error';
|
||||||
|
site.loadingPlugins = false;
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -131,9 +171,11 @@
|
|||||||
site.activeTheme = themes.find(function(t) { return t.status === 'active'; }).name;
|
site.activeTheme = themes.find(function(t) { return t.status === 'active'; }).name;
|
||||||
site.totalThemes = themes.length;
|
site.totalThemes = themes.length;
|
||||||
}
|
}
|
||||||
|
site.loadingTheme = false;
|
||||||
},
|
},
|
||||||
function(response) {
|
function(response) {
|
||||||
site.activeTheme = 'Error';
|
site.activeTheme = 'Error';
|
||||||
|
site.loadingTheme = false;
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -154,23 +196,135 @@
|
|||||||
site.debugging = data.debugging === 1;
|
site.debugging = data.debugging === 1;
|
||||||
site.passwordProtection = data.passwordprotection === 1;
|
site.passwordProtection = data.passwordprotection === 1;
|
||||||
site.maintenanceMode = data.maintenanceMode === 1;
|
site.maintenanceMode = data.maintenanceMode === 1;
|
||||||
|
site.loading = false;
|
||||||
fetchPluginData(site);
|
fetchPluginData(site);
|
||||||
fetchThemeData(site);
|
fetchThemeData(site);
|
||||||
} else {
|
} else {
|
||||||
site.phpVersion = 'PHP 7.4'; // Default value on error
|
site.phpVersion = 'PHP 7.4'; // Default value on error
|
||||||
|
site.loading = false;
|
||||||
console.log('Failed to fetch site data:', response.data.error_message);
|
console.log('Failed to fetch site data:', response.data.error_message);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
function(response) {
|
function(response) {
|
||||||
site.phpVersion = 'PHP 7.4'; // Default value on error
|
site.phpVersion = 'PHP 7.4'; // Default value on error
|
||||||
|
site.loading = false;
|
||||||
console.log('Failed to fetch site data');
|
console.log('Failed to fetch site data');
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($scope.wpSites) {
|
if ($scope.wpSites && $scope.wpSites.length > 0) {
|
||||||
$scope.wpSites.forEach(fetchSiteData);
|
// Load data for first site by default
|
||||||
|
$scope.expandedSites[$scope.wpSites[0].id] = true;
|
||||||
|
fetchSiteData($scope.wpSites[0]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$scope.togglePasswordProtection = function(site) {
|
||||||
|
if (site.passwordProtection) {
|
||||||
|
// Show modal for credentials
|
||||||
|
site.PPUsername = "";
|
||||||
|
site.PPPassword = "";
|
||||||
|
$scope.currentWP = site;
|
||||||
|
$('#passwordProtectionModal').modal('show');
|
||||||
|
} else {
|
||||||
|
// Disable password protection
|
||||||
|
var data = {
|
||||||
|
WPid: site.id,
|
||||||
|
setting: 'password-protection',
|
||||||
|
value: 0
|
||||||
|
};
|
||||||
|
|
||||||
|
GLobalAjaxCall($http, "{% url 'UpdateWPSettings' %}", data,
|
||||||
|
function(response) {
|
||||||
|
if (!response.data.status) {
|
||||||
|
site.passwordProtection = !site.passwordProtection;
|
||||||
|
new PNotify({
|
||||||
|
title: 'Operation Failed!',
|
||||||
|
text: response.data.error_message || 'Failed to disable password protection',
|
||||||
|
type: 'error'
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
new PNotify({
|
||||||
|
title: 'Success!',
|
||||||
|
text: 'Password protection disabled successfully.',
|
||||||
|
type: 'success'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
function(error) {
|
||||||
|
site.passwordProtection = !site.passwordProtection;
|
||||||
|
new PNotify({
|
||||||
|
title: 'Operation Failed!',
|
||||||
|
text: 'Could not connect to server.',
|
||||||
|
type: 'error'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
$scope.submitPasswordProtection = function() {
|
||||||
|
if (!$scope.currentWP) {
|
||||||
|
new PNotify({
|
||||||
|
title: 'Error!',
|
||||||
|
text: 'No WordPress site selected.',
|
||||||
|
type: 'error'
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!$scope.currentWP.PPUsername || !$scope.currentWP.PPPassword) {
|
||||||
|
new PNotify({
|
||||||
|
title: 'Error!',
|
||||||
|
text: 'Please provide both username and password',
|
||||||
|
type: 'error'
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var data = {
|
||||||
|
siteId: $scope.currentWP.id,
|
||||||
|
setting: 'password-protection',
|
||||||
|
value: 1,
|
||||||
|
PPUsername: $scope.currentWP.PPUsername,
|
||||||
|
PPPassword: $scope.currentWP.PPPassword
|
||||||
|
};
|
||||||
|
|
||||||
|
$('#passwordProtectionModal').modal('hide');
|
||||||
|
|
||||||
|
GLobalAjaxCall($http, "{% url 'UpdateWPSettings' %}", data,
|
||||||
|
function(response) {
|
||||||
|
if (response.data.status === 1) {
|
||||||
|
// Update the site's password protection state
|
||||||
|
$scope.currentWP.passwordProtection = true;
|
||||||
|
new PNotify({
|
||||||
|
title: 'Success!',
|
||||||
|
text: 'Password protection enabled successfully!',
|
||||||
|
type: 'success'
|
||||||
|
});
|
||||||
|
// Refresh the site data
|
||||||
|
fetchSiteData($scope.currentWP);
|
||||||
|
} else {
|
||||||
|
// Revert the checkbox state
|
||||||
|
$scope.currentWP.passwordProtection = false;
|
||||||
|
new PNotify({
|
||||||
|
title: 'Error!',
|
||||||
|
text: response.data.error_message || 'Failed to enable password protection',
|
||||||
|
type: 'error'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
function(error) {
|
||||||
|
// Revert the checkbox state
|
||||||
|
$scope.currentWP.passwordProtection = false;
|
||||||
|
new PNotify({
|
||||||
|
title: 'Error!',
|
||||||
|
text: 'Could not connect to server',
|
||||||
|
type: 'error'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
);
|
||||||
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
// Add a range filter for pagination
|
// Add a range filter for pagination
|
||||||
@@ -231,7 +385,16 @@
|
|||||||
<div class="wp-site-header">
|
<div class="wp-site-header">
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-sm-8">
|
<div class="col-sm-8">
|
||||||
<h4>{$ site.title $}</h4>
|
<h4>
|
||||||
|
<i class="fas"
|
||||||
|
ng-class="{'fa-chevron-down': isExpanded(site.id), 'fa-chevron-right': !isExpanded(site.id)}"
|
||||||
|
ng-click="toggleSite(site)"
|
||||||
|
style="cursor: pointer; margin-right: 10px;"></i>
|
||||||
|
{$ site.title $}
|
||||||
|
<span ng-if="site.loading || site.loadingPlugins || site.loadingTheme" class="loading-indicator">
|
||||||
|
<i class="fa fa-spinner fa-spin" style="color: #00749C; font-size: 14px;"></i>
|
||||||
|
</span>
|
||||||
|
</h4>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-sm-4 text-right">
|
<div class="col-sm-4 text-right">
|
||||||
<a ng-href="{% url 'WPHome' %}?ID={$ site.id $}" class="btn btn-primary btn-sm">Manage</a>
|
<a ng-href="{% url 'WPHome' %}?ID={$ site.id $}" class="btn btn-primary btn-sm">Manage</a>
|
||||||
@@ -239,7 +402,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="wp-site-content">
|
<div class="wp-site-content" ng-if="isExpanded(site.id)">
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-sm-3">
|
<div class="col-sm-3">
|
||||||
<img ng-src="https://api.microlink.io/?url={$ getFullUrl(site.url) $}&screenshot=true&meta=false&embed=screenshot.url"
|
<img ng-src="https://api.microlink.io/?url={$ getFullUrl(site.url) $}&screenshot=true&meta=false&embed=screenshot.url"
|
||||||
@@ -261,25 +424,30 @@
|
|||||||
<div class="col-sm-3">
|
<div class="col-sm-3">
|
||||||
<div class="info-box">
|
<div class="info-box">
|
||||||
<label>WordPress</label>
|
<label>WordPress</label>
|
||||||
<span>{$ site.version $}</span>
|
<span>{$ site.version || 'Loading...' $}</span>
|
||||||
|
<i ng-if="site.loading" class="fa fa-spinner fa-spin" style="margin-left: 5px; font-size: 12px;"></i>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-sm-3">
|
<div class="col-sm-3">
|
||||||
<div class="info-box">
|
<div class="info-box">
|
||||||
<label>PHP Version</label>
|
<label>PHP Version</label>
|
||||||
<span>{$ site.phpVersion || 'Loading...' $}</span>
|
<span>{$ site.phpVersion || 'Loading...' $}</span>
|
||||||
|
<i ng-if="site.loading" class="fa fa-spinner fa-spin" style="margin-left: 5px; font-size: 12px;"></i>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-sm-3">
|
<div class="col-sm-3">
|
||||||
<div class="info-box">
|
<div class="info-box">
|
||||||
<label>Theme</label>
|
<label>Theme</label>
|
||||||
<span>{$ site.activeTheme || 'twentytwentyfive' $}</span>
|
<span>{$ site.activeTheme || 'Loading...' $}</span>
|
||||||
|
<i ng-if="site.loadingTheme" class="fa fa-spinner fa-spin" style="margin-left: 5px; font-size: 12px;"></i>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-sm-3">
|
<div class="col-sm-3">
|
||||||
<div class="info-box">
|
<div class="info-box">
|
||||||
<label>Plugins</label>
|
<label>Plugins</label>
|
||||||
<span>{$ site.activePlugins || '1' $} active</span>
|
<span ng-if="site.activePlugins !== undefined">{$ site.activePlugins $} active of {$ site.totalPlugins $}</span>
|
||||||
|
<span ng-if="site.activePlugins === undefined">Loading...</span>
|
||||||
|
<i ng-if="site.loadingPlugins" class="fa fa-spinner fa-spin" style="margin-left: 5px; font-size: 12px;"></i>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -301,7 +469,9 @@
|
|||||||
<div class="col-sm-6">
|
<div class="col-sm-6">
|
||||||
<div class="checkbox">
|
<div class="checkbox">
|
||||||
<label>
|
<label>
|
||||||
<input type="checkbox" ng-model="site.passwordProtection" ng-change="updateSetting(site, 'password-protection')">
|
<input type="checkbox"
|
||||||
|
ng-model="site.passwordProtection"
|
||||||
|
ng-change="togglePasswordProtection(site)">
|
||||||
Password protection
|
Password protection
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
@@ -339,6 +509,36 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Password Protection Modal -->
|
||||||
|
<div class="modal fade" id="passwordProtectionModal" tabindex="-1" role="dialog">
|
||||||
|
<div class="modal-dialog" role="document">
|
||||||
|
<div class="modal-content">
|
||||||
|
<div class="modal-header">
|
||||||
|
<h5 class="modal-title">Password Protection</h5>
|
||||||
|
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
|
||||||
|
<span aria-hidden="true">×</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body">
|
||||||
|
<form>
|
||||||
|
<div class="form-group">
|
||||||
|
<label>Username</label>
|
||||||
|
<input type="text" class="form-control" ng-model="currentWP.PPUsername" required>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label>Password</label>
|
||||||
|
<input type="password" class="form-control" ng-model="currentWP.PPPassword" required>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
<div class="modal-footer">
|
||||||
|
<button type="button" class="btn btn-secondary" data-dismiss="modal">Cancel</button>
|
||||||
|
<button type="button" class="btn btn-primary" ng-click="submitPasswordProtection()">Enable Protection</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
@@ -389,6 +589,18 @@
|
|||||||
.text-center .btn {
|
.text-center .btn {
|
||||||
min-width: 100px;
|
min-width: 100px;
|
||||||
}
|
}
|
||||||
|
.loading-indicator {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
color: #00749C;
|
||||||
|
font-size: 14px;
|
||||||
|
padding: 0 8px;
|
||||||
|
}
|
||||||
|
.loading-indicator i {
|
||||||
|
font-size: 14px;
|
||||||
|
margin-left: 4px;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
{% endblock content %}
|
{% endblock content %}
|
||||||
|
|
||||||
|
|||||||
@@ -7,6 +7,9 @@
|
|||||||
{% get_current_language as LANGUAGE_CODE %}
|
{% get_current_language as LANGUAGE_CODE %}
|
||||||
<!-- Current language: {{ LANGUAGE_CODE }} -->
|
<!-- Current language: {{ LANGUAGE_CODE }} -->
|
||||||
|
|
||||||
|
<!-- Add Font Awesome -->
|
||||||
|
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css">
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
$(document).ready(function () {
|
$(document).ready(function () {
|
||||||
$('[data-toggle="tooltip"]').tooltip();
|
$('[data-toggle="tooltip"]').tooltip();
|
||||||
@@ -14,167 +17,256 @@
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div ng-controller="listWebsites" class="container">
|
<div ng-controller="listWebsites" class="container">
|
||||||
|
<!-- Loading State -->
|
||||||
<div id="page-title">
|
<div ng-show="loading" class="text-center" style="padding: 50px;">
|
||||||
<h2 id="domainNamePage">{% trans "List Websites" %}
|
<div class="spinner-border text-primary" role="status" style="width: 3rem; height: 3rem;">
|
||||||
<a class="pull-right btn btn-primary" href="{% url "createWebsite" %}">{% trans "Create Website" %}</a>
|
<span class="sr-only">Loading...</span>
|
||||||
</h2>
|
|
||||||
<img ng-hide="cyberPanelLoading" src="{% static 'images/loading.gif' %}">
|
|
||||||
<p>{% trans "On this page you can launch, list, modify and delete websites from your server." %}</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="col-sm-10" style="padding: 0px; box-shadow: 0px 0px 1px 0px #888888; margin-bottom: 2%">
|
|
||||||
<input ng-change="searchWebsites()" placeholder="Search..." ng-model="patternAdded" name="dom" type="text"
|
|
||||||
class="form-control" required>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="col-sm-2">
|
|
||||||
<div class="form-group">
|
|
||||||
<select ng-model="recordsToShow" ng-change="getFurtherWebsitesFromDB()"
|
|
||||||
class="form-control" id="example-select">
|
|
||||||
<option>10</option>
|
|
||||||
<option>50</option>
|
|
||||||
<option>100</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
</div>
|
||||||
|
<h4 class="mt-3">{% trans "Loading websites..." %}</h4>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div ng-repeat="web in WebSitesList track by $index" class="panel col-md-12"
|
<!-- Main Content (hidden while loading) -->
|
||||||
style="padding: 0px; box-shadow: 0px 0px 1px 0px #888888;">
|
<div ng-hide="loading">
|
||||||
<div class="">
|
<!-- Password Protection Modal -->
|
||||||
<div class="table-responsive no-gutter text-nowrap" style="overflow-x: hidden;">
|
<div class="modal fade" id="passwordProtectionModal" tabindex="-1" role="dialog">
|
||||||
|
<div class="modal-dialog" role="document">
|
||||||
<div style="border-bottom: 1px solid #888888" class="col-md-12">
|
<div class="modal-content">
|
||||||
<div class="col-lg-10 content-box-header" style="text-transform: none;">
|
<div class="modal-header">
|
||||||
<a href="http://{$ web.domain $}" target="_blank" title="Visit Site">
|
<h5 class="modal-title">Password Protection</h5>
|
||||||
<h2 style="display: inline; color: #414C59;" ng-bind="web.domain"></h2>
|
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
|
||||||
</a>
|
<span aria-hidden="true">×</span>
|
||||||
<a target="_self" href="/filemanager/{$ web.domain $}" title="Open File Manager"> --
|
</button>
|
||||||
{% trans "File Manager" %}</a>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="col-md-2 content-box-header" style="text-transform: none;">
|
<div class="modal-body">
|
||||||
<a href="/websites/{$ web.domain $}" target="_self" title="Manage Website">
|
<form>
|
||||||
<i class="p fa fa-external-link btn-icon"> </i>
|
<div class="form-group">
|
||||||
<span>{% trans "Manage" %}</span>
|
<label>Username</label>
|
||||||
</a>
|
<input type="text" class="form-control" ng-model="currentWP.PPUsername" required>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label>Password</label>
|
||||||
|
<input type="password" class="form-control" ng-model="currentWP.PPPassword" required>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
<div class="modal-footer">
|
||||||
|
<button type="button" class="btn btn-secondary" data-dismiss="modal">Cancel</button>
|
||||||
|
<button type="button" class="btn btn-primary" ng-click="submitPasswordProtection()">Enable Protection</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="col-md-12">
|
<div id="page-title">
|
||||||
<div class="col-md-3 content-box-header">
|
<h2 id="domainNamePage">{% trans "List Websites" %}
|
||||||
<i class="p fa fa-sticky-note btn-icon text-muted" data-toggle="tooltip"
|
<a class="pull-right btn btn-primary" href="{% url "createWebsite" %}">{% trans "Create Website" %}</a>
|
||||||
data-placement="right" title="State"> </i>
|
</h2>
|
||||||
<span ng-bind="web.state" style="text-transform: none"></span>
|
<img ng-hide="cyberPanelLoading" src="{% static 'images/loading.gif' %}">
|
||||||
</div>
|
<p>{% trans "On this page you can launch, list, modify and delete websites from your server." %}</p>
|
||||||
<div class="col-md-3 content-box-header">
|
</div>
|
||||||
<i class="p fa fa-map-marker btn-icon text-muted" data-toggle="tooltip"
|
|
||||||
data-placement="right" title="IP Address"> </i>
|
|
||||||
<span ng-bind="web.ipAddress"></span>
|
|
||||||
</div>
|
|
||||||
<div class="col-md-3 content-box-header">
|
|
||||||
<i class="p fa fa-lock btn-icon text-muted" data-toggle="tooltip" data-placement="right"
|
|
||||||
title="SSL"> </i>
|
|
||||||
<span><a ng-click="issueSSL(web.domain)" href=""
|
|
||||||
style="text-transform: none">{% trans "Issue SSL" %}</a></span>
|
|
||||||
</div>
|
|
||||||
<div class="col-md-3 content-box-header">
|
|
||||||
<span ng-bind="web.phpVersion"></span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="col-md-12">
|
<div class="col-sm-10" style="padding: 0px; box-shadow: 0px 0px 1px 0px #888888; margin-bottom: 2%">
|
||||||
<div class="col-md-3 content-box-header">
|
<input ng-change="searchWebsites()" placeholder="Search..." ng-model="patternAdded" name="dom" type="text"
|
||||||
<i class="p fa fa-hdd-o btn-icon text-muted" data-toggle="tooltip"
|
class="form-control" required>
|
||||||
data-placement="right"
|
</div>
|
||||||
title="Disk Usage"> </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"
|
|
||||||
data-placement="right"
|
|
||||||
title="Packages"> </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"
|
|
||||||
title="Owner"> </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"> </i>
|
|
||||||
<span ng-if="web.wp_sites && web.wp_sites.length > 0" style="text-transform: none">
|
|
||||||
{$ web.wp_sites.length $} WordPress Sites
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- WordPress Sites Section -->
|
<div class="col-sm-2">
|
||||||
<div ng-if="web.showWPSites && web.wp_sites && web.wp_sites.length > 0" class="card mt-3">
|
<div class="form-group">
|
||||||
<div class="card-header">
|
<select ng-model="recordsToShow" ng-change="getFurtherWebsitesFromDB()"
|
||||||
<h5 class="mb-0">WordPress Sites</h5>
|
class="form-control" id="example-select">
|
||||||
|
<option>10</option>
|
||||||
|
<option>50</option>
|
||||||
|
<option>100</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div ng-repeat="web in WebSitesList track by $index" class="panel col-md-12"
|
||||||
|
style="padding: 0px; box-shadow: 0px 0px 1px 0px #888888;">
|
||||||
|
<div class="">
|
||||||
|
<div class="table-responsive no-gutter text-nowrap" style="overflow-x: hidden;">
|
||||||
|
|
||||||
|
<div style="border-bottom: 1px solid #888888" class="col-md-12">
|
||||||
|
<div class="col-lg-10 content-box-header" style="text-transform: none;">
|
||||||
|
<a href="http://{$ web.domain $}" target="_blank" title="Visit Site">
|
||||||
|
<h2 style="display: inline; color: #414C59;" ng-bind="web.domain"></h2>
|
||||||
|
</a>
|
||||||
|
<a target="_self" href="/filemanager/{$ web.domain $}" title="Open File Manager"> --
|
||||||
|
{% trans "File Manager" %}</a>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-2 content-box-header" style="text-transform: none;">
|
||||||
|
<a href="/websites/{$ web.domain $}" target="_self" title="Manage Website">
|
||||||
|
<i class="p fa fa-external-link btn-icon"> </i>
|
||||||
|
<span>{% trans "Manage" %}</span>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="card-body">
|
|
||||||
<div class="row">
|
<div class="col-md-12">
|
||||||
<div class="col-md-6 mb-4" ng-repeat="site in web.wp_sites">
|
<div class="col-md-3 content-box-header">
|
||||||
<div class="card h-100">
|
<i class="p fa fa-sticky-note btn-icon text-muted" data-toggle="tooltip"
|
||||||
<div class="card-header d-flex justify-content-between align-items-center">
|
data-placement="right" title="State"> </i>
|
||||||
<h6 class="mb-0">{{site.title}}</h6>
|
<span ng-bind="web.state" style="text-transform: none"></span>
|
||||||
<div>
|
</div>
|
||||||
<button class="btn btn-sm btn-primary mr-2" ng-click="visitSite(site.url)">
|
<div class="col-md-3 content-box-header">
|
||||||
<i class="fas fa-external-link-alt"></i> Visit
|
<i class="p fa fa-map-marker btn-icon text-muted" data-toggle="tooltip"
|
||||||
</button>
|
data-placement="right" title="IP Address"> </i>
|
||||||
<button class="btn btn-sm btn-info mr-2" ng-click="wpLogin(site.id)">
|
<span ng-bind="web.ipAddress"></span>
|
||||||
<i class="fas fa-sign-in-alt"></i> Login
|
</div>
|
||||||
</button>
|
<div class="col-md-3 content-box-header">
|
||||||
<button class="btn btn-sm btn-secondary" ng-click="manageWP(site.id)">
|
<i class="p fa fa-lock btn-icon text-muted" data-toggle="tooltip" data-placement="right"
|
||||||
<i class="fas fa-cog"></i> Manage
|
title="SSL"> </i>
|
||||||
</button>
|
<span><a ng-click="issueSSL(web.domain)" href=""
|
||||||
|
style="text-transform: none">{% trans "Issue SSL" %}</a></span>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-3 content-box-header">
|
||||||
|
<span ng-bind="web.phpVersion"></span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-md-12">
|
||||||
|
<div class="col-md-3 content-box-header">
|
||||||
|
<i class="fa-solid fa-hard-drive btn-icon text-muted" data-toggle="tooltip"
|
||||||
|
data-placement="right"
|
||||||
|
title="Disk Usage"> </i>
|
||||||
|
<span ng-bind="web.diskUsed" style="text-transform: none"></span>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-3 content-box-header">
|
||||||
|
<i class="fa-solid fa-cubes btn-icon text-muted" data-toggle="tooltip"
|
||||||
|
data-placement="right"
|
||||||
|
title="Packages"> </i>
|
||||||
|
<span ng-bind="web.package" style="text-transform: none"></span>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-3 content-box-header">
|
||||||
|
<i class="fa-solid fa-user btn-icon text-muted" data-toggle="tooltip" data-placement="right"
|
||||||
|
title="Owner"> </i>
|
||||||
|
<span ng-bind="web.admin" style="text-transform: none"></span>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-3 content-box-header">
|
||||||
|
<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 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-sm-12">
|
||||||
|
<div class="wp-site-header">
|
||||||
|
<div class="row">
|
||||||
|
<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-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>
|
||||||
</div>
|
</div>
|
||||||
<div class="card-body">
|
<div class="wp-site-content">
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-md-6">
|
<div class="col-sm-3">
|
||||||
<p><strong>WordPress Version:</strong> {{site.version}}</p>
|
<img ng-src="{$ wp.screenshot $}"
|
||||||
<p><strong>PHP Version:</strong> {{site.phpVersion}}</p>
|
alt="{$ wp.title $}"
|
||||||
<p><strong>Active Theme:</strong> {{site.theme}}</p>
|
class="img-responsive"
|
||||||
<p><strong>Active Plugins:</strong> {{site.activePlugins}}</p>
|
style="max-width: 100%; margin-bottom: 10px; min-height: 150px; background: #f5f5f5;"
|
||||||
|
onerror="this.onerror=null; this.src='https://s.wordpress.org/style/images/about/WordPress-logotype-standard.png';">
|
||||||
|
<div class="text-center wp-action-buttons">
|
||||||
|
<a href="javascript:void(0);" ng-click="visitSite(wp)" class="btn btn-outline-secondary btn-sm wp-action-btn">
|
||||||
|
<i class="fa-solid fa-external-link"></i> Visit Site
|
||||||
|
</a>
|
||||||
|
<a href="{% url 'AutoLogin' %}?id={$ wp.id $}" target="_blank" class="btn btn-outline-primary btn-sm wp-action-btn">
|
||||||
|
<i class="fa-brands fa-wordpress"></i> WP Login
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-md-6">
|
<div class="col-sm-9">
|
||||||
<div class="form-group">
|
<div class="row">
|
||||||
<div class="custom-control custom-switch">
|
<div class="col-sm-3">
|
||||||
<input type="checkbox" class="custom-control-input"
|
<div class="info-box">
|
||||||
id="searchIndex{{site.id}}"
|
<label>WordPress</label>
|
||||||
ng-model="site.searchIndex"
|
<span>{$ wp.version || 'Loading...' $}</span>
|
||||||
ng-change="updateSetting(site.id, 'search-indexing', site.searchIndex ? 'enable' : 'disable')">
|
<i ng-if="wp.loading" class="fa fa-spinner fa-spin" style="margin-left: 5px; font-size: 12px;"></i>
|
||||||
<label class="custom-control-label" for="searchIndex{{site.id}}">Search Indexing</label>
|
</div>
|
||||||
|
</div>
|
||||||
|
<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>
|
</div>
|
||||||
<div class="form-group">
|
<div class="row mt-3">
|
||||||
<div class="custom-control custom-switch">
|
<div class="col-sm-6">
|
||||||
<input type="checkbox" class="custom-control-input"
|
<div class="checkbox">
|
||||||
id="debugging{{site.id}}"
|
<label>
|
||||||
ng-model="site.debugging"
|
<input type="checkbox"
|
||||||
ng-change="updateSetting(site.id, 'debugging', site.debugging ? 'enable' : 'disable')">
|
ng-click="updateSetting(wp, 'search-indexing')"
|
||||||
<label class="custom-control-label" for="debugging{{site.id}}">Debugging</label>
|
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>
|
||||||
</div>
|
<div class="col-sm-6">
|
||||||
<div class="form-group">
|
<div class="checkbox">
|
||||||
<div class="custom-control custom-switch">
|
<label>
|
||||||
<input type="checkbox" class="custom-control-input"
|
<input type="checkbox"
|
||||||
id="passwordProtection{{site.id}}"
|
ng-model="wp.passwordProtection"
|
||||||
ng-model="site.passwordProtection"
|
ng-init="wp.passwordProtection = wp.passwordProtection || false"
|
||||||
ng-change="updateSetting(site.id, 'password-protection', site.passwordProtection ? 'enable' : 'disable')">
|
ng-change="togglePasswordProtection(wp)">
|
||||||
<label class="custom-control-label" for="passwordProtection{{site.id}}">Password Protection</label>
|
Password protection
|
||||||
</div>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group">
|
<div class="checkbox">
|
||||||
<div class="custom-control custom-switch">
|
<label>
|
||||||
<input type="checkbox" class="custom-control-input"
|
<input type="checkbox"
|
||||||
id="maintenanceMode{{site.id}}"
|
ng-click="updateSetting(wp, 'maintenance-mode')"
|
||||||
ng-model="site.maintenanceMode"
|
ng-checked="wp.maintenanceMode == 1">
|
||||||
ng-change="updateSetting(site.id, 'maintenance-mode', site.maintenanceMode ? 'enable' : 'disable')">
|
Maintenance mode
|
||||||
<label class="custom-control-label" for="maintenanceMode{{site.id}}">Maintenance Mode</label>
|
</label>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -184,28 +276,163 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
|
|
||||||
<div id="listFail" class="alert alert-danger">
|
<style>
|
||||||
<p>{% trans "Cannot list websites. Error message:" %} {$ errorMessage $}</p>
|
.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>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
<div style="margin-top: 2%" class="row">
|
||||||
<div style="margin-top: 2%" class="row">
|
<div class="col-md-12">
|
||||||
<div class="col-md-12">
|
<div class="row">
|
||||||
<div class="row">
|
<div class="col-md-9">
|
||||||
<div class="col-md-9">
|
|
||||||
</div>
|
|
||||||
<div class="col-md-3">
|
|
||||||
<div class="form-group">
|
|
||||||
<select ng-model="currentPage" class="form-control"
|
|
||||||
ng-change="getFurtherWebsitesFromDB()">
|
|
||||||
<option ng-repeat="page in pagination">{$ $index + 1 $}</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
<div class="col-md-3">
|
||||||
</div> <!-- end row -->
|
<div class="form-group">
|
||||||
|
<select ng-model="currentPage" class="form-control"
|
||||||
|
ng-change="getFurtherWebsitesFromDB()">
|
||||||
|
<option ng-repeat="page in pagination">{$ $index + 1 $}</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div> <!-- end row -->
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -51,6 +51,7 @@ urlpatterns = [
|
|||||||
path('AddWPsiteforRemoteBackup', views.AddWPsiteforRemoteBackup, name='AddWPsiteforRemoteBackup'),
|
path('AddWPsiteforRemoteBackup', views.AddWPsiteforRemoteBackup, name='AddWPsiteforRemoteBackup'),
|
||||||
path('UpdateRemoteschedules', views.UpdateRemoteschedules, name='UpdateRemoteschedules'),
|
path('UpdateRemoteschedules', views.UpdateRemoteschedules, name='UpdateRemoteschedules'),
|
||||||
path('ScanWordpressSite', views.ScanWordpressSite, name='ScanWordpressSite'),
|
path('ScanWordpressSite', views.ScanWordpressSite, name='ScanWordpressSite'),
|
||||||
|
path('fetchWPDetails', views.fetchWPDetails, name='fetchWPDetails'),
|
||||||
|
|
||||||
# AddPlugin
|
# AddPlugin
|
||||||
path('ConfigurePlugins', views.ConfigurePlugins, name='ConfigurePlugins'),
|
path('ConfigurePlugins', views.ConfigurePlugins, name='ConfigurePlugins'),
|
||||||
@@ -178,6 +179,11 @@ urlpatterns = [
|
|||||||
path('ListDockerSites', views.ListDockerSites, name='ListDockerSites'),
|
path('ListDockerSites', views.ListDockerSites, name='ListDockerSites'),
|
||||||
path('fetchDockersite', views.fetchDockersite, name='fetchDockersite'),
|
path('fetchDockersite', views.fetchDockersite, name='fetchDockersite'),
|
||||||
|
|
||||||
|
# Docker Container Actions
|
||||||
|
path('docker/startContainer', views.startContainer, name='startContainer'),
|
||||||
|
path('docker/stopContainer', views.stopContainer, name='stopContainer'),
|
||||||
|
path('docker/restartContainer', views.restartContainer, name='restartContainer'),
|
||||||
|
|
||||||
# SSH Configs
|
# SSH Configs
|
||||||
path('getSSHConfigs', views.getSSHConfigs, name='getSSHConfigs'),
|
path('getSSHConfigs', views.getSSHConfigs, name='getSSHConfigs'),
|
||||||
path('deleteSSHKey', views.deleteSSHKey, name='deleteSSHKey'),
|
path('deleteSSHKey', views.deleteSSHKey, name='deleteSSHKey'),
|
||||||
@@ -194,6 +200,4 @@ urlpatterns = [
|
|||||||
# Catch all for domains
|
# Catch all for domains
|
||||||
path('<domain>/<childDomain>', views.launchChild, name='launchChild'),
|
path('<domain>/<childDomain>', views.launchChild, name='launchChild'),
|
||||||
path('<domain>', views.domain, name='domain'),
|
path('<domain>', views.domain, name='domain'),
|
||||||
|
|
||||||
path(r'GetWPSitesByDomain', views.GetWPSitesByDomain, name='GetWPSitesByDomain'),
|
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -14,6 +14,9 @@ from websiteFunctions.models import wpplugins
|
|||||||
from websiteFunctions.website import WebsiteManager
|
from websiteFunctions.website import WebsiteManager
|
||||||
from websiteFunctions.pluginManager import pluginManager
|
from websiteFunctions.pluginManager import pluginManager
|
||||||
from django.views.decorators.csrf import csrf_exempt
|
from django.views.decorators.csrf import csrf_exempt
|
||||||
|
from .dockerviews import startContainer as docker_startContainer
|
||||||
|
from .dockerviews import stopContainer as docker_stopContainer
|
||||||
|
from .dockerviews import restartContainer as docker_restartContainer
|
||||||
|
|
||||||
def loadWebsitesHome(request):
|
def loadWebsitesHome(request):
|
||||||
val = request.session['userID']
|
val = request.session['userID']
|
||||||
@@ -1843,19 +1846,40 @@ def Dockersitehome(request, dockerapp):
|
|||||||
except KeyError:
|
except KeyError:
|
||||||
return redirect(loadLoginPage)
|
return redirect(loadLoginPage)
|
||||||
|
|
||||||
def GetWPSitesByDomain(request):
|
def fetchWPDetails(request):
|
||||||
try:
|
try:
|
||||||
userID = request.session['userID']
|
userID = request.session['userID']
|
||||||
data = json.loads(request.body)
|
data = {
|
||||||
domain = data['domain']
|
'domain': request.POST.get('domain')
|
||||||
|
}
|
||||||
wm = WebsiteManager()
|
wm = WebsiteManager()
|
||||||
response = wm.GetWPSitesByDomain(userID, data)
|
return wm.fetchWPSitesForDomain(userID, data)
|
||||||
|
|
||||||
return response
|
|
||||||
except KeyError:
|
except KeyError:
|
||||||
return redirect(reverse('login'))
|
return redirect(loadLoginPage)
|
||||||
except BaseException as msg:
|
|
||||||
data_ret = {'status': 0, 'error_message': str(msg)}
|
@csrf_exempt
|
||||||
json_data = json.dumps(data_ret)
|
def startContainer(request):
|
||||||
return HttpResponse(json_data)
|
try:
|
||||||
|
if request.method == 'POST':
|
||||||
|
return docker_startContainer(request)
|
||||||
|
return HttpResponse('Not allowed')
|
||||||
|
except KeyError:
|
||||||
|
return redirect(loadLoginPage)
|
||||||
|
|
||||||
|
@csrf_exempt
|
||||||
|
def stopContainer(request):
|
||||||
|
try:
|
||||||
|
if request.method == 'POST':
|
||||||
|
return docker_stopContainer(request)
|
||||||
|
return HttpResponse('Not allowed')
|
||||||
|
except KeyError:
|
||||||
|
return redirect(loadLoginPage)
|
||||||
|
|
||||||
|
@csrf_exempt
|
||||||
|
def restartContainer(request):
|
||||||
|
try:
|
||||||
|
if request.method == 'POST':
|
||||||
|
return docker_restartContainer(request)
|
||||||
|
return HttpResponse('Not allowed')
|
||||||
|
except KeyError:
|
||||||
|
return redirect(loadLoginPage)
|
||||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user