- Add is_hub_admin() and resolve_tenant_id_required() to core/security - hub_admin resolves tenant from company or uses None as global sentinel - Update routes and services to skip tenant filter when tenant_id is None - UserService accepts is_hub_admin flag for cross-tenant user management - get_my_companies returns all companies for hub_admin without tenant restriction
403 lines
14 KiB
Python
403 lines
14 KiB
Python
import logging
|
|
import mimetypes
|
|
import os
|
|
from datetime import datetime
|
|
from typing import Any, Dict, Optional
|
|
|
|
from fastapi import APIRouter, Depends, File, HTTPException, Query, UploadFile, status
|
|
from sqlalchemy.orm import Session
|
|
|
|
from core.config import settings
|
|
from core.database import get_core_db
|
|
from core.s3_keys import (
|
|
customs_broker_vu_certificate_key,
|
|
customs_broker_vu_cove_key,
|
|
customs_broker_vu_doda_certificate_key,
|
|
customs_broker_vu_doda_cove_key,
|
|
customs_broker_vu_doda_private_key_key,
|
|
customs_broker_vu_private_key_key,
|
|
)
|
|
from core.security import get_current_user, get_tenant_from_token, resolve_tenant_id_required, validate_access_to_resource
|
|
from core.storage_s3 import delete_object_if_exists, put_object_bytes
|
|
|
|
from api.v1.common.tenant_crud_routes import TenantCRUDRoutes
|
|
from . import dto, services
|
|
from ..layouts_csv.customs_brokers.routes import router as imports_router
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
router = APIRouter()
|
|
|
|
MAX_VU_CER_KEY_BYTES = 5 * 1024 * 1024 # 5 MB
|
|
MAX_COVE_BYTES = 15 * 1024 * 1024 # 15 MB (xml/zip)
|
|
|
|
|
|
def _resolve_tenant_id_int(
|
|
current_user: dict,
|
|
db=None,
|
|
company_id: int = None,
|
|
):
|
|
"""Delega a resolve_tenant_id_required. Hub admin resuelve tenant desde la empresa."""
|
|
return resolve_tenant_id_required(current_user, db=db, company_id=company_id)
|
|
|
|
|
|
def _remove_stored_vu_path(ref: Optional[str]) -> None:
|
|
if not ref:
|
|
return
|
|
if ref.startswith("tenants/"):
|
|
delete_object_if_exists(ref)
|
|
elif os.path.isfile(ref):
|
|
try:
|
|
os.remove(ref)
|
|
except OSError:
|
|
pass
|
|
|
|
# CSV import (mismo flujo que a76.imports: upload → scan → commit)
|
|
router.include_router(imports_router, prefix="/customs-brokers/imports", tags=["customs_brokers / csv_import"])
|
|
|
|
customs_broker_crud = TenantCRUDRoutes(
|
|
service=services.CustomsBrokerService,
|
|
create_schema=dto.CustomsBrokerCreateDTO,
|
|
update_schema=dto.CustomsBrokerUpdateDTO,
|
|
response_schema=dto.CustomsBrokerResponseDTO,
|
|
prefix="/customs-brokers",
|
|
tags=[],
|
|
resource_name="Customs Broker",
|
|
id_name="broker_key",
|
|
id_type=str,
|
|
enable_list=True,
|
|
list_permissions=["customs_brokers.view"],
|
|
get_permissions=["customs_brokers.view"],
|
|
create_permissions=["customs_brokers.create"],
|
|
update_permissions=["customs_brokers.edit"],
|
|
delete_permissions=["customs_brokers.delete"],
|
|
)
|
|
|
|
router.include_router(customs_broker_crud.router)
|
|
|
|
|
|
@router.patch(
|
|
"/customs-brokers/{broker_key}",
|
|
response_model=dto.CustomsBrokerResponseDTO,
|
|
)
|
|
def update_customs_broker(
|
|
broker_key: str,
|
|
broker_data: dto.CustomsBrokerUpdateDTO,
|
|
company_id: int = Query(..., description="Company ID"),
|
|
db: Session = Depends(get_core_db),
|
|
current_user: Dict[str, Any] = Depends(get_current_user),
|
|
):
|
|
"""
|
|
Actualización parcial (PATCH).
|
|
"""
|
|
tenant_id = validate_access_to_resource(db, company_id, current_user, ["customs_brokers.edit"])
|
|
|
|
broker = services.CustomsBrokerService.get_by_id(db, broker_key, tenant_id, company_id)
|
|
if not broker:
|
|
raise HTTPException(status_code=404, detail="Customs Broker not found")
|
|
|
|
updated_broker = services.CustomsBrokerService.update(
|
|
db=db,
|
|
broker_key=broker_key,
|
|
tenant_id=tenant_id,
|
|
broker_data=broker_data,
|
|
company_id=company_id
|
|
)
|
|
|
|
if not updated_broker:
|
|
raise HTTPException(status_code=400, detail="Error updating Customs Broker")
|
|
|
|
return updated_broker
|
|
|
|
|
|
@router.put(
|
|
"/customs-broker-vu/{broker_key}",
|
|
response_model=dto.CustomsBrokerVUResponseDTO,
|
|
)
|
|
def update_customs_broker_vu(
|
|
broker_key: str,
|
|
vu_data: dto.CustomsBrokerVUCreateDTO,
|
|
company_id: int = Query(..., description="Company ID"),
|
|
db: Session = Depends(get_core_db),
|
|
current_user: Dict[str, Any] = Depends(get_current_user),
|
|
):
|
|
tenant_id = validate_access_to_resource(db, company_id, current_user, ["customs_brokers.edit"])
|
|
|
|
broker = services.CustomsBrokerService.get_by_id(db, broker_key, tenant_id, company_id)
|
|
if not broker:
|
|
raise HTTPException(status_code=404, detail="Customs Broker not found")
|
|
|
|
updated_vu = services.CustomsBrokerVUService.update_vu(db, broker_key, vu_data, tenant_id, company_id)
|
|
if not updated_vu:
|
|
raise HTTPException(status_code=404, detail="Customs Broker VU not found")
|
|
return updated_vu
|
|
|
|
|
|
@router.put(
|
|
"/customs-broker-personnel/{broker_key}/{line}",
|
|
response_model=dto.CustomsBrokerPersonnelDTO,
|
|
)
|
|
def update_customs_broker_personnel(
|
|
broker_key: str,
|
|
line: int,
|
|
personnel_data: dto.CustomsBrokerPersonnelDTO,
|
|
company_id: int = Query(..., description="Company ID"),
|
|
db: Session = Depends(get_core_db),
|
|
current_user: Dict[str, Any] = Depends(get_current_user),
|
|
):
|
|
tenant_id = validate_access_to_resource(db, company_id, current_user, ["customs_brokers.edit"])
|
|
|
|
broker = services.CustomsBrokerService.get_by_id(db, broker_key, tenant_id, company_id)
|
|
if not broker:
|
|
raise HTTPException(status_code=404, detail="Customs Broker not found")
|
|
|
|
updated_personnel = services.CustomsBrokerPersonnelService.update_personnel(
|
|
db, broker_key, line, personnel_data, tenant_id, company_id
|
|
)
|
|
if not updated_personnel:
|
|
raise HTTPException(
|
|
status_code=404, detail="Customs Broker Personnel not found"
|
|
)
|
|
return updated_personnel
|
|
|
|
|
|
@router.post(
|
|
"/customs-brokers/{broker_key}/vu/upload",
|
|
summary="Sube VU/DODA (CER, KEY, COVE) al bucket bajo tenants/.../customs_brokers/{id}/...",
|
|
)
|
|
async def upload_customs_broker_vu_file(
|
|
broker_key: str,
|
|
file_kind: str = Query(
|
|
...,
|
|
description=(
|
|
"certificate (.cer), key (.key), cove (xml/zip/txt/pdf/json), "
|
|
"doda_certificate (.cer), doda_key (.key), doda_cove (xml/zip/txt/pdf/json)"
|
|
),
|
|
),
|
|
company_id: int = Query(..., description="Company ID"),
|
|
file: UploadFile = File(...),
|
|
db: Session = Depends(get_core_db),
|
|
current_user: Dict[str, Any] = Depends(get_current_user),
|
|
):
|
|
"""
|
|
Persiste el archivo bajo la misma jerarquía que logos/avatares (tenant/company/...).
|
|
Guarda la clave S3 o ruta local en:
|
|
- VU: certificate_path, key_path, xml_files_path
|
|
- DODA: doda_certificate_path, doda_key_path, doda_xml_files_path
|
|
"""
|
|
validate_access_to_resource(db, company_id, current_user, ["customs_brokers.create"])
|
|
tenant_id = _resolve_tenant_id_int(current_user, db=db, company_id=company_id)
|
|
|
|
broker = services.CustomsBrokerService.get_by_id(db, broker_key, tenant_id, company_id)
|
|
if not broker:
|
|
raise HTTPException(status_code=404, detail="Customs Broker not found")
|
|
|
|
fk = file_kind.lower().strip()
|
|
if fk not in (
|
|
"certificate",
|
|
"key",
|
|
"cove",
|
|
"doda_certificate",
|
|
"doda_key",
|
|
"doda_cove",
|
|
):
|
|
raise HTTPException(
|
|
status_code=400,
|
|
detail=(
|
|
"file_kind must be certificate, key, cove, "
|
|
"doda_certificate, doda_key, or doda_cove"
|
|
),
|
|
)
|
|
|
|
content = await file.read()
|
|
max_bytes = MAX_COVE_BYTES if fk in ("cove", "doda_cove") else MAX_VU_CER_KEY_BYTES
|
|
if len(content) > max_bytes:
|
|
raise HTTPException(
|
|
status_code=400,
|
|
detail=f"File too large (max {max_bytes // (1024 * 1024)} MB)",
|
|
)
|
|
|
|
file_ext = os.path.splitext(file.filename or "")[1].lower()
|
|
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
|
|
vu = services.CustomsBrokerVUService.ensure_vu_for_broker(db, broker)
|
|
broker_id = broker.id
|
|
|
|
field_name: str
|
|
stored: str
|
|
|
|
try:
|
|
if settings.use_s3_object_storage:
|
|
if fk == "certificate":
|
|
if file_ext != ".cer":
|
|
raise HTTPException(status_code=400, detail="certificate must be .cer")
|
|
key = customs_broker_vu_certificate_key(
|
|
tenant_id, company_id, broker_id, timestamp, file_ext
|
|
)
|
|
ct = "application/x-x509-ca-cert"
|
|
field_name = "certificate_path"
|
|
elif fk == "key":
|
|
if file_ext != ".key":
|
|
raise HTTPException(status_code=400, detail="key must be .key")
|
|
key = customs_broker_vu_private_key_key(
|
|
tenant_id, company_id, broker_id, timestamp, file_ext
|
|
)
|
|
ct = "application/pkcs8"
|
|
field_name = "key_path"
|
|
elif fk == "cove":
|
|
key = customs_broker_vu_cove_key(
|
|
tenant_id,
|
|
company_id,
|
|
broker_id,
|
|
timestamp,
|
|
file.filename or "cove.xml",
|
|
)
|
|
ct = (
|
|
file.content_type
|
|
or mimetypes.guess_type(file.filename or "")[0]
|
|
or "application/octet-stream"
|
|
)
|
|
field_name = "xml_files_path"
|
|
elif fk == "doda_certificate":
|
|
if file_ext != ".cer":
|
|
raise HTTPException(status_code=400, detail="doda_certificate must be .cer")
|
|
key = customs_broker_vu_doda_certificate_key(
|
|
tenant_id, company_id, broker_id, timestamp, file_ext
|
|
)
|
|
ct = "application/x-x509-ca-cert"
|
|
field_name = "doda_certificate_path"
|
|
elif fk == "doda_key":
|
|
if file_ext != ".key":
|
|
raise HTTPException(status_code=400, detail="doda_key must be .key")
|
|
key = customs_broker_vu_doda_private_key_key(
|
|
tenant_id, company_id, broker_id, timestamp, file_ext
|
|
)
|
|
ct = "application/pkcs8"
|
|
field_name = "doda_key_path"
|
|
else:
|
|
key = customs_broker_vu_doda_cove_key(
|
|
tenant_id,
|
|
company_id,
|
|
broker_id,
|
|
timestamp,
|
|
file.filename or "doda.xml",
|
|
)
|
|
ct = (
|
|
file.content_type
|
|
or mimetypes.guess_type(file.filename or "")[0]
|
|
or "application/octet-stream"
|
|
)
|
|
field_name = "doda_xml_files_path"
|
|
|
|
old = getattr(vu, field_name)
|
|
_remove_stored_vu_path(old)
|
|
put_object_bytes(key, content, content_type=ct)
|
|
logger.info(
|
|
"Customs broker VU upload kind=%s key=%s bytes=%s",
|
|
fk,
|
|
key,
|
|
len(content),
|
|
)
|
|
stored = key
|
|
else:
|
|
base = os.path.join(
|
|
"uploads", "customs_brokers", str(company_id), str(broker_id)
|
|
)
|
|
if fk == "certificate":
|
|
if file_ext != ".cer":
|
|
raise HTTPException(status_code=400, detail="certificate must be .cer")
|
|
key = customs_broker_vu_certificate_key(
|
|
tenant_id, company_id, broker_id, timestamp, file_ext
|
|
)
|
|
field_name = "certificate_path"
|
|
subdir = "certificates"
|
|
elif fk == "key":
|
|
if file_ext != ".key":
|
|
raise HTTPException(status_code=400, detail="key must be .key")
|
|
key = customs_broker_vu_private_key_key(
|
|
tenant_id, company_id, broker_id, timestamp, file_ext
|
|
)
|
|
field_name = "key_path"
|
|
subdir = "keys"
|
|
elif fk == "cove":
|
|
try:
|
|
key = customs_broker_vu_cove_key(
|
|
tenant_id,
|
|
company_id,
|
|
broker_id,
|
|
timestamp,
|
|
file.filename or "cove.xml",
|
|
)
|
|
except ValueError as e:
|
|
raise HTTPException(status_code=400, detail=str(e)) from e
|
|
field_name = "xml_files_path"
|
|
subdir = "cove"
|
|
elif fk == "doda_certificate":
|
|
if file_ext != ".cer":
|
|
raise HTTPException(status_code=400, detail="doda_certificate must be .cer")
|
|
key = customs_broker_vu_doda_certificate_key(
|
|
tenant_id, company_id, broker_id, timestamp, file_ext
|
|
)
|
|
field_name = "doda_certificate_path"
|
|
subdir = "doda/certificates"
|
|
elif fk == "doda_key":
|
|
if file_ext != ".key":
|
|
raise HTTPException(status_code=400, detail="doda_key must be .key")
|
|
key = customs_broker_vu_doda_private_key_key(
|
|
tenant_id, company_id, broker_id, timestamp, file_ext
|
|
)
|
|
field_name = "doda_key_path"
|
|
subdir = "doda/keys"
|
|
else:
|
|
try:
|
|
key = customs_broker_vu_doda_cove_key(
|
|
tenant_id,
|
|
company_id,
|
|
broker_id,
|
|
timestamp,
|
|
file.filename or "doda.xml",
|
|
)
|
|
except ValueError as e:
|
|
raise HTTPException(status_code=400, detail=str(e)) from e
|
|
field_name = "doda_xml_files_path"
|
|
subdir = "doda/cove"
|
|
|
|
fname = key.rsplit("/", 1)[-1]
|
|
dest_dir = os.path.join(base, subdir)
|
|
os.makedirs(dest_dir, exist_ok=True)
|
|
path = os.path.join(dest_dir, fname)
|
|
old = getattr(vu, field_name)
|
|
_remove_stored_vu_path(old)
|
|
with open(path, "wb") as f:
|
|
f.write(content)
|
|
logger.info(
|
|
"Customs broker VU upload kind=%s path=%s bytes=%s",
|
|
fk,
|
|
path,
|
|
len(content),
|
|
)
|
|
stored = path
|
|
|
|
setattr(vu, field_name, stored)
|
|
db.add(vu)
|
|
db.commit()
|
|
db.refresh(vu)
|
|
except HTTPException:
|
|
db.rollback()
|
|
raise
|
|
except ValueError as e:
|
|
db.rollback()
|
|
raise HTTPException(status_code=400, detail=str(e)) from e
|
|
except Exception as e:
|
|
db.rollback()
|
|
raise HTTPException(
|
|
status_code=500, detail=f"Error saving file: {str(e)}"
|
|
) from e
|
|
|
|
return {
|
|
"message": "File uploaded successfully",
|
|
"file_kind": fk,
|
|
"field": field_name,
|
|
"path": stored,
|
|
"broker_key": broker_key,
|
|
"company_id": company_id,
|
|
} |