Base + compilacion simple
This commit is contained in:
35
src/aplication/BodyValidator.ts
Normal file
35
src/aplication/BodyValidator.ts
Normal 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
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
45
src/aplication/Nfc.usecases.test.ts
Normal file
45
src/aplication/Nfc.usecases.test.ts
Normal 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)
|
||||
})
|
||||
})
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
22
src/aplication/labelTemplate.ts
Normal file
22
src/aplication/labelTemplate.ts
Normal 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
|
||||
`
|
||||
}
|
||||
9
src/aplication/validators.ts
Normal file
9
src/aplication/validators.ts
Normal 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])
|
||||
@@ -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
29
src/domain/Result.ts
Normal 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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
73
src/infrastructure/Nfc.repository.test.ts
Normal file
73
src/infrastructure/Nfc.repository.test.ts
Normal 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);
|
||||
});
|
||||
});
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user