feat: implement role-based access control (RBAC) management system with Keycloak integration and dynamic page permission configuration
This commit is contained in:
No files matched your search
@@ -4,29 +4,76 @@
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="text-left font-weight-bold">Halaman</th>
|
||||
<th class="text-center font-weight-bold" style="width: 120px;">Akses</th>
|
||||
<th class="text-center font-weight-bold" style="width: 80px;">Access</th>
|
||||
<th class="text-center font-weight-bold" style="width: 80px;">View</th>
|
||||
<th class="text-center font-weight-bold" style="width: 80px;">Add</th>
|
||||
<th class="text-center font-weight-bold" style="width: 80px;">Edit</th>
|
||||
<th class="text-center font-weight-bold" style="width: 80px;">Delete</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<template v-for="item in flatNavItems" :key="item.path || item.name">
|
||||
<tr :class="{ 'bg-grey-lighten-4': !item.path }">
|
||||
<template v-for="item in flatNavItems" :key="item.menuKey || item.name">
|
||||
<tr :class="{ 'bg-grey-lighten-4': !item.menuKey }">
|
||||
<td :style="{ paddingLeft: item.level * 24 + 'px' }">
|
||||
<div class="d-flex align-center">
|
||||
<v-icon :icon="item.icon" size="small" class="mr-2" color="grey"></v-icon>
|
||||
<span :class="{ 'font-weight-bold': !item.path }">{{ item.name }}</span>
|
||||
<span :class="{ 'font-weight-bold': !item.menuKey }">{{ item.name }}</span>
|
||||
</div>
|
||||
</td>
|
||||
<td class="text-center">
|
||||
<v-checkbox
|
||||
v-if="item.path"
|
||||
:model-value="isAllowed(item.path)"
|
||||
@update:model-value="toggleAccess(item, !!$event)"
|
||||
v-if="item.menuKey"
|
||||
:model-value="getPermission(item.menuKey, 'canAccess')"
|
||||
@update:model-value="toggleAccess(item, 'canAccess', !!$event)"
|
||||
hide-details
|
||||
density="compact"
|
||||
color="primary"
|
||||
class="d-inline-flex"
|
||||
></v-checkbox>
|
||||
</td>
|
||||
<td class="text-center">
|
||||
<v-checkbox
|
||||
v-if="item.menuKey"
|
||||
:model-value="getPermission(item.menuKey, 'canView')"
|
||||
@update:model-value="toggleAccess(item, 'canView', !!$event)"
|
||||
hide-details
|
||||
density="compact"
|
||||
color="primary"
|
||||
class="d-inline-flex"
|
||||
></v-checkbox>
|
||||
</td>
|
||||
<td class="text-center">
|
||||
<v-checkbox
|
||||
v-if="item.menuKey"
|
||||
:model-value="getPermission(item.menuKey, 'canAdd')"
|
||||
@update:model-value="toggleAccess(item, 'canAdd', !!$event)"
|
||||
hide-details
|
||||
density="compact"
|
||||
color="primary"
|
||||
class="d-inline-flex"
|
||||
></v-checkbox>
|
||||
</td>
|
||||
<td class="text-center">
|
||||
<v-checkbox
|
||||
v-if="item.menuKey"
|
||||
:model-value="getPermission(item.menuKey, 'canEdit')"
|
||||
@update:model-value="toggleAccess(item, 'canEdit', !!$event)"
|
||||
hide-details
|
||||
density="compact"
|
||||
color="primary"
|
||||
class="d-inline-flex"
|
||||
></v-checkbox>
|
||||
</td>
|
||||
<td class="text-center">
|
||||
<v-checkbox
|
||||
v-if="item.menuKey"
|
||||
:model-value="getPermission(item.menuKey, 'canDelete')"
|
||||
@update:model-value="toggleAccess(item, 'canDelete', !!$event)"
|
||||
hide-details
|
||||
density="compact"
|
||||
color="primary"
|
||||
class="d-inline-flex"
|
||||
></v-checkbox>
|
||||
<v-icon v-else icon="mdi-folder-open" size="small" color="grey-lighten-1"></v-icon>
|
||||
</td>
|
||||
</tr>
|
||||
</template>
|
||||
@@ -37,24 +84,24 @@
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue';
|
||||
import { useNavItemsStore } from '~/stores/navItems1';
|
||||
import { defaultNavItems } from '~/stores/navItems1';
|
||||
import type { HakAksesMenu } from '~/server/utils/schemas/permissionSchema';
|
||||
|
||||
const props = defineProps({
|
||||
pages: {
|
||||
type: Array as () => any[], // Be flexible with legacy data
|
||||
hakAksesMenu: {
|
||||
type: Array as () => HakAksesMenu[],
|
||||
default: () => []
|
||||
}
|
||||
});
|
||||
|
||||
const emit = defineEmits(['update:pages']);
|
||||
|
||||
const navItemsStore = useNavItemsStore();
|
||||
const emit = defineEmits(['update:hakAksesMenu']);
|
||||
|
||||
interface FlatNavItem {
|
||||
name: string;
|
||||
path: string;
|
||||
icon: string;
|
||||
level: number;
|
||||
menuKey?: string;
|
||||
}
|
||||
|
||||
const flatNavItems = computed(() => {
|
||||
@@ -66,7 +113,8 @@ const flatNavItems = computed(() => {
|
||||
name: item.name,
|
||||
path: item.path,
|
||||
icon: item.icon,
|
||||
level
|
||||
level,
|
||||
menuKey: item.menuKey
|
||||
});
|
||||
if (item.children && item.children.length > 0) {
|
||||
walk(item.children, level + 1);
|
||||
@@ -74,32 +122,48 @@ const flatNavItems = computed(() => {
|
||||
});
|
||||
};
|
||||
|
||||
walk(navItemsStore.getNavItems);
|
||||
walk(defaultNavItems);
|
||||
return result;
|
||||
});
|
||||
|
||||
const isAllowed = (path: string) => {
|
||||
if (!props.pages || !Array.isArray(props.pages)) return false;
|
||||
|
||||
return props.pages.some(p => {
|
||||
if (typeof p === 'string') return p === path;
|
||||
if (p && typeof p === 'object' && p.path) return p.path === path;
|
||||
return false;
|
||||
});
|
||||
const getPermission = (menuKey: string, action: keyof HakAksesMenu): boolean => {
|
||||
if (!props.hakAksesMenu || !Array.isArray(props.hakAksesMenu)) return false;
|
||||
const menuConfig = props.hakAksesMenu.find((p: HakAksesMenu) => p.menuKey === menuKey);
|
||||
if (!menuConfig) return false;
|
||||
return !!menuConfig[action];
|
||||
};
|
||||
|
||||
const toggleAccess = (item: FlatNavItem, allowed: boolean) => {
|
||||
let newPages = [...props.pages];
|
||||
const toggleAccess = (item: FlatNavItem, action: keyof HakAksesMenu, value: boolean) => {
|
||||
if (!item.menuKey) return;
|
||||
|
||||
if (allowed) {
|
||||
if (!newPages.includes(item.path)) {
|
||||
newPages.push(item.path);
|
||||
// Clone current array
|
||||
let newMenus = JSON.parse(JSON.stringify(props.hakAksesMenu || []));
|
||||
|
||||
let menuIndex = newMenus.findIndex((m: HakAksesMenu) => m.menuKey === item.menuKey);
|
||||
|
||||
if (menuIndex === -1) {
|
||||
if (value) {
|
||||
newMenus.push({
|
||||
menuKey: item.menuKey,
|
||||
name: item.name,
|
||||
canAccess: action === 'canAccess' ? true : false,
|
||||
canView: action === 'canView' ? true : false,
|
||||
canAdd: action === 'canAdd' ? true : false,
|
||||
canEdit: action === 'canEdit' ? true : false,
|
||||
canDelete: action === 'canDelete' ? true : false
|
||||
});
|
||||
}
|
||||
} else {
|
||||
newPages = newPages.filter(p => p !== item.path);
|
||||
newMenus[menuIndex][action] = value;
|
||||
// Auto-grant canAccess and canView if Add/Edit/Delete is checked
|
||||
if (value && (action === 'canAdd' || action === 'canEdit' || action === 'canDelete')) {
|
||||
newMenus[menuIndex].canAccess = true;
|
||||
newMenus[menuIndex].canView = true;
|
||||
}
|
||||
// If all are false, maybe remove it, but keeping it is fine too
|
||||
}
|
||||
|
||||
emit('update:pages', newPages);
|
||||
emit('update:hakAksesMenu', newMenus);
|
||||
};
|
||||
</script>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user