feat: Email Service Integration with AWS SES
- Implemented backend/services/email_service.py with boto3 SES client - Added EmailService class with verification and password reset methods - Updated auth router to use the new email service - Configured docker-compose.yml to pass AWS SES environment variables
This commit is contained in:
parent
bc804a666b
commit
b73f993475
3 changed files with 157 additions and 1 deletions
|
|
@ -8,7 +8,7 @@ from database import get_session
|
||||||
from models import User, Profile
|
from models import User, Profile
|
||||||
from schemas import UserCreate, Token, UserRead
|
from schemas import UserCreate, Token, UserRead
|
||||||
from auth import verify_password, get_password_hash, create_access_token, ACCESS_TOKEN_EXPIRE_MINUTES, get_current_user
|
from auth import verify_password, get_password_hash, create_access_token, ACCESS_TOKEN_EXPIRE_MINUTES, get_current_user
|
||||||
from email_service import (
|
from services.email_service import (
|
||||||
send_verification_email, send_password_reset_email,
|
send_verification_email, send_password_reset_email,
|
||||||
generate_token, get_verification_expiry, get_reset_expiry
|
generate_token, get_verification_expiry, get_reset_expiry
|
||||||
)
|
)
|
||||||
|
|
|
||||||
151
backend/services/email_service.py
Normal file
151
backend/services/email_service.py
Normal file
|
|
@ -0,0 +1,151 @@
|
||||||
|
import os
|
||||||
|
import boto3
|
||||||
|
import secrets
|
||||||
|
from datetime import datetime, timedelta
|
||||||
|
from botocore.exceptions import ClientError
|
||||||
|
from typing import Optional
|
||||||
|
|
||||||
|
class EmailService:
|
||||||
|
def __init__(self):
|
||||||
|
self.region_name = os.getenv("AWS_SES_REGION", "us-east-1")
|
||||||
|
self.aws_access_key_id = os.getenv("AWS_ACCESS_KEY_ID")
|
||||||
|
self.aws_secret_access_key = os.getenv("AWS_SECRET_ACCESS_KEY")
|
||||||
|
self.email_from = os.getenv("EMAIL_FROM", "noreply@elmeg.xyz")
|
||||||
|
self.frontend_url = os.getenv("FRONTEND_URL", "https://elmeg.xyz")
|
||||||
|
|
||||||
|
# Initialize SES client if credentials exist
|
||||||
|
if self.aws_access_key_id and self.aws_secret_access_key:
|
||||||
|
self.client = boto3.client(
|
||||||
|
"ses",
|
||||||
|
region_name=self.region_name,
|
||||||
|
aws_access_key_id=self.aws_access_key_id,
|
||||||
|
aws_secret_access_key=self.aws_secret_access_key,
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
self.client = None
|
||||||
|
print("WARNING: AWS credentials not found. Email service running in dummy mode.")
|
||||||
|
|
||||||
|
def send_email(self, to_email: str, subject: str, html_content: str, text_content: str):
|
||||||
|
"""Send an email using AWS SES"""
|
||||||
|
if not self.client:
|
||||||
|
print(f"DUMMY EMAIL to {to_email}: {subject}")
|
||||||
|
print(text_content)
|
||||||
|
return True
|
||||||
|
|
||||||
|
try:
|
||||||
|
response = self.client.send_email(
|
||||||
|
Source=self.email_from,
|
||||||
|
Destination={
|
||||||
|
"ToAddresses": [to_email],
|
||||||
|
},
|
||||||
|
Message={
|
||||||
|
"Subject": {
|
||||||
|
"Data": subject,
|
||||||
|
"Charset": "UTF-8",
|
||||||
|
},
|
||||||
|
"Body": {
|
||||||
|
"Html": {
|
||||||
|
"Data": html_content,
|
||||||
|
"Charset": "UTF-8",
|
||||||
|
},
|
||||||
|
"Text": {
|
||||||
|
"Data": text_content,
|
||||||
|
"Charset": "UTF-8",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
)
|
||||||
|
return response
|
||||||
|
except ClientError as e:
|
||||||
|
print(f"Error sending email: {e.response['Error']['Message']}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
# Global instance
|
||||||
|
email_service = EmailService()
|
||||||
|
|
||||||
|
# --- Helper Functions (used by auth router) ---
|
||||||
|
|
||||||
|
def generate_token() -> str:
|
||||||
|
"""Generate a secure random token"""
|
||||||
|
return secrets.token_urlsafe(32)
|
||||||
|
|
||||||
|
def get_verification_expiry() -> datetime:
|
||||||
|
"""Get expiration time for verification token (48 hours)"""
|
||||||
|
return datetime.utcnow() + timedelta(hours=48)
|
||||||
|
|
||||||
|
def get_reset_expiry() -> datetime:
|
||||||
|
"""Get expiration time for reset token (1 hour)"""
|
||||||
|
return datetime.utcnow() + timedelta(hours=1)
|
||||||
|
|
||||||
|
def send_verification_email(to_email: str, token: str):
|
||||||
|
"""Send account verification email"""
|
||||||
|
verify_url = f"{email_service.frontend_url}/verify-email?token={token}"
|
||||||
|
|
||||||
|
subject = "Verify your Elmeg account"
|
||||||
|
|
||||||
|
html_content = f"""
|
||||||
|
<html>
|
||||||
|
<body style="font-family: Arial, sans-serif; line-height: 1.6; color: #333;">
|
||||||
|
<div style="max-width: 600px; margin: 0 auto; padding: 20px;">
|
||||||
|
<h2 style="color: #2563eb;">Welcome to Elmeg!</h2>
|
||||||
|
<p>Thanks for signing up. Please verify your email address to get started.</p>
|
||||||
|
<div style="margin: 30px 0;">
|
||||||
|
<a href="{verify_url}" style="background-color: #2563eb; color: white; padding: 12px 24px; text-decoration: none; border-radius: 4px; font-weight: bold;">Verify Email Address</a>
|
||||||
|
</div>
|
||||||
|
<p style="font-size: 14px; color: #666;">Or copy this link to your browser:</p>
|
||||||
|
<p style="font-size: 12px; color: #666;">{verify_url}</p>
|
||||||
|
<hr style="border: 0; border-top: 1px solid #eee; margin: 30px 0;">
|
||||||
|
<p style="font-size: 12px; color: #999;">If you didn't create an account, you can safely ignore this email.</p>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
"""
|
||||||
|
|
||||||
|
text_content = f"""
|
||||||
|
Welcome to Elmeg!
|
||||||
|
|
||||||
|
Please verify your email address by visiting this link:
|
||||||
|
{verify_url}
|
||||||
|
|
||||||
|
If you didn't create an account, safely ignore this email.
|
||||||
|
"""
|
||||||
|
|
||||||
|
return email_service.send_email(to_email, subject, html_content, text_content)
|
||||||
|
|
||||||
|
def send_password_reset_email(to_email: str, token: str):
|
||||||
|
"""Send password reset email"""
|
||||||
|
reset_url = f"{email_service.frontend_url}/reset-password?token={token}"
|
||||||
|
|
||||||
|
subject = "Reset your Elmeg password"
|
||||||
|
|
||||||
|
html_content = f"""
|
||||||
|
<html>
|
||||||
|
<body style="font-family: Arial, sans-serif; line-height: 1.6; color: #333;">
|
||||||
|
<div style="max-width: 600px; margin: 0 auto; padding: 20px;">
|
||||||
|
<h2 style="color: #2563eb;">Password Reset Request</h2>
|
||||||
|
<p>We received a request to reset your password. Click the button below to choose a new one.</p>
|
||||||
|
<div style="margin: 30px 0;">
|
||||||
|
<a href="{reset_url}" style="background-color: #2563eb; color: white; padding: 12px 24px; text-decoration: none; border-radius: 4px; font-weight: bold;">Reset Password</a>
|
||||||
|
</div>
|
||||||
|
<p style="font-size: 14px; color: #666;">Or copy this link to your browser:</p>
|
||||||
|
<p style="font-size: 12px; color: #666;">{reset_url}</p>
|
||||||
|
<p style="font-size: 14px; color: #666;">This link expires in 1 hour.</p>
|
||||||
|
<hr style="border: 0; border-top: 1px solid #eee; margin: 30px 0;">
|
||||||
|
<p style="font-size: 12px; color: #999;">If you didn't request a password reset, you can safely ignore this email.</p>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
"""
|
||||||
|
|
||||||
|
text_content = f"""
|
||||||
|
Reset your Elmeg password
|
||||||
|
|
||||||
|
Click the link below to choose a new password:
|
||||||
|
{reset_url}
|
||||||
|
|
||||||
|
This link expires in 1 hour.
|
||||||
|
|
||||||
|
If you didn't request a password reset, safely ignore this email.
|
||||||
|
"""
|
||||||
|
|
||||||
|
return email_service.send_email(to_email, subject, html_content, text_content)
|
||||||
|
|
@ -11,6 +11,11 @@ services:
|
||||||
environment:
|
environment:
|
||||||
- DATABASE_URL=postgresql://elmeg:elmeg_password@db:5432/elmeg
|
- DATABASE_URL=postgresql://elmeg:elmeg_password@db:5432/elmeg
|
||||||
- SECRET_KEY=${SECRET_KEY:-demo-secret-change-in-production}
|
- SECRET_KEY=${SECRET_KEY:-demo-secret-change-in-production}
|
||||||
|
- AWS_ACCESS_KEY_ID=${AWS_ACCESS_KEY_ID}
|
||||||
|
- AWS_SECRET_ACCESS_KEY=${AWS_SECRET_ACCESS_KEY}
|
||||||
|
- AWS_SES_REGION=${AWS_SES_REGION}
|
||||||
|
- EMAIL_FROM=${EMAIL_FROM}
|
||||||
|
- FRONTEND_URL=${FRONTEND_URL:-https://elmeg.xyz}
|
||||||
command: sh start.sh
|
command: sh start.sh
|
||||||
depends_on:
|
depends_on:
|
||||||
- db
|
- db
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue