Merge pull request #752 from hydralauncher/hyd-228-investigate-why-users-are-being-logged-out-when-updating

feat: prevent api calls when user is not logged in or auth is not loaded yet
This commit is contained in:
Zamitto 2024-07-03 17:05:23 -03:00 committed by GitHub
commit 138f33e0c3
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
21 changed files with 141 additions and 168 deletions

View File

@ -28,7 +28,7 @@ const signOut = async (_event: Electron.IpcMainInvokeEvent) => {
await Promise.all([ await Promise.all([
databaseOperations, databaseOperations,
HydraApi.post("/auth/logout").catch(), HydraApi.post("/auth/logout").catch(() => {}),
]); ]);
}; };

View File

@ -22,7 +22,6 @@ import "./library/open-game-installer-path";
import "./library/update-executable-path"; import "./library/update-executable-path";
import "./library/remove-game"; import "./library/remove-game";
import "./library/remove-game-from-library"; import "./library/remove-game-from-library";
import "./misc/is-user-logged-in";
import "./misc/open-external"; import "./misc/open-external";
import "./misc/show-open-dialog"; import "./misc/show-open-dialog";
import "./torrenting/cancel-game-download"; import "./torrenting/cancel-game-download";

View File

@ -53,18 +53,7 @@ const addGameToLibrary = async (
const game = await gameRepository.findOne({ where: { objectID } }); const game = await gameRepository.findOne({ where: { objectID } });
createGame(game!).then((response) => { createGame(game!);
const {
id: remoteId,
playTimeInMilliseconds,
lastTimePlayed,
} = response.data;
gameRepository.update(
{ objectID },
{ remoteId, playTimeInMilliseconds, lastTimePlayed }
);
});
}); });
}; };

View File

@ -20,7 +20,7 @@ const removeRemoveGameFromLibrary = async (gameId: number) => {
const game = await gameRepository.findOne({ where: { id: gameId } }); const game = await gameRepository.findOne({ where: { id: gameId } });
if (game?.remoteId) { if (game?.remoteId) {
HydraApi.delete(`/games/${game.remoteId}`); HydraApi.delete(`/games/${game.remoteId}`).catch(() => {});
} }
}; };

View File

@ -1,8 +0,0 @@
import { registerEvent } from "../register-event";
import { HydraApi } from "@main/services";
const isUserLoggedIn = async (_event: Electron.IpcMainInvokeEvent) => {
return HydraApi.isLoggedIn();
};
registerEvent("isUserLoggedIn", isUserLoggedIn);

View File

@ -3,7 +3,7 @@ import * as Sentry from "@sentry/electron/main";
import { HydraApi } from "@main/services"; import { HydraApi } from "@main/services";
import { UserProfile } from "@types"; import { UserProfile } from "@types";
import { userAuthRepository } from "@main/repository"; import { userAuthRepository } from "@main/repository";
import { logger } from "@main/services"; import { UserNotLoggedInError } from "@shared";
const getMe = async ( const getMe = async (
_event: Electron.IpcMainInvokeEvent _event: Electron.IpcMainInvokeEvent
@ -27,7 +27,10 @@ const getMe = async (
return me; return me;
}) })
.catch((err) => { .catch((err) => {
logger.error("getMe", err.message); if (err instanceof UserNotLoggedInError) {
return null;
}
return userAuthRepository.findOne({ where: { id: 1 } }); return userAuthRepository.findOne({ where: { id: 1 } });
}); });
}; };

View File

@ -26,9 +26,11 @@ const updateProfile = async (
_event: Electron.IpcMainInvokeEvent, _event: Electron.IpcMainInvokeEvent,
displayName: string, displayName: string,
newProfileImagePath: string | null newProfileImagePath: string | null
): Promise<UserProfile> => { ) => {
if (!newProfileImagePath) { if (!newProfileImagePath) {
return (await patchUserProfile(displayName)).data; return patchUserProfile(displayName).then(
(response) => response.data as UserProfile
);
} }
const stats = fs.statSync(newProfileImagePath); const stats = fs.statSync(newProfileImagePath);
@ -51,11 +53,11 @@ const updateProfile = async (
}); });
return profileImageUrl; return profileImageUrl;
}) })
.catch(() => { .catch(() => undefined);
return undefined;
});
return (await patchUserProfile(displayName, profileImageUrl)).data; return patchUserProfile(displayName, profileImageUrl).then(
(response) => response.data as UserProfile
);
}; };
registerEvent("updateProfile", updateProfile); registerEvent("updateProfile", updateProfile);

View File

@ -95,18 +95,7 @@ const startGameDownload = async (
}, },
}); });
createGame(updatedGame!).then((response) => { createGame(updatedGame!);
const {
id: remoteId,
playTimeInMilliseconds,
lastTimePlayed,
} = response.data;
gameRepository.update(
{ objectID },
{ remoteId, playTimeInMilliseconds, lastTimePlayed }
);
});
await downloadQueueRepository.delete({ game: { id: updatedGame!.id } }); await downloadQueueRepository.delete({ game: { id: updatedGame!.id } });
await downloadQueueRepository.insert({ game: { id: updatedGame!.id } }); await downloadQueueRepository.insert({ game: { id: updatedGame!.id } });

View File

@ -13,9 +13,9 @@ const autoLaunch = async (
}); });
if (enabled) { if (enabled) {
appLauncher.enable().catch(); appLauncher.enable().catch(() => {});
} else { } else {
appLauncher.disable().catch(); appLauncher.disable().catch(() => {});
} }
}; };

View File

@ -25,8 +25,8 @@ const loadState = async (userPreferences: UserPreferences | null) => {
if (userPreferences?.realDebridApiToken) if (userPreferences?.realDebridApiToken)
RealDebridClient.authorize(userPreferences?.realDebridApiToken); RealDebridClient.authorize(userPreferences?.realDebridApiToken);
HydraApi.setupApi().then(async () => { HydraApi.setupApi().then(() => {
if (HydraApi.isLoggedIn()) uploadGamesBatch(); uploadGamesBatch();
}); });
const [nextQueueItem] = await downloadQueueRepository.find({ const [nextQueueItem] = await downloadQueueRepository.find({

View File

@ -5,6 +5,7 @@ import url from "url";
import { uploadGamesBatch } from "./library-sync"; import { uploadGamesBatch } from "./library-sync";
import { clearGamesRemoteIds } from "./library-sync/clear-games-remote-id"; import { clearGamesRemoteIds } from "./library-sync/clear-games-remote-id";
import { logger } from "./logger"; import { logger } from "./logger";
import { UserNotLoggedInError } from "@shared";
export class HydraApi { export class HydraApi {
private static instance: AxiosInstance; private static instance: AxiosInstance;
@ -19,7 +20,7 @@ export class HydraApi {
expirationTimestamp: 0, expirationTimestamp: 0,
}; };
static isLoggedIn() { private static isLoggedIn() {
return this.userAuth.authToken !== ""; return this.userAuth.authToken !== "";
} }
@ -127,14 +128,8 @@ export class HydraApi {
} }
private static async revalidateAccessTokenIfExpired() { private static async revalidateAccessTokenIfExpired() {
if (!this.userAuth.authToken) {
userAuthRepository.delete({ id: 1 });
logger.error("user is not logged in");
this.sendSignOutEvent();
throw new Error("user is not logged in");
}
const now = new Date(); const now = new Date();
if (this.userAuth.expirationTimestamp < now.getTime()) { if (this.userAuth.expirationTimestamp < now.getTime()) {
try { try {
const response = await this.instance.post(`/auth/refresh`, { const response = await this.instance.post(`/auth/refresh`, {
@ -190,6 +185,8 @@ export class HydraApi {
}; };
static async get(url: string) { static async get(url: string) {
if (!this.isLoggedIn()) throw new UserNotLoggedInError();
await this.revalidateAccessTokenIfExpired(); await this.revalidateAccessTokenIfExpired();
return this.instance return this.instance
.get(url, this.getAxiosConfig()) .get(url, this.getAxiosConfig())
@ -197,6 +194,8 @@ export class HydraApi {
} }
static async post(url: string, data?: any) { static async post(url: string, data?: any) {
if (!this.isLoggedIn()) throw new UserNotLoggedInError();
await this.revalidateAccessTokenIfExpired(); await this.revalidateAccessTokenIfExpired();
return this.instance return this.instance
.post(url, data, this.getAxiosConfig()) .post(url, data, this.getAxiosConfig())
@ -204,6 +203,8 @@ export class HydraApi {
} }
static async put(url: string, data?: any) { static async put(url: string, data?: any) {
if (!this.isLoggedIn()) throw new UserNotLoggedInError();
await this.revalidateAccessTokenIfExpired(); await this.revalidateAccessTokenIfExpired();
return this.instance return this.instance
.put(url, data, this.getAxiosConfig()) .put(url, data, this.getAxiosConfig())
@ -211,6 +212,8 @@ export class HydraApi {
} }
static async patch(url: string, data?: any) { static async patch(url: string, data?: any) {
if (!this.isLoggedIn()) throw new UserNotLoggedInError();
await this.revalidateAccessTokenIfExpired(); await this.revalidateAccessTokenIfExpired();
return this.instance return this.instance
.patch(url, data, this.getAxiosConfig()) .patch(url, data, this.getAxiosConfig())
@ -218,6 +221,8 @@ export class HydraApi {
} }
static async delete(url: string) { static async delete(url: string) {
if (!this.isLoggedIn()) throw new UserNotLoggedInError();
await this.revalidateAccessTokenIfExpired(); await this.revalidateAccessTokenIfExpired();
return this.instance return this.instance
.delete(url, this.getAxiosConfig()) .delete(url, this.getAxiosConfig())

View File

@ -1,11 +1,25 @@
import { Game } from "@main/entity"; import { Game } from "@main/entity";
import { HydraApi } from "../hydra-api"; import { HydraApi } from "../hydra-api";
import { gameRepository } from "@main/repository";
export const createGame = async (game: Game) => { export const createGame = async (game: Game) => {
return HydraApi.post(`/games`, { HydraApi.post(`/games`, {
objectId: game.objectID, objectId: game.objectID,
playTimeInMilliseconds: Math.trunc(game.playTimeInMilliseconds), playTimeInMilliseconds: Math.trunc(game.playTimeInMilliseconds),
shop: game.shop, shop: game.shop,
lastTimePlayed: game.lastTimePlayed, lastTimePlayed: game.lastTimePlayed,
}); })
.then((response) => {
const {
id: remoteId,
playTimeInMilliseconds,
lastTimePlayed,
} = response.data;
gameRepository.update(
{ objectID: game.objectID },
{ remoteId, playTimeInMilliseconds, lastTimePlayed }
);
})
.catch(() => {});
}; };

View File

@ -2,14 +2,11 @@ import { gameRepository } from "@main/repository";
import { HydraApi } from "../hydra-api"; import { HydraApi } from "../hydra-api";
import { steamGamesWorker } from "@main/workers"; import { steamGamesWorker } from "@main/workers";
import { getSteamAppAsset } from "@main/helpers"; import { getSteamAppAsset } from "@main/helpers";
import { logger } from "../logger";
import { AxiosError } from "axios";
export const mergeWithRemoteGames = async () => { export const mergeWithRemoteGames = async () => {
try { return HydraApi.get("/games")
const games = await HydraApi.get("/games"); .then(async (response) => {
for (const game of response.data) {
for (const game of games.data) {
const localGame = await gameRepository.findOne({ const localGame = await gameRepository.findOne({
where: { where: {
objectID: game.objectId, objectID: game.objectId,
@ -62,11 +59,6 @@ export const mergeWithRemoteGames = async () => {
} }
} }
} }
} catch (err) { })
if (err instanceof AxiosError) { .catch(() => {});
logger.error("getRemoteGames", err.message);
} else {
logger.error("getRemoteGames", err);
}
}
}; };

View File

@ -6,8 +6,8 @@ export const updateGamePlaytime = async (
deltaInMillis: number, deltaInMillis: number,
lastTimePlayed: Date lastTimePlayed: Date
) => { ) => {
return HydraApi.put(`/games/${game.remoteId}`, { HydraApi.put(`/games/${game.remoteId}`, {
playTimeDeltaInSeconds: Math.trunc(deltaInMillis / 1000), playTimeDeltaInSeconds: Math.trunc(deltaInMillis / 1000),
lastTimePlayed, lastTimePlayed,
}); }).catch(() => {});
}; };

View File

@ -2,14 +2,10 @@ import { gameRepository } from "@main/repository";
import { chunk } from "lodash-es"; import { chunk } from "lodash-es";
import { IsNull } from "typeorm"; import { IsNull } from "typeorm";
import { HydraApi } from "../hydra-api"; import { HydraApi } from "../hydra-api";
import { logger } from "../logger";
import { AxiosError } from "axios";
import { mergeWithRemoteGames } from "./merge-with-remote-games"; import { mergeWithRemoteGames } from "./merge-with-remote-games";
import { WindowManager } from "../window-manager"; import { WindowManager } from "../window-manager";
export const uploadGamesBatch = async () => { export const uploadGamesBatch = async () => {
try {
const games = await gameRepository.find({ const games = await gameRepository.find({
where: { remoteId: IsNull(), isDeleted: false }, where: { remoteId: IsNull(), isDeleted: false },
}); });
@ -27,18 +23,11 @@ export const uploadGamesBatch = async () => {
lastTimePlayed: game.lastTimePlayed, lastTimePlayed: game.lastTimePlayed,
}; };
}) })
); ).catch(() => {});
} }
await mergeWithRemoteGames(); await mergeWithRemoteGames();
if (WindowManager.mainWindow) if (WindowManager.mainWindow)
WindowManager.mainWindow.webContents.send("on-library-batch-complete"); WindowManager.mainWindow.webContents.send("on-library-batch-complete");
} catch (err) {
if (err instanceof AxiosError) {
logger.error("uploadGamesBatch", err.response, err.message);
} else {
logger.error("uploadGamesBatch", err);
}
}
}; };

View File

@ -48,12 +48,7 @@ export const watchProcesses = async () => {
if (game.remoteId) { if (game.remoteId) {
updateGamePlaytime(game, 0, new Date()); updateGamePlaytime(game, 0, new Date());
} else { } else {
createGame({ ...game, lastTimePlayed: new Date() }).then( createGame({ ...game, lastTimePlayed: new Date() });
(response) => {
const { id: remoteId } = response.data;
gameRepository.update({ objectID: game.objectID }, { remoteId });
}
);
} }
gamesPlaytime.set(game.id, { gamesPlaytime.set(game.id, {
@ -72,10 +67,7 @@ export const watchProcesses = async () => {
game.lastTimePlayed! game.lastTimePlayed!
); );
} else { } else {
createGame(game).then((response) => { createGame(game);
const { id: remoteId } = response.data;
gameRepository.update({ objectID: game.objectID }, { remoteId });
});
} }
} }
} }

View File

@ -112,7 +112,6 @@ contextBridge.exposeInMainWorld("electron", {
getDefaultDownloadsPath: () => ipcRenderer.invoke("getDefaultDownloadsPath"), getDefaultDownloadsPath: () => ipcRenderer.invoke("getDefaultDownloadsPath"),
isPortableVersion: () => ipcRenderer.invoke("isPortableVersion"), isPortableVersion: () => ipcRenderer.invoke("isPortableVersion"),
openExternal: (src: string) => ipcRenderer.invoke("openExternal", src), openExternal: (src: string) => ipcRenderer.invoke("openExternal", src),
isUserLoggedIn: () => ipcRenderer.invoke("isUserLoggedIn"),
showOpenDialog: (options: Electron.OpenDialogOptions) => showOpenDialog: (options: Electron.OpenDialogOptions) =>
ipcRenderer.invoke("showOpenDialog", options), ipcRenderer.invoke("showOpenDialog", options),
platform: process.platform, platform: process.platform,

View File

@ -93,13 +93,9 @@ export function App() {
dispatch(setProfileBackground(profileBackground)); dispatch(setProfileBackground(profileBackground));
} }
window.electron.isUserLoggedIn().then((isLoggedIn) => {
if (isLoggedIn) {
fetchUserDetails().then((response) => { fetchUserDetails().then((response) => {
if (response) updateUserDetails(response); if (response) updateUserDetails(response);
}); });
}
});
}, [fetchUserDetails, updateUserDetails, dispatch]); }, [fetchUserDetails, updateUserDetails, dispatch]);
const onSignIn = useCallback(() => { const onSignIn = useCallback(() => {

View File

@ -100,7 +100,6 @@ declare global {
/* Misc */ /* Misc */
openExternal: (src: string) => Promise<void>; openExternal: (src: string) => Promise<void>;
isUserLoggedIn: () => Promise<boolean>;
getVersion: () => Promise<string>; getVersion: () => Promise<string>;
ping: () => string; ping: () => string;
getDefaultDownloadsPath: () => Promise<string>; getDefaultDownloadsPath: () => Promise<string>;

View File

@ -57,8 +57,14 @@ export function useUserDetails() {
); );
const fetchUserDetails = useCallback(async () => { const fetchUserDetails = useCallback(async () => {
return window.electron.getMe(); return window.electron.getMe().then((userDetails) => {
}, []); if (userDetails == null) {
clearUserDetails();
}
return userDetails;
});
}, [clearUserDetails]);
const patchUser = useCallback( const patchUser = useCallback(
async (displayName: string, imageProfileUrl: string | null) => { async (displayName: string, imageProfileUrl: string | null) => {

View File

@ -8,6 +8,13 @@ export enum DownloadSourceStatus {
Errored, Errored,
} }
export class UserNotLoggedInError extends Error {
constructor() {
super("user not logged in");
this.name = "UserNotLoggedInError";
}
}
const FORMAT = ["B", "KB", "MB", "GB", "TB", "PB", "EB", "ZB", "YB"]; const FORMAT = ["B", "KB", "MB", "GB", "TB", "PB", "EB", "ZB", "YB"];
export const formatBytes = (bytes: number): string => { export const formatBytes = (bytes: number): string => {