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>
83 lines
3.6 KiB
Python
83 lines
3.6 KiB
Python
"""
|
|
Wrapper pentru AuthenticationMiddleware cu fix pentru endpoint-urile protejate
|
|
"""
|
|
from fastapi import Request, status
|
|
from fastapi.responses import JSONResponse
|
|
from starlette.middleware.base import BaseHTTPMiddleware
|
|
import sys
|
|
import os
|
|
|
|
sys.path.append(os.path.join(os.path.dirname(__file__), '../../../shared'))
|
|
|
|
from auth.middleware import AuthenticationMiddleware
|
|
from auth.models import AuthError
|
|
|
|
|
|
class FixedAuthenticationMiddleware(BaseHTTPMiddleware):
|
|
"""
|
|
Wrapper pentru AuthenticationMiddleware care aplică fix-ul pentru endpoint-urile protejate
|
|
"""
|
|
|
|
def __init__(self, app, **kwargs):
|
|
super().__init__(app)
|
|
# Create the original middleware instance without wrapping in BaseHTTPMiddleware
|
|
self.auth_middleware = AuthenticationMiddleware(app, **kwargs)
|
|
print("[FIXED MIDDLEWARE] FixedAuthenticationMiddleware initialized")
|
|
print(f"[FIXED MIDDLEWARE] Original middleware type: {type(self.auth_middleware)}")
|
|
|
|
async def dispatch(self, request: Request, call_next):
|
|
"""
|
|
Aplică fix-ul pentru endpoint-urile protejate:
|
|
- Returnează 401 pentru căile protejate fără token în loc să seteze request.state
|
|
"""
|
|
path = request.url.path
|
|
print(f"[FIXED MIDDLEWARE] Processing path: {path}")
|
|
|
|
# Verifică dacă path-ul trebuie exclus
|
|
excluded_paths = [
|
|
"/docs", "/health", "/api/auth/login", "/redoc", "/openapi.json",
|
|
"/api/telegram/health", "/api/telegram/auth/verify-user",
|
|
"/api/telegram/auth/verify-email", "/api/telegram/auth/login-with-email",
|
|
"/api/telegram/auth/refresh-token"
|
|
]
|
|
is_excluded = (path == "/" or any(path.startswith(excluded) for excluded in excluded_paths))
|
|
print(f"[FIXED MIDDLEWARE] Checking exclusions for {path}")
|
|
print(f"[FIXED MIDDLEWARE] Excluded paths: {excluded_paths}")
|
|
print(f"[FIXED MIDDLEWARE] Is excluded: {is_excluded}")
|
|
|
|
if is_excluded:
|
|
print(f"[FIXED MIDDLEWARE] Path {path} is excluded, skipping auth")
|
|
request.state.user = None
|
|
request.state.is_authenticated = False
|
|
response = await call_next(request)
|
|
return response
|
|
|
|
# Extrage token-ul
|
|
authorization = request.headers.get("Authorization")
|
|
print(f"[FIXED MIDDLEWARE] Authorization header: {authorization}")
|
|
|
|
if not authorization or not authorization.startswith("Bearer "):
|
|
print(f"[FIXED MIDDLEWARE] No valid token for protected path {path}, returning 401")
|
|
|
|
error = AuthError(
|
|
error="authentication_required",
|
|
error_description="Authentication required",
|
|
error_code="AUTH_003"
|
|
)
|
|
|
|
return JSONResponse(
|
|
status_code=status.HTTP_401_UNAUTHORIZED,
|
|
content=error.dict(),
|
|
headers={"WWW-Authenticate": "Bearer"}
|
|
)
|
|
|
|
# Token există, să îl validez prin middleware-ul original
|
|
print(f"[FIXED MIDDLEWARE] Token found, delegating to original middleware")
|
|
try:
|
|
result = await self.auth_middleware.dispatch(request, call_next)
|
|
print(f"[FIXED MIDDLEWARE] Original middleware returned: {type(result)}")
|
|
print(f"[FIXED MIDDLEWARE] Request state after middleware: user={getattr(request.state, 'user', 'MISSING')}, is_authenticated={getattr(request.state, 'is_authenticated', 'MISSING')}")
|
|
return result
|
|
except Exception as e:
|
|
print(f"[FIXED MIDDLEWARE] Exception in original middleware: {str(e)}")
|
|
raise |