.
This commit is contained in:
parent
ad12c3a67a
commit
a8c72b1d64
@ -9,6 +9,11 @@
|
|||||||
"date": 1738578708264,
|
"date": 1738578708264,
|
||||||
"name": "debug-2025-02-03.log",
|
"name": "debug-2025-02-03.log",
|
||||||
"hash": "48ca17f819e391cb5ae1909a6ee0fa4d9c8cdb6667ef893547acb004f4a79737"
|
"hash": "48ca17f819e391cb5ae1909a6ee0fa4d9c8cdb6667ef893547acb004f4a79737"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"date": 1738664864746,
|
||||||
|
"name": "debug-2025-02-04.log",
|
||||||
|
"hash": "7f1ecce0e9a97fbb99865ac9bfc3591897975a2fd9562164c9be52aabc47f47f"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"hashType": "sha256"
|
"hashType": "sha256"
|
||||||
|
|||||||
@ -9,6 +9,11 @@
|
|||||||
"date": 1738578708262,
|
"date": 1738578708262,
|
||||||
"name": "error-2025-02-03.log",
|
"name": "error-2025-02-03.log",
|
||||||
"hash": "a21f154f5c386a75eee98a35c2b100da7df1b8002cf99851b90bd12810f1fe8a"
|
"hash": "a21f154f5c386a75eee98a35c2b100da7df1b8002cf99851b90bd12810f1fe8a"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"date": 1738664864743,
|
||||||
|
"name": "error-2025-02-04.log",
|
||||||
|
"hash": "dfb19c1e5b9c2039572425939e77f4d4ab3285df0fcded1edfba3e7c4cc2a94d"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"hashType": "sha256"
|
"hashType": "sha256"
|
||||||
|
|||||||
@ -15,6 +15,36 @@ export abstract class SequelizeRepository<T> implements IAggregateRootRepository
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected async _getBy(
|
||||||
|
model: ModelDefined<any, any>,
|
||||||
|
field: string,
|
||||||
|
value: any,
|
||||||
|
params: any = {},
|
||||||
|
transaction?: Transaction
|
||||||
|
): Promise<any> {
|
||||||
|
const where: { [key: string]: any } = {};
|
||||||
|
|
||||||
|
where[field] = value;
|
||||||
|
|
||||||
|
return model.findOne({
|
||||||
|
where,
|
||||||
|
transaction,
|
||||||
|
...params,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
protected async _getById(
|
||||||
|
model: ModelDefined<any, any>,
|
||||||
|
id: UniqueID | string,
|
||||||
|
params: any = {},
|
||||||
|
transaction?: Transaction
|
||||||
|
): Promise<any> {
|
||||||
|
return model.findByPk(id.toString(), {
|
||||||
|
transaction,
|
||||||
|
...params,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
protected async _exists(
|
protected async _exists(
|
||||||
model: ModelDefined<any, any>,
|
model: ModelDefined<any, any>,
|
||||||
field: string,
|
field: string,
|
||||||
|
|||||||
@ -51,8 +51,9 @@ export abstract class ExpressController {
|
|||||||
return ExpressController.errorResponse(
|
return ExpressController.errorResponse(
|
||||||
new ApiError({
|
new ApiError({
|
||||||
status: 401,
|
status: 401,
|
||||||
title: "Unauthorized",
|
title: httpStatus["401"],
|
||||||
detail: message ?? "You are not authorized to access this resource.",
|
name: httpStatus["401_NAME"],
|
||||||
|
detail: message ?? httpStatus["401_MESSAGE"],
|
||||||
}),
|
}),
|
||||||
this.res
|
this.res
|
||||||
);
|
);
|
||||||
|
|||||||
@ -0,0 +1,9 @@
|
|||||||
|
import { Result, UniqueID } from "@common/domain";
|
||||||
|
import { TabContext } from "../domain";
|
||||||
|
|
||||||
|
export interface ITabContextService {
|
||||||
|
getByTabId(tabId: UniqueID): Promise<Result<TabContext, Error>>;
|
||||||
|
createContext(tabId: UniqueID, userId: UniqueID): Promise<Result<TabContext, Error>>;
|
||||||
|
assignCompany(tabId: UniqueID, companyId: UniqueID): Promise<Result<void, Error>>;
|
||||||
|
removeContext(tabId: UniqueID): Promise<Result<void, Error>>;
|
||||||
|
}
|
||||||
108
apps/server/src/contexts/auth/application/tab-context.service.ts
Normal file
108
apps/server/src/contexts/auth/application/tab-context.service.ts
Normal file
@ -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<Result<TabContext, Error>> {
|
||||||
|
try {
|
||||||
|
return await this._transactionManager.complete(async (transaction) => {
|
||||||
|
// Verificar si la pestaña existe
|
||||||
|
const tabContextOrError = await this._respository.getContextByTabId(tabId, transaction);
|
||||||
|
if (tabContextOrError.isSuccess && !tabContextOrError.data) {
|
||||||
|
return Result.fail(new Error("Invalid or expired Tab ID"));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (tabContextOrError.isFailure) {
|
||||||
|
return Result.fail(tabContextOrError.error);
|
||||||
|
}
|
||||||
|
|
||||||
|
return Result.ok(tabContextOrError.data);
|
||||||
|
});
|
||||||
|
} catch (error: unknown) {
|
||||||
|
return Result.fail(error as Error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Registra un nuevo contexto de pestaña para un usuario
|
||||||
|
*/
|
||||||
|
async createContext(tabId: UniqueID, userId: UniqueID): Promise<Result<TabContext, Error>> {
|
||||||
|
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<Result<void, Error>> {
|
||||||
|
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<Result<void, Error>> {
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
1
apps/server/src/contexts/auth/domain/entities/index.ts
Normal file
1
apps/server/src/contexts/auth/domain/entities/index.ts
Normal file
@ -0,0 +1 @@
|
|||||||
|
export * from "./tab-context";
|
||||||
81
apps/server/src/contexts/auth/domain/entities/tab-context.ts
Normal file
81
apps/server/src/contexts/auth/domain/entities/tab-context.ts
Normal file
@ -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<ITabContextProps> implements ITabContext {
|
||||||
|
private _companyId: UniqueID | undefined;
|
||||||
|
private _branchId: UniqueID | undefined;
|
||||||
|
|
||||||
|
static create(props: ITabContextProps, id?: UniqueID): Result<TabContext, Error> {
|
||||||
|
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,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,4 +1,5 @@
|
|||||||
export * from "./aggregates/authenticated-user";
|
export * from "./aggregates";
|
||||||
export * from "./events/user-authenticated.event";
|
export * from "./entities";
|
||||||
|
export * from "./events";
|
||||||
export * from "./repositories";
|
export * from "./repositories";
|
||||||
export * from "./value-objects";
|
export * from "./value-objects";
|
||||||
|
|||||||
@ -6,7 +6,7 @@ export interface IAuthenticatedUserRepository {
|
|||||||
findUserByEmail(
|
findUserByEmail(
|
||||||
email: EmailAddress,
|
email: EmailAddress,
|
||||||
transaction?: any
|
transaction?: any
|
||||||
): Promise<Result<AuthenticatedUser | null, Error>>;
|
): Promise<Result<AuthenticatedUser, Error>>;
|
||||||
|
userExists(email: EmailAddress, transaction?: any): Promise<Result<boolean, Error>>;
|
||||||
createUser(user: AuthenticatedUser, transaction?: any): Promise<Result<void, Error>>;
|
createUser(user: AuthenticatedUser, transaction?: any): Promise<Result<void, Error>>;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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<Result<TabContext, Error>>;
|
||||||
|
createContext(context: TabContext, transaction?: Transaction): Promise<Result<void, Error>>;
|
||||||
|
contextExists(tabId: UniqueID, transaction?: any): Promise<Result<boolean, Error>>;
|
||||||
|
updateCompanyByTabId(
|
||||||
|
tabId: UniqueID,
|
||||||
|
companyId: UniqueID,
|
||||||
|
transaction?: Transaction
|
||||||
|
): Promise<Result<void, Error>>;
|
||||||
|
deleteContextByTabId(tabId: UniqueID, transaction?: any): Promise<Result<void, Error>>;
|
||||||
|
}
|
||||||
@ -2,13 +2,6 @@ import { Result } from "@common/domain";
|
|||||||
import { AuthenticatedUser } from "@contexts/auth/domain";
|
import { AuthenticatedUser } from "@contexts/auth/domain";
|
||||||
|
|
||||||
export interface IAuthenticatedUserMapper {
|
export interface IAuthenticatedUserMapper {
|
||||||
/**
|
|
||||||
* 🔹 Convierte una entidad de la base de datos en un agregado de dominio `AuthenticatedUser`
|
|
||||||
*/
|
|
||||||
toDomain(entity: any): Result<AuthenticatedUser, Error>;
|
toDomain(entity: any): Result<AuthenticatedUser, Error>;
|
||||||
|
|
||||||
/**
|
|
||||||
* 🔹 Convierte un agregado `AuthenticatedUser` en un objeto listo para persistencia
|
|
||||||
*/
|
|
||||||
toPersistence(aggregate: AuthenticatedUser): any;
|
toPersistence(aggregate: AuthenticatedUser): any;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -16,7 +16,7 @@ export class AuthenticatedUserMapper implements IAuthenticatedUserMapper {
|
|||||||
const usernameResult = Username.create(entity.username);
|
const usernameResult = Username.create(entity.username);
|
||||||
const passwordHashResult = PasswordHash.create(entity.passwordHash);
|
const passwordHashResult = PasswordHash.create(entity.passwordHash);
|
||||||
const emailResult = EmailAddress.create(entity.email);
|
const emailResult = EmailAddress.create(entity.email);
|
||||||
1;
|
|
||||||
// Validar que no haya errores en la creación de los Value Objects
|
// Validar que no haya errores en la creación de los Value Objects
|
||||||
const okOrError = Result.combine([
|
const okOrError = Result.combine([
|
||||||
uniqueIdResult,
|
uniqueIdResult,
|
||||||
@ -35,7 +35,6 @@ export class AuthenticatedUserMapper implements IAuthenticatedUserMapper {
|
|||||||
email: emailResult.data!,
|
email: emailResult.data!,
|
||||||
passwordHash: passwordHashResult.data!,
|
passwordHash: passwordHashResult.data!,
|
||||||
roles: entity.roles || [],
|
roles: entity.roles || [],
|
||||||
token: entity.token,
|
|
||||||
},
|
},
|
||||||
uniqueIdResult.data!
|
uniqueIdResult.data!
|
||||||
);
|
);
|
||||||
|
|||||||
@ -1,2 +1,4 @@
|
|||||||
export * from "./authenticated-user-mapper.interface";
|
export * from "./authenticated-user-mapper.interface";
|
||||||
export * from "./authenticated-user.mapper";
|
export * from "./authenticated-user.mapper";
|
||||||
|
export * from "./tab-context-mapper.interface";
|
||||||
|
export * from "./tab-context.mapper";
|
||||||
|
|||||||
@ -0,0 +1,7 @@
|
|||||||
|
import { Result } from "@common/domain";
|
||||||
|
import { TabContext } from "@contexts/auth/domain";
|
||||||
|
|
||||||
|
export interface ITabContextMapper {
|
||||||
|
toDomain(entity: any): Result<TabContext, Error>;
|
||||||
|
toPersistence(aggregate: TabContext): any;
|
||||||
|
}
|
||||||
@ -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<TabContext, Error> {
|
||||||
|
if (!entity) {
|
||||||
|
return Result.fail(new Error("Entity not found"));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Crear Value Objects asegurando que sean válidos
|
||||||
|
const uniqueIdResult = UniqueID.create(entity.id);
|
||||||
|
const 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();
|
||||||
@ -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<AuthUserModel>;
|
export type AuthUserCreationAttributes = InferCreationAttributes<
|
||||||
|
AuthUserModel,
|
||||||
|
{ omit: "contexts" }
|
||||||
|
> & {
|
||||||
|
contexts: TabContextCreationAttributes[];
|
||||||
|
};
|
||||||
|
|
||||||
export class AuthUserModel extends Model<
|
export class AuthUserModel extends Model<
|
||||||
InferAttributes<AuthUserModel>,
|
InferAttributes<AuthUserModel>,
|
||||||
@ -11,13 +24,23 @@ export class AuthUserModel extends Model<
|
|||||||
return Promise.resolve();
|
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 id: string;
|
||||||
declare username: string;
|
declare username: string;
|
||||||
declare email: string;
|
declare email: string;
|
||||||
declare password: string;
|
declare password: string;
|
||||||
declare roles: string[];
|
declare roles: string[];
|
||||||
declare isActive: boolean;
|
declare isActive: boolean;
|
||||||
|
|
||||||
|
declare contexts: NonAttribute<TabContextModel[]>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default (sequelize: Sequelize) => {
|
export default (sequelize: Sequelize) => {
|
||||||
@ -68,6 +91,12 @@ export default (sequelize: Sequelize) => {
|
|||||||
deletedAt: "deleted_at",
|
deletedAt: "deleted_at",
|
||||||
|
|
||||||
indexes: [{ name: "email_idx", fields: ["email"], unique: true }],
|
indexes: [{ name: "email_idx", fields: ["email"], unique: true }],
|
||||||
|
|
||||||
|
whereMergeStrategy: "and", // <- cómo tratar el merge de un scope
|
||||||
|
|
||||||
|
defaultScope: {},
|
||||||
|
|
||||||
|
scopes: {},
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
return AuthUserModel;
|
return AuthUserModel;
|
||||||
|
|||||||
@ -52,7 +52,7 @@ export class AuthenticatedUserRepository
|
|||||||
async findUserByEmail(
|
async findUserByEmail(
|
||||||
email: EmailAddress,
|
email: EmailAddress,
|
||||||
transaction?: Transaction
|
transaction?: Transaction
|
||||||
): Promise<Result<AuthenticatedUser | null, Error>> {
|
): Promise<Result<AuthenticatedUser, Error>> {
|
||||||
try {
|
try {
|
||||||
const rawUser: any = await this._findById(
|
const rawUser: any = await this._findById(
|
||||||
AuthUserModel,
|
AuthUserModel,
|
||||||
@ -62,7 +62,7 @@ export class AuthenticatedUserRepository
|
|||||||
);
|
);
|
||||||
|
|
||||||
if (!rawUser === true) {
|
if (!rawUser === true) {
|
||||||
return Result.ok(null);
|
return Result.fail(new Error("User with email not exists"));
|
||||||
}
|
}
|
||||||
|
|
||||||
return this._mapper.toDomain(rawUser);
|
return this._mapper.toDomain(rawUser);
|
||||||
|
|||||||
@ -1,2 +1,4 @@
|
|||||||
export * from "./auth-user.model";
|
export * from "./auth-user.model";
|
||||||
export * from "./authenticated-user.repository";
|
export * from "./authenticated-user.repository";
|
||||||
|
export * from "./tab-context.model";
|
||||||
|
export * from "./tab-context.repository";
|
||||||
|
|||||||
@ -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<TabContextModel, { omit: "user" }>,
|
||||||
|
InferCreationAttributes<TabContextModel, { omit: "user" }>
|
||||||
|
> {
|
||||||
|
// To avoid table creation
|
||||||
|
/*static async sync(): Promise<any> {
|
||||||
|
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<AuthUserModel>;
|
||||||
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
};
|
||||||
@ -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<TabContext>
|
||||||
|
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<Result<TabContext, Error>> {
|
||||||
|
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<Result<boolean, Error>> {
|
||||||
|
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<Result<void, Error>> {
|
||||||
|
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<Result<void, Error>> {
|
||||||
|
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<Result<void, Error>> {
|
||||||
|
try {
|
||||||
|
await TabContextModel.destroy({
|
||||||
|
where: {
|
||||||
|
tab_id: tabId.toString(),
|
||||||
|
},
|
||||||
|
transaction,
|
||||||
|
force: false,
|
||||||
|
});
|
||||||
|
return Result.ok();
|
||||||
|
} catch (error: any) {
|
||||||
|
return this._handleDatabaseError(error, this._customErrorMapper);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -7,7 +7,12 @@ export interface IRegisterUserResponseDTO {
|
|||||||
export interface ILoginUserResponseDTO {
|
export interface ILoginUserResponseDTO {
|
||||||
access_token: string;
|
access_token: string;
|
||||||
refresh_token: string;
|
refresh_token: string;
|
||||||
user_id: string;
|
user: {
|
||||||
|
id: string;
|
||||||
|
username: string;
|
||||||
|
email: string;
|
||||||
|
};
|
||||||
|
tab_id: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ILogoutResponseDTO {
|
export interface ILogoutResponseDTO {
|
||||||
|
|||||||
@ -1 +1,2 @@
|
|||||||
export * from "./passport-auth.middleware";
|
export * from "./passport-auth.middleware";
|
||||||
|
export * from "./tab-context.middleware";
|
||||||
|
|||||||
@ -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();
|
||||||
|
};
|
||||||
@ -1,4 +1,5 @@
|
|||||||
import { validateRequest } from "@common/presentation";
|
import { validateRequest } from "@common/presentation";
|
||||||
|
import { validateTabHeader } from "@contexts/auth/presentation";
|
||||||
import {
|
import {
|
||||||
createLoginController,
|
createLoginController,
|
||||||
createRegisterController,
|
createRegisterController,
|
||||||
@ -41,9 +42,14 @@ export const authRouter = (appRouter: Router) => {
|
|||||||
*
|
*
|
||||||
* @apiError (401) {String} message Invalid email or password.
|
* @apiError (401) {String} message Invalid email or password.
|
||||||
*/
|
*/
|
||||||
authRoutes.post("/login", validateRequest(LoginUserSchema), (req, res, next) => {
|
authRoutes.post(
|
||||||
createLoginController().execute(req, res, next);
|
"/login",
|
||||||
});
|
validateRequest(LoginUserSchema),
|
||||||
|
validateTabHeader,
|
||||||
|
(req, res, next) => {
|
||||||
|
createLoginController().execute(req, res, next);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @api {post} /api/auth/select-company Select an active company
|
* @api {post} /api/auth/select-company Select an active company
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user