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

@@ -88,6 +88,22 @@ async def init_database() -> None:
)
""")
# Create email_auth_codes table (email-based authentication)
await db.execute("""
CREATE TABLE IF NOT EXISTS email_auth_codes (
code TEXT PRIMARY KEY,
email TEXT NOT NULL,
oracle_username TEXT NOT NULL,
telegram_user_id INTEGER NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
expires_at TIMESTAMP NOT NULL,
used INTEGER DEFAULT 0,
used_at TIMESTAMP,
failed_attempts INTEGER DEFAULT 0,
FOREIGN KEY (telegram_user_id) REFERENCES telegram_users(telegram_user_id)
)
""")
# Create indexes for better query performance
await db.execute("""
CREATE INDEX IF NOT EXISTS idx_auth_codes_telegram_user
@@ -109,6 +125,22 @@ async def init_database() -> None:
ON telegram_sessions(expires_at)
""")
# Create indexes for email_auth_codes table
await db.execute("""
CREATE INDEX IF NOT EXISTS idx_email_auth_email
ON email_auth_codes(email)
""")
await db.execute("""
CREATE INDEX IF NOT EXISTS idx_email_auth_telegram_user
ON email_auth_codes(telegram_user_id)
""")
await db.execute("""
CREATE INDEX IF NOT EXISTS idx_email_auth_expires
ON email_auth_codes(expires_at)
""")
await db.commit()
logger.info("Database initialized successfully")
@@ -184,6 +216,40 @@ async def cleanup_expired_sessions() -> int:
return 0
async def cleanup_expired_email_codes() -> int:
"""
Delete expired and old used email codes from the database.
This should be called periodically (e.g., hourly).
Returns:
int: Number of email codes deleted
"""
try:
async with aiosqlite.connect(DB_PATH) as db:
db.row_factory = aiosqlite.Row
# Delete expired codes or used codes older than 1 day
cursor = await db.execute("""
DELETE FROM email_auth_codes
WHERE expires_at < ?
OR (used = 1 AND used_at < ?)
""", (
datetime.now(),
datetime.now() - timedelta(days=1)
))
await db.commit()
deleted = cursor.rowcount
if deleted > 0:
logger.info(f"Cleaned up {deleted} expired/old email auth codes")
return deleted
except Exception as e:
logger.error(f"Failed to cleanup email codes: {e}")
return 0
async def get_database_stats() -> dict:
"""
Get database statistics for monitoring.
@@ -238,6 +304,7 @@ __all__ = [
'init_database',
'cleanup_expired_codes',
'cleanup_expired_sessions',
'cleanup_expired_email_codes',
'get_database_stats',
'DB_PATH',
]