0
0
Fork 0

Login fixed

This commit is contained in:
Joan Sanchez 2022-03-11 12:59:10 +01:00
parent 517c6c2d5f
commit 659a396fbb
14 changed files with 311 additions and 44 deletions

View File

@ -80,6 +80,12 @@ module.exports = {
// TypeScript
quotes: ['warn', 'single', { avoidEscape: true }],
'@typescript-eslint/explicit-function-return-type': 'off',
"@typescript-eslint/unbound-method": "off",
'@typescript-eslint/no-unsafe-assignment': 'off',
'@typescript-eslint/no-unsafe-member-access': 'off',
'@typescript-eslint/no-floating-promises': 'off',
// allow debugger during development only
'no-debugger': process.env.NODE_ENV === 'production' ? 'error' : 'off',

View File

@ -58,6 +58,7 @@ module.exports = {
'^src/(.*)$': '<rootDir>/src/$1',
'^app/(.*)$': '<rootDir>/$1',
'^components/(.*)$': '<rootDir>/src/components/$1',
'^composables/(.*)$': '<rootDir>/src/composables/$1',
'^layouts/(.*)$': '<rootDir>/src/layouts/$1',
'^pages/(.*)$': '<rootDir>/src/pages/$1',
'^assets/(.*)$': '<rootDir>/src/assets/$1',

View File

@ -107,7 +107,7 @@ module.exports = configure(function (ctx) {
// directives: [],
// Quasar plugins
plugins: [],
plugins: ['Notify'],
},
// animations: 'all', // --- includes all animations

View File

@ -1,6 +1,10 @@
import { boot } from 'quasar/wrappers';
import axios, { AxiosInstance } from 'axios';
import { useSession } from 'src/composables/useSession';
const { getToken } = useSession();
declare module '@vue/runtime-core' {
interface ComponentCustomProperties {
$axios: AxiosInstance;
@ -15,6 +19,22 @@ declare module '@vue/runtime-core' {
// for each client)
const api = axios.create({ baseURL: 'https://api.example.com' });
axios.interceptors.request.use(
function (context) {
const token = getToken();
if (token.length && context.headers) {
context.headers.Authorization = token;
}
return context;
},
function (error) {
return Promise.reject(error);
}
);
export default boot(({ app }) => {
// for use inside Vue files (Options API) through this.$axios and this.$api

54
src/components/Topbar.vue Normal file
View File

@ -0,0 +1,54 @@
<template>
<q-header class="bg-dark" color="white" elevated bordered>
<q-toolbar class="q-py-sm q-px-md">
<q-btn flat @click="$emit('on-toggle-drawer')" round dense icon="menu" />
<router-link to="/">
<q-btn flat round class="q-ml-xs" v-if="$q.screen.gt.xs">
<q-avatar square size="md">
<q-img src="~/assets/logo_icon.svg" alt="Logo" />
</q-avatar>
</q-btn>
</router-link>
<q-toolbar-title shrink class="text-weight-bold">Salix</q-toolbar-title>
<q-space></q-space>
<div class="q-pl-sm q-gutter-sm row items-center no-wrap">
<q-btn v-if="$q.screen.gt.xs" dense flat size="md" icon="add">
<q-icon name="arrow_drop_down" size="s" />
<q-menu>
<q-list style="min-width: 150px">
<q-item clickable>
<q-item-section>New customer</q-item-section>
</q-item>
<q-item clickable>
<q-item-section>New ticket</q-item-section>
</q-item>
</q-list>
</q-menu>
</q-btn>
<q-btn v-if="$q.screen.gt.xs" dense flat round size="md" icon="notifications" />
<q-btn dense flat no-wrap>
<q-avatar size="lg">
<q-img
:src="`/api/Images/user/160x160/${user.id}/download?access_token=${token}`"
spinner-color="white"
/>
</q-avatar>
<q-tooltip>Account</q-tooltip>
<q-icon name="arrow_drop_down" size="s" />
<UserPanel />
</q-btn>
</div>
</q-toolbar>
</q-header>
</template>
<script lang="ts" setup>
import { useState } from 'src/composables/useState';
import { useSession } from 'src/composables/useSession';
import UserPanel from 'src/components/UserPanel.vue';
const session = useSession();
const state = useState();
const user = state.getUser();
const token = session.getToken();
</script>

View File

@ -0,0 +1,104 @@
<template>
<q-menu>
<div class="row no-wrap q-pa-md">
<div class="column">
<div class="text-h6 q-mb-md">{{ t('components.userPanel.settings') }}</div>
<q-toggle
:label="t(`globals.lang['${locale}']`)"
icon="public"
color="orange"
false-value="es"
true-value="en"
v-model="locale"
/>
<q-toggle
v-model="darkMode"
checked-icon="dark_mode"
color="orange"
unchecked-icon="light_mode"
/>
<q-btn color="orange" outline size="sm" label="Settings" icon="settings" />
</div>
<q-separator vertical inset class="q-mx-lg" />
<div class="column items-center" style="min-width: 150px">
<q-avatar size="80px">
<q-img
:src="`/api/Images/user/160x160/${user.id}/download?access_token=${token}`"
spinner-color="white"
/>
</q-avatar>
<div class="text-subtitle1 q-mt-md">
<strong>{{ user.nickname }}</strong>
</div>
<div class="text-subtitle3 text-grey-7 q-mb-xs">@{{ user.username }}</div>
<q-btn
color="orange"
flat
label="Log Out"
size="sm"
icon="logout"
@click="logout()"
v-close-popup
/>
</div>
</div>
</q-menu>
</template>
<script lang="ts" setup>
import { onMounted, computed } from 'vue';
import { Dark, useQuasar } from 'quasar';
import { useI18n } from 'vue-i18n';
import { useRouter } from 'vue-router';
import axios from 'axios';
import { useState } from 'src/composables/useState';
import { useSession } from 'src/composables/useSession';
const quasar = useQuasar();
const state = useState();
const session = useSession();
const router = useRouter();
const { t, locale } = useI18n();
const darkMode = computed({
get(): boolean {
return Dark.isActive;
},
set(value: boolean): void {
Dark.set(value);
},
});
const user = state.getUser();
const token = session.getToken();
onMounted(() => {
axios
.get('/api/accounts/acl')
.then(({ data }) => {
state.setUser({
id: data.user.id,
username: data.user.name,
nickname: data.user.nickname,
});
})
.catch(() => {
quasar.notify({
message: t('errors.statusUnauthorized'),
type: 'negative',
});
logout();
});
});
function logout(): void {
session.destroy();
router.push('/login');
}
</script>

View File

@ -1,4 +1,4 @@
import store from '@/store';
/* import store from '@/store';
export function useRole() {
function hasAny(roles: string[]): boolean {
@ -15,3 +15,4 @@ export function useRole() {
hasAny,
};
}
*/

View File

@ -2,6 +2,31 @@
// so you can safely delete all default props below
export default {
failed: 'Action failed',
success: 'Action was successful',
'globals': {
'lang': {
'es': 'Spanish',
'en': 'English'
}
},
'errors': {
'statusUnauthorized': 'Access denied',
'statusInternalServerError': 'An internal server error has ocurred'
},
'login': {
'title': 'Login',
'username': 'Username',
'password': 'Password',
'submit': 'Log in',
'keepLogin': 'Keep me logged in',
'loginSuccess': 'You have successfully logged in',
'loginError': 'Invalid username or password'
},
'customer': {},
'components': {
'topbar': {},
'userPanel': {
'settings': 'Settings',
'logOut': 'Log Out'
}
}
};

29
src/i18n/es/index.ts Normal file
View File

@ -0,0 +1,29 @@
export default {
'globals': {
'lang': {
'es': 'Español',
'en': 'Inglés'
}
},
'errors': {
'statusUnauthorized': 'Acceso denegado',
'statusInternalServerError': 'Ha ocurrido un error interno del servidor'
},
'login': {
'title': 'Iniciar sesión',
'username': 'Nombre de usuario',
'password': 'Contraseña',
'submit': 'Iniciar sesión',
'keepLogin': 'Mantener sesión iniciada',
'loginSuccess': 'Inicio de sesión correcto',
'loginError': 'Nombre de usuario o contraseña incorrectos'
},
'customer': {},
'components': {
'topbar': {},
'userPanel': {
'settings': 'Configuración',
'logOut': 'Cerrar sesión'
}
}
}

View File

@ -1,5 +1,7 @@
import en from './en';
import es from './es';
export default {
'en': en,
'es': es,
};

View File

@ -13,30 +13,40 @@
>
<q-scroll-area class="fit text-grey-8">
<q-list padding>
<q-item clickable v-ripple :to="{ path: '/dashboard' }" active-class="text-orange">
<q-item
clickable
v-ripple
:to="{ path: '/dashboard' }"
active-class="text-orange"
>
<q-item-section avatar>
<q-icon name="dashboard" />
</q-item-section>
<q-item-section> Dashboard</q-item-section>
<q-item-section>Dashboard</q-item-section>
</q-item>
<q-item clickable v-ripple :to="{ path: '/customer' }" active-class="text-orange">
<q-item
clickable
v-ripple
:to="{ path: '/customer' }"
active-class="text-orange"
>
<q-item-section avatar>
<q-icon name="people" />
</q-item-section>
<q-item-section> Customers </q-item-section>
<q-item-section>Customers</q-item-section>
</q-item>
<q-item clickable v-ripple :to="{ path: '/ticket' }" active-class="text-orange">
<q-item-section avatar>
<q-icon name="vn:ticket" />
</q-item-section>
<q-item-section> Tickets </q-item-section>
<q-item-section>Tickets</q-item-section>
</q-item>
<q-item clickable v-ripple>
<q-item-section avatar>
<q-icon name="receipt" />
</q-item-section>
<q-item-section> Invoice Out </q-item-section>
<q-item-section>Invoice Out</q-item-section>
</q-item>
<q-item clickable v-ripple>
@ -44,7 +54,7 @@
<q-icon name="shopping_cart" />
</q-item-section>
<q-item-section> Catalog </q-item-section>
<q-item-section>Catalog</q-item-section>
</q-item>
<q-separator />
@ -54,7 +64,7 @@
<q-icon name="drafts" />
</q-item-section>
<q-item-section> Drafts </q-item-section>
<q-item-section>Drafts</q-item-section>
</q-item>
</q-list>
</q-scroll-area>
@ -67,7 +77,7 @@
<script lang="ts" setup>
import { ref } from 'vue';
import Topbar from '@/components/Topbar.vue';
import Topbar from 'src/components/Topbar.vue';
const drawer = ref(false);
const miniState = ref(true);
@ -77,7 +87,4 @@ function onToggleDrawer(): void {
</script>
<style lang="scss" scoped>
.bg-darker {
background-color: $darker;
}
</style>

View File

@ -23,7 +23,13 @@
<q-page>
<div id="login">
<q-card class="login q-pa-xl">
<img src="@/assets/logo.svg" alt="Logo" class="logo q-mb-xl" />
<q-img
src="~/assets/logo.svg"
alt="Logo"
fit="contain"
:ratio="16 / 9"
class="q-mb-md"
/>
<q-form @submit="onSubmit" class="q-gutter-md">
<q-input
filled
@ -70,7 +76,7 @@ import { useI18n } from 'vue-i18n';
import { useRouter } from 'vue-router';
import axios from 'axios';
import { useSession } from '/components/Session';
import { useSession } from 'src/composables/useSession';
const quasar = useQuasar();
const session = useSession();
@ -90,38 +96,38 @@ const darkMode = computed({
},
});
function onSubmit(): void {
axios
.post('/api/accounts/login', {
async function onSubmit(): Promise<void> {
try {
const { data } = await axios.post('/api/accounts/login', {
user: username.value,
password: password.value,
})
.then((response) => {
session.setToken({
token: response.data.token,
keepLogin: keepLogin.value,
});
});
session.setToken({
token: data.token,
keepLogin: keepLogin.value,
});
quasar.notify({
message: t('login.loginSuccess'),
type: 'positive',
});
router.push('/dashboard');
})
.catch((error) => {
const errorCode = error.response.status;
quasar.notify({
message: t('login.loginSuccess'),
type: 'positive',
});
await router.push({ path: '/dashboard' });
} catch (error) {
if (axios.isAxiosError(error)) {
const errorCode = error.response && error.response.status;
if (errorCode === 401) {
quasar.notify({
message: t('login.loginFailed'),
type: 'negative',
});
} else {
quasar.notify({
message: t('errors.statusInternalServerError'),
type: 'negative',
});
}
});
} else {
quasar.notify({
message: t('errors.statusInternalServerError'),
type: 'negative',
});
}
}
}
</script>

View File

@ -6,7 +6,8 @@ import {
createWebHistory,
} from 'vue-router';
import routes from './routes';
import { useSession } from 'src/composables/useSession';
const session = useSession();
/*
* If not building with SSR mode, you can
* directly export the Router instantiation;
@ -35,5 +36,16 @@ export default route(function (/* { store, ssrContext } */) {
),
});
Router.beforeEach((to, from, next) => {
const { isLoggedIn } = session;
if (!isLoggedIn && to.name !== 'Login') {
next({ path: '/login', query: { redirect: to.fullPath } });
} else {
next();
}
});
return Router;
});

View File

@ -9,7 +9,7 @@ const routes: RouteRecordRaw[] = [
{
path: '/',
name: 'Main',
component: () => import('../Layout/Main.vue'),
component: () => import('../layouts/Main.vue'),
redirect: { name: 'Dashboard' },
children: [
{
@ -38,7 +38,7 @@ const routes: RouteRecordRaw[] = [
{
path: '/:pathMatch(.*)*',
name: 'NotFound',
component: () => import('../pages/Layout/NotFound.vue'),
component: () => import('../pages/NotFound.vue'),
},
],
},