Cache System (Backend): - Implemented two-tier hybrid cache: L1 (in-memory) + L2 (SQLite) - L1 cache: Fast dictionary-based with 5-minute TTL for hot data - L2 cache: Persistent SQLite with 1-hour TTL for warm data - Cache decorator with automatic tier management and fallback - Cache key generation with per-user isolation - Event monitoring system for cache statistics - Cache benchmarking utilities for performance testing - Added cache management endpoints: /api/cache/stats, /api/cache/clear, /api/cache/benchmark - Cache configuration via environment variables (CACHE_ENABLED, CACHE_L1_TTL, etc.) Backend Services: - Updated dashboard_service to use @cached decorator with request context - Added cache support to invoice_service and treasury_service - Integrated cache manager into main.py with lifespan events - Added Request parameter to service methods for cache metadata Frontend Enhancements: - New CacheStatsView.vue for real-time cache monitoring dashboard - Cache store (cacheStore.js) for state management - Updated router to include /cache-stats route - Navigation updates in DashboardHeader and HamburgerMenu - Cache stats accessible from main navigation Telegram Bot Improvements: - Enhanced formatters with YTD comparison data - Improved menu navigation and button layout - Better error handling and user feedback - Bot startup improvements with graceful shutdown Auth & Middleware: - Enhanced middleware with cache metadata injection - Improved request state handling for cache source tracking Development: - Updated start-dev.sh with better error handling - Added TELEGRAM_EMAIL_AUTH_PLAN.md documentation - Updated requirements.txt with aiosqlite for async SQLite Performance: - L1 cache provides <1ms response for hot data - L2 cache provides ~5ms response for warm data - Database queries only for cold data or cache misses - Cache hit rates tracked and displayed in real-time 🤖 Generated with Claude Code Co-Authored-By: Claude <noreply@anthropic.com>
333 lines
13 KiB
Python
333 lines
13 KiB
Python
"""
|
|
ROA Reports API - FastAPI Backend
|
|
Aplicația principală pentru rapoarte facturi și încasări
|
|
"""
|
|
from fastapi import FastAPI, Depends
|
|
from fastapi.middleware.cors import CORSMiddleware
|
|
from contextlib import asynccontextmanager
|
|
import sys
|
|
import os
|
|
from datetime import datetime
|
|
from dotenv import load_dotenv
|
|
|
|
# Încărcare environment variables din .env
|
|
load_dotenv()
|
|
|
|
# Configurare TNS_ADMIN pentru Oracle
|
|
tns_path = os.path.join(os.path.dirname(__file__), '../../../../app')
|
|
os.environ['TNS_ADMIN'] = tns_path
|
|
|
|
# Adăugare path pentru shared modules
|
|
sys.path.append(os.path.join(os.path.dirname(__file__), '../../../shared'))
|
|
|
|
from database.oracle_pool import oracle_pool
|
|
from auth.middleware import AuthenticationMiddleware
|
|
# from auth.routes import create_auth_router # Fixed inline
|
|
|
|
# Import routere locale
|
|
from app.routers import invoices, dashboard, treasury, companies, telegram, cache
|
|
|
|
# Auth endpoints pentru test
|
|
from fastapi import APIRouter, HTTPException
|
|
from pydantic import BaseModel
|
|
from datetime import datetime, timedelta
|
|
import jwt
|
|
import logging
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
# JWT Setup
|
|
JWT_SECRET = os.getenv("JWT_SECRET_KEY", "test-secret-key")
|
|
JWT_ALGORITHM = "HS256"
|
|
JWT_EXPIRE_MINUTES = 30
|
|
|
|
class LoginRequest(BaseModel):
|
|
username: str
|
|
password: str
|
|
|
|
class LoginResponse(BaseModel):
|
|
access_token: str
|
|
refresh_token: str # Added refresh token
|
|
token_type: str
|
|
user: dict
|
|
|
|
def create_auth_router():
|
|
"""Create authentication router for testing"""
|
|
auth_router = APIRouter(tags=["authentication"])
|
|
|
|
@auth_router.post("/login", response_model=LoginResponse)
|
|
async def login(credentials: LoginRequest):
|
|
"""Autentificare utilizator prin Oracle database"""
|
|
try:
|
|
async with oracle_pool.get_connection() as connection:
|
|
with connection.cursor() as cursor:
|
|
# Call verificautilizator procedure using SELECT
|
|
cursor.execute("""
|
|
SELECT pack_drepturi.verificautilizator(:username, :password)
|
|
FROM DUAL
|
|
""", {
|
|
'username': credentials.username.upper(),
|
|
'password': credentials.password
|
|
})
|
|
|
|
result = cursor.fetchone()
|
|
verification_result = result[0] if result else -1
|
|
|
|
# Check if authentication was successful
|
|
if verification_result == -1:
|
|
raise HTTPException(status_code=401, detail="Invalid username or password")
|
|
|
|
# Get user companies - first get user ID from UTILIZATORI
|
|
cursor.execute("""
|
|
SELECT ID_UTIL, UTILIZATOR
|
|
FROM UTILIZATORI
|
|
WHERE UPPER(UTILIZATOR) = :username
|
|
""", {'username': credentials.username.upper()})
|
|
|
|
user_row = cursor.fetchone()
|
|
if not user_row:
|
|
raise HTTPException(status_code=401, detail="User not found in system")
|
|
|
|
user_id = user_row[0]
|
|
|
|
# Now get companies using the correct query structure
|
|
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()
|
|
|
|
if not companies_result:
|
|
# Don't fail login if no companies - let frontend show message
|
|
companies = []
|
|
else:
|
|
companies = [str(row[0]) for row in companies_result]
|
|
|
|
# Create JWT token with all required fields
|
|
now = datetime.utcnow()
|
|
expire = now + timedelta(minutes=JWT_EXPIRE_MINUTES)
|
|
token_data = {
|
|
"username": credentials.username, # Changed from "sub" to "username"
|
|
"user_id": user_id, # Include user_id from database
|
|
"companies": companies,
|
|
"permissions": ["read", "reports"], # Default permissions
|
|
"exp": expire,
|
|
"iat": now, # Added issued at time
|
|
"type": "access" # Added token type
|
|
}
|
|
access_token = jwt.encode(token_data, JWT_SECRET, algorithm=JWT_ALGORITHM)
|
|
|
|
# Create refresh token
|
|
refresh_expire = now + timedelta(days=7)
|
|
refresh_token_data = {
|
|
"username": credentials.username,
|
|
"user_id": user_id,
|
|
"companies": companies,
|
|
"permissions": ["read", "reports"],
|
|
"exp": refresh_expire,
|
|
"iat": now,
|
|
"type": "refresh"
|
|
}
|
|
refresh_token = jwt.encode(refresh_token_data, JWT_SECRET, algorithm=JWT_ALGORITHM)
|
|
|
|
return LoginResponse(
|
|
access_token=access_token,
|
|
refresh_token=refresh_token, # Include refresh token
|
|
token_type="bearer",
|
|
user={
|
|
"username": credentials.username,
|
|
"user_id": user_id, # Include user_id
|
|
"companies": companies,
|
|
"permissions": ["read", "reports"] # Include permissions
|
|
}
|
|
)
|
|
|
|
except Exception as e:
|
|
logger.error(f"Login error: {str(e)}")
|
|
raise HTTPException(status_code=500, detail="Internal authentication error")
|
|
|
|
return auth_router
|
|
|
|
@asynccontextmanager
|
|
async def lifespan(app: FastAPI):
|
|
"""Lifecycle events pentru aplicație"""
|
|
# Startup - Initialize Oracle connection pool
|
|
await oracle_pool.initialize()
|
|
logger.info("[ROA Reports API] Oracle pool initialized")
|
|
|
|
# Initialize cache system
|
|
from app.cache import init_cache, run_baseline_benchmarks, init_event_monitor, get_cache
|
|
from app.cache.config import CacheConfig
|
|
|
|
try:
|
|
cache_config = CacheConfig.from_env()
|
|
await init_cache(cache_config)
|
|
logger.info(f"[ROA Reports API] Cache initialized: type={cache_config.cache_type}, enabled={cache_config.enabled}")
|
|
|
|
# Run baseline benchmarks (optional, based on config)
|
|
if cache_config.benchmark_on_startup:
|
|
logger.info("[ROA Reports API] Running baseline performance benchmarks...")
|
|
benchmarks = await run_baseline_benchmarks()
|
|
logger.info(f"[ROA Reports API] Benchmarks completed: {len(benchmarks)} types measured")
|
|
|
|
# Initialize event monitor
|
|
cache = get_cache()
|
|
await init_event_monitor(cache, cache_config)
|
|
if cache_config.auto_invalidate_enabled:
|
|
logger.info("[ROA Reports API] Event-based auto-invalidation ENABLED")
|
|
else:
|
|
logger.info("[ROA Reports API] Event-based auto-invalidation DISABLED")
|
|
|
|
except Exception as e:
|
|
logger.error(f"[ROA Reports API] Cache initialization error: {e}", exc_info=True)
|
|
logger.warning("[ROA Reports API] Continuing without cache")
|
|
|
|
logger.info("[ROA Reports API] Started successfully")
|
|
|
|
yield
|
|
|
|
# Shutdown
|
|
from app.cache import close_cache, get_event_monitor
|
|
|
|
# Stop event monitor
|
|
try:
|
|
monitor = get_event_monitor()
|
|
if monitor:
|
|
await monitor.stop()
|
|
logger.info("[ROA Reports API] Event monitor stopped")
|
|
except Exception as e:
|
|
logger.error(f"[ROA Reports API] Event monitor shutdown error: {e}")
|
|
|
|
# Close cache
|
|
try:
|
|
await close_cache()
|
|
logger.info("[ROA Reports API] Cache closed")
|
|
except Exception as e:
|
|
logger.error(f"[ROA Reports API] Cache shutdown error: {e}")
|
|
|
|
await oracle_pool.close_pool()
|
|
logger.info("[ROA Reports API] Stopped")
|
|
|
|
app = FastAPI(
|
|
title="ROA Reports API",
|
|
description="API pentru rapoarte ERP - facturi, încasări și alte rapoarte financiare",
|
|
version="1.0.0",
|
|
# lifespan=lifespan # Using event handlers instead due to uvicorn compatibility issues
|
|
)
|
|
|
|
# STARTUP EVENT HANDLER (alternative to lifespan)
|
|
@app.on_event("startup")
|
|
async def startup_event():
|
|
"""Application startup - Initialize Oracle pool and cache"""
|
|
print("=" * 80, flush=True)
|
|
print("[STARTUP] Initializing Oracle pool...", flush=True)
|
|
logger.critical("=" * 80)
|
|
logger.critical("[STARTUP] Initializing Oracle pool...")
|
|
await oracle_pool.initialize()
|
|
print("[STARTUP] Oracle pool initialized", flush=True)
|
|
logger.critical("[STARTUP] Oracle pool initialized")
|
|
|
|
print("[STARTUP] Initializing cache system...", flush=True)
|
|
logger.critical("[STARTUP] Initializing cache system...")
|
|
from app.cache import init_cache, init_event_monitor, get_cache
|
|
from app.cache.config import CacheConfig
|
|
|
|
try:
|
|
cache_config = CacheConfig.from_env()
|
|
await init_cache(cache_config)
|
|
print(f"[STARTUP] Cache initialized: type={cache_config.cache_type}, enabled={cache_config.enabled}", flush=True)
|
|
logger.critical(f"[STARTUP] Cache initialized: type={cache_config.cache_type}, enabled={cache_config.enabled}")
|
|
|
|
# Initialize event monitor
|
|
cache = get_cache()
|
|
await init_event_monitor(cache, cache_config)
|
|
if cache_config.auto_invalidate_enabled:
|
|
logger.info("[STARTUP] Event-based auto-invalidation ENABLED")
|
|
else:
|
|
logger.info("[STARTUP] Event-based auto-invalidation DISABLED")
|
|
|
|
except Exception as e:
|
|
logger.error(f"[STARTUP] Cache initialization error: {e}", exc_info=True)
|
|
logger.warning("[STARTUP] Continuing without cache")
|
|
|
|
logger.info("[STARTUP] ROA Reports API started successfully")
|
|
logger.info("=" * 80)
|
|
|
|
# SHUTDOWN EVENT HANDLER
|
|
@app.on_event("shutdown")
|
|
async def shutdown_event():
|
|
"""Application shutdown - Cleanup resources"""
|
|
logger.info("[SHUTDOWN] Stopping event monitor...")
|
|
from app.cache import close_cache, get_event_monitor
|
|
|
|
try:
|
|
monitor = get_event_monitor()
|
|
if monitor:
|
|
await monitor.stop()
|
|
logger.info("[SHUTDOWN] Event monitor stopped")
|
|
except Exception as e:
|
|
logger.error(f"[SHUTDOWN] Event monitor error: {e}")
|
|
|
|
try:
|
|
await close_cache()
|
|
logger.info("[SHUTDOWN] Cache closed")
|
|
except Exception as e:
|
|
logger.error(f"[SHUTDOWN] Cache error: {e}")
|
|
|
|
await oracle_pool.close_pool()
|
|
logger.info("[SHUTDOWN] Oracle pool closed")
|
|
logger.info("[SHUTDOWN] ROA Reports API stopped")
|
|
|
|
# CORS pentru frontend Vue.js
|
|
app.add_middleware(
|
|
CORSMiddleware,
|
|
allow_origins=["*"], # Allow all origins for production deployment
|
|
allow_credentials=True,
|
|
allow_methods=["*"],
|
|
allow_headers=["*"],
|
|
)
|
|
|
|
# Authentication middleware
|
|
app.add_middleware(
|
|
AuthenticationMiddleware,
|
|
excluded_paths=[
|
|
"/", "/docs", "/health", "/api/auth/login", "/redoc", "/openapi.json",
|
|
"/api/telegram/auth/verify-user", # Public endpoint for Telegram bot
|
|
"/api/telegram/auth/refresh-token", # Public endpoint for token refresh
|
|
"/api/telegram/health" # Health check for Telegram router
|
|
]
|
|
)
|
|
|
|
# Include routere with /api prefix
|
|
auth_router = create_auth_router()
|
|
app.include_router(auth_router, prefix="/api/auth", tags=["authentication"])
|
|
app.include_router(companies.router, prefix="/api/companies", tags=["companies"])
|
|
app.include_router(invoices.router, prefix="/api/invoices", tags=["invoices"])
|
|
app.include_router(dashboard.router, prefix="/api/dashboard", tags=["dashboard"])
|
|
app.include_router(treasury.router, prefix="/api/treasury", tags=["treasury"])
|
|
app.include_router(telegram.router, prefix="/api/telegram", tags=["telegram"])
|
|
app.include_router(cache.router, prefix="/api", tags=["cache"])
|
|
|
|
@app.get("/")
|
|
async def root():
|
|
print("[MAIN DEBUG] Root endpoint accessed")
|
|
return {"message": "ROA Reports API", "version": "1.0.0", "status": "running"}
|
|
|
|
@app.get("/health")
|
|
async def health_check():
|
|
# Test database connection
|
|
try:
|
|
async with oracle_pool.get_connection() as conn:
|
|
with conn.cursor() as cursor:
|
|
cursor.execute("SELECT 1 FROM DUAL")
|
|
return {"api": "healthy", "database": "connected", "timestamp": datetime.utcnow().isoformat()}
|
|
except Exception as e:
|
|
return {"api": "healthy", "database": f"error: {str(e)}", "timestamp": datetime.utcnow().isoformat()} |