mirror of
https://github.com/jeffvli/feishin.git
synced 2026-05-08 13:00:13 +02:00
check authentication for all servers on initialization and update permission roles
This commit is contained in:
@@ -4,75 +4,217 @@ import { useCallback, useEffect, useRef, useState } from 'react';
|
||||
|
||||
import { api } from '/@/renderer/api';
|
||||
import { getServerById, useAuthStoreActions, useCurrentServer } from '/@/renderer/store';
|
||||
import { LogCategory, logFn } from '/@/renderer/utils/logger';
|
||||
import { logMsg } from '/@/renderer/utils/logger-message';
|
||||
import { toast } from '/@/shared/components/toast/toast';
|
||||
import { SongListSort, SortOrder } from '/@/shared/types/domain-types';
|
||||
import { AuthState, ServerListItem, ServerType } from '/@/shared/types/types';
|
||||
import { AuthState } from '/@/shared/types/types';
|
||||
|
||||
const localSettings = isElectron() ? window.api.localSettings : null;
|
||||
|
||||
export const useServerAuthenticated = () => {
|
||||
const priorServerId = useRef<string | undefined>(undefined);
|
||||
const server = useCurrentServer();
|
||||
const [ready, setReady] = useState(
|
||||
server?.type === ServerType.NAVIDROME ? AuthState.VALID : AuthState.VALID,
|
||||
);
|
||||
const [ready, setReady] = useState(AuthState.VALID);
|
||||
|
||||
const { updateServer } = useAuthStoreActions();
|
||||
const { setCurrentServer, updateServer } = useAuthStoreActions();
|
||||
|
||||
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
|
||||
const authenticateServer = useCallback(
|
||||
async (serverWithAuth: NonNullable<ReturnType<typeof getServerById>>) => {
|
||||
try {
|
||||
await api.controller.getSongList({
|
||||
apiClientProps: { serverId: server?.id || '' },
|
||||
query: {
|
||||
limit: 1,
|
||||
sortBy: SongListSort.NAME,
|
||||
sortOrder: SortOrder.ASC,
|
||||
startIndex: 0,
|
||||
setReady(AuthState.LOADING);
|
||||
|
||||
// Use userId if available, otherwise fall back to username (for Subsonic/Navidrome)
|
||||
const userId = serverWithAuth.userId || serverWithAuth.username;
|
||||
|
||||
if (!userId) {
|
||||
throw new Error('No user ID or username available');
|
||||
}
|
||||
|
||||
// First, try getUserInfo to check if current credentials are still valid
|
||||
logFn.info(logMsg[LogCategory.SYSTEM].authenticatingServer, {
|
||||
category: LogCategory.SYSTEM,
|
||||
meta: {
|
||||
method: 'getUserInfo',
|
||||
serverId: serverWithAuth.id,
|
||||
serverName: serverWithAuth.name,
|
||||
serverType: serverWithAuth.type,
|
||||
},
|
||||
});
|
||||
|
||||
setReady(AuthState.VALID);
|
||||
try {
|
||||
const userInfo = await api.controller.getUserInfo({
|
||||
apiClientProps: {
|
||||
serverId: serverWithAuth.id,
|
||||
},
|
||||
query: {
|
||||
id: userId,
|
||||
},
|
||||
});
|
||||
|
||||
if (!userInfo) {
|
||||
throw new Error('Failed to get user info');
|
||||
}
|
||||
|
||||
// Update server with user info (in case isAdmin changed)
|
||||
updateServer(serverWithAuth.id, {
|
||||
isAdmin: userInfo.isAdmin,
|
||||
});
|
||||
|
||||
logFn.info(logMsg[LogCategory.SYSTEM].serverAuthenticationSuccess, {
|
||||
category: LogCategory.SYSTEM,
|
||||
meta: {
|
||||
isAdmin: userInfo.isAdmin,
|
||||
method: 'getUserInfo',
|
||||
serverId: serverWithAuth.id,
|
||||
serverName: serverWithAuth.name,
|
||||
serverType: serverWithAuth.type,
|
||||
userId: userInfo.id,
|
||||
},
|
||||
});
|
||||
|
||||
setReady(AuthState.VALID);
|
||||
return;
|
||||
} catch (getUserInfoError: any) {
|
||||
// Check if it's a forbidden/authentication error (401 or 403)
|
||||
const isForbiddenError =
|
||||
getUserInfoError?.response?.status === 401 ||
|
||||
getUserInfoError?.response?.status === 403 ||
|
||||
getUserInfoError?.message?.toLowerCase().includes('forbidden') ||
|
||||
getUserInfoError?.message?.toLowerCase().includes('unauthorized');
|
||||
|
||||
// Only reauthenticate if it's a forbidden error AND password is saved
|
||||
if (isForbiddenError && serverWithAuth.savePassword && localSettings) {
|
||||
const password = await localSettings.passwordGet(serverWithAuth.id);
|
||||
|
||||
if (password) {
|
||||
logFn.info(logMsg[LogCategory.SYSTEM].authenticatingServer, {
|
||||
category: LogCategory.SYSTEM,
|
||||
meta: {
|
||||
method: 'authenticate',
|
||||
reason: 'getUserInfo failed with forbidden error',
|
||||
serverId: serverWithAuth.id,
|
||||
serverName: serverWithAuth.name,
|
||||
serverType: serverWithAuth.type,
|
||||
url: serverWithAuth.url,
|
||||
},
|
||||
});
|
||||
|
||||
// Authenticate using the API controller
|
||||
const authData = await api.controller.authenticate(
|
||||
serverWithAuth.url,
|
||||
{
|
||||
legacy: false,
|
||||
password,
|
||||
username: serverWithAuth.username,
|
||||
},
|
||||
serverWithAuth.type,
|
||||
);
|
||||
|
||||
if (!authData) {
|
||||
throw new Error('Authentication failed: No data returned');
|
||||
}
|
||||
|
||||
// Update server with new credentials
|
||||
const updatedServer = {
|
||||
credential: authData.credential,
|
||||
isAdmin: authData.isAdmin,
|
||||
userId: authData.userId,
|
||||
username: authData.username,
|
||||
...(authData.ndCredential !== undefined && {
|
||||
ndCredential: authData.ndCredential,
|
||||
}),
|
||||
};
|
||||
|
||||
updateServer(serverWithAuth.id, updatedServer);
|
||||
|
||||
logFn.info(logMsg[LogCategory.SYSTEM].serverAuthenticationSuccess, {
|
||||
category: LogCategory.SYSTEM,
|
||||
meta: {
|
||||
isAdmin: authData.isAdmin,
|
||||
method: 'authenticate',
|
||||
serverId: serverWithAuth.id,
|
||||
serverName: serverWithAuth.name,
|
||||
serverType: serverWithAuth.type,
|
||||
userId: authData.userId,
|
||||
username: authData.username,
|
||||
},
|
||||
});
|
||||
|
||||
setReady(AuthState.VALID);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// If not a forbidden error, or no password saved, rethrow the error
|
||||
throw getUserInfoError;
|
||||
}
|
||||
} catch (error) {
|
||||
// Clear server credentials (and saved password).
|
||||
if (server.savePassword && localSettings) {
|
||||
localSettings.passwordRemove(server.id);
|
||||
const errorMessage = (error as Error).message || 'Authentication failed';
|
||||
|
||||
logFn.error(logMsg[LogCategory.SYSTEM].serverAuthenticationFailed, {
|
||||
category: LogCategory.SYSTEM,
|
||||
meta: {
|
||||
error: errorMessage,
|
||||
serverId: serverWithAuth.id,
|
||||
serverName: serverWithAuth.name,
|
||||
serverType: serverWithAuth.type,
|
||||
},
|
||||
});
|
||||
|
||||
// Clear server credentials and saved password on failure
|
||||
if (serverWithAuth.savePassword && localSettings) {
|
||||
localSettings.passwordRemove(serverWithAuth.id);
|
||||
}
|
||||
|
||||
server.credential = '';
|
||||
updateServer(server.id, server);
|
||||
|
||||
toast.error({ message: (error as Error).message });
|
||||
toast.error({
|
||||
message: errorMessage,
|
||||
});
|
||||
|
||||
// Log the user out by setting current server to null
|
||||
setCurrentServer(null);
|
||||
setReady(AuthState.INVALID);
|
||||
}
|
||||
},
|
||||
[updateServer],
|
||||
[updateServer, setCurrentServer],
|
||||
);
|
||||
|
||||
const debouncedAuth = debounce((server: ServerListItem) => {
|
||||
authenticateNavidrome(server).catch(console.error);
|
||||
}, 300);
|
||||
const debouncedAuth = debounce(
|
||||
(serverWithAuth: NonNullable<ReturnType<typeof getServerById>>) => {
|
||||
authenticateServer(serverWithAuth).catch(console.error);
|
||||
},
|
||||
300,
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (!server) {
|
||||
logFn.debug(logMsg[LogCategory.SYSTEM].serverAuthenticationInvalid, {
|
||||
category: LogCategory.SYSTEM,
|
||||
meta: {
|
||||
reason: 'No server selected',
|
||||
},
|
||||
});
|
||||
setReady(AuthState.INVALID);
|
||||
return;
|
||||
}
|
||||
|
||||
if (priorServerId.current !== server?.id) {
|
||||
const serverWithAuth = getServerById(server!.id);
|
||||
priorServerId.current = server?.id || '';
|
||||
if (priorServerId.current !== server.id) {
|
||||
const serverWithAuth = getServerById(server.id);
|
||||
priorServerId.current = server.id;
|
||||
|
||||
if (server?.type === ServerType.NAVIDROME) {
|
||||
setReady(AuthState.LOADING);
|
||||
debouncedAuth(serverWithAuth!);
|
||||
} else {
|
||||
setReady(AuthState.VALID);
|
||||
if (!serverWithAuth) {
|
||||
logFn.error(logMsg[LogCategory.SYSTEM].serverAuthenticationError, {
|
||||
category: LogCategory.SYSTEM,
|
||||
meta: {
|
||||
reason: 'Server not found in store',
|
||||
serverId: server.id,
|
||||
},
|
||||
});
|
||||
setReady(AuthState.INVALID);
|
||||
return;
|
||||
}
|
||||
|
||||
setReady(AuthState.LOADING);
|
||||
debouncedAuth(serverWithAuth);
|
||||
}
|
||||
}, [debouncedAuth, server]);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user