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>
This commit is contained in:
@@ -2,7 +2,7 @@
|
||||
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 fastapi import APIRouter, Body, Depends, HTTPException, Request
|
||||
from typing import List, Optional, Dict, Any
|
||||
# import sys # Removed - no longer needed
|
||||
import os
|
||||
@@ -57,6 +57,7 @@ class VerifyUserRequest(BaseModel):
|
||||
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):
|
||||
@@ -100,6 +101,7 @@ class ExportReportResponse(BaseModel):
|
||||
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):
|
||||
@@ -115,6 +117,7 @@ class TelegramEmailLoginRequest(BaseModel):
|
||||
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):
|
||||
@@ -129,6 +132,21 @@ class TelegramEmailLoginResponse(BaseModel):
|
||||
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)
|
||||
@@ -212,7 +230,7 @@ def generate_linking_code(length: int = 8) -> str:
|
||||
return ''.join(secrets.choice(alphabet) for _ in range(length))
|
||||
|
||||
|
||||
async def get_oracle_user_by_username(username: str) -> Optional[Dict[str, Any]]:
|
||||
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ă.
|
||||
|
||||
@@ -221,12 +239,13 @@ async def get_oracle_user_by_username(username: str) -> Optional[Dict[str, Any]]
|
||||
|
||||
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() as connection:
|
||||
async with oracle_pool.get_connection(server_id) as connection:
|
||||
with connection.cursor() as cursor:
|
||||
# Obține detalii utilizator
|
||||
cursor.execute("""
|
||||
@@ -270,19 +289,20 @@ async def get_oracle_user_by_username(username: str) -> Optional[Dict[str, Any]]
|
||||
return None
|
||||
|
||||
|
||||
async def verify_oracle_user(username: str, password: str) -> Optional[Dict[str, Any]]:
|
||||
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() as connection:
|
||||
async with oracle_pool.get_connection(server_id) as connection:
|
||||
with connection.cursor() as cursor:
|
||||
# Verifică autentificarea
|
||||
cursor.execute("""
|
||||
@@ -344,6 +364,7 @@ async def verify_oracle_user(username: str, password: str) -> Optional[Dict[str,
|
||||
|
||||
@router.post("/auth/generate-code", response_model=GenerateCodeResponse)
|
||||
async def generate_linking_code_endpoint(
|
||||
request: Request,
|
||||
current_user: CurrentUser = Depends(get_current_user)
|
||||
):
|
||||
"""
|
||||
@@ -360,8 +381,12 @@ async def generate_linking_code_endpoint(
|
||||
- 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()
|
||||
|
||||
@@ -378,7 +403,8 @@ async def generate_linking_code_endpoint(
|
||||
"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
|
||||
"expires_in_minutes": expires_in_minutes,
|
||||
"server_id": server_id
|
||||
}
|
||||
)
|
||||
|
||||
@@ -442,7 +468,7 @@ async def verify_user_endpoint(request: VerifyUserRequest):
|
||||
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)
|
||||
user_data = await get_oracle_user_by_username(request.oracle_username, server_id=request.server_id)
|
||||
|
||||
if not user_data:
|
||||
return VerifyUserResponse(
|
||||
@@ -452,7 +478,7 @@ async def verify_user_endpoint(request: VerifyUserRequest):
|
||||
|
||||
# Flow B: Full verification (username + password provided)
|
||||
elif request.username and request.password:
|
||||
user_data = await verify_oracle_user(request.username, request.password)
|
||||
user_data = await verify_oracle_user(request.username, request.password, server_id=request.server_id)
|
||||
|
||||
if not user_data:
|
||||
return VerifyUserResponse(
|
||||
@@ -467,17 +493,19 @@ async def verify_user_endpoint(request: VerifyUserRequest):
|
||||
message="Trebuie furnizat fie oracle_username (auto-linking) fie username+password (verificare completă)"
|
||||
)
|
||||
|
||||
# Generează JWT tokens
|
||||
# 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']
|
||||
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']
|
||||
user_id=user_data['user_id'],
|
||||
server_id=request.server_id
|
||||
)
|
||||
|
||||
return VerifyUserResponse(
|
||||
@@ -528,8 +556,8 @@ async def refresh_token_endpoint(request: RefreshTokenRequest):
|
||||
detail="Refresh token invalid sau expirat"
|
||||
)
|
||||
|
||||
# Obține companiile actualizate din Oracle
|
||||
async with oracle_pool.get_connection() as connection:
|
||||
# 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
|
||||
@@ -546,12 +574,13 @@ async def refresh_token_endpoint(request: RefreshTokenRequest):
|
||||
companies_result = cursor.fetchall()
|
||||
companies = [str(row[0]) for row in companies_result]
|
||||
|
||||
# Generează nou access token
|
||||
# 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
|
||||
permissions=token_data.permissions,
|
||||
server_id=token_data.server_id
|
||||
)
|
||||
|
||||
return RefreshTokenResponse(
|
||||
@@ -580,7 +609,7 @@ async def verify_email_endpoint(request: VerifyEmailRequest):
|
||||
Security: Generic error messages to prevent email enumeration.
|
||||
"""
|
||||
try:
|
||||
async with oracle_pool.get_connection() as connection:
|
||||
async with oracle_pool.get_connection(request.server_id) as connection:
|
||||
with connection.cursor() as cursor:
|
||||
# Query to find username by email
|
||||
cursor.execute("""
|
||||
@@ -649,7 +678,7 @@ async def login_with_email_endpoint(request: TelegramEmailLoginRequest):
|
||||
)
|
||||
|
||||
try:
|
||||
async with oracle_pool.get_connection() as connection:
|
||||
async with oracle_pool.get_connection(request.server_id) as connection:
|
||||
with connection.cursor() as cursor:
|
||||
# 3. Find username by email
|
||||
cursor.execute("""
|
||||
@@ -719,18 +748,18 @@ async def login_with_email_endpoint(request: TelegramEmailLoginRequest):
|
||||
# 6. Get user permissions (default for Telegram)
|
||||
permissions = ['read', 'reports']
|
||||
|
||||
# 7. Generate JWT tokens
|
||||
token_data = {
|
||||
"username": username,
|
||||
"user_id": user_id,
|
||||
"companies": company_ids,
|
||||
"permissions": permissions
|
||||
}
|
||||
|
||||
access_token = jwt_handler.create_access_token(**token_data)
|
||||
# 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
|
||||
user_id=user_id,
|
||||
server_id=request.server_id
|
||||
)
|
||||
|
||||
return TelegramEmailLoginResponse(
|
||||
@@ -754,6 +783,82 @@ async def login_with_email_endpoint(request: TelegramEmailLoginRequest):
|
||||
)
|
||||
|
||||
|
||||
@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,
|
||||
|
||||
Reference in New Issue
Block a user