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 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
+ ![Vue 3](https://img.shields.io/badge/Vue.js-3.5%2B-brightgreen?logo=vuedotjs)
10
+ ![TypeScript](https://img.shields.io/badge/TypeScript-5.9-blue)
11
+ ![Storybook](https://img.shields.io/badge/Storybook-10.2-FF4785)
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
+ }