diff --git a/backup/hak-akses-keycloak/HakAkses.vue b/backup/hak-akses-keycloak/HakAkses.vue new file mode 100644 index 0000000..c5e5003 --- /dev/null +++ b/backup/hak-akses-keycloak/HakAkses.vue @@ -0,0 +1,520 @@ + + + + + \ No newline at end of file diff --git a/backup/hak-akses-keycloak/entities.get.ts b/backup/hak-akses-keycloak/entities.get.ts new file mode 100644 index 0000000..429344a --- /dev/null +++ b/backup/hak-akses-keycloak/entities.get.ts @@ -0,0 +1,93 @@ +// server/api/hak-akses/entities.get.ts +export default defineEventHandler(async (event) => { + console.log("📥 Fetching Keycloak entities (roles & groups)"); + + // Get session from session store + const { getSessionFromCookie } = await import('~/server/utils/sessionStore'); + const session = await getSessionFromCookie(event); + + if (!session) { + throw createError({ + statusCode: 401, + statusMessage: "Session expired or not found", + }); + } + + const config = useRuntimeConfig(); + const accessToken = session.accessToken; + + if (!accessToken) { + throw createError({ + statusCode: 401, + statusMessage: "No access token found in session", + }); + } + + try { + const issuerUrl = new URL(config.keycloakIssuer); + const realm = issuerUrl.pathname.split('/').filter(Boolean).pop() || 'master'; + const adminBaseUrl = `${config.keycloakIssuer.replace('/realms/' + realm, '')}/admin/realms/${realm}`; + + // 1. Fetch Realm Roles + const rolesResponse = await fetch(`${adminBaseUrl}/roles`, { + method: 'GET', + headers: { + 'Authorization': `Bearer ${accessToken}`, + 'Content-Type': 'application/json', + }, + }); + + if (!rolesResponse.ok) { + throw new Error(`Failed to fetch roles: ${rolesResponse.status}`); + } + + const roles = await rolesResponse.json(); + // Filter out potential client-specific roles if needed, keeping realm roles + const realmRoles = roles.filter((r: any) => !r.clientRole); + + // 2. Fetch Groups + const groupsResponse = await fetch(`${adminBaseUrl}/groups`, { + method: 'GET', + headers: { + 'Authorization': `Bearer ${accessToken}`, + 'Content-Type': 'application/json', + }, + }); + + if (!groupsResponse.ok) { + throw new Error(`Failed to fetch groups: ${groupsResponse.status}`); + } + + const groups = await groupsResponse.json(); + + // 3. For each role and group, we'll need to fetch members to show the count + // NOTE: This might be expensive if there are many roles/groups. + // For now, let's just return the list and we'll fetch members on demand in the UI or in a separate task. + // To be efficient, we'll only return basic info here. + + return { + success: true, + data: { + roles: realmRoles.map((r: any) => ({ + id: r.id, + name: r.name, + description: r.description || '', + type: 'role' + })), + groups: groups.map((g: any) => ({ + id: g.id, + name: g.name, + path: g.path, + type: 'group' + })) + } + }; + + } catch (error: any) { + console.error("❌ Error fetching Keycloak entities:", error); + throw createError({ + statusCode: 500, + statusMessage: error.message || "Failed to fetch Keycloak entities", + }); + } +}); diff --git a/backup/hak-akses-keycloak/members.get.ts b/backup/hak-akses-keycloak/members.get.ts new file mode 100644 index 0000000..2e7e709 --- /dev/null +++ b/backup/hak-akses-keycloak/members.get.ts @@ -0,0 +1,74 @@ +// server/api/hak-akses/members.get.ts +export default defineEventHandler(async (event) => { + const query = getQuery(event); + const { type, name, id } = query; + + if (!type || (!name && !id)) { + throw createError({ + statusCode: 400, + statusMessage: "Type and (Name or ID) are required", + }); + } + + // Get session + const { getSessionFromCookie } = await import('~/server/utils/sessionStore'); + const session = await getSessionFromCookie(event); + + if (!session) { + throw createError({ + statusCode: 401, + statusMessage: "Session expired", + }); + } + + const config = useRuntimeConfig(); + const accessToken = session.accessToken; + + try { + const issuerUrl = new URL(config.keycloakIssuer); + const realm = issuerUrl.pathname.split('/').filter(Boolean).pop() || 'master'; + const adminBaseUrl = `${config.keycloakIssuer.replace('/realms/' + realm, '')}/admin/realms/${realm}`; + + let url = ''; + if (type === 'role') { + url = `${adminBaseUrl}/roles/${name}/users`; + } else if (type === 'group') { + url = `${adminBaseUrl}/groups/${id}/members`; + } else { + throw new Error("Invalid type"); + } + + const response = await fetch(url, { + method: 'GET', + headers: { + 'Authorization': `Bearer ${accessToken}`, + 'Content-Type': 'application/json', + }, + }); + + if (!response.ok) { + throw new Error(`Failed to fetch members: ${response.status}`); + } + + const members = await response.json(); + + return { + success: true, + data: members.map((m: any) => ({ + id: m.id, + username: m.username, + email: m.email, + firstName: m.firstName, + lastName: m.lastName, + name: `${m.firstName || ''} ${m.lastName || ''}`.trim() || m.username + })) + }; + + } catch (error: any) { + console.error("❌ Error fetching members:", error); + throw createError({ + statusCode: 500, + statusMessage: error.message || "Failed to fetch members", + }); + } +}); diff --git a/backup/hak-akses-keycloak/useHakAkses.ts b/backup/hak-akses-keycloak/useHakAkses.ts new file mode 100644 index 0000000..9eb231d --- /dev/null +++ b/backup/hak-akses-keycloak/useHakAkses.ts @@ -0,0 +1,113 @@ +// composables/useHakAkses.ts +// Composable for handling user permissions/access based on hakAkses +import { useAuth } from "~/composables/useAuth"; +import type { HakAkses } from "~/types/setting"; + +export const useHakAkses = () => { + const { user, checkAuth } = useAuth(); + + /** + * Get all pages that user has access to based on their roles + */ + const getAllowedPages = async (): Promise => { + // Ensure user is loaded + if (!user.value) { + await checkAuth(); + } + + const currentUser = user.value; + + if (!currentUser) { + return []; + } + + // Get roles and groups from multiple possible sources in User object + const roles = [ + ...(currentUser.roles || []), + ...((currentUser as any).realm_access?.roles || []), + ...((currentUser as any).resource_access?.['web-antrean']?.roles || []) + ]; + + const groups = (currentUser as any).groups || []; + + // Combine everything the user belongs to + const entities = [...new Set([...roles, ...groups])]; + + if (entities.length === 0) { + return []; + } + + try { + // Fetch all hak akses data + const response = await $fetch<{ success: boolean, data: HakAkses[] }>('/api/hak-akses'); + + if (response && response.success && Array.isArray(response.data)) { + const hakAksesList = response.data; + + // Filter hak akses that match user's entities and are active + const userHakAkses = hakAksesList.filter((hakAkses) => + entities.includes(hakAkses.namaHakAkses) && + hakAkses.status === 'aktif' + ); + + // Combine all pages from all matching hak akses + const allPages = userHakAkses.reduce((pages: string[], hakAkses) => { + if (hakAkses.pages && Array.isArray(hakAkses.pages)) { + return [...pages, ...hakAkses.pages]; + } + return pages; + }, []); + + // Remove duplicates + return [...new Set(allPages)]; + } + + return []; + } catch (error) { + console.error('Error fetching allowed pages:', error); + return []; + } + }; + + /** + * Check if user has access to a specific page + */ + const hasPageAccess = async (pagePath: string): Promise => { + const allowedPages = await getAllowedPages(); + return allowedPages.includes(pagePath); + }; + + /** + * Check if user has access to any page in a list + */ + const hasAnyPageAccess = async (pagePaths: string[]): Promise => { + const allowedPages = await getAllowedPages(); + return pagePaths.some(path => allowedPages.includes(path)); + }; + + const allHakAksesData = ref([]); + const isLoading = ref(false); + + const fetchHakAkses = async () => { + isLoading.value = true; + try { + const response = await $fetch<{ success: boolean, data: HakAkses[] }>('/api/hak-akses'); + if (response && response.success) { + allHakAksesData.value = response.data; + } + } catch (error) { + console.error('Error fetching hak akses:', error); + } finally { + isLoading.value = false; + } + }; + + return { + allHakAksesData, + isLoading, + fetchHakAkses, + getAllowedPages, + hasPageAccess, + hasAnyPageAccess + }; +}; diff --git a/components/HakAkses/EditHakAkses.vue b/components/HakAkses/EditHakAkses.vue index df08103..4c1a3d8 100644 --- a/components/HakAkses/EditHakAkses.vue +++ b/components/HakAkses/EditHakAkses.vue @@ -1,457 +1,110 @@ \ No newline at end of file diff --git a/composables/useHakAkses.ts b/composables/useHakAkses.ts new file mode 100644 index 0000000..b120c03 --- /dev/null +++ b/composables/useHakAkses.ts @@ -0,0 +1,117 @@ +// composables/useHakAkses.ts +// Composable for handling user permissions/access based on hakAkses +import { useAuth } from "~/composables/useAuth"; +import type { HakAkses } from "~/types/setting"; + +export const useHakAkses = () => { + const { user, checkAuth } = useAuth(); + + /** + * Get all pages that user has access to based on their roles + */ + const getAllowedPages = async (): Promise => { + // Ensure user is loaded + if (!user.value) { + await checkAuth(); + } + + const currentUser = user.value; + + if (!currentUser) { + return []; + } + + // Get roles and groups from multiple possible sources in User object + const roles = [ + ...(currentUser.roles || []), + ...((currentUser as any).realm_access?.roles || []), + ...((currentUser as any).resource_access?.['web-antrean']?.roles || []) + ]; + + const groups = (currentUser as any).groups || []; + + // Combine everything the user belongs to: Roles, Groups, and their own Username + const entities = [...new Set([ + (currentUser as any).namaUser, // Individual user mapping support + ...roles, + ...groups + ])].filter(Boolean); + + if (entities.length === 0) { + return []; + } + + try { + // Fetch all hak akses data + const response = await $fetch<{ success: boolean, data: HakAkses[] }>('/api/hak-akses'); + + if (response && response.success && Array.isArray(response.data)) { + const hakAksesList = response.data; + + // Filter hak akses that match user's entities and are active + const userHakAkses = hakAksesList.filter((hakAkses) => + entities.includes(hakAkses.namaHakAkses) && + hakAkses.status === 'aktif' + ); + + // Combine all pages from all matching hak akses + const allPages = userHakAkses.reduce((pages: string[], hakAkses) => { + if (hakAkses.pages && Array.isArray(hakAkses.pages)) { + return [...pages, ...hakAkses.pages]; + } + return pages; + }, []); + + // Remove duplicates + return [...new Set(allPages)]; + } + + return []; + } catch (error) { + console.error('Error fetching allowed pages:', error); + return []; + } + }; + + /** + * Check if user has access to a specific page + */ + const hasPageAccess = async (pagePath: string): Promise => { + const allowedPages = await getAllowedPages(); + return allowedPages.includes(pagePath); + }; + + /** + * Check if user has access to any page in a list + */ + const hasAnyPageAccess = async (pagePaths: string[]): Promise => { + const allowedPages = await getAllowedPages(); + return pagePaths.some(path => allowedPages.includes(path)); + }; + + const allHakAksesData = ref([]); + const isLoading = ref(false); + + const fetchHakAkses = async () => { + isLoading.value = true; + try { + const response = await $fetch<{ success: boolean, data: HakAkses[] }>('/api/hak-akses'); + if (response && response.success) { + allHakAksesData.value = response.data; + } + } catch (error) { + console.error('Error fetching hak akses:', error); + } finally { + isLoading.value = false; + } + }; + + return { + allHakAksesData, + isLoading, + fetchHakAkses, + getAllowedPages, + hasPageAccess, + hasAnyPageAccess + }; +}; diff --git a/layouts/default.vue b/layouts/default.vue index 29a32c4..9a3ab7d 100644 --- a/layouts/default.vue +++ b/layouts/default.vue @@ -20,7 +20,7 @@ import { useNavItemsStore } from '~/stores/navItems1'; import { useAuth } from "~/composables/useAuth"; definePageMeta({ - middleware: ['auth', 'permissions'] + middleware: ['auth', 'checkPageAccess'] }) // State for controlling the sidebar @@ -30,221 +30,26 @@ const rail = ref(true); const navItemsStore = useNavItemsStore(); const { user, checkAuth } = useAuth(); -interface NavItem { - id: number; - name: string; - path: string; - icon: string; - children?: NavItem[]; -} +// Navigation will be filtered via navItemsStore using the new hakAkses system -interface BackendPermission { - id: number; - create: boolean; - read: boolean; - update: boolean; - disable: boolean; - delete: boolean; - active: boolean; - pagename: string; - pagesID: number; - level?: number; - sort?: number; - parent?: number; -} - -interface PermissionResponse { - message?: string; - data?: BackendPermission[]; - meta?: { - count: number; - total: number; - }; - error?: string; -} - -// Cache for API permissions -const apiPermissions = ref([]); -const currentUserRoles = ref([]); -const currentUserGroups = ref([]); - -// Get current user data with roles and groups -const fetchCurrentUserData = async () => { - try { - const userData = await $fetch('/api/users/current'); - currentUserRoles.value = [ - ...(userData.realmRoles || []), - ...(userData.roles || []), - ]; - - // Extract groups from paths (e.g., "/Instalasi STIM/Devops/Superadmin" -> "STIM") - const groups: string[] = []; - (userData.groups || []).forEach((g: string) => { - const parts = g.split('/').filter(Boolean); - if (parts.length > 1) { - groups.push(parts[1]); // Get second part as group name - } else if (parts.length === 1) { - groups.push(parts[0]); - } - }); - currentUserGroups.value = groups; - - return { roles: currentUserRoles.value, groups: currentUserGroups.value }; - } catch (error) { - console.error('Error fetching current user data:', error); - return { roles: [], groups: [] }; - } -}; - -// Fetch permissions from backend API (only for nav filtering, not for saving) -// Saving is now handled by permissions middleware -const fetchPermissionsFromAPI = async () => { - const { roles, groups } = await fetchCurrentUserData(); - - if (roles.length === 0 || groups.length === 0) { - console.warn('No roles or groups found for current user'); - return; - } - - // Use first role and first group (or combine as needed) - const primaryRole = roles[0] || ''; - const primaryGroup = groups[0] || ''; - - if (!primaryRole || !primaryGroup) { - return; - } - - try { - const response = await $fetch('/api/permission', { - query: { - roles: primaryRole, - groups: primaryGroup, - }, - }); - - if (response && response.data && Array.isArray(response.data)) { - apiPermissions.value = response.data; - // Note: Auto-save to allHakAksesData is now handled by permissions middleware - } - } catch (error) { - console.error('Error fetching permissions from API:', error); - // Fallback to local storage if API fails - apiPermissions.value = []; - } -}; - -const filteredNavItems = computed(() => { - // If no API permissions, check local storage as fallback - if (apiPermissions.value.length === 0) { - const hakAksesData = useLocalStorage('allHakAksesData', []); - const roleCandidates = [ - ...(user.value?.roles || []), - ...(user.value?.realm_access?.roles || []), - ].map((role) => role.toLowerCase()); - - const localPermission = hakAksesData.value.find((item) => - roleCandidates.includes(item.role?.toLowerCase() || item.namaTipeUser?.toLowerCase()) - ); - - if (localPermission) { - const permissionMap = new Map( - localPermission.hakAksesMenu.map((menu: any) => [menu.name.toLowerCase(), menu]) - ); - - const applyFilter = (items: NavItem[]): NavItem[] => { - return items - .map((item) => { - const menuPerm = permissionMap.get(item.name.toLowerCase()); - const filteredChildren = item.children ? applyFilter(item.children) : []; - const allowThis = menuPerm ? (menuPerm as any).canAccess : false; - const hasChildren = filteredChildren.length > 0; - - if (!allowThis && !hasChildren) return null; - - return { - ...item, - ...(hasChildren ? { children: filteredChildren } : {}), - }; - }) - .filter((item): item is NavItem => item !== null); - }; - - return applyFilter(navItemsStore.navItems) as any[]; - } - - // If no permissions found, show all items - return navItemsStore.navItems; - } - - // Use API permissions to filter - const permissionMap = new Map( - apiPermissions.value.map((perm) => [perm.pagename.toLowerCase(), perm]) - ); - - // Mapping untuk pagename dari API ke nama menu di sidebar - const pagenameToMenuMapping: Record = { - 'halaman utama': ['dashboard', 'halaman utama'], - 'pengaturan': ['master data'], - 'halaman': ['master data'], - 'dashboard': ['dashboard'], - }; - - const applyFilter = (items: NavItem[]): NavItem[] => { - return items - .map((item) => { - // Try to match by pagename or menu name - let perm = permissionMap.get(item.name.toLowerCase()); - - // If no direct match, try fuzzy matching - if (!perm) { - perm = Array.from(permissionMap.values()).find(p => { - const pagenameLower = p.pagename?.toLowerCase() || ''; - const menuNameLower = item.name.toLowerCase(); - - // Direct match - if (pagenameLower === menuNameLower) return true; - - // Contains match - if (pagenameLower.includes(menuNameLower) || menuNameLower.includes(pagenameLower)) { - return true; - } - - // Check mapping - const mappedMenus = pagenameToMenuMapping[pagenameLower]; - if (mappedMenus && mappedMenus.some(m => m === menuNameLower)) { - return true; - } - - return false; - }); - } - - const filteredChildren = item.children ? applyFilter(item.children) : []; - const allowThis = perm ? (perm.active || (perm as any).read) : false; - const hasChildren = filteredChildren.length > 0; - - // If permission allows and has children, show item with filtered children - // If permission allows but no children, show item - // If no permission but has allowed children, show item with children - if (!allowThis && !hasChildren) return null; - - return { - ...item, - ...(hasChildren ? { children: filteredChildren } : {}), - }; - }) - .filter((item): item is NavItem => item !== null); - }; - - return applyFilter(navItemsStore.navItems); -}); +const filteredNavItems = computed(() => navItemsStore.filteredNavItems); onMounted(async () => { await checkAuth(); - // DISABLED: Auto-fetch permissions is now disabled - // Permissions should only be fetched manually via button click in HakAkses page - // await fetchPermissionsFromAPI(); + if (user.value) { + await navItemsStore.refreshNavItems(); + } }); + +// Watch for user changes to refresh navItems +watch(() => user.value, async (newUser) => { + if (newUser) { + await navItemsStore.refreshNavItems(); + } else { + // Optionally reset navItems when logged out + navItemsStore.filteredNavItems = []; + } +}, { deep: true }); \ No newline at end of file diff --git a/pages/Setting/MasterAnjungan.vue b/pages/Setting/MasterAnjungan.vue index 8a22072..fbfe3c1 100644 --- a/pages/Setting/MasterAnjungan.vue +++ b/pages/Setting/MasterAnjungan.vue @@ -248,6 +248,9 @@