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:
@@ -185,6 +185,119 @@ class BackendAPIClient:
|
||||
logger.error(f"Failed to refresh token: {e}")
|
||||
return None
|
||||
|
||||
async def verify_email(self, email: str) -> dict:
|
||||
"""
|
||||
Verify if email exists in Oracle database
|
||||
|
||||
Args:
|
||||
email: Email address to verify
|
||||
|
||||
Returns:
|
||||
dict with 'success' (bool), 'username' (str or None), and 'message' (str)
|
||||
|
||||
Raises:
|
||||
httpx.HTTPError: On network or HTTP errors
|
||||
"""
|
||||
try:
|
||||
if not self.client:
|
||||
self.client = AsyncClient(base_url=self.base_url, timeout=REQUEST_TIMEOUT)
|
||||
|
||||
response = await self.client.post(
|
||||
"/api/telegram/auth/verify-email",
|
||||
json={"email": email}
|
||||
)
|
||||
response.raise_for_status()
|
||||
return response.json()
|
||||
|
||||
except httpx.HTTPStatusError as e:
|
||||
logger.error(f"HTTP error verifying email {email}: {e.response.status_code}")
|
||||
return {
|
||||
"success": False,
|
||||
"username": None,
|
||||
"message": "Eroare la verificarea email-ului"
|
||||
}
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to verify email {email}: {e}")
|
||||
return {
|
||||
"success": False,
|
||||
"username": None,
|
||||
"message": "Eroare la verificarea email-ului"
|
||||
}
|
||||
|
||||
async def login_with_email(
|
||||
self,
|
||||
email: str,
|
||||
password: str,
|
||||
telegram_user_id: int,
|
||||
session_token: str
|
||||
) -> dict:
|
||||
"""
|
||||
Login via email + password with session token
|
||||
|
||||
Args:
|
||||
email: User email address
|
||||
password: Oracle password
|
||||
telegram_user_id: Telegram user ID
|
||||
session_token: Signed token from code validation
|
||||
|
||||
Returns:
|
||||
Login response with JWT tokens and user data
|
||||
|
||||
Raises:
|
||||
httpx.HTTPError: On network or HTTP errors
|
||||
"""
|
||||
try:
|
||||
if not self.client:
|
||||
self.client = AsyncClient(base_url=self.base_url, timeout=REQUEST_TIMEOUT)
|
||||
|
||||
response = await self.client.post(
|
||||
"/api/telegram/auth/login-with-email",
|
||||
json={
|
||||
"email": email,
|
||||
"password": password,
|
||||
"telegram_user_id": telegram_user_id,
|
||||
"session_token": session_token
|
||||
},
|
||||
timeout=30.0 # 30 seconds timeout
|
||||
)
|
||||
|
||||
response.raise_for_status()
|
||||
|
||||
data = response.json()
|
||||
logger.info(f"Email login successful for user {telegram_user_id}")
|
||||
|
||||
return data
|
||||
|
||||
except httpx.HTTPStatusError as e:
|
||||
logger.error(f"Email login HTTP error: {e.response.status_code} - {e.response.text}")
|
||||
|
||||
# Parse error detail if available
|
||||
try:
|
||||
error_data = e.response.json()
|
||||
return {
|
||||
"success": False,
|
||||
"message": error_data.get("detail", "Autentificare eșuată")
|
||||
}
|
||||
except:
|
||||
return {
|
||||
"success": False,
|
||||
"message": "Autentificare eșuată"
|
||||
}
|
||||
|
||||
except httpx.TimeoutException:
|
||||
logger.error("Email login timeout")
|
||||
return {
|
||||
"success": False,
|
||||
"message": "Timeout. Te rugăm să încerci din nou."
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Email login error: {e}", exc_info=True)
|
||||
return {
|
||||
"success": False,
|
||||
"message": "Eroare de conexiune"
|
||||
}
|
||||
|
||||
async def get_user_companies(self, jwt_token: str) -> List[Dict[str, Any]]:
|
||||
"""
|
||||
Get list of companies the user has access to.
|
||||
|
||||
Reference in New Issue
Block a user