mirror of
https://github.com/jeffvli/feishin.git
synced 2026-07-22 02:16:39 +02:00
fix session invalidation on invalid credentials (#2230)
This commit is contained in:
@@ -4,6 +4,7 @@ import qs from 'qs';
|
|||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
|
|
||||||
import i18n from '/@/i18n/i18n';
|
import i18n from '/@/i18n/i18n';
|
||||||
|
import { authenticationFailure } from '/@/renderer/api/utils';
|
||||||
import { useAuthStore } from '/@/renderer/store';
|
import { useAuthStore } from '/@/renderer/store';
|
||||||
import { getServerUrl } from '/@/renderer/utils/normalize-server-url';
|
import { getServerUrl } from '/@/renderer/utils/normalize-server-url';
|
||||||
import { ssType } from '/@/shared/api/subsonic/subsonic-types';
|
import { ssType } from '/@/shared/api/subsonic/subsonic-types';
|
||||||
@@ -12,6 +13,8 @@ import { toast } from '/@/shared/components/toast/toast';
|
|||||||
import { ServerListItemWithCredential } from '/@/shared/types/domain-types';
|
import { ServerListItemWithCredential } from '/@/shared/types/domain-types';
|
||||||
import { ServerFeature } from '/@/shared/types/features-types';
|
import { ServerFeature } from '/@/shared/types/features-types';
|
||||||
|
|
||||||
|
const SUBSONIC_AUTH_ERROR_CODE = 40;
|
||||||
|
|
||||||
const c = initContract();
|
const c = initContract();
|
||||||
|
|
||||||
export const contract = c.router({
|
export const contract = c.router({
|
||||||
@@ -392,17 +395,25 @@ axiosClient.interceptors.response.use(
|
|||||||
if (data['subsonic-response'].status !== 'ok') {
|
if (data['subsonic-response'].status !== 'ok') {
|
||||||
// Suppress code related to non-linked lastfm or spotify from Navidrome
|
// Suppress code related to non-linked lastfm or spotify from Navidrome
|
||||||
if (data['subsonic-response'].error.code !== 0) {
|
if (data['subsonic-response'].error.code !== 0) {
|
||||||
const isAuthenticated = Boolean(useAuthStore.getState().currentServer?.credential);
|
const currentServer = useAuthStore.getState().currentServer;
|
||||||
|
const isAuthenticated = Boolean(currentServer?.credential);
|
||||||
|
const errorCode = data['subsonic-response'].error.code;
|
||||||
|
const errorMessage = data['subsonic-response'].error.message as string | undefined;
|
||||||
|
// Servers may return code as string ("40") — coerce before comparing
|
||||||
|
const numericCode = Number(errorCode);
|
||||||
|
const isAuthError = numericCode === SUBSONIC_AUTH_ERROR_CODE;
|
||||||
|
|
||||||
if (isAuthenticated) {
|
if (isAuthenticated && isAuthError) {
|
||||||
|
authenticationFailure(currentServer, errorMessage);
|
||||||
|
} else if (isAuthenticated) {
|
||||||
toast.error({
|
toast.error({
|
||||||
message: data['subsonic-response'].error.message,
|
message: errorMessage,
|
||||||
title: i18n.t('error.genericError') as string,
|
title: i18n.t('error.genericError') as string,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Since we do status === 200, override this value with the error code
|
// Since we do status === 200, override this value with the error code
|
||||||
response.status = data['subsonic-response'].error.code;
|
response.status = numericCode || errorCode;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,17 +1,44 @@
|
|||||||
|
import isElectron from 'is-electron';
|
||||||
|
|
||||||
|
import i18n from '/@/i18n/i18n';
|
||||||
import { useAuthStore } from '/@/renderer/store';
|
import { useAuthStore } from '/@/renderer/store';
|
||||||
import { toast } from '/@/shared/components/toast/toast';
|
import { toast } from '/@/shared/components/toast/toast';
|
||||||
import { ServerListItem } from '/@/shared/types/types';
|
import { ServerListItem } from '/@/shared/types/types';
|
||||||
|
|
||||||
export const authenticationFailure = (currentServer: null | ServerListItem) => {
|
const localSettings = isElectron() ? window.api.localSettings : null;
|
||||||
|
|
||||||
|
const AUTH_FAILURE_TOAST_ID = 'auth-failure';
|
||||||
|
|
||||||
|
export const authenticationFailure = (
|
||||||
|
currentServer: null | ServerListItem,
|
||||||
|
message?: string,
|
||||||
|
) => {
|
||||||
|
const store = useAuthStore.getState();
|
||||||
|
const serverId = currentServer?.id ?? store.currentServer?.id;
|
||||||
|
|
||||||
toast.error({
|
toast.error({
|
||||||
message: 'Your session has expired.',
|
id: AUTH_FAILURE_TOAST_ID,
|
||||||
|
message: message ?? (i18n.t('error.sessionExpiredError') as string),
|
||||||
});
|
});
|
||||||
|
|
||||||
if (currentServer) {
|
if (!serverId) {
|
||||||
const serverId = currentServer.id;
|
return;
|
||||||
const token = currentServer.ndCredential;
|
}
|
||||||
console.error(`token is expired: ${token}`);
|
|
||||||
useAuthStore.getState().actions.updateServer(serverId, { ndCredential: undefined });
|
console.error(
|
||||||
useAuthStore.getState().actions.setCurrentServer(null);
|
`token is expired: ${currentServer?.ndCredential ?? store.currentServer?.ndCredential}`,
|
||||||
|
);
|
||||||
|
localSettings?.passwordRemove(serverId);
|
||||||
|
|
||||||
|
// logout() clears credentials on the server list entry and sets currentServer to null.
|
||||||
|
// If there is no current server, still clear the matching server list entry.
|
||||||
|
if (store.currentServer) {
|
||||||
|
store.actions.logout();
|
||||||
|
} else {
|
||||||
|
store.actions.updateServer(serverId, {
|
||||||
|
credential: '',
|
||||||
|
ndCredential: undefined,
|
||||||
|
savePassword: false,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -92,7 +92,9 @@ function ServerSelector() {
|
|||||||
const isNavidromeExpired =
|
const isNavidromeExpired =
|
||||||
server.type === ServerType.NAVIDROME && !server.ndCredential;
|
server.type === ServerType.NAVIDROME && !server.ndCredential;
|
||||||
const isJellyfinExpired = server.type === ServerType.JELLYFIN && !server.credential;
|
const isJellyfinExpired = server.type === ServerType.JELLYFIN && !server.credential;
|
||||||
const isSessionExpired = isNavidromeExpired || isJellyfinExpired;
|
const isSubsonicExpired = server.type === ServerType.SUBSONIC && !server.credential;
|
||||||
|
const isSessionExpired =
|
||||||
|
isNavidromeExpired || isJellyfinExpired || isSubsonicExpired;
|
||||||
|
|
||||||
const logo =
|
const logo =
|
||||||
server.type === ServerType.NAVIDROME
|
server.type === ServerType.NAVIDROME
|
||||||
@@ -113,7 +115,11 @@ function ServerSelector() {
|
|||||||
return handleCredentialsModal(server);
|
return handleCredentialsModal(server);
|
||||||
}}
|
}}
|
||||||
size="lg"
|
size="lg"
|
||||||
variant={server.id === currentServer?.id ? 'filled' : 'default'}
|
variant={
|
||||||
|
server.id === currentServer?.id && !isSessionExpired
|
||||||
|
? 'filled'
|
||||||
|
: 'default'
|
||||||
|
}
|
||||||
>
|
>
|
||||||
<Group className={styles.serverRow} justify="space-between">
|
<Group className={styles.serverRow} justify="space-between">
|
||||||
<Group>
|
<Group>
|
||||||
|
|||||||
@@ -114,7 +114,9 @@ export const ServerSelectorItems = () => {
|
|||||||
const isNavidromeExpired =
|
const isNavidromeExpired =
|
||||||
server.type === ServerType.NAVIDROME && !server.ndCredential;
|
server.type === ServerType.NAVIDROME && !server.ndCredential;
|
||||||
const isJellyfinExpired = server.type === ServerType.JELLYFIN && !server.credential;
|
const isJellyfinExpired = server.type === ServerType.JELLYFIN && !server.credential;
|
||||||
const isSessionExpired = isNavidromeExpired || isJellyfinExpired;
|
const isSubsonicExpired = server.type === ServerType.SUBSONIC && !server.credential;
|
||||||
|
const isSessionExpired =
|
||||||
|
isNavidromeExpired || isJellyfinExpired || isSubsonicExpired;
|
||||||
|
|
||||||
const logo =
|
const logo =
|
||||||
server.type === ServerType.NAVIDROME
|
server.type === ServerType.NAVIDROME
|
||||||
@@ -125,7 +127,7 @@ export const ServerSelectorItems = () => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<DropdownMenu.Item
|
<DropdownMenu.Item
|
||||||
isSelected={currentServer?.id === server.id}
|
isSelected={currentServer?.id === server.id && !isSessionExpired}
|
||||||
key={`server-${server.id}`}
|
key={`server-${server.id}`}
|
||||||
leftSection={<img src={logo} style={{ height: '1rem', width: '1rem' }} />}
|
leftSection={<img src={logo} style={{ height: '1rem', width: '1rem' }} />}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
|
|||||||
@@ -6,13 +6,17 @@ import { normalizeServerUrl } from '/@/renderer/features/action-required/utils/s
|
|||||||
import { isServerLock } from '/@/renderer/features/action-required/utils/window-properties';
|
import { isServerLock } from '/@/renderer/features/action-required/utils/window-properties';
|
||||||
import { AppRoute } from '/@/renderer/router/routes';
|
import { AppRoute } from '/@/renderer/router/routes';
|
||||||
import { useAuthStore, useAuthStoreActions } from '/@/renderer/store';
|
import { useAuthStore, useAuthStoreActions } from '/@/renderer/store';
|
||||||
|
import { ServerType } from '/@/shared/types/domain-types';
|
||||||
|
|
||||||
export const AppOutlet = () => {
|
export const AppOutlet = () => {
|
||||||
const currentServer = useAuthStore(
|
const currentServer = useAuthStore(
|
||||||
(state) =>
|
(state) =>
|
||||||
state.currentServer
|
state.currentServer
|
||||||
? {
|
? {
|
||||||
|
credential: state.currentServer.credential,
|
||||||
id: state.currentServer.id,
|
id: state.currentServer.id,
|
||||||
|
ndCredential: state.currentServer.ndCredential,
|
||||||
|
type: state.currentServer.type,
|
||||||
url: state.currentServer.url,
|
url: state.currentServer.url,
|
||||||
}
|
}
|
||||||
: null,
|
: null,
|
||||||
@@ -31,6 +35,12 @@ export const AppOutlet = () => {
|
|||||||
return configuredUrl !== persistedUrl;
|
return configuredUrl !== persistedUrl;
|
||||||
}, [currentServer]);
|
}, [currentServer]);
|
||||||
|
|
||||||
|
const hasMissingCredentials = Boolean(
|
||||||
|
currentServer &&
|
||||||
|
(!currentServer.credential ||
|
||||||
|
(currentServer.type === ServerType.NAVIDROME && !currentServer.ndCredential)),
|
||||||
|
);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (hasServerLockMismatch && currentServer && window.SERVER_URL) {
|
if (hasServerLockMismatch && currentServer && window.SERVER_URL) {
|
||||||
updateServer(currentServer.id, {
|
updateServer(currentServer.id, {
|
||||||
@@ -40,7 +50,14 @@ export const AppOutlet = () => {
|
|||||||
}
|
}
|
||||||
}, [currentServer, hasServerLockMismatch, setCurrentServer, updateServer]);
|
}, [currentServer, hasServerLockMismatch, setCurrentServer, updateServer]);
|
||||||
|
|
||||||
const isActionsRequired = !currentServer || hasServerLockMismatch;
|
// Clear selection when credentials were wiped but currentServer was left set
|
||||||
|
useEffect(() => {
|
||||||
|
if (hasMissingCredentials) {
|
||||||
|
setCurrentServer(null);
|
||||||
|
}
|
||||||
|
}, [currentServer?.id, currentServer?.type, hasMissingCredentials, setCurrentServer]);
|
||||||
|
|
||||||
|
const isActionsRequired = !currentServer || hasServerLockMismatch || hasMissingCredentials;
|
||||||
|
|
||||||
if (isActionsRequired) {
|
if (isActionsRequired) {
|
||||||
return <Navigate replace to={AppRoute.ACTION_REQUIRED} />;
|
return <Navigate replace to={AppRoute.ACTION_REQUIRED} />;
|
||||||
|
|||||||
Reference in New Issue
Block a user