From 350b8a84227df64b99e9455580670d822068a690 Mon Sep 17 00:00:00 2001 From: david Date: Sat, 1 Feb 2025 22:48:13 +0100 Subject: [PATCH] . --- apps/server/.eslintrc.json | 46 ++++++ apps/server/package.json | 2 + apps/server/src/{infrastructure => }/app.ts | 17 ++- .../aggregate-root-repository.interface.ts | 8 ++ .../src/common/domain/aggregate-root.ts | 41 ++++++ .../server/src/common/domain/domain-entity.ts | 37 +++++ .../domain/events/domain-event-handle.ts | 3 + .../domain/events/domain-event.interface.ts | 7 + .../src/common/domain/events/domain-event.ts | 135 ++++++++++++++++++ apps/server/src/common/domain/events/index.ts | 2 + apps/server/src/common/domain/index.ts | 6 + .../common/domain/result.spec.ts | 0 .../{contexts => }/common/domain/result.ts | 0 .../domain/value-objects}/index.ts | 1 - .../domain/value-objects}/unique-id.spec.ts | 2 +- .../domain/value-objects}/unique-id.ts | 2 +- .../domain/value-objects}/value-object.ts | 0 .../common/infrastructure/database/index.ts | 2 + .../database/transaction-manager.interface.ts | 27 ++++ .../database/transaction-manager.ts | 60 ++++++++ .../infrastructure}/index.ts | 0 .../common/infrastructure/passport/index.ts | 1 + .../infrastructure/passport/passport.ts | 27 ++++ .../infrastructure}/sequelize/index.ts | 0 .../sequelize/sequelize-repository.ts | 66 +++++++++ .../sequelize-transaction-manager.ts | 21 +++ .../common/presentation/express/api-error.ts | 37 +++++ .../express/express-controller.ts | 63 ++++++++ .../src/common/presentation/express/index.ts | 4 + .../express/middlewares/error-handler.ts | 31 ++++ .../presentation/express/middlewares/index.ts | 1 + .../presentation/express/validate-request.ts | 29 ++++ apps/server/src/common/presentation/index.ts | 1 + apps/server/src/config/database.ts | 16 ++- .../application/auth-service.interface.ts | 14 ++ .../contexts/auth/application/auth.service.ts | 58 ++++---- .../src/contexts/auth/application/index.ts | 12 +- .../domain/aggregates/authenticated-user.ts | 40 ++++++ .../contexts/auth/domain/aggregates/index.ts | 1 + .../contexts/auth/domain/auth-user.model.ts | 47 ------ .../src/contexts/auth/domain/events/index.ts | 1 + .../domain/events/user-authenticated.event.ts | 13 ++ apps/server/src/contexts/auth/domain/index.ts | 5 +- ...authenticated-user-repository.interface.ts | 1 + .../auth/domain/repositories/index.ts | 1 + .../domain/value-objects/auth-user-roles.ts | 2 +- .../domain/value-objects/email-address.ts | 2 +- .../domain/value-objects/password-hash.ts | 2 +- .../auth/domain/value-objects/username.ts | 2 +- .../contexts/auth/infraestructure/index.ts | 3 + .../auth/infraestructure/jwt.helper.ts | 13 ++ .../authenticated-user-mapper.interface.ts | 14 ++ .../mappers/authenticated-user.mapper.ts | 42 ++++++ .../auth/infraestructure/mappers/index.ts | 2 + .../sequelize/auth-user-repository.spec.ts | 72 ---------- .../sequelize/auth-user.repository.ts | 42 ------ .../authenticated-user.repository.ts | 76 ++++++++++ .../auth/infraestructure/sequelize/index.ts | 2 +- .../auth/presentation/auth.controller.ts | 57 ++++++++ .../contexts/auth/presentation/auth.routes.ts | 77 ++++++++++ .../auth/presentation/controllers/index.ts | 1 + .../controllers/register.controller.ts | 35 +++++ .../auth/presentation/dto/auth.request.dto.ts | 14 ++ .../presentation/dto/auth.response.dto.ts | 31 ++++ .../presentation/dto/auth.validation.dto.ts | 16 +++ .../contexts/auth/presentation/dto/index.ts | 3 + .../src/contexts/auth/presentation/index.ts | 1 + .../sequelize/sequelize-repository.ts | 68 --------- apps/server/src/index.ts | 2 +- .../src/infrastructure/express/auth.routes.ts | 13 -- .../src/infrastructure/express/index.ts | 1 - apps/server/src/infrastructure/index.ts | 1 - apps/server/src/routes/index.ts | 1 + apps/server/src/routes/v1.routes.ts | 21 +++ apps/server/tsconfig.json | 1 + pnpm-lock.yaml | 12 ++ 76 files changed, 1224 insertions(+), 293 deletions(-) create mode 100644 apps/server/.eslintrc.json rename apps/server/src/{infrastructure => }/app.ts (60%) create mode 100644 apps/server/src/common/domain/aggregate-root-repository.interface.ts create mode 100644 apps/server/src/common/domain/aggregate-root.ts create mode 100644 apps/server/src/common/domain/domain-entity.ts create mode 100644 apps/server/src/common/domain/events/domain-event-handle.ts create mode 100644 apps/server/src/common/domain/events/domain-event.interface.ts create mode 100644 apps/server/src/common/domain/events/domain-event.ts create mode 100644 apps/server/src/common/domain/events/index.ts create mode 100644 apps/server/src/common/domain/index.ts rename apps/server/src/{contexts => }/common/domain/result.spec.ts (100%) rename apps/server/src/{contexts => }/common/domain/result.ts (100%) rename apps/server/src/{contexts/common/domain => common/domain/value-objects}/index.ts (70%) rename apps/server/src/{contexts/common/domain => common/domain/value-objects}/unique-id.spec.ts (95%) rename apps/server/src/{contexts/common/domain => common/domain/value-objects}/unique-id.ts (96%) rename apps/server/src/{contexts/common/domain => common/domain/value-objects}/value-object.ts (100%) create mode 100644 apps/server/src/common/infrastructure/database/index.ts create mode 100644 apps/server/src/common/infrastructure/database/transaction-manager.interface.ts create mode 100644 apps/server/src/common/infrastructure/database/transaction-manager.ts rename apps/server/src/{contexts/common/infraestructure => common/infrastructure}/index.ts (100%) create mode 100644 apps/server/src/common/infrastructure/passport/index.ts create mode 100644 apps/server/src/common/infrastructure/passport/passport.ts rename apps/server/src/{contexts/common/infraestructure => common/infrastructure}/sequelize/index.ts (100%) create mode 100644 apps/server/src/common/infrastructure/sequelize/sequelize-repository.ts create mode 100644 apps/server/src/common/infrastructure/sequelize/sequelize-transaction-manager.ts create mode 100644 apps/server/src/common/presentation/express/api-error.ts create mode 100644 apps/server/src/common/presentation/express/express-controller.ts create mode 100644 apps/server/src/common/presentation/express/index.ts create mode 100644 apps/server/src/common/presentation/express/middlewares/error-handler.ts create mode 100644 apps/server/src/common/presentation/express/middlewares/index.ts create mode 100644 apps/server/src/common/presentation/express/validate-request.ts create mode 100644 apps/server/src/common/presentation/index.ts create mode 100644 apps/server/src/contexts/auth/application/auth-service.interface.ts create mode 100644 apps/server/src/contexts/auth/domain/aggregates/authenticated-user.ts create mode 100644 apps/server/src/contexts/auth/domain/aggregates/index.ts delete mode 100644 apps/server/src/contexts/auth/domain/auth-user.model.ts create mode 100644 apps/server/src/contexts/auth/domain/events/index.ts create mode 100644 apps/server/src/contexts/auth/domain/events/user-authenticated.event.ts create mode 100644 apps/server/src/contexts/auth/domain/repositories/authenticated-user-repository.interface.ts create mode 100644 apps/server/src/contexts/auth/domain/repositories/index.ts create mode 100644 apps/server/src/contexts/auth/infraestructure/index.ts create mode 100644 apps/server/src/contexts/auth/infraestructure/jwt.helper.ts create mode 100644 apps/server/src/contexts/auth/infraestructure/mappers/authenticated-user-mapper.interface.ts create mode 100644 apps/server/src/contexts/auth/infraestructure/mappers/authenticated-user.mapper.ts create mode 100644 apps/server/src/contexts/auth/infraestructure/mappers/index.ts delete mode 100644 apps/server/src/contexts/auth/infraestructure/sequelize/auth-user-repository.spec.ts delete mode 100644 apps/server/src/contexts/auth/infraestructure/sequelize/auth-user.repository.ts create mode 100644 apps/server/src/contexts/auth/infraestructure/sequelize/authenticated-user.repository.ts create mode 100644 apps/server/src/contexts/auth/presentation/auth.controller.ts create mode 100644 apps/server/src/contexts/auth/presentation/auth.routes.ts create mode 100644 apps/server/src/contexts/auth/presentation/controllers/index.ts create mode 100644 apps/server/src/contexts/auth/presentation/controllers/register.controller.ts create mode 100644 apps/server/src/contexts/auth/presentation/dto/auth.request.dto.ts create mode 100644 apps/server/src/contexts/auth/presentation/dto/auth.response.dto.ts create mode 100644 apps/server/src/contexts/auth/presentation/dto/auth.validation.dto.ts create mode 100644 apps/server/src/contexts/auth/presentation/dto/index.ts create mode 100644 apps/server/src/contexts/auth/presentation/index.ts delete mode 100644 apps/server/src/contexts/common/infraestructure/sequelize/sequelize-repository.ts delete mode 100644 apps/server/src/infrastructure/express/auth.routes.ts delete mode 100644 apps/server/src/infrastructure/express/index.ts delete mode 100644 apps/server/src/infrastructure/index.ts create mode 100644 apps/server/src/routes/index.ts create mode 100644 apps/server/src/routes/v1.routes.ts diff --git a/apps/server/.eslintrc.json b/apps/server/.eslintrc.json new file mode 100644 index 00000000..f324a57e --- /dev/null +++ b/apps/server/.eslintrc.json @@ -0,0 +1,46 @@ +{ + "root": true, + "env": { + "browser": false, + "es6": true, + "node": true + }, + "extends": [ + "eslint:recommended", + "plugin:@typescript-eslint/recommended", + "plugin:jest/recommended", + "prettier" + ], + "parser": "@typescript-eslint/parser", + "plugins": ["@typescript-eslint", "sort-class-members"], + "rules": { + "@typescript-eslint/no-explicit-any": "off", + "@typescript-eslint/no-inferrable-types": "off", + "@typescript-eslint/no-empty-interface": "off", + "@typescript-eslint/recommended-requiring-type-checking": "off", + "@typescript-eslint/no-unused-vars": "warn", + "lines-between-class-members": ["error", "always", { "exceptAfterSingleLine": true }], + + "sort-class-members/sort-class-members": [ + 2, + { + "order": [ + "[static-properties]", + "[static-methods]", + "[conventional-private-properties]", + "[properties]", + "constructor", + "[methods]", + "[conventional-private-methods]" + ], + "accessorPairPositioning": "getThenSet" + } + ] + }, + "overrides": [ + { + "files": ["**/*.test.ts"], + "env": { "jest": true, "node": true } + } + ] +} diff --git a/apps/server/package.json b/apps/server/package.json index 52f3f29a..db9d7095 100644 --- a/apps/server/package.json +++ b/apps/server/package.json @@ -23,6 +23,7 @@ "@types/jest": "^29.5.14", "@types/jsonwebtoken": "^9.0.8", "@types/node": "^22.10.7", + "@types/passport": "^1.0.17", "@types/passport-jwt": "^4.0.1", "@types/response-time": "^2.3.8", "@typescript-eslint/eslint-plugin": "^8.22.0", @@ -42,6 +43,7 @@ "esbuild": "^0.24.0", "express": "^4.21.2", "helmet": "^8.0.0", + "http-status": "^2.1.0", "jsonwebtoken": "^9.0.2", "mariadb": "^3.4.0", "module-alias": "^2.2.3", diff --git a/apps/server/src/infrastructure/app.ts b/apps/server/src/app.ts similarity index 60% rename from apps/server/src/infrastructure/app.ts rename to apps/server/src/app.ts index 99375305..0e9838cc 100644 --- a/apps/server/src/infrastructure/app.ts +++ b/apps/server/src/app.ts @@ -1,13 +1,15 @@ +import { errorHandler } from "@common/presentation"; import dotenv from "dotenv"; import express, { Application } from "express"; import helmet from "helmet"; import responseTime from "response-time"; -import { authRoutes } from "./express"; +import { v1Routes } from "./routes"; dotenv.config(); export function createApp(): Application { const app = express(); + app.set("port", process.env.PORT ?? 3002); // secure apps by setting various HTTP headers app.use(helmet()); @@ -18,13 +20,16 @@ export function createApp(): Application { app.use(express.text()); app.use(express.urlencoded({ extended: true })); - // set up the response-time middleware - app.use(responseTime()); + app.use(responseTime()); // set up the response-time middleware - app.set("port", process.env.PORT ?? 3002); + // Inicializar Passport + app.use(passport.initialize()); - // Registrar rutas del módulo de autenticación - app.use("/api/auth", authRoutes); + // Registrar rutas de la API + app.use("/api/v1", v1Routes()); + + // Gestión global de errores + app.use(errorHandler); return app; } diff --git a/apps/server/src/common/domain/aggregate-root-repository.interface.ts b/apps/server/src/common/domain/aggregate-root-repository.interface.ts new file mode 100644 index 00000000..272bec7f --- /dev/null +++ b/apps/server/src/common/domain/aggregate-root-repository.interface.ts @@ -0,0 +1,8 @@ +import { Result } from "./result"; + +export interface IAggregateRootRepository { + findById(id: string): Promise>; + create(entity: T): Promise>; + update(entity: T): Promise>; + delete(id: string): Promise>; +} diff --git a/apps/server/src/common/domain/aggregate-root.ts b/apps/server/src/common/domain/aggregate-root.ts new file mode 100644 index 00000000..1f52f282 --- /dev/null +++ b/apps/server/src/common/domain/aggregate-root.ts @@ -0,0 +1,41 @@ +import { DomainEntity } from "./domain-entity"; +import { IDomainEvent } from "./events"; + +export abstract class AggregateRoot extends DomainEntity { + private _domainEvents: IDomainEvent[] = []; + + private logDomainEventAdded(event: IDomainEvent): void { + const thisClass = Reflect.getPrototypeOf(this); + const domainEventClass = Reflect.getPrototypeOf(event); + console.info( + `[Domain Event Created]:`, + thisClass?.constructor.name, + "==>", + domainEventClass?.constructor.name + ); + } + + /** + * 🔹 Agregar un evento de dominio al agregado + */ + protected addDomainEvent(event: IDomainEvent): void { + this._domainEvents.push(event); + + // Log the domain event + this.logDomainEventAdded(event); + } + + /** + * 🔹 Obtener los eventos de dominio pendientes + */ + get domainEvents(): IDomainEvent[] { + return this._domainEvents; + } + + /** + * 🔹 Limpiar la lista de eventos después de procesarlos + */ + public clearDomainEvents(): void { + this._domainEvents.splice(0, this._domainEvents.length); + } +} diff --git a/apps/server/src/common/domain/domain-entity.ts b/apps/server/src/common/domain/domain-entity.ts new file mode 100644 index 00000000..ea0fdca9 --- /dev/null +++ b/apps/server/src/common/domain/domain-entity.ts @@ -0,0 +1,37 @@ +import { UniqueID } from "./value-objects/unique-id"; + +export abstract class DomainEntity { + protected readonly _id: UniqueID; + protected readonly _props: T; + + protected constructor(props: T, id?: UniqueID) { + this._id = id ? id : UniqueID.generateNewID().data; + this._props = props; + } + + protected _flattenProps(props: T): { [s: string]: any } { + return Object.entries(props).reduce((result: any, [key, valueObject]) => { + console.log(key, valueObject.value); + result[key] = valueObject.value; + + return result; + }, {}); + } + + get id(): UniqueID { + return this._id; + } + + equals(other: DomainEntity): boolean { + return other instanceof DomainEntity && this.id.equals(other.id); + } + + toString(): { [s: string]: string } { + const flattenProps = this._flattenProps(this._props); + + return { + id: this._id.toString(), + ...flattenProps.map((prop: any) => String(prop)), + }; + } +} diff --git a/apps/server/src/common/domain/events/domain-event-handle.ts b/apps/server/src/common/domain/events/domain-event-handle.ts new file mode 100644 index 00000000..4301cdc6 --- /dev/null +++ b/apps/server/src/common/domain/events/domain-event-handle.ts @@ -0,0 +1,3 @@ +export interface IHandle { + setupSubscriptions(): void; +} diff --git a/apps/server/src/common/domain/events/domain-event.interface.ts b/apps/server/src/common/domain/events/domain-event.interface.ts new file mode 100644 index 00000000..8a2730d9 --- /dev/null +++ b/apps/server/src/common/domain/events/domain-event.interface.ts @@ -0,0 +1,7 @@ +import { UniqueID } from "../value-objects/unique-id"; + +export interface IDomainEvent { + eventName: string; // Nombre del evento + aggregateId: UniqueID; // ID del agregado que generó el evento + occurredAt: Date; // Fecha y hora del evento +} diff --git a/apps/server/src/common/domain/events/domain-event.ts b/apps/server/src/common/domain/events/domain-event.ts new file mode 100644 index 00000000..cb989292 --- /dev/null +++ b/apps/server/src/common/domain/events/domain-event.ts @@ -0,0 +1,135 @@ +// https://khalilstemmler.com/articles/typescript-domain-driven-design/chain-business-logic-domain-events/ + +import { AggregateRoot } from "../aggregate-root"; +import { UniqueID } from "../value-objects/unique-id"; +import { IDomainEvent } from "./domain-event.interface"; + +export class DomainEvents { + private static handlersMap: { [key: string]: Array<(event: IDomainEvent) => void> } = {}; + private static markedAggregates: AggregateRoot[] = []; + + /** + * @method markAggregateForDispatch + * @static + * @desc Called by aggregate root objects that have created domain + * events to eventually be dispatched when the infrastructure commits + * the unit of work. + */ + + public static markAggregateForDispatch(aggregate: AggregateRoot): void { + const aggregateFound = !!this.findMarkedAggregateByID(aggregate.id); + + if (!aggregateFound) { + this.markedAggregates.push(aggregate); + } + } + + /** + * @method dispatchAggregateEvents + * @static + * @private + * @desc Call all of the handlers for any domain events on this aggregate. + */ + + private static dispatchAggregateEvents(aggregate: AggregateRoot): void { + aggregate.domainEvents.forEach((event: IDomainEvent) => this.dispatch(event)); + } + + /** + * @method removeAggregateFromMarkedDispatchList + * @static + * @desc Removes an aggregate from the marked list. + */ + + private static removeAggregateFromMarkedDispatchList(aggregate: AggregateRoot): void { + const index = this.markedAggregates.findIndex((a) => a.equals(aggregate)); + + this.markedAggregates.splice(index, 1); + } + + /** + * @method findMarkedAggregateByID + * @static + * @desc Finds an aggregate within the list of marked aggregates. + */ + + private static findMarkedAggregateByID(id: UniqueID): AggregateRoot { + let found!: AggregateRoot; + + for (let aggregate of this.markedAggregates) { + if (aggregate.id.equals(id)) { + found = aggregate; + } + } + + return found; + } + + /** + * @method dispatchEventsForAggregate + * @static + * @desc When all we know is the ID of the aggregate, call this + * in order to dispatch any handlers subscribed to events on the + * aggregate. + */ + + public static dispatchEventsForAggregate(id: UniqueID): void { + const aggregate = this.findMarkedAggregateByID(id); + + if (aggregate) { + this.dispatchAggregateEvents(aggregate); + aggregate.clearDomainEvents(); + this.removeAggregateFromMarkedDispatchList(aggregate); + } + } + + /** + * @method register + * @static + * @desc Register a handler to a domain event. + */ + + public static register(callback: (event: IDomainEvent) => void, eventClassName: string): void { + if (!this.handlersMap.hasOwnProperty(eventClassName)) { + this.handlersMap[eventClassName] = []; + } + this.handlersMap[eventClassName].push(callback); + } + + /** + * @method clearHandlers + * @static + * @desc Useful for testing. + */ + + public static clearHandlers(): void { + this.handlersMap = {}; + } + + /** + * @method clearMarkedAggregates + * @static + * @desc Useful for testing. + */ + + public static clearMarkedAggregates(): void { + this.markedAggregates = []; + } + + /** + * @method dispatch + * @static + * @desc Invokes all of the subscribers to a particular domain event. + */ + + private static dispatch(event: IDomainEvent): void { + const eventClassName: string = event.constructor.name; + + if (this.handlersMap.hasOwnProperty(eventClassName)) { + const handlers: any[] = this.handlersMap[eventClassName]; + for (let handler of handlers) { + handler(event); + } + } + } +} diff --git a/apps/server/src/common/domain/events/index.ts b/apps/server/src/common/domain/events/index.ts new file mode 100644 index 00000000..1ba97866 --- /dev/null +++ b/apps/server/src/common/domain/events/index.ts @@ -0,0 +1,2 @@ +export * from "./domain-event"; +export * from "./domain-event.interface"; diff --git a/apps/server/src/common/domain/index.ts b/apps/server/src/common/domain/index.ts new file mode 100644 index 00000000..4e273629 --- /dev/null +++ b/apps/server/src/common/domain/index.ts @@ -0,0 +1,6 @@ +export * from "./aggregate-root"; +export * from "./aggregate-root-repository.interface"; +export * from "./domain-entity"; +export * from "./events/domain-event.interface"; +export * from "./result"; +export * from "./value-objects"; diff --git a/apps/server/src/contexts/common/domain/result.spec.ts b/apps/server/src/common/domain/result.spec.ts similarity index 100% rename from apps/server/src/contexts/common/domain/result.spec.ts rename to apps/server/src/common/domain/result.spec.ts diff --git a/apps/server/src/contexts/common/domain/result.ts b/apps/server/src/common/domain/result.ts similarity index 100% rename from apps/server/src/contexts/common/domain/result.ts rename to apps/server/src/common/domain/result.ts diff --git a/apps/server/src/contexts/common/domain/index.ts b/apps/server/src/common/domain/value-objects/index.ts similarity index 70% rename from apps/server/src/contexts/common/domain/index.ts rename to apps/server/src/common/domain/value-objects/index.ts index f2052b13..2d2be954 100644 --- a/apps/server/src/contexts/common/domain/index.ts +++ b/apps/server/src/common/domain/value-objects/index.ts @@ -1,3 +1,2 @@ -export * from "./result"; export * from "./unique-id"; export * from "./value-object"; diff --git a/apps/server/src/contexts/common/domain/unique-id.spec.ts b/apps/server/src/common/domain/value-objects/unique-id.spec.ts similarity index 95% rename from apps/server/src/contexts/common/domain/unique-id.spec.ts rename to apps/server/src/common/domain/value-objects/unique-id.spec.ts index 811ace92..8e702b80 100644 --- a/apps/server/src/contexts/common/domain/unique-id.spec.ts +++ b/apps/server/src/common/domain/value-objects/unique-id.spec.ts @@ -1,4 +1,4 @@ -import { UniqueID } from "./unique-id"; +import { UniqueID } from "./value-objects/unique-id"; describe("UniqueID Value Object", () => { it("should generate a new UUID using generateNewID()", () => { diff --git a/apps/server/src/contexts/common/domain/unique-id.ts b/apps/server/src/common/domain/value-objects/unique-id.ts similarity index 96% rename from apps/server/src/contexts/common/domain/unique-id.ts rename to apps/server/src/common/domain/value-objects/unique-id.ts index de367d11..2f5797a2 100644 --- a/apps/server/src/contexts/common/domain/unique-id.ts +++ b/apps/server/src/common/domain/value-objects/unique-id.ts @@ -1,6 +1,6 @@ import { v4 as uuidv4 } from "uuid"; import { z } from "zod"; -import { Result } from "./result"; +import { Result } from "../result"; import { ValueObject } from "./value-object"; const UUIDSchema = z.string().uuid({ message: "Invalid UUID format" }); diff --git a/apps/server/src/contexts/common/domain/value-object.ts b/apps/server/src/common/domain/value-objects/value-object.ts similarity index 100% rename from apps/server/src/contexts/common/domain/value-object.ts rename to apps/server/src/common/domain/value-objects/value-object.ts diff --git a/apps/server/src/common/infrastructure/database/index.ts b/apps/server/src/common/infrastructure/database/index.ts new file mode 100644 index 00000000..46489078 --- /dev/null +++ b/apps/server/src/common/infrastructure/database/index.ts @@ -0,0 +1,2 @@ +export * from "./transaction-manager"; +export * from "./transaction-manager.interface"; diff --git a/apps/server/src/common/infrastructure/database/transaction-manager.interface.ts b/apps/server/src/common/infrastructure/database/transaction-manager.interface.ts new file mode 100644 index 00000000..dea3e5a5 --- /dev/null +++ b/apps/server/src/common/infrastructure/database/transaction-manager.interface.ts @@ -0,0 +1,27 @@ +export interface ITransactionManager { + /** + * 🔹 Inicia una transacción + */ + start(): Promise; + + /** + * 🔹 Obtiene la transacción activa + */ + getTransaction(): any; + + /** + * 🔹 Ejecuta un bloque de código dentro de una transacción + * Si algo falla, se hace rollback automáticamente. + */ + execute(work: (transaction: any) => Promise): Promise; + + /** + * 🔹 Confirma la transacción + */ + commit(): Promise; + + /** + * 🔹 Revierte la transacción + */ + rollback(): Promise; +} diff --git a/apps/server/src/common/infrastructure/database/transaction-manager.ts b/apps/server/src/common/infrastructure/database/transaction-manager.ts new file mode 100644 index 00000000..affcd677 --- /dev/null +++ b/apps/server/src/common/infrastructure/database/transaction-manager.ts @@ -0,0 +1,60 @@ +import { ITransactionManager } from "./transaction-manager.interface"; + +export abstract class TransactionManager implements ITransactionManager { + protected _transaction: any | null = null; + + /** + * 🔹 Inicia una transacción si no hay una activa + */ + async start(): Promise { + if (!this._transaction) { + this._transaction = await this._startTransaction(); + } + } + + /** + * 🔹 Devuelve la transacción activa + */ + getTransaction(): any { + if (!this._transaction) { + throw new Error("No active transaction. Call start() first."); + } + return this._transaction; + } + + /** + * 🔹 Ejecuta una función dentro de una transacción + */ + async execute(work: (transaction: any) => Promise): Promise { + await this.start(); + try { + const result = await work(this.getTransaction()); + await this.commit(); + return result; + } catch (error) { + await this.rollback(); + throw error; + } + } + + /** + * 🔹 Métodos abstractos para manejar transacciones + */ + protected abstract _startTransaction(): Promise; + protected abstract _commitTransaction(): Promise; + protected abstract _rollbackTransaction(): Promise; + + async commit(): Promise { + if (this._transaction) { + await this._commitTransaction(); + this._transaction = null; + } + } + + async rollback(): Promise { + if (this._transaction) { + await this._rollbackTransaction(); + this._transaction = null; + } + } +} diff --git a/apps/server/src/contexts/common/infraestructure/index.ts b/apps/server/src/common/infrastructure/index.ts similarity index 100% rename from apps/server/src/contexts/common/infraestructure/index.ts rename to apps/server/src/common/infrastructure/index.ts diff --git a/apps/server/src/common/infrastructure/passport/index.ts b/apps/server/src/common/infrastructure/passport/index.ts new file mode 100644 index 00000000..cee3a0f8 --- /dev/null +++ b/apps/server/src/common/infrastructure/passport/index.ts @@ -0,0 +1 @@ +export * from "./passport"; diff --git a/apps/server/src/common/infrastructure/passport/passport.ts b/apps/server/src/common/infrastructure/passport/passport.ts new file mode 100644 index 00000000..cd2aad5a --- /dev/null +++ b/apps/server/src/common/infrastructure/passport/passport.ts @@ -0,0 +1,27 @@ +import { authUserRepository } from "@contexts/auth/infraestructure"; +import passport from "passport"; +import { ExtractJwt, Strategy as JwtStrategy } from "passport-jwt"; + +const SECRET_KEY = process.env.JWT_SECRET || "supersecretkey"; + +// Configuración de la estrategia JWT +const jwtOptions = { + jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(), + secretOrKey: SECRET_KEY, +}; + +passport.use( + new JwtStrategy(jwtOptions, async (payload, done) => { + try { + const userResult = await authUserRepository.findById(payload.userId); + if (userResult.isError()) { + return done(null, false); + } + return done(null, userResult.data); + } catch (error) { + return done(error, false); + } + }) +); + +export default passport; diff --git a/apps/server/src/contexts/common/infraestructure/sequelize/index.ts b/apps/server/src/common/infrastructure/sequelize/index.ts similarity index 100% rename from apps/server/src/contexts/common/infraestructure/sequelize/index.ts rename to apps/server/src/common/infrastructure/sequelize/index.ts diff --git a/apps/server/src/common/infrastructure/sequelize/sequelize-repository.ts b/apps/server/src/common/infrastructure/sequelize/sequelize-repository.ts new file mode 100644 index 00000000..f4cb5a8c --- /dev/null +++ b/apps/server/src/common/infrastructure/sequelize/sequelize-repository.ts @@ -0,0 +1,66 @@ +import { IAggregateRootRepository, Result } from "@common/domain"; +import { Transaction } from "sequelize"; + +export abstract class SequelizeRepository implements IAggregateRootRepository { + /** + * 🔹 Convertir un modelo de Sequelize en un agregado del dominio + * Cada repositorio concreto debe implementar este método. + */ + protected abstract toDomain(entity: any): Result; + + /** + * 🔹 Convertir un agregado del dominio en datos listos para persistir + */ + protected abstract toPersistence(aggregate: T): any; + + /** + * 🔹 Buscar por ID y devolver el agregado + */ + async findById(id: string, transaction?: Transaction): Promise> { + const entity = await this._findById(id, transaction); + if (!entity) { + return Result.fail(new Error("Entity not found")); + } + return this.toDomain(entity); + } + + /** + * 🔹 Crear un nuevo agregado en la BD + */ + async create(aggregate: T, transaction?: Transaction): Promise> { + const data = this.toPersistence(aggregate); + await this._create(data, transaction); + return Result.ok(); + } + + /** + * 🔹 Actualizar un agregado en la BD + */ + async update(aggregate: T, transaction?: Transaction): Promise> { + const data = this.toPersistence(aggregate); + const updated = await this._update(data.id, data, transaction); + if (!updated) { + return Result.fail(new Error("Failed to update entity")); + } + return Result.ok(); + } + + /** + * 🔹 Eliminar un agregado de la BD + */ + async delete(id: string, transaction?: Transaction): Promise> { + const deleted = await this._delete(id, transaction); + if (!deleted) { + return Result.fail(new Error("Failed to delete entity")); + } + return Result.ok(); + } + + /** + * 🔹 Métodos privados que deben ser implementados en la infraestructura + */ + protected abstract _findById(id: string, transaction?: Transaction): Promise; + protected abstract _create(data: any, transaction?: Transaction): Promise; + protected abstract _update(id: string, data: any, transaction?: Transaction): Promise; + protected abstract _delete(id: string, transaction?: Transaction): Promise; +} diff --git a/apps/server/src/common/infrastructure/sequelize/sequelize-transaction-manager.ts b/apps/server/src/common/infrastructure/sequelize/sequelize-transaction-manager.ts new file mode 100644 index 00000000..afd4b3d3 --- /dev/null +++ b/apps/server/src/common/infrastructure/sequelize/sequelize-transaction-manager.ts @@ -0,0 +1,21 @@ +import { sequelize } from "@config/database"; +import { Transaction } from "sequelize"; +import { TransactionManager } from "../database"; + +export class SequelizeTransactionManager extends TransactionManager { + protected async _startTransaction(): Promise { + return await sequelize.transaction(); + } + + protected async _commitTransaction(): Promise { + if (this._transaction) { + await this._transaction.commit(); + } + } + + protected async _rollbackTransaction(): Promise { + if (this._transaction) { + await this._transaction.rollback(); + } + } +} diff --git a/apps/server/src/common/presentation/express/api-error.ts b/apps/server/src/common/presentation/express/api-error.ts new file mode 100644 index 00000000..6cac5a8d --- /dev/null +++ b/apps/server/src/common/presentation/express/api-error.ts @@ -0,0 +1,37 @@ +interface IApiErrorOptions { + status: number; + title: string; + detail: string; + type?: string; + instance?: string; + errors?: any[]; + [key: string]: any; // Para permitir añadir campos extra +} + +export class ApiError extends Error { + public status: number; + public title: string; + public detail: string; + public type: string; + public instance?: string; + public errors?: any[]; + public timestamp: string; + + constructor(options: IApiErrorOptions) { + super(options.title); + + // Asegura que la instancia sea reconocida correctamente como ApiError + Object.setPrototypeOf(this, ApiError.prototype); + + // Campos obligatorios + this.status = options.status; + this.title = options.title; + this.detail = options.detail; + this.timestamp = new Date().toISOString(); + + // Campos opcionales con valores por defecto + this.type = options.type ?? "about:blank"; + this.instance = options.instance; + this.errors = options.errors; + } +} diff --git a/apps/server/src/common/presentation/express/express-controller.ts b/apps/server/src/common/presentation/express/express-controller.ts new file mode 100644 index 00000000..aa70837c --- /dev/null +++ b/apps/server/src/common/presentation/express/express-controller.ts @@ -0,0 +1,63 @@ +import { NextFunction, Request, Response } from "express"; +import httpStatus from "http-status"; + +export abstract class ExpressController { + protected req!: Request; + protected res!: Response; + protected next!: NextFunction; + + protected abstract executeImpl(): Promise; + + public execute(req: Request, res: Response, next: NextFunction): void { + this.req = req; + this.res = res; + this.next = next; + + this.executeImpl(); + } + + protected ok(dto?: T) { + return dto ? this.res.status(httpStatus.OK).json(dto) : this.res.status(httpStatus.OK).send(); + } + + protected fail(error: string | Error) { + console.error("ExpressController FAIL:", error); + return this.res + .status(httpStatus.INTERNAL_SERVER_ERROR) + .json({ message: error instanceof Error ? error.message : error }); + } + + protected created(dto?: T) { + return dto + ? this.res.status(httpStatus.CREATED).json(dto) + : this.res.status(httpStatus.CREATED).send(); + } + + protected noContent() { + return this.res.status(httpStatus.NO_CONTENT).send(); + } + + protected clientError(message?: string) { + return this.res.status(httpStatus.BAD_REQUEST).json({ message }); + } + + protected unauthorizedError(message?: string) { + return this.res.status(httpStatus.UNAUTHORIZED).json({ message }); + } + + protected forbiddenError(message?: string) { + return this.res.status(httpStatus.FORBIDDEN).json({ message }); + } + + protected notFoundError(message?: string) { + return this.res.status(httpStatus.NOT_FOUND).json({ message }); + } + + protected conflictError(message?: string) { + return this.res.status(httpStatus.CONFLICT).json({ message }); + } + + protected invalidInputError(message?: string) { + return this.res.status(httpStatus.UNPROCESSABLE_ENTITY).json({ message }); + } +} diff --git a/apps/server/src/common/presentation/express/index.ts b/apps/server/src/common/presentation/express/index.ts new file mode 100644 index 00000000..50e4a8fa --- /dev/null +++ b/apps/server/src/common/presentation/express/index.ts @@ -0,0 +1,4 @@ +export * from "./api-error"; +export * from "./express-controller"; +export * from "./middlewares"; +export * from "./validate-request"; diff --git a/apps/server/src/common/presentation/express/middlewares/error-handler.ts b/apps/server/src/common/presentation/express/middlewares/error-handler.ts new file mode 100644 index 00000000..f71b4091 --- /dev/null +++ b/apps/server/src/common/presentation/express/middlewares/error-handler.ts @@ -0,0 +1,31 @@ +import { NextFunction, Request, Response } from "express"; +import { ApiError } from "../api-error"; + +export const errorHandler = (err: any, req: Request, res: Response, next: NextFunction) => { + // Si ya se envió una respuesta, delegamos al siguiente error handler + if (res.headersSent) { + return next(err); + } + + // Verifica si el error es una instancia de ApiError + if (err instanceof ApiError) { + // Respuesta con formato RFC 7807 + return res.status(err.status).json({ + type: err.type, + title: err.title, + status: err.status, + detail: err.detail, + instance: err.instance ?? req.originalUrl, + errors: err.errors ?? [], // Aquí puedes almacenar validaciones, etc. + }); + } + + // Si no es un ApiError, lo tratamos como un error interno (500) + return res.status(500).json({ + type: "https://example.com/probs/internal-server-error", + title: "Internal Server Error", + status: 500, + detail: err.message || "Ha ocurrido un error inesperado.", + instance: req.originalUrl, + }); +}; diff --git a/apps/server/src/common/presentation/express/middlewares/index.ts b/apps/server/src/common/presentation/express/middlewares/index.ts new file mode 100644 index 00000000..6394c391 --- /dev/null +++ b/apps/server/src/common/presentation/express/middlewares/index.ts @@ -0,0 +1 @@ +export * from "./error-handler"; diff --git a/apps/server/src/common/presentation/express/validate-request.ts b/apps/server/src/common/presentation/express/validate-request.ts new file mode 100644 index 00000000..7bea12d1 --- /dev/null +++ b/apps/server/src/common/presentation/express/validate-request.ts @@ -0,0 +1,29 @@ +import { NextFunction, Request, Response } from "express"; +import httpStatus from "http-status"; +import { ZodSchema } from "zod"; +import { ApiError } from "./api-error"; + +export const validateRequest = + (schema: ZodSchema) => (req: Request, res: Response, next: NextFunction) => { + const result = schema.safeParse(req.body); + if (!result.success) { + // Construye errores detallados + const validationErrors = result.error.errors.map((err) => ({ + field: err.path.join("."), + message: err.message, + })); + + throw new ApiError({ + status: httpStatus.BAD_REQUEST, //400 + title: "Validation Error", + detail: "Algunos campos no cumplen con los criterios de validación.", + type: "https://example.com/probs/validation-error", + instance: req.originalUrl, + errors: validationErrors, + }); + } + + // Si pasa la validación, opcionalmente reescribe req.body + req.body = result.data; + next(); + }; diff --git a/apps/server/src/common/presentation/index.ts b/apps/server/src/common/presentation/index.ts new file mode 100644 index 00000000..6b5f6511 --- /dev/null +++ b/apps/server/src/common/presentation/index.ts @@ -0,0 +1 @@ +export * from "./express"; diff --git a/apps/server/src/config/database.ts b/apps/server/src/config/database.ts index ad80341f..6a652c1f 100644 --- a/apps/server/src/config/database.ts +++ b/apps/server/src/config/database.ts @@ -1,5 +1,5 @@ -import { Sequelize } from "sequelize"; import dotenv from "dotenv"; +import { Sequelize } from "sequelize"; dotenv.config(); @@ -11,16 +11,22 @@ export const sequelize = new Sequelize( host: process.env.DB_HOST as string, dialect: "mariadb", port: parseInt(process.env.DB_PORT || "3306", 10), - logging: false, - }, + logging: process.env.DB_LOGGING === "true" ? console.log : false, + pool: { + max: 10, + min: 0, + acquire: 30000, + idle: 10000, + }, + } ); export async function connectToDatabase(): Promise { try { await sequelize.authenticate(); - console.log("Conexión a MariaDB establecida correctamente."); + console.log("✅ Database connection established successfully."); } catch (error) { - console.error("Error al conectar a la base de datos:", error); + console.error("❌ Unable to connect to the database:", error); process.exit(1); } } diff --git a/apps/server/src/contexts/auth/application/auth-service.interface.ts b/apps/server/src/contexts/auth/application/auth-service.interface.ts new file mode 100644 index 00000000..15cbab22 --- /dev/null +++ b/apps/server/src/contexts/auth/application/auth-service.interface.ts @@ -0,0 +1,14 @@ +import { Result } from "@common/domain"; +import { AuthenticatedUser, EmailAddress, PasswordHash, Username } from "../domain"; + +export interface IAuthService { + /** + * 🔹 Registra un nuevo usuario en el sistema. + * Si el email ya existe, devuelve un error. + */ + registerUser( + username: Username, + email: EmailAddress, + password: PasswordHash + ): Promise>; +} diff --git a/apps/server/src/contexts/auth/application/auth.service.ts b/apps/server/src/contexts/auth/application/auth.service.ts index 06831003..60923ce8 100644 --- a/apps/server/src/contexts/auth/application/auth.service.ts +++ b/apps/server/src/contexts/auth/application/auth.service.ts @@ -1,43 +1,47 @@ -import { EmailAddress, PasswordHash, Username } from "contexts/auth/domain"; -import { authUserRepository } from "contexts/auth/infraestructure/sequelize"; -import { Result, UniqueID } from "contexts/common/domain"; +import { Result, UniqueID } from "@common/domain"; +import { ITransactionManager } from "@common/infrastructure/database"; +import { + EmailAddress, + IAuthenticatedUserRepository, + PasswordHash, + Username, +} from "@contexts/auth/domain"; +import { IAuthService } from "./auth-service.interface"; + +export class AuthService implements IAuthService { + private _respository!: IAuthenticatedUserRepository; + private readonly _transactionManager!: ITransactionManager; + + constructor(repository: IAuthenticatedUserRepository, transactionManager: ITransactionManager) { + this._respository = repository; + this._transactionManager = transactionManager; + } -export class AuthService { /** * 🔹 `registerUser` * Registra un nuevo usuario en la base de datos bajo transacción. */ - static async registerUser( - username: string, - email: string, - password: string - ): Promise> { - return await authUserRepository.executeTransaction(async (transaction) => { + async registerUser(params: { + username: Username; + email: EmailAddress; + password: PasswordHash; + }): Promise> { + return await this._transactionManager.execute(async (transaction) => { + const { username, email, password } = params; const userIdResult = UniqueID.generateNewID(); - const usernameResult = Username.create(username); - const emailResult = EmailAddress.create(email); - const passwordResult = await PasswordHash.create(password); - - const combined = Result.combine([userIdResult, usernameResult, emailResult, passwordResult]); - if (combined.isError()) { - return Result.fail(combined.error); - } // Verificar si el usuario ya existe - const userExists = await authUserRepository.userExists( - emailResult.data.getValue(), - transaction - ); + const userExists = await this._respository.userExists(email.toString(), transaction); if (userExists) { return Result.fail(new Error("Email is already registered")); } - const user = await authUserRepository.createUser( + const user = await this._respository.createUser( { - id: userIdResult.data.getValue(), - username: usernameResult.data.getValue(), - email: emailResult.data.getValue(), - password: passwordResult.data.getValue(), + id: userIdResult, + username: username, + email: email, + password: password, isActive: true, }, transaction diff --git a/apps/server/src/contexts/auth/application/index.ts b/apps/server/src/contexts/auth/application/index.ts index 2ab33e86..e2719d0c 100644 --- a/apps/server/src/contexts/auth/application/index.ts +++ b/apps/server/src/contexts/auth/application/index.ts @@ -1 +1,11 @@ -export * from "./auth.service"; +export * from "./auth-service.interface"; +import { ITransactionManager } from "@common/infrastructure/database"; +import { SequelizeTransactionManager } from "@common/infrastructure/sequelize/sequelize-transaction-manager"; +import { AuthenticatedUserRepository } from "../infraestructure"; +import { AuthService } from "./auth.service"; + +const transactionManager: ITransactionManager = new SequelizeTransactionManager(); +const authenticatedUserRepository = new AuthenticatedUserRepository(); +const authService = new AuthService(authenticatedUserRepository, transactionManager); + +export { authService }; diff --git a/apps/server/src/contexts/auth/domain/aggregates/authenticated-user.ts b/apps/server/src/contexts/auth/domain/aggregates/authenticated-user.ts new file mode 100644 index 00000000..fbed7f78 --- /dev/null +++ b/apps/server/src/contexts/auth/domain/aggregates/authenticated-user.ts @@ -0,0 +1,40 @@ +import { AggregateRoot, Result, UniqueID } from "@common/domain"; +import { UserAuthenticatedEvent } from "../events"; +import { EmailAddress, Username } from "../value-objects"; + +export interface IAuthenticatedUserProps { + username: Username; + email: EmailAddress; + roles: string[]; + token: string; +} + +export class AuthenticatedUser extends AggregateRoot { + static create(props: IAuthenticatedUserProps, id?: UniqueID): Result { + const { username, email, roles, token } = props; + + if (!id || !username || !email || !token) { + return Result.fail(new Error("Invalid authenticated user data")); + } + + const user = new AuthenticatedUser({ username, email, roles, token }, id); + + // 🔹 Disparar evento de dominio "UserAuthenticatedEvent" + user.addDomainEvent(new UserAuthenticatedEvent(id, email.toString())); + + return Result.ok(user); + } + + /** + * 🔹 Devuelve una representación lista para persistencia + */ + public toPersistenceData(): any { + return { + id: this._id.toString(), + username: this._props.username.toString(), + email: this._props.email.toString(), + roles: JSON.stringify(this._props.roles), + token: this._props.token, + }; + } +} diff --git a/apps/server/src/contexts/auth/domain/aggregates/index.ts b/apps/server/src/contexts/auth/domain/aggregates/index.ts new file mode 100644 index 00000000..864c998e --- /dev/null +++ b/apps/server/src/contexts/auth/domain/aggregates/index.ts @@ -0,0 +1 @@ +export * from "./authenticated-user"; diff --git a/apps/server/src/contexts/auth/domain/auth-user.model.ts b/apps/server/src/contexts/auth/domain/auth-user.model.ts deleted file mode 100644 index 05ba9671..00000000 --- a/apps/server/src/contexts/auth/domain/auth-user.model.ts +++ /dev/null @@ -1,47 +0,0 @@ -import { Result } from "contexts/common/domain"; -import { EmailAddress, PasswordHash, Username, UserRoles } from "./value-objects"; - -export class AuthUser { - private constructor( - public readonly id: number | null, - public readonly username: Username, - public readonly email: EmailAddress, - private password: PasswordHash, - public readonly roles: UserRoles, - public readonly isActive: boolean - ) {} - - static async create( - id: number | null, - username: string, - email: string | null, - plainPassword: string, - roles: string[], - isActive: boolean - ): Promise> { - const usernameResult = Username.create(username); - const emailResult = EmailAddress.create(email); - const passwordResult = await PasswordHash.create(plainPassword); - const rolesResult = UserRoles.create(roles); - - const combined = Result.combine([usernameResult, emailResult, passwordResult, rolesResult]); - if (combined.isError()) { - return Result.fail(combined.error); - } - - return Result.ok( - new AuthUser( - id, - usernameResult.data, - emailResult.data, - passwordResult.data, - rolesResult.data, - isActive - ) - ); - } - - async validatePassword(plainPassword: string): Promise { - return await this.password.compare(plainPassword); - } -} diff --git a/apps/server/src/contexts/auth/domain/events/index.ts b/apps/server/src/contexts/auth/domain/events/index.ts new file mode 100644 index 00000000..c228d2e1 --- /dev/null +++ b/apps/server/src/contexts/auth/domain/events/index.ts @@ -0,0 +1 @@ +export * from "./user-authenticated.event"; diff --git a/apps/server/src/contexts/auth/domain/events/user-authenticated.event.ts b/apps/server/src/contexts/auth/domain/events/user-authenticated.event.ts new file mode 100644 index 00000000..8ea3924b --- /dev/null +++ b/apps/server/src/contexts/auth/domain/events/user-authenticated.event.ts @@ -0,0 +1,13 @@ +import { IDomainEvent, UniqueID } from "@common/domain"; + +export class UserAuthenticatedEvent implements IDomainEvent { + public readonly eventName = "UserAuthenticated"; + public readonly occurredAt: Date; + + constructor( + public readonly aggregateId: UniqueID, + public readonly email: string // Email en formato string + ) { + this.occurredAt = new Date(); + } +} diff --git a/apps/server/src/contexts/auth/domain/index.ts b/apps/server/src/contexts/auth/domain/index.ts index 360112c0..2cca2244 100644 --- a/apps/server/src/contexts/auth/domain/index.ts +++ b/apps/server/src/contexts/auth/domain/index.ts @@ -1,2 +1,5 @@ -export * from "./auth-user.model"; +export * from "./aggregates/authenticated-user"; +export * from "./auth-user.entity"; +export * from "./events/user-authenticated.event"; +export * from "./repositories"; export * from "./value-objects"; diff --git a/apps/server/src/contexts/auth/domain/repositories/authenticated-user-repository.interface.ts b/apps/server/src/contexts/auth/domain/repositories/authenticated-user-repository.interface.ts new file mode 100644 index 00000000..e86d62c1 --- /dev/null +++ b/apps/server/src/contexts/auth/domain/repositories/authenticated-user-repository.interface.ts @@ -0,0 +1 @@ +export interface IAuthenticatedUserRepository {} diff --git a/apps/server/src/contexts/auth/domain/repositories/index.ts b/apps/server/src/contexts/auth/domain/repositories/index.ts new file mode 100644 index 00000000..ef546408 --- /dev/null +++ b/apps/server/src/contexts/auth/domain/repositories/index.ts @@ -0,0 +1 @@ +export * from "./authenticated-user-repository.interface"; diff --git a/apps/server/src/contexts/auth/domain/value-objects/auth-user-roles.ts b/apps/server/src/contexts/auth/domain/value-objects/auth-user-roles.ts index 7039ab97..435ba6b4 100644 --- a/apps/server/src/contexts/auth/domain/value-objects/auth-user-roles.ts +++ b/apps/server/src/contexts/auth/domain/value-objects/auth-user-roles.ts @@ -1,4 +1,4 @@ -import { Result, ValueObject } from "contexts/common/domain"; +import { Result, ValueObject } from "@common/domain"; import { z } from "zod"; const RoleSchema = z.enum(["Admin", "User", "Manager", "Editor"]); diff --git a/apps/server/src/contexts/auth/domain/value-objects/email-address.ts b/apps/server/src/contexts/auth/domain/value-objects/email-address.ts index e288ef07..ed583e37 100644 --- a/apps/server/src/contexts/auth/domain/value-objects/email-address.ts +++ b/apps/server/src/contexts/auth/domain/value-objects/email-address.ts @@ -1,4 +1,4 @@ -import { Result, ValueObject } from "contexts/common/domain"; +import { Result, ValueObject } from "@common/domain"; import { z } from "zod"; export class EmailAddress extends ValueObject { diff --git a/apps/server/src/contexts/auth/domain/value-objects/password-hash.ts b/apps/server/src/contexts/auth/domain/value-objects/password-hash.ts index de3208fb..9e9402d5 100644 --- a/apps/server/src/contexts/auth/domain/value-objects/password-hash.ts +++ b/apps/server/src/contexts/auth/domain/value-objects/password-hash.ts @@ -1,5 +1,5 @@ +import { Result, ValueObject } from "@common/domain"; import bcrypt from "bcrypt"; -import { Result, ValueObject } from "contexts/common/domain"; import { z } from "zod"; const PasswordSchema = z diff --git a/apps/server/src/contexts/auth/domain/value-objects/username.ts b/apps/server/src/contexts/auth/domain/value-objects/username.ts index d9ba26e6..9e47e740 100644 --- a/apps/server/src/contexts/auth/domain/value-objects/username.ts +++ b/apps/server/src/contexts/auth/domain/value-objects/username.ts @@ -1,4 +1,4 @@ -import { Result, ValueObject } from "contexts/common/domain"; +import { Result, ValueObject } from "@common/domain"; import { z } from "zod"; export class Username extends ValueObject { diff --git a/apps/server/src/contexts/auth/infraestructure/index.ts b/apps/server/src/contexts/auth/infraestructure/index.ts new file mode 100644 index 00000000..91f6b2d5 --- /dev/null +++ b/apps/server/src/contexts/auth/infraestructure/index.ts @@ -0,0 +1,3 @@ +export * from "./jwt.helper"; +export * from "./sequelize"; +12; diff --git a/apps/server/src/contexts/auth/infraestructure/jwt.helper.ts b/apps/server/src/contexts/auth/infraestructure/jwt.helper.ts new file mode 100644 index 00000000..7f8b09af --- /dev/null +++ b/apps/server/src/contexts/auth/infraestructure/jwt.helper.ts @@ -0,0 +1,13 @@ +import jwt from "jsonwebtoken"; + +const SECRET_KEY = process.env.JWT_SECRET || "supersecretkey"; + +export class JwtHelper { + static generateToken(payload: object, expiresIn = "1h"): string { + return jwt.sign(payload, SECRET_KEY, { expiresIn }); + } + + static verifyToken(token: string): any { + return jwt.verify(token, SECRET_KEY); + } +} diff --git a/apps/server/src/contexts/auth/infraestructure/mappers/authenticated-user-mapper.interface.ts b/apps/server/src/contexts/auth/infraestructure/mappers/authenticated-user-mapper.interface.ts new file mode 100644 index 00000000..0bbc51f4 --- /dev/null +++ b/apps/server/src/contexts/auth/infraestructure/mappers/authenticated-user-mapper.interface.ts @@ -0,0 +1,14 @@ +import { Result } from "@common/domain"; +import { AuthenticatedUser } from "@contexts/auth/domain"; + +export interface IAuthenticatedUserMapper { + /** + * 🔹 Convierte una entidad de la base de datos en un agregado de dominio `AuthenticatedUser` + */ + toDomain(entity: any): Result; + + /** + * 🔹 Convierte un agregado `AuthenticatedUser` en un objeto listo para persistencia + */ + toPersistence(aggregate: AuthenticatedUser): any; +} diff --git a/apps/server/src/contexts/auth/infraestructure/mappers/authenticated-user.mapper.ts b/apps/server/src/contexts/auth/infraestructure/mappers/authenticated-user.mapper.ts new file mode 100644 index 00000000..81dbbb8e --- /dev/null +++ b/apps/server/src/contexts/auth/infraestructure/mappers/authenticated-user.mapper.ts @@ -0,0 +1,42 @@ +import { Result, UniqueID } from "@common/domain"; +import { AuthenticatedUser, EmailAddress, Username } from "@contexts/auth/domain"; + +export class AuthenticatedUserMapper { + /** + * 🔹 Convierte una entidad de la base de datos en un agregado de dominio `AuthenticatedUser` + */ + static toDomain(entity: any): Result { + if (!entity) { + return Result.fail(new Error("Entity not found")); + } + + // Crear Value Objects asegurando que sean válidos + const uniqueIdResult = UniqueID.create(entity.id); + const usernameResult = Username.create(entity.username); + const emailResult = EmailAddress.create(entity.email); + + // Validar que no haya errores en la creación de los Value Objects + const okOrError = Result.combine([uniqueIdResult, usernameResult, emailResult]); + if (okOrError.isError()) { + return okOrError; + } + + // Crear el agregado de dominio + return AuthenticatedUser.create( + { + username: usernameResult.data!, + email: emailResult.data!, + roles: entity.roles || [], + token: entity.token, + }, + uniqueIdResult.data! + ); + } + + /** + * 🔹 Convierte un agregado `AuthenticatedUser` en un objeto listo para persistencia + */ + static toPersistence(authenticatedUser: AuthenticatedUser): any { + return authenticatedUser.toPersistenceData(); + } +} diff --git a/apps/server/src/contexts/auth/infraestructure/mappers/index.ts b/apps/server/src/contexts/auth/infraestructure/mappers/index.ts new file mode 100644 index 00000000..469a1b91 --- /dev/null +++ b/apps/server/src/contexts/auth/infraestructure/mappers/index.ts @@ -0,0 +1,2 @@ +export * from "./authenticated-user-mapper.interface"; +export * from "./authenticated-user.mapper"; diff --git a/apps/server/src/contexts/auth/infraestructure/sequelize/auth-user-repository.spec.ts b/apps/server/src/contexts/auth/infraestructure/sequelize/auth-user-repository.spec.ts deleted file mode 100644 index 79588368..00000000 --- a/apps/server/src/contexts/auth/infraestructure/sequelize/auth-user-repository.spec.ts +++ /dev/null @@ -1,72 +0,0 @@ -import { authUserRepository } from "./auth-user.repository"; - -describe("authUserRepository", () => { - beforeEach(() => { - // Resetear la base de datos antes de cada prueba - }); - - it("should create a user successfully", async () => { - const result = await authUserRepository.createUser({ - id: "user-uuid", - username: "testUser", - email: "user@example.com", - password: "hashed-password", - isActive: true, - }); - - expect(result).toHaveProperty("id"); - expect(result.email).toBe("user@example.com"); - }); - - /*it("should find a user by email", async () => { - await authUserRepository.createUser({ - id: "user-uuid", - username: "testUser", - email: "user@example.com", - password: "hashed-password", - isActive: true, - }); - - const user = await authUserRepository.findByEmail("user@example.com"); - expect(user.isOk()).toBe(true); - expect(user.data?.getUserID()).toBe("user-uuid"); - }); - - it("should return an error when user is not found", async () => { - const user = await authUserRepository.findByEmail("notfound@example.com"); - expect(user.isError()).toBe(true); - expect(user.error.message).toBe("User not found"); - });*/ - - it("should check if a user exists", async () => { - await authUserRepository.createUser({ - id: "user-uuid", - username: "testUser", - email: "exists@example.com", - password: "hashed-password", - isActive: true, - }); - - expect(await authUserRepository.userExists("exists@example.com")).toBe(true); - expect(await authUserRepository.userExists("notfound@example.com")).toBe(false); - }); - - it("should count active users", async () => { - await authUserRepository.createUser({ - id: "1", - username: "user1", - email: "user1@example.com", - password: "pass", - isActive: true, - }); - await authUserRepository.createUser({ - id: "2", - username: "user2", - email: "user2@example.com", - password: "pass", - isActive: false, - }); - - expect(await authUserRepository.countActiveUsers()).toBe(1); - }); -}); diff --git a/apps/server/src/contexts/auth/infraestructure/sequelize/auth-user.repository.ts b/apps/server/src/contexts/auth/infraestructure/sequelize/auth-user.repository.ts deleted file mode 100644 index 23258b03..00000000 --- a/apps/server/src/contexts/auth/infraestructure/sequelize/auth-user.repository.ts +++ /dev/null @@ -1,42 +0,0 @@ -import { SequelizeRepository } from "contexts/common/infraestructure"; -import { Transaction } from "sequelize"; -import { AuthUserModel } from "./auth-user.model"; - -class AuthUserRepository extends SequelizeRepository { - constructor() { - super(AuthUserModel); - } - - async createUser( - data: { id: string; username: string; email: string; password: string; isActive: boolean }, - transaction?: Transaction - ): Promise { - return await this.create(data, transaction); - } - - async findAllUsers(): Promise { - return await this.findAll(); - } - - async isUserAssociatedWithCompany( - userId: string, - companyId: string, - transaction?: Transaction - ): Promise { - const association = await AuthUserModel.findOne({ - where: { id: userId, companyId }, - transaction, - }); - return !!association; - } - - async userExists(email: string, transaction?: Transaction): Promise { - return await this.exists("email", email, transaction); - } - - async countActiveUsers(transaction?: Transaction): Promise { - return await this.count({ where: { isActive: true }, transaction }); - } -} - -export const authUserRepository = new AuthUserRepository(); diff --git a/apps/server/src/contexts/auth/infraestructure/sequelize/authenticated-user.repository.ts b/apps/server/src/contexts/auth/infraestructure/sequelize/authenticated-user.repository.ts new file mode 100644 index 00000000..d6834dbe --- /dev/null +++ b/apps/server/src/contexts/auth/infraestructure/sequelize/authenticated-user.repository.ts @@ -0,0 +1,76 @@ +import { Result, UniqueID } from "@common/domain"; +import { SequelizeRepository } from "@common/infrastructure"; +import { + AuthenticatedUser, + EmailAddress, + IAuthenticatedUserRepository, + Username, +} from "@contexts/auth/domain"; +import { Transaction } from "sequelize"; +import { IAuthenticatedUserMapper } from "../mappers"; +import { AuthUserModel } from "./auth-user.model"; + +export class AuthenticatedUserRepository + extends SequelizeRepository + implements IAuthenticatedUserRepository +{ + private readonly _mapper!: IAuthenticatedUserMapper; + + constructor(mapper: IAuthenticatedUserMapper) { + super(); + this._mapper = mapper; + } + + protected async _findById(id: string, transaction?: Transaction): Promise { + return await AuthUserModel.findByPk(id, { transaction }); + } + + async create(user: AuthenticatedUser, transaction?: Transaction): Promise> { + const persistenceData = this._mapper.toPersistence(user); + await AuthUserModel.create(persistenceData, { transaction }); + return Result.ok(); + } + protected async _update(id: string, data: any, transaction?: Transaction): Promise { + const [updated] = await AuthUserModel.update(data, { where: { id }, transaction }); + return updated > 0; + } + + protected async _delete(id: string, transaction?: Transaction): Promise { + const deleted = await AuthUserModel.destroy({ where: { id }, transaction }); + return deleted > 0; + } + + protected toDomain(entity: any): Result { + if (!entity) { + return Result.fail(new Error("Entity not found")); + } + + // 🔹 Crear los Value Objects manejando errores correctamente + const idOrError = UniqueID.create(entity.id); + const usernameOrError = Username.create(entity.username); + const emailOrError = EmailAddress.create(entity.email); + + // 🔹 Si algún Value Object es inválido, devolver el error inmediatamente + const combinedResults = [idOrError, usernameOrError, emailOrError]; + for (const result of combinedResults) { + if (result.isError()) { + return Result.fail(result.error); + } + } + + // 🔹 Crear las propiedades validadas del agregado + const props = { + username: usernameOrError.data!, + email: emailOrError.data!, + roles: entity.roles || [], + token: entity.token, + }; + + // 🔹 Crear el agregado manejando errores + return AuthenticatedUser.create(props, idOrError.data!); + } + + protected toPersistence(authenticatedUser: AuthenticatedUser): any { + this._mapper.toPersistence(authenticatedUser); + } +} diff --git a/apps/server/src/contexts/auth/infraestructure/sequelize/index.ts b/apps/server/src/contexts/auth/infraestructure/sequelize/index.ts index 4d692f7c..ef5048db 100644 --- a/apps/server/src/contexts/auth/infraestructure/sequelize/index.ts +++ b/apps/server/src/contexts/auth/infraestructure/sequelize/index.ts @@ -1,2 +1,2 @@ export * from "./auth-user.model"; -export * from "./auth-user.repository"; +export * from "./authenticated-user.repository"; diff --git a/apps/server/src/contexts/auth/presentation/auth.controller.ts b/apps/server/src/contexts/auth/presentation/auth.controller.ts new file mode 100644 index 00000000..f795fea2 --- /dev/null +++ b/apps/server/src/contexts/auth/presentation/auth.controller.ts @@ -0,0 +1,57 @@ +import { ExpressController } from "@common/presentation/express/express-controller"; +import { Request, Response } from "express"; +import { AuthService } from "../application"; +import { EmailAddress, PasswordHash, Username } from "../domain"; +import { IRegisterUserRequestDTO } from "./dto"; + +class AuthController extends ExpressController { + protected async executeImpl(): Promise { + this.clientError("Method not implemented"); + } + + async register(req: Request, res: Response) { + const { username, email, password }: IRegisterUserRequestDTO = req.body; + + const emailVO = EmailAddress.create(email); + const usernameVO = Username.create(username); + const passwordVO = await PasswordHash.create(password); + + const combined = [emailVO, usernameVO, passwordVO].every((r) => r.isOk()); + + if (!combined) { + return this.clientError("Invalid input data"); + } + + const result = await AuthService.registerUser({ + username: usernameVO.data, + email: emailVO.data, + password: passwordVO.data, + }); + + return result.isError() + ? this.clientError(result.error.message) + : this.created({ userId: result.data.userId }); + } + + async login(req: Request, res: Response) { + const { email, password } = req.body; + const result = await AuthService.login(email, password); + + return result.isError() ? this.unauthorizedError(result.error.message) : this.ok(result.data); + } + + async selectCompany(req: Request, res: Response) { + const userId = (req as any).user.userId; + const { companyId } = req.body; + + const result = await AuthService.selectCompany(userId, companyId); + + return result.isError() ? this.forbiddenError(result.error.message) : this.ok(result.data); + } + + async logout(req: Request, res: Response) { + return this.ok(AuthService.logout()); + } +} + +export const authController = new AuthController(); diff --git a/apps/server/src/contexts/auth/presentation/auth.routes.ts b/apps/server/src/contexts/auth/presentation/auth.routes.ts new file mode 100644 index 00000000..a4051334 --- /dev/null +++ b/apps/server/src/contexts/auth/presentation/auth.routes.ts @@ -0,0 +1,77 @@ +import { authController } from "./auth.controller"; + +import { validateRequest } from "@common/presentation"; +import { NextFunction, Request, Response, Router } from "express"; +import { registerController } from "./controllers"; +import { RegisterUserSchema } from "./dto"; + +const loggerMiddleware = () => (req: Request, res: Response, next: NextFunction) => { + console.log(`${req.method} ${req.path}`); + next(); +}; + +export const authRouter = (appRouter: Router) => { + const authRoutes: Router = Router({ mergeParams: true }); + + /** + * @api {post} /api/auth/register Register a new user + * @apiName RegisterUser + * @apiGroup Authentication + * @apiVersion 1.0.0 + * + * @apiBody {String} username User's unique username. + * @apiBody {String} email User's email address. + * @apiBody {String} password User's password (minimum 8 characters). + * + * @apiSuccess (201) {String} userId The unique ID of the created user. + * + * @apiError (400) {String} message Error message. + */ + authRoutes.post("/register", validateRequest(RegisterUserSchema), registerController.execute); + + /** + * @api {post} /api/auth/login Authenticate a user + * @apiName LoginUser + * @apiGroup Authentication + * @apiVersion 1.0.0 + * + * @apiBody {String} email User's email address. + * @apiBody {String} password User's password. + * + * @apiSuccess (200) {String} token JWT authentication token. + * @apiSuccess (200) {String} userId The unique ID of the authenticated user. + * + * @apiError (401) {String} message Invalid email or password. + */ + authRoutes.post("/login", authController.login); + + /** + * @api {post} /api/auth/select-company Select an active company + * @apiName SelectCompany + * @apiGroup Authentication + * @apiVersion 1.0.0 + * + * @apiHeader {String} Authorization Bearer token. + * + * @apiBody {String} companyId The ID of the company to select. + * + * @apiSuccess (200) {String} message Success message. + * + * @apiError (403) {String} message Unauthorized or invalid company selection. + */ + authRoutes.post("/select-company", authMiddleware, authController.selectCompany); + + /** + * @api {post} /api/auth/logout Logout user + * @apiName LogoutUser + * @apiGroup Authentication + * @apiVersion 1.0.0 + * + * @apiHeader {String} Authorization Bearer token. + * + * @apiSuccess (200) {String} message Success message. + */ + authRoutes.post("/logout", authMiddleware, authController.logout); + + appRouter.use("/auth", authRoutes); +}; diff --git a/apps/server/src/contexts/auth/presentation/controllers/index.ts b/apps/server/src/contexts/auth/presentation/controllers/index.ts new file mode 100644 index 00000000..92dc9747 --- /dev/null +++ b/apps/server/src/contexts/auth/presentation/controllers/index.ts @@ -0,0 +1 @@ +export * from "./register.controller"; diff --git a/apps/server/src/contexts/auth/presentation/controllers/register.controller.ts b/apps/server/src/contexts/auth/presentation/controllers/register.controller.ts new file mode 100644 index 00000000..a7448736 --- /dev/null +++ b/apps/server/src/contexts/auth/presentation/controllers/register.controller.ts @@ -0,0 +1,35 @@ +import { ExpressController } from "@common/presentation"; +import { authService } from "@contexts/auth/application"; +import { AuthService } from "@contexts/auth/application/auth.service"; +import { EmailAddress, PasswordHash, Username } from "@contexts/auth/domain"; + +class RegisterController extends ExpressController { + private readonly _authService!: AuthService; + + constructor(authService: AuthService) { + super(); + this._authService = authService; + } + + async executeImpl() { + const emailVO = EmailAddress.create(this.req.body.email); + const usernameVO = Username.create(this.req.body.username); + const passwordVO = await PasswordHash.create(this.req.body.password); + + if ([emailVO, usernameVO, passwordVO].some((r) => r.isError())) { + return this.clientError("Invalid input data"); + } + + const result = await this._authService.registerUser({ + username: usernameVO.data, + email: emailVO.data, + password: passwordVO.data, + }); + + return result.isError() + ? this.clientError(result.error.message) + : this.created({ userId: result.data.userId }); + } +} + +export const registerController = new RegisterController(authService); diff --git a/apps/server/src/contexts/auth/presentation/dto/auth.request.dto.ts b/apps/server/src/contexts/auth/presentation/dto/auth.request.dto.ts new file mode 100644 index 00000000..d2d72b92 --- /dev/null +++ b/apps/server/src/contexts/auth/presentation/dto/auth.request.dto.ts @@ -0,0 +1,14 @@ +export interface IRegisterUserRequestDTO { + username: string; + email: string; + password: string; +} + +export interface ILoginUserRequestDTO { + email: string; + password: string; +} + +export interface ISelectCompanyRequestDTO { + companyId: string; +} diff --git a/apps/server/src/contexts/auth/presentation/dto/auth.response.dto.ts b/apps/server/src/contexts/auth/presentation/dto/auth.response.dto.ts new file mode 100644 index 00000000..db8398a0 --- /dev/null +++ b/apps/server/src/contexts/auth/presentation/dto/auth.response.dto.ts @@ -0,0 +1,31 @@ +export interface IRegisterUserDTO { + username: string; + email: string; + password: string; +} + +export interface ILoginUserDTO { + email: string; + password: string; +} + +export interface ISelectCompanyDTO { + companyId: string; +} + +export interface IRegisterUserResponseDTO { + userId: string; +} + +export interface ILoginUserResponseDTO { + token: string; + userId: string; +} + +export interface ISelectCompanyResponseDTO { + message: string; +} + +export interface ILogoutResponseDTO { + message: string; +} diff --git a/apps/server/src/contexts/auth/presentation/dto/auth.validation.dto.ts b/apps/server/src/contexts/auth/presentation/dto/auth.validation.dto.ts new file mode 100644 index 00000000..2bf71243 --- /dev/null +++ b/apps/server/src/contexts/auth/presentation/dto/auth.validation.dto.ts @@ -0,0 +1,16 @@ +import { z } from "zod"; + +export const RegisterUserSchema = z.object({ + username: z.string().min(3, "Username must be at least 3 characters long"), + email: z.string().email("Invalid email format"), + password: z.string().min(8, "Password must be at least 8 characters long"), +}); + +export const LoginUserSchema = z.object({ + email: z.string().email("Invalid email format"), + password: z.string().min(8, "Password must be at least 8 characters long"), +}); + +export const SelectCompanySchema = z.object({ + companyId: z.string().min(1, "Company ID is required"), +}); diff --git a/apps/server/src/contexts/auth/presentation/dto/index.ts b/apps/server/src/contexts/auth/presentation/dto/index.ts new file mode 100644 index 00000000..02efe6bf --- /dev/null +++ b/apps/server/src/contexts/auth/presentation/dto/index.ts @@ -0,0 +1,3 @@ +export * from "./auth.request.dto"; +export * from "./auth.response.dto"; +export * from "./auth.validation.dto"; diff --git a/apps/server/src/contexts/auth/presentation/index.ts b/apps/server/src/contexts/auth/presentation/index.ts new file mode 100644 index 00000000..fd010ada --- /dev/null +++ b/apps/server/src/contexts/auth/presentation/index.ts @@ -0,0 +1 @@ +export * from "./auth.controller"; diff --git a/apps/server/src/contexts/common/infraestructure/sequelize/sequelize-repository.ts b/apps/server/src/contexts/common/infraestructure/sequelize/sequelize-repository.ts deleted file mode 100644 index fe8d7692..00000000 --- a/apps/server/src/contexts/common/infraestructure/sequelize/sequelize-repository.ts +++ /dev/null @@ -1,68 +0,0 @@ -import { sequelize } from "@config/database"; -import { FindOptions, Model, ModelDefined, Transaction } from "sequelize"; - -export abstract class SequelizeRepository { - protected readonly model: ModelDefined; - - protected constructor(model: ModelDefined) { - this.model = model; - } - - async findById(id: string, transaction?: Transaction): Promise { - return await this.model.findByPk(id, { transaction }); - } - - async findOneByField(field: string, value: any, transaction?: Transaction): Promise { - return await this.model.findOne({ where: { [field]: value }, transaction }); - } - - async findAll(filter?: FindOptions, transaction?: Transaction): Promise { - return await this.model.findAll({ ...filter, transaction }); - } - - async create(data: Partial, transaction?: Transaction): Promise { - return await this.model.create(data as any, { transaction }); - } - - async update(id: string, data: Partial, transaction?: Transaction): Promise<[number, T[]]> { - return await this.model.update(data as any, { where: { id }, returning: true, transaction }); - } - - async delete(id: string, transaction?: Transaction): Promise { - const deleted = await this.model.destroy({ where: { id }, transaction }); - return deleted > 0; - } - - /** - * 🔹 `exists` - * Verifica si un registro existe en la base de datos basado en un campo y valor. - */ - async exists(field: string, value: any, transaction?: Transaction): Promise { - const count = await this.model.count({ where: { [field]: value }, transaction }); - return count > 0; - } - - /** - * 🔹 `count` - * Cuenta el número de registros que cumplen con una condición. - */ - async count(filter?: FindOptions, transaction?: Transaction): Promise { - return await this.model.count({ ...filter, transaction }); - } - - /** - * 🔹 `executeTransaction` - * Ejecuta una función dentro de una transacción de Sequelize. - */ - async executeTransaction(operation: (transaction: Transaction) => Promise): Promise { - const transaction = await sequelize.transaction(); - try { - const result = await operation(transaction); - await transaction.commit(); - return result; - } catch (error) { - await transaction.rollback(); - throw error; - } - } -} diff --git a/apps/server/src/index.ts b/apps/server/src/index.ts index 3c36556f..79053f50 100644 --- a/apps/server/src/index.ts +++ b/apps/server/src/index.ts @@ -1,5 +1,5 @@ +import { createApp } from "./app"; import { connectToDatabase } from "./config/database"; -import { createApp } from "./infrastructure/app"; const PORT = process.env.PORT || 3000; diff --git a/apps/server/src/infrastructure/express/auth.routes.ts b/apps/server/src/infrastructure/express/auth.routes.ts deleted file mode 100644 index 435d959f..00000000 --- a/apps/server/src/infrastructure/express/auth.routes.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { Router } from "express"; -import * as authController from "../../application/auth/auth.controller"; - -export const authRoutes = () => { - const router = Router(); - - router.post("/register", authController.register); - router.post("/login", authController.login); - router.post("/select-company", authController.selectCompany); - router.post("/logout", authController.logout); - - return router; -}; diff --git a/apps/server/src/infrastructure/express/index.ts b/apps/server/src/infrastructure/express/index.ts deleted file mode 100644 index 0a2a2585..00000000 --- a/apps/server/src/infrastructure/express/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from "./auth.routes"; diff --git a/apps/server/src/infrastructure/index.ts b/apps/server/src/infrastructure/index.ts deleted file mode 100644 index ac5307de..00000000 --- a/apps/server/src/infrastructure/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from "./app"; diff --git a/apps/server/src/routes/index.ts b/apps/server/src/routes/index.ts new file mode 100644 index 00000000..df87e0a6 --- /dev/null +++ b/apps/server/src/routes/index.ts @@ -0,0 +1 @@ +export * from "./v1.routes"; diff --git a/apps/server/src/routes/v1.routes.ts b/apps/server/src/routes/v1.routes.ts new file mode 100644 index 00000000..ad826ade --- /dev/null +++ b/apps/server/src/routes/v1.routes.ts @@ -0,0 +1,21 @@ +import { Router } from "express"; +import { authRouter } from "../contexts/auth/presentation/auth.routes"; + +export const v1Routes = () => { + const routes = Router({ mergeParams: true }); + + routes.get("/hello", (req, res) => { + res.send("Hello world!"); + }); + + routes.use((req, res, next) => { + console.log( + `[${new Date().toLocaleTimeString()}] Incoming request ${req.method} to ${req.path}` + ); + next(); + }); + + authRouter(routes); + + return routes; +}; diff --git a/apps/server/tsconfig.json b/apps/server/tsconfig.json index 12d8bc4f..30c5e95a 100644 --- a/apps/server/tsconfig.json +++ b/apps/server/tsconfig.json @@ -10,6 +10,7 @@ "paths": { "@shared/*": ["../../packages/shared/*"], "@common/*": ["common/*"], + "@contexts/*": ["contexts/*"], "@config/*": ["config/*"] } }, diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index aa4c716b..44c5e029 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -38,6 +38,9 @@ importers: helmet: specifier: ^8.0.0 version: 8.0.0 + http-status: + specifier: ^2.1.0 + version: 2.1.0 jsonwebtoken: specifier: ^9.0.2 version: 9.0.2 @@ -96,6 +99,9 @@ importers: '@types/node': specifier: ^22.10.7 version: 22.12.0 + '@types/passport': + specifier: ^1.0.17 + version: 1.0.17 '@types/passport-jwt': specifier: ^4.0.1 version: 4.0.1 @@ -2092,6 +2098,10 @@ packages: resolution: {integrity: sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==} engines: {node: '>= 0.8'} + http-status@2.1.0: + resolution: {integrity: sha512-O5kPr7AW7wYd/BBiOezTwnVAnmSNFY+J7hlZD2X5IOxVBetjcHAiTXhzj0gMrnojQlwy+UT1/Y3H3vJ3UlmvLA==} + engines: {node: '>= 0.4.0'} + https-proxy-agent@5.0.1: resolution: {integrity: sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==} engines: {node: '>= 6'} @@ -5860,6 +5870,8 @@ snapshots: statuses: 2.0.1 toidentifier: 1.0.1 + http-status@2.1.0: {} + https-proxy-agent@5.0.1: dependencies: agent-base: 6.0.2