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
+178 -24
View File
@@ -1,15 +1,67 @@
from fastapi import FastAPI, Request
from fastapi import FastAPI, Request, BackgroundTasks
from fastapi.middleware.cors import CORSMiddleware
from fastapi.staticfiles import StaticFiles
from fastapi.templating import Jinja2Templates
from fastapi.responses import HTMLResponse
import os
import asyncio
import logging
from datetime import datetime
from app.api.api_v1.api import api_router
from app.core.config import get_settings
from app.services.imap_client import IMAPClient
from app.services.report_store import ReportStore
# Set up logging
logger = logging.getLogger(__name__)
settings = get_settings()
# Global variables for background task management
background_task = None
last_check_time = None
async def scheduled_imap_polling():
"""Background task for periodically checking IMAP for new DMARC reports"""
global last_check_time
try:
# How often to check for emails (in seconds)
check_interval = 3600 # Default: 1 hour
while True:
logger.info("Starting scheduled IMAP polling for DMARC reports")
try:
# Create IMAP client and fetch reports
imap_client = IMAPClient(delete_emails=False)
results = imap_client.fetch_reports(days=9999)
# Update last check time
last_check_time = datetime.now()
if results["success"]:
logger.info(f"IMAP polling completed: {results['processed']} emails processed, "
f"{results['reports_found']} reports found")
# If new domains were found, log them
if results["new_domains"]:
logger.info(f"New domains found: {', '.join(results['new_domains'])}")
else:
logger.error(f"IMAP polling failed: {results.get('error', 'Unknown error')}")
except Exception as e:
logger.error(f"Error in IMAP polling task: {str(e)}")
# Wait for the next check interval
await asyncio.sleep(check_interval)
except asyncio.CancelledError:
logger.info("IMAP polling task cancelled")
def create_app() -> FastAPI:
"""Create and configure the FastAPI application"""
@@ -35,6 +87,31 @@ def create_app() -> FastAPI:
# Mount static files directory
app.mount("/static", StaticFiles(directory=os.path.join(os.path.dirname(__file__), "static")), name="static")
# Set up event handlers for startup and shutdown
@app.on_event("startup")
async def startup_event():
"""Initialize background tasks on application startup"""
global background_task
# Check if IMAP credentials are configured
if all([settings.IMAP_SERVER, settings.IMAP_USERNAME, settings.IMAP_PASSWORD]):
logger.info("Starting IMAP polling background task")
background_task = asyncio.create_task(scheduled_imap_polling())
else:
logger.warning("IMAP credentials not fully configured, polling disabled")
@app.on_event("shutdown")
async def shutdown_event():
"""Clean up background tasks on application shutdown"""
global background_task
if background_task:
logger.info("Cancelling IMAP polling background task")
background_task.cancel()
try:
await background_task
except asyncio.CancelledError:
pass
return app
@@ -46,33 +123,110 @@ templates = Jinja2Templates(directory=templates_dir)
@app.get("/", response_class=HTMLResponse)
async def root(request: Request):
"""Root endpoint that serves the main HTML page"""
return templates.TemplateResponse(
"index.html",
{"request": request, "app_name": settings.PROJECT_NAME}
)
async def dashboard(request: Request):
return templates.TemplateResponse("index.html", {"request": request})
# Frontend routes that should return the SPA template
# Individual page routes
@app.get("/dashboard", response_class=HTMLResponse)
@app.get("/login", response_class=HTMLResponse)
@app.get("/setup", response_class=HTMLResponse)
@app.get("/domains", response_class=HTMLResponse)
@app.get("/reports", response_class=HTMLResponse)
@app.get("/settings", response_class=HTMLResponse)
async def serve_spa(request: Request):
"""Serve the SPA for frontend routes"""
async def dashboard(request: Request):
return templates.TemplateResponse(
"index.html",
{"request": request, "app_name": settings.PROJECT_NAME}
"dashboard.html", {"request": request, "app_name": settings.PROJECT_NAME}
)
# Fallback for other routes (404 handling)
@app.get("/{path:path}", response_class=HTMLResponse)
async def catch_all(request: Request, path: str):
"""Catch-all route that serves the main HTML page for client-side routing"""
@app.get("/login", response_class=HTMLResponse)
async def login(request: Request):
return templates.TemplateResponse(
"index.html",
{"request": request, "app_name": settings.PROJECT_NAME}
)
"login.html", {"request": request, "app_name": settings.PROJECT_NAME}
)
@app.get("/setup", response_class=HTMLResponse)
async def setup(request: Request):
return templates.TemplateResponse(
"setup.html", {"request": request, "app_name": settings.PROJECT_NAME}
)
@app.get("/domains", response_class=HTMLResponse)
async def domains(request: Request):
return templates.TemplateResponse("domains.html", {"request": request})
@app.get("/domain/{domain_id}", response_class=HTMLResponse)
async def domain_details(request: Request, domain_id: str):
"""View detailed reports for a specific domain"""
store = ReportStore.get_instance()
domains = store.get_domains()
if domain_id not in domains:
# Domain not found, redirect to domains list
return templates.TemplateResponse(
"domains.html",
{"request": request, "error": f"Domain {domain_id} not found"}
)
domain_summary = store.get_domain_summary(domain_id)
return templates.TemplateResponse(
"domain_details.html",
{
"request": request,
"domain_id": domain_id,
"domain": {
"name": domain_id,
"description": "", # Add description if available
"policy": domain_summary.get("policy", "unknown")
}
}
)
@app.get("/reports", response_class=HTMLResponse)
async def reports(request: Request):
return templates.TemplateResponse("reports.html", {"request": request})
@app.get("/settings", response_class=HTMLResponse)
async def settings_page(request: Request):
return templates.TemplateResponse("settings.html", {"request": request})
@app.get("/upload", response_class=HTMLResponse)
async def upload_page(request: Request):
return templates.TemplateResponse("upload.html", {"request": request})
# API endpoint to manually trigger IMAP polling
@app.post("/api/v1/admin/trigger-poll")
async def trigger_imap_poll(background_tasks: BackgroundTasks):
"""Manually trigger IMAP polling (admin only)"""
global last_check_time
try:
# Create IMAP client and fetch reports
imap_client = IMAPClient(delete_emails=False)
results = imap_client.fetch_reports(days=7)
# Update last check time
last_check_time = datetime.now()
return {
"success": results["success"],
"timestamp": last_check_time.isoformat(),
"processed": results["processed"],
"reports_found": results["reports_found"],
"new_domains": results["new_domains"]
}
except Exception as e:
logger.error(f"Error triggering IMAP poll: {str(e)}")
return {
"success": False,
"error": str(e)
}
# API endpoint to check status of IMAP polling
@app.get("/api/v1/admin/poll-status")
async def get_poll_status():
"""Get the status of IMAP polling"""
global last_check_time
return {
"is_running": background_task is not None and not background_task.done(),
"last_check": last_check_time.isoformat() if last_check_time else None
}