From b2fce071a9b61594ce13c376ea18b708a6d8ff9a Mon Sep 17 00:00:00 2001 From: Kendall Garner <17521368+kgarner7@users.noreply.github.com> Date: Fri, 16 Feb 2024 13:37:49 -0800 Subject: [PATCH] [bugfix]: Check for Navidrome authentication on startup Resolves #403. This PR introduces a startup check for Navidrome that tries a simple API request (/songs) before loading homepage. If the check fails, Navidrome API will fallback to trying saved password (if available). Notes: - It might also be worthwhile to do a periodic poll? --- .../hooks/use-server-authenticated.ts | 54 +++++++++++++++++++ src/renderer/router/app-outlet.tsx | 11 +++- src/renderer/types.ts | 6 +++ 3 files changed, 69 insertions(+), 2 deletions(-) create mode 100644 src/renderer/hooks/use-server-authenticated.ts diff --git a/src/renderer/hooks/use-server-authenticated.ts b/src/renderer/hooks/use-server-authenticated.ts new file mode 100644 index 00000000..3696595c --- /dev/null +++ b/src/renderer/hooks/use-server-authenticated.ts @@ -0,0 +1,54 @@ +import { useCallback, useEffect, useRef, useState } from 'react'; +import { useCurrentServer } from '/@/renderer/store'; +import { AuthState, ServerListItem, ServerType } from '/@/renderer/types'; +import { api } from '/@/renderer/api'; +import { SongListSort, SortOrder } from '/@/renderer/api/types'; +import { debounce } from 'lodash'; + +export const useServerAuthenticated = () => { + const priorServerId = useRef(); + const server = useCurrentServer(); + const [ready, setReady] = useState( + server?.type === ServerType.NAVIDROME ? AuthState.LOADING : AuthState.VALID, + ); + + const authenticateNavidrome = useCallback(async (server: ServerListItem) => { + // This trick works because navidrome-api.ts will internally check for authentication + // failures and try to log in again (where available). So, all that's necessary is + // making one request first + try { + await api.controller.getSongList({ + apiClientProps: { server }, + query: { + limit: 1, + sortBy: SongListSort.NAME, + sortOrder: SortOrder.ASC, + startIndex: 0, + }, + }); + + setReady(AuthState.VALID); + } catch (error) { + setReady(AuthState.INVALID); + } + }, []); + + const debouncedAuth = debounce((server: ServerListItem) => { + authenticateNavidrome(server).catch(console.error); + }, 300); + + useEffect(() => { + if (priorServerId.current !== server?.id) { + priorServerId.current = server?.id || ''; + + if (server?.type === ServerType.NAVIDROME) { + setReady(AuthState.LOADING); + debouncedAuth(server); + } else { + setReady(AuthState.VALID); + } + } + }, [debouncedAuth, server]); + + return ready; +}; diff --git a/src/renderer/router/app-outlet.tsx b/src/renderer/router/app-outlet.tsx index d77a8981..8e1c52a0 100644 --- a/src/renderer/router/app-outlet.tsx +++ b/src/renderer/router/app-outlet.tsx @@ -3,7 +3,9 @@ import isElectron from 'is-electron'; import { Navigate, Outlet } from 'react-router-dom'; import { AppRoute } from '/@/renderer/router/routes'; import { useCurrentServer, useSetPlayerFallback } from '/@/renderer/store'; -import { toast } from '/@/renderer/components'; +import { Spinner, toast } from '/@/renderer/components'; +import { useServerAuthenticated } from '/@/renderer/hooks/use-server-authenticated'; +import { AuthState } from '/@/renderer/types'; const ipc = isElectron() ? window.electron.ipc : null; const utils = isElectron() ? window.electron.utils : null; @@ -12,6 +14,7 @@ const mpvPlayerListener = isElectron() ? window.electron.mpvPlayerListener : nul export const AppOutlet = () => { const currentServer = useCurrentServer(); const setFallback = useSetPlayerFallback(); + const authState = useServerAuthenticated(); const isActionsRequired = useMemo(() => { const isServerRequired = !currentServer; @@ -37,7 +40,11 @@ export const AppOutlet = () => { }; }, [setFallback]); - if (isActionsRequired) { + if (authState === AuthState.LOADING) { + return ; + } + + if (isActionsRequired || authState === AuthState.INVALID) { return (