From 4e39aacedff59290d2d8ad5ffc6cb87a715ec1fa Mon Sep 17 00:00:00 2001 From: david Date: Fri, 7 Feb 2025 18:46:41 +0100 Subject: [PATCH] Refresh token --- .../application/auth-service.interface.ts | 1 + .../contexts/auth/application/auth.service.ts | 12 +++-- .../domain/value-objects/email-address.ts | 9 +++- .../infraestructure/passport/jwt.helper.ts | 2 - .../passport/passport-auth-provider.ts | 4 +- .../auth/presentation/controllers/index.ts | 2 + .../controllers/login/login.controller.ts | 4 ++ .../controllers/logout/logout.controller.ts | 4 ++ .../controllers/refreshToken/index.ts | 1 + .../refreshToken/refresh-token.controller.ts | 40 +++++++++++++++ .../refreshToken/refresh-token.presenter.ts | 15 ++++++ .../register/register.controller.ts | 4 ++ .../presentation/dto/auth.response.dto.ts | 5 +- .../presentation/dto/auth.validation.dto.ts | 4 +- .../src/contexts/auth/presentation/index.ts | 1 - .../application/company-service.interface.ts | 0 .../companies/application/company.service.ts | 0 .../contexts/companies/application/index.ts | 0 .../src/contexts/companies/domain/index.ts | 5 ++ .../company-repository.interface.ts | 1 + .../companies/domain/repositories/index.ts | 1 + .../companies/infraestructure/index.ts | 3 ++ apps/server/src/routes/auth.routes.ts | 21 +++++++- apps/server/src/routes/company.routes.ts | 49 +++++++++++++++++++ 24 files changed, 173 insertions(+), 15 deletions(-) create mode 100644 apps/server/src/contexts/auth/presentation/controllers/refreshToken/index.ts create mode 100644 apps/server/src/contexts/auth/presentation/controllers/refreshToken/refresh-token.controller.ts create mode 100644 apps/server/src/contexts/auth/presentation/controllers/refreshToken/refresh-token.presenter.ts create mode 100644 apps/server/src/contexts/companies/application/company-service.interface.ts create mode 100644 apps/server/src/contexts/companies/application/company.service.ts create mode 100644 apps/server/src/contexts/companies/application/index.ts create mode 100644 apps/server/src/contexts/companies/domain/index.ts create mode 100644 apps/server/src/contexts/companies/domain/repositories/company-repository.interface.ts create mode 100644 apps/server/src/contexts/companies/domain/repositories/index.ts create mode 100644 apps/server/src/contexts/companies/infraestructure/index.ts create mode 100644 apps/server/src/routes/company.routes.ts diff --git a/apps/server/src/contexts/auth/application/auth-service.interface.ts b/apps/server/src/contexts/auth/application/auth-service.interface.ts index f2a72071..238dd1cd 100644 --- a/apps/server/src/contexts/auth/application/auth-service.interface.ts +++ b/apps/server/src/contexts/auth/application/auth-service.interface.ts @@ -12,6 +12,7 @@ import { IJWTPayload } from "../infraestructure"; export interface IAuthService { generateAccessToken(payload: IJWTPayload): string; generateRefreshToken(payload: IJWTPayload): string; + verifyRefreshToken(token: string): IJWTPayload; registerUser(params: { username: Username; diff --git a/apps/server/src/contexts/auth/application/auth.service.ts b/apps/server/src/contexts/auth/application/auth.service.ts index 95092085..e977b5a9 100644 --- a/apps/server/src/contexts/auth/application/auth.service.ts +++ b/apps/server/src/contexts/auth/application/auth.service.ts @@ -39,6 +39,10 @@ export class AuthService implements IAuthService { return JwtHelper.generateToken(payload, REFRESH_EXPIRATION); } + verifyRefreshToken(token: string): IJWTPayload { + return JwtHelper.verifyToken(token); + } + /** * * Registra un nuevo usuario en la base de datos bajo transacción. @@ -147,16 +151,16 @@ export class AuthService implements IAuthService { // 🔹 Generar Access Token y Refresh Token const accessToken = this.generateAccessToken({ - userId: user.id.toString(), + user_id: user.id.toString(), email: email.toString(), - tabId: tabId.toString(), + tab_id: tabId.toString(), roles: ["USER"], }); const refreshToken = this.generateRefreshToken({ - userId: user.id.toString(), + user_id: user.id.toString(), email: email.toString(), - tabId: tabId.toString(), + tab_id: tabId.toString(), roles: ["USER"], }); diff --git a/apps/server/src/contexts/auth/domain/value-objects/email-address.ts b/apps/server/src/contexts/auth/domain/value-objects/email-address.ts index ed583e37..def02d55 100644 --- a/apps/server/src/contexts/auth/domain/value-objects/email-address.ts +++ b/apps/server/src/contexts/auth/domain/value-objects/email-address.ts @@ -1,9 +1,12 @@ import { Result, ValueObject } from "@common/domain"; import { z } from "zod"; +export const NULLED_EMAIL_ADDRESS = null; + export class EmailAddress extends ValueObject { static create(email: string | null): Result { - const normalizedEmail = email?.trim() === "" ? null : email?.toLowerCase() || null; + const normalizedEmail = + email?.trim() === "" ? NULLED_EMAIL_ADDRESS : email?.toLowerCase() || NULLED_EMAIL_ADDRESS; const result = EmailAddress.validate(normalizedEmail); @@ -16,4 +19,8 @@ export class EmailAddress extends ValueObject { const schema = z.string().email({ message: "Invalid email format" }).or(z.null()); return schema.safeParse(email); } + + isDefined(): boolean { + return !this.isEmpty(); + } } diff --git a/apps/server/src/contexts/auth/infraestructure/passport/jwt.helper.ts b/apps/server/src/contexts/auth/infraestructure/passport/jwt.helper.ts index 308abc40..7f8b09af 100644 --- a/apps/server/src/contexts/auth/infraestructure/passport/jwt.helper.ts +++ b/apps/server/src/contexts/auth/infraestructure/passport/jwt.helper.ts @@ -1,8 +1,6 @@ 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 { diff --git a/apps/server/src/contexts/auth/infraestructure/passport/passport-auth-provider.ts b/apps/server/src/contexts/auth/infraestructure/passport/passport-auth-provider.ts index 7c0ac71f..2bc91650 100644 --- a/apps/server/src/contexts/auth/infraestructure/passport/passport-auth-provider.ts +++ b/apps/server/src/contexts/auth/infraestructure/passport/passport-auth-provider.ts @@ -9,9 +9,9 @@ import { Strategy as LocalStrategy } from "passport-local"; const SECRET_KEY = process.env.JWT_SECRET || "supersecretkey"; export interface IJWTPayload { - userId: string; + user_id: string; email: string; - tabId: string; + tab_id: string; roles: string[]; } diff --git a/apps/server/src/contexts/auth/presentation/controllers/index.ts b/apps/server/src/contexts/auth/presentation/controllers/index.ts index b6bf91fb..3ad116cf 100644 --- a/apps/server/src/contexts/auth/presentation/controllers/index.ts +++ b/apps/server/src/contexts/auth/presentation/controllers/index.ts @@ -1,2 +1,4 @@ export * from "./login"; +export * from "./logout"; +export * from "./refreshToken"; export * from "./register"; diff --git a/apps/server/src/contexts/auth/presentation/controllers/login/login.controller.ts b/apps/server/src/contexts/auth/presentation/controllers/login/login.controller.ts index 60649032..4e864cf1 100644 --- a/apps/server/src/contexts/auth/presentation/controllers/login/login.controller.ts +++ b/apps/server/src/contexts/auth/presentation/controllers/login/login.controller.ts @@ -26,6 +26,10 @@ class LoginController extends ExpressController { return this.clientError("Invalid input data", resultValidation.error); } + if (emailVO.data.isEmpty()) { + return this.clientError("Invalid input data"); + } + const loginResultOrError = await this._authService.loginUser({ email: emailVO.data, plainPassword: plainPasswordVO.data, diff --git a/apps/server/src/contexts/auth/presentation/controllers/logout/logout.controller.ts b/apps/server/src/contexts/auth/presentation/controllers/logout/logout.controller.ts index d1d4afcf..a3a3d4d5 100644 --- a/apps/server/src/contexts/auth/presentation/controllers/logout/logout.controller.ts +++ b/apps/server/src/contexts/auth/presentation/controllers/logout/logout.controller.ts @@ -23,6 +23,10 @@ class LogoutController extends ExpressController { return this.clientError("Invalid input data", resultValidation.error); } + if (emailVO.data.isEmpty()) { + return this.clientError("Invalid input data"); + } + await this._authService.logoutUser({ email: emailVO.data, tabId: tabIdVO.data, diff --git a/apps/server/src/contexts/auth/presentation/controllers/refreshToken/index.ts b/apps/server/src/contexts/auth/presentation/controllers/refreshToken/index.ts new file mode 100644 index 00000000..960bcbde --- /dev/null +++ b/apps/server/src/contexts/auth/presentation/controllers/refreshToken/index.ts @@ -0,0 +1 @@ +export * from "./refresh-token.controller"; diff --git a/apps/server/src/contexts/auth/presentation/controllers/refreshToken/refresh-token.controller.ts b/apps/server/src/contexts/auth/presentation/controllers/refreshToken/refresh-token.controller.ts new file mode 100644 index 00000000..11223f55 --- /dev/null +++ b/apps/server/src/contexts/auth/presentation/controllers/refreshToken/refresh-token.controller.ts @@ -0,0 +1,40 @@ +import { ExpressController } from "@common/presentation"; +import { createAuthService, IAuthService } from "@contexts/auth/application"; +import { IRefreshTokenPresenter, RefreshTokenPresenter } from "./refresh-token.presenter"; + +class RefreshTokenController extends ExpressController { + private readonly _authService!: IAuthService; + private readonly _presenter!: IRefreshTokenPresenter; + + public constructor(authService: IAuthService, presenter: IRefreshTokenPresenter) { + super(); + this._authService = authService; + this._presenter = presenter; + } + + async executeImpl() { + const tabId = String(this.req.headers["x-tab-id"]); + const refreshToken = String(this.req.body.refresh_token); + + const result = this._authService.verifyRefreshToken(refreshToken); + if (!result || !result.email || !result.user_id || !result.tab_id || !result.roles) { + return this.clientError("Invalid input data"); + } + + const { user_id, tab_id, email, roles } = result; + + const newRefreshToken = this._authService.generateRefreshToken({ + user_id, + tab_id, + email, + roles, + }); + + return this.created(this._presenter.map({ refreshToken: newRefreshToken })); + } +} + +export const createRefreshTokenController = () => { + const authService = createAuthService(); + return new RefreshTokenController(authService, RefreshTokenPresenter); +}; diff --git a/apps/server/src/contexts/auth/presentation/controllers/refreshToken/refresh-token.presenter.ts b/apps/server/src/contexts/auth/presentation/controllers/refreshToken/refresh-token.presenter.ts new file mode 100644 index 00000000..89f40fcc --- /dev/null +++ b/apps/server/src/contexts/auth/presentation/controllers/refreshToken/refresh-token.presenter.ts @@ -0,0 +1,15 @@ +import { IRefreshTokenResponseDTO } from "../../dto"; + +export interface IRefreshTokenPresenter { + map: (data: { refreshToken: string }) => IRefreshTokenResponseDTO; +} + +export const RefreshTokenPresenter: IRefreshTokenPresenter = { + map: (data: { refreshToken: string }): IRefreshTokenResponseDTO => { + const { refreshToken } = data; + + return { + refresh_token: refreshToken, + }; + }, +}; diff --git a/apps/server/src/contexts/auth/presentation/controllers/register/register.controller.ts b/apps/server/src/contexts/auth/presentation/controllers/register/register.controller.ts index d1b7f8ff..72d4125d 100644 --- a/apps/server/src/contexts/auth/presentation/controllers/register/register.controller.ts +++ b/apps/server/src/contexts/auth/presentation/controllers/register/register.controller.ts @@ -22,6 +22,10 @@ class RegisterController extends ExpressController { return this.clientError("Invalid input data"); } + if (emailVO.data.isEmpty()) { + return this.clientError("Invalid input data"); + } + const userOrError = await this._authService.registerUser({ username: usernameVO.data, email: emailVO.data, diff --git a/apps/server/src/contexts/auth/presentation/dto/auth.response.dto.ts b/apps/server/src/contexts/auth/presentation/dto/auth.response.dto.ts index e5bc668f..6b4aeee3 100644 --- a/apps/server/src/contexts/auth/presentation/dto/auth.response.dto.ts +++ b/apps/server/src/contexts/auth/presentation/dto/auth.response.dto.ts @@ -15,7 +15,10 @@ export interface ILoginUserResponseDTO { access_token: string; refresh_token: string; }; - //tab_id: string; } export interface ILogoutResponseDTO {} + +export interface IRefreshTokenResponseDTO { + refresh_token: string; +} diff --git a/apps/server/src/contexts/auth/presentation/dto/auth.validation.dto.ts b/apps/server/src/contexts/auth/presentation/dto/auth.validation.dto.ts index 4265af47..96b1a3a8 100644 --- a/apps/server/src/contexts/auth/presentation/dto/auth.validation.dto.ts +++ b/apps/server/src/contexts/auth/presentation/dto/auth.validation.dto.ts @@ -11,6 +11,6 @@ export const LoginUserSchema = z.object({ password: z.string().min(6, "Password must be at least 6 characters long"), }); -export const SelectCompanySchema = z.object({ - companyId: z.string().min(1, "Company ID is required"), +export const RefreshTokenSchema = z.object({ + refresh_token: z.string().min(1, "Refresh token is required"), }); diff --git a/apps/server/src/contexts/auth/presentation/index.ts b/apps/server/src/contexts/auth/presentation/index.ts index d18748da..18ced4b0 100644 --- a/apps/server/src/contexts/auth/presentation/index.ts +++ b/apps/server/src/contexts/auth/presentation/index.ts @@ -1,4 +1,3 @@ -export * from "../../../routes/auth.routes"; export * from "./controllers"; export * from "./dto"; export * from "./middleware"; diff --git a/apps/server/src/contexts/companies/application/company-service.interface.ts b/apps/server/src/contexts/companies/application/company-service.interface.ts new file mode 100644 index 00000000..e69de29b diff --git a/apps/server/src/contexts/companies/application/company.service.ts b/apps/server/src/contexts/companies/application/company.service.ts new file mode 100644 index 00000000..e69de29b diff --git a/apps/server/src/contexts/companies/application/index.ts b/apps/server/src/contexts/companies/application/index.ts new file mode 100644 index 00000000..e69de29b diff --git a/apps/server/src/contexts/companies/domain/index.ts b/apps/server/src/contexts/companies/domain/index.ts new file mode 100644 index 00000000..4a16e729 --- /dev/null +++ b/apps/server/src/contexts/companies/domain/index.ts @@ -0,0 +1,5 @@ +export * from "./aggregates"; +export * from "./entities"; +export * from "./events"; +export * from "./repositories"; +export * from "./value-objects"; diff --git a/apps/server/src/contexts/companies/domain/repositories/company-repository.interface.ts b/apps/server/src/contexts/companies/domain/repositories/company-repository.interface.ts new file mode 100644 index 00000000..e581bcb7 --- /dev/null +++ b/apps/server/src/contexts/companies/domain/repositories/company-repository.interface.ts @@ -0,0 +1 @@ +export interface ICompanyRepository {} diff --git a/apps/server/src/contexts/companies/domain/repositories/index.ts b/apps/server/src/contexts/companies/domain/repositories/index.ts new file mode 100644 index 00000000..4a2beff9 --- /dev/null +++ b/apps/server/src/contexts/companies/domain/repositories/index.ts @@ -0,0 +1 @@ +export * from "./company-repository.interface"; diff --git a/apps/server/src/contexts/companies/infraestructure/index.ts b/apps/server/src/contexts/companies/infraestructure/index.ts new file mode 100644 index 00000000..01cd6cb2 --- /dev/null +++ b/apps/server/src/contexts/companies/infraestructure/index.ts @@ -0,0 +1,3 @@ +export * from "./mappers"; +export * from "./passport"; +export * from "./sequelize"; diff --git a/apps/server/src/routes/auth.routes.ts b/apps/server/src/routes/auth.routes.ts index 05fcb988..8c7a5b08 100644 --- a/apps/server/src/routes/auth.routes.ts +++ b/apps/server/src/routes/auth.routes.ts @@ -1,10 +1,17 @@ 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 { + createLoginController, + createRefreshTokenController, +} from "@contexts/auth/presentation/controllers"; import { createLogoutController } from "@contexts/auth/presentation/controllers/logout/logout.controller"; import { createRegisterController } from "@contexts/auth/presentation/controllers/register/register.controller"; -import { LoginUserSchema, RegisterUserSchema } from "@contexts/auth/presentation/dto"; +import { + LoginUserSchema, + RefreshTokenSchema, + RegisterUserSchema, +} from "@contexts/auth/presentation/dto"; import { NextFunction, Request, Response, Router } from "express"; export const authRouter = (appRouter: Router) => { @@ -73,5 +80,15 @@ export const authRouter = (appRouter: Router) => { } ); + authRoutes.post( + "/refresh", + validateRequestDTO(RefreshTokenSchema), + //validateTabContextHeader, + //authProvider.authenticateJWT(), + (req: Request, res: Response, next: NextFunction) => { + createRefreshTokenController().execute(req, res, next); + } + ); + appRouter.use("/auth", authRoutes); }; diff --git a/apps/server/src/routes/company.routes.ts b/apps/server/src/routes/company.routes.ts new file mode 100644 index 00000000..5bac220b --- /dev/null +++ b/apps/server/src/routes/company.routes.ts @@ -0,0 +1,49 @@ +import { validateRequestDTO } from "@common/presentation"; +import { createAuthProvider } from "@contexts/company/infraestructure"; +import { validateTabContextHeader } from "@contexts/company/presentation"; +import { createLoginController } from "@contexts/company/presentation/controllers"; +import { createLogoutController } from "@contexts/company/presentation/controllers/logout/logout.controller"; +import { createRegisterController } from "@contexts/company/presentation/controllers/register/register.controller"; +import { LoginUserSchema, RegisterUserSchema } from "@contexts/company/presentation/dto"; +import { NextFunction, Request, Response, Router } from "express"; + +export const companyRouter = (appRouter: Router) => { + const companyRoutes: Router = Router({ mergeParams: true }); + const authProvider = createAuthProvider(); + + companyRoutes.get( + "/", + /*validateRequestDTO(ListCompaniesSchema),*/ + validateTabContextHeader, + authProvider.companyenticateJWT(), + + getDealerMiddleware, + handleRequest(listQuotesController) + ); + companyRoutes.get("/:quoteId", checkUser, getDealerMiddleware, handleRequest(getQuoteController)); + companyRoutes.post("/", checkUser, getDealerMiddleware, handleRequest(createQuoteController)); + + companyRoutes.post("/register", validateRequestDTO(RegisterUserSchema), (req, res, next) => { + createRegisterController().execute(req, res, next); + }); + + companyRoutes.post( + "/login", + validateRequestDTO(LoginUserSchema), + validateTabContextHeader, + (req: Request, res: Response, next: NextFunction) => { + createLoginController().execute(req, res, next); + } + ); + + companyRoutes.post( + "/logout", + validateTabContextHeader, + authProvider.companyenticateJWT(), + (req: Request, res: Response, next: NextFunction) => { + createLogoutController().execute(req, res, next); + } + ); + + appRouter.use("/company", companyRoutes); +};