Vue Simple ACL
A simple unopinionated Vue plugin for managing user roles and permissions, access-control list (ACL) and role-based access control (RBAC).
Table of Contents
Features
- Vue 2 and Vue 3 support
- Simple but robust and power ACL plugin
- Manage roles and permissions with ease.
- Lightweight (<3 kB zipped)
- Component
v-can
directive
- Global
$can
helper function
- Sematic alias methods and directives of different verb for directive and helper function. E.g
v-role
, v-permission
, $acl.permission()
, $acl.anyRole()
, etc.
- Middleware support for Vue Router through
meta
property.
- Support user data from plain object, pinia/vuex store and asynchronous function.
- Reactive changes of abilities and permissions
- Define custom ACL rules
- Fully Typecript: The source code is written entirely in TypeScript.
- Fully configurable
Installation
NPM
npm install vue-simple-acl
Yarn
yarn add vue-simple-acl
CDN
UNPKG
JSDelivr
Usage
Usage with Vue 3
import { createApp } from 'vue'
import App from './App.vue'
import router from './store';
import store from './store';
import acl from './acl';
const app = createApp(App);
app.use(router);
app.use(store);
app.use(acl);
app.mount("#app");
Usage with Vue 2
In Vue 2, when using User data from reactive Store/Pinia/Vuex wrapped with computed()
function, which is available in Vue 3 as module by default but not in Vue 2, make sure to install @vue/composition-api first and change the imported module to: import { computed } from '@vue/composition-api'
import Vue from 'vue'
import App from './App.vue'
import router from './router';
import store from './store';
import acl from './acl';
Vue.config.productionTip = false;
Vue.use(acl);
new Vue({
router,
store,
render: h => h(App),
}).$mount('#app')
ACL Rules File
For readability, it is recommend to defined your ACL rules in a separate file.
import router from "../router";
import store from "../store";
import { computed } from 'vue';
import { createAcl, defineAclRules } from 'vue-simple-acl';
import Vue from 'vue';
import VueCompositionAPI from '@vue/composition-api';
Vue.use(VueCompositionAPI);
import { computed } from '@vue/composition-api';
import { createAcl, defineAclRules } from 'vue-simple-acl';
const user = {
id: 1,
name: 'Victory Osayi',
is_editor: true,
is_admin: false,
permissions: ['admin', 'owner', 'moderator', 'create-post', 'edit-post', 'delete-post']
}
const user2 = computed(() => store.state.auth.user);
const user3 = () => {
const authUserId = 1;
return axios.get(`api/users/${authUserId}`)
.then((response) => response.data);
}
const rules = () => defineAclRules((setRule) => {
setRule('create-post', (user) => user.is_admin || user.is_editor);
setRule('is-admin', (user) => user.is_admin);
setRule('is-editor', (user) => user.is_editor);
setRule('edit-post', (user, post) => user.id === post.user_id);
setRule('delete-post', (user, post) => {
return user.id === post.user_id || user.is_admin;
});
setRule(['publish-post', 'unpublish-post'], (user, post) => user.id === post.user_id);
setRule('hide-comment', (user, post, comment) => {
return user.is_admin || user.id === post.user_id || (user.id === comment.user_id && post.id === comment.post_id);
});
setRule('moderator', (user) => user.permissions && user.permissions.includes('moderator'));
});
const simpleAcl = createAcl({
user,
rules,
router,
});
export default simpleAcl;
Usage in component
The v-can
directive can be used in different ways and you can apply one or more modifiers that alters the behaviour of the directive.
<button v-can:create-post>Create Post</button>
<button v-can:edit-post="{ id: 100, user_id: 1, title: 'First Post' }">Edit</button>
<button v-can:edit-post="postData">Edit</button>
Alternative you can use the sematic alias;
<button v-permission:create-post>Create Post</button>
<button v-role:admin>Create Post</button>
<button v-role-or-permission="['admin', 'create-post']">Edit</button>
hide
modifier
This is the default behaviour of the v-can
directive, it remove the component or element from the DOM more like v-if
.
You're not required to apply it unless you want to explicitly state the behavior.
<button v-can:edit-post.hide="postData">Edit</button>
disable
modifier
The disable
modifier applies the disabled attribute to the tag, e.g. to disable a button or input that you are not allowed to use or edit respectively.
<button v-can:edit-post.disable="postData">Edit</button>
<input v-can:edit-post.disable="post" v-model="postTitle" type="text">
readonly
modifier
The readonly
modifier applies the readonly attribute to the tag, e.g. to make an input read only if you don't have permission to edit.
<input v-can:edit-post.readonly="post" v-model="postTitle" type="text">
not
modifier
The not
modifier reverses the ACL query. In this example only if you cannot delete the post the div element is shown.
<div v-can:delete-post.not="postData">You can not delete post created by you, ask admin for help.</div>
any
modifier
By default v-can
directive with value that contains array of multiple abilities and ability arguments will be authorized if all specified abilities passes.
The any
modifier authorized if atleast one or any of specified abilities and ability arguments passes.
<button v-can="['create-post', ['edit-post', post]]">Create Post</button>
<button v-can.any="[['edit-post', post], ['delete-post', post]]">Manage Post</button>
Using helper function in component
You can also use the helper function $can directly in component and javascript:
<form v-if="$can('edit-post', post)">
<input type="text" :value="post.title">
...
</form>
or in Option API
if (this.$can('edit-post', post)) {
axios.put(`/api/posts/${post.id}`, postData)
}
Using helper function in setup
Vue's Composition API
The introduction of setup
and Vue's Composition API, open up new possibilities but to be able to get the full potential out of Vue Simple ACL, we will need to use composable functions to replace access to this.
import { useAcl } from 'vue-simple-acl';
export default {
setup() {
const acl = useAcl();
if (acl.can('edit-post', post)) {
axios.put(`/api/posts/${post.id}`, postData)
}
if (acl.can('hide-comment', [post, comment])) {
}
if (acl.can.not('edit-post', post)) {
}
if (acl.can.any(['is-admin', 'is-editor'])) {
}
if (acl.can.any([ 'is-admin', ['delete-post', post] ])) {
}
const user = acl.user;
const user = acl.getUser();
}
}
To integrate Vue Router, hook up the instance of vue-router
's createRouter({..})
during setup of the Vue Simple ACL.
const simpleAcl = createAcl({
user,
rules,
router,
onDeniedRoute: '/unauthorized'
});
app.use(simpleAcl);
You configure routes by adding can
meta property to the route. E.g. if a router requires create post permission:
{
path: 'posts/create',
name: 'createPost',
component: CreatePost,
meta: {
can: 'create-post',
onDeniedRoute: '/unauthorized'
}
}
If you have a rule that requires multiple abilities, you can do the following:
{
path: 'posts/create',
name: 'createPost',
component: CreatePost,
meta: {
can: ['is-admin', 'create-post'],
onDeniedRoute: { name: 'unauthorizedPage', replace: true }
}
}
or using not
modifier
{
path: 'posts/create',
name: 'createPost',
component: CreatePost,
meta: {
notCan: 'moderator',
onDeniedRoute: { name: 'unauthorizedPage', replace: true }
}
}
or using any
modifier
{
path: 'posts/create',
name: 'createPost',
component: CreatePost,
meta: {
anyCan: ['is-admin', 'create-post'],
onDeniedRoute: { name: 'unauthorizedPage', replace: true }
}
}
You can also have an async evaluation by providing a callback that returns a promise the following:
{
path: 'posts/:postId',
component: PostEditor,
meta: {
can: (to, from, can) => {
return axios.get(`/api/posts/${to.params.id}`)
.then((response) => can('edit-post', response.data));
},
onDeniedRoute: '/unauthorized'
}
}
or using any
modifier
{
path: 'posts/:postId/publish',
component: ManagePost,
meta: {
anyCan: (to, from, anyCan) => {
return axios.get(`/api/posts/${to.params.id}/publish`)
.then((response) => anyCan(['is-admin', ['edit-post', response.data]]));
},
onDeniedRoute: '/unauthorized'
}
}
or get the data of the defined ACL user in the evaluations by passing user
as the optional fourth argument to the defined ACL meta function
{
path: 'posts/:postId/publish',
component: ManagePost,
meta: {
anyCan: (to, from, anyCan, user) => {
return axios.get(`/api/users/${user.id}/posts/${to.params.id}/publish`)
.then((response) => anyCan(['is-admin', ['edit-post', response.data]]));
},
onDeniedRoute: '/unauthorized'
}
}
onDeniedRoute
meta property
By default if you omit the 'onDeniedRoute' property from the a routes meta a denied check will redirect to a value of Vue Simple Acl's createAcl
option onDeniedRoute
which is /
by default. You can change this behaviour by setting the createAcl
option onDeniedRoute
. This is useful if you use the package in an authentication or authorization flow by redirecting to unauthorized page if access is denied.
You can also use an object for more options (see guards section in docs):
onDeniedRoute: { path: '/login': replace: true }
This will use replace rather than push when redirecting to the login page.
$from as value onDeniedRoute
onDeniedRoute: '$from'
You can set the onDeniedRoute to the special value '$from'
which will return the user to wherever they came from
Vue Router meta
Properties
can or allCan | string OR array of abilities OR function of asynchronous evaluation: (to, from, can, user?) => {} | None | Equivalent of $can() and v-can="" |
notCan or canNot | string OR array of abilities OR function of asynchronous evaluation: (to, from, notCan, user?) | None | Equivalent of $can.not() and v-can.not="" |
anyCan or canAny | string OR array of abilities OR function of asynchronous evaluation: (to, from, anyCan, user?) | None | Equivalent of $can.any() and v-can.any="" |
onDeniedRoute | string OR object of route() option | Value of the default option onDeniedRoute | A route to redirect to when `can |
Semantic Alias directives and methods
Vue Simple ACL also provides some directives and methods in different verb as alias for default directive and helper function. You can use these aliases in place of v-can
directive, $can
helper function and vue router can:
meta property for better semantic. See below table.
Permission | As Directives:
v-permission:create-post
v-permission="'create-post'" v-permission.not="'create-post'" v-permission.any="['create-post', ['edit-post', post]]"
In Component: $acl.permission('create-post') $acl.notPermission('create-post') $acl.anyPermission(['create-post', ['edit-post', post]])
In Option API: this.$acl.permission('create-post') this.$acl.notPermission('create-post') this.$acl.anyPermission(['create-post', ['edit-post', post]])
In Composition API/setup() : const acl = useAcl(); acl.permission('create-post') acl.notPermission('create-post') acl.anyPermission(['create-post', ['edit-post', post]])
In Vue Router meta Property: permission: 'create-post' notPermission: ['create-post', 'create-category']
anyPermission: (to, from, anyPermission) => { return axios.get( `/api/posts/${to.params.id} `) .then((response) => anyPermission(['create-post', ['edit-post', response.data]]));
} |
Role | As Directives:
v-role:admin
v-role="'admin'" v-role.not="'editor'" v-role.any="['admin', 'editor']"
In Component: $acl.role('admin') $acl.notRole('editor') $acl.anyRole(['admin', 'editor'])
In Option API: this.$acl.role('admin') this.$acl.notRole('editor') this.$acl.anyRole(['admin', 'editor'])
In Composition API/setup() : const acl = useAcl(); acl.role('admin') acl.notRole('editor') acl.anyRole(['admin', 'editor'])
In Vue Router meta Property: role: 'admin' notRole: 'editor' anyRole: ['admin', 'editor'] |
Role Or Permission | As Directives:
v-role-or-permission="['admin', 'create-post']" v-role-or-permission.not="['editor', 'create-post']" v-role-or-permission.any="['admin', 'create-post', ['edit-post', post]]"
In Component: $acl.roleOrPermission(['admin', 'create-post']) $acl.notRoleOrPermission(['editor', 'create-post']) $acl.anyRoleOrPermission(['admin', 'create-post', ['edit-post', post]])
In Option API: this.$acl.roleOrPermission(['admin', 'create-post']) this.$acl.notRoleOrPermission(['editor', 'create-post']) this.$acl.anyRoleOrPermission(['admin', 'create-post', ['edit-post', post]])
In Composition API/setup() : const acl = useAcl(); acl.roleOrPermission(['admin', 'create-post']) acl.notRoleOrPermission(['editor', 'create-post']) acl.anyRoleOrPermission(['admin', 'create-post', ['edit-post', post]])
In Vue Router meta Property: roleOrPermission: ['admin', 'create-post'] notRoleOrPermission: ['editor', 'create-post', 'create-category']
anyRoleOrPermission: (to, from, anyRoleOrPermission) => { return axios.get( `/api/posts/${to.params.id} `) .then((response) => anyRoleOrPermission(['admin', 'create-post', ['edit-post', response.data]]));
} |
User | Get the data of the defined ACL user.
In Component: $acl.user; // returns user object
$acl.getUser(); // returns user object
In Option API: this.$acl.user; // returns user object
this.$acl.getUser(); // returns user object
In Composition API/setup() : const acl = useAcl(); acl.user; // returns user object
acl.getUser(); // returns user object
In Vue Router meta Property: Pass user as the fourth argument to the defined ACL meta function
roleOrPermission: (to, from, roleOrPermission, user) => { return axios.get( `/api/users/${user.id}/posts/${to.params.id} `) .then((response) => roleOrPermission(['admin', ['edit-post', response.data]]));
} |
Vue Simple ACL Options
can be a user OBJECT, FUNCTION returning a user object
// or an Asynchronous function returning a PROMISE of user object, suitable for performing fetch from API.
user | object or a `function | async function/Promisereturning user object. <br> *Using Async/Promise requires instance of vue-router, the function will be auto hooked to beforeEach()` peroperty of vue-router.* | Yes | None |
rules | function | Yes | None | function returning instance of defineAclRules() e.g () => defineAclRules((setRule) => {...} |
directiveName | object or a function returning user object | No | 'can' | You can set a custom directive name if the default name conflicts with other installed plugins. e.g 'custom-can' then in component like v-custom-can="" |
helperName | object or a function returning user object | No | '$can' | You can set a custom helper name if the default name conflicts with other installed plugins. e.g '$customCan' , then use in component like '$customCan()' or '$customCan.not()' |
enableSematicAlias | boolean | No | true | You can enable or disable the sematic alias directives and methods e.g v-role , v-permission , $acl.* , etc. See Semantic Alias |
router | vue-router | No | None | Inte |
onDeniedRoute | string or object | No | / | A route to redirect to when can evaluation is denied. e.g string path '/unauthorized' OR router option { path: '/unauthorized' } OR { name: 'unauthorizedPage', replace: true } OR special value '$from' which returns back to the request URI |
TODO
- Chore: Write basic tests
- A documentation page with vitepress
🤝 Contributing
- Fork this repository.
- Create new branch with feature name.
- Go to example folder and run
npm install
and npm run serve
.
- The plugin sources files is located in
/src/*
.
- Commit and set commit message with feature name.
- Push your code to your fork repository.
- Create pull request. 🙂
⭐️ Support
If you like this project, You can support me with starring ⭐ this repository, buy me a coffee or become a patron.
📄 License
MIT
Developed by Victory Osayi with ❤️ and ☕️ from Benin City, Edo, Nigeria.