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:
2025-11-11 12:00:46 +02:00
parent 1378ee1e6a
commit 706062dc0f
19 changed files with 2032 additions and 101 deletions

View File

@@ -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}")