refactor(auth, email_service, user_service, templates): update email handling and improve base email template styling

This commit is contained in:
Davidson Gomes 2025-05-12 19:23:11 -03:00
parent 47307a1045
commit a46402fd08
4 changed files with 114 additions and 90 deletions

View File

@ -54,8 +54,7 @@ async def register_user(user_data: UserCreate, db: Session = Depends(get_db)):
Raises: Raises:
HTTPException: If there is an error in registration HTTPException: If there is an error in registration
""" """
# TODO: remover o auto_verify temporariamente para teste user, message = create_user(db, user_data, is_admin=False, auto_verify=False)
user, message = create_user(db, user_data, is_admin=False, auto_verify=True)
if not user: if not user:
logger.error(f"Error registering user: {message}") logger.error(f"Error registering user: {message}")
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=message) raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=message)

View File

@ -1,6 +1,5 @@
import sendgrid import sendgrid
from sendgrid.helpers.mail import Mail, Email, To, Content from sendgrid.helpers.mail import Mail, Email, To, Content
from src.config.settings import settings
import logging import logging
from datetime import datetime from datetime import datetime
from jinja2 import Environment, FileSystemLoader, select_autoescape from jinja2 import Environment, FileSystemLoader, select_autoescape
@ -51,12 +50,12 @@ def send_verification_email(email: str, token: str) -> bool:
bool: True if the email was sent successfully, False otherwise bool: True if the email was sent successfully, False otherwise
""" """
try: try:
sg = sendgrid.SendGridAPIClient(api_key=settings.SENDGRID_API_KEY) sg = sendgrid.SendGridAPIClient(api_key=os.getenv("SENDGRID_API_KEY"))
from_email = Email(settings.EMAIL_FROM) from_email = Email(os.getenv("EMAIL_FROM"))
to_email = To(email) to_email = To(email)
subject = "Email Verification - Evo AI" subject = "Email Verification - Evo AI"
verification_link = f"{settings.APP_URL}/api/v1/auth/verify-email/{token}" verification_link = f"{os.getenv('APP_URL')}/security/verify-email?code={token}"
html_content = _render_template( html_content = _render_template(
"verification_email", "verification_email",
@ -100,12 +99,12 @@ def send_password_reset_email(email: str, token: str) -> bool:
bool: True if the email was sent successfully, False otherwise bool: True if the email was sent successfully, False otherwise
""" """
try: try:
sg = sendgrid.SendGridAPIClient(api_key=settings.SENDGRID_API_KEY) sg = sendgrid.SendGridAPIClient(api_key=os.getenv("SENDGRID_API_KEY"))
from_email = Email(settings.EMAIL_FROM) from_email = Email(os.getenv("EMAIL_FROM"))
to_email = To(email) to_email = To(email)
subject = "Password Reset - Evo AI" subject = "Password Reset - Evo AI"
reset_link = f"{settings.APP_URL}/reset-password?token={token}" reset_link = f"{os.getenv('APP_URL')}/security/reset-password?token={token}"
html_content = _render_template( html_content = _render_template(
"password_reset", "password_reset",
@ -149,12 +148,12 @@ def send_welcome_email(email: str, user_name: str = None) -> bool:
bool: True if the email was sent successfully, False otherwise bool: True if the email was sent successfully, False otherwise
""" """
try: try:
sg = sendgrid.SendGridAPIClient(api_key=settings.SENDGRID_API_KEY) sg = sendgrid.SendGridAPIClient(api_key=os.getenv("SENDGRID_API_KEY"))
from_email = Email(settings.EMAIL_FROM) from_email = Email(os.getenv("EMAIL_FROM"))
to_email = To(email) to_email = To(email)
subject = "Welcome to Evo AI" subject = "Welcome to Evo AI"
dashboard_link = f"{settings.APP_URL}/dashboard" dashboard_link = f"{os.getenv('APP_URL')}/dashboard"
html_content = _render_template( html_content = _render_template(
"welcome_email", "welcome_email",
@ -200,12 +199,14 @@ def send_account_locked_email(
bool: True if the email was sent successfully, False otherwise bool: True if the email was sent successfully, False otherwise
""" """
try: try:
sg = sendgrid.SendGridAPIClient(api_key=settings.SENDGRID_API_KEY) sg = sendgrid.SendGridAPIClient(api_key=os.getenv("SENDGRID_API_KEY"))
from_email = Email(settings.EMAIL_FROM) from_email = Email(os.getenv("EMAIL_FROM"))
to_email = To(email) to_email = To(email)
subject = "Security Alert - Account Locked" subject = "Security Alert - Account Locked"
reset_link = f"{settings.APP_URL}/reset-password?token={reset_token}" reset_link = (
f"{os.getenv('APP_URL')}/security/reset-password?token={reset_token}"
)
html_content = _render_template( html_content = _render_template(
"account_locked", "account_locked",

View File

@ -7,7 +7,7 @@ from src.services.email_service import (
send_verification_email, send_verification_email,
send_password_reset_email, send_password_reset_email,
) )
from datetime import datetime, timedelta from datetime import datetime, timedelta, timezone
import uuid import uuid
import logging import logging
from typing import Optional, Tuple from typing import Optional, Tuple
@ -295,7 +295,11 @@ def reset_password(db: Session, token: str, new_password: str) -> Tuple[bool, st
return False, "Invalid password reset token" return False, "Invalid password reset token"
# Check if the token has expired # Check if the token has expired
if user.password_reset_expiry < datetime.utcnow(): now = datetime.now(timezone.utc)
expiry = user.password_reset_expiry
if expiry is not None and expiry.tzinfo is None:
expiry = expiry.replace(tzinfo=timezone.utc)
if expiry is None or expiry < now:
logger.warning( logger.warning(
f"Attempt to reset password with expired token for user: {user.email}" f"Attempt to reset password with expired token for user: {user.email}"
) )

View File

@ -1,83 +1,103 @@
<!DOCTYPE html> <!DOCTYPE html>
<html> <html>
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>{% block title %}Evo AI{% endblock %}</title> <title>{% block title %}Evo AI{% endblock %}</title>
<style> <style>
body { body {
font-family: Arial, sans-serif; font-family: "Segoe UI", Arial, sans-serif;
line-height: 1.6; background-color: #181a20;
margin: 0; color: #f1f1f1;
padding: 0; margin: 0;
background-color: #f7f7f7; padding: 0;
}
.container {
max-width: 480px;
margin: 32px auto;
background: #23262f;
border-radius: 12px;
box-shadow: 0 4px 24px rgba(0, 0, 0, 0.25);
padding: 0;
overflow: hidden;
}
.header {
background: #23262f;
border-bottom: 1px solid #222;
padding: 32px 0 16px 0;
text-align: center;
}
.header h1 {
color: #00ff9a;
font-size: 2rem;
margin: 0;
letter-spacing: 1px;
}
.content {
padding: 32px 24px 24px 24px;
}
.button {
background: linear-gradient(90deg, #00ff9a 0%, #00e0ff 100%);
color: #181a20 !important;
padding: 14px 0;
border-radius: 6px;
text-decoration: none;
display: block;
font-weight: bold;
text-align: center;
font-size: 1.1rem;
margin: 32px 0 0 0;
transition: filter 0.2s;
box-shadow: 0 2px 8px rgba(0, 255, 154, 0.1);
}
.button:hover {
filter: brightness(1.1);
}
.footer {
font-size: 12px;
text-align: center;
color: #888;
background: #181a20;
border-top: 1px solid #222;
padding: 20px 0 10px 0;
}
.link {
color: #00ff9a;
text-decoration: underline;
word-break: break-all;
}
.warning {
color: #e74c3c;
background: #2c1b1b;
border-radius: 4px;
padding: 12px;
margin-top: 20px;
}
@media (max-width: 600px) {
.container {
max-width: 98vw;
margin: 8px;
} }
.container { .content {
max-width: 600px; padding: 18px 8px 16px 8px;
margin: 0 auto;
padding: 20px;
background-color: #ffffff;
border-radius: 8px;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
}
.header {
background-color: #4A90E2;
color: white;
padding: 15px;
text-align: center;
border-radius: 6px 6px 0 0;
}
.content {
padding: 20px;
}
.button {
background-color: #4A90E2;
color: white !important;
padding: 12px 24px;
text-decoration: none;
border-radius: 4px;
display: inline-block;
font-weight: bold;
text-align: center;
transition: background-color 0.3s;
}
.button:hover {
background-color: #3a7bc8;
}
.footer {
font-size: 12px;
text-align: center;
margin-top: 30px;
color: #888;
border-top: 1px solid #eee;
padding-top: 20px;
}
.link {
word-break: break-all;
color: #4A90E2;
}
.warning {
color: #E74C3C;
padding: 10px;
background-color: #FADBD8;
border-radius: 4px;
margin-top: 20px;
} }
}
</style> </style>
{% block additional_styles %}{% endblock %} {% block additional_styles %}{% endblock %}
</head> </head>
<body> <body>
<div class="container"> <div class="container">
<div class="header"> <div class="header">
<h1>{% block header %}Evo AI{% endblock %}</h1> <h1>{% block header %}Evo AI{% endblock %}</h1>
</div> </div>
<div class="content"> <div class="content">{% block content %}{% endblock %}</div>
{% block content %}{% endblock %} <div class="footer">
</div> <p>
<div class="footer"> {% block footer_message %}This is an automated email, please do not
<p>{% block footer_message %}This is an automated email, please do not reply.{% endblock %}</p> reply.{% endblock %}
<p>&copy; {{ current_year }} Evo AI. All rights reserved.</p> </p>
</div> <p>&copy; {{ current_year }} Evo AI. All rights reserved.</p>
</div>
</div> </div>
</body> </body>
</html> </html>