diff --git a/apps/server/.571b27c4f3dcc376d1f0ca8880ce87cfefd2f30d-audit.json b/apps/server/.571b27c4f3dcc376d1f0ca8880ce87cfefd2f30d-audit.json index 9857aebf..717cef4d 100644 --- a/apps/server/.571b27c4f3dcc376d1f0ca8880ce87cfefd2f30d-audit.json +++ b/apps/server/.571b27c4f3dcc376d1f0ca8880ce87cfefd2f30d-audit.json @@ -9,6 +9,11 @@ "date": 1738578708264, "name": "debug-2025-02-03.log", "hash": "48ca17f819e391cb5ae1909a6ee0fa4d9c8cdb6667ef893547acb004f4a79737" + }, + { + "date": 1738664864746, + "name": "debug-2025-02-04.log", + "hash": "7f1ecce0e9a97fbb99865ac9bfc3591897975a2fd9562164c9be52aabc47f47f" } ], "hashType": "sha256" diff --git a/apps/server/.e6616b1c93d5e50d48b909cd34375b545b447bc6-audit.json b/apps/server/.e6616b1c93d5e50d48b909cd34375b545b447bc6-audit.json index bf5e8ba3..05fdfdfd 100644 --- a/apps/server/.e6616b1c93d5e50d48b909cd34375b545b447bc6-audit.json +++ b/apps/server/.e6616b1c93d5e50d48b909cd34375b545b447bc6-audit.json @@ -9,6 +9,11 @@ "date": 1738578708262, "name": "error-2025-02-03.log", "hash": "a21f154f5c386a75eee98a35c2b100da7df1b8002cf99851b90bd12810f1fe8a" + }, + { + "date": 1738664864743, + "name": "error-2025-02-04.log", + "hash": "dfb19c1e5b9c2039572425939e77f4d4ab3285df0fcded1edfba3e7c4cc2a94d" } ], "hashType": "sha256" diff --git a/apps/server/src/common/infrastructure/sequelize/sequelize-repository.ts b/apps/server/src/common/infrastructure/sequelize/sequelize-repository.ts index 8f87b1a0..483eb9ef 100644 --- a/apps/server/src/common/infrastructure/sequelize/sequelize-repository.ts +++ b/apps/server/src/common/infrastructure/sequelize/sequelize-repository.ts @@ -15,6 +15,36 @@ export abstract class SequelizeRepository implements IAggregateRootRepository }); } + protected async _getBy( + model: ModelDefined, + field: string, + value: any, + params: any = {}, + transaction?: Transaction + ): Promise { + const where: { [key: string]: any } = {}; + + where[field] = value; + + return model.findOne({ + where, + transaction, + ...params, + }); + } + + protected async _getById( + model: ModelDefined, + id: UniqueID | string, + params: any = {}, + transaction?: Transaction + ): Promise { + return model.findByPk(id.toString(), { + transaction, + ...params, + }); + } + protected async _exists( model: ModelDefined, field: string, diff --git a/apps/server/src/common/presentation/express/express-controller.ts b/apps/server/src/common/presentation/express/express-controller.ts index 6426b4ca..1780df19 100644 --- a/apps/server/src/common/presentation/express/express-controller.ts +++ b/apps/server/src/common/presentation/express/express-controller.ts @@ -51,8 +51,9 @@ export abstract class ExpressController { return ExpressController.errorResponse( new ApiError({ status: 401, - title: "Unauthorized", - detail: message ?? "You are not authorized to access this resource.", + title: httpStatus["401"], + name: httpStatus["401_NAME"], + detail: message ?? httpStatus["401_MESSAGE"], }), this.res ); 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 new file mode 100644 index 00000000..77683367 --- /dev/null +++ b/apps/server/src/contexts/auth/application/tab-context-service.interface.ts @@ -0,0 +1,9 @@ +import { Result, UniqueID } from "@common/domain"; +import { TabContext } from "../domain"; + +export interface ITabContextService { + getByTabId(tabId: UniqueID): Promise>; + createContext(tabId: UniqueID, userId: UniqueID): Promise>; + assignCompany(tabId: UniqueID, companyId: UniqueID): Promise>; + removeContext(tabId: 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 new file mode 100644 index 00000000..b85c56b0 --- /dev/null +++ b/apps/server/src/contexts/auth/application/tab-context.service.ts @@ -0,0 +1,108 @@ +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 getByTabId(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(tabId: UniqueID, userId: UniqueID): Promise> { + 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.createContext(contextOrError.data, transaction); + + return Result.ok(contextOrError.data); + }); + } catch (error: unknown) { + return Result.fail(error as Error); + } + } + + /** + * Asigna una empresa activa a un contexto de pestaña + */ + async assignCompany(tabId: UniqueID, companyId: UniqueID): Promise> { + if (!companyId || !tabId) { + return Result.fail(new Error("Tab ID and Company ID are required")); + } + + try { + return await this._transactionManager.complete(async (transaction) => { + // Verificar si la pestaña existe + const tabContextOrError = await this._respository.contextExists(tabId, transaction); + if (tabContextOrError.isFailure || !tabContextOrError.data) { + return Result.fail(new Error("Invalid or expired Tab ID")); + } + + return await this._respository.updateCompanyByTabId(tabId, companyId, transaction); + }); + } catch (error: unknown) { + return Result.fail(error as Error); + } + } + + /** + * Elimina un contexto de pestaña por su ID + */ + async removeContext(tabId: UniqueID): Promise> { + if (!tabId) { + return Result.fail(new Error("Tab ID is required")); + } + try { + return await this._transactionManager.complete(async (transaction) => { + return await this._respository.deleteContextByTabId(tabId, transaction); + }); + } catch (error: unknown) { + return Result.fail(error as Error); + } + } +} diff --git a/apps/server/src/contexts/auth/domain/entities/index.ts b/apps/server/src/contexts/auth/domain/entities/index.ts new file mode 100644 index 00000000..925aef3b --- /dev/null +++ b/apps/server/src/contexts/auth/domain/entities/index.ts @@ -0,0 +1 @@ +export * from "./tab-context"; diff --git a/apps/server/src/contexts/auth/domain/entities/tab-context.ts b/apps/server/src/contexts/auth/domain/entities/tab-context.ts new file mode 100644 index 00000000..773217c1 --- /dev/null +++ b/apps/server/src/contexts/auth/domain/entities/tab-context.ts @@ -0,0 +1,81 @@ +import { DomainEntity, Result, UniqueID } from "@common/domain"; + +export interface ITabContextProps { + tabId: UniqueID; + userId: UniqueID; + companyId?: UniqueID; + branchId?: UniqueID; +} + +export interface ITabContext { + tabId: UniqueID; + userId: UniqueID; + companyId?: UniqueID; + branchId?: UniqueID; + + assignCompany(companyId: UniqueID): void; + assignBranch(branchId: UniqueID): void; + + toPersistenceData(): any; +} + +export class TabContext extends DomainEntity implements ITabContext { + private _companyId: UniqueID | undefined; + private _branchId: UniqueID | undefined; + + static create(props: ITabContextProps, id?: UniqueID): Result { + return Result.ok(new this(props, id)); + } + + protected constructor(props: ITabContextProps, id?: UniqueID) { + const { tabId, userId, companyId, branchId } = props; + + super({ tabId, userId }, id); + this._companyId = companyId; + this._branchId = branchId; + } + + get tabId(): UniqueID { + return this._props.tabId; + } + + get userId(): UniqueID { + return this._props.userId; + } + + get companyId(): UniqueID | undefined { + return this._companyId; + } + + get branchId(): UniqueID | undefined { + return this._branchId; + } + + hasCompanyAssigned(): boolean { + return this._companyId !== undefined; + } + + assignCompany(companyId: UniqueID): void { + this._companyId = companyId; + } + + hasBranchAssigned(): boolean { + return this._branchId !== undefined; + } + + assignBranch(branchId: UniqueID): void { + this._branchId = branchId; + } + + /** + * 🔹 Devuelve una representación lista para persistencia + */ + toPersistenceData(): any { + return { + id: this._id.toString(), + user_id: this.userId.toString(), + company_id: this._companyId ? this._companyId.toString() : undefined, + branchId: this._branchId ? this._branchId.toString() : undefined, + }; + } +} diff --git a/apps/server/src/contexts/auth/domain/index.ts b/apps/server/src/contexts/auth/domain/index.ts index 84f4dfe9..4a16e729 100644 --- a/apps/server/src/contexts/auth/domain/index.ts +++ b/apps/server/src/contexts/auth/domain/index.ts @@ -1,4 +1,5 @@ -export * from "./aggregates/authenticated-user"; -export * from "./events/user-authenticated.event"; +export * from "./aggregates"; +export * from "./entities"; +export * from "./events"; export * from "./repositories"; export * from "./value-objects"; diff --git a/apps/server/src/contexts/auth/domain/repositories/authenticated-user-repository.interface.ts b/apps/server/src/contexts/auth/domain/repositories/authenticated-user-repository.interface.ts index af2eb1cb..56be0885 100644 --- a/apps/server/src/contexts/auth/domain/repositories/authenticated-user-repository.interface.ts +++ b/apps/server/src/contexts/auth/domain/repositories/authenticated-user-repository.interface.ts @@ -6,7 +6,7 @@ export interface IAuthenticatedUserRepository { findUserByEmail( email: EmailAddress, transaction?: any - ): Promise>; - + ): Promise>; + userExists(email: EmailAddress, transaction?: any): Promise>; createUser(user: AuthenticatedUser, transaction?: any): Promise>; } diff --git a/apps/server/src/contexts/auth/domain/repositories/tab-context-repository.interface.ts b/apps/server/src/contexts/auth/domain/repositories/tab-context-repository.interface.ts new file mode 100644 index 00000000..79b9e225 --- /dev/null +++ b/apps/server/src/contexts/auth/domain/repositories/tab-context-repository.interface.ts @@ -0,0 +1,15 @@ +import { Result, UniqueID } from "@common/domain"; +import { Transaction } from "sequelize"; +import { TabContext } from "../entities"; + +export interface ITabContextRepository { + getContextByTabId(tabId: UniqueID, transaction?: any): Promise>; + createContext(context: TabContext, transaction?: Transaction): Promise>; + contextExists(tabId: UniqueID, transaction?: any): Promise>; + updateCompanyByTabId( + tabId: UniqueID, + companyId: UniqueID, + transaction?: Transaction + ): Promise>; + deleteContextByTabId(tabId: UniqueID, transaction?: any): Promise>; +} 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 index 0bbc51f4..b7157135 100644 --- 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 @@ -2,13 +2,6 @@ import { Result } from "@common/domain"; import { AuthenticatedUser } from "@contexts/auth/domain"; export interface IAuthenticatedUserMapper { - /** - * 🔹 Convierte una entidad de la base de datos en un agregado de dominio `AuthenticatedUser` - */ toDomain(entity: any): Result; - - /** - * 🔹 Convierte un agregado `AuthenticatedUser` en un objeto listo para persistencia - */ toPersistence(aggregate: AuthenticatedUser): any; } 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 53d29d21..e8367466 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 @@ -16,7 +16,7 @@ export class AuthenticatedUserMapper implements IAuthenticatedUserMapper { const usernameResult = Username.create(entity.username); const passwordHashResult = PasswordHash.create(entity.passwordHash); const emailResult = EmailAddress.create(entity.email); - 1; + // Validar que no haya errores en la creación de los Value Objects const okOrError = Result.combine([ uniqueIdResult, @@ -35,7 +35,6 @@ export class AuthenticatedUserMapper implements IAuthenticatedUserMapper { email: emailResult.data!, passwordHash: passwordHashResult.data!, roles: entity.roles || [], - token: entity.token, }, uniqueIdResult.data! ); diff --git a/apps/server/src/contexts/auth/infraestructure/mappers/index.ts b/apps/server/src/contexts/auth/infraestructure/mappers/index.ts index 469a1b91..4f3fcdc7 100644 --- a/apps/server/src/contexts/auth/infraestructure/mappers/index.ts +++ b/apps/server/src/contexts/auth/infraestructure/mappers/index.ts @@ -1,2 +1,4 @@ export * from "./authenticated-user-mapper.interface"; export * from "./authenticated-user.mapper"; +export * from "./tab-context-mapper.interface"; +export * from "./tab-context.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 new file mode 100644 index 00000000..0fa965a6 --- /dev/null +++ b/apps/server/src/contexts/auth/infraestructure/mappers/tab-context-mapper.interface.ts @@ -0,0 +1,7 @@ +import { Result } from "@common/domain"; +import { TabContext } from "@contexts/auth/domain"; + +export interface ITabContextMapper { + toDomain(entity: any): Result; + toPersistence(aggregate: TabContext): any; +} 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 new file mode 100644 index 00000000..680889b2 --- /dev/null +++ b/apps/server/src/contexts/auth/infraestructure/mappers/tab-context.mapper.ts @@ -0,0 +1,46 @@ +import { Result, UniqueID } from "@common/domain"; +import { EmailAddress, PasswordHash, TabContext, Username } from "@contexts/auth/domain"; +import { ITabContextMapper } from "./tab-context-mapper.interface"; + +export class TabContextMapper implements ITabContextMapper { + toDomain(entity: any): 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 userIdResult = Username.create(entity.user_id); + const companyIdResult = PasswordHash.create(entity.passwordHash); + const brachIdResult = EmailAddress.create(entity.email); + + // Validar que no haya errores en la creación de los Value Objects + const okOrError = Result.combine([ + uniqueIdResult, + usernameResult, + companyIdResult, + emailResult, + ]); + if (okOrError.isFailure) { + return Result.fail(okOrError.error.message); + } + + // Crear el agregado de dominio + return TabContext.create( + { + username: usernameResult.data!, + email: emailResult.data!, + passwordHash: companyIdResult.data!, + roles: entity.roles || [], + token: entity.token, + }, + uniqueIdResult.data! + ); + } + + toPersistence(tabContext: TabContext): any { + return tabContext.toPersistenceData(); + } +} + +export const createTabContextMapper = (): ITabContextMapper => new TabContextMapper(); diff --git a/apps/server/src/contexts/auth/infraestructure/sequelize/auth-user.model.ts b/apps/server/src/contexts/auth/infraestructure/sequelize/auth-user.model.ts index ddbf651e..78388929 100644 --- a/apps/server/src/contexts/auth/infraestructure/sequelize/auth-user.model.ts +++ b/apps/server/src/contexts/auth/infraestructure/sequelize/auth-user.model.ts @@ -1,6 +1,19 @@ -import { DataTypes, InferAttributes, InferCreationAttributes, Model, Sequelize } from "sequelize"; +import { + DataTypes, + InferAttributes, + InferCreationAttributes, + Model, + NonAttribute, + Sequelize, +} from "sequelize"; +import { TabContextCreationAttributes, TabContextModel } from "./tab-context.model"; -export type AuthUserCreationAttributes = InferCreationAttributes; +export type AuthUserCreationAttributes = InferCreationAttributes< + AuthUserModel, + { omit: "contexts" } +> & { + contexts: TabContextCreationAttributes[]; +}; export class AuthUserModel extends Model< InferAttributes, @@ -11,13 +24,23 @@ export class AuthUserModel extends Model< return Promise.resolve(); }*/ - static associate(connection: Sequelize) {} + static associate(connection: Sequelize) { + const { TabContextModel } = connection.models; + + AuthUserModel.hasMany(TabContextModel, { + as: "contexts", + foreignKey: "user_id", + onDelete: "CASCADE", + }); + } declare id: string; declare username: string; declare email: string; declare password: string; declare roles: string[]; declare isActive: boolean; + + declare contexts: NonAttribute; } export default (sequelize: Sequelize) => { @@ -68,6 +91,12 @@ export default (sequelize: Sequelize) => { deletedAt: "deleted_at", indexes: [{ name: "email_idx", fields: ["email"], unique: true }], + + whereMergeStrategy: "and", // <- cómo tratar el merge de un scope + + defaultScope: {}, + + scopes: {}, } ); return AuthUserModel; 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 d5396e50..5a60b960 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 @@ -52,7 +52,7 @@ export class AuthenticatedUserRepository async findUserByEmail( email: EmailAddress, transaction?: Transaction - ): Promise> { + ): Promise> { try { const rawUser: any = await this._findById( AuthUserModel, @@ -62,7 +62,7 @@ export class AuthenticatedUserRepository ); if (!rawUser === true) { - return Result.ok(null); + return Result.fail(new Error("User with email not exists")); } return this._mapper.toDomain(rawUser); diff --git a/apps/server/src/contexts/auth/infraestructure/sequelize/index.ts b/apps/server/src/contexts/auth/infraestructure/sequelize/index.ts index ef5048db..81bc3e8f 100644 --- a/apps/server/src/contexts/auth/infraestructure/sequelize/index.ts +++ b/apps/server/src/contexts/auth/infraestructure/sequelize/index.ts @@ -1,2 +1,4 @@ export * from "./auth-user.model"; export * from "./authenticated-user.repository"; +export * from "./tab-context.model"; +export * from "./tab-context.repository"; diff --git a/apps/server/src/contexts/auth/infraestructure/sequelize/tab-context.model.ts b/apps/server/src/contexts/auth/infraestructure/sequelize/tab-context.model.ts new file mode 100644 index 00000000..d741af7c --- /dev/null +++ b/apps/server/src/contexts/auth/infraestructure/sequelize/tab-context.model.ts @@ -0,0 +1,87 @@ +import { + DataTypes, + InferAttributes, + InferCreationAttributes, + Model, + NonAttribute, + Sequelize, +} from "sequelize"; +import { AuthUserModel } from "./auth-user.model"; + +export type TabContextCreationAttributes = InferCreationAttributes< + TabContextModel, + { omit: "user" } +>; + +export class TabContextModel extends Model< + InferAttributes, + InferCreationAttributes +> { + // To avoid table creation + /*static async sync(): Promise { + return Promise.resolve(); + }*/ + static associate(connection: Sequelize) { + const { AuthUserModel } = connection.models; + + TabContextModel.belongsTo(AuthUserModel, { + as: "user", + foreignKey: "user_id", + onDelete: "CASCADE", + }); + } + + declare id: string; + declare tab_id: string; + declare user_id: string; + declare company_id: string; + declare branch_id: string; + + declare user: NonAttribute; +} + +export default (sequelize: Sequelize) => { + TabContextModel.init( + { + id: { + type: DataTypes.UUID, + primaryKey: true, + }, + user_id: { + type: DataTypes.UUID, + allowNull: false, + }, + tab_id: { + type: DataTypes.UUID, + allowNull: false, + }, + company_id: { + type: DataTypes.UUID, + allowNull: false, + }, + branch_id: { + type: DataTypes.UUID, + allowNull: false, + }, + }, + { + sequelize, + tableName: "user_tab_contexts", + paranoid: true, // softs deletes + timestamps: true, + + createdAt: "created_at", + updatedAt: "updated_at", + deletedAt: "deleted_at", + + indexes: [{ name: "tab_id_idx", fields: ["tab_id"], unique: true }], + + whereMergeStrategy: "and", // <- cómo tratar el merge de un scope + + defaultScope: {}, + + scopes: {}, + } + ); + return TabContextModel; +}; 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 new file mode 100644 index 00000000..616975a9 --- /dev/null +++ b/apps/server/src/contexts/auth/infraestructure/sequelize/tab-context.repository.ts @@ -0,0 +1,128 @@ +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 { Transaction } from "sequelize"; +import { ITabContextMapper } from "../mappers"; +import { TabContextModel } from "./tab-context.model"; + +export class TabContextRepository + extends SequelizeRepository + implements ITabContextRepository +{ + private readonly _mapper!: ITabContextMapper; + + /** + * 🔹 Función personalizada para mapear errores de unicidad en autenticación + */ + private _customErrorMapper(error: Error): string | null { + if (error.name === "SequelizeUniqueConstraintError") { + return "Tab context already exists"; + } + + return null; + } + + constructor(mapper: ITabContextMapper) { + super(); + this._mapper = mapper; + } + + async getContextByTabId( + tabId: UniqueID, + transaction?: Transaction + ): Promise> { + try { + const rawContext = await this._getBy( + TabContextModel, + "tab_id", + tabId.toString(), + {}, + transaction + ); + + if (!rawContext === true) { + return Result.fail(new Error("Tab context not exists")); + } + + return this._mapper.toDomain(rawContext); + } catch (error: any) { + return this._handleDatabaseError(error, this._customErrorMapper); + } + } + + async contextExists(tabId: UniqueID, transaction?: any): Promise> { + try { + const result: any = await this._exists( + TabContextModel, + "tab_id", + tabId.toString(), + transaction + ); + + return Result.ok(Boolean(result)); + } catch (error: any) { + return this._handleDatabaseError(error, this._customErrorMapper); + } + } + + async createContext( + context: TabContext, + transaction?: Transaction + ): Promise> { + try { + const { id } = context; + const persistenceData = this._mapper.toPersistence(context); + await TabContextModel.create( + { + ...persistenceData, + id: id.toString(), + }, + { + include: [{ all: true }], + transaction, + } + ); + return Result.ok(); + } catch (error: any) { + return this._handleDatabaseError(error, this._customErrorMapper); + } + } + + async updateCompanyByTabId( + tabId: UniqueID, + companyId: UniqueID, + transaction?: Transaction + ): Promise> { + try { + await TabContextModel.update( + { company_id: companyId.toString() }, + { + where: { + tab_id: tabId.toString(), + }, + transaction, + } + ); + + return Result.ok(); + } catch (error: any) { + return this._handleDatabaseError(error, this._customErrorMapper); + } + } + + async deleteContextByTabId(tabId: UniqueID, transaction?: any): Promise> { + try { + await TabContextModel.destroy({ + where: { + tab_id: tabId.toString(), + }, + transaction, + force: false, + }); + return Result.ok(); + } catch (error: any) { + return this._handleDatabaseError(error, this._customErrorMapper); + } + } +} diff --git a/apps/server/src/contexts/auth/presentation/dto/auth.response.dto.ts b/apps/server/src/contexts/auth/presentation/dto/auth.response.dto.ts index ac5e95d2..311ad778 100644 --- a/apps/server/src/contexts/auth/presentation/dto/auth.response.dto.ts +++ b/apps/server/src/contexts/auth/presentation/dto/auth.response.dto.ts @@ -7,7 +7,12 @@ export interface IRegisterUserResponseDTO { export interface ILoginUserResponseDTO { access_token: string; refresh_token: string; - user_id: string; + user: { + id: string; + username: string; + email: string; + }; + tab_id: string; } export interface ILogoutResponseDTO { diff --git a/apps/server/src/contexts/auth/presentation/middleware/index.ts b/apps/server/src/contexts/auth/presentation/middleware/index.ts index 0ea5a7ac..5ce1bf32 100644 --- a/apps/server/src/contexts/auth/presentation/middleware/index.ts +++ b/apps/server/src/contexts/auth/presentation/middleware/index.ts @@ -1 +1,2 @@ export * from "./passport-auth.middleware"; +export * from "./tab-context.middleware"; diff --git a/apps/server/src/contexts/auth/presentation/middleware/tab-context.middleware.ts b/apps/server/src/contexts/auth/presentation/middleware/tab-context.middleware.ts new file mode 100644 index 00000000..1e25ca05 --- /dev/null +++ b/apps/server/src/contexts/auth/presentation/middleware/tab-context.middleware.ts @@ -0,0 +1,52 @@ +import { ApiError, ExpressController } from "@common/presentation"; +import { NextFunction, Request, Response } from "express"; +import httpStatus from "http-status"; + +export const validateTabHeader = (req: Request, res: Response, next: NextFunction) => { + const tabId = req.headers["x-tab-id"]; + if (!tabId) { + return ExpressController.errorResponse( + new ApiError({ + status: 401, + title: httpStatus["401"], + name: httpStatus["401_NAME"], + detail: "Tab ID is required", + }), + res + ); + } + next(); +}; + +export const validateTabContext = async (req: Request, res: Response, next: NextFunction) => { + const tabId = req.headers["x-tab-id"]; + if (!tabId) { + return ExpressController.errorResponse( + new ApiError({ + status: 401, + title: httpStatus["401"], + name: httpStatus["401_NAME"], + detail: "Tab ID is required", + }), + res + ); + } + + const contextOrError = await TabContextRepository.getByTabId(tabId); + if (contextOrError.isFailure) { + return ExpressController.errorResponse( + new ApiError({ + status: 401, + title: httpStatus["401"], + name: httpStatus["401_NAME"], + detail: "Invalid or expired Tab ID", + }), + res + ); + } + + const context = contextOrError.data; + + req.user = { id: context.user_id, company_id: context.company_id }; + next(); +}; diff --git a/apps/server/src/routes/auth.routes.ts b/apps/server/src/routes/auth.routes.ts index dc92bfcf..9c06e26b 100644 --- a/apps/server/src/routes/auth.routes.ts +++ b/apps/server/src/routes/auth.routes.ts @@ -1,4 +1,5 @@ import { validateRequest } from "@common/presentation"; +import { validateTabHeader } from "@contexts/auth/presentation"; import { createLoginController, createRegisterController, @@ -41,9 +42,14 @@ export const authRouter = (appRouter: Router) => { * * @apiError (401) {String} message Invalid email or password. */ - authRoutes.post("/login", validateRequest(LoginUserSchema), (req, res, next) => { - createLoginController().execute(req, res, next); - }); + authRoutes.post( + "/login", + validateRequest(LoginUserSchema), + validateTabHeader, + (req, res, next) => { + createLoginController().execute(req, res, next); + } + ); /** * @api {post} /api/auth/select-company Select an active company