FastAPI + Celery + Next.js + Postgres/Redis app with company monitoring, source collection, LLM-based change analysis, enrichment, and account security (Turnstile, escalating lockout, email verification).
78 lines
3.3 KiB
Python
78 lines
3.3 KiB
Python
"""Transactional account-security email (verification codes, password
|
|
reset codes, lockout notices) - deliberately separate from the alert-
|
|
notification path in app/services/alert_service.py, which dispatches to
|
|
user-configured destinations via app/notifications/factory.py. This is
|
|
always sent to the account's own registered email, from a distinct sender
|
|
identity (settings.resend_security_from_email, e.g. [email protected]
|
|
vs. [email protected]).
|
|
|
|
Picks the Resend HTTP API provider when RESEND_API_KEY is configured, else
|
|
falls back to the existing SmtpEmailProvider. `resolve_provider` is also
|
|
reused by app.services.unban_service for the admin unban-request
|
|
notification, which has the same "Resend if configured, else SMTP" needs.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
from app.core.config import Settings
|
|
from app.notifications.base import DeliveryResult, NotificationMessage
|
|
from app.notifications.resend_email import ResendEmailProvider
|
|
from app.notifications.smtp_email import SmtpEmailProvider
|
|
|
|
|
|
def resolve_provider(settings: Settings):
|
|
if settings.resend_api_key:
|
|
return ResendEmailProvider(settings)
|
|
return SmtpEmailProvider(settings)
|
|
|
|
|
|
async def send_verification_code_email(settings: Settings, to: str, code: str) -> DeliveryResult:
|
|
message = NotificationMessage(
|
|
destination_value=to,
|
|
subject="Verify your CI Agent account",
|
|
body_text=(
|
|
f"Your verification code is {code}.\n\n"
|
|
"This code expires in 36 hours. If you didn't request this, you can ignore this email."
|
|
),
|
|
body_html=(
|
|
f"<p>Your verification code is <strong style='font-size:20px'>{code}</strong>.</p>"
|
|
"<p>This code expires in 36 hours. If you didn't request this, you can ignore this email.</p>"
|
|
),
|
|
)
|
|
return await resolve_provider(settings).send(message)
|
|
|
|
|
|
async def send_password_reset_email(settings: Settings, to: str, code: str) -> DeliveryResult:
|
|
message = NotificationMessage(
|
|
destination_value=to,
|
|
subject="Reset your CI Agent password",
|
|
body_text=(
|
|
f"Your password reset code is {code}.\n\n"
|
|
"This code expires in 36 hours. If you didn't request this, you can ignore this email "
|
|
"and your password will stay unchanged."
|
|
),
|
|
body_html=(
|
|
f"<p>Your password reset code is <strong style='font-size:20px'>{code}</strong>.</p>"
|
|
"<p>This code expires in 36 hours. If you didn't request this, you can ignore this "
|
|
"email and your password will stay unchanged.</p>"
|
|
),
|
|
)
|
|
return await resolve_provider(settings).send(message)
|
|
|
|
|
|
async def send_account_locked_email(settings: Settings, to: str) -> DeliveryResult:
|
|
reset_link = f"{settings.frontend_url}/forgot-password"
|
|
message = NotificationMessage(
|
|
destination_value=to,
|
|
subject="Your CI Agent account was locked",
|
|
body_text=(
|
|
"Your account was locked after repeated failed login attempts.\n\n"
|
|
f"To unlock it, reset your password: {reset_link}"
|
|
),
|
|
body_html=(
|
|
"<p>Your account was locked after repeated failed login attempts.</p>"
|
|
f'<p>To unlock it, <a href="{reset_link}">reset your password</a>.</p>'
|
|
),
|
|
)
|
|
return await resolve_provider(settings).send(message)
|