diff --git a/backend/api/v1/modules/a76/layouts_csv/clients_and_providers/routes.py b/backend/api/v1/modules/a76/layouts_csv/clients_and_providers/routes.py
index c0744dba..8cbf361a 100644
--- a/backend/api/v1/modules/a76/layouts_csv/clients_and_providers/routes.py
+++ b/backend/api/v1/modules/a76/layouts_csv/clients_and_providers/routes.py
@@ -9,7 +9,7 @@ from uuid import uuid4
from fastapi import APIRouter, File, HTTPException, Query, UploadFile, Depends
from sqlalchemy.orm import Session
-from typing import Dict, Any
+from typing import Dict, Any, Optional
from core.celery_app import celery_app
from core.database import get_core_db
@@ -33,6 +33,54 @@ router = APIRouter()
logger = logging.getLogger(__name__)
+def _assert_cp_csv_job_access(
+ db: Session,
+ job_id: str,
+ current_user: Dict[str, Any],
+) -> None:
+ """
+ Exige autenticación + csv_upload.process para la compañía asociada al job
+ (task_runs, meta Redis del job, o meta del layout_import_job_id en commits).
+ """
+ from api.v1.modules.core.tasks_tracking.models import TaskRun
+
+ company_id: Optional[int] = None
+ row = db.query(TaskRun).filter(TaskRun.task_id == job_id).first()
+ if row is not None and row.company_id is not None:
+ company_id = int(row.company_id)
+
+ r = _get_redis()
+ if company_id is None:
+ raw = r.get(f"{CP_IMPORT_META_PREFIX}{job_id}")
+ if raw:
+ meta = json.loads(raw.decode("utf-8"))
+ cid = meta.get("company_id")
+ if cid is not None:
+ company_id = int(cid)
+
+ if company_id is None and row is not None and row.meta_payload:
+ layout_jid = row.meta_payload.get("layout_import_job_id")
+ if layout_jid:
+ raw2 = r.get(f"{CP_IMPORT_META_PREFIX}{layout_jid}")
+ if raw2:
+ meta2 = json.loads(raw2.decode("utf-8"))
+ cid2 = meta2.get("company_id")
+ if cid2 is not None:
+ company_id = int(cid2)
+
+ if company_id is None:
+ raise HTTPException(status_code=403, detail="Sin acceso a este job")
+ try:
+ validate_access_to_resource(
+ db, company_id, current_user, ["csv_upload.process"]
+ )
+ except HTTPException:
+ raise
+ except Exception as e:
+ logger.error("CP import: job access validation failed: %s", e)
+ raise HTTPException(status_code=403, detail="Sin acceso a este job") from None
+
+
def _get_redis():
import redis
url = os.getenv("VALKEY_URL", os.getenv("REDIS_URL", "redis://valkey:6379/0"))
@@ -109,10 +157,15 @@ async def upload_import_file(
@router.get("/{job_id}/status")
-async def get_import_status(job_id: str):
+async def get_import_status(
+ job_id: str,
+ db: Session = Depends(get_core_db),
+ current_user: Dict[str, Any] = Depends(get_current_user),
+):
"""
Polling: estado del escaneo o del commit.
"""
+ _assert_cp_csv_job_access(db, job_id, current_user)
task_result = celery_app.AsyncResult(job_id)
if task_result.state == "PENDING":
@@ -166,7 +219,6 @@ async def commit_import_job(
"""
Fase 2: Usuario confirma; se encola la inserción de filas válidas.
"""
- validate_access_to_resource(db, company_id, current_user, ["csv_upload.process"]) # company_id will be extracted from Redis meta
r = _get_redis()
commit_id = dispatch_tracked_layouts_csv_commit(
db=db,
@@ -178,6 +230,7 @@ async def commit_import_job(
task_name="clients_and_providers_insert_valid_rows",
task_origin="a76/layouts_csv/clients_and_providers/commit",
args=[job_id],
+ required_permissions=["csv_upload.process"],
)
return {
"status": "committing",
@@ -187,5 +240,10 @@ async def commit_import_job(
@router.get("/{job_id}/errors/scan-csv")
-async def download_scan_errors_csv(job_id: str):
+async def download_scan_errors_csv(
+ job_id: str,
+ db: Session = Depends(get_core_db),
+ current_user: Dict[str, Any] = Depends(get_current_user),
+):
+ _assert_cp_csv_job_access(db, job_id, current_user)
return download_scan_errors_csv_stream("cp", job_id)
diff --git a/backend/api/v1/modules/a76/layouts_csv/common/track_commit_dispatch.py b/backend/api/v1/modules/a76/layouts_csv/common/track_commit_dispatch.py
index 296906c4..a0f69978 100644
--- a/backend/api/v1/modules/a76/layouts_csv/common/track_commit_dispatch.py
+++ b/backend/api/v1/modules/a76/layouts_csv/common/track_commit_dispatch.py
@@ -24,6 +24,7 @@ def dispatch_tracked_layouts_csv_commit(
task_name: str,
task_origin: str,
args: list[Any],
+ required_permissions: list[str] | None = None,
) -> str:
"""
Lee tenant_id / company_id del meta en Redis, valida acceso y despacha la tarea Celery
@@ -37,7 +38,11 @@ def dispatch_tracked_layouts_csv_commit(
company_id = meta.get("company_id")
if company_id is not None:
try:
- validate_access_to_resource(db, int(company_id), current_user)
+ validate_access_to_resource(
+ db, int(company_id), current_user, required_permissions
+ )
+ except HTTPException:
+ raise
except Exception:
raise HTTPException(status_code=403, detail="Sin acceso a este job") from None
diff --git a/backend/api/v1/modules/a76/layouts_csv/customs_brokers/routes.py b/backend/api/v1/modules/a76/layouts_csv/customs_brokers/routes.py
index d973d888..5c5ce0b7 100644
--- a/backend/api/v1/modules/a76/layouts_csv/customs_brokers/routes.py
+++ b/backend/api/v1/modules/a76/layouts_csv/customs_brokers/routes.py
@@ -166,7 +166,6 @@ async def commit_import_job(
"""
Fase 2: Usuario confirma; se encola la inserción de filas válidas.
"""
- validate_access_to_resource(db, company_id, current_user, ["csv_upload.process"]) # company_id will be extracted from Redis meta
r = _get_redis()
commit_id = dispatch_tracked_layouts_csv_commit(
db=db,
@@ -178,6 +177,7 @@ async def commit_import_job(
task_name="customs_brokers_insert_valid_rows",
task_origin="a76/layouts_csv/customs_brokers/commit",
args=[job_id],
+ required_permissions=["csv_upload.process"],
)
return {
"status": "committing",
diff --git a/frontend/src/lib/components/dashboard/clients_and_providers/columns.ts b/frontend/src/lib/components/dashboard/clients_and_providers/columns.ts
index a779f66a..8ebed1a0 100644
--- a/frontend/src/lib/components/dashboard/clients_and_providers/columns.ts
+++ b/frontend/src/lib/components/dashboard/clients_and_providers/columns.ts
@@ -2,7 +2,13 @@ import { renderComponent, renderSnippet } from "$lib/components/ui/data-table/in
import { createRawSnippet } from "svelte";
import DataTableActions from "./data-table-actions.svelte";
-export function createColumns(onSuccess) {
+export type ClientsProvidersColumnOptions = {
+ canEdit?: boolean;
+ canDelete?: boolean;
+};
+
+export function createColumns(onSuccess: () => void, options: ClientsProvidersColumnOptions = {}) {
+ const { canEdit = false, canDelete = false } = options;
return [
{
accessorKey: "id",
@@ -150,7 +156,13 @@ export function createColumns(onSuccess) {
{
id: "actions",
header: "Acciones",
- cell: ({ row }) => renderComponent(DataTableActions, { item: row.original, onSuccess })
+ cell: ({ row }) =>
+ renderComponent(DataTableActions, {
+ item: row.original,
+ onSuccess,
+ canEdit,
+ canDelete
+ })
}
];
}
\ No newline at end of file
diff --git a/frontend/src/lib/components/dashboard/clients_and_providers/data-table-actions.svelte b/frontend/src/lib/components/dashboard/clients_and_providers/data-table-actions.svelte
index 291753a7..de0ee204 100644
--- a/frontend/src/lib/components/dashboard/clients_and_providers/data-table-actions.svelte
+++ b/frontend/src/lib/components/dashboard/clients_and_providers/data-table-actions.svelte
@@ -11,10 +11,14 @@
let {
item,
- onSuccess
+ onSuccess,
+ canEdit = false,
+ canDelete = false
}: {
item: ClientProvider;
onSuccess?: () => void;
+ canEdit?: boolean;
+ canDelete?: boolean;
} = $props();
let showDetailsDialog = $state(false);
@@ -41,11 +45,14 @@
}
async function handleToggleStatus() {
- if (isToggling || !companyStore.activeCompany) return;
+ if (!canEdit || isToggling || !companyStore.activeCompany) return;
isToggling = true;
try {
- const response = await clientsProvidersApi.toggleStatus(item.id, companyStore.activeCompany.id);
+ const nextActive = item.is_active !== true;
+ const response = await clientsProvidersApi.update(item.id, companyStore.activeCompany.id, {
+ is_active: nextActive
+ });
if (response.error) {
console.error('Error toggling status:', response.error);
@@ -85,15 +92,18 @@