Merge pull request 'renew-token-check' (!98) from wbuezas/hedera-web-mindshore:renew-token-check into beta
gitea/hedera-web/pipeline/head There was a failure building this commit Details

Reviewed-on: #98
Reviewed-by: Javier Segarra <jsegarra@verdnatura.es>
This commit is contained in:
Javier Segarra 2025-01-17 23:13:21 +00:00
commit 30dca813b6
15 changed files with 7543 additions and 5106 deletions

0
hedera-web@24.50.15 Normal file
View File

View File

@ -16,6 +16,7 @@
"@intlify/vue-i18n-loader": "^4.2.0", "@intlify/vue-i18n-loader": "^4.2.0",
"@quasar/app-webpack": "^3.0.0", "@quasar/app-webpack": "^3.0.0",
"@quasar/cli": "^2.4.1", "@quasar/cli": "^2.4.1",
"@quasar/vite-plugin": "^1.8.1",
"babel-loader": "^9.2.1", "babel-loader": "^9.2.1",
"css-loader": "^7.1.2", "css-loader": "^7.1.2",
"cypress": "^13.6.6", "cypress": "^13.6.6",
@ -31,17 +32,22 @@
"eslint-plugin-vue": "^9.27.0", "eslint-plugin-vue": "^9.27.0",
"eslint-webpack-plugin": "^3.1.1", "eslint-webpack-plugin": "^3.1.1",
"file-loader": "^6.2.0", "file-loader": "^6.2.0",
"happy-dom": "^15.11.7",
"json-loader": "^0.5.7", "json-loader": "^0.5.7",
"postcss-loader": "^8.1.1", "postcss-loader": "^8.1.1",
"sass-embedded": "^1.80.2",
"sass-loader": "^16.0.4", "sass-loader": "^16.0.4",
"tinymce": "^6.3.0", "tinymce": "^6.3.0",
"url-loader": "^4.1.1", "url-loader": "^4.1.1",
"vitest": "^2.1.8",
"vue-loader": "^17.4.2", "vue-loader": "^17.4.2",
"vue-style-loader": "^4.1.3", "vue-style-loader": "^4.1.3",
"yaml-loader": "^0.5.0" "yaml-loader": "^0.5.0"
}, },
"dependencies": { "dependencies": {
"@intlify/unplugin-vue-i18n": "^6.0.1",
"@quasar/extras": "^1.16.9", "@quasar/extras": "^1.16.9",
"@vitejs/plugin-vue": "^5.2.1",
"axios": "^0.21.1", "axios": "^0.21.1",
"core-js": "^3.6.5", "core-js": "^3.6.5",
"pinia": "^2.0.11", "pinia": "^2.0.11",
@ -58,6 +64,7 @@
"db": "cd ../salix && gulp docker", "db": "cd ../salix && gulp docker",
"cy:open": "npm run db && cypress open", "cy:open": "npm run db && cypress open",
"test:e2e": "npm run db && cypress run", "test:e2e": "npm run db && cypress run",
"test:unit": "vitest",
"build": "rm -rf dist/ ; quasar build", "build": "rm -rf dist/ ; quasar build",
"clean": "rm -rf dist/", "clean": "rm -rf dist/",
"lint": "eslint --ext .js,.vue ./" "lint": "eslint --ext .js,.vue ./"

File diff suppressed because it is too large Load Diff

View File

@ -71,7 +71,8 @@ module.exports = configure(function (ctx) {
chain chain
.plugin('eslint-webpack-plugin') .plugin('eslint-webpack-plugin')
.use(ESLintPlugin, [{ extensions: ['js', 'vue'] }]); .use(ESLintPlugin, [{ extensions: ['js', 'vue'] }]);
chain.resolve.alias
.set('@', path.resolve(__dirname, 'src'));
chain.module chain.module
.rule('i18n-resource') .rule('i18n-resource')
.test(/\.(json5?|ya?ml)$/) .test(/\.(json5?|ya?ml)$/)

View File

@ -1,10 +1,10 @@
import { boot } from 'quasar/wrappers'; import { boot } from 'quasar/wrappers';
import { Connection } from '../js/db/connection'; import { Connection } from '../js/db/connection';
import { useUserStore } from 'stores/user'; import { useUserStore } from '@/stores/user';
import axios from 'axios'; import axios from 'axios';
import useNotify from 'src/composables/useNotify.js'; import useNotify from '@/composables/useNotify.js';
import { useAppStore } from 'src/stores/app'; import { useAppStore } from '@/stores/app';
import { Router } from 'src/router';
const { notify } = useNotify(); const { notify } = useNotify();
// Be careful when using SSR for cross-request state pollution // Be careful when using SSR for cross-request state pollution
// due to creating a Singleton instance here; // due to creating a Singleton instance here;
@ -22,6 +22,8 @@ const onRequestError = error => {
}; };
const onResponseError = error => { const onResponseError = error => {
const userStore = useUserStore();
let message = error.message; let message = error.message;
const response = error.response; const response = error.response;
@ -33,7 +35,14 @@ const onResponseError = error => {
notify(message, 'negative'); notify(message, 'negative');
return Promise.reject(error); if (userStore.isLoggedIn && response?.status === 401) {
if (!Router) return;
Router.push({ name: 'login' });
userStore.destroy(false);
} else if (!userStore.isLoggedIn) {
return Promise.reject(error);
}
}; };
export default boot(({ app }) => { export default boot(({ app }) => {
@ -41,7 +50,8 @@ export default boot(({ app }) => {
const appStore = useAppStore(); const appStore = useAppStore();
function addToken(config) { function addToken(config) {
if (userStore.token) { if (userStore.token) {
config.headers.Authorization = userStore.token; if (!config.headers.Authorization)
config.headers.Authorization = userStore.token;
config.headers['Accept-Language'] = appStore.siteLang; config.headers['Accept-Language'] = appStore.siteLang;
} }
return config; return config;

View File

@ -1,6 +1,6 @@
import { boot } from 'quasar/wrappers'; import { boot } from 'quasar/wrappers';
import { createI18n } from 'vue-i18n'; import { createI18n } from 'vue-i18n';
import messages from 'src/i18n'; import messages from '@/i18n';
const i18n = createI18n({ const i18n = createI18n({
locale: navigator.language || navigator.userLanguage, locale: navigator.language || navigator.userLanguage,

View File

@ -1,5 +1,5 @@
import { Notify } from 'quasar'; import { Notify } from 'quasar';
import { i18n } from 'src/boot/i18n'; import { i18n } from '@/boot/i18n';
export default function useNotify() { export default function useNotify() {
const notify = (message, type, icon) => { const notify = (message, type, icon) => {

View File

@ -4,9 +4,11 @@
class="fullscreen row justify-center items-center layout-view scroll" class="fullscreen row justify-center items-center layout-view scroll"
> >
<QPageContainer class="column q-pa-md row items-center justify-center"> <QPageContainer class="column q-pa-md row items-center justify-center">
<transition> <router-view v-slot="{ Component }">
<router-view /> <transition>
</transition> <component :is="Component" />
</transition>
</router-view>
</QPageContainer> </QPageContainer>
</QLayout> </QLayout>
</template> </template>

View File

@ -23,7 +23,7 @@ const query = `SELECT i.id, i.longName, i.size, i.category,
ON im.collectionFk = 'catalog' ON im.collectionFk = 'catalog'
AND im.name = i.image AND im.name = i.image
WHERE (i.longName LIKE CONCAT('%', #search, '%') WHERE (i.longName LIKE CONCAT('%', #search, '%')
OR i.id = #search) AND i.isActive = 1 OR i.id = #search) AND i.isActive
ORDER BY i.longName LIMIT 50`; ORDER BY i.longName LIMIT 50`;
const onSearch = data => (items.value = data || []); const onSearch = data => (items.value = data || []);

View File

@ -4,7 +4,7 @@ import { useI18n } from 'vue-i18n';
import { useUserStore } from 'stores/user'; import { useUserStore } from 'stores/user';
import useNotify from 'src/composables/useNotify.js'; import useNotify from 'src/composables/useNotify.js';
import { useRouter, useRoute } from 'vue-router'; import { useRoute } from 'vue-router';
import { storeToRefs } from 'pinia'; import { storeToRefs } from 'pinia';
import { useAppStore } from 'src/stores/app'; import { useAppStore } from 'src/stores/app';
@ -14,7 +14,6 @@ const { locale } = useI18n({ useScope: 'global' });
const userStore = useUserStore(); const userStore = useUserStore();
const appStore = useAppStore(); const appStore = useAppStore();
const route = useRoute(); const route = useRoute();
const router = useRouter();
const { siteLang, localeOptions } = storeToRefs(appStore); const { siteLang, localeOptions } = storeToRefs(appStore);
const email = ref(null); const email = ref(null);
@ -45,20 +44,14 @@ onMounted(() => {
} }
}); });
const onLogin = async () => {
await userStore.fetchUser();
await router.push({ name: 'home' });
};
const login = async () => { const login = async () => {
await userStore.login(email.value, password.value, remember.value); await userStore.login(email.value, password.value, remember.value);
await onLogin();
}; };
const loginAsGuest = async () => { const loginAsGuest = async () => {
userStore.isGuest = true; userStore.isGuest = true;
localStorage.setItem('hederaGuest', true); localStorage.setItem('hederaGuest', true);
await onLogin(); await userStore.onLogin();
}; };
</script> </script>

View File

@ -18,25 +18,25 @@ import routes from './routes';
* with the Router instance. * with the Router instance.
*/ */
const createHistory = process.env.SERVER
? createMemoryHistory
: process.env.VUE_ROUTER_MODE === 'history'
? createWebHistory
: createWebHashHistory;
const Router = createRouter({
scrollBehavior: () => ({ left: 0, top: 0 }),
routes,
// Leave this as is and make changes in quasar.conf.js instead!
// quasar.conf.js -> build -> vueRouterMode
// quasar.conf.js -> build -> publicPath
history: createHistory(process.env.VUE_ROUTER_BASE)
});
export { Router };
export default route(function (/* { store, ssrContext } */) { export default route(function (/* { store, ssrContext } */) {
const createHistory = process.env.SERVER
? createMemoryHistory
: process.env.VUE_ROUTER_MODE === 'history'
? createWebHistory
: createWebHashHistory;
const Router = createRouter({
scrollBehavior: () => ({ left: 0, top: 0 }),
routes,
// Leave this as is and make changes in quasar.conf.js instead!
// quasar.conf.js -> build -> vueRouterMode
// quasar.conf.js -> build -> publicPath
history: createHistory(
process.env.MODE === 'ssr' ? void 0 : process.env.VUE_ROUTER_BASE
)
});
Router.beforeEach((to, from, next) => { Router.beforeEach((to, from, next) => {
const userStore = useUserStore(); const userStore = useUserStore();
const allowedRoutes = ['login', 'recoverPassword']; const allowedRoutes = ['login', 'recoverPassword'];

View File

@ -1,7 +1,7 @@
import { defineStore } from 'pinia'; import { defineStore } from 'pinia';
import { jApi } from 'boot/axios'; import { jApi } from '@/boot/axios';
import useNotify from 'src/composables/useNotify.js'; import useNotify from '@/composables/useNotify.js';
import { i18n } from 'src/boot/i18n'; import { i18n } from '@/boot/i18n';
import { useQuasar } from 'quasar'; import { useQuasar } from 'quasar';
const { notify } = useNotify(); const { notify } = useNotify();

View File

@ -1,8 +1,8 @@
import { defineStore } from 'pinia'; import { defineStore } from 'pinia';
import { ref, computed, watch } from 'vue'; import { ref, computed, watch } from 'vue';
import { api, jApi } from 'boot/axios'; import { api, jApi } from '@/boot/axios';
import useNotify from 'src/composables/useNotify.js'; import useNotify from '@/composables/useNotify.js';
import { useAppStore } from 'src/stores/app.js'; import { useAppStore } from '@/stores/app.js';
const { notify } = useNotify(); const { notify } = useNotify();
const TOKEN_MULTIMEDIA = 'tokenMultimedia'; const TOKEN_MULTIMEDIA = 'tokenMultimedia';
@ -25,7 +25,8 @@ export const useUserStore = defineStore('user', () => {
const storage = computed(() => const storage = computed(() =>
keepLogin.value ? localStorage : sessionStorage keepLogin.value ? localStorage : sessionStorage
); );
const isLoggedIn = computed(() => !!storage.value.getItem(TOKEN));
const isLoggedIn = computed(() => !!token.value);
const init = async _router => { const init = async _router => {
router = _router; router = _router;
@ -36,11 +37,12 @@ export const useUserStore = defineStore('user', () => {
if (!autoLoginStatus) { if (!autoLoginStatus) {
router.push({ name: 'login' }); router.push({ name: 'login' });
} }
} else {
await fetchTokenConfig();
await fetchUser();
await supplantInit();
startInterval();
} }
await fetchTokenConfig();
await fetchUser();
await supplantInit();
startInterval();
}; };
const getToken = () => { const getToken = () => {
@ -87,9 +89,9 @@ export const useUserStore = defineStore('user', () => {
let destroyTokenPromises = []; let destroyTokenPromises = [];
try { try {
if (destroyTokens) { if (destroyTokens) {
const { data: isValidToken } = await api.get( const response = await api.get('VnUsers/validateToken');
'VnUsers/validateToken' const isValidToken = response?.data;
);
if (isValidToken) { if (isValidToken) {
destroyTokenPromises = Object.entries(tokens).map( destroyTokenPromises = Object.entries(tokens).map(
([key, url]) => destroyToken(url, storage.value, key) ([key, url]) => destroyToken(url, storage.value, key)
@ -101,6 +103,7 @@ export const useUserStore = defineStore('user', () => {
sessionStorage.clear(); sessionStorage.clear();
await Promise.allSettled(destroyTokenPromises); await Promise.allSettled(destroyTokenPromises);
user.value = null; user.value = null;
$reset();
stopRenewer(); stopRenewer();
} }
}; };
@ -117,30 +120,43 @@ export const useUserStore = defineStore('user', () => {
}; };
const fetchMultimediaToken = async data => { const fetchMultimediaToken = async data => {
const { try {
data: { multimediaToken } const response = await api.get('VnUsers/ShareToken', {
} = await api.get('VnUsers/ShareToken', { headers: { Authorization: data.token }
headers: { Authorization: data.token } });
}); const multimediaToken = response?.data?.multimediaToken;
return multimediaToken; return multimediaToken;
} catch (error) {
throw new Error('Error fetching multimedia token');
}
};
const onLogin = async () => {
await fetchUser();
router.push({ name: 'home' });
}; };
const login = async (username, password, remember) => { const login = async (username, password, remember) => {
const params = { user: username, password }; try {
const { data } = await api.post('Accounts/login', params); const params = { user: username, password };
const { data } = await api.post('Accounts/login', params);
const multimediaToken = await fetchMultimediaToken(data); const multimediaToken = await fetchMultimediaToken(data);
if (!multimediaToken) return; if (!multimediaToken) return;
keepLogin.value = remember; keepLogin.value = remember;
setSession({ setSession({
created: Date.now(), created: Date.now(),
tokenMultimedia: multimediaToken.id, tokenMultimedia: multimediaToken.id,
username, username,
...data ...data
}); });
await fetchTokenConfig(); await fetchTokenConfig();
startInterval(); startInterval();
await onLogin();
} catch (error) {
throw new Error('Error logging in');
}
}; };
const tryAutoLogin = async () => { const tryAutoLogin = async () => {
@ -158,10 +174,8 @@ export const useUserStore = defineStore('user', () => {
const logout = async () => { const logout = async () => {
try { try {
await api.post('Accounts/logout'); await destroy();
} catch (e) {} } catch (e) {}
destroy();
$reset();
useAppStore().onLogout(); useAppStore().onLogout();
}; };
@ -192,8 +206,8 @@ export const useUserStore = defineStore('user', () => {
}); });
setToken({ setToken({
_token: tokenData.data.id, _token: tokenData?.data?.id || '',
_tokenMultimedia: tokenMultimedia.data.id _tokenMultimedia: tokenMultimedia?.data?.id || ''
}); });
}; };
@ -272,6 +286,7 @@ export const useUserStore = defineStore('user', () => {
const $reset = () => { const $reset = () => {
token.value = ''; token.value = '';
tokenMultimedia.value = '';
isGuest.value = false; isGuest.value = false;
user.value = null; user.value = null;
supplantedUser.value = null; supplantedUser.value = null;

View File

@ -0,0 +1,91 @@
import { vi, describe, expect, it, beforeAll } from 'vitest';
import { api } from '@/boot/axios';
import { useUserStore } from '@/stores/user';
import { createPinia, setActivePinia } from 'pinia';
describe('session', () => {
let userStore;
beforeAll(() => {
// Configura Pinia y el store
setActivePinia(createPinia());
userStore = useUserStore();
vi.mock('@/boot/axios', () => ({
api: {
post: vi.fn()
}
}));
});
describe('RenewToken', () => {
const expectedToken = 'myToken';
const expectedTokenMultimedia = 'myTokenMultimedia';
beforeAll(() => {
const tokenConfig = {
id: 1,
renewPeriod: 21600,
courtesyTime: 60,
renewInterval: 300
};
userStore.tokenConfig = tokenConfig;
sessionStorage.setItem('renewPeriod', 21600);
});
it('NOT Should renewToken', async () => {
const data = {
username: 'myUser',
created: Date.now(),
ttl: 1,
keepLogin: false,
token: expectedToken,
tokenMultimedia: expectedTokenMultimedia
};
userStore.setSession(data);
expect(sessionStorage.getItem('created')).toBeDefined();
expect(sessionStorage.getItem('ttl')).toEqual('1');
await userStore.checkValidity();
expect(sessionStorage.getItem('token')).toEqual(expectedToken);
expect(sessionStorage.getItem('tokenMultimedia')).toEqual(
expectedTokenMultimedia
);
});
it('Should renewToken', async () => {
const data = {
token: expectedToken,
tokenMultimedia: expectedTokenMultimedia,
keepLogin: false,
ttl: 3600, // 1 hora
created: Date.now() - 100000000 // forzamos a que crea que el token se creó hace 100000000 ms
};
userStore.setSession(data);
// Mockea las respuestas de la API
api.post
.mockResolvedValueOnce({
data: { id: 'newToken1' }
})
.mockResolvedValueOnce({
data: { id: 'newToken2' }
});
// Verifica el estado inicial
expect(sessionStorage.getItem('keepLogin')).toBeFalsy();
expect(sessionStorage.getItem('created')).toBeDefined();
expect(sessionStorage.getItem('ttl')).toEqual('3600');
expect(sessionStorage.getItem('token')).toEqual(expectedToken);
// Llama al método que debe validar y renovar el token
await userStore.checkValidity();
// Verifica que los tokens hayan cambiado
expect(sessionStorage.getItem('token')).not.toEqual(expectedToken);
expect(sessionStorage.getItem('tokenMultimedia')).not.toEqual(
expectedTokenMultimedia
);
});
});
});

38
vitest.config.js Normal file
View File

@ -0,0 +1,38 @@
import { defineConfig } from 'vitest/config';
import vue from '@vitejs/plugin-vue';
import { quasar, transformAssetUrls } from '@quasar/vite-plugin';
// import jsconfigPaths from 'vite-jsconfig-paths';
import VueI18nPlugin from '@intlify/unplugin-vue-i18n/vite';
import path from 'path';
// https://vitejs.dev/config/
export default defineConfig({
resolve: {
alias: {
'@': path.resolve(__dirname, 'src'),
},
},
test: {
environment: 'happy-dom',
include: [
// Matches vitest tests in any subfolder of 'src' or into 'test/vitest/__tests__'
// Matches all files with extension 'js', 'jsx', 'ts' and 'tsx'
'src/test/vitest/**/*.{test,spec}.{js,mjs,cjs,ts,mts,cts,jsx,tsx}',
],
},
plugins: [
vue({
template: { transformAssetUrls }
}),
quasar({
sassVariables: 'src/css/quasar-variables.sass'
}),
VueI18nPlugin({
include: [
path.resolve(__dirname, 'src/i18n/**'),
path.resolve(__dirname, 'src/pages/**/locale/**'),
],
}),
// jsconfigPaths(),
],
});