DEV Community

Cover image for Part 1: User Roles and Management - Quasar
Rachel
Rachel

Posted on • Updated on

Part 1: User Roles and Management - Quasar

The Quasar Frontend

I chose the Quasar Framework for its abundance of out-of-the-box components. I've used it for several prototyping projects already in the past, and it allows for rapid development, which is especially useful for prototyping. I didn't focus too much on the frontend for this starter, as the frontend ends up being highly customized for the client's brand and needs anyway, so I just focused on functionality. As a result, I left some unneeded scaffolded code as-is which provides no value to this starter in its current state.

Getting Started

I'm going to assume some basic understanding of Vue, Vuex, VueRouter, and Quasar. I'm not going into specifics for how the Quasar framework is set up, but I'll cover some customizations for this project. If you're new to Vue.js or Vuex, it's recommended to do a project or two with Vue before jumping into Quasar's Getting Started Guide.

Note: I would advise against copying/pasting any code snippets and instead go directly to the repository to view the code. Due to the amount of code involved, I've omitted lines of code in this article for brevity. It's not written as a tutorial, so please view the repository if you want to create a similar project.

Additional Libraries

In addition to Quasar, I also used several libraries for additional functionality:

The Views

I only needed a few views to implement the various backend functions planned for this starter:

  • Register
  • Login
  • Reset Password
  • Verify Email
  • Account
  • Forgot Password
  • Admin Dashboard
  • Home

Once I identified which views were needed, I created the pages for each view and added it to the routes. Most pages consist of forms to allow data entry. There isn't too much complexity in the UI, as most of them are forms, so I won't go into too much detail, but let me know if you have any questions!

Validation

I used the vuelidate library for form validation. To add it to Quasar, I used a boot file.

Here's an example checking the email in the Login:

<q-input 
   outlined
   v-model="$v.formData.email.$model"
   label-color="accent"
   label="Email"
   type="email"
   lazy-rules
   dense
   class="q-ma-sm"
   :error="$v.formData.email.$invalid && 
   $v.formData.email.$dirty"
            >
      <template v-slot:prepend>
          <q-icon color="accent" name="fas fa-envelope" size="xs" />
      </template>
      <template v-slot:error>
          <div v-if="!$v.formData.email.required">
              This field is required.
          </div>
          <div v-if="!$v.formData.email.email">
              Please provide a valid email address
          </div>
      </template>
</q-input>
Enter fullscreen mode Exit fullscreen mode
import { required, minLength, email } from "vuelidate/lib/validators";

export default {
  data() {
    return {
      formData: {
        email: ""
      }
    };
  },
  validations: {
    formData: {
      email: {
        required,
        email
      }
    }
  },
}
Enter fullscreen mode Exit fullscreen mode

Vuelidate made it pretty easy to check form values with minimal custom code, which I really appreciated (I'm a lazy dev...).

FeathersJS Client

The FeatherJS framework provides several client libraries that can be used to handle authentication and service calls. I incorporated this so I wouldn't have to implement the authentication on my own.

To use the feathers client library, I added a feathersClient.js boot file, which pulled in my axios configuration settings stored in axios.js. The axios configuration enables the display of a loading bar when a request is made.

I also setup Feathers-Vuex with the feathersClient boot file:

// Setting up feathers-vuex
const {
  makeServicePlugin,
  makeAuthPlugin,
  BaseModel,
  models,
  FeathersVuex
} = feathersVuex(feathersClient, {
  serverAlias: "api", // optional for working with multiple APIs (this is the default value)
  idField: "_id", // Must match the id field in your database table/collection
  whitelist: ["$regex", "$options"]
});

export { makeAuthPlugin, makeServicePlugin, BaseModel, models, FeathersVuex };
Enter fullscreen mode Exit fullscreen mode

This provides basic authentication handling via the following vuex actions: auth/authenticate and auth/logout.

Route Guards

Additionally, to handle some access-based views, I created an auth.js boot file which redirects users as needed based on their authorized user status.

router.beforeEach((to, from, next) => {
    if (to.meta.requiresAuth) {
      // if requires admin
      if (store.state.auth.user) {
        if (to.meta.requiresAdmin) {
          if (
            store.state.auth.user.permissions &&
            store.state.auth.user.permissions.includes("admin")
          ) {
            next();
          } else {
            Notify.create({
              message: `Your account is not authorized to see this view. If this is in error, please contact support.`,
              color: "negative"
            });
            next("/account");
          }
        } else if (
          to.path === "/" ||
          to.path === "/login" ||
          to.path === "/register"
        ) {
          next("/account");
        } else if (!LocalStorage.getItem("feathers-jwt") && to.path !== "/") {
          next("/login");
        } else {
          next();
        }
      } else {
        if (to.path !== "/login") {
          next("/login");
        } else {
          next();
        }
      }
    } else {
      next();
    }
Enter fullscreen mode Exit fullscreen mode

I added in metadata to specify the need for the administrative user status for admin routes.

{
    path: "/admin",
    component: () => import("layouts/MainLayout.vue"),
    children: [
      {
        path: "",
        name: "AdminHome",
        component: () => import("pages/admin/Index.vue"),
        meta: { requiresAuth: true, requiresAdmin: true }
      }
    ]
  },
Enter fullscreen mode Exit fullscreen mode

If the route required admin access, it would check if the user held the necessary permissions to access the route.

Navigation

The default layout MainLayout was updated to include several navigation links. For a logged-in user, navigation becomes available for the account, admin (if the user is admin), and logout. For an unauthorized user, login and register links become available.

App.vue

The App.vue entry point leverages the Vue 3 Composition API (which is also added to Quasar via a boot file) to authenticate a user if a JWT token is stored when the app first loads. It also checks for a change in the user's authorization status and will redirect the user if they login/logout.

Services

I created a user service to help with making requests. For the feathers client, I added a folder for feathers specific services, which would mirror the server-side feathers service. I didn't add any customization other than to configure the client-side service to know which server-side service it would need to communicate with:

const servicePath = "users";
const servicePlugin = makeServicePlugin({
  Model: User,
  service: feathersClient.service(servicePath),
  servicePath
});
Enter fullscreen mode Exit fullscreen mode

This is with the Feathers-Vuex Service Plugin. This service should be customized to your user service needs, which this starter does not do.

For the application user service, which provides a service layer for executing requests from FeathersClient or Axios, I separated the user service into admin and account service calls for modularity.

For simplicity, I used axios to make the few unauthenticated calls (creating a user, etc.) needed in this starter, though for production, feathers client should probably be used for all calls.

The service calls would look like this:

export async function updateUser(user, id) {
  return feathersClient.service("users").patch(id, user);
}
Enter fullscreen mode Exit fullscreen mode

or

export async function updateIdentity(password, currentEmail, updatedEmail) {
  return axiosInstance.post("/authManagement", {
    action: "identityChange",
    value: {
      password: password,
      changes: { email: updatedEmail },
      user: {
        email: currentEmail
      }
    }
  });
}
Enter fullscreen mode Exit fullscreen mode

Edit: After working with Feathers-Vuex more, I realized how powerful it actually is and would re-do the service layer in this example to completely use feathers-vuex rather than have a separate user service to handle some other service requests. Definitely a case of read the documentation before using it!

The FeathersJS backend uses feathers-authentication-management and feathers-permissions. These service files demonstrate the necessary payload to send to the FeatherJS backend for the front-end implementation of the feathers-authentication-management library, which will be discussed in greater depth in the next article.

State Management

The vuex store uses Feathers-Vuex to keep the store synchronized with Feathers Client requests. For authentication, there is a store.auth.js file in the root directory to configure the Feathers client authentication service to the users service:

import { makeAuthPlugin } from "../boot/feathersClient";
export default makeAuthPlugin({ userService: "users" });
Enter fullscreen mode Exit fullscreen mode

This is imported in the index as authvuex.

Feathers-Vuex is setup in the store index as follows:

import { FeathersVuex } from "../boot/feathersClient";
import authvuex from "./store.auth";

const servicePlugins = requireModule
  .keys()
  .map(modulePath => requireModule(modulePath).default);

Vue.use(FeathersVuex);

export default function(/* { ssrContext } */) {
  const Store = new Vuex.Store({
    plugins: [...servicePlugins, authvuex],
    modules: {
      account,
      admin
    },
    strict: process.env.DEV
  });

  return Store;
}
Enter fullscreen mode Exit fullscreen mode

The store is namespaced with admin and account modules, mirroring the separation in the user service. The action.js files make all the necessary service calls and handle the result of the call. A default state is defined, and some mutations are defined to handle the results of the service calls.

Did I Miss Anything?

I think this covers the key updates for building out a Quasar frontend to work with a FeathersJS backend. Let me know if I left anything out. Leave a comment with questions, suggestions, etc. I'll update this article accordingly!

Discussion (0)