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"
]
},
{
"blankLine": "any",
"prev": {
"selector": "ExportNamedDeclaration[source], ExportAllDeclaration"
},
"next": {
"selector": "ExportNamedDeclaration[source], ExportAllDeclaration"
}
}
],
"@stylistic/lines-between-class-members": [

View File

@ -26,7 +26,6 @@ export function App() {
renderWorkspace={(snapshot, accountNavigation) => (
<CompaniesWorkspace
accountNavigation={accountNavigation}
csrfToken={snapshot.csrfToken}
languageSwitcher={<LanguageSwitcher />}
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 { StrictMode } from "react";
import { createRoot } from "react-dom/client";
@ -16,6 +18,10 @@ if (!rootElement) {
const { instance, initialization } = createAppI18n();
const queryClient = new QueryClient();
configureCsrfTokenReader(
() => queryClient.getQueryData<SessionResponseDTO>(identityKeys.session)?.csrfToken
);
initialization
.then(() => {
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 { inventoryRequest } from "../../../shared/api/inventory-request";
export async function createAssetRelationship(
companyId: string,
values: RelationshipInputDTO,
token: string
) {
export async function createAssetRelationship(companyId: string, values: RelationshipInputDTO) {
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 { 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(
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({
relationship,
companyId,
csrfToken,
}: EndRelationshipDialogProps) {
const client = useQueryClient();
const mutation = useMutation({
mutationFn: () => endAssetRelationship(companyId, relationship.id, csrfToken),
mutationFn: () => endAssetRelationship(companyId, relationship.id),
onSuccess: () => client.invalidateQueries({ queryKey: inventoryKeys.company(companyId) }),
});

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -2,8 +2,8 @@ import type { AssetInput } from "../../../../common/inventory";
import { AssetResponseSchema } from "../../../../common/inventory";
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(
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 type { AssetFormProps } from "../types";
export function useAssetFormController({
companyId,
csrfToken,
asset,
items,
onSaved,
}: AssetFormProps) {
export function useAssetFormController({ companyId, asset, items, onSaved }: AssetFormProps) {
const client = useQueryClient();
const [creatingTemplate, setCreatingTemplate] = useState(false);
const mounted = useRef(true);
@ -68,7 +62,7 @@ export function useAssetFormController({
(template?.values.kind === "template" ? template.values.sections : undefined) ??
[];
const mutation = useMutation({
mutationFn: (input: AssetInput) => saveAsset(companyId, input, csrfToken, asset?.id),
mutationFn: (input: AssetInput) => saveAsset(companyId, input, asset?.id),
onSuccess: async (response) => {
await Promise.all([
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"]) {
const html = await render(
language,
<AssetInventoryPage companyId={companyId} csrfToken="test" lookups={lookups} />
<AssetInventoryPage companyId={companyId} lookups={lookups} />
);
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 () => {
const html = await render(
"es",
<AssetForm
companyId={companyId}
csrfToken="test"
items={[]}
lookups={lookups}
onSaved={() => undefined}
/>
<AssetForm companyId={companyId} items={[]} lookups={lookups} onSaved={() => undefined} />
);
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",
<CatalogTemplateForm
allowQuickCreate={false}
csrfToken="test"
items={[]}
onSaved={() => undefined}
scope={companyId}
@ -136,12 +129,12 @@ test("Las fichas leen ambos sentidos y conservan la auditoría de relaciones ret
};
const source = await render(
"es",
<AssetRelationships assetId={sourceId} companyId={companyId} csrfToken="test" />,
<AssetRelationships assetId={sourceId} companyId={companyId} />,
[link]
);
const target = await render(
"es",
<AssetRelationships assetId={targetId} companyId={companyId} csrfToken="test" />,
<AssetRelationships assetId={targetId} companyId={companyId} />,
[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"));
const ended = await render(
"es",
<AssetRelationships assetId={sourceId} companyId={companyId} csrfToken="test" />,
<AssetRelationships assetId={sourceId} companyId={companyId} />,
[
{
...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"]) {
const html = await render(
language,
<CatalogRelationForm csrfToken="test" onSaved={() => undefined} scope={companyId} />
<CatalogRelationForm onSaved={() => undefined} scope={companyId} />
);
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"]) {
const html = await render(
language,
<WorkstationsPage companyId={companyId} csrfToken="test" lookups={lookups} />
<WorkstationsPage companyId={companyId} lookups={lookups} />
);
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 () => {
const html = await render(
"es",
<WorkstationForm
companyId={companyId}
csrfToken="test"
lookups={lookups}
onSaved={() => undefined}
/>
<WorkstationForm companyId={companyId} lookups={lookups} onSaved={() => undefined} />
);
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
allowQuickCreate={false}
companyId={companyId}
csrfToken="test"
items={[]}
lookups={lookups}
onSaved={() => undefined}
@ -279,7 +266,6 @@ test("la ficha lateral agrupa información y ofrece acciones superiores y enlace
asset={asset}
canEdit
companyId={companyId}
csrfToken="test"
lookups={{
...lookups,
sites: [{ id: siteId, name: "Fábrica" }],

View File

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

View File

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

View File

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

View File

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

View File

@ -2,6 +2,5 @@ import type { BrandDTO } from "../../../../common/brands";
export type DeleteBrandDialogProps = {
brand: BrandDTO;
csrfToken: string;
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 { BrandLogo } from "../../shared/ui";
import { useBrandCatalogPageController } from "../controllers";
import type { BrandCatalogPageProps } from "../types";
export function BrandCatalogPage(props: BrandCatalogPageProps) {
const { csrfToken } = props;
export function BrandCatalogPage() {
const { query, edit, setEdit, open, setOpen, deleting, setDeleting, status, setStatus, items } =
useBrandCatalogPageController();
const { t, i18n } = useTranslation("assets");
@ -137,19 +135,12 @@ export function BrandCatalogPage(props: BrandCatalogPageProps) {
unavailable={query.isError}
/>
<BrandEditorDialog
csrfToken={csrfToken}
onOpenChange={setOpen}
open={open}
{...(edit ? { brand: edit } : {})}
onSaved={() => setOpen(false)}
/>
{deleting && (
<DeleteBrandDialog
brand={deleting}
csrfToken={csrfToken}
onClose={() => setDeleting(undefined)}
/>
)}
{deleting && <DeleteBrandDialog brand={deleting} onClose={() => setDeleting(undefined)} />}
</div>
);
}

View File

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

View File

@ -1,6 +1,7 @@
import assert from "node:assert/strict";
import { test } from "node:test";
import { configureCsrfTokenReader } from "@metis/core/client";
import { UniqueID } from "@repo/rdx-ddd";
import { Maybe } from "@repo/rdx-utils";
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 () => {
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.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 () => {
const html = await render(
"es",
<BrandForm brand={brand} csrfToken="test" onSaved={() => undefined} />
);
const html = await render("es", <BrandForm brand={brand} onSaved={() => undefined} />);
assert.ok(html.includes('readOnly=""'));
assert.ok(html.includes('accept="image/svg+xml,.svg"'));
assert.ok(html.includes("Quitar logotipo"));
assert.ok(html.indexOf("Una marca identifica") < html.lastIndexOf("Guardar"));
const quick = await render(
"en",
<BrandForm csrfToken="test" onSaved={() => undefined} quickCreate />
);
const quick = await render("en", <BrandForm onSaved={() => undefined} quickCreate />);
assert.equal(quick.includes('type="checkbox"'), false);
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;
let status = 200;
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, "{}");
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");
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 {
assert.deepEqual(await brandApi.remove(brand.id, "test"), response);
assert.deepEqual(await brandApi.remove(brand.id), response);
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"]) {
status = code === "BRAND_NOT_FOUND" ? 404 : 409;
response = { code };
await assert.rejects(
() => brandApi.remove(brand.id, "test"),
() => brandApi.remove(brand.id),
(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",
brandId: brand.id,
legacyName: "LG",
csrfToken: "test",
disabled: false,
required: false,
allowQuickCreate: true,
@ -183,7 +178,6 @@ test("Marcas: el campo de serie se identifica por referencia de catálogo, no po
<AssetForm
asset={asset}
companyId={asset.companyId}
csrfToken="test"
items={items}
lookups={{
sites: [],

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -11,7 +11,6 @@ import type { CatalogTemplateFormProps, TemplateFormValues } from "../types";
export function useCatalogTemplateFormController({
scope,
csrfToken,
item,
copy,
items,
@ -36,7 +35,7 @@ export function useCatalogTemplateFormController({
sections: layout.sections,
},
});
const mutation = useCatalogSave(scope, csrfToken, onSaved, item?.id);
const mutation = useCatalogSave(scope, onSaved, item?.id);
const selected = form.watch("fields");
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 = {
scope: string;
csrfToken: string;
item?: CatalogItemDTO | undefined;
copy?: CatalogItemDTO | undefined;
items: readonly CatalogItemDTO[];

View File

@ -17,7 +17,7 @@ import { useCatalogTemplateFormController } from "../controllers";
import type { CatalogTemplateFormProps } from "../types";
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 } =
useCatalogTemplateFormController(props);
const { t } = useTranslation("assets");
@ -117,7 +117,6 @@ export function CatalogTemplateForm(props: CatalogTemplateFormProps) {
</form>
{creatingField && (
<CatalogFieldCreateDialog
csrfToken={csrfToken}
onOpenChange={(open) => {
if (!open) {
setCreatingField(false);

View File

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

View File

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

View File

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

View File

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

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"],
]) {
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(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 () => {
const field = await render(
"es",
<CatalogFieldForm csrfToken="test" onSaved={() => undefined} scope="global" />
);
const field = await render("es", <CatalogFieldForm onSaved={() => undefined} scope="global" />);
assert.ok(field.indexOf('name="description"') < field.indexOf("<aside"));
assert.ok(field.indexOf("<aside") < field.indexOf('type="submit"'));
assert.ok(field.includes("El tipo de dato queda fijo"));
const template = await render(
"es",
<CatalogTemplateForm csrfToken="test" items={[]} onSaved={() => undefined} scope="global" />
<CatalogTemplateForm items={[]} onSaved={() => undefined} scope="global" />
);
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 function useCatalogSave(
scope: string,
csrfToken: string,
onSaved: CatalogSaved,
id?: string
) {
export function useCatalogSave(scope: string, onSaved: CatalogSaved, id?: string) {
const client = useQueryClient();
const mounted = useRef(true);
@ -24,7 +19,7 @@ export function useCatalogSave(
}, []);
return useMutation({
mutationFn: (values: AssetDefinitionSpec) => catalogApi.save(scope, values, csrfToken, id),
mutationFn: (values: AssetDefinitionSpec) => catalogApi.save(scope, values, id),
onSuccess: async (response) => {
await client.invalidateQueries({ queryKey: ["asset-catalog"] });
client.setQueryData(catalogKeys.scope(scope), { items: response.items });

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -6,10 +6,9 @@ export async function saveSupportConnection(
companyId: string,
assetId: string,
values: SupportInput,
token: string,
id?: string
) {
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({
companyId,
assetId,
csrfToken,
connection,
onSaved,
}: SupportFormProps) {
@ -32,7 +31,7 @@ export function useSupportFormController({
const values = form.watch();
const mutation = useMutation({
mutationFn: (input: SupportInput) =>
saveSupportConnection(companyId, assetId, input, csrfToken, connection?.id),
saveSupportConnection(companyId, assetId, input, connection?.id),
onSuccess: async () => {
await client.invalidateQueries({ queryKey: supportKeys.asset(companyId, assetId) });
onSaved();

View File

@ -46,7 +46,6 @@ test("asistencia: formulario con alta rápida y ayuda antes de guardar", async (
<SupportForm
assetId={UniqueID.generateNewID().toPrimitive()}
companyId={UniqueID.generateNewID().toPrimitive()}
csrfToken="test"
onSaved={() => undefined}
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 () => {
const html = await render(
<SupportToolForm csrfToken="test" onSaved={() => undefined} tool={tool} />
);
const html = await render(<SupportToolForm onSaved={() => undefined} tool={tool} />);
assert.ok(html.includes("readOnly"));
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 = {
companyId: string;
assetId: string;
csrfToken: string;
tools: readonly SupportToolDTO[];
connection?: SupportConnectionDTO;
onSaved: () => void;

View File

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

View File

@ -15,7 +15,7 @@ import type { SupportFormProps } from "../types";
import { SupportHelp } from "./support-help";
export function SupportForm(props: SupportFormProps) {
const { csrfToken, tools, connection } = props;
const { tools, connection } = props;
const { quick, setQuick, form, values, mutation, submit } = useSupportFormController(props);
const { t } = useTranslation("assets");
const prefix = useId();
@ -104,7 +104,6 @@ export function SupportForm(props: SupportFormProps) {
</FormActions>
</form>
<SupportToolEditorDialog
csrfToken={csrfToken}
edit={undefined}
onOpenChange={setQuick}
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({
open,
csrfToken,
onOpenChange,
}: {
open: boolean;
csrfToken: string;
onOpenChange: (open: boolean) => void;
}) {
const { t } = useTranslation("assets");
@ -21,7 +19,7 @@ export function SupportToolsDialog({
open={open}
title={t("support.tools")}
>
<SupportTools csrfToken={csrfToken} />
<SupportTools />
</FormDialog>
);
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -11,7 +11,6 @@ import type { QuickCreateDialogProps } from "../types";
export function useQuickCreateDialogController({
target,
scope,
csrfToken,
onCreated,
items,
onClose,
@ -42,7 +41,7 @@ export function useQuickCreateDialogController({
? { kind: "system", name, active: true }
: { 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(
(item) =>
item.companyId === (scope === "global" ? null : scope) && matches(item.values, values)

View File

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

View File

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

View File

@ -9,14 +9,13 @@ import type { AccessQuickEditorProps } from "../types";
import { QuickCreateDialog } from "./quick-create-dialog";
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 { t } = useTranslation("access");
if (plan.type === "simple") {
return (
<QuickCreateDialog
csrfToken={csrfToken}
items={data.items}
onClose={close}
onCreated={created}
@ -44,7 +43,7 @@ export function AccessQuickEditor(props: AccessQuickEditorProps) {
{plan.type === "person" ? (
<>
<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 ? (
<NestedAccessForm
allowQuickCreate={false}
csrfToken={csrfToken}
data={childData}
initialValues={plan.defaults}
kind={plan.kind}

View File

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

View File

@ -17,7 +17,7 @@ function errorCode(error: Error) {
}
export function AccessWorkspacePage(props: AccessWorkspacePageProps) {
const { scope, people = [], csrfToken } = props;
const { scope, people = [] } = props;
const {
query,
kind,
@ -169,7 +169,6 @@ export function AccessWorkspacePage(props: AccessWorkspacePageProps) {
/>
<AccessEditorDialog
complex={complex}
csrfToken={csrfToken}
data={data}
editor={editor}
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(
<I18nextProvider i18n={i18n}>
<QueryClientProvider client={client}>
<AccessWorkspacePage csrfToken="test" scope="global" />
<AccessWorkspacePage scope="global" />
</QueryClientProvider>
</I18nextProvider>
);
@ -51,7 +51,6 @@ test("cuentas ofrecen caducidad opcional y campos sin contraseña", async () =>
<I18nextProvider i18n={i18n}>
<QueryClientProvider client={client}>
<AccessForm
csrfToken="test"
data={{ items: [], reviews: [], coverage: [] }}
kind="account"
onSaved={() => undefined}
@ -141,7 +140,6 @@ test("los ejemplos del sistema orientan sin rellenar datos del alta", async () =
<I18nextProvider i18n={i18n}>
<QueryClientProvider client={client}>
<AccessForm
csrfToken="test"
data={{ items: [], reviews: [], coverage: [] }}
kind="system"
onSaved={() => undefined}
@ -273,7 +271,6 @@ test("los formularios ofrecen altas relacionadas y el segundo modal evita altas
<QueryClientProvider client={client}>
<AccessForm
allowQuickCreate={allowQuickCreate}
csrfToken="test"
data={{ items: [], reviews: [], coverage: [] }}
kind={kind}
onSaved={() => undefined}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -65,7 +65,7 @@ test("aplicaciones: listado sin formularios, alta en cabecera y traducciones", a
]) {
const html = await render(
language ?? "es",
<ApplicationConfigurationsPage company={company} csrfToken="test" />
<ApplicationConfigurationsPage company={company} />
);
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"]) {
const html = await render(
language,
<ApplicationConfigurationForm company={company} csrfToken="test" onSaved={() => undefined} />
<ApplicationConfigurationForm company={company} onSaved={() => undefined} />
);
const text = language === "es" ? companiesSpanish : companiesEnglish;

View File

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

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