diff --git a/src/composables/useSession.js b/src/composables/useSession.js index 8583e10d4..56bce0279 100644 --- a/src/composables/useSession.js +++ b/src/composables/useSession.js @@ -3,11 +3,14 @@ import { useRole } from './useRole'; import { useUserConfig } from './useUserConfig'; import axios from 'axios'; import useNotify from './useNotify'; +import { useTokenConfig } from './useTokenConfig'; const TOKEN_MULTIMEDIA = 'tokenMultimedia'; const TOKEN = 'token'; export function useSession() { const { notify } = useNotify(); + let isCheckingToken = false; + let intervalId = null; function getToken() { const localToken = localStorage.getItem(TOKEN); @@ -22,10 +25,24 @@ export function useSession() { return localTokenMultimedia || sessionTokenMultimedia || ''; } - function setToken(data) { - const storage = data.keepLogin ? localStorage : sessionStorage; + function setSession(data) { + let keepLogin = data.keepLogin; + const storage = keepLogin ? localStorage : sessionStorage; storage.setItem(TOKEN, data.token); storage.setItem(TOKEN_MULTIMEDIA, data.tokenMultimedia); + storage.setItem('created', data.created); + storage.setItem('ttl', data.ttl); + sessionStorage.setItem('keepLogin', keepLogin); + } + + function keepLogin() { + return sessionStorage.getItem('keepLogin'); + } + + function setToken({ token, tokenMultimedia }) { + const storage = keepLogin() ? localStorage : sessionStorage; + storage.setItem(TOKEN, token); + storage.setItem(TOKEN_MULTIMEDIA, tokenMultimedia); } async function destroyToken(url, storage, key) { if (storage.getItem(key)) { @@ -45,11 +62,15 @@ export function useSession() { tokenMultimedia: 'Accounts/logout', token: 'VnUsers/logout', }; + const storage = keepLogin() ? localStorage : sessionStorage; + for (const [key, url] of Object.entries(tokens)) { - await destroyToken(url, localStorage, key); - await destroyToken(url, sessionStorage, key); + await destroyToken(url, storage, key); } + localStorage.clear(); + sessionStorage.clear(); + const { setUser } = useState(); setUser({ @@ -59,22 +80,75 @@ export function useSession() { lang: '', darkMode: null, }); + + stopRenewer(); } - async function login(token, tokenMultimedia, keepLogin) { - setToken({ token, tokenMultimedia, keepLogin }); + async function login(data) { + setSession(data); await useRole().fetch(); await useUserConfig().fetch(); + await useTokenConfig().fetch(); + + startInterval(); } function isLoggedIn() { const localToken = localStorage.getItem(TOKEN); const sessionToken = sessionStorage.getItem(TOKEN); - + startInterval(); return !!(localToken || sessionToken); } + function startInterval() { + stopRenewer(); + const renewPeriod = +sessionStorage.getItem('renewPeriod'); + if (!renewPeriod) return; + intervalId = setInterval(() => checkValidity(), renewPeriod * 1000); + } + + function stopRenewer() { + clearInterval(intervalId); + } + + async function renewToken() { + const _token = getToken(); + const token = await axios.post('VnUsers/renewToken', { + headers: { Authorization: _token }, + }); + const _tokenMultimedia = getTokenMultimedia(); + const tokenMultimedia = await axios.post('VnUsers/renewToken', { + headers: { Authorization: _tokenMultimedia }, + }); + setToken({ token: token.data.id, tokenMultimedia: tokenMultimedia.data.id }); + } + + async function checkValidity() { + const { getTokenConfig } = useState(); + + const tokenConfig = getTokenConfig() ?? sessionStorage.getItem('tokenConfig'); + const storage = keepLogin() ? localStorage : sessionStorage; + + const created = +storage.getItem('created'); + const ttl = +storage.getItem('ttl'); + + if (isCheckingToken || !created) return; + isCheckingToken = true; + + const renewPeriodInSeconds = Math.min(ttl, tokenConfig.value.renewPeriod) * 1000; + + const maxDate = created + renewPeriodInSeconds; + const now = new Date().getTime(); + + if (isNaN(renewPeriodInSeconds) || now <= maxDate) { + return (isCheckingToken = false); + } + + await renewToken(); + isCheckingToken = false; + } + return { getToken, getTokenMultimedia, @@ -82,5 +156,8 @@ export function useSession() { destroy, login, isLoggedIn, + checkValidity, + setSession, + renewToken, }; } diff --git a/src/composables/useState.js b/src/composables/useState.js index e0b742a73..e671d41bd 100644 --- a/src/composables/useState.js +++ b/src/composables/useState.js @@ -13,6 +13,7 @@ const user = ref({ }); const roles = ref([]); +const tokenConfig = ref({}); const drawer = ref(true); const headerMounted = ref(false); @@ -52,6 +53,15 @@ export function useState() { function setRoles(data) { roles.value = data; } + function getTokenConfig() { + return computed(() => { + return tokenConfig.value; + }); + } + + function setTokenConfig(data) { + tokenConfig.value = data; + } function set(name, data) { state.value[name] = ref(data); @@ -70,6 +80,8 @@ export function useState() { setUser, getRoles, setRoles, + getTokenConfig, + setTokenConfig, set, get, unset, diff --git a/src/composables/useTokenConfig.js b/src/composables/useTokenConfig.js new file mode 100644 index 000000000..5cf1b34ee --- /dev/null +++ b/src/composables/useTokenConfig.js @@ -0,0 +1,28 @@ +import axios from 'axios'; +import { useState } from './useState'; +import useNotify from './useNotify'; + +export function useTokenConfig() { + const state = useState(); + const { notify } = useNotify(); + + async function fetch() { + try { + const { data } = await axios.get('AccessTokenConfigs/findOne', { + filter: { fields: ['renewInterval', 'renewPeriod'] }, + }); + if (!data) return; + state.setTokenConfig(data); + sessionStorage.setItem('renewPeriod', data.renewPeriod); + return data; + } catch (error) { + notify('errors.tokenConfig', 'negative'); + console.error('Error fetching token config:', error); + } + } + + return { + fetch, + state, + }; +} diff --git a/src/i18n/locale/en.yml b/src/i18n/locale/en.yml index 2b71195d1..96adf0e23 100644 --- a/src/i18n/locale/en.yml +++ b/src/i18n/locale/en.yml @@ -106,6 +106,7 @@ errors: statusBadGateway: It seems that the server has fall down statusGatewayTimeout: Could not contact the server userConfig: Error fetching user config + tokenConfig: Error fetching token config writeRequest: The requested operation could not be completed login: title: Login diff --git a/src/i18n/locale/es.yml b/src/i18n/locale/es.yml index d1823b387..cb2be6dc9 100644 --- a/src/i18n/locale/es.yml +++ b/src/i18n/locale/es.yml @@ -106,6 +106,7 @@ errors: statusBadGateway: Parece ser que el servidor ha caído statusGatewayTimeout: No se ha podido contactar con el servidor userConfig: Error al obtener configuración de usuario + tokenConfig: Error al obtener configuración de token writeRequest: No se pudo completar la operación solicitada login: title: Inicio de sesión diff --git a/src/pages/Login/LoginMain.vue b/src/pages/Login/LoginMain.vue index fcde51edf..5a3490f50 100644 --- a/src/pages/Login/LoginMain.vue +++ b/src/pages/Login/LoginMain.vue @@ -38,7 +38,13 @@ async function onSubmit() { if (!multimediaToken) return; - await session.login(data.token, multimediaToken.id, keepLogin.value); + const login = { + ...data, + created: Date.now(), + tokenMultimedia: multimediaToken.id, + keepLogin: keepLogin.value, + }; + await session.login(login); quasar.notify({ message: t('login.loginSuccess'), diff --git a/src/router/index.js b/src/router/index.js index 3e442f0e6..7a0aedcae 100644 --- a/src/router/index.js +++ b/src/router/index.js @@ -12,6 +12,7 @@ import { useSession } from 'src/composables/useSession'; import { useRole } from 'src/composables/useRole'; import { useUserConfig } from 'src/composables/useUserConfig'; import { toLowerCamel } from 'src/filters'; +import { useTokenConfig } from 'src/composables/useTokenConfig'; const state = useState(); const session = useSession(); @@ -55,6 +56,7 @@ export default route(function (/* { store, ssrContext } */) { if (stateRoles.length === 0) { await useRole().fetch(); await useUserConfig().fetch(); + await useTokenConfig().fetch(); } const matches = to.matched; const hasRequiredRoles = matches.every((route) => { diff --git a/test/vitest/__tests__/composables/useSession.spec.js b/test/vitest/__tests__/composables/useSession.spec.js index f9f3dcb80..2292859a9 100644 --- a/test/vitest/__tests__/composables/useSession.spec.js +++ b/test/vitest/__tests__/composables/useSession.spec.js @@ -1,5 +1,5 @@ -import { vi, describe, expect, it } from 'vitest'; -import { axios } from 'app/test/vitest/helper'; +import { vi, describe, expect, it, beforeAll, beforeEach } from 'vitest'; +import { axios, flushPromises } from 'app/test/vitest/helper'; import { useSession } from 'composables/useSession'; import { useState } from 'composables/useState'; @@ -63,73 +63,148 @@ describe('session', () => { }); }); - describe('login', () => { - const expectedUser = { - id: 999, - name: `T'Challa`, - nickname: 'Black Panther', - lang: 'en', - userConfig: { - darkMode: false, - }, - }; - const rolesData = [ - { - role: { - name: 'salesPerson', + describe( + 'login', + () => { + const expectedUser = { + id: 999, + name: `T'Challa`, + nickname: 'Black Panther', + lang: 'en', + userConfig: { + darkMode: false, }, - }, - { - role: { - name: 'admin', + }; + const rolesData = [ + { + role: { + name: 'salesPerson', + }, }, - }, - ]; + { + role: { + name: 'admin', + }, + }, + ]; - it('should fetch the user roles and then set token in the sessionStorage', async () => { - const expectedRoles = ['salesPerson', 'admin']; - vi.spyOn(axios, 'get').mockResolvedValue({ - data: { roles: rolesData, user: expectedUser }, + it('should fetch the user roles and then set token in the sessionStorage', async () => { + const expectedRoles = ['salesPerson', 'admin']; + vi.spyOn(axios, 'get').mockResolvedValue({ + data: { roles: rolesData, user: expectedUser }, + }); + + const expectedToken = 'mySessionToken'; + const expectedTokenMultimedia = 'mySessionTokenMultimedia'; + const keepLogin = false; + + await session.login({ + token: expectedToken, + tokenMultimedia: expectedTokenMultimedia, + keepLogin, + }); + + const roles = state.getRoles(); + const localToken = localStorage.getItem('token'); + const sessionToken = sessionStorage.getItem('token'); + + expect(roles.value).toEqual(expectedRoles); + expect(localToken).toBeNull(); + expect(sessionToken).toEqual(expectedToken); + + await session.destroy(); // this clears token and user for any other test }); - const expectedToken = 'mySessionToken'; - const expectedTokenMultimedia = 'mySessionTokenMultimedia'; - const keepLogin = false; + it('should fetch the user roles and then set token in the localStorage', async () => { + const expectedRoles = ['salesPerson', 'admin']; + vi.spyOn(axios, 'get').mockResolvedValue({ + data: { roles: rolesData, user: expectedUser }, + }); - await session.login(expectedToken,expectedTokenMultimedia, keepLogin); + const expectedToken = 'myLocalToken'; + const expectedTokenMultimedia = 'myLocalTokenMultimedia'; + const keepLogin = true; - const roles = state.getRoles(); - const localToken = localStorage.getItem('token'); - const sessionToken = sessionStorage.getItem('token'); + await session.login({ + token: expectedToken, + tokenMultimedia: expectedTokenMultimedia, + keepLogin, + }); - expect(roles.value).toEqual(expectedRoles); - expect(localToken).toBeNull(); - expect(sessionToken).toEqual(expectedToken); + const roles = state.getRoles(); + const localToken = localStorage.getItem('token'); + const sessionToken = sessionStorage.getItem('token'); - await session.destroy(); // this clears token and user for any other test + expect(roles.value).toEqual(expectedRoles); + expect(localToken).toEqual(expectedToken); + expect(sessionToken).toBeNull(); + + await session.destroy(); // this clears token and user for any other test + }); + }, + {} + ); + + describe('RenewToken', () => { + const expectedToken = 'myToken'; + const expectedTokenMultimedia = 'myTokenMultimedia'; + const currentDate = new Date(); + beforeAll(() => { + const tokenConfig = { + id: 1, + renewPeriod: 21600, + courtesyTime: 60, + renewInterval: 300, + }; + state.setTokenConfig(tokenConfig); + sessionStorage.setItem('renewPeriod', 1); }); + it('NOT Should renewToken', async () => { + const data = { + token: expectedToken, + tokenMultimedia: expectedTokenMultimedia, + keepLogin: false, + ttl: 1, + created: Date.now(), + }; + session.setSession(data); + expect(sessionStorage.getItem('keepLogin')).toBeFalsy(); + expect(sessionStorage.getItem('created')).toBeDefined(); + expect(sessionStorage.getItem('ttl')).toEqual(1); + await session.checkValidity(); + expect(sessionStorage.getItem('token')).toEqual(expectedToken); + expect(sessionStorage.getItem('tokenMultimedia')).toEqual( + expectedTokenMultimedia + ); + }); + it('Should renewToken', async () => { + currentDate.setMinutes(currentDate.getMinutes() - 100); + const data = { + token: expectedToken, + tokenMultimedia: expectedTokenMultimedia, + keepLogin: false, + ttl: 1, + created: currentDate, + }; + session.setSession(data); - it('should fetch the user roles and then set token in the localStorage', async () => { - const expectedRoles = ['salesPerson', 'admin']; - vi.spyOn(axios, 'get').mockResolvedValue({ - data: { roles: rolesData, user: expectedUser }, - }); - - const expectedToken = 'myLocalToken'; - const expectedTokenMultimedia = 'myLocalTokenMultimedia'; - const keepLogin = true; - - await session.login(expectedToken, expectedTokenMultimedia, keepLogin); - - const roles = state.getRoles(); - const localToken = localStorage.getItem('token'); - const sessionToken = sessionStorage.getItem('token'); - - expect(roles.value).toEqual(expectedRoles); - expect(localToken).toEqual(expectedToken); - expect(sessionToken).toBeNull(); - - await session.destroy(); // this clears token and user for any other test + vi.spyOn(axios, 'post') + .mockResolvedValueOnce({ + data: { id: '' }, + }) + .mockResolvedValueOnce({ + data: { + id: '', + }, + }); + expect(sessionStorage.getItem('keepLogin')).toBeFalsy(); + expect(sessionStorage.getItem('created')).toBeDefined(); + expect(sessionStorage.getItem('ttl')).toEqual(1); + await session.checkValidity(); + expect(sessionStorage.getItem('token')).not.toEqual(expectedToken); + expect(sessionStorage.getItem('tokenMultimedia')).not.toEqual( + expectedTokenMultimedia + ); }); }); }); diff --git a/test/vitest/__tests__/composables/useTokenConfig.spec.js b/test/vitest/__tests__/composables/useTokenConfig.spec.js new file mode 100644 index 000000000..a25a4abb1 --- /dev/null +++ b/test/vitest/__tests__/composables/useTokenConfig.spec.js @@ -0,0 +1,31 @@ +import { vi, describe, expect, it } from 'vitest'; +import { axios, flushPromises } from 'app/test/vitest/helper'; +import { useTokenConfig } from 'composables/useTokenConfig'; +const tokenConfig = useTokenConfig(); + +describe('useTokenConfig', () => { + describe('fetch', () => { + it('should call setTokenConfig of the state with the expected data', async () => { + const data = { + id: 1, + renewPeriod: 21600, + courtesyTime: 60, + renewInterval: 300, + }; + vi.spyOn(axios, 'get').mockResolvedValueOnce({ + data, + }); + + vi.spyOn(tokenConfig.state, 'setTokenConfig'); + + tokenConfig.fetch(); + + await flushPromises(); + + expect(tokenConfig.state.setTokenConfig).toHaveBeenCalledWith(data); + + const renewPeriod = sessionStorage.getItem('renewPeriod'); + expect(renewPeriod).toEqual(data.renewPeriod); + }); + }); +});