csrfToken corregido

This commit is contained in:
David Arranz 2026-10-10 12:42:56 +02:00
parent dd64a01622
commit d8a09888e8
334 changed files with 783 additions and 1416 deletions

View File

@ -90,6 +90,15 @@
"function-overload", "function-overload",
"function" "function"
] ]
},
{
"blankLine": "any",
"prev": {
"selector": "ExportNamedDeclaration[source], ExportAllDeclaration"
},
"next": {
"selector": "ExportNamedDeclaration[source], ExportAllDeclaration"
}
} }
], ],
"@stylistic/lines-between-class-members": [ "@stylistic/lines-between-class-members": [

View File

@ -26,7 +26,6 @@ export function App() {
renderWorkspace={(snapshot, accountNavigation) => ( renderWorkspace={(snapshot, accountNavigation) => (
<CompaniesWorkspace <CompaniesWorkspace
accountNavigation={accountNavigation} accountNavigation={accountNavigation}
csrfToken={snapshot.csrfToken}
languageSwitcher={<LanguageSwitcher />} languageSwitcher={<LanguageSwitcher />}
userName={snapshot.user?.name ?? ""} userName={snapshot.user?.name ?? ""}
/> />

View File

@ -1,4 +1,6 @@
import { coreSpanish } from "@metis/core/client"; import { configureCsrfTokenReader, coreSpanish } from "@metis/core/client";
import { identityKeys } from "@metis/identity/client";
import type { SessionResponseDTO } from "@metis/identity/common";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import { StrictMode } from "react"; import { StrictMode } from "react";
import { createRoot } from "react-dom/client"; import { createRoot } from "react-dom/client";
@ -16,6 +18,10 @@ if (!rootElement) {
const { instance, initialization } = createAppI18n(); const { instance, initialization } = createAppI18n();
const queryClient = new QueryClient(); const queryClient = new QueryClient();
configureCsrfTokenReader(
() => queryClient.getQueryData<SessionResponseDTO>(identityKeys.session)?.csrfToken
);
initialization initialization
.then(() => { .then(() => {
createRoot(rootElement).render( createRoot(rootElement).render(

View File

@ -0,0 +1,73 @@
# Centralización de CSRF y revisión de prop drilling
## Cambio aplicado
El token CSRF pertenece a la sesión HTTP, no a los formularios ni a las entidades.
`apps/web/src/main.tsx` configura una función que consulta la sesión actual en la caché
de TanStack Query (`identityKeys.session`). `modules/core/src/web/http/shared/api/csrf-fetch.ts`
lee esa función en cada petición de escritura y añade `x-csrf-token`.
No existe una segunda copia del token en React, localStorage o un contexto. La caché de
sesión sigue siendo la fuente única. GET/HEAD/OPTIONS permiten obtener la sesión sin token;
POST y las demás escrituras requieren el token hexadecimal de 64 caracteres que genera
el servidor. Un token ausente o inválido impide enviar la petición. El transporte solo
acepta rutas locales `/api/` y utiliza credenciales del mismo origen.
Se han eliminado `csrfToken` de props, controladores, callbacks y firmas de los clientes
HTTP de identity, companies, assets y resources. Las peticiones de fotos y logotipos usan
el mismo transporte, conservando su cuerpo binario. El contrato HTTP, las validaciones
de respuestas y errores de cada módulo y la protección del backend se mantienen.
La renovación de sesión tras login, configuración inicial, cambio de contraseña y logout
continúa utilizando las invalidaciones existentes. La siguiente petición lee el token que
haya en la caché en ese momento; no captura el token de un render anterior. Un 403 no se
reintenta automáticamente, para no repetir escrituras. Las pruebas aisladas configuran y
restauran su lector mediante la función de limpieza de `configureCsrfTokenReader`.
## Criterio de análisis
Se revisaron las props y su recorrido entre workspace, páginas, diálogos, formularios,
controles y hooks de los cinco módulos y la aplicación web. Una prop transmitida a un
hijo directo no constituye por sí sola un problema: importa si las capas intermedias solo
la retransmiten, si tiene una fuente compartida y si su alcance es local o global.
La corrección de las demás propiedades queda pendiente: esta tarea implementa CSRF y
analiza el resto, sin modificar sus contratos funcionales.
## Casos encontrados y recomendaciones
| Propiedades | Recorrido real representativo | Valoración y siguiente paso |
| --- | --- | --- |
| `company` / `companyId` | `CompanyDetail` → `CompanySection` → `WorkProfilesPage` → `WorkProfileEditorDialog` → `WorkProfileForm`; también las páginas de configuraciones y sus formularios | Prioridad alta. Un contexto limitado al espacio de la compañía podría aportar el identificador y los datos ya consultados. No crear estado duplicado ni hacer que formularios reutilizables dependan obligatoriamente de ese contexto. Conservar el identificador explícito en los clientes HTTP y en los listados globales. |
| `lookups` | `CompanyInventory` → `AssetInventoryPage` → `AssetEditorDialog` → `AssetForm`; también `WorkstationsPage` → `WorkstationEditorDialog` → `WorkstationForm` → `WorkstationAssetCreateDialog` | Prioridad media. Mezcla datos de sedes/personas con callbacks de alta rápida entre companies y assets. Separar ambos contratos antes de reducir su paso. Un proveedor limitado al inventario puede compartir datos; las acciones de alta deben seguir siendo aportadas por el módulo propietario. |
| `userName` | `App` → `CompaniesWorkspace` → `WorkspaceHeader` | Prioridad baja. Dato de sesión pasado a través de una capa. Un hook público de identidad puede ofrecer el usuario actual a la cabecera si aumenta su uso. Hoy solo hay una capa intermediaria: no justifica por sí sola un contexto adicional. |
| `scope`, `items`, plantillas y catálogos | Página de catálogo/inventario → diálogo de edición → formulario/controles | Datos y ámbito de la operación, en su mayoría consumidos también por las capas intermedias. Mantener explícitos; si varios consumidores consultan lo mismo, reutilizar hooks y claves de Query existentes. `scope` puede ser global o compañía: no convertirlo en una compañía implícita. |
| `onSaved`, `onClose`, `onCreated`, `onChange` | Página → diálogo → formulario/control | Contratos de comunicación local. Mantener: controlan cierre, selección del registro recién creado e invalidación. No reemplazarlos por un bus global. |
| `allowQuickCreate` | Formulario → control de marca/plantilla; formularios anidados establecen `false` | Opción local deliberada para limitar las altas anidadas. Mantener explícita para que el diálogo hijo no abra niveles adicionales por herencia accidental. |
| `accountNavigation`, `languageSwitcher`, `children`, `relatedContent` | `App` → `CompaniesWorkspace` → cabecera/configuración; render props de secciones | Composición de interfaz entre módulos. Son slots, no estado duplicado. Mantener para preservar independencia de módulos y evitar importar identity desde companies. |
| `initialId`, `initialCreate`, navegación y filtros iniciales | Workspace → sección/página → controlador | Traducción de ruta a estado de pantalla. La capa intermedia suele interpretar la ruta. Mantener la responsabilidad del routing; reducir solo retransmisiones innecesarias al trabajar en cada pantalla. |
## Orden sugerido
1. Revisar el alcance de compañía dentro de `CompanyDetail`, sin afectar pantallas globales.
2. Separar datos y acciones de `lookups` y evaluar un proveedor de inventario acotado.
3. Extraer un hook de usuario actual únicamente si más consumidores necesitan la sesión.
No crear un contexto global con todas las props: ocultaría dependencias de formularios,
complicaría las pruebas y podría mezclar ámbitos de compañías distintas.
## Verificación
- Pruebas del transporte: token leído de Query, rotación, borrado de caché, ausencia/invalidez,
lectura sin sesión, destino local, cuerpos JSON/binarios, headers, cancelación y ausencia
de reintentos de escrituras fallidas.
- Pruebas de contratos de identity, accesos, buzones y marcas adaptadas al transporte.
- Suite completa del monorepo: 277 pruebas correctas, incluida integración HTTP con MariaDB.
- Pruebas de espaciado: 3 correctas. Los reexports consecutivos pueden mantenerse agrupados
para que ESLint y la ordenación de exports de Biome no se contradigan.
- Build de producción: correcto.
- Biome y reglas de espaciado sobre ficheros modificados: correctos.
- Typecheck del módulo core: correcto. Las comprobaciones generales de tipos/lint/check
encuentran errores previos fuera del cambio:
`calendar.tsx` de vendor, `personId` antiguo en el fixture web y reglas de llaves en
código de fotos. No se modificó vendor ni se amplió el alcance del refactor para ellos.

View File

@ -2,12 +2,8 @@ import type { RelationshipInputDTO } from "../../../../common/relationships";
import { RelationshipResponseSchema } from "../../../../common/relationships"; import { RelationshipResponseSchema } from "../../../../common/relationships";
import { inventoryRequest } from "../../../shared/api/inventory-request"; import { inventoryRequest } from "../../../shared/api/inventory-request";
export async function createAssetRelationship( export async function createAssetRelationship(companyId: string, values: RelationshipInputDTO) {
companyId: string,
values: RelationshipInputDTO,
token: string
) {
return RelationshipResponseSchema.parse( return RelationshipResponseSchema.parse(
await inventoryRequest(`${companyId}/relationships`, { values }, token) await inventoryRequest(`${companyId}/relationships`, { values })
); );
} }

View File

@ -1,8 +1,8 @@
import { RelationshipResponseSchema } from "../../../../common/relationships"; import { RelationshipResponseSchema } from "../../../../common/relationships";
import { inventoryRequest } from "../../../shared/api/inventory-request"; import { inventoryRequest } from "../../../shared/api/inventory-request";
export async function endAssetRelationship(companyId: string, id: string, token: string) { export async function endAssetRelationship(companyId: string, id: string) {
return RelationshipResponseSchema.parse( return RelationshipResponseSchema.parse(
await inventoryRequest(`${companyId}/relationships/${id}/end`, {}, token) await inventoryRequest(`${companyId}/relationships/${id}/end`, {})
); );
} }

View File

@ -7,11 +7,10 @@ import type { EndRelationshipDialogProps } from "../types";
export function useEndRelationshipDialogController({ export function useEndRelationshipDialogController({
relationship, relationship,
companyId, companyId,
csrfToken,
}: EndRelationshipDialogProps) { }: EndRelationshipDialogProps) {
const client = useQueryClient(); const client = useQueryClient();
const mutation = useMutation({ const mutation = useMutation({
mutationFn: () => endAssetRelationship(companyId, relationship.id, csrfToken), mutationFn: () => endAssetRelationship(companyId, relationship.id),
onSuccess: () => client.invalidateQueries({ queryKey: inventoryKeys.company(companyId) }), onSuccess: () => client.invalidateQueries({ queryKey: inventoryKeys.company(companyId) }),
}); });

View File

@ -3,7 +3,6 @@ import type { RelationshipDTO } from "../../../../common";
export type EndRelationshipDialogProps = { export type EndRelationshipDialogProps = {
relationship: RelationshipDTO; relationship: RelationshipDTO;
companyId: string; companyId: string;
csrfToken: string;
sourceName: string; sourceName: string;
targetName: string; targetName: string;
onClose: () => void; onClose: () => void;

View File

@ -1,6 +1,5 @@
export type AssetRelationshipsProps = { export type AssetRelationshipsProps = {
compact?: boolean; compact?: boolean;
companyId: string; companyId: string;
csrfToken: string;
assetId: string; assetId: string;
}; };

View File

@ -11,7 +11,7 @@ import { useAssetRelationshipsController } from "../controllers";
import type { AssetRelationshipsProps } from "../types"; import type { AssetRelationshipsProps } from "../types";
export function AssetRelationships(props: AssetRelationshipsProps) { export function AssetRelationships(props: AssetRelationshipsProps) {
const { companyId, csrfToken, assetId, compact = false } = props; const { companyId, assetId, compact = false } = props;
const { query, adding, setAdding, setEndingId, ending, active, ended, dateOf } = const { query, adding, setAdding, setEndingId, ending, active, ended, dateOf } =
useAssetRelationshipsController(props); useAssetRelationshipsController(props);
const { t } = useTranslation("assets"); const { t } = useTranslation("assets");
@ -85,7 +85,6 @@ export function AssetRelationships(props: AssetRelationshipsProps) {
)} )}
<DeleteRecordDialog <DeleteRecordDialog
companyId={companyId} companyId={companyId}
csrfToken={csrfToken}
onClose={() => setDeleteId(undefined)} onClose={() => setDeleteId(undefined)}
path={deleteId ? `/api/assets/${companyId}/relationships/${deleteId}/delete` : undefined} path={deleteId ? `/api/assets/${companyId}/relationships/${deleteId}/delete` : undefined}
queryKey={["inventory", companyId]} queryKey={["inventory", companyId]}
@ -94,7 +93,6 @@ export function AssetRelationships(props: AssetRelationshipsProps) {
<RelationshipEditorDialog <RelationshipEditorDialog
assetId={assetId} assetId={assetId}
companyId={companyId} companyId={companyId}
csrfToken={csrfToken}
onOpenChange={setAdding} onOpenChange={setAdding}
onSaved={() => setAdding(false)} onSaved={() => setAdding(false)}
/> />
@ -102,7 +100,6 @@ export function AssetRelationships(props: AssetRelationshipsProps) {
{ending && ( {ending && (
<EndRelationshipDialog <EndRelationshipDialog
companyId={companyId} companyId={companyId}
csrfToken={csrfToken}
onClose={() => setEndingId(undefined)} onClose={() => setEndingId(undefined)}
relationship={ending} relationship={ending}
sourceName={nameOf(ending.values.sourceId)} sourceName={nameOf(ending.values.sourceId)}

View File

@ -11,7 +11,6 @@ import type { RelationshipFormProps } from "../types";
export function useRelationshipFormController({ export function useRelationshipFormController({
companyId, companyId,
csrfToken,
assetId, assetId,
onSaved, onSaved,
}: RelationshipFormProps) { }: RelationshipFormProps) {
@ -29,8 +28,7 @@ export function useRelationshipFormController({
queryFn: () => getAssetInventory(companyId), queryFn: () => getAssetInventory(companyId),
}); });
const mutation = useMutation({ const mutation = useMutation({
mutationFn: (values: RelationshipInputDTO) => mutationFn: (values: RelationshipInputDTO) => createAssetRelationship(companyId, values),
createAssetRelationship(companyId, values, csrfToken),
onSuccess: async () => { onSuccess: async () => {
await client.invalidateQueries({ queryKey: inventoryKeys.company(companyId) }); await client.invalidateQueries({ queryKey: inventoryKeys.company(companyId) });
onSaved(); onSaved();

View File

@ -1,6 +1,5 @@
export type RelationshipFormProps = { export type RelationshipFormProps = {
companyId: string; companyId: string;
csrfToken: string;
assetId: string; assetId: string;
onSaved: () => void; onSaved: () => void;
}; };

View File

@ -6,13 +6,11 @@ import { RelationshipForm } from "./relationship-form";
export function RelationshipEditorDialog({ export function RelationshipEditorDialog({
companyId, companyId,
assetId, assetId,
csrfToken,
onSaved, onSaved,
onOpenChange, onOpenChange,
}: { }: {
companyId: string; companyId: string;
assetId: string; assetId: string;
csrfToken: string;
onSaved: () => void; onSaved: () => void;
onOpenChange: (open: boolean) => void; onOpenChange: (open: boolean) => void;
}) { }) {
@ -26,12 +24,7 @@ export function RelationshipEditorDialog({
open open
title={t("linkAsset")} title={t("linkAsset")}
> >
<RelationshipForm <RelationshipForm assetId={assetId} companyId={companyId} onSaved={onSaved} />
assetId={assetId}
companyId={companyId}
csrfToken={csrfToken}
onSaved={onSaved}
/>
</FormDialog> </FormDialog>
); );
} }

View File

@ -13,7 +13,7 @@ import { useRelationshipFormController } from "../controllers";
import type { RelationshipFormProps } from "../types"; import type { RelationshipFormProps } from "../types";
export function RelationshipForm(props: RelationshipFormProps) { export function RelationshipForm(props: RelationshipFormProps) {
const { companyId, csrfToken } = props; const { companyId } = props;
const { const {
typeId, typeId,
setTypeId, setTypeId,
@ -94,7 +94,6 @@ export function RelationshipForm(props: RelationshipFormProps) {
{quick && ( {quick && (
<CatalogRelationCreateDialog <CatalogRelationCreateDialog
companyId={companyId} companyId={companyId}
csrfToken={csrfToken}
onOpenChange={setQuick} onOpenChange={setQuick}
onSaved={(savedId) => { onSaved={(savedId) => {
setTypeId(savedId); setTypeId(savedId);

View File

@ -6,14 +6,12 @@ import { AssetForm } from "../../shared";
export function WorkstationAssetCreateDialog({ export function WorkstationAssetCreateDialog({
companyId, companyId,
csrfToken,
items, items,
lookups, lookups,
onSaved, onSaved,
onOpenChange, onOpenChange,
}: { }: {
companyId: string; companyId: string;
csrfToken: string;
items: ComponentProps<typeof AssetForm>["items"]; items: ComponentProps<typeof AssetForm>["items"];
lookups: ComponentProps<typeof AssetForm>["lookups"]; lookups: ComponentProps<typeof AssetForm>["lookups"];
onSaved: (id: string) => void; onSaved: (id: string) => void;
@ -34,7 +32,6 @@ export function WorkstationAssetCreateDialog({
<AssetForm <AssetForm
allowQuickCreate={false} allowQuickCreate={false}
companyId={companyId} companyId={companyId}
csrfToken={csrfToken}
items={items} items={items}
lookups={lookups} lookups={lookups}
onSaved={onSaved} onSaved={onSaved}

View File

@ -11,13 +11,11 @@ export function AssetDetailDialog({
asset, asset,
lookups, lookups,
companyId, companyId,
csrfToken,
onClose, onClose,
}: { }: {
asset: ComponentProps<typeof AssetDetail>["asset"]; asset: ComponentProps<typeof AssetDetail>["asset"];
lookups: ComponentProps<typeof AssetDetail>["lookups"]; lookups: ComponentProps<typeof AssetDetail>["lookups"];
companyId: string; companyId: string;
csrfToken: string;
onClose: () => void; onClose: () => void;
}) { }) {
const { t } = useTranslation("assets"); const { t } = useTranslation("assets");
@ -35,11 +33,10 @@ export function AssetDetailDialog({
wide wide
> >
<AssetDetail asset={asset} lookups={lookups}> <AssetDetail asset={asset} lookups={lookups}>
<AssetRelationships assetId={asset.id} companyId={companyId} csrfToken={csrfToken} /> <AssetRelationships assetId={asset.id} companyId={companyId} />
<AssetSupport <AssetSupport
assetId={asset.id} assetId={asset.id}
companyId={companyId} companyId={companyId}
csrfToken={csrfToken}
retired={asset.values.status === "retired"} retired={asset.values.status === "retired"}
/> />
</AssetDetail> </AssetDetail>

View File

@ -12,7 +12,6 @@ import { AssetDetail } from "./asset-detail";
export function AssetSelectionPanel({ export function AssetSelectionPanel({
companyId, companyId,
csrfToken,
asset, asset,
open, open,
lookups, lookups,
@ -22,7 +21,6 @@ export function AssetSelectionPanel({
onEdit, onEdit,
}: { }: {
companyId: string; companyId: string;
csrfToken: string;
asset: AssetDTO | undefined; asset: AssetDTO | undefined;
open: boolean; open: boolean;
lookups: InventoryLookups; lookups: InventoryLookups;
@ -58,19 +56,13 @@ export function AssetSelectionPanel({
</div> </div>
<AssetDetail asset={asset} compact lookups={lookups}> <AssetDetail asset={asset} compact lookups={lookups}>
<DetailSection title={t("relations")}> <DetailSection title={t("relations")}>
<AssetRelationships <AssetRelationships assetId={asset.id} compact companyId={companyId} />
assetId={asset.id}
compact
companyId={companyId}
csrfToken={csrfToken}
/>
</DetailSection> </DetailSection>
<DetailSection title={t("support.title")}> <DetailSection title={t("support.title")}>
<AssetSupport <AssetSupport
assetId={asset.id} assetId={asset.id}
compact compact
companyId={companyId} companyId={companyId}
csrfToken={csrfToken}
retired={asset.values.status === "retired"} retired={asset.values.status === "retired"}
/> />
</DetailSection> </DetailSection>

View File

@ -2,7 +2,6 @@ import type { InventoryLookups } from "../../shared/types";
export type AssetInventoryPageProps = { export type AssetInventoryPageProps = {
companyId: string; companyId: string;
csrfToken: string;
lookups: InventoryLookups; lookups: InventoryLookups;
initialId?: string | undefined; initialId?: string | undefined;
initialCreate?: boolean | undefined; initialCreate?: boolean | undefined;

View File

@ -23,7 +23,7 @@ function selectedRowClass(id: string, selectedId: string | undefined, detailOpen
} }
export function AssetInventoryPage(props: AssetInventoryPageProps) { export function AssetInventoryPage(props: AssetInventoryPageProps) {
const { companyId, csrfToken, lookups } = props; const { companyId, lookups } = props;
const { const {
query, query,
catalog, catalog,
@ -177,7 +177,6 @@ export function AssetInventoryPage(props: AssetInventoryPageProps) {
asset={selected} asset={selected}
canEdit={catalog.isSuccess} canEdit={catalog.isSuccess}
companyId={companyId} companyId={companyId}
csrfToken={csrfToken}
lookups={lookups} lookups={lookups}
onClose={() => setDetailOpen(false)} onClose={() => setDetailOpen(false)}
onEdit={setEditor} onEdit={setEditor}
@ -188,7 +187,6 @@ export function AssetInventoryPage(props: AssetInventoryPageProps) {
<AssetEditorDialog <AssetEditorDialog
asset={edited} asset={edited}
companyId={companyId} companyId={companyId}
csrfToken={csrfToken}
items={catalog.data.items} items={catalog.data.items}
lookups={lookups} lookups={lookups}
onClose={() => setEditor(undefined)} onClose={() => setEditor(undefined)}
@ -203,7 +201,6 @@ export function AssetInventoryPage(props: AssetInventoryPageProps) {
<AssetDetailDialog <AssetDetailDialog
asset={viewed} asset={viewed}
companyId={companyId} companyId={companyId}
csrfToken={csrfToken}
lookups={lookups} lookups={lookups}
onClose={() => setViewId(undefined)} onClose={() => setViewId(undefined)}
/> />

View File

@ -2,8 +2,8 @@ import type { AssetInput } from "../../../../common/inventory";
import { AssetResponseSchema } from "../../../../common/inventory"; import { AssetResponseSchema } from "../../../../common/inventory";
import { inventoryRequest } from "../../../shared/api/inventory-request"; import { inventoryRequest } from "../../../shared/api/inventory-request";
export async function saveAsset(companyId: string, values: AssetInput, token: string, id?: string) { export async function saveAsset(companyId: string, values: AssetInput, id?: string) {
return AssetResponseSchema.parse( return AssetResponseSchema.parse(
await inventoryRequest(`${companyId}${id ? `/${id}/update` : ""}`, { values }, token) await inventoryRequest(`${companyId}${id ? `/${id}/update` : ""}`, { values })
); );
} }

View File

@ -9,13 +9,7 @@ import { inventoryKeys } from "../../../shared/queries";
import { saveAsset } from "../api"; import { saveAsset } from "../api";
import type { AssetFormProps } from "../types"; import type { AssetFormProps } from "../types";
export function useAssetFormController({ export function useAssetFormController({ companyId, asset, items, onSaved }: AssetFormProps) {
companyId,
csrfToken,
asset,
items,
onSaved,
}: AssetFormProps) {
const client = useQueryClient(); const client = useQueryClient();
const [creatingTemplate, setCreatingTemplate] = useState(false); const [creatingTemplate, setCreatingTemplate] = useState(false);
const mounted = useRef(true); const mounted = useRef(true);
@ -68,7 +62,7 @@ export function useAssetFormController({
(template?.values.kind === "template" ? template.values.sections : undefined) ?? (template?.values.kind === "template" ? template.values.sections : undefined) ??
[]; [];
const mutation = useMutation({ const mutation = useMutation({
mutationFn: (input: AssetInput) => saveAsset(companyId, input, csrfToken, asset?.id), mutationFn: (input: AssetInput) => saveAsset(companyId, input, asset?.id),
onSuccess: async (response) => { onSuccess: async (response) => {
await Promise.all([ await Promise.all([
client.invalidateQueries({ queryKey: inventoryKeys.company(companyId) }), client.invalidateQueries({ queryKey: inventoryKeys.company(companyId) }),

View File

@ -67,7 +67,7 @@ test("Inventario traducido con alta de cabecera y sin formulario junto al listad
for (const language of ["es", "en"]) { for (const language of ["es", "en"]) {
const html = await render( const html = await render(
language, language,
<AssetInventoryPage companyId={companyId} csrfToken="test" lookups={lookups} /> <AssetInventoryPage companyId={companyId} lookups={lookups} />
); );
assert.equal(html.includes("<form"), false); assert.equal(html.includes("<form"), false);
@ -84,13 +84,7 @@ test("Inventario traducido con alta de cabecera y sin formulario junto al listad
test("El activo ofrece altas relacionadas, ayuda inferior y no encadena una tercera alta", async () => { test("El activo ofrece altas relacionadas, ayuda inferior y no encadena una tercera alta", async () => {
const html = await render( const html = await render(
"es", "es",
<AssetForm <AssetForm companyId={companyId} items={[]} lookups={lookups} onSaved={() => undefined} />
companyId={companyId}
csrfToken="test"
items={[]}
lookups={lookups}
onSaved={() => undefined}
/>
); );
for (const label of ["Nueva plantilla", "Nueva sede"]) { for (const label of ["Nueva plantilla", "Nueva sede"]) {
@ -102,7 +96,6 @@ test("El activo ofrece altas relacionadas, ayuda inferior y no encadena una terc
"es", "es",
<CatalogTemplateForm <CatalogTemplateForm
allowQuickCreate={false} allowQuickCreate={false}
csrfToken="test"
items={[]} items={[]}
onSaved={() => undefined} onSaved={() => undefined}
scope={companyId} scope={companyId}
@ -136,12 +129,12 @@ test("Las fichas leen ambos sentidos y conservan la auditoría de relaciones ret
}; };
const source = await render( const source = await render(
"es", "es",
<AssetRelationships assetId={sourceId} companyId={companyId} csrfToken="test" />, <AssetRelationships assetId={sourceId} companyId={companyId} />,
[link] [link]
); );
const target = await render( const target = await render(
"es", "es",
<AssetRelationships assetId={targetId} companyId={companyId} csrfToken="test" />, <AssetRelationships assetId={targetId} companyId={companyId} />,
[link] [link]
); );
@ -151,7 +144,7 @@ test("Las fichas leen ambos sentidos y conservan la auditoría de relaciones ret
assert.ok(source.includes("Retirar relación")); assert.ok(source.includes("Retirar relación"));
const ended = await render( const ended = await render(
"es", "es",
<AssetRelationships assetId={sourceId} companyId={companyId} csrfToken="test" />, <AssetRelationships assetId={sourceId} companyId={companyId} />,
[ [
{ {
...link, ...link,
@ -170,7 +163,7 @@ test("El formulario de tipo de relación presenta ayuda justo antes de sus opera
for (const language of ["es", "en"]) { for (const language of ["es", "en"]) {
const html = await render( const html = await render(
language, language,
<CatalogRelationForm csrfToken="test" onSaved={() => undefined} scope={companyId} /> <CatalogRelationForm onSaved={() => undefined} scope={companyId} />
); );
assert.ok(html.indexOf('name="description"') < html.indexOf("<aside")); assert.ok(html.indexOf('name="description"') < html.indexOf("<aside"));
@ -187,7 +180,7 @@ test("Puestos de trabajo: listado traducido, alta en cabecera y formulario modal
for (const language of ["es", "en"]) { for (const language of ["es", "en"]) {
const html = await render( const html = await render(
language, language,
<WorkstationsPage companyId={companyId} csrfToken="test" lookups={lookups} /> <WorkstationsPage companyId={companyId} lookups={lookups} />
); );
assert.ok(html.includes(language === "es" ? "Nuevo puesto" : "New workstation")); assert.ok(html.includes(language === "es" ? "Nuevo puesto" : "New workstation"));
@ -204,12 +197,7 @@ test("Puestos de trabajo: listado traducido, alta en cabecera y formulario modal
test("Puestos de trabajo: altas rápidas y ayuda inferior; un activo anidado no ofrece terceras altas", async () => { test("Puestos de trabajo: altas rápidas y ayuda inferior; un activo anidado no ofrece terceras altas", async () => {
const html = await render( const html = await render(
"es", "es",
<WorkstationForm <WorkstationForm companyId={companyId} lookups={lookups} onSaved={() => undefined} />
companyId={companyId}
csrfToken="test"
lookups={lookups}
onSaved={() => undefined}
/>
); );
for (const label of ["Nueva sede", "Nueva persona", "Nuevo activo"]) { for (const label of ["Nueva sede", "Nueva persona", "Nuevo activo"]) {
@ -222,7 +210,6 @@ test("Puestos de trabajo: altas rápidas y ayuda inferior; un activo anidado no
<AssetForm <AssetForm
allowQuickCreate={false} allowQuickCreate={false}
companyId={companyId} companyId={companyId}
csrfToken="test"
items={[]} items={[]}
lookups={lookups} lookups={lookups}
onSaved={() => undefined} onSaved={() => undefined}
@ -279,7 +266,6 @@ test("la ficha lateral agrupa información y ofrece acciones superiores y enlace
asset={asset} asset={asset}
canEdit canEdit
companyId={companyId} companyId={companyId}
csrfToken="test"
lookups={{ lookups={{
...lookups, ...lookups,
sites: [{ id: siteId, name: "Fábrica" }], sites: [{ id: siteId, name: "Fábrica" }],

View File

@ -17,7 +17,6 @@ export type InventoryLookups = {
export type AssetFormProps = { export type AssetFormProps = {
companyId: string; companyId: string;
csrfToken: string;
asset?: AssetDTO | undefined; asset?: AssetDTO | undefined;
items: readonly CatalogItemDTO[]; items: readonly CatalogItemDTO[];
lookups: InventoryLookups; lookups: InventoryLookups;

View File

@ -7,7 +7,6 @@ import { AssetForm } from "./asset-form";
export function AssetEditorDialog({ export function AssetEditorDialog({
asset, asset,
companyId, companyId,
csrfToken,
items, items,
lookups, lookups,
onSaved, onSaved,
@ -15,7 +14,6 @@ export function AssetEditorDialog({
}: { }: {
asset: ComponentProps<typeof AssetForm>["asset"]; asset: ComponentProps<typeof AssetForm>["asset"];
companyId: string; companyId: string;
csrfToken: string;
items: ComponentProps<typeof AssetForm>["items"]; items: ComponentProps<typeof AssetForm>["items"];
lookups: ComponentProps<typeof AssetForm>["lookups"]; lookups: ComponentProps<typeof AssetForm>["lookups"];
onSaved: (id: string) => void; onSaved: (id: string) => void;
@ -39,7 +37,6 @@ export function AssetEditorDialog({
<AssetForm <AssetForm
asset={asset} asset={asset}
companyId={companyId} companyId={companyId}
csrfToken={csrfToken}
items={items} items={items}
lookups={lookups} lookups={lookups}
onSaved={onSaved} onSaved={onSaved}

View File

@ -20,7 +20,7 @@ import type { AssetFormProps } from "../types";
import { AssetCharacteristicControl } from "./asset-characteristic-control"; import { AssetCharacteristicControl } from "./asset-characteristic-control";
export function AssetForm(props: AssetFormProps) { export function AssetForm(props: AssetFormProps) {
const { companyId, csrfToken, asset, items, lookups, allowQuickCreate = true } = props; const { companyId, asset, items, lookups, allowQuickCreate = true } = props;
const { const {
creatingTemplate, creatingTemplate,
setCreatingTemplate, setCreatingTemplate,
@ -125,7 +125,6 @@ export function AssetForm(props: AssetFormProps) {
<BrandSelect <BrandSelect
allowQuickCreate={allowQuickCreate} allowQuickCreate={allowQuickCreate}
brandId={values.brandId ?? null} brandId={values.brandId ?? null}
csrfToken={csrfToken}
disabled={mutation.isPending} disabled={mutation.isPending}
id={`${prefix}-${field.fieldId}`} id={`${prefix}-${field.fieldId}`}
key={field.fieldId} key={field.fieldId}
@ -190,7 +189,6 @@ export function AssetForm(props: AssetFormProps) {
</form> </form>
{creatingTemplate && ( {creatingTemplate && (
<CatalogTemplateCreateDialog <CatalogTemplateCreateDialog
csrfToken={csrfToken}
items={items} items={items}
onOpenChange={(open) => { onOpenChange={(open) => {
if (!open) { if (!open) {

View File

@ -5,11 +5,11 @@ import type { BrandsResponseSchema } from "../../../../common/brands";
import { brandApi, brandKeys } from "../../shared/api"; import { brandApi, brandKeys } from "../../shared/api";
import type { DeleteBrandDialogProps } from "../types"; import type { DeleteBrandDialogProps } from "../types";
export function useDeleteBrandController({ brand, csrfToken }: DeleteBrandDialogProps) { export function useDeleteBrandController({ brand }: DeleteBrandDialogProps) {
const client = useQueryClient(); const client = useQueryClient();
return useMutation({ return useMutation({
mutationFn: () => brandApi.remove(brand.id, csrfToken), mutationFn: () => brandApi.remove(brand.id),
onSuccess: async (response) => { onSuccess: async (response) => {
client.setQueryData<z.infer<typeof BrandsResponseSchema>>(brandKeys.all, (previous) => client.setQueryData<z.infer<typeof BrandsResponseSchema>>(brandKeys.all, (previous) =>
previous previous

View File

@ -2,6 +2,5 @@ import type { BrandDTO } from "../../../../common/brands";
export type DeleteBrandDialogProps = { export type DeleteBrandDialogProps = {
brand: BrandDTO; brand: BrandDTO;
csrfToken: string;
onClose: () => void; onClose: () => void;
}; };

View File

@ -1,3 +0,0 @@
export type BrandCatalogPageProps = {
csrfToken: string;
};

View File

@ -1 +0,0 @@
export type { BrandCatalogPageProps } from "./brands-page-props";

View File

@ -14,10 +14,8 @@ import { DeleteBrandDialog } from "../../delete";
import { BrandEditorDialog } from "../../shared"; import { BrandEditorDialog } from "../../shared";
import { BrandLogo } from "../../shared/ui"; import { BrandLogo } from "../../shared/ui";
import { useBrandCatalogPageController } from "../controllers"; import { useBrandCatalogPageController } from "../controllers";
import type { BrandCatalogPageProps } from "../types";
export function BrandCatalogPage(props: BrandCatalogPageProps) { export function BrandCatalogPage() {
const { csrfToken } = props;
const { query, edit, setEdit, open, setOpen, deleting, setDeleting, status, setStatus, items } = const { query, edit, setEdit, open, setOpen, deleting, setDeleting, status, setStatus, items } =
useBrandCatalogPageController(); useBrandCatalogPageController();
const { t, i18n } = useTranslation("assets"); const { t, i18n } = useTranslation("assets");
@ -137,19 +135,12 @@ export function BrandCatalogPage(props: BrandCatalogPageProps) {
unavailable={query.isError} unavailable={query.isError}
/> />
<BrandEditorDialog <BrandEditorDialog
csrfToken={csrfToken}
onOpenChange={setOpen} onOpenChange={setOpen}
open={open} open={open}
{...(edit ? { brand: edit } : {})} {...(edit ? { brand: edit } : {})}
onSaved={() => setOpen(false)} onSaved={() => setOpen(false)}
/> />
{deleting && ( {deleting && <DeleteBrandDialog brand={deleting} onClose={() => setDeleting(undefined)} />}
<DeleteBrandDialog
brand={deleting}
csrfToken={csrfToken}
onClose={() => setDeleting(undefined)}
/>
)}
</div> </div>
); );
} }

View File

@ -1,3 +1,4 @@
import { csrfFetch } from "@metis/core/client";
import { z } from "zod/v4"; import { z } from "zod/v4";
import type { BrandInput } from "../../../../common/brands"; import type { BrandInput } from "../../../../common/brands";
@ -11,16 +12,11 @@ export class BrandApiError extends Error {
} }
} }
async function request( async function request(path = "", body?: { values: BrandInput } | Record<string, never>) {
path = "", const response = await csrfFetch(`/api/asset-brands${path}`, {
body?: { values: BrandInput } | Record<string, never>,
token?: string
) {
const response = await fetch(`/api/asset-brands${path}`, {
method: body === undefined ? "GET" : "POST", method: body === undefined ? "GET" : "POST",
credentials: "same-origin", credentials: "same-origin",
headers: headers: body === undefined ? {} : { "content-type": "application/json" },
body === undefined ? {} : { "content-type": "application/json", "x-csrf-token": token ?? "" },
...(body === undefined ? {} : { body: JSON.stringify(body) }), ...(body === undefined ? {} : { body: JSON.stringify(body) }),
}); });
const data: unknown = await response.json(); const data: unknown = await response.json();
@ -38,10 +34,10 @@ export const brandApi = {
async list() { async list() {
return BrandsResponseSchema.parse(await request()); return BrandsResponseSchema.parse(await request());
}, },
async save(values: BrandInput, token: string, id?: string) { async save(values: BrandInput, id?: string) {
return BrandResponseSchema.parse(await request(id ? `/${id}/update` : "", { values }, token)); return BrandResponseSchema.parse(await request(id ? `/${id}/update` : "", { values }));
}, },
async remove(id: string, token: string) { async remove(id: string) {
return BrandResponseSchema.parse(await request(`/${id}/delete`, {}, token)); return BrandResponseSchema.parse(await request(`/${id}/delete`, {}));
}, },
}; };

View File

@ -1,6 +1,7 @@
import assert from "node:assert/strict"; import assert from "node:assert/strict";
import { test } from "node:test"; import { test } from "node:test";
import { configureCsrfTokenReader } from "@metis/core/client";
import { UniqueID } from "@repo/rdx-ddd"; import { UniqueID } from "@repo/rdx-ddd";
import { Maybe } from "@repo/rdx-utils"; import { Maybe } from "@repo/rdx-utils";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
@ -51,7 +52,7 @@ async function render(language: string, node: ReactNode, active = true) {
test("Marcas: listado común traducido, filtros y alta modal sin formulario junto a la tabla", async () => { test("Marcas: listado común traducido, filtros y alta modal sin formulario junto a la tabla", async () => {
for (const language of ["es", "en"]) { for (const language of ["es", "en"]) {
const html = await render(language, <BrandCatalogPage csrfToken="test" />); const html = await render(language, <BrandCatalogPage />);
assert.equal(html.includes("<form"), false); assert.equal(html.includes("<form"), false);
assert.ok(html.includes(language === "es" ? "Añadir marca" : "Add brand")); assert.ok(html.includes(language === "es" ? "Añadir marca" : "Add brand"));
@ -64,24 +65,19 @@ test("Marcas: listado común traducido, filtros y alta modal sin formulario junt
} }
}); });
test("Marcas: nombre de serie protegido y ayuda inferior; alta rápida siempre activa", async () => { test("Marcas: nombre de serie protegido y ayuda inferior; alta rápida siempre activa", async () => {
const html = await render( const html = await render("es", <BrandForm brand={brand} onSaved={() => undefined} />);
"es",
<BrandForm brand={brand} csrfToken="test" onSaved={() => undefined} />
);
assert.ok(html.includes('readOnly=""')); assert.ok(html.includes('readOnly=""'));
assert.ok(html.includes('accept="image/svg+xml,.svg"')); assert.ok(html.includes('accept="image/svg+xml,.svg"'));
assert.ok(html.includes("Quitar logotipo")); assert.ok(html.includes("Quitar logotipo"));
assert.ok(html.indexOf("Una marca identifica") < html.lastIndexOf("Guardar")); assert.ok(html.indexOf("Una marca identifica") < html.lastIndexOf("Guardar"));
const quick = await render( const quick = await render("en", <BrandForm onSaved={() => undefined} quickCreate />);
"en",
<BrandForm csrfToken="test" onSaved={() => undefined} quickCreate />
);
assert.equal(quick.includes('type="checkbox"'), false); assert.equal(quick.includes('type="checkbox"'), false);
assert.ok(quick.includes("Global scope")); assert.ok(quick.includes("Global scope"));
}); });
test("Marcas: eliminar envía CSRF y cuerpo vacío, valida la respuesta y propaga conflictos", async () => { test("Marcas: eliminar envía CSRF y cuerpo vacío, valida la respuesta y propaga conflictos", async (context) => {
context.after(configureCsrfTokenReader(() => "a".repeat(64)));
const originalFetch = globalThis.fetch; const originalFetch = globalThis.fetch;
let status = 200; let status = 200;
let response: unknown = { brand: { ...brand, builtin: false } }; let response: unknown = { brand: { ...brand, builtin: false } };
@ -93,7 +89,7 @@ test("Marcas: eliminar envía CSRF y cuerpo vacío, valida la respuesta y propag
assert.equal(options?.body, "{}"); assert.equal(options?.body, "{}");
const headers = new Headers(options?.headers); const headers = new Headers(options?.headers);
assert.equal(headers.get("x-csrf-token"), "test"); assert.equal(headers.get("x-csrf-token"), "a".repeat(64));
assert.equal(headers.get("content-type"), "application/json"); assert.equal(headers.get("content-type"), "application/json");
return new Response(JSON.stringify(response), { return new Response(JSON.stringify(response), {
@ -102,14 +98,14 @@ test("Marcas: eliminar envía CSRF y cuerpo vacío, valida la respuesta y propag
}); });
}; };
try { try {
assert.deepEqual(await brandApi.remove(brand.id, "test"), response); assert.deepEqual(await brandApi.remove(brand.id), response);
response = { brand: { id: "invalid" } }; response = { brand: { id: "invalid" } };
await assert.rejects(() => brandApi.remove(brand.id, "test")); await assert.rejects(() => brandApi.remove(brand.id));
for (const code of ["BRAND_IN_USE", "BUILTIN_PROTECTED", "BRAND_NOT_FOUND"]) { for (const code of ["BRAND_IN_USE", "BUILTIN_PROTECTED", "BRAND_NOT_FOUND"]) {
status = code === "BRAND_NOT_FOUND" ? 404 : 409; status = code === "BRAND_NOT_FOUND" ? 404 : 409;
response = { code }; response = { code };
await assert.rejects( await assert.rejects(
() => brandApi.remove(brand.id, "test"), () => brandApi.remove(brand.id),
(error: unknown) => error instanceof BrandApiError && error.code === code (error: unknown) => error instanceof BrandApiError && error.code === code
); );
} }
@ -123,7 +119,6 @@ test("Marcas: selector mantiene valores antiguos e inactivos y respeta el límit
label: "Marca", label: "Marca",
brandId: brand.id, brandId: brand.id,
legacyName: "LG", legacyName: "LG",
csrfToken: "test",
disabled: false, disabled: false,
required: false, required: false,
allowQuickCreate: true, allowQuickCreate: true,
@ -183,7 +178,6 @@ test("Marcas: el campo de serie se identifica por referencia de catálogo, no po
<AssetForm <AssetForm
asset={asset} asset={asset}
companyId={asset.companyId} companyId={asset.companyId}
csrfToken="test"
items={items} items={items}
lookups={{ lookups={{
sites: [], sites: [],

View File

@ -8,7 +8,7 @@ import { BRAND_LOGO_MAX_BYTES, BrandInputSchema, BrandLogoSchema } from "../../.
import { brandApi, brandKeys } from "../api"; import { brandApi, brandKeys } from "../api";
import type { BrandFormProps } from "../types"; import type { BrandFormProps } from "../types";
export function useBrandFormController({ csrfToken, brand, onSaved }: BrandFormProps) { export function useBrandFormController({ brand, onSaved }: BrandFormProps) {
const mounted = useRef(true); const mounted = useRef(true);
useEffect(() => { useEffect(() => {
@ -75,7 +75,7 @@ export function useBrandFormController({ csrfToken, brand, onSaved }: BrandFormP
}; };
const mutation = useMutation({ const mutation = useMutation({
mutationFn: (values: BrandInput) => brandApi.save(values, csrfToken, brand?.id), mutationFn: (values: BrandInput) => brandApi.save(values, brand?.id),
onSuccess: async (response) => { onSuccess: async (response) => {
// Publicar primero garantiza que el selector pueda mostrar y seleccionar el nuevo valor. // Publicar primero garantiza que el selector pueda mostrar y seleccionar el nuevo valor.
client.setQueryData<{ client.setQueryData<{

View File

@ -1,7 +1,6 @@
import type { BrandDTO } from "../../../../common/brands"; import type { BrandDTO } from "../../../../common/brands";
export type BrandFormProps = { export type BrandFormProps = {
csrfToken: string;
brand?: BrandDTO; brand?: BrandDTO;
onSaved: (brand: BrandDTO) => void; onSaved: (brand: BrandDTO) => void;
quickCreate?: boolean; quickCreate?: boolean;

View File

@ -5,7 +5,6 @@ export type BrandSelectProps = {
label: string; label: string;
brandId: string | null; brandId: string | null;
legacyName: string; legacyName: string;
csrfToken: string;
disabled: boolean; disabled: boolean;
required: boolean; required: boolean;
allowQuickCreate: boolean; allowQuickCreate: boolean;

View File

@ -11,17 +11,7 @@ import { BrandEditorDialog } from "./brand-editor-dialog";
const LEGACY = "legacy"; const LEGACY = "legacy";
export function BrandSelect(props: BrandSelectProps) { export function BrandSelect(props: BrandSelectProps) {
const { const { id, label, brandId, legacyName, disabled, required, allowQuickCreate, onChange } = props;
id,
label,
brandId,
legacyName,
csrfToken,
disabled,
required,
allowQuickCreate,
onChange,
} = props;
const { query, open, setOpen, brands, legacy, options } = useBrandSelectController(props); const { query, open, setOpen, brands, legacy, options } = useBrandSelectController(props);
const { t } = useTranslation("assets"); const { t } = useTranslation("assets");
@ -73,7 +63,6 @@ export function BrandSelect(props: BrandSelectProps) {
)} )}
{open && ( {open && (
<BrandEditorDialog <BrandEditorDialog
csrfToken={csrfToken}
onOpenChange={setOpen} onOpenChange={setOpen}
onSaved={(brand) => { onSaved={(brand) => {
onChange(brand); onChange(brand);

View File

@ -5,12 +5,10 @@ import { CatalogFieldForm } from "../../shared";
export function CatalogFieldCreateDialog({ export function CatalogFieldCreateDialog({
scope, scope,
csrfToken,
onSaved, onSaved,
onOpenChange, onOpenChange,
}: { }: {
scope: string; scope: string;
csrfToken: string;
onSaved: (id: string) => void; onSaved: (id: string) => void;
onOpenChange: (open: boolean) => void; onOpenChange: (open: boolean) => void;
}) { }) {
@ -25,7 +23,7 @@ export function CatalogFieldCreateDialog({
title={t("newField")} title={t("newField")}
> >
<p className="text-sm text-muted-foreground">{t("quickNotice")}</p> <p className="text-sm text-muted-foreground">{t("quickNotice")}</p>
<CatalogFieldForm csrfToken={csrfToken} onSaved={onSaved} scope={scope} /> <CatalogFieldForm onSaved={onSaved} scope={scope} />
</FormDialog> </FormDialog>
); );
} }

View File

@ -5,12 +5,7 @@ import { useCatalogSave } from "../../../catalog/shared/controllers";
import { FieldFormSchema } from "../schemas"; import { FieldFormSchema } from "../schemas";
import type { CatalogFieldFormProps, FieldFormValues } from "../types"; import type { CatalogFieldFormProps, FieldFormValues } from "../types";
export function useCatalogFieldFormController({ export function useCatalogFieldFormController({ scope, item, onSaved }: CatalogFieldFormProps) {
scope,
csrfToken,
item,
onSaved,
}: CatalogFieldFormProps) {
const current = item?.values.kind === "field" ? item.values : undefined; const current = item?.values.kind === "field" ? item.values : undefined;
const form = useForm<FieldFormValues>({ const form = useForm<FieldFormValues>({
resolver: zodResolver(FieldFormSchema), resolver: zodResolver(FieldFormSchema),
@ -23,7 +18,7 @@ export function useCatalogFieldFormController({
choicesText: current?.choices.join("\n") ?? "", choicesText: current?.choices.join("\n") ?? "",
}, },
}); });
const mutation = useCatalogSave(scope, csrfToken, onSaved, item?.id); const mutation = useCatalogSave(scope, onSaved, item?.id);
const type = form.watch("type"); const type = form.watch("type");
const submit = form.handleSubmit(({ choicesText, ...values }) => const submit = form.handleSubmit(({ choicesText, ...values }) =>
mutation.mutate({ mutation.mutate({

View File

@ -8,7 +8,6 @@ export type FieldFormValues = z.infer<typeof FieldFormSchema>;
export type CatalogFieldFormProps = { export type CatalogFieldFormProps = {
scope: string; scope: string;
csrfToken: string;
item?: CatalogItemDTO | undefined; item?: CatalogItemDTO | undefined;
onSaved: CatalogSaved; onSaved: CatalogSaved;
}; };

View File

@ -5,12 +5,10 @@ import { CatalogRelationForm } from "../../shared";
export function CatalogRelationCreateDialog({ export function CatalogRelationCreateDialog({
companyId, companyId,
csrfToken,
onSaved, onSaved,
onOpenChange, onOpenChange,
}: { }: {
companyId: string; companyId: string;
csrfToken: string;
onSaved: (id: string) => void; onSaved: (id: string) => void;
onOpenChange: (open: boolean) => void; onOpenChange: (open: boolean) => void;
}) { }) {
@ -25,7 +23,7 @@ export function CatalogRelationCreateDialog({
title={t("newRelation")} title={t("newRelation")}
> >
<p className="text-sm text-muted-foreground">{t("quickRelationNotice")}</p> <p className="text-sm text-muted-foreground">{t("quickRelationNotice")}</p>
<CatalogRelationForm csrfToken={csrfToken} onSaved={onSaved} scope={companyId} /> <CatalogRelationForm onSaved={onSaved} scope={companyId} />
</FormDialog> </FormDialog>
); );
} }

View File

@ -8,7 +8,6 @@ import type { CatalogRelationFormProps } from "../types";
export function useCatalogRelationFormController({ export function useCatalogRelationFormController({
scope, scope,
csrfToken,
item, item,
onSaved, onSaved,
}: CatalogRelationFormProps) { }: CatalogRelationFormProps) {
@ -23,7 +22,7 @@ export function useCatalogRelationFormController({
symmetric: false, symmetric: false,
}, },
}); });
const mutation = useCatalogSave(scope, csrfToken, onSaved, item?.id); const mutation = useCatalogSave(scope, onSaved, item?.id);
const symmetric = form.watch("symmetric"); const symmetric = form.watch("symmetric");
const submit = form.handleSubmit((values) => mutation.mutate(values)); const submit = form.handleSubmit((values) => mutation.mutate(values));

View File

@ -3,7 +3,6 @@ import type { CatalogSaved } from "../../../catalog/shared/controllers";
export type CatalogRelationFormProps = { export type CatalogRelationFormProps = {
scope: string; scope: string;
csrfToken: string;
item?: CatalogItemDTO | undefined; item?: CatalogItemDTO | undefined;
onSaved: CatalogSaved; onSaved: CatalogSaved;
}; };

View File

@ -6,13 +6,11 @@ import { CatalogTemplateForm } from "../../shared";
export function CatalogTemplateCreateDialog({ export function CatalogTemplateCreateDialog({
scope, scope,
csrfToken,
items, items,
onSaved, onSaved,
onOpenChange, onOpenChange,
}: { }: {
scope: string; scope: string;
csrfToken: string;
items: ComponentProps<typeof CatalogTemplateForm>["items"]; items: ComponentProps<typeof CatalogTemplateForm>["items"];
onSaved: (id: string) => void; onSaved: (id: string) => void;
onOpenChange: (open: boolean) => void; onOpenChange: (open: boolean) => void;
@ -29,13 +27,7 @@ export function CatalogTemplateCreateDialog({
wide wide
> >
<p className="text-sm text-muted-foreground">{t("relatedNotice")}</p> <p className="text-sm text-muted-foreground">{t("relatedNotice")}</p>
<CatalogTemplateForm <CatalogTemplateForm allowQuickCreate={false} items={items} onSaved={onSaved} scope={scope} />
allowQuickCreate={false}
csrfToken={csrfToken}
items={items}
onSaved={onSaved}
scope={scope}
/>
</FormDialog> </FormDialog>
); );
} }

View File

@ -11,7 +11,6 @@ import type { CatalogTemplateFormProps, TemplateFormValues } from "../types";
export function useCatalogTemplateFormController({ export function useCatalogTemplateFormController({
scope, scope,
csrfToken,
item, item,
copy, copy,
items, items,
@ -36,7 +35,7 @@ export function useCatalogTemplateFormController({
sections: layout.sections, sections: layout.sections,
}, },
}); });
const mutation = useCatalogSave(scope, csrfToken, onSaved, item?.id); const mutation = useCatalogSave(scope, onSaved, item?.id);
const selected = form.watch("fields"); const selected = form.watch("fields");
const fields = items.filter((entry) => entry.values.kind === "field"); const fields = items.filter((entry) => entry.values.kind === "field");

View File

@ -8,7 +8,6 @@ export type TemplateFormValues = z.infer<typeof TemplateFormSchema>;
export type CatalogTemplateFormProps = { export type CatalogTemplateFormProps = {
scope: string; scope: string;
csrfToken: string;
item?: CatalogItemDTO | undefined; item?: CatalogItemDTO | undefined;
copy?: CatalogItemDTO | undefined; copy?: CatalogItemDTO | undefined;
items: readonly CatalogItemDTO[]; items: readonly CatalogItemDTO[];

View File

@ -17,7 +17,7 @@ import { useCatalogTemplateFormController } from "../controllers";
import type { CatalogTemplateFormProps } from "../types"; import type { CatalogTemplateFormProps } from "../types";
export function CatalogTemplateForm(props: CatalogTemplateFormProps) { export function CatalogTemplateForm(props: CatalogTemplateFormProps) {
const { scope, csrfToken, allowQuickCreate = true } = props; const { scope, allowQuickCreate = true } = props;
const { creatingField, setCreatingField, form, mutation, selected, fields, add, submit } = const { creatingField, setCreatingField, form, mutation, selected, fields, add, submit } =
useCatalogTemplateFormController(props); useCatalogTemplateFormController(props);
const { t } = useTranslation("assets"); const { t } = useTranslation("assets");
@ -117,7 +117,6 @@ export function CatalogTemplateForm(props: CatalogTemplateFormProps) {
</form> </form>
{creatingField && ( {creatingField && (
<CatalogFieldCreateDialog <CatalogFieldCreateDialog
csrfToken={csrfToken}
onOpenChange={(open) => { onOpenChange={(open) => {
if (!open) { if (!open) {
setCreatingField(false); setCreatingField(false);

View File

@ -7,7 +7,7 @@ import type { CatalogEditor } from "../../shared";
import { catalogApi, catalogKeys } from "../../shared/api"; import { catalogApi, catalogKeys } from "../../shared/api";
import type { AssetCatalogPageProps } from "../types"; import type { AssetCatalogPageProps } from "../types";
export function useAssetCatalogPageController({ scope, csrfToken }: AssetCatalogPageProps) { export function useAssetCatalogPageController({ scope }: AssetCatalogPageProps) {
const client = useQueryClient(); const client = useQueryClient();
const { searchParams, setSearchParams } = useBrowserSearchParams(); const { searchParams, setSearchParams } = useBrowserSearchParams();
const requestedTab = searchParams.get("tab"); const requestedTab = searchParams.get("tab");
@ -42,7 +42,7 @@ export function useAssetCatalogPageController({ scope, csrfToken }: AssetCatalog
}); });
const mutation = useMutation({ const mutation = useMutation({
mutationFn: ({ item, active }: { item: CatalogItemDTO; active: boolean | null }) => mutationFn: ({ item, active }: { item: CatalogItemDTO; active: boolean | null }) =>
catalogApi.status(scope, item.id, active, csrfToken), catalogApi.status(scope, item.id, active),
onSuccess: async (response) => { onSuccess: async (response) => {
await client.invalidateQueries({ queryKey: ["asset-catalog"] }); await client.invalidateQueries({ queryKey: ["asset-catalog"] });
client.setQueryData(catalogKeys.scope(scope), response); client.setQueryData(catalogKeys.scope(scope), response);

View File

@ -1,4 +1,3 @@
export type AssetCatalogPageProps = { export type AssetCatalogPageProps = {
scope: string; scope: string;
csrfToken: string;
}; };

View File

@ -10,7 +10,7 @@ import type { AssetCatalogPageProps } from "../types";
import { CatalogCell } from "./catalog-cell"; import { CatalogCell } from "./catalog-cell";
export function AssetCatalogPage(props: AssetCatalogPageProps) { export function AssetCatalogPage(props: AssetCatalogPageProps) {
const { scope, csrfToken } = props; const { scope } = props;
const { const {
tab, tab,
setTab, setTab,
@ -177,7 +177,6 @@ export function AssetCatalogPage(props: AssetCatalogPageProps) {
<CatalogDefinitionDialog item={view} items={items} onClose={() => setViewId(undefined)} /> <CatalogDefinitionDialog item={view} items={items} onClose={() => setViewId(undefined)} />
{editor && ( {editor && (
<CatalogEditorDialog <CatalogEditorDialog
csrfToken={csrfToken}
editor={editor} editor={editor}
items={items} items={items}
onClose={() => setEditor(undefined)} onClose={() => setEditor(undefined)}

View File

@ -1,3 +1,4 @@
import { csrfFetch } from "@metis/core/client";
import { z } from "zod/v4"; import { z } from "zod/v4";
import type { AssetDefinitionSpec } from "../../../../common"; import type { AssetDefinitionSpec } from "../../../../common";
@ -11,14 +12,11 @@ export class CatalogApiError extends Error {
} }
} }
async function request(path: string, body?: unknown, csrfToken?: string) { async function request(path: string, body?: unknown) {
const response = await fetch(`/api/asset-catalog/${path}`, { const response = await csrfFetch(`/api/asset-catalog/${path}`, {
method: body === undefined ? "GET" : "POST", method: body === undefined ? "GET" : "POST",
credentials: "same-origin", credentials: "same-origin",
headers: headers: body === undefined ? {} : { "content-type": "application/json" },
body === undefined
? {}
: { "content-type": "application/json", "x-csrf-token": csrfToken ?? "" },
...(body === undefined ? {} : { body: JSON.stringify(body) }), ...(body === undefined ? {} : { body: JSON.stringify(body) }),
}); });
const json: unknown = await response.json(); const json: unknown = await response.json();
@ -36,14 +34,12 @@ export const catalogApi = {
async get(scope: string) { async get(scope: string) {
return CatalogResponseSchema.parse(await request(scope)); return CatalogResponseSchema.parse(await request(scope));
}, },
async save(scope: string, values: AssetDefinitionSpec, csrfToken: string, id?: string) { async save(scope: string, values: AssetDefinitionSpec, id?: string) {
return SaveDefinitionResponseSchema.parse( return SaveDefinitionResponseSchema.parse(
await request(`${scope}${id ? `/${id}/update` : ""}`, { values }, csrfToken) await request(`${scope}${id ? `/${id}/update` : ""}`, { values })
); );
}, },
async status(scope: string, id: string, active: boolean | null, csrfToken: string) { async status(scope: string, id: string, active: boolean | null) {
return CatalogResponseSchema.parse( return CatalogResponseSchema.parse(await request(`${scope}/${id}/status`, { active }));
await request(`${scope}/${id}/status`, { active }, csrfToken)
);
}, },
}; };

View File

@ -48,7 +48,7 @@ test("el catálogo muestra plantillas de serie y acción de alta sin formularios
["en", "New template", "Actions"], ["en", "New template", "Actions"],
]) { ]) {
assert.ok(language && create && actions); assert.ok(language && create && actions);
const html = await render(language, <AssetCatalogPage csrfToken="test" scope="global" />); const html = await render(language, <AssetCatalogPage scope="global" />);
assert.ok(html.includes("Portátil")); assert.ok(html.includes("Portátil"));
assert.ok(html.includes(create)); assert.ok(html.includes(create));
@ -58,17 +58,14 @@ test("el catálogo muestra plantillas de serie y acción de alta sin formularios
} }
}); });
test("los formularios tienen ayuda inferior y selector de campo con alta integrada", async () => { test("los formularios tienen ayuda inferior y selector de campo con alta integrada", async () => {
const field = await render( const field = await render("es", <CatalogFieldForm onSaved={() => undefined} scope="global" />);
"es",
<CatalogFieldForm csrfToken="test" onSaved={() => undefined} scope="global" />
);
assert.ok(field.indexOf('name="description"') < field.indexOf("<aside")); assert.ok(field.indexOf('name="description"') < field.indexOf("<aside"));
assert.ok(field.indexOf("<aside") < field.indexOf('type="submit"')); assert.ok(field.indexOf("<aside") < field.indexOf('type="submit"'));
assert.ok(field.includes("El tipo de dato queda fijo")); assert.ok(field.includes("El tipo de dato queda fijo"));
const template = await render( const template = await render(
"es", "es",
<CatalogTemplateForm csrfToken="test" items={[]} onSaved={() => undefined} scope="global" /> <CatalogTemplateForm items={[]} onSaved={() => undefined} scope="global" />
); );
assert.ok(template.includes('aria-label="Nuevo campo"')); assert.ok(template.includes('aria-label="Nuevo campo"'));

View File

@ -6,12 +6,7 @@ import { catalogApi, catalogKeys } from "../api";
export type CatalogSaved = (id: string, data: SaveDefinitionResponseDTO) => void; export type CatalogSaved = (id: string, data: SaveDefinitionResponseDTO) => void;
export function useCatalogSave( export function useCatalogSave(scope: string, onSaved: CatalogSaved, id?: string) {
scope: string,
csrfToken: string,
onSaved: CatalogSaved,
id?: string
) {
const client = useQueryClient(); const client = useQueryClient();
const mounted = useRef(true); const mounted = useRef(true);
@ -24,7 +19,7 @@ export function useCatalogSave(
}, []); }, []);
return useMutation({ return useMutation({
mutationFn: (values: AssetDefinitionSpec) => catalogApi.save(scope, values, csrfToken, id), mutationFn: (values: AssetDefinitionSpec) => catalogApi.save(scope, values, id),
onSuccess: async (response) => { onSuccess: async (response) => {
await client.invalidateQueries({ queryKey: ["asset-catalog"] }); await client.invalidateQueries({ queryKey: ["asset-catalog"] });
client.setQueryData(catalogKeys.scope(scope), { items: response.items }); client.setQueryData(catalogKeys.scope(scope), { items: response.items });

View File

@ -27,13 +27,11 @@ export function CatalogEditorDialog({
editor, editor,
items, items,
scope, scope,
csrfToken,
onClose, onClose,
}: { }: {
editor: CatalogEditor; editor: CatalogEditor;
items: readonly CatalogItemDTO[]; items: readonly CatalogItemDTO[];
scope: string; scope: string;
csrfToken: string;
onClose: () => void; onClose: () => void;
}) { }) {
const { t } = useTranslation("assets"); const { t } = useTranslation("assets");
@ -52,23 +50,12 @@ export function CatalogEditorDialog({
wide={editor.kind === "template"} wide={editor.kind === "template"}
> >
{editor.kind === "field" ? ( {editor.kind === "field" ? (
<CatalogFieldForm <CatalogFieldForm item={editor.item} onSaved={() => onClose()} scope={scope} />
csrfToken={csrfToken}
item={editor.item}
onSaved={() => onClose()}
scope={scope}
/>
) : editor.kind === "relation" ? ( ) : editor.kind === "relation" ? (
<CatalogRelationForm <CatalogRelationForm item={editor.item} onSaved={() => onClose()} scope={scope} />
csrfToken={csrfToken}
item={editor.item}
onSaved={() => onClose()}
scope={scope}
/>
) : ( ) : (
<CatalogTemplateForm <CatalogTemplateForm
copy={editor.copy} copy={editor.copy}
csrfToken={csrfToken}
item={editor.item} item={editor.item}
items={items} items={items}
onSaved={() => onClose()} onSaved={() => onClose()}

View File

@ -1,10 +1,11 @@
import { csrfFetch } from "@metis/core/client";
import { z } from "zod/v4"; import { z } from "zod/v4";
export async function deleteRecord(path: string, token: string) { export async function deleteRecord(path: string) {
const response = await fetch(path, { const response = await csrfFetch(path, {
method: "POST", method: "POST",
credentials: "same-origin", credentials: "same-origin",
headers: { "content-type": "application/json", "x-csrf-token": token }, headers: { "content-type": "application/json" },
body: "{}", body: "{}",
}); });

View File

@ -1,15 +1,13 @@
import { csrfFetch } from "@metis/core/client";
import { z } from "zod/v4"; import { z } from "zod/v4";
import { InventoryApiError } from "./inventory-api-error"; import { InventoryApiError } from "./inventory-api-error";
export async function inventoryRequest(path: string, body?: unknown, csrfToken?: string) { export async function inventoryRequest(path: string, body?: unknown) {
const response = await fetch(`/api/assets/${path}`, { const response = await csrfFetch(`/api/assets/${path}`, {
method: body === undefined ? "GET" : "POST", method: body === undefined ? "GET" : "POST",
credentials: "same-origin", credentials: "same-origin",
headers: headers: body === undefined ? {} : { "content-type": "application/json" },
body === undefined
? {}
: { "content-type": "application/json", "x-csrf-token": csrfToken ?? "" },
...(body === undefined ? {} : { body: JSON.stringify(body) }), ...(body === undefined ? {} : { body: JSON.stringify(body) }),
}); });
const data: unknown = await response.json(); const data: unknown = await response.json();

View File

@ -1,15 +1,13 @@
import { csrfFetch } from "@metis/core/client";
import { z } from "zod/v4"; import { z } from "zod/v4";
import { SupportApiError } from "./support-api-error"; import { SupportApiError } from "./support-api-error";
export async function supportRequest(path: string, values?: unknown, token?: string) { export async function supportRequest(path: string, values?: unknown) {
const response = await fetch(`/api/asset-support/${path}`, { const response = await csrfFetch(`/api/asset-support/${path}`, {
method: values === undefined ? "GET" : "POST", method: values === undefined ? "GET" : "POST",
credentials: "same-origin", credentials: "same-origin",
headers: headers: values === undefined ? {} : { "content-type": "application/json" },
values === undefined
? {}
: { "content-type": "application/json", "x-csrf-token": token ?? "" },
...(values === undefined ? {} : { body: JSON.stringify({ values }) }), ...(values === undefined ? {} : { body: JSON.stringify({ values }) }),
}); });
const data: unknown = await response.json(); const data: unknown = await response.json();

View File

@ -6,13 +6,11 @@ import { deleteRecord } from "../api/delete-record.api";
export function DeleteRecordDialog({ export function DeleteRecordDialog({
path, path,
csrfToken,
queryKey, queryKey,
onClose, onClose,
companyId, companyId,
}: { }: {
path: string | undefined; path: string | undefined;
csrfToken: string;
queryKey: readonly string[]; queryKey: readonly string[];
onClose: () => void; onClose: () => void;
companyId: string; companyId: string;
@ -30,7 +28,7 @@ export function DeleteRecordDialog({
if (!path) { if (!path) {
return; return;
} }
await deleteRecord(path, csrfToken); await deleteRecord(path);
await Promise.all([ await Promise.all([
client.invalidateQueries({ queryKey }), client.invalidateQueries({ queryKey }),
client.invalidateQueries({ queryKey: ["person-processes", companyId] }), client.invalidateQueries({ queryKey: ["person-processes", companyId] }),

View File

@ -2,6 +2,5 @@ export type AssetSupportProps = {
compact?: boolean; compact?: boolean;
companyId: string; companyId: string;
assetId: string; assetId: string;
csrfToken: string;
retired?: boolean; retired?: boolean;
}; };

View File

@ -11,7 +11,7 @@ import { useAssetSupportController } from "../controllers";
import type { AssetSupportProps } from "../types"; import type { AssetSupportProps } from "../types";
export function AssetSupport(props: AssetSupportProps) { export function AssetSupport(props: AssetSupportProps) {
const { companyId, assetId, csrfToken, retired = false, compact = false } = props; const { companyId, assetId, retired = false, compact = false } = props;
const { const {
query, query,
open, open,
@ -110,7 +110,6 @@ export function AssetSupport(props: AssetSupportProps) {
)} )}
<DeleteRecordDialog <DeleteRecordDialog
companyId={companyId} companyId={companyId}
csrfToken={csrfToken}
onClose={() => setDeleteId(undefined)} onClose={() => setDeleteId(undefined)}
path={ path={
deleteId ? `/api/asset-support/${companyId}/${assetId}/${deleteId}/delete` : undefined deleteId ? `/api/asset-support/${companyId}/${assetId}/${deleteId}/delete` : undefined
@ -120,14 +119,13 @@ export function AssetSupport(props: AssetSupportProps) {
<SupportConnectionEditorDialog <SupportConnectionEditorDialog
assetId={assetId} assetId={assetId}
companyId={companyId} companyId={companyId}
csrfToken={csrfToken}
edit={edit} edit={edit}
onOpenChange={setOpen} onOpenChange={setOpen}
onSaved={() => setOpen(false)} onSaved={() => setOpen(false)}
open={open} open={open}
tools={tools} tools={tools}
/> />
<SupportToolsDialog csrfToken={csrfToken} onOpenChange={setToolsOpen} open={toolsOpen} /> <SupportToolsDialog onOpenChange={setToolsOpen} open={toolsOpen} />
<SupportHistoryDialog <SupportHistoryDialog
history={history} history={history}
onOpenChange={(value) => { onOpenChange={(value) => {

View File

@ -6,10 +6,9 @@ export async function saveSupportConnection(
companyId: string, companyId: string,
assetId: string, assetId: string,
values: SupportInput, values: SupportInput,
token: string,
id?: string id?: string
) { ) {
return SupportResponseSchema.parse( return SupportResponseSchema.parse(
await supportRequest(`${companyId}/${assetId}${id ? `/${id}/update` : ""}`, values, token) await supportRequest(`${companyId}/${assetId}${id ? `/${id}/update` : ""}`, values)
); );
} }

View File

@ -12,7 +12,6 @@ import type { SupportFormProps } from "../types";
export function useSupportFormController({ export function useSupportFormController({
companyId, companyId,
assetId, assetId,
csrfToken,
connection, connection,
onSaved, onSaved,
}: SupportFormProps) { }: SupportFormProps) {
@ -32,7 +31,7 @@ export function useSupportFormController({
const values = form.watch(); const values = form.watch();
const mutation = useMutation({ const mutation = useMutation({
mutationFn: (input: SupportInput) => mutationFn: (input: SupportInput) =>
saveSupportConnection(companyId, assetId, input, csrfToken, connection?.id), saveSupportConnection(companyId, assetId, input, connection?.id),
onSuccess: async () => { onSuccess: async () => {
await client.invalidateQueries({ queryKey: supportKeys.asset(companyId, assetId) }); await client.invalidateQueries({ queryKey: supportKeys.asset(companyId, assetId) });
onSaved(); onSaved();

View File

@ -46,7 +46,6 @@ test("asistencia: formulario con alta rápida y ayuda antes de guardar", async (
<SupportForm <SupportForm
assetId={UniqueID.generateNewID().toPrimitive()} assetId={UniqueID.generateNewID().toPrimitive()}
companyId={UniqueID.generateNewID().toPrimitive()} companyId={UniqueID.generateNewID().toPrimitive()}
csrfToken="test"
onSaved={() => undefined} onSaved={() => undefined}
tools={[tool]} tools={[tool]}
/> />
@ -81,9 +80,7 @@ test("asistencia: traducciones y resumen de método", async () => {
} }
}); });
test("herramienta: nombre de serie protegido y ayuda inferior", async () => { test("herramienta: nombre de serie protegido y ayuda inferior", async () => {
const html = await render( const html = await render(<SupportToolForm onSaved={() => undefined} tool={tool} />);
<SupportToolForm csrfToken="test" onSaved={() => undefined} tool={tool} />
);
assert.ok(html.includes("readOnly")); assert.ok(html.includes("readOnly"));
assert.ok(html.indexOf("¿Para qué sirve una herramienta?") < html.indexOf("Guardar")); assert.ok(html.indexOf("¿Para qué sirve una herramienta?") < html.indexOf("Guardar"));

View File

@ -3,7 +3,6 @@ import type { SupportConnectionDTO, SupportToolDTO } from "../../../../common/su
export type SupportFormProps = { export type SupportFormProps = {
companyId: string; companyId: string;
assetId: string; assetId: string;
csrfToken: string;
tools: readonly SupportToolDTO[]; tools: readonly SupportToolDTO[];
connection?: SupportConnectionDTO; connection?: SupportConnectionDTO;
onSaved: () => void; onSaved: () => void;

View File

@ -9,7 +9,6 @@ export function SupportConnectionEditorDialog({
edit, edit,
companyId, companyId,
assetId, assetId,
csrfToken,
tools, tools,
onSaved, onSaved,
onOpenChange, onOpenChange,
@ -18,7 +17,6 @@ export function SupportConnectionEditorDialog({
edit: ComponentProps<typeof SupportForm>["connection"]; edit: ComponentProps<typeof SupportForm>["connection"];
companyId: string; companyId: string;
assetId: string; assetId: string;
csrfToken: string;
tools: ComponentProps<typeof SupportForm>["tools"]; tools: ComponentProps<typeof SupportForm>["tools"];
onSaved: () => void; onSaved: () => void;
onOpenChange: (open: boolean) => void; onOpenChange: (open: boolean) => void;
@ -36,7 +34,6 @@ export function SupportConnectionEditorDialog({
<SupportForm <SupportForm
assetId={assetId} assetId={assetId}
companyId={companyId} companyId={companyId}
csrfToken={csrfToken}
key={edit?.id ?? "new"} key={edit?.id ?? "new"}
tools={tools} tools={tools}
{...(edit ? { connection: edit } : {})} {...(edit ? { connection: edit } : {})}

View File

@ -15,7 +15,7 @@ import type { SupportFormProps } from "../types";
import { SupportHelp } from "./support-help"; import { SupportHelp } from "./support-help";
export function SupportForm(props: SupportFormProps) { export function SupportForm(props: SupportFormProps) {
const { csrfToken, tools, connection } = props; const { tools, connection } = props;
const { quick, setQuick, form, values, mutation, submit } = useSupportFormController(props); const { quick, setQuick, form, values, mutation, submit } = useSupportFormController(props);
const { t } = useTranslation("assets"); const { t } = useTranslation("assets");
const prefix = useId(); const prefix = useId();
@ -104,7 +104,6 @@ export function SupportForm(props: SupportFormProps) {
</FormActions> </FormActions>
</form> </form>
<SupportToolEditorDialog <SupportToolEditorDialog
csrfToken={csrfToken}
edit={undefined} edit={undefined}
onOpenChange={setQuick} onOpenChange={setQuick}
onSaved={(id) => { onSaved={(id) => {

View File

@ -1 +0,0 @@
export type { SupportToolsProps } from "./support-tools-props";

View File

@ -1,3 +0,0 @@
export type SupportToolsProps = {
csrfToken: string;
};

View File

@ -5,11 +5,9 @@ import { SupportTools } from "./support-tools";
export function SupportToolsDialog({ export function SupportToolsDialog({
open, open,
csrfToken,
onOpenChange, onOpenChange,
}: { }: {
open: boolean; open: boolean;
csrfToken: string;
onOpenChange: (open: boolean) => void; onOpenChange: (open: boolean) => void;
}) { }) {
const { t } = useTranslation("assets"); const { t } = useTranslation("assets");
@ -21,7 +19,7 @@ export function SupportToolsDialog({
open={open} open={open}
title={t("support.tools")} title={t("support.tools")}
> >
<SupportTools csrfToken={csrfToken} /> <SupportTools />
</FormDialog> </FormDialog>
); );
} }

View File

@ -10,10 +10,8 @@ import { useTranslation } from "react-i18next";
import { SupportToolEditorDialog } from "../../shared"; import { SupportToolEditorDialog } from "../../shared";
import { useSupportToolsController } from "../controllers"; import { useSupportToolsController } from "../controllers";
import type { SupportToolsProps } from "../types";
export function SupportTools(props: SupportToolsProps) { export function SupportTools() {
const { csrfToken } = props;
const { query, edit, setEdit, open, setOpen } = useSupportToolsController(); const { query, edit, setEdit, open, setOpen } = useSupportToolsController();
const { t, i18n } = useTranslation("assets"); const { t, i18n } = useTranslation("assets");
@ -100,7 +98,6 @@ export function SupportTools(props: SupportToolsProps) {
unavailable={query.isError} unavailable={query.isError}
/> />
<SupportToolEditorDialog <SupportToolEditorDialog
csrfToken={csrfToken}
edit={edit} edit={edit}
onOpenChange={setOpen} onOpenChange={setOpen}
onSaved={() => setOpen(false)} onSaved={() => setOpen(false)}

View File

@ -2,8 +2,8 @@ import type { SupportToolInput } from "../../../../common/support";
import { SupportToolResponseSchema } from "../../../../common/support"; import { SupportToolResponseSchema } from "../../../../common/support";
import { supportRequest } from "../../../shared/api/support-request"; import { supportRequest } from "../../../shared/api/support-request";
export async function saveSupportTool(values: SupportToolInput, token: string, id?: string) { export async function saveSupportTool(values: SupportToolInput, id?: string) {
return SupportToolResponseSchema.parse( return SupportToolResponseSchema.parse(
await supportRequest(`tools${id ? `/${id}/update` : ""}`, values, token) await supportRequest(`tools${id ? `/${id}/update` : ""}`, values)
); );
} }

View File

@ -8,14 +8,14 @@ import { supportKeys } from "../../../shared/queries";
import { saveSupportTool } from "../api"; import { saveSupportTool } from "../api";
import type { SupportToolFormProps } from "../types"; import type { SupportToolFormProps } from "../types";
export function useSupportToolFormController({ csrfToken, tool, onSaved }: SupportToolFormProps) { export function useSupportToolFormController({ tool, onSaved }: SupportToolFormProps) {
const client = useQueryClient(); const client = useQueryClient();
const form = useForm<SupportToolInput>({ const form = useForm<SupportToolInput>({
resolver: zodResolver(SupportToolInputSchema), resolver: zodResolver(SupportToolInputSchema),
defaultValues: tool ? { name: tool.name, active: tool.active } : { name: "", active: true }, defaultValues: tool ? { name: tool.name, active: tool.active } : { name: "", active: true },
}); });
const mutation = useMutation({ const mutation = useMutation({
mutationFn: (values: SupportToolInput) => saveSupportTool(values, csrfToken, tool?.id), mutationFn: (values: SupportToolInput) => saveSupportTool(values, tool?.id),
onSuccess: async (response) => { onSuccess: async (response) => {
await Promise.all([ await Promise.all([
client.invalidateQueries({ queryKey: supportKeys.tools }), client.invalidateQueries({ queryKey: supportKeys.tools }),

View File

@ -1,7 +1,6 @@
import type { SupportToolDTO } from "../../../../common/support"; import type { SupportToolDTO } from "../../../../common/support";
export type SupportToolFormProps = { export type SupportToolFormProps = {
csrfToken: string;
tool?: SupportToolDTO; tool?: SupportToolDTO;
onSaved: (id: string) => void; onSaved: (id: string) => void;
}; };

View File

@ -7,13 +7,11 @@ import { SupportToolForm } from "./support-tool-form";
export function SupportToolEditorDialog({ export function SupportToolEditorDialog({
open, open,
edit, edit,
csrfToken,
onSaved, onSaved,
onOpenChange, onOpenChange,
}: { }: {
open: boolean; open: boolean;
edit: ComponentProps<typeof SupportToolForm>["tool"]; edit: ComponentProps<typeof SupportToolForm>["tool"];
csrfToken: string;
onSaved: ComponentProps<typeof SupportToolForm>["onSaved"]; onSaved: ComponentProps<typeof SupportToolForm>["onSaved"];
onOpenChange: (open: boolean) => void; onOpenChange: (open: boolean) => void;
}) { }) {
@ -28,7 +26,6 @@ export function SupportToolEditorDialog({
title={t(edit ? "support.editTool" : "support.newTool")} title={t(edit ? "support.editTool" : "support.newTool")}
> >
<SupportToolForm <SupportToolForm
csrfToken={csrfToken}
key={edit?.id ?? "new"} key={edit?.id ?? "new"}
{...(edit ? { tool: edit } : {})} {...(edit ? { tool: edit } : {})}
onSaved={onSaved} onSaved={onSaved}

View File

@ -2,6 +2,5 @@ import type { InventoryLookups } from "../../../assets/shared/types";
export type WorkstationsPageProps = { export type WorkstationsPageProps = {
companyId: string; companyId: string;
csrfToken: string;
lookups: InventoryLookups; lookups: InventoryLookups;
}; };

View File

@ -15,7 +15,7 @@ import { useWorkstationsPageController } from "../controllers";
import type { WorkstationsPageProps } from "../types"; import type { WorkstationsPageProps } from "../types";
export function WorkstationsPage(props: WorkstationsPageProps) { export function WorkstationsPage(props: WorkstationsPageProps) {
const { companyId, csrfToken, lookups } = props; const { companyId, lookups } = props;
const { const {
search, search,
setSearch, setSearch,
@ -152,7 +152,6 @@ export function WorkstationsPage(props: WorkstationsPageProps) {
{editor && ( {editor && (
<WorkstationEditorDialog <WorkstationEditorDialog
companyId={companyId} companyId={companyId}
csrfToken={csrfToken}
lookups={lookups} lookups={lookups}
onClose={() => setEditor(undefined)} onClose={() => setEditor(undefined)}
workstation={edited} workstation={edited}

View File

@ -2,17 +2,8 @@ import type { WorkstationInput } from "../../../../common/workstations";
import { WorkstationResponseSchema } from "../../../../common/workstations"; import { WorkstationResponseSchema } from "../../../../common/workstations";
import { inventoryRequest } from "../../../shared/api/inventory-request"; import { inventoryRequest } from "../../../shared/api/inventory-request";
export async function saveWorkstation( export async function saveWorkstation(companyId: string, values: WorkstationInput, id?: string) {
companyId: string,
values: WorkstationInput,
token: string,
id?: string
) {
return WorkstationResponseSchema.parse( return WorkstationResponseSchema.parse(
await inventoryRequest( await inventoryRequest(`${companyId}/workstations${id ? `/${id}/update` : ""}`, { values })
`${companyId}/workstations${id ? `/${id}/update` : ""}`,
{ values },
token
)
); );
} }

View File

@ -13,7 +13,6 @@ import type { WorkstationFormProps } from "../types";
export function useWorkstationFormController({ export function useWorkstationFormController({
companyId, companyId,
csrfToken,
workstation, workstation,
onSaved, onSaved,
}: WorkstationFormProps) { }: WorkstationFormProps) {
@ -76,8 +75,7 @@ export function useWorkstationFormController({
}; };
const mutation = useMutation({ const mutation = useMutation({
mutationFn: (input: WorkstationInput) => mutationFn: (input: WorkstationInput) => saveWorkstation(companyId, input, workstation?.id),
saveWorkstation(companyId, input, csrfToken, workstation?.id),
onSuccess: async (response) => { onSuccess: async (response) => {
await client.invalidateQueries({ queryKey: inventoryKeys.company(companyId) }); await client.invalidateQueries({ queryKey: inventoryKeys.company(companyId) });
if (mounted.current) { if (mounted.current) {

View File

@ -3,7 +3,6 @@ import type { InventoryLookups } from "../../../assets/shared/types";
export type WorkstationFormProps = { export type WorkstationFormProps = {
companyId: string; companyId: string;
csrfToken: string;
lookups: InventoryLookups; lookups: InventoryLookups;
workstation?: WorkstationDTO | undefined; workstation?: WorkstationDTO | undefined;
onSaved: (id: string) => void; onSaved: (id: string) => void;

View File

@ -7,13 +7,11 @@ import { WorkstationForm } from "./workstation-form";
export function WorkstationEditorDialog({ export function WorkstationEditorDialog({
workstation, workstation,
companyId, companyId,
csrfToken,
lookups, lookups,
onClose, onClose,
}: { }: {
workstation: ComponentProps<typeof WorkstationForm>["workstation"]; workstation: ComponentProps<typeof WorkstationForm>["workstation"];
companyId: string; companyId: string;
csrfToken: string;
lookups: ComponentProps<typeof WorkstationForm>["lookups"]; lookups: ComponentProps<typeof WorkstationForm>["lookups"];
onClose: () => void; onClose: () => void;
}) { }) {
@ -34,7 +32,6 @@ export function WorkstationEditorDialog({
> >
<WorkstationForm <WorkstationForm
companyId={companyId} companyId={companyId}
csrfToken={csrfToken}
lookups={lookups} lookups={lookups}
onSaved={() => onClose()} onSaved={() => onClose()}
workstation={workstation} workstation={workstation}

View File

@ -14,7 +14,7 @@ import { useWorkstationFormController } from "../controllers";
import type { WorkstationFormProps } from "../types"; import type { WorkstationFormProps } from "../types";
export function WorkstationForm(props: WorkstationFormProps) { export function WorkstationForm(props: WorkstationFormProps) {
const { companyId, csrfToken, lookups, workstation } = props; const { companyId, lookups, workstation } = props;
const { const {
form, form,
values, values,
@ -193,7 +193,6 @@ export function WorkstationForm(props: WorkstationFormProps) {
{quick && catalog.data && ( {quick && catalog.data && (
<WorkstationAssetCreateDialog <WorkstationAssetCreateDialog
companyId={companyId} companyId={companyId}
csrfToken={csrfToken}
items={catalog.data.items} items={catalog.data.items}
lookups={lookups} lookups={lookups}
onOpenChange={setQuick} onOpenChange={setQuick}

View File

@ -11,7 +11,6 @@ import type { QuickCreateDialogProps } from "../types";
export function useQuickCreateDialogController({ export function useQuickCreateDialogController({
target, target,
scope, scope,
csrfToken,
onCreated, onCreated,
items, items,
onClose, onClose,
@ -42,7 +41,7 @@ export function useQuickCreateDialogController({
? { kind: "system", name, active: true } ? { kind: "system", name, active: true }
: { kind: "environment", name, active: true, systemId: target.systemId } : { kind: "environment", name, active: true, systemId: target.systemId }
); );
const result = await saveAccess(scope, values, undefined, csrfToken); const result = await saveAccess(scope, values, undefined);
const created = result.items.find( const created = result.items.find(
(item) => (item) =>
item.companyId === (scope === "global" ? null : scope) && matches(item.values, values) item.companyId === (scope === "global" ? null : scope) && matches(item.values, values)

View File

@ -9,7 +9,6 @@ export type AccessQuickEditorProps = {
scope: string; scope: string;
data: AccessResponseDTO; data: AccessResponseDTO;
people: readonly PersonDTO[]; people: readonly PersonDTO[];
csrfToken: string;
onCreated: (id?: string, values?: AccessFieldsDTO) => void; onCreated: (id?: string, values?: AccessFieldsDTO) => void;
onClose: () => void; onClose: () => void;
nestedAccessForm: ComponentType<AccessFormProps>; nestedAccessForm: ComponentType<AccessFormProps>;

View File

@ -13,7 +13,6 @@ export type QuickCreateTarget =
export type QuickCreateDialogProps = { export type QuickCreateDialogProps = {
target: QuickCreateTarget; target: QuickCreateTarget;
scope: string; scope: string;
csrfToken: string;
onClose: () => void; onClose: () => void;
onCreated: (id: string) => void; onCreated: (id: string) => void;
items: readonly AccessItemDTO[]; items: readonly AccessItemDTO[];

View File

@ -9,14 +9,13 @@ import type { AccessQuickEditorProps } from "../types";
import { QuickCreateDialog } from "./quick-create-dialog"; import { QuickCreateDialog } from "./quick-create-dialog";
export function AccessQuickEditor(props: AccessQuickEditorProps) { export function AccessQuickEditor(props: AccessQuickEditorProps) {
const { plan, scope, data, people, csrfToken, nestedAccessForm: NestedAccessForm } = props; const { plan, scope, data, people, nestedAccessForm: NestedAccessForm } = props;
const { close, created, globalQuery } = useAccessQuickEditorController(props); const { close, created, globalQuery } = useAccessQuickEditorController(props);
const { t } = useTranslation("access"); const { t } = useTranslation("access");
if (plan.type === "simple") { if (plan.type === "simple") {
return ( return (
<QuickCreateDialog <QuickCreateDialog
csrfToken={csrfToken}
items={data.items} items={data.items}
onClose={close} onClose={close}
onCreated={created} onCreated={created}
@ -44,7 +43,7 @@ export function AccessQuickEditor(props: AccessQuickEditorProps) {
{plan.type === "person" ? ( {plan.type === "person" ? (
<> <>
<p className="text-sm text-muted-foreground">{t("quickCreateNotice")}</p> <p className="text-sm text-muted-foreground">{t("quickCreateNotice")}</p>
<PersonForm companyId={scope} csrfToken={csrfToken} onSaved={created} requireActive /> <PersonForm companyId={scope} onSaved={created} requireActive />
</> </>
) : ( ) : (
<> <>
@ -54,7 +53,6 @@ export function AccessQuickEditor(props: AccessQuickEditorProps) {
{childData ? ( {childData ? (
<NestedAccessForm <NestedAccessForm
allowQuickCreate={false} allowQuickCreate={false}
csrfToken={csrfToken}
data={childData} data={childData}
initialValues={plan.defaults} initialValues={plan.defaults}
kind={plan.kind} kind={plan.kind}

View File

@ -3,5 +3,4 @@ import type { PersonDTO } from "../../../../common";
export type AccessWorkspacePageProps = { export type AccessWorkspacePageProps = {
scope: string; scope: string;
people?: readonly PersonDTO[]; people?: readonly PersonDTO[];
csrfToken: string;
}; };

View File

@ -17,7 +17,7 @@ function errorCode(error: Error) {
} }
export function AccessWorkspacePage(props: AccessWorkspacePageProps) { export function AccessWorkspacePage(props: AccessWorkspacePageProps) {
const { scope, people = [], csrfToken } = props; const { scope, people = [] } = props;
const { const {
query, query,
kind, kind,
@ -169,7 +169,6 @@ export function AccessWorkspacePage(props: AccessWorkspacePageProps) {
/> />
<AccessEditorDialog <AccessEditorDialog
complex={complex} complex={complex}
csrfToken={csrfToken}
data={data} data={data}
editor={editor} editor={editor}
kind={kind} kind={kind}

View File

@ -32,7 +32,7 @@ test("los catálogos presentan alta mediante acción y no un formulario junto al
const markup = renderToStaticMarkup( const markup = renderToStaticMarkup(
<I18nextProvider i18n={i18n}> <I18nextProvider i18n={i18n}>
<QueryClientProvider client={client}> <QueryClientProvider client={client}>
<AccessWorkspacePage csrfToken="test" scope="global" /> <AccessWorkspacePage scope="global" />
</QueryClientProvider> </QueryClientProvider>
</I18nextProvider> </I18nextProvider>
); );
@ -51,7 +51,6 @@ test("cuentas ofrecen caducidad opcional y campos sin contraseña", async () =>
<I18nextProvider i18n={i18n}> <I18nextProvider i18n={i18n}>
<QueryClientProvider client={client}> <QueryClientProvider client={client}>
<AccessForm <AccessForm
csrfToken="test"
data={{ items: [], reviews: [], coverage: [] }} data={{ items: [], reviews: [], coverage: [] }}
kind="account" kind="account"
onSaved={() => undefined} onSaved={() => undefined}
@ -141,7 +140,6 @@ test("los ejemplos del sistema orientan sin rellenar datos del alta", async () =
<I18nextProvider i18n={i18n}> <I18nextProvider i18n={i18n}>
<QueryClientProvider client={client}> <QueryClientProvider client={client}>
<AccessForm <AccessForm
csrfToken="test"
data={{ items: [], reviews: [], coverage: [] }} data={{ items: [], reviews: [], coverage: [] }}
kind="system" kind="system"
onSaved={() => undefined} onSaved={() => undefined}
@ -273,7 +271,6 @@ test("los formularios ofrecen altas relacionadas y el segundo modal evita altas
<QueryClientProvider client={client}> <QueryClientProvider client={client}>
<AccessForm <AccessForm
allowQuickCreate={allowQuickCreate} allowQuickCreate={allowQuickCreate}
csrfToken="test"
data={{ items: [], reviews: [], coverage: [] }} data={{ items: [], reviews: [], coverage: [] }}
kind={kind} kind={kind}
onSaved={() => undefined} onSaved={() => undefined}

View File

@ -1,3 +1,4 @@
import { csrfFetch } from "@metis/core/client";
import { z } from "zod/v4"; import { z } from "zod/v4";
import type { AccessFieldsDTO } from "../../../../common"; import type { AccessFieldsDTO } from "../../../../common";
@ -5,16 +6,11 @@ import { AccessResponseSchema } from "../../../../common";
import { AccessApiError } from "./access-api-error"; import { AccessApiError } from "./access-api-error";
export async function accessRequest( export async function accessRequest(scope: string, values?: AccessFieldsDTO, id?: string) {
scope: string, const response = await csrfFetch(`/api/access/${scope}${id ? `/${id}/update` : ""}`, {
values?: AccessFieldsDTO,
id?: string,
token?: string
) {
const response = await fetch(`/api/access/${scope}${id ? `/${id}/update` : ""}`, {
method: values ? "POST" : "GET", method: values ? "POST" : "GET",
credentials: "same-origin", credentials: "same-origin",
headers: values ? { "content-type": "application/json", "x-csrf-token": token ?? "" } : {}, headers: values ? { "content-type": "application/json" } : {},
...(values ? { body: JSON.stringify({ values }) } : {}), ...(values ? { body: JSON.stringify({ values }) } : {}),
}); });
const json: unknown = await response.json(); const json: unknown = await response.json();

View File

@ -2,11 +2,6 @@ import type { AccessFieldsDTO } from "../../../../common";
import { accessRequest } from "./access-request"; import { accessRequest } from "./access-request";
export function saveAccess( export function saveAccess(scope: string, values: AccessFieldsDTO, id: string | undefined) {
scope: string, return accessRequest(scope, values, id);
values: AccessFieldsDTO,
id: string | undefined,
token: string
) {
return accessRequest(scope, values, id, token);
} }

View File

@ -20,7 +20,6 @@ export function useAccessFormFieldsController({
item, item,
data, data,
people, people,
csrfToken,
onSaved, onSaved,
allowQuickCreate = true, allowQuickCreate = true,
initialValues = {}, initialValues = {},
@ -56,7 +55,7 @@ export function useAccessFormFieldsController({
throw new AccessApiError("INVALID_REQUEST"); throw new AccessApiError("INVALID_REQUEST");
} }
return saveAccess(scope, parsed.data, item?.id, csrfToken); return saveAccess(scope, parsed.data, item?.id);
}, },
onSuccess: async (result, submitted) => { onSuccess: async (result, submitted) => {
const savedId = const savedId =

View File

@ -13,7 +13,6 @@ export type AccessFormProps = {
item?: AccessItemDTO | undefined; item?: AccessItemDTO | undefined;
data: AccessResponseDTO; data: AccessResponseDTO;
people: readonly PersonDTO[]; people: readonly PersonDTO[];
csrfToken: string;
onSaved: (id?: string, values?: AccessFieldsDTO) => void; onSaved: (id?: string, values?: AccessFieldsDTO) => void;
allowQuickCreate?: boolean; allowQuickCreate?: boolean;
initialValues?: AccessFormValues; initialValues?: AccessFormValues;

View File

@ -12,7 +12,6 @@ export function AccessEditorDialog({
selected, selected,
kind, kind,
scope, scope,
csrfToken,
data, data,
people, people,
complex, complex,
@ -23,7 +22,6 @@ export function AccessEditorDialog({
selected: ComponentProps<typeof AccessForm>["item"]; selected: ComponentProps<typeof AccessForm>["item"];
kind: AccessKind; kind: AccessKind;
scope: string; scope: string;
csrfToken: string;
data: ComponentProps<typeof AccessForm>["data"]; data: ComponentProps<typeof AccessForm>["data"];
people: ComponentProps<typeof AccessForm>["people"]; people: ComponentProps<typeof AccessForm>["people"];
complex: boolean; complex: boolean;
@ -42,7 +40,6 @@ export function AccessEditorDialog({
wide={complex} wide={complex}
> >
<AccessForm <AccessForm
csrfToken={csrfToken}
data={data} data={data}
item={selected} item={selected}
key={editor} key={editor}

View File

@ -17,7 +17,7 @@ export function AccessFormFields(
nestedAccessForm: ComponentType<AccessFormProps>; nestedAccessForm: ComponentType<AccessFormProps>;
} }
) { ) {
const { scope, kind, item, data, people, csrfToken, lockedFields = [], nestedAccessForm } = props; const { scope, kind, item, data, people, lockedFields = [], nestedAccessForm } = props;
const { const {
validation, validation,
quickCreate, quickCreate,
@ -94,7 +94,6 @@ export function AccessFormFields(
</form> </form>
{quickCreate && ( {quickCreate && (
<AccessQuickEditor <AccessQuickEditor
csrfToken={csrfToken}
data={data} data={data}
nestedAccessForm={nestedAccessForm} nestedAccessForm={nestedAccessForm}
onClose={() => setQuickCreate(undefined)} onClose={() => setQuickCreate(undefined)}

View File

@ -12,7 +12,6 @@ export function AccessForm(props: AccessFormProps) {
<ResourceEditor <ResourceEditor
allowQuickCreate={props.allowQuickCreate ?? true} allowQuickCreate={props.allowQuickCreate ?? true}
companyId={props.scope} companyId={props.scope}
csrfToken={props.csrfToken}
initialAccess={ initialAccess={
typeof type === "string" && type typeof type === "string" && type
? { resourceTypeId: type, systemId: null, environmentId: null } ? { resourceTypeId: type, systemId: null, environmentId: null }

View File

@ -8,13 +8,11 @@ import { WorkProfileForm } from "../../../work-profiles/shared";
export function ApplicationQuickEditor({ export function ApplicationQuickEditor({
kind, kind,
company, company,
csrfToken,
onClose, onClose,
onSaved, onSaved,
}: { }: {
kind: "profile" | "application"; kind: "profile" | "application";
company: CompanyDTO; company: CompanyDTO;
csrfToken: string;
onClose: () => void; onClose: () => void;
onSaved: (field: "profileId" | "resourceId", id: string) => void; onSaved: (field: "profileId" | "resourceId", id: string) => void;
}) { }) {
@ -37,14 +35,12 @@ export function ApplicationQuickEditor({
<WorkProfileForm <WorkProfileForm
allowQuickCreate={false} allowQuickCreate={false}
company={company} company={company}
csrfToken={csrfToken}
onSaved={(id) => onSaved("profileId", id)} onSaved={(id) => onSaved("profileId", id)}
/> />
) : ( ) : (
<ResourceEditor <ResourceEditor
allowQuickCreate={false} allowQuickCreate={false}
companyId={company.id} companyId={company.id}
csrfToken={csrfToken}
initialKind="application" initialKind="application"
onSaved={(id) => onSaved("resourceId", id)} onSaved={(id) => onSaved("resourceId", id)}
/> />

View File

@ -2,6 +2,5 @@ import type { CompanyDTO } from "../../../../common";
export type ApplicationConfigurationsPageProps = { export type ApplicationConfigurationsPageProps = {
company: CompanyDTO; company: CompanyDTO;
csrfToken: string;
initialProfileId?: string; initialProfileId?: string;
}; };

View File

@ -16,7 +16,7 @@ import { useApplicationConfigurationsPageController } from "../controllers";
import type { ApplicationConfigurationsPageProps } from "../types"; import type { ApplicationConfigurationsPageProps } from "../types";
export function ApplicationConfigurationsPage(props: ApplicationConfigurationsPageProps) { export function ApplicationConfigurationsPage(props: ApplicationConfigurationsPageProps) {
const { company, csrfToken, initialProfileId } = props; const { company, initialProfileId } = props;
const { const {
editor, editor,
setEditor, setEditor,
@ -42,7 +42,6 @@ export function ApplicationConfigurationsPage(props: ApplicationConfigurationsPa
<div className="space-y-5"> <div className="space-y-5">
<DeleteRecordDialog <DeleteRecordDialog
companyId={company.id} companyId={company.id}
csrfToken={csrfToken}
onClose={() => setDeleteId(undefined)} onClose={() => setDeleteId(undefined)}
path={ path={
deleteId ? `/api/application-configurations/${company.id}/${deleteId}/delete` : undefined deleteId ? `/api/application-configurations/${company.id}/${deleteId}/delete` : undefined
@ -170,7 +169,6 @@ export function ApplicationConfigurationsPage(props: ApplicationConfigurationsPa
<ApplicationConfigurationEditorDialog <ApplicationConfigurationEditorDialog
company={company} company={company}
configuration={edited} configuration={edited}
csrfToken={csrfToken}
initialProfileId={initialProfileId ?? ""} initialProfileId={initialProfileId ?? ""}
onClose={() => setEditor(undefined)} onClose={() => setEditor(undefined)}
/> />

View File

@ -1,3 +1,4 @@
import { csrfFetch } from "@metis/core/client";
import { z } from "zod/v4"; import { z } from "zod/v4";
import type { ApplicationConfigurationInput } from "../../../../common/application-configurations"; import type { ApplicationConfigurationInput } from "../../../../common/application-configurations";
@ -14,18 +15,13 @@ export class ApplicationApiError extends Error {
} }
} }
async function request( async function request(companyId: string, values?: ApplicationConfigurationInput, id?: string) {
companyId: string, const response = await csrfFetch(
values?: ApplicationConfigurationInput,
token?: string,
id?: string
) {
const response = await fetch(
`/api/application-configurations/${companyId}${id ? `/${id}/update` : ""}`, `/api/application-configurations/${companyId}${id ? `/${id}/update` : ""}`,
{ {
method: values ? "POST" : "GET", method: values ? "POST" : "GET",
credentials: "same-origin", credentials: "same-origin",
headers: values ? { "content-type": "application/json", "x-csrf-token": token ?? "" } : {}, headers: values ? { "content-type": "application/json" } : {},
...(values ? { body: JSON.stringify({ values }) } : {}), ...(values ? { body: JSON.stringify({ values }) } : {}),
} }
); );
@ -44,9 +40,7 @@ export const applicationApi = {
async get(companyId: string) { async get(companyId: string) {
return ApplicationConfigurationsResponseSchema.parse(await request(companyId)); return ApplicationConfigurationsResponseSchema.parse(await request(companyId));
}, },
async save(companyId: string, values: ApplicationConfigurationInput, token: string, id?: string) { async save(companyId: string, values: ApplicationConfigurationInput, id?: string) {
return ApplicationConfigurationResponseSchema.parse( return ApplicationConfigurationResponseSchema.parse(await request(companyId, values, id));
await request(companyId, values, token, id)
);
}, },
}; };

View File

@ -65,7 +65,7 @@ test("aplicaciones: listado sin formularios, alta en cabecera y traducciones", a
]) { ]) {
const html = await render( const html = await render(
language ?? "es", language ?? "es",
<ApplicationConfigurationsPage company={company} csrfToken="test" /> <ApplicationConfigurationsPage company={company} />
); );
assert.ok(html.includes(title ?? "")); assert.ok(html.includes(title ?? ""));
@ -78,7 +78,7 @@ test("aplicaciones: selectores con alta rápida y ayuda inferior antes de guarda
for (const language of ["es", "en"]) { for (const language of ["es", "en"]) {
const html = await render( const html = await render(
language, language,
<ApplicationConfigurationForm company={company} csrfToken="test" onSaved={() => undefined} /> <ApplicationConfigurationForm company={company} onSaved={() => undefined} />
); );
const text = language === "es" ? companiesSpanish : companiesEnglish; const text = language === "es" ? companiesSpanish : companiesEnglish;

View File

@ -27,7 +27,6 @@ function applicationStates(
export function useApplicationConfigurationFormController({ export function useApplicationConfigurationFormController({
company, company,
csrfToken,
configuration, configuration,
initialProfileId = "", initialProfileId = "",
onSaved, onSaved,
@ -65,7 +64,7 @@ export function useApplicationConfigurationFormController({
const states = applicationStates(configuration); const states = applicationStates(configuration);
const mutation = useMutation({ const mutation = useMutation({
mutationFn: (input: ApplicationConfigurationInput) => mutationFn: (input: ApplicationConfigurationInput) =>
applicationApi.save(company.id, input, csrfToken, configuration?.id), applicationApi.save(company.id, input, configuration?.id),
onSuccess: async () => { onSuccess: async () => {
await client.invalidateQueries({ queryKey: applicationKeys.company(company.id) }); await client.invalidateQueries({ queryKey: applicationKeys.company(company.id) });
if (mounted.current) { if (mounted.current) {

Some files were not shown because too many files have changed in this diff Show More