feat: Implement multi-tenancy support in middleware and security layers

- Enhanced TenantMiddleware to validate tenant information from JWT tokens.
- Added LicenseValidationMiddleware to check tenant licenses before processing requests.
- Updated security utilities to extract tenant information from tokens and validate company access.
- Introduced CompanyStore to manage active company state and handle company switching in the frontend.
- Modified API routes to include company_id in requests for better resource management.
- Improved logging and error handling throughout the middleware and API layers.
- Updated frontend components to reflect changes in company management and selection.
- Added new API route for fetching user's companies with proper authentication handling.
This commit is contained in:
2025-11-11 14:00:56 -06:00
parent e1eb6bbd01
commit 52b8fcd434
242 changed files with 7067 additions and 3274 deletions

View File

@@ -1,6 +1,7 @@
"""
Endpoints API para gestión de clases SCAII y SCAF
"""
from fastapi import APIRouter, Depends, HTTPException, Query, status
from sqlalchemy.orm import Session
from typing import List, Optional
@@ -9,28 +10,33 @@ from core.database import get_core_db
from core.security import get_current_user, has_role
from .service import ClassService
from .dto import (
ClassCreateDTO,
ClassUpdateDTO,
ClassCreateDTO,
ClassUpdateDTO,
ClassResponseDTO,
ClassBasicDTO,
ClassListDTO,
ClassSearchDTO
ClassSearchDTO,
)
router = APIRouter(prefix="/classes", tags=["Classes"])
@router.get("/", response_model=ClassListDTO)
async def list_classes(
skip: int = Query(0, ge=0, description="Number of records to skip"),
limit: int = Query(100, ge=1, le=1000, description="Maximum number of records to return"),
limit: int = Query(
100, ge=1, le=1000, description="Maximum number of records to return"
),
client_id: Optional[int] = Query(None, description="Filter by client key"),
class_code: Optional[str] = Query(None, description="Search by class code"),
description: Optional[str] = Query(None, description="Search in descriptions"),
material_key: Optional[str] = Query(None, description="Filter by material key"),
fraction: Optional[str] = Query(None, description="Filter by tariff fraction"),
physical_review: Optional[int] = Query(None, description="Filter by physical review indicator"),
physical_review: Optional[int] = Query(
None, description="Filter by physical review indicator"
),
db: Session = Depends(get_core_db),
current_user: dict = Depends(get_current_user)
current_user: dict = Depends(get_current_user),
):
"""
List classes with optional filters and pagination
@@ -49,7 +55,7 @@ async def list_classes(
description=description,
material_key=material_key,
fraction=fraction,
physical_review=physical_review
physical_review=physical_review,
)
return service.list_classes(skip, limit, search_params)
@@ -60,7 +66,7 @@ async def get_classes_by_client(
skip: int = Query(0, ge=0),
limit: int = Query(100, ge=1, le=1000),
db: Session = Depends(get_core_db),
current_user: dict = Depends(get_current_user)
current_user: dict = Depends(get_current_user),
):
"""
Get all classes for a specific client
@@ -80,7 +86,7 @@ async def get_classes_by_client(
async def search_by_fraction(
fraction: str,
db: Session = Depends(get_core_db),
current_user: dict = Depends(get_current_user)
current_user: dict = Depends(get_current_user),
):
"""
Search classes by tariff fraction
@@ -93,7 +99,7 @@ async def search_by_fraction(
async def search_by_material(
material_key: str,
db: Session = Depends(get_core_db),
current_user: dict = Depends(get_current_user)
current_user: dict = Depends(get_current_user),
):
"""
Search classes by material key
@@ -102,11 +108,13 @@ async def search_by_material(
return service.search_by_material(material_key)
@router.get("/search/unit-measure/{unit_of_measure}", response_model=List[ClassBasicDTO])
@router.get(
"/search/unit-measure/{unit_of_measure}", response_model=List[ClassBasicDTO]
)
async def get_classes_by_unit_measure(
unit_of_measure: str,
db: Session = Depends(get_core_db),
current_user: dict = Depends(get_current_user)
current_user: dict = Depends(get_current_user),
):
"""
Get classes by unit of measure
@@ -115,11 +123,13 @@ async def get_classes_by_unit_measure(
return service.get_classes_by_unit_measure(unit_of_measure)
@router.get("/search/physical-review/{physical_review}", response_model=List[ClassBasicDTO])
@router.get(
"/search/physical-review/{physical_review}", response_model=List[ClassBasicDTO]
)
async def get_classes_by_physical_review(
physical_review: int,
db: Session = Depends(get_core_db),
current_user: dict = Depends(get_current_user)
current_user: dict = Depends(get_current_user),
):
"""
Get classes by physical review indicator
@@ -130,8 +140,7 @@ async def get_classes_by_physical_review(
@router.get("/statistics", response_model=dict)
async def get_classes_statistics(
db: Session = Depends(get_core_db),
current_user: dict = Depends(get_current_user)
db: Session = Depends(get_core_db), current_user: dict = Depends(get_current_user)
):
"""
Get basic classes statistics
@@ -145,7 +154,7 @@ async def get_class(
client_id: int,
class_code: str,
db: Session = Depends(get_core_db),
current_user: dict = Depends(get_current_user)
current_user: dict = Depends(get_current_user),
):
"""
Get class by composite key (client_id + class_code)
@@ -154,16 +163,17 @@ async def get_class(
class_obj = service.get_class(client_id, class_code)
if not class_obj:
raise HTTPException(
status_code=404,
detail=f"Class with client_id '{client_id}' and class_code '{class_code}' not found"
status_code=404,
detail=f"Class with client_id '{client_id}' and class_code '{class_code}' not found",
)
return class_obj
@router.post("/", response_model=ClassResponseDTO, status_code=status.HTTP_201_CREATED)
async def create_class(
class_data: ClassCreateDTO,
db: Session = Depends(get_core_db),
current_user: dict = Depends(get_current_user)
current_user: dict = Depends(get_current_user),
):
"""
Create a new class in the system
@@ -171,13 +181,14 @@ async def create_class(
service = ClassService(db)
return service.create_class(class_data)
@router.put("/{client_id}/{class_code}", response_model=ClassResponseDTO)
async def update_class(
client_id: int,
class_code: str,
class_data: ClassUpdateDTO,
db: Session = Depends(get_core_db),
current_user: dict = Depends(get_current_user)
current_user: dict = Depends(get_current_user),
):
"""
Update class information
@@ -186,8 +197,8 @@ async def update_class(
class_obj = service.update_class(client_id, class_code, class_data)
if not class_obj:
raise HTTPException(
status_code=404,
detail=f"Class with client_id '{client_id}' and class_code '{class_code}' not found"
status_code=404,
detail=f"Class with client_id '{client_id}' and class_code '{class_code}' not found",
)
return class_obj
@@ -197,18 +208,18 @@ async def delete_class(
client_id: int,
class_code: str,
db: Session = Depends(get_core_db),
current_user: dict = Depends(get_current_user)
current_user: dict = Depends(get_current_user),
):
"""
Delete class from the system
Note: This will completely remove the class from the system.
"""
service = ClassService(db)
if not service.delete_class(client_id, class_code):
raise HTTPException(
status_code=404,
detail=f"Class with client_id '{client_id}' and class_code '{class_code}' not found"
status_code=404,
detail=f"Class with client_id '{client_id}' and class_code '{class_code}' not found",
)
@@ -218,7 +229,7 @@ async def get_class_basic_info(
client_id: int,
class_code: str,
db: Session = Depends(get_core_db),
current_user: dict = Depends(get_current_user)
current_user: dict = Depends(get_current_user),
):
"""
Get basic information for a class
@@ -227,17 +238,17 @@ async def get_class_basic_info(
class_obj = service.get_class(client_id, class_code)
if not class_obj:
raise HTTPException(
status_code=404,
detail=f"Class with client_id '{client_id}' and class_code '{class_code}' not found"
status_code=404,
detail=f"Class with client_id '{client_id}' and class_code '{class_code}' not found",
)
return ClassBasicDTO(
client_id=class_obj.client_id,
class_code=class_obj.class_code,
description_spanish=class_obj.description_spanish,
description_english=class_obj.description_english,
material_key=class_obj.material_key,
fraction=class_obj.fraction
fraction=class_obj.fraction,
)
@@ -246,7 +257,7 @@ async def get_class_tariff_info(
client_id: int,
class_code: str,
db: Session = Depends(get_core_db),
current_user: dict = Depends(get_current_user)
current_user: dict = Depends(get_current_user),
):
"""
Get tariff information for a class (fractions, IVA exempt, etc.)
@@ -255,10 +266,10 @@ async def get_class_tariff_info(
class_obj = service.get_class(client_id, class_code)
if not class_obj:
raise HTTPException(
status_code=404,
detail=f"Class with client_id '{client_id}' and class_code '{class_code}' not found"
status_code=404,
detail=f"Class with client_id '{client_id}' and class_code '{class_code}' not found",
)
return {
"client_id": class_obj.client_id,
"class_code": class_obj.class_code,
@@ -266,7 +277,5 @@ async def get_class_tariff_info(
"us_fraction": class_obj.us_fraction,
"iva_exempt_fraction": class_obj.iva_exempt_fraction,
"sub_key": class_obj.sub_key,
"physical_review": class_obj.physical_review
"physical_review": class_obj.physical_review,
}