mirror of
https://github.com/usmannasir/cyberpanel.git
synced 2026-02-05 22:29:05 +01:00
@@ -1130,19 +1130,12 @@ app.controller('dashboardStatsController', function ($scope, $http, $timeout) {
|
||||
console.log('Parsed responseData from string:', responseData);
|
||||
} catch (e) {
|
||||
console.error('Failed to parse response as JSON:', e);
|
||||
console.error('Raw response string:', responseData);
|
||||
// Try to extract error from string
|
||||
if (responseData.includes('error')) {
|
||||
if (typeof PNotify !== 'undefined') {
|
||||
new PNotify({
|
||||
title: 'Error',
|
||||
text: 'Failed to block IP address: ' + responseData,
|
||||
type: 'error',
|
||||
delay: 5000
|
||||
});
|
||||
}
|
||||
return;
|
||||
var errorMsg = responseData && responseData.length ? responseData : 'Failed to block IP address';
|
||||
if (typeof PNotify !== 'undefined') {
|
||||
new PNotify({ title: 'Error', text: errorMsg, type: 'error', delay: 5000 });
|
||||
}
|
||||
$scope.blockingIP = null;
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1217,28 +1210,20 @@ app.controller('dashboardStatsController', function ($scope, $http, $timeout) {
|
||||
$scope.lastErrorTime = Date.now();
|
||||
|
||||
var errorMessage = 'Failed to block IP address';
|
||||
if (err.data) {
|
||||
var errData = err.data;
|
||||
if (typeof errData === 'string') {
|
||||
try {
|
||||
errData = JSON.parse(errData);
|
||||
} catch (e) {
|
||||
errorMessage = errData || errorMessage;
|
||||
var errData = err.data;
|
||||
if (typeof errData === 'string') {
|
||||
try {
|
||||
errData = JSON.parse(errData);
|
||||
} catch (e) {
|
||||
if (errData && errData.length) {
|
||||
errorMessage = errData.length > 200 ? errData.substring(0, 200) + '...' : errData;
|
||||
}
|
||||
}
|
||||
if (errData && typeof errData === 'object') {
|
||||
if (errData.error_message) {
|
||||
errorMessage = errData.error_message;
|
||||
} else if (errData.error) {
|
||||
errorMessage = errData.error;
|
||||
} else if (errData.message) {
|
||||
errorMessage = errData.message;
|
||||
}
|
||||
}
|
||||
} else if (err.statusText) {
|
||||
errorMessage = err.statusText;
|
||||
}
|
||||
if (errData && typeof errData === 'object') {
|
||||
errorMessage = errData.error_message || errData.error || errData.message || errorMessage;
|
||||
} else if (err.status) {
|
||||
errorMessage = `HTTP ${err.status}: ${err.statusText || 'Unknown error'}`;
|
||||
errorMessage = 'HTTP ' + err.status + ': ' + (errorMessage);
|
||||
}
|
||||
|
||||
console.error('Final error message:', errorMessage);
|
||||
|
||||
@@ -1380,56 +1380,38 @@
|
||||
}
|
||||
|
||||
$.ajax({
|
||||
url: '/base/blockIPAddress',
|
||||
url: '/firewall/addBannedIP',
|
||||
type: 'POST',
|
||||
contentType: 'application/json',
|
||||
headers: {
|
||||
'X-CSRFToken': getCookie('csrftoken')
|
||||
},
|
||||
data: JSON.stringify({
|
||||
'ip_address': ipAddress,
|
||||
'reason': 'Security alert detected from dashboard'
|
||||
'ip': ipAddress,
|
||||
'reason': 'Brute force attack detected from SSH Security Analysis',
|
||||
'duration': 'permanent'
|
||||
}),
|
||||
success: function(data) {
|
||||
// Handle both success and error responses
|
||||
if (data.status === 1) {
|
||||
showNotification('success', data.message || 'IP address blocked successfully');
|
||||
if (data && data.status === 1) {
|
||||
showNotification('success', data.message || 'IP address blocked successfully. Manage in Firewall > Banned IPs.');
|
||||
// Refresh the page to update the blocked IPs list
|
||||
setTimeout(() => {
|
||||
location.reload();
|
||||
}, 1000);
|
||||
setTimeout(function() { location.reload(); }, 1000);
|
||||
} else {
|
||||
// Handle error response - check for both 'error' and 'error_message' fields
|
||||
var errorMsg = data.error || data.error_message || data.message || 'Failed to block IP address';
|
||||
var errorMsg = (data && (data.error_message || data.error || data.message)) || 'Failed to block IP address';
|
||||
showNotification('error', errorMsg);
|
||||
}
|
||||
},
|
||||
error: function(xhr, status, error) {
|
||||
// Handle network errors and parse JSON errors
|
||||
console.error('Ban IP error:', xhr, status, error);
|
||||
console.error('Ban IP error:', xhr.status, xhr.responseText);
|
||||
var errorMsg = 'Failed to block IP address. Please try again.';
|
||||
|
||||
// Log full response for debugging
|
||||
console.log('Response status:', xhr.status);
|
||||
console.log('Response text:', xhr.responseText);
|
||||
|
||||
if (xhr.responseJSON) {
|
||||
errorMsg = xhr.responseJSON.error || xhr.responseJSON.error_message || xhr.responseJSON.message || errorMsg;
|
||||
console.log('Parsed error from JSON:', errorMsg);
|
||||
} else if (xhr.responseText) {
|
||||
try {
|
||||
var errorData = JSON.parse(xhr.responseText);
|
||||
errorMsg = errorData.error || errorData.error_message || errorData.message || errorMsg;
|
||||
console.log('Parsed error from text:', errorMsg);
|
||||
} catch(e) {
|
||||
console.error('Failed to parse error response:', e);
|
||||
// If parsing fails, try to extract error from response text
|
||||
if (xhr.responseText.includes('error')) {
|
||||
errorMsg = xhr.responseText.substring(0, 200);
|
||||
}
|
||||
}
|
||||
var data = xhr.responseJSON;
|
||||
if (!data && xhr.responseText) {
|
||||
try { data = JSON.parse(xhr.responseText); } catch(e) {}
|
||||
}
|
||||
if (data && (data.error_message || data.error || data.message)) {
|
||||
errorMsg = data.error_message || data.error || data.message;
|
||||
}
|
||||
|
||||
showNotification('error', errorMsg);
|
||||
},
|
||||
complete: function() {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{% load i18n %}
|
||||
{% get_current_language as LANGUAGE_CODE %}
|
||||
{% with CP_VERSION="2.4.4.1" %}
|
||||
{% with CP_VERSION=CYBERPANEL_FULL_VERSION|default:"2.5.5.dev" %}
|
||||
<!DOCTYPE html>
|
||||
<html lang="en" ng-app="CyberCP">
|
||||
<head>
|
||||
|
||||
@@ -16,6 +16,7 @@ from plogical.acl import ACLManager
|
||||
from manageServices.models import PDNSStatus
|
||||
from django.views.decorators.csrf import ensure_csrf_cookie, csrf_exempt
|
||||
from plogical.processUtilities import ProcessUtilities
|
||||
from plogical.firewallUtilities import FirewallUtilities
|
||||
from plogical.httpProc import httpProc
|
||||
from websiteFunctions.models import Websites, WPSites
|
||||
from databases.models import Databases
|
||||
@@ -1318,54 +1319,14 @@ def blockIPAddress(request):
|
||||
'error': 'Invalid IP address'
|
||||
}), content_type='application/json', status=400)
|
||||
|
||||
# Use firewalld (CSF has been discontinued)
|
||||
# Use FirewallUtilities so firewall-cmd runs with proper privileges (root/lscpd)
|
||||
firewall_cmd = 'firewalld'
|
||||
reason = data.get('reason', 'Security alert detected from dashboard')
|
||||
try:
|
||||
# Verify firewalld is active using subprocess for better security
|
||||
import subprocess
|
||||
firewalld_check = subprocess.run(['systemctl', 'is-active', 'firewalld'],
|
||||
capture_output=True, text=True, timeout=10)
|
||||
if not (firewalld_check.returncode == 0 and 'active' in firewalld_check.stdout):
|
||||
return HttpResponse(json.dumps({
|
||||
'status': 0,
|
||||
'error': 'Firewalld is not active. Please enable firewalld service.'
|
||||
}), content_type='application/json', status=500)
|
||||
except subprocess.TimeoutExpired:
|
||||
return HttpResponse(json.dumps({
|
||||
'status': 0,
|
||||
'error': 'Timeout checking firewalld status'
|
||||
}), content_type='application/json', status=500)
|
||||
success, msg = FirewallUtilities.blockIP(ip_address, reason)
|
||||
except Exception as e:
|
||||
return HttpResponse(json.dumps({
|
||||
'status': 0,
|
||||
'error': f'Cannot check firewalld status: {str(e)}'
|
||||
}), content_type='application/json', status=500)
|
||||
|
||||
# Block the IP address using firewalld with subprocess for better security
|
||||
success = False
|
||||
error_message = ''
|
||||
|
||||
try:
|
||||
# Use subprocess with explicit argument lists to prevent injection
|
||||
rich_rule = f'rule family=ipv4 source address={ip_address} drop'
|
||||
add_rule_cmd = ['firewall-cmd', '--permanent', '--add-rich-rule', rich_rule]
|
||||
|
||||
# Execute the add rule command
|
||||
result = subprocess.run(add_rule_cmd, capture_output=True, text=True, timeout=30)
|
||||
if result.returncode == 0:
|
||||
# Reload firewall rules
|
||||
reload_cmd = ['firewall-cmd', '--reload']
|
||||
reload_result = subprocess.run(reload_cmd, capture_output=True, text=True, timeout=30)
|
||||
if reload_result.returncode == 0:
|
||||
success = True
|
||||
else:
|
||||
error_message = f'Failed to reload firewall rules: {reload_result.stderr}'
|
||||
else:
|
||||
error_message = f'Failed to add firewall rule: {result.stderr}'
|
||||
except subprocess.TimeoutExpired:
|
||||
error_message = 'Firewall command timed out'
|
||||
except Exception as e:
|
||||
error_message = f'Firewall command failed: {str(e)}'
|
||||
success = False
|
||||
msg = str(e)
|
||||
|
||||
if success:
|
||||
# Add to banned IPs JSON file for consistency with firewall page
|
||||
@@ -1430,7 +1391,7 @@ def blockIPAddress(request):
|
||||
else:
|
||||
return HttpResponse(json.dumps({
|
||||
'status': 0,
|
||||
'error': error_message or 'Failed to block IP address'
|
||||
'error': msg or 'Failed to block IP address'
|
||||
}), content_type='application/json', status=500)
|
||||
|
||||
except json.JSONDecodeError as e:
|
||||
|
||||
78
deploy-email-limits-fix.sh
Executable file
78
deploy-email-limits-fix.sh
Executable file
@@ -0,0 +1,78 @@
|
||||
#!/bin/bash
|
||||
# Deploy Email Limits fix to a CyberPanel installation.
|
||||
# Copies recommended mailServer files and optionally restarts lscpd.
|
||||
#
|
||||
# Usage (run from anywhere):
|
||||
# sudo bash /home/cyberpanel-repo/deploy-email-limits-fix.sh
|
||||
# sudo bash deploy-email-limits-fix.sh [REPO_DIR] [CP_DIR]
|
||||
#
|
||||
# Or from repo root: cd /home/cyberpanel-repo && sudo bash deploy-email-limits-fix.sh
|
||||
|
||||
set -e
|
||||
|
||||
log() { echo "[$(date +%Y-%m-%d\ %H:%M:%S)] $*"; }
|
||||
err() { log "ERROR: $*" >&2; }
|
||||
|
||||
# Resolve REPO_DIR: explicit arg, then script dir, then common locations
|
||||
if [[ -n "$1" && -d "$1/mailServer" ]]; then
|
||||
REPO_DIR="$1"
|
||||
shift
|
||||
elif [[ -d "$(cd "$(dirname "${BASH_SOURCE[0]}")" 2>/dev/null && pwd)/mailServer" ]]; then
|
||||
REPO_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
elif [[ -d "/home/cyberpanel-repo/mailServer" ]]; then
|
||||
REPO_DIR="/home/cyberpanel-repo"
|
||||
elif [[ -d "./mailServer" ]]; then
|
||||
REPO_DIR="$(pwd)"
|
||||
else
|
||||
err "Repo not found. Use: sudo bash /home/cyberpanel-repo/deploy-email-limits-fix.sh"
|
||||
err "Or: cd /path/to/cyberpanel-repo && sudo bash deploy-email-limits-fix.sh"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
CP_DIR="${1:-/usr/local/CyberCP}"
|
||||
RESTART_LSCPD="${RESTART_LSCPD:-1}"
|
||||
|
||||
if [[ ! -d "$CP_DIR" ]]; then
|
||||
err "CyberPanel directory not found: $CP_DIR"
|
||||
exit 1
|
||||
fi
|
||||
if [[ ! -d "$REPO_DIR/mailServer" ]]; then
|
||||
err "Repo mailServer not found in: $REPO_DIR"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
log "REPO_DIR=$REPO_DIR"
|
||||
log "CP_DIR=$CP_DIR"
|
||||
|
||||
FILES=(
|
||||
"mailServer/mailserverManager.py"
|
||||
"mailServer/templates/mailServer/EmailLimits.html"
|
||||
"mailServer/static/mailServer/mailServer.js"
|
||||
"mailServer/static/mailServer/emailLimitsController.js"
|
||||
)
|
||||
|
||||
for rel in "${FILES[@]}"; do
|
||||
src="$REPO_DIR/$rel"
|
||||
dst="$CP_DIR/$rel"
|
||||
if [[ ! -f "$src" ]]; then
|
||||
err "Source missing: $src"
|
||||
exit 1
|
||||
fi
|
||||
mkdir -p "$(dirname "$dst")"
|
||||
cp -f "$src" "$dst"
|
||||
log "Copied: $rel"
|
||||
done
|
||||
|
||||
if [[ "$RESTART_LSCPD" =~ ^(1|yes|true)$ ]]; then
|
||||
if systemctl is-active --quiet lscpd 2>/dev/null; then
|
||||
log "Restarting lscpd..."
|
||||
systemctl restart lscpd || { err "lscpd restart failed"; exit 1; }
|
||||
log "lscpd restarted."
|
||||
else
|
||||
log "lscpd not running or not a systemd service; skip restart."
|
||||
fi
|
||||
else
|
||||
log "Skipping restart (set RESTART_LSCPD=1 to restart lscpd)."
|
||||
fi
|
||||
|
||||
log "Deploy complete. Hard-refresh /email/EmailLimits in the browser (Ctrl+Shift+R)."
|
||||
64
deploy-ftp-create-account-fix.sh
Normal file
64
deploy-ftp-create-account-fix.sh
Normal file
@@ -0,0 +1,64 @@
|
||||
#!/bin/bash
|
||||
# Deploy FTP Create Account template fix to a CyberPanel installation.
|
||||
# Copies the updated createFTPAccount.html and optionally restarts lscpd.
|
||||
#
|
||||
# Usage (run from anywhere):
|
||||
# sudo bash /home/cyberpanel-repo/deploy-ftp-create-account-fix.sh
|
||||
# sudo bash deploy-ftp-create-account-fix.sh [REPO_DIR] [CP_DIR]
|
||||
#
|
||||
# Or from repo root: cd /home/cyberpanel-repo && sudo bash deploy-ftp-create-account-fix.sh
|
||||
|
||||
set -e
|
||||
|
||||
log() { echo "[$(date +%Y-%m-%d\ %H:%M:%S)] $*"; }
|
||||
err() { log "ERROR: $*" >&2; }
|
||||
|
||||
# Resolve REPO_DIR
|
||||
if [[ -n "$1" && -d "$1/ftp" ]]; then
|
||||
REPO_DIR="$1"
|
||||
shift
|
||||
elif [[ -d "$(cd "$(dirname "${BASH_SOURCE[0]}")" 2>/dev/null && pwd)/ftp" ]]; then
|
||||
REPO_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
elif [[ -d "/home/cyberpanel-repo/ftp" ]]; then
|
||||
REPO_DIR="/home/cyberpanel-repo"
|
||||
elif [[ -d "./ftp" ]]; then
|
||||
REPO_DIR="$(pwd)"
|
||||
else
|
||||
err "Repo not found. Use: sudo bash /home/cyberpanel-repo/deploy-ftp-create-account-fix.sh"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
CP_DIR="${1:-/usr/local/CyberCP}"
|
||||
RESTART_LSCPD="${RESTART_LSCPD:-1}"
|
||||
|
||||
if [[ ! -d "$CP_DIR" ]]; then
|
||||
err "CyberPanel directory not found: $CP_DIR"
|
||||
exit 1
|
||||
fi
|
||||
if [[ ! -f "$REPO_DIR/ftp/templates/ftp/createFTPAccount.html" ]]; then
|
||||
err "Source template not found in: $REPO_DIR"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
log "REPO_DIR=$REPO_DIR"
|
||||
log "CP_DIR=$CP_DIR"
|
||||
|
||||
SRC="$REPO_DIR/ftp/templates/ftp/createFTPAccount.html"
|
||||
DST="$CP_DIR/ftp/templates/ftp/createFTPAccount.html"
|
||||
mkdir -p "$(dirname "$DST")"
|
||||
cp -f "$SRC" "$DST"
|
||||
log "Copied: ftp/templates/ftp/createFTPAccount.html"
|
||||
|
||||
if [[ "$RESTART_LSCPD" =~ ^(1|yes|true)$ ]]; then
|
||||
if systemctl is-active --quiet lscpd 2>/dev/null; then
|
||||
log "Restarting lscpd..."
|
||||
systemctl restart lscpd || { err "lscpd restart failed"; exit 1; }
|
||||
log "lscpd restarted."
|
||||
else
|
||||
log "lscpd not running or not a systemd service; skip restart."
|
||||
fi
|
||||
else
|
||||
log "Skipping restart (set RESTART_LSCPD=1 to restart lscpd)."
|
||||
fi
|
||||
|
||||
log "Deploy complete. Hard-refresh /ftp/createFTPAccount in the browser (Ctrl+Shift+R)."
|
||||
65
deploy-ftp-quotas-table.sh
Normal file
65
deploy-ftp-quotas-table.sh
Normal file
@@ -0,0 +1,65 @@
|
||||
#!/bin/bash
|
||||
# Create the missing ftp_quotas table in the CyberPanel database.
|
||||
# Fixes: (1146, "Table 'cyberpanel.ftp_quotas' doesn't exist") on /ftp/quotaManagement
|
||||
#
|
||||
# Usage:
|
||||
# sudo bash /home/cyberpanel-repo/deploy-ftp-quotas-table.sh
|
||||
# sudo bash deploy-ftp-quotas-table.sh [REPO_DIR] [CP_DIR]
|
||||
|
||||
set -e
|
||||
|
||||
log() { echo "[$(date +%Y-%m-%d\ %H:%M:%S)] $*"; }
|
||||
err() { log "ERROR: $*" >&2; }
|
||||
|
||||
if [[ -n "$1" && -f "$1/sql/create_ftp_quotas.sql" ]]; then
|
||||
REPO_DIR="$1"
|
||||
shift
|
||||
elif [[ -f "$(cd "$(dirname "${BASH_SOURCE[0]}")" 2>/dev/null && pwd)/sql/create_ftp_quotas.sql" ]]; then
|
||||
REPO_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
elif [[ -f "/home/cyberpanel-repo/sql/create_ftp_quotas.sql" ]]; then
|
||||
REPO_DIR="/home/cyberpanel-repo"
|
||||
else
|
||||
err "sql/create_ftp_quotas.sql not found."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
CP_DIR="${1:-/usr/local/CyberCP}"
|
||||
SQL_FILE="$REPO_DIR/sql/create_ftp_quotas.sql"
|
||||
|
||||
if [[ ! -d "$CP_DIR" ]]; then
|
||||
err "CyberPanel directory not found: $CP_DIR"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
log "REPO_DIR=$REPO_DIR"
|
||||
log "CP_DIR=$CP_DIR"
|
||||
|
||||
mkdir -p "$CP_DIR/sql"
|
||||
cp -f "$SQL_FILE" "$CP_DIR/sql/create_ftp_quotas.sql"
|
||||
log "Copied create_ftp_quotas.sql to $CP_DIR/sql/"
|
||||
|
||||
# Run SQL using Django DB connection (no password on command line)
|
||||
log "Creating ftp_quotas table..."
|
||||
export CP_DIR
|
||||
python3 << 'PYEOF'
|
||||
import os
|
||||
import sys
|
||||
|
||||
cp_dir = os.environ.get('CP_DIR', '/usr/local/CyberCP')
|
||||
sys.path.insert(0, cp_dir)
|
||||
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'CyberCP.settings')
|
||||
|
||||
import django
|
||||
django.setup()
|
||||
|
||||
from django.db import connection
|
||||
|
||||
with open(os.path.join(cp_dir, 'sql', 'create_ftp_quotas.sql'), 'r') as f:
|
||||
sql = f.read()
|
||||
|
||||
with connection.cursor() as cursor:
|
||||
cursor.execute(sql)
|
||||
print('Executed CREATE TABLE IF NOT EXISTS ftp_quotas.')
|
||||
PYEOF
|
||||
|
||||
log "Done. Reload https://207.180.193.210:2087/ftp/quotaManagement"
|
||||
@@ -1935,14 +1935,15 @@ class FirewallManager:
|
||||
try:
|
||||
admin = Administrator.objects.get(pk=userID)
|
||||
if admin.acl.adminStatus != 1:
|
||||
return ACLManager.loadError()
|
||||
final_dic = {'status': 0, 'error_message': 'You are not authorized to access this resource.', 'error': 'You are not authorized to access this resource.'}
|
||||
return HttpResponse(json.dumps(final_dic), content_type='application/json', status=403)
|
||||
|
||||
ip = data.get('ip', '').strip()
|
||||
reason = data.get('reason', '').strip()
|
||||
duration = data.get('duration', '24h')
|
||||
|
||||
if not ip or not reason:
|
||||
final_dic = {'status': 0, 'error_message': 'IP address and reason are required'}
|
||||
final_dic = {'status': 0, 'error_message': 'IP address and reason are required', 'error': 'IP address and reason are required'}
|
||||
return HttpResponse(json.dumps(final_dic), content_type='application/json')
|
||||
|
||||
# Validate IP address format
|
||||
@@ -1950,7 +1951,7 @@ class FirewallManager:
|
||||
try:
|
||||
ipaddress.ip_address(ip.split('/')[0]) # Handle CIDR notation
|
||||
except ValueError:
|
||||
final_dic = {'status': 0, 'error_message': 'Invalid IP address format'}
|
||||
final_dic = {'status': 0, 'error_message': 'Invalid IP address format', 'error': 'Invalid IP address format'}
|
||||
return HttpResponse(json.dumps(final_dic), content_type='application/json')
|
||||
|
||||
# Calculate expiration time
|
||||
@@ -1973,7 +1974,8 @@ class FirewallManager:
|
||||
# Check if IP is already banned
|
||||
for banned_ip in banned_ips:
|
||||
if banned_ip.get('ip') == ip and banned_ip.get('active', True):
|
||||
final_dic = {'status': 0, 'error_message': 'IP address %s is already banned' % ip}
|
||||
msg = 'IP address %s is already banned' % ip
|
||||
final_dic = {'status': 0, 'error_message': msg, 'error': msg}
|
||||
return HttpResponse(json.dumps(final_dic), content_type='application/json')
|
||||
|
||||
# Add new banned IP
|
||||
@@ -1991,54 +1993,38 @@ class FirewallManager:
|
||||
# Save to file
|
||||
self._save_banned_ips_store(banned_ips)
|
||||
|
||||
# Apply firewall rule to block the IP using firewalld
|
||||
# Apply firewall rule using FirewallUtilities (runs with proper privileges via ProcessUtilities/lscpd)
|
||||
try:
|
||||
import subprocess
|
||||
# Verify firewalld is active
|
||||
firewalld_check = subprocess.run(['systemctl', 'is-active', 'firewalld'],
|
||||
capture_output=True, text=True, timeout=10)
|
||||
if not (firewalld_check.returncode == 0 and 'active' in firewalld_check.stdout):
|
||||
final_dic = {'status': 0, 'error_message': 'Firewalld is not active. Please enable firewalld service.'}
|
||||
block_ok, block_msg = FirewallUtilities.blockIP(ip, reason)
|
||||
if not block_ok:
|
||||
# Rollback: remove the IP we just added from the store
|
||||
banned_ips_rollback = [b for b in banned_ips if b.get('ip') != ip or not b.get('active', True)]
|
||||
if len(banned_ips_rollback) < len(banned_ips):
|
||||
self._save_banned_ips_store(banned_ips_rollback)
|
||||
logging.CyberCPLogFileWriter.writeToFile('Failed to add firewalld rule for %s: %s' % (ip, block_msg))
|
||||
err_msg = block_msg or 'Failed to add firewall rule'
|
||||
final_dic = {'status': 0, 'error_message': err_msg, 'error': err_msg}
|
||||
return HttpResponse(json.dumps(final_dic), content_type='application/json')
|
||||
|
||||
# Add firewalld rich rule to block the IP
|
||||
rich_rule = f'rule family=ipv4 source address={ip} drop'
|
||||
add_rule_cmd = ['firewall-cmd', '--permanent', '--add-rich-rule', rich_rule]
|
||||
|
||||
# Execute the add rule command
|
||||
result = subprocess.run(add_rule_cmd, capture_output=True, text=True, timeout=30)
|
||||
if result.returncode == 0:
|
||||
# Reload firewall rules
|
||||
reload_cmd = ['firewall-cmd', '--reload']
|
||||
reload_result = subprocess.run(reload_cmd, capture_output=True, text=True, timeout=30)
|
||||
if reload_result.returncode == 0:
|
||||
logging.CyberCPLogFileWriter.writeToFile(f'Banned IP {ip} with reason: {reason}')
|
||||
else:
|
||||
logging.CyberCPLogFileWriter.writeToFile('Failed to reload firewalld for %s: %s' % (ip, reload_result.stderr))
|
||||
final_dic = {'status': 0, 'error_message': 'Failed to reload firewall rules: %s' % reload_result.stderr}
|
||||
return HttpResponse(json.dumps(final_dic), content_type='application/json')
|
||||
else:
|
||||
# Check if rule already exists (this is not an error)
|
||||
if result.stderr and ('ALREADY_ENABLED' in result.stderr or 'already exists' in result.stderr.lower()):
|
||||
logging.CyberCPLogFileWriter.writeToFile('IP %s already blocked in firewalld' % ip)
|
||||
else:
|
||||
logging.CyberCPLogFileWriter.writeToFile('Failed to add firewalld rule for %s: %s' % (ip, result.stderr or ''))
|
||||
final_dic = {'status': 0, 'error_message': 'Failed to add firewall rule: %s' % (result.stderr or 'Unknown error')}
|
||||
return HttpResponse(json.dumps(final_dic), content_type='application/json')
|
||||
except subprocess.TimeoutExpired:
|
||||
logging.CyberCPLogFileWriter.writeToFile('Timeout adding firewalld rule for %s' % ip)
|
||||
final_dic = {'status': 0, 'error_message': 'Firewall command timed out'}
|
||||
return HttpResponse(json.dumps(final_dic), content_type='application/json')
|
||||
logging.CyberCPLogFileWriter.writeToFile(f'Banned IP {ip} with reason: {reason}')
|
||||
except Exception as e:
|
||||
# Rollback store on any exception
|
||||
try:
|
||||
banned_ips_rollback = [b for b in banned_ips if b.get('ip') != ip or not b.get('active', True)]
|
||||
if len(banned_ips_rollback) < len(banned_ips):
|
||||
self._save_banned_ips_store(banned_ips_rollback)
|
||||
except Exception:
|
||||
pass
|
||||
logging.CyberCPLogFileWriter.writeToFile('Failed to add firewalld rule for %s: %s' % (ip, str(e)))
|
||||
final_dic = {'status': 0, 'error_message': 'Firewall command failed: %s' % str(e)}
|
||||
err_msg = 'Firewall command failed: %s' % str(e)
|
||||
final_dic = {'status': 0, 'error_message': err_msg, 'error': err_msg}
|
||||
return HttpResponse(json.dumps(final_dic), content_type='application/json')
|
||||
|
||||
final_dic = {'status': 1, 'message': 'IP address %s has been banned successfully' % ip}
|
||||
return HttpResponse(json.dumps(final_dic), content_type='application/json')
|
||||
|
||||
except BaseException as msg:
|
||||
final_dic = {'status': 0, 'error_message': str(msg)}
|
||||
err_msg = str(msg)
|
||||
final_dic = {'status': 0, 'error_message': err_msg, 'error': err_msg}
|
||||
return HttpResponse(json.dumps(final_dic), content_type='application/json')
|
||||
|
||||
def removeBannedIP(self, userID=None, data=None):
|
||||
@@ -2085,16 +2071,9 @@ class FirewallManager:
|
||||
ip_to_unban = banned_ip.ip_address
|
||||
banned_ip.active = False
|
||||
banned_ip.save()
|
||||
# Remove firewalld rule
|
||||
# Remove firewalld rule using FirewallUtilities (runs with proper privileges)
|
||||
try:
|
||||
import subprocess
|
||||
firewalld_check = subprocess.run(['systemctl', 'is-active', 'firewalld'],
|
||||
capture_output=True, text=True, timeout=10)
|
||||
if firewalld_check.returncode == 0 and 'active' in firewalld_check.stdout:
|
||||
rich_rule = f'rule family=ipv4 source address={ip_to_unban} drop'
|
||||
subprocess.run(['firewall-cmd', '--permanent', '--remove-rich-rule', rich_rule],
|
||||
capture_output=True, text=True, timeout=30)
|
||||
subprocess.run(['firewall-cmd', '--reload'], capture_output=True, text=True, timeout=30)
|
||||
FirewallUtilities.unblockIP(ip_to_unban)
|
||||
except Exception as e:
|
||||
logging.CyberCPLogFileWriter.writeToFile(f'Warning removing firewalld rule: {str(e)}')
|
||||
final_dic = {'status': 1, 'message': f'IP address {ip_to_unban} has been unbanned successfully'}
|
||||
@@ -2124,40 +2103,12 @@ class FirewallManager:
|
||||
# Save updated banned IPs
|
||||
self._save_banned_ips_store(banned_ips)
|
||||
|
||||
# Remove firewalld rule to unblock the IP
|
||||
# Remove firewalld rule using FirewallUtilities (runs with proper privileges)
|
||||
try:
|
||||
import subprocess
|
||||
# Verify firewalld is active
|
||||
firewalld_check = subprocess.run(['systemctl', 'is-active', 'firewalld'],
|
||||
capture_output=True, text=True, timeout=10)
|
||||
if not (firewalld_check.returncode == 0 and 'active' in firewalld_check.stdout):
|
||||
# Firewalld not active, but still mark as unbanned in JSON
|
||||
logging.CyberCPLogFileWriter.writeToFile(f'Warning: Firewalld not active when unbanned IP {ip_to_unban}')
|
||||
else:
|
||||
# Remove firewalld rich rule
|
||||
rich_rule = f'rule family=ipv4 source address={ip_to_unban} drop'
|
||||
remove_rule_cmd = ['firewall-cmd', '--permanent', '--remove-rich-rule', rich_rule]
|
||||
|
||||
# Execute the remove rule command
|
||||
result = subprocess.run(remove_rule_cmd, capture_output=True, text=True, timeout=30)
|
||||
if result.returncode == 0:
|
||||
# Reload firewall rules
|
||||
reload_cmd = ['firewall-cmd', '--reload']
|
||||
reload_result = subprocess.run(reload_cmd, capture_output=True, text=True, timeout=30)
|
||||
if reload_result.returncode == 0:
|
||||
logging.CyberCPLogFileWriter.writeToFile(f'Unbanned IP {ip_to_unban}')
|
||||
else:
|
||||
logging.CyberCPLogFileWriter.writeToFile(f'Warning: Failed to reload firewalld after unbanning {ip_to_unban}: {reload_result.stderr}')
|
||||
else:
|
||||
# Rule might not exist, which is okay
|
||||
if 'NOT_ENABLED' in result.stderr or 'not found' in result.stderr.lower():
|
||||
logging.CyberCPLogFileWriter.writeToFile(f'IP {ip_to_unban} rule not found in firewalld (may have been removed already)')
|
||||
else:
|
||||
logging.CyberCPLogFileWriter.writeToFile(f'Warning: Failed to remove firewalld rule for {ip_to_unban}: {result.stderr}')
|
||||
except subprocess.TimeoutExpired:
|
||||
logging.CyberCPLogFileWriter.writeToFile(f'Timeout removing firewalld rule for {ip_to_unban}')
|
||||
FirewallUtilities.unblockIP(ip_to_unban)
|
||||
logging.CyberCPLogFileWriter.writeToFile(f'Unbanned IP {ip_to_unban}')
|
||||
except Exception as e:
|
||||
logging.CyberCPLogFileWriter.writeToFile(f'Failed to remove firewalld rule for {ip_to_unban}: {str(e)}')
|
||||
logging.CyberCPLogFileWriter.writeToFile(f'Warning removing firewalld rule for {ip_to_unban}: {str(e)}')
|
||||
|
||||
final_dic = {'status': 1, 'message': f'IP address {ip_to_unban} has been unbanned successfully'}
|
||||
final_json = json.dumps(final_dic)
|
||||
|
||||
@@ -15,7 +15,7 @@ app.controller('createFTPAccount', function ($scope, $http) {
|
||||
$scope.generatedPasswordView = true;
|
||||
|
||||
$(document).ready(function () {
|
||||
$( ".ftpDetails" ).hide();
|
||||
$( ".ftpDetails, .account-details" ).hide();
|
||||
$( ".ftpPasswordView" ).hide();
|
||||
|
||||
// Only use select2 if it's actually a function (avoids errors when Rocket Loader defers scripts)
|
||||
@@ -26,9 +26,11 @@ app.controller('createFTPAccount', function ($scope, $http) {
|
||||
$sel.select2();
|
||||
$sel.on('select2:select', function (e) {
|
||||
var data = e.params.data;
|
||||
$scope.ftpDomain = data.text;
|
||||
$scope.$apply();
|
||||
$(".ftpDetails").show();
|
||||
$scope.$evalAsync(function () {
|
||||
$scope.ftpDomain = data.text;
|
||||
$scope.ftpDetails = false;
|
||||
});
|
||||
$(".ftpDetails, .account-details").show();
|
||||
});
|
||||
} else {
|
||||
initNativeSelect();
|
||||
@@ -41,20 +43,23 @@ app.controller('createFTPAccount', function ($scope, $http) {
|
||||
}
|
||||
function initNativeSelect() {
|
||||
$('.create-ftp-acct-select').off('select2:select').on('change', function () {
|
||||
$scope.ftpDomain = $(this).val();
|
||||
$scope.$apply();
|
||||
$(".ftpDetails").show();
|
||||
var val = $(this).val();
|
||||
$scope.$evalAsync(function () {
|
||||
$scope.ftpDomain = val;
|
||||
$scope.ftpDetails = (val && val !== '') ? false : true;
|
||||
});
|
||||
$(".ftpDetails, .account-details").show();
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
$scope.showFTPDetails = function() {
|
||||
if ($scope.ftpDomain && $scope.ftpDomain !== "") {
|
||||
$(".ftpDetails").show();
|
||||
$scope.ftpDetails = false;
|
||||
$(".ftpDetails, .account-details").show();
|
||||
} else {
|
||||
$(".ftpDetails").hide();
|
||||
$scope.ftpDetails = true;
|
||||
$(".ftpDetails, .account-details").hide();
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -452,18 +452,65 @@
|
||||
<div class="col-md-8">
|
||||
<div class="form-group">
|
||||
<label class="form-label">{% trans "Select Website" %}</label>
|
||||
<select ng-model="ftpDomain" ng-change="showFTPDetails()" class="form-control create-ftp-acct-select">
|
||||
<select id="create-ftp-website-select" ng-model="ftpDomain" ng-change="showFTPDetails()" class="form-control create-ftp-acct-select">
|
||||
<option value="">{% trans "Choose a website..." %}</option>
|
||||
{% for items in websiteList %}
|
||||
<option value="{{ items }}">{{ items }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
<script data-cfasync="false">
|
||||
(function() {
|
||||
function hasWebsiteSelected() {
|
||||
var sel = document.getElementById('create-ftp-website-select');
|
||||
if (!sel) return false;
|
||||
if (sel.value && sel.value !== '') return true;
|
||||
var rendered = document.querySelector('.select2-selection__rendered');
|
||||
var text = rendered ? (rendered.title || rendered.textContent || '').trim() : '';
|
||||
return text !== '' && text.indexOf('Choose') === -1;
|
||||
}
|
||||
function showFTPFormSection() {
|
||||
var sel = document.getElementById('create-ftp-website-select');
|
||||
var section = document.querySelector('.account-details.ftpDetails');
|
||||
var container = document.querySelector('.modern-container[ng-controller="createFTPAccount"]');
|
||||
if (section && hasWebsiteSelected()) {
|
||||
section.classList.remove('ng-hide');
|
||||
section.style.setProperty('display', 'block', 'important');
|
||||
try { section.scrollIntoView({ behavior: 'smooth', block: 'nearest' }); } catch (e) {}
|
||||
if (typeof angular !== 'undefined' && container) {
|
||||
try {
|
||||
var scope = angular.element(container).scope();
|
||||
if (scope) {
|
||||
scope.ftpDetails = false;
|
||||
scope.$evalAsync(function() { scope.ftpDetails = false; });
|
||||
}
|
||||
} catch (e) {}
|
||||
}
|
||||
}
|
||||
}
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
var sel = document.getElementById('create-ftp-website-select');
|
||||
if (!sel) return;
|
||||
sel.addEventListener('change', showFTPFormSection);
|
||||
sel.addEventListener('input', showFTPFormSection);
|
||||
if (sel.value) showFTPFormSection();
|
||||
var delays = [50, 150, 350, 600, 1000, 1500, 2200, 3000];
|
||||
for (var i = 0; i < delays.length; i++) {
|
||||
setTimeout(showFTPFormSection, delays[i]);
|
||||
}
|
||||
var pollCount = 0;
|
||||
var pollId = setInterval(function() {
|
||||
showFTPFormSection();
|
||||
if (++pollCount >= 20) clearInterval(pollId);
|
||||
}, 200);
|
||||
});
|
||||
})();
|
||||
</script>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div ng-hide="ftpDetails" class="account-details">
|
||||
<div ng-hide="ftpDetails" class="account-details ftpDetails">
|
||||
<h3><i class="fas fa-cog"></i> {% trans "FTP Account Details" %}</h3>
|
||||
|
||||
<div class="row">
|
||||
|
||||
251
install/env_generator.py
Normal file
251
install/env_generator.py
Normal file
@@ -0,0 +1,251 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
CyberPanel Environment Configuration Generator
|
||||
Generates secure .env file with random passwords during installation
|
||||
"""
|
||||
|
||||
import os
|
||||
import sys
|
||||
import secrets
|
||||
import string
|
||||
import socket
|
||||
import urllib.request
|
||||
import re
|
||||
from pathlib import Path
|
||||
|
||||
def generate_secure_password(length=24):
|
||||
"""
|
||||
Generate a cryptographically secure password
|
||||
|
||||
Args:
|
||||
length: Length of the password to generate (default 24)
|
||||
|
||||
Returns:
|
||||
str: Random password containing uppercase, lowercase, digits and safe special chars
|
||||
"""
|
||||
# Use safe characters that don't require escaping in most contexts
|
||||
safe_chars = string.ascii_letters + string.digits + '!@#$%^&*'
|
||||
return ''.join(secrets.choice(safe_chars) for _ in range(length))
|
||||
|
||||
def generate_secret_key(length=64):
|
||||
"""
|
||||
Generate a cryptographically secure Django secret key
|
||||
|
||||
Args:
|
||||
length: Length of the secret key to generate (default 64)
|
||||
|
||||
Returns:
|
||||
str: Random secret key
|
||||
"""
|
||||
chars = string.ascii_letters + string.digits + '!@#$%^&*(-_=+)'
|
||||
return ''.join(secrets.choice(chars) for _ in range(length))
|
||||
|
||||
def get_public_ip():
|
||||
"""Get the public IP address of the server using multiple methods"""
|
||||
methods = [
|
||||
'https://ipv4.icanhazip.com',
|
||||
'https://api.ipify.org',
|
||||
'https://checkip.amazonaws.com',
|
||||
'https://ipecho.net/plain'
|
||||
]
|
||||
|
||||
for url in methods:
|
||||
try:
|
||||
with urllib.request.urlopen(url, timeout=10) as response:
|
||||
ip = response.read().decode('utf-8').strip()
|
||||
# Validate IP format
|
||||
if re.match(r'^(\d{1,3}\.){3}\d{1,3}$', ip):
|
||||
print(f"✓ Detected public IP: {ip}")
|
||||
return ip
|
||||
except Exception as e:
|
||||
print(f"Failed to get IP from {url}: {e}")
|
||||
continue
|
||||
|
||||
print("⚠️ Could not detect public IP address")
|
||||
return None
|
||||
|
||||
def get_local_ip():
|
||||
"""Get the local IP address of the server"""
|
||||
try:
|
||||
# Connect to a remote address to determine the local IP
|
||||
with socket.socket(socket.AF_INET, socket.SOCK_DGRAM) as s:
|
||||
s.connect(("8.8.8.8", 80))
|
||||
local_ip = s.getsockname()[0]
|
||||
print(f"✓ Detected local IP: {local_ip}")
|
||||
return local_ip
|
||||
except Exception as e:
|
||||
print(f"Failed to detect local IP: {e}")
|
||||
return None
|
||||
|
||||
def create_env_file(cyberpanel_path, mysql_root_password=None, cyberpanel_db_password=None):
|
||||
"""
|
||||
Create .env file with generated secure credentials
|
||||
|
||||
Args:
|
||||
cyberpanel_path: Path to CyberPanel installation directory
|
||||
mysql_root_password: Optional MySQL root password (will generate if None)
|
||||
cyberpanel_db_password: Optional CyberPanel DB password (will generate if None)
|
||||
"""
|
||||
|
||||
# Generate secure passwords if not provided
|
||||
if not mysql_root_password:
|
||||
mysql_root_password = generate_secure_password(24)
|
||||
|
||||
if not cyberpanel_db_password:
|
||||
cyberpanel_db_password = generate_secure_password(24)
|
||||
|
||||
secret_key = generate_secret_key(64)
|
||||
|
||||
# Auto-detect IP addresses for ALLOWED_HOSTS
|
||||
print("🔍 Auto-detecting server IP addresses...")
|
||||
|
||||
# Get hostname and local hostname resolution
|
||||
try:
|
||||
hostname = socket.gethostname()
|
||||
hostname_ip = socket.gethostbyname(hostname)
|
||||
except:
|
||||
hostname = 'localhost'
|
||||
hostname_ip = '127.0.0.1'
|
||||
|
||||
# Get actual local IP address
|
||||
local_ip = get_local_ip()
|
||||
|
||||
# Get public IP address
|
||||
public_ip = get_public_ip()
|
||||
|
||||
# Build ALLOWED_HOSTS list with all detected IPs
|
||||
allowed_hosts = ['localhost', '127.0.0.1']
|
||||
|
||||
# Add hostname if different from localhost
|
||||
if hostname and hostname != 'localhost':
|
||||
allowed_hosts.append(hostname)
|
||||
|
||||
# Add hostname IP if different from localhost
|
||||
if hostname_ip and hostname_ip not in allowed_hosts:
|
||||
allowed_hosts.append(hostname_ip)
|
||||
|
||||
# Add local IP if detected and different
|
||||
if local_ip and local_ip not in allowed_hosts:
|
||||
allowed_hosts.append(local_ip)
|
||||
|
||||
# Add public IP if detected and different
|
||||
if public_ip and public_ip not in allowed_hosts:
|
||||
allowed_hosts.append(public_ip)
|
||||
|
||||
# Add wildcard for maximum compatibility (allows any host)
|
||||
# This ensures CyberPanel works regardless of how the server is accessed
|
||||
allowed_hosts.append('*')
|
||||
|
||||
allowed_hosts_str = ','.join(allowed_hosts)
|
||||
print(f"✓ ALLOWED_HOSTS configured: {allowed_hosts_str}")
|
||||
|
||||
# Create .env content
|
||||
env_content = f"""# CyberPanel Environment Configuration
|
||||
# Generated automatically during installation - DO NOT EDIT MANUALLY
|
||||
# Generated on: {__import__('datetime').datetime.now().strftime('%Y-%m-%d %H:%M:%S')}
|
||||
|
||||
# Django Configuration
|
||||
SECRET_KEY={secret_key}
|
||||
DEBUG=False
|
||||
ALLOWED_HOSTS={allowed_hosts_str}
|
||||
|
||||
# Database Configuration - CyberPanel Database
|
||||
DB_NAME=cyberpanel
|
||||
DB_USER=cyberpanel
|
||||
DB_PASSWORD={cyberpanel_db_password}
|
||||
DB_HOST=localhost
|
||||
DB_PORT=3306
|
||||
|
||||
# Root Database Configuration - MySQL Root Access
|
||||
ROOT_DB_NAME=mysql
|
||||
ROOT_DB_USER=root
|
||||
ROOT_DB_PASSWORD={mysql_root_password}
|
||||
ROOT_DB_HOST=localhost
|
||||
ROOT_DB_PORT=3306
|
||||
|
||||
# Security Settings
|
||||
SECURE_SSL_REDIRECT=False
|
||||
SECURE_HSTS_SECONDS=0
|
||||
SECURE_HSTS_INCLUDE_SUBDOMAINS=False
|
||||
SECURE_HSTS_PRELOAD=False
|
||||
SESSION_COOKIE_SECURE=False
|
||||
CSRF_COOKIE_SECURE=False
|
||||
|
||||
# File Upload Settings
|
||||
DATA_UPLOAD_MAX_MEMORY_SIZE=2147483648
|
||||
|
||||
# Logging Configuration
|
||||
LOG_LEVEL=INFO
|
||||
"""
|
||||
|
||||
# Write .env file
|
||||
env_file_path = os.path.join(cyberpanel_path, '.env')
|
||||
with open(env_file_path, 'w') as f:
|
||||
f.write(env_content)
|
||||
|
||||
# Set secure permissions (owner read/write only)
|
||||
os.chmod(env_file_path, 0o600)
|
||||
|
||||
print(f"✓ Generated secure .env file at: {env_file_path}")
|
||||
print(f"✓ MySQL Root Password: {mysql_root_password}")
|
||||
print(f"✓ CyberPanel DB Password: {cyberpanel_db_password}")
|
||||
print(f"✓ Django Secret Key: {secret_key[:20]}...")
|
||||
|
||||
return {
|
||||
'mysql_root_password': mysql_root_password,
|
||||
'cyberpanel_db_password': cyberpanel_db_password,
|
||||
'secret_key': secret_key
|
||||
}
|
||||
|
||||
def create_env_backup(cyberpanel_path, credentials):
|
||||
"""
|
||||
Create a secure backup of credentials for recovery purposes
|
||||
|
||||
Args:
|
||||
cyberpanel_path: Path to CyberPanel installation directory
|
||||
credentials: Dictionary containing generated credentials
|
||||
"""
|
||||
backup_content = f"""# CyberPanel Credentials Backup
|
||||
# Generated: {__import__('datetime').datetime.now().strftime('%Y-%m-%d %H:%M:%S')}
|
||||
#
|
||||
# IMPORTANT: Store this file securely and delete it after recording credentials
|
||||
# These are your database passwords and should be kept confidential
|
||||
|
||||
MySQL Root Password: {credentials['mysql_root_password']}
|
||||
CyberPanel Database Password: {credentials['cyberpanel_db_password']}
|
||||
Django Secret Key: {credentials['secret_key']}
|
||||
|
||||
# To restore these credentials, copy them to your .env file
|
||||
"""
|
||||
|
||||
backup_file_path = os.path.join(cyberpanel_path, '.env.backup')
|
||||
with open(backup_file_path, 'w') as f:
|
||||
f.write(backup_content)
|
||||
|
||||
# Set secure permissions (owner read/write only)
|
||||
os.chmod(backup_file_path, 0o600)
|
||||
|
||||
print(f"✓ Created credentials backup at: {backup_file_path}")
|
||||
print("⚠️ IMPORTANT: Record these credentials and delete the backup file for security")
|
||||
|
||||
if __name__ == "__main__":
|
||||
if len(sys.argv) < 2:
|
||||
print("Usage: python env_generator.py <cyberpanel_path> [mysql_root_password] [cyberpanel_db_password]")
|
||||
sys.exit(1)
|
||||
|
||||
cyberpanel_path = sys.argv[1]
|
||||
mysql_root_password = sys.argv[2] if len(sys.argv) > 2 else None
|
||||
cyberpanel_db_password = sys.argv[3] if len(sys.argv) > 3 else None
|
||||
|
||||
if not os.path.exists(cyberpanel_path):
|
||||
print(f"Error: CyberPanel path does not exist: {cyberpanel_path}")
|
||||
sys.exit(1)
|
||||
|
||||
try:
|
||||
credentials = create_env_file(cyberpanel_path, mysql_root_password, cyberpanel_db_password)
|
||||
create_env_backup(cyberpanel_path, credentials)
|
||||
print("\n✓ Environment configuration generated successfully!")
|
||||
print("✓ Remember to delete .env.backup after recording credentials")
|
||||
except Exception as e:
|
||||
print(f"Error generating environment configuration: {e}")
|
||||
sys.exit(1)
|
||||
1449
install/installCyberPanel.py
Normal file
1449
install/installCyberPanel.py
Normal file
File diff suppressed because it is too large
Load Diff
@@ -31,6 +31,6 @@ MaxDiskUsage 99
|
||||
CustomerProof yes
|
||||
TLS 1
|
||||
PassivePortRange 40110 40210
|
||||
# Quota enforcement
|
||||
Quota yes
|
||||
# Quota enforcement (maxfiles:maxsizeMB; enables MySQL per-user quotas)
|
||||
Quota 100000:100000
|
||||
|
||||
|
||||
@@ -31,6 +31,6 @@ MaxDiskUsage 99
|
||||
CustomerProof yes
|
||||
TLS 1
|
||||
PassivePortRange 40110 40210
|
||||
# Quota enforcement
|
||||
Quota yes
|
||||
# Quota enforcement (maxfiles:maxsizeMB; enables MySQL per-user quotas)
|
||||
Quota 100000:100000
|
||||
|
||||
|
||||
@@ -43,6 +43,27 @@ import bcrypt
|
||||
import threading as multi
|
||||
import argparse
|
||||
|
||||
|
||||
def _get_email_limits_controller_js():
|
||||
"""Return EmailLimitsNew controller JS: from file or hardcoded fallback so it always works."""
|
||||
try:
|
||||
from django.conf import settings
|
||||
for base in (os.path.dirname(__file__), getattr(settings, 'BASE_DIR', None)):
|
||||
if not base:
|
||||
continue
|
||||
for script_path in (
|
||||
os.path.join(base, 'static', 'mailServer', 'emailLimitsController.js'),
|
||||
os.path.join(base, 'mailServer', 'static', 'mailServer', 'emailLimitsController.js'),
|
||||
):
|
||||
if os.path.isfile(script_path):
|
||||
with open(script_path, 'r') as f:
|
||||
return f.read()
|
||||
except Exception:
|
||||
pass
|
||||
# Hardcoded fallback so page works even when static file is missing or path wrong
|
||||
return r"""(function(){'use strict';var app=typeof window.app!=='undefined'?window.app:angular.module('CyberCP');if(!app)return;app.controller('EmailLimitsNew',function($scope,$http){$scope.creationBox=true;$scope.emailDetails=true;$scope.forwardLoading=false;$scope.forwardError=true;$scope.forwardSuccess=true;$scope.couldNotConnect=true;$scope.notifyBox=true;$scope.showEmailDetails=function(){$scope.creationBox=true;$scope.emailDetails=true;$scope.forwardLoading=true;$scope.forwardError=true;$scope.forwardSuccess=true;$scope.couldNotConnect=true;$scope.notifyBox=true;var url="/email/getEmailsForDomain",data={domain:$scope.emailDomain},config={headers:{'X-CSRFToken':getCookie('csrftoken')}};$http.post(url,data,config).then(function(r){if(r.data.fetchStatus===1){$scope.emails=JSON.parse(r.data.data);$scope.creationBox=true;$scope.emailDetails=false;$scope.forwardLoading=false;$scope.notifyBox=false;}else{$scope.creationBox=true;$scope.emailDetails=true;$scope.forwardLoading=false;$scope.forwardError=false;$scope.errorMessage=r.data.error_message;}},function(){$scope.creationBox=true;$scope.emailDetails=true;$scope.couldNotConnect=false;$scope.notifyBox=false;});};$scope.selectForwardingEmail=function(){$scope.creationBox=false;$scope.emailDetails=false;$scope.forwardLoading=true;$scope.notifyBox=true;var g=$scope.selectedEmail;if($scope.emails)for(var i=0;i<$scope.emails.length;i++)if($scope.emails[i].email===g){$scope.numberofEmails=$scope.emails[i].numberofEmails;$scope.duration=$scope.emails[i].duration;break;}};$scope.SaveChanges=function(){$scope.forwardLoading=true;var url="/email/SaveEmailLimitsNew",data={numberofEmails:$scope.numberofEmails,source:$scope.selectedEmail,duration:$scope.duration},config={headers:{'X-CSRFToken':getCookie('csrftoken')}};$http.post(url,data,config).then(function(r){if(r.data.status===1){$scope.forwardLoading=false;if(typeof PNotify!=='undefined')new PNotify({title:'Success!',text:'Changes applied.',type:'success'});$scope.showEmailDetails();}else{$scope.forwardError=false;$scope.notifyBox=false;if(typeof PNotify!=='undefined')new PNotify({title:'Error!',text:r.data.error_message||'Error',type:'error'});}},function(){$scope.creationBox=true;$scope.couldNotConnect=false;});};});})();"""
|
||||
|
||||
|
||||
class MailServerManager(multi.Thread):
|
||||
|
||||
def __init__(self, request = None, function = None, extraArgs = None):
|
||||
@@ -177,7 +198,9 @@ class MailServerManager(multi.Thread):
|
||||
userID = self.request.session['userID']
|
||||
currentACL = ACLManager.loadedACL(userID)
|
||||
|
||||
if ACLManager.currentContextPermission(currentACL, 'deleteEmail') == 0:
|
||||
# Allow fetch for List Emails (deleteEmail) or Email Limits (emailForwarding)
|
||||
if (ACLManager.currentContextPermission(currentACL, 'deleteEmail') == 0 and
|
||||
ACLManager.currentContextPermission(currentACL, 'emailForwarding') == 0):
|
||||
return ACLManager.loadErrorJson('fetchStatus', 0)
|
||||
|
||||
data = json.loads(self.request.body)
|
||||
@@ -1971,9 +1994,13 @@ protocol sieve {
|
||||
except BaseException as msg:
|
||||
template = 'mailServer/EmailLimits.html'
|
||||
|
||||
# Embed controller script inline so page works (no 404, no file path issues)
|
||||
email_limits_controller_js = _get_email_limits_controller_js()
|
||||
# Prevent </script> in JS from closing the HTML script tag
|
||||
email_limits_controller_js = email_limits_controller_js.replace('</script>', '<\\/script>')
|
||||
|
||||
proc = httpProc(self.request, template,
|
||||
{'websiteList': websitesName, "status": 1}, 'emailForwarding')
|
||||
{'websiteList': websitesName, "status": 1, 'email_limits_controller_js': email_limits_controller_js}, 'emailForwarding')
|
||||
return proc.render()
|
||||
|
||||
def SaveEmailLimitsNew(self):
|
||||
|
||||
146
mailServer/static/mailServer/emailLimitsController.js
Normal file
146
mailServer/static/mailServer/emailLimitsController.js
Normal file
@@ -0,0 +1,146 @@
|
||||
/**
|
||||
* Email Limits page controller - ensures EmailLimitsNew is registered on CyberCP
|
||||
* even if main mailServer.js loads late or fails. Load this script in the
|
||||
* EmailLimits template so the page works reliably.
|
||||
*/
|
||||
(function () {
|
||||
'use strict';
|
||||
var app = (typeof window.app !== 'undefined') ? window.app : angular.module('CyberCP');
|
||||
if (!app) return;
|
||||
|
||||
app.controller('EmailLimitsNew', function ($scope, $http) {
|
||||
$scope.creationBox = true;
|
||||
$scope.emailDetails = true;
|
||||
$scope.forwardLoading = false;
|
||||
$scope.forwardError = true;
|
||||
$scope.forwardSuccess = true;
|
||||
$scope.couldNotConnect = true;
|
||||
$scope.notifyBox = true;
|
||||
|
||||
$scope.showEmailDetails = function () {
|
||||
$scope.creationBox = true;
|
||||
$scope.emailDetails = true;
|
||||
$scope.forwardLoading = true;
|
||||
$scope.forwardError = true;
|
||||
$scope.forwardSuccess = true;
|
||||
$scope.couldNotConnect = true;
|
||||
$scope.notifyBox = true;
|
||||
|
||||
var url = "/email/getEmailsForDomain";
|
||||
var data = { domain: $scope.emailDomain };
|
||||
var config = { headers: { 'X-CSRFToken': getCookie('csrftoken') } };
|
||||
|
||||
$http.post(url, data, config).then(ListInitialDatas, cantLoadInitialDatas);
|
||||
|
||||
function ListInitialDatas(response) {
|
||||
if (response.data.fetchStatus === 1) {
|
||||
$scope.emails = JSON.parse(response.data.data);
|
||||
$scope.creationBox = true;
|
||||
$scope.emailDetails = false;
|
||||
$scope.forwardLoading = false;
|
||||
$scope.forwardError = true;
|
||||
$scope.forwardSuccess = true;
|
||||
$scope.couldNotConnect = true;
|
||||
$scope.notifyBox = false;
|
||||
} else {
|
||||
$scope.creationBox = true;
|
||||
$scope.emailDetails = true;
|
||||
$scope.forwardLoading = false;
|
||||
$scope.forwardError = false;
|
||||
$scope.forwardSuccess = true;
|
||||
$scope.couldNotConnect = true;
|
||||
$scope.notifyBox = false;
|
||||
$scope.errorMessage = response.data.error_message;
|
||||
}
|
||||
}
|
||||
|
||||
function cantLoadInitialDatas() {
|
||||
$scope.creationBox = true;
|
||||
$scope.emailDetails = true;
|
||||
$scope.forwardLoading = false;
|
||||
$scope.forwardError = true;
|
||||
$scope.forwardSuccess = true;
|
||||
$scope.couldNotConnect = false;
|
||||
$scope.notifyBox = false;
|
||||
}
|
||||
};
|
||||
|
||||
$scope.selectForwardingEmail = function () {
|
||||
$scope.creationBox = false;
|
||||
$scope.emailDetails = false;
|
||||
$scope.forwardLoading = true;
|
||||
$scope.forwardError = true;
|
||||
$scope.forwardSuccess = true;
|
||||
$scope.couldNotConnect = true;
|
||||
$scope.notifyBox = true;
|
||||
|
||||
var givenEmail = $scope.selectedEmail;
|
||||
if ($scope.emails) {
|
||||
for (var i = 0; i < $scope.emails.length; i++) {
|
||||
if ($scope.emails[i].email === givenEmail) {
|
||||
$scope.numberofEmails = $scope.emails[i].numberofEmails;
|
||||
$scope.duration = $scope.emails[i].duration;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
$scope.SaveChanges = function () {
|
||||
$scope.creationBox = false;
|
||||
$scope.emailDetails = false;
|
||||
$scope.forwardLoading = true;
|
||||
$scope.forwardError = true;
|
||||
$scope.forwardSuccess = true;
|
||||
$scope.couldNotConnect = true;
|
||||
$scope.notifyBox = true;
|
||||
|
||||
var url = "/email/SaveEmailLimitsNew";
|
||||
var data = {
|
||||
numberofEmails: $scope.numberofEmails,
|
||||
source: $scope.selectedEmail,
|
||||
duration: $scope.duration
|
||||
};
|
||||
var config = { headers: { 'X-CSRFToken': getCookie('csrftoken') } };
|
||||
|
||||
$http.post(url, data, config).then(SaveListInitialDatas, SaveCantLoadInitialDatas);
|
||||
|
||||
function SaveListInitialDatas(response) {
|
||||
if (response.data.status === 1) {
|
||||
$scope.creationBox = false;
|
||||
$scope.emailDetails = false;
|
||||
$scope.forwardLoading = false;
|
||||
$scope.forwardError = true;
|
||||
$scope.forwardSuccess = true;
|
||||
$scope.couldNotConnect = true;
|
||||
$scope.notifyBox = true;
|
||||
if (typeof PNotify === 'function') {
|
||||
new PNotify({ title: 'Success!', text: 'Changes applied.', type: 'success' });
|
||||
}
|
||||
$scope.showEmailDetails();
|
||||
} else {
|
||||
$scope.creationBox = false;
|
||||
$scope.emailDetails = false;
|
||||
$scope.forwardLoading = false;
|
||||
$scope.forwardError = false;
|
||||
$scope.forwardSuccess = true;
|
||||
$scope.couldNotConnect = true;
|
||||
$scope.notifyBox = false;
|
||||
if (typeof PNotify === 'function') {
|
||||
new PNotify({ title: 'Error!', text: response.data.error_message || 'Error', type: 'error' });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function SaveCantLoadInitialDatas() {
|
||||
$scope.creationBox = true;
|
||||
$scope.emailDetails = true;
|
||||
$scope.forwardLoading = false;
|
||||
$scope.forwardError = true;
|
||||
$scope.forwardSuccess = true;
|
||||
$scope.couldNotConnect = false;
|
||||
$scope.notifyBox = false;
|
||||
}
|
||||
};
|
||||
});
|
||||
})();
|
||||
@@ -2,6 +2,14 @@
|
||||
* Created by usman on 8/15/17.
|
||||
*/
|
||||
|
||||
// Ensure app is available (get existing CyberCP module so controllers register correctly)
|
||||
if (typeof app === 'undefined') {
|
||||
if (typeof window !== 'undefined' && typeof window.app !== 'undefined') {
|
||||
app = window.app;
|
||||
} else {
|
||||
app = angular.module('CyberCP');
|
||||
}
|
||||
}
|
||||
|
||||
/* Java script code to create account */
|
||||
app.controller('createEmailAccount', function ($scope, $http) {
|
||||
@@ -1506,6 +1514,7 @@ app.controller('EmailLimitsNew', function ($scope, $http) {
|
||||
// Given email to search for
|
||||
var givenEmail = $scope.selectedEmail;
|
||||
|
||||
if ($scope.emails) {
|
||||
for (var i = 0; i < $scope.emails.length; i++) {
|
||||
if ($scope.emails[i].email === givenEmail) {
|
||||
// Extract numberofEmails and duration
|
||||
@@ -1515,14 +1524,11 @@ app.controller('EmailLimitsNew', function ($scope, $http) {
|
||||
$scope.numberofEmails = numberofEmails;
|
||||
$scope.duration = duration;
|
||||
|
||||
// Use numberofEmails and duration as needed
|
||||
console.log("Number of emails:", numberofEmails);
|
||||
console.log("Duration:", duration);
|
||||
|
||||
// Break out of the loop since the email is found
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
{% extends "baseTemplate/index.html" %}
|
||||
{% load i18n %}
|
||||
{% load static %}
|
||||
{% block title %}{% trans "Email Limits - CyberPanel" %}{% endblock %}
|
||||
{% block content %}
|
||||
|
||||
@@ -387,4 +388,13 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% endblock %}
|
||||
|
||||
{% block footer_scripts %}
|
||||
{# EmailLimitsNew controller: inline from view (preferred) or fallback to static file #}
|
||||
{% if email_limits_controller_js %}
|
||||
<script data-cfasync="false">{{ email_limits_controller_js|safe }}</script>
|
||||
{% else %}
|
||||
<script src="{% static 'mailServer/emailLimitsController.js' %}?v=2.4.4.1" data-cfasync="false"></script>
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
@@ -118,22 +118,23 @@ class FirewallUtilities:
|
||||
|
||||
logging.CyberCPLogFileWriter.writeToFile(f"Blocking IP address: {ip_address} - Reason: {reason}")
|
||||
|
||||
# executioner returns 1 on success, 0 on failure
|
||||
result = ProcessUtilities.executioner(command)
|
||||
if result == 0:
|
||||
if result == 1:
|
||||
logging.CyberCPLogFileWriter.writeToFile(f"Successfully blocked IP: {ip_address}")
|
||||
|
||||
# Reload firewall to apply changes
|
||||
ProcessUtilities.executioner('firewall-cmd --reload')
|
||||
|
||||
# Log the block in a dedicated file
|
||||
block_log_path = "/usr/local/lscp/logs/blocked_ips.log"
|
||||
with open(block_log_path, "a") as f:
|
||||
from datetime import datetime
|
||||
f.write(f"{datetime.now().strftime('%Y-%m-%d %H:%M:%S')} - {ip_address} - {reason}\n")
|
||||
|
||||
block_log_path = "/usr/local/CyberCP/data/blocked_ips.log"
|
||||
try:
|
||||
import os
|
||||
os.makedirs(os.path.dirname(block_log_path), exist_ok=True)
|
||||
with open(block_log_path, "a") as f:
|
||||
from datetime import datetime
|
||||
f.write(f"{datetime.now().strftime('%Y-%m-%d %H:%M:%S')} - {ip_address} - {reason}\n")
|
||||
except Exception as log_err:
|
||||
logging.CyberCPLogFileWriter.writeToFile(f"Warning: could not write blocked_ips.log: {log_err}")
|
||||
return True, f"IP {ip_address} blocked successfully"
|
||||
else:
|
||||
logging.CyberCPLogFileWriter.writeToFile(f"Failed to block IP: {ip_address}")
|
||||
logging.CyberCPLogFileWriter.writeToFile(f"Failed to block IP: {ip_address} (executioner returned %s)" % result)
|
||||
return False, f"Failed to block IP: {ip_address}"
|
||||
|
||||
except Exception as e:
|
||||
@@ -154,16 +155,14 @@ class FirewallUtilities:
|
||||
|
||||
logging.CyberCPLogFileWriter.writeToFile(f"Unblocking IP address: {ip_address}")
|
||||
|
||||
# executioner returns 1 on success, 0 on failure
|
||||
result = ProcessUtilities.executioner(command)
|
||||
if result == 0:
|
||||
if result == 1:
|
||||
logging.CyberCPLogFileWriter.writeToFile(f"Successfully unblocked IP: {ip_address}")
|
||||
|
||||
# Reload firewall to apply changes
|
||||
ProcessUtilities.executioner('firewall-cmd --reload')
|
||||
|
||||
return True, f"IP {ip_address} unblocked successfully"
|
||||
else:
|
||||
logging.CyberCPLogFileWriter.writeToFile(f"Failed to unblock IP: {ip_address}")
|
||||
logging.CyberCPLogFileWriter.writeToFile(f"Failed to unblock IP: {ip_address} (executioner returned %s)" % result)
|
||||
return False, f"Failed to unblock IP: {ip_address}"
|
||||
|
||||
except Exception as e:
|
||||
|
||||
@@ -1682,69 +1682,69 @@ LogFile /var/log/clamav/clamav.log
|
||||
|
||||
# Query each DNS server
|
||||
for url in urls:
|
||||
try:
|
||||
response = requests.get(f'{url}/index.php?ip={ip_address}', timeout=5)
|
||||
try:
|
||||
response = requests.get(f'{url}/index.php?ip={ip_address}', timeout=5)
|
||||
|
||||
if os.path.exists(ProcessUtilities.debugPath):
|
||||
logging.CyberCPLogFileWriter.writeToFile(f'url to call {ip_address} is {url}')
|
||||
if os.path.exists(ProcessUtilities.debugPath):
|
||||
logging.CyberCPLogFileWriter.writeToFile(f'url to call {ip_address} is {url}')
|
||||
|
||||
if response.status_code == 200:
|
||||
try:
|
||||
data = response.json()
|
||||
if response.status_code == 200:
|
||||
try:
|
||||
data = response.json()
|
||||
|
||||
if os.path.exists(ProcessUtilities.debugPath):
|
||||
logging.CyberCPLogFileWriter.writeToFile(f'response from dns system {str(data)}')
|
||||
if os.path.exists(ProcessUtilities.debugPath):
|
||||
logging.CyberCPLogFileWriter.writeToFile(f'response from dns system {str(data)}')
|
||||
|
||||
# Validate response structure
|
||||
if not isinstance(data, dict):
|
||||
logging.CyberCPLogFileWriter.writeToFile(f'Invalid response format from {url}: not a dictionary')
|
||||
continue
|
||||
# Validate response structure
|
||||
if not isinstance(data, dict):
|
||||
logging.CyberCPLogFileWriter.writeToFile(f'Invalid response format from {url}: not a dictionary')
|
||||
continue
|
||||
|
||||
if 'status' not in data:
|
||||
logging.CyberCPLogFileWriter.writeToFile(f'Response from {url} missing "status" key')
|
||||
continue
|
||||
if 'status' not in data:
|
||||
logging.CyberCPLogFileWriter.writeToFile(f'Response from {url} missing "status" key')
|
||||
continue
|
||||
|
||||
if data['status'] == 1:
|
||||
# Validate results structure
|
||||
if 'results' not in data or not isinstance(data['results'], dict):
|
||||
logging.CyberCPLogFileWriter.writeToFile(f'Response from {url} missing or invalid "results" key')
|
||||
if data['status'] == 1:
|
||||
# Validate results structure
|
||||
if 'results' not in data or not isinstance(data['results'], dict):
|
||||
logging.CyberCPLogFileWriter.writeToFile(f'Response from {url} missing or invalid "results" key')
|
||||
continue
|
||||
|
||||
results_dict = data['results']
|
||||
|
||||
# Safely extract results from different DNS servers
|
||||
dns_servers = ['8.8.8.8', '1.1.1.1', '9.9.9.9']
|
||||
for dns_server in dns_servers:
|
||||
if dns_server in results_dict:
|
||||
result_value = results_dict[dns_server]
|
||||
if result_value and result_value not in results:
|
||||
results.append(result_value)
|
||||
|
||||
successful_queries += 1
|
||||
else:
|
||||
if os.path.exists(ProcessUtilities.debugPath):
|
||||
logging.CyberCPLogFileWriter.writeToFile(f'DNS server {url} returned status != 1: {data.get("status", "unknown")}')
|
||||
except ValueError as e:
|
||||
logging.CyberCPLogFileWriter.writeToFile(f'Failed to parse JSON response from {url}: {str(e)}')
|
||||
continue
|
||||
except KeyError as e:
|
||||
logging.CyberCPLogFileWriter.writeToFile(f'Missing key in response from {url}: {str(e)}')
|
||||
continue
|
||||
else:
|
||||
if os.path.exists(ProcessUtilities.debugPath):
|
||||
logging.CyberCPLogFileWriter.writeToFile(f'DNS server {url} returned HTTP {response.status_code}')
|
||||
except Timeout as e:
|
||||
logging.CyberCPLogFileWriter.writeToFile(f'Timeout while querying DNS server {url}: {str(e)}')
|
||||
continue
|
||||
except ConnectionError as e:
|
||||
logging.CyberCPLogFileWriter.writeToFile(f'Connection error while querying DNS server {url}: {str(e)}')
|
||||
continue
|
||||
except RequestException as e:
|
||||
logging.CyberCPLogFileWriter.writeToFile(f'Request error while querying DNS server {url}: {str(e)}')
|
||||
continue
|
||||
except Exception as e:
|
||||
logging.CyberCPLogFileWriter.writeToFile(f'Unexpected error while querying DNS server {url}: {str(e)}')
|
||||
continue
|
||||
|
||||
results_dict = data['results']
|
||||
|
||||
# Safely extract results from different DNS servers
|
||||
dns_servers = ['8.8.8.8', '1.1.1.1', '9.9.9.9']
|
||||
for dns_server in dns_servers:
|
||||
if dns_server in results_dict:
|
||||
result_value = results_dict[dns_server]
|
||||
if result_value and result_value not in results:
|
||||
results.append(result_value)
|
||||
|
||||
successful_queries += 1
|
||||
else:
|
||||
if os.path.exists(ProcessUtilities.debugPath):
|
||||
logging.CyberCPLogFileWriter.writeToFile(f'DNS server {url} returned status != 1: {data.get("status", "unknown")}')
|
||||
except ValueError as e:
|
||||
logging.CyberCPLogFileWriter.writeToFile(f'Failed to parse JSON response from {url}: {str(e)}')
|
||||
continue
|
||||
except KeyError as e:
|
||||
logging.CyberCPLogFileWriter.writeToFile(f'Missing key in response from {url}: {str(e)}')
|
||||
continue
|
||||
else:
|
||||
if os.path.exists(ProcessUtilities.debugPath):
|
||||
logging.CyberCPLogFileWriter.writeToFile(f'DNS server {url} returned HTTP {response.status_code}')
|
||||
except Timeout as e:
|
||||
logging.CyberCPLogFileWriter.writeToFile(f'Timeout while querying DNS server {url}: {str(e)}')
|
||||
continue
|
||||
except ConnectionError as e:
|
||||
logging.CyberCPLogFileWriter.writeToFile(f'Connection error while querying DNS server {url}: {str(e)}')
|
||||
continue
|
||||
except RequestException as e:
|
||||
logging.CyberCPLogFileWriter.writeToFile(f'Request error while querying DNS server {url}: {str(e)}')
|
||||
continue
|
||||
except Exception as e:
|
||||
logging.CyberCPLogFileWriter.writeToFile(f'Unexpected error while querying DNS server {url}: {str(e)}')
|
||||
continue
|
||||
|
||||
if os.path.exists(ProcessUtilities.debugPath):
|
||||
logging.CyberCPLogFileWriter.writeToFile(f'rDNS result of {ip_address} is {str(results)} (successful queries: {successful_queries}/{len(urls)})')
|
||||
|
||||
@@ -378,8 +378,8 @@ class ProcessUtilities(multi.Thread):
|
||||
if getpass.getuser() == 'root':
|
||||
if os.path.exists(ProcessUtilities.debugPath):
|
||||
logging.writeToFile(f"[executioner] Running as root, using normalExecutioner")
|
||||
ProcessUtilities.normalExecutioner(command, shell, user)
|
||||
return 1
|
||||
res = ProcessUtilities.normalExecutioner(command, shell, user)
|
||||
return 1 if res == 1 else 0
|
||||
|
||||
if os.path.exists(ProcessUtilities.debugPath):
|
||||
logging.writeToFile(f"[executioner] Not root, using sendCommand via lscpd")
|
||||
|
||||
@@ -8,20 +8,44 @@ app.controller('createFTPAccount', function ($scope, $http) {
|
||||
|
||||
|
||||
|
||||
$scope.ftpLoading = false;
|
||||
$scope.ftpDetails = true;
|
||||
|
||||
$(document).ready(function () {
|
||||
$( ".ftpDetails" ).hide();
|
||||
$( ".ftpDetails, .account-details" ).hide();
|
||||
$( ".ftpPasswordView" ).hide();
|
||||
$('.create-ftp-acct-select').select2();
|
||||
if (typeof $ !== 'undefined' && $ && typeof $.fn !== 'undefined' && typeof $.fn.select2 === 'function') {
|
||||
try {
|
||||
var $sel = $('.create-ftp-acct-select');
|
||||
if ($sel.length) {
|
||||
$sel.select2();
|
||||
$sel.on('select2:select', function (e) {
|
||||
var data = e.params.data;
|
||||
$scope.ftpDomain = data.text;
|
||||
$scope.ftpDetails = false;
|
||||
$scope.$apply();
|
||||
$(".ftpDetails, .account-details").show();
|
||||
});
|
||||
}
|
||||
} catch (err) {}
|
||||
}
|
||||
$('.create-ftp-acct-select').off('select2:select').on('change', function () {
|
||||
$scope.ftpDomain = $(this).val();
|
||||
$scope.ftpDetails = ($scope.ftpDomain && $scope.ftpDomain !== '') ? false : true;
|
||||
$scope.$apply();
|
||||
$(".ftpDetails, .account-details").show();
|
||||
});
|
||||
});
|
||||
|
||||
$('.create-ftp-acct-select').on('select2:select', function (e) {
|
||||
var data = e.params.data;
|
||||
$scope.ftpDomain = data.text;
|
||||
$( ".ftpDetails" ).show();
|
||||
|
||||
});
|
||||
|
||||
$scope.ftpLoading = true;
|
||||
$scope.showFTPDetails = function() {
|
||||
if ($scope.ftpDomain && $scope.ftpDomain !== "") {
|
||||
$scope.ftpDetails = false;
|
||||
$(".ftpDetails, .account-details").show();
|
||||
} else {
|
||||
$scope.ftpDetails = true;
|
||||
$(".ftpDetails, .account-details").hide();
|
||||
}
|
||||
};
|
||||
|
||||
$scope.createFTPAccount = function () {
|
||||
|
||||
|
||||
@@ -641,11 +641,15 @@ app.controller('servicesManager', function ($scope, $http) {
|
||||
getServiceStatus();
|
||||
$scope.ActionSuccessfull = true;
|
||||
$scope.ActionFailed = false;
|
||||
$scope.actionErrorMsg = '';
|
||||
$scope.couldNotConnect = false;
|
||||
$scope.actionLoader = false;
|
||||
$scope.btnDisable = false;
|
||||
}, 3000);
|
||||
} else {
|
||||
var errMsg = (response.data && response.data.error_message) ? response.data.error_message : 'Action failed';
|
||||
if (errMsg === 0) errMsg = 'Action failed';
|
||||
$scope.actionErrorMsg = errMsg;
|
||||
setTimeout(function () {
|
||||
getServiceStatus();
|
||||
$scope.ActionSuccessfull = false;
|
||||
@@ -654,7 +658,6 @@ app.controller('servicesManager', function ($scope, $http) {
|
||||
$scope.actionLoader = false;
|
||||
$scope.btnDisable = false;
|
||||
}, 5000);
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -622,6 +622,7 @@
|
||||
<div ng-show="ActionFailed" class="alert alert-danger">
|
||||
<i class="fas fa-exclamation-circle alert-icon"></i>
|
||||
<span>{% trans "Action Failed" %}</span>
|
||||
<span ng-show="actionErrorMsg" class="d-block mt-2"><strong>{% trans "Details:" %}</strong> <span ng-bind="actionErrorMsg"></span></span>
|
||||
</div>
|
||||
<div ng-show="ActionSuccessfull" class="alert alert-success">
|
||||
<i class="fas fa-check-circle alert-icon"></i>
|
||||
|
||||
@@ -319,18 +319,36 @@ def servicesAction(request):
|
||||
final_json = json.dumps(final_dic)
|
||||
return HttpResponse(final_json)
|
||||
|
||||
else:
|
||||
if service == 'pure-ftpd':
|
||||
if os.path.exists("/etc/lsb-release"):
|
||||
service = 'pure-ftpd-mysql'
|
||||
else:
|
||||
service = 'pure-ftpd'
|
||||
if service == 'pure-ftpd':
|
||||
if os.path.exists("/etc/lsb-release"):
|
||||
service = 'pure-ftpd-mysql'
|
||||
else:
|
||||
service = 'pure-ftpd'
|
||||
|
||||
command = 'sudo systemctl %s %s' % (action, service)
|
||||
ProcessUtilities.executioner(command)
|
||||
final_dic = {'serviceAction': 1, "error_message": 0}
|
||||
final_json = json.dumps(final_dic)
|
||||
return HttpResponse(final_json)
|
||||
# Run as root with shell so systemctl has permission (panel may run as lscpd)
|
||||
command = 'systemctl %s %s' % (action, service)
|
||||
ProcessUtilities.executioner(command, 'root', True)
|
||||
time.sleep(1)
|
||||
|
||||
# For start action, verify service actually came up; return error if not
|
||||
if action == 'start':
|
||||
try:
|
||||
out = ProcessUtilities.outputExecutioner('systemctl is-active %s' % service, 'root', True)
|
||||
if not (out and out.strip() == 'active'):
|
||||
status_out = ProcessUtilities.outputExecutioner(
|
||||
'systemctl status %s --no-pager -l 2>&1 | head -15' % service, 'root', True)
|
||||
err_msg = (status_out or '').strip().replace('\n', ' ')[:400]
|
||||
final_dic = {'serviceAction': 0, 'error_message': 'Service did not start. ' + err_msg}
|
||||
final_json = json.dumps(final_dic)
|
||||
return HttpResponse(final_json)
|
||||
except Exception as e:
|
||||
final_dic = {'serviceAction': 0, 'error_message': 'Service did not start: %s' % str(e)}
|
||||
final_json = json.dumps(final_dic)
|
||||
return HttpResponse(final_json)
|
||||
|
||||
final_dic = {'serviceAction': 1, "error_message": 0}
|
||||
final_json = json.dumps(final_dic)
|
||||
return HttpResponse(final_json)
|
||||
|
||||
|
||||
except BaseException as msg:
|
||||
|
||||
21
sql/create_ftp_quotas.sql
Normal file
21
sql/create_ftp_quotas.sql
Normal file
@@ -0,0 +1,21 @@
|
||||
-- Create ftp_quotas table for FTP Quota Management (websiteFunctions.models.FTPQuota)
|
||||
-- Run once per CyberPanel database. Safe to run: uses IF NOT EXISTS.
|
||||
|
||||
CREATE TABLE IF NOT EXISTS `ftp_quotas` (
|
||||
`id` INT AUTO_INCREMENT NOT NULL PRIMARY KEY,
|
||||
`user_id` INT NOT NULL,
|
||||
`ftp_user` VARCHAR(255) NOT NULL,
|
||||
`domain_id` INT NULL,
|
||||
`quota_size_mb` INT NOT NULL DEFAULT 0,
|
||||
`quota_used_mb` INT NOT NULL DEFAULT 0,
|
||||
`quota_files` INT NOT NULL DEFAULT 0,
|
||||
`quota_files_used` INT NOT NULL DEFAULT 0,
|
||||
`is_active` TINYINT(1) NOT NULL DEFAULT 1,
|
||||
`created_at` DATETIME(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6),
|
||||
`updated_at` DATETIME(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6) ON UPDATE CURRENT_TIMESTAMP(6),
|
||||
UNIQUE KEY `ftp_quotas_ftp_user_unique` (`ftp_user`),
|
||||
KEY `ftp_quotas_user_id` (`user_id`),
|
||||
KEY `ftp_quotas_domain_id` (`domain_id`),
|
||||
CONSTRAINT `ftp_quotas_user_id_fk` FOREIGN KEY (`user_id`) REFERENCES `loginSystem_administrator` (`id`) ON DELETE CASCADE,
|
||||
CONSTRAINT `ftp_quotas_domain_id_fk` FOREIGN KEY (`domain_id`) REFERENCES `websiteFunctions_websites` (`id`) ON DELETE CASCADE
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||
@@ -15,7 +15,7 @@ app.controller('createFTPAccount', function ($scope, $http) {
|
||||
$scope.generatedPasswordView = true;
|
||||
|
||||
$(document).ready(function () {
|
||||
$( ".ftpDetails" ).hide();
|
||||
$( ".ftpDetails, .account-details" ).hide();
|
||||
$( ".ftpPasswordView" ).hide();
|
||||
|
||||
// Only use select2 if it's actually a function (avoids errors when Rocket Loader defers scripts)
|
||||
@@ -27,8 +27,9 @@ app.controller('createFTPAccount', function ($scope, $http) {
|
||||
$sel.on('select2:select', function (e) {
|
||||
var data = e.params.data;
|
||||
$scope.ftpDomain = data.text;
|
||||
$scope.ftpDetails = false;
|
||||
$scope.$apply();
|
||||
$(".ftpDetails").show();
|
||||
$(".ftpDetails, .account-details").show();
|
||||
});
|
||||
} else {
|
||||
initNativeSelect();
|
||||
@@ -42,19 +43,20 @@ app.controller('createFTPAccount', function ($scope, $http) {
|
||||
function initNativeSelect() {
|
||||
$('.create-ftp-acct-select').off('select2:select').on('change', function () {
|
||||
$scope.ftpDomain = $(this).val();
|
||||
$scope.ftpDetails = ($scope.ftpDomain && $scope.ftpDomain !== '') ? false : true;
|
||||
$scope.$apply();
|
||||
$(".ftpDetails").show();
|
||||
$(".ftpDetails, .account-details").show();
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
$scope.showFTPDetails = function() {
|
||||
if ($scope.ftpDomain && $scope.ftpDomain !== "") {
|
||||
$(".ftpDetails").show();
|
||||
$scope.ftpDetails = false;
|
||||
$(".ftpDetails, .account-details").show();
|
||||
} else {
|
||||
$(".ftpDetails").hide();
|
||||
$scope.ftpDetails = true;
|
||||
$(".ftpDetails, .account-details").hide();
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
146
static/mailServer/emailLimitsController.js
Normal file
146
static/mailServer/emailLimitsController.js
Normal file
@@ -0,0 +1,146 @@
|
||||
/**
|
||||
* Email Limits page controller - ensures EmailLimitsNew is registered on CyberCP
|
||||
* even if main mailServer.js loads late or fails. Load this script in the
|
||||
* EmailLimits template so the page works reliably.
|
||||
*/
|
||||
(function () {
|
||||
'use strict';
|
||||
var app = (typeof window.app !== 'undefined') ? window.app : angular.module('CyberCP');
|
||||
if (!app) return;
|
||||
|
||||
app.controller('EmailLimitsNew', function ($scope, $http) {
|
||||
$scope.creationBox = true;
|
||||
$scope.emailDetails = true;
|
||||
$scope.forwardLoading = false;
|
||||
$scope.forwardError = true;
|
||||
$scope.forwardSuccess = true;
|
||||
$scope.couldNotConnect = true;
|
||||
$scope.notifyBox = true;
|
||||
|
||||
$scope.showEmailDetails = function () {
|
||||
$scope.creationBox = true;
|
||||
$scope.emailDetails = true;
|
||||
$scope.forwardLoading = true;
|
||||
$scope.forwardError = true;
|
||||
$scope.forwardSuccess = true;
|
||||
$scope.couldNotConnect = true;
|
||||
$scope.notifyBox = true;
|
||||
|
||||
var url = "/email/getEmailsForDomain";
|
||||
var data = { domain: $scope.emailDomain };
|
||||
var config = { headers: { 'X-CSRFToken': getCookie('csrftoken') } };
|
||||
|
||||
$http.post(url, data, config).then(ListInitialDatas, cantLoadInitialDatas);
|
||||
|
||||
function ListInitialDatas(response) {
|
||||
if (response.data.fetchStatus === 1) {
|
||||
$scope.emails = JSON.parse(response.data.data);
|
||||
$scope.creationBox = true;
|
||||
$scope.emailDetails = false;
|
||||
$scope.forwardLoading = false;
|
||||
$scope.forwardError = true;
|
||||
$scope.forwardSuccess = true;
|
||||
$scope.couldNotConnect = true;
|
||||
$scope.notifyBox = false;
|
||||
} else {
|
||||
$scope.creationBox = true;
|
||||
$scope.emailDetails = true;
|
||||
$scope.forwardLoading = false;
|
||||
$scope.forwardError = false;
|
||||
$scope.forwardSuccess = true;
|
||||
$scope.couldNotConnect = true;
|
||||
$scope.notifyBox = false;
|
||||
$scope.errorMessage = response.data.error_message;
|
||||
}
|
||||
}
|
||||
|
||||
function cantLoadInitialDatas() {
|
||||
$scope.creationBox = true;
|
||||
$scope.emailDetails = true;
|
||||
$scope.forwardLoading = false;
|
||||
$scope.forwardError = true;
|
||||
$scope.forwardSuccess = true;
|
||||
$scope.couldNotConnect = false;
|
||||
$scope.notifyBox = false;
|
||||
}
|
||||
};
|
||||
|
||||
$scope.selectForwardingEmail = function () {
|
||||
$scope.creationBox = false;
|
||||
$scope.emailDetails = false;
|
||||
$scope.forwardLoading = true;
|
||||
$scope.forwardError = true;
|
||||
$scope.forwardSuccess = true;
|
||||
$scope.couldNotConnect = true;
|
||||
$scope.notifyBox = true;
|
||||
|
||||
var givenEmail = $scope.selectedEmail;
|
||||
if ($scope.emails) {
|
||||
for (var i = 0; i < $scope.emails.length; i++) {
|
||||
if ($scope.emails[i].email === givenEmail) {
|
||||
$scope.numberofEmails = $scope.emails[i].numberofEmails;
|
||||
$scope.duration = $scope.emails[i].duration;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
$scope.SaveChanges = function () {
|
||||
$scope.creationBox = false;
|
||||
$scope.emailDetails = false;
|
||||
$scope.forwardLoading = true;
|
||||
$scope.forwardError = true;
|
||||
$scope.forwardSuccess = true;
|
||||
$scope.couldNotConnect = true;
|
||||
$scope.notifyBox = true;
|
||||
|
||||
var url = "/email/SaveEmailLimitsNew";
|
||||
var data = {
|
||||
numberofEmails: $scope.numberofEmails,
|
||||
source: $scope.selectedEmail,
|
||||
duration: $scope.duration
|
||||
};
|
||||
var config = { headers: { 'X-CSRFToken': getCookie('csrftoken') } };
|
||||
|
||||
$http.post(url, data, config).then(SaveListInitialDatas, SaveCantLoadInitialDatas);
|
||||
|
||||
function SaveListInitialDatas(response) {
|
||||
if (response.data.status === 1) {
|
||||
$scope.creationBox = false;
|
||||
$scope.emailDetails = false;
|
||||
$scope.forwardLoading = false;
|
||||
$scope.forwardError = true;
|
||||
$scope.forwardSuccess = true;
|
||||
$scope.couldNotConnect = true;
|
||||
$scope.notifyBox = true;
|
||||
if (typeof new PNotify === 'function') {
|
||||
new PNotify({ title: 'Success!', text: 'Changes applied.', type: 'success' });
|
||||
}
|
||||
$scope.showEmailDetails();
|
||||
} else {
|
||||
$scope.creationBox = false;
|
||||
$scope.emailDetails = false;
|
||||
$scope.forwardLoading = false;
|
||||
$scope.forwardError = false;
|
||||
$scope.forwardSuccess = true;
|
||||
$scope.couldNotConnect = true;
|
||||
$scope.notifyBox = false;
|
||||
if (typeof new PNotify === 'function') {
|
||||
new PNotify({ title: 'Error!', text: response.data.error_message || 'Error', type: 'error' });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function SaveCantLoadInitialDatas() {
|
||||
$scope.creationBox = true;
|
||||
$scope.emailDetails = true;
|
||||
$scope.forwardLoading = false;
|
||||
$scope.forwardError = true;
|
||||
$scope.forwardSuccess = true;
|
||||
$scope.couldNotConnect = false;
|
||||
$scope.notifyBox = false;
|
||||
}
|
||||
};
|
||||
});
|
||||
})();
|
||||
@@ -2,6 +2,14 @@
|
||||
* Created by usman on 8/15/17.
|
||||
*/
|
||||
|
||||
// Ensure app is available (get existing CyberCP module so controllers register correctly)
|
||||
if (typeof app === 'undefined') {
|
||||
if (typeof window !== 'undefined' && typeof window.app !== 'undefined') {
|
||||
app = window.app;
|
||||
} else {
|
||||
app = angular.module('CyberCP');
|
||||
}
|
||||
}
|
||||
|
||||
/* Java script code to create account */
|
||||
app.controller('createEmailAccount', function ($scope, $http) {
|
||||
|
||||
57
to-do/EMAIL-LIMITS-DEPLOY-CHECKLIST.md
Normal file
57
to-do/EMAIL-LIMITS-DEPLOY-CHECKLIST.md
Normal file
@@ -0,0 +1,57 @@
|
||||
# Email Limits Fix – Deploy Checklist
|
||||
|
||||
Use this after pulling the Email Limits fixes in this repo so that https://your-panel/email/EmailLimits works (controller registers, email list loads, configure section works).
|
||||
|
||||
## Files that are part of the fix
|
||||
|
||||
| File | Purpose |
|
||||
|------|--------|
|
||||
| `mailServer/mailserverManager.py` | Passes controller JS to template; allows getEmailsForDomain for emailForwarding |
|
||||
| `mailServer/templates/mailServer/EmailLimits.html` | Inline controller in footer_scripts (no static file dependency) |
|
||||
| `mailServer/static/mailServer/mailServer.js` | EmailLimitsNew controller + guard for `$scope.emails` |
|
||||
| `mailServer/static/mailServer/emailLimitsController.js` | Standalone controller + PNotify check fix |
|
||||
|
||||
## Option A: Deploy script (recommended)
|
||||
|
||||
**Run from anywhere** (use the full path to the script so the shell can find it):
|
||||
|
||||
```bash
|
||||
sudo bash /home/cyberpanel-repo/deploy-email-limits-fix.sh
|
||||
```
|
||||
|
||||
Or from repo root:
|
||||
|
||||
```bash
|
||||
cd /home/cyberpanel-repo && sudo bash deploy-email-limits-fix.sh
|
||||
```
|
||||
|
||||
- Script auto-detects repo at `/home/cyberpanel-repo` if run from another directory.
|
||||
- Default CyberPanel path: `/usr/local/CyberCP`.
|
||||
- Override: `sudo bash /home/cyberpanel-repo/deploy-email-limits-fix.sh /path/to/repo /usr/local/CyberCP`.
|
||||
- Skip restart: `sudo RESTART_LSCPD=0 bash /home/cyberpanel-repo/deploy-email-limits-fix.sh`.
|
||||
|
||||
## Option B: Manual copy + restart
|
||||
|
||||
On the server, from the repo root (e.g. `/home/cyberpanel-repo`):
|
||||
|
||||
```bash
|
||||
CP_DIR=/usr/local/CyberCP
|
||||
|
||||
cp -f mailServer/mailserverManager.py "$CP_DIR/mailServer/"
|
||||
cp -f mailServer/templates/mailServer/EmailLimits.html "$CP_DIR/mailServer/templates/mailServer/"
|
||||
cp -f mailServer/static/mailServer/mailServer.js "$CP_DIR/mailServer/static/mailServer/"
|
||||
cp -f mailServer/static/mailServer/emailLimitsController.js "$CP_DIR/mailServer/static/mailServer/"
|
||||
|
||||
sudo systemctl restart lscpd
|
||||
```
|
||||
|
||||
## After deploy
|
||||
|
||||
1. Hard refresh the Email Limits page: **Ctrl+Shift+R** (or Cmd+Shift+R).
|
||||
2. Open **Email Limits**, choose a **website**, then check that **email account** dropdown fills and **Configure Email Limits** appears and works.
|
||||
|
||||
## If it still fails
|
||||
|
||||
- Confirm the four files above are present under `$CP_DIR` and were updated (check timestamps).
|
||||
- Check panel/Python logs and browser console for `[$controller:ctrlreg]` or JS errors.
|
||||
- Ensure `lscpd` (or the process serving the panel) was restarted after copying.
|
||||
108
to-do/EMAIL-LIMITS-LIVE-SERVER-CHECKLIST.md
Normal file
108
to-do/EMAIL-LIMITS-LIVE-SERVER-CHECKLIST.md
Normal file
@@ -0,0 +1,108 @@
|
||||
# Email Limits – Live Server Checklist (vs upstream v2.4.4)
|
||||
|
||||
## Upstream v2.4.4 behaviour
|
||||
|
||||
In [usmannasir/cyberpanel at v2.4.4](https://github.com/usmannasir/cyberpanel/tree/v2.4.4):
|
||||
|
||||
- **Template**: `mailServer/templates/mailServer/EmailLimits.html` exists and uses `ng-controller="EmailLimitsNew"` and `{$ … $}` bindings.
|
||||
- **Routes**: `mailServer/urls.py` has `EmailLimits` and `SaveEmailLimitsNew`.
|
||||
- **Controller**: The **`EmailLimitsNew` controller is not present** in `static/mailServer/mailServer.js`. Upstream `mailServer.js` ends at “List Emails” and has no `EmailLimitsNew` block.
|
||||
|
||||
So on a stock v2.4.4 install, the Email Limits page will show raw `{$ selectedEmail $}` and “Could not connect to server” because the Angular controller is never registered.
|
||||
|
||||
---
|
||||
|
||||
## How it is loaded in v2.4.4
|
||||
|
||||
1. **Base template** (`baseTemplate/templates/baseTemplate/index.html`) loads one script bundle:
|
||||
- `{% static 'mailServer/mailServer.js' %}?v={{ CP_VERSION }}`
|
||||
(in the “Additional Scripts” block at the bottom of the body.)
|
||||
|
||||
2. **Email Limits template** only provides content; it does **not** load any extra script in upstream. It expects `EmailLimitsNew` to come from `mailServer.js`, but that controller is missing in v2.4.4.
|
||||
|
||||
3. **Backend**: `mailServer/views.py` → `EmailLimits`, `SaveEmailLimitsNew`; `mailServer/mailserverManager.py` → `EmailLimits()`, `SaveEmailLimitsNew()`.
|
||||
|
||||
---
|
||||
|
||||
## Files that must be on the live server
|
||||
|
||||
Use the paths below relative to the CyberPanel app root (e.g. `/usr/local/CyberCP/` or your repo root). Django static files may be served from `STATIC_ROOT` after `collectstatic`; templates and Python files must be in the app directories.
|
||||
|
||||
### 1. Python / URLs / views (same as upstream + your tweaks)
|
||||
|
||||
| Path | Purpose |
|
||||
|------|--------|
|
||||
| `mailServer/urls.py` | Must include `EmailLimits` and `SaveEmailLimitsNew` routes. |
|
||||
| `mailServer/views.py` | Must define `EmailLimits` and `SaveEmailLimitsNew` and call manager. |
|
||||
| `mailServer/mailserverManager.py` | Must implement `EmailLimits()` and `SaveEmailLimitsNew()` and render `mailServer/EmailLimits.html` with `websiteList` and `status`. |
|
||||
|
||||
### 2. Template (must load the controller script)
|
||||
|
||||
| Path | Purpose |
|
||||
|------|--------|
|
||||
| `mailServer/templates/mailServer/EmailLimits.html` | Must extend `baseTemplate/index.html`, contain `ng-controller="EmailLimitsNew"`, and **include the script tag** that loads `emailLimitsController.js` at the top of `{% block content %}`. |
|
||||
|
||||
### 3. Base template (unchanged from upstream for Email Limits)
|
||||
|
||||
| Path | Purpose |
|
||||
|------|--------|
|
||||
| `baseTemplate/templates/baseTemplate/index.html` | Must load `{% static 'mailServer/mailServer.js' %}` in the same script block as other app JS (no `load_email_limits_controller` needed). |
|
||||
|
||||
### 4. Static files (at least one of the two options)
|
||||
|
||||
**Option A – Use main bundle (repo’s `mailServer.js` with controller)**
|
||||
|
||||
| Path | Purpose |
|
||||
|------|--------|
|
||||
| `static/mailServer/mailServer.js` | Must define `app` (e.g. `window.app` or `angular.module('CyberCP')`) at the top and register `app.controller('EmailLimitsNew', ...)`. |
|
||||
| `mailServer/static/mailServer/mailServer.js` | Same as above if you use app static dirs. |
|
||||
|
||||
**Option B – Use standalone controller (recommended so it works even if `mailServer.js` is old)**
|
||||
|
||||
| Path | Purpose |
|
||||
|------|--------|
|
||||
| `static/mailServer/emailLimitsController.js` | Standalone script that registers `EmailLimitsNew` on the CyberCP module. |
|
||||
| `mailServer/static/mailServer/emailLimitsController.js` | Same file under the app’s `static` dir. |
|
||||
|
||||
The Email Limits template in this repo loads `emailLimitsController.js` at the top of the content block, so the controller is registered on the Email Limits page even if the live server still has an older `mailServer.js` without `EmailLimitsNew`.
|
||||
|
||||
---
|
||||
|
||||
## Quick verification on the live server
|
||||
|
||||
Run from the CyberPanel app root (e.g. `/usr/local/CyberCP/`):
|
||||
|
||||
```bash
|
||||
# 1. Template must contain the controller script and ng-controller
|
||||
grep -l "emailLimitsController.js" mailServer/templates/mailServer/EmailLimits.html && \
|
||||
grep -l "EmailLimitsNew" mailServer/templates/mailServer/EmailLimits.html && \
|
||||
echo "Template OK" || echo "Template MISSING or WRONG"
|
||||
|
||||
# 2. Standalone controller script must exist (at least one location)
|
||||
([ -f static/mailServer/emailLimitsController.js ] || [ -f mailServer/static/mailServer/emailLimitsController.js ]) && \
|
||||
echo "emailLimitsController.js OK" || echo "emailLimitsController.js MISSING"
|
||||
|
||||
# 3. mailServer.js (if you rely on it for Email Limits) must define EmailLimitsNew
|
||||
grep -q "EmailLimitsNew" static/mailServer/mailServer.js 2>/dev/null || grep -q "EmailLimitsNew" mailServer/static/mailServer/mailServer.js 2>/dev/null && \
|
||||
echo "mailServer.js has EmailLimitsNew" || echo "mailServer.js has NO EmailLimitsNew (use emailLimitsController.js)"
|
||||
|
||||
# 4. Routes
|
||||
grep -q "EmailLimits" mailServer/urls.py && echo "URLs OK" || echo "URLs MISSING"
|
||||
```
|
||||
|
||||
After deploying, run:
|
||||
|
||||
```bash
|
||||
python3 manage.py collectstatic --noinput
|
||||
# Restart your app server (e.g. LiteSpeed / Gunicorn)
|
||||
```
|
||||
|
||||
Then hard-refresh the Email Limits page (Ctrl+Shift+R).
|
||||
|
||||
---
|
||||
|
||||
## Summary
|
||||
|
||||
- **Upstream v2.4.4**: Email Limits template and routes exist; **controller is missing** from `mailServer.js`, so the page is broken by default.
|
||||
- **This repo**: Adds `EmailLimitsNew` in `mailServer.js` and a standalone `emailLimitsController.js`, and the Email Limits template loads `emailLimitsController.js` so the page works even with an old `mailServer.js`.
|
||||
- **Live server**: Ensure the template, URLs, views, manager, base template, and either the updated `mailServer.js` or `emailLimitsController.js` (or both) are present as in this checklist, then run `collectstatic` and restart the app.
|
||||
25
to-do/FTP-QUOTAS-TABLE-FIX.md
Normal file
25
to-do/FTP-QUOTAS-TABLE-FIX.md
Normal file
@@ -0,0 +1,25 @@
|
||||
# FTP Quotas Table Fix
|
||||
|
||||
## Problem
|
||||
- **URL:** https://207.180.193.210:2087/ftp/quotaManagement
|
||||
- **Error:** `(1146, "Table 'cyberpanel.ftp_quotas' doesn't exist")`
|
||||
|
||||
The `FTPQuota` model in `websiteFunctions/models.py` uses `db_table = 'ftp_quotas'`, but the table had never been created in the database.
|
||||
|
||||
## Solution
|
||||
1. **SQL:** `sql/create_ftp_quotas.sql` – `CREATE TABLE IF NOT EXISTS ftp_quotas` with columns and FKs to `loginSystem_administrator` and `websiteFunctions_websites`.
|
||||
2. **Deploy script:** `deploy-ftp-quotas-table.sh` – Copies the SQL to `/usr/local/CyberCP/sql/` and runs it using Django’s DB connection (no password on command line).
|
||||
|
||||
## Deploy (already run)
|
||||
```bash
|
||||
sudo bash /home/cyberpanel-repo/deploy-ftp-quotas-table.sh
|
||||
```
|
||||
|
||||
## Manual run (if needed)
|
||||
From repo root:
|
||||
```bash
|
||||
sudo bash deploy-ftp-quotas-table.sh [REPO_DIR] [CP_DIR]
|
||||
```
|
||||
Default `CP_DIR` is `/usr/local/CyberCP`.
|
||||
|
||||
After deployment, reload `/ftp/quotaManagement` in the browser.
|
||||
24
to-do/PURE-FTPD-QUOTA-SYNTAX-FIX.md
Normal file
24
to-do/PURE-FTPD-QUOTA-SYNTAX-FIX.md
Normal file
@@ -0,0 +1,24 @@
|
||||
# Pure-FTPd Quota Syntax Fix (2026-02-04)
|
||||
|
||||
## Problem
|
||||
Pure-FTPd failed to start with:
|
||||
```
|
||||
/etc/pure-ftpd/pure-ftpd.conf:35:1: syntax error line 35: [Quota ...].
|
||||
```
|
||||
|
||||
## Cause
|
||||
The config used `Quota yes`, but Pure-FTPd expects **`Quota maxfiles:maxsize`** (e.g. `Quota 1000:10` for 1000 files and 10 MB). The value is not a boolean.
|
||||
|
||||
## Fix applied
|
||||
|
||||
### On the server
|
||||
- `/etc/pure-ftpd/pure-ftpd.conf`: line 35 set to `Quota 100000:100000` (high default so MySQL per-user quotas apply).
|
||||
- Service started successfully: `systemctl start pure-ftpd`.
|
||||
|
||||
### In the repo
|
||||
- **install/pure-ftpd/pure-ftpd.conf** and **install/pure-ftpd-one/pure-ftpd.conf**: `Quota yes` → `Quota 100000:100000`.
|
||||
- **websiteFunctions/website.py** (`enableFTPQuota`): sed/echo now write `Quota 100000:100000` instead of `Quota yes` (or tabs).
|
||||
|
||||
## Reference
|
||||
- Upstream: https://github.com/jedisct1/pure-ftpd/blob/master/pure-ftpd.conf.in (comment: "Quota 1000:10").
|
||||
- `pure-ftpd --help`: `-n --quota <opt>`.
|
||||
83
to-do/RUNTIME-VS-REPO-2.5.5-DEV.md
Normal file
83
to-do/RUNTIME-VS-REPO-2.5.5-DEV.md
Normal file
@@ -0,0 +1,83 @@
|
||||
# Runtime vs Repo: What Belongs in cyberpanel-repo for 2.5.5-dev
|
||||
|
||||
## Goal
|
||||
|
||||
When users upgrade to **our** (master3395) 2.5.5-dev, the panel should look and behave the same. That means **default** look-and-feel and behavior must be defined in the repo, not only “generated” on the server.
|
||||
|
||||
---
|
||||
|
||||
## What is “runtime generated”?
|
||||
|
||||
On the live server, after install/upgrade you have:
|
||||
|
||||
1. **From the repo (clone/copy)**
|
||||
All app code, templates, static sources, migrations, `version.txt`, default `settings.py`, etc.
|
||||
→ This **should** be in the repo (and already is).
|
||||
|
||||
2. **Generated at install/upgrade**
|
||||
- Python venv under `/usr/local/CyberCP/bin`, `lib`, `lib64`
|
||||
- `collectstatic` output under `/usr/local/CyberCP/public/static`
|
||||
- `version` table and `baseTemplate_cyberpanelcosmetic` row (if created by code/migrations)
|
||||
- `lscpd` binary copy, symlinks, etc.
|
||||
→ The **sources** that produce these (e.g. static sources, migrations) **should** be in the repo.
|
||||
|
||||
3. **Per-server / preserved**
|
||||
- `CyberCP/settings.py` — upgrade **merges** only the `DATABASES` section from the old server; the rest (e.g. `INSTALLED_APPS`) comes from the **new** clone.
|
||||
- `baseTemplate/static/baseTemplate/custom/` (custom CSS files)
|
||||
- DB row `baseTemplate_cyberpanelcosmetic.MainDashboardCSS` (custom dashboard CSS)
|
||||
- `.git/`, phpMyAdmin config, SnappyMail data, etc.
|
||||
→ **Defaults** that define “how 2.5.5-dev looks” should be in the repo; **per-server overrides** stay on the server.
|
||||
|
||||
---
|
||||
|
||||
## What we need in the repo so 2.5.5-dev “looks the same”
|
||||
|
||||
- **Templates, static sources, JS/CSS**
|
||||
Already in repo (e.g. `baseTemplate/`, `static/`). No change needed for “same look” unless you change the design.
|
||||
|
||||
- **Default `settings.py`**
|
||||
Already in repo. Upgrade keeps DB credentials from the server and uses repo for everything else (e.g. `INSTALLED_APPS`).
|
||||
So 2.5.5-dev behavior is driven by the repo’s `settings.py`.
|
||||
|
||||
- **Version**
|
||||
`baseTemplate/views.py` has `VERSION = '2.5.5'`, `BUILD = 'dev'`. Repo’s `version.txt` is `{"version":"2.5.5","build":"dev"}`.
|
||||
Upgrade also writes version into the DB. So version “same as 2.5.5-dev” is already defined in the repo.
|
||||
|
||||
- **Default “look” (cosmetic)**
|
||||
- Code already creates a default `CyberPanelCosmetic` row with **empty** `MainDashboardCSS` if none exists (`baseTemplate/context_processors.py`, `plogical/httpProc.py`, `loginSystem/views.py`).
|
||||
- If **your live server** has custom dashboard CSS (in DB or in `baseTemplate/static/baseTemplate/custom/`), that is **your** customization.
|
||||
- To make “our 2.5.5-dev” ship with that same look as default, you have two options:
|
||||
|
||||
1. **Data migration**
|
||||
Add a baseTemplate data migration that does:
|
||||
- `CyberPanelCosmetic.objects.get_or_create(pk=1, defaults={'MainDashboardCSS': '<your default CSS>'})`
|
||||
so every new/upgraded install gets that default look.
|
||||
|
||||
2. **Static default**
|
||||
Put the CSS in a static file under `baseTemplate/static/` and include it in the base template so the default theme matches your live server.
|
||||
|
||||
- **Migrations**
|
||||
All schema (and optional data) migrations must be in the repo so every 2.5.5-dev install/upgrade runs the same schema and, if you add it, the same default cosmetic data.
|
||||
|
||||
---
|
||||
|
||||
## What should **not** be in the repo
|
||||
|
||||
- **Secrets**: DB password, `SECRET_KEY`, API keys.
|
||||
Keep in `settings.py` only placeholders or env reads; real values stay on the server (or in config.php / env per your rules).
|
||||
|
||||
- **User data**: sites, users, mail, backups.
|
||||
These are per-server.
|
||||
|
||||
- **Generated artifacts**: venv, `collectstatic` output, compiled binaries.
|
||||
Repo holds the **source**; install/upgrade generates these on the server.
|
||||
|
||||
---
|
||||
|
||||
## Summary
|
||||
|
||||
- **Yes:** “Runtime generated” **defaults** that define how 2.5.5-dev looks and behaves **should** be reflected in the repo (templates, static sources, migrations, default cosmetic logic or data).
|
||||
- **Already in repo:** App code, default settings structure, version, static sources, migrations. So 2.5.5-dev upgrades already get the same **code** and **default look** (empty custom CSS).
|
||||
- **Optional:** If your live server has a **specific** custom look (e.g. custom dashboard CSS), and you want that to be the **default** for everyone on 2.5.5-dev, add it to the repo via a data migration or default static CSS as above.
|
||||
|
||||
No change is **required** for “same look” unless you want to ship a non-empty default cosmetic (e.g. your current dashboard CSS) as part of 2.5.5-dev.
|
||||
89
to-do/V2.5.5-DEV-FIXES-AND-DEPLOY.md
Normal file
89
to-do/V2.5.5-DEV-FIXES-AND-DEPLOY.md
Normal file
@@ -0,0 +1,89 @@
|
||||
# v2.5.5-dev: Fixes and Deploy Guide
|
||||
|
||||
This document lists fixes included in **v2.5.5-dev** and how to deploy them on a CyberPanel server.
|
||||
|
||||
---
|
||||
|
||||
## Version and cache busting
|
||||
|
||||
- **baseTemplate:** `CP_VERSION` in `baseTemplate/templates/baseTemplate/index.html` now uses `CYBERPANEL_FULL_VERSION` from context (from `baseTemplate/views.py`: `VERSION = '2.5.5'`, `BUILD = 'dev'`), so static URLs use `?v=2.5.5.dev` and cache busting matches the branch.
|
||||
|
||||
---
|
||||
|
||||
## 1. Email Limits page
|
||||
|
||||
- **Issue:** Raw Angular bindings (`{$ selectedEmail $}`) and `EmailLimitsNew` controller not registered.
|
||||
- **Files:** `mailServer/mailserverManager.py`, `mailServer/templates/mailServer/EmailLimits.html`, `mailServer/static/mailServer/mailServer.js`, `mailServer/static/mailServer/emailLimitsController.js`, and mirrored under `static/mailServer/`.
|
||||
- **Deploy:**
|
||||
```bash
|
||||
sudo bash /home/cyberpanel-repo/deploy-email-limits-fix.sh
|
||||
```
|
||||
- **Details:** See `to-do/EMAIL-LIMITS-DEPLOY-CHECKLIST.md`.
|
||||
|
||||
---
|
||||
|
||||
## 2. FTP Create Account page
|
||||
|
||||
- **Issue:** After selecting a website, the “FTP Account Details” form (username, password, path, quota) did not appear.
|
||||
- **Files:** `ftp/templates/ftp/createFTPAccount.html` (inline script + polling + Angular scope sync), `ftp/static/ftp/ftp.js`, `static/ftp/ftp.js`, `public/static/ftp/ftp.js`.
|
||||
- **Deploy:**
|
||||
```bash
|
||||
sudo bash /home/cyberpanel-repo/deploy-ftp-create-account-fix.sh
|
||||
```
|
||||
- **After deploy:** Hard-refresh `/ftp/createFTPAccount` (Ctrl+Shift+R).
|
||||
|
||||
---
|
||||
|
||||
## 3. FTP Quota Management page
|
||||
|
||||
- **Issue:** `(1146, "Table 'cyberpanel.ftp_quotas' doesn't exist")` on `/ftp/quotaManagement`.
|
||||
- **Files:** `sql/create_ftp_quotas.sql`, `websiteFunctions/models.py` (FTPQuota model already present).
|
||||
- **Deploy:**
|
||||
```bash
|
||||
sudo bash /home/cyberpanel-repo/deploy-ftp-quotas-table.sh
|
||||
```
|
||||
- **Details:** See `to-do/FTP-QUOTAS-TABLE-FIX.md`.
|
||||
|
||||
---
|
||||
|
||||
## 4. mailUtilities indentation fix
|
||||
|
||||
- **File:** `plogical/mailUtilities.py` (indentation fix in DNS query try/except block).
|
||||
- **Deploy:** Copy to `/usr/local/CyberCP/plogical/mailUtilities.py` and restart lscpd if needed.
|
||||
|
||||
---
|
||||
|
||||
## Deploy all fixes (in order)
|
||||
|
||||
Run on the server (e.g. from repo root):
|
||||
|
||||
```bash
|
||||
sudo bash deploy-email-limits-fix.sh
|
||||
sudo bash deploy-ftp-create-account-fix.sh
|
||||
sudo bash deploy-ftp-quotas-table.sh
|
||||
```
|
||||
|
||||
Then hard-refresh the FTP Create Account page in the browser. No need to restart lscpd after the FTP quotas table script (it only runs SQL).
|
||||
|
||||
---
|
||||
|
||||
## Files changed / added in v2.5.5-dev (fixes)
|
||||
|
||||
| Path | Description |
|
||||
|------|-------------|
|
||||
| `baseTemplate/templates/baseTemplate/index.html` | CP_VERSION from CYBERPANEL_FULL_VERSION |
|
||||
| `mailServer/` (Email Limits) | Controller, template, getEmailsForDomain permission |
|
||||
| `ftp/templates/ftp/createFTPAccount.html` | Inline fallback + polling for details form |
|
||||
| `ftp/static/ftp/ftp.js`, `static/ftp/ftp.js`, `public/static/ftp/ftp.js` | showFTPDetails, select2/change handlers |
|
||||
| `websiteFunctions/models.py` | FTPQuota model (table created via sql script) |
|
||||
| `sql/create_ftp_quotas.sql` | CREATE TABLE ftp_quotas |
|
||||
| `plogical/mailUtilities.py` | DNS block indentation fix |
|
||||
| `deploy-email-limits-fix.sh` | Deploy Email Limits fix |
|
||||
| `deploy-ftp-create-account-fix.sh` | Deploy FTP Create Account template |
|
||||
| `deploy-ftp-quotas-table.sh` | Create ftp_quotas table |
|
||||
|
||||
---
|
||||
|
||||
## Optional (not committed by default)
|
||||
|
||||
- **CyberCP/urls.py:** If `emailMarketing` is commented out for local runserver, leave it uncommitted or revert before pushing so production keeps the route.
|
||||
@@ -41,8 +41,8 @@ FTP Quota Management - CyberPanel
|
||||
<div class="alert alert-info">
|
||||
<h5><i class="fas fa-info-circle"></i> FTP Quota System</h5>
|
||||
<p>Enable and manage individual FTP user quotas. This allows you to set storage limits for each FTP user.</p>
|
||||
<button class="btn btn-primary" onclick="enableFTPQuota()">
|
||||
<i class="fas fa-play"></i> Enable FTP Quota System
|
||||
<button type="button" id="btnEnableFTPQuota" class="btn btn-primary" onclick="enableFTPQuota()">
|
||||
<i class="fas fa-play"></i> <span id="btnEnableFTPQuotaText">Enable FTP Quota System</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -125,15 +125,79 @@ FTP Quota Management - CyberPanel
|
||||
</div>
|
||||
|
||||
<script>
|
||||
function getCsrfToken() {
|
||||
var name = 'csrftoken';
|
||||
var cookies = document.cookie.split(';');
|
||||
for (var i = 0; i < cookies.length; i++) {
|
||||
var c = cookies[i].trim();
|
||||
if (c.indexOf(name + '=') === 0) return c.substring(name.length + 1);
|
||||
}
|
||||
return '{{ csrf_token }}';
|
||||
}
|
||||
|
||||
function showNotification(type, message) {
|
||||
if (typeof PNotify !== 'undefined') {
|
||||
try {
|
||||
new PNotify({ type: type === 'success' ? 'success' : 'error', text: message });
|
||||
return;
|
||||
} catch (e) {}
|
||||
}
|
||||
var alertClass = type === 'success' ? 'alert-success' : 'alert-danger';
|
||||
var icon = type === 'success' ? 'fa-check-circle' : 'fa-exclamation-circle';
|
||||
var notification = '<div class="alert ' + alertClass + ' alert-dismissible fade show" role="alert">' +
|
||||
'<i class="fas ' + icon + '"></i> ' + message +
|
||||
'<button type="button" class="close" data-dismiss="alert"><span>×</span></button></div>';
|
||||
var target = document.querySelector('.ftp-quota-info .alert-info');
|
||||
if (target && target.parentNode) {
|
||||
var wrap = document.createElement('div');
|
||||
wrap.innerHTML = notification;
|
||||
target.parentNode.insertBefore(wrap.firstChild, target);
|
||||
setTimeout(function() {
|
||||
var al = target.parentNode.querySelector('.alert');
|
||||
if (al && al.remove) al.remove();
|
||||
}, 6000);
|
||||
}
|
||||
}
|
||||
|
||||
function enableFTPQuota() {
|
||||
$.post('{% url "enableFTPQuota" %}', {
|
||||
'csrfmiddlewaretoken': '{{ csrf_token }}'
|
||||
}, function(data) {
|
||||
if (data.status === 1) {
|
||||
showNotification('success', (data && (data.message || data.error_message)) || 'Success');
|
||||
refreshQuotas();
|
||||
} else {
|
||||
showNotification('error', (data && (data.error_message || data.message)) || 'Unknown error');
|
||||
var btn = document.getElementById('btnEnableFTPQuota');
|
||||
var btnText = document.getElementById('btnEnableFTPQuotaText');
|
||||
if (!btn || !btnText) return;
|
||||
var originalHtml = btnText.innerHTML;
|
||||
btn.disabled = true;
|
||||
btnText.innerHTML = '<i class="fas fa-spinner fa-spin"></i> Enabling...';
|
||||
$.ajax({
|
||||
url: '{% url "enableFTPQuota" %}',
|
||||
type: 'POST',
|
||||
data: { 'csrfmiddlewaretoken': getCsrfToken() },
|
||||
headers: { 'X-CSRFToken': getCsrfToken() },
|
||||
dataType: 'json',
|
||||
success: function(data) {
|
||||
if (data && data.status === 1) {
|
||||
showNotification('success', data.message || 'FTP quota system enabled successfully');
|
||||
refreshQuotas();
|
||||
} else {
|
||||
showNotification('error', (data && (data.message || data.error_message)) || 'Enable failed');
|
||||
}
|
||||
},
|
||||
error: function(xhr) {
|
||||
var msg = 'Request failed';
|
||||
if (xhr.responseJSON && (xhr.responseJSON.message || xhr.responseJSON.error_message)) {
|
||||
msg = xhr.responseJSON.message || xhr.responseJSON.error_message;
|
||||
} else if (xhr.responseText && xhr.responseText.length < 500) {
|
||||
try {
|
||||
var j = JSON.parse(xhr.responseText);
|
||||
msg = j.message || j.error_message || msg;
|
||||
} catch (e) {}
|
||||
}
|
||||
if (xhr.status === 403 || xhr.status === 302) {
|
||||
msg = 'Session may have expired. Please refresh the page and try again.';
|
||||
}
|
||||
showNotification('error', msg);
|
||||
},
|
||||
complete: function() {
|
||||
btn.disabled = false;
|
||||
btnText.innerHTML = originalHtml;
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -230,26 +294,6 @@ function saveQuota() {
|
||||
});
|
||||
}
|
||||
|
||||
function showNotification(type, message) {
|
||||
const alertClass = type === 'success' ? 'alert-success' : 'alert-danger';
|
||||
const icon = type === 'success' ? 'fa-check-circle' : 'fa-exclamation-circle';
|
||||
|
||||
const notification = `
|
||||
<div class="alert ${alertClass} alert-dismissible fade show" role="alert">
|
||||
<i class="fas ${icon}"></i> ${message}
|
||||
<button type="button" class="close" data-dismiss="alert">
|
||||
<span>×</span>
|
||||
</button>
|
||||
</div>
|
||||
`;
|
||||
|
||||
$('.card-body').prepend(notification);
|
||||
|
||||
setTimeout(() => {
|
||||
$('.alert').fadeOut();
|
||||
}, 5000);
|
||||
}
|
||||
|
||||
// Load quotas on page load
|
||||
$(document).ready(function() {
|
||||
refreshQuotas();
|
||||
|
||||
@@ -8801,7 +8801,8 @@ StrictHostKeyChecking no
|
||||
|
||||
def enableFTPQuota(self, userID=None, data=None):
|
||||
"""
|
||||
Enable FTP quota system
|
||||
Enable FTP quota: ensure Quota yes in existing config (do not overwrite), restart Pure-FTPd.
|
||||
Uses correct service name (pure-ftpd-mysql on Debian/Ubuntu, pure-ftpd on RHEL/Alma).
|
||||
"""
|
||||
try:
|
||||
currentACL = ACLManager.loadedACL(userID)
|
||||
@@ -8811,60 +8812,79 @@ StrictHostKeyChecking no
|
||||
if not (currentACL.get('admin', 0) == 1):
|
||||
return ACLManager.loadErrorJson('status', 0)
|
||||
|
||||
# Backup existing configurations
|
||||
logging.CyberCPLogFileWriter.writeToFile("Backing up existing Pure-FTPd configurations...")
|
||||
# Resolve Pure-FTPd service name (Debian/Ubuntu use pure-ftpd-mysql)
|
||||
if os.path.exists('/etc/lsb-release'):
|
||||
ftp_service = 'pure-ftpd-mysql'
|
||||
else:
|
||||
ftp_service = 'pure-ftpd'
|
||||
|
||||
import shutil
|
||||
from datetime import datetime
|
||||
|
||||
timestamp = datetime.now().strftime('%Y%m%d_%H%M%S')
|
||||
|
||||
# Backup pure-ftpd.conf
|
||||
if os.path.exists('/etc/pure-ftpd/pure-ftpd.conf'):
|
||||
shutil.copy('/etc/pure-ftpd/pure-ftpd.conf', f'/etc/pure-ftpd/pure-ftpd.conf.backup.{timestamp}')
|
||||
|
||||
# Backup pureftpd-mysql.conf
|
||||
if os.path.exists('/etc/pure-ftpd/pureftpd-mysql.conf'):
|
||||
shutil.copy('/etc/pure-ftpd/pureftpd-mysql.conf', f'/etc/pure-ftpd/pureftpd-mysql.conf.backup.{timestamp}')
|
||||
|
||||
# Apply new configurations
|
||||
logging.CyberCPLogFileWriter.writeToFile("Applying FTP quota configurations...")
|
||||
|
||||
# Copy updated configurations
|
||||
if os.path.exists('/usr/local/CyberCP/install/pure-ftpd/pure-ftpd.conf'):
|
||||
shutil.copy('/usr/local/CyberCP/install/pure-ftpd/pure-ftpd.conf', '/etc/pure-ftpd/pure-ftpd.conf')
|
||||
|
||||
if os.path.exists('/usr/local/CyberCP/install/pure-ftpd/pureftpd-mysql.conf'):
|
||||
shutil.copy('/usr/local/CyberCP/install/pure-ftpd/pureftpd-mysql.conf', '/etc/pure-ftpd/pureftpd-mysql.conf')
|
||||
conf_path = '/etc/pure-ftpd/pure-ftpd.conf'
|
||||
# Only ensure Quota is enabled; do not overwrite existing config (preserves DB credentials, paths)
|
||||
if os.path.exists(conf_path):
|
||||
# If service is not running, try restoring latest backup (in case a previous run overwrote working config)
|
||||
try:
|
||||
out = ProcessUtilities.outputExecutioner(
|
||||
"systemctl is-active %s 2>/dev/null || true" % ftp_service, 'root', True)
|
||||
if not (out and out.strip() == 'active'):
|
||||
# Restore latest backups if present
|
||||
ProcessUtilities.executioner(
|
||||
"ls -t /etc/pure-ftpd/pure-ftpd.conf.backup.* 2>/dev/null | head -1 | xargs -r -I {} cp {} /etc/pure-ftpd/pure-ftpd.conf",
|
||||
'root', True)
|
||||
ProcessUtilities.executioner(
|
||||
"ls -t /etc/pure-ftpd/pureftpd-mysql.conf.backup.* 2>/dev/null | head -1 | xargs -r -I {} cp {} /etc/pure-ftpd/pureftpd-mysql.conf",
|
||||
'root', True)
|
||||
except Exception:
|
||||
pass
|
||||
# Add or replace Quota line via root (Pure-FTPd expects maxfiles:maxsizeMB, not "yes")
|
||||
ProcessUtilities.executioner(
|
||||
"grep -q '^Quota' %s && sed -i 's/^Quota.*/Quota 100000:100000/' %s || echo 'Quota 100000:100000' >> %s" % (conf_path, conf_path, conf_path),
|
||||
'root', True)
|
||||
logging.CyberCPLogFileWriter.writeToFile("Set Quota yes in existing pure-ftpd.conf")
|
||||
else:
|
||||
# First-time: copy from repo
|
||||
from datetime import datetime
|
||||
timestamp = datetime.now().strftime('%Y%m%d_%H%M%S')
|
||||
if os.path.exists('/usr/local/CyberCP/install/pure-ftpd/pure-ftpd.conf'):
|
||||
ProcessUtilities.executioner(
|
||||
'cp /usr/local/CyberCP/install/pure-ftpd/pure-ftpd.conf /etc/pure-ftpd/pure-ftpd.conf', 'root', True)
|
||||
if os.path.exists('/usr/local/CyberCP/install/pure-ftpd/pureftpd-mysql.conf'):
|
||||
ProcessUtilities.executioner(
|
||||
'cp /usr/local/CyberCP/install/pure-ftpd/pureftpd-mysql.conf /etc/pure-ftpd/pureftpd-mysql.conf', 'root', True)
|
||||
|
||||
# Restart Pure-FTPd
|
||||
logging.CyberCPLogFileWriter.writeToFile("Restarting Pure-FTPd service...")
|
||||
ProcessUtilities.executioner('systemctl restart pure-ftpd')
|
||||
logging.CyberCPLogFileWriter.writeToFile("Restarting Pure-FTPd service (%s)..." % ftp_service)
|
||||
ProcessUtilities.executioner('systemctl restart %s' % ftp_service, 'root', True)
|
||||
time.sleep(1)
|
||||
|
||||
# Verify configuration
|
||||
if ProcessUtilities.executioner('systemctl is-active --quiet pure-ftpd'):
|
||||
try:
|
||||
output = ProcessUtilities.outputExecutioner('systemctl is-active %s' % ftp_service, 'root', True)
|
||||
is_active = (output and output.strip() == 'active')
|
||||
except Exception:
|
||||
is_active = False
|
||||
|
||||
if is_active:
|
||||
logging.CyberCPLogFileWriter.writeToFile("FTP quota system enabled successfully")
|
||||
|
||||
data_ret = {
|
||||
'status': 1,
|
||||
'message': 'FTP quota system enabled successfully'
|
||||
}
|
||||
data_ret = {'status': 1, 'message': 'FTP quota system enabled successfully'}
|
||||
else:
|
||||
data_ret = {
|
||||
'status': 0,
|
||||
'message': 'Failed to restart Pure-FTPd service'
|
||||
}
|
||||
# Capture failure reason for the user
|
||||
try:
|
||||
status_out = ProcessUtilities.outputExecutioner(
|
||||
'systemctl status %s --no-pager -l 2>&1 | head -20' % ftp_service, 'root', True)
|
||||
status_preview = (status_out or '').strip().replace('\n', ' ')[:300]
|
||||
except Exception:
|
||||
status_preview = ''
|
||||
logging.CyberCPLogFileWriter.writeToFile("Pure-FTPd service not active after restart")
|
||||
msg = 'Pure-FTPd did not start. Run: systemctl status %s' % ftp_service
|
||||
if status_preview:
|
||||
msg += '. ' + status_preview
|
||||
data_ret = {'status': 0, 'message': msg}
|
||||
|
||||
json_data = json.dumps(data_ret)
|
||||
return HttpResponse(json_data)
|
||||
return HttpResponse(json.dumps(data_ret), content_type='application/json')
|
||||
|
||||
except Exception as e:
|
||||
data_ret = {
|
||||
'status': 0,
|
||||
'message': f'Error enabling FTP quota: {str(e)}'
|
||||
}
|
||||
json_data = json.dumps(data_ret)
|
||||
return HttpResponse(json_data)
|
||||
logging.CyberCPLogFileWriter.writeToFile("Error enabling FTP quota: %s" % str(e))
|
||||
data_ret = {'status': 0, 'message': 'Error enabling FTP quota: %s' % str(e)}
|
||||
return HttpResponse(json.dumps(data_ret), content_type='application/json')
|
||||
|
||||
def getFTPQuotas(self, userID=None, data=None):
|
||||
"""
|
||||
|
||||
Reference in New Issue
Block a user