refactor: modify route guard pattern

This commit is contained in:
2026-06-19 18:36:05 +05:30
parent fffd8e3e12
commit 664d80809a
17 changed files with 224 additions and 60 deletions

View File

@@ -0,0 +1,16 @@
'use client';
import { PERMISSIONS } from '@/constants/permissions';
import { PermissionRouteGuard } from '@/guards';
export default function ClientsLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<PermissionRouteGuard permission={PERMISSIONS.CLIENT.READ}>
{children}
</PermissionRouteGuard>
);
}

View File

@@ -0,0 +1,16 @@
'use client';
import { PERMISSIONS } from '@/constants/permissions';
import { PermissionRouteGuard } from '@/guards';
export default function PackageLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<PermissionRouteGuard permission={PERMISSIONS.PACKAGE.READ}>
{children}
</PermissionRouteGuard>
);
}

View File

@@ -0,0 +1,16 @@
'use client';
import { PERMISSIONS } from '@/constants/permissions';
import { PermissionRouteGuard } from '@/guards';
export default function PlansLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<PermissionRouteGuard permission={PERMISSIONS.PLAN.READ}>
{children}
</PermissionRouteGuard>
);
}

View File

@@ -0,0 +1,16 @@
'use client';
import { PERMISSIONS } from '@/constants/permissions';
import { PermissionRouteGuard } from '@/guards';
export default function ProjectLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<PermissionRouteGuard permission={PERMISSIONS.PROJECT.READ}>
{children}
</PermissionRouteGuard>
);
}

View File

@@ -0,0 +1,16 @@
'use client';
import { PERMISSIONS } from '@/constants/permissions';
import { PermissionRouteGuard } from '@/guards';
export default function RolesLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<PermissionRouteGuard permission={PERMISSIONS.ROLE.READ}>
{children}
</PermissionRouteGuard>
);
}

View File

@@ -0,0 +1,16 @@
'use client';
import { PERMISSIONS } from '@/constants/permissions';
import { PermissionRouteGuard } from '@/guards';
export default function SegmentLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<PermissionRouteGuard permission={PERMISSIONS.SEGMENT.READ}>
{children}
</PermissionRouteGuard>
);
}

View File

@@ -0,0 +1,16 @@
'use client';
import { PERMISSIONS } from '@/constants/permissions';
import { PermissionRouteGuard } from '@/guards';
export default function TenantsLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<PermissionRouteGuard permission={PERMISSIONS.TENANT.READ}>
{children}
</PermissionRouteGuard>
);
}

View File

@@ -0,0 +1,16 @@
'use client';
import { PERMISSIONS } from '@/constants/permissions';
import { PermissionRouteGuard } from '@/guards';
export default function TicketLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<PermissionRouteGuard permission={PERMISSIONS.TICKET.READ}>
{children}
</PermissionRouteGuard>
);
}

View File

@@ -0,0 +1,16 @@
'use client';
import { PERMISSIONS } from '@/constants/permissions';
import { PermissionRouteGuard } from '@/guards';
export default function UploadLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<PermissionRouteGuard permission={PERMISSIONS.VIDEO.UPLOAD}>
{children}
</PermissionRouteGuard>
);
}

View File

@@ -0,0 +1,16 @@
'use client';
import { PERMISSIONS } from '@/constants/permissions';
import { PermissionRouteGuard } from '@/guards';
export default function UsersLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<PermissionRouteGuard permission={PERMISSIONS.USER.READ}>
{children}
</PermissionRouteGuard>
);
}

View File

@@ -286,12 +286,12 @@ function DataTableContent<TData, TValue>({
<TableRow <TableRow
key={row.id} key={row.id}
data-state={row.getIsSelected() && 'selected'} data-state={row.getIsSelected() && 'selected'}
onClick={() => onRowClick?.(row.original)}
className={cn(onRowClick && 'cursor-pointer')}
> >
{row.getVisibleCells().map((cell) => { {row.getVisibleCells().map((cell) => {
const truncate = shouldTruncateCell(cell.column); const truncate = shouldTruncateCell(cell.column);
const pinned = cell.column.getIsPinned(); const pinned = cell.column.getIsPinned();
const isActionsCell = cell.column.id === 'actions';
const isRowClickable = Boolean(onRowClick && !isActionsCell);
const content = flexRender( const content = flexRender(
cell.column.columnDef.cell, cell.column.columnDef.cell,
cell.getContext(), cell.getContext(),
@@ -301,9 +301,16 @@ function DataTableContent<TData, TValue>({
<TableCell <TableCell
key={cell.id} key={cell.id}
style={getPinnedColumnStyles(cell.column)} style={getPinnedColumnStyles(cell.column)}
onClick={
isRowClickable
? () => onRowClick?.(row.original)
: undefined
}
className={cn( className={cn(
'max-w-0 overflow-hidden border-b border-border', 'max-w-0 overflow-hidden border-b border-border',
pinned && 'bg-card', pinned && 'bg-card',
isRowClickable && 'cursor-pointer',
isActionsCell && 'cursor-default',
)} )}
> >
{truncate ? ( {truncate ? (

View File

@@ -1,44 +0,0 @@
import { PERMISSIONS } from '@/constants/permissions';
import { ROUTES } from '@/utils/routes';
export type AppRoute = {
path: string;
permission?: string;
match?: 'exact' | 'prefix';
};
export const appRoutes: AppRoute[] = [
{
path: ROUTES.ROLES,
permission: PERMISSIONS.ROLE.READ,
},
{
path: ROUTES.USERS,
permission: PERMISSIONS.USER.READ,
},
{
path: ROUTES.CLIENTS,
permission: PERMISSIONS.CLIENT.READ,
},
{
path: ROUTES.PLANS,
permission: PERMISSIONS.PLAN.READ,
},
{
path: ROUTES.UPLOAD,
permission: PERMISSIONS.VIDEO.UPLOAD,
},
{
path: ROUTES.TICKET,
permission: PERMISSIONS.TICKET.READ,
match: 'prefix',
},
];
export function getRoutePermission(pathname: string) {
return appRoutes.find((route) =>
route.match === 'prefix'
? pathname === route.path || pathname.startsWith(`${route.path}/`)
: route.path === pathname,
)?.permission;
}

View File

@@ -47,16 +47,19 @@ export const menuItems: MenuItem[] = [
title: 'Project', title: 'Project',
path: ROUTES.PROJECT, path: ROUTES.PROJECT,
icon: Layers, icon: Layers,
permission: PERMISSIONS.PROJECT.READ,
}, },
{ {
title: 'Package', title: 'Package',
path: ROUTES.PACKAGE, path: ROUTES.PACKAGE,
icon: Package, icon: Package,
permission: PERMISSIONS.PACKAGE.READ,
}, },
{ {
title: 'Segment', title: 'Segment',
path: ROUTES.CHAINAGE, path: ROUTES.CHAINAGE,
icon: Milestone, icon: Milestone,
permission: PERMISSIONS.SEGMENT.READ,
}, },
{ {
title: 'Administration', title: 'Administration',

View File

@@ -29,6 +29,24 @@ export const PERMISSIONS = {
UPDATE: 'administration.tenants.update', UPDATE: 'administration.tenants.update',
DELETE: 'administration.tenants.delete', DELETE: 'administration.tenants.delete',
}, },
PROJECT: {
CREATE: 'management.project.create',
READ: 'management.project.read',
UPDATE: 'management.project.update',
DELETE: 'management.project.delete',
},
PACKAGE: {
CREATE: 'management.package.create',
READ: 'management.package.read',
UPDATE: 'management.package.update',
DELETE: 'management.package.delete',
},
SEGMENT: {
CREATE: 'management.segment.create',
READ: 'management.segment.read',
UPDATE: 'management.segment.update',
DELETE: 'management.segment.delete',
},
TICKET: { TICKET: {
ASSIGN: 'ticket.assign', ASSIGN: 'ticket.assign',
WORK: 'ticket.work', WORK: 'ticket.work',

View File

@@ -3,22 +3,16 @@
import { AppLoader } from '@/components/AppLoader'; import { AppLoader } from '@/components/AppLoader';
import { useAppStore } from '@/store/app.store'; import { useAppStore } from '@/store/app.store';
import { useAuthStore } from '@/store/auth.store'; import { useAuthStore } from '@/store/auth.store';
import { getRoutePermission } from '@/config/app.routes';
import { usePermissions } from '@/hooks/usePermissions';
import { ROUTES } from '@/utils/routes'; import { ROUTES } from '@/utils/routes';
import { usePathname, useRouter } from 'next/navigation'; import { useRouter } from 'next/navigation';
import type { ReactNode } from 'react'; import type { ReactNode } from 'react';
import { useEffect } from 'react'; import { useEffect } from 'react';
export function AuthGuard({ children }: { children: ReactNode }) { export function AuthGuard({ children }: { children: ReactNode }) {
const router = useRouter(); const router = useRouter();
const pathname = usePathname();
const accessToken = useAuthStore((state) => state.accessToken); const accessToken = useAuthStore((state) => state.accessToken);
const isInitialized = useAppStore((state) => state.isInitialized); const isInitialized = useAppStore((state) => state.isInitialized);
const user = useAppStore((state) => state.user); const user = useAppStore((state) => state.user);
const { hasPermission } = usePermissions();
const requiredPermission = getRoutePermission(pathname);
const canAccessRoute = hasPermission(requiredPermission);
useEffect(() => { useEffect(() => {
if (!isInitialized) return; if (!isInitialized) return;
@@ -28,17 +22,11 @@ export function AuthGuard({ children }: { children: ReactNode }) {
} }
}, [accessToken, isInitialized, router]); }, [accessToken, isInitialized, router]);
useEffect(() => {
if (!isInitialized || !accessToken || !user || canAccessRoute) return;
router.replace(ROUTES.ACCESS);
}, [accessToken, canAccessRoute, isInitialized, router, user]);
if (!isInitialized || (accessToken && !user)) { if (!isInitialized || (accessToken && !user)) {
return <AppLoader label="Preparing workspace" />; return <AppLoader label="Preparing workspace" />;
} }
if (!accessToken || !user || !canAccessRoute) { if (!accessToken || !user) {
return null; return null;
} }

View File

@@ -0,0 +1,31 @@
'use client';
import { useRouter } from 'next/navigation';
import { useEffect, type ReactNode } from 'react';
import { usePermissions, type PermissionInput } from '@/hooks/usePermissions';
import { ROUTES } from '@/utils/routes';
interface PermissionRouteGuardProps {
children: ReactNode;
permission: PermissionInput;
}
export function PermissionRouteGuard({
children,
permission,
}: PermissionRouteGuardProps) {
const router = useRouter();
const { hasPermission } = usePermissions();
const allowed = hasPermission(permission);
useEffect(() => {
if (!allowed) {
router.replace(ROUTES.ACCESS);
}
}, [allowed, router]);
if (!allowed) return null;
return children;
}

View File

@@ -1,3 +1,4 @@
export * from './AuthGuard'; export * from './AuthGuard';
export * from './GuestGuard'; export * from './GuestGuard';
export * from './PermissionGuard'; export * from './PermissionGuard';
export * from './PermissionRouteGuard';