171 lines
4.9 KiB
JavaScript
171 lines
4.9 KiB
JavaScript
/**
|
|
* Simple JSON file-based cache for DNS records.
|
|
* No native dependencies — works on any Node version.
|
|
*
|
|
* Structure of dns-cache.json:
|
|
* {
|
|
* "cloudflare": {
|
|
* "zone123": {
|
|
* "synced_at": "2024-01-01T00:00:00.000Z",
|
|
* "records": [ { id, type, name, content, ttl, priority }, ... ]
|
|
* }
|
|
* }
|
|
* }
|
|
*/
|
|
|
|
const fs = require('fs');
|
|
const path = require('path');
|
|
|
|
const CACHE_PATH = process.env.DB_PATH || path.join(__dirname, '..', 'dns-cache.json');
|
|
|
|
// ─── Load / save ─────────────────────────────────────────────────────────────
|
|
|
|
function load() {
|
|
try {
|
|
return JSON.parse(fs.readFileSync(CACHE_PATH, 'utf8'));
|
|
} catch {
|
|
return {};
|
|
}
|
|
}
|
|
|
|
function save(data) {
|
|
fs.writeFileSync(CACHE_PATH, JSON.stringify(data, null, 2), 'utf8');
|
|
}
|
|
|
|
// ─── Public API ──────────────────────────────────────────────────────────────
|
|
|
|
function getRecords(provider, zoneId) {
|
|
const data = load();
|
|
return data[provider]?.[zoneId]?.records ?? [];
|
|
}
|
|
|
|
function getSyncedAt(provider, zoneId) {
|
|
const data = load();
|
|
return data[provider]?.[zoneId]?.synced_at ?? null;
|
|
}
|
|
|
|
// Returns { [zoneId]: { synced_at, record_count } } for all cached zones of a provider
|
|
function getProviderSyncStatus(provider) {
|
|
const data = load();
|
|
const zones = data[provider] ?? {};
|
|
const result = {};
|
|
for (const [zoneId, zone] of Object.entries(zones)) {
|
|
result[zoneId] = {
|
|
synced_at: zone.synced_at ?? null,
|
|
record_count: (zone.records ?? []).length,
|
|
};
|
|
}
|
|
return result;
|
|
}
|
|
|
|
function getZoneName(provider, zoneId) {
|
|
const data = load();
|
|
return data[provider]?.[zoneId]?.zone_name ?? zoneId;
|
|
}
|
|
|
|
function replaceZoneRecords(provider, zoneId, records, zoneName) {
|
|
const data = load();
|
|
if (!data[provider]) data[provider] = {};
|
|
data[provider][zoneId] = {
|
|
zone_name: zoneName ?? data[provider]?.[zoneId]?.zone_name ?? zoneId,
|
|
synced_at: new Date().toISOString(),
|
|
records: records.map(r => ({
|
|
id: r.id,
|
|
type: r.type,
|
|
name: r.name,
|
|
content: r.content,
|
|
ttl: r.ttl ?? null,
|
|
priority: r.priority ?? null,
|
|
})),
|
|
};
|
|
save(data);
|
|
}
|
|
|
|
function upsertRecord(provider, zoneId, record) {
|
|
const data = load();
|
|
if (!data[provider]) data[provider] = {};
|
|
if (!data[provider][zoneId]) data[provider][zoneId] = { synced_at: null, records: [] };
|
|
|
|
const records = data[provider][zoneId].records;
|
|
const idx = records.findIndex(r => r.id === record.id);
|
|
const entry = {
|
|
id: record.id,
|
|
type: record.type,
|
|
name: record.name,
|
|
content: record.content,
|
|
ttl: record.ttl ?? null,
|
|
priority: record.priority ?? null,
|
|
};
|
|
|
|
if (idx >= 0) records[idx] = entry;
|
|
else records.push(entry);
|
|
|
|
save(data);
|
|
}
|
|
|
|
function deleteRecord(provider, zoneId, recordId) {
|
|
const data = load();
|
|
const zone = data[provider]?.[zoneId];
|
|
if (!zone) return;
|
|
zone.records = zone.records.filter(r => r.id !== recordId);
|
|
save(data);
|
|
}
|
|
|
|
/**
|
|
* Returns all cached zones across all providers as a flat list.
|
|
* Groups duplicate zone names so you can spot the same domain on multiple providers.
|
|
*/
|
|
function getAllZones() {
|
|
const data = load();
|
|
const byName = {};
|
|
|
|
for (const [provider, zones] of Object.entries(data)) {
|
|
for (const [zoneId, zone] of Object.entries(zones)) {
|
|
const name = zone.zone_name ?? zoneId;
|
|
if (!byName[name]) byName[name] = [];
|
|
byName[name].push({
|
|
provider,
|
|
zone_id: zoneId,
|
|
record_count: (zone.records ?? []).length,
|
|
synced_at: zone.synced_at ?? null,
|
|
});
|
|
}
|
|
}
|
|
|
|
return Object.entries(byName)
|
|
.map(([name, entries]) => ({ name, entries, duplicate: entries.length > 1 }))
|
|
.sort((a, b) => a.name.localeCompare(b.name));
|
|
}
|
|
|
|
function getStats() {
|
|
const data = load();
|
|
const typeCounts = {};
|
|
let totalZones = 0;
|
|
let zonesWithMx = 0;
|
|
const perProvider = {};
|
|
|
|
for (const [provider, zones] of Object.entries(data)) {
|
|
const zoneCount = Object.keys(zones).length;
|
|
totalZones += zoneCount;
|
|
perProvider[provider] = { zones: zoneCount };
|
|
|
|
for (const zone of Object.values(zones)) {
|
|
const records = zone.records ?? [];
|
|
const hasMx = records.some(r => r.type === 'MX');
|
|
if (hasMx) zonesWithMx++;
|
|
|
|
for (const r of records) {
|
|
typeCounts[r.type] = (typeCounts[r.type] || 0) + 1;
|
|
}
|
|
}
|
|
}
|
|
|
|
const recordTypes = Object.entries(typeCounts)
|
|
.map(([type, count]) => ({ type, count }))
|
|
.sort((a, b) => b.count - a.count);
|
|
|
|
return { totalZones, zonesWithMx, recordTypes, perProvider };
|
|
}
|
|
|
|
module.exports = { getRecords, getSyncedAt, getZoneName, getProviderSyncStatus, getAllZones, replaceZoneRecords, upsertRecord, deleteRecord, getStats };
|