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:
2025-12-29 23:48:14 +02:00
parent 2a101f1ef5
commit c5e051ad80
378 changed files with 7566 additions and 73730 deletions

View 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