QPANC - Parte 15 - Quasar - Login
Tags: quasar, postgres, aspnetcore, docker
Published: 2020-04-19 23:38:49

> **QPANC** são as iniciais de **Q**uasar **P**ostgreSQL **A**SP **N**ET **C**ore.

  * [Source](https://github.com/TobyMosque/qpanc)
  * [Introdução](https://dev.to/quasar-pt-br/arquiteturando-uma-aplicacao-com-quasar-postgresql-asp-net-core-e-docker-introducao-o7o)
  * [Parte I - ASP.NET - Inicializando os Projetos](https://dev.to/quasar-pt-br/qpanc-parte-1-asp-net-inicializando-os-projetos-lkm)
  * [Parte 2 - PostgreSQL](https://dev.to/quasar-pt-br/qpanc-parte-2-postgresql-51pb)
  * [Parte 3 - ASP.NET - Registrando Serviços e Lendo Variáveis de Ambiente](https://dev.to/quasar-pt-br/qpanc-parte-3-asp-net-registrando-servicos-e-lendo-variaveis-de-ambiente-3do2)
  * [Parte 4 - ASP.NET - Entity Framework e ASP.NET Core Identity](https://dev.to/quasar-pt-br/qpanc-parte-4-asp-net-entity-framework-e-asp-net-core-identity-40f9)
  * [Parte 5 - ASP.NET - Documentação Interativa com Swagger](https://dev.to/quasar-pt-br/qpanc-parte-5-asp-net-documentacao-interativa-com-swagger-4n0o)
  * [Parte 6 - ASP.NET - Regionalização](https://dev.to/quasar-pt-br/qpanc-parte-6-asp-net-regionalizacao-13p5)
  * [Parte 7 - ASP.NET - Autenticação e Autorização](https://dev.to/quasar-pt-br/qpanc-parte-7-asp-net-autenticacao-e-autorizacao-bpd)
  * [Parte 8 - ASP.NET - CORS](https://dev.to/quasar-pt-br/qpanc-parte-8-asp-net-cors-52dn)
  * [Parte 9 - Quasar - Criação e Configuração do Projeto](https://dev.to/quasar-pt-br/qpanc-parte-9-quasar-criacao-e-configuracao-do-projeto-5hbi)
  * [Parte 10 - Quasar - Configurações e Customizações](https://dev.to/quasar-pt-br/qpanc-parte-10-quasar-configuracoes-e-customizacoes-2i86)
  * [Parte 11 - Quasar - Componentes - Diferença entre SPA e SSR](https://dev.to/quasar-pt-br/qpanc-parte-11-quasar-componentes-diferenca-entre-spa-e-ssr-1a3c)
  * [Parte 12 - Quasar - Serviços](https://dev.to/quasar-pt-br/qpanc-parte-12-quasar-servicos-4e61)
  * [Parte 13 - Quasar - Regionalização e Stores](https://dev.to/quasar-pt-br/qpanc-parte-13-quasar-regionalizacao-e-stores-1kep)
  * [Parte 14 - Quasar - Consumindo a API](https://dev.to/quasar-pt-br/qpanc-parte-14-quasar-consumindo-a-api-fn6)
  * [Parte 15 - Quasar - Login](https://dev.to/quasar-pt-br/qpanc-parte-15-quasar-login-jc6)
  * [Parte 16 - Quasar - Áreas Protegidas](https://dev.to/quasar-pt-br/qpanc-parte-16-quasar-areas-protegidas-3jkn)
  * [Parte 17 - Quasar - Registro](https://dev.to/quasar-pt-br/qpanc-parte-17-quasar-registro-3b8c)
  * [Parte 18 - Docker - Maquina Virtual Linux](https://dev.to/quasar-pt-br/qpanc-parte-18-docker-maquina-virtual-linux-g5c)
  * [Parte 19 - Docker - Registro e Build](https://dev.to/quasar-pt-br/qpanc-parte-19-docker-registro-e-build-31i4)
  * [Parte 20 - Docker - Traefik e Publicação](https://dev.to/quasar-pt-br/qpanc-parte-20-docker-traefik-e-publicacao-3g3d)
  * [Demo Online](https://qpanc.tobiasmesquita.dev)

##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**
```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**
```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**
```html
    <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**
```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**
```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**
```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](https://dev-to-uploads.s3.amazonaws.com/i/w9h7ttih50w74ge81c3f.png)

##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`:

```js
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**
```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 }
```
```js
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.:

```js
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](https://dev-to-uploads.s3.amazonaws.com/i/gfhlpd252j1s3unzzut9.png)

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`.

```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`.