feat: Migrate to ultrathin monolith architecture

Consolidate 3 separate applications (reports-app, data-entry-app, telegram-bot) into a unified
architecture with single backend and frontend:

Backend Changes:
- Unified FastAPI backend at backend/ with modular structure
- Modules: reports, data_entry, telegram in backend/modules/
- Centralized config.py and main.py with all routers registered
- Single worker mode (--workers 1) for Telegram bot compatibility
- Shared Oracle connection pool and JWT authentication
- Unified requirements.txt and environment configuration

Frontend Changes:
- Single Vue.js SPA with module-based routing
- Unified frontend at src/ with modules in src/modules/{reports,data-entry}/
- Shared components and stores in src/shared/
- Error boundaries for module isolation
- Dual API proxy in Vite for module communication

Infrastructure:
- New unified startup scripts: start-prod.sh, start-test.sh, start-backend.sh
- Environment templates: .env.dev.example, .env.test.example, .env.prod.example
- Updated deployment scripts for Windows IIS
- Simplified SSH tunnel management

Documentation:
- Comprehensive CLAUDE.md with architecture overview
- Module-specific docs in docs/{data-entry,telegram}/
- Architecture decision records in docs/ARCHITECTURE-DECISIONS.md
- Deployment guides consolidated in deployment/windows/docs/

This migration reduces complexity, improves maintainability, and enables easier
deployment while maintaining all existing functionality.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
2025-12-29 23:48:14 +02:00
parent 2a101f1ef5
commit c5e051ad80
378 changed files with 7566 additions and 73730 deletions

View File

@@ -0,0 +1,32 @@
"""Telegram module router factory."""
from fastapi import APIRouter
def create_telegram_router() -> APIRouter:
"""
Create and configure Telegram module router.
Includes all Telegram bot internal API endpoints:
- /auth/verify-user - Verify Telegram user authentication
- /auth/generate-code - Generate auth code for linking
- /auth/verify-code - Verify auth code
- /stats - Bot database statistics
Returns:
APIRouter: Configured router for Telegram module
"""
router = APIRouter()
# Import routers here to avoid circular imports
from .auth_codes import router as auth_codes_router
from .internal_api import internal_api as internal_api_router
# Include all sub-routers (no prefix - already prefixed in main.py with /api/telegram)
# Auth codes router provides /auth/* endpoints
router.include_router(auth_codes_router, tags=["telegram-auth"])
# Internal API router provides additional endpoints like /stats
router.include_router(internal_api_router, tags=["telegram-internal"])
return router

View File

@@ -0,0 +1,840 @@
"""
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 # 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: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")
class VerifyEmailRequest(BaseModel):
"""Request pentru verificarea email-ului în Oracle"""
email: str = Field(description="Adresa de email Oracle")
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")
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")
# ==================== 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) -> 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("/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() 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() 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
token_data = {
"username": username,
"user_id": user_id,
"companies": company_ids,
"permissions": permissions
}
access_token = jwt_handler.create_access_token(**token_data)
refresh_token = jwt_handler.create_refresh_token(
username=username,
user_id=user_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("/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()
}

View File

@@ -0,0 +1,353 @@
"""
Internal API for Backend Communication
This FastAPI application provides internal endpoints for the ROA2WEB backend
to communicate with the Telegram bot service. Main purpose is to save
authentication codes generated in the web frontend.
This API runs alongside the Telegram bot and is accessible only internally
(not exposed to public internet).
"""
import logging
import os
from datetime import datetime
from typing import Optional
from fastapi import APIRouter, HTTPException, status
from fastapi.responses import JSONResponse
from pydantic import BaseModel, Field
from backend.modules.telegram.db.operations import create_auth_code, get_auth_code
from backend.modules.telegram.db.database import get_database_stats
logger = logging.getLogger(__name__)
# Initialize APIRouter (converted from FastAPI app for unified backend)
internal_api = APIRouter()
# ============================================================================
# REQUEST/RESPONSE MODELS
# ============================================================================
class SaveAuthCodeRequest(BaseModel):
"""
Request model for saving an authentication code.
"""
code: str = Field(
...,
description="8-character authentication code",
min_length=8,
max_length=8
)
telegram_user_id: int = Field(
...,
description="Telegram user ID (if known, otherwise 0)",
ge=0
)
oracle_username: str = Field(
...,
description="Oracle username to link"
)
expires_in_minutes: int = Field(
default=5,
description="Code expiration time in minutes",
ge=1,
le=60
)
class SaveAuthCodeResponse(BaseModel):
"""
Response model for save auth code endpoint.
"""
success: bool = Field(..., description="Whether the operation succeeded")
code: str = Field(..., description="The saved authentication code")
expires_at: Optional[str] = Field(None, description="Expiration timestamp (ISO format)")
message: Optional[str] = Field(None, description="Additional message")
class VerifyAuthCodeRequest(BaseModel):
"""
Request model for verifying an authentication code.
"""
code: str = Field(..., description="Authentication code to verify")
class VerifyAuthCodeResponse(BaseModel):
"""
Response model for verify auth code endpoint.
"""
valid: bool = Field(..., description="Whether the code is valid")
oracle_username: Optional[str] = Field(None, description="Oracle username if valid")
telegram_user_id: Optional[int] = Field(None, description="Telegram user ID if set")
message: Optional[str] = Field(None, description="Additional message")
class HealthResponse(BaseModel):
"""
Response model for health check endpoint.
"""
status: str = Field(..., description="Service status")
timestamp: str = Field(..., description="Current timestamp")
database_stats: Optional[dict] = Field(None, description="Database statistics")
# ============================================================================
# ENDPOINTS
# ============================================================================
@internal_api.post(
"/internal/save-code",
response_model=SaveAuthCodeResponse,
status_code=status.HTTP_201_CREATED,
summary="Save Authentication Code",
description="Save an authentication code for Telegram linking (called by backend)"
)
async def save_auth_code(request: SaveAuthCodeRequest):
"""
Save an authentication code to SQLite database.
This endpoint is called by the FastAPI backend when a user generates
a linking code in the web frontend.
**Flow:**
1. User logs in to web frontend
2. User clicks "Link Telegram Account"
3. Backend generates 8-character code
4. Backend calls this endpoint to save code
5. Backend returns code to user for display
6. User sends code to Telegram bot via /start command
Args:
request: SaveAuthCodeRequest with code, oracle_username, etc.
Returns:
SaveAuthCodeResponse with success status and code details
Raises:
HTTPException 400: If code already exists or invalid data
HTTPException 500: If database operation fails
"""
try:
logger.info(
f"Saving auth code for Oracle user: {request.oracle_username}, "
f"code: {request.code}"
)
# Check if code already exists
existing_code = await get_auth_code(request.code)
if existing_code:
logger.warning(f"Code {request.code} already exists")
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail=f"Code {request.code} already exists. Generate a new unique code."
)
# Create auth code in database
success = await create_auth_code(
code=request.code,
telegram_user_id=request.telegram_user_id,
oracle_username=request.oracle_username,
expires_in_minutes=request.expires_in_minutes
)
if not success:
logger.error(f"Failed to save auth code {request.code}")
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail="Failed to save authentication code to database"
)
# Calculate expiration time
from datetime import timedelta
expires_at = (datetime.now() + timedelta(minutes=request.expires_in_minutes)).isoformat()
logger.info(f"Auth code {request.code} saved successfully")
return SaveAuthCodeResponse(
success=True,
code=request.code,
expires_at=expires_at,
message=f"Code saved successfully, expires in {request.expires_in_minutes} minutes"
)
except HTTPException:
raise
except Exception as e:
logger.error(f"Error saving auth code: {e}", exc_info=True)
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail=f"Internal server error: {str(e)}"
)
@internal_api.post(
"/internal/verify-code",
response_model=VerifyAuthCodeResponse,
summary="Verify Authentication Code",
description="Verify if an authentication code is valid (without using it)"
)
async def verify_auth_code(request: VerifyAuthCodeRequest):
"""
Verify if an authentication code exists and is valid.
This is a read-only check that does NOT mark the code as used.
Useful for backend to verify codes before user links Telegram account.
Args:
request: VerifyAuthCodeRequest with code to verify
Returns:
VerifyAuthCodeResponse with validation status
Raises:
HTTPException 404: If code not found
"""
try:
logger.info(f"Verifying auth code: {request.code}")
code_data = await get_auth_code(request.code)
if not code_data:
return VerifyAuthCodeResponse(
valid=False,
message="Code not found"
)
# Check if code is expired
expires_at_str = code_data.get('expires_at')
expires_at = datetime.fromisoformat(expires_at_str) if expires_at_str else None
is_expired = expires_at and datetime.now() >= expires_at
is_used = code_data.get('used', 0) == 1
if is_expired:
return VerifyAuthCodeResponse(
valid=False,
oracle_username=code_data.get('oracle_username'),
message="Code expired"
)
if is_used:
return VerifyAuthCodeResponse(
valid=False,
oracle_username=code_data.get('oracle_username'),
message="Code already used"
)
# Code is valid
return VerifyAuthCodeResponse(
valid=True,
oracle_username=code_data.get('oracle_username'),
telegram_user_id=code_data.get('telegram_user_id'),
message="Code is valid"
)
except Exception as e:
logger.error(f"Error verifying auth code: {e}", exc_info=True)
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail=f"Internal server error: {str(e)}"
)
@internal_api.get(
"/internal/health",
response_model=HealthResponse,
summary="Health Check",
description="Check if the internal API and database are healthy"
)
async def health_check():
"""
Health check endpoint.
Returns service status and database statistics.
Returns:
HealthResponse with status and stats
"""
try:
# Get database stats
stats = await get_database_stats()
return HealthResponse(
status="healthy",
timestamp=datetime.now().isoformat(),
database_stats=stats
)
except Exception as e:
logger.error(f"Health check failed: {e}", exc_info=True)
return HealthResponse(
status="unhealthy",
timestamp=datetime.now().isoformat(),
database_stats={"error": str(e)}
)
@internal_api.get(
"/internal/stats",
summary="Database Statistics",
description="Get detailed database statistics"
)
async def get_stats():
"""
Get detailed database statistics.
Returns:
JSON with database statistics
"""
try:
stats = await get_database_stats()
return JSONResponse(
status_code=status.HTTP_200_OK,
content={
"success": True,
"timestamp": datetime.now().isoformat(),
"stats": stats
}
)
except Exception as e:
logger.error(f"Error getting stats: {e}", exc_info=True)
return JSONResponse(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
content={
"success": False,
"error": str(e)
}
)
# ============================================================================
# EXCEPTION HANDLERS
# ============================================================================
# ============================================================================
# NOTE: Exception handlers and startup/shutdown events removed
# These are FastAPI-specific and don't work with APIRouter
# The unified backend (main.py) handles these at the app level
# ============================================================================
# @internal_api.exception_handler(Exception) - Not supported by APIRouter
# async def global_exception_handler(request, exc):
# """Global exception handler - moved to main.py"""
# pass
# @internal_api.on_event("startup") - Not supported by APIRouter
# async def startup_event():
# """Startup event - handled by main.py lifespan"""
# pass
# @internal_api.on_event("shutdown") - Not supported by APIRouter
# async def shutdown_event():
# """Shutdown event - handled by main.py lifespan"""
# pass
# Export the APIRouter
__all__ = ['internal_api']