Add domain management, reports, settings, and upload templates

- Implemented domain management page with a list of monitored domains and their DMARC, SPF, and DKIM statuses.
- Created reports page with filtering options for domain, report type, and date range, displaying DMARC reports.
- Developed settings page for IMAP configuration and DMARC policy management, including form validation and feedback.
- Added upload page for DMARC report files with drag-and-drop functionality and file type validation.
- Integrated Alpine.js for interactivity and dynamic data handling across all templates.
This commit is contained in:
Christian Krakau-Louis
2025-04-20 18:38:29 +02:00
parent f910cb0ba4
commit 69f8438a36
48 changed files with 3449 additions and 1029 deletions
@@ -0,0 +1,44 @@
{% macro alert(variant='info', dismissible=False, id='') %}
<div
class="alert alert-{{ variant }} shadow-lg mb-4"
{% if id %}id="{{ id }}"{% endif %}
{% if dismissible %}x-data="{ open: true }" x-show="open"{% endif %}
>
{% if variant == 'info' %}
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" class="stroke-info flex-shrink-0 w-6 h-6"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"></path></svg>
{% elif variant == 'success' %}
<svg xmlns="http://www.w3.org/2000/svg" class="stroke-success flex-shrink-0 h-6 w-6" fill="none" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z" /></svg>
{% elif variant == 'warning' %}
<svg xmlns="http://www.w3.org/2000/svg" class="stroke-warning flex-shrink-0 h-6 w-6" fill="none" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z" /></svg>
{% elif variant == 'error' %}
<svg xmlns="http://www.w3.org/2000/svg" class="stroke-error flex-shrink-0 h-6 w-6" fill="none" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 14l2-2m0 0l2-2m-2 2l-2-2m2 2l2 2m7-2a9 9 0 11-18 0 9 9 0 0118 0z" /></svg>
{% endif %}
<div>
{{ caller() }}
</div>
{% if dismissible %}
<button
type="button"
class="btn btn-ghost btn-sm btn-square"
x-on:click="open = false"
aria-label="Close"
>
<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"><path d="M18 6 6 18"></path><path d="m6 6 12 12"></path></svg>
</button>
{% endif %}
</div>
{% endmacro %}
{% macro alert_title() %}
<h5 class="font-medium text-base">
{{ caller() }}
</h5>
{% endmacro %}
{% macro alert_description() %}
<div class="text-sm">
{{ caller() }}
</div>
{% endmacro %}
@@ -0,0 +1,15 @@
{% macro button(variant='default', size='md', class='', type='button', disabled=False) %}
<button
type="{{ type }}"
class="btn btn-{{ variant }} btn-{{ size }} {{ class }}"
{% if disabled %}disabled{% endif %}
>
{{ caller() }}
</button>
{% endmacro %}
{% macro button_link(href='#', variant='default', size='md', class='') %}
<a href="{{ href }}" class="btn btn-{{ variant }} btn-{{ size }} {{ class }}">
{{ caller() }}
</a>
{% endmacro %}
@@ -0,0 +1,35 @@
{% macro card() %}
<div class="card bg-base-100 shadow">
{{ caller() }}
</div>
{% endmacro %}
{% macro card_header() %}
<div class="card-body pt-6 pb-2">
{{ caller() }}
</div>
{% endmacro %}
{% macro card_title(text='') %}
<h3 class="card-title text-lg font-bold">
{% if text %}{{ text }}{% else %}{{ caller() }}{% endif %}
</h3>
{% endmacro %}
{% macro card_description(text='') %}
<p class="text-sm opacity-70 mt-1">
{% if text %}{{ text }}{% else %}{{ caller() }}{% endif %}
</p>
{% endmacro %}
{% macro card_content() %}
<div class="card-body py-4">
{{ caller() }}
</div>
{% endmacro %}
{% macro card_footer() %}
<div class="card-actions justify-end p-4 pt-0">
{{ caller() }}
</div>
{% endmacro %}
@@ -0,0 +1,44 @@
{% macro input(
type='text',
name='',
id='',
value='',
placeholder='',
required=False,
disabled=False,
readonly=False,
class='',
min='',
max='',
step=''
) %}
<input
type="{{ type }}"
name="{{ name }}"
id="{{ id or name }}"
value="{{ value }}"
placeholder="{{ placeholder }}"
class="input input-bordered w-full {{ class }}"
{% if required %}required{% endif %}
{% if disabled %}disabled{% endif %}
{% if readonly %}readonly{% endif %}
{% if min %}min="{{ min }}"{% endif %}
{% if max %}max="{{ max }}"{% endif %}
{% if step %}step="{{ step }}"{% endif %}
>
{% endmacro %}
{% macro label(for='', required=False, class='') %}
<label
for="{{ for }}"
class="label"
>
<span class="label-text {{ class }}">{{ caller() }}{% if required %} <span class="text-error">*</span>{% endif %}</span>
</label>
{% endmacro %}
{% macro form_group() %}
<div class="form-control w-full mb-4">
{{ caller() }}
</div>
{% endmacro %}
@@ -0,0 +1,37 @@
{% macro table(class='', zebra=True) %}
<div class="overflow-x-auto">
<table class="table {% if zebra %}table-zebra{% endif %} {{ class }}">
{{ caller() }}
</table>
</div>
{% endmacro %}
{% macro thead() %}
<thead>
{{ caller() }}
</thead>
{% endmacro %}
{% macro tbody() %}
<tbody>
{{ caller() }}
</tbody>
{% endmacro %}
{% macro tr(class='') %}
<tr class="{{ class }}">
{{ caller() }}
</tr>
{% endmacro %}
{% macro th(class='') %}
<th class="{{ class }}">
{{ caller() }}
</th>
{% endmacro %}
{% macro td(class='', colspan='', rowspan='') %}
<td class="{{ class }}" {% if colspan %}colspan="{{ colspan }}"{% endif %} {% if rowspan %}rowspan="{{ rowspan }}"{% endif %}>
{{ caller() }}
</td>
{% endmacro %}
+408
View File
@@ -0,0 +1,408 @@
{% extends "layouts/base.html" %}
{% block title %}DMARQ - DaisyUI Components Demo{% endblock %}
{% block page_title %}DaisyUI Components Demo{% endblock %}
{% block content %}
<div class="space-y-12">
<!-- Introduction -->
<section>
<div class="card bg-base-100 shadow-lg">
<div class="card-body">
<h2 class="card-title text-2xl">DaisyUI Components for DMARQ</h2>
<p class="text-lg mt-2">
This page demonstrates all the available DaisyUI components that can be used in the DMARQ application.
All components are styled using the custom DMARQ theme.
</p>
<div class="mt-4 flex gap-2">
<button class="btn btn-primary">Primary</button>
<button class="btn btn-secondary">Secondary</button>
<button class="btn btn-accent">Accent</button>
</div>
</div>
</div>
</section>
<!-- Theme Colors -->
<section>
<h2 class="text-2xl font-semibold mb-4">Theme Colors</h2>
<div class="grid grid-cols-1 md:grid-cols-3 gap-4">
<div class="p-4 bg-primary text-primary-content rounded-lg">primary</div>
<div class="p-4 bg-secondary text-secondary-content rounded-lg">secondary</div>
<div class="p-4 bg-accent text-accent-content rounded-lg">accent</div>
<div class="p-4 bg-neutral text-neutral-content rounded-lg">neutral</div>
<div class="p-4 bg-base-100 border border-base-300 rounded-lg">base-100</div>
<div class="p-4 bg-base-200 rounded-lg">base-200</div>
<div class="p-4 bg-base-300 rounded-lg">base-300</div>
<div class="p-4 bg-info text-info-content rounded-lg">info</div>
<div class="p-4 bg-success text-success-content rounded-lg">success</div>
<div class="p-4 bg-warning text-warning-content rounded-lg">warning</div>
<div class="p-4 bg-error text-error-content rounded-lg">error</div>
</div>
</section>
<!-- Buttons -->
<section>
<h2 class="text-2xl font-semibold mb-4">Buttons</h2>
<div class="card bg-base-100 shadow">
<div class="card-body">
<h3 class="text-xl font-medium mb-4">Button Variants</h3>
<div class="flex flex-wrap gap-2 mb-8">
<button class="btn">Default</button>
<button class="btn btn-neutral">Neutral</button>
<button class="btn btn-primary">Primary</button>
<button class="btn btn-secondary">Secondary</button>
<button class="btn btn-accent">Accent</button>
<button class="btn btn-ghost">Ghost</button>
<button class="btn btn-link">Link</button>
</div>
<h3 class="text-xl font-medium mb-4">Button States</h3>
<div class="flex flex-wrap gap-2 mb-8">
<button class="btn btn-primary">Normal</button>
<button class="btn btn-primary btn-outline">Outline</button>
<button class="btn btn-primary" disabled>Disabled</button>
<button class="btn btn-primary loading">Loading</button>
</div>
<h3 class="text-xl font-medium mb-4">Button Sizes</h3>
<div class="flex flex-wrap items-center gap-2">
<button class="btn btn-primary btn-xs">Extra Small</button>
<button class="btn btn-primary btn-sm">Small</button>
<button class="btn btn-primary">Normal</button>
<button class="btn btn-primary btn-lg">Large</button>
</div>
</div>
</div>
</section>
<!-- Alerts -->
<section>
<h2 class="text-2xl font-semibold mb-4">Alerts</h2>
<div class="space-y-4">
<div class="alert alert-info">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" class="stroke-info flex-shrink-0 w-6 h-6"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"></path></svg>
<div>
<h5 class="font-medium text-base">Information</h5>
<div class="text-sm">This is an informational alert.</div>
</div>
</div>
<div class="alert alert-success">
<svg xmlns="http://www.w3.org/2000/svg" class="stroke-success flex-shrink-0 h-6 w-6" fill="none" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z" /></svg>
<div>
<h5 class="font-medium text-base">Success!</h5>
<div class="text-sm">Your action was completed successfully.</div>
</div>
</div>
<div class="alert alert-warning">
<svg xmlns="http://www.w3.org/2000/svg" class="stroke-warning flex-shrink-0 h-6 w-6" fill="none" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z" /></svg>
<div>
<h5 class="font-medium text-base">Warning!</h5>
<div class="text-sm">This action requires your attention.</div>
</div>
</div>
<div class="alert alert-error">
<svg xmlns="http://www.w3.org/2000/svg" class="stroke-error flex-shrink-0 h-6 w-6" fill="none" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 14l2-2m0 0l2-2m-2 2l-2-2m2 2l2 2m7-2a9 9 0 11-18 0 9 9 0 0118 0z" /></svg>
<div>
<h5 class="font-medium text-base">Error!</h5>
<div class="text-sm">There was an error processing your request.</div>
</div>
</div>
</div>
</section>
<!-- Forms -->
<section>
<h2 class="text-2xl font-semibold mb-4">Forms</h2>
<div class="card bg-base-100 shadow">
<div class="card-body">
<form>
<div class="grid gap-6 mb-6 md:grid-cols-2">
<div class="form-control w-full">
<label class="label">
<span class="label-text">First name</span>
<span class="label-text-alt">Required</span>
</label>
<input type="text" placeholder="John" class="input input-bordered w-full" />
</div>
<div class="form-control w-full">
<label class="label">
<span class="label-text">Last name</span>
</label>
<input type="text" placeholder="Doe" class="input input-bordered w-full" />
</div>
</div>
<div class="grid gap-6 mb-6 md:grid-cols-2">
<div class="form-control w-full">
<label class="label">
<span class="label-text">Email</span>
</label>
<input type="email" placeholder="john.doe@example.com" class="input input-bordered w-full" />
</div>
<div class="form-control w-full">
<label class="label">
<span class="label-text">Phone</span>
</label>
<input type="tel" placeholder="123-456-7890" class="input input-bordered w-full" />
</div>
</div>
<div class="mb-6">
<div class="form-control">
<label class="label">
<span class="label-text">Message</span>
</label>
<textarea class="textarea textarea-bordered h-24" placeholder="Your message here"></textarea>
</div>
</div>
<div class="mb-6">
<div class="form-control">
<label class="label cursor-pointer">
<span class="label-text">Agree to terms and conditions</span>
<input type="checkbox" class="checkbox checkbox-primary" />
</label>
</div>
</div>
<div class="flex items-center justify-end space-x-4">
<button type="button" class="btn btn-ghost">Cancel</button>
<button type="submit" class="btn btn-primary">Submit</button>
</div>
</form>
</div>
</div>
</section>
<!-- Tables -->
<section>
<h2 class="text-2xl font-semibold mb-4">Tables</h2>
<div class="card bg-base-100 shadow">
<div class="card-body">
<div class="overflow-x-auto">
<table class="table table-zebra w-full">
<!-- head -->
<thead>
<tr>
<th></th>
<th>Name</th>
<th>Email</th>
<th>Role</th>
<th>Status</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
<!-- rows -->
<tr>
<th>1</th>
<td>John Doe</td>
<td>john@example.com</td>
<td>Admin</td>
<td><span class="badge badge-success">Active</span></td>
<td>
<div class="flex gap-2">
<button class="btn btn-xs">Edit</button>
<button class="btn btn-error btn-xs">Delete</button>
</div>
</td>
</tr>
<tr>
<th>2</th>
<td>Jane Smith</td>
<td>jane@example.com</td>
<td>User</td>
<td><span class="badge badge-warning">Pending</span></td>
<td>
<div class="flex gap-2">
<button class="btn btn-xs">Edit</button>
<button class="btn btn-error btn-xs">Delete</button>
</div>
</td>
</tr>
<tr>
<th>3</th>
<td>Robert Johnson</td>
<td>robert@example.com</td>
<td>User</td>
<td><span class="badge badge-error">Inactive</span></td>
<td>
<div class="flex gap-2">
<button class="btn btn-xs">Edit</button>
<button class="btn btn-error btn-xs">Delete</button>
</div>
</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
</section>
<!-- Badges and Tags -->
<section>
<h2 class="text-2xl font-semibold mb-4">Badges and Tags</h2>
<div class="card bg-base-100 shadow">
<div class="card-body">
<h3 class="text-xl font-medium mb-4">Badge Variants</h3>
<div class="flex flex-wrap gap-2 mb-8">
<span class="badge">Neutral</span>
<span class="badge badge-primary">Primary</span>
<span class="badge badge-secondary">Secondary</span>
<span class="badge badge-accent">Accent</span>
<span class="badge badge-info">Info</span>
<span class="badge badge-success">Success</span>
<span class="badge badge-warning">Warning</span>
<span class="badge badge-error">Error</span>
</div>
<h3 class="text-xl font-medium mb-4">Badge Sizes</h3>
<div class="flex flex-wrap items-center gap-2">
<span class="badge badge-xs">Extra Small</span>
<span class="badge badge-sm">Small</span>
<span class="badge">Normal</span>
<span class="badge badge-lg">Large</span>
</div>
</div>
</div>
</section>
<!-- Modals -->
<section>
<h2 class="text-2xl font-semibold mb-4">Modals</h2>
<div class="card bg-base-100 shadow">
<div class="card-body">
<!-- Open the modal using the button -->
<button class="btn btn-primary" onclick="demo_modal.showModal()">Open Modal</button>
<dialog id="demo_modal" class="modal">
<div class="modal-box">
<h3 class="font-bold text-lg">Hello!</h3>
<p class="py-4">This is a modal example using DaisyUI.</p>
<div class="modal-action">
<form method="dialog">
<!-- if there is a button in form, it will close the modal -->
<button class="btn">Close</button>
</form>
</div>
</div>
</dialog>
</div>
</div>
</section>
<!-- Progress -->
<section>
<h2 class="text-2xl font-semibold mb-4">Progress</h2>
<div class="card bg-base-100 shadow">
<div class="card-body space-y-4">
<div>
<div class="text-sm mb-1">Default Progress (75%)</div>
<progress class="progress w-full" value="75" max="100"></progress>
</div>
<div>
<div class="text-sm mb-1">Primary Progress (45%)</div>
<progress class="progress progress-primary w-full" value="45" max="100"></progress>
</div>
<div>
<div class="text-sm mb-1">Secondary Progress (60%)</div>
<progress class="progress progress-secondary w-full" value="60" max="100"></progress>
</div>
<div>
<div class="text-sm mb-1">Success Progress (90%)</div>
<progress class="progress progress-success w-full" value="90" max="100"></progress>
</div>
<div>
<div class="text-sm mb-1">Warning Progress (50%)</div>
<progress class="progress progress-warning w-full" value="50" max="100"></progress>
</div>
<div>
<div class="text-sm mb-1">Error Progress (25%)</div>
<progress class="progress progress-error w-full" value="25" max="100"></progress>
</div>
</div>
</div>
</section>
<!-- Tabs -->
<section>
<h2 class="text-2xl font-semibold mb-4">Tabs</h2>
<div class="card bg-base-100 shadow">
<div class="card-body">
<div role="tablist" class="tabs tabs-bordered">
<input type="radio" name="my_tabs" role="tab" class="tab" aria-label="Overview" checked />
<div role="tabpanel" class="tab-content p-6">
<p>This is the Overview tab content. Tabs are useful for organizing content into different sections.</p>
</div>
<input type="radio" name="my_tabs" role="tab" class="tab" aria-label="Details" />
<div role="tabpanel" class="tab-content p-6">
<p>This is the Details tab content. You can include any type of content here, including forms, tables, or other components.</p>
</div>
<input type="radio" name="my_tabs" role="tab" class="tab" aria-label="Stats" />
<div role="tabpanel" class="tab-content p-6">
<p>This is the Stats tab content. Tabs help keep your interface clean and organized.</p>
</div>
</div>
</div>
</div>
</section>
<!-- Accordion -->
<section>
<h2 class="text-2xl font-semibold mb-4">Accordion</h2>
<div class="card bg-base-100 shadow">
<div class="card-body">
<div class="collapse collapse-arrow bg-base-200 mb-2">
<input type="radio" name="accordion-1" checked="checked" />
<div class="collapse-title font-medium">
What is DMARQ?
</div>
<div class="collapse-content">
<p>DMARQ is a DMARC monitoring tool that helps organizations track and analyze their email authentication compliance.</p>
</div>
</div>
<div class="collapse collapse-arrow bg-base-200 mb-2">
<input type="radio" name="accordion-1" />
<div class="collapse-title font-medium">
How does DMARC work?
</div>
<div class="collapse-content">
<p>DMARC (Domain-based Message Authentication, Reporting and Conformance) is an email authentication protocol that builds upon SPF and DKIM to help prevent email spoofing.</p>
</div>
</div>
<div class="collapse collapse-arrow bg-base-200">
<input type="radio" name="accordion-1" />
<div class="collapse-title font-medium">
What are the benefits of using DMARQ?
</div>
<div class="collapse-content">
<p>DMARQ provides insights into email authentication failures, helps identify legitimate sources that may be failing authentication, and offers recommendations to improve email deliverability and security.</p>
</div>
</div>
</div>
</div>
</section>
<!-- Tooltips -->
<section>
<h2 class="text-2xl font-semibold mb-4">Tooltips</h2>
<div class="card bg-base-100 shadow">
<div class="card-body">
<div class="flex justify-center gap-6">
<div class="tooltip" data-tip="Top tooltip">
<button class="btn">Top</button>
</div>
<div class="tooltip tooltip-bottom" data-tip="Bottom tooltip">
<button class="btn">Bottom</button>
</div>
<div class="tooltip tooltip-left" data-tip="Left tooltip">
<button class="btn">Left</button>
</div>
<div class="tooltip tooltip-right" data-tip="Right tooltip">
<button class="btn">Right</button>
</div>
</div>
</div>
</div>
</section>
</div>
{% endblock %}
+490
View File
@@ -0,0 +1,490 @@
{% 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 - {{ domain.name }} Details{% endblock %}
{% block content %}
<div class="container mx-auto py-4" x-data="domainDetailsApp({{ domain_id }})">
<nav class="mb-4 text-sm">
<ol class="flex items-center space-x-2">
<li><a href="/" class="hover:text-primary">Dashboard</a></li>
<li><span class="text-muted-foreground px-2">/</span></li>
<li><a href="/domains" class="hover:text-primary">Domains</a></li>
<li><span class="text-muted-foreground px-2">/</span></li>
<li><span class="font-medium">{{ domain.name }}</span></li>
</ol>
</nav>
<div class="grid grid-cols-1 gap-6">
<!-- Domain Overview -->
<div class="flex justify-between items-start">
<div>
<h1 class="text-2xl font-bold mb-1">{{ domain.name }}</h1>
<p class="text-muted-foreground">{{ domain.description or "Domain monitored by DMARQ" }}</p>
</div>
<div class="flex space-x-2">
{% call button(variant="outline", size="sm") %}
<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="M17 3a2.85 2.83 0 1 1 4 4L7.5 20.5 2 22l1.5-5.5Z"></path><path d="m15 5 4 4"></path></svg>
Edit Domain
{% endcall %}
{% call button(variant="outline", size="sm") %}
<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 2v7.5"></path><path d="m4.24 10.37 6.58-3.79"></path><path d="m3.24 16.98 7.5-1.01"></path><path d="m13.24 15.97 7.5 1.01"></path><path d="m13.24 3.79 6.58 3.79"></path><path d="M12 2a10 10 0 1 0 0 20 10 10 0 0 0 0-20Z"></path><path d="M12 12a1 1 0 1 0 0 2 1 1 0 0 0 0-2Z"></path></svg>
Check DNS
{% endcall %}
</div>
</div>
<!-- Domain Stats -->
<div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-4">
<!-- DMARC Compliance Card -->
{% call card() %}
{% call card_header() %}
<div class="flex items-center justify-between">
{% call card_title() %}DMARC Compliance{% endcall %}
<div class="p-2 bg-primary/10 rounded-full">
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="text-primary"><path d="M22 11.08V12a10 10 0 1 1-5.93-9.14"></path><polyline points="22 4 12 14.01 9 11.01"></polyline></svg>
</div>
</div>
{% endcall %}
{% call card_content() %}
<div class="stat-card">
<div id="compliance-rate" class="stat-value" x-text="stats.complianceRate + '%'">-</div>
<p class="stat-description">Emails passing DMARC</p>
</div>
{% endcall %}
{% endcall %}
<!-- Total Emails Card -->
{% call card() %}
{% call card_header() %}
<div class="flex items-center justify-between">
{% call card_title() %}Total Emails{% endcall %}
<div class="p-2 bg-secondary/10 rounded-full">
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="text-secondary"><rect width="20" height="16" x="2" y="4" rx="2"></rect><path d="m22 7-8.97 5.7a1.94 1.94 0 0 1-2.06 0L2 7"></path></svg>
</div>
</div>
{% endcall %}
{% call card_content() %}
<div class="stat-card">
<div id="total-emails" class="stat-value" x-text="stats.totalEmails">-</div>
<p class="stat-description">Total emails processed</p>
</div>
{% endcall %}
{% endcall %}
<!-- Failed Emails Card -->
{% call card() %}
{% call card_header() %}
<div class="flex items-center justify-between">
{% call card_title() %}Failed Emails{% endcall %}
<div class="p-2 bg-red-500/10 rounded-full">
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="text-red-500"><circle cx="12" cy="12" r="10"></circle><line x1="15" y1="9" x2="9" y2="15"></line><line x1="9" y1="9" x2="15" y2="15"></line></svg>
</div>
</div>
{% endcall %}
{% call card_content() %}
<div class="stat-card">
<div id="failed-emails" class="stat-value" x-text="stats.failedEmails">-</div>
<p class="stat-description">Emails failing DMARC</p>
</div>
{% endcall %}
{% endcall %}
<!-- Reports Card -->
{% call card() %}
{% call card_header() %}
<div class="flex items-center justify-between">
{% call card_title() %}Reports{% endcall %}
<div class="p-2 bg-accent/10 rounded-full">
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="text-accent"><path d="M14.5 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V7.5L14.5 2z"></path><polyline points="14 2 14 8 20 8"></polyline></svg>
</div>
</div>
{% endcall %}
{% call card_content() %}
<div class="stat-card">
<div id="report-count" class="stat-value" x-text="stats.reportCount">-</div>
<p class="stat-description">DMARC reports received</p>
</div>
{% endcall %}
{% endcall %}
</div>
<!-- Compliance Chart -->
{% call card() %}
{% call card_header() %}
{% call card_title() %}Compliance Over Time{% endcall %}
{% call card_description() %}
DMARC pass rate for the past 30 days
{% endcall %}
{% endcall %}
{% call card_content() %}
<div class="h-64">
<canvas id="compliance-chart"></canvas>
</div>
{% endcall %}
{% endcall %}
<!-- DNS Records -->
{% call card() %}
{% call card_header() %}
{% call card_title() %}DNS Records{% endcall %}
{% call card_description() %}
Email authentication DNS records for this domain
{% endcall %}
{% endcall %}
{% call card_content() %}
<div class="grid gap-4">
<div>
<h3 class="font-semibold mb-1 flex items-center">
<span class="mr-2">DMARC Record</span>
<span x-show="dns.dmarc" class="inline-flex h-2 w-2 rounded-full bg-green-500"></span>
<span x-show="!dns.dmarc" class="inline-flex h-2 w-2 rounded-full bg-red-500"></span>
</h3>
<div class="bg-muted p-2 rounded text-sm overflow-x-auto font-mono" x-text="dns.dmarcRecord || 'No DMARC record found'">-</div>
</div>
<div>
<h3 class="font-semibold mb-1 flex items-center">
<span class="mr-2">SPF Record</span>
<span x-show="dns.spf" class="inline-flex h-2 w-2 rounded-full bg-green-500"></span>
<span x-show="!dns.spf" class="inline-flex h-2 w-2 rounded-full bg-red-500"></span>
</h3>
<div class="bg-muted p-2 rounded text-sm overflow-x-auto font-mono" x-text="dns.spfRecord || 'No SPF record found'">-</div>
</div>
<div>
<h3 class="font-semibold mb-1 flex items-center">
<span class="mr-2">DKIM Selectors</span>
<span x-show="dns.dkim && dns.dkim.length > 0" class="inline-flex h-2 w-2 rounded-full bg-green-500"></span>
<span x-show="!dns.dkim || dns.dkim.length === 0" class="inline-flex h-2 w-2 rounded-full bg-red-500"></span>
</h3>
<div class="bg-muted p-2 rounded text-sm overflow-x-auto font-mono" x-text="dns.dkimSelectors || 'No DKIM selectors configured'">-</div>
</div>
</div>
{% endcall %}
{% endcall %}
<!-- Source Report Table -->
{% call card() %}
{% call card_header() %}
<div class="flex items-center justify-between">
{% call card_title() %}Sending Sources{% endcall %}
<div class="space-x-2">
<select x-model="filters.dateRange" class="select select-sm select-bordered">
<option value="7">Last 7 days</option>
<option value="30">Last 30 days</option>
<option value="90">Last 90 days</option>
<option value="all">All time</option>
</select>
<input
x-model="filters.sourceFilter"
type="text"
placeholder="Filter sources..."
class="input input-sm input-bordered max-w-xs"
>
</div>
</div>
{% call card_description() %}
IP addresses and servers sending email as this domain
{% endcall %}
{% endcall %}
{% call card_content() %}
{% call table() %}
{% call thead() %}
{% call tr() %}
{% call th() %}Source IP{% endcall %}
{% call th() %}Total Emails{% endcall %}
{% call th() %}SPF{% endcall %}
{% call th() %}DKIM{% endcall %}
{% call th() %}DMARC{% endcall %}
{% call th() %}Disposition{% endcall %}
{% endcall %}
{% endcall %}
{% call tbody() %}
<template x-if="sources.length === 0">
<tr>
<td colspan="6" class="text-center py-4">
<div class="text-muted-foreground">No data available for this time period</div>
</td>
</tr>
</template>
<template x-for="source in filteredSources" :key="source.ip">
{% call tr() %}
{% call td() %}
<span x-text="source.ip"></span>
{% endcall %}
{% call td() %}
<span x-text="source.count"></span>
{% endcall %}
{% call td() %}
<template x-if="source.spf === 'pass'">
<span class="inline-flex items-center px-2 py-1 rounded-full text-xs bg-green-100 text-green-800">Pass</span>
</template>
<template x-if="source.spf === 'fail'">
<span class="inline-flex items-center px-2 py-1 rounded-full text-xs bg-red-100 text-red-800">Fail</span>
</template>
<template x-if="source.spf === 'neutral' || source.spf === 'none'">
<span class="inline-flex items-center px-2 py-1 rounded-full text-xs bg-gray-100 text-gray-800" x-text="source.spf"></span>
</template>
{% endcall %}
{% call td() %}
<template x-if="source.dkim === 'pass'">
<span class="inline-flex items-center px-2 py-1 rounded-full text-xs bg-green-100 text-green-800">Pass</span>
</template>
<template x-if="source.dkim === 'fail'">
<span class="inline-flex items-center px-2 py-1 rounded-full text-xs bg-red-100 text-red-800">Fail</span>
</template>
<template x-if="source.dkim === 'neutral' || source.dkim === 'none'">
<span class="inline-flex items-center px-2 py-1 rounded-full text-xs bg-gray-100 text-gray-800" x-text="source.dkim"></span>
</template>
{% endcall %}
{% call td() %}
<template x-if="source.dmarc === 'pass'">
<span class="inline-flex items-center px-2 py-1 rounded-full text-xs bg-green-100 text-green-800">Pass</span>
</template>
<template x-if="source.dmarc === 'fail'">
<span class="inline-flex items-center px-2 py-1 rounded-full text-xs bg-red-100 text-red-800">Fail</span>
</template>
{% endcall %}
{% call td() %}
<template x-if="source.disposition === 'none'">
<span class="inline-flex items-center px-2 py-1 rounded-full text-xs bg-blue-100 text-blue-800">None</span>
</template>
<template x-if="source.disposition === 'quarantine'">
<span class="inline-flex items-center px-2 py-1 rounded-full text-xs bg-yellow-100 text-yellow-800">Quarantine</span>
</template>
<template x-if="source.disposition === 'reject'">
<span class="inline-flex items-center px-2 py-1 rounded-full text-xs bg-red-100 text-red-800">Reject</span>
</template>
{% endcall %}
{% endcall %}
</template>
{% endcall %}
{% endcall %}
{% endcall %}
{% endcall %}
<!-- Recent Reports -->
{% call card() %}
{% call card_header() %}
{% call card_title() %}Recent Reports{% endcall %}
{% call card_description() %}
Latest DMARC reports received for this domain
{% endcall %}
{% endcall %}
{% call card_content() %}
{% call table() %}
{% call thead() %}
{% call tr() %}
{% call th() %}Date{% endcall %}
{% call th() %}Organization{% endcall %}
{% call th() %}Emails{% endcall %}
{% call th() %}Pass Rate{% endcall %}
{% call th() %}Policy{% endcall %}
{% call th() %}Actions{% endcall %}
{% endcall %}
{% endcall %}
{% call tbody() %}
<template x-if="reports.length === 0">
<tr>
<td colspan="6" class="text-center py-4">
<div class="text-muted-foreground">No reports available</div>
</td>
</tr>
</template>
<template x-for="report in reports" :key="report.id">
{% call tr() %}
{% call td() %}
<span x-text="formatDate(report.begin_date)"></span>
{% endcall %}
{% call td() %}
<span x-text="report.org_name"></span>
{% endcall %}
{% call td() %}
<span x-text="report.total_emails"></span>
{% endcall %}
{% call td() %}
<span class="inline-flex items-center px-2 py-1 rounded text-xs"
:class="getPassRateClass(report.pass_rate)">
<span x-text="report.pass_rate + '%'"></span>
</span>
{% endcall %}
{% call td() %}
<span x-text="report.policy"></span>
{% endcall %}
{% call td() %}
<a :href="'/reports/' + report.id" class="btn btn-sm btn-outline">
View
</a>
{% endcall %}
{% endcall %}
</template>
{% endcall %}
{% endcall %}
{% endcall %}
{% endcall %}
</div>
</div>
{% endblock %}
{% block scripts %}
<script src="https://cdn.jsdelivr.net/npm/chart.js@4.3.0/dist/chart.umd.min.js"></script>
<script>
function domainDetailsApp(domainId) {
return {
domainId: domainId,
stats: {
complianceRate: '-',
totalEmails: '-',
failedEmails: '-',
reportCount: '-'
},
dns: {
dmarc: false,
dmarcRecord: '',
spf: false,
spfRecord: '',
dkim: false,
dkimSelectors: ''
},
reports: [],
sources: [],
complianceChart: null,
filters: {
dateRange: '30',
sourceFilter: ''
},
init() {
this.fetchDomainStats();
this.fetchDNSRecords();
this.fetchReports();
this.fetchSources();
this.$watch('filters.dateRange', () => {
this.fetchSources();
});
},
get filteredSources() {
if (!this.sources) return [];
return this.sources.filter(source => {
if (!this.filters.sourceFilter) return true;
return source.ip.toLowerCase().includes(this.filters.sourceFilter.toLowerCase());
});
},
async fetchDomainStats() {
try {
const response = await fetch(`/api/v1/domains/${this.domainId}/stats`);
if (response.ok) {
const data = await response.json();
this.stats = data;
}
} catch (error) {
console.error('Error fetching domain stats:', error);
}
},
async fetchDNSRecords() {
try {
const response = await fetch(`/api/v1/domains/${this.domainId}/dns`);
if (response.ok) {
const data = await response.json();
this.dns = data;
}
} catch (error) {
console.error('Error fetching DNS records:', error);
}
},
async fetchReports() {
try {
const response = await fetch(`/api/v1/domains/${this.domainId}/reports?limit=10`);
if (response.ok) {
const data = await response.json();
this.reports = data.reports;
this.initComplianceChart(data.compliance_timeline);
}
} catch (error) {
console.error('Error fetching reports:', error);
}
},
async fetchSources() {
try {
const response = await fetch(`/api/v1/domains/${this.domainId}/sources?days=${this.filters.dateRange}`);
if (response.ok) {
const data = await response.json();
this.sources = data.sources;
}
} catch (error) {
console.error('Error fetching sources:', error);
}
},
initComplianceChart(timelineData) {
if (!timelineData) return;
const ctx = document.getElementById('compliance-chart').getContext('2d');
if (this.complianceChart) {
this.complianceChart.destroy();
}
const labels = timelineData.map(item => item.date);
const complianceData = timelineData.map(item => item.compliance_rate);
this.complianceChart = new Chart(ctx, {
type: 'line',
data: {
labels: labels,
datasets: [{
label: 'Compliance Rate',
data: complianceData,
borderColor: '#1A237E',
backgroundColor: 'rgba(26, 35, 126, 0.1)',
tension: 0.3,
fill: true
}]
},
options: {
responsive: true,
maintainAspectRatio: false,
scales: {
y: {
beginAtZero: true,
max: 100,
ticks: {
callback: value => value + '%'
}
}
},
plugins: {
tooltip: {
callbacks: {
label: function(context) {
return context.parsed.y + '%';
}
}
}
}
}
});
},
formatDate(timestamp) {
const date = new Date(timestamp * 1000);
return date.toLocaleDateString();
},
getPassRateClass(rate) {
if (rate >= 90) return 'bg-green-100 text-green-800';
if (rate >= 50) return 'bg-yellow-100 text-yellow-800';
return 'bg-red-100 text-red-800';
}
};
}
</script>
{% endblock %}
+135
View File
@@ -0,0 +1,135 @@
{% 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 %}
{% call button(variant="outline", size="sm") %}
<span class="mr-1">+</span> Add Domain
{% endcall %}
</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="'/domain/' + 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>
</div>
{% endblock %}
{% block scripts %}
<script>
function domainsApp() {
return {
domains: [],
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();
// Format domains for display
this.domains = data.domains.map(domain => ({
name: domain.domain_name,
dmarc_status: true, // In Milestone 1, assume DMARC is configured if we have reports
dmarc_policy: domain.policy || 'p=none',
spf_status: true, // In future milestones, this will come from DNS checks
dkim_status: true, // In future milestones, this will come from DNS checks
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);
}
}
}
}
</script>
{% endblock %}
File diff suppressed because it is too large Load Diff
+62
View File
@@ -0,0 +1,62 @@
<!DOCTYPE html>
<html lang="en" data-theme="dmarqlight" class="dark:data-theme-dmarqdark">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{% block title %}DMARQ - DMARC Monitoring{% endblock %}</title>
<!-- Fonts -->
<link href="https://fonts.googleapis.com/css2?family=Montserrat:wght@400;500;600;700&family=Open+Sans:wght@400;500;600&display=swap" rel="stylesheet">
<!-- Tailwind CSS & DaisyUI via CDN -->
<script src="https://cdn.tailwindcss.com"></script>
<link href="https://cdn.jsdelivr.net/npm/daisyui@4.12.24/dist/full.css" rel="stylesheet" type="text/css"/>
<!-- Alpine.js for interactivity -->
<script defer src="https://cdn.jsdelivr.net/npm/alpinejs@3.x.x/dist/cdn.min.js"></script>
{% block head %}{% endblock %}
</head>
<body class="min-h-screen bg-base-100 font-body antialiased">
<!-- Updated Menu Bar -->
<header class="navbar bg-primary text-primary-content">
<div class="flex-1">
<a href="/" class="btn btn-ghost normal-case text-xl">
<img src="/static/img/monogram_light.png" alt="DMARQ Logo" class="w-8 h-8 mr-2">
DMARQ
</a>
</div>
<div class="flex-none">
<ul class="menu menu-horizontal px-1">
<li><a href="/">Dashboard</a></li>
<li><a href="/domains">Domains</a></li>
<li><a href="/reports">Reports</a></li>
<li><a href="/upload">Upload</a></li>
<li><a href="/settings">Settings</a></li>
</ul>
</div>
</header>
<!-- Updated Main Content Area with page-specific data initialization -->
<main class="p-4 md:p-6">
{% block content %}{% endblock %}
</main>
<!-- Scripts -->
{% block scripts %}{% endblock %}
<!-- Initialize theme from localStorage -->
<script>
document.addEventListener('DOMContentLoaded', function() {
const darkMode = localStorage.getItem('darkMode') === 'true';
if (darkMode) {
document.documentElement.classList.add('dark');
document.documentElement.setAttribute('data-theme', 'dmarqdark');
} else {
document.documentElement.classList.remove('dark');
document.documentElement.setAttribute('data-theme', 'dmarqlight');
}
});
</script>
</body>
</html>
+219
View File
@@ -0,0 +1,219 @@
{% 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 - Reports{% endblock %}
{% block content %}
<div class="container mx-auto py-4" x-data="reportsApp()">
<h1 class="text-2xl font-bold mb-6">DMARC Reports</h1>
<!-- Report Filters -->
<div class="mb-6">
{% call card() %}
{% call card_content() %}
<div class="flex flex-wrap gap-4">
<div class="w-full sm:w-auto">
<label class="block text-sm font-medium mb-1">Domain</label>
<select class="select select-bordered w-full" x-model="filters.domain">
<option value="">All Domains</option>
<template x-for="domain in domains" :key="domain">
<option x-text="domain" :value="domain"></option>
</template>
</select>
</div>
<div class="w-full sm:w-auto">
<label class="block text-sm font-medium mb-1">Report Type</label>
<select class="select select-bordered w-full" x-model="filters.reportType">
<option value="">All Types</option>
<option value="aggregate">Aggregate (RUA)</option>
<option value="forensic">Forensic (RUF)</option>
</select>
</div>
<div class="w-full sm:w-auto">
<label class="block text-sm font-medium mb-1">Date Range</label>
<select class="select select-bordered w-full" x-model="filters.dateRange">
<option value="7">Last 7 days</option>
<option value="14">Last 14 days</option>
<option value="30">Last 30 days</option>
<option value="90">Last 90 days</option>
<option value="all">All time</option>
</select>
</div>
<div class="w-full sm:w-auto flex items-end">
{% call button(variant="outline") %}
<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"><polyline points="23 4 23 10 17 10"></polyline><path d="M20.49 15a9 9 0 1 1-2.12-9.36L23 10"></path></svg>
Reset Filters
{% endcall %}
</div>
</div>
{% endcall %}
{% endcall %}
</div>
<!-- Reports Table -->
<div class="mb-8">
{% call card() %}
{% call card_header() %}
<div class="flex items-center justify-between">
{% call card_title() %}DMARC Reports{% endcall %}
</div>
{% call card_description() %}
Showing <span x-text="filteredReports.length"></span> reports
{% endcall %}
{% endcall %}
{% call card_content() %}
{% call table() %}
{% call thead() %}
{% call tr() %}
{% call th() %}Date{% endcall %}
{% call th() %}Type{% endcall %}
{% call th() %}Domain{% endcall %}
{% call th() %}Organization{% endcall %}
{% call th() %}Messages{% endcall %}
{% call th() %}Pass Rate{% endcall %}
{% call th("text-right") %}Actions{% endcall %}
{% endcall %}
{% endcall %}
{% call tbody() %}
<template x-for="(report, index) in filteredReports" :key="index">
{% call tr() %}
{% call td() %}
<div x-text="formatDate(report.date)"></div>
{% endcall %}
{% call td() %}
<div class="inline-flex items-center px-2 py-1 rounded text-xs"
:class="report.type === 'aggregate' ? 'bg-blue-100 text-blue-800' : 'bg-amber-100 text-amber-800'">
<span x-text="report.type === 'aggregate' ? 'Aggregate' : 'Forensic'"></span>
</div>
{% endcall %}
{% call td() %}
<div class="font-medium" x-text="report.domain"></div>
{% endcall %}
{% call td() %}
<div x-text="report.organization"></div>
{% endcall %}
{% call td() %}
<div x-text="report.messages"></div>
{% endcall %}
{% call td() %}
<div class="inline-flex items-center px-2 py-1 rounded"
:class="getPassRateColor(report.passRate)">
<span x-text="report.passRate + '%'"></span>
</div>
{% endcall %}
{% call td("text-right") %}
{% call button(variant="outline", size="sm") %}
View Details
{% endcall %}
{% endcall %}
{% endcall %}
</template>
{% endcall %}
{% endcall %}
{% endcall %}
{% endcall %}
</div>
</div>
{% endblock %}
{% block scripts %}
<script>
function reportsApp() {
return {
filters: {
domain: '',
reportType: '',
dateRange: '30'
},
domains: ['example.com', 'mydomain.com'],
reports: [
{
id: 1,
date: '2023-04-15',
type: 'aggregate',
domain: 'example.com',
organization: 'Google',
messages: 128,
passRate: 96
},
{
id: 2,
date: '2023-04-15',
type: 'aggregate',
domain: 'mydomain.com',
organization: 'Microsoft',
messages: 64,
passRate: 100
},
{
id: 3,
date: '2023-04-14',
type: 'forensic',
domain: 'example.com',
organization: 'Yahoo',
messages: 1,
passRate: 0
}
],
init() {
// When API is ready, fetch reports from server
// this.fetchReports();
},
get filteredReports() {
return this.reports.filter(report => {
// Filter by domain
if (this.filters.domain && report.domain !== this.filters.domain) {
return false;
}
// Filter by report type
if (this.filters.reportType && report.type !== this.filters.reportType) {
return false;
}
// Filter by date range
if (this.filters.dateRange !== 'all') {
const days = parseInt(this.filters.dateRange);
const cutoff = new Date();
cutoff.setDate(cutoff.getDate() - days);
const reportDate = new Date(report.date);
if (reportDate < cutoff) {
return false;
}
}
return true;
});
},
formatDate(dateStr) {
const date = new Date(dateStr);
return date.toLocaleDateString();
},
getPassRateColor(rate) {
if (rate >= 90) return 'bg-green-100 text-green-800';
if (rate >= 50) return 'bg-yellow-100 text-yellow-800';
return 'bg-red-100 text-red-800';
},
async fetchReports() {
try {
const response = await fetch('/api/v1/reports');
this.reports = await response.json();
// Extract unique domains
this.domains = [...new Set(this.reports.map(r => r.domain))];
} catch (error) {
console.error('Error fetching reports:', error);
}
}
}
}
</script>
{% endblock %}
+336
View File
@@ -0,0 +1,336 @@
{% 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 %}
{% from "components/ui/alert.html" import alert, alert_title, alert_description %}
{% from "components/ui/input.html" import input, label, form_group %}
{% block title %}Settings - DMARQ{% endblock %}
{% block page_title %}Settings{% endblock %}
{% block content %}
<div class="grid gap-4 md:gap-8 py-4">
<!-- IMAP Configuration -->
{% call card() %}
{% call card_header() %}
{% call card_title() %}IMAP Configuration{% endcall %}
{% call card_description() %}
Configure the IMAP connection to automatically retrieve DMARC reports from your email
{% endcall %}
{% endcall %}
{% call card_content() %}
<form id="imap-form" class="space-y-6" x-data="imapForm()">
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
<div class="space-y-4">
{% call form_group() %}
{% call label(for="imap_server", required=True) %}IMAP Server{% endcall %}
{{ input(type="text", name="imap_server", id="imap_server", placeholder="mail.example.com", required=True) }}
{% endcall %}
{% call form_group() %}
{% call label(for="imap_port", required=True) %}IMAP Port{% endcall %}
{{ input(type="number", name="imap_port", id="imap_port", value="993", required=True) }}
{% endcall %}
{% call form_group() %}
{% call label(for="imap_ssl") %}Use SSL{% endcall %}
<div class="flex items-center space-x-2">
<input type="checkbox" id="imap_ssl" name="imap_ssl" class="h-4 w-4 rounded border-border text-primary focus:ring-primary" checked />
<label for="imap_ssl" class="text-sm text-muted-foreground">Enable SSL/TLS connection (recommended)</label>
</div>
{% endcall %}
</div>
<div class="space-y-4">
{% call form_group() %}
{% call label(for="imap_username", required=True) %}IMAP Username{% endcall %}
{{ input(type="text", name="imap_username", id="imap_username", placeholder="dmarc-reports@example.com", required=True) }}
{% endcall %}
{% call form_group() %}
{% call label(for="imap_password", required=True) %}IMAP Password{% endcall %}
<div class="relative">
{{ input(type="password", name="imap_password", id="imap_password", required=True) }}
<button
type="button"
class="absolute right-2 top-2.5 text-muted-foreground hover:text-foreground"
x-on:click="togglePassword"
>
<svg x-show="!showPassword" 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"><path d="M9.88 9.88a3 3 0 1 0 4.24 4.24"></path><path d="M10.73 5.08A10.43 10.43 0 0 1 12 5c7 0 10 7 10 7a13.16 13.16 0 0 1-1.67 2.68"></path><path d="M6.61 6.61A13.526 13.526 0 0 0 2 12s3 7 10 7a9.74 9.74 0 0 0 5.39-1.61"></path><line x1="2" x2="22" y1="2" y2="22"></line></svg>
<svg x-show="showPassword" 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" x-cloak><path d="M2 12s3-7 10-7 10 7 10 7-3 7-10 7-10-7-10-7Z"></path><circle cx="12" cy="12" r="3"></circle></svg>
</button>
</div>
{% endcall %}
{% call form_group() %}
{% call label(for="polling_interval") %}Polling Interval (minutes){% endcall %}
{{ input(type="number", name="polling_interval", id="polling_interval", value="60", min="15", max="1440") }}
<p class="text-xs text-muted-foreground mt-1">How often to check for new reports (minimum 15 minutes)</p>
{% endcall %}
</div>
</div>
<div id="test-connection-result" x-show="testResult" x-cloak>
<template x-if="testStatus === 'success'">
{% call alert(variant="success") %}
{% call alert_title() %}Connection Successful{% endcall %}
{% call alert_description() %}
<p x-text="testResult"></p>
{% endcall %}
{% endcall %}
</template>
<template x-if="testStatus === 'error'">
{% call alert(variant="error") %}
{% call alert_title() %}Connection Failed{% endcall %}
{% call alert_description() %}
<p x-text="testResult"></p>
{% endcall %}
{% endcall %}
</template>
</div>
<div id="save-result" x-show="saveResult" x-cloak>
<template x-if="saveStatus === 'success'">
{% call alert(variant="success") %}
{% call alert_title() %}Settings Saved{% endcall %}
{% call alert_description() %}
<p x-text="saveResult"></p>
{% endcall %}
{% endcall %}
</template>
<template x-if="saveStatus === 'error'">
{% call alert(variant="error") %}
{% call alert_title() %}Save Failed{% endcall %}
{% call alert_description() %}
<p x-text="saveResult"></p>
{% endcall %}
{% endcall %}
</template>
</div>
<div class="flex items-center justify-end space-x-4">
<button
type="button"
class="btn btn-outline btn-md"
x-on:click="testConnection"
x-bind:disabled="isTesting || isSaving"
>
<span x-show="!isTesting">
<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="M22 11.08V12a10 10 0 1 1-5.93-9.14"></path><polyline points="22 4 12 14.01 9 11.01"></polyline></svg>
Test Connection
</span>
<span x-show="isTesting" class="flex items-center" x-cloak>
<svg class="animate-spin -ml-1 mr-2 h-4 w-4" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
</svg>
Testing...
</span>
</button>
<button
type="submit"
class="btn btn-default btn-md"
x-bind:disabled="isTesting || isSaving"
>
<span x-show="!isSaving">
<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="M19 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h11l5 5v11a2 2 0 0 1-2 2z"></path><polyline points="17 21 17 13 7 13 7 21"></polyline><polyline points="7 3 7 8 15 8"></polyline></svg>
Save Configuration
</span>
<span x-show="isSaving" class="flex items-center" x-cloak>
<svg class="animate-spin -ml-1 mr-2 h-4 w-4 text-white" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
</svg>
Saving...
</span>
</button>
</div>
</form>
{% endcall %}
{% endcall %}
<!-- DMARC Policy Management -->
{% call card() %}
{% call card_header() %}
{% call card_title() %}DMARC Policy Management{% endcall %}
{% call card_description() %}
Configure default DMARC policy settings for newly added domains
{% endcall %}
{% endcall %}
{% call card_content() %}
<form id="dmarc-policy-form" class="space-y-6" x-data="{isUpdating: false, updateResult: ''}">
<div class="space-y-4">
{% call form_group() %}
{% call label(for="default_policy") %}Default DMARC Policy{% endcall %}
<select id="default_policy" name="default_policy" class="input w-full">
<option value="none">None (monitoring only)</option>
<option value="quarantine">Quarantine (send to spam)</option>
<option value="reject">Reject (block delivery)</option>
</select>
<p class="text-xs text-muted-foreground mt-1">Policy applied to new domains when no specific policy is set</p>
{% endcall %}
{% call form_group() %}
{% call label(for="percent") %}Percentage{% endcall %}
<div class="flex items-center">
<input type="range" id="percent" name="percent" min="0" max="100" value="100" class="w-full h-2 bg-muted rounded-lg appearance-none cursor-pointer" />
<span class="ml-2 text-sm font-medium w-10" id="percent-display">100%</span>
</div>
<p class="text-xs text-muted-foreground mt-1">Percentage of messages to which the DMARC policy is applied</p>
{% endcall %}
</div>
<div class="flex justify-end">
<button type="submit" class="btn btn-default btn-md">
<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="M19 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h11l5 5v11a2 2 0 0 1-2 2z"></path><polyline points="17 21 17 13 7 13 7 21"></polyline><polyline points="7 3 7 8 15 8"></polyline></svg>
Save Policy Settings
</button>
</div>
</form>
{% endcall %}
{% endcall %}
</div>
{% endblock %}
{% block scripts %}
<script>
function imapForm() {
return {
showPassword: false,
isTesting: false,
isSaving: false,
testResult: '',
testStatus: '',
saveResult: '',
saveStatus: '',
togglePassword() {
this.showPassword = !this.showPassword;
const passwordInput = document.getElementById('imap_password');
passwordInput.type = this.showPassword ? 'text' : 'password';
},
async testConnection() {
this.isTesting = true;
this.testResult = '';
const formData = new FormData(document.getElementById('imap-form'));
const data = Object.fromEntries(formData.entries());
data.imap_ssl = formData.get('imap_ssl') === 'on';
try {
const response = await fetch('/api/v1/admin/test-imap', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(data)
});
const result = await response.json();
if (response.ok) {
this.testStatus = 'success';
this.testResult = 'Successfully connected to the IMAP server. Found ' + (result.message_count || 0) + ' messages in the inbox.';
} else {
throw new Error(result.detail || 'Failed to connect to the IMAP server');
}
} catch (error) {
this.testStatus = 'error';
this.testResult = `Connection failed: ${error.message}`;
} finally {
this.isTesting = false;
}
},
init() {
// Load existing IMAP settings
this.loadImapSettings();
// Update percent display
const percentInput = document.getElementById('percent');
const percentDisplay = document.getElementById('percent-display');
if (percentInput && percentDisplay) {
percentInput.addEventListener('input', function() {
percentDisplay.textContent = this.value + '%';
});
}
// Handle form submission
const form = document.getElementById('imap-form');
form.addEventListener('submit', async (e) => {
e.preventDefault();
await this.saveImapSettings();
});
},
async loadImapSettings() {
try {
const response = await fetch('/api/v1/admin/imap-settings');
if (response.ok) {
const settings = await response.json();
document.getElementById('imap_server').value = settings.imap_server || '';
document.getElementById('imap_port').value = settings.imap_port || 993;
document.getElementById('imap_username').value = settings.imap_username || '';
document.getElementById('imap_password').value = settings.imap_password || '';
document.getElementById('imap_ssl').checked = settings.imap_ssl !== false;
document.getElementById('polling_interval').value = settings.polling_interval || 60;
}
} catch (error) {
console.error('Failed to load IMAP settings:', error);
}
},
async saveImapSettings() {
this.isSaving = true;
this.saveResult = '';
const formData = new FormData(document.getElementById('imap-form'));
const data = Object.fromEntries(formData.entries());
data.imap_ssl = formData.get('imap_ssl') === 'on';
try {
const response = await fetch('/api/v1/admin/imap-settings', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(data)
});
const result = await response.json();
if (response.ok) {
this.saveStatus = 'success';
this.saveResult = 'IMAP settings saved successfully. The system will now check for new DMARC reports based on your settings.';
} else {
throw new Error(result.detail || 'Failed to save IMAP settings');
}
} catch (error) {
this.saveStatus = 'error';
this.saveResult = `Save failed: ${error.message}`;
} finally {
this.isSaving = false;
}
}
};
}
// Initialize any scripts after DOM load
document.addEventListener('DOMContentLoaded', function() {
// DMARC policy form handling
const policyForm = document.getElementById('dmarc-policy-form');
if (policyForm) {
policyForm.addEventListener('submit', function(e) {
e.preventDefault();
// In a real app, you would save the policy settings here
alert('DMARC policy settings saved');
});
}
});
</script>
{% endblock %}
+203
View File
@@ -0,0 +1,203 @@
{% 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 %}
{% from "components/ui/alert.html" import alert, alert_title, alert_description %}
{% block title %}Upload DMARC Reports - DMARQ{% endblock %}
{% block page_title %}Upload DMARC Reports{% endblock %}
{% block content %}
<div class="grid gap-4 md:gap-8 py-4">
<!-- Upload Card -->
{% call card() %}
{% call card_header() %}
{% call card_title() %}Upload DMARC Reports{% endcall %}
{% call card_description() %}
Upload your DMARC aggregate report files (XML, ZIP, or GZIP)
{% endcall %}
{% endcall %}
{% call card_content() %}
<form id="upload-form" enctype="multipart/form-data" class="space-y-6" x-data="uploadForm()">
<div
class="flex flex-col items-center justify-center border-2 border-dashed border-border rounded-lg p-8 text-center hover:bg-muted/50 transition-colors cursor-pointer relative"
x-on:dragover.prevent="dragover = true"
x-on:dragleave.prevent="dragover = false"
x-on:drop.prevent="handleDrop($event)"
x-bind:class="{'border-primary/50 bg-primary/5': dragover}"
>
<div class="mb-4 text-muted-foreground">
<svg xmlns="http://www.w3.org/2000/svg" width="40" height="40" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="mx-auto"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"></path><polyline points="17 8 12 3 7 8"></polyline><line x1="12" x2="12" y1="3" y2="15"></line></svg>
</div>
<p class="mb-2 text-base font-medium">
<span class="text-primary">Click to upload</span> or drag and drop
</p>
<p class="text-sm text-muted-foreground">
XML, ZIP, or GZIP files only
</p>
<input id="report-file" type="file" name="file" accept=".xml,.zip,.gz,.gzip" class="absolute inset-0 w-full h-full opacity-0 cursor-pointer" x-on:change="handleFileSelect" />
</div>
<div id="file-selected" x-show="selectedFile" class="p-3 bg-muted rounded-md" x-cloak>
<div class="flex items-center justify-between">
<div class="flex items-center space-x-2">
<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="text-primary"><path d="M14.5 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V7.5L14.5 2z"></path><polyline points="14 2 14 8 20 8"></polyline></svg>
<span id="file-name" class="text-sm font-medium" x-text="selectedFile"></span>
</div>
<button type="button" id="remove-file" class="text-muted-foreground hover:text-foreground" x-on:click="clearFile">
<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"><path d="M18 6 6 18"></path><path d="m6 6 12 12"></path></svg>
</button>
</div>
</div>
<div id="upload-result" x-show="uploadResult" x-cloak>
<template x-if="uploadStatus === 'success'">
{% call alert(variant="success") %}
{% call alert_title() %}Upload Successful{% endcall %}
{% call alert_description() %}
<p x-text="uploadResult"></p>
{% endcall %}
{% endcall %}
</template>
<template x-if="uploadStatus === 'error'">
{% call alert(variant="error") %}
{% call alert_title() %}Upload Failed{% endcall %}
{% call alert_description() %}
<p x-text="uploadResult"></p>
{% endcall %}
{% endcall %}
</template>
<template x-if="uploadStatus === 'processing'">
{% call alert(variant="info") %}
{% call alert_title() %}Processing{% endcall %}
{% call alert_description() %}
<div class="flex items-center space-x-2">
<svg class="animate-spin h-5 w-5 text-primary" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
</svg>
<span>Uploading and processing your DMARC report...</span>
</div>
{% endcall %}
{% endcall %}
</template>
</div>
<div class="flex justify-end">
<button
type="submit"
class="btn btn-default btn-md"
x-bind:disabled="!selectedFile || isUploading"
>
<span x-show="!isUploading">
<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="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"></path><polyline points="17 8 12 3 7 8"></polyline><line x1="12" x2="12" y1="3" y2="15"></line></svg>
Upload Report
</span>
<span x-show="isUploading" class="flex items-center">
<svg class="animate-spin -ml-1 mr-2 h-4 w-4 text-white" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
</svg>
Uploading...
</span>
</button>
</div>
</form>
{% endcall %}
{% endcall %}
</div>
{% endblock %}
{% block scripts %}
<script>
function uploadForm() {
return {
selectedFile: '',
isUploading: false,
dragover: false,
uploadResult: '',
uploadStatus: '',
handleFileSelect(event) {
const file = event.target.files[0];
if (file) {
this.selectedFile = file.name;
}
},
handleDrop(event) {
this.dragover = false;
const file = event.dataTransfer.files[0];
if (file && (file.name.endsWith('.xml') || file.name.endsWith('.zip') || file.name.endsWith('.gz') || file.name.endsWith('.gzip'))) {
document.getElementById('report-file').files = event.dataTransfer.files;
this.selectedFile = file.name;
} else {
this.uploadStatus = 'error';
this.uploadResult = 'Invalid file type. Please upload XML, ZIP, or GZIP files only.';
}
},
clearFile() {
this.selectedFile = '';
this.uploadStatus = '';
this.uploadResult = '';
document.getElementById('report-file').value = '';
},
init() {
const form = document.getElementById('upload-form');
form.addEventListener('submit', async (e) => {
e.preventDefault();
const fileInput = document.getElementById('report-file');
if (!fileInput.files.length) {
this.uploadStatus = 'error';
this.uploadResult = 'Please select a file to upload.';
return;
}
this.isUploading = true;
this.uploadStatus = 'processing';
this.uploadResult = 'Uploading and processing report...';
const formData = new FormData();
formData.append('file', fileInput.files[0]);
try {
const response = await fetch('/api/v1/reports/upload', {
method: 'POST',
body: formData
});
const data = await response.json();
if (response.ok) {
this.uploadStatus = 'success';
this.uploadResult = `Report processed successfully. Found ${data.processed_records || 0} records for domain ${data.domain || ''}.`;
this.selectedFile = '';
fileInput.value = '';
// Refresh dashboard data after 1 second
setTimeout(() => {
window.dispatchEvent(new CustomEvent('dmarq:refresh-data'));
}, 1000);
} else {
throw new Error(data.detail || 'Unknown error');
}
} catch (error) {
this.uploadStatus = 'error';
this.uploadResult = `Upload failed: ${error.message}`;
} finally {
this.isUploading = false;
}
});
}
};
}
</script>
{% endblock %}