Add permissions
This commit is contained in:
@@ -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
|
||||
|
||||
34
frontend/src/boot/permissions.ts
Normal file
34
frontend/src/boot/permissions.ts
Normal 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';
|
||||
}
|
||||
}
|
||||
},
|
||||
});
|
||||
});
|
||||
@@ -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: '',
|
||||
|
||||
@@ -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) => {
|
||||
|
||||
@@ -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<{
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>();
|
||||
|
||||
|
||||
@@ -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: '注意',
|
||||
|
||||
@@ -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')},
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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[] = [];
|
||||
|
||||
Reference in New Issue
Block a user