From ba08e0b11273814035fd91e269bbf757e9e969c4 Mon Sep 17 00:00:00 2001 From: Zamitto <167933696+zamitto@users.noreply.github.com> Date: Thu, 13 Jun 2024 21:08:30 -0300 Subject: [PATCH] feat: refactor api --- src/main/entity/user-preferences.entity.ts | 3 + src/main/events/helpers/validators.ts | 5 + src/main/main.ts | 2 +- src/main/services/hydra-api.ts | 112 ++++++++++----------- 4 files changed, 65 insertions(+), 57 deletions(-) diff --git a/src/main/entity/user-preferences.entity.ts b/src/main/entity/user-preferences.entity.ts index d25dfee1..0b8ca960 100644 --- a/src/main/entity/user-preferences.entity.ts +++ b/src/main/entity/user-preferences.entity.ts @@ -43,4 +43,7 @@ export class UserPreferences { @Column("text", { default: "" }) refreshToken: string; + + @Column("int", { default: 0 }) + tokenExpirationTimestamp: number; } diff --git a/src/main/events/helpers/validators.ts b/src/main/events/helpers/validators.ts index f3c9d844..e3163aff 100644 --- a/src/main/events/helpers/validators.ts +++ b/src/main/events/helpers/validators.ts @@ -12,3 +12,8 @@ export const downloadSourceSchema = z.object({ }) ), }); + +export const refreshTokenSchema = z.object({ + accessToken: z.string(), + expiresIn: z.number(), +}); diff --git a/src/main/main.ts b/src/main/main.ts index 807e48b6..7e5692d5 100644 --- a/src/main/main.ts +++ b/src/main/main.ts @@ -21,7 +21,7 @@ const loadState = async (userPreferences: UserPreferences | null) => { if (userPreferences?.realDebridApiToken) RealDebridClient.authorize(userPreferences?.realDebridApiToken); - HydraApi.createInstance(); + HydraApi.setupApi(); const [nextQueueItem] = await downloadQueueRepository.find({ order: { diff --git a/src/main/services/hydra-api.ts b/src/main/services/hydra-api.ts index 824f41e3..ac08b91f 100644 --- a/src/main/services/hydra-api.ts +++ b/src/main/services/hydra-api.ts @@ -1,13 +1,19 @@ +import { refreshTokenSchema } from "@main/events/helpers/validators"; import { userPreferencesRepository } from "@main/repository"; import axios, { AxiosInstance } from "axios"; export class HydraApi { private static instance: AxiosInstance; - static authToken = ""; - static refreshToken = ""; + private static readonly EXPIRATION_OFFSET_IN_MS = 1000 * 60 * 5; - static async createInstance() { + private static userAuth = { + authToken: "", + refreshToken: "", + expirationTimestamp: 0, + }; + + static async setupApi() { this.instance = axios.create({ baseURL: import.meta.env.MAIN_VITE_API_URL, }); @@ -16,72 +22,66 @@ export class HydraApi { where: { id: 1 }, }); - this.authToken = userPreferences?.accessToken ?? ""; - this.refreshToken = userPreferences?.refreshToken ?? ""; + this.userAuth = { + authToken: userPreferences?.accessToken ?? "", + refreshToken: userPreferences?.refreshToken ?? "", + expirationTimestamp: userPreferences?.tokenExpirationTimestamp ?? 0, + }; + } - this.instance.interceptors.request.use( - (config) => { - config.headers.Authorization = `Bearer ${this.authToken}`; - return config; + private static async revalidateAccessTokenIfExpired() { + const now = new Date(); + if (this.userAuth.expirationTimestamp > now.getTime()) { + const response = await this.instance.post(`/auth/refresh`, { + refreshToken: this.userAuth.refreshToken, + }); + + const { accessToken, expiresIn } = refreshTokenSchema.parse( + response.data + ); + + const tokenExpirationTimestamp = + now.getTime() + expiresIn - this.EXPIRATION_OFFSET_IN_MS; + + this.userAuth.authToken = accessToken; + this.userAuth.expirationTimestamp = tokenExpirationTimestamp; + + userPreferencesRepository.upsert( + { + id: 1, + accessToken, + tokenExpirationTimestamp, + }, + ["id"] + ); + } + } + + private static getAxiosConfig() { + return { + headers: { + Authorization: `Bearer ${this.userAuth.authToken}`, }, - (error) => { - return Promise.reject(error); - } - ); - - this.instance.interceptors.response.use( - (response) => response, - async (error) => { - const originalRequest = error.config; - if (error.response.status === 401 && !originalRequest._retry) { - originalRequest._retry = true; - const refreshToken = this.refreshToken; - - if (!refreshToken) return error; - - try { - const response = await axios.post( - `${import.meta.env.MAIN_VITE_API_URL}/auth/refresh`, - { refreshToken } - ); - const newAccessToken = response.data.accessToken; - this.authToken = newAccessToken; - - userPreferencesRepository.upsert( - { - id: 1, - accessToken: newAccessToken, - }, - ["id"] - ); - - originalRequest.headers.Authorization = `Bearer ${newAccessToken}`; - return axios(originalRequest); //recall Api with new token - } catch (err) { - this.authToken = ""; - this.refreshToken = ""; - return error; - } - } - - return error; - } - ); + }; } static async get(url: string) { - return this.instance.get(url); + this.revalidateAccessTokenIfExpired(); + return this.instance.get(url, this.getAxiosConfig()); } static async post(url: string, data?: any) { - return this.instance.post(url, data); + this.revalidateAccessTokenIfExpired(); + return this.instance.post(url, data, this.getAxiosConfig()); } static async put(url, data?: any) { - return this.instance.put(url, data); + this.revalidateAccessTokenIfExpired(); + return this.instance.put(url, data, this.getAxiosConfig()); } static async patch(url, data?: any) { - return this.instance.patch(url, data); + this.revalidateAccessTokenIfExpired(); + return this.instance.patch(url, data, this.getAxiosConfig()); } }