Merge branch 'dev' of https://gitea.verdnatura.es/verdnatura/salix-front into 4075-ticket_boxing

This commit is contained in:
Alex Moreno 2022-09-05 07:55:19 +02:00
commit 22b5d06fca
16 changed files with 450 additions and 21457 deletions

21627
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -21,7 +21,7 @@
"@quasar/extras": "^1.14.0",
"axios": "^0.21.1",
"core-js": "^3.6.5",
"quasar": "^2.7.1",
"quasar": "^2.7.3",
"vue": "^3.0.0",
"vue-i18n": "^9.0.0",
"vue-router": "^4.0.0"

View File

@ -1,6 +1,15 @@
<script setup>
import axios from 'axios';
import { useQuasar } from 'quasar';
import { useI18n } from 'vue-i18n';
import { useRouter } from 'vue-router';
import { useSession } from 'src/composables/useSession';
const quasar = useQuasar();
const router = useRouter();
const session = useSession();
const { t } = useI18n();
const { isLoggedIn } = session;
quasar.iconMapFn = (iconName) => {
if (iconName.startsWith('vn:')) {
@ -11,6 +20,51 @@ quasar.iconMapFn = (iconName) => {
};
}
};
function responseError(error) {
let message = error.message;
let logOut = false;
switch (error.response?.status) {
case 401:
message = 'login.loginError';
if (isLoggedIn()) {
message = 'errors.statusUnauthorized';
logOut = true;
}
break;
case 403:
message = 'errors.statusUnauthorized';
break;
case 500:
message = 'errors.statusInternalServerError';
break;
case 502:
message = 'errors.statusBadGateway';
break;
case 504:
message = 'errors.statusGatewayTimeout';
break;
}
let translatedMessage = t(message);
if (!translatedMessage) translatedMessage = message;
quasar.notify({
message: translatedMessage,
type: 'negative',
});
if (logOut) {
session.destroy();
router.push({ path: '/login' });
}
return Promise.resolve(error);
}
axios.interceptors.response.use((response) => response, responseError);
</script>
<template>

87
src/__tests__/App.spec.js Normal file
View File

@ -0,0 +1,87 @@
import { jest, describe, expect, it, beforeAll } from '@jest/globals';
import { createWrapper } from 'app/tests/jest/jestHelpers';
import App from '../App.vue';
import { useSession } from 'src/composables/useSession';
const mockPush = jest.fn();
const mockLoggedIn = jest.fn();
const mockDestroy = jest.fn();
const session = useSession();
jest.mock('vue-router', () => ({
useRouter: () => ({
push: mockPush,
currentRoute: { value: 'myCurrentRoute' }
}),
}));
jest.mock('src/composables/useSession', () => ({
useSession: () => ({
isLoggedIn: mockLoggedIn,
destroy: mockDestroy
}),
}));
jest.mock('vue-i18n', () => ({
createI18n: () => { },
useI18n: () => ({
t: () => { }
}),
}));
describe('App', () => {
let vm;
beforeAll(() => {
const options = {
global: {
stubs: ['router-view']
}
};
vm = createWrapper(App, options).vm;
});
it('should return a login error message', async () => {
jest.spyOn(vm.quasar, 'notify');
session.isLoggedIn.mockReturnValue(false);
const response = {
response: {
status: 401
}
};
await vm.responseError(response);
expect(vm.quasar.notify).toHaveBeenCalledWith(expect.objectContaining(
{
type: 'negative',
message: 'login.loginError'
}
));
});
it('should return an unauthorized error message', async () => {
jest.spyOn(vm.quasar, 'notify');
session.isLoggedIn.mockReturnValue(true);
const response = {
response: {
status: 401
}
};
await vm.responseError(response);
expect(vm.quasar.notify).toHaveBeenCalledWith(expect.objectContaining(
{
type: 'negative',
message: 'errors.statusUnauthorized'
}
));
expect(session.destroy).toHaveBeenCalled();
});
});

View File

@ -1,17 +1,10 @@
import { boot } from 'quasar/wrappers';
import axios from 'axios';
import { useSession } from 'src/composables/useSession';
// Be careful when using SSR for cross-request state pollution
// due to creating a Singleton instance here;
// If any client changes this (global) instance, it might be a
// good idea to move this instance creation inside of the
// "export default () => {}" function below (which runs individually
// for each client)
const api = axios.create({ baseURL: 'https://api.example.com' });
const { getToken } = useSession();
axios.defaults.baseURL = '/api/';
axios.interceptors.request.use(
function (context) {
const token = getToken();
@ -26,17 +19,3 @@ axios.interceptors.request.use(
return Promise.reject(error);
}
);
export default boot(({ app }) => {
// for use inside Vue files (Options API) through this.$axios and this.$api
app.config.globalProperties.$axios = axios;
// ^ ^ ^ this will allow you to use this.$axios (for Vue Options API form)
// so you won't necessarily have to import axios in each vue file
app.config.globalProperties.$api = api;
// ^ ^ ^ this will allow you to use this.$api (for Vue Options API form)
// so you can easily perform requests against your app's API
});
export { api };

View File

@ -60,6 +60,11 @@ async function fetch() {
params: { filter },
});
if (!data) {
isLoading.value = false;
return;
}
hasMoreData.value = data.length === rowsPerPage;
for (const row of data) rows.value.push(row);

View File

@ -39,7 +39,7 @@ function updatePreferences() {
}
async function saveDarkMode(value) {
const query = `/api/UserConfigs/${user.value.id}`;
const query = `/UserConfigs/${user.value.id}`;
await axios.patch(query, {
darkMode: value,
});
@ -47,7 +47,7 @@ async function saveDarkMode(value) {
}
async function saveLanguage(value) {
const query = `/api/Accounts/${user.value.id}`;
const query = `/Accounts/${user.value.id}`;
await axios.patch(query, {
lang: value,
});

View File

@ -57,7 +57,7 @@ export function useNavigation() {
};
async function fetchFavorites() {
const response = await axios.get('api/starredModules/getStarredModules');
const response = await axios.get('StarredModules/getStarredModules');
const filteredModules = modules.value.filter((module) => {
return response.data.find((element) => element.moduleFk == salixModules[module.name]);
@ -72,7 +72,7 @@ export function useNavigation() {
event.stopPropagation();
const params = { moduleName: salixModules[moduleName] };
const query = 'api/starredModules/toggleStarredModule';
const query = 'StarredModules/toggleStarredModule';
await axios.post(query, params);
updateFavorites(moduleName);

View File

@ -5,7 +5,7 @@ export function useRole() {
const state = useState();
async function fetch() {
const { data } = await axios.get('/api/accounts/acl');
const { data } = await axios.get('Accounts/acl');
const roles = data.roles.map(userRoles => userRoles.role.name);
const userData = {

View File

@ -19,6 +19,8 @@ export default {
errors: {
statusUnauthorized: 'Access denied',
statusInternalServerError: 'An internal server error has ocurred',
statusBadGateway: 'It seems that the server has fall down',
statusGatewayTimeout: 'Could not contact the server',
},
login: {
title: 'Login',

View File

@ -19,6 +19,8 @@ export default {
errors: {
statusUnauthorized: 'Acceso denegado',
statusInternalServerError: 'Ha ocurrido un error interno del servidor',
statusBadGateway: 'Parece ser que el servidor ha caído',
statusGatewayTimeout: 'No se ha podido contactar con el servidor',
},
login: {
title: 'Inicio de sesión',

View File

@ -17,7 +17,7 @@ const entityId = computed(function () {
const customer = ref({});
async function fetch() {
const { data } = await axios.get(`/api/Clients/${entityId.value}`);
const { data } = await axios.get(`Clients/${entityId.value}`);
if (data) customer.value = data;
}

View File

@ -13,7 +13,7 @@ function navigate(id) {
<template>
<q-page class="q-pa-md">
<smart-card url="/api/Clients" sort-by="id DESC" @on-navigate="navigate" auto-load>
<smart-card url="/Clients" sort-by="id DESC" @on-navigate="navigate" auto-load>
<template #labels="{ row }">
<q-list>
<q-item class="q-pa-none">

View File

@ -17,12 +17,13 @@ const password = ref('');
const keepLogin = ref(true);
async function onSubmit() {
try {
const { data } = await axios.post('/api/accounts/login', {
const { data } = await axios.post('Accounts/login', {
user: username.value,
password: password.value,
});
if (!data) return;
await session.login(data.token, keepLogin.value);
quasar.notify({
@ -36,22 +37,6 @@ async function onSubmit() {
} else {
router.push({ name: 'Dashboard' });
}
} catch (error) {
if (axios.isAxiosError(error)) {
const errorCode = error.response && error.response.status;
if (errorCode === 401) {
quasar.notify({
message: t('login.loginError'),
type: 'negative',
});
}
} else {
quasar.notify({
message: t('errors.statusInternalServerError'),
type: 'negative',
});
}
}
}
</script>

View File

@ -17,6 +17,10 @@ describe('Login', () => {
vm = createWrapper(Login).vm;
});
afterEach(() => {
jest.clearAllMocks();
});
it('should successfully set the token into session', async () => {
const expectedUser = {
id: 999,
@ -43,15 +47,11 @@ describe('Login', () => {
});
it('should not set the token into session if any error occurred', async () => {
jest.spyOn(axios, 'post').mockRejectedValue(new Error('error'));
jest.spyOn(vm.quasar, 'notify')
expect(vm.session.getToken()).toEqual('');
jest.spyOn(axios, 'post').mockReturnValue({ data: null });
jest.spyOn(vm.quasar, 'notify');
await vm.onSubmit();
expect(vm.quasar.notify).toHaveBeenCalledWith(expect.objectContaining(
{ 'type': 'negative' }
));
expect(vm.quasar.notify).not.toHaveBeenCalled();
});
});

View File

@ -1,6 +1,6 @@
import { route } from 'quasar/wrappers';
import { createRouter, createMemoryHistory, createWebHistory, createWebHashHistory } from 'vue-router';
import { Notify } from 'quasar';
// import { Notify } from 'quasar';
import routes from './routes';
import { i18n } from 'src/boot/i18n';
import { useState } from 'src/composables/useState';
@ -46,20 +46,20 @@ export default route(function (/* { store, ssrContext } */) {
}
if (isLoggedIn()) {
try {
// try {
const stateRoles = state.getRoles().value;
if (stateRoles.length === 0) {
await role.fetch();
}
} catch (error) {
Notify.create({
message: t('errors.statusUnauthorized'),
type: 'negative',
});
// } catch (error) {
// Notify.create({
// message: t('errors.statusUnauthorized'),
// type: 'negative',
// });
session.destroy();
return next({ path: '/login' });
}
// session.destroy();
// return next({ path: '/login' });
// }
const matches = to.matched;
const hasRequiredRoles = matches.every(route => {