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:
32
backend/modules/telegram/routers/__init__.py
Normal file
32
backend/modules/telegram/routers/__init__.py
Normal 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
|
||||
840
backend/modules/telegram/routers/auth_codes.py
Normal file
840
backend/modules/telegram/routers/auth_codes.py
Normal 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()
|
||||
}
|
||||
353
backend/modules/telegram/routers/internal_api.py
Normal file
353
backend/modules/telegram/routers/internal_api.py
Normal 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']
|
||||
Reference in New Issue
Block a user