Files
orion/app/modules/messaging/routes/api/admin_email_templates.py
Samir Boulahtit ce822af883
Some checks failed
CI / ruff (push) Successful in 9s
CI / pytest (push) Failing after 47m32s
CI / validate (push) Successful in 23s
CI / dependency-scanning (push) Successful in 29s
CI / docs (push) Has been skipped
CI / deploy (push) Has been skipped
feat: production launch — email audit, team invites, security headers, router fixes
- Fix loyalty & monitoring router bugs (_get_router → named routers)
- Implement team invitation email with send_template + seed templates (en/fr/de)
- Add SecurityHeadersMiddleware (nosniff, HSTS, referrer-policy, permissions-policy)
- Build email audit admin page: service, schemas, API, page route, menu, i18n, HTML, JS
- Clean stale TODO in platform-menu-config.js
- Add 67 tests (unit + integration) covering all new functionality

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-27 18:24:30 +01:00

444 lines
13 KiB
Python

# app/modules/messaging/routes/api/admin_email_templates.py
"""
Admin email template management endpoints.
Allows platform administrators to:
- View all email templates
- Edit template content for all languages
- Preview templates with sample data
- Send test emails
- View email logs
"""
import logging
from typing import Any
from fastapi import APIRouter, Depends
from pydantic import BaseModel, EmailStr, Field
from sqlalchemy.orm import Session
from app.api.deps import get_current_admin_api
from app.core.database import get_db
from app.modules.messaging.schemas.email import (
EmailLogDetail,
EmailLogListResponse,
EmailLogStatsResponse,
)
from app.modules.messaging.services.email_service import EmailService
from app.modules.messaging.services.email_template_service import EmailTemplateService
from app.modules.tenancy.schemas.auth import UserContext
admin_email_templates_router = APIRouter(prefix="/email-templates")
admin_email_logs_router = APIRouter(prefix="/email-logs")
logger = logging.getLogger(__name__)
# =============================================================================
# SCHEMAS
# =============================================================================
class TemplateUpdate(BaseModel):
"""Schema for updating a platform template."""
subject: str = Field(..., min_length=1, max_length=500)
body_html: str = Field(..., min_length=1)
body_text: str | None = None
class PreviewRequest(BaseModel):
"""Schema for previewing a template."""
template_code: str
language: str = "en"
variables: dict[str, Any] = {}
class TestEmailRequest(BaseModel):
"""Schema for sending a test email."""
template_code: str
language: str = "en"
to_email: EmailStr
variables: dict[str, Any] = {}
class TemplateListItem(BaseModel):
"""Schema for a template in the list."""
code: str
name: str
description: str | None = None
category: str
languages: list[str] # Matches service output field name
is_platform_only: bool = False
variables: list[str] = []
class Config:
from_attributes = True
class TemplateListResponse(BaseModel):
"""Response schema for listing templates."""
templates: list[TemplateListItem]
class CategoriesResponse(BaseModel):
"""Response schema for template categories."""
categories: list[str]
# =============================================================================
# ENDPOINTS
# =============================================================================
@admin_email_templates_router.get("", response_model=TemplateListResponse)
def list_templates(
current_user: UserContext = Depends(get_current_admin_api),
db: Session = Depends(get_db),
):
"""
List all platform email templates.
Returns templates grouped by code with available languages.
"""
service = EmailTemplateService(db)
return TemplateListResponse(templates=service.list_platform_templates())
@admin_email_templates_router.get("/categories", response_model=CategoriesResponse)
def get_categories(
current_user: UserContext = Depends(get_current_admin_api),
db: Session = Depends(get_db),
):
"""Get list of email template categories."""
service = EmailTemplateService(db)
return CategoriesResponse(categories=service.get_template_categories())
@admin_email_templates_router.get("/{code}")
def get_template(
code: str,
current_user: UserContext = Depends(get_current_admin_api),
db: Session = Depends(get_db),
):
"""
Get a specific template with all language versions.
Returns template metadata and content for all available languages.
"""
service = EmailTemplateService(db)
return service.get_platform_template(code)
@admin_email_templates_router.get("/{code}/{language}")
def get_template_language(
code: str,
language: str,
current_user: UserContext = Depends(get_current_admin_api),
db: Session = Depends(get_db),
):
"""
Get a specific template for a specific language.
Returns template content with variables information.
"""
service = EmailTemplateService(db)
template = service.get_platform_template_language(code, language)
return {
"code": template.code,
"language": template.language,
"name": template.name,
"description": template.description,
"category": template.category,
"subject": template.subject,
"body_html": template.body_html,
"body_text": template.body_text,
"variables": template.variables,
"required_variables": template.required_variables,
"is_platform_only": template.is_platform_only,
}
@admin_email_templates_router.put("/{code}/{language}")
def update_template(
code: str,
language: str,
template_data: TemplateUpdate,
current_user: UserContext = Depends(get_current_admin_api),
db: Session = Depends(get_db),
):
"""
Update a platform email template.
Updates the template content for a specific language.
"""
service = EmailTemplateService(db)
service.update_platform_template(
code=code,
language=language,
subject=template_data.subject,
body_html=template_data.body_html,
body_text=template_data.body_text,
)
db.commit()
return {"message": "Template updated successfully"}
@admin_email_templates_router.post("/{code}/preview")
def preview_template(
code: str,
preview_data: PreviewRequest,
current_user: UserContext = Depends(get_current_admin_api),
db: Session = Depends(get_db),
):
"""
Preview a template with sample variables.
Renders the template with provided variables and returns the result.
"""
service = EmailTemplateService(db)
# Merge with sample variables if not provided
variables = {
**_get_sample_variables(code),
**preview_data.variables,
}
return service.preview_template(code, preview_data.language, variables)
@admin_email_templates_router.post("/{code}/test")
def send_test_email(
code: str,
test_data: TestEmailRequest,
current_user: UserContext = Depends(get_current_admin_api),
db: Session = Depends(get_db),
):
"""
Send a test email using the template.
Sends the template to the specified email address with sample data.
"""
# Merge with sample variables
variables = {
**_get_sample_variables(code),
**test_data.variables,
}
try:
email_svc = EmailService(db)
email_log = email_svc.send_template(
template_code=code,
to_email=test_data.to_email,
variables=variables,
language=test_data.language,
)
if email_log.status == "sent":
return {
"success": True,
"message": f"Test email sent to {test_data.to_email}",
}
return {
"success": False,
"message": email_log.error_message or "Failed to send email",
}
except Exception as e:
logger.exception(f"Failed to send test email: {e}")
return {
"success": False,
"message": str(e),
}
@admin_email_templates_router.get("/{code}/logs")
def get_template_logs(
code: str,
limit: int = 50,
offset: int = 0,
current_user: UserContext = Depends(get_current_admin_api),
db: Session = Depends(get_db),
):
"""
Get email logs for a specific template.
Returns recent email send attempts for the template.
"""
service = EmailTemplateService(db)
logs, total = service.get_template_logs(code, limit, offset)
return {
"logs": logs,
"total": total,
"limit": limit,
"offset": offset,
}
# =============================================================================
# HELPERS
# =============================================================================
def _get_sample_variables(template_code: str) -> dict[str, Any]:
"""Get sample variables for testing templates."""
samples = {
"signup_welcome": {
"first_name": "John",
"merchant_name": "Acme Corp",
"email": "john@example.com",
"store_code": "acme",
"login_url": "https://example.com/login",
"trial_days": "14",
"tier_name": "Business",
"platform_name": "Orion",
},
"order_confirmation": {
"customer_name": "Jane Doe",
"order_number": "ORD-12345",
"order_total": "€99.99",
"order_items_count": "3",
"order_date": "2024-01-15",
"shipping_address": "123 Main St, Luxembourg City, L-1234",
"platform_name": "Orion",
},
"password_reset": {
"customer_name": "John Doe",
"reset_link": "https://example.com/reset?token=abc123",
"expiry_hours": "1",
"platform_name": "Orion",
},
"team_invite": {
"invitee_name": "Jane",
"inviter_name": "John",
"store_name": "Acme Corp",
"role": "Admin",
"accept_url": "https://example.com/accept",
"expires_in_days": "7",
"platform_name": "Orion",
},
"team_invitation": {
"invited_by_name": "John Doe",
"store_name": "Acme Corp",
"role_name": "Manager",
"acceptance_link": "https://example.com/store/invitation/accept?token=abc123",
"expiry_days": "7",
"platform_name": "Orion",
},
"subscription_welcome": {
"store_name": "Acme Corp",
"tier_name": "Business",
"billing_cycle": "Monthly",
"amount": "€49.99",
"next_billing_date": "2024-02-15",
"dashboard_url": "https://example.com/dashboard",
"platform_name": "Orion",
},
"payment_failed": {
"store_name": "Acme Corp",
"tier_name": "Business",
"amount": "€49.99",
"retry_date": "2024-01-18",
"update_payment_url": "https://example.com/billing",
"support_email": "support@orion.lu",
"platform_name": "Orion",
},
"subscription_cancelled": {
"store_name": "Acme Corp",
"tier_name": "Business",
"end_date": "2024-02-15",
"reactivate_url": "https://example.com/billing",
"platform_name": "Orion",
},
"trial_ending": {
"store_name": "Acme Corp",
"tier_name": "Business",
"days_remaining": "3",
"trial_end_date": "2024-01-18",
"upgrade_url": "https://example.com/upgrade",
"features_list": "Unlimited products, API access, Priority support",
"platform_name": "Orion",
},
}
return samples.get(template_code, {"platform_name": "Orion"})
# =============================================================================
# EMAIL LOG (AUDIT) ENDPOINTS
# =============================================================================
@admin_email_logs_router.get("", response_model=EmailLogListResponse)
def list_email_logs(
page: int = 1,
per_page: int = 50,
search: str | None = None,
status: str | None = None,
template_code: str | None = None,
store_id: int | None = None,
date_from: str | None = None,
date_to: str | None = None,
current_user: UserContext = Depends(get_current_admin_api),
db: Session = Depends(get_db),
):
"""
Get paginated email logs with filters.
Supports filtering by recipient email search, status, template type,
store, and date range.
"""
filters = {}
if search:
filters["search"] = search
if status:
filters["status"] = status
if template_code:
filters["template_code"] = template_code
if store_id:
filters["store_id"] = store_id
if date_from:
filters["date_from"] = date_from
if date_to:
filters["date_to"] = date_to
skip = (page - 1) * per_page
service = EmailTemplateService(db)
items, total = service.get_email_logs(filters=filters, skip=skip, limit=per_page)
total_pages = (total + per_page - 1) // per_page
return EmailLogListResponse(
items=items,
total=total,
page=page,
per_page=per_page,
total_pages=total_pages,
)
@admin_email_logs_router.get("/stats", response_model=EmailLogStatsResponse)
def get_email_log_stats(
current_user: UserContext = Depends(get_current_admin_api),
db: Session = Depends(get_db),
):
"""Get email log statistics: counts by status and by template type."""
service = EmailTemplateService(db)
return service.get_email_log_stats()
@admin_email_logs_router.get("/{log_id}", response_model=EmailLogDetail)
def get_email_log_detail(
log_id: int,
current_user: UserContext = Depends(get_current_admin_api),
db: Session = Depends(get_db),
):
"""Get full detail for a single email log including body content for preview."""
service = EmailTemplateService(db)
return service.get_email_log_detail(log_id)