import { defineNuxtRouteMiddleware, navigateTo } from '#app'; import type { RouteLocationNormalized } from 'vue-router'; import { useAuth } from '~/composables/useAuth'; import { useUserMenuStore } from '~/store/userMenu'; export default defineNuxtRouteMiddleware(async (to: RouteLocationNormalized) => { // console.log('🛡️ Auth middleware triggered for:', to.path); // Allow the login page and access denied page without checks if (to.path === '/auth/login' || to.path === '/auth/access-denied') { // console.log('⏭️ Allowing access to auth page:', to.path); return; } // Skip middleware on server-side - auth plugin handles client-side initialization if (process.server) { // console.log('⏭️ Server-side: Skipping auth check (plugin will handle on client)'); return; } // Check for authentication signal from successful login redirect const isAuthRedirect = to.query.authenticated === 'true'; const { user, checkAuth } = useAuth(); // Special handling for authentication redirect from Keycloak callback if (isAuthRedirect) { // console.log('🔐 Authentication redirect detected'); // Give browser a moment to process the cookie from redirect await new Promise(resolve => setTimeout(resolve, 100)); // Verify session was created successfully await checkAuth(); if (user.value) { // console.log('✅ Session verified after redirect:', user.value.name); // Clean up query parameter return navigateTo({ path: to.path, query: {} }, { replace: true }); } else { // console.error('❌ Session verification failed after redirect'); return navigateTo('/auth/login?error=session_failed'); } } // For all other routes, check if user is authenticated // User state should already be populated by auth plugin // Only call checkAuth if user state is not available (edge case) if (!user.value) { // console.log('⚠️ User not in state, verifying session...'); await checkAuth(); } if (user.value) { // console.log('✅ User authenticated:', user.value.name); // Authorization Check: Verify if user has access to the requested path const menuStore = useUserMenuStore(); // Check if user has no access (role inactive) if (menuStore.noAccess) { // console.warn('⛔ User has no access (role inactive or no permissions)'); return navigateTo({ path: '/auth/access-denied', query: { reason: 'no_role_or_inactive' } }, { replace: true }); } // Skip authorization check for certain paths const publicPaths = ['/','/dashboard', '/auth/access-denied']; const isPublicPath = publicPaths.includes(to.path); if (!isPublicPath) { // If menu is not fetched yet, wait for it to load (with timeout) if (!menuStore.menuFetched) { // console.log('⏳ Menu not fetched yet, loading...'); const token = localStorage.getItem('idToken'); if (token && user.value.id) { try { // Try to load menu await menuStore.fetchUserMenu(user.value.id, token); // console.log('✅ Menu loaded in middleware'); } catch (error) { // console.error('❌ Failed to load menu in middleware:', error); // If menu fails to load, deny access by default for security return navigateTo({ path: '/auth/access-denied', query: { path: to.path, reason: 'menu_load_failed' } }, { replace: true }); } } else { // console.error('❌ Cannot load menu: No token or user ID'); return navigateTo('/auth/login'); } } // Now that menu is loaded (or was already loaded), check authorization if (menuStore.hasMenu) { // Extract all eligible paths from user's menu const eligiblePaths = new Set(); menuStore.menuItems.forEach((group) => { if (group.children) { group.children.forEach((item) => { if (item.to) { eligiblePaths.add(item.to); } // Handle nested children if any if (item.children) { item.children.forEach((subItem) => { if (subItem.to) { eligiblePaths.add(subItem.to); } }); } }); } }); // Define path dependencies: child path requires parent path access const pathDependencies: Record = { '/setting/hak-akses/form': ['/setting/hak-akses'], '/antrean/kategori': ['/antrean/list-kategori'], '/antrean/spesialis': ['/antrean/list-spesialis'], '/antrean/subspesialis': ['/antrean/list-spesialis'], }; // Helper function to check if a path matches considering dynamic segments const pathMatches = (requestedPath: string, eligiblePath: string): boolean => { // Exact match if (requestedPath === eligiblePath) return true; // Check if eligiblePath is a parent of requestedPath (for nested routes) // e.g., /antrean/kategori matches /antrean/kategori/123 const eligibleSegments = eligiblePath.split('/').filter(s => s); const requestedSegments = requestedPath.split('/').filter(s => s); if (eligibleSegments.length > requestedSegments.length) return false; // Check if all eligible segments match (considering the requested path might have more segments) return eligibleSegments.every((seg, idx) => seg === requestedSegments[idx]); }; // Check if user has access to the requested path (direct or pattern match) let hasAccess = false; for (const eligiblePath of eligiblePaths) { if (pathMatches(to.path, eligiblePath)) { hasAccess = true; break; } } // If no direct access, check if this path is a child with satisfied dependencies // If user has the required parent paths, grant access to the child let grantedByDependency = false; if (!hasAccess) { for (const [childPath, requiredPaths] of Object.entries(pathDependencies)) { if (pathMatches(to.path, childPath)) { // This is a child path with dependencies // Check if user has all required parent paths const hasAllRequired = requiredPaths.every(requiredPath => Array.from(eligiblePaths).some(eligible => pathMatches(requiredPath, eligible)) ); if (hasAllRequired) { grantedByDependency = true; // console.log('✅ Access granted by dependency:', { // requestedPath: to.path, // satisfiedDependencies: requiredPaths // }); break; } } } } // console.log('🔐 Authorization check:', { // requestedPath: to.path, // eligiblePaths: Array.from(eligiblePaths), // hasAccess, // grantedByDependency // }); if (!hasAccess && !grantedByDependency) { // console.warn('⛔ Access denied: User does not have permission to access', to.path); return navigateTo({ path: '/auth/access-denied', query: { path: to.path } }, { replace: true }); } // console.log('✅ Authorization granted for:', to.path); } else { // Menu fetched but empty (no access) // console.error('❌ Menu is empty, denying access for security'); return navigateTo({ path: '/auth/access-denied', query: { path: to.path, reason: 'no_permissions' } }, { replace: true }); } } else { // console.log('✅ Public path, skipping authorization:', to.path); } return; // Allow access } else { // console.log('❌ No active session, redirecting to login'); return navigateTo('/auth/login'); } });