mirror of
https://github.com/usmannasir/cyberpanel.git
synced 2026-01-31 03:39:06 +01:00
Update pluginHolder with Free/Paid badges and Plugin Information support
- Added Free/Paid badges to Grid View, Table View, and Plugin Store - Fixed intermittent badge display issues with robust boolean handling - Updated plugin store to show plugin icons and proper pricing badges - Removed Deactivate/Uninstall from Plugin Store (only Install/Installed) - Fixed template syntax errors and duplicate navigation buttons - Enhanced cache handling for plugin metadata (is_paid, patreon_url, etc.) - Improved JavaScript cache-busting and isPaid normalization
This commit is contained in:
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);
|
||||
@@ -816,20 +913,35 @@
|
||||
<div class="content-section">
|
||||
<h2 class="section-title">{% trans "Plugins" %}</h2>
|
||||
|
||||
<!-- 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')">
|
||||
<!-- View Toggle (always shown) -->
|
||||
<div class="view-toggle">
|
||||
{% if plugins %}
|
||||
<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>
|
||||
{% else %}
|
||||
<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>
|
||||
{% endif %}
|
||||
<a href="/plugins/help/" class="view-btn" style="text-decoration: none;">
|
||||
<i class="fas fa-book"></i>
|
||||
{% trans "Plugin Development Guide" %}
|
||||
@@ -837,6 +949,7 @@
|
||||
</div>
|
||||
|
||||
{% if plugins %}
|
||||
|
||||
<!-- Grid View -->
|
||||
<div id="gridView" class="plugins-grid">
|
||||
{% for plugin in plugins %}
|
||||
@@ -856,22 +969,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-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 +1069,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 +1084,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);">
|
||||
@@ -1097,13 +1222,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 +1249,8 @@
|
||||
</div>
|
||||
|
||||
<script>
|
||||
// Cache-busting version: 2026-01-25-v5 - Fixed is_paid boolean type enforcement and consistent rendering
|
||||
// Force browser to reload this script by changing version number
|
||||
let storePlugins = [];
|
||||
let currentFilter = 'all';
|
||||
|
||||
@@ -1148,9 +1274,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 +1347,17 @@ function escapeHtml(text) {
|
||||
}
|
||||
|
||||
function displayStorePlugins() {
|
||||
// Version: 2026-01-25-v5 - Store view: Removed Status column, always show Free/Paid badges, fixed boolean handling
|
||||
// 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 +1369,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 +1416,34 @@ 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-v5 - Normalize is_paid to handle all possible values, force boolean
|
||||
let isPaid = false;
|
||||
if (plugin.is_paid !== undefined && plugin.is_paid !== null) {
|
||||
const isPaidValue = plugin.is_paid;
|
||||
// Handle all possible true values (boolean, string, number)
|
||||
if (isPaidValue === true || isPaidValue === 'true' || isPaidValue === 'True' || isPaidValue === 1 || isPaidValue === '1' || String(isPaidValue).toLowerCase() === 'true') {
|
||||
isPaid = true;
|
||||
} else {
|
||||
isPaid = false; // Explicitly set to false for any other value
|
||||
}
|
||||
}
|
||||
// Force boolean type
|
||||
isPaid = Boolean(isPaid);
|
||||
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 +1832,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'
|
||||
@@ -135,6 +136,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,6 +187,27 @@ 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')
|
||||
|
||||
# CRITICAL: Always explicitly set is_paid as boolean True/False
|
||||
if paid_elem is not None and paid_elem.text and str(paid_elem.text).strip().lower() == 'true':
|
||||
data['is_paid'] = True # Explicit boolean 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 # Explicit boolean False
|
||||
data['patreon_tier'] = None
|
||||
data['patreon_url'] = None
|
||||
|
||||
# Force boolean type (defensive programming) - CRITICAL: Always ensure boolean
|
||||
data['is_paid'] = bool(data['is_paid']) if 'is_paid' in data else False
|
||||
|
||||
# Final safety check - ensure is_paid exists and is boolean before adding to list
|
||||
if 'is_paid' not in data or not isinstance(data['is_paid'], bool):
|
||||
data['is_paid'] = False
|
||||
|
||||
pluginList.append(data)
|
||||
processed_plugins.add(plugin) # Mark as processed
|
||||
@@ -236,6 +263,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 +308,26 @@ 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')
|
||||
|
||||
# CRITICAL: Always explicitly set is_paid as boolean True/False
|
||||
if paid_elem is not None and paid_elem.text and str(paid_elem.text).strip().lower() == 'true':
|
||||
data['is_paid'] = True # Explicit boolean 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:
|
||||
data['is_paid'] = False # Explicit boolean False
|
||||
|
||||
# Force boolean type (defensive programming) - CRITICAL: Always ensure boolean
|
||||
data['is_paid'] = bool(data['is_paid']) if 'is_paid' in data else False
|
||||
|
||||
# Final safety check - ensure is_paid exists and is boolean before adding to list
|
||||
if 'is_paid' not in data or not isinstance(data['is_paid'], bool):
|
||||
data['is_paid'] = False
|
||||
|
||||
pluginList.append(data)
|
||||
|
||||
except ElementTree.ParseError as e:
|
||||
@@ -315,11 +367,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 +406,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
|
||||
@@ -562,6 +648,55 @@ 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)
|
||||
# CRITICAL FIX: Always check local meta.xml FIRST as source of truth
|
||||
# This ensures cache entries without is_paid are properly enriched
|
||||
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 we have a local meta.xml, use it as the source of truth (most reliable)
|
||||
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
|
||||
# Also update patreon fields if available
|
||||
patreon_tier_elem = root.find('patreon_tier')
|
||||
if patreon_tier_elem is not None and patreon_tier_elem.text:
|
||||
plugin['patreon_tier'] = patreon_tier_elem.text
|
||||
patreon_url_elem = root.find('patreon_url')
|
||||
if patreon_url_elem is not None and patreon_url_elem.text:
|
||||
plugin['patreon_url'] = patreon_url_elem.text
|
||||
else:
|
||||
plugin['is_paid'] = False
|
||||
except Exception as e:
|
||||
logging.writeToFile(f"Error parsing meta.xml for {plugin_dir} in _enrich_store_plugins: {str(e)}")
|
||||
# Fall back to normalizing existing value
|
||||
is_paid_value = plugin.get('is_paid', False)
|
||||
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
|
||||
else:
|
||||
plugin['is_paid'] = False
|
||||
else:
|
||||
# No local meta.xml, normalize existing value from cache/API
|
||||
is_paid_value = plugin.get('is_paid', False)
|
||||
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
|
||||
else:
|
||||
plugin['is_paid'] = False # Default to free if we can't determine
|
||||
|
||||
# Ensure it's a proper boolean (not string or other type)
|
||||
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 +768,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 +793,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 +898,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 +923,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 +1017,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 +1266,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)
|
||||
|
||||
Reference in New Issue
Block a user