Complete implementation of multi-server Oracle database support: Backend: - Multi-pool Oracle with lazy loading per server - Email-to-server cache for automatic server discovery - JWT tokens include server_id claim - /auth/check-identity and /auth/check-email endpoints - /auth/my-servers endpoint for listing user's accessible servers - Server switch with password re-authentication Frontend: - New ServerSelector component for header dropdown - Multi-step login flow (identity → server → password) - Server switching from header with password modal - Mobile drawer menu with server selection - Dark mode support for all new components - URL bookmark support with ?server= query param Scripts: - Unified start.sh replacing start-prod.sh/start-test.sh - Unified ssh-tunnel.sh with multi-server support - Updated status.sh for new architecture Tests: - E2E tests for multi-server and single-server login flows - Backend unit tests for all new endpoints - Oracle multi-pool integration tests Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
186 lines
6.6 KiB
Python
186 lines
6.6 KiB
Python
"""
|
|
Shared Companies Router Factory for ROA2WEB Applications
|
|
|
|
Creates a FastAPI router for /api/companies endpoints that can be used
|
|
by both the unified monolith backend.
|
|
|
|
Usage:
|
|
from shared.routes.companies import create_companies_router
|
|
|
|
companies_router = create_companies_router(oracle_pool, cache_decorator=cached)
|
|
app.include_router(companies_router, prefix="/api/companies")
|
|
"""
|
|
|
|
import logging
|
|
from typing import Optional, Callable, List
|
|
|
|
from fastapi import APIRouter, Depends, HTTPException, Request
|
|
|
|
from auth.dependencies import get_current_user
|
|
from auth.models import CurrentUser
|
|
from models.company import Company, CompanyListResponse
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
def create_companies_router(
|
|
oracle_pool,
|
|
cache_decorator: Optional[Callable] = None,
|
|
tags: Optional[List[str]] = None
|
|
) -> APIRouter:
|
|
"""
|
|
Factory function to create a companies router.
|
|
|
|
Args:
|
|
oracle_pool: The Oracle connection pool instance
|
|
cache_decorator: Optional caching decorator (e.g., @cached)
|
|
tags: OpenAPI tags for the router
|
|
|
|
Returns:
|
|
Configured FastAPI router for company endpoints
|
|
"""
|
|
router = APIRouter(
|
|
redirect_slashes=False,
|
|
tags=tags or ["companies"]
|
|
)
|
|
|
|
# Helper function to get companies - can be cached
|
|
async def _get_user_companies_data(username: str, server_id: Optional[str] = None) -> List[Company]:
|
|
"""
|
|
Get list of companies for a user from Oracle.
|
|
|
|
Args:
|
|
username: The username to get companies for
|
|
server_id: The Oracle server ID (for multi-server mode)
|
|
"""
|
|
companies = []
|
|
|
|
async with oracle_pool.get_connection(server_id) as connection:
|
|
with connection.cursor() as cursor:
|
|
try:
|
|
# Get user ID
|
|
cursor.execute("""
|
|
SELECT ID_UTIL, UTILIZATOR
|
|
FROM UTILIZATORI
|
|
WHERE UPPER(UTILIZATOR) = :username
|
|
""", {'username': username.upper()})
|
|
|
|
user_row = cursor.fetchone()
|
|
if not user_row:
|
|
logger.warning(f"User {username} not found in UTILIZATORI")
|
|
return []
|
|
|
|
user_id = user_row[0]
|
|
|
|
# Get companies for user (program 2 = data entry/reports)
|
|
cursor.execute("""
|
|
SELECT A.ID_FIRMA, A.FIRMA, A.SCHEMA, A.COD_FISCAL
|
|
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})
|
|
|
|
for row in cursor.fetchall():
|
|
companies.append(Company(
|
|
id_firma=row[0],
|
|
name=row[1],
|
|
schema_name=row[2],
|
|
fiscal_code=row[3],
|
|
is_active=True
|
|
))
|
|
|
|
logger.info(f"Found {len(companies)} companies for user {username}")
|
|
|
|
except Exception as e:
|
|
logger.error(f"Error fetching companies: {e}")
|
|
|
|
return companies
|
|
|
|
# Apply cache decorator if provided
|
|
# Include server_id in cache key for multi-server mode
|
|
if cache_decorator:
|
|
_get_user_companies_data = cache_decorator(
|
|
cache_type='companies',
|
|
key_params=['username', 'server_id']
|
|
)(_get_user_companies_data)
|
|
|
|
@router.get("", response_model=CompanyListResponse)
|
|
@router.get("/", response_model=CompanyListResponse)
|
|
async def get_user_companies(
|
|
request: Request,
|
|
current_user: CurrentUser = Depends(get_current_user)
|
|
):
|
|
"""Get list of companies the user has access to."""
|
|
try:
|
|
# Get server_id from request state (injected by auth middleware from JWT)
|
|
server_id = getattr(request.state, 'server_id', None)
|
|
companies = await _get_user_companies_data(current_user.username, server_id)
|
|
|
|
return CompanyListResponse(
|
|
companies=companies,
|
|
total_count=len(companies)
|
|
)
|
|
except Exception as e:
|
|
logger.error(f"Error in get_user_companies: {e}")
|
|
raise HTTPException(500, f"Eroare la obținerea listei de firme: {str(e)}")
|
|
|
|
@router.get("/{company_id}", response_model=Company)
|
|
async def get_company_details(
|
|
company_id: str,
|
|
request: Request,
|
|
current_user: CurrentUser = Depends(get_current_user)
|
|
):
|
|
"""Get details of a specific company."""
|
|
# Validate access
|
|
if company_id not in current_user.companies:
|
|
raise HTTPException(403, f"Nu aveți acces la firma {company_id}")
|
|
|
|
try:
|
|
# Get server_id from request state (injected by auth middleware from JWT)
|
|
server_id = getattr(request.state, 'server_id', None)
|
|
async with oracle_pool.get_connection(server_id) as connection:
|
|
with connection.cursor() as cursor:
|
|
cursor.execute("""
|
|
SELECT ID_FIRMA, FIRMA, SCHEMA, COD_FISCAL
|
|
FROM V_NOM_FIRME
|
|
WHERE ID_FIRMA = :company_id
|
|
""", {'company_id': int(company_id)})
|
|
row = cursor.fetchone()
|
|
|
|
if not row:
|
|
raise HTTPException(404, f"Firma {company_id} nu a fost găsită")
|
|
|
|
return Company(
|
|
id_firma=row[0],
|
|
name=row[1],
|
|
schema_name=row[2],
|
|
fiscal_code=row[3] or "",
|
|
is_active=True
|
|
)
|
|
|
|
except HTTPException:
|
|
raise
|
|
except Exception as e:
|
|
raise HTTPException(500, f"Eroare la obținerea detaliilor firmei: {str(e)}")
|
|
|
|
@router.get("/{company_id}/validate")
|
|
async def validate_company_access(
|
|
company_id: str,
|
|
current_user: CurrentUser = Depends(get_current_user)
|
|
):
|
|
"""Validate if user has access to a company."""
|
|
has_access = company_id in current_user.companies
|
|
|
|
return {
|
|
"company_id": company_id,
|
|
"has_access": has_access,
|
|
"user": current_user.username,
|
|
"message": "Acces validat" if has_access else "Acces refuzat"
|
|
}
|
|
|
|
return router
|