Files
antrean-operasi/middleware/auth.ts
2026-02-27 13:26:52 +07:00

213 lines
8.0 KiB
TypeScript

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<string>();
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<string, string[]> = {
'/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');
}
});