diff --git a/apps/server/src/app.ts b/apps/server/src/app.ts index 410697d1..31e87534 100644 --- a/apps/server/src/app.ts +++ b/apps/server/src/app.ts @@ -1,6 +1,6 @@ import { logger } from "@common/infrastructure/logger"; import { globalErrorHandler } from "@common/presentation"; -import { createAuthProvider } from "@contexts/auth/infraestructure"; +import { authProvider } from "@contexts/auth/infraestructure"; import dotenv from "dotenv"; import express, { Application } from "express"; import helmet from "helmet"; @@ -26,7 +26,7 @@ export function createApp(): Application { // Inicializar Auth Provider app.use((req, res, next) => { - createAuthProvider().initialize(); + authProvider.initialize(); next(); }); diff --git a/apps/server/src/common/infrastructure/logger/index.ts b/apps/server/src/common/infrastructure/logger/index.ts index 4a7373da..d9de1ded 100644 --- a/apps/server/src/common/infrastructure/logger/index.ts +++ b/apps/server/src/common/infrastructure/logger/index.ts @@ -6,29 +6,37 @@ import DailyRotateFile from "winston-daily-rotate-file"; dotenv.config(); +const splatSymbol = Symbol.for("splat"); + const initLogger = () => { const isProduction = process.env.NODE_ENV === "production"; const consoleFormat = format.combine( format.colorize(), - format.timestamp(), + format.timestamp({ format: "YYYY-MM-DD HH:mm:ss" }), format.align(), format.splat(), + format.metadata(), format.errors({ stack: !isProduction }), + format.printf((info) => { const rid = rTracer.id(); + if (typeof info.message === "object") { + info.message = JSON.stringify(info.message, null, 3); + } + let out = isProduction && rid ? `${info.timestamp} [request-id:${rid}] - ${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}`; if (info.metadata?.error?.stack) { out = `${out} ${info.metadata.error.stack}`; } - } + }*/ return out; }) diff --git a/apps/server/src/common/infrastructure/sequelize/sequelize-repository.ts b/apps/server/src/common/infrastructure/sequelize/sequelize-repository.ts index 483eb9ef..91f8bf07 100644 --- a/apps/server/src/common/infrastructure/sequelize/sequelize-repository.ts +++ b/apps/server/src/common/infrastructure/sequelize/sequelize-repository.ts @@ -3,6 +3,18 @@ import { ModelDefined, Transaction } from "sequelize"; import { logger } from "../logger"; export abstract class SequelizeRepository implements IAggregateRootRepository { + protected async _findAll( + model: ModelDefined, + //queryCriteria?: IQueryCriteria, + params: any = {}, + transaction?: Transaction + ): Promise { + return model.findAll({ + transaction, + ...params, + }); + } + protected _findById( model: ModelDefined, id: string, diff --git a/apps/server/src/common/infrastructure/sequelize/sequelize-transaction-manager.ts b/apps/server/src/common/infrastructure/sequelize/sequelize-transaction-manager.ts index 6f69058d..afd4b3d3 100644 --- a/apps/server/src/common/infrastructure/sequelize/sequelize-transaction-manager.ts +++ b/apps/server/src/common/infrastructure/sequelize/sequelize-transaction-manager.ts @@ -19,7 +19,3 @@ export class SequelizeTransactionManager extends TransactionManager { } } } - -export const createSequelizeTransactionManager = () => { - return new SequelizeTransactionManager(); -}; diff --git a/apps/server/src/common/presentation/express/express-controller.ts b/apps/server/src/common/presentation/express/express-controller.ts index 232eb6f5..14ad723e 100644 --- a/apps/server/src/common/presentation/express/express-controller.ts +++ b/apps/server/src/common/presentation/express/express-controller.ts @@ -32,13 +32,13 @@ export abstract class ExpressController { /** * 🔹 Respuesta para errores de cliente (400 Bad Request) */ - public clientError(message: string, errors?: any[]) { + public clientError(message: string, errors?: any[] | any) { return ExpressController.errorResponse( new ApiError({ status: 400, title: "Bad Request", detail: message, - errors, + errors: Array.isArray(errors) ? errors : [errors], }), this.res ); diff --git a/apps/server/src/contexts/auth/application/auth-service.interface.ts b/apps/server/src/contexts/auth/application/auth-service.interface.ts deleted file mode 100644 index 238dd1cd..00000000 --- a/apps/server/src/contexts/auth/application/auth-service.interface.ts +++ /dev/null @@ -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>; - - 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>; - - getUserByEmail(params: { email: EmailAddress }): Promise>; -} diff --git a/apps/server/src/contexts/auth/application/auth.service.ts b/apps/server/src/contexts/auth/application/auth.service.ts deleted file mode 100644 index e977b5a9..00000000 --- a/apps/server/src/contexts/auth/application/auth.service.ts +++ /dev/null @@ -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> { - 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> { - 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> { - 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); - } - } -} diff --git a/apps/server/src/contexts/auth/application/index.ts b/apps/server/src/contexts/auth/application/index.ts index 0bf41b63..e8634dcd 100644 --- a/apps/server/src/contexts/auth/application/index.ts +++ b/apps/server/src/contexts/auth/application/index.ts @@ -1,24 +1,4 @@ -import { createSequelizeTransactionManager } from "@common/infrastructure"; - -import { createAuthenticatedUserRepository, createTabContextRepository } from "../infraestructure"; -import { IAuthService } from "./auth-service.interface"; -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); -}; +export * from "./list-users"; +export * from "./login"; +export * from "./logout"; +export * from "./register"; diff --git a/apps/server/src/contexts/auth/application/list-users/index.ts b/apps/server/src/contexts/auth/application/list-users/index.ts new file mode 100644 index 00000000..47eeda4a --- /dev/null +++ b/apps/server/src/contexts/auth/application/list-users/index.ts @@ -0,0 +1 @@ +export * from "./list-users.use-case"; diff --git a/apps/server/src/contexts/auth/application/list-users/list-users.use-case.ts b/apps/server/src/contexts/auth/application/list-users/list-users.use-case.ts new file mode 100644 index 00000000..dc53408a --- /dev/null +++ b/apps/server/src/contexts/auth/application/list-users/list-users.use-case.ts @@ -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> { + return await this.transactionManager.complete(async (transaction) => { + return await this.userService.findUsers(transaction); + }); + } +} diff --git a/apps/server/src/contexts/auth/application/login/index.ts b/apps/server/src/contexts/auth/application/login/index.ts new file mode 100644 index 00000000..71e9adab --- /dev/null +++ b/apps/server/src/contexts/auth/application/login/index.ts @@ -0,0 +1 @@ +export * from "./login.use-case"; diff --git a/apps/server/src/contexts/auth/application/login/login.use-case.ts b/apps/server/src/contexts/auth/application/login/login.use-case.ts new file mode 100644 index 00000000..34dde47c --- /dev/null +++ b/apps/server/src/contexts/auth/application/login/login.use-case.ts @@ -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); + }); + } +} diff --git a/apps/server/src/contexts/auth/application/logout/index.ts b/apps/server/src/contexts/auth/application/logout/index.ts new file mode 100644 index 00000000..c7dcefc5 --- /dev/null +++ b/apps/server/src/contexts/auth/application/logout/index.ts @@ -0,0 +1 @@ +export * from "./logout.use-case"; diff --git a/apps/server/src/contexts/auth/application/logout/logout.use-case.ts b/apps/server/src/contexts/auth/application/logout/logout.use-case.ts new file mode 100644 index 00000000..6e6bd550 --- /dev/null +++ b/apps/server/src/contexts/auth/application/logout/logout.use-case.ts @@ -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); + }); + } +} diff --git a/apps/server/src/contexts/auth/application/refresh-token/index.ts b/apps/server/src/contexts/auth/application/refresh-token/index.ts new file mode 100644 index 00000000..71e9adab --- /dev/null +++ b/apps/server/src/contexts/auth/application/refresh-token/index.ts @@ -0,0 +1 @@ +export * from "./login.use-case"; diff --git a/apps/server/src/contexts/auth/application/refresh-token/login.use-case.ts b/apps/server/src/contexts/auth/application/refresh-token/login.use-case.ts new file mode 100644 index 00000000..34dde47c --- /dev/null +++ b/apps/server/src/contexts/auth/application/refresh-token/login.use-case.ts @@ -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); + }); + } +} diff --git a/apps/server/src/contexts/auth/application/register/index.ts b/apps/server/src/contexts/auth/application/register/index.ts new file mode 100644 index 00000000..e1939e9b --- /dev/null +++ b/apps/server/src/contexts/auth/application/register/index.ts @@ -0,0 +1 @@ +export * from "./refresh-token.use-case"; diff --git a/apps/server/src/contexts/auth/application/register/refresh-token.use-case.ts b/apps/server/src/contexts/auth/application/register/refresh-token.use-case.ts new file mode 100644 index 00000000..95cd4e0f --- /dev/null +++ b/apps/server/src/contexts/auth/application/register/refresh-token.use-case.ts @@ -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, + }); + }); + } +} diff --git a/apps/server/src/contexts/auth/application/tab-context-service.interface.ts b/apps/server/src/contexts/auth/application/tab-context-service.interface.ts deleted file mode 100644 index ffc2eb3b..00000000 --- a/apps/server/src/contexts/auth/application/tab-context-service.interface.ts +++ /dev/null @@ -1,8 +0,0 @@ -import { Result, UniqueID } from "@common/domain"; -import { TabContext } from "../domain"; - -export interface ITabContextService { - getContextByTabId(tabId: UniqueID): Promise>; - createContext(params: { tabId: UniqueID; userId: UniqueID }): Promise>; - removeContext(params: { tabId: UniqueID; userId: UniqueID }): Promise>; -} diff --git a/apps/server/src/contexts/auth/application/tab-context.service.ts b/apps/server/src/contexts/auth/application/tab-context.service.ts deleted file mode 100644 index b2d73cd6..00000000 --- a/apps/server/src/contexts/auth/application/tab-context.service.ts +++ /dev/null @@ -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> { - 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> { - 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> { - 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); - } - } -} diff --git a/apps/server/src/contexts/auth/domain/aggregates/index.ts b/apps/server/src/contexts/auth/domain/aggregates/index.ts index 864c998e..a20563f2 100644 --- a/apps/server/src/contexts/auth/domain/aggregates/index.ts +++ b/apps/server/src/contexts/auth/domain/aggregates/index.ts @@ -1 +1,4 @@ export * from "./authenticated-user"; + +export * from "./role"; +export * from "./user"; diff --git a/apps/server/src/contexts/auth/domain/aggregates/role.ts b/apps/server/src/contexts/auth/domain/aggregates/role.ts new file mode 100644 index 00000000..7a5887dc --- /dev/null +++ b/apps/server/src/contexts/auth/domain/aggregates/role.ts @@ -0,0 +1,14 @@ +import { AggregateRoot, Result, UniqueID } from "@common/domain"; + +export interface IRoleProps {} + +export interface IRole {} + +export class Role extends AggregateRoot implements IRole { + static create(props: IRoleProps, id: UniqueID): Result { + const role = new Role(props, id); + return Result.ok(role); + } + + toPersistenceData(): any {} +} diff --git a/apps/server/src/contexts/auth/domain/aggregates/user-permission.ts b/apps/server/src/contexts/auth/domain/aggregates/user-permission.ts new file mode 100644 index 00000000..e69de29b diff --git a/apps/server/src/contexts/auth/domain/aggregates/user.ts b/apps/server/src/contexts/auth/domain/aggregates/user.ts new file mode 100644 index 00000000..e0d21595 --- /dev/null +++ b/apps/server/src/contexts/auth/domain/aggregates/user.ts @@ -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 implements IUser { + static create(props: IUserProps, id: UniqueID): Result { + 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()), + }; + } +} diff --git a/apps/server/src/contexts/auth/domain/entities/index.ts b/apps/server/src/contexts/auth/domain/entities/index.ts index 925aef3b..f78c931c 100644 --- a/apps/server/src/contexts/auth/domain/entities/index.ts +++ b/apps/server/src/contexts/auth/domain/entities/index.ts @@ -1 +1,4 @@ +export * from "./login-data"; +export * from "./logout-data"; +export * from "./register-data"; export * from "./tab-context"; diff --git a/apps/server/src/contexts/auth/domain/entities/login-data.ts b/apps/server/src/contexts/auth/domain/entities/login-data.ts new file mode 100644 index 00000000..2dab132b --- /dev/null +++ b/apps/server/src/contexts/auth/domain/entities/login-data.ts @@ -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 implements ILoginData { + static create(props: ILoginDataProps): Result { + return Result.ok(new this(props)); + } + + static createFromPrimitives(props: { + email: string; + plainPassword: string; + tabId: string; + }): Result { + 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; + } +} diff --git a/apps/server/src/contexts/auth/domain/entities/logout-data.ts b/apps/server/src/contexts/auth/domain/entities/logout-data.ts new file mode 100644 index 00000000..836e4b3f --- /dev/null +++ b/apps/server/src/contexts/auth/domain/entities/logout-data.ts @@ -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 implements ILogoutData { + static create(props: ILogoutDataProps): Result { + return Result.ok(new this(props)); + } + + static createFromPrimitives(props: { email: string; tabId: string }): Result { + 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; + } +} diff --git a/apps/server/src/contexts/auth/domain/entities/register-data.ts b/apps/server/src/contexts/auth/domain/entities/register-data.ts new file mode 100644 index 00000000..ea789a74 --- /dev/null +++ b/apps/server/src/contexts/auth/domain/entities/register-data.ts @@ -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 implements IRegisterData { + static create(props: IRegisterDataProps): Result { + return Result.ok(new this(props)); + } + + static createFromPrimitives(props: { + username: string; + email: string; + plainPassword: string; + }): Result { + 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; + } +} diff --git a/apps/server/src/contexts/auth/domain/repositories/index.ts b/apps/server/src/contexts/auth/domain/repositories/index.ts index c493a1cc..b5f0a9ed 100644 --- a/apps/server/src/contexts/auth/domain/repositories/index.ts +++ b/apps/server/src/contexts/auth/domain/repositories/index.ts @@ -1,2 +1,4 @@ export * from "./authenticated-user-repository.interface"; export * from "./tab-context-repository.interface"; +export * from "./user-permission-repository.interface"; +export * from "./user-repository.interface"; diff --git a/apps/server/src/contexts/auth/domain/repositories/user-permission-repository.interface.ts b/apps/server/src/contexts/auth/domain/repositories/user-permission-repository.interface.ts new file mode 100644 index 00000000..b64bffb8 --- /dev/null +++ b/apps/server/src/contexts/auth/domain/repositories/user-permission-repository.interface.ts @@ -0,0 +1 @@ +export interface IUserPermissionRepository {} diff --git a/apps/server/src/contexts/auth/domain/repositories/user-repository.interface.ts b/apps/server/src/contexts/auth/domain/repositories/user-repository.interface.ts new file mode 100644 index 00000000..47b31743 --- /dev/null +++ b/apps/server/src/contexts/auth/domain/repositories/user-repository.interface.ts @@ -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>; + findById(id: UniqueID, transaction?: any): Promise>; + findByEmail(email: EmailAddress, transaction?: any): Promise>; +} diff --git a/apps/server/src/contexts/auth/domain/services/auth-service.interface.ts b/apps/server/src/contexts/auth/domain/services/auth-service.interface.ts new file mode 100644 index 00000000..6fd99665 --- /dev/null +++ b/apps/server/src/contexts/auth/domain/services/auth-service.interface.ts @@ -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; + generateRefreshToken(payload: IJWTPayload): Result; + verifyRefreshToken(token: Token): IJWTPayload; + + registerUser( + registerData: RegisterData, + transaction?: any + ): Promise>; + + loginUser( + loginData: LoginData, + transaction?: any + ): Promise< + Result< + { + user: AuthenticatedUser; + tabContext: TabContext; + tokens: { + accessToken: Token; + refreshToken: Token; + }; + }, + Error + > + >; + + logoutUser(logoutData: LogoutData, transaction?: any): Promise>; + getUserByEmail(email: EmailAddress, transaction?: any): Promise>; +} diff --git a/apps/server/src/contexts/auth/domain/services/auth.service.ts b/apps/server/src/contexts/auth/domain/services/auth.service.ts new file mode 100644 index 00000000..d210f6ae --- /dev/null +++ b/apps/server/src/contexts/auth/domain/services/auth.service.ts @@ -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 { + return Token.create(JwtHelper.generateToken(payload, ACCESS_EXPIRATION)); + } + + generateRefreshToken(payload: IJWTPayload): Result { + 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> { + 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> { + 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> { + 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); + } +} diff --git a/apps/server/src/contexts/auth/domain/services/index.ts b/apps/server/src/contexts/auth/domain/services/index.ts new file mode 100644 index 00000000..abde394c --- /dev/null +++ b/apps/server/src/contexts/auth/domain/services/index.ts @@ -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"; diff --git a/apps/server/src/contexts/auth/domain/services/tab-context-service.interface.ts b/apps/server/src/contexts/auth/domain/services/tab-context-service.interface.ts new file mode 100644 index 00000000..aabc7c29 --- /dev/null +++ b/apps/server/src/contexts/auth/domain/services/tab-context-service.interface.ts @@ -0,0 +1,14 @@ +import { Result, UniqueID } from "@common/domain"; +import { TabContext } from "../entities"; + +export interface ITabContextService { + getContextByTabId(tabId: UniqueID): Promise>; + createContext( + params: { tabId: UniqueID; userId: UniqueID }, + transaction?: any + ): Promise>; + removeContext( + params: { tabId: UniqueID; userId: UniqueID }, + transaction?: any + ): Promise>; +} diff --git a/apps/server/src/contexts/auth/domain/services/tab-context.service.ts b/apps/server/src/contexts/auth/domain/services/tab-context.service.ts new file mode 100644 index 00000000..56185580 --- /dev/null +++ b/apps/server/src/contexts/auth/domain/services/tab-context.service.ts @@ -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> { + // 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> { + 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> { + 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); + } +} diff --git a/apps/server/src/contexts/auth/domain/services/user-service.interface.ts b/apps/server/src/contexts/auth/domain/services/user-service.interface.ts new file mode 100644 index 00000000..84a93464 --- /dev/null +++ b/apps/server/src/contexts/auth/domain/services/user-service.interface.ts @@ -0,0 +1,7 @@ +import { Result, UniqueID } from "@common/domain"; +import { User } from "../aggregates"; + +export interface IUserService { + findUsers(transaction?: any): Promise>; + findUserById(userId: UniqueID, transaction?: any): Promise>; +} diff --git a/apps/server/src/contexts/auth/domain/services/user.service.ts b/apps/server/src/contexts/auth/domain/services/user.service.ts new file mode 100644 index 00000000..ab346572 --- /dev/null +++ b/apps/server/src/contexts/auth/domain/services/user.service.ts @@ -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> { + 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> { + return await this.userRepository.findById(userId, transaction); + } + + /*public async createUser( + data: { name: string; email: EmailAddress }, + transaction?: Transaction + ): Promise> { + // 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); + }*/ +} diff --git a/apps/server/src/contexts/auth/domain/value-objects/hash-password.ts b/apps/server/src/contexts/auth/domain/value-objects/hash-password.ts index 0a1e6431..de2be1b2 100644 --- a/apps/server/src/contexts/auth/domain/value-objects/hash-password.ts +++ b/apps/server/src/contexts/auth/domain/value-objects/hash-password.ts @@ -5,8 +5,8 @@ import { z } from "zod"; export class HashPassword extends ValueObject { private static readonly SALT_ROUNDS = 10; - static create(plainPassword: string): Result { - const result = HashPassword.validate(plainPassword); + static createFromPlainText(plainTextPassword: string): Result { + const result = HashPassword.validate(plainTextPassword); if (!result.success) { return Result.fail(new Error(result.error.errors[0].message)); @@ -25,10 +25,6 @@ export class HashPassword extends ValueObject { return Result.ok(new HashPassword(hashedPassword)); } - static createFromPlainText(plainTextPassword: string): Result { - return HashPassword.create(plainTextPassword); - } - async verifyPassword(plainTextPassword: string): Promise { return await bcrypt.compare(plainTextPassword, this._value); } diff --git a/apps/server/src/contexts/auth/domain/value-objects/index.ts b/apps/server/src/contexts/auth/domain/value-objects/index.ts index 9fdade14..944f6de9 100644 --- a/apps/server/src/contexts/auth/domain/value-objects/index.ts +++ b/apps/server/src/contexts/auth/domain/value-objects/index.ts @@ -2,4 +2,5 @@ export * from "./auth-user-roles"; export * from "./email-address"; export * from "./hash-password"; export * from "./plain-password"; +export * from "./token"; export * from "./username"; diff --git a/apps/server/src/contexts/auth/domain/value-objects/token.ts b/apps/server/src/contexts/auth/domain/value-objects/token.ts new file mode 100644 index 00000000..1b18a793 --- /dev/null +++ b/apps/server/src/contexts/auth/domain/value-objects/token.ts @@ -0,0 +1,19 @@ +import { Result, ValueObject } from "@common/domain"; +import { z } from "zod"; + +export class Token extends ValueObject { + static create(token: string): Result { + 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); + } +} diff --git a/apps/server/src/contexts/auth/infraestructure/index.ts b/apps/server/src/contexts/auth/infraestructure/index.ts index 01cd6cb2..f512c2c3 100644 --- a/apps/server/src/contexts/auth/infraestructure/index.ts +++ b/apps/server/src/contexts/auth/infraestructure/index.ts @@ -1,3 +1,4 @@ export * from "./mappers"; +export * from "./middleware"; export * from "./passport"; export * from "./sequelize"; diff --git a/apps/server/src/contexts/auth/infraestructure/mappers/authenticated-user-mapper.interface.ts b/apps/server/src/contexts/auth/infraestructure/mappers/authenticated-user-mapper.interface.ts deleted file mode 100644 index 60de81a0..00000000 --- a/apps/server/src/contexts/auth/infraestructure/mappers/authenticated-user-mapper.interface.ts +++ /dev/null @@ -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; - toPersistence(aggregate: AuthenticatedUser): AuthUserModel; -} diff --git a/apps/server/src/contexts/auth/infraestructure/mappers/authenticated-user.mapper.ts b/apps/server/src/contexts/auth/infraestructure/mappers/authenticated-user.mapper.ts index 66517016..2d955df5 100644 --- a/apps/server/src/contexts/auth/infraestructure/mappers/authenticated-user.mapper.ts +++ b/apps/server/src/contexts/auth/infraestructure/mappers/authenticated-user.mapper.ts @@ -1,9 +1,13 @@ import { Result, UniqueID } from "@common/domain"; import { AuthenticatedUser, EmailAddress, HashPassword, Username } from "@contexts/auth/domain"; import { AuthUserModel } from "../sequelize"; -import { IAuthenticatedUserMapper } from "./authenticated-user-mapper.interface"; -export class AuthenticatedUserMapper implements IAuthenticatedUserMapper { +export interface IAuthenticatedUserMapper { + toDomain(entity: AuthUserModel): Result; + toPersistence(aggregate: AuthenticatedUser): AuthUserModel; +} + +class AuthenticatedUserMapper implements IAuthenticatedUserMapper { /** * 🔹 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 => - new AuthenticatedUserMapper(); +const authenticatedUserMapper: IAuthenticatedUserMapper = new AuthenticatedUserMapper(); +export { authenticatedUserMapper }; diff --git a/apps/server/src/contexts/auth/infraestructure/mappers/index.ts b/apps/server/src/contexts/auth/infraestructure/mappers/index.ts index 4f3fcdc7..fcc87a1f 100644 --- a/apps/server/src/contexts/auth/infraestructure/mappers/index.ts +++ b/apps/server/src/contexts/auth/infraestructure/mappers/index.ts @@ -1,4 +1,3 @@ -export * from "./authenticated-user-mapper.interface"; export * from "./authenticated-user.mapper"; -export * from "./tab-context-mapper.interface"; export * from "./tab-context.mapper"; +export * from "./user.mapper"; diff --git a/apps/server/src/contexts/auth/infraestructure/mappers/tab-context-mapper.interface.ts b/apps/server/src/contexts/auth/infraestructure/mappers/tab-context-mapper.interface.ts deleted file mode 100644 index 98bb4b69..00000000 --- a/apps/server/src/contexts/auth/infraestructure/mappers/tab-context-mapper.interface.ts +++ /dev/null @@ -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; - toPersistence(aggregate: TabContext): TabContextModel; -} diff --git a/apps/server/src/contexts/auth/infraestructure/mappers/tab-context.mapper.ts b/apps/server/src/contexts/auth/infraestructure/mappers/tab-context.mapper.ts index 7dc5a150..d1aecc7e 100644 --- a/apps/server/src/contexts/auth/infraestructure/mappers/tab-context.mapper.ts +++ b/apps/server/src/contexts/auth/infraestructure/mappers/tab-context.mapper.ts @@ -1,9 +1,13 @@ import { Result, UniqueID } from "@common/domain"; import { TabContext } from "@contexts/auth/domain"; import { TabContextModel } from "../sequelize"; -import { ITabContextMapper } from "./tab-context-mapper.interface"; -export class TabContextMapper implements ITabContextMapper { +export interface ITabContextMapper { + toDomain(entity: TabContextModel): Result; + toPersistence(aggregate: TabContext): TabContextModel; +} + +class TabContextMapper implements ITabContextMapper { toDomain(entity: TabContextModel): Result { if (!entity) { return Result.fail(new Error("Entity not found")); @@ -13,16 +17,16 @@ export class TabContextMapper implements ITabContextMapper { const uniqueIdResult = UniqueID.create(entity.id); const tabIdResult = UniqueID.create(entity.tab_id); const userIdResult = UniqueID.create(entity.user_id); - const companyIdResult = UniqueID.create(entity.company_id, false); - const brachIdResult = UniqueID.create(entity.branch_id, false); + //const companyIdResult = UniqueID.create(entity.company_id, false); + //const brachIdResult = UniqueID.create(entity.branch_id, false); // Validar que no haya errores en la creación de los Value Objects const okOrError = Result.combine([ uniqueIdResult, tabIdResult, userIdResult, - companyIdResult, - brachIdResult, + //companyIdResult, + //brachIdResult, ]); if (okOrError.isFailure) { return Result.fail(okOrError.error.message); @@ -33,8 +37,8 @@ export class TabContextMapper implements ITabContextMapper { { tabId: tabIdResult.data!, userId: userIdResult.data!, - companyId: companyIdResult.data, - branchId: brachIdResult.data, + //companyId: companyIdResult.data, + //branchId: brachIdResult.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 }; diff --git a/apps/server/src/contexts/auth/infraestructure/mappers/user.mapper.ts b/apps/server/src/contexts/auth/infraestructure/mappers/user.mapper.ts new file mode 100644 index 00000000..47d9a851 --- /dev/null +++ b/apps/server/src/contexts/auth/infraestructure/mappers/user.mapper.ts @@ -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; + toDomainArray(entities: UserModel[]): Result; + + 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 { + 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 { + 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 }; diff --git a/apps/server/src/contexts/auth/presentation/middleware/index.ts b/apps/server/src/contexts/auth/infraestructure/middleware/index.ts similarity index 100% rename from apps/server/src/contexts/auth/presentation/middleware/index.ts rename to apps/server/src/contexts/auth/infraestructure/middleware/index.ts diff --git a/apps/server/src/contexts/auth/presentation/middleware/passport-auth.middleware.ts b/apps/server/src/contexts/auth/infraestructure/middleware/passport-auth.middleware.ts similarity index 85% rename from apps/server/src/contexts/auth/presentation/middleware/passport-auth.middleware.ts rename to apps/server/src/contexts/auth/infraestructure/middleware/passport-auth.middleware.ts index 7d906d18..a9b6317d 100644 --- a/apps/server/src/contexts/auth/presentation/middleware/passport-auth.middleware.ts +++ b/apps/server/src/contexts/auth/infraestructure/middleware/passport-auth.middleware.ts @@ -1,6 +1,7 @@ import { UniqueID } from "@common/domain"; import { ApiError, ExpressController } from "@common/presentation"; import { AuthenticatedUser } from "@contexts/auth/domain"; +import { authProvider } from "@contexts/auth/infraestructure"; import { NextFunction, Request, Response } from "express"; // 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 -export const authenticateUser = [_authorizeUser((user) => user.isUser)]; +export const checkUser = [authProvider.authenticateJWT(), _authorizeUser((user) => user.isUser)]; // 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) export const checkUserIsAdminOrOwner = [ diff --git a/apps/server/src/contexts/auth/presentation/middleware/tab-context.middleware.ts b/apps/server/src/contexts/auth/infraestructure/middleware/tab-context.middleware.ts similarity index 100% rename from apps/server/src/contexts/auth/presentation/middleware/tab-context.middleware.ts rename to apps/server/src/contexts/auth/infraestructure/middleware/tab-context.middleware.ts diff --git a/apps/server/src/contexts/auth/infraestructure/passport/index.ts b/apps/server/src/contexts/auth/infraestructure/passport/index.ts index 6e22cbc9..61f318ef 100644 --- a/apps/server/src/contexts/auth/infraestructure/passport/index.ts +++ b/apps/server/src/contexts/auth/infraestructure/passport/index.ts @@ -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"; -export { IJWTPayload }; +const transactionManager = new SequelizeTransactionManager(); +const authService = new AuthService(authenticatedUserRepository, tabContextRepository); +const tabContextService = new TabContextService(tabContextRepository); -export const createAuthProvider = () => - new PassportAuthProvider(createAuthService(), createTabContextService()); +const authProvider = new PassportAuthProvider(authService, tabContextService, transactionManager); +export { authProvider, IJWTPayload }; diff --git a/apps/server/src/contexts/auth/infraestructure/passport/jwt.helper.ts b/apps/server/src/contexts/auth/infraestructure/passport/jwt.helper.ts index 7f8b09af..c0f3981c 100644 --- a/apps/server/src/contexts/auth/infraestructure/passport/jwt.helper.ts +++ b/apps/server/src/contexts/auth/infraestructure/passport/jwt.helper.ts @@ -1,10 +1,11 @@ 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 { 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 { diff --git a/apps/server/src/contexts/auth/infraestructure/passport/passport-auth-provider.ts b/apps/server/src/contexts/auth/infraestructure/passport/passport-auth-provider.ts index 2bc91650..f18e0ba9 100644 --- a/apps/server/src/contexts/auth/infraestructure/passport/passport-auth-provider.ts +++ b/apps/server/src/contexts/auth/infraestructure/passport/passport-auth-provider.ts @@ -1,7 +1,7 @@ import { Result, UniqueID } from "@common/domain"; -import { IAuthService } from "@contexts/auth/application"; -import { ITabContextService } from "@contexts/auth/application/tab-context-service.interface"; +import { ITransactionManager } from "@common/infrastructure/database"; import { AuthenticatedUser, EmailAddress, PlainPassword } from "@contexts/auth/domain"; +import { IAuthService, ITabContextService } from "@contexts/auth/domain/services"; import passport from "passport"; import { ExtractJwt, Strategy as JwtStrategy } from "passport-jwt"; import { Strategy as LocalStrategy } from "passport-local"; @@ -16,9 +16,6 @@ export interface IJWTPayload { } export class PassportAuthProvider { - private readonly _authService: IAuthService; - private readonly _tabContextService: ITabContextService; - private async _getUserByEmailAndPassword( email: string, password: string @@ -33,9 +30,7 @@ export class PassportAuthProvider { return Result.fail(plainPasswordVO.error); } - const userResult = await this._authService.getUserByEmail({ - email: emailVO.data, - }); + const userResult = await this.authService.getUserByEmail(emailVO.data); if (userResult.isFailure || !userResult.data) { return Result.fail(new Error("Invalid email or password")); @@ -52,10 +47,10 @@ export class PassportAuthProvider { } 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 tabIdVO = UniqueID.create(tabId); + const userIdVO = UniqueID.create(user_id); + const tabIdVO = UniqueID.create(tab_id); const emailVO = EmailAddress.create(email!); const okOrError = Result.combine([userIdVO, tabIdVO, emailVO]); @@ -63,9 +58,7 @@ export class PassportAuthProvider { return Result.fail(okOrError.error.message); } - const userResult = await this._authService.getUserByEmail({ - email: emailVO.data, - }); + const userResult = await this.authService.getUserByEmail(emailVO.data); if (userResult.isFailure) { return Result.fail(new Error("Invalid token data")); @@ -80,7 +73,7 @@ export class PassportAuthProvider { 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) { return Result.fail(new Error("Invalid token data")); } @@ -93,10 +86,11 @@ export class PassportAuthProvider { }); } - constructor(authService: IAuthService, tabContextService: ITabContextService) { - this._authService = authService; - this._tabContextService = tabContextService; - } + constructor( + private readonly authService: IAuthService, + private readonly tabContextService: ITabContextService, + private readonly transactionManager: ITransactionManager + ) {} /** * 🔹 Configura PassportJS @@ -111,10 +105,10 @@ export class PassportAuthProvider { "jwt", new JwtStrategy(jwtOptions, async (tokenPayload, done) => { try { - const result = await this._getUserAndContextByToken(tokenPayload); - return result.isSuccess - ? done(null, result) - : done(result.error, false, { message: "Invalid JWT data" }); + const userOrError = await this._getUserAndContextByToken(tokenPayload); + return userOrError.isSuccess + ? done(null, userOrError.data) + : done(userOrError.error, false, { message: "Invalid JWT data" }); } catch (error) { return done(error, false); } @@ -127,10 +121,10 @@ export class PassportAuthProvider { { usernameField: "email", passwordField: "password" }, async (email, password, done) => { try { - const user = await this._getUserByEmailAndPassword(email, password); - return user - ? done(null, user) - : done(null, false, { message: "Invalid email or password" }); + const userOrError = await this._getUserByEmailAndPassword(email, password); + return userOrError.isSuccess + ? done(null, userOrError.data) + : done(userOrError.error, false, { message: "Invalid email or password" }); } catch (error) { return done(error, false); } diff --git a/apps/server/src/contexts/auth/infraestructure/sequelize/authenticated-user.repository.ts b/apps/server/src/contexts/auth/infraestructure/sequelize/authenticated-user.repository.ts index c2d0918d..94765e4f 100644 --- a/apps/server/src/contexts/auth/infraestructure/sequelize/authenticated-user.repository.ts +++ b/apps/server/src/contexts/auth/infraestructure/sequelize/authenticated-user.repository.ts @@ -6,10 +6,10 @@ import { IAuthenticatedUserRepository, } from "@contexts/auth/domain"; import { Transaction } from "sequelize"; -import { IAuthenticatedUserMapper } from "../mappers"; +import { authenticatedUserMapper, IAuthenticatedUserMapper } from "../mappers"; import { AuthUserModel } from "./auth-user.model"; -export class AuthenticatedUserRepository +class AuthenticatedUserRepository extends SequelizeRepository implements IAuthenticatedUserRepository { @@ -85,3 +85,6 @@ export class AuthenticatedUserRepository } } } + +const authenticatedUserRepository = new AuthenticatedUserRepository(authenticatedUserMapper); +export { authenticatedUserRepository }; diff --git a/apps/server/src/contexts/auth/infraestructure/sequelize/index.ts b/apps/server/src/contexts/auth/infraestructure/sequelize/index.ts index e4c04d61..0fc90355 100644 --- a/apps/server/src/contexts/auth/infraestructure/sequelize/index.ts +++ b/apps/server/src/contexts/auth/infraestructure/sequelize/index.ts @@ -1,17 +1,25 @@ -import { IAuthenticatedUserRepository, ITabContextRepository } from "../../domain"; -import { createAuthenticatedUserMapper, createTabContextMapper } from "../mappers"; -import { AuthenticatedUserRepository } from "./authenticated-user.repository"; -import { TabContextRepository } from "./tab-context.repository"; +import { IAuthenticatedUserRepository, ITabContextRepository, IUserRepository } from "../../domain"; +import { authenticatedUserRepository } from "./authenticated-user.repository"; +import { tabContextRepository } from "./tab-context.repository"; +import { userRepository } from "./user.repository"; export * from "./auth-user.model"; +export * from "./authenticated-user.repository"; + export * from "./tab-context.model"; +export * from "./tab-context.repository"; + +export * from "./user.model"; +export * from "./user.repository"; export const createAuthenticatedUserRepository = (): IAuthenticatedUserRepository => { - const authenticatedUserMapper = createAuthenticatedUserMapper(); - return new AuthenticatedUserRepository(authenticatedUserMapper); + return authenticatedUserRepository; }; export const createTabContextRepository = (): ITabContextRepository => { - const tabContextMapper = createTabContextMapper(); - return new TabContextRepository(tabContextMapper); + return tabContextRepository; +}; + +export const createUserRepository = (): IUserRepository => { + return userRepository; }; diff --git a/apps/server/src/contexts/auth/infraestructure/sequelize/tab-context.repository.ts b/apps/server/src/contexts/auth/infraestructure/sequelize/tab-context.repository.ts index 6c527ec8..531f7606 100644 --- a/apps/server/src/contexts/auth/infraestructure/sequelize/tab-context.repository.ts +++ b/apps/server/src/contexts/auth/infraestructure/sequelize/tab-context.repository.ts @@ -1,12 +1,11 @@ import { Result, UniqueID } from "@common/domain"; import { SequelizeRepository } from "@common/infrastructure"; -import { TabContext } from "@contexts/auth/domain/"; -import { ITabContextRepository } from "@contexts/auth/domain/repositories/tab-context-repository.interface"; +import { ITabContextRepository, TabContext } from "@contexts/auth/domain/"; import { Op, Transaction } from "sequelize"; -import { ITabContextMapper } from "../mappers"; +import { ITabContextMapper, tabContextMapper } from "../mappers"; import { TabContextModel } from "./tab-context.model"; -export class TabContextRepository +class TabContextRepository extends SequelizeRepository implements ITabContextRepository { @@ -125,3 +124,6 @@ export class TabContextRepository } } } + +const tabContextRepository = new TabContextRepository(tabContextMapper); +export { tabContextRepository }; diff --git a/apps/server/src/contexts/auth/infraestructure/sequelize/user.model.ts b/apps/server/src/contexts/auth/infraestructure/sequelize/user.model.ts new file mode 100644 index 00000000..a15e58a4 --- /dev/null +++ b/apps/server/src/contexts/auth/infraestructure/sequelize/user.model.ts @@ -0,0 +1,55 @@ +import { DataTypes, InferAttributes, InferCreationAttributes, Model, Sequelize } from "sequelize"; + +export type UserCreationAttributes = InferCreationAttributes; + +export class UserModel extends Model< + InferAttributes, + InferCreationAttributes +> { + // To avoid table creation + /*static async sync(): Promise { + 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; +}; diff --git a/apps/server/src/contexts/auth/infraestructure/sequelize/user.repository.ts b/apps/server/src/contexts/auth/infraestructure/sequelize/user.repository.ts new file mode 100644 index 00000000..5823e679 --- /dev/null +++ b/apps/server/src/contexts/auth/infraestructure/sequelize/user.repository.ts @@ -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 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> { + 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> { + 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> { + 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 }; diff --git a/apps/server/src/contexts/auth/presentation/controllers/index.ts b/apps/server/src/contexts/auth/presentation/controllers/index.ts index 3ad116cf..644a8bb1 100644 --- a/apps/server/src/contexts/auth/presentation/controllers/index.ts +++ b/apps/server/src/contexts/auth/presentation/controllers/index.ts @@ -1,3 +1,4 @@ +export * from "./listUsers"; export * from "./login"; export * from "./logout"; export * from "./refreshToken"; diff --git a/apps/server/src/contexts/auth/presentation/controllers/listUsers/index.ts b/apps/server/src/contexts/auth/presentation/controllers/listUsers/index.ts new file mode 100644 index 00000000..9b390787 --- /dev/null +++ b/apps/server/src/contexts/auth/presentation/controllers/listUsers/index.ts @@ -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); +}; diff --git a/apps/server/src/contexts/auth/presentation/controllers/listUsers/list-users.controller.ts b/apps/server/src/contexts/auth/presentation/controllers/listUsers/list-users.controller.ts new file mode 100644 index 00000000..fdd9881a --- /dev/null +++ b/apps/server/src/contexts/auth/presentation/controllers/listUsers/list-users.controller.ts @@ -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 { + 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); + } +} diff --git a/apps/server/src/contexts/auth/presentation/controllers/listUsers/list-users.presenter.ts b/apps/server/src/contexts/auth/presentation/controllers/listUsers/list-users.presenter.ts new file mode 100644 index 00000000..56a7d2bb --- /dev/null +++ b/apps/server/src/contexts/auth/presentation/controllers/listUsers/list-users.presenter.ts @@ -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(), + })), +}; diff --git a/apps/server/src/contexts/auth/presentation/controllers/login/index.ts b/apps/server/src/contexts/auth/presentation/controllers/login/index.ts index d51a1d0a..bb1aead2 100644 --- a/apps/server/src/contexts/auth/presentation/controllers/login/index.ts +++ b/apps/server/src/contexts/auth/presentation/controllers/login/index.ts @@ -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); +}; diff --git a/apps/server/src/contexts/auth/presentation/controllers/login/login.controller.ts b/apps/server/src/contexts/auth/presentation/controllers/login/login.controller.ts index 4e864cf1..7643b5be 100644 --- a/apps/server/src/contexts/auth/presentation/controllers/login/login.controller.ts +++ b/apps/server/src/contexts/auth/presentation/controllers/login/login.controller.ts @@ -1,50 +1,48 @@ -import { Result, UniqueID } from "@common/domain"; import { ExpressController } from "@common/presentation"; -import { createAuthService, IAuthService } from "@contexts/auth/application"; -import { EmailAddress, PlainPassword } from "@contexts/auth/domain"; -import { ILoginPresenter, LoginPresenter } from "./login.presenter"; +import { LoginUseCase } from "@contexts/auth/application"; +import { LoginData } from "@contexts/auth/domain"; +import { ILoginPresenter } from "./login.presenter"; -class LoginController extends ExpressController { - private readonly _authService!: IAuthService; - private readonly _presenter!: ILoginPresenter; - - public constructor(authService: IAuthService, presenter: ILoginPresenter) { +export class LoginController extends ExpressController { + public constructor( + private readonly login: LoginUseCase, + private readonly presenter: ILoginPresenter + ) { super(); - this._authService = authService; - this._presenter = presenter; } async executeImpl() { - const tabId = this.req.headers["x-tab-id"]; - const emailVO = EmailAddress.create(this.req.body.email); - const plainPasswordVO = PlainPassword.create(this.req.body.password); - const tabIdVO = UniqueID.create(String(tabId)); - - 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, + const loginDataOrError = LoginData.createFromPrimitives({ + email: this.req.body.email, + plainPassword: this.req.body.password, + tabId: String(this.req.headers["x-tab-id"]), }); - if (loginResultOrError.isFailure) { - return this.unauthorizedError(loginResultOrError.error.message); + if (loginDataOrError.isFailure) { + return this.clientError("Invalid input data", loginDataOrError.error); } - return this.created(this._presenter.map(loginResultOrError.data)); + const loginResultOrError = await this.login.execute(loginDataOrError.data); + + if (loginResultOrError.isFailure) { + return this.handleError(loginResultOrError.error); + } + + return this.ok(this.presenter.toDTO(loginResultOrError.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.unauthorizedError(message); } } - -export const createLoginController = () => { - const authService = createAuthService(); - return new LoginController(authService, LoginPresenter); -}; diff --git a/apps/server/src/contexts/auth/presentation/controllers/login/login.presenter.ts b/apps/server/src/contexts/auth/presentation/controllers/login/login.presenter.ts index ad7818b0..d6bd1a39 100644 --- a/apps/server/src/contexts/auth/presentation/controllers/login/login.presenter.ts +++ b/apps/server/src/contexts/auth/presentation/controllers/login/login.presenter.ts @@ -2,7 +2,7 @@ import { AuthenticatedUser, TabContext } from "@contexts/auth/domain"; import { ILoginUserResponseDTO } from "../../dto"; export interface ILoginPresenter { - map: (data: { + toDTO: (data: { user: AuthenticatedUser; tabContext: TabContext; tokens: { @@ -12,8 +12,8 @@ export interface ILoginPresenter { }) => ILoginUserResponseDTO; } -export const LoginPresenter: ILoginPresenter = { - map: (data: { +export const loginPresenter: ILoginPresenter = { + toDTO: (data: { user: AuthenticatedUser; tabContext: TabContext; tokens: { @@ -39,8 +39,8 @@ export const LoginPresenter: ILoginPresenter = { tab_id: tabContextData.tab_id, }, tokens: { - access_token: accessToken, - refresh_token: refreshToken, + access_token: accessToken.toString(), + refresh_token: refreshToken.toString(), }, }; }, diff --git a/apps/server/src/contexts/auth/presentation/controllers/logout/index.ts b/apps/server/src/contexts/auth/presentation/controllers/logout/index.ts index cf0bb10e..ad42ca9c 100644 --- a/apps/server/src/contexts/auth/presentation/controllers/logout/index.ts +++ b/apps/server/src/contexts/auth/presentation/controllers/logout/index.ts @@ -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); +}; diff --git a/apps/server/src/contexts/auth/presentation/controllers/logout/logout.controller.ts b/apps/server/src/contexts/auth/presentation/controllers/logout/logout.controller.ts index a3a3d4d5..335a6e34 100644 --- a/apps/server/src/contexts/auth/presentation/controllers/logout/logout.controller.ts +++ b/apps/server/src/contexts/auth/presentation/controllers/logout/logout.controller.ts @@ -1,42 +1,43 @@ -import { Result, UniqueID } from "@common/domain"; import { ExpressController } from "@common/presentation"; -import { createAuthService, IAuthService } from "@contexts/auth/application"; -import { EmailAddress } from "@contexts/auth/domain"; +import { LogoutUseCase } from "@contexts/auth/application/logout"; +import { LogoutData } from "@contexts/auth/domain"; -class LogoutController extends ExpressController { - private readonly _authService!: IAuthService; - - public constructor(authService: IAuthService) { +export class LogoutController extends ExpressController { + public constructor(private readonly logout: LogoutUseCase) { super(); - this._authService = authService; } async executeImpl() { - const tabId = this.req.headers["x-tab-id"]; - - const emailVO = EmailAddress.create(this.req.body.email); - 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, + const logoutDataOrError = LogoutData.createFromPrimitives({ + email: this.req.body.email, + tabId: String(this.req.headers["x-tab-id"]), }); + 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(); } -} -export const createLogoutController = () => { - const authService = createAuthService(); - return new LogoutController(authService); -}; + 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.clientError(message); + } +} diff --git a/apps/server/src/contexts/auth/presentation/controllers/refreshToken/index.ts b/apps/server/src/contexts/auth/presentation/controllers/refreshToken/index.ts index 960bcbde..886c11e8 100644 --- a/apps/server/src/contexts/auth/presentation/controllers/refreshToken/index.ts +++ b/apps/server/src/contexts/auth/presentation/controllers/refreshToken/index.ts @@ -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); +}; diff --git a/apps/server/src/contexts/auth/presentation/controllers/refreshToken/refresh-token.controller.ts b/apps/server/src/contexts/auth/presentation/controllers/refreshToken/refresh-token.controller.ts index 11223f55..ac36e874 100644 --- a/apps/server/src/contexts/auth/presentation/controllers/refreshToken/refresh-token.controller.ts +++ b/apps/server/src/contexts/auth/presentation/controllers/refreshToken/refresh-token.controller.ts @@ -1,40 +1,49 @@ import { ExpressController } from "@common/presentation"; -import { createAuthService, IAuthService } from "@contexts/auth/application"; -import { IRefreshTokenPresenter, RefreshTokenPresenter } from "./refresh-token.presenter"; +import { RefreshTokenUseCase } from "@contexts/auth/application"; +import { Token } from "@contexts/auth/domain"; +import { IRefreshTokenPresenter } from "./refresh-token.presenter"; -class RefreshTokenController extends ExpressController { - private readonly _authService!: IAuthService; - private readonly _presenter!: IRefreshTokenPresenter; - - public constructor(authService: IAuthService, presenter: IRefreshTokenPresenter) { +export class RefreshTokenController extends ExpressController { + public constructor( + private readonly refreshToken: RefreshTokenUseCase, + private readonly presenter: IRefreshTokenPresenter + ) { super(); - this._authService = authService; - this._presenter = presenter; } async executeImpl() { - const tabId = String(this.req.headers["x-tab-id"]); - const refreshToken = String(this.req.body.refresh_token); + //const tabId = String(this.req.headers["x-tab-id"]); + const refreshTokenOrError = Token.create(String(this.req.body.refresh_token)); - const result = this._authService.verifyRefreshToken(refreshToken); - if (!result || !result.email || !result.user_id || !result.tab_id || !result.roles) { - return this.clientError("Invalid input data"); + if (refreshTokenOrError.isFailure) { + return this.clientError("Invalid input data", refreshTokenOrError.error); } - const { user_id, tab_id, email, roles } = result; + const newRefreshTokenOrError = this.refreshToken.execute(refreshTokenOrError.data); - const newRefreshToken = this._authService.generateRefreshToken({ - user_id, - tab_id, - email, - roles, - }); + if (newRefreshTokenOrError.isFailure) { + return this.handleError(newRefreshTokenOrError.error); + } - return this.created(this._presenter.map({ refreshToken: newRefreshToken })); + return this.created(this.presenter.toDto(newRefreshTokenOrError.data)); + } + + 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); } } - -export const createRefreshTokenController = () => { - const authService = createAuthService(); - return new RefreshTokenController(authService, RefreshTokenPresenter); -}; diff --git a/apps/server/src/contexts/auth/presentation/controllers/refreshToken/refresh-token.presenter.ts b/apps/server/src/contexts/auth/presentation/controllers/refreshToken/refresh-token.presenter.ts index 89f40fcc..72bef456 100644 --- a/apps/server/src/contexts/auth/presentation/controllers/refreshToken/refresh-token.presenter.ts +++ b/apps/server/src/contexts/auth/presentation/controllers/refreshToken/refresh-token.presenter.ts @@ -1,15 +1,16 @@ +import { Token } from "@contexts/auth/domain"; import { IRefreshTokenResponseDTO } from "../../dto"; export interface IRefreshTokenPresenter { - map: (data: { refreshToken: string }) => IRefreshTokenResponseDTO; + toDto: (data: { refreshToken: Token }) => IRefreshTokenResponseDTO; } -export const RefreshTokenPresenter: IRefreshTokenPresenter = { - map: (data: { refreshToken: string }): IRefreshTokenResponseDTO => { +export const refreshTokenPresenter: IRefreshTokenPresenter = { + toDto: (data: { refreshToken: Token }): IRefreshTokenResponseDTO => { const { refreshToken } = data; return { - refresh_token: refreshToken, + refresh_token: refreshToken.toString(), }; }, }; diff --git a/apps/server/src/contexts/auth/presentation/controllers/register/index.ts b/apps/server/src/contexts/auth/presentation/controllers/register/index.ts index 92dc9747..c436ab0e 100644 --- a/apps/server/src/contexts/auth/presentation/controllers/register/index.ts +++ b/apps/server/src/contexts/auth/presentation/controllers/register/index.ts @@ -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); +}; diff --git a/apps/server/src/contexts/auth/presentation/controllers/register/register.controller.ts b/apps/server/src/contexts/auth/presentation/controllers/register/register.controller.ts index 72d4125d..36518067 100644 --- a/apps/server/src/contexts/auth/presentation/controllers/register/register.controller.ts +++ b/apps/server/src/contexts/auth/presentation/controllers/register/register.controller.ts @@ -1,61 +1,52 @@ import { ExpressController } from "@common/presentation"; -import { createAuthService, IAuthService } from "@contexts/auth/application"; -import { EmailAddress, HashPassword, Username } from "@contexts/auth/domain"; -import { IRegisterPresenter, RegisterPresenter } from "./register.presenter"; +import { RefreshTokenUseCase } from "@contexts/auth/application/register"; +import { RegisterData } from "@contexts/auth/domain"; +import { IRegisterPresenter } from "./register.presenter"; -class RegisterController extends ExpressController { - private readonly _authService!: IAuthService; - private readonly _presenter!: IRegisterPresenter; - - public constructor(authService: IAuthService, presenter: IRegisterPresenter) { +export class RegisterController extends ExpressController { + public constructor( + private readonly register: RefreshTokenUseCase, + private readonly presenter: IRegisterPresenter + ) { super(); - this._authService = authService; - this._presenter = presenter; } async executeImpl() { - const emailVO = EmailAddress.create(this.req.body.email); - const usernameVO = Username.create(this.req.body.username); - const hashPasswordVO = HashPassword.create(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, + const registerDataOrError = RegisterData.createFromPrimitives({ + email: this.req.body.email, + username: this.req.body.username, + plainPassword: this.req.body.password, }); - if (userOrError.isFailure) { - const message = userOrError.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); + if (registerDataOrError.isFailure) { + return this.clientError("Invalid input data", registerDataOrError.error); } - return this.created(this._presenter.map(userOrError.data)); + const userOrError = await this.register.execute(registerDataOrError.data); + + if (userOrError.isFailure) { + 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")) { + 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); } } - -export const createRegisterController = () => { - const authService = createAuthService(); - return new RegisterController(authService, RegisterPresenter); -}; diff --git a/apps/server/src/contexts/auth/presentation/controllers/register/register.presenter.ts b/apps/server/src/contexts/auth/presentation/controllers/register/register.presenter.ts index 94e1e236..b391993f 100644 --- a/apps/server/src/contexts/auth/presentation/controllers/register/register.presenter.ts +++ b/apps/server/src/contexts/auth/presentation/controllers/register/register.presenter.ts @@ -2,11 +2,11 @@ import { AuthenticatedUser } from "@contexts/auth/domain"; import { IRegisterUserResponseDTO } from "../../dto"; export interface IRegisterPresenter { - map: (user: AuthenticatedUser) => IRegisterUserResponseDTO; + toDto: (user: AuthenticatedUser) => IRegisterUserResponseDTO; } -export const RegisterPresenter: IRegisterPresenter = { - map: (user: AuthenticatedUser): IRegisterUserResponseDTO => { +export const registerPresenter: IRegisterPresenter = { + toDto: (user: AuthenticatedUser): IRegisterUserResponseDTO => { //const { user, token, refreshToken } = loginUser; //const roles = user.getRoles()?.map((rol) => rol.toString()) || []; diff --git a/apps/server/src/contexts/auth/presentation/dto/index.ts b/apps/server/src/contexts/auth/presentation/dto/index.ts index 02efe6bf..9e7426a1 100644 --- a/apps/server/src/contexts/auth/presentation/dto/index.ts +++ b/apps/server/src/contexts/auth/presentation/dto/index.ts @@ -1,3 +1,7 @@ export * from "./auth.request.dto"; export * from "./auth.response.dto"; export * from "./auth.validation.dto"; + +export * from "./user.request.dto"; +export * from "./user.response.dto"; +export * from "./user.validation.dto"; diff --git a/apps/server/src/contexts/auth/presentation/dto/user.request.dto.ts b/apps/server/src/contexts/auth/presentation/dto/user.request.dto.ts new file mode 100644 index 00000000..bcb6ea3c --- /dev/null +++ b/apps/server/src/contexts/auth/presentation/dto/user.request.dto.ts @@ -0,0 +1 @@ +export interface IListUsersRequestDTO {} diff --git a/apps/server/src/contexts/auth/presentation/dto/user.response.dto.ts b/apps/server/src/contexts/auth/presentation/dto/user.response.dto.ts new file mode 100644 index 00000000..1bc570f2 --- /dev/null +++ b/apps/server/src/contexts/auth/presentation/dto/user.response.dto.ts @@ -0,0 +1,4 @@ +export interface IListUsersResponseDTO { + id: string; + email: string; +} diff --git a/apps/server/src/contexts/auth/presentation/dto/user.validation.dto.ts b/apps/server/src/contexts/auth/presentation/dto/user.validation.dto.ts new file mode 100644 index 00000000..2ddd2864 --- /dev/null +++ b/apps/server/src/contexts/auth/presentation/dto/user.validation.dto.ts @@ -0,0 +1,3 @@ +import { z } from "zod"; + +export const ListUsersSchema = z.object({}); diff --git a/apps/server/src/contexts/auth/presentation/index.ts b/apps/server/src/contexts/auth/presentation/index.ts index 18ced4b0..a123289d 100644 --- a/apps/server/src/contexts/auth/presentation/index.ts +++ b/apps/server/src/contexts/auth/presentation/index.ts @@ -1,3 +1,2 @@ export * from "./controllers"; export * from "./dto"; -export * from "./middleware"; diff --git a/apps/server/src/index.ts b/apps/server/src/index.ts index 0aea5854..f086e5c1 100644 --- a/apps/server/src/index.ts +++ b/apps/server/src/index.ts @@ -43,7 +43,8 @@ const serverError = (error: NodeJS.ErrnoException) => { logger.info(`⛔️ Server wasn't able to start properly.`); 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 { logger.error(error); } diff --git a/apps/server/src/routes/auth.routes.ts b/apps/server/src/routes/auth.routes.ts index 8c7a5b08..0609a89f 100644 --- a/apps/server/src/routes/auth.routes.ts +++ b/apps/server/src/routes/auth.routes.ts @@ -1,12 +1,10 @@ import { validateRequestDTO } from "@common/presentation"; -import { createAuthProvider } from "@contexts/auth/infraestructure"; -import { validateTabContextHeader } from "@contexts/auth/presentation"; +import { checkUser, validateTabContextHeader } from "@contexts/auth/infraestructure"; import { - createLoginController, - createRefreshTokenController, + loginController, + logoutController, + registerController, } 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 { LoginUserSchema, RefreshTokenSchema, @@ -16,8 +14,6 @@ import { NextFunction, Request, Response, Router } from "express"; export const authRouter = (appRouter: Router) => { const authRoutes: Router = Router({ mergeParams: true }); - const authProvider = createAuthProvider(); - /** * @api {post} /api/auth/register Register a new user * @apiName RegisterUser @@ -33,7 +29,7 @@ export const authRouter = (appRouter: Router) => { * @apiError (400) {String} message Error message. */ 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), validateTabContextHeader, (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( "/logout", validateTabContextHeader, - authProvider.authenticateJWT(), + checkUser, (req: Request, res: Response, next: NextFunction) => { - createLogoutController().execute(req, res, next); + logoutController().execute(req, res, next); } ); authRoutes.post( "/refresh", validateRequestDTO(RefreshTokenSchema), - //validateTabContextHeader, - //authProvider.authenticateJWT(), (req: Request, res: Response, next: NextFunction) => { - createRefreshTokenController().execute(req, res, next); + refreshTokenController().execute(req, res, next); } ); diff --git a/apps/server/src/routes/user.routes.ts b/apps/server/src/routes/user.routes.ts new file mode 100644 index 00000000..c52be9de --- /dev/null +++ b/apps/server/src/routes/user.routes.ts @@ -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); +}; diff --git a/apps/server/src/routes/v1.routes.ts b/apps/server/src/routes/v1.routes.ts index beb49c00..a61e33db 100644 --- a/apps/server/src/routes/v1.routes.ts +++ b/apps/server/src/routes/v1.routes.ts @@ -1,5 +1,6 @@ import { Router } from "express"; import { authRouter } from "./auth.routes"; +import { userRouter } from "./user.routes"; export const v1Routes = () => { const routes = Router({ mergeParams: true }); @@ -9,6 +10,7 @@ export const v1Routes = () => { }); authRouter(routes); + userRouter(routes); return routes; }; diff --git a/packages/rdx-criteria/.eslintrc.json b/packages/rdx-criteria/.eslintrc.json new file mode 100644 index 00000000..f4e1b0c2 --- /dev/null +++ b/packages/rdx-criteria/.eslintrc.json @@ -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"] + } + } + ] +} diff --git a/packages/rdx-criteria/.gitignore b/packages/rdx-criteria/.gitignore new file mode 100644 index 00000000..76add878 --- /dev/null +++ b/packages/rdx-criteria/.gitignore @@ -0,0 +1,2 @@ +node_modules +dist \ No newline at end of file diff --git a/packages/rdx-criteria/package.json b/packages/rdx-criteria/package.json new file mode 100644 index 00000000..fe7e0aed --- /dev/null +++ b/packages/rdx-criteria/package.json @@ -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" + } +} diff --git a/packages/rdx-criteria/packages/criteria/package.json b/packages/rdx-criteria/packages/criteria/package.json new file mode 100644 index 00000000..069d8ddb --- /dev/null +++ b/packages/rdx-criteria/packages/criteria/package.json @@ -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" + } +} diff --git a/packages/rdx-criteria/packages/criteria/src/criteria.ts b/packages/rdx-criteria/packages/criteria/src/criteria.ts new file mode 100644 index 00000000..e69de29b diff --git a/packages/rdx-criteria/packages/criteria/src/index.ts b/packages/rdx-criteria/packages/criteria/src/index.ts new file mode 100644 index 00000000..e69de29b diff --git a/packages/rdx-criteria/packages/criteria/src/pagination.ts/index.ts b/packages/rdx-criteria/packages/criteria/src/pagination.ts/index.ts new file mode 100644 index 00000000..85731be1 --- /dev/null +++ b/packages/rdx-criteria/packages/criteria/src/pagination.ts/index.ts @@ -0,0 +1,5 @@ +export interface ICriteriaProps {} + +export interface ICriteria {} + +class Criteria implements ICriteria {} diff --git a/packages/rdx-criteria/packages/criteria/src/pagination.ts/pagination-defaults.ts b/packages/rdx-criteria/packages/criteria/src/pagination.ts/pagination-defaults.ts new file mode 100644 index 00000000..b632747a --- /dev/null +++ b/packages/rdx-criteria/packages/criteria/src/pagination.ts/pagination-defaults.ts @@ -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; diff --git a/packages/rdx-criteria/packages/tsconfig.json b/packages/rdx-criteria/packages/tsconfig.json new file mode 100644 index 00000000..e0d66374 --- /dev/null +++ b/packages/rdx-criteria/packages/tsconfig.json @@ -0,0 +1,7 @@ +{ + "extends": "../../tsconfig.json", + "compilerOptions": { + "outDir": "dist" + }, + "include": ["src/**/*"] +} diff --git a/packages/rdx-criteria/tsconfig.json b/packages/rdx-criteria/tsconfig.json new file mode 100644 index 00000000..5f65a300 --- /dev/null +++ b/packages/rdx-criteria/tsconfig.json @@ -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" + } +}