FTP, dashboard, and notification fixes for v2.5.5-dev

- FTP: Fix createFTPAccount (ChildDomains), deleteFTPAccount, ResetFTPConfigurations, listFTPAccounts, quotaManagement
- FTP: Add quotaManagement page at /ftp/quotaManagement, improve reset status polling and error display
- Dashboard: Fix Angular ng-click parse error (remove return false from blockIPAddress)
- Dashboard: Add data-cfasync=false to jQuery/system-status for Rocket Loader compatibility
- FTP Quota Management: Improve error handling, fix refreshQuotas success/error callbacks
- Notification: Add updated_at column for usernotificationpreferences (run SQL migration)
This commit is contained in:
master3395
2026-01-30 19:46:05 +01:00
parent ea5be31de5
commit 27c8b48309
21 changed files with 1591 additions and 730 deletions

View File

@@ -1071,6 +1071,99 @@ app.controller('dashboardStatsController', function ($scope, $http, $timeout) {
});
}
};
// Ban IP from SSH Logs
$scope.banIPFromSSHLog = function(ipAddress) {
if (!ipAddress) {
new PNotify({
title: 'Error',
text: 'No IP address provided',
type: 'error',
delay: 5000
});
return;
}
if ($scope.blockingIP === ipAddress) {
return; // Already processing
}
if ($scope.blockedIPs[ipAddress]) {
new PNotify({
title: 'Info',
text: `IP address ${ipAddress} is already banned`,
type: 'info',
delay: 3000
});
return;
}
$scope.blockingIP = ipAddress;
// Use the Banned IPs system
var data = {
ip: ipAddress,
reason: 'Suspicious activity detected from SSH logs',
duration: 'permanent'
};
var config = {
headers: {
'X-CSRFToken': getCookie('csrftoken')
}
};
$http.post('/firewall/addBannedIP', data, config).then(function (response) {
$scope.blockingIP = null;
if (response.data && response.data.status === 1) {
// Mark IP as blocked
$scope.blockedIPs[ipAddress] = true;
// Show success notification
new PNotify({
title: 'IP Address Banned',
text: `IP address ${ipAddress} has been permanently banned and added to the firewall. You can manage it in the Firewall > Banned IPs section.`,
type: 'success',
delay: 5000
});
// Refresh SSH logs to update the UI
$scope.refreshSSHLogs();
} else {
// Show error notification
var errorMsg = 'Failed to ban IP address';
if (response.data && response.data.error_message) {
errorMsg = response.data.error_message;
} else if (response.data && response.data.error) {
errorMsg = response.data.error;
}
new PNotify({
title: 'Error',
text: errorMsg,
type: 'error',
delay: 5000
});
}
}, function (err) {
$scope.blockingIP = null;
var errorMessage = 'Failed to ban IP address';
if (err.data && err.data.error_message) {
errorMessage = err.data.error_message;
} else if (err.data && err.data.error) {
errorMessage = err.data.error;
} else if (err.data && err.data.message) {
errorMessage = err.data.message;
}
new PNotify({
title: 'Error',
text: errorMessage,
type: 'error',
delay: 5000
});
});
};
// Initial fetch
$scope.refreshTopProcesses();

View File

@@ -972,22 +972,22 @@
<strong style="font-size: 12px; color: #1e293b;">Recommendation:</strong>
<p style="margin: 4px 0 0 0; font-size: 12px; color: #475569; white-space: pre-line;">{$ alert.recommendation $}</p>
</div>
<!-- Add to Firewall Button for Brute Force Attacks -->
<div ng-if="alert.title === 'Brute Force Attack Detected' && alert.details && alert.details['IP Address']" style="margin-top: 12px;">
<button ng-click="blockIPAddress(alert.details['IP Address'])"
ng-disabled="blockingIP === alert.details['IP Address']"
<!-- Add to Firewall Button for all alerts with IP addresses -->
<div ng-if="alert.details && (alert.details['IP Address'] || alert.details['Top IP'])" style="margin-top: 12px;">
<button ng-click="blockIPAddress(alert.details['IP Address'] || alert.details['Top IP']); $event.stopPropagation()"
ng-disabled="blockingIP === (alert.details['IP Address'] || alert.details['Top IP'])"
style="background: #dc2626; color: white; border: none; padding: 8px 16px; border-radius: 6px; font-size: 12px; font-weight: 600; cursor: pointer; display: inline-flex; align-items: center; gap: 6px;"
onmouseover="this.style.background='#b91c1c'"
onmouseout="this.style.background='#dc2626'">
<i class="fas fa-ban" ng-if="blockingIP !== alert.details['IP Address']"></i>
<i class="fas fa-spinner fa-spin" ng-if="blockingIP === alert.details['IP Address']"></i>
<span ng-if="blockingIP !== alert.details['IP Address']">Ban IP Permanently</span>
<span ng-if="blockingIP === alert.details['IP Address']">Banning...</span>
<i class="fas fa-ban" ng-if="blockingIP !== (alert.details['IP Address'] || alert.details['Top IP'])"></i>
<i class="fas fa-spinner fa-spin" ng-if="blockingIP === (alert.details['IP Address'] || alert.details['Top IP'])"></i>
<span ng-if="blockingIP !== (alert.details['IP Address'] || alert.details['Top IP'])">Ban IP Permanently</span>
<span ng-if="blockingIP === (alert.details['IP Address'] || alert.details['Top IP'])">Banning...</span>
</button>
<a href="/firewall/" target="_blank" style="margin-left: 10px; color: #5b5fcf; font-size: 12px; text-decoration: none;">
<i class="fas fa-external-link-alt"></i> Manage in Firewall
</a>
<span ng-if="blockedIPs && blockedIPs[alert.details['IP Address']]"
<span ng-if="blockedIPs && blockedIPs[alert.details['IP Address'] || alert.details['Top IP']]"
style="margin-left: 10px; color: #10b981; font-size: 12px; font-weight: 600;">
<i class="fas fa-check-circle"></i> Blocked
</span>
@@ -1015,12 +1015,43 @@
<tr>
<th>TIMESTAMP</th>
<th>MESSAGE</th>
<th>IP ADDRESS</th>
<th>ACTIONS</th>
</tr>
</thead>
<tbody>
<tr ng-repeat="log in sshLogs">
<td>{$ log.timestamp $}</td>
<td>{$ log.message $}</td>
<td>
<span ng-if="log.ip_address" style="font-family: monospace; color: #5b5fcf; font-weight: 600;">
{$ log.ip_address $}
</span>
<span ng-if="!log.ip_address" style="color: #8893a7;">-</span>
</td>
<td>
<button ng-if="log.ip_address && blockingIP !== log.ip_address && !blockedIPs[log.ip_address]"
ng-click="banIPFromSSHLog(log.ip_address)"
style="background: #dc2626; color: white; border: none; padding: 6px 12px; border-radius: 6px; font-size: 11px; font-weight: 600; cursor: pointer; display: inline-flex; align-items: center; gap: 4px;"
onmouseover="this.style.background='#b91c1c'"
onmouseout="this.style.background='#dc2626'"
title="Ban this IP address permanently">
<i class="fas fa-ban"></i>
Ban IP
</button>
<button ng-if="log.ip_address && blockingIP === log.ip_address"
disabled
style="background: #9ca3af; color: white; border: none; padding: 6px 12px; border-radius: 6px; font-size: 11px; font-weight: 600; cursor: not-allowed; display: inline-flex; align-items: center; gap: 4px;">
<i class="fas fa-spinner fa-spin"></i>
Banning...
</button>
<span ng-if="log.ip_address && blockedIPs[log.ip_address]"
style="color: #10b981; font-size: 11px; font-weight: 600; display: inline-flex; align-items: center; gap: 4px;">
<i class="fas fa-check-circle"></i>
Banned
</span>
<span ng-if="!log.ip_address" style="color: #8893a7; font-size: 11px;">-</span>
</td>
</tr>
</tbody>
</table>
@@ -1292,31 +1323,67 @@
});
}
const formData = {
'csrfmiddlewaretoken': getCookie('csrftoken'),
'ip_address': ipAddress,
'reason': 'Brute force attack detected from dashboard'
};
$.post('/base/blockIPAddress', formData, function(data) {
if (data.status === 1) {
showNotification('success', data.message);
// Refresh the page to update the blocked IPs list
setTimeout(() => {
location.reload();
}, 1000);
} else {
showNotification('error', data.message);
}
}).fail(function() {
showNotification('error', 'Failed to block IP address. Please try again.');
}).always(function() {
// Clear loading state
if (typeof angular !== 'undefined' && angular.element(document.body).scope()) {
var scope = angular.element(document.body).scope();
scope.$apply(function() {
scope.blockingIP = null;
});
$.ajax({
url: '/base/blockIPAddress',
type: 'POST',
contentType: 'application/json',
headers: {
'X-CSRFToken': getCookie('csrftoken')
},
data: JSON.stringify({
'ip_address': ipAddress,
'reason': 'Security alert detected from dashboard'
}),
success: function(data) {
// Handle both success and error responses
if (data.status === 1) {
showNotification('success', data.message || 'IP address blocked successfully');
// Refresh the page to update the blocked IPs list
setTimeout(() => {
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';
showNotification('error', errorMsg);
}
},
error: function(xhr, status, error) {
// Handle network errors and parse JSON errors
console.error('Ban IP error:', xhr, status, error);
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);
}
}
}
showNotification('error', errorMsg);
},
complete: function() {
// Clear loading state
if (typeof angular !== 'undefined' && angular.element(document.body).scope()) {
var scope = angular.element(document.body).scope();
scope.$apply(function() {
scope.blockingIP = null;
});
}
}
});
}

View File

@@ -26,15 +26,15 @@
<!-- Readability Fixes CSS -->
<link rel="stylesheet" type="text/css" href="{% static 'baseTemplate/assets/readability-fixes.css' %}?v={{ CP_VERSION }}">
<!-- Core Scripts -->
<script src="{% static 'baseTemplate/angularjs.1.6.5.js' %}?v={{ CP_VERSION }}"></script>
<script src="https://code.jquery.com/jquery-2.2.4.min.js"></script>
<!-- Core Scripts (data-cfasync=false prevents Cloudflare Rocket Loader from breaking load order) -->
<script src="{% static 'baseTemplate/angularjs.1.6.5.js' %}?v={{ CP_VERSION }}" data-cfasync="false"></script>
<script src="https://code.jquery.com/jquery-2.2.4.min.js" data-cfasync="false"></script>
<!-- Bootstrap JavaScript -->
<script src="{% static 'baseTemplate/assets/bootstrap/js/bootstrap.min.js' %}?v={{ CP_VERSION }}"></script>
<script src="{% static 'baseTemplate/bootstrap-toggle.min.js' %}?v={{ CP_VERSION }}"></script>
<script src="{% static 'baseTemplate/custom-js/qrious.min.js' %}?v={{ CP_VERSION }}"></script>
<script src="{% static 'baseTemplate/custom-js/system-status.js' %}?v={{ CP_VERSION }}"></script>
<script src="{% static 'baseTemplate/custom-js/system-status.js' %}?v={{ CP_VERSION }}" data-cfasync="false"></script>
<!-- Font Awesome -->
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
@@ -46,6 +46,10 @@
<link rel="stylesheet" type="text/css" href="{% static 'baseTemplate/custom-js/pnotify.custom.min.css' %}?v={{ CP_VERSION }}">
<script src="{% static 'baseTemplate/custom-js/pnotify.custom.min.js' %}?v={{ CP_VERSION }}"></script>
<!-- Select2 (required by FTP create account; load after jQuery; exclude from Rocket Loader to preserve order) -->
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/select2@4.0.13/dist/css/select2.min.css">
<script src="https://cdn.jsdelivr.net/npm/select2@4.0.13/dist/js/select2.full.min.js" data-cfasync="false"></script>
<!-- Modern Design System -->
<style>
/* CSS Variables for Theme */
@@ -252,6 +256,119 @@
color: #fbbf24;
}
/* Notification Center Button */
.notification-center-btn {
position: relative;
width: 40px;
height: 40px;
border-radius: 12px;
display: flex;
align-items: center;
justify-content: center;
background: transparent;
color: var(--text-secondary);
border: 1px solid var(--border-color);
cursor: pointer;
transition: all 0.3s ease;
font-size: 18px;
}
.notification-center-btn:hover {
background: var(--accent-color);
color: white;
border-color: var(--accent-color);
transform: translateY(-2px);
box-shadow: 0 5px 15px rgba(88,86,214,0.3);
}
.notification-badge {
position: absolute;
top: -5px;
right: -5px;
background: #dc2626;
color: white;
font-size: 0.7rem;
font-weight: bold;
padding: 0.15rem 0.4rem;
border-radius: 10px;
min-width: 1.2rem;
text-align: center;
line-height: 1.2;
border: 2px solid white;
display: flex;
align-items: center;
justify-content: center;
}
.notification-center-dropdown {
position: absolute;
top: calc(100% + 10px);
right: 0;
background: white;
border: 1px solid var(--border-color);
border-radius: 16px;
box-shadow: 0 20px 40px rgba(0, 0, 0, 0.15);
width: 520px;
max-width: calc(100vw - 40px);
max-height: 600px;
overflow-y: auto;
z-index: 10000;
display: none;
}
.notification-center-dropdown.show { display: block; }
.notification-center-header {
padding: 1.25rem 1.5rem;
border-bottom: 1px solid var(--border-color);
display: flex;
justify-content: space-between;
align-items: center;
}
.notification-center-header h3 { margin: 0; font-size: 1.25rem; font-weight: 700; }
.notification-center-list { padding: 1rem; }
.notification-center-empty { color: var(--text-secondary); padding: 1rem; }
.notification-center-item {
padding: 1.5rem;
border: 1px solid var(--border-color);
border-radius: 12px;
margin-bottom: 1rem;
background: white;
}
.notification-center-item.dismissed { opacity: 0.7; background: #f9fafb; }
.notification-center-item-title {
font-weight: 700;
margin-bottom: 0.75rem;
display: flex;
align-items: center;
gap: 0.75rem;
}
.notification-center-item-title .dismissed-badge {
font-size: 0.75rem;
color: #6b7280;
margin-left: auto;
padding: 0.25rem 0.5rem;
background: #f3f4f6;
border-radius: 6px;
}
.notification-center-item-text { color: var(--text-secondary); font-size: 0.95rem; margin-bottom: 1rem; line-height: 1.6; }
.notification-center-item-actions { display: flex; gap: 0.75rem; flex-wrap: wrap; }
.notification-center-item-link {
display: inline-flex;
align-items: center;
gap: 0.5rem;
color: white;
background: linear-gradient(135deg, var(--accent-color) 0%, #6d6bd4 100%);
text-decoration: none;
font-size: 0.9rem;
font-weight: 600;
padding: 0.75rem 1.5rem;
border-radius: 10px;
}
.notification-center-item-link-secondary {
display: inline-flex;
align-items: center;
gap: 0.5rem;
color: var(--accent-color);
text-decoration: none;
font-size: 0.9rem;
}
/* Sidebar */
#sidebar {
width: 260px;
@@ -1211,7 +1328,20 @@
<div id="header-right">
<div class="info-text">Connect with us — Watch tutorials, <a href="https://community.cyberpanel.net/" target="_blank" rel="noopener" style="color: inherit; text-decoration: underline;">Join discussions</a>, and <a href="https://platform.cyberpersons.com/" target="_blank" rel="noopener" style="color: inherit; text-decoration: underline;">get support</a>.</div>
<div class="social-links">
<div class="social-links" style="position: relative;">
<button id="notification-center-btn" class="notification-center-btn" title="Notifications" onclick="toggleNotificationCenter()">
<i class="fas fa-bell"></i>
<span id="notification-badge" class="notification-badge">0</span>
</button>
<div id="notification-center-dropdown" class="notification-center-dropdown">
<div class="notification-center-header">
<h3>Notifications</h3>
<button onclick="toggleNotificationCenter()" style="background: none; border: none; cursor: pointer; font-size: 1.2rem; color: #6b7280;">&times;</button>
</div>
<div id="notification-center-list" class="notification-center-list">
<div class="notification-center-empty">Loading notifications...</div>
</div>
</div>
<a href="https://web.facebook.com/groups/cyberpanel" target="_blank" rel="noopener" title="Facebook">
<i class="fab fa-facebook-f"></i>
</a>
@@ -1621,7 +1751,7 @@
</a>
{% endif %}
{% if admin %}
<a href="#" class="menu-item" onclick="loadFTPQuotaManagement(); return false;">
<a href="{% url 'ftpQuotaManagementPage' %}" class="menu-item" target="_blank">
<span>FTP Quota Management</span>
</a>
{% endif %}
@@ -2055,7 +2185,7 @@
<script src="{% static 'databases/databases.js' %}?v={{ CP_VERSION }}"></script>
<script src="{% static 'dns/dns.js' %}?v={{ CP_VERSION }}"></script>
<script src="{% static 'mailServer/mailServer.js' %}?v={{ CP_VERSION }}"></script>
<script src="{% static 'ftp/ftp.js' %}?v={{ CP_VERSION }}"></script>
<script src="{% static 'ftp/ftp.js' %}?v={{ CP_VERSION }}" data-cfasync="false"></script>
<script src="{% static 'backup/backup.js' %}?v={{ CP_VERSION }}"></script>
<script src="{% static 'managePHP/managePHP.js' %}?v={{ CP_VERSION }}"></script>
<script src="{% static 'serverLogs/serverLogs.js' %}?v={{ CP_VERSION }}"></script>
@@ -2300,8 +2430,88 @@
localStorage.setItem('htaccessNotificationDismissed', 'true');
}
function isNotificationDismissed(notificationKey) {
if (notificationKey === 'backup-notification') {
return {% if backup_notification_dismissed %}true{% else %}false{% endif %};
}
if (notificationKey === 'ai-scanner-notification') {
return {% if ai_scanner_notification_dismissed %}true{% else %}false{% endif %};
}
if (notificationKey === 'htaccess-notification') {
return localStorage.getItem('htaccessNotificationDismissed') === 'true';
}
return false;
}
function toggleNotificationCenter() {
const dropdown = document.getElementById('notification-center-dropdown');
if (dropdown) {
dropdown.classList.toggle('show');
if (dropdown.classList.contains('show')) {
loadNotificationCenter();
}
}
}
function loadNotificationCenter() {
const list = document.getElementById('notification-center-list');
if (!list) return;
const notifications = [
{ id: 'backup', title: 'Backup Security', icon: 'fas fa-shield-alt',
text: 'Looks like your websites are not secured with automatic backups.',
link: '/backup/OneClickBackups', linkText: 'Configure now',
learnMoreLink: 'https://cyberpanel.net/docs/backup',
dismissed: isNotificationDismissed('backup-notification') },
{ id: 'ai-scanner', title: 'AI Security Scanner', icon: 'fas fa-brain',
text: 'Secure your websites with AI-powered malware detection. Advanced threat detection • Real-time scanning • Zero false positives',
link: '/aiscanner/', linkText: 'Start AI Security Scan',
learnMoreLink: 'https://cyberpanel.net/docs/ai-scanner',
dismissed: isNotificationDismissed('ai-scanner-notification') },
{ id: 'htaccess', title: '.htaccess Support', icon: 'fas fa-magic',
text: 'Revolutionary .htaccess Support Now Live! Full .htaccess support • PHP configuration now works • Zero rule rewrites needed',
link: 'https://cyberpanel.net/cyberpanel-htaccess-module', linkText: 'View Details',
learnMoreLink: 'https://cyberpanel.net/cyberpanel-htaccess-module',
dismissed: isNotificationDismissed('htaccess-notification') }
];
if (notifications.length === 0) {
list.innerHTML = '<div class="notification-center-empty">No notifications available</div>';
} else {
list.innerHTML = notifications.map(notif => {
let linkIcon = notif.linkText.includes('Configure') ? '<i class="fas fa-cog"></i>' :
notif.linkText.includes('Start') ? '<i class="fas fa-rocket"></i>' :
(notif.linkText.includes('View') || notif.linkText.includes('Details')) ? '<i class="fas fa-external-link-alt"></i>' : '<i class="fas fa-arrow-right"></i>';
const isExternal = notif.link.startsWith('http');
const learnExternal = notif.learnMoreLink && notif.learnMoreLink.startsWith('http');
return `<div class="notification-center-item ${notif.dismissed ? 'dismissed' : ''}">
<div class="notification-center-item-title">
<i class="${notif.icon}"></i>
<span>${notif.title}</span>
${notif.dismissed ? '<span class="dismissed-badge">Dismissed</span>' : ''}
</div>
<div class="notification-center-item-text">${notif.text}</div>
<div class="notification-center-item-actions">
<a href="${notif.link}" class="notification-center-item-link" ${isExternal ? 'target="_blank" rel="noopener"' : ''}>${linkIcon} <span>${notif.linkText}</span></a>
${notif.learnMoreLink ? `<a href="${notif.learnMoreLink}" class="notification-center-item-link-secondary" ${learnExternal ? 'target="_blank" rel="noopener"' : ''}><i class="fas fa-info-circle"></i> <span>Learn More</span> <i class="fas fa-arrow-right"></i></a>` : ''}
</div>
</div>`;
}).join('');
}
const activeCount = notifications.filter(n => !n.dismissed).length;
const badge = document.getElementById('notification-badge');
if (badge) {
badge.textContent = activeCount;
badge.style.display = 'flex';
}
}
document.addEventListener('click', function(event) {
const dropdown = document.getElementById('notification-center-dropdown');
const button = document.getElementById('notification-center-btn');
if (dropdown && button && !dropdown.contains(event.target) && !button.contains(event.target)) {
dropdown.classList.remove('show');
}
});
// Check all notification statuses when page loads
document.addEventListener('DOMContentLoaded', function() {
loadNotificationCenter();
checkBackupStatus();
// Show AI Scanner notification with a slight delay for better UX
setTimeout(checkAIScannerStatus, 1000);
@@ -2388,32 +2598,47 @@
<!-- FTP Quota and Bandwidth Management Functions -->
<script>
function loadFTPQuotaManagement() {
// Load FTP quota management interface
$.get('{% url "getFTPQuotas" %}', function(data) {
if (data.status === 1) {
// Create modal or redirect to dedicated page
window.open('/websiteFunctions/ftpQuotaManagement.html', '_blank');
} else {
alert('Error loading FTP quota management: ' + data.message);
$.ajax({
url: '{% url "getFTPQuotas" %}',
method: 'GET',
dataType: 'json',
success: function(data) {
if (data.status === 1) {
window.open('{% url "ftpQuotaManagementPage" %}', '_blank');
} else {
var msg = (data && (data.error_message || data.message || data.errorMessage)) || 'Unknown error';
alert('Error loading FTP quota management: ' + msg);
}
},
error: function(xhr) {
var msg = (xhr.responseJSON && (xhr.responseJSON.error_message || xhr.responseJSON.message)) || (xhr.statusText || 'Request failed');
alert('Error loading FTP quota management: ' + msg);
}
});
}
function loadBandwidthManagement() {
// Load bandwidth management interface
$.get('{% url "getBandwidthResetLogs" %}', function(data) {
if (data.status === 1) {
// Create modal or redirect to dedicated page
window.open('/websiteFunctions/bandwidthManagement.html', '_blank');
} else {
alert('Error loading bandwidth management: ' + data.message);
$.ajax({
url: '{% url "getBandwidthResetLogs" %}',
method: 'GET',
dataType: 'json',
success: function(data) {
if (data.status === 1) {
window.open('{% url "bandwidthManagementPage" %}', '_blank');
} else {
var msg = (data && (data.error_message || data.message || data.errorMessage)) || 'Unknown error';
alert('Error loading bandwidth management: ' + msg);
}
},
error: function(xhr) {
var msg = (xhr.responseJSON && (xhr.responseJSON.error_message || xhr.responseJSON.message)) || (xhr.statusText || 'Request failed');
alert('Error loading bandwidth management: ' + msg);
}
});
}
function loadSecurityManagement() {
// Load security management interface
window.open('/websiteFunctions/securityManagement.html', '_blank');
window.open('{% url "securityManagementPage" %}', '_blank');
}
</script>

View File

@@ -523,12 +523,17 @@ def RestartCyberPanel(request):
def getDashboardStats(request):
try:
val = request.session['userID']
val = request.session.get('userID')
if val is None:
return HttpResponse(
json.dumps({'status': 0, 'error_message': 'Session required'}),
content_type='application/json'
)
currentACL = ACLManager.loadedACL(val)
admin = Administrator.objects.get(pk=val)
# Check if user is admin
if currentACL['admin'] == 1:
if currentACL.get('admin', 0) == 1:
# Admin can see all resources
total_users = Administrator.objects.count()
total_sites = Websites.objects.count()
@@ -566,7 +571,7 @@ def getDashboardStats(request):
total_emails = EUsers.objects.filter(emailOwner__domainOwner__domain__in=website_names).count()
# Count FTP users associated with user's domains
total_ftp_users = FTPUsers.objects.filter(domain__in=website_names).count()
total_ftp_users = FTPUsers.objects.filter(domain__domain__in=website_names).count()
else:
total_wp_sites = 0
total_dbs = 0
@@ -584,18 +589,24 @@ def getDashboardStats(request):
}
return HttpResponse(json.dumps(data), content_type='application/json')
except Exception as e:
return HttpResponse(json.dumps({'status': 0, 'error_message': str(e)}), content_type='application/json')
logging.writeToFile('getDashboardStats error: %s' % str(e))
return HttpResponse(
json.dumps({'status': 0, 'error_message': 'Failed to load dashboard stats'}),
content_type='application/json'
)
def getTrafficStats(request):
try:
val = request.session['userID']
val = request.session.get('userID')
if val is None:
return HttpResponse(
json.dumps({'status': 0, 'error_message': 'Session required'}),
content_type='application/json'
)
currentACL = ACLManager.loadedACL(val)
# Only admins should see system-wide network stats
if not currentACL.get('admin', 0):
return HttpResponse(json.dumps({'status': 0, 'error_message': 'Admin access required', 'admin_only': True}), content_type='application/json')
# Get network stats from /proc/net/dev (Linux)
rx = tx = 0
with open('/proc/net/dev', 'r') as f:
for line in f.readlines():
@@ -603,42 +614,49 @@ def getTrafficStats(request):
continue
if ':' in line:
parts = line.split()
rx += int(parts[1])
tx += int(parts[9])
data = {
'rx_bytes': rx,
'tx_bytes': tx,
'status': 1
}
try:
if len(parts) >= 10:
rx += int(parts[1])
tx += int(parts[9])
except (ValueError, IndexError):
continue
data = {'rx_bytes': rx, 'tx_bytes': tx, 'status': 1}
return HttpResponse(json.dumps(data), content_type='application/json')
except Exception as e:
return HttpResponse(json.dumps({'status': 0, 'error_message': str(e)}), content_type='application/json')
logging.writeToFile('getTrafficStats error: %s' % str(e))
return HttpResponse(
json.dumps({'status': 0, 'error_message': 'Failed to load traffic stats'}),
content_type='application/json'
)
def getDiskIOStats(request):
try:
val = request.session['userID']
val = request.session.get('userID')
if val is None:
return HttpResponse(
json.dumps({'status': 0, 'error_message': 'Session required'}),
content_type='application/json'
)
currentACL = ACLManager.loadedACL(val)
# Only admins should see system-wide disk I/O stats
if not currentACL.get('admin', 0):
return HttpResponse(json.dumps({'status': 0, 'error_message': 'Admin access required', 'admin_only': True}), content_type='application/json')
# Parse /proc/diskstats for all disks
read_sectors = 0
write_sectors = 0
sector_size = 512 # Most Linux systems use 512 bytes per sector
sector_size = 512
with open('/proc/diskstats', 'r') as f:
for line in f:
parts = line.split()
if len(parts) < 14:
continue
# parts[2] is device name, skip loopback/ram devices
dev = parts[2]
if dev.startswith('loop') or dev.startswith('ram'):
continue
# 6th and 10th columns: sectors read/written
read_sectors += int(parts[5])
write_sectors += int(parts[9])
try:
read_sectors += int(parts[5])
write_sectors += int(parts[9])
except (ValueError, IndexError):
continue
data = {
'read_bytes': read_sectors * sector_size,
'write_bytes': write_sectors * sector_size,
@@ -646,34 +664,42 @@ def getDiskIOStats(request):
}
return HttpResponse(json.dumps(data), content_type='application/json')
except Exception as e:
return HttpResponse(json.dumps({'status': 0, 'error_message': str(e)}), content_type='application/json')
logging.writeToFile('getDiskIOStats error: %s' % str(e))
return HttpResponse(
json.dumps({'status': 0, 'error_message': 'Failed to load disk I/O stats'}),
content_type='application/json'
)
def getCPULoadGraph(request):
try:
val = request.session['userID']
val = request.session.get('userID')
if val is None:
return HttpResponse(
json.dumps({'status': 0, 'error_message': 'Session required'}),
content_type='application/json'
)
currentACL = ACLManager.loadedACL(val)
# Only admins should see system-wide CPU stats
if not currentACL.get('admin', 0):
return HttpResponse(json.dumps({'status': 0, 'error_message': 'Admin access required', 'admin_only': True}), content_type='application/json')
# Parse /proc/stat for the 'cpu' line
cpu_times = []
with open('/proc/stat', 'r') as f:
for line in f:
if line.startswith('cpu '):
parts = line.strip().split()
# parts[1:] are user, nice, system, idle, iowait, irq, softirq, steal, guest, guest_nice
cpu_times = [float(x) for x in parts[1:]]
try:
cpu_times = [float(x) for x in parts[1:]]
except (ValueError, IndexError):
pass
break
else:
cpu_times = []
data = {
'cpu_times': cpu_times,
'status': 1
}
data = {'cpu_times': cpu_times, 'status': 1}
return HttpResponse(json.dumps(data), content_type='application/json')
except Exception as e:
return HttpResponse(json.dumps({'status': 0, 'error_message': str(e)}), content_type='application/json')
logging.writeToFile('getCPULoadGraph error: %s' % str(e))
return HttpResponse(
json.dumps({'status': 0, 'error_message': 'Failed to load CPU stats'}),
content_type='application/json'
)
@csrf_exempt
@require_GET
@@ -790,6 +816,7 @@ def getRecentSSHLogs(request):
if not currentACL.get('admin', 0):
return HttpResponse(json.dumps({'error': 'Admin only'}), content_type='application/json', status=403)
from plogical.processUtilities import ProcessUtilities
import re
distro = ProcessUtilities.decideDistro()
if distro in [ProcessUtilities.ubuntu, ProcessUtilities.ubuntu20]:
log_path = '/var/log/auth.log'
@@ -801,6 +828,9 @@ def getRecentSSHLogs(request):
return HttpResponse(json.dumps({'error': f'Failed to read log: {str(e)}'}), content_type='application/json', status=500)
lines = output.split('\n')
logs = []
# IP address regex patterns (IPv4)
ipv4_pattern = r'\b(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\b'
for line in lines:
if not line.strip():
continue
@@ -811,7 +841,26 @@ def getRecentSSHLogs(request):
else:
timestamp = ''
message = line
logs.append({'timestamp': timestamp, 'message': message, 'raw': line})
# Extract IP address from the log line
ip_address = None
ip_matches = re.findall(ipv4_pattern, line)
if ip_matches:
# Filter out localhost and common non-external IPs
for ip in ip_matches:
if ip not in ['127.0.0.1', '0.0.0.0', '::1'] and not ip.startswith('192.168.') and not ip.startswith('10.') and not ip.startswith('172.'):
ip_address = ip
break
# If no external IP found, use the first match anyway (might be needed for internal attacks)
if not ip_address and ip_matches:
ip_address = ip_matches[0]
logs.append({
'timestamp': timestamp,
'message': message,
'raw': line,
'ip_address': ip_address
})
return HttpResponse(json.dumps({'logs': logs}), content_type='application/json')
except Exception as e:
return HttpResponse(json.dumps({'error': str(e)}), content_type='application/json', status=500)
@@ -1153,7 +1202,30 @@ def blockIPAddress(request):
'error': 'Premium feature required'
}), content_type='application/json', status=403)
data = json.loads(request.body)
# Parse request body - Django request.body is always bytes
try:
if not request.body:
return HttpResponse(json.dumps({
'status': 0,
'error': 'Request body is empty'
}), content_type='application/json', status=400)
body_str = request.body.decode('utf-8')
if not body_str or body_str.strip() == '':
return HttpResponse(json.dumps({
'status': 0,
'error': 'Request body is empty'
}), content_type='application/json', status=400)
data = json.loads(body_str)
except (json.JSONDecodeError, UnicodeDecodeError) as e:
import plogical.CyberCPLogFileWriter as logging
logging.CyberCPLogFileWriter.writeToFile(f'JSON decode error in blockIPAddress: {str(e)}, body: {request.body[:200] if request.body else "empty"}')
return HttpResponse(json.dumps({
'status': 0,
'error': f'Invalid request format: {str(e)}'
}), content_type='application/json', status=400)
ip_address = data.get('ip_address', '').strip()
if not ip_address:
@@ -1248,6 +1320,54 @@ def blockIPAddress(request):
error_message = f'Firewall command failed: {str(e)}'
if success:
# Add to banned IPs JSON file for consistency with firewall page
try:
import os
import time
banned_ips_file = '/etc/cyberpanel/banned_ips.json'
banned_ips = []
if os.path.exists(banned_ips_file):
try:
with open(banned_ips_file, 'r') as f:
banned_ips = json.load(f)
except:
banned_ips = []
# Check if IP is already banned
ip_already_banned = False
for banned_ip in banned_ips:
if banned_ip.get('ip') == ip_address and banned_ip.get('active', True):
ip_already_banned = True
break
if not ip_already_banned:
# Get reason from request data
reason = data.get('reason', 'Security alert detected from dashboard')
# Add new banned IP
new_banned_ip = {
'id': int(time.time()),
'ip': ip_address,
'reason': reason,
'duration': 'permanent',
'banned_on': time.time(),
'expires': 'Never',
'active': True
}
banned_ips.append(new_banned_ip)
# Ensure directory exists
os.makedirs(os.path.dirname(banned_ips_file), exist_ok=True)
# Save to file
with open(banned_ips_file, 'w') as f:
json.dump(banned_ips, f, indent=2)
except Exception as e:
# Log but don't fail the request if JSON update fails
import plogical.CyberCPLogFileWriter as logging
logging.CyberCPLogFileWriter.writeToFile(f'Warning: Failed to update banned_ips.json: {str(e)}')
# Log the action
import plogical.CyberCPLogFileWriter as logging
logging.CyberCPLogFileWriter.writeToFile(f'IP address {ip_address} blocked via CyberPanel dashboard by user {user_id}')
@@ -1263,7 +1383,18 @@ def blockIPAddress(request):
'error': error_message or 'Failed to block IP address'
}), content_type='application/json', status=500)
except json.JSONDecodeError as e:
import plogical.CyberCPLogFileWriter as logging
logging.CyberCPLogFileWriter.writeToFile(f'JSON decode error in blockIPAddress: {str(e)}, body: {request.body}')
return HttpResponse(json.dumps({
'status': 0,
'error': f'Invalid JSON in request: {str(e)}'
}), content_type='application/json', status=400)
except Exception as e:
import plogical.CyberCPLogFileWriter as logging
import traceback
error_trace = traceback.format_exc()
logging.CyberCPLogFileWriter.writeToFile(f'Error in blockIPAddress: {str(e)}\n{error_trace}')
return HttpResponse(json.dumps({
'status': 0,
'error': f'Server error: {str(e)}'

View File

@@ -1810,49 +1810,49 @@ class FirewallManager:
def getBannedIPs(self, userID=None):
"""
Get list of banned IP addresses
Get list of banned IP addresses from database
"""
try:
admin = Administrator.objects.get(pk=userID)
if admin.acl.adminStatus != 1:
return ACLManager.loadError()
# For now, we'll use a simple file-based storage
# In production, you might want to use a database
banned_ips_file = '/etc/cyberpanel/banned_ips.json'
# Import BannedIP model and Django Q
from firewall.models import BannedIP
from django.db.models import Q
banned_ips = []
if os.path.exists(banned_ips_file):
try:
with open(banned_ips_file, 'r') as f:
banned_ips = json.load(f)
except:
banned_ips = []
# Get all active banned IPs that haven't expired
current_time = int(time.time())
banned_ips_queryset = BannedIP.objects.filter(
active=True
).filter(
Q(expires__isnull=True) | Q(expires__gt=current_time)
).order_by('-banned_on')
# Filter out expired bans
current_time = time.time()
active_banned_ips = []
for banned_ip in banned_ips:
if banned_ip.get('expires') == 'Never' or banned_ip.get('expires', 0) > current_time:
banned_ip['active'] = True
if banned_ip.get('expires') != 'Never':
banned_ip['expires'] = time.strftime('%Y-%m-%d %H:%M:%S', time.localtime(banned_ip['expires']))
else:
banned_ip['expires'] = 'Never'
banned_ip['banned_on'] = time.strftime('%Y-%m-%d %H:%M:%S', time.localtime(banned_ip.get('banned_on', current_time)))
else:
banned_ip['active'] = False
banned_ip['expires'] = time.strftime('%Y-%m-%d %H:%M:%S', time.localtime(banned_ip.get('expires', current_time)))
banned_ip['banned_on'] = time.strftime('%Y-%m-%d %H:%M:%S', time.localtime(banned_ip.get('banned_on', current_time)))
for banned_ip in banned_ips_queryset:
# Format the data for frontend
ip_data = {
'id': banned_ip.id,
'ip': banned_ip.ip_address,
'reason': banned_ip.reason,
'duration': banned_ip.duration,
'banned_on': banned_ip.get_banned_on_display(),
'expires': banned_ip.get_expires_display(),
'active': not banned_ip.is_expired() and banned_ip.active
}
active_banned_ips.append(banned_ip)
# Only include truly active bans
if ip_data['active']:
active_banned_ips.append(ip_data)
final_dic = {'status': 1, 'bannedIPs': active_banned_ips}
final_json = json.dumps(final_dic)
return HttpResponse(final_json)
return HttpResponse(final_json, content_type='application/json')
except BaseException as msg:
import plogical.CyberCPLogFileWriter as logging
logging.CyberCPLogFileWriter.writeToFile(f'Error in getBannedIPs: {str(msg)}')
final_dic = {'status': 0, 'error_message': str(msg)}
final_json = json.dumps(final_dic)
return HttpResponse(final_json)
@@ -1934,19 +1934,53 @@ class FirewallManager:
with open(banned_ips_file, 'w') as f:
json.dump(banned_ips, f, indent=2)
# Apply firewall rule to block the IP
# Apply firewall rule to block the IP using firewalld
try:
# Add iptables rule to block the IP
if '/' in ip:
# CIDR notation
subprocess.run(['iptables', '-A', 'INPUT', '-s', ip, '-j', 'DROP'], check=True)
else:
# Single IP
subprocess.run(['iptables', '-A', 'INPUT', '-s', ip, '-j', 'DROP'], check=True)
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.'}
final_json = json.dumps(final_dic)
return HttpResponse(final_json)
logging.CyberCPLogFileWriter.writeToFile(f'Banned IP {ip} with reason: {reason}')
except subprocess.CalledProcessError as e:
logging.CyberCPLogFileWriter.writeToFile(f'Failed to add iptables rule for {ip}: {str(e)}')
# 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(f'Failed to reload firewalld for {ip}: {reload_result.stderr}')
final_dic = {'status': 0, 'error_message': f'Failed to reload firewall rules: {reload_result.stderr}'}
final_json = json.dumps(final_dic)
return HttpResponse(final_json)
else:
# Check if rule already exists (this is not an error)
if 'ALREADY_ENABLED' in result.stderr or 'already exists' in result.stderr.lower():
logging.CyberCPLogFileWriter.writeToFile(f'IP {ip} already blocked in firewalld')
else:
logging.CyberCPLogFileWriter.writeToFile(f'Failed to add firewalld rule for {ip}: {result.stderr}')
final_dic = {'status': 0, 'error_message': f'Failed to add firewall rule: {result.stderr}'}
final_json = json.dumps(final_dic)
return HttpResponse(final_json)
except subprocess.TimeoutExpired:
logging.CyberCPLogFileWriter.writeToFile(f'Timeout adding firewalld rule for {ip}')
final_dic = {'status': 0, 'error_message': 'Firewall command timed out'}
final_json = json.dumps(final_dic)
return HttpResponse(final_json)
except Exception as e:
logging.CyberCPLogFileWriter.writeToFile(f'Failed to add firewalld rule for {ip}: {str(e)}')
final_dic = {'status': 0, 'error_message': f'Firewall command failed: {str(e)}'}
final_json = json.dumps(final_dic)
return HttpResponse(final_json)
final_dic = {'status': 1, 'message': f'IP address {ip} has been banned successfully'}
final_json = json.dumps(final_dic)
@@ -1996,19 +2030,40 @@ class FirewallManager:
with open(banned_ips_file, 'w') as f:
json.dump(banned_ips, f, indent=2)
# Remove iptables rule
# Remove firewalld rule to unblock the IP
try:
# Remove iptables rule to unblock the IP
if '/' in ip_to_unban:
# CIDR notation
subprocess.run(['iptables', '-D', 'INPUT', '-s', ip_to_unban, '-j', 'DROP'], check=False)
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:
# Single IP
subprocess.run(['iptables', '-D', 'INPUT', '-s', ip_to_unban, '-j', 'DROP'], check=False)
logging.CyberCPLogFileWriter.writeToFile(f'Unbanned IP {ip_to_unban}')
except subprocess.CalledProcessError as e:
logging.CyberCPLogFileWriter.writeToFile(f'Failed to remove iptables rule for {ip_to_unban}: {str(e)}')
# 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}')
except Exception as e:
logging.CyberCPLogFileWriter.writeToFile(f'Failed to remove 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)

View File

@@ -1,4 +1,8 @@
from django.db import models
from django.utils import timezone
from django.utils.timezone import now
import time
# Create your models here.
@@ -7,3 +11,57 @@ class FirewallRules(models.Model):
proto = models.CharField(max_length=10)
port = models.CharField(max_length=25)
ipAddress = models.CharField(max_length=30,default="0.0.0.0/0")
class BannedIP(models.Model):
"""
Model to store banned IP addresses
"""
ip_address = models.GenericIPAddressField(unique=True, db_index=True, verbose_name="IP Address")
reason = models.CharField(max_length=255, verbose_name="Ban Reason")
duration = models.CharField(max_length=50, default='permanent', verbose_name="Duration")
banned_on = models.DateTimeField(auto_now_add=True, verbose_name="Banned On")
expires = models.BigIntegerField(null=True, blank=True, verbose_name="Expires Timestamp")
# expires can be: null (never expires), or Unix timestamp
active = models.BooleanField(default=True, db_index=True, verbose_name="Active")
class Meta:
db_table = 'firewall_bannedips'
verbose_name = "Banned IP"
verbose_name_plural = "Banned IPs"
indexes = [
models.Index(fields=['ip_address', 'active']),
models.Index(fields=['active', 'expires']),
]
def __str__(self):
return f"{self.ip_address} - {self.reason}"
def is_expired(self):
"""
Check if the ban has expired
Returns True if expired, False if still active
"""
if self.expires is None:
# Never expires
return False
current_time = int(time.time())
return self.expires <= current_time
def get_expires_display(self):
"""
Get human-readable expiration date
"""
if self.expires is None:
return "Never"
return timezone.datetime.fromtimestamp(self.expires).strftime('%Y-%m-%d %H:%M:%S')
def get_banned_on_display(self):
"""
Get human-readable banned on date
"""
if self.banned_on:
if hasattr(self.banned_on, 'strftime'):
return self.banned_on.strftime('%Y-%m-%d %H:%M:%S')
return str(self.banned_on)
return timezone.now().strftime('%Y-%m-%d %H:%M:%S')

View File

@@ -2,10 +2,25 @@
* Created by usman on 9/5/17.
*/
// Helper function to get CSRF token cookie
function getCookie(name) {
var cookieValue = null;
if (document.cookie && document.cookie !== '') {
var cookies = document.cookie.split(';');
for (var i = 0; i < cookies.length; i++) {
var cookie = jQuery.trim(cookies[i]);
if (cookie.substring(0, name.length + 1) === (name + '=')) {
cookieValue = decodeURIComponent(cookie.substring(name.length + 1));
break;
}
}
}
return cookieValue;
}
/* Java script code to ADD Firewall Rules */
app.controller('firewallController', function ($scope, $http) {
app.controller('firewallController', function ($scope, $http, $timeout) {
$scope.rulesLoading = true;
$scope.actionFailed = true;
@@ -18,7 +33,10 @@ app.controller('firewallController', function ($scope, $http) {
// Banned IPs variables
$scope.activeTab = 'rules';
$scope.bannedIPs = [];
$scope.bannedIPs = []; // Initialize as empty array
// Initialize banned IPs array - start as null so template shows empty state
// Will be set to array after API call
$scope.bannedIPsLoading = false;
$scope.bannedIPActionFailed = true;
$scope.bannedIPActionSuccess = true;
@@ -30,7 +48,144 @@ app.controller('firewallController', function ($scope, $http) {
firewallStatus();
populateCurrentRecords();
populateBannedIPs();
// Load banned IPs immediately when controller initializes
console.log('=== FIREWALL CONTROLLER INITIALIZING ===');
console.log('Initializing firewall controller, loading banned IPs...');
// Define populateBannedIPs function first, then call it
// This ensures the function is available when setTimeout executes
function populateBannedIPs() {
console.log('=== populateBannedIPs() START ===');
console.log('Current scope.bannedIPs:', $scope.bannedIPs);
console.log('Current activeTab:', $scope.activeTab);
$scope.bannedIPsLoading = true;
var url = "/firewall/getBannedIPs";
var csrfToken = getCookie('csrftoken');
var config = {
headers: {
'X-CSRFToken': csrfToken
}
};
console.log('Making request to:', url);
console.log('CSRF Token:', csrfToken ? 'Found (' + csrfToken.substring(0, 10) + '...)' : 'MISSING!');
$http.post(url, {}, config).then(
function(response) {
console.log('=== API RESPONSE RECEIVED ===');
console.log('Response status:', response.status);
console.log('Response data:', JSON.stringify(response.data, null, 2));
$scope.bannedIPsLoading = false;
// Reset error flags
$scope.bannedIPActionFailed = true;
$scope.bannedIPActionSuccess = true;
$scope.bannedIPCouldNotConnect = true;
if (response.data && response.data.status === 1) {
var bannedIPsArray = response.data.bannedIPs || [];
console.log('Raw bannedIPs from API:', bannedIPsArray);
console.log('Banned IPs count:', bannedIPsArray.length);
console.log('Is array?', Array.isArray(bannedIPsArray));
// Ensure it's an array
if (!Array.isArray(bannedIPsArray)) {
console.error('ERROR: bannedIPs is not an array:', typeof bannedIPsArray);
bannedIPsArray = [];
}
// Assign to scope - Angular $http callbacks already run within $apply
console.log('Assigning to scope.bannedIPs...');
$scope.bannedIPs = bannedIPsArray;
console.log('After assignment - scope.bannedIPs:', $scope.bannedIPs);
console.log('After assignment - scope.bannedIPs.length:', $scope.bannedIPs ? $scope.bannedIPs.length : 'undefined');
console.log('After assignment - activeTab:', $scope.activeTab);
// No need to call $apply - $http callbacks run within $apply automatically
console.log('View should update automatically (Angular $http handles $apply)');
console.log('=== populateBannedIPs() SUCCESS ===');
} else {
console.error('ERROR: API returned status !== 1');
console.error('Response data:', response.data);
$scope.bannedIPs = [];
$scope.bannedIPActionFailed = false;
$scope.bannedIPErrorMessage = (response.data && response.data.error_message) || 'Unknown error';
}
},
function(error) {
console.error('=== HTTP ERROR ===');
console.error('Error object:', error);
console.error('Error status:', error.status);
console.error('Error data:', error.data);
console.error('Error statusText:', error.statusText);
$scope.bannedIPsLoading = false;
$scope.bannedIPActionFailed = true;
$scope.bannedIPActionSuccess = true;
$scope.bannedIPCouldNotConnect = false;
$scope.bannedIPs = [];
try {
if (!$scope.$$phase && !$scope.$root.$$phase) {
$scope.$apply();
}
} catch(e) {
console.error('Error in $apply (error handler):', e);
}
}
);
}
// Expose to scope for template access
$scope.populateBannedIPs = function() {
console.log('$scope.populateBannedIPs() called from template');
populateBannedIPs();
};
// Load banned IPs on page load - use $timeout for Angular compatibility
// Wrap in try-catch to ensure it executes even if there are other errors
try {
$timeout(function() {
try {
console.log('=== Calling populateBannedIPs from $timeout on page load ===');
populateBannedIPs();
} catch(e) {
console.error('Error in populateBannedIPs from timeout:', e);
}
}, 500);
} catch(e) {
console.error('Error setting up timeout for populateBannedIPs:', e);
}
// Also load when switching to banned tab - use deep watch for immediate trigger
try {
$scope.$watch('activeTab', function(newVal, oldVal) {
console.log('=== activeTab WATCH TRIGGERED ===');
console.log('activeTab changed from', oldVal, 'to', newVal);
if (newVal === 'banned') {
console.log('Switched to banned IPs tab, calling populateBannedIPs...');
// Call immediately
try {
if (typeof populateBannedIPs === 'function') {
console.log('Calling populateBannedIPs from $watch...');
populateBannedIPs();
} else if (typeof $scope.populateBannedIPs === 'function') {
console.log('Calling $scope.populateBannedIPs from $watch...');
$scope.populateBannedIPs();
} else {
console.error('ERROR: populateBannedIPs is not available!');
}
} catch(e) {
console.error('Error calling populateBannedIPs from watch:', e);
}
}
}, true); // Use deep watch (true parameter)
} catch(e) {
console.error('Error setting up $watch for activeTab:', e);
}
$scope.addRule = function () {
@@ -154,7 +309,7 @@ app.controller('firewallController', function ($scope, $http) {
}
};
}
$scope.deleteRule = function (id, proto, port, ruleIP) {
@@ -513,11 +668,280 @@ app.controller('firewallController', function ($scope, $http) {
}
}
// Banned IPs Functions
$scope.addBannedIP = function() {
if (!$scope.banIP || !$scope.banReason) {
$scope.bannedIPActionFailed = false;
$scope.bannedIPErrorMessage = "Please fill in all required fields";
return;
}
$scope.bannedIPsLoading = true;
$scope.bannedIPActionFailed = true;
$scope.bannedIPActionSuccess = true;
$scope.bannedIPCouldNotConnect = true;
var data = {
ip: $scope.banIP,
reason: $scope.banReason,
duration: $scope.banDuration
};
var url = "/firewall/addBannedIP";
var config = {
headers: {
'X-CSRFToken': getCookie('csrftoken')
}
};
$http.post(url, data, config).then(function(response) {
$scope.bannedIPsLoading = false;
// Reset error flags
$scope.bannedIPActionFailed = true;
$scope.bannedIPActionSuccess = true;
$scope.bannedIPCouldNotConnect = true;
if (response.data.status === 1) {
$scope.bannedIPActionSuccess = false;
$scope.banIP = '';
$scope.banReason = '';
$scope.banDuration = '24h';
console.log('IP banned successfully, refreshing list...');
populateBannedIPs(); // Refresh the list
} else {
$scope.bannedIPActionFailed = false;
$scope.bannedIPErrorMessage = response.data.error_message || 'Unknown error';
console.error('Failed to ban IP:', response.data);
}
}, function(error) {
$scope.bannedIPsLoading = false;
$scope.bannedIPActionFailed = true;
$scope.bannedIPActionSuccess = true;
$scope.bannedIPCouldNotConnect = false;
console.error('Error banning IP:', error);
});
};
$scope.removeBannedIP = function(id, ip) {
if (!confirm('Are you sure you want to unban IP address ' + ip + '?')) {
return;
}
$scope.bannedIPsLoading = true;
$scope.bannedIPActionFailed = true;
$scope.bannedIPActionSuccess = true;
$scope.bannedIPCouldNotConnect = true;
var data = { id: id };
var url = "/firewall/removeBannedIP";
var config = {
headers: {
'X-CSRFToken': getCookie('csrftoken')
}
};
$http.post(url, data, config).then(function(response) {
$scope.bannedIPsLoading = false;
if (response.data.status === 1) {
$scope.bannedIPActionSuccess = false;
populateBannedIPs(); // Refresh the list
} else {
$scope.bannedIPActionFailed = false;
$scope.bannedIPErrorMessage = response.data.error_message;
}
}, function(error) {
$scope.bannedIPsLoading = false;
$scope.bannedIPCouldNotConnect = false;
});
};
$scope.deleteBannedIP = function(id, ip) {
if (!confirm('Are you sure you want to permanently delete the record for IP address ' + ip + '? This action cannot be undone.')) {
return;
}
$scope.bannedIPsLoading = true;
$scope.bannedIPActionFailed = true;
$scope.bannedIPActionSuccess = true;
$scope.bannedIPCouldNotConnect = true;
var data = { id: id };
var url = "/firewall/deleteBannedIP";
var config = {
headers: {
'X-CSRFToken': getCookie('csrftoken')
}
};
$http.post(url, data, config).then(function(response) {
$scope.bannedIPsLoading = false;
if (response.data.status === 1) {
$scope.bannedIPActionSuccess = false;
populateBannedIPs(); // Refresh the list
} else {
$scope.bannedIPActionFailed = false;
$scope.bannedIPErrorMessage = response.data.error_message;
}
}, function(error) {
$scope.bannedIPsLoading = false;
$scope.bannedIPCouldNotConnect = false;
});
};
// Export/Import Firewall Rules Functions
$scope.exportRules = function () {
$scope.rulesLoading = false;
$scope.actionFailed = true;
$scope.actionSuccess = true;
url = "/firewall/exportFirewallRules";
var data = {};
var config = {
headers: {
'X-CSRFToken': getCookie('csrftoken')
}
};
$http.post(url, data, config).then(exportSuccess, exportError);
function exportSuccess(response) {
$scope.rulesLoading = true;
// Check if response is JSON (error) or file download
if (typeof response.data === 'string' && response.data.includes('{')) {
try {
var errorData = JSON.parse(response.data);
if (errorData.exportStatus === 0) {
$scope.actionFailed = false;
$scope.actionSuccess = true;
$scope.errorMessage = errorData.error_message;
return;
}
} catch (e) {
// If not JSON, assume it's the file content
}
}
// If we get here, it's a successful file download
$scope.actionFailed = true;
$scope.actionSuccess = false;
}
function exportError(response) {
$scope.rulesLoading = true;
$scope.actionFailed = false;
$scope.actionSuccess = true;
$scope.errorMessage = "Could not connect to server. Please refresh this page.";
}
};
$scope.importRules = function () {
// Create file input element
var input = document.createElement('input');
input.type = 'file';
input.accept = '.json';
input.style.display = 'none';
input.onchange = function(event) {
var file = event.target.files[0];
if (file) {
var reader = new FileReader();
reader.onload = function(e) {
try {
var importData = JSON.parse(e.target.result);
// Validate file format
if (!importData.rules || !Array.isArray(importData.rules)) {
$scope.$apply(function() {
$scope.actionFailed = false;
$scope.actionSuccess = true;
$scope.errorMessage = "Invalid import file format. Please select a valid firewall rules export file.";
});
return;
}
// Upload file to server
uploadImportFile(file);
} catch (error) {
$scope.$apply(function() {
$scope.actionFailed = false;
$scope.actionSuccess = true;
$scope.errorMessage = "Invalid JSON file. Please select a valid firewall rules export file.";
});
}
};
reader.readAsText(file);
}
};
document.body.appendChild(input);
input.click();
document.body.removeChild(input);
};
function uploadImportFile(file) {
$scope.rulesLoading = false;
$scope.actionFailed = true;
$scope.actionSuccess = true;
var formData = new FormData();
formData.append('import_file', file);
var config = {
headers: {
'X-CSRFToken': getCookie('csrftoken'),
'Content-Type': undefined
},
transformRequest: angular.identity
};
$http.post("/firewall/importFirewallRules", formData, config).then(importSuccess, importError);
function importSuccess(response) {
$scope.rulesLoading = true;
if (response.data.importStatus === 1) {
$scope.actionFailed = true;
$scope.actionSuccess = false;
// Refresh rules list
populateCurrentRecords();
// Show import summary
var summary = `Import completed successfully!\n` +
`Imported: ${response.data.imported_count} rules\n` +
`Skipped: ${response.data.skipped_count} rules\n` +
`Errors: ${response.data.error_count} rules`;
if (response.data.errors && response.data.errors.length > 0) {
summary += `\n\nErrors:\n${response.data.errors.join('\n')}`;
}
alert(summary);
} else {
$scope.actionFailed = false;
$scope.actionSuccess = true;
$scope.errorMessage = response.data.error_message;
}
}
function importError(response) {
$scope.rulesLoading = true;
$scope.actionFailed = false;
$scope.actionSuccess = true;
$scope.errorMessage = "Could not connect to server. Please refresh this page.";
}
}
});
/* Java script code to ADD Firewall Rules */
/* Java script code to Secure SSH */
@@ -2413,287 +2837,4 @@ app.controller('litespeed_ent_conf', function ($scope, $http, $timeout, $window)
}
}
// Banned IPs Functions
function populateBannedIPs() {
$scope.bannedIPsLoading = true;
var url = "/firewall/getBannedIPs";
var config = {
headers: {
'X-CSRFToken': getCookie('csrftoken')
}
};
$http.post(url, {}, config).then(function(response) {
$scope.bannedIPsLoading = false;
if (response.data.status === 1) {
$scope.bannedIPs = response.data.bannedIPs || [];
} else {
$scope.bannedIPs = [];
$scope.bannedIPActionFailed = false;
$scope.bannedIPErrorMessage = response.data.error_message;
}
}, function(error) {
$scope.bannedIPsLoading = false;
$scope.bannedIPCouldNotConnect = false;
});
}
$scope.addBannedIP = function() {
if (!$scope.banIP || !$scope.banReason) {
$scope.bannedIPActionFailed = false;
$scope.bannedIPErrorMessage = "Please fill in all required fields";
return;
}
$scope.bannedIPsLoading = true;
$scope.bannedIPActionFailed = true;
$scope.bannedIPActionSuccess = true;
$scope.bannedIPCouldNotConnect = true;
var data = {
ip: $scope.banIP,
reason: $scope.banReason,
duration: $scope.banDuration
};
var url = "/firewall/addBannedIP";
var config = {
headers: {
'X-CSRFToken': getCookie('csrftoken')
}
};
$http.post(url, data, config).then(function(response) {
$scope.bannedIPsLoading = false;
if (response.data.status === 1) {
$scope.bannedIPActionSuccess = false;
$scope.banIP = '';
$scope.banReason = '';
$scope.banDuration = '24h';
populateBannedIPs(); // Refresh the list
} else {
$scope.bannedIPActionFailed = false;
$scope.bannedIPErrorMessage = response.data.error_message;
}
}, function(error) {
$scope.bannedIPsLoading = false;
$scope.bannedIPCouldNotConnect = false;
});
};
$scope.removeBannedIP = function(id, ip) {
if (!confirm('Are you sure you want to unban IP address ' + ip + '?')) {
return;
}
$scope.bannedIPsLoading = true;
$scope.bannedIPActionFailed = true;
$scope.bannedIPActionSuccess = true;
$scope.bannedIPCouldNotConnect = true;
var data = { id: id };
var url = "/firewall/removeBannedIP";
var config = {
headers: {
'X-CSRFToken': getCookie('csrftoken')
}
};
$http.post(url, data, config).then(function(response) {
$scope.bannedIPsLoading = false;
if (response.data.status === 1) {
$scope.bannedIPActionSuccess = false;
populateBannedIPs(); // Refresh the list
} else {
$scope.bannedIPActionFailed = false;
$scope.bannedIPErrorMessage = response.data.error_message;
}
}, function(error) {
$scope.bannedIPsLoading = false;
$scope.bannedIPCouldNotConnect = false;
});
};
$scope.deleteBannedIP = function(id, ip) {
if (!confirm('Are you sure you want to permanently delete the record for IP address ' + ip + '? This action cannot be undone.')) {
return;
}
$scope.bannedIPsLoading = true;
$scope.bannedIPActionFailed = true;
$scope.bannedIPActionSuccess = true;
$scope.bannedIPCouldNotConnect = true;
var data = { id: id };
var url = "/firewall/deleteBannedIP";
var config = {
headers: {
'X-CSRFToken': getCookie('csrftoken')
}
};
$http.post(url, data, config).then(function(response) {
$scope.bannedIPsLoading = false;
if (response.data.status === 1) {
$scope.bannedIPActionSuccess = false;
populateBannedIPs(); // Refresh the list
} else {
$scope.bannedIPActionFailed = false;
$scope.bannedIPErrorMessage = response.data.error_message;
}
}, function(error) {
$scope.bannedIPsLoading = false;
$scope.bannedIPCouldNotConnect = false;
});
};
// Export/Import Firewall Rules Functions
$scope.exportRules = function () {
$scope.rulesLoading = false;
$scope.actionFailed = true;
$scope.actionSuccess = true;
url = "/firewall/exportFirewallRules";
var data = {};
var config = {
headers: {
'X-CSRFToken': getCookie('csrftoken')
}
};
$http.post(url, data, config).then(exportSuccess, exportError);
function exportSuccess(response) {
$scope.rulesLoading = true;
// Check if response is JSON (error) or file download
if (typeof response.data === 'string' && response.data.includes('{')) {
try {
var errorData = JSON.parse(response.data);
if (errorData.exportStatus === 0) {
$scope.actionFailed = false;
$scope.actionSuccess = true;
$scope.errorMessage = errorData.error_message;
return;
}
} catch (e) {
// If not JSON, assume it's the file content
}
}
// If we get here, it's a successful file download
$scope.actionFailed = true;
$scope.actionSuccess = false;
}
function exportError(response) {
$scope.rulesLoading = true;
$scope.actionFailed = false;
$scope.actionSuccess = true;
$scope.errorMessage = "Could not connect to server. Please refresh this page.";
}
};
$scope.importRules = function () {
// Create file input element
var input = document.createElement('input');
input.type = 'file';
input.accept = '.json';
input.style.display = 'none';
input.onchange = function(event) {
var file = event.target.files[0];
if (file) {
var reader = new FileReader();
reader.onload = function(e) {
try {
var importData = JSON.parse(e.target.result);
// Validate file format
if (!importData.rules || !Array.isArray(importData.rules)) {
$scope.$apply(function() {
$scope.actionFailed = false;
$scope.actionSuccess = true;
$scope.errorMessage = "Invalid import file format. Please select a valid firewall rules export file.";
});
return;
}
// Upload file to server
uploadImportFile(file);
} catch (error) {
$scope.$apply(function() {
$scope.actionFailed = false;
$scope.actionSuccess = true;
$scope.errorMessage = "Invalid JSON file. Please select a valid firewall rules export file.";
});
}
};
reader.readAsText(file);
}
};
document.body.appendChild(input);
input.click();
document.body.removeChild(input);
};
function uploadImportFile(file) {
$scope.rulesLoading = false;
$scope.actionFailed = true;
$scope.actionSuccess = true;
var formData = new FormData();
formData.append('import_file', file);
var config = {
headers: {
'X-CSRFToken': getCookie('csrftoken'),
'Content-Type': undefined
},
transformRequest: angular.identity
};
$http.post("/firewall/importFirewallRules", formData, config).then(importSuccess, importError);
function importSuccess(response) {
$scope.rulesLoading = true;
if (response.data.importStatus === 1) {
$scope.actionFailed = true;
$scope.actionSuccess = false;
// Refresh rules list
populateCurrentRecords();
// Show import summary
var summary = `Import completed successfully!\n` +
`Imported: ${response.data.imported_count} rules\n` +
`Skipped: ${response.data.skipped_count} rules\n` +
`Errors: ${response.data.error_count} rules`;
if (response.data.errors && response.data.errors.length > 0) {
summary += `\n\nErrors:\n${response.data.errors.join('\n')}`;
}
alert(summary);
} else {
$scope.actionFailed = false;
$scope.actionSuccess = true;
$scope.errorMessage = response.data.error_message;
}
}
function importError(response) {
$scope.rulesLoading = true;
$scope.actionFailed = false;
$scope.actionSuccess = true;
$scope.errorMessage = "Could not connect to server. Please refresh this page.";
}
}
});

View File

@@ -895,7 +895,7 @@
{% trans "Firewall Rules" %}
</button>
<button type="button"
ng-click="activeTab = 'banned'"
ng-click="activeTab = 'banned'; populateBannedIPs();"
ng-class="{'tab-active': activeTab === 'banned'}"
class="tab-button">
<i class="fas fa-ban"></i>
@@ -1102,8 +1102,8 @@
</div>
<!-- Banned IPs List -->
<div class="banned-list-section">
<table class="banned-table" ng-if="bannedIPs.length > 0">
<div class="banned-list-section" ng-init="activeTab === 'banned' && populateBannedIPs()">
<table class="banned-table" ng-if="bannedIPs && bannedIPs.length > 0">
<thead>
<tr>
<th>{% trans "IP Address" %}</th>
@@ -1121,16 +1121,16 @@
{$ bannedIP.ip $}
</td>
<td class="reason">
<span class="reason-text">{$ bannedIP.reason $}</span>
<span class="reason-text ng-binding">{$ bannedIP.reason $}</span>
</td>
<td class="banned-date">
<td class="banned-date ng-binding">
<i class="fas fa-calendar"></i>
{$ bannedIP.banned_on | date:'MMM dd, yyyy HH:mm' $}
{$ bannedIP.banned_on $}
</td>
<td class="expires-date">
<i class="fas fa-clock"></i>
<span ng-if="bannedIP.expires === 'Never'">{% trans "Never" %}</span>
<span ng-if="bannedIP.expires !== 'Never'">{$ bannedIP.expires | date:'MMM dd, yyyy HH:mm' $}</span>
<span ng-if="bannedIP.expires !== 'Never'">{$ bannedIP.expires $}</span>
</td>
<td class="status">
<span ng-class="{'status-active': bannedIP.active, 'status-expired': !bannedIP.active}"
@@ -1160,7 +1160,7 @@
</table>
<!-- Empty State -->
<div ng-if="bannedIPs.length == 0" class="empty-state">
<div ng-if="!bannedIPs || bannedIPs.length == 0" class="empty-state">
<i class="fas fa-shield-check empty-icon"></i>
<h3 class="empty-title">{% trans "No Banned IPs" %}</h3>
<p class="empty-text">{% trans "All IP addresses are currently allowed. Add banned IPs to block suspicious or malicious traffic." %}</p>
@@ -1169,17 +1169,17 @@
<!-- Messages -->
<div style="padding: 0 2rem 2rem;">
<div ng-hide="bannedIPActionFailed" class="alert alert-danger">
<div ng-show="bannedIPActionFailed === false" class="alert alert-danger">
<i class="fas fa-exclamation-circle alert-icon"></i>
<span>{% trans "Action failed. Error message:" %} {$ bannedIPErrorMessage $}</span>
</div>
<div ng-hide="bannedIPActionSuccess" class="alert alert-success">
<div ng-show="bannedIPActionSuccess === false" class="alert alert-success">
<i class="fas fa-check-circle alert-icon"></i>
<span>{% trans "Action completed successfully." %}</span>
</div>
<div ng-hide="bannedIPCouldNotConnect" class="alert alert-danger">
<div ng-show="bannedIPCouldNotConnect === false" class="alert alert-danger">
<i class="fas fa-exclamation-circle alert-icon"></i>
<span>{% trans "Could not connect to server. Please refresh this page." %}</span>
</div>

View File

@@ -17,7 +17,7 @@ except:
import plogical.CyberCPLogFileWriter as logging
try:
from loginSystem.views import loadLoginPage
from websiteFunctions.models import Websites
from websiteFunctions.models import Websites, ChildDomains
from plogical.ftpUtilities import FTPUtilities
from plogical.acl import ACLManager
except:
@@ -102,13 +102,14 @@ class FTPManager:
result = FTPUtilities.submitFTPCreation(domainName, userName, password, path, admin.userName, api, customQuotaSize, enableCustomQuota)
if result[0] == 1:
data_ret = {'status': 1, 'creatFTPStatus': 1, 'error_message': 'None'}
created_username = (admin.userName + "_" + userName) if api == '0' else userName
data_ret = {'status': 1, 'creatFTPStatus': 1, 'error_message': 'None', 'createdFTPUsername': created_username}
json_data = json.dumps(data_ret)
return HttpResponse(json_data)
return HttpResponse(json_data, content_type='application/json')
else:
data_ret = {'status': 0, 'creatFTPStatus': 0, 'error_message': result[1]}
json_data = json.dumps(data_ret)
return HttpResponse(json_data)
return HttpResponse(json_data, content_type='application/json')
except BaseException as msg:
# Enhanced error handling with better user feedback
@@ -135,7 +136,7 @@ class FTPManager:
data_ret = {'status': 0, 'creatFTPStatus': 0, 'error_message': error_message}
json_data = json.dumps(data_ret)
return HttpResponse(json_data)
return HttpResponse(json_data, content_type='application/json')
def deleteFTPAccount(self):
userID = self.request.session['userID']
@@ -161,23 +162,30 @@ class FTPManager:
return ACLManager.loadErrorJson('fetchStatus', 0)
data = json.loads(self.request.body)
domain = data['ftpDomain']
domain = data.get('ftpDomain') or data.get('selectedDomain', '')
if not domain or not str(domain).strip():
return HttpResponse(json.dumps({'fetchStatus': 0, 'error_message': 'No domain selected'}), content_type='application/json')
admin = Administrator.objects.get(pk=userID)
if ACLManager.checkOwnership(domain, admin, currentACL) == 1:
pass
else:
return ACLManager.loadErrorJson()
return ACLManager.loadErrorJson('fetchStatus', 0)
website = Websites.objects.get(domain=domain)
try:
child = ChildDomains.objects.get(domain=domain)
website = child.master
except ChildDomains.DoesNotExist:
website = Websites.objects.get(domain=domain)
ftpAccounts = website.users_set.all()
ftpAccounts = website.users_set.values_list('user', flat=True)
json_data = "["
checker = 0
for items in ftpAccounts:
dic = {"userName": items.user}
for userName in ftpAccounts:
dic = {"userName": userName}
if checker == 0:
json_data = json_data + json.dumps(dic)
@@ -187,12 +195,12 @@ class FTPManager:
json_data = json_data + ']'
final_json = json.dumps({'fetchStatus': 1, 'error_message': "None", "data": json_data})
return HttpResponse(final_json)
return HttpResponse(final_json, content_type='application/json')
except BaseException as msg:
data_ret = {'fetchStatus': 0, 'error_message': str(msg)}
json_data = json.dumps(data_ret)
return HttpResponse(json_data)
return HttpResponse(json_data, content_type='application/json')
def submitFTPDelete(self):
try:
@@ -808,9 +816,15 @@ class FTPManager:
logging.CyberCPLogFileWriter.statusWriter(self.extraArgs['tempStatusPath'], 'Completed [200].')
except BaseException as msg:
final_dic = {'status': 0, 'error_message': str(msg)}
final_json = json.dumps(final_dic)
return HttpResponse(final_json)
err_msg = str(msg)
try:
logging.CyberCPLogFileWriter.statusWriter(
self.extraArgs['tempStatusPath'],
'[ERROR] %s [404].' % err_msg
)
except Exception:
pass
return 0
def main():

View File

@@ -18,21 +18,32 @@ app.controller('createFTPAccount', function ($scope, $http) {
$( ".ftpDetails" ).hide();
$( ".ftpPasswordView" ).hide();
// Check if select2 is available
if ($.fn.select2) {
$('.create-ftp-acct-select').select2();
$('.create-ftp-acct-select').on('select2:select', function (e) {
var data = e.params.data;
$scope.ftpDomain = data.text;
$( ".ftpDetails" ).show();
});
// Only use select2 if it's actually a function (avoids errors when Rocket Loader defers scripts)
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.$apply();
$(".ftpDetails").show();
});
} else {
initNativeSelect();
}
} catch (err) {
initNativeSelect();
}
} else {
// Fallback for regular select
$('.create-ftp-acct-select').on('change', function (e) {
initNativeSelect();
}
function initNativeSelect() {
$('.create-ftp-acct-select').off('select2:select').on('change', function () {
$scope.ftpDomain = $(this).val();
$scope.$apply();
$( ".ftpDetails" ).show();
$(".ftpDetails").show();
});
}
});
@@ -108,7 +119,8 @@ app.controller('createFTPAccount', function ($scope, $http) {
ftpDomain: ftpDomain,
ftpUserName: ftpUserName,
passwordByPass: ftpPassword,
path: path,
path: path || '',
api: '0',
enableCustomQuota: $scope.enableCustomQuota || false,
customQuotaSize: $scope.customQuotaSize || 0,
};
@@ -123,52 +135,36 @@ app.controller('createFTPAccount', function ($scope, $http) {
function ListInitialDatas(response) {
if (response.data.creatFTPStatus === 1) {
if (response.data && response.data.creatFTPStatus === 1) {
$scope.ftpLoading = false; // Hide loading on success
$scope.successfullyCreatedFTP = false;
$scope.canNotCreateFTP = true;
$scope.couldNotConnect = true;
$scope.createdFTPUsername = ftpDomain + "_" + ftpUserName;
// Also show PNotify if available
$scope.createdFTPUsername = (response.data.createdFTPUsername != null && response.data.createdFTPUsername !== '') ? response.data.createdFTPUsername : (ftpDomain + '_' + ftpUserName);
if (typeof PNotify !== 'undefined') {
new PNotify({
title: 'Success!',
text: 'FTP account successfully created.',
type: 'success'
});
new PNotify({ title: 'Success!', text: 'FTP account successfully created.', type: 'success' });
}
} else {
$scope.ftpLoading = false; // Hide loading on error
$scope.ftpLoading = false;
$scope.canNotCreateFTP = false;
$scope.successfullyCreatedFTP = true;
$scope.couldNotConnect = true;
$scope.errorMessage = response.data.error_message;
// Also show PNotify if available
$scope.errorMessage = (response.data && response.data.error_message) ? response.data.error_message : 'Unknown error';
if (typeof PNotify !== 'undefined') {
new PNotify({
title: 'Operation Failed!',
text: response.data.error_message,
type: 'error'
});
new PNotify({ title: 'Operation Failed!', text: $scope.errorMessage, type: 'error' });
}
}
}
function cantLoadInitialDatas(response) {
$scope.ftpLoading = false; // Hide loading on connection error
$scope.couldNotConnect = false;
$scope.canNotCreateFTP = true;
$scope.successfullyCreatedFTP = true;
// Also show PNotify if available
if (typeof PNotify !== 'undefined') {
new PNotify({
title: 'Operation Failed!',
text: 'Could not connect to server, please refresh this page',
type: 'error'
});
$scope.ftpLoading = false;
if ($scope.successfullyCreatedFTP !== false) {
$scope.couldNotConnect = false;
$scope.canNotCreateFTP = true;
$scope.successfullyCreatedFTP = true;
if (typeof PNotify !== 'undefined') {
new PNotify({ title: 'Operation Failed!', text: 'Could not connect to server, please refresh this page', type: 'error' });
}
}
}
@@ -260,32 +256,24 @@ app.controller('deleteFTPAccount', function ($scope, $http) {
} else {
$scope.ftpAccountsOfDomain = true;
$scope.deleteFTPButton = true;
$scope.deleteFailure = true;
$scope.deleteFailure = false;
$scope.deleteSuccess = true;
$scope.couldNotConnect = false;
$scope.couldNotConnect = true;
$scope.deleteFTPButtonInit = true;
$scope.errorMessage = (response.data && (response.data.error_message || response.data.errorMessage)) || 'Unknown error';
}
}
function cantLoadInitialDatas(response) {
$scope.ftpAccountsOfDomain = true;
$scope.deleteFTPButton = true;
$scope.deleteFailure = true;
$scope.deleteSuccess = true;
$scope.couldNotConnect = false;
$scope.deleteFTPButtonInit = true;
}
};
$scope.deleteFTPAccount = function () {
@@ -370,7 +358,7 @@ app.controller('deleteFTPAccount', function ($scope, $http) {
/* Java script code to delete ftp account ends here */
app.controller('listFTPAccounts', function ($scope, $http, ) {
app.controller('listFTPAccounts', function ($scope, $http) {
$scope.recordsFetched = true;
$scope.passwordChanged = true;
@@ -639,151 +627,117 @@ app.controller('listFTPAccounts', function ($scope, $http, ) {
app.controller('Resetftpconf', function ($scope, $http, $timeout){
app.controller('Resetftpconf', function ($scope, $http, $timeout, $window){
$scope.Loading = true;
$scope.NotifyBox = true;
$scope.InstallBox = true;
$scope.installationDetailsForm = false;
$scope.alertType = '';
$scope.errorMessage = '';
$scope.resetftp = function () {
$scope.Loading = false;
$scope.installationDetailsForm = true;
$scope.InstallBox = false;
$scope.alertType = '';
$scope.NotifyBox = true;
url = "/ftp/resetftpnow";
var data = {
};
var url = "/ftp/resetftpnow";
var data = {};
var config = {
headers: {
'X-CSRFToken': getCookie('csrftoken')
'X-CSRFToken': getCookie('csrftoken'),
'Content-Type': 'application/json'
}
};
$http.post(url, data, config).then(ListInitialData, cantLoadInitialData);
function ListInitialData(response) {
if (response.data.status === 1) {
function ListInitialData(response) {
if (response.data && response.data.status === 1) {
$scope.NotifyBox = true;
$scope.InstallBox = false;
$scope.Loading = false;
$scope.failedToStartInallation = true;
$scope.couldNotConnect = true;
$scope.modSecSuccessfullyInstalled = true;
$scope.installationFailed = true;
$scope.statusfile = response.data.tempStatusPath
$scope.alertType = '';
$scope.statusfile = response.data.tempStatusPath;
$timeout(getRequestStatus, 1000);
} else {
$scope.errorMessage = response.data.error_message;
$scope.errorMessage = (response.data && (response.data.error_message || response.data.errorMessage)) || 'Unknown error';
$scope.alertType = 'failedToStart';
$scope.NotifyBox = false;
$scope.InstallBox = true;
$scope.Loading = true;
$scope.failedToStartInallation = false;
$scope.couldNotConnect = true;
$scope.modSecSuccessfullyInstalled = true;
$scope.Loading = false;
}
}
function cantLoadInitialData(response) {
$scope.cyberhosting = true;
new PNotify({
title: 'Error!',
text: 'Could not connect to server, please refresh this page.',
type: 'error'
});
$scope.errorMessage = (response && response.data && (response.data.error_message || response.data.errorMessage)) || 'Could not connect to server. Please refresh this page.';
$scope.alertType = 'couldNotConnect';
$scope.NotifyBox = false;
$scope.InstallBox = true;
$scope.Loading = false;
try {
new PNotify({ title: 'Error!', text: $scope.errorMessage, type: 'error' });
} catch (e) {}
}
}
var statusPollPromise = null;
function getRequestStatus() {
$scope.NotifyBox = true;
$scope.InstallBox = false;
$scope.Loading = false;
$scope.failedToStartInallation = true;
$scope.couldNotConnect = true;
$scope.modSecSuccessfullyInstalled = true;
$scope.installationFailed = true;
url = "/ftp/getresetstatus";
var data = {
statusfile: $scope.statusfile
};
$scope.alertType = '';
var url = "/ftp/getresetstatus";
var data = { statusfile: $scope.statusfile };
var config = {
headers: {
'X-CSRFToken': getCookie('csrftoken')
'X-CSRFToken': getCookie('csrftoken'),
'Content-Type': 'application/json'
}
};
$http.post(url, data, config).then(ListInitialDatas, cantLoadInitialDatas);
function ListInitialDatas(response) {
if (!response.data) return;
if (response.data.abort === 0) {
$scope.NotifyBox = true;
$scope.InstallBox = false;
$scope.Loading = false;
$scope.failedToStartInallation = true;
$scope.couldNotConnect = true;
$scope.modSecSuccessfullyInstalled = true;
$scope.installationFailed = true;
$scope.requestData = response.data.requestStatus;
$timeout(getRequestStatus, 1000);
$scope.alertType = '';
$scope.requestData = response.data.requestStatus || '';
statusPollPromise = $timeout(getRequestStatus, 1000);
} else {
// Notifications
$timeout.cancel();
if (statusPollPromise) {
$timeout.cancel(statusPollPromise);
statusPollPromise = null;
}
$scope.NotifyBox = false;
$scope.InstallBox = false;
$scope.Loading = true;
$scope.failedToStartInallation = true;
$scope.couldNotConnect = true;
$scope.requestData = response.data.requestStatus;
$scope.Loading = false;
$scope.requestData = response.data.requestStatus || '';
if (response.data.installed === 0) {
$scope.installationFailed = false;
$scope.errorMessage = response.data.error_message;
$scope.alertType = 'resetFailed';
$scope.errorMessage = response.data.error_message || 'Reset failed';
} else {
$scope.modSecSuccessfullyInstalled = false;
$timeout(function () {
$window.location.reload();
}, 3000);
$scope.alertType = 'success';
$timeout(function () { $window.location.reload(); }, 3000);
}
}
}
function cantLoadInitialDatas(response) {
if (statusPollPromise) {
$timeout.cancel(statusPollPromise);
statusPollPromise = null;
}
$scope.alertType = 'couldNotConnect';
$scope.errorMessage = (response && response.data && (response.data.error_message || response.data.errorMessage)) || 'Could not connect to server. Please refresh this page.';
$scope.NotifyBox = false;
$scope.InstallBox = false;
$scope.Loading = true;
$scope.failedToStartInallation = true;
$scope.couldNotConnect = false;
$scope.modSecSuccessfullyInstalled = true;
$scope.installationFailed = true;
$scope.InstallBox = true;
$scope.Loading = false;
}
}
});

View File

@@ -308,24 +308,22 @@
</button>
</div>
<!-- Alert Messages -->
<div ng-hide="NotifyBox">
<div ng-hide="failedToStartInallation" class="alert alert-danger">
<!-- Alert Messages (only one shown at a time) -->
<div ng-show="!NotifyBox && alertType">
<div ng-if="alertType === 'failedToStart'" class="alert alert-danger">
<i class="fas fa-exclamation-circle"></i>
{% trans "Failed to start reset process. Error message:" %} {$ errorMessage $}
{% trans "Failed to start reset process. Error message:" %} <span ng-bind="errorMessage || 'Unknown error'"></span>
</div>
<div ng-hide="couldNotConnect" class="alert alert-danger">
<div ng-if="alertType === 'couldNotConnect'" class="alert alert-danger">
<i class="fas fa-times-circle"></i>
{% trans "Could not connect. Please refresh this page." %}
<span ng-if="errorMessage" ng-bind="errorMessage"></span>
</div>
<div ng-hide="installationFailed" class="alert alert-danger">
<div ng-if="alertType === 'resetFailed'" class="alert alert-danger">
<i class="fas fa-exclamation-circle"></i>
{% trans "Reset process failed." %} {$ errorMessage $}
{% trans "Reset process failed." %} <span ng-bind="errorMessage || 'Unknown error'"></span>
</div>
<div ng-hide="modSecSuccessfullyInstalled" class="alert alert-success">
<div ng-if="alertType === 'success'" class="alert alert-success">
<i class="fas fa-check-circle"></i>
{% trans "Successfully completed reset process." %}
</div>

View File

@@ -575,7 +575,7 @@
</div>
<div style="margin-top: 2rem;">
<button type="button" ng-click="createFTPAccount()" class="btn-primary">
<button type="button" ng-click="createFTPAccount()" ng-disabled="ftpLoading" class="btn-primary">
<i class="fas fa-plus-circle"></i>
{% trans "Create FTP Account" %}
</button>

View File

@@ -18,4 +18,7 @@ urlpatterns = [
path('updateFTPQuota', views.updateFTPQuota, name='updateFTPQuota'),
path('getFTPQuotaUsage', views.getFTPQuotaUsage, name='getFTPQuotaUsage'),
path('migrateFTPQuotas', views.migrateFTPQuotas, name='migrateFTPQuotas'),
# FTP Quota Management page (at /ftp/quotaManagement to avoid websites/<domain> catch-all)
path('quotaManagement', views.ftpQuotaManagementPage, name='ftpQuotaManagementPage'),
]

View File

@@ -1,5 +1,6 @@
# -*- coding: utf-8 -*-
import json
import os
import time
from random import randint
@@ -20,6 +21,15 @@ def loadFTPHome(request):
except KeyError:
return redirect(loadLoginPage)
def ftpQuotaManagementPage(request):
"""Render the FTP Quota Management page (served from /ftp/ to avoid websites/<domain> conflict)."""
try:
proc = httpProc(request, 'websiteFunctions/ftpQuotaManagement.html', {}, 'admin')
return proc.render()
except KeyError:
return redirect(loadLoginPage)
def createFTPAccount(request):
try:
@@ -113,29 +123,43 @@ def ResetFTPConfigurations(request):
def resetftpnow(request):
try:
from plogical.virtualHostUtilities import virtualHostUtilities
userID = request.session['userID']
currentACL = ACLManager.loadedACL(userID)
if currentACL['admin'] == 1:
pass
else:
if currentACL['admin'] != 1:
return ACLManager.loadErrorJson('FilemanagerAdmin', 0)
data = json.loads(request.body)
tempStatusPath = "/home/cyberpanel/" + str(randint(1000, 9999))
execPath = f"/usr/local/CyberCP/bin/python /usr/local/CyberCP/ftp/ftpManager.py ResetFTPConfigurations --tempStatusPath {tempStatusPath}"
try:
body = request.body
if isinstance(body, bytes):
body = body.decode('utf-8') if body else '{}'
data = json.loads(body) if body and body.strip() else {}
except (json.JSONDecodeError, ValueError):
data = {}
tempStatusPath = os.path.join('/tmp', 'cyberpanel_ftp_reset_' + str(randint(10000, 99999)))
try:
with open(tempStatusPath, 'w') as f:
f.write("Starting FTP reset...,0\n")
except OSError as e:
data_ret = {'status': 0, 'error_message': 'Cannot create status file: ' + str(e), 'tempStatusPath': ''}
return HttpResponse(json.dumps(data_ret), content_type='application/json')
python_path = '/usr/local/CyberCP/bin/python'
if not os.path.exists(python_path):
for p in ('/usr/bin/python3', '/usr/bin/python'):
if os.path.exists(p):
python_path = p
break
else:
python_path = 'python3'
execPath = f"{python_path} /usr/local/CyberCP/ftp/ftpManager.py ResetFTPConfigurations --tempStatusPath {tempStatusPath}"
ProcessUtilities.popenExecutioner(execPath)
time.sleep(2)
data_ret = {'status': 1, 'error_message': "None",
'tempStatusPath': tempStatusPath}
data_ret = {'status': 1, 'error_message': "None", 'tempStatusPath': tempStatusPath}
json_data = json.dumps(data_ret)
return HttpResponse(json_data)
return HttpResponse(json_data, content_type='application/json')
except KeyError:
return redirect(loadLoginPage)
@@ -152,42 +176,56 @@ def getresetstatus(request):
else:
return ACLManager.loadErrorJson('FilemanagerAdmin', 0)
data = json.loads(request.body)
statusfile = data['statusfile']
installStatus = ProcessUtilities.outputExecutioner("sudo cat " + statusfile)
try:
body = request.body
if isinstance(body, bytes):
body = body.decode('utf-8') if body else '{}'
data = json.loads(body) if body and body.strip() else {}
except (json.JSONDecodeError, ValueError, TypeError):
data = {}
statusfile = data.get('statusfile', '')
if not statusfile:
return HttpResponse(json.dumps({'abort': 1, 'installed': 0, 'error_message': 'Missing status file', 'requestStatus': ''}), content_type='application/json')
result = ProcessUtilities.outputExecutioner("cat " + statusfile)
if result is None:
installStatus = ""
elif isinstance(result, tuple):
installStatus = result[1] if len(result) > 1 else ""
else:
installStatus = str(result) if result else ""
if installStatus.find("[200]") > -1:
command = 'sudo rm -f ' + statusfile
command = 'rm -f ' + statusfile
ProcessUtilities.executioner(command)
final_json = json.dumps({
return HttpResponse(json.dumps({
'error_message': "None",
'requestStatus': installStatus,
'abort': 1,
'installed': 1,
})
return HttpResponse(final_json)
}), content_type='application/json')
elif installStatus.find("[404]") > -1:
command = 'sudo rm -f ' + statusfile
command = 'rm -f ' + statusfile
ProcessUtilities.executioner(command)
err_msg = installStatus.replace('[404]', '').strip() if installStatus else 'Reset failed'
if not err_msg or err_msg == ',':
err_msg = 'FTP configuration reset failed'
final_json = json.dumps({
'abort': 1,
'installed': 0,
'error_message': "None",
'error_message': err_msg,
'requestStatus': installStatus,
})
return HttpResponse(final_json)
return HttpResponse(final_json, content_type='application/json')
else:
final_json = json.dumps({
return HttpResponse(json.dumps({
'abort': 0,
'error_message': "None",
'requestStatus': installStatus,
})
return HttpResponse(final_json)
'requestStatus': installStatus or '',
}), content_type='application/json')
except KeyError:
return redirect(loadLoginPage)

View File

@@ -131,9 +131,10 @@ class FTPUtilities:
## need to get gid and uid
try:
website = ChildDomains.objects.get(domain=domainName)
externalApp = website.master.externalApp
except:
child = ChildDomains.objects.get(domain=domainName)
website = child.master
externalApp = child.master.externalApp
except ChildDomains.DoesNotExist:
website = Websites.objects.get(domain=domainName)
externalApp = website.externalApp

View File

@@ -141,22 +141,24 @@ class modSec:
writeToFile.writelines("ModSecurity Installed.[200]\n")
writeToFile.close()
# Check if custom OLS binary is installed - if so, replace with compatible ModSecurity
custom_ols_marker = "/usr/local/lsws/modules/cyberpanel_ols.so"
if os.path.exists(custom_ols_marker):
writeToFile = open(modSec.installLogPath, 'a')
writeToFile.writelines("Custom OLS detected, installing compatible ModSecurity...\n")
writeToFile.close()
# Always download and install compatible ModSecurity binary to prevent LMDB dependency crashes
# This fixes the "undefined symbol: mdb_env_create" error that causes OpenLiteSpeed to crash
writeToFile = open(modSec.installLogPath, 'a')
writeToFile.writelines("Downloading compatible ModSecurity binary to prevent LMDB dependency issues...\n")
writeToFile.close()
platform = modSec.detectPlatform()
if modSec.downloadCompatibleModSec(platform):
writeToFile = open(modSec.installLogPath, 'a')
writeToFile.writelines("Compatible ModSecurity installed successfully.\n")
writeToFile.close()
else:
writeToFile = open(modSec.installLogPath, 'a')
writeToFile.writelines("WARNING: Could not install compatible ModSecurity. May experience crashes.\n")
writeToFile.close()
platform = modSec.detectPlatform()
if modSec.downloadCompatibleModSec(platform):
writeToFile = open(modSec.installLogPath, 'a')
writeToFile.writelines("Compatible ModSecurity binary installed successfully.\n")
writeToFile.close()
logging.CyberCPLogFileWriter.writeToFile("Compatible ModSecurity binary installed to prevent LMDB dependency crashes [installModSec]")
else:
writeToFile = open(modSec.installLogPath, 'a')
writeToFile.writelines("WARNING: Could not install compatible ModSecurity binary. Using package-manager binary instead.\n")
writeToFile.writelines("WARNING: If you experience crashes (SIGSEGV signal 11), manually download compatible binary.\n")
writeToFile.close()
logging.CyberCPLogFileWriter.writeToFile("WARNING: Could not install compatible ModSecurity binary - may experience LMDB dependency crashes [installModSec]")
return 1
except BaseException as msg:

View File

@@ -8,27 +8,64 @@ app.controller('createFTPAccount', function ($scope, $http) {
$scope.ftpLoading = false;
$scope.ftpDetails = true;
$scope.canNotCreateFTP = true;
$scope.successfullyCreatedFTP = true;
$scope.couldNotConnect = true;
$scope.generatedPasswordView = true;
$(document).ready(function () {
$( ".ftpDetails" ).hide();
$( ".ftpPasswordView" ).hide();
$('.create-ftp-acct-select').select2();
$(".ftpDetails").hide();
$(".ftpPasswordView").hide();
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").show();
});
} else {
initNativeSelect();
}
} catch (err) {
initNativeSelect();
}
} else {
initNativeSelect();
}
function initNativeSelect() {
$('.create-ftp-acct-select').off('select2:select').on('change', function () {
$scope.ftpDomain = $(this).val();
$scope.ftpDetails = !($scope.ftpDomain && $scope.ftpDomain !== "");
$scope.$apply();
if ($scope.ftpDomain && $scope.ftpDomain !== "") $(".ftpDetails").show();
else $(".ftpDetails").hide();
});
}
});
$('.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 !== "") {
$(".ftpDetails").show();
$scope.ftpDetails = false;
} else {
$(".ftpDetails").hide();
$scope.ftpDetails = true;
}
};
$scope.createFTPAccount = function () {
$scope.ftpLoading = false;
$scope.ftpLoading = true;
$scope.ftpDetails = false;
$scope.canNotCreate = true;
$scope.successfullyCreated = true;
$scope.canNotCreateFTP = true;
$scope.successfullyCreatedFTP = true;
$scope.couldNotConnect = true;
var ftpDomain = $scope.ftpDomain;
@@ -36,19 +73,18 @@ app.controller('createFTPAccount', function ($scope, $http) {
var ftpPassword = $scope.ftpPassword;
var path = $scope.ftpPath;
if (typeof path === 'undefined') {
path = "";
}
var url = "/ftp/submitFTPCreation";
if (typeof path === 'undefined' || path == null) path = "";
else path = String(path).trim();
var data = {
ftpDomain: ftpDomain,
ftpUserName: ftpUserName,
passwordByPass: ftpPassword,
path: path,
enableCustomQuota: $scope.enableCustomQuota || false,
customQuotaSize: $scope.customQuotaSize || 0,
};
var url = "/ftp/submitFTPCreation";
var config = {
headers: {
@@ -63,57 +99,60 @@ app.controller('createFTPAccount', function ($scope, $http) {
if (response.data.creatFTPStatus === 1) {
$scope.ftpLoading = true;
new PNotify({
title: 'Success!',
text: 'FTP account successfully created.',
type: 'success'
});
$scope.ftpLoading = false;
$scope.successfullyCreatedFTP = false;
$scope.canNotCreateFTP = true;
$scope.couldNotConnect = true;
$scope.createdFTPUsername = ftpDomain + "_" + ftpUserName;
if (typeof PNotify !== 'undefined') {
new PNotify({ title: 'Success!', text: 'FTP account successfully created.', type: 'success' });
}
} else {
$scope.ftpLoading = true;
new PNotify({
title: 'Operation Failed!',
text: response.data.error_message,
type: 'error'
});
$scope.ftpLoading = false;
$scope.canNotCreateFTP = false;
$scope.successfullyCreatedFTP = true;
$scope.couldNotConnect = true;
$scope.errorMessage = response.data.error_message;
if (typeof PNotify !== 'undefined') {
new PNotify({ title: 'Operation Failed!', text: response.data.error_message, type: 'error' });
}
}
}
function cantLoadInitialDatas(response) {
$scope.ftpLoading = true;
new PNotify({
title: 'Operation Failed!',
text: 'Could not connect to server, please refresh this page',
type: 'error'
});
$scope.ftpLoading = false;
$scope.couldNotConnect = false;
$scope.canNotCreateFTP = true;
$scope.successfullyCreatedFTP = true;
if (typeof PNotify !== 'undefined') {
new PNotify({ title: 'Operation Failed!', text: 'Could not connect to server, please refresh this page', type: 'error' });
}
}
};
$scope.hideFewDetails = function () {
$scope.successfullyCreated = true;
$scope.successfullyCreatedFTP = true;
$scope.canNotCreateFTP = true;
$scope.couldNotConnect = true;
};
///
$scope.generatePassword = function () {
$( ".ftpPasswordView" ).show();
$(".ftpPasswordView").show();
$scope.generatedPasswordView = false;
$scope.ftpPassword = randomPassword(16);
};
$scope.usePassword = function () {
$(".ftpPasswordView" ).hide();
$(".ftpPasswordView").hide();
$scope.generatedPasswordView = true;
};
$scope.toggleCustomQuota = function () {
if (!$scope.enableCustomQuota) $scope.customQuotaSize = 0;
};
});

View File

@@ -4,7 +4,7 @@
<type>Utility</type>
<version>1.0.0</version>
<description>A comprehensive test plugin for CyberPanel with enable/disable functionality, test button, popup messages, and inline integration</description>
<author>CyberPanel Development Team</author>
<author>usmannasir</author>
<website>https://github.com/cyberpanel/testPlugin</website>
<license>MIT</license>
<dependencies>

View File

@@ -112,22 +112,35 @@ function enableFTPQuota() {
'csrfmiddlewaretoken': '{{ csrf_token }}'
}, function(data) {
if (data.status === 1) {
showNotification('success', data.message);
showNotification('success', (data && (data.message || data.error_message)) || 'Success');
refreshQuotas();
} else {
showNotification('error', data.message);
showNotification('error', (data && (data.error_message || data.message)) || 'Unknown error');
}
});
}
function refreshQuotas() {
$.post('{% url "getFTPQuotas" %}', {
'csrfmiddlewaretoken': '{{ csrf_token }}'
}, function(data) {
if (data.status === 1) {
displayQuotas(data.quotas);
} else {
showNotification('error', data.message);
var tbody = document.getElementById('quotasTableBody');
if (tbody) tbody.innerHTML = '<tr><td colspan="9" class="text-center"><i class="fas fa-spinner fa-spin"></i> Loading...</td></tr>';
$.ajax({
url: '{% url "getFTPQuotas" %}',
type: 'POST',
data: { 'csrfmiddlewaretoken': '{{ csrf_token }}' },
dataType: 'json',
success: function(data) {
if (data && data.status === 1) {
displayQuotas(data.quotas || []);
} else {
var msg = (data && (data.error_message || data.message)) || 'Unknown error';
showNotification('error', msg);
if (tbody) tbody.innerHTML = '<tr><td colspan="9" class="text-center text-danger">' + msg + '</td></tr>';
}
},
error: function(xhr) {
var msg = (xhr.responseJSON && (xhr.responseJSON.error_message || xhr.responseJSON.message)) || (xhr.statusText || 'Request failed');
showNotification('error', msg);
if (tbody) tbody.innerHTML = '<tr><td colspan="9" class="text-center text-danger">' + msg + '</td></tr>';
}
});
}
@@ -190,11 +203,11 @@ function saveQuota() {
$.post('{% url "updateFTPQuota" %}', formData, function(data) {
if (data.status === 1) {
showNotification('success', data.message);
showNotification('success', (data && (data.message || data.error_message)) || 'Success');
$('#editQuotaModal').modal('hide');
refreshQuotas();
} else {
showNotification('error', data.message);
showNotification('error', (data && (data.error_message || data.message)) || 'Unknown error');
}
});
}

View File

@@ -201,31 +201,33 @@ urlpatterns = [
path('resetVHostConfigToDefault', views.resetVHostConfigToDefault, name='resetVHostConfigToDefault'),
path('getTerminalJWT', views.get_terminal_jwt, name='get_terminal_jwt'),
# Catch all for domains
path('<domain>/<childDomain>', views.launchChild, name='launchChild'),
path('<domain>', views.domain, name='domain'),
path('get_website_resources/', views.get_website_resources, name='get_website_resources'),
# Subdomain Log Fix
path('fixSubdomainLogs', views.fixSubdomainLogs, name='fixSubdomainLogs'),
path('fixSubdomainLogsAction', views.fixSubdomainLogsAction, name='fixSubdomainLogsAction'),
# FTP Quota Management
# FTP Quota Management (API endpoints only; page is at /ftp/quotaManagement)
path('enableFTPQuota', views.enableFTPQuota, name='enableFTPQuota'),
path('getFTPQuotas', views.getFTPQuotas, name='getFTPQuotas'),
path('updateFTPQuota', views.updateFTPQuota, name='updateFTPQuota'),
# Bandwidth Management
path('bandwidthManagement', views.bandwidthManagementPage, name='bandwidthManagementPage'),
path('resetBandwidth', views.resetBandwidth, name='resetBandwidth'),
path('getBandwidthResetLogs', views.getBandwidthResetLogs, name='getBandwidthResetLogs'),
path('scheduleBandwidthReset', views.scheduleBandwidthReset, name='scheduleBandwidthReset'),
# Security Management
path('securityManagement', views.securityManagementPage, name='securityManagementPage'),
# IP Blocking
path('blockIPAddress', views.blockIPAddress, name='blockIPAddress'),
path('unblockIPAddress', views.unblockIPAddress, name='unblockIPAddress'),
path('getBlockedIPs', views.getBlockedIPs, name='getBlockedIPs'),
path('checkIPStatus', views.checkIPStatus, name='checkIPStatus'),
# Catch all for domains (must be last)
path('<domain>/<childDomain>', views.launchChild, name='launchChild'),
path('<domain>', views.domain, name='domain'),
]

View File

@@ -2236,6 +2236,33 @@ def fixSubdomainLogsAction(request):
return redirect(loadLoginPage)
# FTP Quota Management Views
def ftpQuotaManagementPage(request):
"""Render the FTP Quota Management page."""
try:
userID = request.session['userID']
proc = httpProc(request, 'websiteFunctions/ftpQuotaManagement.html', {}, 'admin')
return proc.render()
except KeyError:
return redirect(loadLoginPage)
def bandwidthManagementPage(request):
"""Render the Bandwidth Management page."""
try:
userID = request.session['userID']
proc = httpProc(request, 'websiteFunctions/bandwidthManagement.html', {}, 'admin')
return proc.render()
except KeyError:
return redirect(loadLoginPage)
def securityManagementPage(request):
"""Render the Security Management page."""
try:
userID = request.session['userID']
proc = httpProc(request, 'websiteFunctions/securityManagement.html', {}, 'admin')
return proc.render()
except KeyError:
return redirect(loadLoginPage)
def enableFTPQuota(request):
try:
userID = request.session['userID']