- Add A-Z alphabetical filter keyboard for clients and suppliers lists (same pattern as company selection, without emoji) - Increase clients/suppliers list pagination from 10 to 20 items per page - Remove emoji from company A-Z filter button for consistency - Add 6 new callback handlers: clients_alpha_menu, clients_alpha:LETTER, clients_alpha_page:PAGE:LETTER, and supplier equivalents - Dashboard service and models updates - Telegram bot: email handlers, auth, DB operations, internal API improvements - Frontend: dashboard cards updates (CashFlow, Clienti, Furnizori, Treasury) - Frontend: SolduriCompactCard and CollapsibleCard improvements - DashboardView enhancements - start.sh and run-with-restart.sh script updates - IIS web.config and service worker updates Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
857 lines
29 KiB
Python
857 lines
29 KiB
Python
"""
|
|
Telegram bot handlers for email-based authentication flow
|
|
"""
|
|
from telegram import Update, InlineKeyboardButton, InlineKeyboardMarkup
|
|
from telegram.ext import (
|
|
ContextTypes,
|
|
ConversationHandler,
|
|
CommandHandler,
|
|
MessageHandler,
|
|
CallbackQueryHandler,
|
|
filters
|
|
)
|
|
import logging
|
|
from datetime import datetime
|
|
import asyncio
|
|
|
|
from typing import Optional
|
|
|
|
from backend.modules.telegram.auth.email_auth import (
|
|
is_valid_email_format,
|
|
verify_email_in_oracle,
|
|
generate_email_code,
|
|
generate_session_token,
|
|
check_rate_limit,
|
|
clear_rate_limit
|
|
)
|
|
from shared.auth.email_server_cache import email_server_cache
|
|
from backend.modules.telegram.utils.email_service import get_email_service
|
|
from backend.modules.telegram.db.operations import (
|
|
create_email_auth_code,
|
|
get_email_auth_code,
|
|
get_pending_email_code,
|
|
mark_email_code_used,
|
|
increment_failed_attempts,
|
|
delete_user_email_codes,
|
|
is_user_authenticated,
|
|
link_user_to_oracle,
|
|
create_or_update_user
|
|
)
|
|
from backend.modules.telegram.api.client import get_backend_client
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
# Conversation states
|
|
AWAITING_EMAIL, AWAITING_CODE, AWAITING_PASSWORD, AWAITING_SERVER_SELECTION = range(4)
|
|
|
|
# Constants
|
|
MAX_CODE_ATTEMPTS = 3
|
|
|
|
|
|
# ============================================================================
|
|
# HELPER FUNCTIONS
|
|
# ============================================================================
|
|
|
|
async def edit_login_message(
|
|
context: ContextTypes.DEFAULT_TYPE,
|
|
chat_id: int,
|
|
text: str,
|
|
reply_markup=None,
|
|
parse_mode="Markdown"
|
|
):
|
|
"""
|
|
Helper function to edit the login message stored in context.
|
|
If message_id is not stored, creates a new message instead.
|
|
"""
|
|
message_id = context.user_data.get('login_message_id')
|
|
|
|
if message_id:
|
|
try:
|
|
await context.bot.edit_message_text(
|
|
chat_id=chat_id,
|
|
message_id=message_id,
|
|
text=text,
|
|
reply_markup=reply_markup,
|
|
parse_mode=parse_mode
|
|
)
|
|
except Exception as e:
|
|
logger.warning(f"Could not edit message {message_id}: {e}")
|
|
# Fallback: send new message and update ID
|
|
msg = await context.bot.send_message(
|
|
chat_id=chat_id,
|
|
text=text,
|
|
reply_markup=reply_markup,
|
|
parse_mode=parse_mode
|
|
)
|
|
context.user_data['login_message_id'] = msg.message_id
|
|
else:
|
|
# No message ID stored - create new message
|
|
msg = await context.bot.send_message(
|
|
chat_id=chat_id,
|
|
text=text,
|
|
reply_markup=reply_markup,
|
|
parse_mode=parse_mode
|
|
)
|
|
context.user_data['login_message_id'] = msg.message_id
|
|
|
|
|
|
async def delete_login_message(context: ContextTypes.DEFAULT_TYPE, chat_id: int):
|
|
"""Delete the login message and clear the message_id from context"""
|
|
message_id = context.user_data.get('login_message_id')
|
|
|
|
if message_id:
|
|
try:
|
|
await context.bot.delete_message(chat_id=chat_id, message_id=message_id)
|
|
except Exception as e:
|
|
logger.warning(f"Could not delete message {message_id}: {e}")
|
|
|
|
# Clear from context
|
|
context.user_data.pop('login_message_id', None)
|
|
|
|
|
|
# ============================================================================
|
|
# ENTRY POINTS: /login command and action:login button
|
|
# ============================================================================
|
|
|
|
async def login_command(update: Update, context: ContextTypes.DEFAULT_TYPE):
|
|
"""
|
|
Handler pentru /login command
|
|
Oferă opțiuni de autentificare: Email sau Web App
|
|
"""
|
|
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\n"
|
|
"Folosește:\n"
|
|
"• /companies - Vezi companiile tale\n"
|
|
"• /help - Comenzi disponibile\n"
|
|
"• /unlink - Deautentifică-te"
|
|
)
|
|
return ConversationHandler.END
|
|
|
|
# Check rate limiting (3 requests per hour)
|
|
if not await check_rate_limit(f"login_{user.id}", max_attempts=3, window_minutes=60):
|
|
await update.message.reply_text(
|
|
"Prea multe încercări de autentificare.\n\n"
|
|
"Te rugăm să aștepți 60 de minute înainte de a încerca din nou."
|
|
)
|
|
return ConversationHandler.END
|
|
|
|
# Afișează opțiuni de autentificare
|
|
keyboard = [
|
|
[InlineKeyboardButton("Login cu Email + Parolă", callback_data="email_login")],
|
|
[InlineKeyboardButton("Login din Web App", callback_data="web_login_info")],
|
|
[InlineKeyboardButton("Anulează", callback_data="cancel")]
|
|
]
|
|
reply_markup = InlineKeyboardMarkup(keyboard)
|
|
|
|
# CREATE message and SAVE message_id
|
|
msg = await update.message.reply_text(
|
|
"Alege metoda de autentificare:",
|
|
reply_markup=reply_markup,
|
|
parse_mode="Markdown"
|
|
)
|
|
|
|
# Save message ID for future edits
|
|
context.user_data['login_message_id'] = msg.message_id
|
|
|
|
return AWAITING_EMAIL
|
|
|
|
|
|
async def action_login_callback(update: Update, context: ContextTypes.DEFAULT_TYPE):
|
|
"""
|
|
Handler pentru butonul Login din meniu (action:login)
|
|
Oferă opțiuni de autentificare: Email sau Web App
|
|
"""
|
|
query = update.callback_query
|
|
user = update.effective_user
|
|
|
|
logger.info(f"[EMAIL_AUTH] action_login_callback triggered for user {user.id}")
|
|
|
|
await query.answer()
|
|
|
|
# Check dacă e deja autentificat
|
|
if await is_user_authenticated(user.id):
|
|
await query.edit_message_text(
|
|
"Ești deja autentificat.\n\n"
|
|
"Folosește:\n"
|
|
"• /companies - Vezi companiile tale\n"
|
|
"• /help - Comenzi disponibile\n"
|
|
"• /unlink - Deautentifică-te"
|
|
)
|
|
return ConversationHandler.END
|
|
|
|
# Check rate limiting (3 requests per hour)
|
|
if not await check_rate_limit(f"login_{user.id}", max_attempts=3, window_minutes=60):
|
|
await query.edit_message_text(
|
|
"Prea multe încercări de autentificare.\n\n"
|
|
"Te rugăm să aștepți 60 de minute înainte de a încerca din nou."
|
|
)
|
|
return ConversationHandler.END
|
|
|
|
# Afișează opțiuni de autentificare
|
|
keyboard = [
|
|
[InlineKeyboardButton("Login cu Email + Parolă", callback_data="email_login")],
|
|
[InlineKeyboardButton("Login din Web App", callback_data="web_login_info")],
|
|
[InlineKeyboardButton("Anulează", callback_data="cancel")]
|
|
]
|
|
reply_markup = InlineKeyboardMarkup(keyboard)
|
|
|
|
# EDIT existing menu message and SAVE message_id
|
|
await query.edit_message_text(
|
|
"Alege metoda de autentificare:",
|
|
reply_markup=reply_markup,
|
|
parse_mode="Markdown"
|
|
)
|
|
|
|
# Save message ID for future edits
|
|
context.user_data['login_message_id'] = query.message.message_id
|
|
|
|
return AWAITING_EMAIL
|
|
|
|
|
|
# ============================================================================
|
|
# CALLBACK: Email Login
|
|
# ============================================================================
|
|
|
|
async def email_login_callback(update: Update, context: ContextTypes.DEFAULT_TYPE):
|
|
"""Callback pentru butonul 'Login cu Email'"""
|
|
query = update.callback_query
|
|
user = update.effective_user
|
|
|
|
logger.info(f"[EMAIL_AUTH] email_login_callback triggered for user {user.id}")
|
|
|
|
await query.answer()
|
|
|
|
# IMPORTANT: Salvează message_id înainte de a edita
|
|
context.user_data['login_message_id'] = query.message.message_id
|
|
|
|
# EDIT same message - remove buttons, ask for email
|
|
await query.edit_message_text(
|
|
text="Introdu adresa de email ROA:",
|
|
parse_mode="Markdown"
|
|
)
|
|
|
|
return AWAITING_EMAIL
|
|
|
|
|
|
async def web_login_info_callback(update: Update, context: ContextTypes.DEFAULT_TYPE):
|
|
"""Info despre web app login"""
|
|
query = update.callback_query
|
|
await query.answer()
|
|
|
|
await query.edit_message_text(
|
|
"**Login din Web App**\n\n"
|
|
"Pentru această metodă:\n\n"
|
|
"1. Accesează aplicația web ROA2WEB\n"
|
|
"2. Autentifică-te cu username + parolă\n"
|
|
"3. Apasă butonul \"Link Telegram\"\n"
|
|
"4. Copiază codul generat (8 caractere)\n"
|
|
"5. Trimite-mi codul: /start ABC123XY\n\n"
|
|
"Vei fi autentificat automat.",
|
|
parse_mode="Markdown"
|
|
)
|
|
|
|
# IMPORTANT: Salvează message_id pentru ca /start să poată edita același mesaj
|
|
context.user_data['web_login_message_id'] = query.message.message_id
|
|
|
|
return ConversationHandler.END
|
|
|
|
|
|
# ============================================================================
|
|
# STATE: AWAITING_EMAIL
|
|
# ============================================================================
|
|
|
|
async def _send_email_code(
|
|
context: ContextTypes.DEFAULT_TYPE,
|
|
chat_id: int,
|
|
email: str,
|
|
server_id: Optional[str],
|
|
user_id: int
|
|
) -> int:
|
|
"""
|
|
Generate and send email verification code on the specified server.
|
|
Returns AWAITING_CODE on success or ConversationHandler.END on failure.
|
|
"""
|
|
try:
|
|
# Verify email in Oracle (on specific server if known)
|
|
username = await verify_email_in_oracle(email, server_id=server_id)
|
|
|
|
# IMPORTANT: Generic response to prevent email enumeration
|
|
# We always say "code sent" even if email doesn't exist
|
|
if username:
|
|
code = generate_email_code()
|
|
code_saved = await create_email_auth_code(
|
|
code=code,
|
|
email=email,
|
|
username=username,
|
|
telegram_user_id=user_id,
|
|
expiry_minutes=5
|
|
)
|
|
|
|
if not code_saved:
|
|
await edit_login_message(
|
|
context=context,
|
|
chat_id=chat_id,
|
|
text="Eroare la salvarea codului.\n\nIncearcă din nou cu /login"
|
|
)
|
|
return ConversationHandler.END
|
|
|
|
email_service = get_email_service()
|
|
email_sent = await email_service.send_auth_code(email, code, username)
|
|
|
|
if email_sent:
|
|
logger.info(f"[EMAIL-AUTH] ✅ Code sent for {email[:3]}***@*** (user {user_id}, server={server_id})")
|
|
else:
|
|
logger.error(f"[EMAIL-AUTH] ❌ Failed to send code (user {user_id}, server={server_id})")
|
|
|
|
# Wait 1 second for UX (looks like verification happened)
|
|
await asyncio.sleep(1)
|
|
|
|
# ALWAYS show success (prevent enumeration)
|
|
await edit_login_message(
|
|
context=context,
|
|
chat_id=chat_id,
|
|
text=f"Cod trimis pe {email}\n\nIntrodu codul primit pe email:",
|
|
reply_markup=InlineKeyboardMarkup([
|
|
[InlineKeyboardButton("Retrimite Cod", callback_data=f"resend:{email}")],
|
|
[InlineKeyboardButton("Anulează", callback_data="cancel")]
|
|
])
|
|
)
|
|
|
|
context.user_data['pending_email'] = email
|
|
context.user_data['pending_username'] = username
|
|
|
|
return AWAITING_CODE
|
|
|
|
except Exception as e:
|
|
logger.error(f"Error sending email code: {e}", exc_info=True)
|
|
await edit_login_message(
|
|
context=context,
|
|
chat_id=chat_id,
|
|
text="Eroare internă.\n\nIncearcă din nou mai târziu."
|
|
)
|
|
return ConversationHandler.END
|
|
|
|
|
|
async def receive_email(update: Update, context: ContextTypes.DEFAULT_TYPE):
|
|
"""Handler pentru primirea email-ului"""
|
|
email = update.message.text.strip().lower()
|
|
user_id = update.effective_user.id
|
|
chat_id = update.effective_chat.id
|
|
|
|
# ȘTERG mesajul utilizatorului imediat (chat curat)
|
|
try:
|
|
await update.message.delete()
|
|
except Exception as e:
|
|
logger.warning(f"Could not delete email message: {e}")
|
|
|
|
# Validare format email
|
|
if not is_valid_email_format(email):
|
|
await edit_login_message(
|
|
context=context,
|
|
chat_id=chat_id,
|
|
text="Email invalid\n\nIntrodu o adresă validă (nume@domeniu.ro)",
|
|
reply_markup=InlineKeyboardMarkup([
|
|
[InlineKeyboardButton("Anulează", callback_data="cancel")]
|
|
])
|
|
)
|
|
return AWAITING_EMAIL
|
|
|
|
# Clean up old pending codes
|
|
existing_code = await get_pending_email_code(user_id)
|
|
if existing_code:
|
|
await delete_user_email_codes(user_id)
|
|
logger.info(f"Deleted existing pending code for user {user_id}")
|
|
|
|
# Show loading
|
|
await edit_login_message(
|
|
context=context,
|
|
chat_id=chat_id,
|
|
text="Verificare email...",
|
|
reply_markup=None
|
|
)
|
|
|
|
# Check server cache for multi-server routing
|
|
try:
|
|
await email_server_cache.refresh_if_needed()
|
|
servers = email_server_cache.get_servers_for_email(email)
|
|
except Exception as e:
|
|
logger.warning(f"Could not check email server cache: {e}")
|
|
servers = []
|
|
|
|
if len(servers) > 1:
|
|
# Multiple servers — ask user to select before sending code
|
|
context.user_data['pending_email'] = email
|
|
|
|
try:
|
|
from backend.config import settings
|
|
keyboard = []
|
|
for srv_id in servers:
|
|
srv = settings.get_oracle_server(srv_id)
|
|
srv_name = srv.name if srv else srv_id
|
|
keyboard.append([InlineKeyboardButton(srv_name, callback_data=f"select_server:{srv_id}")])
|
|
except Exception:
|
|
# Fallback: use server IDs as labels
|
|
keyboard = [
|
|
[InlineKeyboardButton(srv_id, callback_data=f"select_server:{srv_id}")]
|
|
for srv_id in servers
|
|
]
|
|
keyboard.append([InlineKeyboardButton("Anulează", callback_data="cancel")])
|
|
|
|
await edit_login_message(
|
|
context=context,
|
|
chat_id=chat_id,
|
|
text="Email identificat pe mai multe servere.\n\nSelectează serverul pentru autentificare:",
|
|
reply_markup=InlineKeyboardMarkup(keyboard)
|
|
)
|
|
return AWAITING_SERVER_SELECTION
|
|
|
|
# Single server or no cache hit — proceed directly
|
|
server_id = servers[0] if servers else None
|
|
context.user_data['server_id'] = server_id
|
|
|
|
return await _send_email_code(context, chat_id, email, server_id, user_id)
|
|
|
|
|
|
# ============================================================================
|
|
# STATE: AWAITING_SERVER_SELECTION
|
|
# ============================================================================
|
|
|
|
async def handle_server_selected(update: Update, context: ContextTypes.DEFAULT_TYPE):
|
|
"""Callback pentru selectarea serverului Oracle (mod multi-server)"""
|
|
query = update.callback_query
|
|
user_id = update.effective_user.id
|
|
chat_id = update.effective_chat.id
|
|
|
|
await query.answer()
|
|
|
|
# Extract server_id from callback data: "select_server:<id>"
|
|
server_id = query.data.split(":", 1)[1]
|
|
|
|
email = context.user_data.get('pending_email')
|
|
if not email:
|
|
await edit_login_message(
|
|
context=context,
|
|
chat_id=chat_id,
|
|
text="Sesiune expirată\n\nIncearcă din nou cu /login"
|
|
)
|
|
return ConversationHandler.END
|
|
|
|
# Save selected server to context
|
|
context.user_data['server_id'] = server_id
|
|
logger.info(f"[EMAIL-AUTH] User {user_id} selected server '{server_id}' for {email[:3]}***")
|
|
|
|
# Clean up old pending codes then send code on selected server
|
|
await delete_user_email_codes(user_id)
|
|
|
|
return await _send_email_code(context, chat_id, email, server_id, user_id)
|
|
|
|
|
|
# ============================================================================
|
|
# STATE: AWAITING_CODE
|
|
# ============================================================================
|
|
|
|
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
|
|
|
|
# ȘTERG mesajul utilizatorului imediat (chat curat)
|
|
try:
|
|
await update.message.delete()
|
|
except Exception as e:
|
|
logger.warning(f"Could not delete code message: {e}")
|
|
|
|
# Validare format cod (6 digits)
|
|
if not (code.isdigit() and len(code) == 6):
|
|
await edit_login_message(
|
|
context=context,
|
|
chat_id=update.effective_chat.id,
|
|
text="Cod invalid\n\nIntrodu cele 6 cifre din email.",
|
|
reply_markup=InlineKeyboardMarkup([
|
|
[InlineKeyboardButton("Retrimite Cod", callback_data=f"resend:{context.user_data.get('pending_email', '')}")],
|
|
[InlineKeyboardButton("Anulează", callback_data="cancel")]
|
|
])
|
|
)
|
|
return AWAITING_CODE
|
|
|
|
# Verifică cod în DB
|
|
try:
|
|
code_data = await get_email_auth_code(code)
|
|
|
|
if not code_data:
|
|
# EDIT login message to show error
|
|
await edit_login_message(
|
|
context=context,
|
|
chat_id=update.effective_chat.id,
|
|
text="Cod invalid sau expirat\n\nIncearcă din nou cu /login"
|
|
)
|
|
return ConversationHandler.END
|
|
|
|
# Verificări de securitate
|
|
|
|
# 1. Check if already used
|
|
if code_data['used']:
|
|
await edit_login_message(
|
|
context=context,
|
|
chat_id=update.effective_chat.id,
|
|
text="Cod deja folosit\n\nIncearcă din nou cu /login"
|
|
)
|
|
return ConversationHandler.END
|
|
|
|
# 2. Check if expired
|
|
if datetime.now() > code_data['expires_at']:
|
|
await edit_login_message(
|
|
context=context,
|
|
chat_id=update.effective_chat.id,
|
|
text="Cod expirat\n\nIncearcă din nou cu /login"
|
|
)
|
|
return ConversationHandler.END
|
|
|
|
# 3. Check if belongs to this user
|
|
if code_data['telegram_user_id'] != user_id:
|
|
logger.warning(
|
|
f"User {user_id} tried to use code belonging to "
|
|
f"user {code_data['telegram_user_id']}"
|
|
)
|
|
await edit_login_message(
|
|
context=context,
|
|
chat_id=update.effective_chat.id,
|
|
text="Cod invalid"
|
|
)
|
|
return ConversationHandler.END
|
|
|
|
# 4. Check failed attempts (max 3)
|
|
if code_data['failed_attempts'] >= MAX_CODE_ATTEMPTS:
|
|
await edit_login_message(
|
|
context=context,
|
|
chat_id=update.effective_chat.id,
|
|
text="Prea multe încercări greșite\n\nIncearcă din nou cu /login"
|
|
)
|
|
return ConversationHandler.END
|
|
|
|
# Cod valid - Marchează ca folosit
|
|
await mark_email_code_used(code)
|
|
|
|
# Salvează date verificate în context
|
|
context.user_data['verified_username'] = code_data['oracle_username']
|
|
context.user_data['verified_email'] = code_data['email']
|
|
context.user_data['session_token'] = generate_session_token(
|
|
user_id,
|
|
code_data['email']
|
|
)
|
|
|
|
# EDIT same message - ask for password
|
|
await edit_login_message(
|
|
context=context,
|
|
chat_id=update.effective_chat.id,
|
|
text="Cod validat!\n\nIntroduci parola ROA:",
|
|
reply_markup=None # No buttons for security
|
|
)
|
|
|
|
return AWAITING_PASSWORD
|
|
|
|
except Exception as e:
|
|
logger.error(f"Error validating code: {e}", exc_info=True)
|
|
await edit_login_message(
|
|
context=context,
|
|
chat_id=update.effective_chat.id,
|
|
text="Eroare la validarea codului.\n\nIncearcă din nou."
|
|
)
|
|
return ConversationHandler.END
|
|
|
|
|
|
async def resend_code_callback(update: Update, context: ContextTypes.DEFAULT_TYPE):
|
|
"""Retrimite codul pe email"""
|
|
query = update.callback_query
|
|
await query.answer("Retrimitem codul...")
|
|
|
|
# Extract email from callback data
|
|
callback_data = query.data # Format: "resend:email@example.com"
|
|
if not callback_data.startswith("resend:"):
|
|
await edit_login_message(
|
|
context=context,
|
|
chat_id=update.effective_chat.id,
|
|
text="Eroare\n\nIncearcă din nou cu /login"
|
|
)
|
|
return ConversationHandler.END
|
|
|
|
email = callback_data.split(":", 1)[1]
|
|
user_id = update.effective_user.id
|
|
|
|
# Check rate limiting for resend (max 2 per 10 minutes)
|
|
if not await check_rate_limit(f"resend_{user_id}", max_attempts=2, window_minutes=10):
|
|
await edit_login_message(
|
|
context=context,
|
|
chat_id=update.effective_chat.id,
|
|
text="Prea multe solicitări\n\nAșteaptă 10 minute."
|
|
)
|
|
return ConversationHandler.END
|
|
|
|
# Get username from context or re-verify
|
|
username = context.user_data.get('pending_username')
|
|
|
|
if not username:
|
|
username = await verify_email_in_oracle(email)
|
|
if not username:
|
|
await edit_login_message(
|
|
context=context,
|
|
chat_id=update.effective_chat.id,
|
|
text="Eroare\n\nIncearcă din nou cu /login"
|
|
)
|
|
return ConversationHandler.END
|
|
|
|
# Delete old code and generate new one
|
|
await delete_user_email_codes(user_id)
|
|
|
|
code = generate_email_code()
|
|
|
|
# Save new code
|
|
await create_email_auth_code(
|
|
code=code,
|
|
email=email,
|
|
username=username,
|
|
telegram_user_id=user_id,
|
|
expiry_minutes=5
|
|
)
|
|
|
|
# Send email
|
|
email_service = get_email_service()
|
|
await email_service.send_auth_code(email, code, username)
|
|
|
|
# FIX BUG: EDIT message and KEEP buttons!
|
|
await edit_login_message(
|
|
context=context,
|
|
chat_id=update.effective_chat.id,
|
|
text=f"Cod retrimis pe {email}\n\nIntrodu codul primit pe email:",
|
|
reply_markup=InlineKeyboardMarkup([
|
|
[InlineKeyboardButton("Retrimite Cod", callback_data=f"resend:{email}")],
|
|
[InlineKeyboardButton("Anulează", callback_data="cancel")]
|
|
])
|
|
)
|
|
|
|
return AWAITING_CODE
|
|
|
|
|
|
# ============================================================================
|
|
# STATE: AWAITING_PASSWORD
|
|
# ============================================================================
|
|
|
|
async def receive_password(update: Update, context: ContextTypes.DEFAULT_TYPE):
|
|
"""Handler pentru primirea parolei Oracle"""
|
|
password = update.message.text.strip()
|
|
user_id = update.effective_user.id
|
|
|
|
# Șterge IMEDIAT mesajul cu parola (securitate)
|
|
try:
|
|
await update.message.delete()
|
|
logger.info(f"Password message deleted for user {user_id}")
|
|
except Exception as e:
|
|
logger.warning(f"Could not delete password message: {e}")
|
|
|
|
# Get verified data from context
|
|
username = context.user_data.get('verified_username')
|
|
email = context.user_data.get('verified_email')
|
|
session_token = context.user_data.get('session_token')
|
|
|
|
if not all([username, email, session_token]):
|
|
await edit_login_message(
|
|
context=context,
|
|
chat_id=update.effective_chat.id,
|
|
text="Sesiune expirată\n\nIncearcă din nou cu /login"
|
|
)
|
|
return ConversationHandler.END
|
|
|
|
# EDIT login message to show loading
|
|
await edit_login_message(
|
|
context=context,
|
|
chat_id=update.effective_chat.id,
|
|
text="Verificare...",
|
|
reply_markup=None
|
|
)
|
|
|
|
try:
|
|
# Call backend endpoint pentru verificare parolă + JWT
|
|
backend_client = get_backend_client()
|
|
server_id = context.user_data.get('server_id')
|
|
|
|
response = await backend_client.login_with_email(
|
|
email=email,
|
|
password=password,
|
|
telegram_user_id=user_id,
|
|
session_token=session_token,
|
|
server_id=server_id
|
|
)
|
|
|
|
if not response.get('success'):
|
|
await edit_login_message(
|
|
context=context,
|
|
chat_id=update.effective_chat.id,
|
|
text="Credențiale invalide\n\nParolă incorectă sau cont inactiv.\n\nIncearcă din nou cu /login"
|
|
)
|
|
return ConversationHandler.END
|
|
|
|
# Success - Salvează user în telegram_users
|
|
# First create or update user record
|
|
await create_or_update_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
|
|
)
|
|
|
|
# Then link to Oracle
|
|
from datetime import datetime, timedelta
|
|
token_expires_at = datetime.now() + timedelta(minutes=30) # Default expiry
|
|
|
|
await link_user_to_oracle(
|
|
telegram_user_id=user_id,
|
|
oracle_username=response['username'],
|
|
jwt_token=response['access_token'],
|
|
jwt_refresh_token=response['refresh_token'],
|
|
token_expires_at=token_expires_at
|
|
)
|
|
|
|
# Clear rate limits on successful auth
|
|
clear_rate_limit(f"login_{user_id}")
|
|
clear_rate_limit(f"resend_{user_id}")
|
|
|
|
# Get session and active company BEFORE editing message
|
|
from backend.modules.telegram.agent.session import get_session_manager
|
|
from backend.modules.telegram.bot.menus import create_main_menu, pad_message_for_wide_buttons
|
|
|
|
session_manager = get_session_manager()
|
|
session = await session_manager.get_or_create_session(user_id)
|
|
company = session.get_active_company()
|
|
|
|
company_name = company['name'] if company else None
|
|
company_cui = company.get('cui') if company else None
|
|
|
|
# Create main menu keyboard
|
|
keyboard = create_main_menu(
|
|
company_name=company_name,
|
|
company_cui=company_cui,
|
|
is_authenticated=True, # Now authenticated
|
|
cache_enabled=True # Default enabled
|
|
)
|
|
|
|
# Menu message with company info
|
|
companies_count = len(response.get('companies', []))
|
|
|
|
if company_name:
|
|
menu_text = f"{company_name}"
|
|
else:
|
|
menu_text = f"Companii disponibile: {companies_count}\n\nSelectează o companie pentru a continua"
|
|
|
|
menu_message = pad_message_for_wide_buttons(menu_text)
|
|
|
|
# EDIT login message to show menu (no deletion, direct edit)
|
|
await edit_login_message(
|
|
context=context,
|
|
chat_id=update.effective_chat.id,
|
|
text=menu_message,
|
|
reply_markup=keyboard
|
|
)
|
|
|
|
# Clear sensitive data from context
|
|
context.user_data.clear()
|
|
|
|
logger.info(f"User {user_id} authenticated successfully via email")
|
|
|
|
return ConversationHandler.END
|
|
|
|
except Exception as e:
|
|
logger.error(f"Error during password verification: {e}", exc_info=True)
|
|
await edit_login_message(
|
|
context=context,
|
|
chat_id=update.effective_chat.id,
|
|
text="Eroare la autentificare.\n\nIncearcă din nou cu /login"
|
|
)
|
|
return ConversationHandler.END
|
|
|
|
|
|
# ============================================================================
|
|
# CANCEL HANDLER
|
|
# ============================================================================
|
|
|
|
async def cancel_login(update: Update, context: ContextTypes.DEFAULT_TYPE):
|
|
"""Cancel conversation"""
|
|
|
|
# EDIT login message to show cancellation (don't delete)
|
|
if update.callback_query:
|
|
# Called from button
|
|
await edit_login_message(
|
|
context=context,
|
|
chat_id=update.effective_chat.id,
|
|
text="Login anulat",
|
|
reply_markup=None
|
|
)
|
|
elif update.message:
|
|
# Called from /cancel command
|
|
await edit_login_message(
|
|
context=context,
|
|
chat_id=update.effective_chat.id,
|
|
text="Login anulat",
|
|
reply_markup=None
|
|
)
|
|
|
|
# Clear context
|
|
context.user_data.clear()
|
|
|
|
return ConversationHandler.END
|
|
|
|
|
|
async def conversation_timeout(update: Update, context: ContextTypes.DEFAULT_TYPE):
|
|
"""Handler for conversation timeout"""
|
|
|
|
# EDIT login message to show timeout
|
|
await edit_login_message(
|
|
context=context,
|
|
chat_id=update.effective_chat.id,
|
|
text="Sesiune expirată\n\nConversația a expirat după 5 minute.\n\nIncearcă din nou cu /login"
|
|
)
|
|
|
|
# Clear context
|
|
context.user_data.clear()
|
|
|
|
return ConversationHandler.END
|
|
|
|
|
|
# ============================================================================
|
|
# CONVERSATION HANDLER SETUP
|
|
# ============================================================================
|
|
|
|
email_login_handler = ConversationHandler(
|
|
entry_points=[
|
|
CommandHandler('login', login_command),
|
|
CallbackQueryHandler(action_login_callback, pattern='^action:login$'),
|
|
CallbackQueryHandler(email_login_callback, pattern='^email_login$')
|
|
],
|
|
states={
|
|
AWAITING_EMAIL: [
|
|
MessageHandler(filters.TEXT & ~filters.COMMAND, receive_email)
|
|
],
|
|
AWAITING_SERVER_SELECTION: [
|
|
CallbackQueryHandler(handle_server_selected, pattern='^select_server:')
|
|
],
|
|
AWAITING_CODE: [
|
|
MessageHandler(filters.TEXT & ~filters.COMMAND, receive_code),
|
|
CallbackQueryHandler(resend_code_callback, pattern='^resend:')
|
|
],
|
|
AWAITING_PASSWORD: [
|
|
MessageHandler(filters.TEXT & ~filters.COMMAND, receive_password)
|
|
],
|
|
},
|
|
fallbacks=[
|
|
CommandHandler('cancel', cancel_login),
|
|
CallbackQueryHandler(cancel_login, pattern='^cancel$'),
|
|
CallbackQueryHandler(web_login_info_callback, pattern='^web_login_info$')
|
|
],
|
|
per_message=False, # Track conversation per user, not per message
|
|
allow_reentry=True, # Allow starting new conversation even if previous one is active
|
|
name="email_login_conversation"
|
|
)
|