.
This commit is contained in:
parent
173ec9e8d8
commit
38ead39cb3
@ -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",
|
||||
|
||||
@ -4,7 +4,7 @@ module.exports = {
|
||||
"9d6c903873c341816995a8be0355c6f0d6d471fc6aedacf50790e9b1e49c45b3",
|
||||
refresh_secret_key:
|
||||
"3972dc40c69327b65352ed097419213b0b75561169dba562410b85660bb1f305",
|
||||
token_expiration: "15m",
|
||||
token_expiration: "5m",
|
||||
refresh_token_expiration: "7d",
|
||||
},
|
||||
|
||||
|
||||
@ -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<never, IUseCaseError>
|
||||
| Result<User, never>;
|
||||
| Result<AuthUser, never>;
|
||||
|
||||
export class FindUserByEmailUseCase
|
||||
implements
|
||||
@ -73,7 +73,7 @@ export class FindUserByEmailUseCase
|
||||
),
|
||||
);
|
||||
}
|
||||
return Result.ok<User>(user);
|
||||
return Result.ok<AuthUser>(user);
|
||||
} catch (error: unknown) {
|
||||
const _error = error as IInfrastructureError;
|
||||
return Result.fail(
|
||||
|
||||
@ -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<never, IUseCaseError>
|
||||
| Result<User, never>;
|
||||
| Result<AuthUser, never>;
|
||||
|
||||
export class LoginUseCase
|
||||
implements IUseCase<ILogin_DTO, Promise<LoginResponseOrError>>
|
||||
@ -64,7 +64,7 @@ export class LoginUseCase
|
||||
),
|
||||
);
|
||||
}
|
||||
return Result.ok<User>(user);
|
||||
return Result.ok<AuthUser>(user);
|
||||
} catch (error: unknown) {
|
||||
const _error = error as IInfrastructureError;
|
||||
return Result.fail(
|
||||
|
||||
@ -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<IAuthRepository>,
|
||||
): Promise<User | null> => {
|
||||
): Promise<AuthUser | null> => {
|
||||
const user = await adapter
|
||||
.startTransaction()
|
||||
.complete(async (t) =>
|
||||
|
||||
115
server/src/contexts/auth/domain/entities/AuthUser.ts
Normal file
115
server/src/contexts/auth/domain/entities/AuthUser.ts
Normal file
@ -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<IAuthUserProps>
|
||||
implements IAuthUser
|
||||
{
|
||||
public static create(
|
||||
props: IAuthUserProps,
|
||||
id?: UniqueID,
|
||||
): Result<AuthUser, IDomainError> {
|
||||
//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<AuthUser>(user);
|
||||
}
|
||||
|
||||
public static async hashPassword(password): Promise<string> {
|
||||
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<string> {
|
||||
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<string> {
|
||||
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));
|
||||
@ -1 +1 @@
|
||||
export * from "./User";
|
||||
export * from "./AuthUser";
|
||||
|
||||
@ -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<any> {
|
||||
findUserByEmail(email: Email): Promise<User | null>;
|
||||
findUserByEmail(email: Email): Promise<AuthUser | null>;
|
||||
}
|
||||
|
||||
@ -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<User>
|
||||
extends SequelizeRepository<AuthUser>
|
||||
implements IAuthRepository
|
||||
{
|
||||
protected mapper: IUserMapper;
|
||||
@ -31,8 +31,8 @@ export class AuthRepository
|
||||
this.mapper = mapper;
|
||||
}
|
||||
|
||||
public async getById(id: UniqueID): Promise<User | null> {
|
||||
const rawUser: any = await this._getById("User_Model", id);
|
||||
public async getById(id: UniqueID): Promise<AuthUser | null> {
|
||||
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<User | null> {
|
||||
public async findUserByEmail(email: Email): Promise<AuthUser | null> {
|
||||
const rawUser: any = await this._getBy(
|
||||
"User_Model",
|
||||
"AuthUser_Model",
|
||||
"email",
|
||||
email.toPrimitive(),
|
||||
);
|
||||
@ -59,7 +59,7 @@ export class AuthRepository
|
||||
queryCriteria?: IQueryCriteria,
|
||||
): Promise<ICollection<any>> {
|
||||
const { rows, count } = await this._findAll(
|
||||
"User_Model",
|
||||
"AuthUser_Model",
|
||||
queryCriteria,
|
||||
/*{
|
||||
include: [], // esto es para quitar las asociaciones al hacer la consulta
|
||||
|
||||
@ -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<string | undefined>,
|
||||
status?: number | Array<number | undefined>,
|
||||
) => {
|
||||
|
||||
@ -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 = <User>this.req.user;
|
||||
const user = <AuthUser>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,
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
@ -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<string | undefined>,
|
||||
status?: number | Array<number | undefined>,
|
||||
) => {
|
||||
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 = <AuthUser>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 = <AuthUser>req.user;
|
||||
if (!user.isAdmin) {
|
||||
return generateExpressErrorResponse(req, res, httpStatus.UNAUTHORIZED);
|
||||
}
|
||||
next();
|
||||
},
|
||||
]);
|
||||
|
||||
@ -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 });
|
||||
|
||||
|
||||
@ -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);
|
||||
};
|
||||
|
||||
@ -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<User_Model, TCreationUser_Attributes, User> {}
|
||||
extends ISequelizeMapper<
|
||||
AuthUser_Model,
|
||||
TCreationUser_Attributes,
|
||||
AuthUser
|
||||
> {}
|
||||
|
||||
class UserMapper
|
||||
extends SequelizeMapper<User_Model, TCreationUser_Attributes, User>
|
||||
extends SequelizeMapper<AuthUser_Model, TCreationUser_Attributes, AuthUser>
|
||||
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<string, any> | undefined,
|
||||
) {
|
||||
return {
|
||||
|
||||
@ -6,11 +6,11 @@ import {
|
||||
Sequelize,
|
||||
} from "sequelize";
|
||||
|
||||
export type TCreationUser_Attributes = InferCreationAttributes<User_Model>;
|
||||
export type TCreationUser_Attributes = InferCreationAttributes<AuthUser_Model>;
|
||||
|
||||
export class User_Model extends Model<
|
||||
InferAttributes<User_Model>,
|
||||
InferCreationAttributes<User_Model>
|
||||
export class AuthUser_Model extends Model<
|
||||
InferAttributes<AuthUser_Model>,
|
||||
InferCreationAttributes<AuthUser_Model>
|
||||
> {
|
||||
// To avoid table creation
|
||||
/*static async sync(): Promise<any> {
|
||||
@ -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;
|
||||
};
|
||||
@ -1 +1 @@
|
||||
export * from "./user.model";
|
||||
export * from "./authUser.model";
|
||||
|
||||
@ -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 = {
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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<IListArticles_Response_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<IListArticles_Response_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;
|
||||
},
|
||||
};
|
||||
|
||||
@ -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<T>(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<IError_Response_DTO> {
|
||||
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 = <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,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@ -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<IError_Response_DTO> => {
|
||||
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 = <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,
|
||||
};
|
||||
}
|
||||
76
server/src/contexts/users/application/ListUsersUseCase.ts
Normal file
76
server/src/contexts/users/application/ListUsersUseCase.ts
Normal file
@ -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<never, IUseCaseError> // Misc errors (value objects)
|
||||
| Result<ICollection<User>, never>; // Success!
|
||||
|
||||
export class ListUsersUseCase
|
||||
implements IUseCase<IListUsersParams, Promise<ListUsersResult>>
|
||||
{
|
||||
private _adapter: ISequelizeAdapter;
|
||||
private _repositoryManager: IRepositoryManager;
|
||||
|
||||
constructor(props: {
|
||||
adapter: ISequelizeAdapter;
|
||||
repositoryManager: IRepositoryManager;
|
||||
}) {
|
||||
this._adapter = props.adapter;
|
||||
this._repositoryManager = props.repositoryManager;
|
||||
}
|
||||
|
||||
private getRepositoryByName<T>(name: string) {
|
||||
return this._repositoryManager.getRepository<T>(name);
|
||||
}
|
||||
|
||||
async execute(params: Partial<IListUsersParams>): Promise<ListUsersResult> {
|
||||
const { queryCriteria } = params;
|
||||
|
||||
return this.findUsers(queryCriteria);
|
||||
}
|
||||
|
||||
private async findUsers(queryCriteria) {
|
||||
const transaction = this._adapter.startTransaction();
|
||||
const userRepoBuilder = this.getRepositoryByName<IUserRepository>("User");
|
||||
|
||||
let users: ICollection<User> = 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,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
1
server/src/contexts/users/application/index.ts
Normal file
1
server/src/contexts/users/application/index.ts
Normal file
@ -0,0 +1 @@
|
||||
export * from "./ListUsersUseCase";
|
||||
@ -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<IUserProps> 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!);
|
||||
}
|
||||
1
server/src/contexts/users/domain/entities/index.ts
Normal file
1
server/src/contexts/users/domain/entities/index.ts
Normal file
@ -0,0 +1 @@
|
||||
export * from "./User";
|
||||
2
server/src/contexts/users/domain/index.ts
Normal file
2
server/src/contexts/users/domain/index.ts
Normal file
@ -0,0 +1,2 @@
|
||||
export * from "./entities";
|
||||
export * from "./repository";
|
||||
@ -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<any> {
|
||||
getById(id: UniqueID): Promise<User | null>;
|
||||
findAll(queryCriteria?: IQueryCriteria): Promise<ICollection<User>>;
|
||||
}
|
||||
1
server/src/contexts/users/domain/repository/index.ts
Normal file
1
server/src/contexts/users/domain/repository/index.ts
Normal file
@ -0,0 +1 @@
|
||||
export * from "./UserRepository.interface";
|
||||
1
server/src/contexts/users/index.ts
Normal file
1
server/src/contexts/users/index.ts
Normal file
@ -0,0 +1 @@
|
||||
export * from "./infrastructure";
|
||||
35
server/src/contexts/users/infrastructure/User.context.ts
Normal file
35
server/src/contexts/users/infrastructure/User.context.ts
Normal file
@ -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;
|
||||
}
|
||||
}
|
||||
85
server/src/contexts/users/infrastructure/User.repository.ts
Normal file
85
server/src/contexts/users/infrastructure/User.repository.ts
Normal file
@ -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<string, any>;
|
||||
filters: Record<string, any>;
|
||||
};
|
||||
|
||||
export class UserRepository
|
||||
extends SequelizeRepository<User>
|
||||
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<User | null> {
|
||||
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<User | null> {
|
||||
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<ICollection<any>> {
|
||||
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),
|
||||
});
|
||||
});
|
||||
};
|
||||
@ -0,0 +1 @@
|
||||
export * from "./listUsers";
|
||||
@ -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<any> {
|
||||
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 = <ICollection<User>>result.object;
|
||||
|
||||
return this.ok<IListResponse_DTO<IListUsers_Response_DTO>>(
|
||||
this.presenter.mapArray(customers, this.context, {
|
||||
page: queryCriteria.pagination.offset,
|
||||
limit: queryCriteria.pagination.limit,
|
||||
}),
|
||||
);
|
||||
} catch (e: unknown) {
|
||||
return this.fail(e as IServerError);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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,
|
||||
);
|
||||
};
|
||||
@ -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<User>,
|
||||
context: IUserContext,
|
||||
params: {
|
||||
page: number;
|
||||
limit: number;
|
||||
},
|
||||
) => IListResponse_DTO<IListUsers_Response_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<User>,
|
||||
context: IUserContext,
|
||||
params: {
|
||||
page: number;
|
||||
limit: number;
|
||||
},
|
||||
): IListResponse_DTO<IListUsers_Response_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;
|
||||
},
|
||||
};
|
||||
@ -0,0 +1 @@
|
||||
export * from "./ListUsers.presenter";
|
||||
@ -0,0 +1,2 @@
|
||||
export * from "./controllers";
|
||||
export * from "./routes";
|
||||
23
server/src/contexts/users/infrastructure/express/routes.ts
Normal file
23
server/src/contexts/users/infrastructure/express/routes.ts
Normal file
@ -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);
|
||||
};
|
||||
2
server/src/contexts/users/infrastructure/index.ts
Normal file
2
server/src/contexts/users/infrastructure/index.ts
Normal file
@ -0,0 +1,2 @@
|
||||
export * from "./express";
|
||||
export * from "./sequelize";
|
||||
@ -0,0 +1 @@
|
||||
export * from "./user.mapper";
|
||||
@ -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<User_Model, TCreationUser_Attributes, User> {}
|
||||
|
||||
class UserMapper
|
||||
extends SequelizeMapper<User_Model, TCreationUser_Attributes, User>
|
||||
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<string, any> | 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,
|
||||
});
|
||||
@ -0,0 +1 @@
|
||||
export * from "./user.model";
|
||||
@ -0,0 +1,86 @@
|
||||
import {
|
||||
DataTypes,
|
||||
InferAttributes,
|
||||
InferCreationAttributes,
|
||||
Model,
|
||||
Op,
|
||||
Sequelize,
|
||||
} from "sequelize";
|
||||
|
||||
export type TCreationUser_Attributes = InferCreationAttributes<User_Model>;
|
||||
|
||||
export class User_Model extends Model<
|
||||
InferAttributes<User_Model>,
|
||||
InferCreationAttributes<User_Model>
|
||||
> {
|
||||
// To avoid table creation
|
||||
/*static async sync(): Promise<any> {
|
||||
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;
|
||||
};
|
||||
@ -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;
|
||||
|
||||
@ -1,3 +1,5 @@
|
||||
export * from "./common";
|
||||
|
||||
export * from "./auth";
|
||||
export * from "./catalog";
|
||||
export * from "./common";
|
||||
export * from "./users";
|
||||
|
||||
@ -0,0 +1,5 @@
|
||||
export interface IListUsers_Response_DTO {
|
||||
id: string;
|
||||
name: string;
|
||||
email: string;
|
||||
}
|
||||
@ -0,0 +1 @@
|
||||
export * from "./IListUsers_Response.dto";
|
||||
1
shared/lib/contexts/users/application/dto/index.ts
Normal file
1
shared/lib/contexts/users/application/dto/index.ts
Normal file
@ -0,0 +1 @@
|
||||
export * from "./IListUsers.dto";
|
||||
1
shared/lib/contexts/users/application/index.ts
Normal file
1
shared/lib/contexts/users/application/index.ts
Normal file
@ -0,0 +1 @@
|
||||
export * from "./dto";
|
||||
1
shared/lib/contexts/users/index.ts
Normal file
1
shared/lib/contexts/users/index.ts
Normal file
@ -0,0 +1 @@
|
||||
export * from "./application";
|
||||
Loading…
Reference in New Issue
Block a user