This commit is contained in:
David Arranz 2025-02-07 12:14:36 +01:00
parent bc6e4e594f
commit eef6e6fa97
18 changed files with 173 additions and 154 deletions

1
.gitignore vendored
View File

@ -38,6 +38,7 @@ error-*.log
# Misc
.DS_Store
*.pem
*-audit.json
#Jetbrains
.idea

View File

@ -1,6 +1,6 @@
import { logger } from "@common/infrastructure/logger";
import { globalErrorHandler } from "@common/presentation";
import { initializePassportAuthProvide } from "@contexts/auth/infraestructure";
import { createAuthProvider } from "@contexts/auth/infraestructure";
import dotenv from "dotenv";
import express, { Application } from "express";
import helmet from "helmet";
@ -24,9 +24,9 @@ export function createApp(): Application {
app.use(responseTime()); // set up the response-time middleware
// Inicializar Passport
// Inicializar Auth Provider
app.use((req, res, next) => {
initializePassportAuthProvide();
createAuthProvider().initialize();
next();
});

View File

@ -1,10 +1,17 @@
import { Result, UniqueID } from "@common/domain";
import { AuthenticatedUser, EmailAddress, HashPassword, PlainPassword, Username } from "../domain";
import {
AuthenticatedUser,
EmailAddress,
HashPassword,
PlainPassword,
TabContext,
Username,
} from "../domain";
import { IJWTPayload } from "../infraestructure";
export interface IAuthService {
generateAccessToken(payload: object): string;
generateRefreshToken(payload: object): string;
verifyToken(token: string): Promise<AuthenticatedUser | null>;
generateAccessToken(payload: IJWTPayload): string;
generateRefreshToken(payload: IJWTPayload): string;
registerUser(params: {
username: Username;
@ -20,6 +27,7 @@ export interface IAuthService {
Result<
{
user: AuthenticatedUser;
tabContext: TabContext;
tokens: {
accessToken: string;
refreshToken: string;
@ -31,8 +39,5 @@ export interface IAuthService {
logoutUser(params: { email: EmailAddress; tabId: UniqueID }): Promise<Result<void, Error>>;
verifyUser(params: {
email: EmailAddress;
plainPassword: PlainPassword;
}): Promise<Result<AuthenticatedUser, Error>>;
getUserByEmail(params: { email: EmailAddress }): Promise<Result<AuthenticatedUser, Error>>;
}

View File

@ -1,19 +1,18 @@
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 { JwtHelper } from "../infraestructure/passport/jwt.helper";
import { IJWTPayload } from "../infraestructure/passport/passport-auth-provider";
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";
@ -32,16 +31,12 @@ export class AuthService implements IAuthService {
this._transactionManager = transactionManager;
}
generateAccessToken(payload: object): string {
return jwt.sign(payload, SECRET_KEY, { expiresIn: ACCESS_EXPIRATION });
generateAccessToken(payload: IJWTPayload): string {
return JwtHelper.generateToken(payload, 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);
generateRefreshToken(payload: IJWTPayload): string {
return JwtHelper.generateToken(payload, REFRESH_EXPIRATION);
}
/**
@ -104,6 +99,7 @@ export class AuthService implements IAuthService {
Result<
{
user: AuthenticatedUser;
tabContext: TabContext;
tokens: {
accessToken: string;
refreshToken: string;
@ -145,7 +141,9 @@ export class AuthService implements IAuthService {
return Result.fail(new Error("Error creating user context"));
}
await this._tabContactRepo.registerContextByTabId(contextOrError.data, transaction);
const tabContext = contextOrError.data;
await this._tabContactRepo.registerContextByTabId(tabContext, transaction);
// 🔹 Generar Access Token y Refresh Token
const accessToken = this.generateAccessToken({
@ -157,10 +155,14 @@ export class AuthService implements IAuthService {
const refreshToken = this.generateRefreshToken({
userId: user.id.toString(),
email: email.toString(),
tabId: tabId.toString(),
roles: ["USER"],
});
return Result.ok({
user,
tabContext,
tokens: {
accessToken,
refreshToken,
@ -213,27 +215,17 @@ export class AuthService implements IAuthService {
}
}
async verifyUser(params: {
email: EmailAddress;
plainPassword: PlainPassword;
}): Promise<Result<AuthenticatedUser, Error>> {
async getUserByEmail(params: { email: EmailAddress }): Promise<Result<AuthenticatedUser, Error>> {
try {
return await this._transactionManager.complete(async (transaction) => {
const { email, plainPassword } = params;
const { email } = 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);
return Result.ok(userResult.data);
});
} catch (error: unknown) {
return Result.fail(error as Error);

View File

@ -19,9 +19,9 @@ export interface IAuthenticatedUser {
isUser: boolean;
isAdmin: boolean;
contexts: ICollection<QuoteItem>;
verifyPassword(candidatePassword: PlainPassword): Promise<boolean>;
hasRole(role: string): boolean;
hasRoles(roles: string[]): boolean;
getRoles(): string[];
toPersistenceData(): any;
}
@ -43,10 +43,6 @@ export class AuthenticatedUser
return Result.ok(user);
}
private _hasRole(role: string): boolean {
return (this._props.roles || []).some((r) => r === role);
}
verifyPassword(candidatePassword: PlainPassword): Promise<boolean> {
return this._props.hashPassword.verifyPassword(candidatePassword.toString());
}
@ -55,6 +51,14 @@ export class AuthenticatedUser
return this._props.roles;
}
hasRole(role: string): boolean {
return (this._props.roles || []).some((r) => r === role);
}
hasRoles(roles: string[]): boolean {
return roles.map((rol) => this.hasRole(rol)).some((value) => value != false);
}
get username(): Username {
return this._props.username;
}
@ -64,11 +68,11 @@ export class AuthenticatedUser
}
get isUser(): boolean {
return this._hasRole("user");
return this.hasRole("user");
}
get isAdmin(): boolean {
return this._hasRole("admin");
return this.hasRole("admin");
}
/**

View File

@ -1,4 +1,3 @@
export * from "./jwt.helper";
export * from "./mappers";
export * from "./passport";
export * from "./sequelize";

View File

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

View File

@ -1,6 +1,8 @@
import jwt from "jsonwebtoken";
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 JwtHelper {
static generateToken(payload: object, expiresIn = "1h"): string {

View File

@ -1,4 +1,6 @@
import { Result, UniqueID } from "@common/domain";
import { IAuthService } from "@contexts/auth/application";
import { ITabContextService } from "@contexts/auth/application/tab-context-service.interface";
import { AuthenticatedUser, EmailAddress, PlainPassword } from "@contexts/auth/domain";
import passport from "passport";
import { ExtractJwt, Strategy as JwtStrategy } from "passport-jwt";
@ -6,35 +8,100 @@ import { Strategy as LocalStrategy } from "passport-local";
const SECRET_KEY = process.env.JWT_SECRET || "supersecretkey";
export interface IJWTPayload {
userId: string;
email: string;
tabId: string;
roles: string[];
}
export class PassportAuthProvider {
private readonly _authService: IAuthService;
private readonly _tabContextService: ITabContextService;
private async _verifyUser(email: string, password: string): Promise<AuthenticatedUser | null> {
private async _getUserByEmailAndPassword(
email: string,
password: string
): Promise<Result<AuthenticatedUser, Error>> {
const emailVO = EmailAddress.create(email);
if (emailVO.isFailure) return Promise.resolve(null);
if (emailVO.isFailure) {
return Result.fail(emailVO.error);
}
const plainPasswordVO = PlainPassword.create(password);
if (plainPasswordVO.isFailure) return Promise.resolve(null);
if (plainPasswordVO.isFailure) {
return Result.fail(plainPasswordVO.error);
}
const userResult = await this._authService.verifyUser({
const userResult = await this._authService.getUserByEmail({
email: emailVO.data,
plainPassword: plainPasswordVO.data,
});
if (userResult.isFailure || !userResult.data) {
return Promise.resolve(null);
return Result.fail(new Error("Invalid email or password"));
}
const user = userResult.data;
const isValidPassword = await user.verifyPassword(plainPasswordVO.data);
return !isValidPassword ? Promise.resolve(null) : Promise.resolve(user);
if (!isValidPassword) {
return Result.fail(new Error("Invalid email or password"));
}
return Result.ok(user);
}
private async _getUserAndContextByToken(token: IJWTPayload) {
const { userId, email, roles, tabId } = token;
const userIdVO = UniqueID.create(userId);
const tabIdVO = UniqueID.create(tabId);
const emailVO = EmailAddress.create(email!);
const okOrError = Result.combine([userIdVO, tabIdVO, emailVO]);
if (okOrError.isFailure) {
return Result.fail(okOrError.error.message);
}
const userResult = await this._authService.getUserByEmail({
email: emailVO.data,
});
if (userResult.isFailure) {
return Result.fail(new Error("Invalid token data"));
}
const user = userResult.data;
const checkUserId = user.id.equals(userIdVO.data);
const checkRoles = 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,
});
}
constructor(authService: IAuthService, tabContextService: ITabContextService) {
this._authService = authService;
this._tabContextService = tabContextService;
}
/**
* 🔹 Configura PassportJS
*/
initializePassport(): void {
initialize(): void {
const jwtOptions = {
jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
secretOrKey: SECRET_KEY,
@ -42,10 +109,12 @@ export class PassportAuthProvider {
passport.use(
"jwt",
new JwtStrategy(jwtOptions, (tokenPayload, done) => {
new JwtStrategy(jwtOptions, async (tokenPayload, done) => {
try {
console.log(tokenPayload);
return done(null, tokenPayload);
const result = await this._getUserAndContextByToken(tokenPayload);
return result.isSuccess
? done(null, result)
: done(result.error, false, { message: "Invalid JWT data" });
} catch (error) {
return done(error, false);
}
@ -58,7 +127,7 @@ export class PassportAuthProvider {
{ usernameField: "email", passwordField: "password" },
async (email, password, done) => {
try {
const user = await this._verifyUser(email, password);
const user = await this._getUserByEmailAndPassword(email, password);
return user
? done(null, user)
: done(null, false, { message: "Invalid email or password" });
@ -72,8 +141,7 @@ export class PassportAuthProvider {
passport.initialize();
}
constructor(authService: IAuthService) {
this._authService = authService;
this.initializePassport();
authenticateJWT() {
return passport.authenticate("jwt", { session: false });
}
}

View File

@ -1,19 +1,6 @@
import {
DataTypes,
InferAttributes,
InferCreationAttributes,
Model,
NonAttribute,
Sequelize,
} from "sequelize";
import { TabContextCreationAttributes, TabContextModel } from "./tab-context.model";
import { DataTypes, InferAttributes, InferCreationAttributes, Model, Sequelize } from "sequelize";
export type AuthUserCreationAttributes = InferCreationAttributes<
AuthUserModel,
{ omit: "contexts" }
> & {
contexts: TabContextCreationAttributes[];
};
export type AuthUserCreationAttributes = InferCreationAttributes<AuthUserModel>;
export class AuthUserModel extends Model<
InferAttributes<AuthUserModel>,
@ -24,22 +11,11 @@ export class AuthUserModel extends Model<
return Promise.resolve();
}*/
static associate(connection: Sequelize) {
const { TabContextModel } = connection.models;
AuthUserModel.hasMany(TabContextModel, {
as: "contexts",
foreignKey: "user_id",
onDelete: "CASCADE",
});
}
declare id: string;
declare username: string;
declare email: string;
declare hash_password: string;
declare roles: string[];
declare contexts: NonAttribute<TabContextModel[]>;
}
export default (sequelize: Sequelize) => {

View File

@ -1,21 +1,10 @@
import {
DataTypes,
InferAttributes,
InferCreationAttributes,
Model,
NonAttribute,
Sequelize,
} from "sequelize";
import { AuthUserModel } from "./auth-user.model";
import { DataTypes, InferAttributes, InferCreationAttributes, Model, Sequelize } from "sequelize";
export type TabContextCreationAttributes = InferCreationAttributes<
TabContextModel,
{ omit: "user" }
>;
export type TabContextCreationAttributes = InferCreationAttributes<TabContextModel>;
export class TabContextModel extends Model<
InferAttributes<TabContextModel, { omit: "user" }>,
InferCreationAttributes<TabContextModel, { omit: "user" }>
InferAttributes<TabContextModel>,
InferCreationAttributes<TabContextModel>
> {
// To avoid table creation
/*static async sync(): Promise<any> {
@ -23,21 +12,11 @@ export class TabContextModel extends Model<
}*/
static associate(connection: Sequelize) {
const { AuthUserModel } = connection.models;
TabContextModel.belongsTo(AuthUserModel, {
as: "user",
foreignKey: "user_id",
onDelete: "CASCADE",
});
}
declare id: string;
declare tab_id: string;
declare user_id: string;
declare company_id: string;
declare branch_id: string;
declare user: NonAttribute<AuthUserModel>;
}
export default (sequelize: Sequelize) => {
@ -55,14 +34,6 @@ export default (sequelize: Sequelize) => {
type: DataTypes.UUID,
allowNull: false,
},
company_id: {
type: DataTypes.UUID,
allowNull: false,
},
branch_id: {
type: DataTypes.UUID,
allowNull: false,
},
},
{
sequelize,

View File

@ -26,17 +26,17 @@ class LoginController extends ExpressController {
return this.clientError("Invalid input data", resultValidation.error);
}
const userOrError = await this._authService.loginUser({
const loginResultOrError = await this._authService.loginUser({
email: emailVO.data,
plainPassword: plainPasswordVO.data,
tabId: tabIdVO.data,
});
if (userOrError.isFailure) {
return this.unauthorizedError(userOrError.error.message);
if (loginResultOrError.isFailure) {
return this.unauthorizedError(loginResultOrError.error.message);
}
return this.created(this._presenter.map(userOrError.data));
return this.created(this._presenter.map(loginResultOrError.data));
}
}

View File

@ -1,9 +1,10 @@
import { AuthenticatedUser } from "@contexts/auth/domain";
import { AuthenticatedUser, TabContext } from "@contexts/auth/domain";
import { ILoginUserResponseDTO } from "../../dto";
export interface ILoginPresenter {
map: (data: {
user: AuthenticatedUser;
tabContext: TabContext;
tokens: {
accessToken: string;
refreshToken: string;
@ -14,6 +15,7 @@ export interface ILoginPresenter {
export const LoginPresenter: ILoginPresenter = {
map: (data: {
user: AuthenticatedUser;
tabContext: TabContext;
tokens: {
accessToken: string;
refreshToken: string;
@ -21,16 +23,20 @@ export const LoginPresenter: ILoginPresenter = {
}): ILoginUserResponseDTO => {
const {
user,
tabContext,
tokens: { accessToken, refreshToken },
} = data;
const userData = user.toPersistenceData();
const tabContextData = tabContext.toPersistenceData();
return {
user: {
id: userData.id,
email: userData.email,
username: userData.username,
tab_id: tabContextData.tab_id,
},
tokens: {
access_token: accessToken,

View File

@ -8,7 +8,3 @@ export interface ILoginUserRequestDTO {
email: string;
password: string;
}
export interface ISelectCompanyRequestDTO {
companyId: string;
}

View File

@ -9,6 +9,7 @@ export interface ILoginUserResponseDTO {
id: string;
username: string;
email: string;
tab_id: string;
};
tokens: {
access_token: string;
@ -17,6 +18,4 @@ export interface ILoginUserResponseDTO {
//tab_id: string;
}
export interface ILogoutResponseDTO {
message: string;
}
export interface ILogoutResponseDTO {}

View File

@ -2,19 +2,16 @@ import { UniqueID } from "@common/domain";
import { ApiError, ExpressController } from "@common/presentation";
import { AuthenticatedUser } from "@contexts/auth/domain";
import { NextFunction, Request, Response } from "express";
import passport from "passport";
// Extender el Request de Express para incluir el usuario autenticado optionalmente
interface AuthenticatedRequest extends Request {
user?: AuthenticatedUser;
}
// Middleware para autenticar usando passport con el local-jwt strategy
const _authenticateJwt = passport.authenticate("jwt", { session: false });
// 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(
@ -32,15 +29,19 @@ const _authorizeUser = (condition: (user: AuthenticatedUser) => boolean) => {
};
};
// Middleware para autenticar usando passport con el local-jwt strategy
//export const authenticateJWT = [];
//export const validateUserRegister = [_authenticateEmail];
// Verifica que el usuario esté autenticado
export const validateUser = [_authenticateJwt, _authorizeUser((user) => user.isUser)];
export const authenticateUser = [_authorizeUser((user) => user.isUser)];
// Verifica que el usuario sea administrador
export const validateUserIsAdmin = [_authenticateJwt, _authorizeUser((user) => user.isAdmin)];
export const authenticateUserIsAdmin = [_authorizeUser((user) => user.isAdmin)];
// Middleware para verificar que el usuario sea administrador o el dueño de los datos (self)
export const validateUserIsAdminOrOwner = [
_authenticateJwt,
export const checkUserIsAdminOrOwner = [
(req: AuthenticatedRequest, res: Response, next: NextFunction) => {
const user = req.user as AuthenticatedUser;
const { userId } = req.params;

View File

@ -1,6 +1,5 @@
import { UniqueID } from "@common/domain";
import { ApiError, ExpressController } from "@common/presentation";
import { createTabContextService } from "@contexts/auth/application";
import { TabContext } from "@contexts/auth/domain";
import { NextFunction, Request, Response } from "express";
import httpStatus from "http-status";
@ -40,7 +39,7 @@ export const validateTabContextHeader = async (
res
);
}
const contextOrError = await createTabContextService().getContextByTabId(tabIdOrError.data);
/*const contextOrError = await createTabContextService().getContextByTabId(tabIdOrError.data);
if (contextOrError.isFailure) {
return ExpressController.errorResponse(
new ApiError({
@ -55,6 +54,6 @@ export const validateTabContextHeader = async (
const context = contextOrError.data;
req.tabContext = context;
req.tabContext = context;*/
next();
};

View File

@ -1,5 +1,6 @@
import { validateRequest } from "@common/presentation";
import { validateTabContextHeader, validateUser } from "@contexts/auth/presentation";
import { validateRequestDTO } from "@common/presentation";
import { createAuthProvider } from "@contexts/auth/infraestructure";
import { validateTabContextHeader } 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";
@ -8,6 +9,7 @@ import { NextFunction, Request, Response, Router } from "express";
export const authRouter = (appRouter: Router) => {
const authRoutes: Router = Router({ mergeParams: true });
const authProvider = createAuthProvider();
/**
* @api {post} /api/auth/register Register a new user
@ -23,7 +25,7 @@ export const authRouter = (appRouter: Router) => {
*
* @apiError (400) {String} message Error message.
*/
authRoutes.post("/register", validateRequest(RegisterUserSchema), (req, res, next) => {
authRoutes.post("/register", validateRequestDTO(RegisterUserSchema), (req, res, next) => {
createRegisterController().execute(req, res, next);
});
@ -44,7 +46,7 @@ export const authRouter = (appRouter: Router) => {
*/
authRoutes.post(
"/login",
validateRequest(LoginUserSchema),
validateRequestDTO(LoginUserSchema),
validateTabContextHeader,
(req: Request, res: Response, next: NextFunction) => {
createLoginController().execute(req, res, next);
@ -64,8 +66,8 @@ export const authRouter = (appRouter: Router) => {
*/
authRoutes.post(
"/logout",
validateUser,
validateTabContextHeader,
authProvider.authenticateJWT(),
(req: Request, res: Response, next: NextFunction) => {
createLogoutController().execute(req, res, next);
}