diff --git a/dns/dnsManager.py b/dns/dnsManager.py index 3d8ac699c..a46d2ec49 100644 --- a/dns/dnsManager.py +++ b/dns/dnsManager.py @@ -13,10 +13,12 @@ import json try: from plogical.dnsUtilities import DNS from loginSystem.models import Administrator - from .models import Domains,Records + from .models import Domains, Records from plogical.mailUtilities import mailUtilities -except: - pass + from websiteFunctions.models import Websites, ChildDomains +except Exception: + Websites = None + ChildDomains = None import os from re import match,I,M from plogical.acl import ACLManager @@ -847,6 +849,356 @@ class DNSManager: final_json = json.dumps(final_dic) return HttpResponse(final_json) + def getExportRecordsCloudFlare(self, userID=None, data=None): + """Fetch all DNS records for a zone (all types) for export. Returns JSON list.""" + try: + currentACL = ACLManager.loadedACL(userID) + if ACLManager.currentContextPermission(currentACL, 'addDeleteRecords') == 0: + return ACLManager.loadErrorJson('fetchStatus', 0) + zone_domain = data.get('selectedZone', '').strip() + if not zone_domain: + final_json = json.dumps({'status': 0, 'fetchStatus': 0, 'error_message': 'Zone is required.', 'data': '[]'}) + return HttpResponse(final_json) + admin = Administrator.objects.get(pk=userID) + self.admin = admin + if ACLManager.checkOwnershipZone(zone_domain, admin, currentACL) != 1: + return ACLManager.loadErrorJson() + self.loadCFKeys() + params = {'name': zone_domain, 'per_page': 50} + cf = CloudFlare.CloudFlare(email=self.email, token=self.key) + zones = cf.zones.get(params=params) + if not zones: + final_json = json.dumps({'status': 0, 'fetchStatus': 0, 'error_message': 'Zone not found.', 'data': '[]'}) + return HttpResponse(final_json) + zone_id = sorted(zones, key=lambda v: v['name'])[0]['id'] + all_records = [] + page = 1 + per_page = 100 + while True: + try: + dns_records = cf.zones.dns_records.get(zone_id, params={'per_page': per_page, 'page': page}) + except BaseException as e: + final_json = json.dumps({'status': 0, 'fetchStatus': 0, 'error_message': str(e), 'data': '[]'}) + return HttpResponse(final_json) + if not dns_records: + break + for dns_record in dns_records: + ttl = 'AUTO' if dns_record.get('ttl') == 1 else dns_record.get('ttl', 3600) + prio = dns_record.get('priority') or 0 + all_records.append({ + 'id': dns_record.get('id'), + 'type': dns_record.get('type'), + 'name': dns_record.get('name'), + 'content': dns_record.get('content'), + 'priority': prio, + 'ttl': ttl, + 'proxy': dns_record.get('proxied', False), + 'proxiable': dns_record.get('proxiable', False), + }) + if len(dns_records) < per_page: + break + page += 1 + final_json = json.dumps({ + 'status': 1, + 'fetchStatus': 1, + 'error_message': '', + 'data': json.dumps(all_records), + }, default=str) + return HttpResponse(final_json) + except BaseException as msg: + final_json = json.dumps({'status': 0, 'fetchStatus': 0, 'error_message': str(msg), 'data': '[]'}) + return HttpResponse(final_json) + + def clearAllDNSRecordsCloudFlare(self, userID=None, data=None): + """Delete all DNS records for a zone. Returns list of deleted records for local backup/restore.""" + try: + currentACL = ACLManager.loadedACL(userID) + if ACLManager.currentContextPermission(currentACL, 'addDeleteRecords') == 0: + return ACLManager.loadErrorJson('delete_status', 0) + zone_domain = data.get('selectedZone', '').strip() + if not zone_domain: + final_json = json.dumps({'status': 0, 'delete_status': 0, 'error_message': 'Zone is required.', 'deleted_records': []}) + return HttpResponse(final_json) + admin = Administrator.objects.get(pk=userID) + self.admin = admin + if ACLManager.checkOwnershipZone(zone_domain, admin, currentACL) != 1: + return ACLManager.loadErrorJson() + self.loadCFKeys() + params = {'name': zone_domain, 'per_page': 50} + cf = CloudFlare.CloudFlare(email=self.email, token=self.key) + zones = cf.zones.get(params=params) + if not zones: + final_json = json.dumps({'status': 0, 'delete_status': 0, 'error_message': 'Zone not found.', 'deleted_records': []}) + return HttpResponse(final_json) + zone_id = sorted(zones, key=lambda v: v['name'])[0]['id'] + deleted = [] + page = 1 + per_page = 100 + while True: + try: + dns_records = cf.zones.dns_records.get(zone_id, params={'per_page': per_page, 'page': page}) + except BaseException as e: + final_json = json.dumps({'status': 0, 'delete_status': 0, 'error_message': str(e), 'deleted_records': deleted}) + return HttpResponse(final_json) + if not dns_records: + break + for dns_record in dns_records: + rec_type = dns_record.get('type', '') + if rec_type in ('SOA', 'NS'): + continue + rec_id = dns_record.get('id') + ttl = 'AUTO' if dns_record.get('ttl') == 1 else dns_record.get('ttl', 3600) + prio = dns_record.get('priority') or 0 + deleted.append({ + 'id': rec_id, + 'type': rec_type, + 'name': dns_record.get('name'), + 'content': dns_record.get('content'), + 'priority': prio, + 'ttl': ttl, + 'proxy': dns_record.get('proxied', False), + 'proxiable': dns_record.get('proxiable', False), + }) + try: + cf.zones.dns_records.delete(zone_id, rec_id) + except BaseException as e: + final_json = json.dumps({'status': 0, 'delete_status': 0, 'error_message': str(e), 'deleted_records': deleted}) + return HttpResponse(final_json) + if len(dns_records) < per_page: + break + page += 1 + final_json = json.dumps({'status': 1, 'delete_status': 1, 'error_message': '', 'deleted_records': deleted}, default=str) + return HttpResponse(final_json) + except BaseException as msg: + final_json = json.dumps({'status': 0, 'delete_status': 0, 'error_message': str(msg), 'deleted_records': []}) + return HttpResponse(final_json) + + def importDNSRecordsCloudFlare(self, userID=None, data=None): + """Import DNS records from a list. Creates each record via CloudFlare API.""" + try: + currentACL = ACLManager.loadedACL(userID) + if ACLManager.currentContextPermission(currentACL, 'addDeleteRecords') == 0: + return ACLManager.loadErrorJson('import_status', 0) + zone_domain = data.get('selectedZone', '').strip() + records = data.get('records', []) + if not zone_domain: + final_json = json.dumps({'status': 0, 'import_status': 0, 'error_message': 'Zone is required.', 'imported': 0, 'failed': []}) + return HttpResponse(final_json) + if not isinstance(records, list): + final_json = json.dumps({'status': 0, 'import_status': 0, 'error_message': 'records must be a list.', 'imported': 0, 'failed': []}) + return HttpResponse(final_json) + admin = Administrator.objects.get(pk=userID) + self.admin = admin + if ACLManager.checkOwnershipZone(zone_domain, admin, currentACL) != 1: + return ACLManager.loadErrorJson() + self.loadCFKeys() + params = {'name': zone_domain, 'per_page': 50} + cf = CloudFlare.CloudFlare(email=self.email, token=self.key) + zones = cf.zones.get(params=params) + if not zones: + final_json = json.dumps({'status': 0, 'import_status': 0, 'error_message': 'Zone not found.', 'imported': 0, 'failed': []}) + return HttpResponse(final_json) + zone_id = sorted(zones, key=lambda v: v['name'])[0]['id'] + imported = 0 + failed = [] + for rec in records: + name = (rec.get('name') or '').strip() + rec_type = (rec.get('type') or '').strip().upper() + content = (rec.get('content') or '').strip() + if not name or not rec_type or not content: + failed.append({'name': name or '(empty)', 'error': 'Name, type and content required.'}) + continue + ttl_val = rec.get('ttl', 3600) + if ttl_val == 'AUTO' or ttl_val == 1: + ttl_int = 1 + else: + try: + ttl_int = int(ttl_val) + if ttl_int < 0: + ttl_int = 1 + elif ttl_int > 86400 and ttl_int != 1: + ttl_int = 86400 + except (ValueError, TypeError): + ttl_int = 3600 + priority = 0 + try: + priority = int(rec.get('priority', 0) or 0) + except (ValueError, TypeError): + pass + proxied = bool(rec.get('proxy', False) and rec.get('proxiable', True)) + try: + DNS.createDNSRecordCloudFlare(cf, zone_id, name, rec_type, content, priority, ttl_int, proxied=proxied) + imported += 1 + except BaseException as e: + failed.append({'name': name, 'error': str(e)}) + final_json = json.dumps({ + 'status': 1, + 'import_status': 1, + 'error_message': '', + 'imported': imported, + 'failed': failed, + }, default=str) + return HttpResponse(final_json) + except BaseException as msg: + final_json = json.dumps({'status': 0, 'import_status': 0, 'error_message': str(msg), 'imported': 0, 'failed': []}) + return HttpResponse(final_json) + + def _get_valid_hostnames_for_zone(self, zone_domain): + """Return set of valid hostnames for this zone (main domain + child domains in panel).""" + valid = set() + valid.add(zone_domain.lower().strip()) + if Websites is None or ChildDomains is None: + return valid + try: + website = Websites.objects.get(domain=zone_domain) + valid.add(website.domain.lower()) + for child in website.childdomains_set.all(): + valid.add(child.domain.lower()) + except (Websites.DoesNotExist, Exception): + pass + return valid + + def getStaleDNSRecordsCloudFlare(self, userID=None, data=None): + """List DNS records that point to subdomains/hostnames no longer in the panel (orphan/stale).""" + try: + currentACL = ACLManager.loadedACL(userID) + if ACLManager.currentContextPermission(currentACL, 'addDeleteRecords') == 0: + return ACLManager.loadErrorJson('fetchStatus', 0) + zone_domain = data.get('selectedZone', '').strip() + if not zone_domain: + final_json = json.dumps({'status': 0, 'fetchStatus': 0, 'error_message': 'Zone is required.', 'stale_records': []}) + return HttpResponse(final_json) + admin = Administrator.objects.get(pk=userID) + self.admin = admin + if ACLManager.checkOwnershipZone(zone_domain, admin, currentACL) != 1: + return ACLManager.loadErrorJson() + valid_hostnames = self._get_valid_hostnames_for_zone(zone_domain) + self.loadCFKeys() + params = {'name': zone_domain, 'per_page': 50} + cf = CloudFlare.CloudFlare(email=self.email, token=self.key) + zones = cf.zones.get(params=params) + if not zones: + final_json = json.dumps({'status': 0, 'fetchStatus': 0, 'error_message': 'Zone not found.', 'stale_records': []}) + return HttpResponse(final_json) + zone_id = sorted(zones, key=lambda v: v['name'])[0]['id'] + stale = [] + page = 1 + per_page = 100 + zone_lower = zone_domain.lower() + # Only consider A, AAAA, CNAME as "subdomain" records that can be stale + host_record_types = ('A', 'AAAA', 'CNAME') + while True: + try: + dns_records = cf.zones.dns_records.get(zone_id, params={'per_page': per_page, 'page': page}) + except BaseException as e: + final_json = json.dumps({'status': 0, 'fetchStatus': 0, 'error_message': str(e), 'stale_records': []}) + return HttpResponse(final_json) + if not dns_records: + break + for dns_record in dns_records: + rec_type = (dns_record.get('type') or '').strip().upper() + if rec_type not in host_record_types: + continue + name = (dns_record.get('name') or '').strip() + if not name: + continue + fqdn = name.lower().rstrip('.') + if fqdn in valid_hostnames: + continue + ttl = 'AUTO' if dns_record.get('ttl') == 1 else dns_record.get('ttl', 3600) + stale.append({ + 'id': dns_record.get('id'), + 'type': rec_type, + 'name': name, + 'content': dns_record.get('content', ''), + 'priority': dns_record.get('priority') or 0, + 'ttl': ttl, + 'proxy': dns_record.get('proxied', False), + }) + if len(dns_records) < per_page: + break + page += 1 + final_json = json.dumps({ + 'status': 1, + 'fetchStatus': 1, + 'error_message': '', + 'stale_records': stale, + }, default=str) + return HttpResponse(final_json) + except BaseException as msg: + final_json = json.dumps({'status': 0, 'fetchStatus': 0, 'error_message': str(msg), 'stale_records': []}) + return HttpResponse(final_json) + + def removeStaleDNSRecordsCloudFlare(self, userID=None, data=None): + """Remove DNS records that are stale (optionally by id list). Returns deleted list for backup.""" + try: + currentACL = ACLManager.loadedACL(userID) + if ACLManager.currentContextPermission(currentACL, 'addDeleteRecords') == 0: + return ACLManager.loadErrorJson('delete_status', 0) + zone_domain = data.get('selectedZone', '').strip() + ids_to_remove = data.get('ids', []) + if not zone_domain: + final_json = json.dumps({'status': 0, 'delete_status': 0, 'error_message': 'Zone is required.', 'deleted_records': []}) + return HttpResponse(final_json) + admin = Administrator.objects.get(pk=userID) + self.admin = admin + if ACLManager.checkOwnershipZone(zone_domain, admin, currentACL) != 1: + return ACLManager.loadErrorJson() + self.loadCFKeys() + params = {'name': zone_domain, 'per_page': 50} + cf = CloudFlare.CloudFlare(email=self.email, token=self.key) + zones = cf.zones.get(params=params) + if not zones: + final_json = json.dumps({'status': 0, 'delete_status': 0, 'error_message': 'Zone not found.', 'deleted_records': []}) + return HttpResponse(final_json) + zone_id = sorted(zones, key=lambda v: v['name'])[0]['id'] + if not ids_to_remove: + valid_hostnames = self._get_valid_hostnames_for_zone(zone_domain) + zone_lower = zone_domain.lower() + host_record_types = ('A', 'AAAA', 'CNAME') + page = 1 + per_page = 100 + while True: + dns_records = cf.zones.dns_records.get(zone_id, params={'per_page': per_page, 'page': page}) + if not dns_records: + break + for dns_record in dns_records: + rec_type = (dns_record.get('type') or '').strip().upper() + if rec_type not in host_record_types: + continue + name = (dns_record.get('name') or '').strip() + if not name: + continue + fqdn = name.lower().rstrip('.') + if fqdn not in valid_hostnames: + ids_to_remove.append(dns_record.get('id')) + if len(dns_records) < per_page: + break + page += 1 + deleted = [] + for rec_id in ids_to_remove: + try: + rec = cf.zones.dns_records.get(zone_id, rec_id) + ttl = 'AUTO' if rec.get('ttl') == 1 else rec.get('ttl', 3600) + deleted.append({ + 'id': rec_id, + 'type': rec.get('type'), + 'name': rec.get('name'), + 'content': rec.get('content'), + 'priority': rec.get('priority') or 0, + 'ttl': ttl, + 'proxy': rec.get('proxied', False), + }) + cf.zones.dns_records.delete(zone_id, rec_id) + except BaseException as e: + final_json = json.dumps({'status': 0, 'delete_status': 0, 'error_message': str(e), 'deleted_records': deleted}) + return HttpResponse(final_json) + final_json = json.dumps({'status': 1, 'delete_status': 1, 'error_message': '', 'deleted_records': deleted}, default=str) + return HttpResponse(final_json) + except BaseException as msg: + final_json = json.dumps({'status': 0, 'delete_status': 0, 'error_message': str(msg), 'deleted_records': []}) + return HttpResponse(final_json) + def updateDNSRecordCloudFlare(self, userID=None, data=None): """Update an existing CloudFlare DNS record (name, type, ttl, content, priority, proxied).""" try: diff --git a/dns/static/dns/dns.js b/dns/static/dns/dns.js index b7a36b268..d396ffdfd 100644 --- a/dns/static/dns/dns.js +++ b/dns/static/dns/dns.js @@ -496,9 +496,33 @@ app.controller('addModifyDNSRecords', function ($scope, $http) { } }; - $scope.deleteRecord = function (id) { + $scope.confirmDeleteRecord = function (record) { + var msg = 'Delete DNS record?\n\nName: ' + (record.name || '') + '\nType: ' + (record.type || '') + '\nValue: ' + (record.content || ''); + if (!$window.confirm(msg)) { + return; + } + var zone = $scope.selectedZone; + if (!zone) { + return; + } + if (!$scope.cfDeletedBackup[zone]) { + $scope.cfDeletedBackup[zone] = []; + } + $scope.cfDeletedBackup[zone].push({ + type: record.type, + name: record.name, + content: record.content, + priority: parseInt(record.priority, 10) || 0, + ttl: record.ttlNum || record.ttl || 3600, + proxy: record.proxy, + proxiable: record.proxiable !== false + }); + $scope.deleteRecord(record.id); + }; - var selectedZone = $scope.selectedZone; + $scope.deleteRecord = function (id) { + + var selectedZone = $scope.selectedZone; url = "/dns/deleteDNSRecord"; @@ -732,6 +756,22 @@ app.controller('configureDefaultNameservers', function ($scope, $http) { /* Java script code for CloudFlare */ +app.directive('cfImportFile', function () { + return { + link: function (scope, element) { + element.on('change', function (ev) { + var files = ev.target && ev.target.files; + if (files && files.length && scope.onImportFile) { + scope.$apply(function () { + scope.onImportFile(files); + }); + } + ev.target.value = ''; + }); + } + }; +}); + app.controller('addModifyDNSRecordsCloudFlare', function ($scope, $http, $window) { $scope.saveCFConfigs = function () { @@ -813,6 +853,13 @@ app.controller('addModifyDNSRecordsCloudFlare', function ($scope, $http, $window $scope.couldNotAddRecord = true; $scope.recordValueDefault = false; $scope.records = []; + $scope.cfDeletedBackup = {}; + $scope.exportLoading = false; + $scope.clearAllLoading = false; + $scope.restoreLoading = false; + $scope.staleRecords = []; + $scope.staleModalVisible = false; + $scope.staleLoading = false; // Hide records boxes $(".aaaaRecord").hide(); @@ -1140,6 +1187,198 @@ app.controller('addModifyDNSRecordsCloudFlare', function ($scope, $http, $window }; + $scope.hasBackupForZone = function () { + var zone = $scope.selectedZone; + if (!zone) return false; + var list = $scope.cfDeletedBackup[zone]; + return list && list.length > 0; + }; + + $scope.confirmClearAll = function () { + var zone = $scope.selectedZone; + if (!zone) return; + var msg1 = 'This will remove ALL DNS records for this zone in CloudFlare. This action cannot be undone on CloudFlare.\n\nA local copy will be kept so you can use Restore.\n\nContinue?'; + if (!$window.confirm(msg1)) return; + var msg2 = 'Type the zone name below to confirm:\n\n' + zone; + var typed = $window.prompt(msg2); + if (typed === null) return; + if (typed.trim() !== zone) { + new PNotify({ title: 'Cancelled', text: 'Zone name did not match. No records were deleted.', type: 'warning' }); + return; + } + $scope.clearAllLoading = true; + url = '/dns/clearAllDNSRecordsCloudFlare'; + var data = { selectedZone: zone }; + var config = { headers: { 'X-CSRFToken': getCookie('csrftoken') } }; + $http.post(url, data, config).then(function (response) { + $scope.clearAllLoading = false; + if (response.data.delete_status === 1 && response.data.deleted_records) { + $scope.cfDeletedBackup[zone] = response.data.deleted_records; + $scope.canNotFetchRecords = true; + $scope.recordsFetched = false; + $scope.recordDeleted = false; + populateCurrentRecords(); + new PNotify({ title: 'Done', text: 'All DNS records were deleted. Use Restore to undo.', type: 'success' }); + } else { + $scope.errorMessage = response.data.error_message || 'Clear all failed'; + new PNotify({ title: 'Error', text: $scope.errorMessage, type: 'error' }); + } + }, function () { + $scope.clearAllLoading = false; + new PNotify({ title: 'Error', text: 'Could not connect to server.', type: 'error' }); + }); + }; + + $scope.restoreFromBackup = function () { + var zone = $scope.selectedZone; + var list = $scope.cfDeletedBackup[zone]; + if (!zone || !list || list.length === 0) return; + $scope.restoreLoading = true; + url = '/dns/importDNSRecordsCloudFlare'; + var data = { selectedZone: zone, records: list }; + var config = { headers: { 'X-CSRFToken': getCookie('csrftoken') } }; + $http.post(url, data, config).then(function (response) { + $scope.restoreLoading = false; + if (response.data.import_status === 1) { + $scope.cfDeletedBackup[zone] = []; + populateCurrentRecords(); + var failed = response.data.failed || []; + var msg = response.data.imported + ' record(s) restored.'; + if (failed.length) msg += ' ' + failed.length + ' failed.'; + new PNotify({ title: 'Restore done', text: msg, type: failed.length ? 'warning' : 'success' }); + } else { + new PNotify({ title: 'Error', text: response.data.error_message || 'Restore failed', type: 'error' }); + } + }, function () { + $scope.restoreLoading = false; + new PNotify({ title: 'Error', text: 'Could not connect to server.', type: 'error' }); + }); + }; + + $scope.exportRecords = function () { + var zone = $scope.selectedZone; + if (!zone) return; + $scope.exportLoading = true; + url = '/dns/getExportRecordsCloudFlare'; + var data = { selectedZone: zone }; + var config = { headers: { 'X-CSRFToken': getCookie('csrftoken') } }; + $http.post(url, data, config).then(function (response) { + $scope.exportLoading = false; + if (response.data.fetchStatus === 1 && response.data.data) { + var arr = typeof response.data.data === 'string' ? JSON.parse(response.data.data) : response.data.data; + var blob = new Blob([JSON.stringify(arr, null, 2)], { type: 'application/json' }); + var a = document.createElement('a'); + a.href = (window.URL || window.webkitURL).createObjectURL(blob); + a.download = 'dns-records-' + zone.replace(/\./g, '-') + '.json'; + a.click(); + if (a.href) (window.URL || window.webkitURL).revokeObjectURL(a.href); + new PNotify({ title: 'Export done', text: 'DNS records downloaded.', type: 'success' }); + } else { + new PNotify({ title: 'Error', text: response.data.error_message || 'Export failed', type: 'error' }); + } + }, function () { + $scope.exportLoading = false; + new PNotify({ title: 'Error', text: 'Could not connect to server.', type: 'error' }); + }); + }; + + $scope.onImportFile = function (files) { + if (!files || !files.length) return; + var zone = $scope.selectedZone; + if (!zone) { + new PNotify({ title: 'Error', text: 'Select a zone first.', type: 'error' }); + return; + } + var file = files[0]; + var reader = new FileReader(); + reader.onload = function (e) { + var text = e.target && e.target.result; + if (!text) { + new PNotify({ title: 'Error', text: 'Could not read file.', type: 'error' }); + return; + } + var arr; + try { + arr = JSON.parse(text); + } catch (err) { + new PNotify({ title: 'Error', text: 'Invalid JSON: ' + (err.message || ''), type: 'error' }); + return; + } + if (!Array.isArray(arr)) { + if (arr && Array.isArray(arr.records)) arr = arr.records; + else if (arr && arr.data) arr = Array.isArray(arr.data) ? arr.data : [arr.data]; + else arr = [arr]; + } + url = '/dns/importDNSRecordsCloudFlare'; + var data = { selectedZone: zone, records: arr }; + var config = { headers: { 'X-CSRFToken': getCookie('csrftoken') } }; + $http.post(url, data, config).then(function (response) { + if (response.data.import_status === 1) { + populateCurrentRecords(); + var failed = response.data.failed || []; + var msg = response.data.imported + ' record(s) imported.'; + if (failed.length) msg += ' ' + failed.length + ' failed.'; + new PNotify({ title: 'Import done', text: msg, type: failed.length ? 'warning' : 'success' }); + } else { + new PNotify({ title: 'Error', text: response.data.error_message || 'Import failed', type: 'error' }); + } + }, function () { + new PNotify({ title: 'Error', text: 'Could not connect to server.', type: 'error' }); + }); + }; + reader.readAsText(file, 'UTF-8'); + }; + + $scope.checkStaleRecords = function () { + var zone = $scope.selectedZone; + if (!zone) return; + $scope.staleLoading = true; + url = '/dns/getStaleDNSRecordsCloudFlare'; + var data = { selectedZone: zone }; + var config = { headers: { 'X-CSRFToken': getCookie('csrftoken') } }; + $http.post(url, data, config).then(function (response) { + $scope.staleLoading = false; + if (response.data.fetchStatus === 1) { + $scope.staleRecords = response.data.stale_records || []; + $scope.staleModalVisible = true; + } else { + new PNotify({ title: 'Error', text: response.data.error_message || 'Could not fetch stale records', type: 'error' }); + } + }, function () { + $scope.staleLoading = false; + new PNotify({ title: 'Error', text: 'Could not connect to server.', type: 'error' }); + }); + }; + + $scope.closeStaleModal = function () { + $scope.staleModalVisible = false; + $scope.staleRecords = []; + }; + + $scope.removeStaleRecords = function () { + if (!$scope.staleRecords || $scope.staleRecords.length === 0) return; + var zone = $scope.selectedZone; + var msg = 'Remove ' + $scope.staleRecords.length + ' orphan DNS record(s)? A local copy will be kept for Restore.'; + if (!$window.confirm(msg)) return; + var ids = $scope.staleRecords.map(function (r) { return r.id; }); + url = '/dns/removeStaleDNSRecordsCloudFlare'; + var data = { selectedZone: zone, ids: ids }; + var config = { headers: { 'X-CSRFToken': getCookie('csrftoken') } }; + $http.post(url, data, config).then(function (response) { + if (response.data.delete_status === 1 && response.data.deleted_records) { + if (!$scope.cfDeletedBackup[zone]) $scope.cfDeletedBackup[zone] = []; + $scope.cfDeletedBackup[zone] = $scope.cfDeletedBackup[zone].concat(response.data.deleted_records); + $scope.closeStaleModal(); + populateCurrentRecords(); + new PNotify({ title: 'Done', text: response.data.deleted_records.length + ' orphan record(s) removed. Use Restore to undo.', type: 'success' }); + } else { + new PNotify({ title: 'Error', text: response.data.error_message || 'Remove failed', type: 'error' }); + } + }, function () { + new PNotify({ title: 'Error', text: 'Could not connect to server.', type: 'error' }); + }); + }; + $scope.syncCF = function () { $scope.recordsLoading = false; diff --git a/dns/templates/dns/addDeleteDNSRecordsCloudFlare.html b/dns/templates/dns/addDeleteDNSRecordsCloudFlare.html index c7703bf51..55967b9e3 100644 --- a/dns/templates/dns/addDeleteDNSRecordsCloudFlare.html +++ b/dns/templates/dns/addDeleteDNSRecordsCloudFlare.html @@ -959,6 +959,25 @@ {% trans "DNS Records" %} +
+ + + + + +
+
@@ -1040,7 +1059,7 @@ @@ -1128,6 +1147,46 @@
+ +
+
+

+ {% trans "Orphan / Stale DNS Records" %} +

+

{% trans "Records below point to subdomains or hostnames that no longer exist in the panel. You can remove them to clean up CloudFlare." %}

+
+ {% trans "No orphan records found." %} +
+
+ + + + + + + + + + + + + + + +
{% trans "Name" %}{% trans "Type" %}{% trans "Value" %}
+
+ + +
+
+
+ +
+
+
+
diff --git a/dns/urls.py b/dns/urls.py index 2ca5850c2..dd910c071 100644 --- a/dns/urls.py +++ b/dns/urls.py @@ -30,4 +30,9 @@ urlpatterns = [ re_path(r'^updateDNSRecordCloudFlare$', views.updateDNSRecordCloudFlare, name='updateDNSRecordCloudFlare'), re_path(r'^syncCF$', views.syncCF, name='syncCF'), re_path(r'^enableProxy$', views.enableProxy, name='enableProxy'), + re_path(r'^getExportRecordsCloudFlare$', views.getExportRecordsCloudFlare, name='getExportRecordsCloudFlare'), + re_path(r'^clearAllDNSRecordsCloudFlare$', views.clearAllDNSRecordsCloudFlare, name='clearAllDNSRecordsCloudFlare'), + re_path(r'^importDNSRecordsCloudFlare$', views.importDNSRecordsCloudFlare, name='importDNSRecordsCloudFlare'), + re_path(r'^getStaleDNSRecordsCloudFlare$', views.getStaleDNSRecordsCloudFlare, name='getStaleDNSRecordsCloudFlare'), + re_path(r'^removeStaleDNSRecordsCloudFlare$', views.removeStaleDNSRecordsCloudFlare, name='removeStaleDNSRecordsCloudFlare'), ] diff --git a/dns/views.py b/dns/views.py index ba07d9bdd..ce8dc785e 100644 --- a/dns/views.py +++ b/dns/views.py @@ -377,3 +377,48 @@ def enableProxy(request): return redirect(loadLoginPage) except (ValueError, TypeError): return HttpResponse(json.dumps({'status': 0, 'error_message': 'Invalid request'}), status=400, content_type='application/json') + + +def getExportRecordsCloudFlare(request): + try: + userID = request.session['userID'] + dm = DNSManager() + return dm.getExportRecordsCloudFlare(userID, json.loads(request.body or '{}')) + except KeyError: + return redirect(loadLoginPage) + + +def clearAllDNSRecordsCloudFlare(request): + try: + userID = request.session['userID'] + dm = DNSManager() + return dm.clearAllDNSRecordsCloudFlare(userID, json.loads(request.body or '{}')) + except KeyError: + return redirect(loadLoginPage) + + +def importDNSRecordsCloudFlare(request): + try: + userID = request.session['userID'] + dm = DNSManager() + return dm.importDNSRecordsCloudFlare(userID, json.loads(request.body or '{}')) + except KeyError: + return redirect(loadLoginPage) + + +def getStaleDNSRecordsCloudFlare(request): + try: + userID = request.session['userID'] + dm = DNSManager() + return dm.getStaleDNSRecordsCloudFlare(userID, json.loads(request.body or '{}')) + except KeyError: + return redirect(loadLoginPage) + + +def removeStaleDNSRecordsCloudFlare(request): + try: + userID = request.session['userID'] + dm = DNSManager() + return dm.removeStaleDNSRecordsCloudFlare(userID, json.loads(request.body or '{}')) + except KeyError: + return redirect(loadLoginPage) diff --git a/public/static/dns/dns.js b/public/static/dns/dns.js index dba1d46ef..3da48e366 100644 --- a/public/static/dns/dns.js +++ b/public/static/dns/dns.js @@ -732,6 +732,22 @@ app.controller('configureDefaultNameservers', function ($scope, $http) { /* Java script code for CloudFlare */ +app.directive('cfImportFile', function () { + return { + link: function (scope, element) { + element.on('change', function (ev) { + var files = ev.target && ev.target.files; + if (files && files.length && scope.onImportFile) { + scope.$apply(function () { + scope.onImportFile(files); + }); + } + ev.target.value = ''; + }); + } + }; +}); + app.filter('dnsRecordSearch', function () { return function (records, searchText) { if (!records || !Array.isArray(records)) return records; @@ -827,6 +843,13 @@ app.controller('addModifyDNSRecordsCloudFlare', function ($scope, $http, $window $scope.couldNotAddRecord = true; $scope.recordValueDefault = false; $scope.records = []; + $scope.cfDeletedBackup = {}; + $scope.exportLoading = false; + $scope.clearAllLoading = false; + $scope.restoreLoading = false; + $scope.staleRecords = []; + $scope.staleModalVisible = false; + $scope.staleLoading = false; $scope.showEditModal = false; $scope.editRecord = {}; @@ -1079,8 +1102,31 @@ app.controller('addModifyDNSRecordsCloudFlare', function ($scope, $http, $window } - $scope.deleteRecord = function (id) { + $scope.confirmDeleteRecord = function (record) { + var msg = 'Delete DNS record?\n\nName: ' + (record.name || '') + '\nType: ' + (record.type || '') + '\nValue: ' + (record.content || ''); + if (!$window.confirm(msg)) { + return; + } + var zone = $scope.selectedZone; + if (!zone) { + return; + } + if (!$scope.cfDeletedBackup[zone]) { + $scope.cfDeletedBackup[zone] = []; + } + $scope.cfDeletedBackup[zone].push({ + type: record.type, + name: record.name, + content: record.content, + priority: parseInt(record.priority, 10) || 0, + ttl: record.ttlNum || record.ttl || 3600, + proxy: record.proxy, + proxiable: record.proxiable !== false + }); + $scope.deleteRecord(record.id); + }; + $scope.deleteRecord = function (id) { var selectedZone = $scope.selectedZone; @@ -1164,6 +1210,198 @@ app.controller('addModifyDNSRecordsCloudFlare', function ($scope, $http, $window }; + $scope.hasBackupForZone = function () { + var zone = $scope.selectedZone; + if (!zone) return false; + var list = $scope.cfDeletedBackup[zone]; + return list && list.length > 0; + }; + + $scope.confirmClearAll = function () { + var zone = $scope.selectedZone; + if (!zone) return; + var msg1 = 'This will remove ALL DNS records for this zone in CloudFlare. This action cannot be undone on CloudFlare.\n\nA local copy will be kept so you can use Restore.\n\nContinue?'; + if (!$window.confirm(msg1)) return; + var msg2 = 'Type the zone name below to confirm:\n\n' + zone; + var typed = $window.prompt(msg2); + if (typed === null) return; + if (typed.trim() !== zone) { + new PNotify({ title: 'Cancelled', text: 'Zone name did not match. No records were deleted.', type: 'warning' }); + return; + } + $scope.clearAllLoading = true; + url = '/dns/clearAllDNSRecordsCloudFlare'; + var data = { selectedZone: zone }; + var config = { headers: { 'X-CSRFToken': getCookie('csrftoken') } }; + $http.post(url, data, config).then(function (response) { + $scope.clearAllLoading = false; + if (response.data.delete_status === 1 && response.data.deleted_records) { + $scope.cfDeletedBackup[zone] = response.data.deleted_records; + $scope.canNotFetchRecords = true; + $scope.recordsFetched = false; + $scope.recordDeleted = false; + populateCurrentRecords(); + new PNotify({ title: 'Done', text: 'All DNS records were deleted. Use Restore to undo.', type: 'success' }); + } else { + $scope.errorMessage = response.data.error_message || 'Clear all failed'; + new PNotify({ title: 'Error', text: $scope.errorMessage, type: 'error' }); + } + }, function () { + $scope.clearAllLoading = false; + new PNotify({ title: 'Error', text: 'Could not connect to server.', type: 'error' }); + }); + }; + + $scope.restoreFromBackup = function () { + var zone = $scope.selectedZone; + var list = $scope.cfDeletedBackup[zone]; + if (!zone || !list || list.length === 0) return; + $scope.restoreLoading = true; + url = '/dns/importDNSRecordsCloudFlare'; + var data = { selectedZone: zone, records: list }; + var config = { headers: { 'X-CSRFToken': getCookie('csrftoken') } }; + $http.post(url, data, config).then(function (response) { + $scope.restoreLoading = false; + if (response.data.import_status === 1) { + $scope.cfDeletedBackup[zone] = []; + populateCurrentRecords(); + var failed = response.data.failed || []; + var msg = response.data.imported + ' record(s) restored.'; + if (failed.length) msg += ' ' + failed.length + ' failed.'; + new PNotify({ title: 'Restore done', text: msg, type: failed.length ? 'warning' : 'success' }); + } else { + new PNotify({ title: 'Error', text: response.data.error_message || 'Restore failed', type: 'error' }); + } + }, function () { + $scope.restoreLoading = false; + new PNotify({ title: 'Error', text: 'Could not connect to server.', type: 'error' }); + }); + }; + + $scope.exportRecords = function () { + var zone = $scope.selectedZone; + if (!zone) return; + $scope.exportLoading = true; + url = '/dns/getExportRecordsCloudFlare'; + var data = { selectedZone: zone }; + var config = { headers: { 'X-CSRFToken': getCookie('csrftoken') } }; + $http.post(url, data, config).then(function (response) { + $scope.exportLoading = false; + if (response.data.fetchStatus === 1 && response.data.data) { + var arr = typeof response.data.data === 'string' ? JSON.parse(response.data.data) : response.data.data; + var blob = new Blob([JSON.stringify(arr, null, 2)], { type: 'application/json' }); + var a = document.createElement('a'); + a.href = (window.URL || window.webkitURL).createObjectURL(blob); + a.download = 'dns-records-' + zone.replace(/\./g, '-') + '.json'; + a.click(); + if (a.href) (window.URL || window.webkitURL).revokeObjectURL(a.href); + new PNotify({ title: 'Export done', text: 'DNS records downloaded.', type: 'success' }); + } else { + new PNotify({ title: 'Error', text: response.data.error_message || 'Export failed', type: 'error' }); + } + }, function () { + $scope.exportLoading = false; + new PNotify({ title: 'Error', text: 'Could not connect to server.', type: 'error' }); + }); + }; + + $scope.onImportFile = function (files) { + if (!files || !files.length) return; + var zone = $scope.selectedZone; + if (!zone) { + new PNotify({ title: 'Error', text: 'Select a zone first.', type: 'error' }); + return; + } + var file = files[0]; + var reader = new FileReader(); + reader.onload = function (e) { + var text = e.target && e.target.result; + if (!text) { + new PNotify({ title: 'Error', text: 'Could not read file.', type: 'error' }); + return; + } + var arr; + try { + arr = JSON.parse(text); + } catch (err) { + new PNotify({ title: 'Error', text: 'Invalid JSON: ' + (err.message || ''), type: 'error' }); + return; + } + if (!Array.isArray(arr)) { + if (arr && Array.isArray(arr.records)) arr = arr.records; + else if (arr && arr.data) arr = Array.isArray(arr.data) ? arr.data : [arr.data]; + else arr = [arr]; + } + url = '/dns/importDNSRecordsCloudFlare'; + var data = { selectedZone: zone, records: arr }; + var config = { headers: { 'X-CSRFToken': getCookie('csrftoken') } }; + $http.post(url, data, config).then(function (response) { + if (response.data.import_status === 1) { + populateCurrentRecords(); + var failed = response.data.failed || []; + var msg = response.data.imported + ' record(s) imported.'; + if (failed.length) msg += ' ' + failed.length + ' failed.'; + new PNotify({ title: 'Import done', text: msg, type: failed.length ? 'warning' : 'success' }); + } else { + new PNotify({ title: 'Error', text: response.data.error_message || 'Import failed', type: 'error' }); + } + }, function () { + new PNotify({ title: 'Error', text: 'Could not connect to server.', type: 'error' }); + }); + }; + reader.readAsText(file, 'UTF-8'); + }; + + $scope.checkStaleRecords = function () { + var zone = $scope.selectedZone; + if (!zone) return; + $scope.staleLoading = true; + url = '/dns/getStaleDNSRecordsCloudFlare'; + var data = { selectedZone: zone }; + var config = { headers: { 'X-CSRFToken': getCookie('csrftoken') } }; + $http.post(url, data, config).then(function (response) { + $scope.staleLoading = false; + if (response.data.fetchStatus === 1) { + $scope.staleRecords = response.data.stale_records || []; + $scope.staleModalVisible = true; + } else { + new PNotify({ title: 'Error', text: response.data.error_message || 'Could not fetch stale records', type: 'error' }); + } + }, function () { + $scope.staleLoading = false; + new PNotify({ title: 'Error', text: 'Could not connect to server.', type: 'error' }); + }); + }; + + $scope.closeStaleModal = function () { + $scope.staleModalVisible = false; + $scope.staleRecords = []; + }; + + $scope.removeStaleRecords = function () { + if (!$scope.staleRecords || $scope.staleRecords.length === 0) return; + var zone = $scope.selectedZone; + var msg = 'Remove ' + $scope.staleRecords.length + ' orphan DNS record(s)? A local copy will be kept for Restore.'; + if (!$window.confirm(msg)) return; + var ids = $scope.staleRecords.map(function (r) { return r.id; }); + url = '/dns/removeStaleDNSRecordsCloudFlare'; + var data = { selectedZone: zone, ids: ids }; + var config = { headers: { 'X-CSRFToken': getCookie('csrftoken') } }; + $http.post(url, data, config).then(function (response) { + if (response.data.delete_status === 1 && response.data.deleted_records) { + if (!$scope.cfDeletedBackup[zone]) $scope.cfDeletedBackup[zone] = []; + $scope.cfDeletedBackup[zone] = $scope.cfDeletedBackup[zone].concat(response.data.deleted_records); + $scope.closeStaleModal(); + populateCurrentRecords(); + new PNotify({ title: 'Done', text: response.data.deleted_records.length + ' orphan record(s) removed. Use Restore to undo.', type: 'success' }); + } else { + new PNotify({ title: 'Error', text: response.data.error_message || 'Remove failed', type: 'error' }); + } + }, function () { + new PNotify({ title: 'Error', text: 'Could not connect to server.', type: 'error' }); + }); + }; + $scope.dnsTypeList = ['A', 'AAAA', 'CNAME', 'MX', 'TXT', 'NS', 'SOA', 'SRV', 'CAA', 'SPF', 'DNSKEY', 'CDNSKEY', 'HTTPS', 'SVCB', 'URI', 'LOC', 'NAPTR', 'SMIMEA', 'SSHFP', 'TLSA', 'PTR']; $scope.getTypeOptions = function (record) { var list = angular.copy($scope.dnsTypeList); diff --git a/static/dns/dns.js b/static/dns/dns.js index c0f4d34cf..912b2e693 100644 --- a/static/dns/dns.js +++ b/static/dns/dns.js @@ -732,6 +732,22 @@ app.controller('configureDefaultNameservers', function ($scope, $http) { /* Java script code for CloudFlare */ +app.directive('cfImportFile', function () { + return { + link: function (scope, element) { + element.on('change', function (ev) { + var files = ev.target && ev.target.files; + if (files && files.length && scope.onImportFile) { + scope.$apply(function () { + scope.onImportFile(files); + }); + } + ev.target.value = ''; + }); + } + }; +}); + app.filter('dnsRecordSearch', function () { return function (records, searchText) { if (!records || !Array.isArray(records)) return records; @@ -828,6 +844,13 @@ app.controller('addModifyDNSRecordsCloudFlare', function ($scope, $http, $window $scope.couldNotAddRecord = true; $scope.recordValueDefault = false; $scope.records = []; + $scope.cfDeletedBackup = {}; + $scope.exportLoading = false; + $scope.clearAllLoading = false; + $scope.restoreLoading = false; + $scope.staleRecords = []; + $scope.staleModalVisible = false; + $scope.staleLoading = false; $scope.showEditModal = false; $scope.editRecord = {}; @@ -1083,6 +1106,30 @@ app.controller('addModifyDNSRecordsCloudFlare', function ($scope, $http, $window } + $scope.confirmDeleteRecord = function (record) { + var msg = 'Delete DNS record?\n\nName: ' + (record.name || '') + '\nType: ' + (record.type || '') + '\nValue: ' + (record.content || ''); + if (!$window.confirm(msg)) { + return; + } + var zone = $scope.selectedZone; + if (!zone) { + return; + } + if (!$scope.cfDeletedBackup[zone]) { + $scope.cfDeletedBackup[zone] = []; + } + $scope.cfDeletedBackup[zone].push({ + type: record.type, + name: record.name, + content: record.content, + priority: parseInt(record.priority, 10) || 0, + ttl: record.ttlNum || record.ttl || 3600, + proxy: record.proxy, + proxiable: record.proxiable !== false + }); + $scope.deleteRecord(record.id); + }; + $scope.deleteRecord = function (id) { @@ -1168,6 +1215,198 @@ app.controller('addModifyDNSRecordsCloudFlare', function ($scope, $http, $window }; + $scope.hasBackupForZone = function () { + var zone = $scope.selectedZone; + if (!zone) return false; + var list = $scope.cfDeletedBackup[zone]; + return list && list.length > 0; + }; + + $scope.confirmClearAll = function () { + var zone = $scope.selectedZone; + if (!zone) return; + var msg1 = 'This will remove ALL DNS records for this zone in CloudFlare. This action cannot be undone on CloudFlare.\n\nA local copy will be kept so you can use Restore.\n\nContinue?'; + if (!$window.confirm(msg1)) return; + var msg2 = 'Type the zone name below to confirm:\n\n' + zone; + var typed = $window.prompt(msg2); + if (typed === null) return; + if (typed.trim() !== zone) { + new PNotify({ title: 'Cancelled', text: 'Zone name did not match. No records were deleted.', type: 'warning' }); + return; + } + $scope.clearAllLoading = true; + url = '/dns/clearAllDNSRecordsCloudFlare'; + var data = { selectedZone: zone }; + var config = { headers: { 'X-CSRFToken': getCookie('csrftoken') } }; + $http.post(url, data, config).then(function (response) { + $scope.clearAllLoading = false; + if (response.data.delete_status === 1 && response.data.deleted_records) { + $scope.cfDeletedBackup[zone] = response.data.deleted_records; + $scope.canNotFetchRecords = true; + $scope.recordsFetched = false; + $scope.recordDeleted = false; + populateCurrentRecords(); + new PNotify({ title: 'Done', text: 'All DNS records were deleted. Use Restore to undo.', type: 'success' }); + } else { + $scope.errorMessage = response.data.error_message || 'Clear all failed'; + new PNotify({ title: 'Error', text: $scope.errorMessage, type: 'error' }); + } + }, function () { + $scope.clearAllLoading = false; + new PNotify({ title: 'Error', text: 'Could not connect to server.', type: 'error' }); + }); + }; + + $scope.restoreFromBackup = function () { + var zone = $scope.selectedZone; + var list = $scope.cfDeletedBackup[zone]; + if (!zone || !list || list.length === 0) return; + $scope.restoreLoading = true; + url = '/dns/importDNSRecordsCloudFlare'; + var data = { selectedZone: zone, records: list }; + var config = { headers: { 'X-CSRFToken': getCookie('csrftoken') } }; + $http.post(url, data, config).then(function (response) { + $scope.restoreLoading = false; + if (response.data.import_status === 1) { + $scope.cfDeletedBackup[zone] = []; + populateCurrentRecords(); + var failed = response.data.failed || []; + var msg = response.data.imported + ' record(s) restored.'; + if (failed.length) msg += ' ' + failed.length + ' failed.'; + new PNotify({ title: 'Restore done', text: msg, type: failed.length ? 'warning' : 'success' }); + } else { + new PNotify({ title: 'Error', text: response.data.error_message || 'Restore failed', type: 'error' }); + } + }, function () { + $scope.restoreLoading = false; + new PNotify({ title: 'Error', text: 'Could not connect to server.', type: 'error' }); + }); + }; + + $scope.exportRecords = function () { + var zone = $scope.selectedZone; + if (!zone) return; + $scope.exportLoading = true; + url = '/dns/getExportRecordsCloudFlare'; + var data = { selectedZone: zone }; + var config = { headers: { 'X-CSRFToken': getCookie('csrftoken') } }; + $http.post(url, data, config).then(function (response) { + $scope.exportLoading = false; + if (response.data.fetchStatus === 1 && response.data.data) { + var arr = typeof response.data.data === 'string' ? JSON.parse(response.data.data) : response.data.data; + var blob = new Blob([JSON.stringify(arr, null, 2)], { type: 'application/json' }); + var a = document.createElement('a'); + a.href = (window.URL || window.webkitURL).createObjectURL(blob); + a.download = 'dns-records-' + zone.replace(/\./g, '-') + '.json'; + a.click(); + if (a.href) (window.URL || window.webkitURL).revokeObjectURL(a.href); + new PNotify({ title: 'Export done', text: 'DNS records downloaded.', type: 'success' }); + } else { + new PNotify({ title: 'Error', text: response.data.error_message || 'Export failed', type: 'error' }); + } + }, function () { + $scope.exportLoading = false; + new PNotify({ title: 'Error', text: 'Could not connect to server.', type: 'error' }); + }); + }; + + $scope.onImportFile = function (files) { + if (!files || !files.length) return; + var zone = $scope.selectedZone; + if (!zone) { + new PNotify({ title: 'Error', text: 'Select a zone first.', type: 'error' }); + return; + } + var file = files[0]; + var reader = new FileReader(); + reader.onload = function (e) { + var text = e.target && e.target.result; + if (!text) { + new PNotify({ title: 'Error', text: 'Could not read file.', type: 'error' }); + return; + } + var arr; + try { + arr = JSON.parse(text); + } catch (err) { + new PNotify({ title: 'Error', text: 'Invalid JSON: ' + (err.message || ''), type: 'error' }); + return; + } + if (!Array.isArray(arr)) { + if (arr && Array.isArray(arr.records)) arr = arr.records; + else if (arr && arr.data) arr = Array.isArray(arr.data) ? arr.data : [arr.data]; + else arr = [arr]; + } + url = '/dns/importDNSRecordsCloudFlare'; + var data = { selectedZone: zone, records: arr }; + var config = { headers: { 'X-CSRFToken': getCookie('csrftoken') } }; + $http.post(url, data, config).then(function (response) { + if (response.data.import_status === 1) { + populateCurrentRecords(); + var failed = response.data.failed || []; + var msg = response.data.imported + ' record(s) imported.'; + if (failed.length) msg += ' ' + failed.length + ' failed.'; + new PNotify({ title: 'Import done', text: msg, type: failed.length ? 'warning' : 'success' }); + } else { + new PNotify({ title: 'Error', text: response.data.error_message || 'Import failed', type: 'error' }); + } + }, function () { + new PNotify({ title: 'Error', text: 'Could not connect to server.', type: 'error' }); + }); + }; + reader.readAsText(file, 'UTF-8'); + }; + + $scope.checkStaleRecords = function () { + var zone = $scope.selectedZone; + if (!zone) return; + $scope.staleLoading = true; + url = '/dns/getStaleDNSRecordsCloudFlare'; + var data = { selectedZone: zone }; + var config = { headers: { 'X-CSRFToken': getCookie('csrftoken') } }; + $http.post(url, data, config).then(function (response) { + $scope.staleLoading = false; + if (response.data.fetchStatus === 1) { + $scope.staleRecords = response.data.stale_records || []; + $scope.staleModalVisible = true; + } else { + new PNotify({ title: 'Error', text: response.data.error_message || 'Could not fetch stale records', type: 'error' }); + } + }, function () { + $scope.staleLoading = false; + new PNotify({ title: 'Error', text: 'Could not connect to server.', type: 'error' }); + }); + }; + + $scope.closeStaleModal = function () { + $scope.staleModalVisible = false; + $scope.staleRecords = []; + }; + + $scope.removeStaleRecords = function () { + if (!$scope.staleRecords || $scope.staleRecords.length === 0) return; + var zone = $scope.selectedZone; + var msg = 'Remove ' + $scope.staleRecords.length + ' orphan DNS record(s)? A local copy will be kept for Restore.'; + if (!$window.confirm(msg)) return; + var ids = $scope.staleRecords.map(function (r) { return r.id; }); + url = '/dns/removeStaleDNSRecordsCloudFlare'; + var data = { selectedZone: zone, ids: ids }; + var config = { headers: { 'X-CSRFToken': getCookie('csrftoken') } }; + $http.post(url, data, config).then(function (response) { + if (response.data.delete_status === 1 && response.data.deleted_records) { + if (!$scope.cfDeletedBackup[zone]) $scope.cfDeletedBackup[zone] = []; + $scope.cfDeletedBackup[zone] = $scope.cfDeletedBackup[zone].concat(response.data.deleted_records); + $scope.closeStaleModal(); + populateCurrentRecords(); + new PNotify({ title: 'Done', text: response.data.deleted_records.length + ' orphan record(s) removed. Use Restore to undo.', type: 'success' }); + } else { + new PNotify({ title: 'Error', text: response.data.error_message || 'Remove failed', type: 'error' }); + } + }, function () { + new PNotify({ title: 'Error', text: 'Could not connect to server.', type: 'error' }); + }); + }; + $scope.dnsTypeList = ['A', 'AAAA', 'CNAME', 'MX', 'TXT', 'NS', 'SOA', 'SRV', 'CAA', 'SPF', 'DNSKEY', 'CDNSKEY', 'HTTPS', 'SVCB', 'URI', 'LOC', 'NAPTR', 'SMIMEA', 'SSHFP', 'TLSA', 'PTR']; $scope.getTypeOptions = function (record) { var list = angular.copy($scope.dnsTypeList);