Files
Next.js/src/hooks/auth.ts
David Melendez c3868062e6 init commit
2026-01-14 22:44:08 +01:00

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);
};
}