[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?
This commit is contained in:
parent
20b161ee86
commit
b2fce071a9
3 changed files with 69 additions and 2 deletions
54
src/renderer/hooks/use-server-authenticated.ts
Normal file
54
src/renderer/hooks/use-server-authenticated.ts
Normal file
|
@ -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<string>();
|
||||||
|
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;
|
||||||
|
};
|
|
@ -3,7 +3,9 @@ import isElectron from 'is-electron';
|
||||||
import { Navigate, Outlet } from 'react-router-dom';
|
import { Navigate, Outlet } from 'react-router-dom';
|
||||||
import { AppRoute } from '/@/renderer/router/routes';
|
import { AppRoute } from '/@/renderer/router/routes';
|
||||||
import { useCurrentServer, useSetPlayerFallback } from '/@/renderer/store';
|
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 ipc = isElectron() ? window.electron.ipc : null;
|
||||||
const utils = isElectron() ? window.electron.utils : null;
|
const utils = isElectron() ? window.electron.utils : null;
|
||||||
|
@ -12,6 +14,7 @@ const mpvPlayerListener = isElectron() ? window.electron.mpvPlayerListener : nul
|
||||||
export const AppOutlet = () => {
|
export const AppOutlet = () => {
|
||||||
const currentServer = useCurrentServer();
|
const currentServer = useCurrentServer();
|
||||||
const setFallback = useSetPlayerFallback();
|
const setFallback = useSetPlayerFallback();
|
||||||
|
const authState = useServerAuthenticated();
|
||||||
|
|
||||||
const isActionsRequired = useMemo(() => {
|
const isActionsRequired = useMemo(() => {
|
||||||
const isServerRequired = !currentServer;
|
const isServerRequired = !currentServer;
|
||||||
|
@ -37,7 +40,11 @@ export const AppOutlet = () => {
|
||||||
};
|
};
|
||||||
}, [setFallback]);
|
}, [setFallback]);
|
||||||
|
|
||||||
if (isActionsRequired) {
|
if (authState === AuthState.LOADING) {
|
||||||
|
return <Spinner container />;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isActionsRequired || authState === AuthState.INVALID) {
|
||||||
return (
|
return (
|
||||||
<Navigate
|
<Navigate
|
||||||
replace
|
replace
|
||||||
|
|
|
@ -213,3 +213,9 @@ export enum FontType {
|
||||||
}
|
}
|
||||||
|
|
||||||
export type TitleTheme = 'dark' | 'light' | 'system';
|
export type TitleTheme = 'dark' | 'light' | 'system';
|
||||||
|
|
||||||
|
export enum AuthState {
|
||||||
|
INVALID = 'invalid',
|
||||||
|
LOADING = 'loading',
|
||||||
|
VALID = 'valid',
|
||||||
|
}
|
||||||
|
|
Reference in a new issue