diff --git a/src/apiclient.d.ts b/src/apiclient.d.ts index 0d8729e7d..fcb949fd2 100644 --- a/src/apiclient.d.ts +++ b/src/apiclient.d.ts @@ -336,6 +336,7 @@ declare module 'jellyfin-apiclient' { handleMessageReceived(msg: any): void; logout(): Promise; minServerVersion(val?: string): string; + updateSavedServerId(server: any): Promise; user(apiClient: ApiClient): Promise; } diff --git a/src/components/ConnectionErrorPage.tsx b/src/components/ConnectionErrorPage.tsx index 3dd69494b..db74acdc5 100644 --- a/src/components/ConnectionErrorPage.tsx +++ b/src/components/ConnectionErrorPage.tsx @@ -1,11 +1,13 @@ -import React, { FC, useEffect, useState } from 'react'; +import DOMPurify from 'dompurify'; +import React, { FC, useCallback, useEffect, useState } from 'react'; import { appHost } from 'components/apphost'; import Page from 'components/Page'; +import toast from 'components/toast/toast'; import { AppFeature } from 'constants/appFeature'; import LinkButton from 'elements/emby-button/LinkButton'; import globalize from 'lib/globalize'; -import { ConnectionState } from 'lib/jellyfin-apiclient'; +import { ConnectionState, ServerConnections } from 'lib/jellyfin-apiclient'; interface ConnectionErrorPageProps { state: ConnectionState @@ -17,9 +19,29 @@ const ConnectionErrorPage: FC = ({ const [ title, setTitle ] = useState(); const [ htmlMessage, setHtmlMessage ] = useState(); const [ message, setMessage ] = useState(); + const [ isConnectDisabled, setIsConnectDisabled ] = useState(false); + + const onForceConnect = useCallback(async () => { + setIsConnectDisabled(true); + + try { + const server = ServerConnections.getLastUsedServer(); + await ServerConnections.updateSavedServerId(server); + window.location.reload(); + } catch (err) { + console.error('[ConnectionErrorPage] Failed to force connect to server', err); + toast(globalize.translate('HeaderConnectionFailure')); + setIsConnectDisabled(false); + } + }, []); useEffect(() => { switch (state) { + case ConnectionState.ServerMismatch: + setTitle(globalize.translate('HeaderServerMismatch')); + setHtmlMessage(undefined); + setMessage(globalize.translate('MessageServerMismatch')); + return; case ConnectionState.ServerUpdateNeeded: setTitle(globalize.translate('HeaderUpdateRequired')); setHtmlMessage(globalize.translate( @@ -46,11 +68,17 @@ const ConnectionErrorPage: FC = ({

{title}

{htmlMessage && ( -

+

)} {message && ( -

{message}

+

+ {message} +

)} + {appHost.supports(AppFeature.MultiServer) && ( = ({ {globalize.translate('ButtonChangeServer')} )} + + {state === ConnectionState.ServerMismatch && ( + + {globalize.translate('ConnectAnyway')} + + )}
); diff --git a/src/components/ConnectionRequired.tsx b/src/components/ConnectionRequired.tsx index 9fd4cd45d..5f69a10f6 100644 --- a/src/components/ConnectionRequired.tsx +++ b/src/components/ConnectionRequired.tsx @@ -31,6 +31,12 @@ type ConnectionRequiredProps = { level?: AccessLevelValue }; +const ERROR_STATES = [ + ConnectionState.ServerMismatch, + ConnectionState.ServerUpdateNeeded, + ConnectionState.Unavailable +]; + const fetchPublicSystemInfo = async (apiClient: ApiClient) => { const infoResponse = await fetch( `${apiClient.serverAddress()}/System/Info/Public`, @@ -184,7 +190,7 @@ const ConnectionRequired: FunctionComponent = ({ console.debug('[ConnectionRequired] connection state', firstConnection?.State); ServerConnections.firstConnection = true; - if ([ ConnectionState.ServerUpdateNeeded, ConnectionState.Unavailable ].includes(firstConnection?.State)) { + if (ERROR_STATES.includes(firstConnection?.State)) { setErrorState(firstConnection.State); } else if (level === AccessLevel.Wizard) { handleWizard(firstConnection) diff --git a/src/lib/jellyfin-apiclient/connectionManager.js b/src/lib/jellyfin-apiclient/connectionManager.js index 7c962c952..898844b6f 100644 --- a/src/lib/jellyfin-apiclient/connectionManager.js +++ b/src/lib/jellyfin-apiclient/connectionManager.js @@ -49,6 +49,10 @@ function normalizeAddress(address) { return address; } +function sortByAccess(a, b) { + return (b.DateLastAccessed || 0) - (a.DateLastAccessed || 0); +} + export default class ConnectionManager { constructor(credentialProvider, appName, appVersion, deviceName, deviceId, capabilities) { console.log('Begin ConnectionManager constructor'); @@ -78,7 +82,7 @@ export default class ConnectionManager { self.getLastUsedServer = () => { const servers = credentialProvider.credentials().Servers; - servers.sort((a, b) => (b.DateLastAccessed || 0) - (a.DateLastAccessed || 0)); + servers.sort(sortByAccess); if (!servers.length) { return null; @@ -345,25 +349,24 @@ export default class ConnectionManager { const servers = credentials.Servers.slice(0); - servers.sort((a, b) => (b.DateLastAccessed || 0) - (a.DateLastAccessed || 0)); + servers.sort(sortByAccess); return servers; }; self.getAvailableServers = () => { - console.log('Begin getAvailableServers'); + console.debug('[ConnectionManager] Begin getAvailableServers'); // Clone the array const credentials = credentialProvider.credentials(); - return Promise.all([findServers()]).then((responses) => { - const foundServers = responses[0]; + return findServers().then(foundServers => { const servers = credentials.Servers.slice(0); foundServers.forEach(server => { credentialProvider.addOrUpdateServer(servers, server); }); - servers.sort((a, b) => (b.DateLastAccessed || 0) - (a.DateLastAccessed || 0)); + servers.sort(sortByAccess); credentials.Servers = servers; credentialProvider.credentials(credentials); @@ -508,7 +511,7 @@ export default class ConnectionManager { addressesStrings.push(addresses[addresses.length - 1].url); } - console.log('tryReconnect: ' + addressesStrings.join('|')); + console.info('[ConnectionManager] tryReconnect addresses', addressesStrings); return new Promise((resolve, reject) => { const state = {}; @@ -523,10 +526,10 @@ export default class ConnectionManager { }, url.timeout); }); }); - } + }; self.connectToServer = (server, options) => { - console.log('begin connectToServer'); + console.debug('[ConnectionManager] begin connectToServer'); return new Promise((resolve) => { options = options || {}; @@ -538,17 +541,17 @@ export default class ConnectionManager { result = result.data; if (compareVersions(self.minServerVersion(), result.Version) === 1) { - console.log('minServerVersion requirement not met. Server version: ' + result.Version); + console.warn('[ConnectionManager] minServerVersion requirement not met. Server version:', result.Version); resolve({ State: ConnectionState.ServerUpdateNeeded, Servers: [server] }); } else if (server.Id && result.Id !== server.Id) { - console.log( - 'http request succeeded, but found a different server Id than what was expected' + console.warn( + '[ConnectionManager] http request succeeded, but found a different server Id than what was expected' ); resolve({ - State: ConnectionState.Unavailable + State: ConnectionState.ServerMismatch }); } else { onSuccessfulConnection(server, result, connectionMode, serverUrl, true, resolve, options); @@ -622,6 +625,20 @@ export default class ConnectionManager { } } + self.updateSavedServerId = async (server) => { + const { data: serverResponse } = await tryReconnect(server); + // Update the server ID to match the new value + server.Id = serverResponse.Id; + // Force the user to login again + server.AccessToken = null; + server.UserId = null; + + // Save the updated server in the credential provider + const credentials = credentialProvider.credentials(); + credentialProvider.addOrUpdateServer(credentials.Servers, server); + credentialProvider.credentials(credentials); + }; + function tryConnectToAddress(address, options) { const server = { ManualAddress: address, diff --git a/src/lib/jellyfin-apiclient/connectionState.ts b/src/lib/jellyfin-apiclient/connectionState.ts index 03ff95dc5..b47d6909c 100644 --- a/src/lib/jellyfin-apiclient/connectionState.ts +++ b/src/lib/jellyfin-apiclient/connectionState.ts @@ -3,6 +3,7 @@ */ export const enum ConnectionState { SignedIn = 'SignedIn', + ServerMismatch = 'ServerMismatch', ServerSignIn = 'ServerSignIn', ServerSelection = 'ServerSelection', ServerUpdateNeeded = 'ServerUpdateNeeded', diff --git a/src/strings/en-us.json b/src/strings/en-us.json index a5d9c9534..9dd457388 100644 --- a/src/strings/en-us.json +++ b/src/strings/en-us.json @@ -184,6 +184,7 @@ "ConfirmDeletion": "Confirm Deletion", "ConfirmEndPlayerSession": "Would you like to shut Jellyfin down on {0}?", "Connect": "Connect", + "ConnectAnyway": "Connect Anyway", "Console": "Console", "ContinueWatching": "Continue watching", "Continuing": "Continuing", @@ -523,6 +524,7 @@ "HeaderSeriesOptions": "Series Options", "HeaderSeriesStatus": "Series Status", "HeaderServerAddressSettings": "Server Address Settings", + "HeaderServerMismatch": "Server Mismatch", "HeaderServerUnavailable": "Server Unavailable", "HeaderSetupLibrary": "Set up your media libraries", "HeaderSortBy": "Sort By", @@ -1182,6 +1184,7 @@ "MessageRestoreInProgress": "Restore in progress", "MessageRestoreSuccess": "Restore successfully completed", "MessageSent": "Message sent.", + "MessageServerMismatch": "The server you are connecting to is not the same server that you previously connected to at this address. If this is expected, please select \"Connect Anyway\". Otherwise, your connection or the server might be compromised.", "MessageSplitVersionsError": "An error occurred while splitting versions", "MessageSyncPlayCreateGroupDenied": "Permission required to create a group.", "MessageSyncPlayDisabled": "SyncPlay disabled.",