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,9 +1,9 @@
from pydantic import BaseModel, Field
from pydantic import ConfigDict
class CustomsSectionDTO(BaseModel):
customs_code: str = Field(..., min_length=1, max_length=3)
section_name: str
model_config = ConfigDict(from_attributes=True)

View File

@@ -2,15 +2,16 @@ from sqlalchemy import String, PrimaryKeyConstraint
from sqlalchemy.orm import mapped_column
from core.database import Base
class CustomsSection(Base):
__tablename__ = "customs_sections" #GAduanaSec
__tablename__ = "customs_sections" # GAduanaSec
__table_args__ = (
PrimaryKeyConstraint("customs_code", name="customs_code_pkey"),
{"schema": "public"}
{"schema": "public"},
)
customs_code = mapped_column(String(3), nullable=False)
section_name = mapped_column(String(255), nullable=False)
section_name = mapped_column(String(255), nullable=False)
def __repr__(self):
return f"<CustomsSection(code={self.customs_code}, name={self.section_name })>"
return f"<CustomsSection(code={self.customs_code}, name={self.section_name })>"

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_sections(
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(CustomsSection)
@@ -26,21 +25,31 @@ def list_customs_sections(
"items": [CustomsSectionDTO.model_validate(obj) for obj in items],
"total": total,
"page": page,
"page_size": page_size
"page_size": page_size,
}
@router.get("/{customs_code}", response_model=CustomsSectionDTO)
def get_customs_section(customs_code: str, db: Session = Depends(get_core_db), current_user: dict = Depends(get_current_user)):
obj = db.query(CustomsSection).filter(CustomsSection.customs_code == customs_code).first()
def get_customs_section(
customs_code: str,
db: Session = Depends(get_core_db),
current_user: dict = Depends(get_current_user),
):
obj = (
db.query(CustomsSection)
.filter(CustomsSection.customs_code == customs_code)
.first()
)
if not obj:
raise HTTPException(status_code=404, detail="Not found")
return obj
@router.post("/", response_model=CustomsSectionDTO, status_code=201)
def create_customs_section(
data: CustomsSectionDTO,
db: Session = Depends(get_core_db),
current_user: dict = Depends(has_role("admin"))
current_user: dict = Depends(has_role("admin")),
):
obj = CustomsSection(**data.dict())
db.add(obj)
@@ -48,14 +57,19 @@ def create_customs_section(
db.refresh(obj)
return obj
@router.put("/{customs_code}", response_model=CustomsSectionDTO)
def update_customs_section(
customs_code: str,
data: CustomsSectionDTO,
db: Session = Depends(get_core_db),
current_user: dict = Depends(has_role("admin"))
current_user: dict = Depends(has_role("admin")),
):
obj = db.query(CustomsSection).filter(CustomsSection.customs_code == customs_code).first()
obj = (
db.query(CustomsSection)
.filter(CustomsSection.customs_code == customs_code)
.first()
)
if not obj:
raise HTTPException(status_code=404, detail="Not found")
for field, value in data.dict().items():
@@ -64,13 +78,18 @@ def update_customs_section(
db.refresh(obj)
return obj
@router.delete("/{customs_code}", status_code=204)
def delete_customs_section(
customs_code: str,
db: Session = Depends(get_core_db),
current_user: dict = Depends(has_role("admin"))
current_user: dict = Depends(has_role("admin")),
):
obj = db.query(CustomsSection).filter(CustomsSection.customs_code == customs_code).first()
obj = (
db.query(CustomsSection)
.filter(CustomsSection.customs_code == customs_code)
.first()
)
if not obj:
raise HTTPException(status_code=404, detail="Not found")
db.delete(obj)

View File

@@ -1,10 +1,13 @@
seed = [
("01", "AEROPUERTO INTERNACIONAL GENERAL JUAN N. ALVAREZ, ACAPULCO, GUERRERO."),
("01", "AEROPUERTO INTERNACIONAL GENERAL JUAN N. ALVAREZ, ACAPULCO, GUERRERO."),
("010", "ACAPULCO, ACAPULCO DE JUAREZ, GUERRERO."),
("012", "AEROPUERTO INTERNACIONAL GENERAL JUAN N. ALVAREZ, ACAPULCO, GUERRERO."),
("020", "AGUA PRIETA, AGUA PRIETA, SONORA."),
("050", "SUBTENIENTE LOPEZ, SUBTENIENTE LOPEZ, QUINTANA ROO."),
("051", "SUBTENIENTE LOPEZ II „CHACTEMAL“, OTHÓN P. BLANCO, CHETUMAL, QUINTANA ROO."),
(
"051",
"SUBTENIENTE LOPEZ II „CHACTEMAL“, OTHÓN P. BLANCO, CHETUMAL, QUINTANA ROO.",
),
("060", "CIUDAD DEL CARMEN, CIUDAD DEL CARMEN, CAMPECHE."),
("063", "SEYBAPLAYA, CHAMPOTON, CAMPECHE."),
("070", "CIUDAD JUAREZ, CIUDAD JUAREZ, CHIHUAHUA."),
@@ -15,8 +18,14 @@ seed = [
("080", "COATZACOALCOS, COATZACOALCOS, VERACRUZ."),
("110", "ENSENADA, ENSENADA, BAJA CALIFORNIA."),
("120", "GUAYMAS, GUAYMAS, SONORA."),
("121", "AEROPUERTO INTERNACIONAL GENERAL IGNACIO PESQUEIRA GARCIA, HERMOSILLO, SONORA."),
("123", "CIUDAD OBREGON ADYACENTE AL AEROPUERTO DE CIUDAD OBREGON, CAJEME, SONORA."),
(
"121",
"AEROPUERTO INTERNACIONAL GENERAL IGNACIO PESQUEIRA GARCIA, HERMOSILLO, SONORA.",
),
(
"123",
"CIUDAD OBREGON ADYACENTE AL AEROPUERTO DE CIUDAD OBREGON, CAJEME, SONORA.",
),
("140", "LA PAZ, LA PAZ, BAJA CALIFORNIA SUR."),
("142", "SAN JOSE DEL CABO, LOS CABOS, BAJA CALIFORNIA SUR."),
("143", "CABO SAN LUCAS, LOS CABOS, BAJA CALIFORNIA SUR."),
@@ -25,7 +34,7 @@ seed = [
("147", "PICHILINGÜE, LA PAZ, BAJA CALIFORNIA SUR."),
("160", "MANZANILLO, MANZANILLO, COLIMA."),
("161", "ARMERÍA, ARMERÍA, COLIMA."),
("17", "AEROPUERTO INTERNACIONAL GENERAL SERVANDO CANALES, MATAMOROS, TAMAULIPAS."),
("17", "AEROPUERTO INTERNACIONAL GENERAL SERVANDO CANALES, MATAMOROS, TAMAULIPAS."),
("170", "MATAMOROS, MATAMOROS, TAMAULIPAS."),
("171", "LUCIO BLANCO-LOS INDIOS, MATAMOROS, TAMAULIPAS."),
("172", "SECCION ADUANERA FERROVIARIA DE MATAMOROS."),
@@ -36,22 +45,31 @@ seed = [
("192", "LOS ALGODONES, MEXICALI, BAJA CALIFORNIA."),
("193", "SAN FELIPE, MEXICALI, BAJA CALIFORNIA."),
("200", "MÉXICO, CIUDAD DE MÉXICO."),
("202", "IMPORTACION Y EXPORTACION DE CONTENEDORES, DELEGACION AZCAPOTZALCO, CIUDAD DE MÉXICO."),
(
"202",
"IMPORTACION Y EXPORTACION DE CONTENEDORES, DELEGACION AZCAPOTZALCO, CIUDAD DE MÉXICO.",
),
("220", "NACO, NACO, SONORA."),
("230", "NOGALES, NOGALES, SONORA."),
("231", "SASABE, SARIC, SONORA."),
("24", "AEROPUERTO INTERNACIONAL DE NUEVO LAREDO „QUETZALCOATL“, NUEVO LAREDO, TAMAULIPAS."),
(
"24",
"AEROPUERTO INTERNACIONAL DE NUEVO LAREDO „QUETZALCOATL“, NUEVO LAREDO, TAMAULIPAS.",
),
("240", "NUEVO LAREDO, NUEVO LAREDO, TAMAULIPAS."),
("250", "OJINAGA, OJINAGA, CHIHUAHUA."),
("260", "PUERTO PALOMAS, PUERTO PALOMAS, CHIHUAHUA."),
("27", "RIO ESCONDIDO, NAVA, COAHUILA."),
("27", "RIO ESCONDIDO, NAVA, COAHUILA."),
("270", "PIEDRAS NEGRAS, PIEDRAS NEGRAS, COAHUILA."),
("271", "AEROPUERTO INTERNACIONAL PLAN DE GUADALUPE, RAMOS ARIZPE, COAHUILA."),
("280", "PROGRESO, PROGRESO, YUCATAN."),
("282", "AEROPUERTO INTERNACIONAL LIC. MANUEL CRESCENCIO REJON, MERIDA, YUCATAN."),
("300", "CIUDAD REYNOSA, CIUDAD REYNOSA, TAMAULIPAS."),
("302", "LAS FLORES, RIO BRAVO, TAMAULIPAS."),
("304", "AEROPUERTO INTERNACIONAL GENERAL. LUCIO BLANCO, CIUDAD REYNOSA, TAMAULIPAS."),
(
"304",
"AEROPUERTO INTERNACIONAL GENERAL. LUCIO BLANCO, CIUDAD REYNOSA, TAMAULIPAS.",
),
("305", "RIO BRAVO-DONNA, RIO BRAVO, TAMAULIPAS."),
("306", "ANZALDUAS, CIUDAD REYNOSA, TAMAULIPAS."),
("310", "SALINA CRUZ, SALINA CRUZ, OAXACA."),
@@ -59,7 +77,7 @@ seed = [
("330", "SAN LUIS RIO COLORADO, SAN LUIS RIO COLORADO, SONORA."),
("340", "CIUDAD MIGUEL ALEMAN, CIUDAD MIGUEL ALEMAN, TAMAULIPAS."),
("342", "GUERRERO, GUERRERO, TAMAULIPAS."),
("37", "AEROPUERTO INTERNACIONAL DE TAPACHULA, TAPACHULA, CHIAPAS."),
("37", "AEROPUERTO INTERNACIONAL DE TAPACHULA, TAPACHULA, CHIAPAS."),
("370", "CIUDAD HIDALGO, CIUDAD HIDALGO, CHIAPAS."),
("372", "CIUDAD TALISMAN, TUXTLA CHICO, CHIAPAS."),
("375", "PUERTO CHIAPAS, TAPACHULA, CHIAPAS."),
@@ -67,27 +85,42 @@ seed = [
("380", "TAMPICO, TAMPICO, TAMAULIPAS."),
("390", "TECATE, TECATE, BAJA CALIFORNIA."),
("400", "TIJUANA, TIJUANA, BAJA CALIFORNIA."),
("402", "AEROPUERTO INTERNACIONAL GENERAL ABELARDO L. RODRIGUEZ, TIJUANA, BAJA CALIFORNIA."),
(
"402",
"AEROPUERTO INTERNACIONAL GENERAL ABELARDO L. RODRIGUEZ, TIJUANA, BAJA CALIFORNIA.",
),
("420", "TUXPAN, TUXPAN DE RODRIGUEZ CANO, VERACRUZ."),
("421", "TUXPAN, TUXPAN, VERACRUZ."),
("430", "VERACRUZ, VERACRUZ, VERACRUZ."),
("432", "AEROPUERTO INTERNACIONAL GENERAL HERIBERTO JARA CORONA, VERACRUZ, VERACRUZ."),
(
"432",
"AEROPUERTO INTERNACIONAL GENERAL HERIBERTO JARA CORONA, VERACRUZ, VERACRUZ.",
),
("440", "CIUDAD ACUÑA, CIUDAD ACUÑA, COAHUILA."),
("460", "TORREON, TORREON, COAHUILA."),
("461", "AEROPUERTO DE TORREÓN, COAHUILA DE ZARAGOZA."),
("462", "GOMEZ PALACIO, GOMEZ PALACIO, DURANGO."),
("463", "AEROPUERTO INTERNACIONAL GENERAL GUADALUPE VICTORIA, DURANGO, DURANGO."),
("470", "AEROPUERTO INTERNACIONAL DE LA CIUDAD DE MEXICO."),
("471", "SATELITE, PARA IMPORTACION Y EXPORTACION POR VIA AEREA, AEROPUERTO INTERNACIONAL BENITO JUAREZ DE LA CIUDAD DE MEXICO."),
("472", "CENTRO POSTAL MECANIZADO, POR VIA POSTAL Y POR TRAFICO AEREO, AEROPUERTO INTERNACIONAL BENITO JUAREZ DE LA CIUDAD DE MEXICO."),
(
"471",
"SATELITE, PARA IMPORTACION Y EXPORTACION POR VIA AEREA, AEROPUERTO INTERNACIONAL BENITO JUAREZ DE LA CIUDAD DE MEXICO.",
),
(
"472",
"CENTRO POSTAL MECANIZADO, POR VIA POSTAL Y POR TRAFICO AEREO, AEROPUERTO INTERNACIONAL BENITO JUAREZ DE LA CIUDAD DE MEXICO.",
),
("480", "GUADALAJARA, TLACOMULCO DE ZUÑIGA, JALISCO."),
("481", "PUERTO VALLARTA, PUERTO VALLARTA, JALISCO."),
("484", "TERMINAL INTERMODAL FERROVIARIA, GUADALAJARA, JALISCO."),
("50", "SONORA, PITIQUITO, SONORA."),
("50", "SONORA, PITIQUITO, SONORA."),
("500", "SONOYTA, SONOYTA, SONORA."),
("501", "SAN EMETERIO, GENERAL PLUTARCO ELIAS CALLES, SONORA."),
("510", "LAZARO CARDENAS, LAZARO CARDENAS, MICHOACAN."),
("511", "AEROPUERTO INTERNACIONAL IXTAPA-ZIHUATANEJO, ZIHUATANEJO DE AZUETA, GUERRERO."),
(
"511",
"AEROPUERTO INTERNACIONAL IXTAPA-ZIHUATANEJO, ZIHUATANEJO DE AZUETA, GUERRERO.",
),
("520", "MONTERREY, GENERAL MARIANO ESCOBEDO, NUEVO LEON."),
("521", "AEROPUERTO INTERNACIONAL GENERAL MARIANO ESCOBEDO, APODACA, NUEVO LEON."),
("523", "SALINAS VICTORIA A (TERMINAL FERROVIARIA), SALINAS VICTORIA, NUEVO LEON."),
@@ -102,14 +135,23 @@ seed = [
("651", "SAN CAYETANO MORELOS, TOLUCA, ESTADO DE MÉXICO"),
("670", "CHIHUAHUA, CHIHUAHUA, CHIHUAHUA."),
("671", "PARQUE INDUSTRIAL LAS AMERICAS, CHIHUAHUA, CHIHUAHUA."),
("672", "AEROPUERTO INTERNACIONAL GENERAL ROBERTO FIERRO VILLALOBOS, CHIHUAHUA, CHIHUAHUA."),
("73", "CHICALOTE, SAN FRANCISCO DE LOS ROMO, AGUASCALIENTES."),
(
"672",
"AEROPUERTO INTERNACIONAL GENERAL ROBERTO FIERRO VILLALOBOS, CHIHUAHUA, CHIHUAHUA.",
),
("73", "CHICALOTE, SAN FRANCISCO DE LOS ROMO, AGUASCALIENTES."),
("730", "AGUASCALIENTES, AGUASCALIENTES, AGUASCALIENTES."),
("731", "PARQUE MULTIMODAL INTERPUERTO, SAN LUIS POTOSI, SAN LUIS POTOSI."),
("732", "AEROPUERTO INTERNACIONAL GENERAL LEOBARDO C. RUIZ, EN CALERA ZACATECAS."),
("733", "AEROPUERTO INTERNACIONAL PONCIANO ARRIAGA, SOLEDAD DE GRACIANO SANCHEZ, SAN LUIS POTOSI."),
(
"733",
"AEROPUERTO INTERNACIONAL PONCIANO ARRIAGA, SOLEDAD DE GRACIANO SANCHEZ, SAN LUIS POTOSI.",
),
("734", "LA PILA-VILLA, VILLA DE REYES, SAN LUIS POTOSI."),
("735", "AEROPUERTO INTERNACIONAL LIC. JESUS TERAN PEREDO, AGUASCALIENTES, AGUASCALIENTES."),
(
"735",
"AEROPUERTO INTERNACIONAL LIC. JESUS TERAN PEREDO, AGUASCALIENTES, AGUASCALIENTES.",
),
("750", "PUEBLA, HEROICA PUEBLA DE ZARAGOZA, PUEBLA."),
("751", "CUERNAVACA, JIUTEPEC, MORELOS."),
("754", "AEROPUERTO INTERNACIONAL HERMANOS SERDAN, HUEJOTZINGO, PUEBLA."),
@@ -117,9 +159,12 @@ seed = [
("810", "ALTAMIRA, ALTAMIRA, TAMAULIPAS."),
("820", "CIUDAD CAMARGO, CIUDAD CAMARGO, TAMAULIPAS."),
("830", "DOS BOCAS, PARAISO, TABASCO."),
("831", "AEROPUERTO INTERNACIONAL C.P.A. CARLOS ROVIROSA PEREZ, CIUDAD DE VILLAHERMOSA, CENTRO, TABASCO."),
(
"831",
"AEROPUERTO INTERNACIONAL C.P.A. CARLOS ROVIROSA PEREZ, CIUDAD DE VILLAHERMOSA, CENTRO, TABASCO.",
),
("834", "EL CEIBO, TENOSIQUE, TABASCO."),
("840", "GUANAJUATO, SILAO, GUANAJUATO."),
("841", "CELAYA, CELAYA, GUANAJUATO."),
("842", "AEROPUERTO INTERNACIONAL DE GUANAJUATO, SILAO, GUANAJUATO."),
]
]

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_sections(client, access_token):
headers = {"Authorization": f"Bearer {access_token}"}
@@ -16,20 +17,28 @@ def test_list_customs_sections(client, access_token):
assert "page" in response.json()
assert "page_size" in response.json()
@pytest.mark.usefixtures("client", "access_token")
def test_get_customs_section_not_found(client, access_token):
headers = {"Authorization": f"Bearer {access_token}"}
response = client.get("/customs-sections/invalid_code", headers=headers)
assert response.status_code == 404
def test_create_customs_section_forbidden():
response = client.post("/customs-sections/", json={"customs_code": "TST", "description": "Test"})
response = client.post(
"/customs-sections/", json={"customs_code": "TST", "description": "Test"}
)
assert response.status_code in (403, 405, 404)
def test_update_customs_section_forbidden():
response = client.put("/customs-sections/TST", json={"customs_code": "TST", "description": "Test"})
response = client.put(
"/customs-sections/TST", json={"customs_code": "TST", "description": "Test"}
)
assert response.status_code in (403, 405, 404)
def test_delete_customs_section_forbidden():
response = client.delete("/customs-sections/TST")
assert response.status_code in (403, 405, 404)