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

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