Refresh token

This commit is contained in:
David Arranz 2025-02-07 18:46:41 +01:00
parent eef6e6fa97
commit 4e39aacedf
24 changed files with 173 additions and 15 deletions

View File

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

View File

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

View File

@ -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<string | null> {
static create(email: string | null): Result<EmailAddress, Error> {
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<string | null> {
const schema = z.string().email({ message: "Invalid email format" }).or(z.null());
return schema.safeParse(email);
}
isDefined(): boolean {
return !this.isEmpty();
}
}

View File

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

View File

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

View File

@ -1,2 +1,4 @@
export * from "./login";
export * from "./logout";
export * from "./refreshToken";
export * from "./register";

View File

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

View File

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

View File

@ -0,0 +1 @@
export * from "./refresh-token.controller";

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,4 +1,3 @@
export * from "../../../routes/auth.routes";
export * from "./controllers";
export * from "./dto";
export * from "./middleware";

View File

@ -0,0 +1,5 @@
export * from "./aggregates";
export * from "./entities";
export * from "./events";
export * from "./repositories";
export * from "./value-objects";

View File

@ -0,0 +1 @@
export interface ICompanyRepository {}

View File

@ -0,0 +1 @@
export * from "./company-repository.interface";

View File

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

View File

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

View File

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