Fix CloudFlare DNS Management: Filter main domains only, auto-delete DNS records on domain removal, improve table display

- Filter domain dropdown to show only main domains (exclude sub-domains)
- Add automatic CloudFlare DNS record deletion when domains/sub-domains are removed
- Improve DNS Records table display to match SSH Logins/Logs table styling
- Add loading states and proper table structure with ng-if conditions
- Update CSS to match activity-table styling with sticky headers
This commit is contained in:
master3395 2026-01-04 02:13:10 +01:00
parent d8dbe6e410
commit cfee3d9867
6 changed files with 264 additions and 24 deletions

View File

@ -649,7 +649,10 @@ class DNSManager:
if os.path.exists(cfPath):
CloudFlare = 1
domainsList = ACLManager.findAllDomains(currentACL, userID)
allDomains = ACLManager.findAllDomains(currentACL, userID)
# Filter to only show main domains (domains with exactly one dot, e.g., "example.com")
# Sub-domains have two or more dots (e.g., "subdomain.example.com")
domainsList = [domain for domain in allDomains if domain.count('.') == 1]
self.admin = admin
self.loadCFKeys()
data = {"domainsList": domainsList, "status": status, 'CloudFlare': CloudFlare, 'cfEmail': self.email,

View File

@ -807,10 +807,12 @@ app.controller('addModifyDNSRecordsCloudFlare', function ($scope, $http, $window
$scope.recordAdded = true;
$scope.couldNotConnect = true;
$scope.recordsLoading = true;
$scope.loadingRecords = true;
$scope.recordDeleted = true;
$scope.couldNotDeleteRecords = true;
$scope.couldNotAddRecord = true;
$scope.recordValueDefault = false;
$scope.records = [];
// Hide records boxes
$(".aaaaRecord").hide();
@ -981,7 +983,7 @@ app.controller('addModifyDNSRecordsCloudFlare', function ($scope, $http, $window
};
function populateCurrentRecords() {
$scope.loadingRecords = true;
var selectedZone = $scope.selectedZone;
url = "/dns/getCurrentRecordsForDomainCloudFlare";
@ -1002,6 +1004,7 @@ app.controller('addModifyDNSRecordsCloudFlare', function ($scope, $http, $window
function ListInitialDatas(response) {
$scope.loadingRecords = false;
if (response.data.fetchStatus === 1) {
$scope.records = JSON.parse(response.data.data);
@ -1028,6 +1031,7 @@ app.controller('addModifyDNSRecordsCloudFlare', function ($scope, $http, $window
$scope.couldNotConnect = true;
$scope.recordsLoading = true;
$scope.couldNotAddRecord = true;
$scope.records = [];
$scope.errorMessage = response.data.error_message;
}
@ -1035,7 +1039,7 @@ app.controller('addModifyDNSRecordsCloudFlare', function ($scope, $http, $window
}
function cantLoadInitialDatas(response) {
$scope.loadingRecords = false;
$scope.addRecordsBox = true;
$scope.currentRecords = true;
$scope.canNotFetchRecords = true;
@ -1044,6 +1048,7 @@ app.controller('addModifyDNSRecordsCloudFlare', function ($scope, $http, $window
$scope.recordAdded = true;
$scope.couldNotConnect = false;
$scope.couldNotAddRecord = true;
$scope.records = [];
}

View File

@ -310,32 +310,100 @@
overflow: hidden;
border: 1px solid var(--border-primary, #e8e9ff);
margin-top: 2rem;
display: table;
border-collapse: separate;
border-spacing: 0;
}
.records-table th,
.records-table td {
display: table-cell !important;
}
.records-table tr {
display: table-row !important;
}
.records-table thead {
background: var(--bg-secondary, #f8f9ff);
display: table-header-group !important;
background: linear-gradient(135deg, #5b5fcf 0%, #4a4fc7 100%);
}
.records-table tbody {
display: table-row-group !important;
}
.records-table th {
padding: 1rem;
text-align: left;
font-weight: 600;
color: var(--text-primary, #1e293b);
font-size: 0.875rem;
padding: 14px 12px;
font-size: 11px;
font-weight: 700;
color: #ffffff;
text-transform: uppercase;
letter-spacing: 0.05em;
border-bottom: 1px solid var(--border-primary, #e8e9ff);
letter-spacing: 0.8px;
border-bottom: 2px solid rgba(91, 95, 207, 0.3);
background: linear-gradient(135deg, #5b5fcf 0%, #4a4fc7 100%);
position: sticky;
top: 0;
z-index: 10;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
vertical-align: middle;
white-space: nowrap;
}
.records-table td {
padding: 1rem;
color: var(--text-secondary, #64748b);
font-size: 0.875rem;
border-bottom: 1px solid var(--border-light, #f3f4f6);
.records-table tbody tr {
border-bottom: 1px solid var(--border-color, #f0f0ff);
}
.records-table tbody tr:hover {
background: var(--bg-secondary, #f8f9ff);
background: var(--bg-hover, #f8f9ff);
}
.records-table td {
padding: 12px 12px;
font-size: 13px;
color: var(--text-primary, #2f3640);
border-bottom: 1px solid var(--border-color, #f0f0ff);
vertical-align: middle;
word-wrap: break-word;
overflow-wrap: break-word;
}
.records-table td:nth-child(1) {
min-width: 200px;
max-width: 300px;
}
.records-table td:nth-child(2) {
min-width: 80px;
max-width: 120px;
}
.records-table td:nth-child(3) {
min-width: 80px;
max-width: 120px;
}
.records-table td:nth-child(4) {
min-width: 150px;
max-width: 300px;
}
.records-table td:nth-child(5) {
min-width: 80px;
max-width: 120px;
}
.records-table td:nth-child(6) {
min-width: 80px;
max-width: 120px;
text-align: center;
}
.records-table td:nth-child(7) {
min-width: 80px;
max-width: 120px;
text-align: center;
}
.delete-icon {
@ -767,14 +835,21 @@
<!-- DNS Records Table -->
<div ng-hide="currentRecords" style="margin-top: 3rem;">
<div style="margin-top: 3rem;">
<h4 class="mb-3" style="color: var(--text-primary, #1e293b); font-weight: 600;">
<i class="fas fa-list"></i>
{% trans "DNS Records" %}
</h4>
<table class="records-table">
<div ng-if="loadingRecords" style="text-align: center; padding: 20px; color: #8893a7;">
Loading DNS records...
</div>
<div ng-if="!loadingRecords && records.length === 0" style="text-align: center; padding: 20px; color: #8893a7;">
No DNS records found.
</div>
<table class="records-table activity-table" ng-if="!loadingRecords && records.length > 0">
<thead>
<tr>
<th>{% trans "Name" %}</th>
@ -788,7 +863,7 @@
</thead>
<tbody>
<tr ng-repeat="record in records track by $index">
<td ng-bind="record.name"></td>
<td><strong ng-bind="record.name"></strong></td>
<td>
<span style="padding: 0.25rem 0.75rem; background: var(--bg-hover, #f0f1ff); color: #5b5fcf; border-radius: 4px; font-weight: 500; font-size: 0.75rem;">
{{ record.type }}
@ -806,18 +881,41 @@
</td>
<td style="text-align: center;">
<i class="fas fa-trash delete-icon"
style="color: #ef4444;"
style="color: #ef4444; cursor: pointer;"
ng-click="deleteRecord(record.id)"
title="{% trans 'Delete Record' %}"></i>
</td>
</tr>
</tbody>
</table>
<!-- Loading placeholder table -->
<table class="records-table activity-table" ng-if="loadingRecords">
<thead>
<tr>
<th>{% trans "Name" %}</th>
<th>{% trans "Type" %}</th>
<th>{% trans "TTL" %}</th>
<th>{% trans "Value" %}</th>
<th>{% trans "Priority" %}</th>
<th>{% trans "Proxy" %}</th>
<th style="text-align: center;">{% trans "Actions" %}</th>
</tr>
</thead>
<tbody>
<tr ng-repeat="i in [1,2,3,4,5]">
<td>Loading...</td>
<td>Loading...</td>
<td>Loading...</td>
<td>Loading...</td>
<td>Loading...</td>
<td>Loading...</td>
<td>Loading...</td>
</tr>
</tbody>
</table>
</div>
<!-- DNS Records Table -->
</div>

View File

@ -839,6 +839,102 @@ class DNS:
## There does not exist a zone for this domain.
pass
@staticmethod
def deleteCloudFlareDNSRecords(domainName, adminUserName=None):
"""
Delete all CloudFlare DNS records for a domain when domain is removed from CyberPanel.
This function is called automatically when domains/sub-domains are deleted.
"""
try:
# Check if CloudFlare is configured for this admin user
if adminUserName:
cfFile = '%s%s' % (DNS.CFPath, adminUserName)
else:
# Try to find admin user from domain
try:
from loginSystem.models import Administrator
from websiteFunctions.models import Websites, ChildDomains
try:
website = Websites.objects.get(domain=domainName)
adminUserName = website.admin.userName
except:
try:
childDomain = ChildDomains.objects.get(domain=domainName)
adminUserName = childDomain.master.admin.userName
except:
return 0, "Could not find admin user for domain"
cfFile = '%s%s' % (DNS.CFPath, adminUserName)
except:
return 0, "Could not determine admin user"
if not os.path.exists(cfFile):
# CloudFlare not configured for this user, skip deletion
return 1, "CloudFlare not configured"
# Load CloudFlare credentials
data = open(cfFile, 'r').readlines()
email = data[0].rstrip('\n')
token = data[1].rstrip('\n')
# Initialize CloudFlare API
cf = CloudFlare.CloudFlare(email=email, token=token)
try:
# Find the zone for this domain
params = {'name': domainName, 'per_page': 50}
zones = cf.zones.get(params=params)
for zone in sorted(zones, key=lambda v: v['name']):
if zone['name'] == domainName:
zone_id = zone['id']
# Get all DNS records for this zone
try:
dns_records = cf.zones.dns_records.get(zone_id)
# Delete all DNS records
deleted_count = 0
for record in dns_records:
try:
cf.zones.dns_records.delete(zone_id, record['id'])
deleted_count += 1
except Exception as e:
logging.CyberCPLogFileWriter.writeToFile(
f'Error deleting CloudFlare DNS record {record["id"]} for {domainName}: {str(e)}')
if deleted_count > 0:
logging.CyberCPLogFileWriter.writeToFile(
f'Deleted {deleted_count} CloudFlare DNS records for {domainName}')
return 1, f"Deleted {deleted_count} DNS records"
else:
return 1, "No DNS records found to delete"
except CloudFlare.exceptions.CloudFlareAPIError as e:
logging.CyberCPLogFileWriter.writeToFile(
f'CloudFlare API error deleting DNS records for {domainName}: {str(e)}')
return 0, str(e)
except Exception as e:
logging.CyberCPLogFileWriter.writeToFile(
f'Error getting CloudFlare DNS records for {domainName}: {str(e)}')
return 0, str(e)
# Zone not found in CloudFlare
return 1, "Domain not found in CloudFlare"
except CloudFlare.exceptions.CloudFlareAPIError as e:
logging.CyberCPLogFileWriter.writeToFile(
f'CloudFlare API error for {domainName}: {str(e)}')
return 0, str(e)
except Exception as e:
logging.CyberCPLogFileWriter.writeToFile(
f'Error deleting CloudFlare DNS records for {domainName}: {str(e)}')
return 0, str(e)
except BaseException as msg:
logging.CyberCPLogFileWriter.writeToFile(
f'Error in deleteCloudFlareDNSRecords for {domainName}: {str(msg)}')
return 0, str(msg)
@staticmethod
def createDNSZone(virtualHostName, admin):
try:

View File

@ -398,6 +398,12 @@ class vhost:
delWebsite = Websites.objects.get(domain=virtualHostName)
externalApp = delWebsite.externalApp
# Get admin user name for CloudFlare cleanup
adminUserName = None
try:
adminUserName = delWebsite.admin.userName
except:
pass
##
@ -411,6 +417,14 @@ class vhost:
numberOfSites = Websites.objects.count() + ChildDomains.objects.count()
vhost.deleteCoreConf(items.domain, numberOfSites)
# Delete CloudFlare DNS records for child domain
try:
DNS.deleteCloudFlareDNSRecords(items.domain, adminUserName)
except Exception as cfError:
# Log error but don't fail deletion if CloudFlare deletion fails
logging.CyberCPLogFileWriter.writeToFile(
f'CloudFlare DNS deletion failed for child domain {items.domain}: {str(cfError)}')
### Delete ACME Folder
if os.path.exists('/root/.acme.sh/%s' % (items.domain)):
@ -455,6 +469,14 @@ class vhost:
for items in databases:
mysqlUtilities.deleteDatabase(items.dbName, items.dbUser)
# Delete CloudFlare DNS records for main domain before deletion
try:
DNS.deleteCloudFlareDNSRecords(virtualHostName, adminUserName)
except Exception as cfError:
# Log error but don't fail deletion if CloudFlare deletion fails
logging.CyberCPLogFileWriter.writeToFile(
f'CloudFlare DNS deletion failed for {virtualHostName}: {str(cfError)}')
delWebsite.delete()
## Deleting DNS Zone if there is any.

View File

@ -1834,6 +1834,22 @@ local_name %s {
vhost.deleteCoreConf(virtualHostName, numberOfWebsites)
delWebsite = ChildDomains.objects.get(domain=virtualHostName)
# Get admin user name before deletion for CloudFlare cleanup
adminUserName = None
try:
adminUserName = delWebsite.master.admin.userName
except:
pass
# Delete CloudFlare DNS records for this domain
try:
from plogical.dnsUtilities import DNS
DNS.deleteCloudFlareDNSRecords(virtualHostName, adminUserName)
except Exception as cfError:
# Log error but don't fail domain deletion if CloudFlare deletion fails
logging.CyberCPLogFileWriter.writeToFile(
f'CloudFlare DNS deletion failed for {virtualHostName}: {str(cfError)}')
if DeleteDocRoot:
command = 'rm -rf %s' % (delWebsite.path)
ProcessUtilities.executioner(command)