Creating the User Vuex Module, Pages, and Routes

Now, it's time to start giving the application a recognizable face. In this chapter, we will start developing the interaction between the user and the application.

We will use the knowledge we've gathered from the preceding chapters to bring this application to life by using custom business rules, Vuex data stores, special application layouts, and pages that your user will be able to interact with.

In this chapter, we will learn how to create the User Vuex module so that we can store and manage everything related to the user and the user registration, login, validation, and edit pages.

In this chapter, we'll cover the following recipes:

  • Creating the User Vuex module in your application
  • Creating User pages and routes for your application

Let's get started!

Technical requirements

In this chapter, we will be using Node.js, AWS Amplify, and Quasar Framework.

Attention, Windows users! You need to install an npm package called windows-build-tools to be able to install the required packages. To do this, open PowerShell as administrator and execute the > npm install -g windows-build-tools command.

To install Quasar Framework, you need to open a Terminal (macOS or Linux) or Command Prompt/PowerShell (Windows) and execute the following command:

> npm install -g @quasar/cli

To install AWS Amplify, you need to open a Terminal (macOS or Linux) or Command Prompt/PowerShell (Windows) and execute the following command:

> npm install -g @aws-amplify/cli

Creating the User Vuex module in your application

Now, it's time to start storing data in our application state manager or Vuex. In the application context, all the data that is stored is saved within namespaces.

In this recipe, we will learn how to create the user Vuex module. Using our knowledge from the previous chapter, we will then create actions to create a new user, update their data, validate the user, sign in the user on Amplify, and list all the users on the application.

Getting ready

The prerequisite for this recipe is Node.js 12+.

The Node.js global objects that are required for this recipe are as follows:

  • @aws-amplify/cli
  • @quasar/cli

To start our User Vuex store module, we will continue with the project that we created in Chapter 4, Creating Custom Application Components and Layouts.

This recipe will be completed using GraphQL queries and mutations, as well as their drivers, which were written in the Creating your first GraphQL API and Creating the AWS Amplify driver for your application recipes of Chapter 3, Setting Up Our Chat App - AWS Amplify Environment and GraphQL.

How to do it...

We will split the creation of the User Vuex module into five parts: creating the state, mutations, getters, and actions, and then adding the module to Vuex.

Creating the User Vuex state

To store data on a Vuex module, we need a state that will store it for us. Follow these steps to create the User state:

  1. In the store folder, create a new folder called user. Inside, create a new file called state.js and open it.
  2. Create a new function called createState, which returns a JavaScript object that provides the id, username, email, name, avatar, password, loading, validated, and error properties. The id, username, email, name, and password properties will be defined as an empty string, while the loading and validated properties will be defined as false. error will be defined as undefined and avatar is a JavaScript object with three properties – key, bucket, and region:
export function createState() {
return {
id: '',
username: '',
email: '',
name: '',
avatar: {
key: '',
bucket: '',
region: '',
},
password: '',
loading: false,
validated: false,
error: undefined,
};
}
  1. Finally, in order to export the state as a singleton and make it available as a JavaScript object, we need to export default the execution of the createState function:
export default createState();

Creating the User Vuex mutations

To save any data on a state, Vuex needs a mutation. Follow these steps to create the User mutation that will manage the mutations for this module:

  1. Create a new file called types.js inside the store/user folder and open it.
  2. In the file, export a default JavaScript object that provides the CREATE_USER, SET_USER_DATA, CLEAR_USER, USER_VALIDATED, LOADING, and ERROR properties. The values are the same as the properties, but they are formatted as strings:
export default {
CREATE_USER: 'CREATE_USER',
SET_USER_DATA: 'SET_USER_DATA',
CLEAR_USER: 'CLEAR_USER',
USER_VALIDATED: 'USER_VALIDATED',
LOADING: 'LOADING',
ERROR: 'ERROR',
};
  1. Create a new file called mutations.js inside the store/user folder and open it.
  2. Import the newly created types.js file and the createState JavaScript object from state.js:
import MT from './types';
import { createState } from './state';
  1. Create a new function called setLoading, with the state as the first argument. Inside, we will set state.loading to true:
function setLoading(state) {
state.loading = true;
}
  1. Create a new function called setError, with state as the first argument and error as the second with a default value of new Error(). Inside, we will set state.error to error and state.loading to false:
function setError(state, error = new Error()) {
state.error = error;
state.loading = false;
}
  1. Create a new function called createUser, with state as the first argument and a JavaScript object as the second. This JavaScript object will provide the id, email, password, name, and username properties. All of the properties will be empty strings. Inside the function, we will define the state properties as the ones we received in the argument of the function:
function createUser(state, {
id = '',
email = '',
password = '',
name = '',
username = '',
}) {
state.username = username;
state.email = email;
state.name = name;
state.id = id;
state.password = window.btoa(password);
state.loading = false;
}
  1. Create a new function called validateUser with state as the first argument. Inside it, we will set the state.validated property to true, delete the state.password property, and set the state.loading property to false:
function validateUser(state) {
state.validated = true;
delete state.password;
state.loading = false;
}
  1. Create a new function called setUserData, with state as the first argument and a JavaScript object as the second arguments. This object will provide the id, email, password, name, and username properties. All of them will be empty strings. avatar is a JavaScript object with three properties: key, bucket, and region. Inside the function, we will define the state properties as the ones we received in the argument of the function:
function setUserData(state, {
id = '',
email = '',
name = '',
username = '',
avatar = {
key: '',
bucket: '',
region: '',
},
}) {
state.id = id;
state.email = email;
state.name = name;
state.username = username;
state.avatar = avatar || {
key: '',
bucket: '',
region: '',
};

delete state.password;

state.validated = true;
state.loading = false;
}
  1. Create a new function called clearUser with state as the first argument. Then, in the function of it, we will get a new clean state from the createState function and iterate over the current state, defining the values of the state properties back to the default values:
function clearUser(state) {
const newState = createState();

Object.keys(state).forEach((key) => {
state[key] = newState[key];
});
}
  1. Finally, export a default JavaScript object, with the keys as the imported mutation types and the value as the functions that correspond to each type:
  • Set MT.LOADING to setLoading
  • Set MT.ERROR to setError
  • Set MT.CREATE_USER to createUser
  • Set MT.USER_VALIDATED to validateUser
  • Set MT.SET_USER_DATA to setUserData
  • Set MT.CLEAR_USER to clearUser:
export default {
[MT.LOADING]: setLoading,
[MT.ERROR]: setError,
[MT.CREATE_USER]: createUser,
[MT.USER_VALIDATED]: validateUser,
[MT.SET_USER_DATA]: setUserData,
[MT.CLEAR_USER]: clearUser,
};

Creating the User Vuex getters

To access the data stored on the state, we need to create some getters. Follow these steps to create getters for the user module:

In a getter function, the first argument that that function will receive will always be the current state of the Vuex store.
  1. Create a new file called getters.js inside the store/user folder.
  2. Create a new function called getUserId that returns state.id:
const getUserId = (state) => state.id;
  1. Create a new function called getUserEmail that returns state.email:
const getUserEmail = (state) => state.email;
  1. Create a new function called getUserUsername that returns state.username:
const getUserUsername = (state) => state.username;
  1. Create a new function called getUserAvatar that returns state.avatar:
const getUserAvatar = (state) => state.avatar;
  1. Create a new function called getUser that returns a JavaScript object that provides the id, name, username, avatar, and email properties. The values of these properties will correspond to state:
const getUser = (state) => ({
id: state.id,
name: state.name,
username: state.username,
avatar: state.avatar,
email: state.email,
});
  1. Create a new function called isLoading that returns state.loading:
const isLoading = (state) => state.loading;
  1. Create a new function called hasError that returns state.error:
const hasError = (state) => state.error;
  1. Finally, export a default JavaScript object with the created functions (getUserId, getUserEmail, getUserUsername, getUserAvatar, getUser, isLoading, and hasError) as properties:
export default {
getUserId,
getUserEmail,
getUserUsername,
getUserAvatar,
getUser,
isLoading,
hasError,
};

Creating the User Vuex actions

Follow these steps to create the User Vuex actions:

  1. Create a file called actions.js inside the store/user folder and open it.
  2. First, we need to import the functions, enums, and classes that we will be using here:
  • Import graphqlOperation from the aws-amplify npm package.
  • Import getUser and listUsers from the GraphQL queries.
  • Import createUser and updateUser from the GraphQL mutations.
  • Import the signUp, validateUser, signIn, getCurrentAuthUser, and changePassword functions from driver/auth.js.
  • Import AuthAPI from driver/appsync.
  • Import the Vuex mutation types from ./types.js:
import { graphqlOperation } from 'aws-amplify';
import { getUser, listUsers } from 'src/graphql/queries';
import { createUser, updateUser } from 'src/graphql/mutations';
import { AuthAPI } from 'src/driver/appsync';
import {
signUp,
validateUser,
signIn,
getCurrentAuthUser,
changePassword,
} from 'src/driver/auth';
import MT from './types';

  1. Create a new asynchronous function called initialLogin. This function will receive a JavaScript object as the first argument. This will provide a commit property. In this function, we will get the currently authenticated user, get their data from the GraphQL API, and commit the user data to the Vuex store:
async function initialLogin({ commit }) {
try {
commit(MT.LOADING);

const AuthUser = await getCurrentAuthUser();

const { data } = await AuthAPI.graphql(graphqlOperation(getUser, {
id: AuthUser.username,
}));

commit(MT.SET_USER_DATA, data.getUser);

return Promise.resolve(AuthUser);
} catch (err) {
commit(MT.ERROR, err);
return Promise.reject(err);
}
}
  1. Create a new asynchronous function called signUpNewUser. This function will receive a JavaScript object with a commit property as the first argument. The second argument is also a JavaScript object but has the email, name, and password properties. In this function, we will execute the signUp function from the auth.js driver to sign up and create the user in the AWS Cognito user pool, and then commit the user data to the Vuex store:
async function signUpNewUser({ commit }, {
email = '',
name = '',
username = '',
password = '',
}) {
try {
commit(MT.LOADING);

const userData = await signUp(email, password);

commit(MT.CREATE_USER, {
id: userData.userSub,
email,
password,
name,
username,
});

return Promise.resolve(userData);
} catch (err) {
commit(MT.ERROR, err);
return Promise.reject(err);
}
}
  1. Create a new asynchronous function called createNewUser. This function will receive a JavaScript object with the commit and state properties as the first argument. For the second argument, the function will receive a code string. In this function, we will fetch the user data from state and execute the validateUser function from the auth.js driver to check if the user is a valid user in the AWS Cognito user pool. Then we will execute the signIn function from auth.js, passing email and password as parameters password needs to be converted into an encrypted base64 string before we send it to the function. After that, we will fetch the authenticated user data and send it to the GraphQL API to create a new user:
async function createNewUser({ commit, state }, code) {
try {
commit(MT.LOADING);
const {
email,
name,
username,
password,
} = state;
const userData = await validateUser(email, code);

await signIn(`${email}`, `${window.atob(password)}`);

const { id } = await getCurrentAuthUser();

await AuthAPI.graphql(graphqlOperation(
createUser,
{
input: {
id,
username,
email,
name,
},
},
));

commit(MT.USER_VALIDATED);

return Promise.resolve(userData);
} catch (err) {
commit(MT.ERROR, err);
return Promise.reject(err);
}
}
  1. Create a new asynchronous function called signInUser. This function will receive a JavaScript object with the commit and dispatch properties as the first argument. The second argument, which is also a JavaScript object, will have the email and password properties. Inside this function, we will execute the signIn function from the auth.js driver, pass email and password as parameters, and then dispatch the initialLogin Vuex action:
async function signInUser({ commit, dispatch }, { email = '', password = '' }) {
try {
commit(MT.LOADING);

await signIn(`${email}`, `${password}`);

await dispatch('initialLogin');

return Promise.resolve(true);
} catch (err) {
commit(MT.ERROR);
return Promise.reject(err);
}
}
  1. Create a new asynchronous function called editUser. This function will receive a JavaScript object with the commit and state properties as the first argument. The second argument, which is also a JavaScript object, will have the username, name, avatar, password, and newPassword properties. Inside this function, we will merge the state values with the new ones that we received as arguments. We will then send them to the GraphQL API to update the user information. Then, we will check if we have both the password and newPasssword properties filled in. If so, we will execute the changePassword function from the auth.js driver to change the user's password in the AWS Cognito user pool:
async function editUser({ commit, state }, {
username = '',
name = '',
avatar = {
key: '',
bucket: '',
region: '',
},
password = '',
newPassword = '',
}) {
try {
commit(MT.LOADING);

const updateObject = {
...{
name: state.name,
username: state.username,
avatar: state.avatar,
},
...{
name,
username,
avatar,
},
};

const { data } = await AuthAPI.graphql(graphqlOperation(updateUser,
{ input: { id: state.id, ...updateObject } }));

if (password && newPassword) {
await changePassword(password, newPassword);
}

commit(MT.SET_USER_DATA, data.updateUser);

return Promise.resolve(data.updateUser);
} catch (err) {
return Promise.reject(err);
}
}
  1. Create a new asynchronous function called listAllUsers. This function will fetch all the users on the database and return a list:
async function listAllUsers() {
try {
const {
data: {
listUsers: {
items: usersList,
},
},
} = await AuthAPI.graphql(graphqlOperation(
listUsers,
));

return Promise.resolve(usersList);
} catch (e) {
return Promise.reject(e);
}
}
  1. Finally, we will export all the default created functions:
export default {
initialLogin,
signUpNewUser,
createNewUser,
signInUser,
editUser,
listAllUsers,
};

Adding the User module to Vuex

Follow these steps to import the created User module into the Vuex state:

  1. Create a new file called index.js inside the store/user folder.
  2. Import the state.js, actions.js, mutation.js, and getters.js files that we just created:
import state from './state';
import actions from './actions';
import mutations from './mutations';
import getters from './getters';
  1. Create an export default with a JavaScript object that provides the state, actions, mutations, getters, and namespaced (set to true) properties:
export default {
namespaced: true,
state,
actions,
mutations,
getters,
};
  1. Open the index.js file inside the store folder.
  2. Import the newly created index.js inside the store/user folder:
import Vue from 'vue';
import Vuex from 'vuex';
import user from './user';
  1. In the new Vuex class instantiation, we need to add a new property called modules and define it as a JavaScript object. Then, we need to add a new user property – this will be automatically used as the value because it has the same name as the imported User module from the previous step:
export default function (/* { ssrContext } */) {
const Store = new Vuex.Store({
modules: {
user,
},
strict: process.env.DEV,
});

return Store;
}

How it works...

When you declare your Vuex store, you need to create three main properties: state, mutations, and actions. These properties act as a single structure, bound to the Vue application through the injected $store prototype or the exported store variable.

A state is a centralized object that holds your information and makes it available to be used by mutations, actions, or components. Changing state always requires that a synchronous function is executed through a mutation.

A mutation is a synchronous function that can change state and be traced. This means that when you're developing, you can time travel through all the executed mutations in the Vuex store.

An action is an asynchronous function that can be used to hold business logic, API calls, dispatch other actions, and execute mutations. These functions are the common entry points when you need to make changes to a Vuex store.

A simple representation of a Vuex store can be seen in the following diagram:

In this recipe, we created the User Vuex module. This module includes all the business logic that will help us manage the user in our application, from creating a new user to updating it.

When we looked at the Vuex actions, we used the AppSync API client to fetch the data and send it to our GraphQL API. We did this using the queries and mutations that were created by the Amplify CLI. To be able to communicate with the GraphQL API so that we could update the user, we fetched the data we used in the Auth Driver from the Creating the AWS Amplify driver for your application recipe in Chapter 3, Setting Up Our Chat App - AWS Amplify Environment and GraphQL.

Those API requests are manipulated by the Vuex mutations and stored in the Vuex state, which we can access through the Vuex getter.

See also

Creating User pages and routes for your application

When working with a Vue application, you will need a way to manage the location of your users. You can handle this using a dynamic component, but the best way to do this is through route management.

In this recipe, we will learn how to create our application pages with the business rules required for each route. We will then use route management to handle everything.

Getting ready

The prerequisites for this recipe are as follows:

  • The project we created in the previous recipe
  • Node.js 12+

The Node.js global objects that are required for this recipe are as follows:

  • @aws-amplify/cli
  • @quasar/cli

To start our User page and routes, we will continue with the project that we created in the Creating the User Vuex module on your application recipe.

How to do it...

In this recipe, we will create all the pages that we will need for our user in our application: the login page, the signup page, and the user edit page.

Adding the Dialog plugin to Quasar

To use the Quasar Dialog plugin, we need to add it to the configuration file.

Open the quasar.conf.js file inside the project root folder and find the framework property. Then, in the plugins property, add the 'Dialog' string to the array so that Quasar loads the Dialog plugin when it boots the application:

framework: {
...
plugins: [
'Dialog',
],
...
},

Creating the User login page

For the User login page, we will use two of the components that we created previously: PasswordInput and EmailInput.

Single-file component <script> section

It's time to create the <script> section of the User login page:

  1. In the src/pages folder, open the Index.vue file.
  2. Import the mapActions and mapGetters functions from the vuex package:
import { mapActions, mapGetters } from 'vuex';
  1. Create an export default JavaScript object with five properties; that is, name (defined as 'Index'), components, data, computed, and methods:
export default {
name: 'Index',
components: {
},
data: () => ({
}),
computed: {
},
methods: {
},
};
  1. In the components property, add two new properties called PasswordInput and EmailInput. Define PasswordInput as an anonymous function with a return value of import('components/PasswordInput') and EmailInput as an anonymous function with a return value of import('components/EmailInput'):
components: {
PasswordInput: () => import('components/PasswordInput'),
EmailInput: () => import('components/EmailInput'),
},
    1. In the data property, we will return a JavaScript object that provides two properties, email and password, both of which will be empty strings:
    data: () => ({
    email: '',
    password: '',
    }),
    1. In the computed property, we will destruct the mapGetters function, passing the namespace of what module we want as the first parameter (in this case, 'user'). We will pass an array of getters we want to import (in this case, isLoading) as the second parameter:
    computed: {
    ...mapGetters('user', [
    'isLoading',
    'getUserId',
    ]),
    },
    1. On the beforeMount lifecycle hook, we will add an if statement, checking if the getUserId is truthy, and then redirect the user to the Contacts route.
    async beforeMount() {
    if (this.getUserId) {
    await this.$router.replace({ name: 'Contacts' });
    }
    },
    1. Finally, for the methods property, we will destruct the mapActions function, passing the namespace of the module we want – in this case, 'user' – as the first parameter. For the second parameter, we will use an array of actions we want to import – in this scenario, this is signInUser. Next, we need to add the asynchronous onSubmit method, which will dispatch signInUser and send the user to the Contacts route, and the createAccount method, which will send the user to the SignUp route:
    methods: {
    ...mapActions('user', [
    'signInUser',
    ]),
    async onSubmit() {
    try {
    await this.signInUser({
    email: this.email,
    password: this.password,
    });
    await this.$router.push({ name: 'Contacts' });
    } catch (e) {
    this.$q.dialog({
    message: e.message,
    });
    }
    },
    createAccount() {
    this.$router.push({ name: 'SignUp' });
    },
    },

    Single-file component <template> section

    Now, we need to add the <template> section to finish our page:

    1. Create a component called QPage with the class attribute defined as "bg-grey-1 flex flex-center":
    <q-page padding class="bg-grey-1 flex flex-center">
    </q-page>
    1. Inside the QPage component, create a QCard component with the style attribute defined as "width: 350px":
    <q-card style="width: 350px">
    </q-card>
    1. Inside the QCard component, create a QCardSection with an h6 child component that has the class attribute defined as no-margin:
    <q-card-section>
    <h6 class="no-margin">Chat Application</h6>
    </q-card-section>
    1. Now, create a QCardSection with a QForm child component that has the class attribute defined as q-gutter-md. Inside the QForm component, create an EmailInput component, with the v-model directive bound to the data.email, and a PasswordInput component, with the v-model directive bound to the data.password property:
    <q-card-section>
    <q-form
    class="q-gutter-md"
    >
    <email-input
    v-model.trim="email"
    />
    <password-input
    v-model.trim="password"
    />
    </q-form>
    </q-card-section>
    1. Then, create a QCardActions component with an align attribute defined as right. Inside, add a QBtn with the label attribute set to "Create new Account", color set to primary, class set to q-ml-sm, flat set to true, and the @click event listener bound to the createAccount method. Next, create another QBtn component with the label attribute set to "Login", type set to "submit", color set to primary, and the @click event listener bound to the onSubmit method:
    <q-card-actions align="right">
    <q-btn
    label="Create new account"
    color="primary"
    flat
    class="q-ml-sm"
    @click="createAccount"
    />
    <q-btn
    label="Login"
    type="submit"
    color="primary"
    @click="onSubmit"
    />
    </q-card-actions>
    1. Finally, create a QInnerLoading component with the :showing attribute bound to computed.isLoading. This will need to have a QSpinner child component that provides the size attribute. Set this to 50px and color to primary:
    <q-inner-loading :showing="isLoading">
    <q-spinner size="50px" color="primary"/>
    </q-inner-loading>

    To run the server and see your progress, you need to open a Terminal (macOS or Linux) or Command Prompt/PowerShell (Windows) and execute the following command:

    > quasar dev
    Remember to always execute the command npm run lint --fix, to automatically fix any code lint error.

    Here is a preview of what the page will look like:

    Creating the User signup page

    For the User signup page, we will use four components that we've already created: NameInput, UsernameInput, PasswordInput, and EmailInput.

    Single-file component <script> section

    Here, we will create the <script> section of the User signup page:

    1. Inside the src/pages folder, create a new file called SignUp.vue and open it.
    2. Import the mapActions and mapGetters functions from the vuex package:
    import { mapActions, mapGetters } from 'vuex';
    1. Create an export default JavaScript object that provides five properties: name (defined as 'SignUp'), components, data, computed, and methods:
    export default {
    name: 'SignUp',
    components: {},
    data: () => ({
    }),
    computed: {
    },
    methods: {
    },
    };
    1. In the components property, add four new properties: NameInput, UsernameInput, PasswordInput, and EmailInput. Define them like so:
    • NameInput as an anonymous function with a return value of import('components/NameInput')
    • UsernameInput as an anonymous function with a return value of import('components/UsernameInput')
    • PasswordInput as an anonymous function with a return value of import('components/PasswordInput')
    • EmailInput as an anonymous function with a return value of import('components/EmailInput')

    This can be seen in the following code:

    components: {
    PasswordInput: () => import('components/PasswordInput'),
    EmailInput: () => import('components/EmailInput'),
    UsernameInput: () => import('components/UsernameInput'),
    NameInput: () => import('components/NameInput'),
    },
    1. In the data property, we will return a JavaScript object that provides four properties name, username, email, and password all of which will be empty strings:
    data: () => ({
    name: '',
    username: '',
    email: '',
    password: '',
    }),
    1. In the computed property, we will destruct the mapGetters function, passing the namespace of what module we want in this case, 'user' – as the first parameter. For the second parameter, we will use an array of getters we want to import in this scenario, this is isLoading:
    computed: {
    ...mapGetters('user', [
    'isLoading',
    ]),
    },
    1. Finally, for the methods property, first, we will destruct the mapActions function, passing the namespace of what module we want – in this case, 'user' – as the first parameter. For the second parameter, we will pass an array of actions we want to import – in this scenario, this is signUpNewUser. Next, we need to add the asynchronous onSubmit method, which will dispatch signUpNewUser and then send the user to the Validate route, and the onReset method, which will clear the data:
    methods: {
    ...mapActions('user', [
    'signUpNewUser',
    ]),
    async onSubmit() {
    try {
    await this.signUpNewUser({
    name: this.name,
    username: this.username,
    email: this.email,
    password: this.password,
    });
    await this.$router.replace({ name: 'Validate' });
    } catch (e) {
    this.$q.dialog({
    message: e.message,
    });
    }
    },
    onReset() {
    this.email = '';
    this.password = '';
    },
    },

    Single-file component <template> section

    To finish the page, we need to add the <template> section:

    1. Create a QPage component with the class attribute defined as "bg-grey-1 flex flex-center":
    <q-page padding class="bg-grey-1 flex flex-center">
    </q-page>
    1. Inside the QPage component, create a QCard component with the style attribute defined as "width: 350px":
    <q-card style="width: 350px">
    </q-card>
    1. Inside the QCard component, create a QCardSection with a h6 child component where the class attribute is defined as no-margin:
    <q-card-section>
    <h6 class="no-margin">Create a new Account</h6>
    </q-card-section>
    1. After that, create a QCardSection with a QForm child component where the class attribute is defined as q-gutter-md. Inside the QForm component, create a NameInput component with the v-model directive bound to data.name, a UsernameInput component with the v-model directive bound to data.username, an EmailInput component with the v-model directive bound to data.email, and a PasswordInput component with the v-model directive bound to the data.password property:
    <q-card-section>
    <q-form
    class="q-gutter-md"
    >
    <name-input
    v-model.trim="name"
    />
    <username-input
    v-model.trim="username"
    />
    <email-input
    v-model.trim="email"
    />
    <password-input
    v-model.trim="password"
    />
    </q-form>
    </q-card-section>
    1. Now, create a QCardActions component with the align attribute set to right. Inside, add a QBtn with the label attribute set to "Reset", color set to primary, class set to q-ml-sm, flat set to true, and the @click event listener bound to the onReset method. Then, create another QBtn component with the label attribute set to "Create", type set to "submit", color set to primary, and the @click event listener bound to the onSubmit method:
    <q-card-actions align="right">
    <q-btn
    label="Reset"
    type="reset"
    color="primary"
    flat
    class="q-ml-sm"
    @click="onReset"
    />
    <q-btn
    label="Create"
    type="submit"
    color="primary"
    @click="onSubmit"
    />
    </q-card-actions>
    1. Finally, create a QInnerLoading component with the :showing attribute bound to computed.isLoading. This will need to have a QSpinner child component. The size attribute needs to be set to 50px and color needs to be set to primary:
    <q-inner-loading :showing="isLoading">
    <q-spinner size="50px" color="primary"/>
    </q-inner-loading>

    To run the server and see your progress, you need to open a Terminal (macOS or Linux) or Command Prompt/PowerShell (Windows) and execute the following command:

    > quasar dev
    Remember to always execute the command npm run lint --fix, to automatically fix any code lint error.

    Here is a preview of what the page will look like:

    Creating the User validation page

    Once the user has created an account, AWS Amplify will send an email with a validation pin-code that we will need to be sent back for validation purposes. This page will be the validation page.

    Single-file component <script> section

    Follow these steps to create the <script> section for the User validation page:

    1. Inside the src/pages folder, create a new file called Validate.vue and open it.
    2. Import the mapActions and mapGetters functions from the vuex package, and resendValidationCode from src/driver/auth:
    import { mapActions, mapGetters } from 'vuex';
    import { resendValidationCode } from 'src/driver/auth';
    1. Create an export default JavaScript object that provides four properties: name (defined as 'Validate'), data, computed, and methods:
    export default {
    name: 'Validate',
    data: () => ({
    }),
    computed: {
    },
    methods: {
    },
    };
    1. Inside the data property, we will return a JavaScript object with a code property as an empty string:
    data: () => ({
    code: '',
    }),
    1. Inside the computed property, we will destruct the mapGetters function, passing the namespace of what module we want in this case, 'user' – as the first parameter. For the second parameter, we will pass an array of getters we want to import in this scenario, isLoading and getUserEmail:
    computed: {
    ...mapGetters('user', [
    'isLoading',
    'getUserEmail',
    ]),
    },
    1. Finally, for the methods property we will destruct the mapActions function, passing the namespace of what module we want – in this case, 'user' – as the first parameter. For the second parameter, we will pass an array of actions we want to import – in this scenario, createNewUser. Next, we need to add the asynchronous onSubmit method, which will dispatch createNewUser and send the user to the Index route; the resendCode method, which will resend the user another validation code; and the onReset method, which will reset the data:
    methods: {
    ...mapActions('user', [
    'createNewUser',
    ]),
    async onSubmit() {
    try {
    await this.createNewUser(this.code);
    await this.$router.replace({ name: 'Index' });
    } catch (e) {
    console.error(e);
    this.$q.dialog({
    message: e.message,
    });
    }
    },
    async resendCode() {
    await resendValidationCode(this.getUserEmail);
    },
    onReset() {
    this.code = '';
    },
    },

    Single-file component <template> section

    Follow these steps to create the <template> section of the User validation page:

    1. Create a QPage component with the class attribute defined as "bg-grey-1 flex flex-center":
    <q-page padding class="bg-grey-1 flex flex-center">
    </q-page>
    1. Inside the QPage component, create a QCard component with the style attribute defined as "width: 350px":
    <q-card style="width: 350px">
    </q-card>
    1. Inside the QCard component, create a QCardSection with an h6 child component and the class attribute defined as no-margin. Then, create a sibling element with the class attribute defined as text-subtitle2:
    <q-card-section>
    <h6 class="no-margin">Validate new account</h6>
    <div class="text-subtitle2">{{ getUserEmail }}</div>
    </q-card-section>
    1. Create a QCardSection with two children components. These will be HTML elements, P:
    <q-card-section>
    <p>A validation code were sent to you E-mail.</p>
    <p>Please enter it to validate your new account.</p>
    </q-card-section>
    1. After that, create a QCardSection with a QForm child component and the class attribute defined as q-gutter-md. Inside the QForm component, add the QInput component as a child element. Then, inside the QInput component, bind the v-model directive to data.code. Inside the QInput rules attribute, define the rules value as an array of validation that will check if any code has been typed in. Enable lazy-rules so that it will only validate after a while:
    <q-card-section>
    <q-form
    class="q-gutter-md"
    >
    <q-input
    v-model.trim="code"
    :rules="[ val => val && val.length > 0
    || 'Please type the validation code']"
    outlined
    label="Validation Code"
    lazy-rules
    />
    </q-form>
    </q-card-section>
    1. Now, create a QCardActions component with the align attribute set to right. Inside, add a QBtn with the label attribute set to "Reset", color set to primary, class set to q-ml-sm, flat set to true, and the @click event listener bound to the onReset method. Create another QBtn with the label attribute set to "Re-send code", color set to secondary, class set to q-ml-sm, flat set to true, and the @click event listener bound to the resendCode method. Finally, create a QBtn component with the label attribute set to "Validate", type set to "submit", color set to primary, and the @click event listener bound to the onSubmit method:
    <q-card-actions align="right">
    <q-btn
    label="Reset"
    type="reset"
    color="primary"
    flat
    class="q-ml-sm"
    />
    <q-btn
    flat
    label="Re-send code"
    color="secondary"
    class="q-ml-sm"
    @click="resendCode"
    />
    <q-btn
    label="Validate"
    type="submit"
    color="primary"
    @click="onSubmit"
    />
    </q-card-actions>
    1. Finally, create a QInnerLoading component with the :showing attribute bound to computed.isLoading. It should have QSpinner child component with size set to 50px and color set to primary:
    <q-inner-loading :showing="isLoading">
    <q-spinner size="50px" color="primary"/>
    </q-inner-loading>

    To run the server and see your progress, you need to open a Terminal (macOS or Linux) or Command Prompt/PowerShell (Windows) and execute the following command:

    > quasar dev
    Remember to always execute the command npm run lint --fix, to automatically fix any code lint error.

    Here is a preview of what the page will look like:

    Creating the User edit page

    For the User edit page, we will use four components that we've already created: NameInput, UsernameInput, AvatarInput, and PasswordInput.

    Single-file component <script> section

    Follow these steps to start developing the <script> section of the User edit page:

    1. Inside the src/pages folder, create a new file called Edit.vue and open it.
    2. Import the mapActions and mapGetters functions from the vuex package:
    import { mapActions, mapGetters } from 'vuex';
    1. Create an export default JavaScript object that provides four properties: name (defined as 'SignUp'), data, computed, and methods:
    export default {
    name: 'EditUser',
    components: {},

    data: () => ({
    }),
    created() {},
    computed: {
    },
    methods: {
    },
    };
    1. Inside the components property, add four new properties called NameInput, UsernameInput, PasswordInput, AvatarInput. Set them like so:

    NameInput as an anonymous function with a return value of import('components/NameInput')

    UsernameInput as an anonymous function with a return value of import('components/UsernameInput')

    PasswordInput as an anonymous function with a return value of import('components/PasswordInput')

    AvatarInput as an anonymous function with a return value of import('components/AvatarInput'):

    components: {
    AvatarInput: () => import('/components/AvatarInput'),
    PasswordInput: () => import('components/PasswordInput'),
    UsernameInput: () => import('components/UsernameInput'),
    NameInput: () => import('components/NameInput'),
    },
    1. Inside the data property, we will return a JavaScript object that provides five properties: name, username, avatar, email, and password. All of these will be empty strings:
    data: () => ({
    name: '',
    username: '',
    avatar: '',
    password: '',
    newPassword: '',
    }),
    1. Inside the created life cycle hook, define data.name as getUser.name, data.username as getUser.username, and data.avatar as getUser.avatar:
    created() {
    this.name = this.getUser.name;
    this.username = this.getUser.username;
    this.avatar = this.getUser.avatar;
    },
    1. Inside the computed property, we will destruct the mapGetters function, passing the namespace of what module we want in this case, 'user' – as the first parameter. For the second parameter, we will pass an array of getters we want to import in this scenario, isLoading:
    computed: {
    ...mapGetters('user', [
    'getUser',
    'isLoading',
    ]),
    },
    1. Finally, for the methods property, we will destruct the mapActions function, passing the namespace of what module we want – in this case, 'user' – as the first parameter. For the second parameter, we will pass an array of actions we want to import – in this scenario, editUser. Next, we need to add the asynchronous onSubmit method, which will dispatch $refs.avatar.uploadFile() and then dispatch editUser to send the user to the Chat route, and the onReset method, which will clear the data:
    methods: {
    ...mapActions('user', [
    'editUser',
    ]),
    async onSubmit() {
    try {
    await this.$refs.avatar.uploadFile();

    await this.editUser({
    name: this.name,
    avatar: this.$refs.avatar.s3file,
    username: this.username,
    password: this.password,
    newPassword: this.newPassword,
    });

    await this.$router.replace({ name: 'Contacts' });
    } catch (e) {
    this.$q.dialog({
    message: e.message,
    });
    }
    },
    onReset() {
    this.name = this.getUser.name;
    this.username = this.getUser.username;
    this.password = '';
    this.newPassword = '';
    },
    },

    Single-file component <template> section

    Follow these steps to create the <template> section of the User edit page:

    1. Create a QPage component with the class attribute defined as "bg-grey-1 flex flex-center":
    <q-page padding class="bg-grey-1 flex flex-center">
    </q-page>
    1. Inside the QPage component, create a QCard component with the style attribute defined as "width: 350px":
    <q-card style="width: 350px">
    </q-card>
    1. Inside the QCard component, create a QCardSection with an h6 child component and with the class attribute defined as no-margin:
    <q-card-section>
    <h6 class="no-margin">Edit user account</h6>
    </q-card-section>
    1. After that, create a QCardSection with a QForm child component with the class attribute defined as q-gutter-md. Inside the QForm component, create an AvatarInput component with a reference directive defined as avatar and the v-model directive bound to data.avatar, a NameInput component with the v-model directive bound to data.name, a UsernameInput component with the v-model directive bound to data.username, an EmailInput component with the v-model directive bound to data.email, and a PasswordInput component with the v-model directive bound to the data.password property:
    <q-card-section>
    <q-form
    class="q-gutter-md"
    >
    <avatar-input
    v-model="avatar"
    ref="avatar"
    />
    <name-input
    v-model.trim="name"
    />
    <username-input
    v-model.trim="username"
    />
    <q-separator/>
    <password-input
    v-model.trim="password"
    label="Your old password"
    />
    <password-input
    v-model.trim="newPassword"
    label="Your new password"
    />
    </q-form>
    </q-card-section>
    1. Now, create a QCardActions component with the align attribute set to right. Inside, add a QBtn with the label attribute set to "Reset", color set to primary, class set to q-ml-sm, flat set to true, and the @click event listener bound to the onReset method. Then, create another QBtn component with the label attribute set to "Create", type set to "submit", color set to primary, and the @click event listener bound to the onSubmit method:
    <q-card-actions align="right">
    <q-btn
    label="Reset"
    type="reset"
    color="primary"
    flat
    class="q-ml-sm"
    @click="onReset"
    />
    <q-btn
    label="Update"
    type="submit"
    color="primary"
    @click="onSubmit"
    />
    </q-card-actions>
    1. Finally, create a QInnerLoading component with the :showing attribute bound to computed.isLoading. It should have a QSpinner child component with size set to 50px and color set to primary:
    <q-inner-loading :showing="isLoading">
    <q-spinner size="50px" color="primary"/>
    </q-inner-loading>

    To run the server and see your progress, you need to open a Terminal (macOS or Linux) or Command Prompt/PowerShell (Windows) and execute the following command:

    > quasar dev
    Remember to always execute the command npm run lint --fix, to automatically fix any code lint error.

    Here is a preview of what the page will look like:

    Creating application routes

    Now that we've created the user pages, components, and layout, we need to bind everything together so that it can be accessed by the user. To do this, we need to create the routes and make them available so that the user can navigate between the pages. Follow these steps to do this:

    1. Open the routes.js file inside the router folder.
    2. Make the routes constant an empty array:
    const routes = [];
    1. Add a JavaScript object with three properties, path, component, and children, to this array. The path property is a string and will be a static URL, the component property is an anonymous function that will return a WebPack import function with the component that will be rendered, and the children property is an array of components that will be rendered inside path. Each of the children components is a JavaScript object with the same properties, plus a new one called name:
    {
    path: '/',
    component: () => import('layouts/Base.vue'),
    children: [
    {
    path: '',
    name: 'Index',
    meta: {
    authenticated: false,
    },
    component: () => import('pages/Index.vue'),
    },
    ],
    },
    1. Now, for the /chat URL, we need to create two new placeholder pages inside the pages folder: Contacts.vue and Messages.vue. Inside these files, create an empty component with the following template:
    <template>
    <div />
    </template>
    <script>
    export default {
    name: 'PlaceholderPage',
    };
    </script>
    1. Inside the message route, we need to add two special parameters: :id and path. These parameters will be used to fetch a specific message between users:
    {
    path: '/chat',
    component: () => import('layouts/Chat.vue'),
    children: [
    {
    path: 'contacts',
    name: 'Contacts',
    component: () => import('pages/Contacts.vue'),
    },
    {
    path: 'messages/:id/:name',
    name: 'Messages',
    meta: {
    authenticated: true,
    goBack: {
    name: 'Contacts',
    },
    },
    component: () => import('pages/Messages.vue'),
    },
    ],
    },
    1. For the /user URL, we will create just one child route, the edit route. Inside this route, we are using the alias property since vue-router needs to have a child with path empty for the first child rendering. We will have also have a /user/edit route available inside our application:
    {
    path: '/user',
    component: () => import('layouts/Chat.vue'),
    children: [
    {
    path: '',
    alias: 'edit',
    name: 'Edit',
    meta: {
    authenticated: true,
    goBack: {
    name: 'Contacts',
    },
    },
    component: () => import('pages/Edit.vue'),
    },
    ],
    },

    1. Finally, for creating new users, we need to add the /register URL with two children: SignUp and Validate. The SignUp route will be the main route on the registered URL and will be called directly when the user enters this URL. The Validate route will only be called when the user is redirected to the /register/validate URL:
    {
    path: '/register',
    component: () => import('layouts/Base.vue'),
    children: [
    {
    path: '',
    alias: 'sign-up',
    name: 'SignUp',
    meta: {
    authenticated: false,
    },
    component: () => import('pages/SignUp.vue'),
    },
    {
    path: 'validate',
    name: 'Validate',
    meta: {
    authenticated: false,
    },
    component: () => import('pages/Validate.vue'),
    },
    ],
    },

    Adding the authentication guard

    To validate the user authentication token every time the user enters your application, if the token is valid, or if the user is trying to access a route without access, we need to create an authentication guard for our application:

    1. Create a new file called routeGuard.js inside the src/boot folder.
    2. Create a default export asynchronous function. Inside this parameter, add a JavaScript object with a property named app. Inside the function, create a constant with an object restructuring of app that gets the store property. Then, create a try/catch block. In the try part, check if the 'user/getUserId' gather isn't present and dispatch 'user/initialLogin'. Finally, inside the catch block, redirect the user to the Index route:
    export default async ({ app }) => {
    const { store } = app;

    try {
    if (!store.getters['user/getUserId']) {
    await store.dispatch('user/initialLogin');
    }
    } catch {
    await app.router.replace({ name: 'Index' });
    }
    };
    1. Finally, open the quasar.conf.js file inside the root folder of your project and find the boot property. Add the 'routerGuard' item to the array:
    boot: [
    'amplify',
    'axios',
    'routeGuard',
    ],

    How it works...

    In this chapter, we developed micro components such as NameInput, EmailInput, and so on to simplify the process of developing macro components or containers, such as pages.

    In this recipe, we used the components we developed in the previous recipe to create a complete page, such as the User login, User edit, and User registration pages.

    Using vue-router to manage the parent-child process of wrapping a page with a custom layout, we used the layouts we created in the previous recipes of this book to create the routes for our application. We made them available so that we can access the application as a normal web application, with custom URLs and routes.

    Finally, we added some authentication middleware to our main initialization Vue file so that we could redirect an already authenticated user. This means that they don't need to authenticate again when they enter the application for a second time.

    There's more...

    Now, your application is ready for user registration and login. It's possible to navigate through the user registration pages and receive an email from Amazon with a verification code so that you can verify the user on the server.

    To check your process and see it running on your local environment, open a Terminal (macOS or Linux) or Command Prompt/PowerShell (Windows) and execute the following command:

    > quasar dev
    Remember to always execute the command npm run lint --fix, to automatically fix any code lint error.

    See also

    ..................Content has been hidden....................

    You can't read the all page of ebook, please click here login for view all page.
    Reset
    52.14.130.13