From 7f336df1463c40c7146c5d5bf9d9e41a105a421a Mon Sep 17 00:00:00 2001 From: David Date: Thu, 8 Oct 2026 14:14:01 +0200 Subject: [PATCH] =?UTF-8?q?Refactorizaci=C3=B3n?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- AGENTS.md | 2 + apps/server/src/app.test.ts | 45 ++ apps/server/src/app.ts | 29 +- apps/server/src/config/database.ts | 3 +- apps/server/src/index.ts | 16 +- apps/web/src/app.tsx | 60 +-- apps/web/src/language-switcher.tsx | 60 +++ modules/assets/src/web/asset-form.tsx | 13 +- .../assets/src/web/asset-relationships.tsx | 131 +----- .../assets/src/web/asset-selection-panel.tsx | 63 +++ modules/assets/src/web/asset-support.tsx | 53 +-- modules/assets/src/web/catalog-cell.tsx | 75 +++ modules/assets/src/web/catalog-details.tsx | 15 + modules/assets/src/web/catalog-error.tsx | 23 + modules/assets/src/web/catalog-form.tsx | 56 +-- modules/assets/src/web/catalog-name.tsx | 36 ++ modules/assets/src/web/catalog-page.tsx | 178 +------ .../assets/src/web/catalog-relation-form.tsx | 4 +- .../assets/src/web/catalog-row-actions.tsx | 39 ++ .../assets/src/web/catalog-template-form.tsx | 6 +- modules/assets/src/web/inventory-feedback.tsx | 12 + modules/assets/src/web/inventory-page.tsx | 63 +-- modules/assets/src/web/relationship-form.tsx | 135 ++++++ modules/assets/src/web/support-feedback.tsx | 17 + modules/assets/src/web/support-form.tsx | 3 +- modules/assets/src/web/support-help.tsx | 14 - modules/assets/src/web/support-history.tsx | 31 ++ modules/assets/src/web/support-summary.tsx | 27 ++ modules/assets/src/web/support-tool-form.tsx | 3 +- modules/assets/src/web/support.test.tsx | 2 +- modules/assets/src/web/use-catalog-save.ts | 34 ++ modules/assets/src/web/workstation-form.tsx | 4 +- modules/assets/src/web/workstations-page.tsx | 25 +- .../src/api/domain/access/access-policy.ts | 3 - .../src/web/access/access-field-control.tsx | 42 +- .../src/web/access/access-field-option.ts | 1 + .../src/web/access/access-form-fields.tsx | 223 +++++++++ .../src/web/access/access-form-props.ts | 22 + .../companies/src/web/access/access-form.tsx | 338 +------------- .../src/web/access/access-quick-editor.tsx | 114 +++++ .../companies/src/web/access/access-row.tsx | 42 +- .../src/web/access/access-workspace.tsx | 3 +- .../companies/src/web/access/entry-state.tsx | 38 ++ .../src/web/access/permissions-control.tsx | 45 ++ .../application-configuration-form.tsx | 63 +-- .../application-configurations-page.tsx | 31 +- .../web/applications/application-history.tsx | 35 ++ .../applications/application-quick-editor.tsx | 53 +++ .../certificate-configuration-form.tsx | 61 +-- .../certificate-configurations-page.tsx | 29 +- .../web/certificates/certificate-history.tsx | 33 ++ .../certificates/certificate-quick-editor.tsx | 53 +++ .../src/web/components/assignment-form.tsx | 2 +- .../src/web/components/companies-page.tsx | 3 +- .../web/components/companies-workspace.tsx | 101 +--- .../src/web/components/company-detail.tsx | 218 +-------- .../src/web/components/company-section.tsx | 106 +++++ .../src/web/components/company-tab.ts | 22 + .../src/web/components/dashboard-api.ts | 3 +- .../src/web/components/dashboard-page.tsx | 38 +- .../src/web/components/dashboard-panel.tsx | 39 ++ .../src/web/components/department-form.tsx | 92 ++++ .../src/web/components/department-section.tsx | 91 +--- .../web/components/global-catalog-content.tsx | 26 ++ .../src/web/components/people-section.tsx | 194 +------- .../src/web/components/person-details.tsx | 205 ++++++++ .../src/web/components/site-section.tsx | 99 ++++ .../src/web/components/workspace-content.tsx | 75 +++ .../src/web/components/workspace-location.ts | 3 + .../src/web/folders/folder-mapping-form.tsx | 62 +-- .../web/folders/folder-mapping-targets.tsx | 88 ++++ .../template-detail.tsx | 27 +- .../template-feedback.tsx | 25 + .../intervention-templates/template-form.tsx | 3 +- .../intervention-templates/template-help.tsx | 22 - .../template-history.tsx | 27 ++ .../intervention-templates/templates-page.tsx | 5 +- .../intervention-company-field.tsx | 41 ++ .../web/interventions/intervention-detail.tsx | 96 +--- .../interventions/intervention-feedback.tsx | 25 + .../web/interventions/intervention-form.tsx | 40 +- .../web/interventions/intervention-help.tsx | 22 - .../interventions/intervention-snapshot.tsx | 92 ++++ .../web/interventions/interventions-page.tsx | 24 +- .../ticket-intervention-form.tsx | 50 ++ .../interventions/ticket-interventions.tsx | 47 +- .../src/web/mail/mail-configuration-form.tsx | 216 ++------- .../web/mail/mail-configuration-targets.tsx | 109 +++++ .../src/web/mail/mail-quick-editor.tsx | 75 +++ .../src/web/mail/mailbox-usage-prompt.tsx | 30 ++ .../printers/printer-configurations-page.tsx | 29 +- .../src/web/printers/printer-history.tsx | 29 ++ .../src/web/profiles/environment-content.tsx | 239 ++++++++++ .../src/web/profiles/environment-section.tsx | 67 +++ .../web/profiles/hardware-characteristics.tsx | 26 ++ .../web/profiles/profile-configurations.tsx | 66 +++ .../src/web/profiles/profile-selection.ts | 7 + .../web/profiles/use-environment-queries.ts | 120 +++++ .../web/profiles/work-environment-page.tsx | 441 +----------------- .../src/web/profiles/work-profiles-page.tsx | 69 +-- .../src/web/tickets/ticket-feedback.tsx | 17 + .../companies/src/web/tickets/ticket-form.tsx | 3 +- .../companies/src/web/tickets/ticket-help.tsx | 14 - .../src/web/tickets/ticket-quick-editor.tsx | 2 +- .../src/web/tickets/tickets-page.tsx | 37 +- .../security/scrypt-password-hasher.test.ts | 1 + .../security/scrypt-password-hasher.ts | 4 +- .../components/authenticated-workspace.tsx | 121 +++++ .../src/web/components/custom-workspace.tsx | 82 ++++ .../src/web/components/identity-workspace.tsx | 194 +------- .../src/web/components/workspace-page.ts | 14 + modules/resources/src/web/catalog-cell.tsx | 81 ++++ modules/resources/src/web/catalog-error.tsx | 23 + modules/resources/src/web/catalog-form.tsx | 56 +-- modules/resources/src/web/catalog-name.tsx | 36 ++ modules/resources/src/web/catalog-page.tsx | 172 +------ .../resources/src/web/catalog-row-actions.tsx | 39 ++ .../src/web/catalog-template-form.tsx | 6 +- .../resources/src/web/integration-summary.tsx | 47 ++ .../resources/src/web/inventory-feedback.tsx | 12 + modules/resources/src/web/resource-detail.tsx | 47 +- modules/resources/src/web/resource-editor.tsx | 3 +- modules/resources/src/web/resource-form.tsx | 15 +- modules/resources/src/web/resources-page.tsx | 6 +- modules/resources/src/web/use-catalog-save.ts | 34 ++ 125 files changed, 3802 insertions(+), 3306 deletions(-) create mode 100644 apps/web/src/language-switcher.tsx create mode 100644 modules/assets/src/web/asset-selection-panel.tsx create mode 100644 modules/assets/src/web/catalog-cell.tsx create mode 100644 modules/assets/src/web/catalog-details.tsx create mode 100644 modules/assets/src/web/catalog-error.tsx create mode 100644 modules/assets/src/web/catalog-name.tsx create mode 100644 modules/assets/src/web/catalog-row-actions.tsx create mode 100644 modules/assets/src/web/inventory-feedback.tsx create mode 100644 modules/assets/src/web/relationship-form.tsx create mode 100644 modules/assets/src/web/support-feedback.tsx create mode 100644 modules/assets/src/web/support-history.tsx create mode 100644 modules/assets/src/web/support-summary.tsx create mode 100644 modules/assets/src/web/use-catalog-save.ts create mode 100644 modules/companies/src/web/access/access-field-option.ts create mode 100644 modules/companies/src/web/access/access-form-fields.tsx create mode 100644 modules/companies/src/web/access/access-form-props.ts create mode 100644 modules/companies/src/web/access/access-quick-editor.tsx create mode 100644 modules/companies/src/web/access/entry-state.tsx create mode 100644 modules/companies/src/web/access/permissions-control.tsx create mode 100644 modules/companies/src/web/applications/application-history.tsx create mode 100644 modules/companies/src/web/applications/application-quick-editor.tsx create mode 100644 modules/companies/src/web/certificates/certificate-history.tsx create mode 100644 modules/companies/src/web/certificates/certificate-quick-editor.tsx create mode 100644 modules/companies/src/web/components/company-section.tsx create mode 100644 modules/companies/src/web/components/company-tab.ts create mode 100644 modules/companies/src/web/components/dashboard-panel.tsx create mode 100644 modules/companies/src/web/components/department-form.tsx create mode 100644 modules/companies/src/web/components/global-catalog-content.tsx create mode 100644 modules/companies/src/web/components/person-details.tsx create mode 100644 modules/companies/src/web/components/site-section.tsx create mode 100644 modules/companies/src/web/components/workspace-content.tsx create mode 100644 modules/companies/src/web/components/workspace-location.ts create mode 100644 modules/companies/src/web/folders/folder-mapping-targets.tsx create mode 100644 modules/companies/src/web/intervention-templates/template-feedback.tsx create mode 100644 modules/companies/src/web/intervention-templates/template-history.tsx create mode 100644 modules/companies/src/web/interventions/intervention-company-field.tsx create mode 100644 modules/companies/src/web/interventions/intervention-feedback.tsx create mode 100644 modules/companies/src/web/interventions/intervention-snapshot.tsx create mode 100644 modules/companies/src/web/interventions/ticket-intervention-form.tsx create mode 100644 modules/companies/src/web/mail/mail-configuration-targets.tsx create mode 100644 modules/companies/src/web/mail/mail-quick-editor.tsx create mode 100644 modules/companies/src/web/mail/mailbox-usage-prompt.tsx create mode 100644 modules/companies/src/web/printers/printer-history.tsx create mode 100644 modules/companies/src/web/profiles/environment-content.tsx create mode 100644 modules/companies/src/web/profiles/environment-section.tsx create mode 100644 modules/companies/src/web/profiles/hardware-characteristics.tsx create mode 100644 modules/companies/src/web/profiles/profile-configurations.tsx create mode 100644 modules/companies/src/web/profiles/profile-selection.ts create mode 100644 modules/companies/src/web/profiles/use-environment-queries.ts create mode 100644 modules/companies/src/web/tickets/ticket-feedback.tsx create mode 100644 modules/identity/src/web/components/authenticated-workspace.tsx create mode 100644 modules/identity/src/web/components/custom-workspace.tsx create mode 100644 modules/identity/src/web/components/workspace-page.ts create mode 100644 modules/resources/src/web/catalog-cell.tsx create mode 100644 modules/resources/src/web/catalog-error.tsx create mode 100644 modules/resources/src/web/catalog-name.tsx create mode 100644 modules/resources/src/web/catalog-row-actions.tsx create mode 100644 modules/resources/src/web/integration-summary.tsx create mode 100644 modules/resources/src/web/inventory-feedback.tsx create mode 100644 modules/resources/src/web/use-catalog-save.ts diff --git a/AGENTS.md b/AGENTS.md index c08b0b8..333144b 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -9,6 +9,8 @@ Antes de modificar código: comprobar pwd, git status --short, node --version y Usar Node.js 24.x y la versión de PNPM fijada en package.json. No imprimir, copiar ni versionar .env locales o secretos. +`packages/shadcn-ui` contiene código de terceros/vendor. No refactorizar, reformatear, reorganizar ni modificar su implementación salvo instrucción explícita del usuario. Puede consultarse su API pública para integrarlo desde código propio. + ## Convenciones - Código e identificadores en inglés; comentarios, TSDoc y documentación en castellano. diff --git a/apps/server/src/app.test.ts b/apps/server/src/app.test.ts index 59a7c57..9de5210 100644 --- a/apps/server/src/app.test.ts +++ b/apps/server/src/app.test.ts @@ -44,3 +44,48 @@ test("HTTP real: estado, contrato, errores y trazas sin datos sensibles", async }); } }); + +test("HTTP real: entrada JSON inválida y errores internos conservan un contrato seguro", async () => { + const entries: HttpLogEntry[] = []; + const server = createApp((entry) => entries.push(entry), { + middleware: (_request, _response, next) => next(), + register: (app) => { + app.get("/api/failure", () => { + throw new Error("secret-test-value: internal SQL and credentials"); + }); + }, + }).listen(0, "127.0.0.1"); + try { + await once(server, "listening"); + const address = server.address(); + assert.ok(address && typeof address !== "string"); + const baseUrl = `http://127.0.0.1:${address.port}`; + for (const [body, contentType, status] of [ + ['{"token":"secret-test-value",', "application/json", 400], + [JSON.stringify({ value: "x".repeat(64 * 1024) }), "application/json", 413], + ["{}", "application/json; charset=unsupported", 415], + ] as const) { + const response = await fetch(`${baseUrl}/api/__health`, { + method: "POST", + headers: { "content-type": contentType }, + body, + }); + assert.equal(response.status, status); + const error = HttpErrorResponseSchema.parse(await response.json()); + assert.equal(error.code, "INVALID_REQUEST"); + assert.equal(error.requestId, response.headers.get("x-request-id")); + } + const response = await fetch(`${baseUrl}/api/failure`); + assert.equal(response.status, 500); + const error = HttpErrorResponseSchema.parse(await response.json()); + assert.equal(error.code, "INTERNAL_ERROR"); + assert.equal(error.requestId, response.headers.get("x-request-id")); + assert.equal(entries.length, 4); + assert.equal(JSON.stringify(entries).includes("secret-test-value"), false); + } finally { + await new Promise((resolve, reject) => { + server.close((error) => (error ? reject(error) : resolve())); + server.closeAllConnections(); + }); + } +}); diff --git a/apps/server/src/app.ts b/apps/server/src/app.ts index fea8dd4..76c8db7 100644 --- a/apps/server/src/app.ts +++ b/apps/server/src/app.ts @@ -47,17 +47,38 @@ export function createApp( }; response.status(404).json(error); }); - const errorHandler: ErrorRequestHandler = (_error: unknown, _request, response, next) => { + const errorHandler: ErrorRequestHandler = (failure: unknown, _request, response, next) => { if (response.headersSent) { - next(_error); + next(failure); return; } + const status = bodyFailureStatus(failure); const error: HttpErrorResponseDTO = { - code: "INTERNAL_ERROR", + code: status === 500 ? "INTERNAL_ERROR" : "INVALID_REQUEST", requestId: response.getHeader("x-request-id")?.toString() ?? "", }; - response.status(500).json(error); + response.status(status).json(error); }; app.use(errorHandler); return app; } + +function bodyFailureStatus(failure: unknown): number { + if (!(failure instanceof Error && "type" in failure)) { + return 500; + } + // Solo se reconocen errores públicos del parser, nunca mensajes o status arbitrarios. + switch (failure.type) { + case "entity.parse.failed": + case "request.aborted": + case "request.size.invalid": + return 400; + case "entity.too.large": + return 413; + case "charset.unsupported": + case "encoding.unsupported": + return 415; + default: + return 500; + } +} diff --git a/apps/server/src/config/database.ts b/apps/server/src/config/database.ts index eca5a75..567b8e4 100644 --- a/apps/server/src/config/database.ts +++ b/apps/server/src/config/database.ts @@ -25,7 +25,8 @@ export function createDatabase(environment: ReturnType { console.error(JSON.stringify({ event: "server_start_failed" })); process.exitCode = 1; }); + let stopping = false; const shutdown = () => { + if (stopping) { + return; + } + stopping = true; identity.stop(); - server.close(() => { + server.close((error) => { + if (error) { + console.error(JSON.stringify({ event: "server_close_failed" })); + process.exitCode = 1; + } database.close().catch(() => { console.error(JSON.stringify({ event: "database_close_failed" })); process.exitCode = 1; @@ -32,7 +41,10 @@ async function start(): Promise { process.once("SIGINT", shutdown); process.once("SIGTERM", shutdown); } catch (error) { - await database.close(); + // El fallo del cleanup no debe sustituir la causa original del arranque. + await database.close().catch(() => { + console.error(JSON.stringify({ event: "database_close_failed" })); + }); throw error; } } diff --git a/apps/web/src/app.tsx b/apps/web/src/app.tsx index 3117ef5..84d40b3 100644 --- a/apps/web/src/app.tsx +++ b/apps/web/src/app.tsx @@ -1,66 +1,8 @@ import { CompaniesWorkspace } from "@metis/companies/client"; import { IdentityWorkspace, identityApi, identityKeys } from "@metis/identity/client"; -import { - Select, - SelectContent, - SelectItem, - SelectTrigger, - SelectValue, -} from "@repo/shadcn-ui/components/select"; import { useQuery } from "@tanstack/react-query"; -import { useId, useState } from "react"; -import { useTranslation } from "react-i18next"; -function LanguageSwitcher() { - const { t, i18n } = useTranslation("core"); - const languageOptions = ["es", "en"].map((value) => ({ - value, - label: t(`languageNames.${value}`), - })); - const [languageError, setLanguageError] = useState(false); - const languageLabelId = useId(); - - return ( -
-
- {t("language")} - -
- - {languageError && ( -

- {t("languageError")} -

- )} -
- ); -} +import { LanguageSwitcher } from "./language-switcher"; export function App() { const session = useQuery({ diff --git a/apps/web/src/language-switcher.tsx b/apps/web/src/language-switcher.tsx new file mode 100644 index 0000000..9c6cb79 --- /dev/null +++ b/apps/web/src/language-switcher.tsx @@ -0,0 +1,60 @@ +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "@repo/shadcn-ui/components/select"; +import { useId, useState } from "react"; +import { useTranslation } from "react-i18next"; + +export function LanguageSwitcher() { + const { t, i18n } = useTranslation("core"); + const languageOptions = ["es", "en"].map((value) => ({ + value, + label: t(`languageNames.${value}`), + })); + const [languageError, setLanguageError] = useState(false); + const languageLabelId = useId(); + + return ( +
+
+ {t("language")} + +
+ + {languageError && ( +

+ {t("languageError")} +

+ )} +
+ ); +} diff --git a/modules/assets/src/web/asset-form.tsx b/modules/assets/src/web/asset-form.tsx index 18156aa..fdcc004 100644 --- a/modules/assets/src/web/asset-form.tsx +++ b/modules/assets/src/web/asset-form.tsx @@ -18,21 +18,16 @@ import { AssetInputSchema, AssetStatusSchema } from "../common/inventory"; import { AssetCharacteristicControl } from "./asset-characteristic-control"; import { BrandSelect } from "./brand-select"; import { CatalogTemplateForm } from "./catalog-template-form"; -import { InventoryApiError, inventoryApi, inventoryKeys } from "./inventory-api"; +import { inventoryApi, inventoryKeys } from "./inventory-api"; +import { InventoryFeedback } from "./inventory-feedback"; + export type InventoryLookups = { sites: readonly { id: string; name: string }[]; people: readonly { id: string; name: string; active: boolean }[]; onCreateSite: (onCreated: (id: string) => void) => void; onCreatePerson: (onCreated: (id: string) => void) => void; }; -export function InventoryFeedback({ error }: { error: Error | null }) { - const { t } = useTranslation("assets"); - return error ? ( -

- {t(`inventoryErrors.${error instanceof InventoryApiError ? error.code : "INTERNAL_ERROR"}`)} -

- ) : null; -} + export function AssetForm({ companyId, csrfToken, diff --git a/modules/assets/src/web/asset-relationships.tsx b/modules/assets/src/web/asset-relationships.tsx index 191d658..3940029 100644 --- a/modules/assets/src/web/asset-relationships.tsx +++ b/modules/assets/src/web/asset-relationships.tsx @@ -1,19 +1,14 @@ import { Button } from "@repo/shadcn-ui/components/button"; -import { ContextHelp } from "@repo/shadcn-ui/components/context-help"; import { FormActions, FormDialog } from "@repo/shadcn-ui/components/form-dialog"; -import { Label } from "@repo/shadcn-ui/components/label"; -import { SelectWithAction } from "@repo/shadcn-ui/components/select-with-action"; -import { Textarea } from "@repo/shadcn-ui/components/textarea"; import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; -import { useId, useState } from "react"; +import { useState } from "react"; import { useTranslation } from "react-i18next"; -import type { RelationshipDTO, RelationshipInputDTO } from "../common"; +import type { RelationshipDTO } from "../common"; -import { InventoryFeedback } from "./asset-form"; -import { catalogApi, catalogKeys } from "./catalog-api"; -import { CatalogRelationForm } from "./catalog-relation-form"; import { inventoryApi, inventoryKeys } from "./inventory-api"; +import { InventoryFeedback } from "./inventory-feedback"; +import { RelationshipForm } from "./relationship-form"; export function AssetRelationships({ companyId, @@ -148,121 +143,3 @@ export function AssetRelationships({ ); } -function RelationshipForm({ - companyId, - csrfToken, - assetId, - onSaved, -}: { - companyId: string; - csrfToken: string; - assetId: string; - onSaved: () => void; -}) { - const { t } = useTranslation("assets"); - const id = useId(); - const client = useQueryClient(); - const [typeId, setTypeId] = useState(""); - const [targetId, setTargetId] = useState(""); - const [notes, setNotes] = useState(""); - const [quick, setQuick] = useState(false); - const catalog = useQuery({ - queryKey: catalogKeys.scope(companyId), - queryFn: () => catalogApi.get(companyId), - }); - const inventory = useQuery({ - queryKey: inventoryKeys.company(companyId), - queryFn: () => inventoryApi.get(companyId), - }); - const mutation = useMutation({ - mutationFn: (values: RelationshipInputDTO) => inventoryApi.link(companyId, values, csrfToken), - onSuccess: async () => { - await client.invalidateQueries({ queryKey: inventoryKeys.company(companyId) }); - onSaved(); - }, - }); - return ( - <> -
{ - event.preventDefault(); - mutation.mutate({ sourceId: assetId, targetId, typeId, notes }); - }} - > -
- - setQuick(true) }} - disabled={mutation.isPending || catalog.isPending} - id={`${id}-type`} - label={t("relationshipType")} - onValueChange={setTypeId} - options={(catalog.data?.items ?? []) - .filter((item) => item.values.kind === "relation" && item.effectiveActive) - .map((item) => ({ value: item.id, label: item.values.name }))} - placeholder={t("chooseRelation")} - required - value={typeId} - /> -
-
- - asset.id !== assetId) - .map((asset) => ({ value: asset.id, label: asset.values.name }))} - placeholder={t("chooseAsset")} - required - value={targetId} - /> -
-
- -