2
0
mirror of https://github.com/inventree/InvenTree.git synced 2025-04-28 11:36:44 +00:00

[UI] Permission Enhancements (#8785)

* Update page permissions

- Add permission check to <InstanceDetail>
- HIde breadcrumbs and tree for part
- Hide breadcrumbs and tree for stock

* Additional permissions checks
This commit is contained in:
Oliver 2024-12-29 17:00:27 +11:00 committed by GitHub
parent cd0ee7dbab
commit 5e79c6906c
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
13 changed files with 93 additions and 34 deletions

View File

@ -1,17 +1,24 @@
import { LoadingOverlay } from '@mantine/core'; import { LoadingOverlay } from '@mantine/core';
import type { ModelType } from '../../enums/ModelType';
import type { UserRoles } from '../../enums/Roles';
import { useUserState } from '../../states/UserState'; import { useUserState } from '../../states/UserState';
import ClientError from '../errors/ClientError'; import ClientError from '../errors/ClientError';
import PermissionDenied from '../errors/PermissionDenied';
import ServerError from '../errors/ServerError'; import ServerError from '../errors/ServerError';
export default function InstanceDetail({ export default function InstanceDetail({
status, status,
loading, loading,
children children,
requiredRole,
requiredPermission
}: Readonly<{ }: Readonly<{
status: number; status: number;
loading: boolean; loading: boolean;
children: React.ReactNode; children: React.ReactNode;
requiredRole?: UserRoles;
requiredPermission?: ModelType;
}>) { }>) {
const user = useUserState(); const user = useUserState();
@ -27,5 +34,13 @@ export default function InstanceDetail({
return <ClientError status={status} />; return <ClientError status={status} />;
} }
if (requiredRole && !user.hasViewRole(requiredRole)) {
return <PermissionDenied />;
}
if (requiredPermission && !user.hasViewPermission(requiredPermission)) {
return <PermissionDenied />;
}
return <>{children}</>; return <>{children}</>;
} }

View File

@ -519,7 +519,11 @@ export default function BuildDetail() {
{holdOrder.modal} {holdOrder.modal}
{issueOrder.modal} {issueOrder.modal}
{completeOrder.modal} {completeOrder.modal}
<InstanceDetail status={requestStatus} loading={instanceQuery.isFetching}> <InstanceDetail
status={requestStatus}
loading={instanceQuery.isFetching}
requiredRole={UserRoles.build}
>
<Stack gap='xs'> <Stack gap='xs'>
<PageDetail <PageDetail
title={`${t`Build Order`}: ${build.reference}`} title={`${t`Build Order`}: ${build.reference}`}

View File

@ -398,7 +398,11 @@ export default function SupplierPartDetail() {
{deleteSupplierPart.modal} {deleteSupplierPart.modal}
{duplicateSupplierPart.modal} {duplicateSupplierPart.modal}
{editSupplierPart.modal} {editSupplierPart.modal}
<InstanceDetail status={requestStatus} loading={instanceQuery.isFetching}> <InstanceDetail
status={requestStatus}
loading={instanceQuery.isFetching}
requiredRole={UserRoles.purchase_order}
>
<Stack gap='xs'> <Stack gap='xs'>
<PageDetail <PageDetail
title={t`Supplier Part`} title={t`Supplier Part`}

View File

@ -318,6 +318,7 @@ export default function CategoryDetail() {
<InstanceDetail <InstanceDetail
status={requestStatus} status={requestStatus}
loading={id ? instanceQuery.isFetching : false} loading={id ? instanceQuery.isFetching : false}
requiredRole={UserRoles.part_category}
> >
<Stack gap='xs'> <Stack gap='xs'>
<LoadingOverlay visible={instanceQuery.isFetching} /> <LoadingOverlay visible={instanceQuery.isFetching} />

View File

@ -970,18 +970,24 @@ export default function PartDetail() {
{editPart.modal} {editPart.modal}
{deletePart.modal} {deletePart.modal}
{orderPartsWizard.wizard} {orderPartsWizard.wizard}
<InstanceDetail status={requestStatus} loading={instanceQuery.isFetching}> <InstanceDetail
status={requestStatus}
loading={instanceQuery.isFetching}
requiredRole={UserRoles.part}
>
<Stack gap='xs'> <Stack gap='xs'>
<NavigationTree {user.hasViewRole(UserRoles.part_category) && (
title={t`Part Categories`} <NavigationTree
modelType={ModelType.partcategory} title={t`Part Categories`}
endpoint={ApiEndpoints.category_tree} modelType={ModelType.partcategory}
opened={treeOpen} endpoint={ApiEndpoints.category_tree}
onClose={() => { opened={treeOpen}
setTreeOpen(false); onClose={() => {
}} setTreeOpen(false);
selectedId={part?.category} }}
/> selectedId={part?.category}
/>
)}
<PageDetail <PageDetail
title={`${t`Part`}: ${part.full_name}`} title={`${t`Part`}: ${part.full_name}`}
icon={ icon={
@ -992,9 +998,13 @@ export default function PartDetail() {
subtitle={part.description} subtitle={part.description}
imageUrl={part.image} imageUrl={part.image}
badges={badges} badges={badges}
breadcrumbs={breadcrumbs} breadcrumbs={
user.hasViewRole(UserRoles.part_category)
? breadcrumbs
: undefined
}
breadcrumbAction={() => { breadcrumbAction={() => {
setTreeOpen(true); // Open the category tree setTreeOpen(true);
}} }}
editAction={editPart.open} editAction={editPart.open}
editEnabled={user.hasChangeRole(UserRoles.part)} editEnabled={user.hasChangeRole(UserRoles.part)}

View File

@ -483,7 +483,11 @@ export default function PurchaseOrderDetail() {
{completeOrder.modal} {completeOrder.modal}
{editPurchaseOrder.modal} {editPurchaseOrder.modal}
{duplicatePurchaseOrder.modal} {duplicatePurchaseOrder.modal}
<InstanceDetail status={requestStatus} loading={instanceQuery.isFetching}> <InstanceDetail
status={requestStatus}
loading={instanceQuery.isFetching}
requiredRole={UserRoles.purchase_order}
>
<Stack gap='xs'> <Stack gap='xs'>
<PageDetail <PageDetail
title={`${t`Purchase Order`}: ${order.reference}`} title={`${t`Purchase Order`}: ${order.reference}`}

View File

@ -24,7 +24,8 @@ export default function PurchasingIndex() {
name: 'purchaseorders', name: 'purchaseorders',
label: t`Purchase Orders`, label: t`Purchase Orders`,
icon: <IconShoppingCart />, icon: <IconShoppingCart />,
content: <PurchaseOrderTable /> content: <PurchaseOrderTable />,
hidden: !user.hasViewRole(UserRoles.purchase_order)
}, },
{ {
name: 'suppliers', name: 'suppliers',
@ -49,7 +50,7 @@ export default function PurchasingIndex() {
) )
} }
]; ];
}, []); }, [user]);
if (!user.isLoggedIn() || !user.hasViewRole(UserRoles.purchase_order)) { if (!user.isLoggedIn() || !user.hasViewRole(UserRoles.purchase_order)) {
return <PermissionDenied />; return <PermissionDenied />;

View File

@ -470,7 +470,11 @@ export default function ReturnOrderDetail() {
{holdOrder.modal} {holdOrder.modal}
{completeOrder.modal} {completeOrder.modal}
{duplicateReturnOrder.modal} {duplicateReturnOrder.modal}
<InstanceDetail status={requestStatus} loading={instanceQuery.isFetching}> <InstanceDetail
status={requestStatus}
loading={instanceQuery.isFetching}
requiredRole={UserRoles.return_order}
>
<Stack gap='xs'> <Stack gap='xs'>
<PageDetail <PageDetail
title={`${t`Return Order`}: ${order.reference}`} title={`${t`Return Order`}: ${order.reference}`}

View File

@ -25,13 +25,15 @@ export default function PurchasingIndex() {
name: 'salesorders', name: 'salesorders',
label: t`Sales Orders`, label: t`Sales Orders`,
icon: <IconTruckDelivery />, icon: <IconTruckDelivery />,
content: <SalesOrderTable /> content: <SalesOrderTable />,
hidden: !user.hasViewRole(UserRoles.sales_order)
}, },
{ {
name: 'returnorders', name: 'returnorders',
label: t`Return Orders`, label: t`Return Orders`,
icon: <IconTruckReturn />, icon: <IconTruckReturn />,
content: <ReturnOrderTable /> content: <ReturnOrderTable />,
hidden: !user.hasViewRole(UserRoles.return_order)
}, },
{ {
name: 'suppliers', name: 'suppliers',
@ -42,7 +44,7 @@ export default function PurchasingIndex() {
) )
} }
]; ];
}, []); }, [user]);
if (!user.isLoggedIn() || !user.hasViewRole(UserRoles.sales_order)) { if (!user.isLoggedIn() || !user.hasViewRole(UserRoles.sales_order)) {
return <PermissionDenied />; return <PermissionDenied />;

View File

@ -534,7 +534,11 @@ export default function SalesOrderDetail() {
{completeOrder.modal} {completeOrder.modal}
{editSalesOrder.modal} {editSalesOrder.modal}
{duplicateSalesOrder.modal} {duplicateSalesOrder.modal}
<InstanceDetail status={requestStatus} loading={instanceQuery.isFetching}> <InstanceDetail
status={requestStatus}
loading={instanceQuery.isFetching}
requiredRole={UserRoles.sales_order}
>
<Stack gap='xs'> <Stack gap='xs'>
<PageDetail <PageDetail
title={`${t`Sales Order`}: ${order.reference}`} title={`${t`Sales Order`}: ${order.reference}`}

View File

@ -352,6 +352,7 @@ export default function SalesOrderShipmentDetail() {
<InstanceDetail <InstanceDetail
status={shipmentStatus} status={shipmentStatus}
loading={shipmentQuery.isFetching || customerQuery.isFetching} loading={shipmentQuery.isFetching || customerQuery.isFetching}
requiredRole={UserRoles.sales_order}
> >
<Stack gap='xs'> <Stack gap='xs'>
<PageDetail <PageDetail

View File

@ -363,6 +363,7 @@ export default function Stock() {
<InstanceDetail <InstanceDetail
status={requestStatus} status={requestStatus}
loading={id ? instanceQuery.isFetching : false} loading={id ? instanceQuery.isFetching : false}
requiredRole={UserRoles.stock_location}
> >
<Stack> <Stack>
<NavigationTree <NavigationTree

View File

@ -884,16 +884,22 @@ export default function StockDetail() {
}, [stockitem, instanceQuery, enableExpiry]); }, [stockitem, instanceQuery, enableExpiry]);
return ( return (
<InstanceDetail status={requestStatus} loading={instanceQuery.isFetching}> <InstanceDetail
requiredRole={UserRoles.stock}
status={requestStatus}
loading={instanceQuery.isFetching}
>
<Stack> <Stack>
<NavigationTree {user.hasViewRole(UserRoles.stock_location) && (
title={t`Stock Locations`} <NavigationTree
modelType={ModelType.stocklocation} title={t`Stock Locations`}
endpoint={ApiEndpoints.stock_location_tree} modelType={ModelType.stocklocation}
opened={treeOpen} endpoint={ApiEndpoints.stock_location_tree}
onClose={() => setTreeOpen(false)} opened={treeOpen}
selectedId={stockitem?.location} onClose={() => setTreeOpen(false)}
/> selectedId={stockitem?.location}
/>
)}
<PageDetail <PageDetail
title={t`Stock Item`} title={t`Stock Item`}
subtitle={stockitem.part_detail?.full_name} subtitle={stockitem.part_detail?.full_name}
@ -901,7 +907,9 @@ export default function StockDetail() {
editAction={editStockItem.open} editAction={editStockItem.open}
editEnabled={user.hasChangePermission(ModelType.stockitem)} editEnabled={user.hasChangePermission(ModelType.stockitem)}
badges={stockBadges} badges={stockBadges}
breadcrumbs={breadcrumbs} breadcrumbs={
user.hasViewRole(UserRoles.stock_location) ? breadcrumbs : undefined
}
breadcrumbAction={() => { breadcrumbAction={() => {
setTreeOpen(true); setTreeOpen(true);
}} }}