feat: production launch — email audit, team invites, security headers, router fixes
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

- 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>
This commit is contained in:
2026-02-27 18:24:30 +01:00
parent 4ebd419987
commit ce822af883
25 changed files with 2485 additions and 19 deletions

View File

@@ -1763,6 +1763,180 @@ Dese Link leeft an {{ expiry_hours }} Stonn(en) of. Wann Dir des Passwuertzrecks
Mat beschte Greiss,
D'{{ platform_name }} Team
""",
},
# -------------------------------------------------------------------------
# TEAM INVITATION
# -------------------------------------------------------------------------
{
"code": "team_invitation",
"language": "en",
"name": "Team Invitation",
"description": "Sent when a team member is invited to a store",
"category": EmailCategory.SYSTEM.value,
"variables": json.dumps([
"invited_by_name", "store_name", "role_name",
"acceptance_link", "expiry_days"
]),
"subject": "You've been invited to join {{ store_name }}",
"body_html": """<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
</head>
<body style="font-family: Arial, sans-serif; line-height: 1.6; color: #333; max-width: 600px; margin: 0 auto; padding: 20px;">
<div style="background: linear-gradient(135deg, #6366f1 0%, #8b5cf6 100%); padding: 30px; border-radius: 10px 10px 0 0;">
<h1 style="color: white; margin: 0; font-size: 28px;">Team Invitation</h1>
</div>
<div style="background: #f9fafb; padding: 30px; border-radius: 0 0 10px 10px;">
<p style="font-size: 16px;">Hello,</p>
<p><strong>{{ invited_by_name }}</strong> has invited you to join <strong>{{ store_name }}</strong> as a <strong>{{ role_name }}</strong>.</p>
<div style="text-align: center; margin: 30px 0;">
<a href="{{ acceptance_link }}" style="background: #6366f1; color: white; padding: 14px 28px; text-decoration: none; border-radius: 8px; font-weight: bold; display: inline-block;">
Accept Invitation
</a>
</div>
<p style="color: #6b7280; font-size: 14px;">This invitation expires in {{ expiry_days }} days. If you did not expect this invitation, you can safely ignore this email.</p>
<p>Best regards,<br><strong>The Orion Team</strong></p>
</div>
<div style="text-align: center; padding: 20px; color: #9ca3af; font-size: 12px;">
<p>&copy; 2024 Orion. Built for Luxembourg e-commerce.</p>
</div>
</body>
</html>""",
"body_text": """Team Invitation
Hello,
{{ invited_by_name }} has invited you to join {{ store_name }} as a {{ role_name }}.
Accept Invitation: {{ acceptance_link }}
This invitation expires in {{ expiry_days }} days. If you did not expect this invitation, you can safely ignore this email.
Best regards,
The Orion Team
""",
},
{
"code": "team_invitation",
"language": "fr",
"name": "Invitation d'équipe",
"description": "Envoyé lorsqu'un membre est invité à rejoindre une boutique",
"category": EmailCategory.SYSTEM.value,
"variables": json.dumps([
"invited_by_name", "store_name", "role_name",
"acceptance_link", "expiry_days"
]),
"subject": "Vous avez été invité(e) à rejoindre {{ store_name }}",
"body_html": """<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
</head>
<body style="font-family: Arial, sans-serif; line-height: 1.6; color: #333; max-width: 600px; margin: 0 auto; padding: 20px;">
<div style="background: linear-gradient(135deg, #6366f1 0%, #8b5cf6 100%); padding: 30px; border-radius: 10px 10px 0 0;">
<h1 style="color: white; margin: 0; font-size: 28px;">Invitation d'équipe</h1>
</div>
<div style="background: #f9fafb; padding: 30px; border-radius: 0 0 10px 10px;">
<p style="font-size: 16px;">Bonjour,</p>
<p><strong>{{ invited_by_name }}</strong> vous a invité(e) à rejoindre <strong>{{ store_name }}</strong> en tant que <strong>{{ role_name }}</strong>.</p>
<div style="text-align: center; margin: 30px 0;">
<a href="{{ acceptance_link }}" style="background: #6366f1; color: white; padding: 14px 28px; text-decoration: none; border-radius: 8px; font-weight: bold; display: inline-block;">
Accepter l'invitation
</a>
</div>
<p style="color: #6b7280; font-size: 14px;">Cette invitation expire dans {{ expiry_days }} jours. Si vous n'attendiez pas cette invitation, vous pouvez ignorer cet email.</p>
<p>Cordialement,<br><strong>L'équipe Orion</strong></p>
</div>
<div style="text-align: center; padding: 20px; color: #9ca3af; font-size: 12px;">
<p>&copy; 2024 Orion. Conçu pour le e-commerce luxembourgeois.</p>
</div>
</body>
</html>""",
"body_text": """Invitation d'équipe
Bonjour,
{{ invited_by_name }} vous a invité(e) à rejoindre {{ store_name }} en tant que {{ role_name }}.
Accepter l'invitation : {{ acceptance_link }}
Cette invitation expire dans {{ expiry_days }} jours. Si vous n'attendiez pas cette invitation, vous pouvez ignorer cet email.
Cordialement,
L'équipe Orion
""",
},
{
"code": "team_invitation",
"language": "de",
"name": "Teameinladung",
"description": "Gesendet wenn ein Teammitglied zu einem Shop eingeladen wird",
"category": EmailCategory.SYSTEM.value,
"variables": json.dumps([
"invited_by_name", "store_name", "role_name",
"acceptance_link", "expiry_days"
]),
"subject": "Sie wurden eingeladen, {{ store_name }} beizutreten",
"body_html": """<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
</head>
<body style="font-family: Arial, sans-serif; line-height: 1.6; color: #333; max-width: 600px; margin: 0 auto; padding: 20px;">
<div style="background: linear-gradient(135deg, #6366f1 0%, #8b5cf6 100%); padding: 30px; border-radius: 10px 10px 0 0;">
<h1 style="color: white; margin: 0; font-size: 28px;">Teameinladung</h1>
</div>
<div style="background: #f9fafb; padding: 30px; border-radius: 0 0 10px 10px;">
<p style="font-size: 16px;">Hallo,</p>
<p><strong>{{ invited_by_name }}</strong> hat Sie eingeladen, <strong>{{ store_name }}</strong> als <strong>{{ role_name }}</strong> beizutreten.</p>
<div style="text-align: center; margin: 30px 0;">
<a href="{{ acceptance_link }}" style="background: #6366f1; color: white; padding: 14px 28px; text-decoration: none; border-radius: 8px; font-weight: bold; display: inline-block;">
Einladung annehmen
</a>
</div>
<p style="color: #6b7280; font-size: 14px;">Diese Einladung läuft in {{ expiry_days }} Tagen ab. Wenn Sie diese Einladung nicht erwartet haben, können Sie diese E-Mail ignorieren.</p>
<p>Mit freundlichen Grüßen,<br><strong>Das Orion-Team</strong></p>
</div>
<div style="text-align: center; padding: 20px; color: #9ca3af; font-size: 12px;">
<p>&copy; 2024 Orion. Entwickelt für den luxemburgischen E-Commerce.</p>
</div>
</body>
</html>""",
"body_text": """Teameinladung
Hallo,
{{ invited_by_name }} hat Sie eingeladen, {{ store_name }} als {{ role_name }} beizutreten.
Einladung annehmen: {{ acceptance_link }}
Diese Einladung läuft in {{ expiry_days }} Tagen ab. Wenn Sie diese Einladung nicht erwartet haben, können Sie diese E-Mail ignorieren.
Mit freundlichen Grüßen,
Das Orion-Team
""",
},
]