locator-ars-lib 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/README.md +162 -0
- package/dist/locator-ars-lib.es.js +132 -0
- package/dist/locator-ars-lib.umd.js +1 -0
- package/examples/BasicUsage.vue +132 -0
- package/package.json +34 -0
- package/src/components/Check.vue +33 -0
- package/src/composables/usePermissions.ts +57 -0
- package/src/directives/vCan.ts +55 -0
- package/src/index.ts +20 -0
- package/src/plugin.ts +23 -0
- package/src/services/permissionsService.ts +50 -0
- package/tsconfig.json +31 -0
- package/tsconfig.node.json +10 -0
- package/vite.config.ts +23 -0
package/README.md
ADDED
@@ -0,0 +1,162 @@
|
|
1
|
+
# Locator ARS Lib
|
2
|
+
|
3
|
+
Библиотека для управления проверкой прав доступа во Vue 3 приложениях.
|
4
|
+
|
5
|
+
## Установка
|
6
|
+
|
7
|
+
```bash
|
8
|
+
npm install locator-ars-lib
|
9
|
+
```
|
10
|
+
|
11
|
+
или
|
12
|
+
|
13
|
+
```bash
|
14
|
+
yarn add locator-ars-lib
|
15
|
+
```
|
16
|
+
|
17
|
+
## Настройка
|
18
|
+
|
19
|
+
Добавьте плагин в ваше Vue приложение:
|
20
|
+
|
21
|
+
```typescript
|
22
|
+
// main.ts
|
23
|
+
import { createApp } from "vue";
|
24
|
+
import App from "./App.vue";
|
25
|
+
import LocatorArsLib from "locator-ars-lib";
|
26
|
+
|
27
|
+
const app = createApp(App);
|
28
|
+
|
29
|
+
// Установка с настройками (опционально)
|
30
|
+
app.use(LocatorArsLib, {
|
31
|
+
baseUrl: "https://your-api.com", // Базовый URL для API (опционально)
|
32
|
+
endpoint: "/api/v1/dashboard/access", // Путь для проверки прав (опционально)
|
33
|
+
});
|
34
|
+
|
35
|
+
app.mount("#app");
|
36
|
+
```
|
37
|
+
|
38
|
+
## Использование
|
39
|
+
|
40
|
+
### 1. Компонент Check
|
41
|
+
|
42
|
+
Оберните компоненты, требующие проверку прав:
|
43
|
+
|
44
|
+
```vue
|
45
|
+
<template>
|
46
|
+
<Check action="edit_user">
|
47
|
+
<button>Edit User</button>
|
48
|
+
</Check>
|
49
|
+
</template>
|
50
|
+
```
|
51
|
+
|
52
|
+
С фолбэком при отсутствии прав:
|
53
|
+
|
54
|
+
```vue
|
55
|
+
<template>
|
56
|
+
<Check action="delete_item">
|
57
|
+
<button>Delete</button>
|
58
|
+
|
59
|
+
<template #fallback>
|
60
|
+
<span>You don't have permission to delete</span>
|
61
|
+
</template>
|
62
|
+
</Check>
|
63
|
+
</template>
|
64
|
+
```
|
65
|
+
|
66
|
+
С отображением загрузки:
|
67
|
+
|
68
|
+
```vue
|
69
|
+
<template>
|
70
|
+
<Check action="create_project">
|
71
|
+
<button>Create Project</button>
|
72
|
+
|
73
|
+
<template #loading>
|
74
|
+
<span>Checking permissions...</span>
|
75
|
+
</template>
|
76
|
+
|
77
|
+
<template #fallback>
|
78
|
+
<span>No permission</span>
|
79
|
+
</template>
|
80
|
+
</Check>
|
81
|
+
</template>
|
82
|
+
```
|
83
|
+
|
84
|
+
### 2. Директива v-can
|
85
|
+
|
86
|
+
Используйте директиву для простой проверки на элементах:
|
87
|
+
|
88
|
+
```vue
|
89
|
+
<template>
|
90
|
+
<button v-can="'edit_user'">Edit User</button>
|
91
|
+
|
92
|
+
<div v-can="'view_analytics'">
|
93
|
+
<!-- Аналитика будет видна только при наличии прав -->
|
94
|
+
</div>
|
95
|
+
</template>
|
96
|
+
```
|
97
|
+
|
98
|
+
### 3. Композиция usePermissions
|
99
|
+
|
100
|
+
Для программного использования в компонентах:
|
101
|
+
|
102
|
+
```vue
|
103
|
+
<script setup>
|
104
|
+
import { usePermissions } from "locator-ars-lib";
|
105
|
+
|
106
|
+
// Проверка одного права
|
107
|
+
const { can, isLoading } = usePermissions("manage_users");
|
108
|
+
|
109
|
+
// Проверка с реактивным действием
|
110
|
+
const action = ref("edit_post");
|
111
|
+
const { can: canEditPost } = usePermissions(action);
|
112
|
+
|
113
|
+
// Ручная проверка
|
114
|
+
const { check, can: canDeleteUser } = usePermissions("delete_user", {
|
115
|
+
autoCheck: false,
|
116
|
+
});
|
117
|
+
|
118
|
+
// Проверить вручную при необходимости
|
119
|
+
function handleDelete() {
|
120
|
+
check().then(() => {
|
121
|
+
if (canDeleteUser.value) {
|
122
|
+
// Выполнить удаление
|
123
|
+
}
|
124
|
+
});
|
125
|
+
}
|
126
|
+
</script>
|
127
|
+
```
|
128
|
+
|
129
|
+
## API
|
130
|
+
|
131
|
+
### Компонент Check
|
132
|
+
|
133
|
+
| Prop | Тип | По умолчанию | Описание |
|
134
|
+
| -------- | ------- | ------------ | ----------------------------------------------- |
|
135
|
+
| action | string | | Название действия для проверки |
|
136
|
+
| fallback | boolean | false | Показывать ли fallback слот при отсутствии прав |
|
137
|
+
|
138
|
+
### Директива v-can
|
139
|
+
|
140
|
+
```vue
|
141
|
+
v-can="'action_name'"
|
142
|
+
```
|
143
|
+
|
144
|
+
### usePermissions composable
|
145
|
+
|
146
|
+
```typescript
|
147
|
+
const {
|
148
|
+
can, // Computed<boolean> - есть ли права
|
149
|
+
isLoading, // Ref<boolean> - выполняется ли загрузка
|
150
|
+
error, // Ref<Error | null> - ошибка, если есть
|
151
|
+
check, // () => Promise<void> - функция для ручной проверки
|
152
|
+
} = usePermissions(action, options);
|
153
|
+
```
|
154
|
+
|
155
|
+
| Параметр | Тип | По умолчанию | Описание |
|
156
|
+
| ----------------- | --------------------- | ------------ | ------------------------------------ |
|
157
|
+
| action | string \| Ref<string> | | Название действия для проверки |
|
158
|
+
| options.autoCheck | boolean | true | Автоматически проверять при создании |
|
159
|
+
|
160
|
+
## Лицензия
|
161
|
+
|
162
|
+
MIT
|
@@ -0,0 +1,132 @@
|
|
1
|
+
var h = Object.defineProperty;
|
2
|
+
var y = (e, s, n) => s in e ? h(e, s, { enumerable: !0, configurable: !0, writable: !0, value: n }) : e[s] = n;
|
3
|
+
var o = (e, s, n) => (y(e, typeof s != "symbol" ? s + "" : s, n), n);
|
4
|
+
import { inject as v, ref as l, computed as m, watch as _, onUnmounted as g, defineComponent as k, toRefs as w, renderSlot as u } from "vue";
|
5
|
+
import P from "axios";
|
6
|
+
class S {
|
7
|
+
constructor(s) {
|
8
|
+
o(this, "axios");
|
9
|
+
o(this, "cache", /* @__PURE__ */ new Map());
|
10
|
+
o(this, "endpoint");
|
11
|
+
this.axios = P.create({
|
12
|
+
baseURL: (s == null ? void 0 : s.baseUrl) || ""
|
13
|
+
}), this.endpoint = (s == null ? void 0 : s.endpoint) || "/api/v1/dashboard/access";
|
14
|
+
}
|
15
|
+
async can(s) {
|
16
|
+
if (this.cache.has(s))
|
17
|
+
return this.cache.get(s);
|
18
|
+
try {
|
19
|
+
const a = (await this.axios.get(this.endpoint, {
|
20
|
+
params: { action: s }
|
21
|
+
})).data.allowed || !1;
|
22
|
+
return this.cache.set(s, a), a;
|
23
|
+
} catch (n) {
|
24
|
+
return console.error(`Error checking permission for ${s}:`, n), !1;
|
25
|
+
}
|
26
|
+
}
|
27
|
+
clearCache(s) {
|
28
|
+
s ? this.cache.delete(s) : this.cache.clear();
|
29
|
+
}
|
30
|
+
}
|
31
|
+
const f = Symbol("Permissions");
|
32
|
+
function C(e) {
|
33
|
+
return new S(e);
|
34
|
+
}
|
35
|
+
const $ = {
|
36
|
+
install(e, s) {
|
37
|
+
const n = C(s);
|
38
|
+
e.provide(f, n);
|
39
|
+
}
|
40
|
+
};
|
41
|
+
function d() {
|
42
|
+
const e = v(f);
|
43
|
+
if (!e)
|
44
|
+
throw new Error("Permissions plugin not installed!");
|
45
|
+
return e;
|
46
|
+
}
|
47
|
+
function D(e, s = {}) {
|
48
|
+
const n = d(), a = l(null), i = l(!1), t = l(null), p = m(() => typeof e == "string" ? e : e.value), c = async () => {
|
49
|
+
if (p.value) {
|
50
|
+
i.value = !0, t.value = null;
|
51
|
+
try {
|
52
|
+
a.value = await n.can(p.value);
|
53
|
+
} catch (r) {
|
54
|
+
t.value = r instanceof Error ? r : new Error(String(r)), a.value = !1;
|
55
|
+
} finally {
|
56
|
+
i.value = !1;
|
57
|
+
}
|
58
|
+
}
|
59
|
+
};
|
60
|
+
if (s.autoCheck !== !1 && c(), typeof e != "string") {
|
61
|
+
const r = _(e, () => {
|
62
|
+
c();
|
63
|
+
});
|
64
|
+
g(() => {
|
65
|
+
r();
|
66
|
+
});
|
67
|
+
}
|
68
|
+
return {
|
69
|
+
isAllowed: a,
|
70
|
+
isLoading: i,
|
71
|
+
error: t,
|
72
|
+
check: c,
|
73
|
+
can: m(() => a.value === !0)
|
74
|
+
};
|
75
|
+
}
|
76
|
+
const E = k({
|
77
|
+
name: "Check",
|
78
|
+
props: {
|
79
|
+
action: {
|
80
|
+
type: String,
|
81
|
+
required: !0
|
82
|
+
},
|
83
|
+
fallback: {
|
84
|
+
type: Boolean,
|
85
|
+
default: !1
|
86
|
+
}
|
87
|
+
},
|
88
|
+
setup(e) {
|
89
|
+
const { action: s } = w(e), { can: n, isLoading: a } = D(s, { autoCheck: !0 });
|
90
|
+
return {
|
91
|
+
can: n,
|
92
|
+
isLoading: a
|
93
|
+
};
|
94
|
+
}
|
95
|
+
}), L = (e, s) => {
|
96
|
+
const n = e.__vccOpts || e;
|
97
|
+
for (const [a, i] of s)
|
98
|
+
n[a] = i;
|
99
|
+
return n;
|
100
|
+
};
|
101
|
+
function b(e, s, n, a, i, t) {
|
102
|
+
return e.can ? u(e.$slots, "default", { key: 0 }) : e.isLoading ? u(e.$slots, "loading", { key: 2 }) : u(e.$slots, "fallback", { key: 1 });
|
103
|
+
}
|
104
|
+
const x = /* @__PURE__ */ L(E, [["render", b]]), U = {
|
105
|
+
async mounted(e, s) {
|
106
|
+
const n = e.style.display;
|
107
|
+
e._permission_data = {
|
108
|
+
action: s.value,
|
109
|
+
originalDisplay: n
|
110
|
+
}, await d().can(s.value) || (e.style.display = "none");
|
111
|
+
},
|
112
|
+
async updated(e, s) {
|
113
|
+
(!e._permission_data || e._permission_data.action !== s.value) && (e._permission_data = {
|
114
|
+
action: s.value,
|
115
|
+
originalDisplay: e.style.display || ""
|
116
|
+
}, await d().can(s.value) ? e.style.display = e._permission_data.originalDisplay : e.style.display = "none");
|
117
|
+
},
|
118
|
+
unmounted(e) {
|
119
|
+
e._permission_data && (e.style.display = e._permission_data.originalDisplay, delete e._permission_data);
|
120
|
+
}
|
121
|
+
}, A = {
|
122
|
+
install(e, s) {
|
123
|
+
e.use($, s), e.component("Check", x), e.directive("can", U);
|
124
|
+
}
|
125
|
+
};
|
126
|
+
export {
|
127
|
+
x as Check,
|
128
|
+
A as default,
|
129
|
+
C as setupPermissions,
|
130
|
+
D as usePermissions,
|
131
|
+
U as vCan
|
132
|
+
};
|
@@ -0,0 +1 @@
|
|
1
|
+
(function(n,i){typeof exports=="object"&&typeof module<"u"?i(exports,require("vue"),require("axios")):typeof define=="function"&&define.amd?define(["exports","vue","axios"],i):(n=typeof globalThis<"u"?globalThis:n||self,i(n.LocatorArsLib={},n.Vue,n.axios))})(this,function(n,i,t){"use strict";var $=Object.defineProperty;var L=(n,i,t)=>i in n?$(n,i,{enumerable:!0,configurable:!0,writable:!0,value:t}):n[i]=t;var u=(n,i,t)=>(L(n,typeof i!="symbol"?i+"":i,t),t);class P{constructor(s){u(this,"axios");u(this,"cache",new Map);u(this,"endpoint");this.axios=t.create({baseURL:(s==null?void 0:s.baseUrl)||""}),this.endpoint=(s==null?void 0:s.endpoint)||"/api/v1/dashboard/access"}async can(s){if(this.cache.has(s))return this.cache.get(s);try{const a=(await this.axios.get(this.endpoint,{params:{action:s}})).data.allowed||!1;return this.cache.set(s,a),a}catch(r){return console.error(`Error checking permission for ${s}:`,r),!1}}clearCache(s){s?this.cache.delete(s):this.cache.clear()}}const p=Symbol("Permissions");function m(e){return new P(e)}const S={install(e,s){const r=m(s);e.provide(p,r)}};function d(){const e=i.inject(p);if(!e)throw new Error("Permissions plugin not installed!");return e}function h(e,s={}){const r=d(),a=i.ref(null),o=i.ref(!1),l=i.ref(null),_=i.computed(()=>typeof e=="string"?e:e.value),f=async()=>{if(_.value){o.value=!0,l.value=null;try{a.value=await r.can(_.value)}catch(c){l.value=c instanceof Error?c:new Error(String(c)),a.value=!1}finally{o.value=!1}}};if(s.autoCheck!==!1&&f(),typeof e!="string"){const c=i.watch(e,()=>{f()});i.onUnmounted(()=>{c()})}return{isAllowed:a,isLoading:o,error:l,check:f,can:i.computed(()=>a.value===!0)}}const g=i.defineComponent({name:"Check",props:{action:{type:String,required:!0},fallback:{type:Boolean,default:!1}},setup(e){const{action:s}=i.toRefs(e),{can:r,isLoading:a}=h(s,{autoCheck:!0});return{can:r,isLoading:a}}}),k=(e,s)=>{const r=e.__vccOpts||e;for(const[a,o]of s)r[a]=o;return r};function w(e,s,r,a,o,l){return e.can?i.renderSlot(e.$slots,"default",{key:0}):e.isLoading?i.renderSlot(e.$slots,"loading",{key:2}):i.renderSlot(e.$slots,"fallback",{key:1})}const y=k(g,[["render",w]]),v={async mounted(e,s){const r=e.style.display;e._permission_data={action:s.value,originalDisplay:r},await d().can(s.value)||(e.style.display="none")},async updated(e,s){(!e._permission_data||e._permission_data.action!==s.value)&&(e._permission_data={action:s.value,originalDisplay:e.style.display||""},await d().can(s.value)?e.style.display=e._permission_data.originalDisplay:e.style.display="none")},unmounted(e){e._permission_data&&(e.style.display=e._permission_data.originalDisplay,delete e._permission_data)}},C={install(e,s){e.use(S,s),e.component("Check",y),e.directive("can",v)}};n.Check=y,n.default=C,n.setupPermissions=m,n.usePermissions=h,n.vCan=v,Object.defineProperties(n,{__esModule:{value:!0},[Symbol.toStringTag]:{value:"Module"}})});
|
@@ -0,0 +1,132 @@
|
|
1
|
+
<template>
|
2
|
+
<div class="example-container">
|
3
|
+
<h1>Permissions Examples</h1>
|
4
|
+
|
5
|
+
<!-- Пример использования компонента Check -->
|
6
|
+
<div class="example-block">
|
7
|
+
<h2>Using Check Component</h2>
|
8
|
+
|
9
|
+
<Check action="view_dashboard">
|
10
|
+
<div class="dashboard-panel">
|
11
|
+
<h3>Dashboard Content</h3>
|
12
|
+
<p>This content is only visible for users with 'view_dashboard' permission</p>
|
13
|
+
</div>
|
14
|
+
|
15
|
+
<template #fallback>
|
16
|
+
<div class="no-permission">
|
17
|
+
<p>You don't have permission to view the dashboard</p>
|
18
|
+
</div>
|
19
|
+
</template>
|
20
|
+
|
21
|
+
<template #loading>
|
22
|
+
<div class="loading">
|
23
|
+
<p>Loading permissions...</p>
|
24
|
+
</div>
|
25
|
+
</template>
|
26
|
+
</Check>
|
27
|
+
</div>
|
28
|
+
|
29
|
+
<!-- Пример использования директивы v-can -->
|
30
|
+
<div class="example-block">
|
31
|
+
<h2>Using v-can Directive</h2>
|
32
|
+
|
33
|
+
<button v-can="'edit_user'" class="action-button">
|
34
|
+
Edit User
|
35
|
+
</button>
|
36
|
+
|
37
|
+
<button v-can="'delete_user'" class="action-button danger">
|
38
|
+
Delete User
|
39
|
+
</button>
|
40
|
+
</div>
|
41
|
+
|
42
|
+
<!-- Пример использования composable usePermissions -->
|
43
|
+
<div class="example-block">
|
44
|
+
<h2>Using usePermissions Composable</h2>
|
45
|
+
|
46
|
+
<div class="permission-status">
|
47
|
+
<p>Can create project: <span :class="canCreateProject ? 'allowed' : 'denied'">{{ canCreateProject ? 'Yes' : 'No' }}</span></p>
|
48
|
+
<p>Is loading: {{ isLoadingPermission ? 'Yes' : 'No' }}</p>
|
49
|
+
<button @click="checkPermissionManually" class="action-button">
|
50
|
+
Check Again
|
51
|
+
</button>
|
52
|
+
</div>
|
53
|
+
|
54
|
+
<div v-if="canCreateProject" class="action-panel">
|
55
|
+
<button class="action-button primary">Create New Project</button>
|
56
|
+
</div>
|
57
|
+
</div>
|
58
|
+
</div>
|
59
|
+
</template>
|
60
|
+
|
61
|
+
<script setup lang="ts">
|
62
|
+
import { Check, usePermissions } from 'locator-ars-lib'
|
63
|
+
|
64
|
+
// Использование composable
|
65
|
+
const { can: canCreateProject, isLoading: isLoadingPermission, check: checkPermissionManually } = usePermissions('create_project')
|
66
|
+
</script>
|
67
|
+
|
68
|
+
<style scoped>
|
69
|
+
.example-container {
|
70
|
+
max-width: 800px;
|
71
|
+
margin: 0 auto;
|
72
|
+
padding: 20px;
|
73
|
+
font-family: Arial, sans-serif;
|
74
|
+
}
|
75
|
+
|
76
|
+
.example-block {
|
77
|
+
margin-bottom: 30px;
|
78
|
+
padding: 20px;
|
79
|
+
border: 1px solid #eee;
|
80
|
+
border-radius: 5px;
|
81
|
+
}
|
82
|
+
|
83
|
+
.dashboard-panel {
|
84
|
+
background-color: #f5f5f5;
|
85
|
+
padding: 15px;
|
86
|
+
border-radius: 4px;
|
87
|
+
}
|
88
|
+
|
89
|
+
.action-button {
|
90
|
+
padding: 8px 16px;
|
91
|
+
margin-right: 10px;
|
92
|
+
border: none;
|
93
|
+
border-radius: 4px;
|
94
|
+
background-color: #4a90e2;
|
95
|
+
color: white;
|
96
|
+
cursor: pointer;
|
97
|
+
}
|
98
|
+
|
99
|
+
.action-button.danger {
|
100
|
+
background-color: #e25c4a;
|
101
|
+
}
|
102
|
+
|
103
|
+
.action-button.primary {
|
104
|
+
background-color: #42b983;
|
105
|
+
}
|
106
|
+
|
107
|
+
.permission-status {
|
108
|
+
margin-bottom: 15px;
|
109
|
+
}
|
110
|
+
|
111
|
+
.allowed {
|
112
|
+
color: #42b983;
|
113
|
+
font-weight: bold;
|
114
|
+
}
|
115
|
+
|
116
|
+
.denied {
|
117
|
+
color: #e25c4a;
|
118
|
+
font-weight: bold;
|
119
|
+
}
|
120
|
+
|
121
|
+
.no-permission {
|
122
|
+
padding: 15px;
|
123
|
+
background-color: #ffeeee;
|
124
|
+
border-left: 4px solid #e25c4a;
|
125
|
+
}
|
126
|
+
|
127
|
+
.loading {
|
128
|
+
padding: 15px;
|
129
|
+
background-color: #eeeeff;
|
130
|
+
border-left: 4px solid #4a90e2;
|
131
|
+
}
|
132
|
+
</style>
|
package/package.json
ADDED
@@ -0,0 +1,34 @@
|
|
1
|
+
{
|
2
|
+
"name": "locator-ars-lib",
|
3
|
+
"version": "1.0.0",
|
4
|
+
"description": "Permissions library for Vue 3 applications",
|
5
|
+
"main": "dist/index.js",
|
6
|
+
"types": "dist/index.d.ts",
|
7
|
+
"scripts": {
|
8
|
+
"build": "vue-tsc && vite build",
|
9
|
+
"test": "vitest run",
|
10
|
+
"lint": "eslint src --ext .ts,.vue"
|
11
|
+
},
|
12
|
+
"keywords": [
|
13
|
+
"vue",
|
14
|
+
"permissions",
|
15
|
+
"access control",
|
16
|
+
"vue3"
|
17
|
+
],
|
18
|
+
"author": "badkiko",
|
19
|
+
"license": "MIT",
|
20
|
+
"peerDependencies": {
|
21
|
+
"axios": "^0.27.0",
|
22
|
+
"vue": "^3.0.0"
|
23
|
+
},
|
24
|
+
"devDependencies": {
|
25
|
+
"@types/node": "^18.19.100",
|
26
|
+
"@vitejs/plugin-vue": "^4.0.0",
|
27
|
+
"@vue/runtime-core": "^3.5.13",
|
28
|
+
"axios": "^0.27.2",
|
29
|
+
"typescript": "^4.9.3",
|
30
|
+
"vite": "^4.0.0",
|
31
|
+
"vue": "^3.2.45",
|
32
|
+
"vue-tsc": "^1.0.9"
|
33
|
+
}
|
34
|
+
}
|
@@ -0,0 +1,33 @@
|
|
1
|
+
<template>
|
2
|
+
<slot v-if="can" />
|
3
|
+
<slot name="fallback" v-else-if="!isLoading" />
|
4
|
+
<slot name="loading" v-else />
|
5
|
+
</template>
|
6
|
+
|
7
|
+
<script lang="ts">
|
8
|
+
import { defineComponent, toRefs, PropType } from 'vue';
|
9
|
+
import { usePermissions } from '../composables/usePermissions';
|
10
|
+
|
11
|
+
export default defineComponent({
|
12
|
+
name: 'Check',
|
13
|
+
props: {
|
14
|
+
action: {
|
15
|
+
type: String as PropType<string>,
|
16
|
+
required: true
|
17
|
+
},
|
18
|
+
fallback: {
|
19
|
+
type: Boolean,
|
20
|
+
default: false
|
21
|
+
}
|
22
|
+
},
|
23
|
+
setup(props) {
|
24
|
+
const { action } = toRefs(props);
|
25
|
+
const { can, isLoading } = usePermissions(action, { autoCheck: true });
|
26
|
+
|
27
|
+
return {
|
28
|
+
can,
|
29
|
+
isLoading
|
30
|
+
};
|
31
|
+
}
|
32
|
+
});
|
33
|
+
</script>
|
@@ -0,0 +1,57 @@
|
|
1
|
+
import { ref, computed, Ref, watch, onUnmounted } from 'vue'
|
2
|
+
import { usePermissionsService } from '../plugin'
|
3
|
+
|
4
|
+
export interface UsePermissionsOptions {
|
5
|
+
autoCheck?: boolean
|
6
|
+
}
|
7
|
+
|
8
|
+
export function usePermissions(action: string | Ref<string>, options: UsePermissionsOptions = {}) {
|
9
|
+
const permissionsService = usePermissionsService()
|
10
|
+
const isAllowed = ref<boolean | null>(null)
|
11
|
+
const isLoading = ref(false)
|
12
|
+
const error = ref<Error | null>(null)
|
13
|
+
|
14
|
+
const actionValue = computed(() => {
|
15
|
+
return typeof action === 'string' ? action : action.value
|
16
|
+
})
|
17
|
+
|
18
|
+
const checkPermission = async () => {
|
19
|
+
if (!actionValue.value) return
|
20
|
+
|
21
|
+
isLoading.value = true
|
22
|
+
error.value = null
|
23
|
+
|
24
|
+
try {
|
25
|
+
isAllowed.value = await permissionsService.can(actionValue.value)
|
26
|
+
} catch (err) {
|
27
|
+
error.value = err instanceof Error ? err : new Error(String(err))
|
28
|
+
isAllowed.value = false
|
29
|
+
} finally {
|
30
|
+
isLoading.value = false
|
31
|
+
}
|
32
|
+
}
|
33
|
+
|
34
|
+
// Auto-check on mount if requested
|
35
|
+
if (options.autoCheck !== false) {
|
36
|
+
checkPermission()
|
37
|
+
}
|
38
|
+
|
39
|
+
// Re-check when action changes
|
40
|
+
if (typeof action !== 'string') {
|
41
|
+
const unwatch = watch(action, () => {
|
42
|
+
checkPermission()
|
43
|
+
})
|
44
|
+
|
45
|
+
onUnmounted(() => {
|
46
|
+
unwatch()
|
47
|
+
})
|
48
|
+
}
|
49
|
+
|
50
|
+
return {
|
51
|
+
isAllowed,
|
52
|
+
isLoading,
|
53
|
+
error,
|
54
|
+
check: checkPermission,
|
55
|
+
can: computed(() => isAllowed.value === true)
|
56
|
+
}
|
57
|
+
}
|
@@ -0,0 +1,55 @@
|
|
1
|
+
import { ObjectDirective, DirectiveBinding } from 'vue'
|
2
|
+
import { usePermissionsService } from '../plugin'
|
3
|
+
|
4
|
+
interface CanHTMLElement extends HTMLElement {
|
5
|
+
_permission_data?: {
|
6
|
+
action: string
|
7
|
+
originalDisplay: string
|
8
|
+
}
|
9
|
+
}
|
10
|
+
|
11
|
+
export const vCan: ObjectDirective = {
|
12
|
+
async mounted(el: CanHTMLElement, binding: DirectiveBinding) {
|
13
|
+
// Store original display value
|
14
|
+
const originalDisplay = el.style.display
|
15
|
+
|
16
|
+
// Store action and original display for updates
|
17
|
+
el._permission_data = {
|
18
|
+
action: binding.value,
|
19
|
+
originalDisplay
|
20
|
+
}
|
21
|
+
|
22
|
+
const permissionsService = usePermissionsService()
|
23
|
+
const hasPermission = await permissionsService.can(binding.value)
|
24
|
+
|
25
|
+
if (!hasPermission) {
|
26
|
+
el.style.display = 'none'
|
27
|
+
}
|
28
|
+
},
|
29
|
+
|
30
|
+
async updated(el: CanHTMLElement, binding: DirectiveBinding) {
|
31
|
+
if (!el._permission_data || el._permission_data.action !== binding.value) {
|
32
|
+
// Action has changed or directive is new
|
33
|
+
el._permission_data = {
|
34
|
+
action: binding.value,
|
35
|
+
originalDisplay: el.style.display || ''
|
36
|
+
}
|
37
|
+
|
38
|
+
const permissionsService = usePermissionsService()
|
39
|
+
const hasPermission = await permissionsService.can(binding.value)
|
40
|
+
|
41
|
+
if (!hasPermission) {
|
42
|
+
el.style.display = 'none'
|
43
|
+
} else {
|
44
|
+
el.style.display = el._permission_data.originalDisplay
|
45
|
+
}
|
46
|
+
}
|
47
|
+
},
|
48
|
+
|
49
|
+
unmounted(el: CanHTMLElement) {
|
50
|
+
if (el._permission_data) {
|
51
|
+
el.style.display = el._permission_data.originalDisplay
|
52
|
+
delete el._permission_data
|
53
|
+
}
|
54
|
+
}
|
55
|
+
}
|
package/src/index.ts
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
import { App } from 'vue'
|
2
|
+
import Check from './components/Check.vue'
|
3
|
+
import { usePermissions } from './composables/usePermissions'
|
4
|
+
import { PermissionsPlugin, setupPermissions } from './plugin'
|
5
|
+
import { vCan } from './directives/vCan'
|
6
|
+
|
7
|
+
export {
|
8
|
+
Check,
|
9
|
+
usePermissions,
|
10
|
+
setupPermissions,
|
11
|
+
vCan
|
12
|
+
}
|
13
|
+
|
14
|
+
export default {
|
15
|
+
install(app: App, options?: { baseUrl?: string }) {
|
16
|
+
app.use(PermissionsPlugin, options)
|
17
|
+
app.component('Check', Check)
|
18
|
+
app.directive('can', vCan)
|
19
|
+
}
|
20
|
+
}
|
package/src/plugin.ts
ADDED
@@ -0,0 +1,23 @@
|
|
1
|
+
import { App, inject, InjectionKey } from 'vue'
|
2
|
+
import { PermissionsService, PermissionsOptions } from './services/permissionsService'
|
3
|
+
|
4
|
+
export const PermissionsKey: InjectionKey<PermissionsService> = Symbol('Permissions')
|
5
|
+
|
6
|
+
export function setupPermissions(options?: PermissionsOptions): PermissionsService {
|
7
|
+
return new PermissionsService(options)
|
8
|
+
}
|
9
|
+
|
10
|
+
export const PermissionsPlugin = {
|
11
|
+
install(app: App, options?: PermissionsOptions) {
|
12
|
+
const permissionsService = setupPermissions(options)
|
13
|
+
app.provide(PermissionsKey, permissionsService)
|
14
|
+
}
|
15
|
+
}
|
16
|
+
|
17
|
+
export function usePermissionsService(): PermissionsService {
|
18
|
+
const permissionsService = inject(PermissionsKey)
|
19
|
+
if (!permissionsService) {
|
20
|
+
throw new Error('Permissions plugin not installed!')
|
21
|
+
}
|
22
|
+
return permissionsService
|
23
|
+
}
|
@@ -0,0 +1,50 @@
|
|
1
|
+
import axios, { AxiosInstance } from 'axios'
|
2
|
+
|
3
|
+
export interface PermissionsOptions {
|
4
|
+
baseUrl?: string
|
5
|
+
endpoint?: string
|
6
|
+
}
|
7
|
+
|
8
|
+
export class PermissionsService {
|
9
|
+
private axios: AxiosInstance
|
10
|
+
private cache: Map<string, boolean> = new Map()
|
11
|
+
private endpoint: string
|
12
|
+
|
13
|
+
constructor(options?: PermissionsOptions) {
|
14
|
+
this.axios = axios.create({
|
15
|
+
baseURL: options?.baseUrl || ''
|
16
|
+
})
|
17
|
+
this.endpoint = options?.endpoint || '/api/v1/dashboard/access'
|
18
|
+
}
|
19
|
+
|
20
|
+
async can(action: string): Promise<boolean> {
|
21
|
+
// Check if we have a cached result
|
22
|
+
if (this.cache.has(action)) {
|
23
|
+
return this.cache.get(action) as boolean
|
24
|
+
}
|
25
|
+
|
26
|
+
try {
|
27
|
+
const response = await this.axios.get(this.endpoint, {
|
28
|
+
params: { action }
|
29
|
+
})
|
30
|
+
|
31
|
+
const allowed = response.data.allowed || false
|
32
|
+
|
33
|
+
// Cache the result
|
34
|
+
this.cache.set(action, allowed)
|
35
|
+
|
36
|
+
return allowed
|
37
|
+
} catch (error) {
|
38
|
+
console.error(`Error checking permission for ${action}:`, error)
|
39
|
+
return false
|
40
|
+
}
|
41
|
+
}
|
42
|
+
|
43
|
+
clearCache(action?: string): void {
|
44
|
+
if (action) {
|
45
|
+
this.cache.delete(action)
|
46
|
+
} else {
|
47
|
+
this.cache.clear()
|
48
|
+
}
|
49
|
+
}
|
50
|
+
}
|
package/tsconfig.json
ADDED
@@ -0,0 +1,31 @@
|
|
1
|
+
{
|
2
|
+
"compilerOptions": {
|
3
|
+
"target": "ES2020",
|
4
|
+
"useDefineForClassFields": true,
|
5
|
+
"module": "ESNext",
|
6
|
+
"lib": ["ES2020", "DOM", "DOM.Iterable"],
|
7
|
+
"skipLibCheck": true,
|
8
|
+
|
9
|
+
/* Bundler mode */
|
10
|
+
"moduleResolution": "node",
|
11
|
+
"allowImportingTsExtensions": true,
|
12
|
+
"resolveJsonModule": true,
|
13
|
+
"isolatedModules": true,
|
14
|
+
"noEmit": true,
|
15
|
+
"jsx": "preserve",
|
16
|
+
"esModuleInterop": true,
|
17
|
+
|
18
|
+
/* Linting */
|
19
|
+
"strict": true,
|
20
|
+
"noUnusedLocals": true,
|
21
|
+
"noUnusedParameters": true,
|
22
|
+
"noFallthroughCasesInSwitch": true,
|
23
|
+
|
24
|
+
/* Type declarations */
|
25
|
+
"declaration": true,
|
26
|
+
"declarationDir": "dist",
|
27
|
+
"outDir": "dist"
|
28
|
+
},
|
29
|
+
"include": ["src/**/*.ts", "src/**/*.d.ts", "src/**/*.tsx", "src/**/*.vue"],
|
30
|
+
"references": [{ "path": "./tsconfig.node.json" }]
|
31
|
+
}
|
package/vite.config.ts
ADDED
@@ -0,0 +1,23 @@
|
|
1
|
+
import { defineConfig } from 'vite'
|
2
|
+
import vue from '@vitejs/plugin-vue'
|
3
|
+
import { resolve } from 'path'
|
4
|
+
|
5
|
+
export default defineConfig({
|
6
|
+
plugins: [vue()],
|
7
|
+
build: {
|
8
|
+
lib: {
|
9
|
+
entry: resolve(__dirname, 'src/index.ts'),
|
10
|
+
name: 'LocatorArsLib',
|
11
|
+
fileName: (format) => `locator-ars-lib.${format}.js`
|
12
|
+
},
|
13
|
+
rollupOptions: {
|
14
|
+
external: ['vue', 'axios'],
|
15
|
+
output: {
|
16
|
+
globals: {
|
17
|
+
vue: 'Vue',
|
18
|
+
axios: 'axios'
|
19
|
+
}
|
20
|
+
}
|
21
|
+
}
|
22
|
+
}
|
23
|
+
})
|