From 7cd155f33103b95ec98b42a3b33003ffba0b293a Mon Sep 17 00:00:00 2001 From: David Arranz Date: Mon, 20 May 2024 00:04:23 +0200 Subject: [PATCH] . --- .prettierc.json | 13 ++- .vscode/settings.json | 1 - server/src/config/environments/development.ts | 4 +- .../application/FindUserByEmail.useCase.ts | 7 +- .../auth/application/Login.useCase.ts | 11 +- .../controllers/login/Login.controller.ts | 7 +- .../infrastructure/mappers/user.mapper.ts | 8 +- .../application/ListArticlesUseCase.ts | 3 +- .../application/useCases/UseCaseError.ts | 14 +-- .../infrastructure/InfrastructureError.ts | 56 ++++----- .../express/ExpressErrorResponse.ts | 2 +- .../infrastructure/express/middlewares.ts | 6 +- .../sequelize/SequelizeAdapter.ts | 2 +- .../sequelize/SequelizeModel.interface.ts | 5 +- .../sequelize/SequelizeRepository.ts | 6 +- .../users/application/CreateUser.useCase.ts | 102 ++++++++-------- .../users/application/DeleteUser.useCase.ts | 3 +- .../users/application/GetUser.useCase.ts | 5 +- .../users/application/ListUsers.useCase.ts | 3 +- .../users/application/UpdateUser.useCase.ts | 31 ++++- .../domain/entities/User.specifications.ts | 9 ++ .../contexts/users/domain/entities/User.ts | 51 +++----- .../repository/UserRepository.interface.ts | 3 + .../users/infrastructure/User.repository.ts | 14 +++ .../createUser/CreateUser.controller.ts | 32 ++--- .../deleteUser/DeleteUser.controller.ts | 7 +- .../controllers/getUser/GetUser.controller.ts | 7 +- .../updateUser/UpdateUser.controller.ts | 11 +- .../infrastructure/mappers/user.mapper.ts | 10 +- .../express/api/context.middleware.ts | 8 ++ server/src/infrastructure/express/api/v1.ts | 16 +-- .../common/domain/entities/Password.ts | 110 ++++++++++++++++++ .../contexts/common/domain/entities/index.ts | 1 + .../CreateUser.dto/ICreateUser_Request.dto.ts | 4 +- 34 files changed, 354 insertions(+), 218 deletions(-) create mode 100644 server/src/contexts/users/domain/entities/User.specifications.ts create mode 100644 server/src/infrastructure/express/api/context.middleware.ts create mode 100644 shared/lib/contexts/common/domain/entities/Password.ts diff --git a/.prettierc.json b/.prettierc.json index e98db94..59b8ce8 100644 --- a/.prettierc.json +++ b/.prettierc.json @@ -1,10 +1,11 @@ { - "semi": true, - "printWidth": 80, + "printWidth": 130, + "tabWidth": 4, "useTabs": false, - "endOfLine": "auto", - - "trailingComma": "all", + "semi": true, "singleQuote": false, - "bracketSpacing": true + "trailingComma": "all", + "bracketSpacing": true, + "jsxBracketSameLine": true, + "arrowParens": "always" } diff --git a/.vscode/settings.json b/.vscode/settings.json index 6c15390..e960ecf 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,5 +1,4 @@ { - //"typescript.surveys.enabled": false, "editor.codeActionsOnSave": { "source.organizeImports": "explicit", "source.fixAll.eslint": "explicit" diff --git a/server/src/config/environments/development.ts b/server/src/config/environments/development.ts index f63a944..ebc0725 100644 --- a/server/src/config/environments/development.ts +++ b/server/src/config/environments/development.ts @@ -4,8 +4,8 @@ module.exports = { "9d6c903873c341816995a8be0355c6f0d6d471fc6aedacf50790e9b1e49c45b3", refresh_secret_key: "3972dc40c69327b65352ed097419213b0b75561169dba562410b85660bb1f305", - token_expiration: "5m", - refresh_token_expiration: "7d", + token_expiration: "7d", + refresh_token_expiration: "30d", }, database: { diff --git a/server/src/contexts/auth/application/FindUserByEmail.useCase.ts b/server/src/contexts/auth/application/FindUserByEmail.useCase.ts index b60dcc6..7927f6e 100644 --- a/server/src/contexts/auth/application/FindUserByEmail.useCase.ts +++ b/server/src/contexts/auth/application/FindUserByEmail.useCase.ts @@ -3,7 +3,6 @@ import { IUseCaseError, IUseCaseRequest, UseCaseError, - handleUseCaseError, } from "@/contexts/common/application"; import { IRepositoryManager } from "@/contexts/common/domain"; import { IInfrastructureError } from "@/contexts/common/infrastructure"; @@ -49,7 +48,7 @@ export class FindUserByEmailUseCase const emailOrError = ensureEmailIsValid(email); if (emailOrError.isFailure) { return Result.fail( - handleUseCaseError( + UseCaseError.create( UseCaseError.INVALID_INPUT_DATA, "Email or password is not valid", emailOrError.error, @@ -66,7 +65,7 @@ export class FindUserByEmailUseCase if (user === null) { return Result.fail( - handleUseCaseError( + UseCaseError.create( UseCaseError.NOT_FOUND_ERROR, `User with email ${email} not found`, ), @@ -76,7 +75,7 @@ export class FindUserByEmailUseCase } catch (error: unknown) { const _error = error as IInfrastructureError; return Result.fail( - handleUseCaseError( + UseCaseError.create( UseCaseError.REPOSITORY_ERROR, "Error al buscar el usuario", _error, diff --git a/server/src/contexts/auth/application/Login.useCase.ts b/server/src/contexts/auth/application/Login.useCase.ts index 6624fc7..dba5a11 100644 --- a/server/src/contexts/auth/application/Login.useCase.ts +++ b/server/src/contexts/auth/application/Login.useCase.ts @@ -2,12 +2,11 @@ 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 { ILogin_DTO, Result, ensureUserEmailIsValid } from "@shared/contexts"; +import { ILogin_DTO, Result, ensureEmailIsValid } from "@shared/contexts"; import { AuthUser } from "../domain"; import { findUserByEmail } from "./authServices"; @@ -38,10 +37,10 @@ export class LoginUseCase // Validaciones de datos - const emailOrError = ensureUserEmailIsValid(email); + const emailOrError = ensureEmailIsValid(email); if (emailOrError.isFailure) { return Result.fail( - handleUseCaseError( + UseCaseError.create( UseCaseError.INVALID_INPUT_DATA, "Email or password is not valid", emailOrError.error, @@ -58,7 +57,7 @@ export class LoginUseCase ); if (user === null || !user.verifyPassword(password)) { return Result.fail( - handleUseCaseError( + UseCaseError.create( UseCaseError.INVALID_INPUT_DATA, "Email or password is not valid", ), @@ -68,7 +67,7 @@ export class LoginUseCase } catch (error: unknown) { const _error = error as IInfrastructureError; return Result.fail( - handleUseCaseError( + UseCaseError.create( UseCaseError.REPOSITORY_ERROR, "Error al buscar el usuario", _error, diff --git a/server/src/contexts/auth/infrastructure/express/controllers/login/Login.controller.ts b/server/src/contexts/auth/infrastructure/express/controllers/login/Login.controller.ts index cd3839d..7b5ae7d 100644 --- a/server/src/contexts/auth/infrastructure/express/controllers/login/Login.controller.ts +++ b/server/src/contexts/auth/infrastructure/express/controllers/login/Login.controller.ts @@ -1,10 +1,7 @@ import { config } from "@/config"; import { AuthUser } from "@/contexts/auth/domain"; import { IServerError } from "@/contexts/common/domain/errors"; -import { - InfrastructureError, - handleInfrastructureError, -} from "@/contexts/common/infrastructure"; +import { InfrastructureError } from "@/contexts/common/infrastructure"; import { ExpressController } from "@/contexts/common/infrastructure/express"; import { ILogin_Response_DTO } from "@shared/contexts"; import JWT from "jsonwebtoken"; @@ -34,7 +31,7 @@ export class LoginController extends ExpressController { if (!user) { const errorMessage = "Unexpected missing user data"; - const infraError = handleInfrastructureError( + const infraError = InfrastructureError.create( InfrastructureError.UNEXCEPTED_ERROR, errorMessage, ); diff --git a/server/src/contexts/auth/infrastructure/mappers/user.mapper.ts b/server/src/contexts/auth/infrastructure/mappers/user.mapper.ts index c86cc42..01d002e 100644 --- a/server/src/contexts/auth/infrastructure/mappers/user.mapper.ts +++ b/server/src/contexts/auth/infrastructure/mappers/user.mapper.ts @@ -2,7 +2,7 @@ import { ISequelizeMapper, SequelizeMapper, } from "@/contexts/common/infrastructure"; -import { Email, Name, UniqueID } from "@shared/contexts"; +import { Email, Name, Password, UniqueID } from "@shared/contexts"; import { AuthUser, IAuthUserProps } from "../../domain/entities"; import { IAuthContext } from "../Auth.context"; import { @@ -29,7 +29,11 @@ class UserMapper const props: IAuthUserProps = { name: this.mapsValue(source, "name", Name.create), email: this.mapsValue(source, "email", Email.create), - hashed_password: source.password, + hashed_password: this.mapsValue( + source, + "password", + Password.createFromHashedText, + ), }; const id = this.mapsValue(source, "id", UniqueID.create); diff --git a/server/src/contexts/catalog/application/ListArticlesUseCase.ts b/server/src/contexts/catalog/application/ListArticlesUseCase.ts index 1b3b8ba..2bad7e3 100644 --- a/server/src/contexts/catalog/application/ListArticlesUseCase.ts +++ b/server/src/contexts/catalog/application/ListArticlesUseCase.ts @@ -2,7 +2,6 @@ import { IUseCase, IUseCaseError, UseCaseError, - handleUseCaseError, } from "@/contexts/common/application/useCases"; import { IRepositoryManager } from "@/contexts/common/domain"; import { @@ -67,7 +66,7 @@ export class ListArticlesUseCase } catch (error: unknown) { const _error = error as IInfrastructureError; return Result.fail( - handleUseCaseError( + UseCaseError.create( UseCaseError.REPOSITORY_ERROR, "Error al listar el catálogo", _error, diff --git a/server/src/contexts/common/application/useCases/UseCaseError.ts b/server/src/contexts/common/application/useCases/UseCaseError.ts index a3374df..b0a8827 100755 --- a/server/src/contexts/common/application/useCases/UseCaseError.ts +++ b/server/src/contexts/common/application/useCases/UseCaseError.ts @@ -2,6 +2,10 @@ import { IServerError, ServerError } from "../../domain/errors"; export interface IUseCaseError extends IServerError {} +export type UseCaseErrorDetails = { + path?: string; +} & Record; + export class UseCaseError extends ServerError implements IUseCaseError { public static readonly INVALID_REQUEST_PARAM = "INVALID_REQUEST_PARAM"; public static readonly INVALID_INPUT_DATA = "INVALID_INPUT_DATA"; @@ -13,16 +17,8 @@ export class UseCaseError extends ServerError implements IUseCaseError { public static create( code: string, message: string, - details?: Record, + details?: UseCaseErrorDetails, ): UseCaseError { return new UseCaseError(code, message, details); } } - -export function handleUseCaseError( - code: string, - message: string, - payload?: Record, -): IUseCaseError { - return UseCaseError.create(code, message, payload); -} diff --git a/server/src/contexts/common/infrastructure/InfrastructureError.ts b/server/src/contexts/common/infrastructure/InfrastructureError.ts index 0bf4a7a..8c2c494 100755 --- a/server/src/contexts/common/infrastructure/InfrastructureError.ts +++ b/server/src/contexts/common/infrastructure/InfrastructureError.ts @@ -18,42 +18,28 @@ export class InfrastructureError public static create( code: string, message: string, - payload?: Record, + error?: UseCaseError | ValidationError, ): InfrastructureError { + let payload = {}; + + if (error) { + if (error.name === "ValidationError") { + //Joi error => error.details + payload = (error).details; + } else { + // UseCaseError + const _error = error; + const _payload = Array.isArray(_error.payload) + ? _error.payload + : [_error.payload]; + + payload = _payload.map((item: Record) => ({ + path: item.path, + message: error.message, + })); + } + } + return new InfrastructureError(code, message, payload); } } - -function _isJoiError(error: Error) { - return error.name === "ValidationError"; -} - -export function handleInfrastructureError( - code: string, - message: string, - error?: Error, // UseCaseError | ValidationError -): IInfrastructureError { - let payload = {}; - - if (error) { - if (_isJoiError(error)) { - //Joi => error.details - payload = (error).details; - } else { - // UseCaseError - /*const useCaseError = error; - if (useCaseError.payload.path) { - const errorItem = {}; - errorItem[`${useCaseError.payload.path}`] = useCaseError.message; - payload = {+ - errors: [errorItem], - }; - }*/ - payload = (error).payload; - } - } - - console.log(payload); - - return InfrastructureError.create(code, message, payload); -} diff --git a/server/src/contexts/common/infrastructure/express/ExpressErrorResponse.ts b/server/src/contexts/common/infrastructure/express/ExpressErrorResponse.ts index 686f562..deb019d 100644 --- a/server/src/contexts/common/infrastructure/express/ExpressErrorResponse.ts +++ b/server/src/contexts/common/infrastructure/express/ExpressErrorResponse.ts @@ -51,7 +51,7 @@ function generateExpressError( if (item.path) { return item.path ? { - [String(item.path)]: useCaseError.message, + [String(item.path)]: item.message || useCaseError.message, } : {}; } else { diff --git a/server/src/contexts/common/infrastructure/express/middlewares.ts b/server/src/contexts/common/infrastructure/express/middlewares.ts index 911a801..0ae9cd2 100644 --- a/server/src/contexts/common/infrastructure/express/middlewares.ts +++ b/server/src/contexts/common/infrastructure/express/middlewares.ts @@ -33,4 +33,8 @@ function applyMiddleware(middlewares: string | Array) { }; } -export { applyMiddleware, registerMiddleware }; +function createMiddlewareMap() { + return new Map(); +} + +export { applyMiddleware, createMiddlewareMap, registerMiddleware }; diff --git a/server/src/contexts/common/infrastructure/sequelize/SequelizeAdapter.ts b/server/src/contexts/common/infrastructure/sequelize/SequelizeAdapter.ts index a7b272c..85c6f8a 100644 --- a/server/src/contexts/common/infrastructure/sequelize/SequelizeAdapter.ts +++ b/server/src/contexts/common/infrastructure/sequelize/SequelizeAdapter.ts @@ -73,7 +73,7 @@ export class SequelizeAdapter implements ISequelizeAdapter { return this._connection.sync(params); } - public getModel(modelName: string) { + public getModel(modelName: string): any { if (this.hasModel(modelName)) { return this._models[modelName]; } diff --git a/server/src/contexts/common/infrastructure/sequelize/SequelizeModel.interface.ts b/server/src/contexts/common/infrastructure/sequelize/SequelizeModel.interface.ts index 38a5e0e..d6a1d29 100644 --- a/server/src/contexts/common/infrastructure/sequelize/SequelizeModel.interface.ts +++ b/server/src/contexts/common/infrastructure/sequelize/SequelizeModel.interface.ts @@ -1,10 +1,11 @@ -import { Model, Sequelize } from "sequelize"; +import { Model, ModelStatic, Sequelize } from "sequelize"; -interface ISequelizeModel extends Model {} +interface ISequelizeModel extends InstanceType> {} interface ISequelizeModels { [prop: string]: ISequelizeModel; } + interface ISequelizeModel extends Model { associate?: (connection: Sequelize, models?: ISequelizeModels) => void; hooks?: (connection: Sequelize) => void; diff --git a/server/src/contexts/common/infrastructure/sequelize/SequelizeRepository.ts b/server/src/contexts/common/infrastructure/sequelize/SequelizeRepository.ts index 2e45cfc..1a1991d 100644 --- a/server/src/contexts/common/infrastructure/sequelize/SequelizeRepository.ts +++ b/server/src/contexts/common/infrastructure/sequelize/SequelizeRepository.ts @@ -86,10 +86,6 @@ export abstract class SequelizeRepository implements IRepository { queryCriteria, }); - if (!_model) { - throw new Error(`[SequelizeRepository] Model ${modelName} not found!`); - } - const args = { ...query, distinct: true, @@ -110,7 +106,7 @@ export abstract class SequelizeRepository implements IRepository { value: any, params: any = {}, ): Promise { - const _model = this.adapter.getModel(modelName); + const _model = this.adapter.getModel(modelName) as ModelDefined; const where = {}; where[field] = value; diff --git a/server/src/contexts/users/application/CreateUser.useCase.ts b/server/src/contexts/users/application/CreateUser.useCase.ts index ca222b9..61bd1d5 100644 --- a/server/src/contexts/users/application/CreateUser.useCase.ts +++ b/server/src/contexts/users/application/CreateUser.useCase.ts @@ -2,19 +2,21 @@ 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 { + DomainError, Email, ICreateUser_Request_DTO, IDomainError, Name, + Password, Result, UniqueID, + ensureEmailIsValid, ensureIdIsValid, - ensureUserEmailIsValid, } from "@shared/contexts"; import { IUserRepository, User } from "../domain"; import { existsUserByEmail, existsUserByID } from "./userServices"; @@ -38,27 +40,20 @@ export class CreateUserUseCase this._repositoryManager = props.repositoryManager; } - private getRepositoryByName(name: string) { - return this._repositoryManager.getRepository(name); - } + async execute(request: ICreateUser_Request_DTO) { + const { id, email } = request; - async execute( - request: ICreateUser_Request_DTO, - ): Promise { - const userDTO = request; - - const userRepository = this.getRepositoryByName("User"); + const userRepository = this._getUserRepository(); // Validaciones de datos - const userIdOrError = ensureIdIsValid(userDTO.id); + const userIdOrError = ensureIdIsValid(id); if (userIdOrError.isFailure) { + const message = userIdOrError.error.message; //`User ID ${userDTO.id} is not valid`; return Result.fail( - handleUseCaseError( - UseCaseError.INVALID_INPUT_DATA, - "User ID is not valid", - userIdOrError.error, - ), + UseCaseError.create(UseCaseError.INVALID_INPUT_DATA, message, [ + { path: "id" }, + ]), ); } @@ -68,19 +63,18 @@ export class CreateUserUseCase userRepository, ); if (idExists) { + const message = `Another user with ID ${id} exists`; return Result.fail( - handleUseCaseError( - UseCaseError.RESOURCE_ALREADY_EXITS, - `Another user with ID ${userDTO.id} exists`, - userIdOrError.error, - ), + UseCaseError.create(UseCaseError.RESOURCE_ALREADY_EXITS, message, { + path: "id", + }), ); } - const emailOrError = ensureUserEmailIsValid(userDTO.email); + const emailOrError = ensureEmailIsValid(email); if (emailOrError.isFailure) { return Result.fail( - handleUseCaseError( + UseCaseError.create( UseCaseError.INVALID_INPUT_DATA, "Email or password is not valid", emailOrError.error, @@ -96,17 +90,17 @@ export class CreateUserUseCase if (emailExists) { return Result.fail( - handleUseCaseError( + UseCaseError.create( UseCaseError.RESOURCE_ALREADY_EXITS, - `Another user with email ${userDTO.email} exists`, - userIdOrError.error, + `Another user with email ${email} exists`, + { path: "email" }, ), ); } // Crear user - const userOrError = this.tryCreateUserInstance( - userDTO, + const userOrError = this._tryCreateUserInstance( + request, userIdOrError.object, ); @@ -116,42 +110,50 @@ export class CreateUserUseCase let message = ""; switch (domainError.code) { - case User.ERROR_CUSTOMER_WITHOUT_NAME: - return handleInvalidInputDataFailure( - "El usuario debe ser una compañía o tener nombre y apellidos.", - domainError, + case User.ERROR_USER_WITHOUT_NAME: + return Result.fail( + UseCaseError.create( + UseCaseError.INVALID_INPUT_DATA, + "El usuario debe tener un nombre.", + domainError, + ), ); break; case DomainError.INVALID_INPUT_DATA: errorCode = UseCaseError.INVALID_INPUT_DATA; message = domainError.message; - return handleInvalidInputDataFailure( - "El usuario debe ser una compañía o tener nombre y apellidos.", - domainError, + return Result.fail( + UseCaseError.create( + UseCaseError.INVALID_INPUT_DATA, + "El usuario tiene algún dato erróneo.", + domainError, + ), ); break; default: errorCode = UseCaseError.UNEXCEPTED_ERROR; message = domainError.message; - return handleUseCaseError(errorCode, message, domainError); + return Result.fail( + UseCaseError.create(errorCode, message, domainError), + ); break; } } - return this.saveUser(userOrError.object); + return this._saveUser(userOrError.object); } - private async saveUser(user: User) { + private async _saveUser(user: User) { // Guardar el contacto const transaction = this._adapter.startTransaction(); - const userRepoBuilder = this.getRepositoryByName("User"); + const userRepository = this._getUserRepository(); let userRepo: IUserRepository; try { await transaction.complete(async (t) => { - userRepo = userRepoBuilder({ transaction: t }); + userRepo = userRepository({ transaction: t }); await userRepo.create(user); }); @@ -159,16 +161,12 @@ export class CreateUserUseCase } catch (error: unknown) { const _error = error as IInfrastructureError; return Result.fail( - handleUseCaseError( - UseCaseError.REPOSITORY_ERROR, - "Error al guardar el usuario", - _error, - ), + UseCaseError.create(UseCaseError.REPOSITORY_ERROR, _error.message), ); } } - private tryCreateUserInstance( + private _tryCreateUserInstance( userDTO: ICreateUser_Request_DTO, userId: UniqueID, ): Result { @@ -182,12 +180,22 @@ export class CreateUserUseCase return Result.fail(emailOrError.error); } + const passwordOrError = Password.createFromPlainText(userDTO.password); + if (passwordOrError.isFailure) { + return Result.fail(passwordOrError.error); + } + return User.create( { name: nameOrError.object, email: emailOrError.object, + password: passwordOrError.object, }, userId, ); } + + private _getUserRepository() { + return this._repositoryManager.getRepository("User"); + } } diff --git a/server/src/contexts/users/application/DeleteUser.useCase.ts b/server/src/contexts/users/application/DeleteUser.useCase.ts index e3f55dd..ae3aa68 100644 --- a/server/src/contexts/users/application/DeleteUser.useCase.ts +++ b/server/src/contexts/users/application/DeleteUser.useCase.ts @@ -3,7 +3,6 @@ import { IUseCaseError, IUseCaseRequest, UseCaseError, - handleUseCaseError, } from "@/contexts/common/application/useCases"; import { IRepositoryManager } from "@/contexts/common/domain"; import { ISequelizeAdapter } from "@/contexts/common/infrastructure/sequelize"; @@ -55,7 +54,7 @@ export class DeleteUserUseCase } catch (error: unknown) { //const _error = error as IInfrastructureError; return Result.fail( - handleUseCaseError( + UseCaseError.create( UseCaseError.REPOSITORY_ERROR, "Error al eliminar el usuario", ), diff --git a/server/src/contexts/users/application/GetUser.useCase.ts b/server/src/contexts/users/application/GetUser.useCase.ts index 4d688b6..6d4e971 100644 --- a/server/src/contexts/users/application/GetUser.useCase.ts +++ b/server/src/contexts/users/application/GetUser.useCase.ts @@ -3,7 +3,6 @@ import { IUseCaseError, IUseCaseRequest, UseCaseError, - handleUseCaseError, } from "@/contexts/common/application/useCases"; import { IRepositoryManager } from "@/contexts/common/domain"; import { ISequelizeAdapter } from "@/contexts/common/infrastructure/sequelize"; @@ -64,7 +63,7 @@ export class GetUserUseCase if (!user) { return Result.fail( - handleUseCaseError(UseCaseError.NOT_FOUND_ERROR, "User not found"), + UseCaseError.create(UseCaseError.NOT_FOUND_ERROR, "User not found"), ); } @@ -72,7 +71,7 @@ export class GetUserUseCase } catch (error: unknown) { const _error = error as IInfrastructureError; return Result.fail( - handleUseCaseError( + UseCaseError.create( UseCaseError.REPOSITORY_ERROR, "Error al consultar el usuario", _error, diff --git a/server/src/contexts/users/application/ListUsers.useCase.ts b/server/src/contexts/users/application/ListUsers.useCase.ts index 5b9a712..135a014 100644 --- a/server/src/contexts/users/application/ListUsers.useCase.ts +++ b/server/src/contexts/users/application/ListUsers.useCase.ts @@ -2,7 +2,6 @@ import { IUseCase, IUseCaseError, UseCaseError, - handleUseCaseError, } from "@/contexts/common/application/useCases"; import { IRepositoryManager } from "@/contexts/common/domain"; import { @@ -65,7 +64,7 @@ export class ListUsersUseCase } catch (error: unknown) { const _error = error as IInfrastructureError; return Result.fail( - handleUseCaseError( + UseCaseError.create( UseCaseError.REPOSITORY_ERROR, "Error al listar los usurios", _error, diff --git a/server/src/contexts/users/application/UpdateUser.useCase.ts b/server/src/contexts/users/application/UpdateUser.useCase.ts index 8da3c8f..063ae6f 100644 --- a/server/src/contexts/users/application/UpdateUser.useCase.ts +++ b/server/src/contexts/users/application/UpdateUser.useCase.ts @@ -3,7 +3,6 @@ import { IUseCaseError, IUseCaseRequest, UseCaseError, - handleUseCaseError, } from "@/contexts/common/application/useCases"; import { IRepositoryManager } from "@/contexts/common/domain"; import { ISequelizeAdapter } from "@/contexts/common/infrastructure/sequelize"; @@ -15,6 +14,7 @@ import { DomainError, IDomainError, IUpdateUser_DTO, + IUpdateUser_Request_DTO, Note, PostalCode, Province, @@ -27,6 +27,7 @@ import { UserName, UserPhone, UserTIN, + ensureIdIsValid, } from "@shared/contexts"; import { IInfrastructureError } from "@/contexts/common/infrastructure"; @@ -40,10 +41,11 @@ import { UserAddressType, UserShippingAddress, } from "../domain"; +import { existsUserByID } from "./userServices"; export interface IUpdateUserUseCaseRequest extends IUseCaseRequest { id: UniqueID; - userDTO: IUpdateUser_DTO; + userDTO: IUpdateUser_Request_DTO; } export type UpdateUserResponseOrError = @@ -73,13 +75,30 @@ export class UpdateUserUseCase request: IUpdateUserUseCaseRequest, ): Promise { const { id, userDTO } = request; + const userRepository = this.getRepositoryByName("User"); + + // Validaciones de datos + const userIdOrError = ensureIdIsValid(userDTO.id); + if (userIdOrError.isFailure) { + return Result.fail( + UseCaseError.create( + UseCaseError.INVALID_INPUT_DATA, + "User ID is not valid", + userIdOrError.error, + ), + ); + } // Comprobar que existe el user - const idExists = await this._findUserID(id); + const idExists = await existsUserByID( + userIdOrError.object, + this._adapter, + userRepository, + ); if (!idExists) { const message = `User with ID ${id.toString()} not found`; return Result.fail( - handleUseCaseError(UseCaseError.NOT_FOUND_ERROR, message, [ + UseCaseError.create(UseCaseError.NOT_FOUND_ERROR, message, [ { path: "id" }, ]), ); @@ -117,7 +136,7 @@ export class UpdateUserUseCase } return Result.fail( - handleUseCaseError(errorCode, message, payload), + UseCaseError.create(errorCode, message, payload), ); } @@ -326,7 +345,7 @@ export class UpdateUserUseCase } catch (error: unknown) { const _error = error as IInfrastructureError; return Result.fail( - handleUseCaseError( + UseCaseError.create( UseCaseError.REPOSITORY_ERROR, "Error al guardar el usuario", _error, diff --git a/server/src/contexts/users/domain/entities/User.specifications.ts b/server/src/contexts/users/domain/entities/User.specifications.ts new file mode 100644 index 0000000..d163900 --- /dev/null +++ b/server/src/contexts/users/domain/entities/User.specifications.ts @@ -0,0 +1,9 @@ +import { CompositeSpecification } from "@/contexts/common/domain"; + +import { User } from "./User"; + +export class UserHasName extends CompositeSpecification { + public isSatisfiedBy(candidate: User): boolean { + return !candidate.name.isEmpty(); + } +} diff --git a/server/src/contexts/users/domain/entities/User.ts b/server/src/contexts/users/domain/entities/User.ts index c85119c..261ff61 100644 --- a/server/src/contexts/users/domain/entities/User.ts +++ b/server/src/contexts/users/domain/entities/User.ts @@ -5,22 +5,25 @@ import { Email, IDomainError, Name, + Password, Result, UniqueID, + handleDomainError, } from "@shared/contexts"; +import { UserHasName } from "./User.specifications"; export interface IUserProps { name: Name; email: Email; - password?: string; - hashed_password?: string; + password: Password; } +//type ISecuredUserProps = Omit; + export interface IUser { id: UniqueID; name: Name; email: Email; - hashed_password: string; isUser: boolean; isAdmin: boolean; @@ -28,33 +31,29 @@ export interface IUser { } export class User extends AggregateRoot implements IUser { + static readonly ERROR_USER_WITHOUT_NAME = "ERROR_USER_WITHOUT_NAME"; + 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); + // Reglas de negocio / validaciones + const isValidUser = new UserHasName().isSatisfiedBy(user); + + if (!isValidUser) { + return Result.fail(handleDomainError(User.ERROR_USER_WITHOUT_NAME)); + } + return Result.ok(user); } public static async hashPassword(password): Promise { - return hashPassword(password, await genSalt()); + return Password.hashPassword(password); } - private _hashed_password: string; - - private constructor(props: IUserProps, id?: UniqueID) { - super({ ...props, password: "", hashed_password: "" }, id); - - this._protectPassword(props); - } + private _password: string; get name(): Name { return this.props.name; @@ -65,7 +64,7 @@ export class User extends AggregateRoot implements IUser { } get hashed_password(): string { - return this._hashed_password; + return this._password; } get isUser(): boolean { @@ -77,17 +76,7 @@ export class User extends AggregateRoot implements IUser { } 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!; - } + return bCrypt.compareSync(candidatePassword, this._password!); } } @@ -108,5 +97,3 @@ async function hashPassword(password: string, salt: string): Promise { }); }); } - -User.hashPassword("123456").then((value) => console.log(value)); diff --git a/server/src/contexts/users/domain/repository/UserRepository.interface.ts b/server/src/contexts/users/domain/repository/UserRepository.interface.ts index f037038..1fc8ff1 100644 --- a/server/src/contexts/users/domain/repository/UserRepository.interface.ts +++ b/server/src/contexts/users/domain/repository/UserRepository.interface.ts @@ -3,6 +3,9 @@ import { Email, ICollection, IQueryCriteria, UniqueID } from "@shared/contexts"; import { User } from "../entities"; export interface IUserRepository extends IRepository { + create(user: User): Promise; + update(user: User): Promise; + getById(id: UniqueID): Promise; findUserByEmail(email: Email): Promise; findAll(queryCriteria?: IQueryCriteria): Promise>; diff --git a/server/src/contexts/users/infrastructure/User.repository.ts b/server/src/contexts/users/infrastructure/User.repository.ts index ee2a644..3bef551 100644 --- a/server/src/contexts/users/infrastructure/User.repository.ts +++ b/server/src/contexts/users/infrastructure/User.repository.ts @@ -30,6 +30,20 @@ export class UserRepository this.mapper = mapper; } + public async create(user: User): Promise { + const userData = this.mapper.mapToPersistence(user); + await this._save("User_Model", user.id, userData); + } + + public async update(user: User): Promise { + const userData = this.mapper.mapToPersistence(user); + + // borrando y luego creando + await this.removeById(user.id, true); + + await this._save("User_Model", user.id, userData, {}); + } + public async getById(id: UniqueID): Promise { const rawUser: any = await this._getById("User_Model", id); diff --git a/server/src/contexts/users/infrastructure/express/controllers/createUser/CreateUser.controller.ts b/server/src/contexts/users/infrastructure/express/controllers/createUser/CreateUser.controller.ts index 04f8f45..6f2a331 100644 --- a/server/src/contexts/users/infrastructure/express/controllers/createUser/CreateUser.controller.ts +++ b/server/src/contexts/users/infrastructure/express/controllers/createUser/CreateUser.controller.ts @@ -3,13 +3,9 @@ import { IServerError } from "@/contexts/common/domain/errors"; import { IInfrastructureError, InfrastructureError, - handleInfrastructureError, } from "@/contexts/common/infrastructure"; import { ExpressController } from "@/contexts/common/infrastructure/express"; -import { - CreateUserResponseOrError, - CreateUserUseCase, -} from "@/contexts/users/application"; +import { CreateUserUseCase } from "@/contexts/users/application"; import { User } from "@/contexts/users/domain"; import { ICreateUser_Request_DTO, @@ -48,7 +44,7 @@ export class CreateUserController extends ExpressController { if (userDTOOrError.isFailure) { const errorMessage = "User data not valid"; - const infraError = handleInfrastructureError( + const infraError = InfrastructureError.create( InfrastructureError.INVALID_INPUT_DATA, errorMessage, userDTOOrError.error, @@ -57,9 +53,7 @@ export class CreateUserController extends ExpressController { } // Llamar al caso de uso - const result: CreateUserResponseOrError = await this.useCase.execute( - userDTO, - ); + const result = await this.useCase.execute(userDTO); if (result.isFailure) { return this._handleExecuteError(result.error); @@ -82,7 +76,7 @@ export class CreateUserController extends ExpressController { switch (error.code) { case UseCaseError.INVALID_INPUT_DATA: errorMessage = "User data not valid"; - infraError = handleInfrastructureError( + infraError = InfrastructureError.create( InfrastructureError.INVALID_INPUT_DATA, errorMessage, error, @@ -93,18 +87,28 @@ export class CreateUserController extends ExpressController { case UseCaseError.RESOURCE_ALREADY_EXITS: errorMessage = "User already exists"; - infraError = handleInfrastructureError( - InfrastructureError.INVALID_INPUT_DATA, + infraError = InfrastructureError.create( + InfrastructureError.RESOURCE_ALREADY_REGISTERED, errorMessage, error, ); - return this.conflictError(error.message, error); + return this.conflictError(errorMessage, infraError); + break; + + case UseCaseError.REPOSITORY_ERROR: + errorMessage = "Error saving user"; + infraError = InfrastructureError.create( + InfrastructureError.UNEXCEPTED_ERROR, + errorMessage, + error, + ); + return this.conflictError(errorMessage, infraError); break; case UseCaseError.UNEXCEPTED_ERROR: errorMessage = error.message; - infraError = handleInfrastructureError( + infraError = InfrastructureError.create( InfrastructureError.UNEXCEPTED_ERROR, errorMessage, error, diff --git a/server/src/contexts/users/infrastructure/express/controllers/deleteUser/DeleteUser.controller.ts b/server/src/contexts/users/infrastructure/express/controllers/deleteUser/DeleteUser.controller.ts index 9ed7149..13b2416 100644 --- a/server/src/contexts/users/infrastructure/express/controllers/deleteUser/DeleteUser.controller.ts +++ b/server/src/contexts/users/infrastructure/express/controllers/deleteUser/DeleteUser.controller.ts @@ -6,7 +6,6 @@ import { IServerError } from "@/contexts/common/domain/errors"; import { IInfrastructureError, InfrastructureError, - handleInfrastructureError, } from "@/contexts/common/infrastructure"; import { ExpressController } from "@/contexts/common/infrastructure/express"; import { DeleteUserUseCase } from "@/contexts/users/application"; @@ -33,7 +32,7 @@ export class DeleteUserController extends ExpressController { const userIdOrError = ensureIdIsValid(userId); if (userIdOrError.isFailure) { const errorMessage = "User ID is not valid"; - const infraError = handleInfrastructureError( + const infraError = InfrastructureError.create( InfrastructureError.INVALID_INPUT_DATA, errorMessage, userIdOrError.error, @@ -63,7 +62,7 @@ export class DeleteUserController extends ExpressController { case UseCaseError.NOT_FOUND_ERROR: errorMessage = "User not found"; - infraError = handleInfrastructureError( + infraError = InfrastructureError.create( InfrastructureError.RESOURCE_NOT_FOUND_ERROR, errorMessage, error, @@ -75,7 +74,7 @@ export class DeleteUserController extends ExpressController { case UseCaseError.UNEXCEPTED_ERROR: errorMessage = error.message; - infraError = handleInfrastructureError( + infraError = InfrastructureError.create( InfrastructureError.UNEXCEPTED_ERROR, errorMessage, error, diff --git a/server/src/contexts/users/infrastructure/express/controllers/getUser/GetUser.controller.ts b/server/src/contexts/users/infrastructure/express/controllers/getUser/GetUser.controller.ts index 98ff505..fad1d6a 100644 --- a/server/src/contexts/users/infrastructure/express/controllers/getUser/GetUser.controller.ts +++ b/server/src/contexts/users/infrastructure/express/controllers/getUser/GetUser.controller.ts @@ -11,7 +11,6 @@ import { IServerError } from "@/contexts/common/domain/errors"; import { IInfrastructureError, InfrastructureError, - handleInfrastructureError, } from "@/contexts/common/infrastructure"; import { IUserContext } from "../../../User.context"; import { IGetUserPresenter } from "./presenter"; @@ -43,7 +42,7 @@ export class GetUserController extends ExpressController { const userIdOrError = ensureIdIsValid(userId); if (userIdOrError.isFailure) { const errorMessage = "User ID is not valid"; - const infraError = handleInfrastructureError( + const infraError = InfrastructureError.create( InfrastructureError.INVALID_INPUT_DATA, errorMessage, userIdOrError.error, @@ -78,7 +77,7 @@ export class GetUserController extends ExpressController { case UseCaseError.NOT_FOUND_ERROR: errorMessage = "User not found"; - infraError = handleInfrastructureError( + infraError = InfrastructureError.create( InfrastructureError.RESOURCE_NOT_FOUND_ERROR, errorMessage, error, @@ -90,7 +89,7 @@ export class GetUserController extends ExpressController { case UseCaseError.UNEXCEPTED_ERROR: errorMessage = error.message; - infraError = handleInfrastructureError( + infraError = InfrastructureError.create( InfrastructureError.UNEXCEPTED_ERROR, errorMessage, error, diff --git a/server/src/contexts/users/infrastructure/express/controllers/updateUser/UpdateUser.controller.ts b/server/src/contexts/users/infrastructure/express/controllers/updateUser/UpdateUser.controller.ts index c3af028..569d734 100644 --- a/server/src/contexts/users/infrastructure/express/controllers/updateUser/UpdateUser.controller.ts +++ b/server/src/contexts/users/infrastructure/express/controllers/updateUser/UpdateUser.controller.ts @@ -3,7 +3,6 @@ import { IServerError } from "@/contexts/common/domain/errors"; import { IInfrastructureError, InfrastructureError, - handleInfrastructureError, } from "@/contexts/common/infrastructure"; import { ExpressController } from "@/contexts/common/infrastructure/express"; import { @@ -49,7 +48,7 @@ export class UpdateUserController extends ExpressController { const userIdOrError = ensureIdIsValid(userId); if (userIdOrError.isFailure) { const errorMessage = "User ID is not valid"; - const infraError = handleInfrastructureError( + const infraError = InfrastructureError.create( InfrastructureError.INVALID_INPUT_DATA, errorMessage, userIdOrError.error, @@ -62,7 +61,7 @@ export class UpdateUserController extends ExpressController { if (userDTOOrError.isFailure) { const errorMessage = "User data not valid"; - const infraError = handleInfrastructureError( + const infraError = InfrastructureError.create( InfrastructureError.INVALID_INPUT_DATA, errorMessage, userDTOOrError.error, @@ -98,7 +97,7 @@ export class UpdateUserController extends ExpressController { case UseCaseError.NOT_FOUND_ERROR: errorMessage = "User not found"; - infraError = handleInfrastructureError( + infraError = InfrastructureError.create( InfrastructureError.RESOURCE_NOT_FOUND_ERROR, errorMessage, error, @@ -110,7 +109,7 @@ export class UpdateUserController extends ExpressController { case UseCaseError.INVALID_INPUT_DATA: errorMessage = "User data not valid"; - infraError = handleInfrastructureError( + infraError = InfrastructureError.create( InfrastructureError.INVALID_INPUT_DATA, "Datos del cliente a actulizar erróneos", error, @@ -121,7 +120,7 @@ export class UpdateUserController extends ExpressController { case UseCaseError.UNEXCEPTED_ERROR: errorMessage = error.message; - infraError = handleInfrastructureError( + infraError = InfrastructureError.create( InfrastructureError.UNEXCEPTED_ERROR, errorMessage, error, diff --git a/server/src/contexts/users/infrastructure/mappers/user.mapper.ts b/server/src/contexts/users/infrastructure/mappers/user.mapper.ts index 460b9fa..6946c7c 100644 --- a/server/src/contexts/users/infrastructure/mappers/user.mapper.ts +++ b/server/src/contexts/users/infrastructure/mappers/user.mapper.ts @@ -2,7 +2,7 @@ import { ISequelizeMapper, SequelizeMapper, } from "@/contexts/common/infrastructure"; -import { Email, Name, UniqueID } from "@shared/contexts"; +import { Email, Name, Password, UniqueID } from "@shared/contexts"; import { IUserProps, User } from "../../domain"; import { IUserContext } from "../User.context"; import { TCreationUser_Attributes, User_Model } from "../sequelize/user.model"; @@ -22,7 +22,11 @@ class UserMapper const props: IUserProps = { name: this.mapsValue(source, "name", Name.create), email: this.mapsValue(source, "email", Email.create), - hashed_password: source.password, + password: this.mapsValue( + source, + "password", + Password.createFromHashedText, + ), }; const id = this.mapsValue(source, "id", UniqueID.create); @@ -43,7 +47,7 @@ class UserMapper id: source.id.toPrimitive(), name: source.name.toPrimitive(), email: source.email.toPrimitive(), - password: source.hashed_password, + password: "", }; } } diff --git a/server/src/infrastructure/express/api/context.middleware.ts b/server/src/infrastructure/express/api/context.middleware.ts new file mode 100644 index 0000000..ab1a8d6 --- /dev/null +++ b/server/src/infrastructure/express/api/context.middleware.ts @@ -0,0 +1,8 @@ +import { RepositoryManager } from "@/contexts/common/domain"; +import { createSequelizeAdapter } from "@/contexts/common/infrastructure/sequelize"; + +export const createContextMiddleware = () => ({ + adapter: createSequelizeAdapter(), + repositoryManager: RepositoryManager.getInstance(), + services: {}, +}); diff --git a/server/src/infrastructure/express/api/v1.ts b/server/src/infrastructure/express/api/v1.ts index 5318571..16f5028 100644 --- a/server/src/infrastructure/express/api/v1.ts +++ b/server/src/infrastructure/express/api/v1.ts @@ -1,9 +1,9 @@ import { AuthRouter } from "@/contexts/auth"; import { CatalogRouter } from "@/contexts/catalog"; -import { RepositoryManager } from "@/contexts/common/domain"; -import { createSequelizeAdapter } from "@/contexts/common/infrastructure/sequelize"; +import { createMiddlewareMap } from "@/contexts/common/infrastructure/express"; import { UserRouter } from "@/contexts/users"; import Express from "express"; +import { createContextMiddleware } from "./context.middleware"; export const v1Routes = () => { const routes = Express.Router({ mergeParams: true }); @@ -12,22 +12,14 @@ export const v1Routes = () => { res.send("Hello world!"); }); - //v1Routes.use("/auth", authRoutes); - //v1Routes.use("/catalog", catalogRoutes); - 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(); + res.locals["context"] = createContextMiddleware(); + res.locals["middlewares"] = createMiddlewareMap(); return next(); }, diff --git a/shared/lib/contexts/common/domain/entities/Password.ts b/shared/lib/contexts/common/domain/entities/Password.ts new file mode 100644 index 0000000..4c6f085 --- /dev/null +++ b/shared/lib/contexts/common/domain/entities/Password.ts @@ -0,0 +1,110 @@ +import bCrypt from "bcryptjs"; + +import Joi from "joi"; +import { UndefinedOr } from "../../../../utilities"; +import { RuleValidator } from "../RuleValidator"; +import { DomainError, handleDomainError } from "../errors"; +import { Result } from "./Result"; +import { + IStringValueObjectOptions, + StringValueObject, +} from "./StringValueObject"; + +export interface IPasswordOptions extends IStringValueObjectOptions {} + +export class Password extends StringValueObject { + private static readonly MIN_LENGTH = 4; + private static readonly MAX_LENGTH = 255; + + protected static validate( + value: UndefinedOr, + options: IPasswordOptions, + ) { + const rule = Joi.string() + .allow(null) + .allow("") + .default("") + .trim() + .min(Password.MIN_LENGTH) + .max(Password.MAX_LENGTH) + .label(options.label ? options.label : "value"); + + return RuleValidator.validate(rule, value); + } + + public static createFromHashedText( + value: UndefinedOr, + options: IPasswordOptions = {}, + ) { + const _options = { + label: "password", + ...options, + }; + + const validationResult = Password.validate(value, _options); + + if (validationResult.isFailure) { + return Result.fail( + handleDomainError( + DomainError.INVALID_INPUT_DATA, + validationResult.error.message, + _options, + ), + ); + } + + return Result.ok(new Password(validationResult.object)); + } + + public static async createFromPlainText( + value: UndefinedOr, + options: IPasswordOptions = {}, + ) { + const _options = { + label: "password", + ...options, + }; + + const validationResult = Password.validate(value, _options); + + if (validationResult.isFailure) { + return Result.fail( + handleDomainError( + DomainError.INVALID_INPUT_DATA, + validationResult.error.message, + _options, + ), + ); + } + + return Result.ok( + new Password(await Password.hashPassword(validationResult.object)), + ); + } + + public static async hashPassword(plainText: string): Promise { + return hashPassword(plainText, await genSalt()); + } + + public verifyPassword(candidatePassword: string): boolean { + return bCrypt.compareSync(candidatePassword, this.value!); + } +} + +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); + }); + }); +} diff --git a/shared/lib/contexts/common/domain/entities/index.ts b/shared/lib/contexts/common/domain/entities/index.ts index 569c43e..210eb2d 100644 --- a/shared/lib/contexts/common/domain/entities/index.ts +++ b/shared/lib/contexts/common/domain/entities/index.ts @@ -11,6 +11,7 @@ export * from "./MoneyValue"; export * from "./Name"; export * from "./Note"; export * from "./NullableValueObject"; +export * from "./Password"; export * from "./Percentage"; export * from "./Phone"; export * from "./Quantity"; diff --git a/shared/lib/contexts/users/application/dto/CreateUser.dto/ICreateUser_Request.dto.ts b/shared/lib/contexts/users/application/dto/CreateUser.dto/ICreateUser_Request.dto.ts index 2d92dae..03d2022 100644 --- a/shared/lib/contexts/users/application/dto/CreateUser.dto/ICreateUser_Request.dto.ts +++ b/shared/lib/contexts/users/application/dto/CreateUser.dto/ICreateUser_Request.dto.ts @@ -5,15 +5,17 @@ export interface ICreateUser_Request_DTO { id: string; name: string; email: string; + password: string; } export function ensureCreateUser_Request_DTOIsValid( userDTO: ICreateUser_Request_DTO, -): Result { +) { const schema = Joi.object({ id: Joi.string(), name: Joi.string(), email: Joi.string(), + password: Joi.string(), }).unknown(true); const result = RuleValidator.validate(