feat: implement role-based access control (RBAC) management system with Keycloak integration and dynamic page permission configuration

This commit is contained in:
Fanrouver
2026-07-14 09:41:55 +07:00
parent a5d7338e58
commit 49e0ee2bc4
14 changed files with 737 additions and 163 deletions

No files matched your search

+59 -45
View File
@@ -1,70 +1,84 @@
// middleware/checkPageAccess.ts
// Middleware to check if user has access to the page based on hakAkses
import { defineNuxtRouteMiddleware, navigateTo } from '#app';
import { useAuth } from '~/composables/useAuth';
import { usePermissionStore } from '~/stores/permissionStore';
export default defineNuxtRouteMiddleware(async (to, from) => {
// Skip check for public pages
const publicPaths = ['/LoginPage', '/auth/login', '/index-legacy'];
// index.vue is the debug dashboard, let's keep it accessible for now as requested
if (to.path === '/' || publicPaths.includes(to.path)) {
return;
}
// On server-side, skip access check - let client handle it
// This matches auth.ts behavior and prevents SSR failures when cookie context is missing
if (process.server) {
console.log('⏭️ Server-side: Skipping page access check (will verify on client)');
return;
}
// Import useAuth and useHakAkses
const { user, checkAuth } = useAuth();
const { getAllowedPages } = useHakAkses();
// If user not loaded, try to load
if (!user.value) {
await checkAuth();
}
// If still not authenticated, redirect to login
if (!user.value) {
return navigateTo('/LoginPage');
}
try {
const allowedPages = await getAllowedPages();
const targetPath = to.path.endsWith('/') && to.path.length > 1 ? to.path.slice(0, -1) : to.path;
const targetPathLower = targetPath.toLowerCase();
const toPathLower = to.path.toLowerCase();
// Check if user has access to this page
// We also check against the raw path just in case, case-insensitive
const isAllowed = allowedPages.some(path => {
const normalizedAllowed = path.endsWith('/') && path.length > 1 ? path.slice(0, -1) : path;
const normalizedAllowedLower = normalizedAllowed.toLowerCase();
const pathLower = path.toLowerCase();
return normalizedAllowedLower === targetPathLower || pathLower === toPathLower;
});
if (!isAllowed) {
console.warn(`Access denied to ${to.path}. User allowed pages:`, allowedPages);
// Redirect to first allowed page if available, else stay/error
if (allowedPages.length > 0) {
// If dashboard is allowed, go there, else go to the first allowed one
const dashboardPath = allowedPages.find(p => p === '/' || p === '/dashboard');
return navigateTo(dashboardPath || allowedPages[0]);
} else {
// No access to any page - technically this shouldn't happen if user has roles
console.error('User has roles but no allowed pages found in configuration.');
// For now, allow root as fallback since index.vue is kept
if (to.path === '/') return;
const permissionStore = usePermissionStore();
// Ensure permissions are loaded
if (!permissionStore.isLoaded) {
const roles = [
...(user.value.realm_access?.roles || []),
...(user.value.roles || [])
];
const groups: string[] = [];
const rawGroups = (user.value as any).groups || [];
rawGroups.forEach((g: string) => {
const parts = g.split('/').filter(Boolean);
if (parts.length > 1) {
groups.push(parts[1]);
} else if (parts.length === 1) {
groups.push(parts[0]);
}
});
const primaryRole = roles[0] || '';
const primaryGroup = groups[0] || '';
const username = user.value.preferred_username || user.value.email || user.value.name || '';
await permissionStore.load(primaryRole, primaryGroup, username);
}
let menuKey = "";
if (to.name) {
menuKey = to.name.toString().toLowerCase().replace(/_|-/g, '-');
}
// If no explicit route name, allow it for now.
if (!menuKey) return;
// The previous implementation allowed some pages implicitly.
// If it's not configured, our can() method returns false.
// We should allow access if it's explicitly allowed.
let hasAccess = permissionStore.can(menuKey, 'canAccess');
// Default allow Dashboard for all authenticated users
if (menuKey === 'dashboard') {
hasAccess = true;
}
if (!hasAccess) {
console.warn(`Access denied to ${to.path}. User lacks 'canAccess' for menuKey: ${menuKey}`);
// Find a fallback page that the user CAN access
const fallbackMenu = permissionStore.permissions.find(p => p.canAccess);
if (fallbackMenu && fallbackMenu.menuKey) {
// Note: menuKey might not be a valid path, but if we map routeName to menuKey,
// we can try to navigate to the route name instead.
return navigateTo({ name: fallbackMenu.menuKey });
}
} catch (error) {
console.error('Error checking page access:', error);
// On error, we might want to allow or block. Let's allow but log.
return;
// Final fallback
return navigateTo('/');
}
});