feat: add permission-gated navigation and admin access

This commit is contained in:
2026-06-16 16:16:03 +05:30
parent 893b49c701
commit 964b177ae1
18 changed files with 401 additions and 112 deletions

View File

@@ -0,0 +1,20 @@
'use client';
import { ShieldX } from 'lucide-react';
export default function AccessPage() {
return (
<main className="flex min-h-[calc(100vh-9rem)] items-center justify-center">
<div className="flex max-w-md flex-col items-center text-center">
<div className="mb-4 flex size-14 items-center justify-center rounded-full border bg-card">
<ShieldX className="size-7 text-muted-foreground" />
</div>
<h1 className="text-2xl font-semibold">Access denied</h1>
<p className="mt-2 text-sm text-muted-foreground">
You do not have permission to access any available module. Contact your administrator if
you need access.
</p>
</div>
</main>
);
}

View File

@@ -6,6 +6,8 @@ import { Edit, RotateCcw } from 'lucide-react';
import { Badge } from '@/components/ui/badge';
import { Button } from '@/components/ui/button';
import { PERMISSIONS } from '@/constants/permissions';
import { usePermissions } from '@/hooks/usePermissions';
import type { Role } from '@/types';
interface UseRoleColumnsParams {
@@ -19,8 +21,12 @@ export function useRoleColumns({
onToggleStatus,
isStatusPending,
}: UseRoleColumnsParams): ColumnDef<Role>[] {
return useMemo(
() => [
const { hasPermission } = usePermissions();
const canEdit = hasPermission(PERMISSIONS.ROLE.UPDATE);
const canDelete = hasPermission(PERMISSIONS.ROLE.DELETE);
return useMemo(() => {
const columns: ColumnDef<Role>[] = [
{
accessorKey: 'name',
header: 'Name',
@@ -53,16 +59,23 @@ export function useRoleColumns({
</Badge>
),
},
{
id: 'actions',
header: () => <div className="text-right">Actions</div>,
cell: ({ row }) => {
const role = row.original;
return (
<div className="flex justify-end gap-2">
];
if (!canEdit && !canDelete) return columns;
columns.push({
id: 'actions',
header: () => <div className="text-right">Actions</div>,
cell: ({ row }) => {
const role = row.original;
return (
<div className="flex justify-end gap-2">
{canEdit ? (
<Button variant="outline" size="sm" onClick={() => onEdit(role)}>
<Edit className="size-4" />
</Button>
) : null}
{canDelete ? (
<Button
variant="outline"
size="sm"
@@ -72,11 +85,12 @@ export function useRoleColumns({
<RotateCcw className="mr-2 size-4" />
{role.effective_status ? 'Deactivate' : 'Activate'}
</Button>
</div>
);
},
) : null}
</div>
);
},
],
[isStatusPending, onEdit, onToggleStatus],
);
});
return columns;
}, [canDelete, canEdit, isStatusPending, onEdit, onToggleStatus]);
}

View File

@@ -47,10 +47,11 @@ export function useRolesQuery(params: UseRolesQueryParams) {
});
}
export function usePermissionsTreeQuery() {
export function usePermissionsTreeQuery(enabled: boolean) {
const permissionsQuery = useQuery({
queryKey: ['permissions', 'tree'],
queryFn: permissionService.getPermissions,
enabled,
});
const permissionTree = useMemo(
() => mapPermissionTree(permissionsQuery.data?.tree || []),

View File

@@ -6,6 +6,8 @@ import { Plus, ShieldCheck } from 'lucide-react';
import { PageHeader } from '@/components/page-header';
import { PoweredBy } from '@/components/powered-by';
import { Button } from '@/components/ui/button';
import { PERMISSIONS } from '@/constants/permissions';
import { PermissionGuard } from '@/guards';
import { useAppStore } from '@/store/app.store';
import type { Role } from '@/types';
@@ -41,7 +43,7 @@ export default function RolesPage() {
searchTerm: debouncedSearchTerm,
statusFilter,
});
const permissionsQuery = usePermissionsTreeQuery();
const permissionsQuery = usePermissionsTreeQuery(isSheetOpen);
const roleForm = useRoleForm({
organizationId,
permissionTree: permissionsQuery.permissionTree,
@@ -105,10 +107,12 @@ export default function RolesPage() {
description="Manage roles and permission access"
icon={ShieldCheck}
actions={
<Button onClick={openCreate}>
<Plus className="mr-2 size-4" />
Add Role
</Button>
<PermissionGuard permissions={PERMISSIONS.ROLE.CREATE}>
<Button onClick={openCreate}>
<Plus className="mr-2 size-4" />
Add Role
</Button>
</PermissionGuard>
}
/>

View File

@@ -6,6 +6,8 @@ import { Edit, RotateCcw } from 'lucide-react';
import { Badge } from '@/components/ui/badge';
import { Button } from '@/components/ui/button';
import { PERMISSIONS } from '@/constants/permissions';
import { usePermissions } from '@/hooks/usePermissions';
import type { AdministrationUser } from '@/types';
function formatDate(value?: string | null) {
@@ -28,8 +30,12 @@ export function useUserColumns({
onToggleStatus,
pendingUserId,
}: UseUserColumnsParams): ColumnDef<AdministrationUser>[] {
return useMemo(
() => [
const { hasPermission } = usePermissions();
const canEdit = hasPermission(PERMISSIONS.USER.UPDATE);
const canDelete = hasPermission(PERMISSIONS.USER.DELETE);
return useMemo(() => {
const columns: ColumnDef<AdministrationUser>[] = [
{
accessorKey: 'first_name',
header: 'Name',
@@ -80,16 +86,23 @@ export function useUserColumns({
</Badge>
),
},
{
id: 'actions',
header: () => <div className="text-right">Actions</div>,
cell: ({ row }) => {
const user = row.original;
return (
<div className="flex justify-end gap-2">
];
if (!canEdit && !canDelete) return columns;
columns.push({
id: 'actions',
header: () => <div className="text-right">Actions</div>,
cell: ({ row }) => {
const user = row.original;
return (
<div className="flex justify-end gap-2">
{canEdit ? (
<Button variant="outline" size="sm" onClick={() => onEdit(user)}>
<Edit className="size-4" />
</Button>
) : null}
{canDelete ? (
<Button
variant="outline"
size="sm"
@@ -99,11 +112,12 @@ export function useUserColumns({
<RotateCcw className="mr-2 size-4" />
{user.effective_status === 'active' ? 'Deactivate' : 'Activate'}
</Button>
</div>
);
},
) : null}
</div>
);
},
],
[onEdit, onToggleStatus, pendingUserId],
);
});
return columns;
}, [canDelete, canEdit, onEdit, onToggleStatus, pendingUserId]);
}

View File

@@ -6,6 +6,8 @@ import { Plus, Users } from 'lucide-react';
import { PageHeader } from '@/components/page-header';
import { PoweredBy } from '@/components/powered-by';
import { Button } from '@/components/ui/button';
import { PERMISSIONS } from '@/constants/permissions';
import { PermissionGuard } from '@/guards';
import type { AdministrationUser } from '@/types';
import { useUserColumns } from './components/UserColumns';
@@ -103,10 +105,12 @@ export default function UsersPage() {
description="Manage users and role assignment"
icon={Users}
actions={
<Button onClick={openCreate}>
<Plus className="mr-2 size-4" />
Add User
</Button>
<PermissionGuard permissions={PERMISSIONS.USER.CREATE}>
<Button onClick={openCreate}>
<Plus className="mr-2 size-4" />
Add User
</Button>
</PermissionGuard>
}
/>