feat: add Cloudflare DNS integration

Closes #31
This commit is contained in:
Christian Krakau-Louis
2026-05-23 01:13:06 +02:00
parent a060bc59c2
commit 521dbc92d0
13 changed files with 1644 additions and 46 deletions
+106 -1
View File
@@ -189,7 +189,65 @@
<input type="text" x-model="s['cloudflare.zone_id']"
class="input input-bordered w-full"
placeholder="xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" />
<label class="label"><span class="label-text-alt text-muted-foreground">Found on the Cloudflare dashboard overview for your domain</span></label>
<label class="label"><span class="label-text-alt text-muted-foreground">Optional. Leave blank to discover all zones available to the token.</span></label>
</div>
<div class="border-t border-border pt-4 space-y-4">
<div class="flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between">
<div>
<h3 class="text-sm font-semibold">Domain Discovery</h3>
<p class="text-sm text-muted-foreground">Find Cloudflare zones and import them into DMARQ.</p>
</div>
<div class="flex flex-col gap-2 sm:flex-row">
<button type="button" class="btn btn-outline btn-sm" :disabled="saving || loadingCfZones" @click="discoverCloudflareZones()">
<template x-if="!loadingCfZones">
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none"
stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="mr-2">
<path d="M3 12a9 9 0 1 0 9-9 9.75 9.75 0 0 0-6.74 2.74L3 8"></path>
<path d="M3 3v5h5"></path>
</svg>
</template>
<template x-if="loadingCfZones"><span class="loading loading-spinner loading-xs mr-2"></span></template>
Discover
</button>
<button type="button" class="btn btn-default btn-sm" :disabled="saving || importingCfZones || cfZones.length === 0" @click="importCloudflareZones()">
<template x-if="!importingCfZones">
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none"
stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="mr-2">
<path d="M12 5v14"></path>
<path d="m19 12-7 7-7-7"></path>
</svg>
</template>
<template x-if="importingCfZones"><span class="loading loading-spinner loading-xs mr-2"></span></template>
Import New
</button>
</div>
</div>
<template x-if="cfZones.length > 0">
<div class="overflow-x-auto rounded-md border border-border">
<table class="table table-sm">
<thead>
<tr>
<th>Domain</th>
<th>Status</th>
<th>Account</th>
<th>Imported</th>
</tr>
</thead>
<tbody>
<template x-for="zone in cfZones" :key="zone.id">
<tr>
<td class="font-medium" x-text="zone.name"></td>
<td x-text="zone.status || 'unknown'"></td>
<td x-text="zone.account_name || ''"></td>
<td>
<span class="badge" :class="zone.imported ? 'badge-success' : 'badge-outline'" x-text="zone.imported ? 'Yes' : 'No'"></span>
</td>
</tr>
</template>
</tbody>
</table>
</div>
</template>
</div>
<div class="flex justify-end">
<button type="submit" class="btn btn-default btn-md" :disabled="saving">
@@ -577,6 +635,9 @@ function settingsApp() {
alertHistory: [],
loadingConfigAudit: false,
configAudit: [],
loadingCfZones: false,
importingCfZones: false,
cfZones: [],
showCfToken: false,
// Session cookie is sent automatically by the browser (httpOnly, same-origin).
@@ -804,6 +865,50 @@ function settingsApp() {
}
},
async discoverCloudflareZones() {
this.loadingCfZones = true;
try {
await this.saveCategory('cloudflare');
const res = await fetch('/api/v1/domains/cloudflare/discover', {
headers: this.apiHeaders(),
});
const data = await res.json().catch(() => ({}));
if (!res.ok) {
this.showFlash('Cloudflare discovery failed: ' + (data.detail || res.statusText), false);
} else {
this.cfZones = data || [];
this.showFlash(`${this.cfZones.length} Cloudflare zone${this.cfZones.length === 1 ? '' : 's'} found.`, true);
}
} catch (err) {
this.showFlash('Error discovering Cloudflare zones: ' + err.message, false);
} finally {
this.loadingCfZones = false;
}
},
async importCloudflareZones() {
this.importingCfZones = true;
try {
const domains = this.cfZones.filter(z => !z.imported).map(z => z.name);
const res = await fetch('/api/v1/domains/cloudflare/import', {
method: 'POST',
headers: this.apiHeaders(),
body: JSON.stringify({ domains }),
});
const data = await res.json().catch(() => ({}));
if (!res.ok) {
this.showFlash('Cloudflare import failed: ' + (data.detail || res.statusText), false);
} else {
await this.discoverCloudflareZones();
this.showFlash(`${data.imported.length} domain${data.imported.length === 1 ? '' : 's'} imported.`, true);
}
} catch (err) {
this.showFlash('Error importing Cloudflare zones: ' + err.message, false);
} finally {
this.importingCfZones = false;
}
},
showFlash(msg, ok) {
this.flashMsg = msg;
this.flashOk = ok;