feat: Migrate to ultrathin monolith architecture
Consolidate 3 separate applications (reports-app, data-entry-app, telegram-bot) into a unified
architecture with single backend and frontend:
Backend Changes:
- Unified FastAPI backend at backend/ with modular structure
- Modules: reports, data_entry, telegram in backend/modules/
- Centralized config.py and main.py with all routers registered
- Single worker mode (--workers 1) for Telegram bot compatibility
- Shared Oracle connection pool and JWT authentication
- Unified requirements.txt and environment configuration
Frontend Changes:
- Single Vue.js SPA with module-based routing
- Unified frontend at src/ with modules in src/modules/{reports,data-entry}/
- Shared components and stores in src/shared/
- Error boundaries for module isolation
- Dual API proxy in Vite for module communication
Infrastructure:
- New unified startup scripts: start-prod.sh, start-test.sh, start-backend.sh
- Environment templates: .env.dev.example, .env.test.example, .env.prod.example
- Updated deployment scripts for Windows IIS
- Simplified SSH tunnel management
Documentation:
- Comprehensive CLAUDE.md with architecture overview
- Module-specific docs in docs/{data-entry,telegram}/
- Architecture decision records in docs/ARCHITECTURE-DECISIONS.md
- Deployment guides consolidated in deployment/windows/docs/
This migration reduces complexity, improves maintainability, and enables easier
deployment while maintaining all existing functionality.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
263
backend/modules/telegram/utils/email_service.py
Normal file
263
backend/modules/telegram/utils/email_service.py
Normal file
@@ -0,0 +1,263 @@
|
||||
"""
|
||||
Async SMTP Email Service with retry logic and proper error handling
|
||||
"""
|
||||
import aiosmtplib
|
||||
from email.mime.text import MIMEText
|
||||
from email.mime.multipart import MIMEMultipart
|
||||
import os
|
||||
import logging
|
||||
from typing import Optional
|
||||
import asyncio
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class EmailService:
|
||||
"""Async SMTP client for sending authentication codes"""
|
||||
|
||||
def __init__(self):
|
||||
self.smtp_host = os.getenv("SMTP_HOST", "mail.romfast.ro")
|
||||
self.smtp_port = int(os.getenv("SMTP_PORT", "587"))
|
||||
self.smtp_user = os.getenv("SMTP_USER")
|
||||
self.smtp_password = os.getenv("SMTP_PASSWORD")
|
||||
self.from_email = os.getenv("SMTP_FROM_EMAIL")
|
||||
self.from_name = os.getenv("SMTP_FROM_NAME", "ROA2WEB")
|
||||
self.use_tls = os.getenv("SMTP_USE_TLS", "true").lower() == "true"
|
||||
|
||||
# Retry configuration
|
||||
self.max_retries = int(os.getenv("EMAIL_MAX_RETRIES", "3"))
|
||||
self.retry_delay = float(os.getenv("EMAIL_RETRY_DELAY", "2.0")) # seconds
|
||||
|
||||
# Validate required config
|
||||
if not all([self.smtp_user, self.smtp_password, self.from_email]):
|
||||
raise ValueError("SMTP configuration incomplete. Check .env file.")
|
||||
|
||||
async def send_auth_code(
|
||||
self,
|
||||
to_email: str,
|
||||
code: str,
|
||||
username: str
|
||||
) -> bool:
|
||||
"""
|
||||
Send authentication code via email with retry logic
|
||||
|
||||
Args:
|
||||
to_email: Recipient email address
|
||||
code: 6-digit authentication code
|
||||
username: Oracle username for personalization
|
||||
|
||||
Returns:
|
||||
True if email sent successfully (after retries if needed)
|
||||
|
||||
Raises:
|
||||
No exceptions - returns False on all failures
|
||||
"""
|
||||
subject = "Codul tău de autentificare ROA2WEB"
|
||||
html_body = self._create_email_template(code, username)
|
||||
|
||||
for attempt in range(1, self.max_retries + 1):
|
||||
try:
|
||||
await self._send_email(to_email, subject, html_body)
|
||||
logger.info(
|
||||
f"Email sent successfully to {to_email} "
|
||||
f"(attempt {attempt}/{self.max_retries})"
|
||||
)
|
||||
return True
|
||||
|
||||
except aiosmtplib.SMTPException as e:
|
||||
logger.warning(
|
||||
f"SMTP error on attempt {attempt}/{self.max_retries}: {e}"
|
||||
)
|
||||
if attempt < self.max_retries:
|
||||
# Exponential backoff: 2s, 4s, 8s
|
||||
delay = self.retry_delay * (2 ** (attempt - 1))
|
||||
logger.info(f"Retrying in {delay}s...")
|
||||
await asyncio.sleep(delay)
|
||||
else:
|
||||
logger.error(f"Failed to send email to {to_email} after {self.max_retries} attempts")
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Unexpected error sending email: {e}", exc_info=True)
|
||||
return False
|
||||
|
||||
return False
|
||||
|
||||
async def _send_email(
|
||||
self,
|
||||
to_email: str,
|
||||
subject: str,
|
||||
html_body: str
|
||||
) -> None:
|
||||
"""
|
||||
Internal async SMTP sender
|
||||
|
||||
Raises:
|
||||
aiosmtplib.SMTPException: On SMTP errors
|
||||
"""
|
||||
message = MIMEMultipart("alternative")
|
||||
message["From"] = f"{self.from_name} <{self.from_email}>"
|
||||
message["To"] = to_email
|
||||
message["Subject"] = subject
|
||||
|
||||
# Attach HTML body
|
||||
html_part = MIMEText(html_body, "html", "utf-8")
|
||||
message.attach(html_part)
|
||||
|
||||
# Send via async SMTP with STARTTLS
|
||||
# Using start_tls parameter for automatic STARTTLS handling
|
||||
smtp = aiosmtplib.SMTP(
|
||||
hostname=self.smtp_host,
|
||||
port=self.smtp_port,
|
||||
start_tls=self.use_tls, # Use start_tls instead of use_tls
|
||||
timeout=30
|
||||
)
|
||||
|
||||
try:
|
||||
await smtp.connect()
|
||||
await smtp.login(self.smtp_user, self.smtp_password)
|
||||
await smtp.send_message(message)
|
||||
finally:
|
||||
try:
|
||||
await smtp.quit()
|
||||
except:
|
||||
pass
|
||||
|
||||
def _create_email_template(self, code: str, username: str) -> str:
|
||||
"""Generate HTML email template"""
|
||||
return f"""
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<style>
|
||||
body {{
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Arial, sans-serif;
|
||||
line-height: 1.6;
|
||||
color: #333;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
background-color: #f5f5f5;
|
||||
}}
|
||||
.container {{
|
||||
max-width: 600px;
|
||||
margin: 0 auto;
|
||||
background: white;
|
||||
}}
|
||||
.header {{
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
color: white;
|
||||
padding: 40px 20px;
|
||||
text-align: center;
|
||||
}}
|
||||
.header h1 {{
|
||||
margin: 0;
|
||||
font-size: 28px;
|
||||
}}
|
||||
.content {{
|
||||
padding: 40px 20px;
|
||||
}}
|
||||
.code-box {{
|
||||
background: linear-gradient(135deg, #f5f7fa 0%, #c3cfe2 100%);
|
||||
border: 3px solid #667eea;
|
||||
border-radius: 12px;
|
||||
padding: 30px;
|
||||
margin: 30px 0;
|
||||
text-align: center;
|
||||
}}
|
||||
.code {{
|
||||
font-size: 42px;
|
||||
font-weight: bold;
|
||||
letter-spacing: 12px;
|
||||
color: #667eea;
|
||||
font-family: 'Courier New', monospace;
|
||||
display: block;
|
||||
margin: 15px 0;
|
||||
}}
|
||||
.warning {{
|
||||
background-color: #fff3cd;
|
||||
border-left: 4px solid #ffc107;
|
||||
padding: 15px;
|
||||
margin: 20px 0;
|
||||
border-radius: 4px;
|
||||
}}
|
||||
.footer {{
|
||||
text-align: center;
|
||||
color: #666;
|
||||
font-size: 12px;
|
||||
padding: 20px;
|
||||
border-top: 1px solid #e0e0e0;
|
||||
background-color: #f9f9f9;
|
||||
}}
|
||||
.button {{
|
||||
display: inline-block;
|
||||
padding: 12px 24px;
|
||||
background-color: #667eea;
|
||||
color: white;
|
||||
text-decoration: none;
|
||||
border-radius: 6px;
|
||||
margin-top: 20px;
|
||||
}}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<div class="header">
|
||||
<h1>ROA2WEB</h1>
|
||||
<p style="margin: 10px 0 0 0; opacity: 0.9;">Autentificare Telegram Bot</p>
|
||||
</div>
|
||||
|
||||
<div class="content">
|
||||
<p>Salut <strong>{username}</strong>,</p>
|
||||
|
||||
<p>Ai solicitat autentificarea în aplicația ROA2WEB Telegram Bot.</p>
|
||||
|
||||
<div class="code-box">
|
||||
<p style="margin: 0; font-size: 14px; color: #666; font-weight: 500;">
|
||||
Codul tău de autentificare:
|
||||
</p>
|
||||
<span class="code">{code}</span>
|
||||
<p style="margin: 0; font-size: 12px; color: #888;">
|
||||
Introdu acest cod în conversația Telegram
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="warning">
|
||||
<strong>Important:</strong> Acest cod expiră în <strong>5 minute</strong>
|
||||
și poate fi folosit o singură dată.
|
||||
</div>
|
||||
|
||||
<p>După introducerea codului, vei fi solicitat să introduci parola ta Oracle.</p>
|
||||
|
||||
<hr style="border: none; border-top: 1px solid #e0e0e0; margin: 30px 0;">
|
||||
|
||||
<p style="font-size: 14px; color: #666;">
|
||||
<strong>Nu ai solicitat acest cod?</strong><br>
|
||||
Dacă nu ai inițiat această autentificare, poți ignora acest email în siguranță.
|
||||
Nimeni nu va avea acces la contul tău fără parola ta Oracle.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="footer">
|
||||
<p><strong>ROA2WEB</strong> - ERP Reports Application</p>
|
||||
<p>Acest email a fost trimis automat. Te rugăm să nu răspunzi.</p>
|
||||
<p style="margin-top: 10px; color: #999;">
|
||||
© 2025 ROA2WEB. All rights reserved.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
"""
|
||||
|
||||
|
||||
# Singleton instance
|
||||
_email_service: Optional[EmailService] = None
|
||||
|
||||
|
||||
def get_email_service() -> EmailService:
|
||||
"""Get or create singleton email service instance"""
|
||||
global _email_service
|
||||
if _email_service is None:
|
||||
_email_service = EmailService()
|
||||
return _email_service
|
||||
Reference in New Issue
Block a user