DEV Community

Tobias Mesquita for Quasar Framework Brasil

Posted on

QPANC - Parte 16 - Quasar - Áreas Protegidas

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

33 Área Principal/Protegida - Parte I.

Agora, precisamos criar um segundo layout, que será utilizado pelas paginas protegidas.

Iremos adicionar a fonte mdi-v5, por ele ter um maior leques de ícones que a fonte oficial do google, isto será feito em quasar.config.js > extras > mdi-v5.

module.exports = function (ctx) {
  return {
    extras: [
      'mdi-v5'
    ]
  }
}

Então crie a pasta main em QPANC.App/src/layouts e adicione os arquivos store.js, index.js, index.sass e index.vue

QPANC.App/src/layouts/main/store.js

import { factory } from '@toby.mosque/utils'

class MainLayoutModel {
  constructor ({
    leftDrawerOpen = false
  } = {}) {
    this.leftDrawerOpen = leftDrawerOpen
  }
}

const options = {
  model: MainLayoutModel
}

export default factory.store({
  options,
  actions: {
    async initialize ({ state }, { route, next }) {
    },
    async logout (context) {
      await this.$axios.delete('/Auth/Logout')
      commit('app/token', undefined, { root: true })
      this.$router.push('/login')
    }
  }
})

export { options, MainLayoutModel }

QPANC.App/src/layouts/main/index.js

import EssentialLink from 'components/EssentialLink'
import { factory } from '@toby.mosque/utils'
import store, { options } from './store'

const moduleName = 'layout-main'
export default factory.page({
  name: 'MainLayout',
  options,
  moduleName,
  storeModule: store,
  components: {
    EssentialLink
  },
  data () {
    return {
      essentialLinks: [
        {
          title: 'Docs',
          caption: 'quasar.dev',
          icon: 'school',
          link: 'https://quasar.dev'
        },
        {
          title: 'Github',
          caption: 'github.com/quasarframework',
          icon: 'code',
          link: 'https://github.com/quasarframework'
        },
        {
          title: 'Discord Chat Channel',
          caption: 'chat.quasar.dev',
          icon: 'chat',
          link: 'https://chat.quasar.dev'
        },
        {
          title: 'Forum',
          caption: 'forum.quasar.dev',
          icon: 'record_voice_over',
          link: 'https://forum.quasar.dev'
        },
        {
          title: 'Twitter',
          caption: '@quasarframework',
          icon: 'rss_feed',
          link: 'https://twitter.quasar.dev'
        },
        {
          title: 'Facebook',
          caption: '@QuasarFramework',
          icon: 'public',
          link: 'https://facebook.quasar.dev'
        }
      ]
    }
  },
  methods: {
    logout () {
      return this.$store.dispatch('layout-main/logout')
    }
  }
})

QPANC.App/src/layouts/main/index.sass

#layout-main

QPANC.App/src/layouts/main/index.html

<template>
  <q-layout id="layout-main" view="lHh Lpr lFf" class="bg-main">
    <q-header elevated>
      <q-toolbar>
        <q-btn
          flat
          dense
          round
          icon="menu"
          aria-label="Menu"
          @click="leftDrawerOpen = !leftDrawerOpen"
        />

        <q-toolbar-title>
          Quasar App
        </q-toolbar-title>

        <div>
          Quasar v{{ $q.version }}
          <q-btn flat icon="mdi-exit-to-app" label="logout" @click="logout"></q-btn>
        </div>
      </q-toolbar>
    </q-header>

    <q-drawer
      v-model="leftDrawerOpen"
      show-if-above
      bordered
      content-class="bg-content"
    >
      <q-list>
        <q-item-label header>
          Essential Links
        </q-item-label>
        <EssentialLink
          v-for="link in essentialLinks"
          :key="link.title"
          v-bind="link"
        />
      </q-list>
    </q-drawer>

    <q-page-container>
      <router-view />
    </q-page-container>
  </q-layout>
</template>

<script src="./index.js"></script>
<style src="./index.sass" lang="sass"></style>

Note que, este é basicamente o layout padrão do quasar, adaptado para o modo dark/light e com a função de logout.

E agora, vamos adicionar a nossa primeira pagina que irá utilizar este layout. Crie a pasta home na pasta QPANC.App/src/pages e adicione os arquivos store.js, index.js, index.sass e index.vue

QPANC.App/src/pages/home/store.js

import { factory } from '@toby.mosque/utils'

class HomePageModel {
}

const options = {
  model: HomePageModel
}

export default factory.store({
  options,
  actions: {
    async initialize ({ state }, { route, next }) {
    }
  }
})

export { options, HomePageModel }

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

import { factory } from '@toby.mosque/utils'
import store, { options } from './store'

const moduleName = 'page-home'
export default factory.page({
  name: 'HomePage',
  options,
  moduleName,
  storeModule: store
})

QPANC.App/src/pages/home/index.sass

#page-home

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

<template>
  <q-page id="page-home" class="flex flex-center">
    Home
  </q-page>
</template>

<script src="./index.js"></script>
<style src="./index.sass" lang="sass"></style>

tanto trabalho para exibir a palavra Home, mas precisávamos de uma pagina apenas.

Agora, crie o arquivo main.js na pasta QPANC.App/src/router/areas e adicione as seguintes rotas.

QPANC.App/src/router/areas/main.js

export default function (context) {
  return {
    path: '',
    component: () => import('layouts/main/index.vue'),
    children: [
      { name: 'home', path: 'home', component: () => import('pages/home/index.vue') }
    ],
    meta: {
      authorize: true
    }
  }
}

Note a presença do meta, nós usamos este campo, quando queremos adiciona alguma informação de controle a rota, neste caso, que o usuário precisa está logado para acessar este componente (e os seus respectivos filhos).

inclua a área main no arquivo routes.js:

QPANC.App/src/router/routes.js

import clean from './areas/clean'
import main from './areas/main'

export default function (context) {
  const routes = [{
    path: '/',
    component: { /* ... */ },
    children: [
      {
        path: '',
        beforeEnter (to, from, next) { /* ... */ }
      },
      clean(context),
      main(context)
    ]
  }]

  // Always leave this as last one
  if (process.env.MODE !== 'ssr') { /*...*/ }

  return routes
}

E agora vamos criar um navigation guard global, que fará uso do meta authorize, para isto, precisamos fazer a seguinte alteração no index.js.

QPANC.App/src/router/index.js

/* ... */

export default function (context) {
  context.router = new VueRouter({ /*...*/ })

  context.router.beforeEach((to, from, next) => {
    const { store } = context
    let protectedRoutes = to.matched.filter(route => route.meta.authorize)
    if (protectedRoutes.length > 0) {
      const logged = store.getters['app/isLogged']()
      if (!logged) {
        next('/login')
      }
    }
    next()
  })
  return context.router
}

Então execute a aplicação e tente acessar a raiz da aplicação (rota '/'), e veja que seja redirecionado para '/login' se não estiver logado e '/home' se estiver.

Caso coloque um console.log({ path: to.path, authorize: requireAuth }) no beforeEach, você verá que para o path '/' o protectedRoutes.length será 0.

Caso tente acessar a pagina de login (rota /login) enquanto logado, você terá acesso a esta rota, o que é esperado.

Caso tente acessar a pagina home (rota /home) enquanto não está logado, você será enviado para a rota /login, o que também é esperado.

34 Área Principal/Protegida - Parte 2.

Agora, iremos cria uma segunda pagina, só que para acessar esta pagina, o usuário além de autenticado, precisa está autorizado, neste caso, a autorização é feita apenas para os usuários que tenham a role Developer

O primeiro passo, é criar uma nova page, copie a pasta home e renomeie a copia para devboard, e claro, em prol da consistência, não esqueça de renomear qual quer incidência da palavra home para devboard.

Agora que temos um componente, temos que criar um rota para ele, isto será feito no arquivo main.js em QPANC.App/src/router/areas

QPANC.App/src/router/areas/main.js

export default function (context) {
  return {
    path: '',
    component: () => import('layouts/main/index.vue'),
    children: [
      { name: 'home', path: 'home', component: () => import('pages/home/index.vue') },
      {
        name: 'devboard',
        path: 'devboard',
        component: () => import('pages/devboard/index.vue'),
        meta: {
          authorize: {
            roles: ['Developer']
          }
        }
      }
    ],
    meta: {
      authorize: true
    }
  }
}

Note que os meta authorize, tentam ter o mesmo comportamento que o AuthorizeAttribute no C#, desta forma, todos os componentes com authorize serão testando, o que na pratica combina todos os authorize.

O segundo ponto, é que no campo roles é possível declarar múltiplas role, porém teremos um OR e não um AND. exemplo, se tivemos ['Developer', 'Admin'], então basta que o usuário seja um Admin ou Developer para ter acesso a esta rota.

O próximo passo, é incluir alguns getters na store app, estes getters serão responsável por testar de o usuário possui algumas das roles declaradas.

QPANC.App/src/store/app.js

import { factory } from '@toby.mosque/utils'

/*...*/

const roleClaimName = 'http://schemas.microsoft.com/ws/2008/06/identity/claims/role'
export default factory.store({
  options,
  getters: {
    /*...*/
    roles (state, getters) {
      if (!getters.decoded || !getters.decoded.exp) {
        return []
      }
      const roles = getters.decoded[roleClaimName]
      if (Array.isArray(roles)) {
        return roles
      } else {
        return [roles]
      }
    },
    isOnRoles (state, getters) {
      return (roles) => {
        return getters.roles.some(role => roles.include(role))
      }
    }
  }
})

Note que, ao decompor o token JWT, o campo http://schemas.microsoft.com/ws/2008/06/identity/claims/role poderá ser um array ou uma string, caso ele seja uma string, devemos retornar um array tendo ele como único elemento.

O getter isOnRoles testa se o usuário logado possui pelo menos uma role que pertença a lista de roles passada.

Agora, precisamos fazer uma alteração no beforeEach que está no index.js em QPANC.App/src/router, para que ele faça uso dos novos getters.

QPANC.App/src/router/index.js

router.beforeEach((to, from, next) => {
  const { store } = context
  let protectedRoutes = to.matched.filter(route => route.meta.authorize)
  if (protectedRoutes.length > 0) {
    const logged = store.getters['app/isLogged']()
    if (!logged) {
      return next('/login')
    }

    protectedRoutes = protectedRoutes.filter(route => route.meta.authorize.roles)
    if (protectedRoutes.length > 0) {
      for (const protectedRoute of protectedRoutes) {
        const { roles } = protectedRoute.meta.authorize
        const isOnRoles = store.getters['app/isOnRoles'](roles)
        if (!isOnRoles) {
          return next('/home')
        }
      }
    }
  }
  next()
})

Perceba que, caso o usuário não esteja logado, e tenta acessar uma área que requer autenticação, ele será redirecionado para a tela de login, porém se estiver logado, mas não tenha autorização, ele seja redirecionado para a tela inicial (home).

E para testamos este fluxo, iremos criar um QBtn na pagina home, que deve ser visível apenas para usuários com a role Developer

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

<template>
  <q-page id="page-home" class="flex flex-center">
    Home
    <template v-if="isDeveloper">
      <q-btn to="/devboard" :label="$('actions.devboard')" />
    </template>
  </q-page>
</template>

<script src="./index.js"></script>
<style src="./index.sass" lang="sass"></style>

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

import { factory } from '@toby.mosque/utils'
import store, { options } from './store'

const moduleName = 'page-home'
export default factory.page({
  name: 'HomePage',
  options,
  moduleName,
  storeModule: store,
  computed: {
    isOnRoles () {
      return this.$store.getters['app/isOnRoles']
    },
    isDeveloper () {
      return this.isOnRoles(['Developer'])
    }
  }
})

Quasar.App/src/i18n/en-us/index.js

export default {
  actions: {
    devboard: 'Developer Board'
  }
}

Quasar.App/src/i18n/pt-br/index.js

export default {
  actions: {
    devboard: 'Painel do Desenvolvedor'
  }
}

Agora, acesse a pagina Home, e desde que seja um Developer, você verá um QBtn que irá redirecionar para a pagina Devboard.

Alt Text
Alt Text

Agora, tente alterar a regra de autorização, para ao invés de testar se o usuário é um Developer, testar se é ele um Admin (estou suponto que o seu usuário não é um Admin).

Neste caso, o QBtn da pagina Home deverá desaparecer, e caso tente acessar a pagina Devboard diretamente, será redirecionado para a Home, como este redirecionamento é feito no servidor, a pagina Devboard sequer será renderizada.

Discussion (0)