""" API Router pentru Telegram Bot Integration Furnizează endpoint-uri pentru autentificare, linking și export rapoarte pentru Telegram bot """ from fastapi import APIRouter, Depends, HTTPException, Request from typing import List, Optional, Dict, Any import sys import os import secrets import string import httpx from datetime import datetime, timedelta from pydantic import BaseModel, Field sys.path.append(os.path.join(os.path.dirname(__file__), '../../../../shared')) from auth.dependencies import get_current_user from auth.models import CurrentUser from auth.jwt_handler import jwt_handler from database.oracle_pool import oracle_pool # Telegram bot internal API URL (running on same server) TELEGRAM_BOT_INTERNAL_API = os.getenv("TELEGRAM_BOT_INTERNAL_API", "http://localhost:8002") router = APIRouter(redirect_slashes=False) # ==================== Schemas ==================== class GenerateCodeRequest(BaseModel): """Request pentru generarea unui cod de linking""" telegram_user_id: int = Field(description="ID-ul utilizatorului Telegram") telegram_username: Optional[str] = Field(default=None, description="Username-ul Telegram") telegram_first_name: Optional[str] = Field(default=None, description="Prenumele utilizatorului") telegram_last_name: Optional[str] = Field(default=None, description="Numele utilizatorului") class GenerateCodeResponse(BaseModel): """Response pentru generarea unui cod de linking""" linking_code: str = Field(description="Codul de linking generat (8 caractere)") expires_at: datetime = Field(description="Data și ora expirării codului") expires_in_minutes: int = Field(description="Minutele până la expirare") class VerifyUserRequest(BaseModel): """ Request pentru verificarea utilizatorului în Oracle Suportă 2 flow-uri: 1. Auto-linking (recomandat): doar linking_code și oracle_username - Bot-ul verifică codul în SQLite, extrage oracle_username - Backend face lookup în Oracle fără verificare parolă - Codul valid este proof-of-authorization 2. Full verification (opțional): username, password, linking_code - Verificare completă cu parolă în Oracle """ linking_code: str = Field(description="Codul de linking de la /generate-code") oracle_username: Optional[str] = Field(default=None, description="Username Oracle (pentru auto-linking)") username: Optional[str] = Field(default=None, description="Username pentru verificare completă") password: Optional[str] = Field(default=None, description="Parolă pentru verificare completă") class VerifyUserResponse(BaseModel): """Response pentru verificarea utilizatorului""" success: bool = Field(description="True dacă verificarea a avut succes") access_token: Optional[str] = Field(default=None, description="JWT access token") refresh_token: Optional[str] = Field(default=None, description="JWT refresh token") user: Optional[Dict[str, Any]] = Field(default=None, description="Detalii utilizator") message: str = Field(description="Mesaj de status") class RefreshTokenRequest(BaseModel): """Request pentru refresh JWT token""" refresh_token: str = Field(description="Refresh token-ul obținut la autentificare") class RefreshTokenResponse(BaseModel): """Response pentru refresh token""" access_token: str = Field(description="Noul JWT access token") expires_in: int = Field(description="Timpul de expirare în secunde") token_type: str = Field(default="bearer", description="Tipul token-ului") class ExportReportRequest(BaseModel): """Request pentru exportul unui raport""" company_id: int = Field(description="ID-ul firmei") report_type: str = Field(description="Tipul raportului (invoices, payments, dashboard)") format: str = Field(default="excel", description="Formatul exportului (excel, pdf, csv)") filters: Optional[Dict[str, Any]] = Field(default=None, description="Filtre pentru raport") class ExportReportResponse(BaseModel): """Response pentru exportul raportului""" success: bool = Field(description="True dacă exportul a avut succes") file_url: Optional[str] = Field(default=None, description="URL-ul fișierului generat") file_name: Optional[str] = Field(default=None, description="Numele fișierului generat") file_size_bytes: Optional[int] = Field(default=None, description="Mărimea fișierului în bytes") message: str = Field(description="Mesaj de status") # ==================== Helper Functions ==================== def generate_linking_code(length: int = 8) -> str: """ Generează un cod alfanumeric aleatoriu pentru linking Args: length: Lungimea codului (default: 8) Returns: Codul generat (uppercase alphanumeric) """ alphabet = string.ascii_uppercase + string.digits # Exclude caractere care pot fi confundate: 0, O, I, 1 alphabet = alphabet.replace('0', '').replace('O', '').replace('I', '').replace('1', '') return ''.join(secrets.choice(alphabet) for _ in range(length)) async def get_oracle_user_by_username(username: str) -> Optional[Dict[str, Any]]: """ Obține informații despre utilizator din Oracle FĂRĂ verificare parolă. Folosit pentru auto-linking când utilizatorul a fost deja autentificat prin generarea unui linking code valid în aplicația web. Args: username: Username-ul utilizatorului Oracle Returns: Dict cu informații despre utilizator sau None dacă nu există """ try: async with oracle_pool.get_connection() as connection: with connection.cursor() as cursor: # Obține detalii utilizator cursor.execute(""" SELECT ID_UTIL, UTILIZATOR FROM UTILIZATORI WHERE UPPER(UTILIZATOR) = :username """, {'username': username.upper()}) user_row = cursor.fetchone() if not user_row: return None user_id = user_row[0] actual_username = user_row[1] # Obține companiile utilizatorului cursor.execute(""" SELECT A.ID_FIRMA, A.FIRMA FROM V_NOM_FIRME A WHERE A.ID_FIRMA IN ( SELECT ID_FIRMA FROM VDEF_UTIL_FIRME WHERE ID_PROGRAM = 2 AND ID_UTIL = :user_id ) ORDER BY A.FIRMA """, {'user_id': user_id}) companies_result = cursor.fetchall() companies = [str(row[0]) for row in companies_result] return { 'user_id': user_id, 'username': actual_username, 'companies': companies, 'permissions': ['read', 'reports'] } except Exception as e: print(f"Error getting Oracle user by username: {e}") return None async def verify_oracle_user(username: str, password: str) -> Optional[Dict[str, Any]]: """ Verifică utilizatorul în Oracle folosind pack_drepturi.verificautilizator Args: username: Username-ul utilizatorului password: Parola utilizatorului Returns: Dict cu informații despre utilizator sau None dacă verificarea eșuează """ try: async with oracle_pool.get_connection() as connection: with connection.cursor() as cursor: # Verifică autentificarea cursor.execute(""" SELECT pack_drepturi.verificautilizator(:username, :password) FROM DUAL """, { 'username': username.upper(), 'password': password }) result = cursor.fetchone() verification_result = result[0] if result else -1 if verification_result == -1: return None # Obține detalii utilizator cursor.execute(""" SELECT ID_UTIL, UTILIZATOR FROM UTILIZATORI WHERE UPPER(UTILIZATOR) = :username """, {'username': username.upper()}) user_row = cursor.fetchone() if not user_row: return None user_id = user_row[0] # Obține companiile utilizatorului cursor.execute(""" SELECT A.ID_FIRMA, A.FIRMA FROM V_NOM_FIRME A WHERE A.ID_FIRMA IN ( SELECT ID_FIRMA FROM VDEF_UTIL_FIRME WHERE ID_PROGRAM = 2 AND ID_UTIL = :user_id ) ORDER BY A.FIRMA """, {'user_id': user_id}) companies_result = cursor.fetchall() companies = [str(row[0]) for row in companies_result] return { 'user_id': user_id, 'username': username, 'companies': companies, 'permissions': ['read', 'reports'] } except Exception as e: print(f"Error verifying Oracle user: {e}") return None # ==================== Endpoints ==================== @router.post("/auth/generate-code", response_model=GenerateCodeResponse) async def generate_linking_code_endpoint( current_user: CurrentUser = Depends(get_current_user) ): """ Generează un cod de linking pentru conectarea unui utilizator Telegram Flow: 1. Utilizatorul autentificat în aplicație solicită un cod 2. Se generează un cod unic de 8 caractere 3. Codul este trimis la Telegram bot pentru salvare în SQLite cu TTL de 15 minute 4. Utilizatorul introduce codul în Telegram bot pentru linking Note: - Acest endpoint necesită autentificare JWT (utilizatorul trebuie să fie logat în aplicație) - Codul expiră după 15 minute - Fiecare request generează un cod nou (codurile vechi devin invalide) - Nu este nevoie de telegram_user_id în acest moment (utilizatorul nu e încă conectat la Telegram) """ try: # Generează cod unic linking_code = generate_linking_code() # Setează expirarea la 15 minute expires_at = datetime.utcnow() + timedelta(minutes=15) expires_in_minutes = 15 # Salvează codul în database-ul Telegram bot (SQLite) via internal API try: async with httpx.AsyncClient(timeout=5.0) as client: save_code_response = await client.post( f"{TELEGRAM_BOT_INTERNAL_API}/internal/save-code", json={ "code": linking_code, "telegram_user_id": 0, # Not known yet (user hasn't linked) "oracle_username": current_user.username, "expires_in_minutes": expires_in_minutes } ) # Accept both 200 (OK) and 201 (Created) as success if save_code_response.status_code not in [200, 201]: raise HTTPException( status_code=500, detail=f"Failed to save code to Telegram bot: {save_code_response.text}" ) except httpx.TimeoutException: raise HTTPException( status_code=503, detail="Telegram bot service is not responding. Please try again later." ) except httpx.ConnectError: raise HTTPException( status_code=503, detail="Cannot connect to Telegram bot service. Please contact administrator." ) return GenerateCodeResponse( linking_code=linking_code, expires_at=expires_at, expires_in_minutes=expires_in_minutes ) except HTTPException: raise except Exception as e: raise HTTPException( status_code=500, detail=f"Eroare la generarea codului de linking: {str(e)}" ) @router.post("/auth/verify-user", response_model=VerifyUserResponse) async def verify_user_endpoint(request: VerifyUserRequest): """ Verifică utilizatorul în Oracle și returnează JWT tokens Suportă 2 flow-uri de autentificare: Flow A - Auto-linking (RECOMANDAT): 1. Bot verifică linking_code în SQLite (code valid = user s-a autentificat în web app) 2. Bot extrage oracle_username din cod 3. Bot trimite: {linking_code, oracle_username} 4. Backend face lookup în Oracle (FĂRĂ verificare parolă) 5. Backend generează și returnează JWT tokens Flow B - Full verification (OPȚIONAL): 1. Bot cere username și parolă de la user în Telegram 2. Bot trimite: {linking_code, username, password} 3. Backend verifică credențialele în Oracle 4. Backend generează și returnează JWT tokens Note: - Acest endpoint NU necesită autentificare JWT (este public pentru bot) - Flow A oferă UX superior (fără re-introducere parolă) - Linking code-ul valid este proof-of-authorization """ try: # Flow A: Auto-linking (oracle_username provided, no password) if request.oracle_username and not request.password: user_data = await get_oracle_user_by_username(request.oracle_username) if not user_data: return VerifyUserResponse( success=False, message=f"Utilizatorul {request.oracle_username} nu există în Oracle" ) # Flow B: Full verification (username + password provided) elif request.username and request.password: user_data = await verify_oracle_user(request.username, request.password) if not user_data: return VerifyUserResponse( success=False, message="Username sau parolă incorectă" ) # Invalid request (missing required fields) else: return VerifyUserResponse( success=False, message="Trebuie furnizat fie oracle_username (auto-linking) fie username+password (verificare completă)" ) # Generează JWT tokens access_token = jwt_handler.create_access_token( username=user_data['username'], companies=user_data['companies'], user_id=user_data['user_id'], permissions=user_data['permissions'] ) refresh_token = jwt_handler.create_refresh_token( username=user_data['username'], user_id=user_data['user_id'] ) return VerifyUserResponse( success=True, access_token=access_token, refresh_token=refresh_token, user={ 'user_id': user_data['user_id'], 'username': user_data['username'], 'companies': user_data['companies'], 'permissions': user_data['permissions'] }, message="Autentificare reușită" ) except Exception as e: raise HTTPException( status_code=500, detail=f"Eroare la verificarea utilizatorului: {str(e)}" ) @router.post("/auth/refresh-token", response_model=RefreshTokenResponse) async def refresh_token_endpoint(request: RefreshTokenRequest): """ Refresh-uiește un JWT access token folosind refresh token-ul Acest endpoint este folosit de Telegram bot pentru a obține un nou access token când cel curent expiră, fără a solicita din nou username/password. Flow: 1. Botul Telegram detectează că access token-ul a expirat 2. Trimite refresh token-ul la acest endpoint 3. Se validează refresh token-ul și se generează un nou access token 4. Botul stochează noul access token în SQLite Note: - Refresh token-ul este valid 7 zile (vs 30 minute pentru access token) - Dacă refresh token-ul expiră, utilizatorul trebuie să se re-autentifice """ try: # Verifică refresh token-ul token_data = jwt_handler.verify_token(request.refresh_token) if not token_data or token_data.token_type != "refresh": raise HTTPException( status_code=401, detail="Refresh token invalid sau expirat" ) # Obține companiile actualizate din Oracle async with oracle_pool.get_connection() as connection: with connection.cursor() as cursor: cursor.execute(""" SELECT A.ID_FIRMA FROM V_NOM_FIRME A WHERE A.ID_FIRMA IN ( SELECT ID_FIRMA FROM VDEF_UTIL_FIRME WHERE ID_PROGRAM = 2 AND ID_UTIL = :user_id ) ORDER BY A.FIRMA """, {'user_id': token_data.user_id}) companies_result = cursor.fetchall() companies = [str(row[0]) for row in companies_result] # Generează nou access token new_access_token = jwt_handler.create_access_token( username=token_data.username, companies=companies, user_id=token_data.user_id, permissions=token_data.permissions ) return RefreshTokenResponse( access_token=new_access_token, expires_in=jwt_handler.access_token_expire_minutes * 60, token_type="bearer" ) except HTTPException: raise except Exception as e: raise HTTPException( status_code=500, detail=f"Eroare la refresh token: {str(e)}" ) @router.post("/export", response_model=ExportReportResponse) async def export_report_endpoint( request: ExportReportRequest, current_user: CurrentUser = Depends(get_current_user) ): """ Exportă un raport în format Excel, PDF sau CSV Acest endpoint este folosit de Telegram bot pentru a genera rapoarte și a le trimite utilizatorului. Flow: 1. Botul trimite cerere de export cu parametrii raportului 2. Se validează că utilizatorul are acces la firma specificată 3. Se generează raportul în formatul solicitat 4. Se returnează URL-ul sau conținutul fișierului Tipuri de rapoarte suportate: - invoices: Facturi (cu filtre: dată, status, client) - payments: Încasări (cu filtre: dată, metodă plată) - dashboard: Statistici dashboard (rezumat) Formate suportate: - excel: XLSX (cel mai complet) - pdf: PDF (pentru printing) - csv: CSV (pentru import în alte sisteme) Note: - Utilizatorul trebuie să aibă acces la firma specificată - Fișierele generate sunt temporare (șterse după 1 oră) """ try: # Verifică accesul la firmă company_id_str = str(request.company_id) if company_id_str not in current_user.companies: raise HTTPException( status_code=403, detail=f"Nu aveți acces la firma {request.company_id}" ) # TODO: Implementare export în funcție de report_type și format # Deocamdată returnăm un placeholder return ExportReportResponse( success=True, file_url=f"/api/telegram/downloads/report_{request.report_type}_{request.company_id}.{request.format}", file_name=f"raport_{request.report_type}_{datetime.now().strftime('%Y%m%d_%H%M%S')}.{request.format}", file_size_bytes=0, message=f"Raport {request.report_type} generat cu succes în format {request.format}" ) except HTTPException: raise except Exception as e: raise HTTPException( status_code=500, detail=f"Eroare la generarea raportului: {str(e)}" ) @router.get("/health") async def telegram_health_check(): """ Health check pentru routerul Telegram Verifică conectivitatea la Oracle și disponibilitatea serviciilor """ try: async with oracle_pool.get_connection() as connection: with connection.cursor() as cursor: cursor.execute("SELECT 1 FROM DUAL") return { "status": "healthy", "service": "telegram-router", "database": "connected", "timestamp": datetime.utcnow().isoformat() } except Exception as e: return { "status": "degraded", "service": "telegram-router", "database": f"error: {str(e)}", "timestamp": datetime.utcnow().isoformat() }