Implement email-based 2FA authentication for Telegram bot with Oracle integration fixes
This commit adds a complete email authentication flow for the Telegram bot, allowing users to login with email + password instead of web app linking codes. Includes critical bug fixes for Oracle integration. **New Features:** - Email-based 2FA authentication with 6-digit codes sent via SMTP - Backend endpoints: verify-email and login-with-email - ConversationHandler for email authentication flow in Telegram bot - Session token verification to prevent user ID spoofing - Rate limiting (5 attempts per 5 minutes) - Email code expiry (5 minutes) with automatic cleanup **Bug Fixes:** - Fixed Oracle column name: ACTIV → INACTIV (with inverted logic) - Fixed Oracle password verification: verificautilizator returns checksum, not user_id - Fixed username case sensitivity: Oracle usernames must be uppercase - Fixed SMTP connection: use start_tls parameter instead of manual STARTTLS - Added middleware exclusions for public email auth endpoints **Backend Changes:** - Added verify-email endpoint (public) in telegram.py - Added login-with-email endpoint (public) with rate limiting and session verification - Updated middleware exclusions in main.py and auth_middleware_wrapper.py - Added AUTH_SESSION_SECRET configuration for session token signing **Telegram Bot Changes:** - New modules: app/auth/email_auth.py, app/bot/email_handlers.py - New utilities: app/utils/email_service.py (SMTP email sending) - Updated handlers.py: ignore callbacks handled by ConversationHandler - Updated menus.py: show Login button for unauthenticated users - Updated API client: verify_email() and login_with_email() methods - Database: email_auth_codes table with cleanup task **Configuration:** - Added SMTP configuration to telegram-bot .env.example - Added AUTH_SESSION_SECRET to backend .env.example - Updated .gitignore: exclude temporary files (*.pid, *.checksum, test scripts) **Dependencies:** - Added aiosmtplib for async SMTP email sending 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -158,16 +158,24 @@ async def start_command(update: Update, context: ContextTypes.DEFAULT_TYPE):
|
||||
)
|
||||
|
||||
else:
|
||||
# User not linked - show instructions with interactive buttons
|
||||
keyboard = InlineKeyboardMarkup([
|
||||
[InlineKeyboardButton("Cum obtin codul de link?", callback_data="login_help")],
|
||||
[InlineKeyboardButton("Am deja cod - Linkez contul", callback_data="login_prompt")]
|
||||
])
|
||||
# User not linked - show main menu with Login button
|
||||
from app.bot.menus import create_main_menu, pad_message_for_wide_buttons
|
||||
|
||||
keyboard = create_main_menu(
|
||||
company_name=None,
|
||||
company_cui=None,
|
||||
is_authenticated=False, # Not authenticated - shows Login button
|
||||
cache_enabled=None
|
||||
)
|
||||
|
||||
welcome_text = pad_message_for_wide_buttons(
|
||||
"**Bun venit la ROA2WEB Bot**\n\n"
|
||||
"Pentru a incepe, te rog să te autentifici.\n\n"
|
||||
"Selectează o companie pentru a continua"
|
||||
)
|
||||
|
||||
await update.message.reply_text(
|
||||
"**Bun venit la ROA2WEB Bot**\n\n"
|
||||
"Pentru a incepe, conecteaza contul tau ROA2WEB.\n\n"
|
||||
"Alege o optiune:",
|
||||
welcome_text,
|
||||
reply_markup=keyboard,
|
||||
parse_mode=ParseMode.MARKDOWN
|
||||
)
|
||||
@@ -1792,41 +1800,8 @@ async def handle_action_callback(query, telegram_user_id: int, callback_data: st
|
||||
parse_mode=ParseMode.MARKDOWN
|
||||
)
|
||||
|
||||
elif action_type == "login":
|
||||
# Prompt user to enter link code directly (same as login_prompt functionality)
|
||||
from telegram import ForceReply
|
||||
from app.bot.menus import pad_message_for_wide_buttons
|
||||
|
||||
# Edit the current message with instructions
|
||||
login_text = pad_message_for_wide_buttons(
|
||||
"**Conectare Cont ROA2WEB**\n\n"
|
||||
"Trimite-mi codul primit din aplicația web.\n\n"
|
||||
"Poți trimite:\n"
|
||||
"• Doar codul: ABC12XYZ\n"
|
||||
"• Sau comanda: /start ABC12XYZ\n\n"
|
||||
"Codul expiră în 15 minute."
|
||||
)
|
||||
|
||||
# Buttons for help or cancel
|
||||
keyboard = InlineKeyboardMarkup([
|
||||
[InlineKeyboardButton("Cum obțin codul?", callback_data="login_help")],
|
||||
[InlineKeyboardButton("« Anulează", callback_data="action:menu")]
|
||||
])
|
||||
|
||||
await query.edit_message_text(
|
||||
login_text,
|
||||
reply_markup=keyboard,
|
||||
parse_mode=ParseMode.MARKDOWN
|
||||
)
|
||||
|
||||
# Send a follow-up message with ForceReply to prompt input
|
||||
await query.message.reply_text(
|
||||
"Scrie sau lipește codul aici:",
|
||||
reply_markup=ForceReply(
|
||||
selective=True,
|
||||
input_field_placeholder="ABC12XYZ"
|
||||
)
|
||||
)
|
||||
# NOTE: action:login is handled by email_login_handler ConversationHandler
|
||||
# See app/bot/email_handlers.py for the complete email authentication flow
|
||||
|
||||
|
||||
async def handle_details_callback(query, telegram_user_id: int, callback_data: str):
|
||||
@@ -1997,10 +1972,32 @@ async def button_callback(update: Update, context: ContextTypes.DEFAULT_TYPE):
|
||||
"""
|
||||
try:
|
||||
query = update.callback_query
|
||||
callback_data = query.data
|
||||
|
||||
# ========== IGNORE CALLBACKS HANDLED BY CONVERSATION HANDLER ==========
|
||||
# These callbacks are managed by email_login_handler ConversationHandler
|
||||
# Return immediately without answering to let ConversationHandler process them
|
||||
conversation_patterns = [
|
||||
'action:login', # Login button from menu
|
||||
'email_login', # Email login button
|
||||
'web_login_info', # Web app login info button
|
||||
'cancel', # Cancel button
|
||||
]
|
||||
|
||||
# Check exact matches
|
||||
if callback_data in conversation_patterns:
|
||||
logger.info(f"[BUTTON_CALLBACK] Ignoring {callback_data} - handled by ConversationHandler")
|
||||
return
|
||||
|
||||
# Check prefix matches (e.g., resend:email@example.com)
|
||||
if callback_data.startswith('resend:'):
|
||||
logger.info(f"[BUTTON_CALLBACK] Ignoring {callback_data} - handled by ConversationHandler")
|
||||
return
|
||||
|
||||
# ========== PROCESS ALL OTHER CALLBACKS ==========
|
||||
await query.answer()
|
||||
|
||||
telegram_user_id = update.effective_user.id
|
||||
callback_data = query.data
|
||||
|
||||
logger.info(f"Button callback: {callback_data} from user {telegram_user_id}")
|
||||
|
||||
|
||||
Reference in New Issue
Block a user