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,10 +1,10 @@
from pydantic import BaseModel, Field
from pydantic import ConfigDict
class CustomsWarehouseDTO(BaseModel):
key: str = Field(..., min_length=1, max_length=3)
customs: str
fiscalized_warehouse: str
model_config = ConfigDict(from_attributes=True)

View File

@@ -2,16 +2,19 @@ from sqlalchemy import String, PrimaryKeyConstraint
from sqlalchemy.orm import mapped_column, Mapped
from core.database import Base
class CustomsWarehouse(Base):
__tablename__ = "customs_warehouses" #GRecintos
__tablename__ = "customs_warehouses" # GRecintos
__table_args__ = (
PrimaryKeyConstraint("key", "customs", name="pk_customs_warehouse"),
{"schema": "public"} # opcional
{"schema": "public"}, # opcional
)
key: Mapped[str] = mapped_column(String(3), nullable=False) # clave del recinto
customs: Mapped[str] = mapped_column(String(100), nullable=False) # aduana asociada
fiscalized_warehouse: Mapped[str] = mapped_column(String(1000)) # recintos fiscalizados (valor legal)
key: Mapped[str] = mapped_column(String(3), nullable=False) # clave del recinto
customs: Mapped[str] = mapped_column(String(100), nullable=False) # aduana asociada
fiscalized_warehouse: Mapped[str] = mapped_column(
String(1000)
) # recintos fiscalizados (valor legal)
def __repr__(self):
return f"<CustomsWarehouse(key={self.key}, customs={self.customs}, fiscalized_warehouse={self.fiscalized_warehouse})>"

View File

@@ -1,4 +1,3 @@
from fastapi import APIRouter, Depends, HTTPException, Query
from sqlalchemy.orm import Session
from core.database import get_core_db
@@ -16,7 +15,7 @@ def list_customs_warehouses(
page: int = Query(1, ge=1, description="Número de página"),
page_size: int = Query(50, ge=1, le=100, description="Tamaño de página"),
db: Session = Depends(get_core_db),
current_user: dict = Depends(get_current_user)
current_user: dict = Depends(get_current_user),
):
skip = (page - 1) * page_size
query = db.query(CustomsWarehouse)
@@ -26,21 +25,32 @@ def list_customs_warehouses(
"items": [CustomsWarehouseDTO.model_validate(obj) for obj in items],
"total": total,
"page": page,
"page_size": page_size
"page_size": page_size,
}
@router.get("/{key}/{customs}", response_model=CustomsWarehouseDTO)
def get_customs_warehouse(key: str, customs: str, db: Session = Depends(get_core_db), current_user: dict = Depends(get_current_user)):
obj = db.query(CustomsWarehouse).filter(CustomsWarehouse.key == key, CustomsWarehouse.customs == customs).first()
def get_customs_warehouse(
key: str,
customs: str,
db: Session = Depends(get_core_db),
current_user: dict = Depends(get_current_user),
):
obj = (
db.query(CustomsWarehouse)
.filter(CustomsWarehouse.key == key, CustomsWarehouse.customs == customs)
.first()
)
if not obj:
raise HTTPException(status_code=404, detail="Not found")
return obj
@router.post("/", response_model=CustomsWarehouseDTO, status_code=201)
def create_customs_warehouse(
data: CustomsWarehouseDTO,
db: Session = Depends(get_core_db),
current_user: dict = Depends(has_role("admin"))
current_user: dict = Depends(has_role("admin")),
):
obj = CustomsWarehouse(**data.dict())
db.add(obj)
@@ -48,15 +58,20 @@ def create_customs_warehouse(
db.refresh(obj)
return obj
@router.put("/{key}/{customs}", response_model=CustomsWarehouseDTO)
def update_customs_warehouse(
key: str,
customs: str,
data: CustomsWarehouseDTO,
db: Session = Depends(get_core_db),
current_user: dict = Depends(has_role("admin"))
current_user: dict = Depends(has_role("admin")),
):
obj = db.query(CustomsWarehouse).filter(CustomsWarehouse.key == key, CustomsWarehouse.customs == customs).first()
obj = (
db.query(CustomsWarehouse)
.filter(CustomsWarehouse.key == key, CustomsWarehouse.customs == customs)
.first()
)
if not obj:
raise HTTPException(status_code=404, detail="Not found")
for field, value in data.dict().items():
@@ -65,14 +80,19 @@ def update_customs_warehouse(
db.refresh(obj)
return obj
@router.delete("/{key}/{customs}", status_code=204)
def delete_customs_warehouse(
key: str,
customs: str,
db: Session = Depends(get_core_db),
current_user: dict = Depends(has_role("admin"))
current_user: dict = Depends(has_role("admin")),
):
obj = db.query(CustomsWarehouse).filter(CustomsWarehouse.key == key, CustomsWarehouse.customs == customs).first()
obj = (
db.query(CustomsWarehouse)
.filter(CustomsWarehouse.key == key, CustomsWarehouse.customs == customs)
.first()
)
if not obj:
raise HTTPException(status_code=404, detail="Not found")
db.delete(obj)

View File

@@ -1,28 +1,68 @@
seed = [
("1 ", "Acapulco", "Administración Portuaria Integral de Acapulco, S.A. de C.V."),
("10 ", "Aeropuerto Internacional de la Ciudad de México", "Cargo Service Center de México, S.A. de C.V."),
("12 ", "Aeropuerto Internacional de la Ciudad de México", "DHL Express México, S.A. de C.V."),
("14 ", "Aeropuerto Internacional de la Ciudad de México", "Lufthansa Cargo Servicios Logísticos de México, S.A. de C.V."),
(
"10 ",
"Aeropuerto Internacional de la Ciudad de México",
"Cargo Service Center de México, S.A. de C.V.",
),
(
"12 ",
"Aeropuerto Internacional de la Ciudad de México",
"DHL Express México, S.A. de C.V.",
),
(
"14 ",
"Aeropuerto Internacional de la Ciudad de México",
"Lufthansa Cargo Servicios Logísticos de México, S.A. de C.V.",
),
("145", "México", "Ferrocarril y Terminal de Valle de México, S.A. de C.V."),
("146", "Veracruz", "Cargill de México, S.A. de C.V."),
("147", "Aeropuerto Internacional de la Ciudad de México", "Braniff Transport Carga, S.A. de C.V."),
("148", "Nuevo Laredo", "Inspecciones Fitosanitarias y Aduaneras de Nuevo Laredo, S.A. de C.V."),
(
"147",
"Aeropuerto Internacional de la Ciudad de México",
"Braniff Transport Carga, S.A. de C.V.",
),
(
"148",
"Nuevo Laredo",
"Inspecciones Fitosanitarias y Aduaneras de Nuevo Laredo, S.A. de C.V.",
),
("149", "Nuevo Laredo", "PG Servicios de Logística, S.C."),
("15 ", "Aeropuerto Internacional de la Ciudad de México", "Tramitadores Asociados de Aerocarga, S.A. de C.V."),
(
"15 ",
"Aeropuerto Internacional de la Ciudad de México",
"Tramitadores Asociados de Aerocarga, S.A. de C.V.",
),
("150", "Piedras Negras", "Mercurio Cargo, S.A. de C.V."),
("151", "Colombia", "S.R. Asesores Aduanales de Nuevo Laredo, S.C."),
("154", "Monterrey", "Federal Express Holdings (México) y Compañía, S.N.C. de C.V."),
(
"154",
"Monterrey",
"Federal Express Holdings (México) y Compañía, S.N.C. de C.V.",
),
("155", "Ciudad Hidalgo", "Corporativo de Servicios del Sureste, S.A. de C.V."),
("158", "Monterrey", "Aeropuerto de Monterrey, S.A. de C.V."),
("16 ", "Aeropuerto Internacional de la Ciudad de México", "Transportación México Express, S.A. de C.V."),
(
"16 ",
"Aeropuerto Internacional de la Ciudad de México",
"Transportación México Express, S.A. de C.V.",
),
("160", "Manzanillo", "Frigorífico de Manzanillo, S.A. de C.V."),
("161", "Colombia", "Santos Esquivel y Compañía, S.C."),
("162", "Guadalajara", "Ferrocarril Mexicano, S.A. de C.V."),
("164", "Monterrey", "United Parcel Service de México, S.A. de C.V."),
("165", "Aguascalientes", "Centros de Intercambio de Carga Express Estafeta, S.A. de C.V."),
(
"165",
"Aguascalientes",
"Centros de Intercambio de Carga Express Estafeta, S.A. de C.V.",
),
("166", "Altamira", "Administración Portuaria Integral de Altamira, S.A. de C.V."),
("167", "Ciudad Juárez", "Accel, Recinto Fiscalizado, S.A. de C.V."),
("17 ", "Aeropuerto Internacional de la Ciudad de México", "United Parcel Service de México, S.A. de C.V."),
(
"17 ",
"Aeropuerto Internacional de la Ciudad de México",
"United Parcel Service de México, S.A. de C.V.",
),
("171", "Chihuahua", "Aeropuerto de Chihuahua, S.A. de C.V."),
("172", "Veracruz", "Servicios Especiales Portuarios, S.A. de C.V."),
("173", "Lázaro Cárdenas", "UTTSA, S.A. de C.V."),
@@ -34,7 +74,11 @@ seed = [
("179", "Altamira", "D.A. Hinojosa Terminal Multiusos, S.A. de C.V."),
("18 ", "Aeropuerto Internacional de la Ciudad de México", "Varig de México, S.A."),
("180", "Altamira", "Inmobiliaria Portuaria de Altamira, S.A. de C.V."),
("182", "Veracruz", "Servicios, Maniobras y Almacenamientos de Veracruz, S.A. de C.V."),
(
"182",
"Veracruz",
"Servicios, Maniobras y Almacenamientos de Veracruz, S.A. de C.V.",
),
("184", "Progreso", "Terminal de Contenedores de Yucatán, S.A. de C.V."),
("186", "Nuevo Laredo", "Logis Servicios Comerciales, S.A. de C.V."),
("187", "Manzanillo", "Tecnoadministración del Pacífico, S.A. de C.V."),
@@ -50,8 +94,16 @@ seed = [
("203", "Altamira", "Grupo Castañeda, S.A. de C.V."),
("204", "Monterrey", "Ferrocarril Mexicano, S.A. de C.V."),
("210", "Querétaro", "Terminal Logistics, S.A. de C.V."),
("211", "Aeropuerto Internacional de la Ciudad de México", "World Express Cargo de México, S.A. de C.V."),
("212", "Piedras Negras", "Consultores de Logística en Comercio Exterior, S.A. de C.V."),
(
"211",
"Aeropuerto Internacional de la Ciudad de México",
"World Express Cargo de México, S.A. de C.V.",
),
(
"212",
"Piedras Negras",
"Consultores de Logística en Comercio Exterior, S.A. de C.V.",
),
("214", "Altamira", "Possehl México, S.A. de C.V."),
("215", "Tampico", "Refitam, S.A. de C.V."),
("217", "Veracruz", "SSA México, S.A. de C.V."),
@@ -62,12 +114,20 @@ seed = [
("222", "Matamoros", "Puerto Los Indios, S.A. de C.V."),
("223", "Monterrey", "DHL Express México, S.A. de C.V."),
("224", "Aguascalientes", "Nafta Rail, S.A. de C.V."),
("225", "Altamira", "Integradora de Servicios, Transporte y Almacenaje, S.A. de C.V."),
(
"225",
"Altamira",
"Integradora de Servicios, Transporte y Almacenaje, S.A. de C.V.",
),
("226", "Nuevo Laredo", "DAF, Delivery After Frontier, S.A. de C.V."),
("227", "Matamoros", "Profesionales Mexicanos del Comercio Exterior, S.C."),
("228", "Guadalajara", "CLA Guadalajara, S.A. de C.V."),
("229", "Manzanillo", "Maniobras Integradas del Puerto, S.A. de C.V."),
("23 ", "Coatzacoalcos", "Administración Portuaria Integral de Coatzacoalcos, S.A. de C.V."),
(
"23 ",
"Coatzacoalcos",
"Administración Portuaria Integral de Coatzacoalcos, S.A. de C.V.",
),
("230", "Querétaro", "Terminal Intermodal Logística de Hidalgo, S.A.P.I. de C.V."),
("231", "Lázaro Cárdenas", "Terminales Portuarias del Pacífico, S.A.P.I. de C.V."),
("232", "Lázaro Cárdenas", "Arcelormittal Portuarios, S.A. de C.V."),
@@ -83,34 +143,74 @@ seed = [
("26 ", "Colombia", "Mex Securit, S.A. de C.V."),
("27 ", "Ensenada", "Ensenada International Terminal, S.A. de C.V."),
("28 ", "Guadalajara", "Almacenadora GWTC, S.A. de C.V."),
("29 ", "Guadalajara", "Federal Express Holdings (México) y Compañía, S.N.C. de C.V."),
("3 ", "Aeropuerto Internacional de la Ciudad de México", "Aerovías de México, S.A. de C.V."),
(
"29 ",
"Guadalajara",
"Federal Express Holdings (México) y Compañía, S.N.C. de C.V.",
),
(
"3 ",
"Aeropuerto Internacional de la Ciudad de México",
"Aerovías de México, S.A. de C.V.",
),
("30 ", "Guaymas", "Administración Portuaria Integral de Guaymas, S.A. de C.V."),
("31 ", "Lázaro Cárdenas", "Administración Portuaria Integral de Lázaro Cárdenas, S.A. de C.V."),
(
"31 ",
"Lázaro Cárdenas",
"Administración Portuaria Integral de Lázaro Cárdenas, S.A. de C.V.",
),
("33 ", "Lázaro Cárdenas", "Aarhuskarlshamn México, S.A. de C.V."),
("35 ", "Manzanillo", "Administración Portuaria Integral de Manzanillo, S.A. de C.V."),
(
"35 ",
"Manzanillo",
"Administración Portuaria Integral de Manzanillo, S.A. de C.V.",
),
("36 ", "Manzanillo", "Comercializadora La Junta, S.A. de C.V."),
("38 ", "Manzanillo", "Operadora de la Cuenca del Pacífico, S.A. de C.V."),
("39 ", "Manzanillo", "SSA México, S.A. de C.V."),
("4 ", "Aeropuerto Internacional de la Ciudad de México", "AAACESA Almacenes Fiscalizados, S.A. de C.V."),
(
"4 ",
"Aeropuerto Internacional de la Ciudad de México",
"AAACESA Almacenes Fiscalizados, S.A. de C.V.",
),
("40 ", "Manzanillo", "Terminal Internacional de Manzanillo, S.A. de C.V."),
("42 ", "Mazatlán", "Administración Portuaria Integral de Mazatlán, S.A. de C.V."),
("43 ", "Mazatlán", "Administración Portuaria Integral de Topolobampo, S.A. de C.V."),
(
"43 ",
"Mazatlán",
"Administración Portuaria Integral de Topolobampo, S.A. de C.V.",
),
("44 ", "Monterrey", "Braniff Air Freight and Company, S.A. de C.V."),
("45 ", "Monterrey", "Kansas City Southern de México, S.A. de C.V."),
("46 ", "Nogales", "Servicios de Almacén Fiscalizado de Nogales, S.A. de C.V."),
("47 ", "Progreso", "Administración Portuaria Integral de Progreso, S.A. de C.V."),
("49 ", "Progreso", "Grupo de Desarrollo del Sureste, S.A. de C.V."),
("5 ", "Aeropuerto Internacional de la Ciudad de México", "México Cargo Handling, S.A. de C.V."),
(
"5 ",
"Aeropuerto Internacional de la Ciudad de México",
"México Cargo Handling, S.A. de C.V.",
),
("50 ", "Progreso", "Multisur, S.A. de C.V."),
("51 ", "Querétaro", "Servicios Integrales y Desarrollo GMG, S.A. de C.V."),
("52 ", "Reynosa", "Recintos Fiscalizados de Noreste, S.A. de C.V."),
("53 ", "Salina Cruz", "Administración Portuaria Integral de Salina Cruz, S.A. de C.V."),
("54 ", "Cancún", "Administración Portuaria Integral de Quintana Roo, S.A. de C.V."),
(
"53 ",
"Salina Cruz",
"Administración Portuaria Integral de Salina Cruz, S.A. de C.V.",
),
(
"54 ",
"Cancún",
"Administración Portuaria Integral de Quintana Roo, S.A. de C.V.",
),
("56 ", "Toluca", "Braniff Air Freight and Company, S.A. de C.V."),
("57 ", "Toluca", "Federal Express Holdings (México) y Compañía, S.N.C. de C.V."),
("59 ", "Tuxpan", "Administración Portuaria Integral de Tuxpan, S.A. de C.V."),
("6 ", "Aeropuerto Internacional de la Ciudad de México", "American Airlines de México, S.A. de C.V."),
(
"6 ",
"Aeropuerto Internacional de la Ciudad de México",
"American Airlines de México, S.A. de C.V.",
),
("60 ", "Tuxpan", "Fenoresinas, S.A. de C.V."),
("61 ", "Tuxpan", "Terminal Marítima de Tuxpan, S.A. de C.V."),
("62 ", "Tuxpan", "Terminales Marítimas Transunisa, S.A. de C.V."),
@@ -118,8 +218,16 @@ seed = [
("64 ", "Veracruz", "Almacenadora Golmex, S.A. de C.V."),
("66 ", "Veracruz", "CIF Almacenajes y Servicios, S.A. de C.V."),
("67 ", "Veracruz", "Corporación Integral de Comercio Exterior, S.A. de C.V."),
("69 ", "Veracruz", "Internacional de Contenedores Asociados de Veracruz, S.A. de C.V."),
("7 ", "Aeropuerto Internacional de la Ciudad de México", "Braniff Air Freight and Company, S.A. de C.V."),
(
"69 ",
"Veracruz",
"Internacional de Contenedores Asociados de Veracruz, S.A. de C.V.",
),
(
"7 ",
"Aeropuerto Internacional de la Ciudad de México",
"Braniff Air Freight and Company, S.A. de C.V.",
),
("71 ", "Veracruz", "Reparación Integral de Contenedores, S.A. de C.V."),
("73 ", "Veracruz", "Terminales de Cargas Especializadas, S.A. de C.V."),
("74 ", "Veracruz", "Vopak Terminals México, S.A. de C.V."),
@@ -127,9 +235,17 @@ seed = [
("76 ", "Manzanillo", "Cemex México, S.A. de C.V."),
("77 ", "Manzanillo", "Corporación Multimodal, S.A. de C.V."),
("78 ", "Ensenada", "Administración Portuaria Integral de Ensenada, S.A. de C.V."),
("8 ", "Aeropuerto Internacional de la Ciudad de México", "Iberia de México, S.A."),
(
"8 ",
"Aeropuerto Internacional de la Ciudad de México",
"Iberia de México, S.A.",
),
("81 ", "Tuxpan", "Frigoríficos Especializados de Tuxpan, S.A. de C.V."),
("82 ", "Veracruz", "SSA México, S.A. de C.V."),
("9 ", "Aeropuerto Internacional de la Ciudad de México", "Compañía Mexicana de Aviación, S.A. de C.V."),
(
"9 ",
"Aeropuerto Internacional de la Ciudad de México",
"Compañía Mexicana de Aviación, S.A. de C.V.",
),
("98 ", "Veracruz", "Corporación Portuaria de Veracruz, S.A. de C.V."),
]
]

View File

@@ -7,6 +7,7 @@ app = FastAPI()
app.include_router(router)
client = TestClient(app)
@pytest.mark.usefixtures("client", "access_token")
def test_list_customs_warehouses(client, access_token):
headers = {"Authorization": f"Bearer {access_token}"}
@@ -16,20 +17,32 @@ def test_list_customs_warehouses(client, access_token):
assert "page" in response.json()
assert "page_size" in response.json()
@pytest.mark.usefixtures("client", "access_token")
def test_get_customs_warehouse_not_found(client, access_token):
headers = {"Authorization": f"Bearer {access_token}"}
response = client.get("/customs-warehouses/invalid_key/invalid_customs", headers=headers)
response = client.get(
"/customs-warehouses/invalid_key/invalid_customs", headers=headers
)
assert response.status_code == 404
def test_create_customs_warehouse_forbidden():
response = client.post("/customs-warehouses/", json={"key": "TST", "customs": "TST", "description": "Test"})
response = client.post(
"/customs-warehouses/",
json={"key": "TST", "customs": "TST", "description": "Test"},
)
assert response.status_code in (403, 405, 404)
def test_update_customs_warehouse_forbidden():
response = client.put("/customs-warehouses/TST/TST", json={"key": "TST", "customs": "TST", "description": "Test"})
response = client.put(
"/customs-warehouses/TST/TST",
json={"key": "TST", "customs": "TST", "description": "Test"},
)
assert response.status_code in (403, 405, 404)
def test_delete_customs_warehouse_forbidden():
response = client.delete("/customs-warehouses/TST/TST")
assert response.status_code in (403, 405, 404)