Role-based authorization and access control for frontend

In this project module, we'll dive into the exciting world of authorization and access control in front-end development. Today I will share with you my experience with Vue 3, Pinia for global state management and TypeScript technologies. However, it's worth noting that the basic principles we'll cover here apply to any modern front-end technology. So, even if you prefer a different technology stack, you will still find this material useful.

We will focus on developing a role-based authorization and access control system for the frontend. This aspect of web development plays a key role in ensuring the security of the application and defining the functionality available to different users.

Let's begin our journey in the world of front-end development by learning how to effectively implement authorization and access control using modern tools and best practices. Get ready for an exciting dive into the world of frontend security!

Start

Let's start with the most basic step – sending a user login request. In this process, we send a request to the server providing the user's credentials. After a successful login, the server responds to us with a token, which we save for later use.

Besides the token, we also need to get information about the user, such as their role and access rights. To do this, we use a getMe request, which returns information about the current user after successful authentication.

state: () => {
        const currentUserString = localStorage.getItem('current_user');
        const currentUser = currentUserString ? JSON.parse(currentUserString) : {};
        return {
            current_user: currentUser as CurrentUser,
        }
},
      
actions: {
  async logIn(payload: UserAuth): Promise<AxiosResponse> {
    try {
        const res = await axios.post('/api-auth/Auth/Login', {...payload});
        if (res.data) {
            token.save(res.data);
            await getMe();
            return res;
        }
    } catch (error) {
        throw new Error('Ошибка входа. Пожалуйста, попробуйте еще раз.');
    }
  }

  async getMe(): Promise<AxiosResponse> {
    try {
        const res = await axios.get('/api-admin/User/GetMe');
        if (res && res.data) {
            const usersStore = useUsersStore(); // Предполагается, что useUsersStore() возвращает хранилище пользователей
            usersStore.current_user = res.data;
            localStorage.setItem('current_user', JSON.stringify(res.data));
            return res;
        }
    } catch (error) {
        throw new Error('Ошибка получения данных пользователя.');
    }
  }
}
interface Token {
  accessToken?: string;
  refreshToken?: string;
}

export const token = {
    save(value: Token) {
        if (value.accessToken) {
            Cookies.set(StorageItems.ACCESS_TOKEN, value.accessToken);
        }
        if (value.refreshToken) {
            Cookies.set(StorageItems.REFRESH_TOKEN, value.refreshToken);
        }
    },
    get():Token {
        return {
            accessToken: Cookies.get(StorageItems.ACCESS_TOKEN),
            refreshToken: Cookies.get(StorageItems.REFRESH_TOKEN)
        }
    },
    remove() {
        Cookies.remove(StorageItems.ACCESS_TOKEN);
        Cookies.remove(StorageItems.REFRESH_TOKEN);
    },
}

First of all, we send a request to authenticate the user using their credentials. After successful login, the server returns an access token, which is required to perform secure operations in the application.

For the convenience of working with tokens, we are creating a service that allows you to save, receive and update tokens. In this case, we save the received token in the user's cookie.

Next, to get information about the current user, we send a request to the server using the getMe method using the stored token. The server processes this request and returns information about the user, such as his role and access rights.

Having received this data, we save it in our browser's local storage using localStorage. Also, for the convenience of working with this data in the future, we save it in the user’s state. This allows us to easily access user information anywhere in the application and use it according to our needs.

Restrictions

One way to ensure readability and maintain code clarity is to use enums to represent user access values. For example, after retrieving a user's accesses from the server, we can store their values ​​as an array of numbers representing specific accesses.

However, for ease of use and understanding of the code, we can create an enum.ts file and define an enum in it, where each access will have a clear value associated with a specific numeric value.

export const enum PERMISSIONS {
    USER_ADD = 101,
    USER_EDIT = 102,
    USER_VIEW = 103,
    USER_DELETE = 104,
    ORGANIZATION_ADD = 105,
    ORGANIZATION_EDIT = 106,
    ORGANIZATION_VIEW = 107,
    ORGANIZATION_DELETE = 108,
}

This approach avoids the use of “magic” numbers in the code and makes it more understandable and maintainable.

Access processing

To handle user accesses, we can create another service called userCan. This service will take one or more numeric permission values ​​as an argument and return a Boolean value indicating whether the current user has the specified permissions or not.

export function userCan(permissions: number | number[]): boolean {
  const store= useUsersStore();
  return store.current_user.permissions.includes(permissions);
}

Further

After creating the userCan function, we can simply use it wherever we need to perform user access checks. Here are some examples:

const initialFetch = async () => {  
  if (userCan(PERMISSIONS.ORGANIZATION_VIEW)) {
    await organizationStore.fetchOrganizations();
  }
}

In this example, we use the userCan function to check if the current user has access to view organizations. If there is access, we call the fetchOrganizations method from the organization store.

<div class="d-flex" v-if="userCan(PERMISSIONS.ORGANIZATION_EDIT)">
   <el-switch @change="statusHandler(scope.row)" v-model="scope.row.isActive"/>
</div>

Here we have used userCan in a Vue.js template to conditionally render an element. If the user has access to edit organizations, a status switch (el-switch) is displayed, allowing you to change the status of the item.

Similar Posts

Leave a Reply

Your email address will not be published. Required fields are marked *