This commit is contained in:
David Arranz 2025-02-15 22:30:12 +01:00
parent 4e39aacedf
commit 80db9dfb9e
93 changed files with 1567 additions and 699 deletions

View File

@ -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();
}); });

View File

@ -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;
}) })

View File

@ -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,

View File

@ -19,7 +19,3 @@ export class SequelizeTransactionManager extends TransactionManager {
} }
} }
} }
export const createSequelizeTransactionManager = () => {
return new SequelizeTransactionManager();
};

View File

@ -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
); );

View File

@ -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>>;
}

View File

@ -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);
}
}
}

View File

@ -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);
};

View File

@ -0,0 +1 @@
export * from "./list-users.use-case";

View File

@ -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);
});
}
}

View File

@ -0,0 +1 @@
export * from "./login.use-case";

View File

@ -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);
});
}
}

View File

@ -0,0 +1 @@
export * from "./logout.use-case";

View File

@ -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);
});
}
}

View File

@ -0,0 +1 @@
export * from "./login.use-case";

View File

@ -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);
});
}
}

View File

@ -0,0 +1 @@
export * from "./refresh-token.use-case";

View File

@ -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,
});
});
}
}

View File

@ -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>>;
}

View File

@ -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);
}
}
}

View File

@ -1 +1,4 @@
export * from "./authenticated-user"; export * from "./authenticated-user";
export * from "./role";
export * from "./user";

View 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 {}
}

View 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()),
};
}
}

View File

@ -1 +1,4 @@
export * from "./login-data";
export * from "./logout-data";
export * from "./register-data";
export * from "./tab-context"; export * from "./tab-context";

View 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;
}
}

View 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;
}
}

View File

@ -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;
}
}

View File

@ -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";

View File

@ -0,0 +1 @@
export interface IUserPermissionRepository {}

View File

@ -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>>;
}

View File

@ -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>>;
}

View 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);
}
}

View 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";

View File

@ -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>>;
}

View File

@ -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);
}
}

View File

@ -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>>;
}

View File

@ -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);
}*/
}

View File

@ -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);
} }

View File

@ -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";

View 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);
}
}

View File

@ -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";

View File

@ -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;
}

View File

@ -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 };

View File

@ -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";

View File

@ -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;
}

View File

@ -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 };

View File

@ -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 };

View File

@ -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 = [

View File

@ -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 };

View File

@ -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 {

View File

@ -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);
} }

View File

@ -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 };

View File

@ -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;
}; };

View File

@ -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 };

View File

@ -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;
};

View File

@ -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 };

View File

@ -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";

View File

@ -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);
};

View File

@ -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);
}
}

View File

@ -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(),
})),
};

View File

@ -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);
};

View File

@ -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);
}
}

View File

@ -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(),
}, },
}; };
}, },

View File

@ -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);
};

View File

@ -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); }
};

View File

@ -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);
};

View File

@ -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);
}
}

View File

@ -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(),
}; };
}, },
}; };

View File

@ -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);
};

View File

@ -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);
};

View File

@ -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()) || [];

View File

@ -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";

View File

@ -0,0 +1 @@
export interface IListUsersRequestDTO {}

View File

@ -0,0 +1,4 @@
export interface IListUsersResponseDTO {
id: string;
email: string;
}

View File

@ -0,0 +1,3 @@
import { z } from "zod";
export const ListUsersSchema = z.object({});

View File

@ -1,3 +1,2 @@
export * from "./controllers"; export * from "./controllers";
export * from "./dto"; export * from "./dto";
export * from "./middleware";

View File

@ -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);
} }

View File

@ -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);
} }
); );

View 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);
};

View File

@ -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;
}; };

View 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
View File

@ -0,0 +1,2 @@
node_modules
dist

View 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"
}
}

View 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"
}
}

View File

@ -0,0 +1,5 @@
export interface ICriteriaProps {}
export interface ICriteria {}
class Criteria implements ICriteria {}

View File

@ -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;

View File

@ -0,0 +1,7 @@
{
"extends": "../../tsconfig.json",
"compilerOptions": {
"outDir": "dist"
},
"include": ["src/**/*"]
}

View 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"
}
}