217 lines
10 KiB
HTML
217 lines
10 KiB
HTML
{% extends "layouts/base.html" %}
|
|
{% from "components/ui/card.html" import card, card_header, card_title, card_description, card_content, card_footer %}
|
|
{% from "components/ui/button.html" import button, button_link %}
|
|
{% from "components/ui/table.html" import table, thead, tbody, tr, th, td %}
|
|
|
|
{% block title %}DMARQ - Domains{% endblock %}
|
|
|
|
{% block content %}
|
|
<div class="container mx-auto py-4" x-data="domainsApp()">
|
|
<h1 class="text-2xl font-bold mb-6">Domain Management</h1>
|
|
|
|
{% if error %}
|
|
<div class="bg-red-100 border-l-4 border-red-500 text-red-700 p-4 mb-6" role="alert">
|
|
<p>{{ error }}</p>
|
|
</div>
|
|
{% endif %}
|
|
|
|
<!-- Domain List -->
|
|
<div class="mb-8">
|
|
{% call card() %}
|
|
{% call card_header() %}
|
|
<div class="flex items-center justify-between">
|
|
{% call card_title() %}Monitored Domains{% endcall %}
|
|
<button type="button" class="btn btn-outline btn-sm" @click="openCreate = true">
|
|
<span class="mr-1">+</span> Add Domain
|
|
</button>
|
|
</div>
|
|
{% call card_description() %}
|
|
Domains currently being monitored for DMARC compliance
|
|
{% endcall %}
|
|
{% endcall %}
|
|
{% call card_content() %}
|
|
{% call table() %}
|
|
{% call thead() %}
|
|
{% call tr() %}
|
|
{% call th() %}Domain{% endcall %}
|
|
{% call th() %}DMARC Status{% endcall %}
|
|
{% call th() %}SPF Status{% endcall %}
|
|
{% call th() %}DKIM Status{% endcall %}
|
|
{% call th("text-right") %}Actions{% endcall %}
|
|
{% endcall %}
|
|
{% endcall %}
|
|
{% call tbody() %}
|
|
<template x-if="domains.length === 0">
|
|
{% call tr() %}
|
|
{% call td(colspan="5", class="text-center") %}
|
|
<p class="py-4 text-muted-foreground">No domains found. Add a domain to get started.</p>
|
|
{% endcall %}
|
|
{% endcall %}
|
|
</template>
|
|
<template x-for="(domain, index) in domains" :key="index">
|
|
{% call tr() %}
|
|
{% call td() %}
|
|
<div class="font-medium" x-text="domain.name"></div>
|
|
{% endcall %}
|
|
{% call td() %}
|
|
<div class="flex items-center">
|
|
<span class="w-2 h-2 rounded-full"
|
|
:class="domain.dmarc_status ? 'bg-green-500' : 'bg-red-500'"></span>
|
|
<span class="ml-2" x-text="domain.dmarc_policy || 'Not configured'"></span>
|
|
</div>
|
|
{% endcall %}
|
|
{% call td() %}
|
|
<div class="flex items-center">
|
|
<span class="w-2 h-2 rounded-full"
|
|
:class="domain.spf_status ? 'bg-green-500' : 'bg-red-500'"></span>
|
|
<span class="ml-2" x-text="domain.spf_status ? 'Configured' : 'Missing'"></span>
|
|
</div>
|
|
{% endcall %}
|
|
{% call td() %}
|
|
<div class="flex items-center">
|
|
<span class="w-2 h-2 rounded-full"
|
|
:class="domain.dkim_status ? 'bg-green-500' : 'bg-red-500'"></span>
|
|
<span class="ml-2" x-text="domain.dkim_status ? 'Configured' : 'Missing'"></span>
|
|
</div>
|
|
{% endcall %}
|
|
{% call td("text-right") %}
|
|
<div class="flex justify-end space-x-2">
|
|
<a :href="'/domains/' + domain.name" class="btn btn-sm btn-outline">
|
|
Details
|
|
</a>
|
|
{% call button(variant="outline", size="sm") %}
|
|
Edit
|
|
{% endcall %}
|
|
</div>
|
|
{% endcall %}
|
|
{% endcall %}
|
|
</template>
|
|
{% endcall %}
|
|
{% endcall %}
|
|
{% endcall %}
|
|
{% endcall %}
|
|
</div>
|
|
|
|
<dialog class="modal" :open="openCreate">
|
|
<div class="modal-box max-w-2xl">
|
|
<h2 class="text-xl font-semibold">Add monitored domain</h2>
|
|
<p class="mt-1 text-sm text-base-content/70">Track DNS health and future DMARC reports before the first report arrives.</p>
|
|
<form class="mt-5 space-y-4" @submit.prevent="createDomain">
|
|
<label class="form-control">
|
|
<span class="label-text font-medium">Domain</span>
|
|
<input x-model.trim="newDomain.name" type="text" required placeholder="example.com" class="input input-bordered">
|
|
</label>
|
|
<label class="form-control">
|
|
<span class="label-text font-medium">Description</span>
|
|
<textarea x-model.trim="newDomain.description" rows="3" class="textarea textarea-bordered" placeholder="Production mail domain"></textarea>
|
|
</label>
|
|
<label class="form-control">
|
|
<span class="label-text font-medium">DKIM selectors</span>
|
|
<input x-model.trim="newDomain.dkim_selectors" type="text" placeholder="default, google, selector1" class="input input-bordered">
|
|
</label>
|
|
<div x-show="createError" role="alert" class="alert alert-error py-3">
|
|
<span x-text="createError"></span>
|
|
</div>
|
|
<div class="modal-action">
|
|
<button type="button" class="btn btn-ghost" @click="closeCreate" :disabled="saving">Cancel</button>
|
|
<button type="submit" class="btn btn-primary" :disabled="saving">
|
|
<span x-show="saving" class="loading loading-spinner loading-xs"></span>
|
|
Add domain
|
|
</button>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
<form method="dialog" class="modal-backdrop">
|
|
<button type="button" @click="closeCreate">close</button>
|
|
</form>
|
|
</dialog>
|
|
</div>
|
|
{% endblock %}
|
|
|
|
{% block scripts %}
|
|
<script>
|
|
function domainsApp() {
|
|
return {
|
|
domains: [],
|
|
openCreate: false,
|
|
saving: false,
|
|
createError: '',
|
|
newDomain: {
|
|
name: '',
|
|
description: '',
|
|
dkim_selectors: '',
|
|
},
|
|
|
|
init() {
|
|
// Fetch domains from server
|
|
this.fetchDomains();
|
|
},
|
|
|
|
async fetchDomains() {
|
|
try {
|
|
const response = await fetch('/api/v1/domains/summary');
|
|
if (response.ok) {
|
|
const data = await response.json();
|
|
|
|
// Map API fields — DNS status comes directly from live lookups
|
|
this.domains = data.domains.map(domain => ({
|
|
name: domain.domain_name,
|
|
dmarc_status: domain.dmarc_status ?? false,
|
|
dmarc_policy: domain.dmarc_policy || 'Not configured',
|
|
spf_status: domain.spf_status ?? false,
|
|
dkim_status: domain.dkim_status ?? false,
|
|
reports_count: domain.report_count,
|
|
emails_count: domain.total_emails,
|
|
compliance_rate: domain.pass_rate
|
|
}));
|
|
} else {
|
|
console.error('Error fetching domains:', response.status);
|
|
}
|
|
} catch (error) {
|
|
console.error('Error fetching domains:', error);
|
|
}
|
|
},
|
|
|
|
closeCreate() {
|
|
this.openCreate = false;
|
|
this.createError = '';
|
|
this.newDomain = { name: '', description: '', dkim_selectors: '' };
|
|
},
|
|
|
|
async createDomain() {
|
|
this.saving = true;
|
|
this.createError = '';
|
|
try {
|
|
const selectors = this.newDomain.dkim_selectors
|
|
.split(',')
|
|
.map((selector) => selector.trim())
|
|
.filter(Boolean);
|
|
const response = await fetch('/api/v1/domains/domains', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({
|
|
name: this.newDomain.name,
|
|
description: this.newDomain.description || null,
|
|
dkim_selectors: selectors,
|
|
}),
|
|
});
|
|
if (!response.ok) {
|
|
const data = await response.json().catch(() => ({}));
|
|
const detail = typeof data.detail === 'string'
|
|
? data.detail
|
|
: Object.values(data.detail || {}).join(', ');
|
|
throw new Error(detail || 'Domain could not be added.');
|
|
}
|
|
this.closeCreate();
|
|
await this.fetchDomains();
|
|
} catch (error) {
|
|
this.createError = error.message || 'Domain could not be added.';
|
|
} finally {
|
|
this.saving = false;
|
|
}
|
|
},
|
|
}
|
|
}
|
|
</script>
|
|
{% endblock %}
|