DNS CloudFlare: delete confirmation, clear all, restore, export/import, orphan check

- Delete record: confirmation dialog and local backup before delete
- Clear all DNS records: double confirmation (zone name), local backup, Restore button
- Export/Import DNS records (JSON) for zone
- Check orphan DNS: find A/AAAA/CNAME for hostnames no longer in panel, remove with backup
- Backend: getExportRecordsCloudFlare, clearAllDNSRecordsCloudFlare, importDNSRecordsCloudFlare, getStaleDNSRecordsCloudFlare, removeStaleDNSRecordsCloudFlare
This commit is contained in:
master3395
2026-02-17 01:43:01 +01:00
parent bb8454d3f0
commit 90dab2caf1
7 changed files with 1184 additions and 7 deletions

View File

@@ -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:

View File

@@ -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;

View File

@@ -959,6 +959,25 @@
{% trans "DNS Records" %}
</h4>
<div class="mb-3" ng-if="selectedZone" style="display: flex; flex-wrap: wrap; gap: 8px; align-items: center;">
<button type="button" ng-click="exportRecords()" class="btn-secondary" ng-disabled="exportLoading">
<i class="fas fa-download"></i> {% trans "Export" %}
</button>
<label class="btn-secondary" style="margin-bottom: 0; cursor: pointer;">
<i class="fas fa-upload"></i> {% trans "Import" %}
<input type="file" accept=".json,application/json" style="display: none;" cf-import-file>
</label>
<button type="button" ng-click="confirmClearAll()" class="btn-secondary" style="border-color: #ef4444; color: #ef4444;" ng-disabled="clearAllLoading">
<i class="fas fa-trash-alt"></i> {% trans "Clear all DNS records" %}
</button>
<button type="button" ng-click="restoreFromBackup()" class="btn-primary" ng-show="hasBackupForZone()" ng-disabled="restoreLoading">
<i class="fas fa-undo"></i> {% trans "Restore" %}
</button>
<button type="button" ng-click="checkStaleRecords()" class="btn-secondary" ng-disabled="staleLoading" title="{% trans 'Find DNS records for subdomains that no longer exist in the panel' %}">
<i class="fas fa-broom"></i> {% trans "Check orphan DNS" %}
</button>
</div>
<div class="dns-search-wrap mb-3" ng-if="!loadingRecords && records.length > 0">
<span class="dns-search-icon-left"><i class="fas fa-search"></i></span>
<input type="text" class="form-control dns-search-input" ng-model="dnsSearch.filter" placeholder="{% trans 'Search name, type, value...' %}" title="{% trans 'Search through all records' %}">
@@ -1040,7 +1059,7 @@
<td style="text-align: center;">
<i class="fas fa-trash delete-icon"
style="color: #ef4444; cursor: pointer;"
ng-click="deleteRecord(record.id)"
ng-click="confirmDeleteRecord(record)"
title="{% trans 'Delete Record' %}"></i>
</td>
</tr>
@@ -1128,6 +1147,46 @@
</div>
</div>
<!-- Stale / Orphan DNS Records Modal -->
<div class="edit-record-overlay" ng-show="staleModalVisible" ng-click="closeStaleModal()">
<div class="edit-record-modal" ng-click="$event.stopPropagation()" style="max-width: 600px;">
<h4 class="mb-4" style="color: var(--text-primary, #1e293b); font-weight: 600;">
<i class="fas fa-broom"></i> {% trans "Orphan / Stale DNS Records" %}
</h4>
<p class="text-muted small mb-3">{% trans "Records below point to subdomains or hostnames that no longer exist in the panel. You can remove them to clean up CloudFlare." %}</p>
<div ng-if="staleRecords.length === 0" class="alert alert-success">
<i class="fas fa-check-circle"></i> {% trans "No orphan records found." %}
</div>
<div ng-if="staleRecords.length > 0">
<table class="records-table activity-table" style="margin-bottom: 1rem;">
<thead>
<tr>
<th>{% trans "Name" %}</th>
<th>{% trans "Type" %}</th>
<th>{% trans "Value" %}</th>
</tr>
</thead>
<tbody>
<tr ng-repeat="r in staleRecords">
<td><strong ng-bind="r.name"></strong></td>
<td ng-bind="r.type"></td>
<td class="cell-value" ng-bind="r.content" title="{{ r.content }}"></td>
</tr>
</tbody>
</table>
<div style="display: flex; gap: 10px; justify-content: flex-end; margin-top: 1rem;">
<button type="button" class="btn-secondary" ng-click="closeStaleModal()">{% trans "Close" %}</button>
<button type="button" class="btn-primary" style="border-color: #ef4444; color: #ef4444;" ng-click="removeStaleRecords()">
<i class="fas fa-trash-alt"></i> {% trans "Remove all orphan records" %}
</button>
</div>
</div>
<div ng-if="staleRecords.length === 0" style="margin-top: 1rem;">
<button type="button" class="btn-secondary" ng-click="closeStaleModal()">{% trans "Close" %}</button>
</div>
</div>
</div>
<!-- Alert Messages -->
<div style="margin-top: 2rem;">
<div ng-hide="canNotFetchRecords" class="alert alert-danger">

View File

@@ -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'),
]

View File

@@ -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)

View File

@@ -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);

View File

@@ -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);