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/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/main.ts b/src/main/main.ts index d5998b5a..7e5692d5 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.setupApi(); + const [nextQueueItem] = await downloadQueueRepository.find({ order: { id: "DESC", 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 new file mode 100644 index 00000000..8ae05bcf --- /dev/null +++ b/src/main/services/hydra-api.ts @@ -0,0 +1,84 @@ +import { userAuthRepository } from "@main/repository"; +import axios, { AxiosInstance } from "axios"; + +export class HydraApi { + private static instance: AxiosInstance; + + private static readonly EXPIRATION_OFFSET_IN_MS = 1000 * 60 * 5; + + private static userAuth = { + authToken: "", + refreshToken: "", + expirationTimestamp: 0, + }; + + static async setupApi() { + this.instance = axios.create({ + baseURL: import.meta.env.MAIN_VITE_API_URL, + }); + + const userAuth = await userAuthRepository.findOne({ + where: { id: 1 }, + }); + + this.userAuth = { + authToken: userAuth?.accessToken ?? "", + refreshToken: userAuth?.refreshToken ?? "", + expirationTimestamp: userAuth?.tokenExpirationTimestamp ?? 0, + }; + } + + 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 } = response.data; + + const tokenExpirationTimestamp = + now.getTime() + expiresIn - this.EXPIRATION_OFFSET_IN_MS; + + this.userAuth.authToken = accessToken; + this.userAuth.expirationTimestamp = tokenExpirationTimestamp; + + userAuthRepository.upsert( + { + id: 1, + accessToken, + tokenExpirationTimestamp, + }, + ["id"] + ); + } + } + + private static getAxiosConfig() { + return { + headers: { + Authorization: `Bearer ${this.userAuth.authToken}`, + }, + }; + } + + static async get(url: string) { + await this.revalidateAccessTokenIfExpired(); + return this.instance.get(url, this.getAxiosConfig()); + } + + static async post(url: string, data?: any) { + await this.revalidateAccessTokenIfExpired(); + return this.instance.post(url, data, this.getAxiosConfig()); + } + + static async put(url, data?: any) { + await this.revalidateAccessTokenIfExpired(); + return this.instance.put(url, data, this.getAxiosConfig()); + } + + static async patch(url, data?: any) { + await this.revalidateAccessTokenIfExpired(); + return this.instance.patch(url, data, this.getAxiosConfig()); + } +} 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 {