From 261fddb1cf0b2677413064040986a417049b4fea Mon Sep 17 00:00:00 2001 From: wbuezas Date: Tue, 1 Oct 2024 14:19:34 -0300 Subject: [PATCH 01/10] Renew token --- src/i18n/ca-ES/index.js | 7 +- src/i18n/en-US/index.js | 7 +- src/i18n/es-ES/index.js | 7 +- src/i18n/fr-FR/index.js | 8 +- src/i18n/pt-PT/index.js | 7 +- src/stores/user.js | 161 ++++++++++++++++++++++++++++++++++------ 6 files changed, 168 insertions(+), 29 deletions(-) diff --git a/src/i18n/ca-ES/index.js b/src/i18n/ca-ES/index.js index 0e033814..ae946728 100644 --- a/src/i18n/ca-ES/index.js +++ b/src/i18n/ca-ES/index.js @@ -161,5 +161,10 @@ export default { ErrCantWrite: "No s'ha pogut escriure el fitxer al disc", ErrExtension: "La pujada del fitxer s'ha aturat per una extensió", ErrDefault: 'Error de pujada desconegut', - 'Sync complete': 'Sincronització completa' + 'Sync complete': 'Sincronització completa', + // Errors + errors: { + statusUnauthorized: 'Accés denegat', + tokenConfig: 'Error al obtenir la configuració del token' + } }; diff --git a/src/i18n/en-US/index.js b/src/i18n/en-US/index.js index e3f546f8..4b478c83 100644 --- a/src/i18n/en-US/index.js +++ b/src/i18n/en-US/index.js @@ -194,5 +194,10 @@ export default { ErrCantWrite: 'Failed to write file to disk', ErrExtension: 'File upload stopped by extension', ErrDefault: 'Unknown upload error', - 'Sync complete': 'Synchronization complete' + 'Sync complete': 'Synchronization complete', + // Errors + errors: { + statusUnauthorized: 'Access denied', + tokenConfig: 'Error fetching token config' + } }; diff --git a/src/i18n/es-ES/index.js b/src/i18n/es-ES/index.js index d1fbdd7a..e9f609c8 100644 --- a/src/i18n/es-ES/index.js +++ b/src/i18n/es-ES/index.js @@ -194,5 +194,10 @@ export default { ErrCantWrite: 'Failed to write file to disk', ErrExtension: 'File upload stopped by extension', ErrDefault: 'Unknown upload error', - 'Sync complete': 'Sincronización completada' + 'Sync complete': 'Sincronización completada', + // Errors + errors: { + statusUnauthorized: 'Acceso denegado', + tokenConfig: 'Error al obtener configuración de token' + } }; diff --git a/src/i18n/fr-FR/index.js b/src/i18n/fr-FR/index.js index 6c04a77f..43462a49 100644 --- a/src/i18n/fr-FR/index.js +++ b/src/i18n/fr-FR/index.js @@ -165,5 +165,11 @@ export default { ErrCantWrite: "Échec de l'écriture du fichier sur le disque", ErrExtension: 'Téléchargement du fichier arrêté par extension', ErrDefault: 'Erreur de téléchargement inconnue', - 'Sync complete': 'Synchronisation terminée' + 'Sync complete': 'Synchronisation terminée', + // Errors + errors: { + statusUnauthorized: 'Accès refusé', + tokenConfig: + 'Erreur lors de la récupération de la configuration du jeton' + } }; diff --git a/src/i18n/pt-PT/index.js b/src/i18n/pt-PT/index.js index abaf4f49..018d54a8 100644 --- a/src/i18n/pt-PT/index.js +++ b/src/i18n/pt-PT/index.js @@ -158,5 +158,10 @@ export default { ErrCantWrite: 'Erro ao gravar arquivo no disco', ErrExtension: 'Erro de extensão do arquivo', ErrDefault: 'Erro desconhecido ao subir arquivo', - 'Sync complete': 'Sincronização completa' + 'Sync complete': 'Sincronização completa', + // Errors + errors: { + statusUnauthorized: 'Acesso negado', + tokenConfig: 'Erro ao obter configuração do token' + } }; diff --git a/src/stores/user.js b/src/stores/user.js index 5c347005..1e998ba2 100644 --- a/src/stores/user.js +++ b/src/stores/user.js @@ -9,12 +9,8 @@ const { notify } = useNotify(); export const useUserStore = defineStore('user', { state: () => { - const token = - sessionStorage.getItem('vnToken') || - localStorage.getItem('vnToken'); - return { - token, + token: '', isGuest: false, user: null, supplantedUser: null, @@ -24,7 +20,11 @@ export const useUserStore = defineStore('user', { { label: t('langs.ca'), lang: 'ca-ES', value: 'ca' }, { label: t('langs.fr'), lang: 'fr-FR', value: 'fr' }, { label: t('langs.pt'), lang: 'pt-PT', value: 'pt' } - ] + ], + keepLogin: false, + intervalId: null, + isCheckingToken: false, + tokenConfig: null }; }, @@ -46,10 +46,77 @@ export const useUserStore = defineStore('user', { this.updateSiteLocale(); }, - async getToken() { - this.token = + getToken() { + const token = sessionStorage.getItem('vnToken') || localStorage.getItem('vnToken'); + this.token = token; + return token; + }, + + setToken(token) { + const storage = this.keepLogin ? localStorage : sessionStorage; + storage.setItem('vnToken', token); + this.token = token; + }, + + async destroyToken(url, storage, key) { + if (storage.getItem(key)) { + try { + await api.post(url, null, { + headers: { Authorization: storage.getItem(key) } + }); + } catch (error) { + notify('errors.statusUnauthorized', 'negative'); + } finally { + storage.removeItem(key); + } + } + }, + + async destroy(destroyTokens = true) { + const tokens = { + tokenMultimedia: 'Accounts/logout', + token: 'VnUsers/logout' + }; + const storage = this.keepLogin ? localStorage : sessionStorage; + let destroyTokenPromises = []; + try { + if (destroyTokens) { + const { data: isValidToken } = await api.get( + 'VnUsers/validateToken' + ); + if (isValidToken) + destroyTokenPromises = Object.entries(tokens).map( + ([key, url]) => this.destroyToken(url, storage, key) + ); + } + } finally { + localStorage.clear(); + sessionStorage.clear(); + await Promise.allSettled(destroyTokenPromises); + this.user = null; + this.stopRenewer(); + } + }, + + setSession(data) { + const storage = this.keepLogin ? localStorage : sessionStorage; + storage.setItem('vnToken', data.token); + storage.setItem('created', data.created); + storage.setItem('ttl', data.ttl); + sessionStorage.setItem('keepLogin', this.keepLogin); + }, + + async login(user, password, remember) { + const params = { user, password }; + const { data } = await api.post('Accounts/login', params); + this.name = user; + this.keepLogin = remember; + this.setToken(data.token); + this.setSession({ created: Date.now(), ...data }); + await this.fetchTokenConfig(); + this.startInterval(); }, async tryAutoLogin() { @@ -67,22 +134,6 @@ export const useUserStore = defineStore('user', { return false; }, - async login(user, password, remember) { - const params = { user, password }; - const res = await api.post('Accounts/login', params); - - if (remember) { - localStorage.setItem('vnToken', res.data.token); - } else { - sessionStorage.setItem('vnToken', res.data.token); - } - - this.$patch({ - token: res.data.token, - name: user - }); - }, - async logout() { if (this.token != null) { try { @@ -96,6 +147,68 @@ export const useUserStore = defineStore('user', { useAppStore().onLogout(); }, + async fetchTokenConfig() { + try { + const { data } = await api.get('AccessTokenConfigs/findOne', { + filter: { fields: ['renewInterval', 'renewPeriod'] } + }); + if (!data) return; + this.tokenConfig = data; + sessionStorage.setItem('tokenConfig', data); + sessionStorage.setItem('renewPeriod', data.renewPeriod); + return data; + } catch (error) { + notify('errors.tokenConfig', 'negative'); + console.error('Error fetching token config:', error); + } + }, + + async renewToken() { + const _token = this.getToken(); + const token = await api.post('VnUsers/renewToken', { + headers: { Authorization: _token } + }); + + this.setToken(token.data.id); + }, + + async checkValidity() { + const tokenConfig = + this.tokenConfig ?? sessionStorage.getItem('tokenConfig'); + const storage = this.keepLogin ? localStorage : sessionStorage; + + const created = +storage.getItem('created'); + const ttl = +storage.getItem('ttl'); + if (this.isCheckingToken || !created) return; + this.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 (this.isCheckingToken = false); + } + + await this.renewToken(); + this.isCheckingToken = false; + }, + + stopRenewer() { + clearInterval(this.intervalId); + }, + + startInterval() { + this.stopRenewer(); + const renewPeriod = +sessionStorage.getItem('renewPeriod'); + if (!renewPeriod) return; + this.intervalId = setInterval( + () => this.checkValidity(), + renewPeriod * 1000 + ); + }, + async fetchUser(userType = 'user') { try { const userData = await jApi.getObject( From 0246e39f0f2dd12b49a23ca9aeed5764f62f94cb Mon Sep 17 00:00:00 2001 From: wbuezas Date: Wed, 2 Oct 2024 16:39:09 -0300 Subject: [PATCH 02/10] Renew token logic --- src/stores/user.js | 63 +++++++++++++++++++++++----------------------- 1 file changed, 32 insertions(+), 31 deletions(-) diff --git a/src/stores/user.js b/src/stores/user.js index 1e998ba2..11ec29c6 100644 --- a/src/stores/user.js +++ b/src/stores/user.js @@ -29,21 +29,28 @@ export const useUserStore = defineStore('user', { }, getters: { - loggedIn: state => state.token != null, + loggedIn: state => !!state.token, userLocaleOption: state => - state.localeOptions?.find(l => l.value === state?.user?.lang) + state.localeOptions?.find(l => l.value === state?.user?.lang), + storage: state => (state.keepLogin ? localStorage : sessionStorage) }, actions: { async init() { this.isGuest = localStorage.getItem('hederaGuest') || false; - const autoLoginStatus = await this.tryAutoLogin(); - if (!autoLoginStatus) { - this.router.push({ name: 'login' }); + await this.getToken(); + if (!this.loggedIn) { + const autoLoginStatus = await this.tryAutoLogin(); + if (!autoLoginStatus) { + this.router.push({ name: 'login' }); + } + return; } + await this.fetchTokenConfig(); await this.fetchUser(); await this.supplantInit(); this.updateSiteLocale(); + this.startInterval(); }, getToken() { @@ -55,8 +62,7 @@ export const useUserStore = defineStore('user', { }, setToken(token) { - const storage = this.keepLogin ? localStorage : sessionStorage; - storage.setItem('vnToken', token); + this.storage.setItem('vnToken', token); this.token = token; }, @@ -79,7 +85,6 @@ export const useUserStore = defineStore('user', { tokenMultimedia: 'Accounts/logout', token: 'VnUsers/logout' }; - const storage = this.keepLogin ? localStorage : sessionStorage; let destroyTokenPromises = []; try { if (destroyTokens) { @@ -88,7 +93,8 @@ export const useUserStore = defineStore('user', { ); if (isValidToken) destroyTokenPromises = Object.entries(tokens).map( - ([key, url]) => this.destroyToken(url, storage, key) + ([key, url]) => + this.destroyToken(url, this.storage, key) ); } } finally { @@ -100,12 +106,15 @@ export const useUserStore = defineStore('user', { } }, + isLoggedIn() { + const token = this.getToken(); + return !!token; + }, + setSession(data) { - const storage = this.keepLogin ? localStorage : sessionStorage; - storage.setItem('vnToken', data.token); - storage.setItem('created', data.created); - storage.setItem('ttl', data.ttl); - sessionStorage.setItem('keepLogin', this.keepLogin); + this.storage.setItem('created', data.created); + this.storage.setItem('ttl', data.ttl); + localStorage.setItem('keepLogin', this.keepLogin); }, async login(user, password, remember) { @@ -135,15 +144,11 @@ export const useUserStore = defineStore('user', { }, async logout() { - if (this.token != null) { - try { - await api.post('Accounts/logout'); - } catch (e) {} - localStorage.removeItem('vnToken'); - sessionStorage.removeItem('vnToken'); - } + try { + await api.post('Accounts/logout'); + } catch (e) {} + this.destroy(); this.$reset(); - localStorage.removeItem('hederaGuest'); useAppStore().onLogout(); }, @@ -154,8 +159,7 @@ export const useUserStore = defineStore('user', { }); if (!data) return; this.tokenConfig = data; - sessionStorage.setItem('tokenConfig', data); - sessionStorage.setItem('renewPeriod', data.renewPeriod); + this.storage.setItem('renewPeriod', data.renewPeriod); return data; } catch (error) { notify('errors.tokenConfig', 'negative'); @@ -173,17 +177,14 @@ export const useUserStore = defineStore('user', { }, async checkValidity() { - const tokenConfig = - this.tokenConfig ?? sessionStorage.getItem('tokenConfig'); - const storage = this.keepLogin ? localStorage : sessionStorage; + const created = +this.storage.getItem('created'); + const ttl = +this.storage.getItem('ttl'); - const created = +storage.getItem('created'); - const ttl = +storage.getItem('ttl'); if (this.isCheckingToken || !created) return; this.isCheckingToken = true; const renewPeriodInSeconds = - Math.min(ttl, tokenConfig.value.renewPeriod) * 1000; + Math.min(ttl, this.tokenConfig?.renewPeriod) * 1000; const maxDate = created + renewPeriodInSeconds; const now = new Date().getTime(); @@ -201,7 +202,7 @@ export const useUserStore = defineStore('user', { startInterval() { this.stopRenewer(); - const renewPeriod = +sessionStorage.getItem('renewPeriod'); + const renewPeriod = +this.storage.getItem('renewPeriod'); if (!renewPeriod) return; this.intervalId = setInterval( () => this.checkValidity(), From a97219ad59225bc18f11e1fc441d0c6bebf5f168 Mon Sep 17 00:00:00 2001 From: wbuezas Date: Wed, 2 Oct 2024 18:40:06 -0300 Subject: [PATCH 03/10] Fix supplant users --- src/App.vue | 5 +- src/layouts/MainLayout.vue | 4 +- src/stores/app.js | 5 +- src/stores/user.js | 547 ++++++++++++++++++++----------------- 4 files changed, 311 insertions(+), 250 deletions(-) diff --git a/src/App.vue b/src/App.vue index 1e865e99..2771cb35 100644 --- a/src/App.vue +++ b/src/App.vue @@ -2,11 +2,14 @@ import { useAppStore } from 'stores/app'; import { useUserStore } from 'stores/user'; import { onBeforeMount } from 'vue'; +import { useRouter } from 'vue-router'; + const appStore = useAppStore(); const userStore = useUserStore(); +const router = useRouter(); onBeforeMount(async () => { - await userStore.init(); + await userStore.init(router); await appStore.init(); }); diff --git a/src/layouts/MainLayout.vue b/src/layouts/MainLayout.vue index 94daa6f8..228a342c 100644 --- a/src/layouts/MainLayout.vue +++ b/src/layouts/MainLayout.vue @@ -13,7 +13,7 @@ const appStore = useAppStore(); const hiddenMenuItems = new Set(['Reports']); const refreshContentKey = ref(0); -const { user, supplantedUser } = storeToRefs(userStore); +const { mainUser, supplantedUser } = storeToRefs(userStore); const { menuEssentialLinks, title, subtitle, useRightDrawer, rightDrawerOpen } = storeToRefs(appStore); const actions = ref(null); @@ -82,7 +82,7 @@ const logoutSupplantedUser = async () => {