vue-accessguard 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +181 -0
- package/dist/logo.png +0 -0
- package/dist/vue-access-guard.es.js +112 -0
- package/dist/vue-access-guard.umd.js +1 -0
- package/package.json +58 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 sawalabs
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
<div align="center">
|
|
2
|
+
<img src="./public/logo.png" alt="AccessGuard Logo" width="100%" />
|
|
3
|
+
</div>
|
|
4
|
+
|
|
5
|
+
# @sawalabs/vue-accessguard
|
|
6
|
+
|
|
7
|
+
The **vue-accessguard** library is a robust, lightweight **Role-Based Access Control (RBAC)** solution for **Vue 3** applications. It provides granular control over UI rendering and module protection using Directives, Components, and Composables for protecting views depending on user *roles* or *permissions*. It supports dynamic **Wildcard Matching** (e.g. `*` for Super Admin or `resource:*` for namespaces).
|
|
8
|
+
|
|
9
|
+

|
|
10
|
+

|
|
11
|
+

|
|
12
|
+
|
|
13
|
+
## Features
|
|
14
|
+
- 🚀 **`v-can` Directive:** Quickly hide DOM elements using logical checks.
|
|
15
|
+
- 📦 **`<Guard>` Component:** Dynamically render elements reactively if conditions are met.
|
|
16
|
+
- 🛠️ **Composable (`useAccessGuard`):** A unified API to pragmatically validate authorization states within Vue components logic/setup context.
|
|
17
|
+
- ⭐ **Wildcard Permissions:** Simplify access checks using wildcard notation like `admin:*` or globally with `*`.
|
|
18
|
+
- 📘 **Storybook Integrated:** Interactive visual documentation inside `./src/stories`.
|
|
19
|
+
- ⚡ **Vite Support:** ES / UMD modules included. Built tightly with Vue 3 `provide` / `inject`.
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
## Installation
|
|
23
|
+
|
|
24
|
+
Using `pnpm`, `npm` or `yarn`:
|
|
25
|
+
|
|
26
|
+
```bash
|
|
27
|
+
pnpm add @sawalabs/vue-accessguard
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
## Setup & Configuration
|
|
31
|
+
|
|
32
|
+
Vue-accessguard needs an `AccessGuardProvider` context wrapped around the app layout. You need to provide user data with their `roles` and `permissions` to allow access management globally.
|
|
33
|
+
|
|
34
|
+
### 1. Register the Plugin
|
|
35
|
+
Include the plugin into your main entry file, like `main.ts`, in order to register the `v-can` directive globally.
|
|
36
|
+
|
|
37
|
+
```typescript
|
|
38
|
+
import { createApp } from 'vue'
|
|
39
|
+
import App from './App.vue'
|
|
40
|
+
import { install as AccessGuardPlugin } from '@sawalabs/vue-accessguard'
|
|
41
|
+
|
|
42
|
+
const app = createApp(App)
|
|
43
|
+
|
|
44
|
+
app.use(AccessGuardPlugin)
|
|
45
|
+
app.mount('#app')
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
### 2. Wrap App with `AccessGuardProvider`
|
|
49
|
+
Add `AccessGuardProvider` at the root component or main application layout. Supply the user permissions asynchronously.
|
|
50
|
+
|
|
51
|
+
```vue
|
|
52
|
+
<script setup lang="ts">
|
|
53
|
+
import { ref } from 'vue'
|
|
54
|
+
import { AccessGuardProvider, type AccessUser } from '@sawalabs/vue-accessguard'
|
|
55
|
+
|
|
56
|
+
// Example of authentication user state
|
|
57
|
+
const currentUser = ref<AccessUser>({
|
|
58
|
+
roles: ['editor'],
|
|
59
|
+
permissions: ['post:read', 'post:write']
|
|
60
|
+
})
|
|
61
|
+
</script>
|
|
62
|
+
|
|
63
|
+
<template>
|
|
64
|
+
<AccessGuardProvider :user="currentUser">
|
|
65
|
+
<AppContent />
|
|
66
|
+
</AccessGuardProvider>
|
|
67
|
+
</template>
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
---
|
|
71
|
+
|
|
72
|
+
## Core Concepts
|
|
73
|
+
|
|
74
|
+
Understanding these fundamental access concepts is the key to effectively using the Vue AccessGuard library parameters:
|
|
75
|
+
|
|
76
|
+
### `permissions` vs `roles`
|
|
77
|
+
* **`permissions`**: Fine-grained, action-oriented strings that explicitly describe what an entity can *do* within a system (e.g., `'article:read'`, `'post:delete'`, `'user:impersonate'`). Usually, these reflect direct API actions or view visibility rules.
|
|
78
|
+
* **`roles`**: Generalized groups or titles assigned to a user (e.g., `'admin'`, `'editor'`, `'viewer'`). Behind the scenes, a role is essentially a collection of various permissions. Use `roles` across broad system boundaries (e.g., entirely hiding the "Admin Area Layout") and `permissions` for specific sub-components (e.g., the "Delete" button within the dashboard).
|
|
79
|
+
|
|
80
|
+
### Match Modes (`any` vs `all`)
|
|
81
|
+
Whenever you pass an array of required roles or permissions strings to evaluate, AccessGuard needs to know *how* to evaluate them using the `mode` parameter:
|
|
82
|
+
* **`mode: 'any'` (Default)**: The check passes if the authenticated user has **at least one** of the requested strings. Used as a logical **OR**.
|
|
83
|
+
* **`mode: 'all'`**: The check strictly passes only if the authenticated user has **100%** of the requested strings. Used as a logical **AND**.
|
|
84
|
+
|
|
85
|
+
---
|
|
86
|
+
|
|
87
|
+
## Directives (`v-can`)
|
|
88
|
+
|
|
89
|
+
The `v-can` directive removes elements from the screen by manipulating the style `display: none` property based on user permissions.
|
|
90
|
+
|
|
91
|
+
```vue
|
|
92
|
+
<template>
|
|
93
|
+
<!-- Single permission check -->
|
|
94
|
+
<button v-can="'post:delete'">Delete Post</button>
|
|
95
|
+
|
|
96
|
+
<!-- Multiple permission check (any of them will render) -->
|
|
97
|
+
<button v-can="['post:edit', 'post:delete']">Manage Post</button>
|
|
98
|
+
|
|
99
|
+
<!-- Strictly require ALL permissions in the array -->
|
|
100
|
+
<button v-can="{ permission: ['post:edit', 'post:publish'], mode: 'all' }">
|
|
101
|
+
Publish Post
|
|
102
|
+
</button>
|
|
103
|
+
</template>
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
---
|
|
107
|
+
|
|
108
|
+
## Component (`<Guard>`)
|
|
109
|
+
|
|
110
|
+
The `<Guard>` component conditionally renders DOM trees based on *roles* or *permissions*. Unlike `v-can`, the entire DOM subtree inside `Guard` isn't created if the authorization fails.
|
|
111
|
+
|
|
112
|
+
```vue
|
|
113
|
+
<script setup lang="ts">
|
|
114
|
+
import { Guard } from '@sawalabs/vue-accessguard'
|
|
115
|
+
</script>
|
|
116
|
+
|
|
117
|
+
<template>
|
|
118
|
+
<!-- Renders if user has 'admin' or 'manager' role -->
|
|
119
|
+
<Guard :role="['admin', 'manager']" mode="any">
|
|
120
|
+
<AdminPanel />
|
|
121
|
+
</Guard>
|
|
122
|
+
|
|
123
|
+
<!-- Only renders if user has both permissions -->
|
|
124
|
+
<Guard :permission="['user:create', 'user:delete']" mode="all">
|
|
125
|
+
<UserManagement />
|
|
126
|
+
</Guard>
|
|
127
|
+
</template>
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
---
|
|
131
|
+
|
|
132
|
+
## Composable (`useAccessGuard`)
|
|
133
|
+
|
|
134
|
+
Used inside `<script setup>` contexts to handle granular logical flows pragmatically.
|
|
135
|
+
|
|
136
|
+
```vue
|
|
137
|
+
<script setup lang="ts">
|
|
138
|
+
import { useAccessGuard } from '@sawalabs/vue-accessguard'
|
|
139
|
+
|
|
140
|
+
const { can, cannot, hasRole } = useAccessGuard()
|
|
141
|
+
|
|
142
|
+
const submitForm = () => {
|
|
143
|
+
if (!hasRole('admin')) {
|
|
144
|
+
alert("Admins only!")
|
|
145
|
+
return
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
if (can(['article:write', 'article:publish'], 'all')) {
|
|
149
|
+
console.log("Ready to execute logic.")
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
</script>
|
|
153
|
+
|
|
154
|
+
<template>
|
|
155
|
+
<button @click="submitForm" :disabled="cannot('article:publish')">
|
|
156
|
+
Submit
|
|
157
|
+
</button>
|
|
158
|
+
</template>
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
---
|
|
162
|
+
|
|
163
|
+
## Wildcard Matching
|
|
164
|
+
|
|
165
|
+
Vue-accessguard scales linearly with enterprise needs with built in string-based access matchers:
|
|
166
|
+
|
|
167
|
+
- **Super Admin (`*`)**: Given the string `['*']`, AccessGuard bypasses all validations locally. Giving infinite access to all parts.
|
|
168
|
+
- **Resource Wildcard (`[resource]:*`)**: Providing user permissions such as `['post:*']` covers all operations linked to `can('post:read')`, `can('post:delete')`, `can('post:create')`.
|
|
169
|
+
|
|
170
|
+
## Storybook
|
|
171
|
+
|
|
172
|
+
You can visually test permission mocking by launching Storybook locally:
|
|
173
|
+
|
|
174
|
+
```bash
|
|
175
|
+
pnpm run storybook
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
Navigate to `http://localhost:6006` to modify UI controls, testing elements against random permutations.
|
|
179
|
+
|
|
180
|
+
---
|
|
181
|
+
**License**: MIT
|
package/dist/logo.png
ADDED
|
Binary file
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import { defineComponent as l, toRef as y, provide as A, effectScope as v, watchEffect as h, inject as G, computed as w } from "vue";
|
|
2
|
+
const u = /* @__PURE__ */ Symbol("AccessGuard"), b = l({
|
|
3
|
+
name: "AccessGuardProvider",
|
|
4
|
+
props: {
|
|
5
|
+
user: {
|
|
6
|
+
type: Object,
|
|
7
|
+
required: !0
|
|
8
|
+
}
|
|
9
|
+
},
|
|
10
|
+
setup(r, { slots: n }) {
|
|
11
|
+
const s = y(r, "user");
|
|
12
|
+
return A(u, {
|
|
13
|
+
user: s
|
|
14
|
+
}), () => n.default?.();
|
|
15
|
+
}
|
|
16
|
+
});
|
|
17
|
+
function d(r, n, s = "any") {
|
|
18
|
+
if (!r?.length) return !1;
|
|
19
|
+
const t = Array.isArray(n) ? n : [n], e = (o) => {
|
|
20
|
+
if (r.includes("*") || r.includes(o)) return !0;
|
|
21
|
+
const [c] = o.split(":"), i = `${c}:*`;
|
|
22
|
+
return r.includes(i);
|
|
23
|
+
};
|
|
24
|
+
return s === "all" ? t.every(e) : t.some(e);
|
|
25
|
+
}
|
|
26
|
+
const S = {
|
|
27
|
+
mounted(r, n) {
|
|
28
|
+
const s = n.instance;
|
|
29
|
+
if (!s) return;
|
|
30
|
+
let t = s.$.parent, e;
|
|
31
|
+
for (; t && !e; )
|
|
32
|
+
e = t.provides?.[u], t = t.parent;
|
|
33
|
+
if (!e)
|
|
34
|
+
throw new Error("[AccessGuard] v-can used outside AccessGuardProvider");
|
|
35
|
+
const o = v();
|
|
36
|
+
o.run(() => {
|
|
37
|
+
h(() => {
|
|
38
|
+
const c = n.value;
|
|
39
|
+
let i, a = "any";
|
|
40
|
+
typeof c == "string" || Array.isArray(c) ? i = c : (i = c.permission, a = c.mode ?? "any");
|
|
41
|
+
const m = e.user.value?.permissions ?? [], p = d(m, i, a);
|
|
42
|
+
r.style.display = p ? "" : "none";
|
|
43
|
+
});
|
|
44
|
+
}), r._scope = o;
|
|
45
|
+
},
|
|
46
|
+
unmounted(r) {
|
|
47
|
+
r._scope?.stop();
|
|
48
|
+
}
|
|
49
|
+
};
|
|
50
|
+
function f() {
|
|
51
|
+
const r = G(
|
|
52
|
+
u
|
|
53
|
+
);
|
|
54
|
+
if (!r)
|
|
55
|
+
throw new Error("useAccessGuard must be used within AccessGuardProvider");
|
|
56
|
+
const n = (t, e = "any") => {
|
|
57
|
+
const o = r.user.value?.permissions || [];
|
|
58
|
+
return d(o, t, e);
|
|
59
|
+
};
|
|
60
|
+
return {
|
|
61
|
+
can: n,
|
|
62
|
+
cannot: (t, e) => !n(t, e),
|
|
63
|
+
hasRole: (t, e = "any") => {
|
|
64
|
+
const o = r.user.value?.roles || [], c = Array.isArray(t) ? t : [t];
|
|
65
|
+
return e === "all" ? c.every((i) => o.includes(i)) : c.some((i) => o.includes(i));
|
|
66
|
+
}
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
const g = l({
|
|
70
|
+
name: "Guard",
|
|
71
|
+
props: {
|
|
72
|
+
permission: {
|
|
73
|
+
type: [String, Array],
|
|
74
|
+
required: !1
|
|
75
|
+
},
|
|
76
|
+
role: {
|
|
77
|
+
type: [String, Array],
|
|
78
|
+
required: !1
|
|
79
|
+
},
|
|
80
|
+
mode: {
|
|
81
|
+
type: String,
|
|
82
|
+
default: "any"
|
|
83
|
+
}
|
|
84
|
+
},
|
|
85
|
+
setup(r, { slots: n }) {
|
|
86
|
+
const s = f(), t = w(() => {
|
|
87
|
+
if (!r.permission && !r.role) return !0;
|
|
88
|
+
let e = !0;
|
|
89
|
+
return r.permission && (e = e && s.can(r.permission, r.mode)), r.role && e && (e = e && s.hasRole(r.role, r.mode)), e;
|
|
90
|
+
});
|
|
91
|
+
return () => t.value ? n.default?.() : null;
|
|
92
|
+
}
|
|
93
|
+
});
|
|
94
|
+
function x(r) {
|
|
95
|
+
const { can: n, hasRole: s } = f();
|
|
96
|
+
return r.beforeEach((t) => {
|
|
97
|
+
const e = t.meta;
|
|
98
|
+
if (e.permission && !n(e.permission, e.mode) || e.role && !s(e.role, e.mode))
|
|
99
|
+
return e.redirect || "/";
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
function E(r) {
|
|
103
|
+
r.directive("can", S);
|
|
104
|
+
}
|
|
105
|
+
export {
|
|
106
|
+
b as AccessGuardProvider,
|
|
107
|
+
u as AccessGuardSymbol,
|
|
108
|
+
g as Guard,
|
|
109
|
+
x as applyAccessGuard,
|
|
110
|
+
E as install,
|
|
111
|
+
f as useAccessGuard
|
|
112
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
(function(t,c){typeof exports=="object"&&typeof module<"u"?c(exports,require("vue")):typeof define=="function"&&define.amd?define(["exports","vue"],c):(t=typeof globalThis<"u"?globalThis:t||self,c(t.VueAccessGuard={},t.Vue))})(this,(function(t,c){"use strict";const d=Symbol("AccessGuard"),y=c.defineComponent({name:"AccessGuardProvider",props:{user:{type:Object,required:!0}},setup(r,{slots:s}){const o=c.toRef(r,"user");return c.provide(d,{user:o}),()=>s.default?.()}});function f(r,s,o="any"){if(!r?.length)return!1;const n=Array.isArray(s)?s:[s],e=i=>{if(r.includes("*")||r.includes(i))return!0;const[u]=i.split(":"),a=`${u}:*`;return r.includes(a)};return o==="all"?n.every(e):n.some(e)}const p={mounted(r,s){const o=s.instance;if(!o)return;let n=o.$.parent,e;for(;n&&!e;)e=n.provides?.[d],n=n.parent;if(!e)throw new Error("[AccessGuard] v-can used outside AccessGuardProvider");const i=c.effectScope();i.run(()=>{c.watchEffect(()=>{const u=s.value;let a,m="any";typeof u=="string"||Array.isArray(u)?a=u:(a=u.permission,m=u.mode??"any");const v=e.user.value?.permissions??[],w=f(v,a,m);r.style.display=w?"":"none"})}),r._scope=i},unmounted(r){r._scope?.stop()}};function l(){const r=c.inject(d);if(!r)throw new Error("useAccessGuard must be used within AccessGuardProvider");const s=(n,e="any")=>{const i=r.user.value?.permissions||[];return f(i,n,e)};return{can:s,cannot:(n,e)=>!s(n,e),hasRole:(n,e="any")=>{const i=r.user.value?.roles||[],u=Array.isArray(n)?n:[n];return e==="all"?u.every(a=>i.includes(a)):u.some(a=>i.includes(a))}}}const A=c.defineComponent({name:"Guard",props:{permission:{type:[String,Array],required:!1},role:{type:[String,Array],required:!1},mode:{type:String,default:"any"}},setup(r,{slots:s}){const o=l(),n=c.computed(()=>{if(!r.permission&&!r.role)return!0;let e=!0;return r.permission&&(e=e&&o.can(r.permission,r.mode)),r.role&&e&&(e=e&&o.hasRole(r.role,r.mode)),e});return()=>n.value?s.default?.():null}});function G(r){const{can:s,hasRole:o}=l();return r.beforeEach(n=>{const e=n.meta;if(e.permission&&!s(e.permission,e.mode)||e.role&&!o(e.role,e.mode))return e.redirect||"/"})}function h(r){r.directive("can",p)}t.AccessGuardProvider=y,t.AccessGuardSymbol=d,t.Guard=A,t.applyAccessGuard=G,t.install=h,t.useAccessGuard=l,Object.defineProperty(t,Symbol.toStringTag,{value:"Module"})}));
|
package/package.json
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "vue-accessguard",
|
|
3
|
+
"publishConfig": {
|
|
4
|
+
"access": "public"
|
|
5
|
+
},
|
|
6
|
+
"version": "1.0.0",
|
|
7
|
+
"type": "module",
|
|
8
|
+
"main": "dist/vue-access-guard.umd.js",
|
|
9
|
+
"module": "dist/vue-access-guard.es.js",
|
|
10
|
+
"types": "./dist/index.d.ts",
|
|
11
|
+
"files": [
|
|
12
|
+
"dist"
|
|
13
|
+
],
|
|
14
|
+
"exports": {
|
|
15
|
+
".": {
|
|
16
|
+
"import": "./dist/vue-access-guard.es.js",
|
|
17
|
+
"require": "./dist/vue-access-guard.umd.js"
|
|
18
|
+
}
|
|
19
|
+
},
|
|
20
|
+
"dependencies": {
|
|
21
|
+
"vue": "^3.5.25"
|
|
22
|
+
},
|
|
23
|
+
"devDependencies": {
|
|
24
|
+
"@chromatic-com/storybook": "^3.2.7",
|
|
25
|
+
"@storybook/addon-a11y": "^8.6.17",
|
|
26
|
+
"@storybook/addon-actions": "^8.6.17",
|
|
27
|
+
"@storybook/addon-docs": "^8.6.17",
|
|
28
|
+
"@storybook/addon-onboarding": "^8.6.17",
|
|
29
|
+
"@storybook/blocks": "^8.6.14",
|
|
30
|
+
"@storybook/vue3": "^8.6.17",
|
|
31
|
+
"@storybook/vue3-vite": "^8.6.17",
|
|
32
|
+
"@types/node": "^24.10.1",
|
|
33
|
+
"@vitejs/plugin-vue": "^6.0.2",
|
|
34
|
+
"@vitest/browser-playwright": "4.0.18",
|
|
35
|
+
"@vitest/coverage-v8": "4.0.18",
|
|
36
|
+
"@vue/test-utils": "^2.4.6",
|
|
37
|
+
"@vue/tsconfig": "^0.8.1",
|
|
38
|
+
"jsdom": "^28.1.0",
|
|
39
|
+
"playwright": "^1.58.2",
|
|
40
|
+
"storybook": "^8.6.17",
|
|
41
|
+
"storybook-vue3-router": "^4.0.1",
|
|
42
|
+
"typescript": "~5.9.3",
|
|
43
|
+
"vite": "^7.3.1",
|
|
44
|
+
"vitest": "^4.0.18",
|
|
45
|
+
"vue-router": "^4.6.4",
|
|
46
|
+
"vue-tsc": "^3.1.5"
|
|
47
|
+
},
|
|
48
|
+
"peerDependencies": {
|
|
49
|
+
"vue": "^3.5.29"
|
|
50
|
+
},
|
|
51
|
+
"sideEffects": false,
|
|
52
|
+
"scripts": {
|
|
53
|
+
"dev": "vite",
|
|
54
|
+
"build": "vue-tsc -b && vite build",
|
|
55
|
+
"storybook": "storybook dev -p 6006",
|
|
56
|
+
"build-storybook": "storybook build"
|
|
57
|
+
}
|
|
58
|
+
}
|