mirror of
https://github.com/usmannasir/cyberpanel.git
synced 2026-02-27 08:50:46 +01:00
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:
@@ -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:
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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'),
|
||||
]
|
||||
|
||||
45
dns/views.py
45
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)
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user