Files
roa2web-service-auto/backend/modules/telegram/routers/auth_codes.py
Claude Agent 30f55cf18b feat: Add A-Z filter for clients/suppliers in Telegram bot
- Add A-Z alphabetical filter keyboard for clients and suppliers lists
  (same pattern as company selection, without emoji)
- Increase clients/suppliers list pagination from 10 to 20 items per page
- Remove emoji from company A-Z filter button for consistency
- Add 6 new callback handlers: clients_alpha_menu, clients_alpha:LETTER,
  clients_alpha_page:PAGE:LETTER, and supplier equivalents
- Dashboard service and models updates
- Telegram bot: email handlers, auth, DB operations, internal API improvements
- Frontend: dashboard cards updates (CashFlow, Clienti, Furnizori, Treasury)
- Frontend: SolduriCompactCard and CollapsibleCard improvements
- DashboardView enhancements
- start.sh and run-with-restart.sh script updates
- IIS web.config and service worker updates

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-21 14:34:15 +00:00

946 lines
36 KiB
Python

"""
API Router pentru Telegram Bot Integration
Furnizează endpoint-uri pentru autentificare, linking și export rapoarte pentru Telegram bot
"""
from fastapi import APIRouter, Body, Depends, HTTPException, Request
from typing import List, Optional, Dict, Any
# import sys # Removed - no longer needed
import os
import secrets
import string
import httpx
from datetime import datetime, timedelta
from pydantic import BaseModel, Field
from shared.auth.dependencies import get_current_user
from shared.auth.models import CurrentUser
from shared.auth.jwt_handler import jwt_handler
from shared.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:8000/api/telegram")
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ă")
server_id: Optional[str] = Field(default=None, description="Oracle server ID (pentru multi-server mode)")
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")
class VerifyEmailRequest(BaseModel):
"""Request pentru verificarea email-ului în Oracle"""
email: str = Field(description="Adresa de email Oracle")
server_id: Optional[str] = Field(default=None, description="Oracle server ID (pentru multi-server mode)")
class VerifyEmailResponse(BaseModel):
"""Response pentru verificarea email-ului"""
success: bool = Field(description="True dacă email-ul există și este activ")
username: Optional[str] = Field(default=None, description="Username-ul Oracle asociat")
message: str = Field(description="Mesaj de status")
class TelegramEmailLoginRequest(BaseModel):
"""Request pentru autentificare prin email + parolă"""
email: str = Field(description="Adresa de email Oracle")
password: str = Field(description="Parola Oracle")
telegram_user_id: int = Field(description="ID-ul utilizatorului Telegram")
session_token: str = Field(description="Token de sesiune pentru preveni spoofing")
server_id: Optional[str] = Field(default=None, description="Oracle server ID (pentru multi-server mode)")
class TelegramEmailLoginResponse(BaseModel):
"""Response pentru autentificare prin email + parolă"""
success: bool = Field(description="True dacă autentificarea 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")
token_type: str = Field(default="bearer", description="Tipul token-ului")
user_id: Optional[int] = Field(default=None, description="ID-ul utilizatorului Oracle")
username: Optional[str] = Field(default=None, description="Username-ul Oracle")
companies: List[Dict[str, Any]] = Field(default_factory=list, description="Lista companiilor")
message: str = Field(description="Mesaj de status")
class SwitchServerRequest(BaseModel):
"""Request pentru schimbarea serverului Oracle"""
oracle_username: str = Field(description="Username Oracle al utilizatorului curent")
new_server_id: str = Field(description="ID-ul noului server Oracle")
oracle_password: Optional[str] = Field(default=None, description="Parola Oracle pe noul server (obligatorie dacă servere diferite au parole diferite)")
class SwitchServerResponse(BaseModel):
"""Response pentru schimbarea serverului Oracle"""
success: bool = Field(description="True dacă schimbarea a reușit")
access_token: Optional[str] = Field(default=None, description="Noul JWT access token")
refresh_token: Optional[str] = Field(default=None, description="Noul JWT refresh token")
message: str = Field(description="Mesaj de status")
# ==================== Helper Functions ====================
# Rate limiting storage (in-memory)
from collections import defaultdict
_endpoint_rate_limits = defaultdict(list)
def check_endpoint_rate_limit(
identifier: str,
max_attempts: int = 5,
window_minutes: int = 5
) -> bool:
"""Backend rate limiting for sensitive endpoints"""
now = datetime.now()
cutoff = now - timedelta(minutes=window_minutes)
# Clean old attempts
_endpoint_rate_limits[identifier] = [
attempt for attempt in _endpoint_rate_limits[identifier]
if attempt > cutoff
]
# Check limit
if len(_endpoint_rate_limits[identifier]) >= max_attempts:
return False
# Add attempt
_endpoint_rate_limits[identifier].append(now)
return True
def verify_session_token(
telegram_user_id: int,
email: str,
token: str
) -> bool:
"""
Verify session token from bot to prevent user ID spoofing
Token format: user_id:email:signature
"""
import hashlib
try:
parts = token.split(":")
if len(parts) != 3:
return False
token_user_id, token_email, signature = parts
# Verify user ID and email match
if int(token_user_id) != telegram_user_id or token_email != email:
return False
# Verify signature
secret = os.getenv("AUTH_SESSION_SECRET", "change-me-in-production")
payload = f"{telegram_user_id}:{email}:{secret}"
expected_signature = hashlib.sha256(payload.encode()).hexdigest()[:16]
if signature != expected_signature:
return False
return True
except Exception:
return False
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, server_id: Optional[str] = None) -> 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
server_id: ID-ul serverului Oracle (pentru multi-server mode)
Returns:
Dict cu informații despre utilizator sau None dacă nu există
"""
try:
async with oracle_pool.get_connection(server_id) 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, server_id: Optional[str] = None) -> Optional[Dict[str, Any]]:
"""
Verifică utilizatorul în Oracle folosind pack_drepturi.verificautilizator
Args:
username: Username-ul utilizatorului
password: Parola utilizatorului
server_id: ID-ul serverului Oracle (pentru multi-server mode)
Returns:
Dict cu informații despre utilizator sau None dacă verificarea eșuează
"""
try:
async with oracle_pool.get_connection(server_id) 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(
request: Request,
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)
- server_id este extras automat din JWT (setat de auth middleware în request.state)
"""
try:
# Extrage server_id din JWT (setat de auth middleware)
server_id = getattr(request.state, 'server_id', None)
# 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,
"server_id": server_id
}
)
# 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, server_id=request.server_id)
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, server_id=request.server_id)
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 (cu server_id pentru multi-server mode)
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'],
server_id=request.server_id
)
refresh_token = jwt_handler.create_refresh_token(
username=user_data['username'],
user_id=user_data['user_id'],
server_id=request.server_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 (folosind server_id din refresh token)
async with oracle_pool.get_connection(token_data.server_id) 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 (păstrează server_id din refresh 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,
server_id=token_data.server_id
)
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("/auth/verify-email", response_model=VerifyEmailResponse)
async def verify_email_endpoint(request: VerifyEmailRequest):
"""
Verify if email exists in Oracle UTILIZATORI table (PUBLIC endpoint)
This is a PUBLIC endpoint used by the telegram bot during email authentication.
Returns username if email exists and user is active.
Security: Generic error messages to prevent email enumeration.
"""
try:
async with oracle_pool.get_connection(request.server_id) as connection:
with connection.cursor() as cursor:
# Query to find username by email
cursor.execute("""
SELECT UTILIZATOR
FROM CONTAFIN_ORACLE.UTILIZATORI
WHERE UPPER(EMAIL) = UPPER(:email)
AND INACTIV = 0
AND STERS = 0
""", {"email": request.email})
row = cursor.fetchone()
if row:
username = row[0]
return VerifyEmailResponse(
success=True,
username=username,
message="Email verificat cu succes"
)
else:
# Generic message (no enumeration)
return VerifyEmailResponse(
success=False,
username=None,
message="Email invalid sau inactiv"
)
except Exception as e:
# Generic error message (no details exposed)
return VerifyEmailResponse(
success=False,
username=None,
message="Eroare la verificarea email-ului"
)
@router.post("/auth/login-with-email", response_model=TelegramEmailLoginResponse)
async def login_with_email_endpoint(request: TelegramEmailLoginRequest):
"""
Telegram email + password authentication endpoint
Security features:
- Rate limiting: 5 attempts per 5 minutes
- Session token verification (prevent user ID spoofing)
- Generic error messages (no username/email enumeration)
- Password verification in Oracle (not stored)
"""
# 1. Rate limiting
rate_limit_key = f"email_login_{request.telegram_user_id}"
if not check_endpoint_rate_limit(rate_limit_key, max_attempts=5, window_minutes=5):
raise HTTPException(
status_code=429,
detail="Prea multe încercări. Te rugăm să aștepți 5 minute."
)
# 2. Verify session token (prevent user ID spoofing)
if not verify_session_token(
request.telegram_user_id,
request.email,
request.session_token
):
raise HTTPException(
status_code=401,
detail="Sesiune invalidă. Te rugăm să reîncepi autentificarea."
)
try:
async with oracle_pool.get_connection(request.server_id) as connection:
with connection.cursor() as cursor:
# 3. Find username by email
cursor.execute("""
SELECT ID_UTIL, UTILIZATOR, INACTIV, STERS
FROM CONTAFIN_ORACLE.UTILIZATORI
WHERE UPPER(EMAIL) = UPPER(:email)
""", {"email": request.email})
user_row = cursor.fetchone()
# SECURITY: Generic error message (no email enumeration)
if not user_row:
raise HTTPException(
status_code=401,
detail="Credențiale invalide" # Generic message
)
user_id, username, inactiv, sters = user_row
# Check if user is active (INACTIV=0 means active, STERS=0 means not deleted)
if inactiv != 0 or sters != 0:
raise HTTPException(
status_code=401,
detail="Credențiale invalide" # Generic message
)
# 4. Verify password via Oracle stored procedure
# NOTE: This procedure returns a verification code, NOT the user_id!
# Returns -1 if authentication fails, any other value means success
cursor.execute("""
SELECT pack_drepturi.verificautilizator(:username, :password)
FROM DUAL
""", {
"username": username.upper(), # IMPORTANT: Oracle usernames are uppercase
"password": request.password
})
verification_result = cursor.fetchone()[0]
# SECURITY: Generic error message (no username leak)
if verification_result == -1:
raise HTTPException(
status_code=401,
detail="Credențiale invalide" # Generic message
)
# 5. Get user companies
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 = [
{"id": str(row[0]), "name": row[1]}
for row in companies_result
]
company_ids = [str(row[0]) for row in companies_result]
# 6. Get user permissions (default for Telegram)
permissions = ['read', 'reports']
# 7. Generate JWT tokens (with server_id for multi-server routing)
access_token = jwt_handler.create_access_token(
username=username,
user_id=user_id,
companies=company_ids,
permissions=permissions,
server_id=request.server_id
)
refresh_token = jwt_handler.create_refresh_token(
username=username,
user_id=user_id,
server_id=request.server_id
)
return TelegramEmailLoginResponse(
success=True,
access_token=access_token,
refresh_token=refresh_token,
user_id=user_id,
username=username,
companies=companies,
message="Autentificare reușită"
)
except HTTPException:
raise
except Exception as e:
print(f"Error in login_with_email: {e}")
raise HTTPException(
status_code=500,
detail="Eroare internă. Te rugăm să încerci din nou mai târziu."
)
@router.post("/auth/switch-server", response_model=SwitchServerResponse)
async def switch_server_endpoint(
request: SwitchServerRequest,
current_user: CurrentUser = Depends(get_current_user)
):
"""
Schimbă serverul Oracle activ fără re-autentificare.
Flux:
1. Verifică că oracle_username din request == username din JWT (anti-impersonare)
2. Verifică că utilizatorul există pe noul server (fără parolă, prin get_oracle_user_by_username)
3. Generează JWT nou cu noul server_id
4. Returnează tokenurile noi
Securitate: endpoint protejat cu Bearer JWT valid (Depends(get_current_user)).
"""
# Anti-impersonare: utilizatorul poate schimba serverul doar pentru propriul cont
if request.oracle_username.upper() != current_user.username.upper():
raise HTTPException(status_code=403, detail="Acces interzis: username nepotrivit")
# Verifică că utilizatorul există pe noul server
user_data = await get_oracle_user_by_username(request.oracle_username, request.new_server_id)
if not user_data:
return SwitchServerResponse(
success=False,
message=f"Utilizatorul nu există pe serverul {request.new_server_id}"
)
# Dacă parola e furnizată, verifică-o pe noul server înainte de a emite JWT
if request.oracle_password:
try:
async with oracle_pool.get_connection(request.new_server_id) as connection:
with connection.cursor() as cursor:
cursor.execute("""
SELECT pack_drepturi.verificautilizator(:username, :password)
FROM DUAL
""", {
"username": request.oracle_username.upper(),
"password": request.oracle_password
})
verification_result = cursor.fetchone()[0]
if verification_result == -1:
return SwitchServerResponse(
success=False,
message="Parolă incorectă pentru acest server"
)
except Exception as e:
logger.error(f"Password verification error during server switch: {e}")
return SwitchServerResponse(
success=False,
message="Eroare la verificarea parolei pe noul server"
)
# Generează JWT nou cu noul server_id
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'],
server_id=request.new_server_id
)
refresh_token = jwt_handler.create_refresh_token(
username=user_data['username'],
user_id=user_data['user_id'],
server_id=request.new_server_id
)
return SwitchServerResponse(
success=True,
access_token=access_token,
refresh_token=refresh_token,
message="Server schimbat cu succes"
)
@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()
}