From 97f8c551994056c974c09db2251d3337c85c4a85 Mon Sep 17 00:00:00 2001 From: Alvar San Martin Date: Mon, 9 Mar 2026 17:11:09 +0100 Subject: [PATCH] Base + compilacion simple --- build.local.sh | 2 + deployment/database/init.sql | 0 deployment/database/migrations/1.0.0_base.sql | 14 +- .../migrations/1.0.1_Proc-activate-card.sql | 3 +- deployment/local/docker/Dockerfile.local | 24 +- deployment/local/docker/docker-compose.yaml | 8 +- deployment/local/start.sh | 3 +- package-lock.json | 544 ++++++++++++++++++ package.json | 5 +- run.local.sh | 2 + src/aplication/BodyValidator.ts | 35 ++ src/aplication/Nfc.controller.ts | 36 ++ src/aplication/Nfc.usecases.test.ts | 45 ++ src/aplication/Nfc.usecases.ts | 76 +++ src/aplication/labelTemplate.ts | 22 + src/aplication/validators.ts | 9 + src/domain/NfcRegistry.ts | 34 +- src/domain/Result.ts | 29 + src/infrastructure/Nfc.repository.test.ts | 73 +++ src/infrastructure/Nfc.repository.ts | 66 +++ stop.local.sh | 2 + tsconfig.json | 6 +- 22 files changed, 1006 insertions(+), 32 deletions(-) create mode 100755 build.local.sh create mode 100644 deployment/database/init.sql create mode 100755 run.local.sh create mode 100644 src/aplication/BodyValidator.ts create mode 100644 src/aplication/Nfc.usecases.test.ts create mode 100644 src/aplication/labelTemplate.ts create mode 100644 src/aplication/validators.ts create mode 100644 src/domain/Result.ts create mode 100644 src/infrastructure/Nfc.repository.test.ts create mode 100755 stop.local.sh diff --git a/build.local.sh b/build.local.sh new file mode 100755 index 0000000..5588e48 --- /dev/null +++ b/build.local.sh @@ -0,0 +1,2 @@ +#/bin/bash +docker compose -f deployment/local/docker/docker-compose.yaml --project-directory ./ build diff --git a/deployment/database/init.sql b/deployment/database/init.sql new file mode 100644 index 0000000..e69de29 diff --git a/deployment/database/migrations/1.0.0_base.sql b/deployment/database/migrations/1.0.0_base.sql index 6de6489..b16e7c2 100644 --- a/deployment/database/migrations/1.0.0_base.sql +++ b/deployment/database/migrations/1.0.0_base.sql @@ -1,3 +1,6 @@ +-- Me he pasado mucho con todo el proceso, lo más probable es que haya que eliminar +-- la parte de control de activacion que eso ya no lo lleva la aplicacion. + CREATE EXTENSION pgcrypto; -- para los random bytes -- 1. Función de generacion de uuidv7 copiada de github porque no está en postgre 16 CREATE OR REPLACE FUNCTION @@ -27,7 +30,7 @@ $$ ; -- 2. Tabla de Tarjetas --- NUNCA se guarda el numero completo (PAN). Solo los últimos 4 dígitos. +-- Posiblemente haya que mantener solo el id e ignorar el PAN CREATE TABLE payment_cards ( card_id UUID PRIMARY KEY DEFAULT uuid_generate_v7(), user_id UUID REFERENCES users(user_id), @@ -39,18 +42,23 @@ CREATE TABLE payment_cards ( ); -- 3. Tabla de Códigos de Activación +-- No creo que vaya a recibir confirmación de activación porque es de otro proyecto, +-- pero por lo menos se mantiene el registro de cuando se ha creado. +-- El algoritmo de hash es sha256 CREATE TABLE activation_codes ( code_id UUID PRIMARY KEY DEFAULT uuid_generate_v7(), card_id UUID REFERENCES payment_cards(card_id), -- Una tarjeta, maximo un un código activo borrar o solo con expires_at? + code_plain TEXT NOT NULL, -- code_hash TEXT NOT NULL, -- Guardar el código hasheado, el original se imprime y se manda is_used BOOLEAN DEFAULT FALSE, is_blocked BOOLEAN DEFAULT FALSE, failed_attempts INT DEFAULT 0, - expires_at TIMESTAMPTZ NOT NULL, + expires_at TIMESTAMPTZ, -- Si es nulo es permanenete (Solo para pruebas) created_at TIMESTAMPTZ DEFAULT NOW() ); -- 4. Registro de Auditoría y Dispositivos (Log de Uso) +-- Lo mismo, muy sobredimensionado, no creo que haya falta en este punto CREATE TABLE activation_logs ( log_id UUID PRIMARY KEY DEFAULT uuid_generate_v7(), card_id UUID REFERENCES payment_cards(card_id), @@ -62,6 +70,8 @@ CREATE TABLE activation_logs ( created_at TIMESTAMPTZ DEFAULT NOW() ); +-- Trigger para cuando se haga una actualizacion en activation_codes +-- se supone que cuando se intenta activar la tarjeta. CREATE OR REPLACE FUNCTION log_activation_attempt() RETURNS TRIGGER AS $$ BEGIN diff --git a/deployment/database/migrations/1.0.1_Proc-activate-card.sql b/deployment/database/migrations/1.0.1_Proc-activate-card.sql index 308e31b..56ae8ca 100644 --- a/deployment/database/migrations/1.0.1_Proc-activate-card.sql +++ b/deployment/database/migrations/1.0.1_Proc-activate-card.sql @@ -1,6 +1,7 @@ -- He separado el procedimiento de activacion en otro archivo por comodidad de desarrollo -- creo que es mas seguro meter la lógica de activaciones directamente en la bdd que en -- un server node y asi evitamos problemas de consistencia entre versiones. +-- CREATE OR REPLACE FUNCTION activate_payment_card( p_card_id UUID, -- uuidv7 de la tarjeta @@ -47,7 +48,7 @@ BEGIN RETURN; END IF; - + -- 2.4 Si el código es demasiado viejo IF v_expires_at < NOW() THEN RETURN QUERY SELECT FALSE, 'CODE_EXPIRED'; RETURN; diff --git a/deployment/local/docker/Dockerfile.local b/deployment/local/docker/Dockerfile.local index 10dcbf6..631768f 100644 --- a/deployment/local/docker/Dockerfile.local +++ b/deployment/local/docker/Dockerfile.local @@ -1,22 +1,24 @@ # Stage base para coordinar las fases de build y ejecucion -FROM node:22-alpine AS base +FROM node:22-alpine # Hace falta para la herramienta de migraciones, cuando se publique se # sustituira por el paquete de npm -RUN apk --no-cache add git=latest -WORKDIR /usr/local/app - -COPY ./package.json ./package.lock ./ +#RUN apk --no-cache add git +WORKDIR /app +COPY ./package.json ./package-lock.json ./ +COPY ./src ./src # copia el codigo en general -COPY tsconfig*.json ./ +COPY tsconfig.json ./ COPY .env* ./ -COPY ./.yarnrc.yml ./ -COPY ./deployment/local/docker/start.sh ./ +COPY ./deployment/local/start.sh ./ # Copiar el archivo de migrations? porque ahora no creo que se esté lanzando nada COPY ./deployment/database/migrations ./deployment/database/migrations -RUN npm install --production && \ - npm build && \ - chmod +x start.sh + +RUN npm config set registry https://git.savefamilygps.net/api/packages/SaveFamily/npm/ &&\ + echo "registry=https://registry.npmjs.org/" >> .npmrc &&\ + npm install &&\ + ls && npm run build &&\ + chmod +x start.sh EXPOSE ${PORT} ENTRYPOINT [ "./start.sh" ] diff --git a/deployment/local/docker/docker-compose.yaml b/deployment/local/docker/docker-compose.yaml index b4bd886..81c220b 100644 --- a/deployment/local/docker/docker-compose.yaml +++ b/deployment/local/docker/docker-compose.yaml @@ -6,8 +6,8 @@ networks: services: sf-nfc-api: - container_name: sf-nfc-api - image: sf-nfc-api + container_name: sf-nfc-server + image: sf-nfc-server build: context: ./ dockerfile: deployment/local/docker/Dockerfile.local @@ -45,13 +45,13 @@ services: env_file: - .env ports: - - "${POSTGRES_PORT}:${POSTGRES_PORT}" + - "${POSTGRES_PORT}:5432" volumes: - ./sql-data/:/var/lib/postgres/data - - ./deployment/database/init.sql:/docker-entrypoint-initdb.d/init.sql healthcheck: test: ["CMD-SHELL", "pg_isready -U $${POSTGRES_USER} -d $${POSTGRES_DB}"] interval: 5s retries: 5 start_period: 5s timeout: 5s + command: -p 5432 diff --git a/deployment/local/start.sh b/deployment/local/start.sh index a40eaf9..ea5ec4d 100644 --- a/deployment/local/start.sh +++ b/deployment/local/start.sh @@ -1,3 +1,4 @@ #!/bin/sh echo "Lanzando migraciones e iniciando servidor" -npm run migrate && npm run start +npm run build +npm run start diff --git a/package-lock.json b/package-lock.json index cf3e57d..23829f0 100644 --- a/package-lock.json +++ b/package-lock.json @@ -19,7 +19,9 @@ "@types/express": "^5.0.6", "@types/node": "^25.3.3", "@types/pg": "^8.18.0", + "esbuild": "0.27.3", "ts-node": "^10.9.2", + "tsx": "^4.21.0", "typescript": "^5.9.3" } }, @@ -45,6 +47,448 @@ "node": ">=12" } }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.3.tgz", + "integrity": "sha512-9fJMTNFTWZMh5qwrBItuziu834eOCUcEqymSH7pY+zoMVEZg3gcPuBNxH1EvfVYe9h0x/Ptw8KBzv7qxb7l8dg==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.3.tgz", + "integrity": "sha512-i5D1hPY7GIQmXlXhs2w8AWHhenb00+GxjxRncS2ZM7YNVGNfaMxgzSGuO8o8SJzRc/oZwU2bcScvVERk03QhzA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.3.tgz", + "integrity": "sha512-YdghPYUmj/FX2SYKJ0OZxf+iaKgMsKHVPF1MAq/P8WirnSpCStzKJFjOjzsW0QQ7oIAiccHdcqjbHmJxRb/dmg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.3.tgz", + "integrity": "sha512-IN/0BNTkHtk8lkOM8JWAYFg4ORxBkZQf9zXiEOfERX/CzxW3Vg1ewAhU7QSWQpVIzTW+b8Xy+lGzdYXV6UZObQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.3.tgz", + "integrity": "sha512-Re491k7ByTVRy0t3EKWajdLIr0gz2kKKfzafkth4Q8A5n1xTHrkqZgLLjFEHVD+AXdUGgQMq+Godfq45mGpCKg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.3.tgz", + "integrity": "sha512-vHk/hA7/1AckjGzRqi6wbo+jaShzRowYip6rt6q7VYEDX4LEy1pZfDpdxCBnGtl+A5zq8iXDcyuxwtv3hNtHFg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.3.tgz", + "integrity": "sha512-ipTYM2fjt3kQAYOvo6vcxJx3nBYAzPjgTCk7QEgZG8AUO3ydUhvelmhrbOheMnGOlaSFUoHXB6un+A7q4ygY9w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.3.tgz", + "integrity": "sha512-dDk0X87T7mI6U3K9VjWtHOXqwAMJBNN2r7bejDsc+j03SEjtD9HrOl8gVFByeM0aJksoUuUVU9TBaZa2rgj0oA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.3.tgz", + "integrity": "sha512-s6nPv2QkSupJwLYyfS+gwdirm0ukyTFNl3KTgZEAiJDd+iHZcbTPPcWCcRYH+WlNbwChgH2QkE9NSlNrMT8Gfw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.3.tgz", + "integrity": "sha512-sZOuFz/xWnZ4KH3YfFrKCf1WyPZHakVzTiqji3WDc0BCl2kBwiJLCXpzLzUBLgmp4veFZdvN5ChW4Eq/8Fc2Fg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.3.tgz", + "integrity": "sha512-yGlQYjdxtLdh0a3jHjuwOrxQjOZYD/C9PfdbgJJF3TIZWnm/tMd/RcNiLngiu4iwcBAOezdnSLAwQDPqTmtTYg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.3.tgz", + "integrity": "sha512-WO60Sn8ly3gtzhyjATDgieJNet/KqsDlX5nRC5Y3oTFcS1l0KWba+SEa9Ja1GfDqSF1z6hif/SkpQJbL63cgOA==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.3.tgz", + "integrity": "sha512-APsymYA6sGcZ4pD6k+UxbDjOFSvPWyZhjaiPyl/f79xKxwTnrn5QUnXR5prvetuaSMsb4jgeHewIDCIWljrSxw==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.3.tgz", + "integrity": "sha512-eizBnTeBefojtDb9nSh4vvVQ3V9Qf9Df01PfawPcRzJH4gFSgrObw+LveUyDoKU3kxi5+9RJTCWlj4FjYXVPEA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.3.tgz", + "integrity": "sha512-3Emwh0r5wmfm3ssTWRQSyVhbOHvqegUDRd0WhmXKX2mkHJe1SFCMJhagUleMq+Uci34wLSipf8Lagt4LlpRFWQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.3.tgz", + "integrity": "sha512-pBHUx9LzXWBc7MFIEEL0yD/ZVtNgLytvx60gES28GcWMqil8ElCYR4kvbV2BDqsHOvVDRrOxGySBM9Fcv744hw==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.3.tgz", + "integrity": "sha512-Czi8yzXUWIQYAtL/2y6vogER8pvcsOsk5cpwL4Gk5nJqH5UZiVByIY8Eorm5R13gq+DQKYg0+JyQoytLQas4dA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.3.tgz", + "integrity": "sha512-sDpk0RgmTCR/5HguIZa9n9u+HVKf40fbEUt+iTzSnCaGvY9kFP0YKBWZtJaraonFnqef5SlJ8/TiPAxzyS+UoA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.3.tgz", + "integrity": "sha512-P14lFKJl/DdaE00LItAukUdZO5iqNH7+PjoBm+fLQjtxfcfFE20Xf5CrLsmZdq5LFFZzb5JMZ9grUwvtVYzjiA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.3.tgz", + "integrity": "sha512-AIcMP77AvirGbRl/UZFTq5hjXK+2wC7qFRGoHSDrZ5v5b8DK/GYpXW3CPRL53NkvDqb9D+alBiC/dV0Fb7eJcw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.3.tgz", + "integrity": "sha512-DnW2sRrBzA+YnE70LKqnM3P+z8vehfJWHXECbwBmH/CU51z6FiqTQTHFenPlHmo3a8UgpLyH3PT+87OViOh1AQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.3.tgz", + "integrity": "sha512-NinAEgr/etERPTsZJ7aEZQvvg/A6IsZG/LgZy+81wON2huV7SrK3e63dU0XhyZP4RKGyTm7aOgmQk0bGp0fy2g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.3.tgz", + "integrity": "sha512-PanZ+nEz+eWoBJ8/f8HKxTTD172SKwdXebZ0ndd953gt1HRBbhMsaNqjTyYLGLPdoWHy4zLU7bDVJztF5f3BHA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.3.tgz", + "integrity": "sha512-B2t59lWWYrbRDw/tjiWOuzSsFh1Y/E95ofKz7rIVYSQkUYBjfSgf6oeYPNWHToFRr2zx52JKApIcAS/D5TUBnA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.3.tgz", + "integrity": "sha512-QLKSFeXNS8+tHW7tZpMtjlNb7HKau0QDpwm49u0vUp9y1WOF+PEzkU84y9GqYaAVW8aH8f3GcBck26jh54cX4Q==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.3.tgz", + "integrity": "sha512-4uJGhsxuptu3OcpVAzli+/gWusVGwZZHTlS63hh++ehExkVT8SgiEf7/uC/PclrPPkLhZqGgCTjd0VWLo6xMqA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, "node_modules/@jridgewell/resolve-uri": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", @@ -726,6 +1170,48 @@ "node": ">= 0.4" } }, + "node_modules/esbuild": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.3.tgz", + "integrity": "sha512-8VwMnyGCONIs6cWue2IdpHxHnAjzxnw2Zr7MkVxB2vjmQ2ivqGFb4LEG3SMnv0Gb2F/G/2yA8zUaiL1gywDCCg==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.27.3", + "@esbuild/android-arm": "0.27.3", + "@esbuild/android-arm64": "0.27.3", + "@esbuild/android-x64": "0.27.3", + "@esbuild/darwin-arm64": "0.27.3", + "@esbuild/darwin-x64": "0.27.3", + "@esbuild/freebsd-arm64": "0.27.3", + "@esbuild/freebsd-x64": "0.27.3", + "@esbuild/linux-arm": "0.27.3", + "@esbuild/linux-arm64": "0.27.3", + "@esbuild/linux-ia32": "0.27.3", + "@esbuild/linux-loong64": "0.27.3", + "@esbuild/linux-mips64el": "0.27.3", + "@esbuild/linux-ppc64": "0.27.3", + "@esbuild/linux-riscv64": "0.27.3", + "@esbuild/linux-s390x": "0.27.3", + "@esbuild/linux-x64": "0.27.3", + "@esbuild/netbsd-arm64": "0.27.3", + "@esbuild/netbsd-x64": "0.27.3", + "@esbuild/openbsd-arm64": "0.27.3", + "@esbuild/openbsd-x64": "0.27.3", + "@esbuild/openharmony-arm64": "0.27.3", + "@esbuild/sunos-x64": "0.27.3", + "@esbuild/win32-arm64": "0.27.3", + "@esbuild/win32-ia32": "0.27.3", + "@esbuild/win32-x64": "0.27.3" + } + }, "node_modules/escape-html": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", @@ -911,6 +1397,21 @@ "node": ">= 0.8" } }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, "node_modules/function-bind": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", @@ -966,6 +1467,19 @@ "node": ">= 0.4" } }, + "node_modules/get-tsconfig": { + "version": "4.13.6", + "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.13.6.tgz", + "integrity": "sha512-shZT/QMiSHc/YBLxxOkMtgSid5HFoauqCE3/exfsEcwg1WkeqjG+V40yBbBrsD+jW2HDXcs28xOfcbm2jI8Ddw==", + "dev": true, + "license": "MIT", + "dependencies": { + "resolve-pkg-maps": "^1.0.0" + }, + "funding": { + "url": "https://github.com/privatenumber/get-tsconfig?sponsor=1" + } + }, "node_modules/gopd": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", @@ -1658,6 +2172,16 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/resolve-pkg-maps": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz", + "integrity": "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1" + } + }, "node_modules/revalidator": { "version": "0.1.8", "resolved": "https://registry.npmjs.org/revalidator/-/revalidator-0.1.8.tgz", @@ -1971,6 +2495,26 @@ } } }, + "node_modules/tsx": { + "version": "4.21.0", + "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.21.0.tgz", + "integrity": "sha512-5C1sg4USs1lfG0GFb2RLXsdpXqBSEhAaA/0kPL01wxzpMqLILNxIxIOKiILz+cdg/pLnOUxFYOR5yhHU666wbw==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "~0.27.0", + "get-tsconfig": "^4.7.5" + }, + "bin": { + "tsx": "dist/cli.mjs" + }, + "engines": { + "node": ">=18.0.0" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + } + }, "node_modules/tunnel-ssh": { "version": "4.1.6", "resolved": "https://registry.npmjs.org/tunnel-ssh/-/tunnel-ssh-4.1.6.tgz", diff --git a/package.json b/package.json index 32caac2..e11c0ac 100644 --- a/package.json +++ b/package.json @@ -5,9 +5,10 @@ "main": "index.js", "type": "module", "scripts": { - "test": "echo \"Error: no test specified\" && exit 1", + "test": "tsx --test", "dev": "tsx src/main.ts", "build": "tsc", + "build:esbuild": "esbuild --bundle src/main.ts --outdir=dist --platform=node --format=esm --packages=external", "start": "node dist/main.js", "migrate": "db-migrate -e .env -m ./deployment/database/migrations/ -t 99.0.0" }, @@ -17,7 +18,9 @@ "@types/express": "^5.0.6", "@types/node": "^25.3.3", "@types/pg": "^8.18.0", + "esbuild": "0.27.3", "ts-node": "^10.9.2", + "tsx": "^4.21.0", "typescript": "^5.9.3" }, "dependencies": { diff --git a/run.local.sh b/run.local.sh new file mode 100755 index 0000000..8a821f1 --- /dev/null +++ b/run.local.sh @@ -0,0 +1,2 @@ +#/bin/bash +docker compose -f ./deployment/local/docker/docker-compose.yaml --project-directory ./ up --watch diff --git a/src/aplication/BodyValidator.ts b/src/aplication/BodyValidator.ts new file mode 100644 index 0000000..32efce0 --- /dev/null +++ b/src/aplication/BodyValidator.ts @@ -0,0 +1,35 @@ +import type { Result } from "domain/Result.js" + +export type Validator = { + field: keyof T, + errorMsg: string, + validationFunc: (obj: T) => boolean +} + +/** + * Ejecuta una lista de validadores en orden, si alguno + * falla devuelve un Error + */ +export class BodyValidator { + validatorList: Validator[] = [] + constructor( + validators: Validator[] + ) { + this.validatorList = validators + } + + public validate(obj: T): Result<{ msg: string, field: string }, boolean> { + for (const validator of this.validatorList) { + if (validator.validationFunc(obj) == false) + return { + error: { + msg: validator.errorMsg, + field: String(validator.field) + } + } + } + return { + data: true + }; + } +} diff --git a/src/aplication/Nfc.controller.ts b/src/aplication/Nfc.controller.ts index e69de29..1627255 100644 --- a/src/aplication/Nfc.controller.ts +++ b/src/aplication/Nfc.controller.ts @@ -0,0 +1,36 @@ +import type { ServerContext } from "domain/ServerContext.js"; +import type { NfcUsecases } from "./Nfc.usecases.js"; +import type { Request, Response } from "express" +import { baseValidator } from "./validators.js"; +import { error } from "node:console"; + +export class NfcController { + private ctx: ServerContext; + private nfcUsecases: NfcUsecases; + + constructor(args: { + serverContext: ServerContext, + nfcUsecases: NfcUsecases + }) { + this.ctx = args.serverContext + this.nfcUsecases = args.nfcUsecases + } + + public generateActivationTag() { + return (req: Request, res: Response) => { + const body = req.body + const validate = baseValidator.validate(body) + + if (validate.error != undefined) { + res.status(422).json({ + error: validate.error + }) + return 1; + } + + + return 0; + + } + } +} diff --git a/src/aplication/Nfc.usecases.test.ts b/src/aplication/Nfc.usecases.test.ts new file mode 100644 index 0000000..5f3c082 --- /dev/null +++ b/src/aplication/Nfc.usecases.test.ts @@ -0,0 +1,45 @@ +import { httpclient } from "config/httpclient.config.js"; +import { pgClient } from "config/pgclient.config.js"; +import type { ServerContext } from "domain/ServerContext.js"; +import test, { describe } from "node:test"; +import { NfcUsecases } from "./Nfc.usecases.js"; +import assert from "node:assert"; + + +describe("NFC activation code", () => { + const serverContext: ServerContext = { + PostgresClient: pgClient, + HttpClient: httpclient + } + + const nfcUsecases = new NfcUsecases(serverContext) + + test("Should generate 8 digit codes", () => { + // @ts-expect-error + const code = nfcUsecases.generateActivationCode() + + assert(code != undefined) + assert(code.length == 8) + }) + + test("Generated codes must be validated by its function", () => { + // @ts-expect-error + const code = nfcUsecases.generateActivationCode() + // @ts-expect-error + const validation = nfcUsecases.validateActivationCode({ + code + }) + console.log("Codigo, validation", code, validation) + assert(validation == true) + }) + + test("Invalid codes must be invalidated by its function", () => { + const code = "10000000" // Claramente 0 % 32 no es 1 + // @ts-expect-error + const validation = nfcUsecases.validateActivationCode({ + code + }) + + assert(validation == false) + }) +}) diff --git a/src/aplication/Nfc.usecases.ts b/src/aplication/Nfc.usecases.ts index 87403e9..45007e4 100644 --- a/src/aplication/Nfc.usecases.ts +++ b/src/aplication/Nfc.usecases.ts @@ -1,9 +1,85 @@ import type { ServerContext } from "domain/ServerContext.js"; +import { labelTemplate } from "./labelTemplate.js"; export class NfcUsecases { + private ctx: ServerContext; constructor( serverContext: ServerContext ) { + this.ctx = serverContext + } + private getRandomDayOffset() { + const ONE_DAY_MS = 24 * 60 * 60 * 1000; + const TEN_DAYS_MS = 10 * ONE_DAY_MS; + + const randomOffset = (Math.random() * (2 * TEN_DAYS_MS)) - TEN_DAYS_MS; + + return Math.floor(randomOffset); + } + + /** + * Devuelve un código de activación para una tarjeta con la forma + * [Caracter validacion][7 caracteres] + * El modulo de la suma de los 7 caracteres % RADIX da el valor entero del caracter + * de validacion + */ + private generateActivationCode() { + const RADIX = 32 + // offset de +-10 dias para evitar ataques (se podria adivianr el código) + const randomOffset = this.getRandomDayOffset() + const milis = Date + .now() + const milisOffset = milis + randomOffset + const code = milisOffset.toString(RADIX) + .slice(2) // La parte de año / mes no es significativa + + // Algoritmo de validacion = mod(sum(caracteres)) + const validation = this.generataValidationChar({ + code: code, + radix: RADIX + }) + + return validation + code + } + + private generataValidationChar(args: { + code: string, + radix: number + }) { + const validation = (args.code + .split("") + .map(e => Number.parseInt(e, args.radix)) + .reduce((acc, curr) => acc + curr) % args.radix).toString(args.radix) + + return validation + } + + private validateActivationCode(args: { + code: string, + radix?: number + }) { + const radix = args.radix ?? 32 + const values = args.code.slice(1) + const validationChar = args.code.slice(0, 1) + const testValidationChar = this.generataValidationChar({ + code: values, + radix: radix + }) + return validationChar == testValidationChar + } + + public generateActivationLabel() { + return () => { + const codigo = this.generateActivationCode() + const label = labelTemplate(codigo) + + // Introducir en la bdd + + return { + code: codigo, + label: label + } + } } } diff --git a/src/aplication/labelTemplate.ts b/src/aplication/labelTemplate.ts new file mode 100644 index 0000000..b508262 --- /dev/null +++ b/src/aplication/labelTemplate.ts @@ -0,0 +1,22 @@ +export function labelTemplate(code: string) { + return ` +^XA + +---------- SET LABEL SIZE (30mm x 30mm) ---------- +^PW240 +^LL240 + +---------- CENTERED QR CODE ---------- +^FO50,20 +^BQN,2,7 +^FD${code}^FS + +---------- BOTTOM CENTERED TEXT ---------- +^FO0,195 +^A0N,25,25 +^FB240,1,0,C +^FD${code}^FS + +^XZ +` +} diff --git a/src/aplication/validators.ts b/src/aplication/validators.ts new file mode 100644 index 0000000..e7b2903 --- /dev/null +++ b/src/aplication/validators.ts @@ -0,0 +1,9 @@ +import { BodyValidator, type Validator } from "./BodyValidator.js"; + +const cardIdExists: Validator<{ cardId?: string }> = { + field: "cardId", + validationFunc: (body) => body.cardId != undefined, + errorMsg: "El campo cardId esta undefined" +} + +export const baseValidator = new BodyValidator([cardIdExists]) diff --git a/src/domain/NfcRegistry.ts b/src/domain/NfcRegistry.ts index bf0924a..b3bdb60 100644 --- a/src/domain/NfcRegistry.ts +++ b/src/domain/NfcRegistry.ts @@ -1,12 +1,28 @@ -export type nfcRegsitry = { - account_id: string, - account_number: string +export type CardDTO = { + card_id: string } -export type activationCodes = { - id: number, - code: string, - account_id: string, - creation_date: string, - expiration_date: string +export type ActivationCodeDTO = { + code_id: string, + card_id: string, + code_plain: string, + code_hash: string, + is_used: boolean, + is_blocked: boolean, + failed_attempts: number, + expires_at?: string, + created_at: string, +} + +export type ActivationCodeCreateDTO = Pick + +export type ActivationLogDTO = { + log_id: string, + card_id: string, + code_id: string, + action_type: string, + ip_address: string, + device_info: Record, + geo_location: string, + created_at: Date, } diff --git a/src/domain/Result.ts b/src/domain/Result.ts new file mode 100644 index 0000000..1c579a9 --- /dev/null +++ b/src/domain/Result.ts @@ -0,0 +1,29 @@ +export type Success = { + error?: undefined | null, + data: D +} + +export type Failure = { + data?: undefined | null, + error: E +} + +/** + * Result + */ +export type Result = Failure | Success + +export async function tryCatch(func: Promise): Promise> { + try { + const res = await func; + return { + data: res + } + } catch (e: unknown) { + return { + error: { + msg: e as Error + } + } + } +} diff --git a/src/infrastructure/Nfc.repository.test.ts b/src/infrastructure/Nfc.repository.test.ts new file mode 100644 index 0000000..820d5c2 --- /dev/null +++ b/src/infrastructure/Nfc.repository.test.ts @@ -0,0 +1,73 @@ +import test, { describe, before, after } from "node:test"; +import assert from "node:assert"; +import { httpclient } from "config/httpclient.config.js"; +import { pgClient } from "config/pgclient.config.js"; +import { NfcRepository } from "./Nfc.repository.js"; +import type { ServerContext } from "domain/ServerContext.js"; + +describe("NfcRepository Integration Tests", () => { + const serverContext: ServerContext = { + PostgresClient: pgClient, + HttpClient: httpclient + }; + + const repo = new NfcRepository(serverContext); + const testCardId = "test-card-" + Date.now(); + const testCode = "12345678"; + + // Clean up before and after tests to ensure isolation + const cleanup = async () => { + await pgClient.query("DELETE FROM activation_codes WHERE card_id = $1", [testCardId]); + }; + + before(async () => { + await cleanup(); + }); + + after(async () => { + await cleanup(); + }); + + test("createActivationCode should insert a record into the database", async () => { + const result = await repo.createActivationCode({ + cardId: testCardId, + code: testCode + }); + + if (result.error) { + assert.fail(`createActivationCode failed: ${result.error}`); + } + + assert.ok(result.data, "Data should be returned"); + assert.strictEqual(result.data.card_id, testCardId); + assert.strictEqual(result.data.code_plain, testCode); + assert.ok(result.data.code_hash, "Hash should be generated"); + }); + + test("findActivationCodes should retrieve the inserted record", async () => { + // We assume the previous test inserted the record, but lets be safe or just rely on sequence + const result = await repo.findActivationCodes(testCardId); + + if (result.error) { + assert.fail(`findActivationCodes failed: ${result.error}`); + } + + assert.ok(result.data, "Data should be returned"); + assert.ok(Array.isArray(result.data)); + + const found = result.data.find(code => code.code_plain === testCode); + assert.ok(found, "The inserted code should be found"); + assert.strictEqual(found.card_id, testCardId); + }); + + test("findActivationCodes should return empty array if card has no codes", async () => { + const result = await repo.findActivationCodes("non-existent-card"); + + if (result.error) { + assert.fail(`findActivationCodes failed: ${result.error}`); + } + + assert.ok(result.data, "Data should be returned"); + assert.strictEqual(result.data.length, 0); + }); +}); diff --git a/src/infrastructure/Nfc.repository.ts b/src/infrastructure/Nfc.repository.ts index e69de29..4821926 100644 --- a/src/infrastructure/Nfc.repository.ts +++ b/src/infrastructure/Nfc.repository.ts @@ -0,0 +1,66 @@ +import type { ActivationCodeDTO } from "domain/NfcRegistry.js"; +import type { Result } from "domain/Result.js"; +import type { ServerContext } from "domain/ServerContext.js"; + +// TODO: Pasar a Result + +export class NfcRepository { + private ctx: ServerContext; + + constructor(serverConext: ServerContext) { + this.ctx = serverConext; + } + + /** + * Devuleve todos los códigos de activación que ha podido tener una tarjeta + */ + public async findActivationCodes(cardId: string, args?: {}): Promise> { + const query = ` + SELECT * FROM activation_codes + WHERE card_id = $1 + ` + const values = [cardId] + try { + const codeResult = await this.ctx.PostgresClient.query(query, values) + return { + data: codeResult.rows + } + } catch (e) { + return { + error: e as string + } + } + } + + public async createActivationCode(args: { cardId: string, code: string }): Promise> { + const query = ` + INSERT INTO activation_codes( + card_id, + code_plain, + code_hash + ) + VALUES ( + $1, + $2, + digest($2,'sha256') + ) + RETURNING( + code_id,card_id,code_plain,code_hash,is_used,is_blocked,failed_attempts,created_at,expires_at + ) + ` + const values = [args.cardId, args.code] + try { + const insertResult = await this.ctx.PostgresClient.query(query, values) + return { + data: insertResult.rows[0]! + } + } catch (e) { + console.error("Error createActivationCode: ", e) + return { + error: e as string + } + } + } + + +} diff --git a/stop.local.sh b/stop.local.sh new file mode 100755 index 0000000..29acd37 --- /dev/null +++ b/stop.local.sh @@ -0,0 +1,2 @@ +#/bin/bash +docker compose -f deployment/local/docker/docker-compose.yaml --project-directory ./ down -v diff --git a/tsconfig.json b/tsconfig.json index 1a43722..045015e 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -7,9 +7,9 @@ "outDir": "./dist", // Environment Settings // See also https://aka.ms/tsconfig/module - "module": "nodenext", - "target": "esnext", - "moduleResolution": "nodenext", + "module": "esnext", + "target": "es2022", + "moduleResolution": "bundler", // For nodejs: "lib": [ "esnext"