mirror of https://github.com/halo-dev/halo
feat: add multi-role assignment support for users (#7037)
#### What type of PR is this? /area ui /kind feature /milestone 2.20.x #### What this PR does / why we need it: 支持为用户分配多个角色。 <img width="634" alt="image" src="https://github.com/user-attachments/assets/caa40327-518a-4bef-afc3-75a020018d3d"> <img width="764" alt="image" src="https://github.com/user-attachments/assets/8b4b807e-6c72-45d9-9368-75e70bb12dcc"> TODO: - [x] Console / UC 侧边栏显示多个角色 - [x] 支持在管理端查看用户聚合的角色模板列表,或者在分配时显示所选角色其下的角色模板列表,能够让管理员清楚的知道用户具体权限。 #### Which issue(s) this PR fixes: Fixes # #### Special notes for your reviewer: #### Does this PR introduce a user-facing change? ```release-note 支持为用户分配多个角色。 ```pull/7075/head
parent
964bc28052
commit
391aac62d3
|
@ -9,11 +9,13 @@ import { coreMenuGroups } from "@console/router/constant";
|
||||||
import {
|
import {
|
||||||
Dialog,
|
Dialog,
|
||||||
IconAccountCircleLine,
|
IconAccountCircleLine,
|
||||||
|
IconArrowDownLine,
|
||||||
IconLogoutCircleRLine,
|
IconLogoutCircleRLine,
|
||||||
IconMore,
|
IconMore,
|
||||||
IconSearch,
|
IconSearch,
|
||||||
IconUserSettings,
|
IconShieldUser,
|
||||||
VAvatar,
|
VAvatar,
|
||||||
|
VDropdown,
|
||||||
VTag,
|
VTag,
|
||||||
} from "@halo-dev/components";
|
} from "@halo-dev/components";
|
||||||
import { useEventListener } from "@vueuse/core";
|
import { useEventListener } from "@vueuse/core";
|
||||||
|
@ -152,10 +154,10 @@ onMounted(() => {
|
||||||
>
|
>
|
||||||
{{ currentUser?.spec.displayName }}
|
{{ currentUser?.spec.displayName }}
|
||||||
</div>
|
</div>
|
||||||
<div v-if="currentRoles?.[0]" class="flex">
|
<div v-if="currentRoles?.length" class="flex mt-1">
|
||||||
<VTag>
|
<VTag v-if="currentRoles.length === 1">
|
||||||
<template #leftIcon>
|
<template #leftIcon>
|
||||||
<IconUserSettings />
|
<IconShieldUser />
|
||||||
</template>
|
</template>
|
||||||
{{
|
{{
|
||||||
currentRoles[0].metadata.annotations?.[
|
currentRoles[0].metadata.annotations?.[
|
||||||
|
@ -163,6 +165,41 @@ onMounted(() => {
|
||||||
] || currentRoles[0].metadata.name
|
] || currentRoles[0].metadata.name
|
||||||
}}
|
}}
|
||||||
</VTag>
|
</VTag>
|
||||||
|
<VDropdown v-else>
|
||||||
|
<div class="flex gap-1">
|
||||||
|
<VTag>
|
||||||
|
<template #leftIcon>
|
||||||
|
<IconShieldUser />
|
||||||
|
</template>
|
||||||
|
{{ $t("core.sidebar.profile.aggregate_role") }}
|
||||||
|
</VTag>
|
||||||
|
<IconArrowDownLine />
|
||||||
|
</div>
|
||||||
|
<template #popper>
|
||||||
|
<div class="p-1">
|
||||||
|
<h2
|
||||||
|
class="text-gray-600 text-sm font-semibold border-b border-gray-100 pb-1.5"
|
||||||
|
>
|
||||||
|
{{ $t("core.sidebar.profile.aggregate_role") }}
|
||||||
|
</h2>
|
||||||
|
<div class="flex gap-2 flex-wrap mt-2">
|
||||||
|
<VTag
|
||||||
|
v-for="role in currentRoles"
|
||||||
|
:key="role.metadata.name"
|
||||||
|
>
|
||||||
|
<template #leftIcon>
|
||||||
|
<IconShieldUser />
|
||||||
|
</template>
|
||||||
|
{{
|
||||||
|
role.metadata.annotations?.[
|
||||||
|
rbacAnnotations.DISPLAY_NAME
|
||||||
|
] || role.metadata.name
|
||||||
|
}}
|
||||||
|
</VTag>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</VDropdown>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
@ -299,6 +336,7 @@ onMounted(() => {
|
||||||
|
|
||||||
.profile-placeholder {
|
.profile-placeholder {
|
||||||
height: 70px;
|
height: 70px;
|
||||||
|
flex: none;
|
||||||
|
|
||||||
.current-profile {
|
.current-profile {
|
||||||
height: 70px;
|
height: 70px;
|
||||||
|
|
|
@ -183,8 +183,8 @@ const handleUpdateRole = async () => {
|
||||||
<div>
|
<div>
|
||||||
<dl class="divide-y divide-gray-100">
|
<dl class="divide-y divide-gray-100">
|
||||||
<div
|
<div
|
||||||
v-for="(group, groupIndex) in roleTemplateGroups"
|
v-for="(group, index) in roleTemplateGroups"
|
||||||
:key="groupIndex"
|
:key="index"
|
||||||
class="bg-white px-4 py-5 hover:bg-gray-50 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-6"
|
class="bg-white px-4 py-5 hover:bg-gray-50 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-6"
|
||||||
>
|
>
|
||||||
<dt class="text-sm font-medium text-gray-900">
|
<dt class="text-sm font-medium text-gray-900">
|
||||||
|
@ -224,7 +224,7 @@ const handleUpdateRole = async () => {
|
||||||
</dt>
|
</dt>
|
||||||
<dd class="mt-1 text-sm text-gray-900 sm:col-span-2 sm:mt-0">
|
<dd class="mt-1 text-sm text-gray-900 sm:col-span-2 sm:mt-0">
|
||||||
<ul class="space-y-2">
|
<ul class="space-y-2">
|
||||||
<li v-for="(role, index) in group.roles" :key="index">
|
<li v-for="role in group.roles" :key="role.metadata.name">
|
||||||
<label
|
<label
|
||||||
class="inline-flex w-72 cursor-pointer flex-row items-center gap-4 rounded-base border p-5 hover:border-primary"
|
class="inline-flex w-72 cursor-pointer flex-row items-center gap-4 rounded-base border p-5 hover:border-primary"
|
||||||
>
|
>
|
||||||
|
|
|
@ -101,7 +101,7 @@ const tabbarItems = computed(() => {
|
||||||
}));
|
}));
|
||||||
});
|
});
|
||||||
|
|
||||||
const handleDelete = async (userToDelete: User) => {
|
const handleDelete = async (user: User) => {
|
||||||
Dialog.warning({
|
Dialog.warning({
|
||||||
title: t("core.user.operations.delete.title"),
|
title: t("core.user.operations.delete.title"),
|
||||||
description: t("core.common.dialog.descriptions.cannot_be_recovered"),
|
description: t("core.common.dialog.descriptions.cannot_be_recovered"),
|
||||||
|
@ -111,7 +111,7 @@ const handleDelete = async (userToDelete: User) => {
|
||||||
onConfirm: async () => {
|
onConfirm: async () => {
|
||||||
try {
|
try {
|
||||||
await coreApiClient.user.deleteUser({
|
await coreApiClient.user.deleteUser({
|
||||||
name: userToDelete.metadata.name,
|
name: user.metadata.name,
|
||||||
});
|
});
|
||||||
Toast.success(t("core.common.toast.delete_success"));
|
Toast.success(t("core.common.toast.delete_success"));
|
||||||
router.push({ name: "Users" });
|
router.push({ name: "Users" });
|
||||||
|
@ -189,11 +189,17 @@ function onGrantPermissionModalClose() {
|
||||||
<VDropdownItem @click="passwordChangeModal = true">
|
<VDropdownItem @click="passwordChangeModal = true">
|
||||||
{{ $t("core.user.detail.actions.change_password.title") }}
|
{{ $t("core.user.detail.actions.change_password.title") }}
|
||||||
</VDropdownItem>
|
</VDropdownItem>
|
||||||
<VDropdownItem @click="grantPermissionModal = true">
|
<VDropdownItem
|
||||||
|
v-if="currentUser?.metadata.name !== user?.user.metadata.name"
|
||||||
|
@click="grantPermissionModal = true"
|
||||||
|
>
|
||||||
{{ $t("core.user.detail.actions.grant_permission.title") }}
|
{{ $t("core.user.detail.actions.grant_permission.title") }}
|
||||||
</VDropdownItem>
|
</VDropdownItem>
|
||||||
<VDropdownItem
|
<VDropdownItem
|
||||||
v-if="user?.user"
|
v-if="
|
||||||
|
user &&
|
||||||
|
currentUser?.metadata.name !== user?.user.metadata.name
|
||||||
|
"
|
||||||
type="danger"
|
type="danger"
|
||||||
@click="handleDelete(user.user)"
|
@click="handleDelete(user.user)"
|
||||||
>
|
>
|
||||||
|
|
|
@ -11,7 +11,7 @@ import {
|
||||||
IconAddCircle,
|
IconAddCircle,
|
||||||
IconLockPasswordLine,
|
IconLockPasswordLine,
|
||||||
IconRefreshLine,
|
IconRefreshLine,
|
||||||
IconUserFollow,
|
IconShieldUser,
|
||||||
IconUserSettings,
|
IconUserSettings,
|
||||||
Toast,
|
Toast,
|
||||||
VAvatar,
|
VAvatar,
|
||||||
|
@ -288,7 +288,7 @@ function onGrantPermissionModalClose() {
|
||||||
type="default"
|
type="default"
|
||||||
>
|
>
|
||||||
<template #icon>
|
<template #icon>
|
||||||
<IconUserFollow class="h-full w-full" />
|
<IconShieldUser class="h-full w-full" />
|
||||||
</template>
|
</template>
|
||||||
{{ $t("core.user.actions.roles") }}
|
{{ $t("core.user.actions.roles") }}
|
||||||
</VButton>
|
</VButton>
|
||||||
|
@ -469,19 +469,21 @@ function onGrantPermissionModalClose() {
|
||||||
<template #end>
|
<template #end>
|
||||||
<VEntityField>
|
<VEntityField>
|
||||||
<template #description>
|
<template #description>
|
||||||
<div
|
<VSpace>
|
||||||
v-for="(role, roleIndex) in user.roles"
|
<VTag
|
||||||
:key="roleIndex"
|
v-for="role in user.roles"
|
||||||
class="flex items-center"
|
:key="role.metadata.name"
|
||||||
>
|
>
|
||||||
<VTag>
|
<template #leftIcon>
|
||||||
|
<IconShieldUser />
|
||||||
|
</template>
|
||||||
{{
|
{{
|
||||||
role.metadata.annotations?.[
|
role.metadata.annotations?.[
|
||||||
rbacAnnotations.DISPLAY_NAME
|
rbacAnnotations.DISPLAY_NAME
|
||||||
] || role.metadata.name
|
] || role.metadata.name
|
||||||
}}
|
}}
|
||||||
</VTag>
|
</VTag>
|
||||||
</div>
|
</VSpace>
|
||||||
</template>
|
</template>
|
||||||
</VEntityField>
|
</VEntityField>
|
||||||
<VEntityField v-if="user.user.metadata.deletionTimestamp">
|
<VEntityField v-if="user.user.metadata.deletionTimestamp">
|
||||||
|
|
|
@ -1,9 +1,17 @@
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import SubmitButton from "@/components/button/SubmitButton.vue";
|
import SubmitButton from "@/components/button/SubmitButton.vue";
|
||||||
|
import { rbacAnnotations } from "@/constants/annotations";
|
||||||
|
import { SUPER_ROLE_NAME } from "@/constants/constants";
|
||||||
|
import { roleLabels } from "@/constants/labels";
|
||||||
import type { User } from "@halo-dev/api-client";
|
import type { User } from "@halo-dev/api-client";
|
||||||
import { consoleApiClient } from "@halo-dev/api-client";
|
import { consoleApiClient, coreApiClient } from "@halo-dev/api-client";
|
||||||
import { VButton, VModal, VSpace } from "@halo-dev/components";
|
import { Toast, VButton, VModal, VSpace } from "@halo-dev/components";
|
||||||
import { ref } from "vue";
|
import { useMutation, useQuery } from "@tanstack/vue-query";
|
||||||
|
import { computed, onMounted, ref } from "vue";
|
||||||
|
import { useI18n } from "vue-i18n";
|
||||||
|
import RolesView from "./RolesView.vue";
|
||||||
|
|
||||||
|
const { t } = useI18n();
|
||||||
|
|
||||||
const props = withDefaults(
|
const props = withDefaults(
|
||||||
defineProps<{
|
defineProps<{
|
||||||
|
@ -19,51 +27,137 @@ const emit = defineEmits<{
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
const modal = ref<InstanceType<typeof VModal> | null>(null);
|
const modal = ref<InstanceType<typeof VModal> | null>(null);
|
||||||
const selectedRole = ref("");
|
|
||||||
const isSubmitting = ref(false);
|
|
||||||
|
|
||||||
const handleGrantPermission = async () => {
|
const selectedRoleNames = ref<string[]>([]);
|
||||||
try {
|
|
||||||
isSubmitting.value = true;
|
onMounted(() => {
|
||||||
await consoleApiClient.user.grantPermission({
|
if (!props.user) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
selectedRoleNames.value = JSON.parse(
|
||||||
|
props.user.metadata.annotations?.[rbacAnnotations.ROLE_NAMES] || "[]"
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
const { mutate, isLoading } = useMutation({
|
||||||
|
mutationKey: ["core:user:grant-permissions"],
|
||||||
|
mutationFn: async ({ roles }: { roles: string[] }) => {
|
||||||
|
return await consoleApiClient.user.grantPermission({
|
||||||
name: props.user?.metadata.name as string,
|
name: props.user?.metadata.name as string,
|
||||||
grantRequest: {
|
grantRequest: {
|
||||||
roles: [selectedRole.value],
|
roles: roles,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
},
|
||||||
|
onSuccess() {
|
||||||
|
Toast.success(t("core.common.toast.operation_success"));
|
||||||
modal.value?.close();
|
modal.value?.close();
|
||||||
} catch (error) {
|
},
|
||||||
console.error("Failed to grant permission to user", error);
|
});
|
||||||
} finally {
|
|
||||||
isSubmitting.value = false;
|
function onSubmit(data: { roles: string[] }) {
|
||||||
|
mutate({ roles: data.roles });
|
||||||
}
|
}
|
||||||
};
|
|
||||||
|
const { data: allRoles } = useQuery({
|
||||||
|
queryKey: ["core:roles"],
|
||||||
|
queryFn: async () => {
|
||||||
|
const { data } = await coreApiClient.role.listRole({
|
||||||
|
page: 0,
|
||||||
|
size: 0,
|
||||||
|
labelSelector: [`!${roleLabels.TEMPLATE}`],
|
||||||
|
});
|
||||||
|
return data;
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const { data: allRoleTemplates } = useQuery({
|
||||||
|
queryKey: ["core:role-templates"],
|
||||||
|
queryFn: async () => {
|
||||||
|
const { data } = await coreApiClient.role.listRole({
|
||||||
|
page: 0,
|
||||||
|
size: 0,
|
||||||
|
labelSelector: [`${roleLabels.TEMPLATE}=true`, "!halo.run/hidden"],
|
||||||
|
});
|
||||||
|
return data.items;
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const currentRoleTemplates = computed(() => {
|
||||||
|
if (!selectedRoleNames.value.length) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
const selectedRoles = allRoles.value?.items.filter((role) =>
|
||||||
|
selectedRoleNames.value.includes(role.metadata.name)
|
||||||
|
);
|
||||||
|
|
||||||
|
let allDependsRoleTemplates: string[] = [];
|
||||||
|
|
||||||
|
selectedRoles?.forEach((role) => {
|
||||||
|
allDependsRoleTemplates = allDependsRoleTemplates.concat(
|
||||||
|
JSON.parse(
|
||||||
|
role.metadata.annotations?.[rbacAnnotations.DEPENDENCIES] || "[]"
|
||||||
|
)
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
return allRoleTemplates.value?.filter((item) => {
|
||||||
|
return allDependsRoleTemplates.includes(item.metadata.name);
|
||||||
|
});
|
||||||
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<VModal
|
<VModal
|
||||||
ref="modal"
|
ref="modal"
|
||||||
:title="$t('core.user.grant_permission_modal.title')"
|
:title="$t('core.user.grant_permission_modal.title')"
|
||||||
:width="500"
|
:width="600"
|
||||||
|
:centered="false"
|
||||||
@close="emit('close')"
|
@close="emit('close')"
|
||||||
>
|
>
|
||||||
|
<div>
|
||||||
<FormKit
|
<FormKit
|
||||||
id="grant-permission-form"
|
id="grant-permission-form"
|
||||||
name="grant-permission-form"
|
name="grant-permission-form"
|
||||||
:config="{ validationVisibility: 'submit' }"
|
:config="{ validationVisibility: 'submit' }"
|
||||||
type="form"
|
type="form"
|
||||||
@submit="handleGrantPermission"
|
@submit="onSubmit"
|
||||||
>
|
>
|
||||||
|
<!-- @vue-ignore -->
|
||||||
<FormKit
|
<FormKit
|
||||||
v-model="selectedRole"
|
v-model="selectedRoleNames"
|
||||||
|
multiple
|
||||||
|
name="roles"
|
||||||
:label="$t('core.user.grant_permission_modal.fields.role.label')"
|
:label="$t('core.user.grant_permission_modal.fields.role.label')"
|
||||||
type="roleSelect"
|
type="roleSelect"
|
||||||
|
:placeholder="
|
||||||
|
$t('core.user.grant_permission_modal.fields.role.placeholder')
|
||||||
|
"
|
||||||
></FormKit>
|
></FormKit>
|
||||||
</FormKit>
|
</FormKit>
|
||||||
|
|
||||||
|
<div v-if="selectedRoleNames.length">
|
||||||
|
<div
|
||||||
|
v-if="selectedRoleNames.includes(SUPER_ROLE_NAME)"
|
||||||
|
class="text-sm text-gray-600 mt-4"
|
||||||
|
>
|
||||||
|
{{ $t("core.user.grant_permission_modal.roles_preview.all") }}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-else-if="currentRoleTemplates?.length" class="space-y-3 mt-4">
|
||||||
|
<span class="text-sm text-gray-600">
|
||||||
|
{{ $t("core.user.grant_permission_modal.roles_preview.includes") }}
|
||||||
|
</span>
|
||||||
|
<RolesView :role-templates="currentRoleTemplates" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<template #footer>
|
<template #footer>
|
||||||
<VSpace>
|
<VSpace>
|
||||||
<SubmitButton
|
<SubmitButton
|
||||||
:loading="isSubmitting"
|
:loading="isLoading"
|
||||||
type="secondary"
|
type="secondary"
|
||||||
:text="$t('core.common.buttons.submit')"
|
:text="$t('core.common.buttons.submit')"
|
||||||
@submit="$formkit.submit('grant-permission-form')"
|
@submit="$formkit.submit('grant-permission-form')"
|
||||||
|
|
|
@ -0,0 +1,107 @@
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { useRoleTemplateSelection } from "@/composables/use-role";
|
||||||
|
import { rbacAnnotations } from "@/constants/annotations";
|
||||||
|
import { pluginLabels } from "@/constants/labels";
|
||||||
|
import type { Role } from "@halo-dev/api-client";
|
||||||
|
import { toRefs } from "vue";
|
||||||
|
|
||||||
|
const props = withDefaults(
|
||||||
|
defineProps<{
|
||||||
|
roleTemplates?: Role[];
|
||||||
|
}>(),
|
||||||
|
{
|
||||||
|
roleTemplates: () => [],
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
const { roleTemplates } = toRefs(props);
|
||||||
|
|
||||||
|
const { roleTemplateGroups } = useRoleTemplateSelection(roleTemplates);
|
||||||
|
</script>
|
||||||
|
<template>
|
||||||
|
<dl
|
||||||
|
class="divide-y divide-gray-100 border border-gray-100 rounded-base overflow-hidden"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
v-for="(group, index) in roleTemplateGroups"
|
||||||
|
:key="index"
|
||||||
|
class="bg-white px-4 py-5 hover:bg-gray-50 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-6"
|
||||||
|
>
|
||||||
|
<dt class="text-sm font-medium text-gray-900">
|
||||||
|
<div>
|
||||||
|
{{ $t(`core.rbac.${group.module}`, group.module as string) }}
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
v-if="
|
||||||
|
group.roles.length &&
|
||||||
|
group.roles[0].metadata.labels?.[pluginLabels.NAME]
|
||||||
|
"
|
||||||
|
class="mt-3 text-xs text-gray-500"
|
||||||
|
>
|
||||||
|
<i18n-t keypath="core.role.common.text.provided_by_plugin" tag="div">
|
||||||
|
<template #plugin>
|
||||||
|
<RouterLink
|
||||||
|
:to="{
|
||||||
|
name: 'PluginDetail',
|
||||||
|
params: {
|
||||||
|
name: group.roles[0].metadata.labels?.[pluginLabels.NAME],
|
||||||
|
},
|
||||||
|
}"
|
||||||
|
class="hover:text-blue-600"
|
||||||
|
>
|
||||||
|
{{ group.roles[0].metadata.labels?.[pluginLabels.NAME] }}
|
||||||
|
</RouterLink>
|
||||||
|
</template>
|
||||||
|
</i18n-t>
|
||||||
|
</div>
|
||||||
|
</dt>
|
||||||
|
<dd class="mt-1 text-sm text-gray-900 sm:col-span-2 sm:mt-0">
|
||||||
|
<ul class="space-y-2">
|
||||||
|
<li v-for="role in group.roles" :key="role.metadata.name">
|
||||||
|
<label
|
||||||
|
class="inline-flex w-72 cursor-pointer flex-row items-center gap-4 rounded-base border p-5 hover:border-primary"
|
||||||
|
>
|
||||||
|
<input type="checkbox" disabled checked />
|
||||||
|
<div class="flex flex-1 flex-col gap-y-3">
|
||||||
|
<span class="font-medium text-gray-900">
|
||||||
|
{{
|
||||||
|
$t(
|
||||||
|
`core.rbac.${
|
||||||
|
role.metadata.annotations?.[
|
||||||
|
rbacAnnotations.DISPLAY_NAME
|
||||||
|
]
|
||||||
|
}`,
|
||||||
|
role.metadata.annotations?.[
|
||||||
|
rbacAnnotations.DISPLAY_NAME
|
||||||
|
] as string
|
||||||
|
)
|
||||||
|
}}
|
||||||
|
</span>
|
||||||
|
<span
|
||||||
|
v-if="
|
||||||
|
role.metadata.annotations?.[rbacAnnotations.DEPENDENCIES]
|
||||||
|
"
|
||||||
|
class="text-xs text-gray-400"
|
||||||
|
>
|
||||||
|
{{
|
||||||
|
$t("core.role.common.text.dependent_on", {
|
||||||
|
roles: JSON.parse(
|
||||||
|
role.metadata.annotations?.[
|
||||||
|
rbacAnnotations.DEPENDENCIES
|
||||||
|
]
|
||||||
|
)
|
||||||
|
.map((item: string) =>
|
||||||
|
$t(`core.rbac.${item}`, item as string)
|
||||||
|
)
|
||||||
|
.join(","),
|
||||||
|
})
|
||||||
|
}}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</label>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</dd>
|
||||||
|
</div>
|
||||||
|
</dl>
|
||||||
|
</template>
|
|
@ -4,9 +4,9 @@ import { formatDatetime } from "@/utils/date";
|
||||||
import type { DetailedUser } from "@halo-dev/api-client";
|
import type { DetailedUser } from "@halo-dev/api-client";
|
||||||
import {
|
import {
|
||||||
IconInformation,
|
IconInformation,
|
||||||
IconUserSettings,
|
|
||||||
VDescription,
|
VDescription,
|
||||||
VDescriptionItem,
|
VDescriptionItem,
|
||||||
|
VSpace,
|
||||||
VTag,
|
VTag,
|
||||||
} from "@halo-dev/components";
|
} from "@halo-dev/components";
|
||||||
import RiVerifiedBadgeLine from "~icons/ri/verified-badge-line";
|
import RiVerifiedBadgeLine from "~icons/ri/verified-badge-line";
|
||||||
|
@ -55,9 +55,10 @@ withDefaults(defineProps<{ user?: DetailedUser }>(), {
|
||||||
:label="$t('core.user.detail.fields.roles')"
|
:label="$t('core.user.detail.fields.roles')"
|
||||||
class="!px-2"
|
class="!px-2"
|
||||||
>
|
>
|
||||||
|
<VSpace>
|
||||||
<VTag
|
<VTag
|
||||||
v-for="(role, index) in user?.roles"
|
v-for="role in user?.roles"
|
||||||
:key="index"
|
:key="role.metadata.name"
|
||||||
@click="
|
@click="
|
||||||
$router.push({
|
$router.push({
|
||||||
name: 'RoleDetail',
|
name: 'RoleDetail',
|
||||||
|
@ -65,14 +66,12 @@ withDefaults(defineProps<{ user?: DetailedUser }>(), {
|
||||||
})
|
})
|
||||||
"
|
"
|
||||||
>
|
>
|
||||||
<template #leftIcon>
|
|
||||||
<IconUserSettings />
|
|
||||||
</template>
|
|
||||||
{{
|
{{
|
||||||
role.metadata.annotations?.[rbacAnnotations.DISPLAY_NAME] ||
|
role.metadata.annotations?.[rbacAnnotations.DISPLAY_NAME] ||
|
||||||
role.metadata.name
|
role.metadata.name
|
||||||
}}
|
}}
|
||||||
</VTag>
|
</VTag>
|
||||||
|
</VSpace>
|
||||||
</VDescriptionItem>
|
</VDescriptionItem>
|
||||||
<VDescriptionItem
|
<VDescriptionItem
|
||||||
:label="$t('core.user.detail.fields.bio')"
|
:label="$t('core.user.detail.fields.bio')"
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
import { rbacAnnotations } from "@/constants/annotations";
|
import { rbacAnnotations } from "@/constants/annotations";
|
||||||
|
import { SUPER_ROLE_NAME } from "@/constants/constants";
|
||||||
import { useRoleStore } from "@/stores/role";
|
import { useRoleStore } from "@/stores/role";
|
||||||
import { useUserStore } from "@/stores/user";
|
import { useUserStore } from "@/stores/user";
|
||||||
import { hasPermission } from "@/utils/permission";
|
import { hasPermission } from "@/utils/permission";
|
||||||
|
@ -24,6 +25,10 @@ export function setupPermissionGuard(router: Router) {
|
||||||
}
|
}
|
||||||
|
|
||||||
function isConsoleAccessDisallowed(currentRoles?: Role[]): boolean {
|
function isConsoleAccessDisallowed(currentRoles?: Role[]): boolean {
|
||||||
|
if (currentRoles?.some((role) => role.metadata.name === SUPER_ROLE_NAME)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
currentRoles?.some(
|
currentRoles?.some(
|
||||||
(role) =>
|
(role) =>
|
||||||
|
|
|
@ -1,6 +1,5 @@
|
||||||
import { rbacAnnotations } from "@/constants/annotations";
|
import { rbacAnnotations } from "@/constants/annotations";
|
||||||
import { roleLabels } from "@/constants/labels";
|
import { roleLabels } from "@/constants/labels";
|
||||||
import { i18n } from "@/locales";
|
|
||||||
import type { FormKitNode, FormKitTypeDefinition } from "@formkit/core";
|
import type { FormKitNode, FormKitTypeDefinition } from "@formkit/core";
|
||||||
import { coreApiClient } from "@halo-dev/api-client";
|
import { coreApiClient } from "@halo-dev/api-client";
|
||||||
import { select } from "./select";
|
import { select } from "./select";
|
||||||
|
@ -14,12 +13,6 @@ function optionsHandler(node: FormKitNode) {
|
||||||
});
|
});
|
||||||
|
|
||||||
const options = [
|
const options = [
|
||||||
{
|
|
||||||
label: i18n.global.t(
|
|
||||||
"core.user.grant_permission_modal.fields.role.placeholder"
|
|
||||||
),
|
|
||||||
value: "",
|
|
||||||
},
|
|
||||||
...data.items.map((role) => {
|
...data.items.map((role) => {
|
||||||
return {
|
return {
|
||||||
label:
|
label:
|
||||||
|
|
|
@ -33,6 +33,8 @@ core:
|
||||||
tooltip: Profile
|
tooltip: Profile
|
||||||
visit_homepage:
|
visit_homepage:
|
||||||
title: Visit homepage
|
title: Visit homepage
|
||||||
|
profile:
|
||||||
|
aggregate_role: Aggregate Role
|
||||||
uc_sidebar:
|
uc_sidebar:
|
||||||
menu:
|
menu:
|
||||||
items:
|
items:
|
||||||
|
@ -42,6 +44,8 @@ core:
|
||||||
operations:
|
operations:
|
||||||
console:
|
console:
|
||||||
tooltip: Console
|
tooltip: Console
|
||||||
|
profile:
|
||||||
|
aggregate_role: Aggregate Role
|
||||||
dashboard:
|
dashboard:
|
||||||
title: Dashboard
|
title: Dashboard
|
||||||
actions:
|
actions:
|
||||||
|
@ -1059,6 +1063,9 @@ core:
|
||||||
role:
|
role:
|
||||||
label: Role
|
label: Role
|
||||||
placeholder: Please select a role
|
placeholder: Please select a role
|
||||||
|
roles_preview:
|
||||||
|
all: The currently selected role contains all permissions
|
||||||
|
includes: "The currently selected role contains the following permissions:"
|
||||||
detail:
|
detail:
|
||||||
title: User detail
|
title: User detail
|
||||||
tabs:
|
tabs:
|
||||||
|
|
|
@ -31,6 +31,8 @@ core:
|
||||||
tooltip: 个人中心
|
tooltip: 个人中心
|
||||||
visit_homepage:
|
visit_homepage:
|
||||||
title: 访问首页
|
title: 访问首页
|
||||||
|
profile:
|
||||||
|
aggregate_role: 聚合角色
|
||||||
uc_sidebar:
|
uc_sidebar:
|
||||||
menu:
|
menu:
|
||||||
items:
|
items:
|
||||||
|
@ -40,6 +42,8 @@ core:
|
||||||
operations:
|
operations:
|
||||||
console:
|
console:
|
||||||
tooltip: 管理控制台
|
tooltip: 管理控制台
|
||||||
|
profile:
|
||||||
|
aggregate_role: 聚合角色
|
||||||
dashboard:
|
dashboard:
|
||||||
title: 仪表板
|
title: 仪表板
|
||||||
actions:
|
actions:
|
||||||
|
@ -989,6 +993,9 @@ core:
|
||||||
role:
|
role:
|
||||||
label: 角色
|
label: 角色
|
||||||
placeholder: 请选择角色
|
placeholder: 请选择角色
|
||||||
|
roles_preview:
|
||||||
|
all: 当前所选角色包含所有权限
|
||||||
|
includes: 当前所选角色包含的权限:
|
||||||
detail:
|
detail:
|
||||||
title: 用户详情
|
title: 用户详情
|
||||||
tabs:
|
tabs:
|
||||||
|
|
|
@ -31,6 +31,8 @@ core:
|
||||||
tooltip: 個人中心
|
tooltip: 個人中心
|
||||||
visit_homepage:
|
visit_homepage:
|
||||||
title: 訪問首頁
|
title: 訪問首頁
|
||||||
|
profile:
|
||||||
|
aggregate_role: 聚合角色
|
||||||
uc_sidebar:
|
uc_sidebar:
|
||||||
menu:
|
menu:
|
||||||
items:
|
items:
|
||||||
|
@ -40,6 +42,8 @@ core:
|
||||||
operations:
|
operations:
|
||||||
console:
|
console:
|
||||||
tooltip: 管理控制台
|
tooltip: 管理控制台
|
||||||
|
profile:
|
||||||
|
aggregate_role: 聚合角色
|
||||||
dashboard:
|
dashboard:
|
||||||
title: 儀表板
|
title: 儀表板
|
||||||
actions:
|
actions:
|
||||||
|
@ -966,6 +970,9 @@ core:
|
||||||
role:
|
role:
|
||||||
label: 角色
|
label: 角色
|
||||||
placeholder: 請選擇角色
|
placeholder: 請選擇角色
|
||||||
|
roles_preview:
|
||||||
|
all: 目前所選角色包含所有權限
|
||||||
|
includes: 目前選定角色所包含的權限:
|
||||||
detail:
|
detail:
|
||||||
title: 用戶詳情
|
title: 用戶詳情
|
||||||
tabs:
|
tabs:
|
||||||
|
|
|
@ -2,15 +2,18 @@
|
||||||
import { RoutesMenu } from "@/components/menu/RoutesMenu";
|
import { RoutesMenu } from "@/components/menu/RoutesMenu";
|
||||||
import { useRouteMenuGenerator } from "@/composables/use-route-menu-generator";
|
import { useRouteMenuGenerator } from "@/composables/use-route-menu-generator";
|
||||||
import { rbacAnnotations } from "@/constants/annotations";
|
import { rbacAnnotations } from "@/constants/annotations";
|
||||||
|
import { SUPER_ROLE_NAME } from "@/constants/constants";
|
||||||
import { useUserStore } from "@/stores/user";
|
import { useUserStore } from "@/stores/user";
|
||||||
import { coreMenuGroups } from "@console/router/constant";
|
import { coreMenuGroups } from "@console/router/constant";
|
||||||
import {
|
import {
|
||||||
Dialog,
|
Dialog,
|
||||||
|
IconArrowDownLine,
|
||||||
IconLogoutCircleRLine,
|
IconLogoutCircleRLine,
|
||||||
IconMore,
|
IconMore,
|
||||||
IconSettings3Line,
|
IconSettings3Line,
|
||||||
IconUserSettings,
|
IconShieldUser,
|
||||||
VAvatar,
|
VAvatar,
|
||||||
|
VDropdown,
|
||||||
VTag,
|
VTag,
|
||||||
} from "@halo-dev/components";
|
} from "@halo-dev/components";
|
||||||
import {
|
import {
|
||||||
|
@ -86,6 +89,12 @@ onMounted(() => {
|
||||||
});
|
});
|
||||||
|
|
||||||
const disallowAccessConsole = computed(() => {
|
const disallowAccessConsole = computed(() => {
|
||||||
|
if (
|
||||||
|
currentRoles?.value?.some((role) => role.metadata.name === SUPER_ROLE_NAME)
|
||||||
|
) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
const hasDisallowAccessConsoleRole = currentRoles?.value?.some((role) => {
|
const hasDisallowAccessConsoleRole = currentRoles?.value?.some((role) => {
|
||||||
return (
|
return (
|
||||||
role.metadata.annotations?.[rbacAnnotations.DISALLOW_ACCESS_CONSOLE] ===
|
role.metadata.annotations?.[rbacAnnotations.DISALLOW_ACCESS_CONSOLE] ===
|
||||||
|
@ -121,7 +130,7 @@ const disallowAccessConsole = computed(() => {
|
||||||
<VAvatar
|
<VAvatar
|
||||||
:src="currentUser?.spec.avatar"
|
:src="currentUser?.spec.avatar"
|
||||||
:alt="currentUser?.spec.displayName"
|
:alt="currentUser?.spec.displayName"
|
||||||
size="md"
|
size="sm"
|
||||||
circle
|
circle
|
||||||
></VAvatar>
|
></VAvatar>
|
||||||
</div>
|
</div>
|
||||||
|
@ -129,10 +138,10 @@ const disallowAccessConsole = computed(() => {
|
||||||
<div class="flex text-sm font-medium">
|
<div class="flex text-sm font-medium">
|
||||||
{{ currentUser?.spec.displayName }}
|
{{ currentUser?.spec.displayName }}
|
||||||
</div>
|
</div>
|
||||||
<div v-if="currentRoles?.[0]" class="flex">
|
<div v-if="currentRoles?.length" class="flex mt-1">
|
||||||
<VTag>
|
<VTag v-if="currentRoles.length === 1">
|
||||||
<template #leftIcon>
|
<template #leftIcon>
|
||||||
<IconUserSettings />
|
<IconShieldUser />
|
||||||
</template>
|
</template>
|
||||||
{{
|
{{
|
||||||
currentRoles[0].metadata.annotations?.[
|
currentRoles[0].metadata.annotations?.[
|
||||||
|
@ -140,6 +149,41 @@ const disallowAccessConsole = computed(() => {
|
||||||
] || currentRoles[0].metadata.name
|
] || currentRoles[0].metadata.name
|
||||||
}}
|
}}
|
||||||
</VTag>
|
</VTag>
|
||||||
|
<VDropdown v-else>
|
||||||
|
<div class="flex gap-1">
|
||||||
|
<VTag>
|
||||||
|
<template #leftIcon>
|
||||||
|
<IconShieldUser />
|
||||||
|
</template>
|
||||||
|
{{ $t("core.uc_sidebar.profile.aggregate_role") }}
|
||||||
|
</VTag>
|
||||||
|
<IconArrowDownLine />
|
||||||
|
</div>
|
||||||
|
<template #popper>
|
||||||
|
<div class="p-1">
|
||||||
|
<h2
|
||||||
|
class="text-gray-600 text-sm font-semibold border-b border-gray-100 pb-1.5"
|
||||||
|
>
|
||||||
|
{{ $t("core.uc_sidebar.profile.aggregate_role") }}
|
||||||
|
</h2>
|
||||||
|
<div class="flex gap-2 flex-wrap mt-2">
|
||||||
|
<VTag
|
||||||
|
v-for="role in currentRoles"
|
||||||
|
:key="role.metadata.name"
|
||||||
|
>
|
||||||
|
<template #leftIcon>
|
||||||
|
<IconShieldUser />
|
||||||
|
</template>
|
||||||
|
{{
|
||||||
|
role.metadata.annotations?.[
|
||||||
|
rbacAnnotations.DISPLAY_NAME
|
||||||
|
] || role.metadata.name
|
||||||
|
}}
|
||||||
|
</VTag>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</VDropdown>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex items-center gap-1">
|
<div class="flex items-center gap-1">
|
||||||
|
@ -276,6 +320,7 @@ const disallowAccessConsole = computed(() => {
|
||||||
|
|
||||||
.profile-placeholder {
|
.profile-placeholder {
|
||||||
height: 70px;
|
height: 70px;
|
||||||
|
flex: none;
|
||||||
|
|
||||||
.current-profile {
|
.current-profile {
|
||||||
height: 70px;
|
height: 70px;
|
||||||
|
|
|
@ -5,11 +5,12 @@ import type { DetailedUser, ListedAuthProvider } from "@halo-dev/api-client";
|
||||||
import { consoleApiClient } from "@halo-dev/api-client";
|
import { consoleApiClient } from "@halo-dev/api-client";
|
||||||
import {
|
import {
|
||||||
Dialog,
|
Dialog,
|
||||||
IconUserSettings,
|
IconShieldUser,
|
||||||
VAlert,
|
VAlert,
|
||||||
VButton,
|
VButton,
|
||||||
VDescription,
|
VDescription,
|
||||||
VDescriptionItem,
|
VDescriptionItem,
|
||||||
|
VSpace,
|
||||||
VTag,
|
VTag,
|
||||||
} from "@halo-dev/components";
|
} from "@halo-dev/components";
|
||||||
import { useQuery } from "@tanstack/vue-query";
|
import { useQuery } from "@tanstack/vue-query";
|
||||||
|
@ -133,15 +134,17 @@ const emailVerifyModal = ref(false);
|
||||||
:label="$t('core.uc_profile.detail.fields.roles')"
|
:label="$t('core.uc_profile.detail.fields.roles')"
|
||||||
class="!px-2"
|
class="!px-2"
|
||||||
>
|
>
|
||||||
|
<VSpace>
|
||||||
<VTag v-for="role in user?.roles" :key="role.metadata.name">
|
<VTag v-for="role in user?.roles" :key="role.metadata.name">
|
||||||
<template #leftIcon>
|
<template #leftIcon>
|
||||||
<IconUserSettings />
|
<IconShieldUser />
|
||||||
</template>
|
</template>
|
||||||
{{
|
{{
|
||||||
role.metadata.annotations?.[rbacAnnotations.DISPLAY_NAME] ||
|
role.metadata.annotations?.[rbacAnnotations.DISPLAY_NAME] ||
|
||||||
role.metadata.name
|
role.metadata.name
|
||||||
}}
|
}}
|
||||||
</VTag>
|
</VTag>
|
||||||
|
</VSpace>
|
||||||
</VDescriptionItem>
|
</VDescriptionItem>
|
||||||
<VDescriptionItem
|
<VDescriptionItem
|
||||||
:label="$t('core.uc_profile.detail.fields.bio')"
|
:label="$t('core.uc_profile.detail.fields.bio')"
|
||||||
|
|
Loading…
Reference in New Issue