feat: Add A-Z filter for clients/suppliers in Telegram bot
- 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>
This commit is contained in:
@@ -14,6 +14,8 @@ 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,
|
||||
@@ -22,6 +24,7 @@ from backend.modules.telegram.auth.email_auth import (
|
||||
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,
|
||||
@@ -39,7 +42,7 @@ from backend.modules.telegram.api.client import get_backend_client
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
# Conversation states
|
||||
AWAITING_EMAIL, AWAITING_CODE, AWAITING_PASSWORD = range(3)
|
||||
AWAITING_EMAIL, AWAITING_CODE, AWAITING_PASSWORD, AWAITING_SERVER_SELECTION = range(4)
|
||||
|
||||
# Constants
|
||||
MAX_CODE_ATTEMPTS = 3
|
||||
@@ -261,57 +264,25 @@ async def web_login_info_callback(update: Update, context: ContextTypes.DEFAULT_
|
||||
# STATE: AWAITING_EMAIL
|
||||
# ============================================================================
|
||||
|
||||
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
|
||||
|
||||
# ȘTERG mesajul utilizatorului imediat (chat curat)
|
||||
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:
|
||||
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):
|
||||
# Show error in main message
|
||||
await edit_login_message(
|
||||
context=context,
|
||||
chat_id=update.effective_chat.id,
|
||||
text="Email invalid\n\nIntrodu o adresă validă (nume@domeniu.ro)",
|
||||
reply_markup=InlineKeyboardMarkup([
|
||||
[InlineKeyboardButton("Anulează", callback_data="cancel")]
|
||||
])
|
||||
)
|
||||
return AWAITING_EMAIL
|
||||
|
||||
# Check for existing pending code
|
||||
existing_code = await get_pending_email_code(user_id)
|
||||
if existing_code:
|
||||
# Delete old pending code
|
||||
await delete_user_email_codes(user_id)
|
||||
logger.info(f"Deleted existing pending code for user {user_id}")
|
||||
|
||||
# EDIT login message to show loading
|
||||
await edit_login_message(
|
||||
context=context,
|
||||
chat_id=update.effective_chat.id,
|
||||
text="Verificare email...",
|
||||
reply_markup=None
|
||||
)
|
||||
|
||||
try:
|
||||
# Verifică email în Oracle
|
||||
username = await verify_email_in_oracle(email)
|
||||
# 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:
|
||||
# Email exists - generate and send code
|
||||
code = generate_email_code()
|
||||
|
||||
# Save code in database
|
||||
code_saved = await create_email_auth_code(
|
||||
code=code,
|
||||
email=email,
|
||||
@@ -323,27 +294,26 @@ async def receive_email(update: Update, context: ContextTypes.DEFAULT_TYPE):
|
||||
if not code_saved:
|
||||
await edit_login_message(
|
||||
context=context,
|
||||
chat_id=update.effective_chat.id,
|
||||
chat_id=chat_id,
|
||||
text="Eroare la salvarea codului.\n\nIncearcă din nou cu /login"
|
||||
)
|
||||
return ConversationHandler.END
|
||||
|
||||
# Send email (async with retry)
|
||||
email_service = get_email_service()
|
||||
email_sent = await email_service.send_auth_code(email, code, username)
|
||||
|
||||
if not email_sent:
|
||||
logger.error(f"Failed to send email to {email}")
|
||||
# Don't reveal this to user - they'll timeout naturally
|
||||
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 better UX (looks like verification happened)
|
||||
# Wait 1 second for UX (looks like verification happened)
|
||||
await asyncio.sleep(1)
|
||||
|
||||
# ALWAYS show this message (prevent enumeration)
|
||||
# EDIT same message with success + buttons
|
||||
# ALWAYS show success (prevent enumeration)
|
||||
await edit_login_message(
|
||||
context=context,
|
||||
chat_id=update.effective_chat.id,
|
||||
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}")],
|
||||
@@ -351,22 +321,135 @@ async def receive_email(update: Update, context: ContextTypes.DEFAULT_TYPE):
|
||||
])
|
||||
)
|
||||
|
||||
# Save email in context for resend functionality
|
||||
context.user_data['pending_email'] = email
|
||||
context.user_data['pending_username'] = username
|
||||
|
||||
return AWAITING_CODE
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error in receive_email: {e}", exc_info=True)
|
||||
logger.error(f"Error sending email code: {e}", exc_info=True)
|
||||
await edit_login_message(
|
||||
context=context,
|
||||
chat_id=update.effective_chat.id,
|
||||
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
|
||||
# ============================================================================
|
||||
@@ -593,12 +676,14 @@ async def receive_password(update: Update, context: ContextTypes.DEFAULT_TYPE):
|
||||
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
|
||||
session_token=session_token,
|
||||
server_id=server_id
|
||||
)
|
||||
|
||||
if not response.get('success'):
|
||||
@@ -749,6 +834,9 @@ email_login_handler = ConversationHandler(
|
||||
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:')
|
||||
|
||||
Reference in New Issue
Block a user