315 lines
7.8 KiB
TypeScript
315 lines
7.8 KiB
TypeScript
/**
|
|
* Route Protection Hooks
|
|
* Professional Next.js Resume Builder - Enterprise Auth System
|
|
*
|
|
* @author David Valera Melendez <david@valera-melendez.de>
|
|
* @created 2025-08-08
|
|
* @location Made in Germany 🇩🇪
|
|
*/
|
|
|
|
'use client';
|
|
|
|
import React, { useEffect } from 'react';
|
|
import { useRouter, usePathname } from 'next/navigation';
|
|
import { useAuthStore } from '@/lib/auth-store';
|
|
import { UserRole } from '@/types/auth';
|
|
|
|
/**
|
|
* Hook to protect routes that require authentication
|
|
*/
|
|
export function useAuthGuard(options?: {
|
|
requiredRoles?: UserRole[];
|
|
redirectTo?: string;
|
|
checkOnMount?: boolean;
|
|
}) {
|
|
const router = useRouter();
|
|
const pathname = usePathname();
|
|
const { isAuthenticated, user, checkAuth } = useAuthStore();
|
|
|
|
const {
|
|
requiredRoles = [],
|
|
redirectTo = '/auth/login',
|
|
checkOnMount = true,
|
|
} = options || {};
|
|
|
|
useEffect(() => {
|
|
if (checkOnMount) {
|
|
checkAuth();
|
|
}
|
|
}, [checkAuth, checkOnMount]);
|
|
|
|
useEffect(() => {
|
|
// Skip check during initial load
|
|
if (checkOnMount && !isAuthenticated && !user) {
|
|
return;
|
|
}
|
|
|
|
// Check authentication
|
|
if (!isAuthenticated) {
|
|
const loginUrl = new URL(redirectTo, window.location.origin);
|
|
loginUrl.searchParams.set('returnUrl', pathname);
|
|
router.push(loginUrl.toString() as any);
|
|
return;
|
|
}
|
|
|
|
// Check role requirements
|
|
if (requiredRoles.length > 0 && user) {
|
|
const hasRequiredRole = requiredRoles.some(role =>
|
|
user.roles.includes(role)
|
|
);
|
|
|
|
if (!hasRequiredRole) {
|
|
router.push('/access-denied');
|
|
return;
|
|
}
|
|
}
|
|
}, [isAuthenticated, user, router, pathname, requiredRoles, redirectTo]);
|
|
|
|
return {
|
|
isAuthenticated,
|
|
user,
|
|
hasRequiredRole: (role: UserRole) => user?.roles.includes(role) || false,
|
|
hasAnyRole: (roles: UserRole[]) => roles.some(role => user?.roles.includes(role)) || false,
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Hook to protect routes that should only be accessible to guests (non-authenticated users)
|
|
*/
|
|
export function useGuestGuard(redirectTo: string = '/dashboard') {
|
|
const router = useRouter();
|
|
const { isAuthenticated, checkAuth } = useAuthStore();
|
|
|
|
useEffect(() => {
|
|
checkAuth();
|
|
}, [checkAuth]);
|
|
|
|
useEffect(() => {
|
|
if (isAuthenticated) {
|
|
router.push(redirectTo as any);
|
|
}
|
|
}, [isAuthenticated, router, redirectTo]);
|
|
|
|
return { isAuthenticated };
|
|
}
|
|
|
|
/**
|
|
* Hook to check if user has specific permissions
|
|
*/
|
|
export function usePermissions() {
|
|
const { user } = useAuthStore();
|
|
|
|
const hasPermission = (permission: string): boolean => {
|
|
return user?.permissions?.includes(permission) || false;
|
|
};
|
|
|
|
const hasRole = (role: UserRole): boolean => {
|
|
return user?.roles?.includes(role) || false;
|
|
};
|
|
|
|
const hasAnyRole = (roles: UserRole[]): boolean => {
|
|
return roles.some(role => hasRole(role));
|
|
};
|
|
|
|
const hasAllRoles = (roles: UserRole[]): boolean => {
|
|
return roles.every(role => hasRole(role));
|
|
};
|
|
|
|
const isAdmin = (): boolean => {
|
|
return hasRole(UserRole.ADMIN);
|
|
};
|
|
|
|
const isModerator = (): boolean => {
|
|
return hasRole(UserRole.MODERATOR);
|
|
};
|
|
|
|
const canAccess = (resource: string, action: string): boolean => {
|
|
const permission = `${resource}:${action}`;
|
|
return hasPermission(permission) || isAdmin();
|
|
};
|
|
|
|
return {
|
|
user,
|
|
hasPermission,
|
|
hasRole,
|
|
hasAnyRole,
|
|
hasAllRoles,
|
|
isAdmin,
|
|
isModerator,
|
|
canAccess,
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Hook for handling authentication redirects
|
|
*/
|
|
export function useAuthRedirect() {
|
|
const router = useRouter();
|
|
const pathname = usePathname();
|
|
|
|
const redirectToLogin = (returnUrl?: string) => {
|
|
const loginUrl = new URL('/auth/login', window.location.origin);
|
|
loginUrl.searchParams.set('returnUrl', returnUrl || pathname);
|
|
router.push(loginUrl.toString() as any);
|
|
};
|
|
|
|
const redirectToDashboard = () => {
|
|
router.push('/dashboard');
|
|
};
|
|
|
|
const redirectToReturnUrl = (defaultUrl: string = '/dashboard') => {
|
|
const urlParams = new URLSearchParams(window.location.search);
|
|
const returnUrl = urlParams.get('returnUrl');
|
|
|
|
if (returnUrl && returnUrl.startsWith('/') && !returnUrl.startsWith('//')) {
|
|
router.push(returnUrl as any);
|
|
} else {
|
|
router.push(defaultUrl as any);
|
|
}
|
|
};
|
|
|
|
const redirectToAccessDenied = () => {
|
|
router.push('/access-denied');
|
|
};
|
|
|
|
return {
|
|
redirectToLogin,
|
|
redirectToDashboard,
|
|
redirectToReturnUrl,
|
|
redirectToAccessDenied,
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Hook to check authentication status and handle loading states
|
|
*/
|
|
export function useAuthStatus() {
|
|
const {
|
|
isAuthenticated,
|
|
isLoading,
|
|
user,
|
|
error,
|
|
checkAuth,
|
|
clearError
|
|
} = useAuthStore();
|
|
|
|
useEffect(() => {
|
|
if (!isAuthenticated && !isLoading) {
|
|
checkAuth();
|
|
}
|
|
}, [isAuthenticated, isLoading, checkAuth]);
|
|
|
|
return {
|
|
isAuthenticated,
|
|
isLoading,
|
|
user,
|
|
error,
|
|
clearError,
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Hook for checking specific route permissions
|
|
*/
|
|
export function useRoutePermissions(routePath: string) {
|
|
const { user } = useAuthStore();
|
|
|
|
// Define route permissions
|
|
const routePermissions: Record<string, {
|
|
requiredRoles?: UserRole[];
|
|
requiredPermissions?: string[];
|
|
}> = {
|
|
'/admin': { requiredRoles: [UserRole.ADMIN] },
|
|
'/admin/users': { requiredRoles: [UserRole.ADMIN] },
|
|
'/admin/analytics': { requiredRoles: [UserRole.ADMIN] },
|
|
'/moderation': { requiredRoles: [UserRole.ADMIN, UserRole.MODERATOR] },
|
|
'/dashboard': { requiredRoles: [UserRole.USER, UserRole.ADMIN, UserRole.MODERATOR] },
|
|
'/profile': { requiredRoles: [UserRole.USER, UserRole.ADMIN, UserRole.MODERATOR] },
|
|
'/resume-builder': { requiredRoles: [UserRole.USER, UserRole.ADMIN, UserRole.MODERATOR] },
|
|
};
|
|
|
|
const permissions = routePermissions[routePath];
|
|
|
|
if (!permissions) {
|
|
return { canAccess: true, missingPermissions: [] };
|
|
}
|
|
|
|
const missingPermissions: string[] = [];
|
|
let canAccess = true;
|
|
|
|
// Check required roles
|
|
if (permissions.requiredRoles && user) {
|
|
const hasRequiredRole = permissions.requiredRoles.some(role =>
|
|
user.roles.includes(role)
|
|
);
|
|
|
|
if (!hasRequiredRole) {
|
|
canAccess = false;
|
|
missingPermissions.push(`Required roles: ${permissions.requiredRoles.join(', ')}`);
|
|
}
|
|
}
|
|
|
|
// Check required permissions
|
|
if (permissions.requiredPermissions && user) {
|
|
const missingPerms = permissions.requiredPermissions.filter(permission =>
|
|
!user.permissions?.includes(permission)
|
|
);
|
|
|
|
if (missingPerms.length > 0) {
|
|
canAccess = false;
|
|
missingPermissions.push(`Required permissions: ${missingPerms.join(', ')}`);
|
|
}
|
|
}
|
|
|
|
return {
|
|
canAccess,
|
|
missingPermissions,
|
|
requiredRoles: permissions.requiredRoles || [],
|
|
requiredPermissions: permissions.requiredPermissions || [],
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Higher-order component for route protection
|
|
*/
|
|
export function withAuthGuard<T extends object>(
|
|
Component: React.ComponentType<T>,
|
|
options?: {
|
|
requiredRoles?: UserRole[];
|
|
redirectTo?: string;
|
|
loading?: React.ComponentType;
|
|
}
|
|
) {
|
|
return function AuthGuardedComponent(props: T) {
|
|
const { isAuthenticated, user } = useAuthGuard(options);
|
|
|
|
if (!isAuthenticated || !user) {
|
|
if (options?.loading) {
|
|
const LoadingComponent = options.loading;
|
|
return React.createElement(LoadingComponent);
|
|
}
|
|
return React.createElement('div', null, 'Loading...');
|
|
}
|
|
|
|
return React.createElement(Component, props);
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Higher-order component for guest-only routes
|
|
*/
|
|
export function withGuestGuard<T extends object>(
|
|
Component: React.ComponentType<T>,
|
|
redirectTo?: string
|
|
) {
|
|
return function GuestGuardedComponent(props: T) {
|
|
const { isAuthenticated } = useGuestGuard(redirectTo);
|
|
|
|
if (isAuthenticated) {
|
|
return React.createElement('div', null, 'Redirecting...');
|
|
}
|
|
|
|
return React.createElement(Component, props);
|
|
};
|
|
}
|