mirror of
https://github.com/usmannasir/cyberpanel.git
synced 2026-01-28 10:19:04 +01:00
@@ -13,6 +13,16 @@ https://docs.djangoproject.com/en/1.11/ref/settings/
|
||||
import os
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
# Patreon OAuth Configuration for Paid Plugins
|
||||
# SECURITY: Environment variables take precedence. Hardcoded values are fallback for this server only.
|
||||
# For repository version, use empty defaults and set via environment variables.
|
||||
PATREON_CLIENT_ID = os.environ.get('PATREON_CLIENT_ID', 'LFXeXUcfrM8MeVbUcmGbB7BgeJ9RzZi2v_H9wL4d9vG6t1dV4SUnQ4ibn9IYzvt7')
|
||||
PATREON_CLIENT_SECRET = os.environ.get('PATREON_CLIENT_SECRET', 'APuJ5qoL3TLFmNnGDVkgl-qr3sCzp2CQsKfslBbp32hhnhlD0y6-ZcSCkb_FaUJv')
|
||||
PATREON_CREATOR_ID = os.environ.get('PATREON_CREATOR_ID', '')
|
||||
PATREON_MEMBERSHIP_TIER_ID = os.environ.get('PATREON_MEMBERSHIP_TIER_ID', '27789984') # CyberPanel Paid Plugin tier
|
||||
PATREON_CREATOR_ACCESS_TOKEN = os.environ.get('PATREON_CREATOR_ACCESS_TOKEN', 'niAHRiI9SgrRCMmaf5exoXXphy3RWXWsX4kO5Yv9SQI')
|
||||
PATREON_CREATOR_REFRESH_TOKEN = os.environ.get('PATREON_CREATOR_REFRESH_TOKEN', 'VZlCQoPwJUr4NLni1N82-K_CpJHTAOYUOCx2PujdjQg')
|
||||
|
||||
# Build paths inside the project like this: os.path.join(BASE_DIR, ...)
|
||||
BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||
|
||||
@@ -55,7 +65,6 @@ INSTALLED_APPS = [
|
||||
|
||||
# Apps with multiple or complex dependencies
|
||||
'emailPremium',
|
||||
'testPlugin', # Test plugin
|
||||
'emailMarketing', # Depends on websiteFunctions and loginSystem
|
||||
'cloudAPI', # Depends on websiteFunctions
|
||||
'containerization', # Depends on websiteFunctions
|
||||
@@ -84,12 +93,6 @@ INSTALLED_APPS = [
|
||||
|
||||
# Add plugins that are installed (plugin installer handles adding/removing)
|
||||
# Plugins are added by plugin installer when plugins are installed
|
||||
if os.path.exists('/usr/local/CyberCP/discordWebhooks/__init__.py'):
|
||||
INSTALLED_APPS.append('discordWebhooks')
|
||||
if os.path.exists('/usr/local/CyberCP/fail2ban/__init__.py'):
|
||||
INSTALLED_APPS.append('fail2ban')
|
||||
if os.path.exists('/usr/local/CyberCP/pm2Manager/__init__.py'):
|
||||
INSTALLED_APPS.append('pm2Manager')
|
||||
|
||||
MIDDLEWARE = [
|
||||
'django.middleware.security.SecurityMiddleware',
|
||||
|
||||
@@ -44,15 +44,14 @@ urlpatterns = [
|
||||
path('filemanager/', include('filemanager.urls')),
|
||||
path('emailPremium/', include('emailPremium.urls')),
|
||||
path('manageservices/', include('manageServices.urls')),
|
||||
path('plugins/testPlugin/', include('testPlugin.urls')),
|
||||
path('plugins/', include('pluginHolder.urls')),
|
||||
path('emailMarketing/', include('emailMarketing.urls')),
|
||||
path('cloudAPI/', include('cloudAPI.urls')),
|
||||
path('docker/', include('dockerManager.urls')),
|
||||
path('container/', include('containerization.urls')),
|
||||
path('CloudLinux/', include('CLManager.urls')),
|
||||
path('IncrementalBackups/', include('IncBackups.urls')),
|
||||
path('aiscanner/', include('aiScanner.urls')),
|
||||
path('emailMarketing/', include('emailMarketing.urls')),
|
||||
# path('Terminal/', include('WebTerminal.urls')),
|
||||
path('', include('loginSystem.urls')),
|
||||
]
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
{% load i18n %}
|
||||
{% get_current_language as LANGUAGE_CODE %}
|
||||
{% with CP_VERSION="2.4.4.1" %}
|
||||
{% with CP_VERSION="2.5.5-dev-fix" %}
|
||||
<!DOCTYPE html>
|
||||
<html lang="en" ng-app="CyberCP">
|
||||
<html lang="en" ng-app="CyberCP" {% if cosmetic.MainDashboardCSS %}data-dynamic-css="{{ cosmetic.MainDashboardCSS|escape }}"{% endif %}>
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
@@ -17,30 +17,39 @@
|
||||
|
||||
<!-- Custom CSS -->
|
||||
<style>
|
||||
{{ cosmetic.MainDashboardCSS | safe }}
|
||||
/* Custom dashboard CSS - base styles */
|
||||
body { visibility: visible; }
|
||||
</style>
|
||||
|
||||
<!-- Mobile Responsive CSS -->
|
||||
<link rel="stylesheet" type="text/css" href="{% static 'baseTemplate/assets/mobile-responsive.css' %}?v={{ CP_VERSION }}">
|
||||
|
||||
<!-- Readability Fixes CSS -->
|
||||
<link rel="stylesheet" type="text/css" href="{% static 'baseTemplate/assets/readability-fixes.css' %}?v={{ CP_VERSION }}">
|
||||
|
||||
<!-- Core Scripts -->
|
||||
<!-- Core Scripts - AngularJS MUST load FIRST before all other scripts -->
|
||||
<script src="{% static 'baseTemplate/angularjs.1.6.5.js' %}?v={{ CP_VERSION }}"></script>
|
||||
|
||||
<!-- Dynamic CSS from Django template (injected via JavaScript to avoid linter errors) -->
|
||||
<script>
|
||||
// Inject dynamic CSS from data attribute to avoid CSS/JS linter errors
|
||||
(function() {
|
||||
var htmlElement = document.documentElement;
|
||||
var cssContent = htmlElement.getAttribute('data-dynamic-css');
|
||||
if (cssContent) {
|
||||
var style = document.createElement('style');
|
||||
style.textContent = cssContent;
|
||||
document.head.appendChild(style);
|
||||
}
|
||||
})();
|
||||
</script>
|
||||
<script src="https://code.jquery.com/jquery-2.2.4.min.js"></script>
|
||||
<!-- Bootstrap JavaScript -->
|
||||
|
||||
<script src="{% static 'baseTemplate/assets/bootstrap/js/bootstrap.min.js' %}?v={{ CP_VERSION }}"></script>
|
||||
<script src="{% static 'baseTemplate/bootstrap-toggle.min.js' %}?v={{ CP_VERSION }}"></script>
|
||||
<script src="{% static 'baseTemplate/custom-js/qrious.min.js' %}?v={{ CP_VERSION }}"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/qrious/dist/qrious.min.js"></script>
|
||||
<script src="{% static 'baseTemplate/custom-js/system-status.js' %}?v={{ CP_VERSION }}"></script>
|
||||
|
||||
<!-- Font Awesome -->
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
|
||||
|
||||
<!-- Chart.js -->
|
||||
<script src="{% static 'baseTemplate/custom-js/chart.umd.min.js' %}?v={{ CP_VERSION }}"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
|
||||
|
||||
<!-- PNotify -->
|
||||
<link rel="stylesheet" type="text/css" href="{% static 'baseTemplate/custom-js/pnotify.custom.min.css' %}?v={{ CP_VERSION }}">
|
||||
@@ -1566,7 +1575,7 @@
|
||||
|
||||
{% block header_scripts %}{% endblock %}
|
||||
</head>
|
||||
<body>
|
||||
<body data-has-backup-configured="{% if request.session.has_backup_configured %}true{% else %}false{% endif %}">
|
||||
<!-- Header -->
|
||||
<div id="header">
|
||||
<button id="mobile-menu-toggle" class="mobile-menu-toggle" onclick="toggleSidebar()" style="display: none;">
|
||||
@@ -1647,7 +1656,7 @@
|
||||
<div class="info-line">
|
||||
<strong>Uptime:</strong>
|
||||
<span ng-show="!uptimeLoaded">Loading...</span>
|
||||
<span ng-show="uptimeLoaded" ng-bind="uptime">N/A</span>
|
||||
<span ng-show="uptimeLoaded">{$ uptime $}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -1803,9 +1812,6 @@
|
||||
<a href="{% url 'listChildDomains' %}" class="menu-item">
|
||||
<span>List Sub/Addon Domains</span>
|
||||
</a>
|
||||
<a href="{% url 'fixSubdomainLogs' %}" class="menu-item">
|
||||
<span>Fix Subdomain Logs</span>
|
||||
</a>
|
||||
{% if admin or modifyWebsite %}
|
||||
<a href="{% url 'modifyWebsite' %}" class="menu-item">
|
||||
<span>Modify Website</span>
|
||||
@@ -2157,7 +2163,7 @@
|
||||
<a href="{% url 'manageImages' %}" class="menu-item">
|
||||
<span>Manage Images</span>
|
||||
</a>
|
||||
<a href="{% url 'listContainers' %}" class="menu-item">
|
||||
<a href="{% url 'listContainersPage' %}" class="menu-item">
|
||||
<span>Manage Containers</span>
|
||||
</a>
|
||||
<a href="{% url 'containerImage' %}" class="menu-item">
|
||||
@@ -2287,9 +2293,6 @@
|
||||
<a href="{% url 'aiScannerHome' %}" class="menu-item">
|
||||
<span>AI Scanner</span>
|
||||
</a>
|
||||
<a href="#" class="menu-item" onclick="loadSecurityManagement(); return false;">
|
||||
<span>Security Management</span>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<a href="#" class="menu-item" onclick="toggleSubmenu('mail-settings-submenu', this); return false;">
|
||||
@@ -2309,9 +2312,6 @@
|
||||
<a href="{% url 'SpamAssassin' %}" class="menu-item">
|
||||
<span>SpamAssassin</span>
|
||||
</a>
|
||||
<a href="{% url 'emailMarketing' %}" class="menu-item">
|
||||
<span>Email Marketing</span>
|
||||
</a>
|
||||
<a href="{% url 'MailScanner' %}" class="menu-item">
|
||||
<span>MailScanner</span>
|
||||
</a>
|
||||
@@ -2343,11 +2343,6 @@
|
||||
<a href="{% url 'managePureFtpd' %}" class="menu-item">
|
||||
<span>Manage FTP</span>
|
||||
</a>
|
||||
{% if admin %}
|
||||
<a href="#" class="menu-item" onclick="loadBandwidthManagement(); return false;">
|
||||
<span>Bandwidth Management</span>
|
||||
</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<a href="#" class="menu-item" onclick="toggleSubmenu('plugins-submenu', this); return false;">
|
||||
@@ -2452,6 +2447,12 @@
|
||||
<script src="{% static 'CLManager/CLManager.js' %}?v={{ CP_VERSION }}"></script>
|
||||
|
||||
<!-- Scripts -->
|
||||
<!-- Set backup configuration status from Django template via data attribute -->
|
||||
<script>
|
||||
// Read backup configuration status from data attribute (set by Django template)
|
||||
var backupConfigElement = document.querySelector('[data-has-backup-configured]');
|
||||
window.hasBackupConfigured = backupConfigElement ? backupConfigElement.getAttribute('data-has-backup-configured') === 'true' : false;
|
||||
</script>
|
||||
<script>
|
||||
function toggleSidebar() {
|
||||
const sidebar = document.getElementById('sidebar');
|
||||
@@ -2580,9 +2581,10 @@
|
||||
// Check if user has backup configured (you'll need to implement this API)
|
||||
// For now, we'll show it by default unless they have a backup plan
|
||||
// This should be replaced with an actual check
|
||||
{% if not request.session.has_backup_configured %}
|
||||
// Note: hasBackupConfigured is set via Django template before this script
|
||||
if (typeof window.hasBackupConfigured !== 'undefined' && !window.hasBackupConfigured) {
|
||||
showBackupNotification();
|
||||
{% endif %}
|
||||
}
|
||||
|
||||
// For demonstration, let's show it if URL doesn't contain 'OneClickBackups'
|
||||
if (!window.location.href.includes('OneClickBackups')) {
|
||||
@@ -2819,7 +2821,44 @@
|
||||
item.classList.remove('active', 'has-active-child');
|
||||
});
|
||||
|
||||
// Find and activate the matching menu item
|
||||
// Special handling for plugin pages - expand Plugins submenu and highlight
|
||||
if (currentPath.startsWith('/plugins/')) {
|
||||
const pluginsSubmenu = document.getElementById('plugins-submenu');
|
||||
const pluginsMenuItem = pluginsSubmenu ? pluginsSubmenu.previousElementSibling : null;
|
||||
|
||||
if (pluginsSubmenu) {
|
||||
pluginsSubmenu.classList.add('show');
|
||||
}
|
||||
|
||||
if (pluginsMenuItem && pluginsMenuItem.classList.contains('menu-item')) {
|
||||
pluginsMenuItem.classList.add('expanded', 'has-active-child');
|
||||
}
|
||||
|
||||
// Check if it's the installed page
|
||||
if (currentPath === '/plugins/installed' || currentPath.startsWith('/plugins/installed')) {
|
||||
const installedLink = pluginsSubmenu ? pluginsSubmenu.querySelector('a[href*="installed"]') : null;
|
||||
if (installedLink) {
|
||||
installedLink.classList.add('active');
|
||||
}
|
||||
} else if (currentPath.match(/^\/plugins\/[^\/]+\/(settings|help)/)) {
|
||||
// It's a plugin settings/help page - highlight "Installed" menu item
|
||||
const installedLink = pluginsSubmenu ? pluginsSubmenu.querySelector('a[href*="installed"]') : null;
|
||||
if (installedLink) {
|
||||
installedLink.classList.add('active');
|
||||
}
|
||||
} else {
|
||||
// Try to match specific plugin links in the submenu
|
||||
const pluginLinks = pluginsSubmenu ? pluginsSubmenu.querySelectorAll('a.menu-item') : [];
|
||||
pluginLinks.forEach(link => {
|
||||
const href = link.getAttribute('href');
|
||||
if (href && (currentPath === href || currentPath.startsWith(href + '/'))) {
|
||||
link.classList.add('active');
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Find and activate the matching menu item (exact matches)
|
||||
allMenuItems.forEach(item => {
|
||||
const href = item.getAttribute('href');
|
||||
if (href && href !== '#' && currentPath === href) {
|
||||
@@ -2917,4 +2956,4 @@
|
||||
{% block footer_scripts %}{% endblock %}
|
||||
</body>
|
||||
</html>
|
||||
{% endwith %}
|
||||
{% endwith %}
|
||||
|
||||
@@ -698,9 +698,31 @@ class ContainerManager(multi.Thread):
|
||||
return ACLManager.loadErrorJson('listContainerStatus', 0)
|
||||
|
||||
currentACL = ACLManager.loadedACL(userID)
|
||||
pageNumber = int(data['page'])
|
||||
json_data = self.findContainersJson(currentACL, userID, pageNumber)
|
||||
final_dic = {'listContainerStatus': 1, 'error_message': "None", "data": json_data}
|
||||
pageNumber = max(1, int(data.get('page', 1)))
|
||||
items_per_page = 10
|
||||
|
||||
all_containers = ACLManager.findContainersObjects(currentACL, userID)
|
||||
totalCount = len(all_containers)
|
||||
totalPages = max(1, int(ceil(float(totalCount) / float(items_per_page))))
|
||||
|
||||
start = (pageNumber - 1) * items_per_page
|
||||
end = start + items_per_page
|
||||
page_containers = all_containers[start:end]
|
||||
|
||||
rows = []
|
||||
for items in page_containers:
|
||||
rows.append({'name': items.name, 'admin': items.admin.userName, 'tag': items.tag, 'image': items.image})
|
||||
json_data = json.dumps(rows)
|
||||
|
||||
final_dic = {
|
||||
'listContainerStatus': 1,
|
||||
'error_message': 'None',
|
||||
'data': json_data,
|
||||
'totalCount': totalCount,
|
||||
'totalPages': totalPages,
|
||||
'currentPage': pageNumber,
|
||||
'itemsPerPage': items_per_page
|
||||
}
|
||||
final_json = json.dumps(final_dic)
|
||||
return HttpResponse(final_json)
|
||||
except BaseException as msg:
|
||||
@@ -2407,9 +2429,9 @@ class ContainerManager(multi.Thread):
|
||||
json_data = json.dumps(data_ret)
|
||||
return HttpResponse(json_data)
|
||||
|
||||
def listContainers(self, userID=None):
|
||||
def listContainersJson(self, userID=None):
|
||||
"""
|
||||
Get list of all Docker containers
|
||||
Get list of all Docker containers as JSON (for Angular API).
|
||||
"""
|
||||
try:
|
||||
admin = Administrator.objects.get(pk=userID)
|
||||
@@ -2441,13 +2463,13 @@ class ContainerManager(multi.Thread):
|
||||
'containers': container_list
|
||||
}
|
||||
json_data = json.dumps(data_ret)
|
||||
return HttpResponse(json_data)
|
||||
return HttpResponse(json_data, content_type='application/json')
|
||||
|
||||
except Exception as msg:
|
||||
logging.CyberCPLogFileWriter.writeToFile(str(msg) + ' [ContainerManager.listContainers]')
|
||||
logging.CyberCPLogFileWriter.writeToFile(str(msg) + ' [ContainerManager.listContainersJson]')
|
||||
data_ret = {'status': 0, 'error_message': str(msg)}
|
||||
json_data = json.dumps(data_ret)
|
||||
return HttpResponse(json_data)
|
||||
return HttpResponse(json_data, content_type='application/json')
|
||||
|
||||
def getDockerNetworks(self, userID=None):
|
||||
"""
|
||||
|
||||
@@ -729,6 +729,59 @@ app.controller('listContainers', function ($scope, $http) {
|
||||
$scope.assignActive = "";
|
||||
$scope.dockerOwner = "";
|
||||
|
||||
/* Pagination (ACTIVITY BOARD-style) */
|
||||
var CONTAINERS_PER_PAGE = 10;
|
||||
$scope.pagination = { containers: { currentPage: 1, itemsPerPage: CONTAINERS_PER_PAGE } };
|
||||
$scope.gotoPageInput = { containers: 1 };
|
||||
$scope.totalCount = 0;
|
||||
$scope.totalPages = 1;
|
||||
$scope.Math = Math;
|
||||
|
||||
$scope.getTotalPages = function(section) {
|
||||
if (section === 'containers') return Math.max(1, $scope.totalPages || 1);
|
||||
return 1;
|
||||
};
|
||||
|
||||
$scope.goToPage = function(section, page) {
|
||||
if (section !== 'containers') return;
|
||||
var totalPages = $scope.getTotalPages(section);
|
||||
var p = parseInt(page, 10);
|
||||
if (isNaN(p) || p < 1) p = 1;
|
||||
if (p > totalPages) p = totalPages;
|
||||
$scope.getFurtherContainersFromDB(p);
|
||||
};
|
||||
|
||||
$scope.nextPage = function(section) {
|
||||
if (section !== 'containers') return;
|
||||
if ($scope.pagination.containers.currentPage < $scope.getTotalPages(section)) {
|
||||
$scope.getFurtherContainersFromDB($scope.pagination.containers.currentPage + 1);
|
||||
}
|
||||
};
|
||||
|
||||
$scope.prevPage = function(section) {
|
||||
if (section !== 'containers') return;
|
||||
if ($scope.pagination.containers.currentPage > 1) {
|
||||
$scope.getFurtherContainersFromDB($scope.pagination.containers.currentPage - 1);
|
||||
}
|
||||
};
|
||||
|
||||
$scope.getPageNumbers = function(section) {
|
||||
if (section !== 'containers') return [];
|
||||
var totalPages = $scope.getTotalPages(section);
|
||||
var current = $scope.pagination.containers.currentPage;
|
||||
var maxVisible = 5;
|
||||
var pages = [];
|
||||
if (totalPages <= maxVisible) {
|
||||
for (var i = 1; i <= totalPages; i++) pages.push(i);
|
||||
} else {
|
||||
var start = Math.max(1, current - 2);
|
||||
var end = Math.min(totalPages, start + maxVisible - 1);
|
||||
if (end - start + 1 < maxVisible) start = Math.max(1, end - maxVisible + 1);
|
||||
for (var j = start; j <= end; j++) pages.push(j);
|
||||
}
|
||||
return pages;
|
||||
};
|
||||
|
||||
$scope.assignContainer = function (name) {
|
||||
console.log('assignContainer called with:', name);
|
||||
$scope.assignActive = name;
|
||||
@@ -783,7 +836,7 @@ app.controller('listContainers', function ($scope, $http) {
|
||||
title: 'Container assigned successfully',
|
||||
type: 'success'
|
||||
});
|
||||
window.location.href = '/docker/listContainers';
|
||||
window.location.href = '/docker/containers';
|
||||
}
|
||||
else {
|
||||
new PNotify({
|
||||
@@ -1264,77 +1317,35 @@ app.controller('listContainers', function ($scope, $http) {
|
||||
$scope.logInfo = null;
|
||||
};
|
||||
|
||||
url = "/docker/getContainerList";
|
||||
|
||||
var data = {page: 1};
|
||||
|
||||
var config = {
|
||||
headers: {
|
||||
'X-CSRFToken': getCookie('csrftoken')
|
||||
}
|
||||
};
|
||||
|
||||
$http.post(url, data, config).then(ListInitialData, cantLoadInitialData);
|
||||
|
||||
|
||||
function ListInitialData(response) {
|
||||
console.log(response);
|
||||
|
||||
function handleContainerListResponse(response) {
|
||||
if (response.data.listContainerStatus === 1) {
|
||||
|
||||
var finalData = JSON.parse(response.data.data);
|
||||
$scope.ContainerList = finalData;
|
||||
console.log($scope.ContainerList);
|
||||
$scope.totalCount = response.data.totalCount || 0;
|
||||
$scope.totalPages = Math.max(1, response.data.totalPages || 1);
|
||||
var cp = Math.max(1, parseInt(response.data.currentPage, 10) || 1);
|
||||
$scope.pagination.containers.currentPage = cp;
|
||||
$scope.gotoPageInput.containers = cp;
|
||||
$("#listFail").hide();
|
||||
}
|
||||
else {
|
||||
} else {
|
||||
$("#listFail").fadeIn();
|
||||
$scope.errorMessage = response.data.error_message;
|
||||
|
||||
$scope.errorMessage = response.data.error_message || 'Failed to load containers';
|
||||
}
|
||||
}
|
||||
|
||||
function cantLoadInitialData(response) {
|
||||
console.log("not good");
|
||||
$("#listFail").fadeIn();
|
||||
$scope.errorMessage = (response && response.data && response.data.error_message) ? response.data.error_message : 'Could not connect to server';
|
||||
}
|
||||
|
||||
var config = {
|
||||
headers: { 'X-CSRFToken': getCookie('csrftoken') }
|
||||
};
|
||||
$http.post("/docker/getContainerList", { page: 1 }, config).then(handleContainerListResponse, cantLoadInitialData);
|
||||
|
||||
$scope.getFurtherContainersFromDB = function (pageNumber) {
|
||||
|
||||
var config = {
|
||||
headers: {
|
||||
'X-CSRFToken': getCookie('csrftoken')
|
||||
}
|
||||
};
|
||||
|
||||
var data = {page: pageNumber};
|
||||
|
||||
|
||||
dataurl = "/docker/getContainerList";
|
||||
|
||||
$http.post(dataurl, data, config).then(ListInitialData, cantLoadInitialData);
|
||||
|
||||
|
||||
function ListInitialData(response) {
|
||||
if (response.data.listContainerStatus === 1) {
|
||||
|
||||
var finalData = JSON.parse(response.data.data);
|
||||
$scope.ContainerList = finalData;
|
||||
$("#listFail").hide();
|
||||
}
|
||||
else {
|
||||
$("#listFail").fadeIn();
|
||||
$scope.errorMessage = response.data.error_message;
|
||||
console.log(response.data);
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
function cantLoadInitialData(response) {
|
||||
console.log("not good");
|
||||
}
|
||||
|
||||
|
||||
var p = Math.max(1, parseInt(pageNumber, 10) || 1);
|
||||
$http.post("/docker/getContainerList", { page: p }, config).then(handleContainerListResponse, cantLoadInitialData);
|
||||
};
|
||||
});
|
||||
|
||||
@@ -1906,7 +1917,7 @@ app.controller('viewContainer', function ($scope, $http, $interval, $timeout) {
|
||||
text: 'Redirecting...',
|
||||
type: 'success'
|
||||
});
|
||||
window.location.href = '/docker/listContainers';
|
||||
window.location.href = '/docker/containers';
|
||||
}
|
||||
else {
|
||||
new PNotify({
|
||||
@@ -2703,257 +2714,3 @@ app.controller('manageImages', function ($scope, $http) {
|
||||
}
|
||||
});
|
||||
|
||||
// Container List Controller
|
||||
app.controller('listContainers', function ($scope, $http, $timeout, $window) {
|
||||
$scope.containers = [];
|
||||
$scope.loading = false;
|
||||
$scope.updateContainerName = '';
|
||||
$scope.currentImage = '';
|
||||
$scope.newImage = '';
|
||||
$scope.newTag = 'latest';
|
||||
|
||||
// Load containers list
|
||||
$scope.loadContainers = function() {
|
||||
$scope.loading = true;
|
||||
var url = '/docker/listContainers';
|
||||
var config = {
|
||||
headers: {
|
||||
'X-CSRFToken': getCookie('csrftoken')
|
||||
}
|
||||
};
|
||||
|
||||
$http.post(url, {}, config).then(function(response) {
|
||||
$scope.loading = false;
|
||||
if (response.data.status === 1) {
|
||||
$scope.containers = response.data.containers || [];
|
||||
} else {
|
||||
new PNotify({
|
||||
title: 'Error Loading Containers',
|
||||
text: response.data.error_message || 'Failed to load containers',
|
||||
type: 'error'
|
||||
});
|
||||
}
|
||||
}, function(error) {
|
||||
$scope.loading = false;
|
||||
new PNotify({
|
||||
title: 'Connection Error',
|
||||
text: 'Could not connect to server',
|
||||
type: 'error'
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
// Initialize containers on page load
|
||||
$scope.loadContainers();
|
||||
|
||||
// Open update container modal
|
||||
$scope.openUpdateModal = function(container) {
|
||||
$scope.updateContainerName = container.name;
|
||||
$scope.currentImage = container.image;
|
||||
$scope.newImage = '';
|
||||
$scope.newTag = 'latest';
|
||||
$('#updateContainer').modal('show');
|
||||
};
|
||||
|
||||
// Perform container update
|
||||
$scope.performUpdate = function() {
|
||||
if (!$scope.newImage && !$scope.newTag) {
|
||||
new PNotify({
|
||||
title: 'Missing Information',
|
||||
text: 'Please enter a new image name or tag',
|
||||
type: 'error'
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// If no new image specified, use current image with new tag
|
||||
var imageToUse = $scope.newImage || $scope.currentImage.split(':')[0];
|
||||
var tagToUse = $scope.newTag || 'latest';
|
||||
|
||||
var data = {
|
||||
containerName: $scope.updateContainerName,
|
||||
newImage: imageToUse,
|
||||
newTag: tagToUse
|
||||
};
|
||||
|
||||
var url = '/docker/updateContainer';
|
||||
var config = {
|
||||
headers: {
|
||||
'X-CSRFToken': getCookie('csrftoken')
|
||||
}
|
||||
};
|
||||
|
||||
// Show loading
|
||||
$('#updateContainer').modal('hide');
|
||||
new PNotify({
|
||||
title: 'Updating Container',
|
||||
text: 'Please wait while the container is being updated...',
|
||||
type: 'info',
|
||||
hide: false
|
||||
});
|
||||
|
||||
$http.post(url, data, config).then(function(response) {
|
||||
if (response.data.updateContainerStatus === 1) {
|
||||
new PNotify({
|
||||
title: 'Container Updated Successfully',
|
||||
text: response.data.message || 'Container has been updated successfully',
|
||||
type: 'success'
|
||||
});
|
||||
// Reload containers list
|
||||
$scope.loadContainers();
|
||||
} else {
|
||||
new PNotify({
|
||||
title: 'Update Failed',
|
||||
text: response.data.error_message || 'Failed to update container',
|
||||
type: 'error'
|
||||
});
|
||||
}
|
||||
}, function(error) {
|
||||
new PNotify({
|
||||
title: 'Update Failed',
|
||||
text: 'Could not connect to server',
|
||||
type: 'error'
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
// Container actions
|
||||
$scope.startContainer = function(containerName) {
|
||||
var data = { containerName: containerName };
|
||||
var url = '/docker/startContainer';
|
||||
var config = {
|
||||
headers: {
|
||||
'X-CSRFToken': getCookie('csrftoken')
|
||||
}
|
||||
};
|
||||
|
||||
$http.post(url, data, config).then(function(response) {
|
||||
if (response.data.startContainerStatus === 1) {
|
||||
new PNotify({
|
||||
title: 'Container Started',
|
||||
text: 'Container has been started successfully',
|
||||
type: 'success'
|
||||
});
|
||||
$scope.loadContainers();
|
||||
} else {
|
||||
new PNotify({
|
||||
title: 'Start Failed',
|
||||
text: response.data.error_message || 'Failed to start container',
|
||||
type: 'error'
|
||||
});
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
$scope.stopContainer = function(containerName) {
|
||||
var data = { containerName: containerName };
|
||||
var url = '/docker/stopContainer';
|
||||
var config = {
|
||||
headers: {
|
||||
'X-CSRFToken': getCookie('csrftoken')
|
||||
}
|
||||
};
|
||||
|
||||
$http.post(url, data, config).then(function(response) {
|
||||
if (response.data.stopContainerStatus === 1) {
|
||||
new PNotify({
|
||||
title: 'Container Stopped',
|
||||
text: 'Container has been stopped successfully',
|
||||
type: 'success'
|
||||
});
|
||||
$scope.loadContainers();
|
||||
} else {
|
||||
new PNotify({
|
||||
title: 'Stop Failed',
|
||||
text: response.data.error_message || 'Failed to stop container',
|
||||
type: 'error'
|
||||
});
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
$scope.restartContainer = function(containerName) {
|
||||
var data = { containerName: containerName };
|
||||
var url = '/docker/restartContainer';
|
||||
var config = {
|
||||
headers: {
|
||||
'X-CSRFToken': getCookie('csrftoken')
|
||||
}
|
||||
};
|
||||
|
||||
$http.post(url, data, config).then(function(response) {
|
||||
if (response.data.restartContainerStatus === 1) {
|
||||
new PNotify({
|
||||
title: 'Container Restarted',
|
||||
text: 'Container has been restarted successfully',
|
||||
type: 'success'
|
||||
});
|
||||
$scope.loadContainers();
|
||||
} else {
|
||||
new PNotify({
|
||||
title: 'Restart Failed',
|
||||
text: response.data.error_message || 'Failed to restart container',
|
||||
type: 'error'
|
||||
});
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
$scope.deleteContainerWithData = function(containerName) {
|
||||
if (confirm('Are you sure you want to delete this container and all its data? This action cannot be undone.')) {
|
||||
var data = { containerName: containerName };
|
||||
var url = '/docker/deleteContainerWithData';
|
||||
var config = {
|
||||
headers: {
|
||||
'X-CSRFToken': getCookie('csrftoken')
|
||||
}
|
||||
};
|
||||
|
||||
$http.post(url, data, config).then(function(response) {
|
||||
if (response.data.deleteContainerWithDataStatus === 1) {
|
||||
new PNotify({
|
||||
title: 'Container Deleted',
|
||||
text: 'Container and all data have been deleted successfully',
|
||||
type: 'success'
|
||||
});
|
||||
$scope.loadContainers();
|
||||
} else {
|
||||
new PNotify({
|
||||
title: 'Delete Failed',
|
||||
text: response.data.error_message || 'Failed to delete container',
|
||||
type: 'error'
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
$scope.deleteContainerKeepData = function(containerName) {
|
||||
if (confirm('Are you sure you want to delete this container but keep the data? The container will be removed but volumes will be preserved.')) {
|
||||
var data = { containerName: containerName };
|
||||
var url = '/docker/deleteContainerKeepData';
|
||||
var config = {
|
||||
headers: {
|
||||
'X-CSRFToken': getCookie('csrftoken')
|
||||
}
|
||||
};
|
||||
|
||||
$http.post(url, data, config).then(function(response) {
|
||||
if (response.data.deleteContainerKeepDataStatus === 1) {
|
||||
new PNotify({
|
||||
title: 'Container Deleted',
|
||||
text: 'Container has been deleted but data has been preserved',
|
||||
type: 'success'
|
||||
});
|
||||
$scope.loadContainers();
|
||||
} else {
|
||||
new PNotify({
|
||||
title: 'Delete Failed',
|
||||
text: response.data.error_message || 'Failed to delete container',
|
||||
type: 'error'
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
});
|
||||
@@ -433,7 +433,7 @@
|
||||
<i class="fas fa-search"></i>
|
||||
{% trans "Search & Manage Images" %}
|
||||
</a>
|
||||
<a href="{% url 'listContainers' %}" class="btn-secondary">
|
||||
<a href="{% url 'listContainersPage' %}" class="btn-secondary">
|
||||
<i class="fas fa-cube"></i>
|
||||
{% trans "View Containers" %}
|
||||
</a>
|
||||
|
||||
@@ -26,7 +26,7 @@
|
||||
<div class="example-box-wrapper">
|
||||
<div class="row">
|
||||
<div class="col-md-4">
|
||||
<a href="{% url 'listContainers' %}" title="{% trans 'Manage Containers' %}" class="tile-box tile-box-shortcut btn-primary">
|
||||
<a href="{% url 'listContainersPage' %}" title="{% trans 'Manage Containers' %}" class="tile-box tile-box-shortcut btn-primary">
|
||||
<div class="tile-header">
|
||||
{% trans "Manage Containers" %}
|
||||
</div>
|
||||
|
||||
@@ -674,14 +674,37 @@
|
||||
<strong>{% trans "Error:" %}</strong> {$ errorMessage $}
|
||||
</div>
|
||||
|
||||
<div class="pagination" ng-if="pagination">
|
||||
<ul>
|
||||
{% for items in pagination %}
|
||||
<li ng-click="getFurtherContainersFromDB({{ forloop.counter }})">
|
||||
<a href="">{{ forloop.counter }}</a>
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
<!-- Pagination Controls (ACTIVITY BOARD-style: Go to page) -->
|
||||
<div ng-if="pagination && pagination.containers && (ContainerList.length > 0 || totalCount > 0)" class="pagination-wrapper" style="margin-top: 20px; padding: 15px; background: #f8f9ff; border-radius: 8px; display: flex; flex-wrap: wrap; align-items: center; justify-content: space-between; gap: 10px;">
|
||||
<div style="display: flex; align-items: center; gap: 8px; flex-wrap: wrap;">
|
||||
<button ng-click="prevPage('containers')" ng-disabled="!pagination.containers || (pagination.containers.currentPage || 1) === 1"
|
||||
class="pagination-btn" style="padding: 6px 12px; border: 1px solid #e8e9ff; background: white; border-radius: 6px; cursor: pointer; font-size: 12px; color: #2f3640;">
|
||||
<i class="fas fa-chevron-left"></i> {% trans "Prev" %}
|
||||
</button>
|
||||
<span style="font-size: 12px; color: #64748b; margin: 0 8px;">
|
||||
{% trans "Page" %} {$ (pagination.containers.currentPage || 1) $} {% trans "of" %} {$ (getTotalPages('containers') || 1) $}
|
||||
</span>
|
||||
<button ng-click="nextPage('containers')" ng-disabled="!pagination.containers || !getTotalPages || (pagination.containers.currentPage || 1) >= (getTotalPages('containers') || 1)"
|
||||
class="pagination-btn" style="padding: 6px 12px; border: 1px solid #e8e9ff; background: white; border-radius: 6px; cursor: pointer; font-size: 12px; color: #2f3640;">
|
||||
{% trans "Next" %} <i class="fas fa-chevron-right"></i>
|
||||
</button>
|
||||
</div>
|
||||
<div style="display: flex; align-items: center; gap: 8px; flex-wrap: wrap;">
|
||||
<label style="font-size: 12px; color: #64748b; margin: 0;">{% trans "Go to page:" %}</label>
|
||||
<input type="number" min="1" max="{$ (getTotalPages('containers') || 1) $}"
|
||||
ng-model="gotoPageInput.containers"
|
||||
ng-keyup="$event.keyCode === 13 && goToPage('containers', gotoPageInput.containers)"
|
||||
style="width: 60px; padding: 6px; border: 1px solid #e8e9ff; border-radius: 6px; font-size: 12px; text-align: center;">
|
||||
<button ng-click="goToPage('containers', gotoPageInput.containers)"
|
||||
class="pagination-btn" style="padding: 6px 12px; border: 1px solid #5b5fcf; background: #5b5fcf; color: white; border-radius: 6px; cursor: pointer; font-size: 12px;">
|
||||
{% trans "Go" %}
|
||||
</button>
|
||||
</div>
|
||||
<div style="font-size: 12px; color: #64748b; margin: 0;" ng-if="totalCount > 0">
|
||||
<span ng-if="pagination && pagination.containers">
|
||||
{% trans "Showing" %} {$ ((pagination.containers.currentPage || 1) - 1) * (pagination.containers.itemsPerPage || 10) + 1 $}-{$ Math.min((pagination.containers.currentPage || 1) * (pagination.containers.itemsPerPage || 10), totalCount) $} {% trans "of" %} {$ totalCount $} {% trans "entries" %}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -949,5 +972,5 @@
|
||||
{% endblock %}
|
||||
|
||||
{% block footer_scripts %}
|
||||
<script src="{% static 'dockerManager/dockerManager.js' %}"></script>
|
||||
<script src="{% static 'dockerManager/dockerManager.js' %}?v=4"></script>
|
||||
{% endblock %}
|
||||
|
||||
@@ -9,6 +9,7 @@ urlpatterns = [
|
||||
re_path(r'^getTags$', views.getTags, name='getTags'),
|
||||
re_path(r'^runContainer', views.runContainer, name='runContainer'),
|
||||
re_path(r'^submitContainerCreation$', views.submitContainerCreation, name='submitContainerCreation'),
|
||||
re_path(r'^containers$', views.listContainersPage, name='listContainersPage'),
|
||||
re_path(r'^listContainers$', views.listContainers, name='listContainers'),
|
||||
re_path(r'^getContainerList$', views.getContainerList, name='getContainerList'),
|
||||
re_path(r'^getContainerLogs$', views.getContainerLogs, name='getContainerLogs'),
|
||||
@@ -33,7 +34,6 @@ urlpatterns = [
|
||||
re_path(r'^updateContainerPorts$', views.updateContainerPorts, name='updateContainerPorts'),
|
||||
re_path(r'^manageNetworks$', views.manageNetworks, name='manageNetworks'),
|
||||
re_path(r'^updateContainer$', views.updateContainer, name='updateContainer'),
|
||||
re_path(r'^listContainers$', views.listContainers, name='listContainers'),
|
||||
re_path(r'^deleteContainerWithData$', views.deleteContainerWithData, name='deleteContainerWithData'),
|
||||
re_path(r'^deleteContainerKeepData$', views.deleteContainerKeepData, name='deleteContainerKeepData'),
|
||||
re_path(r'^recreateContainer$', views.recreateContainer, name='recreateContainer'),
|
||||
|
||||
@@ -179,14 +179,40 @@ def runContainer(request):
|
||||
return redirect(loadLoginPage)
|
||||
|
||||
@preDockerRun
|
||||
def listContainers(request):
|
||||
def listContainersPage(request):
|
||||
"""
|
||||
GET /docker/containers: Render HTML page only. Separate URL avoids
|
||||
cache/proxy ever serving JSON (listContainers used to return JSON).
|
||||
"""
|
||||
try:
|
||||
userID = request.session['userID']
|
||||
cm = ContainerManager()
|
||||
return cm.listContainers(request, userID)
|
||||
resp = cm.listContainers(request, userID)
|
||||
resp['Cache-Control'] = 'no-store, no-cache, must-revalidate, max-age=0'
|
||||
resp['Pragma'] = 'no-cache'
|
||||
resp['Expires'] = '0'
|
||||
return resp
|
||||
except KeyError:
|
||||
return redirect(loadLoginPage)
|
||||
|
||||
|
||||
@preDockerRun
|
||||
def listContainers(request):
|
||||
"""
|
||||
GET: Redirect to /docker/containers (HTML). POST: 405.
|
||||
listContainers URL historically returned JSON; caches may serve stale JSON.
|
||||
Use /docker/containers for the page to avoid that.
|
||||
"""
|
||||
try:
|
||||
request.session['userID'] # ensure logged in
|
||||
except KeyError:
|
||||
return redirect(loadLoginPage)
|
||||
|
||||
if request.method != 'GET':
|
||||
return HttpResponse('Method Not Allowed', status=405)
|
||||
from django.urls import reverse
|
||||
return redirect(reverse('listContainersPage'))
|
||||
|
||||
@preDockerRun
|
||||
def getContainerLogs(request):
|
||||
try:
|
||||
@@ -746,27 +772,6 @@ def getContainerEnv(request):
|
||||
except KeyError:
|
||||
return redirect(loadLoginPage)
|
||||
|
||||
@preDockerRun
|
||||
def listContainers(request):
|
||||
"""
|
||||
Get list of all Docker containers
|
||||
"""
|
||||
try:
|
||||
userID = request.session['userID']
|
||||
currentACL = ACLManager.loadedACL(userID)
|
||||
|
||||
if currentACL['admin'] == 1:
|
||||
pass
|
||||
else:
|
||||
return ACLManager.loadErrorJson()
|
||||
|
||||
cm = ContainerManager()
|
||||
coreResult = cm.listContainers(userID)
|
||||
|
||||
return coreResult
|
||||
except KeyError:
|
||||
return redirect(loadLoginPage)
|
||||
|
||||
@preDockerRun
|
||||
def getDockerNetworks(request):
|
||||
"""
|
||||
|
||||
496
paypalPremiumPlugin/views.py
Normal file
496
paypalPremiumPlugin/views.py
Normal file
@@ -0,0 +1,496 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
PayPal Premium Plugin Views - Enhanced Security Version
|
||||
This version uses remote server verification with multiple security layers
|
||||
SECURITY: All PayPal verification happens on YOUR server, not user's server
|
||||
"""
|
||||
|
||||
from django.shortcuts import render, redirect
|
||||
from django.http import JsonResponse
|
||||
from plogical.mailUtilities import mailUtilities
|
||||
from plogical.httpProc import httpProc
|
||||
from plogical.CyberCPLogFileWriter import CyberCPLogFileWriter as logging
|
||||
from functools import wraps
|
||||
import sys
|
||||
import os
|
||||
import urllib.request
|
||||
import urllib.error
|
||||
import json
|
||||
import hashlib
|
||||
import socket
|
||||
import platform
|
||||
import subprocess
|
||||
import time
|
||||
import uuid
|
||||
|
||||
# Remote verification server (YOUR server, not user's server)
|
||||
REMOTE_VERIFICATION_URL = 'https://api.newstargeted.com/api/verify-paypal-payment'
|
||||
PLUGIN_NAME = 'paypalPremiumPlugin' # PayPal Premium Plugin Example
|
||||
PLUGIN_VERSION = '1.0.0'
|
||||
|
||||
# PayPal configuration
|
||||
PAYPAL_ME_URL = 'https://paypal.me/KimBS?locale.x=en_US&country.x=NO'
|
||||
PAYPAL_PAYMENT_LINK = '' # Can be set to a PayPal Payment Link URL
|
||||
|
||||
# Security configuration
|
||||
CACHE_FILE = '/tmp/.paypalPremiumPlugin_license_cache'
|
||||
CACHE_DURATION = 3600 # 1 hour
|
||||
|
||||
# File integrity hashes (generated after plugin finalization)
|
||||
# To regenerate: python3 -c "import hashlib; print(hashlib.sha256(open('views.py', 'rb').read()).hexdigest())"
|
||||
PLUGIN_FILE_HASHES = {
|
||||
'views.py': '4899d70dde220b38d691a5cefdc4fd77b6d3e250ac1c7e12fa280d6f4ad31eb1', # Updated with security features
|
||||
'urls.py': '92433d401c358cd33ffd1926881920fd1867bb6d7dad1c3c2ed1e7d3b0abc2c6',
|
||||
}
|
||||
|
||||
def get_server_fingerprint():
|
||||
"""
|
||||
Generate unique server fingerprint
|
||||
Ties license to specific server hardware/configuration
|
||||
"""
|
||||
fingerprint_data = []
|
||||
|
||||
try:
|
||||
# Server hostname
|
||||
fingerprint_data.append(socket.gethostname())
|
||||
|
||||
# Primary IP
|
||||
fingerprint_data.append(socket.gethostbyname(socket.gethostname()))
|
||||
|
||||
# System information
|
||||
fingerprint_data.append(platform.node())
|
||||
fingerprint_data.append(platform.machine())
|
||||
fingerprint_data.append(platform.processor())
|
||||
|
||||
# MAC address
|
||||
fingerprint_data.append(str(uuid.getnode()))
|
||||
|
||||
# Disk information (if available)
|
||||
try:
|
||||
result = subprocess.run(['df', '-h', '/'], capture_output=True, text=True, timeout=2)
|
||||
fingerprint_data.append(result.stdout[:100])
|
||||
except:
|
||||
pass
|
||||
|
||||
# Create hash
|
||||
fingerprint_string = '|'.join(str(x) for x in fingerprint_data)
|
||||
return hashlib.sha256(fingerprint_string.encode()).hexdigest()
|
||||
except Exception as e:
|
||||
# Fallback fingerprint
|
||||
return hashlib.sha256(f"{socket.gethostname()}|{platform.node()}".encode()).hexdigest()
|
||||
|
||||
def verify_code_integrity():
|
||||
"""
|
||||
Verify plugin files haven't been tampered with
|
||||
Returns: (is_valid, error_message)
|
||||
"""
|
||||
plugin_dir = os.path.dirname(os.path.abspath(__file__))
|
||||
|
||||
for filename, expected_hash in PLUGIN_FILE_HASHES.items():
|
||||
if not expected_hash:
|
||||
continue # Skip if hash not set
|
||||
|
||||
filepath = os.path.join(plugin_dir, filename)
|
||||
if os.path.exists(filepath):
|
||||
try:
|
||||
with open(filepath, 'rb') as f:
|
||||
file_content = f.read()
|
||||
file_hash = hashlib.sha256(file_content).hexdigest()
|
||||
|
||||
if file_hash != expected_hash:
|
||||
return False, f"File {filename} has been modified (integrity check failed)"
|
||||
except Exception as e:
|
||||
return False, f"Error checking {filename}: {str(e)}"
|
||||
|
||||
return True, None
|
||||
|
||||
def get_cached_verification():
|
||||
"""Get cached verification result"""
|
||||
if os.path.exists(CACHE_FILE):
|
||||
try:
|
||||
with open(CACHE_FILE, 'r') as f:
|
||||
cache_data = json.load(f)
|
||||
cache_time = cache_data.get('timestamp', 0)
|
||||
server_fp = cache_data.get('server_fingerprint')
|
||||
|
||||
# Verify server fingerprint matches
|
||||
current_fp = get_server_fingerprint()
|
||||
if server_fp != current_fp:
|
||||
return None # Server changed, invalidate cache
|
||||
|
||||
# Check if cache is still valid
|
||||
if time.time() - cache_time < CACHE_DURATION:
|
||||
return cache_data.get('has_access', False)
|
||||
except:
|
||||
pass
|
||||
return None
|
||||
|
||||
def cache_verification_result(has_access, server_fp):
|
||||
"""Cache verification result"""
|
||||
try:
|
||||
with open(CACHE_FILE, 'w') as f:
|
||||
json.dump({
|
||||
'has_access': has_access,
|
||||
'server_fingerprint': server_fp,
|
||||
'timestamp': time.time()
|
||||
}, f)
|
||||
os.chmod(CACHE_FILE, 0o600) # Secure permissions (owner read/write only)
|
||||
except Exception as e:
|
||||
pass # Silently fail caching
|
||||
|
||||
def cyberpanel_login_required(view_func):
|
||||
"""
|
||||
Custom decorator that checks for CyberPanel session userID
|
||||
"""
|
||||
@wraps(view_func)
|
||||
def _wrapped_view(request, *args, **kwargs):
|
||||
try:
|
||||
userID = request.session['userID']
|
||||
# User is authenticated via CyberPanel session
|
||||
return view_func(request, *args, **kwargs)
|
||||
except KeyError:
|
||||
# Not logged in, redirect to login
|
||||
from loginSystem.views import loadLoginPage
|
||||
return redirect(loadLoginPage)
|
||||
return _wrapped_view
|
||||
|
||||
def secure_verification_required(view_func):
|
||||
"""
|
||||
Enhanced decorator with multiple security checks
|
||||
"""
|
||||
@wraps(view_func)
|
||||
def _wrapped_view(request, *args, **kwargs):
|
||||
# Check 1: Login required
|
||||
try:
|
||||
userID = request.session['userID']
|
||||
except KeyError:
|
||||
from loginSystem.views import loadLoginPage
|
||||
return redirect(loadLoginPage)
|
||||
|
||||
# Check 2: Code integrity
|
||||
is_valid, integrity_error = verify_code_integrity()
|
||||
if not is_valid:
|
||||
# Log security violation
|
||||
logging.writeToFile(f"SECURITY VIOLATION: {integrity_error} - User: {request.session.get('userID')}")
|
||||
|
||||
# Show error (don't reveal details)
|
||||
context = {
|
||||
'error': 'Plugin integrity check failed. Please reinstall the plugin.',
|
||||
'security_violation': True
|
||||
}
|
||||
proc = httpProc(request, 'paypalPremiumPlugin/subscription_required.html', context, 'admin')
|
||||
return proc.render()
|
||||
|
||||
# Check 3: Remote verification
|
||||
user_email = getattr(request.user, 'email', None) if hasattr(request, 'user') and request.user else None
|
||||
if not user_email:
|
||||
user_email = request.session.get('email', '') or getattr(request.user, 'username', '')
|
||||
|
||||
domain = request.get_host()
|
||||
user_ip = request.META.get('REMOTE_ADDR', '')
|
||||
|
||||
verification_result = check_remote_payment_secure(
|
||||
user_email,
|
||||
user_ip,
|
||||
domain
|
||||
)
|
||||
|
||||
if not verification_result.get('has_access', False):
|
||||
# Show payment required page
|
||||
context = {
|
||||
'plugin_name': 'PayPal Premium Plugin Example',
|
||||
'is_paid': True,
|
||||
'paypal_me_url': verification_result.get('paypal_me_url', PAYPAL_ME_URL),
|
||||
'paypal_payment_link': verification_result.get('paypal_payment_link', PAYPAL_PAYMENT_LINK),
|
||||
'message': verification_result.get('message', 'PayPal payment required'),
|
||||
'error': verification_result.get('error')
|
||||
}
|
||||
proc = httpProc(request, 'paypalPremiumPlugin/subscription_required.html', context, 'admin')
|
||||
return proc.render()
|
||||
|
||||
# All checks passed - proceed
|
||||
return view_func(request, *args, **kwargs)
|
||||
|
||||
return _wrapped_view
|
||||
|
||||
def remote_verification_required(view_func):
|
||||
"""
|
||||
Decorator that checks PayPal payment via remote server
|
||||
No secrets stored in plugin - all verification happens on your server
|
||||
"""
|
||||
@wraps(view_func)
|
||||
def _wrapped_view(request, *args, **kwargs):
|
||||
# First check login
|
||||
try:
|
||||
userID = request.session['userID']
|
||||
except KeyError:
|
||||
from loginSystem.views import loadLoginPage
|
||||
return redirect(loadLoginPage)
|
||||
|
||||
# Get user email
|
||||
user_email = getattr(request.user, 'email', None) if hasattr(request, 'user') and request.user else None
|
||||
if not user_email:
|
||||
# Try to get from session or username
|
||||
user_email = request.session.get('email', '') or getattr(request.user, 'username', '')
|
||||
|
||||
# Check payment via remote server
|
||||
verification_result = check_remote_payment_secure(
|
||||
user_email,
|
||||
request.META.get('REMOTE_ADDR', ''),
|
||||
request.get_host()
|
||||
)
|
||||
|
||||
if not verification_result.get('has_access', False):
|
||||
# User doesn't have payment - show payment required page
|
||||
context = {
|
||||
'plugin_name': 'PayPal Premium Plugin Example',
|
||||
'is_paid': True,
|
||||
'paypal_me_url': verification_result.get('paypal_me_url', PAYPAL_ME_URL),
|
||||
'paypal_payment_link': verification_result.get('paypal_payment_link', PAYPAL_PAYMENT_LINK),
|
||||
'message': verification_result.get('message', 'PayPal payment required'),
|
||||
'error': verification_result.get('error')
|
||||
}
|
||||
proc = httpProc(request, 'paypalPremiumPlugin/subscription_required.html', context, 'admin')
|
||||
return proc.render()
|
||||
|
||||
# User has access - proceed with view
|
||||
return view_func(request, *args, **kwargs)
|
||||
|
||||
return _wrapped_view
|
||||
|
||||
def check_remote_payment_secure(user_email, user_ip='', domain=''):
|
||||
"""
|
||||
Enhanced remote payment verification with multiple security layers
|
||||
|
||||
Args:
|
||||
user_email: User's email address
|
||||
user_ip: User's IP address (for logging/security)
|
||||
domain: Current domain (for domain binding)
|
||||
|
||||
Returns:
|
||||
dict: {
|
||||
'has_access': bool,
|
||||
'paypal_me_url': str,
|
||||
'paypal_payment_link': str,
|
||||
'message': str,
|
||||
'error': str or None
|
||||
}
|
||||
"""
|
||||
# Layer 1: Code integrity check
|
||||
is_valid, integrity_error = verify_code_integrity()
|
||||
if not is_valid:
|
||||
return {
|
||||
'has_access': False,
|
||||
'paypal_me_url': PAYPAL_ME_URL,
|
||||
'paypal_payment_link': PAYPAL_PAYMENT_LINK,
|
||||
'message': 'Plugin integrity check failed',
|
||||
'error': integrity_error,
|
||||
'security_violation': True
|
||||
}
|
||||
|
||||
# Layer 2: Check cache
|
||||
cached_result = get_cached_verification()
|
||||
if cached_result is not None:
|
||||
return {
|
||||
'has_access': cached_result,
|
||||
'paypal_me_url': PAYPAL_ME_URL,
|
||||
'paypal_payment_link': PAYPAL_PAYMENT_LINK,
|
||||
'message': 'Access granted' if cached_result else 'PayPal payment required'
|
||||
}
|
||||
|
||||
# Layer 3: Server fingerprinting
|
||||
server_fp = get_server_fingerprint()
|
||||
|
||||
# Layer 4: Prepare secure request
|
||||
request_data = {
|
||||
'user_email': user_email,
|
||||
'plugin_name': PLUGIN_NAME,
|
||||
'plugin_version': PLUGIN_VERSION,
|
||||
'server_fingerprint': server_fp,
|
||||
'domain': domain,
|
||||
'user_ip': user_ip,
|
||||
'timestamp': int(time.time())
|
||||
}
|
||||
|
||||
try:
|
||||
# Make request to remote verification server
|
||||
req = urllib.request.Request(
|
||||
REMOTE_VERIFICATION_URL,
|
||||
data=json.dumps(request_data).encode('utf-8'),
|
||||
headers={
|
||||
'Content-Type': 'application/json',
|
||||
'User-Agent': f'CyberPanel-Plugin/{PLUGIN_VERSION}',
|
||||
'X-Plugin-Name': PLUGIN_NAME,
|
||||
'X-Timestamp': str(request_data['timestamp'])
|
||||
}
|
||||
)
|
||||
|
||||
# Send request with timeout
|
||||
try:
|
||||
with urllib.request.urlopen(req, timeout=10) as response:
|
||||
response_data = json.loads(response.read().decode('utf-8'))
|
||||
|
||||
if response_data.get('success', False):
|
||||
has_access = response_data.get('has_access', False)
|
||||
|
||||
# Cache result
|
||||
cache_verification_result(has_access, server_fp)
|
||||
|
||||
return {
|
||||
'has_access': has_access,
|
||||
'paypal_me_url': response_data.get('paypal_me_url', PAYPAL_ME_URL),
|
||||
'paypal_payment_link': response_data.get('paypal_payment_link', PAYPAL_PAYMENT_LINK),
|
||||
'message': response_data.get('message', 'Access granted' if has_access else 'PayPal payment required'),
|
||||
'error': None
|
||||
}
|
||||
else:
|
||||
return {
|
||||
'has_access': False,
|
||||
'paypal_me_url': response_data.get('paypal_me_url', PAYPAL_ME_URL),
|
||||
'paypal_payment_link': response_data.get('paypal_payment_link', PAYPAL_PAYMENT_LINK),
|
||||
'message': response_data.get('message', 'PayPal payment required'),
|
||||
'error': response_data.get('error')
|
||||
}
|
||||
except urllib.error.HTTPError as e:
|
||||
# Server returned error
|
||||
error_body = e.read().decode('utf-8') if e.fp else 'Unknown error'
|
||||
return {
|
||||
'has_access': False,
|
||||
'paypal_me_url': PAYPAL_ME_URL,
|
||||
'paypal_payment_link': PAYPAL_PAYMENT_LINK,
|
||||
'message': 'Unable to verify payment. Please try again later.',
|
||||
'error': f'HTTP {e.code}: {error_body}'
|
||||
}
|
||||
except urllib.error.URLError as e:
|
||||
# Network error
|
||||
return {
|
||||
'has_access': False,
|
||||
'paypal_me_url': PAYPAL_ME_URL,
|
||||
'paypal_payment_link': PAYPAL_PAYMENT_LINK,
|
||||
'message': 'Unable to connect to verification server. Please check your internet connection.',
|
||||
'error': str(e.reason) if hasattr(e, 'reason') else str(e)
|
||||
}
|
||||
except Exception as e:
|
||||
# Other errors
|
||||
return {
|
||||
'has_access': False,
|
||||
'paypal_me_url': PAYPAL_ME_URL,
|
||||
'paypal_payment_link': PAYPAL_PAYMENT_LINK,
|
||||
'message': 'Verification error occurred. Please try again later.',
|
||||
'error': str(e)
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
logging.writeToFile(f"Error in remote payment check: {str(e)}")
|
||||
return {
|
||||
'has_access': False,
|
||||
'paypal_me_url': PAYPAL_ME_URL,
|
||||
'paypal_payment_link': PAYPAL_PAYMENT_LINK,
|
||||
'message': 'Verification error occurred. Please try again later.',
|
||||
'error': str(e)
|
||||
}
|
||||
|
||||
def check_remote_payment(user_email, user_ip=''):
|
||||
"""
|
||||
Legacy function for backward compatibility
|
||||
"""
|
||||
return check_remote_payment_secure(user_email, user_ip, '')
|
||||
|
||||
@cyberpanel_login_required
|
||||
def main_view(request):
|
||||
"""
|
||||
Main view for PayPal premium plugin
|
||||
Shows plugin information and features if paid, or payment required message if not
|
||||
"""
|
||||
mailUtilities.checkHome()
|
||||
|
||||
# Get user email for verification
|
||||
user_email = getattr(request.user, 'email', None) if hasattr(request, 'user') and request.user else None
|
||||
if not user_email:
|
||||
user_email = request.session.get('email', '') or getattr(request.user, 'username', '')
|
||||
|
||||
# Check payment status (but don't block access)
|
||||
verification_result = check_remote_payment_secure(
|
||||
user_email,
|
||||
request.META.get('REMOTE_ADDR', ''),
|
||||
request.get_host()
|
||||
)
|
||||
has_access = verification_result.get('has_access', False)
|
||||
|
||||
# Determine plugin status
|
||||
plugin_status = 'Active' if has_access else 'Payment Required'
|
||||
|
||||
context = {
|
||||
'plugin_name': 'PayPal Premium Plugin Example',
|
||||
'version': PLUGIN_VERSION,
|
||||
'status': plugin_status,
|
||||
'has_access': has_access,
|
||||
'description': 'This is an example paid plugin that requires PayPal payment.' if not has_access else 'This is an example paid plugin. You have access because payment has been verified!',
|
||||
'paypal_me_url': verification_result.get('paypal_me_url', PAYPAL_ME_URL),
|
||||
'paypal_payment_link': verification_result.get('paypal_payment_link', PAYPAL_PAYMENT_LINK),
|
||||
'features': [
|
||||
'Premium Feature 1',
|
||||
'Premium Feature 2',
|
||||
'Premium Feature 3',
|
||||
'Advanced Configuration',
|
||||
'Priority Support'
|
||||
] if has_access else []
|
||||
}
|
||||
|
||||
proc = httpProc(request, 'paypalPremiumPlugin/index.html', context, 'admin')
|
||||
return proc.render()
|
||||
|
||||
@cyberpanel_login_required
|
||||
def settings_view(request):
|
||||
"""
|
||||
Settings page for PayPal premium plugin
|
||||
Shows settings but disables them if user doesn't have PayPal payment
|
||||
"""
|
||||
mailUtilities.checkHome()
|
||||
|
||||
# Get user email for verification
|
||||
user_email = getattr(request.user, 'email', None) if hasattr(request, 'user') and request.user else None
|
||||
if not user_email:
|
||||
user_email = request.session.get('email', '') or getattr(request.user, 'username', '')
|
||||
|
||||
# Check payment status (but don't block access)
|
||||
verification_result = check_remote_payment_secure(
|
||||
user_email,
|
||||
request.META.get('REMOTE_ADDR', ''),
|
||||
request.get_host()
|
||||
)
|
||||
has_access = verification_result.get('has_access', False)
|
||||
|
||||
# Determine plugin status
|
||||
plugin_status = 'Active' if has_access else 'Payment Required'
|
||||
|
||||
context = {
|
||||
'plugin_name': 'PayPal Premium Plugin Example',
|
||||
'version': PLUGIN_VERSION,
|
||||
'plugin_status': plugin_status,
|
||||
'status': plugin_status, # Keep both for compatibility
|
||||
'description': 'Configure your premium plugin settings',
|
||||
'has_access': has_access,
|
||||
'paypal_me_url': verification_result.get('paypal_me_url', PAYPAL_ME_URL),
|
||||
'paypal_payment_link': verification_result.get('paypal_payment_link', PAYPAL_PAYMENT_LINK),
|
||||
'verification_message': verification_result.get('message', '')
|
||||
}
|
||||
|
||||
proc = httpProc(request, 'paypalPremiumPlugin/settings.html', context, 'admin')
|
||||
return proc.render()
|
||||
|
||||
@cyberpanel_login_required
|
||||
@secure_verification_required
|
||||
def api_status_view(request):
|
||||
"""
|
||||
API endpoint for plugin status
|
||||
Only accessible with PayPal payment (verified remotely with enhanced security)
|
||||
"""
|
||||
return JsonResponse({
|
||||
'plugin_name': 'PayPal Premium Plugin Example',
|
||||
'version': PLUGIN_VERSION,
|
||||
'status': 'active',
|
||||
'payment': 'verified',
|
||||
'description': 'Premium plugin is active and accessible',
|
||||
'verification_method': 'remote_secure'
|
||||
})
|
||||
BIN
pluginHolder/discordWebhooks.zip
Normal file
BIN
pluginHolder/discordWebhooks.zip
Normal file
Binary file not shown.
BIN
pluginHolder/fail2ban.zip
Normal file
BIN
pluginHolder/fail2ban.zip
Normal file
Binary file not shown.
245
pluginHolder/patreon_verifier.py
Normal file
245
pluginHolder/patreon_verifier.py
Normal file
@@ -0,0 +1,245 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
Patreon Verifier for CyberPanel Plugins
|
||||
Verifies Patreon membership status for paid plugins
|
||||
"""
|
||||
|
||||
import urllib.request
|
||||
import urllib.error
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
|
||||
# Patreon API configuration
|
||||
PATREON_API_BASE = 'https://www.patreon.com/api/oauth2/v2'
|
||||
PATREON_MEMBERSHIP_TIER = 'CyberPanel Paid Plugin' # The membership tier name to check
|
||||
|
||||
class PatreonVerifier:
|
||||
"""
|
||||
Verifies Patreon membership status for CyberPanel users
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
"""Initialize Patreon verifier"""
|
||||
# Try to import from Django settings first, then fallback to environment
|
||||
try:
|
||||
from django.conf import settings
|
||||
self.client_id = getattr(settings, 'PATREON_CLIENT_ID', os.environ.get('PATREON_CLIENT_ID', ''))
|
||||
self.client_secret = getattr(settings, 'PATREON_CLIENT_SECRET', os.environ.get('PATREON_CLIENT_SECRET', ''))
|
||||
self.creator_id = getattr(settings, 'PATREON_CREATOR_ID', os.environ.get('PATREON_CREATOR_ID', ''))
|
||||
self.membership_tier_id = getattr(settings, 'PATREON_MEMBERSHIP_TIER_ID', os.environ.get('PATREON_MEMBERSHIP_TIER_ID', '27789984'))
|
||||
self.creator_access_token = getattr(settings, 'PATREON_CREATOR_ACCESS_TOKEN', os.environ.get('PATREON_CREATOR_ACCESS_TOKEN', ''))
|
||||
except:
|
||||
# Fallback to environment variables only
|
||||
self.client_id = os.environ.get('PATREON_CLIENT_ID', '')
|
||||
self.client_secret = os.environ.get('PATREON_CLIENT_SECRET', '')
|
||||
self.creator_id = os.environ.get('PATREON_CREATOR_ID', '')
|
||||
self.membership_tier_id = os.environ.get('PATREON_MEMBERSHIP_TIER_ID', '27789984')
|
||||
self.creator_access_token = os.environ.get('PATREON_CREATOR_ACCESS_TOKEN', '')
|
||||
|
||||
# Cache for membership checks (to avoid excessive API calls)
|
||||
self.cache_file = '/home/cyberpanel/patreon_cache.json'
|
||||
self.cache_duration = 300 # Cache for 5 minutes
|
||||
|
||||
def get_user_patreon_token(self, user_email):
|
||||
"""
|
||||
Get stored Patreon access token for a user
|
||||
This should be stored when user authorizes via Patreon OAuth
|
||||
"""
|
||||
# In a real implementation, you'd store this in a database
|
||||
# For now, we'll check if there's a stored token file
|
||||
token_file = f'/home/cyberpanel/patreon_tokens/{user_email}.token'
|
||||
if os.path.exists(token_file):
|
||||
try:
|
||||
with open(token_file, 'r') as f:
|
||||
token_data = json.load(f)
|
||||
return token_data.get('access_token')
|
||||
except:
|
||||
return None
|
||||
return None
|
||||
|
||||
def check_membership_cached(self, user_email):
|
||||
"""
|
||||
Check membership with caching
|
||||
"""
|
||||
import time
|
||||
cache_data = self._load_cache()
|
||||
cache_key = f"membership_{user_email}"
|
||||
|
||||
if cache_key in cache_data:
|
||||
cached_result = cache_data[cache_key]
|
||||
if time.time() - cached_result.get('timestamp', 0) < self.cache_duration:
|
||||
return cached_result.get('has_membership', False)
|
||||
|
||||
# Check membership via API
|
||||
has_membership = self.check_membership(user_email)
|
||||
|
||||
# Update cache
|
||||
cache_data[cache_key] = {
|
||||
'has_membership': has_membership,
|
||||
'timestamp': time.time()
|
||||
}
|
||||
self._save_cache(cache_data)
|
||||
|
||||
return has_membership
|
||||
|
||||
def check_membership(self, user_email):
|
||||
"""
|
||||
Check if user has active Patreon membership for 'CyberPanel Paid Plugin'
|
||||
|
||||
Args:
|
||||
user_email: User's email address
|
||||
|
||||
Returns:
|
||||
bool: True if user has active membership, False otherwise
|
||||
"""
|
||||
access_token = self.get_user_patreon_token(user_email)
|
||||
|
||||
if not access_token:
|
||||
return False
|
||||
|
||||
try:
|
||||
# Get user's identity
|
||||
user_info = self._get_user_identity(access_token)
|
||||
if not user_info:
|
||||
return False
|
||||
|
||||
member_id = user_info.get('id')
|
||||
if not member_id:
|
||||
return False
|
||||
|
||||
# Get user's memberships
|
||||
memberships = self._get_memberships(access_token, member_id)
|
||||
if not memberships:
|
||||
return False
|
||||
|
||||
# Check if user has the required membership tier
|
||||
# First try to match by tier ID (more accurate)
|
||||
for membership in memberships:
|
||||
tier_id = membership.get('id', '')
|
||||
tier_name = membership.get('attributes', {}).get('title', '')
|
||||
|
||||
# Check by tier ID first (most accurate)
|
||||
if tier_id == self.membership_tier_id:
|
||||
# Check if membership is active
|
||||
status = membership.get('attributes', {}).get('patron_status', '')
|
||||
if status in ['active_patron', 'former_patron']:
|
||||
# Check if currently entitled
|
||||
entitled = membership.get('attributes', {}).get('currently_entitled_amount_cents', 0)
|
||||
if entitled > 0:
|
||||
return True
|
||||
|
||||
# Fallback: Check by tier name (for compatibility)
|
||||
if PATREON_MEMBERSHIP_TIER.lower() in tier_name.lower():
|
||||
# Check if membership is active
|
||||
status = membership.get('attributes', {}).get('patron_status', '')
|
||||
if status in ['active_patron', 'former_patron']:
|
||||
# Check if currently entitled
|
||||
entitled = membership.get('attributes', {}).get('currently_entitled_amount_cents', 0)
|
||||
if entitled > 0:
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
except Exception as e:
|
||||
import logging
|
||||
logging.writeToFile(f"Error checking Patreon membership for {user_email}: {str(e)}")
|
||||
return False
|
||||
|
||||
def _get_user_identity(self, access_token):
|
||||
"""
|
||||
Get user identity from Patreon API
|
||||
"""
|
||||
url = f"{PATREON_API_BASE}/identity"
|
||||
|
||||
try:
|
||||
req = urllib.request.Request(url)
|
||||
req.add_header('Authorization', f'Bearer {access_token}')
|
||||
|
||||
with urllib.request.urlopen(req) as response:
|
||||
data = json.loads(response.read().decode())
|
||||
return data.get('data', {})
|
||||
except Exception as e:
|
||||
import logging
|
||||
logging.writeToFile(f"Error getting Patreon identity: {str(e)}")
|
||||
return None
|
||||
|
||||
def _get_memberships(self, access_token, member_id):
|
||||
"""
|
||||
Get user's memberships from Patreon API
|
||||
"""
|
||||
url = f"{PATREON_API_BASE}/members/{member_id}?include=currently_entitled_tiers"
|
||||
|
||||
try:
|
||||
req = urllib.request.Request(url)
|
||||
req.add_header('Authorization', f'Bearer {access_token}')
|
||||
|
||||
with urllib.request.urlopen(req) as response:
|
||||
data = json.loads(response.read().decode())
|
||||
|
||||
# Parse included tiers
|
||||
memberships = []
|
||||
included = data.get('included', [])
|
||||
|
||||
for item in included:
|
||||
if item.get('type') == 'tier':
|
||||
# Include tier ID in the membership data
|
||||
tier_data = {
|
||||
'id': item.get('id', ''),
|
||||
'type': item.get('type', ''),
|
||||
'attributes': item.get('attributes', {})
|
||||
}
|
||||
memberships.append(tier_data)
|
||||
|
||||
return memberships
|
||||
except Exception as e:
|
||||
import logging
|
||||
logging.writeToFile(f"Error getting Patreon memberships: {str(e)}")
|
||||
return []
|
||||
|
||||
def _load_cache(self):
|
||||
"""Load cache from file"""
|
||||
if os.path.exists(self.cache_file):
|
||||
try:
|
||||
with open(self.cache_file, 'r') as f:
|
||||
return json.load(f)
|
||||
except:
|
||||
return {}
|
||||
return {}
|
||||
|
||||
def _save_cache(self, cache_data):
|
||||
"""Save cache to file"""
|
||||
try:
|
||||
os.makedirs(os.path.dirname(self.cache_file), exist_ok=True)
|
||||
with open(self.cache_file, 'w') as f:
|
||||
json.dump(cache_data, f)
|
||||
except Exception as e:
|
||||
import logging
|
||||
logging.writeToFile(f"Error saving Patreon cache: {str(e)}")
|
||||
|
||||
def verify_plugin_access(self, user_email, plugin_name):
|
||||
"""
|
||||
Verify if user can access a paid plugin
|
||||
|
||||
Args:
|
||||
user_email: User's email address
|
||||
plugin_name: Name of the plugin to check
|
||||
|
||||
Returns:
|
||||
dict: {
|
||||
'has_access': bool,
|
||||
'is_paid': bool,
|
||||
'message': str
|
||||
}
|
||||
"""
|
||||
# Check if plugin is paid (this will be checked from meta.xml)
|
||||
# For now, we'll assume the plugin system will pass this info
|
||||
|
||||
# Check membership
|
||||
has_membership = self.check_membership_cached(user_email)
|
||||
|
||||
return {
|
||||
'has_access': has_membership,
|
||||
'is_paid': True, # This will be determined by plugin metadata
|
||||
'message': 'Access granted' if has_membership else 'Patreon subscription required'
|
||||
}
|
||||
130
pluginHolder/plugin_access.py
Normal file
130
pluginHolder/plugin_access.py
Normal file
@@ -0,0 +1,130 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
Plugin Access Control
|
||||
Checks if user has access to paid plugins
|
||||
"""
|
||||
|
||||
from .patreon_verifier import PatreonVerifier
|
||||
import logging
|
||||
|
||||
def check_plugin_access(request, plugin_name, plugin_meta=None):
|
||||
"""
|
||||
Check if user has access to a plugin
|
||||
|
||||
Args:
|
||||
request: Django request object
|
||||
plugin_name: Name of the plugin
|
||||
plugin_meta: Plugin metadata dict (optional, will be loaded if not provided)
|
||||
|
||||
Returns:
|
||||
dict: {
|
||||
'has_access': bool,
|
||||
'is_paid': bool,
|
||||
'message': str,
|
||||
'patreon_url': str or None
|
||||
}
|
||||
"""
|
||||
# Default response for free plugins
|
||||
default_response = {
|
||||
'has_access': True,
|
||||
'is_paid': False,
|
||||
'message': 'Access granted',
|
||||
'patreon_url': None
|
||||
}
|
||||
|
||||
# If plugin_meta not provided, try to load it
|
||||
if plugin_meta is None:
|
||||
plugin_meta = _load_plugin_meta(plugin_name)
|
||||
|
||||
# Check if plugin is paid
|
||||
if not plugin_meta or not plugin_meta.get('is_paid', False):
|
||||
return default_response
|
||||
|
||||
# Plugin is paid - check Patreon membership
|
||||
if not request.user or not request.user.is_authenticated:
|
||||
return {
|
||||
'has_access': False,
|
||||
'is_paid': True,
|
||||
'message': 'Please log in to access this plugin',
|
||||
'patreon_url': plugin_meta.get('patreon_url')
|
||||
}
|
||||
|
||||
# Get user email
|
||||
user_email = getattr(request.user, 'email', None)
|
||||
if not user_email:
|
||||
# Try to get from username or other fields
|
||||
user_email = getattr(request.user, 'username', '')
|
||||
|
||||
if not user_email:
|
||||
return {
|
||||
'has_access': False,
|
||||
'is_paid': True,
|
||||
'message': 'Unable to verify user identity',
|
||||
'patreon_url': plugin_meta.get('patreon_url')
|
||||
}
|
||||
|
||||
# Check Patreon membership
|
||||
verifier = PatreonVerifier()
|
||||
has_membership = verifier.check_membership_cached(user_email)
|
||||
|
||||
if has_membership:
|
||||
return {
|
||||
'has_access': True,
|
||||
'is_paid': True,
|
||||
'message': 'Access granted',
|
||||
'patreon_url': None
|
||||
}
|
||||
else:
|
||||
return {
|
||||
'has_access': False,
|
||||
'is_paid': True,
|
||||
'message': f'This plugin requires a Patreon subscription to "{plugin_meta.get("patreon_tier", "CyberPanel Paid Plugin")}"',
|
||||
'patreon_url': plugin_meta.get('patreon_url', 'https://www.patreon.com/c/newstargeted/membership')
|
||||
}
|
||||
|
||||
def _load_plugin_meta(plugin_name):
|
||||
"""
|
||||
Load plugin metadata from meta.xml
|
||||
|
||||
Args:
|
||||
plugin_name: Name of the plugin
|
||||
|
||||
Returns:
|
||||
dict: Plugin metadata or None
|
||||
"""
|
||||
import os
|
||||
from xml.etree import ElementTree
|
||||
|
||||
installed_path = f'/usr/local/CyberCP/{plugin_name}/meta.xml'
|
||||
source_path = f'/home/cyberpanel/plugins/{plugin_name}/meta.xml'
|
||||
|
||||
meta_path = None
|
||||
if os.path.exists(installed_path):
|
||||
meta_path = installed_path
|
||||
elif os.path.exists(source_path):
|
||||
meta_path = source_path
|
||||
|
||||
if not meta_path:
|
||||
return None
|
||||
|
||||
try:
|
||||
tree = ElementTree.parse(meta_path)
|
||||
root = tree.getroot()
|
||||
|
||||
# Extract paid plugin information
|
||||
paid_elem = root.find('paid')
|
||||
patreon_tier_elem = root.find('patreon_tier')
|
||||
patreon_url_elem = root.find('patreon_url')
|
||||
|
||||
is_paid = False
|
||||
if paid_elem is not None and paid_elem.text and paid_elem.text.lower() == 'true':
|
||||
is_paid = True
|
||||
|
||||
return {
|
||||
'is_paid': is_paid,
|
||||
'patreon_tier': patreon_tier_elem.text if patreon_tier_elem is not None and patreon_tier_elem.text else 'CyberPanel Paid Plugin',
|
||||
'patreon_url': patreon_url_elem.text if patreon_url_elem is not None else 'https://www.patreon.com/c/newstargeted/membership'
|
||||
}
|
||||
except Exception as e:
|
||||
logging.writeToFile(f"Error loading plugin meta for {plugin_name}: {str(e)}")
|
||||
return None
|
||||
351
pluginHolder/templates/pluginHolder/plugin_help.html
Normal file
351
pluginHolder/templates/pluginHolder/plugin_help.html
Normal file
@@ -0,0 +1,351 @@
|
||||
{% extends "baseTemplate/index.html" %}
|
||||
{% load i18n %}
|
||||
{% block title %}{% trans "Plugin Help - " %}{{ plugin_name }} - CyberPanel{% endblock %}
|
||||
|
||||
{% block header_scripts %}
|
||||
<style>
|
||||
/* Plugin Help Page Styles */
|
||||
.plugin-help-wrapper {
|
||||
background: transparent;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.plugin-help-container {
|
||||
max-width: 900px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
/* Page Header */
|
||||
.help-page-header {
|
||||
background: var(--bg-primary, white);
|
||||
border-radius: 12px;
|
||||
padding: 30px;
|
||||
margin-bottom: 25px;
|
||||
box-shadow: var(--shadow-md, 0 2px 8px rgba(0,0,0,0.08));
|
||||
border: 1px solid var(--border-primary, #e8e9ff);
|
||||
border-left: 4px solid #5856d6;
|
||||
}
|
||||
|
||||
.help-page-header h1 {
|
||||
font-size: 32px;
|
||||
font-weight: 700;
|
||||
color: var(--text-primary, #2f3640);
|
||||
margin: 0 0 15px 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 15px;
|
||||
}
|
||||
|
||||
.help-page-header .help-icon {
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
background: #5856d6;
|
||||
border-radius: 12px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: white;
|
||||
font-size: 24px;
|
||||
box-shadow: 0 4px 12px rgba(88,86,214,0.3);
|
||||
}
|
||||
|
||||
.help-page-header .plugin-title {
|
||||
font-size: 24px;
|
||||
font-weight: 600;
|
||||
color: var(--text-primary, #2f3640);
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.help-page-header .plugin-meta {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 20px;
|
||||
margin-top: 15px;
|
||||
padding-top: 15px;
|
||||
border-top: 1px solid var(--border-primary, #e8e9ff);
|
||||
font-size: 14px;
|
||||
color: var(--text-secondary, #64748b);
|
||||
}
|
||||
|
||||
.help-page-header .plugin-meta-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.help-page-header .plugin-meta-item strong {
|
||||
color: var(--text-primary, #2f3640);
|
||||
}
|
||||
|
||||
/* Content Section */
|
||||
.help-content-section {
|
||||
background: var(--bg-primary, white);
|
||||
border-radius: 12px;
|
||||
padding: 35px;
|
||||
margin-bottom: 25px;
|
||||
box-shadow: var(--shadow-md, 0 2px 8px rgba(0,0,0,0.08));
|
||||
border: 1px solid var(--border-primary, #e8e9ff);
|
||||
}
|
||||
|
||||
.help-content {
|
||||
line-height: 1.8;
|
||||
color: var(--text-primary, #2f3640);
|
||||
font-size: 15px;
|
||||
}
|
||||
|
||||
/* Help content styling for HTML */
|
||||
.help-content h1,
|
||||
.help-content h2,
|
||||
.help-content h3,
|
||||
.help-content h4,
|
||||
.help-content h5,
|
||||
.help-content h6 {
|
||||
color: var(--text-primary, #2f3640);
|
||||
margin-top: 30px;
|
||||
margin-bottom: 15px;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.help-content h1 {
|
||||
font-size: 28px;
|
||||
border-bottom: 2px solid #5856d6;
|
||||
padding-bottom: 10px;
|
||||
}
|
||||
|
||||
.help-content h2 {
|
||||
font-size: 24px;
|
||||
color: #5856d6;
|
||||
}
|
||||
|
||||
.help-content h3 {
|
||||
font-size: 20px;
|
||||
}
|
||||
|
||||
.help-content p {
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
.help-content ul,
|
||||
.help-content ol {
|
||||
margin-left: 25px;
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
.help-content li {
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.help-content code {
|
||||
background: var(--bg-secondary, #f8f9ff);
|
||||
padding: 2px 6px;
|
||||
border-radius: 4px;
|
||||
font-family: 'Courier New', monospace;
|
||||
font-size: 14px;
|
||||
color: #5856d6;
|
||||
}
|
||||
|
||||
.help-content pre {
|
||||
background: var(--bg-secondary, #f8f9ff);
|
||||
padding: 15px;
|
||||
border-radius: 8px;
|
||||
overflow-x: auto;
|
||||
border-left: 4px solid #5856d6;
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
.help-content pre code {
|
||||
background: none;
|
||||
padding: 0;
|
||||
color: var(--text-primary, #2f3640);
|
||||
}
|
||||
|
||||
.help-content blockquote {
|
||||
border-left: 4px solid #5856d6;
|
||||
padding-left: 20px;
|
||||
margin-left: 0;
|
||||
margin-bottom: 15px;
|
||||
color: var(--text-secondary, #64748b);
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
.help-content table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
.help-content table th,
|
||||
.help-content table td {
|
||||
padding: 12px;
|
||||
border: 1px solid var(--border-primary, #e8e9ff);
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.help-content table th {
|
||||
background: var(--bg-secondary, #f8f9ff);
|
||||
font-weight: 600;
|
||||
color: var(--text-primary, #2f3640);
|
||||
}
|
||||
|
||||
.help-content a {
|
||||
color: #5856d6;
|
||||
text-decoration: none;
|
||||
border-bottom: 1px dotted #5856d6;
|
||||
}
|
||||
|
||||
.help-content a:hover {
|
||||
color: #4a48c4;
|
||||
border-bottom: 1px solid #4a48c4;
|
||||
}
|
||||
|
||||
.help-content img {
|
||||
max-width: 100%;
|
||||
height: auto;
|
||||
border-radius: 8px;
|
||||
margin: 15px 0;
|
||||
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
|
||||
}
|
||||
|
||||
/* Actions Section */
|
||||
.help-actions {
|
||||
background: var(--bg-primary, white);
|
||||
border-radius: 12px;
|
||||
padding: 25px;
|
||||
box-shadow: var(--shadow-md, 0 2px 8px rgba(0,0,0,0.08));
|
||||
border: 1px solid var(--border-primary, #e8e9ff);
|
||||
display: flex;
|
||||
gap: 15px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.help-action-btn {
|
||||
padding: 10px 20px;
|
||||
border-radius: 8px;
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
text-decoration: none;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.help-action-btn-primary {
|
||||
background: #5856d6;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.help-action-btn-primary:hover {
|
||||
background: #4a48c4;
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 4px 12px rgba(88,86,214,0.3);
|
||||
}
|
||||
|
||||
.help-action-btn-secondary {
|
||||
background: var(--bg-secondary, #f8f9ff);
|
||||
color: var(--text-secondary, #64748b);
|
||||
border: 1px solid var(--border-primary, #e8e9ff);
|
||||
}
|
||||
|
||||
.help-action-btn-secondary:hover {
|
||||
background: var(--bg-hover, #f0f1ff);
|
||||
color: #5856d6;
|
||||
border-color: #5856d6;
|
||||
}
|
||||
|
||||
/* Responsive */
|
||||
@media (max-width: 768px) {
|
||||
.plugin-help-wrapper {
|
||||
padding: 15px;
|
||||
}
|
||||
|
||||
.help-page-header {
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.help-page-header h1 {
|
||||
font-size: 24px;
|
||||
flex-direction: column;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.help-content-section {
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.help-actions {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.help-action-btn {
|
||||
width: 100%;
|
||||
justify-content: center;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
{% load static %}
|
||||
|
||||
<div class="plugin-help-wrapper">
|
||||
<div class="plugin-help-container">
|
||||
<!-- Page Header -->
|
||||
<div class="help-page-header">
|
||||
<h1>
|
||||
<div class="help-icon">
|
||||
<i class="fas fa-question-circle"></i>
|
||||
</div>
|
||||
{% trans "Module Help" %}
|
||||
</h1>
|
||||
<div class="plugin-title">{{ plugin_name }}</div>
|
||||
{% if plugin_description %}
|
||||
<p style="color: var(--text-secondary, #64748b); margin: 10px 0 0 0;">
|
||||
{{ plugin_description }}
|
||||
</p>
|
||||
{% endif %}
|
||||
<div class="plugin-meta">
|
||||
<div class="plugin-meta-item">
|
||||
<i class="fas fa-info-circle"></i>
|
||||
<strong>{% trans "Version:" %}</strong> {{ plugin_version }}
|
||||
</div>
|
||||
<div class="plugin-meta-item">
|
||||
<i class="fas fa-user"></i>
|
||||
<strong>{% trans "Author:" %}</strong> {{ plugin_author }}
|
||||
</div>
|
||||
{% if installed %}
|
||||
<div class="plugin-meta-item">
|
||||
<i class="fas fa-check-circle"></i>
|
||||
<strong>{% trans "Status:" %}</strong> <span style="color: #28a745;">{% trans "Installed" %}</span>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Help Content -->
|
||||
<div class="help-content-section">
|
||||
<div class="help-content">
|
||||
{{ help_content|safe }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Actions -->
|
||||
<div class="help-actions">
|
||||
{% if installed %}
|
||||
<a href="/plugins/{{ plugin_name_dir }}/" class="help-action-btn help-action-btn-primary">
|
||||
<i class="fas fa-cog"></i>
|
||||
{% trans "Plugin Settings" %}
|
||||
</a>
|
||||
{% endif %}
|
||||
<a href="/plugins/installed" class="help-action-btn help-action-btn-secondary">
|
||||
<i class="fas fa-arrow-left"></i>
|
||||
{% trans "Back to Plugins" %}
|
||||
</a>
|
||||
<a href="https://github.com/master3395/cyberpanel-plugins/tree/main/{{ plugin_name_dir }}" target="_blank" class="help-action-btn help-action-btn-secondary">
|
||||
<i class="fab fa-github"></i>
|
||||
{% trans "View on GitHub" %}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
93
pluginHolder/templates/pluginHolder/plugin_not_found.html
Normal file
93
pluginHolder/templates/pluginHolder/plugin_not_found.html
Normal file
@@ -0,0 +1,93 @@
|
||||
{% extends "baseTemplate/index.html" %}
|
||||
{% load i18n %}
|
||||
{% block title %}{% trans "Plugin Not Found - CyberPanel" %}{% endblock %}
|
||||
|
||||
{% block header_scripts %}
|
||||
<style>
|
||||
.error-wrapper {
|
||||
background: transparent;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.error-container {
|
||||
max-width: 600px;
|
||||
margin: 0 auto;
|
||||
text-align: center;
|
||||
padding: 60px 20px;
|
||||
}
|
||||
|
||||
.error-icon {
|
||||
font-size: 72px;
|
||||
color: #dc3545;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.error-title {
|
||||
font-size: 28px;
|
||||
font-weight: 700;
|
||||
color: var(--text-primary, #2f3640);
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
.error-message {
|
||||
font-size: 16px;
|
||||
color: var(--text-secondary, #64748b);
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
|
||||
.error-actions {
|
||||
display: flex;
|
||||
gap: 15px;
|
||||
justify-content: center;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.error-btn {
|
||||
padding: 10px 20px;
|
||||
border-radius: 8px;
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
text-decoration: none;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.error-btn-primary {
|
||||
background: #5856d6;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.error-btn-primary:hover {
|
||||
background: #4a48c4;
|
||||
}
|
||||
</style>
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="error-wrapper">
|
||||
<div class="error-container">
|
||||
<div class="error-icon">
|
||||
<i class="fas fa-exclamation-triangle"></i>
|
||||
</div>
|
||||
<h1 class="error-title">{% trans "Plugin Not Found" %}</h1>
|
||||
<p class="error-message">
|
||||
{% if plugin_name %}
|
||||
{% trans "The plugin" %} "<strong>{{ plugin_name }}</strong>" {% trans "could not be found." %}
|
||||
{% else %}
|
||||
{% trans "The requested plugin could not be found." %}
|
||||
{% endif %}
|
||||
{% if error %}
|
||||
<br><small style="color: #dc3545;">{{ error }}</small>
|
||||
{% endif %}
|
||||
</p>
|
||||
<div class="error-actions">
|
||||
<a href="/plugins/installed" class="error-btn error-btn-primary">
|
||||
<i class="fas fa-arrow-left"></i>
|
||||
{% trans "Back to Plugins" %}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
@@ -182,6 +182,103 @@
|
||||
color: var(--text-secondary, #64748b);
|
||||
}
|
||||
|
||||
.plugin-pricing-badge {
|
||||
display: inline-block;
|
||||
padding: 3px 8px;
|
||||
border-radius: 4px;
|
||||
font-size: 10px;
|
||||
font-weight: 600;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
margin-left: 6px;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.plugin-pricing-badge.free {
|
||||
background: #d4edda;
|
||||
color: #155724;
|
||||
border: 1px solid #c3e6cb;
|
||||
}
|
||||
|
||||
.plugin-pricing-badge.paid {
|
||||
background: #fff3cd;
|
||||
color: #856404;
|
||||
border: 1px solid #ffeaa7;
|
||||
}
|
||||
|
||||
.paid-badge {
|
||||
display: inline-block;
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
color: white;
|
||||
padding: 4px 10px;
|
||||
border-radius: 12px;
|
||||
font-size: 10px;
|
||||
font-weight: 600;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
margin-left: 8px;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.paid-badge i {
|
||||
margin-right: 4px;
|
||||
}
|
||||
|
||||
.subscription-warning {
|
||||
background: #fff3cd;
|
||||
border: 1px solid #ffc107;
|
||||
border-radius: 8px;
|
||||
padding: 12px;
|
||||
margin-top: 10px;
|
||||
font-size: 12px;
|
||||
color: #856404;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
flex-wrap: wrap;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.subscription-warning-content {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.subscription-warning i {
|
||||
margin-right: 6px;
|
||||
color: #ffc107;
|
||||
}
|
||||
|
||||
.subscription-warning-button {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
padding: 8px 16px;
|
||||
background: linear-gradient(135deg, #f96854 0%, #f96854 100%);
|
||||
color: white;
|
||||
text-decoration: none;
|
||||
border-radius: 6px;
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
transition: all 0.3s ease;
|
||||
white-space: nowrap;
|
||||
box-shadow: 0 2px 4px rgba(249, 104, 84, 0.3);
|
||||
}
|
||||
|
||||
.subscription-warning-button:hover {
|
||||
background: linear-gradient(135deg, #e55a47 0%, #e55a47 100%);
|
||||
transform: translateY(-1px);
|
||||
box-shadow: 0 4px 8px rgba(249, 104, 84, 0.4);
|
||||
color: white;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.subscription-warning-button i {
|
||||
margin-right: 0;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.plugin-description {
|
||||
font-size: 13px;
|
||||
color: var(--text-secondary, #64748b);
|
||||
@@ -803,30 +900,49 @@
|
||||
<div class="plugins-container">
|
||||
<!-- Page Header -->
|
||||
<div class="page-header">
|
||||
<h1>
|
||||
<div class="icon">
|
||||
<i class="fas fa-plug"></i>
|
||||
<div style="display: flex; justify-content: space-between; align-items: flex-start; width: 100%;">
|
||||
<div>
|
||||
<h1>
|
||||
<div class="icon">
|
||||
<i class="fas fa-plug"></i>
|
||||
</div>
|
||||
{% trans "Installed Plugins" %}
|
||||
</h1>
|
||||
<p>{% trans "List of installed plugins on your CyberPanel" %}</p>
|
||||
</div>
|
||||
{% trans "Installed Plugins" %}
|
||||
</h1>
|
||||
<p>{% trans "List of installed plugins on your CyberPanel" %}</p>
|
||||
<div style="display: flex; gap: 20px; align-items: center; margin-top: 10px;">
|
||||
<div style="display: flex; align-items: center; gap: 8px;">
|
||||
<i class="fas fa-check-circle" style="color: #28a745; font-size: 18px;"></i>
|
||||
<span style="font-weight: 600; color: var(--text-primary, #2f3640);">
|
||||
{% trans "Installed:" %} {{ installed_count|default:0 }}
|
||||
</span>
|
||||
</div>
|
||||
<div style="display: flex; align-items: center; gap: 8px;">
|
||||
<i class="fas fa-power-off" style="color: #007bff; font-size: 18px;"></i>
|
||||
<span style="font-weight: 600; color: var(--text-primary, #2f3640);">
|
||||
{% trans "Active:" %} {{ active_count|default:0 }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Plugins Section -->
|
||||
<div class="content-section">
|
||||
<h2 class="section-title">{% trans "Plugins" %}</h2>
|
||||
|
||||
{% if plugins %}
|
||||
<!-- View Toggle -->
|
||||
<div id="plugins-view-toggle" class="view-toggle" style="{% if not plugins %}margin-top: 0;{% endif %}">
|
||||
<button class="view-btn {% if plugins %}active{% else %}{% endif %}" onclick="toggleView('grid')">
|
||||
<div class="view-toggle">
|
||||
<button class="view-btn active" onclick="toggleView('grid')">
|
||||
<i class="fas fa-th-large"></i>
|
||||
{% trans "Grid View" %}
|
||||
</button>
|
||||
<button class="view-btn {% if not plugins %}active{% endif %}" onclick="toggleView('table')">
|
||||
<button class="view-btn" onclick="toggleView('table')">
|
||||
<i class="fas fa-list"></i>
|
||||
{% trans "Table View" %}
|
||||
</button>
|
||||
<button class="view-btn {% if not plugins %}active{% endif %}" onclick="toggleView('store')">
|
||||
<button class="view-btn" onclick="toggleView('store')">
|
||||
<i class="fas fa-store"></i>
|
||||
{% trans "CyberPanel Plugin Store" %}
|
||||
</button>
|
||||
@@ -836,7 +952,6 @@
|
||||
</a>
|
||||
</div>
|
||||
|
||||
{% if plugins %}
|
||||
<!-- Grid View -->
|
||||
<div id="gridView" class="plugins-grid">
|
||||
{% for plugin in plugins %}
|
||||
@@ -856,22 +971,35 @@
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="plugin-info">
|
||||
<h3 class="plugin-name">{{ plugin.name }}</h3>
|
||||
<h3 class="plugin-name">{{ plugin.name }}{% if plugin.is_paid|default:False|default_if_none:False %} <span class="paid-badge" title="{% trans 'Paid Plugin - Patreon Subscription Required' %}"><i class="fas fa-crown"></i> {% trans "Premium" %}</span>{% endif %}</h3>
|
||||
<div class="plugin-meta">
|
||||
<span class="plugin-type">{{ plugin.type }}</span>
|
||||
<span class="plugin-version-number">v{{ plugin.version }}</span>
|
||||
{% if plugin.is_paid|default:False|default_if_none:False %}
|
||||
<span class="plugin-pricing-badge paid">{% trans "Paid" %}</span>
|
||||
{% else %}
|
||||
<span class="plugin-pricing-badge free">{% trans "Free" %}</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% if plugin.author %}
|
||||
<div class="plugin-author" style="margin-top: 8px; font-size: 13px; color: var(--text-secondary, #64748b);">
|
||||
<i class="fas fa-user" style="margin-right: 5px;"></i>
|
||||
<span>{% trans "Author:" %} {{ plugin.author }}</span>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="plugin-description">
|
||||
{{ plugin.desc }}
|
||||
{% if plugin.is_paid|default:False|default_if_none:False %}
|
||||
<div class="subscription-warning">
|
||||
<div class="subscription-warning-content">
|
||||
<i class="fas fa-exclamation-triangle"></i>
|
||||
<strong>{% trans "Paid Plugin:" %}</strong> {% trans "Requires Patreon subscription to" %} "{{ plugin.patreon_tier|default:'CyberPanel Paid Plugin' }}"
|
||||
</div>
|
||||
{% if plugin.patreon_url %}
|
||||
<a href="{{ plugin.patreon_url }}" target="_blank" rel="noopener noreferrer" class="subscription-warning-button">
|
||||
<i class="fab fa-patreon"></i>
|
||||
{% trans "Subscribe on Patreon" %}
|
||||
</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<div class="plugin-status-section">
|
||||
@@ -943,7 +1071,6 @@
|
||||
<thead>
|
||||
<tr>
|
||||
<th>{% trans "Plugin Name" %}</th>
|
||||
<th>{% trans "Author" %}</th>
|
||||
<th>{% trans "Version" %}</th>
|
||||
<th>{% trans "Modify Date" %}</th>
|
||||
<th>{% trans "Status" %}</th>
|
||||
@@ -959,13 +1086,13 @@
|
||||
<td>
|
||||
<strong>{{ plugin.name }}</strong>
|
||||
</td>
|
||||
<td>
|
||||
<span style="color: var(--text-secondary, #64748b); font-size: 13px;">
|
||||
{{ plugin.author|default:"Unknown" }}
|
||||
</span>
|
||||
</td>
|
||||
<td>
|
||||
<span class="plugin-version-number">{{ plugin.version }}</span>
|
||||
{% if plugin.is_paid|default:False|default_if_none:False %}
|
||||
<span class="plugin-pricing-badge paid">{% trans "Paid" %}</span>
|
||||
{% else %}
|
||||
<span class="plugin-pricing-badge free">{% trans "Free" %}</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>
|
||||
<small style="color: var(--text-secondary, #64748b);">
|
||||
@@ -1043,6 +1170,26 @@
|
||||
<h3 class="empty-title">{% trans "No Plugins Installed" %}</h3>
|
||||
<p class="empty-description">{% trans "You haven't installed any plugins yet. Plugins extend CyberPanel's functionality with additional features." %}</p>
|
||||
</div>
|
||||
|
||||
<!-- View Toggle (only shown when no plugins installed) -->
|
||||
<div class="view-toggle" style="margin-top: 25px;">
|
||||
<button class="view-btn" onclick="toggleView('grid')">
|
||||
<i class="fas fa-th-large"></i>
|
||||
{% trans "Grid View" %}
|
||||
</button>
|
||||
<button class="view-btn" onclick="toggleView('table')">
|
||||
<i class="fas fa-list"></i>
|
||||
{% trans "Table View" %}
|
||||
</button>
|
||||
<button class="view-btn active" onclick="toggleView('store')">
|
||||
<i class="fas fa-store"></i>
|
||||
{% trans "CyberPanel Plugin Store" %}
|
||||
</button>
|
||||
<a href="/plugins/help/" class="view-btn" style="text-decoration: none;">
|
||||
<i class="fas fa-book"></i>
|
||||
{% trans "Plugin Development Guide" %}
|
||||
</a>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<!-- CyberPanel Plugin Store (always available) -->
|
||||
@@ -1097,13 +1244,12 @@
|
||||
<table class="store-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>{% trans "Icon" %}</th>
|
||||
<th>{% trans "Plugin Name" %}</th>
|
||||
<th>{% trans "Author" %}</th>
|
||||
<th>{% trans "Version" %}</th>
|
||||
<th>{% trans "Pricing" %}</th>
|
||||
<th>{% trans "Modify Date" %}</th>
|
||||
<th>{% trans "Status" %}</th>
|
||||
<th>{% trans "Action" %}</th>
|
||||
<th>{% trans "Active" %}</th>
|
||||
<th>{% trans "Help" %}</th>
|
||||
<th>{% trans "About" %}</th>
|
||||
</tr>
|
||||
@@ -1125,6 +1271,8 @@
|
||||
</div>
|
||||
|
||||
<script>
|
||||
// Cache-busting version: 2026-01-25-v4 - Fixed is_paid normalization and ensured consistent rendering
|
||||
// Force browser to reload this script by changing version number
|
||||
let storePlugins = [];
|
||||
let currentFilter = 'all';
|
||||
|
||||
@@ -1148,9 +1296,7 @@ function toggleView(view) {
|
||||
const gridView = document.getElementById('gridView');
|
||||
const tableView = document.getElementById('tableView');
|
||||
const storeView = document.getElementById('storeView');
|
||||
// Only select view buttons from the main view-toggle (prevent duplicates)
|
||||
const viewToggle = document.getElementById('plugins-view-toggle');
|
||||
const viewBtns = viewToggle ? viewToggle.querySelectorAll('.view-btn') : document.querySelectorAll('.view-btn');
|
||||
const viewBtns = document.querySelectorAll('.view-btn');
|
||||
|
||||
viewBtns.forEach(btn => btn.classList.remove('active'));
|
||||
|
||||
@@ -1223,11 +1369,17 @@ function escapeHtml(text) {
|
||||
}
|
||||
|
||||
function displayStorePlugins() {
|
||||
// Version: 2026-01-25-v4 - Store view: Removed Status column, always show Free/Paid badges
|
||||
// CRITICAL: This function MUST create exactly 7 columns (no Status, no Deactivate/Uninstall)
|
||||
const tbody = document.getElementById('storeTableBody');
|
||||
if (!tbody) {
|
||||
console.error('storeTableBody not found!');
|
||||
return;
|
||||
}
|
||||
tbody.innerHTML = '';
|
||||
|
||||
if (!storePlugins || storePlugins.length === 0) {
|
||||
tbody.innerHTML = '<tr><td colspan="9" style="text-align: center; padding: 20px; color: var(--text-secondary, #64748b);">No plugins available in store</td></tr>';
|
||||
tbody.innerHTML = '<tr><td colspan="8" style="text-align: center; padding: 20px; color: var(--text-secondary, #64748b);">No plugins available in store</td></tr>';
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -1239,57 +1391,38 @@ function displayStorePlugins() {
|
||||
);
|
||||
}
|
||||
|
||||
filteredPlugins.forEach(plugin => {
|
||||
filteredPlugins.forEach(plugin => {
|
||||
const row = document.createElement('tr');
|
||||
|
||||
// Status column
|
||||
let statusHtml = '';
|
||||
if (plugin.installed) {
|
||||
statusHtml = '<span class="status-installed">Installed</span>';
|
||||
} else {
|
||||
statusHtml = '<span class="status-not-installed">Not Installed</span>';
|
||||
// Plugin icon - based on plugin type (same logic as Grid/Table views)
|
||||
const pluginType = (plugin.type || 'Plugin').toLowerCase();
|
||||
let iconClass = 'fas fa-puzzle-piece'; // Default icon
|
||||
if (pluginType.includes('security')) {
|
||||
iconClass = 'fas fa-shield-alt';
|
||||
} else if (pluginType.includes('performance')) {
|
||||
iconClass = 'fas fa-rocket';
|
||||
} else if (pluginType.includes('utility')) {
|
||||
iconClass = 'fas fa-tools';
|
||||
} else if (pluginType.includes('backup')) {
|
||||
iconClass = 'fas fa-save';
|
||||
}
|
||||
const iconHtml = `<div class="plugin-icon" style="width: 40px; height: 40px; background: var(--bg-secondary, #f8f9ff); border-radius: 8px; display: flex; align-items: center; justify-content: center; font-size: 18px; color: #5856d6; margin: 0 auto;">
|
||||
<i class="${iconClass}"></i>
|
||||
</div>`;
|
||||
|
||||
// Action column
|
||||
// Action column - Store view only shows Install/Installed (no Deactivate/Uninstall)
|
||||
// NOTE: Store view should NOT show Deactivate/Uninstall buttons - users manage from Grid/Table views
|
||||
let actionHtml = '';
|
||||
if (plugin.installed) {
|
||||
if (plugin.enabled) {
|
||||
actionHtml = `<div class="plugin-actions">
|
||||
<button class="btn-action btn-deactivate" onclick="deactivatePlugin('${plugin.plugin_dir}')">
|
||||
<i class="fas fa-toggle-on"></i> Deactivate
|
||||
</button>
|
||||
<button class="btn-action btn-uninstall" onclick="uninstallPluginFromStore('${plugin.plugin_dir}')">
|
||||
<i class="fas fa-trash"></i> Uninstall
|
||||
</button>
|
||||
</div>`;
|
||||
} else {
|
||||
actionHtml = `<div class="plugin-actions">
|
||||
<button class="btn-action btn-activate" onclick="activatePlugin('${plugin.plugin_dir}')">
|
||||
<i class="fas fa-toggle-off"></i> Activate
|
||||
</button>
|
||||
<button class="btn-action btn-uninstall" onclick="uninstallPluginFromStore('${plugin.plugin_dir}')">
|
||||
<i class="fas fa-trash"></i> Uninstall
|
||||
</button>
|
||||
</div>`;
|
||||
}
|
||||
// Show "Installed" text
|
||||
actionHtml = '<span class="status-installed">Installed</span>';
|
||||
} else {
|
||||
// Show Install button
|
||||
actionHtml = `<button class="btn-action btn-install" onclick="installFromStore('${plugin.plugin_dir}')">
|
||||
<i class="fas fa-download"></i> Install
|
||||
</button>`;
|
||||
}
|
||||
|
||||
// Active column
|
||||
let activeHtml = '';
|
||||
if (plugin.installed) {
|
||||
if (plugin.enabled) {
|
||||
activeHtml = '<span class="active-status active-yes"><i class="fas fa-check-circle"></i></span>';
|
||||
} else {
|
||||
activeHtml = '<span class="active-status active-no"><i class="fas fa-times-circle"></i></span>';
|
||||
}
|
||||
} else {
|
||||
activeHtml = '<span class="active-status active-na">-</span>';
|
||||
}
|
||||
|
||||
// Help column - always use consistent help URL
|
||||
const helpUrl = `/plugins/${plugin.plugin_dir}/help/`;
|
||||
const helpHtml = `<a href="${helpUrl}" class="btn-link" title="Plugin Help">
|
||||
@@ -1305,19 +1438,29 @@ function displayStorePlugins() {
|
||||
// Modify Date column - show N/A for store plugins (they're from GitHub, not local)
|
||||
const modifyDateHtml = plugin.modify_date ? `<small style="color: var(--text-secondary, #64748b);">${escapeHtml(plugin.modify_date)}</small>` : '<small style="color: var(--text-secondary, #64748b);">N/A</small>';
|
||||
|
||||
// Author column
|
||||
const authorHtml = plugin.author ? `<span style="color: var(--text-secondary, #64748b); font-size: 13px;">${escapeHtml(plugin.author)}</span>` : '<span style="color: var(--text-secondary, #64748b); font-size: 13px;">Unknown</span>';
|
||||
// Pricing badge - ALWAYS show a badge (default to Free if is_paid is missing/undefined/null)
|
||||
// Version: 2026-01-25-v4 - Normalize is_paid to handle all possible values
|
||||
let isPaid = false;
|
||||
if (plugin.is_paid !== undefined && plugin.is_paid !== null) {
|
||||
const isPaidValue = plugin.is_paid;
|
||||
if (isPaidValue === true || isPaidValue === 'true' || isPaidValue === 'True' || isPaidValue === 1 || isPaidValue === '1' || String(isPaidValue).toLowerCase() === 'true') {
|
||||
isPaid = true;
|
||||
}
|
||||
}
|
||||
const pricingBadge = isPaid
|
||||
? '<span class="plugin-pricing-badge paid">Paid</span>'
|
||||
: '<span class="plugin-pricing-badge free">Free</span>';
|
||||
|
||||
// Version: 2026-01-25-v5 - Added plugin icons to Store view (8 columns: Icon, Plugin Name, Version, Pricing, Modify Date, Action, Help, About)
|
||||
row.innerHTML = `
|
||||
<td style="text-align: center;">${iconHtml}</td>
|
||||
<td>
|
||||
<strong>${escapeHtml(plugin.name)}</strong>
|
||||
</td>
|
||||
<td>${authorHtml}</td>
|
||||
<td>${escapeHtml(plugin.version)}</td>
|
||||
<td><span class="plugin-version-number">${escapeHtml(plugin.version)}</span></td>
|
||||
<td>${pricingBadge}</td>
|
||||
<td>${modifyDateHtml}</td>
|
||||
<td>${statusHtml}</td>
|
||||
<td>${actionHtml}</td>
|
||||
<td class="active-column">${activeHtml}</td>
|
||||
<td>${helpHtml}</td>
|
||||
<td>${aboutHtml}</td>
|
||||
`;
|
||||
@@ -1706,19 +1849,6 @@ if (localStorage.getItem('pluginStoreNoticeDismissed') === 'true') {
|
||||
|
||||
// Initialize view on page load
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
// Remove any duplicate view-toggle divs (keep only the one with id='plugins-view-toggle')
|
||||
const mainViewToggle = document.getElementById('plugins-view-toggle');
|
||||
if (mainViewToggle) {
|
||||
const allViewToggles = document.querySelectorAll('.view-toggle');
|
||||
allViewToggles.forEach(toggle => {
|
||||
if (toggle !== mainViewToggle && toggle.id !== 'plugins-view-toggle') {
|
||||
// This is a duplicate, remove it
|
||||
console.log('Removing duplicate view-toggle div');
|
||||
toggle.remove();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Default to grid view if plugins exist, otherwise show store
|
||||
const gridView = document.getElementById('gridView');
|
||||
const storeView = document.getElementById('storeView');
|
||||
|
||||
BIN
pluginHolder/testPlugin.zip
Normal file
BIN
pluginHolder/testPlugin.zip
Normal file
Binary file not shown.
@@ -18,6 +18,7 @@ import urllib.error
|
||||
import time
|
||||
sys.path.append('/usr/local/CyberCP')
|
||||
from pluginInstaller.pluginInstaller import pluginInstaller
|
||||
from .patreon_verifier import PatreonVerifier
|
||||
|
||||
# Plugin state file location
|
||||
PLUGIN_STATE_DIR = '/home/cyberpanel/plugin_states'
|
||||
@@ -127,7 +128,9 @@ def installed(request):
|
||||
data['desc'] = desc_elem.text
|
||||
data['version'] = version_elem.text
|
||||
data['plugin_dir'] = plugin # Plugin directory name
|
||||
data['installed'] = os.path.exists(completePath) # True if installed, False if only in source
|
||||
# Check if plugin is installed (only if it exists in /usr/local/CyberCP/)
|
||||
# Source directory presence doesn't mean installed - it just means the source files are available
|
||||
data['installed'] = os.path.exists(completePath)
|
||||
|
||||
# Get plugin enabled state (only for installed plugins)
|
||||
if data['installed']:
|
||||
@@ -135,6 +138,11 @@ def installed(request):
|
||||
else:
|
||||
data['enabled'] = False
|
||||
|
||||
# Initialize is_paid to False by default (will be set later if paid)
|
||||
data['is_paid'] = False
|
||||
data['patreon_tier'] = None
|
||||
data['patreon_url'] = None
|
||||
|
||||
# Get modify date from local file (fast, no API calls)
|
||||
# GitHub commit dates are fetched in the plugin store, not here to avoid timeouts
|
||||
modify_date = 'N/A'
|
||||
@@ -181,16 +189,40 @@ def installed(request):
|
||||
data['author'] = author_elem.text
|
||||
else:
|
||||
data['author'] = 'Unknown'
|
||||
|
||||
# Extract paid plugin information
|
||||
paid_elem = root.find('paid')
|
||||
patreon_tier_elem = root.find('patreon_tier')
|
||||
|
||||
if paid_elem is not None and paid_elem.text and paid_elem.text.lower() == 'true':
|
||||
data['is_paid'] = True
|
||||
data['patreon_tier'] = patreon_tier_elem.text if patreon_tier_elem is not None and patreon_tier_elem.text else 'CyberPanel Paid Plugin'
|
||||
data['patreon_url'] = root.find('patreon_url').text if root.find('patreon_url') is not None else 'https://www.patreon.com/c/newstargeted/membership'
|
||||
else:
|
||||
data['is_paid'] = False
|
||||
data['patreon_tier'] = None
|
||||
data['patreon_url'] = None
|
||||
|
||||
pluginList.append(data)
|
||||
processed_plugins.add(plugin) # Mark as processed
|
||||
except ElementTree.ParseError as e:
|
||||
errorPlugins.append({'name': plugin, 'error': f'XML parse error: {str(e)}'})
|
||||
logging.writeToFile(f"Plugin {plugin}: XML parse error - {str(e)}")
|
||||
# Don't mark as processed if it failed - let installed check handle it
|
||||
# This ensures plugins that exist in /usr/local/CyberCP/ but have bad source meta.xml still get counted
|
||||
if not os.path.exists(completePath):
|
||||
# Only skip if it's not actually installed
|
||||
continue
|
||||
# If it exists in installed location, don't mark as processed so it gets checked there
|
||||
continue
|
||||
except Exception as e:
|
||||
errorPlugins.append({'name': plugin, 'error': f'Error loading plugin: {str(e)}'})
|
||||
logging.writeToFile(f"Plugin {plugin}: Error loading - {str(e)}")
|
||||
# Don't mark as processed if it failed - let installed check handle it
|
||||
if not os.path.exists(completePath):
|
||||
# Only skip if it's not actually installed
|
||||
continue
|
||||
# If it exists in installed location, don't mark as processed so it gets checked there
|
||||
continue
|
||||
|
||||
# Also check for installed plugins that don't have source directories
|
||||
@@ -236,6 +268,11 @@ def installed(request):
|
||||
data['installed'] = True # This is an installed plugin
|
||||
data['enabled'] = _is_plugin_enabled(plugin)
|
||||
|
||||
# Initialize is_paid to False by default (will be set later if paid)
|
||||
data['is_paid'] = False
|
||||
data['patreon_tier'] = None
|
||||
data['patreon_url'] = None
|
||||
|
||||
# Get modify date from installed location
|
||||
modify_date = 'N/A'
|
||||
try:
|
||||
@@ -276,6 +313,17 @@ def installed(request):
|
||||
else:
|
||||
data['author'] = 'Unknown'
|
||||
|
||||
# Extract paid plugin information (is_paid already initialized to False above)
|
||||
paid_elem = root.find('paid')
|
||||
patreon_tier_elem = root.find('patreon_tier')
|
||||
|
||||
if paid_elem is not None and paid_elem.text and paid_elem.text.lower() == 'true':
|
||||
data['is_paid'] = True
|
||||
data['patreon_tier'] = patreon_tier_elem.text if patreon_tier_elem is not None and patreon_tier_elem.text else 'CyberPanel Paid Plugin'
|
||||
patreon_url_elem = root.find('patreon_url')
|
||||
data['patreon_url'] = patreon_url_elem.text if patreon_url_elem is not None and patreon_url_elem.text else 'https://www.patreon.com/membership/27789984'
|
||||
# else: is_paid already False from initialization above
|
||||
|
||||
pluginList.append(data)
|
||||
|
||||
except ElementTree.ParseError as e:
|
||||
@@ -287,8 +335,38 @@ def installed(request):
|
||||
logging.writeToFile(f"Installed plugin {plugin}: Error loading - {str(e)}")
|
||||
continue
|
||||
|
||||
# Calculate installed and active counts
|
||||
# Double-check by also counting plugins that actually exist in /usr/local/CyberCP/
|
||||
installed_plugins_in_filesystem = set()
|
||||
if os.path.exists(installedPath):
|
||||
for plugin in os.listdir(installedPath):
|
||||
pluginInstalledDir = os.path.join(installedPath, plugin)
|
||||
if os.path.isdir(pluginInstalledDir):
|
||||
metaXmlPath = os.path.join(pluginInstalledDir, 'meta.xml')
|
||||
if os.path.exists(metaXmlPath):
|
||||
installed_plugins_in_filesystem.add(plugin)
|
||||
|
||||
# Count installed plugins from the list
|
||||
installed_count = len([p for p in pluginList if p.get('installed', False)])
|
||||
active_count = len([p for p in pluginList if p.get('installed', False) and p.get('enabled', False)])
|
||||
|
||||
# If there's a discrepancy, use the filesystem count as the source of truth
|
||||
filesystem_installed_count = len(installed_plugins_in_filesystem)
|
||||
if filesystem_installed_count != installed_count:
|
||||
logging.writeToFile(f"WARNING: Plugin count mismatch! List says {installed_count}, filesystem has {filesystem_installed_count}")
|
||||
logging.writeToFile(f"Plugins in filesystem: {sorted(installed_plugins_in_filesystem)}")
|
||||
logging.writeToFile(f"Plugins in list with installed=True: {[p.get('plugin_dir') for p in pluginList if p.get('installed', False)]}")
|
||||
# Use filesystem count as source of truth
|
||||
installed_count = filesystem_installed_count
|
||||
|
||||
# Debug logging to help identify discrepancies
|
||||
logging.writeToFile(f"Plugin count: Total={len(pluginList)}, Installed={installed_count}, Active={active_count}")
|
||||
for p in pluginList:
|
||||
logging.writeToFile(f" - {p.get('plugin_dir')}: installed={p.get('installed')}, enabled={p.get('enabled')}")
|
||||
|
||||
proc = httpProc(request, 'pluginHolder/plugins.html',
|
||||
{'plugins': pluginList, 'error_plugins': errorPlugins}, 'admin')
|
||||
{'plugins': pluginList, 'error_plugins': errorPlugins,
|
||||
'installed_count': installed_count, 'active_count': active_count}, 'admin')
|
||||
return proc.render()
|
||||
|
||||
@csrf_exempt
|
||||
@@ -315,11 +393,25 @@ def install_plugin(request, plugin_name):
|
||||
# Create zip file for installation (pluginInstaller expects a zip)
|
||||
import tempfile
|
||||
import shutil
|
||||
import zipfile
|
||||
temp_dir = tempfile.mkdtemp()
|
||||
zip_path = os.path.join(temp_dir, plugin_name + '.zip')
|
||||
|
||||
# Create zip from source directory
|
||||
shutil.make_archive(os.path.join(temp_dir, plugin_name), 'zip', pluginSource)
|
||||
# Create zip from source directory with correct structure
|
||||
# The ZIP must contain plugin_name/ directory structure for proper extraction
|
||||
plugin_zip = zipfile.ZipFile(zip_path, 'w', zipfile.ZIP_DEFLATED)
|
||||
|
||||
# Walk through source directory and add files with plugin_name prefix
|
||||
for root, dirs, files in os.walk(pluginSource):
|
||||
for file in files:
|
||||
file_path = os.path.join(root, file)
|
||||
# Calculate relative path from plugin source
|
||||
arcname = os.path.relpath(file_path, pluginSource)
|
||||
# Add plugin_name prefix to maintain directory structure
|
||||
arcname = os.path.join(plugin_name, arcname)
|
||||
plugin_zip.write(file_path, arcname)
|
||||
|
||||
plugin_zip.close()
|
||||
|
||||
# Verify zip file was created
|
||||
if not os.path.exists(zip_path):
|
||||
@@ -340,11 +432,31 @@ def install_plugin(request, plugin_name):
|
||||
raise Exception(f'Zip file {zip_file} not found in temp directory')
|
||||
|
||||
# Install using pluginInstaller
|
||||
pluginInstaller.installPlugin(plugin_name)
|
||||
try:
|
||||
pluginInstaller.installPlugin(plugin_name)
|
||||
except Exception as install_error:
|
||||
# Log the full error for debugging
|
||||
error_msg = str(install_error)
|
||||
logging.writeToFile(f"pluginInstaller.installPlugin raised exception: {error_msg}")
|
||||
# Check if plugin directory exists despite the error
|
||||
pluginInstalled = '/usr/local/CyberCP/' + plugin_name
|
||||
if os.path.exists(pluginInstalled):
|
||||
logging.writeToFile(f"Plugin directory exists despite error, continuing...")
|
||||
else:
|
||||
raise Exception(f'Plugin installation failed: {error_msg}')
|
||||
|
||||
# Wait a moment for file system to sync
|
||||
import time
|
||||
time.sleep(2)
|
||||
|
||||
# Verify plugin was actually installed
|
||||
pluginInstalled = '/usr/local/CyberCP/' + plugin_name
|
||||
if not os.path.exists(pluginInstalled):
|
||||
# Check if files were extracted to root instead
|
||||
root_files = ['README.md', 'apps.py', 'meta.xml', 'urls.py', 'views.py']
|
||||
found_root_files = [f for f in root_files if os.path.exists(os.path.join('/usr/local/CyberCP', f))]
|
||||
if found_root_files:
|
||||
raise Exception(f'Plugin installation failed: Files extracted to wrong location. Found {found_root_files} in /usr/local/CyberCP/ root instead of {pluginInstalled}/')
|
||||
raise Exception(f'Plugin installation failed: {pluginInstalled} does not exist after installation')
|
||||
|
||||
# Set plugin to enabled by default after installation
|
||||
@@ -551,10 +663,11 @@ def _enrich_store_plugins(plugins):
|
||||
continue
|
||||
|
||||
# Check if plugin is installed locally
|
||||
# Plugin is only considered "installed" if it exists in /usr/local/CyberCP/
|
||||
# Source directory presence doesn't mean installed - it just means the source files are available
|
||||
installed_path = os.path.join(plugin_install_dir, plugin_dir)
|
||||
source_path = os.path.join(plugin_source_dir, plugin_dir)
|
||||
|
||||
plugin['installed'] = os.path.exists(installed_path) or os.path.exists(source_path)
|
||||
plugin['installed'] = os.path.exists(installed_path)
|
||||
|
||||
# Check if plugin is enabled (only if installed)
|
||||
if plugin['installed']:
|
||||
@@ -562,6 +675,40 @@ def _enrich_store_plugins(plugins):
|
||||
else:
|
||||
plugin['enabled'] = False
|
||||
|
||||
# Ensure is_paid field exists and is properly set (default to False if not set or invalid)
|
||||
# Handle all possible cases: missing, None, empty string, string values, boolean
|
||||
is_paid_value = plugin.get('is_paid', False)
|
||||
|
||||
# Normalize is_paid to boolean
|
||||
if is_paid_value is None or is_paid_value == '' or is_paid_value == 'false' or is_paid_value == 'False' or is_paid_value == '0':
|
||||
plugin['is_paid'] = False
|
||||
elif is_paid_value is True or is_paid_value == 'true' or is_paid_value == 'True' or is_paid_value == '1' or str(is_paid_value).lower() == 'true':
|
||||
plugin['is_paid'] = True
|
||||
elif 'is_paid' not in plugin or plugin.get('is_paid') is None:
|
||||
# Try to check from local meta.xml if available
|
||||
meta_path = None
|
||||
if os.path.exists(installed_path):
|
||||
meta_path = os.path.join(installed_path, 'meta.xml')
|
||||
elif os.path.exists(source_path):
|
||||
meta_path = os.path.join(source_path, 'meta.xml')
|
||||
|
||||
if meta_path and os.path.exists(meta_path):
|
||||
try:
|
||||
pluginMetaData = ElementTree.parse(meta_path)
|
||||
root = pluginMetaData.getroot()
|
||||
paid_elem = root.find('paid')
|
||||
if paid_elem is not None and paid_elem.text and paid_elem.text.lower() == 'true':
|
||||
plugin['is_paid'] = True
|
||||
else:
|
||||
plugin['is_paid'] = False
|
||||
except:
|
||||
plugin['is_paid'] = False
|
||||
else:
|
||||
plugin['is_paid'] = False # Default to free if we can't determine
|
||||
else:
|
||||
# Already set, but ensure it's boolean
|
||||
plugin['is_paid'] = bool(plugin['is_paid']) if plugin['is_paid'] not in [True, False] else plugin['is_paid']
|
||||
|
||||
enriched.append(plugin)
|
||||
|
||||
return enriched
|
||||
@@ -633,6 +780,20 @@ def _fetch_plugins_from_github():
|
||||
logging.writeToFile(f"Could not fetch commit date for {plugin_name}: {str(e)}")
|
||||
modify_date = 'N/A'
|
||||
|
||||
# Extract paid plugin information
|
||||
paid_elem = root.find('paid')
|
||||
patreon_tier_elem = root.find('patreon_tier')
|
||||
|
||||
is_paid = False
|
||||
patreon_tier = None
|
||||
patreon_url = None
|
||||
|
||||
if paid_elem is not None and paid_elem.text and paid_elem.text.lower() == 'true':
|
||||
is_paid = True
|
||||
patreon_tier = patreon_tier_elem.text if patreon_tier_elem is not None and patreon_tier_elem.text else 'CyberPanel Paid Plugin'
|
||||
patreon_url_elem = root.find('patreon_url')
|
||||
patreon_url = patreon_url_elem.text if patreon_url_elem is not None else 'https://www.patreon.com/c/newstargeted/membership'
|
||||
|
||||
plugin_data = {
|
||||
'plugin_dir': plugin_name,
|
||||
'name': root.find('name').text if root.find('name') is not None else plugin_name,
|
||||
@@ -644,7 +805,10 @@ def _fetch_plugins_from_github():
|
||||
'author': root.find('author').text if root.find('author') is not None else 'Unknown',
|
||||
'github_url': f'https://github.com/master3395/cyberpanel-plugins/tree/main/{plugin_name}',
|
||||
'about_url': f'https://github.com/master3395/cyberpanel-plugins/tree/main/{plugin_name}',
|
||||
'modify_date': modify_date
|
||||
'modify_date': modify_date,
|
||||
'is_paid': is_paid,
|
||||
'patreon_tier': patreon_tier,
|
||||
'patreon_url': patreon_url
|
||||
}
|
||||
|
||||
plugins.append(plugin_data)
|
||||
@@ -746,7 +910,7 @@ def fetch_plugin_store(request):
|
||||
@csrf_exempt
|
||||
@require_http_methods(["POST"])
|
||||
def install_from_store(request, plugin_name):
|
||||
"""Install plugin from GitHub store"""
|
||||
"""Install plugin from GitHub store, with fallback to local source"""
|
||||
mailUtilities.checkHome()
|
||||
|
||||
try:
|
||||
@@ -771,44 +935,80 @@ def install_from_store(request, plugin_name):
|
||||
zip_path = os.path.join(temp_dir, plugin_name + '.zip')
|
||||
|
||||
try:
|
||||
# Download repository as ZIP
|
||||
repo_zip_url = 'https://github.com/master3395/cyberpanel-plugins/archive/refs/heads/main.zip'
|
||||
logging.writeToFile(f"Downloading plugin from: {repo_zip_url}")
|
||||
# Try to download from GitHub first
|
||||
use_local_fallback = False
|
||||
try:
|
||||
# Download repository as ZIP
|
||||
repo_zip_url = 'https://github.com/master3395/cyberpanel-plugins/archive/refs/heads/main.zip'
|
||||
logging.writeToFile(f"Downloading plugin from: {repo_zip_url}")
|
||||
|
||||
repo_req = urllib.request.Request(
|
||||
repo_zip_url,
|
||||
headers={
|
||||
'User-Agent': 'CyberPanel-Plugin-Store/1.0',
|
||||
'Accept': 'application/zip'
|
||||
}
|
||||
)
|
||||
|
||||
with urllib.request.urlopen(repo_req, timeout=30) as repo_response:
|
||||
repo_zip_data = repo_response.read()
|
||||
|
||||
# Extract plugin directory from repository ZIP
|
||||
repo_zip = zipfile.ZipFile(io.BytesIO(repo_zip_data))
|
||||
|
||||
# Find plugin directory in ZIP
|
||||
plugin_prefix = f'cyberpanel-plugins-main/{plugin_name}/'
|
||||
plugin_files = [f for f in repo_zip.namelist() if f.startswith(plugin_prefix)]
|
||||
|
||||
if not plugin_files:
|
||||
logging.writeToFile(f"Plugin {plugin_name} not found in GitHub repository, trying local source")
|
||||
use_local_fallback = True
|
||||
else:
|
||||
logging.writeToFile(f"Found {len(plugin_files)} files for plugin {plugin_name} in GitHub")
|
||||
|
||||
# Create plugin ZIP file from GitHub with correct structure
|
||||
# The ZIP must contain plugin_name/ directory structure for proper extraction
|
||||
plugin_zip = zipfile.ZipFile(zip_path, 'w', zipfile.ZIP_DEFLATED)
|
||||
|
||||
for file_path in plugin_files:
|
||||
# Remove the repository root prefix
|
||||
relative_path = file_path[len(plugin_prefix):]
|
||||
if relative_path: # Skip directories
|
||||
file_data = repo_zip.read(file_path)
|
||||
# Add plugin_name prefix to maintain directory structure
|
||||
arcname = os.path.join(plugin_name, relative_path)
|
||||
plugin_zip.writestr(arcname, file_data)
|
||||
|
||||
plugin_zip.close()
|
||||
repo_zip.close()
|
||||
except Exception as github_error:
|
||||
logging.writeToFile(f"GitHub download failed for {plugin_name}: {str(github_error)}, trying local source")
|
||||
use_local_fallback = True
|
||||
|
||||
repo_req = urllib.request.Request(
|
||||
repo_zip_url,
|
||||
headers={
|
||||
'User-Agent': 'CyberPanel-Plugin-Store/1.0',
|
||||
'Accept': 'application/zip'
|
||||
}
|
||||
)
|
||||
|
||||
with urllib.request.urlopen(repo_req, timeout=30) as repo_response:
|
||||
repo_zip_data = repo_response.read()
|
||||
|
||||
# Extract plugin directory from repository ZIP
|
||||
repo_zip = zipfile.ZipFile(io.BytesIO(repo_zip_data))
|
||||
|
||||
# Find plugin directory in ZIP
|
||||
plugin_prefix = f'cyberpanel-plugins-main/{plugin_name}/'
|
||||
plugin_files = [f for f in repo_zip.namelist() if f.startswith(plugin_prefix)]
|
||||
|
||||
if not plugin_files:
|
||||
raise Exception(f'Plugin {plugin_name} not found in GitHub repository')
|
||||
|
||||
logging.writeToFile(f"Found {len(plugin_files)} files for plugin {plugin_name}")
|
||||
|
||||
# Create plugin ZIP file
|
||||
plugin_zip = zipfile.ZipFile(zip_path, 'w', zipfile.ZIP_DEFLATED)
|
||||
|
||||
for file_path in plugin_files:
|
||||
# Remove the repository root prefix
|
||||
relative_path = file_path[len(plugin_prefix):]
|
||||
if relative_path: # Skip directories
|
||||
file_data = repo_zip.read(file_path)
|
||||
plugin_zip.writestr(relative_path, file_data)
|
||||
|
||||
plugin_zip.close()
|
||||
# Fallback to local source if GitHub download failed
|
||||
if use_local_fallback:
|
||||
pluginSource = '/home/cyberpanel/plugins/' + plugin_name
|
||||
if not os.path.exists(pluginSource):
|
||||
raise Exception(f'Plugin {plugin_name} not found in GitHub repository and local source not found at {pluginSource}')
|
||||
|
||||
logging.writeToFile(f"Using local source for {plugin_name} from {pluginSource}")
|
||||
|
||||
# Create zip from local source directory with correct structure
|
||||
# The ZIP must contain plugin_name/ directory structure for proper extraction
|
||||
import zipfile
|
||||
plugin_zip = zipfile.ZipFile(zip_path, 'w', zipfile.ZIP_DEFLATED)
|
||||
|
||||
# Walk through source directory and add files with plugin_name prefix
|
||||
for root, dirs, files in os.walk(pluginSource):
|
||||
for file in files:
|
||||
file_path = os.path.join(root, file)
|
||||
# Calculate relative path from plugin source
|
||||
arcname = os.path.relpath(file_path, pluginSource)
|
||||
# Add plugin_name prefix to maintain directory structure
|
||||
arcname = os.path.join(plugin_name, arcname)
|
||||
plugin_zip.write(file_path, arcname)
|
||||
|
||||
plugin_zip.close()
|
||||
|
||||
# Verify ZIP was created
|
||||
if not os.path.exists(zip_path):
|
||||
@@ -829,15 +1029,31 @@ def install_from_store(request, plugin_name):
|
||||
logging.writeToFile(f"Installing plugin using pluginInstaller")
|
||||
|
||||
# Install using pluginInstaller (direct call, not via command line)
|
||||
pluginInstaller.installPlugin(plugin_name)
|
||||
try:
|
||||
pluginInstaller.installPlugin(plugin_name)
|
||||
except Exception as install_error:
|
||||
# Log the full error for debugging
|
||||
error_msg = str(install_error)
|
||||
logging.writeToFile(f"pluginInstaller.installPlugin raised exception: {error_msg}")
|
||||
# Check if plugin directory exists despite the error
|
||||
pluginInstalled = '/usr/local/CyberCP/' + plugin_name
|
||||
if os.path.exists(pluginInstalled):
|
||||
logging.writeToFile(f"Plugin directory exists despite error, continuing...")
|
||||
else:
|
||||
raise Exception(f'Plugin installation failed: {error_msg}')
|
||||
|
||||
# Wait a moment for file system to sync and service to restart
|
||||
import time
|
||||
time.sleep(2)
|
||||
time.sleep(3) # Increased wait time for file system sync
|
||||
|
||||
# Verify plugin was actually installed
|
||||
pluginInstalled = '/usr/local/CyberCP/' + plugin_name
|
||||
if not os.path.exists(pluginInstalled):
|
||||
# Check if files were extracted to root instead
|
||||
root_files = ['README.md', 'apps.py', 'meta.xml', 'urls.py', 'views.py']
|
||||
found_root_files = [f for f in root_files if os.path.exists(os.path.join('/usr/local/CyberCP', f))]
|
||||
if found_root_files:
|
||||
raise Exception(f'Plugin installation failed: Files extracted to wrong location. Found {found_root_files} in /usr/local/CyberCP/ root instead of {pluginInstalled}/')
|
||||
raise Exception(f'Plugin installation failed: {pluginInstalled} does not exist after installation')
|
||||
|
||||
logging.writeToFile(f"Plugin {plugin_name} installed successfully")
|
||||
@@ -1062,3 +1278,58 @@ def plugin_help(request, plugin_name):
|
||||
|
||||
proc = httpProc(request, 'pluginHolder/plugin_help.html', context, 'admin')
|
||||
return proc.render()
|
||||
|
||||
@csrf_exempt
|
||||
@require_http_methods(["GET"])
|
||||
def check_plugin_subscription(request, plugin_name):
|
||||
"""
|
||||
API endpoint to check if user has Patreon subscription for a paid plugin
|
||||
|
||||
Args:
|
||||
request: Django request object
|
||||
plugin_name: Name of the plugin to check
|
||||
|
||||
Returns:
|
||||
JsonResponse: {
|
||||
'has_access': bool,
|
||||
'is_paid': bool,
|
||||
'message': str,
|
||||
'patreon_url': str or None
|
||||
}
|
||||
"""
|
||||
try:
|
||||
# Check if user is authenticated
|
||||
if not request.user or not request.user.is_authenticated:
|
||||
return JsonResponse({
|
||||
'success': False,
|
||||
'has_access': False,
|
||||
'is_paid': False,
|
||||
'message': 'Please log in to check subscription status',
|
||||
'patreon_url': None
|
||||
}, status=401)
|
||||
|
||||
# Load plugin metadata
|
||||
from .plugin_access import check_plugin_access, _load_plugin_meta
|
||||
|
||||
plugin_meta = _load_plugin_meta(plugin_name)
|
||||
|
||||
# Check access
|
||||
access_result = check_plugin_access(request, plugin_name, plugin_meta)
|
||||
|
||||
return JsonResponse({
|
||||
'success': True,
|
||||
'has_access': access_result['has_access'],
|
||||
'is_paid': access_result['is_paid'],
|
||||
'message': access_result['message'],
|
||||
'patreon_url': access_result.get('patreon_url')
|
||||
})
|
||||
|
||||
except Exception as e:
|
||||
logging.writeToFile(f"Error checking subscription for {plugin_name}: {str(e)}")
|
||||
return JsonResponse({
|
||||
'success': False,
|
||||
'has_access': False,
|
||||
'is_paid': False,
|
||||
'message': f'Error checking subscription: {str(e)}',
|
||||
'patreon_url': None
|
||||
}, status=500)
|
||||
|
||||
@@ -71,8 +71,8 @@ class pluginInstaller:
|
||||
|
||||
@staticmethod
|
||||
def upgradingSettingsFile(pluginName):
|
||||
data = open("/usr/local/CyberCP/CyberCP/settings.py", 'r').readlines()
|
||||
writeToFile = open("/usr/local/CyberCP/CyberCP/settings.py", 'w')
|
||||
data = open("/usr/local/CyberCP/CyberCP/settings.py", 'r', encoding='utf-8').readlines()
|
||||
writeToFile = open("/usr/local/CyberCP/CyberCP/settings.py", 'w', encoding='utf-8')
|
||||
|
||||
for items in data:
|
||||
if items.find("'emailPremium',") > -1:
|
||||
@@ -90,8 +90,8 @@ class pluginInstaller:
|
||||
Plugin URLs must be inserted BEFORE the generic 'plugins/' line
|
||||
to ensure proper route matching (more specific routes first)
|
||||
"""
|
||||
data = open("/usr/local/CyberCP/CyberCP/urls.py", 'r').readlines()
|
||||
writeToFile = open("/usr/local/CyberCP/CyberCP/urls.py", 'w')
|
||||
data = open("/usr/local/CyberCP/CyberCP/urls.py", 'r', encoding='utf-8').readlines()
|
||||
writeToFile = open("/usr/local/CyberCP/CyberCP/urls.py", 'w', encoding='utf-8')
|
||||
urlPatternAdded = False
|
||||
|
||||
for items in data:
|
||||
@@ -109,7 +109,7 @@ class pluginInstaller:
|
||||
if not urlPatternAdded:
|
||||
pluginInstaller.stdOut(f"Warning: 'plugins/' line not found, using fallback insertion after 'manageservices'")
|
||||
writeToFile.close()
|
||||
writeToFile = open("/usr/local/CyberCP/CyberCP/urls.py", 'w')
|
||||
writeToFile = open("/usr/local/CyberCP/CyberCP/urls.py", 'w', encoding='utf-8')
|
||||
for items in data:
|
||||
if items.find("manageservices") > -1:
|
||||
writeToFile.writelines(items)
|
||||
@@ -132,8 +132,8 @@ class pluginInstaller:
|
||||
|
||||
@staticmethod
|
||||
def addInterfaceLink(pluginName):
|
||||
data = open("/usr/local/CyberCP/baseTemplate/templates/baseTemplate/index.html", 'r').readlines()
|
||||
writeToFile = open("/usr/local/CyberCP/baseTemplate/templates/baseTemplate/index.html", 'w')
|
||||
data = open("/usr/local/CyberCP/baseTemplate/templates/baseTemplate/index.html", 'r', encoding='utf-8').readlines()
|
||||
writeToFile = open("/usr/local/CyberCP/baseTemplate/templates/baseTemplate/index.html", 'w', encoding='utf-8')
|
||||
|
||||
for items in data:
|
||||
if items.find("{# pluginsList #}") > -1:
|
||||
@@ -155,7 +155,7 @@ class pluginInstaller:
|
||||
|
||||
os.chdir('/usr/local/CyberCP')
|
||||
|
||||
command = "/usr/local/CyberCP/bin/python manage.py collectstatic --noinput"
|
||||
command = "python3 /usr/local/CyberCP/manage.py collectstatic --noinput"
|
||||
subprocess.call(shlex.split(command))
|
||||
|
||||
command = "mv /usr/local/CyberCP/static /usr/local/lscp/cyberpanel"
|
||||
@@ -168,9 +168,9 @@ class pluginInstaller:
|
||||
def installMigrations(pluginName):
|
||||
currentDir = os.getcwd()
|
||||
os.chdir('/usr/local/CyberCP')
|
||||
command = "/usr/local/CyberCP/bin/python manage.py makemigrations %s" % pluginName
|
||||
command = "python3 /usr/local/CyberCP/manage.py makemigrations %s" % pluginName
|
||||
subprocess.call(shlex.split(command))
|
||||
command = "/usr/local/CyberCP/bin/python manage.py migrate %s" % pluginName
|
||||
command = "python3 /usr/local/CyberCP/manage.py migrate %s" % pluginName
|
||||
subprocess.call(shlex.split(command))
|
||||
os.chdir(currentDir)
|
||||
|
||||
@@ -285,16 +285,120 @@ class pluginInstaller:
|
||||
@staticmethod
|
||||
def removeFiles(pluginName):
|
||||
pluginPath = '/usr/local/CyberCP/' + pluginName
|
||||
if os.path.exists(pluginPath):
|
||||
shutil.rmtree(pluginPath)
|
||||
if not os.path.exists(pluginPath):
|
||||
# Directory doesn't exist - already removed
|
||||
pluginInstaller.stdOut(f'Plugin directory does not exist (already removed): {pluginName}')
|
||||
return
|
||||
|
||||
try:
|
||||
# Check if we're running as root
|
||||
is_root = os.geteuid() == 0 if hasattr(os, 'geteuid') else False
|
||||
use_sudo = not is_root
|
||||
|
||||
# First try: Use shutil.rmtree (works if permissions are correct)
|
||||
try:
|
||||
shutil.rmtree(pluginPath)
|
||||
pluginInstaller.stdOut(f'Plugin directory removed: {pluginName}')
|
||||
return
|
||||
except (OSError, PermissionError) as e:
|
||||
pluginInstaller.stdOut(f'Direct removal failed, trying with permission fix: {str(e)}')
|
||||
|
||||
# Second try: Fix permissions, then remove
|
||||
try:
|
||||
import subprocess
|
||||
import stat
|
||||
|
||||
if use_sudo:
|
||||
# Use ProcessUtilities which handles privileged commands
|
||||
# Fix ownership recursively
|
||||
chown_cmd = f'chown -R cyberpanel:cyberpanel {pluginPath}'
|
||||
ProcessUtilities.normalExecutioner(chown_cmd)
|
||||
|
||||
# Fix permissions recursively
|
||||
chmod_cmd = f'chmod -R u+rwX,go+rX {pluginPath}'
|
||||
ProcessUtilities.normalExecutioner(chmod_cmd)
|
||||
else:
|
||||
# Running as root - fix permissions directly
|
||||
import pwd
|
||||
import grp
|
||||
try:
|
||||
cyberpanel_uid = pwd.getpwnam('cyberpanel').pw_uid
|
||||
cyberpanel_gid = grp.getgrnam('cyberpanel').gr_gid
|
||||
except (KeyError, OSError):
|
||||
cyberpanel_uid = 0
|
||||
cyberpanel_gid = 0
|
||||
|
||||
# Recursively fix ownership and permissions
|
||||
for root, dirs, files in os.walk(pluginPath):
|
||||
try:
|
||||
os.chown(root, cyberpanel_uid, cyberpanel_gid)
|
||||
os.chmod(root, stat.S_IRWXU | stat.S_IRWXG | stat.S_IROTH | stat.S_IXOTH)
|
||||
except (OSError, PermissionError):
|
||||
pass
|
||||
|
||||
for d in dirs:
|
||||
dir_path = os.path.join(root, d)
|
||||
try:
|
||||
os.chown(dir_path, cyberpanel_uid, cyberpanel_gid)
|
||||
os.chmod(dir_path, stat.S_IRWXU | stat.S_IRWXG | stat.S_IROTH | stat.S_IXOTH)
|
||||
except (OSError, PermissionError):
|
||||
pass
|
||||
|
||||
for f in files:
|
||||
file_path = os.path.join(root, f)
|
||||
try:
|
||||
os.chown(file_path, cyberpanel_uid, cyberpanel_gid)
|
||||
os.chmod(file_path, stat.S_IRUSR | stat.S_IWUSR | stat.S_IRGRP | stat.S_IROTH)
|
||||
except (OSError, PermissionError):
|
||||
pass
|
||||
|
||||
# Now try to remove
|
||||
shutil.rmtree(pluginPath)
|
||||
pluginInstaller.stdOut(f'Plugin directory removed after permission fix: {pluginName}')
|
||||
return
|
||||
except Exception as e:
|
||||
pluginInstaller.stdOut(f'Permission fix and removal failed: {str(e)}')
|
||||
|
||||
# Third try: Use rm -rf (with or without sudo depending on privileges)
|
||||
try:
|
||||
if use_sudo:
|
||||
# Use ProcessUtilities for privileged removal
|
||||
rm_cmd = f'rm -rf {pluginPath}'
|
||||
ProcessUtilities.normalExecutioner(rm_cmd)
|
||||
else:
|
||||
# Running as root - use subprocess directly
|
||||
result = subprocess.run(
|
||||
['rm', '-rf', pluginPath],
|
||||
capture_output=True, text=True, timeout=30
|
||||
)
|
||||
if result.returncode != 0:
|
||||
raise Exception(f"rm -rf failed: {result.stderr}")
|
||||
|
||||
pluginInstaller.stdOut(f'Plugin directory removed using rm -rf: {pluginName}')
|
||||
return
|
||||
except Exception as e:
|
||||
raise Exception(f"All removal methods failed. Last error: {str(e)}")
|
||||
|
||||
except Exception as e:
|
||||
pluginInstaller.stdOut(f"Error removing plugin files: {str(e)}")
|
||||
raise Exception(f"Failed to remove plugin directory: {str(e)}")
|
||||
|
||||
@staticmethod
|
||||
def removeFromSettings(pluginName):
|
||||
data = open("/usr/local/CyberCP/CyberCP/settings.py", 'r').readlines()
|
||||
writeToFile = open("/usr/local/CyberCP/CyberCP/settings.py", 'w')
|
||||
|
||||
for items in data:
|
||||
if items.find(pluginName) > -1:
|
||||
data = open("/usr/local/CyberCP/CyberCP/settings.py", 'r', encoding='utf-8').readlines()
|
||||
writeToFile = open("/usr/local/CyberCP/CyberCP/settings.py", 'w', encoding='utf-8')
|
||||
|
||||
in_installed_apps = False
|
||||
for i, items in enumerate(data):
|
||||
# Track if we're in INSTALLED_APPS section
|
||||
if 'INSTALLED_APPS' in items and '=' in items:
|
||||
in_installed_apps = True
|
||||
elif in_installed_apps and items.strip().startswith(']'):
|
||||
in_installed_apps = False
|
||||
|
||||
# More precise matching: look for plugin name in quotes (e.g., 'pluginName' or "pluginName")
|
||||
# Only match if we're in INSTALLED_APPS section to prevent false positives
|
||||
if in_installed_apps and (f"'{pluginName}'" in items or f'"{pluginName}"' in items):
|
||||
continue
|
||||
else:
|
||||
writeToFile.writelines(items)
|
||||
@@ -302,11 +406,15 @@ class pluginInstaller:
|
||||
|
||||
@staticmethod
|
||||
def removeFromURLs(pluginName):
|
||||
data = open("/usr/local/CyberCP/CyberCP/urls.py", 'r').readlines()
|
||||
writeToFile = open("/usr/local/CyberCP/CyberCP/urls.py", 'w')
|
||||
data = open("/usr/local/CyberCP/CyberCP/urls.py", 'r', encoding='utf-8').readlines()
|
||||
writeToFile = open("/usr/local/CyberCP/CyberCP/urls.py", 'w', encoding='utf-8')
|
||||
|
||||
for items in data:
|
||||
if items.find(pluginName) > -1:
|
||||
# More precise matching: look for plugin name in path() or include() calls
|
||||
# Match patterns like: path('plugins/pluginName/', include('pluginName.urls'))
|
||||
# This prevents partial matches
|
||||
if (f"plugins/{pluginName}/" in items or f"'{pluginName}.urls'" in items or f'"{pluginName}.urls"' in items or
|
||||
f"include('{pluginName}.urls')" in items or f'include("{pluginName}.urls")' in items):
|
||||
continue
|
||||
else:
|
||||
writeToFile.writelines(items)
|
||||
@@ -322,8 +430,8 @@ class pluginInstaller:
|
||||
|
||||
@staticmethod
|
||||
def removeInterfaceLink(pluginName):
|
||||
data = open("/usr/local/CyberCP/baseTemplate/templates/baseTemplate/index.html", 'r').readlines()
|
||||
writeToFile = open("/usr/local/CyberCP/baseTemplate/templates/baseTemplate/index.html", 'w')
|
||||
data = open("/usr/local/CyberCP/baseTemplate/templates/baseTemplate/index.html", 'r', encoding='utf-8').readlines()
|
||||
writeToFile = open("/usr/local/CyberCP/baseTemplate/templates/baseTemplate/index.html", 'w', encoding='utf-8')
|
||||
|
||||
for items in data:
|
||||
if items.find(pluginName) > -1 and items.find('<li>') > -1:
|
||||
@@ -336,7 +444,7 @@ class pluginInstaller:
|
||||
def removeMigrations(pluginName):
|
||||
currentDir = os.getcwd()
|
||||
os.chdir('/usr/local/CyberCP')
|
||||
command = "/usr/local/CyberCP/bin/python manage.py migrate %s zero" % pluginName
|
||||
command = "python3 /usr/local/CyberCP/manage.py migrate %s zero" % pluginName
|
||||
subprocess.call(shlex.split(command))
|
||||
os.chdir(currentDir)
|
||||
|
||||
|
||||
269
premiumPlugin/views.py
Normal file
269
premiumPlugin/views.py
Normal file
@@ -0,0 +1,269 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
Premium Plugin Views - Remote Verification Version
|
||||
This version uses remote server verification (no secrets in plugin)
|
||||
SECURITY: All Patreon API calls happen on YOUR server, not user's server
|
||||
"""
|
||||
|
||||
from django.shortcuts import render, redirect
|
||||
from django.http import JsonResponse
|
||||
from plogical.mailUtilities import mailUtilities
|
||||
from plogical.httpProc import httpProc
|
||||
from functools import wraps
|
||||
import sys
|
||||
import os
|
||||
import urllib.request
|
||||
import urllib.error
|
||||
import json
|
||||
|
||||
# Remote verification server (YOUR server, not user's server)
|
||||
REMOTE_VERIFICATION_URL = 'https://api.newstargeted.com/api/verify-patreon-membership'
|
||||
PLUGIN_NAME = 'premiumPlugin' # Patreon Premium Plugin Example
|
||||
PLUGIN_VERSION = '1.0.0'
|
||||
|
||||
def cyberpanel_login_required(view_func):
|
||||
"""
|
||||
Custom decorator that checks for CyberPanel session userID
|
||||
"""
|
||||
@wraps(view_func)
|
||||
def _wrapped_view(request, *args, **kwargs):
|
||||
try:
|
||||
userID = request.session['userID']
|
||||
# User is authenticated via CyberPanel session
|
||||
return view_func(request, *args, **kwargs)
|
||||
except KeyError:
|
||||
# Not logged in, redirect to login
|
||||
from loginSystem.views import loadLoginPage
|
||||
return redirect(loadLoginPage)
|
||||
return _wrapped_view
|
||||
|
||||
def remote_verification_required(view_func):
|
||||
"""
|
||||
Decorator that checks Patreon membership via remote server
|
||||
No secrets stored in plugin - all verification happens on your server
|
||||
"""
|
||||
@wraps(view_func)
|
||||
def _wrapped_view(request, *args, **kwargs):
|
||||
# First check login
|
||||
try:
|
||||
userID = request.session['userID']
|
||||
except KeyError:
|
||||
from loginSystem.views import loadLoginPage
|
||||
return redirect(loadLoginPage)
|
||||
|
||||
# Get user email
|
||||
user_email = getattr(request.user, 'email', None) if hasattr(request, 'user') and request.user else None
|
||||
if not user_email:
|
||||
# Try to get from session or username
|
||||
user_email = request.session.get('email', '') or getattr(request.user, 'username', '')
|
||||
|
||||
# Check membership via remote server
|
||||
verification_result = check_remote_membership(user_email, request.META.get('REMOTE_ADDR', ''))
|
||||
|
||||
if not verification_result.get('has_access', False):
|
||||
# User doesn't have subscription - show subscription required page
|
||||
context = {
|
||||
'plugin_name': 'Patreon Premium Plugin Example',
|
||||
'is_paid': True,
|
||||
'patreon_tier': verification_result.get('patreon_tier', 'CyberPanel Paid Plugin'),
|
||||
'patreon_url': verification_result.get('patreon_url', 'https://www.patreon.com/c/newstargeted/membership'),
|
||||
'message': verification_result.get('message', 'Patreon subscription required'),
|
||||
'error': verification_result.get('error')
|
||||
}
|
||||
proc = httpProc(request, 'premiumPlugin/subscription_required.html', context, 'admin')
|
||||
return proc.render()
|
||||
|
||||
# User has access - proceed with view
|
||||
return view_func(request, *args, **kwargs)
|
||||
|
||||
return _wrapped_view
|
||||
|
||||
def check_remote_membership(user_email, user_ip=''):
|
||||
"""
|
||||
Check Patreon membership via remote verification server
|
||||
|
||||
Args:
|
||||
user_email: User's email address
|
||||
user_ip: User's IP address (for logging/security)
|
||||
|
||||
Returns:
|
||||
dict: {
|
||||
'has_access': bool,
|
||||
'patreon_tier': str,
|
||||
'patreon_url': str,
|
||||
'message': str,
|
||||
'error': str or None
|
||||
}
|
||||
"""
|
||||
try:
|
||||
# Prepare request data
|
||||
request_data = {
|
||||
'user_email': user_email,
|
||||
'plugin_name': PLUGIN_NAME,
|
||||
'plugin_version': PLUGIN_VERSION,
|
||||
'user_ip': user_ip,
|
||||
'tier_id': '27789984' # CyberPanel Paid Plugin tier ID
|
||||
}
|
||||
|
||||
# Make request to remote verification server
|
||||
req = urllib.request.Request(
|
||||
REMOTE_VERIFICATION_URL,
|
||||
data=json.dumps(request_data).encode('utf-8'),
|
||||
headers={
|
||||
'Content-Type': 'application/json',
|
||||
'User-Agent': f'CyberPanel-Plugin/{PLUGIN_VERSION}',
|
||||
'X-Plugin-Name': PLUGIN_NAME
|
||||
}
|
||||
)
|
||||
|
||||
# Send request with timeout
|
||||
try:
|
||||
with urllib.request.urlopen(req, timeout=10) as response:
|
||||
response_data = json.loads(response.read().decode('utf-8'))
|
||||
|
||||
if response_data.get('success', False):
|
||||
return {
|
||||
'has_access': response_data.get('has_access', False),
|
||||
'patreon_tier': response_data.get('patreon_tier', 'CyberPanel Paid Plugin'),
|
||||
'patreon_url': response_data.get('patreon_url', 'https://www.patreon.com/c/newstargeted/membership'),
|
||||
'message': response_data.get('message', 'Access granted'),
|
||||
'error': None
|
||||
}
|
||||
else:
|
||||
return {
|
||||
'has_access': False,
|
||||
'patreon_tier': response_data.get('patreon_tier', 'CyberPanel Paid Plugin'),
|
||||
'patreon_url': response_data.get('patreon_url', 'https://www.patreon.com/c/newstargeted/membership'),
|
||||
'message': response_data.get('message', 'Patreon subscription required'),
|
||||
'error': response_data.get('error')
|
||||
}
|
||||
except urllib.error.HTTPError as e:
|
||||
# Server returned error
|
||||
error_body = e.read().decode('utf-8') if e.fp else 'Unknown error'
|
||||
return {
|
||||
'has_access': False,
|
||||
'patreon_tier': 'CyberPanel Paid Plugin',
|
||||
'patreon_url': 'https://www.patreon.com/c/newstargeted/membership',
|
||||
'message': 'Unable to verify subscription. Please try again later.',
|
||||
'error': f'HTTP {e.code}: {error_body}'
|
||||
}
|
||||
except urllib.error.URLError as e:
|
||||
# Network error
|
||||
return {
|
||||
'has_access': False,
|
||||
'patreon_tier': 'CyberPanel Paid Plugin',
|
||||
'patreon_url': 'https://www.patreon.com/c/newstargeted/membership',
|
||||
'message': 'Unable to connect to verification server. Please check your internet connection.',
|
||||
'error': str(e.reason) if hasattr(e, 'reason') else str(e)
|
||||
}
|
||||
except Exception as e:
|
||||
# Other errors
|
||||
return {
|
||||
'has_access': False,
|
||||
'patreon_tier': 'CyberPanel Paid Plugin',
|
||||
'patreon_url': 'https://www.patreon.com/c/newstargeted/membership',
|
||||
'message': 'Verification error occurred. Please try again later.',
|
||||
'error': str(e)
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
import logging
|
||||
logging.writeToFile(f"Error in remote membership check: {str(e)}")
|
||||
return {
|
||||
'has_access': False,
|
||||
'patreon_tier': 'CyberPanel Paid Plugin',
|
||||
'patreon_url': 'https://www.patreon.com/c/newstargeted/membership',
|
||||
'message': 'Verification error occurred. Please try again later.',
|
||||
'error': str(e)
|
||||
}
|
||||
|
||||
@cyberpanel_login_required
|
||||
def main_view(request):
|
||||
"""
|
||||
Main view for premium plugin
|
||||
Shows plugin information and features if subscribed, or subscription required message if not
|
||||
"""
|
||||
mailUtilities.checkHome()
|
||||
|
||||
# Get user email for verification
|
||||
user_email = getattr(request.user, 'email', None) if hasattr(request, 'user') and request.user else None
|
||||
if not user_email:
|
||||
user_email = request.session.get('email', '') or getattr(request.user, 'username', '')
|
||||
|
||||
# Check membership status (but don't block access)
|
||||
verification_result = check_remote_membership(user_email, request.META.get('REMOTE_ADDR', ''))
|
||||
has_access = verification_result.get('has_access', False)
|
||||
|
||||
# Determine plugin status
|
||||
plugin_status = 'Active' if has_access else 'Subscription Required'
|
||||
|
||||
context = {
|
||||
'plugin_name': 'Patreon Premium Plugin Example',
|
||||
'version': PLUGIN_VERSION,
|
||||
'status': plugin_status,
|
||||
'has_access': has_access,
|
||||
'description': 'This is an example paid plugin that requires Patreon subscription.' if not has_access else 'This is an example paid plugin. You have access because you are subscribed to Patreon!',
|
||||
'patreon_tier': verification_result.get('patreon_tier', 'CyberPanel Paid Plugin'),
|
||||
'patreon_url': verification_result.get('patreon_url', 'https://www.patreon.com/membership/27789984'),
|
||||
'features': [
|
||||
'Premium Feature 1',
|
||||
'Premium Feature 2',
|
||||
'Premium Feature 3',
|
||||
'Advanced Configuration',
|
||||
'Priority Support'
|
||||
] if has_access else []
|
||||
}
|
||||
|
||||
proc = httpProc(request, 'premiumPlugin/index.html', context, 'admin')
|
||||
return proc.render()
|
||||
|
||||
@cyberpanel_login_required
|
||||
def settings_view(request):
|
||||
"""
|
||||
Settings page for premium plugin
|
||||
Shows settings but disables them if user doesn't have Patreon subscription
|
||||
"""
|
||||
mailUtilities.checkHome()
|
||||
|
||||
# Get user email for verification
|
||||
user_email = getattr(request.user, 'email', None) if hasattr(request, 'user') and request.user else None
|
||||
if not user_email:
|
||||
user_email = request.session.get('email', '') or getattr(request.user, 'username', '')
|
||||
|
||||
# Check membership status (but don't block access)
|
||||
verification_result = check_remote_membership(user_email, request.META.get('REMOTE_ADDR', ''))
|
||||
has_access = verification_result.get('has_access', False)
|
||||
|
||||
# Determine plugin status
|
||||
plugin_status = 'Active' if has_access else 'Subscription Required'
|
||||
|
||||
context = {
|
||||
'plugin_name': 'Patreon Premium Plugin Example',
|
||||
'version': PLUGIN_VERSION,
|
||||
'plugin_status': plugin_status,
|
||||
'status': plugin_status, # Keep both for compatibility
|
||||
'description': 'Configure your premium plugin settings',
|
||||
'has_access': has_access,
|
||||
'patreon_tier': verification_result.get('patreon_tier', 'CyberPanel Paid Plugin'),
|
||||
'patreon_url': verification_result.get('patreon_url', 'https://www.patreon.com/membership/27789984'),
|
||||
'verification_message': verification_result.get('message', '')
|
||||
}
|
||||
|
||||
proc = httpProc(request, 'premiumPlugin/settings.html', context, 'admin')
|
||||
return proc.render()
|
||||
|
||||
@cyberpanel_login_required
|
||||
@remote_verification_required
|
||||
def api_status_view(request):
|
||||
"""
|
||||
API endpoint for plugin status
|
||||
Only accessible with Patreon subscription (verified remotely)
|
||||
"""
|
||||
return JsonResponse({
|
||||
'plugin_name': 'Patreon Premium Plugin Example',
|
||||
'version': PLUGIN_VERSION,
|
||||
'status': 'active',
|
||||
'subscription': 'active',
|
||||
'description': 'Premium plugin is active and accessible',
|
||||
'verification_method': 'remote'
|
||||
})
|
||||
File diff suppressed because it is too large
Load Diff
@@ -729,6 +729,59 @@ app.controller('listContainers', function ($scope, $http) {
|
||||
$scope.assignActive = "";
|
||||
$scope.dockerOwner = "";
|
||||
|
||||
/* Pagination (ACTIVITY BOARD-style) */
|
||||
var CONTAINERS_PER_PAGE = 10;
|
||||
$scope.pagination = { containers: { currentPage: 1, itemsPerPage: CONTAINERS_PER_PAGE } };
|
||||
$scope.gotoPageInput = { containers: 1 };
|
||||
$scope.totalCount = 0;
|
||||
$scope.totalPages = 1;
|
||||
$scope.Math = Math;
|
||||
|
||||
$scope.getTotalPages = function(section) {
|
||||
if (section === 'containers') return Math.max(1, $scope.totalPages || 1);
|
||||
return 1;
|
||||
};
|
||||
|
||||
$scope.goToPage = function(section, page) {
|
||||
if (section !== 'containers') return;
|
||||
var totalPages = $scope.getTotalPages(section);
|
||||
var p = parseInt(page, 10);
|
||||
if (isNaN(p) || p < 1) p = 1;
|
||||
if (p > totalPages) p = totalPages;
|
||||
$scope.getFurtherContainersFromDB(p);
|
||||
};
|
||||
|
||||
$scope.nextPage = function(section) {
|
||||
if (section !== 'containers') return;
|
||||
if ($scope.pagination.containers.currentPage < $scope.getTotalPages(section)) {
|
||||
$scope.getFurtherContainersFromDB($scope.pagination.containers.currentPage + 1);
|
||||
}
|
||||
};
|
||||
|
||||
$scope.prevPage = function(section) {
|
||||
if (section !== 'containers') return;
|
||||
if ($scope.pagination.containers.currentPage > 1) {
|
||||
$scope.getFurtherContainersFromDB($scope.pagination.containers.currentPage - 1);
|
||||
}
|
||||
};
|
||||
|
||||
$scope.getPageNumbers = function(section) {
|
||||
if (section !== 'containers') return [];
|
||||
var totalPages = $scope.getTotalPages(section);
|
||||
var current = $scope.pagination.containers.currentPage;
|
||||
var maxVisible = 5;
|
||||
var pages = [];
|
||||
if (totalPages <= maxVisible) {
|
||||
for (var i = 1; i <= totalPages; i++) pages.push(i);
|
||||
} else {
|
||||
var start = Math.max(1, current - 2);
|
||||
var end = Math.min(totalPages, start + maxVisible - 1);
|
||||
if (end - start + 1 < maxVisible) start = Math.max(1, end - maxVisible + 1);
|
||||
for (var j = start; j <= end; j++) pages.push(j);
|
||||
}
|
||||
return pages;
|
||||
};
|
||||
|
||||
$scope.assignContainer = function (name) {
|
||||
console.log('assignContainer called with:', name);
|
||||
$scope.assignActive = name;
|
||||
@@ -783,7 +836,7 @@ app.controller('listContainers', function ($scope, $http) {
|
||||
title: 'Container assigned successfully',
|
||||
type: 'success'
|
||||
});
|
||||
window.location.href = '/docker/listContainers';
|
||||
window.location.href = '/docker/containers';
|
||||
}
|
||||
else {
|
||||
new PNotify({
|
||||
@@ -1264,77 +1317,35 @@ app.controller('listContainers', function ($scope, $http) {
|
||||
$scope.logInfo = null;
|
||||
};
|
||||
|
||||
url = "/docker/getContainerList";
|
||||
|
||||
var data = {page: 1};
|
||||
|
||||
var config = {
|
||||
headers: {
|
||||
'X-CSRFToken': getCookie('csrftoken')
|
||||
}
|
||||
};
|
||||
|
||||
$http.post(url, data, config).then(ListInitialData, cantLoadInitialData);
|
||||
|
||||
|
||||
function ListInitialData(response) {
|
||||
console.log(response);
|
||||
|
||||
function handleContainerListResponse(response) {
|
||||
if (response.data.listContainerStatus === 1) {
|
||||
|
||||
var finalData = JSON.parse(response.data.data);
|
||||
$scope.ContainerList = finalData;
|
||||
console.log($scope.ContainerList);
|
||||
$scope.totalCount = response.data.totalCount || 0;
|
||||
$scope.totalPages = Math.max(1, response.data.totalPages || 1);
|
||||
var cp = Math.max(1, parseInt(response.data.currentPage, 10) || 1);
|
||||
$scope.pagination.containers.currentPage = cp;
|
||||
$scope.gotoPageInput.containers = cp;
|
||||
$("#listFail").hide();
|
||||
}
|
||||
else {
|
||||
} else {
|
||||
$("#listFail").fadeIn();
|
||||
$scope.errorMessage = response.data.error_message;
|
||||
|
||||
$scope.errorMessage = response.data.error_message || 'Failed to load containers';
|
||||
}
|
||||
}
|
||||
|
||||
function cantLoadInitialData(response) {
|
||||
console.log("not good");
|
||||
$("#listFail").fadeIn();
|
||||
$scope.errorMessage = (response && response.data && response.data.error_message) ? response.data.error_message : 'Could not connect to server';
|
||||
}
|
||||
|
||||
var config = {
|
||||
headers: { 'X-CSRFToken': getCookie('csrftoken') }
|
||||
};
|
||||
$http.post("/docker/getContainerList", { page: 1 }, config).then(handleContainerListResponse, cantLoadInitialData);
|
||||
|
||||
$scope.getFurtherContainersFromDB = function (pageNumber) {
|
||||
|
||||
var config = {
|
||||
headers: {
|
||||
'X-CSRFToken': getCookie('csrftoken')
|
||||
}
|
||||
};
|
||||
|
||||
var data = {page: pageNumber};
|
||||
|
||||
|
||||
dataurl = "/docker/getContainerList";
|
||||
|
||||
$http.post(dataurl, data, config).then(ListInitialData, cantLoadInitialData);
|
||||
|
||||
|
||||
function ListInitialData(response) {
|
||||
if (response.data.listContainerStatus === 1) {
|
||||
|
||||
var finalData = JSON.parse(response.data.data);
|
||||
$scope.ContainerList = finalData;
|
||||
$("#listFail").hide();
|
||||
}
|
||||
else {
|
||||
$("#listFail").fadeIn();
|
||||
$scope.errorMessage = response.data.error_message;
|
||||
console.log(response.data);
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
function cantLoadInitialData(response) {
|
||||
console.log("not good");
|
||||
}
|
||||
|
||||
|
||||
var p = Math.max(1, parseInt(pageNumber, 10) || 1);
|
||||
$http.post("/docker/getContainerList", { page: p }, config).then(handleContainerListResponse, cantLoadInitialData);
|
||||
};
|
||||
});
|
||||
|
||||
@@ -1906,7 +1917,7 @@ app.controller('viewContainer', function ($scope, $http, $interval, $timeout) {
|
||||
text: 'Redirecting...',
|
||||
type: 'success'
|
||||
});
|
||||
window.location.href = '/docker/listContainers';
|
||||
window.location.href = '/docker/containers';
|
||||
}
|
||||
else {
|
||||
new PNotify({
|
||||
@@ -2703,257 +2714,3 @@ app.controller('manageImages', function ($scope, $http) {
|
||||
}
|
||||
});
|
||||
|
||||
// Container List Controller
|
||||
app.controller('listContainers', function ($scope, $http, $timeout, $window) {
|
||||
$scope.containers = [];
|
||||
$scope.loading = false;
|
||||
$scope.updateContainerName = '';
|
||||
$scope.currentImage = '';
|
||||
$scope.newImage = '';
|
||||
$scope.newTag = 'latest';
|
||||
|
||||
// Load containers list
|
||||
$scope.loadContainers = function() {
|
||||
$scope.loading = true;
|
||||
var url = '/docker/listContainers';
|
||||
var config = {
|
||||
headers: {
|
||||
'X-CSRFToken': getCookie('csrftoken')
|
||||
}
|
||||
};
|
||||
|
||||
$http.post(url, {}, config).then(function(response) {
|
||||
$scope.loading = false;
|
||||
if (response.data.status === 1) {
|
||||
$scope.containers = response.data.containers || [];
|
||||
} else {
|
||||
new PNotify({
|
||||
title: 'Error Loading Containers',
|
||||
text: response.data.error_message || 'Failed to load containers',
|
||||
type: 'error'
|
||||
});
|
||||
}
|
||||
}, function(error) {
|
||||
$scope.loading = false;
|
||||
new PNotify({
|
||||
title: 'Connection Error',
|
||||
text: 'Could not connect to server',
|
||||
type: 'error'
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
// Initialize containers on page load
|
||||
$scope.loadContainers();
|
||||
|
||||
// Open update container modal
|
||||
$scope.openUpdateModal = function(container) {
|
||||
$scope.updateContainerName = container.name;
|
||||
$scope.currentImage = container.image;
|
||||
$scope.newImage = '';
|
||||
$scope.newTag = 'latest';
|
||||
$('#updateContainer').modal('show');
|
||||
};
|
||||
|
||||
// Perform container update
|
||||
$scope.performUpdate = function() {
|
||||
if (!$scope.newImage && !$scope.newTag) {
|
||||
new PNotify({
|
||||
title: 'Missing Information',
|
||||
text: 'Please enter a new image name or tag',
|
||||
type: 'error'
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// If no new image specified, use current image with new tag
|
||||
var imageToUse = $scope.newImage || $scope.currentImage.split(':')[0];
|
||||
var tagToUse = $scope.newTag || 'latest';
|
||||
|
||||
var data = {
|
||||
containerName: $scope.updateContainerName,
|
||||
newImage: imageToUse,
|
||||
newTag: tagToUse
|
||||
};
|
||||
|
||||
var url = '/docker/updateContainer';
|
||||
var config = {
|
||||
headers: {
|
||||
'X-CSRFToken': getCookie('csrftoken')
|
||||
}
|
||||
};
|
||||
|
||||
// Show loading
|
||||
$('#updateContainer').modal('hide');
|
||||
new PNotify({
|
||||
title: 'Updating Container',
|
||||
text: 'Please wait while the container is being updated...',
|
||||
type: 'info',
|
||||
hide: false
|
||||
});
|
||||
|
||||
$http.post(url, data, config).then(function(response) {
|
||||
if (response.data.updateContainerStatus === 1) {
|
||||
new PNotify({
|
||||
title: 'Container Updated Successfully',
|
||||
text: response.data.message || 'Container has been updated successfully',
|
||||
type: 'success'
|
||||
});
|
||||
// Reload containers list
|
||||
$scope.loadContainers();
|
||||
} else {
|
||||
new PNotify({
|
||||
title: 'Update Failed',
|
||||
text: response.data.error_message || 'Failed to update container',
|
||||
type: 'error'
|
||||
});
|
||||
}
|
||||
}, function(error) {
|
||||
new PNotify({
|
||||
title: 'Update Failed',
|
||||
text: 'Could not connect to server',
|
||||
type: 'error'
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
// Container actions
|
||||
$scope.startContainer = function(containerName) {
|
||||
var data = { containerName: containerName };
|
||||
var url = '/docker/startContainer';
|
||||
var config = {
|
||||
headers: {
|
||||
'X-CSRFToken': getCookie('csrftoken')
|
||||
}
|
||||
};
|
||||
|
||||
$http.post(url, data, config).then(function(response) {
|
||||
if (response.data.startContainerStatus === 1) {
|
||||
new PNotify({
|
||||
title: 'Container Started',
|
||||
text: 'Container has been started successfully',
|
||||
type: 'success'
|
||||
});
|
||||
$scope.loadContainers();
|
||||
} else {
|
||||
new PNotify({
|
||||
title: 'Start Failed',
|
||||
text: response.data.error_message || 'Failed to start container',
|
||||
type: 'error'
|
||||
});
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
$scope.stopContainer = function(containerName) {
|
||||
var data = { containerName: containerName };
|
||||
var url = '/docker/stopContainer';
|
||||
var config = {
|
||||
headers: {
|
||||
'X-CSRFToken': getCookie('csrftoken')
|
||||
}
|
||||
};
|
||||
|
||||
$http.post(url, data, config).then(function(response) {
|
||||
if (response.data.stopContainerStatus === 1) {
|
||||
new PNotify({
|
||||
title: 'Container Stopped',
|
||||
text: 'Container has been stopped successfully',
|
||||
type: 'success'
|
||||
});
|
||||
$scope.loadContainers();
|
||||
} else {
|
||||
new PNotify({
|
||||
title: 'Stop Failed',
|
||||
text: response.data.error_message || 'Failed to stop container',
|
||||
type: 'error'
|
||||
});
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
$scope.restartContainer = function(containerName) {
|
||||
var data = { containerName: containerName };
|
||||
var url = '/docker/restartContainer';
|
||||
var config = {
|
||||
headers: {
|
||||
'X-CSRFToken': getCookie('csrftoken')
|
||||
}
|
||||
};
|
||||
|
||||
$http.post(url, data, config).then(function(response) {
|
||||
if (response.data.restartContainerStatus === 1) {
|
||||
new PNotify({
|
||||
title: 'Container Restarted',
|
||||
text: 'Container has been restarted successfully',
|
||||
type: 'success'
|
||||
});
|
||||
$scope.loadContainers();
|
||||
} else {
|
||||
new PNotify({
|
||||
title: 'Restart Failed',
|
||||
text: response.data.error_message || 'Failed to restart container',
|
||||
type: 'error'
|
||||
});
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
$scope.deleteContainerWithData = function(containerName) {
|
||||
if (confirm('Are you sure you want to delete this container and all its data? This action cannot be undone.')) {
|
||||
var data = { containerName: containerName };
|
||||
var url = '/docker/deleteContainerWithData';
|
||||
var config = {
|
||||
headers: {
|
||||
'X-CSRFToken': getCookie('csrftoken')
|
||||
}
|
||||
};
|
||||
|
||||
$http.post(url, data, config).then(function(response) {
|
||||
if (response.data.deleteContainerWithDataStatus === 1) {
|
||||
new PNotify({
|
||||
title: 'Container Deleted',
|
||||
text: 'Container and all data have been deleted successfully',
|
||||
type: 'success'
|
||||
});
|
||||
$scope.loadContainers();
|
||||
} else {
|
||||
new PNotify({
|
||||
title: 'Delete Failed',
|
||||
text: response.data.error_message || 'Failed to delete container',
|
||||
type: 'error'
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
$scope.deleteContainerKeepData = function(containerName) {
|
||||
if (confirm('Are you sure you want to delete this container but keep the data? The container will be removed but volumes will be preserved.')) {
|
||||
var data = { containerName: containerName };
|
||||
var url = '/docker/deleteContainerKeepData';
|
||||
var config = {
|
||||
headers: {
|
||||
'X-CSRFToken': getCookie('csrftoken')
|
||||
}
|
||||
};
|
||||
|
||||
$http.post(url, data, config).then(function(response) {
|
||||
if (response.data.deleteContainerKeepDataStatus === 1) {
|
||||
new PNotify({
|
||||
title: 'Container Deleted',
|
||||
text: 'Container has been deleted but data has been preserved',
|
||||
type: 'success'
|
||||
});
|
||||
$scope.loadContainers();
|
||||
} else {
|
||||
new PNotify({
|
||||
title: 'Delete Failed',
|
||||
text: response.data.error_message || 'Failed to delete container',
|
||||
type: 'error'
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
});
|
||||
@@ -36,7 +36,8 @@ from math import ceil
|
||||
from plogical.alias import AliasManager
|
||||
from plogical.applicationInstaller import ApplicationInstaller
|
||||
from plogical import hashPassword, randomPassword
|
||||
from emailMarketing.emACL import emACL
|
||||
# emailMarketing removed from INSTALLED_APPS
|
||||
# from emailMarketing.emACL import emACL
|
||||
from plogical.processUtilities import ProcessUtilities
|
||||
from managePHP.phpManager import PHPManager
|
||||
from ApachController.ApacheVhosts import ApacheVhost
|
||||
@@ -3729,7 +3730,9 @@ context /cyberpanel_suspension_page.html {
|
||||
|
||||
from plogical.processUtilities import ProcessUtilities
|
||||
|
||||
marketingStatus = emACL.checkIfEMEnabled(admin.userName)
|
||||
# emailMarketing removed - always return False for marketing status
|
||||
# marketingStatus = emACL.checkIfEMEnabled(admin.userName)
|
||||
marketingStatus = False
|
||||
|
||||
Data['marketingStatus'] = marketingStatus
|
||||
Data['ftpTotal'] = website.package.ftpAccounts
|
||||
|
||||
Reference in New Issue
Block a user