diff --git a/dns/dnsManager.py b/dns/dnsManager.py index 379a8ac43..83d17706b 100644 --- a/dns/dnsManager.py +++ b/dns/dnsManager.py @@ -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, diff --git a/dns/static/dns/dns.js b/dns/static/dns/dns.js index 0767bb30d..b7a36b268 100644 --- a/dns/static/dns/dns.js +++ b/dns/static/dns/dns.js @@ -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 = []; } diff --git a/dns/templates/dns/addDeleteDNSRecordsCloudFlare.html b/dns/templates/dns/addDeleteDNSRecordsCloudFlare.html index a41c88b04..81c335c72 100644 --- a/dns/templates/dns/addDeleteDNSRecordsCloudFlare.html +++ b/dns/templates/dns/addDeleteDNSRecordsCloudFlare.html @@ -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 @@ - -
+

{% trans "DNS Records" %}

- +
+ Loading DNS records... +
+ +
+ No DNS records found. +
+ +
@@ -788,7 +863,7 @@ - +
{% trans "Name" %}
{{ record.type }} @@ -806,18 +881,41 @@
+ + + + + + + + + + + + + + + + + + + + + + + + + +
{% trans "Name" %}{% trans "Type" %}{% trans "TTL" %}{% trans "Value" %}{% trans "Priority" %}{% trans "Proxy" %}{% trans "Actions" %}
Loading...Loading...Loading...Loading...Loading...Loading...Loading...
- - -
diff --git a/plogical/dnsUtilities.py b/plogical/dnsUtilities.py index a7924982a..78db0bc59 100644 --- a/plogical/dnsUtilities.py +++ b/plogical/dnsUtilities.py @@ -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: diff --git a/plogical/vhost.py b/plogical/vhost.py index 7d952f590..540d72ec2 100644 --- a/plogical/vhost.py +++ b/plogical/vhost.py @@ -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. diff --git a/plogical/virtualHostUtilities.py b/plogical/virtualHostUtilities.py index 78a4d2847..fbf6c58e2 100644 --- a/plogical/virtualHostUtilities.py +++ b/plogical/virtualHostUtilities.py @@ -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)