Base + compilacion simple

This commit is contained in:
2026-03-09 17:11:09 +01:00
parent 221abe0d33
commit 97f8c55199
22 changed files with 1006 additions and 32 deletions

2
build.local.sh Executable file
View File

@@ -0,0 +1,2 @@
#/bin/bash
docker compose -f deployment/local/docker/docker-compose.yaml --project-directory ./ build

View File

View File

@@ -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

View File

@@ -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;

View File

@@ -1,21 +1,23 @@
# 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 && \
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" ]

View File

@@ -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

View File

@@ -1,3 +1,4 @@
#!/bin/sh
echo "Lanzando migraciones e iniciando servidor"
npm run migrate && npm run start
npm run build
npm run start

544
package-lock.json generated
View File

@@ -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",

View File

@@ -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": {

2
run.local.sh Executable file
View File

@@ -0,0 +1,2 @@
#/bin/bash
docker compose -f ./deployment/local/docker/docker-compose.yaml --project-directory ./ up --watch

View File

@@ -0,0 +1,35 @@
import type { Result } from "domain/Result.js"
export type Validator<T extends Object> = {
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<T extends Object> {
validatorList: Validator<T>[] = []
constructor(
validators: Validator<T>[]
) {
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
};
}
}

View File

@@ -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;
}
}
}

View File

@@ -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)
})
})

View File

@@ -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
}
}
}
}

View File

@@ -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
`
}

View File

@@ -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])

View File

@@ -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<ActivationCodeDTO, "card_id" | "code_plain">
export type ActivationLogDTO = {
log_id: string,
card_id: string,
code_id: string,
action_type: string,
ip_address: string,
device_info: Record<string, any>,
geo_location: string,
created_at: Date,
}

29
src/domain/Result.ts Normal file
View File

@@ -0,0 +1,29 @@
export type Success<D> = {
error?: undefined | null,
data: D
}
export type Failure<E = Error> = {
data?: undefined | null,
error: E
}
/**
* Result<Error,Data>
*/
export type Result<E, D> = Failure<E> | Success<D>
export async function tryCatch<T>(func: Promise<T>): Promise<Result<{ msg: Error }, T>> {
try {
const res = await func;
return {
data: res
}
} catch (e: unknown) {
return {
error: {
msg: e as Error
}
}
}
}

View File

@@ -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);
});
});

View File

@@ -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<E,T>
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<Result<string, ActivationCodeDTO[]>> {
const query = `
SELECT * FROM activation_codes
WHERE card_id = $1
`
const values = [cardId]
try {
const codeResult = await this.ctx.PostgresClient.query<ActivationCodeDTO>(query, values)
return {
data: codeResult.rows
}
} catch (e) {
return {
error: e as string
}
}
}
public async createActivationCode(args: { cardId: string, code: string }): Promise<Result<string, ActivationCodeDTO>> {
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<ActivationCodeDTO>(query, values)
return {
data: insertResult.rows[0]!
}
} catch (e) {
console.error("Error createActivationCode: ", e)
return {
error: e as string
}
}
}
}

2
stop.local.sh Executable file
View File

@@ -0,0 +1,2 @@
#/bin/bash
docker compose -f deployment/local/docker/docker-compose.yaml --project-directory ./ down -v

View File

@@ -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"