refactor: modify route guard pattern
This commit is contained in:
16
src/app/(modules)/clients/layout.tsx
Normal file
16
src/app/(modules)/clients/layout.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
16
src/app/(modules)/package/layout.tsx
Normal file
16
src/app/(modules)/package/layout.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
16
src/app/(modules)/plans/layout.tsx
Normal file
16
src/app/(modules)/plans/layout.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
16
src/app/(modules)/project/layout.tsx
Normal file
16
src/app/(modules)/project/layout.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
16
src/app/(modules)/roles/layout.tsx
Normal file
16
src/app/(modules)/roles/layout.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
16
src/app/(modules)/segment/layout.tsx
Normal file
16
src/app/(modules)/segment/layout.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
16
src/app/(modules)/tenants/layout.tsx
Normal file
16
src/app/(modules)/tenants/layout.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
16
src/app/(modules)/ticket/layout.tsx
Normal file
16
src/app/(modules)/ticket/layout.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
16
src/app/(modules)/upload/layout.tsx
Normal file
16
src/app/(modules)/upload/layout.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
16
src/app/(modules)/users/layout.tsx
Normal file
16
src/app/(modules)/users/layout.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -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 ? (
|
||||||
|
|||||||
@@ -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;
|
|
||||||
}
|
|
||||||
@@ -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',
|
||||||
|
|||||||
@@ -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',
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
31
src/guards/PermissionRouteGuard.tsx
Normal file
31
src/guards/PermissionRouteGuard.tsx
Normal 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;
|
||||||
|
}
|
||||||
@@ -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';
|
||||||
|
|||||||
Reference in New Issue
Block a user