CASL is a JavaScript library that allows you to manage the permissions of a user based on their role.
One of our roles as front-end developers is to reduce the number of requests sent to the server.
For example, we do front-end validations of a form. This means we don’t have to request the server with the data and cause the server to reply with validation errors.
We also manage permissions in the front end, so the user doesn’t have to request certain APIs for which they do not have permission. Eventually, we reduce the load on the server and for the user.
This does not mean we eliminate requesting unauthorized permissions APIs totally. We still need some of them in particular cases.
You can download the project repo here. You can find the final result here.
npx create-react-app casl-app
npm install redux react-redux redux-thunk
npm install @casl/react @casl/ability
Create a new file, name it can.js
, and paste the following:
import { Ability, AbilityBuilder } from "@casl/ability";import { store } from "../redux/storeConfig/store";const ability = new Ability();export default (action, subject) => {return ability.can(action, subject);};
Here we are importing Ability
and AbilityBuilder
from @casl/ability
.
Then we are creating a new instance from the Ability()
.
After that, we are exporting a default function that we will use later to check for the permission of the logged-in user.
Import your store and subscribe to it inside can.js
.
import { Ability, AbilityBuilder } from "@casl/ability";import { store } from "../redux/storeConfig/store";const ability = new Ability();export default (action, subject) => {return ability.can(action, subject);};store.subscribe(() => {let auth = store.getState().auth;});
Here, we get auth
from the store.
We can also see the redux folder and files.
|--redux
|-- auth
|-- authActions.js
|-- authReducer.js
|-- storeConfig
|-- store.js
|-- rootReducer.js
export const login = (user) => async (dispatch) => {dispatch({type: "LOGIN",payload: {id: 1,name: "Youssef",permissions: ["add_users", "delete_users"],},});};export const logout = () => async (dispatch) => {dispatch({type: "LOGOUT",});};
const INITIAL_STATE = {};const authReducer = (state = INITIAL_STATE, action) => {switch (action.type) {case "LOGIN":return { ...state, ...action.payload };case "LOGOUT":return {};default:return state;}};export default authReducer;
import { createStore, applyMiddleware, compose } from "redux";import createDebounce from "redux-debounced";import thunk from "redux-thunk";import rootReducer from "../rootReducer";const middlewares = [thunk, createDebounce()];const composeEnhancers = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose;const store = createStore(rootReducer,{},composeEnhancers(applyMiddleware(...middlewares)));export { store };
import { combineReducers } from "redux";import authReducer from "./auth/authReducer";const rootReducer = combineReducers({auth: authReducer,});export default rootReducer;
In the login action, we code the payload with an object of id, name, and permissions array.
defineRulesFor
function in can.js
import { Ability, AbilityBuilder } from "@casl/ability";import { store } from "../redux/storeConfig/store";const ability = new Ability();export default (action, subject) => {return ability.can(action, subject);};store.subscribe(() => {let auth = store.getState().auth;ability.update(defineRulesFor(auth));});const defineRulesFor = (auth) => {const permissions = auth.permissions;const { can, rules } = new AbilityBuilder();// This logic depends on how the// server sends you the permissions arrayif (permissions) {permissions.forEach((p) => {let per = p.split("_");can(per[0], per[1]);});}return rules;};
We create the defineRulesFor
function. It takes auth
as an argument, which we can get from the store we are subscribing to it.
Next, we add ability.update(defineRulesFor(auth))
to the store.subscribe()
body.
Then, we add can
and rules
from new AbilityBuilder()
The permissions array is a number of strings
separated by _
:
permissions: ["add_users", "delete_users"]
We split those strings and pass the action
and the subject
to the can
function.
This logic might change if the server, for examples, only sends IDs:
const permissions = [2, 3, 5, 7];
if (permissions) {
permissions.forEach((p) => {
if (p === 3) can("add", "users");
if (p === 7) can("delete", "users");
});
}
Or maybe a pre-defined role.
const role = "Editor";
if (role === "Editor") {
can("add", "users");
can("delete", "users");
}
And so on.
We check permissions inside App.jsx.
import React, { useState } from "react";import { useDispatch, useSelector } from "react-redux";import { login, logout } from "./redux/auth/authActions";import CAN from "./casl/can";export default () => {const dispatch = useDispatch();const { auth } = useSelector((state) => state);// rerender the component when `auth` changesuseState(() => {}, [auth]);return (<React.Fragment><h1>Welcome, {auth?.name || "Please Login!"}</h1>{CAN("add", "users") && (<buttononClick={() => {alert("User Added!");}}>Add User</button>)}{CAN("delete", "users") && (<buttononClick={() => {alert("User Deleted!");}}>Delete User</button>)}<div><buttononClick={() => {dispatch(login());}}>Login</button><buttononClick={() => {dispatch(logout());}}>Logout</button></div></React.Fragment>);};
Here, we display the buttons based on the permission of the logged-in user.
We need to rerender the component when the
auth
changes usinguseEffect
.
Check the final result here.