This commit is contained in:
David Arranz 2025-02-06 12:55:47 +01:00
parent 36788f34e8
commit e8d606714a
16 changed files with 224 additions and 200 deletions

View File

@ -9,6 +9,11 @@
"date": 1738768744297,
"name": "debug-2025-02-05.log",
"hash": "35182b14bda063a4b734238473f84cfa66a0362a76d8f12f0c20277df81c7256"
},
{
"date": 1738834648631,
"name": "debug-2025-02-06.log",
"hash": "fbca91e028d38f5065d0e5bdbbfdbf49aadd06feddd074c6a1419b7c9e6b4fd7"
}
],
"hashType": "sha256"

View File

@ -9,6 +9,11 @@
"date": 1738768744292,
"name": "error-2025-02-05.log",
"hash": "c32d976d68382b2ba2ddec8c907c30547ec9fda2bb31180bfdbeb685964810a8"
},
{
"date": 1738834648629,
"name": "error-2025-02-06.log",
"hash": "17c45ac8b0d8d1bf22f601247a7f1384f004d06420d39e78eb65cb0a26815e13"
}
],
"hashType": "sha256"

View File

@ -14,6 +14,7 @@ const initLogger = () => {
format.timestamp(),
format.align(),
format.splat(),
format.errors({ stack: !isProduction }),
format.printf((info) => {
const rid = rTracer.id();

View File

@ -1,11 +0,0 @@
import { AuthenticatedUser } from "../domain";
export interface IAuthProvider {
/* JWT Strategy */
generateAccessToken(payload: object): string;
generateRefreshToken(payload: object): string;
verifyToken(token: string): Promise<AuthenticatedUser | null>;
/* LocalStrategy */
//_verifyUser(email: string, password: string): Promise<AuthenticatedUser | null>;
}

View File

@ -2,6 +2,10 @@ import { Result, UniqueID } from "@common/domain";
import { AuthenticatedUser, EmailAddress, HashPassword, PlainPassword, Username } from "../domain";
export interface IAuthService {
generateAccessToken(payload: object): string;
generateRefreshToken(payload: object): string;
verifyToken(token: string): Promise<AuthenticatedUser | null>;
registerUser(params: {
username: Username;
email: EmailAddress;
@ -24,4 +28,11 @@ export interface IAuthService {
Error
>
>;
logoutUser(params: { email: EmailAddress; tabId: UniqueID }): Promise<Result<void, Error>>;
verifyUser(params: {
email: EmailAddress;
plainPassword: PlainPassword;
}): Promise<Result<AuthenticatedUser, Error>>;
}

View File

@ -1,33 +1,47 @@
import { Result, UniqueID } from "@common/domain";
import { ITransactionManager } from "@common/infrastructure/database";
import jwt from "jsonwebtoken";
import {
AuthenticatedUser,
EmailAddress,
HashPassword,
IAuthenticatedUserRepository,
PlainPassword,
TabContext,
Username,
} from "../domain";
import { ITabContextRepository } from "../domain/repositories/tab-context-repository.interface";
import { IAuthProvider } from "./auth-provider.interface";
import { IAuthService } from "./auth-service.interface";
const SECRET_KEY = process.env.JWT_SECRET || "supersecretkey";
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;
private readonly _authProvider: IAuthProvider;
constructor(
userRepo: IAuthenticatedUserRepository,
tabContextRepo: ITabContextRepository,
transactionManager: ITransactionManager,
authProvider: IAuthProvider
transactionManager: ITransactionManager
) {
this._userRepo = userRepo;
this._tabContactRepo = tabContextRepo;
this._transactionManager = transactionManager;
this._authProvider = authProvider;
}
generateAccessToken(payload: object): string {
return jwt.sign(payload, SECRET_KEY, { expiresIn: ACCESS_EXPIRATION });
}
generateRefreshToken(payload: object): string {
return jwt.sign(payload, SECRET_KEY, { expiresIn: REFRESH_EXPIRATION });
}
verifyToken(token: string): any {
return jwt.verify(token, SECRET_KEY);
}
/**
@ -125,25 +139,23 @@ export class AuthService implements IAuthService {
const contextOrError = TabContext.create({
userId: user.id,
tabId: tabId,
companyId: UniqueID.generateUndefinedID().data,
branchId: UniqueID.generateUndefinedID().data,
});
if (contextOrError.isFailure) {
return Result.fail(new Error("Error creating user context"));
}
await this._tabContactRepo.registerContext(contextOrError.data, transaction);
await this._tabContactRepo.registerContextByTabId(contextOrError.data, transaction);
// 🔹 Generar Access Token y Refresh Token
const accessToken = this._authProvider.generateAccessToken({
const accessToken = this.generateAccessToken({
userId: user.id.toString(),
email: email.toString(),
tabId: tabId.toString(),
roles: ["USER"],
});
const refreshToken = this._authProvider.generateRefreshToken({
const refreshToken = this.generateRefreshToken({
userId: user.id.toString(),
});
@ -159,4 +171,72 @@ export class AuthService implements IAuthService {
return Result.fail(error as Error);
}
}
/**
*
* Autentica a un usuario validando su email y contraseña.
*/
async logoutUser(params: { email: EmailAddress; tabId: UniqueID }): Promise<Result<void, Error>> {
try {
return await this._transactionManager.complete(async (transaction) => {
const { email, tabId } = params;
// Verificar que el tab ID está definido
if (!tabId.isDefined()) {
return Result.fail(new Error("Invalid tab id"));
}
// 🔹 Verificar si el usuario existe en la base de datos
const userResult = await this._userRepo.getUserByEmail(email, transaction);
if (userResult.isFailure) {
return Result.fail(new Error("Invalid email or password"));
}
const user = userResult.data;
const contextOrError = TabContext.create({
userId: user.id,
tabId: tabId,
});
if (contextOrError.isFailure) {
return Result.fail(new Error("Error creating user context"));
}
// Desregistrar el contexto de ese tab ID
await this._tabContactRepo.unregisterContextByTabId(contextOrError.data, transaction);
return Result.ok();
});
} catch (error: unknown) {
return Result.fail(error as Error);
}
}
async verifyUser(params: {
email: EmailAddress;
plainPassword: PlainPassword;
}): Promise<Result<AuthenticatedUser, Error>> {
try {
return await this._transactionManager.complete(async (transaction) => {
const { email, plainPassword } = params;
const userResult = await this._userRepo.getUserByEmail(email, transaction);
if (userResult.isFailure || !userResult.data) {
return Result.fail(new Error("Invalid email or password"));
}
const user = userResult.data;
const isValidPassword = await user.verifyPassword(plainPassword);
if (!isValidPassword) {
return Result.fail(new Error("Invalid email or password"));
}
return Result.ok(user);
});
} catch (error: unknown) {
return Result.fail(error as Error);
}
}
}

View File

@ -1,14 +1,11 @@
import { createSequelizeTransactionManager } from "@common/infrastructure";
import { createAuthenticatedUserRepository, createTabContextRepository } from "../infraestructure";
import { createPassportAuthProvider } from "../infraestructure/passport/passport-auth-provider";
import { IAuthProvider } from "./auth-provider.interface";
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-provider.interface";
export * from "./auth-service.interface";
export const createAuthService = (): IAuthService => {
@ -16,16 +13,7 @@ export const createAuthService = (): IAuthService => {
const authenticatedUserRepository = createAuthenticatedUserRepository();
const tabContextRepository = createTabContextRepository();
const authProvider: IAuthProvider = createPassportAuthProvider(
authenticatedUserRepository,
transactionManager
);
return new AuthService(
authenticatedUserRepository,
tabContextRepository,
transactionManager,
authProvider
);
return new AuthService(authenticatedUserRepository, tabContextRepository, transactionManager);
};
export const createTabContextService = (): ITabContextService => {

View File

@ -3,12 +3,6 @@ import { TabContext } from "../domain";
export interface ITabContextService {
getContextByTabId(tabId: UniqueID): Promise<Result<TabContext, Error>>;
createContext(params: {
tabId: UniqueID;
userId: UniqueID;
companyId: UniqueID;
branchId: UniqueID;
}): Promise<Result<TabContext, Error>>;
assignCompany(tabId: UniqueID, companyId: UniqueID): Promise<Result<void, Error>>;
removeContext(tabId: UniqueID): Promise<Result<void, Error>>;
createContext(params: { tabId: UniqueID; userId: UniqueID }): Promise<Result<TabContext, Error>>;
removeContext(params: { tabId: UniqueID; userId: UniqueID }): Promise<Result<void, Error>>;
}

View File

@ -42,10 +42,8 @@ export class TabContextService implements ITabContextService {
async createContext(params: {
tabId: UniqueID;
userId: UniqueID;
companyId: UniqueID;
branchId: UniqueID;
}): Promise<Result<TabContext, Error>> {
const { tabId, userId, companyId, branchId } = params;
const { tabId, userId } = params;
if (!userId || !tabId) {
return Result.fail(new Error("User ID and Tab ID are required"));
@ -57,8 +55,6 @@ export class TabContextService implements ITabContextService {
{
userId,
tabId,
companyId,
branchId,
},
UniqueID.generateNewID().data
);
@ -67,7 +63,7 @@ export class TabContextService implements ITabContextService {
return Result.fail(contextOrError.error);
}
await this._respository.registerContext(contextOrError.data, transaction);
await this._respository.registerContextByTabId(contextOrError.data, transaction);
return Result.ok(contextOrError.data);
});
@ -76,39 +72,31 @@ export class TabContextService implements ITabContextService {
}
}
/**
* 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"));
async removeContext(params: { tabId: UniqueID; userId: UniqueID }): Promise<Result<void, Error>> {
const { tabId, userId } = params;
if (!userId || !tabId) {
return Result.fail(new Error("User ID and Tab ID are required"));
}
try {
return await this._transactionManager.complete(async (transaction) => {
return await this._respository.deleteContextByTabId(tabId, transaction);
const contextOrError = TabContext.create(
{
userId,
tabId,
},
UniqueID.generateNewID().data
);
if (contextOrError.isFailure) {
return Result.fail(contextOrError.error);
}
return await this._respository.unregisterContextByTabId(contextOrError.data, transaction);
});
} catch (error: unknown) {
return Result.fail(error as Error);

View File

@ -3,18 +3,11 @@ 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;
hasCompanyAssigned(): boolean;
hasBranchAssigned(): boolean;
toPersistenceData(): any;
}
@ -32,22 +25,6 @@ export class TabContext extends DomainEntity<ITabContextProps> implements ITabCo
return this._props.userId;
}
get companyId(): UniqueID {
return this._props.companyId;
}
get branchId(): UniqueID {
return this._props.branchId;
}
hasCompanyAssigned(): boolean {
return this._props.companyId.isDefined();
}
hasBranchAssigned(): boolean {
return this._props.branchId.isDefined();
}
/**
* 🔹 Devuelve una representación lista para persistencia
*/
@ -56,8 +33,6 @@ export class TabContext extends DomainEntity<ITabContextProps> implements ITabCo
id: this._id.toString(),
tab_id: this.tabId.toString(),
user_id: this.userId.toString(),
company_id: this.companyId.toString(),
branch_id: this.branchId.toString(),
};
}
}

View File

@ -4,12 +4,16 @@ import { TabContext } from "../entities";
export interface ITabContextRepository {
getContextByTabId(tabId: UniqueID, transaction?: any): Promise<Result<TabContext, Error>>;
registerContext(context: TabContext, transaction?: Transaction): Promise<Result<void, Error>>;
contextExists(tabId: UniqueID, transaction?: any): Promise<Result<boolean, Error>>;
updateCompanyByTabId(
tabId: UniqueID,
companyId: UniqueID,
contextExistsByTabId(tabId: UniqueID, transaction?: any): Promise<Result<boolean, Error>>;
registerContextByTabId(
context: TabContext,
transaction?: Transaction
): Promise<Result<void, Error>>;
unregisterContextByTabId(
context: TabContext,
transaction?: Transaction
): Promise<Result<void, Error>>;
deleteContextByTabId(tabId: UniqueID, transaction?: any): Promise<Result<void, Error>>;
}

View File

@ -1 +1,9 @@
export * from "./passport-auth-provider";
import { createAuthService } from "@contexts/auth/application";
import { PassportAuthProvider } from "./passport-auth-provider";
export const createPassportAuthProvider = () => {
const _authService = createAuthService();
return new PassportAuthProvider(_authService);
};
export const initializePassportAuthProvide = () => createPassportAuthProvider();

View File

@ -1,38 +1,32 @@
import { createSequelizeTransactionManager } from "@common/infrastructure";
import { ITransactionManager } from "@common/infrastructure/database";
import { IAuthProvider } from "@contexts/auth/application";
import {
AuthenticatedUser,
EmailAddress,
IAuthenticatedUserRepository,
PlainPassword,
} from "@contexts/auth/domain";
import jwt from "jsonwebtoken";
import { IAuthService } from "@contexts/auth/application";
import { AuthenticatedUser, EmailAddress, PlainPassword } from "@contexts/auth/domain";
import passport from "passport";
import { ExtractJwt, Strategy as JwtStrategy } from "passport-jwt";
import { Strategy as LocalStrategy } from "passport-local";
import { createAuthenticatedUserRepository } from "../sequelize";
const SECRET_KEY = process.env.JWT_SECRET || "supersecretkey";
const ACCESS_EXPIRATION = process.env.JWT_ACCESS_EXPIRATION || "1h";
const REFRESH_EXPIRATION = process.env.JWT_REFRESH_EXPIRATION || "7d";
export class PassportAuthProvider implements IAuthProvider {
private readonly _repository: IAuthenticatedUserRepository;
private readonly _transactionManager!: ITransactionManager;
export class PassportAuthProvider {
private readonly _authService: IAuthService;
private async _verifyUser(email: string, password: string): Promise<AuthenticatedUser | null> {
const emailVO = EmailAddress.create(email);
if (emailVO.isFailure) return Promise.resolve(null);
const passwordVO = PlainPassword.create(password);
if (passwordVO.isFailure) return Promise.resolve(null);
const plainPasswordVO = PlainPassword.create(password);
if (plainPasswordVO.isFailure) return Promise.resolve(null);
const userResult = await this._repository.getUserByEmail(emailVO.data);
if (userResult.isFailure || !userResult.data) return Promise.resolve(null);
const userResult = await this._authService.verifyUser({
email: emailVO.data,
plainPassword: plainPasswordVO.data,
});
if (userResult.isFailure || !userResult.data) {
return Promise.resolve(null);
}
const user = userResult.data;
const isValidPassword = await user.verifyPassword(passwordVO.data);
const isValidPassword = await user.verifyPassword(plainPasswordVO.data);
return !isValidPassword ? Promise.resolve(null) : Promise.resolve(user);
}
@ -50,6 +44,7 @@ export class PassportAuthProvider implements IAuthProvider {
"jwt",
new JwtStrategy(jwtOptions, (tokenPayload, done) => {
try {
console.log(tokenPayload);
return done(null, tokenPayload);
} catch (error) {
return done(error, false);
@ -77,32 +72,8 @@ export class PassportAuthProvider implements IAuthProvider {
passport.initialize();
}
constructor(repository: IAuthenticatedUserRepository, transactionManager: ITransactionManager) {
this._repository = repository;
this._transactionManager = transactionManager;
constructor(authService: IAuthService) {
this._authService = authService;
this.initializePassport();
}
generateAccessToken(payload: object): string {
return jwt.sign(payload, SECRET_KEY, { expiresIn: String(ACCESS_EXPIRATION) });
}
generateRefreshToken(payload: object): string {
return jwt.sign(payload, SECRET_KEY, { expiresIn: REFRESH_EXPIRATION });
}
verifyToken(token: string): any {
return jwt.verify(token, SECRET_KEY);
}
}
export const createPassportAuthProvider = (
repository?: IAuthenticatedUserRepository,
transactionManager?: ITransactionManager
) => {
const _transactionManager = transactionManager || createSequelizeTransactionManager();
const _repository = repository || createAuthenticatedUserRepository();
return new PassportAuthProvider(_repository, _transactionManager);
};
export const initializePassportAuthProvide = () => createPassportAuthProvider();

View File

@ -2,7 +2,7 @@ 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 { Op, Transaction } from "sequelize";
import { createTabContextMapper, ITabContextMapper } from "../mappers";
import { TabContextModel } from "./tab-context.model";
@ -51,7 +51,7 @@ export class TabContextRepository
}
}
async contextExists(tabId: UniqueID, transaction?: any): Promise<Result<boolean, Error>> {
async contextExistsByTabId(tabId: UniqueID, transaction?: any): Promise<Result<boolean, Error>> {
try {
const result: any = await this._exists(
TabContextModel,
@ -67,57 +67,57 @@ export class TabContextRepository
}
/**
* Crea un contexto para un tab id o actualiza si ya existe
* Registra un contexto para un tab id o actualiza si ya existe
* @param context
* @param transaction
* @returns
*/
async registerContext(
async registerContextByTabId(
context: TabContext,
transaction?: Transaction
): Promise<Result<void, Error>> {
try {
const { id } = context;
const persistenceData = this._mapper.toPersistence(context);
const { userId, tabId } = context;
const data = this._mapper.toPersistence(context);
// Si existe el contexto de ese tabId, lo actualizo.
if (await this._exists(TabContextModel, "tab_id", tabId.toString())) {
await TabContextModel.update(data, {
where: { [Op.and]: [{ tab_id: tabId.toString() }, { user_id: userId.toString() }] },
transaction,
});
} else {
await TabContextModel.create(data, {
include: [{ all: true }],
transaction,
});
}
await this._save(TabContextModel, id, persistenceData, {}, transaction);
return Result.ok();
} catch (error: any) {
return this._handleDatabaseError(error, this._customErrorMapper);
}
}
async updateCompanyByTabId(
tabId: UniqueID,
companyId: UniqueID,
/**
* Desregistra un contexto para un tab id o actualiza si ya existe
* @param context
* @param transaction
* @returns
*/
async unregisterContextByTabId(
context: TabContext,
transaction?: Transaction
): Promise<Result<void, Error>> {
try {
await TabContextModel.update(
{ company_id: companyId.toString() },
{
where: {
tab_id: tabId.toString(),
},
transaction,
}
);
const { userId, tabId } = context;
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(),
},
where: { [Op.and]: [{ tab_id: tabId.toString() }, { user_id: userId.toString() }] },
transaction,
force: false,
});
return Result.ok();
} catch (error: any) {

View File

@ -1,6 +1,7 @@
import { UniqueID } from "@common/domain";
import { Result, UniqueID } from "@common/domain";
import { ExpressController } from "@common/presentation";
import { createAuthService, IAuthService } from "@contexts/auth/application";
import { EmailAddress } from "@contexts/auth/domain";
class LogoutController extends ExpressController {
private readonly _authService!: IAuthService;
@ -12,22 +13,21 @@ class LogoutController extends ExpressController {
async executeImpl() {
const tabId = this.req.headers["x-tab-id"];
const emailVO = EmailAddress.create(this.req.body.email);
const tabIdVO = UniqueID.create(String(tabId));
if (tabIdVO.isFailure) {
return this.clientError("Invalid tab id", [tabIdVO.error]);
const resultValidation = Result.combine([emailVO, tabIdVO]);
if (resultValidation.isFailure) {
return this.clientError("Invalid input data", resultValidation.error);
}
const userOrError = await this._authService.logoutUser({
await this._authService.logoutUser({
email: emailVO.data,
plainPassword: plainPasswordVO.data,
tabId: tabIdVO.data,
});
if (userOrError.isFailure) {
return this.unauthorizedError(userOrError.error.message);
}
return this.ok();
}
}

View File

@ -1,9 +1,10 @@
import { validateRequest } from "@common/presentation";
import { validateTabContextHeader, validateUser } from "@contexts/auth/presentation";
import { createLoginController } 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, RegisterUserSchema } from "@contexts/auth/presentation/dto";
import { Router } from "express";
import { NextFunction, Request, Response, Router } from "express";
export const authRouter = (appRouter: Router) => {
const authRoutes: Router = Router({ mergeParams: true });
@ -45,7 +46,7 @@ export const authRouter = (appRouter: Router) => {
"/login",
validateRequest(LoginUserSchema),
validateTabContextHeader,
(req, res, next) => {
(req: Request, res: Response, next: NextFunction) => {
createLoginController().execute(req, res, next);
}
);
@ -61,10 +62,14 @@ export const authRouter = (appRouter: Router) => {
*
* @apiSuccess (200) {String} message Success message.
*/
authRoutes.post("/logout", validateUser, validateTabContextHeader, (req, res, next) => {
res.sendStatus(200);
//createLogoutController().execute(req, res, next);
});
authRoutes.post(
"/logout",
validateUser,
validateTabContextHeader,
(req: Request, res: Response, next: NextFunction) => {
createLogoutController().execute(req, res, next);
}
);
appRouter.use("/auth", authRoutes);
};