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