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

@@ -207,42 +207,44 @@ def create_main_menu(
"""
keyboard = []
# Row 1: Company selection (full width, single line - InlineKeyboardButton doesn't support multiline)
if company_name:
# Short company name for button (CUI and month will be shown in message text)
# Truncate long names to fit in button
max_length = 35
display_name = company_name if len(company_name) <= max_length else company_name[:max_length-3] + "..."
# Only show financial menu if authenticated
if is_authenticated:
# Row 1: Company selection (full width, single line - InlineKeyboardButton doesn't support multiline)
if company_name:
# Short company name for button (CUI and month will be shown in message text)
# Truncate long names to fit in button
max_length = 35
display_name = company_name if len(company_name) <= max_length else company_name[:max_length-3] + "..."
keyboard.append([
InlineKeyboardButton(
f"{display_name}",
callback_data="menu:select_company"
)
])
else:
keyboard.append([
InlineKeyboardButton(
"Selectare Companie",
callback_data="menu:select_company"
)
])
keyboard.append([
InlineKeyboardButton(
f"{display_name}",
callback_data="menu:select_company"
)
])
else:
keyboard.append([
InlineKeyboardButton(
"Selectare Companie",
callback_data="menu:select_company"
)
])
# Rows 2-4: Financial options (2 buttons per row, made wide by message text padding)
keyboard.extend([
[
InlineKeyboardButton("Sold Companie", callback_data="menu:sold"),
InlineKeyboardButton("Trezorerie Casa", callback_data="menu:casa")
],
[
InlineKeyboardButton("Trezorerie Banca", callback_data="menu:banca"),
InlineKeyboardButton("Sold Clienti", callback_data="menu:clienti")
],
[
InlineKeyboardButton("Sold Furnizori", callback_data="menu:furnizori"),
InlineKeyboardButton("Evolutie Incasari", callback_data="menu:evolutie")
]
])
# Rows 2-4: Financial options (2 buttons per row, made wide by message text padding)
keyboard.extend([
[
InlineKeyboardButton("Sold Companie", callback_data="menu:sold"),
InlineKeyboardButton("Trezorerie Casa", callback_data="menu:casa")
],
[
InlineKeyboardButton("Trezorerie Banca", callback_data="menu:banca"),
InlineKeyboardButton("Sold Clienti", callback_data="menu:clienti")
],
[
InlineKeyboardButton("Sold Furnizori", callback_data="menu:furnizori"),
InlineKeyboardButton("Evolutie Incasari", callback_data="menu:evolutie")
]
])
# Row 5: Cache options (2 buttons per row, only if authenticated)
if is_authenticated: