Add permissions

This commit is contained in:
xue jiahao
2024-12-24 22:17:18 +08:00
parent 5ebfd22652
commit a92873b971
11 changed files with 173 additions and 103 deletions

View File

@@ -37,7 +37,8 @@ module.exports = configure(function (/* ctx */) {
// https://v2.quasar.dev/quasar-cli-vite/boot-files
boot: [
'axios',
'error-handler'
'error-handler',
'permissions'
],
// https://v2.quasar.dev/quasar-cli-vite/quasar-config-js#css

View File

@@ -0,0 +1,34 @@
// src/boot/permissions.ts
import { boot } from 'quasar/wrappers';
import { useAuthStore } from 'src/stores/useAuthStore';
import { DirectiveBinding } from 'vue';
export const MenuMapping = {
home: null,
app: null,
version: null,
user: 'user',
role: 'role',
domain: null,
userDomain: null,
};
const store = useAuthStore();
export default boot(({ app }) => {
app.directive('permissions', {
mounted(el: HTMLElement, binding: DirectiveBinding) {
const { value } = binding;
if (!value || store.isSuperAdmin) {
return;
}
if (!store.permissions[value]) {
if (el.parentNode) {
el.parentNode.removeChild(el);
} else {
el.style.display = 'none';
}
}
},
});
});

View File

@@ -1,5 +1,6 @@
<template>
<q-item
v-permissions="permission"
clickable
tag="a"
:target="target?target:'_blank'"
@@ -35,6 +36,7 @@ export interface EssentialLinkProps {
isSeparator?: boolean;
target?:string;
disable?:boolean;
permission?: string
}
withDefaults(defineProps<EssentialLinkProps>(), {
caption: '',

View File

@@ -53,7 +53,7 @@
</template>
<template v-slot:actions="{ row }">
<q-btn round title="解除" flat color="primary" padding="xs" size="1em" :loading="row.isRemoving" icon="person_off" @click="removeShareTo(row)" />
<q-btn round title="解除" flat color="primary" :disable="isActionDisable && isActionDisable(row)" padding="xs" size="1em" :loading="row.isRemoving" icon="person_off" @click="removeShareTo(row)" />
</template>
</sharing-user-list>
@@ -68,7 +68,7 @@
</template>
<script setup lang="ts">
import { ref, watch, onMounted } from 'vue';
import { ref, watch } from 'vue';
import { IDomainOwnerDisplay } from '../../types/DomainTypes';
import { IUser, IUserDisplay } from '../../types/UserTypes';
import { api } from 'boot/axios';
@@ -76,6 +76,7 @@ import { useAuthStore } from 'stores/useAuthStore';
import SharingUserList from 'components/ShareDomain/SharingUserList.vue';
import RoleLabel from 'components/ShareDomain/RoleLabel.vue';
import { Dialog } from 'quasar'
import { IResponse } from 'src/types/BaseTypes';
const authStore = useAuthStore();
@@ -84,7 +85,7 @@ interface Props {
domain: IDomainOwnerDisplay;
dialogTitle: string;
userListTitle: string;
hideSelf: boolean;
isActionDisable?: (user: IUserDisplay) => boolean;
shareApi: (user: IUserDisplay, domain: IDomainOwnerDisplay) => Promise<any>;
removeSharedApi: (user: IUserDisplay, domain: IDomainOwnerDisplay) => Promise<any>;
getSharedApi: (domain: IDomainOwnerDisplay) => Promise<any>;
@@ -205,10 +206,6 @@ const loadShared = async () => {
loading.value = true;
sharedUsersIdSet.clear();
if(props.hideSelf) {
sharedUsersIdSet.add((Number)(authStore.userId));
}
const { data } = await props.getSharedApi(props.domain);
sharedUsers.value = data.data.reduce((arr: IUserDisplayWithShareRole[], item: IUser) => {

View File

@@ -6,8 +6,8 @@
:share-api="shareApi"
:remove-shared-api="removeSharedApi"
:get-shared-api="getSharedApi"
:is-action-disable="(row) => row.id === authStore.userId"
:model-value="modelValue"
hide-self
@update:modelValue="updateModelValue"
@close="close"
/>
@@ -19,6 +19,10 @@ import ShareDomainDialog from 'components/ShareDomain/ShareDomainDialog.vue';
import { IUserDisplay } from '../../types/UserTypes';
import { IDomainOwnerDisplay } from '../../types/DomainTypes';
import { api } from 'boot/axios';
import { useAuthStore } from 'src/stores/useAuthStore';
import { IResponse } from 'src/types/BaseTypes';
const authStore = useAuthStore();
interface Props {
modelValue: boolean;
@@ -35,11 +39,11 @@ async function shareApi(user: IUserDisplay, domain: IDomainOwnerDisplay) {
}
async function removeSharedApi(user: IUserDisplay, domain: IDomainOwnerDisplay) {
return api.delete(`api/managedomain/${domain.id}/${user.id}`);
return api.delete<IResponse>(`api/managedomain/${domain.id}/${user.id}`);
}
async function getSharedApi(domain: IDomainOwnerDisplay) {
return api.get(`/api/managedomainuser/${domain.id}`);
return api.get<IResponse>(`/api/managedomainuser/${domain.id}`);
}
const emit = defineEmits<{

View File

@@ -19,10 +19,6 @@
</q-item-label>
<EssentialLink v-for="link in essentialLinks" :key="link.title" v-bind="link" />
<div v-if="isAdmin()">
<EssentialLink v-for="link in adminLinks" :key="link.title" v-bind="link" />
</div>
<EssentialLink v-for="link in domainLinks" :key="link.title" v-bind="link" />
</q-list>
</q-drawer>
@@ -34,12 +30,13 @@
</template>
<script setup lang="ts">
import { computed, onMounted, reactive } from 'vue';
import { computed, onMounted, reactive, getCurrentInstance } from 'vue';
import EssentialLink, { EssentialLinkProps } from 'components/EssentialLink.vue';
import DomainSelector from 'components/DomainSelector.vue';
import UserInfoButton from 'components/UserInfoButton.vue';
import { useAuthStore } from 'stores/useAuthStore';
import { useRoute } from 'vue-router';
import { MenuMapping } from 'src/boot/permissions';
const authStore = useAuthStore();
const route = useRoute()
@@ -52,7 +49,8 @@ const essentialLinks: EssentialLinkProps[] = reactive([
icon: 'home',
link: '/',
target: '_self',
disable: noDomain
disable: noDomain,
permission: MenuMapping.home
},
// {
// title: 'フローエディター',
@@ -67,7 +65,8 @@ const essentialLinks: EssentialLinkProps[] = reactive([
icon: 'widgets',
link: '/#/app',
target: '_self',
disable: noDomain
disable: noDomain,
permission: MenuMapping.app
},
// {
// title: '条件エディター',
@@ -80,6 +79,41 @@ const essentialLinks: EssentialLinkProps[] = reactive([
title: '',
isSeparator: true
},
// ------------ユーザー-------------
{
title: 'ユーザー管理',
caption: 'ユーザーを管理する',
icon: 'manage_accounts',
link: '/#/user',
target: '_self',
permission: MenuMapping.user
},
{
title: 'ロール管理',
caption: 'ロールを管理する',
icon: 'work',
link: '/#/role',
target: '_self',
permission: MenuMapping.role
},
// ------------ドメイン-------------
{
title: 'ドメイン管理',
caption: 'kintoneのドメイン設定',
icon: 'domain',
link: '/#/domain',
target: '_self',
permission: MenuMapping.domain
},
{
title: 'ドメイン適用',
caption: 'ユーザー使用可能なドメインの設定',
icon: 'assignment_ind',
link: '/#/userDomain',
target: '_self',
id: 'userDomain',
permission: MenuMapping.userDomain
},
// {
// title:'Kintone ポータル',
// caption:'Kintone',
@@ -100,40 +134,6 @@ const essentialLinks: EssentialLinkProps[] = reactive([
// },
]);
const domainLinks: EssentialLinkProps[] = reactive([
{
title: 'ドメイン管理',
caption: 'kintoneのドメイン設定',
icon: 'domain',
link: '/#/domain',
target: '_self'
},
{
title: 'ドメイン適用',
caption: 'ユーザー使用可能なドメインの設定',
icon: 'assignment_ind',
link: '/#/userDomain',
target: '_self'
},
]);
const adminLinks: EssentialLinkProps[] = reactive([
{
title: 'ユーザー管理',
caption: 'ユーザーを管理する',
icon: 'manage_accounts',
link: '/#/user',
target: '_self'
},
{
title: 'ロール管理',
caption: 'ロールを管理する',
icon: 'work',
link: '/#/role',
target: '_self'
},
])
const version = process.env.version;
const productName = process.env.productName;
@@ -142,10 +142,8 @@ onMounted(() => {
});
function toggleLeftDrawer() {
getCurrentInstance();
debugger;
authStore.toggleLeftMenu();
}
function isAdmin(){
const permission = authStore.permissions;
return permission === 'admin'
}
</script>

View File

@@ -27,7 +27,7 @@
:pagination="pagination" >
<template v-slot:top>
<q-btn color="primary" :disable="allLoading || loading || selected?.id == -2" label="追加" @click="showAddRoleDialog" />
<q-btn color="primary" :disable="allLoading || loading || selected?.id == EMPTY_ROLE.id" label="追加" @click="showAddRoleDialog" />
<q-space />
<q-input borderless dense filled debounce="300" v-model="filter" placeholder="検索">
<template v-slot:append>
@@ -132,7 +132,12 @@ const statusFilterOptions = [
const pagination = ref({ sortBy: 'id', descending: true, rowsPerPage: 20 });
const roles = computed(() => userStore.roles.concat(EMPTY_ROLE));
const roles = computed(() => {
if (userStore.roles.length > 0) {
return userStore.roles.concat(EMPTY_ROLE);
}
return userStore.roles;
});
const selected = ref<IRolesDisplay>();

View File

@@ -34,13 +34,14 @@ const routerInstance = createRouter({
export default route(function (/* { store, ssrContext } */) {
routerInstance.beforeEach(async (to) => {
routerInstance.beforeEach(async (to, from) => {
// clear alert on route change
//const alertStore = useAlertStore();
//alertStore.clear();
// redirect to login page if not logged in and trying to access a restricted page
const publicPages = ['/login'];
const loginPage = '/login';
const publicPages = [loginPage];
const authRequired = !publicPages.includes(to.path);
const authStore = useAuthStore();
@@ -49,8 +50,12 @@ export default route(function (/* { store, ssrContext } */) {
return '/login';
}
if (authStore.token && to.path === loginPage) {
return from.path == '/' ? '/' : false;
}
// redirect to domain setting page if no domain exist
const domainPages = [...publicPages, '/domain', '/userDomain', '/user'];
const domainPages = [...publicPages, '/domain', '/userDomain', '/user', '/role'];
if (!authStore.hasDomain && !domainPages.includes(to.path)) {
Dialog.create({
title: '注意',

View File

@@ -18,12 +18,10 @@ const routes: RouteRecordRaw[] = [
children: [
{ path: '', component: () => import('pages/IndexPage.vue') },
{ path: 'ruleEditor', component: () => import('pages/RuleEditor.vue') },
{ path: 'test', component: () => import('pages/testQursar.vue') },
{ path: 'flow', component: () => import('pages/testFlow.vue') },
{ path: 'FlowChartTest', component: () => import('pages/FlowChartTest.vue') },
{ path: 'flowEditor', component: () => import('pages/FlowEditorPage.vue') },
// { path: 'FlowChart', component: () => import('pages/FlowChart.vue') },
{ path: 'right', component: () => import('pages/testRight.vue') },
{ path: 'domain', component: () => import('pages/TenantDomain.vue') },
{ path: 'userdomain', component: () => import('pages/UserDomain.vue')},
{ path: 'user', component: () => import('pages/UserManagement.vue')},

View File

@@ -1,27 +1,31 @@
import { defineStore } from 'pinia';
import { api } from 'boot/axios';
import { router } from 'src/router';
import { IDomainInfo } from '../types/DomainTypes';
import { IDomain, IDomainInfo } from '../types/DomainTypes';
import { jwtDecode } from 'jwt-decode';
import { useAppStore } from './useAppStore';
import { useUserStore } from './useUserStore';
import { userToUserRolesDisplay, useUserStore } from './useUserStore';
import { IRolesDisplay, IUser, IUserRolesDisplay } from 'src/types/UserTypes';
import { IResponse } from 'src/types/BaseTypes';
interface UserInfo {
firstName: string;
lastName: string;
fullName: string;
email: string;
interface IPermission {
id: number,
menu: string,
function: string,
privilege: string,
link: string
}
type IPermissions = { [key: string]: string };
export interface IUserState {
token?: string;
returnUrl: string;
currentDomain: IDomainInfo;
LeftDrawer: boolean;
userId?: string;
userInfo: UserInfo;
roles:string,
permissions: string;
userInfo: IUserRolesDisplay;
tenant: string;
permissions: IPermissions;
}
export const useAuthStore = defineStore('auth', {
@@ -30,17 +34,22 @@ export const useAuthStore = defineStore('auth', {
returnUrl: '',
LeftDrawer: false,
currentDomain: {} as IDomainInfo,
userId: '',
userInfo: {} as UserInfo,
roles:'',
permissions: '',
userInfo: {} as IUserRolesDisplay,
tenant: '',
permissions: {} as IPermissions
}),
getters: {
toggleLeftDrawer(): boolean {
return this.LeftDrawer;
userId(): number {
return this.userInfo.id;
},
hasDomain(): boolean {
return this.currentDomain.id !== undefined;
},
getRoles(): IRolesDisplay[] {
return this.userInfo.roles;
},
isSuperAdmin(): boolean {
return this.userInfo.isSuperuser;
}
},
actions: {
@@ -56,15 +65,20 @@ export const useAuthStore = defineStore('auth', {
params.append('password', password);
try {
const result = await api.post(`api/token`, params);
console.info(result);
// console.info(result);
this.token = result.data.access_token;
const tokenJson = jwtDecode(result.data.access_token);
this.userId = tokenJson.sub;
this.permissions = (tokenJson as any).permissions==='ALL' ? 'admin': 'user';
const tokenJson = jwtDecode<{sub: number, tenant: string, roles: string}>(result.data.access_token);
this.tenant = tokenJson.tenant;
this.userInfo.id = tokenJson.sub;
this.userInfo.isSuperuser = tokenJson.roles === 'super';
api.defaults.headers['Authorization'] = 'Bearer ' + this.token;
await this.loadCurrentDomain();
this.userInfo = await this.getUserInfo();
router.push(this.returnUrl || '/');
await Promise.all([
this.loadCurrentDomain(),
this.loadUserInfo(),
this.loadPermission()
]);
await router.push(this.returnUrl || '/');
this.returnUrl = '';
return true;
} catch (e) {
console.error(e);
@@ -72,29 +86,40 @@ export const useAuthStore = defineStore('auth', {
}
},
async loadCurrentDomain() {
const resp = await api.get(`api/defaultdomain`);
const resp = await api.get<IResponse<IDomain>>(`api/defaultdomain`);
const activedomain = resp?.data?.data;
this.currentDomain = {
id: activedomain?.id,
domainName: activedomain?.name,
kintoneUrl: activedomain?.url,
};
},
async getUserInfo():Promise<UserInfo>{
const resp = (await api.get(`api/v1/users/me`)).data.data;
return {
firstName: resp.first_name,
lastName: resp.last_name,
fullName: resp.last_name + ' ' + resp.first_name,
email: resp.email,
if (!activedomain) {
this.currentDomain = {} as IDomainInfo;
} else {
this.currentDomain = {
id: activedomain.id,
domainName: activedomain.name,
kintoneUrl: activedomain.url,
};
}
},
logout() {
async loadUserInfo() {
const resp = (await api.get<IResponse<IUser>>(`api/v1/users/me`))?.data?.data;
this.userInfo = userToUserRolesDisplay(resp)
},
async loadPermission() {
this.permissions = {} as IPermissions;
if (this.isSuperAdmin) return;
const resp = (await api.get<IResponse<IPermission[]>>(`api/v1/userpermssions`)).data.data;
resp.forEach((permission) => {
this.permissions[permission.link] = permission.menu;
this.permissions[permission.privilege] = permission.function;
});
},
async logout() {
this.token = '';
this.currentDomain = {} as IDomainInfo; // 清空当前域
useAppStore().reset();
useUserStore().reset();
router.push('/login');
await router.push('/login');
this.tenant = '';
this.userInfo = {} as IUserRolesDisplay;
this.permissions = {} as IPermissions;
},
async setCurrentDomain(domain?: IDomainInfo) {
if (!domain) {

View File

@@ -124,6 +124,7 @@ export function userToUserDisplay(user: IUser): IUserDisplay {
}
export function userToUserRolesDisplay(user: IUser): IUserRolesDisplay {
if (!user) return {} as IUserRolesDisplay;
const userRolesDisplay = userToUserDisplay(user) as IUserRolesDisplay;
const roles: IRolesDisplay[] = [];
const roleIds: number[] = [];