Fix permissions handling with react context

Signed-off-by: Émile Ré <nemile.re@gmail.com>
This commit is contained in:
Émile Ré
2025-12-02 17:11:02 +04:00
parent accc8292b5
commit e65097f248
57 changed files with 609 additions and 840 deletions

View File

@@ -6,7 +6,8 @@ import type { CustomDomainManagerDeleteMutation } from "./__generated__/CustomDo
import { CreateCustomDomainDialog } from "./CreateCustomDomainDialog";
import { DeleteCustomDomainDialog } from "./DeleteCustomDomainDialog";
import { DomainDetailsDialog } from "./DomainDetailsDialog";
import { Authorized } from "/permissions";
import { PermissionsContext } from "/providers/PermissionsContext";
import { use } from "react";
const deleteCustomDomainMutation = graphql`
mutation CustomDomainManagerDeleteMutation($input: DeleteCustomDomainInput!) {
@@ -45,7 +46,7 @@ export function CustomDomainManager({
customDomain,
}: CustomDomainManagerProps) {
const { __ } = useTranslate();
const { isAuthorized } = use(PermissionsContext);
const [deleteCustomDomain] =
useMutationWithToasts<CustomDomainManagerDeleteMutation>(
deleteCustomDomainMutation,
@@ -107,11 +108,11 @@ export function CustomDomainManager({
)}
</p>
<div className="flex justify-center">
<Authorized entity="Organization" action="createCustomDomain">
{isAuthorized("Organization", "createCustomDomain") && (
<CreateCustomDomainDialog organizationId={organizationId}>
<Button icon={IconPlusLarge}>{__("Add Domain")}</Button>
</CreateCustomDomainDialog>
</Authorized>
)}
</div>
</div>
</Card>
@@ -139,14 +140,14 @@ export function CustomDomainManager({
<Button variant="secondary">{__("View Details")}</Button>
</DomainDetailsDialog>
<Authorized entity="CustomDomain" action="deleteCustomDomain">
{isAuthorized("CustomDomain", "deleteCustomDomain") && (
<DeleteCustomDomainDialog
domainName={domain.domain}
onConfirm={handleDeleteDomain}
>
<Button variant="danger">{__("Delete")}</Button>
</DeleteCustomDomainDialog>
</Authorized>
)}
</div>
</div>
</div>

View File

@@ -17,8 +17,9 @@ import { useMutationWithToasts } from "/hooks/useMutationWithToasts";
import { z } from "zod";
import { useFormWithSchema } from "/hooks/useFormWithSchema";
import { Controller } from "react-hook-form";
import { Suspense } from "react";
import { getAssignableRoles } from "/permissions";
import { Suspense, use } from "react";
import { getAssignableRoles } from "@probo/helpers";
import { PermissionsContext } from "/providers/PermissionsContext";
const inviteMutation = graphql`
mutation InviteUserDialogMutation(
@@ -56,7 +57,8 @@ type Props = PropsWithChildren & {
function InviteUserDialogContent({ children, connectionId, onRefetch }: Props) {
const { __ } = useTranslate();
const organizationId = useOrganizationId();
const assignableRoles = getAssignableRoles(organizationId);
const { role: currentUserRole } = use(PermissionsContext);
const assignableRoles = getAssignableRoles(currentUserRole);
const [inviteUser, isInviting] = useMutationWithToasts(inviteMutation, {
successMessage: __("Invitation sent successfully"),
errorMessage: __("Failed to send invitation"),

View File

@@ -2,7 +2,8 @@ import { Badge, Button, Card } from "@probo/ui";
import { useTranslate } from "@probo/i18n";
import { sprintf } from "@probo/helpers";
import type { TrustCenterGraphQuery$data } from "/hooks/graph/__generated__/TrustCenterGraphQuery.graphql";
import { Authorized } from "/permissions";
import { use } from "react";
import { PermissionsContext } from "/providers/PermissionsContext";
type Props = {
organizationId: string;
@@ -11,7 +12,7 @@ type Props = {
export function SlackConnections({ organizationId, slackConnections: connectedSlackConnections }: Props) {
const { __, dateTimeFormat } = useTranslate();
const { isAuthorized } = use(PermissionsContext);
const slackConnectionDefinitions = [
{
id: "SLACK",
@@ -77,11 +78,11 @@ export function SlackConnections({ organizationId, slackConnections: connectedSl
</Badge>
</div>
) : (
<Authorized entity="TrustCenter" action="updateTrustCenter">
isAuthorized("TrustCenter", "updateTrustCenter") && (
<Button variant="secondary" asChild>
<a href={getUrl(slackConnection.id)}>{__("Connect")}</a>
</Button>
</Authorized>
)
)}
</Card>
))}

View File

@@ -1,3 +1,6 @@
import { promisifyMutation } from "@probo/helpers";
import { usePageTitle } from "@probo/hooks";
import { useTranslate } from "@probo/i18n";
import {
ActionDropdown,
Avatar,
@@ -15,21 +18,17 @@ import {
useConfirm,
useDialogRef,
} from "@probo/ui";
import { Fragment } from "react";
import { Fragment, use } from "react";
import { graphql, useMutation, useRelayEnvironment } from "react-relay";
import { useTranslate } from "@probo/i18n";
import { usePageTitle } from "@probo/hooks";
import type { ItemOf } from "/types";
import { Link, useLocation, useParams } from "react-router";
import type { TaskFormDialogFragment$key } from "./__generated__/TaskFormDialogFragment.graphql";
import TaskFormDialog, {
taskUpdateMutation,
} from "/components/tasks/TaskFormDialog";
import { useOrganizationId } from "/hooks/useOrganizationId";
import { Link, useLocation, useParams } from "react-router";
import { promisifyMutation } from "@probo/helpers";
import { Authorized } from "/permissions";
import { isAuthorized } from "/permissions";
import type { TaskFormDialogFragment$key } from "./__generated__/TaskFormDialogFragment.graphql";
import { updateStoreCounter } from "/hooks/useMutationWithIncrement";
import { useOrganizationId } from "/hooks/useOrganizationId";
import { PermissionsContext } from "/providers/PermissionsContext";
import type { ItemOf } from "/types";
type Props = {
tasks: ({
@@ -51,7 +50,6 @@ type Props = {
export default function TasksCard({ tasks, connectionId }: Props) {
const { __ } = useTranslate();
const organizationId = useOrganizationId();
const hash = useLocation().hash.replace("#", "");
const hashes = [
@@ -70,8 +68,10 @@ export default function TasksCard({ tasks, connectionId }: Props) {
usePageTitle(__("Tasks"));
const hasAnyAction = isAuthorized(organizationId, "Task", "updateTask") ||
isAuthorized(organizationId, "Task", "deleteTask");
const { isAuthorized } = use(PermissionsContext);
const hasAnyAction = isAuthorized("Task", "updateTask") ||
isAuthorized("Task", "deleteTask");
return (
<div className="space-y-6">
@@ -151,6 +151,7 @@ function TaskRow(props: TaskRowProps) {
const confirm = useConfirm();
const [deleteTask] = useMutation(deleteMutation);
const params = useParams<{ measureId?: string }>();
const { isAuthorized } = use(PermissionsContext);
const relayEnv = useRelayEnvironment();
const [updateTask, isUpdating] = useMutation(taskUpdateMutation);
@@ -232,15 +233,15 @@ function TaskRow(props: TaskRowProps) {
)}
{props.hasAnyAction && (
<ActionDropdown>
<Authorized entity="Task" action="updateTask">
{isAuthorized("Task", "updateTask") && (
<DropdownItem
icon={IconPencil}
onClick={() => dialogRef.current?.open()}
>
{__("Edit")}
</DropdownItem>
</Authorized>
<Authorized entity="Task" action="deleteTask">
)}
{isAuthorized("Task", "deleteTask") && (
<DropdownItem
variant="danger"
icon={IconTrashCan}
@@ -248,7 +249,7 @@ function TaskRow(props: TaskRowProps) {
>
{__("Delete")}
</DropdownItem>
</Authorized>
)}
</ActionDropdown>
)}
</div>

View File

@@ -14,11 +14,11 @@ import {
} from "@probo/ui";
import { useTranslate } from "@probo/i18n";
import { useFragment } from "react-relay";
import { useMemo, useState, useCallback, useEffect } from "react";
import { useMemo, useState, useCallback, useEffect, use } from "react";
import { sprintf, getAuditStateVariant, getAuditStateLabel, formatDate, getTrustCenterVisibilityOptions } from "@probo/helpers";
import { useOrganizationId } from "/hooks/useOrganizationId";
import type { TrustCenterAuditsCardFragment$key } from "./__generated__/TrustCenterAuditsCardFragment.graphql";
import { isAuthorized } from "/permissions";
import { PermissionsContext } from "/providers/PermissionsContext";
const trustCenterAuditFragment = graphql`
fragment TrustCenterAuditsCardFragment on Audit {
@@ -124,8 +124,8 @@ function AuditRow(props: {
const organizationId = useOrganizationId();
const { __ } = useTranslate();
const [optimisticValue, setOptimisticValue] = useState<string | null>(null);
const canUpdate = organizationId ? isAuthorized(organizationId, "TrustCenter", "updateTrustCenter") : false;
const { isAuthorized } = use(PermissionsContext);
const canUpdate = organizationId ? isAuthorized("TrustCenter", "updateTrustCenter") : false;
const handleValueChange = useCallback((value: string | {}) => {
const stringValue = typeof value === 'string' ? value : '';

View File

@@ -17,10 +17,10 @@ import {
import { useTranslate } from "@probo/i18n";
import type { TrustCenterDocumentsCardFragment$key } from "./__generated__/TrustCenterDocumentsCardFragment.graphql";
import { useFragment } from "react-relay";
import { useMemo, useState, useCallback, useEffect } from "react";
import { useMemo, useState, useCallback, useEffect, use } from "react";
import { sprintf, getTrustCenterVisibilityOptions } from "@probo/helpers";
import { useOrganizationId } from "/hooks/useOrganizationId";
import { isAuthorized } from "/permissions";
import { PermissionsContext } from "/providers/PermissionsContext";
const trustCenterDocumentFragment = graphql`
fragment TrustCenterDocumentsCardFragment on Document {
@@ -129,8 +129,8 @@ function DocumentRow(props: {
const organizationId = useOrganizationId();
const { __ } = useTranslate();
const [optimisticValue, setOptimisticValue] = useState<string | null>(null);
const canUpdate = organizationId ? isAuthorized(organizationId, "TrustCenter", "updateTrustCenter") : false;
const { isAuthorized } = use(PermissionsContext);
const canUpdate = isAuthorized("TrustCenter", "updateTrustCenter");
const handleValueChange = useCallback((value: string | {}) => {
const stringValue = typeof value === 'string' ? value : '';

View File

@@ -18,12 +18,10 @@ import {
import { useTranslate } from "@probo/i18n";
import type { TrustCenterFilesCardFragment$key, TrustCenterFilesCardFragment$data } from "./__generated__/TrustCenterFilesCardFragment.graphql";
import { useFragment } from "react-relay";
import { useMemo, useState, useCallback, useEffect } from "react";
import { useMemo, useState, useCallback, useEffect, use } from "react";
import { sprintf, getTrustCenterVisibilityOptions } from "@probo/helpers";
import { formatDate } from "@probo/helpers";
import { Authorized } from "/permissions";
import { isAuthorized } from "/permissions";
import { useParams } from "react-router";
import { PermissionsContext } from "/providers/PermissionsContext";
const trustCenterFileFragment = graphql`
fragment TrustCenterFilesCardFragment on TrustCenterFile {
@@ -150,9 +148,8 @@ function FileRow(props: {
const file = props.file;
const { __ } = useTranslate();
const [optimisticValue, setOptimisticValue] = useState<string | null>(null);
const { organizationId } = useParams();
const canUpdate = organizationId ? isAuthorized(organizationId, "TrustCenter", "updateTrustCenter") : false;
const { isAuthorized } = use(PermissionsContext);
const canUpdate = isAuthorized("TrustCenter", "updateTrustCenter");
const handleValueChange = useCallback((value: string | {}) => {
const stringValue = typeof value === 'string' ? value : '';
@@ -207,7 +204,7 @@ function FileRow(props: {
onClick={() => window.open(file.fileUrl, '_blank', 'noopener,noreferrer')}
title={__("Download")}
/>
<Authorized entity="TrustCenterFile" action="updateTrustCenterFile">
{isAuthorized("TrustCenterFile", "updateTrustCenterFile") && (
<Button
variant="secondary"
icon={IconPencil}
@@ -215,8 +212,8 @@ function FileRow(props: {
disabled={props.disabled}
title={__("Edit")}
/>
</Authorized>
<Authorized entity="TrustCenterFile" action="deleteTrustCenterFile">
)}
{isAuthorized("TrustCenterFile", "deleteTrustCenterFile") && (
<Button
variant="danger"
icon={IconTrashCan}
@@ -224,7 +221,7 @@ function FileRow(props: {
disabled={props.disabled}
title={__("Delete")}
/>
</Authorized>
)}
</div>
</Td>
</Tr>

View File

@@ -14,14 +14,14 @@ import {
IconPencil,
IconArrowLink,
} from "@probo/ui";
import { type ReactNode, useRef, useState } from "react";
import { type ReactNode, useRef, useState, use } from "react";
import {
useTrustCenterReferences,
useUpdateTrustCenterReferenceRankMutation,
} from "/hooks/graph/TrustCenterReferenceGraph";
import { TrustCenterReferenceDialog, type TrustCenterReferenceDialogRef } from "./TrustCenterReferenceDialog";
import { DeleteTrustCenterReferenceDialog } from "./DeleteTrustCenterReferenceDialog";
import { Authorized } from "/permissions";
import { PermissionsContext } from "/providers/PermissionsContext";
type Props = {
trustCenterId: string;
@@ -41,6 +41,7 @@ type Reference = {
export function TrustCenterReferencesSection({ trustCenterId }: Props) {
const { __ } = useTranslate();
const { isAuthorized } = use(PermissionsContext);
const dialogRef = useRef<TrustCenterReferenceDialogRef>(null);
const [draggedIndex, setDraggedIndex] = useState<number | null>(null);
const [dragOverIndex, setDragOverIndex] = useState<number | null>(null);
@@ -112,14 +113,14 @@ export function TrustCenterReferencesSection({ trustCenterId }: Props) {
{__("Showcase your customers and partners on your trust center")}
</p>
</div>
<Authorized entity="TrustCenter" action="createTrustCenterReference">
{isAuthorized("TrustCenter", "createTrustCenterReference") && (
<Button
icon={IconPlusLarge}
onClick={handleCreate}
>
{__("Add Reference")}
</Button>
</Authorized>
)}
</div>
<Table>
@@ -190,6 +191,7 @@ function ReferenceRow({
onDrop,
}: ReferenceRowProps) {
const [isMouseDown, setIsMouseDown] = useState(false);
const { isAuthorized } = use(PermissionsContext);
const className = [
isDragging && "opacity-50 cursor-grabbing",
@@ -231,14 +233,14 @@ function ReferenceRow({
icon={IconArrowLink}
onClick={onVisitWebsite}
/>
<Authorized entity="TrustCenterReference" action="updateTrustCenterReference">
{isAuthorized("TrustCenterReference", "updateTrustCenterReference") && (
<Button
variant="secondary"
icon={IconPencil}
onClick={onEdit}
/>
</Authorized>
<Authorized entity="TrustCenterReference" action="deleteTrustCenterReference">
)}
{isAuthorized("TrustCenterReference", "deleteTrustCenterReference") && (
<DeleteTrustCenterReferenceDialog
referenceId={reference.id}
referenceName={reference.name}
@@ -249,7 +251,7 @@ function ReferenceRow({
icon={IconTrashCan}
/>
</DeleteTrustCenterReferenceDialog>
</Authorized>
)}
</div>
</Td>
</Tr>

View File

@@ -14,11 +14,11 @@ import {
} from "@probo/ui";
import { useTranslate } from "@probo/i18n";
import { useFragment } from "react-relay";
import { useMemo, useState, useEffect } from "react";
import { useMemo, useState, use } from "react";
import { sprintf } from "@probo/helpers";
import { useOrganizationId } from "/hooks/useOrganizationId";
import { isAuthorized } from "/permissions/permissions";
import type { TrustCenterVendorsCardFragment$key } from "./__generated__/TrustCenterVendorsCardFragment.graphql";
import { PermissionsContext } from "/providers/PermissionsContext";
const trustCenterVendorFragment = graphql`
fragment TrustCenterVendorsCardFragment on Vendor {
@@ -49,44 +49,14 @@ type Props<Params> = {
export function TrustCenterVendorsCard<Params>(props: Props<Params>) {
const { __ } = useTranslate();
const organizationId = useOrganizationId();
const [limit, setLimit] = useState<number | null>(100);
const [canUpdate, setCanUpdate] = useState<boolean>(false);
const { isAuthorized } = use(PermissionsContext);
const canUpdate = isAuthorized("Vendor", "updateVendor");
const vendors = useMemo(() => {
return limit ? props.vendors.slice(0, limit) : props.vendors;
}, [props.vendors, limit]);
const showMoreButton = limit !== null && props.vendors.length > limit;
useEffect(() => {
if (!organizationId) {
setCanUpdate(false);
return;
}
try {
const authorized = isAuthorized(organizationId, "Vendor", "updateVendor");
setCanUpdate(authorized);
} catch (promise) {
if (promise instanceof Promise) {
promise
.then(() => {
try {
const authorized = isAuthorized(organizationId, "Vendor", "updateVendor");
setCanUpdate(authorized);
} catch {
setCanUpdate(false);
}
})
.catch(() => {
setCanUpdate(false);
});
} else {
setCanUpdate(false);
}
}
}, [organizationId]);
const onToggleVisibility = (vendorId: string, showOnTrustCenter: boolean) => {
props.onToggleVisibility({
variables: {

View File

@@ -39,15 +39,14 @@ import {
UserDropdown as UserDropdownRoot,
useToast,
} from "@probo/ui";
import { Suspense, useEffect, useState } from "react";
import { Suspense, use, useEffect, useState } from "react";
import { ErrorBoundary } from "react-error-boundary";
import { useLazyLoadQuery } from "react-relay";
import { Link, Navigate, Outlet, useParams } from "react-router";
import { graphql } from "relay-runtime";
import type { MainLayoutQuery as MainLayoutQueryType } from "./__generated__/MainLayoutQuery.graphql";
import { PageError } from "/components/PageError";
import { Authorized } from "/permissions";
import { PermissionsProvider } from "/providers/PermissionsProvider";
import { PermissionsContext, PermissionsProvider } from "/providers/PermissionsProvider";
import { buildEndpoint } from "/providers/RelayProviders";
const MainLayoutQuery = graphql`
@@ -98,6 +97,7 @@ function MainLayoutContent({
prefix: string;
}) {
const { __ } = useTranslate();
const { isAuthorized } = use(PermissionsContext);
const data = useLazyLoadQuery<MainLayoutQueryType>(MainLayoutQuery, {
organizationId,
});
@@ -116,132 +116,132 @@ function MainLayoutContent({
}
sidebar={
<ul className="space-y-[2px]">
<Authorized entity="Organization" action="listMeetings">
{isAuthorized("Organization", "listMeetings") && (
<SidebarItem
label={__("Meetings")}
icon={IconCalendar1}
to={`${prefix}/meetings`}
/>
</Authorized>
<Authorized entity="Organization" action="listTasks">
)}
{isAuthorized("Organization", "listTasks") && (
<SidebarItem
label={__("Tasks")}
icon={IconInboxEmpty}
to={`${prefix}/tasks`}
/>
</Authorized>
<Authorized entity="Organization" action="listMeasures">
)}
{isAuthorized("Organization", "listMeasures") && (
<SidebarItem
label={__("Measures")}
icon={IconTodo}
to={`${prefix}/measures`}
/>
</Authorized>
<Authorized entity="Organization" action="listRisks">
)}
{isAuthorized("Organization", "listRisks") && (
<SidebarItem
label={__("Risks")}
icon={IconFire3}
to={`${prefix}/risks`}
/>
</Authorized>
<Authorized entity="Organization" action="listFrameworks">
)}
{isAuthorized("Organization", "listFrameworks") && (
<SidebarItem
label={__("Frameworks")}
icon={IconBank}
to={`${prefix}/frameworks`}
/>
</Authorized>
<Authorized entity="Organization" action="listPeople">
)}
{isAuthorized("Organization", "listPeople") && (
<SidebarItem
label={__("People")}
icon={IconGroup1}
to={`${prefix}/people`}
/>
</Authorized>
<Authorized entity="Organization" action="listVendors">
)}
{isAuthorized("Organization", "listVendors") && (
<SidebarItem
label={__("Vendors")}
icon={IconStore}
to={`${prefix}/vendors`}
/>
</Authorized>
<Authorized entity="Organization" action="listDocuments">
)}
{isAuthorized("Organization", "listDocuments") && (
<SidebarItem
label={__("Documents")}
icon={IconPageTextLine}
to={`${prefix}/documents`}
/>
</Authorized>
<Authorized entity="Organization" action="listAssets">
)}
{isAuthorized("Organization", "listAssets") && (
<SidebarItem
label={__("Assets")}
icon={IconBox}
to={`${prefix}/assets`}
/>
</Authorized>
<Authorized entity="Organization" action="listData">
)}
{isAuthorized("Organization", "listData") && (
<SidebarItem
label={__("Data")}
icon={IconListStack}
to={`${prefix}/data`}
/>
</Authorized>
<Authorized entity="Organization" action="listAudits">
)}
{isAuthorized("Organization", "listAudits") && (
<SidebarItem
label={__("Audits")}
icon={IconMedal}
to={`${prefix}/audits`}
/>
</Authorized>
<Authorized entity="Organization" action="listNonconformities">
)}
{isAuthorized("Organization", "listNonconformities") && (
<SidebarItem
label={__("Nonconformities")}
icon={IconCrossLargeX}
to={`${prefix}/nonconformities`}
/>
</Authorized>
<Authorized entity="Organization" action="listObligations">
)}
{isAuthorized("Organization", "listObligations") && (
<SidebarItem
label={__("Obligations")}
icon={IconBook}
to={`${prefix}/obligations`}
/>
</Authorized>
<Authorized entity="Organization" action="listContinualImprovements">
)}
{isAuthorized("Organization", "listContinualImprovements") && (
<SidebarItem
label={__("Continual Improvements")}
icon={IconRotateCw}
to={`${prefix}/continual-improvements`}
/>
</Authorized>
<Authorized entity="Organization" action="listProcessingActivities">
)}
{isAuthorized("Organization", "listProcessingActivities") && (
<SidebarItem
label={__("Processing Activities")}
icon={IconCircleProgress}
to={`${prefix}/processing-activities`}
/>
</Authorized>
<Authorized entity="Organization" action="listSnapshots">
)}
{isAuthorized("Organization", "listSnapshots") && (
<SidebarItem
label={__("Snapshots")}
icon={IconClock}
to={`${prefix}/snapshots`}
/>
</Authorized>
<Authorized entity="Organization" action="getTrustCenter">
)}
{isAuthorized("Organization", "getTrustCenter") && (
<SidebarItem
label={__("Trust Center")}
icon={IconShield}
to={`${prefix}/trust-center`}
/>
</Authorized>
<Authorized entity="Organization" action="listMembers">
)}
{isAuthorized("Organization", "listMembers") && (
<SidebarItem
label={__("Settings")}
icon={IconSettingsGear2}
to={`${prefix}/settings`}
/>
</Authorized>
)}
</ul>
}
>
@@ -255,6 +255,7 @@ function MainLayoutContent({
function UserDropdown({ organizationId }: { organizationId: string }) {
const { __ } = useTranslate();
const { toast } = useToast();
const { isAuthorized } = use(PermissionsContext);
const user = useLazyLoadQuery<MainLayoutQueryType>(MainLayoutQuery, {
organizationId,
}).viewer.user;
@@ -290,13 +291,13 @@ function UserDropdown({ organizationId }: { organizationId: string }) {
return (
<UserDropdownRoot fullName={user.fullName} email={user.email}>
<Authorized entity="Organization" action="deleteOrganization">
{isAuthorized("Organization", "deleteOrganization") && (
<UserDropdownItem
to="/api-keys"
icon={IconKey}
label={__("API Keys")}
/>
</Authorized>
)}
<UserDropdownItem
to="mailto:support@getprobo.com"
icon={IconCircleQuestionmark}

View File

@@ -29,7 +29,8 @@ import { VendorsMultiSelectField } from "/components/form/VendorsMultiSelectFiel
import type { AssetGraphNodeQuery } from "/hooks/graph/__generated__/AssetGraphNodeQuery.graphql";
import { useFormWithSchema } from "/hooks/useFormWithSchema";
import { useOrganizationId } from "/hooks/useOrganizationId";
import { Authorized } from "/permissions";
import { use } from "react";
import { PermissionsContext } from "/providers/PermissionsContext";
const updateAssetSchema = z.object({
name: z.string().min(1, "Name is required"),
@@ -51,7 +52,7 @@ export default function AssetDetailsPage(props: Props) {
const organizationId = useOrganizationId();
const { snapshotId } = useParams<{ snapshotId?: string }>();
const isSnapshotMode = Boolean(snapshotId);
const { isAuthorized } = use(PermissionsContext);
if (!assetEntry || !assetEntry.id) {
return <div>{__("Asset not found")}</div>;
}
@@ -116,7 +117,7 @@ export default function AssetDetailsPage(props: Props) {
</Badge>
</div>
{!isSnapshotMode && (
<Authorized entity="Asset" action="deleteAsset">
isAuthorized("Asset", "deleteAsset") && (
<ActionDropdown variant="secondary">
<DropdownItem
variant="danger"
@@ -126,7 +127,7 @@ export default function AssetDetailsPage(props: Props) {
{__("Delete")}
</DropdownItem>
</ActionDropdown>
</Authorized>
)
)}
</div>
@@ -182,11 +183,11 @@ export default function AssetDetailsPage(props: Props) {
<div className="flex justify-end">
{formState.isDirty && !isSnapshotMode && (
<Authorized entity="Asset" action="updateAsset">
isAuthorized("Asset", "updateAsset") && (
<Button type="submit" disabled={formState.isSubmitting}>
{formState.isSubmitting ? __("Updating...") : __("Update")}
</Button>
</Authorized>
)
)}
</div>
</form>

View File

@@ -23,7 +23,7 @@ import {
} from "/hooks/graph/AssetGraph";
import type { AssetGraphListQuery } from "/hooks/graph/__generated__/AssetGraphListQuery.graphql";
import { useOrganizationId } from "/hooks/useOrganizationId";
import { PermissionsContext } from "/providers/PermissionsProvider";
import { PermissionsContext } from "/providers/PermissionsContext";
const paginatedAssetsFragment = graphql`
fragment AssetsPageFragment on Organization

View File

@@ -34,7 +34,8 @@ import { useFormWithSchema } from "/hooks/useFormWithSchema";
import z from "zod";
import { getAuditStateLabel, getAuditStateVariant, auditStates, fileSize, sprintf, formatDatetime, formatError, formatDate, type GraphQLError } from "@probo/helpers";
import type { AuditGraphNodeQuery } from "/hooks/graph/__generated__/AuditGraphNodeQuery.graphql";
import { Authorized } from "/permissions";
import { use } from "react";
import { PermissionsContext } from "/providers/PermissionsContext";
const updateAuditSchema = z.object({
name: z.string().nullable().optional(),
@@ -52,7 +53,7 @@ export default function AuditDetailsPage(props: Props) {
const auditEntry = audit.node;
const { __ } = useTranslate();
const organizationId = useOrganizationId();
const { isAuthorized } = use(PermissionsContext);
if (!auditEntry || !auditEntry.id || !auditEntry.framework) {
return <div>{__("Audit not found")}</div>;
}
@@ -146,7 +147,7 @@ export default function AuditDetailsPage(props: Props) {
</Badge>
</div>
<ActionDropdown variant="secondary">
<Authorized entity="Audit" action="deleteAudit">
{isAuthorized("Audit", "deleteAudit") && (
<DropdownItem
variant="danger"
icon={IconTrashCan}
@@ -154,7 +155,7 @@ export default function AuditDetailsPage(props: Props) {
>
{__("Delete")}
</DropdownItem>
</Authorized>
)}
</ActionDropdown>
</div>
@@ -187,11 +188,11 @@ export default function AuditDetailsPage(props: Props) {
<div className="flex justify-end">
{formState.isDirty && (
<Authorized entity="Audit" action="updateAudit">
isAuthorized("Audit", "updateAudit") && (
<Button type="submit" disabled={formState.isSubmitting}>
{formState.isSubmitting ? __("Updating...") : __("Update")}
</Button>
</Authorized>
)
)}
</div>
</form>

View File

@@ -31,8 +31,8 @@ import type {
AuditsPageFragment$key,
} from "./__generated__/AuditsPageFragment.graphql";
import { SortableTable } from "/components/SortableTable";
import { Authorized } from "/permissions";
import { isAuthorized } from "/permissions";
import { PermissionsContext } from "/providers/PermissionsContext";
import { use } from "react";
const paginatedAuditsFragment = graphql`
fragment AuditsPageFragment on Organization
@@ -83,6 +83,7 @@ type Props = {
export default function AuditsPage(props: Props) {
const { __ } = useTranslate();
const organizationId = useOrganizationId();
const { isAuthorized } = use(PermissionsContext);
const data = usePreloadedQuery(auditsQuery, props.queryRef);
const pagination = usePaginationFragment(
@@ -94,8 +95,8 @@ export default function AuditsPage(props: Props) {
usePageTitle(__("Audits"));
const hasAnyAction = isAuthorized(organizationId, "Audit", "updateAudit") ||
isAuthorized(organizationId, "Audit", "deleteAudit");
const hasAnyAction = isAuthorized("Audit", "updateAudit") ||
isAuthorized("Audit", "deleteAudit");
return (
<div className="space-y-6">
@@ -105,14 +106,14 @@ export default function AuditsPage(props: Props) {
"Manage your organization's compliance audits and their progress."
)}
>
<Authorized entity="Organization" action="createAudit">
{isAuthorized("Organization", "createAudit") && (
<CreateAuditDialog
connection={connectionId}
organizationId={organizationId}
>
<Button icon={IconPlusLarge}>{__("Add audit")}</Button>
</CreateAuditDialog>
</Authorized>
)}
</PageHeader>
<SortableTable {...pagination}>
<Thead>
@@ -153,6 +154,7 @@ function AuditRow({
const organizationId = useOrganizationId();
const { __ } = useTranslate();
const deleteAudit = useDeleteAudit(entry, connectionId);
const { isAuthorized } = use(PermissionsContext);
return (
<Tr to={`/organizations/${organizationId}/audits/${entry.id}`}>
@@ -177,7 +179,7 @@ function AuditRow({
{hasAnyAction && (
<Td noLink width={50} className="text-end">
<ActionDropdown>
<Authorized entity="Audit" action="deleteAudit">
{isAuthorized("Audit", "deleteAudit") && (
<DropdownItem
onClick={deleteAudit}
variant="danger"
@@ -185,7 +187,7 @@ function AuditRow({
>
{__("Delete")}
</DropdownItem>
</Authorized>
)}
</ActionDropdown>
</Td>
)}

View File

@@ -35,7 +35,8 @@ import z from "zod";
import { getStatusVariant, getStatusLabel, formatDatetime, validateSnapshotConsistency } from "@probo/helpers";
import { SnapshotBanner } from "/components/SnapshotBanner";
import type { ContinualImprovementGraphNodeQuery } from "/hooks/graph/__generated__/ContinualImprovementGraphNodeQuery.graphql";
import { Authorized } from "/permissions";
import { use } from "react";
import { PermissionsContext } from "/providers/PermissionsContext";
const updateImprovementSchema = z.object({
referenceId: z.string().min(1, "Reference ID is required"),
@@ -59,7 +60,7 @@ export default function ContinualImprovementDetailsPage(props: Props) {
const organizationId = useOrganizationId();
const { snapshotId } = useParams<{ snapshotId?: string }>();
const isSnapshotMode = Boolean(snapshotId);
const { isAuthorized } = use(PermissionsContext);
if (!improvement) {
return <div>{__("Continual improvement entry not found")}</div>;
}
@@ -150,13 +151,13 @@ export default function ContinualImprovementDetailsPage(props: Props) {
]}
/>
{!isSnapshotMode && (
<Authorized entity="ContinualImprovement" action="deleteContinualImprovement">
isAuthorized("ContinualImprovement", "deleteContinualImprovement") && (
<ActionDropdown>
<DropdownItem onClick={deleteImprovement} variant="danger">
{__("Delete")}
</DropdownItem>
</ActionDropdown>
</Authorized>
)
)}
</div>
@@ -289,7 +290,7 @@ export default function ContinualImprovementDetailsPage(props: Props) {
{!isSnapshotMode && (
<div className="flex justify-end pt-4">
<Authorized entity="ContinualImprovement" action="updateContinualImprovement">
{isAuthorized("ContinualImprovement", "updateContinualImprovement") && (
<Button
type="submit"
variant="primary"
@@ -297,7 +298,7 @@ export default function ContinualImprovementDetailsPage(props: Props) {
>
{formState.isSubmitting ? __("Saving...") : __("Save Changes")}
</Button>
</Authorized>
)}
</div>
)}
</form>

View File

@@ -37,8 +37,8 @@ import type {
ContinualImprovementsPageFragment$key,
ContinualImprovementsPageFragment$data,
} from "./__generated__/ContinualImprovementsPageFragment.graphql";
import { Authorized } from "/permissions";
import { isAuthorized } from "/permissions";
import { use } from "react";
import { PermissionsContext } from "/providers/PermissionsContext";
interface ContinualImprovementsPageProps {
queryRef: PreloadedQuery<ContinualImprovementsPageQuery>;
@@ -93,6 +93,7 @@ export default function ContinualImprovementsPage({ queryRef }: ContinualImprove
const organizationId = useOrganizationId();
const { snapshotId } = useParams<{ snapshotId?: string }>();
const isSnapshotMode = Boolean(snapshotId);
const { isAuthorized } = use(PermissionsContext);
usePageTitle(__("Continual Improvements"));
@@ -130,8 +131,8 @@ export default function ContinualImprovementsPage({ queryRef }: ContinualImprove
const improvements = data?.continualImprovements?.edges?.map((edge) => edge.node) ?? [];
const hasAnyAction = !isSnapshotMode && (
isAuthorized(organizationId, "ContinualImprovement", "updateContinualImprovement") ||
isAuthorized(organizationId, "ContinualImprovement", "deleteContinualImprovement")
isAuthorized("ContinualImprovement", "updateContinualImprovement") ||
isAuthorized("ContinualImprovement", "deleteContinualImprovement")
);
return (
@@ -141,7 +142,7 @@ export default function ContinualImprovementsPage({ queryRef }: ContinualImprove
)}
<PageHeader title={__("Continual Improvements")} description={__("Manage your continual improvements.")}>
{!isSnapshotMode && (
<Authorized entity="Organization" action="createContinualImprovement">
isAuthorized("Organization", "createContinualImprovement") && (
<CreateContinualImprovementDialog
organizationId={organizationId}
connectionId={connectionId}
@@ -150,7 +151,7 @@ export default function ContinualImprovementsPage({ queryRef }: ContinualImprove
{__("Add continual improvement")}
</Button>
</CreateContinualImprovementDialog>
</Authorized>
)
)}
</PageHeader>
@@ -225,6 +226,7 @@ function ImprovementRow({
const [deleteImprovement] = useMutation(deleteContinualImprovementMutation);
const confirm = useConfirm();
const isSnapshotMode = Boolean(snapshotId);
const { isAuthorized } = use(PermissionsContext);
const handleDelete = () => {
@@ -282,7 +284,7 @@ function ImprovementRow({
{hasAnyAction && (
<Td noLink width={50} className="text-end">
<ActionDropdown>
<Authorized entity="ContinualImprovement" action="deleteContinualImprovement">
{isAuthorized("ContinualImprovement", "deleteContinualImprovement") && (
<DropdownItem
icon={IconTrashCan}
variant="danger"
@@ -290,7 +292,7 @@ function ImprovementRow({
>
{__("Delete")}
</DropdownItem>
</Authorized>
)}
</ActionDropdown>
</Td>
)}

View File

@@ -35,8 +35,8 @@ import type {
import type { DataListQuery } from "./__generated__/DataListQuery.graphql";
import { SortableTable } from "/components/SortableTable";
import { SnapshotBanner } from "/components/SnapshotBanner";
import { Authorized } from "/permissions";
import { isAuthorized } from "/permissions";
import { use } from "react";
import { PermissionsContext } from "/providers/PermissionsContext";
const paginatedDataFragment = graphql`
fragment DataPageFragment on Organization
@@ -93,6 +93,7 @@ export default function DataPage(props: Props) {
const organizationId = useOrganizationId();
const { snapshotId } = useParams<{ snapshotId?: string }>();
const isSnapshotMode = Boolean(snapshotId);
const { isAuthorized } = use(PermissionsContext);
const queryData = usePreloadedQuery<DatumGraphListQuery>(
dataQuery,
@@ -121,8 +122,9 @@ export default function DataPage(props: Props) {
usePageTitle(__("Data"));
const hasAnyAction = !isSnapshotMode && ( isAuthorized(organizationId, "Datum", "updateDatum") ||
isAuthorized(organizationId, "Datum", "deleteDatum")
const hasAnyAction = !isSnapshotMode && (
isAuthorized("Datum", "updateDatum") ||
isAuthorized("Datum", "deleteDatum")
);
return (
@@ -137,7 +139,7 @@ export default function DataPage(props: Props) {
)}
>
{!snapshotId && (
<Authorized entity="Organization" action="createDatum">
isAuthorized("Organization", "createDatum") && (
<CreateDatumDialog
connection={connectionId}
organizationId={organizationId}
@@ -145,7 +147,7 @@ export default function DataPage(props: Props) {
>
<Button icon={IconPlusLarge}>{__("Add data")}</Button>
</CreateDatumDialog>
</Authorized>
)
)}
</PageHeader>
<SortableTable
@@ -186,7 +188,7 @@ function DataRow({
const { __ } = useTranslate();
const deleteDatum = useDeleteDatum(entry, connectionId);
const vendors = entry.vendors?.edges.map((edge) => edge.node) ?? [];
const { isAuthorized } = use(PermissionsContext);
const detailUrl = snapshotId
? `/organizations/${organizationId}/snapshots/${snapshotId}/data/${entry.id}`
: `/organizations/${organizationId}/data/${entry.id}`;
@@ -228,7 +230,7 @@ function DataRow({
{hasAnyAction && (
<Td noLink width={50} className="text-end">
<ActionDropdown>
<Authorized entity="Datum" action="deleteDatum">
{isAuthorized("Datum", "deleteDatum") && (
<DropdownItem
onClick={deleteDatum}
variant="danger"
@@ -236,7 +238,7 @@ function DataRow({
>
{__("Delete")}
</DropdownItem>
</Authorized>
)}
</ActionDropdown>
</Td>
)}

View File

@@ -29,7 +29,8 @@ import z from "zod";
import { SnapshotBanner } from "/components/SnapshotBanner";
import { validateSnapshotConsistency } from "@probo/helpers";
import type { DatumGraphNodeQuery } from "/hooks/graph/__generated__/DatumGraphNodeQuery.graphql";
import { Authorized } from "/permissions";
import { use } from "react";
import { PermissionsContext } from "/providers/PermissionsContext";
const updateDatumSchema = z.object({
name: z.string().min(1, "Name is required"),
@@ -57,6 +58,7 @@ export default function DatumDetailsPage(props: Props) {
const { __ } = useTranslate();
const organizationId = useOrganizationId();
const { isAuthorized } = use(PermissionsContext);
const deleteDatum = useDeleteDatum(
datumEntry,
@@ -125,7 +127,7 @@ export default function DatumDetailsPage(props: Props) {
<Badge variant="info">{datumEntry?.dataClassification}</Badge>
</div>
{!isSnapshotMode && (
<Authorized entity="Datum" action="deleteDatum">
isAuthorized("Datum", "deleteDatum") && (
<ActionDropdown variant="secondary">
<DropdownItem
variant="danger"
@@ -135,7 +137,7 @@ export default function DatumDetailsPage(props: Props) {
{__("Delete")}
</DropdownItem>
</ActionDropdown>
</Authorized>
)
)}
</div>
@@ -180,11 +182,11 @@ export default function DatumDetailsPage(props: Props) {
{!isSnapshotMode && (
<div className="flex justify-end">
{formState.isDirty && (
<Authorized entity="Datum" action="updateDatum">
isAuthorized("Datum", "updateDatum") && (
<Button type="submit" disabled={formState.isSubmitting}>
{formState.isSubmitting ? __("Updating...") : __("Update")}
</Button>
</Authorized>
)
)}
</div>
)}

View File

@@ -67,7 +67,7 @@ import {
PdfDownloadDialog,
type PdfDownloadDialogRef,
} from "/components/documents/PdfDownloadDialog";
import { useRef, useState } from "react";
import { use, useRef, useState } from "react";
import type { NodeOf } from "/types.ts";
import clsx from "clsx";
import { PeopleSelectField } from "/components/form/PeopleSelectField";
@@ -76,7 +76,7 @@ import { DocumentTypeOptions } from "/components/form/DocumentTypeOptions";
import { DocumentClassificationOptions } from "/components/form/DocumentClassificationOptions";
import { z } from "zod";
import { useFormWithSchema } from "/hooks/useFormWithSchema";
import { Authorized } from "/permissions";
import { PermissionsContext } from "/providers/PermissionsContext";
type Props = {
queryRef: PreloadedQuery<DocumentGraphNodeQuery>;
@@ -132,37 +132,6 @@ const documentFragment = graphql`
}
`;
graphql`
fragment DocumentDetailPageRowFragment on Document {
id
title
description
documentType
classification
updatedAt
owner {
id
fullName
}
versions(first: 1) {
edges {
node {
id
status
signatures(first: 100) {
edges {
node {
id
state
}
}
}
}
}
}
}
`;
const publishDocumentVersionMutation = graphql`
mutation DocumentDetailPagePublishMutation(
$input: PublishDocumentVersionInput!
@@ -170,7 +139,31 @@ const publishDocumentVersionMutation = graphql`
publishDocumentVersion(input: $input) {
document {
id
...DocumentDetailPageRowFragment
title
description
documentType
classification
updatedAt
owner {
id
fullName
}
versions(first: 1) {
edges {
node {
id
status
signatures(first: 100) {
edges {
node {
id
state
}
}
}
}
}
}
}
}
}
@@ -230,6 +223,7 @@ export default function DocumentDetailPage(props: Props) {
const { __ } = useTranslate();
const organizationId = useOrganizationId();
const navigate = useNavigate();
const { isAuthorized } = use(PermissionsContext);
const [isEditingTitle, setIsEditingTitle] = useState(false);
const [isEditingOwner, setIsEditingOwner] = useState(false);
@@ -522,16 +516,16 @@ export default function DocumentDetailPage(props: Props) {
</Dropdown>
<ActionDropdown variant="secondary">
<Authorized entity="Document" action="updateDocument">
{isAuthorized("Document", "updateDocument") && (
<DropdownItem
onClick={() => updateDialogRef.current?.open()}
icon={IconPencil}
>
{isDraft ? __("Edit draft document") : __("Create new draft")}
</DropdownItem>
</Authorized>
)}
{isDraft && versions.length > 1 && (
<Authorized entity="Document" action="deleteDocument">
isAuthorized("Document", "deleteDocument") && (
<DropdownItem
onClick={handleDeleteDraft}
icon={IconTrashCan}
@@ -539,7 +533,7 @@ export default function DocumentDetailPage(props: Props) {
>
{__("Delete draft document")}
</DropdownItem>
</Authorized>
)
)}
<DropdownItem
onClick={() => pdfDownloadDialogRef.current?.open()}
@@ -548,7 +542,7 @@ export default function DocumentDetailPage(props: Props) {
>
{__("Download PDF")}
</DropdownItem>
<Authorized entity="Document" action="deleteDocument">
{isAuthorized("Document", "deleteDocument") && (
<DropdownItem
variant="danger"
icon={IconTrashCan}
@@ -557,7 +551,7 @@ export default function DocumentDetailPage(props: Props) {
>
{__("Delete document")}
</DropdownItem>
</Authorized>
)}
</ActionDropdown>
</div>
</div>

View File

@@ -29,7 +29,7 @@ import {
useLazyLoadQuery,
type PreloadedQuery,
} from "react-relay";
import { useRef } from "react";
import { use, useRef } from "react";
import { graphql } from "relay-runtime";
import type { DocumentGraphListQuery } from "/hooks/graph/__generated__/DocumentGraphListQuery.graphql";
import {
@@ -57,8 +57,7 @@ import {
type BulkExportDialogRef,
} from "/components/documents/BulkExportDialog";
import type { DocumentsPageUserEmailQuery } from "./__generated__/DocumentsPageUserEmailQuery.graphql";
import { Authorized } from "/permissions";
import { isAuthorized } from "/permissions";
import { PermissionsContext } from "/providers/PermissionsProvider.tsx";
const documentsFragment = graphql`
fragment DocumentsPageListFragment on Organization
@@ -107,6 +106,7 @@ const UserEmailQuery = graphql`
export default function DocumentsPage(props: Props) {
const { __ } = useTranslate();
const { isAuthorized } = use(PermissionsContext);
const organization = usePreloadedQuery(
documentsQuery,
@@ -137,8 +137,8 @@ export default function DocumentsPage(props: Props) {
usePageTitle(__("Documents"));
const hasAnyAction = isAuthorized(organization.id, "Document", "updateDocument") ||
isAuthorized(organization.id, "Document", "deleteDocument");
const hasAnyAction = isAuthorized("Document", "updateDocument") ||
isAuthorized("Document", "deleteDocument");
const handleSendSigningNotifications = () => {
sendSigningNotifications({
@@ -198,21 +198,21 @@ export default function DocumentsPage(props: Props) {
description={__("Manage your organization's documents")}
>
<div className="flex gap-2">
<Authorized entity="Document" action="sendSigningNotifications">
<Button
icon={IconBell2}
variant="secondary"
onClick={handleSendSigningNotifications}
>
{__("Send signing notifications")}
</Button>
</Authorized>
<Authorized entity="Organization" action="createDocument">
{isAuthorized("Document", "sendSigningNotifications") && (
<Button
icon={IconBell2}
variant="secondary"
onClick={handleSendSigningNotifications}
>
{__("Send signing notifications")}
</Button>
)}
{isAuthorized("Organization", "createDocument") && (
<CreateDocumentDialog
connection={connectionId}
trigger={<Button icon={IconPlusLarge}>{__("New document")}</Button>}
/>
</Authorized>
)}
</div>
</PageHeader>
{documents.length > 0 ? (
@@ -258,7 +258,7 @@ export default function DocumentsPage(props: Props) {
</button>
</div>
<div className="flex gap-2 items-center">
<Authorized entity="Document" action="updateDocument">
{isAuthorized("Document", "updateDocument") && (
<PublishDocumentsDialog
documentIds={selection}
onSave={clear}
@@ -270,8 +270,8 @@ export default function DocumentsPage(props: Props) {
{__("Publish")}
</Button>
</PublishDocumentsDialog>
</Authorized>
<Authorized entity="Document" action="bulkRequestSignatures">
)}
{isAuthorized("Document", "bulkRequestSignatures") && (
<SignatureDocumentsDialog
documentIds={selection}
onSave={clear}
@@ -284,7 +284,7 @@ export default function DocumentsPage(props: Props) {
{__("Request signature")}
</Button>
</SignatureDocumentsDialog>
</Authorized>
)}
<BulkExportDialog
ref={bulkExportDialogRef}
onExport={handleBulkExport}
@@ -300,7 +300,7 @@ export default function DocumentsPage(props: Props) {
{__("Export")}
</Button>
</BulkExportDialog>
<Authorized entity="Document" action="deleteDocument">
{isAuthorized("Document", "deleteDocument") && (
<Button
variant="danger"
icon={IconTrashCan}
@@ -309,7 +309,7 @@ export default function DocumentsPage(props: Props) {
>
{__("Delete")}
</Button>
</Authorized>
)}
</div>
</div>
</Th>
@@ -392,6 +392,7 @@ function DocumentRow({
onCheck: () => void;
hasAnyAction: boolean;
}) {
const { isAuthorized } = use(PermissionsContext);
const document = useFragment<DocumentsPageRowFragment$key>(
rowFragment,
documentKey
@@ -465,7 +466,7 @@ function DocumentRow({
{hasAnyAction && (
<Td noLink width={50} className="text-end w-18">
<ActionDropdown>
<Authorized entity="Document" action="deleteDocument">
{isAuthorized("Document", "deleteDocument") && (
<DropdownItem
variant="danger"
icon={IconTrashCan}
@@ -473,7 +474,7 @@ function DocumentRow({
>
{__("Delete")}
</DropdownItem>
</Authorized>
)}
</ActionDropdown>
</Td>
)}

View File

@@ -8,7 +8,7 @@ import {
Spinner,
} from "@probo/ui";
import { useTranslate } from "@probo/i18n";
import { Suspense, useState, useEffect, useRef } from "react";
import { Suspense, useState, useEffect, useRef, use } from "react";
import type { ItemOf, NodeOf } from "/types";
import { graphql, useFragment, useRefetchableFragment } from "react-relay";
import { usePeople } from "/hooks/graph/PeopleGraph.ts";
@@ -20,7 +20,7 @@ import { useOutletContext } from "react-router";
import type { DocumentSignaturesTab_signature$key } from "/pages/organizations/documents/tabs/__generated__/DocumentSignaturesTab_signature.graphql.ts";
import type { DocumentSignaturesTab_version$key } from "/pages/organizations/documents/tabs/__generated__/DocumentSignaturesTab_version.graphql.ts";
import type { DocumentSignaturesTabRefetchQuery } from "./__generated__/DocumentSignaturesTabRefetchQuery.graphql";
import { Authorized } from "/permissions";
import { PermissionsContext } from "/providers/PermissionsContext";
type Version = NodeOf<DocumentDetailPageDocumentFragment$data["versions"]>;
@@ -246,6 +246,7 @@ function SignatureItem(props: {
}) {
const signature = useFragment(signatureFragment, props.signature);
const { __, dateTimeFormat } = useTranslate();
const { isAuthorized } = use(PermissionsContext);
const [requestSignature, isSendingRequest] = useMutationWithToasts(
requestSignatureMutation,
{
@@ -275,26 +276,26 @@ function SignatureItem(props: {
</div>
</div>
{props.signable && (
<Authorized entity="Document" action="requestSignature">
<Button
variant="secondary"
className="ml-auto"
disabled={isSendingRequest}
onClick={() => {
requestSignature({
variables: {
input: {
documentVersionId: props.versionId,
signatoryId: props.people.id,
isAuthorized("Document", "requestSignature") && (
<Button
variant="secondary"
className="ml-auto"
disabled={isSendingRequest}
onClick={() => {
requestSignature({
variables: {
input: {
documentVersionId: props.versionId,
signatoryId: props.people.id,
},
connections: [props.connectionId],
},
connections: [props.connectionId],
},
});
}}
>
{__("Request signature")}
</Button>
</Authorized>
});
}}
>
{__("Request signature")}
</Button>
)
)}
</div>
);
@@ -331,25 +332,25 @@ function SignatureItem(props: {
{__("Signed")}
</Badge>
) : (
<Authorized entity="DocumentVersionSignature" action="cancelSignatureRequest">
<Button
variant="danger"
className="ml-auto"
disabled={isCancellingSignature}
onClick={() => {
cancelSignature({
variables: {
input: {
documentVersionSignatureId: signature.id,
isAuthorized("DocumentVersionSignature", "cancelSignatureRequest") && (
<Button
variant="danger"
className="ml-auto"
disabled={isCancellingSignature}
onClick={() => {
cancelSignature({
variables: {
input: {
documentVersionSignatureId: signature.id,
},
connections: [props.connectionId],
},
connections: [props.connectionId],
},
});
}}
>
{__("Cancel request")}
</Button>
</Authorized>
});
}}
>
{__("Cancel request")}
</Button>
)
)}
</div>
);

View File

@@ -27,7 +27,8 @@ import { promisifyMutation } from "@probo/helpers";
import type { FrameworkGraphControlNodeQuery } from "/hooks/graph/__generated__/FrameworkGraphControlNodeQuery.graphql";
import { frameworkControlNodeQuery } from "/hooks/graph/FrameworkGraph";
import type { FrameworkDetailPageFragment$data } from "./__generated__/FrameworkDetailPageFragment.graphql";
import { Authorized } from "/permissions";
import { use } from "react";
import { PermissionsContext } from "/providers/PermissionsContext";
const attachMeasureMutation = graphql`
mutation FrameworkControlPageAttachMutation(
@@ -166,7 +167,7 @@ export default function FrameworkControlPage({ queryRef }: Props) {
const organizationId = useOrganizationId();
const confirm = useConfirm();
const navigate = useNavigate();
const { isAuthorized } = use(PermissionsContext);
const [detachMeasure, isDetachingMeasure] = useMutation(detachMeasureMutation);
const [attachMeasure, isAttachingMeasure] = useMutation(attachMeasureMutation);
const [detachDocument, isDetachingDocument] = useMutation(detachDocumentMutation);
@@ -236,7 +237,7 @@ export default function FrameworkControlPage({ queryRef }: Props) {
</div>
</div>
<div className="flex gap-2">
<Authorized entity="Control" action="updateControl">
{isAuthorized("Control", "updateControl") && (
<FrameworkControlDialog
frameworkId={framework.id}
connectionId={connectionId}
@@ -246,8 +247,8 @@ export default function FrameworkControlPage({ queryRef }: Props) {
{__("Edit control")}
</Button>
</FrameworkControlDialog>
</Authorized>
<Authorized entity="Control" action="deleteControl">
)}
{isAuthorized("Control", "deleteControl") && (
<ActionDropdown variant="secondary">
<DropdownItem
icon={IconTrashCan}
@@ -257,7 +258,7 @@ export default function FrameworkControlPage({ queryRef }: Props) {
{__("Delete")}
</DropdownItem>
</ActionDropdown>
</Authorized>
)}
</div>
</div>

View File

@@ -31,7 +31,8 @@ import type { FrameworkDetailPageExportFrameworkMutation } from "./__generated__
import { FrameworkFormDialog } from "./dialogs/FrameworkFormDialog";
import { FrameworkControlDialog } from "./dialogs/FrameworkControlDialog";
import { useMutationWithToasts } from "/hooks/useMutationWithToasts";
import { Authorized } from "/permissions";
import { use } from "react";
import { PermissionsContext } from "/providers/PermissionsContext";
const frameworkDetailFragment = graphql`
fragment FrameworkDetailPageFragment on Framework {
@@ -103,7 +104,7 @@ export default function FrameworkDetailPage(props: Props) {
framework,
ConnectionHandler.getConnectionID(organizationId, connectionListKey)!
);
const { isAuthorized } = use(PermissionsContext);
const [generateFrameworkStateOfApplicability] =
useMutationWithToasts<FrameworkDetailPageGenerateFrameworkStateOfApplicabilityMutation>(
generateFrameworkStateOfApplicabilityMutation,
@@ -150,7 +151,7 @@ export default function FrameworkDetailPage(props: Props) {
</>
}
>
<Authorized entity="Framework" action="updateFramework">
{isAuthorized("Framework", "updateFramework") && (
<FrameworkFormDialog
organizationId={organizationId}
framework={framework}
@@ -159,7 +160,7 @@ export default function FrameworkDetailPage(props: Props) {
{__("Edit")}
</Button>
</FrameworkFormDialog>
</Authorized>
)}
<ActionDropdown variant="secondary">
<DropdownItem
variant="primary"
@@ -191,11 +192,11 @@ export default function FrameworkDetailPage(props: Props) {
>
{__("Export Framework")}
</DropdownItem>
<Authorized entity="Framework" action="deleteFramework">
{isAuthorized("Framework", "deleteFramework") && (
<DropdownItem icon={IconTrashCan} variant="danger" onClick={onDelete}>
{__("Delete")}
</DropdownItem>
</Authorized>
)}
</ActionDropdown>
</PageHeader>
<div className="text-lg font-semibold">
@@ -216,7 +217,7 @@ export default function FrameworkDetailPage(props: Props) {
active={selectedControl?.id === control.id}
/>
))}
<Authorized entity="Organization" action="createControl">
{isAuthorized("Organization", "createControl") && (
<FrameworkControlDialog
frameworkId={framework.id}
connectionId={connectionId}
@@ -226,7 +227,7 @@ export default function FrameworkDetailPage(props: Props) {
{__("Add new control")}
</button>
</FrameworkControlDialog>
</Authorized>
)}
</div>
<Outlet context={{ framework }} />
</div>

View File

@@ -26,11 +26,10 @@ import {
import { Link } from "react-router";
import type { FrameworksPageCardFragment$key } from "./__generated__/FrameworksPageCardFragment.graphql";
import { useMutationWithToasts } from "/hooks/useMutationWithToasts";
import { useState, type ChangeEventHandler } from "react";
import { useState, type ChangeEventHandler, use } from "react";
import { FrameworkLogo } from "/components/FrameworkLogo";
import { FrameworkFormDialog } from "./dialogs/FrameworkFormDialog";
import { Authorized } from "/permissions";
import { isAuthorized } from "/permissions";
import { PermissionsContext } from "/providers/PermissionsContext";
type Props = {
queryRef: PreloadedQuery<FrameworkGraphListQuery>;
@@ -54,6 +53,7 @@ const importFrameworkMutation = graphql`
export default function FrameworksPage(props: Props) {
const { __ } = useTranslate();
const { isAuthorized } = use(PermissionsContext);
usePageTitle(__("Frameworks"));
const data = usePreloadedQuery(frameworksQuery, props.queryRef);
const connectionId = data.organization.frameworks!.__id;
@@ -120,8 +120,8 @@ export default function FrameworksPage(props: Props) {
const isLoading = isUploading || isImporting;
const hasAnyAction = isAuthorized(data.organization.id!, "Framework", "updateFramework") ||
isAuthorized(data.organization.id!, "Framework", "deleteFramework");
const hasAnyAction = isAuthorized("Framework", "updateFramework") ||
isAuthorized("Framework", "deleteFramework");
return (
<div className="space-y-6">
@@ -134,20 +134,22 @@ export default function FrameworksPage(props: Props) {
title={__("Frameworks")}
description={__("Manage your compliance frameworks")}
>
<Authorized entity="Organization" action="createFramework">
<FileButton
variant="secondary"
icon={IconFolderUpload}
onChange={handleUpload}
disabled={isLoading}
>
{__("Import")}
</FileButton>
<FrameworkSelector
onSelect={importNamedFramework}
disabled={isLoading}
/>
</Authorized>
{isAuthorized("Organization", "createFramework") && (
<>
<FileButton
variant="secondary"
icon={IconFolderUpload}
onChange={handleUpload}
disabled={isLoading}
>
{__("Import")}
</FileButton>
<FrameworkSelector
onSelect={importNamedFramework}
disabled={isLoading}
/>
</>
)}
</PageHeader>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
@@ -182,6 +184,7 @@ type FrameworkCardProps = {
function FrameworkCard(props: FrameworkCardProps) {
const framework = useFragment(frameworkCardFragment, props.framework);
const { isAuthorized } = use(PermissionsContext);
const deleteFramework = useDeleteFrameworkMutation(
framework,
props.connectionId
@@ -200,7 +203,7 @@ function FrameworkCard(props: FrameworkCardProps) {
<FrameworkLogo {...framework} />
{props.hasAnyAction && (
<ActionDropdown className="z-10 relative">
<Authorized entity="Framework" action="updateFramework">
{isAuthorized("Framework", "updateFramework") && (
<DropdownItem
icon={IconPencil}
onClick={() => {
@@ -209,8 +212,8 @@ function FrameworkCard(props: FrameworkCardProps) {
>
{__("Edit")}
</DropdownItem>
</Authorized>
<Authorized entity="Framework" action="deleteFramework">
)}
{isAuthorized("Framework", "deleteFramework") && (
<DropdownItem
icon={IconTrashCan}
onClick={() => deleteFramework()}
@@ -218,7 +221,7 @@ function FrameworkCard(props: FrameworkCardProps) {
>
{__("Delete")}
</DropdownItem>
</Authorized>
)}
</ActionDropdown>
)}
</div>

View File

@@ -43,7 +43,8 @@ import {
sprintf,
} from "@probo/helpers";
import MeasureFormDialog from "./dialog/MeasureFormDialog";
import { Authorized } from "/permissions";
import { use } from "react";
import { PermissionsContext } from "/providers/PermissionsContext";
type Props = {
queryRef: PreloadedQuery<MeasureGraphNodeQuery>;
@@ -59,7 +60,7 @@ export default function MeasureDetailPage(props: Props) {
const navigate = useNavigate();
const confirm = useConfirm();
const [updateMeasure, isUpdating] = useUpdateMeasure();
const { isAuthorized } = use(PermissionsContext);
if (!measureId) {
throw new Error(
"Cannot load measure detail page without measureId parameter",
@@ -136,33 +137,35 @@ export default function MeasureDetailPage(props: Props) {
/>
<PageHeader title={measure.name} description={measure.description}>
<Authorized entity="Measure" action="updateMeasure">
<MeasureFormDialog measure={measure}>
<Button variant="secondary" icon={IconPencil}>
{__("Edit")}
</Button>
</MeasureFormDialog>
<Select
disabled={isUpdating}
onValueChange={onStateChange}
name="state"
placeholder={__("Select state")}
className="rounded-full"
value={measure.state}
>
{measureStates.map((state) => (
<Option key={state} value={state}>
{getMeasureStateLabel(__, state)}
</Option>
))}
</Select>
</Authorized>
{isAuthorized("Measure", "updateMeasure") && (
<>
<MeasureFormDialog measure={measure}>
<Button variant="secondary" icon={IconPencil}>
{__("Edit")}
</Button>
</MeasureFormDialog>
<Select
disabled={isUpdating}
onValueChange={onStateChange}
name="state"
placeholder={__("Select state")}
className="rounded-full"
value={measure.state}
>
{measureStates.map((state) => (
<Option key={state} value={state}>
{getMeasureStateLabel(__, state)}
</Option>
))}
</Select>
</>
)}
<ActionDropdown variant="secondary">
<Authorized entity="Measure" action="deleteMeasure">
{isAuthorized("Measure", "deleteMeasure") && (
<DropdownItem variant="danger" icon={IconTrashCan} onClick={onDelete}>
{__("Delete")}
</DropdownItem>
</Authorized>
)}
</ActionDropdown>
</PageHeader>

View File

@@ -40,7 +40,7 @@ import type {
MeasuresPageFragment$key,
} from "./__generated__/MeasuresPageFragment.graphql";
import { groupBy, objectKeys, slugify, sprintf } from "@probo/helpers";
import { useMemo, useRef, useState, type ChangeEventHandler } from "react";
import { useMemo, useRef, useState, type ChangeEventHandler, use } from "react";
import type { NodeOf } from "/types";
import type { MeasuresPageImportMutation } from "./__generated__/MeasuresPageImportMutation.graphql";
import { useMutationWithToasts } from "/hooks/useMutationWithToasts";
@@ -48,8 +48,7 @@ import { useOrganizationId } from "/hooks/useOrganizationId";
import { Link, useParams } from "react-router";
import MeasureFormDialog from "./dialog/MeasureFormDialog";
import { usePageTitle } from "@probo/hooks";
import { Authorized } from "/permissions";
import { isAuthorized } from "/permissions";
import { PermissionsContext } from "/providers/PermissionsContext";
type Props = {
queryRef: PreloadedQuery<MeasureGraphListQuery>;
@@ -92,6 +91,7 @@ const importMeasuresMutation = graphql`
export default function MeasuresPage(props: Props) {
const { __ } = useTranslate();
const { isAuthorized } = use(PermissionsContext);
const organization = usePreloadedQuery(
measuresQuery,
props.queryRef
@@ -115,8 +115,8 @@ export default function MeasuresPage(props: Props) {
const importFileRef = useRef<HTMLInputElement>(null);
usePageTitle(__("Measures"));
const hasAnyAction = isAuthorized(organization.id, "Measure", "updateMeasure") ||
isAuthorized(organization.id, "Measure", "deleteMeasure");
const hasAnyAction = isAuthorized("Measure", "updateMeasure") ||
isAuthorized("Measure", "deleteMeasure");
const handleImport: ChangeEventHandler<HTMLInputElement> = (event) => {
const file = event.target.files?.[0];
@@ -148,21 +148,23 @@ export default function MeasuresPage(props: Props) {
"Measures are actions taken to reduce the risk. Add them to track their implementation status."
)}
>
<Authorized entity="Organization" action="createMeasure">
<FileButton
ref={importFileRef}
variant="secondary"
icon={IconFolderUpload}
onChange={handleImport}
>
{__("Import")}
</FileButton>
<MeasureFormDialog connection={connectionId}>
<Button variant="primary" icon={IconPlusLarge}>
{__("New measure")}
</Button>
</MeasureFormDialog>
</Authorized>
{isAuthorized("Organization", "createMeasure") && (
<>
<FileButton
ref={importFileRef}
variant="secondary"
icon={IconFolderUpload}
onChange={handleImport}
>
{__("Import")}
</FileButton>
<MeasureFormDialog connection={connectionId}>
<Button variant="primary" icon={IconPlusLarge}>
{__("New measure")}
</Button>
</MeasureFormDialog>
</>
)}
</PageHeader>
<MeasureImplementation measures={measures} className="my-10" />
{objectKeys(measuresPerCategory)
@@ -269,6 +271,7 @@ function MeasureRow(props: MeasureRowProps) {
const [deleteMeasure, isDeleting] = useDeleteMeasureMutation();
const confirm = useConfirm();
const organizationId = useOrganizationId();
const { isAuthorized } = use(PermissionsContext);
const onDelete = () => {
confirm(
@@ -306,15 +309,15 @@ function MeasureRow(props: MeasureRowProps) {
{props.hasAnyAction && (
<Td noLink width={50} className="text-end">
<ActionDropdown>
<Authorized entity="Measure" action="updateMeasure">
{isAuthorized("Measure", "updateMeasure") && (
<DropdownItem
icon={IconPencil}
onClick={() => dialogRef.current?.open()}
>
{__("Edit")}
</DropdownItem>
</Authorized>
<Authorized entity="Measure" action="deleteMeasure">
)}
{isAuthorized("Measure", "deleteMeasure") && (
<DropdownItem
onClick={onDelete}
disabled={isDeleting}
@@ -323,7 +326,7 @@ function MeasureRow(props: MeasureRowProps) {
>
{__("Delete")}
</DropdownItem>
</Authorized>
)}
</ActionDropdown>
</Td>
)}

View File

@@ -21,12 +21,12 @@ import {
UpdateMeetingMinutesDialog,
type UpdateMeetingMinutesDialogRef,
} from "./dialogs/UpdateMeetingMinutesDialog";
import { useRef, useState, useEffect } from "react";
import { useRef, useState, useEffect, use } from "react";
import {
meetingNodeQuery,
useDeleteMeetingMutation,
} from "/hooks/graph/MeetingGraph";
import { isAuthorized } from "/permissions";
import { PermissionsContext } from "/providers/PermissionsContext";
const meetingFragment = graphql`
fragment MeetingDetailPageMeetingFragment on Meeting {
@@ -54,6 +54,7 @@ export default function MeetingDetailPage(props: Props) {
const { __ } = useTranslate();
const organizationId = useOrganizationId();
const navigate = useNavigate();
const { isAuthorized } = use(PermissionsContext);
if (!meeting) {
return <div>{__("Meeting not found")}</div>;
@@ -74,14 +75,14 @@ export default function MeetingDetailPage(props: Props) {
}
try {
const updateAuth = isAuthorized(organizationId, "Meeting", "updateMeeting");
const updateAuth = isAuthorized("Meeting", "updateMeeting");
setCanUpdate(updateAuth);
} catch (promise) {
if (promise instanceof Promise) {
promise
.then(() => {
try {
const updateAuth = isAuthorized(organizationId, "Meeting", "updateMeeting");
const updateAuth = isAuthorized("Meeting", "updateMeeting");
setCanUpdate(updateAuth);
} catch {
setCanUpdate(false);
@@ -96,14 +97,14 @@ export default function MeetingDetailPage(props: Props) {
}
try {
const deleteAuth = isAuthorized(organizationId, "Meeting", "deleteMeeting");
const deleteAuth = isAuthorized("Meeting", "deleteMeeting");
setCanDelete(deleteAuth);
} catch (promise) {
if (promise instanceof Promise) {
promise
.then(() => {
try {
const deleteAuth = isAuthorized(organizationId, "Meeting", "deleteMeeting");
const deleteAuth = isAuthorized("Meeting", "deleteMeeting");
setCanDelete(deleteAuth);
} catch {
setCanDelete(false);

View File

@@ -42,7 +42,8 @@ import { Link } from "react-router";
import { useState, useEffect, useRef } from "react";
import { useMutationWithToasts } from "/hooks/useMutationWithToasts";
import type { MeetingsPage_UpdateSummaryMutation } from "./__generated__/MeetingsPage_UpdateSummaryMutation.graphql";
import { Authorized } from "/permissions";
import { use } from "react";
import { PermissionsContext } from "/providers/PermissionsContext";
const meetingsFragment = graphql`
fragment MeetingsPageListFragment on Organization
@@ -85,7 +86,7 @@ type Props = {
export default function MeetingsPage(props: Props) {
const { __ } = useTranslate();
const { isAuthorized } = use(PermissionsContext);
const organization = usePreloadedQuery(
meetingsQuery,
props.queryRef
@@ -218,7 +219,7 @@ export default function MeetingsPage(props: Props) {
<h3 className="text-sm font-semibold text-txt-secondary">
{__("Summary")}
</h3>
<Authorized entity="Meeting" action="updateMeeting">
{isAuthorized("Meeting", "updateMeeting") && (
<Button
variant="quaternary"
icon={IconPencil}
@@ -226,7 +227,7 @@ export default function MeetingsPage(props: Props) {
>
{__("Edit")}
</Button>
</Authorized>
)}
</div>
<div className="w-full">
{displayedSummary ? (
@@ -248,11 +249,11 @@ export default function MeetingsPage(props: Props) {
"Track and manage your organization's meetings and their minutes."
)}
>
<Authorized entity="Organization" action="createMeeting">
{isAuthorized("Organization", "createMeeting") && (
<CreateMeetingDialog connectionId={connectionId}>
<Button icon={IconPlusLarge}>{__("Add meeting")}</Button>
</CreateMeetingDialog>
</Authorized>
)}
</PageHeader>
{meetingNodes.length > 0 ? (
<SortableTable {...pagination}>
@@ -320,7 +321,7 @@ function MeetingRow({
const { __ } = useTranslate();
const [deleteMeeting] = useDeleteMeetingMutation();
const confirm = useConfirm();
const { isAuthorized } = use(PermissionsContext);
const handleDelete = () => {
confirm(
() =>
@@ -370,7 +371,7 @@ function MeetingRow({
</span>
)}
</Td>
<Authorized entity="Meeting" action="deleteMeeting">
{isAuthorized("Meeting", "deleteMeeting") && (
<Td noLink width={50} className="text-end w-18">
<ActionDropdown>
<DropdownItem
@@ -382,7 +383,7 @@ function MeetingRow({
</DropdownItem>
</ActionDropdown>
</Td>
</Authorized>
)}
</Tr>
);
}

View File

@@ -31,13 +31,13 @@ import { deleteNonconformityMutation, NonconformitiesConnectionKey } from "../..
import { sprintf, promisifyMutation, getStatusVariant, getStatusLabel, formatDate } from "@probo/helpers";
import { SnapshotBanner } from "/components/SnapshotBanner";
import { useParams } from "react-router";
import { Authorized } from "/permissions";
import { isAuthorized } from "/permissions";
import type { NonconformitiesPageQuery } from "./__generated__/NonconformitiesPageQuery.graphql";
import type {
NonconformitiesPageFragment$key,
NonconformitiesPageFragment$data,
} from "./__generated__/NonconformitiesPageFragment.graphql";
import { use } from "react";
import { PermissionsContext } from "/providers/PermissionsContext";
type Nonconformity = NonconformitiesPageFragment$data['nonconformities']['edges'][number]['node'];
@@ -103,6 +103,7 @@ export default function NonconformitiesPage({ queryRef }: NonconformitiesPagePro
const organizationId = useOrganizationId();
const { snapshotId } = useParams<{ snapshotId?: string }>();
const isSnapshotMode = Boolean(snapshotId);
const { isAuthorized } = use(PermissionsContext);
usePageTitle(__("Nonconformities"));
@@ -132,8 +133,8 @@ export default function NonconformitiesPage({ queryRef }: NonconformitiesPagePro
const nonconformities: Nonconformity[] = nonconformitiesData?.nonconformities?.edges?.map((edge) => edge.node) ?? [];
const hasAnyAction = !isSnapshotMode && (
isAuthorized(organizationId, "Nonconformity", "updateNonconformity") ||
isAuthorized(organizationId, "Nonconformity", "deleteNonconformity")
isAuthorized("Nonconformity", "updateNonconformity") ||
isAuthorized("Nonconformity", "deleteNonconformity")
);
return (
@@ -148,11 +149,11 @@ export default function NonconformitiesPage({ queryRef }: NonconformitiesPagePro
)}
>
{!isSnapshotMode && (
<Authorized entity="Organization" action="createNonconformity">
isAuthorized("Organization", "createNonconformity") && (
<CreateNonconformityDialog organizationId={organizationId} connection={connectionId}>
<Button icon={IconPlusLarge}>{__("Add nonconformity")}</Button>
</CreateNonconformityDialog>
</Authorized>
)
)}
</PageHeader>
@@ -231,7 +232,7 @@ function NonconformityRow({
const { __ } = useTranslate();
const confirm = useConfirm();
const [deleteNonconformity] = useMutation(deleteNonconformityMutation);
const { isAuthorized } = use(PermissionsContext);
const nonconformityDetailUrl = isSnapshotMode
? `/organizations/${organizationId}/snapshots/${snapshotId}/nonconformities/${nonconformity.id}`
@@ -298,7 +299,7 @@ function NonconformityRow({
{hasAnyAction && (
<Td noLink width={50} className="text-end">
<ActionDropdown>
<Authorized entity="Nonconformity" action="deleteNonconformity">
{isAuthorized("Nonconformity", "deleteNonconformity") && (
<DropdownItem
icon={IconTrashCan}
variant="danger"
@@ -306,7 +307,7 @@ function NonconformityRow({
>
{__("Delete")}
</DropdownItem>
</Authorized>
)}
</ActionDropdown>
</Td>
)}

View File

@@ -34,7 +34,8 @@ import { useFormWithSchema } from "/hooks/useFormWithSchema";
import z from "zod";
import { getStatusVariant, getStatusLabel, formatDatetime, validateSnapshotConsistency, getStatusOptions, formatError, type GraphQLError } from "@probo/helpers";
import type { NonconformityGraphNodeQuery } from "/hooks/graph/__generated__/NonconformityGraphNodeQuery.graphql";
import { Authorized } from "/permissions";
import { PermissionsContext } from "/providers/PermissionsContext";
import { use } from "react";
const updateNonconformitySchema = z.object({
referenceId: z.string().min(1, "Reference ID is required"),
@@ -63,6 +64,7 @@ export default function NonconformityDetailsPage(props: Props) {
const organizationId = useOrganizationId();
const { snapshotId } = useParams<{ snapshotId?: string }>();
const isSnapshotMode = Boolean(snapshotId);
const { isAuthorized } = use(PermissionsContext);
validateSnapshotConsistency(nonconformity, snapshotId);
@@ -162,7 +164,7 @@ export default function NonconformityDetailsPage(props: Props) {
</div>
{!isSnapshotMode && (
<ActionDropdown variant="secondary">
<Authorized entity="Nonconformity" action="deleteNonconformity">
{isAuthorized("Nonconformity", "deleteNonconformity") && (
<DropdownItem
variant="danger"
icon={IconTrashCan}
@@ -170,7 +172,7 @@ export default function NonconformityDetailsPage(props: Props) {
>
{__("Delete")}
</DropdownItem>
</Authorized>
)}
</ActionDropdown>
)}
</div>
@@ -279,11 +281,11 @@ export default function NonconformityDetailsPage(props: Props) {
<div className="flex justify-end">
{formState.isDirty && !isSnapshotMode && (
<Authorized entity="Nonconformity" action="updateNonconformity">
isAuthorized("Nonconformity", "updateNonconformity") && (
<Button type="submit" disabled={formState.isSubmitting}>
{formState.isSubmitting ? __("Updating...") : __("Update")}
</Button>
</Authorized>
)
)}
</div>
</form>

View File

@@ -35,7 +35,8 @@ import z from "zod";
import { getObligationStatusVariant, getObligationStatusLabel, formatDatetime, getObligationStatusOptions, validateSnapshotConsistency } from "@probo/helpers";
import { SnapshotBanner } from "/components/SnapshotBanner";
import type { ObligationGraphNodeQuery } from "/hooks/graph/__generated__/ObligationGraphNodeQuery.graphql";
import { Authorized } from "/permissions";
import { use } from "react";
import { PermissionsContext } from "/providers/PermissionsContext";
const updateObligationSchema = z.object({
area: z.string().optional(),
@@ -61,6 +62,7 @@ export default function ObligationDetailsPage(props: Props) {
const organizationId = useOrganizationId();
const { snapshotId } = useParams<{ snapshotId?: string }>();
const isSnapshotMode = Boolean(snapshotId);
const { isAuthorized } = use(PermissionsContext);
if (!obligation) {
return <div>{__("Obligation not found")}</div>;
@@ -157,13 +159,13 @@ export default function ObligationDetailsPage(props: Props) {
</div>
{!isSnapshotMode && (
<Authorized entity="Obligation" action="deleteObligation">
isAuthorized("Obligation", "deleteObligation") && (
<ActionDropdown>
<DropdownItem icon={IconTrashCan} onClick={deleteObligation}>
{__("Delete")}
</DropdownItem>
</ActionDropdown>
</Authorized>
)
)}
</div>
@@ -301,14 +303,14 @@ export default function ObligationDetailsPage(props: Props) {
{!isSnapshotMode && (
<div className="flex justify-end">
<Authorized entity="Obligation" action="updateObligation">
{isAuthorized("Obligation", "updateObligation") && (
<Button
type="submit"
disabled={formState.isSubmitting}
>
{formState.isSubmitting ? __("Saving...") : __("Save Changes")}
</Button>
</Authorized>
)}
</div>
)}
</form>

View File

@@ -31,12 +31,12 @@ import { deleteObligationMutation } from "../../../hooks/graph/ObligationGraph";
import { promisifyMutation, getObligationStatusVariant, getObligationStatusLabel, formatDate } from "@probo/helpers";
import { SnapshotBanner } from "/components/SnapshotBanner";
import type { ObligationsPageQuery } from "./__generated__/ObligationsPageQuery.graphql";
import { Authorized } from "/permissions";
import { isAuthorized } from "/permissions";
import type {
ObligationsPageFragment$key,
ObligationsPageFragment$data,
} from "./__generated__/ObligationsPageFragment.graphql";
import { use } from "react";
import { PermissionsContext } from "/providers/PermissionsContext";
type Obligation = ObligationsPageFragment$data['obligations']['edges'][number]['node'];
@@ -95,6 +95,7 @@ export default function ObligationsPage({ queryRef }: ObligationsPageProps) {
const organizationId = useOrganizationId();
const { snapshotId } = useParams<{ snapshotId?: string }>();
const isSnapshotMode = Boolean(snapshotId);
const { isAuthorized } = use(PermissionsContext);
usePageTitle(__("Obligations"));
@@ -120,8 +121,8 @@ export default function ObligationsPage({ queryRef }: ObligationsPageProps) {
const obligations: Obligation[] = obligationsData?.obligations?.edges?.map((edge) => edge.node) ?? [];
const hasAnyAction = !isSnapshotMode && (
isAuthorized(organizationId, "Obligation", "updateObligation") ||
isAuthorized(organizationId, "Obligation", "deleteObligation")
isAuthorized("Obligation", "updateObligation") ||
isAuthorized("Obligation", "deleteObligation")
);
return (
@@ -136,11 +137,11 @@ export default function ObligationsPage({ queryRef }: ObligationsPageProps) {
)}
>
{!snapshotId && (
<Authorized entity="Organization" action="createObligation">
isAuthorized("Organization", "createObligation") && (
<CreateObligationDialog organizationId={organizationId} connection={connectionId}>
<Button icon={IconPlusLarge}>{__("Add obligation")}</Button>
</CreateObligationDialog>
</Authorized>
)
)}
</PageHeader>
@@ -214,7 +215,7 @@ function ObligationRow({
const [deleteObligation] = useMutation(deleteObligationMutation);
const confirm = useConfirm();
const isSnapshotMode = Boolean(snapshotId);
const { isAuthorized } = use(PermissionsContext);
const handleDelete = () => {
confirm(
@@ -261,7 +262,7 @@ function ObligationRow({
{hasAnyAction && (
<Td noLink width={50} className="text-end">
<ActionDropdown>
<Authorized entity="Obligation" action="deleteObligation">
{isAuthorized("Obligation", "deleteObligation") && (
<DropdownItem
icon={IconTrashCan}
variant="danger"
@@ -269,7 +270,7 @@ function ObligationRow({
>
{__("Delete")}
</DropdownItem>
</Authorized>
)}
</ActionDropdown>
</Td>
)}

View File

@@ -21,7 +21,8 @@ import {
import { useTranslate } from "@probo/i18n";
import { useOrganizationId } from "/hooks/useOrganizationId";
import { Outlet } from "react-router";
import { Authorized } from "/permissions";
import { use } from "react";
import { PermissionsContext } from "/providers/PermissionsContext";
type Props = {
queryRef: PreloadedQuery<PeopleGraphNodeQuery>;
@@ -32,6 +33,7 @@ export default function PeopleDetailPage(props: Props) {
const people = data.node;
const { __ } = useTranslate();
const organizationId = useOrganizationId();
const { isAuthorized } = use(PermissionsContext);
const deletePeople = useDeletePeople(
people,
ConnectionHandler.getConnectionID(organizationId, PeopleConnectionKey)
@@ -55,7 +57,7 @@ export default function PeopleDetailPage(props: Props) {
<Avatar name={people.fullName ?? ""} size="xl" />
<div className="text-2xl">{people.fullName}</div>
</div>
<Authorized entity="People" action="deletePeople">
{isAuthorized("People", "deletePeople") && (
<ActionDropdown variant="secondary">
<DropdownItem
variant="danger"
@@ -65,7 +67,7 @@ export default function PeopleDetailPage(props: Props) {
{__("Delete")}
</DropdownItem>
</ActionDropdown>
</Authorized>
)}
</div>
<Tabs>

View File

@@ -23,8 +23,8 @@ import { usePageTitle } from "@probo/hooks";
import { getRole } from "@probo/helpers";
import { CreatePeopleDialog } from "./dialogs/CreatePeopleDialog";
import { useOrganizationId } from "/hooks/useOrganizationId";
import { Authorized } from "/permissions";
import { isAuthorized } from "/permissions";
import { PermissionsContext } from "/providers/PermissionsContext";
import { use } from "react";
type People = NodeOf<PeopleGraphPaginatedFragment$data["peoples"]>;
@@ -42,14 +42,14 @@ export default function PeopleListPage({
queryRef: PreloadedQuery<PeopleGraphPaginatedQuery>;
}) {
const { __ } = useTranslate();
const organizationId = useOrganizationId();
const { isAuthorized } = use(PermissionsContext);
const { people, refetch, connectionId, hasNext, loadNext, isLoadingNext } =
usePeopleQuery(queryRef);
usePageTitle(__("Members"));
const hasAnyAction = isAuthorized(organizationId, "People", "updatePeople") ||
isAuthorized(organizationId, "People", "deletePeople");
const hasAnyAction = isAuthorized("People", "updatePeople") ||
isAuthorized("People", "deletePeople");
return (
<div className="space-y-6">
@@ -59,11 +59,11 @@ export default function PeopleListPage({
"Keep track of your company's workforce and their progress towards completing tasks assigned to them."
)}
>
<Authorized entity="Organization" action="createPeople">
{isAuthorized("Organization", "createPeople") && (
<CreatePeopleDialog connectionId={connectionId}>
<Button icon={IconPlusLarge}>{__("Add member")}</Button>
</CreatePeopleDialog>
</Authorized>
)}
</PageHeader>
<SortableTable
refetch={refetch}
@@ -107,6 +107,7 @@ function PeopleRow({
const { __ } = useTranslate();
const deletePeople = useDeletePeople(people, connectionId);
const contractEnded = isContractEnded(people);
const { isAuthorized } = use(PermissionsContext);
return (
<Tr
@@ -129,7 +130,7 @@ function PeopleRow({
{hasAnyAction && (
<Td noLink width={50} className="text-end">
<ActionDropdown>
<Authorized entity="People" action="deletePeople">
{isAuthorized("People", "deletePeople") && (
<DropdownItem
icon={IconTrashCan}
variant="danger"
@@ -137,7 +138,7 @@ function PeopleRow({
>
{__("Delete")}
</DropdownItem>
</Authorized>
)}
</ActionDropdown>
</Td>
)}

View File

@@ -9,7 +9,8 @@ import type { PeopleGraphUpdateMutation } from "/hooks/graph/__generated__/Peopl
import { updatePeopleMutation } from "/hooks/graph/PeopleGraph";
import { Button, Card, Field, Input } from "@probo/ui";
import { EmailsField } from "/components/form/EmailsField";
import { Authorized } from "/permissions";
import { PermissionsContext } from "/providers/PermissionsContext";
import { use } from "react";
const schema = z.object({
fullName: z.string().min(1),
@@ -49,7 +50,7 @@ export default function PeopleProfileTab() {
errorMessage: __("Failed to update member"),
}
);
const { isAuthorized } = use(PermissionsContext);
const onSubmit = handleSubmit((data) => {
const input = {
id: people.id!,
@@ -95,11 +96,11 @@ export default function PeopleProfileTab() {
</Card>
<div className="flex justify-end">
{formState.isDirty && (
<Authorized entity="People" action="updatePeople">
isAuthorized("People", "updatePeople") && (
<Button type="submit" disabled={isMutating}>
{__("Update")}
</Button>
</Authorized>
)
)}
</div>
</form>

View File

@@ -32,14 +32,14 @@ import { CreateProcessingActivityDialog } from "./dialogs/CreateProcessingActivi
import { deleteProcessingActivityMutation, ProcessingActivitiesConnectionKey } from "../../../hooks/graph/ProcessingActivityGraph";
import { sprintf, promisifyMutation } from "@probo/helpers";
import { SnapshotBanner } from "/components/SnapshotBanner";
import { Authorized } from "/permissions";
import { isAuthorized } from "/permissions";
import type { NodeOf } from "/types";
import type { ProcessingActivitiesPageQuery } from "./__generated__/ProcessingActivitiesPageQuery.graphql";
import type {
ProcessingActivitiesPageFragment$key,
ProcessingActivitiesPageFragment$data,
} from "./__generated__/ProcessingActivitiesPageFragment.graphql";
import { PermissionsContext } from "/providers/PermissionsContext";
import { use } from "react";
interface ProcessingActivitiesPageProps {
queryRef: PreloadedQuery<ProcessingActivitiesPageQuery>;
@@ -91,7 +91,7 @@ export default function ProcessingActivitiesPage({ queryRef }: ProcessingActivit
const organizationId = useOrganizationId();
const { snapshotId } = useParams<{ snapshotId?: string }>();
const isSnapshotMode = Boolean(snapshotId);
const { isAuthorized } = use(PermissionsContext);
usePageTitle(__("Processing Activities"));
@@ -129,8 +129,8 @@ export default function ProcessingActivitiesPage({ queryRef }: ProcessingActivit
const activities = data?.processingActivities?.edges?.map((edge) => edge.node) ?? [];
const hasAnyAction = !isSnapshotMode && (
isAuthorized(organizationId, "ProcessingActivity", "updateProcessingActivity") ||
isAuthorized(organizationId, "ProcessingActivity", "deleteProcessingActivity")
isAuthorized("ProcessingActivity", "updateProcessingActivity") ||
isAuthorized("ProcessingActivity", "deleteProcessingActivity")
);
return (
@@ -140,7 +140,7 @@ export default function ProcessingActivitiesPage({ queryRef }: ProcessingActivit
)}
<PageHeader title={__("Processing Activities")} description={__("Manage your processing activities under GDPR")}>
{!isSnapshotMode && (
<Authorized entity="Organization" action="createProcessingActivity">
isAuthorized("Organization", "createProcessingActivity") && (
<CreateProcessingActivityDialog
organizationId={organizationId}
connectionId={connectionId}
@@ -149,7 +149,7 @@ export default function ProcessingActivitiesPage({ queryRef }: ProcessingActivit
{__("Add processing activity")}
</Button>
</CreateProcessingActivityDialog>
</Authorized>
)
)}
</PageHeader>
@@ -222,6 +222,7 @@ function ActivityRow({
const isSnapshotMode = Boolean(snapshotId);
const [deleteActivity] = useMutation(deleteProcessingActivityMutation);
const confirm = useConfirm();
const { isAuthorized } = use(PermissionsContext);
const handleDelete = () => {
confirm(
@@ -270,7 +271,7 @@ function ActivityRow({
{hasAnyAction && (
<Td noLink width={50} className="text-end">
<ActionDropdown>
<Authorized entity="ProcessingActivity" action="deleteProcessingActivity">
{isAuthorized("ProcessingActivity", "deleteProcessingActivity") && (
<DropdownItem
icon={IconTrashCan}
variant="danger"
@@ -278,7 +279,7 @@ function ActivityRow({
>
{__("Delete")}
</DropdownItem>
</Authorized>
)}
</ActionDropdown>
</Td>
)}

View File

@@ -41,7 +41,8 @@ import {
} from "../../../components/form/ProcessingActivityEnumOptions";
import type { ProcessingActivityGraphNodeQuery } from "/hooks/graph/__generated__/ProcessingActivityGraphNodeQuery.graphql";
import { Authorized } from "/permissions";
import { use } from "react";
import { PermissionsContext } from "/providers/PermissionsContext";
const updateProcessingActivitySchema = z.object({
name: z.string().min(1, "Name is required"),
@@ -74,6 +75,7 @@ export default function ProcessingActivityDetailsPage(props: Props) {
const organizationId = useOrganizationId();
const { snapshotId } = useParams<{ snapshotId?: string }>();
const isSnapshotMode = Boolean(snapshotId);
const { isAuthorized } = use(PermissionsContext);
if (!activity) {
return <div>{__("Processing activity not found")}</div>;
@@ -171,13 +173,13 @@ export default function ProcessingActivityDetailsPage(props: Props) {
]}
/>
{!isSnapshotMode && (
<Authorized entity="ProcessingActivity" action="deleteProcessingActivity">
isAuthorized("ProcessingActivity", "deleteProcessingActivity") && (
<ActionDropdown>
<DropdownItem onClick={deleteActivity} variant="danger">
{__("Delete")}
</DropdownItem>
</ActionDropdown>
</Authorized>
)
)}
</div>

View File

@@ -30,7 +30,8 @@ import {
} from "/hooks/graph/RiskGraph";
import type { RiskGraphNodeQuery } from "/hooks/graph/__generated__/RiskGraphNodeQuery.graphql";
import { SnapshotBanner } from "/components/SnapshotBanner";
import { Authorized } from "/permissions";
import { use } from "react";
import { PermissionsContext } from "/providers/PermissionsContext";
type Props = {
queryRef: PreloadedQuery<RiskGraphNodeQuery>;
@@ -41,6 +42,7 @@ export default function RiskDetailPage(props: Props) {
const organizationId = useOrganizationId();
const navigate = useNavigate();
const isSnapshotMode = Boolean(snapshotId);
const { isAuthorized } = use(PermissionsContext);
if (!riskId) {
throw new Error("Cannot load risk detail page without riskId parameter");
@@ -121,7 +123,7 @@ export default function RiskDetailPage(props: Props) {
/>
{!isSnapshotMode && (
<div className="flex gap-2">
<Authorized entity="Risk" action="updateRisk">
{isAuthorized("Risk", "updateRisk") && (
<FormRiskDialog
trigger={
<Button icon={IconPencil} variant="secondary">
@@ -130,8 +132,8 @@ export default function RiskDetailPage(props: Props) {
}
risk={{ id: riskId, ...risk }}
/>
</Authorized>
<Authorized entity="Risk" action="deleteRisk">
)}
{isAuthorized("Risk", "deleteRisk") && (
<ActionDropdown variant="secondary">
<DropdownItem
variant="danger"
@@ -141,7 +143,7 @@ export default function RiskDetailPage(props: Props) {
{__("Delete")}
</DropdownItem>
</ActionDropdown>
</Authorized>
)}
</div>
)}
</div>

View File

@@ -29,8 +29,8 @@ import type { RiskGraphListQuery } from "/hooks/graph/__generated__/RiskGraphLis
import type { RiskGraphFragment$data } from "/hooks/graph/__generated__/RiskGraphFragment.graphql";
import { useParams } from "react-router";
import { SnapshotBanner } from "/components/SnapshotBanner";
import { Authorized } from "/permissions";
import { isAuthorized } from "/permissions";
import { use } from "react";
import { PermissionsContext } from "/providers/PermissionsContext";
type Props = {
queryRef: PreloadedQuery<RiskGraphListQuery>;
@@ -41,6 +41,7 @@ export default function RisksPage(props: Props) {
const organizationId = useOrganizationId();
const { snapshotId } = useParams<{ snapshotId?: string }>();
const isSnapshotMode = Boolean(snapshotId);
const { isAuthorized } = use(PermissionsContext);
const { connectionId, risks, ...pagination } = useRisksQuery(props.queryRef);
@@ -57,8 +58,8 @@ export default function RisksPage(props: Props) {
usePageTitle(__("Risks"));
const hasAnyAction = !isSnapshotMode && (
isAuthorized(organizationId, "Risk", "updateRisk") ||
isAuthorized(organizationId, "Risk", "deleteRisk")
isAuthorized("Risk", "updateRisk") ||
isAuthorized("Risk", "deleteRisk")
);
return (
@@ -71,7 +72,7 @@ export default function RisksPage(props: Props) {
)}
>
{!isSnapshotMode && (
<Authorized entity="Organization" action="createRisk">
isAuthorized("Organization", "createRisk") && (
<FormRiskDialog
connection={connectionId}
onSuccess={() => {
@@ -79,7 +80,7 @@ export default function RisksPage(props: Props) {
}}
trigger={<Button icon={IconPlusLarge}>{__("New Risk")}</Button>}
/>
</Authorized>
)
)}
</PageHeader>
@@ -143,6 +144,7 @@ function RiskRow(props: RowProps) {
const isSnapshotMode = Boolean(snapshotId);
const [deleteRisk] = useDeleteRiskMutation();
const confirm = useConfirm();
const { isAuthorized } = use(PermissionsContext);
const onDelete = () => {
confirm(
() =>
@@ -194,16 +196,16 @@ function RiskRow(props: RowProps) {
{props.hasAnyAction && (
<Td noLink className="text-end">
<ActionDropdown>
<Authorized entity="Risk" action="updateRisk">
{isAuthorized("Risk", "updateRisk") && (
<DropdownItem
icon={IconPencil}
onClick={() => formDialogRef.current?.open()}
>
{__("Edit")}
</DropdownItem>
</Authorized>
)}
<Authorized entity="Risk" action="deleteRisk">
{isAuthorized("Risk", "deleteRisk") && (
<DropdownItem
variant="danger"
icon={IconTrashCan}
@@ -211,7 +213,7 @@ function RiskRow(props: RowProps) {
>
{__("Delete")}
</DropdownItem>
</Authorized>
)}
</ActionDropdown>
</Td>
)}

View File

@@ -1,4 +1,4 @@
import { useState, useRef, useEffect, type ChangeEventHandler } from "react";
import { useState, useRef, useEffect, type ChangeEventHandler, use } from "react";
import { useOutletContext, useNavigate } from "react-router";
import { useFragment, graphql } from "react-relay";
import {
@@ -23,7 +23,7 @@ import { z } from "zod";
import type { GeneralSettingsTabFragment$key } from "./__generated__/GeneralSettingsTabFragment.graphql";
import { DeleteOrganizationDialog } from "/components/organizations/DeleteOrganizationDialog";
import { useDeleteOrganizationMutation } from "/hooks/graph/OrganizationGraph";
import { isAuthorized } from "/permissions";
import { PermissionsContext } from "/providers/PermissionsContext";
const generalSettingsTabFragment = graphql`
fragment GeneralSettingsTabFragment on Organization {
@@ -90,9 +90,10 @@ export default function GeneralSettingsTab() {
const { organization: organizationKey } = useOutletContext<OutletContext>();
const organization = useFragment(generalSettingsTabFragment, organizationKey);
const deleteDialogRef = useDialogRef();
const { isAuthorized } = use(PermissionsContext);
const canUpdate = isAuthorized(organization.id, "Organization", "updateOrganization");
const canDelete = isAuthorized(organization.id, "Organization", "deleteOrganization");
const canUpdate = isAuthorized("Organization", "updateOrganization");
const canDelete = isAuthorized("Organization", "deleteOrganization");
const [logoPreview, setLogoPreview] = useState<string | null>(null);
const [horizontalLogoPreview, setHorizontalLogoPreview] = useState<

View File

@@ -1,4 +1,4 @@
import { useState, Suspense } from "react";
import { useState, Suspense, use } from "react";
import { useOutletContext } from "react-router";
import { usePaginationFragment, graphql } from "react-relay";
import {
@@ -29,11 +29,9 @@ import { useTranslate } from "@probo/i18n";
import { SortableTable, SortableTh } from "/components/SortableTable";
import { InviteUserDialog } from "/components/organizations/InviteUserDialog";
import { useMutationWithToasts } from "/hooks/useMutationWithToasts";
import { sprintf } from "@probo/helpers";
import { getAssignableRoles, sprintf } from "@probo/helpers";
import clsx from "clsx";
import type { NodeOf } from "/types";
import { Authorized } from "/permissions";
import { getAssignableRoles, getUserRole } from "/permissions";
import type {
MembersSettingsTabMembershipsFragment$data,
MembersSettingsTabMembershipsFragment$key
@@ -42,6 +40,7 @@ import type {
MembersSettingsTabInvitationsFragment$data,
MembersSettingsTabInvitationsFragment$key
} from "./__generated__/MembersSettingsTabInvitationsFragment.graphql";
import { PermissionsContext } from "/providers/PermissionsContext";
const paginatedMembershipsFragment = graphql`
fragment MembersSettingsTabMembershipsFragment on Organization
@@ -153,7 +152,7 @@ type OutletContext = {
export default function MembersSettingsTab() {
const { __ } = useTranslate();
const { organization: organizationKey } = useOutletContext<OutletContext>();
const { isAuthorized } = use(PermissionsContext);
const membershipsPagination = usePaginationFragment(
paginatedMembershipsFragment,
organizationKey as MembersSettingsTabMembershipsFragment$key
@@ -180,14 +179,14 @@ export default function MembersSettingsTab() {
<div className="space-y-2">
<div className="flex items-center justify-between">
<h2 className="text-base font-medium">{__("Workspace members")}</h2>
<Authorized entity="Organization" action="inviteUser">
{isAuthorized("Organization", "inviteUser") && (
<InviteUserDialog
connectionId={invitationsPagination.data.invitations?.__id}
onRefetch={refetchInvitations}
>
<Button variant="secondary">{__("Invite member")}</Button>
</InviteUserDialog>
</Authorized>
)}
</div>
<Tabs>
@@ -314,6 +313,7 @@ function InvitationRow(props: {
}) {
const { __ } = useTranslate();
const confirm = useConfirm();
const { isAuthorized } = use(PermissionsContext);
const [deleteInvitation, isDeleting] = useMutationWithToasts(
deleteInvitationMutation,
{
@@ -376,7 +376,7 @@ function InvitationRow(props: {
{isDeleting ? (
<Spinner size={16} />
) : (
<Authorized entity="Invitation" action="deleteInvitation">
isAuthorized("Invitation", "deleteInvitation") && (
<Button
variant="danger"
onClick={onDelete}
@@ -384,7 +384,7 @@ function InvitationRow(props: {
icon={IconTrashCan}
aria-label={__("Delete invitation")}
/>
</Authorized>
)
)}
</div>
</Td>
@@ -399,9 +399,9 @@ function MembershipRowContent(props: {
onRefetch: () => void;
}) {
const { __ } = useTranslate();
const availableRoles = getAssignableRoles(props.organizationId);
const currentUserRole = getUserRole(props.organizationId);
const { role: currentUserRole } = use(PermissionsContext);
const availableRoles = getAssignableRoles(currentUserRole);
const { isAuthorized } = use(PermissionsContext);
const [removeMember, isRemoving] = useMutationWithToasts(removeMemberMutation, {
successMessage: __("Member removed successfully"),
errorMessage: __("Failed to remove member"),
@@ -494,31 +494,27 @@ function MembershipRowContent(props: {
className="flex gap-2 justify-end"
onClick={(e) => e.stopPropagation()}
>
<Authorized entity="Organization" action="updateMembership">
{canEditThisRole && (
<Button
variant="secondary"
onClick={handleEditClick}
disabled={isUpdating}
icon={IconPencil}
aria-label={__("Edit role")}
/>
)}
</Authorized>
{isAuthorized("Organization", "updateMembership") && canEditThisRole && (
<Button
variant="secondary"
onClick={handleEditClick}
disabled={isUpdating}
icon={IconPencil}
aria-label={__("Edit role")}
/>
)}
{isRemoving ? (
<Spinner size={16} />
) : (
<Authorized entity="Organization" action="removeMember">
{canEditThisRole && (
<Button
variant="danger"
onClick={onRemove}
disabled={isRemoving}
icon={IconTrashCan}
aria-label={__("Remove member")}
/>
)}
</Authorized>
isAuthorized("Organization", "removeMember") && canEditThisRole && (
<Button
variant="danger"
onClick={onRemove}
disabled={isRemoving}
icon={IconTrashCan}
aria-label={__("Remove member")}
/>
)
)}
</div>
</Td>

View File

@@ -1,4 +1,4 @@
import { useState, useEffect } from "react";
import { useState, useEffect, use } from "react";
import { useOutletContext } from "react-router";
import { useFragment, graphql } from "react-relay";
import { Controller } from "react-hook-form";
@@ -37,7 +37,7 @@ import {
useVerifyDomainMutation,
} from "/hooks/graph/SAMLConfigurationGraph";
import type { SAMLSettingsTabFragment$key } from "./__generated__/SAMLSettingsTabFragment.graphql";
import { Authorized } from "/permissions";
import { PermissionsContext } from "/providers/PermissionsContext";
const samlSettingsTabFragment = graphql`
fragment SAMLSettingsTabFragment on Organization {
@@ -97,6 +97,7 @@ type SetupStep = "initiate" | "verify" | "configure";
export default function SAMLSettingsTab() {
const { __ } = useTranslate();
const { organization: organizationKey } = useOutletContext<OutletContext>();
const { isAuthorized } = use(PermissionsContext);
const organization = useFragment(samlSettingsTabFragment, organizationKey);
const configs = organization.samlConfigurations;
@@ -372,11 +373,11 @@ export default function SAMLSettingsTab() {
<div className="space-y-4">
<div className="flex justify-between items-center">
<h2 className="text-base font-medium">{__("SAML Single Sign-On")}</h2>
<Authorized entity="Organization" action="createSAMLConfiguration">
{isAuthorized("Organization", "createSAMLConfiguration") && (
<Button onClick={() => handleOpenModal()}>
{__("Add Configuration")}
</Button>
</Authorized>
)}
</div>
{configs.length === 0 ? (
@@ -388,11 +389,11 @@ export default function SAMLSettingsTab() {
<p className="text-gray-600 mb-6">
{__("Set up SAML 2.0 single sign-on for your organization by adding a configuration for each email domain.")}
</p>
<Authorized entity="Organization" action="createSAMLConfiguration">
{isAuthorized("Organization", "createSAMLConfiguration") && (
<Button onClick={() => handleOpenModal()}>
{__("Add Your First Configuration")}
</Button>
</Authorized>
)}
</div>
</Card>
) : (
@@ -457,7 +458,7 @@ export default function SAMLSettingsTab() {
<div className="flex gap-2 justify-end">
{config.domainVerified ? (
<>
<Authorized entity="SAMLConfiguration" action="updateSAMLConfiguration">
{isAuthorized("SAMLConfiguration", "updateSAMLConfiguration") && (
<Button
variant={config.enabled ? "danger" : "primary"}
onClick={() => handleToggleEnabled(config)}
@@ -465,34 +466,34 @@ export default function SAMLSettingsTab() {
>
{config.enabled ? __("Disable") : __("Enable")}
</Button>
</Authorized>
<Authorized entity="SAMLConfiguration" action="updateSAMLConfiguration">
)}
{isAuthorized("SAMLConfiguration", "updateSAMLConfiguration") && (
<Button
variant="secondary"
onClick={() => handleOpenModal(config)}
>
{__("Edit")}
</Button>
</Authorized>
)}
</>
) : (
<>
<Authorized entity="Organization" action="verifyDomain">
{isAuthorized("Organization", "verifyDomain") && (
<Button
variant="primary"
onClick={() => handleOpenModal(config)}
>
{__("Verify Domain")}
</Button>
</Authorized>
<Authorized entity="Organization" action="deleteOrganization">
)}
{isAuthorized("Organization", "deleteOrganization") && (
<Button
variant="danger"
onClick={() => handleDelete(config)}
>
{__("Delete")}
</Button>
</Authorized>
)}
</>
)}
</div>

View File

@@ -34,8 +34,8 @@ import type { NodeOf } from "/types";
import SnapshotFormDialog from "./dialog/SnapshotFormDialog";
import { usePageTitle } from "@probo/hooks";
import { useOrganizationId } from "/hooks/useOrganizationId";
import { Authorized } from "/permissions";
import { isAuthorized } from "/permissions";
import { PermissionsContext } from "/providers/PermissionsContext";
import { use } from "react";
type Props = {
queryRef: PreloadedQuery<SnapshotGraphListQuery>;
@@ -71,9 +71,10 @@ export default function SnapshotsPage(props: Props) {
);
const connectionId = data.snapshots.__id;
const snapshots = data.snapshots.edges.map((edge) => edge.node);
const { isAuthorized } = use(PermissionsContext);
usePageTitle(__("Snapshots"));
const hasAnyAction = isAuthorized(organizationId, "Snapshot", "deleteSnapshot");
const hasAnyAction = isAuthorized("Snapshot", "deleteSnapshot");
return (
<div className="space-y-6">
@@ -83,13 +84,13 @@ export default function SnapshotsPage(props: Props) {
"Snapshots capture point-in-time views of your organization's compliance state. Create snapshots to track progress over time."
)}
>
<Authorized entity="Organization" action="createSnapshot">
{isAuthorized("Organization", "createSnapshot") && (
<SnapshotFormDialog connection={connectionId}>
<Button variant="primary" icon={IconPlusLarge}>
{__("New snapshot")}
</Button>
</SnapshotFormDialog>
</Authorized>
)}
</PageHeader>
{snapshots.length > 0 ? (
@@ -139,7 +140,7 @@ type SnapshotRowProps = {
function SnapshotRow(props: SnapshotRowProps) {
const { __ } = useTranslate();
const deleteSnapshot = useDeleteSnapshot(props.snapshot, props.connectionId);
const { isAuthorized } = use(PermissionsContext);
const typePath = getSnapshotTypeUrlPath(props.snapshot.type);
return (
@@ -159,7 +160,7 @@ function SnapshotRow(props: SnapshotRowProps) {
{props.hasAnyAction && (
<Td noLink width={50} className="text-end">
<ActionDropdown>
<Authorized entity="Snapshot" action="deleteSnapshot">
{isAuthorized("Snapshot", "deleteSnapshot") && (
<DropdownItem
onClick={deleteSnapshot}
variant="danger"
@@ -167,7 +168,7 @@ function SnapshotRow(props: SnapshotRowProps) {
>
{__("Delete")}
</DropdownItem>
</Authorized>
)}
</ActionDropdown>
</Td>
)}

View File

@@ -12,7 +12,8 @@ import { tasksQuery } from "/hooks/graph/TaskGraph";
import { usePageTitle } from "@probo/hooks";
import TasksCard from "/components/tasks/TasksCard";
import TaskFormDialog from "/components/tasks/TaskFormDialog";
import { Authorized } from "/permissions";
import { PermissionsContext } from "/providers/PermissionsContext";
import { use } from "react";
const tasksFragment = graphql`
fragment TasksPageFragment on Organization
@@ -66,7 +67,7 @@ export default function TasksPage({ queryRef }: Props) {
);
const tasks = data.tasks?.edges.map((edge) => edge.node);
const connectionId = data.tasks.__id;
const { isAuthorized } = use(PermissionsContext);
usePageTitle(__("Tasks"));
return (
@@ -77,11 +78,11 @@ export default function TasksPage({ queryRef }: Props) {
"Track your assigned compliance tasks and keep progress on track."
)}
>
<Authorized entity="Organization" action="createTask">
{isAuthorized("Organization", "createTask") && (
<TaskFormDialog connection={connectionId}>
<Button icon={IconPlusLarge}>{__("New task")}</Button>
</TaskFormDialog>
</Authorized>
)}
</PageHeader>
<TasksCard connectionId={connectionId} tasks={tasks ?? []} />
</div>

View File

@@ -24,7 +24,7 @@ import {
import { useTranslate } from "@probo/i18n";
import { formatDate } from "@probo/helpers";
import { useOutletContext } from "react-router";
import { useState, useCallback, useEffect, useRef } from "react";
import { useState, useCallback, useEffect, useRef, use } from "react";
import { useQueryLoader, usePreloadedQuery } from 'react-relay';
import z from "zod";
import {
@@ -36,7 +36,7 @@ import {
} from "/hooks/graph/TrustCenterAccessGraph";
import { useFormWithSchema } from "/hooks/useFormWithSchema";
import { useMutationWithToasts } from "/hooks/useMutationWithToasts";
import { Authorized } from "/permissions";
import { PermissionsContext } from "/providers/PermissionsContext";
type ContextType = {
organization: {
@@ -130,7 +130,7 @@ function DocumentAccessesLoader({
export default function TrustCenterAccessTab() {
const { __ } = useTranslate();
const { organization } = useOutletContext<ContextType>();
const { isAuthorized } = use(PermissionsContext);
const inviteSchema = z.object({
name: z.string().min(1, __("Name is required")).min(2, __("Name must be at least 2 characters long")),
email: z.string().min(1, __("Email is required")).email(__("Please enter a valid email address")),
@@ -423,14 +423,14 @@ export default function TrustCenterAccessTab() {
</p>
</div>
{organization.trustCenter?.id && (
<Authorized entity="TrustCenter" action="createTrustCenterAccess">
{isAuthorized("TrustCenter", "createTrustCenterAccess") && (
<Button icon={IconPlusLarge} onClick={() => {
inviteForm.reset();
dialogRef.current?.open();
}}>
{__("Add Access")}
</Button>
</Authorized>
)}
)}
</div>
@@ -515,22 +515,22 @@ export default function TrustCenterAccessTab() {
className="flex gap-2 justify-end"
onClick={(e) => e.stopPropagation()}
>
<Authorized entity="TrustCenterAccess" action="updateTrustCenterAccess">
{isAuthorized("TrustCenterAccess", "updateTrustCenterAccess") && (
<Button
variant="secondary"
onClick={() => handleEditAccess(access)}
disabled={isUpdating}
icon={IconPencil}
/>
</Authorized>
<Authorized entity="TrustCenterAccess" action="deleteTrustCenterAccess">
)}
{isAuthorized("TrustCenterAccess", "deleteTrustCenterAccess") && (
<Button
variant="danger"
onClick={() => handleDelete(access.id)}
disabled={isDeleting}
icon={IconTrashCan}
/>
</Authorized>
)}
</div>
</Td>
</Tr>

View File

@@ -13,7 +13,7 @@ import {
} from "@probo/ui";
import { useTranslate } from "@probo/i18n";
import { useOutletContext } from "react-router";
import { useState, useCallback } from "react";
import { useState, useCallback, use } from "react";
import z from "zod";
import { getTrustCenterVisibilityOptions } from "@probo/helpers";
import {
@@ -23,8 +23,8 @@ import {
} from "/hooks/graph/TrustCenterFileGraph";
import { useFormWithSchema } from "/hooks/useFormWithSchema";
import { TrustCenterFilesCard } from "/components/trustCenter/TrustCenterFilesCard";
import { Authorized } from "/permissions";
import type { TrustCenterFilesCardFragment$key } from "/components/trustCenter/__generated__/TrustCenterFilesCardFragment.graphql";
import { PermissionsContext } from "/providers/PermissionsContext";
type ContextType = {
organization: {
@@ -41,7 +41,7 @@ type ContextType = {
export default function TrustCenterFilesTab() {
const { __ } = useTranslate();
const { organization } = useOutletContext<ContextType>();
const { isAuthorized } = use(PermissionsContext);
const createSchema = z.object({
name: z.string().min(1, __("Name is required")),
category: z.string().min(1, __("Category is required")),
@@ -201,11 +201,11 @@ export default function TrustCenterFilesTab() {
{__("Upload and manage files for your trust center")}
</p>
</div>
<Authorized entity="Organization" action="createTrustCenterFile">
{isAuthorized("Organization", "createTrustCenterFile") && (
<Button icon={IconPlusLarge} onClick={() => createDialogRef.current?.open()}>
{__("Add File")}
</Button>
</Authorized>
)}
</div>
{(isUpdating || isDeleting) && (
<div className="flex items-center justify-center">

View File

@@ -11,10 +11,9 @@ import {
import { useOutletContext } from "react-router";
import { useUpdateTrustCenterMutation, useUploadTrustCenterNDAMutation, useDeleteTrustCenterNDAMutation } from "/hooks/graph/TrustCenterGraph";
import type { TrustCenterGraphQuery$data } from "/hooks/graph/__generated__/TrustCenterGraphQuery.graphql";
import { useState } from "react";
import { use, useState } from "react";
import { SlackConnections } from "../../../components/organizations/SlackConnection";
import { isAuthorized } from "/permissions";
import { useParams } from "react-router";
import { PermissionsContext } from "/providers/PermissionsContext";
type ContextType = {
organization: TrustCenterGraphQuery$data["organization"];
@@ -24,14 +23,14 @@ export default function TrustCenterOverviewTab() {
const { __ } = useTranslate();
const { toast } = useToast();
const { organization } = useOutletContext<ContextType>();
const { organizationId } = useParams();
const { isAuthorized } = use(PermissionsContext);
const [updateTrustCenter, isUpdating] = useUpdateTrustCenterMutation();
const [uploadNDA, isUploadingNDA] = useUploadTrustCenterNDAMutation();
const [deleteNDA, isDeletingNDA] = useDeleteTrustCenterNDAMutation();
const [isActive, setIsActive] = useState(organization.trustCenter?.active || false);
const canUpdateTrustCenter = organizationId ? isAuthorized(organizationId, "TrustCenter", "updateTrustCenter") : false;
const canUpdateTrustCenter = isAuthorized("TrustCenter", "updateTrustCenter");
const handleToggleActive = async (active: boolean) => {
if (!organization.trustCenter?.id) {

View File

@@ -29,7 +29,8 @@ import { ImportAssessmentDialog } from "./dialogs/ImportAssessmentDialog";
import { complianceReportsFragment } from "./tabs/VendorComplianceTab";
import type { VendorComplianceTabFragment$key } from "./tabs/__generated__/VendorComplianceTabFragment.graphql";
import { SnapshotBanner } from "/components/SnapshotBanner";
import { Authorized } from "/permissions";
import { use } from "react";
import { PermissionsContext } from "/providers/PermissionsContext";
type Props = {
queryRef: PreloadedQuery<VendorGraphNodeQuery>;
@@ -42,6 +43,7 @@ export default function VendorDetailPage(props: Props) {
const organizationId = useOrganizationId();
const { snapshotId } = useParams<{ snapshotId?: string }>();
const isSnapshotMode = Boolean(snapshotId);
const { isAuthorized } = use(PermissionsContext);
if (!vendor) {
return <div>{__("Vendor not found")}</div>;
@@ -98,7 +100,7 @@ export default function VendorDetailPage(props: Props) {
{__("Assessment From Website")}
</Button>
</ImportAssessmentDialog>
<Authorized entity="Vendor" action="deleteVendor">
{isAuthorized("Vendor", "deleteVendor") && (
<ActionDropdown variant="secondary">
<DropdownItem
variant="danger"
@@ -108,7 +110,7 @@ export default function VendorDetailPage(props: Props) {
{__("Delete")}
</DropdownItem>
</ActionDropdown>
</Authorized>
)}
</div>
)}
</div>

View File

@@ -37,8 +37,8 @@ import type {
} from "/hooks/graph/__generated__/VendorGraphPaginatedFragment.graphql";
import { SortableTable, SortableTh } from "/components/SortableTable";
import { SnapshotBanner } from "/components/SnapshotBanner";
import { Authorized } from "/permissions";
import { isAuthorized } from "/permissions";
import { use } from "react";
import { PermissionsContext } from "/providers/PermissionsContext";
type Vendor = NodeOf<VendorGraphPaginatedFragment$data["vendors"]>;
@@ -51,6 +51,7 @@ export default function VendorsPage(props: Props) {
const organizationId = useOrganizationId();
const { snapshotId } = useParams<{ snapshotId?: string }>();
const isSnapshotMode = Boolean(snapshotId);
const { isAuthorized } = use(PermissionsContext);
const data = usePreloadedQuery(vendorsQuery, props.queryRef);
const pagination = usePaginationFragment(
@@ -64,8 +65,8 @@ export default function VendorsPage(props: Props) {
usePageTitle(__("Vendors"));
const hasAnyAction = !isSnapshotMode && (
isAuthorized(organizationId, "Vendor", "updateVendor") ||
isAuthorized(organizationId, "Vendor", "deleteVendor")
isAuthorized("Vendor", "updateVendor") ||
isAuthorized("Vendor", "deleteVendor")
);
return (
@@ -78,14 +79,14 @@ export default function VendorsPage(props: Props) {
)}
>
{!isSnapshotMode && (
<Authorized entity="Organization" action="createVendor">
isAuthorized("Organization", "createVendor") && (
<CreateVendorDialog
connection={connectionId}
organizationId={organizationId}
>
<Button icon={IconPlusLarge}>{__("Add vendor")}</Button>
</CreateVendorDialog>
</Authorized>
)
)}
</PageHeader>
<SortableTable {...pagination}>
@@ -129,7 +130,7 @@ function VendorRow({
const isSnapshotMode = Boolean(snapshotId);
const { __ } = useTranslate();
const latestAssessment = vendor.riskAssessments?.edges[0]?.node;
const { isAuthorized } = use(PermissionsContext);
const deleteVendor = useDeleteVendor(vendor, connectionId);
const vendorUrl = isSnapshotMode && snapshotId
@@ -159,7 +160,7 @@ function VendorRow({
{hasAnyAction && (
<Td noLink width={50} className="text-end">
<ActionDropdown>
<Authorized entity="Vendor" action="deleteVendor">
{isAuthorized("Vendor", "deleteVendor") && (
<DropdownItem
onClick={deleteVendor}
variant="danger"
@@ -167,7 +168,7 @@ function VendorRow({
>
{__("Delete")}
</DropdownItem>
</Authorized>
)}
</ActionDropdown>
</Td>
)}

View File

@@ -6,7 +6,7 @@ import { PeopleSelectField } from "/components/form/PeopleSelectField";
import { ControlledField } from "/components/form/ControlledField";
import { CountriesField } from "/components/form/CountriesField";
import { useOrganizationId } from "/hooks/useOrganizationId";
import { useMemo } from "react";
import { use, useMemo } from "react";
import { usePageTitle } from "@probo/hooks";
import { downloadFile, formatDate } from "@probo/helpers";
import { useFragment, graphql } from "react-relay";
@@ -20,7 +20,7 @@ import type { useVendorFormFragment$key } from "/hooks/forms/__generated__/useVe
import type { VendorOverviewTabBusinessAssociateAgreementFragment$key } from "./__generated__/VendorOverviewTabBusinessAssociateAgreementFragment.graphql";
import type { VendorOverviewTabDataPrivacyAgreementFragment$key } from "./__generated__/VendorOverviewTabDataPrivacyAgreementFragment.graphql";
import type { VendorCategory } from "@probo/vendors";
import { Authorized } from "/permissions";
import { PermissionsContext } from "/providers/PermissionsContext";
const vendorBusinessAssociateAgreementFragment = graphql`
fragment VendorOverviewTabBusinessAssociateAgreementFragment on Vendor {
@@ -58,7 +58,7 @@ export default function VendorOverviewTab() {
}>();
const { __ } = useTranslate();
const { isAuthorized } = use(PermissionsContext);
const vendorCategories: { value: VendorCategory; label: string }[] = [
{ value: "ANALYTICS", label: __("Analytics") },
{ value: "CLOUD_MONITORING", label: __("Cloud Monitoring") },
@@ -396,11 +396,11 @@ export default function VendorOverviewTab() {
{/* Submit */}
{!isSnapshotMode && (
<div className="flex justify-end">
<Authorized entity="Vendor" action="updateVendor">
{isAuthorized("Vendor", "updateVendor") && (
<Button type="submit" disabled={isSubmitting}>
{__("Update vendor")}
</Button>
</Authorized>
)}
</div>
)}
</form>

View File

@@ -1,48 +0,0 @@
import { type ReactNode } from "react";
import { useParams } from "react-router";
import { usePermissions } from "./permissions";
type Props = {
entity: string;
action: string;
children: ReactNode;
fallback?: ReactNode;
};
/**
* Conditionally render children based on authorization check
* Automatically fetches permissions if not cached
*
* @param entity - The entity name (e.g., "Document", "Organization", "Vendor")
* @param action - The action/field name (e.g., "get", "createDocument", "updateVendor")
*
* @example
* <Authorized entity="Organization" action="createDocument">
* <CreateButton />
* </Authorized>
*
* @example
* <Authorized entity="Document" action="get">
* <DocumentViewer />
* </Authorized>
*
* @example
* <Authorized entity="Vendor" action="updateVendor">
* <EditButton />
* </Authorized>
*/
export function Authorized({
entity,
action,
children,
fallback = null,
}: Props) {
const { organizationId } = useParams();
const { loading, error, isAuthorized } = usePermissions(organizationId || "");
if (!organizationId || loading || error || !isAuthorized(entity, action)) {
return fallback;
}
return children;
}

View File

@@ -1,3 +0,0 @@
export { isAuthorized, getUserRole, getAssignableRoles } from "./permissions";
export { Authorized } from "./Authorized";
export type { EntityPermissions } from "./permissions";

View File

@@ -1,211 +0,0 @@
import { useEffect, useState } from 'react';
// Authorization system that checks permissions from the backend
// Permissions are fetched per organization from /authz/:organizationId/permissions
// Format: { permissions: { "Document": { "node": true, "updateDocument": true }, "Organization": { "createDocument": true } }, role: "ADMIN" }
export type EntityPermissions = Record<string, Record<string, boolean>>;
type PermissionsResponse = {
permissions: EntityPermissions;
role: string;
};
type PermissionsCache = {
[organizationId: string]: {
permissions: EntityPermissions;
role: string;
};
};
const cache: PermissionsCache = {};
const pendingRequests: Map<string, Promise<PermissionsResponse>> = new Map();
/**
* Fetch permissions for the current user's role in the organization
*/
function fetchPermissions(organizationId: string): Promise<PermissionsResponse> {
if (cache[organizationId]) {
return Promise.resolve(cache[organizationId]);
}
const pending = pendingRequests.get(organizationId);
if (pending) {
return pending;
}
const promise = fetch(`/authz/${encodeURIComponent(organizationId)}/permissions`, {
credentials: 'include',
})
.then((response) => {
if (!response.ok) {
throw new Error(`Failed to fetch permissions: ${response.statusText}`);
}
return response.json();
})
.then((data: PermissionsResponse) => {
if (!data || typeof data.permissions !== 'object' || !data.role) {
throw new Error('Invalid permissions response structure');
}
cache[organizationId] = {
permissions: data.permissions,
role: data.role,
};
pendingRequests.delete(organizationId);
return data;
})
.catch((error) => {
pendingRequests.delete(organizationId);
throw error;
});
pendingRequests.set(organizationId, promise);
return promise;
}
/**
* React hook to fetch and manage permissions for an organization
*
* @param organizationId - The organization ID
* @returns Object with loading, error, permissions, role, and helper functions
*
* @example
* const { loading, error, permissions, role, isAuthorized, getAssignableRoles } = usePermissions(orgId);
*
* if (loading) return <Spinner />;
* if (error) return <ErrorMessage />;
* if (isAuthorized("Document", "updateDocument")) { ... }
*/
export function usePermissions(organizationId: string) {
const [state, setState] = useState<{
loading: boolean;
error: Error | null;
permissions: EntityPermissions | null;
role: string | null;
}>({
loading: true,
error: null,
permissions: null,
role: null,
});
useEffect(() => {
setState({ loading: true, error: null, permissions: null, role: null });
fetchPermissions(organizationId)
.then((data) => {
setState({
loading: false,
error: null,
permissions: data.permissions,
role: data.role,
});
})
.catch((error) => {
setState({
loading: false,
error,
permissions: null,
role: null,
});
});
}, [organizationId]);
const checkAuthorized = (entity: string, action: string): boolean => {
if (!state.permissions) return false;
const entityPermissions = state.permissions[entity];
if (!entityPermissions) return false;
return entityPermissions[action] === true;
};
const getAssignableRolesList = (): string[] => {
if (!state.role) return [];
if (state.role === "OWNER" || state.role === "FULL") {
return ["OWNER", "ADMIN", "VIEWER"];
}
if (state.role === "ADMIN") {
return ["ADMIN", "VIEWER"];
}
return [];
};
return {
loading: state.loading,
error: state.error,
permissions: state.permissions,
role: state.role,
isAuthorized: checkAuthorized,
getAssignableRoles: getAssignableRolesList,
};
}
/**
* Check if the user has permission for an entity and action (synchronous)
* Returns false if permissions are not loaded yet
*
* @param organizationId - The organization ID
* @param entity - The entity name (e.g., "Document", "Organization", "Vendor")
* @param action - The action/field name (e.g., "node", "updateDocument", "createDocument")
* @returns true if the user has permission, false otherwise
*
* @example
* isAuthorized(orgId, "Document", "get")
* isAuthorized(orgId, "Document", "updateDocument")
* isAuthorized(orgId, "Organization", "createDocument")
*/
export function isAuthorized(
organizationId: string,
entity: string,
action: string
): boolean {
const cached = cache[organizationId];
if (!cached) return false;
const entityPermissions = cached.permissions[entity];
if (!entityPermissions) return false;
return entityPermissions[action] === true;
}
/**
* Get the current user's role in the organization
* Returns empty string if not loaded yet
*
* @param organizationId - The organization ID
* @returns The user's role (e.g., "OWNER", "ADMIN", "VIEWER", "FULL")
*
* @example
* getUserRole(orgId) // Returns "ADMIN"
*/
export function getUserRole(organizationId: string): string {
const cached = cache[organizationId];
return cached?.role || "";
}
/**
* Get available roles that the current user can assign
* Based on the rule: OWNER and FULL can assign any role, ADMIN can assign ADMIN and VIEWER but not OWNER
*
* @param organizationId - The organization ID
* @returns Array of roles that can be assigned
*/
export function getAssignableRoles(organizationId: string): string[] {
const currentRole = getUserRole(organizationId);
if (!currentRole) return [];
if (currentRole === "OWNER" || currentRole === "FULL") {
return ["OWNER", "ADMIN", "VIEWER"];
}
if (currentRole === "ADMIN") {
return ["ADMIN", "VIEWER"];
}
return [];
}

View File

@@ -0,0 +1,17 @@
import { createContext } from "react";
import { Role } from "@probo/helpers";
export type PermissionsResponse = {
permissions: Record<string, Record<string, boolean>>;
role: Role;
};
type PermissionsContextType = {
isAuthorized: (entity: string, action: string) => boolean;
} & PermissionsResponse;
export const PermissionsContext = createContext<PermissionsContextType>({
permissions: {},
role: Role.VIEWER,
isAuthorized: () => false,
});

View File

@@ -1,27 +1,7 @@
import { useSuspenseQuery } from "@tanstack/react-query";
import { createContext, type PropsWithChildren } from "react";
import { type PropsWithChildren } from "react";
import { useOrganizationId } from "/hooks/useOrganizationId";
enum Role {
OWNER = "OWNER",
ADMIN = "ADMIN",
VIEWER = "VIEWER",
}
type PermissionsResponse = {
permissions: Record<string, Record<string, boolean>>;
role: Role;
};
type PermissionsContextType = {
isAuthorized: (entity: string, action: string) => boolean;
} & PermissionsResponse;
export const PermissionsContext = createContext<PermissionsContextType>({
permissions: {},
role: Role.VIEWER,
isAuthorized: () => false,
});
import { PermissionsContext, type PermissionsResponse } from "./PermissionsContext";
export function PermissionsProvider(props: PropsWithChildren) {
const { children } = props;