mirror of
https://github.com/usmannasir/cyberpanel.git
synced 2026-03-25 21:40:07 +01:00
@@ -53,6 +53,20 @@ _default_origins = [
|
||||
# Merge environment and default origins, avoiding duplicates
|
||||
CSRF_TRUSTED_ORIGINS = list(dict.fromkeys(_csrf_origins_list + _default_origins))
|
||||
|
||||
# Optional file: one trusted origin per line (e.g. https://203.0.113.1:2087) for IP:port panel access.
|
||||
# Create /etc/cyberpanel/csrf_trusted_origins on the server if JSON POSTs get 403 CSRF when using HTTPS by IP.
|
||||
_csrf_trusted_origins_file = '/etc/cyberpanel/csrf_trusted_origins'
|
||||
if os.path.isfile(_csrf_trusted_origins_file):
|
||||
try:
|
||||
with open(_csrf_trusted_origins_file, 'r', encoding='utf-8', errors='replace') as _csrf_f:
|
||||
for _csrf_line in _csrf_f:
|
||||
_csrf_line = _csrf_line.strip()
|
||||
if _csrf_line and not _csrf_line.startswith('#'):
|
||||
if _csrf_line not in CSRF_TRUSTED_ORIGINS:
|
||||
CSRF_TRUSTED_ORIGINS.append(_csrf_line)
|
||||
except OSError:
|
||||
pass
|
||||
|
||||
# Application definition
|
||||
|
||||
INSTALLED_APPS = [
|
||||
|
||||
@@ -2130,7 +2130,11 @@
|
||||
</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
{% endif %}
|
||||
{% if admin or managePlugins %}
|
||||
{% if managePlugins and not admin %}
|
||||
<div class="section-header">{% trans "PLUGINS" %}</div>
|
||||
{% endif %}
|
||||
<a href="#" class="menu-item" onclick="toggleSubmenu('plugins-submenu', this); return false;">
|
||||
<div class="icon-wrapper">
|
||||
<i class="fas fa-plug"></i>
|
||||
|
||||
@@ -283,6 +283,17 @@ class ACLManager:
|
||||
finalResponse['mailServerSSL'] = config['mailServerSSL']
|
||||
finalResponse['sslReconcile'] = config.get('sslReconcile', 0)
|
||||
|
||||
## Plugin management (Plugin Store / installed plugins UI and APIs)
|
||||
|
||||
_mpv = config.get('managePlugins', 0)
|
||||
if _mpv in (1, True, '1', 'true'):
|
||||
finalResponse['managePlugins'] = 1
|
||||
else:
|
||||
try:
|
||||
finalResponse['managePlugins'] = 1 if int(_mpv) else 0
|
||||
except (TypeError, ValueError):
|
||||
finalResponse['managePlugins'] = 0
|
||||
|
||||
return finalResponse
|
||||
|
||||
@staticmethod
|
||||
|
||||
50
plogical/plugin_acl.py
Normal file
50
plogical/plugin_acl.py
Normal file
@@ -0,0 +1,50 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""Shared ACL checks for CyberPanel plugin management (core + store plugins)."""
|
||||
from functools import wraps
|
||||
from django.http import JsonResponse
|
||||
|
||||
|
||||
def user_can_manage_plugins(request):
|
||||
"""True if session user is full admin or has managePlugins ACL."""
|
||||
try:
|
||||
user_id = request.session['userID']
|
||||
except KeyError:
|
||||
return False
|
||||
try:
|
||||
from plogical.acl import ACLManager
|
||||
acl = ACLManager.loadedACL(user_id)
|
||||
if acl.get('admin') == 1:
|
||||
return True
|
||||
try:
|
||||
return int(acl.get('managePlugins', 0) or 0) == 1
|
||||
except (TypeError, ValueError):
|
||||
return False
|
||||
except BaseException:
|
||||
return False
|
||||
|
||||
|
||||
def deny_plugin_manage_json_response(request):
|
||||
"""401 if no session, else 403 JSON for plugin management APIs."""
|
||||
try:
|
||||
request.session['userID']
|
||||
except KeyError:
|
||||
return JsonResponse({
|
||||
'success': False,
|
||||
'error_message': 'Authentication required.',
|
||||
'error': 'Authentication required.',
|
||||
}, status=401)
|
||||
return JsonResponse({
|
||||
'success': False,
|
||||
'error_message': 'You are not authorized to manage plugins.',
|
||||
'error': 'You are not authorized to manage plugins.',
|
||||
}, status=403)
|
||||
|
||||
|
||||
def require_manage_plugins_api(view_func):
|
||||
"""Decorator: JSON 401/403 if user cannot manage plugins (use after login/session check)."""
|
||||
@wraps(view_func)
|
||||
def _wrapped(request, *args, **kwargs):
|
||||
if not user_can_manage_plugins(request):
|
||||
return deny_plugin_manage_json_response(request)
|
||||
return view_func(request, *args, **kwargs)
|
||||
return _wrapped
|
||||
@@ -12,11 +12,13 @@ import json
|
||||
from datetime import datetime, timedelta
|
||||
from xml.etree import ElementTree
|
||||
from plogical.httpProc import httpProc
|
||||
from plogical.plugin_acl import user_can_manage_plugins, deny_plugin_manage_json_response
|
||||
from plogical.CyberCPLogFileWriter import CyberCPLogFileWriter as logging
|
||||
import sys
|
||||
import urllib.request
|
||||
import urllib.error
|
||||
import time
|
||||
import inspect
|
||||
sys.path.append('/usr/local/CyberCP')
|
||||
from pluginInstaller.pluginInstaller import pluginInstaller
|
||||
from .patreon_verifier import PatreonVerifier
|
||||
@@ -43,6 +45,36 @@ PLUGIN_SOURCE_PATHS = ['/home/cyberpanel/plugins', '/home/cyberpanel-plugins']
|
||||
# These plugins show "Built-in" badge and only Settings button (no Deactivate/Uninstall)
|
||||
BUILTIN_PLUGINS = frozenset(['emailMarketing', 'emailPremium'])
|
||||
|
||||
|
||||
def _install_plugin_compat(plugin_name, zip_path_abs):
|
||||
"""
|
||||
Call pluginInstaller.installPlugin with zip_path when supported (newer CyberPanel).
|
||||
Older installs only accept installPlugin(pluginName) and expect pluginName.zip in CWD.
|
||||
"""
|
||||
zip_path_abs = os.path.abspath(zip_path_abs)
|
||||
work_dir = os.path.dirname(zip_path_abs)
|
||||
use_zip_kw = False
|
||||
try:
|
||||
sig = inspect.signature(pluginInstaller.installPlugin)
|
||||
use_zip_kw = 'zip_path' in sig.parameters
|
||||
except (TypeError, ValueError):
|
||||
use_zip_kw = False
|
||||
if use_zip_kw:
|
||||
pluginInstaller.installPlugin(plugin_name, zip_path=zip_path_abs)
|
||||
return
|
||||
prev_cwd = os.getcwd()
|
||||
try:
|
||||
os.chdir(work_dir)
|
||||
expected_zip = os.path.join(work_dir, plugin_name + '.zip')
|
||||
if zip_path_abs != expected_zip:
|
||||
shutil.copy2(zip_path_abs, expected_zip)
|
||||
pluginInstaller.installPlugin(plugin_name)
|
||||
finally:
|
||||
try:
|
||||
os.chdir(prev_cwd)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# Core CyberPanel app dirs under /usr/local/CyberCP that must not be counted as "installed plugins"
|
||||
# (matches pluginHolder.urls so Installed count = store/plugin dirs only, not core apps)
|
||||
RESERVED_PLUGIN_DIRS = frozenset([
|
||||
@@ -175,7 +207,7 @@ def _set_plugin_state(plugin_name, enabled):
|
||||
def help_page(request):
|
||||
"""Display plugin development help page"""
|
||||
mailUtilities.checkHome()
|
||||
proc = httpProc(request, 'pluginHolder/help.html', {}, 'admin')
|
||||
proc = httpProc(request, 'pluginHolder/help.html', {}, 'managePlugins')
|
||||
return proc.render()
|
||||
|
||||
def installed(request):
|
||||
@@ -580,7 +612,7 @@ def installed(request):
|
||||
proc = httpProc(request, 'pluginHolder/plugins.html',
|
||||
{'plugins': pluginList, 'error_plugins': errorPlugins,
|
||||
'installed_count': installed_count, 'active_count': active_count,
|
||||
'cache_expiry_timestamp': cache_expiry_timestamp}, 'admin')
|
||||
'cache_expiry_timestamp': cache_expiry_timestamp}, 'managePlugins')
|
||||
return proc.render()
|
||||
|
||||
@csrf_exempt
|
||||
@@ -588,6 +620,8 @@ def installed(request):
|
||||
def install_plugin(request, plugin_name):
|
||||
"""Install a plugin"""
|
||||
try:
|
||||
if not user_can_manage_plugins(request):
|
||||
return deny_plugin_manage_json_response(request)
|
||||
# Check if plugin source exists (in any configured source path)
|
||||
pluginSource = _get_plugin_source_path(plugin_name)
|
||||
if not pluginSource:
|
||||
@@ -638,13 +672,11 @@ def install_plugin(request, plugin_name):
|
||||
zip_path_abs = os.path.abspath(zip_path)
|
||||
if not os.path.exists(zip_path_abs):
|
||||
raise Exception(f'Zip file not found: {zip_path_abs}')
|
||||
original_cwd = os.getcwd()
|
||||
os.chdir(temp_dir)
|
||||
|
||||
try:
|
||||
# Install using pluginInstaller with explicit zip path (avoids cwd races)
|
||||
# Install using pluginInstaller (zip_path kw when supported; else legacy CWD + pluginName.zip)
|
||||
try:
|
||||
pluginInstaller.installPlugin(plugin_name, zip_path=zip_path_abs)
|
||||
_install_plugin_compat(plugin_name, zip_path_abs)
|
||||
except Exception as install_error:
|
||||
# Log the full error for debugging
|
||||
error_msg = str(install_error)
|
||||
@@ -680,7 +712,6 @@ def install_plugin(request, plugin_name):
|
||||
'message': f'Plugin {plugin_name} installed successfully'
|
||||
})
|
||||
finally:
|
||||
os.chdir(original_cwd)
|
||||
# Cleanup
|
||||
shutil.rmtree(temp_dir, ignore_errors=True)
|
||||
|
||||
@@ -696,6 +727,8 @@ def install_plugin(request, plugin_name):
|
||||
def uninstall_plugin(request, plugin_name):
|
||||
"""Uninstall a plugin - but keep source files and settings"""
|
||||
try:
|
||||
if not user_can_manage_plugins(request):
|
||||
return deny_plugin_manage_json_response(request)
|
||||
# Check if plugin is installed
|
||||
pluginInstalled = '/usr/local/CyberCP/' + plugin_name
|
||||
if not os.path.exists(pluginInstalled):
|
||||
@@ -750,6 +783,8 @@ def uninstall_plugin(request, plugin_name):
|
||||
def enable_plugin(request, plugin_name):
|
||||
"""Enable a plugin"""
|
||||
try:
|
||||
if not user_can_manage_plugins(request):
|
||||
return deny_plugin_manage_json_response(request)
|
||||
# Check if plugin is installed
|
||||
pluginInstalled = '/usr/local/CyberCP/' + plugin_name
|
||||
if not os.path.exists(pluginInstalled):
|
||||
@@ -782,6 +817,8 @@ def enable_plugin(request, plugin_name):
|
||||
def disable_plugin(request, plugin_name):
|
||||
"""Disable a plugin"""
|
||||
try:
|
||||
if not user_can_manage_plugins(request):
|
||||
return deny_plugin_manage_json_response(request)
|
||||
# Check if plugin is installed
|
||||
pluginInstalled = '/usr/local/CyberCP/' + plugin_name
|
||||
if not os.path.exists(pluginInstalled):
|
||||
@@ -1366,6 +1403,8 @@ def _fetch_plugins_from_github():
|
||||
def fetch_plugin_store(request):
|
||||
"""Fetch plugins from the plugin store with caching"""
|
||||
try:
|
||||
if not user_can_manage_plugins(request):
|
||||
return deny_plugin_manage_json_response(request)
|
||||
mailUtilities.checkHome()
|
||||
except Exception as e:
|
||||
logging.writeToFile(f"fetch_plugin_store: checkHome failed: {str(e)}")
|
||||
@@ -1433,6 +1472,8 @@ def upgrade_plugin(request, plugin_name):
|
||||
mailUtilities.checkHome()
|
||||
|
||||
try:
|
||||
if not user_can_manage_plugins(request):
|
||||
return deny_plugin_manage_json_response(request)
|
||||
# Check if plugin is installed
|
||||
pluginInstalled = '/usr/local/CyberCP/' + plugin_name
|
||||
if not os.path.exists(pluginInstalled):
|
||||
@@ -1520,55 +1561,50 @@ def upgrade_plugin(request, plugin_name):
|
||||
zip_path_abs = os.path.abspath(zip_path)
|
||||
if not os.path.exists(zip_path_abs):
|
||||
raise Exception(f'Zip file not found: {zip_path_abs}')
|
||||
original_cwd = os.getcwd()
|
||||
os.chdir(temp_dir)
|
||||
|
||||
logging.writeToFile(f"Upgrading plugin using pluginInstaller (zip={zip_path_abs})")
|
||||
|
||||
# Install using pluginInstaller (zip_path kw when supported; else legacy)
|
||||
try:
|
||||
logging.writeToFile(f"Upgrading plugin using pluginInstaller (zip={zip_path_abs})")
|
||||
|
||||
# Install using pluginInstaller with explicit zip path (this will overwrite existing files)
|
||||
try:
|
||||
pluginInstaller.installPlugin(plugin_name, zip_path=zip_path_abs)
|
||||
except Exception as install_error:
|
||||
error_msg = str(install_error)
|
||||
logging.writeToFile(f"pluginInstaller.installPlugin raised exception: {error_msg}")
|
||||
# Check if plugin directory exists despite the error
|
||||
if not os.path.exists(pluginInstalled):
|
||||
raise Exception(f'Plugin upgrade failed: {error_msg}')
|
||||
|
||||
# Wait for file system to sync
|
||||
import time
|
||||
time.sleep(3)
|
||||
|
||||
# Verify plugin was upgraded
|
||||
_install_plugin_compat(plugin_name, zip_path_abs)
|
||||
except Exception as install_error:
|
||||
error_msg = str(install_error)
|
||||
logging.writeToFile(f"pluginInstaller.installPlugin raised exception: {error_msg}")
|
||||
# Check if plugin directory exists despite the error
|
||||
if not os.path.exists(pluginInstalled):
|
||||
raise Exception(f'Plugin upgrade failed: {pluginInstalled} does not exist after upgrade')
|
||||
|
||||
# Sync meta.xml from GitHub raw so version matches store (archive ZIP can be cached/stale)
|
||||
raise Exception(f'Plugin upgrade failed: {error_msg}')
|
||||
|
||||
# Wait for file system to sync
|
||||
import time
|
||||
time.sleep(3)
|
||||
|
||||
# Verify plugin was upgraded
|
||||
if not os.path.exists(pluginInstalled):
|
||||
raise Exception(f'Plugin upgrade failed: {pluginInstalled} does not exist after upgrade')
|
||||
|
||||
# Sync meta.xml from GitHub raw so version matches store (archive ZIP can be cached/stale)
|
||||
_sync_meta_xml_from_github(plugin_name, '/usr/local/CyberCP')
|
||||
new_version = _get_installed_version(plugin_name, '/usr/local/CyberCP')
|
||||
# If version unchanged, meta sync may have failed (e.g. network); retry once
|
||||
if new_version == installed_version:
|
||||
logging.writeToFile(f"Plugin {plugin_name}: version unchanged after first meta sync, retrying sync")
|
||||
_sync_meta_xml_from_github(plugin_name, '/usr/local/CyberCP')
|
||||
new_version = _get_installed_version(plugin_name, '/usr/local/CyberCP')
|
||||
# If version unchanged, meta sync may have failed (e.g. network); retry once
|
||||
if new_version == installed_version:
|
||||
logging.writeToFile(f"Plugin {plugin_name}: version unchanged after first meta sync, retrying sync")
|
||||
_sync_meta_xml_from_github(plugin_name, '/usr/local/CyberCP')
|
||||
new_version = _get_installed_version(plugin_name, '/usr/local/CyberCP')
|
||||
if new_version == installed_version:
|
||||
logging.writeToFile(f"Plugin {plugin_name}: version still {installed_version} after upgrade; meta.xml may not have been updated from GitHub")
|
||||
|
||||
logging.writeToFile(f"Plugin {plugin_name} upgraded successfully from {installed_version} to {new_version}")
|
||||
|
||||
backup_message = ''
|
||||
if backup_path:
|
||||
backup_message = f' Backup created at: {backup_info.get("timestamp", "unknown")}'
|
||||
|
||||
return JsonResponse({
|
||||
'success': True,
|
||||
'message': f'Plugin {plugin_name} upgraded successfully from {installed_version} to {new_version}.{backup_message}',
|
||||
'backup_created': backup_path is not None,
|
||||
'backup_path': backup_path if backup_path else None
|
||||
})
|
||||
finally:
|
||||
os.chdir(original_cwd)
|
||||
if new_version == installed_version:
|
||||
logging.writeToFile(f"Plugin {plugin_name}: version still {installed_version} after upgrade; meta.xml may not have been updated from GitHub")
|
||||
|
||||
logging.writeToFile(f"Plugin {plugin_name} upgraded successfully from {installed_version} to {new_version}")
|
||||
|
||||
backup_message = ''
|
||||
if backup_path:
|
||||
backup_message = f' Backup created at: {backup_info.get("timestamp", "unknown")}'
|
||||
|
||||
return JsonResponse({
|
||||
'success': True,
|
||||
'message': f'Plugin {plugin_name} upgraded successfully from {installed_version} to {new_version}.{backup_message}',
|
||||
'backup_created': backup_path is not None,
|
||||
'backup_path': backup_path if backup_path else None
|
||||
})
|
||||
|
||||
finally:
|
||||
# Cleanup
|
||||
@@ -1600,6 +1636,8 @@ def get_plugin_backups(request, plugin_name):
|
||||
mailUtilities.checkHome()
|
||||
|
||||
try:
|
||||
if not user_can_manage_plugins(request):
|
||||
return deny_plugin_manage_json_response(request)
|
||||
backups = _get_plugin_backups(plugin_name)
|
||||
return JsonResponse({
|
||||
'success': True,
|
||||
@@ -1620,6 +1658,8 @@ def revert_plugin(request, plugin_name):
|
||||
mailUtilities.checkHome()
|
||||
|
||||
try:
|
||||
if not user_can_manage_plugins(request):
|
||||
return deny_plugin_manage_json_response(request)
|
||||
# Get backup path from request
|
||||
data = json.loads(request.body)
|
||||
backup_path = data.get('backup_path')
|
||||
@@ -1684,6 +1724,8 @@ def install_from_store(request, plugin_name):
|
||||
mailUtilities.checkHome()
|
||||
|
||||
try:
|
||||
if not user_can_manage_plugins(request):
|
||||
return deny_plugin_manage_json_response(request)
|
||||
# Check if already installed
|
||||
pluginInstalled = '/usr/local/CyberCP/' + plugin_name
|
||||
if os.path.exists(pluginInstalled):
|
||||
@@ -1792,55 +1834,50 @@ def install_from_store(request, plugin_name):
|
||||
|
||||
# Pass absolute path so extraction does not depend on cwd (installPlugin may change cwd)
|
||||
zip_path_abs = os.path.abspath(zip_path)
|
||||
original_cwd = os.getcwd()
|
||||
os.chdir(temp_dir)
|
||||
|
||||
logging.writeToFile(f"Installing plugin using pluginInstaller (zip={zip_path_abs})")
|
||||
|
||||
# Install using pluginInstaller (zip_path kw when supported; else legacy)
|
||||
try:
|
||||
logging.writeToFile(f"Installing plugin using pluginInstaller (zip={zip_path_abs})")
|
||||
|
||||
# Install using pluginInstaller with explicit zip path (avoids cwd races)
|
||||
try:
|
||||
pluginInstaller.installPlugin(plugin_name, zip_path=zip_path_abs)
|
||||
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(3) # Increased wait time for file system sync
|
||||
|
||||
# Verify plugin was actually installed
|
||||
_install_plugin_compat(plugin_name, zip_path_abs)
|
||||
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 not os.path.exists(pluginInstalled):
|
||||
# Exclude README.md - main CyberPanel repo has it at root
|
||||
root_files = ['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')
|
||||
|
||||
# Sync meta.xml from GitHub raw so version matches store
|
||||
_sync_meta_xml_from_github(plugin_name, '/usr/local/CyberCP')
|
||||
|
||||
logging.writeToFile(f"Plugin {plugin_name} installed successfully")
|
||||
|
||||
# Set plugin to enabled by default after installation
|
||||
_set_plugin_state(plugin_name, True)
|
||||
|
||||
_ensure_plugin_meta_xml(plugin_name)
|
||||
return JsonResponse({
|
||||
'success': True,
|
||||
'message': f'Plugin {plugin_name} installed successfully from store'
|
||||
})
|
||||
finally:
|
||||
os.chdir(original_cwd)
|
||||
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(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):
|
||||
# Exclude README.md - main CyberPanel repo has it at root
|
||||
root_files = ['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')
|
||||
|
||||
# Sync meta.xml from GitHub raw so version matches store
|
||||
_sync_meta_xml_from_github(plugin_name, '/usr/local/CyberCP')
|
||||
|
||||
logging.writeToFile(f"Plugin {plugin_name} installed successfully")
|
||||
|
||||
# Set plugin to enabled by default after installation
|
||||
_set_plugin_state(plugin_name, True)
|
||||
|
||||
_ensure_plugin_meta_xml(plugin_name)
|
||||
return JsonResponse({
|
||||
'success': True,
|
||||
'message': f'Plugin {plugin_name} installed successfully from store'
|
||||
})
|
||||
|
||||
finally:
|
||||
# Cleanup
|
||||
@@ -1870,6 +1907,8 @@ def install_from_store(request, plugin_name):
|
||||
def debug_loaded_plugins(request):
|
||||
"""Return which plugins have URL routes loaded and which failed (for diagnosing 404s)."""
|
||||
try:
|
||||
if not user_can_manage_plugins(request):
|
||||
return deny_plugin_manage_json_response(request)
|
||||
import pluginHolder.urls as urls_mod
|
||||
loaded = list(getattr(urls_mod, '_loaded_plugins', []))
|
||||
failed = dict(getattr(urls_mod, '_failed_plugins', {}))
|
||||
@@ -1891,6 +1930,9 @@ def plugin_settings_proxy(request, plugin_name):
|
||||
the plugin was installed after the worker started (dynamic URL list is built at import time).
|
||||
"""
|
||||
mailUtilities.checkHome()
|
||||
if not user_can_manage_plugins(request):
|
||||
from django.http import HttpResponseForbidden
|
||||
return HttpResponseForbidden('You are not authorized to manage plugins.')
|
||||
plugin_path = '/usr/local/CyberCP/' + plugin_name
|
||||
urls_py = os.path.join(plugin_path, 'urls.py')
|
||||
if not plugin_name or not os.path.isdir(plugin_path) or not os.path.exists(urls_py):
|
||||
@@ -1927,7 +1969,7 @@ def plugin_help(request, plugin_name):
|
||||
if not os.path.exists(plugin_path) or not os.path.exists(meta_xml_path):
|
||||
proc = httpProc(request, 'pluginHolder/plugin_not_found.html', {
|
||||
'plugin_name': plugin_name
|
||||
}, 'admin')
|
||||
}, 'managePlugins')
|
||||
return proc.render()
|
||||
|
||||
# Parse meta.xml
|
||||
@@ -1949,7 +1991,7 @@ def plugin_help(request, plugin_name):
|
||||
logging.writeToFile(f"Error parsing meta.xml for {plugin_name}: {str(e)}")
|
||||
proc = httpProc(request, 'pluginHolder/plugin_not_found.html', {
|
||||
'plugin_name': plugin_name
|
||||
}, 'admin')
|
||||
}, 'managePlugins')
|
||||
return proc.render()
|
||||
|
||||
# Look for help content files (README.md, CHANGELOG.md, HELP.md, etc.)
|
||||
@@ -2100,7 +2142,7 @@ def plugin_help(request, plugin_name):
|
||||
'help_content': help_content,
|
||||
}
|
||||
|
||||
proc = httpProc(request, 'pluginHolder/plugin_help.html', context, 'admin')
|
||||
proc = httpProc(request, 'pluginHolder/plugin_help.html', context, 'managePlugins')
|
||||
return proc.render()
|
||||
|
||||
@csrf_exempt
|
||||
@@ -2122,6 +2164,8 @@ def check_plugin_subscription(request, plugin_name):
|
||||
}
|
||||
"""
|
||||
try:
|
||||
if not user_can_manage_plugins(request):
|
||||
return deny_plugin_manage_json_response(request)
|
||||
# Check if user is authenticated
|
||||
if not request.user or not request.user.is_authenticated:
|
||||
return JsonResponse({
|
||||
|
||||
@@ -4,6 +4,7 @@ import subprocess
|
||||
import shlex
|
||||
import argparse
|
||||
import os
|
||||
import errno
|
||||
import shutil
|
||||
import time
|
||||
import tempfile
|
||||
@@ -57,6 +58,68 @@ class pluginInstaller:
|
||||
pluginHome = '/usr/local/CyberCP/' + pluginName
|
||||
return os.path.exists(pluginHome + '/enable_migrations')
|
||||
|
||||
@staticmethod
|
||||
def _write_lines_to_protected_file(target_path, lines):
|
||||
"""
|
||||
Write UTF-8 lines to a file. Core panel files are often root:root 644; the panel
|
||||
process may need a privileged copy (lscpd/sudo) to update them.
|
||||
"""
|
||||
try:
|
||||
with open(target_path, 'w', encoding='utf-8') as wf:
|
||||
wf.writelines(lines)
|
||||
return
|
||||
except (PermissionError, OSError) as e:
|
||||
pluginInstaller.stdOut('Direct write failed for %s: %s' % (target_path, str(e)))
|
||||
fd, tmp_path = tempfile.mkstemp(prefix='cpwr_', suffix='.txt', dir='/tmp')
|
||||
try:
|
||||
with os.fdopen(fd, 'w', encoding='utf-8') as wf:
|
||||
wf.writelines(lines)
|
||||
cmd = 'cp %s %s' % (shlex.quote(tmp_path), shlex.quote(target_path))
|
||||
if ProcessUtilities.executioner(cmd) == 1:
|
||||
pluginInstaller.stdOut('Wrote %s via privileged copy' % target_path)
|
||||
return
|
||||
except Exception as ex:
|
||||
pluginInstaller.stdOut('Privileged write failed: %s' % str(ex))
|
||||
finally:
|
||||
try:
|
||||
os.unlink(tmp_path)
|
||||
except OSError:
|
||||
pass
|
||||
raise PermissionError(
|
||||
'Cannot write %s. As root: chgrp lscpd %s && chmod 664 %s'
|
||||
% (target_path, target_path, target_path)
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def _read_lines_from_protected_file(source_path):
|
||||
"""
|
||||
Read UTF-8 lines from a core panel file. If the panel user cannot read the file
|
||||
(e.g. root:root 600), copy via ProcessUtilities.executioner to /tmp then read.
|
||||
"""
|
||||
try:
|
||||
with open(source_path, 'r', encoding='utf-8') as rf:
|
||||
return rf.readlines()
|
||||
except OSError as e:
|
||||
if e.errno not in (errno.EACCES, errno.EPERM):
|
||||
raise
|
||||
pluginInstaller.stdOut('Direct read failed for %s: %s' % (source_path, str(e)))
|
||||
fd, tmp_path = tempfile.mkstemp(prefix='cprd_', suffix='.txt', dir='/tmp')
|
||||
os.close(fd)
|
||||
try:
|
||||
cmd = 'cp %s %s' % (shlex.quote(source_path), shlex.quote(tmp_path))
|
||||
if ProcessUtilities.executioner(cmd) != 1:
|
||||
raise PermissionError(
|
||||
'Privileged read copy failed for %s. As root: chgrp lscpd %s && chmod 640 %s'
|
||||
% (source_path, source_path, source_path)
|
||||
)
|
||||
with open(tmp_path, 'r', encoding='utf-8') as rf:
|
||||
return rf.readlines()
|
||||
finally:
|
||||
try:
|
||||
os.unlink(tmp_path)
|
||||
except OSError:
|
||||
pass
|
||||
|
||||
### Functions Related to plugin installation.
|
||||
|
||||
@staticmethod
|
||||
@@ -153,53 +216,79 @@ class pluginInstaller:
|
||||
|
||||
@staticmethod
|
||||
def upgradingSettingsFile(pluginName):
|
||||
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')
|
||||
|
||||
settings_path = "/usr/local/CyberCP/CyberCP/settings.py"
|
||||
data = pluginInstaller._read_lines_from_protected_file(settings_path)
|
||||
line_plugin = " '" + pluginName + "',\n"
|
||||
if any(line.strip() in ("'" + pluginName + "',", '"' + pluginName + '",') for line in data):
|
||||
pluginInstaller.stdOut(
|
||||
'Plugin %s already listed in settings.py; skipping INSTALLED_APPS insert.' % pluginName
|
||||
)
|
||||
return
|
||||
out = []
|
||||
inserted = False
|
||||
for items in data:
|
||||
if items.find("'emailPremium',") > -1:
|
||||
writeToFile.writelines(items)
|
||||
writeToFile.writelines(" '" + pluginName + "',\n")
|
||||
out.append(items)
|
||||
out.append(line_plugin)
|
||||
inserted = True
|
||||
else:
|
||||
writeToFile.writelines(items)
|
||||
|
||||
writeToFile.close()
|
||||
out.append(items)
|
||||
if not inserted:
|
||||
out = []
|
||||
for items in data:
|
||||
if "'pluginHolder'," in items or '"pluginHolder",' in items:
|
||||
out.append(items)
|
||||
out.append(line_plugin)
|
||||
inserted = True
|
||||
else:
|
||||
out.append(items)
|
||||
if not inserted:
|
||||
pluginInstaller.stdOut(
|
||||
'Warning: no emailPremium or pluginHolder anchor in settings.py; '
|
||||
'add %r to INSTALLED_APPS manually or upgrade CyberPanel (auto-sync plugins on disk).'
|
||||
% pluginName
|
||||
)
|
||||
return
|
||||
pluginInstaller._write_lines_to_protected_file(settings_path, out)
|
||||
|
||||
@staticmethod
|
||||
def upgradingURLs(pluginName):
|
||||
"""
|
||||
Add plugin URL pattern to urls.py
|
||||
Plugin URLs must be inserted BEFORE the generic 'plugins/' line
|
||||
to ensure proper route matching (more specific routes first)
|
||||
Legacy: add explicit path('plugins/<name>/', ...). Modern CyberPanel uses
|
||||
pluginHolder.urls for all plugins — skip to avoid duplicate routes and root-only writes.
|
||||
"""
|
||||
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')
|
||||
urls_path = "/usr/local/CyberCP/CyberCP/urls.py"
|
||||
_url_lines = pluginInstaller._read_lines_from_protected_file(urls_path)
|
||||
content = ''.join(_url_lines)
|
||||
if "include('pluginHolder.urls')" in content or 'include("pluginHolder.urls")' in content:
|
||||
pluginInstaller.stdOut(
|
||||
'pluginHolder.urls found; skipping per-plugin urls.py line for %s (dynamic routes).' % pluginName
|
||||
)
|
||||
return
|
||||
data = content.splitlines(keepends=True)
|
||||
out = []
|
||||
urlPatternAdded = False
|
||||
|
||||
for items in data:
|
||||
# Insert plugin URL BEFORE the generic 'plugins/' line
|
||||
# This ensures more specific routes are matched first
|
||||
if items.find("path('plugins/', include('pluginHolder.urls'))") > -1 or items.find("path(\"plugins/\", include('pluginHolder.urls'))") > -1:
|
||||
if items.find("path('plugins/', include('pluginHolder.urls'))") > -1 or items.find(
|
||||
"path(\"plugins/\", include('pluginHolder.urls'))"
|
||||
) > -1:
|
||||
if not urlPatternAdded:
|
||||
writeToFile.writelines(pluginInstaller.getUrlPattern(pluginName))
|
||||
out.append(pluginInstaller.getUrlPattern(pluginName))
|
||||
urlPatternAdded = True
|
||||
writeToFile.writelines(items)
|
||||
out.append(items)
|
||||
else:
|
||||
writeToFile.writelines(items)
|
||||
|
||||
# Fallback: if 'plugins/' line not found, insert after 'manageservices'
|
||||
out.append(items)
|
||||
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', encoding='utf-8')
|
||||
pluginInstaller.stdOut("Warning: 'plugins/' line not found, using fallback insertion after 'manageservices'")
|
||||
out = []
|
||||
for items in data:
|
||||
if items.find("manageservices") > -1:
|
||||
writeToFile.writelines(items)
|
||||
writeToFile.writelines(pluginInstaller.getUrlPattern(pluginName))
|
||||
out.append(items)
|
||||
out.append(pluginInstaller.getUrlPattern(pluginName))
|
||||
urlPatternAdded = True
|
||||
else:
|
||||
writeToFile.writelines(items)
|
||||
|
||||
writeToFile.close()
|
||||
out.append(items)
|
||||
pluginInstaller._write_lines_to_protected_file(urls_path, out)
|
||||
|
||||
@staticmethod
|
||||
def informCyberPanel(pluginName):
|
||||
@@ -214,19 +303,19 @@ class pluginInstaller:
|
||||
|
||||
@staticmethod
|
||||
def addInterfaceLink(pluginName):
|
||||
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')
|
||||
|
||||
path_html = "/usr/local/CyberCP/baseTemplate/templates/baseTemplate/index.html"
|
||||
data = pluginInstaller._read_lines_from_protected_file(path_html)
|
||||
out = []
|
||||
for items in data:
|
||||
if items.find("{# pluginsList #}") > -1:
|
||||
writeToFile.writelines(items)
|
||||
writeToFile.writelines(" ")
|
||||
writeToFile.writelines(
|
||||
'<li><a href="{% url \'' + pluginName + '\' %}" title="{% trans \'' + pluginName + '\' %}"><span>{% trans "' + pluginName + '" %}</span></a></li>\n')
|
||||
out.append(items)
|
||||
out.append(" ")
|
||||
out.append(
|
||||
'<li><a href="{% url \'' + pluginName + '\' %}" title="{% trans \'' + pluginName + '\' %}"><span>{% trans "' + pluginName + '" %}</span></a></li>\n'
|
||||
)
|
||||
else:
|
||||
writeToFile.writelines(items)
|
||||
|
||||
writeToFile.close()
|
||||
out.append(items)
|
||||
pluginInstaller._write_lines_to_protected_file(path_html, out)
|
||||
|
||||
@staticmethod
|
||||
def staticContent():
|
||||
@@ -469,10 +558,12 @@ class pluginInstaller:
|
||||
def removeFromSettings(pluginName):
|
||||
settings_path = "/usr/local/CyberCP/CyberCP/settings.py"
|
||||
try:
|
||||
with open(settings_path, 'r', encoding='utf-8') as f:
|
||||
data = f.readlines()
|
||||
except (OSError, IOError) as e:
|
||||
raise Exception(f'Cannot read {settings_path}: {e}. Ensure the panel user can read it.')
|
||||
data = pluginInstaller._read_lines_from_protected_file(settings_path)
|
||||
except (OSError, IOError, PermissionError) as e:
|
||||
raise Exception(
|
||||
f'Cannot read {settings_path}: {e}. Ensure the panel user can read it '
|
||||
f'(e.g. chgrp lscpd {settings_path} && chmod 640 {settings_path}).'
|
||||
)
|
||||
in_installed_apps = False
|
||||
out_lines = []
|
||||
for i, items in enumerate(data):
|
||||
@@ -484,9 +575,8 @@ class pluginInstaller:
|
||||
continue
|
||||
out_lines.append(items)
|
||||
try:
|
||||
with open(settings_path, 'w', encoding='utf-8') as writeToFile:
|
||||
writeToFile.writelines(out_lines)
|
||||
except (OSError, IOError) as e:
|
||||
pluginInstaller._write_lines_to_protected_file(settings_path, out_lines)
|
||||
except (OSError, IOError, PermissionError) as e:
|
||||
raise Exception(
|
||||
f'Cannot write {settings_path}: {e}. '
|
||||
'Ensure the file is writable by the panel user (e.g. chgrp lscpd ... ; chmod g+w ...).'
|
||||
@@ -496,10 +586,12 @@ class pluginInstaller:
|
||||
def removeFromURLs(pluginName):
|
||||
urls_path = "/usr/local/CyberCP/CyberCP/urls.py"
|
||||
try:
|
||||
with open(urls_path, 'r', encoding='utf-8') as f:
|
||||
data = f.readlines()
|
||||
except (OSError, IOError) as e:
|
||||
raise Exception(f'Cannot read {urls_path}: {e}.')
|
||||
data = pluginInstaller._read_lines_from_protected_file(urls_path)
|
||||
except (OSError, IOError, PermissionError) as e:
|
||||
raise Exception(
|
||||
f'Cannot read {urls_path}: {e}. '
|
||||
f'As root: chgrp lscpd {urls_path} && chmod 640 {urls_path}'
|
||||
)
|
||||
out_lines = []
|
||||
for items in data:
|
||||
if (f"plugins/{pluginName}/" in items or f"'{pluginName}.urls'" in items or f'"{pluginName}.urls"' in items or
|
||||
@@ -507,9 +599,8 @@ class pluginInstaller:
|
||||
continue
|
||||
out_lines.append(items)
|
||||
try:
|
||||
with open(urls_path, 'w', encoding='utf-8') as f:
|
||||
f.writelines(out_lines)
|
||||
except (OSError, IOError) as e:
|
||||
pluginInstaller._write_lines_to_protected_file(urls_path, out_lines)
|
||||
except (OSError, IOError, PermissionError) as e:
|
||||
raise Exception(f'Cannot write {urls_path}: {e}. Ensure the file is writable by the panel user (chgrp lscpd; chmod g+w).')
|
||||
|
||||
@staticmethod
|
||||
@@ -521,15 +612,14 @@ class pluginInstaller:
|
||||
|
||||
@staticmethod
|
||||
def removeInterfaceLink(pluginName):
|
||||
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')
|
||||
|
||||
path_html = "/usr/local/CyberCP/baseTemplate/templates/baseTemplate/index.html"
|
||||
data = pluginInstaller._read_lines_from_protected_file(path_html)
|
||||
out = []
|
||||
for items in data:
|
||||
if items.find(pluginName) > -1 and items.find('<li>') > -1:
|
||||
continue
|
||||
else:
|
||||
writeToFile.writelines(items)
|
||||
writeToFile.close()
|
||||
out.append(items)
|
||||
pluginInstaller._write_lines_to_protected_file(path_html, out)
|
||||
|
||||
@staticmethod
|
||||
def removeMigrations(pluginName):
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -171,14 +171,28 @@ app.controller('createUserCtr', function ($scope, $http) {
|
||||
|
||||
|
||||
/* Java script code to modify user account */
|
||||
app.controller('modifyUser', function ($scope, $http) {
|
||||
app.controller('modifyUser', function ($scope, $http, $timeout) {
|
||||
|
||||
var qrCode = window.qr = new QRious({
|
||||
element: document.getElementById('qr'),
|
||||
var qrEl = document.getElementById('qr');
|
||||
var qrCode = window.qr = (qrEl && typeof QRious !== 'undefined') ? new QRious({
|
||||
element: qrEl,
|
||||
size: 200,
|
||||
value: 'QRious'
|
||||
});
|
||||
}) : null;
|
||||
if (!qrCode && qrEl) {
|
||||
try { window.qr = new QRious({ element: qrEl, size: 200, value: 'QRious' }); } catch (e) { /* ignore */ }
|
||||
}
|
||||
|
||||
$scope.userSearch = '';
|
||||
/* Prefer global set by inline script (before Angular); fallback to script tag */
|
||||
var list = (typeof window.__CP_ACCT_NAMES !== 'undefined' && Array.isArray(window.__CP_ACCT_NAMES))
|
||||
? window.__CP_ACCT_NAMES
|
||||
: (function() {
|
||||
var el = document.getElementById('acctNamesData');
|
||||
if (!el || !el.textContent) return [];
|
||||
try { return JSON.parse(el.textContent); } catch (e) { return []; }
|
||||
})();
|
||||
$scope.acctNamesList = Array.isArray(list) ? list : [];
|
||||
|
||||
$scope.userModificationLoading = true;
|
||||
$scope.acctDetailsFetched = true;
|
||||
@@ -253,9 +267,7 @@ app.controller('modifyUser', function ($scope, $http) {
|
||||
$scope.formattedSecretKey = response.data.secretKey.match(/.{1,4}/g).join(' ');
|
||||
|
||||
// Update the QR code with new provisioning URI
|
||||
qrCode.set({
|
||||
value: response.data.otpauth
|
||||
});
|
||||
if (qrCode) qrCode.set({ value: response.data.otpauth });
|
||||
|
||||
// Show success message
|
||||
alert('2FA secret has been successfully regenerated! Please update your authenticator app with the new QR code or secret key.');
|
||||
@@ -271,20 +283,24 @@ app.controller('modifyUser', function ($scope, $http) {
|
||||
// WebAuthn Functions
|
||||
$scope.loadWebAuthnData = function() {
|
||||
if (!$scope.accountUsername) return;
|
||||
|
||||
$scope.webauthnDataLoaded = false;
|
||||
var url = '/webauthn/credentials/' + $scope.accountUsername + '/';
|
||||
|
||||
$http.get(url).then(function(response) {
|
||||
if (response.data.success) {
|
||||
$scope.webauthnCredentials = response.data.credentials;
|
||||
$scope.webauthnCredentials = response.data.credentials || [];
|
||||
$scope.webauthnEnabled = response.data.settings.enabled;
|
||||
$scope.webauthnRequirePasskey = response.data.settings.require_passkey;
|
||||
$scope.webauthnAllowMultiple = response.data.settings.allow_multiple_credentials;
|
||||
$scope.webauthnMaxCredentials = response.data.settings.max_credentials;
|
||||
$scope.canAddCredential = response.data.settings.can_add_credential;
|
||||
$scope.canAddCredential = !!response.data.settings.can_add_credential;
|
||||
$scope.webauthnDataLoaded = true;
|
||||
} else {
|
||||
$scope.canAddCredential = true;
|
||||
}
|
||||
}, function(error) {
|
||||
console.error('Error loading WebAuthn data:', error);
|
||||
$scope.canAddCredential = true;
|
||||
$scope.webauthnCredentials = [];
|
||||
});
|
||||
};
|
||||
|
||||
@@ -294,27 +310,59 @@ app.controller('modifyUser', function ($scope, $http) {
|
||||
} else {
|
||||
$scope.webauthnCredentials = [];
|
||||
$scope.canAddCredential = true;
|
||||
$scope.webauthnDataLoaded = false;
|
||||
}
|
||||
};
|
||||
|
||||
/* Inline passkey UX like diabetes.newstargeted.com/profile?tab=2fa: name input + button + message area, no modal */
|
||||
$scope.newPasskeyName = '';
|
||||
$scope.webauthnMessage = '';
|
||||
$scope.webauthnMessageError = false;
|
||||
$scope.registerPasskeyLoading = false;
|
||||
|
||||
$scope.registerNewPasskey = function() {
|
||||
if (!window.cyberPanelWebAuthn) {
|
||||
alert('WebAuthn is not supported in this browser');
|
||||
if (!$scope.accountUsername) {
|
||||
$scope.webauthnMessage = 'Please select a user account first.';
|
||||
$scope.webauthnMessageError = true;
|
||||
return;
|
||||
}
|
||||
|
||||
var credentialName = prompt('Enter a name for this passkey:', 'Passkey ' + new Date().toLocaleDateString());
|
||||
if (!credentialName) return;
|
||||
|
||||
window.cyberPanelWebAuthn.registerPasskey($scope.accountUsername, credentialName)
|
||||
if (typeof window.cyberPanelWebAuthn === 'undefined') {
|
||||
$scope.webauthnMessage = 'WebAuthn script not loaded. Refresh the page (Ctrl+F5) and try again.';
|
||||
$scope.webauthnMessageError = true;
|
||||
return;
|
||||
}
|
||||
if (typeof window.cyberPanelWebAuthn.isSupported !== 'function' || !window.cyberPanelWebAuthn.isSupported()) {
|
||||
$scope.webauthnMessage = 'WebAuthn is not supported in this browser.';
|
||||
$scope.webauthnMessageError = true;
|
||||
return;
|
||||
}
|
||||
var name = ($scope.newPasskeyName || '').trim() || 'Security key';
|
||||
$scope.webauthnMessage = '';
|
||||
$scope.webauthnMessageError = false;
|
||||
$scope.registerPasskeyLoading = true;
|
||||
var username = $scope.accountUsername;
|
||||
window.cyberPanelWebAuthn.registerPasskey(username, name, { silent: true })
|
||||
.then(function(response) {
|
||||
if (response.success) {
|
||||
$scope.loadWebAuthnData();
|
||||
$scope.$apply();
|
||||
if (response && response.success) {
|
||||
$scope.webauthnMessage = 'Passkey registered successfully.';
|
||||
$scope.webauthnMessageError = false;
|
||||
$scope.newPasskeyName = '';
|
||||
$timeout(function() { $scope.loadWebAuthnData(); }, 0);
|
||||
} else {
|
||||
$scope.webauthnMessage = (response && response.error) ? response.error : 'Registration failed.';
|
||||
$scope.webauthnMessageError = true;
|
||||
}
|
||||
})
|
||||
.catch(function(error) {
|
||||
var msg = (error && error.message) ? error.message : 'Passkey registration failed.';
|
||||
if (error && error.name === 'NotAllowedError') msg = 'Registration was cancelled or timed out.';
|
||||
$scope.webauthnMessage = msg;
|
||||
$scope.webauthnMessageError = true;
|
||||
console.error('Error registering passkey:', error);
|
||||
})
|
||||
.finally(function() {
|
||||
$scope.registerPasskeyLoading = false;
|
||||
if (!$scope.$$phase && !$scope.$root.$$phase) { try { $scope.$apply(); } catch (e) { /* already applied */ } }
|
||||
});
|
||||
};
|
||||
|
||||
@@ -435,14 +483,11 @@ app.controller('modifyUser', function ($scope, $http) {
|
||||
$scope.webauthnTimeout = 60;
|
||||
$scope.webauthnCredentials = [];
|
||||
$scope.canAddCredential = true;
|
||||
|
||||
$scope.webauthnDataLoaded = false;
|
||||
// Load WebAuthn settings and credentials
|
||||
$scope.loadWebAuthnData();
|
||||
|
||||
qrCode.set({
|
||||
value: userDetails.otpauth
|
||||
});
|
||||
|
||||
if (qrCode) qrCode.set({ value: userDetails.otpauth });
|
||||
|
||||
$scope.userModificationLoading = true;
|
||||
$scope.acctDetailsFetched = false;
|
||||
@@ -537,8 +582,8 @@ app.controller('modifyUser', function ($scope, $http) {
|
||||
|
||||
$http.post(url, data, config).then(function(response) {
|
||||
ListInitialDatas(response);
|
||||
// Save WebAuthn settings after successful user modification
|
||||
if (response.data.saveStatus == 1) {
|
||||
// Save WebAuthn settings after successful user modification (only if WebAuthn script loaded)
|
||||
if (response.data.saveStatus == 1 && window.cyberPanelWebAuthn) {
|
||||
$scope.saveWebAuthnSettings();
|
||||
}
|
||||
}, cantLoadInitialDatas);
|
||||
@@ -554,7 +599,7 @@ app.controller('modifyUser', function ($scope, $http) {
|
||||
$scope.userModified = false;
|
||||
$scope.canotModifyUser = false; // hide modify error on success
|
||||
$scope.couldNotConnect = true;
|
||||
$scope.canotFetchDetails = true;
|
||||
$scope.canotFetchDetails = false; // hide "Cannot fetch details" on save success
|
||||
$scope.detailsFetched = true;
|
||||
$scope.userAccountsLimit = true;
|
||||
$scope.accountTypeView = true;
|
||||
@@ -592,7 +637,7 @@ app.controller('modifyUser', function ($scope, $http) {
|
||||
$scope.couldNotConnect = false;
|
||||
$scope.canotFetchDetails = true;
|
||||
$scope.detailsFetched = true;
|
||||
|
||||
$scope.errorMessage = (response && response.data && (response.data.error_message || response.data.message || response.data.errorMessage)) || 'Unknown error';
|
||||
|
||||
}
|
||||
|
||||
@@ -1934,7 +1979,11 @@ app.controller('listTableUsers', function ($scope, $http) {
|
||||
|
||||
var UserToDelete;
|
||||
|
||||
$scope.populateCurrentRecords = function () {
|
||||
/**
|
||||
* Reload the user table from the server.
|
||||
* @param {boolean} [suppressSuccessNotify=false] - When true, no success toast (avoids double popups after delete/edit/suspend when the caller already notified).
|
||||
*/
|
||||
$scope.populateCurrentRecords = function (suppressSuccessNotify) {
|
||||
$scope.cyberpanelLoading = false;
|
||||
|
||||
url = "/users/fetchTableUsers";
|
||||
@@ -1958,11 +2007,13 @@ app.controller('listTableUsers', function ($scope, $http) {
|
||||
|
||||
$scope.records = JSON.parse(response.data.data);
|
||||
|
||||
safePNotify({
|
||||
title: 'Success!',
|
||||
text: 'Users successfully fetched!',
|
||||
type: 'success'
|
||||
});
|
||||
if (!suppressSuccessNotify) {
|
||||
safePNotify({
|
||||
title: 'Success!',
|
||||
text: 'Users successfully fetched!',
|
||||
type: 'success'
|
||||
});
|
||||
}
|
||||
|
||||
} else {
|
||||
safePNotify({
|
||||
@@ -1984,7 +2035,8 @@ app.controller('listTableUsers', function ($scope, $http) {
|
||||
}
|
||||
|
||||
};
|
||||
$scope.populateCurrentRecords();
|
||||
/* Initial load: silent (no "fetched" toast on every page open) */
|
||||
$scope.populateCurrentRecords(true);
|
||||
|
||||
|
||||
$scope.deleteUserInitial = function (name){
|
||||
@@ -2014,7 +2066,7 @@ app.controller('listTableUsers', function ($scope, $http) {
|
||||
function ListInitialDatas(response) {
|
||||
$scope.cyberpanelLoading = true;
|
||||
if (response.data.deleteStatus === 1) {
|
||||
$scope.populateCurrentRecords();
|
||||
$scope.populateCurrentRecords(true);
|
||||
hideModalById('deleteModal');
|
||||
safePNotify({
|
||||
title: 'Success!',
|
||||
@@ -2078,7 +2130,7 @@ app.controller('listTableUsers', function ($scope, $http) {
|
||||
function ListInitialDatas(response) {
|
||||
|
||||
if (response.data.status === 1) {
|
||||
$scope.populateCurrentRecords();
|
||||
$scope.populateCurrentRecords(true);
|
||||
hideModalById('editModal');
|
||||
safePNotify({
|
||||
title: 'Success!',
|
||||
@@ -2132,7 +2184,7 @@ app.controller('listTableUsers', function ($scope, $http) {
|
||||
$scope.cyberpanelLoading = true;
|
||||
|
||||
if (response.data.status === 1) {
|
||||
$scope.populateCurrentRecords();
|
||||
$scope.populateCurrentRecords(true);
|
||||
hideModalById('editModal');
|
||||
safePNotify({
|
||||
title: 'Success!',
|
||||
@@ -2186,7 +2238,7 @@ app.controller('listTableUsers', function ($scope, $http) {
|
||||
function ListInitialDatas(response) {
|
||||
$scope.cyberpanelLoading = true;
|
||||
if (response.data.status === 1) {
|
||||
$scope.populateCurrentRecords();
|
||||
$scope.populateCurrentRecords(true);
|
||||
safePNotify({
|
||||
title: 'Success!',
|
||||
text: 'Action successfully started.',
|
||||
|
||||
@@ -51,7 +51,7 @@ class HomeDirectoryManager:
|
||||
return home_dirs
|
||||
|
||||
except Exception as e:
|
||||
logging.CyberCPLogFileWriter.writeToFile(f"Error detecting home directories: {str(e)}")
|
||||
logging.writeToFile(f"Error detecting home directories: {str(e)}")
|
||||
return []
|
||||
|
||||
@staticmethod
|
||||
@@ -103,7 +103,7 @@ class HomeDirectoryManager:
|
||||
return home_dirs[0]['path']
|
||||
|
||||
except Exception as e:
|
||||
logging.CyberCPLogFileWriter.writeToFile(f"Error selecting best home directory: {str(e)}")
|
||||
logging.writeToFile(f"Error selecting best home directory: {str(e)}")
|
||||
return '/home'
|
||||
|
||||
@staticmethod
|
||||
@@ -134,7 +134,7 @@ class HomeDirectoryManager:
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
logging.CyberCPLogFileWriter.writeToFile(f"Error creating user directory: {str(e)}")
|
||||
logging.writeToFile(f"Error creating user directory: {str(e)}")
|
||||
return False
|
||||
|
||||
@staticmethod
|
||||
@@ -154,7 +154,7 @@ class HomeDirectoryManager:
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
logging.CyberCPLogFileWriter.writeToFile(f"Error setting ownership: {str(e)}")
|
||||
logging.writeToFile(f"Error setting ownership: {str(e)}")
|
||||
return False
|
||||
|
||||
@staticmethod
|
||||
@@ -188,7 +188,7 @@ class HomeDirectoryManager:
|
||||
return True, "User migrated successfully"
|
||||
|
||||
except Exception as e:
|
||||
logging.CyberCPLogFileWriter.writeToFile(f"Error migrating user: {str(e)}")
|
||||
logging.writeToFile(f"Error migrating user: {str(e)}")
|
||||
return False, str(e)
|
||||
|
||||
@staticmethod
|
||||
@@ -217,7 +217,7 @@ class HomeDirectoryManager:
|
||||
return stats
|
||||
|
||||
except Exception as e:
|
||||
logging.CyberCPLogFileWriter.writeToFile(f"Error getting home directory stats: {str(e)}")
|
||||
logging.writeToFile(f"Error getting home directory stats: {str(e)}")
|
||||
return None
|
||||
|
||||
@staticmethod
|
||||
|
||||
@@ -35,7 +35,7 @@ def loadHomeDirectoryManagement(request):
|
||||
return proc.render()
|
||||
|
||||
except Exception as e:
|
||||
logging.CyberCPLogFileWriter.writeToFile(f"Error loading home directory management: {str(e)}")
|
||||
logging.writeToFile(f"Error loading home directory management: {str(e)}")
|
||||
return ACLManager.loadError()
|
||||
|
||||
def detectHomeDirectories(request):
|
||||
@@ -71,7 +71,7 @@ def detectHomeDirectories(request):
|
||||
})
|
||||
|
||||
except Exception as e:
|
||||
logging.CyberCPLogFileWriter.writeToFile(f"Error detecting home directories: {str(e)}")
|
||||
logging.writeToFile(f"Error detecting home directories: {str(e)}")
|
||||
return JsonResponse({'status': 0, 'error_message': str(e)})
|
||||
|
||||
def updateHomeDirectory(request):
|
||||
@@ -109,7 +109,7 @@ def updateHomeDirectory(request):
|
||||
return JsonResponse({'status': 0, 'error_message': 'Home directory not found'})
|
||||
|
||||
except Exception as e:
|
||||
logging.CyberCPLogFileWriter.writeToFile(f"Error updating home directory: {str(e)}")
|
||||
logging.writeToFile(f"Error updating home directory: {str(e)}")
|
||||
return JsonResponse({'status': 0, 'error_message': str(e)})
|
||||
|
||||
def deleteHomeDirectory(request):
|
||||
@@ -149,7 +149,7 @@ def deleteHomeDirectory(request):
|
||||
return JsonResponse({'status': 0, 'error_message': 'Home directory not found'})
|
||||
|
||||
except Exception as e:
|
||||
logging.CyberCPLogFileWriter.writeToFile(f"Error deleting home directory: {str(e)}")
|
||||
logging.writeToFile(f"Error deleting home directory: {str(e)}")
|
||||
return JsonResponse({'status': 0, 'error_message': str(e)})
|
||||
|
||||
def getHomeDirectoryStats(request):
|
||||
@@ -165,7 +165,7 @@ def getHomeDirectoryStats(request):
|
||||
return JsonResponse({'status': 1, 'stats': stats})
|
||||
|
||||
except Exception as e:
|
||||
logging.CyberCPLogFileWriter.writeToFile(f"Error getting home directory stats: {str(e)}")
|
||||
logging.writeToFile(f"Error getting home directory stats: {str(e)}")
|
||||
return JsonResponse({'status': 0, 'error_message': str(e)})
|
||||
|
||||
def getUserHomeDirectories(request):
|
||||
@@ -174,7 +174,9 @@ def getUserHomeDirectories(request):
|
||||
userID = request.session['userID']
|
||||
currentACL = ACLManager.loadedACL(userID)
|
||||
|
||||
if currentACL['admin'] != 1 and currentACL['createNewUser'] != 1:
|
||||
# Same visibility as create user page: admins, ACL editors, and users allowed to create accounts
|
||||
if (currentACL['admin'] != 1 and currentACL['createNewUser'] != 1
|
||||
and currentACL.get('changeUserACL', 0) != 1):
|
||||
return JsonResponse({'status': 0, 'error_message': 'Unauthorized access'})
|
||||
|
||||
# Get active home directories (tables home_directories / user_home_mappings may not exist yet)
|
||||
@@ -195,7 +197,7 @@ def getUserHomeDirectories(request):
|
||||
return JsonResponse({'status': 1, 'directories': directories})
|
||||
|
||||
except Exception as e:
|
||||
logging.CyberCPLogFileWriter.writeToFile(f"Error getting user home directories: {str(e)}")
|
||||
logging.writeToFile(f"Error getting user home directories: {str(e)}")
|
||||
# If tables don't exist (e.g. user_home_mappings), return empty list so Modify Website still works
|
||||
return JsonResponse({'status': 1, 'directories': []})
|
||||
|
||||
@@ -251,5 +253,5 @@ def migrateUser(request):
|
||||
return JsonResponse({'status': 0, 'error_message': 'Target home directory not found'})
|
||||
|
||||
except Exception as e:
|
||||
logging.CyberCPLogFileWriter.writeToFile(f"Error migrating user: {str(e)}")
|
||||
logging.writeToFile(f"Error migrating user: {str(e)}")
|
||||
return JsonResponse({'status': 0, 'error_message': str(e)})
|
||||
|
||||
@@ -773,6 +773,7 @@ app.controller('createACLCTRL', function ($scope, $http) {
|
||||
//
|
||||
|
||||
$scope.versionManagement = false;
|
||||
$scope.managePlugins = false;
|
||||
|
||||
// User Management
|
||||
|
||||
@@ -854,6 +855,7 @@ app.controller('createACLCTRL', function ($scope, $http) {
|
||||
|
||||
//
|
||||
versionManagement: $scope.versionManagement,
|
||||
managePlugins: $scope.managePlugins,
|
||||
|
||||
// User Management
|
||||
|
||||
@@ -977,6 +979,7 @@ app.controller('createACLCTRL', function ($scope, $http) {
|
||||
//
|
||||
|
||||
$scope.versionManagement = true;
|
||||
$scope.managePlugins = true;
|
||||
|
||||
// User Management
|
||||
|
||||
@@ -1048,6 +1051,7 @@ app.controller('createACLCTRL', function ($scope, $http) {
|
||||
//
|
||||
|
||||
$scope.versionManagement = false;
|
||||
$scope.managePlugins = false;
|
||||
|
||||
// User Management
|
||||
|
||||
@@ -1232,6 +1236,7 @@ app.controller('modifyACLCtrl', function ($scope, $http) {
|
||||
//
|
||||
|
||||
$scope.versionManagement = Boolean(response.data.versionManagement);
|
||||
$scope.managePlugins = Boolean(response.data.managePlugins);
|
||||
|
||||
// User Management
|
||||
|
||||
@@ -1333,6 +1338,7 @@ app.controller('modifyACLCtrl', function ($scope, $http) {
|
||||
adminStatus: $scope.makeAdmin,
|
||||
//
|
||||
versionManagement: $scope.versionManagement,
|
||||
managePlugins: $scope.managePlugins,
|
||||
|
||||
// User Management
|
||||
|
||||
@@ -1456,6 +1462,7 @@ app.controller('modifyACLCtrl', function ($scope, $http) {
|
||||
//
|
||||
|
||||
$scope.versionManagement = true;
|
||||
$scope.managePlugins = true;
|
||||
|
||||
// User Management
|
||||
|
||||
@@ -1527,6 +1534,7 @@ app.controller('modifyACLCtrl', function ($scope, $http) {
|
||||
//
|
||||
|
||||
$scope.versionManagement = false;
|
||||
$scope.managePlugins = false;
|
||||
|
||||
// User Management
|
||||
|
||||
@@ -1979,7 +1987,11 @@ app.controller('listTableUsers', function ($scope, $http) {
|
||||
|
||||
var UserToDelete;
|
||||
|
||||
$scope.populateCurrentRecords = function () {
|
||||
/**
|
||||
* Reload the user table from the server.
|
||||
* @param {boolean} [suppressSuccessNotify=false] - When true, no success toast (avoids double popups after delete/edit/suspend when the caller already notified).
|
||||
*/
|
||||
$scope.populateCurrentRecords = function (suppressSuccessNotify) {
|
||||
$scope.cyberpanelLoading = false;
|
||||
|
||||
url = "/users/fetchTableUsers";
|
||||
@@ -2003,11 +2015,13 @@ app.controller('listTableUsers', function ($scope, $http) {
|
||||
|
||||
$scope.records = JSON.parse(response.data.data);
|
||||
|
||||
safePNotify({
|
||||
title: 'Success!',
|
||||
text: 'Users successfully fetched!',
|
||||
type: 'success'
|
||||
});
|
||||
if (!suppressSuccessNotify) {
|
||||
safePNotify({
|
||||
title: 'Success!',
|
||||
text: 'Users successfully fetched!',
|
||||
type: 'success'
|
||||
});
|
||||
}
|
||||
|
||||
} else {
|
||||
safePNotify({
|
||||
@@ -2029,7 +2043,8 @@ app.controller('listTableUsers', function ($scope, $http) {
|
||||
}
|
||||
|
||||
};
|
||||
$scope.populateCurrentRecords();
|
||||
/* Initial load: silent (no "fetched" toast on every page open) */
|
||||
$scope.populateCurrentRecords(true);
|
||||
|
||||
|
||||
$scope.deleteUserInitial = function (name){
|
||||
@@ -2059,7 +2074,7 @@ app.controller('listTableUsers', function ($scope, $http) {
|
||||
function ListInitialDatas(response) {
|
||||
$scope.cyberpanelLoading = true;
|
||||
if (response.data.deleteStatus === 1) {
|
||||
$scope.populateCurrentRecords();
|
||||
$scope.populateCurrentRecords(true);
|
||||
hideModalById('deleteModal');
|
||||
safePNotify({
|
||||
title: 'Success!',
|
||||
@@ -2123,7 +2138,7 @@ app.controller('listTableUsers', function ($scope, $http) {
|
||||
function ListInitialDatas(response) {
|
||||
|
||||
if (response.data.status === 1) {
|
||||
$scope.populateCurrentRecords();
|
||||
$scope.populateCurrentRecords(true);
|
||||
hideModalById('editModal');
|
||||
safePNotify({
|
||||
title: 'Success!',
|
||||
@@ -2177,7 +2192,7 @@ app.controller('listTableUsers', function ($scope, $http) {
|
||||
$scope.cyberpanelLoading = true;
|
||||
|
||||
if (response.data.status === 1) {
|
||||
$scope.populateCurrentRecords();
|
||||
$scope.populateCurrentRecords(true);
|
||||
hideModalById('editModal');
|
||||
safePNotify({
|
||||
title: 'Success!',
|
||||
@@ -2231,7 +2246,7 @@ app.controller('listTableUsers', function ($scope, $http) {
|
||||
function ListInitialDatas(response) {
|
||||
$scope.cyberpanelLoading = true;
|
||||
if (response.data.status === 1) {
|
||||
$scope.populateCurrentRecords();
|
||||
$scope.populateCurrentRecords(true);
|
||||
safePNotify({
|
||||
title: 'Success!',
|
||||
text: 'Action successfully started.',
|
||||
|
||||
@@ -162,6 +162,9 @@
|
||||
<div class="checkbox-wrapper">
|
||||
<label><input ng-model="versionManagement" type="checkbox" value=""> {% trans "Version Management" %}</label>
|
||||
</div>
|
||||
<div class="checkbox-wrapper">
|
||||
<label><input ng-model="managePlugins" type="checkbox" value=""> {% trans "Plugin management" %} ({% trans "Plugin Store and installed plugins" %})</label>
|
||||
</div>
|
||||
<div class="checkbox-wrapper">
|
||||
<div class="form-label" style="margin-bottom: 0.5rem;">{% trans "User Management" %}</div>
|
||||
<label><input ng-model="createNewUser" type="checkbox" value=""> {% trans "Create New User" %}</label>
|
||||
|
||||
@@ -283,9 +283,11 @@
|
||||
<div class="form-col-6">
|
||||
<div class="form-group">
|
||||
<label class="form-label">{% trans "Access Control List (ACL)" %}</label>
|
||||
<select ng-model="selectedACL" class="form-control">
|
||||
{# ng-init: Angular otherwise leaves selectedACL undefined until user picks an option, so POST omits ACL and user creation fails. #}
|
||||
<select ng-model="selectedACL" class="form-control"
|
||||
ng-init="selectedACL = selectedACL || '{{ default_acl_name|escapejs }}'">
|
||||
{% for items in aclNames %}
|
||||
<option>{{ items }}</option>
|
||||
<option value="{{ items|escape }}">{{ items }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
<p class="help-text">{% trans "Select the permission set for this user" %}</p>
|
||||
|
||||
@@ -374,6 +374,10 @@
|
||||
<input ng-model="versionManagement" type="checkbox" id="versionManagement">
|
||||
<label for="versionManagement" class="checkbox-label">{% trans "Version Management" %}</label>
|
||||
</div>
|
||||
<div class="checkbox-wrapper">
|
||||
<input ng-model="managePlugins" type="checkbox" id="managePlugins">
|
||||
<label for="managePlugins" class="checkbox-label">{% trans "Plugin management" %} ({% trans "Plugin Store and installed plugins" %})</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -124,6 +124,7 @@ class TestUserManagement(TestCase):
|
||||
data_ret = {'aclName': 'hello', 'makeAdmin':1,
|
||||
'createNewUser': 1,
|
||||
'versionManagement': 1,
|
||||
'managePlugins': 0,
|
||||
'listUsers': 1,
|
||||
'resellerCenter': 1,
|
||||
'deleteUser': 1,
|
||||
@@ -190,6 +191,7 @@ class TestUserManagement(TestCase):
|
||||
'adminStatus':1,
|
||||
'createNewUser': 1,
|
||||
'versionManagement': 1,
|
||||
'managePlugins': 0,
|
||||
'listUsers': 1,
|
||||
'resellerCenter': 1,
|
||||
'deleteUser': 1,
|
||||
|
||||
@@ -4,6 +4,8 @@
|
||||
from django.shortcuts import render, redirect
|
||||
from django.http import HttpResponse
|
||||
from django.db import models
|
||||
from django.db.utils import IntegrityError, ProgrammingError, OperationalError
|
||||
from django.views.decorators.csrf import ensure_csrf_cookie
|
||||
from loginSystem.views import loadLoginPage
|
||||
from loginSystem.models import Administrator, ACL
|
||||
import json
|
||||
@@ -50,24 +52,31 @@ def viewProfile(request):
|
||||
return proc.render()
|
||||
|
||||
|
||||
@ensure_csrf_cookie
|
||||
def createUser(request):
|
||||
userID = request.session['userID']
|
||||
currentACL = ACLManager.loadedACL(userID)
|
||||
|
||||
if currentACL['admin'] == 1:
|
||||
aclNames = ACLManager.unFileteredACLs()
|
||||
default_acl = aclNames[0] if aclNames else 'user'
|
||||
proc = httpProc(request, 'userManagment/createUser.html',
|
||||
{'aclNames': aclNames, 'securityLevels': SecurityLevel.list()})
|
||||
{'aclNames': aclNames, 'default_acl_name': default_acl,
|
||||
'securityLevels': SecurityLevel.list()})
|
||||
return proc.render()
|
||||
elif currentACL['changeUserACL'] == 1:
|
||||
aclNames = ACLManager.unFileteredACLs()
|
||||
default_acl = aclNames[0] if aclNames else 'user'
|
||||
proc = httpProc(request, 'userManagment/createUser.html',
|
||||
{'aclNames': aclNames, 'securityLevels': SecurityLevel.list()})
|
||||
{'aclNames': aclNames, 'default_acl_name': default_acl,
|
||||
'securityLevels': SecurityLevel.list()})
|
||||
return proc.render()
|
||||
elif currentACL['createNewUser'] == 1:
|
||||
aclNames = ['user']
|
||||
default_acl = 'user'
|
||||
proc = httpProc(request, 'userManagment/createUser.html',
|
||||
{'aclNames': aclNames, 'securityLevels': SecurityLevel.list()})
|
||||
{'aclNames': aclNames, 'default_acl_name': default_acl,
|
||||
'securityLevels': SecurityLevel.list()})
|
||||
return proc.render()
|
||||
else:
|
||||
return ACLManager.loadError()
|
||||
@@ -213,8 +222,32 @@ def submitUserCreation(request):
|
||||
email = data['email']
|
||||
userName = data['userName']
|
||||
password = data['password']
|
||||
websitesLimit = data['websitesLimit']
|
||||
selectedACL = data['selectedACL']
|
||||
if userName is None:
|
||||
userName = ''
|
||||
else:
|
||||
userName = str(userName).strip()
|
||||
if not userName:
|
||||
data_ret = {'status': 0, 'createStatus': 0,
|
||||
'error_message': 'Username is required.'}
|
||||
json_data = json.dumps(data_ret)
|
||||
return HttpResponse(json_data, content_type='application/json')
|
||||
if Administrator.objects.filter(userName=userName).exists():
|
||||
data_ret = {'status': 0, 'createStatus': 0,
|
||||
'error_message': 'That username is already in use. Choose a different username.'}
|
||||
json_data = json.dumps(data_ret)
|
||||
return HttpResponse(json_data, content_type='application/json')
|
||||
try:
|
||||
websitesLimit = int(data['websitesLimit'])
|
||||
except (KeyError, TypeError, ValueError):
|
||||
websitesLimit = 0
|
||||
selectedACL = data.get('selectedACL')
|
||||
if selectedACL is None or (isinstance(selectedACL, str) and not selectedACL.strip()):
|
||||
data_ret = {'status': 0, 'createStatus': 0,
|
||||
'error_message': 'Please select an access control list (ACL).'}
|
||||
json_data = json.dumps(data_ret)
|
||||
return HttpResponse(json_data, content_type='application/json')
|
||||
if isinstance(selectedACL, str):
|
||||
selectedACL = selectedACL.strip()
|
||||
selectedHomeDirectory = data.get('selectedHomeDirectory', '')
|
||||
|
||||
if ACLManager.CheckRegEx("^[\w'\-,.][^0-9_!¡?÷?¿/\\+=@#$%ˆ&*(){}|~<>;:[\]]{2,}$", firstName) == 0:
|
||||
@@ -239,7 +272,13 @@ def submitUserCreation(request):
|
||||
except:
|
||||
securityLevel = 'HIGH'
|
||||
|
||||
selectedACL = ACL.objects.get(name=selectedACL)
|
||||
try:
|
||||
selectedACL = ACL.objects.get(name=selectedACL)
|
||||
except ACL.DoesNotExist:
|
||||
data_ret = {'status': 0, 'createStatus': 0,
|
||||
'error_message': 'The selected access level (ACL) was not found. Refresh the page and try again.'}
|
||||
json_data = json.dumps(data_ret)
|
||||
return HttpResponse(json_data, content_type='application/json')
|
||||
|
||||
if selectedACL.adminStatus == 1:
|
||||
type = 1
|
||||
@@ -325,34 +364,43 @@ def submitUserCreation(request):
|
||||
# Handle home directory assignment
|
||||
from .homeDirectoryManager import HomeDirectoryManager
|
||||
from .models import HomeDirectory, UserHomeMapping
|
||||
|
||||
if selectedHomeDirectory:
|
||||
# Use selected home directory
|
||||
|
||||
# Always resolve home_dir after home_path. Previously, if selectedHomeDirectory was set but
|
||||
# the row was missing (DoesNotExist), we set home_path but left home_dir undefined and
|
||||
# UserHomeMapping.objects.create() raised UnboundLocalError → "Failed to create user account".
|
||||
home_dir = None
|
||||
if selectedHomeDirectory not in (None, '', 0, '0', 'false', 'null'):
|
||||
try:
|
||||
home_dir = HomeDirectory.objects.get(id=selectedHomeDirectory)
|
||||
home_path = home_dir.path
|
||||
except HomeDirectory.DoesNotExist:
|
||||
hid = int(str(selectedHomeDirectory).strip())
|
||||
if hid > 0:
|
||||
home_dir = HomeDirectory.objects.get(id=hid)
|
||||
home_path = home_dir.path
|
||||
else:
|
||||
home_path = HomeDirectoryManager.getBestHomeDirectory()
|
||||
except (ValueError, TypeError, HomeDirectory.DoesNotExist):
|
||||
home_path = HomeDirectoryManager.getBestHomeDirectory()
|
||||
else:
|
||||
# Auto-select best home directory
|
||||
home_path = HomeDirectoryManager.getBestHomeDirectory()
|
||||
|
||||
if home_dir is None:
|
||||
try:
|
||||
home_dir = HomeDirectory.objects.get(path=home_path)
|
||||
except HomeDirectory.DoesNotExist:
|
||||
# Create home directory entry if it doesn't exist
|
||||
home_dir = HomeDirectory.objects.create(
|
||||
name=home_path.split('/')[-1],
|
||||
name=(home_path.split('/')[-1] or 'home'),
|
||||
path=home_path,
|
||||
is_active=True,
|
||||
is_default=(home_path == '/home')
|
||||
)
|
||||
|
||||
except HomeDirectory.MultipleObjectsReturned:
|
||||
home_dir = HomeDirectory.objects.filter(path=home_path).order_by('id').first()
|
||||
|
||||
# Create user directory
|
||||
if HomeDirectoryManager.createUserDirectory(userName, home_path):
|
||||
# Create user-home mapping
|
||||
UserHomeMapping.objects.create(
|
||||
# Create user-home mapping (ignore duplicate if re-run)
|
||||
UserHomeMapping.objects.get_or_create(
|
||||
user=newAdmin,
|
||||
home_directory=home_dir
|
||||
defaults={'home_directory': home_dir}
|
||||
)
|
||||
else:
|
||||
# Log error but don't fail user creation
|
||||
@@ -364,11 +412,23 @@ def submitUserCreation(request):
|
||||
final_json = json.dumps(data_ret)
|
||||
return HttpResponse(final_json, content_type='application/json')
|
||||
|
||||
except IntegrityError as e:
|
||||
secure_log_error(e, 'submitUserCreation', request.session.get('userID', 'Unknown'))
|
||||
data_ret = {
|
||||
'status': 0,
|
||||
'createStatus': 0,
|
||||
'error_message': 'That username is already in use. Choose a different username.',
|
||||
}
|
||||
json_data = json.dumps(data_ret)
|
||||
return HttpResponse(json_data, content_type='application/json')
|
||||
|
||||
except Exception as e:
|
||||
secure_log_error(e, 'submitUserCreation', request.session.get('userID', 'Unknown'))
|
||||
data_ret = secure_error_response(e, 'Failed to create user account')
|
||||
if isinstance(data_ret, dict):
|
||||
data_ret['createStatus'] = 0
|
||||
json_data = json.dumps(data_ret)
|
||||
return HttpResponse(json_data)
|
||||
return HttpResponse(json_data, content_type='application/json')
|
||||
|
||||
except KeyError:
|
||||
data_ret = {'status': 0, 'createStatus': 0, 'error_message': "Not logged in as admin", }
|
||||
@@ -570,6 +630,62 @@ def deleteUser(request):
|
||||
return ACLManager.loadError()
|
||||
|
||||
|
||||
def robust_delete_administrator(admin_instance):
|
||||
"""
|
||||
Delete an Administrator when optional WebAuthn tables were never migrated.
|
||||
ORM .delete() still collects WebAuthnCredential etc. and MySQL raises 1146 if
|
||||
webauthn_credentials is missing. Fall back to SQL DELETE on the admin row
|
||||
(and any WebAuthn tables that do exist) after removing child admins (owner FK is integer, not DB FK).
|
||||
"""
|
||||
from django.db import connection
|
||||
|
||||
if admin_instance is None:
|
||||
return
|
||||
|
||||
pk = admin_instance.pk
|
||||
db_table = Administrator._meta.db_table
|
||||
|
||||
try:
|
||||
table_names = set(connection.introspection.table_names())
|
||||
except Exception:
|
||||
table_names = set()
|
||||
|
||||
webauthn_tables = (
|
||||
('webauthn_credentials', 'user_id'),
|
||||
('webauthn_challenges', 'user_id'),
|
||||
('webauthn_sessions', 'user_id'),
|
||||
('webauthn_settings', 'user_id'),
|
||||
)
|
||||
|
||||
for child in Administrator.objects.filter(owner=pk):
|
||||
robust_delete_administrator(child)
|
||||
|
||||
use_orm = 'webauthn_credentials' in table_names
|
||||
if use_orm:
|
||||
try:
|
||||
admin_instance.delete()
|
||||
return
|
||||
except (ProgrammingError, OperationalError) as exc:
|
||||
err = str(exc).lower()
|
||||
if 'webauthn' not in err and "doesn't exist" not in err and 'does not exist' not in err:
|
||||
raise
|
||||
|
||||
qn = connection.ops.quote_name
|
||||
tbl = qn(db_table)
|
||||
col_id = qn('id')
|
||||
with connection.cursor() as cursor:
|
||||
for tname, cname in webauthn_tables:
|
||||
if tname in table_names:
|
||||
try:
|
||||
cursor.execute(
|
||||
'DELETE FROM %s WHERE %s = %%s' % (qn(tname), qn(cname)),
|
||||
[pk],
|
||||
)
|
||||
except (ProgrammingError, OperationalError):
|
||||
pass
|
||||
cursor.execute('DELETE FROM %s WHERE %s = %%s' % (tbl, col_id), [pk])
|
||||
|
||||
|
||||
def submitUserDeletion(request):
|
||||
|
||||
try:
|
||||
@@ -610,31 +726,28 @@ def submitUserDeletion(request):
|
||||
|
||||
user = Administrator.objects.get(userName=accountUsername)
|
||||
|
||||
childUsers = Administrator.objects.filter(owner=user.pk)
|
||||
|
||||
for items in childUsers:
|
||||
items.delete()
|
||||
|
||||
user.delete()
|
||||
robust_delete_administrator(user)
|
||||
|
||||
data_ret = {'status': 1, 'deleteStatus': 1, 'error_message': 'None'}
|
||||
json_data = json.dumps(data_ret)
|
||||
return HttpResponse(json_data)
|
||||
return HttpResponse(json_data, content_type='application/json')
|
||||
else:
|
||||
data_ret = {'status': 0, 'deleteStatus': 0, 'error_message': 'Not enough privileges.'}
|
||||
json_data = json.dumps(data_ret)
|
||||
return HttpResponse(json_data)
|
||||
return HttpResponse(json_data, content_type='application/json')
|
||||
|
||||
except Exception as e:
|
||||
secure_log_error(e, 'submitUserDeletion', request.session.get('userID', 'Unknown'))
|
||||
data_ret = secure_error_response(e, 'Failed to delete user account')
|
||||
if isinstance(data_ret, dict):
|
||||
data_ret['deleteStatus'] = 0
|
||||
json_data = json.dumps(data_ret)
|
||||
return HttpResponse(json_data)
|
||||
return HttpResponse(json_data, content_type='application/json')
|
||||
|
||||
except KeyError:
|
||||
data_ret = {'deleteStatus': 0, 'error_message': "Not logged in as admin", }
|
||||
json_data = json.dumps(data_ret)
|
||||
return HttpResponse(json_data)
|
||||
return HttpResponse(json_data, content_type='application/json')
|
||||
|
||||
|
||||
def createNewACL(request):
|
||||
@@ -1089,7 +1202,8 @@ def userMigration(request):
|
||||
return proc.render()
|
||||
|
||||
except Exception as e:
|
||||
logging.CyberCPLogFileWriter.writeToFile(f"Error loading user migration: {str(e)}")
|
||||
from plogical.CyberCPLogFileWriter import CyberCPLogFileWriter as _cp_log
|
||||
_cp_log.writeToFile(f"Error loading user migration: {str(e)}")
|
||||
return ACLManager.loadError()
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user