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
@@ -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('/');
|
||||
}
|
||||
});
|
||||
Reference in New Issue
Block a user