Spaces:
Sleeping
Sleeping
Commit ·
0b5ef0b
1
Parent(s): 1dc181f
update
Browse files- app/routers/auth.py +288 -296
- pyproject.toml +1 -0
app/routers/auth.py
CHANGED
|
@@ -1,16 +1,10 @@
|
|
| 1 |
import os
|
| 2 |
import random
|
| 3 |
-
import smtplib
|
| 4 |
-
import ssl
|
| 5 |
import string
|
| 6 |
import asyncio
|
| 7 |
import logging
|
| 8 |
-
import socket
|
| 9 |
from datetime import datetime, timedelta
|
| 10 |
-
from
|
| 11 |
-
from email.mime.text import MIMEText
|
| 12 |
-
from email.utils import formataddr
|
| 13 |
-
from typing import Optional, Dict, Any
|
| 14 |
from concurrent.futures import ThreadPoolExecutor
|
| 15 |
|
| 16 |
from fastapi import APIRouter, HTTPException, Depends, BackgroundTasks
|
|
@@ -20,6 +14,11 @@ from app.database.database_query import DatabaseQuery
|
|
| 20 |
from app.middleware.auth import create_access_token, get_current_user
|
| 21 |
from dotenv import load_dotenv
|
| 22 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 23 |
load_dotenv()
|
| 24 |
|
| 25 |
# Configure logging
|
|
@@ -29,123 +28,29 @@ logger = logging.getLogger(__name__)
|
|
| 29 |
# Thread pool for background email sending
|
| 30 |
email_executor = ThreadPoolExecutor(max_workers=3)
|
| 31 |
|
| 32 |
-
#
|
| 33 |
-
|
| 34 |
-
|
| 35 |
-
|
| 36 |
-
|
| 37 |
-
|
| 38 |
-
|
| 39 |
-
|
| 40 |
-
|
| 41 |
-
|
| 42 |
-
SMTP_HOST = _raw_host or SMTP_SERVER or "smtp.gmail.com"
|
| 43 |
-
try:
|
| 44 |
-
SMTP_PORT = int(_raw_port) if _raw_port else 587
|
| 45 |
-
except Exception:
|
| 46 |
-
SMTP_PORT = 587
|
| 47 |
-
|
| 48 |
-
SMTP_USER = os.getenv("SMTP_USER")
|
| 49 |
-
SMTP_PASSWORD = os.getenv("SMTP_PASSWORD")
|
| 50 |
-
EMAILS_FROM_EMAIL = os.getenv("EMAILS_FROM_EMAIL") or SMTP_USER
|
| 51 |
-
EMAILS_FROM_NAME = os.getenv("EMAILS_FROM_NAME") or "No Reply"
|
| 52 |
-
|
| 53 |
-
# Advanced SMTP Configuration
|
| 54 |
-
SMTP_TIMEOUT = int(os.getenv("SMTP_TIMEOUT", "30")) # Increased timeout
|
| 55 |
-
SMTP_USE_TLS = os.getenv("SMTP_USE_TLS", "true").lower() == "true"
|
| 56 |
-
SMTP_USE_SSL = os.getenv("SMTP_USE_SSL", "false").lower() == "true"
|
| 57 |
-
SMTP_DEBUG = os.getenv("SMTP_DEBUG", "false").lower() == "true"
|
| 58 |
ENABLE_EMAIL_SENDING = os.getenv("ENABLE_EMAIL_SENDING", "true").lower() == "true"
|
|
|
|
| 59 |
|
| 60 |
-
#
|
| 61 |
-
|
| 62 |
-
|
|
|
|
|
|
|
| 63 |
|
| 64 |
router = APIRouter()
|
| 65 |
query = DatabaseQuery()
|
| 66 |
|
| 67 |
|
| 68 |
-
def test_smtp_connection() -> Dict[str, Any]:
|
| 69 |
-
"""Test SMTP connection and return diagnostic information"""
|
| 70 |
-
result = {
|
| 71 |
-
"host": SMTP_HOST,
|
| 72 |
-
"port": SMTP_PORT,
|
| 73 |
-
"user": SMTP_USER,
|
| 74 |
-
"reachable": False,
|
| 75 |
-
"error": None,
|
| 76 |
-
"dns_resolved": False,
|
| 77 |
-
"ip_address": None
|
| 78 |
-
}
|
| 79 |
-
|
| 80 |
-
try:
|
| 81 |
-
# Test DNS resolution
|
| 82 |
-
ip_address = socket.gethostbyname(SMTP_HOST)
|
| 83 |
-
result["dns_resolved"] = True
|
| 84 |
-
result["ip_address"] = ip_address
|
| 85 |
-
logger.info(f"DNS resolved {SMTP_HOST} to {ip_address}")
|
| 86 |
-
|
| 87 |
-
# Test socket connection
|
| 88 |
-
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
| 89 |
-
sock.settimeout(10)
|
| 90 |
-
connection_result = sock.connect_ex((SMTP_HOST, SMTP_PORT))
|
| 91 |
-
sock.close()
|
| 92 |
-
|
| 93 |
-
if connection_result == 0:
|
| 94 |
-
result["reachable"] = True
|
| 95 |
-
logger.info(f"Successfully connected to {SMTP_HOST}:{SMTP_PORT}")
|
| 96 |
-
else:
|
| 97 |
-
result["error"] = f"Socket connection failed with code {connection_result}"
|
| 98 |
-
logger.error(f"Failed to connect to {SMTP_HOST}:{SMTP_PORT} - Error code: {connection_result}")
|
| 99 |
-
|
| 100 |
-
except socket.gaierror as e:
|
| 101 |
-
result["error"] = f"DNS resolution failed: {str(e)}"
|
| 102 |
-
logger.error(f"DNS resolution failed for {SMTP_HOST}: {str(e)}")
|
| 103 |
-
except socket.timeout:
|
| 104 |
-
result["error"] = "Connection timeout"
|
| 105 |
-
logger.error(f"Connection timeout to {SMTP_HOST}:{SMTP_PORT}")
|
| 106 |
-
except Exception as e:
|
| 107 |
-
result["error"] = str(e)
|
| 108 |
-
logger.error(f"Connection test failed: {str(e)}")
|
| 109 |
-
|
| 110 |
-
return result
|
| 111 |
-
|
| 112 |
-
|
| 113 |
-
def send_email_via_sendgrid(to_email: str, subject: str, html_content: str) -> bool:
|
| 114 |
-
"""Send email using SendGrid API as fallback"""
|
| 115 |
-
if not SENDGRID_API_KEY:
|
| 116 |
-
logger.error("SendGrid API key not configured")
|
| 117 |
-
return False
|
| 118 |
-
|
| 119 |
-
try:
|
| 120 |
-
import requests
|
| 121 |
-
|
| 122 |
-
url = "https://api.sendgrid.com/v3/mail/send"
|
| 123 |
-
headers = {
|
| 124 |
-
"Authorization": f"Bearer {SENDGRID_API_KEY}",
|
| 125 |
-
"Content-Type": "application/json"
|
| 126 |
-
}
|
| 127 |
-
|
| 128 |
-
data = {
|
| 129 |
-
"personalizations": [{"to": [{"email": to_email}]}],
|
| 130 |
-
"from": {"email": EMAILS_FROM_EMAIL, "name": EMAILS_FROM_NAME},
|
| 131 |
-
"subject": subject,
|
| 132 |
-
"content": [{"type": "text/html", "value": html_content}]
|
| 133 |
-
}
|
| 134 |
-
|
| 135 |
-
response = requests.post(url, json=data, headers=headers, timeout=10)
|
| 136 |
-
|
| 137 |
-
if response.status_code in [200, 202]:
|
| 138 |
-
logger.info(f"Email sent via SendGrid to {to_email}")
|
| 139 |
-
return True
|
| 140 |
-
else:
|
| 141 |
-
logger.error(f"SendGrid error: {response.status_code} - {response.text}")
|
| 142 |
-
return False
|
| 143 |
-
|
| 144 |
-
except Exception as e:
|
| 145 |
-
logger.error(f"SendGrid sending failed: {str(e)}")
|
| 146 |
-
return False
|
| 147 |
-
|
| 148 |
-
|
| 149 |
def send_email_sync(
|
| 150 |
to_email: str,
|
| 151 |
subject: str,
|
|
@@ -153,146 +58,42 @@ def send_email_sync(
|
|
| 153 |
failure_message: str = "Failed to send email",
|
| 154 |
raise_on_error: bool = False
|
| 155 |
) -> bool:
|
| 156 |
-
"""
|
| 157 |
-
|
| 158 |
-
# Skip if disabled
|
| 159 |
if not ENABLE_EMAIL_SENDING:
|
| 160 |
logger.info(f"Email sending disabled. Would have sent to {to_email}")
|
| 161 |
return True
|
| 162 |
-
|
| 163 |
-
|
| 164 |
-
|
| 165 |
-
connection_test = test_smtp_connection()
|
| 166 |
-
logger.info(f"SMTP Connection test: {connection_test}")
|
| 167 |
-
if not connection_test["reachable"]:
|
| 168 |
-
logger.warning(f"SMTP server not reachable: {connection_test['error']}")
|
| 169 |
-
|
| 170 |
-
# Try SendGrid as fallback
|
| 171 |
-
if USE_SENDGRID:
|
| 172 |
-
return send_email_via_sendgrid(to_email, subject, html_content)
|
| 173 |
-
|
| 174 |
-
# If no fallback, decide whether to fail
|
| 175 |
-
if raise_on_error:
|
| 176 |
-
raise HTTPException(status_code=500, detail=f"Email service unreachable: {connection_test['error']}")
|
| 177 |
-
return False
|
| 178 |
-
|
| 179 |
-
if not all([SMTP_HOST, SMTP_PORT, SMTP_USER, SMTP_PASSWORD]):
|
| 180 |
-
logger.error("Email service is not configured properly")
|
| 181 |
-
|
| 182 |
-
# Try SendGrid as fallback
|
| 183 |
-
if USE_SENDGRID:
|
| 184 |
-
return send_email_via_sendgrid(to_email, subject, html_content)
|
| 185 |
-
|
| 186 |
if raise_on_error:
|
| 187 |
-
raise HTTPException(status_code=500, detail="Email service
|
| 188 |
return False
|
| 189 |
|
| 190 |
-
|
| 191 |
-
|
| 192 |
-
|
| 193 |
-
|
| 194 |
-
|
| 195 |
-
|
| 196 |
-
|
| 197 |
-
|
| 198 |
-
|
| 199 |
-
|
| 200 |
-
|
| 201 |
-
message.attach(MIMEText(html_content, "html"))
|
| 202 |
|
| 203 |
-
|
| 204 |
-
|
| 205 |
-
|
| 206 |
-
if SMTP_USE_SSL:
|
| 207 |
-
methods_to_try.append(("SSL", 465))
|
| 208 |
-
methods_to_try.append(("STARTTLS", SMTP_PORT))
|
| 209 |
-
if SMTP_PORT not in [465, 587]:
|
| 210 |
-
methods_to_try.append(("PLAIN", SMTP_PORT))
|
| 211 |
|
| 212 |
-
|
| 213 |
-
|
| 214 |
-
|
| 215 |
-
|
| 216 |
-
|
| 217 |
-
|
| 218 |
-
|
| 219 |
-
|
| 220 |
-
|
| 221 |
-
|
| 222 |
-
if SMTP_DEBUG:
|
| 223 |
-
server.set_debuglevel(2)
|
| 224 |
-
|
| 225 |
-
auth_password = SMTP_PASSWORD
|
| 226 |
-
if "gmail" in SMTP_HOST.lower() and " " in auth_password:
|
| 227 |
-
auth_password = auth_password.replace(" ", "")
|
| 228 |
-
|
| 229 |
-
server.login(SMTP_USER, auth_password)
|
| 230 |
-
server.sendmail(SMTP_USER, [to_email], message.as_string())
|
| 231 |
-
|
| 232 |
-
elif method == "STARTTLS":
|
| 233 |
-
# STARTTLS connection (port 587 or custom)
|
| 234 |
-
context = ssl.create_default_context()
|
| 235 |
-
with smtplib.SMTP(SMTP_HOST, port, timeout=SMTP_TIMEOUT) as server:
|
| 236 |
-
if SMTP_DEBUG:
|
| 237 |
-
server.set_debuglevel(2)
|
| 238 |
-
|
| 239 |
-
# Some servers require EHLO before STARTTLS
|
| 240 |
-
server.ehlo()
|
| 241 |
-
|
| 242 |
-
if SMTP_USE_TLS:
|
| 243 |
-
server.starttls(context=context)
|
| 244 |
-
server.ehlo() # EHLO again after STARTTLS
|
| 245 |
-
|
| 246 |
-
auth_password = SMTP_PASSWORD
|
| 247 |
-
if "gmail" in SMTP_HOST.lower() and " " in auth_password:
|
| 248 |
-
auth_password = auth_password.replace(" ", "")
|
| 249 |
-
|
| 250 |
-
server.login(SMTP_USER, auth_password)
|
| 251 |
-
server.sendmail(SMTP_USER, [to_email], message.as_string())
|
| 252 |
-
|
| 253 |
-
else:
|
| 254 |
-
# Plain SMTP (port 25 or custom)
|
| 255 |
-
with smtplib.SMTP(SMTP_HOST, port, timeout=SMTP_TIMEOUT) as server:
|
| 256 |
-
if SMTP_DEBUG:
|
| 257 |
-
server.set_debuglevel(2)
|
| 258 |
-
|
| 259 |
-
auth_password = SMTP_PASSWORD
|
| 260 |
-
if "gmail" in SMTP_HOST.lower() and " " in auth_password:
|
| 261 |
-
auth_password = auth_password.replace(" ", "")
|
| 262 |
-
|
| 263 |
-
server.login(SMTP_USER, auth_password)
|
| 264 |
-
server.sendmail(SMTP_USER, [to_email], message.as_string())
|
| 265 |
-
|
| 266 |
-
logger.info(f"Email sent successfully via {method} to {to_email}")
|
| 267 |
-
return True
|
| 268 |
-
|
| 269 |
-
except socket.error as e:
|
| 270 |
-
last_error = f"Network error ({method}): {str(e)}"
|
| 271 |
-
logger.error(last_error)
|
| 272 |
-
continue
|
| 273 |
-
except smtplib.SMTPAuthenticationError as e:
|
| 274 |
-
last_error = f"Authentication failed ({method}): {str(e)}"
|
| 275 |
-
logger.error(last_error)
|
| 276 |
-
break # No point trying other methods if auth fails
|
| 277 |
-
except smtplib.SMTPException as e:
|
| 278 |
-
last_error = f"SMTP error ({method}): {str(e)}"
|
| 279 |
-
logger.error(last_error)
|
| 280 |
-
continue
|
| 281 |
-
except Exception as e:
|
| 282 |
-
last_error = f"Unexpected error ({method}): {str(e)}"
|
| 283 |
-
logger.error(last_error)
|
| 284 |
-
continue
|
| 285 |
-
|
| 286 |
-
# If all SMTP methods failed, try SendGrid
|
| 287 |
-
if USE_SENDGRID:
|
| 288 |
-
logger.info("Falling back to SendGrid")
|
| 289 |
-
return send_email_via_sendgrid(to_email, subject, html_content)
|
| 290 |
-
|
| 291 |
-
# All methods failed
|
| 292 |
-
logger.error(f"All email sending methods failed. Last error: {last_error}")
|
| 293 |
-
if raise_on_error:
|
| 294 |
-
raise HTTPException(status_code=500, detail=f"{failure_message}: {last_error}")
|
| 295 |
-
return False
|
| 296 |
|
| 297 |
|
| 298 |
async def send_email_async(
|
|
@@ -315,29 +116,118 @@ async def send_email_async(
|
|
| 315 |
)
|
| 316 |
|
| 317 |
|
| 318 |
-
# Add a test endpoint for debugging
|
| 319 |
@router.get('/test-email-connection')
|
| 320 |
async def test_email_connection():
|
| 321 |
-
"""Test endpoint to check
|
| 322 |
-
result =
|
| 323 |
-
|
| 324 |
-
"
|
| 325 |
-
"
|
| 326 |
-
"
|
| 327 |
-
"
|
| 328 |
}
|
| 329 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 330 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 331 |
class LoginRequest(BaseModel):
|
| 332 |
identifier: str
|
| 333 |
password: str
|
| 334 |
|
| 335 |
-
|
| 336 |
class LoginResponse(BaseModel):
|
| 337 |
message: str
|
| 338 |
token: str
|
| 339 |
|
| 340 |
-
|
| 341 |
class RegisterRequest(BaseModel):
|
| 342 |
username: str
|
| 343 |
email: EmailStr
|
|
@@ -345,25 +235,20 @@ class RegisterRequest(BaseModel):
|
|
| 345 |
name: str
|
| 346 |
age: int
|
| 347 |
|
| 348 |
-
|
| 349 |
class VerifyEmailRequest(BaseModel):
|
| 350 |
username: str
|
| 351 |
code: str
|
| 352 |
|
| 353 |
-
|
| 354 |
class ResendCodeRequest(BaseModel):
|
| 355 |
username: str
|
| 356 |
|
| 357 |
-
|
| 358 |
class ForgotPasswordRequest(BaseModel):
|
| 359 |
email: EmailStr
|
| 360 |
|
| 361 |
-
|
| 362 |
class ResetPasswordRequest(BaseModel):
|
| 363 |
token: str
|
| 364 |
password: str
|
| 365 |
|
| 366 |
-
|
| 367 |
class ChatSessionCheck(BaseModel):
|
| 368 |
session_id: str
|
| 369 |
|
|
@@ -418,32 +303,74 @@ async def register(register_data: RegisterRequest, background_tasks: BackgroundT
|
|
| 418 |
'code_expiration': code_expiration
|
| 419 |
}
|
| 420 |
|
| 421 |
-
# Store temp user first - this is critical
|
| 422 |
query.create_or_update_temp_user(username, email, temp_user)
|
| 423 |
|
| 424 |
-
#
|
| 425 |
email_content = f'''
|
| 426 |
-
|
| 427 |
-
|
| 428 |
-
|
| 429 |
-
<
|
| 430 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 431 |
'''
|
| 432 |
-
|
| 433 |
background_tasks.add_task(
|
| 434 |
send_email_sync,
|
| 435 |
email,
|
| 436 |
-
'Verify your
|
| 437 |
email_content,
|
| 438 |
"Failed to send verification email",
|
| 439 |
-
False
|
| 440 |
)
|
| 441 |
|
| 442 |
return {
|
| 443 |
-
"message": "Registration successful
|
| 444 |
-
"note": "
|
| 445 |
}
|
| 446 |
-
|
| 447 |
except Exception as e:
|
| 448 |
if isinstance(e, HTTPException):
|
| 449 |
raise e
|
|
@@ -479,7 +406,7 @@ async def verify_email(verify_data: VerifyEmailRequest):
|
|
| 479 |
# Set default settings
|
| 480 |
query.set_user_language(username, "English")
|
| 481 |
query.set_user_theme(username, False)
|
| 482 |
-
|
| 483 |
default_preferences = {
|
| 484 |
'keywords': True,
|
| 485 |
'references': True,
|
|
@@ -489,7 +416,7 @@ async def verify_email(verify_data: VerifyEmailRequest):
|
|
| 489 |
}
|
| 490 |
query.set_user_preferences(username, default_preferences)
|
| 491 |
|
| 492 |
-
return {"message": "Email verification successful"}
|
| 493 |
except Exception as e:
|
| 494 |
if isinstance(e, HTTPException):
|
| 495 |
raise e
|
|
@@ -514,16 +441,48 @@ async def resend_code(resend_data: ResendCodeRequest, background_tasks: Backgrou
|
|
| 514 |
query.create_or_update_temp_user(username, temp_user['email'], temp_user)
|
| 515 |
|
| 516 |
email_content = f'''
|
| 517 |
-
|
| 518 |
-
|
| 519 |
-
|
| 520 |
-
<
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 521 |
'''
|
| 522 |
-
|
| 523 |
background_tasks.add_task(
|
| 524 |
send_email_sync,
|
| 525 |
temp_user['email'],
|
| 526 |
-
'
|
| 527 |
email_content,
|
| 528 |
"Failed to send verification email",
|
| 529 |
False
|
|
@@ -564,29 +523,62 @@ async def forgot_password(data: ForgotPasswordRequest, background_tasks: Backgro
|
|
| 564 |
expiration = datetime.utcnow() + timedelta(hours=1)
|
| 565 |
|
| 566 |
query.store_reset_token(email, reset_token, expiration)
|
| 567 |
-
|
| 568 |
base_url = os.getenv("FRONTEND_URL", "http://localhost:3000")
|
| 569 |
reset_link = f"{base_url}/reset-password?token={reset_token}"
|
| 570 |
|
| 571 |
email_content = f'''
|
| 572 |
-
|
| 573 |
-
|
| 574 |
-
|
| 575 |
-
<
|
| 576 |
-
|
| 577 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 578 |
'''
|
| 579 |
-
|
| 580 |
background_tasks.add_task(
|
| 581 |
send_email_sync,
|
| 582 |
email,
|
| 583 |
-
'Reset
|
| 584 |
email_content,
|
| 585 |
"Failed to send password reset email",
|
| 586 |
False
|
| 587 |
)
|
| 588 |
|
| 589 |
-
return {"message": "Password reset instructions sent to email"}
|
| 590 |
except Exception as e:
|
| 591 |
if isinstance(e, HTTPException):
|
| 592 |
raise e
|
|
@@ -609,7 +601,7 @@ async def reset_password(data: ResetPasswordRequest):
|
|
| 609 |
hashed_password = generate_password_hash(new_password)
|
| 610 |
query.update_password(reset_info['email'], hashed_password)
|
| 611 |
|
| 612 |
-
return {"message": "Password successfully reset"}
|
| 613 |
except Exception as e:
|
| 614 |
if isinstance(e, HTTPException):
|
| 615 |
raise e
|
|
|
|
| 1 |
import os
|
| 2 |
import random
|
|
|
|
|
|
|
| 3 |
import string
|
| 4 |
import asyncio
|
| 5 |
import logging
|
|
|
|
| 6 |
from datetime import datetime, timedelta
|
| 7 |
+
from typing import Dict, Any
|
|
|
|
|
|
|
|
|
|
| 8 |
from concurrent.futures import ThreadPoolExecutor
|
| 9 |
|
| 10 |
from fastapi import APIRouter, HTTPException, Depends, BackgroundTasks
|
|
|
|
| 14 |
from app.middleware.auth import create_access_token, get_current_user
|
| 15 |
from dotenv import load_dotenv
|
| 16 |
|
| 17 |
+
# Brevo SDK imports (new)
|
| 18 |
+
import brevo_python
|
| 19 |
+
from brevo_python.rest import ApiException
|
| 20 |
+
from brevo_python.models.send_smtp_email import SendSmtpEmail
|
| 21 |
+
|
| 22 |
load_dotenv()
|
| 23 |
|
| 24 |
# Configure logging
|
|
|
|
| 28 |
# Thread pool for background email sending
|
| 29 |
email_executor = ThreadPoolExecutor(max_workers=3)
|
| 30 |
|
| 31 |
+
# Brevo API Configuration
|
| 32 |
+
# Use env var if provided, else fallback to provided key
|
| 33 |
+
BREVO_API_KEY = os.getenv(
|
| 34 |
+
"BREVO_API_KEY",
|
| 35 |
+
"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"
|
| 36 |
+
)
|
| 37 |
+
EMAILS_FROM_EMAIL = os.getenv("EMAILS_FROM_EMAIL", "noreply@yourapp.com")
|
| 38 |
+
EMAILS_FROM_NAME = os.getenv("EMAILS_FROM_NAME", "Your App")
|
| 39 |
+
|
| 40 |
+
# Configuration
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 41 |
ENABLE_EMAIL_SENDING = os.getenv("ENABLE_EMAIL_SENDING", "true").lower() == "true"
|
| 42 |
+
SMTP_DEBUG = os.getenv("SMTP_DEBUG", "false").lower() == "true"
|
| 43 |
|
| 44 |
+
# Initialize Brevo SDK client once (new)
|
| 45 |
+
brevo_config = brevo_python.Configuration()
|
| 46 |
+
brevo_config.api_key["api-key"] = BREVO_API_KEY
|
| 47 |
+
brevo_api_client = brevo_python.ApiClient(brevo_config)
|
| 48 |
+
brevo_emails_api = brevo_python.TransactionalEmailsApi(brevo_api_client)
|
| 49 |
|
| 50 |
router = APIRouter()
|
| 51 |
query = DatabaseQuery()
|
| 52 |
|
| 53 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 54 |
def send_email_sync(
|
| 55 |
to_email: str,
|
| 56 |
subject: str,
|
|
|
|
| 58 |
failure_message: str = "Failed to send email",
|
| 59 |
raise_on_error: bool = False
|
| 60 |
) -> bool:
|
| 61 |
+
"""Send email using Brevo Python SDK (TransactionalEmailsApi)"""
|
|
|
|
|
|
|
| 62 |
if not ENABLE_EMAIL_SENDING:
|
| 63 |
logger.info(f"Email sending disabled. Would have sent to {to_email}")
|
| 64 |
return True
|
| 65 |
+
|
| 66 |
+
if not BREVO_API_KEY:
|
| 67 |
+
logger.error("Brevo API key not configured")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 68 |
if raise_on_error:
|
| 69 |
+
raise HTTPException(status_code=500, detail="Email service not configured")
|
| 70 |
return False
|
| 71 |
|
| 72 |
+
try:
|
| 73 |
+
if SMTP_DEBUG:
|
| 74 |
+
logger.info(f"Sending email to {to_email} with subject: {subject}")
|
| 75 |
+
|
| 76 |
+
send_smtp_email = SendSmtpEmail(
|
| 77 |
+
sender={"name": EMAILS_FROM_NAME, "email": EMAILS_FROM_EMAIL},
|
| 78 |
+
to=[{"email": to_email}],
|
| 79 |
+
subject=subject,
|
| 80 |
+
html_content=html_content,
|
| 81 |
+
)
|
|
|
|
|
|
|
| 82 |
|
| 83 |
+
brevo_emails_api.send_transac_email(send_smtp_email)
|
| 84 |
+
logger.info(f"Email sent successfully via Brevo SDK to {to_email}")
|
| 85 |
+
return True
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 86 |
|
| 87 |
+
except ApiException as e:
|
| 88 |
+
logger.error(f"Brevo SDK ApiException: {e}")
|
| 89 |
+
if raise_on_error:
|
| 90 |
+
raise HTTPException(status_code=500, detail=f"{failure_message}")
|
| 91 |
+
return False
|
| 92 |
+
except Exception as e:
|
| 93 |
+
logger.error(f"Brevo SDK sending failed: {str(e)}")
|
| 94 |
+
if raise_on_error:
|
| 95 |
+
raise HTTPException(status_code=500, detail=f"{failure_message}")
|
| 96 |
+
return False
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 97 |
|
| 98 |
|
| 99 |
async def send_email_async(
|
|
|
|
| 116 |
)
|
| 117 |
|
| 118 |
|
|
|
|
| 119 |
@router.get('/test-email-connection')
|
| 120 |
async def test_email_connection():
|
| 121 |
+
"""Test endpoint to check Brevo API configuration"""
|
| 122 |
+
result = {
|
| 123 |
+
"brevo_api_configured": bool(BREVO_API_KEY),
|
| 124 |
+
"email_enabled": ENABLE_EMAIL_SENDING,
|
| 125 |
+
"from_email": EMAILS_FROM_EMAIL,
|
| 126 |
+
"from_name": EMAILS_FROM_NAME,
|
| 127 |
+
"method": "Brevo Python SDK (TransactionalEmailsApi)"
|
| 128 |
}
|
| 129 |
|
| 130 |
+
# Optional: simple API connectivity check using REST (kept minimal)
|
| 131 |
+
if BREVO_API_KEY:
|
| 132 |
+
try:
|
| 133 |
+
import requests
|
| 134 |
+
headers = {
|
| 135 |
+
"api-key": BREVO_API_KEY,
|
| 136 |
+
"accept": "application/json"
|
| 137 |
+
}
|
| 138 |
+
response = requests.get("https://api.brevo.com/v3/account", headers=headers, timeout=10)
|
| 139 |
+
|
| 140 |
+
if response.status_code == 200:
|
| 141 |
+
account_data = response.json()
|
| 142 |
+
result["brevo_api_test"] = "Success"
|
| 143 |
+
result["account_email"] = account_data.get("email", "Unknown")
|
| 144 |
+
result["plan_type"] = account_data.get("plan", [{}])[0].get("type", "Unknown") if account_data.get("plan") else "Unknown"
|
| 145 |
+
else:
|
| 146 |
+
result["brevo_api_test"] = f"Error: {response.status_code}"
|
| 147 |
+
|
| 148 |
+
except Exception as e:
|
| 149 |
+
result["brevo_api_test"] = f"Error: {str(e)}"
|
| 150 |
+
|
| 151 |
+
return result
|
| 152 |
+
|
| 153 |
+
|
| 154 |
+
@router.post('/test-send-email')
|
| 155 |
+
async def test_send_email(email: EmailStr):
|
| 156 |
+
"""Test endpoint to send a test email"""
|
| 157 |
+
try:
|
| 158 |
+
test_content = f'''
|
| 159 |
+
<!DOCTYPE html>
|
| 160 |
+
<html>
|
| 161 |
+
<head>
|
| 162 |
+
<style>
|
| 163 |
+
body {{ font-family: Arial, sans-serif; margin: 0; padding: 0; }}
|
| 164 |
+
.container {{ max-width: 600px; margin: 0 auto; background-color: #ffffff; }}
|
| 165 |
+
.header {{ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); color: white; padding: 40px 20px; text-align: center; }}
|
| 166 |
+
.content {{ padding: 40px 20px; }}
|
| 167 |
+
.success-badge {{ background-color: #4CAF50; color: white; padding: 10px 20px; border-radius: 20px; display: inline-block; margin: 20px 0; }}
|
| 168 |
+
.info-box {{ background-color: #f8f9fa; border-left: 4px solid #667eea; padding: 15px; margin: 20px 0; }}
|
| 169 |
+
.footer {{ background-color: #f8f9fa; padding: 20px; text-align: center; color: #6c757d; font-size: 12px; }}
|
| 170 |
+
</style>
|
| 171 |
+
</head>
|
| 172 |
+
<body>
|
| 173 |
+
<div class="container">
|
| 174 |
+
<div class="header">
|
| 175 |
+
<h1>üöÄ Test Email Success!</h1>
|
| 176 |
+
<p>Your Brevo integration is working perfectly!</p>
|
| 177 |
+
</div>
|
| 178 |
+
<div class="content">
|
| 179 |
+
<div class="success-badge">‚úÖ Email Service Active</div>
|
| 180 |
+
|
| 181 |
+
<h2>Congratulations!</h2>
|
| 182 |
+
<p>This test email confirms that your Brevo API integration is working correctly.</p>
|
| 183 |
+
|
| 184 |
+
<div class="info-box">
|
| 185 |
+
<strong>üìä Test Details:</strong><br>
|
| 186 |
+
• Sent at: {datetime.utcnow().strftime('%Y-%m-%d %H:%M:%S')} UTC<br>
|
| 187 |
+
• Service: Brevo REST API<br>
|
| 188 |
+
• Status: ✅ Successful<br>
|
| 189 |
+
• Method: Direct API Call
|
| 190 |
+
</div>
|
| 191 |
+
|
| 192 |
+
<p>üéâ <strong>Your email features are now ready to use!</strong></p>
|
| 193 |
+
<p>Users can now receive verification codes, password reset links, and other important notifications.</p>
|
| 194 |
+
</div>
|
| 195 |
+
<div class="footer">
|
| 196 |
+
<p>This is an automated test email from your application.</p>
|
| 197 |
+
<p>Powered by Brevo API</p>
|
| 198 |
+
</div>
|
| 199 |
+
</div>
|
| 200 |
+
</body>
|
| 201 |
+
</html>
|
| 202 |
+
'''
|
| 203 |
|
| 204 |
+
success = send_email_sync(
|
| 205 |
+
email,
|
| 206 |
+
"üöÄ Test Email - Brevo Integration Working!",
|
| 207 |
+
test_content,
|
| 208 |
+
"Failed to send test email",
|
| 209 |
+
True
|
| 210 |
+
)
|
| 211 |
+
|
| 212 |
+
return {
|
| 213 |
+
"success": success,
|
| 214 |
+
"message": f"Test email {'‚úÖ sent successfully' if success else '‚ùå failed'} to {email}",
|
| 215 |
+
"method": "Brevo Python SDK"
|
| 216 |
+
}
|
| 217 |
+
|
| 218 |
+
except Exception as e:
|
| 219 |
+
raise HTTPException(status_code=500, detail=str(e))
|
| 220 |
+
|
| 221 |
+
|
| 222 |
+
# All your existing models and endpoints remain exactly the same...
|
| 223 |
class LoginRequest(BaseModel):
|
| 224 |
identifier: str
|
| 225 |
password: str
|
| 226 |
|
|
|
|
| 227 |
class LoginResponse(BaseModel):
|
| 228 |
message: str
|
| 229 |
token: str
|
| 230 |
|
|
|
|
| 231 |
class RegisterRequest(BaseModel):
|
| 232 |
username: str
|
| 233 |
email: EmailStr
|
|
|
|
| 235 |
name: str
|
| 236 |
age: int
|
| 237 |
|
|
|
|
| 238 |
class VerifyEmailRequest(BaseModel):
|
| 239 |
username: str
|
| 240 |
code: str
|
| 241 |
|
|
|
|
| 242 |
class ResendCodeRequest(BaseModel):
|
| 243 |
username: str
|
| 244 |
|
|
|
|
| 245 |
class ForgotPasswordRequest(BaseModel):
|
| 246 |
email: EmailStr
|
| 247 |
|
|
|
|
| 248 |
class ResetPasswordRequest(BaseModel):
|
| 249 |
token: str
|
| 250 |
password: str
|
| 251 |
|
|
|
|
| 252 |
class ChatSessionCheck(BaseModel):
|
| 253 |
session_id: str
|
| 254 |
|
|
|
|
| 303 |
'code_expiration': code_expiration
|
| 304 |
}
|
| 305 |
|
|
|
|
| 306 |
query.create_or_update_temp_user(username, email, temp_user)
|
| 307 |
|
| 308 |
+
# Beautiful verification email
|
| 309 |
email_content = f'''
|
| 310 |
+
<!DOCTYPE html>
|
| 311 |
+
<html>
|
| 312 |
+
<head>
|
| 313 |
+
<style>
|
| 314 |
+
body {{ font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; margin: 0; padding: 0; background-color: #f5f5f5; }}
|
| 315 |
+
.container {{ max-width: 600px; margin: 0 auto; background-color: #ffffff; box-shadow: 0 0 10px rgba(0,0,0,0.1); }}
|
| 316 |
+
.header {{ background: linear-gradient(135deg, #4CAF50 0%, #45a049 100%); color: white; padding: 40px 20px; text-align: center; }}
|
| 317 |
+
.content {{ padding: 40px 30px; }}
|
| 318 |
+
.code-container {{ background: linear-gradient(135deg, #e8f5e8 0%, #f0f8f0 100%); border: 2px solid #4CAF50; padding: 30px; text-align: center; margin: 30px 0; border-radius: 15px; }}
|
| 319 |
+
.verification-code {{ font-size: 42px; font-weight: bold; color: #4CAF50; letter-spacing: 8px; margin: 10px 0; text-shadow: 1px 1px 2px rgba(0,0,0,0.1); }}
|
| 320 |
+
.footer {{ background-color: #f8f9fa; padding: 25px; text-align: center; color: #6c757d; border-top: 1px solid #dee2e6; }}
|
| 321 |
+
.warning {{ background-color: #fff3cd; border: 1px solid #ffeaa7; color: #856404; padding: 15px; border-radius: 5px; margin: 20px 0; }}
|
| 322 |
+
.highlight {{ color: #4CAF50; font-weight: bold; }}
|
| 323 |
+
</style>
|
| 324 |
+
</head>
|
| 325 |
+
<body>
|
| 326 |
+
<div class="container">
|
| 327 |
+
<div class="header">
|
| 328 |
+
<h1>üéâ Welcome to {EMAILS_FROM_NAME}!</h1>
|
| 329 |
+
<p>Almost there! Just one more step...</p>
|
| 330 |
+
</div>
|
| 331 |
+
<div class="content">
|
| 332 |
+
<p>Hi <strong class="highlight">{name}</strong>,</p>
|
| 333 |
+
<p>Thank you for joining {EMAILS_FROM_NAME}! To complete your registration and secure your account, please verify your email address using the code below:</p>
|
| 334 |
+
|
| 335 |
+
<div class="code-container">
|
| 336 |
+
<p style="margin: 0; color: #666; font-size: 16px;">Your Verification Code</p>
|
| 337 |
+
<div class="verification-code">{verification_code}</div>
|
| 338 |
+
<p style="margin: 0; color: #666; font-size: 14px;">Enter this code in the app</p>
|
| 339 |
+
</div>
|
| 340 |
+
|
| 341 |
+
<div class="warning">
|
| 342 |
+
<strong>‚è∞ Important:</strong> This code expires in <strong>10 minutes</strong> for your security.
|
| 343 |
+
</div>
|
| 344 |
+
|
| 345 |
+
<p>If you didn't create an account, please ignore this email and your email address will not be registered.</p>
|
| 346 |
+
|
| 347 |
+
<p>Need help? Feel free to contact our support team.</p>
|
| 348 |
+
|
| 349 |
+
<p>Welcome aboard! üöÄ</p>
|
| 350 |
+
</div>
|
| 351 |
+
<div class="footer">
|
| 352 |
+
<p>This is an automated message, please do not reply to this email.</p>
|
| 353 |
+
<p>© 2024 {EMAILS_FROM_NAME}. All rights reserved.</p>
|
| 354 |
+
</div>
|
| 355 |
+
</div>
|
| 356 |
+
</body>
|
| 357 |
+
</html>
|
| 358 |
'''
|
| 359 |
+
|
| 360 |
background_tasks.add_task(
|
| 361 |
send_email_sync,
|
| 362 |
email,
|
| 363 |
+
f'üîê Verify your {EMAILS_FROM_NAME} account',
|
| 364 |
email_content,
|
| 365 |
"Failed to send verification email",
|
| 366 |
+
False
|
| 367 |
)
|
| 368 |
|
| 369 |
return {
|
| 370 |
+
"message": "üéâ Registration successful! Check your email for verification code.",
|
| 371 |
+
"note": "üìß Code expires in 10 minutes. Check spam folder if needed."
|
| 372 |
}
|
| 373 |
+
|
| 374 |
except Exception as e:
|
| 375 |
if isinstance(e, HTTPException):
|
| 376 |
raise e
|
|
|
|
| 406 |
# Set default settings
|
| 407 |
query.set_user_language(username, "English")
|
| 408 |
query.set_user_theme(username, False)
|
| 409 |
+
|
| 410 |
default_preferences = {
|
| 411 |
'keywords': True,
|
| 412 |
'references': True,
|
|
|
|
| 416 |
}
|
| 417 |
query.set_user_preferences(username, default_preferences)
|
| 418 |
|
| 419 |
+
return {"message": "Email verification successful! You can now log in."}
|
| 420 |
except Exception as e:
|
| 421 |
if isinstance(e, HTTPException):
|
| 422 |
raise e
|
|
|
|
| 441 |
query.create_or_update_temp_user(username, temp_user['email'], temp_user)
|
| 442 |
|
| 443 |
email_content = f'''
|
| 444 |
+
<!DOCTYPE html>
|
| 445 |
+
<html>
|
| 446 |
+
<head>
|
| 447 |
+
<style>
|
| 448 |
+
.container {{ font-family: Arial, sans-serif; max-width: 600px; margin: 0 auto; }}
|
| 449 |
+
.header {{ background-color: #FF9800; color: white; padding: 30px; text-align: center; }}
|
| 450 |
+
.content {{ padding: 40px; }}
|
| 451 |
+
.code-box {{ background-color: #fff3e0; border: 2px solid #FF9800; padding: 20px; text-align: center; margin: 30px 0; border-radius: 10px; }}
|
| 452 |
+
.code {{ font-size: 36px; font-weight: bold; color: #FF9800; letter-spacing: 5px; }}
|
| 453 |
+
.footer {{ background-color: #f5f5f5; padding: 20px; text-align: center; color: #666; font-size: 12px; }}
|
| 454 |
+
</style>
|
| 455 |
+
</head>
|
| 456 |
+
<body>
|
| 457 |
+
<div class="container">
|
| 458 |
+
<div class="header">
|
| 459 |
+
<h1>üîÑ New Verification Code</h1>
|
| 460 |
+
</div>
|
| 461 |
+
<div class="content">
|
| 462 |
+
<p>Hi <strong>{temp_user['name']}</strong>,</p>
|
| 463 |
+
<p>You requested a new verification code. Here's your fresh code:</p>
|
| 464 |
+
|
| 465 |
+
<div class="code-box">
|
| 466 |
+
<div class="code">{verification_code}</div>
|
| 467 |
+
<p style="margin: 10px 0 0 0; color: #666;">New Verification Code</p>
|
| 468 |
+
</div>
|
| 469 |
+
|
| 470 |
+
<p><strong>‚è∞ This code will expire in 10 minutes.</strong></p>
|
| 471 |
+
<p>Enter this code in your application to complete verification.</p>
|
| 472 |
+
</div>
|
| 473 |
+
<div class="footer">
|
| 474 |
+
<p>This is an automated email, please do not reply.</p>
|
| 475 |
+
<p>© 2024 {EMAILS_FROM_NAME}. All rights reserved.</p>
|
| 476 |
+
</div>
|
| 477 |
+
</div>
|
| 478 |
+
</body>
|
| 479 |
+
</html>
|
| 480 |
'''
|
| 481 |
+
|
| 482 |
background_tasks.add_task(
|
| 483 |
send_email_sync,
|
| 484 |
temp_user['email'],
|
| 485 |
+
f'üîÑ New verification code for {EMAILS_FROM_NAME}',
|
| 486 |
email_content,
|
| 487 |
"Failed to send verification email",
|
| 488 |
False
|
|
|
|
| 523 |
expiration = datetime.utcnow() + timedelta(hours=1)
|
| 524 |
|
| 525 |
query.store_reset_token(email, reset_token, expiration)
|
| 526 |
+
|
| 527 |
base_url = os.getenv("FRONTEND_URL", "http://localhost:3000")
|
| 528 |
reset_link = f"{base_url}/reset-password?token={reset_token}"
|
| 529 |
|
| 530 |
email_content = f'''
|
| 531 |
+
<!DOCTYPE html>
|
| 532 |
+
<html>
|
| 533 |
+
<head>
|
| 534 |
+
<style>
|
| 535 |
+
.container {{ font-family: Arial, sans-serif; max-width: 600px; margin: 0 auto; }}
|
| 536 |
+
.header {{ background-color: #f44336; color: white; padding: 30px; text-align: center; }}
|
| 537 |
+
.content {{ padding: 40px; }}
|
| 538 |
+
.button {{ background-color: #f44336; color: white; padding: 15px 30px; text-decoration: none; border-radius: 5px; display: inline-block; margin: 20px 0; }}
|
| 539 |
+
.link-box {{ background-color: #f5f5f5; padding: 15px; word-break: break-all; border-radius: 5px; margin: 20px 0; }}
|
| 540 |
+
.footer {{ background-color: #f5f5f5; padding: 20px; text-align: center; color: #666; font-size: 12px; }}
|
| 541 |
+
</style>
|
| 542 |
+
</head>
|
| 543 |
+
<body>
|
| 544 |
+
<div class="container">
|
| 545 |
+
<div class="header">
|
| 546 |
+
<h1>üîí Password Reset Request</h1>
|
| 547 |
+
</div>
|
| 548 |
+
<div class="content">
|
| 549 |
+
<p>Hi,</p>
|
| 550 |
+
<p>You requested to reset your password for your {EMAILS_FROM_NAME} account.</p>
|
| 551 |
+
<p>Click the button below to reset your password:</p>
|
| 552 |
+
|
| 553 |
+
<div style="text-align: center;">
|
| 554 |
+
<a href="{reset_link}" class="button">Reset My Password</a>
|
| 555 |
+
</div>
|
| 556 |
+
|
| 557 |
+
<p>Or copy and paste this link in your browser:</p>
|
| 558 |
+
<div class="link-box">{reset_link}</div>
|
| 559 |
+
|
| 560 |
+
<p><strong>‚è∞ This link will expire in 1 hour.</strong></p>
|
| 561 |
+
<p><strong>üîí Security Note:</strong> If you didn't request this password reset, please ignore this email and your password will remain unchanged.</p>
|
| 562 |
+
</div>
|
| 563 |
+
<div class="footer">
|
| 564 |
+
<p>This is an automated email, please do not reply.</p>
|
| 565 |
+
<p>© 2024 {EMAILS_FROM_NAME}. All rights reserved.</p>
|
| 566 |
+
</div>
|
| 567 |
+
</div>
|
| 568 |
+
</body>
|
| 569 |
+
</html>
|
| 570 |
'''
|
| 571 |
+
|
| 572 |
background_tasks.add_task(
|
| 573 |
send_email_sync,
|
| 574 |
email,
|
| 575 |
+
f'üîí Reset your {EMAILS_FROM_NAME} password',
|
| 576 |
email_content,
|
| 577 |
"Failed to send password reset email",
|
| 578 |
False
|
| 579 |
)
|
| 580 |
|
| 581 |
+
return {"message": "Password reset instructions sent to your email. Check your inbox!"}
|
| 582 |
except Exception as e:
|
| 583 |
if isinstance(e, HTTPException):
|
| 584 |
raise e
|
|
|
|
| 601 |
hashed_password = generate_password_hash(new_password)
|
| 602 |
query.update_password(reset_info['email'], hashed_password)
|
| 603 |
|
| 604 |
+
return {"message": "Password successfully reset! You can now log in with your new password."}
|
| 605 |
except Exception as e:
|
| 606 |
if isinstance(e, HTTPException):
|
| 607 |
raise e
|
pyproject.toml
CHANGED
|
@@ -186,6 +186,7 @@ dependencies = [
|
|
| 186 |
"zipp==3.23.0",
|
| 187 |
"zstandard==0.23.0",
|
| 188 |
"MagicConvert==0.1.3",
|
|
|
|
| 189 |
]
|
| 190 |
|
| 191 |
[build-system]
|
|
|
|
| 186 |
"zipp==3.23.0",
|
| 187 |
"zstandard==0.23.0",
|
| 188 |
"MagicConvert==0.1.3",
|
| 189 |
+
"brevo_python",
|
| 190 |
]
|
| 191 |
|
| 192 |
[build-system]
|