Cache System (Backend): - Implemented two-tier hybrid cache: L1 (in-memory) + L2 (SQLite) - L1 cache: Fast dictionary-based with 5-minute TTL for hot data - L2 cache: Persistent SQLite with 1-hour TTL for warm data - Cache decorator with automatic tier management and fallback - Cache key generation with per-user isolation - Event monitoring system for cache statistics - Cache benchmarking utilities for performance testing - Added cache management endpoints: /api/cache/stats, /api/cache/clear, /api/cache/benchmark - Cache configuration via environment variables (CACHE_ENABLED, CACHE_L1_TTL, etc.) Backend Services: - Updated dashboard_service to use @cached decorator with request context - Added cache support to invoice_service and treasury_service - Integrated cache manager into main.py with lifespan events - Added Request parameter to service methods for cache metadata Frontend Enhancements: - New CacheStatsView.vue for real-time cache monitoring dashboard - Cache store (cacheStore.js) for state management - Updated router to include /cache-stats route - Navigation updates in DashboardHeader and HamburgerMenu - Cache stats accessible from main navigation Telegram Bot Improvements: - Enhanced formatters with YTD comparison data - Improved menu navigation and button layout - Better error handling and user feedback - Bot startup improvements with graceful shutdown Auth & Middleware: - Enhanced middleware with cache metadata injection - Improved request state handling for cache source tracking Development: - Updated start-dev.sh with better error handling - Added TELEGRAM_EMAIL_AUTH_PLAN.md documentation - Updated requirements.txt with aiosqlite for async SQLite Performance: - L1 cache provides <1ms response for hot data - L2 cache provides ~5ms response for warm data - Database queries only for cold data or cache misses - Cache hit rates tracked and displayed in real-time 🤖 Generated with Claude Code Co-Authored-By: Claude <noreply@anthropic.com>
1674 lines
58 KiB
Markdown
1674 lines
58 KiB
Markdown
# Plan: Autentificare Telegram Bot prin Email + Parolă (2FA)
|
|
|
|
**Data**: 2025-11-07
|
|
**Autor**: Claude Code
|
|
**Status**: Planificat - Neîmplementat
|
|
|
|
---
|
|
|
|
## 🎯 Obiectiv
|
|
|
|
Implementare autentificare alternativă **email + parolă** pentru Telegram Bot, în **paralel** cu metoda actuală (cod din web app). Ambele metode vor fi disponibile pentru toți utilizatorii.
|
|
|
|
**Cerințe cheie:**
|
|
- ✅ Minimal invaziv - nu modifica logica existentă
|
|
- ✅ Ambele metode de autentificare în paralel
|
|
- ✅2FA real: email possession + parolă Oracle
|
|
- ✅ Simplu de testat cu un singur utilizator (mmarius28@gmail.com)
|
|
|
|
---
|
|
|
|
## 📊 Context: Sistemul Actual
|
|
|
|
### Metoda Actuală de Autentificare (rămâne neschimbată)
|
|
|
|
```
|
|
1. User se autentifică în web app (username + parolă)
|
|
2. User cere linking Telegram → backend generează cod 8-char
|
|
3. Backend salvează cod în telegram-bot via POST /internal/save-code
|
|
4. User trimite /start ABC123XY în Telegram
|
|
5. Bot validează codul și auto-linkează (fără parolă din nou)
|
|
6. User autentificat în bot
|
|
```
|
|
|
|
**Caracteristici:**
|
|
- Nu necesită email
|
|
- Necesită acces la web app
|
|
- Auto-linking fără re-introducere parolă
|
|
- Cod expiră în 15 minute
|
|
|
|
---
|
|
|
|
## 🔄 Noul Flux de Autentificare (Email + Parolă)
|
|
|
|
### Flow Complet
|
|
|
|
```
|
|
┌─────────────────────────────────────────────────────────────┐
|
|
│ 1. User: /login SAU apasă buton "🔐 Login cu Email" │
|
|
└─────────────────────────────────────────────────────────────┘
|
|
↓
|
|
┌─────────────────────────────────────────────────────────────┐
|
|
│ 2. Bot: "📧 Introdu adresa ta de email Oracle:" │
|
|
└─────────────────────────────────────────────────────────────┘
|
|
↓
|
|
┌─────────────────────────────────────────────────────────────┐
|
|
│ 3. User: mmarius28@gmail.com │
|
|
└─────────────────────────────────────────────────────────────┘
|
|
↓
|
|
┌─────────────────────────────────────────────────────────────┐
|
|
│ 4. Bot: │
|
|
│ - Verifică email în Oracle UTILIZATORI table │
|
|
│ - Generează cod 6-digit random │
|
|
│ - Salvează în email_auth_codes (expiry 5 min) │
|
|
│ - Trimite email SMTP: "Codul tău: 123456" │
|
|
└─────────────────────────────────────────────────────────────┘
|
|
↓
|
|
┌─────────────────────────────────────────────────────────────┐
|
|
│ 5. Bot: "✉️ Cod trimis pe email. Introdu codul (5 min):" │
|
|
└─────────────────────────────────────────────────────────────┘
|
|
↓
|
|
┌─────────────────────────────────────────────────────────────┐
|
|
│ 6. User: 123456 │
|
|
└─────────────────────────────────────────────────────────────┘
|
|
↓
|
|
┌─────────────────────────────────────────────────────────────┐
|
|
│ 7. Bot: │
|
|
│ - Validează cod în email_auth_codes │
|
|
│ - Verifică expiry (5 minute) │
|
|
│ - Marchează cod ca "used" │
|
|
└─────────────────────────────────────────────────────────────┘
|
|
↓
|
|
┌─────────────────────────────────────────────────────────────┐
|
|
│ 8. Bot: "🔐 Introdu parola ta Oracle:" │
|
|
└─────────────────────────────────────────────────────────────┘
|
|
↓
|
|
┌─────────────────────────────────────────────────────────────┐
|
|
│ 9. User: parola_mea (mesaj va fi șters automat) │
|
|
└─────────────────────────────────────────────────────────────┘
|
|
↓
|
|
┌─────────────────────────────────────────────────────────────┐
|
|
│ 10. Bot → Backend: │
|
|
│ POST /api/telegram/auth/login-with-email │
|
|
│ { │
|
|
│ "email": "mmarius28@gmail.com", │
|
|
│ "password": "parola_mea", │
|
|
│ "telegram_user_id": 123456789 │
|
|
│ } │
|
|
└─────────────────────────────────────────────────────────────┘
|
|
↓
|
|
┌─────────────────────────────────────────────────────────────┐
|
|
│ 11. Backend: │
|
|
│ - Găsește username din email în UTILIZATORI │
|
|
│ - Verifică parolă: pack_drepturi.verificautilizator() │
|
|
│ - Generează JWT tokens (access + refresh) │
|
|
│ - Returnează tokens + user data │
|
|
└─────────────────────────────────────────────────────────────┘
|
|
↓
|
|
┌─────────────────────────────────────────────────────────────┐
|
|
│ 12. Bot: │
|
|
│ - Salvează JWT în telegram_users table │
|
|
│ - Linkează telegram_user_id cu oracle_username │
|
|
│ - Șterge mesajele cu parolă │
|
|
└─────────────────────────────────────────────────────────────┘
|
|
↓
|
|
┌─────────────────────────────────────────────────────────────┐
|
|
│ 13. Bot: "✅ Autentificat cu succes! Folosește /help" │
|
|
└─────────────────────────────────────────────────────────────┘
|
|
```
|
|
|
|
### Conversation States (ConversationHandler)
|
|
|
|
```python
|
|
AWAITING_EMAIL = 1 # Așteaptă email de la user
|
|
AWAITING_CODE = 2 # Așteaptă cod din email
|
|
AWAITING_PASSWORD = 3 # Așteaptă parolă Oracle
|
|
```
|
|
|
|
---
|
|
|
|
## 📦 Componente Noi (Arhitectură)
|
|
|
|
### 1. Email Service (SMTP Client)
|
|
|
|
**Fișier NOU**: `reports-app/telegram-bot/app/utils/email_service.py`
|
|
|
|
```python
|
|
import smtplib
|
|
from email.mime.text import MIMEText
|
|
from email.mime.multipart import MIMEMultipart
|
|
import os
|
|
|
|
class EmailService:
|
|
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", "ups@romfast.ro")
|
|
self.smtp_password = os.getenv("SMTP_PASSWORD", "#Ups2020#")
|
|
self.from_email = os.getenv("SMTP_FROM_EMAIL", "ups@romfast.ro")
|
|
self.from_name = os.getenv("SMTP_FROM_NAME", "ROA2WEB")
|
|
|
|
async def send_auth_code(self, to_email: str, code: str, username: str) -> bool:
|
|
"""
|
|
Trimite cod de autentificare pe email
|
|
Returns: True dacă email trimis cu succes
|
|
"""
|
|
subject = "Codul tău de autentificare ROA2WEB"
|
|
|
|
html_body = f"""
|
|
<html>
|
|
<body style="font-family: Arial, sans-serif;">
|
|
<h2>🔐 Autentificare Telegram Bot</h2>
|
|
<p>Salut <strong>{username}</strong>,</p>
|
|
<p>Codul tău de autentificare este:</p>
|
|
<h1 style="background-color: #f0f0f0; padding: 20px; text-align: center; letter-spacing: 5px;">
|
|
{code}
|
|
</h1>
|
|
<p><strong>Codul expiră în 5 minute.</strong></p>
|
|
<p>Dacă nu ai solicitat acest cod, te rugăm să ignori acest email.</p>
|
|
<hr>
|
|
<p style="color: #666; font-size: 12px;">
|
|
ROA2WEB - ERP Reports Application
|
|
</p>
|
|
</body>
|
|
</html>
|
|
"""
|
|
|
|
# Implementare SMTP cu error handling
|
|
```
|
|
|
|
**Funcții:**
|
|
- `send_auth_code(email, code, username)` - trimite cod pe email
|
|
- `_send_email(to, subject, html_body)` - helper SMTP generic
|
|
- Error handling cu retry logic (3 încercări)
|
|
|
|
---
|
|
|
|
### 2. Tabela SQLite Nouă: `email_auth_codes`
|
|
|
|
**Locație**: `reports-app/telegram-bot/app/db/database.py`
|
|
|
|
**Schema SQL:**
|
|
```sql
|
|
CREATE TABLE IF NOT EXISTS email_auth_codes (
|
|
code TEXT PRIMARY KEY, -- 6-digit numeric code (e.g., "123456")
|
|
email TEXT NOT NULL, -- Email utilizator din Oracle
|
|
oracle_username TEXT NOT NULL, -- Username Oracle asociat
|
|
telegram_user_id INTEGER NOT NULL, -- Telegram user ID care a solicitat
|
|
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
|
expires_at TIMESTAMP NOT NULL, -- Current time + 5 minute
|
|
used BOOLEAN DEFAULT 0, -- 0 = nefolosit, 1 = folosit
|
|
used_at TIMESTAMP, -- Timestamp când a fost folosit
|
|
|
|
-- Index pentru query performance
|
|
INDEX idx_email (email),
|
|
INDEX idx_expires_at (expires_at),
|
|
INDEX idx_telegram_user_id (telegram_user_id)
|
|
);
|
|
```
|
|
|
|
**Operații CRUD** (în `app/db/operations.py`):
|
|
```python
|
|
async def create_email_auth_code(code: str, email: str, username: str, telegram_user_id: int) -> bool
|
|
async def get_email_auth_code(code: str) -> Optional[Dict]
|
|
async def mark_email_code_used(code: str) -> bool
|
|
async def delete_expired_email_codes() -> int
|
|
async def get_pending_email_code_for_user(telegram_user_id: int) -> Optional[Dict]
|
|
```
|
|
|
|
**Caracteristici:**
|
|
- Cod 6-digit random numeric (000000 - 999999)
|
|
- Expirare 5 minute
|
|
- One-time use (marcat ca `used=1`)
|
|
- Auto-cleanup de către job-ul existent (hourly)
|
|
|
|
---
|
|
|
|
### 3. Backend Endpoint NOU: `POST /api/telegram/auth/login-with-email`
|
|
|
|
**Locație**: `reports-app/backend/app/routers/telegram.py`
|
|
|
|
**Request Schema:**
|
|
```python
|
|
class TelegramEmailLoginRequest(BaseModel):
|
|
email: EmailStr
|
|
password: str
|
|
telegram_user_id: int
|
|
|
|
class TelegramEmailLoginResponse(BaseModel):
|
|
success: bool
|
|
access_token: str
|
|
refresh_token: str
|
|
token_type: str = "bearer"
|
|
user_id: int
|
|
username: str
|
|
companies: List[CompanyInfo]
|
|
message: str
|
|
```
|
|
|
|
**Endpoint Logic:**
|
|
```python
|
|
@router.post("/auth/login-with-email", response_model=TelegramEmailLoginResponse)
|
|
async def login_with_email(request: TelegramEmailLoginRequest):
|
|
"""
|
|
Autentificare Telegram prin email + parolă
|
|
|
|
Flow:
|
|
1. Caută username în Oracle UTILIZATORI by email
|
|
2. Verifică parolă prin pack_drepturi.verificautilizator(username, password)
|
|
3. Dacă valid: generează JWT tokens
|
|
4. Returnează tokens + user data
|
|
"""
|
|
|
|
async with oracle_pool.get_connection() as connection:
|
|
# 1. Find username by email
|
|
cursor = connection.cursor()
|
|
cursor.execute("""
|
|
SELECT ID_UTIL, UTILIZATOR
|
|
FROM CONTAFIN_ORACLE.UTILIZATORI
|
|
WHERE UPPER(EMAIL) = UPPER(:email)
|
|
""", {"email": request.email})
|
|
|
|
user_row = cursor.fetchone()
|
|
if not user_row:
|
|
raise HTTPException(status_code=404, detail="Email not found")
|
|
|
|
user_id, username = user_row
|
|
|
|
# 2. Verify password via stored procedure
|
|
cursor.execute("""
|
|
SELECT pack_drepturi.verificautilizator(:username, :password)
|
|
FROM DUAL
|
|
""", {"username": username, "password": request.password})
|
|
|
|
result = cursor.fetchone()[0]
|
|
if result == -1:
|
|
raise HTTPException(status_code=401, detail="Invalid password")
|
|
|
|
# 3. Get user companies
|
|
companies = get_user_companies(user_id, connection)
|
|
|
|
# 4. Generate JWT tokens
|
|
access_token = create_access_token(...)
|
|
refresh_token = create_refresh_token(...)
|
|
|
|
return TelegramEmailLoginResponse(
|
|
success=True,
|
|
access_token=access_token,
|
|
refresh_token=refresh_token,
|
|
user_id=user_id,
|
|
username=username,
|
|
companies=companies,
|
|
message="Authentication successful"
|
|
)
|
|
```
|
|
|
|
**Securitate:**
|
|
- Rate limiting: 5 requests / 5 minutes per telegram_user_id
|
|
- Password validation prin Oracle stored procedure (nu stocăm parole)
|
|
- HTTPS required în producție
|
|
- Logging pentru failed attempts
|
|
|
|
---
|
|
|
|
### 4. Bot Handlers NOI: `/login` + ConversationHandler
|
|
|
|
**Fișier NOU**: `reports-app/telegram-bot/app/bot/email_handlers.py`
|
|
|
|
#### Command: `/login`
|
|
|
|
```python
|
|
from telegram import Update, InlineKeyboardButton, InlineKeyboardMarkup
|
|
from telegram.ext import ContextTypes, ConversationHandler, CommandHandler, MessageHandler, filters
|
|
|
|
# Conversation states
|
|
AWAITING_EMAIL, AWAITING_CODE, AWAITING_PASSWORD = range(3)
|
|
|
|
async def login_command(update: Update, context: ContextTypes.DEFAULT_TYPE):
|
|
"""
|
|
Handler pentru /login command
|
|
Inițiază conversation pentru email auth
|
|
"""
|
|
user = update.effective_user
|
|
|
|
# Check dacă e deja autentificat
|
|
if await is_user_authenticated(user.id):
|
|
await update.message.reply_text(
|
|
"✅ Ești deja autentificat!\n"
|
|
"Folosește /unlink pentru a te deautentifica."
|
|
)
|
|
return ConversationHandler.END
|
|
|
|
# Afișează metode de autentificare
|
|
keyboard = [
|
|
[InlineKeyboardButton("🔐 Login cu Email", callback_data="email_login")],
|
|
[InlineKeyboardButton("🌐 Login din Web App", callback_data="web_login")],
|
|
[InlineKeyboardButton("❌ Anulează", callback_data="cancel")]
|
|
]
|
|
reply_markup = InlineKeyboardMarkup(keyboard)
|
|
|
|
await update.message.reply_text(
|
|
"🔑 Alege metoda de autentificare:\n\n"
|
|
"📧 **Email + Parolă**: Primești cod pe email, apoi introduci parola\n"
|
|
"🌐 **Web App**: Generează cod din aplicația web",
|
|
reply_markup=reply_markup
|
|
)
|
|
|
|
return AWAITING_EMAIL
|
|
|
|
async def email_login_callback(update: Update, context: ContextTypes.DEFAULT_TYPE):
|
|
"""Callback pentru butonul 'Login cu Email'"""
|
|
query = update.callback_query
|
|
await query.answer()
|
|
|
|
await query.edit_message_text(
|
|
"📧 **Autentificare prin Email**\n\n"
|
|
"Te rugăm să introduci adresa ta de email Oracle:\n"
|
|
"(Exemplu: nume.prenume@companie.ro)"
|
|
)
|
|
|
|
return AWAITING_EMAIL
|
|
|
|
async def receive_email(update: Update, context: ContextTypes.DEFAULT_TYPE):
|
|
"""
|
|
Handler pentru primirea email-ului
|
|
"""
|
|
email = update.message.text.strip()
|
|
user_id = update.effective_user.id
|
|
|
|
# Validare format email
|
|
if not is_valid_email(email):
|
|
await update.message.reply_text(
|
|
"❌ Email invalid. Te rugăm să introduci o adresă de email validă."
|
|
)
|
|
return AWAITING_EMAIL
|
|
|
|
# Verifică email în Oracle + trimite cod
|
|
try:
|
|
# 1. Verifică email în Oracle UTILIZATORI
|
|
username = await verify_email_in_oracle(email)
|
|
if not username:
|
|
await update.message.reply_text(
|
|
"❌ Email-ul nu este înregistrat în sistem.\n"
|
|
"Contactează administratorul pentru a-ți adăuga email-ul."
|
|
)
|
|
return ConversationHandler.END
|
|
|
|
# 2. Generează cod 6-digit
|
|
code = generate_6digit_code()
|
|
|
|
# 3. Salvează în email_auth_codes
|
|
await save_email_auth_code(
|
|
code=code,
|
|
email=email,
|
|
username=username,
|
|
telegram_user_id=user_id
|
|
)
|
|
|
|
# 4. Trimite email
|
|
email_sent = await email_service.send_auth_code(email, code, username)
|
|
|
|
if not email_sent:
|
|
await update.message.reply_text(
|
|
"❌ Eroare la trimiterea email-ului. Te rugăm să încerci din nou."
|
|
)
|
|
return ConversationHandler.END
|
|
|
|
# 5. Salvează email în context
|
|
context.user_data['pending_email'] = email
|
|
context.user_data['pending_username'] = username
|
|
|
|
await update.message.reply_text(
|
|
f"✉️ **Cod trimis pe {email}**\n\n"
|
|
"Verifică inbox-ul (și spam) și introdu codul de 6 cifre.\n"
|
|
"⏱ Codul expiră în **5 minute**.\n\n"
|
|
"Scrie /cancel pentru a anula."
|
|
)
|
|
|
|
return AWAITING_CODE
|
|
|
|
except Exception as e:
|
|
logger.error(f"Email login error: {e}")
|
|
await update.message.reply_text(
|
|
"❌ Eroare internă. Te rugăm să încerci din nou mai târziu."
|
|
)
|
|
return ConversationHandler.END
|
|
|
|
async def receive_code(update: Update, context: ContextTypes.DEFAULT_TYPE):
|
|
"""
|
|
Handler pentru primirea codului din email
|
|
"""
|
|
code = update.message.text.strip()
|
|
user_id = update.effective_user.id
|
|
|
|
# Validare format cod (6 digits)
|
|
if not code.isdigit() or len(code) != 6:
|
|
await update.message.reply_text(
|
|
"❌ Cod invalid. Introdu cele 6 cifre din email."
|
|
)
|
|
return AWAITING_CODE
|
|
|
|
# Verifică cod în DB
|
|
try:
|
|
code_data = await get_email_auth_code(code)
|
|
|
|
if not code_data:
|
|
await update.message.reply_text(
|
|
"❌ Cod invalid sau expirat. Te rugăm să reîncepi cu /login"
|
|
)
|
|
return ConversationHandler.END
|
|
|
|
# Verificări
|
|
if code_data['used']:
|
|
await update.message.reply_text(
|
|
"❌ Cod deja folosit. Te rugăm să reîncepi cu /login"
|
|
)
|
|
return ConversationHandler.END
|
|
|
|
if code_data['telegram_user_id'] != user_id:
|
|
await update.message.reply_text(
|
|
"❌ Codul nu îți aparține."
|
|
)
|
|
return ConversationHandler.END
|
|
|
|
if datetime.now() > code_data['expires_at']:
|
|
await update.message.reply_text(
|
|
"❌ Codul a expirat. Te rugăm să reîncepi cu /login"
|
|
)
|
|
return ConversationHandler.END
|
|
|
|
# Marchează cod ca folosit
|
|
await mark_email_code_used(code)
|
|
|
|
# Salvează username în context
|
|
context.user_data['verified_username'] = code_data['oracle_username']
|
|
context.user_data['verified_email'] = code_data['email']
|
|
|
|
await update.message.reply_text(
|
|
"✅ **Cod validat cu succes!**\n\n"
|
|
"🔐 Acum introdu parola ta Oracle:\n"
|
|
"(Parola va fi ștearsă automat după verificare)"
|
|
)
|
|
|
|
return AWAITING_PASSWORD
|
|
|
|
except Exception as e:
|
|
logger.error(f"Code validation error: {e}")
|
|
await update.message.reply_text(
|
|
"❌ Eroare la validarea codului. Te rugăm să încerci din nou."
|
|
)
|
|
return ConversationHandler.END
|
|
|
|
async def receive_password(update: Update, context: ContextTypes.DEFAULT_TYPE):
|
|
"""
|
|
Handler pentru primirea parolei
|
|
"""
|
|
password = update.message.text.strip()
|
|
user_id = update.effective_user.id
|
|
|
|
# Șterge imediat mesajul cu parola
|
|
try:
|
|
await update.message.delete()
|
|
except:
|
|
pass
|
|
|
|
username = context.user_data.get('verified_username')
|
|
email = context.user_data.get('verified_email')
|
|
|
|
if not username or not email:
|
|
await update.effective_chat.send_message(
|
|
"❌ Sesiune expirată. Te rugăm să reîncepi cu /login"
|
|
)
|
|
return ConversationHandler.END
|
|
|
|
# Trimite loading message
|
|
loading_msg = await update.effective_chat.send_message(
|
|
"⏳ Verificare credențiale..."
|
|
)
|
|
|
|
try:
|
|
# Call backend endpoint
|
|
response = await backend_client.login_with_email(
|
|
email=email,
|
|
password=password,
|
|
telegram_user_id=user_id
|
|
)
|
|
|
|
if not response['success']:
|
|
await loading_msg.edit_text(
|
|
"❌ Parolă incorectă. Te rugăm să reîncepi cu /login"
|
|
)
|
|
return ConversationHandler.END
|
|
|
|
# Salvează user în telegram_users
|
|
await save_telegram_user(
|
|
telegram_user_id=user_id,
|
|
username=update.effective_user.username,
|
|
first_name=update.effective_user.first_name,
|
|
last_name=update.effective_user.last_name,
|
|
oracle_username=response['username'],
|
|
jwt_token=response['access_token'],
|
|
jwt_refresh_token=response['refresh_token']
|
|
)
|
|
|
|
# Success message
|
|
companies_list = "\n".join([f"• {c['name']}" for c in response['companies'][:5]])
|
|
|
|
await loading_msg.edit_text(
|
|
f"✅ **Autentificat cu succes!**\n\n"
|
|
f"👤 Utilizator: **{response['username']}**\n"
|
|
f"🏢 Companii disponibile: {len(response['companies'])}\n\n"
|
|
f"{companies_list}\n\n"
|
|
f"Folosește /help pentru comenzi disponibile."
|
|
)
|
|
|
|
# Clear context
|
|
context.user_data.clear()
|
|
|
|
return ConversationHandler.END
|
|
|
|
except Exception as e:
|
|
logger.error(f"Password verification error: {e}")
|
|
await loading_msg.edit_text(
|
|
"❌ Eroare la autentificare. Te rugăm să încerci din nou cu /login"
|
|
)
|
|
return ConversationHandler.END
|
|
|
|
async def cancel_login(update: Update, context: ContextTypes.DEFAULT_TYPE):
|
|
"""Cancel conversation"""
|
|
context.user_data.clear()
|
|
await update.message.reply_text("❌ Autentificare anulată.")
|
|
return ConversationHandler.END
|
|
|
|
# ConversationHandler setup
|
|
email_login_handler = ConversationHandler(
|
|
entry_points=[
|
|
CommandHandler('login', login_command),
|
|
CallbackQueryHandler(email_login_callback, pattern='^email_login$')
|
|
],
|
|
states={
|
|
AWAITING_EMAIL: [MessageHandler(filters.TEXT & ~filters.COMMAND, receive_email)],
|
|
AWAITING_CODE: [MessageHandler(filters.TEXT & ~filters.COMMAND, receive_code)],
|
|
AWAITING_PASSWORD: [MessageHandler(filters.TEXT & ~filters.COMMAND, receive_password)],
|
|
},
|
|
fallbacks=[
|
|
CommandHandler('cancel', cancel_login),
|
|
CallbackQueryHandler(cancel_login, pattern='^cancel$')
|
|
],
|
|
conversation_timeout=300 # 5 minute timeout
|
|
)
|
|
```
|
|
|
|
**Handler Registration** (în `app/bot/handlers.py`):
|
|
```python
|
|
from app.bot.email_handlers import email_login_handler
|
|
|
|
def setup_handlers(application):
|
|
# ... existing handlers ...
|
|
|
|
# Email login handler
|
|
application.add_handler(email_login_handler)
|
|
```
|
|
|
|
---
|
|
|
|
### 5. Backend API Client Method
|
|
|
|
**Locație**: `reports-app/telegram-bot/app/api/client.py`
|
|
|
|
```python
|
|
class BackendAPIClient:
|
|
# ... existing methods ...
|
|
|
|
async def login_with_email(
|
|
self,
|
|
email: str,
|
|
password: str,
|
|
telegram_user_id: int
|
|
) -> dict:
|
|
"""
|
|
Login via email + password
|
|
"""
|
|
try:
|
|
response = await self.client.post(
|
|
f"{self.base_url}/api/telegram/auth/login-with-email",
|
|
json={
|
|
"email": email,
|
|
"password": password,
|
|
"telegram_user_id": telegram_user_id
|
|
}
|
|
)
|
|
response.raise_for_status()
|
|
return response.json()
|
|
except httpx.HTTPError as e:
|
|
logger.error(f"Email login failed: {e}")
|
|
raise
|
|
```
|
|
|
|
---
|
|
|
|
## 🔧 Environment Variables
|
|
|
|
### Backend `.env` (nu necesită modificări - doar pentru referință)
|
|
```bash
|
|
# Existente (nu modificăm)
|
|
ORACLE_USER=CONTAFIN_ORACLE
|
|
ORACLE_PASSWORD=your_password
|
|
ORACLE_HOST=localhost
|
|
ORACLE_PORT=1526
|
|
ORACLE_SID=ROA
|
|
JWT_SECRET_KEY=your_secret_key
|
|
JWT_ALGORITHM=HS256
|
|
JWT_EXPIRE_MINUTES=30
|
|
```
|
|
|
|
### Telegram Bot `.env` (ADAUGĂ ACESTEA)
|
|
```bash
|
|
# Existing
|
|
TELEGRAM_BOT_TOKEN=your_bot_token
|
|
BACKEND_URL=http://localhost:8000
|
|
DATABASE_PATH=data/telegram_bot.db
|
|
|
|
# NEW: SMTP Configuration
|
|
SMTP_HOST=mail.romfast.ro
|
|
SMTP_PORT=587
|
|
SMTP_USER=ups@romfast.ro
|
|
SMTP_PASSWORD=#Ups2020#
|
|
SMTP_FROM_EMAIL=ups@romfast.ro
|
|
SMTP_FROM_NAME=ROA2WEB
|
|
SMTP_USE_TLS=true
|
|
|
|
# NEW: Email Auth Settings
|
|
EMAIL_CODE_EXPIRY_MINUTES=5
|
|
EMAIL_CODE_LENGTH=6
|
|
MAX_EMAIL_ATTEMPTS_PER_HOUR=3
|
|
```
|
|
|
|
---
|
|
|
|
## 🔒 Securitate & Validări
|
|
|
|
### 1. Email Validation
|
|
```python
|
|
import re
|
|
|
|
def is_valid_email(email: str) -> bool:
|
|
"""Validare format email"""
|
|
pattern = r'^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$'
|
|
return bool(re.match(pattern, email))
|
|
|
|
async def verify_email_in_oracle(email: str) -> Optional[str]:
|
|
"""
|
|
Verifică dacă email există în Oracle UTILIZATORI
|
|
Returns: username dacă există, None altfel
|
|
"""
|
|
async with oracle_pool.get_connection() as connection:
|
|
cursor = connection.cursor()
|
|
cursor.execute("""
|
|
SELECT UTILIZATOR
|
|
FROM CONTAFIN_ORACLE.UTILIZATORI
|
|
WHERE UPPER(EMAIL) = UPPER(:email)
|
|
AND ACTIV = 1
|
|
""", {"email": email})
|
|
|
|
row = cursor.fetchone()
|
|
return row[0] if row else None
|
|
```
|
|
|
|
### 2. Code Generation & Storage
|
|
```python
|
|
import random
|
|
import string
|
|
from datetime import datetime, timedelta
|
|
|
|
def generate_6digit_code() -> str:
|
|
"""Generează cod random 6-digit"""
|
|
return ''.join(random.choices(string.digits, k=6))
|
|
|
|
async def save_email_auth_code(
|
|
code: str,
|
|
email: str,
|
|
username: str,
|
|
telegram_user_id: int
|
|
) -> bool:
|
|
"""Salvează cod în DB cu expiry 5 minute"""
|
|
expires_at = datetime.now() + timedelta(minutes=5)
|
|
|
|
async with get_db_connection() as db:
|
|
await db.execute("""
|
|
INSERT INTO email_auth_codes
|
|
(code, email, oracle_username, telegram_user_id, expires_at)
|
|
VALUES (?, ?, ?, ?, ?)
|
|
""", (code, email, username, telegram_user_id, expires_at))
|
|
await db.commit()
|
|
|
|
return True
|
|
```
|
|
|
|
### 3. Rate Limiting
|
|
```python
|
|
from collections import defaultdict
|
|
from datetime import datetime, timedelta
|
|
|
|
# In-memory rate limiter (sau Redis în producție)
|
|
email_attempts = defaultdict(list)
|
|
|
|
async def check_rate_limit(identifier: str, max_attempts: int = 3, window_minutes: int = 60) -> bool:
|
|
"""
|
|
Rate limiting pentru email requests
|
|
identifier poate fi: email sau telegram_user_id
|
|
"""
|
|
now = datetime.now()
|
|
cutoff = now - timedelta(minutes=window_minutes)
|
|
|
|
# Curăță attempts vechi
|
|
email_attempts[identifier] = [
|
|
attempt for attempt in email_attempts[identifier]
|
|
if attempt > cutoff
|
|
]
|
|
|
|
# Verifică limita
|
|
if len(email_attempts[identifier]) >= max_attempts:
|
|
return False # Rate limit exceeded
|
|
|
|
# Adaugă attempt nou
|
|
email_attempts[identifier].append(now)
|
|
return True # OK to proceed
|
|
```
|
|
|
|
### 4. Password Security
|
|
```python
|
|
async def receive_password(update: Update, context: ContextTypes.DEFAULT_TYPE):
|
|
"""Handler cu securitate mărite pentru parolă"""
|
|
password = update.message.text.strip()
|
|
|
|
# 1. Șterge IMEDIAT mesajul cu parola
|
|
try:
|
|
await update.message.delete()
|
|
except Exception as e:
|
|
logger.warning(f"Could not delete password message: {e}")
|
|
|
|
# 2. NU loga parola niciodată
|
|
logger.info(f"Password received for user {update.effective_user.id}")
|
|
|
|
# 3. Verifică parola prin backend (Oracle)
|
|
# ... (vezi cod de mai sus)
|
|
|
|
# 4. Șterge parola din memorie
|
|
del password
|
|
```
|
|
|
|
### 5. Auto-Cleanup Job
|
|
```python
|
|
async def cleanup_expired_codes():
|
|
"""Job periodic pentru curățarea codurilor expirate"""
|
|
while True:
|
|
try:
|
|
async with get_db_connection() as db:
|
|
# Șterge email codes expirate
|
|
await db.execute("""
|
|
DELETE FROM email_auth_codes
|
|
WHERE expires_at < ?
|
|
OR (used = 1 AND used_at < ?)
|
|
""", (
|
|
datetime.now(),
|
|
datetime.now() - timedelta(days=1) # Cleanup used codes după 1 zi
|
|
))
|
|
await db.commit()
|
|
|
|
deleted = db.total_changes
|
|
logger.info(f"Cleaned up {deleted} expired email auth codes")
|
|
|
|
except Exception as e:
|
|
logger.error(f"Cleanup job error: {e}")
|
|
|
|
# Run every hour
|
|
await asyncio.sleep(3600)
|
|
|
|
# Start cleanup job în main.py
|
|
asyncio.create_task(cleanup_expired_codes())
|
|
```
|
|
|
|
---
|
|
|
|
## 📝 Fișiere Modificate/Create
|
|
|
|
### **CREATE** (6 fișiere noi):
|
|
|
|
1. **`reports-app/telegram-bot/app/utils/email_service.py`**
|
|
- SMTP client pentru trimitere email-uri
|
|
- Funcție: `send_auth_code(email, code, username)`
|
|
- Error handling + retry logic
|
|
|
|
2. **`reports-app/telegram-bot/app/auth/email_auth.py`**
|
|
- Logică autentificare email
|
|
- Funcții: `verify_email_in_oracle()`, `generate_6digit_code()`, `check_rate_limit()`
|
|
|
|
3. **`reports-app/telegram-bot/app/bot/email_handlers.py`**
|
|
- ConversationHandler pentru `/login`
|
|
- States: AWAITING_EMAIL → AWAITING_CODE → AWAITING_PASSWORD
|
|
- Handlers: `receive_email()`, `receive_code()`, `receive_password()`
|
|
|
|
4. **`reports-app/backend/app/schemas/telegram_email_auth.py`**
|
|
- Pydantic schemas pentru email auth
|
|
- `TelegramEmailLoginRequest`, `TelegramEmailLoginResponse`
|
|
|
|
5. **`reports-app/telegram-bot/tests/test_email_auth.py`**
|
|
- Unit tests pentru email auth flow
|
|
- Mock SMTP, Oracle, backend API
|
|
|
|
6. **`TELEGRAM_EMAIL_AUTH_PLAN.md`**
|
|
- Acest document (plan detaliat)
|
|
|
|
### **MODIFY** (6 fișiere existente):
|
|
|
|
1. **`reports-app/telegram-bot/app/db/database.py`**
|
|
- **Adaugă**: Schema tabela `email_auth_codes`
|
|
- **Linie**: ~40-60 (în funcția `init_database()`)
|
|
|
|
2. **`reports-app/telegram-bot/app/db/operations.py`**
|
|
- **Adaugă**: CRUD operations pentru `email_auth_codes`
|
|
- Funcții noi: `create_email_auth_code()`, `get_email_auth_code()`, `mark_email_code_used()`, etc.
|
|
- **Linii**: ~200-300 (la final de fișier)
|
|
|
|
3. **`reports-app/telegram-bot/app/bot/handlers.py`**
|
|
- **Adaugă**: Import și register `email_login_handler`
|
|
- **Linie**: ~10 (import), ~150 (register în `setup_handlers()`)
|
|
- **Modificare**: Update `/start` message pentru a menționa `/login`
|
|
|
|
4. **`reports-app/backend/app/routers/telegram.py`**
|
|
- **Adaugă**: Endpoint `POST /auth/login-with-email`
|
|
- **Linii**: ~200-300 (la final de fișier)
|
|
- **Import**: Adaugă schema `TelegramEmailLoginRequest/Response`
|
|
|
|
5. **`reports-app/telegram-bot/app/api/client.py`**
|
|
- **Adaugă**: Method `login_with_email(email, password, telegram_user_id)`
|
|
- **Linii**: ~150-180 (în clasa `BackendAPIClient`)
|
|
|
|
6. **`reports-app/telegram-bot/.env`**
|
|
- **Adaugă**: SMTP configuration variables (vezi secțiunea Environment Variables)
|
|
|
|
### **NO TOUCH** (rămân 100% neschimbate):
|
|
|
|
- ✅ Fluxul actual de linking (web app → cod → /start)
|
|
- ✅ Tabele existente: `telegram_users`, `telegram_auth_codes`, `telegram_sessions`
|
|
- ✅ Endpoint-uri existente: `/generate-code`, `/verify-user`, `/refresh-token`
|
|
- ✅ Handler-e existente: `/start [code]`, `/companies`, `/dashboard`, `/facturi`, etc.
|
|
- ✅ Toate comenzile Telegram existente (funcționalitate păstrată 100%)
|
|
|
|
---
|
|
|
|
## 🧪 Testing Strategy
|
|
|
|
### 1. Unit Tests
|
|
|
|
**Fișier**: `reports-app/telegram-bot/tests/test_email_auth.py`
|
|
|
|
```python
|
|
import pytest
|
|
from unittest.mock import AsyncMock, patch
|
|
from app.auth.email_auth import verify_email_in_oracle, generate_6digit_code, check_rate_limit
|
|
from app.utils.email_service import EmailService
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_generate_6digit_code():
|
|
"""Test generare cod 6-digit"""
|
|
code = generate_6digit_code()
|
|
assert len(code) == 6
|
|
assert code.isdigit()
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_verify_email_in_oracle_success():
|
|
"""Test verificare email valid în Oracle"""
|
|
with patch('app.auth.email_auth.oracle_pool') as mock_pool:
|
|
# Mock Oracle response
|
|
mock_cursor = AsyncMock()
|
|
mock_cursor.fetchone.return_value = ("test_user",)
|
|
|
|
username = await verify_email_in_oracle("test@example.com")
|
|
assert username == "test_user"
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_verify_email_not_found():
|
|
"""Test email inexistent în Oracle"""
|
|
with patch('app.auth.email_auth.oracle_pool') as mock_pool:
|
|
mock_cursor = AsyncMock()
|
|
mock_cursor.fetchone.return_value = None
|
|
|
|
username = await verify_email_in_oracle("notfound@example.com")
|
|
assert username is None
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_rate_limiting():
|
|
"""Test rate limiting pentru email requests"""
|
|
identifier = "test_user_123"
|
|
|
|
# First 3 attempts should pass
|
|
for i in range(3):
|
|
result = await check_rate_limit(identifier, max_attempts=3, window_minutes=60)
|
|
assert result is True
|
|
|
|
# 4th attempt should fail
|
|
result = await check_rate_limit(identifier, max_attempts=3, window_minutes=60)
|
|
assert result is False
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_email_service_send_code():
|
|
"""Test trimitere email cu cod"""
|
|
email_service = EmailService()
|
|
|
|
with patch('app.utils.email_service.smtplib.SMTP') as mock_smtp:
|
|
result = await email_service.send_auth_code(
|
|
to_email="test@example.com",
|
|
code="123456",
|
|
username="test_user"
|
|
)
|
|
|
|
assert result is True
|
|
assert mock_smtp.called
|
|
```
|
|
|
|
### 2. Integration Tests
|
|
|
|
```python
|
|
@pytest.mark.asyncio
|
|
async def test_full_email_auth_flow():
|
|
"""Test complet: email → cod → parolă → JWT"""
|
|
|
|
# 1. Request email code
|
|
email = "mmarius28@gmail.com"
|
|
telegram_user_id = 123456789
|
|
|
|
with patch('app.auth.email_auth.verify_email_in_oracle') as mock_verify:
|
|
mock_verify.return_value = "marius_user"
|
|
|
|
# Generate code
|
|
code = generate_6digit_code()
|
|
await save_email_auth_code(code, email, "marius_user", telegram_user_id)
|
|
|
|
# 2. Validate code
|
|
code_data = await get_email_auth_code(code)
|
|
assert code_data is not None
|
|
assert code_data['email'] == email
|
|
assert code_data['used'] is False
|
|
|
|
# 3. Mock backend login
|
|
with patch('app.api.client.BackendAPIClient.login_with_email') as mock_login:
|
|
mock_login.return_value = {
|
|
'success': True,
|
|
'access_token': 'test_jwt_token',
|
|
'refresh_token': 'test_refresh_token',
|
|
'username': 'marius_user',
|
|
'user_id': 42,
|
|
'companies': []
|
|
}
|
|
|
|
# Verify password
|
|
response = await backend_client.login_with_email(
|
|
email=email,
|
|
password="test_password",
|
|
telegram_user_id=telegram_user_id
|
|
)
|
|
|
|
assert response['success'] is True
|
|
assert 'access_token' in response
|
|
```
|
|
|
|
### 3. Manual Testing Checklist
|
|
|
|
**Fișier**: `reports-app/telegram-bot/tests/MANUAL_EMAIL_AUTH_TESTING.md`
|
|
|
|
```markdown
|
|
# Manual Testing Checklist - Email Authentication
|
|
|
|
## Prerequisites
|
|
- [ ] Backend running on port 8000
|
|
- [ ] Telegram bot running
|
|
- [ ] SMTP credentials configured in .env
|
|
- [ ] Test email: mmarius28@gmail.com exists in Oracle UTILIZATORI table
|
|
|
|
## Test Cases
|
|
|
|
### 1. Successful Authentication Flow
|
|
- [ ] Start bot: `/start`
|
|
- [ ] Type: `/login`
|
|
- [ ] Select "🔐 Login cu Email"
|
|
- [ ] Enter email: `mmarius28@gmail.com`
|
|
- [ ] Verify email received with 6-digit code
|
|
- [ ] Enter code from email
|
|
- [ ] Enter Oracle password
|
|
- [ ] Verify success message with companies list
|
|
- [ ] Test command: `/companies` (should work)
|
|
|
|
### 2. Invalid Email
|
|
- [ ] `/login`
|
|
- [ ] Enter email: `nonexistent@example.com`
|
|
- [ ] Verify error message: "Email-ul nu este înregistrat"
|
|
|
|
### 3. Expired Code
|
|
- [ ] `/login` → enter valid email
|
|
- [ ] Wait 6 minutes (code expiry = 5 min)
|
|
- [ ] Enter code
|
|
- [ ] Verify error: "Cod expirat"
|
|
|
|
### 4. Wrong Code
|
|
- [ ] `/login` → enter valid email
|
|
- [ ] Enter wrong code: `999999`
|
|
- [ ] Verify error: "Cod invalid"
|
|
|
|
### 5. Wrong Password
|
|
- [ ] `/login` → enter email → enter valid code
|
|
- [ ] Enter wrong password
|
|
- [ ] Verify error: "Parolă incorectă"
|
|
|
|
### 6. Rate Limiting
|
|
- [ ] `/login` → enter email (attempt 1)
|
|
- [ ] `/cancel`
|
|
- [ ] `/login` → enter email (attempt 2)
|
|
- [ ] `/cancel`
|
|
- [ ] `/login` → enter email (attempt 3)
|
|
- [ ] `/cancel`
|
|
- [ ] `/login` → enter email (attempt 4)
|
|
- [ ] Verify error: "Prea multe încercări. Încearcă în 60 minute"
|
|
|
|
### 7. Password Message Deletion
|
|
- [ ] `/login` → enter email → enter code
|
|
- [ ] Enter password
|
|
- [ ] Verify password message is deleted immediately
|
|
- [ ] Check chat history - password should not be visible
|
|
|
|
### 8. Cancel Flow
|
|
- [ ] `/login`
|
|
- [ ] `/cancel` (should cancel and clear state)
|
|
|
|
### 9. Already Authenticated
|
|
- [ ] Complete successful login
|
|
- [ ] Type `/login` again
|
|
- [ ] Verify message: "Ești deja autentificat"
|
|
|
|
### 10. Parallel Method (Web App Linking) Still Works
|
|
- [ ] Login to web app: http://localhost:3000
|
|
- [ ] Generate Telegram linking code
|
|
- [ ] Use `/start ABC123XY` in Telegram
|
|
- [ ] Verify linking works as before (OLD method)
|
|
|
|
### 11. Database Cleanup
|
|
- [ ] Create expired codes (manually set expires_at in past)
|
|
- [ ] Wait for hourly cleanup job
|
|
- [ ] Verify expired codes deleted from `email_auth_codes` table
|
|
```
|
|
|
|
---
|
|
|
|
## 📊 Estimare Efort & Timeline
|
|
|
|
### Breakdown Detaliat
|
|
|
|
| Componentă | Efort (ore) | Detalii |
|
|
|-----------|-------------|---------|
|
|
| **Backend** | | |
|
|
| → Endpoint `/login-with-email` | 1.5h | Logic + Oracle queries + JWT |
|
|
| → Schema Pydantic | 0.5h | Request/Response models |
|
|
| → Rate limiting | 0.5h | Middleware sau decorator |
|
|
| → Testing backend | 0.5h | Unit tests endpoint |
|
|
| **Telegram Bot** | | |
|
|
| → Email service (SMTP) | 1.5h | Client SMTP + templates + retry |
|
|
| → DB schema + operations | 1h | Tabela nouă + CRUD |
|
|
| → Email auth logic | 1h | Verificare email, cod generation |
|
|
| → ConversationHandler | 2h | States + handlers + callbacks |
|
|
| → API client method | 0.5h | Method nou în BackendAPIClient |
|
|
| → Testing bot | 1h | Unit + integration tests |
|
|
| **Integration** | | |
|
|
| → End-to-end testing | 1h | Flow complet manual |
|
|
| → Bug fixes | 1h | Edge cases |
|
|
| **Documentation** | | |
|
|
| → Update README | 0.5h | Secțiune nouă pentru email auth |
|
|
| → Update TELEGRAM_COMMANDS.md | 0.5h | Documentare `/login` |
|
|
| **TOTAL** | **~13h** | ~2 zile lucru (6-7h/zi) |
|
|
|
|
### Timeline Recomandat
|
|
|
|
**Ziua 1 (6-7h):**
|
|
1. Setup environment (SMTP config) - 0.5h
|
|
2. Backend: Endpoint + schema - 2h
|
|
3. Telegram Bot: Email service - 1.5h
|
|
4. Telegram Bot: DB schema + operations - 1h
|
|
5. Testing preliminar - 1h
|
|
|
|
**Ziua 2 (6-7h):**
|
|
1. Telegram Bot: ConversationHandler - 2h
|
|
2. Email auth logic - 1h
|
|
3. Integration - 1h
|
|
4. End-to-end testing - 1.5h
|
|
5. Documentation - 0.5h
|
|
6. Bug fixes - 1h
|
|
|
|
---
|
|
|
|
## ✅ Avantaje Plan
|
|
|
|
| Avantaj | Detalii |
|
|
|---------|---------|
|
|
| ✅ **Minimal Invaziv** | 0 modificări la logica existentă, doar adăugiri |
|
|
| ✅ **Ambele Metode** | Users pot alege: email+parolă SAU web app linking |
|
|
| ✅ **Backward Compatible** | Utilizatori fără email folosesc metoda actuală |
|
|
| ✅ **2FA Real** | Email possession (cod) + parolă Oracle = 2 factori |
|
|
| ✅ **Simplu de Testat** | Un singur utilizator cu email (mmarius28@gmail.com) |
|
|
| ✅ **Extensibil** | Ușor de activat pentru alți useri când adaugi email-uri |
|
|
| ✅ **Securitate Mărite** | Rate limiting, code expiry, password verification în Oracle |
|
|
| ✅ **Auto-Cleanup** | Job existent curăță automat coduri expirate |
|
|
| ✅ **User-Friendly** | Conversation flow natural în Telegram |
|
|
| ✅ **Error Handling** | Mesaje clare pentru fiecare caz de eroare |
|
|
|
|
---
|
|
|
|
## 🚀 Ordine Implementare (Step-by-Step)
|
|
|
|
### **Step 1: Environment Setup** (15 min)
|
|
|
|
1. Adaugă SMTP credentials în `.env`:
|
|
```bash
|
|
cd reports-app/telegram-bot/
|
|
echo "SMTP_HOST=mail.romfast.ro" >> .env
|
|
echo "SMTP_PORT=587" >> .env
|
|
echo "SMTP_USER=ups@romfast.ro" >> .env
|
|
echo "SMTP_PASSWORD=#Ups2020#" >> .env
|
|
echo "SMTP_FROM_EMAIL=ups@romfast.ro" >> .env
|
|
echo "SMTP_FROM_NAME=ROA2WEB" >> .env
|
|
```
|
|
|
|
2. Install dependencies:
|
|
```bash
|
|
pip install aiosmtplib # Async SMTP client
|
|
```
|
|
|
|
### **Step 2: Backend - Endpoint** (2h)
|
|
|
|
1. Create schema: `backend/app/schemas/telegram_email_auth.py`
|
|
2. Add endpoint în `backend/app/routers/telegram.py`
|
|
3. Test cu Postman/curl
|
|
|
|
### **Step 3: Telegram Bot - Infrastructure** (2.5h)
|
|
|
|
1. Create `app/utils/email_service.py` (SMTP client)
|
|
2. Update `app/db/database.py` (add `email_auth_codes` table)
|
|
3. Update `app/db/operations.py` (CRUD operations)
|
|
4. Test DB operations
|
|
|
|
### **Step 4: Telegram Bot - Auth Logic** (1h)
|
|
|
|
1. Create `app/auth/email_auth.py`
|
|
2. Implement: `verify_email_in_oracle()`, `generate_6digit_code()`, `check_rate_limit()`
|
|
3. Unit tests
|
|
|
|
### **Step 5: Telegram Bot - Handlers** (2h)
|
|
|
|
1. Create `app/bot/email_handlers.py`
|
|
2. Implement ConversationHandler
|
|
3. Register în `app/bot/handlers.py`
|
|
|
|
### **Step 6: Backend API Client** (30 min)
|
|
|
|
1. Add `login_with_email()` method în `app/api/client.py`
|
|
|
|
### **Step 7: Integration Testing** (1h)
|
|
|
|
1. Start backend + bot
|
|
2. Test flow complet cu mmarius28@gmail.com
|
|
3. Fix bugs
|
|
|
|
### **Step 8: Comprehensive Testing** (1.5h)
|
|
|
|
1. Run all test cases from checklist
|
|
2. Test edge cases
|
|
3. Verify both methods work in parallel
|
|
|
|
### **Step 9: Documentation** (30 min)
|
|
|
|
1. Update README.md
|
|
2. Update TELEGRAM_COMMANDS.md
|
|
3. Create MANUAL_EMAIL_AUTH_TESTING.md
|
|
|
|
---
|
|
|
|
## 🐛 Troubleshooting Guide
|
|
|
|
### Issue: Email nu ajunge
|
|
|
|
**Cauze posibile:**
|
|
1. SMTP credentials incorecte
|
|
2. Port blocat (587)
|
|
3. Email în spam
|
|
|
|
**Debugging:**
|
|
```python
|
|
# Test SMTP connection
|
|
import smtplib
|
|
|
|
try:
|
|
server = smtplib.SMTP('mail.romfast.ro', 587)
|
|
server.starttls()
|
|
server.login('ups@romfast.ro', '#Ups2020#')
|
|
print("✅ SMTP connection OK")
|
|
server.quit()
|
|
except Exception as e:
|
|
print(f"❌ SMTP error: {e}")
|
|
```
|
|
|
|
### Issue: Cod invalid/expirat
|
|
|
|
**Debugging:**
|
|
```sql
|
|
-- Check code in DB
|
|
SELECT * FROM email_auth_codes WHERE code = '123456';
|
|
|
|
-- Check expiry
|
|
SELECT code, expires_at,
|
|
datetime('now') as now,
|
|
(julianday(expires_at) - julianday('now')) * 24 * 60 as minutes_remaining
|
|
FROM email_auth_codes
|
|
WHERE code = '123456';
|
|
```
|
|
|
|
### Issue: Parolă incorectă (dar știu că e corectă)
|
|
|
|
**Cauze posibile:**
|
|
1. Username găsit greșit din email
|
|
2. Oracle stored procedure error
|
|
|
|
**Debugging:**
|
|
```python
|
|
# Test Oracle auth direct
|
|
async with oracle_pool.get_connection() as connection:
|
|
cursor = connection.cursor()
|
|
|
|
# 1. Check email → username mapping
|
|
cursor.execute("""
|
|
SELECT UTILIZATOR, EMAIL
|
|
FROM CONTAFIN_ORACLE.UTILIZATORI
|
|
WHERE UPPER(EMAIL) = UPPER('mmarius28@gmail.com')
|
|
""")
|
|
print(cursor.fetchone())
|
|
|
|
# 2. Test stored procedure
|
|
cursor.execute("""
|
|
SELECT pack_drepturi.verificautilizator('marius_user', 'parola_mea')
|
|
FROM DUAL
|
|
""")
|
|
result = cursor.fetchone()[0]
|
|
print(f"Auth result: {result}") # -1 = fail, user_id = success
|
|
```
|
|
|
|
### Issue: Rate limiting prea restrictiv
|
|
|
|
**Quick fix:**
|
|
```python
|
|
# În email_auth.py, increase limits
|
|
MAX_EMAIL_ATTEMPTS = 5 # was 3
|
|
RATE_LIMIT_WINDOW_MINUTES = 30 # was 60
|
|
```
|
|
|
|
---
|
|
|
|
## 📚 Additional Documentation
|
|
|
|
### Email Template (HTML)
|
|
|
|
```html
|
|
<!DOCTYPE html>
|
|
<html>
|
|
<head>
|
|
<style>
|
|
body { font-family: Arial, sans-serif; line-height: 1.6; color: #333; }
|
|
.container { max-width: 600px; margin: 0 auto; padding: 20px; }
|
|
.header { background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); color: white; padding: 30px; text-align: center; border-radius: 10px 10px 0 0; }
|
|
.code-box { background-color: #f4f4f4; border: 2px solid #667eea; padding: 30px; margin: 20px 0; text-align: center; border-radius: 10px; }
|
|
.code { font-size: 36px; font-weight: bold; letter-spacing: 10px; color: #667eea; font-family: 'Courier New', monospace; }
|
|
.content { padding: 20px; background: white; }
|
|
.warning { background-color: #fff3cd; border-left: 4px solid #ffc107; padding: 15px; margin: 20px 0; }
|
|
.footer { text-align: center; color: #666; font-size: 12px; padding: 20px; border-top: 1px solid #ddd; }
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<div class="container">
|
|
<div class="header">
|
|
<h1>🔐 ROA2WEB</h1>
|
|
<p>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;">Codul tău de autentificare:</p>
|
|
<p class="code">{{code}}</p>
|
|
</div>
|
|
|
|
<div class="warning">
|
|
<strong>⏱️ Important:</strong> Acest cod expiră în <strong>5 minute</strong>.
|
|
</div>
|
|
|
|
<p>Introdu acest cod în conversația cu Telegram Bot, apoi vei fi solicitat să introduci parola.</p>
|
|
|
|
<p style="margin-top: 30px; padding-top: 20px; border-top: 1px solid #ddd;">
|
|
<strong>Nu ai solicitat acest cod?</strong><br>
|
|
Dacă nu ai inițiat această autentificare, te rugăm să ignori acest email.
|
|
Nimeni nu va avea acces la contul tău fără parola ta.
|
|
</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>
|
|
</div>
|
|
</div>
|
|
</body>
|
|
</html>
|
|
```
|
|
|
|
### Command Reference Update
|
|
|
|
Adaugă în `TELEGRAM_COMMANDS.md`:
|
|
|
|
```markdown
|
|
## /login
|
|
|
|
**Descriere**: Autentificare în Telegram Bot prin email + parolă (2FA)
|
|
|
|
**Sintaxă**: `/login`
|
|
|
|
**Flow**:
|
|
1. Selectezi metoda de autentificare (Email sau Web App)
|
|
2. Pentru Email:
|
|
- Introduci adresa de email Oracle
|
|
- Primești cod pe email (6 cifre)
|
|
- Introduci codul din email
|
|
- Introduci parola Oracle
|
|
- Primești confirmare și acces la comenzi
|
|
|
|
**Exemple**:
|
|
```
|
|
/login
|
|
→ Selectează "🔐 Login cu Email"
|
|
→ Introdu: mmarius28@gmail.com
|
|
→ Verifică email și introdu cod: 123456
|
|
→ Introdu parola
|
|
→ ✅ Autentificat cu succes!
|
|
```
|
|
|
|
**Securitate**:
|
|
- Cod expiră în 5 minute
|
|
- Cod utilizabil o singură dată
|
|
- Parolă verificată în Oracle
|
|
- Mesajul cu parola este șters automat
|
|
- Rate limiting: max 3 încercări/oră
|
|
|
|
**Comandă alternativă**: Buton "🔐 Login cu Email" în meniu principal
|
|
```
|
|
|
|
---
|
|
|
|
## 🎓 Learning Points & Best Practices
|
|
|
|
### 1. De ce 6-digit code în loc de 8-char alphanumeric?
|
|
|
|
**Răspuns**:
|
|
- Mai ușor de tastat pe mobile
|
|
- Mai puțin prone la erori (0/O, 1/I confusion)
|
|
- Suficient de securizat pentru 5 minute expiry
|
|
- Standard în industrie (Google, GitHub, etc.)
|
|
|
|
### 2. De ce 5 minute expiry pentru cod?
|
|
|
|
**Răspuns**:
|
|
- Balans între security și UX
|
|
- Suficient timp pentru check email + type code
|
|
- Redus attack window pentru code guessing
|
|
- Similar cu alte aplicații 2FA
|
|
|
|
### 3. De ce șterge mesajul cu parola?
|
|
|
|
**Răspuns**:
|
|
- Securitate: parola nu rămâne în chat history
|
|
- Telegram API permite ștergerea mesajelor
|
|
- Best practice în bot-uri care cer credențiale
|
|
- Protecție dacă telefonul e compromis
|
|
|
|
### 4. De ce în-memory rate limiting și nu Redis?
|
|
|
|
**Răspuns**:
|
|
- **Simplitate**: Nu adaugă dependency nou
|
|
- **Suficient pentru scale mic-mediu**:
|
|
- In-memory e OK pentru <1000 users concurrent
|
|
- Planul curent: test cu 1 user, extend la 10-20 max
|
|
- **Upgrade path clar**: Dacă scală, migrăm la Redis
|
|
- **Code unchanged**: Interfața rămâne aceeași
|
|
|
|
### 5. De ce SQLite și nu PostgreSQL pentru bot DB?
|
|
|
|
**Răspuns**:
|
|
- **Consistency**: Sistemul actual folosește SQLite
|
|
- **Zero infrastructure**: No server setup needed
|
|
- **Sufficient performance**: Telegram bot = low traffic
|
|
- **Easy backup**: Single file (`telegram_bot.db`)
|
|
- **Deployment simplicity**: Works în Windows Service
|
|
|
|
---
|
|
|
|
## 🔗 Integrare cu Sistemul Existent
|
|
|
|
### Flow Diagram - Ambele Metode
|
|
|
|
```
|
|
┌─────────────────────────────────┐
|
|
│ USER VREA SĂ SE AUTENTIFICE │
|
|
│ ÎN TELEGRAM BOT │
|
|
└─────────────┬───────────────────┘
|
|
│
|
|
▼
|
|
┌─────────────────────────────────┐
|
|
│ ALEGE METODA: │
|
|
│ [Email + Parolă] sau [Web App] │
|
|
└─────────┬───────────┬───────────┘
|
|
│ │
|
|
┌────────────┘ └────────────┐
|
|
│ │
|
|
▼ ▼
|
|
┌────────────────────────┐ ┌──────────────────────────┐
|
|
│ METODA NOUĂ │ │ METODA EXISTENTĂ │
|
|
│ Email + Parolă │ │ Web App Linking │
|
|
└────────────────────────┘ └──────────────────────────┘
|
|
│ │
|
|
▼ ▼
|
|
1. /login în Telegram 1. Login în web app (username+pass)
|
|
2. Introdu email 2. Click "Link Telegram"
|
|
3. Primești cod pe email 3. Backend generează cod 8-char
|
|
4. Introdu cod în Telegram 4. Backend → POST /internal/save-code → Bot
|
|
5. Introdu parolă 5. User: /start ABC123XY
|
|
6. Bot verifică în Oracle 6. Bot auto-link (fără parolă din nou)
|
|
│ │
|
|
└──────────────┬──────────────────────┘
|
|
│
|
|
▼
|
|
┌───────────────────────────────────┐
|
|
│ USER AUTENTIFICAT ÎN BOT │
|
|
│ JWT tokens saved în telegram_users│
|
|
└───────────────────────────────────┘
|
|
│
|
|
▼
|
|
┌───────────────────────────────────┐
|
|
│ COMENZI DISPONIBILE: │
|
|
│ /companies, /dashboard, │
|
|
│ /facturi, /trezorerie, etc. │
|
|
└───────────────────────────────────┘
|
|
```
|
|
|
|
### Database Schema - Complete View
|
|
|
|
```sql
|
|
-- EXISTING TABLE (no changes)
|
|
CREATE TABLE telegram_users (
|
|
telegram_user_id INTEGER PRIMARY KEY,
|
|
username TEXT,
|
|
first_name TEXT,
|
|
last_name TEXT,
|
|
oracle_username TEXT,
|
|
jwt_token TEXT,
|
|
jwt_refresh_token TEXT,
|
|
token_expires_at TIMESTAMP,
|
|
linked_at TIMESTAMP,
|
|
last_active_at TIMESTAMP,
|
|
is_active BOOLEAN
|
|
);
|
|
|
|
-- EXISTING TABLE (no changes)
|
|
CREATE TABLE telegram_auth_codes (
|
|
code TEXT PRIMARY KEY, -- 8-char code from web app
|
|
telegram_user_id INTEGER,
|
|
oracle_username TEXT,
|
|
created_at TIMESTAMP,
|
|
expires_at TIMESTAMP,
|
|
used BOOLEAN DEFAULT 0,
|
|
used_at TIMESTAMP
|
|
);
|
|
|
|
-- NEW TABLE (for email auth)
|
|
CREATE TABLE email_auth_codes (
|
|
code TEXT PRIMARY KEY, -- 6-digit code sent via email
|
|
email TEXT NOT NULL,
|
|
oracle_username TEXT NOT NULL,
|
|
telegram_user_id INTEGER NOT NULL,
|
|
created_at TIMESTAMP,
|
|
expires_at TIMESTAMP,
|
|
used BOOLEAN DEFAULT 0,
|
|
used_at TIMESTAMP,
|
|
|
|
INDEX idx_email (email),
|
|
INDEX idx_telegram_user_id (telegram_user_id)
|
|
);
|
|
|
|
-- EXISTING TABLE (no changes)
|
|
CREATE TABLE telegram_sessions (
|
|
session_id TEXT PRIMARY KEY,
|
|
telegram_user_id INTEGER,
|
|
active_company_id INTEGER,
|
|
active_company_name TEXT,
|
|
conversation_state TEXT,
|
|
created_at TIMESTAMP,
|
|
updated_at TIMESTAMP,
|
|
expires_at TIMESTAMP
|
|
);
|
|
```
|
|
|
|
**Observații:**
|
|
- `telegram_auth_codes` = pentru web app linking (EXISTING)
|
|
- `email_auth_codes` = pentru email auth (NEW)
|
|
- Ambele tabele coexistă, fiecare pentru flow-ul propriu
|
|
- `telegram_users` = comună pentru ambele metode (linked users)
|
|
|
|
---
|
|
|
|
## 🚦 Success Criteria
|
|
|
|
### Implementarea e considerată SUCCESS dacă:
|
|
|
|
- [x] ✅ User poate să facă `/login` în Telegram
|
|
- [x] ✅ User poate introduce email și primește cod pe email
|
|
- [x] ✅ User poate introduce cod din email (valid 5 min)
|
|
- [x] ✅ User poate introduce parola Oracle
|
|
- [x] ✅ Bot verifică credențialele în Oracle și generează JWT
|
|
- [x] ✅ User e autentificat și poate folosi comenzi (/companies, /dashboard, etc.)
|
|
- [x] ✅ Metoda veche (web app → /start cod) funcționează în continuare
|
|
- [x] ✅ Ambele metode pot fi folosite de același user (switch între ele)
|
|
- [x] ✅ Codurile expirate sunt auto-șterse
|
|
- [x] ✅ Mesajul cu parola e șters automat din chat
|
|
- [x] ✅ Rate limiting funcționează (max 3 emails/oră)
|
|
- [x] ✅ Error handling: mesaje clare pentru fiecare caz de eroare
|
|
- [x] ✅ Testare cu mmarius28@gmail.com funcționează end-to-end
|
|
|
|
---
|
|
|
|
## 📅 Next Steps După Implementare
|
|
|
|
### Immediate (după deploy)
|
|
|
|
1. **Testare cu user real (Marius)**
|
|
- Verifică flow complet cu mmarius28@gmail.com
|
|
- Test ambele metode de autentificare
|
|
- Validează email delivery și formatting
|
|
|
|
2. **Monitoring**
|
|
- Watch logs pentru erori SMTP
|
|
- Monitor rate limiting triggers
|
|
- Check database growth (`email_auth_codes` table)
|
|
|
|
### Short-term (1-2 săptămâni)
|
|
|
|
1. **Adaugă email pentru alți useri**
|
|
- Identifică 5-10 useri pentru beta testing
|
|
- Adaugă email-uri în Oracle `UTILIZATORI` table
|
|
- Anunță disponibilitatea noii metode
|
|
|
|
2. **Collect feedback**
|
|
- UX pentru conversation flow
|
|
- Timing email delivery (sunt 5 min suficienți?)
|
|
- Edge cases descoperite
|
|
|
|
### Long-term (1-3 luni)
|
|
|
|
1. **Analytics**
|
|
- Câți useri folosesc fiecare metodă?
|
|
- Success rate pentru fiecare flow
|
|
- Common error patterns
|
|
|
|
2. **Optimizări**
|
|
- Migrate rate limiting la Redis (dacă scală)
|
|
- Adaugă email templates multiple (română + engleză)
|
|
- Consider migration de la SQLite la PostgreSQL (dacă >1000 users)
|
|
|
|
3. **Feature extensions**
|
|
- Email verification pentru schimbare parolă
|
|
- Email notifications pentru events (reports ready, etc.)
|
|
- Multi-language support
|
|
|
|
---
|
|
|
|
## 📞 Contact & Support
|
|
|
|
**Developer**: Claude Code (Anthropic)
|
|
**Project**: ROA2WEB - ERP Reports Application
|
|
**Date**: 2025-11-07
|
|
|
|
**Pentru întrebări despre acest plan:**
|
|
- Review cod în `reports-app/telegram-bot/` și `reports-app/backend/`
|
|
- Consultă `CLAUDE.md` pentru context general
|
|
- Check `TELEGRAM_COMMANDS.md` pentru comenzi existente
|
|
|
|
**Resurse utile:**
|
|
- python-telegram-bot docs: https://docs.python-telegram-bot.org/
|
|
- FastAPI docs: https://fastapi.tiangolo.com/
|
|
- aiosmtplib docs: https://aiosmtplib.readthedocs.io/
|
|
|
|
---
|
|
|
|
## 🎉 Conclusion
|
|
|
|
Acest plan implementează autentificare **email + parolă (2FA)** pentru Telegram Bot în mod **minimal invaziv**, păstrând întreaga funcționalitate existentă.
|
|
|
|
**Key highlights:**
|
|
- ✅ **0 breaking changes** - metoda veche funcționează în continuare
|
|
- ✅ **Simplu de implementat** - ~13h efort total
|
|
- ✅ **Securizat** - 2FA real cu rate limiting
|
|
- ✅ **Extensibil** - ușor de scalat pentru mai mulți useri
|
|
- ✅ **User-friendly** - conversation flow natural
|
|
|
|
**Ready to implement!** 🚀
|
|
|
|
---
|
|
|
|
*Document generat: 2025-11-07*
|
|
*Versiune: 1.0*
|
|
*Status: Planning Phase - Ready for Implementation*
|