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:
@@ -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)
|
||||
|
||||
|
||||
@@ -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 })>"
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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."),
|
||||
]
|
||||
]
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user