Skip to content

Commit 1e3bde7

Browse files
committed
feat:添加访问控制功能,包括角色权限设置和路由访问控制。新增组件和指令以支持基于权限的内容显示。
1 parent a71580e commit 1e3bde7

8 files changed

Lines changed: 385 additions & 0 deletions

File tree

playground/src/app.ts

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
/**
2+
*
3+
* @authors liwb (you@example.org)
4+
* @date 2024/8/21 13:35
5+
* @version $ IIFE
6+
*/
7+
8+
import { useAccess, access as accessApi } from 'winjs';
9+
10+
// 设置默认角色权限
11+
accessApi.setRole('admin');
12+
13+
console.log('access getRole', accessApi.getRole())
14+
// console.log('access hasAccess', access.hasAccess('/'))
15+
16+
export const access = {
17+
noFoundHandler({ next }) {
18+
const accessIds = accessApi.getAccess();
19+
if (!accessIds.includes('/404')) {
20+
accessApi.setAccess(accessIds.concat(['/404']));
21+
}
22+
next('/404');
23+
},
24+
};
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
<template>
2+
<div v-access="accessId">只有 Admin 可见</div>
3+
</template>
4+
5+
<script setup lang="ts">
6+
const accessId = '/admin';
7+
</script>
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
<template>
2+
<div v-access="accessId">只有 Normal 可见</div>
3+
</template>
4+
5+
<script setup lang="ts">
6+
const accessId = '/normal';
7+
</script>

templates/core.tpl

Lines changed: 224 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,224 @@
1+
import { computed, reactive, unref } from 'vue';
2+
import type { Ref } from 'vue';
3+
import createComponent from './createComponent';
4+
import createDirective from './createDirective';
5+
6+
export interface Access {
7+
hasAccess: (accessId: string | number) => Promise<boolean>;
8+
hasAccessSync: (accessId: string | number) => boolean;
9+
isDataReady: () => boolean;
10+
setRole: (roleId: string | Promise<string>) => void;
11+
getRole: () => string;
12+
setAccess: (accessIds: Array<string | number> | Promise<Array<string | number>>) => void;
13+
getAccess: () => string[];
14+
match: (path: string, accessIds: string[]) => boolean;
15+
setPresetAccess: (accessId: string | string[]) => void;
16+
}
17+
18+
/**
19+
* Checks if a given value is a plain object.
20+
*
21+
* @param {object} object - The value to check.
22+
* @returns {boolean} - True if the value is a plain object, otherwise false.
23+
*
24+
* @example
25+
* console.log(isPlainObject({})); // true
26+
* console.log(isPlainObject([])); // false
27+
* console.log(isPlainObject(null)); // false
28+
* console.log(isPlainObject(Object.create(null))); // true
29+
* console.log(Buffer.from('hello, world')); // false
30+
*/
31+
function isPlainObject(object: object): boolean {
32+
if (typeof object !== 'object') {
33+
return false;
34+
}
35+
36+
if (object == null) {
37+
return false;
38+
}
39+
40+
if (Object.getPrototypeOf(object) === null) {
41+
return true;
42+
}
43+
44+
if (object.toString() !== '[object Object]') {
45+
return false;
46+
}
47+
48+
let proto = object;
49+
50+
while (Object.getPrototypeOf(proto) !== null) {
51+
proto = Object.getPrototypeOf(proto);
52+
}
53+
54+
return Object.getPrototypeOf(object) === proto;
55+
}
56+
57+
function isPromise(obj) {
58+
return (
59+
!!obj &&
60+
(typeof obj === 'object' || typeof obj === 'function') &&
61+
typeof obj.then === 'function'
62+
);
63+
}
64+
65+
const state = reactive({
66+
roles: {{{ roles }}},
67+
currentRoleId: "",
68+
currentAccessIds: []
69+
});
70+
const rolePromiseList: Promise<any>[] = [];
71+
const accessPromiseList: Promise<any>[] = [];
72+
73+
// 预设的 accessId
74+
const presetAccessIds = [];
75+
const setPresetAccess = (access) => {
76+
const accessIds = Array.isArray(access) ? access : [access];
77+
78+
presetAccessIds.push(...accessIds.filter(id => !presetAccessIds.includes(id)));
79+
};
80+
81+
const getAllowAccessIds = () => {
82+
const result = [...presetAccessIds, ...state.currentAccessIds];
83+
84+
const roleAccessIds = state.roles[state.currentRoleId];
85+
if (Array.isArray(roleAccessIds) && roleAccessIds.length > 0) {
86+
result.push(...roleAccessIds);
87+
}
88+
89+
return result;
90+
};
91+
92+
const _syncSetAccessIds = (promise) => {
93+
accessPromiseList.push(promise);
94+
promise
95+
.then((accessIds) => {
96+
setAccess(accessIds);
97+
})
98+
.catch((e) => {
99+
console.error(e);
100+
})
101+
.then(() => {
102+
const index = accessPromiseList.indexOf(promise);
103+
if (index !== -1) {
104+
accessPromiseList.splice(index, 1);
105+
}
106+
});
107+
};
108+
109+
const setAccess = (accessIds) => {
110+
if (isPromise(accessIds)) {
111+
return _syncSetAccessIds(accessIds);
112+
}
113+
if (isPlainObject(accessIds)) {
114+
if (accessIds.accessIds) {
115+
setAccess(accessIds.accessIds);
116+
}
117+
if (accessIds.roleId) {
118+
setRole(accessIds.roleId);
119+
}
120+
return;
121+
}
122+
if (!Array.isArray(accessIds)) {
123+
throw new Error('[plugin-access]: argument to the setAccess() must be array or promise or object');
124+
}
125+
state.currentAccessIds = accessIds;
126+
};
127+
128+
const _syncSetRoleId = (promise) => {
129+
rolePromiseList.push(promise);
130+
promise
131+
.then((roleId) => {
132+
setRole(roleId);
133+
})
134+
.catch((e) => {
135+
console.error(e);
136+
})
137+
.then(() => {
138+
const index = rolePromiseList.indexOf(promise);
139+
if (index !== -1) {
140+
rolePromiseList.splice(index, 1);
141+
}
142+
});
143+
};
144+
145+
const setRole = async (roleId) => {
146+
if (isPromise(roleId)) {
147+
return _syncSetRoleId(roleId);
148+
}
149+
if (typeof roleId !== 'string') {
150+
throw new Error('[plugin-access]: argument to the setRole() must be string or promise');
151+
}
152+
state.currentRoleId = roleId;
153+
};
154+
155+
const match = (path, accessIds) => {
156+
if (path === null || path === undefined) {
157+
return false;
158+
}
159+
if (!Array.isArray(accessIds) || accessIds.length === 0) {
160+
return false;
161+
}
162+
path = path.split('?')[0];
163+
// 进入"/"路由时,此时path为“”
164+
if (path === '') {
165+
path = '/';
166+
}
167+
const len = accessIds.length;
168+
for (let i = 0; i < len; i++) {
169+
if (path === accessIds[i]) {
170+
return true;
171+
}
172+
// 支持*匹配
173+
const reg = new RegExp(`^${accessIds[i].replace('*', '.+')}$`);
174+
if (reg.test(path)) {
175+
return true;
176+
}
177+
}
178+
return false;
179+
};
180+
181+
const isDataReady = () => {
182+
return rolePromiseList.length || accessPromiseList.length;
183+
};
184+
185+
const hasAccess = async (path) => {
186+
if (!isDataReady()) {
187+
return match(path, getAllowAccessIds());
188+
}
189+
await Promise.all(rolePromiseList.concat(accessPromiseList));
190+
return match(path, getAllowAccessIds());
191+
};
192+
193+
export const install = (app) => {
194+
app.directive('access', createDirective(useAccess));
195+
app.component('Access', createComponent(useAccess));
196+
};
197+
198+
export const hasAccessSync = (path) => {
199+
return match(unref(path), getAllowAccessIds());
200+
};
201+
202+
export const access: Access = {
203+
hasAccess,
204+
hasAccessSync,
205+
isDataReady,
206+
setRole,
207+
getRole: () => {
208+
return state.currentRoleId;
209+
},
210+
setAccess,
211+
match,
212+
getAccess: getAllowAccessIds,
213+
setPresetAccess
214+
};
215+
216+
type UseAccessFunction = (accessId: string | number) => Ref<boolean>;
217+
218+
export const useAccess: UseAccessFunction = (path) => {
219+
const allowPageIds = computed(getAllowAccessIds);
220+
const result = computed(() => {
221+
return match(unref(path), allowPageIds.value);
222+
});
223+
return result;
224+
};

templates/createComponent.tpl

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
export default function createComponent(useAccess) {
2+
return (props, { slots }) => {
3+
const access = useAccess(props.id);
4+
if (!access.value || !slots.default) return null;
5+
return slots.default();
6+
};
7+
}

templates/createDirective.tpl

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
import { watch } from 'vue';
2+
3+
const cache = new WeakMap();
4+
const setDisplay = (el, access) => {
5+
if (access.value) {
6+
el.style.display = el._display;
7+
} else {
8+
el.style.display = 'none';
9+
}
10+
};
11+
export default function createDirective(useAccess) {
12+
return {
13+
beforeMount(el) {
14+
const ctx = {};
15+
ctx.watch = (path) => {
16+
el._display = el._display || el.style.display;
17+
const access = useAccess(path);
18+
setDisplay(el, access);
19+
return watch(access, () => {
20+
setDisplay(el, access);
21+
});
22+
};
23+
cache.set(el, ctx);
24+
},
25+
mounted(el, binding) {
26+
const ctx = cache.get(el);
27+
if (ctx.unwatch) {
28+
ctx.unwatch();
29+
}
30+
ctx.unwatch = ctx.watch(binding.value);
31+
},
32+
updated(el, binding) {
33+
const ctx = cache.get(el);
34+
if (ctx.unwatch) {
35+
ctx.unwatch();
36+
}
37+
ctx.unwatch = ctx.watch(binding.value);
38+
},
39+
beforeUnmount(el) {
40+
const ctx = cache.get(el);
41+
if (ctx.unwatch) {
42+
ctx.unwatch();
43+
}
44+
}
45+
};
46+
}

templates/runtime.tpl

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
import { ApplyPluginsType } from '@@/exports';
2+
import { getPluginManager } from '../core/plugin';
3+
import { access, install } from './index';
4+
5+
export function onRouterCreated({ router }) {
6+
router.beforeEach(async (to, from, next) => {
7+
const runtimeConfig = getPluginManager().applyPlugins({
8+
key: 'access',
9+
type: ApplyPluginsType.modify,
10+
initialValue: {}
11+
});
12+
console.log('onRouterCreated runtimeConfig', runtimeConfig);
13+
console.log('onRouterCreated runtimeConfig', to.matched);
14+
if (to.matched.length === 0) {
15+
if (runtimeConfig.noFoundHandler && typeof runtimeConfig.noFoundHandler === 'function') {
16+
return runtimeConfig.noFoundHandler({
17+
router,
18+
to,
19+
from,
20+
next
21+
});
22+
}
23+
return next(false);
24+
}
25+
if (Array.isArray(runtimeConfig.ignoreAccess)) {
26+
const isIgnored = await access.match(to.matched[to.matched.length - 1].path, runtimeConfig.ignoreAccess);
27+
if (isIgnored) {
28+
return next();
29+
}
30+
}
31+
// path是匹配路由的path,不是页面hash
32+
const canRoute = await access.hasAccess(to.matched[to.matched.length - 1].path);
33+
if (canRoute) {
34+
return next();
35+
}
36+
if (runtimeConfig.unAccessHandler && typeof runtimeConfig.unAccessHandler === 'function') {
37+
return runtimeConfig.unAccessHandler({
38+
router,
39+
to,
40+
from,
41+
next
42+
});
43+
}
44+
next(false);
45+
});
46+
}
47+
48+
export function onAppCreated({ app }) {
49+
install(app);
50+
}

templates/types.d.ts

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
import type { NavigationGuard, NavigationGuardNext, RouteLocationNormalized, Router } from 'vue-router';
2+
3+
interface CustomNavigationGuardOption {
4+
router: Router;
5+
to: RouteLocationNormalized;
6+
from: RouteLocationNormalized;
7+
next: NavigationGuardNext;
8+
}
9+
10+
interface CustomNavigationGuard {
11+
(option: CustomNavigationGuardOption): ReturnType<NavigationGuard>;
12+
}
13+
14+
export interface AccessPluginRuntimeConfig {
15+
access?: {
16+
noFoundHandler?: CustomNavigationGuard;
17+
unAccessHandler?: CustomNavigationGuard;
18+
ignoreAccess?: string[];
19+
};
20+
}

0 commit comments

Comments
 (0)