Files
Magent/backend/app/routers/recaps.py
T

145 lines
5.4 KiB
Python

from datetime import datetime, timezone
from typing import Literal
from urllib.parse import urlsplit
from uuid import UUID
from fastapi import APIRouter, Depends, HTTPException, Query, Response
from pydantic import BaseModel, ConfigDict, Field, field_validator
from ..services.public_urls import magent_public_url
from ..auth import require_admin
from ..feature_guards import require_stats
from ..services import email_recaps as recaps, recap_store as store
def no_cache(response: Response):
response.headers["Cache-Control"] = "no-store"
router = APIRouter(tags=["email-recaps"], dependencies=[Depends(no_cache)])
class StrictPayload(BaseModel):
model_config = ConfigDict(extra="forbid")
class Preference(StrictPayload):
enabled: bool
automatic_monthly: bool | None = Field(default=None, strict=True)
class RecapSettings(StrictPayload):
enabled: bool
day: int = Field(ge=1, le=28)
hour: int = Field(ge=0, le=23)
public_url: str = Field(default="", max_length=500)
@field_validator("public_url")
@classmethod
def origin_only(cls, value: str) -> str:
value = value.strip().rstrip('/')
if not value:
return value
try:
url = urlsplit(value)
port = url.port
except ValueError as exc:
raise ValueError("Enter the public Magent address, such as https://magent.example.com.") from exc
if (url.scheme not in {"http", "https"} or not url.hostname or url.username or url.password
or url.path or url.query or url.fragment or any(char.isspace() or ord(char) < 33 for char in value)
or any(char in value for char in '<>"\\') or (port is not None and port < 1)):
raise ValueError("Enter a http(s) Magent address without a path, credentials or query.")
return value
class TestEmail(StrictPayload):
month: str | None = Field(default=None, pattern=r"^[0-9]{4}-[0-9]{2}$")
request_id: UUID
class TokenAction(StrictPayload):
token: str = Field(min_length=40, max_length=100, pattern=r"^[A-Za-z0-9_-]+$")
action: Literal["confirm", "unsubscribe"]
def error(exc: recaps.RecapError):
raise HTTPException(exc.status, exc.detail) from exc
@router.get("/profile/email-recaps")
def preferences(user: dict = Depends(require_stats)) -> dict:
try:
return recaps.preferences(user)
except recaps.RecapError as exc:
error(exc)
@router.put("/profile/email-recaps")
async def preference(payload: Preference, user: dict = Depends(require_stats)) -> dict:
try:
if payload.enabled:
return await recaps.subscribe(user, payload.automatic_monthly)
store.disable(recaps.current_account(user)["id"])
return recaps.preferences(user)
except recaps.RecapError as exc:
error(exc)
@router.post("/email-recaps/check")
def check_token(payload: TokenAction) -> dict:
try:
return recaps.token_action(payload.token, payload.action)
except recaps.RecapError as exc:
error(exc)
@router.post("/email-recaps/confirm")
def apply_token(payload: TokenAction) -> dict:
try:
return recaps.token_action(payload.token, payload.action, apply=True)
except recaps.RecapError as exc:
error(exc)
@router.get("/admin/email-recaps")
def overview(offset: int = Query(default=0, ge=0), user: dict = Depends(require_admin)) -> dict:
ready, detail = recaps.delivery_ready()
months = recaps.month_periods(None, datetime.now(timezone.utc))["available_months"][1:]
return {"settings": store.settings(), "ready": ready, "detail": detail, "months": months,
"worker_enabled": recaps.worker_enabled(), **store.history(offset=offset)}
@router.put("/admin/email-recaps")
def settings(payload: RecapSettings, user: dict = Depends(require_admin)) -> dict:
if payload.enabled:
# Validate against the proposed URL without writing any partial settings.
ready, detail = recaps.smtp_email_config_ready()
runtime = recaps.get_runtime_settings()
if not magent_public_url(payload.public_url or store.settings()["public_url"]) or not ready or not recaps.worker_enabled() or not runtime.jellystat_base_url or not runtime.jellystat_api_key:
raise HTTPException(409, "Set the public address, enable SMTP email and connect Jellystat before starting the schedule." if ready else detail)
return store.save_settings({**payload.model_dump(), "public_url": magent_public_url(payload.public_url or store.settings()["public_url"])}, datetime.now(timezone.utc))
@router.get("/admin/email-recaps/preview")
async def preview(month: str | None = Query(default=None, max_length=7, pattern=r"^[0-9]{4}-[0-9]{2}$"), user: dict = Depends(require_admin)) -> dict:
try:
return await recaps.preview(user, month)
except recaps.RecapError as exc:
error(exc)
@router.post("/admin/email-recaps/test", status_code=202)
def test_email(payload: TestEmail, user: dict = Depends(require_admin)) -> dict:
try:
return recaps.queue_test(user, payload.month, str(payload.request_id))
except recaps.RecapError as exc:
error(exc)
@router.post('/profile/email-recaps/send', status_code=202)
def email_personal_report(payload: TestEmail, user: dict = Depends(require_stats)) -> dict:
try:
return recaps.queue_personal(user, payload.month, str(payload.request_id))
except recaps.RecapError as exc:
error(exc)