Refactor code for improved readability and consistency
- Cleaned up whitespace and formatting in various files including http.py, language.py, logger.py, safe_exec.py, and SQL migration scripts. - Consolidated import statements and removed unnecessary blank lines. - Updated logging configuration for better clarity. - Enhanced the safe execution code with improved error handling and logging. - Removed commented-out code and unnecessary variables in backfill_zero_trades.py and other scripts. - Added a pyproject.toml for Ruff and Vulture configuration. - Introduced requirements-dev.txt for development dependencies. - Removed commented-out stock entries in init.sql for cleaner migration scripts.
This commit is contained in:
@@ -1,14 +1,16 @@
|
||||
"""
|
||||
Email Service - Handles email verification codes and notifications.
|
||||
"""
|
||||
|
||||
import os
|
||||
import random
|
||||
import string
|
||||
import smtplib
|
||||
from email.mime.text import MIMEText
|
||||
from email.mime.multipart import MIMEMultipart
|
||||
import string
|
||||
from datetime import datetime, timedelta
|
||||
from typing import Tuple, Optional
|
||||
from email.mime.multipart import MIMEMultipart
|
||||
from email.mime.text import MIMEText
|
||||
from typing import Tuple
|
||||
|
||||
from app.utils.db import get_db_connection
|
||||
from app.utils.logger import get_logger
|
||||
|
||||
@@ -28,110 +30,109 @@ def get_email_service():
|
||||
|
||||
class EmailService:
|
||||
"""Email service for verification codes and notifications"""
|
||||
|
||||
|
||||
def __init__(self):
|
||||
self._load_config()
|
||||
|
||||
|
||||
def _load_config(self):
|
||||
"""Load email configuration from environment variables"""
|
||||
self.smtp_host = os.getenv('SMTP_HOST', '')
|
||||
self.smtp_port = int(os.getenv('SMTP_PORT', '587'))
|
||||
self.smtp_user = os.getenv('SMTP_USER', '')
|
||||
self.smtp_password = os.getenv('SMTP_PASSWORD', '')
|
||||
self.smtp_from = os.getenv('SMTP_FROM', '') or self.smtp_user
|
||||
self.smtp_use_tls = os.getenv('SMTP_USE_TLS', 'true').lower() == 'true'
|
||||
self.smtp_use_ssl = os.getenv('SMTP_USE_SSL', 'false').lower() == 'true'
|
||||
|
||||
self.smtp_host = os.getenv("SMTP_HOST", "")
|
||||
self.smtp_port = int(os.getenv("SMTP_PORT", "587"))
|
||||
self.smtp_user = os.getenv("SMTP_USER", "")
|
||||
self.smtp_password = os.getenv("SMTP_PASSWORD", "")
|
||||
self.smtp_from = os.getenv("SMTP_FROM", "") or self.smtp_user
|
||||
self.smtp_use_tls = os.getenv("SMTP_USE_TLS", "true").lower() == "true"
|
||||
self.smtp_use_ssl = os.getenv("SMTP_USE_SSL", "false").lower() == "true"
|
||||
|
||||
# Verification code settings
|
||||
self.code_expire_minutes = int(os.getenv('VERIFICATION_CODE_EXPIRE_MINUTES', '10'))
|
||||
self.code_expire_minutes = int(os.getenv("VERIFICATION_CODE_EXPIRE_MINUTES", "10"))
|
||||
self.code_length = 6
|
||||
|
||||
|
||||
# Verification code attempt limits (anti-brute-force)
|
||||
self.code_max_attempts = int(os.getenv('VERIFICATION_CODE_MAX_ATTEMPTS', '5'))
|
||||
self.code_lock_minutes = int(os.getenv('VERIFICATION_CODE_LOCK_MINUTES', '30'))
|
||||
|
||||
self.code_max_attempts = int(os.getenv("VERIFICATION_CODE_MAX_ATTEMPTS", "5"))
|
||||
self.code_lock_minutes = int(os.getenv("VERIFICATION_CODE_LOCK_MINUTES", "30"))
|
||||
|
||||
# Check if email is properly configured
|
||||
self.email_enabled = bool(self.smtp_host and self.smtp_user and self.smtp_password)
|
||||
|
||||
|
||||
if not self.email_enabled:
|
||||
logger.warning("Email service is not configured. SMTP settings are missing.")
|
||||
|
||||
|
||||
def is_configured(self) -> bool:
|
||||
"""Check if email service is properly configured"""
|
||||
return self.email_enabled
|
||||
|
||||
|
||||
# =========================================================================
|
||||
# Verification Code Generation & Storage
|
||||
# =========================================================================
|
||||
|
||||
|
||||
def generate_code(self) -> str:
|
||||
"""Generate a random numeric verification code"""
|
||||
return ''.join(random.choices(string.digits, k=self.code_length))
|
||||
|
||||
def create_verification_code(self, email: str, code_type: str,
|
||||
ip_address: str = None) -> Tuple[bool, str]:
|
||||
return "".join(random.choices(string.digits, k=self.code_length))
|
||||
|
||||
def create_verification_code(self, email: str, code_type: str, ip_address: str = None) -> Tuple[bool, str]:
|
||||
"""
|
||||
Create and store a new verification code.
|
||||
|
||||
|
||||
Args:
|
||||
email: Email address
|
||||
code_type: Type of verification (register, reset_password, change_password, change_email)
|
||||
ip_address: Requester's IP address
|
||||
|
||||
|
||||
Returns:
|
||||
(success, code_or_message)
|
||||
"""
|
||||
try:
|
||||
code = self.generate_code()
|
||||
expires_at = datetime.now() + timedelta(minutes=self.code_expire_minutes)
|
||||
|
||||
|
||||
with get_db_connection() as db:
|
||||
cur = db.cursor()
|
||||
|
||||
|
||||
# Invalidate any existing unused codes of the same type for this email
|
||||
cur.execute(
|
||||
"""
|
||||
UPDATE qd_verification_codes
|
||||
SET used_at = NOW()
|
||||
UPDATE qd_verification_codes
|
||||
SET used_at = NOW()
|
||||
WHERE email = ? AND type = ? AND used_at IS NULL
|
||||
""",
|
||||
(email, code_type)
|
||||
(email, code_type),
|
||||
)
|
||||
|
||||
|
||||
# Insert new code
|
||||
cur.execute(
|
||||
"""
|
||||
INSERT INTO qd_verification_codes
|
||||
INSERT INTO qd_verification_codes
|
||||
(email, code, type, expires_at, ip_address)
|
||||
VALUES (?, ?, ?, ?, ?)
|
||||
""",
|
||||
(email, code, code_type, expires_at, ip_address)
|
||||
(email, code, code_type, expires_at, ip_address),
|
||||
)
|
||||
db.commit()
|
||||
cur.close()
|
||||
|
||||
|
||||
return True, code
|
||||
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to create verification code: {e}")
|
||||
return False, 'Failed to generate verification code'
|
||||
|
||||
return False, "Failed to generate verification code"
|
||||
|
||||
def verify_code(self, email: str, code: str, code_type: str) -> Tuple[bool, str]:
|
||||
"""
|
||||
Verify a submitted code with brute-force protection.
|
||||
|
||||
|
||||
Args:
|
||||
email: Email address
|
||||
code: The code to verify
|
||||
code_type: Type of verification
|
||||
|
||||
|
||||
Returns:
|
||||
(valid, message)
|
||||
"""
|
||||
try:
|
||||
with get_db_connection() as db:
|
||||
cur = db.cursor()
|
||||
|
||||
|
||||
# Check if locked due to too many failed attempts
|
||||
lock_window = datetime.now() - timedelta(minutes=self.code_lock_minutes)
|
||||
cur.execute(
|
||||
@@ -141,13 +142,13 @@ class EmailService:
|
||||
AND attempts >= ? AND last_attempt_at > ?
|
||||
AND used_at IS NULL
|
||||
""",
|
||||
(email, code_type, self.code_max_attempts, lock_window.isoformat())
|
||||
(email, code_type, self.code_max_attempts, lock_window.isoformat()),
|
||||
)
|
||||
lock_row = cur.fetchone()
|
||||
if lock_row and lock_row['cnt'] > 0:
|
||||
if lock_row and lock_row["cnt"] > 0:
|
||||
cur.close()
|
||||
return False, f'Too many failed attempts. Please try again in {self.code_lock_minutes} minutes'
|
||||
|
||||
return False, f"Too many failed attempts. Please try again in {self.code_lock_minutes} minutes"
|
||||
|
||||
# Find latest unused code for this email/type
|
||||
cur.execute(
|
||||
"""
|
||||
@@ -156,99 +157,97 @@ class EmailService:
|
||||
ORDER BY created_at DESC
|
||||
LIMIT 1
|
||||
""",
|
||||
(email, code_type)
|
||||
(email, code_type),
|
||||
)
|
||||
row = cur.fetchone()
|
||||
|
||||
|
||||
if not row:
|
||||
cur.close()
|
||||
return False, 'Invalid verification code'
|
||||
|
||||
code_id = row['id']
|
||||
stored_code = row['stored_code']
|
||||
attempts = row['attempts'] or 0
|
||||
|
||||
return False, "Invalid verification code"
|
||||
|
||||
code_id = row["id"]
|
||||
stored_code = row["stored_code"]
|
||||
attempts = row["attempts"] or 0
|
||||
|
||||
# Check if code matches
|
||||
if stored_code != code:
|
||||
# Increment attempt counter
|
||||
new_attempts = attempts + 1
|
||||
cur.execute(
|
||||
"""
|
||||
UPDATE qd_verification_codes
|
||||
UPDATE qd_verification_codes
|
||||
SET attempts = ?, last_attempt_at = NOW()
|
||||
WHERE id = ?
|
||||
""",
|
||||
(new_attempts, code_id)
|
||||
(new_attempts, code_id),
|
||||
)
|
||||
db.commit()
|
||||
cur.close()
|
||||
|
||||
|
||||
remaining = self.code_max_attempts - new_attempts
|
||||
if remaining <= 0:
|
||||
return False, f'Too many failed attempts. Please try again in {self.code_lock_minutes} minutes'
|
||||
return False, f'Invalid verification code. {remaining} attempts remaining'
|
||||
|
||||
return False, f"Too many failed attempts. Please try again in {self.code_lock_minutes} minutes"
|
||||
return False, f"Invalid verification code. {remaining} attempts remaining"
|
||||
|
||||
# Check expiration
|
||||
expires_at = row['expires_at']
|
||||
expires_at = row["expires_at"]
|
||||
if isinstance(expires_at, str):
|
||||
expires_at = datetime.fromisoformat(expires_at)
|
||||
|
||||
|
||||
if datetime.now() > expires_at:
|
||||
cur.close()
|
||||
return False, 'Verification code has expired'
|
||||
|
||||
return False, "Verification code has expired"
|
||||
|
||||
# Mark as used
|
||||
cur.execute(
|
||||
"UPDATE qd_verification_codes SET used_at = NOW() WHERE id = ?",
|
||||
(code_id,)
|
||||
)
|
||||
cur.execute("UPDATE qd_verification_codes SET used_at = NOW() WHERE id = ?", (code_id,))
|
||||
db.commit()
|
||||
cur.close()
|
||||
|
||||
return True, 'verified'
|
||||
|
||||
|
||||
return True, "verified"
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to verify code: {e}")
|
||||
return False, 'Verification failed'
|
||||
|
||||
return False, "Verification failed"
|
||||
|
||||
# =========================================================================
|
||||
# Email Sending
|
||||
# =========================================================================
|
||||
|
||||
|
||||
def send_email(self, to_email: str, subject: str, html_body: str) -> Tuple[bool, str]:
|
||||
"""
|
||||
Send an email.
|
||||
|
||||
|
||||
Args:
|
||||
to_email: Recipient email address
|
||||
subject: Email subject
|
||||
html_body: HTML body content
|
||||
|
||||
|
||||
Returns:
|
||||
(success, message)
|
||||
"""
|
||||
if not self.email_enabled:
|
||||
logger.warning(f"Email not sent (service disabled): {subject} to {to_email}")
|
||||
return False, 'Email service is not configured'
|
||||
|
||||
return False, "Email service is not configured"
|
||||
|
||||
try:
|
||||
msg = MIMEMultipart('alternative')
|
||||
msg['Subject'] = subject
|
||||
msg['From'] = self.smtp_from
|
||||
msg['To'] = to_email
|
||||
|
||||
msg = MIMEMultipart("alternative")
|
||||
msg["Subject"] = subject
|
||||
msg["From"] = self.smtp_from
|
||||
msg["To"] = to_email
|
||||
|
||||
# Plain text version (fallback)
|
||||
text_body = html_body.replace('<br>', '\n').replace('<br/>', '\n')
|
||||
text_body = html_body.replace("<br>", "\n").replace("<br/>", "\n")
|
||||
# Simple HTML tag removal for plain text
|
||||
import re
|
||||
text_body = re.sub('<[^<]+?>', '', text_body)
|
||||
|
||||
part1 = MIMEText(text_body, 'plain', 'utf-8')
|
||||
part2 = MIMEText(html_body, 'html', 'utf-8')
|
||||
|
||||
|
||||
text_body = re.sub("<[^<]+?>", "", text_body)
|
||||
|
||||
part1 = MIMEText(text_body, "plain", "utf-8")
|
||||
part2 = MIMEText(html_body, "html", "utf-8")
|
||||
|
||||
msg.attach(part1)
|
||||
msg.attach(part2)
|
||||
|
||||
|
||||
# Connect and send
|
||||
if self.smtp_use_ssl:
|
||||
server = smtplib.SMTP_SSL(self.smtp_host, self.smtp_port)
|
||||
@@ -256,34 +255,33 @@ class EmailService:
|
||||
server = smtplib.SMTP(self.smtp_host, self.smtp_port)
|
||||
if self.smtp_use_tls:
|
||||
server.starttls()
|
||||
|
||||
|
||||
server.login(self.smtp_user, self.smtp_password)
|
||||
server.sendmail(self.smtp_from, to_email, msg.as_string())
|
||||
server.quit()
|
||||
|
||||
|
||||
logger.info(f"Email sent successfully: {subject} to {to_email}")
|
||||
return True, 'sent'
|
||||
|
||||
return True, "sent"
|
||||
|
||||
except smtplib.SMTPAuthenticationError as e:
|
||||
logger.error(f"SMTP authentication failed: {e}")
|
||||
return False, 'Email authentication failed'
|
||||
return False, "Email authentication failed"
|
||||
except smtplib.SMTPException as e:
|
||||
logger.error(f"SMTP error: {e}")
|
||||
return False, 'Failed to send email'
|
||||
return False, "Failed to send email"
|
||||
except Exception as e:
|
||||
logger.error(f"Email error: {e}")
|
||||
return False, 'Failed to send email'
|
||||
|
||||
def send_verification_code(self, email: str, code_type: str,
|
||||
ip_address: str = None) -> Tuple[bool, str]:
|
||||
return False, "Failed to send email"
|
||||
|
||||
def send_verification_code(self, email: str, code_type: str, ip_address: str = None) -> Tuple[bool, str]:
|
||||
"""
|
||||
Generate and send a verification code email.
|
||||
|
||||
|
||||
Args:
|
||||
email: Recipient email address
|
||||
code_type: Type of verification (register, reset_password, change_password)
|
||||
ip_address: Requester's IP address
|
||||
|
||||
|
||||
Returns:
|
||||
(success, message)
|
||||
"""
|
||||
@@ -291,36 +289,36 @@ class EmailService:
|
||||
success, code_or_msg = self.create_verification_code(email, code_type, ip_address)
|
||||
if not success:
|
||||
return False, code_or_msg
|
||||
|
||||
|
||||
code = code_or_msg
|
||||
|
||||
|
||||
# Prepare email content based on type
|
||||
if code_type == 'register':
|
||||
subject = 'QuantDinger - Verification Code for Registration'
|
||||
action_text = 'complete your registration'
|
||||
elif code_type == 'login':
|
||||
subject = 'QuantDinger - Quick Login Verification Code'
|
||||
action_text = 'log in to your account'
|
||||
elif code_type == 'reset_password':
|
||||
subject = 'QuantDinger - Password Reset Verification Code'
|
||||
action_text = 'reset your password'
|
||||
elif code_type == 'change_password':
|
||||
subject = 'QuantDinger - Verification Code for Password Change'
|
||||
action_text = 'change your password'
|
||||
elif code_type == 'change_email':
|
||||
subject = 'QuantDinger - Verification Code for Email Change'
|
||||
action_text = 'change your email address'
|
||||
if code_type == "register":
|
||||
subject = "DinQuant - Verification Code for Registration"
|
||||
action_text = "complete your registration"
|
||||
elif code_type == "login":
|
||||
subject = "DinQuant - Quick Login Verification Code"
|
||||
action_text = "log in to your account"
|
||||
elif code_type == "reset_password":
|
||||
subject = "DinQuant - Password Reset Verification Code"
|
||||
action_text = "reset your password"
|
||||
elif code_type == "change_password":
|
||||
subject = "DinQuant - Verification Code for Password Change"
|
||||
action_text = "change your password"
|
||||
elif code_type == "change_email":
|
||||
subject = "DinQuant - Verification Code for Email Change"
|
||||
action_text = "change your email address"
|
||||
else:
|
||||
subject = 'QuantDinger - Verification Code'
|
||||
action_text = 'complete the verification'
|
||||
|
||||
subject = "DinQuant - Verification Code"
|
||||
action_text = "complete the verification"
|
||||
|
||||
html_body = f"""
|
||||
<div style="font-family: Arial, sans-serif; max-width: 600px; margin: 0 auto; padding: 20px;">
|
||||
<div style="text-align: center; margin-bottom: 30px;">
|
||||
<h1 style="color: #1890ff; margin: 0;">QuantDinger</h1>
|
||||
<h1 style="color: #1890ff; margin: 0;">DinQuant</h1>
|
||||
<p style="color: #666; margin-top: 5px;">AI-Driven Quantitative Insights</p>
|
||||
</div>
|
||||
|
||||
|
||||
<div style="background: #f5f5f5; border-radius: 8px; padding: 30px; text-align: center;">
|
||||
<p style="color: #333; font-size: 16px; margin: 0 0 20px 0;">
|
||||
Your verification code to {action_text} is:
|
||||
@@ -332,30 +330,31 @@ class EmailService:
|
||||
This code will expire in {self.code_expire_minutes} minutes.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
|
||||
<div style="margin-top: 30px; padding: 20px; background: #fff8e6; border-radius: 8px;">
|
||||
<p style="color: #d48806; font-size: 14px; margin: 0;">
|
||||
<strong>Security Notice:</strong> If you did not request this code,
|
||||
<strong>Security Notice:</strong> If you did not request this code,
|
||||
please ignore this email. Do not share this code with anyone.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
|
||||
<div style="text-align: center; margin-top: 30px; color: #999; font-size: 12px;">
|
||||
<p>© QuantDinger. All rights reserved.</p>
|
||||
<p>© DinQuant. All rights reserved.</p>
|
||||
</div>
|
||||
</div>
|
||||
"""
|
||||
|
||||
|
||||
# Send email
|
||||
return self.send_email(email, subject, html_body)
|
||||
|
||||
|
||||
# =========================================================================
|
||||
# Email Validation
|
||||
# =========================================================================
|
||||
|
||||
|
||||
@staticmethod
|
||||
def is_valid_email(email: str) -> bool:
|
||||
"""Basic email format validation"""
|
||||
import re
|
||||
pattern = r'^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$'
|
||||
|
||||
pattern = r"^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$"
|
||||
return bool(re.match(pattern, email))
|
||||
|
||||
Reference in New Issue
Block a user