Use notification templates for welcome and password reset emails

Co-authored-by: cadtoolbox <12723486+cadtoolbox@users.noreply.github.com>
This commit is contained in:
copilot-swe-agent[bot]
2026-02-10 00:48:09 +00:00
co-authored by cadtoolbox
parent 9631e006d2
commit 94e01499b7
3 changed files with 174 additions and 5 deletions
+7 -3
View File
@@ -35,7 +35,7 @@ from backend.app.schemas.auth import (
UserResponse,
)
from backend.app.services.email_service import (
create_password_reset_email,
create_password_reset_email_from_template,
generate_secure_password,
get_smtp_settings,
save_smtp_settings,
@@ -559,7 +559,9 @@ async def forgot_password(request: ForgotPasswordRequest, db: AsyncSession = Dep
login_url = await get_external_login_url(db)
# Send password reset email
subject, text_body, html_body = create_password_reset_email(user.username, new_password, login_url)
subject, text_body, html_body = await create_password_reset_email_from_template(
db, user.username, new_password, login_url
)
send_email(smtp_settings, user.email, subject, text_body, html_body)
logger.info(f"Password reset email sent to {user.email}")
@@ -633,7 +635,9 @@ async def reset_user_password(
login_url = await get_external_login_url(db)
# Send password reset email
subject, text_body, html_body = create_password_reset_email(user.username, new_password, login_url)
subject, text_body, html_body = await create_password_reset_email_from_template(
db, user.username, new_password, login_url
)
send_email(smtp_settings, user.email, subject, text_body, html_body)
logger.info(f"Password reset by admin {admin_user.username} for user {user.username}")
+4 -2
View File
@@ -19,7 +19,7 @@ from backend.app.models.settings import Settings
from backend.app.models.user import User
from backend.app.schemas.auth import ChangePasswordRequest, GroupBrief, UserCreate, UserResponse, UserUpdate
from backend.app.services.email_service import (
create_welcome_email,
create_welcome_email_from_template,
generate_secure_password,
get_smtp_settings,
send_email,
@@ -153,7 +153,9 @@ async def create_user(
smtp_settings = await get_smtp_settings(db)
if smtp_settings:
login_url = await get_external_login_url(db)
subject, text_body, html_body = create_welcome_email(new_user.username, password, login_url)
subject, text_body, html_body = await create_welcome_email_from_template(
db, new_user.username, password, login_url
)
send_email(smtp_settings, new_user.email, subject, text_body, html_body)
logger.info(f"Welcome email sent to {new_user.email}")
else:
+163
View File
@@ -3,15 +3,18 @@
from __future__ import annotations
import logging
import re
import secrets
import smtplib
import string
from email.mime.multipart import MIMEMultipart
from email.mime.text import MIMEText
from typing import Any
from sqlalchemy import select
from sqlalchemy.ext.asyncio import AsyncSession
from backend.app.models.notification_template import NotificationTemplate
from backend.app.models.settings import Settings
from backend.app.schemas.auth import SMTPSettings
@@ -53,6 +56,40 @@ def generate_secure_password(length: int = 16) -> str:
return "".join(password_chars)
async def get_notification_template(db: AsyncSession, event_type: str) -> NotificationTemplate | None:
"""Get a notification template by event type from database.
Args:
db: Database session
event_type: Type of event (e.g., 'user_created', 'password_reset')
Returns:
NotificationTemplate object or None if not found
"""
result = await db.execute(
select(NotificationTemplate).where(NotificationTemplate.event_type == event_type)
)
return result.scalar_one_or_none()
def render_template(template_str: str, variables: dict[str, Any]) -> str:
"""Render a template string with variables.
Args:
template_str: Template string with {variable} placeholders
variables: Dictionary of variables to substitute
Returns:
Rendered template string
"""
result = template_str
for key, value in variables.items():
result = result.replace("{" + key + "}", str(value) if value is not None else "")
# Remove any remaining unreplaced placeholders
result = re.sub(r"\{[a-z_]+\}", "", result)
return result
async def get_smtp_settings(db: AsyncSession) -> SMTPSettings | None:
"""Get SMTP settings from database.
@@ -346,3 +383,129 @@ BamBuddy Team
"""
return subject, text_body, html_body
async def create_welcome_email_from_template(
db: AsyncSession, username: str, password: str, login_url: str, app_name: str = "BamBuddy"
) -> tuple[str, str, str]:
"""Create welcome email content using notification template from database.
Args:
db: Database session
username: Username of the new user
password: Auto-generated password
login_url: URL to login page
app_name: Application name (default: BamBuddy)
Returns:
Tuple of (subject, text_body, html_body)
"""
# Try to get template from database
template = await get_notification_template(db, "user_created")
if template:
# Render template with variables
variables = {
"app_name": app_name,
"username": username,
"password": password,
"login_url": login_url,
}
subject = render_template(template.title_template, variables)
text_body = render_template(template.body_template, variables)
# Create HTML version with embedded login button
html_body = f"""<!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, #667eea 0%, #764ba2 100%); padding: 20px; border-radius: 8px 8px 0 0;">
<h1 style="color: white; margin: 0; font-size: 24px;">{subject}</h1>
</div>
<div style="background: #f9f9f9; padding: 30px; border-radius: 0 0 8px 8px; border: 1px solid #ddd; border-top: none;">
<div style="white-space: pre-wrap; font-size: 16px;">{text_body}</div>
<div style="text-align: center; margin: 30px 0;">
<a href="{login_url}" style="display: inline-block; background: #667eea; color: white; padding: 12px 30px; text-decoration: none; border-radius: 4px; font-weight: bold;">Login Now</a>
</div>
</div>
</body>
</html>
"""
logger.info("Using custom welcome email template from database")
return subject, text_body, html_body
else:
# Fallback to hardcoded template
logger.warning("No welcome email template found in database, using default")
return create_welcome_email(username, password, login_url)
async def create_password_reset_email_from_template(
db: AsyncSession, username: str, password: str, login_url: str, app_name: str = "BamBuddy"
) -> tuple[str, str, str]:
"""Create password reset email content using notification template from database.
Args:
db: Database session
username: Username of the user
password: New auto-generated password
login_url: URL to login page
app_name: Application name (default: BamBuddy)
Returns:
Tuple of (subject, text_body, html_body)
"""
# Try to get template from database
template = await get_notification_template(db, "password_reset")
if template:
# Render template with variables
variables = {
"app_name": app_name,
"username": username,
"password": password,
"login_url": login_url,
}
subject = render_template(template.title_template, variables)
text_body = render_template(template.body_template, variables)
# Create HTML version with embedded login button
html_body = f"""<!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, #667eea 0%, #764ba2 100%); padding: 20px; border-radius: 8px 8px 0 0;">
<h1 style="color: white; margin: 0; font-size: 24px;">{subject}</h1>
</div>
<div style="background: #f9f9f9; padding: 30px; border-radius: 0 0 8px 8px; border: 1px solid #ddd; border-top: none;">
<div style="white-space: pre-wrap; font-size: 16px;">{text_body}</div>
<div style="text-align: center; margin: 30px 0;">
<a href="{login_url}" style="display: inline-block; background: #667eea; color: white; padding: 12px 30px; text-decoration: none; border-radius: 4px; font-weight: bold;">Login Now</a>
</div>
<div style="background: #fff3cd; border: 1px solid #ffc107; border-radius: 4px; padding: 15px; margin: 20px 0;">
<p style="margin: 0; font-size: 14px; color: #856404;">
<strong>⚠️ Security Alert:</strong> If you did not request this password reset, please contact your administrator immediately.
</p>
</div>
</div>
</body>
</html>
"""
logger.info("Using custom password reset email template from database")
return subject, text_body, html_body
else:
# Fallback to hardcoded template
logger.warning("No password reset email template found in database, using default")
return create_password_reset_email(username, password, login_url)