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)