From 9264739af94c55660042fc7e3f5e87c5b0cb69d2 Mon Sep 17 00:00:00 2001 From: David Arranz Date: Wed, 15 May 2024 21:56:22 +0200 Subject: [PATCH] . --- server/package.json | 15 +- server/src/config/environments/development.ts | 9 + server/src/config/environments/production.ts | 7 + .../contexts/auth/application/LoginUseCase.ts | 94 +++++++++ .../contexts/auth/application/authServices.ts | 0 server/src/contexts/auth/application/index.ts | 1 + .../src/contexts/auth/domain/entities/User.ts | 95 +++++++++ .../contexts/auth/domain/entities/index.ts | 1 + server/src/contexts/auth/domain/index.ts | 1 + .../repository/AuthRepository.interface.ts | 8 + .../contexts/auth/domain/repository/index.ts | 1 + server/src/contexts/auth/index.ts | 1 + .../auth/infrastructure/Auth.context.ts | 35 ++++ .../auth/infrastructure/Auth.repository.ts | 86 ++++++++ .../controllers/AuthenticateController.ts | 67 +++++++ .../express/controllers/LoginController.ts | 108 ++++++++++ .../express/controllers/index.ts | 20 ++ .../auth/infrastructure/express/index.ts | 3 + .../express/passport/authenticate.ts | 34 ++++ .../express/passport/configurePassportAuth.ts | 11 ++ .../express/passport/emailStrategy.ts | 69 +++++++ .../infrastructure/express/passport/index.ts | 2 + .../express/passport/jwtStrategy.ts | 27 +++ .../express/passport/passport.bak | 184 ++++++++++++++++++ .../auth/infrastructure/express/routes.ts | 88 +++++++++ .../src/contexts/auth/infrastructure/index.ts | 2 + .../auth/infrastructure/mappers/index.ts | 1 + .../infrastructure/mappers/user.mapper.ts | 52 +++++ .../auth/infrastructure/sequelize/index.ts | 1 + .../infrastructure/sequelize/user.model.ts | 61 ++++++ server/src/contexts/catalog/index.ts | 1 + .../infrastructure/Catalog.repository.ts | 25 ++- .../infrastructure/express/catalogRoutes.ts | 40 ---- .../listArticles/ListArticlesController.ts | 4 +- .../express/controllers/listArticles/index.ts | 22 +-- .../catalog/infrastructure/express/index.ts | 2 +- .../catalog/infrastructure/express/routes.ts | 26 +++ .../contexts/catalog/infrastructure/index.ts | 2 + .../infrastructure/sequelize/article.model.ts | 2 +- .../application/useCases/UseCaseError.ts | 57 +----- .../infrastructure/Controller.interface.ts | 1 + .../express/ExpressController.ts | 17 +- .../express/PassportStrategyController.ts | 3 + .../common/infrastructure/express/index.ts | 4 +- .../infrastructure/express/middlewares.ts | 36 ++++ .../contexts/common/infrastructure/index.ts | 1 + server/src/infrastructure/express/api/v1.ts | 49 ++++- server/src/infrastructure/express/app.ts | 14 +- server/src/infrastructure/http/server.ts | 20 +- .../contexts/auth/application/User.service.ts | 10 + .../auth/application/dto/ILogin.dto.ts | 24 +++ .../application/dto/ILogin_Response.dto.ts | 3 + .../contexts/auth/application/dto/index.ts | 2 + shared/lib/contexts/auth/application/index.ts | 2 + shared/lib/contexts/auth/index.ts | 1 + shared/lib/contexts/index.ts | 1 + 56 files changed, 1298 insertions(+), 155 deletions(-) create mode 100644 server/src/contexts/auth/application/LoginUseCase.ts create mode 100644 server/src/contexts/auth/application/authServices.ts create mode 100644 server/src/contexts/auth/application/index.ts create mode 100644 server/src/contexts/auth/domain/entities/User.ts create mode 100644 server/src/contexts/auth/domain/entities/index.ts create mode 100644 server/src/contexts/auth/domain/index.ts create mode 100644 server/src/contexts/auth/domain/repository/AuthRepository.interface.ts create mode 100644 server/src/contexts/auth/domain/repository/index.ts create mode 100644 server/src/contexts/auth/index.ts create mode 100644 server/src/contexts/auth/infrastructure/Auth.context.ts create mode 100644 server/src/contexts/auth/infrastructure/Auth.repository.ts create mode 100644 server/src/contexts/auth/infrastructure/express/controllers/AuthenticateController.ts create mode 100644 server/src/contexts/auth/infrastructure/express/controllers/LoginController.ts create mode 100644 server/src/contexts/auth/infrastructure/express/controllers/index.ts create mode 100644 server/src/contexts/auth/infrastructure/express/index.ts create mode 100644 server/src/contexts/auth/infrastructure/express/passport/authenticate.ts create mode 100644 server/src/contexts/auth/infrastructure/express/passport/configurePassportAuth.ts create mode 100644 server/src/contexts/auth/infrastructure/express/passport/emailStrategy.ts create mode 100644 server/src/contexts/auth/infrastructure/express/passport/index.ts create mode 100644 server/src/contexts/auth/infrastructure/express/passport/jwtStrategy.ts create mode 100644 server/src/contexts/auth/infrastructure/express/passport/passport.bak create mode 100644 server/src/contexts/auth/infrastructure/express/routes.ts create mode 100644 server/src/contexts/auth/infrastructure/index.ts create mode 100644 server/src/contexts/auth/infrastructure/mappers/index.ts create mode 100644 server/src/contexts/auth/infrastructure/mappers/user.mapper.ts create mode 100644 server/src/contexts/auth/infrastructure/sequelize/index.ts create mode 100644 server/src/contexts/auth/infrastructure/sequelize/user.model.ts create mode 100644 server/src/contexts/catalog/index.ts delete mode 100644 server/src/contexts/catalog/infrastructure/express/catalogRoutes.ts create mode 100644 server/src/contexts/catalog/infrastructure/express/routes.ts create mode 100644 server/src/contexts/common/infrastructure/Controller.interface.ts create mode 100644 server/src/contexts/common/infrastructure/express/PassportStrategyController.ts create mode 100644 server/src/contexts/common/infrastructure/express/middlewares.ts create mode 100644 shared/lib/contexts/auth/application/User.service.ts create mode 100644 shared/lib/contexts/auth/application/dto/ILogin.dto.ts create mode 100644 shared/lib/contexts/auth/application/dto/ILogin_Response.dto.ts create mode 100644 shared/lib/contexts/auth/application/dto/index.ts create mode 100644 shared/lib/contexts/auth/application/index.ts create mode 100644 shared/lib/contexts/auth/index.ts diff --git a/server/package.json b/server/package.json index 5165909..d688cca 100644 --- a/server/package.json +++ b/server/package.json @@ -16,15 +16,21 @@ "author": "Rodax Software ", "license": "ISC", "devDependencies": { + "@types/bcryptjs": "^2.4.6", "@types/cors": "^2.8.13", "@types/dinero.js": "^1.9.1", - "@types/express": "^4.17.13", + "@types/express": "^4.17.21", + "@types/express-session": "^1.18.0", "@types/glob": "^8.1.0", "@types/jest": "^29.5.6", + "@types/jsonwebtoken": "^9.0.6", "@types/luxon": "^3.3.1", "@types/module-alias": "^2.0.1", "@types/morgan": "^1.9.4", - "@types/node": "^20.4.9", + "@types/node": "^20.12.11", + "@types/passport": "^1.0.16", + "@types/passport-jwt": "^4.0.1", + "@types/passport-local": "^1.0.38", "@types/response-time": "^2.3.5", "@types/supertest": "^2.0.11", "@types/validator": "^13.11.1", @@ -50,6 +56,7 @@ "dependencies": { "@joi/date": "^2.1.0", "@reis/joi-luxon": "^3.0.0", + "bcryptjs": "^2.4.3", "cls-rtracer": "^2.6.3", "cors": "^2.8.5", "cross-env": "5.0.5", @@ -59,11 +66,15 @@ "helmet": "^7.0.0", "joi": "^17.12.3", "joi-phone-number": "^5.1.1", + "jsonwebtoken": "^9.0.2", "lodash": "^4.17.21", "luxon": "^3.4.0", "moment": "^2.29.4", "morgan": "^1.10.0", "mysql2": "^3.6.0", + "passport": "^0.7.0", + "passport-jwt": "^4.0.1", + "passport-local": "^1.0.0", "path": "^0.12.7", "remove": "^0.1.5", "response-time": "^2.3.2", diff --git a/server/src/config/environments/development.ts b/server/src/config/environments/development.ts index b839828..77f379f 100644 --- a/server/src/config/environments/development.ts +++ b/server/src/config/environments/development.ts @@ -1,4 +1,13 @@ module.exports = { + jwt: { + secret_key: + "9d6c903873c341816995a8be0355c6f0d6d471fc6aedacf50790e9b1e49c45b3", + refresh_secret_key: + "3972dc40c69327b65352ed097419213b0b75561169dba562410b85660bb1f305", + token_expiration: "15m", + refresh_token_expiration: "7d", + }, + database: { username: "rodax", password: "rodax", diff --git a/server/src/config/environments/production.ts b/server/src/config/environments/production.ts index bd67a85..1fbb907 100644 --- a/server/src/config/environments/production.ts +++ b/server/src/config/environments/production.ts @@ -1,4 +1,11 @@ module.exports = { + jwt: { + secret_key: "", + refresh_secret_key: "", + token_expiration: "15m", + refresh_token_expiration: "7d", + }, + database: { username: "uecko", password: "", diff --git a/server/src/contexts/auth/application/LoginUseCase.ts b/server/src/contexts/auth/application/LoginUseCase.ts new file mode 100644 index 0000000..a3317ad --- /dev/null +++ b/server/src/contexts/auth/application/LoginUseCase.ts @@ -0,0 +1,94 @@ +import { + IUseCase, + IUseCaseError, + UseCaseError, + handleUseCaseError, +} from "@/contexts/common/application"; +import { IRepositoryManager } from "@/contexts/common/domain"; +import { IInfrastructureError } from "@/contexts/common/infrastructure"; +import { ISequelizeAdapter } from "@/contexts/common/infrastructure/sequelize"; +import { + Email, + ILogin_DTO, + Result, + ensureUserEmailIsValid, +} from "@shared/contexts"; +import { User } from "../domain"; +import { IAuthRepository } from "../domain/repository"; + +export type LoginResponseOrError = + | Result + | Result; + +export class LoginUseCase + implements IUseCase> +{ + private _adapter: ISequelizeAdapter; + private _repositoryManager: IRepositoryManager; + + constructor(props: { + adapter: ISequelizeAdapter; + repositoryManager: IRepositoryManager; + }) { + this._adapter = props.adapter; + this._repositoryManager = props.repositoryManager; + } + + private getRepositoryByName(name: string) { + return this._repositoryManager.getRepository(name); + } + + async execute(request: ILogin_DTO): Promise { + const { email, password } = request; + + // Validaciones de datos + + const emailOrError = ensureUserEmailIsValid(email); + if (emailOrError.isFailure) { + return Result.fail( + handleUseCaseError( + UseCaseError.INVALID_INPUT_DATA, + "Email or password is not valid", + emailOrError.error, + ), + ); + } + + // Crear auth + try { + const user = await this.findUserEmail(emailOrError.object); + if (user === null || !user.verifyPassword(password)) { + return Result.fail( + handleUseCaseError( + UseCaseError.INVALID_INPUT_DATA, + "Email or password is not valid", + ), + ); + } + return Result.ok(user); + } catch (error: unknown) { + const _error = error as IInfrastructureError; + return Result.fail( + handleUseCaseError( + UseCaseError.REPOSITORY_ERROR, + "Error al buscar el usuario", + _error, + ), + ); + } + } + + private async findUserEmail(email: Email): Promise { + const transaction = this._adapter.startTransaction(); + const authRepoBuilder = this.getRepositoryByName("Auth"); + + let user: User | null = null; + + await transaction.complete(async (t) => { + const authRepo = authRepoBuilder({ transaction: t }); + user = await authRepo.findByEmail(email); + }); + + return user; + } +} diff --git a/server/src/contexts/auth/application/authServices.ts b/server/src/contexts/auth/application/authServices.ts new file mode 100644 index 0000000..e69de29 diff --git a/server/src/contexts/auth/application/index.ts b/server/src/contexts/auth/application/index.ts new file mode 100644 index 0000000..1c1e388 --- /dev/null +++ b/server/src/contexts/auth/application/index.ts @@ -0,0 +1 @@ +export * from "./authServices"; diff --git a/server/src/contexts/auth/domain/entities/User.ts b/server/src/contexts/auth/domain/entities/User.ts new file mode 100644 index 0000000..93f7620 --- /dev/null +++ b/server/src/contexts/auth/domain/entities/User.ts @@ -0,0 +1,95 @@ +import bCrypt from "bcryptjs"; + +import { + AggregateRoot, + Email, + IDomainError, + Result, + UniqueID, +} from "@shared/contexts"; + +export interface IUserProps { + email: Email; + password?: string; + hashed_password?: string; +} + +export interface IUser { + id: UniqueID; + email: Email; + hashed_password: string; + + verifyPassword: (candidatePassword: string) => boolean; +} + +export class User extends AggregateRoot implements IUser { + public static create( + props: IUserProps, + id?: UniqueID, + ): Result { + //const isNew = !!id === false; + + // Se hace en el constructor de la Entidad + /* if (isNew) { + id = UniqueEntityID.create(); + }*/ + + const user = new User(props, id); + + return Result.ok(user); + } + + public static async hashPassword(password): Promise { + return hashPassword(password, await genSalt()); + } + + private _hashed_password: string; + + private constructor(props: IUserProps, id?: UniqueID) { + super({ ...props, password: "", hashed_password: "" }, id); + + this._protectPassword(props); + } + + get email(): Email { + return this.props.email; + } + + get hashed_password(): string { + return this._hashed_password; + } + + public verifyPassword(candidatePassword: string): boolean { + return bCrypt.compareSync(candidatePassword, this._hashed_password!); + } + + private async _protectPassword(props: IUserProps) { + const { password, hashed_password } = props; + + if (password) { + this._hashed_password = await User.hashPassword(password); + } else { + this._hashed_password = hashed_password!; + } + } +} + +async function genSalt(rounds = 10): Promise { + return new Promise((resolve, reject) => { + bCrypt.genSalt(rounds, function (err, salt) { + if (err) return reject(err); + return resolve(salt); + }); + }); +} + +async function hashPassword(password: string, salt: string): Promise { + return new Promise((resolve, reject) => { + bCrypt.hash(password, salt, function (err, hash) { + if (err) return reject(err); + return resolve(hash); + }); + }); +} + +User.hashPassword("123456").then((value) => console.log(value)); diff --git a/server/src/contexts/auth/domain/entities/index.ts b/server/src/contexts/auth/domain/entities/index.ts new file mode 100644 index 0000000..3ce758c --- /dev/null +++ b/server/src/contexts/auth/domain/entities/index.ts @@ -0,0 +1 @@ +export * from "./User"; diff --git a/server/src/contexts/auth/domain/index.ts b/server/src/contexts/auth/domain/index.ts new file mode 100644 index 0000000..8524695 --- /dev/null +++ b/server/src/contexts/auth/domain/index.ts @@ -0,0 +1 @@ +export * from "./entities"; diff --git a/server/src/contexts/auth/domain/repository/AuthRepository.interface.ts b/server/src/contexts/auth/domain/repository/AuthRepository.interface.ts new file mode 100644 index 0000000..7944042 --- /dev/null +++ b/server/src/contexts/auth/domain/repository/AuthRepository.interface.ts @@ -0,0 +1,8 @@ +import { IRepository } from "@/contexts/common/domain"; +import { Email, UniqueID } from "@shared/contexts"; +import { User } from "../entities"; + +export interface IAuthRepository extends IRepository { + getById(id: UniqueID): Promise; + findByEmail(email: Email): Promise; +} diff --git a/server/src/contexts/auth/domain/repository/index.ts b/server/src/contexts/auth/domain/repository/index.ts new file mode 100644 index 0000000..608eeb6 --- /dev/null +++ b/server/src/contexts/auth/domain/repository/index.ts @@ -0,0 +1 @@ +export * from "./AuthRepository.interface"; diff --git a/server/src/contexts/auth/index.ts b/server/src/contexts/auth/index.ts new file mode 100644 index 0000000..cdbc093 --- /dev/null +++ b/server/src/contexts/auth/index.ts @@ -0,0 +1 @@ +export * from "./infrastructure"; diff --git a/server/src/contexts/auth/infrastructure/Auth.context.ts b/server/src/contexts/auth/infrastructure/Auth.context.ts new file mode 100644 index 0000000..aa1107c --- /dev/null +++ b/server/src/contexts/auth/infrastructure/Auth.context.ts @@ -0,0 +1,35 @@ +import { + IRepositoryManager, + RepositoryManager, +} from "@/contexts/common/domain"; +import { + ISequelizeAdapter, + createSequelizeAdapter, +} from "@/contexts/common/infrastructure/sequelize"; + +export interface IAuthContext { + adapter: ISequelizeAdapter; + repositoryManager: IRepositoryManager; + //services: IApplicationService; +} + +export class AuthContext { + private static instance: AuthContext | null = null; + + public static getInstance(): IAuthContext { + if (!AuthContext.instance) { + AuthContext.instance = new AuthContext({ + adapter: createSequelizeAdapter(), + repositoryManager: RepositoryManager.getInstance(), + }); + } + + return AuthContext.instance.context; + } + + private context: IAuthContext; + + private constructor(context: IAuthContext) { + this.context = context; + } +} diff --git a/server/src/contexts/auth/infrastructure/Auth.repository.ts b/server/src/contexts/auth/infrastructure/Auth.repository.ts new file mode 100644 index 0000000..45afba3 --- /dev/null +++ b/server/src/contexts/auth/infrastructure/Auth.repository.ts @@ -0,0 +1,86 @@ +import { IAuthContext } from "./Auth.context"; + +import { + ISequelizeAdapter, + SequelizeRepository, +} from "@/contexts/common/infrastructure/sequelize"; +import { Email, ICollection, IQueryCriteria, UniqueID } from "@shared/contexts"; +import { Transaction } from "sequelize"; +import { User } from "../domain/entities"; +import { IAuthRepository } from "../domain/repository/AuthRepository.interface"; +import { IUserMapper, createUserMapper } from "./mappers/user.mapper"; + +export type QueryParams = { + pagination: Record; + filters: Record; +}; + +export class AuthRepository + extends SequelizeRepository + implements IAuthRepository +{ + protected mapper: IUserMapper; + + public constructor(props: { + mapper: IUserMapper; + adapter: ISequelizeAdapter; + transaction: Transaction; + }) { + const { adapter, mapper, transaction } = props; + super({ adapter, transaction }); + this.mapper = mapper; + } + + public async getById(id: UniqueID): Promise { + const rawUser: any = await this._getById("User_Model", id); + + if (!rawUser === true) { + return null; + } + + return this.mapper.mapToDomain(rawUser); + } + + public async findByEmail(email: Email): Promise { + const rawUser: any = await this._getBy( + "User_Model", + "email", + email.toPrimitive(), + ); + + if (!rawUser === true) { + return null; + } + + return this.mapper.mapToDomain(rawUser); + } + + public async findAll( + queryCriteria?: IQueryCriteria, + ): Promise> { + const { rows, count } = await this._findAll( + "User_Model", + queryCriteria, + /*{ + include: [], // esto es para quitar las asociaciones al hacer la consulta + }*/ + ); + + return this.mapper.mapArrayAndCountToDomain(rows, count); + } +} + +export const registerAuthRepository = (context: IAuthContext) => { + const adapter = context.adapter; + const repoManager = context.repositoryManager; + + repoManager.registerRepository("Auth", (params = { transaction: null }) => { + const { transaction } = params; + + return new AuthRepository({ + transaction, + adapter, + mapper: createUserMapper(context), + }); + }); +}; diff --git a/server/src/contexts/auth/infrastructure/express/controllers/AuthenticateController.ts b/server/src/contexts/auth/infrastructure/express/controllers/AuthenticateController.ts new file mode 100644 index 0000000..af51bc2 --- /dev/null +++ b/server/src/contexts/auth/infrastructure/express/controllers/AuthenticateController.ts @@ -0,0 +1,67 @@ +// Import the necessary packages and modules +import { IServerError } from "@/contexts/common/domain/errors"; +import { ExpressController } from "@/contexts/common/infrastructure/express"; +import passport from "passport"; + +// Export a middleware function to authenticate incoming requests +/*export const authenticate = (req, res, next) => { + // Use Passport to authenticate the request using the "jwt" strategy + passport.authenticate("jwt", { session: false }, (err, user) => { + console.log(user); + if (err) next(err); // If there's an error, pass it on to the next middleware + if (!user) { + // If the user is not authenticated, send a 401 Unauthorized response + return res.status(401).json({ + message: "Unauthorized access. No token provided.", + }); + } + // If the user is authenticated, attach the user object to the request and move on to the next middleware + req.user = user; + next(); + })(req, res, next); +};*/ + +export class AuthenticateController extends ExpressController { + //private context: AuthContext; + + constructor() { + //context: IAuthContext, + super(); + + /*const { useCase, presenter } = props; + this.useCase = useCase; + this.presenter = presenter; + this.context = context;*/ + } + + async executeImpl() { + try { + return passport.authenticate( + "local-jwt", + { session: false }, + ( + err: any, + user?: Express.User | false | null, + info?: object | string | Array, + status?: number | Array, + ) => { + if (err) { + return this.next(err); + } + + if (!user) { + return this.unauthorizedError( + "Unauthorized access. No token provided.", + ); + } + + // If the user is authenticated, attach the user object to the request and move on to the next middleware + this.req.user = user; + return this.next(); + }, + ); + } catch (e: unknown) { + return this.fail(e as IServerError); + } + } +} diff --git a/server/src/contexts/auth/infrastructure/express/controllers/LoginController.ts b/server/src/contexts/auth/infrastructure/express/controllers/LoginController.ts new file mode 100644 index 0000000..f91e4ac --- /dev/null +++ b/server/src/contexts/auth/infrastructure/express/controllers/LoginController.ts @@ -0,0 +1,108 @@ +import { IUseCaseError, UseCaseError } from "@/contexts/common/application"; +import { IServerError } from "@/contexts/common/domain/errors"; +import { + IInfrastructureError, + InfrastructureError, + handleInfrastructureError, +} from "@/contexts/common/infrastructure"; +import { ExpressController } from "@/contexts/common/infrastructure/express"; +import { ILogin_DTO, ensureLogin_DTOIsValid } from "@shared/contexts"; + +export class LoginController extends ExpressController { + private useCase: LoginUseCase; + private presenter: ILoginPresenter; + private context: IAuthContext; + + constructor( + props: { + useCase: LoginUseCase; + presenter: ILoginPresenter; + }, + context: IAuthContext, + ) { + super(); + + const { useCase, presenter } = props; + this.useCase = useCase; + this.presenter = presenter; + this.context = context; + } + + async executeImpl() { + try { + const loginDTO: ILogin_DTO = this.req.body; + + // Validaciones de DTO + const loginDTOOrError = ensureLogin_DTOIsValid(loginDTO); + + if (loginDTOOrError.isFailure) { + const errorMessage = "Login data not valid"; + const infraError = handleInfrastructureError( + InfrastructureError.INVALID_INPUT_DATA, + errorMessage, + loginDTOOrError.error, + ); + return this.invalidInputError(errorMessage, infraError); + } + + const result = await this.useCase.execute(); + + if (result.isFailure) { + return this._handleExecuteError(result.error); + } + + console.log("login OK => generate token JWT"); + + const customer = result.object; + + return this.created( + this.presenter.map(customer, this.context), + ); + } catch (e: unknown) { + return this.fail(e as IServerError); + } + } + + private _handleExecuteError(error: IUseCaseError) { + let errorMessage: string; + let infraError: IInfrastructureError; + + switch (error.code) { + case UseCaseError.INVALID_INPUT_DATA: + errorMessage = "Login data not valid"; + infraError = handleInfrastructureError( + InfrastructureError.INVALID_INPUT_DATA, + errorMessage, + error, + ); + return this.invalidInputError(errorMessage, infraError); + break; + + case UseCaseError.NOT_FOUND_ERROR: + errorMessage = "User not found"; + + infraError = handleInfrastructureError( + InfrastructureError.INVALID_INPUT_DATA, + errorMessage, + error, + ); + return this.conflictError(error.message, error); + break; + + case UseCaseError.UNEXCEPTED_ERROR: + errorMessage = error.message; + + infraError = handleInfrastructureError( + InfrastructureError.UNEXCEPTED_ERROR, + errorMessage, + error, + ); + return this.internalServerError(errorMessage, infraError); + break; + + default: + errorMessage = error.message; + return this.clientError(errorMessage); + } + } +} diff --git a/server/src/contexts/auth/infrastructure/express/controllers/index.ts b/server/src/contexts/auth/infrastructure/express/controllers/index.ts new file mode 100644 index 0000000..73b710c --- /dev/null +++ b/server/src/contexts/auth/infrastructure/express/controllers/index.ts @@ -0,0 +1,20 @@ +import { AuthenticateController } from "./AuthenticateController"; + +const authenticate = (context: any) => { + //const adapter = context.adapter; + //const repoManager = context.repositoryManager; + + /*repoManager.registerRepository("Auth", (params = { transaction: null }) => { + const { transaction } = params; + + return new AuthRepository({ + transaction, + adapter, + mapper: createAuthMapper(context), + }); + });*/ + + //const listArticlesUseCase = new ListArticlesUseCase(context); + + return new AuthenticateController(); +}; diff --git a/server/src/contexts/auth/infrastructure/express/index.ts b/server/src/contexts/auth/infrastructure/express/index.ts new file mode 100644 index 0000000..6daa344 --- /dev/null +++ b/server/src/contexts/auth/infrastructure/express/index.ts @@ -0,0 +1,3 @@ +export * from "./controllers"; +export * from "./passport"; +export * from "./routes"; diff --git a/server/src/contexts/auth/infrastructure/express/passport/authenticate.ts b/server/src/contexts/auth/infrastructure/express/passport/authenticate.ts new file mode 100644 index 0000000..564e04f --- /dev/null +++ b/server/src/contexts/auth/infrastructure/express/passport/authenticate.ts @@ -0,0 +1,34 @@ +/* eslint-disable @typescript-eslint/no-unused-vars */ + +import Express from "express"; +import passport from "passport"; + +export const authenticate = ( + req: Express.Request, + res: Express.Response, + next: Express.NextFunction, +) => { + // Use Passport to authenticate the request using the "jwt" strategy + passport.authenticate( + "local-jwt", + { session: false }, + ( + err: any, + user?: Express.User | false | null, + info?: object | string | Array, + status?: number | Array, + ) => { + console.log(user); + if (err) next(err); // If there's an error, pass it on to the next middleware + if (!user) { + // If the user is not authenticated, send a 401 Unauthorized response + return res.status(401).json({ + message: "Unauthorized access. No token provided.", + }); + } + // If the user is authenticated, attach the user object to the request and move on to the next middleware + req.user = user; + next(); + }, + )(req, res, next); +}; diff --git a/server/src/contexts/auth/infrastructure/express/passport/configurePassportAuth.ts b/server/src/contexts/auth/infrastructure/express/passport/configurePassportAuth.ts new file mode 100644 index 0000000..6d4044a --- /dev/null +++ b/server/src/contexts/auth/infrastructure/express/passport/configurePassportAuth.ts @@ -0,0 +1,11 @@ +import { PassportStatic } from "passport"; +import { AuthContext } from "../../Auth.context"; +import { initEmailStrategy } from "./emailStrategy"; +import { jwtStrategy } from "./jwtStrategy"; + +// Export a function that will be used to configure Passport authentication +export const configurePassportAuth = (passport: PassportStatic) => { + console.log("passport: configuring strategies !!!!!!!!!!!!!!!!!!"); + passport.use("local-email", initEmailStrategy(AuthContext.getInstance())); + passport.use("local-jwt", jwtStrategy); +}; diff --git a/server/src/contexts/auth/infrastructure/express/passport/emailStrategy.ts b/server/src/contexts/auth/infrastructure/express/passport/emailStrategy.ts new file mode 100644 index 0000000..dc65a81 --- /dev/null +++ b/server/src/contexts/auth/infrastructure/express/passport/emailStrategy.ts @@ -0,0 +1,69 @@ +import { LoginUseCase } from "@/contexts/auth/application/LoginUseCase"; +import { IServerError } from "@/contexts/common/domain/errors"; +import { PassportStrategyController } from "@/contexts/common/infrastructure/express"; +import { ensureLogin_DTOIsValid } from "@shared/contexts"; +import { Strategy as EmailStrategy, IVerifyOptions } from "passport-local"; + +import { IAuthContext } from "../../Auth.context"; +import { registerAuthRepository } from "../../Auth.repository"; + +const strategyOpts = { + usernameField: "email", // Campo utilizado para el email en el formulario + passwordField: "password", +}; + +class EmailStrategyController extends PassportStrategyController { + private useCase: LoginUseCase; + private context: IAuthContext; + + constructor( + props: { + useCase: LoginUseCase; + }, + context: any, + ) { + super(); + + const { useCase } = props; + this.useCase = useCase; + this.context = context; + } + + public async verifyStrategy( + email: string, + password: string, + done: ( + error: any, + user?: Express.User | false, + options?: IVerifyOptions, + ) => void, + ) { + const loginDTOOrError = ensureLogin_DTOIsValid({ email, password }); + + if (loginDTOOrError.isFailure) { + return done(null, false); + } + + try { + const result = await this.useCase.execute({ email, password }); + if (result.isFailure) { + return done(null, false); + } + + return done(null, result.object); + } catch (e: unknown) { + return done(e as IServerError); + } + } +} + +export const initEmailStrategy = (context: IAuthContext) => + new EmailStrategy(strategyOpts, async (...params) => { + registerAuthRepository(context); + return new EmailStrategyController( + { + useCase: new LoginUseCase(context), + }, + context, + ).verifyStrategy(...params); + }); diff --git a/server/src/contexts/auth/infrastructure/express/passport/index.ts b/server/src/contexts/auth/infrastructure/express/passport/index.ts new file mode 100644 index 0000000..fdacb83 --- /dev/null +++ b/server/src/contexts/auth/infrastructure/express/passport/index.ts @@ -0,0 +1,2 @@ +export * from "./authenticate"; +export * from "./configurePassportAuth"; diff --git a/server/src/contexts/auth/infrastructure/express/passport/jwtStrategy.ts b/server/src/contexts/auth/infrastructure/express/passport/jwtStrategy.ts new file mode 100644 index 0000000..5359493 --- /dev/null +++ b/server/src/contexts/auth/infrastructure/express/passport/jwtStrategy.ts @@ -0,0 +1,27 @@ +import { config } from "@/config"; +import { ExtractJwt, Strategy as JWTStrategy } from "passport-jwt"; + +const strategyOpts = { + jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(), // Extract the JWT from the Authorization header + secretOrKey: config.jwt.secret_key, +}; + +export const jwtStrategy = new JWTStrategy( + strategyOpts, + async (jwt_payload, done) => { + console.log( + "PASSPORT USE LOCAL-JWT !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!", + ); + console.log(jwt_payload); + + /* + const user = await authenticateUserByEmail("assa@asds.com", "password"); + if (user) { + return done(null, user); + } else { + return done(null, false); + } + */ + return done(null, { id: "xzxxxxx", email: "assa@asds.com" }); + }, +); diff --git a/server/src/contexts/auth/infrastructure/express/passport/passport.bak b/server/src/contexts/auth/infrastructure/express/passport/passport.bak new file mode 100644 index 0000000..c33f3bb --- /dev/null +++ b/server/src/contexts/auth/infrastructure/express/passport/passport.bak @@ -0,0 +1,184 @@ +import { + InfrastructureError, + handleInfrastructureError, +} from "@/contexts/common/infrastructure"; +import { ensureLogin_DTOIsValid } from "@shared/contexts"; +import passport from "passport"; +import { ExtractJwt, Strategy as JwtStrategy } from "passport-jwt"; +import { Strategy as LocalStrategy } from "passport-local"; + +/*declare global { + namespace Express { + interface User { + id?: string; + email: string; + password: string; + } + } +}*/ + +//import { authenticateUserByEmail, deserializeUserById } from './../../../services'; +const authenticateUserByEmail = async ( + email: string, + password: string, +): Promise => { + // Simulación de búsqueda del usuario en la base de datos + const user = { + id: "1", + email: "usuario@example.com", + password: "$2a$10$EdfkgQ7vMTGvXq1qI7qJQ.g3WiYGqCSCdzhZ/LG20YKwA1YjgLJnO", + }; // Contraseña hasheada: "password" + + return user; + + /*const res = await pool.query('SELECT * FROM users WHERE email = $1', [email]); + + if (res.rows.length) { + const user = res.rows[0]; + const match = await bcrypt.compare(password, user.password); + + if (match) { + return user; + } else { + throw new Error('Incorrect email and/or password'); + } + } else { + throw new Error('User not found'); + }*/ +}; + +const deserializeUserById = async ( + id: number, +): Promise => { + /*const res = await pool.query('SELECT * FROM users WHERE id = $1', [id]); + + if (res.rows.length) { + return res.rows[0]; + } else { + throw new Error('User was not found'); + }*/ + + const user = { + id: "1", + email: "usuario@example.com", + password: "$2a$10$EdfkgQ7vMTGvXq1qI7qJQ.g3WiYGqCSCdzhZ/LG20YKwA1YjgLJnO", + }; // Contraseña hasheada: "password" + + return user; +}; + +// Configurar la estrategia de autenticación local de Passport.js +passport.use( + "local-email", + new LocalStrategy( + { + usernameField: "email", // Campo utilizado para el email en el formulario + passwordField: "password", + }, + async (email, password, done) => { + console.log("local-email"); + + const loginDTO = { email, password }; + const loginDTOOrError = ensureLogin_DTOIsValid(loginDTO); + + if (loginDTOOrError.isFailure) { + const errorMessage = "Login data not valid"; + const infraError = handleInfrastructureError( + InfrastructureError.INVALID_INPUT_DATA, + errorMessage, + loginDTOOrError.error, + ); + done(infraError); + } + + const result = await this.useCase.execute(); + + try { + // Buscar el usuario en la base de datos por email + const user = await authenticateUserByEmail(email, password); + return done(null, user as Express.User); + } catch (error) { + return done(error); + } + }, + ), +); + +passport.use( + "local-jwt", + new JwtStrategy( + { + jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(), + secretOrKey: "secret", + }, + async (jwt_payload, done) => { + console.log(jwt_payload); + + const user = await authenticateUserByEmail("assa@asds.com", "password"); + return done(null, user); + }, + ), +); + +/*passport.use( + "jwt2", + new CustomStrategy(async (req, done) => { + const token = + req && req.headers && req.headers["x-access-token"] + ? req.headers["x-access-token"] + : null; + const appVersion = + req && req.headers && req.headers["accept-version"] + ? req.headers["accept-version"] + : null; + console.log("appVersion: ", appVersion); + + if (!token) { + console.error("Unauthorized. Token missing."); + return done(null, false, { message: "Unauthorized. Token missing." }); + } + + const result = securityHelper.verify(token); + //console.log('token result => ', result); + + if (result && result.id) { + //recuperamos el usuario de la petición + let user = await authService.extraMethods.findUser({ id: result.id }); + if (user) { + user = user.toJSON(); + userService._updateLastLoginAndVersionUser(user.id, appVersion); + user.app_version = appVersion; + user.token = token; + delete user.password; + + console.log("Logged in Successfully"); + console.log(user); + return done(null, user, { message: "Logged in Successfully" }); + } else { + console.error("Unauthorized. User not found."); + return done(null, false, { message: "Unauthorized. User not found." }); + } + } else { + //console.log('Token no válido'); + console.error("Unauthorized. Invalid token."); + return done(null, false, { message: "Unauthorized. Invalid token." }); + } + }), +);*/ + +// Serializar y deserializar el usuario para almacenar y recuperar la sesión +passport.serializeUser((user: Express.User, done) => { + done(null, user.id); +}); + +passport.deserializeUser(async (id: number, done) => { + try { + const user = await deserializeUserById(id); + + done(null, user as Express.User); + } catch (error) { + done(error, null); + } +}); + +export { passport }; diff --git a/server/src/contexts/auth/infrastructure/express/routes.ts b/server/src/contexts/auth/infrastructure/express/routes.ts new file mode 100644 index 0000000..492c84b --- /dev/null +++ b/server/src/contexts/auth/infrastructure/express/routes.ts @@ -0,0 +1,88 @@ +import { config } from "@/config"; +import Express from "express"; +import JWT from "jsonwebtoken"; +import passport from "passport"; +import { User } from "../../domain"; + +/*authRoutes.post( + "/login", + passport.authenticate("local-email"), + + (req: Express.Request, res: Express.Response, next: Express.NextFunction) => { + console.log("login OK => generate token JWT"); + + // Generar token JWT + const token = JWT.sign({ userId: req.user?.id }, "clave_secreta", { + expiresIn: "1h", + }); // Clave secreta y expiración de 1 hora + + // Enviar token como respuesta + res.json({ token }); + }, +); + +authRoutes.post("/logout", passport.authenticate("local-jwt")); + +authRoutes.get( + "/profile", + passport.authenticate("local-jwt", { session: false }), + (req: Express.Request, res: Express.Response, next: Express.NextFunction) => { + res.json({ + message: "You made it to the secure route", + user: req.user, + token: req.query.secret_token, + }); + }, +);*/ + +//export { authRouter }; + +export const AuthRouter = (appRouter: Express.Router) => { + const authRoutes: Express.Router = Express.Router({ mergeParams: true }); + + //appRouter.use(registerMiddleware("authenticate", authenticate)); + + authRoutes.post( + "/login", + passport.authenticate("local-email", { session: false }), + (req, res, next) => { + if (req.isAuthenticated()) { + const user: User = req.user; + + const accessToken = JWT.sign( + { id: user.id, email: user.email }, + config.jwt.secret_key, + { expiresIn: config.jwt.token_expiration }, + ); + const refreshToken = JWT.sign( + { id: user.id, email: user.email }, + config.jwt.refresh_secret_key, + { expiresIn: config.jwt.refresh_token_expiration }, + ); + + //refreshTokens.push(refreshToken); + + return res.json({ accessToken, refreshToken }); + } + return res.status(401).json({}); + }, + ); + + authRoutes.post( + "/login2", + (req: Express.Request, res: Express.Response, next: Express.NextFunction) => + passport.authenticate( + "local-email", + { session: false }, + (err, user, info) => { + console.log(err, user, info); + next(err); + }, + )(req, res, next), + (req, res, next) => { + res.status(200).json({}); + }, + ); + + appRouter.use("/auth", authRoutes); +}; diff --git a/server/src/contexts/auth/infrastructure/index.ts b/server/src/contexts/auth/infrastructure/index.ts new file mode 100644 index 0000000..c754fca --- /dev/null +++ b/server/src/contexts/auth/infrastructure/index.ts @@ -0,0 +1,2 @@ +export * from "./express"; +export * from "./sequelize"; diff --git a/server/src/contexts/auth/infrastructure/mappers/index.ts b/server/src/contexts/auth/infrastructure/mappers/index.ts new file mode 100644 index 0000000..adc0092 --- /dev/null +++ b/server/src/contexts/auth/infrastructure/mappers/index.ts @@ -0,0 +1 @@ +export * from "./user.mapper"; diff --git a/server/src/contexts/auth/infrastructure/mappers/user.mapper.ts b/server/src/contexts/auth/infrastructure/mappers/user.mapper.ts new file mode 100644 index 0000000..cf1106a --- /dev/null +++ b/server/src/contexts/auth/infrastructure/mappers/user.mapper.ts @@ -0,0 +1,52 @@ +import { + ISequelizeMapper, + SequelizeMapper, +} from "@/contexts/common/infrastructure"; +import { Email, UniqueID } from "@shared/contexts"; +import { IUserProps, User } from "../../domain/entities"; +import { IAuthContext } from "../Auth.context"; +import { TCreationUser_Attributes, User_Model } from "../sequelize/user.model"; + +export interface IUserMapper + extends ISequelizeMapper {} + +class UserMapper + extends SequelizeMapper + implements IUserMapper +{ + public constructor(props: { context: IAuthContext }) { + super(props); + } + + protected toDomainMappingImpl(source: User_Model, params: any): User { + const props: IUserProps = { + email: this.mapsValue(source, "email", Email.create), + hashed_password: source.password, + }; + + const id = this.mapsValue(source, "id", UniqueID.create); + const userOrError = User.create(props, id); + + if (userOrError.isFailure) { + throw userOrError.error; + } + + return userOrError.object; + } + + protected toPersistenceMappingImpl( + source: User, + params?: Record | undefined, + ) { + return { + id: source.id.toPrimitive(), + email: source.email.toPrimitive(), + password: source.hashed_password, + }; + } +} + +export const createUserMapper = (context: IAuthContext): IUserMapper => + new UserMapper({ + context, + }); diff --git a/server/src/contexts/auth/infrastructure/sequelize/index.ts b/server/src/contexts/auth/infrastructure/sequelize/index.ts new file mode 100644 index 0000000..3787888 --- /dev/null +++ b/server/src/contexts/auth/infrastructure/sequelize/index.ts @@ -0,0 +1 @@ +export * from "./user.model"; diff --git a/server/src/contexts/auth/infrastructure/sequelize/user.model.ts b/server/src/contexts/auth/infrastructure/sequelize/user.model.ts new file mode 100644 index 0000000..3ef6f82 --- /dev/null +++ b/server/src/contexts/auth/infrastructure/sequelize/user.model.ts @@ -0,0 +1,61 @@ +import { + DataTypes, + InferAttributes, + InferCreationAttributes, + Model, + Sequelize, +} from "sequelize"; + +export type TCreationUser_Attributes = InferCreationAttributes; + +export class User_Model extends Model< + InferAttributes, + InferCreationAttributes +> { + // To avoid table creation + /*static async sync(): Promise { + return Promise.resolve(); + }*/ + + static associate(connection: Sequelize) {} + + declare id: string; + declare email: string; + declare password: string; +} + +export default (sequelize: Sequelize) => { + User_Model.init( + { + id: { + type: new DataTypes.UUID(), + primaryKey: true, + }, + + email: { + type: DataTypes.STRING, + allowNull: false, + }, + password: { + type: DataTypes.STRING, + allowNull: false, + }, + }, + { + sequelize, + tableName: "users", + + //paranoid: true, // softs deletes + //timestamps: true, + //version: true, + + //createdAt: "created_at", + //updatedAt: "updated_at", + //deletedAt: "deleted_at", + + indexes: [{ name: "email_idx", unique: true, fields: ["email"] }], + }, + ); + + return User_Model; +}; diff --git a/server/src/contexts/catalog/index.ts b/server/src/contexts/catalog/index.ts new file mode 100644 index 0000000..4f72a4e --- /dev/null +++ b/server/src/contexts/catalog/index.ts @@ -0,0 +1 @@ +export * from "./infrastructure/express"; diff --git a/server/src/contexts/catalog/infrastructure/Catalog.repository.ts b/server/src/contexts/catalog/infrastructure/Catalog.repository.ts index 44f7208..f8a76d6 100644 --- a/server/src/contexts/catalog/infrastructure/Catalog.repository.ts +++ b/server/src/contexts/catalog/infrastructure/Catalog.repository.ts @@ -4,9 +4,10 @@ import { } from "@/contexts/common/infrastructure/sequelize"; import { ICollection, IQueryCriteria, UniqueID } from "@shared/contexts"; import { Transaction } from "sequelize"; +import { ICatalogContext } from "."; import { Article } from "../domain/entities"; import { ICatalogRepository } from "../domain/repository/CatalogRepository.interface"; -import { IArticleMapper } from "./mappers/article.mapper"; +import { IArticleMapper, createArticleMapper } from "./mappers/article.mapper"; export type QueryParams = { pagination: Record; @@ -40,11 +41,11 @@ export class CatalogRepository } public async findAll( - queryCriteria?: IQueryCriteria + queryCriteria?: IQueryCriteria, ): Promise> { const { rows, count } = await this._findAll( "Article_Model", - queryCriteria + queryCriteria, /*{ include: [], // esto es para quitar las asociaciones al hacer la consulta }*/ @@ -53,3 +54,21 @@ export class CatalogRepository return this.mapper.mapArrayAndCountToDomain(rows, count); } } + +export const registerCatalogRepository = (context: ICatalogContext) => { + const adapter = context.adapter; + const repoManager = context.repositoryManager; + + repoManager.registerRepository( + "Article", + (params = { transaction: null }) => { + const { transaction } = params; + + return new CatalogRepository({ + transaction, + adapter, + mapper: createArticleMapper(context), + }); + }, + ); +}; diff --git a/server/src/contexts/catalog/infrastructure/express/catalogRoutes.ts b/server/src/contexts/catalog/infrastructure/express/catalogRoutes.ts deleted file mode 100644 index f82b04a..0000000 --- a/server/src/contexts/catalog/infrastructure/express/catalogRoutes.ts +++ /dev/null @@ -1,40 +0,0 @@ -import express, { NextFunction, Request, Response, Router } from "express"; - -import { RepositoryManager } from "@/contexts/common/domain"; -import { createSequelizeAdapter } from "@/contexts/common/infrastructure/sequelize"; -import { createListArticlesController } from "./controllers"; - -const catalogRouter: Router = express.Router({ mergeParams: true }); - -const logMiddleware = (req, res, next) => { - console.log( - `[${new Date().toLocaleTimeString()}] Incoming request to ${req.path}` - ); - next(); -}; - -catalogRouter.use(logMiddleware); - -const contextMiddleware = (req: Request, res: Response, next: NextFunction) => { - res.locals["context"] = { - adapter: createSequelizeAdapter(), - repositoryManager: RepositoryManager.getInstance(), - services: {}, - }; - - return next(); -}; - -catalogRouter.use(contextMiddleware); - -catalogRouter.get("/", (req: Request, res: Response, next: NextFunction) => - createListArticlesController(res.locals["context"]).execute(req, res, next) -); - -/*catalogRouter.get( - "/:articleId", - (req: Request, res: Response, next: NextFunction) => - createGetCustomerController(res.locals["context"]).execute(req, res, next) -);*/ - -export { catalogRouter }; diff --git a/server/src/contexts/catalog/infrastructure/express/controllers/listArticles/ListArticlesController.ts b/server/src/contexts/catalog/infrastructure/express/controllers/listArticles/ListArticlesController.ts index 33bde5e..8c47cbb 100644 --- a/server/src/contexts/catalog/infrastructure/express/controllers/listArticles/ListArticlesController.ts +++ b/server/src/contexts/catalog/infrastructure/express/controllers/listArticles/ListArticlesController.ts @@ -29,7 +29,7 @@ export class ListArticlesController extends ExpressController { useCase: ListArticlesUseCase; presenter: IListArticlesPresenter; }, - context: ICatalogContext + context: ICatalogContext, ) { super(); @@ -79,7 +79,7 @@ export class ListArticlesController extends ExpressController { this.presenter.mapArray(customers, this.context, { page: queryCriteria.pagination.offset, limit: queryCriteria.pagination.limit, - }) + }), ); } catch (e: unknown) { return this.fail(e as IServerError); diff --git a/server/src/contexts/catalog/infrastructure/express/controllers/listArticles/index.ts b/server/src/contexts/catalog/infrastructure/express/controllers/listArticles/index.ts index b1c6cd0..8b6ae20 100644 --- a/server/src/contexts/catalog/infrastructure/express/controllers/listArticles/index.ts +++ b/server/src/contexts/catalog/infrastructure/express/controllers/listArticles/index.ts @@ -1,34 +1,18 @@ import { ListArticlesUseCase } from "@/contexts/catalog/application"; import { ICatalogContext } from "../../.."; -import { CatalogRepository } from "../../../Catalog.repository"; -import { createArticleMapper } from "../../../mappers/article.mapper"; +import { registerCatalogRepository } from "../../../Catalog.repository"; import { ListArticlesController } from "./ListArticlesController"; import { listArticlesPresenter } from "./presenter"; export const createListArticlesController = (context: ICatalogContext) => { - const adapter = context.adapter; - const repoManager = context.repositoryManager; - - repoManager.registerRepository( - "Article", - (params = { transaction: null }) => { - const { transaction } = params; - - return new CatalogRepository({ - transaction, - adapter, - mapper: createArticleMapper(context), - }); - } - ); - const listArticlesUseCase = new ListArticlesUseCase(context); + registerCatalogRepository(context); return new ListArticlesController( { useCase: listArticlesUseCase, presenter: listArticlesPresenter, }, - context + context, ); }; diff --git a/server/src/contexts/catalog/infrastructure/express/index.ts b/server/src/contexts/catalog/infrastructure/express/index.ts index 5d84f8d..e5fd210 100644 --- a/server/src/contexts/catalog/infrastructure/express/index.ts +++ b/server/src/contexts/catalog/infrastructure/express/index.ts @@ -1,2 +1,2 @@ -export * from "./catalogRoutes"; export * from "./controllers"; +export * from "./routes"; diff --git a/server/src/contexts/catalog/infrastructure/express/routes.ts b/server/src/contexts/catalog/infrastructure/express/routes.ts new file mode 100644 index 0000000..3c14354 --- /dev/null +++ b/server/src/contexts/catalog/infrastructure/express/routes.ts @@ -0,0 +1,26 @@ +import { applyMiddleware } from "@/contexts/common/infrastructure/express"; +import Express from "express"; +import { createListArticlesController } from "./controllers"; + +/*catalogRoutes.get( + "/:articleId", + (req: Request, res: Response, next: NextFunction) => + createGetCustomerController(res.locals["context"]).execute(req, res, next) +);*/ + +export const CatalogRouter = (appRouter: Express.Router) => { + const catalogRoutes: Express.Router = Express.Router({ mergeParams: true }); + + catalogRoutes.get( + "/", + applyMiddleware("authenticate"), + (req: Express.Request, res: Express.Response, next: Express.NextFunction) => + createListArticlesController(res.locals["context"]).execute( + req, + res, + next, + ), + ); + + appRouter.use("/catalog", catalogRoutes); +}; diff --git a/server/src/contexts/catalog/infrastructure/index.ts b/server/src/contexts/catalog/infrastructure/index.ts index e434410..ef5b5c5 100644 --- a/server/src/contexts/catalog/infrastructure/index.ts +++ b/server/src/contexts/catalog/infrastructure/index.ts @@ -7,3 +7,5 @@ export interface ICatalogContext { repositoryManager: IRepositoryManager; services: IApplicationService; } + +export * from "./express"; diff --git a/server/src/contexts/catalog/infrastructure/sequelize/article.model.ts b/server/src/contexts/catalog/infrastructure/sequelize/article.model.ts index 23de766..6b51f37 100644 --- a/server/src/contexts/catalog/infrastructure/sequelize/article.model.ts +++ b/server/src/contexts/catalog/infrastructure/sequelize/article.model.ts @@ -80,7 +80,7 @@ export default (sequelize: Sequelize) => { { name: "updated_at_idx", fields: ["updated_at"] }, ], - whereMergeStrategy: "and", + whereMergeStrategy: "and", // <- cómo tratar el merge de un scope scopes: { quickSearch(value) { return { diff --git a/server/src/contexts/common/application/useCases/UseCaseError.ts b/server/src/contexts/common/application/useCases/UseCaseError.ts index cae8bce..a3374df 100755 --- a/server/src/contexts/common/application/useCases/UseCaseError.ts +++ b/server/src/contexts/common/application/useCases/UseCaseError.ts @@ -13,7 +13,7 @@ export class UseCaseError extends ServerError implements IUseCaseError { public static create( code: string, message: string, - details?: Record + details?: Record, ): UseCaseError { return new UseCaseError(code, message, details); } @@ -22,60 +22,7 @@ export class UseCaseError extends ServerError implements IUseCaseError { export function handleUseCaseError( code: string, message: string, - payload?: Record + payload?: Record, ): IUseCaseError { return UseCaseError.create(code, message, payload); } - -/*export function handleNotFoundError( - message: string, - validationError: Error, - details?: Record -): Result { - return handleUseCaseError( - UseCaseError.NOT_FOUND_ERROR, - message, - validationError, - details - ); -} - -export function handleInvalidInputDataError( - message: string, - validationError: Error, - details?: Record -): Result { - return handleUseCaseError( - UseCaseError.INVALID_INPUT_DATA, - message, - validationError, - details - ); -} - -export function handleResourceAlreadyExitsError( - message: string, - validationError: Error, - details?: Record -): Result { - return handleUseCaseError( - UseCaseError.RESOURCE_ALREADY_EXITS, - message, - validationError, - details - ); -} - -export function handleRepositoryError( - message: string, - repositoryError: Error, - details?: Record -): Result { - return handleUseCaseError( - UseCaseError.REPOSITORY_ERROR, - message, - repositoryError, - details - ); -} -*/ diff --git a/server/src/contexts/common/infrastructure/Controller.interface.ts b/server/src/contexts/common/infrastructure/Controller.interface.ts new file mode 100644 index 0000000..ffe0462 --- /dev/null +++ b/server/src/contexts/common/infrastructure/Controller.interface.ts @@ -0,0 +1 @@ +export interface IController {} diff --git a/server/src/contexts/common/infrastructure/express/ExpressController.ts b/server/src/contexts/common/infrastructure/express/ExpressController.ts index 50b9317..9059d23 100644 --- a/server/src/contexts/common/infrastructure/express/ExpressController.ts +++ b/server/src/contexts/common/infrastructure/express/ExpressController.ts @@ -7,11 +7,10 @@ import { } from "@shared/contexts"; import { UseCaseError } from "../../application"; import { IServerError } from "../../domain/errors"; +import { IController } from "../Controller.interface"; import { InfrastructureError } from "../InfrastructureError"; import { ProblemDocument, ProblemDocumentExtension } from "./ProblemDocument"; -export interface IController {} - export abstract class ExpressController implements IController { protected req: express.Request; protected res: express.Response; @@ -25,7 +24,7 @@ export abstract class ExpressController implements IController { public execute( req: express.Request, res: express.Response, - next: express.NextFunction + next: express.NextFunction, ): void { this.req = req; this.res = res; @@ -33,7 +32,7 @@ export abstract class ExpressController implements IController { this.serverURL = `${ new URL( - `${this.req.protocol}://${this.req.get("host")}${this.req.originalUrl}` + `${this.req.protocol}://${this.req.get("host")}${this.req.originalUrl}`, ).origin }/api/v1`; @@ -121,7 +120,7 @@ export abstract class ExpressController implements IController { private _jsonResponse( statusCode: number, - jsonPayload: any + jsonPayload: any, ): express.Response { return this.res.status(statusCode).json(jsonPayload).send(); } @@ -129,7 +128,7 @@ export abstract class ExpressController implements IController { private _errorResponse( statusCode: number, message?: string, - error?: Error | InfrastructureError + error?: Error | InfrastructureError, ): express.Response { const context = {}; @@ -164,13 +163,13 @@ export abstract class ExpressController implements IController { detail: message, instance: this.req.baseUrl, }, - extension - ) + extension, + ), ); } private _processError( - error: Error | InfrastructureError + error: Error | InfrastructureError, ): IErrorExtra_Response_DTO { /** * diff --git a/server/src/contexts/common/infrastructure/express/PassportStrategyController.ts b/server/src/contexts/common/infrastructure/express/PassportStrategyController.ts new file mode 100644 index 0000000..e162acf --- /dev/null +++ b/server/src/contexts/common/infrastructure/express/PassportStrategyController.ts @@ -0,0 +1,3 @@ +import { IController } from "../Controller.interface"; + +export abstract class PassportStrategyController implements IController {} diff --git a/server/src/contexts/common/infrastructure/express/index.ts b/server/src/contexts/common/infrastructure/express/index.ts index 8e594da..8f90595 100644 --- a/server/src/contexts/common/infrastructure/express/index.ts +++ b/server/src/contexts/common/infrastructure/express/index.ts @@ -1 +1,3 @@ -export * from './ExpressController'; +export * from "./ExpressController"; +export * from "./PassportStrategyController"; +export * from "./middlewares"; diff --git a/server/src/contexts/common/infrastructure/express/middlewares.ts b/server/src/contexts/common/infrastructure/express/middlewares.ts new file mode 100644 index 0000000..911a801 --- /dev/null +++ b/server/src/contexts/common/infrastructure/express/middlewares.ts @@ -0,0 +1,36 @@ +import Express from "express"; + +const registeredMiddlewares: Record = {}; + +function registerMiddleware(name: string, middleware: Express.RequestHandler) { + return ( + req: Express.Request, + res: Express.Response, + next: Express.NextFunction, + ) => { + registeredMiddlewares[name] = middleware; + next(); + }; +} + +function applyMiddleware(middlewares: string | Array) { + const middlewareNames = + typeof middlewares === "string" ? [middlewares] : middlewares; + + return ( + req: Express.Request, + res: Express.Response, + next: Express.NextFunction, + ) => { + middlewareNames.forEach((name) => { + const middleware = registeredMiddlewares[name]; + if (middleware) { + middleware(req, res, next); + } else { + next(); + } + }); + }; +} + +export { applyMiddleware, registerMiddleware }; diff --git a/server/src/contexts/common/infrastructure/index.ts b/server/src/contexts/common/infrastructure/index.ts index 6bcfbf9..0d15aac 100644 --- a/server/src/contexts/common/infrastructure/index.ts +++ b/server/src/contexts/common/infrastructure/index.ts @@ -1,3 +1,4 @@ export * from "./ContextFactory"; +export * from "./Controller.interface"; export * from "./InfrastructureError"; export * from "./mappers"; diff --git a/server/src/infrastructure/express/api/v1.ts b/server/src/infrastructure/express/api/v1.ts index fa1e681..12c1de3 100644 --- a/server/src/infrastructure/express/api/v1.ts +++ b/server/src/infrastructure/express/api/v1.ts @@ -1,12 +1,45 @@ -import { catalogRouter } from "@/contexts/catalog/infrastructure/express/catalogRoutes"; -import express from "express"; +import { AuthRouter } from "@/contexts/auth"; +import { CatalogRouter } from "@/contexts/catalog"; +import { RepositoryManager } from "@/contexts/common/domain"; +import { createSequelizeAdapter } from "@/contexts/common/infrastructure/sequelize"; +import Express from "express"; -const v1Router = express.Router({ mergeParams: true }); +export const v1Routes = () => { + const routes = Express.Router({ mergeParams: true }); -v1Router.get("/hello", (req, res) => { - res.send("Hello world!"); -}); + routes.get("/hello", (req, res) => { + res.send("Hello world!"); + }); -v1Router.use("/catalog", catalogRouter); + //v1Routes.use("/auth", authRoutes); + //v1Routes.use("/catalog", catalogRoutes); -export { v1Router }; + routes.use( + ( + req: Express.Request, + res: Express.Response, + next: Express.NextFunction, + ) => { + res.locals["context"] = { + adapter: createSequelizeAdapter(), + repositoryManager: RepositoryManager.getInstance(), + services: {}, + }; + + res.locals["middlewares"] = new Map(); + + return next(); + }, + ); + routes.use((req, res, next) => { + console.log( + `[${new Date().toLocaleTimeString()}] Incoming request to ${req.path}`, + ); + next(); + }); + + AuthRouter(routes); + CatalogRouter(routes); + + return routes; +}; diff --git a/server/src/infrastructure/express/app.ts b/server/src/infrastructure/express/app.ts index 103da96..cf6b777 100644 --- a/server/src/infrastructure/express/app.ts +++ b/server/src/infrastructure/express/app.ts @@ -4,9 +4,11 @@ import express from "express"; import helmet from "helmet"; import responseTime from "response-time"; +import { configurePassportAuth } from "@/contexts/auth"; import morgan from "morgan"; +import passport from "passport"; import { initLogger } from "../logger"; -import { v1Router } from "./api/v1"; +import { v1Routes } from "./api/v1"; const logger = initLogger(rTracer); @@ -39,7 +41,7 @@ app.use( "Pagination-Page", "Pagination-Limit", ], - }) + }), ); // secure apps by setting various HTTP headers @@ -49,9 +51,13 @@ app.use(helmet()); //app.use(morgan('common')); app.use(morgan("dev")); +// Autentication +app.use(passport.initialize()); +configurePassportAuth(passport); + // Express configuration app.set("port", process.env.PORT ?? 3000); -app.use("/api/v1", v1Router); +app.use("/api/v1", v1Routes()); -export default app; +export { app }; diff --git a/server/src/infrastructure/http/server.ts b/server/src/infrastructure/http/server.ts index a7850ac..a6aeeb6 100644 --- a/server/src/infrastructure/http/server.ts +++ b/server/src/infrastructure/http/server.ts @@ -7,7 +7,7 @@ import { DateTime, Settings } from "luxon"; import { createSequelizeAdapter } from "@/contexts/common/infrastructure/sequelize"; import { trace } from "console"; import { config } from "../../config"; -import app from "../express/app"; +import { app } from "../express/app"; import { initLogger } from "../logger"; process.env.TZ = "UTC"; @@ -25,7 +25,7 @@ export const currentState = assign( environment: config.enviroment, connections: {}, }, - config + config, ); const serverStop = (server: http.Server) => { @@ -36,7 +36,7 @@ const serverStop = (server: http.Server) => { setTimeout(() => { logger.error( - "Could not close connections in time, forcefully shutting down" + "Could not close connections in time, forcefully shutting down", ); resolve(); }, forceTimeout).unref(); @@ -68,7 +68,7 @@ const serverError = (error: any) => { if (error.code === "EADDRINUSE") { logger.debug(`⛔️ Server wasn't able to start properly.`); logger.error( - `The port ${error.port} is already used by another application.` + `The port ${error.port} is already used by another application.`, ); } else { logger.debug(`⛔️ Server wasn't able to start properly.`); @@ -99,12 +99,12 @@ const server: http.Server = http process.on("SIGINT", () => { //firebirdConn.disconnect(); serverStop(server); - }) + }), ) .on("close", () => logger.info( - `Shut down at: ${DateTime.now().toLocaleString(DateTime.DATETIME_FULL)}` - ) + `Shut down at: ${DateTime.now().toLocaleString(DateTime.DATETIME_FULL)}`, + ), ) .on("connection", serverConnection) .on("error", serverError); @@ -116,16 +116,16 @@ try { server.listen(currentState.server.port, () => { const now = DateTime.now(); logger.info( - `Time: ${now.toLocaleString(DateTime.DATETIME_FULL)} ${now.zoneName}` + `Time: ${now.toLocaleString(DateTime.DATETIME_FULL)} ${now.zoneName}`, ); logger.info( - `Launched in: ${now.diff(currentState.launchedAt).toMillis()} ms` + `Launched in: ${now.diff(currentState.launchedAt).toMillis()} ms`, ); logger.info(`Environment: ${currentState.environment}`); logger.info(`Process PID: ${process.pid}`); logger.info("To shut down your server, press + C at any time"); logger.info( - `⚡️ Server: http://${currentState.server.hostname}:${currentState.server.port}` + `⚡️ Server: http://${currentState.server.hostname}:${currentState.server.port}`, ); }); }); diff --git a/shared/lib/contexts/auth/application/User.service.ts b/shared/lib/contexts/auth/application/User.service.ts new file mode 100644 index 0000000..96863f2 --- /dev/null +++ b/shared/lib/contexts/auth/application/User.service.ts @@ -0,0 +1,10 @@ +import { UndefinedOr } from "../../../utilities"; +import { Email, Result } from "../../common"; + +export const ensureUserEmailIsValid = (value: UndefinedOr) => { + const valueOrError = Email.create(value); + + return valueOrError.isSuccess + ? Result.ok(valueOrError.object) + : Result.fail(valueOrError.error); +}; diff --git a/shared/lib/contexts/auth/application/dto/ILogin.dto.ts b/shared/lib/contexts/auth/application/dto/ILogin.dto.ts new file mode 100644 index 0000000..62d263e --- /dev/null +++ b/shared/lib/contexts/auth/application/dto/ILogin.dto.ts @@ -0,0 +1,24 @@ +import Joi from "joi"; +import { Result, RuleValidator } from "../../../common"; + +export interface ILogin_DTO { + email: string; + password: string; +} + +export function ensureLogin_DTOIsValid( + loginDTO: ILogin_DTO, +): Result { + const schema = Joi.object({ + email: Joi.string().email().required(), + password: Joi.string().min(4).alphanum().required(), + }); + + let result = RuleValidator.validate(schema, loginDTO); + + if (result.isFailure) { + return Result.fail(result.error); + } + + return Result.ok(true); +} diff --git a/shared/lib/contexts/auth/application/dto/ILogin_Response.dto.ts b/shared/lib/contexts/auth/application/dto/ILogin_Response.dto.ts new file mode 100644 index 0000000..a30712c --- /dev/null +++ b/shared/lib/contexts/auth/application/dto/ILogin_Response.dto.ts @@ -0,0 +1,3 @@ +export interface ILogin_Response_DTO { + token: string; +} diff --git a/shared/lib/contexts/auth/application/dto/index.ts b/shared/lib/contexts/auth/application/dto/index.ts new file mode 100644 index 0000000..00a98d9 --- /dev/null +++ b/shared/lib/contexts/auth/application/dto/index.ts @@ -0,0 +1,2 @@ +export * from "./ILogin.dto"; +export * from "./ILogin_Response.dto"; diff --git a/shared/lib/contexts/auth/application/index.ts b/shared/lib/contexts/auth/application/index.ts new file mode 100644 index 0000000..ba9e164 --- /dev/null +++ b/shared/lib/contexts/auth/application/index.ts @@ -0,0 +1,2 @@ +export * from "./User.service"; +export * from "./dto"; diff --git a/shared/lib/contexts/auth/index.ts b/shared/lib/contexts/auth/index.ts new file mode 100644 index 0000000..f4fe054 --- /dev/null +++ b/shared/lib/contexts/auth/index.ts @@ -0,0 +1 @@ +export * from "./application"; diff --git a/shared/lib/contexts/index.ts b/shared/lib/contexts/index.ts index c57efc9..24c3852 100644 --- a/shared/lib/contexts/index.ts +++ b/shared/lib/contexts/index.ts @@ -1,2 +1,3 @@ +export * from "./auth"; export * from "./catalog"; export * from "./common";