Add initial MVP documentation for DMARQ platform, detailing backend architecture, frontend implementation, and deployment structure
This commit is contained in:
@@ -0,0 +1,122 @@
|
||||
from typing import List, Optional, Dict, Any
|
||||
from fastapi import APIRouter, HTTPException, status
|
||||
from pydantic import BaseModel
|
||||
|
||||
from app.services.report_store import ReportStore
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
class DomainBase(BaseModel):
|
||||
"""Base Domain schema"""
|
||||
name: str
|
||||
description: Optional[str] = None
|
||||
policy: Optional[str] = None
|
||||
|
||||
class DomainResponse(DomainBase):
|
||||
"""Domain response schema"""
|
||||
reports_count: int = 0
|
||||
emails_count: int = 0
|
||||
compliance_rate: float = 0.0
|
||||
|
||||
class DomainSummaryResponse(BaseModel):
|
||||
"""Domain summary for dashboard"""
|
||||
total_domains: int
|
||||
total_emails: int
|
||||
overall_pass_rate: float
|
||||
reports_processed: int
|
||||
domains: List[Dict[str, Any]]
|
||||
|
||||
@router.get("/summary", response_model=DomainSummaryResponse)
|
||||
async def get_domains_summary():
|
||||
"""
|
||||
Get summary statistics for all domains, formatted for the dashboard.
|
||||
"""
|
||||
store = ReportStore.get_instance()
|
||||
domains = store.get_domains()
|
||||
summaries = store.get_all_domain_summaries()
|
||||
|
||||
# Calculate overall statistics
|
||||
total_domains = len(domains)
|
||||
total_emails = 0
|
||||
total_passed = 0
|
||||
total_reports = 0
|
||||
|
||||
domains_list = []
|
||||
|
||||
for domain_name in domains:
|
||||
summary = summaries.get(domain_name, {})
|
||||
total_emails += summary.get("total_count", 0)
|
||||
total_passed += summary.get("passed_count", 0)
|
||||
total_reports += summary.get("reports_processed", 0)
|
||||
|
||||
# Format domain data for frontend
|
||||
domains_list.append({
|
||||
"id": domain_name, # Using the domain name as ID for now
|
||||
"domain_name": domain_name,
|
||||
"total_emails": summary.get("total_count", 0),
|
||||
"passed_count": summary.get("passed_count", 0),
|
||||
"failed_count": summary.get("failed_count", 0),
|
||||
"pass_rate": summary.get("compliance_rate", 0),
|
||||
"report_count": summary.get("reports_processed", 0)
|
||||
})
|
||||
|
||||
# Calculate overall pass rate
|
||||
overall_pass_rate = 0
|
||||
if total_emails > 0:
|
||||
overall_pass_rate = round((total_passed / total_emails) * 100, 1)
|
||||
|
||||
return DomainSummaryResponse(
|
||||
total_domains=total_domains,
|
||||
total_emails=total_emails,
|
||||
overall_pass_rate=overall_pass_rate,
|
||||
reports_processed=total_reports,
|
||||
domains=domains_list
|
||||
)
|
||||
|
||||
@router.get("/domains", response_model=List[DomainResponse])
|
||||
async def read_domains():
|
||||
"""
|
||||
Retrieve domains with their statistics.
|
||||
For Milestone 1, this simply returns domains from the in-memory store.
|
||||
"""
|
||||
store = ReportStore.get_instance()
|
||||
domains = store.get_domains()
|
||||
summaries = store.get_all_domain_summaries()
|
||||
|
||||
result = []
|
||||
for domain_name in domains:
|
||||
summary = summaries.get(domain_name, {})
|
||||
domain_response = DomainResponse(
|
||||
name=domain_name,
|
||||
policy=summary.get("policy", "unknown"),
|
||||
reports_count=summary.get("reports_processed", 0),
|
||||
emails_count=summary.get("total_count", 0),
|
||||
compliance_rate=summary.get("compliance_rate", 0.0)
|
||||
)
|
||||
result.append(domain_response)
|
||||
|
||||
return result
|
||||
|
||||
@router.get("/domains/{domain_name}", response_model=DomainResponse)
|
||||
async def read_domain(domain_name: str):
|
||||
"""
|
||||
Get statistics for a specific domain.
|
||||
"""
|
||||
store = ReportStore.get_instance()
|
||||
domains = store.get_domains()
|
||||
|
||||
if domain_name not in domains:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail="Domain not found",
|
||||
)
|
||||
|
||||
summary = store.get_domain_summary(domain_name)
|
||||
|
||||
return DomainResponse(
|
||||
name=domain_name,
|
||||
policy=summary.get("policy", "unknown"),
|
||||
reports_count=summary.get("reports_processed", 0),
|
||||
emails_count=summary.get("total_count", 0),
|
||||
compliance_rate=summary.get("compliance_rate", 0.0)
|
||||
)
|
||||
@@ -0,0 +1,18 @@
|
||||
from fastapi import APIRouter
|
||||
|
||||
from app.api.api_v1.endpoints.setup import setup_status
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
@router.get("/health", status_code=200)
|
||||
async def health_check():
|
||||
"""
|
||||
Health check endpoint to verify API status.
|
||||
For Milestone 1, this simply returns status information without checking a database.
|
||||
"""
|
||||
return {
|
||||
"status": "ok",
|
||||
"version": "0.1.0",
|
||||
"service": "dmarq",
|
||||
"is_setup_complete": setup_status["is_setup_complete"]
|
||||
}
|
||||
@@ -0,0 +1,135 @@
|
||||
from typing import Dict, List, Any
|
||||
from fastapi import APIRouter, File, HTTPException, UploadFile, status
|
||||
from pydantic import BaseModel
|
||||
|
||||
from app.services.dmarc_parser import DMARCParser
|
||||
from app.services.report_store import ReportStore
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
class UploadResponse(BaseModel):
|
||||
"""Response model for report upload"""
|
||||
success: bool
|
||||
domain: str
|
||||
message: str
|
||||
processed_records: int = 0 # Added this field to track processed records
|
||||
|
||||
class DomainSummary(BaseModel):
|
||||
"""Domain summary response model"""
|
||||
domain: str
|
||||
total_count: int
|
||||
passed_count: int
|
||||
failed_count: int
|
||||
reports_processed: int
|
||||
compliance_rate: float
|
||||
|
||||
class ReportSummary(BaseModel):
|
||||
"""DMARC report summary model"""
|
||||
report_id: str
|
||||
org_name: str
|
||||
begin_date: str
|
||||
end_date: str
|
||||
total_count: int
|
||||
passed_count: int
|
||||
failed_count: int
|
||||
|
||||
@router.post("/upload", response_model=UploadResponse)
|
||||
async def upload_report(file: UploadFile = File(...)):
|
||||
"""
|
||||
Upload and process a DMARC aggregate report file (XML, ZIP, or GZIP)
|
||||
"""
|
||||
try:
|
||||
# Read the file content
|
||||
file_content = await file.read()
|
||||
filename = file.filename
|
||||
|
||||
# Parse the report
|
||||
parser = DMARCParser()
|
||||
report = parser.parse_file(file_content, filename)
|
||||
|
||||
# Store the report
|
||||
store = ReportStore.get_instance()
|
||||
store.add_report(report)
|
||||
|
||||
domain = report.get("domain", "unknown")
|
||||
processed_records = report.get("summary", {}).get("total_count", 0)
|
||||
|
||||
return UploadResponse(
|
||||
success=True,
|
||||
domain=domain,
|
||||
message=f"Report processed successfully for domain {domain}",
|
||||
processed_records=processed_records
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail=f"Error processing report: {str(e)}"
|
||||
)
|
||||
|
||||
@router.get("/domains", response_model=List[str])
|
||||
async def get_domains():
|
||||
"""
|
||||
Get list of all domains with reports
|
||||
"""
|
||||
store = ReportStore.get_instance()
|
||||
return store.get_domains()
|
||||
|
||||
@router.get("/domain/{domain}/summary", response_model=DomainSummary)
|
||||
async def get_domain_summary(domain: str):
|
||||
"""
|
||||
Get summary statistics for a specific domain
|
||||
"""
|
||||
store = ReportStore.get_instance()
|
||||
summary = store.get_domain_summary(domain)
|
||||
|
||||
if not summary:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail=f"No reports found for domain {domain}"
|
||||
)
|
||||
|
||||
return DomainSummary(
|
||||
domain=domain,
|
||||
**summary
|
||||
)
|
||||
|
||||
@router.get("/summary", response_model=List[DomainSummary])
|
||||
async def get_all_summaries():
|
||||
"""
|
||||
Get summary statistics for all domains
|
||||
"""
|
||||
store = ReportStore.get_instance()
|
||||
all_summaries = store.get_all_domain_summaries()
|
||||
|
||||
return [
|
||||
DomainSummary(domain=domain, **summary)
|
||||
for domain, summary in all_summaries.items()
|
||||
]
|
||||
|
||||
@router.get("/domain/{domain}/reports", response_model=List[ReportSummary])
|
||||
async def get_domain_reports(domain: str):
|
||||
"""
|
||||
Get all reports for a specific domain
|
||||
"""
|
||||
store = ReportStore.get_instance()
|
||||
reports = store.get_domain_reports(domain)
|
||||
|
||||
if not reports:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail=f"No reports found for domain {domain}"
|
||||
)
|
||||
|
||||
return [
|
||||
ReportSummary(
|
||||
report_id=report.get("report_id", ""),
|
||||
org_name=report.get("org_name", ""),
|
||||
begin_date=report.get("begin_date", ""),
|
||||
end_date=report.get("end_date", ""),
|
||||
total_count=report.get("summary", {}).get("total_count", 0),
|
||||
passed_count=report.get("summary", {}).get("passed_count", 0),
|
||||
failed_count=report.get("summary", {}).get("failed_count", 0)
|
||||
)
|
||||
for report in reports
|
||||
]
|
||||
@@ -0,0 +1,65 @@
|
||||
from fastapi import APIRouter, HTTPException, status
|
||||
from pydantic import BaseModel, EmailStr
|
||||
from typing import Dict, Optional
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
# Simple in-memory storage for setup status (for Milestone 1)
|
||||
setup_status = {
|
||||
"is_setup_complete": False,
|
||||
"admin_email": None,
|
||||
"app_name": "DMARQ",
|
||||
}
|
||||
|
||||
class SetupStatusResponse(BaseModel):
|
||||
"""Setup status response"""
|
||||
is_setup_complete: bool
|
||||
app_name: str
|
||||
|
||||
class AdminSetupRequest(BaseModel):
|
||||
"""Admin user setup request body"""
|
||||
email: EmailStr
|
||||
username: str
|
||||
password: str
|
||||
|
||||
class SystemConfigRequest(BaseModel):
|
||||
"""System configuration setup request body"""
|
||||
app_name: str
|
||||
base_url: str
|
||||
|
||||
@router.get("/status", response_model=SetupStatusResponse)
|
||||
async def get_setup_status():
|
||||
"""Get the current setup status"""
|
||||
return SetupStatusResponse(
|
||||
is_setup_complete=setup_status["is_setup_complete"],
|
||||
app_name=setup_status["app_name"]
|
||||
)
|
||||
|
||||
@router.post("/admin", status_code=201)
|
||||
async def setup_admin(request: AdminSetupRequest):
|
||||
"""
|
||||
Setup admin user during initial system configuration.
|
||||
For Milestone 1, this simply stores the admin email in memory.
|
||||
"""
|
||||
if setup_status["is_setup_complete"]:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail="Setup already completed"
|
||||
)
|
||||
|
||||
# Store admin email
|
||||
setup_status["admin_email"] = request.email
|
||||
|
||||
return {"message": "Admin user setup completed"}
|
||||
|
||||
@router.post("/system", status_code=200)
|
||||
async def setup_system(request: SystemConfigRequest):
|
||||
"""
|
||||
Setup system configuration.
|
||||
For Milestone 1, this simply stores the app name in memory.
|
||||
"""
|
||||
# Store app name
|
||||
setup_status["app_name"] = request.app_name
|
||||
setup_status["is_setup_complete"] = True
|
||||
|
||||
return {"message": "System settings saved successfully"}
|
||||
Reference in New Issue
Block a user