feat: refine ui permissions (#628)

#### What type of PR is this?

/kind feature
/kind improvement
/milestone 2.0

#### What this PR does / why we need it:

完善 UI 权限控制。适配 https://github.com/halo-dev/halo/pull/2488

#### Which issue(s) this PR fixes:

Fixes https://github.com/halo-dev/halo/issues/2342

#### Special notes for your reviewer:

/cc @halo-dev/sig-halo-console 

测试方式:

1. Halo 需要使用 https://github.com/halo-dev/halo/issues/2342 PR 的分支。
2. 创建新的角色,并勾选需要测试的权限。
3. 创建新的用户并赋予新的角色。
4. 测试操作权限。


#### Does this PR introduce a user-facing change?

```release-note
完善 UI 权限控制
```
pull/633/head
Ryan Wang 2022-09-30 17:48:19 +08:00 committed by GitHub
parent a6e913abc5
commit 3ae432ac75
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
30 changed files with 278 additions and 58 deletions

View File

@ -173,9 +173,10 @@ async function loadCurrentUser() {
app.directive( app.directive(
"permission", "permission",
(el: HTMLElement, binding: DirectiveBinding<string[]>) => { (el: HTMLElement, binding: DirectiveBinding<string[]>) => {
const uiPermissions = Array.from<string>( // const uiPermissions = Array.from<string>(
currentPermissions.uiPermissions // currentPermissions.uiPermissions
); // );
const uiPermissions = Array.from<string>(["system:attachments:view"]);
const { value } = binding; const { value } = binding;
const { any, enable } = binding.modifiers; const { any, enable } = binding.modifiers;

View File

@ -38,6 +38,9 @@ import cloneDeep from "lodash.clonedeep";
import { isImage } from "@/utils/image"; import { isImage } from "@/utils/image";
import { useRouteQuery } from "@vueuse/router"; import { useRouteQuery } from "@vueuse/router";
import { useFetchAttachmentGroup } from "./composables/use-attachment-group"; import { useFetchAttachmentGroup } from "./composables/use-attachment-group";
import { usePermission } from "@/utils/permission";
const { currentUserHasPermission } = usePermission();
const policyVisible = ref(false); const policyVisible = ref(false);
const uploadVisible = ref(false); const uploadVisible = ref(false);
@ -233,13 +236,21 @@ onMounted(() => {
</template> </template>
<template #actions> <template #actions>
<VSpace> <VSpace>
<VButton size="sm" @click="policyVisible = true"> <VButton
v-permission="['system:attachments:manage']"
size="sm"
@click="policyVisible = true"
>
<template #icon> <template #icon>
<IconDatabase2Line class="h-full w-full" /> <IconDatabase2Line class="h-full w-full" />
</template> </template>
存储策略 存储策略
</VButton> </VButton>
<VButton type="secondary" @click="uploadVisible = true"> <VButton
v-permission="['system:attachments:manage']"
type="secondary"
@click="uploadVisible = true"
>
<template #icon> <template #icon>
<IconUpload class="h-full w-full" /> <IconUpload class="h-full w-full" />
</template> </template>
@ -258,7 +269,10 @@ onMounted(() => {
<div <div
class="relative flex flex-col items-start sm:flex-row sm:items-center" class="relative flex flex-col items-start sm:flex-row sm:items-center"
> >
<div class="mr-4 hidden items-center sm:flex"> <div
v-permission="['system:attachments:manage']"
class="mr-4 hidden items-center sm:flex"
>
<input <input
v-model="checkedAll" v-model="checkedAll"
class="h-4 w-4 rounded border-gray-300 text-indigo-600" class="h-4 w-4 rounded border-gray-300 text-indigo-600"
@ -489,7 +503,11 @@ onMounted(() => {
<template #actions> <template #actions>
<VSpace> <VSpace>
<VButton @click="handleFetchAttachments"></VButton> <VButton @click="handleFetchAttachments"></VButton>
<VButton type="secondary" @click="uploadVisible = true"> <VButton
v-permission="['system:attachments:manage']"
type="secondary"
@click="uploadVisible = true"
>
<template #icon> <template #icon>
<IconUpload class="h-full w-full" /> <IconUpload class="h-full w-full" />
</template> </template>
@ -565,6 +583,7 @@ onMounted(() => {
<div <div
v-if="!attachment.metadata.deletionTimestamp" v-if="!attachment.metadata.deletionTimestamp"
v-permission="['system:attachments:manage']"
:class="{ '!flex': selectedAttachments.has(attachment) }" :class="{ '!flex': selectedAttachments.has(attachment) }"
class="absolute top-0 left-0 hidden h-1/3 w-full cursor-pointer justify-end bg-gradient-to-b from-gray-300 to-transparent ease-in-out group-hover:flex" class="absolute top-0 left-0 hidden h-1/3 w-full cursor-pointer justify-end bg-gradient-to-b from-gray-300 to-transparent ease-in-out group-hover:flex"
> >
@ -588,7 +607,12 @@ onMounted(() => {
> >
<li v-for="(attachment, index) in attachments.items" :key="index"> <li v-for="(attachment, index) in attachments.items" :key="index">
<VEntity :is-selected="isChecked(attachment)"> <VEntity :is-selected="isChecked(attachment)">
<template #checkbox> <template
v-if="
!currentUserHasPermission(['system:attachments:manage'])
"
#checkbox
>
<input <input
:checked="selectedAttachments.has(attachment)" :checked="selectedAttachments.has(attachment)"
class="h-4 w-4 rounded border-gray-300 text-indigo-600" class="h-4 w-4 rounded border-gray-300 text-indigo-600"
@ -669,7 +693,12 @@ onMounted(() => {
</template> </template>
</VEntityField> </VEntityField>
</template> </template>
<template #dropdownItems> <template
v-if="
!currentUserHasPermission(['system:attachments:manage'])
"
#dropdownItems
>
<VButton <VButton
v-close-popper v-close-popper
block block

View File

@ -132,7 +132,10 @@ onMounted(async () => {
{{ group.spec.displayName }} {{ group.spec.displayName }}
</span> </span>
</div> </div>
<FloatingDropdown v-if="!readonly"> <FloatingDropdown
v-if="!readonly"
v-permission="['system:attachments:manage']"
>
<IconMore @click.stop /> <IconMore @click.stop />
<template #popper> <template #popper>
<div class="w-48 p-2"> <div class="w-48 p-2">
@ -153,6 +156,7 @@ onMounted(async () => {
</div> </div>
<div <div
v-if="!loading && !readonly" v-if="!loading && !readonly"
v-permission="['system:attachments:manage']"
class="flex cursor-pointer items-center rounded-base bg-gray-100 p-2 text-gray-500 transition-all hover:bg-gray-200 hover:text-gray-900 hover:shadow-sm" class="flex cursor-pointer items-center rounded-base bg-gray-100 p-2 text-gray-500 transition-all hover:bg-gray-200 hover:text-gray-900 hover:shadow-sm"
@click="editingModal = true" @click="editingModal = true"
> >

View File

@ -17,6 +17,7 @@ export default definePlugin({
component: AttachmentList, component: AttachmentList,
meta: { meta: {
title: "附件", title: "附件",
permissions: ["system:attachments:view"],
}, },
}, },
], ],

View File

@ -241,7 +241,10 @@ function handleSelectUser(user: User | undefined) {
<div <div
class="relative flex flex-col items-start sm:flex-row sm:items-center" class="relative flex flex-col items-start sm:flex-row sm:items-center"
> >
<div class="mr-4 hidden items-center sm:flex"> <div
v-permission="['system:comments:manage']"
class="mr-4 hidden items-center sm:flex"
>
<input <input
v-model="checkAll" v-model="checkAll"
class="h-4 w-4 rounded border-gray-300 text-indigo-600" class="h-4 w-4 rounded border-gray-300 text-indigo-600"

View File

@ -25,6 +25,9 @@ import ReplyListItem from "./ReplyListItem.vue";
import { apiClient } from "@/utils/api-client"; import { apiClient } from "@/utils/api-client";
import type { RouteLocationRaw } from "vue-router"; import type { RouteLocationRaw } from "vue-router";
import cloneDeep from "lodash.clonedeep"; import cloneDeep from "lodash.clonedeep";
import { usePermission } from "@/utils/permission";
const { currentUserHasPermission } = usePermission();
const props = withDefaults( const props = withDefaults(
defineProps<{ defineProps<{
@ -247,7 +250,10 @@ const subjectRefResult = computed(() => {
<div class="absolute inset-x-0 top-0 h-[1px] bg-black/50"></div> <div class="absolute inset-x-0 top-0 h-[1px] bg-black/50"></div>
<div class="absolute inset-x-0 bottom-0 h-[1px] bg-black/50"></div> <div class="absolute inset-x-0 bottom-0 h-[1px] bg-black/50"></div>
</template> </template>
<template #checkbox> <template
v-if="!currentUserHasPermission(['system:comments:manage'])"
#checkbox
>
<slot name="checkbox" /> <slot name="checkbox" />
</template> </template>
<template #start> <template #start>
@ -336,7 +342,10 @@ const subjectRefResult = computed(() => {
</template> </template>
</VEntityField> </VEntityField>
</template> </template>
<template #dropdownItems> <template
v-if="!currentUserHasPermission(['system:comments:manage'])"
#dropdownItems
>
<VButton <VButton
v-if="!comment?.comment.spec.approved" v-if="!comment?.comment.spec.approved"
v-close-popper v-close-popper

View File

@ -172,6 +172,7 @@ const isHoveredReply = computed(() => {
<template #dropdownItems> <template #dropdownItems>
<VButton <VButton
v-if="!reply?.reply.spec.approved" v-if="!reply?.reply.spec.approved"
v-permission="['system:comments:manage']"
v-close-popper v-close-popper
type="secondary" type="secondary"
block block
@ -179,7 +180,13 @@ const isHoveredReply = computed(() => {
> >
审核通过 审核通过
</VButton> </VButton>
<VButton v-close-popper block type="danger" @click="handleDelete"> <VButton
v-permission="['system:comments:manage']"
v-close-popper
block
type="danger"
@click="handleDelete"
>
删除 删除
</VButton> </VButton>
</template> </template>

View File

@ -17,6 +17,7 @@ export default definePlugin({
meta: { meta: {
title: "评论", title: "评论",
searchable: true, searchable: true,
permissions: ["system:comments:view"],
}, },
}, },
], ],

View File

@ -32,6 +32,9 @@ import { apiClient } from "@/utils/api-client";
import { formatDatetime } from "@/utils/date"; import { formatDatetime } from "@/utils/date";
import { RouterLink } from "vue-router"; import { RouterLink } from "vue-router";
import cloneDeep from "lodash.clonedeep"; import cloneDeep from "lodash.clonedeep";
import { usePermission } from "@/utils/permission";
const { currentUserHasPermission } = usePermission();
enum SinglePagePhase { enum SinglePagePhase {
DRAFT = "未发布", DRAFT = "未发布",
@ -327,7 +330,10 @@ function handleSortItemChange(sortItem?: SortItem) {
<div <div
class="relative flex flex-col items-start sm:flex-row sm:items-center" class="relative flex flex-col items-start sm:flex-row sm:items-center"
> >
<div class="mr-4 hidden items-center sm:flex"> <div
v-permission="['system:singlepages:manage']"
class="mr-4 hidden items-center sm:flex"
>
<input <input
v-model="checkAll" v-model="checkAll"
class="h-4 w-4 rounded border-gray-300 text-indigo-600" class="h-4 w-4 rounded border-gray-300 text-indigo-600"
@ -509,7 +515,11 @@ function handleSortItemChange(sortItem?: SortItem) {
<template #actions> <template #actions>
<VSpace> <VSpace>
<VButton @click="handleFetchSinglePages"></VButton> <VButton @click="handleFetchSinglePages"></VButton>
<VButton :route="{ name: 'SinglePageEditor' }" type="primary"> <VButton
v-permission="['system:singlepages:manage']"
:route="{ name: 'SinglePageEditor' }"
type="primary"
>
<template #icon> <template #icon>
<IconAddCircle class="h-full w-full" /> <IconAddCircle class="h-full w-full" />
</template> </template>
@ -525,7 +535,10 @@ function handleSortItemChange(sortItem?: SortItem) {
> >
<li v-for="(singlePage, index) in singlePages.items" :key="index"> <li v-for="(singlePage, index) in singlePages.items" :key="index">
<VEntity :is-selected="checkAll"> <VEntity :is-selected="checkAll">
<template #checkbox> <template
v-if="!currentUserHasPermission(['system:singlepages:manage'])"
#checkbox
>
<input <input
v-model="checkAll" v-model="checkAll"
class="h-4 w-4 rounded border-gray-300 text-indigo-600" class="h-4 w-4 rounded border-gray-300 text-indigo-600"
@ -622,7 +635,10 @@ function handleSortItemChange(sortItem?: SortItem) {
</template> </template>
</VEntityField> </VEntityField>
</template> </template>
<template #dropdownItems> <template
v-if="!currentUserHasPermission(['system:singlepages:manage'])"
#dropdownItems
>
<VButton <VButton
v-close-popper v-close-popper
block block

View File

@ -70,7 +70,11 @@ watchEffect(() => {
<IconPages class="mr-2 self-center" /> <IconPages class="mr-2 self-center" />
</template> </template>
<template #actions> <template #actions>
<VButton :route="{ name: 'SinglePageEditor' }" type="secondary"> <VButton
v-permission="['system:singlepages:manage']"
:route="{ name: 'SinglePageEditor' }"
type="secondary"
>
<template #icon> <template #icon>
<IconAddCircle class="h-full w-full" /> <IconAddCircle class="h-full w-full" />
</template> </template>

View File

@ -47,6 +47,7 @@ export default definePlugin({
meta: { meta: {
title: "自定义页面", title: "自定义页面",
searchable: true, searchable: true,
permissions: ["system:singlepages:view"],
}, },
}, },
], ],
@ -62,6 +63,7 @@ export default definePlugin({
meta: { meta: {
title: "页面编辑", title: "页面编辑",
searchable: true, searchable: true,
permissions: ["system:singlepages:manage"],
}, },
}, },
], ],

View File

@ -37,6 +37,9 @@ import { apiClient } from "@/utils/api-client";
import { formatDatetime } from "@/utils/date"; import { formatDatetime } from "@/utils/date";
import { usePostCategory } from "@/modules/contents/posts/categories/composables/use-post-category"; import { usePostCategory } from "@/modules/contents/posts/categories/composables/use-post-category";
import { usePostTag } from "@/modules/contents/posts/tags/composables/use-post-tag"; import { usePostTag } from "@/modules/contents/posts/tags/composables/use-post-tag";
import { usePermission } from "@/utils/permission";
const { currentUserHasPermission } = usePermission();
enum PostPhase { enum PostPhase {
DRAFT = "未发布", DRAFT = "未发布",
@ -398,7 +401,11 @@ function handleContributorChange(user?: User) {
<VSpace> <VSpace>
<VButton :route="{ name: 'Categories' }" size="sm">分类</VButton> <VButton :route="{ name: 'Categories' }" size="sm">分类</VButton>
<VButton :route="{ name: 'Tags' }" size="sm">标签</VButton> <VButton :route="{ name: 'Tags' }" size="sm">标签</VButton>
<VButton :route="{ name: 'PostEditor' }" type="secondary"> <VButton
v-permission="['system:posts:manage']"
:route="{ name: 'PostEditor' }"
type="secondary"
>
<template #icon> <template #icon>
<IconAddCircle class="h-full w-full" /> <IconAddCircle class="h-full w-full" />
</template> </template>
@ -415,7 +422,10 @@ function handleContributorChange(user?: User) {
<div <div
class="relative flex flex-col items-start sm:flex-row sm:items-center" class="relative flex flex-col items-start sm:flex-row sm:items-center"
> >
<div class="mr-4 hidden items-center sm:flex"> <div
v-permission="['system:posts:manage']"
class="mr-4 hidden items-center sm:flex"
>
<input <input
v-model="checkedAll" v-model="checkedAll"
class="h-4 w-4 rounded border-gray-300 text-indigo-600" class="h-4 w-4 rounded border-gray-300 text-indigo-600"
@ -769,7 +779,11 @@ function handleContributorChange(user?: User) {
<template #actions> <template #actions>
<VSpace> <VSpace>
<VButton @click="handleFetchPosts"></VButton> <VButton @click="handleFetchPosts"></VButton>
<VButton :route="{ name: 'PostEditor' }" type="primary"> <VButton
v-permission="['system:posts:manage']"
:route="{ name: 'PostEditor' }"
type="primary"
>
<template #icon> <template #icon>
<IconAddCircle class="h-full w-full" /> <IconAddCircle class="h-full w-full" />
</template> </template>
@ -785,7 +799,10 @@ function handleContributorChange(user?: User) {
> >
<li v-for="(post, index) in posts.items" :key="index"> <li v-for="(post, index) in posts.items" :key="index">
<VEntity :is-selected="checkSelection(post.post)"> <VEntity :is-selected="checkSelection(post.post)">
<template #checkbox> <template
v-if="!currentUserHasPermission(['system:posts:manage'])"
#checkbox
>
<input <input
v-model="selectedPostNames" v-model="selectedPostNames"
:value="post.post.metadata.name" :value="post.post.metadata.name"
@ -904,7 +921,10 @@ function handleContributorChange(user?: User) {
</template> </template>
</VEntityField> </VEntityField>
</template> </template>
<template #dropdownItems> <template
v-if="!currentUserHasPermission(['system:posts:manage'])"
#dropdownItems
>
<VButton <VButton
v-close-popper v-close-popper
block block

View File

@ -84,7 +84,11 @@ const onEditingModalClose = () => {
</template> </template>
<template #actions> <template #actions>
<VButton type="secondary" @click="editingModal = true"> <VButton
v-permission="['system:posts:manage']"
type="secondary"
@click="editingModal = true"
>
<template #icon> <template #icon>
<IconAddCircle class="h-full w-full" /> <IconAddCircle class="h-full w-full" />
</template> </template>
@ -115,7 +119,11 @@ const onEditingModalClose = () => {
<template #actions> <template #actions>
<VSpace> <VSpace>
<VButton @click="handleFetchCategories"></VButton> <VButton @click="handleFetchCategories"></VButton>
<VButton type="primary" @click="editingModal = true"> <VButton
v-permission="['system:posts:manage']"
type="primary"
@click="editingModal = true"
>
<template #icon> <template #icon>
<IconAddCircle class="h-full w-full" /> <IconAddCircle class="h-full w-full" />
</template> </template>

View File

@ -58,6 +58,7 @@ function onDelete(category: CategoryTree) {
<VEntity> <VEntity>
<template #prepend> <template #prepend>
<div <div
v-permission="['system:posts:manage']"
class="drag-element absolute inset-y-0 left-0 hidden w-3.5 cursor-move items-center bg-gray-100 transition-all hover:bg-gray-200 group-hover:flex" class="drag-element absolute inset-y-0 left-0 hidden w-3.5 cursor-move items-center bg-gray-100 transition-all hover:bg-gray-200 group-hover:flex"
> >
<IconList class="h-3.5 w-3.5" /> <IconList class="h-3.5 w-3.5" />
@ -88,6 +89,7 @@ function onDelete(category: CategoryTree) {
</template> </template>
<template #dropdownItems> <template #dropdownItems>
<VButton <VButton
v-permission="['system:posts:manage']"
v-close-popper v-close-popper
block block
type="secondary" type="secondary"
@ -96,6 +98,7 @@ function onDelete(category: CategoryTree) {
修改 修改
</VButton> </VButton>
<VButton <VButton
v-permission="['system:posts:manage']"
v-close-popper v-close-popper
block block
type="danger" type="danger"

View File

@ -20,6 +20,7 @@ export default definePlugin({
meta: { meta: {
title: "文章", title: "文章",
searchable: true, searchable: true,
permissions: ["system:posts:view"],
}, },
}, },
{ {
@ -29,6 +30,7 @@ export default definePlugin({
meta: { meta: {
title: "文章编辑", title: "文章编辑",
searchable: true, searchable: true,
permissions: ["system:posts:manage"],
}, },
}, },
{ {
@ -42,6 +44,7 @@ export default definePlugin({
meta: { meta: {
title: "文章分类", title: "文章分类",
searchable: true, searchable: true,
permissions: ["system:posts:view"],
}, },
}, },
], ],
@ -57,6 +60,7 @@ export default definePlugin({
meta: { meta: {
title: "文章标签", title: "文章标签",
searchable: true, searchable: true,
permissions: ["system:posts:view"],
}, },
}, },
], ],

View File

@ -115,7 +115,11 @@ onMounted(async () => {
<IconBookRead class="mr-2 self-center" /> <IconBookRead class="mr-2 self-center" />
</template> </template>
<template #actions> <template #actions>
<VButton type="secondary" @click="editingModal = true"> <VButton
v-permission="['system:posts:manage']"
type="secondary"
@click="editingModal = true"
>
<template #icon> <template #icon>
<IconAddCircle class="h-full w-full" /> <IconAddCircle class="h-full w-full" />
</template> </template>
@ -204,6 +208,7 @@ onMounted(async () => {
</template> </template>
<template #dropdownItems> <template #dropdownItems>
<VButton <VButton
v-permission="['system:posts:manage']"
v-close-popper v-close-popper
block block
type="secondary" type="secondary"
@ -212,6 +217,7 @@ onMounted(async () => {
修改 修改
</VButton> </VButton>
<VButton <VButton
v-permission="['system:posts:manage']"
v-close-popper v-close-popper
block block
type="danger" type="danger"

View File

@ -8,24 +8,35 @@ import {
IconUserSettings, IconUserSettings,
IconPalette, IconPalette,
VCard, VCard,
IconUserLine,
} from "@halo-dev/components"; } from "@halo-dev/components";
import { markRaw, type Component } from "vue"; import { inject, markRaw, type Component } from "vue";
import { useRouter } from "vue-router"; import { useRouter } from "vue-router";
import type { RouteLocationRaw } from "vue-router"; import type { RouteLocationRaw } from "vue-router";
import type { User } from "@halo-dev/api-client";
interface Action { interface Action {
icon: Component; icon: Component;
title: string; title: string;
route: RouteLocationRaw; route: RouteLocationRaw;
permissions?: string[];
} }
const currentUser = inject<User>("currentUser");
const actions: Action[] = [ const actions: Action[] = [
{
icon: markRaw(IconUserLine),
title: "个人资料",
route: { name: "UserDetail", params: { name: currentUser?.metadata.name } },
},
{ {
icon: markRaw(IconBookRead), icon: markRaw(IconBookRead),
title: "创建文章", title: "创建文章",
route: { route: {
name: "PostEditor", name: "PostEditor",
}, },
permissions: ["system:posts:manage"],
}, },
{ {
icon: markRaw(IconPages), icon: markRaw(IconPages),
@ -33,6 +44,7 @@ const actions: Action[] = [
route: { route: {
name: "SinglePageEditor", name: "SinglePageEditor",
}, },
permissions: ["system:singlepages:manage"],
}, },
{ {
icon: markRaw(IconFolder), icon: markRaw(IconFolder),
@ -43,6 +55,7 @@ const actions: Action[] = [
action: "upload", action: "upload",
}, },
}, },
permissions: ["system:attachments:manage"],
}, },
{ {
icon: markRaw(IconPalette), icon: markRaw(IconPalette),
@ -50,6 +63,7 @@ const actions: Action[] = [
route: { route: {
name: "ThemeDetail", name: "ThemeDetail",
}, },
permissions: ["system:themes:view"],
}, },
{ {
icon: markRaw(IconPlug), icon: markRaw(IconPlug),
@ -57,6 +71,7 @@ const actions: Action[] = [
route: { route: {
name: "Plugins", name: "Plugins",
}, },
permissions: ["system:plugins:view"],
}, },
{ {
icon: markRaw(IconUserSettings), icon: markRaw(IconUserSettings),
@ -67,6 +82,7 @@ const actions: Action[] = [
action: "create", action: "create",
}, },
}, },
permissions: ["system:users:manage"],
}, },
]; ];
@ -82,6 +98,7 @@ const router = useRouter();
<div <div
v-for="(action, index) in actions" v-for="(action, index) in actions"
:key="index" :key="index"
v-permission="action.permissions"
class="group relative cursor-pointer bg-white p-6 transition-all hover:bg-gray-50" class="group relative cursor-pointer bg-white p-6 transition-all hover:bg-gray-50"
@click="router.push(action.route)" @click="router.push(action.route)"
> >

View File

@ -175,6 +175,7 @@ const handleDelete = async (menuItem: MenuTreeItem) => {
<div class="mt-4 flex sm:mt-0"> <div class="mt-4 flex sm:mt-0">
<VSpace> <VSpace>
<VButton <VButton
v-permission="['system:menus:manage']"
size="xs" size="xs"
type="default" type="default"
@click="menuItemEditingModal = true" @click="menuItemEditingModal = true"
@ -194,7 +195,11 @@ const handleDelete = async (menuItem: MenuTreeItem) => {
<template #actions> <template #actions>
<VSpace> <VSpace>
<VButton @click="handleFetchMenuItems"> </VButton> <VButton @click="handleFetchMenuItems"> </VButton>
<VButton type="primary" @click="menuItemEditingModal = true"> <VButton
v-permission="['system:menus:manage']"
type="primary"
@click="menuItemEditingModal = true"
>
<template #icon> <template #icon>
<IconAddCircle class="h-full w-full" /> <IconAddCircle class="h-full w-full" />
</template> </template>

View File

@ -12,11 +12,11 @@ import { setFocus } from "@/formkit/utils/focus";
const props = withDefaults( const props = withDefaults(
defineProps<{ defineProps<{
visible: boolean; visible: boolean;
menu: Menu | null; menu?: Menu;
}>(), }>(),
{ {
visible: false, visible: false,
menu: null, menu: undefined,
} }
); );

View File

@ -10,6 +10,9 @@ import {
import Draggable from "vuedraggable"; import Draggable from "vuedraggable";
import { ref } from "vue"; import { ref } from "vue";
import type { MenuTreeItem } from "@/modules/interface/menus/utils"; import type { MenuTreeItem } from "@/modules/interface/menus/utils";
import { usePermission } from "@/utils/permission";
const { currentUserHasPermission } = usePermission();
withDefaults( withDefaults(
defineProps<{ defineProps<{
@ -74,6 +77,7 @@ function getMenuItemRefDisplayName(menuItem: MenuTreeItem) {
<VEntity> <VEntity>
<template #prepend> <template #prepend>
<div <div
v-permission="['system:menus:manage']"
class="drag-element absolute inset-y-0 left-0 hidden w-3.5 cursor-move items-center bg-gray-100 transition-all hover:bg-gray-200 group-hover:flex" class="drag-element absolute inset-y-0 left-0 hidden w-3.5 cursor-move items-center bg-gray-100 transition-all hover:bg-gray-200 group-hover:flex"
> >
<IconList class="h-3.5 w-3.5" /> <IconList class="h-3.5 w-3.5" />
@ -98,7 +102,10 @@ function getMenuItemRefDisplayName(menuItem: MenuTreeItem) {
</template> </template>
</VEntityField> </VEntityField>
</template> </template>
<template #dropdownItems> <template
v-if="!currentUserHasPermission(['system:menus:manage'])"
#dropdownItems
>
<VButton <VButton
v-close-popper v-close-popper
block block

View File

@ -14,6 +14,9 @@ import { defineExpose, onMounted, ref } from "vue";
import type { Menu } from "@halo-dev/api-client"; import type { Menu } from "@halo-dev/api-client";
import { apiClient } from "@/utils/api-client"; import { apiClient } from "@/utils/api-client";
import { useRouteQuery } from "@vueuse/router"; import { useRouteQuery } from "@vueuse/router";
import { usePermission } from "@/utils/permission";
const { currentUserHasPermission } = usePermission();
const props = withDefaults( const props = withDefaults(
defineProps<{ defineProps<{
@ -31,13 +34,13 @@ const emit = defineEmits<{
const menus = ref<Menu[]>([] as Menu[]); const menus = ref<Menu[]>([] as Menu[]);
const loading = ref(false); const loading = ref(false);
const selectedMenuToUpdate = ref<Menu | null>(null); const selectedMenuToUpdate = ref<Menu>();
const menuEditingModal = ref<boolean>(false); const menuEditingModal = ref<boolean>(false);
const dialog = useDialog(); const dialog = useDialog();
const handleFetchMenus = async () => { const handleFetchMenus = async () => {
selectedMenuToUpdate.value = null; selectedMenuToUpdate.value = undefined;
try { try {
loading.value = true; loading.value = true;
@ -95,7 +98,7 @@ const handleDeleteMenu = async (menu: Menu) => {
}); });
}; };
const handleOpenEditingModal = (menu: Menu | null) => { const handleOpenEditingModal = (menu?: Menu) => {
selectedMenuToUpdate.value = menu; selectedMenuToUpdate.value = menu;
menuEditingModal.value = true; menuEditingModal.value = true;
}; };
@ -161,7 +164,10 @@ defineExpose({
</template> </template>
</VEntityField> </VEntityField>
</template> </template>
<template #dropdownItems> <template
v-if="!currentUserHasPermission(['system:menus:manage'])"
#dropdownItems
>
<VButton <VButton
v-close-popper v-close-popper
block block
@ -182,8 +188,8 @@ defineExpose({
</VEntity> </VEntity>
</li> </li>
</ul> </ul>
<template #footer> <template v-if="!currentUserHasPermission(['system:menus:manage'])" #footer>
<VButton block type="secondary" @click="handleOpenEditingModal(null)"> <VButton block type="secondary" @click="handleOpenEditingModal()">
新增 新增
</VButton> </VButton>
</template> </template>

View File

@ -17,6 +17,7 @@ export default definePlugin({
meta: { meta: {
title: "菜单", title: "菜单",
searchable: true, searchable: true,
permissions: ["system:menus:view"],
}, },
}, },
], ],

View File

@ -57,7 +57,7 @@ const handleReloadThemeSetting = async () => {
</p> </p>
</div> </div>
</div> </div>
<FloatingDropdown> <FloatingDropdown v-permission="['system:themes:manage']">
<div <div
class="cursor-pointer rounded p-1 transition-all hover:text-blue-600 group-hover:bg-gray-100" class="cursor-pointer rounded p-1 transition-all hover:text-blue-600 group-hover:bg-gray-100"
> >

View File

@ -17,6 +17,9 @@ import ThemeInstallModal from "./ThemeInstallModal.vue";
import { ref, watch } from "vue"; import { ref, watch } from "vue";
import type { Theme } from "@halo-dev/api-client"; import type { Theme } from "@halo-dev/api-client";
import { apiClient } from "@/utils/api-client"; import { apiClient } from "@/utils/api-client";
import { usePermission } from "@/utils/permission";
const { currentUserHasPermission } = usePermission();
const props = withDefaults( const props = withDefaults(
defineProps<{ defineProps<{
@ -141,7 +144,11 @@ defineExpose({
<template #actions> <template #actions>
<VSpace> <VSpace>
<VButton @click="handleFetchThemes"> </VButton> <VButton @click="handleFetchThemes"> </VButton>
<VButton type="primary" @click="themeInstall = true"> <VButton
v-permission="['system:themes:manage']"
type="primary"
@click="themeInstall = true"
>
<template #icon> <template #icon>
<IconAddCircle class="h-full w-full" /> <IconAddCircle class="h-full w-full" />
</template> </template>
@ -238,7 +245,10 @@ defineExpose({
</template> </template>
</VEntityField> </VEntityField>
</template> </template>
<template #dropdownItems> <template
v-if="!currentUserHasPermission(['system:themes:manage'])"
#dropdownItems
>
<VButton <VButton
v-close-popper v-close-popper
block block
@ -262,7 +272,11 @@ defineExpose({
</ul> </ul>
<template #footer> <template #footer>
<VSpace> <VSpace>
<VButton type="secondary" @click="themeInstall = true"> <VButton
v-permission="['system:themes:manage']"
type="secondary"
@click="themeInstall = true"
>
安装主题 安装主题
</VButton> </VButton>
<VButton @click="onVisibleChange(false)"></VButton> <VButton @click="onVisibleChange(false)"></VButton>

View File

@ -156,6 +156,7 @@ watch([() => route.name, () => route.params], async () => {
</VButton> </VButton>
<VButton <VButton
v-if="!isActivated" v-if="!isActivated"
v-permission="['system:themes:manage']"
size="sm" size="sm"
type="primary" type="primary"
@click="handleActiveTheme" @click="handleActiveTheme"

View File

@ -20,6 +20,7 @@ export default definePlugin({
meta: { meta: {
title: "主题", title: "主题",
searchable: true, searchable: true,
permissions: ["system:themes:view"],
}, },
}, },
{ {
@ -28,6 +29,7 @@ export default definePlugin({
component: ThemeSetting, component: ThemeSetting,
meta: { meta: {
title: "主题设置", title: "主题设置",
permissions: ["system:themes:view"],
}, },
}, },
], ],

View File

@ -12,6 +12,9 @@ import { toRefs } from "vue";
import { usePluginLifeCycle } from "../composables/use-plugin"; import { usePluginLifeCycle } from "../composables/use-plugin";
import type { Plugin } from "@halo-dev/api-client"; import type { Plugin } from "@halo-dev/api-client";
import { formatDatetime } from "@/utils/date"; import { formatDatetime } from "@/utils/date";
import { usePermission } from "@/utils/permission";
const { currentUserHasPermission } = usePermission();
const props = withDefaults( const props = withDefaults(
defineProps<{ defineProps<{
@ -86,12 +89,9 @@ const { isStarted, changeStatus, uninstall } = usePluginLifeCycle(plugin);
</span> </span>
</template> </template>
</VEntityField> </VEntityField>
<VEntityField> <VEntityField v-permission="['system:plugins:manage']">
<template #description> <template #description>
<div <div class="flex items-center">
v-permission="['system:plugins:manage']"
class="flex items-center"
>
<VSwitch <VSwitch
:model-value="plugin?.spec.enabled" :model-value="plugin?.spec.enabled"
@click="changeStatus" @click="changeStatus"
@ -100,7 +100,10 @@ const { isStarted, changeStatus, uninstall } = usePluginLifeCycle(plugin);
</template> </template>
</VEntityField> </VEntityField>
</template> </template>
<template #dropdownItems> <template
v-if="!currentUserHasPermission(['system:plugins:manage'])"
#dropdownItems
>
<VButton v-close-popper block type="danger" @click="uninstall"> <VButton v-close-popper block type="danger" @click="uninstall">
卸载 卸载
</VButton> </VButton>

View File

@ -32,6 +32,9 @@ import { useFetchRole } from "@/modules/system/roles/composables/use-role";
import cloneDeep from "lodash.clonedeep"; import cloneDeep from "lodash.clonedeep";
import { apiClient } from "@/utils/api-client"; import { apiClient } from "@/utils/api-client";
import Fuse from "fuse.js"; import Fuse from "fuse.js";
import { usePermission } from "@/utils/permission";
const { currentUserHasPermission } = usePermission();
const editingModal = ref<boolean>(false); const editingModal = ref<boolean>(false);
const selectedRole = ref<Role>(); const selectedRole = ref<Role>();
@ -272,7 +275,10 @@ const handleDelete = async (role: Role) => {
</template> </template>
</VEntityField> </VEntityField>
</template> </template>
<template #dropdownItems> <template
v-if="!currentUserHasPermission(['system:roles:manage'])"
#dropdownItems
>
<VButton <VButton
v-close-popper v-close-popper
block block

View File

@ -26,6 +26,9 @@ import { rbacAnnotations } from "@/constants/annotations";
import { formatDatetime } from "@/utils/date"; import { formatDatetime } from "@/utils/date";
import { useRouteQuery } from "@vueuse/router"; import { useRouteQuery } from "@vueuse/router";
import Fuse from "fuse.js"; import Fuse from "fuse.js";
import { usePermission } from "@/utils/permission";
const { currentUserHasPermission } = usePermission();
const dialog = useDialog(); const dialog = useDialog();
@ -254,10 +257,12 @@ onMounted(() => {
<div <div
class="relative flex flex-col items-start sm:flex-row sm:items-center" class="relative flex flex-col items-start sm:flex-row sm:items-center"
> >
<div class="mr-4 hidden items-center sm:flex"> <div
v-permission="['system:users:manage']"
class="mr-4 hidden items-center sm:flex"
>
<input <input
v-model="checkedAll" v-model="checkedAll"
v-permission="['system:users:manage']"
class="h-4 w-4 rounded border-gray-300 text-indigo-600" class="h-4 w-4 rounded border-gray-300 text-indigo-600"
type="checkbox" type="checkbox"
@change="handleCheckAllChange" @change="handleCheckAllChange"
@ -382,10 +387,12 @@ onMounted(() => {
<ul class="box-border h-full w-full divide-y divide-gray-100" role="list"> <ul class="box-border h-full w-full divide-y divide-gray-100" role="list">
<li v-for="(user, index) in searchResults" :key="index"> <li v-for="(user, index) in searchResults" :key="index">
<VEntity :is-selected="checkSelection(user)"> <VEntity :is-selected="checkSelection(user)">
<template #checkbox> <template
v-if="!currentUserHasPermission(['system:users:manage'])"
#checkbox
>
<input <input
v-model="selectedUserNames" v-model="selectedUserNames"
v-permission="['system:users:manage']"
:value="user.metadata.name" :value="user.metadata.name"
class="h-4 w-4 rounded border-gray-300 text-indigo-600" class="h-4 w-4 rounded border-gray-300 text-indigo-600"
name="post-checkbox" name="post-checkbox"
@ -438,10 +445,12 @@ onMounted(() => {
</template> </template>
</VEntityField> </VEntityField>
</template> </template>
<template #dropdownItems> <template
v-if="!currentUserHasPermission(['system:users:manage'])"
#dropdownItems
>
<VButton <VButton
v-close-popper v-close-popper
v-permission="['system:users:manage']"
block block
type="secondary" type="secondary"
@click="handleOpenCreateModal(user)" @click="handleOpenCreateModal(user)"
@ -450,7 +459,6 @@ onMounted(() => {
</VButton> </VButton>
<VButton <VButton
v-close-popper v-close-popper
v-permission="['system:users:manage']"
block block
@click="handleOpenPasswordChangeModal(user)" @click="handleOpenPasswordChangeModal(user)"
> >
@ -459,7 +467,6 @@ onMounted(() => {
<VButton <VButton
v-if="currentUser?.metadata.name !== user.metadata.name" v-if="currentUser?.metadata.name !== user.metadata.name"
v-close-popper v-close-popper
v-permission="['system:users:manage']"
block block
@click="handleOpenGrantPermissionModal(user)" @click="handleOpenGrantPermissionModal(user)"
> >
@ -468,7 +475,6 @@ onMounted(() => {
<VButton <VButton
v-if="currentUser?.metadata.name !== user.metadata.name" v-if="currentUser?.metadata.name !== user.metadata.name"
v-close-popper v-close-popper
v-permission="['system:users:manage']"
block block
type="danger" type="danger"
@click="handleDelete(user)" @click="handleDelete(user)"

View File

@ -1,5 +1,15 @@
import { useRoleStore } from "@/stores/role";
import isEqual from "lodash.isequal"; import isEqual from "lodash.isequal";
/**
* It returns true if the user has all the permissions required to access a resource
*
* @param uiPermissions - The permissions that the user has.
* @param targetPermissions - The permissions that the user needs to have in order to access the
* resource.
* @param {boolean} any - boolean - if true, the user only needs to have one of the targetPermissions
* to pass the check.
*/
export function hasPermission( export function hasPermission(
uiPermissions: Array<string>, uiPermissions: Array<string>,
targetPermissions: Array<string>, targetPermissions: Array<string>,
@ -24,3 +34,27 @@ export function hasPermission(
return !!(!any && isEqual(intersection, targetPermissions)); return !!(!any && isEqual(intersection, targetPermissions));
} }
interface usePermissionReturn {
currentUserHasPermission: (targetPermissions: Array<string>) => boolean;
}
/**
* It returns a function that checks if the current user has a permission
*
* @returns An object with a function called currentUserHasPermission
*/
export function usePermission(): usePermissionReturn {
const roleStore = useRoleStore();
const { uiPermissions } = roleStore.permissions;
const currentUserHasPermission = (
targetPermissions: Array<string>
): boolean => {
return hasPermission(uiPermissions, targetPermissions, true);
};
return {
currentUserHasPermission,
};
}