muhammadnoman76 commited on
Commit
0b5ef0b
·
1 Parent(s): 1dc181f
Files changed (2) hide show
  1. app/routers/auth.py +288 -296
  2. 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 email.mime.multipart import MIMEMultipart
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
- # Email Configuration
33
- SMTP_SERVER = os.getenv("SMTP_SERVER")
34
- _raw_host = os.getenv("SMTP_HOST")
35
- _raw_port = os.getenv("SMTP_PORT")
36
-
37
- # Be forgiving if env is swapped
38
- if _raw_host and _raw_host.strip().isdigit() and not _raw_port:
39
- SMTP_HOST = SMTP_SERVER or "smtp.gmail.com"
40
- SMTP_PORT = int(_raw_host.strip())
41
- else:
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
- # Alternative email service (if primary fails)
61
- USE_SENDGRID = os.getenv("USE_SENDGRID", "false").lower() == "true"
62
- SENDGRID_API_KEY = os.getenv("SENDGRID_API_KEY")
 
 
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
- """Enhanced email sending with multiple fallback options"""
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
- # First, test the connection
164
- if SMTP_DEBUG:
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 is not configured properly")
188
  return False
189
 
190
- message = MIMEMultipart("alternative")
191
- from_email = EMAILS_FROM_EMAIL or SMTP_USER
192
- from_header = formataddr((EMAILS_FROM_NAME, from_email)) if EMAILS_FROM_NAME else from_email
193
-
194
- message["Subject"] = subject
195
- message["From"] = from_header
196
- message["To"] = to_email
197
-
198
- if EMAILS_FROM_EMAIL and EMAILS_FROM_EMAIL != SMTP_USER:
199
- message["Reply-To"] = EMAILS_FROM_EMAIL
200
-
201
- message.attach(MIMEText(html_content, "html"))
202
 
203
- # Try different connection methods
204
- methods_to_try = []
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
- last_error = None
213
-
214
- for method, port in methods_to_try:
215
- try:
216
- logger.info(f"Trying {method} connection to {SMTP_HOST}:{port}")
217
-
218
- if method == "SSL":
219
- # SSL connection (port 465)
220
- context = ssl.create_default_context()
221
- with smtplib.SMTP_SSL(SMTP_HOST, port, timeout=SMTP_TIMEOUT, context=context) as server:
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 email configuration"""
322
- result = test_smtp_connection()
323
- return {
324
- "smtp_configured": all([SMTP_HOST, SMTP_PORT, SMTP_USER, SMTP_PASSWORD]),
325
- "connection_test": result,
326
- "sendgrid_configured": bool(SENDGRID_API_KEY),
327
- "email_enabled": ENABLE_EMAIL_SENDING
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
- # Send email in background - won't block registration
425
  email_content = f'''
426
- <p>Hi {name},</p>
427
- <p>Thank you for registering. Please use the following code to verify your email address:</p>
428
- <h2 style="color: #4CAF50;">{verification_code}</h2>
429
- <p>This code will expire in 10 minutes.</p>
430
- <p>If you didn't register for this account, please ignore this email.</p>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
431
  '''
432
-
433
  background_tasks.add_task(
434
  send_email_sync,
435
  email,
436
- 'Verify your email address',
437
  email_content,
438
  "Failed to send verification email",
439
- False # Don't raise error - registration succeeds even if email fails
440
  )
441
 
442
  return {
443
- "message": "Registration successful. A verification code has been sent to your email.",
444
- "note": "If you don't receive the email, please use the resend option."
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
- <p>Hi {temp_user['name']},</p>
518
- <p>You requested a new verification code. Please use the following code to verify your email address:</p>
519
- <h2 style="color: #4CAF50;">{verification_code}</h2>
520
- <p>This code will expire in 10 minutes.</p>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
521
  '''
522
-
523
  background_tasks.add_task(
524
  send_email_sync,
525
  temp_user['email'],
526
- 'Your new verification code',
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
- <p>Hi,</p>
573
- <p>You requested to reset your password. Click the link below to reset it:</p>
574
- <p><a href="{reset_link}" style="background-color: #4CAF50; color: white; padding: 10px 20px; text-decoration: none; border-radius: 5px;">Reset Password</a></p>
575
- <p>Or copy this link: {reset_link}</p>
576
- <p>This link will expire in 1 hour.</p>
577
- <p>If you didn't request this, please ignore this email.</p>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
578
  '''
579
-
580
  background_tasks.add_task(
581
  send_email_sync,
582
  email,
583
- 'Reset Your Password',
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]