init commit
This commit is contained in:
314
src/hooks/auth.ts
Normal file
314
src/hooks/auth.ts
Normal file
@@ -0,0 +1,314 @@
|
||||
/**
|
||||
* 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);
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user