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:
@@ -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 %}
|
||||
@@ -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 %}
|
||||
@@ -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 %}
|
||||
@@ -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 %}
|
||||
+285
-852
File diff suppressed because it is too large
Load Diff
@@ -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>
|
||||
@@ -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 %}
|
||||
@@ -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 %}
|
||||
@@ -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 %}
|
||||
Reference in New Issue
Block a user