Files
CIAgent/apps/api/app/services/security_email_service.py
saksham 1a4c80958f Initial commit: CI Agent competitive-intelligence monitoring app
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).
2026-08-05 10:48:20 -04:00

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)