This commit is contained in:
David Arranz 2025-02-16 20:30:20 +01:00
parent 80db9dfb9e
commit cf38a2ad9d
37 changed files with 1189 additions and 253 deletions

View File

@ -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) => {

View File

@ -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;

View File

@ -1,4 +1,5 @@
export * from "./list-users";
export * from "./login";
export * from "./logout";
export * from "./refresh-token";
export * from "./register";

View File

@ -1 +1 @@
export * from "./login.use-case";
export * from "./refresh-token.use-case";

View File

@ -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,
});
});
}

View File

@ -1 +1 @@
export * from "./refresh-token.use-case";
export * from "./register.use-case";

View File

@ -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);
});
}
}

View File

@ -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 {

View File

@ -1,3 +1,4 @@
export * from "./jwt-payload";
export * from "./login-data";
export * from "./logout-data";
export * from "./register-data";

View 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(),
};
}
}

View File

@ -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]);

View File

@ -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]);

View File

@ -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);

View File

@ -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(),

View File

@ -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>>;
}

View File

@ -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>;

View File

@ -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);

View File

@ -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

View File

@ -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;

View File

@ -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);
}
}

View File

@ -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;
}

View File

@ -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 = [

View File

@ -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()];

View File

@ -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 };

View File

@ -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();
};
}
}

View File

@ -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);
}

View File

@ -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;

View File

@ -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 {

View File

@ -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();
}

View File

@ -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";

View File

@ -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")

View File

@ -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(),
};

View File

@ -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);

View File

@ -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);
}

View File

@ -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);
}

View File

@ -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);

File diff suppressed because it is too large Load Diff