From 94e01499b7e1c36323a978c277db994118d8a827 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 10 Feb 2026 00:48:09 +0000 Subject: [PATCH] Use notification templates for welcome and password reset emails Co-authored-by: cadtoolbox <12723486+cadtoolbox@users.noreply.github.com> --- backend/app/api/routes/auth.py | 10 +- backend/app/api/routes/users.py | 6 +- backend/app/services/email_service.py | 163 ++++++++++++++++++++++++++ 3 files changed, 174 insertions(+), 5 deletions(-) diff --git a/backend/app/api/routes/auth.py b/backend/app/api/routes/auth.py index d3981a320..5e2f858a0 100644 --- a/backend/app/api/routes/auth.py +++ b/backend/app/api/routes/auth.py @@ -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}") diff --git a/backend/app/api/routes/users.py b/backend/app/api/routes/users.py index 8eb8d2ae8..735542f21 100644 --- a/backend/app/api/routes/users.py +++ b/backend/app/api/routes/users.py @@ -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: diff --git a/backend/app/services/email_service.py b/backend/app/services/email_service.py index a19d9fcbd..f0c60167b 100644 --- a/backend/app/services/email_service.py +++ b/backend/app/services/email_service.py @@ -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""" + + + + + + +
+

{subject}

+
+
+
{text_body}
+ +
+ Login Now +
+
+ + +""" + + 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""" + + + + + + +
+

{subject}

+
+
+
{text_body}
+ +
+ Login Now +
+ +
+

+ ⚠️ Security Alert: If you did not request this password reset, please contact your administrator immediately. +

+
+
+ + +""" + + 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)