diff --git a/CONTRIBUTORS.md b/CONTRIBUTORS.md index 4ac70e92044d..6c9f4585a2e1 100644 --- a/CONTRIBUTORS.md +++ b/CONTRIBUTORS.md @@ -103,6 +103,7 @@ - [TheBosZ](https://github.com/thebosz) - [qm3jp](https://github.com/qm3jp) - [johnnyg](https://github.com/johnnyg) +- [klaki892](https://github.com/klaki892) ## Emby Contributors diff --git a/src/apps/stable/routes/user/settings/index.tsx b/src/apps/stable/routes/user/settings/index.tsx index df5071f3c9de..ae95b7d8f8cb 100644 --- a/src/apps/stable/routes/user/settings/index.tsx +++ b/src/apps/stable/routes/user/settings/index.tsx @@ -337,6 +337,25 @@ const UserSettingsPage: FC = () => { )} + + + + + + {globalize.translate('ButtonSwitchUser')} + + + + + = ({ onMenuClose(); }, [ onMenuClose ]); + const onSwitchUserClick = useCallback(() => { + Dashboard.switchUser(); + onMenuClose(); + }, [ onMenuClose ]); + return ( = ({ )} + + + + + + {globalize.translate('ButtonSwitchUser')} + + + diff --git a/src/controllers/session/addServer/index.js b/src/controllers/session/addServer/index.js index 68b14e89099e..7c3eadbeae98 100644 --- a/src/controllers/session/addServer/index.js +++ b/src/controllers/session/addServer/index.js @@ -39,7 +39,7 @@ function submitServer(page) { // eslint-disable-next-line sonarjs/slow-regex const host = page.querySelector('#txtServerHost').value.replace(/\/+$/, ''); ServerConnections.connectToAddress(host, { - enableAutoLogin: appSettings.enableAutoLogin() + enableAutoLogin: appSettings.enableRememberMe() }).then(function(result) { handleConnectionResult(page, result); }, function() { diff --git a/src/controllers/session/login/index.html b/src/controllers/session/login/index.html index 63096a3d74cb..f29ad7c67880 100644 --- a/src/controllers/session/login/index.html +++ b/src/controllers/session/login/index.html @@ -17,6 +17,11 @@ ${HeaderPleaseSignIn} ${RememberMe} + + + ${LabelAutomaticallySignIn} + + ${ButtonSignIn} diff --git a/src/controllers/session/login/index.js b/src/controllers/session/login/index.js index aa9f7029fd67..1e0677d9a07a 100644 --- a/src/controllers/session/login/index.js +++ b/src/controllers/session/login/index.js @@ -24,13 +24,13 @@ import './login.scss'; const enableFocusTransform = !browser.slow && !browser.edge; -function authenticateUserByName(page, apiClient, url, username, password) { +function authenticateUserByName(page, apiClient, url, username, password, enableAutoSignIn) { loading.show(); apiClient.authenticateUserByName(username, password).then(function (result) { const user = result.User; loading.hide(); - onLoginSuccessful(user.Id, result.AccessToken, apiClient, url); + onLoginSuccessful(user.Id, result.AccessToken, apiClient, url, enableAutoSignIn); }, function (response) { page.querySelector('#txtManualPassword').value = ''; loading.hide(); @@ -112,13 +112,29 @@ function authenticateQuickConnect(apiClient, targetUrl) { }); } -function onLoginSuccessful(id, accessToken, apiClient, url) { +function onLoginSuccessful(id, accessToken, apiClient, url, enableAutoSignIn) { + // Multi-account auto-login logic + const serverId = apiClient.serverInfo().Id; + if (enableAutoSignIn) { + ServerConnections.setAutoLoginUser(serverId, id); + } else { + const currentAutoLoginUser = ServerConnections.getAutoLoginUser(serverId); + if (currentAutoLoginUser === id) { + ServerConnections.setAutoLoginUser(serverId, null); + } + } + Dashboard.onServerChanged(id, accessToken, apiClient); Dashboard.navigate(url || 'home'); } function showManualForm(context, showCancel, focusPassword) { - context.querySelector('.chkRememberLogin').checked = appSettings.enableAutoLogin(); + const rememberMe = appSettings.enableRememberMe(); + context.querySelector('.chkRememberLogin').checked = rememberMe; + const autoSignIn = context.querySelector('.chkAutoSignIn'); + if (!rememberMe) { + autoSignIn.checked = false; + } context.querySelector('.manualLoginForm').classList.remove('hide'); context.querySelector('.visualLoginForm').classList.add('hide'); context.querySelector('.btnManual').classList.add('hide'); @@ -193,7 +209,7 @@ export default function (view, params) { return ServerConnections.getOrCreateApiClient(serverId); } - return ApiClient; + return ServerConnections.currentApiClient(); } function getTargetUrl() { @@ -234,18 +250,48 @@ export default function (view, params) { } else if (haspw == 'false') { authenticateUserByName(context, getApiClient(), getTargetUrl(), name, ''); } else { - context.querySelector('#txtManualName').value = name; - context.querySelector('#txtManualPassword').value = ''; - showManualForm(context, true, true); + const apiClient = getApiClient(); + const serverId = apiClient.serverInfo().Id; + + loading.show(); + ServerConnections.loginWithSavedCredentials(serverId, id).then(function (result) { + loading.hide(); + if (result.ApiClient && result.ApiClient.accessToken()) { + onLoginSuccessful(id, result.ApiClient.accessToken(), result.ApiClient, getTargetUrl(), false); + } else { + context.querySelector('#txtManualName').value = name; + context.querySelector('#txtManualPassword').value = ''; + showManualForm(context, true, true); + } + }); } } }); view.querySelector('.manualLoginForm').addEventListener('submit', function (e) { - appSettings.enableAutoLogin(view.querySelector('.chkRememberLogin').checked); - authenticateUserByName(view, getApiClient(), getTargetUrl(), view.querySelector('#txtManualName').value, view.querySelector('#txtManualPassword').value); + appSettings.enableRememberMe(view.querySelector('.chkRememberLogin').checked); + authenticateUserByName( + view, + getApiClient(), + getTargetUrl(), + view.querySelector('#txtManualName').value, + view.querySelector('#txtManualPassword').value, + view.querySelector('.chkAutoSignIn').checked + ); e.preventDefault(); return false; }); + + view.querySelector('.chkRememberLogin').addEventListener('change', function () { + if (!this.checked) { + view.querySelector('.chkAutoSignIn').checked = false; + } + }); + + view.querySelector('.chkAutoSignIn').addEventListener('change', function () { + if (this.checked) { + view.querySelector('.chkRememberLogin').checked = true; + } + }); view.querySelector('.btnForgotPassword').addEventListener('click', function () { Dashboard.navigate('forgotpassword'); }); @@ -272,6 +318,12 @@ export default function (view, params) { const apiClient = getApiClient(); + if (!apiClient) { + loading.hide(); + Dashboard.selectServer(); + return; + } + apiClient.getQuickConnect('Enabled') .then(enabled => { if (enabled === true) { diff --git a/src/controllers/session/selectServer/index.js b/src/controllers/session/selectServer/index.js index 1da0932f7984..af2357c6fdbb 100644 --- a/src/controllers/session/selectServer/index.js +++ b/src/controllers/session/selectServer/index.js @@ -3,7 +3,6 @@ import loading from '../../../components/loading/loading'; import { appRouter } from '../../../components/router/appRouter'; import layoutManager from '../../../components/layoutManager'; import libraryMenu from '../../../scripts/libraryMenu'; -import appSettings from '../../../scripts/settings/appSettings'; import focusManager from '../../../components/focusManager'; import globalize from '../../../lib/globalize'; import actionSheet from '../../../components/actionSheet/actionSheet'; @@ -104,9 +103,19 @@ function showServerConnectionFailure() { export default function (view, params) { function connectToServer(server) { + let enableAutoLogin = false; + if (server.AutoLoginUserId && server.Users) { + const user = server.Users.find(u => u.UserId === server.AutoLoginUserId); + if (user && user.AccessToken) { + server.UserId = user.UserId; + server.AccessToken = user.AccessToken; + enableAutoLogin = true; + } + } + loading.show(); ServerConnections.connectToServer(server, { - enableAutoLogin: appSettings.enableAutoLogin() + enableAutoLogin: enableAutoLogin }).then(function (result) { loading.hide(); const apiClient = result.ApiClient; diff --git a/src/lib/jellyfin-apiclient/ServerConnections.js b/src/lib/jellyfin-apiclient/ServerConnections.js index bae007970a48..0876febfe2a8 100644 --- a/src/lib/jellyfin-apiclient/ServerConnections.js +++ b/src/lib/jellyfin-apiclient/ServerConnections.js @@ -42,6 +42,7 @@ class ServerConnections extends ConnectionManager { setUserInfo(null, null); // Ensure the updated credentials are persisted to storage credentialProvider.credentials(credentialProvider.credentials()); + localStorage.removeItem('enableRememberMe'); if (window.NativeShell && typeof window.NativeShell.onLocalUserSignedOut === 'function') { window.NativeShell.onLocalUserSignedOut(logoutInfo); @@ -52,6 +53,7 @@ class ServerConnections extends ConnectionManager { apiClient.getMaxBandwidth = getMaxBandwidth; apiClient.normalizeImageOptions = normalizeImageOptions; }); + this.shouldSaveCredentials = () => appSettings.enableRememberMe(); } initApiClient(server) { @@ -77,7 +79,7 @@ class ServerConnections extends ConnectionManager { connect(options) { return super.connect({ - enableAutoLogin: appSettings.enableAutoLogin(), + enableAutoLogin: appSettings.enableRememberMe(), ...options }); } diff --git a/src/lib/jellyfin-apiclient/connectionManager.js b/src/lib/jellyfin-apiclient/connectionManager.js index 898844b6fcf0..08d7dfb326a7 100644 --- a/src/lib/jellyfin-apiclient/connectionManager.js +++ b/src/lib/jellyfin-apiclient/connectionManager.js @@ -63,6 +63,40 @@ export default class ConnectionManager { // Set the minimum version to match the SDK self._minServerVersion = MINIMUM_VERSION; + self.shouldSaveCredentials = () => true; + + self.setAutoLoginUser = (serverId, userId) => { + const credentials = credentialProvider.credentials(); + const server = credentials.Servers.find(s => s.Id === serverId); + if (server) { + server.AutoLoginUserId = userId; + credentialProvider.addOrUpdateServer(credentials.Servers, server); + credentialProvider.credentials(credentials); + } + }; + + self.getAutoLoginUser = (serverId) => { + const credentials = credentialProvider.credentials(); + const server = credentials.Servers.find(s => s.Id === serverId); + return server ? server.AutoLoginUserId : null; + }; + + self.loginWithSavedCredentials = (serverId, userId) => { + const server = self.getServerInfo(serverId); + if (server) { + // Check for saved user in multi-user list + if (server.Users) { + const user = server.Users.find(u => u.UserId === userId); + if (user && user.AccessToken) { + server.UserId = user.UserId; + server.AccessToken = user.AccessToken; + return self.connectToServer(server, { enableAutoLogin: true }); + } + } + } + return Promise.resolve({ State: ConnectionState.ServerSignIn }); + }; + self.appVersion = () => appVersion; self.appName = () => appName; @@ -114,7 +148,7 @@ export default class ConnectionManager { apiClient.serverInfo(existingServer); - apiClient.onAuthenticated = (instance, result) => onAuthenticated(instance, result, {}, true); + apiClient.onAuthenticated = (instance, result) => onAuthenticated(instance, result, {}, self.shouldSaveCredentials()); if (!existingServers.length) { const credentials = credentialProvider.credentials(); @@ -144,7 +178,7 @@ export default class ConnectionManager { apiClient.serverInfo(server); apiClient.onAuthenticated = (instance, result) => { - return onAuthenticated(instance, result, {}, true); + return onAuthenticated(instance, result, {}, self.shouldSaveCredentials()); }; events.trigger(self, 'apiclientcreated', [apiClient]); @@ -178,9 +212,27 @@ export default class ConnectionManager { } server.Id = result.ServerId; + // Multi-user support: Initialize Users array if it doesn't exist + if (!server.Users) { + server.Users = []; + } + if (saveCredentials) { server.UserId = result.User.Id; server.AccessToken = result.AccessToken; + + // Add or update user in Users array + const existingUserIndex = server.Users.findIndex((u) => u.UserId === result.User.Id); + const userInfo = { + UserId: result.User.Id, + AccessToken: result.AccessToken + }; + + if (existingUserIndex !== -1) { + server.Users[existingUserIndex] = userInfo; + } else { + server.Users.push(userInfo); + } } else { server.UserId = null; server.AccessToken = null; @@ -329,21 +381,40 @@ export default class ConnectionManager { function logoutOfServer(apiClient) { const serverInfo = apiClient.serverInfo() || {}; + const userId = apiClient.getCurrentUserId(); const logoutInfo = { - serverId: serverInfo.Id + serverId: serverInfo.Id, + userId: userId }; return apiClient.logout().then( () => { + removeSavedUser(serverInfo.Id, userId); events.trigger(self, 'localusersignedout', [logoutInfo]); }, () => { + removeSavedUser(serverInfo.Id, userId); events.trigger(self, 'localusersignedout', [logoutInfo]); } ); } + function removeSavedUser(serverId, userId) { + const credentials = credentialProvider.credentials(); + const server = credentials.Servers.find((s) => s.Id === serverId); + + if (server) { + if (server.Users) { + server.Users = server.Users.filter((u) => u.UserId !== userId); + } + + if (server.AutoLoginUserId === userId) { + server.AutoLoginUserId = null; + } + } + } + self.getSavedServers = () => { const credentials = credentialProvider.credentials(); @@ -425,7 +496,21 @@ export default class ConnectionManager { const firstServer = servers.length ? servers[0] : null; // See if we have any saved credentials and can auto sign in if (firstServer) { - return self.connectToServer(firstServer, options).then((result) => { + if (firstServer.AutoLoginUserId && firstServer.Users) { + const autoLoginUser = firstServer.Users.find(u => u.UserId === firstServer.AutoLoginUserId); + if (autoLoginUser && autoLoginUser.AccessToken) { + firstServer.UserId = autoLoginUser.UserId; + firstServer.AccessToken = autoLoginUser.AccessToken; + + return self.connectToServer(firstServer, options).then((result) => { + console.log('resolving connectToServers with result.State: ' + result.State); + return result; + }); + } + } + // We pass enableAutoLogin: false to ensure we connect (getting to user selection) but do NOT auto-login. + const newOptions = Object.assign({}, options, { enableAutoLogin: false }); + return self.connectToServer(firstServer, newOptions).then((result) => { console.log('resolving connectToServers with result.State: ' + result.State); return result; }); diff --git a/src/scripts/libraryMenu.js b/src/scripts/libraryMenu.js index 86fa3bab357b..a1a1ccf18ec9 100644 --- a/src/scripts/libraryMenu.js +++ b/src/scripts/libraryMenu.js @@ -354,6 +354,7 @@ function refreshLibraryInfoInDrawer(user) { } html += `${globalize.translate('Settings')}`; + html += `${globalize.translate('ButtonSwitchUser')}`; html += `${globalize.translate('ButtonSignOut')}`; if (appHost.supports(AppFeature.ExitMenu)) { @@ -385,6 +386,11 @@ function refreshLibraryInfoInDrawer(user) { if (btnLogout) { btnLogout.addEventListener('click', onLogoutClick); } + + const btnSwitchUser = navDrawerScrollContainer.querySelector('.btnSwitchUser'); + if (btnSwitchUser) { + btnSwitchUser.addEventListener('click', onSwitchUserClick); + } } function onSidebarLinkClick() { @@ -518,6 +524,10 @@ function onLogoutClick() { Dashboard.logout(); } +function onSwitchUserClick() { + Dashboard.switchUser(); +} + function updateCastIcon() { const context = document; const info = playbackManager.getPlayerInfo(); diff --git a/src/scripts/settings/appSettings.js b/src/scripts/settings/appSettings.js index d4d56735eded..f91e23052484 100644 --- a/src/scripts/settings/appSettings.js +++ b/src/scripts/settings/appSettings.js @@ -11,12 +11,12 @@ class AppSettings { return name; } - enableAutoLogin(val) { + enableRememberMe(val) { if (val !== undefined) { - this.set('enableAutoLogin', val.toString()); + this.set('enableRememberMe', val.toString()); } - return toBoolean(this.get('enableAutoLogin'), true); + return toBoolean(this.get('enableRememberMe'), true); } /** diff --git a/src/strings/en-us.json b/src/strings/en-us.json index b7a36b267c52..2906a0cb51da 100644 --- a/src/strings/en-us.json +++ b/src/strings/en-us.json @@ -136,6 +136,7 @@ "ButtonShutdown": "Shutdown", "ButtonSignIn": "Sign In", "ButtonSignOut": "Sign Out", + "ButtonSwitchUser": "Switch User", "ButtonExitApp": "Exit Application", "ButtonSpace": "Space", "ButtonSplit": "Split", @@ -624,6 +625,7 @@ "LabelAutomaticallyAddToCollection": "Automatically add to collection", "LabelAutomaticallyAddToCollectionHelp": "When at least 2 movies have the same collection name, they will be automatically added to the collection.", "LabelAutomaticallyRefreshInternetMetadataEvery": "Automatically refresh metadata from the internet", + "LabelAutomaticallySignIn": "Sign in automatically", "LabelAutomaticDiscovery": "Enable Auto Discovery", "LabelAutomaticDiscoveryHelp": "Allow applications to automatically detect Jellyfin by using UDP port 7359.", "LabelAvailable": "Available", diff --git a/src/strings/en_US.json b/src/strings/en_US.json index 3bb119a83ece..e2167660ea49 100644 --- a/src/strings/en_US.json +++ b/src/strings/en_US.json @@ -136,6 +136,7 @@ "ButtonShutdown": "Shutdown", "ButtonSignIn": "Sign In", "ButtonSignOut": "Sign Out", + "ButtonSwitchUser": "Switch User", "ButtonExitApp": "Exit Application", "ButtonSpace": "Space", "ButtonSplit": "Split", diff --git a/src/utils/dashboard.js b/src/utils/dashboard.js index b7deb9f8d02d..e018caa2e41c 100644 --- a/src/utils/dashboard.js +++ b/src/utils/dashboard.js @@ -233,6 +233,19 @@ export const pageIdOn = function(eventName, id, fn) { }); }; +export function switchUser() { + const apiClient = ServerConnections.currentApiClient(); + let url = 'login'; + + if (apiClient) { + const serverId = apiClient.serverInfo().Id; + ServerConnections.setAutoLoginUser(serverId, null); + url += '?serverid=' + serverId; + } + + navigate(url); +} + const Dashboard = { alert, capabilities, @@ -249,6 +262,7 @@ const Dashboard = { processPluginConfigurationUpdateResult, processServerConfigurationUpdateResult, selectServer, + switchUser, serverAddress, showLoadingMsg, datetime,