From def30d52eb04a60f2c4b34f6133cea0e797fb72a Mon Sep 17 00:00:00 2001 From: Luc Claustres Date: Mon, 3 Feb 2025 18:08:48 +0100 Subject: [PATCH] feat: Added a logout event when the user triggers a logout to allow others connected clients to logout as well (closes #1076) --- core/api/services/index.js | 2 +- core/api/services/users/users.service.js | 5 +++++ .../account/KDeleteAccountManager.vue | 2 +- .../components/screen/KLogoutScreen.vue | 4 +++- .../components/screen/KOAuthLogoutScreen.vue | 4 +++- core/client/composables/session.js | 5 +++++ core/client/index.js | 2 +- core/client/utils/utils.session.js | 21 ++++++++++++++++--- 8 files changed, 37 insertions(+), 8 deletions(-) create mode 100644 core/api/services/users/users.service.js diff --git a/core/api/services/index.js b/core/api/services/index.js index d2711ff82..693ea9ea7 100644 --- a/core/api/services/index.js +++ b/core/api/services/index.js @@ -143,7 +143,7 @@ export default async function () { const authConfig = app.get('authentication') if (authConfig) { - await app.createService('users', { modelsPath, servicesPath }) + await app.createService('users', { modelsPath, servicesPath, methods: ['logout'], events: ['logout'] }) debug('\'users\' service created') await app.createService('account', { servicesPath, diff --git a/core/api/services/users/users.service.js b/core/api/services/users/users.service.js new file mode 100644 index 000000000..c2903552f --- /dev/null +++ b/core/api/services/users/users.service.js @@ -0,0 +1,5 @@ +export default { + logout (user) { + this.emit('logout', user) + } +} diff --git a/core/client/components/account/KDeleteAccountManager.vue b/core/client/components/account/KDeleteAccountManager.vue index d2959e630..7ada53f9a 100644 --- a/core/client/components/account/KDeleteAccountManager.vue +++ b/core/client/components/account/KDeleteAccountManager.vue @@ -48,7 +48,7 @@ async function onDelete () { }).onOk(async (data) => { try { await api.getService('users').remove(User._id) - // Redirecting to logut will logut the user but logout an inexsiting user will raise an error + // Redirecting to logout will logout the user but logout an inexsiting user will raise an error // We prefer to clean the token manually instead // router.push({ name: 'logout' }) Store.set('user', null) diff --git a/core/client/components/screen/KLogoutScreen.vue b/core/client/components/screen/KLogoutScreen.vue index a9c79bc2e..23ee0ce42 100644 --- a/core/client/components/screen/KLogoutScreen.vue +++ b/core/client/components/screen/KLogoutScreen.vue @@ -11,10 +11,12 @@ import config from 'config' import { ref } from 'vue' import KScreen from './KScreen.vue' import { logout } from '../../utils/utils.session.js' +import { Store } from '../../store.js' // Data const actions = ref(_.get(config, 'screens.logout.actions', [])) +const user = Store.get('user') // Immediate -logout() +if (user) logout() diff --git a/core/client/components/screen/KOAuthLogoutScreen.vue b/core/client/components/screen/KOAuthLogoutScreen.vue index 43ba2399d..334c362ce 100644 --- a/core/client/components/screen/KOAuthLogoutScreen.vue +++ b/core/client/components/screen/KOAuthLogoutScreen.vue @@ -12,6 +12,7 @@ import { ref, onMounted } from 'vue' import { useRoute } from 'vue-router' import KScreen from './KScreen.vue' import { logout } from '../../utils/utils.session.js' +import { Store } from '../../store.js' // Data const route = useRoute() @@ -19,10 +20,11 @@ const actions = ref(_.get(config, 'screens.logout.actions', [])) // When called with a prameter this means we should logout from the OAuth provider as well // In this case we do not show the logout screen as the OAuth provider has its own const provider = ref(route.params.provider) +const user = Store.get('user') // Hooks onMounted(async () => { - await logout() + if (user) await logout() // When called with a prameter this means we should logout from the OAuth provider as well if (provider.value) { location.href = `oauth-logout/${provider.value}` diff --git a/core/client/composables/session.js b/core/client/composables/session.js index 04533c76c..031654a8b 100644 --- a/core/client/composables/session.js +++ b/core/client/composables/session.js @@ -174,6 +174,11 @@ export function useSession (options = {}) { } // Then redirection Events.on('user-abilities-changed', redirect) + api.on('logout', () => { + // Used to automatically redirect when the user has requested a logout from another client + // We don't use redirect() here as in this case the user is already logout and it would redirect to login instead + router.push({ name: 'logout' }) + }) try { await restoreSession() diff --git a/core/client/index.js b/core/client/index.js index ad438e215..11612ad7b 100644 --- a/core/client/index.js +++ b/core/client/index.js @@ -92,7 +92,7 @@ export default async function initialize () { Exporter.initialize(_.get(config, 'exporter')) Schema.initialize(_.get(config, 'schema')) - // Listen to the 'patched' event on the users + // Listen to events on the users utils.subscribeToUserChanges() // Register the readers diff --git a/core/client/utils/utils.session.js b/core/client/utils/utils.session.js index f0e011d80..955e92936 100644 --- a/core/client/utils/utils.session.js +++ b/core/client/utils/utils.session.js @@ -66,9 +66,9 @@ export async function restoreSession () { await authenticate(authentication) } catch (error) { // This ensure an old token is not kept e.g. when the user has been deleted - // await logout() // It actually causes a call to the remove method on the authentication service, which fails due to missing access token // See https://github.com/kalisio/kdk/issues/757, as a consequence we prefer to clean the token manually instead + // await logout() await api.authentication.removeAccessToken() // Rethrow for caller to handle throw error @@ -97,14 +97,29 @@ export async function updateUser (user) { } } +export async function logoutUser (user) { + // User has logout from another client session, logout here as well. + // No check required for user ID as only the target user should receive the logout event. + // We cannot use the logout route directly or api.logout() as this will trigger a new remove request to the authentication service. + // Here we just want to set the application state "as if" the user as requested a logout without doing it actually. + await LocalCache.removeItem('authentication') + await api.authentication.removeAccessToken() + await api.authentication.reset() + Store.set('user', null) + // In this specific case as we bypass actual authentication the events will not be emitted + api.emit('logout', user) +} + export function subscribeToUserChanges () { - // Listen to the 'patched' event on the users + // Listen to the 'patched'/'logout' event on the users const users = api.getService('users') users.on('patched', updateUser) + users.on('logout', logoutUser) } export function unsubscribeToUserChanges () { - // Listen to the 'patched' event on the users + // Listen to the 'patched'/'logout' event on the users const users = api.getService('users') users.off('patched', updateUser) + users.off('logout', logoutUser) }