diff --git a/server/package.json b/server/package.json index 4971332..efea801 100644 --- a/server/package.json +++ b/server/package.json @@ -22,6 +22,7 @@ "@types/express": "^4.17.21", "@types/express-session": "^1.18.0", "@types/glob": "^8.1.0", + "@types/http-status": "^1.1.2", "@types/jest": "^29.5.6", "@types/jsonwebtoken": "^9.0.6", "@types/luxon": "^3.3.1", @@ -64,6 +65,7 @@ "express": "^4.18.2", "express-openapi-validator": "^5.0.4", "helmet": "^7.0.0", + "http-status": "^1.7.4", "joi": "^17.12.3", "joi-phone-number": "^5.1.1", "jsonwebtoken": "^9.0.2", diff --git a/server/src/config/environments/development.ts b/server/src/config/environments/development.ts index 77f379f..f63a944 100644 --- a/server/src/config/environments/development.ts +++ b/server/src/config/environments/development.ts @@ -4,7 +4,7 @@ module.exports = { "9d6c903873c341816995a8be0355c6f0d6d471fc6aedacf50790e9b1e49c45b3", refresh_secret_key: "3972dc40c69327b65352ed097419213b0b75561169dba562410b85660bb1f305", - token_expiration: "15m", + token_expiration: "5m", refresh_token_expiration: "7d", }, diff --git a/server/src/contexts/auth/application/FindUserByEmail.useCase.ts b/server/src/contexts/auth/application/FindUserByEmail.useCase.ts index 9fab3cd..5bc7e95 100644 --- a/server/src/contexts/auth/application/FindUserByEmail.useCase.ts +++ b/server/src/contexts/auth/application/FindUserByEmail.useCase.ts @@ -9,7 +9,7 @@ import { IRepositoryManager } from "@/contexts/common/domain"; import { IInfrastructureError } from "@/contexts/common/infrastructure"; import { ISequelizeAdapter } from "@/contexts/common/infrastructure/sequelize"; import { Result, ensureUserEmailIsValid } from "@shared/contexts"; -import { User } from "../domain"; +import { AuthUser } from "../domain"; import { findUserByEmail } from "./authServices"; export interface FindUserByEmailRequest extends IUseCaseRequest { @@ -18,7 +18,7 @@ export interface FindUserByEmailRequest extends IUseCaseRequest { export type FindUserByEmailResponseOrError = | Result - | Result; + | Result; export class FindUserByEmailUseCase implements @@ -73,7 +73,7 @@ export class FindUserByEmailUseCase ), ); } - return Result.ok(user); + return Result.ok(user); } catch (error: unknown) { const _error = error as IInfrastructureError; return Result.fail( diff --git a/server/src/contexts/auth/application/Login.useCase.ts b/server/src/contexts/auth/application/Login.useCase.ts index 46c89ba..6624fc7 100644 --- a/server/src/contexts/auth/application/Login.useCase.ts +++ b/server/src/contexts/auth/application/Login.useCase.ts @@ -8,12 +8,12 @@ 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 { User } from "../domain"; +import { AuthUser } from "../domain"; import { findUserByEmail } from "./authServices"; export type LoginResponseOrError = | Result - | Result; + | Result; export class LoginUseCase implements IUseCase> @@ -64,7 +64,7 @@ export class LoginUseCase ), ); } - return Result.ok(user); + return Result.ok(user); } catch (error: unknown) { const _error = error as IInfrastructureError; return Result.fail( diff --git a/server/src/contexts/auth/application/authServices.ts b/server/src/contexts/auth/application/authServices.ts index eee0e89..18c9487 100644 --- a/server/src/contexts/auth/application/authServices.ts +++ b/server/src/contexts/auth/application/authServices.ts @@ -1,13 +1,13 @@ import { IAdapter, RepositoryBuilder } from "@/contexts/common/domain"; import { Email } from "@shared/contexts"; -import { User } from "../domain"; +import { AuthUser } from "../domain"; import { IAuthRepository } from "../domain/repository"; export const findUserByEmail = async ( email: Email, adapter: IAdapter, repository: RepositoryBuilder, -): Promise => { +): Promise => { const user = await adapter .startTransaction() .complete(async (t) => diff --git a/server/src/contexts/auth/domain/entities/AuthUser.ts b/server/src/contexts/auth/domain/entities/AuthUser.ts new file mode 100644 index 0000000..b393e10 --- /dev/null +++ b/server/src/contexts/auth/domain/entities/AuthUser.ts @@ -0,0 +1,115 @@ +import bCrypt from "bcryptjs"; + +import { + AggregateRoot, + Email, + IDomainError, + Name, + Result, + UniqueID, +} from "@shared/contexts"; + +export interface IAuthUserProps { + name: Name; + email: Email; + password?: string; + hashed_password?: string; +} + +export interface IAuthUser { + id: UniqueID; + name: Name; + email: Email; + hashed_password: string; + isUser: boolean; + isAdmin: boolean; + + verifyPassword: (candidatePassword: string) => boolean; +} + +export class AuthUser + extends AggregateRoot + implements IAuthUser +{ + public static create( + props: IAuthUserProps, + id?: UniqueID, + ): Result { + //const isNew = !!id === false; + + // Se hace en el constructor de la Entidad + /* if (isNew) { + id = UniqueEntityID.create(); + }*/ + + const user = new AuthUser(props, id); + + return Result.ok(user); + } + + public static async hashPassword(password): Promise { + return hashPassword(password, await genSalt()); + } + + private _hashed_password: string; + + private constructor(props: IAuthUserProps, id?: UniqueID) { + super({ ...props, password: "", hashed_password: "" }, id); + + this._protectPassword(props); + } + + get name(): Name { + return this.props.name; + } + + get email(): Email { + return this.props.email; + } + + get hashed_password(): string { + return this._hashed_password; + } + + get isUser(): boolean { + return true; + } + + get isAdmin(): boolean { + return true; + } + + public verifyPassword(candidatePassword: string): boolean { + return bCrypt.compareSync(candidatePassword, this._hashed_password!); + } + + private async _protectPassword(props: IAuthUserProps) { + const { password, hashed_password } = props; + + if (password) { + this._hashed_password = await AuthUser.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); + }); + }); +} + +AuthUser.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 index 3ce758c..73b9b64 100644 --- a/server/src/contexts/auth/domain/entities/index.ts +++ b/server/src/contexts/auth/domain/entities/index.ts @@ -1 +1 @@ -export * from "./User"; +export * from "./AuthUser"; diff --git a/server/src/contexts/auth/domain/repository/AuthRepository.interface.ts b/server/src/contexts/auth/domain/repository/AuthRepository.interface.ts index 85a87f0..5de5462 100644 --- a/server/src/contexts/auth/domain/repository/AuthRepository.interface.ts +++ b/server/src/contexts/auth/domain/repository/AuthRepository.interface.ts @@ -1,7 +1,7 @@ import { IRepository } from "@/contexts/common/domain"; import { Email } from "@shared/contexts"; -import { User } from "../entities"; +import { AuthUser } from "../entities"; export interface IAuthRepository extends IRepository { - findUserByEmail(email: Email): Promise; + findUserByEmail(email: Email): Promise; } diff --git a/server/src/contexts/auth/infrastructure/Auth.repository.ts b/server/src/contexts/auth/infrastructure/Auth.repository.ts index 9bf5373..5593fce 100644 --- a/server/src/contexts/auth/infrastructure/Auth.repository.ts +++ b/server/src/contexts/auth/infrastructure/Auth.repository.ts @@ -6,7 +6,7 @@ import { } from "@/contexts/common/infrastructure/sequelize"; import { Email, ICollection, IQueryCriteria, UniqueID } from "@shared/contexts"; import { Transaction } from "sequelize"; -import { User } from "../domain/entities"; +import { AuthUser } from "../domain/entities"; import { IAuthRepository } from "../domain/repository/AuthRepository.interface"; import { IUserMapper, createUserMapper } from "./mappers/user.mapper"; @@ -16,7 +16,7 @@ export type QueryParams = { }; export class AuthRepository - extends SequelizeRepository + extends SequelizeRepository implements IAuthRepository { protected mapper: IUserMapper; @@ -31,8 +31,8 @@ export class AuthRepository this.mapper = mapper; } - public async getById(id: UniqueID): Promise { - const rawUser: any = await this._getById("User_Model", id); + public async getById(id: UniqueID): Promise { + const rawUser: any = await this._getById("AuthUser_Model", id); if (!rawUser === true) { return null; @@ -41,9 +41,9 @@ export class AuthRepository return this.mapper.mapToDomain(rawUser); } - public async findUserByEmail(email: Email): Promise { + public async findUserByEmail(email: Email): Promise { const rawUser: any = await this._getBy( - "User_Model", + "AuthUser_Model", "email", email.toPrimitive(), ); @@ -59,7 +59,7 @@ export class AuthRepository queryCriteria?: IQueryCriteria, ): Promise> { const { rows, count } = await this._findAll( - "User_Model", + "AuthUser_Model", queryCriteria, /*{ include: [], // esto es para quitar las asociaciones al hacer la consulta diff --git a/server/src/contexts/auth/infrastructure/express/controllers/AuthenticateController.ts b/server/src/contexts/auth/infrastructure/express/controllers/AuthenticateController.ts index 3ba3051..fe7e3e7 100644 --- a/server/src/contexts/auth/infrastructure/express/controllers/AuthenticateController.ts +++ b/server/src/contexts/auth/infrastructure/express/controllers/AuthenticateController.ts @@ -1,5 +1,5 @@ // Import the necessary packages and modules -import { User } from "@/contexts/auth/domain"; +import { AuthUser } from "@/contexts/auth/domain"; import { IServerError } from "@/contexts/common/domain/errors"; import { ExpressController } from "@/contexts/common/infrastructure/express"; import passport from "passport"; @@ -42,7 +42,7 @@ export class AuthenticateController extends ExpressController { { session: false }, ( err: any, - user?: User | false | null, + user?: AuthUser | false | null, info?: object | string | Array, status?: number | Array, ) => { 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 eb47379..cd3839d 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,5 +1,5 @@ import { config } from "@/config"; -import { User } from "@/contexts/auth/domain"; +import { AuthUser } from "@/contexts/auth/domain"; import { IServerError } from "@/contexts/common/domain/errors"; import { InfrastructureError, @@ -30,7 +30,7 @@ export class LoginController extends ExpressController { async executeImpl() { try { - const user = this.req.user; + const user = this.req.user; if (!user) { const errorMessage = "Unexpected missing user data"; @@ -55,13 +55,13 @@ export class LoginController extends ExpressController { } } - private _generateUserToken(user: User) { + private _generateUserToken(user: AuthUser) { return JWT.sign({ email: user.email.toString() }, config.jwt.secret_key, { expiresIn: config.jwt.token_expiration, }); } - private _generateUserRefreshToken(user: User) { + private _generateUserRefreshToken(user: AuthUser) { return JWT.sign( { email: user.email.toString() }, config.jwt.refresh_secret_key, diff --git a/server/src/contexts/auth/infrastructure/express/controllers/login/presenter/Login.presenter.ts b/server/src/contexts/auth/infrastructure/express/controllers/login/presenter/Login.presenter.ts index fcc418a..3b2e0fd 100644 --- a/server/src/contexts/auth/infrastructure/express/controllers/login/presenter/Login.presenter.ts +++ b/server/src/contexts/auth/infrastructure/express/controllers/login/presenter/Login.presenter.ts @@ -1,9 +1,9 @@ -import { IUser } from "@/contexts/auth/domain"; +import { IAuthUser } from "@/contexts/auth/domain"; import { IAuthContext } from "@/contexts/auth/infrastructure/Auth.context"; import { ILogin_Response_DTO } from "@shared/contexts"; export interface ILoginUser { - user: IUser; + user: IAuthUser; token: string; refreshToken: string; } diff --git a/server/src/contexts/auth/infrastructure/express/passport/authMiddleware.ts b/server/src/contexts/auth/infrastructure/express/passport/authMiddleware.ts index f6810fd..8400ac0 100644 --- a/server/src/contexts/auth/infrastructure/express/passport/authMiddleware.ts +++ b/server/src/contexts/auth/infrastructure/express/passport/authMiddleware.ts @@ -1,40 +1,55 @@ -/* eslint-disable @typescript-eslint/no-unused-vars */ - +import { AuthUser } from "@/contexts/auth/domain"; +import { generateExpressErrorResponse } from "@/contexts/common/infrastructure/express/ExpressErrorResponse"; +import Express from "express"; +import httpStatus from "http-status"; import passport from "passport"; -export const isLoggedUser = passport.authenticate("local-jwt", { - session: false, -}); - -/*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?: 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; +function compose(middlewareArray: any[]) { + if (!middlewareArray.length) { + return function ( + req: Express.Request, + res: Express.Response, + next: Express.NextFunction, + ) { next(); - }, - )(req, res, next); -}; + }; + } + const head = middlewareArray[0]; + const tail = middlewareArray.slice(1); -*/ + return function ( + req: Express.Request, + res: Express.Response, + next: Express.NextFunction, + ) { + head(req, res, function (err: unknown) { + if (err) return next(err); + compose(tail)(req, res, next); + }); + }; +} + +export const isLoggedUser = compose([ + passport.authenticate("local-jwt", { + session: false, + }), + (req: Express.Request, res: Express.Response, next: Express.NextFunction) => { + const user = req.user; + if (user.isUser) { + return generateExpressErrorResponse(req, res, httpStatus.UNAUTHORIZED); + } + next(); + }, +]); + +export const isAdminUser = compose([ + isLoggedUser, + (req: Express.Request, res: Express.Response, next: Express.NextFunction) => { + const user = req.user; + if (!user.isAdmin) { + return generateExpressErrorResponse(req, res, httpStatus.UNAUTHORIZED); + } + next(); + }, +]); diff --git a/server/src/contexts/auth/infrastructure/express/passport/emailStrategy.ts b/server/src/contexts/auth/infrastructure/express/passport/emailStrategy.ts index df3b61e..3bc2362 100644 --- a/server/src/contexts/auth/infrastructure/express/passport/emailStrategy.ts +++ b/server/src/contexts/auth/infrastructure/express/passport/emailStrategy.ts @@ -4,7 +4,7 @@ import { ensureLogin_DTOIsValid } from "@shared/contexts"; import { Strategy as EmailStrategy, IVerifyOptions } from "passport-local"; import { LoginUseCase } from "@/contexts/auth/application"; -import { User } from "@/contexts/auth/domain"; +import { AuthUser } from "@/contexts/auth/domain"; import { IAuthContext } from "../../Auth.context"; import { registerAuthRepository } from "../../Auth.repository"; @@ -33,7 +33,11 @@ class EmailStrategyController extends PassportStrategyController { public async verifyStrategy( email: string, password: string, - done: (error: any, user?: User | false, options?: IVerifyOptions) => void, + done: ( + error: any, + user?: AuthUser | false, + options?: IVerifyOptions, + ) => void, ) { const loginDTOOrError = ensureLogin_DTOIsValid({ email, password }); diff --git a/server/src/contexts/auth/infrastructure/express/routes.ts b/server/src/contexts/auth/infrastructure/express/routes.ts index cf02cd8..b9d7844 100644 --- a/server/src/contexts/auth/infrastructure/express/routes.ts +++ b/server/src/contexts/auth/infrastructure/express/routes.ts @@ -49,5 +49,20 @@ export const AuthRouter = (appRouter: Express.Router) => { createLoginController(res.locals["context"]).execute(req, res, next), ); + authRoutes.post( + "/logout", + isLoggedUser, + ( + req: Express.Request, + res: Express.Response, + next: Express.NextFunction, + ) => { + //req.logout(); <-- ?? + return res.status(200).json(); + }, + ); + + authRoutes.post("/register"); + appRouter.use("/auth", authRoutes); }; diff --git a/server/src/contexts/auth/infrastructure/mappers/user.mapper.ts b/server/src/contexts/auth/infrastructure/mappers/user.mapper.ts index d736a50..c86cc42 100644 --- a/server/src/contexts/auth/infrastructure/mappers/user.mapper.ts +++ b/server/src/contexts/auth/infrastructure/mappers/user.mapper.ts @@ -3,30 +3,37 @@ import { SequelizeMapper, } from "@/contexts/common/infrastructure"; import { Email, Name, UniqueID } from "@shared/contexts"; -import { IUserProps, User } from "../../domain/entities"; +import { AuthUser, IAuthUserProps } from "../../domain/entities"; import { IAuthContext } from "../Auth.context"; -import { TCreationUser_Attributes, User_Model } from "../sequelize/user.model"; +import { + AuthUser_Model, + TCreationUser_Attributes, +} from "../sequelize/authUser.model"; export interface IUserMapper - extends ISequelizeMapper {} + extends ISequelizeMapper< + AuthUser_Model, + TCreationUser_Attributes, + AuthUser + > {} class UserMapper - extends SequelizeMapper + extends SequelizeMapper implements IUserMapper { public constructor(props: { context: IAuthContext }) { super(props); } - protected toDomainMappingImpl(source: User_Model, params: any): User { - const props: IUserProps = { + protected toDomainMappingImpl(source: AuthUser_Model, params: any): AuthUser { + const props: IAuthUserProps = { name: this.mapsValue(source, "name", Name.create), 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); + const userOrError = AuthUser.create(props, id); if (userOrError.isFailure) { throw userOrError.error; @@ -36,7 +43,7 @@ class UserMapper } protected toPersistenceMappingImpl( - source: User, + source: AuthUser, params?: Record | undefined, ) { return { diff --git a/server/src/contexts/auth/infrastructure/sequelize/user.model.ts b/server/src/contexts/auth/infrastructure/sequelize/authUser.model.ts similarity index 80% rename from server/src/contexts/auth/infrastructure/sequelize/user.model.ts rename to server/src/contexts/auth/infrastructure/sequelize/authUser.model.ts index f88b243..fdc659d 100644 --- a/server/src/contexts/auth/infrastructure/sequelize/user.model.ts +++ b/server/src/contexts/auth/infrastructure/sequelize/authUser.model.ts @@ -6,11 +6,11 @@ import { Sequelize, } from "sequelize"; -export type TCreationUser_Attributes = InferCreationAttributes; +export type TCreationUser_Attributes = InferCreationAttributes; -export class User_Model extends Model< - InferAttributes, - InferCreationAttributes +export class AuthUser_Model extends Model< + InferAttributes, + InferCreationAttributes > { // To avoid table creation /*static async sync(): Promise { @@ -26,7 +26,7 @@ export class User_Model extends Model< } export default (sequelize: Sequelize) => { - User_Model.init( + AuthUser_Model.init( { id: { type: new DataTypes.UUID(), @@ -63,5 +63,5 @@ export default (sequelize: Sequelize) => { }, ); - return User_Model; + return AuthUser_Model; }; diff --git a/server/src/contexts/auth/infrastructure/sequelize/index.ts b/server/src/contexts/auth/infrastructure/sequelize/index.ts index 3787888..3b4dd77 100644 --- a/server/src/contexts/auth/infrastructure/sequelize/index.ts +++ b/server/src/contexts/auth/infrastructure/sequelize/index.ts @@ -1 +1 @@ -export * from "./user.model"; +export * from "./authUser.model"; diff --git a/server/src/contexts/catalog/infrastructure/Catalog.repository.ts b/server/src/contexts/catalog/infrastructure/Catalog.repository.ts index f8a76d6..2b27231 100644 --- a/server/src/contexts/catalog/infrastructure/Catalog.repository.ts +++ b/server/src/contexts/catalog/infrastructure/Catalog.repository.ts @@ -5,8 +5,8 @@ import { import { ICollection, IQueryCriteria, UniqueID } from "@shared/contexts"; import { Transaction } from "sequelize"; import { ICatalogContext } from "."; +import { ICatalogRepository } from "../domain"; import { Article } from "../domain/entities"; -import { ICatalogRepository } from "../domain/repository/CatalogRepository.interface"; import { IArticleMapper, createArticleMapper } from "./mappers/article.mapper"; export type QueryParams = { 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 8b6ae20..5ebed19 100644 --- a/server/src/contexts/catalog/infrastructure/express/controllers/listArticles/index.ts +++ b/server/src/contexts/catalog/infrastructure/express/controllers/listArticles/index.ts @@ -5,12 +5,11 @@ import { ListArticlesController } from "./ListArticlesController"; import { listArticlesPresenter } from "./presenter"; export const createListArticlesController = (context: ICatalogContext) => { - const listArticlesUseCase = new ListArticlesUseCase(context); registerCatalogRepository(context); return new ListArticlesController( { - useCase: listArticlesUseCase, + useCase: new ListArticlesUseCase(context), presenter: listArticlesPresenter, }, context, diff --git a/server/src/contexts/catalog/infrastructure/express/controllers/listArticles/presenter/ListArticles.presenter.ts b/server/src/contexts/catalog/infrastructure/express/controllers/listArticles/presenter/ListArticles.presenter.ts index 686a7fd..c0c2232 100644 --- a/server/src/contexts/catalog/infrastructure/express/controllers/listArticles/presenter/ListArticles.presenter.ts +++ b/server/src/contexts/catalog/infrastructure/express/controllers/listArticles/presenter/ListArticles.presenter.ts @@ -9,7 +9,7 @@ import { export interface IListArticlesPresenter { map: ( article: Article, - context: ICatalogContext + context: ICatalogContext, ) => IListArticles_Response_DTO; mapArray: ( @@ -18,17 +18,15 @@ export interface IListArticlesPresenter { params: { page: number; limit: number; - } + }, ) => IListResponse_DTO; } export const listArticlesPresenter: IListArticlesPresenter = { map: ( article: Article, - context: ICatalogContext + context: ICatalogContext, ): IListArticles_Response_DTO => { - console.time("listArticlesPresenter.map"); - const result: IListArticles_Response_DTO = { id: article.id.toString(), catalog_name: article.catalog_name.toString(), @@ -40,9 +38,6 @@ export const listArticlesPresenter: IListArticlesPresenter = { points: article.points.toNumber(), retail_price: article.retail_price.toObject(), }; - - console.timeEnd("listArticlesPresenter.map"); - return result; }, @@ -52,15 +47,13 @@ export const listArticlesPresenter: IListArticlesPresenter = { params: { page: number; limit: number; - } + }, ): IListResponse_DTO => { - console.time("listArticlesPresenter.mapArray"); - const { page, limit } = params; const totalCount = articles.totalCount ?? 0; const items = articles.items.map((article: Article) => - listArticlesPresenter.map(article, context) + listArticlesPresenter.map(article, context), ); const result = { @@ -71,8 +64,6 @@ export const listArticlesPresenter: IListArticlesPresenter = { items, }; - console.timeEnd("listArticlesPresenter.mapArray"); - return result; }, }; diff --git a/server/src/contexts/common/infrastructure/express/ExpressController.ts b/server/src/contexts/common/infrastructure/express/ExpressController.ts index 9059d23..7432681 100644 --- a/server/src/contexts/common/infrastructure/express/ExpressController.ts +++ b/server/src/contexts/common/infrastructure/express/ExpressController.ts @@ -1,15 +1,11 @@ +import { IError_Response_DTO } from "@shared/contexts"; import * as express from "express"; +import httpStatus from "http-status"; import { URL } from "url"; - -import { - IErrorExtra_Response_DTO, - IError_Response_DTO, -} 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"; +import { generateExpressErrorResponse } from "./ExpressErrorResponse"; export abstract class ExpressController implements IController { protected req: express.Request; @@ -55,19 +51,22 @@ export abstract class ExpressController implements IController { console.trace("Show me"); console.groupEnd(); - return this._errorResponse(500, error ? error.toString() : "Fail"); + return this._errorResponse( + httpStatus.INTERNAL_SERVER_ERROR, + error ? error.toString() : "Fail", + ); } public created(dto?: T) { if (dto) { - return this.res.status(201).json(dto).send(); + return this.res.status(httpStatus.CREATED).json(dto).send(); } - return this.res.status(201).send(); + return this.res.status(httpStatus.CREATED).send(); } public noContent() { - return this.res.status(204).send(); + return this.res.status(httpStatus.NO_CONTENT).send(); } public download(filepath: string, filename: string, done?: any) { @@ -75,47 +74,51 @@ export abstract class ExpressController implements IController { } public clientError(message?: string) { - return this._errorResponse(400, message); + return this._errorResponse(httpStatus.BAD_REQUEST, message); } public unauthorizedError(message?: string) { - return this._errorResponse(401, message); + return this._errorResponse(httpStatus.UNAUTHORIZED, message); } public paymentRequiredError(message?: string) { - return this._errorResponse(402, message); + return this._errorResponse(httpStatus.PAYMENT_REQUIRED, message); } public forbiddenError(message?: string) { - return this._errorResponse(403, message); + return this._errorResponse(httpStatus.FORBIDDEN, message); } public notFoundError(message: string, error?: IServerError) { - return this._errorResponse(404, message, error); + return this._errorResponse(httpStatus.NOT_FOUND, message, error); } public conflictError(message: string, error?: IServerError) { - return this._errorResponse(409, message, error); + return this._errorResponse(httpStatus.CONFLICT, message, error); } public invalidInputError(message?: string, error?: InfrastructureError) { - return this._errorResponse(422, message, error); + return this._errorResponse(httpStatus.UNPROCESSABLE_ENTITY, message, error); } public tooManyError(message: string, error?: Error) { - return this._errorResponse(429, message, error); + return this._errorResponse(httpStatus.TOO_MANY_REQUESTS, message, error); } public internalServerError(message?: string, error?: IServerError) { - return this._errorResponse(500, message, error); + return this._errorResponse( + httpStatus.INTERNAL_SERVER_ERROR, + message, + error, + ); } public todoError(message?: string) { - return this._errorResponse(501, message); + return this._errorResponse(httpStatus.NOT_IMPLEMENTED, message); } public unavailableError(message?: string) { - return this._errorResponse(503, message); + return this._errorResponse(httpStatus.SERVICE_UNAVAILABLE, message); } private _jsonResponse( @@ -130,102 +133,12 @@ export abstract class ExpressController implements IController { message?: string, error?: Error | InfrastructureError, ): express.Response { - const context = {}; - - if (Object.keys(this.res.locals).length) { - if ("user" in this.res.locals) { - context["user"] = this.res.locals.user; - } - } - - if (Object.keys(this.req.params).length) { - context["params"] = this.req.params; - } - - if (Object.keys(this.req.query).length) { - context["query"] = this.req.query; - } - - if (Object.keys(this.req.body).length) { - context["body"] = this.req.body; - } - - const extension = new ProblemDocumentExtension({ - context, - extra: error ? { ...this._processError(error) } : {}, - }); - - return this._jsonResponse( + return generateExpressErrorResponse( + this.req, + this.res, statusCode, - new ProblemDocument( - { - status: statusCode, - detail: message, - instance: this.req.baseUrl, - }, - extension, - ), + message, + error, ); } - - private _processError( - error: Error | InfrastructureError, - ): IErrorExtra_Response_DTO { - /** - * - * - * - { - code: "INVALID_INPUT_DATA", - payload: { - label: "tin", - path: "tin", // [{path: "first_name"}, {path: "last_name"}] - }, - name: "UseCaseError", - } - - - { - code: "INVALID_INPUT_DATA", - payload: [ - { - tin: "{tin} is not allowed to be empty", - }, - { - first_name: "{first_name} is not allowed to be empty", - }, - { - last_name: "{last_name} is not allowed to be empty", - }, - { - company_name: "{company_name} is not allowed to be empty", - }, - ], - name: "InfrastructureError", - } - - */ - - const useCaseError = error; - - const payload = !Array.isArray(useCaseError.payload) - ? Array(useCaseError.payload) - : useCaseError.payload; - - const errors = payload.map((item) => { - if (item.path) { - return item.path - ? { - [String(item.path)]: useCaseError.message, - } - : {}; - } else { - return item; - } - }); - - return { - errors, - }; - } } diff --git a/server/src/contexts/common/infrastructure/express/ExpressErrorResponse.ts b/server/src/contexts/common/infrastructure/express/ExpressErrorResponse.ts new file mode 100644 index 0000000..e4984e3 --- /dev/null +++ b/server/src/contexts/common/infrastructure/express/ExpressErrorResponse.ts @@ -0,0 +1,78 @@ +import { + IErrorExtra_Response_DTO, + IError_Response_DTO, +} from "@shared/contexts"; +import Express from "express"; +import { UseCaseError } from "../../application"; +import { InfrastructureError } from "../InfrastructureError"; +import { ProblemDocument, ProblemDocumentExtension } from "./ProblemDocument"; + +export const generateExpressErrorResponse = ( + req: Express.Request, + res: Express.Response, + statusCode: number, + message?: string, + error?: Error | InfrastructureError, +): Express.Response => { + const context = {}; + + if (Object.keys(res.locals).length) { + if ("user" in res.locals) { + context["user"] = res.locals.user; + } + } + + if (Object.keys(req.params).length) { + context["params"] = req.params; + } + + if (Object.keys(req.query).length) { + context["query"] = req.query; + } + + if (Object.keys(req.body).length) { + context["body"] = req.body; + } + + const extension = new ProblemDocumentExtension({ + context, + extra: error ? { ...generateExpressError(error) } : {}, + }); + + const jsonPayload = new ProblemDocument( + { + status: statusCode, + detail: message, + instance: req.baseUrl, + }, + extension, + ); + + return res.status(statusCode).json(jsonPayload).send(); +}; + +function generateExpressError( + error: Error | InfrastructureError, +): IErrorExtra_Response_DTO { + const useCaseError = error; + + const payload = !Array.isArray(useCaseError.payload) + ? Array(useCaseError.payload) + : useCaseError.payload; + + const errors = payload.map((item) => { + if (item.path) { + return item.path + ? { + [String(item.path)]: useCaseError.message, + } + : {}; + } else { + return item; + } + }); + + return { + errors, + }; +} diff --git a/server/src/contexts/users/application/ListUsersUseCase.ts b/server/src/contexts/users/application/ListUsersUseCase.ts new file mode 100644 index 0000000..5b9a712 --- /dev/null +++ b/server/src/contexts/users/application/ListUsersUseCase.ts @@ -0,0 +1,76 @@ +import { + IUseCase, + IUseCaseError, + UseCaseError, + handleUseCaseError, +} from "@/contexts/common/application/useCases"; +import { IRepositoryManager } from "@/contexts/common/domain"; +import { + Collection, + ICollection, + IQueryCriteria, + Result, +} from "@shared/contexts"; + +import { IInfrastructureError } from "@/contexts/common/infrastructure"; +import { ISequelizeAdapter } from "@/contexts/common/infrastructure/sequelize"; +import { User } from "../domain"; +import { IUserRepository } from "../domain/repository"; + +export interface IListUsersParams { + queryCriteria: IQueryCriteria; +} + +export type ListUsersResult = + | Result // Misc errors (value objects) + | Result, never>; // Success! + +export class ListUsersUseCase + 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(params: Partial): Promise { + const { queryCriteria } = params; + + return this.findUsers(queryCriteria); + } + + private async findUsers(queryCriteria) { + const transaction = this._adapter.startTransaction(); + const userRepoBuilder = this.getRepositoryByName("User"); + + let users: ICollection = new Collection(); + + try { + await transaction.complete(async (t) => { + users = await userRepoBuilder({ transaction: t }).findAll( + queryCriteria, + ); + }); + return Result.ok(users); + } catch (error: unknown) { + const _error = error as IInfrastructureError; + return Result.fail( + handleUseCaseError( + UseCaseError.REPOSITORY_ERROR, + "Error al listar los usurios", + _error, + ), + ); + } + } +} diff --git a/server/src/contexts/users/application/index.ts b/server/src/contexts/users/application/index.ts new file mode 100644 index 0000000..303db18 --- /dev/null +++ b/server/src/contexts/users/application/index.ts @@ -0,0 +1 @@ +export * from "./ListUsersUseCase"; diff --git a/server/src/contexts/auth/domain/entities/User.ts b/server/src/contexts/users/domain/entities/User.ts similarity index 94% rename from server/src/contexts/auth/domain/entities/User.ts rename to server/src/contexts/users/domain/entities/User.ts index a8725bd..c85119c 100644 --- a/server/src/contexts/auth/domain/entities/User.ts +++ b/server/src/contexts/users/domain/entities/User.ts @@ -21,6 +21,8 @@ export interface IUser { name: Name; email: Email; hashed_password: string; + isUser: boolean; + isAdmin: boolean; verifyPassword: (candidatePassword: string) => boolean; } @@ -66,6 +68,14 @@ export class User extends AggregateRoot implements IUser { return this._hashed_password; } + get isUser(): boolean { + return true; + } + + get isAdmin(): boolean { + return true; + } + public verifyPassword(candidatePassword: string): boolean { return bCrypt.compareSync(candidatePassword, this._hashed_password!); } diff --git a/server/src/contexts/users/domain/entities/index.ts b/server/src/contexts/users/domain/entities/index.ts new file mode 100644 index 0000000..3ce758c --- /dev/null +++ b/server/src/contexts/users/domain/entities/index.ts @@ -0,0 +1 @@ +export * from "./User"; diff --git a/server/src/contexts/users/domain/index.ts b/server/src/contexts/users/domain/index.ts new file mode 100644 index 0000000..6347a2b --- /dev/null +++ b/server/src/contexts/users/domain/index.ts @@ -0,0 +1,2 @@ +export * from "./entities"; +export * from "./repository"; diff --git a/server/src/contexts/users/domain/repository/UserRepository.interface.ts b/server/src/contexts/users/domain/repository/UserRepository.interface.ts new file mode 100644 index 0000000..31756ce --- /dev/null +++ b/server/src/contexts/users/domain/repository/UserRepository.interface.ts @@ -0,0 +1,8 @@ +import { IRepository } from "@/contexts/common/domain"; +import { ICollection, IQueryCriteria, UniqueID } from "@shared/contexts"; +import { User } from "../entities"; + +export interface IUserRepository extends IRepository { + getById(id: UniqueID): Promise; + findAll(queryCriteria?: IQueryCriteria): Promise>; +} diff --git a/server/src/contexts/users/domain/repository/index.ts b/server/src/contexts/users/domain/repository/index.ts new file mode 100644 index 0000000..d91b76c --- /dev/null +++ b/server/src/contexts/users/domain/repository/index.ts @@ -0,0 +1 @@ +export * from "./UserRepository.interface"; diff --git a/server/src/contexts/users/index.ts b/server/src/contexts/users/index.ts new file mode 100644 index 0000000..cdbc093 --- /dev/null +++ b/server/src/contexts/users/index.ts @@ -0,0 +1 @@ +export * from "./infrastructure"; diff --git a/server/src/contexts/users/infrastructure/User.context.ts b/server/src/contexts/users/infrastructure/User.context.ts new file mode 100644 index 0000000..76920ba --- /dev/null +++ b/server/src/contexts/users/infrastructure/User.context.ts @@ -0,0 +1,35 @@ +import { + IRepositoryManager, + RepositoryManager, +} from "@/contexts/common/domain"; +import { + ISequelizeAdapter, + createSequelizeAdapter, +} from "@/contexts/common/infrastructure/sequelize"; + +export interface IUserContext { + adapter: ISequelizeAdapter; + repositoryManager: IRepositoryManager; + //services: IApplicationService; +} + +export class UserContext { + private static instance: UserContext | null = null; + + public static getInstance(): IUserContext { + if (!UserContext.instance) { + UserContext.instance = new UserContext({ + adapter: createSequelizeAdapter(), + repositoryManager: RepositoryManager.getInstance(), + }); + } + + return UserContext.instance.context; + } + + private context: IUserContext; + + private constructor(context: IUserContext) { + this.context = context; + } +} diff --git a/server/src/contexts/users/infrastructure/User.repository.ts b/server/src/contexts/users/infrastructure/User.repository.ts new file mode 100644 index 0000000..034e11d --- /dev/null +++ b/server/src/contexts/users/infrastructure/User.repository.ts @@ -0,0 +1,85 @@ +import { + ISequelizeAdapter, + SequelizeRepository, +} from "@/contexts/common/infrastructure/sequelize"; +import { Email, ICollection, IQueryCriteria, UniqueID } from "@shared/contexts"; +import { Transaction } from "sequelize"; +import { User } from "../domain"; +import { IUserRepository } from "../domain/repository"; +import { IUserContext } from "./User.context"; +import { IUserMapper, createUserMapper } from "./mappers"; + +export type QueryParams = { + pagination: Record; + filters: Record; +}; + +export class UserRepository + extends SequelizeRepository + implements IUserRepository +{ + 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 findUserByEmail(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 registerUserRepository = (context: IUserContext) => { + const adapter = context.adapter; + const repoManager = context.repositoryManager; + + repoManager.registerRepository("User", (params = { transaction: null }) => { + const { transaction } = params; + + return new UserRepository({ + transaction, + adapter, + mapper: createUserMapper(context), + }); + }); +}; diff --git a/server/src/contexts/users/infrastructure/express/controllers/index.ts b/server/src/contexts/users/infrastructure/express/controllers/index.ts new file mode 100644 index 0000000..dbb5728 --- /dev/null +++ b/server/src/contexts/users/infrastructure/express/controllers/index.ts @@ -0,0 +1 @@ +export * from "./listUsers"; diff --git a/server/src/contexts/users/infrastructure/express/controllers/listUsers/ListUsers.controller.ts b/server/src/contexts/users/infrastructure/express/controllers/listUsers/ListUsers.controller.ts new file mode 100644 index 0000000..5b68b4b --- /dev/null +++ b/server/src/contexts/users/infrastructure/express/controllers/listUsers/ListUsers.controller.ts @@ -0,0 +1,88 @@ +import Joi from "joi"; + +import { QueryCriteriaService } from "@/contexts/common/application/services"; +import { IServerError } from "@/contexts/common/domain/errors"; +import { ExpressController } from "@/contexts/common/infrastructure/express"; +import { + ListUsersResult, + ListUsersUseCase, +} from "@/contexts/users/application"; +import { User } from "@/contexts/users/domain"; +import { + ICollection, + IListResponse_DTO, + IListUsers_Response_DTO, + IQueryCriteria, + Result, + RuleValidator, +} from "@shared/contexts"; +import { IUserContext } from "../../../User.context"; +import { IListUsersPresenter } from "./presenter"; + +export class ListUsersController extends ExpressController { + private useCase: ListUsersUseCase; + private presenter: IListUsersPresenter; + private context: IUserContext; + + constructor( + props: { + useCase: ListUsersUseCase; + presenter: IListUsersPresenter; + }, + context: IUserContext, + ) { + super(); + + const { useCase, presenter } = props; + this.useCase = useCase; + this.presenter = presenter; + this.context = context; + } + + protected validateQuery(query): Result { + const schema = Joi.object({ + page: Joi.number().optional(), + limit: Joi.number().optional(), + $sort_by: Joi.string().optional(), + $filters: Joi.string().optional(), + q: Joi.string().optional(), + }).optional(); + + return RuleValidator.validate(schema, query); + } + + async executeImpl() { + const queryOrError = this.validateQuery(this.req.query); + if (queryOrError.isFailure) { + return this.clientError(queryOrError.error.message); + } + + const queryParams = queryOrError.object; + + try { + const queryCriteria: IQueryCriteria = + QueryCriteriaService.parse(queryParams); + + console.log(queryCriteria); + + const result: ListUsersResult = await this.useCase.execute({ + queryCriteria, + }); + + if (result.isFailure) { + return this.clientError(result.error.message); + } + + const customers = >result.object; + + return this.ok>( + 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/users/infrastructure/express/controllers/listUsers/index.ts b/server/src/contexts/users/infrastructure/express/controllers/listUsers/index.ts new file mode 100644 index 0000000..c46d7db --- /dev/null +++ b/server/src/contexts/users/infrastructure/express/controllers/listUsers/index.ts @@ -0,0 +1,16 @@ +import { ListUsersUseCase } from "@/contexts/users/application"; +import { IUserContext } from "../../../User.context"; +import { registerUserRepository } from "../../../User.repository"; +import { ListUsersController } from "./ListUsers.controller"; +import { listUsersPresenter } from "./presenter"; + +export const createListUsersController = (context: IUserContext) => { + registerUserRepository(context); + return new ListUsersController( + { + useCase: new ListUsersUseCase(context), + presenter: listUsersPresenter, + }, + context, + ); +}; diff --git a/server/src/contexts/users/infrastructure/express/controllers/listUsers/presenter/ListUsers.presenter.ts b/server/src/contexts/users/infrastructure/express/controllers/listUsers/presenter/ListUsers.presenter.ts new file mode 100644 index 0000000..cc33a76 --- /dev/null +++ b/server/src/contexts/users/infrastructure/express/controllers/listUsers/presenter/ListUsers.presenter.ts @@ -0,0 +1,57 @@ +import { User } from "@/contexts/users/domain"; +import { IUserContext } from "@/contexts/users/infrastructure/User.context"; +import { + ICollection, + IListResponse_DTO, + IListUsers_Response_DTO, +} from "@shared/contexts"; + +export interface IListUsersPresenter { + map: (user: User, context: IUserContext) => IListUsers_Response_DTO; + + mapArray: ( + users: ICollection, + context: IUserContext, + params: { + page: number; + limit: number; + }, + ) => IListResponse_DTO; +} + +export const listUsersPresenter: IListUsersPresenter = { + // eslint-disable-next-line @typescript-eslint/no-unused-vars + map: (user: User, context: IUserContext): IListUsers_Response_DTO => { + return { + id: user.id.toString(), + name: user.name.toString(), + email: user.email.toString(), + }; + }, + + mapArray: ( + users: ICollection, + context: IUserContext, + params: { + page: number; + limit: number; + }, + ): IListResponse_DTO => { + const { page, limit } = params; + + const totalCount = users.totalCount ?? 0; + const items = users.items.map((user: User) => + listUsersPresenter.map(user, context), + ); + + const result = { + page, + per_page: limit, + total_pages: Math.ceil(totalCount / limit), + total_items: totalCount, + items, + }; + + return result; + }, +}; diff --git a/server/src/contexts/users/infrastructure/express/controllers/listUsers/presenter/index.ts b/server/src/contexts/users/infrastructure/express/controllers/listUsers/presenter/index.ts new file mode 100644 index 0000000..b7bb981 --- /dev/null +++ b/server/src/contexts/users/infrastructure/express/controllers/listUsers/presenter/index.ts @@ -0,0 +1 @@ +export * from "./ListUsers.presenter"; diff --git a/server/src/contexts/users/infrastructure/express/index.ts b/server/src/contexts/users/infrastructure/express/index.ts new file mode 100644 index 0000000..e5fd210 --- /dev/null +++ b/server/src/contexts/users/infrastructure/express/index.ts @@ -0,0 +1,2 @@ +export * from "./controllers"; +export * from "./routes"; diff --git a/server/src/contexts/users/infrastructure/express/routes.ts b/server/src/contexts/users/infrastructure/express/routes.ts new file mode 100644 index 0000000..5779a06 --- /dev/null +++ b/server/src/contexts/users/infrastructure/express/routes.ts @@ -0,0 +1,23 @@ +import { applyMiddleware } from "@/contexts/common/infrastructure/express"; +import Express from "express"; +import { createListUsersController } from "./controllers"; + +export const UserRouter = (appRouter: Express.Router) => { + const userRoutes: Express.Router = Express.Router({ mergeParams: true }); + + userRoutes.get( + "/", + applyMiddleware("isAdminUser"), + (req: Express.Request, res: Express.Response, next: Express.NextFunction) => + createListUsersController(res.locals["context"]).execute(req, res, next), + ); + + /*userRoutes.get( + "/:id", + applyMiddleware("isAdminUser"), + (req: Express.Request, res: Express.Response, next: Express.NextFunction) => + createGettUserController(res.locals["context"]).execute(req, res, next), + );*/ + + appRouter.use("/users", userRoutes); +}; diff --git a/server/src/contexts/users/infrastructure/index.ts b/server/src/contexts/users/infrastructure/index.ts new file mode 100644 index 0000000..c754fca --- /dev/null +++ b/server/src/contexts/users/infrastructure/index.ts @@ -0,0 +1,2 @@ +export * from "./express"; +export * from "./sequelize"; diff --git a/server/src/contexts/users/infrastructure/mappers/index.ts b/server/src/contexts/users/infrastructure/mappers/index.ts new file mode 100644 index 0000000..adc0092 --- /dev/null +++ b/server/src/contexts/users/infrastructure/mappers/index.ts @@ -0,0 +1 @@ +export * from "./user.mapper"; diff --git a/server/src/contexts/users/infrastructure/mappers/user.mapper.ts b/server/src/contexts/users/infrastructure/mappers/user.mapper.ts new file mode 100644 index 0000000..460b9fa --- /dev/null +++ b/server/src/contexts/users/infrastructure/mappers/user.mapper.ts @@ -0,0 +1,54 @@ +import { + ISequelizeMapper, + SequelizeMapper, +} from "@/contexts/common/infrastructure"; +import { Email, Name, UniqueID } from "@shared/contexts"; +import { IUserProps, User } from "../../domain"; +import { IUserContext } from "../User.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: IUserContext }) { + super(props); + } + + protected toDomainMappingImpl(source: User_Model, params: any): User { + const props: IUserProps = { + name: this.mapsValue(source, "name", Name.create), + 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(), + name: source.name.toPrimitive(), + email: source.email.toPrimitive(), + password: source.hashed_password, + }; + } +} + +export const createUserMapper = (context: IUserContext): IUserMapper => + new UserMapper({ + context, + }); diff --git a/server/src/contexts/users/infrastructure/sequelize/index.ts b/server/src/contexts/users/infrastructure/sequelize/index.ts new file mode 100644 index 0000000..3787888 --- /dev/null +++ b/server/src/contexts/users/infrastructure/sequelize/index.ts @@ -0,0 +1 @@ +export * from "./user.model"; diff --git a/server/src/contexts/users/infrastructure/sequelize/user.model.ts b/server/src/contexts/users/infrastructure/sequelize/user.model.ts new file mode 100644 index 0000000..11c0a43 --- /dev/null +++ b/server/src/contexts/users/infrastructure/sequelize/user.model.ts @@ -0,0 +1,86 @@ +import { + DataTypes, + InferAttributes, + InferCreationAttributes, + Model, + Op, + 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 name: string; + declare email: string; + declare password: string; +} + +export default (sequelize: Sequelize) => { + User_Model.init( + { + id: { + type: new DataTypes.UUID(), + primaryKey: true, + }, + + name: { + type: DataTypes.STRING, + }, + + 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", fields: ["email"] }], + + whereMergeStrategy: "and", // <- cómo tratar el merge de un scope + scopes: { + quickSearch(value) { + return { + where: { + [Op.or]: { + name: { + [Op.like]: `%${value}%`, + }, + email: { + [Op.like]: `%${value}%`, + }, + }, + }, + }; + }, + }, + }, + ); + + return User_Model; +}; diff --git a/server/src/infrastructure/express/api/v1.ts b/server/src/infrastructure/express/api/v1.ts index 12c1de3..5318571 100644 --- a/server/src/infrastructure/express/api/v1.ts +++ b/server/src/infrastructure/express/api/v1.ts @@ -2,6 +2,7 @@ import { AuthRouter } from "@/contexts/auth"; import { CatalogRouter } from "@/contexts/catalog"; import { RepositoryManager } from "@/contexts/common/domain"; import { createSequelizeAdapter } from "@/contexts/common/infrastructure/sequelize"; +import { UserRouter } from "@/contexts/users"; import Express from "express"; export const v1Routes = () => { @@ -39,6 +40,7 @@ export const v1Routes = () => { }); AuthRouter(routes); + UserRouter(routes); CatalogRouter(routes); return routes; diff --git a/shared/lib/contexts/index.ts b/shared/lib/contexts/index.ts index 24c3852..0260577 100644 --- a/shared/lib/contexts/index.ts +++ b/shared/lib/contexts/index.ts @@ -1,3 +1,5 @@ +export * from "./common"; + export * from "./auth"; export * from "./catalog"; -export * from "./common"; +export * from "./users"; diff --git a/shared/lib/contexts/users/application/dto/IListUsers.dto/IListUsers_Response.dto.ts b/shared/lib/contexts/users/application/dto/IListUsers.dto/IListUsers_Response.dto.ts new file mode 100644 index 0000000..cae1253 --- /dev/null +++ b/shared/lib/contexts/users/application/dto/IListUsers.dto/IListUsers_Response.dto.ts @@ -0,0 +1,5 @@ +export interface IListUsers_Response_DTO { + id: string; + name: string; + email: string; +} diff --git a/shared/lib/contexts/users/application/dto/IListUsers.dto/index.ts b/shared/lib/contexts/users/application/dto/IListUsers.dto/index.ts new file mode 100644 index 0000000..029101a --- /dev/null +++ b/shared/lib/contexts/users/application/dto/IListUsers.dto/index.ts @@ -0,0 +1 @@ +export * from "./IListUsers_Response.dto"; diff --git a/shared/lib/contexts/users/application/dto/index.ts b/shared/lib/contexts/users/application/dto/index.ts new file mode 100644 index 0000000..1a5bfe5 --- /dev/null +++ b/shared/lib/contexts/users/application/dto/index.ts @@ -0,0 +1 @@ +export * from "./IListUsers.dto"; diff --git a/shared/lib/contexts/users/application/index.ts b/shared/lib/contexts/users/application/index.ts new file mode 100644 index 0000000..0392b1b --- /dev/null +++ b/shared/lib/contexts/users/application/index.ts @@ -0,0 +1 @@ +export * from "./dto"; diff --git a/shared/lib/contexts/users/index.ts b/shared/lib/contexts/users/index.ts new file mode 100644 index 0000000..f4fe054 --- /dev/null +++ b/shared/lib/contexts/users/index.ts @@ -0,0 +1 @@ +export * from "./application";