mirror of
https://github.com/usmannasir/cyberpanel.git
synced 2026-05-07 04:26:51 +02:00
pluginHolder: persist premium activation keys in MariaDB.
Store plugin activation entitlements in DB and use them in access checks so upgrades do not relock premium plugins.
This commit is contained in:
@@ -3,4 +3,22 @@
|
||||
|
||||
from django.db import models
|
||||
|
||||
# Create your models here.
|
||||
|
||||
class PluginActivationKey(models.Model):
|
||||
"""
|
||||
Optional ORM mirror for activation keys persisted in MariaDB.
|
||||
Runtime code uses raw SQL CREATE TABLE IF NOT EXISTS for migration safety.
|
||||
"""
|
||||
plugin_name = models.CharField(max_length=191)
|
||||
user_identity = models.CharField(max_length=191)
|
||||
activation_key_hash = models.CharField(max_length=64)
|
||||
key_last4 = models.CharField(max_length=4, blank=True, default='')
|
||||
source = models.CharField(max_length=50, blank=True, default='manual')
|
||||
is_active = models.BooleanField(default=True)
|
||||
created_at = models.DateTimeField(auto_now_add=True)
|
||||
updated_at = models.DateTimeField(auto_now=True)
|
||||
|
||||
class Meta:
|
||||
managed = False
|
||||
db_table = 'plugin_activation_keys'
|
||||
unique_together = (('plugin_name', 'user_identity'),)
|
||||
|
||||
@@ -5,7 +5,133 @@ Checks if user has access to paid plugins
|
||||
"""
|
||||
|
||||
from .patreon_verifier import PatreonVerifier
|
||||
import logging
|
||||
import hashlib
|
||||
from django.db import connection
|
||||
from plogical.CyberCPLogFileWriter import CyberCPLogFileWriter as logging
|
||||
|
||||
|
||||
def _normalize_identity(value):
|
||||
if not value:
|
||||
return ''
|
||||
return str(value).strip().lower()
|
||||
|
||||
|
||||
def _hash_activation_key(raw_key):
|
||||
return hashlib.sha256(raw_key.encode('utf-8')).hexdigest()
|
||||
|
||||
|
||||
def _ensure_activation_table():
|
||||
"""
|
||||
Create table on-demand so upgrade paths without Django migrations are safe.
|
||||
"""
|
||||
sql = """
|
||||
CREATE TABLE IF NOT EXISTS plugin_activation_keys (
|
||||
id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
|
||||
plugin_name VARCHAR(191) NOT NULL,
|
||||
user_identity VARCHAR(191) NOT NULL,
|
||||
activation_key_hash CHAR(64) NOT NULL,
|
||||
key_last4 VARCHAR(4) NOT NULL DEFAULT '',
|
||||
source VARCHAR(50) NOT NULL DEFAULT 'manual',
|
||||
is_active TINYINT(1) NOT NULL DEFAULT 1,
|
||||
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
PRIMARY KEY (id),
|
||||
UNIQUE KEY uniq_plugin_identity (plugin_name, user_identity),
|
||||
KEY idx_identity (user_identity),
|
||||
KEY idx_plugin (plugin_name)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||
"""
|
||||
with connection.cursor() as cursor:
|
||||
cursor.execute(sql)
|
||||
|
||||
|
||||
def save_activation_key(plugin_name, user_identity, activation_key, source='manual'):
|
||||
"""
|
||||
Persist activation key hash in MariaDB (upsert by plugin_name + user_identity).
|
||||
"""
|
||||
plugin_name = _normalize_identity(plugin_name)
|
||||
user_identity = _normalize_identity(user_identity)
|
||||
activation_key = str(activation_key or '').strip()
|
||||
if not plugin_name or not user_identity or not activation_key:
|
||||
return False
|
||||
|
||||
try:
|
||||
_ensure_activation_table()
|
||||
key_hash = _hash_activation_key(activation_key)
|
||||
key_last4 = activation_key[-4:] if len(activation_key) >= 4 else activation_key
|
||||
with connection.cursor() as cursor:
|
||||
cursor.execute(
|
||||
"""
|
||||
INSERT INTO plugin_activation_keys
|
||||
(plugin_name, user_identity, activation_key_hash, key_last4, source, is_active)
|
||||
VALUES (%s, %s, %s, %s, %s, 1)
|
||||
ON DUPLICATE KEY UPDATE
|
||||
activation_key_hash = VALUES(activation_key_hash),
|
||||
key_last4 = VALUES(key_last4),
|
||||
source = VALUES(source),
|
||||
is_active = 1
|
||||
""",
|
||||
[plugin_name, user_identity, key_hash, key_last4, source]
|
||||
)
|
||||
return True
|
||||
except Exception as e:
|
||||
logging.writeToFile('plugin_access.save_activation_key failed: %s' % str(e))
|
||||
return False
|
||||
|
||||
|
||||
def has_saved_activation(plugin_name, user_identity):
|
||||
plugin_name = _normalize_identity(plugin_name)
|
||||
user_identity = _normalize_identity(user_identity)
|
||||
if not plugin_name or not user_identity:
|
||||
return False
|
||||
|
||||
try:
|
||||
_ensure_activation_table()
|
||||
with connection.cursor() as cursor:
|
||||
cursor.execute(
|
||||
"""
|
||||
SELECT 1
|
||||
FROM plugin_activation_keys
|
||||
WHERE plugin_name = %s
|
||||
AND user_identity = %s
|
||||
AND is_active = 1
|
||||
LIMIT 1
|
||||
""",
|
||||
[plugin_name, user_identity]
|
||||
)
|
||||
return cursor.fetchone() is not None
|
||||
except Exception as e:
|
||||
logging.writeToFile('plugin_access.has_saved_activation failed: %s' % str(e))
|
||||
return False
|
||||
|
||||
|
||||
def verify_saved_activation_key(plugin_name, user_identity, activation_key):
|
||||
plugin_name = _normalize_identity(plugin_name)
|
||||
user_identity = _normalize_identity(user_identity)
|
||||
activation_key = str(activation_key or '').strip()
|
||||
if not plugin_name or not user_identity or not activation_key:
|
||||
return False
|
||||
|
||||
try:
|
||||
_ensure_activation_table()
|
||||
key_hash = _hash_activation_key(activation_key)
|
||||
with connection.cursor() as cursor:
|
||||
cursor.execute(
|
||||
"""
|
||||
SELECT 1
|
||||
FROM plugin_activation_keys
|
||||
WHERE plugin_name = %s
|
||||
AND user_identity = %s
|
||||
AND activation_key_hash = %s
|
||||
AND is_active = 1
|
||||
LIMIT 1
|
||||
""",
|
||||
[plugin_name, user_identity, key_hash]
|
||||
)
|
||||
return cursor.fetchone() is not None
|
||||
except Exception as e:
|
||||
logging.writeToFile('plugin_access.verify_saved_activation_key failed: %s' % str(e))
|
||||
return False
|
||||
|
||||
def check_plugin_access(request, plugin_name, plugin_meta=None):
|
||||
"""
|
||||
@@ -63,7 +189,16 @@ def check_plugin_access(request, plugin_name, plugin_meta=None):
|
||||
'patreon_url': plugin_meta.get('patreon_url')
|
||||
}
|
||||
|
||||
# Check Patreon membership
|
||||
# First allow DB-backed activation keys (survives upgrades)
|
||||
if has_saved_activation(plugin_name, user_email):
|
||||
return {
|
||||
'has_access': True,
|
||||
'is_paid': True,
|
||||
'message': 'Access granted',
|
||||
'patreon_url': None
|
||||
}
|
||||
|
||||
# Fallback to Patreon membership
|
||||
verifier = PatreonVerifier()
|
||||
has_membership = verifier.check_membership_cached(user_email)
|
||||
|
||||
|
||||
@@ -104,6 +104,7 @@ urlpatterns = [
|
||||
path('api/revert/<str:plugin_name>/', views.revert_plugin, name='revert_plugin'),
|
||||
path('api/debug-plugins/', views.debug_loaded_plugins, name='debug_loaded_plugins'),
|
||||
path('api/check-subscription/<str:plugin_name>/', views.check_plugin_subscription, name='check_plugin_subscription'),
|
||||
path('api/store-activation/<str:plugin_name>/', views.store_plugin_activation_key, name='store_plugin_activation_key'),
|
||||
path('<str:plugin_name>/settings/', views.plugin_settings_proxy, name='plugin_settings_proxy'),
|
||||
path('<str:plugin_name>/help/', views.plugin_help, name='plugin_help'),
|
||||
]
|
||||
|
||||
@@ -2322,10 +2322,11 @@ def plugin_help(request, plugin_name):
|
||||
return proc.render()
|
||||
|
||||
@csrf_exempt
|
||||
@require_http_methods(["GET"])
|
||||
@require_http_methods(["GET", "POST"])
|
||||
def check_plugin_subscription(request, plugin_name):
|
||||
"""
|
||||
API endpoint to check if user has Patreon subscription for a paid plugin
|
||||
API endpoint to check plugin premium access.
|
||||
Supports optional activation key save/verify to persist entitlement in MariaDB.
|
||||
|
||||
Args:
|
||||
request: Django request object
|
||||
@@ -2353,10 +2354,46 @@ def check_plugin_subscription(request, plugin_name):
|
||||
}, status=401)
|
||||
|
||||
# Load plugin metadata
|
||||
from .plugin_access import check_plugin_access, _load_plugin_meta
|
||||
from .plugin_access import (
|
||||
check_plugin_access,
|
||||
_load_plugin_meta,
|
||||
save_activation_key,
|
||||
verify_saved_activation_key
|
||||
)
|
||||
|
||||
plugin_meta = _load_plugin_meta(plugin_name)
|
||||
|
||||
user_email = getattr(request.user, 'email', None) or getattr(request.user, 'username', '')
|
||||
activation_key = ''
|
||||
if request.method == 'POST':
|
||||
try:
|
||||
payload = json.loads(request.body.decode('utf-8') or '{}')
|
||||
except Exception:
|
||||
payload = {}
|
||||
activation_key = str(payload.get('activation_key', '')).strip()
|
||||
if activation_key and user_email:
|
||||
# If key is already known for this user/plugin -> immediate access
|
||||
if verify_saved_activation_key(plugin_name, user_email, activation_key):
|
||||
return JsonResponse({
|
||||
'success': True,
|
||||
'has_access': True,
|
||||
'is_paid': bool(plugin_meta and plugin_meta.get('is_paid', False)),
|
||||
'message': 'Access granted',
|
||||
'patreon_url': None,
|
||||
'activation_saved': True
|
||||
})
|
||||
# Save submitted key as persistent entitlement (admin-managed workflow)
|
||||
saved = save_activation_key(plugin_name, user_email, activation_key, source='plugin_settings')
|
||||
if saved:
|
||||
return JsonResponse({
|
||||
'success': True,
|
||||
'has_access': True,
|
||||
'is_paid': bool(plugin_meta and plugin_meta.get('is_paid', False)),
|
||||
'message': 'Activation key saved',
|
||||
'patreon_url': None,
|
||||
'activation_saved': True
|
||||
})
|
||||
|
||||
# Check access
|
||||
access_result = check_plugin_access(request, plugin_name, plugin_meta)
|
||||
|
||||
@@ -2365,7 +2402,8 @@ def check_plugin_subscription(request, plugin_name):
|
||||
'has_access': access_result['has_access'],
|
||||
'is_paid': access_result['is_paid'],
|
||||
'message': access_result['message'],
|
||||
'patreon_url': access_result.get('patreon_url')
|
||||
'patreon_url': access_result.get('patreon_url'),
|
||||
'activation_saved': access_result['has_access'] and access_result['is_paid']
|
||||
})
|
||||
|
||||
except Exception as e:
|
||||
@@ -2374,6 +2412,42 @@ def check_plugin_subscription(request, plugin_name):
|
||||
'success': False,
|
||||
'has_access': False,
|
||||
'is_paid': False,
|
||||
'message': f'Error checking subscription: {str(e)}',
|
||||
'message': 'Error checking subscription',
|
||||
'patreon_url': None
|
||||
}, status=500)
|
||||
|
||||
|
||||
@csrf_exempt
|
||||
@require_http_methods(["POST"])
|
||||
def store_plugin_activation_key(request, plugin_name):
|
||||
"""
|
||||
Store activation key in MariaDB so upgrades do not lose premium entitlement.
|
||||
"""
|
||||
try:
|
||||
if not user_can_manage_plugins(request):
|
||||
return deny_plugin_manage_json_response(request)
|
||||
if not request.user or not request.user.is_authenticated:
|
||||
return JsonResponse({'success': False, 'message': 'Authentication required'}, status=401)
|
||||
|
||||
try:
|
||||
payload = json.loads(request.body.decode('utf-8') or '{}')
|
||||
except Exception:
|
||||
payload = {}
|
||||
|
||||
activation_key = str(payload.get('activation_key', '')).strip()
|
||||
if not activation_key:
|
||||
return JsonResponse({'success': False, 'message': 'activation_key is required'}, status=400)
|
||||
|
||||
user_email = getattr(request.user, 'email', None) or getattr(request.user, 'username', '')
|
||||
if not user_email:
|
||||
return JsonResponse({'success': False, 'message': 'Unable to determine user identity'}, status=400)
|
||||
|
||||
from .plugin_access import save_activation_key
|
||||
ok = save_activation_key(plugin_name, user_email, activation_key, source='api')
|
||||
if not ok:
|
||||
return JsonResponse({'success': False, 'message': 'Failed to persist activation key'}, status=500)
|
||||
|
||||
return JsonResponse({'success': True, 'message': 'Activation key saved'})
|
||||
except Exception as e:
|
||||
logging.writeToFile('store_plugin_activation_key failed for %s: %s' % (plugin_name, str(e)))
|
||||
return JsonResponse({'success': False, 'message': 'Internal server error'}, status=500)
|
||||
|
||||
Reference in New Issue
Block a user