diff --git a/src/main/entity/user-auth.ts b/src/main/entity/user-auth.ts index 61ca6738..d383e3cb 100644 --- a/src/main/entity/user-auth.ts +++ b/src/main/entity/user-auth.ts @@ -11,6 +11,15 @@ export class UserAuth { @PrimaryGeneratedColumn() id: number; + @Column("text", { default: "" }) + userId: string; + + @Column("text", { default: "" }) + displayName: string; + + @Column("text", { default: "" }) + profileImageUrl: string; + @Column("text", { default: "" }) accessToken: string; diff --git a/src/main/events/helpers/validators.ts b/src/main/events/helpers/validators.ts index 68df8bfa..f3c9d844 100644 --- a/src/main/events/helpers/validators.ts +++ b/src/main/events/helpers/validators.ts @@ -12,20 +12,3 @@ export const downloadSourceSchema = z.object({ }) ), }); - -const gamesArray = z.array( - z.object({ - id: z.string().length(8), - objectId: z.string().max(255), - playTimeInSeconds: z.number().int(), - shop: z.enum(["steam", "epic"]), - lastTimePlayed: z.coerce.date().nullable(), - }) -); - -export const userProfileSchema = z.object({ - displayName: z.string(), - profileImageUrl: z.string().url().nullable(), - libraryGames: gamesArray, - recentGames: gamesArray, -}); diff --git a/src/main/events/index.ts b/src/main/events/index.ts index 99fd785e..8371b60d 100644 --- a/src/main/events/index.ts +++ b/src/main/events/index.ts @@ -1,5 +1,6 @@ import { defaultDownloadsPath } from "@main/constants"; import { app, ipcMain } from "electron"; +import { HydraApi } from "@main/services/hydra-api"; import "./catalogue/get-catalogue"; import "./catalogue/get-game-shop-details"; @@ -40,7 +41,9 @@ import "./download-sources/add-download-source"; import "./download-sources/remove-download-source"; import "./download-sources/sync-download-sources"; import "./profile/get-user-profile"; +import "./profile/get-me"; ipcMain.handle("ping", () => "pong"); ipcMain.handle("getVersion", () => app.getVersion()); +ipcMain.handle("isUserLoggedIn", () => HydraApi.isLoggedIn()); ipcMain.handle("getDefaultDownloadsPath", () => defaultDownloadsPath); diff --git a/src/main/events/profile/get-me.ts b/src/main/events/profile/get-me.ts new file mode 100644 index 00000000..fe43536a --- /dev/null +++ b/src/main/events/profile/get-me.ts @@ -0,0 +1,30 @@ +import { registerEvent } from "../register-event"; +import { HydraApi } from "@main/services/hydra-api"; +import { UserProfile } from "@types"; +import { userAuthRepository } from "@main/repository"; + +const getMe = async ( + _event: Electron.IpcMainInvokeEvent +): Promise => { + return HydraApi.get(`/profile/me`) + .then((response) => { + const me = response.data; + + userAuthRepository.upsert( + { + id: 1, + displayName: me.displayName, + profileImageUrl: me.displayName, + userId: me.id, + }, + ["id"] + ); + + return me; + }) + .catch(() => { + return userAuthRepository.findOne({ where: { id: 1 } }); + }); +}; + +registerEvent("getMe", getMe); diff --git a/src/main/events/profile/get-user-profile.ts b/src/main/events/profile/get-user-profile.ts index 7829e928..4cbe7f55 100644 --- a/src/main/events/profile/get-user-profile.ts +++ b/src/main/events/profile/get-user-profile.ts @@ -1,6 +1,4 @@ import { registerEvent } from "../register-event"; -import { userProfileSchema } from "../helpers/validators"; -import { logger } from "@main/services"; import { HydraApi } from "@main/services/hydra-api"; import { steamGamesWorker } from "@main/workers"; import { UserProfile } from "@types"; @@ -13,7 +11,7 @@ const getUserProfile = async ( ): Promise => { try { const response = await HydraApi.get(`/user/${username}`); - const profile = userProfileSchema.parse(response.data); + const profile = response.data; const recentGames = await Promise.all( profile.recentGames.map(async (game) => { @@ -51,7 +49,6 @@ const getUserProfile = async ( return { ...profile, libraryGames, recentGames }; } catch (err) { - logger.error(`getUserProfile: ${username}`, err); return null; } }; diff --git a/src/main/services/hydra-api.ts b/src/main/services/hydra-api.ts index 8ae05bcf..41e7a39f 100644 --- a/src/main/services/hydra-api.ts +++ b/src/main/services/hydra-api.ts @@ -1,5 +1,5 @@ import { userAuthRepository } from "@main/repository"; -import axios, { AxiosInstance } from "axios"; +import axios, { AxiosError, AxiosInstance } from "axios"; export class HydraApi { private static instance: AxiosInstance; @@ -12,6 +12,10 @@ export class HydraApi { expirationTimestamp: 0, }; + static isLoggedIn() { + return this.userAuth.authToken !== ""; + } + static async setupApi() { this.instance = axios.create({ baseURL: import.meta.env.MAIN_VITE_API_URL, @@ -31,26 +35,48 @@ export class HydraApi { 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, - }); + try { + const response = await this.instance.post(`/auth/refresh`, { + refreshToken: this.userAuth.refreshToken, + }); - const { accessToken, expiresIn } = response.data; + const { accessToken, expiresIn } = response.data; - const tokenExpirationTimestamp = - now.getTime() + expiresIn - this.EXPIRATION_OFFSET_IN_MS; + const tokenExpirationTimestamp = + now.getTime() + expiresIn - this.EXPIRATION_OFFSET_IN_MS; - this.userAuth.authToken = accessToken; - this.userAuth.expirationTimestamp = tokenExpirationTimestamp; + this.userAuth.authToken = accessToken; + this.userAuth.expirationTimestamp = tokenExpirationTimestamp; - userAuthRepository.upsert( - { - id: 1, - accessToken, - tokenExpirationTimestamp, - }, - ["id"] - ); + userAuthRepository.upsert( + { + id: 1, + accessToken, + tokenExpirationTimestamp, + }, + ["id"] + ); + } catch (err) { + if ( + err instanceof AxiosError && + (err?.response?.status === 401 || err?.response?.status === 403) + ) { + this.userAuth.authToken = ""; + this.userAuth.expirationTimestamp = 0; + + userAuthRepository.upsert( + { + id: 1, + accessToken: "", + refreshToken: "", + tokenExpirationTimestamp: 0, + }, + ["id"] + ); + } + + throw err; + } } } diff --git a/src/preload/index.ts b/src/preload/index.ts index 0648353c..2f837167 100644 --- a/src/preload/index.ts +++ b/src/preload/index.ts @@ -104,6 +104,7 @@ contextBridge.exposeInMainWorld("electron", { /* Misc */ ping: () => ipcRenderer.invoke("ping"), getVersion: () => ipcRenderer.invoke("getVersion"), + isUserLoggedIn: () => ipcRenderer.invoke("isUserLoggedIn"), getDefaultDownloadsPath: () => ipcRenderer.invoke("getDefaultDownloadsPath"), openExternal: (src: string) => ipcRenderer.invoke("openExternal", src), showOpenDialog: (options: Electron.OpenDialogOptions) => @@ -129,4 +130,5 @@ contextBridge.exposeInMainWorld("electron", { /* Profile */ getUserProfile: (username: string) => ipcRenderer.invoke("getUserProfile", username), + getMe: () => ipcRenderer.invoke("getMe"), }); diff --git a/src/renderer/src/components/sidebar/sidebar-profile.tsx b/src/renderer/src/components/sidebar/sidebar-profile.tsx new file mode 100644 index 00000000..f26ffa4e --- /dev/null +++ b/src/renderer/src/components/sidebar/sidebar-profile.tsx @@ -0,0 +1,80 @@ +import { useEffect, useState } from "react"; +import { useNavigate } from "react-router-dom"; +import { type UserProfile } from "@types"; +import * as styles from "./sidebar.css"; +import { PersonIcon } from "@primer/octicons-react"; + +export function SidebarProfile() { + const navigate = useNavigate(); + + const [userProfile, setUserProfile] = useState(null); + const [isUserProfileLoading, setIsUserProfileLoading] = useState(true); + + const handleClickProfile = () => { + navigate(`/profile/${userProfile!.id}`); + }; + + const handleClickLogin = () => { + window.electron.openExternal("https://losbroxas.org"); + }; + + useEffect(() => { + setIsUserProfileLoading(true); + window.electron.isUserLoggedIn().then(async (isLoggedIn) => { + if (isLoggedIn) { + const userProfile = await window.electron.getMe(); + setUserProfile(userProfile); + } + + setIsUserProfileLoading(false); + }); + }, []); + + if (isUserProfileLoading) return null; + + if (userProfile == null) { + return ( + <> + + + ); + } + + return ( + <> + + + ); +} diff --git a/src/renderer/src/components/sidebar/sidebar.tsx b/src/renderer/src/components/sidebar/sidebar.tsx index de72798b..5fb20577 100644 --- a/src/renderer/src/components/sidebar/sidebar.tsx +++ b/src/renderer/src/components/sidebar/sidebar.tsx @@ -13,7 +13,7 @@ import * as styles from "./sidebar.css"; import { buildGameDetailsPath } from "@renderer/helpers"; import SteamLogo from "@renderer/assets/steam-logo.svg?react"; -import { PersonIcon } from "@primer/octicons-react"; +import { SidebarProfile } from "./sidebar-profile"; const SIDEBAR_MIN_WIDTH = 200; const SIDEBAR_INITIAL_WIDTH = 250; @@ -143,10 +143,6 @@ export function Sidebar() { } }; - const handleClickProfile = () => { - navigate("/profile/olejRejN"); - }; - return ( <>