Skip to content

Instantly share code, notes, and snippets.

@pabloko
Last active July 3, 2025 09:49
Show Gist options
  • Save pabloko/2914470a8957c125e838bea1d330d2e8 to your computer and use it in GitHub Desktop.
Save pabloko/2914470a8957c125e838bea1d330d2e8 to your computer and use it in GitHub Desktop.
CloudFlare DNS UI/api

CloudFlare DNS UI/api

A very simple, one-file, cloudflare dns manager.

It uses an api token and can enumerate the zones (domains) list and edit its DNS records, create new records, and clear the cache.

The token must have access to edit Zone.DNS and optionally purge cache.

The nodejs server is needed to reverse-proxy cloudflare api and allow CORS requests.

chrome_Ryak0ZNCVT

const PORT = 3000
const http = require('http');
const request = require('request');
const path = require('path');
const fs = require('fs');
const src = fs.readFileSync(path.basename(__filename));
const html = src.toString().split('/'+'*TEMPLATE')[1].split('TEMPLATE*'+'/')[0];
const server = http.createServer((req, res) => {
// CORS fix
if (req.method === 'OPTIONS') {
res.writeHead(200, {
'Access-Control-Allow-Origin': '*',
'Access-Control-Allow-Methods': 'GET, POST, PATCH, DELETE',
'Access-Control-Allow-Headers': 'Authorization, Content-Type',
'Access-Control-Max-Age': '86400'
});
res.end();
return;
}
// Serve template
if (req.url === '/') {
res.writeHead(200, { 'Content-Type': 'text/html' });
res.end(html);
return;
}
// Disallow anything not in api
if (!req.url.startsWith('/api/')) {
res.writeHead(404, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ success: false, errors: [{ code: 404, message: 'Not found' }] }));
return;
}
console.log(req.method, req.url);
const apiUrl = `https://api.cloudflare.com/client/v4${req.url.replace('/api', '')}`;
const headers = { 'Authorization': req.headers['authorization'], 'Content-Type': 'application/json' };
// Proxy to Cloudflare API
let body = '';
req.on('data', chunk => body += chunk);
req.on('end', () => {
request({
method: req.method,
url: apiUrl,
headers,
body: body || undefined,
json: true
}, (error, response, body) => {
console.log(error, body);
res.writeHead(response.statusCode, { 'Content-Type': 'application/json', 'Access-Control-Allow-Origin': '*' });
res.end(JSON.stringify(body));
});
});
});
server.listen(PORT, () => console.log('Proxy running on http://localhost:'+PORT));
/*TEMPLATE
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Cloudflare DNS Manager</title>
<script src="https://cdn.tailwindcss.com"></script>
</head>
<body class="bg-gray-100 font-sans">
<div class="container mx-auto p-4">
<h1 class="text-2xl font-bold mb-4">Cloudflare DNS Manager</h1>
<div class="mb-4 flex space-x-2">
<input id="apiToken" type="password" placeholder="API Token" class="border p-2 rounded w-1/3">
<button onclick="fetchZones()" class="bg-blue-500 text-white px-4 py-2 rounded hover:bg-blue-600">Load&nbsp;Domains</button>
<select id="zoneSelect" onchange="fetchRecords()" class="border p-2 rounded w-1/2" disabled>
<option value="">Select a domain</option>
</select>
<button onclick="clearCache()" class="bg-yellow-500 text-white px-4 py-2 rounded hover:bg-yellow-600">Clear&nbsp;Cache</button>
</div>
<div id="errorBox" class="hidden bg-red-100 border border-red-400 text-red-700 px-4 py-3 rounded mb-4"></div>
<table id="dnsTable" class="w-full border-collapse hidden">
<thead>
<tr class="bg-gray-200">
<th class="border p-2">Type</th>
<th class="border p-2">Name</th>
<th class="border p-2">Content</th>
<th class="border p-2" style="width: 100px;">Priority</th>
<th class="border p-2" style="width: 100px;">TTL</th>
<th class="border p-2" style="width: 60px;">Proxy</th>
<th class="border p-2">Actions</th>
</tr>
</thead>
<tbody id="dnsTableBody"></tbody>
</table>
</div>
<script>
const recordTypes = ['A', 'AAAA', 'CNAME', 'MX', 'NS', 'TXT', 'SRV'];
const proxyableTypes = ['A', 'AAAA', 'CNAME'];
const proxyUrl = '/api/';
let currentDomain = '';
function showError(error) {
const errorBox = document.getElementById('errorBox');
errorBox.innerHTML = `Error ${error.code}: ${error.message}`;
errorBox.classList.remove('hidden');
setTimeout(() => errorBox.classList.add('hidden'), 5000);
}
async function fetchZones() {
const apiToken = document.getElementById('apiToken').value;
if (!apiToken) {
showError({ code: 0, message: 'Please enter API Token' });
return;
}
try {
const response = await fetch(`${proxyUrl}zones`, {
headers: { 'Authorization': `Bearer ${apiToken}`, 'Content-Type': 'application/json' }
});
const data = await response.json();
if (!data.success) {
showError(data.errors[0]);
return;
}
populateZoneSelect(data.result);
} catch (error) {
showError({ code: 0, message: 'Network error: ' + error.message });
}
}
function populateZoneSelect(zones) {
const zoneSelect = document.getElementById('zoneSelect');
zoneSelect.innerHTML = '<option value="">Select a domain</option>';
zones.forEach(zone => {
const option = document.createElement('option');
option.value = zone.id;
option.textContent = zone.name;
option.dataset.domain = zone.name;
zoneSelect.appendChild(option);
});
zoneSelect.disabled = false;
}
async function fetchRecords() {
const apiToken = document.getElementById('apiToken').value;
const zoneSelect = document.getElementById('zoneSelect');
const zoneId = zoneSelect.value;
currentDomain = zoneSelect.options[zoneSelect.selectedIndex]?.dataset.domain || '';
if (!zoneId) {
document.getElementById('dnsTable').classList.add('hidden');
return;
}
try {
const response = await fetch(`${proxyUrl}zones/${zoneId}/dns_records`, {
headers: { 'Authorization': `Bearer ${apiToken}`, 'Content-Type': 'application/json' }
});
const data = await response.json();
if (!data.success) {
showError(data.errors[0]);
return;
}
renderTable(data.result);
} catch (error) {
showError({ code: 0, message: 'Network error: ' + error.message });
}
}
async function clearCache() {
const apiToken = document.getElementById('apiToken').value;
const zoneId = document.getElementById('zoneSelect').value;
if (!zoneId) {
showError({ code: 0, message: 'Please select a domain' });
return;
}
try {
const response = await fetch(`${proxyUrl}zones/${zoneId}/purge_cache`, {
method: 'POST',
headers: { 'Authorization': `Bearer ${apiToken}`, 'Content-Type': 'application/json' },
body: JSON.stringify({ purge_everything: true })
});
const data = await response.json();
if (!data.success) {
showError(data.errors[0]);
return;
}
showError({ code: 200, message: 'Cache cleared successfully' });
} catch (error) {
showError({ code: 0, message: 'Network error: ' + error.message });
}
}
function renderTable(records) {
const tbody = document.getElementById('dnsTableBody');
tbody.innerHTML = '';
document.getElementById('dnsTable').classList.remove('hidden');
records.forEach(record => {
const row = createRow(record);
tbody.appendChild(row);
});
const newRow = createRow({ id: '', type: 'A', name: '', content: '', priority: '', ttl: 1, proxied: false });
newRow.dataset.newRecord = 'true';
tbody.appendChild(newRow);
}
function createRow(record) {
const name = record.name === currentDomain ? '@' : record.name.replace(`.${currentDomain}`, '');
const priority = record.priority != null ? record.priority : '';
const row = document.createElement('tr');
row.innerHTML = `
<td class="border p-2">
<select class="border p-1 w-full" onchange="toggleProxyAndPriority(this)">
${recordTypes.map(type => `<option value="${type}" ${record.type === type ? 'selected' : ''}>${type}</option>`).join('')}
</select>
</td>
<td class="border p-2"><input type="text" value="${name}" class="border p-1 w-full"></td>
<td class="border p-2"><input type="text" value='${record.content || ''}' class="border p-1 w-full"></td>
<td class="border p-2"><input type="number" value="${priority}" min="0" max="65535" class="border p-1 w-full priority-input" ${record.type === 'MX' ? '' : 'disabled'}></td>
<td class="border p-2"><input type="number" value="${record.ttl || 3600}" min="1" class="border p-1 w-full"></td>
<td class="border p-2 text-center">
<input type="checkbox" ${record.proxied ? 'checked' : ''} ${proxyableTypes.includes(record.type) ? '' : 'disabled'} class="proxy-checkbox">
</td>
<td class="border p-2">
<button onclick="saveRecord(this)" class="bg-green-500 text-white px-2 py-1 rounded hover:bg-green-600">Save</button>
${record.id ? `<button onclick="deleteRecord(this, '${record.id}')" class="bg-red-500 text-white px-2 py-1 rounded hover:bg-red-600 ml-2">Delete</button>` : ''}
</td>
`;
row.dataset.id = record.id;
return row;
}
function toggleProxyAndPriority(select) {
const row = select.closest('tr');
const proxyCheckbox = row.querySelector('.proxy-checkbox');
const priorityInput = row.querySelector('.priority-input');
proxyCheckbox.disabled = !proxyableTypes.includes(select.value);
priorityInput.disabled = select.value !== 'MX';
if (proxyCheckbox.disabled) proxyCheckbox.checked = false;
if (priorityInput.disabled) priorityInput.value = '';
}
async function saveRecord(button) {
const row = button.closest('tr');
const apiToken = document.getElementById('apiToken').value;
const zoneId = document.getElementById('zoneSelect').value;
const isNew = row.dataset.newRecord === 'true';
const recordId = row.dataset.id;
const type = row.querySelector('select').value;
let name = row.querySelector('input[type="text"]').value;
const content = row.querySelectorAll('input[type="text"]')[1].value;
const priority = parseInt(row.querySelector('.priority-input').value) || undefined;
const ttl = parseInt(row.querySelectorAll('input[type="number"]')[1].value);
const proxied = row.querySelector('.proxy-checkbox').checked;
// Append domain to name, convert '@' to domain
name = name === '@' ? currentDomain : name ? `${name}.${currentDomain}` : currentDomain;
try {
const url = `${proxyUrl}zones/${zoneId}/dns_records${isNew ? '' : `/${recordId}`}`;
const method = isNew ? 'POST' : 'PATCH';
const body = JSON.stringify({ type, name, content, ttl, proxied, ...(type === 'MX' && priority ? { priority } : {}) });
const response = await fetch(url, {
method,
headers: { 'Authorization': `Bearer ${apiToken}`, 'Content-Type': 'application/json' },
body
});
const data = await response.json();
if (!data.success) {
showError(data.errors[0]);
return;
}
fetchRecords();
} catch (error) {
showError({ code: 0, message: 'Network error: ' + error.message });
}
}
async function deleteRecord(button, recordId) {
const apiToken = document.getElementById('apiToken').value;
const zoneId = document.getElementById('zoneSelect').value;
try {
const response = await fetch(`${proxyUrl}zones/${zoneId}/dns_records/${recordId}`, {
method: 'DELETE',
headers: { 'Authorization': `Bearer ${apiToken}`, 'Content-Type': 'application/json' }
});
const data = await response.json();
if (!data.success) {
showError(data.errors[0]);
return;
}
fetchRecords();
} catch (error) {
showError({ code: 0, message: 'Network error: ' + error.message });
}
}
</script>
</body>
</html>
TEMPLATE*/
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment