DEV Community

Tobias Mesquita for Quasar Framework Brasil

Posted on

QPANC - Parte 15 - Quasar - Login

QPANC são as iniciais de Quasar PostgreSQL ASP NET Core.

31 - Componente de Login - Part 2

Agora que terminamos de configurar os boots, podemos retornar ao componente de Login, e fazer a integração dele com a API.

O primeiro passo, é adicionar a escuta ao evento unprocessable, que é emitido pelo axios sempre que ocorre um erro 422.

QPANC.App/src/pages/login/index.js

export default factory.page({ name: 'LoginPage', created () { if (process.env.CLIENT) { this.$root.$on('unprocessable', this.unprocessable) } }, destroy () { if (process.env.CLIENT) { this.$root.$off('unprocessable', this.unprocessable) } }, methods: { unprocessable (errors) { console.log(errors) } } }) 

O segundo passo, é adicionar a regra server aos campos que sofrem validação.:

QPANC.App/src/pages/login/index.js

export default factory.page({ name: 'LoginPage', data () { const self = this const validation = validations(self, { userName: ['required', 'email', 'server'], password: ['required', 'server'] }) return { validation, validationArgs: { userName: { server: true }, password: { server: true } } } }, }) 

Como as validações feitas sobre o userName no lado do servidor também são realizadas pelo front e de se esperar que a API nunca retorne um erro para este campo. Então, você pode remover esta validação ou manter.

Agora, adicione o evento @blur=validationArgs.${name}.server = true aos componentes que são validados no servidor.:

QPANC.App/src/pages/login/index.vue

 <q-form ref="form" class="row q-col-gutter-sm"> <div class="col col-12"> <q-input v-model="userName" :label="$t('fields.userName')" :rules="validation.userName" @blur="validationArgs.userName.server = true"></q-input> </div> <div class="col col-12"> <q-input type="password" v-model="password" :label="$t('fields.password')" :rules="validation.password" @blur="validationArgs.password.server = true"></q-input> </div> ... </q-form> 

Como as validações feitas no servidor são exibidas de forma estática, precisamos limpar elas manualmente, por isto a necessidade do evento @blur, assim como do this.validation.resetServer() antes de validar novamente.

QPANC.App/src/pages/login/index.js

export default factory.page({ name: 'LoginPage', methods: { async login () { this.validation.resetServer() const isValid = await this.$refs.form.validate() if (isValid) { await this.$store.dispatch(`${moduleName}/login`) } } } }) 

O ultimo passo, e atualizar o argumento server, sempre que o evento unprocessable ocorrer:

QPANC.App/src/pages/login/index.js

export default factory.page({ name: 'LoginPage', methods: { unprocessable (errors) { switch (true) { case !!errors.UserName: this.validationArgs.userName.server = errors.UserName[0]; break case !!errors.Password: this.validationArgs.password.server = errors.Password[0]; break } this.$refs.form.validate() } } }) 

Note que estamos chamando o this.$refs.form.validate(), para forçar que os campos sejam revalidados, assim exibindo a mensagem de erro.

Agora que o componente está aplicando as validações, tanto as feitas no lado do cliente, quanto as remotas, podemos fazer as alterações necessárias na store.

QPANC.App/src/pages/login/index.js

export default factory.store({ actions: { async login ({ state, commit }) { const { data: token } = await this.$axios.post('/Auth/Login', state) commit('app/token', token, { root: true }) this.$router.push('/home') } } }) 

Caso ocorra algum erro na API, não precisa se preocupar, pois o interceptor do axios irá lidar com ele.

Alt Text

32 Lendo o Token JWT

Agora que já temos a nossa tela de Login, a já persistimos o token, precisamos adicionar uma forma de ler ele. para isto, iremos adicionar o pacote jwt-decode.

yarn add jwt-decode 

O próximo passo é incrementar o nosso modulo app com alguns getters:

import { factory } from '@toby.mosque/utils' import jwtDecode from 'jwt-decode' class AppStoreModel { constructor ({ token = '', localeOs = '', localeUser = '' } = {}) { this.token = token this.localeOs = localeOs this.localeUser = localeUser } } const options = { model: AppStoreModel } export default factory.store({ options, getters: { decoded (state) { if (!state.token) { return undefined } return jwtDecode(state.token) }, expireAt (state, getters) { if (!getters.decoded || !getters.decoded.exp) { return undefined } const expiration = getters.decoded.exp * 1000 return new Date(expiration) }, isLogged (state, getters) { return function () { const now = new Date() return getters.expireAt && getters.expireAt > now } }, locale (state) { return state.localeUser || state.localeOs } } }) export { options, AppStoreModel } 

O getter decoded vai retornar o token decodificado, o getter expireAt vai retornar quando o token expira, e por fim o isLogged testa se a data de validade do token é maior do que agora.

Para o isLogged usamos um getter que retorna uma function ao invés de uma action, pois precisamos que este método seja chamado de forma síncrona. e actions são chamadas de forma assíncrona.

O isLogged retorna uma função ao invés de realizar o teste de forma direta, isto é necessário, por que o new Date não é reativo, desta forma ele seria executado apenas uma vez para cada token, fazendo que o token fosse sempre valido, mesmo expirado.

Uma forma de contornar isto, seria adicionar a hora atual ao state de outro modulo (não deve ser feito no app, para que esta data não seja persistida em um Cookie), então atualizar ele a cada segundo, porém isto adicionaria um custo extra, que seria justificável, caso precise exibir à hora atual na aplicação, ou tenhas mais regras que dependam a hora atual.

store/clock.js

import { factory } from '@toby.mosque/utils' class CloseStoreModel { constructor ({ now = '', interval = 0 } = {}) { this.now = now || new Date().toISOString() this.interval = interval } } const options = { model: CloseStoreModel } export default factory.store({ options, actions: { config ({ commit }) { if (process.env.CLIENT) { const interval = setInterval (() => { commit('now', new Date().toISOString()) }, 1000) commit('interval', interval) }() return jwtDecode(state.token) }, destroy ({ state, commit }) { clearInterval(state.interval) commit('interval', 0) } } }) export { options, CloseStoreModel } 
isLogged (state, getters, rootState) { const now = new Date(rootState.clock.now) return getters.expireAt && getters.expireAt > now } 

Note que no exemplo acima, armazenamos no state uma string no formato ISO, ao invés do objeto date, isto é necessário, pois é provável que o servidor esteja em um horário diferente do usuário, um outro complicador, é a implementação do Date, que é diferente no Browser (cliente) quando comparado ao NodeJS (servidor), desta forma, deve-se SEMPRE armazenar datas como strings nas stores e nunca como objetos.

Caso decida inspecionar os valores retornados por estes getters, pode fazer o seguinte.:

const logged = store.getters['app/isLogged']() console.log({ token: store.state.app.token, decoded: store.getters['app/decoded'], expireAt: store.getters['app/expireAt'], isLogged: store.getters['app/isLogged'], logged: logged }) 

O resultado deverá ser algo semelhante ao exibido na imagem abaixo.:

Alt Text

E por fim, um exemplo de uso, vai condicionar o redirecionamento da rota '/' ao fato do usuário está logado ou não, para tal, vamos modificar o arquivo src/router/routes.js.

import clean from './areas/clean' export default function (context) { const routes = [{ path: '/', component: { /* ... */ }, children: [ { path: '', beforeEnter (to, from, next) { const { store } = context const logged = store.getters['app/isLogged']() if (logged) { next('/home') } else { next('/login') } } }, clean(context) ] }] // Always leave this as last one if (process.env.MODE !== 'ssr') { /* ... */ } return routes } 

O nosso único impeditivo agora, é o fato que não temos uma rota /home.

Top comments (0)