.
This commit is contained in:
parent
80db9dfb9e
commit
cf38a2ad9d
@ -16,7 +16,6 @@ const initLogger = () => {
|
||||
format.timestamp({ format: "YYYY-MM-DD HH:mm:ss" }),
|
||||
format.align(),
|
||||
format.splat(),
|
||||
format.metadata(),
|
||||
format.errors({ stack: !isProduction }),
|
||||
|
||||
format.printf((info) => {
|
||||
|
||||
@ -1,10 +1,14 @@
|
||||
import { logger } from "@common/infrastructure/logger";
|
||||
import {
|
||||
AuthenticatedRequest,
|
||||
TabContextRequest,
|
||||
} from "@contexts/auth/infraestructure/express/types";
|
||||
import { NextFunction, Request, Response } from "express";
|
||||
import httpStatus from "http-status";
|
||||
import { ApiError } from "./api-error";
|
||||
|
||||
export abstract class ExpressController {
|
||||
protected req!: Request;
|
||||
protected req!: Request | AuthenticatedRequest | TabContextRequest;
|
||||
protected res!: Response;
|
||||
protected next!: NextFunction;
|
||||
|
||||
|
||||
@ -1,4 +1,5 @@
|
||||
export * from "./list-users";
|
||||
export * from "./login";
|
||||
export * from "./logout";
|
||||
export * from "./refresh-token";
|
||||
export * from "./register";
|
||||
|
||||
@ -1 +1 @@
|
||||
export * from "./login.use-case";
|
||||
export * from "./refresh-token.use-case";
|
||||
|
||||
@ -1,4 +1,3 @@
|
||||
import { Result } from "@common/domain";
|
||||
import { ITransactionManager } from "@common/infrastructure/database";
|
||||
import { Token } from "@contexts/auth/domain";
|
||||
import { IAuthService } from "@contexts/auth/domain/services";
|
||||
@ -11,18 +10,14 @@ export class RefreshTokenUseCase {
|
||||
|
||||
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 payloadData = this.authService.verifyRefreshToken(token);
|
||||
|
||||
const { user_id, tab_id, email, roles } = payload;
|
||||
/*if (!payload || !payload.email || !payload.user_id || !payload.tab_id || !payload.roles) {
|
||||
return Result.fail(new Error("Invalid input data"));
|
||||
}*/
|
||||
|
||||
return this.authService.generateRefreshToken({
|
||||
user_id,
|
||||
tab_id,
|
||||
email,
|
||||
roles,
|
||||
...payloadData,
|
||||
});
|
||||
});
|
||||
}
|
||||
@ -1 +1 @@
|
||||
export * from "./refresh-token.use-case";
|
||||
export * from "./register.use-case";
|
||||
|
||||
@ -1,16 +1,16 @@
|
||||
import { ITransactionManager } from "@common/infrastructure/database";
|
||||
import { LoginData } from "@contexts/auth/domain";
|
||||
import { RegisterData } from "@contexts/auth/domain";
|
||||
import { IAuthService } from "@contexts/auth/domain/services";
|
||||
|
||||
export class LoginUseCase {
|
||||
export class RegisterUseCase {
|
||||
constructor(
|
||||
private readonly authService: IAuthService,
|
||||
private readonly transactionManager: ITransactionManager
|
||||
) {}
|
||||
|
||||
public async execute(loginData: LoginData) {
|
||||
public async execute(registerData: RegisterData) {
|
||||
return await this.transactionManager.complete(async (transaction) => {
|
||||
return await this.authService.loginUser(loginData, transaction);
|
||||
return await this.authService.registerUser(registerData, transaction);
|
||||
});
|
||||
}
|
||||
}
|
||||
@ -56,7 +56,7 @@ export class AuthenticatedUser
|
||||
}
|
||||
|
||||
hasRoles(roles: string[]): boolean {
|
||||
return roles.map((rol) => this.hasRole(rol)).some((value) => value != false);
|
||||
return roles && roles.map((rol) => this.hasRole(rol)).some((value) => value != false);
|
||||
}
|
||||
|
||||
get username(): Username {
|
||||
|
||||
@ -1,3 +1,4 @@
|
||||
export * from "./jwt-payload";
|
||||
export * from "./login-data";
|
||||
export * from "./logout-data";
|
||||
export * from "./register-data";
|
||||
|
||||
75
apps/server/src/contexts/auth/domain/entities/jwt-payload.ts
Normal file
75
apps/server/src/contexts/auth/domain/entities/jwt-payload.ts
Normal file
@ -0,0 +1,75 @@
|
||||
import { DomainEntity, Result, UniqueID } from "@common/domain";
|
||||
import { EmailAddress } from "../value-objects";
|
||||
|
||||
export interface IJWTPayloadProps {
|
||||
tabId: UniqueID;
|
||||
userId: UniqueID;
|
||||
email: EmailAddress;
|
||||
}
|
||||
|
||||
export interface IJWTPayloadPrimitives {
|
||||
tab_id: string;
|
||||
user_id: string;
|
||||
email: string;
|
||||
}
|
||||
|
||||
export interface IJWTPayload {
|
||||
tabId: UniqueID;
|
||||
userId: UniqueID;
|
||||
email: EmailAddress;
|
||||
|
||||
toPersistenceData(): any;
|
||||
}
|
||||
|
||||
export class JWTPayload extends DomainEntity<IJWTPayloadProps> implements IJWTPayload {
|
||||
static create(props: IJWTPayloadProps): Result<JWTPayload, Error> {
|
||||
if (props.email.isEmpty()) {
|
||||
return Result.fail(new Error("Email is required"));
|
||||
}
|
||||
|
||||
return Result.ok(new JWTPayload(props));
|
||||
}
|
||||
|
||||
static createFromPrimitives(values: IJWTPayloadPrimitives): Result<JWTPayload, Error> {
|
||||
const { email, user_id, tab_id } = values;
|
||||
const emailOrError = EmailAddress.create(email);
|
||||
const userIdOrError = UniqueID.create(user_id, false);
|
||||
const tabIdOrError = UniqueID.create(tab_id, false);
|
||||
|
||||
const result = Result.combine([emailOrError, userIdOrError, tabIdOrError]);
|
||||
|
||||
if (result.isFailure) {
|
||||
return Result.fail(result.error);
|
||||
}
|
||||
|
||||
if (emailOrError.data.isEmpty()) {
|
||||
return Result.fail(new Error("Email is required"));
|
||||
}
|
||||
|
||||
return JWTPayload.create({
|
||||
email: emailOrError.data,
|
||||
userId: userIdOrError.data,
|
||||
tabId: tabIdOrError.data,
|
||||
});
|
||||
}
|
||||
|
||||
get tabId(): UniqueID {
|
||||
return this._props.tabId;
|
||||
}
|
||||
|
||||
get userId(): UniqueID {
|
||||
return this._props.userId;
|
||||
}
|
||||
|
||||
get email(): EmailAddress {
|
||||
return this._props.email;
|
||||
}
|
||||
|
||||
toPersistenceData(): any {
|
||||
return {
|
||||
tab_id: this.tabId.toString(),
|
||||
user_id: this.userId.toString(),
|
||||
email: this.email.toString(),
|
||||
};
|
||||
}
|
||||
}
|
||||
@ -7,6 +7,12 @@ export interface ILoginDataProps {
|
||||
tabId: UniqueID;
|
||||
}
|
||||
|
||||
export interface ILoginDataPrimitives {
|
||||
email: string;
|
||||
plainPassword: string;
|
||||
tabId: string;
|
||||
}
|
||||
|
||||
export interface ILoginData {
|
||||
email: EmailAddress;
|
||||
plainPassword: PlainPassword;
|
||||
@ -18,15 +24,11 @@ export class LoginData extends DomainEntity<ILoginDataProps> implements ILoginDa
|
||||
return Result.ok(new this(props));
|
||||
}
|
||||
|
||||
static createFromPrimitives(props: {
|
||||
email: string;
|
||||
plainPassword: string;
|
||||
tabId: string;
|
||||
}): Result<LoginData, Error> {
|
||||
const { email, plainPassword, tabId } = props;
|
||||
static createFromPrimitives(values: ILoginDataPrimitives): Result<LoginData, Error> {
|
||||
const { email, plainPassword, tabId } = values;
|
||||
const emailOrError = EmailAddress.create(email);
|
||||
const plainPasswordOrError = PlainPassword.create(plainPassword);
|
||||
const tabIdOrError = UniqueID.create(tabId);
|
||||
const tabIdOrError = UniqueID.create(tabId, false);
|
||||
|
||||
const result = Result.combine([emailOrError, plainPasswordOrError, tabIdOrError]);
|
||||
|
||||
|
||||
@ -6,6 +6,11 @@ export interface ILogoutDataProps {
|
||||
tabId: UniqueID;
|
||||
}
|
||||
|
||||
export interface ILogoutDataPrimitives {
|
||||
email: string;
|
||||
tabId: string;
|
||||
}
|
||||
|
||||
export interface ILogoutData {
|
||||
email: EmailAddress;
|
||||
tabId: UniqueID;
|
||||
@ -16,10 +21,10 @@ export class LogoutData extends DomainEntity<ILogoutDataProps> implements ILogou
|
||||
return Result.ok(new this(props));
|
||||
}
|
||||
|
||||
static createFromPrimitives(props: { email: string; tabId: string }): Result<LogoutData, Error> {
|
||||
const { email, tabId } = props;
|
||||
static createFromPrimitives(values: ILogoutDataPrimitives): Result<LogoutData, Error> {
|
||||
const { email, tabId } = values;
|
||||
const emailOrError = EmailAddress.create(email);
|
||||
const tabIdOrError = UniqueID.create(tabId);
|
||||
const tabIdOrError = UniqueID.create(tabId, false);
|
||||
|
||||
const result = Result.combine([emailOrError, tabIdOrError]);
|
||||
|
||||
|
||||
@ -7,6 +7,12 @@ export interface IRegisterDataProps {
|
||||
hashPassword: HashPassword;
|
||||
}
|
||||
|
||||
export interface IRegisterDataPrimitives {
|
||||
username: string;
|
||||
email: string;
|
||||
plainPassword: string;
|
||||
}
|
||||
|
||||
export interface IRegisterData {
|
||||
username: Username;
|
||||
email: EmailAddress;
|
||||
@ -18,11 +24,7 @@ export class RegisterData extends DomainEntity<IRegisterDataProps> implements IR
|
||||
return Result.ok(new this(props));
|
||||
}
|
||||
|
||||
static createFromPrimitives(props: {
|
||||
username: string;
|
||||
email: string;
|
||||
plainPassword: string;
|
||||
}): Result<RegisterData, Error> {
|
||||
static createFromPrimitives(props: IRegisterDataPrimitives): Result<RegisterData, Error> {
|
||||
const { username, email, plainPassword } = props;
|
||||
|
||||
const userNameOrError = Username.create(username);
|
||||
|
||||
@ -5,6 +5,12 @@ export interface ITabContextProps {
|
||||
userId: UniqueID;
|
||||
}
|
||||
|
||||
export interface ITabContextPrimitives {
|
||||
id: string;
|
||||
tab_id: string;
|
||||
user_id: string;
|
||||
}
|
||||
|
||||
export interface ITabContext {
|
||||
tabId: UniqueID;
|
||||
userId: UniqueID;
|
||||
@ -17,6 +23,23 @@ export class TabContext extends DomainEntity<ITabContextProps> implements ITabCo
|
||||
return Result.ok(new this(props, id));
|
||||
}
|
||||
|
||||
static createFromPrimitives(values: ITabContextPrimitives): Result<TabContext, Error> {
|
||||
const { user_id, tab_id } = values;
|
||||
const userIdOrError = UniqueID.create(user_id, false);
|
||||
const tabIdOrError = UniqueID.create(tab_id, false);
|
||||
|
||||
const result = Result.combine([userIdOrError, tabIdOrError]);
|
||||
|
||||
if (result.isFailure) {
|
||||
return Result.fail(result.error);
|
||||
}
|
||||
|
||||
return TabContext.create({
|
||||
userId: userIdOrError.data,
|
||||
tabId: tabIdOrError.data,
|
||||
});
|
||||
}
|
||||
|
||||
get tabId(): UniqueID {
|
||||
return this._props.tabId;
|
||||
}
|
||||
@ -25,10 +48,7 @@ export class TabContext extends DomainEntity<ITabContextProps> implements ITabCo
|
||||
return this._props.userId;
|
||||
}
|
||||
|
||||
/**
|
||||
* 🔹 Devuelve una representación lista para persistencia
|
||||
*/
|
||||
toPersistenceData(): any {
|
||||
toPersistenceData(): ITabContextPrimitives {
|
||||
return {
|
||||
id: this._id.toString(),
|
||||
tab_id: this.tabId.toString(),
|
||||
|
||||
@ -1,9 +1,13 @@
|
||||
import { Result } from "@common/domain";
|
||||
import { AuthenticatedUser } from "../aggregates";
|
||||
import { EmailAddress } from "../value-objects";
|
||||
import { EmailAddress, Username } from "../value-objects";
|
||||
|
||||
export interface IAuthenticatedUserRepository {
|
||||
getUserByEmail(email: EmailAddress, transaction?: any): Promise<Result<AuthenticatedUser, Error>>;
|
||||
userExists(email: EmailAddress, transaction?: any): Promise<Result<boolean, Error>>;
|
||||
userExists(
|
||||
username: Username,
|
||||
email: EmailAddress,
|
||||
transaction?: any
|
||||
): Promise<Result<boolean, Error>>;
|
||||
createUser(user: AuthenticatedUser, transaction?: any): Promise<Result<void, Error>>;
|
||||
}
|
||||
|
||||
@ -2,13 +2,13 @@ import { Result } from "@common/domain";
|
||||
import {
|
||||
AuthenticatedUser,
|
||||
EmailAddress,
|
||||
IJWTPayload,
|
||||
LoginData,
|
||||
LogoutData,
|
||||
RegisterData,
|
||||
TabContext,
|
||||
Token,
|
||||
} from "..";
|
||||
import { IJWTPayload } from "../../infraestructure";
|
||||
|
||||
export interface IAuthService {
|
||||
generateAccessToken(payload: IJWTPayload): Result<Token, Error>;
|
||||
|
||||
@ -3,13 +3,14 @@ import {
|
||||
AuthenticatedUser,
|
||||
EmailAddress,
|
||||
IAuthenticatedUserRepository,
|
||||
IJWTPayload,
|
||||
JWTPayload,
|
||||
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";
|
||||
|
||||
@ -23,11 +24,13 @@ export class AuthService implements IAuthService {
|
||||
) {}
|
||||
|
||||
generateAccessToken(payload: IJWTPayload): Result<Token, Error> {
|
||||
return Token.create(JwtHelper.generateToken(payload, ACCESS_EXPIRATION));
|
||||
const data = payload.toPersistenceData();
|
||||
return Token.create(JwtHelper.generateToken(data, ACCESS_EXPIRATION));
|
||||
}
|
||||
|
||||
generateRefreshToken(payload: IJWTPayload): Result<Token, Error> {
|
||||
return Token.create(JwtHelper.generateToken(payload, REFRESH_EXPIRATION));
|
||||
const data = payload.toPersistenceData();
|
||||
return Token.create(JwtHelper.generateToken(data, REFRESH_EXPIRATION));
|
||||
}
|
||||
|
||||
verifyRefreshToken(token: Token): IJWTPayload {
|
||||
@ -45,7 +48,7 @@ export class AuthService implements IAuthService {
|
||||
const { username, email, hashPassword } = registerData;
|
||||
|
||||
// Verificar si el usuario ya existe
|
||||
const userExists = await this.authUserRepo.userExists(email, transaction);
|
||||
const userExists = await this.authUserRepo.userExists(username, email, transaction);
|
||||
if (userExists.isSuccess && userExists.data) {
|
||||
return Result.fail(new Error("Email is already registered"));
|
||||
}
|
||||
@ -95,6 +98,7 @@ export class AuthService implements IAuthService {
|
||||
Error
|
||||
>
|
||||
> {
|
||||
let result: any;
|
||||
const { email, plainPassword, tabId } = loginData;
|
||||
|
||||
// Verificar que el tab ID está definido
|
||||
@ -103,12 +107,12 @@ export class AuthService implements IAuthService {
|
||||
}
|
||||
|
||||
// 🔹 Verificar si el usuario existe en la base de datos
|
||||
const userResult = await this.authUserRepo.getUserByEmail(email, transaction);
|
||||
if (userResult.isFailure) {
|
||||
result = await this.authUserRepo.getUserByEmail(email, transaction);
|
||||
if (result.isFailure) {
|
||||
return Result.fail(new Error("Invalid email or password"));
|
||||
}
|
||||
|
||||
const user = userResult.data;
|
||||
const user = result.data;
|
||||
|
||||
// 🔹 Verificar que la contraseña sea correcta
|
||||
const isValidPassword = await user.verifyPassword(plainPassword);
|
||||
@ -122,30 +126,26 @@ export class AuthService implements IAuthService {
|
||||
tabId: tabId,
|
||||
});
|
||||
|
||||
if (contextOrError.isFailure) {
|
||||
return Result.fail(new Error("Error creating user context"));
|
||||
// 🔹 Generar Access Token y Refresh Token
|
||||
const payloadOrError = JWTPayload.create({
|
||||
userId: user.id,
|
||||
email: email,
|
||||
tabId: tabId,
|
||||
//roles: ["USER"],
|
||||
});
|
||||
|
||||
result = Result.combine([contextOrError, payloadOrError]);
|
||||
if (result.isFailure) {
|
||||
return Result.fail(new Error("Error on login"));
|
||||
}
|
||||
|
||||
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 accessTokenOrError = this.generateAccessToken(payloadOrError.data);
|
||||
const refreshTokenOrError = this.generateRefreshToken(payloadOrError.data);
|
||||
|
||||
const refreshTokenOrError = this.generateRefreshToken({
|
||||
user_id: user.id.toString(),
|
||||
email: email.toString(),
|
||||
tab_id: tabId.toString(),
|
||||
roles: ["USER"],
|
||||
});
|
||||
|
||||
const result = Result.combine([accessTokenOrError, refreshTokenOrError]);
|
||||
result = Result.combine([accessTokenOrError, refreshTokenOrError]);
|
||||
|
||||
if (result.isFailure) {
|
||||
return Result.fail(result.error);
|
||||
|
||||
@ -2,7 +2,7 @@ import { Result, UniqueID } from "@common/domain";
|
||||
import { TabContext } from "../entities";
|
||||
|
||||
export interface ITabContextService {
|
||||
getContextByTabId(tabId: UniqueID): Promise<Result<TabContext, Error>>;
|
||||
getContextByTabId(tabId: UniqueID, transaction?: any): Promise<Result<TabContext, Error>>;
|
||||
createContext(
|
||||
params: { tabId: UniqueID; userId: UniqueID },
|
||||
transaction?: any
|
||||
|
||||
@ -1,10 +1,12 @@
|
||||
import { Result, ValueObject } from "@common/domain";
|
||||
import { logger } from "@common/infrastructure/logger";
|
||||
import { z } from "zod";
|
||||
|
||||
export const NULLED_EMAIL_ADDRESS = null;
|
||||
|
||||
export class EmailAddress extends ValueObject<string | null> {
|
||||
static create(email: string | null): Result<EmailAddress, Error> {
|
||||
logger.debug(`Creating EmailAddress from ${email}`);
|
||||
const normalizedEmail =
|
||||
email?.trim() === "" ? NULLED_EMAIL_ADDRESS : email?.toLowerCase() || NULLED_EMAIL_ADDRESS;
|
||||
|
||||
|
||||
@ -13,7 +13,7 @@ export class Token extends ValueObject<string> {
|
||||
}
|
||||
|
||||
private static validate(token: string) {
|
||||
const schema = z.string().min(319, { message: "Invalid token string" });
|
||||
const schema = z.string().min(1, { message: "Invalid token string" });
|
||||
return schema.safeParse(token);
|
||||
}
|
||||
}
|
||||
|
||||
@ -0,0 +1,10 @@
|
||||
import { AuthenticatedUser, TabContext } from "@contexts/auth/domain";
|
||||
import { Request } from "express";
|
||||
|
||||
export interface TabContextRequest extends Request {
|
||||
tabContext?: TabContext;
|
||||
}
|
||||
|
||||
export interface AuthenticatedRequest extends Request {
|
||||
user?: AuthenticatedUser;
|
||||
}
|
||||
@ -1,19 +1,16 @@
|
||||
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
|
||||
interface AuthenticatedRequest extends Request {
|
||||
user?: AuthenticatedUser;
|
||||
}
|
||||
//import { authProvider } from "@contexts/auth/infraestructure";
|
||||
import { NextFunction, Response } from "express";
|
||||
import { AuthenticatedRequest } from "../express/types";
|
||||
import { authProvider } from "../passport";
|
||||
|
||||
// Comprueba el rol del usuario
|
||||
const _authorizeUser = (condition: (user: AuthenticatedUser) => boolean) => {
|
||||
return (req: AuthenticatedRequest, res: Response, next: NextFunction) => {
|
||||
console.log(req.user);
|
||||
const user = req.user as AuthenticatedUser;
|
||||
|
||||
if (!user || !condition(user)) {
|
||||
return ExpressController.errorResponse(
|
||||
new ApiError({
|
||||
@ -25,16 +22,21 @@ const _authorizeUser = (condition: (user: AuthenticatedUser) => boolean) => {
|
||||
);
|
||||
}
|
||||
|
||||
//setAuthContext(req, res, user);
|
||||
return next();
|
||||
};
|
||||
};
|
||||
|
||||
// Verifica que el usuario esté autenticado
|
||||
export const checkUser = [authProvider.authenticateJWT(), _authorizeUser((user) => user.isUser)];
|
||||
export const checkUser = [
|
||||
authProvider.authenticateJWT(),
|
||||
_authorizeUser((user) => true /*user.isUser*/),
|
||||
];
|
||||
|
||||
// Verifica que el usuario sea administrador
|
||||
export const checkUserIsAdmin = [_authorizeUser((user) => user.isAdmin)];
|
||||
export const checkUserIsAdmin = [
|
||||
authProvider.authenticateJWT(),
|
||||
_authorizeUser((user) => user.isAdmin),
|
||||
];
|
||||
|
||||
// Middleware para verificar que el usuario sea administrador o el dueño de los datos (self)
|
||||
export const checkUserIsAdminOrOwner = [
|
||||
|
||||
@ -1,59 +1,3 @@
|
||||
import { UniqueID } from "@common/domain";
|
||||
import { ApiError, ExpressController } from "@common/presentation";
|
||||
import { TabContext } from "@contexts/auth/domain";
|
||||
import { NextFunction, Request, Response } from "express";
|
||||
import httpStatus from "http-status";
|
||||
import { authProvider } from "../passport";
|
||||
|
||||
// Extender el Request de Express para incluir el usuario autenticado optionalmente
|
||||
interface TabContextRequest extends Request {
|
||||
tabContext?: TabContext;
|
||||
}
|
||||
|
||||
export const validateTabContextHeader = async (
|
||||
req: TabContextRequest,
|
||||
res: Response,
|
||||
next: NextFunction
|
||||
) => {
|
||||
const tabId = String(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 tabIdOrError = UniqueID.create(tabId, false);
|
||||
if (tabIdOrError.isFailure) {
|
||||
return ExpressController.errorResponse(
|
||||
new ApiError({
|
||||
status: 422,
|
||||
title: httpStatus["422"],
|
||||
name: httpStatus["422_NAME"],
|
||||
detail: "Invalid Tab ID",
|
||||
}),
|
||||
res
|
||||
);
|
||||
}
|
||||
/*const contextOrError = await createTabContextService().getContextByTabId(tabIdOrError.data);
|
||||
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.tabContext = context;*/
|
||||
next();
|
||||
};
|
||||
export const checkTabContext = [authProvider.authenticateTabId()];
|
||||
|
||||
@ -1,11 +1,11 @@
|
||||
import { SequelizeTransactionManager } from "@common/infrastructure";
|
||||
import { AuthService, TabContextService } from "@contexts/auth/domain/services";
|
||||
import { authenticatedUserRepository, tabContextRepository } from "../sequelize";
|
||||
import { IJWTPayload, PassportAuthProvider } from "./passport-auth-provider";
|
||||
import { PassportAuthProvider } from "./passport-auth-provider";
|
||||
|
||||
const transactionManager = new SequelizeTransactionManager();
|
||||
const authService = new AuthService(authenticatedUserRepository, tabContextRepository);
|
||||
const tabContextService = new TabContextService(tabContextRepository);
|
||||
|
||||
const authProvider = new PassportAuthProvider(authService, tabContextService, transactionManager);
|
||||
export { authProvider, IJWTPayload };
|
||||
export { authProvider };
|
||||
|
||||
@ -1,59 +1,38 @@
|
||||
import { NextFunction, Response } from "express";
|
||||
|
||||
import { Result, UniqueID } from "@common/domain";
|
||||
import { ITransactionManager } from "@common/infrastructure/database";
|
||||
import { AuthenticatedUser, EmailAddress, PlainPassword } from "@contexts/auth/domain";
|
||||
import { logger } from "@common/infrastructure/logger";
|
||||
import { EmailAddress, TabContext } 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";
|
||||
import { TabContextRequest } from "../express/types";
|
||||
|
||||
const SECRET_KEY = process.env.JWT_SECRET || "supersecretkey";
|
||||
|
||||
export interface IJWTPayload {
|
||||
user_id: string;
|
||||
email: string;
|
||||
tab_id: string;
|
||||
roles: string[];
|
||||
}
|
||||
|
||||
export class PassportAuthProvider {
|
||||
private async _getUserByEmailAndPassword(
|
||||
email: string,
|
||||
password: string
|
||||
): Promise<Result<AuthenticatedUser, Error>> {
|
||||
const emailVO = EmailAddress.create(email);
|
||||
if (emailVO.isFailure) {
|
||||
return Result.fail(emailVO.error);
|
||||
private async _getContextByTabId(value: string): Promise<Result<TabContext, Error>> {
|
||||
const tabIdOrError = UniqueID.create(value, false);
|
||||
if (tabIdOrError.isFailure) {
|
||||
return Result.fail(new Error("Invalid tab ID"));
|
||||
}
|
||||
|
||||
const plainPasswordVO = PlainPassword.create(password);
|
||||
if (plainPasswordVO.isFailure) {
|
||||
return Result.fail(plainPasswordVO.error);
|
||||
const tabResult = await this.tabContextService.getContextByTabId(tabIdOrError.data);
|
||||
if (tabResult.isFailure) {
|
||||
return Result.fail(new Error("Invalid token data"));
|
||||
}
|
||||
|
||||
const userResult = await this.authService.getUserByEmail(emailVO.data);
|
||||
|
||||
if (userResult.isFailure || !userResult.data) {
|
||||
return Result.fail(new Error("Invalid email or password"));
|
||||
}
|
||||
|
||||
const user = userResult.data;
|
||||
const isValidPassword = await user.verifyPassword(plainPasswordVO.data);
|
||||
|
||||
if (!isValidPassword) {
|
||||
return Result.fail(new Error("Invalid email or password"));
|
||||
}
|
||||
|
||||
return Result.ok(user);
|
||||
return Result.ok(tabResult.data);
|
||||
}
|
||||
|
||||
private async _getUserAndContextByToken(token: IJWTPayload) {
|
||||
const { user_id, email, roles, tab_id } = token;
|
||||
private async _getUserByToken(tokenPayload: any) {
|
||||
const { user_id, email, roles } = tokenPayload;
|
||||
|
||||
const userIdVO = UniqueID.create(user_id);
|
||||
const tabIdVO = UniqueID.create(tab_id);
|
||||
const emailVO = EmailAddress.create(email!);
|
||||
|
||||
const okOrError = Result.combine([userIdVO, tabIdVO, emailVO]);
|
||||
const okOrError = Result.combine([userIdVO, emailVO]);
|
||||
if (okOrError.isFailure) {
|
||||
return Result.fail(okOrError.error.message);
|
||||
}
|
||||
@ -67,23 +46,13 @@ export class PassportAuthProvider {
|
||||
const user = userResult.data;
|
||||
|
||||
const checkUserId = user.id.equals(userIdVO.data);
|
||||
const checkRoles = user.hasRoles(roles);
|
||||
const checkRoles = true; //user.hasRoles(roles);
|
||||
|
||||
if (!checkUserId || !checkRoles) {
|
||||
return Result.fail(new Error("Invalid token data"));
|
||||
}
|
||||
|
||||
const tabResult = await this.tabContextService.getContextByTabId(tabIdVO.data);
|
||||
if (tabResult.isFailure) {
|
||||
return Result.fail(new Error("Invalid token data"));
|
||||
}
|
||||
|
||||
const tabContext = tabResult.data;
|
||||
|
||||
return Result.ok({
|
||||
user,
|
||||
tabContext,
|
||||
});
|
||||
return Result.ok(user);
|
||||
}
|
||||
|
||||
constructor(
|
||||
@ -105,37 +74,41 @@ export class PassportAuthProvider {
|
||||
"jwt",
|
||||
new JwtStrategy(jwtOptions, async (tokenPayload, done) => {
|
||||
try {
|
||||
const userOrError = await this._getUserAndContextByToken(tokenPayload);
|
||||
return userOrError.isSuccess
|
||||
? done(null, userOrError.data)
|
||||
: done(userOrError.error, false, { message: "Invalid JWT data" });
|
||||
const userOrError = await this._getUserByToken(tokenPayload);
|
||||
if (userOrError.isFailure) {
|
||||
return done(userOrError.error, false, { message: "Invalid JWT data" });
|
||||
}
|
||||
|
||||
return done(null, userOrError.data);
|
||||
} catch (error) {
|
||||
return done(error, false);
|
||||
}
|
||||
})
|
||||
);
|
||||
|
||||
passport.use(
|
||||
"email",
|
||||
new LocalStrategy(
|
||||
{ usernameField: "email", passwordField: "password" },
|
||||
async (email, password, done) => {
|
||||
try {
|
||||
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);
|
||||
}
|
||||
}
|
||||
)
|
||||
);
|
||||
|
||||
passport.initialize();
|
||||
}
|
||||
|
||||
authenticateJWT() {
|
||||
logger.debug("Authenticating JWT");
|
||||
return passport.authenticate("jwt", { session: false });
|
||||
}
|
||||
|
||||
authenticateTabId() {
|
||||
logger.debug("Authenticating Tab ID");
|
||||
return async (req: TabContextRequest, res: Response, next: NextFunction) => {
|
||||
const tabIdValue = req.header("X-Tab-ID");
|
||||
if (!tabIdValue) {
|
||||
return res.status(401).json({ message: "Tab ID is required" });
|
||||
}
|
||||
|
||||
const tabContextOrError = await this._getContextByTabId(tabIdValue);
|
||||
if (tabContextOrError.isFailure) {
|
||||
return res.status(401).json({ message: "Invalid tab context data" });
|
||||
}
|
||||
|
||||
req.tabContext = tabContextOrError.data;
|
||||
return next();
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@ -4,6 +4,7 @@ import {
|
||||
AuthenticatedUser,
|
||||
EmailAddress,
|
||||
IAuthenticatedUserRepository,
|
||||
Username,
|
||||
} from "@contexts/auth/domain";
|
||||
import { Transaction } from "sequelize";
|
||||
import { authenticatedUserMapper, IAuthenticatedUserMapper } from "../mappers";
|
||||
@ -20,7 +21,7 @@ class AuthenticatedUserRepository
|
||||
*/
|
||||
private _customErrorMapper(error: Error): string | null {
|
||||
if (error.name === "SequelizeUniqueConstraintError") {
|
||||
return "User with this email already exists";
|
||||
return "User with this email or username already exists";
|
||||
}
|
||||
|
||||
return null;
|
||||
@ -32,18 +33,26 @@ class AuthenticatedUserRepository
|
||||
}
|
||||
|
||||
async userExists(
|
||||
username: Username,
|
||||
email: EmailAddress,
|
||||
transaction?: Transaction
|
||||
): Promise<Result<boolean, Error>> {
|
||||
try {
|
||||
const rawUser: any = await this._findById(
|
||||
const userWithEmail = await this._findById(
|
||||
AuthUserModel,
|
||||
"email",
|
||||
email.toString(),
|
||||
transaction
|
||||
);
|
||||
|
||||
return Result.ok(Boolean(rawUser));
|
||||
const userWithUsername = await this._findById(
|
||||
AuthUserModel,
|
||||
"username",
|
||||
username.toString(),
|
||||
transaction
|
||||
);
|
||||
|
||||
return Result.ok(Boolean(userWithEmail || userWithUsername));
|
||||
} catch (error: any) {
|
||||
return this._handleDatabaseError(error, this._customErrorMapper);
|
||||
}
|
||||
|
||||
@ -13,7 +13,7 @@ class UserRepository extends SequelizeRepository<User> implements IUserRepositor
|
||||
*/
|
||||
private _customErrorMapper(error: Error): string | null {
|
||||
if (error.name === "SequelizeUniqueConstraintError") {
|
||||
return "User with this email already exists";
|
||||
return "User with this email or username already exists";
|
||||
}
|
||||
|
||||
return null;
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import { AuthenticatedUser, TabContext } from "@contexts/auth/domain";
|
||||
import { AuthenticatedUser, TabContext, Token } from "@contexts/auth/domain";
|
||||
import { ILoginUserResponseDTO } from "../../dto";
|
||||
|
||||
export interface ILoginPresenter {
|
||||
@ -6,8 +6,8 @@ export interface ILoginPresenter {
|
||||
user: AuthenticatedUser;
|
||||
tabContext: TabContext;
|
||||
tokens: {
|
||||
accessToken: string;
|
||||
refreshToken: string;
|
||||
accessToken: Token;
|
||||
refreshToken: Token;
|
||||
};
|
||||
}) => ILoginUserResponseDTO;
|
||||
}
|
||||
@ -17,8 +17,8 @@ export const loginPresenter: ILoginPresenter = {
|
||||
user: AuthenticatedUser;
|
||||
tabContext: TabContext;
|
||||
tokens: {
|
||||
accessToken: string;
|
||||
refreshToken: string;
|
||||
accessToken: Token;
|
||||
refreshToken: Token;
|
||||
};
|
||||
}): ILoginUserResponseDTO => {
|
||||
const {
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
import { ExpressController } from "@common/presentation";
|
||||
import { LogoutUseCase } from "@contexts/auth/application/logout";
|
||||
import { LogoutData } from "@contexts/auth/domain";
|
||||
import { AuthenticatedUser, LogoutData, TabContext } from "@contexts/auth/domain";
|
||||
|
||||
export class LogoutController extends ExpressController {
|
||||
public constructor(private readonly logout: LogoutUseCase) {
|
||||
@ -8,9 +8,12 @@ export class LogoutController extends ExpressController {
|
||||
}
|
||||
|
||||
async executeImpl() {
|
||||
const logoutDataOrError = LogoutData.createFromPrimitives({
|
||||
email: this.req.body.email,
|
||||
tabId: String(this.req.headers["x-tab-id"]),
|
||||
const user = this.req.user as AuthenticatedUser;
|
||||
const tabContext = this.req.tabContext as TabContext;
|
||||
|
||||
const logoutDataOrError = LogoutData.create({
|
||||
email: user.email,
|
||||
tabId: tabContext.tabId,
|
||||
});
|
||||
|
||||
if (logoutDataOrError.isFailure) {
|
||||
@ -23,6 +26,8 @@ export class LogoutController extends ExpressController {
|
||||
return this.handleError(logoutOrError.error);
|
||||
}
|
||||
|
||||
// Habría que invalidar el token del cliente
|
||||
|
||||
return this.ok();
|
||||
}
|
||||
|
||||
|
||||
@ -1,4 +1,5 @@
|
||||
import { SequelizeTransactionManager } from "@common/infrastructure";
|
||||
import { RefreshTokenUseCase } from "@contexts/auth/application";
|
||||
import { AuthService } from "@contexts/auth/domain/services";
|
||||
import { authenticatedUserRepository, tabContextRepository } from "@contexts/auth/infraestructure";
|
||||
import { RefreshTokenController } from "./refresh-token.controller";
|
||||
|
||||
@ -19,7 +19,7 @@ export class RefreshTokenController extends ExpressController {
|
||||
return this.clientError("Invalid input data", refreshTokenOrError.error);
|
||||
}
|
||||
|
||||
const newRefreshTokenOrError = this.refreshToken.execute(refreshTokenOrError.data);
|
||||
const newRefreshTokenOrError = await this.refreshToken.execute(refreshTokenOrError.data);
|
||||
|
||||
if (newRefreshTokenOrError.isFailure) {
|
||||
return this.handleError(newRefreshTokenOrError.error);
|
||||
@ -31,10 +31,6 @@ export class RefreshTokenController extends ExpressController {
|
||||
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")
|
||||
|
||||
@ -2,13 +2,11 @@ import { Token } from "@contexts/auth/domain";
|
||||
import { IRefreshTokenResponseDTO } from "../../dto";
|
||||
|
||||
export interface IRefreshTokenPresenter {
|
||||
toDto: (data: { refreshToken: Token }) => IRefreshTokenResponseDTO;
|
||||
toDto: (refreshToken: Token) => IRefreshTokenResponseDTO;
|
||||
}
|
||||
|
||||
export const refreshTokenPresenter: IRefreshTokenPresenter = {
|
||||
toDto: (data: { refreshToken: Token }): IRefreshTokenResponseDTO => {
|
||||
const { refreshToken } = data;
|
||||
|
||||
toDto: (refreshToken: Token): IRefreshTokenResponseDTO => {
|
||||
return {
|
||||
refresh_token: refreshToken.toString(),
|
||||
};
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
import { SequelizeTransactionManager } from "@common/infrastructure";
|
||||
import { RefreshTokenUseCase } from "@contexts/auth/application/register";
|
||||
import { RegisterUseCase } from "@contexts/auth/application/register";
|
||||
import { AuthService } from "@contexts/auth/domain/services";
|
||||
import { authenticatedUserRepository, tabContextRepository } from "@contexts/auth/infraestructure";
|
||||
import { RegisterController } from "./register.controller";
|
||||
@ -9,7 +9,7 @@ export const registerController = () => {
|
||||
const transactionManager = new SequelizeTransactionManager();
|
||||
const authService = new AuthService(authenticatedUserRepository, tabContextRepository);
|
||||
|
||||
const useCase = new RefreshTokenUseCase(authService, transactionManager);
|
||||
const useCase = new RegisterUseCase(authService, transactionManager);
|
||||
const presenter = registerPresenter;
|
||||
|
||||
return new RegisterController(useCase, presenter);
|
||||
|
||||
@ -1,11 +1,11 @@
|
||||
import { ExpressController } from "@common/presentation";
|
||||
import { RefreshTokenUseCase } from "@contexts/auth/application/register";
|
||||
import { RegisterUseCase } from "@contexts/auth/application";
|
||||
import { RegisterData } from "@contexts/auth/domain";
|
||||
import { IRegisterPresenter } from "./register.presenter";
|
||||
|
||||
export class RegisterController extends ExpressController {
|
||||
public constructor(
|
||||
private readonly register: RefreshTokenUseCase,
|
||||
private readonly register: RegisterUseCase,
|
||||
private readonly presenter: IRegisterPresenter
|
||||
) {
|
||||
super();
|
||||
@ -34,7 +34,7 @@ export class RegisterController extends ExpressController {
|
||||
private handleError(error: Error) {
|
||||
const message = error.message;
|
||||
|
||||
if (message.includes("User with this email already exists")) {
|
||||
if (message.includes("User with this email or username already exists")) {
|
||||
return this.conflictError(message);
|
||||
}
|
||||
|
||||
|
||||
@ -1,8 +1,9 @@
|
||||
import { validateRequestDTO } from "@common/presentation";
|
||||
import { checkUser, validateTabContextHeader } from "@contexts/auth/infraestructure";
|
||||
import { checkTabContext, checkUser } from "@contexts/auth/infraestructure";
|
||||
import {
|
||||
loginController,
|
||||
logoutController,
|
||||
refreshTokenController,
|
||||
registerController,
|
||||
} from "@contexts/auth/presentation/controllers";
|
||||
import {
|
||||
@ -50,7 +51,7 @@ export const authRouter = (appRouter: Router) => {
|
||||
authRoutes.post(
|
||||
"/login",
|
||||
validateRequestDTO(LoginUserSchema),
|
||||
validateTabContextHeader,
|
||||
checkTabContext,
|
||||
(req: Request, res: Response, next: NextFunction) => {
|
||||
loginController().execute(req, res, next);
|
||||
}
|
||||
@ -69,7 +70,7 @@ export const authRouter = (appRouter: Router) => {
|
||||
*/
|
||||
authRoutes.post(
|
||||
"/logout",
|
||||
validateTabContextHeader,
|
||||
checkTabContext,
|
||||
checkUser,
|
||||
(req: Request, res: Response, next: NextFunction) => {
|
||||
logoutController().execute(req, res, next);
|
||||
@ -79,6 +80,7 @@ export const authRouter = (appRouter: Router) => {
|
||||
authRoutes.post(
|
||||
"/refresh",
|
||||
validateRequestDTO(RefreshTokenSchema),
|
||||
checkTabContext,
|
||||
(req: Request, res: Response, next: NextFunction) => {
|
||||
refreshTokenController().execute(req, res, next);
|
||||
}
|
||||
|
||||
@ -1,21 +1,15 @@
|
||||
import { validateRequestDTO } from "@common/presentation";
|
||||
import { createAuthProvider } from "@contexts/auth/infraestructure";
|
||||
import {
|
||||
listUsersController,
|
||||
ListUsersSchema,
|
||||
validateTabContextHeader,
|
||||
} from "@contexts/auth/presentation";
|
||||
import { listUsersController, ListUsersSchema } 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(),
|
||||
//validateTabContextHeader,
|
||||
//authProvider.authenticateJWT(),
|
||||
//authProvider.checkIsAdmin(),
|
||||
async (req: Request, res: Response, next: NextFunction) => {
|
||||
listUsersController().execute(req, res, next);
|
||||
|
||||
902
pnpm-lock.yaml
902
pnpm-lock.yaml
File diff suppressed because it is too large
Load Diff
Loading…
Reference in New Issue
Block a user