.
This commit is contained in:
parent
4e39aacedf
commit
80db9dfb9e
@ -1,6 +1,6 @@
|
|||||||
import { logger } from "@common/infrastructure/logger";
|
import { logger } from "@common/infrastructure/logger";
|
||||||
import { globalErrorHandler } from "@common/presentation";
|
import { globalErrorHandler } from "@common/presentation";
|
||||||
import { createAuthProvider } from "@contexts/auth/infraestructure";
|
import { authProvider } from "@contexts/auth/infraestructure";
|
||||||
import dotenv from "dotenv";
|
import dotenv from "dotenv";
|
||||||
import express, { Application } from "express";
|
import express, { Application } from "express";
|
||||||
import helmet from "helmet";
|
import helmet from "helmet";
|
||||||
@ -26,7 +26,7 @@ export function createApp(): Application {
|
|||||||
|
|
||||||
// Inicializar Auth Provider
|
// Inicializar Auth Provider
|
||||||
app.use((req, res, next) => {
|
app.use((req, res, next) => {
|
||||||
createAuthProvider().initialize();
|
authProvider.initialize();
|
||||||
next();
|
next();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@ -6,29 +6,37 @@ import DailyRotateFile from "winston-daily-rotate-file";
|
|||||||
|
|
||||||
dotenv.config();
|
dotenv.config();
|
||||||
|
|
||||||
|
const splatSymbol = Symbol.for("splat");
|
||||||
|
|
||||||
const initLogger = () => {
|
const initLogger = () => {
|
||||||
const isProduction = process.env.NODE_ENV === "production";
|
const isProduction = process.env.NODE_ENV === "production";
|
||||||
|
|
||||||
const consoleFormat = format.combine(
|
const consoleFormat = format.combine(
|
||||||
format.colorize(),
|
format.colorize(),
|
||||||
format.timestamp(),
|
format.timestamp({ format: "YYYY-MM-DD HH:mm:ss" }),
|
||||||
format.align(),
|
format.align(),
|
||||||
format.splat(),
|
format.splat(),
|
||||||
|
format.metadata(),
|
||||||
format.errors({ stack: !isProduction }),
|
format.errors({ stack: !isProduction }),
|
||||||
|
|
||||||
format.printf((info) => {
|
format.printf((info) => {
|
||||||
const rid = rTracer.id();
|
const rid = rTracer.id();
|
||||||
|
|
||||||
|
if (typeof info.message === "object") {
|
||||||
|
info.message = JSON.stringify(info.message, null, 3);
|
||||||
|
}
|
||||||
|
|
||||||
let out =
|
let out =
|
||||||
isProduction && rid
|
isProduction && rid
|
||||||
? `${info.timestamp} [request-id:${rid}] - ${info.level}: [${info.label}]: ${info.message}`
|
? `${info.timestamp} [request-id:${rid}] - ${info.level}: [${info.label}]: ${info.message}`
|
||||||
: `${info.timestamp} - ${info.level}: [${info.label}]: ${info.message}`;
|
: `${info.timestamp} - ${info.level}: [${info.label}]: ${info.message}`;
|
||||||
|
|
||||||
if (info.metadata?.error) {
|
/*if (info.metadata["error"]) {
|
||||||
out = `${out} ${info.metadata.error}`;
|
out = `${out} ${info.metadata.error}`;
|
||||||
if (info.metadata?.error?.stack) {
|
if (info.metadata?.error?.stack) {
|
||||||
out = `${out} ${info.metadata.error.stack}`;
|
out = `${out} ${info.metadata.error.stack}`;
|
||||||
}
|
}
|
||||||
}
|
}*/
|
||||||
|
|
||||||
return out;
|
return out;
|
||||||
})
|
})
|
||||||
|
|||||||
@ -3,6 +3,18 @@ import { ModelDefined, Transaction } from "sequelize";
|
|||||||
import { logger } from "../logger";
|
import { logger } from "../logger";
|
||||||
|
|
||||||
export abstract class SequelizeRepository<T> implements IAggregateRootRepository<T> {
|
export abstract class SequelizeRepository<T> implements IAggregateRootRepository<T> {
|
||||||
|
protected async _findAll(
|
||||||
|
model: ModelDefined<any, any>,
|
||||||
|
//queryCriteria?: IQueryCriteria,
|
||||||
|
params: any = {},
|
||||||
|
transaction?: Transaction
|
||||||
|
): Promise<any[]> {
|
||||||
|
return model.findAll({
|
||||||
|
transaction,
|
||||||
|
...params,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
protected _findById(
|
protected _findById(
|
||||||
model: ModelDefined<any, any>,
|
model: ModelDefined<any, any>,
|
||||||
id: string,
|
id: string,
|
||||||
|
|||||||
@ -19,7 +19,3 @@ export class SequelizeTransactionManager extends TransactionManager {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export const createSequelizeTransactionManager = () => {
|
|
||||||
return new SequelizeTransactionManager();
|
|
||||||
};
|
|
||||||
|
|||||||
@ -32,13 +32,13 @@ export abstract class ExpressController {
|
|||||||
/**
|
/**
|
||||||
* 🔹 Respuesta para errores de cliente (400 Bad Request)
|
* 🔹 Respuesta para errores de cliente (400 Bad Request)
|
||||||
*/
|
*/
|
||||||
public clientError(message: string, errors?: any[]) {
|
public clientError(message: string, errors?: any[] | any) {
|
||||||
return ExpressController.errorResponse(
|
return ExpressController.errorResponse(
|
||||||
new ApiError({
|
new ApiError({
|
||||||
status: 400,
|
status: 400,
|
||||||
title: "Bad Request",
|
title: "Bad Request",
|
||||||
detail: message,
|
detail: message,
|
||||||
errors,
|
errors: Array.isArray(errors) ? errors : [errors],
|
||||||
}),
|
}),
|
||||||
this.res
|
this.res
|
||||||
);
|
);
|
||||||
|
|||||||
@ -1,44 +0,0 @@
|
|||||||
import { Result, UniqueID } from "@common/domain";
|
|
||||||
import {
|
|
||||||
AuthenticatedUser,
|
|
||||||
EmailAddress,
|
|
||||||
HashPassword,
|
|
||||||
PlainPassword,
|
|
||||||
TabContext,
|
|
||||||
Username,
|
|
||||||
} from "../domain";
|
|
||||||
import { IJWTPayload } from "../infraestructure";
|
|
||||||
|
|
||||||
export interface IAuthService {
|
|
||||||
generateAccessToken(payload: IJWTPayload): string;
|
|
||||||
generateRefreshToken(payload: IJWTPayload): string;
|
|
||||||
verifyRefreshToken(token: string): IJWTPayload;
|
|
||||||
|
|
||||||
registerUser(params: {
|
|
||||||
username: Username;
|
|
||||||
email: EmailAddress;
|
|
||||||
hashPassword: HashPassword;
|
|
||||||
}): Promise<Result<AuthenticatedUser, Error>>;
|
|
||||||
|
|
||||||
loginUser(params: {
|
|
||||||
email: EmailAddress;
|
|
||||||
plainPassword: PlainPassword;
|
|
||||||
tabId: UniqueID;
|
|
||||||
}): Promise<
|
|
||||||
Result<
|
|
||||||
{
|
|
||||||
user: AuthenticatedUser;
|
|
||||||
tabContext: TabContext;
|
|
||||||
tokens: {
|
|
||||||
accessToken: string;
|
|
||||||
refreshToken: string;
|
|
||||||
};
|
|
||||||
},
|
|
||||||
Error
|
|
||||||
>
|
|
||||||
>;
|
|
||||||
|
|
||||||
logoutUser(params: { email: EmailAddress; tabId: UniqueID }): Promise<Result<void, Error>>;
|
|
||||||
|
|
||||||
getUserByEmail(params: { email: EmailAddress }): Promise<Result<AuthenticatedUser, Error>>;
|
|
||||||
}
|
|
||||||
@ -1,238 +0,0 @@
|
|||||||
import { Result, UniqueID } from "@common/domain";
|
|
||||||
import { ITransactionManager } from "@common/infrastructure/database";
|
|
||||||
import {
|
|
||||||
AuthenticatedUser,
|
|
||||||
EmailAddress,
|
|
||||||
HashPassword,
|
|
||||||
IAuthenticatedUserRepository,
|
|
||||||
TabContext,
|
|
||||||
Username,
|
|
||||||
} from "../domain";
|
|
||||||
import { ITabContextRepository } from "../domain/repositories/tab-context-repository.interface";
|
|
||||||
import { JwtHelper } from "../infraestructure/passport/jwt.helper";
|
|
||||||
import { IJWTPayload } from "../infraestructure/passport/passport-auth-provider";
|
|
||||||
import { IAuthService } from "./auth-service.interface";
|
|
||||||
|
|
||||||
const ACCESS_EXPIRATION = process.env.JWT_ACCESS_EXPIRATION || "1h";
|
|
||||||
const REFRESH_EXPIRATION = process.env.JWT_REFRESH_EXPIRATION || "7d";
|
|
||||||
|
|
||||||
export class AuthService implements IAuthService {
|
|
||||||
private readonly _userRepo!: IAuthenticatedUserRepository;
|
|
||||||
private readonly _tabContactRepo!: ITabContextRepository;
|
|
||||||
private readonly _transactionManager!: ITransactionManager;
|
|
||||||
|
|
||||||
constructor(
|
|
||||||
userRepo: IAuthenticatedUserRepository,
|
|
||||||
tabContextRepo: ITabContextRepository,
|
|
||||||
transactionManager: ITransactionManager
|
|
||||||
) {
|
|
||||||
this._userRepo = userRepo;
|
|
||||||
this._tabContactRepo = tabContextRepo;
|
|
||||||
this._transactionManager = transactionManager;
|
|
||||||
}
|
|
||||||
|
|
||||||
generateAccessToken(payload: IJWTPayload): string {
|
|
||||||
return JwtHelper.generateToken(payload, ACCESS_EXPIRATION);
|
|
||||||
}
|
|
||||||
|
|
||||||
generateRefreshToken(payload: IJWTPayload): string {
|
|
||||||
return JwtHelper.generateToken(payload, REFRESH_EXPIRATION);
|
|
||||||
}
|
|
||||||
|
|
||||||
verifyRefreshToken(token: string): IJWTPayload {
|
|
||||||
return JwtHelper.verifyToken(token);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
*
|
|
||||||
* Registra un nuevo usuario en la base de datos bajo transacción.
|
|
||||||
*/
|
|
||||||
async registerUser(params: {
|
|
||||||
username: Username;
|
|
||||||
email: EmailAddress;
|
|
||||||
hashPassword: HashPassword;
|
|
||||||
}): Promise<Result<AuthenticatedUser, Error>> {
|
|
||||||
try {
|
|
||||||
return await this._transactionManager.complete(async (transaction) => {
|
|
||||||
const { username, email, hashPassword } = params;
|
|
||||||
|
|
||||||
// Verificar si el usuario ya existe
|
|
||||||
const userExists = await this._userRepo.userExists(email, transaction);
|
|
||||||
if (userExists.isSuccess && userExists.data) {
|
|
||||||
return Result.fail(new Error("Email is already registered"));
|
|
||||||
}
|
|
||||||
|
|
||||||
const newUserId = UniqueID.generateNewID().data;
|
|
||||||
|
|
||||||
const userOrError = AuthenticatedUser.create(
|
|
||||||
{
|
|
||||||
username,
|
|
||||||
email,
|
|
||||||
hashPassword,
|
|
||||||
roles: ["USER"],
|
|
||||||
},
|
|
||||||
newUserId
|
|
||||||
);
|
|
||||||
|
|
||||||
if (userOrError.isFailure) {
|
|
||||||
return Result.fail(userOrError.error);
|
|
||||||
}
|
|
||||||
|
|
||||||
const createdResult = await this._userRepo.createUser(userOrError.data, transaction);
|
|
||||||
|
|
||||||
if (createdResult.isFailure) {
|
|
||||||
return Result.fail(createdResult.error);
|
|
||||||
}
|
|
||||||
|
|
||||||
return Result.ok(userOrError.data);
|
|
||||||
});
|
|
||||||
} catch (error: unknown) {
|
|
||||||
return Result.fail(error as Error);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
*
|
|
||||||
* Autentica a un usuario validando su email y contraseña.
|
|
||||||
*/
|
|
||||||
async loginUser(params: {
|
|
||||||
email: EmailAddress;
|
|
||||||
plainPassword: HashPassword;
|
|
||||||
tabId: UniqueID;
|
|
||||||
}): Promise<
|
|
||||||
Result<
|
|
||||||
{
|
|
||||||
user: AuthenticatedUser;
|
|
||||||
tabContext: TabContext;
|
|
||||||
tokens: {
|
|
||||||
accessToken: string;
|
|
||||||
refreshToken: string;
|
|
||||||
};
|
|
||||||
},
|
|
||||||
Error
|
|
||||||
>
|
|
||||||
> {
|
|
||||||
try {
|
|
||||||
return await this._transactionManager.complete(async (transaction) => {
|
|
||||||
const { email, plainPassword, tabId } = params;
|
|
||||||
|
|
||||||
// Verificar que el tab ID está definido
|
|
||||||
if (!tabId.isDefined()) {
|
|
||||||
return Result.fail(new Error("Invalid tab id"));
|
|
||||||
}
|
|
||||||
|
|
||||||
// 🔹 Verificar si el usuario existe en la base de datos
|
|
||||||
const userResult = await this._userRepo.getUserByEmail(email, transaction);
|
|
||||||
if (userResult.isFailure) {
|
|
||||||
return Result.fail(new Error("Invalid email or password"));
|
|
||||||
}
|
|
||||||
|
|
||||||
const user = userResult.data;
|
|
||||||
|
|
||||||
// 🔹 Verificar que la contraseña sea correcta
|
|
||||||
const isValidPassword = await user.verifyPassword(plainPassword);
|
|
||||||
if (!isValidPassword) {
|
|
||||||
return Result.fail(new Error("Invalid email or password"));
|
|
||||||
}
|
|
||||||
|
|
||||||
// Registrar o actualizar el contexto de ese tab ID
|
|
||||||
const contextOrError = TabContext.create({
|
|
||||||
userId: user.id,
|
|
||||||
tabId: tabId,
|
|
||||||
});
|
|
||||||
|
|
||||||
if (contextOrError.isFailure) {
|
|
||||||
return Result.fail(new Error("Error creating user context"));
|
|
||||||
}
|
|
||||||
|
|
||||||
const tabContext = contextOrError.data;
|
|
||||||
|
|
||||||
await this._tabContactRepo.registerContextByTabId(tabContext, transaction);
|
|
||||||
|
|
||||||
// 🔹 Generar Access Token y Refresh Token
|
|
||||||
const accessToken = this.generateAccessToken({
|
|
||||||
user_id: user.id.toString(),
|
|
||||||
email: email.toString(),
|
|
||||||
tab_id: tabId.toString(),
|
|
||||||
roles: ["USER"],
|
|
||||||
});
|
|
||||||
|
|
||||||
const refreshToken = this.generateRefreshToken({
|
|
||||||
user_id: user.id.toString(),
|
|
||||||
email: email.toString(),
|
|
||||||
tab_id: tabId.toString(),
|
|
||||||
roles: ["USER"],
|
|
||||||
});
|
|
||||||
|
|
||||||
return Result.ok({
|
|
||||||
user,
|
|
||||||
tabContext,
|
|
||||||
tokens: {
|
|
||||||
accessToken,
|
|
||||||
refreshToken,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
});
|
|
||||||
} catch (error: unknown) {
|
|
||||||
return Result.fail(error as Error);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
*
|
|
||||||
* Autentica a un usuario validando su email y contraseña.
|
|
||||||
*/
|
|
||||||
async logoutUser(params: { email: EmailAddress; tabId: UniqueID }): Promise<Result<void, Error>> {
|
|
||||||
try {
|
|
||||||
return await this._transactionManager.complete(async (transaction) => {
|
|
||||||
const { email, tabId } = params;
|
|
||||||
|
|
||||||
// Verificar que el tab ID está definido
|
|
||||||
if (!tabId.isDefined()) {
|
|
||||||
return Result.fail(new Error("Invalid tab id"));
|
|
||||||
}
|
|
||||||
|
|
||||||
// 🔹 Verificar si el usuario existe en la base de datos
|
|
||||||
const userResult = await this._userRepo.getUserByEmail(email, transaction);
|
|
||||||
if (userResult.isFailure) {
|
|
||||||
return Result.fail(new Error("Invalid email or password"));
|
|
||||||
}
|
|
||||||
|
|
||||||
const user = userResult.data;
|
|
||||||
|
|
||||||
const contextOrError = TabContext.create({
|
|
||||||
userId: user.id,
|
|
||||||
tabId: tabId,
|
|
||||||
});
|
|
||||||
|
|
||||||
if (contextOrError.isFailure) {
|
|
||||||
return Result.fail(new Error("Error creating user context"));
|
|
||||||
}
|
|
||||||
|
|
||||||
// Desregistrar el contexto de ese tab ID
|
|
||||||
await this._tabContactRepo.unregisterContextByTabId(contextOrError.data, transaction);
|
|
||||||
|
|
||||||
return Result.ok();
|
|
||||||
});
|
|
||||||
} catch (error: unknown) {
|
|
||||||
return Result.fail(error as Error);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async getUserByEmail(params: { email: EmailAddress }): Promise<Result<AuthenticatedUser, Error>> {
|
|
||||||
try {
|
|
||||||
return await this._transactionManager.complete(async (transaction) => {
|
|
||||||
const { email } = params;
|
|
||||||
const userResult = await this._userRepo.getUserByEmail(email, transaction);
|
|
||||||
|
|
||||||
if (userResult.isFailure || !userResult.data) {
|
|
||||||
return Result.fail(new Error("Invalid email or password"));
|
|
||||||
}
|
|
||||||
|
|
||||||
return Result.ok(userResult.data);
|
|
||||||
});
|
|
||||||
} catch (error: unknown) {
|
|
||||||
return Result.fail(error as Error);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,24 +1,4 @@
|
|||||||
import { createSequelizeTransactionManager } from "@common/infrastructure";
|
export * from "./list-users";
|
||||||
|
export * from "./login";
|
||||||
import { createAuthenticatedUserRepository, createTabContextRepository } from "../infraestructure";
|
export * from "./logout";
|
||||||
import { IAuthService } from "./auth-service.interface";
|
export * from "./register";
|
||||||
import { AuthService } from "./auth.service";
|
|
||||||
import { ITabContextService } from "./tab-context-service.interface";
|
|
||||||
import { TabContextService } from "./tab-context.service";
|
|
||||||
|
|
||||||
export * from "./auth-service.interface";
|
|
||||||
|
|
||||||
export const createAuthService = (): IAuthService => {
|
|
||||||
const transactionManager = createSequelizeTransactionManager();
|
|
||||||
const userRepo = createAuthenticatedUserRepository();
|
|
||||||
const tabContextRepo = createTabContextRepository();
|
|
||||||
|
|
||||||
return new AuthService(userRepo, tabContextRepo, transactionManager);
|
|
||||||
};
|
|
||||||
|
|
||||||
export const createTabContextService = (): ITabContextService => {
|
|
||||||
const transactionManager = createSequelizeTransactionManager();
|
|
||||||
const tabContextRepository = createTabContextRepository();
|
|
||||||
|
|
||||||
return new TabContextService(tabContextRepository, transactionManager);
|
|
||||||
};
|
|
||||||
|
|||||||
@ -0,0 +1 @@
|
|||||||
|
export * from "./list-users.use-case";
|
||||||
@ -0,0 +1,17 @@
|
|||||||
|
import { Result } from "@common/domain";
|
||||||
|
import { ITransactionManager } from "@common/infrastructure/database";
|
||||||
|
import { User } from "@contexts/auth/domain";
|
||||||
|
import { IUserService } from "@contexts/auth/domain/services";
|
||||||
|
|
||||||
|
export class ListUsersUseCase {
|
||||||
|
constructor(
|
||||||
|
private readonly userService: IUserService,
|
||||||
|
private readonly transactionManager: ITransactionManager
|
||||||
|
) {}
|
||||||
|
|
||||||
|
public async execute(): Promise<Result<User[], Error>> {
|
||||||
|
return await this.transactionManager.complete(async (transaction) => {
|
||||||
|
return await this.userService.findUsers(transaction);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
1
apps/server/src/contexts/auth/application/login/index.ts
Normal file
1
apps/server/src/contexts/auth/application/login/index.ts
Normal file
@ -0,0 +1 @@
|
|||||||
|
export * from "./login.use-case";
|
||||||
@ -0,0 +1,16 @@
|
|||||||
|
import { ITransactionManager } from "@common/infrastructure/database";
|
||||||
|
import { LoginData } from "@contexts/auth/domain";
|
||||||
|
import { IAuthService } from "@contexts/auth/domain/services";
|
||||||
|
|
||||||
|
export class LoginUseCase {
|
||||||
|
constructor(
|
||||||
|
private readonly authService: IAuthService,
|
||||||
|
private readonly transactionManager: ITransactionManager
|
||||||
|
) {}
|
||||||
|
|
||||||
|
public async execute(loginData: LoginData) {
|
||||||
|
return await this.transactionManager.complete(async (transaction) => {
|
||||||
|
return await this.authService.loginUser(loginData, transaction);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1 @@
|
|||||||
|
export * from "./logout.use-case";
|
||||||
@ -0,0 +1,16 @@
|
|||||||
|
import { ITransactionManager } from "@common/infrastructure/database";
|
||||||
|
import { LogoutData } from "@contexts/auth/domain";
|
||||||
|
import { IAuthService } from "@contexts/auth/domain/services";
|
||||||
|
|
||||||
|
export class LogoutUseCase {
|
||||||
|
constructor(
|
||||||
|
private readonly authService: IAuthService,
|
||||||
|
private readonly transactionManager: ITransactionManager
|
||||||
|
) {}
|
||||||
|
|
||||||
|
public async execute(logoutData: LogoutData) {
|
||||||
|
return await this.transactionManager.complete(async (transaction) => {
|
||||||
|
return await this.authService.logoutUser(logoutData, transaction);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1 @@
|
|||||||
|
export * from "./login.use-case";
|
||||||
@ -0,0 +1,16 @@
|
|||||||
|
import { ITransactionManager } from "@common/infrastructure/database";
|
||||||
|
import { LoginData } from "@contexts/auth/domain";
|
||||||
|
import { IAuthService } from "@contexts/auth/domain/services";
|
||||||
|
|
||||||
|
export class LoginUseCase {
|
||||||
|
constructor(
|
||||||
|
private readonly authService: IAuthService,
|
||||||
|
private readonly transactionManager: ITransactionManager
|
||||||
|
) {}
|
||||||
|
|
||||||
|
public async execute(loginData: LoginData) {
|
||||||
|
return await this.transactionManager.complete(async (transaction) => {
|
||||||
|
return await this.authService.loginUser(loginData, transaction);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1 @@
|
|||||||
|
export * from "./refresh-token.use-case";
|
||||||
@ -0,0 +1,29 @@
|
|||||||
|
import { Result } from "@common/domain";
|
||||||
|
import { ITransactionManager } from "@common/infrastructure/database";
|
||||||
|
import { Token } from "@contexts/auth/domain";
|
||||||
|
import { IAuthService } from "@contexts/auth/domain/services";
|
||||||
|
|
||||||
|
export class RefreshTokenUseCase {
|
||||||
|
constructor(
|
||||||
|
private readonly authService: IAuthService,
|
||||||
|
private readonly transactionManager: ITransactionManager
|
||||||
|
) {}
|
||||||
|
|
||||||
|
public async execute(token: Token) {
|
||||||
|
return await this.transactionManager.complete(async (transaction) => {
|
||||||
|
const payload = this.authService.verifyRefreshToken(token);
|
||||||
|
if (!payload || !payload.email || !payload.user_id || !payload.tab_id || !payload.roles) {
|
||||||
|
return Result.fail(new Error("Invalid input data"));
|
||||||
|
}
|
||||||
|
|
||||||
|
const { user_id, tab_id, email, roles } = payload;
|
||||||
|
|
||||||
|
return this.authService.generateRefreshToken({
|
||||||
|
user_id,
|
||||||
|
tab_id,
|
||||||
|
email,
|
||||||
|
roles,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,8 +0,0 @@
|
|||||||
import { Result, UniqueID } from "@common/domain";
|
|
||||||
import { TabContext } from "../domain";
|
|
||||||
|
|
||||||
export interface ITabContextService {
|
|
||||||
getContextByTabId(tabId: UniqueID): Promise<Result<TabContext, Error>>;
|
|
||||||
createContext(params: { tabId: UniqueID; userId: UniqueID }): Promise<Result<TabContext, Error>>;
|
|
||||||
removeContext(params: { tabId: UniqueID; userId: UniqueID }): Promise<Result<void, Error>>;
|
|
||||||
}
|
|
||||||
@ -1,105 +0,0 @@
|
|||||||
import { Result, UniqueID } from "@common/domain";
|
|
||||||
import { ITransactionManager } from "@common/infrastructure/database";
|
|
||||||
import { TabContext } from "../domain";
|
|
||||||
import { ITabContextRepository } from "../domain/repositories/tab-context-repository.interface";
|
|
||||||
import { ITabContextService } from "./tab-context-service.interface";
|
|
||||||
|
|
||||||
export class TabContextService implements ITabContextService {
|
|
||||||
private readonly _respository!: ITabContextRepository;
|
|
||||||
private readonly _transactionManager!: ITransactionManager;
|
|
||||||
|
|
||||||
constructor(repository: ITabContextRepository, transactionManager: ITransactionManager) {
|
|
||||||
this._respository = repository;
|
|
||||||
this._transactionManager = transactionManager;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Obtiene el contexto de una pestaña por su ID
|
|
||||||
*/
|
|
||||||
async getContextByTabId(tabId: UniqueID): Promise<Result<TabContext, Error>> {
|
|
||||||
try {
|
|
||||||
return await this._transactionManager.complete(async (transaction) => {
|
|
||||||
// Verificar si la pestaña existe
|
|
||||||
const tabContextOrError = await this._respository.getContextByTabId(tabId, transaction);
|
|
||||||
if (tabContextOrError.isSuccess && !tabContextOrError.data) {
|
|
||||||
return Result.fail(new Error("Invalid or expired Tab ID"));
|
|
||||||
}
|
|
||||||
|
|
||||||
if (tabContextOrError.isFailure) {
|
|
||||||
return Result.fail(tabContextOrError.error);
|
|
||||||
}
|
|
||||||
|
|
||||||
return Result.ok(tabContextOrError.data);
|
|
||||||
});
|
|
||||||
} catch (error: unknown) {
|
|
||||||
return Result.fail(error as Error);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Registra un nuevo contexto de pestaña para un usuario
|
|
||||||
*/
|
|
||||||
async createContext(params: {
|
|
||||||
tabId: UniqueID;
|
|
||||||
userId: UniqueID;
|
|
||||||
}): Promise<Result<TabContext, Error>> {
|
|
||||||
const { tabId, userId } = params;
|
|
||||||
|
|
||||||
if (!userId || !tabId) {
|
|
||||||
return Result.fail(new Error("User ID and Tab ID are required"));
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
return await this._transactionManager.complete(async (transaction) => {
|
|
||||||
const contextOrError = TabContext.create(
|
|
||||||
{
|
|
||||||
userId,
|
|
||||||
tabId,
|
|
||||||
},
|
|
||||||
UniqueID.generateNewID().data
|
|
||||||
);
|
|
||||||
|
|
||||||
if (contextOrError.isFailure) {
|
|
||||||
return Result.fail(contextOrError.error);
|
|
||||||
}
|
|
||||||
|
|
||||||
await this._respository.registerContextByTabId(contextOrError.data, transaction);
|
|
||||||
|
|
||||||
return Result.ok(contextOrError.data);
|
|
||||||
});
|
|
||||||
} catch (error: unknown) {
|
|
||||||
return Result.fail(error as Error);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Elimina un contexto de pestaña por su ID
|
|
||||||
*/
|
|
||||||
async removeContext(params: { tabId: UniqueID; userId: UniqueID }): Promise<Result<void, Error>> {
|
|
||||||
const { tabId, userId } = params;
|
|
||||||
|
|
||||||
if (!userId || !tabId) {
|
|
||||||
return Result.fail(new Error("User ID and Tab ID are required"));
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
return await this._transactionManager.complete(async (transaction) => {
|
|
||||||
const contextOrError = TabContext.create(
|
|
||||||
{
|
|
||||||
userId,
|
|
||||||
tabId,
|
|
||||||
},
|
|
||||||
UniqueID.generateNewID().data
|
|
||||||
);
|
|
||||||
|
|
||||||
if (contextOrError.isFailure) {
|
|
||||||
return Result.fail(contextOrError.error);
|
|
||||||
}
|
|
||||||
|
|
||||||
return await this._respository.unregisterContextByTabId(contextOrError.data, transaction);
|
|
||||||
});
|
|
||||||
} catch (error: unknown) {
|
|
||||||
return Result.fail(error as Error);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1 +1,4 @@
|
|||||||
export * from "./authenticated-user";
|
export * from "./authenticated-user";
|
||||||
|
|
||||||
|
export * from "./role";
|
||||||
|
export * from "./user";
|
||||||
|
|||||||
14
apps/server/src/contexts/auth/domain/aggregates/role.ts
Normal file
14
apps/server/src/contexts/auth/domain/aggregates/role.ts
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
import { AggregateRoot, Result, UniqueID } from "@common/domain";
|
||||||
|
|
||||||
|
export interface IRoleProps {}
|
||||||
|
|
||||||
|
export interface IRole {}
|
||||||
|
|
||||||
|
export class Role extends AggregateRoot<IRoleProps> implements IRole {
|
||||||
|
static create(props: IRoleProps, id: UniqueID): Result<Role, Error> {
|
||||||
|
const role = new Role(props, id);
|
||||||
|
return Result.ok(role);
|
||||||
|
}
|
||||||
|
|
||||||
|
toPersistenceData(): any {}
|
||||||
|
}
|
||||||
74
apps/server/src/contexts/auth/domain/aggregates/user.ts
Normal file
74
apps/server/src/contexts/auth/domain/aggregates/user.ts
Normal file
@ -0,0 +1,74 @@
|
|||||||
|
import { AggregateRoot, Result, UniqueID } from "@common/domain";
|
||||||
|
import { UserAuthenticatedEvent } from "../events";
|
||||||
|
import { EmailAddress, Username } from "../value-objects";
|
||||||
|
|
||||||
|
export interface IUserProps {
|
||||||
|
username: Username;
|
||||||
|
email: EmailAddress;
|
||||||
|
roles: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IUser {
|
||||||
|
username: Username;
|
||||||
|
email: EmailAddress;
|
||||||
|
|
||||||
|
isUser: boolean;
|
||||||
|
isAdmin: boolean;
|
||||||
|
|
||||||
|
hasRole(role: string): boolean;
|
||||||
|
hasRoles(roles: string[]): boolean;
|
||||||
|
getRoles(): string[];
|
||||||
|
toPersistenceData(): any;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class User extends AggregateRoot<IUserProps> implements IUser {
|
||||||
|
static create(props: IUserProps, id: UniqueID): Result<User, Error> {
|
||||||
|
const user = new User(props, id);
|
||||||
|
|
||||||
|
// 🔹 Disparar evento de dominio "UserAuthenticatedEvent"
|
||||||
|
const { email } = props;
|
||||||
|
user.addDomainEvent(new UserAuthenticatedEvent(id, email.toString()));
|
||||||
|
|
||||||
|
return Result.ok(user);
|
||||||
|
}
|
||||||
|
|
||||||
|
getRoles(): string[] {
|
||||||
|
return this._props.roles;
|
||||||
|
}
|
||||||
|
|
||||||
|
hasRole(role: string): boolean {
|
||||||
|
return (this._props.roles || []).some((r) => r === role);
|
||||||
|
}
|
||||||
|
|
||||||
|
hasRoles(roles: string[]): boolean {
|
||||||
|
return roles.map((rol) => this.hasRole(rol)).some((value) => value != false);
|
||||||
|
}
|
||||||
|
|
||||||
|
get username(): Username {
|
||||||
|
return this._props.username;
|
||||||
|
}
|
||||||
|
|
||||||
|
get email(): EmailAddress {
|
||||||
|
return this._props.email;
|
||||||
|
}
|
||||||
|
|
||||||
|
get isUser(): boolean {
|
||||||
|
return this.hasRole("user");
|
||||||
|
}
|
||||||
|
|
||||||
|
get isAdmin(): boolean {
|
||||||
|
return this.hasRole("admin");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 🔹 Devuelve una representación lista para persistencia
|
||||||
|
*/
|
||||||
|
toPersistenceData(): any {
|
||||||
|
return {
|
||||||
|
id: this._id.toString(),
|
||||||
|
username: this._props.username.toString(),
|
||||||
|
email: this._props.email.toString(),
|
||||||
|
roles: this._props.roles.map((role) => role.toString()),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1 +1,4 @@
|
|||||||
|
export * from "./login-data";
|
||||||
|
export * from "./logout-data";
|
||||||
|
export * from "./register-data";
|
||||||
export * from "./tab-context";
|
export * from "./tab-context";
|
||||||
|
|||||||
59
apps/server/src/contexts/auth/domain/entities/login-data.ts
Normal file
59
apps/server/src/contexts/auth/domain/entities/login-data.ts
Normal file
@ -0,0 +1,59 @@
|
|||||||
|
import { DomainEntity, Result, UniqueID } from "@common/domain";
|
||||||
|
import { EmailAddress, PlainPassword } from "../value-objects";
|
||||||
|
|
||||||
|
export interface ILoginDataProps {
|
||||||
|
email: EmailAddress;
|
||||||
|
plainPassword: PlainPassword;
|
||||||
|
tabId: UniqueID;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ILoginData {
|
||||||
|
email: EmailAddress;
|
||||||
|
plainPassword: PlainPassword;
|
||||||
|
tabId: UniqueID;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class LoginData extends DomainEntity<ILoginDataProps> implements ILoginData {
|
||||||
|
static create(props: ILoginDataProps): Result<LoginData, Error> {
|
||||||
|
return Result.ok(new this(props));
|
||||||
|
}
|
||||||
|
|
||||||
|
static createFromPrimitives(props: {
|
||||||
|
email: string;
|
||||||
|
plainPassword: string;
|
||||||
|
tabId: string;
|
||||||
|
}): Result<LoginData, Error> {
|
||||||
|
const { email, plainPassword, tabId } = props;
|
||||||
|
const emailOrError = EmailAddress.create(email);
|
||||||
|
const plainPasswordOrError = PlainPassword.create(plainPassword);
|
||||||
|
const tabIdOrError = UniqueID.create(tabId);
|
||||||
|
|
||||||
|
const result = Result.combine([emailOrError, plainPasswordOrError, tabIdOrError]);
|
||||||
|
|
||||||
|
if (result.isFailure) {
|
||||||
|
return Result.fail(result.error);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (emailOrError.data.isEmpty()) {
|
||||||
|
return Result.fail(new Error("Email is required"));
|
||||||
|
}
|
||||||
|
|
||||||
|
return LoginData.create({
|
||||||
|
email: emailOrError.data,
|
||||||
|
plainPassword: plainPasswordOrError.data,
|
||||||
|
tabId: tabIdOrError.data,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
get email(): EmailAddress {
|
||||||
|
return this._props.email;
|
||||||
|
}
|
||||||
|
|
||||||
|
get plainPassword(): PlainPassword {
|
||||||
|
return this._props.plainPassword;
|
||||||
|
}
|
||||||
|
|
||||||
|
get tabId(): UniqueID {
|
||||||
|
return this._props.tabId;
|
||||||
|
}
|
||||||
|
}
|
||||||
47
apps/server/src/contexts/auth/domain/entities/logout-data.ts
Normal file
47
apps/server/src/contexts/auth/domain/entities/logout-data.ts
Normal file
@ -0,0 +1,47 @@
|
|||||||
|
import { DomainEntity, Result, UniqueID } from "@common/domain";
|
||||||
|
import { EmailAddress } from "../value-objects";
|
||||||
|
|
||||||
|
export interface ILogoutDataProps {
|
||||||
|
email: EmailAddress;
|
||||||
|
tabId: UniqueID;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ILogoutData {
|
||||||
|
email: EmailAddress;
|
||||||
|
tabId: UniqueID;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class LogoutData extends DomainEntity<ILogoutDataProps> implements ILogoutData {
|
||||||
|
static create(props: ILogoutDataProps): Result<LogoutData, Error> {
|
||||||
|
return Result.ok(new this(props));
|
||||||
|
}
|
||||||
|
|
||||||
|
static createFromPrimitives(props: { email: string; tabId: string }): Result<LogoutData, Error> {
|
||||||
|
const { email, tabId } = props;
|
||||||
|
const emailOrError = EmailAddress.create(email);
|
||||||
|
const tabIdOrError = UniqueID.create(tabId);
|
||||||
|
|
||||||
|
const result = Result.combine([emailOrError, tabIdOrError]);
|
||||||
|
|
||||||
|
if (result.isFailure) {
|
||||||
|
return Result.fail(result.error);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (emailOrError.data.isEmpty()) {
|
||||||
|
return Result.fail(new Error("Email is required"));
|
||||||
|
}
|
||||||
|
|
||||||
|
return LogoutData.create({
|
||||||
|
email: emailOrError.data,
|
||||||
|
tabId: tabIdOrError.data,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
get email(): EmailAddress {
|
||||||
|
return this._props.email;
|
||||||
|
}
|
||||||
|
|
||||||
|
get tabId(): UniqueID {
|
||||||
|
return this._props.tabId;
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,59 @@
|
|||||||
|
import { DomainEntity, Result } from "@common/domain";
|
||||||
|
import { EmailAddress, HashPassword, Username } from "../value-objects";
|
||||||
|
|
||||||
|
export interface IRegisterDataProps {
|
||||||
|
username: Username;
|
||||||
|
email: EmailAddress;
|
||||||
|
hashPassword: HashPassword;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IRegisterData {
|
||||||
|
username: Username;
|
||||||
|
email: EmailAddress;
|
||||||
|
hashPassword: HashPassword;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class RegisterData extends DomainEntity<IRegisterDataProps> implements IRegisterData {
|
||||||
|
static create(props: IRegisterDataProps): Result<RegisterData, Error> {
|
||||||
|
return Result.ok(new this(props));
|
||||||
|
}
|
||||||
|
|
||||||
|
static createFromPrimitives(props: {
|
||||||
|
username: string;
|
||||||
|
email: string;
|
||||||
|
plainPassword: string;
|
||||||
|
}): Result<RegisterData, Error> {
|
||||||
|
const { username, email, plainPassword } = props;
|
||||||
|
|
||||||
|
const userNameOrError = Username.create(username);
|
||||||
|
const emailOrError = EmailAddress.create(email);
|
||||||
|
const hashPasswordOrError = HashPassword.createFromPlainText(plainPassword);
|
||||||
|
|
||||||
|
const result = Result.combine([userNameOrError, emailOrError, hashPasswordOrError]);
|
||||||
|
|
||||||
|
if (result.isFailure) {
|
||||||
|
return Result.fail(result.error);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (emailOrError.data.isEmpty()) {
|
||||||
|
return Result.fail(new Error("Email is required"));
|
||||||
|
}
|
||||||
|
return RegisterData.create({
|
||||||
|
username: userNameOrError.data,
|
||||||
|
email: emailOrError.data,
|
||||||
|
hashPassword: hashPasswordOrError.data,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
get username(): Username {
|
||||||
|
return this._props.username;
|
||||||
|
}
|
||||||
|
|
||||||
|
get email(): EmailAddress {
|
||||||
|
return this._props.email;
|
||||||
|
}
|
||||||
|
|
||||||
|
get hashPassword(): HashPassword {
|
||||||
|
return this._props.hashPassword;
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,2 +1,4 @@
|
|||||||
export * from "./authenticated-user-repository.interface";
|
export * from "./authenticated-user-repository.interface";
|
||||||
export * from "./tab-context-repository.interface";
|
export * from "./tab-context-repository.interface";
|
||||||
|
export * from "./user-permission-repository.interface";
|
||||||
|
export * from "./user-repository.interface";
|
||||||
|
|||||||
@ -0,0 +1 @@
|
|||||||
|
export interface IUserPermissionRepository {}
|
||||||
@ -0,0 +1,9 @@
|
|||||||
|
import { Result, UniqueID } from "@common/domain";
|
||||||
|
import { User } from "../aggregates";
|
||||||
|
import { EmailAddress } from "../value-objects";
|
||||||
|
|
||||||
|
export interface IUserRepository {
|
||||||
|
findAll(transaction?: any): Promise<Result<User[], Error>>;
|
||||||
|
findById(id: UniqueID, transaction?: any): Promise<Result<User, Error>>;
|
||||||
|
findByEmail(email: EmailAddress, transaction?: any): Promise<Result<User, Error>>;
|
||||||
|
}
|
||||||
@ -0,0 +1,42 @@
|
|||||||
|
import { Result } from "@common/domain";
|
||||||
|
import {
|
||||||
|
AuthenticatedUser,
|
||||||
|
EmailAddress,
|
||||||
|
LoginData,
|
||||||
|
LogoutData,
|
||||||
|
RegisterData,
|
||||||
|
TabContext,
|
||||||
|
Token,
|
||||||
|
} from "..";
|
||||||
|
import { IJWTPayload } from "../../infraestructure";
|
||||||
|
|
||||||
|
export interface IAuthService {
|
||||||
|
generateAccessToken(payload: IJWTPayload): Result<Token, Error>;
|
||||||
|
generateRefreshToken(payload: IJWTPayload): Result<Token, Error>;
|
||||||
|
verifyRefreshToken(token: Token): IJWTPayload;
|
||||||
|
|
||||||
|
registerUser(
|
||||||
|
registerData: RegisterData,
|
||||||
|
transaction?: any
|
||||||
|
): Promise<Result<AuthenticatedUser, Error>>;
|
||||||
|
|
||||||
|
loginUser(
|
||||||
|
loginData: LoginData,
|
||||||
|
transaction?: any
|
||||||
|
): Promise<
|
||||||
|
Result<
|
||||||
|
{
|
||||||
|
user: AuthenticatedUser;
|
||||||
|
tabContext: TabContext;
|
||||||
|
tokens: {
|
||||||
|
accessToken: Token;
|
||||||
|
refreshToken: Token;
|
||||||
|
};
|
||||||
|
},
|
||||||
|
Error
|
||||||
|
>
|
||||||
|
>;
|
||||||
|
|
||||||
|
logoutUser(logoutData: LogoutData, transaction?: any): Promise<Result<void, Error>>;
|
||||||
|
getUserByEmail(email: EmailAddress, transaction?: any): Promise<Result<AuthenticatedUser, Error>>;
|
||||||
|
}
|
||||||
214
apps/server/src/contexts/auth/domain/services/auth.service.ts
Normal file
214
apps/server/src/contexts/auth/domain/services/auth.service.ts
Normal file
@ -0,0 +1,214 @@
|
|||||||
|
import { Result, UniqueID } from "@common/domain";
|
||||||
|
import {
|
||||||
|
AuthenticatedUser,
|
||||||
|
EmailAddress,
|
||||||
|
IAuthenticatedUserRepository,
|
||||||
|
LoginData,
|
||||||
|
RegisterData,
|
||||||
|
TabContext,
|
||||||
|
Token,
|
||||||
|
} from "..";
|
||||||
|
import { JwtHelper } from "../../infraestructure/passport/jwt.helper";
|
||||||
|
import { IJWTPayload } from "../../infraestructure/passport/passport-auth-provider";
|
||||||
|
import { ITabContextRepository } from "../repositories/tab-context-repository.interface";
|
||||||
|
import { IAuthService } from "./auth-service.interface";
|
||||||
|
|
||||||
|
const ACCESS_EXPIRATION = process.env.JWT_ACCESS_EXPIRATION || "1h";
|
||||||
|
const REFRESH_EXPIRATION = process.env.JWT_REFRESH_EXPIRATION || "7d";
|
||||||
|
|
||||||
|
export class AuthService implements IAuthService {
|
||||||
|
constructor(
|
||||||
|
private readonly authUserRepo: IAuthenticatedUserRepository,
|
||||||
|
private readonly tabContextRepo: ITabContextRepository
|
||||||
|
) {}
|
||||||
|
|
||||||
|
generateAccessToken(payload: IJWTPayload): Result<Token, Error> {
|
||||||
|
return Token.create(JwtHelper.generateToken(payload, ACCESS_EXPIRATION));
|
||||||
|
}
|
||||||
|
|
||||||
|
generateRefreshToken(payload: IJWTPayload): Result<Token, Error> {
|
||||||
|
return Token.create(JwtHelper.generateToken(payload, REFRESH_EXPIRATION));
|
||||||
|
}
|
||||||
|
|
||||||
|
verifyRefreshToken(token: Token): IJWTPayload {
|
||||||
|
return JwtHelper.verifyToken(token.toString());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* Registra un nuevo usuario en la base de datos bajo transacción.
|
||||||
|
*/
|
||||||
|
async registerUser(
|
||||||
|
registerData: RegisterData,
|
||||||
|
transaction?: any
|
||||||
|
): Promise<Result<AuthenticatedUser, Error>> {
|
||||||
|
const { username, email, hashPassword } = registerData;
|
||||||
|
|
||||||
|
// Verificar si el usuario ya existe
|
||||||
|
const userExists = await this.authUserRepo.userExists(email, transaction);
|
||||||
|
if (userExists.isSuccess && userExists.data) {
|
||||||
|
return Result.fail(new Error("Email is already registered"));
|
||||||
|
}
|
||||||
|
|
||||||
|
const newUserId = UniqueID.generateNewID().data;
|
||||||
|
|
||||||
|
const userOrError = AuthenticatedUser.create(
|
||||||
|
{
|
||||||
|
username,
|
||||||
|
email,
|
||||||
|
hashPassword,
|
||||||
|
roles: ["USER"],
|
||||||
|
},
|
||||||
|
newUserId
|
||||||
|
);
|
||||||
|
|
||||||
|
if (userOrError.isFailure) {
|
||||||
|
return Result.fail(userOrError.error);
|
||||||
|
}
|
||||||
|
|
||||||
|
const createdResult = await this.authUserRepo.createUser(userOrError.data, transaction);
|
||||||
|
|
||||||
|
if (createdResult.isFailure) {
|
||||||
|
return Result.fail(createdResult.error);
|
||||||
|
}
|
||||||
|
|
||||||
|
return Result.ok(userOrError.data);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* Autentica a un usuario validando su email y contraseña.
|
||||||
|
*/
|
||||||
|
async loginUser(
|
||||||
|
loginData: LoginData,
|
||||||
|
transaction?: any
|
||||||
|
): Promise<
|
||||||
|
Result<
|
||||||
|
{
|
||||||
|
user: AuthenticatedUser;
|
||||||
|
tabContext: TabContext;
|
||||||
|
tokens: {
|
||||||
|
accessToken: Token;
|
||||||
|
refreshToken: Token;
|
||||||
|
};
|
||||||
|
},
|
||||||
|
Error
|
||||||
|
>
|
||||||
|
> {
|
||||||
|
const { email, plainPassword, tabId } = loginData;
|
||||||
|
|
||||||
|
// Verificar que el tab ID está definido
|
||||||
|
if (!tabId.isDefined()) {
|
||||||
|
return Result.fail(new Error("Invalid tab id"));
|
||||||
|
}
|
||||||
|
|
||||||
|
// 🔹 Verificar si el usuario existe en la base de datos
|
||||||
|
const userResult = await this.authUserRepo.getUserByEmail(email, transaction);
|
||||||
|
if (userResult.isFailure) {
|
||||||
|
return Result.fail(new Error("Invalid email or password"));
|
||||||
|
}
|
||||||
|
|
||||||
|
const user = userResult.data;
|
||||||
|
|
||||||
|
// 🔹 Verificar que la contraseña sea correcta
|
||||||
|
const isValidPassword = await user.verifyPassword(plainPassword);
|
||||||
|
if (!isValidPassword) {
|
||||||
|
return Result.fail(new Error("Invalid email or password"));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Registrar o actualizar el contexto de ese tab ID
|
||||||
|
const contextOrError = TabContext.create({
|
||||||
|
userId: user.id,
|
||||||
|
tabId: tabId,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (contextOrError.isFailure) {
|
||||||
|
return Result.fail(new Error("Error creating user context"));
|
||||||
|
}
|
||||||
|
|
||||||
|
const tabContext = contextOrError.data;
|
||||||
|
|
||||||
|
await this.tabContextRepo.registerContextByTabId(tabContext, transaction);
|
||||||
|
|
||||||
|
// 🔹 Generar Access Token y Refresh Token
|
||||||
|
const accessTokenOrError = this.generateAccessToken({
|
||||||
|
user_id: user.id.toString(),
|
||||||
|
email: email.toString(),
|
||||||
|
tab_id: tabId.toString(),
|
||||||
|
roles: ["USER"],
|
||||||
|
});
|
||||||
|
|
||||||
|
const refreshTokenOrError = this.generateRefreshToken({
|
||||||
|
user_id: user.id.toString(),
|
||||||
|
email: email.toString(),
|
||||||
|
tab_id: tabId.toString(),
|
||||||
|
roles: ["USER"],
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = Result.combine([accessTokenOrError, refreshTokenOrError]);
|
||||||
|
|
||||||
|
if (result.isFailure) {
|
||||||
|
return Result.fail(result.error);
|
||||||
|
}
|
||||||
|
|
||||||
|
return Result.ok({
|
||||||
|
user,
|
||||||
|
tabContext,
|
||||||
|
tokens: {
|
||||||
|
accessToken: accessTokenOrError.data,
|
||||||
|
refreshToken: refreshTokenOrError.data,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* Autentica a un usuario validando su email y contraseña.
|
||||||
|
*/
|
||||||
|
async logoutUser(
|
||||||
|
params: { email: EmailAddress; tabId: UniqueID },
|
||||||
|
transaction?: any
|
||||||
|
): Promise<Result<void, Error>> {
|
||||||
|
const { email, tabId } = params;
|
||||||
|
|
||||||
|
// Verificar que el tab ID está definido
|
||||||
|
if (!tabId.isDefined()) {
|
||||||
|
return Result.fail(new Error("Invalid tab id"));
|
||||||
|
}
|
||||||
|
|
||||||
|
// 🔹 Verificar si el usuario existe en la base de datos
|
||||||
|
const userResult = await this.authUserRepo.getUserByEmail(email, transaction);
|
||||||
|
if (userResult.isFailure) {
|
||||||
|
return Result.fail(new Error("Invalid email or password"));
|
||||||
|
}
|
||||||
|
|
||||||
|
const user = userResult.data;
|
||||||
|
|
||||||
|
const contextOrError = TabContext.create({
|
||||||
|
userId: user.id,
|
||||||
|
tabId: tabId,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (contextOrError.isFailure) {
|
||||||
|
return Result.fail(new Error("Error creating user context"));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Desregistrar el contexto de ese tab ID
|
||||||
|
await this.tabContextRepo.unregisterContextByTabId(contextOrError.data, transaction);
|
||||||
|
|
||||||
|
return Result.ok();
|
||||||
|
}
|
||||||
|
|
||||||
|
async getUserByEmail(
|
||||||
|
email: EmailAddress,
|
||||||
|
transaction?: any
|
||||||
|
): Promise<Result<AuthenticatedUser, Error>> {
|
||||||
|
const userResult = await this.authUserRepo.getUserByEmail(email, transaction);
|
||||||
|
|
||||||
|
if (userResult.isFailure || !userResult.data) {
|
||||||
|
return Result.fail(new Error("Invalid email or password"));
|
||||||
|
}
|
||||||
|
|
||||||
|
return Result.ok(userResult.data);
|
||||||
|
}
|
||||||
|
}
|
||||||
8
apps/server/src/contexts/auth/domain/services/index.ts
Normal file
8
apps/server/src/contexts/auth/domain/services/index.ts
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
export * from "./auth-service.interface";
|
||||||
|
export * from "./auth.service";
|
||||||
|
|
||||||
|
export * from "./tab-context-service.interface";
|
||||||
|
export * from "./tab-context.service";
|
||||||
|
|
||||||
|
export * from "./user-service.interface";
|
||||||
|
export * from "./user.service";
|
||||||
@ -0,0 +1,14 @@
|
|||||||
|
import { Result, UniqueID } from "@common/domain";
|
||||||
|
import { TabContext } from "../entities";
|
||||||
|
|
||||||
|
export interface ITabContextService {
|
||||||
|
getContextByTabId(tabId: UniqueID): Promise<Result<TabContext, Error>>;
|
||||||
|
createContext(
|
||||||
|
params: { tabId: UniqueID; userId: UniqueID },
|
||||||
|
transaction?: any
|
||||||
|
): Promise<Result<TabContext, Error>>;
|
||||||
|
removeContext(
|
||||||
|
params: { tabId: UniqueID; userId: UniqueID },
|
||||||
|
transaction?: any
|
||||||
|
): Promise<Result<void, Error>>;
|
||||||
|
}
|
||||||
@ -0,0 +1,86 @@
|
|||||||
|
import { Result, UniqueID } from "@common/domain";
|
||||||
|
import { TabContext } from "../entities";
|
||||||
|
import { ITabContextRepository } from "../repositories";
|
||||||
|
import { ITabContextService } from "./tab-context-service.interface";
|
||||||
|
|
||||||
|
export class TabContextService implements ITabContextService {
|
||||||
|
constructor(private readonly tabContextRepo: ITabContextRepository) {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Obtiene el contexto de una pestaña por su ID
|
||||||
|
*/
|
||||||
|
async getContextByTabId(tabId: UniqueID, transaction?: any): Promise<Result<TabContext, Error>> {
|
||||||
|
// Verificar si la pestaña existe
|
||||||
|
const tabContextOrError = await this.tabContextRepo.getContextByTabId(tabId, transaction);
|
||||||
|
if (tabContextOrError.isSuccess && !tabContextOrError.data) {
|
||||||
|
return Result.fail(new Error("Invalid or expired Tab ID"));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (tabContextOrError.isFailure) {
|
||||||
|
return Result.fail(tabContextOrError.error);
|
||||||
|
}
|
||||||
|
|
||||||
|
return Result.ok(tabContextOrError.data);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Registra un nuevo contexto de pestaña para un usuario
|
||||||
|
*/
|
||||||
|
async createContext(
|
||||||
|
params: {
|
||||||
|
tabId: UniqueID;
|
||||||
|
userId: UniqueID;
|
||||||
|
},
|
||||||
|
transaction?: any
|
||||||
|
): Promise<Result<TabContext, Error>> {
|
||||||
|
const { tabId, userId } = params;
|
||||||
|
|
||||||
|
if (!userId || !tabId) {
|
||||||
|
return Result.fail(new Error("User ID and Tab ID are required"));
|
||||||
|
}
|
||||||
|
|
||||||
|
const contextOrError = TabContext.create(
|
||||||
|
{
|
||||||
|
userId,
|
||||||
|
tabId,
|
||||||
|
},
|
||||||
|
UniqueID.generateNewID().data
|
||||||
|
);
|
||||||
|
|
||||||
|
if (contextOrError.isFailure) {
|
||||||
|
return Result.fail(contextOrError.error);
|
||||||
|
}
|
||||||
|
|
||||||
|
await this.tabContextRepo.registerContextByTabId(contextOrError.data, transaction);
|
||||||
|
|
||||||
|
return Result.ok(contextOrError.data);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Elimina un contexto de pestaña por su ID
|
||||||
|
*/
|
||||||
|
async removeContext(
|
||||||
|
params: { tabId: UniqueID; userId: UniqueID },
|
||||||
|
transaction?: any
|
||||||
|
): Promise<Result<void, Error>> {
|
||||||
|
const { tabId, userId } = params;
|
||||||
|
|
||||||
|
if (!userId || !tabId) {
|
||||||
|
return Result.fail(new Error("User ID and Tab ID are required"));
|
||||||
|
}
|
||||||
|
|
||||||
|
const contextOrError = TabContext.create(
|
||||||
|
{
|
||||||
|
userId,
|
||||||
|
tabId,
|
||||||
|
},
|
||||||
|
UniqueID.generateNewID().data
|
||||||
|
);
|
||||||
|
|
||||||
|
if (contextOrError.isFailure) {
|
||||||
|
return Result.fail(contextOrError.error);
|
||||||
|
}
|
||||||
|
|
||||||
|
return await this.tabContextRepo.unregisterContextByTabId(contextOrError.data, transaction);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,7 @@
|
|||||||
|
import { Result, UniqueID } from "@common/domain";
|
||||||
|
import { User } from "../aggregates";
|
||||||
|
|
||||||
|
export interface IUserService {
|
||||||
|
findUsers(transaction?: any): Promise<Result<User[], Error>>;
|
||||||
|
findUserById(userId: UniqueID, transaction?: any): Promise<Result<User>>;
|
||||||
|
}
|
||||||
@ -0,0 +1,39 @@
|
|||||||
|
import { Result, UniqueID } from "@common/domain";
|
||||||
|
import { IUserRepository, User } from "..";
|
||||||
|
import { IUserService } from "./user-service.interface";
|
||||||
|
|
||||||
|
export class UserService implements IUserService {
|
||||||
|
constructor(private readonly userRepository: IUserRepository) {}
|
||||||
|
|
||||||
|
async findUsers(transaction?: any): Promise<Result<User[], Error>> {
|
||||||
|
const usersOrError = await this.userRepository.findAll(transaction);
|
||||||
|
if (usersOrError.isFailure) {
|
||||||
|
return Result.fail(usersOrError.error);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Solo devolver usuarios activos
|
||||||
|
const activeUsers = usersOrError.data.filter((user) => user /*.isActive*/);
|
||||||
|
return Result.ok(activeUsers);
|
||||||
|
}
|
||||||
|
|
||||||
|
async findUserById(userId: UniqueID, transaction?: any): Promise<Result<User>> {
|
||||||
|
return await this.userRepository.findById(userId, transaction);
|
||||||
|
}
|
||||||
|
|
||||||
|
/*public async createUser(
|
||||||
|
data: { name: string; email: EmailAddress },
|
||||||
|
transaction?: Transaction
|
||||||
|
): Promise<Result<User>> {
|
||||||
|
// Evitar duplicados por email
|
||||||
|
const existingUser = await this.userRepository.findByEmail(data.email);
|
||||||
|
if (existingUser.isSuccess) {
|
||||||
|
return Result.fail(new Error("El correo ya está registrado."));
|
||||||
|
}
|
||||||
|
|
||||||
|
const newUser = User.create({
|
||||||
|
email,
|
||||||
|
username
|
||||||
|
})
|
||||||
|
return await this.userRepository.save(newUser, transaction);
|
||||||
|
}*/
|
||||||
|
}
|
||||||
@ -5,8 +5,8 @@ import { z } from "zod";
|
|||||||
export class HashPassword extends ValueObject<string> {
|
export class HashPassword extends ValueObject<string> {
|
||||||
private static readonly SALT_ROUNDS = 10;
|
private static readonly SALT_ROUNDS = 10;
|
||||||
|
|
||||||
static create(plainPassword: string): Result<HashPassword, Error> {
|
static createFromPlainText(plainTextPassword: string): Result<HashPassword, Error> {
|
||||||
const result = HashPassword.validate(plainPassword);
|
const result = HashPassword.validate(plainTextPassword);
|
||||||
|
|
||||||
if (!result.success) {
|
if (!result.success) {
|
||||||
return Result.fail(new Error(result.error.errors[0].message));
|
return Result.fail(new Error(result.error.errors[0].message));
|
||||||
@ -25,10 +25,6 @@ export class HashPassword extends ValueObject<string> {
|
|||||||
return Result.ok(new HashPassword(hashedPassword));
|
return Result.ok(new HashPassword(hashedPassword));
|
||||||
}
|
}
|
||||||
|
|
||||||
static createFromPlainText(plainTextPassword: string): Result<HashPassword, Error> {
|
|
||||||
return HashPassword.create(plainTextPassword);
|
|
||||||
}
|
|
||||||
|
|
||||||
async verifyPassword(plainTextPassword: string): Promise<boolean> {
|
async verifyPassword(plainTextPassword: string): Promise<boolean> {
|
||||||
return await bcrypt.compare(plainTextPassword, this._value);
|
return await bcrypt.compare(plainTextPassword, this._value);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -2,4 +2,5 @@ export * from "./auth-user-roles";
|
|||||||
export * from "./email-address";
|
export * from "./email-address";
|
||||||
export * from "./hash-password";
|
export * from "./hash-password";
|
||||||
export * from "./plain-password";
|
export * from "./plain-password";
|
||||||
|
export * from "./token";
|
||||||
export * from "./username";
|
export * from "./username";
|
||||||
|
|||||||
19
apps/server/src/contexts/auth/domain/value-objects/token.ts
Normal file
19
apps/server/src/contexts/auth/domain/value-objects/token.ts
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
import { Result, ValueObject } from "@common/domain";
|
||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
export class Token extends ValueObject<string> {
|
||||||
|
static create(token: string): Result<Token, Error> {
|
||||||
|
const result = Token.validate(token);
|
||||||
|
|
||||||
|
if (!result.success) {
|
||||||
|
return Result.fail(new Error(result.error.errors[0].message));
|
||||||
|
}
|
||||||
|
|
||||||
|
return Result.ok(new Token(result.data));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static validate(token: string) {
|
||||||
|
const schema = z.string().min(319, { message: "Invalid token string" });
|
||||||
|
return schema.safeParse(token);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,3 +1,4 @@
|
|||||||
export * from "./mappers";
|
export * from "./mappers";
|
||||||
|
export * from "./middleware";
|
||||||
export * from "./passport";
|
export * from "./passport";
|
||||||
export * from "./sequelize";
|
export * from "./sequelize";
|
||||||
|
|||||||
@ -1,8 +0,0 @@
|
|||||||
import { Result } from "@common/domain";
|
|
||||||
import { AuthenticatedUser } from "@contexts/auth/domain";
|
|
||||||
import { AuthUserModel } from "../sequelize";
|
|
||||||
|
|
||||||
export interface IAuthenticatedUserMapper {
|
|
||||||
toDomain(entity: AuthUserModel): Result<AuthenticatedUser, Error>;
|
|
||||||
toPersistence(aggregate: AuthenticatedUser): AuthUserModel;
|
|
||||||
}
|
|
||||||
@ -1,9 +1,13 @@
|
|||||||
import { Result, UniqueID } from "@common/domain";
|
import { Result, UniqueID } from "@common/domain";
|
||||||
import { AuthenticatedUser, EmailAddress, HashPassword, Username } from "@contexts/auth/domain";
|
import { AuthenticatedUser, EmailAddress, HashPassword, Username } from "@contexts/auth/domain";
|
||||||
import { AuthUserModel } from "../sequelize";
|
import { AuthUserModel } from "../sequelize";
|
||||||
import { IAuthenticatedUserMapper } from "./authenticated-user-mapper.interface";
|
|
||||||
|
|
||||||
export class AuthenticatedUserMapper implements IAuthenticatedUserMapper {
|
export interface IAuthenticatedUserMapper {
|
||||||
|
toDomain(entity: AuthUserModel): Result<AuthenticatedUser, Error>;
|
||||||
|
toPersistence(aggregate: AuthenticatedUser): AuthUserModel;
|
||||||
|
}
|
||||||
|
|
||||||
|
class AuthenticatedUserMapper implements IAuthenticatedUserMapper {
|
||||||
/**
|
/**
|
||||||
* 🔹 Convierte una entidad de la base de datos en un agregado de dominio `AuthenticatedUser`
|
* 🔹 Convierte una entidad de la base de datos en un agregado de dominio `AuthenticatedUser`
|
||||||
*/
|
*/
|
||||||
@ -49,5 +53,5 @@ export class AuthenticatedUserMapper implements IAuthenticatedUserMapper {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export const createAuthenticatedUserMapper = (): IAuthenticatedUserMapper =>
|
const authenticatedUserMapper: IAuthenticatedUserMapper = new AuthenticatedUserMapper();
|
||||||
new AuthenticatedUserMapper();
|
export { authenticatedUserMapper };
|
||||||
|
|||||||
@ -1,4 +1,3 @@
|
|||||||
export * from "./authenticated-user-mapper.interface";
|
|
||||||
export * from "./authenticated-user.mapper";
|
export * from "./authenticated-user.mapper";
|
||||||
export * from "./tab-context-mapper.interface";
|
|
||||||
export * from "./tab-context.mapper";
|
export * from "./tab-context.mapper";
|
||||||
|
export * from "./user.mapper";
|
||||||
|
|||||||
@ -1,8 +0,0 @@
|
|||||||
import { Result } from "@common/domain";
|
|
||||||
import { TabContext } from "@contexts/auth/domain";
|
|
||||||
import { TabContextModel } from "../sequelize";
|
|
||||||
|
|
||||||
export interface ITabContextMapper {
|
|
||||||
toDomain(entity: TabContextModel): Result<TabContext, Error>;
|
|
||||||
toPersistence(aggregate: TabContext): TabContextModel;
|
|
||||||
}
|
|
||||||
@ -1,9 +1,13 @@
|
|||||||
import { Result, UniqueID } from "@common/domain";
|
import { Result, UniqueID } from "@common/domain";
|
||||||
import { TabContext } from "@contexts/auth/domain";
|
import { TabContext } from "@contexts/auth/domain";
|
||||||
import { TabContextModel } from "../sequelize";
|
import { TabContextModel } from "../sequelize";
|
||||||
import { ITabContextMapper } from "./tab-context-mapper.interface";
|
|
||||||
|
|
||||||
export class TabContextMapper implements ITabContextMapper {
|
export interface ITabContextMapper {
|
||||||
|
toDomain(entity: TabContextModel): Result<TabContext, Error>;
|
||||||
|
toPersistence(aggregate: TabContext): TabContextModel;
|
||||||
|
}
|
||||||
|
|
||||||
|
class TabContextMapper implements ITabContextMapper {
|
||||||
toDomain(entity: TabContextModel): Result<TabContext, Error> {
|
toDomain(entity: TabContextModel): Result<TabContext, Error> {
|
||||||
if (!entity) {
|
if (!entity) {
|
||||||
return Result.fail(new Error("Entity not found"));
|
return Result.fail(new Error("Entity not found"));
|
||||||
@ -13,16 +17,16 @@ export class TabContextMapper implements ITabContextMapper {
|
|||||||
const uniqueIdResult = UniqueID.create(entity.id);
|
const uniqueIdResult = UniqueID.create(entity.id);
|
||||||
const tabIdResult = UniqueID.create(entity.tab_id);
|
const tabIdResult = UniqueID.create(entity.tab_id);
|
||||||
const userIdResult = UniqueID.create(entity.user_id);
|
const userIdResult = UniqueID.create(entity.user_id);
|
||||||
const companyIdResult = UniqueID.create(entity.company_id, false);
|
//const companyIdResult = UniqueID.create(entity.company_id, false);
|
||||||
const brachIdResult = UniqueID.create(entity.branch_id, false);
|
//const brachIdResult = UniqueID.create(entity.branch_id, false);
|
||||||
|
|
||||||
// Validar que no haya errores en la creación de los Value Objects
|
// Validar que no haya errores en la creación de los Value Objects
|
||||||
const okOrError = Result.combine([
|
const okOrError = Result.combine([
|
||||||
uniqueIdResult,
|
uniqueIdResult,
|
||||||
tabIdResult,
|
tabIdResult,
|
||||||
userIdResult,
|
userIdResult,
|
||||||
companyIdResult,
|
//companyIdResult,
|
||||||
brachIdResult,
|
//brachIdResult,
|
||||||
]);
|
]);
|
||||||
if (okOrError.isFailure) {
|
if (okOrError.isFailure) {
|
||||||
return Result.fail(okOrError.error.message);
|
return Result.fail(okOrError.error.message);
|
||||||
@ -33,8 +37,8 @@ export class TabContextMapper implements ITabContextMapper {
|
|||||||
{
|
{
|
||||||
tabId: tabIdResult.data!,
|
tabId: tabIdResult.data!,
|
||||||
userId: userIdResult.data!,
|
userId: userIdResult.data!,
|
||||||
companyId: companyIdResult.data,
|
//companyId: companyIdResult.data,
|
||||||
branchId: brachIdResult.data,
|
//branchId: brachIdResult.data,
|
||||||
},
|
},
|
||||||
uniqueIdResult.data!
|
uniqueIdResult.data!
|
||||||
);
|
);
|
||||||
@ -45,4 +49,5 @@ export class TabContextMapper implements ITabContextMapper {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export const createTabContextMapper = (): ITabContextMapper => new TabContextMapper();
|
const tabContextMapper: ITabContextMapper = new TabContextMapper();
|
||||||
|
export { tabContextMapper };
|
||||||
|
|||||||
@ -0,0 +1,84 @@
|
|||||||
|
import { Result, UniqueID } from "@common/domain";
|
||||||
|
import { EmailAddress, User, Username } from "@contexts/auth/domain";
|
||||||
|
import { UserModel } from "../sequelize";
|
||||||
|
|
||||||
|
export interface IUserMapper {
|
||||||
|
toDomain(entity: UserModel): Result<User, Error>;
|
||||||
|
toDomainArray(entities: UserModel[]): Result<User[], Error>;
|
||||||
|
|
||||||
|
toPersistence(aggregate: User): UserModel;
|
||||||
|
toPersistenceArray(users: User[]): UserModel[];
|
||||||
|
}
|
||||||
|
|
||||||
|
class UserMapper implements IUserMapper {
|
||||||
|
/**
|
||||||
|
* 🔹 Convierte una entidad de la base de datos en un agregado de dominio `User`
|
||||||
|
*/
|
||||||
|
toDomain(entity: UserModel): Result<User, Error> {
|
||||||
|
if (!entity) {
|
||||||
|
return Result.fail(new Error("Entity not found"));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Crear Value Objects asegurando que sean válidos
|
||||||
|
const uniqueIdResult = UniqueID.create(entity.id);
|
||||||
|
const usernameResult = Username.create(entity.username);
|
||||||
|
const emailResult = EmailAddress.create(entity.email);
|
||||||
|
|
||||||
|
// Validar que no haya errores en la creación de los Value Objects
|
||||||
|
const okOrError = Result.combine([uniqueIdResult, usernameResult, emailResult]);
|
||||||
|
if (okOrError.isFailure) {
|
||||||
|
return Result.fail(okOrError.error.message);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Crear el agregado de dominio
|
||||||
|
return User.create(
|
||||||
|
{
|
||||||
|
username: usernameResult.data!,
|
||||||
|
email: emailResult.data!,
|
||||||
|
roles: [],
|
||||||
|
//roles: entity.roles || [],
|
||||||
|
},
|
||||||
|
uniqueIdResult.data!
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 🔹 Convierte un array de entidades de la base de datos en un array de agregados de dominio `User`
|
||||||
|
*/
|
||||||
|
toDomainArray(entities: UserModel[]): Result<User[], Error> {
|
||||||
|
if (!Array.isArray(entities) || entities.length === 0) {
|
||||||
|
return Result.fail(new Error("Entities array is empty or invalid"));
|
||||||
|
}
|
||||||
|
|
||||||
|
const usersResults = entities.map(this.toDomain);
|
||||||
|
|
||||||
|
const okOrError = Result.combine(usersResults);
|
||||||
|
if (okOrError.isFailure) {
|
||||||
|
return Result.fail(okOrError.error);
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = usersResults.map((result) => result.data!);
|
||||||
|
return Result.ok(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 🔹 Convierte un agregado `User` en un objeto listo para persistencia
|
||||||
|
*/
|
||||||
|
toPersistence(user: User): UserModel {
|
||||||
|
return user.toPersistenceData();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 🔹 Convierte un array de agregados `User` en un array de objetos listos para persistencia
|
||||||
|
*/
|
||||||
|
toPersistenceArray(users: User[]): UserModel[] {
|
||||||
|
if (!Array.isArray(users) || users.length === 0) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
return users.map(this.toPersistence);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const userMapper: IUserMapper = new UserMapper();
|
||||||
|
export { userMapper };
|
||||||
@ -1,6 +1,7 @@
|
|||||||
import { UniqueID } from "@common/domain";
|
import { UniqueID } from "@common/domain";
|
||||||
import { ApiError, ExpressController } from "@common/presentation";
|
import { ApiError, ExpressController } from "@common/presentation";
|
||||||
import { AuthenticatedUser } from "@contexts/auth/domain";
|
import { AuthenticatedUser } from "@contexts/auth/domain";
|
||||||
|
import { authProvider } from "@contexts/auth/infraestructure";
|
||||||
import { NextFunction, Request, Response } from "express";
|
import { NextFunction, Request, Response } from "express";
|
||||||
|
|
||||||
// Extender el Request de Express para incluir el usuario autenticado optionalmente
|
// Extender el Request de Express para incluir el usuario autenticado optionalmente
|
||||||
@ -29,16 +30,11 @@ const _authorizeUser = (condition: (user: AuthenticatedUser) => boolean) => {
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
// Middleware para autenticar usando passport con el local-jwt strategy
|
|
||||||
//export const authenticateJWT = [];
|
|
||||||
|
|
||||||
//export const validateUserRegister = [_authenticateEmail];
|
|
||||||
|
|
||||||
// Verifica que el usuario esté autenticado
|
// Verifica que el usuario esté autenticado
|
||||||
export const authenticateUser = [_authorizeUser((user) => user.isUser)];
|
export const checkUser = [authProvider.authenticateJWT(), _authorizeUser((user) => user.isUser)];
|
||||||
|
|
||||||
// Verifica que el usuario sea administrador
|
// Verifica que el usuario sea administrador
|
||||||
export const authenticateUserIsAdmin = [_authorizeUser((user) => user.isAdmin)];
|
export const checkUserIsAdmin = [_authorizeUser((user) => user.isAdmin)];
|
||||||
|
|
||||||
// Middleware para verificar que el usuario sea administrador o el dueño de los datos (self)
|
// Middleware para verificar que el usuario sea administrador o el dueño de los datos (self)
|
||||||
export const checkUserIsAdminOrOwner = [
|
export const checkUserIsAdminOrOwner = [
|
||||||
@ -1,7 +1,11 @@
|
|||||||
import { createAuthService, createTabContextService } from "../../application";
|
import { SequelizeTransactionManager } from "@common/infrastructure";
|
||||||
|
import { AuthService, TabContextService } from "@contexts/auth/domain/services";
|
||||||
|
import { authenticatedUserRepository, tabContextRepository } from "../sequelize";
|
||||||
import { IJWTPayload, PassportAuthProvider } from "./passport-auth-provider";
|
import { IJWTPayload, PassportAuthProvider } from "./passport-auth-provider";
|
||||||
|
|
||||||
export { IJWTPayload };
|
const transactionManager = new SequelizeTransactionManager();
|
||||||
|
const authService = new AuthService(authenticatedUserRepository, tabContextRepository);
|
||||||
|
const tabContextService = new TabContextService(tabContextRepository);
|
||||||
|
|
||||||
export const createAuthProvider = () =>
|
const authProvider = new PassportAuthProvider(authService, tabContextService, transactionManager);
|
||||||
new PassportAuthProvider(createAuthService(), createTabContextService());
|
export { authProvider, IJWTPayload };
|
||||||
|
|||||||
@ -1,10 +1,11 @@
|
|||||||
import jwt from "jsonwebtoken";
|
import jwt from "jsonwebtoken";
|
||||||
|
|
||||||
const SECRET_KEY = process.env.JWT_SECRET || "supersecretkey";
|
const SECRET_KEY: jwt.Secret = process.env.JWT_SECRET || "supersecretkey";
|
||||||
|
|
||||||
export class JwtHelper {
|
export class JwtHelper {
|
||||||
static generateToken(payload: object, expiresIn = "1h"): string {
|
static generateToken(payload: object, expiresIn = "1h"): string {
|
||||||
return jwt.sign(payload, SECRET_KEY, { expiresIn });
|
const params: jwt.SignOptions = { expiresIn: expiresIn as jwt.SignOptions["expiresIn"] };
|
||||||
|
return jwt.sign(payload, SECRET_KEY, params);
|
||||||
}
|
}
|
||||||
|
|
||||||
static verifyToken(token: string): any {
|
static verifyToken(token: string): any {
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
import { Result, UniqueID } from "@common/domain";
|
import { Result, UniqueID } from "@common/domain";
|
||||||
import { IAuthService } from "@contexts/auth/application";
|
import { ITransactionManager } from "@common/infrastructure/database";
|
||||||
import { ITabContextService } from "@contexts/auth/application/tab-context-service.interface";
|
|
||||||
import { AuthenticatedUser, EmailAddress, PlainPassword } from "@contexts/auth/domain";
|
import { AuthenticatedUser, EmailAddress, PlainPassword } from "@contexts/auth/domain";
|
||||||
|
import { IAuthService, ITabContextService } from "@contexts/auth/domain/services";
|
||||||
import passport from "passport";
|
import passport from "passport";
|
||||||
import { ExtractJwt, Strategy as JwtStrategy } from "passport-jwt";
|
import { ExtractJwt, Strategy as JwtStrategy } from "passport-jwt";
|
||||||
import { Strategy as LocalStrategy } from "passport-local";
|
import { Strategy as LocalStrategy } from "passport-local";
|
||||||
@ -16,9 +16,6 @@ export interface IJWTPayload {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export class PassportAuthProvider {
|
export class PassportAuthProvider {
|
||||||
private readonly _authService: IAuthService;
|
|
||||||
private readonly _tabContextService: ITabContextService;
|
|
||||||
|
|
||||||
private async _getUserByEmailAndPassword(
|
private async _getUserByEmailAndPassword(
|
||||||
email: string,
|
email: string,
|
||||||
password: string
|
password: string
|
||||||
@ -33,9 +30,7 @@ export class PassportAuthProvider {
|
|||||||
return Result.fail(plainPasswordVO.error);
|
return Result.fail(plainPasswordVO.error);
|
||||||
}
|
}
|
||||||
|
|
||||||
const userResult = await this._authService.getUserByEmail({
|
const userResult = await this.authService.getUserByEmail(emailVO.data);
|
||||||
email: emailVO.data,
|
|
||||||
});
|
|
||||||
|
|
||||||
if (userResult.isFailure || !userResult.data) {
|
if (userResult.isFailure || !userResult.data) {
|
||||||
return Result.fail(new Error("Invalid email or password"));
|
return Result.fail(new Error("Invalid email or password"));
|
||||||
@ -52,10 +47,10 @@ export class PassportAuthProvider {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private async _getUserAndContextByToken(token: IJWTPayload) {
|
private async _getUserAndContextByToken(token: IJWTPayload) {
|
||||||
const { userId, email, roles, tabId } = token;
|
const { user_id, email, roles, tab_id } = token;
|
||||||
|
|
||||||
const userIdVO = UniqueID.create(userId);
|
const userIdVO = UniqueID.create(user_id);
|
||||||
const tabIdVO = UniqueID.create(tabId);
|
const tabIdVO = UniqueID.create(tab_id);
|
||||||
const emailVO = EmailAddress.create(email!);
|
const emailVO = EmailAddress.create(email!);
|
||||||
|
|
||||||
const okOrError = Result.combine([userIdVO, tabIdVO, emailVO]);
|
const okOrError = Result.combine([userIdVO, tabIdVO, emailVO]);
|
||||||
@ -63,9 +58,7 @@ export class PassportAuthProvider {
|
|||||||
return Result.fail(okOrError.error.message);
|
return Result.fail(okOrError.error.message);
|
||||||
}
|
}
|
||||||
|
|
||||||
const userResult = await this._authService.getUserByEmail({
|
const userResult = await this.authService.getUserByEmail(emailVO.data);
|
||||||
email: emailVO.data,
|
|
||||||
});
|
|
||||||
|
|
||||||
if (userResult.isFailure) {
|
if (userResult.isFailure) {
|
||||||
return Result.fail(new Error("Invalid token data"));
|
return Result.fail(new Error("Invalid token data"));
|
||||||
@ -80,7 +73,7 @@ export class PassportAuthProvider {
|
|||||||
return Result.fail(new Error("Invalid token data"));
|
return Result.fail(new Error("Invalid token data"));
|
||||||
}
|
}
|
||||||
|
|
||||||
const tabResult = await this._tabContextService.getContextByTabId(tabIdVO.data);
|
const tabResult = await this.tabContextService.getContextByTabId(tabIdVO.data);
|
||||||
if (tabResult.isFailure) {
|
if (tabResult.isFailure) {
|
||||||
return Result.fail(new Error("Invalid token data"));
|
return Result.fail(new Error("Invalid token data"));
|
||||||
}
|
}
|
||||||
@ -93,10 +86,11 @@ export class PassportAuthProvider {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
constructor(authService: IAuthService, tabContextService: ITabContextService) {
|
constructor(
|
||||||
this._authService = authService;
|
private readonly authService: IAuthService,
|
||||||
this._tabContextService = tabContextService;
|
private readonly tabContextService: ITabContextService,
|
||||||
}
|
private readonly transactionManager: ITransactionManager
|
||||||
|
) {}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 🔹 Configura PassportJS
|
* 🔹 Configura PassportJS
|
||||||
@ -111,10 +105,10 @@ export class PassportAuthProvider {
|
|||||||
"jwt",
|
"jwt",
|
||||||
new JwtStrategy(jwtOptions, async (tokenPayload, done) => {
|
new JwtStrategy(jwtOptions, async (tokenPayload, done) => {
|
||||||
try {
|
try {
|
||||||
const result = await this._getUserAndContextByToken(tokenPayload);
|
const userOrError = await this._getUserAndContextByToken(tokenPayload);
|
||||||
return result.isSuccess
|
return userOrError.isSuccess
|
||||||
? done(null, result)
|
? done(null, userOrError.data)
|
||||||
: done(result.error, false, { message: "Invalid JWT data" });
|
: done(userOrError.error, false, { message: "Invalid JWT data" });
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
return done(error, false);
|
return done(error, false);
|
||||||
}
|
}
|
||||||
@ -127,10 +121,10 @@ export class PassportAuthProvider {
|
|||||||
{ usernameField: "email", passwordField: "password" },
|
{ usernameField: "email", passwordField: "password" },
|
||||||
async (email, password, done) => {
|
async (email, password, done) => {
|
||||||
try {
|
try {
|
||||||
const user = await this._getUserByEmailAndPassword(email, password);
|
const userOrError = await this._getUserByEmailAndPassword(email, password);
|
||||||
return user
|
return userOrError.isSuccess
|
||||||
? done(null, user)
|
? done(null, userOrError.data)
|
||||||
: done(null, false, { message: "Invalid email or password" });
|
: done(userOrError.error, false, { message: "Invalid email or password" });
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
return done(error, false);
|
return done(error, false);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -6,10 +6,10 @@ import {
|
|||||||
IAuthenticatedUserRepository,
|
IAuthenticatedUserRepository,
|
||||||
} from "@contexts/auth/domain";
|
} from "@contexts/auth/domain";
|
||||||
import { Transaction } from "sequelize";
|
import { Transaction } from "sequelize";
|
||||||
import { IAuthenticatedUserMapper } from "../mappers";
|
import { authenticatedUserMapper, IAuthenticatedUserMapper } from "../mappers";
|
||||||
import { AuthUserModel } from "./auth-user.model";
|
import { AuthUserModel } from "./auth-user.model";
|
||||||
|
|
||||||
export class AuthenticatedUserRepository
|
class AuthenticatedUserRepository
|
||||||
extends SequelizeRepository<AuthenticatedUser>
|
extends SequelizeRepository<AuthenticatedUser>
|
||||||
implements IAuthenticatedUserRepository
|
implements IAuthenticatedUserRepository
|
||||||
{
|
{
|
||||||
@ -85,3 +85,6 @@ export class AuthenticatedUserRepository
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const authenticatedUserRepository = new AuthenticatedUserRepository(authenticatedUserMapper);
|
||||||
|
export { authenticatedUserRepository };
|
||||||
|
|||||||
@ -1,17 +1,25 @@
|
|||||||
import { IAuthenticatedUserRepository, ITabContextRepository } from "../../domain";
|
import { IAuthenticatedUserRepository, ITabContextRepository, IUserRepository } from "../../domain";
|
||||||
import { createAuthenticatedUserMapper, createTabContextMapper } from "../mappers";
|
import { authenticatedUserRepository } from "./authenticated-user.repository";
|
||||||
import { AuthenticatedUserRepository } from "./authenticated-user.repository";
|
import { tabContextRepository } from "./tab-context.repository";
|
||||||
import { TabContextRepository } from "./tab-context.repository";
|
import { userRepository } from "./user.repository";
|
||||||
|
|
||||||
export * from "./auth-user.model";
|
export * from "./auth-user.model";
|
||||||
|
export * from "./authenticated-user.repository";
|
||||||
|
|
||||||
export * from "./tab-context.model";
|
export * from "./tab-context.model";
|
||||||
|
export * from "./tab-context.repository";
|
||||||
|
|
||||||
|
export * from "./user.model";
|
||||||
|
export * from "./user.repository";
|
||||||
|
|
||||||
export const createAuthenticatedUserRepository = (): IAuthenticatedUserRepository => {
|
export const createAuthenticatedUserRepository = (): IAuthenticatedUserRepository => {
|
||||||
const authenticatedUserMapper = createAuthenticatedUserMapper();
|
return authenticatedUserRepository;
|
||||||
return new AuthenticatedUserRepository(authenticatedUserMapper);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export const createTabContextRepository = (): ITabContextRepository => {
|
export const createTabContextRepository = (): ITabContextRepository => {
|
||||||
const tabContextMapper = createTabContextMapper();
|
return tabContextRepository;
|
||||||
return new TabContextRepository(tabContextMapper);
|
};
|
||||||
|
|
||||||
|
export const createUserRepository = (): IUserRepository => {
|
||||||
|
return userRepository;
|
||||||
};
|
};
|
||||||
|
|||||||
@ -1,12 +1,11 @@
|
|||||||
import { Result, UniqueID } from "@common/domain";
|
import { Result, UniqueID } from "@common/domain";
|
||||||
import { SequelizeRepository } from "@common/infrastructure";
|
import { SequelizeRepository } from "@common/infrastructure";
|
||||||
import { TabContext } from "@contexts/auth/domain/";
|
import { ITabContextRepository, TabContext } from "@contexts/auth/domain/";
|
||||||
import { ITabContextRepository } from "@contexts/auth/domain/repositories/tab-context-repository.interface";
|
|
||||||
import { Op, Transaction } from "sequelize";
|
import { Op, Transaction } from "sequelize";
|
||||||
import { ITabContextMapper } from "../mappers";
|
import { ITabContextMapper, tabContextMapper } from "../mappers";
|
||||||
import { TabContextModel } from "./tab-context.model";
|
import { TabContextModel } from "./tab-context.model";
|
||||||
|
|
||||||
export class TabContextRepository
|
class TabContextRepository
|
||||||
extends SequelizeRepository<TabContext>
|
extends SequelizeRepository<TabContext>
|
||||||
implements ITabContextRepository
|
implements ITabContextRepository
|
||||||
{
|
{
|
||||||
@ -125,3 +124,6 @@ export class TabContextRepository
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const tabContextRepository = new TabContextRepository(tabContextMapper);
|
||||||
|
export { tabContextRepository };
|
||||||
|
|||||||
@ -0,0 +1,55 @@
|
|||||||
|
import { DataTypes, InferAttributes, InferCreationAttributes, Model, Sequelize } from "sequelize";
|
||||||
|
|
||||||
|
export type UserCreationAttributes = InferCreationAttributes<UserModel>;
|
||||||
|
|
||||||
|
export class UserModel extends Model<
|
||||||
|
InferAttributes<UserModel>,
|
||||||
|
InferCreationAttributes<UserModel>
|
||||||
|
> {
|
||||||
|
// To avoid table creation
|
||||||
|
/*static async sync(): Promise<any> {
|
||||||
|
return Promise.resolve();
|
||||||
|
}*/
|
||||||
|
|
||||||
|
declare id: string;
|
||||||
|
declare username: string;
|
||||||
|
declare email: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default (sequelize: Sequelize) => {
|
||||||
|
UserModel.init(
|
||||||
|
{
|
||||||
|
id: {
|
||||||
|
type: DataTypes.UUID,
|
||||||
|
primaryKey: true,
|
||||||
|
},
|
||||||
|
username: {
|
||||||
|
type: DataTypes.STRING,
|
||||||
|
allowNull: false,
|
||||||
|
},
|
||||||
|
email: {
|
||||||
|
type: DataTypes.STRING,
|
||||||
|
allowNull: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
sequelize,
|
||||||
|
tableName: "users",
|
||||||
|
paranoid: true, // softs deletes
|
||||||
|
timestamps: true,
|
||||||
|
|
||||||
|
createdAt: "created_at",
|
||||||
|
updatedAt: "updated_at",
|
||||||
|
deletedAt: "deleted_at",
|
||||||
|
|
||||||
|
indexes: [{ name: "email_idx", fields: ["email"], unique: true }],
|
||||||
|
|
||||||
|
whereMergeStrategy: "and", // <- cómo tratar el merge de un scope
|
||||||
|
|
||||||
|
defaultScope: {},
|
||||||
|
|
||||||
|
scopes: {},
|
||||||
|
}
|
||||||
|
);
|
||||||
|
return UserModel;
|
||||||
|
};
|
||||||
@ -0,0 +1,71 @@
|
|||||||
|
import { Result, UniqueID } from "@common/domain";
|
||||||
|
import { SequelizeRepository } from "@common/infrastructure";
|
||||||
|
import { EmailAddress, IUserRepository, User } from "@contexts/auth/domain";
|
||||||
|
import { Transaction } from "sequelize";
|
||||||
|
import { IUserMapper, userMapper } from "../mappers";
|
||||||
|
import { UserModel } from "./user.model";
|
||||||
|
|
||||||
|
class UserRepository extends SequelizeRepository<User> implements IUserRepository {
|
||||||
|
private readonly _mapper!: IUserMapper;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 🔹 Función personalizada para mapear errores de unicidad en autenticación
|
||||||
|
*/
|
||||||
|
private _customErrorMapper(error: Error): string | null {
|
||||||
|
if (error.name === "SequelizeUniqueConstraintError") {
|
||||||
|
return "User with this email already exists";
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
constructor(mapper: IUserMapper) {
|
||||||
|
super();
|
||||||
|
this._mapper = mapper;
|
||||||
|
}
|
||||||
|
|
||||||
|
async findAll(transaction?: Transaction): Promise<Result<User[], Error>> {
|
||||||
|
try {
|
||||||
|
const rawUsers: any = await this._findAll(UserModel, {}, transaction);
|
||||||
|
|
||||||
|
if (!rawUsers === true) {
|
||||||
|
return Result.fail(new Error("User with email not exists"));
|
||||||
|
}
|
||||||
|
|
||||||
|
return this._mapper.toDomainArray(rawUsers);
|
||||||
|
} catch (error: any) {
|
||||||
|
return this._handleDatabaseError(error, this._customErrorMapper);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async findById(id: UniqueID, transaction?: Transaction): Promise<Result<User, Error>> {
|
||||||
|
try {
|
||||||
|
const rawUser: any = await this._getById(UserModel, id, {}, transaction);
|
||||||
|
|
||||||
|
if (!rawUser === true) {
|
||||||
|
return Result.fail(new Error(`User with id ${id.toString()} not exists`));
|
||||||
|
}
|
||||||
|
|
||||||
|
return this._mapper.toDomain(rawUser);
|
||||||
|
} catch (error: any) {
|
||||||
|
return this._handleDatabaseError(error, this._customErrorMapper);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async findByEmail(email: EmailAddress, transaction?: Transaction): Promise<Result<User, Error>> {
|
||||||
|
try {
|
||||||
|
const rawUser: any = await this._getBy(UserModel, "email", email.toString(), {}, transaction);
|
||||||
|
|
||||||
|
if (!rawUser === true) {
|
||||||
|
return Result.fail(new Error(`User with email ${email.toString()} not exists`));
|
||||||
|
}
|
||||||
|
|
||||||
|
return this._mapper.toDomain(rawUser);
|
||||||
|
} catch (error: any) {
|
||||||
|
return this._handleDatabaseError(error, this._customErrorMapper);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const userRepository = new UserRepository(userMapper);
|
||||||
|
export { userRepository };
|
||||||
@ -1,3 +1,4 @@
|
|||||||
|
export * from "./listUsers";
|
||||||
export * from "./login";
|
export * from "./login";
|
||||||
export * from "./logout";
|
export * from "./logout";
|
||||||
export * from "./refreshToken";
|
export * from "./refreshToken";
|
||||||
|
|||||||
@ -0,0 +1,16 @@
|
|||||||
|
import { SequelizeTransactionManager } from "@common/infrastructure";
|
||||||
|
import { ListUsersUseCase } from "@contexts/auth/application/list-users/list-users.use-case";
|
||||||
|
import { UserService } from "@contexts/auth/domain/services/user.service";
|
||||||
|
import { userRepository } from "@contexts/auth/infraestructure";
|
||||||
|
import { ListUsersController } from "./list-users.controller";
|
||||||
|
import { listUsersPresenter } from "./list-users.presenter";
|
||||||
|
|
||||||
|
export const listUsersController = () => {
|
||||||
|
const transactionManager = new SequelizeTransactionManager();
|
||||||
|
const userService = new UserService(userRepository);
|
||||||
|
|
||||||
|
const useCase = new ListUsersUseCase(userService, transactionManager);
|
||||||
|
const presenter = listUsersPresenter;
|
||||||
|
|
||||||
|
return new ListUsersController(useCase, presenter);
|
||||||
|
};
|
||||||
@ -0,0 +1,37 @@
|
|||||||
|
import { ExpressController } from "@common/presentation";
|
||||||
|
import { ListUsersUseCase } from "@contexts/auth/application";
|
||||||
|
import { IListUsersPresenter } from "./list-users.presenter";
|
||||||
|
|
||||||
|
export class ListUsersController extends ExpressController {
|
||||||
|
public constructor(
|
||||||
|
private readonly listUsers: ListUsersUseCase,
|
||||||
|
private readonly presenter: IListUsersPresenter
|
||||||
|
) {
|
||||||
|
super();
|
||||||
|
}
|
||||||
|
|
||||||
|
protected async executeImpl(): Promise<void | any> {
|
||||||
|
const usersOrError = await this.listUsers.execute();
|
||||||
|
|
||||||
|
if (usersOrError.isFailure) {
|
||||||
|
return this.handleError(usersOrError.error);
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.ok(this.presenter.toDTO(usersOrError.data));
|
||||||
|
}
|
||||||
|
|
||||||
|
private handleError(error: Error) {
|
||||||
|
const message = error.message;
|
||||||
|
|
||||||
|
if (
|
||||||
|
message.includes("Database connection lost") ||
|
||||||
|
message.includes("Database request timed out")
|
||||||
|
) {
|
||||||
|
return this.unavailableError(
|
||||||
|
"Database service is currently unavailable. Please try again later."
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.conflictError(message);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,14 @@
|
|||||||
|
import { User } from "@contexts/auth/domain";
|
||||||
|
import { IListUsersResponseDTO } from "../../dto";
|
||||||
|
|
||||||
|
export interface IListUsersPresenter {
|
||||||
|
toDTO: (users: User[]) => IListUsersResponseDTO[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export const listUsersPresenter: IListUsersPresenter = {
|
||||||
|
toDTO: (users: User[]): IListUsersResponseDTO[] =>
|
||||||
|
users.map((user) => ({
|
||||||
|
id: user.id.toString(),
|
||||||
|
email: user.email.toString(),
|
||||||
|
})),
|
||||||
|
};
|
||||||
@ -1 +1,16 @@
|
|||||||
export * from "./login.controller";
|
import { SequelizeTransactionManager } from "@common/infrastructure";
|
||||||
|
import { LoginUseCase } from "@contexts/auth/application";
|
||||||
|
import { AuthService } from "@contexts/auth/domain/services";
|
||||||
|
import { authenticatedUserRepository, tabContextRepository } from "@contexts/auth/infraestructure";
|
||||||
|
import { LoginController } from "./login.controller";
|
||||||
|
import { loginPresenter } from "./login.presenter";
|
||||||
|
|
||||||
|
export const loginController = () => {
|
||||||
|
const transactionManager = new SequelizeTransactionManager();
|
||||||
|
const authService = new AuthService(authenticatedUserRepository, tabContextRepository);
|
||||||
|
|
||||||
|
const useCase = new LoginUseCase(authService, transactionManager);
|
||||||
|
const presenter = loginPresenter;
|
||||||
|
|
||||||
|
return new LoginController(useCase, presenter);
|
||||||
|
};
|
||||||
|
|||||||
@ -1,50 +1,48 @@
|
|||||||
import { Result, UniqueID } from "@common/domain";
|
|
||||||
import { ExpressController } from "@common/presentation";
|
import { ExpressController } from "@common/presentation";
|
||||||
import { createAuthService, IAuthService } from "@contexts/auth/application";
|
import { LoginUseCase } from "@contexts/auth/application";
|
||||||
import { EmailAddress, PlainPassword } from "@contexts/auth/domain";
|
import { LoginData } from "@contexts/auth/domain";
|
||||||
import { ILoginPresenter, LoginPresenter } from "./login.presenter";
|
import { ILoginPresenter } from "./login.presenter";
|
||||||
|
|
||||||
class LoginController extends ExpressController {
|
export class LoginController extends ExpressController {
|
||||||
private readonly _authService!: IAuthService;
|
public constructor(
|
||||||
private readonly _presenter!: ILoginPresenter;
|
private readonly login: LoginUseCase,
|
||||||
|
private readonly presenter: ILoginPresenter
|
||||||
public constructor(authService: IAuthService, presenter: ILoginPresenter) {
|
) {
|
||||||
super();
|
super();
|
||||||
this._authService = authService;
|
|
||||||
this._presenter = presenter;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async executeImpl() {
|
async executeImpl() {
|
||||||
const tabId = this.req.headers["x-tab-id"];
|
const loginDataOrError = LoginData.createFromPrimitives({
|
||||||
const emailVO = EmailAddress.create(this.req.body.email);
|
email: this.req.body.email,
|
||||||
const plainPasswordVO = PlainPassword.create(this.req.body.password);
|
plainPassword: this.req.body.password,
|
||||||
const tabIdVO = UniqueID.create(String(tabId));
|
tabId: String(this.req.headers["x-tab-id"]),
|
||||||
|
|
||||||
const resultValidation = Result.combine([emailVO, plainPasswordVO, tabIdVO]);
|
|
||||||
|
|
||||||
if (resultValidation.isFailure) {
|
|
||||||
return this.clientError("Invalid input data", resultValidation.error);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (emailVO.data.isEmpty()) {
|
|
||||||
return this.clientError("Invalid input data");
|
|
||||||
}
|
|
||||||
|
|
||||||
const loginResultOrError = await this._authService.loginUser({
|
|
||||||
email: emailVO.data,
|
|
||||||
plainPassword: plainPasswordVO.data,
|
|
||||||
tabId: tabIdVO.data,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if (loginDataOrError.isFailure) {
|
||||||
|
return this.clientError("Invalid input data", loginDataOrError.error);
|
||||||
|
}
|
||||||
|
|
||||||
|
const loginResultOrError = await this.login.execute(loginDataOrError.data);
|
||||||
|
|
||||||
if (loginResultOrError.isFailure) {
|
if (loginResultOrError.isFailure) {
|
||||||
return this.unauthorizedError(loginResultOrError.error.message);
|
return this.handleError(loginResultOrError.error);
|
||||||
}
|
}
|
||||||
|
|
||||||
return this.created(this._presenter.map(loginResultOrError.data));
|
return this.ok(this.presenter.toDTO(loginResultOrError.data));
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export const createLoginController = () => {
|
private handleError(error: Error) {
|
||||||
const authService = createAuthService();
|
const message = error.message;
|
||||||
return new LoginController(authService, LoginPresenter);
|
|
||||||
};
|
if (
|
||||||
|
message.includes("Database connection lost") ||
|
||||||
|
message.includes("Database request timed out")
|
||||||
|
) {
|
||||||
|
return this.unavailableError(
|
||||||
|
"Database service is currently unavailable. Please try again later."
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.unauthorizedError(message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@ -2,7 +2,7 @@ import { AuthenticatedUser, TabContext } from "@contexts/auth/domain";
|
|||||||
import { ILoginUserResponseDTO } from "../../dto";
|
import { ILoginUserResponseDTO } from "../../dto";
|
||||||
|
|
||||||
export interface ILoginPresenter {
|
export interface ILoginPresenter {
|
||||||
map: (data: {
|
toDTO: (data: {
|
||||||
user: AuthenticatedUser;
|
user: AuthenticatedUser;
|
||||||
tabContext: TabContext;
|
tabContext: TabContext;
|
||||||
tokens: {
|
tokens: {
|
||||||
@ -12,8 +12,8 @@ export interface ILoginPresenter {
|
|||||||
}) => ILoginUserResponseDTO;
|
}) => ILoginUserResponseDTO;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const LoginPresenter: ILoginPresenter = {
|
export const loginPresenter: ILoginPresenter = {
|
||||||
map: (data: {
|
toDTO: (data: {
|
||||||
user: AuthenticatedUser;
|
user: AuthenticatedUser;
|
||||||
tabContext: TabContext;
|
tabContext: TabContext;
|
||||||
tokens: {
|
tokens: {
|
||||||
@ -39,8 +39,8 @@ export const LoginPresenter: ILoginPresenter = {
|
|||||||
tab_id: tabContextData.tab_id,
|
tab_id: tabContextData.tab_id,
|
||||||
},
|
},
|
||||||
tokens: {
|
tokens: {
|
||||||
access_token: accessToken,
|
access_token: accessToken.toString(),
|
||||||
refresh_token: refreshToken,
|
refresh_token: refreshToken.toString(),
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|||||||
@ -1 +1,14 @@
|
|||||||
export * from "./logout.controller";
|
import { SequelizeTransactionManager } from "@common/infrastructure";
|
||||||
|
import { LogoutUseCase } from "@contexts/auth/application";
|
||||||
|
import { AuthService } from "@contexts/auth/domain/services";
|
||||||
|
import { authenticatedUserRepository, tabContextRepository } from "@contexts/auth/infraestructure";
|
||||||
|
import { LogoutController } from "./logout.controller";
|
||||||
|
|
||||||
|
export const logoutController = () => {
|
||||||
|
const transactionManager = new SequelizeTransactionManager();
|
||||||
|
const authService = new AuthService(authenticatedUserRepository, tabContextRepository);
|
||||||
|
|
||||||
|
const useCase = new LogoutUseCase(authService, transactionManager);
|
||||||
|
|
||||||
|
return new LogoutController(useCase);
|
||||||
|
};
|
||||||
|
|||||||
@ -1,42 +1,43 @@
|
|||||||
import { Result, UniqueID } from "@common/domain";
|
|
||||||
import { ExpressController } from "@common/presentation";
|
import { ExpressController } from "@common/presentation";
|
||||||
import { createAuthService, IAuthService } from "@contexts/auth/application";
|
import { LogoutUseCase } from "@contexts/auth/application/logout";
|
||||||
import { EmailAddress } from "@contexts/auth/domain";
|
import { LogoutData } from "@contexts/auth/domain";
|
||||||
|
|
||||||
class LogoutController extends ExpressController {
|
export class LogoutController extends ExpressController {
|
||||||
private readonly _authService!: IAuthService;
|
public constructor(private readonly logout: LogoutUseCase) {
|
||||||
|
|
||||||
public constructor(authService: IAuthService) {
|
|
||||||
super();
|
super();
|
||||||
this._authService = authService;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async executeImpl() {
|
async executeImpl() {
|
||||||
const tabId = this.req.headers["x-tab-id"];
|
const logoutDataOrError = LogoutData.createFromPrimitives({
|
||||||
|
email: this.req.body.email,
|
||||||
const emailVO = EmailAddress.create(this.req.body.email);
|
tabId: String(this.req.headers["x-tab-id"]),
|
||||||
const tabIdVO = UniqueID.create(String(tabId));
|
|
||||||
|
|
||||||
const resultValidation = Result.combine([emailVO, tabIdVO]);
|
|
||||||
|
|
||||||
if (resultValidation.isFailure) {
|
|
||||||
return this.clientError("Invalid input data", resultValidation.error);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (emailVO.data.isEmpty()) {
|
|
||||||
return this.clientError("Invalid input data");
|
|
||||||
}
|
|
||||||
|
|
||||||
await this._authService.logoutUser({
|
|
||||||
email: emailVO.data,
|
|
||||||
tabId: tabIdVO.data,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if (logoutDataOrError.isFailure) {
|
||||||
|
return this.clientError("Invalid input data", logoutDataOrError.error);
|
||||||
|
}
|
||||||
|
|
||||||
|
const logoutOrError = await this.logout.execute(logoutDataOrError.data);
|
||||||
|
|
||||||
|
if (logoutOrError.isFailure) {
|
||||||
|
return this.handleError(logoutOrError.error);
|
||||||
|
}
|
||||||
|
|
||||||
return this.ok();
|
return this.ok();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private handleError(error: Error) {
|
||||||
|
const message = error.message;
|
||||||
|
|
||||||
|
if (
|
||||||
|
message.includes("Database connection lost") ||
|
||||||
|
message.includes("Database request timed out")
|
||||||
|
) {
|
||||||
|
return this.unavailableError(
|
||||||
|
"Database service is currently unavailable. Please try again later."
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export const createLogoutController = () => {
|
return this.clientError(message);
|
||||||
const authService = createAuthService();
|
}
|
||||||
return new LogoutController(authService);
|
}
|
||||||
};
|
|
||||||
|
|||||||
@ -1 +1,15 @@
|
|||||||
export * from "./refresh-token.controller";
|
import { SequelizeTransactionManager } from "@common/infrastructure";
|
||||||
|
import { AuthService } from "@contexts/auth/domain/services";
|
||||||
|
import { authenticatedUserRepository, tabContextRepository } from "@contexts/auth/infraestructure";
|
||||||
|
import { RefreshTokenController } from "./refresh-token.controller";
|
||||||
|
import { refreshTokenPresenter } from "./refresh-token.presenter";
|
||||||
|
|
||||||
|
export const refreshTokenController = () => {
|
||||||
|
const transactionManager = new SequelizeTransactionManager();
|
||||||
|
const authService = new AuthService(authenticatedUserRepository, tabContextRepository);
|
||||||
|
|
||||||
|
const useCase = new RefreshTokenUseCase(authService, transactionManager);
|
||||||
|
const presenter = refreshTokenPresenter;
|
||||||
|
|
||||||
|
return new RefreshTokenController(useCase, presenter);
|
||||||
|
};
|
||||||
|
|||||||
@ -1,40 +1,49 @@
|
|||||||
import { ExpressController } from "@common/presentation";
|
import { ExpressController } from "@common/presentation";
|
||||||
import { createAuthService, IAuthService } from "@contexts/auth/application";
|
import { RefreshTokenUseCase } from "@contexts/auth/application";
|
||||||
import { IRefreshTokenPresenter, RefreshTokenPresenter } from "./refresh-token.presenter";
|
import { Token } from "@contexts/auth/domain";
|
||||||
|
import { IRefreshTokenPresenter } from "./refresh-token.presenter";
|
||||||
|
|
||||||
class RefreshTokenController extends ExpressController {
|
export class RefreshTokenController extends ExpressController {
|
||||||
private readonly _authService!: IAuthService;
|
public constructor(
|
||||||
private readonly _presenter!: IRefreshTokenPresenter;
|
private readonly refreshToken: RefreshTokenUseCase,
|
||||||
|
private readonly presenter: IRefreshTokenPresenter
|
||||||
public constructor(authService: IAuthService, presenter: IRefreshTokenPresenter) {
|
) {
|
||||||
super();
|
super();
|
||||||
this._authService = authService;
|
|
||||||
this._presenter = presenter;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async executeImpl() {
|
async executeImpl() {
|
||||||
const tabId = String(this.req.headers["x-tab-id"]);
|
//const tabId = String(this.req.headers["x-tab-id"]);
|
||||||
const refreshToken = String(this.req.body.refresh_token);
|
const refreshTokenOrError = Token.create(String(this.req.body.refresh_token));
|
||||||
|
|
||||||
const result = this._authService.verifyRefreshToken(refreshToken);
|
if (refreshTokenOrError.isFailure) {
|
||||||
if (!result || !result.email || !result.user_id || !result.tab_id || !result.roles) {
|
return this.clientError("Invalid input data", refreshTokenOrError.error);
|
||||||
return this.clientError("Invalid input data");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const { user_id, tab_id, email, roles } = result;
|
const newRefreshTokenOrError = this.refreshToken.execute(refreshTokenOrError.data);
|
||||||
|
|
||||||
const newRefreshToken = this._authService.generateRefreshToken({
|
if (newRefreshTokenOrError.isFailure) {
|
||||||
user_id,
|
return this.handleError(newRefreshTokenOrError.error);
|
||||||
tab_id,
|
|
||||||
email,
|
|
||||||
roles,
|
|
||||||
});
|
|
||||||
|
|
||||||
return this.created(this._presenter.map({ refreshToken: newRefreshToken }));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export const createRefreshTokenController = () => {
|
return this.created(this.presenter.toDto(newRefreshTokenOrError.data));
|
||||||
const authService = createAuthService();
|
}
|
||||||
return new RefreshTokenController(authService, RefreshTokenPresenter);
|
|
||||||
};
|
private handleError(error: Error) {
|
||||||
|
const message = error.message;
|
||||||
|
|
||||||
|
if (message.includes("User with this email already exists")) {
|
||||||
|
return this.conflictError(message);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
message.includes("Database connection lost") ||
|
||||||
|
message.includes("Database request timed out")
|
||||||
|
) {
|
||||||
|
return this.unavailableError(
|
||||||
|
"Database service is currently unavailable. Please try again later."
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.internalServerError(message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@ -1,15 +1,16 @@
|
|||||||
|
import { Token } from "@contexts/auth/domain";
|
||||||
import { IRefreshTokenResponseDTO } from "../../dto";
|
import { IRefreshTokenResponseDTO } from "../../dto";
|
||||||
|
|
||||||
export interface IRefreshTokenPresenter {
|
export interface IRefreshTokenPresenter {
|
||||||
map: (data: { refreshToken: string }) => IRefreshTokenResponseDTO;
|
toDto: (data: { refreshToken: Token }) => IRefreshTokenResponseDTO;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const RefreshTokenPresenter: IRefreshTokenPresenter = {
|
export const refreshTokenPresenter: IRefreshTokenPresenter = {
|
||||||
map: (data: { refreshToken: string }): IRefreshTokenResponseDTO => {
|
toDto: (data: { refreshToken: Token }): IRefreshTokenResponseDTO => {
|
||||||
const { refreshToken } = data;
|
const { refreshToken } = data;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
refresh_token: refreshToken,
|
refresh_token: refreshToken.toString(),
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
@ -1 +1,16 @@
|
|||||||
export * from "./register.controller";
|
import { SequelizeTransactionManager } from "@common/infrastructure";
|
||||||
|
import { RefreshTokenUseCase } from "@contexts/auth/application/register";
|
||||||
|
import { AuthService } from "@contexts/auth/domain/services";
|
||||||
|
import { authenticatedUserRepository, tabContextRepository } from "@contexts/auth/infraestructure";
|
||||||
|
import { RegisterController } from "./register.controller";
|
||||||
|
import { registerPresenter } from "./register.presenter";
|
||||||
|
|
||||||
|
export const registerController = () => {
|
||||||
|
const transactionManager = new SequelizeTransactionManager();
|
||||||
|
const authService = new AuthService(authenticatedUserRepository, tabContextRepository);
|
||||||
|
|
||||||
|
const useCase = new RefreshTokenUseCase(authService, transactionManager);
|
||||||
|
const presenter = registerPresenter;
|
||||||
|
|
||||||
|
return new RegisterController(useCase, presenter);
|
||||||
|
};
|
||||||
|
|||||||
@ -1,39 +1,38 @@
|
|||||||
import { ExpressController } from "@common/presentation";
|
import { ExpressController } from "@common/presentation";
|
||||||
import { createAuthService, IAuthService } from "@contexts/auth/application";
|
import { RefreshTokenUseCase } from "@contexts/auth/application/register";
|
||||||
import { EmailAddress, HashPassword, Username } from "@contexts/auth/domain";
|
import { RegisterData } from "@contexts/auth/domain";
|
||||||
import { IRegisterPresenter, RegisterPresenter } from "./register.presenter";
|
import { IRegisterPresenter } from "./register.presenter";
|
||||||
|
|
||||||
class RegisterController extends ExpressController {
|
export class RegisterController extends ExpressController {
|
||||||
private readonly _authService!: IAuthService;
|
public constructor(
|
||||||
private readonly _presenter!: IRegisterPresenter;
|
private readonly register: RefreshTokenUseCase,
|
||||||
|
private readonly presenter: IRegisterPresenter
|
||||||
public constructor(authService: IAuthService, presenter: IRegisterPresenter) {
|
) {
|
||||||
super();
|
super();
|
||||||
this._authService = authService;
|
|
||||||
this._presenter = presenter;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async executeImpl() {
|
async executeImpl() {
|
||||||
const emailVO = EmailAddress.create(this.req.body.email);
|
const registerDataOrError = RegisterData.createFromPrimitives({
|
||||||
const usernameVO = Username.create(this.req.body.username);
|
email: this.req.body.email,
|
||||||
const hashPasswordVO = HashPassword.create(this.req.body.password);
|
username: this.req.body.username,
|
||||||
|
plainPassword: this.req.body.password,
|
||||||
if ([emailVO, usernameVO, hashPasswordVO].some((r) => r.isFailure)) {
|
|
||||||
return this.clientError("Invalid input data");
|
|
||||||
}
|
|
||||||
|
|
||||||
if (emailVO.data.isEmpty()) {
|
|
||||||
return this.clientError("Invalid input data");
|
|
||||||
}
|
|
||||||
|
|
||||||
const userOrError = await this._authService.registerUser({
|
|
||||||
username: usernameVO.data,
|
|
||||||
email: emailVO.data,
|
|
||||||
hashPassword: hashPasswordVO.data,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if (registerDataOrError.isFailure) {
|
||||||
|
return this.clientError("Invalid input data", registerDataOrError.error);
|
||||||
|
}
|
||||||
|
|
||||||
|
const userOrError = await this.register.execute(registerDataOrError.data);
|
||||||
|
|
||||||
if (userOrError.isFailure) {
|
if (userOrError.isFailure) {
|
||||||
const message = userOrError.error.message;
|
return this.handleError(userOrError.error);
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.created(this.presenter.toDto(userOrError.data));
|
||||||
|
}
|
||||||
|
|
||||||
|
private handleError(error: Error) {
|
||||||
|
const message = error.message;
|
||||||
|
|
||||||
if (message.includes("User with this email already exists")) {
|
if (message.includes("User with this email already exists")) {
|
||||||
return this.conflictError(message);
|
return this.conflictError(message);
|
||||||
@ -50,12 +49,4 @@ class RegisterController extends ExpressController {
|
|||||||
|
|
||||||
return this.internalServerError(message);
|
return this.internalServerError(message);
|
||||||
}
|
}
|
||||||
|
|
||||||
return this.created(this._presenter.map(userOrError.data));
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
export const createRegisterController = () => {
|
|
||||||
const authService = createAuthService();
|
|
||||||
return new RegisterController(authService, RegisterPresenter);
|
|
||||||
};
|
|
||||||
|
|||||||
@ -2,11 +2,11 @@ import { AuthenticatedUser } from "@contexts/auth/domain";
|
|||||||
import { IRegisterUserResponseDTO } from "../../dto";
|
import { IRegisterUserResponseDTO } from "../../dto";
|
||||||
|
|
||||||
export interface IRegisterPresenter {
|
export interface IRegisterPresenter {
|
||||||
map: (user: AuthenticatedUser) => IRegisterUserResponseDTO;
|
toDto: (user: AuthenticatedUser) => IRegisterUserResponseDTO;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const RegisterPresenter: IRegisterPresenter = {
|
export const registerPresenter: IRegisterPresenter = {
|
||||||
map: (user: AuthenticatedUser): IRegisterUserResponseDTO => {
|
toDto: (user: AuthenticatedUser): IRegisterUserResponseDTO => {
|
||||||
//const { user, token, refreshToken } = loginUser;
|
//const { user, token, refreshToken } = loginUser;
|
||||||
//const roles = user.getRoles()?.map((rol) => rol.toString()) || [];
|
//const roles = user.getRoles()?.map((rol) => rol.toString()) || [];
|
||||||
|
|
||||||
|
|||||||
@ -1,3 +1,7 @@
|
|||||||
export * from "./auth.request.dto";
|
export * from "./auth.request.dto";
|
||||||
export * from "./auth.response.dto";
|
export * from "./auth.response.dto";
|
||||||
export * from "./auth.validation.dto";
|
export * from "./auth.validation.dto";
|
||||||
|
|
||||||
|
export * from "./user.request.dto";
|
||||||
|
export * from "./user.response.dto";
|
||||||
|
export * from "./user.validation.dto";
|
||||||
|
|||||||
@ -0,0 +1 @@
|
|||||||
|
export interface IListUsersRequestDTO {}
|
||||||
@ -0,0 +1,4 @@
|
|||||||
|
export interface IListUsersResponseDTO {
|
||||||
|
id: string;
|
||||||
|
email: string;
|
||||||
|
}
|
||||||
@ -0,0 +1,3 @@
|
|||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
export const ListUsersSchema = z.object({});
|
||||||
@ -1,3 +1,2 @@
|
|||||||
export * from "./controllers";
|
export * from "./controllers";
|
||||||
export * from "./dto";
|
export * from "./dto";
|
||||||
export * from "./middleware";
|
|
||||||
|
|||||||
@ -43,7 +43,8 @@ const serverError = (error: NodeJS.ErrnoException) => {
|
|||||||
logger.info(`⛔️ Server wasn't able to start properly.`);
|
logger.info(`⛔️ Server wasn't able to start properly.`);
|
||||||
|
|
||||||
if (error.code === "EADDRINUSE") {
|
if (error.code === "EADDRINUSE") {
|
||||||
logger.error(`The port ${error.port} is already used by another application.`);
|
logger.error(error.message);
|
||||||
|
//logger.error(`The port ${error.port} is already used by another application.`);
|
||||||
} else {
|
} else {
|
||||||
logger.error(error);
|
logger.error(error);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,12 +1,10 @@
|
|||||||
import { validateRequestDTO } from "@common/presentation";
|
import { validateRequestDTO } from "@common/presentation";
|
||||||
import { createAuthProvider } from "@contexts/auth/infraestructure";
|
import { checkUser, validateTabContextHeader } from "@contexts/auth/infraestructure";
|
||||||
import { validateTabContextHeader } from "@contexts/auth/presentation";
|
|
||||||
import {
|
import {
|
||||||
createLoginController,
|
loginController,
|
||||||
createRefreshTokenController,
|
logoutController,
|
||||||
|
registerController,
|
||||||
} from "@contexts/auth/presentation/controllers";
|
} from "@contexts/auth/presentation/controllers";
|
||||||
import { createLogoutController } from "@contexts/auth/presentation/controllers/logout/logout.controller";
|
|
||||||
import { createRegisterController } from "@contexts/auth/presentation/controllers/register/register.controller";
|
|
||||||
import {
|
import {
|
||||||
LoginUserSchema,
|
LoginUserSchema,
|
||||||
RefreshTokenSchema,
|
RefreshTokenSchema,
|
||||||
@ -16,8 +14,6 @@ import { NextFunction, Request, Response, Router } from "express";
|
|||||||
|
|
||||||
export const authRouter = (appRouter: Router) => {
|
export const authRouter = (appRouter: Router) => {
|
||||||
const authRoutes: Router = Router({ mergeParams: true });
|
const authRoutes: Router = Router({ mergeParams: true });
|
||||||
const authProvider = createAuthProvider();
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @api {post} /api/auth/register Register a new user
|
* @api {post} /api/auth/register Register a new user
|
||||||
* @apiName RegisterUser
|
* @apiName RegisterUser
|
||||||
@ -33,7 +29,7 @@ export const authRouter = (appRouter: Router) => {
|
|||||||
* @apiError (400) {String} message Error message.
|
* @apiError (400) {String} message Error message.
|
||||||
*/
|
*/
|
||||||
authRoutes.post("/register", validateRequestDTO(RegisterUserSchema), (req, res, next) => {
|
authRoutes.post("/register", validateRequestDTO(RegisterUserSchema), (req, res, next) => {
|
||||||
createRegisterController().execute(req, res, next);
|
registerController().execute(req, res, next);
|
||||||
});
|
});
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -56,7 +52,7 @@ export const authRouter = (appRouter: Router) => {
|
|||||||
validateRequestDTO(LoginUserSchema),
|
validateRequestDTO(LoginUserSchema),
|
||||||
validateTabContextHeader,
|
validateTabContextHeader,
|
||||||
(req: Request, res: Response, next: NextFunction) => {
|
(req: Request, res: Response, next: NextFunction) => {
|
||||||
createLoginController().execute(req, res, next);
|
loginController().execute(req, res, next);
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -74,19 +70,17 @@ export const authRouter = (appRouter: Router) => {
|
|||||||
authRoutes.post(
|
authRoutes.post(
|
||||||
"/logout",
|
"/logout",
|
||||||
validateTabContextHeader,
|
validateTabContextHeader,
|
||||||
authProvider.authenticateJWT(),
|
checkUser,
|
||||||
(req: Request, res: Response, next: NextFunction) => {
|
(req: Request, res: Response, next: NextFunction) => {
|
||||||
createLogoutController().execute(req, res, next);
|
logoutController().execute(req, res, next);
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
authRoutes.post(
|
authRoutes.post(
|
||||||
"/refresh",
|
"/refresh",
|
||||||
validateRequestDTO(RefreshTokenSchema),
|
validateRequestDTO(RefreshTokenSchema),
|
||||||
//validateTabContextHeader,
|
|
||||||
//authProvider.authenticateJWT(),
|
|
||||||
(req: Request, res: Response, next: NextFunction) => {
|
(req: Request, res: Response, next: NextFunction) => {
|
||||||
createRefreshTokenController().execute(req, res, next);
|
refreshTokenController().execute(req, res, next);
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
26
apps/server/src/routes/user.routes.ts
Normal file
26
apps/server/src/routes/user.routes.ts
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
import { validateRequestDTO } from "@common/presentation";
|
||||||
|
import { createAuthProvider } from "@contexts/auth/infraestructure";
|
||||||
|
import {
|
||||||
|
listUsersController,
|
||||||
|
ListUsersSchema,
|
||||||
|
validateTabContextHeader,
|
||||||
|
} from "@contexts/auth/presentation";
|
||||||
|
import { NextFunction, Request, Response, Router } from "express";
|
||||||
|
|
||||||
|
export const userRouter = (appRouter: Router) => {
|
||||||
|
const authRoutes: Router = Router({ mergeParams: true });
|
||||||
|
const authProvider = createAuthProvider();
|
||||||
|
|
||||||
|
authRoutes.get(
|
||||||
|
"/",
|
||||||
|
validateRequestDTO(ListUsersSchema),
|
||||||
|
validateTabContextHeader,
|
||||||
|
authProvider.authenticateJWT(),
|
||||||
|
//authProvider.checkIsAdmin(),
|
||||||
|
async (req: Request, res: Response, next: NextFunction) => {
|
||||||
|
listUsersController().execute(req, res, next);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
appRouter.use("/users", authRoutes);
|
||||||
|
};
|
||||||
@ -1,5 +1,6 @@
|
|||||||
import { Router } from "express";
|
import { Router } from "express";
|
||||||
import { authRouter } from "./auth.routes";
|
import { authRouter } from "./auth.routes";
|
||||||
|
import { userRouter } from "./user.routes";
|
||||||
|
|
||||||
export const v1Routes = () => {
|
export const v1Routes = () => {
|
||||||
const routes = Router({ mergeParams: true });
|
const routes = Router({ mergeParams: true });
|
||||||
@ -9,6 +10,7 @@ export const v1Routes = () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
authRouter(routes);
|
authRouter(routes);
|
||||||
|
userRouter(routes);
|
||||||
|
|
||||||
return routes;
|
return routes;
|
||||||
};
|
};
|
||||||
|
|||||||
18
packages/rdx-criteria/.eslintrc.json
Normal file
18
packages/rdx-criteria/.eslintrc.json
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
{
|
||||||
|
"extends": ["eslint-config-codely/typescript"],
|
||||||
|
"overrides": [
|
||||||
|
{
|
||||||
|
"files": ["*.ts"],
|
||||||
|
"parserOptions": {
|
||||||
|
"project": [
|
||||||
|
"./tsconfig.json",
|
||||||
|
"./packages/criteria/tsconfig.json",
|
||||||
|
"./packages/criteria-mysql/tsconfig.json"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"rules": {
|
||||||
|
"@typescript-eslint/no-floating-promises": ["off"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
2
packages/rdx-criteria/.gitignore
vendored
Normal file
2
packages/rdx-criteria/.gitignore
vendored
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
node_modules
|
||||||
|
dist
|
||||||
25
packages/rdx-criteria/package.json
Normal file
25
packages/rdx-criteria/package.json
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
{
|
||||||
|
"name": "@codelytv/criteria-monorepo",
|
||||||
|
"private": true,
|
||||||
|
"author": "",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=22"
|
||||||
|
},
|
||||||
|
"packageManager": "pnpm@9.3.0",
|
||||||
|
"workspaces": [
|
||||||
|
"packages/*"
|
||||||
|
],
|
||||||
|
"scripts": {
|
||||||
|
"test": "pnpm -r run test",
|
||||||
|
"build": "pnpm -r run build",
|
||||||
|
"release": "pnpm run build & pnpm changeset publish"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@changesets/cli": "^2.27.5",
|
||||||
|
"@types/node": "^22.10.7",
|
||||||
|
"eslint-config-codely": "^3.1.4",
|
||||||
|
"tsx": "^4.13.2",
|
||||||
|
"typescript": "^5.7.3"
|
||||||
|
}
|
||||||
|
}
|
||||||
17
packages/rdx-criteria/packages/criteria/package.json
Normal file
17
packages/rdx-criteria/packages/criteria/package.json
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
{
|
||||||
|
"name": "@rodax-software/criteria",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"description": "",
|
||||||
|
"keywords": [],
|
||||||
|
"author": "Rodax Software",
|
||||||
|
"license": "",
|
||||||
|
"main": "dist/index.js",
|
||||||
|
"types": "dist/index.d.ts",
|
||||||
|
"scripts": {
|
||||||
|
"test": "node --import tsx --test test/*.test.ts",
|
||||||
|
"build": "tsc --build --verbose tsconfig.json"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@faker-js/faker": "^8.3.1"
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,5 @@
|
|||||||
|
export interface ICriteriaProps {}
|
||||||
|
|
||||||
|
export interface ICriteria {}
|
||||||
|
|
||||||
|
class Criteria implements ICriteria {}
|
||||||
@ -0,0 +1,7 @@
|
|||||||
|
export const INITIAL_PAGE_INDEX = 0;
|
||||||
|
export const INITIAL_PAGE_SIZE = 10;
|
||||||
|
|
||||||
|
export const MIN_PAGE_INDEX = 0;
|
||||||
|
export const MIN_PAGE_SIZE = 1;
|
||||||
|
|
||||||
|
export const MAX_PAGE_SIZE = 9999; //Number.MAX_SAFE_INTEGER;
|
||||||
7
packages/rdx-criteria/packages/tsconfig.json
Normal file
7
packages/rdx-criteria/packages/tsconfig.json
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
{
|
||||||
|
"extends": "../../tsconfig.json",
|
||||||
|
"compilerOptions": {
|
||||||
|
"outDir": "dist"
|
||||||
|
},
|
||||||
|
"include": ["src/**/*"]
|
||||||
|
}
|
||||||
20
packages/rdx-criteria/tsconfig.json
Normal file
20
packages/rdx-criteria/tsconfig.json
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"experimentalDecorators": true,
|
||||||
|
"emitDecoratorMetadata": true,
|
||||||
|
"allowJs": true,
|
||||||
|
"esModuleInterop": true,
|
||||||
|
"forceConsistentCasingInFileNames": true,
|
||||||
|
"isolatedModules": true,
|
||||||
|
"module": "esnext",
|
||||||
|
"moduleResolution": "node",
|
||||||
|
"resolveJsonModule": true,
|
||||||
|
"noEmit": false,
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"strict": true,
|
||||||
|
"incremental": false,
|
||||||
|
"declaration": true,
|
||||||
|
"exactOptionalPropertyTypes": true,
|
||||||
|
"target": "es2020"
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue
Block a user