213 lines
8.0 KiB
TypeScript
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');
|
|
}
|
|
}); |