From 81f2e509c5371fd5a56f7d2066ab8861d355f8d3 Mon Sep 17 00:00:00 2001 From: Zamitto <167933696+zamitto@users.noreply.github.com> Date: Tue, 11 Jun 2024 22:09:24 -0300 Subject: [PATCH 1/6] feat: create HydraApi --- .env.example | 3 +- src/main/entity/user-preferences.entity.ts | 6 ++ src/main/main.ts | 3 + src/main/services/hydra-api.ts | 88 ++++++++++++++++++++++ src/main/vite-env.d.ts | 3 +- 5 files changed, 99 insertions(+), 4 deletions(-) create mode 100644 src/main/services/hydra-api.ts diff --git a/.env.example b/.env.example index 636467e5..f55344d1 100644 --- a/.env.example +++ b/.env.example @@ -1,4 +1,3 @@ MAIN_VITE_STEAMGRIDDB_API_KEY=YOUR_API_KEY -MAIN_VITE_ONLINEFIX_USERNAME=YOUR_USERNAME -MAIN_VITE_ONLINEFIX_PASSWORD=YOUR_PASSWORD +MAIN_VITE_API_URL=API_URL diff --git a/src/main/entity/user-preferences.entity.ts b/src/main/entity/user-preferences.entity.ts index 92db958d..d25dfee1 100644 --- a/src/main/entity/user-preferences.entity.ts +++ b/src/main/entity/user-preferences.entity.ts @@ -37,4 +37,10 @@ export class UserPreferences { @UpdateDateColumn() updatedAt: Date; + + @Column("text", { default: "" }) + accessToken: string; + + @Column("text", { default: "" }) + refreshToken: string; } diff --git a/src/main/main.ts b/src/main/main.ts index d5998b5a..807e48b6 100644 --- a/src/main/main.ts +++ b/src/main/main.ts @@ -9,6 +9,7 @@ import { RealDebridClient } from "./services/real-debrid"; import { fetchDownloadSourcesAndUpdate } from "./helpers"; import { publishNewRepacksNotifications } from "./services/notifications"; import { MoreThan } from "typeorm"; +import { HydraApi } from "./services/hydra-api"; startMainLoop(); @@ -20,6 +21,8 @@ const loadState = async (userPreferences: UserPreferences | null) => { if (userPreferences?.realDebridApiToken) RealDebridClient.authorize(userPreferences?.realDebridApiToken); + HydraApi.createInstance(); + const [nextQueueItem] = await downloadQueueRepository.find({ order: { id: "DESC", diff --git a/src/main/services/hydra-api.ts b/src/main/services/hydra-api.ts new file mode 100644 index 00000000..e65d70d2 --- /dev/null +++ b/src/main/services/hydra-api.ts @@ -0,0 +1,88 @@ +import { userPreferencesRepository } from "@main/repository"; +import axios, { AxiosInstance } from "axios"; + +export class HydraApi { + private static instance: AxiosInstance; + + static authToken = ""; + static refreshToken = ""; + + static async createInstance() { + this.instance = axios.create({ + baseURL: import.meta.env.MAIN_VITE_API_URL, + }); + + const userPreferences = await userPreferencesRepository.findOne({ + where: { id: 1 }, + }); + + this.authToken = userPreferences?.accessToken ?? ""; + this.refreshToken = userPreferences?.refreshToken ?? ""; + + this.instance.interceptors.request.use( + (config) => { + config.headers.Authorization = `Bearer ${this.authToken}`; + return config; + }, + (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) { + 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 (error) { + this.authToken = ""; + this.refreshToken = ""; + return error; + } + } + + return error; + } + + return error; + } + ); + } + + static async get(url: string) { + return this.instance.get(url); + } + + static async post(url: string, data?: any) { + return this.instance.post(url, data); + } + + static async put(url, data?: any) { + return this.instance.put(url, data); + } + + static async patch(url, data?: any) { + return this.instance.patch(url, data); + } +} diff --git a/src/main/vite-env.d.ts b/src/main/vite-env.d.ts index 7542ff52..4dbec1d2 100644 --- a/src/main/vite-env.d.ts +++ b/src/main/vite-env.d.ts @@ -2,8 +2,7 @@ interface ImportMetaEnv { readonly MAIN_VITE_STEAMGRIDDB_API_KEY: string; - readonly MAIN_VITE_ONLINEFIX_USERNAME: string; - readonly MAIN_VITE_ONLINEFIX_PASSWORD: string; + readonly MAIN_VITE_API_URL: string; } interface ImportMeta { From f21a8bf78498398a8e5be36b24cd8cb53cae73b9 Mon Sep 17 00:00:00 2001 From: Zamitto <167933696+zamitto@users.noreply.github.com> Date: Thu, 13 Jun 2024 20:12:13 -0300 Subject: [PATCH 2/6] feat: create HydraApi --- src/main/services/hydra-api.ts | 47 +++++++++++++++++----------------- 1 file changed, 23 insertions(+), 24 deletions(-) diff --git a/src/main/services/hydra-api.ts b/src/main/services/hydra-api.ts index e65d70d2..824f41e3 100644 --- a/src/main/services/hydra-api.ts +++ b/src/main/services/hydra-api.ts @@ -36,33 +36,32 @@ export class HydraApi { if (error.response.status === 401 && !originalRequest._retry) { originalRequest._retry = true; const refreshToken = this.refreshToken; - if (refreshToken) { - 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"] - ); + if (!refreshToken) return error; - originalRequest.headers.Authorization = `Bearer ${newAccessToken}`; - return axios(originalRequest); //recall Api with new token - } catch (error) { - this.authToken = ""; - this.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; } return error; 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 3/6] 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()); } } From 7bbaae91c9d454ae26e5f77777abd938b558bc50 Mon Sep 17 00:00:00 2001 From: Zamitto <167933696+zamitto@users.noreply.github.com> Date: Thu, 13 Jun 2024 22:52:29 -0300 Subject: [PATCH 4/6] fix: await and conditional --- src/main/services/hydra-api.ts | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/main/services/hydra-api.ts b/src/main/services/hydra-api.ts index ac08b91f..776021ea 100644 --- a/src/main/services/hydra-api.ts +++ b/src/main/services/hydra-api.ts @@ -31,7 +31,7 @@ export class HydraApi { private static async revalidateAccessTokenIfExpired() { const now = new Date(); - if (this.userAuth.expirationTimestamp > now.getTime()) { + if (this.userAuth.expirationTimestamp < now.getTime()) { const response = await this.instance.post(`/auth/refresh`, { refreshToken: this.userAuth.refreshToken, }); @@ -66,22 +66,22 @@ export class HydraApi { } static async get(url: string) { - this.revalidateAccessTokenIfExpired(); + await this.revalidateAccessTokenIfExpired(); return this.instance.get(url, this.getAxiosConfig()); } static async post(url: string, data?: any) { - this.revalidateAccessTokenIfExpired(); + await this.revalidateAccessTokenIfExpired(); return this.instance.post(url, data, this.getAxiosConfig()); } static async put(url, data?: any) { - this.revalidateAccessTokenIfExpired(); + await this.revalidateAccessTokenIfExpired(); return this.instance.put(url, data, this.getAxiosConfig()); } static async patch(url, data?: any) { - this.revalidateAccessTokenIfExpired(); + await this.revalidateAccessTokenIfExpired(); return this.instance.patch(url, data, this.getAxiosConfig()); } } From 9e5118d1dc2567e9f182a6d818fd821f12d5d24f Mon Sep 17 00:00:00 2001 From: Zamitto <167933696+zamitto@users.noreply.github.com> Date: Thu, 13 Jun 2024 23:11:40 -0300 Subject: [PATCH 5/6] remove zod schema --- src/main/events/helpers/validators.ts | 5 ----- src/main/services/hydra-api.ts | 4 +--- 2 files changed, 1 insertion(+), 8 deletions(-) diff --git a/src/main/events/helpers/validators.ts b/src/main/events/helpers/validators.ts index e3163aff..f3c9d844 100644 --- a/src/main/events/helpers/validators.ts +++ b/src/main/events/helpers/validators.ts @@ -12,8 +12,3 @@ export const downloadSourceSchema = z.object({ }) ), }); - -export const refreshTokenSchema = z.object({ - accessToken: z.string(), - expiresIn: z.number(), -}); diff --git a/src/main/services/hydra-api.ts b/src/main/services/hydra-api.ts index 776021ea..56caab8b 100644 --- a/src/main/services/hydra-api.ts +++ b/src/main/services/hydra-api.ts @@ -36,9 +36,7 @@ export class HydraApi { refreshToken: this.userAuth.refreshToken, }); - const { accessToken, expiresIn } = refreshTokenSchema.parse( - response.data - ); + const { accessToken, expiresIn } = response.data; const tokenExpirationTimestamp = now.getTime() + expiresIn - this.EXPIRATION_OFFSET_IN_MS; From 1797abb2da6ba3e71d46f90dc8312daa1113629e Mon Sep 17 00:00:00 2001 From: Zamitto <167933696+zamitto@users.noreply.github.com> Date: Thu, 13 Jun 2024 23:31:32 -0300 Subject: [PATCH 6/6] refactor user preferences and create user auth entity --- src/main/data-source.ts | 2 ++ src/main/entity/user-auth.ts | 28 ++++++++++++++++++++++ src/main/entity/user-preferences.entity.ts | 9 ------- src/main/repository.ts | 3 +++ src/main/services/hydra-api.ts | 13 +++++----- 5 files changed, 39 insertions(+), 16 deletions(-) create mode 100644 src/main/entity/user-auth.ts diff --git a/src/main/data-source.ts b/src/main/data-source.ts index 71c00c5b..b47ce2c0 100644 --- a/src/main/data-source.ts +++ b/src/main/data-source.ts @@ -11,6 +11,7 @@ import type { BetterSqlite3ConnectionOptions } from "typeorm/driver/better-sqlit import { databasePath } from "./constants"; import migrations from "./migrations"; +import { UserAuth } from "./entity/user-auth"; export const createDataSource = ( options: Partial @@ -24,6 +25,7 @@ export const createDataSource = ( GameShopCache, DownloadSource, DownloadQueue, + UserAuth, ], synchronize: true, database: databasePath, diff --git a/src/main/entity/user-auth.ts b/src/main/entity/user-auth.ts new file mode 100644 index 00000000..61ca6738 --- /dev/null +++ b/src/main/entity/user-auth.ts @@ -0,0 +1,28 @@ +import { + Entity, + PrimaryGeneratedColumn, + Column, + CreateDateColumn, + UpdateDateColumn, +} from "typeorm"; + +@Entity("user_auth") +export class UserAuth { + @PrimaryGeneratedColumn() + id: number; + + @Column("text", { default: "" }) + accessToken: string; + + @Column("text", { default: "" }) + refreshToken: string; + + @Column("int", { default: 0 }) + tokenExpirationTimestamp: number; + + @CreateDateColumn() + createdAt: Date; + + @UpdateDateColumn() + updatedAt: Date; +} diff --git a/src/main/entity/user-preferences.entity.ts b/src/main/entity/user-preferences.entity.ts index 0b8ca960..92db958d 100644 --- a/src/main/entity/user-preferences.entity.ts +++ b/src/main/entity/user-preferences.entity.ts @@ -37,13 +37,4 @@ export class UserPreferences { @UpdateDateColumn() updatedAt: Date; - - @Column("text", { default: "" }) - accessToken: string; - - @Column("text", { default: "" }) - refreshToken: string; - - @Column("int", { default: 0 }) - tokenExpirationTimestamp: number; } diff --git a/src/main/repository.ts b/src/main/repository.ts index 6d66cae8..af6ebcbd 100644 --- a/src/main/repository.ts +++ b/src/main/repository.ts @@ -7,6 +7,7 @@ import { Repack, UserPreferences, } from "@main/entity"; +import { UserAuth } from "./entity/user-auth"; export const gameRepository = dataSource.getRepository(Game); @@ -21,3 +22,5 @@ export const downloadSourceRepository = dataSource.getRepository(DownloadSource); export const downloadQueueRepository = dataSource.getRepository(DownloadQueue); + +export const userAuthRepository = dataSource.getRepository(UserAuth); diff --git a/src/main/services/hydra-api.ts b/src/main/services/hydra-api.ts index 56caab8b..8ae05bcf 100644 --- a/src/main/services/hydra-api.ts +++ b/src/main/services/hydra-api.ts @@ -1,5 +1,4 @@ -import { refreshTokenSchema } from "@main/events/helpers/validators"; -import { userPreferencesRepository } from "@main/repository"; +import { userAuthRepository } from "@main/repository"; import axios, { AxiosInstance } from "axios"; export class HydraApi { @@ -18,14 +17,14 @@ export class HydraApi { baseURL: import.meta.env.MAIN_VITE_API_URL, }); - const userPreferences = await userPreferencesRepository.findOne({ + const userAuth = await userAuthRepository.findOne({ where: { id: 1 }, }); this.userAuth = { - authToken: userPreferences?.accessToken ?? "", - refreshToken: userPreferences?.refreshToken ?? "", - expirationTimestamp: userPreferences?.tokenExpirationTimestamp ?? 0, + authToken: userAuth?.accessToken ?? "", + refreshToken: userAuth?.refreshToken ?? "", + expirationTimestamp: userAuth?.tokenExpirationTimestamp ?? 0, }; } @@ -44,7 +43,7 @@ export class HydraApi { this.userAuth.authToken = accessToken; this.userAuth.expirationTimestamp = tokenExpirationTimestamp; - userPreferencesRepository.upsert( + userAuthRepository.upsert( { id: 1, accessToken,