Add role management
Signed-off-by: Sacha Al Himdani <sacha@getprobo.com>
This commit is contained in:
@@ -68,6 +68,20 @@ export function PageError({ resetErrorBoundary, error: propsError }: Props) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (error && error.toString().includes("FORBIDDEN")) {
|
||||||
|
return (
|
||||||
|
<div className={classNames.wrapper}>
|
||||||
|
<h1 className={classNames.title}>
|
||||||
|
<IconPageCross size={26} />
|
||||||
|
{__("Page not found")}
|
||||||
|
</h1>
|
||||||
|
<p className={classNames.description}>
|
||||||
|
{__("The page you are looking for does not exist")}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
if (error && error.toString().includes("UNAUTHORIZED")) {
|
if (error && error.toString().includes("UNAUTHORIZED")) {
|
||||||
return (
|
return (
|
||||||
<div className={classNames.wrapper}>
|
<div className={classNames.wrapper}>
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import type { CustomDomainManagerDeleteMutation } from "./__generated__/CustomDo
|
|||||||
import { CreateCustomDomainDialog } from "./CreateCustomDomainDialog";
|
import { CreateCustomDomainDialog } from "./CreateCustomDomainDialog";
|
||||||
import { DeleteCustomDomainDialog } from "./DeleteCustomDomainDialog";
|
import { DeleteCustomDomainDialog } from "./DeleteCustomDomainDialog";
|
||||||
import { DomainDetailsDialog } from "./DomainDetailsDialog";
|
import { DomainDetailsDialog } from "./DomainDetailsDialog";
|
||||||
|
import { Authorized } from "/permissions";
|
||||||
|
|
||||||
const deleteCustomDomainMutation = graphql`
|
const deleteCustomDomainMutation = graphql`
|
||||||
mutation CustomDomainManagerDeleteMutation($input: DeleteCustomDomainInput!) {
|
mutation CustomDomainManagerDeleteMutation($input: DeleteCustomDomainInput!) {
|
||||||
@@ -106,9 +107,11 @@ export function CustomDomainManager({
|
|||||||
)}
|
)}
|
||||||
</p>
|
</p>
|
||||||
<div className="flex justify-center">
|
<div className="flex justify-center">
|
||||||
|
<Authorized entity="Organization" action="createCustomDomain">
|
||||||
<CreateCustomDomainDialog organizationId={organizationId}>
|
<CreateCustomDomainDialog organizationId={organizationId}>
|
||||||
<Button icon={IconPlusLarge}>{__("Add Domain")}</Button>
|
<Button icon={IconPlusLarge}>{__("Add Domain")}</Button>
|
||||||
</CreateCustomDomainDialog>
|
</CreateCustomDomainDialog>
|
||||||
|
</Authorized>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</Card>
|
</Card>
|
||||||
@@ -136,12 +139,14 @@ export function CustomDomainManager({
|
|||||||
<Button variant="secondary">{__("View Details")}</Button>
|
<Button variant="secondary">{__("View Details")}</Button>
|
||||||
</DomainDetailsDialog>
|
</DomainDetailsDialog>
|
||||||
|
|
||||||
|
<Authorized entity="CustomDomain" action="deleteCustomDomain">
|
||||||
<DeleteCustomDomainDialog
|
<DeleteCustomDomainDialog
|
||||||
domainName={domain.domain}
|
domainName={domain.domain}
|
||||||
onConfirm={handleDeleteDomain}
|
onConfirm={handleDeleteDomain}
|
||||||
>
|
>
|
||||||
<Button variant="danger">{__("Delete")}</Button>
|
<Button variant="danger">{__("Delete")}</Button>
|
||||||
</DeleteCustomDomainDialog>
|
</DeleteCustomDomainDialog>
|
||||||
|
</Authorized>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -5,6 +5,8 @@ import {
|
|||||||
DialogFooter,
|
DialogFooter,
|
||||||
Field,
|
Field,
|
||||||
Checkbox,
|
Checkbox,
|
||||||
|
Select,
|
||||||
|
Option,
|
||||||
useDialogRef,
|
useDialogRef,
|
||||||
} from "@probo/ui";
|
} from "@probo/ui";
|
||||||
import type { PropsWithChildren } from "react";
|
import type { PropsWithChildren } from "react";
|
||||||
@@ -15,6 +17,8 @@ import { useMutationWithToasts } from "/hooks/useMutationWithToasts";
|
|||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import { useFormWithSchema } from "/hooks/useFormWithSchema";
|
import { useFormWithSchema } from "/hooks/useFormWithSchema";
|
||||||
import { Controller } from "react-hook-form";
|
import { Controller } from "react-hook-form";
|
||||||
|
import { Suspense } from "react";
|
||||||
|
import { getAssignableRoles } from "/permissions";
|
||||||
|
|
||||||
const inviteMutation = graphql`
|
const inviteMutation = graphql`
|
||||||
mutation InviteUserDialogMutation(
|
mutation InviteUserDialogMutation(
|
||||||
@@ -40,6 +44,7 @@ const inviteMutation = graphql`
|
|||||||
const schema = z.object({
|
const schema = z.object({
|
||||||
email: z.string().email(),
|
email: z.string().email(),
|
||||||
fullName: z.string(),
|
fullName: z.string(),
|
||||||
|
role: z.enum(["OWNER", "ADMIN", "FULL", "VIEWER"]).default("VIEWER"),
|
||||||
createPeople: z.boolean().default(false),
|
createPeople: z.boolean().default(false),
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -48,16 +53,17 @@ type Props = PropsWithChildren & {
|
|||||||
onRefetch: () => void;
|
onRefetch: () => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
export function InviteUserDialog({ children, connectionId, onRefetch }: Props) {
|
function InviteUserDialogContent({ children, connectionId, onRefetch }: Props) {
|
||||||
const { __ } = useTranslate();
|
const { __ } = useTranslate();
|
||||||
const organizationId = useOrganizationId();
|
const organizationId = useOrganizationId();
|
||||||
|
const assignableRoles = getAssignableRoles(organizationId);
|
||||||
const [inviteUser, isInviting] = useMutationWithToasts(inviteMutation, {
|
const [inviteUser, isInviting] = useMutationWithToasts(inviteMutation, {
|
||||||
successMessage: __("Invitation sent successfully"),
|
successMessage: __("Invitation sent successfully"),
|
||||||
errorMessage: __("Failed to send invitation"),
|
errorMessage: __("Failed to send invitation"),
|
||||||
});
|
});
|
||||||
const { register, handleSubmit, formState, reset, control } = useFormWithSchema(
|
const { register, handleSubmit, formState, reset, control } = useFormWithSchema(
|
||||||
schema,
|
schema,
|
||||||
{ defaultValues: { createPeople: false } },
|
{ defaultValues: { role: "VIEWER", createPeople: false } },
|
||||||
);
|
);
|
||||||
|
|
||||||
const dialogRef = useDialogRef();
|
const dialogRef = useDialogRef();
|
||||||
@@ -69,6 +75,7 @@ export function InviteUserDialog({ children, connectionId, onRefetch }: Props) {
|
|||||||
organizationId,
|
organizationId,
|
||||||
email: data.email,
|
email: data.email,
|
||||||
fullName: data.fullName,
|
fullName: data.fullName,
|
||||||
|
role: data.role,
|
||||||
createPeople: data.createPeople,
|
createPeople: data.createPeople,
|
||||||
},
|
},
|
||||||
connections: connectionId ? [connectionId] : ["SettingsPageInvitations_invitations"],
|
connections: connectionId ? [connectionId] : ["SettingsPageInvitations_invitations"],
|
||||||
@@ -107,6 +114,32 @@ export function InviteUserDialog({ children, connectionId, onRefetch }: Props) {
|
|||||||
{...register("fullName")}
|
{...register("fullName")}
|
||||||
error={formState.errors.fullName?.message}
|
error={formState.errors.fullName?.message}
|
||||||
/>
|
/>
|
||||||
|
<Field label={__("Role")} required>
|
||||||
|
<Controller
|
||||||
|
name="role"
|
||||||
|
control={control}
|
||||||
|
render={({ field }) => (
|
||||||
|
<>
|
||||||
|
<Select value={field.value} onValueChange={field.onChange}>
|
||||||
|
{assignableRoles.includes("OWNER") && <Option value="OWNER">{__("Owner")}</Option>}
|
||||||
|
{assignableRoles.includes("ADMIN") && <Option value="ADMIN">{__("Admin")}</Option>}
|
||||||
|
{assignableRoles.includes("VIEWER") && <Option value="VIEWER">{__("Viewer")}</Option>}
|
||||||
|
</Select>
|
||||||
|
<div className="mt-2 text-sm text-txt-tertiary">
|
||||||
|
{field.value === "OWNER" && (
|
||||||
|
<p>{__("Full access to everything")}</p>
|
||||||
|
)}
|
||||||
|
{field.value === "ADMIN" && (
|
||||||
|
<p>{__("Full access except organization setup and API keys")}</p>
|
||||||
|
)}
|
||||||
|
{field.value === "VIEWER" && (
|
||||||
|
<p>{__("Read-only access")}</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</Field>
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<div className="flex items-center space-x-3">
|
<div className="flex items-center space-x-3">
|
||||||
<Controller
|
<Controller
|
||||||
@@ -142,3 +175,11 @@ export function InviteUserDialog({ children, connectionId, onRefetch }: Props) {
|
|||||||
</Dialog>
|
</Dialog>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function InviteUserDialog(props: Props) {
|
||||||
|
return (
|
||||||
|
<Suspense fallback={props.children}>
|
||||||
|
<InviteUserDialogContent {...props} />
|
||||||
|
</Suspense>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import { Badge, Button, Card } from "@probo/ui";
|
|||||||
import { useTranslate } from "@probo/i18n";
|
import { useTranslate } from "@probo/i18n";
|
||||||
import { sprintf } from "@probo/helpers";
|
import { sprintf } from "@probo/helpers";
|
||||||
import type { TrustCenterGraphQuery$data } from "/hooks/graph/__generated__/TrustCenterGraphQuery.graphql";
|
import type { TrustCenterGraphQuery$data } from "/hooks/graph/__generated__/TrustCenterGraphQuery.graphql";
|
||||||
|
import { Authorized } from "/permissions";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
organizationId: string;
|
organizationId: string;
|
||||||
@@ -76,9 +77,11 @@ export function SlackConnections({ organizationId, slackConnections: connectedSl
|
|||||||
</Badge>
|
</Badge>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
|
<Authorized entity="TrustCenter" action="updateTrustCenter">
|
||||||
<Button variant="secondary" asChild>
|
<Button variant="secondary" asChild>
|
||||||
<a href={getUrl(slackConnection.id)}>{__("Connect")}</a>
|
<a href={getUrl(slackConnection.id)}>{__("Connect")}</a>
|
||||||
</Button>
|
</Button>
|
||||||
|
</Authorized>
|
||||||
)}
|
)}
|
||||||
</Card>
|
</Card>
|
||||||
))}
|
))}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
/**
|
/**
|
||||||
* @generated SignedSource<<617ea2708c8402c706671dc1a63b1316>>
|
* @generated SignedSource<<eda42f72473c65692ddd9cee68c0ce81>>
|
||||||
* @lightSyntaxTransform
|
* @lightSyntaxTransform
|
||||||
* @nogrep
|
* @nogrep
|
||||||
*/
|
*/
|
||||||
@@ -9,12 +9,13 @@
|
|||||||
// @ts-nocheck
|
// @ts-nocheck
|
||||||
|
|
||||||
import { ConcreteRequest } from 'relay-runtime';
|
import { ConcreteRequest } from 'relay-runtime';
|
||||||
export type Role = "ADMIN" | "MEMBER" | "OWNER" | "VIEWER";
|
export type MembershipRole = "ADMIN" | "OWNER" | "VIEWER";
|
||||||
export type InviteUserInput = {
|
export type InviteUserInput = {
|
||||||
createPeople: boolean;
|
createPeople: boolean;
|
||||||
email: string;
|
email: string;
|
||||||
fullName: string;
|
fullName: string;
|
||||||
organizationId: string;
|
organizationId: string;
|
||||||
|
role: MembershipRole;
|
||||||
};
|
};
|
||||||
export type InviteUserDialogMutation$variables = {
|
export type InviteUserDialogMutation$variables = {
|
||||||
connections: ReadonlyArray<string>;
|
connections: ReadonlyArray<string>;
|
||||||
@@ -30,7 +31,7 @@ export type InviteUserDialogMutation$data = {
|
|||||||
readonly expiresAt: any;
|
readonly expiresAt: any;
|
||||||
readonly fullName: string;
|
readonly fullName: string;
|
||||||
readonly id: string;
|
readonly id: string;
|
||||||
readonly role: Role;
|
readonly role: MembershipRole;
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -26,6 +26,8 @@ import TaskFormDialog, {
|
|||||||
import { useOrganizationId } from "/hooks/useOrganizationId";
|
import { useOrganizationId } from "/hooks/useOrganizationId";
|
||||||
import { Link, useLocation, useParams } from "react-router";
|
import { Link, useLocation, useParams } from "react-router";
|
||||||
import { promisifyMutation } from "@probo/helpers";
|
import { promisifyMutation } from "@probo/helpers";
|
||||||
|
import { Authorized } from "/permissions";
|
||||||
|
import { isAuthorized } from "/permissions";
|
||||||
import type { TaskFormDialogFragment$key } from "./__generated__/TaskFormDialogFragment.graphql";
|
import type { TaskFormDialogFragment$key } from "./__generated__/TaskFormDialogFragment.graphql";
|
||||||
import { updateStoreCounter } from "/hooks/useMutationWithIncrement";
|
import { updateStoreCounter } from "/hooks/useMutationWithIncrement";
|
||||||
|
|
||||||
@@ -49,6 +51,7 @@ type Props = {
|
|||||||
|
|
||||||
export default function TasksCard({ tasks, connectionId }: Props) {
|
export default function TasksCard({ tasks, connectionId }: Props) {
|
||||||
const { __ } = useTranslate();
|
const { __ } = useTranslate();
|
||||||
|
const organizationId = useOrganizationId();
|
||||||
const hash = useLocation().hash.replace("#", "");
|
const hash = useLocation().hash.replace("#", "");
|
||||||
|
|
||||||
const hashes = [
|
const hashes = [
|
||||||
@@ -67,6 +70,9 @@ export default function TasksCard({ tasks, connectionId }: Props) {
|
|||||||
|
|
||||||
usePageTitle(__("Tasks"));
|
usePageTitle(__("Tasks"));
|
||||||
|
|
||||||
|
const hasAnyAction = isAuthorized(organizationId, "Task", "updateTask") ||
|
||||||
|
isAuthorized(organizationId, "Task", "deleteTask");
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-6">
|
<div className="space-y-6">
|
||||||
{tasks?.length === 0 ? (
|
{tasks?.length === 0 ? (
|
||||||
@@ -100,6 +106,7 @@ export default function TasksCard({ tasks, connectionId }: Props) {
|
|||||||
key={task.id}
|
key={task.id}
|
||||||
task={task}
|
task={task}
|
||||||
connectionId={connectionId}
|
connectionId={connectionId}
|
||||||
|
hasAnyAction={hasAnyAction}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
</Fragment>
|
</Fragment>
|
||||||
@@ -110,6 +117,7 @@ export default function TasksCard({ tasks, connectionId }: Props) {
|
|||||||
key={task.id}
|
key={task.id}
|
||||||
task={task}
|
task={task}
|
||||||
connectionId={connectionId}
|
connectionId={connectionId}
|
||||||
|
hasAnyAction={hasAnyAction}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
@@ -122,6 +130,7 @@ export default function TasksCard({ tasks, connectionId }: Props) {
|
|||||||
type TaskRowProps = {
|
type TaskRowProps = {
|
||||||
task: ItemOf<Props["tasks"]> & TaskFormDialogFragment$key;
|
task: ItemOf<Props["tasks"]> & TaskFormDialogFragment$key;
|
||||||
connectionId: string;
|
connectionId: string;
|
||||||
|
hasAnyAction: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
const deleteMutation = graphql`
|
const deleteMutation = graphql`
|
||||||
@@ -221,13 +230,17 @@ function TaskRow(props: TaskRowProps) {
|
|||||||
<Avatar name={props.task.assignedTo?.fullName ?? ""} />
|
<Avatar name={props.task.assignedTo?.fullName ?? ""} />
|
||||||
</Link>
|
</Link>
|
||||||
)}
|
)}
|
||||||
|
{props.hasAnyAction && (
|
||||||
<ActionDropdown>
|
<ActionDropdown>
|
||||||
|
<Authorized entity="Task" action="updateTask">
|
||||||
<DropdownItem
|
<DropdownItem
|
||||||
icon={IconPencil}
|
icon={IconPencil}
|
||||||
onClick={() => dialogRef.current?.open()}
|
onClick={() => dialogRef.current?.open()}
|
||||||
>
|
>
|
||||||
{__("Edit")}
|
{__("Edit")}
|
||||||
</DropdownItem>
|
</DropdownItem>
|
||||||
|
</Authorized>
|
||||||
|
<Authorized entity="Task" action="deleteTask">
|
||||||
<DropdownItem
|
<DropdownItem
|
||||||
variant="danger"
|
variant="danger"
|
||||||
icon={IconTrashCan}
|
icon={IconTrashCan}
|
||||||
@@ -235,7 +248,9 @@ function TaskRow(props: TaskRowProps) {
|
|||||||
>
|
>
|
||||||
{__("Delete")}
|
{__("Delete")}
|
||||||
</DropdownItem>
|
</DropdownItem>
|
||||||
|
</Authorized>
|
||||||
</ActionDropdown>
|
</ActionDropdown>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ import { useMemo, useState, useCallback, useEffect } from "react";
|
|||||||
import { sprintf, getAuditStateVariant, getAuditStateLabel, formatDate, getTrustCenterVisibilityOptions } from "@probo/helpers";
|
import { sprintf, getAuditStateVariant, getAuditStateLabel, formatDate, getTrustCenterVisibilityOptions } from "@probo/helpers";
|
||||||
import { useOrganizationId } from "/hooks/useOrganizationId";
|
import { useOrganizationId } from "/hooks/useOrganizationId";
|
||||||
import type { TrustCenterAuditsCardFragment$key } from "./__generated__/TrustCenterAuditsCardFragment.graphql";
|
import type { TrustCenterAuditsCardFragment$key } from "./__generated__/TrustCenterAuditsCardFragment.graphql";
|
||||||
|
import { isAuthorized } from "/permissions";
|
||||||
|
|
||||||
const trustCenterAuditFragment = graphql`
|
const trustCenterAuditFragment = graphql`
|
||||||
fragment TrustCenterAuditsCardFragment on Audit {
|
fragment TrustCenterAuditsCardFragment on Audit {
|
||||||
@@ -124,6 +125,8 @@ function AuditRow(props: {
|
|||||||
const { __ } = useTranslate();
|
const { __ } = useTranslate();
|
||||||
const [optimisticValue, setOptimisticValue] = useState<string | null>(null);
|
const [optimisticValue, setOptimisticValue] = useState<string | null>(null);
|
||||||
|
|
||||||
|
const canUpdate = organizationId ? isAuthorized(organizationId, "TrustCenter", "updateTrustCenter") : false;
|
||||||
|
|
||||||
const handleValueChange = useCallback((value: string | {}) => {
|
const handleValueChange = useCallback((value: string | {}) => {
|
||||||
const stringValue = typeof value === 'string' ? value : '';
|
const stringValue = typeof value === 'string' ? value : '';
|
||||||
const typedValue = stringValue as "NONE" | "PRIVATE" | "PUBLIC";
|
const typedValue = stringValue as "NONE" | "PRIVATE" | "PUBLIC";
|
||||||
@@ -164,7 +167,7 @@ function AuditRow(props: {
|
|||||||
type="select"
|
type="select"
|
||||||
value={currentValue}
|
value={currentValue}
|
||||||
onValueChange={handleValueChange}
|
onValueChange={handleValueChange}
|
||||||
disabled={props.disabled}
|
disabled={props.disabled || !canUpdate}
|
||||||
className="w-[105px]"
|
className="w-[105px]"
|
||||||
>
|
>
|
||||||
{visibilityOptions.map((option) => (
|
{visibilityOptions.map((option) => (
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ import { useFragment } from "react-relay";
|
|||||||
import { useMemo, useState, useCallback, useEffect } from "react";
|
import { useMemo, useState, useCallback, useEffect } from "react";
|
||||||
import { sprintf, getTrustCenterVisibilityOptions } from "@probo/helpers";
|
import { sprintf, getTrustCenterVisibilityOptions } from "@probo/helpers";
|
||||||
import { useOrganizationId } from "/hooks/useOrganizationId";
|
import { useOrganizationId } from "/hooks/useOrganizationId";
|
||||||
|
import { isAuthorized } from "/permissions";
|
||||||
|
|
||||||
const trustCenterDocumentFragment = graphql`
|
const trustCenterDocumentFragment = graphql`
|
||||||
fragment TrustCenterDocumentsCardFragment on Document {
|
fragment TrustCenterDocumentsCardFragment on Document {
|
||||||
@@ -129,6 +130,8 @@ function DocumentRow(props: {
|
|||||||
const { __ } = useTranslate();
|
const { __ } = useTranslate();
|
||||||
const [optimisticValue, setOptimisticValue] = useState<string | null>(null);
|
const [optimisticValue, setOptimisticValue] = useState<string | null>(null);
|
||||||
|
|
||||||
|
const canUpdate = organizationId ? isAuthorized(organizationId, "TrustCenter", "updateTrustCenter") : false;
|
||||||
|
|
||||||
const handleValueChange = useCallback((value: string | {}) => {
|
const handleValueChange = useCallback((value: string | {}) => {
|
||||||
const stringValue = typeof value === 'string' ? value : '';
|
const stringValue = typeof value === 'string' ? value : '';
|
||||||
const typedValue = stringValue as "NONE" | "PRIVATE" | "PUBLIC";
|
const typedValue = stringValue as "NONE" | "PRIVATE" | "PUBLIC";
|
||||||
@@ -164,7 +167,7 @@ function DocumentRow(props: {
|
|||||||
type="select"
|
type="select"
|
||||||
value={currentValue}
|
value={currentValue}
|
||||||
onValueChange={handleValueChange}
|
onValueChange={handleValueChange}
|
||||||
disabled={props.disabled}
|
disabled={props.disabled || !canUpdate}
|
||||||
className="w-[105px]"
|
className="w-[105px]"
|
||||||
>
|
>
|
||||||
{visibilityOptions.map((option) => (
|
{visibilityOptions.map((option) => (
|
||||||
|
|||||||
@@ -21,6 +21,9 @@ import { useFragment } from "react-relay";
|
|||||||
import { useMemo, useState, useCallback, useEffect } from "react";
|
import { useMemo, useState, useCallback, useEffect } from "react";
|
||||||
import { sprintf, getTrustCenterVisibilityOptions } from "@probo/helpers";
|
import { sprintf, getTrustCenterVisibilityOptions } from "@probo/helpers";
|
||||||
import { formatDate } from "@probo/helpers";
|
import { formatDate } from "@probo/helpers";
|
||||||
|
import { Authorized } from "/permissions";
|
||||||
|
import { isAuthorized } from "/permissions";
|
||||||
|
import { useParams } from "react-router";
|
||||||
|
|
||||||
const trustCenterFileFragment = graphql`
|
const trustCenterFileFragment = graphql`
|
||||||
fragment TrustCenterFilesCardFragment on TrustCenterFile {
|
fragment TrustCenterFilesCardFragment on TrustCenterFile {
|
||||||
@@ -147,6 +150,9 @@ function FileRow(props: {
|
|||||||
const file = props.file;
|
const file = props.file;
|
||||||
const { __ } = useTranslate();
|
const { __ } = useTranslate();
|
||||||
const [optimisticValue, setOptimisticValue] = useState<string | null>(null);
|
const [optimisticValue, setOptimisticValue] = useState<string | null>(null);
|
||||||
|
const { organizationId } = useParams();
|
||||||
|
|
||||||
|
const canUpdate = organizationId ? isAuthorized(organizationId, "TrustCenter", "updateTrustCenter") : false;
|
||||||
|
|
||||||
const handleValueChange = useCallback((value: string | {}) => {
|
const handleValueChange = useCallback((value: string | {}) => {
|
||||||
const stringValue = typeof value === 'string' ? value : '';
|
const stringValue = typeof value === 'string' ? value : '';
|
||||||
@@ -179,7 +185,7 @@ function FileRow(props: {
|
|||||||
type="select"
|
type="select"
|
||||||
value={currentValue}
|
value={currentValue}
|
||||||
onValueChange={handleValueChange}
|
onValueChange={handleValueChange}
|
||||||
disabled={props.disabled}
|
disabled={props.disabled || !canUpdate}
|
||||||
className="w-[105px]"
|
className="w-[105px]"
|
||||||
>
|
>
|
||||||
{visibilityOptions.map((option) => (
|
{visibilityOptions.map((option) => (
|
||||||
@@ -201,6 +207,7 @@ function FileRow(props: {
|
|||||||
onClick={() => window.open(file.fileUrl, '_blank', 'noopener,noreferrer')}
|
onClick={() => window.open(file.fileUrl, '_blank', 'noopener,noreferrer')}
|
||||||
title={__("Download")}
|
title={__("Download")}
|
||||||
/>
|
/>
|
||||||
|
<Authorized entity="TrustCenterFile" action="updateTrustCenterFile">
|
||||||
<Button
|
<Button
|
||||||
variant="secondary"
|
variant="secondary"
|
||||||
icon={IconPencil}
|
icon={IconPencil}
|
||||||
@@ -208,6 +215,8 @@ function FileRow(props: {
|
|||||||
disabled={props.disabled}
|
disabled={props.disabled}
|
||||||
title={__("Edit")}
|
title={__("Edit")}
|
||||||
/>
|
/>
|
||||||
|
</Authorized>
|
||||||
|
<Authorized entity="TrustCenterFile" action="deleteTrustCenterFile">
|
||||||
<Button
|
<Button
|
||||||
variant="danger"
|
variant="danger"
|
||||||
icon={IconTrashCan}
|
icon={IconTrashCan}
|
||||||
@@ -215,6 +224,7 @@ function FileRow(props: {
|
|||||||
disabled={props.disabled}
|
disabled={props.disabled}
|
||||||
title={__("Delete")}
|
title={__("Delete")}
|
||||||
/>
|
/>
|
||||||
|
</Authorized>
|
||||||
</div>
|
</div>
|
||||||
</Td>
|
</Td>
|
||||||
</Tr>
|
</Tr>
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ import {
|
|||||||
} from "/hooks/graph/TrustCenterReferenceGraph";
|
} from "/hooks/graph/TrustCenterReferenceGraph";
|
||||||
import { TrustCenterReferenceDialog, type TrustCenterReferenceDialogRef } from "./TrustCenterReferenceDialog";
|
import { TrustCenterReferenceDialog, type TrustCenterReferenceDialogRef } from "./TrustCenterReferenceDialog";
|
||||||
import { DeleteTrustCenterReferenceDialog } from "./DeleteTrustCenterReferenceDialog";
|
import { DeleteTrustCenterReferenceDialog } from "./DeleteTrustCenterReferenceDialog";
|
||||||
|
import { Authorized } from "/permissions";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
trustCenterId: string;
|
trustCenterId: string;
|
||||||
@@ -111,12 +112,14 @@ export function TrustCenterReferencesSection({ trustCenterId }: Props) {
|
|||||||
{__("Showcase your customers and partners on your trust center")}
|
{__("Showcase your customers and partners on your trust center")}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
<Authorized entity="TrustCenter" action="createTrustCenterReference">
|
||||||
<Button
|
<Button
|
||||||
icon={IconPlusLarge}
|
icon={IconPlusLarge}
|
||||||
onClick={handleCreate}
|
onClick={handleCreate}
|
||||||
>
|
>
|
||||||
{__("Add Reference")}
|
{__("Add Reference")}
|
||||||
</Button>
|
</Button>
|
||||||
|
</Authorized>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Table>
|
<Table>
|
||||||
@@ -228,11 +231,14 @@ function ReferenceRow({
|
|||||||
icon={IconArrowLink}
|
icon={IconArrowLink}
|
||||||
onClick={onVisitWebsite}
|
onClick={onVisitWebsite}
|
||||||
/>
|
/>
|
||||||
|
<Authorized entity="TrustCenterReference" action="updateTrustCenterReference">
|
||||||
<Button
|
<Button
|
||||||
variant="secondary"
|
variant="secondary"
|
||||||
icon={IconPencil}
|
icon={IconPencil}
|
||||||
onClick={onEdit}
|
onClick={onEdit}
|
||||||
/>
|
/>
|
||||||
|
</Authorized>
|
||||||
|
<Authorized entity="TrustCenterReference" action="deleteTrustCenterReference">
|
||||||
<DeleteTrustCenterReferenceDialog
|
<DeleteTrustCenterReferenceDialog
|
||||||
referenceId={reference.id}
|
referenceId={reference.id}
|
||||||
referenceName={reference.name}
|
referenceName={reference.name}
|
||||||
@@ -243,6 +249,7 @@ function ReferenceRow({
|
|||||||
icon={IconTrashCan}
|
icon={IconTrashCan}
|
||||||
/>
|
/>
|
||||||
</DeleteTrustCenterReferenceDialog>
|
</DeleteTrustCenterReferenceDialog>
|
||||||
|
</Authorized>
|
||||||
</div>
|
</div>
|
||||||
</Td>
|
</Td>
|
||||||
</Tr>
|
</Tr>
|
||||||
|
|||||||
@@ -14,9 +14,10 @@ import {
|
|||||||
} from "@probo/ui";
|
} from "@probo/ui";
|
||||||
import { useTranslate } from "@probo/i18n";
|
import { useTranslate } from "@probo/i18n";
|
||||||
import { useFragment } from "react-relay";
|
import { useFragment } from "react-relay";
|
||||||
import { useMemo, useState } from "react";
|
import { useMemo, useState, useEffect } from "react";
|
||||||
import { sprintf } from "@probo/helpers";
|
import { sprintf } from "@probo/helpers";
|
||||||
import { useOrganizationId } from "/hooks/useOrganizationId";
|
import { useOrganizationId } from "/hooks/useOrganizationId";
|
||||||
|
import { isAuthorized } from "/permissions/permissions";
|
||||||
import type { TrustCenterVendorsCardFragment$key } from "./__generated__/TrustCenterVendorsCardFragment.graphql";
|
import type { TrustCenterVendorsCardFragment$key } from "./__generated__/TrustCenterVendorsCardFragment.graphql";
|
||||||
|
|
||||||
const trustCenterVendorFragment = graphql`
|
const trustCenterVendorFragment = graphql`
|
||||||
@@ -48,12 +49,44 @@ type Props<Params> = {
|
|||||||
|
|
||||||
export function TrustCenterVendorsCard<Params>(props: Props<Params>) {
|
export function TrustCenterVendorsCard<Params>(props: Props<Params>) {
|
||||||
const { __ } = useTranslate();
|
const { __ } = useTranslate();
|
||||||
|
const organizationId = useOrganizationId();
|
||||||
const [limit, setLimit] = useState<number | null>(100);
|
const [limit, setLimit] = useState<number | null>(100);
|
||||||
|
const [canUpdate, setCanUpdate] = useState<boolean>(false);
|
||||||
|
|
||||||
const vendors = useMemo(() => {
|
const vendors = useMemo(() => {
|
||||||
return limit ? props.vendors.slice(0, limit) : props.vendors;
|
return limit ? props.vendors.slice(0, limit) : props.vendors;
|
||||||
}, [props.vendors, limit]);
|
}, [props.vendors, limit]);
|
||||||
const showMoreButton = limit !== null && props.vendors.length > 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) => {
|
const onToggleVisibility = (vendorId: string, showOnTrustCenter: boolean) => {
|
||||||
props.onToggleVisibility({
|
props.onToggleVisibility({
|
||||||
variables: {
|
variables: {
|
||||||
@@ -74,13 +107,13 @@ export function TrustCenterVendorsCard<Params>(props: Props<Params>) {
|
|||||||
<Th>{__("Name")}</Th>
|
<Th>{__("Name")}</Th>
|
||||||
<Th>{__("Category")}</Th>
|
<Th>{__("Category")}</Th>
|
||||||
<Th>{__("Visibility")}</Th>
|
<Th>{__("Visibility")}</Th>
|
||||||
<Th></Th>
|
{canUpdate && <Th></Th>}
|
||||||
</Tr>
|
</Tr>
|
||||||
</Thead>
|
</Thead>
|
||||||
<Tbody>
|
<Tbody>
|
||||||
{vendors.length === 0 && (
|
{vendors.length === 0 && (
|
||||||
<Tr>
|
<Tr>
|
||||||
<Td colSpan={4} className="text-center text-txt-secondary">
|
<Td colSpan={canUpdate ? 4 : 3} className="text-center text-txt-secondary">
|
||||||
{__("No vendors available")}
|
{__("No vendors available")}
|
||||||
</Td>
|
</Td>
|
||||||
</Tr>
|
</Tr>
|
||||||
@@ -91,6 +124,7 @@ export function TrustCenterVendorsCard<Params>(props: Props<Params>) {
|
|||||||
vendor={vendor}
|
vendor={vendor}
|
||||||
onToggleVisibility={onToggleVisibility}
|
onToggleVisibility={onToggleVisibility}
|
||||||
disabled={props.disabled}
|
disabled={props.disabled}
|
||||||
|
canUpdate={canUpdate}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
</Tbody>
|
</Tbody>
|
||||||
@@ -113,6 +147,7 @@ function VendorRow(props: {
|
|||||||
vendor: TrustCenterVendorsCardFragment$key;
|
vendor: TrustCenterVendorsCardFragment$key;
|
||||||
onToggleVisibility: (vendorId: string, showOnTrustCenter: boolean) => void;
|
onToggleVisibility: (vendorId: string, showOnTrustCenter: boolean) => void;
|
||||||
disabled?: boolean;
|
disabled?: boolean;
|
||||||
|
canUpdate: boolean;
|
||||||
}) {
|
}) {
|
||||||
const vendor = useFragment(trustCenterVendorFragment, props.vendor);
|
const vendor = useFragment(trustCenterVendorFragment, props.vendor);
|
||||||
const organizationId = useOrganizationId();
|
const organizationId = useOrganizationId();
|
||||||
@@ -135,6 +170,7 @@ function VendorRow(props: {
|
|||||||
{vendor.showOnTrustCenter ? __("Visible") : __("None")}
|
{vendor.showOnTrustCenter ? __("Visible") : __("None")}
|
||||||
</Badge>
|
</Badge>
|
||||||
</Td>
|
</Td>
|
||||||
|
{props.canUpdate && (
|
||||||
<Td noLink width={100} className="text-end">
|
<Td noLink width={100} className="text-end">
|
||||||
<Button
|
<Button
|
||||||
variant="secondary"
|
variant="secondary"
|
||||||
@@ -145,6 +181,7 @@ function VendorRow(props: {
|
|||||||
{vendor.showOnTrustCenter ? __("Hide") : __("Show")}
|
{vendor.showOnTrustCenter ? __("Hide") : __("Show")}
|
||||||
</Button>
|
</Button>
|
||||||
</Td>
|
</Td>
|
||||||
|
)}
|
||||||
</Tr>
|
</Tr>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -47,6 +47,7 @@ import { useToast } from "@probo/ui";
|
|||||||
import { ErrorBoundary } from "react-error-boundary";
|
import { ErrorBoundary } from "react-error-boundary";
|
||||||
import { PageError } from "/components/PageError";
|
import { PageError } from "/components/PageError";
|
||||||
import { buildEndpoint } from "/providers/RelayProviders";
|
import { buildEndpoint } from "/providers/RelayProviders";
|
||||||
|
import { Authorized } from "/permissions";
|
||||||
|
|
||||||
const MainLayoutQuery = graphql`
|
const MainLayoutQuery = graphql`
|
||||||
query MainLayoutQuery($organizationId: ID!) {
|
query MainLayoutQuery($organizationId: ID!) {
|
||||||
@@ -72,7 +73,6 @@ const MainLayoutQuery = graphql`
|
|||||||
*/
|
*/
|
||||||
export function MainLayout() {
|
export function MainLayout() {
|
||||||
const { organizationId } = useParams();
|
const { organizationId } = useParams();
|
||||||
const { __ } = useTranslate();
|
|
||||||
|
|
||||||
const prefix = `/organizations/${organizationId}`;
|
const prefix = `/organizations/${organizationId}`;
|
||||||
|
|
||||||
@@ -80,14 +80,31 @@ export function MainLayout() {
|
|||||||
return <Navigate to="/" />;
|
return <Navigate to="/" />;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Suspense fallback={<Skeleton className="w-full h-screen" />}>
|
||||||
|
<MainLayoutContent organizationId={organizationId} prefix={prefix} />
|
||||||
|
</Suspense>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function MainLayoutContent({
|
||||||
|
organizationId,
|
||||||
|
prefix,
|
||||||
|
}: {
|
||||||
|
organizationId: string;
|
||||||
|
prefix: string;
|
||||||
|
}) {
|
||||||
|
const { __ } = useTranslate();
|
||||||
|
const data = useLazyLoadQuery<MainLayoutQueryType>(MainLayoutQuery, {
|
||||||
|
organizationId,
|
||||||
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Layout
|
<Layout
|
||||||
header={
|
header={
|
||||||
<>
|
<>
|
||||||
<div className="mr-auto">
|
<div className="mr-auto">
|
||||||
<Suspense fallback={<Skeleton className="w-20 h-8" />}>
|
<OrganizationSelector currentOrganization={data.organization} />
|
||||||
<OrganizationSelectorWrapper organizationId={organizationId} />
|
|
||||||
</Suspense>
|
|
||||||
</div>
|
</div>
|
||||||
<Suspense fallback={<Skeleton className="w-32 h-8" />}>
|
<Suspense fallback={<Skeleton className="w-32 h-8" />}>
|
||||||
<UserDropdown organizationId={organizationId} />
|
<UserDropdown organizationId={organizationId} />
|
||||||
@@ -96,96 +113,132 @@ export function MainLayout() {
|
|||||||
}
|
}
|
||||||
sidebar={
|
sidebar={
|
||||||
<ul className="space-y-[2px]">
|
<ul className="space-y-[2px]">
|
||||||
|
<Authorized entity="Organization" action="listMeetings">
|
||||||
<SidebarItem
|
<SidebarItem
|
||||||
label={__("Meetings")}
|
label={__("Meetings")}
|
||||||
icon={IconCalendar1}
|
icon={IconCalendar1}
|
||||||
to={`${prefix}/meetings`}
|
to={`${prefix}/meetings`}
|
||||||
/>
|
/>
|
||||||
|
</Authorized>
|
||||||
|
<Authorized entity="Organization" action="listTasks">
|
||||||
<SidebarItem
|
<SidebarItem
|
||||||
label={__("Tasks")}
|
label={__("Tasks")}
|
||||||
icon={IconInboxEmpty}
|
icon={IconInboxEmpty}
|
||||||
to={`${prefix}/tasks`}
|
to={`${prefix}/tasks`}
|
||||||
/>
|
/>
|
||||||
|
</Authorized>
|
||||||
|
<Authorized entity="Organization" action="listMeasures">
|
||||||
<SidebarItem
|
<SidebarItem
|
||||||
label={__("Measures")}
|
label={__("Measures")}
|
||||||
icon={IconTodo}
|
icon={IconTodo}
|
||||||
to={`${prefix}/measures`}
|
to={`${prefix}/measures`}
|
||||||
/>
|
/>
|
||||||
|
</Authorized>
|
||||||
|
<Authorized entity="Organization" action="listRisks">
|
||||||
<SidebarItem
|
<SidebarItem
|
||||||
label={__("Risks")}
|
label={__("Risks")}
|
||||||
icon={IconFire3}
|
icon={IconFire3}
|
||||||
to={`${prefix}/risks`}
|
to={`${prefix}/risks`}
|
||||||
/>
|
/>
|
||||||
|
</Authorized>
|
||||||
|
<Authorized entity="Organization" action="listFrameworks">
|
||||||
<SidebarItem
|
<SidebarItem
|
||||||
label={__("Frameworks")}
|
label={__("Frameworks")}
|
||||||
icon={IconBank}
|
icon={IconBank}
|
||||||
to={`${prefix}/frameworks`}
|
to={`${prefix}/frameworks`}
|
||||||
/>
|
/>
|
||||||
|
</Authorized>
|
||||||
|
<Authorized entity="Organization" action="listPeople">
|
||||||
<SidebarItem
|
<SidebarItem
|
||||||
label={__("People")}
|
label={__("People")}
|
||||||
icon={IconGroup1}
|
icon={IconGroup1}
|
||||||
to={`${prefix}/people`}
|
to={`${prefix}/people`}
|
||||||
/>
|
/>
|
||||||
|
</Authorized>
|
||||||
|
<Authorized entity="Organization" action="listVendors">
|
||||||
<SidebarItem
|
<SidebarItem
|
||||||
label={__("Vendors")}
|
label={__("Vendors")}
|
||||||
icon={IconStore}
|
icon={IconStore}
|
||||||
to={`${prefix}/vendors`}
|
to={`${prefix}/vendors`}
|
||||||
/>
|
/>
|
||||||
|
</Authorized>
|
||||||
|
<Authorized entity="Organization" action="listDocuments">
|
||||||
<SidebarItem
|
<SidebarItem
|
||||||
label={__("Documents")}
|
label={__("Documents")}
|
||||||
icon={IconPageTextLine}
|
icon={IconPageTextLine}
|
||||||
to={`${prefix}/documents`}
|
to={`${prefix}/documents`}
|
||||||
/>
|
/>
|
||||||
|
</Authorized>
|
||||||
|
<Authorized entity="Organization" action="listAssets">
|
||||||
<SidebarItem
|
<SidebarItem
|
||||||
label={__("Assets")}
|
label={__("Assets")}
|
||||||
icon={IconBox}
|
icon={IconBox}
|
||||||
to={`${prefix}/assets`}
|
to={`${prefix}/assets`}
|
||||||
/>
|
/>
|
||||||
|
</Authorized>
|
||||||
|
<Authorized entity="Organization" action="listData">
|
||||||
<SidebarItem
|
<SidebarItem
|
||||||
label={__("Data")}
|
label={__("Data")}
|
||||||
icon={IconListStack}
|
icon={IconListStack}
|
||||||
to={`${prefix}/data`}
|
to={`${prefix}/data`}
|
||||||
/>
|
/>
|
||||||
|
</Authorized>
|
||||||
|
<Authorized entity="Organization" action="listAudits">
|
||||||
<SidebarItem
|
<SidebarItem
|
||||||
label={__("Audits")}
|
label={__("Audits")}
|
||||||
icon={IconMedal}
|
icon={IconMedal}
|
||||||
to={`${prefix}/audits`}
|
to={`${prefix}/audits`}
|
||||||
/>
|
/>
|
||||||
|
</Authorized>
|
||||||
|
<Authorized entity="Organization" action="listNonconformities">
|
||||||
<SidebarItem
|
<SidebarItem
|
||||||
label={__("Nonconformities")}
|
label={__("Nonconformities")}
|
||||||
icon={IconCrossLargeX}
|
icon={IconCrossLargeX}
|
||||||
to={`${prefix}/nonconformities`}
|
to={`${prefix}/nonconformities`}
|
||||||
/>
|
/>
|
||||||
|
</Authorized>
|
||||||
|
<Authorized entity="Organization" action="listObligations">
|
||||||
<SidebarItem
|
<SidebarItem
|
||||||
label={__("Obligations")}
|
label={__("Obligations")}
|
||||||
icon={IconBook}
|
icon={IconBook}
|
||||||
to={`${prefix}/obligations`}
|
to={`${prefix}/obligations`}
|
||||||
/>
|
/>
|
||||||
|
</Authorized>
|
||||||
|
<Authorized entity="Organization" action="listContinualImprovements">
|
||||||
<SidebarItem
|
<SidebarItem
|
||||||
label={__("Continual Improvements")}
|
label={__("Continual Improvements")}
|
||||||
icon={IconRotateCw}
|
icon={IconRotateCw}
|
||||||
to={`${prefix}/continual-improvements`}
|
to={`${prefix}/continual-improvements`}
|
||||||
/>
|
/>
|
||||||
|
</Authorized>
|
||||||
|
<Authorized entity="Organization" action="listProcessingActivities">
|
||||||
<SidebarItem
|
<SidebarItem
|
||||||
label={__("Processing Activities")}
|
label={__("Processing Activities")}
|
||||||
icon={IconCircleProgress}
|
icon={IconCircleProgress}
|
||||||
to={`${prefix}/processing-activities`}
|
to={`${prefix}/processing-activities`}
|
||||||
/>
|
/>
|
||||||
|
</Authorized>
|
||||||
|
<Authorized entity="Organization" action="listSnapshots">
|
||||||
<SidebarItem
|
<SidebarItem
|
||||||
label={__("Snapshots")}
|
label={__("Snapshots")}
|
||||||
icon={IconClock}
|
icon={IconClock}
|
||||||
to={`${prefix}/snapshots`}
|
to={`${prefix}/snapshots`}
|
||||||
/>
|
/>
|
||||||
|
</Authorized>
|
||||||
|
<Authorized entity="Organization" action="getTrustCenter">
|
||||||
<SidebarItem
|
<SidebarItem
|
||||||
label={__("Trust Center")}
|
label={__("Trust Center")}
|
||||||
icon={IconShield}
|
icon={IconShield}
|
||||||
to={`${prefix}/trust-center`}
|
to={`${prefix}/trust-center`}
|
||||||
/>
|
/>
|
||||||
|
</Authorized>
|
||||||
|
<Authorized entity="Organization" action="listMembers">
|
||||||
<SidebarItem
|
<SidebarItem
|
||||||
label={__("Settings")}
|
label={__("Settings")}
|
||||||
icon={IconSettingsGear2}
|
icon={IconSettingsGear2}
|
||||||
to={`${prefix}/settings`}
|
to={`${prefix}/settings`}
|
||||||
/>
|
/>
|
||||||
|
</Authorized>
|
||||||
</ul>
|
</ul>
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
@@ -234,11 +287,13 @@ function UserDropdown({ organizationId }: { organizationId: string }) {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<UserDropdownRoot fullName={user.fullName} email={user.email}>
|
<UserDropdownRoot fullName={user.fullName} email={user.email}>
|
||||||
|
<Authorized entity="Organization" action="deleteOrganization">
|
||||||
<UserDropdownItem
|
<UserDropdownItem
|
||||||
to="/api-keys"
|
to="/api-keys"
|
||||||
icon={IconKey}
|
icon={IconKey}
|
||||||
label={__("API Keys")}
|
label={__("API Keys")}
|
||||||
/>
|
/>
|
||||||
|
</Authorized>
|
||||||
<UserDropdownItem
|
<UserDropdownItem
|
||||||
to="mailto:support@getprobo.com"
|
to="mailto:support@getprobo.com"
|
||||||
icon={IconCircleQuestionmark}
|
icon={IconCircleQuestionmark}
|
||||||
@@ -287,16 +342,6 @@ interface InvitationsResponse {
|
|||||||
invitations: Invitation[];
|
invitations: Invitation[];
|
||||||
}
|
}
|
||||||
|
|
||||||
function OrganizationSelectorWrapper({
|
|
||||||
organizationId,
|
|
||||||
}: {
|
|
||||||
organizationId: string;
|
|
||||||
}) {
|
|
||||||
const data = useLazyLoadQuery<MainLayoutQueryType>(MainLayoutQuery, {
|
|
||||||
organizationId,
|
|
||||||
});
|
|
||||||
return <OrganizationSelector currentOrganization={data.organization} />;
|
|
||||||
}
|
|
||||||
|
|
||||||
function OrganizationSelector({
|
function OrganizationSelector({
|
||||||
currentOrganization,
|
currentOrganization,
|
||||||
|
|||||||
@@ -120,7 +120,7 @@ export default function APIKeysPage() {
|
|||||||
try {
|
try {
|
||||||
const [apiKeysResponse, organizationsResponse] = await Promise.all([
|
const [apiKeysResponse, organizationsResponse] = await Promise.all([
|
||||||
fetch('/connect/api-keys', { credentials: 'include' }),
|
fetch('/connect/api-keys', { credentials: 'include' }),
|
||||||
fetch('/connect/organizations', { credentials: 'include' }),
|
fetch('/connect/organizations?role=OWNER', { credentials: 'include' }),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
if (apiKeysResponse.status === 401 || organizationsResponse.status === 401) {
|
if (apiKeysResponse.status === 401 || organizationsResponse.status === 401) {
|
||||||
|
|||||||
@@ -28,6 +28,7 @@ import { useFormWithSchema } from "/hooks/useFormWithSchema";
|
|||||||
import z from "zod";
|
import z from "zod";
|
||||||
import { getAssetTypeVariant, validateSnapshotConsistency } from "@probo/helpers";
|
import { getAssetTypeVariant, validateSnapshotConsistency } from "@probo/helpers";
|
||||||
import { SnapshotBanner } from "/components/SnapshotBanner";
|
import { SnapshotBanner } from "/components/SnapshotBanner";
|
||||||
|
import { Authorized } from "/permissions";
|
||||||
|
|
||||||
const updateAssetSchema = z.object({
|
const updateAssetSchema = z.object({
|
||||||
name: z.string().min(1, "Name is required"),
|
name: z.string().min(1, "Name is required"),
|
||||||
@@ -114,6 +115,7 @@ export default function AssetDetailsPage(props: Props) {
|
|||||||
</Badge>
|
</Badge>
|
||||||
</div>
|
</div>
|
||||||
{!isSnapshotMode && (
|
{!isSnapshotMode && (
|
||||||
|
<Authorized entity="Asset" action="deleteAsset">
|
||||||
<ActionDropdown variant="secondary">
|
<ActionDropdown variant="secondary">
|
||||||
<DropdownItem
|
<DropdownItem
|
||||||
variant="danger"
|
variant="danger"
|
||||||
@@ -123,6 +125,7 @@ export default function AssetDetailsPage(props: Props) {
|
|||||||
{__("Delete")}
|
{__("Delete")}
|
||||||
</DropdownItem>
|
</DropdownItem>
|
||||||
</ActionDropdown>
|
</ActionDropdown>
|
||||||
|
</Authorized>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -178,9 +181,11 @@ export default function AssetDetailsPage(props: Props) {
|
|||||||
|
|
||||||
<div className="flex justify-end">
|
<div className="flex justify-end">
|
||||||
{formState.isDirty && !isSnapshotMode && (
|
{formState.isDirty && !isSnapshotMode && (
|
||||||
|
<Authorized entity="Asset" action="updateAsset">
|
||||||
<Button type="submit" disabled={formState.isSubmitting}>
|
<Button type="submit" disabled={formState.isSubmitting}>
|
||||||
{formState.isSubmitting ? __("Updating...") : __("Update")}
|
{formState.isSubmitting ? __("Updating...") : __("Update")}
|
||||||
</Button>
|
</Button>
|
||||||
|
</Authorized>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
|||||||
@@ -35,6 +35,8 @@ import type {
|
|||||||
} from "./__generated__/AssetsPageFragment.graphql";
|
} from "./__generated__/AssetsPageFragment.graphql";
|
||||||
import { SortableTable } from "/components/SortableTable";
|
import { SortableTable } from "/components/SortableTable";
|
||||||
import { SnapshotBanner } from "/components/SnapshotBanner";
|
import { SnapshotBanner } from "/components/SnapshotBanner";
|
||||||
|
import { Authorized } from "/permissions";
|
||||||
|
import { isAuthorized } from "/permissions";
|
||||||
|
|
||||||
const paginatedAssetsFragment = graphql`
|
const paginatedAssetsFragment = graphql`
|
||||||
fragment AssetsPageFragment on Organization
|
fragment AssetsPageFragment on Organization
|
||||||
@@ -106,6 +108,11 @@ export default function AssetsPage(props: Props) {
|
|||||||
|
|
||||||
usePageTitle(__("Assets"));
|
usePageTitle(__("Assets"));
|
||||||
|
|
||||||
|
const hasAnyAction = !isSnapshotMode && (
|
||||||
|
isAuthorized(organizationId, "Asset", "updateAsset") ||
|
||||||
|
isAuthorized(organizationId, "Asset", "deleteAsset")
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-6">
|
<div className="space-y-6">
|
||||||
{snapshotId && <SnapshotBanner snapshotId={snapshotId} />}
|
{snapshotId && <SnapshotBanner snapshotId={snapshotId} />}
|
||||||
@@ -116,12 +123,14 @@ export default function AssetsPage(props: Props) {
|
|||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{!isSnapshotMode && (
|
{!isSnapshotMode && (
|
||||||
|
<Authorized entity="Organization" action="createAsset">
|
||||||
<CreateAssetDialog
|
<CreateAssetDialog
|
||||||
connection={connectionId}
|
connection={connectionId}
|
||||||
organizationId={organizationId}
|
organizationId={organizationId}
|
||||||
>
|
>
|
||||||
<Button icon={IconPlusLarge}>{__("Add asset")}</Button>
|
<Button icon={IconPlusLarge}>{__("Add asset")}</Button>
|
||||||
</CreateAssetDialog>
|
</CreateAssetDialog>
|
||||||
|
</Authorized>
|
||||||
)}
|
)}
|
||||||
</PageHeader>
|
</PageHeader>
|
||||||
<SortableTable {...pagination}>
|
<SortableTable {...pagination}>
|
||||||
@@ -132,7 +141,7 @@ export default function AssetsPage(props: Props) {
|
|||||||
<Th>{__("Amount")}</Th>
|
<Th>{__("Amount")}</Th>
|
||||||
<Th>{__("Owner")}</Th>
|
<Th>{__("Owner")}</Th>
|
||||||
<Th>{__("Vendors")}</Th>
|
<Th>{__("Vendors")}</Th>
|
||||||
<Th></Th>
|
{hasAnyAction && <Th></Th>}
|
||||||
</Tr>
|
</Tr>
|
||||||
</Thead>
|
</Thead>
|
||||||
<Tbody>
|
<Tbody>
|
||||||
@@ -141,6 +150,7 @@ export default function AssetsPage(props: Props) {
|
|||||||
key={entry.id}
|
key={entry.id}
|
||||||
entry={entry}
|
entry={entry}
|
||||||
connectionId={connectionId}
|
connectionId={connectionId}
|
||||||
|
hasAnyAction={hasAnyAction}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
</Tbody>
|
</Tbody>
|
||||||
@@ -152,9 +162,11 @@ export default function AssetsPage(props: Props) {
|
|||||||
function AssetRow({
|
function AssetRow({
|
||||||
entry,
|
entry,
|
||||||
connectionId,
|
connectionId,
|
||||||
|
hasAnyAction,
|
||||||
}: {
|
}: {
|
||||||
entry: AssetEntry;
|
entry: AssetEntry;
|
||||||
connectionId: string;
|
connectionId: string;
|
||||||
|
hasAnyAction: boolean;
|
||||||
}) {
|
}) {
|
||||||
const organizationId = useOrganizationId();
|
const organizationId = useOrganizationId();
|
||||||
const { __ } = useTranslate();
|
const { __ } = useTranslate();
|
||||||
@@ -204,8 +216,9 @@ function AssetRow({
|
|||||||
<span className="text-txt-secondary text-sm">{__("None")}</span>
|
<span className="text-txt-secondary text-sm">{__("None")}</span>
|
||||||
)}
|
)}
|
||||||
</Td>
|
</Td>
|
||||||
|
{hasAnyAction && (
|
||||||
<Td noLink width={50} className="text-end">
|
<Td noLink width={50} className="text-end">
|
||||||
{!isSnapshotMode && (
|
<Authorized entity="Asset" action="deleteAsset">
|
||||||
<ActionDropdown>
|
<ActionDropdown>
|
||||||
<DropdownItem
|
<DropdownItem
|
||||||
onClick={deleteAsset}
|
onClick={deleteAsset}
|
||||||
@@ -215,8 +228,9 @@ function AssetRow({
|
|||||||
{__("Delete")}
|
{__("Delete")}
|
||||||
</DropdownItem>
|
</DropdownItem>
|
||||||
</ActionDropdown>
|
</ActionDropdown>
|
||||||
)}
|
</Authorized>
|
||||||
</Td>
|
</Td>
|
||||||
|
)}
|
||||||
</Tr>
|
</Tr>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -34,6 +34,7 @@ import { useFormWithSchema } from "/hooks/useFormWithSchema";
|
|||||||
import z from "zod";
|
import z from "zod";
|
||||||
import { getAuditStateLabel, getAuditStateVariant, auditStates, fileSize, sprintf, formatDatetime, formatError, formatDate, type GraphQLError } from "@probo/helpers";
|
import { getAuditStateLabel, getAuditStateVariant, auditStates, fileSize, sprintf, formatDatetime, formatError, formatDate, type GraphQLError } from "@probo/helpers";
|
||||||
import type { AuditGraphNodeQuery } from "/hooks/graph/__generated__/AuditGraphNodeQuery.graphql";
|
import type { AuditGraphNodeQuery } from "/hooks/graph/__generated__/AuditGraphNodeQuery.graphql";
|
||||||
|
import { Authorized } from "/permissions";
|
||||||
|
|
||||||
const updateAuditSchema = z.object({
|
const updateAuditSchema = z.object({
|
||||||
name: z.string().nullable().optional(),
|
name: z.string().nullable().optional(),
|
||||||
@@ -145,6 +146,7 @@ export default function AuditDetailsPage(props: Props) {
|
|||||||
</Badge>
|
</Badge>
|
||||||
</div>
|
</div>
|
||||||
<ActionDropdown variant="secondary">
|
<ActionDropdown variant="secondary">
|
||||||
|
<Authorized entity="Audit" action="deleteAudit">
|
||||||
<DropdownItem
|
<DropdownItem
|
||||||
variant="danger"
|
variant="danger"
|
||||||
icon={IconTrashCan}
|
icon={IconTrashCan}
|
||||||
@@ -152,6 +154,7 @@ export default function AuditDetailsPage(props: Props) {
|
|||||||
>
|
>
|
||||||
{__("Delete")}
|
{__("Delete")}
|
||||||
</DropdownItem>
|
</DropdownItem>
|
||||||
|
</Authorized>
|
||||||
</ActionDropdown>
|
</ActionDropdown>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -184,9 +187,11 @@ export default function AuditDetailsPage(props: Props) {
|
|||||||
|
|
||||||
<div className="flex justify-end">
|
<div className="flex justify-end">
|
||||||
{formState.isDirty && (
|
{formState.isDirty && (
|
||||||
|
<Authorized entity="Audit" action="updateAudit">
|
||||||
<Button type="submit" disabled={formState.isSubmitting}>
|
<Button type="submit" disabled={formState.isSubmitting}>
|
||||||
{formState.isSubmitting ? __("Updating...") : __("Update")}
|
{formState.isSubmitting ? __("Updating...") : __("Update")}
|
||||||
</Button>
|
</Button>
|
||||||
|
</Authorized>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
|||||||
@@ -31,6 +31,8 @@ import type {
|
|||||||
AuditsPageFragment$key,
|
AuditsPageFragment$key,
|
||||||
} from "./__generated__/AuditsPageFragment.graphql";
|
} from "./__generated__/AuditsPageFragment.graphql";
|
||||||
import { SortableTable } from "/components/SortableTable";
|
import { SortableTable } from "/components/SortableTable";
|
||||||
|
import { Authorized } from "/permissions";
|
||||||
|
import { isAuthorized } from "/permissions";
|
||||||
|
|
||||||
const paginatedAuditsFragment = graphql`
|
const paginatedAuditsFragment = graphql`
|
||||||
fragment AuditsPageFragment on Organization
|
fragment AuditsPageFragment on Organization
|
||||||
@@ -92,6 +94,9 @@ export default function AuditsPage(props: Props) {
|
|||||||
|
|
||||||
usePageTitle(__("Audits"));
|
usePageTitle(__("Audits"));
|
||||||
|
|
||||||
|
const hasAnyAction = isAuthorized(organizationId, "Audit", "updateAudit") ||
|
||||||
|
isAuthorized(organizationId, "Audit", "deleteAudit");
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-6">
|
<div className="space-y-6">
|
||||||
<PageHeader
|
<PageHeader
|
||||||
@@ -100,12 +105,14 @@ export default function AuditsPage(props: Props) {
|
|||||||
"Manage your organization's compliance audits and their progress."
|
"Manage your organization's compliance audits and their progress."
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
|
<Authorized entity="Organization" action="createAudit">
|
||||||
<CreateAuditDialog
|
<CreateAuditDialog
|
||||||
connection={connectionId}
|
connection={connectionId}
|
||||||
organizationId={organizationId}
|
organizationId={organizationId}
|
||||||
>
|
>
|
||||||
<Button icon={IconPlusLarge}>{__("Add audit")}</Button>
|
<Button icon={IconPlusLarge}>{__("Add audit")}</Button>
|
||||||
</CreateAuditDialog>
|
</CreateAuditDialog>
|
||||||
|
</Authorized>
|
||||||
</PageHeader>
|
</PageHeader>
|
||||||
<SortableTable {...pagination}>
|
<SortableTable {...pagination}>
|
||||||
<Thead>
|
<Thead>
|
||||||
@@ -116,7 +123,7 @@ export default function AuditsPage(props: Props) {
|
|||||||
<Th>{__("Valid From")}</Th>
|
<Th>{__("Valid From")}</Th>
|
||||||
<Th>{__("Valid Until")}</Th>
|
<Th>{__("Valid Until")}</Th>
|
||||||
<Th>{__("Report")}</Th>
|
<Th>{__("Report")}</Th>
|
||||||
<Th></Th>
|
{hasAnyAction && <Th></Th>}
|
||||||
</Tr>
|
</Tr>
|
||||||
</Thead>
|
</Thead>
|
||||||
<Tbody>
|
<Tbody>
|
||||||
@@ -125,6 +132,7 @@ export default function AuditsPage(props: Props) {
|
|||||||
key={entry.id}
|
key={entry.id}
|
||||||
entry={entry}
|
entry={entry}
|
||||||
connectionId={connectionId}
|
connectionId={connectionId}
|
||||||
|
hasAnyAction={hasAnyAction}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
</Tbody>
|
</Tbody>
|
||||||
@@ -136,9 +144,11 @@ export default function AuditsPage(props: Props) {
|
|||||||
function AuditRow({
|
function AuditRow({
|
||||||
entry,
|
entry,
|
||||||
connectionId,
|
connectionId,
|
||||||
|
hasAnyAction,
|
||||||
}: {
|
}: {
|
||||||
entry: AuditEntry;
|
entry: AuditEntry;
|
||||||
connectionId: string;
|
connectionId: string;
|
||||||
|
hasAnyAction: boolean;
|
||||||
}) {
|
}) {
|
||||||
const organizationId = useOrganizationId();
|
const organizationId = useOrganizationId();
|
||||||
const { __ } = useTranslate();
|
const { __ } = useTranslate();
|
||||||
@@ -164,8 +174,10 @@ function AuditRow({
|
|||||||
<Badge variant="neutral">{__("Not uploaded")}</Badge>
|
<Badge variant="neutral">{__("Not uploaded")}</Badge>
|
||||||
)}
|
)}
|
||||||
</Td>
|
</Td>
|
||||||
|
{hasAnyAction && (
|
||||||
<Td noLink width={50} className="text-end">
|
<Td noLink width={50} className="text-end">
|
||||||
<ActionDropdown>
|
<ActionDropdown>
|
||||||
|
<Authorized entity="Audit" action="deleteAudit">
|
||||||
<DropdownItem
|
<DropdownItem
|
||||||
onClick={deleteAudit}
|
onClick={deleteAudit}
|
||||||
variant="danger"
|
variant="danger"
|
||||||
@@ -173,8 +185,10 @@ function AuditRow({
|
|||||||
>
|
>
|
||||||
{__("Delete")}
|
{__("Delete")}
|
||||||
</DropdownItem>
|
</DropdownItem>
|
||||||
|
</Authorized>
|
||||||
</ActionDropdown>
|
</ActionDropdown>
|
||||||
</Td>
|
</Td>
|
||||||
|
)}
|
||||||
</Tr>
|
</Tr>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -35,6 +35,7 @@ import z from "zod";
|
|||||||
import { getStatusVariant, getStatusLabel, formatDatetime, validateSnapshotConsistency } from "@probo/helpers";
|
import { getStatusVariant, getStatusLabel, formatDatetime, validateSnapshotConsistency } from "@probo/helpers";
|
||||||
import { SnapshotBanner } from "/components/SnapshotBanner";
|
import { SnapshotBanner } from "/components/SnapshotBanner";
|
||||||
import type { ContinualImprovementGraphNodeQuery } from "/hooks/graph/__generated__/ContinualImprovementGraphNodeQuery.graphql";
|
import type { ContinualImprovementGraphNodeQuery } from "/hooks/graph/__generated__/ContinualImprovementGraphNodeQuery.graphql";
|
||||||
|
import { Authorized } from "/permissions";
|
||||||
|
|
||||||
const updateImprovementSchema = z.object({
|
const updateImprovementSchema = z.object({
|
||||||
referenceId: z.string().min(1, "Reference ID is required"),
|
referenceId: z.string().min(1, "Reference ID is required"),
|
||||||
@@ -149,11 +150,13 @@ export default function ContinualImprovementDetailsPage(props: Props) {
|
|||||||
]}
|
]}
|
||||||
/>
|
/>
|
||||||
{!isSnapshotMode && (
|
{!isSnapshotMode && (
|
||||||
|
<Authorized entity="ContinualImprovement" action="deleteContinualImprovement">
|
||||||
<ActionDropdown>
|
<ActionDropdown>
|
||||||
<DropdownItem onClick={deleteImprovement} variant="danger">
|
<DropdownItem onClick={deleteImprovement} variant="danger">
|
||||||
{__("Delete")}
|
{__("Delete")}
|
||||||
</DropdownItem>
|
</DropdownItem>
|
||||||
</ActionDropdown>
|
</ActionDropdown>
|
||||||
|
</Authorized>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -286,6 +289,7 @@ export default function ContinualImprovementDetailsPage(props: Props) {
|
|||||||
|
|
||||||
{!isSnapshotMode && (
|
{!isSnapshotMode && (
|
||||||
<div className="flex justify-end pt-4">
|
<div className="flex justify-end pt-4">
|
||||||
|
<Authorized entity="ContinualImprovement" action="updateContinualImprovement">
|
||||||
<Button
|
<Button
|
||||||
type="submit"
|
type="submit"
|
||||||
variant="primary"
|
variant="primary"
|
||||||
@@ -293,6 +297,7 @@ export default function ContinualImprovementDetailsPage(props: Props) {
|
|||||||
>
|
>
|
||||||
{formState.isSubmitting ? __("Saving...") : __("Save Changes")}
|
{formState.isSubmitting ? __("Saving...") : __("Save Changes")}
|
||||||
</Button>
|
</Button>
|
||||||
|
</Authorized>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</form>
|
</form>
|
||||||
|
|||||||
@@ -37,6 +37,8 @@ import type {
|
|||||||
ContinualImprovementsPageFragment$key,
|
ContinualImprovementsPageFragment$key,
|
||||||
ContinualImprovementsPageFragment$data,
|
ContinualImprovementsPageFragment$data,
|
||||||
} from "./__generated__/ContinualImprovementsPageFragment.graphql";
|
} from "./__generated__/ContinualImprovementsPageFragment.graphql";
|
||||||
|
import { Authorized } from "/permissions";
|
||||||
|
import { isAuthorized } from "/permissions";
|
||||||
|
|
||||||
interface ContinualImprovementsPageProps {
|
interface ContinualImprovementsPageProps {
|
||||||
queryRef: PreloadedQuery<ContinualImprovementsPageQuery>;
|
queryRef: PreloadedQuery<ContinualImprovementsPageQuery>;
|
||||||
@@ -127,6 +129,11 @@ export default function ContinualImprovementsPage({ queryRef }: ContinualImprove
|
|||||||
);
|
);
|
||||||
const improvements = data?.continualImprovements?.edges?.map((edge) => edge.node) ?? [];
|
const improvements = data?.continualImprovements?.edges?.map((edge) => edge.node) ?? [];
|
||||||
|
|
||||||
|
const hasAnyAction = !isSnapshotMode && (
|
||||||
|
isAuthorized(organizationId, "ContinualImprovement", "updateContinualImprovement") ||
|
||||||
|
isAuthorized(organizationId, "ContinualImprovement", "deleteContinualImprovement")
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-6">
|
<div className="space-y-6">
|
||||||
{isSnapshotMode && snapshotId && (
|
{isSnapshotMode && snapshotId && (
|
||||||
@@ -134,6 +141,7 @@ export default function ContinualImprovementsPage({ queryRef }: ContinualImprove
|
|||||||
)}
|
)}
|
||||||
<PageHeader title={__("Continual Improvements")} description={__("Manage your continual improvements.")}>
|
<PageHeader title={__("Continual Improvements")} description={__("Manage your continual improvements.")}>
|
||||||
{!isSnapshotMode && (
|
{!isSnapshotMode && (
|
||||||
|
<Authorized entity="Organization" action="createContinualImprovement">
|
||||||
<CreateContinualImprovementDialog
|
<CreateContinualImprovementDialog
|
||||||
organizationId={organizationId}
|
organizationId={organizationId}
|
||||||
connectionId={connectionId}
|
connectionId={connectionId}
|
||||||
@@ -142,6 +150,7 @@ export default function ContinualImprovementsPage({ queryRef }: ContinualImprove
|
|||||||
{__("Add continual improvement")}
|
{__("Add continual improvement")}
|
||||||
</Button>
|
</Button>
|
||||||
</CreateContinualImprovementDialog>
|
</CreateContinualImprovementDialog>
|
||||||
|
</Authorized>
|
||||||
)}
|
)}
|
||||||
</PageHeader>
|
</PageHeader>
|
||||||
|
|
||||||
@@ -156,7 +165,7 @@ export default function ContinualImprovementsPage({ queryRef }: ContinualImprove
|
|||||||
<Th>{__("Priority")}</Th>
|
<Th>{__("Priority")}</Th>
|
||||||
<Th>{__("Owner")}</Th>
|
<Th>{__("Owner")}</Th>
|
||||||
<Th>{__("Target Date")}</Th>
|
<Th>{__("Target Date")}</Th>
|
||||||
{!isSnapshotMode && <Th>{__("Actions")}</Th>}
|
{hasAnyAction && <Th>{__("Actions")}</Th>}
|
||||||
</Tr>
|
</Tr>
|
||||||
</Thead>
|
</Thead>
|
||||||
<Tbody>
|
<Tbody>
|
||||||
@@ -166,6 +175,7 @@ export default function ContinualImprovementsPage({ queryRef }: ContinualImprove
|
|||||||
improvement={improvement}
|
improvement={improvement}
|
||||||
connectionId={connectionId}
|
connectionId={connectionId}
|
||||||
snapshotId={snapshotId}
|
snapshotId={snapshotId}
|
||||||
|
hasAnyAction={hasAnyAction}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
</Tbody>
|
</Tbody>
|
||||||
@@ -203,10 +213,12 @@ function ImprovementRow({
|
|||||||
improvement,
|
improvement,
|
||||||
connectionId,
|
connectionId,
|
||||||
snapshotId,
|
snapshotId,
|
||||||
|
hasAnyAction,
|
||||||
}: {
|
}: {
|
||||||
improvement: NodeOf<NonNullable<ContinualImprovementsPageFragment$data['continualImprovements']>>;
|
improvement: NodeOf<NonNullable<ContinualImprovementsPageFragment$data['continualImprovements']>>;
|
||||||
connectionId: string;
|
connectionId: string;
|
||||||
snapshotId?: string;
|
snapshotId?: string;
|
||||||
|
hasAnyAction: boolean;
|
||||||
}) {
|
}) {
|
||||||
const organizationId = useOrganizationId();
|
const organizationId = useOrganizationId();
|
||||||
const { __ } = useTranslate();
|
const { __ } = useTranslate();
|
||||||
@@ -267,9 +279,10 @@ function ImprovementRow({
|
|||||||
<span className="text-txt-tertiary">{__("No target date")}</span>
|
<span className="text-txt-tertiary">{__("No target date")}</span>
|
||||||
)}
|
)}
|
||||||
</Td>
|
</Td>
|
||||||
{!isSnapshotMode && (
|
{hasAnyAction && (
|
||||||
<Td noLink width={50} className="text-end">
|
<Td noLink width={50} className="text-end">
|
||||||
<ActionDropdown>
|
<ActionDropdown>
|
||||||
|
<Authorized entity="ContinualImprovement" action="deleteContinualImprovement">
|
||||||
<DropdownItem
|
<DropdownItem
|
||||||
icon={IconTrashCan}
|
icon={IconTrashCan}
|
||||||
variant="danger"
|
variant="danger"
|
||||||
@@ -277,6 +290,7 @@ function ImprovementRow({
|
|||||||
>
|
>
|
||||||
{__("Delete")}
|
{__("Delete")}
|
||||||
</DropdownItem>
|
</DropdownItem>
|
||||||
|
</Authorized>
|
||||||
</ActionDropdown>
|
</ActionDropdown>
|
||||||
</Td>
|
</Td>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -35,6 +35,8 @@ import type {
|
|||||||
import type { DataListQuery } from "./__generated__/DataListQuery.graphql";
|
import type { DataListQuery } from "./__generated__/DataListQuery.graphql";
|
||||||
import { SortableTable } from "/components/SortableTable";
|
import { SortableTable } from "/components/SortableTable";
|
||||||
import { SnapshotBanner } from "/components/SnapshotBanner";
|
import { SnapshotBanner } from "/components/SnapshotBanner";
|
||||||
|
import { Authorized } from "/permissions";
|
||||||
|
import { isAuthorized } from "/permissions";
|
||||||
|
|
||||||
const paginatedDataFragment = graphql`
|
const paginatedDataFragment = graphql`
|
||||||
fragment DataPageFragment on Organization
|
fragment DataPageFragment on Organization
|
||||||
@@ -119,6 +121,10 @@ export default function DataPage(props: Props) {
|
|||||||
|
|
||||||
usePageTitle(__("Data"));
|
usePageTitle(__("Data"));
|
||||||
|
|
||||||
|
const hasAnyAction = !isSnapshotMode && ( isAuthorized(organizationId, "Datum", "updateDatum") ||
|
||||||
|
isAuthorized(organizationId, "Datum", "deleteDatum")
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-6">
|
<div className="space-y-6">
|
||||||
{isSnapshotMode && snapshotId && (
|
{isSnapshotMode && snapshotId && (
|
||||||
@@ -131,6 +137,7 @@ export default function DataPage(props: Props) {
|
|||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{!snapshotId && (
|
{!snapshotId && (
|
||||||
|
<Authorized entity="Organization" action="createDatum">
|
||||||
<CreateDatumDialog
|
<CreateDatumDialog
|
||||||
connection={connectionId}
|
connection={connectionId}
|
||||||
organizationId={organizationId}
|
organizationId={organizationId}
|
||||||
@@ -138,6 +145,7 @@ export default function DataPage(props: Props) {
|
|||||||
>
|
>
|
||||||
<Button icon={IconPlusLarge}>{__("Add data")}</Button>
|
<Button icon={IconPlusLarge}>{__("Add data")}</Button>
|
||||||
</CreateDatumDialog>
|
</CreateDatumDialog>
|
||||||
|
</Authorized>
|
||||||
)}
|
)}
|
||||||
</PageHeader>
|
</PageHeader>
|
||||||
<SortableTable
|
<SortableTable
|
||||||
@@ -150,12 +158,12 @@ export default function DataPage(props: Props) {
|
|||||||
<Th>{__("Classification")}</Th>
|
<Th>{__("Classification")}</Th>
|
||||||
<Th>{__("Owner")}</Th>
|
<Th>{__("Owner")}</Th>
|
||||||
<Th>{__("Vendors")}</Th>
|
<Th>{__("Vendors")}</Th>
|
||||||
<Th></Th>
|
{hasAnyAction && <Th></Th>}
|
||||||
</Tr>
|
</Tr>
|
||||||
</Thead>
|
</Thead>
|
||||||
<Tbody>
|
<Tbody>
|
||||||
{dataEntries.map((entry) => (
|
{dataEntries.map((entry) => (
|
||||||
<DataRow key={entry.id} entry={entry} connectionId={connectionId} snapshotId={snapshotId} />
|
<DataRow key={entry.id} entry={entry} connectionId={connectionId} snapshotId={snapshotId} hasAnyAction={hasAnyAction} />
|
||||||
))}
|
))}
|
||||||
</Tbody>
|
</Tbody>
|
||||||
</SortableTable>
|
</SortableTable>
|
||||||
@@ -167,10 +175,12 @@ function DataRow({
|
|||||||
entry,
|
entry,
|
||||||
connectionId,
|
connectionId,
|
||||||
snapshotId,
|
snapshotId,
|
||||||
|
hasAnyAction,
|
||||||
}: {
|
}: {
|
||||||
entry: DataEntry;
|
entry: DataEntry;
|
||||||
connectionId: string;
|
connectionId: string;
|
||||||
snapshotId?: string;
|
snapshotId?: string;
|
||||||
|
hasAnyAction: boolean;
|
||||||
}) {
|
}) {
|
||||||
const organizationId = useOrganizationId();
|
const organizationId = useOrganizationId();
|
||||||
const { __ } = useTranslate();
|
const { __ } = useTranslate();
|
||||||
@@ -215,8 +225,10 @@ function DataRow({
|
|||||||
<span className="text-txt-secondary text-sm">{__("None")}</span>
|
<span className="text-txt-secondary text-sm">{__("None")}</span>
|
||||||
)}
|
)}
|
||||||
</Td>
|
</Td>
|
||||||
|
{hasAnyAction && (
|
||||||
<Td noLink width={50} className="text-end">
|
<Td noLink width={50} className="text-end">
|
||||||
{!snapshotId && (<ActionDropdown>
|
<ActionDropdown>
|
||||||
|
<Authorized entity="Datum" action="deleteDatum">
|
||||||
<DropdownItem
|
<DropdownItem
|
||||||
onClick={deleteDatum}
|
onClick={deleteDatum}
|
||||||
variant="danger"
|
variant="danger"
|
||||||
@@ -224,9 +236,10 @@ function DataRow({
|
|||||||
>
|
>
|
||||||
{__("Delete")}
|
{__("Delete")}
|
||||||
</DropdownItem>
|
</DropdownItem>
|
||||||
|
</Authorized>
|
||||||
</ActionDropdown>
|
</ActionDropdown>
|
||||||
)}
|
|
||||||
</Td>
|
</Td>
|
||||||
|
)}
|
||||||
</Tr>
|
</Tr>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -29,6 +29,7 @@ import z from "zod";
|
|||||||
import { SnapshotBanner } from "/components/SnapshotBanner";
|
import { SnapshotBanner } from "/components/SnapshotBanner";
|
||||||
import { validateSnapshotConsistency } from "@probo/helpers";
|
import { validateSnapshotConsistency } from "@probo/helpers";
|
||||||
import type { DatumGraphNodeQuery } from "/hooks/graph/__generated__/DatumGraphNodeQuery.graphql";
|
import type { DatumGraphNodeQuery } from "/hooks/graph/__generated__/DatumGraphNodeQuery.graphql";
|
||||||
|
import { Authorized } from "/permissions";
|
||||||
|
|
||||||
const updateDatumSchema = z.object({
|
const updateDatumSchema = z.object({
|
||||||
name: z.string().min(1, "Name is required"),
|
name: z.string().min(1, "Name is required"),
|
||||||
@@ -124,6 +125,7 @@ export default function DatumDetailsPage(props: Props) {
|
|||||||
<Badge variant="info">{datumEntry?.dataClassification}</Badge>
|
<Badge variant="info">{datumEntry?.dataClassification}</Badge>
|
||||||
</div>
|
</div>
|
||||||
{!isSnapshotMode && (
|
{!isSnapshotMode && (
|
||||||
|
<Authorized entity="Datum" action="deleteDatum">
|
||||||
<ActionDropdown variant="secondary">
|
<ActionDropdown variant="secondary">
|
||||||
<DropdownItem
|
<DropdownItem
|
||||||
variant="danger"
|
variant="danger"
|
||||||
@@ -133,6 +135,7 @@ export default function DatumDetailsPage(props: Props) {
|
|||||||
{__("Delete")}
|
{__("Delete")}
|
||||||
</DropdownItem>
|
</DropdownItem>
|
||||||
</ActionDropdown>
|
</ActionDropdown>
|
||||||
|
</Authorized>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -177,9 +180,11 @@ export default function DatumDetailsPage(props: Props) {
|
|||||||
{!isSnapshotMode && (
|
{!isSnapshotMode && (
|
||||||
<div className="flex justify-end">
|
<div className="flex justify-end">
|
||||||
{formState.isDirty && (
|
{formState.isDirty && (
|
||||||
|
<Authorized entity="Datum" action="updateDatum">
|
||||||
<Button type="submit" disabled={formState.isSubmitting}>
|
<Button type="submit" disabled={formState.isSubmitting}>
|
||||||
{formState.isSubmitting ? __("Updating...") : __("Update")}
|
{formState.isSubmitting ? __("Updating...") : __("Update")}
|
||||||
</Button>
|
</Button>
|
||||||
|
</Authorized>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -76,6 +76,7 @@ import { DocumentTypeOptions } from "/components/form/DocumentTypeOptions";
|
|||||||
import { DocumentClassificationOptions } from "/components/form/DocumentClassificationOptions";
|
import { DocumentClassificationOptions } from "/components/form/DocumentClassificationOptions";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import { useFormWithSchema } from "/hooks/useFormWithSchema";
|
import { useFormWithSchema } from "/hooks/useFormWithSchema";
|
||||||
|
import { Authorized } from "/permissions";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
queryRef: PreloadedQuery<DocumentGraphNodeQuery>;
|
queryRef: PreloadedQuery<DocumentGraphNodeQuery>;
|
||||||
@@ -521,13 +522,16 @@ export default function DocumentDetailPage(props: Props) {
|
|||||||
</Dropdown>
|
</Dropdown>
|
||||||
|
|
||||||
<ActionDropdown variant="secondary">
|
<ActionDropdown variant="secondary">
|
||||||
|
<Authorized entity="Document" action="updateDocument">
|
||||||
<DropdownItem
|
<DropdownItem
|
||||||
onClick={() => updateDialogRef.current?.open()}
|
onClick={() => updateDialogRef.current?.open()}
|
||||||
icon={IconPencil}
|
icon={IconPencil}
|
||||||
>
|
>
|
||||||
{isDraft ? __("Edit draft document") : __("Create new draft")}
|
{isDraft ? __("Edit draft document") : __("Create new draft")}
|
||||||
</DropdownItem>
|
</DropdownItem>
|
||||||
|
</Authorized>
|
||||||
{isDraft && versions.length > 1 && (
|
{isDraft && versions.length > 1 && (
|
||||||
|
<Authorized entity="Document" action="deleteDocument">
|
||||||
<DropdownItem
|
<DropdownItem
|
||||||
onClick={handleDeleteDraft}
|
onClick={handleDeleteDraft}
|
||||||
icon={IconTrashCan}
|
icon={IconTrashCan}
|
||||||
@@ -535,6 +539,7 @@ export default function DocumentDetailPage(props: Props) {
|
|||||||
>
|
>
|
||||||
{__("Delete draft document")}
|
{__("Delete draft document")}
|
||||||
</DropdownItem>
|
</DropdownItem>
|
||||||
|
</Authorized>
|
||||||
)}
|
)}
|
||||||
<DropdownItem
|
<DropdownItem
|
||||||
onClick={() => pdfDownloadDialogRef.current?.open()}
|
onClick={() => pdfDownloadDialogRef.current?.open()}
|
||||||
@@ -543,6 +548,7 @@ export default function DocumentDetailPage(props: Props) {
|
|||||||
>
|
>
|
||||||
{__("Download PDF")}
|
{__("Download PDF")}
|
||||||
</DropdownItem>
|
</DropdownItem>
|
||||||
|
<Authorized entity="Document" action="deleteDocument">
|
||||||
<DropdownItem
|
<DropdownItem
|
||||||
variant="danger"
|
variant="danger"
|
||||||
icon={IconTrashCan}
|
icon={IconTrashCan}
|
||||||
@@ -551,6 +557,7 @@ export default function DocumentDetailPage(props: Props) {
|
|||||||
>
|
>
|
||||||
{__("Delete document")}
|
{__("Delete document")}
|
||||||
</DropdownItem>
|
</DropdownItem>
|
||||||
|
</Authorized>
|
||||||
</ActionDropdown>
|
</ActionDropdown>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -57,6 +57,8 @@ import {
|
|||||||
type BulkExportDialogRef,
|
type BulkExportDialogRef,
|
||||||
} from "/components/documents/BulkExportDialog";
|
} from "/components/documents/BulkExportDialog";
|
||||||
import type { DocumentsPageUserEmailQuery } from "./__generated__/DocumentsPageUserEmailQuery.graphql";
|
import type { DocumentsPageUserEmailQuery } from "./__generated__/DocumentsPageUserEmailQuery.graphql";
|
||||||
|
import { Authorized } from "/permissions";
|
||||||
|
import { isAuthorized } from "/permissions";
|
||||||
|
|
||||||
const documentsFragment = graphql`
|
const documentsFragment = graphql`
|
||||||
fragment DocumentsPageListFragment on Organization
|
fragment DocumentsPageListFragment on Organization
|
||||||
@@ -135,6 +137,9 @@ export default function DocumentsPage(props: Props) {
|
|||||||
|
|
||||||
usePageTitle(__("Documents"));
|
usePageTitle(__("Documents"));
|
||||||
|
|
||||||
|
const hasAnyAction = isAuthorized(organization.id, "Document", "updateDocument") ||
|
||||||
|
isAuthorized(organization.id, "Document", "deleteDocument");
|
||||||
|
|
||||||
const handleSendSigningNotifications = () => {
|
const handleSendSigningNotifications = () => {
|
||||||
sendSigningNotifications({
|
sendSigningNotifications({
|
||||||
variables: {
|
variables: {
|
||||||
@@ -193,6 +198,7 @@ export default function DocumentsPage(props: Props) {
|
|||||||
description={__("Manage your organization's documents")}
|
description={__("Manage your organization's documents")}
|
||||||
>
|
>
|
||||||
<div className="flex gap-2">
|
<div className="flex gap-2">
|
||||||
|
<Authorized entity="Document" action="sendSigningNotifications">
|
||||||
<Button
|
<Button
|
||||||
icon={IconBell2}
|
icon={IconBell2}
|
||||||
variant="secondary"
|
variant="secondary"
|
||||||
@@ -200,10 +206,13 @@ export default function DocumentsPage(props: Props) {
|
|||||||
>
|
>
|
||||||
{__("Send signing notifications")}
|
{__("Send signing notifications")}
|
||||||
</Button>
|
</Button>
|
||||||
|
</Authorized>
|
||||||
|
<Authorized entity="Organization" action="createDocument">
|
||||||
<CreateDocumentDialog
|
<CreateDocumentDialog
|
||||||
connection={connectionId}
|
connection={connectionId}
|
||||||
trigger={<Button icon={IconPlusLarge}>{__("New document")}</Button>}
|
trigger={<Button icon={IconPlusLarge}>{__("New document")}</Button>}
|
||||||
/>
|
/>
|
||||||
|
</Authorized>
|
||||||
</div>
|
</div>
|
||||||
</PageHeader>
|
</PageHeader>
|
||||||
{documents.length > 0 ? (
|
{documents.length > 0 ? (
|
||||||
@@ -232,7 +241,7 @@ export default function DocumentsPage(props: Props) {
|
|||||||
<Th className="w-60">{__("Owner")}</Th>
|
<Th className="w-60">{__("Owner")}</Th>
|
||||||
<Th className="w-60">{__("Last update")}</Th>
|
<Th className="w-60">{__("Last update")}</Th>
|
||||||
<Th className="w-20">{__("Signatures")}</Th>
|
<Th className="w-20">{__("Signatures")}</Th>
|
||||||
<Th className="w-18"></Th>
|
{hasAnyAction && <Th className="w-18"></Th>}
|
||||||
</Tr>
|
</Tr>
|
||||||
) : (
|
) : (
|
||||||
<Tr>
|
<Tr>
|
||||||
@@ -249,6 +258,7 @@ export default function DocumentsPage(props: Props) {
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex gap-2 items-center">
|
<div className="flex gap-2 items-center">
|
||||||
|
<Authorized entity="Document" action="updateDocument">
|
||||||
<PublishDocumentsDialog
|
<PublishDocumentsDialog
|
||||||
documentIds={selection}
|
documentIds={selection}
|
||||||
onSave={clear}
|
onSave={clear}
|
||||||
@@ -260,6 +270,8 @@ export default function DocumentsPage(props: Props) {
|
|||||||
{__("Publish")}
|
{__("Publish")}
|
||||||
</Button>
|
</Button>
|
||||||
</PublishDocumentsDialog>
|
</PublishDocumentsDialog>
|
||||||
|
</Authorized>
|
||||||
|
<Authorized entity="Document" action="bulkRequestSignatures">
|
||||||
<SignatureDocumentsDialog
|
<SignatureDocumentsDialog
|
||||||
documentIds={selection}
|
documentIds={selection}
|
||||||
onSave={clear}
|
onSave={clear}
|
||||||
@@ -272,6 +284,7 @@ export default function DocumentsPage(props: Props) {
|
|||||||
{__("Request signature")}
|
{__("Request signature")}
|
||||||
</Button>
|
</Button>
|
||||||
</SignatureDocumentsDialog>
|
</SignatureDocumentsDialog>
|
||||||
|
</Authorized>
|
||||||
<BulkExportDialog
|
<BulkExportDialog
|
||||||
ref={bulkExportDialogRef}
|
ref={bulkExportDialogRef}
|
||||||
onExport={handleBulkExport}
|
onExport={handleBulkExport}
|
||||||
@@ -287,6 +300,7 @@ export default function DocumentsPage(props: Props) {
|
|||||||
{__("Export")}
|
{__("Export")}
|
||||||
</Button>
|
</Button>
|
||||||
</BulkExportDialog>
|
</BulkExportDialog>
|
||||||
|
<Authorized entity="Document" action="deleteDocument">
|
||||||
<Button
|
<Button
|
||||||
variant="danger"
|
variant="danger"
|
||||||
icon={IconTrashCan}
|
icon={IconTrashCan}
|
||||||
@@ -295,6 +309,7 @@ export default function DocumentsPage(props: Props) {
|
|||||||
>
|
>
|
||||||
{__("Delete")}
|
{__("Delete")}
|
||||||
</Button>
|
</Button>
|
||||||
|
</Authorized>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</Th>
|
</Th>
|
||||||
@@ -310,6 +325,7 @@ export default function DocumentsPage(props: Props) {
|
|||||||
document={document}
|
document={document}
|
||||||
organizationId={organization.id}
|
organizationId={organization.id}
|
||||||
connectionId={connectionId}
|
connectionId={connectionId}
|
||||||
|
hasAnyAction={hasAnyAction}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
</Tbody>
|
</Tbody>
|
||||||
@@ -367,12 +383,14 @@ function DocumentRow({
|
|||||||
organizationId,
|
organizationId,
|
||||||
checked,
|
checked,
|
||||||
onCheck,
|
onCheck,
|
||||||
|
hasAnyAction,
|
||||||
}: {
|
}: {
|
||||||
document: DocumentsPageRowFragment$key;
|
document: DocumentsPageRowFragment$key;
|
||||||
organizationId: string;
|
organizationId: string;
|
||||||
connectionId: string;
|
connectionId: string;
|
||||||
checked: boolean;
|
checked: boolean;
|
||||||
onCheck: () => void;
|
onCheck: () => void;
|
||||||
|
hasAnyAction: boolean;
|
||||||
}) {
|
}) {
|
||||||
const document = useFragment<DocumentsPageRowFragment$key>(
|
const document = useFragment<DocumentsPageRowFragment$key>(
|
||||||
rowFragment,
|
rowFragment,
|
||||||
@@ -444,8 +462,10 @@ function DocumentRow({
|
|||||||
<Td className="w-20">
|
<Td className="w-20">
|
||||||
{signedCount}/{signatures.length}
|
{signedCount}/{signatures.length}
|
||||||
</Td>
|
</Td>
|
||||||
|
{hasAnyAction && (
|
||||||
<Td noLink width={50} className="text-end w-18">
|
<Td noLink width={50} className="text-end w-18">
|
||||||
<ActionDropdown>
|
<ActionDropdown>
|
||||||
|
<Authorized entity="Document" action="deleteDocument">
|
||||||
<DropdownItem
|
<DropdownItem
|
||||||
variant="danger"
|
variant="danger"
|
||||||
icon={IconTrashCan}
|
icon={IconTrashCan}
|
||||||
@@ -453,8 +473,10 @@ function DocumentRow({
|
|||||||
>
|
>
|
||||||
{__("Delete")}
|
{__("Delete")}
|
||||||
</DropdownItem>
|
</DropdownItem>
|
||||||
|
</Authorized>
|
||||||
</ActionDropdown>
|
</ActionDropdown>
|
||||||
</Td>
|
</Td>
|
||||||
|
)}
|
||||||
</Tr>
|
</Tr>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,6 +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_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 { DocumentSignaturesTab_version$key } from "/pages/organizations/documents/tabs/__generated__/DocumentSignaturesTab_version.graphql.ts";
|
||||||
import type { DocumentSignaturesTabRefetchQuery } from "./__generated__/DocumentSignaturesTabRefetchQuery.graphql";
|
import type { DocumentSignaturesTabRefetchQuery } from "./__generated__/DocumentSignaturesTabRefetchQuery.graphql";
|
||||||
|
import { Authorized } from "/permissions";
|
||||||
|
|
||||||
type Version = NodeOf<DocumentDetailPageDocumentFragment$data["versions"]>;
|
type Version = NodeOf<DocumentDetailPageDocumentFragment$data["versions"]>;
|
||||||
|
|
||||||
@@ -274,6 +275,7 @@ function SignatureItem(props: {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{props.signable && (
|
{props.signable && (
|
||||||
|
<Authorized entity="Document" action="requestSignature">
|
||||||
<Button
|
<Button
|
||||||
variant="secondary"
|
variant="secondary"
|
||||||
className="ml-auto"
|
className="ml-auto"
|
||||||
@@ -292,6 +294,7 @@ function SignatureItem(props: {
|
|||||||
>
|
>
|
||||||
{__("Request signature")}
|
{__("Request signature")}
|
||||||
</Button>
|
</Button>
|
||||||
|
</Authorized>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
@@ -328,6 +331,7 @@ function SignatureItem(props: {
|
|||||||
{__("Signed")}
|
{__("Signed")}
|
||||||
</Badge>
|
</Badge>
|
||||||
) : (
|
) : (
|
||||||
|
<Authorized entity="DocumentVersionSignature" action="cancelSignatureRequest">
|
||||||
<Button
|
<Button
|
||||||
variant="danger"
|
variant="danger"
|
||||||
className="ml-auto"
|
className="ml-auto"
|
||||||
@@ -345,6 +349,7 @@ function SignatureItem(props: {
|
|||||||
>
|
>
|
||||||
{__("Cancel request")}
|
{__("Cancel request")}
|
||||||
</Button>
|
</Button>
|
||||||
|
</Authorized>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -27,6 +27,7 @@ import { promisifyMutation } from "@probo/helpers";
|
|||||||
import type { FrameworkGraphControlNodeQuery } from "/hooks/graph/__generated__/FrameworkGraphControlNodeQuery.graphql";
|
import type { FrameworkGraphControlNodeQuery } from "/hooks/graph/__generated__/FrameworkGraphControlNodeQuery.graphql";
|
||||||
import { frameworkControlNodeQuery } from "/hooks/graph/FrameworkGraph";
|
import { frameworkControlNodeQuery } from "/hooks/graph/FrameworkGraph";
|
||||||
import type { FrameworkDetailPageFragment$data } from "./__generated__/FrameworkDetailPageFragment.graphql";
|
import type { FrameworkDetailPageFragment$data } from "./__generated__/FrameworkDetailPageFragment.graphql";
|
||||||
|
import { Authorized } from "/permissions";
|
||||||
|
|
||||||
const attachMeasureMutation = graphql`
|
const attachMeasureMutation = graphql`
|
||||||
mutation FrameworkControlPageAttachMutation(
|
mutation FrameworkControlPageAttachMutation(
|
||||||
@@ -235,6 +236,7 @@ export default function FrameworkControlPage({ queryRef }: Props) {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex gap-2">
|
<div className="flex gap-2">
|
||||||
|
<Authorized entity="Control" action="updateControl">
|
||||||
<FrameworkControlDialog
|
<FrameworkControlDialog
|
||||||
frameworkId={framework.id}
|
frameworkId={framework.id}
|
||||||
connectionId={connectionId}
|
connectionId={connectionId}
|
||||||
@@ -244,6 +246,8 @@ export default function FrameworkControlPage({ queryRef }: Props) {
|
|||||||
{__("Edit control")}
|
{__("Edit control")}
|
||||||
</Button>
|
</Button>
|
||||||
</FrameworkControlDialog>
|
</FrameworkControlDialog>
|
||||||
|
</Authorized>
|
||||||
|
<Authorized entity="Control" action="deleteControl">
|
||||||
<ActionDropdown variant="secondary">
|
<ActionDropdown variant="secondary">
|
||||||
<DropdownItem
|
<DropdownItem
|
||||||
icon={IconTrashCan}
|
icon={IconTrashCan}
|
||||||
@@ -253,6 +257,7 @@ export default function FrameworkControlPage({ queryRef }: Props) {
|
|||||||
{__("Delete")}
|
{__("Delete")}
|
||||||
</DropdownItem>
|
</DropdownItem>
|
||||||
</ActionDropdown>
|
</ActionDropdown>
|
||||||
|
</Authorized>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -31,6 +31,7 @@ import type { FrameworkDetailPageExportFrameworkMutation } from "./__generated__
|
|||||||
import { FrameworkFormDialog } from "./dialogs/FrameworkFormDialog";
|
import { FrameworkFormDialog } from "./dialogs/FrameworkFormDialog";
|
||||||
import { FrameworkControlDialog } from "./dialogs/FrameworkControlDialog";
|
import { FrameworkControlDialog } from "./dialogs/FrameworkControlDialog";
|
||||||
import { useMutationWithToasts } from "/hooks/useMutationWithToasts";
|
import { useMutationWithToasts } from "/hooks/useMutationWithToasts";
|
||||||
|
import { Authorized } from "/permissions";
|
||||||
|
|
||||||
const frameworkDetailFragment = graphql`
|
const frameworkDetailFragment = graphql`
|
||||||
fragment FrameworkDetailPageFragment on Framework {
|
fragment FrameworkDetailPageFragment on Framework {
|
||||||
@@ -149,6 +150,7 @@ export default function FrameworkDetailPage(props: Props) {
|
|||||||
</>
|
</>
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
|
<Authorized entity="Framework" action="updateFramework">
|
||||||
<FrameworkFormDialog
|
<FrameworkFormDialog
|
||||||
organizationId={organizationId}
|
organizationId={organizationId}
|
||||||
framework={framework}
|
framework={framework}
|
||||||
@@ -157,6 +159,7 @@ export default function FrameworkDetailPage(props: Props) {
|
|||||||
{__("Edit")}
|
{__("Edit")}
|
||||||
</Button>
|
</Button>
|
||||||
</FrameworkFormDialog>
|
</FrameworkFormDialog>
|
||||||
|
</Authorized>
|
||||||
<ActionDropdown variant="secondary">
|
<ActionDropdown variant="secondary">
|
||||||
<DropdownItem
|
<DropdownItem
|
||||||
variant="primary"
|
variant="primary"
|
||||||
@@ -188,9 +191,11 @@ export default function FrameworkDetailPage(props: Props) {
|
|||||||
>
|
>
|
||||||
{__("Export Framework")}
|
{__("Export Framework")}
|
||||||
</DropdownItem>
|
</DropdownItem>
|
||||||
|
<Authorized entity="Framework" action="deleteFramework">
|
||||||
<DropdownItem icon={IconTrashCan} variant="danger" onClick={onDelete}>
|
<DropdownItem icon={IconTrashCan} variant="danger" onClick={onDelete}>
|
||||||
{__("Delete")}
|
{__("Delete")}
|
||||||
</DropdownItem>
|
</DropdownItem>
|
||||||
|
</Authorized>
|
||||||
</ActionDropdown>
|
</ActionDropdown>
|
||||||
</PageHeader>
|
</PageHeader>
|
||||||
<div className="text-lg font-semibold">
|
<div className="text-lg font-semibold">
|
||||||
@@ -211,6 +216,7 @@ export default function FrameworkDetailPage(props: Props) {
|
|||||||
active={selectedControl?.id === control.id}
|
active={selectedControl?.id === control.id}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
|
<Authorized entity="Organization" action="createControl">
|
||||||
<FrameworkControlDialog
|
<FrameworkControlDialog
|
||||||
frameworkId={framework.id}
|
frameworkId={framework.id}
|
||||||
connectionId={connectionId}
|
connectionId={connectionId}
|
||||||
@@ -220,6 +226,7 @@ export default function FrameworkDetailPage(props: Props) {
|
|||||||
{__("Add new control")}
|
{__("Add new control")}
|
||||||
</button>
|
</button>
|
||||||
</FrameworkControlDialog>
|
</FrameworkControlDialog>
|
||||||
|
</Authorized>
|
||||||
</div>
|
</div>
|
||||||
<Outlet context={{ framework }} />
|
<Outlet context={{ framework }} />
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -29,6 +29,8 @@ import { useMutationWithToasts } from "/hooks/useMutationWithToasts";
|
|||||||
import { useState, type ChangeEventHandler } from "react";
|
import { useState, type ChangeEventHandler } from "react";
|
||||||
import { FrameworkLogo } from "/components/FrameworkLogo";
|
import { FrameworkLogo } from "/components/FrameworkLogo";
|
||||||
import { FrameworkFormDialog } from "./dialogs/FrameworkFormDialog";
|
import { FrameworkFormDialog } from "./dialogs/FrameworkFormDialog";
|
||||||
|
import { Authorized } from "/permissions";
|
||||||
|
import { isAuthorized } from "/permissions";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
queryRef: PreloadedQuery<FrameworkGraphListQuery>;
|
queryRef: PreloadedQuery<FrameworkGraphListQuery>;
|
||||||
@@ -118,6 +120,9 @@ export default function FrameworksPage(props: Props) {
|
|||||||
|
|
||||||
const isLoading = isUploading || isImporting;
|
const isLoading = isUploading || isImporting;
|
||||||
|
|
||||||
|
const hasAnyAction = isAuthorized(data.organization.id!, "Framework", "updateFramework") ||
|
||||||
|
isAuthorized(data.organization.id!, "Framework", "deleteFramework");
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-6">
|
<div className="space-y-6">
|
||||||
<FrameworkFormDialog
|
<FrameworkFormDialog
|
||||||
@@ -129,6 +134,7 @@ export default function FrameworksPage(props: Props) {
|
|||||||
title={__("Frameworks")}
|
title={__("Frameworks")}
|
||||||
description={__("Manage your compliance frameworks")}
|
description={__("Manage your compliance frameworks")}
|
||||||
>
|
>
|
||||||
|
<Authorized entity="Organization" action="createFramework">
|
||||||
<FileButton
|
<FileButton
|
||||||
variant="secondary"
|
variant="secondary"
|
||||||
icon={IconFolderUpload}
|
icon={IconFolderUpload}
|
||||||
@@ -141,6 +147,7 @@ export default function FrameworksPage(props: Props) {
|
|||||||
onSelect={importNamedFramework}
|
onSelect={importNamedFramework}
|
||||||
disabled={isLoading}
|
disabled={isLoading}
|
||||||
/>
|
/>
|
||||||
|
</Authorized>
|
||||||
</PageHeader>
|
</PageHeader>
|
||||||
|
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
||||||
@@ -150,6 +157,7 @@ export default function FrameworksPage(props: Props) {
|
|||||||
connectionId={connectionId}
|
connectionId={connectionId}
|
||||||
key={framework.id}
|
key={framework.id}
|
||||||
framework={framework}
|
framework={framework}
|
||||||
|
hasAnyAction={hasAnyAction}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
@@ -169,6 +177,7 @@ type FrameworkCardProps = {
|
|||||||
organizationId: string;
|
organizationId: string;
|
||||||
connectionId: string;
|
connectionId: string;
|
||||||
framework: FrameworksPageCardFragment$key;
|
framework: FrameworksPageCardFragment$key;
|
||||||
|
hasAnyAction: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
function FrameworkCard(props: FrameworkCardProps) {
|
function FrameworkCard(props: FrameworkCardProps) {
|
||||||
@@ -189,7 +198,9 @@ function FrameworkCard(props: FrameworkCardProps) {
|
|||||||
/>
|
/>
|
||||||
<div className="flex justify-between mb-3">
|
<div className="flex justify-between mb-3">
|
||||||
<FrameworkLogo {...framework} />
|
<FrameworkLogo {...framework} />
|
||||||
|
{props.hasAnyAction && (
|
||||||
<ActionDropdown className="z-10 relative">
|
<ActionDropdown className="z-10 relative">
|
||||||
|
<Authorized entity="Framework" action="updateFramework">
|
||||||
<DropdownItem
|
<DropdownItem
|
||||||
icon={IconPencil}
|
icon={IconPencil}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
@@ -198,6 +209,8 @@ function FrameworkCard(props: FrameworkCardProps) {
|
|||||||
>
|
>
|
||||||
{__("Edit")}
|
{__("Edit")}
|
||||||
</DropdownItem>
|
</DropdownItem>
|
||||||
|
</Authorized>
|
||||||
|
<Authorized entity="Framework" action="deleteFramework">
|
||||||
<DropdownItem
|
<DropdownItem
|
||||||
icon={IconTrashCan}
|
icon={IconTrashCan}
|
||||||
onClick={() => deleteFramework()}
|
onClick={() => deleteFramework()}
|
||||||
@@ -205,7 +218,9 @@ function FrameworkCard(props: FrameworkCardProps) {
|
|||||||
>
|
>
|
||||||
{__("Delete")}
|
{__("Delete")}
|
||||||
</DropdownItem>
|
</DropdownItem>
|
||||||
|
</Authorized>
|
||||||
</ActionDropdown>
|
</ActionDropdown>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
<h2 className="text-xl font-medium">
|
<h2 className="text-xl font-medium">
|
||||||
<Link
|
<Link
|
||||||
|
|||||||
@@ -43,6 +43,7 @@ import {
|
|||||||
sprintf,
|
sprintf,
|
||||||
} from "@probo/helpers";
|
} from "@probo/helpers";
|
||||||
import MeasureFormDialog from "./dialog/MeasureFormDialog";
|
import MeasureFormDialog from "./dialog/MeasureFormDialog";
|
||||||
|
import { Authorized } from "/permissions";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
queryRef: PreloadedQuery<MeasureGraphNodeQuery>;
|
queryRef: PreloadedQuery<MeasureGraphNodeQuery>;
|
||||||
@@ -135,6 +136,7 @@ export default function MeasureDetailPage(props: Props) {
|
|||||||
/>
|
/>
|
||||||
|
|
||||||
<PageHeader title={measure.name} description={measure.description}>
|
<PageHeader title={measure.name} description={measure.description}>
|
||||||
|
<Authorized entity="Measure" action="updateMeasure">
|
||||||
<MeasureFormDialog measure={measure}>
|
<MeasureFormDialog measure={measure}>
|
||||||
<Button variant="secondary" icon={IconPencil}>
|
<Button variant="secondary" icon={IconPencil}>
|
||||||
{__("Edit")}
|
{__("Edit")}
|
||||||
@@ -154,10 +156,13 @@ export default function MeasureDetailPage(props: Props) {
|
|||||||
</Option>
|
</Option>
|
||||||
))}
|
))}
|
||||||
</Select>
|
</Select>
|
||||||
|
</Authorized>
|
||||||
<ActionDropdown variant="secondary">
|
<ActionDropdown variant="secondary">
|
||||||
|
<Authorized entity="Measure" action="deleteMeasure">
|
||||||
<DropdownItem variant="danger" icon={IconTrashCan} onClick={onDelete}>
|
<DropdownItem variant="danger" icon={IconTrashCan} onClick={onDelete}>
|
||||||
{__("Delete")}
|
{__("Delete")}
|
||||||
</DropdownItem>
|
</DropdownItem>
|
||||||
|
</Authorized>
|
||||||
</ActionDropdown>
|
</ActionDropdown>
|
||||||
</PageHeader>
|
</PageHeader>
|
||||||
|
|
||||||
|
|||||||
@@ -48,6 +48,8 @@ import { useOrganizationId } from "/hooks/useOrganizationId";
|
|||||||
import { Link, useParams } from "react-router";
|
import { Link, useParams } from "react-router";
|
||||||
import MeasureFormDialog from "./dialog/MeasureFormDialog";
|
import MeasureFormDialog from "./dialog/MeasureFormDialog";
|
||||||
import { usePageTitle } from "@probo/hooks";
|
import { usePageTitle } from "@probo/hooks";
|
||||||
|
import { Authorized } from "/permissions";
|
||||||
|
import { isAuthorized } from "/permissions";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
queryRef: PreloadedQuery<MeasureGraphListQuery>;
|
queryRef: PreloadedQuery<MeasureGraphListQuery>;
|
||||||
@@ -113,6 +115,9 @@ export default function MeasuresPage(props: Props) {
|
|||||||
const importFileRef = useRef<HTMLInputElement>(null);
|
const importFileRef = useRef<HTMLInputElement>(null);
|
||||||
usePageTitle(__("Measures"));
|
usePageTitle(__("Measures"));
|
||||||
|
|
||||||
|
const hasAnyAction = isAuthorized(organization.id, "Measure", "updateMeasure") ||
|
||||||
|
isAuthorized(organization.id, "Measure", "deleteMeasure");
|
||||||
|
|
||||||
const handleImport: ChangeEventHandler<HTMLInputElement> = (event) => {
|
const handleImport: ChangeEventHandler<HTMLInputElement> = (event) => {
|
||||||
const file = event.target.files?.[0];
|
const file = event.target.files?.[0];
|
||||||
if (!file) {
|
if (!file) {
|
||||||
@@ -143,6 +148,7 @@ export default function MeasuresPage(props: Props) {
|
|||||||
"Measures are actions taken to reduce the risk. Add them to track their implementation status."
|
"Measures are actions taken to reduce the risk. Add them to track their implementation status."
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
|
<Authorized entity="Organization" action="createMeasure">
|
||||||
<FileButton
|
<FileButton
|
||||||
ref={importFileRef}
|
ref={importFileRef}
|
||||||
variant="secondary"
|
variant="secondary"
|
||||||
@@ -156,6 +162,7 @@ export default function MeasuresPage(props: Props) {
|
|||||||
{__("New measure")}
|
{__("New measure")}
|
||||||
</Button>
|
</Button>
|
||||||
</MeasureFormDialog>
|
</MeasureFormDialog>
|
||||||
|
</Authorized>
|
||||||
</PageHeader>
|
</PageHeader>
|
||||||
<MeasureImplementation measures={measures} className="my-10" />
|
<MeasureImplementation measures={measures} className="my-10" />
|
||||||
{objectKeys(measuresPerCategory)
|
{objectKeys(measuresPerCategory)
|
||||||
@@ -166,6 +173,7 @@ export default function MeasuresPage(props: Props) {
|
|||||||
category={category}
|
category={category}
|
||||||
measures={measuresPerCategory[category]}
|
measures={measuresPerCategory[category]}
|
||||||
connectionId={connectionId}
|
connectionId={connectionId}
|
||||||
|
hasAnyAction={hasAnyAction}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
@@ -176,6 +184,7 @@ type CategoryProps = {
|
|||||||
category: string;
|
category: string;
|
||||||
measures: NodeOf<MeasuresPageFragment$data["measures"]>[];
|
measures: NodeOf<MeasuresPageFragment$data["measures"]>[];
|
||||||
connectionId: string;
|
connectionId: string;
|
||||||
|
hasAnyAction: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
function Category(props: CategoryProps) {
|
function Category(props: CategoryProps) {
|
||||||
@@ -219,7 +228,7 @@ function Category(props: CategoryProps) {
|
|||||||
<Tr>
|
<Tr>
|
||||||
<Th>{__("Measure")}</Th>
|
<Th>{__("Measure")}</Th>
|
||||||
<Th>{__("State")}</Th>
|
<Th>{__("State")}</Th>
|
||||||
<Th></Th>
|
{props.hasAnyAction && <Th></Th>}
|
||||||
</Tr>
|
</Tr>
|
||||||
</Thead>
|
</Thead>
|
||||||
<Tbody>
|
<Tbody>
|
||||||
@@ -228,6 +237,7 @@ function Category(props: CategoryProps) {
|
|||||||
key={measure.id}
|
key={measure.id}
|
||||||
measure={measure}
|
measure={measure}
|
||||||
connectionId={props.connectionId}
|
connectionId={props.connectionId}
|
||||||
|
hasAnyAction={props.hasAnyAction}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
</Tbody>
|
</Tbody>
|
||||||
@@ -251,6 +261,7 @@ function Category(props: CategoryProps) {
|
|||||||
type MeasureRowProps = {
|
type MeasureRowProps = {
|
||||||
measure: NodeOf<MeasuresPageFragment$data["measures"]>;
|
measure: NodeOf<MeasuresPageFragment$data["measures"]>;
|
||||||
connectionId: string;
|
connectionId: string;
|
||||||
|
hasAnyAction: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
function MeasureRow(props: MeasureRowProps) {
|
function MeasureRow(props: MeasureRowProps) {
|
||||||
@@ -292,14 +303,18 @@ function MeasureRow(props: MeasureRowProps) {
|
|||||||
<Td width={120}>
|
<Td width={120}>
|
||||||
<MeasureBadge state={props.measure.state} />
|
<MeasureBadge state={props.measure.state} />
|
||||||
</Td>
|
</Td>
|
||||||
|
{props.hasAnyAction && (
|
||||||
<Td noLink width={50} className="text-end">
|
<Td noLink width={50} className="text-end">
|
||||||
<ActionDropdown>
|
<ActionDropdown>
|
||||||
|
<Authorized entity="Measure" action="updateMeasure">
|
||||||
<DropdownItem
|
<DropdownItem
|
||||||
icon={IconPencil}
|
icon={IconPencil}
|
||||||
onClick={() => dialogRef.current?.open()}
|
onClick={() => dialogRef.current?.open()}
|
||||||
>
|
>
|
||||||
{__("Edit")}
|
{__("Edit")}
|
||||||
</DropdownItem>
|
</DropdownItem>
|
||||||
|
</Authorized>
|
||||||
|
<Authorized entity="Measure" action="deleteMeasure">
|
||||||
<DropdownItem
|
<DropdownItem
|
||||||
onClick={onDelete}
|
onClick={onDelete}
|
||||||
disabled={isDeleting}
|
disabled={isDeleting}
|
||||||
@@ -308,8 +323,10 @@ function MeasureRow(props: MeasureRowProps) {
|
|||||||
>
|
>
|
||||||
{__("Delete")}
|
{__("Delete")}
|
||||||
</DropdownItem>
|
</DropdownItem>
|
||||||
|
</Authorized>
|
||||||
</ActionDropdown>
|
</ActionDropdown>
|
||||||
</Td>
|
</Td>
|
||||||
|
)}
|
||||||
</Tr>
|
</Tr>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -21,11 +21,12 @@ import {
|
|||||||
UpdateMeetingMinutesDialog,
|
UpdateMeetingMinutesDialog,
|
||||||
type UpdateMeetingMinutesDialogRef,
|
type UpdateMeetingMinutesDialogRef,
|
||||||
} from "./dialogs/UpdateMeetingMinutesDialog";
|
} from "./dialogs/UpdateMeetingMinutesDialog";
|
||||||
import { useRef } from "react";
|
import { useRef, useState, useEffect } from "react";
|
||||||
import {
|
import {
|
||||||
meetingNodeQuery,
|
meetingNodeQuery,
|
||||||
useDeleteMeetingMutation,
|
useDeleteMeetingMutation,
|
||||||
} from "/hooks/graph/MeetingGraph";
|
} from "/hooks/graph/MeetingGraph";
|
||||||
|
import { isAuthorized } from "/permissions";
|
||||||
|
|
||||||
const meetingFragment = graphql`
|
const meetingFragment = graphql`
|
||||||
fragment MeetingDetailPageMeetingFragment on Meeting {
|
fragment MeetingDetailPageMeetingFragment on Meeting {
|
||||||
@@ -62,6 +63,63 @@ export default function MeetingDetailPage(props: Props) {
|
|||||||
const confirm = useConfirm();
|
const confirm = useConfirm();
|
||||||
const updateMinutesDialogRef = useRef<UpdateMeetingMinutesDialogRef>(null);
|
const updateMinutesDialogRef = useRef<UpdateMeetingMinutesDialogRef>(null);
|
||||||
|
|
||||||
|
const [canUpdate, setCanUpdate] = useState<boolean>(false);
|
||||||
|
const [canDelete, setCanDelete] = useState<boolean>(false);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!organizationId) {
|
||||||
|
setCanUpdate(false);
|
||||||
|
setCanDelete(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const updateAuth = isAuthorized(organizationId, "Meeting", "updateMeeting");
|
||||||
|
setCanUpdate(updateAuth);
|
||||||
|
} catch (promise) {
|
||||||
|
if (promise instanceof Promise) {
|
||||||
|
promise
|
||||||
|
.then(() => {
|
||||||
|
try {
|
||||||
|
const updateAuth = isAuthorized(organizationId, "Meeting", "updateMeeting");
|
||||||
|
setCanUpdate(updateAuth);
|
||||||
|
} catch {
|
||||||
|
setCanUpdate(false);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
setCanUpdate(false);
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
setCanUpdate(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const deleteAuth = isAuthorized(organizationId, "Meeting", "deleteMeeting");
|
||||||
|
setCanDelete(deleteAuth);
|
||||||
|
} catch (promise) {
|
||||||
|
if (promise instanceof Promise) {
|
||||||
|
promise
|
||||||
|
.then(() => {
|
||||||
|
try {
|
||||||
|
const deleteAuth = isAuthorized(organizationId, "Meeting", "deleteMeeting");
|
||||||
|
setCanDelete(deleteAuth);
|
||||||
|
} catch {
|
||||||
|
setCanDelete(false);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
setCanDelete(false);
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
setCanDelete(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, [organizationId]);
|
||||||
|
|
||||||
|
const hasAnyAction = canUpdate || canDelete;
|
||||||
|
|
||||||
usePageTitle(meeting.name);
|
usePageTitle(meeting.name);
|
||||||
|
|
||||||
const handleDelete = () => {
|
const handleDelete = () => {
|
||||||
@@ -105,13 +163,17 @@ export default function MeetingDetailPage(props: Props) {
|
|||||||
},
|
},
|
||||||
]}
|
]}
|
||||||
/>
|
/>
|
||||||
|
{hasAnyAction && (
|
||||||
<ActionDropdown variant="secondary">
|
<ActionDropdown variant="secondary">
|
||||||
|
{canUpdate && (
|
||||||
<DropdownItem
|
<DropdownItem
|
||||||
onClick={() => updateMinutesDialogRef.current?.open()}
|
onClick={() => updateMinutesDialogRef.current?.open()}
|
||||||
icon={IconPencil}
|
icon={IconPencil}
|
||||||
>
|
>
|
||||||
{__("Edit minutes")}
|
{__("Edit minutes")}
|
||||||
</DropdownItem>
|
</DropdownItem>
|
||||||
|
)}
|
||||||
|
{canDelete && (
|
||||||
<DropdownItem
|
<DropdownItem
|
||||||
variant="danger"
|
variant="danger"
|
||||||
icon={IconTrashCan}
|
icon={IconTrashCan}
|
||||||
@@ -120,7 +182,9 @@ export default function MeetingDetailPage(props: Props) {
|
|||||||
>
|
>
|
||||||
{__("Delete meeting")}
|
{__("Delete meeting")}
|
||||||
</DropdownItem>
|
</DropdownItem>
|
||||||
|
)}
|
||||||
</ActionDropdown>
|
</ActionDropdown>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
<PageHeader
|
<PageHeader
|
||||||
title={meeting.name}
|
title={meeting.name}
|
||||||
|
|||||||
@@ -42,6 +42,7 @@ import { Link } from "react-router";
|
|||||||
import { useState, useEffect, useRef } from "react";
|
import { useState, useEffect, useRef } from "react";
|
||||||
import { useMutationWithToasts } from "/hooks/useMutationWithToasts";
|
import { useMutationWithToasts } from "/hooks/useMutationWithToasts";
|
||||||
import type { MeetingsPage_UpdateSummaryMutation } from "./__generated__/MeetingsPage_UpdateSummaryMutation.graphql";
|
import type { MeetingsPage_UpdateSummaryMutation } from "./__generated__/MeetingsPage_UpdateSummaryMutation.graphql";
|
||||||
|
import { Authorized } from "/permissions";
|
||||||
|
|
||||||
const meetingsFragment = graphql`
|
const meetingsFragment = graphql`
|
||||||
fragment MeetingsPageListFragment on Organization
|
fragment MeetingsPageListFragment on Organization
|
||||||
@@ -217,6 +218,7 @@ export default function MeetingsPage(props: Props) {
|
|||||||
<h3 className="text-sm font-semibold text-txt-secondary">
|
<h3 className="text-sm font-semibold text-txt-secondary">
|
||||||
{__("Summary")}
|
{__("Summary")}
|
||||||
</h3>
|
</h3>
|
||||||
|
<Authorized entity="Meeting" action="updateMeeting">
|
||||||
<Button
|
<Button
|
||||||
variant="quaternary"
|
variant="quaternary"
|
||||||
icon={IconPencil}
|
icon={IconPencil}
|
||||||
@@ -224,6 +226,7 @@ export default function MeetingsPage(props: Props) {
|
|||||||
>
|
>
|
||||||
{__("Edit")}
|
{__("Edit")}
|
||||||
</Button>
|
</Button>
|
||||||
|
</Authorized>
|
||||||
</div>
|
</div>
|
||||||
<div className="w-full">
|
<div className="w-full">
|
||||||
{displayedSummary ? (
|
{displayedSummary ? (
|
||||||
@@ -245,9 +248,11 @@ export default function MeetingsPage(props: Props) {
|
|||||||
"Track and manage your organization's meetings and their minutes."
|
"Track and manage your organization's meetings and their minutes."
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
|
<Authorized entity="Organization" action="createMeeting">
|
||||||
<CreateMeetingDialog connectionId={connectionId}>
|
<CreateMeetingDialog connectionId={connectionId}>
|
||||||
<Button icon={IconPlusLarge}>{__("Add meeting")}</Button>
|
<Button icon={IconPlusLarge}>{__("Add meeting")}</Button>
|
||||||
</CreateMeetingDialog>
|
</CreateMeetingDialog>
|
||||||
|
</Authorized>
|
||||||
</PageHeader>
|
</PageHeader>
|
||||||
{meetingNodes.length > 0 ? (
|
{meetingNodes.length > 0 ? (
|
||||||
<SortableTable {...pagination}>
|
<SortableTable {...pagination}>
|
||||||
@@ -365,6 +370,7 @@ function MeetingRow({
|
|||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
</Td>
|
</Td>
|
||||||
|
<Authorized entity="Meeting" action="deleteMeeting">
|
||||||
<Td noLink width={50} className="text-end w-18">
|
<Td noLink width={50} className="text-end w-18">
|
||||||
<ActionDropdown>
|
<ActionDropdown>
|
||||||
<DropdownItem
|
<DropdownItem
|
||||||
@@ -376,6 +382,7 @@ function MeetingRow({
|
|||||||
</DropdownItem>
|
</DropdownItem>
|
||||||
</ActionDropdown>
|
</ActionDropdown>
|
||||||
</Td>
|
</Td>
|
||||||
|
</Authorized>
|
||||||
</Tr>
|
</Tr>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
/**
|
/**
|
||||||
* @generated SignedSource<<60e0c9d7301cff5c1df299e76debb633>>
|
* @generated SignedSource<<1fff8c5cca1610284185c485630e84de>>
|
||||||
* @lightSyntaxTransform
|
* @lightSyntaxTransform
|
||||||
* @nogrep
|
* @nogrep
|
||||||
*/
|
*/
|
||||||
@@ -19,6 +19,7 @@ export type MeetingsPage_UpdateSummaryMutation$variables = {
|
|||||||
export type MeetingsPage_UpdateSummaryMutation$data = {
|
export type MeetingsPage_UpdateSummaryMutation$data = {
|
||||||
readonly updateOrganizationContext: {
|
readonly updateOrganizationContext: {
|
||||||
readonly context: {
|
readonly context: {
|
||||||
|
readonly organizationId: string;
|
||||||
readonly summary: string | null | undefined;
|
readonly summary: string | null | undefined;
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
@@ -59,6 +60,13 @@ v1 = [
|
|||||||
"name": "context",
|
"name": "context",
|
||||||
"plural": false,
|
"plural": false,
|
||||||
"selections": [
|
"selections": [
|
||||||
|
{
|
||||||
|
"alias": null,
|
||||||
|
"args": null,
|
||||||
|
"kind": "ScalarField",
|
||||||
|
"name": "organizationId",
|
||||||
|
"storageKey": null
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"alias": null,
|
"alias": null,
|
||||||
"args": null,
|
"args": null,
|
||||||
@@ -91,16 +99,16 @@ return {
|
|||||||
"selections": (v1/*: any*/)
|
"selections": (v1/*: any*/)
|
||||||
},
|
},
|
||||||
"params": {
|
"params": {
|
||||||
"cacheID": "e6bead1dde5239f3cfd2fa1440191454",
|
"cacheID": "cb37cdde6dc5ac655a7cefc63d9e72e7",
|
||||||
"id": null,
|
"id": null,
|
||||||
"metadata": {},
|
"metadata": {},
|
||||||
"name": "MeetingsPage_UpdateSummaryMutation",
|
"name": "MeetingsPage_UpdateSummaryMutation",
|
||||||
"operationKind": "mutation",
|
"operationKind": "mutation",
|
||||||
"text": "mutation MeetingsPage_UpdateSummaryMutation(\n $input: UpdateOrganizationContextInput!\n) {\n updateOrganizationContext(input: $input) {\n context {\n summary\n }\n }\n}\n"
|
"text": "mutation MeetingsPage_UpdateSummaryMutation(\n $input: UpdateOrganizationContextInput!\n) {\n updateOrganizationContext(input: $input) {\n context {\n organizationId\n summary\n }\n }\n}\n"
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
})();
|
})();
|
||||||
|
|
||||||
(node as any).hash = "f354a34b18a449f02508c45d0e7d9dc5";
|
(node as any).hash = "8bfa5b636dbc3535dbed22e1869bc941";
|
||||||
|
|
||||||
export default node;
|
export default node;
|
||||||
|
|||||||
@@ -31,6 +31,8 @@ import { deleteNonconformityMutation, NonconformitiesConnectionKey } from "../..
|
|||||||
import { sprintf, promisifyMutation, getStatusVariant, getStatusLabel, formatDate } from "@probo/helpers";
|
import { sprintf, promisifyMutation, getStatusVariant, getStatusLabel, formatDate } from "@probo/helpers";
|
||||||
import { SnapshotBanner } from "/components/SnapshotBanner";
|
import { SnapshotBanner } from "/components/SnapshotBanner";
|
||||||
import { useParams } from "react-router";
|
import { useParams } from "react-router";
|
||||||
|
import { Authorized } from "/permissions";
|
||||||
|
import { isAuthorized } from "/permissions";
|
||||||
import type { NonconformitiesPageQuery } from "./__generated__/NonconformitiesPageQuery.graphql";
|
import type { NonconformitiesPageQuery } from "./__generated__/NonconformitiesPageQuery.graphql";
|
||||||
import type {
|
import type {
|
||||||
NonconformitiesPageFragment$key,
|
NonconformitiesPageFragment$key,
|
||||||
@@ -129,6 +131,11 @@ export default function NonconformitiesPage({ queryRef }: NonconformitiesPagePro
|
|||||||
);
|
);
|
||||||
const nonconformities: Nonconformity[] = nonconformitiesData?.nonconformities?.edges?.map((edge) => edge.node) ?? [];
|
const nonconformities: Nonconformity[] = nonconformitiesData?.nonconformities?.edges?.map((edge) => edge.node) ?? [];
|
||||||
|
|
||||||
|
const hasAnyAction = !isSnapshotMode && (
|
||||||
|
isAuthorized(organizationId, "Nonconformity", "updateNonconformity") ||
|
||||||
|
isAuthorized(organizationId, "Nonconformity", "deleteNonconformity")
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-6">
|
<div className="space-y-6">
|
||||||
{isSnapshotMode && (
|
{isSnapshotMode && (
|
||||||
@@ -141,9 +148,11 @@ export default function NonconformitiesPage({ queryRef }: NonconformitiesPagePro
|
|||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{!isSnapshotMode && (
|
{!isSnapshotMode && (
|
||||||
|
<Authorized entity="Organization" action="createNonconformity">
|
||||||
<CreateNonconformityDialog organizationId={organizationId} connection={connectionId}>
|
<CreateNonconformityDialog organizationId={organizationId} connection={connectionId}>
|
||||||
<Button icon={IconPlusLarge}>{__("Add nonconformity")}</Button>
|
<Button icon={IconPlusLarge}>{__("Add nonconformity")}</Button>
|
||||||
</CreateNonconformityDialog>
|
</CreateNonconformityDialog>
|
||||||
|
</Authorized>
|
||||||
)}
|
)}
|
||||||
</PageHeader>
|
</PageHeader>
|
||||||
|
|
||||||
@@ -169,7 +178,7 @@ export default function NonconformitiesPage({ queryRef }: NonconformitiesPagePro
|
|||||||
<Th>{__("Audit")}</Th>
|
<Th>{__("Audit")}</Th>
|
||||||
<Th>{__("Owner")}</Th>
|
<Th>{__("Owner")}</Th>
|
||||||
<Th>{__("Due Date")}</Th>
|
<Th>{__("Due Date")}</Th>
|
||||||
{!isSnapshotMode && (<Th>{__("Actions")}</Th>)}
|
{hasAnyAction && <Th>{__("Actions")}</Th>}
|
||||||
</Tr>
|
</Tr>
|
||||||
</Thead>
|
</Thead>
|
||||||
<Tbody>
|
<Tbody>
|
||||||
@@ -180,6 +189,7 @@ export default function NonconformitiesPage({ queryRef }: NonconformitiesPagePro
|
|||||||
connectionId={connectionId}
|
connectionId={connectionId}
|
||||||
isSnapshotMode={isSnapshotMode}
|
isSnapshotMode={isSnapshotMode}
|
||||||
snapshotId={snapshotId}
|
snapshotId={snapshotId}
|
||||||
|
hasAnyAction={hasAnyAction}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
</Tbody>
|
</Tbody>
|
||||||
@@ -209,11 +219,13 @@ function NonconformityRow({
|
|||||||
connectionId,
|
connectionId,
|
||||||
isSnapshotMode,
|
isSnapshotMode,
|
||||||
snapshotId,
|
snapshotId,
|
||||||
|
hasAnyAction,
|
||||||
}: {
|
}: {
|
||||||
nonconformity: Nonconformity;
|
nonconformity: Nonconformity;
|
||||||
connectionId: string;
|
connectionId: string;
|
||||||
isSnapshotMode: boolean;
|
isSnapshotMode: boolean;
|
||||||
snapshotId?: string;
|
snapshotId?: string;
|
||||||
|
hasAnyAction: boolean;
|
||||||
}) {
|
}) {
|
||||||
const organizationId = useOrganizationId();
|
const organizationId = useOrganizationId();
|
||||||
const { __ } = useTranslate();
|
const { __ } = useTranslate();
|
||||||
@@ -283,8 +295,10 @@ function NonconformityRow({
|
|||||||
<span className="text-txt-tertiary">{__("No due date")}</span>
|
<span className="text-txt-tertiary">{__("No due date")}</span>
|
||||||
)}
|
)}
|
||||||
</Td>
|
</Td>
|
||||||
{!isSnapshotMode && (<Td noLink width={50} className="text-end">
|
{hasAnyAction && (
|
||||||
|
<Td noLink width={50} className="text-end">
|
||||||
<ActionDropdown>
|
<ActionDropdown>
|
||||||
|
<Authorized entity="Nonconformity" action="deleteNonconformity">
|
||||||
<DropdownItem
|
<DropdownItem
|
||||||
icon={IconTrashCan}
|
icon={IconTrashCan}
|
||||||
variant="danger"
|
variant="danger"
|
||||||
@@ -292,6 +306,7 @@ function NonconformityRow({
|
|||||||
>
|
>
|
||||||
{__("Delete")}
|
{__("Delete")}
|
||||||
</DropdownItem>
|
</DropdownItem>
|
||||||
|
</Authorized>
|
||||||
</ActionDropdown>
|
</ActionDropdown>
|
||||||
</Td>
|
</Td>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -34,6 +34,7 @@ import { useFormWithSchema } from "/hooks/useFormWithSchema";
|
|||||||
import z from "zod";
|
import z from "zod";
|
||||||
import { getStatusVariant, getStatusLabel, formatDatetime, validateSnapshotConsistency, getStatusOptions, formatError, type GraphQLError } from "@probo/helpers";
|
import { getStatusVariant, getStatusLabel, formatDatetime, validateSnapshotConsistency, getStatusOptions, formatError, type GraphQLError } from "@probo/helpers";
|
||||||
import type { NonconformityGraphNodeQuery } from "/hooks/graph/__generated__/NonconformityGraphNodeQuery.graphql";
|
import type { NonconformityGraphNodeQuery } from "/hooks/graph/__generated__/NonconformityGraphNodeQuery.graphql";
|
||||||
|
import { Authorized } from "/permissions";
|
||||||
|
|
||||||
const updateNonconformitySchema = z.object({
|
const updateNonconformitySchema = z.object({
|
||||||
referenceId: z.string().min(1, "Reference ID is required"),
|
referenceId: z.string().min(1, "Reference ID is required"),
|
||||||
@@ -161,6 +162,7 @@ export default function NonconformityDetailsPage(props: Props) {
|
|||||||
</div>
|
</div>
|
||||||
{!isSnapshotMode && (
|
{!isSnapshotMode && (
|
||||||
<ActionDropdown variant="secondary">
|
<ActionDropdown variant="secondary">
|
||||||
|
<Authorized entity="Nonconformity" action="deleteNonconformity">
|
||||||
<DropdownItem
|
<DropdownItem
|
||||||
variant="danger"
|
variant="danger"
|
||||||
icon={IconTrashCan}
|
icon={IconTrashCan}
|
||||||
@@ -168,6 +170,7 @@ export default function NonconformityDetailsPage(props: Props) {
|
|||||||
>
|
>
|
||||||
{__("Delete")}
|
{__("Delete")}
|
||||||
</DropdownItem>
|
</DropdownItem>
|
||||||
|
</Authorized>
|
||||||
</ActionDropdown>
|
</ActionDropdown>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
@@ -276,9 +279,11 @@ export default function NonconformityDetailsPage(props: Props) {
|
|||||||
|
|
||||||
<div className="flex justify-end">
|
<div className="flex justify-end">
|
||||||
{formState.isDirty && !isSnapshotMode && (
|
{formState.isDirty && !isSnapshotMode && (
|
||||||
|
<Authorized entity="Nonconformity" action="updateNonconformity">
|
||||||
<Button type="submit" disabled={formState.isSubmitting}>
|
<Button type="submit" disabled={formState.isSubmitting}>
|
||||||
{formState.isSubmitting ? __("Updating...") : __("Update")}
|
{formState.isSubmitting ? __("Updating...") : __("Update")}
|
||||||
</Button>
|
</Button>
|
||||||
|
</Authorized>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
|||||||
@@ -35,6 +35,7 @@ import z from "zod";
|
|||||||
import { getObligationStatusVariant, getObligationStatusLabel, formatDatetime, getObligationStatusOptions, validateSnapshotConsistency } from "@probo/helpers";
|
import { getObligationStatusVariant, getObligationStatusLabel, formatDatetime, getObligationStatusOptions, validateSnapshotConsistency } from "@probo/helpers";
|
||||||
import { SnapshotBanner } from "/components/SnapshotBanner";
|
import { SnapshotBanner } from "/components/SnapshotBanner";
|
||||||
import type { ObligationGraphNodeQuery } from "/hooks/graph/__generated__/ObligationGraphNodeQuery.graphql";
|
import type { ObligationGraphNodeQuery } from "/hooks/graph/__generated__/ObligationGraphNodeQuery.graphql";
|
||||||
|
import { Authorized } from "/permissions";
|
||||||
|
|
||||||
const updateObligationSchema = z.object({
|
const updateObligationSchema = z.object({
|
||||||
area: z.string().optional(),
|
area: z.string().optional(),
|
||||||
@@ -156,11 +157,13 @@ export default function ObligationDetailsPage(props: Props) {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{!isSnapshotMode && (
|
{!isSnapshotMode && (
|
||||||
|
<Authorized entity="Obligation" action="deleteObligation">
|
||||||
<ActionDropdown>
|
<ActionDropdown>
|
||||||
<DropdownItem icon={IconTrashCan} onClick={deleteObligation}>
|
<DropdownItem icon={IconTrashCan} onClick={deleteObligation}>
|
||||||
{__("Delete")}
|
{__("Delete")}
|
||||||
</DropdownItem>
|
</DropdownItem>
|
||||||
</ActionDropdown>
|
</ActionDropdown>
|
||||||
|
</Authorized>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -298,12 +301,14 @@ export default function ObligationDetailsPage(props: Props) {
|
|||||||
|
|
||||||
{!isSnapshotMode && (
|
{!isSnapshotMode && (
|
||||||
<div className="flex justify-end">
|
<div className="flex justify-end">
|
||||||
|
<Authorized entity="Obligation" action="updateObligation">
|
||||||
<Button
|
<Button
|
||||||
type="submit"
|
type="submit"
|
||||||
disabled={formState.isSubmitting}
|
disabled={formState.isSubmitting}
|
||||||
>
|
>
|
||||||
{formState.isSubmitting ? __("Saving...") : __("Save Changes")}
|
{formState.isSubmitting ? __("Saving...") : __("Save Changes")}
|
||||||
</Button>
|
</Button>
|
||||||
|
</Authorized>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</form>
|
</form>
|
||||||
|
|||||||
@@ -31,6 +31,8 @@ import { deleteObligationMutation } from "../../../hooks/graph/ObligationGraph";
|
|||||||
import { promisifyMutation, getObligationStatusVariant, getObligationStatusLabel, formatDate } from "@probo/helpers";
|
import { promisifyMutation, getObligationStatusVariant, getObligationStatusLabel, formatDate } from "@probo/helpers";
|
||||||
import { SnapshotBanner } from "/components/SnapshotBanner";
|
import { SnapshotBanner } from "/components/SnapshotBanner";
|
||||||
import type { ObligationsPageQuery } from "./__generated__/ObligationsPageQuery.graphql";
|
import type { ObligationsPageQuery } from "./__generated__/ObligationsPageQuery.graphql";
|
||||||
|
import { Authorized } from "/permissions";
|
||||||
|
import { isAuthorized } from "/permissions";
|
||||||
import type {
|
import type {
|
||||||
ObligationsPageFragment$key,
|
ObligationsPageFragment$key,
|
||||||
ObligationsPageFragment$data,
|
ObligationsPageFragment$data,
|
||||||
@@ -117,6 +119,11 @@ export default function ObligationsPage({ queryRef }: ObligationsPageProps) {
|
|||||||
const connectionId = obligationsData?.obligations?.__id || "";
|
const connectionId = obligationsData?.obligations?.__id || "";
|
||||||
const obligations: Obligation[] = obligationsData?.obligations?.edges?.map((edge) => edge.node) ?? [];
|
const obligations: Obligation[] = obligationsData?.obligations?.edges?.map((edge) => edge.node) ?? [];
|
||||||
|
|
||||||
|
const hasAnyAction = !isSnapshotMode && (
|
||||||
|
isAuthorized(organizationId, "Obligation", "updateObligation") ||
|
||||||
|
isAuthorized(organizationId, "Obligation", "deleteObligation")
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-6">
|
<div className="space-y-6">
|
||||||
{isSnapshotMode && snapshotId && (
|
{isSnapshotMode && snapshotId && (
|
||||||
@@ -129,9 +136,11 @@ export default function ObligationsPage({ queryRef }: ObligationsPageProps) {
|
|||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{!snapshotId && (
|
{!snapshotId && (
|
||||||
|
<Authorized entity="Organization" action="createObligation">
|
||||||
<CreateObligationDialog organizationId={organizationId} connection={connectionId}>
|
<CreateObligationDialog organizationId={organizationId} connection={connectionId}>
|
||||||
<Button icon={IconPlusLarge}>{__("Add obligation")}</Button>
|
<Button icon={IconPlusLarge}>{__("Add obligation")}</Button>
|
||||||
</CreateObligationDialog>
|
</CreateObligationDialog>
|
||||||
|
</Authorized>
|
||||||
)}
|
)}
|
||||||
</PageHeader>
|
</PageHeader>
|
||||||
|
|
||||||
@@ -156,7 +165,7 @@ export default function ObligationsPage({ queryRef }: ObligationsPageProps) {
|
|||||||
<Th>{__("Status")}</Th>
|
<Th>{__("Status")}</Th>
|
||||||
<Th>{__("Owner")}</Th>
|
<Th>{__("Owner")}</Th>
|
||||||
<Th>{__("Due Date")}</Th>
|
<Th>{__("Due Date")}</Th>
|
||||||
<Th>{__("Actions")}</Th>
|
{hasAnyAction && <Th>{__("Actions")}</Th>}
|
||||||
</Tr>
|
</Tr>
|
||||||
</Thead>
|
</Thead>
|
||||||
<Tbody>
|
<Tbody>
|
||||||
@@ -166,6 +175,7 @@ export default function ObligationsPage({ queryRef }: ObligationsPageProps) {
|
|||||||
obligation={obligation}
|
obligation={obligation}
|
||||||
connectionId={connectionId}
|
connectionId={connectionId}
|
||||||
snapshotId={snapshotId}
|
snapshotId={snapshotId}
|
||||||
|
hasAnyAction={hasAnyAction}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
</Tbody>
|
</Tbody>
|
||||||
@@ -192,10 +202,12 @@ function ObligationRow({
|
|||||||
obligation,
|
obligation,
|
||||||
connectionId,
|
connectionId,
|
||||||
snapshotId,
|
snapshotId,
|
||||||
|
hasAnyAction,
|
||||||
}: {
|
}: {
|
||||||
obligation: Obligation;
|
obligation: Obligation;
|
||||||
connectionId: string;
|
connectionId: string;
|
||||||
snapshotId?: string;
|
snapshotId?: string;
|
||||||
|
hasAnyAction: boolean;
|
||||||
}) {
|
}) {
|
||||||
const organizationId = useOrganizationId();
|
const organizationId = useOrganizationId();
|
||||||
const { __ } = useTranslate();
|
const { __ } = useTranslate();
|
||||||
@@ -246,9 +258,10 @@ function ObligationRow({
|
|||||||
<span className="text-txt-tertiary">{__("No due date")}</span>
|
<span className="text-txt-tertiary">{__("No due date")}</span>
|
||||||
)}
|
)}
|
||||||
</Td>
|
</Td>
|
||||||
|
{hasAnyAction && (
|
||||||
<Td noLink width={50} className="text-end">
|
<Td noLink width={50} className="text-end">
|
||||||
{!isSnapshotMode && (
|
|
||||||
<ActionDropdown>
|
<ActionDropdown>
|
||||||
|
<Authorized entity="Obligation" action="deleteObligation">
|
||||||
<DropdownItem
|
<DropdownItem
|
||||||
icon={IconTrashCan}
|
icon={IconTrashCan}
|
||||||
variant="danger"
|
variant="danger"
|
||||||
@@ -256,9 +269,10 @@ function ObligationRow({
|
|||||||
>
|
>
|
||||||
{__("Delete")}
|
{__("Delete")}
|
||||||
</DropdownItem>
|
</DropdownItem>
|
||||||
|
</Authorized>
|
||||||
</ActionDropdown>
|
</ActionDropdown>
|
||||||
)}
|
|
||||||
</Td>
|
</Td>
|
||||||
|
)}
|
||||||
</Tr>
|
</Tr>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ import {
|
|||||||
import { useTranslate } from "@probo/i18n";
|
import { useTranslate } from "@probo/i18n";
|
||||||
import { useOrganizationId } from "/hooks/useOrganizationId";
|
import { useOrganizationId } from "/hooks/useOrganizationId";
|
||||||
import { Outlet } from "react-router";
|
import { Outlet } from "react-router";
|
||||||
|
import { Authorized } from "/permissions";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
queryRef: PreloadedQuery<PeopleGraphNodeQuery>;
|
queryRef: PreloadedQuery<PeopleGraphNodeQuery>;
|
||||||
@@ -54,6 +55,7 @@ export default function PeopleDetailPage(props: Props) {
|
|||||||
<Avatar name={people.fullName ?? ""} size="xl" />
|
<Avatar name={people.fullName ?? ""} size="xl" />
|
||||||
<div className="text-2xl">{people.fullName}</div>
|
<div className="text-2xl">{people.fullName}</div>
|
||||||
</div>
|
</div>
|
||||||
|
<Authorized entity="People" action="deletePeople">
|
||||||
<ActionDropdown variant="secondary">
|
<ActionDropdown variant="secondary">
|
||||||
<DropdownItem
|
<DropdownItem
|
||||||
variant="danger"
|
variant="danger"
|
||||||
@@ -63,6 +65,7 @@ export default function PeopleDetailPage(props: Props) {
|
|||||||
{__("Delete")}
|
{__("Delete")}
|
||||||
</DropdownItem>
|
</DropdownItem>
|
||||||
</ActionDropdown>
|
</ActionDropdown>
|
||||||
|
</Authorized>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Tabs>
|
<Tabs>
|
||||||
|
|||||||
@@ -23,6 +23,8 @@ import { usePageTitle } from "@probo/hooks";
|
|||||||
import { getRole } from "@probo/helpers";
|
import { getRole } from "@probo/helpers";
|
||||||
import { CreatePeopleDialog } from "./dialogs/CreatePeopleDialog";
|
import { CreatePeopleDialog } from "./dialogs/CreatePeopleDialog";
|
||||||
import { useOrganizationId } from "/hooks/useOrganizationId";
|
import { useOrganizationId } from "/hooks/useOrganizationId";
|
||||||
|
import { Authorized } from "/permissions";
|
||||||
|
import { isAuthorized } from "/permissions";
|
||||||
|
|
||||||
type People = NodeOf<PeopleGraphPaginatedFragment$data["peoples"]>;
|
type People = NodeOf<PeopleGraphPaginatedFragment$data["peoples"]>;
|
||||||
|
|
||||||
@@ -40,11 +42,15 @@ export default function PeopleListPage({
|
|||||||
queryRef: PreloadedQuery<PeopleGraphPaginatedQuery>;
|
queryRef: PreloadedQuery<PeopleGraphPaginatedQuery>;
|
||||||
}) {
|
}) {
|
||||||
const { __ } = useTranslate();
|
const { __ } = useTranslate();
|
||||||
|
const organizationId = useOrganizationId();
|
||||||
const { people, refetch, connectionId, hasNext, loadNext, isLoadingNext } =
|
const { people, refetch, connectionId, hasNext, loadNext, isLoadingNext } =
|
||||||
usePeopleQuery(queryRef);
|
usePeopleQuery(queryRef);
|
||||||
|
|
||||||
usePageTitle(__("Members"));
|
usePageTitle(__("Members"));
|
||||||
|
|
||||||
|
const hasAnyAction = isAuthorized(organizationId, "People", "updatePeople") ||
|
||||||
|
isAuthorized(organizationId, "People", "deletePeople");
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-6">
|
<div className="space-y-6">
|
||||||
<PageHeader
|
<PageHeader
|
||||||
@@ -53,9 +59,11 @@ export default function PeopleListPage({
|
|||||||
"Keep track of your company's workforce and their progress towards completing tasks assigned to them."
|
"Keep track of your company's workforce and their progress towards completing tasks assigned to them."
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
|
<Authorized entity="Organization" action="createPeople">
|
||||||
<CreatePeopleDialog connectionId={connectionId}>
|
<CreatePeopleDialog connectionId={connectionId}>
|
||||||
<Button icon={IconPlusLarge}>{__("Add member")}</Button>
|
<Button icon={IconPlusLarge}>{__("Add member")}</Button>
|
||||||
</CreatePeopleDialog>
|
</CreatePeopleDialog>
|
||||||
|
</Authorized>
|
||||||
</PageHeader>
|
</PageHeader>
|
||||||
<SortableTable
|
<SortableTable
|
||||||
refetch={refetch}
|
refetch={refetch}
|
||||||
@@ -68,7 +76,7 @@ export default function PeopleListPage({
|
|||||||
<SortableTh field="FULL_NAME">{__("Name")}</SortableTh>
|
<SortableTh field="FULL_NAME">{__("Name")}</SortableTh>
|
||||||
<SortableTh field="KIND">{__("Role")}</SortableTh>
|
<SortableTh field="KIND">{__("Role")}</SortableTh>
|
||||||
<Th>{__("Position")}</Th>
|
<Th>{__("Position")}</Th>
|
||||||
<Th>{__("Actions")}</Th>
|
{hasAnyAction && <Th>{__("Actions")}</Th>}
|
||||||
</Tr>
|
</Tr>
|
||||||
</Thead>
|
</Thead>
|
||||||
<Tbody>
|
<Tbody>
|
||||||
@@ -77,6 +85,7 @@ export default function PeopleListPage({
|
|||||||
key={person.id}
|
key={person.id}
|
||||||
people={person}
|
people={person}
|
||||||
connectionId={connectionId}
|
connectionId={connectionId}
|
||||||
|
hasAnyAction={hasAnyAction}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
</Tbody>
|
</Tbody>
|
||||||
@@ -88,9 +97,11 @@ export default function PeopleListPage({
|
|||||||
function PeopleRow({
|
function PeopleRow({
|
||||||
people,
|
people,
|
||||||
connectionId,
|
connectionId,
|
||||||
|
hasAnyAction,
|
||||||
}: {
|
}: {
|
||||||
people: People;
|
people: People;
|
||||||
connectionId: string;
|
connectionId: string;
|
||||||
|
hasAnyAction: boolean;
|
||||||
}) {
|
}) {
|
||||||
const organizationId = useOrganizationId();
|
const organizationId = useOrganizationId();
|
||||||
const { __ } = useTranslate();
|
const { __ } = useTranslate();
|
||||||
@@ -115,8 +126,10 @@ function PeopleRow({
|
|||||||
</Td>
|
</Td>
|
||||||
<Td className="text-sm">{getRole(__, people.kind)}</Td>
|
<Td className="text-sm">{getRole(__, people.kind)}</Td>
|
||||||
<Td className="text-sm">{people.position}</Td>
|
<Td className="text-sm">{people.position}</Td>
|
||||||
|
{hasAnyAction && (
|
||||||
<Td noLink width={50} className="text-end">
|
<Td noLink width={50} className="text-end">
|
||||||
<ActionDropdown>
|
<ActionDropdown>
|
||||||
|
<Authorized entity="People" action="deletePeople">
|
||||||
<DropdownItem
|
<DropdownItem
|
||||||
icon={IconTrashCan}
|
icon={IconTrashCan}
|
||||||
variant="danger"
|
variant="danger"
|
||||||
@@ -124,8 +137,10 @@ function PeopleRow({
|
|||||||
>
|
>
|
||||||
{__("Delete")}
|
{__("Delete")}
|
||||||
</DropdownItem>
|
</DropdownItem>
|
||||||
|
</Authorized>
|
||||||
</ActionDropdown>
|
</ActionDropdown>
|
||||||
</Td>
|
</Td>
|
||||||
|
)}
|
||||||
</Tr>
|
</Tr>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import type { PeopleGraphUpdateMutation } from "/hooks/graph/__generated__/Peopl
|
|||||||
import { updatePeopleMutation } from "/hooks/graph/PeopleGraph";
|
import { updatePeopleMutation } from "/hooks/graph/PeopleGraph";
|
||||||
import { Button, Card, Field, Input } from "@probo/ui";
|
import { Button, Card, Field, Input } from "@probo/ui";
|
||||||
import { EmailsField } from "/components/form/EmailsField";
|
import { EmailsField } from "/components/form/EmailsField";
|
||||||
|
import { Authorized } from "/permissions";
|
||||||
|
|
||||||
const schema = z.object({
|
const schema = z.object({
|
||||||
fullName: z.string().min(1),
|
fullName: z.string().min(1),
|
||||||
@@ -94,9 +95,11 @@ export default function PeopleProfileTab() {
|
|||||||
</Card>
|
</Card>
|
||||||
<div className="flex justify-end">
|
<div className="flex justify-end">
|
||||||
{formState.isDirty && (
|
{formState.isDirty && (
|
||||||
|
<Authorized entity="People" action="updatePeople">
|
||||||
<Button type="submit" disabled={isMutating}>
|
<Button type="submit" disabled={isMutating}>
|
||||||
{__("Update")}
|
{__("Update")}
|
||||||
</Button>
|
</Button>
|
||||||
|
</Authorized>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
|||||||
@@ -32,6 +32,8 @@ import { CreateProcessingActivityDialog } from "./dialogs/CreateProcessingActivi
|
|||||||
import { deleteProcessingActivityMutation, ProcessingActivitiesConnectionKey } from "../../../hooks/graph/ProcessingActivityGraph";
|
import { deleteProcessingActivityMutation, ProcessingActivitiesConnectionKey } from "../../../hooks/graph/ProcessingActivityGraph";
|
||||||
import { sprintf, promisifyMutation } from "@probo/helpers";
|
import { sprintf, promisifyMutation } from "@probo/helpers";
|
||||||
import { SnapshotBanner } from "/components/SnapshotBanner";
|
import { SnapshotBanner } from "/components/SnapshotBanner";
|
||||||
|
import { Authorized } from "/permissions";
|
||||||
|
import { isAuthorized } from "/permissions";
|
||||||
import type { NodeOf } from "/types";
|
import type { NodeOf } from "/types";
|
||||||
import type { ProcessingActivitiesPageQuery } from "./__generated__/ProcessingActivitiesPageQuery.graphql";
|
import type { ProcessingActivitiesPageQuery } from "./__generated__/ProcessingActivitiesPageQuery.graphql";
|
||||||
import type {
|
import type {
|
||||||
@@ -126,6 +128,11 @@ export default function ProcessingActivitiesPage({ queryRef }: ProcessingActivit
|
|||||||
);
|
);
|
||||||
const activities = data?.processingActivities?.edges?.map((edge) => edge.node) ?? [];
|
const activities = data?.processingActivities?.edges?.map((edge) => edge.node) ?? [];
|
||||||
|
|
||||||
|
const hasAnyAction = !isSnapshotMode && (
|
||||||
|
isAuthorized(organizationId, "ProcessingActivity", "updateProcessingActivity") ||
|
||||||
|
isAuthorized(organizationId, "ProcessingActivity", "deleteProcessingActivity")
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-6">
|
<div className="space-y-6">
|
||||||
{isSnapshotMode && snapshotId && (
|
{isSnapshotMode && snapshotId && (
|
||||||
@@ -133,6 +140,7 @@ export default function ProcessingActivitiesPage({ queryRef }: ProcessingActivit
|
|||||||
)}
|
)}
|
||||||
<PageHeader title={__("Processing Activities")} description={__("Manage your processing activities under GDPR")}>
|
<PageHeader title={__("Processing Activities")} description={__("Manage your processing activities under GDPR")}>
|
||||||
{!isSnapshotMode && (
|
{!isSnapshotMode && (
|
||||||
|
<Authorized entity="Organization" action="createProcessingActivity">
|
||||||
<CreateProcessingActivityDialog
|
<CreateProcessingActivityDialog
|
||||||
organizationId={organizationId}
|
organizationId={organizationId}
|
||||||
connectionId={connectionId}
|
connectionId={connectionId}
|
||||||
@@ -141,6 +149,7 @@ export default function ProcessingActivitiesPage({ queryRef }: ProcessingActivit
|
|||||||
{__("Add processing activity")}
|
{__("Add processing activity")}
|
||||||
</Button>
|
</Button>
|
||||||
</CreateProcessingActivityDialog>
|
</CreateProcessingActivityDialog>
|
||||||
|
</Authorized>
|
||||||
)}
|
)}
|
||||||
</PageHeader>
|
</PageHeader>
|
||||||
|
|
||||||
@@ -155,7 +164,7 @@ export default function ProcessingActivitiesPage({ queryRef }: ProcessingActivit
|
|||||||
<Th>{__("Lawful Basis")}</Th>
|
<Th>{__("Lawful Basis")}</Th>
|
||||||
<Th>{__("Location")}</Th>
|
<Th>{__("Location")}</Th>
|
||||||
<Th>{__("International Transfers")}</Th>
|
<Th>{__("International Transfers")}</Th>
|
||||||
{!isSnapshotMode && <Th>{__("Actions")}</Th>}
|
{hasAnyAction && <Th>{__("Actions")}</Th>}
|
||||||
</Tr>
|
</Tr>
|
||||||
</Thead>
|
</Thead>
|
||||||
<Tbody>
|
<Tbody>
|
||||||
@@ -164,6 +173,7 @@ export default function ProcessingActivitiesPage({ queryRef }: ProcessingActivit
|
|||||||
key={activity.id}
|
key={activity.id}
|
||||||
activity={activity}
|
activity={activity}
|
||||||
connectionId={connectionId}
|
connectionId={connectionId}
|
||||||
|
hasAnyAction={hasAnyAction}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
</Tbody>
|
</Tbody>
|
||||||
@@ -200,9 +210,11 @@ export default function ProcessingActivitiesPage({ queryRef }: ProcessingActivit
|
|||||||
function ActivityRow({
|
function ActivityRow({
|
||||||
activity,
|
activity,
|
||||||
connectionId,
|
connectionId,
|
||||||
|
hasAnyAction,
|
||||||
}: {
|
}: {
|
||||||
activity: NodeOf<NonNullable<ProcessingActivitiesPageFragment$data['processingActivities']>>;
|
activity: NodeOf<NonNullable<ProcessingActivitiesPageFragment$data['processingActivities']>>;
|
||||||
connectionId: string;
|
connectionId: string;
|
||||||
|
hasAnyAction: boolean;
|
||||||
}) {
|
}) {
|
||||||
const organizationId = useOrganizationId();
|
const organizationId = useOrganizationId();
|
||||||
const { __ } = useTranslate();
|
const { __ } = useTranslate();
|
||||||
@@ -255,9 +267,10 @@ function ActivityRow({
|
|||||||
{activity.internationalTransfers ? __("Yes") : __("No")}
|
{activity.internationalTransfers ? __("Yes") : __("No")}
|
||||||
</Badge>
|
</Badge>
|
||||||
</Td>
|
</Td>
|
||||||
{!isSnapshotMode && (
|
{hasAnyAction && (
|
||||||
<Td noLink width={50} className="text-end">
|
<Td noLink width={50} className="text-end">
|
||||||
<ActionDropdown>
|
<ActionDropdown>
|
||||||
|
<Authorized entity="ProcessingActivity" action="deleteProcessingActivity">
|
||||||
<DropdownItem
|
<DropdownItem
|
||||||
icon={IconTrashCan}
|
icon={IconTrashCan}
|
||||||
variant="danger"
|
variant="danger"
|
||||||
@@ -265,6 +278,7 @@ function ActivityRow({
|
|||||||
>
|
>
|
||||||
{__("Delete")}
|
{__("Delete")}
|
||||||
</DropdownItem>
|
</DropdownItem>
|
||||||
|
</Authorized>
|
||||||
</ActionDropdown>
|
</ActionDropdown>
|
||||||
</Td>
|
</Td>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -41,6 +41,7 @@ import {
|
|||||||
} from "../../../components/form/ProcessingActivityEnumOptions";
|
} from "../../../components/form/ProcessingActivityEnumOptions";
|
||||||
|
|
||||||
import type { ProcessingActivityGraphNodeQuery } from "/hooks/graph/__generated__/ProcessingActivityGraphNodeQuery.graphql";
|
import type { ProcessingActivityGraphNodeQuery } from "/hooks/graph/__generated__/ProcessingActivityGraphNodeQuery.graphql";
|
||||||
|
import { Authorized } from "/permissions";
|
||||||
|
|
||||||
const updateProcessingActivitySchema = z.object({
|
const updateProcessingActivitySchema = z.object({
|
||||||
name: z.string().min(1, "Name is required"),
|
name: z.string().min(1, "Name is required"),
|
||||||
@@ -170,11 +171,13 @@ export default function ProcessingActivityDetailsPage(props: Props) {
|
|||||||
]}
|
]}
|
||||||
/>
|
/>
|
||||||
{!isSnapshotMode && (
|
{!isSnapshotMode && (
|
||||||
|
<Authorized entity="ProcessingActivity" action="deleteProcessingActivity">
|
||||||
<ActionDropdown>
|
<ActionDropdown>
|
||||||
<DropdownItem onClick={deleteActivity} variant="danger">
|
<DropdownItem onClick={deleteActivity} variant="danger">
|
||||||
{__("Delete")}
|
{__("Delete")}
|
||||||
</DropdownItem>
|
</DropdownItem>
|
||||||
</ActionDropdown>
|
</ActionDropdown>
|
||||||
|
</Authorized>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -407,6 +410,7 @@ export default function ProcessingActivityDetailsPage(props: Props) {
|
|||||||
|
|
||||||
{!isSnapshotMode && (
|
{!isSnapshotMode && (
|
||||||
<div className="flex justify-end pt-4">
|
<div className="flex justify-end pt-4">
|
||||||
|
<Authorized entity="ProcessingActivity" action="updateProcessingActivity">
|
||||||
<Button
|
<Button
|
||||||
type="submit"
|
type="submit"
|
||||||
variant="primary"
|
variant="primary"
|
||||||
@@ -414,6 +418,7 @@ export default function ProcessingActivityDetailsPage(props: Props) {
|
|||||||
>
|
>
|
||||||
{formState.isSubmitting ? __("Saving...") : __("Save Changes")}
|
{formState.isSubmitting ? __("Saving...") : __("Save Changes")}
|
||||||
</Button>
|
</Button>
|
||||||
|
</Authorized>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</form>
|
</form>
|
||||||
|
|||||||
@@ -30,6 +30,7 @@ import {
|
|||||||
} from "/hooks/graph/RiskGraph";
|
} from "/hooks/graph/RiskGraph";
|
||||||
import type { RiskGraphNodeQuery } from "/hooks/graph/__generated__/RiskGraphNodeQuery.graphql";
|
import type { RiskGraphNodeQuery } from "/hooks/graph/__generated__/RiskGraphNodeQuery.graphql";
|
||||||
import { SnapshotBanner } from "/components/SnapshotBanner";
|
import { SnapshotBanner } from "/components/SnapshotBanner";
|
||||||
|
import { Authorized } from "/permissions";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
queryRef: PreloadedQuery<RiskGraphNodeQuery>;
|
queryRef: PreloadedQuery<RiskGraphNodeQuery>;
|
||||||
@@ -120,6 +121,7 @@ export default function RiskDetailPage(props: Props) {
|
|||||||
/>
|
/>
|
||||||
{!isSnapshotMode && (
|
{!isSnapshotMode && (
|
||||||
<div className="flex gap-2">
|
<div className="flex gap-2">
|
||||||
|
<Authorized entity="Risk" action="updateRisk">
|
||||||
<FormRiskDialog
|
<FormRiskDialog
|
||||||
trigger={
|
trigger={
|
||||||
<Button icon={IconPencil} variant="secondary">
|
<Button icon={IconPencil} variant="secondary">
|
||||||
@@ -128,6 +130,8 @@ export default function RiskDetailPage(props: Props) {
|
|||||||
}
|
}
|
||||||
risk={{ id: riskId, ...risk }}
|
risk={{ id: riskId, ...risk }}
|
||||||
/>
|
/>
|
||||||
|
</Authorized>
|
||||||
|
<Authorized entity="Risk" action="deleteRisk">
|
||||||
<ActionDropdown variant="secondary">
|
<ActionDropdown variant="secondary">
|
||||||
<DropdownItem
|
<DropdownItem
|
||||||
variant="danger"
|
variant="danger"
|
||||||
@@ -137,6 +141,7 @@ export default function RiskDetailPage(props: Props) {
|
|||||||
{__("Delete")}
|
{__("Delete")}
|
||||||
</DropdownItem>
|
</DropdownItem>
|
||||||
</ActionDropdown>
|
</ActionDropdown>
|
||||||
|
</Authorized>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -29,6 +29,8 @@ import type { RiskGraphListQuery } from "/hooks/graph/__generated__/RiskGraphLis
|
|||||||
import type { RiskGraphFragment$data } from "/hooks/graph/__generated__/RiskGraphFragment.graphql";
|
import type { RiskGraphFragment$data } from "/hooks/graph/__generated__/RiskGraphFragment.graphql";
|
||||||
import { useParams } from "react-router";
|
import { useParams } from "react-router";
|
||||||
import { SnapshotBanner } from "/components/SnapshotBanner";
|
import { SnapshotBanner } from "/components/SnapshotBanner";
|
||||||
|
import { Authorized } from "/permissions";
|
||||||
|
import { isAuthorized } from "/permissions";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
queryRef: PreloadedQuery<RiskGraphListQuery>;
|
queryRef: PreloadedQuery<RiskGraphListQuery>;
|
||||||
@@ -54,6 +56,11 @@ export default function RisksPage(props: Props) {
|
|||||||
|
|
||||||
usePageTitle(__("Risks"));
|
usePageTitle(__("Risks"));
|
||||||
|
|
||||||
|
const hasAnyAction = !isSnapshotMode && (
|
||||||
|
isAuthorized(organizationId, "Risk", "updateRisk") ||
|
||||||
|
isAuthorized(organizationId, "Risk", "deleteRisk")
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-6">
|
<div className="space-y-6">
|
||||||
{snapshotId && <SnapshotBanner snapshotId={snapshotId} />}
|
{snapshotId && <SnapshotBanner snapshotId={snapshotId} />}
|
||||||
@@ -64,6 +71,7 @@ export default function RisksPage(props: Props) {
|
|||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{!isSnapshotMode && (
|
{!isSnapshotMode && (
|
||||||
|
<Authorized entity="Organization" action="createRisk">
|
||||||
<FormRiskDialog
|
<FormRiskDialog
|
||||||
connection={connectionId}
|
connection={connectionId}
|
||||||
onSuccess={() => {
|
onSuccess={() => {
|
||||||
@@ -71,6 +79,7 @@ export default function RisksPage(props: Props) {
|
|||||||
}}
|
}}
|
||||||
trigger={<Button icon={IconPlusLarge}>{__("New Risk")}</Button>}
|
trigger={<Button icon={IconPlusLarge}>{__("New Risk")}</Button>}
|
||||||
/>
|
/>
|
||||||
|
</Authorized>
|
||||||
)}
|
)}
|
||||||
</PageHeader>
|
</PageHeader>
|
||||||
|
|
||||||
@@ -101,7 +110,7 @@ export default function RisksPage(props: Props) {
|
|||||||
<SortableTh field="OWNER_FULL_NAME">
|
<SortableTh field="OWNER_FULL_NAME">
|
||||||
{__("Owner")}
|
{__("Owner")}
|
||||||
</SortableTh>
|
</SortableTh>
|
||||||
<Th></Th>
|
{hasAnyAction && <Th></Th>}
|
||||||
</Tr>
|
</Tr>
|
||||||
</Thead>
|
</Thead>
|
||||||
<Tbody>
|
<Tbody>
|
||||||
@@ -111,6 +120,7 @@ export default function RisksPage(props: Props) {
|
|||||||
key={risk.id}
|
key={risk.id}
|
||||||
connectionId={connectionId}
|
connectionId={connectionId}
|
||||||
organizationId={organizationId}
|
organizationId={organizationId}
|
||||||
|
hasAnyAction={hasAnyAction}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
</Tbody>
|
</Tbody>
|
||||||
@@ -123,6 +133,7 @@ type RowProps = {
|
|||||||
risk: NodeOf<RiskGraphFragment$data["risks"]>;
|
risk: NodeOf<RiskGraphFragment$data["risks"]>;
|
||||||
connectionId: string;
|
connectionId: string;
|
||||||
organizationId: string;
|
organizationId: string;
|
||||||
|
hasAnyAction: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
function RiskRow(props: RowProps) {
|
function RiskRow(props: RowProps) {
|
||||||
@@ -180,16 +191,19 @@ function RiskRow(props: RowProps) {
|
|||||||
<SeverityBadge score={risk.residualRiskScore} />
|
<SeverityBadge score={risk.residualRiskScore} />
|
||||||
</Td>
|
</Td>
|
||||||
<Td>{risk.owner?.fullName || __("Unassigned")}</Td>
|
<Td>{risk.owner?.fullName || __("Unassigned")}</Td>
|
||||||
|
{props.hasAnyAction && (
|
||||||
<Td noLink className="text-end">
|
<Td noLink className="text-end">
|
||||||
{!isSnapshotMode && (
|
|
||||||
<ActionDropdown>
|
<ActionDropdown>
|
||||||
|
<Authorized entity="Risk" action="updateRisk">
|
||||||
<DropdownItem
|
<DropdownItem
|
||||||
icon={IconPencil}
|
icon={IconPencil}
|
||||||
onClick={() => formDialogRef.current?.open()}
|
onClick={() => formDialogRef.current?.open()}
|
||||||
>
|
>
|
||||||
{__("Edit")}
|
{__("Edit")}
|
||||||
</DropdownItem>
|
</DropdownItem>
|
||||||
|
</Authorized>
|
||||||
|
|
||||||
|
<Authorized entity="Risk" action="deleteRisk">
|
||||||
<DropdownItem
|
<DropdownItem
|
||||||
variant="danger"
|
variant="danger"
|
||||||
icon={IconTrashCan}
|
icon={IconTrashCan}
|
||||||
@@ -197,9 +211,10 @@ function RiskRow(props: RowProps) {
|
|||||||
>
|
>
|
||||||
{__("Delete")}
|
{__("Delete")}
|
||||||
</DropdownItem>
|
</DropdownItem>
|
||||||
|
</Authorized>
|
||||||
</ActionDropdown>
|
</ActionDropdown>
|
||||||
)}
|
|
||||||
</Td>
|
</Td>
|
||||||
|
)}
|
||||||
</Tr>
|
</Tr>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ import { z } from "zod";
|
|||||||
import type { GeneralSettingsTabFragment$key } from "./__generated__/GeneralSettingsTabFragment.graphql";
|
import type { GeneralSettingsTabFragment$key } from "./__generated__/GeneralSettingsTabFragment.graphql";
|
||||||
import { DeleteOrganizationDialog } from "/components/organizations/DeleteOrganizationDialog";
|
import { DeleteOrganizationDialog } from "/components/organizations/DeleteOrganizationDialog";
|
||||||
import { useDeleteOrganizationMutation } from "/hooks/graph/OrganizationGraph";
|
import { useDeleteOrganizationMutation } from "/hooks/graph/OrganizationGraph";
|
||||||
|
import { isAuthorized } from "/permissions";
|
||||||
|
|
||||||
const generalSettingsTabFragment = graphql`
|
const generalSettingsTabFragment = graphql`
|
||||||
fragment GeneralSettingsTabFragment on Organization {
|
fragment GeneralSettingsTabFragment on Organization {
|
||||||
@@ -90,6 +91,9 @@ export default function GeneralSettingsTab() {
|
|||||||
const organization = useFragment(generalSettingsTabFragment, organizationKey);
|
const organization = useFragment(generalSettingsTabFragment, organizationKey);
|
||||||
const deleteDialogRef = useDialogRef();
|
const deleteDialogRef = useDialogRef();
|
||||||
|
|
||||||
|
const canUpdate = isAuthorized(organization.id, "Organization", "updateOrganization");
|
||||||
|
const canDelete = isAuthorized(organization.id, "Organization", "deleteOrganization");
|
||||||
|
|
||||||
const [logoPreview, setLogoPreview] = useState<string | null>(null);
|
const [logoPreview, setLogoPreview] = useState<string | null>(null);
|
||||||
const [horizontalLogoPreview, setHorizontalLogoPreview] = useState<
|
const [horizontalLogoPreview, setHorizontalLogoPreview] = useState<
|
||||||
string | null
|
string | null
|
||||||
@@ -267,6 +271,7 @@ export default function GeneralSettingsTab() {
|
|||||||
name={organization.name}
|
name={organization.name}
|
||||||
size="xl"
|
size="xl"
|
||||||
/>
|
/>
|
||||||
|
{canUpdate && (
|
||||||
<FileButton
|
<FileButton
|
||||||
disabled={formState.isSubmitting || isUpdatingOrganization}
|
disabled={formState.isSubmitting || isUpdatingOrganization}
|
||||||
onChange={handleLogoChange}
|
onChange={handleLogoChange}
|
||||||
@@ -278,6 +283,7 @@ export default function GeneralSettingsTab() {
|
|||||||
? __("Uploading...")
|
? __("Uploading...")
|
||||||
: __("Change logo")}
|
: __("Change logo")}
|
||||||
</FileButton>
|
</FileButton>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
@@ -301,6 +307,7 @@ export default function GeneralSettingsTab() {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
{canUpdate && (
|
||||||
<FileButton
|
<FileButton
|
||||||
disabled={formState.isSubmitting || isUpdatingOrganization}
|
disabled={formState.isSubmitting || isUpdatingOrganization}
|
||||||
onChange={handleHorizontalLogoChange}
|
onChange={handleHorizontalLogoChange}
|
||||||
@@ -313,7 +320,8 @@ export default function GeneralSettingsTab() {
|
|||||||
? __("Change horizontal logo")
|
? __("Change horizontal logo")
|
||||||
: __("Upload horizontal logo")}
|
: __("Upload horizontal logo")}
|
||||||
</FileButton>
|
</FileButton>
|
||||||
{organization.horizontalLogoUrl && (
|
)}
|
||||||
|
{canUpdate && organization.horizontalLogoUrl && (
|
||||||
<Dialog
|
<Dialog
|
||||||
ref={deleteDialogRef}
|
ref={deleteDialogRef}
|
||||||
trigger={
|
trigger={
|
||||||
@@ -357,7 +365,7 @@ export default function GeneralSettingsTab() {
|
|||||||
</div>
|
</div>
|
||||||
<Field
|
<Field
|
||||||
{...register("name")}
|
{...register("name")}
|
||||||
readOnly={formState.isSubmitting}
|
readOnly={formState.isSubmitting || !canUpdate}
|
||||||
name="name"
|
name="name"
|
||||||
type="text"
|
type="text"
|
||||||
label={__("Organization name")}
|
label={__("Organization name")}
|
||||||
@@ -367,7 +375,7 @@ export default function GeneralSettingsTab() {
|
|||||||
<Label>{__("Description")}</Label>
|
<Label>{__("Description")}</Label>
|
||||||
<Textarea
|
<Textarea
|
||||||
{...register("description")}
|
{...register("description")}
|
||||||
readOnly={formState.isSubmitting}
|
readOnly={formState.isSubmitting || !canUpdate}
|
||||||
name="description"
|
name="description"
|
||||||
placeholder={__("Brief description of your organization")}
|
placeholder={__("Brief description of your organization")}
|
||||||
rows={3}
|
rows={3}
|
||||||
@@ -376,7 +384,7 @@ export default function GeneralSettingsTab() {
|
|||||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||||
<Field
|
<Field
|
||||||
{...register("websiteUrl")}
|
{...register("websiteUrl")}
|
||||||
readOnly={formState.isSubmitting}
|
readOnly={formState.isSubmitting || !canUpdate}
|
||||||
name="websiteUrl"
|
name="websiteUrl"
|
||||||
type="url"
|
type="url"
|
||||||
label={__("Website URL")}
|
label={__("Website URL")}
|
||||||
@@ -384,7 +392,7 @@ export default function GeneralSettingsTab() {
|
|||||||
/>
|
/>
|
||||||
<Field
|
<Field
|
||||||
{...register("email")}
|
{...register("email")}
|
||||||
readOnly={formState.isSubmitting}
|
readOnly={formState.isSubmitting || !canUpdate}
|
||||||
name="email"
|
name="email"
|
||||||
type="email"
|
type="email"
|
||||||
label={__("Email")}
|
label={__("Email")}
|
||||||
@@ -395,13 +403,13 @@ export default function GeneralSettingsTab() {
|
|||||||
<Label>{__("Headquarter Address")}</Label>
|
<Label>{__("Headquarter Address")}</Label>
|
||||||
<Textarea
|
<Textarea
|
||||||
{...register("headquarterAddress")}
|
{...register("headquarterAddress")}
|
||||||
readOnly={formState.isSubmitting}
|
readOnly={formState.isSubmitting || !canUpdate}
|
||||||
name="headquarterAddress"
|
name="headquarterAddress"
|
||||||
placeholder={__("123 Main St, City, Country")}
|
placeholder={__("123 Main St, City, Country")}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{formState.isDirty && (
|
{formState.isDirty && canUpdate && (
|
||||||
<div className="flex justify-end pt-6">
|
<div className="flex justify-end pt-6">
|
||||||
<Button
|
<Button
|
||||||
type="submit"
|
type="submit"
|
||||||
@@ -416,6 +424,7 @@ export default function GeneralSettingsTab() {
|
|||||||
</Card>
|
</Card>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{canDelete && (
|
||||||
<div className="space-y-4 mt-12">
|
<div className="space-y-4 mt-12">
|
||||||
<h2 className="text-base font-medium text-red-600">
|
<h2 className="text-base font-medium text-red-600">
|
||||||
{__("Danger Zone")}
|
{__("Danger Zone")}
|
||||||
@@ -447,6 +456,7 @@ export default function GeneralSettingsTab() {
|
|||||||
</DeleteOrganizationDialog>
|
</DeleteOrganizationDialog>
|
||||||
</Card>
|
</Card>
|
||||||
</div>
|
</div>
|
||||||
|
)}
|
||||||
</form>
|
</form>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,11 +1,18 @@
|
|||||||
import { useState } from "react";
|
import { useState, Suspense } from "react";
|
||||||
import { useOutletContext } from "react-router";
|
import { useOutletContext } from "react-router";
|
||||||
import { usePaginationFragment, graphql } from "react-relay";
|
import { usePaginationFragment, graphql } from "react-relay";
|
||||||
import {
|
import {
|
||||||
Badge,
|
Badge,
|
||||||
Button,
|
Button,
|
||||||
Card,
|
Card,
|
||||||
|
Dialog,
|
||||||
|
DialogContent,
|
||||||
|
DialogFooter,
|
||||||
|
Field,
|
||||||
|
IconPencil,
|
||||||
IconTrashCan,
|
IconTrashCan,
|
||||||
|
Option,
|
||||||
|
Select,
|
||||||
Spinner,
|
Spinner,
|
||||||
TabBadge,
|
TabBadge,
|
||||||
TabItem,
|
TabItem,
|
||||||
@@ -16,6 +23,7 @@ import {
|
|||||||
Thead,
|
Thead,
|
||||||
Tr,
|
Tr,
|
||||||
useConfirm,
|
useConfirm,
|
||||||
|
useDialogRef,
|
||||||
} from "@probo/ui";
|
} from "@probo/ui";
|
||||||
import { useTranslate } from "@probo/i18n";
|
import { useTranslate } from "@probo/i18n";
|
||||||
import { SortableTable, SortableTh } from "/components/SortableTable";
|
import { SortableTable, SortableTh } from "/components/SortableTable";
|
||||||
@@ -24,6 +32,8 @@ import { useMutationWithToasts } from "/hooks/useMutationWithToasts";
|
|||||||
import { sprintf } from "@probo/helpers";
|
import { sprintf } from "@probo/helpers";
|
||||||
import clsx from "clsx";
|
import clsx from "clsx";
|
||||||
import type { NodeOf } from "/types";
|
import type { NodeOf } from "/types";
|
||||||
|
import { Authorized } from "/permissions";
|
||||||
|
import { getAssignableRoles, getUserRole } from "/permissions";
|
||||||
import type {
|
import type {
|
||||||
MembersSettingsTabMembershipsFragment$data,
|
MembersSettingsTabMembershipsFragment$data,
|
||||||
MembersSettingsTabMembershipsFragment$key
|
MembersSettingsTabMembershipsFragment$key
|
||||||
@@ -112,6 +122,19 @@ const removeMemberMutation = graphql`
|
|||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
const updateMembershipMutation = graphql`
|
||||||
|
mutation MembersSettingsTab_UpdateMembershipMutation(
|
||||||
|
$input: UpdateMembershipInput!
|
||||||
|
) {
|
||||||
|
updateMembership(input: $input) {
|
||||||
|
membership {
|
||||||
|
id
|
||||||
|
role
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
const deleteInvitationMutation = graphql`
|
const deleteInvitationMutation = graphql`
|
||||||
mutation MembersSettingsTab_DeleteInvitationMutation(
|
mutation MembersSettingsTab_DeleteInvitationMutation(
|
||||||
$input: DeleteInvitationInput!
|
$input: DeleteInvitationInput!
|
||||||
@@ -157,12 +180,14 @@ export default function MembersSettingsTab() {
|
|||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<h2 className="text-base font-medium">{__("Workspace members")}</h2>
|
<h2 className="text-base font-medium">{__("Workspace members")}</h2>
|
||||||
|
<Authorized entity="Organization" action="inviteUser">
|
||||||
<InviteUserDialog
|
<InviteUserDialog
|
||||||
connectionId={invitationsPagination.data.invitations?.__id}
|
connectionId={invitationsPagination.data.invitations?.__id}
|
||||||
onRefetch={refetchInvitations}
|
onRefetch={refetchInvitations}
|
||||||
>
|
>
|
||||||
<Button variant="secondary">{__("Invite member")}</Button>
|
<Button variant="secondary">{__("Invite member")}</Button>
|
||||||
</InviteUserDialog>
|
</InviteUserDialog>
|
||||||
|
</Authorized>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Tabs>
|
<Tabs>
|
||||||
@@ -351,6 +376,7 @@ function InvitationRow(props: {
|
|||||||
{isDeleting ? (
|
{isDeleting ? (
|
||||||
<Spinner size={16} />
|
<Spinner size={16} />
|
||||||
) : (
|
) : (
|
||||||
|
<Authorized entity="Organization" action="deleteInvitation">
|
||||||
<Button
|
<Button
|
||||||
variant="danger"
|
variant="danger"
|
||||||
onClick={onDelete}
|
onClick={onDelete}
|
||||||
@@ -358,6 +384,7 @@ function InvitationRow(props: {
|
|||||||
icon={IconTrashCan}
|
icon={IconTrashCan}
|
||||||
aria-label={__("Delete invitation")}
|
aria-label={__("Delete invitation")}
|
||||||
/>
|
/>
|
||||||
|
</Authorized>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</Td>
|
</Td>
|
||||||
@@ -365,19 +392,33 @@ function InvitationRow(props: {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function MembershipRow(props: {
|
function MembershipRowContent(props: {
|
||||||
membership: NodeOf<MembersSettingsTabMembershipsFragment$data["memberships"]>;
|
membership: NodeOf<MembersSettingsTabMembershipsFragment$data["memberships"]>;
|
||||||
connectionId?: string;
|
connectionId?: string;
|
||||||
organizationId: string;
|
organizationId: string;
|
||||||
onRefetch: () => void;
|
onRefetch: () => void;
|
||||||
}) {
|
}) {
|
||||||
const { __ } = useTranslate();
|
const { __ } = useTranslate();
|
||||||
|
const availableRoles = getAssignableRoles(props.organizationId);
|
||||||
|
const currentUserRole = getUserRole(props.organizationId);
|
||||||
|
|
||||||
const [removeMember, isRemoving] = useMutationWithToasts(removeMemberMutation, {
|
const [removeMember, isRemoving] = useMutationWithToasts(removeMemberMutation, {
|
||||||
successMessage: __("Member removed successfully"),
|
successMessage: __("Member removed successfully"),
|
||||||
errorMessage: __("Failed to remove member"),
|
errorMessage: __("Failed to remove member"),
|
||||||
});
|
});
|
||||||
|
const [updateMembership, isUpdating] = useMutationWithToasts(updateMembershipMutation, {
|
||||||
|
successMessage: __("Role updated successfully"),
|
||||||
|
errorMessage: __("Failed to update role"),
|
||||||
|
});
|
||||||
const confirm = useConfirm();
|
const confirm = useConfirm();
|
||||||
|
const editDialogRef = useDialogRef();
|
||||||
const [isRemoved, setIsRemoved] = useState(false);
|
const [isRemoved, setIsRemoved] = useState(false);
|
||||||
|
const [selectedRole, setSelectedRole] = useState<string>(props.membership.role);
|
||||||
|
|
||||||
|
// Only OWNER can edit OWNER members
|
||||||
|
const canEditThisRole = props.membership.role === "OWNER"
|
||||||
|
? currentUserRole === "OWNER"
|
||||||
|
: true;
|
||||||
|
|
||||||
if (isRemoved) {
|
if (isRemoved) {
|
||||||
return null;
|
return null;
|
||||||
@@ -409,7 +450,29 @@ function MembershipRow(props: {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleEditClick = () => {
|
||||||
|
setSelectedRole(props.membership.role);
|
||||||
|
editDialogRef.current?.open();
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleUpdateRole = (e: React.FormEvent) => {
|
||||||
|
e.preventDefault();
|
||||||
|
updateMembership({
|
||||||
|
variables: {
|
||||||
|
input: {
|
||||||
|
memberId: props.membership.id,
|
||||||
|
organizationId: props.organizationId,
|
||||||
|
role: selectedRole,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
onCompleted: () => {
|
||||||
|
editDialogRef.current?.close();
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
<>
|
||||||
<Tr className={clsx(isRemoving && "opacity-60 pointer-events-none")}>
|
<Tr className={clsx(isRemoving && "opacity-60 pointer-events-none")}>
|
||||||
<Td>
|
<Td>
|
||||||
<div className="font-semibold">{props.membership.fullName}</div>
|
<div className="font-semibold">{props.membership.fullName}</div>
|
||||||
@@ -426,14 +489,27 @@ function MembershipRow(props: {
|
|||||||
<Badge>{props.membership.role}</Badge>
|
<Badge>{props.membership.role}</Badge>
|
||||||
</Td>
|
</Td>
|
||||||
<Td>{new Date(props.membership.createdAt).toLocaleDateString()}</Td>
|
<Td>{new Date(props.membership.createdAt).toLocaleDateString()}</Td>
|
||||||
<Td noLink width={80} className="text-end">
|
<Td noLink width={160} className="text-end">
|
||||||
<div
|
<div
|
||||||
className="flex gap-2 justify-end"
|
className="flex gap-2 justify-end"
|
||||||
onClick={(e) => e.stopPropagation()}
|
onClick={(e) => e.stopPropagation()}
|
||||||
>
|
>
|
||||||
|
<Authorized entity="Organization" action="updateMembership">
|
||||||
|
{canEditThisRole && (
|
||||||
|
<Button
|
||||||
|
variant="secondary"
|
||||||
|
onClick={handleEditClick}
|
||||||
|
disabled={isUpdating}
|
||||||
|
icon={IconPencil}
|
||||||
|
aria-label={__("Edit role")}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</Authorized>
|
||||||
{isRemoving ? (
|
{isRemoving ? (
|
||||||
<Spinner size={16} />
|
<Spinner size={16} />
|
||||||
) : (
|
) : (
|
||||||
|
<Authorized entity="Organization" action="removeMember">
|
||||||
|
{canEditThisRole && (
|
||||||
<Button
|
<Button
|
||||||
variant="danger"
|
variant="danger"
|
||||||
onClick={onRemove}
|
onClick={onRemove}
|
||||||
@@ -442,8 +518,69 @@ function MembershipRow(props: {
|
|||||||
aria-label={__("Remove member")}
|
aria-label={__("Remove member")}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
</Authorized>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</Td>
|
</Td>
|
||||||
</Tr>
|
</Tr>
|
||||||
|
|
||||||
|
<Dialog ref={editDialogRef} title={__("Edit Member Role")}>
|
||||||
|
<form onSubmit={handleUpdateRole}>
|
||||||
|
<DialogContent padded className="space-y-6">
|
||||||
|
<div>
|
||||||
|
<p className="text-txt-secondary text-sm mb-4">
|
||||||
|
{sprintf(__("Update the role for %s"), props.membership.fullName)}
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<Field label={__("Role")} required>
|
||||||
|
<Select value={selectedRole} onValueChange={setSelectedRole}>
|
||||||
|
{availableRoles.includes("OWNER") && <Option value="OWNER">{__("Owner")}</Option>}
|
||||||
|
{availableRoles.includes("ADMIN") && <Option value="ADMIN">{__("Admin")}</Option>}
|
||||||
|
{availableRoles.includes("VIEWER") && <Option value="VIEWER">{__("Viewer")}</Option>}
|
||||||
|
</Select>
|
||||||
|
</Field>
|
||||||
|
|
||||||
|
<div className="mt-4 space-y-2 text-sm text-txt-tertiary">
|
||||||
|
{selectedRole === "OWNER" && (
|
||||||
|
<p>{__("Full access to everything")}</p>
|
||||||
|
)}
|
||||||
|
{selectedRole === "ADMIN" && (
|
||||||
|
<p>{__("Full access except organization setup and API keys")}</p>
|
||||||
|
)}
|
||||||
|
{selectedRole === "VIEWER" && (
|
||||||
|
<p>{__("Read-only access")}</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</DialogContent>
|
||||||
|
<DialogFooter>
|
||||||
|
<Button type="submit" disabled={isUpdating || selectedRole === props.membership.role}>
|
||||||
|
{isUpdating && <Spinner />}
|
||||||
|
{__("Update Role")}
|
||||||
|
</Button>
|
||||||
|
</DialogFooter>
|
||||||
|
</form>
|
||||||
|
</Dialog>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function MembershipRow(props: {
|
||||||
|
membership: NodeOf<MembersSettingsTabMembershipsFragment$data["memberships"]>;
|
||||||
|
connectionId?: string;
|
||||||
|
organizationId: string;
|
||||||
|
onRefetch: () => void;
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<Suspense fallback={
|
||||||
|
<Tr>
|
||||||
|
<Td><Spinner size={16} /></Td>
|
||||||
|
<Td></Td>
|
||||||
|
<Td></Td>
|
||||||
|
<Td></Td>
|
||||||
|
</Tr>
|
||||||
|
}>
|
||||||
|
<MembershipRowContent {...props} />
|
||||||
|
</Suspense>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -37,6 +37,7 @@ import {
|
|||||||
useVerifyDomainMutation,
|
useVerifyDomainMutation,
|
||||||
} from "/hooks/graph/SAMLConfigurationGraph";
|
} from "/hooks/graph/SAMLConfigurationGraph";
|
||||||
import type { SAMLSettingsTabFragment$key } from "./__generated__/SAMLSettingsTabFragment.graphql";
|
import type { SAMLSettingsTabFragment$key } from "./__generated__/SAMLSettingsTabFragment.graphql";
|
||||||
|
import { Authorized } from "/permissions";
|
||||||
|
|
||||||
const samlSettingsTabFragment = graphql`
|
const samlSettingsTabFragment = graphql`
|
||||||
fragment SAMLSettingsTabFragment on Organization {
|
fragment SAMLSettingsTabFragment on Organization {
|
||||||
@@ -371,9 +372,11 @@ export default function SAMLSettingsTab() {
|
|||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
<div className="flex justify-between items-center">
|
<div className="flex justify-between items-center">
|
||||||
<h2 className="text-base font-medium">{__("SAML Single Sign-On")}</h2>
|
<h2 className="text-base font-medium">{__("SAML Single Sign-On")}</h2>
|
||||||
|
<Authorized entity="Organization" action="createSAMLConfiguration">
|
||||||
<Button onClick={() => handleOpenModal()}>
|
<Button onClick={() => handleOpenModal()}>
|
||||||
{__("Add Configuration")}
|
{__("Add Configuration")}
|
||||||
</Button>
|
</Button>
|
||||||
|
</Authorized>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{configs.length === 0 ? (
|
{configs.length === 0 ? (
|
||||||
@@ -385,9 +388,11 @@ export default function SAMLSettingsTab() {
|
|||||||
<p className="text-gray-600 mb-6">
|
<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.")}
|
{__("Set up SAML 2.0 single sign-on for your organization by adding a configuration for each email domain.")}
|
||||||
</p>
|
</p>
|
||||||
|
<Authorized entity="Organization" action="createSAMLConfiguration">
|
||||||
<Button onClick={() => handleOpenModal()}>
|
<Button onClick={() => handleOpenModal()}>
|
||||||
{__("Add Your First Configuration")}
|
{__("Add Your First Configuration")}
|
||||||
</Button>
|
</Button>
|
||||||
|
</Authorized>
|
||||||
</div>
|
</div>
|
||||||
</Card>
|
</Card>
|
||||||
) : (
|
) : (
|
||||||
@@ -452,6 +457,7 @@ export default function SAMLSettingsTab() {
|
|||||||
<div className="flex gap-2 justify-end">
|
<div className="flex gap-2 justify-end">
|
||||||
{config.domainVerified ? (
|
{config.domainVerified ? (
|
||||||
<>
|
<>
|
||||||
|
<Authorized entity="SAMLConfiguration" action="updateSAMLConfiguration">
|
||||||
<Button
|
<Button
|
||||||
variant={config.enabled ? "danger" : "primary"}
|
variant={config.enabled ? "danger" : "primary"}
|
||||||
onClick={() => handleToggleEnabled(config)}
|
onClick={() => handleToggleEnabled(config)}
|
||||||
@@ -459,27 +465,34 @@ export default function SAMLSettingsTab() {
|
|||||||
>
|
>
|
||||||
{config.enabled ? __("Disable") : __("Enable")}
|
{config.enabled ? __("Disable") : __("Enable")}
|
||||||
</Button>
|
</Button>
|
||||||
|
</Authorized>
|
||||||
|
<Authorized entity="SAMLConfiguration" action="updateSAMLConfiguration">
|
||||||
<Button
|
<Button
|
||||||
variant="secondary"
|
variant="secondary"
|
||||||
onClick={() => handleOpenModal(config)}
|
onClick={() => handleOpenModal(config)}
|
||||||
>
|
>
|
||||||
{__("Edit")}
|
{__("Edit")}
|
||||||
</Button>
|
</Button>
|
||||||
|
</Authorized>
|
||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
|
<Authorized entity="Organization" action="verifyDomain">
|
||||||
<Button
|
<Button
|
||||||
variant="primary"
|
variant="primary"
|
||||||
onClick={() => handleOpenModal(config)}
|
onClick={() => handleOpenModal(config)}
|
||||||
>
|
>
|
||||||
{__("Verify Domain")}
|
{__("Verify Domain")}
|
||||||
</Button>
|
</Button>
|
||||||
|
</Authorized>
|
||||||
|
<Authorized entity="Organization" action="deleteOrganization">
|
||||||
<Button
|
<Button
|
||||||
variant="danger"
|
variant="danger"
|
||||||
onClick={() => handleDelete(config)}
|
onClick={() => handleDelete(config)}
|
||||||
>
|
>
|
||||||
{__("Delete")}
|
{__("Delete")}
|
||||||
</Button>
|
</Button>
|
||||||
|
</Authorized>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
/**
|
/**
|
||||||
* @generated SignedSource<<8df0455e495843c9db156c36f38c97d6>>
|
* @generated SignedSource<<0a1a073bbc42108dd13e235594c27457>>
|
||||||
* @lightSyntaxTransform
|
* @lightSyntaxTransform
|
||||||
* @nogrep
|
* @nogrep
|
||||||
*/
|
*/
|
||||||
@@ -10,7 +10,7 @@
|
|||||||
|
|
||||||
import { ReaderFragment } from 'relay-runtime';
|
import { ReaderFragment } from 'relay-runtime';
|
||||||
export type InvitationStatus = "ACCEPTED" | "EXPIRED" | "PENDING";
|
export type InvitationStatus = "ACCEPTED" | "EXPIRED" | "PENDING";
|
||||||
export type Role = "ADMIN" | "MEMBER" | "OWNER" | "VIEWER";
|
export type MembershipRole = "ADMIN" | "OWNER" | "VIEWER";
|
||||||
import { FragmentRefs } from "relay-runtime";
|
import { FragmentRefs } from "relay-runtime";
|
||||||
export type MembersSettingsTabInvitationsFragment$data = {
|
export type MembersSettingsTabInvitationsFragment$data = {
|
||||||
readonly id: string;
|
readonly id: string;
|
||||||
@@ -24,7 +24,7 @@ export type MembersSettingsTabInvitationsFragment$data = {
|
|||||||
readonly expiresAt: any;
|
readonly expiresAt: any;
|
||||||
readonly fullName: string;
|
readonly fullName: string;
|
||||||
readonly id: string;
|
readonly id: string;
|
||||||
readonly role: Role;
|
readonly role: MembershipRole;
|
||||||
readonly status: InvitationStatus;
|
readonly status: InvitationStatus;
|
||||||
};
|
};
|
||||||
}>;
|
}>;
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
/**
|
/**
|
||||||
* @generated SignedSource<<ff12447635b4a42587b7d7de6ea1b4e1>>
|
* @generated SignedSource<<752dcbc2d152883fa2ac09b52039f01c>>
|
||||||
* @lightSyntaxTransform
|
* @lightSyntaxTransform
|
||||||
* @nogrep
|
* @nogrep
|
||||||
*/
|
*/
|
||||||
@@ -9,7 +9,7 @@
|
|||||||
// @ts-nocheck
|
// @ts-nocheck
|
||||||
|
|
||||||
import { ReaderFragment } from 'relay-runtime';
|
import { ReaderFragment } from 'relay-runtime';
|
||||||
export type Role = "ADMIN" | "MEMBER" | "OWNER" | "VIEWER";
|
export type MembershipRole = "ADMIN" | "OWNER" | "VIEWER";
|
||||||
export type UserAuthMethod = "PASSWORD" | "SAML";
|
export type UserAuthMethod = "PASSWORD" | "SAML";
|
||||||
import { FragmentRefs } from "relay-runtime";
|
import { FragmentRefs } from "relay-runtime";
|
||||||
export type MembersSettingsTabMembershipsFragment$data = {
|
export type MembersSettingsTabMembershipsFragment$data = {
|
||||||
@@ -23,7 +23,7 @@ export type MembersSettingsTabMembershipsFragment$data = {
|
|||||||
readonly emailAddress: string;
|
readonly emailAddress: string;
|
||||||
readonly fullName: string;
|
readonly fullName: string;
|
||||||
readonly id: string;
|
readonly id: string;
|
||||||
readonly role: Role;
|
readonly role: MembershipRole;
|
||||||
};
|
};
|
||||||
}>;
|
}>;
|
||||||
readonly totalCount: number;
|
readonly totalCount: number;
|
||||||
|
|||||||
@@ -0,0 +1,116 @@
|
|||||||
|
/**
|
||||||
|
* @generated SignedSource<<562744e07bdb502c26aebe267b181b0e>>
|
||||||
|
* @lightSyntaxTransform
|
||||||
|
* @nogrep
|
||||||
|
*/
|
||||||
|
|
||||||
|
/* tslint:disable */
|
||||||
|
/* eslint-disable */
|
||||||
|
// @ts-nocheck
|
||||||
|
|
||||||
|
import { ConcreteRequest } from 'relay-runtime';
|
||||||
|
export type MembershipRole = "ADMIN" | "OWNER" | "VIEWER";
|
||||||
|
export type UpdateMembershipInput = {
|
||||||
|
memberId: string;
|
||||||
|
organizationId: string;
|
||||||
|
role: MembershipRole;
|
||||||
|
};
|
||||||
|
export type MembersSettingsTab_UpdateMembershipMutation$variables = {
|
||||||
|
input: UpdateMembershipInput;
|
||||||
|
};
|
||||||
|
export type MembersSettingsTab_UpdateMembershipMutation$data = {
|
||||||
|
readonly updateMembership: {
|
||||||
|
readonly membership: {
|
||||||
|
readonly id: string;
|
||||||
|
readonly role: MembershipRole;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
export type MembersSettingsTab_UpdateMembershipMutation = {
|
||||||
|
response: MembersSettingsTab_UpdateMembershipMutation$data;
|
||||||
|
variables: MembersSettingsTab_UpdateMembershipMutation$variables;
|
||||||
|
};
|
||||||
|
|
||||||
|
const node: ConcreteRequest = (function(){
|
||||||
|
var v0 = [
|
||||||
|
{
|
||||||
|
"defaultValue": null,
|
||||||
|
"kind": "LocalArgument",
|
||||||
|
"name": "input"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
v1 = [
|
||||||
|
{
|
||||||
|
"alias": null,
|
||||||
|
"args": [
|
||||||
|
{
|
||||||
|
"kind": "Variable",
|
||||||
|
"name": "input",
|
||||||
|
"variableName": "input"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"concreteType": "UpdateMembershipPayload",
|
||||||
|
"kind": "LinkedField",
|
||||||
|
"name": "updateMembership",
|
||||||
|
"plural": false,
|
||||||
|
"selections": [
|
||||||
|
{
|
||||||
|
"alias": null,
|
||||||
|
"args": null,
|
||||||
|
"concreteType": "Membership",
|
||||||
|
"kind": "LinkedField",
|
||||||
|
"name": "membership",
|
||||||
|
"plural": false,
|
||||||
|
"selections": [
|
||||||
|
{
|
||||||
|
"alias": null,
|
||||||
|
"args": null,
|
||||||
|
"kind": "ScalarField",
|
||||||
|
"name": "id",
|
||||||
|
"storageKey": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"alias": null,
|
||||||
|
"args": null,
|
||||||
|
"kind": "ScalarField",
|
||||||
|
"name": "role",
|
||||||
|
"storageKey": null
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"storageKey": null
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"storageKey": null
|
||||||
|
}
|
||||||
|
];
|
||||||
|
return {
|
||||||
|
"fragment": {
|
||||||
|
"argumentDefinitions": (v0/*: any*/),
|
||||||
|
"kind": "Fragment",
|
||||||
|
"metadata": null,
|
||||||
|
"name": "MembersSettingsTab_UpdateMembershipMutation",
|
||||||
|
"selections": (v1/*: any*/),
|
||||||
|
"type": "Mutation",
|
||||||
|
"abstractKey": null
|
||||||
|
},
|
||||||
|
"kind": "Request",
|
||||||
|
"operation": {
|
||||||
|
"argumentDefinitions": (v0/*: any*/),
|
||||||
|
"kind": "Operation",
|
||||||
|
"name": "MembersSettingsTab_UpdateMembershipMutation",
|
||||||
|
"selections": (v1/*: any*/)
|
||||||
|
},
|
||||||
|
"params": {
|
||||||
|
"cacheID": "675437becfdd9cc5ea20d2b40b9ace37",
|
||||||
|
"id": null,
|
||||||
|
"metadata": {},
|
||||||
|
"name": "MembersSettingsTab_UpdateMembershipMutation",
|
||||||
|
"operationKind": "mutation",
|
||||||
|
"text": "mutation MembersSettingsTab_UpdateMembershipMutation(\n $input: UpdateMembershipInput!\n) {\n updateMembership(input: $input) {\n membership {\n id\n role\n }\n }\n}\n"
|
||||||
|
}
|
||||||
|
};
|
||||||
|
})();
|
||||||
|
|
||||||
|
(node as any).hash = "29ead2b06842cc8ed98bbea1cf6c1bed";
|
||||||
|
|
||||||
|
export default node;
|
||||||
@@ -34,6 +34,8 @@ import type { NodeOf } from "/types";
|
|||||||
import SnapshotFormDialog from "./dialog/SnapshotFormDialog";
|
import SnapshotFormDialog from "./dialog/SnapshotFormDialog";
|
||||||
import { usePageTitle } from "@probo/hooks";
|
import { usePageTitle } from "@probo/hooks";
|
||||||
import { useOrganizationId } from "/hooks/useOrganizationId";
|
import { useOrganizationId } from "/hooks/useOrganizationId";
|
||||||
|
import { Authorized } from "/permissions";
|
||||||
|
import { isAuthorized } from "/permissions";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
queryRef: PreloadedQuery<SnapshotGraphListQuery>;
|
queryRef: PreloadedQuery<SnapshotGraphListQuery>;
|
||||||
@@ -71,6 +73,8 @@ export default function SnapshotsPage(props: Props) {
|
|||||||
const snapshots = data.snapshots.edges.map((edge) => edge.node);
|
const snapshots = data.snapshots.edges.map((edge) => edge.node);
|
||||||
usePageTitle(__("Snapshots"));
|
usePageTitle(__("Snapshots"));
|
||||||
|
|
||||||
|
const hasAnyAction = isAuthorized(organizationId, "Snapshot", "deleteSnapshot");
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-6">
|
<div className="space-y-6">
|
||||||
<PageHeader
|
<PageHeader
|
||||||
@@ -79,11 +83,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."
|
"Snapshots capture point-in-time views of your organization's compliance state. Create snapshots to track progress over time."
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
|
<Authorized entity="Organization" action="createSnapshot">
|
||||||
<SnapshotFormDialog connection={connectionId}>
|
<SnapshotFormDialog connection={connectionId}>
|
||||||
<Button variant="primary" icon={IconPlusLarge}>
|
<Button variant="primary" icon={IconPlusLarge}>
|
||||||
{__("New snapshot")}
|
{__("New snapshot")}
|
||||||
</Button>
|
</Button>
|
||||||
</SnapshotFormDialog>
|
</SnapshotFormDialog>
|
||||||
|
</Authorized>
|
||||||
</PageHeader>
|
</PageHeader>
|
||||||
|
|
||||||
{snapshots.length > 0 ? (
|
{snapshots.length > 0 ? (
|
||||||
@@ -94,7 +100,7 @@ export default function SnapshotsPage(props: Props) {
|
|||||||
<Th>{__("Type")}</Th>
|
<Th>{__("Type")}</Th>
|
||||||
<Th>{__("Description")}</Th>
|
<Th>{__("Description")}</Th>
|
||||||
<Th>{__("Created")}</Th>
|
<Th>{__("Created")}</Th>
|
||||||
<Th></Th>
|
{hasAnyAction && <Th></Th>}
|
||||||
</Tr>
|
</Tr>
|
||||||
</Thead>
|
</Thead>
|
||||||
<Tbody>
|
<Tbody>
|
||||||
@@ -104,6 +110,7 @@ export default function SnapshotsPage(props: Props) {
|
|||||||
snapshot={snapshot}
|
snapshot={snapshot}
|
||||||
connectionId={connectionId}
|
connectionId={connectionId}
|
||||||
organizationId={organizationId}
|
organizationId={organizationId}
|
||||||
|
hasAnyAction={hasAnyAction}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
</Tbody>
|
</Tbody>
|
||||||
@@ -126,6 +133,7 @@ type SnapshotRowProps = {
|
|||||||
snapshot: NodeOf<SnapshotsPageFragment$data["snapshots"]>;
|
snapshot: NodeOf<SnapshotsPageFragment$data["snapshots"]>;
|
||||||
connectionId: string;
|
connectionId: string;
|
||||||
organizationId: string;
|
organizationId: string;
|
||||||
|
hasAnyAction: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
function SnapshotRow(props: SnapshotRowProps) {
|
function SnapshotRow(props: SnapshotRowProps) {
|
||||||
@@ -148,8 +156,10 @@ function SnapshotRow(props: SnapshotRowProps) {
|
|||||||
<Td className="text-txt-tertiary">
|
<Td className="text-txt-tertiary">
|
||||||
{formatDate(props.snapshot.createdAt)}
|
{formatDate(props.snapshot.createdAt)}
|
||||||
</Td>
|
</Td>
|
||||||
|
{props.hasAnyAction && (
|
||||||
<Td noLink width={50} className="text-end">
|
<Td noLink width={50} className="text-end">
|
||||||
<ActionDropdown>
|
<ActionDropdown>
|
||||||
|
<Authorized entity="Snapshot" action="deleteSnapshot">
|
||||||
<DropdownItem
|
<DropdownItem
|
||||||
onClick={deleteSnapshot}
|
onClick={deleteSnapshot}
|
||||||
variant="danger"
|
variant="danger"
|
||||||
@@ -157,8 +167,10 @@ function SnapshotRow(props: SnapshotRowProps) {
|
|||||||
>
|
>
|
||||||
{__("Delete")}
|
{__("Delete")}
|
||||||
</DropdownItem>
|
</DropdownItem>
|
||||||
|
</Authorized>
|
||||||
</ActionDropdown>
|
</ActionDropdown>
|
||||||
</Td>
|
</Td>
|
||||||
|
)}
|
||||||
</Tr>
|
</Tr>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import { tasksQuery } from "/hooks/graph/TaskGraph";
|
|||||||
import { usePageTitle } from "@probo/hooks";
|
import { usePageTitle } from "@probo/hooks";
|
||||||
import TasksCard from "/components/tasks/TasksCard";
|
import TasksCard from "/components/tasks/TasksCard";
|
||||||
import TaskFormDialog from "/components/tasks/TaskFormDialog";
|
import TaskFormDialog from "/components/tasks/TaskFormDialog";
|
||||||
|
import { Authorized } from "/permissions";
|
||||||
|
|
||||||
const tasksFragment = graphql`
|
const tasksFragment = graphql`
|
||||||
fragment TasksPageFragment on Organization
|
fragment TasksPageFragment on Organization
|
||||||
@@ -76,9 +77,11 @@ export default function TasksPage({ queryRef }: Props) {
|
|||||||
"Track your assigned compliance tasks and keep progress on track."
|
"Track your assigned compliance tasks and keep progress on track."
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
|
<Authorized entity="Organization" action="createTask">
|
||||||
<TaskFormDialog connection={connectionId}>
|
<TaskFormDialog connection={connectionId}>
|
||||||
<Button icon={IconPlusLarge}>{__("New task")}</Button>
|
<Button icon={IconPlusLarge}>{__("New task")}</Button>
|
||||||
</TaskFormDialog>
|
</TaskFormDialog>
|
||||||
|
</Authorized>
|
||||||
</PageHeader>
|
</PageHeader>
|
||||||
<TasksCard connectionId={connectionId} tasks={tasks ?? []} />
|
<TasksCard connectionId={connectionId} tasks={tasks ?? []} />
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -36,6 +36,7 @@ import {
|
|||||||
} from "/hooks/graph/TrustCenterAccessGraph";
|
} from "/hooks/graph/TrustCenterAccessGraph";
|
||||||
import { useFormWithSchema } from "/hooks/useFormWithSchema";
|
import { useFormWithSchema } from "/hooks/useFormWithSchema";
|
||||||
import { useMutationWithToasts } from "/hooks/useMutationWithToasts";
|
import { useMutationWithToasts } from "/hooks/useMutationWithToasts";
|
||||||
|
import { Authorized } from "/permissions";
|
||||||
|
|
||||||
type ContextType = {
|
type ContextType = {
|
||||||
organization: {
|
organization: {
|
||||||
@@ -422,12 +423,14 @@ export default function TrustCenterAccessTab() {
|
|||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
{organization.trustCenter?.id && (
|
{organization.trustCenter?.id && (
|
||||||
|
<Authorized entity="TrustCenter" action="createTrustCenterAccess">
|
||||||
<Button icon={IconPlusLarge} onClick={() => {
|
<Button icon={IconPlusLarge} onClick={() => {
|
||||||
inviteForm.reset();
|
inviteForm.reset();
|
||||||
dialogRef.current?.open();
|
dialogRef.current?.open();
|
||||||
}}>
|
}}>
|
||||||
{__("Add Access")}
|
{__("Add Access")}
|
||||||
</Button>
|
</Button>
|
||||||
|
</Authorized>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -512,18 +515,22 @@ export default function TrustCenterAccessTab() {
|
|||||||
className="flex gap-2 justify-end"
|
className="flex gap-2 justify-end"
|
||||||
onClick={(e) => e.stopPropagation()}
|
onClick={(e) => e.stopPropagation()}
|
||||||
>
|
>
|
||||||
|
<Authorized entity="TrustCenterAccess" action="updateTrustCenterAccess">
|
||||||
<Button
|
<Button
|
||||||
variant="secondary"
|
variant="secondary"
|
||||||
onClick={() => handleEditAccess(access)}
|
onClick={() => handleEditAccess(access)}
|
||||||
disabled={isUpdating}
|
disabled={isUpdating}
|
||||||
icon={IconPencil}
|
icon={IconPencil}
|
||||||
/>
|
/>
|
||||||
|
</Authorized>
|
||||||
|
<Authorized entity="TrustCenterAccess" action="deleteTrustCenterAccess">
|
||||||
<Button
|
<Button
|
||||||
variant="danger"
|
variant="danger"
|
||||||
onClick={() => handleDelete(access.id)}
|
onClick={() => handleDelete(access.id)}
|
||||||
disabled={isDeleting}
|
disabled={isDeleting}
|
||||||
icon={IconTrashCan}
|
icon={IconTrashCan}
|
||||||
/>
|
/>
|
||||||
|
</Authorized>
|
||||||
</div>
|
</div>
|
||||||
</Td>
|
</Td>
|
||||||
</Tr>
|
</Tr>
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ import {
|
|||||||
} from "/hooks/graph/TrustCenterFileGraph";
|
} from "/hooks/graph/TrustCenterFileGraph";
|
||||||
import { useFormWithSchema } from "/hooks/useFormWithSchema";
|
import { useFormWithSchema } from "/hooks/useFormWithSchema";
|
||||||
import { TrustCenterFilesCard } from "/components/trustCenter/TrustCenterFilesCard";
|
import { TrustCenterFilesCard } from "/components/trustCenter/TrustCenterFilesCard";
|
||||||
|
import { Authorized } from "/permissions";
|
||||||
import type { TrustCenterFilesCardFragment$key } from "/components/trustCenter/__generated__/TrustCenterFilesCardFragment.graphql";
|
import type { TrustCenterFilesCardFragment$key } from "/components/trustCenter/__generated__/TrustCenterFilesCardFragment.graphql";
|
||||||
|
|
||||||
type ContextType = {
|
type ContextType = {
|
||||||
@@ -200,9 +201,11 @@ export default function TrustCenterFilesTab() {
|
|||||||
{__("Upload and manage files for your trust center")}
|
{__("Upload and manage files for your trust center")}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
<Authorized entity="Organization" action="createTrustCenterFile">
|
||||||
<Button icon={IconPlusLarge} onClick={() => createDialogRef.current?.open()}>
|
<Button icon={IconPlusLarge} onClick={() => createDialogRef.current?.open()}>
|
||||||
{__("Add File")}
|
{__("Add File")}
|
||||||
</Button>
|
</Button>
|
||||||
|
</Authorized>
|
||||||
</div>
|
</div>
|
||||||
{(isUpdating || isDeleting) && (
|
{(isUpdating || isDeleting) && (
|
||||||
<div className="flex items-center justify-center">
|
<div className="flex items-center justify-center">
|
||||||
|
|||||||
@@ -13,6 +13,8 @@ import { useUpdateTrustCenterMutation, useUploadTrustCenterNDAMutation, useDelet
|
|||||||
import type { TrustCenterGraphQuery$data } from "/hooks/graph/__generated__/TrustCenterGraphQuery.graphql";
|
import type { TrustCenterGraphQuery$data } from "/hooks/graph/__generated__/TrustCenterGraphQuery.graphql";
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { SlackConnections } from "../../../components/organizations/SlackConnection";
|
import { SlackConnections } from "../../../components/organizations/SlackConnection";
|
||||||
|
import { isAuthorized } from "/permissions";
|
||||||
|
import { useParams } from "react-router";
|
||||||
|
|
||||||
type ContextType = {
|
type ContextType = {
|
||||||
organization: TrustCenterGraphQuery$data["organization"];
|
organization: TrustCenterGraphQuery$data["organization"];
|
||||||
@@ -22,12 +24,15 @@ export default function TrustCenterOverviewTab() {
|
|||||||
const { __ } = useTranslate();
|
const { __ } = useTranslate();
|
||||||
const { toast } = useToast();
|
const { toast } = useToast();
|
||||||
const { organization } = useOutletContext<ContextType>();
|
const { organization } = useOutletContext<ContextType>();
|
||||||
|
const { organizationId } = useParams();
|
||||||
|
|
||||||
const [updateTrustCenter, isUpdating] = useUpdateTrustCenterMutation();
|
const [updateTrustCenter, isUpdating] = useUpdateTrustCenterMutation();
|
||||||
const [uploadNDA, isUploadingNDA] = useUploadTrustCenterNDAMutation();
|
const [uploadNDA, isUploadingNDA] = useUploadTrustCenterNDAMutation();
|
||||||
const [deleteNDA, isDeletingNDA] = useDeleteTrustCenterNDAMutation();
|
const [deleteNDA, isDeletingNDA] = useDeleteTrustCenterNDAMutation();
|
||||||
const [isActive, setIsActive] = useState(organization.trustCenter?.active || false);
|
const [isActive, setIsActive] = useState(organization.trustCenter?.active || false);
|
||||||
|
|
||||||
|
const canUpdateTrustCenter = organizationId ? isAuthorized(organizationId, "TrustCenter", "updateTrustCenter") : false;
|
||||||
|
|
||||||
const handleToggleActive = async (active: boolean) => {
|
const handleToggleActive = async (active: boolean) => {
|
||||||
if (!organization.trustCenter?.id) {
|
if (!organization.trustCenter?.id) {
|
||||||
toast({
|
toast({
|
||||||
@@ -128,6 +133,7 @@ export default function TrustCenterOverviewTab() {
|
|||||||
<Checkbox
|
<Checkbox
|
||||||
checked={isActive}
|
checked={isActive}
|
||||||
onChange={handleToggleActive}
|
onChange={handleToggleActive}
|
||||||
|
disabled={!canUpdateTrustCenter}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -210,16 +216,20 @@ export default function TrustCenterOverviewTab() {
|
|||||||
>
|
>
|
||||||
{__("Download PDF")}
|
{__("Download PDF")}
|
||||||
</Button>
|
</Button>
|
||||||
|
{canUpdateTrustCenter && (
|
||||||
<Button
|
<Button
|
||||||
variant="quaternary"
|
variant="quaternary"
|
||||||
icon={IconTrashCan}
|
icon={IconTrashCan}
|
||||||
onClick={handleNDADelete}
|
onClick={handleNDADelete}
|
||||||
disabled={isDeletingNDA}
|
disabled={isDeletingNDA}
|
||||||
/>
|
/>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
|
<>
|
||||||
|
{canUpdateTrustCenter ? (
|
||||||
<Dropzone
|
<Dropzone
|
||||||
description={__("Upload PDF files up to 10MB")}
|
description={__("Upload PDF files up to 10MB")}
|
||||||
isUploading={isUploadingNDA}
|
isUploading={isUploadingNDA}
|
||||||
@@ -229,6 +239,12 @@ export default function TrustCenterOverviewTab() {
|
|||||||
}}
|
}}
|
||||||
maxSize={10}
|
maxSize={10}
|
||||||
/>
|
/>
|
||||||
|
) : (
|
||||||
|
<p className="text-sm text-txt-tertiary">
|
||||||
|
{__("No NDA file uploaded")}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</Card>
|
</Card>
|
||||||
|
|||||||
@@ -29,6 +29,7 @@ import { ImportAssessmentDialog } from "./dialogs/ImportAssessmentDialog";
|
|||||||
import { complianceReportsFragment } from "./tabs/VendorComplianceTab";
|
import { complianceReportsFragment } from "./tabs/VendorComplianceTab";
|
||||||
import type { VendorComplianceTabFragment$key } from "./tabs/__generated__/VendorComplianceTabFragment.graphql";
|
import type { VendorComplianceTabFragment$key } from "./tabs/__generated__/VendorComplianceTabFragment.graphql";
|
||||||
import { SnapshotBanner } from "/components/SnapshotBanner";
|
import { SnapshotBanner } from "/components/SnapshotBanner";
|
||||||
|
import { Authorized } from "/permissions";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
queryRef: PreloadedQuery<VendorGraphNodeQuery>;
|
queryRef: PreloadedQuery<VendorGraphNodeQuery>;
|
||||||
@@ -97,6 +98,7 @@ export default function VendorDetailPage(props: Props) {
|
|||||||
{__("Assessment From Website")}
|
{__("Assessment From Website")}
|
||||||
</Button>
|
</Button>
|
||||||
</ImportAssessmentDialog>
|
</ImportAssessmentDialog>
|
||||||
|
<Authorized entity="Vendor" action="deleteVendor">
|
||||||
<ActionDropdown variant="secondary">
|
<ActionDropdown variant="secondary">
|
||||||
<DropdownItem
|
<DropdownItem
|
||||||
variant="danger"
|
variant="danger"
|
||||||
@@ -106,6 +108,7 @@ export default function VendorDetailPage(props: Props) {
|
|||||||
{__("Delete")}
|
{__("Delete")}
|
||||||
</DropdownItem>
|
</DropdownItem>
|
||||||
</ActionDropdown>
|
</ActionDropdown>
|
||||||
|
</Authorized>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -37,6 +37,8 @@ import type {
|
|||||||
} from "/hooks/graph/__generated__/VendorGraphPaginatedFragment.graphql";
|
} from "/hooks/graph/__generated__/VendorGraphPaginatedFragment.graphql";
|
||||||
import { SortableTable, SortableTh } from "/components/SortableTable";
|
import { SortableTable, SortableTh } from "/components/SortableTable";
|
||||||
import { SnapshotBanner } from "/components/SnapshotBanner";
|
import { SnapshotBanner } from "/components/SnapshotBanner";
|
||||||
|
import { Authorized } from "/permissions";
|
||||||
|
import { isAuthorized } from "/permissions";
|
||||||
|
|
||||||
type Vendor = NodeOf<VendorGraphPaginatedFragment$data["vendors"]>;
|
type Vendor = NodeOf<VendorGraphPaginatedFragment$data["vendors"]>;
|
||||||
|
|
||||||
@@ -61,6 +63,11 @@ export default function VendorsPage(props: Props) {
|
|||||||
|
|
||||||
usePageTitle(__("Vendors"));
|
usePageTitle(__("Vendors"));
|
||||||
|
|
||||||
|
const hasAnyAction = !isSnapshotMode && (
|
||||||
|
isAuthorized(organizationId, "Vendor", "updateVendor") ||
|
||||||
|
isAuthorized(organizationId, "Vendor", "deleteVendor")
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-6">
|
<div className="space-y-6">
|
||||||
{snapshotId && <SnapshotBanner snapshotId={snapshotId} />}
|
{snapshotId && <SnapshotBanner snapshotId={snapshotId} />}
|
||||||
@@ -71,12 +78,14 @@ export default function VendorsPage(props: Props) {
|
|||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{!isSnapshotMode && (
|
{!isSnapshotMode && (
|
||||||
|
<Authorized entity="Organization" action="createVendor">
|
||||||
<CreateVendorDialog
|
<CreateVendorDialog
|
||||||
connection={connectionId}
|
connection={connectionId}
|
||||||
organizationId={organizationId}
|
organizationId={organizationId}
|
||||||
>
|
>
|
||||||
<Button icon={IconPlusLarge}>{__("Add vendor")}</Button>
|
<Button icon={IconPlusLarge}>{__("Add vendor")}</Button>
|
||||||
</CreateVendorDialog>
|
</CreateVendorDialog>
|
||||||
|
</Authorized>
|
||||||
)}
|
)}
|
||||||
</PageHeader>
|
</PageHeader>
|
||||||
<SortableTable {...pagination}>
|
<SortableTable {...pagination}>
|
||||||
@@ -86,7 +95,7 @@ export default function VendorsPage(props: Props) {
|
|||||||
<Th>{__("Accessed At")}</Th>
|
<Th>{__("Accessed At")}</Th>
|
||||||
<Th>{__("Data Risk")}</Th>
|
<Th>{__("Data Risk")}</Th>
|
||||||
<Th>{__("Business Risk")}</Th>
|
<Th>{__("Business Risk")}</Th>
|
||||||
<Th></Th>
|
{hasAnyAction && <Th></Th>}
|
||||||
</Tr>
|
</Tr>
|
||||||
</Thead>
|
</Thead>
|
||||||
<Tbody>
|
<Tbody>
|
||||||
@@ -96,6 +105,7 @@ export default function VendorsPage(props: Props) {
|
|||||||
vendor={vendor}
|
vendor={vendor}
|
||||||
organizationId={organizationId}
|
organizationId={organizationId}
|
||||||
connectionId={connectionId}
|
connectionId={connectionId}
|
||||||
|
hasAnyAction={hasAnyAction}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
</Tbody>
|
</Tbody>
|
||||||
@@ -108,10 +118,12 @@ function VendorRow({
|
|||||||
vendor,
|
vendor,
|
||||||
organizationId,
|
organizationId,
|
||||||
connectionId,
|
connectionId,
|
||||||
|
hasAnyAction,
|
||||||
}: {
|
}: {
|
||||||
vendor: Vendor;
|
vendor: Vendor;
|
||||||
organizationId: string;
|
organizationId: string;
|
||||||
connectionId: string;
|
connectionId: string;
|
||||||
|
hasAnyAction: boolean;
|
||||||
}) {
|
}) {
|
||||||
const { snapshotId } = useParams<{ snapshotId?: string }>();
|
const { snapshotId } = useParams<{ snapshotId?: string }>();
|
||||||
const isSnapshotMode = Boolean(snapshotId);
|
const isSnapshotMode = Boolean(snapshotId);
|
||||||
@@ -144,9 +156,10 @@ function VendorRow({
|
|||||||
<Td>
|
<Td>
|
||||||
<RiskBadge level={latestAssessment?.businessImpact ?? "NONE"} />
|
<RiskBadge level={latestAssessment?.businessImpact ?? "NONE"} />
|
||||||
</Td>
|
</Td>
|
||||||
|
{hasAnyAction && (
|
||||||
<Td noLink width={50} className="text-end">
|
<Td noLink width={50} className="text-end">
|
||||||
{!isSnapshotMode && (
|
|
||||||
<ActionDropdown>
|
<ActionDropdown>
|
||||||
|
<Authorized entity="Vendor" action="deleteVendor">
|
||||||
<DropdownItem
|
<DropdownItem
|
||||||
onClick={deleteVendor}
|
onClick={deleteVendor}
|
||||||
variant="danger"
|
variant="danger"
|
||||||
@@ -154,9 +167,10 @@ function VendorRow({
|
|||||||
>
|
>
|
||||||
{__("Delete")}
|
{__("Delete")}
|
||||||
</DropdownItem>
|
</DropdownItem>
|
||||||
|
</Authorized>
|
||||||
</ActionDropdown>
|
</ActionDropdown>
|
||||||
)}
|
|
||||||
</Td>
|
</Td>
|
||||||
|
)}
|
||||||
</Tr>
|
</Tr>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ import type { useVendorFormFragment$key } from "/hooks/forms/__generated__/useVe
|
|||||||
import type { VendorOverviewTabBusinessAssociateAgreementFragment$key } from "./__generated__/VendorOverviewTabBusinessAssociateAgreementFragment.graphql";
|
import type { VendorOverviewTabBusinessAssociateAgreementFragment$key } from "./__generated__/VendorOverviewTabBusinessAssociateAgreementFragment.graphql";
|
||||||
import type { VendorOverviewTabDataPrivacyAgreementFragment$key } from "./__generated__/VendorOverviewTabDataPrivacyAgreementFragment.graphql";
|
import type { VendorOverviewTabDataPrivacyAgreementFragment$key } from "./__generated__/VendorOverviewTabDataPrivacyAgreementFragment.graphql";
|
||||||
import type { VendorCategory } from "@probo/vendors";
|
import type { VendorCategory } from "@probo/vendors";
|
||||||
|
import { Authorized } from "/permissions";
|
||||||
|
|
||||||
const vendorBusinessAssociateAgreementFragment = graphql`
|
const vendorBusinessAssociateAgreementFragment = graphql`
|
||||||
fragment VendorOverviewTabBusinessAssociateAgreementFragment on Vendor {
|
fragment VendorOverviewTabBusinessAssociateAgreementFragment on Vendor {
|
||||||
@@ -395,9 +396,11 @@ export default function VendorOverviewTab() {
|
|||||||
{/* Submit */}
|
{/* Submit */}
|
||||||
{!isSnapshotMode && (
|
{!isSnapshotMode && (
|
||||||
<div className="flex justify-end">
|
<div className="flex justify-end">
|
||||||
|
<Authorized entity="Vendor" action="updateVendor">
|
||||||
<Button type="submit" disabled={isSubmitting}>
|
<Button type="submit" disabled={isSubmitting}>
|
||||||
{__("Update vendor")}
|
{__("Update vendor")}
|
||||||
</Button>
|
</Button>
|
||||||
|
</Authorized>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</form>
|
</form>
|
||||||
|
|||||||
80
apps/console/src/permissions/Authorized.tsx
Normal file
80
apps/console/src/permissions/Authorized.tsx
Normal file
@@ -0,0 +1,80 @@
|
|||||||
|
import { type ReactNode, useState, useEffect } from "react";
|
||||||
|
import { useParams } from "react-router";
|
||||||
|
import { isAuthorized } 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 [hasAccess, setHasAccess] = useState<boolean | null>(null);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!organizationId) {
|
||||||
|
setHasAccess(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Try to check authorization, catching promise throws
|
||||||
|
try {
|
||||||
|
const authorized = isAuthorized(organizationId, entity, action);
|
||||||
|
setHasAccess(authorized);
|
||||||
|
} catch (promise) {
|
||||||
|
// If a promise is thrown (Suspense pattern), wait for it
|
||||||
|
if (promise instanceof Promise) {
|
||||||
|
promise
|
||||||
|
.then(() => {
|
||||||
|
// Permissions loaded, try again
|
||||||
|
try {
|
||||||
|
const authorized = isAuthorized(organizationId, entity, action);
|
||||||
|
setHasAccess(authorized);
|
||||||
|
} catch {
|
||||||
|
setHasAccess(false);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
setHasAccess(false);
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
setHasAccess(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, [organizationId, entity, action]);
|
||||||
|
|
||||||
|
if (!organizationId || hasAccess === null || hasAccess === false) {
|
||||||
|
return fallback;
|
||||||
|
}
|
||||||
|
|
||||||
|
return children;
|
||||||
|
}
|
||||||
3
apps/console/src/permissions/index.ts
Normal file
3
apps/console/src/permissions/index.ts
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
export { isAuthorized, getUserRole, getAssignableRoles } from "./permissions";
|
||||||
|
export { Authorized } from "./Authorized";
|
||||||
|
export type { EntityPermissions } from "./permissions";
|
||||||
131
apps/console/src/permissions/permissions.ts
Normal file
131
apps/console/src/permissions/permissions.ts
Normal file
@@ -0,0 +1,131 @@
|
|||||||
|
// 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;
|
||||||
|
};
|
||||||
|
|
||||||
|
let cachedPermissions: EntityPermissions | null = null;
|
||||||
|
let cachedRole: string | null = null;
|
||||||
|
let cachePromise: Promise<PermissionsResponse> | null = null;
|
||||||
|
let currentOrganizationId: string | null = null;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetch permissions for the current user's role in the organization
|
||||||
|
*/
|
||||||
|
function fetchPermissions(organizationId: string): Promise<PermissionsResponse> {
|
||||||
|
if (cachedPermissions && cachedRole && currentOrganizationId === organizationId) {
|
||||||
|
return Promise.resolve({ permissions: cachedPermissions, role: cachedRole });
|
||||||
|
}
|
||||||
|
|
||||||
|
if (cachePromise && currentOrganizationId === organizationId) {
|
||||||
|
return cachePromise;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (currentOrganizationId !== organizationId) {
|
||||||
|
cachedPermissions = null;
|
||||||
|
cachedRole = null;
|
||||||
|
cachePromise = null;
|
||||||
|
currentOrganizationId = organizationId;
|
||||||
|
}
|
||||||
|
|
||||||
|
const requestedOrgId = organizationId;
|
||||||
|
|
||||||
|
cachePromise = 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 (currentOrganizationId === requestedOrgId) {
|
||||||
|
cachedPermissions = data.permissions;
|
||||||
|
cachedRole = data.role;
|
||||||
|
}
|
||||||
|
cachePromise = null;
|
||||||
|
return data;
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
cachePromise = null;
|
||||||
|
cachedPermissions = null;
|
||||||
|
cachedRole = null;
|
||||||
|
throw error;
|
||||||
|
});
|
||||||
|
|
||||||
|
return cachePromise;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if the user has permission for an entity and action
|
||||||
|
*
|
||||||
|
* @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
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* isAuthorized(orgId, "Document", "get") // Check if user can query Document nodes
|
||||||
|
* isAuthorized(orgId, "Document", "updateDocument") // Check if user can update documents
|
||||||
|
* isAuthorized(orgId, "Organization", "createDocument") // Check if user can create documents
|
||||||
|
*/
|
||||||
|
export function isAuthorized(
|
||||||
|
organizationId: string,
|
||||||
|
entity: string,
|
||||||
|
action: string
|
||||||
|
): boolean {
|
||||||
|
if (!cachedPermissions || currentOrganizationId !== organizationId) {
|
||||||
|
throw fetchPermissions(organizationId);
|
||||||
|
}
|
||||||
|
|
||||||
|
const entityPermissions = cachedPermissions[entity];
|
||||||
|
if (!entityPermissions) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return entityPermissions[action] === true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the current user's role in the organization
|
||||||
|
*
|
||||||
|
* @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 {
|
||||||
|
if (!cachedRole || currentOrganizationId !== organizationId) {
|
||||||
|
throw fetchPermissions(organizationId);
|
||||||
|
}
|
||||||
|
|
||||||
|
return cachedRole;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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 === "OWNER" || currentRole === "FULL") {
|
||||||
|
return ["OWNER", "ADMIN", "VIEWER"];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (currentRole === "ADMIN") {
|
||||||
|
return ["ADMIN", "VIEWER"];
|
||||||
|
}
|
||||||
|
|
||||||
|
return [];
|
||||||
|
}
|
||||||
@@ -10,8 +10,8 @@ import type { PropsWithChildren } from "react";
|
|||||||
import { RelayEnvironmentProvider } from "react-relay";
|
import { RelayEnvironmentProvider } from "react-relay";
|
||||||
|
|
||||||
export class UnAuthenticatedError extends Error {
|
export class UnAuthenticatedError extends Error {
|
||||||
constructor() {
|
constructor(message?: string) {
|
||||||
super("UNAUTHENTICATED");
|
super(message || "UNAUTHENTICATED");
|
||||||
this.name = "UnAuthenticatedError";
|
this.name = "UnAuthenticatedError";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -45,12 +45,19 @@ export class AuthenticationRequiredError extends Error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export class UnauthorizedError extends Error {
|
export class UnauthorizedError extends Error {
|
||||||
constructor() {
|
constructor(message?: string) {
|
||||||
super("UNAUTHORIZED");
|
super(message || "UNAUTHORIZED");
|
||||||
this.name = "UnauthorizedError";
|
this.name = "UnauthorizedError";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export class ForbiddenError extends Error {
|
||||||
|
constructor(message?: string) {
|
||||||
|
super(message || "FORBIDDEN");
|
||||||
|
this.name = "ForbiddenError";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export function buildEndpoint(path: string): string {
|
export function buildEndpoint(path: string): string {
|
||||||
const host = import.meta.env.VITE_API_URL;
|
const host = import.meta.env.VITE_API_URL;
|
||||||
|
|
||||||
@@ -81,6 +88,9 @@ const hasAuthenticationRequiredError = (error: GraphQLError) =>
|
|||||||
const hasUnauthorizedError = (error: GraphQLError) =>
|
const hasUnauthorizedError = (error: GraphQLError) =>
|
||||||
error.extensions?.code == "UNAUTHORIZED";
|
error.extensions?.code == "UNAUTHORIZED";
|
||||||
|
|
||||||
|
const hasForbiddenError = (error: GraphQLError) =>
|
||||||
|
error.extensions?.code == "FORBIDDEN";
|
||||||
|
|
||||||
const fetchRelay: FetchFunction = async (
|
const fetchRelay: FetchFunction = async (
|
||||||
request,
|
request,
|
||||||
variables,
|
variables,
|
||||||
@@ -147,8 +157,9 @@ const fetchRelay: FetchFunction = async (
|
|||||||
if (json.errors) {
|
if (json.errors) {
|
||||||
const errors = json.errors as GraphQLError[];
|
const errors = json.errors as GraphQLError[];
|
||||||
|
|
||||||
if (errors.find(hasUnauthenticatedError)) {
|
const unauthenticatedError = errors.find(hasUnauthenticatedError);
|
||||||
throw new UnAuthenticatedError();
|
if (unauthenticatedError) {
|
||||||
|
throw new UnAuthenticatedError(unauthenticatedError.message);
|
||||||
}
|
}
|
||||||
|
|
||||||
const authRequiredError = errors.find(hasAuthenticationRequiredError);
|
const authRequiredError = errors.find(hasAuthenticationRequiredError);
|
||||||
@@ -163,8 +174,14 @@ const fetchRelay: FetchFunction = async (
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if (errors.find(hasUnauthorizedError)) {
|
const unauthorizedError = errors.find(hasUnauthorizedError);
|
||||||
throw new UnauthorizedError();
|
if (unauthorizedError) {
|
||||||
|
throw new UnauthorizedError(unauthorizedError.message);
|
||||||
|
}
|
||||||
|
|
||||||
|
const forbiddenError = errors.find(hasForbiddenError);
|
||||||
|
if (forbiddenError) {
|
||||||
|
throw new ForbiddenError(forbiddenError.message);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ import {
|
|||||||
relayEnvironment,
|
relayEnvironment,
|
||||||
UnAuthenticatedError,
|
UnAuthenticatedError,
|
||||||
UnauthorizedError,
|
UnauthorizedError,
|
||||||
|
ForbiddenError,
|
||||||
} from "./providers/RelayProviders";
|
} from "./providers/RelayProviders";
|
||||||
import { PageSkeleton } from "./components/skeletons/PageSkeleton.tsx";
|
import { PageSkeleton } from "./components/skeletons/PageSkeleton.tsx";
|
||||||
import { loadQuery, type PreloadedQuery } from "react-relay";
|
import { loadQuery, type PreloadedQuery } from "react-relay";
|
||||||
@@ -59,6 +60,10 @@ function ErrorBoundary({ error: propsError }: { error?: string }) {
|
|||||||
return <PageError error="UNAUTHORIZED" />;
|
return <PageError error="UNAUTHORIZED" />;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (error instanceof ForbiddenError) {
|
||||||
|
return <PageError error="FORBIDDEN" />;
|
||||||
|
}
|
||||||
|
|
||||||
return <PageError error={error?.toString()} />;
|
return <PageError error={error?.toString()} />;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ export default defineConfig({
|
|||||||
"/pages": fileURLToPath(new URL("./src/pages", import.meta.url)),
|
"/pages": fileURLToPath(new URL("./src/pages", import.meta.url)),
|
||||||
"/routes": fileURLToPath(new URL("./src/routes", import.meta.url)),
|
"/routes": fileURLToPath(new URL("./src/routes", import.meta.url)),
|
||||||
"/providers": fileURLToPath(new URL("./src/providers", import.meta.url)),
|
"/providers": fileURLToPath(new URL("./src/providers", import.meta.url)),
|
||||||
|
"/permissions": fileURLToPath(new URL("./src/permissions", import.meta.url)),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,7 +1,10 @@
|
|||||||
export interface GraphQLError {
|
export interface GraphQLError {
|
||||||
message?: string;
|
message?: string;
|
||||||
|
extensions?: {
|
||||||
|
code?: string;
|
||||||
|
};
|
||||||
source?: {
|
source?: {
|
||||||
errors?: Array<{ message: string }>;
|
errors?: Array<{ message: string; extensions?: { code?: string } }>;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import {
|
|||||||
Description,
|
Description,
|
||||||
Cancel,
|
Cancel,
|
||||||
} from "@radix-ui/react-alert-dialog";
|
} from "@radix-ui/react-alert-dialog";
|
||||||
import { useCallback, useState, type ComponentProps } from "react";
|
import { useCallback, useState, useMemo, type ComponentProps } from "react";
|
||||||
import { Button } from "../../Atoms/Button/Button";
|
import { Button } from "../../Atoms/Button/Button";
|
||||||
import { Root as Portal } from "@radix-ui/react-portal";
|
import { Root as Portal } from "@radix-ui/react-portal";
|
||||||
import { dialog } from "./Dialog";
|
import { dialog } from "./Dialog";
|
||||||
@@ -67,38 +67,69 @@ export function useConfirm() {
|
|||||||
* Global component that displays a dialog when confirm() is called
|
* Global component that displays a dialog when confirm() is called
|
||||||
*/
|
*/
|
||||||
export function ConfirmDialog() {
|
export function ConfirmDialog() {
|
||||||
const { message, title, variant, label, onConfirm, close } =
|
const message = useConfirmStore((state) => state.message);
|
||||||
useConfirmStore();
|
const isOpen = !!message;
|
||||||
|
|
||||||
|
if (!isOpen) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return <ConfirmDialogContent />;
|
||||||
|
}
|
||||||
|
|
||||||
|
function ConfirmDialogContent() {
|
||||||
|
const message = useConfirmStore((state) => state.message);
|
||||||
|
const title = useConfirmStore((state) => state.title);
|
||||||
|
const variant = useConfirmStore((state) => state.variant);
|
||||||
|
const label = useConfirmStore((state) => state.label);
|
||||||
|
const onConfirm = useConfirmStore((state) => state.onConfirm);
|
||||||
|
const close = useConfirmStore((state) => state.close);
|
||||||
|
|
||||||
const { __ } = useTranslate();
|
const { __ } = useTranslate();
|
||||||
const isOpen = !!message;
|
const isOpen = !!message;
|
||||||
const {
|
|
||||||
overlay,
|
|
||||||
content,
|
|
||||||
header,
|
|
||||||
title: titleClassname,
|
|
||||||
footer,
|
|
||||||
} = dialog();
|
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
|
|
||||||
|
const dialogStyles = useMemo(() => {
|
||||||
|
const styles = dialog();
|
||||||
|
return {
|
||||||
|
overlay: styles.overlay(),
|
||||||
|
content: styles.content({ className: "max-w-[500px]" }),
|
||||||
|
header: styles.header(),
|
||||||
|
title: styles.title(),
|
||||||
|
footer: styles.footer(),
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
const handleConfirm = async () => {
|
const handleConfirm = async () => {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
try {
|
try {
|
||||||
await onConfirm();
|
await onConfirm();
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Confirm action failed:', error);
|
||||||
} finally {
|
} finally {
|
||||||
close();
|
close();
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleOpenChange = (open: boolean) => {
|
||||||
|
if (!open) {
|
||||||
|
setLoading(false);
|
||||||
|
close();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Root open={isOpen} onOpenChange={close}>
|
<Root open={isOpen} onOpenChange={handleOpenChange}>
|
||||||
<Portal>
|
<Portal>
|
||||||
<Overlay className={overlay()} />
|
<Overlay className={dialogStyles.overlay} />
|
||||||
<Content className={content({ className: "max-w-[500px]" })}>
|
<Content className={dialogStyles.content}>
|
||||||
<header className={header()}>
|
<header className={dialogStyles.header}>
|
||||||
<Title children={title} className={titleClassname()} />
|
<Title children={title} className={dialogStyles.title} />
|
||||||
</header>
|
</header>
|
||||||
<Description className="p-6" children={message} />
|
<Description className="p-6" children={message} />
|
||||||
<footer className={footer()}>
|
<footer className={dialogStyles.footer}>
|
||||||
<Cancel asChild>
|
<Cancel asChild>
|
||||||
<Button disabled={loading} variant="tertiary">
|
<Button disabled={loading} variant="tertiary">
|
||||||
{__("Cancel")}
|
{__("Cancel")}
|
||||||
|
|||||||
@@ -77,9 +77,9 @@ func ExtractEmailDomain(email string) (string, error) {
|
|||||||
return domain, nil
|
return domain, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func MapSAMLRoleToSystemRole(samlRole string) *coredata.Role {
|
func MapSAMLRoleToSystemRole(samlRole string) *coredata.MembershipRole {
|
||||||
if samlRole != "" && isValidRole(samlRole) {
|
if samlRole != "" && isValidRole(samlRole) {
|
||||||
role := coredata.Role(samlRole)
|
role := coredata.MembershipRole(samlRole)
|
||||||
return &role
|
return &role
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -88,7 +88,7 @@ func MapSAMLRoleToSystemRole(samlRole string) *coredata.Role {
|
|||||||
|
|
||||||
func isValidRole(role string) bool {
|
func isValidRole(role string) bool {
|
||||||
switch role {
|
switch role {
|
||||||
case "OWNER", "ADMIN", "MEMBER", "VIEWER":
|
case "OWNER", "ADMIN", "VIEWER":
|
||||||
return true
|
return true
|
||||||
default:
|
default:
|
||||||
return false
|
return false
|
||||||
|
|||||||
@@ -407,7 +407,7 @@ func (s *SAMLService) InitiateSAMLLogin(
|
|||||||
type SAMLUserInfo struct {
|
type SAMLUserInfo struct {
|
||||||
Email string
|
Email string
|
||||||
FullName string
|
FullName string
|
||||||
Role *coredata.Role
|
Role *coredata.MembershipRole
|
||||||
SAMLSubject string
|
SAMLSubject string
|
||||||
OrganizationID gid.GID
|
OrganizationID gid.GID
|
||||||
SAMLConfigID gid.GID
|
SAMLConfigID gid.GID
|
||||||
|
|||||||
@@ -46,6 +46,8 @@ type (
|
|||||||
invitationTokenValidity time.Duration
|
invitationTokenValidity time.Duration
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ErrCreateOrganizationDisabled struct{}
|
||||||
|
|
||||||
TenantAuthService struct {
|
TenantAuthService struct {
|
||||||
pg *pg.Client
|
pg *pg.Client
|
||||||
encryptionKey cipher.EncryptionKey
|
encryptionKey cipher.EncryptionKey
|
||||||
@@ -192,6 +194,10 @@ func (e ErrSAMLAutoSignupDisabled) Error() string {
|
|||||||
return "SAML auto-signup is disabled for this organization"
|
return "SAML auto-signup is disabled for this organization"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (e ErrCreateOrganizationDisabled) Error() string {
|
||||||
|
return "organization creation is disabled for users without existing admin or owner membership"
|
||||||
|
}
|
||||||
|
|
||||||
func (e ErrSAMLAuthRequired) Error() string {
|
func (e ErrSAMLAuthRequired) Error() string {
|
||||||
return "SAML authentication required for this organization"
|
return "SAML authentication required for this organization"
|
||||||
}
|
}
|
||||||
@@ -232,6 +238,28 @@ func (s *Service) WithTenant(tenantID gid.TenantID) *TenantAuthService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s Service) CanCreateOrganization(ctx context.Context, userID gid.GID) error {
|
||||||
|
if !s.disableSignup {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var memberships coredata.Memberships
|
||||||
|
err := s.pg.WithConn(ctx, func(conn pg.Conn) error {
|
||||||
|
return memberships.LoadByUserID(ctx, conn, coredata.NewNoScope(), userID)
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("cannot load user memberships: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, membership := range memberships {
|
||||||
|
if membership.Role == coredata.MembershipRoleOwner || membership.Role == coredata.MembershipRoleAdmin {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return &ErrCreateOrganizationDisabled{}
|
||||||
|
}
|
||||||
|
|
||||||
func (s Service) ForgetPassword(
|
func (s Service) ForgetPassword(
|
||||||
ctx context.Context,
|
ctx context.Context,
|
||||||
email string,
|
email string,
|
||||||
@@ -1351,11 +1379,18 @@ func (s *Service) CreateUserAPIKey(
|
|||||||
|
|
||||||
for _, membership := range memberships {
|
for _, membership := range memberships {
|
||||||
scope := coredata.NewScope(membership.MembershipID.TenantID())
|
scope := coredata.NewScope(membership.MembershipID.TenantID())
|
||||||
|
|
||||||
|
var m coredata.Membership
|
||||||
|
if err := m.LoadByID(ctx, tx, scope, membership.MembershipID); err != nil {
|
||||||
|
return fmt.Errorf("cannot load membership: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
userAPIKeyMembership := &coredata.UserAPIKeyMembership{
|
userAPIKeyMembership := &coredata.UserAPIKeyMembership{
|
||||||
ID: gid.New(membership.MembershipID.TenantID(), coredata.UserAPIKeyMembershipEntityType),
|
ID: gid.New(membership.MembershipID.TenantID(), coredata.UserAPIKeyMembershipEntityType),
|
||||||
UserAPIKeyID: userAPIKey.ID,
|
UserAPIKeyID: userAPIKey.ID,
|
||||||
MembershipID: membership.MembershipID,
|
MembershipID: membership.MembershipID,
|
||||||
Role: membership.Role,
|
Role: membership.Role,
|
||||||
|
OrganizationID: m.OrganizationID,
|
||||||
CreatedAt: now,
|
CreatedAt: now,
|
||||||
UpdatedAt: now,
|
UpdatedAt: now,
|
||||||
}
|
}
|
||||||
@@ -1513,11 +1548,18 @@ func (s *Service) UpdateUserAPIKeyMemberships(
|
|||||||
now := time.Now()
|
now := time.Now()
|
||||||
for _, membership := range memberships {
|
for _, membership := range memberships {
|
||||||
scope := coredata.NewScope(membership.MembershipID.TenantID())
|
scope := coredata.NewScope(membership.MembershipID.TenantID())
|
||||||
|
|
||||||
|
var m coredata.Membership
|
||||||
|
if err := m.LoadByID(ctx, tx, scope, membership.MembershipID); err != nil {
|
||||||
|
return fmt.Errorf("cannot load membership: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
userAPIKeyMembership := &coredata.UserAPIKeyMembership{
|
userAPIKeyMembership := &coredata.UserAPIKeyMembership{
|
||||||
ID: gid.New(membership.MembershipID.TenantID(), coredata.UserAPIKeyMembershipEntityType),
|
ID: gid.New(membership.MembershipID.TenantID(), coredata.UserAPIKeyMembershipEntityType),
|
||||||
UserAPIKeyID: userAPIKey.ID,
|
UserAPIKeyID: userAPIKey.ID,
|
||||||
MembershipID: membership.MembershipID,
|
MembershipID: membership.MembershipID,
|
||||||
Role: membership.Role,
|
Role: membership.Role,
|
||||||
|
OrganizationID: m.OrganizationID,
|
||||||
CreatedAt: now,
|
CreatedAt: now,
|
||||||
UpdatedAt: now,
|
UpdatedAt: now,
|
||||||
}
|
}
|
||||||
|
|||||||
707
pkg/authz/permissions.go
Normal file
707
pkg/authz/permissions.go
Normal file
@@ -0,0 +1,707 @@
|
|||||||
|
// Copyright (c) 2025 Probo Inc <hello@getprobo.com>.
|
||||||
|
//
|
||||||
|
// Permission to use, copy, modify, and/or distribute this software for any
|
||||||
|
// purpose with or without fee is hereby granted, provided that the above
|
||||||
|
// copyright notice and this permission notice appear in all copies.
|
||||||
|
//
|
||||||
|
// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
|
||||||
|
// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
|
||||||
|
// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
|
||||||
|
// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
|
||||||
|
// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
|
||||||
|
// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
|
||||||
|
// PERFORMANCE OF THIS SOFTWARE.
|
||||||
|
|
||||||
|
package authz
|
||||||
|
|
||||||
|
import (
|
||||||
|
"slices"
|
||||||
|
|
||||||
|
"go.probo.inc/probo/pkg/coredata"
|
||||||
|
)
|
||||||
|
|
||||||
|
type (
|
||||||
|
Role string
|
||||||
|
|
||||||
|
Action string
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
RoleOwner Role = "OWNER"
|
||||||
|
RoleAdmin Role = "ADMIN"
|
||||||
|
RoleViewer Role = "VIEWER"
|
||||||
|
RoleFull Role = "FULL"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
ActionGet Action = "get"
|
||||||
|
|
||||||
|
ActionGetAssetType Action = "getAssetType"
|
||||||
|
ActionGetAssignedTo Action = "getAssignedTo"
|
||||||
|
ActionGetAuthMethod Action = "getAuthMethod"
|
||||||
|
ActionGetBusinessAssociateAgreement Action = "getBusinessAssociateAgreement"
|
||||||
|
ActionGetBusinessOwner Action = "getBusinessOwner"
|
||||||
|
ActionGetCustomDomain Action = "getCustomDomain"
|
||||||
|
ActionGetDataPrivacyAgreement Action = "getDataPrivacyAgreement"
|
||||||
|
ActionGetFile Action = "getFile"
|
||||||
|
ActionGetFileUrl Action = "getFileUrl"
|
||||||
|
ActionGetFramework Action = "getFramework"
|
||||||
|
ActionGetHorizontalLogoUrl Action = "getHorizontalLogoUrl"
|
||||||
|
ActionGetLogoUrl Action = "getLogoUrl"
|
||||||
|
ActionGetMeasure Action = "getMeasure"
|
||||||
|
ActionGetNdaFileUrl Action = "getNdaFileUrl"
|
||||||
|
ActionGetOrganization Action = "getOrganization"
|
||||||
|
ActionGetOwner Action = "getOwner"
|
||||||
|
ActionGetSecurityOwner Action = "getSecurityOwner"
|
||||||
|
ActionGetSnapshot Action = "getSnapshot"
|
||||||
|
ActionGetTask Action = "getTask"
|
||||||
|
ActionGetTrustCenter Action = "getTrustCenter"
|
||||||
|
ActionGetTrustCenterFile Action = "getTrustCenterFile"
|
||||||
|
ActionGetVendor Action = "getVendor"
|
||||||
|
|
||||||
|
ActionActiveCount Action = "activeCount"
|
||||||
|
ActionAudit Action = "audit"
|
||||||
|
ActionAvailableDocumentAccesses Action = "availableDocumentAccesses"
|
||||||
|
ActionDocument Action = "document"
|
||||||
|
ActionDocumentVersion Action = "documentVersion"
|
||||||
|
ActionDownloadUrl Action = "downloadUrl"
|
||||||
|
ActionMemberships Action = "memberships"
|
||||||
|
ActionPendingRequestCount Action = "pendingRequestCount"
|
||||||
|
ActionPeoples Action = "peoples"
|
||||||
|
ActionReport Action = "report"
|
||||||
|
ActionReportUrl Action = "reportUrl"
|
||||||
|
ActionSignatures Action = "signatures"
|
||||||
|
ActionSignedBy Action = "signedBy"
|
||||||
|
ActionSpMetadataUrl Action = "spMetadataUrl"
|
||||||
|
ActionTestLoginUrl Action = "testLoginUrl"
|
||||||
|
ActionTotalCount Action = "totalCount"
|
||||||
|
ActionTrustCenterFile Action = "trustCenterFile"
|
||||||
|
|
||||||
|
ActionListAccesses Action = "listAccesses"
|
||||||
|
ActionListAssets Action = "listAssets"
|
||||||
|
ActionListAudits Action = "listAudits"
|
||||||
|
ActionListComplianceReports Action = "listComplianceReports"
|
||||||
|
ActionListContacts Action = "listContacts"
|
||||||
|
ActionListContinualImprovements Action = "listContinualImprovements"
|
||||||
|
ActionListControls Action = "listControls"
|
||||||
|
ActionListData Action = "listData"
|
||||||
|
ActionListDocuments Action = "listDocuments"
|
||||||
|
ActionListEvidences Action = "listEvidences"
|
||||||
|
ActionListFrameworks Action = "listFrameworks"
|
||||||
|
ActionListInvitations Action = "listInvitations"
|
||||||
|
ActionListMeasures Action = "listMeasures"
|
||||||
|
ActionListMeetings Action = "listMeetings"
|
||||||
|
ActionListMembers Action = "listMembers"
|
||||||
|
ActionListNonconformities Action = "listNonconformities"
|
||||||
|
ActionListObligations Action = "listObligations"
|
||||||
|
ActionListPeople Action = "listPeople"
|
||||||
|
ActionListProcessingActivities Action = "listProcessingActivities"
|
||||||
|
ActionListReferences Action = "listReferences"
|
||||||
|
ActionListRiskAssessments Action = "listRiskAssessments"
|
||||||
|
ActionListRisks Action = "listRisks"
|
||||||
|
ActionListSAMLConfigurations Action = "listSAMLConfigurations"
|
||||||
|
ActionListServices Action = "listServices"
|
||||||
|
ActionListSlackConnections Action = "listSlackConnections"
|
||||||
|
ActionListSnapshots Action = "listSnapshots"
|
||||||
|
ActionListTasks Action = "listTasks"
|
||||||
|
ActionListTrustCenterFiles Action = "listTrustCenterFiles"
|
||||||
|
ActionListVendors Action = "listVendors"
|
||||||
|
ActionListVersions Action = "listVersions"
|
||||||
|
|
||||||
|
ActionCreateAsset Action = "createAsset"
|
||||||
|
ActionCreateAudit Action = "createAudit"
|
||||||
|
ActionCreateContinualImprovement Action = "createContinualImprovement"
|
||||||
|
ActionCreateControl Action = "createControl"
|
||||||
|
ActionCreateControlAuditMapping Action = "createControlAuditMapping"
|
||||||
|
ActionCreateControlDocumentMapping Action = "createControlDocumentMapping"
|
||||||
|
ActionCreateControlMeasureMapping Action = "createControlMeasureMapping"
|
||||||
|
ActionCreateControlSnapshotMapping Action = "createControlSnapshotMapping"
|
||||||
|
ActionCreateCustomDomain Action = "createCustomDomain"
|
||||||
|
ActionCreateDatum Action = "createDatum"
|
||||||
|
ActionCreateDocument Action = "createDocument"
|
||||||
|
ActionCreateDraftDocumentVersion Action = "createDraftDocumentVersion"
|
||||||
|
ActionCreateFramework Action = "createFramework"
|
||||||
|
ActionCreateMeasure Action = "createMeasure"
|
||||||
|
ActionCreateMeeting Action = "createMeeting"
|
||||||
|
ActionCreateNonconformity Action = "createNonconformity"
|
||||||
|
ActionCreateObligation Action = "createObligation"
|
||||||
|
ActionCreatePeople Action = "createPeople"
|
||||||
|
ActionCreateProcessingActivity Action = "createProcessingActivity"
|
||||||
|
ActionCreateRisk Action = "createRisk"
|
||||||
|
ActionCreateRiskDocumentMapping Action = "createRiskDocumentMapping"
|
||||||
|
ActionCreateRiskMeasureMapping Action = "createRiskMeasureMapping"
|
||||||
|
ActionCreateRiskObligationMapping Action = "createRiskObligationMapping"
|
||||||
|
ActionCreateSAMLConfiguration Action = "createSAMLConfiguration"
|
||||||
|
ActionCreateSnapshot Action = "createSnapshot"
|
||||||
|
ActionCreateTask Action = "createTask"
|
||||||
|
ActionCreateTrustCenter Action = "createTrustCenter"
|
||||||
|
ActionCreateTrustCenterAccess Action = "createTrustCenterAccess"
|
||||||
|
ActionCreateTrustCenterFile Action = "createTrustCenterFile"
|
||||||
|
ActionCreateTrustCenterReference Action = "createTrustCenterReference"
|
||||||
|
ActionCreateVendor Action = "createVendor"
|
||||||
|
ActionCreateVendorContact Action = "createVendorContact"
|
||||||
|
ActionCreateVendorRiskAssessment Action = "createVendorRiskAssessment"
|
||||||
|
ActionCreateVendorService Action = "createVendorService"
|
||||||
|
|
||||||
|
ActionUpdateAsset Action = "updateAsset"
|
||||||
|
ActionUpdateAudit Action = "updateAudit"
|
||||||
|
ActionUpdateContinualImprovement Action = "updateContinualImprovement"
|
||||||
|
ActionUpdateControl Action = "updateControl"
|
||||||
|
ActionUpdateDatum Action = "updateDatum"
|
||||||
|
ActionUpdateDocument Action = "updateDocument"
|
||||||
|
ActionUpdateDocumentVersion Action = "updateDocumentVersion"
|
||||||
|
ActionUpdateFramework Action = "updateFramework"
|
||||||
|
ActionUpdateMeasure Action = "updateMeasure"
|
||||||
|
ActionUpdateMeeting Action = "updateMeeting"
|
||||||
|
ActionUpdateMembership Action = "updateMembership"
|
||||||
|
ActionUpdateNonconformity Action = "updateNonconformity"
|
||||||
|
ActionUpdateObligation Action = "updateObligation"
|
||||||
|
ActionUpdateOrganization Action = "updateOrganization"
|
||||||
|
ActionUpdatePeople Action = "updatePeople"
|
||||||
|
ActionUpdateProcessingActivity Action = "updateProcessingActivity"
|
||||||
|
ActionUpdateRisk Action = "updateRisk"
|
||||||
|
ActionUpdateSAMLConfiguration Action = "updateSAMLConfiguration"
|
||||||
|
ActionUpdateTask Action = "updateTask"
|
||||||
|
ActionUpdateTrustCenter Action = "updateTrustCenter"
|
||||||
|
ActionUpdateTrustCenterAccess Action = "updateTrustCenterAccess"
|
||||||
|
ActionUpdateTrustCenterFile Action = "updateTrustCenterFile"
|
||||||
|
ActionUpdateTrustCenterReference Action = "updateTrustCenterReference"
|
||||||
|
ActionUpdateVendor Action = "updateVendor"
|
||||||
|
ActionUpdateVendorBusinessAssociateAgreement Action = "updateVendorBusinessAssociateAgreement"
|
||||||
|
ActionUpdateVendorContact Action = "updateVendorContact"
|
||||||
|
ActionUpdateVendorDataPrivacyAgreement Action = "updateVendorDataPrivacyAgreement"
|
||||||
|
ActionUpdateVendorService Action = "updateVendorService"
|
||||||
|
|
||||||
|
ActionDeleteAsset Action = "deleteAsset"
|
||||||
|
ActionDeleteAudit Action = "deleteAudit"
|
||||||
|
ActionDeleteAuditReport Action = "deleteAuditReport"
|
||||||
|
ActionDeleteContinualImprovement Action = "deleteContinualImprovement"
|
||||||
|
ActionDeleteControl Action = "deleteControl"
|
||||||
|
ActionDeleteControlAuditMapping Action = "deleteControlAuditMapping"
|
||||||
|
ActionDeleteControlDocumentMapping Action = "deleteControlDocumentMapping"
|
||||||
|
ActionDeleteControlMeasureMapping Action = "deleteControlMeasureMapping"
|
||||||
|
ActionDeleteControlSnapshotMapping Action = "deleteControlSnapshotMapping"
|
||||||
|
ActionDeleteCustomDomain Action = "deleteCustomDomain"
|
||||||
|
ActionDeleteDatum Action = "deleteDatum"
|
||||||
|
ActionDeleteDocument Action = "deleteDocument"
|
||||||
|
ActionDeleteDraftDocumentVersion Action = "deleteDraftDocumentVersion"
|
||||||
|
ActionDeleteEvidence Action = "deleteEvidence"
|
||||||
|
ActionDeleteFramework Action = "deleteFramework"
|
||||||
|
ActionDeleteInvitation Action = "deleteInvitation"
|
||||||
|
ActionDeleteMeasure Action = "deleteMeasure"
|
||||||
|
ActionDeleteMeeting Action = "deleteMeeting"
|
||||||
|
ActionDeleteNonconformity Action = "deleteNonconformity"
|
||||||
|
ActionDeleteObligation Action = "deleteObligation"
|
||||||
|
ActionDeleteOrganization Action = "deleteOrganization"
|
||||||
|
ActionDeleteOrganizationHorizontalLogo Action = "deleteOrganizationHorizontalLogo"
|
||||||
|
ActionDeletePeople Action = "deletePeople"
|
||||||
|
ActionDeleteProcessingActivity Action = "deleteProcessingActivity"
|
||||||
|
ActionDeleteRisk Action = "deleteRisk"
|
||||||
|
ActionDeleteRiskDocumentMapping Action = "deleteRiskDocumentMapping"
|
||||||
|
ActionDeleteRiskMeasureMapping Action = "deleteRiskMeasureMapping"
|
||||||
|
ActionDeleteRiskObligationMapping Action = "deleteRiskObligationMapping"
|
||||||
|
ActionDeleteSAMLConfiguration Action = "deleteSAMLConfiguration"
|
||||||
|
ActionDeleteSnapshot Action = "deleteSnapshot"
|
||||||
|
ActionDeleteTask Action = "deleteTask"
|
||||||
|
ActionDeleteTrustCenterAccess Action = "deleteTrustCenterAccess"
|
||||||
|
ActionDeleteTrustCenterFile Action = "deleteTrustCenterFile"
|
||||||
|
ActionDeleteTrustCenterNDA Action = "deleteTrustCenterNDA"
|
||||||
|
ActionDeleteTrustCenterReference Action = "deleteTrustCenterReference"
|
||||||
|
ActionDeleteVendor Action = "deleteVendor"
|
||||||
|
ActionDeleteVendorBusinessAssociateAgreement Action = "deleteVendorBusinessAssociateAgreement"
|
||||||
|
ActionDeleteVendorComplianceReport Action = "deleteVendorComplianceReport"
|
||||||
|
ActionDeleteVendorContact Action = "deleteVendorContact"
|
||||||
|
ActionDeleteVendorDataPrivacyAgreement Action = "deleteVendorDataPrivacyAgreement"
|
||||||
|
ActionDeleteVendorService Action = "deleteVendorService"
|
||||||
|
|
||||||
|
ActionAcceptInvitation Action = "acceptInvitation"
|
||||||
|
ActionAssessVendor Action = "assessVendor"
|
||||||
|
ActionAssignTask Action = "assignTask"
|
||||||
|
ActionBulkDeleteDocuments Action = "bulkDeleteDocuments"
|
||||||
|
ActionBulkExportDocuments Action = "bulkExportDocuments"
|
||||||
|
ActionBulkPublishDocumentVersions Action = "bulkPublishDocumentVersions"
|
||||||
|
ActionBulkRequestSignatures Action = "bulkRequestSignatures"
|
||||||
|
ActionCancelSignatureRequest Action = "cancelSignatureRequest"
|
||||||
|
ActionConfirmEmail Action = "confirmEmail"
|
||||||
|
ActionDisableSAML Action = "disableSAML"
|
||||||
|
ActionEnableSAML Action = "enableSAML"
|
||||||
|
ActionExportDocumentVersionPDF Action = "exportDocumentVersionPDF"
|
||||||
|
ActionExportFramework Action = "exportFramework"
|
||||||
|
ActionGenerateDocumentChangelog Action = "generateDocumentChangelog"
|
||||||
|
ActionGenerateFrameworkStateOfApplicability Action = "generateFrameworkStateOfApplicability"
|
||||||
|
ActionImportFramework Action = "importFramework"
|
||||||
|
ActionImportMeasure Action = "importMeasure"
|
||||||
|
ActionInitiateDomainVerification Action = "initiateDomainVerification"
|
||||||
|
ActionInviteUser Action = "inviteUser"
|
||||||
|
ActionPublishDocumentVersion Action = "publishDocumentVersion"
|
||||||
|
ActionRemoveMember Action = "removeMember"
|
||||||
|
ActionRequestSignature Action = "requestSignature"
|
||||||
|
ActionSendSigningNotifications Action = "sendSigningNotifications"
|
||||||
|
ActionUnassignTask Action = "unassignTask"
|
||||||
|
ActionUploadAuditReport Action = "uploadAuditReport"
|
||||||
|
ActionUploadMeasureEvidence Action = "uploadMeasureEvidence"
|
||||||
|
ActionUploadTrustCenterNDA Action = "uploadTrustCenterNDA"
|
||||||
|
ActionUploadVendorBusinessAssociateAgreement Action = "uploadVendorBusinessAssociateAgreement"
|
||||||
|
ActionUploadVendorComplianceReport Action = "uploadVendorComplianceReport"
|
||||||
|
ActionUploadVendorDataPrivacyAgreement Action = "uploadVendorDataPrivacyAgreement"
|
||||||
|
ActionVerifyDomain Action = "verifyDomain"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
AllRoles = []Role{RoleOwner, RoleAdmin, RoleViewer, RoleFull}
|
||||||
|
EditRoles = []Role{RoleOwner, RoleAdmin, RoleFull}
|
||||||
|
)
|
||||||
|
|
||||||
|
var Permissions = map[uint16]map[Action][]Role{
|
||||||
|
coredata.OrganizationEntityType: {
|
||||||
|
ActionGet: AllRoles,
|
||||||
|
ActionGetLogoUrl: AllRoles,
|
||||||
|
ActionGetHorizontalLogoUrl: AllRoles,
|
||||||
|
ActionMemberships: AllRoles,
|
||||||
|
ActionPeoples: AllRoles,
|
||||||
|
ActionTotalCount: AllRoles,
|
||||||
|
ActionListMembers: AllRoles,
|
||||||
|
ActionListInvitations: AllRoles,
|
||||||
|
ActionListSlackConnections: AllRoles,
|
||||||
|
ActionListFrameworks: AllRoles,
|
||||||
|
ActionListControls: AllRoles,
|
||||||
|
ActionListVendors: AllRoles,
|
||||||
|
ActionListPeople: AllRoles,
|
||||||
|
ActionListDocuments: AllRoles,
|
||||||
|
ActionListMeetings: AllRoles,
|
||||||
|
ActionListMeasures: AllRoles,
|
||||||
|
ActionListRisks: AllRoles,
|
||||||
|
ActionListTasks: AllRoles,
|
||||||
|
ActionListAssets: AllRoles,
|
||||||
|
ActionListData: AllRoles,
|
||||||
|
ActionListAudits: AllRoles,
|
||||||
|
ActionListNonconformities: AllRoles,
|
||||||
|
ActionListObligations: AllRoles,
|
||||||
|
ActionListContinualImprovements: AllRoles,
|
||||||
|
ActionListProcessingActivities: AllRoles,
|
||||||
|
ActionListSnapshots: AllRoles,
|
||||||
|
ActionListTrustCenterFiles: AllRoles,
|
||||||
|
ActionGetTrustCenter: AllRoles,
|
||||||
|
ActionGetCustomDomain: AllRoles,
|
||||||
|
ActionListSAMLConfigurations: AllRoles,
|
||||||
|
ActionConfirmEmail: AllRoles,
|
||||||
|
ActionAcceptInvitation: AllRoles,
|
||||||
|
|
||||||
|
ActionUpdateOrganization: EditRoles,
|
||||||
|
ActionDeleteOrganizationHorizontalLogo: EditRoles,
|
||||||
|
ActionCreateTrustCenter: EditRoles,
|
||||||
|
ActionInviteUser: EditRoles,
|
||||||
|
ActionDeleteInvitation: EditRoles,
|
||||||
|
ActionUpdateMembership: EditRoles,
|
||||||
|
ActionCreatePeople: EditRoles,
|
||||||
|
ActionCreateVendor: EditRoles,
|
||||||
|
ActionCreateFramework: EditRoles,
|
||||||
|
ActionImportFramework: EditRoles,
|
||||||
|
ActionCreateControl: EditRoles,
|
||||||
|
ActionCreateMeasure: EditRoles,
|
||||||
|
ActionImportMeasure: EditRoles,
|
||||||
|
ActionCreateMeeting: EditRoles,
|
||||||
|
ActionCreateTask: EditRoles,
|
||||||
|
ActionCreateRisk: EditRoles,
|
||||||
|
ActionCreateDocument: EditRoles,
|
||||||
|
ActionCreateAsset: EditRoles,
|
||||||
|
ActionCreateDatum: EditRoles,
|
||||||
|
ActionCreateAudit: EditRoles,
|
||||||
|
ActionCreateNonconformity: EditRoles,
|
||||||
|
ActionCreateObligation: EditRoles,
|
||||||
|
ActionCreateContinualImprovement: EditRoles,
|
||||||
|
ActionCreateProcessingActivity: EditRoles,
|
||||||
|
ActionCreateSnapshot: EditRoles,
|
||||||
|
ActionCreateTrustCenterFile: EditRoles,
|
||||||
|
ActionSendSigningNotifications: EditRoles,
|
||||||
|
|
||||||
|
ActionRemoveMember: {RoleOwner, RoleFull},
|
||||||
|
|
||||||
|
ActionCreateCustomDomain: {RoleOwner},
|
||||||
|
ActionInitiateDomainVerification: {RoleOwner},
|
||||||
|
ActionVerifyDomain: {RoleOwner},
|
||||||
|
ActionCreateSAMLConfiguration: {RoleOwner},
|
||||||
|
ActionDeleteOrganization: {RoleOwner},
|
||||||
|
},
|
||||||
|
coredata.TrustCenterEntityType: {
|
||||||
|
ActionGet: AllRoles,
|
||||||
|
ActionGetNdaFileUrl: AllRoles,
|
||||||
|
ActionGetOrganization: AllRoles,
|
||||||
|
ActionListAccesses: AllRoles,
|
||||||
|
ActionListReferences: AllRoles,
|
||||||
|
|
||||||
|
ActionUpdateTrustCenter: EditRoles,
|
||||||
|
ActionUploadTrustCenterNDA: EditRoles,
|
||||||
|
ActionDeleteTrustCenterNDA: EditRoles,
|
||||||
|
ActionCreateTrustCenterAccess: EditRoles,
|
||||||
|
ActionCreateTrustCenterReference: EditRoles,
|
||||||
|
},
|
||||||
|
coredata.TrustCenterAccessEntityType: {
|
||||||
|
ActionGet: AllRoles,
|
||||||
|
ActionActiveCount: AllRoles,
|
||||||
|
ActionPendingRequestCount: AllRoles,
|
||||||
|
ActionAvailableDocumentAccesses: AllRoles,
|
||||||
|
|
||||||
|
ActionUpdateTrustCenterAccess: EditRoles,
|
||||||
|
ActionDeleteTrustCenterAccess: EditRoles,
|
||||||
|
},
|
||||||
|
coredata.TrustCenterReferenceEntityType: {
|
||||||
|
ActionGet: AllRoles,
|
||||||
|
ActionGetLogoUrl: AllRoles,
|
||||||
|
|
||||||
|
ActionUpdateTrustCenterReference: EditRoles,
|
||||||
|
ActionDeleteTrustCenterReference: EditRoles,
|
||||||
|
},
|
||||||
|
coredata.TrustCenterFileEntityType: {
|
||||||
|
ActionGet: AllRoles,
|
||||||
|
ActionGetFileUrl: AllRoles,
|
||||||
|
|
||||||
|
ActionUpdateTrustCenterFile: EditRoles,
|
||||||
|
ActionGetTrustCenterFile: EditRoles,
|
||||||
|
ActionDeleteTrustCenterFile: EditRoles,
|
||||||
|
},
|
||||||
|
coredata.UserEntityType: {
|
||||||
|
ActionGet: AllRoles,
|
||||||
|
},
|
||||||
|
coredata.MembershipEntityType: {
|
||||||
|
ActionGet: AllRoles,
|
||||||
|
ActionGetAuthMethod: AllRoles,
|
||||||
|
},
|
||||||
|
coredata.InvitationEntityType: {
|
||||||
|
ActionGet: AllRoles,
|
||||||
|
ActionGetOrganization: AllRoles,
|
||||||
|
},
|
||||||
|
coredata.PeopleEntityType: {
|
||||||
|
ActionGet: AllRoles,
|
||||||
|
|
||||||
|
ActionUpdatePeople: EditRoles,
|
||||||
|
ActionDeletePeople: EditRoles,
|
||||||
|
},
|
||||||
|
coredata.VendorEntityType: {
|
||||||
|
ActionGet: AllRoles,
|
||||||
|
ActionGetOrganization: AllRoles,
|
||||||
|
ActionListComplianceReports: AllRoles,
|
||||||
|
ActionGetBusinessAssociateAgreement: AllRoles,
|
||||||
|
ActionGetDataPrivacyAgreement: AllRoles,
|
||||||
|
ActionListContacts: AllRoles,
|
||||||
|
ActionListServices: AllRoles,
|
||||||
|
ActionListRiskAssessments: AllRoles,
|
||||||
|
ActionGetBusinessOwner: AllRoles,
|
||||||
|
ActionGetSecurityOwner: AllRoles,
|
||||||
|
|
||||||
|
ActionUpdateVendor: EditRoles,
|
||||||
|
ActionDeleteVendor: EditRoles,
|
||||||
|
ActionCreateVendorContact: EditRoles,
|
||||||
|
ActionCreateVendorService: EditRoles,
|
||||||
|
ActionUploadVendorComplianceReport: EditRoles,
|
||||||
|
ActionUploadVendorBusinessAssociateAgreement: EditRoles,
|
||||||
|
ActionDeleteVendorBusinessAssociateAgreement: EditRoles,
|
||||||
|
ActionUploadVendorDataPrivacyAgreement: EditRoles,
|
||||||
|
ActionCreateVendorRiskAssessment: EditRoles,
|
||||||
|
ActionAssessVendor: EditRoles,
|
||||||
|
},
|
||||||
|
coredata.VendorComplianceReportEntityType: {
|
||||||
|
ActionGet: AllRoles,
|
||||||
|
ActionGetVendor: AllRoles,
|
||||||
|
ActionGetFile: AllRoles,
|
||||||
|
|
||||||
|
ActionDeleteVendorComplianceReport: EditRoles,
|
||||||
|
},
|
||||||
|
coredata.VendorBusinessAssociateAgreementEntityType: {
|
||||||
|
ActionGet: AllRoles,
|
||||||
|
ActionGetVendor: AllRoles,
|
||||||
|
ActionGetFileUrl: AllRoles,
|
||||||
|
|
||||||
|
ActionUpdateVendorBusinessAssociateAgreement: EditRoles,
|
||||||
|
ActionDeleteVendorBusinessAssociateAgreement: EditRoles,
|
||||||
|
},
|
||||||
|
coredata.VendorContactEntityType: {
|
||||||
|
ActionGet: AllRoles,
|
||||||
|
ActionGetVendor: AllRoles,
|
||||||
|
|
||||||
|
ActionUpdateVendorContact: EditRoles,
|
||||||
|
ActionDeleteVendorContact: EditRoles,
|
||||||
|
},
|
||||||
|
coredata.VendorServiceEntityType: {
|
||||||
|
ActionGet: AllRoles,
|
||||||
|
ActionGetVendor: AllRoles,
|
||||||
|
|
||||||
|
ActionUpdateVendorService: EditRoles,
|
||||||
|
ActionDeleteVendorService: EditRoles,
|
||||||
|
},
|
||||||
|
coredata.VendorDataPrivacyAgreementEntityType: {
|
||||||
|
ActionGet: AllRoles,
|
||||||
|
ActionGetVendor: AllRoles,
|
||||||
|
ActionGetFileUrl: AllRoles,
|
||||||
|
|
||||||
|
ActionUpdateVendorDataPrivacyAgreement: EditRoles,
|
||||||
|
ActionDeleteVendorDataPrivacyAgreement: EditRoles,
|
||||||
|
},
|
||||||
|
coredata.VendorRiskAssessmentEntityType: {
|
||||||
|
ActionGet: AllRoles,
|
||||||
|
},
|
||||||
|
coredata.FrameworkEntityType: {
|
||||||
|
ActionGet: AllRoles,
|
||||||
|
ActionGetOrganization: AllRoles,
|
||||||
|
ActionListControls: AllRoles,
|
||||||
|
|
||||||
|
ActionCreateControl: EditRoles,
|
||||||
|
ActionUpdateFramework: EditRoles,
|
||||||
|
ActionDeleteFramework: EditRoles,
|
||||||
|
ActionGenerateFrameworkStateOfApplicability: EditRoles,
|
||||||
|
ActionExportFramework: EditRoles,
|
||||||
|
},
|
||||||
|
coredata.ControlEntityType: {
|
||||||
|
ActionGet: AllRoles,
|
||||||
|
ActionGetFramework: AllRoles,
|
||||||
|
ActionListMeasures: AllRoles,
|
||||||
|
ActionListDocuments: AllRoles,
|
||||||
|
ActionListAudits: AllRoles,
|
||||||
|
ActionListSnapshots: AllRoles,
|
||||||
|
|
||||||
|
ActionUpdateControl: EditRoles,
|
||||||
|
ActionDeleteControl: EditRoles,
|
||||||
|
ActionCreateControlMeasureMapping: EditRoles,
|
||||||
|
ActionCreateControlDocumentMapping: EditRoles,
|
||||||
|
ActionDeleteControlMeasureMapping: EditRoles,
|
||||||
|
ActionDeleteControlDocumentMapping: EditRoles,
|
||||||
|
ActionCreateControlAuditMapping: EditRoles,
|
||||||
|
ActionDeleteControlAuditMapping: EditRoles,
|
||||||
|
ActionCreateControlSnapshotMapping: EditRoles,
|
||||||
|
ActionDeleteControlSnapshotMapping: EditRoles,
|
||||||
|
},
|
||||||
|
coredata.MeasureEntityType: {
|
||||||
|
ActionGet: AllRoles,
|
||||||
|
ActionListEvidences: AllRoles,
|
||||||
|
ActionListTasks: AllRoles,
|
||||||
|
ActionListRisks: AllRoles,
|
||||||
|
ActionListControls: AllRoles,
|
||||||
|
ActionTotalCount: AllRoles,
|
||||||
|
|
||||||
|
ActionUpdateMeasure: EditRoles,
|
||||||
|
ActionDeleteMeasure: EditRoles,
|
||||||
|
ActionUploadMeasureEvidence: EditRoles,
|
||||||
|
},
|
||||||
|
coredata.TaskEntityType: {
|
||||||
|
ActionGet: AllRoles,
|
||||||
|
ActionGetAssignedTo: AllRoles,
|
||||||
|
ActionGetOrganization: AllRoles,
|
||||||
|
ActionGetMeasure: AllRoles,
|
||||||
|
ActionListEvidences: AllRoles,
|
||||||
|
|
||||||
|
ActionUpdateTask: EditRoles,
|
||||||
|
ActionDeleteTask: EditRoles,
|
||||||
|
ActionAssignTask: EditRoles,
|
||||||
|
ActionUnassignTask: EditRoles,
|
||||||
|
},
|
||||||
|
coredata.EvidenceEntityType: {
|
||||||
|
ActionGet: AllRoles,
|
||||||
|
ActionGetFile: AllRoles,
|
||||||
|
ActionGetTask: AllRoles,
|
||||||
|
ActionGetMeasure: AllRoles,
|
||||||
|
|
||||||
|
ActionDeleteEvidence: EditRoles,
|
||||||
|
},
|
||||||
|
coredata.DocumentEntityType: {
|
||||||
|
ActionGet: AllRoles,
|
||||||
|
ActionExportDocumentVersionPDF: AllRoles,
|
||||||
|
ActionGetOwner: AllRoles,
|
||||||
|
ActionGetOrganization: AllRoles,
|
||||||
|
ActionListVersions: AllRoles,
|
||||||
|
ActionListControls: AllRoles,
|
||||||
|
ActionTotalCount: AllRoles,
|
||||||
|
|
||||||
|
ActionUpdateDocument: EditRoles,
|
||||||
|
ActionDeleteDocument: EditRoles,
|
||||||
|
ActionPublishDocumentVersion: EditRoles,
|
||||||
|
ActionBulkPublishDocumentVersions: EditRoles,
|
||||||
|
ActionBulkDeleteDocuments: EditRoles,
|
||||||
|
ActionBulkExportDocuments: EditRoles,
|
||||||
|
ActionGenerateDocumentChangelog: EditRoles,
|
||||||
|
ActionCreateDraftDocumentVersion: EditRoles,
|
||||||
|
ActionDeleteDraftDocumentVersion: EditRoles,
|
||||||
|
ActionUpdateDocumentVersion: EditRoles,
|
||||||
|
ActionRequestSignature: EditRoles,
|
||||||
|
ActionBulkRequestSignatures: EditRoles,
|
||||||
|
ActionSendSigningNotifications: EditRoles,
|
||||||
|
ActionCancelSignatureRequest: EditRoles,
|
||||||
|
},
|
||||||
|
coredata.DocumentVersionEntityType: {
|
||||||
|
ActionGet: AllRoles,
|
||||||
|
ActionGetFile: AllRoles,
|
||||||
|
ActionGetOwner: AllRoles,
|
||||||
|
ActionDocument: AllRoles,
|
||||||
|
ActionSignatures: AllRoles,
|
||||||
|
|
||||||
|
ActionUpdateDocumentVersion: EditRoles,
|
||||||
|
},
|
||||||
|
coredata.DocumentVersionSignatureEntityType: {
|
||||||
|
ActionGet: AllRoles,
|
||||||
|
ActionDocumentVersion: AllRoles,
|
||||||
|
ActionSignedBy: AllRoles,
|
||||||
|
},
|
||||||
|
coredata.RiskEntityType: {
|
||||||
|
ActionGet: AllRoles,
|
||||||
|
ActionGetOwner: AllRoles,
|
||||||
|
ActionGetOrganization: AllRoles,
|
||||||
|
ActionTotalCount: AllRoles,
|
||||||
|
ActionListControls: AllRoles,
|
||||||
|
ActionListMeasures: AllRoles,
|
||||||
|
ActionListDocuments: AllRoles,
|
||||||
|
ActionListObligations: AllRoles,
|
||||||
|
|
||||||
|
ActionUpdateRisk: EditRoles,
|
||||||
|
ActionDeleteRisk: EditRoles,
|
||||||
|
ActionCreateRiskMeasureMapping: EditRoles,
|
||||||
|
ActionDeleteRiskMeasureMapping: EditRoles,
|
||||||
|
ActionCreateRiskDocumentMapping: EditRoles,
|
||||||
|
ActionDeleteRiskDocumentMapping: EditRoles,
|
||||||
|
ActionCreateRiskObligationMapping: EditRoles,
|
||||||
|
ActionDeleteRiskObligationMapping: EditRoles,
|
||||||
|
},
|
||||||
|
coredata.AssetEntityType: {
|
||||||
|
ActionGet: AllRoles,
|
||||||
|
ActionGetOwner: AllRoles,
|
||||||
|
ActionListVendors: AllRoles,
|
||||||
|
ActionGetAssetType: AllRoles,
|
||||||
|
ActionGetOrganization: AllRoles,
|
||||||
|
|
||||||
|
ActionUpdateAsset: EditRoles,
|
||||||
|
ActionDeleteAsset: EditRoles,
|
||||||
|
},
|
||||||
|
coredata.DatumEntityType: {
|
||||||
|
ActionGet: AllRoles,
|
||||||
|
ActionGetOwner: AllRoles,
|
||||||
|
ActionGetOrganization: AllRoles,
|
||||||
|
ActionListVendors: AllRoles,
|
||||||
|
|
||||||
|
ActionUpdateDatum: EditRoles,
|
||||||
|
ActionDeleteDatum: EditRoles,
|
||||||
|
},
|
||||||
|
coredata.AuditEntityType: {
|
||||||
|
ActionGet: AllRoles,
|
||||||
|
ActionGetFile: AllRoles,
|
||||||
|
ActionGetFramework: AllRoles,
|
||||||
|
ActionGetOrganization: AllRoles,
|
||||||
|
ActionReport: AllRoles,
|
||||||
|
ActionReportUrl: AllRoles,
|
||||||
|
ActionListControls: AllRoles,
|
||||||
|
|
||||||
|
ActionUpdateAudit: EditRoles,
|
||||||
|
ActionDeleteAudit: EditRoles,
|
||||||
|
ActionUploadAuditReport: EditRoles,
|
||||||
|
ActionDeleteAuditReport: EditRoles,
|
||||||
|
},
|
||||||
|
coredata.ReportEntityType: {
|
||||||
|
ActionGet: AllRoles,
|
||||||
|
ActionGetFile: AllRoles,
|
||||||
|
ActionGetOrganization: AllRoles,
|
||||||
|
ActionGetSnapshot: AllRoles,
|
||||||
|
ActionDownloadUrl: AllRoles,
|
||||||
|
},
|
||||||
|
coredata.NonconformityEntityType: {
|
||||||
|
ActionGet: AllRoles,
|
||||||
|
ActionGetOwner: AllRoles,
|
||||||
|
ActionGetOrganization: AllRoles,
|
||||||
|
ActionAudit: AllRoles,
|
||||||
|
|
||||||
|
ActionUpdateNonconformity: EditRoles,
|
||||||
|
ActionDeleteNonconformity: EditRoles,
|
||||||
|
},
|
||||||
|
coredata.ObligationEntityType: {
|
||||||
|
ActionGet: AllRoles,
|
||||||
|
ActionGetOrganization: AllRoles,
|
||||||
|
ActionGetOwner: AllRoles,
|
||||||
|
ActionListRisks: AllRoles,
|
||||||
|
|
||||||
|
ActionUpdateObligation: EditRoles,
|
||||||
|
ActionDeleteObligation: EditRoles,
|
||||||
|
},
|
||||||
|
coredata.ContinualImprovementEntityType: {
|
||||||
|
ActionGet: AllRoles,
|
||||||
|
ActionGetOwner: AllRoles,
|
||||||
|
ActionGetOrganization: AllRoles,
|
||||||
|
|
||||||
|
ActionUpdateContinualImprovement: EditRoles,
|
||||||
|
ActionDeleteContinualImprovement: EditRoles,
|
||||||
|
},
|
||||||
|
coredata.ProcessingActivityEntityType: {
|
||||||
|
ActionGet: AllRoles,
|
||||||
|
ActionGetOrganization: AllRoles,
|
||||||
|
ActionListVendors: AllRoles,
|
||||||
|
|
||||||
|
ActionUpdateProcessingActivity: EditRoles,
|
||||||
|
ActionDeleteProcessingActivity: EditRoles,
|
||||||
|
},
|
||||||
|
coredata.SnapshotEntityType: {
|
||||||
|
ActionGet: AllRoles,
|
||||||
|
ActionGetOrganization: AllRoles,
|
||||||
|
ActionListControls: AllRoles,
|
||||||
|
|
||||||
|
ActionDeleteSnapshot: EditRoles,
|
||||||
|
},
|
||||||
|
coredata.CustomDomainEntityType: {
|
||||||
|
ActionGet: {RoleOwner, RoleAdmin},
|
||||||
|
|
||||||
|
ActionDeleteCustomDomain: {RoleOwner},
|
||||||
|
},
|
||||||
|
coredata.SAMLConfigurationEntityType: {
|
||||||
|
ActionGet: {RoleOwner, RoleAdmin},
|
||||||
|
ActionSpMetadataUrl: {RoleOwner, RoleAdmin},
|
||||||
|
ActionTestLoginUrl: {RoleOwner, RoleAdmin},
|
||||||
|
|
||||||
|
ActionUpdateSAMLConfiguration: {RoleOwner, RoleAdmin},
|
||||||
|
ActionDeleteSAMLConfiguration: {RoleOwner, RoleAdmin},
|
||||||
|
ActionEnableSAML: {RoleOwner, RoleAdmin},
|
||||||
|
ActionDisableSAML: {RoleOwner, RoleAdmin},
|
||||||
|
},
|
||||||
|
coredata.FileEntityType: {
|
||||||
|
ActionGet: AllRoles,
|
||||||
|
ActionDownloadUrl: AllRoles,
|
||||||
|
},
|
||||||
|
coredata.TrustCenterDocumentAccessEntityType: {
|
||||||
|
ActionGet: AllRoles,
|
||||||
|
ActionReport: AllRoles,
|
||||||
|
ActionTrustCenterFile: AllRoles,
|
||||||
|
},
|
||||||
|
coredata.MeetingEntityType: {
|
||||||
|
ActionGet: AllRoles,
|
||||||
|
ActionGetOrganization: AllRoles,
|
||||||
|
ActionTotalCount: AllRoles,
|
||||||
|
|
||||||
|
ActionUpdateMeeting: EditRoles,
|
||||||
|
ActionDeleteMeeting: EditRoles,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetPermissionsForAction(entityType uint16, action Action) []Role {
|
||||||
|
if entityActions, ok := Permissions[entityType]; ok {
|
||||||
|
if roles, ok := entityActions[action]; ok {
|
||||||
|
return roles
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetPermissionsByRole(userRole Role) map[string]map[Action]bool {
|
||||||
|
permissions := make(map[string]map[Action]bool)
|
||||||
|
|
||||||
|
for entityType, actions := range Permissions {
|
||||||
|
entityTypeName, ok := coredata.EntityModel(entityType)
|
||||||
|
if !ok {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if permissions[entityTypeName] == nil {
|
||||||
|
permissions[entityTypeName] = make(map[Action]bool)
|
||||||
|
}
|
||||||
|
|
||||||
|
for action, allowedRoles := range actions {
|
||||||
|
if slices.Contains(allowedRoles, userRole) {
|
||||||
|
permissions[entityTypeName][action] = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return permissions
|
||||||
|
}
|
||||||
@@ -19,6 +19,7 @@ import (
|
|||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/url"
|
"net/url"
|
||||||
|
"slices"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"go.gearno.de/kit/pg"
|
"go.gearno.de/kit/pg"
|
||||||
@@ -37,6 +38,14 @@ func (e *TenantAccessError) Error() string {
|
|||||||
return "not authorized"
|
return "not authorized"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type PermissionDeniedError struct {
|
||||||
|
Message string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *PermissionDeniedError) Error() string {
|
||||||
|
return e.Message
|
||||||
|
}
|
||||||
|
|
||||||
type (
|
type (
|
||||||
Service struct {
|
Service struct {
|
||||||
pg *pg.Client
|
pg *pg.Client
|
||||||
@@ -103,6 +112,27 @@ func (s *Service) GetAllUserOrganizations(
|
|||||||
return organizations, err
|
return organizations, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *Service) GetUserOrganizationsWithRole(
|
||||||
|
ctx context.Context,
|
||||||
|
userID gid.GID,
|
||||||
|
role coredata.MembershipRole,
|
||||||
|
) (coredata.Organizations, error) {
|
||||||
|
organizations := coredata.Organizations{}
|
||||||
|
|
||||||
|
err := s.pg.WithConn(
|
||||||
|
ctx,
|
||||||
|
func(conn pg.Conn) error {
|
||||||
|
if err := organizations.LoadAllByUserIDWithRole(ctx, conn, userID, role); err != nil {
|
||||||
|
return fmt.Errorf("cannot load user organizations with role: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
return organizations, err
|
||||||
|
}
|
||||||
|
|
||||||
func (s *Service) GetAllOrganizationsForUserAPIKeyId(
|
func (s *Service) GetAllOrganizationsForUserAPIKeyId(
|
||||||
ctx context.Context,
|
ctx context.Context,
|
||||||
userAPIKeyID gid.GID,
|
userAPIKeyID gid.GID,
|
||||||
@@ -143,70 +173,6 @@ func (s *Service) GetUserOrganizations(
|
|||||||
return organizations, err
|
return organizations, err
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Service) AcceptInvitation(
|
|
||||||
ctx context.Context,
|
|
||||||
token string,
|
|
||||||
userID gid.GID,
|
|
||||||
) error {
|
|
||||||
payload, err := statelesstoken.ValidateToken[coredata.InvitationData](
|
|
||||||
s.tokenSecret,
|
|
||||||
TokenTypeOrganizationInvitation,
|
|
||||||
token,
|
|
||||||
)
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("invalid invitation token: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
invitationData := payload.Data
|
|
||||||
scope := coredata.NewScope(invitationData.InvitationID.TenantID())
|
|
||||||
|
|
||||||
return s.pg.WithTx(
|
|
||||||
ctx,
|
|
||||||
func(tx pg.Conn) error {
|
|
||||||
invitation := &coredata.Invitation{}
|
|
||||||
if err := invitation.LoadByID(ctx, tx, scope, invitationData.InvitationID); err != nil {
|
|
||||||
var errInvitationNotFound *coredata.ErrInvitationNotFound
|
|
||||||
if errors.As(err, &errInvitationNotFound) {
|
|
||||||
return fmt.Errorf("invitation was deleted or no longer exists")
|
|
||||||
}
|
|
||||||
return fmt.Errorf("cannot load invitation: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if invitation.AcceptedAt != nil {
|
|
||||||
return fmt.Errorf("invitation already accepted")
|
|
||||||
}
|
|
||||||
|
|
||||||
if time.Now().After(invitation.ExpiresAt) {
|
|
||||||
return fmt.Errorf("invitation expired")
|
|
||||||
}
|
|
||||||
|
|
||||||
now := time.Now()
|
|
||||||
membershipID := gid.New(scope.GetTenantID(), coredata.MembershipEntityType)
|
|
||||||
|
|
||||||
membership := &coredata.Membership{
|
|
||||||
ID: membershipID,
|
|
||||||
UserID: userID,
|
|
||||||
OrganizationID: invitation.OrganizationID,
|
|
||||||
Role: invitation.Role,
|
|
||||||
CreatedAt: now,
|
|
||||||
UpdatedAt: now,
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := membership.Create(ctx, tx, scope); err != nil {
|
|
||||||
return fmt.Errorf("cannot add user to organization: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
invitation.AcceptedAt = &now
|
|
||||||
if err := invitation.Update(ctx, tx, scope); err != nil {
|
|
||||||
return fmt.Errorf("cannot mark invitation as accepted: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
},
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *Service) AcceptInvitationByID(
|
func (s *Service) AcceptInvitationByID(
|
||||||
ctx context.Context,
|
ctx context.Context,
|
||||||
invitationID gid.GID,
|
invitationID gid.GID,
|
||||||
@@ -274,36 +240,11 @@ func (s *Service) AcceptInvitationByID(
|
|||||||
return acceptedInvitation, nil
|
return acceptedInvitation, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Service) GetUserInvitations(
|
|
||||||
ctx context.Context,
|
|
||||||
email string,
|
|
||||||
cursor *page.Cursor[coredata.InvitationOrderField],
|
|
||||||
filter *coredata.InvitationFilter,
|
|
||||||
) (*page.Page[*coredata.Invitation, coredata.InvitationOrderField], error) {
|
|
||||||
var invitations coredata.Invitations
|
|
||||||
|
|
||||||
err := s.pg.WithConn(
|
|
||||||
ctx,
|
|
||||||
func(conn pg.Conn) error {
|
|
||||||
if err := invitations.LoadByEmail(ctx, conn, coredata.NewNoScope(), email, cursor, filter); err != nil {
|
|
||||||
return fmt.Errorf("cannot load invitations: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
},
|
|
||||||
)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return page.NewPage(invitations, cursor), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
type UserInvitation struct {
|
type UserInvitation struct {
|
||||||
ID gid.GID
|
ID gid.GID
|
||||||
Email string
|
Email string
|
||||||
FullName string
|
FullName string
|
||||||
Role coredata.Role
|
Role coredata.MembershipRole
|
||||||
ExpiresAt time.Time
|
ExpiresAt time.Time
|
||||||
AcceptedAt *time.Time
|
AcceptedAt *time.Time
|
||||||
CreatedAt time.Time
|
CreatedAt time.Time
|
||||||
@@ -417,7 +358,7 @@ func (s *TenantAuthzService) AddUserToOrganization(
|
|||||||
ctx context.Context,
|
ctx context.Context,
|
||||||
userID gid.GID,
|
userID gid.GID,
|
||||||
orgID gid.GID,
|
orgID gid.GID,
|
||||||
role coredata.Role,
|
role coredata.MembershipRole,
|
||||||
) error {
|
) error {
|
||||||
now := time.Now()
|
now := time.Now()
|
||||||
membershipID := gid.New(s.scope.GetTenantID(), coredata.MembershipEntityType)
|
membershipID := gid.New(s.scope.GetTenantID(), coredata.MembershipEntityType)
|
||||||
@@ -538,6 +479,30 @@ func (s *TenantAuthzService) DeleteInvitation(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *TenantAuthzService) GetMembershipByUserAndOrganizationID(
|
||||||
|
ctx context.Context,
|
||||||
|
userID gid.GID,
|
||||||
|
orgID gid.GID,
|
||||||
|
) (*coredata.Membership, error) {
|
||||||
|
membership := &coredata.Membership{}
|
||||||
|
|
||||||
|
err := s.pg.WithConn(
|
||||||
|
ctx,
|
||||||
|
func(conn pg.Conn) error {
|
||||||
|
if err := membership.LoadByUserAndOrg(ctx, conn, s.scope, userID, orgID); err != nil {
|
||||||
|
return fmt.Errorf("cannot load membership: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return membership, nil
|
||||||
|
}
|
||||||
|
|
||||||
func (s *TenantAuthzService) GetMembershipsByOrganizationID(
|
func (s *TenantAuthzService) GetMembershipsByOrganizationID(
|
||||||
ctx context.Context,
|
ctx context.Context,
|
||||||
orgID gid.GID,
|
orgID gid.GID,
|
||||||
@@ -609,41 +574,11 @@ func (s *TenantAuthzService) CountOrganizationUsers(
|
|||||||
return count, nil
|
return count, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *TenantAuthzService) CanUserAccessOrganization(
|
|
||||||
ctx context.Context,
|
|
||||||
userID gid.GID,
|
|
||||||
orgID gid.GID,
|
|
||||||
) (bool, error) {
|
|
||||||
membership := &coredata.Membership{}
|
|
||||||
|
|
||||||
haveAccess := false
|
|
||||||
|
|
||||||
err := s.pg.WithConn(
|
|
||||||
ctx,
|
|
||||||
func(conn pg.Conn) error {
|
|
||||||
if err := membership.LoadByUserAndOrg(ctx, conn, s.scope, userID, orgID); err != nil {
|
|
||||||
if _, ok := err.(coredata.ErrMembershipNotFound); ok {
|
|
||||||
return nil // Not an error, just no access
|
|
||||||
}
|
|
||||||
return fmt.Errorf("cannot check organization access: %w", err)
|
|
||||||
}
|
|
||||||
haveAccess = true
|
|
||||||
return nil
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
return false, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return haveAccess, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *TenantAuthzService) GetUserRoleInOrganization(
|
func (s *TenantAuthzService) GetUserRoleInOrganization(
|
||||||
ctx context.Context,
|
ctx context.Context,
|
||||||
userID gid.GID,
|
userID gid.GID,
|
||||||
orgID gid.GID,
|
orgID gid.GID,
|
||||||
) (coredata.Role, error) {
|
) (coredata.MembershipRole, error) {
|
||||||
membership := &coredata.Membership{}
|
membership := &coredata.Membership{}
|
||||||
|
|
||||||
err := s.pg.WithConn(
|
err := s.pg.WithConn(
|
||||||
@@ -690,30 +625,55 @@ func (s *TenantAuthzService) RemoveMemberFromOrganization(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *TenantAuthzService) UpdateUserRole(
|
func (s *TenantAuthzService) UpdateMembershipRole(
|
||||||
ctx context.Context,
|
ctx context.Context,
|
||||||
userID gid.GID,
|
|
||||||
orgID gid.GID,
|
orgID gid.GID,
|
||||||
newRole coredata.Role,
|
memberID gid.GID,
|
||||||
) error {
|
newRole coredata.MembershipRole,
|
||||||
return s.pg.WithTx(
|
) (*coredata.Membership, error) {
|
||||||
|
membership := &coredata.Membership{}
|
||||||
|
|
||||||
|
err := s.pg.WithTx(
|
||||||
ctx,
|
ctx,
|
||||||
func(tx pg.Conn) error {
|
func(tx pg.Conn) error {
|
||||||
membership := &coredata.Membership{}
|
if err := membership.LoadByID(ctx, tx, s.scope, memberID); err != nil {
|
||||||
if err := membership.LoadByUserAndOrg(ctx, tx, s.scope, userID, orgID); err != nil {
|
return fmt.Errorf("cannot load membership: %w", err)
|
||||||
return fmt.Errorf("cannot find membership: %w", err)
|
}
|
||||||
|
|
||||||
|
if membership.OrganizationID != orgID {
|
||||||
|
return fmt.Errorf("membership does not belong to organization")
|
||||||
|
}
|
||||||
|
|
||||||
|
// If the new role cannot create API keys, delete all related API key memberships
|
||||||
|
if newRole != coredata.MembershipRoleOwner {
|
||||||
|
var apiKeyMemberships coredata.UserAPIKeyMemberships
|
||||||
|
if err := apiKeyMemberships.LoadByMembershipID(ctx, tx, s.scope, memberID); err != nil {
|
||||||
|
return fmt.Errorf("cannot load api key memberships: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, apiKeyMembership := range apiKeyMemberships {
|
||||||
|
if err := apiKeyMembership.Delete(ctx, tx, s.scope); err != nil {
|
||||||
|
return fmt.Errorf("cannot delete api key membership: %w", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
membership.Role = newRole
|
membership.Role = newRole
|
||||||
membership.UpdatedAt = time.Now()
|
membership.UpdatedAt = time.Now()
|
||||||
|
|
||||||
if err := membership.Update(ctx, tx, s.scope); err != nil {
|
if err := membership.Update(ctx, tx, s.scope); err != nil {
|
||||||
return fmt.Errorf("cannot update user role: %w", err)
|
return fmt.Errorf("cannot update membership role: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return membership, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *TenantAuthzService) InviteUserToOrganization(
|
func (s *TenantAuthzService) InviteUserToOrganization(
|
||||||
@@ -721,7 +681,7 @@ func (s *TenantAuthzService) InviteUserToOrganization(
|
|||||||
organizationID gid.GID,
|
organizationID gid.GID,
|
||||||
emailAddress string,
|
emailAddress string,
|
||||||
fullName string,
|
fullName string,
|
||||||
role coredata.Role,
|
role coredata.MembershipRole,
|
||||||
) (*coredata.Invitation, error) {
|
) (*coredata.Invitation, error) {
|
||||||
var invitation *coredata.Invitation
|
var invitation *coredata.Invitation
|
||||||
|
|
||||||
@@ -827,7 +787,7 @@ func (s *TenantAuthzService) EnsureSAMLMembership(
|
|||||||
ctx context.Context,
|
ctx context.Context,
|
||||||
userID gid.GID,
|
userID gid.GID,
|
||||||
organizationID gid.GID,
|
organizationID gid.GID,
|
||||||
role *coredata.Role,
|
role *coredata.MembershipRole,
|
||||||
) error {
|
) error {
|
||||||
now := time.Now()
|
now := time.Now()
|
||||||
|
|
||||||
@@ -842,7 +802,7 @@ func (s *TenantAuthzService) EnsureSAMLMembership(
|
|||||||
return fmt.Errorf("cannot load membership: %w", err)
|
return fmt.Errorf("cannot load membership: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
membershipRole := coredata.RoleMember
|
membershipRole := coredata.MembershipRoleViewer
|
||||||
if role != nil {
|
if role != nil {
|
||||||
membershipRole = *role
|
membershipRole = *role
|
||||||
}
|
}
|
||||||
@@ -878,15 +838,94 @@ func (s *TenantAuthzService) EnsureSAMLMembership(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
// This is a placeholder for future permission system
|
func (s *TenantAuthzService) Authorize(
|
||||||
func (s *TenantAuthzService) HasPermission(
|
|
||||||
ctx context.Context,
|
ctx context.Context,
|
||||||
userID gid.GID,
|
user *coredata.User,
|
||||||
orgID gid.GID,
|
apiKey *coredata.UserAPIKey,
|
||||||
resource string,
|
entityGID gid.GID,
|
||||||
action string,
|
action Action,
|
||||||
) (bool, error) {
|
) error {
|
||||||
// For now, just check if user is a member
|
requiredRoles := GetPermissionsForAction(entityGID.EntityType(), action)
|
||||||
// In the future, this will check specific permissions based on role
|
if requiredRoles == nil {
|
||||||
return s.CanUserAccessOrganization(ctx, userID, orgID)
|
return &PermissionDeniedError{
|
||||||
|
Message: fmt.Sprintf("no permissions defined for action %s on entity type %d", action, entityGID.EntityType()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
role, err := s.GetUserOrAPIKeyRole(ctx, user, apiKey, entityGID)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("cannot get user or API key role: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if !slices.Contains(requiredRoles, role) {
|
||||||
|
return &PermissionDeniedError{
|
||||||
|
Message: fmt.Sprintf("role %s not authorized for action %s, requires one of %v", role, action, requiredRoles),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *TenantAuthzService) CanAssignRole(
|
||||||
|
ctx context.Context,
|
||||||
|
user *coredata.User,
|
||||||
|
apiKey *coredata.UserAPIKey,
|
||||||
|
entityGID gid.GID,
|
||||||
|
targetRole coredata.MembershipRole,
|
||||||
|
) error {
|
||||||
|
currentRole, err := s.GetUserOrAPIKeyRole(ctx, user, apiKey, entityGID)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("cannot get user or API key role: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if currentRole == RoleOwner || currentRole == RoleFull {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if currentRole == RoleAdmin {
|
||||||
|
if targetRole == coredata.MembershipRoleOwner {
|
||||||
|
return &PermissionDeniedError{Message: "admin users cannot assign owner role"}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return &PermissionDeniedError{Message: fmt.Sprintf("role %s cannot assign roles", currentRole)}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *TenantAuthzService) GetUserOrAPIKeyRole(
|
||||||
|
ctx context.Context,
|
||||||
|
user *coredata.User,
|
||||||
|
apiKey *coredata.UserAPIKey,
|
||||||
|
entityGID gid.GID,
|
||||||
|
) (Role, error) {
|
||||||
|
var role Role
|
||||||
|
err := s.pg.WithConn(
|
||||||
|
ctx,
|
||||||
|
func(conn pg.Conn) error {
|
||||||
|
if user != nil {
|
||||||
|
membership := &coredata.Membership{}
|
||||||
|
if err := membership.LoadRoleByUserAndEntityID(ctx, conn, s.scope, user.ID, entityGID); err != nil {
|
||||||
|
return fmt.Errorf("cannot get user role: %w", err)
|
||||||
|
}
|
||||||
|
role = Role(membership.Role.String())
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if apiKey != nil {
|
||||||
|
apiKeyMembership := &coredata.UserAPIKeyMembership{}
|
||||||
|
if err := apiKeyMembership.LoadRoleByAPIKeyAndEntityID(ctx, conn, s.scope, apiKey.ID, entityGID); err != nil {
|
||||||
|
return fmt.Errorf("cannot get API key role: %w", err)
|
||||||
|
}
|
||||||
|
role = Role(apiKeyMembership.Role.String())
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return fmt.Errorf("no user or API key provided")
|
||||||
|
},
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
return role, nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -47,13 +47,14 @@ func (a *UserAPIKeyMembership) Insert(
|
|||||||
) error {
|
) error {
|
||||||
q := `
|
q := `
|
||||||
INSERT INTO
|
INSERT INTO
|
||||||
authz_api_keys_memberships (id, tenant_id, auth_user_api_key_id, membership_id, role, created_at, updated_at)
|
authz_api_keys_memberships (id, tenant_id, auth_user_api_key_id, membership_id, role, organization_id, created_at, updated_at)
|
||||||
VALUES (
|
VALUES (
|
||||||
@id,
|
@id,
|
||||||
@tenant_id,
|
@tenant_id,
|
||||||
@auth_user_api_key_id,
|
@auth_user_api_key_id,
|
||||||
@membership_id,
|
@membership_id,
|
||||||
@role,
|
@role,
|
||||||
|
@organization_id,
|
||||||
@created_at,
|
@created_at,
|
||||||
@updated_at
|
@updated_at
|
||||||
)
|
)
|
||||||
@@ -65,6 +66,7 @@ VALUES (
|
|||||||
"auth_user_api_key_id": a.UserAPIKeyID,
|
"auth_user_api_key_id": a.UserAPIKeyID,
|
||||||
"membership_id": a.MembershipID,
|
"membership_id": a.MembershipID,
|
||||||
"role": a.Role,
|
"role": a.Role,
|
||||||
|
"organization_id": a.OrganizationID,
|
||||||
"created_at": a.CreatedAt,
|
"created_at": a.CreatedAt,
|
||||||
"updated_at": a.UpdatedAt,
|
"updated_at": a.UpdatedAt,
|
||||||
}
|
}
|
||||||
@@ -127,6 +129,133 @@ ORDER BY akm.created_at DESC
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// LoadRoleByAPIKeyAndEntityID loads an API key's role by querying any entity to extract its organization_id
|
||||||
|
func (a *UserAPIKeyMembership) LoadRoleByAPIKeyAndEntityID(
|
||||||
|
ctx context.Context,
|
||||||
|
conn pg.Conn,
|
||||||
|
scope Scoper,
|
||||||
|
apiKeyID gid.GID,
|
||||||
|
entityID gid.GID,
|
||||||
|
) error {
|
||||||
|
entityType := entityID.EntityType()
|
||||||
|
|
||||||
|
// For organization, the entity ID is the organization ID
|
||||||
|
if entityType == OrganizationEntityType {
|
||||||
|
return a.LoadByAPIKeyIDAndOrganizationID(ctx, conn, scope, apiKeyID, entityID)
|
||||||
|
}
|
||||||
|
|
||||||
|
tableName, ok := EntityTable(entityType)
|
||||||
|
if !ok {
|
||||||
|
return fmt.Errorf("unsupported entity type for API key role lookup: %d", entityType)
|
||||||
|
}
|
||||||
|
|
||||||
|
query := fmt.Sprintf(`
|
||||||
|
SELECT
|
||||||
|
akm.id,
|
||||||
|
akm.auth_user_api_key_id,
|
||||||
|
akm.membership_id,
|
||||||
|
akm.role,
|
||||||
|
akm.created_at,
|
||||||
|
akm.updated_at
|
||||||
|
FROM
|
||||||
|
authz_api_keys_memberships akm
|
||||||
|
INNER JOIN authz_memberships m ON m.id = akm.membership_id
|
||||||
|
INNER JOIN %s e ON e.id = @entity_id
|
||||||
|
WHERE
|
||||||
|
%s
|
||||||
|
AND akm.auth_user_api_key_id = @api_key_id
|
||||||
|
AND m.organization_id = e.organization_id
|
||||||
|
LIMIT 1;
|
||||||
|
`, tableName, scope.SQLFragment())
|
||||||
|
|
||||||
|
args := pgx.NamedArgs{
|
||||||
|
"api_key_id": apiKeyID,
|
||||||
|
"entity_id": entityID,
|
||||||
|
}
|
||||||
|
maps.Copy(args, scope.SQLArguments())
|
||||||
|
|
||||||
|
rows, err := conn.Query(ctx, query, args)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("cannot query API key membership by entity: %w", err)
|
||||||
|
}
|
||||||
|
defer rows.Close()
|
||||||
|
|
||||||
|
if !rows.Next() {
|
||||||
|
return fmt.Errorf("API key membership not found for key %s and entity %s", apiKeyID, entityID)
|
||||||
|
}
|
||||||
|
|
||||||
|
var membership UserAPIKeyMembership
|
||||||
|
err = rows.Scan(
|
||||||
|
&membership.ID,
|
||||||
|
&membership.UserAPIKeyID,
|
||||||
|
&membership.MembershipID,
|
||||||
|
&membership.Role,
|
||||||
|
&membership.CreatedAt,
|
||||||
|
&membership.UpdatedAt,
|
||||||
|
)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("cannot scan API key membership: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
*a = membership
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *UserAPIKeyMembership) LoadByAPIKeyIDAndOrganizationID(
|
||||||
|
ctx context.Context,
|
||||||
|
conn pg.Conn,
|
||||||
|
scope Scoper,
|
||||||
|
apiKeyID gid.GID,
|
||||||
|
organizationID gid.GID,
|
||||||
|
) error {
|
||||||
|
q := `
|
||||||
|
SELECT
|
||||||
|
akm.id,
|
||||||
|
akm.auth_user_api_key_id,
|
||||||
|
akm.membership_id,
|
||||||
|
akm.role,
|
||||||
|
akm.created_at,
|
||||||
|
akm.updated_at,
|
||||||
|
m.organization_id,
|
||||||
|
o.name as organization_name
|
||||||
|
FROM
|
||||||
|
authz_api_keys_memberships akm
|
||||||
|
JOIN
|
||||||
|
authz_memberships m ON akm.membership_id = m.id
|
||||||
|
JOIN
|
||||||
|
organizations o ON m.organization_id = o.id
|
||||||
|
WHERE
|
||||||
|
akm.auth_user_api_key_id = @api_key_id
|
||||||
|
AND m.organization_id = @organization_id
|
||||||
|
AND m.%s
|
||||||
|
`
|
||||||
|
|
||||||
|
q = fmt.Sprintf(q, scope.SQLFragment())
|
||||||
|
|
||||||
|
args := pgx.StrictNamedArgs{
|
||||||
|
"api_key_id": apiKeyID,
|
||||||
|
"organization_id": organizationID,
|
||||||
|
}
|
||||||
|
maps.Copy(args, scope.SQLArguments())
|
||||||
|
|
||||||
|
rows, err := conn.Query(ctx, q, args)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("cannot query user api key membership: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
membership, err := pgx.CollectExactlyOneRow(rows, pgx.RowToStructByName[UserAPIKeyMembership])
|
||||||
|
if err != nil {
|
||||||
|
if err == pgx.ErrNoRows {
|
||||||
|
return fmt.Errorf("API key does not have access to organization")
|
||||||
|
}
|
||||||
|
return fmt.Errorf("cannot collect user api key membership: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
*a = membership
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
func (a *UserAPIKeyMembership) Delete(
|
func (a *UserAPIKeyMembership) Delete(
|
||||||
ctx context.Context,
|
ctx context.Context,
|
||||||
conn pg.Conn,
|
conn pg.Conn,
|
||||||
@@ -155,6 +284,56 @@ WHERE
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (a *UserAPIKeyMemberships) LoadByMembershipID(
|
||||||
|
ctx context.Context,
|
||||||
|
conn pg.Conn,
|
||||||
|
scope Scoper,
|
||||||
|
membershipID gid.GID,
|
||||||
|
) error {
|
||||||
|
q := `
|
||||||
|
SELECT
|
||||||
|
akm.id,
|
||||||
|
akm.auth_user_api_key_id,
|
||||||
|
akm.membership_id,
|
||||||
|
akm.role,
|
||||||
|
akm.created_at,
|
||||||
|
akm.updated_at,
|
||||||
|
m.organization_id,
|
||||||
|
o.name as organization_name
|
||||||
|
FROM
|
||||||
|
authz_api_keys_memberships akm
|
||||||
|
JOIN
|
||||||
|
authz_memberships m ON akm.membership_id = m.id
|
||||||
|
JOIN
|
||||||
|
organizations o ON m.organization_id = o.id
|
||||||
|
WHERE
|
||||||
|
akm.membership_id = @membership_id
|
||||||
|
AND m.%s
|
||||||
|
ORDER BY akm.created_at DESC
|
||||||
|
`
|
||||||
|
|
||||||
|
q = fmt.Sprintf(q, scope.SQLFragment())
|
||||||
|
|
||||||
|
args := pgx.StrictNamedArgs{
|
||||||
|
"membership_id": membershipID,
|
||||||
|
}
|
||||||
|
maps.Copy(args, scope.SQLArguments())
|
||||||
|
|
||||||
|
rows, err := conn.Query(ctx, q, args)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("cannot query user api key memberships by membership id: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
memberships, err := pgx.CollectRows(rows, pgx.RowToAddrOfStructByName[UserAPIKeyMembership])
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("cannot collect user api key memberships: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
*a = memberships
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
func DeleteAllUserAPIKeyMembershipsByUserAPIKeyID(
|
func DeleteAllUserAPIKeyMembershipsByUserAPIKeyID(
|
||||||
ctx context.Context,
|
ctx context.Context,
|
||||||
conn pg.Conn,
|
conn pg.Conn,
|
||||||
|
|||||||
@@ -46,6 +46,7 @@ func (av AssetVendors) Merge(
|
|||||||
conn pg.Conn,
|
conn pg.Conn,
|
||||||
scope Scoper,
|
scope Scoper,
|
||||||
assetID gid.GID,
|
assetID gid.GID,
|
||||||
|
organizationID gid.GID,
|
||||||
vendorIDs []gid.GID,
|
vendorIDs []gid.GID,
|
||||||
) error {
|
) error {
|
||||||
q := `
|
q := `
|
||||||
@@ -54,6 +55,7 @@ WITH vendor_ids AS (
|
|||||||
unnest(@vendor_ids::text[]) AS vendor_id,
|
unnest(@vendor_ids::text[]) AS vendor_id,
|
||||||
@tenant_id AS tenant_id,
|
@tenant_id AS tenant_id,
|
||||||
@asset_id AS asset_id,
|
@asset_id AS asset_id,
|
||||||
|
@organization_id AS organization_id,
|
||||||
@created_at::timestamptz AS created_at
|
@created_at::timestamptz AS created_at
|
||||||
)
|
)
|
||||||
MERGE INTO asset_vendors AS tgt
|
MERGE INTO asset_vendors AS tgt
|
||||||
@@ -62,8 +64,8 @@ ON tgt.tenant_id = src.tenant_id
|
|||||||
AND tgt.asset_id = src.asset_id
|
AND tgt.asset_id = src.asset_id
|
||||||
AND tgt.vendor_id = src.vendor_id
|
AND tgt.vendor_id = src.vendor_id
|
||||||
WHEN NOT MATCHED
|
WHEN NOT MATCHED
|
||||||
THEN INSERT (tenant_id, asset_id, vendor_id, created_at)
|
THEN INSERT (tenant_id, asset_id, vendor_id, organization_id, created_at)
|
||||||
VALUES (src.tenant_id, src.asset_id, src.vendor_id, src.created_at)
|
VALUES (src.tenant_id, src.asset_id, src.vendor_id, src.organization_id, src.created_at)
|
||||||
WHEN NOT MATCHED BY SOURCE
|
WHEN NOT MATCHED BY SOURCE
|
||||||
AND tgt.tenant_id = @tenant_id AND tgt.asset_id = @asset_id
|
AND tgt.tenant_id = @tenant_id AND tgt.asset_id = @asset_id
|
||||||
THEN DELETE
|
THEN DELETE
|
||||||
@@ -72,6 +74,7 @@ WHEN NOT MATCHED
|
|||||||
args := pgx.StrictNamedArgs{
|
args := pgx.StrictNamedArgs{
|
||||||
"tenant_id": scope.GetTenantID(),
|
"tenant_id": scope.GetTenantID(),
|
||||||
"asset_id": assetID,
|
"asset_id": assetID,
|
||||||
|
"organization_id": organizationID,
|
||||||
"created_at": time.Now(),
|
"created_at": time.Now(),
|
||||||
"vendor_ids": vendorIDs,
|
"vendor_ids": vendorIDs,
|
||||||
}
|
}
|
||||||
@@ -89,17 +92,19 @@ func (av AssetVendors) Insert(
|
|||||||
conn pg.Conn,
|
conn pg.Conn,
|
||||||
scope Scoper,
|
scope Scoper,
|
||||||
assetID gid.GID,
|
assetID gid.GID,
|
||||||
|
organizationID gid.GID,
|
||||||
vendorIDs []gid.GID,
|
vendorIDs []gid.GID,
|
||||||
) error {
|
) error {
|
||||||
q := `
|
q := `
|
||||||
WITH vendor_ids AS (
|
WITH vendor_ids AS (
|
||||||
SELECT unnest(@vendor_ids::text[]) AS vendor_id
|
SELECT unnest(@vendor_ids::text[]) AS vendor_id
|
||||||
)
|
)
|
||||||
INSERT INTO asset_vendors (tenant_id, asset_id, vendor_id, created_at)
|
INSERT INTO asset_vendors (tenant_id, asset_id, vendor_id, organization_id, created_at)
|
||||||
SELECT
|
SELECT
|
||||||
@tenant_id AS tenant_id,
|
@tenant_id AS tenant_id,
|
||||||
@asset_id AS asset_id,
|
@asset_id AS asset_id,
|
||||||
vendor_id,
|
vendor_id,
|
||||||
|
@organization_id AS organization_id,
|
||||||
@created_at AS created_at
|
@created_at AS created_at
|
||||||
FROM vendor_ids
|
FROM vendor_ids
|
||||||
`
|
`
|
||||||
@@ -107,6 +112,7 @@ FROM vendor_ids
|
|||||||
args := pgx.StrictNamedArgs{
|
args := pgx.StrictNamedArgs{
|
||||||
"tenant_id": scope.GetTenantID(),
|
"tenant_id": scope.GetTenantID(),
|
||||||
"asset_id": assetID,
|
"asset_id": assetID,
|
||||||
|
"organization_id": organizationID,
|
||||||
"created_at": time.Now(),
|
"created_at": time.Now(),
|
||||||
"vendor_ids": vendorIDs,
|
"vendor_ids": vendorIDs,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -31,6 +31,7 @@ import (
|
|||||||
type (
|
type (
|
||||||
Control struct {
|
Control struct {
|
||||||
ID gid.GID `db:"id"`
|
ID gid.GID `db:"id"`
|
||||||
|
OrganizationID gid.GID `db:"organization_id"`
|
||||||
SectionTitle string `db:"section_title"`
|
SectionTitle string `db:"section_title"`
|
||||||
FrameworkID gid.GID `db:"framework_id"`
|
FrameworkID gid.GID `db:"framework_id"`
|
||||||
Name string `db:"name"`
|
Name string `db:"name"`
|
||||||
@@ -127,6 +128,7 @@ WITH ctrl AS (
|
|||||||
c.id,
|
c.id,
|
||||||
c.section_title,
|
c.section_title,
|
||||||
c.framework_id,
|
c.framework_id,
|
||||||
|
c.organization_id,
|
||||||
c.tenant_id,
|
c.tenant_id,
|
||||||
c.name,
|
c.name,
|
||||||
c.description,
|
c.description,
|
||||||
@@ -146,6 +148,7 @@ SELECT
|
|||||||
id,
|
id,
|
||||||
section_title,
|
section_title,
|
||||||
framework_id,
|
framework_id,
|
||||||
|
organization_id,
|
||||||
name,
|
name,
|
||||||
description,
|
description,
|
||||||
status,
|
status,
|
||||||
@@ -236,6 +239,7 @@ WITH ctrl AS (
|
|||||||
c.id,
|
c.id,
|
||||||
c.section_title,
|
c.section_title,
|
||||||
c.framework_id,
|
c.framework_id,
|
||||||
|
c.organization_id,
|
||||||
c.tenant_id,
|
c.tenant_id,
|
||||||
c.name,
|
c.name,
|
||||||
c.description,
|
c.description,
|
||||||
@@ -255,6 +259,7 @@ SELECT
|
|||||||
id,
|
id,
|
||||||
section_title,
|
section_title,
|
||||||
framework_id,
|
framework_id,
|
||||||
|
organization_id,
|
||||||
name,
|
name,
|
||||||
description,
|
description,
|
||||||
status,
|
status,
|
||||||
@@ -351,6 +356,7 @@ WITH ctrl AS (
|
|||||||
c.id,
|
c.id,
|
||||||
c.section_title,
|
c.section_title,
|
||||||
c.framework_id,
|
c.framework_id,
|
||||||
|
c.organization_id,
|
||||||
c.tenant_id,
|
c.tenant_id,
|
||||||
c.name,
|
c.name,
|
||||||
c.description,
|
c.description,
|
||||||
@@ -376,6 +382,7 @@ SELECT
|
|||||||
id,
|
id,
|
||||||
section_title,
|
section_title,
|
||||||
framework_id,
|
framework_id,
|
||||||
|
organization_id,
|
||||||
name,
|
name,
|
||||||
description,
|
description,
|
||||||
status,
|
status,
|
||||||
@@ -455,6 +462,7 @@ SELECT
|
|||||||
id,
|
id,
|
||||||
section_title,
|
section_title,
|
||||||
framework_id,
|
framework_id,
|
||||||
|
organization_id,
|
||||||
name,
|
name,
|
||||||
description,
|
description,
|
||||||
status,
|
status,
|
||||||
@@ -548,6 +556,7 @@ WITH ctrl AS (
|
|||||||
c.id,
|
c.id,
|
||||||
c.section_title,
|
c.section_title,
|
||||||
c.framework_id,
|
c.framework_id,
|
||||||
|
c.organization_id,
|
||||||
c.tenant_id,
|
c.tenant_id,
|
||||||
c.name,
|
c.name,
|
||||||
c.description,
|
c.description,
|
||||||
@@ -567,6 +576,7 @@ SELECT
|
|||||||
id,
|
id,
|
||||||
section_title,
|
section_title,
|
||||||
framework_id,
|
framework_id,
|
||||||
|
organization_id,
|
||||||
name,
|
name,
|
||||||
description,
|
description,
|
||||||
status,
|
status,
|
||||||
@@ -613,6 +623,7 @@ SELECT
|
|||||||
id,
|
id,
|
||||||
section_title,
|
section_title,
|
||||||
framework_id,
|
framework_id,
|
||||||
|
organization_id,
|
||||||
name,
|
name,
|
||||||
description,
|
description,
|
||||||
status,
|
status,
|
||||||
@@ -661,6 +672,7 @@ SELECT
|
|||||||
id,
|
id,
|
||||||
section_title,
|
section_title,
|
||||||
framework_id,
|
framework_id,
|
||||||
|
organization_id,
|
||||||
name,
|
name,
|
||||||
description,
|
description,
|
||||||
status,
|
status,
|
||||||
@@ -707,6 +719,7 @@ INSERT INTO
|
|||||||
controls (
|
controls (
|
||||||
tenant_id,
|
tenant_id,
|
||||||
id,
|
id,
|
||||||
|
organization_id,
|
||||||
framework_id,
|
framework_id,
|
||||||
section_title,
|
section_title,
|
||||||
name,
|
name,
|
||||||
@@ -719,6 +732,7 @@ INSERT INTO
|
|||||||
VALUES (
|
VALUES (
|
||||||
@tenant_id,
|
@tenant_id,
|
||||||
@control_id,
|
@control_id,
|
||||||
|
@organization_id,
|
||||||
@framework_id,
|
@framework_id,
|
||||||
@section_title,
|
@section_title,
|
||||||
@name,
|
@name,
|
||||||
@@ -733,6 +747,7 @@ VALUES (
|
|||||||
args := pgx.StrictNamedArgs{
|
args := pgx.StrictNamedArgs{
|
||||||
"tenant_id": scope.GetTenantID(),
|
"tenant_id": scope.GetTenantID(),
|
||||||
"control_id": c.ID,
|
"control_id": c.ID,
|
||||||
|
"organization_id": c.OrganizationID,
|
||||||
"framework_id": c.FrameworkID,
|
"framework_id": c.FrameworkID,
|
||||||
"section_title": c.SectionTitle,
|
"section_title": c.SectionTitle,
|
||||||
"name": c.Name,
|
"name": c.Name,
|
||||||
@@ -884,6 +899,7 @@ WITH ctrl AS (
|
|||||||
c.id,
|
c.id,
|
||||||
c.section_title,
|
c.section_title,
|
||||||
c.framework_id,
|
c.framework_id,
|
||||||
|
c.organization_id,
|
||||||
c.tenant_id,
|
c.tenant_id,
|
||||||
c.name,
|
c.name,
|
||||||
c.description,
|
c.description,
|
||||||
@@ -903,6 +919,7 @@ SELECT
|
|||||||
id,
|
id,
|
||||||
section_title,
|
section_title,
|
||||||
framework_id,
|
framework_id,
|
||||||
|
organization_id,
|
||||||
name,
|
name,
|
||||||
description,
|
description,
|
||||||
status,
|
status,
|
||||||
@@ -994,6 +1011,7 @@ WITH ctrl AS (
|
|||||||
c.id,
|
c.id,
|
||||||
c.section_title,
|
c.section_title,
|
||||||
c.framework_id,
|
c.framework_id,
|
||||||
|
c.organization_id,
|
||||||
c.tenant_id,
|
c.tenant_id,
|
||||||
c.name,
|
c.name,
|
||||||
c.description,
|
c.description,
|
||||||
@@ -1013,6 +1031,7 @@ SELECT
|
|||||||
id,
|
id,
|
||||||
section_title,
|
section_title,
|
||||||
framework_id,
|
framework_id,
|
||||||
|
organization_id,
|
||||||
name,
|
name,
|
||||||
description,
|
description,
|
||||||
status,
|
status,
|
||||||
|
|||||||
@@ -29,6 +29,7 @@ type (
|
|||||||
ControlAudit struct {
|
ControlAudit struct {
|
||||||
ControlID gid.GID `db:"control_id"`
|
ControlID gid.GID `db:"control_id"`
|
||||||
AuditID gid.GID `db:"audit_id"`
|
AuditID gid.GID `db:"audit_id"`
|
||||||
|
OrganizationID gid.GID `db:"organization_id"`
|
||||||
CreatedAt time.Time `db:"created_at"`
|
CreatedAt time.Time `db:"created_at"`
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -45,12 +46,14 @@ INSERT INTO
|
|||||||
controls_audits (
|
controls_audits (
|
||||||
control_id,
|
control_id,
|
||||||
audit_id,
|
audit_id,
|
||||||
|
organization_id,
|
||||||
tenant_id,
|
tenant_id,
|
||||||
created_at
|
created_at
|
||||||
)
|
)
|
||||||
VALUES (
|
VALUES (
|
||||||
@control_id,
|
@control_id,
|
||||||
@audit_id,
|
@audit_id,
|
||||||
|
@organization_id,
|
||||||
@tenant_id,
|
@tenant_id,
|
||||||
@created_at
|
@created_at
|
||||||
)
|
)
|
||||||
@@ -60,6 +63,7 @@ ON CONFLICT (control_id, audit_id) DO NOTHING;
|
|||||||
args := pgx.StrictNamedArgs{
|
args := pgx.StrictNamedArgs{
|
||||||
"control_id": ca.ControlID,
|
"control_id": ca.ControlID,
|
||||||
"audit_id": ca.AuditID,
|
"audit_id": ca.AuditID,
|
||||||
|
"organization_id": ca.OrganizationID,
|
||||||
"tenant_id": scope.GetTenantID(),
|
"tenant_id": scope.GetTenantID(),
|
||||||
"created_at": ca.CreatedAt,
|
"created_at": ca.CreatedAt,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -31,6 +31,7 @@ type (
|
|||||||
ControlDocument struct {
|
ControlDocument struct {
|
||||||
ControlID gid.GID `db:"control_id"`
|
ControlID gid.GID `db:"control_id"`
|
||||||
DocumentID gid.GID `db:"document_id"`
|
DocumentID gid.GID `db:"document_id"`
|
||||||
|
OrganizationID gid.GID `db:"organization_id"`
|
||||||
TenantID gid.TenantID `db:"tenant_id"`
|
TenantID gid.TenantID `db:"tenant_id"`
|
||||||
CreatedAt time.Time `db:"created_at"`
|
CreatedAt time.Time `db:"created_at"`
|
||||||
}
|
}
|
||||||
@@ -57,12 +58,14 @@ INSERT INTO
|
|||||||
controls_documents (
|
controls_documents (
|
||||||
control_id,
|
control_id,
|
||||||
document_id,
|
document_id,
|
||||||
|
organization_id,
|
||||||
tenant_id,
|
tenant_id,
|
||||||
created_at
|
created_at
|
||||||
)
|
)
|
||||||
VALUES (
|
VALUES (
|
||||||
@control_id,
|
@control_id,
|
||||||
@document_id,
|
@document_id,
|
||||||
|
@organization_id,
|
||||||
@tenant_id,
|
@tenant_id,
|
||||||
@created_at
|
@created_at
|
||||||
);
|
);
|
||||||
@@ -71,6 +74,7 @@ VALUES (
|
|||||||
args := pgx.StrictNamedArgs{
|
args := pgx.StrictNamedArgs{
|
||||||
"control_id": cp.ControlID,
|
"control_id": cp.ControlID,
|
||||||
"document_id": cp.DocumentID,
|
"document_id": cp.DocumentID,
|
||||||
|
"organization_id": cp.OrganizationID,
|
||||||
"tenant_id": scope.GetTenantID(),
|
"tenant_id": scope.GetTenantID(),
|
||||||
"created_at": cp.CreatedAt,
|
"created_at": cp.CreatedAt,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,15 +20,16 @@ import (
|
|||||||
"maps"
|
"maps"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"go.probo.inc/probo/pkg/gid"
|
|
||||||
"github.com/jackc/pgx/v5"
|
"github.com/jackc/pgx/v5"
|
||||||
"go.gearno.de/kit/pg"
|
"go.gearno.de/kit/pg"
|
||||||
|
"go.probo.inc/probo/pkg/gid"
|
||||||
)
|
)
|
||||||
|
|
||||||
type (
|
type (
|
||||||
ControlMeasure struct {
|
ControlMeasure struct {
|
||||||
ControlID gid.GID `db:"control_id"`
|
ControlID gid.GID `db:"control_id"`
|
||||||
MeasureID gid.GID `db:"measure_id"`
|
MeasureID gid.GID `db:"measure_id"`
|
||||||
|
OrganizationID gid.GID `db:"organization_id"`
|
||||||
TenantID gid.TenantID `db:"tenant_id"`
|
TenantID gid.TenantID `db:"tenant_id"`
|
||||||
CreatedAt time.Time `db:"created_at"`
|
CreatedAt time.Time `db:"created_at"`
|
||||||
}
|
}
|
||||||
@@ -46,12 +47,14 @@ INSERT INTO
|
|||||||
controls_measures (
|
controls_measures (
|
||||||
control_id,
|
control_id,
|
||||||
measure_id,
|
measure_id,
|
||||||
|
organization_id,
|
||||||
tenant_id,
|
tenant_id,
|
||||||
created_at
|
created_at
|
||||||
)
|
)
|
||||||
VALUES (
|
VALUES (
|
||||||
@control_id,
|
@control_id,
|
||||||
@measure_id,
|
@measure_id,
|
||||||
|
@organization_id,
|
||||||
@tenant_id,
|
@tenant_id,
|
||||||
@created_at
|
@created_at
|
||||||
)
|
)
|
||||||
@@ -61,6 +64,7 @@ ON CONFLICT (control_id, measure_id) DO NOTHING;
|
|||||||
args := pgx.StrictNamedArgs{
|
args := pgx.StrictNamedArgs{
|
||||||
"control_id": cm.ControlID,
|
"control_id": cm.ControlID,
|
||||||
"measure_id": cm.MeasureID,
|
"measure_id": cm.MeasureID,
|
||||||
|
"organization_id": cm.OrganizationID,
|
||||||
"tenant_id": scope.GetTenantID(),
|
"tenant_id": scope.GetTenantID(),
|
||||||
"created_at": cm.CreatedAt,
|
"created_at": cm.CreatedAt,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -29,6 +29,7 @@ type (
|
|||||||
ControlSnapshot struct {
|
ControlSnapshot struct {
|
||||||
ControlID gid.GID `db:"control_id"`
|
ControlID gid.GID `db:"control_id"`
|
||||||
SnapshotID gid.GID `db:"snapshot_id"`
|
SnapshotID gid.GID `db:"snapshot_id"`
|
||||||
|
OrganizationID gid.GID `db:"organization_id"`
|
||||||
CreatedAt time.Time `db:"created_at"`
|
CreatedAt time.Time `db:"created_at"`
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -45,12 +46,14 @@ INSERT INTO
|
|||||||
controls_snapshots (
|
controls_snapshots (
|
||||||
control_id,
|
control_id,
|
||||||
snapshot_id,
|
snapshot_id,
|
||||||
|
organization_id,
|
||||||
tenant_id,
|
tenant_id,
|
||||||
created_at
|
created_at
|
||||||
)
|
)
|
||||||
VALUES (
|
VALUES (
|
||||||
@control_id,
|
@control_id,
|
||||||
@snapshot_id,
|
@snapshot_id,
|
||||||
|
@organization_id,
|
||||||
@tenant_id,
|
@tenant_id,
|
||||||
@created_at
|
@created_at
|
||||||
)
|
)
|
||||||
@@ -60,6 +63,7 @@ ON CONFLICT (control_id, snapshot_id) DO NOTHING;
|
|||||||
args := pgx.StrictNamedArgs{
|
args := pgx.StrictNamedArgs{
|
||||||
"control_id": cs.ControlID,
|
"control_id": cs.ControlID,
|
||||||
"snapshot_id": cs.SnapshotID,
|
"snapshot_id": cs.SnapshotID,
|
||||||
|
"organization_id": cs.OrganizationID,
|
||||||
"tenant_id": scope.GetTenantID(),
|
"tenant_id": scope.GetTenantID(),
|
||||||
"created_at": cs.CreatedAt,
|
"created_at": cs.CreatedAt,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -22,17 +22,18 @@ import (
|
|||||||
"maps"
|
"maps"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"go.probo.inc/probo/pkg/crypto/cipher"
|
|
||||||
"go.probo.inc/probo/pkg/gid"
|
|
||||||
"go.probo.inc/probo/pkg/page"
|
|
||||||
"github.com/jackc/pgx/v5"
|
"github.com/jackc/pgx/v5"
|
||||||
"github.com/jackc/pgx/v5/pgconn"
|
"github.com/jackc/pgx/v5/pgconn"
|
||||||
"go.gearno.de/kit/pg"
|
"go.gearno.de/kit/pg"
|
||||||
|
"go.probo.inc/probo/pkg/crypto/cipher"
|
||||||
|
"go.probo.inc/probo/pkg/gid"
|
||||||
|
"go.probo.inc/probo/pkg/page"
|
||||||
)
|
)
|
||||||
|
|
||||||
type (
|
type (
|
||||||
CustomDomain struct {
|
CustomDomain struct {
|
||||||
ID gid.GID `db:"id"`
|
ID gid.GID `db:"id"`
|
||||||
|
OrganizationID gid.GID `db:"organization_id"`
|
||||||
Domain string `db:"domain"`
|
Domain string `db:"domain"`
|
||||||
HTTPChallengeToken *string `db:"http_challenge_token"`
|
HTTPChallengeToken *string `db:"http_challenge_token"`
|
||||||
HTTPChallengeKeyAuth *string `db:"http_challenge_key_auth"`
|
HTTPChallengeKeyAuth *string `db:"http_challenge_key_auth"`
|
||||||
@@ -159,6 +160,7 @@ func (cd *CustomDomain) LoadByID(
|
|||||||
q := `
|
q := `
|
||||||
SELECT
|
SELECT
|
||||||
id,
|
id,
|
||||||
|
organization_id,
|
||||||
domain,
|
domain,
|
||||||
http_challenge_token,
|
http_challenge_token,
|
||||||
http_challenge_key_auth,
|
http_challenge_key_auth,
|
||||||
@@ -211,6 +213,7 @@ func (cd *CustomDomain) LoadByIDForUpdate(
|
|||||||
q := `
|
q := `
|
||||||
SELECT
|
SELECT
|
||||||
id,
|
id,
|
||||||
|
organization_id,
|
||||||
domain,
|
domain,
|
||||||
http_challenge_token,
|
http_challenge_token,
|
||||||
http_challenge_key_auth,
|
http_challenge_key_auth,
|
||||||
@@ -263,6 +266,7 @@ func (cd *CustomDomain) LoadByDomain(
|
|||||||
q := `
|
q := `
|
||||||
SELECT
|
SELECT
|
||||||
id,
|
id,
|
||||||
|
organization_id,
|
||||||
domain,
|
domain,
|
||||||
http_challenge_token,
|
http_challenge_token,
|
||||||
http_challenge_key_auth,
|
http_challenge_key_auth,
|
||||||
@@ -320,6 +324,7 @@ func (cd *CustomDomain) Insert(
|
|||||||
INSERT INTO custom_domains (
|
INSERT INTO custom_domains (
|
||||||
id,
|
id,
|
||||||
tenant_id,
|
tenant_id,
|
||||||
|
organization_id,
|
||||||
domain,
|
domain,
|
||||||
http_challenge_token,
|
http_challenge_token,
|
||||||
http_challenge_key_auth,
|
http_challenge_key_auth,
|
||||||
@@ -337,6 +342,7 @@ INSERT INTO custom_domains (
|
|||||||
) VALUES (
|
) VALUES (
|
||||||
@id,
|
@id,
|
||||||
@tenant_id,
|
@tenant_id,
|
||||||
|
@organization_id,
|
||||||
@domain,
|
@domain,
|
||||||
@http_challenge_token,
|
@http_challenge_token,
|
||||||
@http_challenge_key_auth,
|
@http_challenge_key_auth,
|
||||||
@@ -357,6 +363,7 @@ INSERT INTO custom_domains (
|
|||||||
args := pgx.NamedArgs{
|
args := pgx.NamedArgs{
|
||||||
"id": cd.ID,
|
"id": cd.ID,
|
||||||
"tenant_id": scope.GetTenantID(),
|
"tenant_id": scope.GetTenantID(),
|
||||||
|
"organization_id": cd.OrganizationID,
|
||||||
"domain": cd.Domain,
|
"domain": cd.Domain,
|
||||||
"http_challenge_token": cd.HTTPChallengeToken,
|
"http_challenge_token": cd.HTTPChallengeToken,
|
||||||
"http_challenge_key_auth": cd.HTTPChallengeKeyAuth,
|
"http_challenge_key_auth": cd.HTTPChallengeKeyAuth,
|
||||||
@@ -487,6 +494,7 @@ func (cd *CustomDomain) LoadByHTTPChallengeToken(
|
|||||||
q := `
|
q := `
|
||||||
SELECT
|
SELECT
|
||||||
id,
|
id,
|
||||||
|
organization_id,
|
||||||
domain,
|
domain,
|
||||||
http_challenge_token,
|
http_challenge_token,
|
||||||
http_challenge_key_auth,
|
http_challenge_key_auth,
|
||||||
@@ -537,6 +545,7 @@ func (domains *CustomDomains) ListDomainsForRenewal(
|
|||||||
q := `
|
q := `
|
||||||
SELECT
|
SELECT
|
||||||
id,
|
id,
|
||||||
|
organization_id,
|
||||||
domain,
|
domain,
|
||||||
http_challenge_token,
|
http_challenge_token,
|
||||||
http_challenge_key_auth,
|
http_challenge_key_auth,
|
||||||
@@ -589,6 +598,7 @@ func (domains *CustomDomains) ListDomainsWithPendingHTTPChallenges(
|
|||||||
q := `
|
q := `
|
||||||
SELECT
|
SELECT
|
||||||
id,
|
id,
|
||||||
|
organization_id,
|
||||||
domain,
|
domain,
|
||||||
http_challenge_token,
|
http_challenge_token,
|
||||||
http_challenge_key_auth,
|
http_challenge_key_auth,
|
||||||
@@ -644,6 +654,7 @@ func (domains *CustomDomains) LoadActiveCertificates(
|
|||||||
q := `
|
q := `
|
||||||
SELECT
|
SELECT
|
||||||
id,
|
id,
|
||||||
|
organization_id,
|
||||||
domain,
|
domain,
|
||||||
http_challenge_token,
|
http_challenge_token,
|
||||||
http_challenge_key_auth,
|
http_challenge_key_auth,
|
||||||
@@ -693,6 +704,7 @@ func (domains *CustomDomains) ListStaleProvisioningDomains(
|
|||||||
q := `
|
q := `
|
||||||
SELECT
|
SELECT
|
||||||
id,
|
id,
|
||||||
|
organization_id,
|
||||||
domain,
|
domain,
|
||||||
http_challenge_token,
|
http_challenge_token,
|
||||||
http_challenge_key_auth,
|
http_challenge_key_auth,
|
||||||
|
|||||||
@@ -41,6 +41,7 @@ func (dv DatumVendors) Merge(
|
|||||||
conn pg.Conn,
|
conn pg.Conn,
|
||||||
scope Scoper,
|
scope Scoper,
|
||||||
datumID gid.GID,
|
datumID gid.GID,
|
||||||
|
organizationID gid.GID,
|
||||||
vendorIDs []gid.GID,
|
vendorIDs []gid.GID,
|
||||||
) error {
|
) error {
|
||||||
q := `
|
q := `
|
||||||
@@ -49,6 +50,7 @@ WITH vendor_ids AS (
|
|||||||
unnest(@vendor_ids::text[]) AS vendor_id,
|
unnest(@vendor_ids::text[]) AS vendor_id,
|
||||||
@tenant_id AS tenant_id,
|
@tenant_id AS tenant_id,
|
||||||
@datum_id AS datum_id,
|
@datum_id AS datum_id,
|
||||||
|
@organization_id AS organization_id,
|
||||||
@created_at::timestamptz AS created_at
|
@created_at::timestamptz AS created_at
|
||||||
)
|
)
|
||||||
MERGE INTO data_vendors AS tgt
|
MERGE INTO data_vendors AS tgt
|
||||||
@@ -57,8 +59,8 @@ ON tgt.tenant_id = src.tenant_id
|
|||||||
AND tgt.datum_id = src.datum_id
|
AND tgt.datum_id = src.datum_id
|
||||||
AND tgt.vendor_id = src.vendor_id
|
AND tgt.vendor_id = src.vendor_id
|
||||||
WHEN NOT MATCHED THEN
|
WHEN NOT MATCHED THEN
|
||||||
INSERT (tenant_id, datum_id, vendor_id, created_at)
|
INSERT (tenant_id, datum_id, vendor_id, organization_id, created_at)
|
||||||
VALUES (src.tenant_id, src.datum_id, src.vendor_id, src.created_at)
|
VALUES (src.tenant_id, src.datum_id, src.vendor_id, src.organization_id, src.created_at)
|
||||||
WHEN NOT MATCHED BY SOURCE
|
WHEN NOT MATCHED BY SOURCE
|
||||||
AND tgt.tenant_id = @tenant_id AND tgt.datum_id = @datum_id
|
AND tgt.tenant_id = @tenant_id AND tgt.datum_id = @datum_id
|
||||||
THEN DELETE
|
THEN DELETE
|
||||||
@@ -67,6 +69,7 @@ WHEN NOT MATCHED BY SOURCE
|
|||||||
args := pgx.StrictNamedArgs{
|
args := pgx.StrictNamedArgs{
|
||||||
"tenant_id": scope.GetTenantID(),
|
"tenant_id": scope.GetTenantID(),
|
||||||
"datum_id": datumID,
|
"datum_id": datumID,
|
||||||
|
"organization_id": organizationID,
|
||||||
"created_at": time.Now(),
|
"created_at": time.Now(),
|
||||||
"vendor_ids": vendorIDs,
|
"vendor_ids": vendorIDs,
|
||||||
}
|
}
|
||||||
@@ -84,17 +87,19 @@ func (dv DatumVendors) Insert(
|
|||||||
conn pg.Conn,
|
conn pg.Conn,
|
||||||
scope Scoper,
|
scope Scoper,
|
||||||
datumID gid.GID,
|
datumID gid.GID,
|
||||||
|
organizationID gid.GID,
|
||||||
vendorIDs []gid.GID,
|
vendorIDs []gid.GID,
|
||||||
) error {
|
) error {
|
||||||
q := `
|
q := `
|
||||||
WITH vendor_ids AS (
|
WITH vendor_ids AS (
|
||||||
SELECT unnest(@vendor_ids::text[]) AS vendor_id
|
SELECT unnest(@vendor_ids::text[]) AS vendor_id
|
||||||
)
|
)
|
||||||
INSERT INTO data_vendors (tenant_id, datum_id, vendor_id, created_at)
|
INSERT INTO data_vendors (tenant_id, datum_id, vendor_id, organization_id, created_at)
|
||||||
SELECT
|
SELECT
|
||||||
@tenant_id::text AS tenant_id,
|
@tenant_id::text AS tenant_id,
|
||||||
@datum_id::text AS datum_id,
|
@datum_id::text AS datum_id,
|
||||||
vendor_id,
|
vendor_id,
|
||||||
|
@organization_id::text AS organization_id,
|
||||||
@created_at::timestamptz AS created_at
|
@created_at::timestamptz AS created_at
|
||||||
FROM vendor_ids
|
FROM vendor_ids
|
||||||
`
|
`
|
||||||
@@ -102,6 +107,7 @@ FROM vendor_ids
|
|||||||
args := pgx.StrictNamedArgs{
|
args := pgx.StrictNamedArgs{
|
||||||
"tenant_id": scope.GetTenantID(),
|
"tenant_id": scope.GetTenantID(),
|
||||||
"datum_id": datumID,
|
"datum_id": datumID,
|
||||||
|
"organization_id": organizationID,
|
||||||
"created_at": time.Now(),
|
"created_at": time.Now(),
|
||||||
"vendor_ids": vendorIDs,
|
"vendor_ids": vendorIDs,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -21,16 +21,17 @@ import (
|
|||||||
"maps"
|
"maps"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"go.probo.inc/probo/pkg/gid"
|
|
||||||
"go.probo.inc/probo/pkg/page"
|
|
||||||
"github.com/jackc/pgx/v5"
|
"github.com/jackc/pgx/v5"
|
||||||
"github.com/jackc/pgx/v5/pgconn"
|
"github.com/jackc/pgx/v5/pgconn"
|
||||||
"go.gearno.de/kit/pg"
|
"go.gearno.de/kit/pg"
|
||||||
|
"go.probo.inc/probo/pkg/gid"
|
||||||
|
"go.probo.inc/probo/pkg/page"
|
||||||
)
|
)
|
||||||
|
|
||||||
type (
|
type (
|
||||||
DocumentVersion struct {
|
DocumentVersion struct {
|
||||||
ID gid.GID `db:"id"`
|
ID gid.GID `db:"id"`
|
||||||
|
OrganizationID gid.GID `db:"organization_id"`
|
||||||
DocumentID gid.GID `db:"document_id"`
|
DocumentID gid.GID `db:"document_id"`
|
||||||
Title string `db:"title"`
|
Title string `db:"title"`
|
||||||
OwnerID gid.GID `db:"owner_id"`
|
OwnerID gid.GID `db:"owner_id"`
|
||||||
@@ -81,6 +82,7 @@ func (p *DocumentVersions) LoadByDocumentID(
|
|||||||
q := `
|
q := `
|
||||||
SELECT
|
SELECT
|
||||||
id,
|
id,
|
||||||
|
organization_id,
|
||||||
document_id,
|
document_id,
|
||||||
title,
|
title,
|
||||||
owner_id,
|
owner_id,
|
||||||
@@ -140,6 +142,7 @@ func (p *DocumentVersion) LoadByID(
|
|||||||
q := `
|
q := `
|
||||||
SELECT
|
SELECT
|
||||||
id,
|
id,
|
||||||
|
organization_id,
|
||||||
document_id,
|
document_id,
|
||||||
title,
|
title,
|
||||||
owner_id,
|
owner_id,
|
||||||
@@ -190,6 +193,7 @@ func (p DocumentVersion) Insert(
|
|||||||
INSERT INTO document_versions (
|
INSERT INTO document_versions (
|
||||||
tenant_id,
|
tenant_id,
|
||||||
id,
|
id,
|
||||||
|
organization_id,
|
||||||
document_id,
|
document_id,
|
||||||
title,
|
title,
|
||||||
owner_id,
|
owner_id,
|
||||||
@@ -204,6 +208,7 @@ INSERT INTO document_versions (
|
|||||||
VALUES (
|
VALUES (
|
||||||
@tenant_id,
|
@tenant_id,
|
||||||
@id,
|
@id,
|
||||||
|
@organization_id,
|
||||||
@document_id,
|
@document_id,
|
||||||
@title,
|
@title,
|
||||||
@owner_id,
|
@owner_id,
|
||||||
@@ -219,6 +224,7 @@ VALUES (
|
|||||||
args := pgx.StrictNamedArgs{
|
args := pgx.StrictNamedArgs{
|
||||||
"tenant_id": scope.GetTenantID(),
|
"tenant_id": scope.GetTenantID(),
|
||||||
"id": p.ID,
|
"id": p.ID,
|
||||||
|
"organization_id": p.OrganizationID,
|
||||||
"document_id": p.DocumentID,
|
"document_id": p.DocumentID,
|
||||||
"title": p.Title,
|
"title": p.Title,
|
||||||
"owner_id": p.OwnerID,
|
"owner_id": p.OwnerID,
|
||||||
@@ -264,6 +270,7 @@ func (p *DocumentVersion) LoadByDocumentIDAndVersionNumber(
|
|||||||
q := `
|
q := `
|
||||||
SELECT
|
SELECT
|
||||||
id,
|
id,
|
||||||
|
organization_id,
|
||||||
document_id,
|
document_id,
|
||||||
title,
|
title,
|
||||||
owner_id,
|
owner_id,
|
||||||
@@ -316,6 +323,7 @@ func (p *DocumentVersion) LoadLatestVersion(
|
|||||||
q := `
|
q := `
|
||||||
SELECT
|
SELECT
|
||||||
id,
|
id,
|
||||||
|
organization_id,
|
||||||
document_id,
|
document_id,
|
||||||
title,
|
title,
|
||||||
owner_id,
|
owner_id,
|
||||||
@@ -366,6 +374,7 @@ func (p *DocumentVersion) LoadLatestPublishedVersion(
|
|||||||
q := `
|
q := `
|
||||||
SELECT
|
SELECT
|
||||||
id,
|
id,
|
||||||
|
organization_id,
|
||||||
document_id,
|
document_id,
|
||||||
title,
|
title,
|
||||||
owner_id,
|
owner_id,
|
||||||
|
|||||||
@@ -21,16 +21,17 @@ import (
|
|||||||
"maps"
|
"maps"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"go.probo.inc/probo/pkg/gid"
|
|
||||||
"go.probo.inc/probo/pkg/page"
|
|
||||||
"github.com/jackc/pgx/v5"
|
"github.com/jackc/pgx/v5"
|
||||||
"github.com/jackc/pgx/v5/pgconn"
|
"github.com/jackc/pgx/v5/pgconn"
|
||||||
"go.gearno.de/kit/pg"
|
"go.gearno.de/kit/pg"
|
||||||
|
"go.probo.inc/probo/pkg/gid"
|
||||||
|
"go.probo.inc/probo/pkg/page"
|
||||||
)
|
)
|
||||||
|
|
||||||
type (
|
type (
|
||||||
DocumentVersionSignature struct {
|
DocumentVersionSignature struct {
|
||||||
ID gid.GID `json:"id"`
|
ID gid.GID `json:"id"`
|
||||||
|
OrganizationID gid.GID `json:"-"`
|
||||||
DocumentVersionID gid.GID `json:"document_version_id"`
|
DocumentVersionID gid.GID `json:"document_version_id"`
|
||||||
State DocumentVersionSignatureState `json:"state"`
|
State DocumentVersionSignatureState `json:"state"`
|
||||||
SignedBy gid.GID `json:"signed_by"`
|
SignedBy gid.GID `json:"signed_by"`
|
||||||
@@ -87,6 +88,7 @@ func (pvs *DocumentVersionSignature) LoadByDocumentVersionIDAndSignatory(
|
|||||||
q := `
|
q := `
|
||||||
SELECT
|
SELECT
|
||||||
id,
|
id,
|
||||||
|
organization_id,
|
||||||
document_version_id,
|
document_version_id,
|
||||||
state,
|
state,
|
||||||
signed_by,
|
signed_by,
|
||||||
@@ -132,6 +134,7 @@ func (pvs *DocumentVersionSignature) LoadByID(
|
|||||||
q := `
|
q := `
|
||||||
SELECT
|
SELECT
|
||||||
id,
|
id,
|
||||||
|
organization_id,
|
||||||
document_version_id,
|
document_version_id,
|
||||||
state,
|
state,
|
||||||
signed_by,
|
signed_by,
|
||||||
@@ -175,6 +178,7 @@ func (pvs DocumentVersionSignature) Insert(
|
|||||||
INSERT INTO document_version_signatures (
|
INSERT INTO document_version_signatures (
|
||||||
id,
|
id,
|
||||||
tenant_id,
|
tenant_id,
|
||||||
|
organization_id,
|
||||||
document_version_id,
|
document_version_id,
|
||||||
state,
|
state,
|
||||||
signed_by,
|
signed_by,
|
||||||
@@ -185,6 +189,7 @@ INSERT INTO document_version_signatures (
|
|||||||
) VALUES (
|
) VALUES (
|
||||||
@id,
|
@id,
|
||||||
@tenant_id,
|
@tenant_id,
|
||||||
|
@organization_id,
|
||||||
@document_version_id,
|
@document_version_id,
|
||||||
@state,
|
@state,
|
||||||
@signed_by,
|
@signed_by,
|
||||||
@@ -198,6 +203,7 @@ INSERT INTO document_version_signatures (
|
|||||||
args := pgx.StrictNamedArgs{
|
args := pgx.StrictNamedArgs{
|
||||||
"id": pvs.ID,
|
"id": pvs.ID,
|
||||||
"tenant_id": scope.GetTenantID(),
|
"tenant_id": scope.GetTenantID(),
|
||||||
|
"organization_id": pvs.OrganizationID,
|
||||||
"document_version_id": pvs.DocumentVersionID,
|
"document_version_id": pvs.DocumentVersionID,
|
||||||
"state": pvs.State,
|
"state": pvs.State,
|
||||||
"signed_by": pvs.SignedBy,
|
"signed_by": pvs.SignedBy,
|
||||||
@@ -234,6 +240,7 @@ func (pvss *DocumentVersionSignatures) LoadByDocumentVersionID(
|
|||||||
q := `
|
q := `
|
||||||
SELECT
|
SELECT
|
||||||
id,
|
id,
|
||||||
|
organization_id,
|
||||||
document_version_id,
|
document_version_id,
|
||||||
state,
|
state,
|
||||||
signed_by,
|
signed_by,
|
||||||
@@ -346,6 +353,7 @@ func (pvss *DocumentVersionSignaturesWithPeople) LoadByDocumentVersionIDWithPeop
|
|||||||
WITH sigs AS (
|
WITH sigs AS (
|
||||||
SELECT
|
SELECT
|
||||||
dvs.id,
|
dvs.id,
|
||||||
|
dvs.organization_id,
|
||||||
dvs.tenant_id,
|
dvs.tenant_id,
|
||||||
dvs.document_version_id,
|
dvs.document_version_id,
|
||||||
dvs.state,
|
dvs.state,
|
||||||
@@ -367,6 +375,7 @@ WITH sigs AS (
|
|||||||
)
|
)
|
||||||
SELECT
|
SELECT
|
||||||
id,
|
id,
|
||||||
|
organization_id,
|
||||||
document_version_id,
|
document_version_id,
|
||||||
state,
|
state,
|
||||||
signed_by,
|
signed_by,
|
||||||
|
|||||||
@@ -20,9 +20,9 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"go.probo.inc/probo/pkg/gid"
|
|
||||||
"github.com/jackc/pgx/v5"
|
"github.com/jackc/pgx/v5"
|
||||||
"go.gearno.de/kit/pg"
|
"go.gearno.de/kit/pg"
|
||||||
|
"go.probo.inc/probo/pkg/gid"
|
||||||
)
|
)
|
||||||
|
|
||||||
type (
|
type (
|
||||||
|
|||||||
@@ -68,3 +68,211 @@ const (
|
|||||||
UserAPIKeyMembershipEntityType uint16 = 44
|
UserAPIKeyMembershipEntityType uint16 = 44
|
||||||
MeetingEntityType uint16 = 45
|
MeetingEntityType uint16 = 45
|
||||||
)
|
)
|
||||||
|
|
||||||
|
type EntityInfo struct {
|
||||||
|
Model string
|
||||||
|
Table string
|
||||||
|
}
|
||||||
|
|
||||||
|
var entityRegistry = map[uint16]EntityInfo{
|
||||||
|
OrganizationEntityType: {
|
||||||
|
Model: "Organization",
|
||||||
|
Table: "organizations",
|
||||||
|
},
|
||||||
|
FrameworkEntityType: {
|
||||||
|
Model: "Framework",
|
||||||
|
Table: "frameworks",
|
||||||
|
},
|
||||||
|
MeasureEntityType: {
|
||||||
|
Model: "Measure",
|
||||||
|
Table: "measures",
|
||||||
|
},
|
||||||
|
TaskEntityType: {
|
||||||
|
Model: "Task",
|
||||||
|
Table: "tasks",
|
||||||
|
},
|
||||||
|
EvidenceEntityType: {
|
||||||
|
Model: "Evidence",
|
||||||
|
Table: "evidences",
|
||||||
|
},
|
||||||
|
ConnectorEntityType: {
|
||||||
|
Model: "Connector",
|
||||||
|
Table: "connectors",
|
||||||
|
},
|
||||||
|
VendorRiskAssessmentEntityType: {
|
||||||
|
Model: "VendorRiskAssessment",
|
||||||
|
Table: "vendor_risk_assessments",
|
||||||
|
},
|
||||||
|
VendorEntityType: {
|
||||||
|
Model: "Vendor",
|
||||||
|
Table: "vendors",
|
||||||
|
},
|
||||||
|
PeopleEntityType: {
|
||||||
|
Model: "People",
|
||||||
|
Table: "peoples",
|
||||||
|
},
|
||||||
|
VendorComplianceReportEntityType: {
|
||||||
|
Model: "VendorComplianceReport",
|
||||||
|
Table: "vendor_compliance_reports",
|
||||||
|
},
|
||||||
|
DocumentEntityType: {
|
||||||
|
Model: "Document",
|
||||||
|
Table: "documents",
|
||||||
|
},
|
||||||
|
UserEntityType: {
|
||||||
|
Model: "User",
|
||||||
|
Table: "auth_users",
|
||||||
|
},
|
||||||
|
SessionEntityType: {
|
||||||
|
Model: "Session",
|
||||||
|
Table: "auth_sessions",
|
||||||
|
},
|
||||||
|
EmailEntityType: {
|
||||||
|
Model: "Email",
|
||||||
|
Table: "auth_emails",
|
||||||
|
},
|
||||||
|
ControlEntityType: {
|
||||||
|
Model: "Control",
|
||||||
|
Table: "controls",
|
||||||
|
},
|
||||||
|
RiskEntityType: {
|
||||||
|
Model: "Risk",
|
||||||
|
Table: "risks",
|
||||||
|
},
|
||||||
|
DocumentVersionEntityType: {
|
||||||
|
Model: "DocumentVersion",
|
||||||
|
Table: "document_versions",
|
||||||
|
},
|
||||||
|
DocumentVersionSignatureEntityType: {
|
||||||
|
Model: "DocumentVersionSignature",
|
||||||
|
Table: "document_version_signatures",
|
||||||
|
},
|
||||||
|
AssetEntityType: {
|
||||||
|
Model: "Asset",
|
||||||
|
Table: "assets",
|
||||||
|
},
|
||||||
|
DatumEntityType: {
|
||||||
|
Model: "Datum",
|
||||||
|
Table: "data",
|
||||||
|
},
|
||||||
|
AuditEntityType: {
|
||||||
|
Model: "Audit",
|
||||||
|
Table: "audits",
|
||||||
|
},
|
||||||
|
ReportEntityType: {
|
||||||
|
Model: "Report",
|
||||||
|
Table: "reports",
|
||||||
|
},
|
||||||
|
TrustCenterEntityType: {
|
||||||
|
Model: "TrustCenter",
|
||||||
|
Table: "trust_centers",
|
||||||
|
},
|
||||||
|
TrustCenterAccessEntityType: {
|
||||||
|
Model: "TrustCenterAccess",
|
||||||
|
Table: "trust_center_accesses",
|
||||||
|
},
|
||||||
|
VendorBusinessAssociateAgreementEntityType: {
|
||||||
|
Model: "VendorBusinessAssociateAgreement",
|
||||||
|
Table: "vendor_business_associate_agreements",
|
||||||
|
},
|
||||||
|
FileEntityType: {
|
||||||
|
Model: "File",
|
||||||
|
Table: "files",
|
||||||
|
},
|
||||||
|
VendorContactEntityType: {
|
||||||
|
Model: "VendorContact",
|
||||||
|
Table: "vendor_contacts",
|
||||||
|
},
|
||||||
|
VendorDataPrivacyAgreementEntityType: {
|
||||||
|
Model: "VendorDataPrivacyAgreement",
|
||||||
|
Table: "vendor_data_privacy_agreements",
|
||||||
|
},
|
||||||
|
NonconformityEntityType: {
|
||||||
|
Model: "Nonconformity",
|
||||||
|
Table: "nonconformities",
|
||||||
|
},
|
||||||
|
ObligationEntityType: {
|
||||||
|
Model: "Obligation",
|
||||||
|
Table: "obligations",
|
||||||
|
},
|
||||||
|
VendorServiceEntityType: {
|
||||||
|
Model: "VendorService",
|
||||||
|
Table: "vendor_services",
|
||||||
|
},
|
||||||
|
SnapshotEntityType: {
|
||||||
|
Model: "Snapshot",
|
||||||
|
Table: "snapshots",
|
||||||
|
},
|
||||||
|
ContinualImprovementEntityType: {
|
||||||
|
Model: "ContinualImprovement",
|
||||||
|
Table: "continual_improvements",
|
||||||
|
},
|
||||||
|
ProcessingActivityEntityType: {
|
||||||
|
Model: "ProcessingActivity",
|
||||||
|
Table: "processing_activities",
|
||||||
|
},
|
||||||
|
ExportJobEntityType: {
|
||||||
|
Model: "ExportJob",
|
||||||
|
Table: "export_jobs",
|
||||||
|
},
|
||||||
|
TrustCenterReferenceEntityType: {
|
||||||
|
Model: "TrustCenterReference",
|
||||||
|
Table: "trust_center_references",
|
||||||
|
},
|
||||||
|
TrustCenterDocumentAccessEntityType: {
|
||||||
|
Model: "",
|
||||||
|
Table: "trust_center_document_accesses",
|
||||||
|
},
|
||||||
|
CustomDomainEntityType: {
|
||||||
|
Model: "CustomDomain",
|
||||||
|
Table: "custom_domains",
|
||||||
|
},
|
||||||
|
InvitationEntityType: {
|
||||||
|
Model: "Invitation",
|
||||||
|
Table: "authz_invitations",
|
||||||
|
},
|
||||||
|
MembershipEntityType: {
|
||||||
|
Model: "Membership",
|
||||||
|
Table: "authz_memberships",
|
||||||
|
},
|
||||||
|
SlackMessageEntityType: {
|
||||||
|
Model: "SlackMessage",
|
||||||
|
Table: "slack_messages",
|
||||||
|
},
|
||||||
|
TrustCenterFileEntityType: {
|
||||||
|
Model: "TrustCenterFile",
|
||||||
|
Table: "trust_center_files",
|
||||||
|
},
|
||||||
|
SAMLConfigurationEntityType: {
|
||||||
|
Model: "SAMLConfiguration",
|
||||||
|
Table: "auth_saml_configurations",
|
||||||
|
},
|
||||||
|
UserAPIKeyEntityType: {
|
||||||
|
Model: "UserAPIKey",
|
||||||
|
Table: "auth_user_api_keys",
|
||||||
|
},
|
||||||
|
UserAPIKeyMembershipEntityType: {
|
||||||
|
Model: "UserAPIKeyMembership",
|
||||||
|
Table: "authz_api_keys_memberships",
|
||||||
|
},
|
||||||
|
MeetingEntityType: {
|
||||||
|
Model: "Meeting",
|
||||||
|
Table: "meetings",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
func EntityTable(entityType uint16) (string, bool) {
|
||||||
|
info, ok := entityRegistry[entityType]
|
||||||
|
if !ok {
|
||||||
|
return "", false
|
||||||
|
}
|
||||||
|
return info.Table, true
|
||||||
|
}
|
||||||
|
|
||||||
|
func EntityModel(entityType uint16) (string, bool) {
|
||||||
|
info, ok := entityRegistry[entityType]
|
||||||
|
if !ok {
|
||||||
|
return "", false
|
||||||
|
}
|
||||||
|
return info.Model, true
|
||||||
|
}
|
||||||
|
|||||||
@@ -31,6 +31,7 @@ import (
|
|||||||
type (
|
type (
|
||||||
Evidence struct {
|
Evidence struct {
|
||||||
ID gid.GID `db:"id"`
|
ID gid.GID `db:"id"`
|
||||||
|
OrganizationID gid.GID `db:"organization_id"`
|
||||||
MeasureID gid.GID `db:"measure_id"`
|
MeasureID gid.GID `db:"measure_id"`
|
||||||
TaskID *gid.GID `db:"task_id"`
|
TaskID *gid.GID `db:"task_id"`
|
||||||
State EvidenceState `db:"state"`
|
State EvidenceState `db:"state"`
|
||||||
@@ -141,6 +142,7 @@ INSERT INTO
|
|||||||
evidences (
|
evidences (
|
||||||
tenant_id,
|
tenant_id,
|
||||||
id,
|
id,
|
||||||
|
organization_id,
|
||||||
measure_id,
|
measure_id,
|
||||||
task_id,
|
task_id,
|
||||||
reference_id,
|
reference_id,
|
||||||
@@ -155,6 +157,7 @@ INSERT INTO
|
|||||||
VALUES (
|
VALUES (
|
||||||
@tenant_id,
|
@tenant_id,
|
||||||
@evidence_id,
|
@evidence_id,
|
||||||
|
@organization_id,
|
||||||
@measure_id,
|
@measure_id,
|
||||||
@task_id,
|
@task_id,
|
||||||
@reference_id,
|
@reference_id,
|
||||||
@@ -171,6 +174,7 @@ VALUES (
|
|||||||
args := pgx.StrictNamedArgs{
|
args := pgx.StrictNamedArgs{
|
||||||
"tenant_id": scope.GetTenantID(),
|
"tenant_id": scope.GetTenantID(),
|
||||||
"evidence_id": e.ID,
|
"evidence_id": e.ID,
|
||||||
|
"organization_id": e.OrganizationID,
|
||||||
"measure_id": e.MeasureID,
|
"measure_id": e.MeasureID,
|
||||||
"task_id": e.TaskID,
|
"task_id": e.TaskID,
|
||||||
"reference_id": e.ReferenceID,
|
"reference_id": e.ReferenceID,
|
||||||
@@ -208,6 +212,7 @@ func (e *Evidence) LoadByID(
|
|||||||
q := `
|
q := `
|
||||||
SELECT
|
SELECT
|
||||||
id,
|
id,
|
||||||
|
organization_id,
|
||||||
task_id,
|
task_id,
|
||||||
measure_id,
|
measure_id,
|
||||||
reference_id,
|
reference_id,
|
||||||
@@ -288,6 +293,7 @@ func (e *Evidences) LoadByMeasureID(
|
|||||||
q := `
|
q := `
|
||||||
SELECT
|
SELECT
|
||||||
id,
|
id,
|
||||||
|
organization_id,
|
||||||
measure_id,
|
measure_id,
|
||||||
task_id,
|
task_id,
|
||||||
reference_id,
|
reference_id,
|
||||||
@@ -369,6 +375,7 @@ func (e *Evidences) LoadByTaskID(
|
|||||||
q := `
|
q := `
|
||||||
SELECT
|
SELECT
|
||||||
id,
|
id,
|
||||||
|
organization_id,
|
||||||
measure_id,
|
measure_id,
|
||||||
task_id,
|
task_id,
|
||||||
reference_id,
|
reference_id,
|
||||||
|
|||||||
@@ -8,14 +8,15 @@ import (
|
|||||||
"maps"
|
"maps"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"go.probo.inc/probo/pkg/gid"
|
|
||||||
"github.com/jackc/pgx/v5"
|
"github.com/jackc/pgx/v5"
|
||||||
"go.gearno.de/kit/pg"
|
"go.gearno.de/kit/pg"
|
||||||
|
"go.probo.inc/probo/pkg/gid"
|
||||||
)
|
)
|
||||||
|
|
||||||
type (
|
type (
|
||||||
ExportJob struct {
|
ExportJob struct {
|
||||||
ID gid.GID `db:"id"`
|
ID gid.GID `db:"id"`
|
||||||
|
OrganizationID gid.GID `db:"organization_id"`
|
||||||
Type ExportJobType `db:"type"`
|
Type ExportJobType `db:"type"`
|
||||||
Arguments json.RawMessage `db:"arguments"`
|
Arguments json.RawMessage `db:"arguments"`
|
||||||
Error *string `db:"error"`
|
Error *string `db:"error"`
|
||||||
@@ -54,6 +55,7 @@ func (ej *ExportJob) Insert(
|
|||||||
q := `
|
q := `
|
||||||
INSERT INTO export_jobs (
|
INSERT INTO export_jobs (
|
||||||
id,
|
id,
|
||||||
|
organization_id,
|
||||||
tenant_id,
|
tenant_id,
|
||||||
type,
|
type,
|
||||||
arguments,
|
arguments,
|
||||||
@@ -63,6 +65,7 @@ INSERT INTO export_jobs (
|
|||||||
created_at
|
created_at
|
||||||
) VALUES (
|
) VALUES (
|
||||||
@id,
|
@id,
|
||||||
|
@organization_id,
|
||||||
@tenant_id,
|
@tenant_id,
|
||||||
@type,
|
@type,
|
||||||
@arguments,
|
@arguments,
|
||||||
@@ -73,6 +76,7 @@ INSERT INTO export_jobs (
|
|||||||
)`
|
)`
|
||||||
args := pgx.StrictNamedArgs{
|
args := pgx.StrictNamedArgs{
|
||||||
"id": ej.ID,
|
"id": ej.ID,
|
||||||
|
"organization_id": ej.OrganizationID,
|
||||||
"tenant_id": scope.GetTenantID(),
|
"tenant_id": scope.GetTenantID(),
|
||||||
"type": ej.Type,
|
"type": ej.Type,
|
||||||
"arguments": ej.Arguments,
|
"arguments": ej.Arguments,
|
||||||
@@ -126,6 +130,7 @@ func (ej *ExportJob) LoadByID(
|
|||||||
q := `
|
q := `
|
||||||
SELECT
|
SELECT
|
||||||
id,
|
id,
|
||||||
|
organization_id,
|
||||||
type,
|
type,
|
||||||
arguments,
|
arguments,
|
||||||
error,
|
error,
|
||||||
@@ -167,6 +172,7 @@ func (ej *ExportJob) LoadNextPendingForUpdateSkipLocked(
|
|||||||
q := `
|
q := `
|
||||||
SELECT
|
SELECT
|
||||||
id,
|
id,
|
||||||
|
organization_id,
|
||||||
type,
|
type,
|
||||||
arguments,
|
arguments,
|
||||||
error,
|
error,
|
||||||
|
|||||||
@@ -21,15 +21,16 @@ import (
|
|||||||
"maps"
|
"maps"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"go.probo.inc/probo/pkg/gid"
|
|
||||||
"github.com/jackc/pgx/v5"
|
"github.com/jackc/pgx/v5"
|
||||||
"github.com/jackc/pgx/v5/pgconn"
|
"github.com/jackc/pgx/v5/pgconn"
|
||||||
"go.gearno.de/kit/pg"
|
"go.gearno.de/kit/pg"
|
||||||
|
"go.probo.inc/probo/pkg/gid"
|
||||||
)
|
)
|
||||||
|
|
||||||
type (
|
type (
|
||||||
File struct {
|
File struct {
|
||||||
ID gid.GID `db:"id"`
|
ID gid.GID `db:"id"`
|
||||||
|
OrganizationID gid.GID `db:"organization_id"`
|
||||||
BucketName string `db:"bucket_name"`
|
BucketName string `db:"bucket_name"`
|
||||||
MimeType string `db:"mime_type"`
|
MimeType string `db:"mime_type"`
|
||||||
FileName string `db:"file_name"`
|
FileName string `db:"file_name"`
|
||||||
@@ -68,6 +69,7 @@ func (f *File) LoadByID(
|
|||||||
q := `
|
q := `
|
||||||
SELECT
|
SELECT
|
||||||
id,
|
id,
|
||||||
|
organization_id,
|
||||||
bucket_name,
|
bucket_name,
|
||||||
mime_type,
|
mime_type,
|
||||||
file_name,
|
file_name,
|
||||||
@@ -119,6 +121,7 @@ INSERT INTO
|
|||||||
files (
|
files (
|
||||||
id,
|
id,
|
||||||
tenant_id,
|
tenant_id,
|
||||||
|
organization_id,
|
||||||
bucket_name,
|
bucket_name,
|
||||||
mime_type,
|
mime_type,
|
||||||
file_name,
|
file_name,
|
||||||
@@ -131,6 +134,7 @@ INSERT INTO
|
|||||||
VALUES (
|
VALUES (
|
||||||
@file_id,
|
@file_id,
|
||||||
@tenant_id,
|
@tenant_id,
|
||||||
|
@organization_id,
|
||||||
@bucket_name,
|
@bucket_name,
|
||||||
@mime_type,
|
@mime_type,
|
||||||
@file_name,
|
@file_name,
|
||||||
@@ -145,6 +149,7 @@ VALUES (
|
|||||||
args := pgx.StrictNamedArgs{
|
args := pgx.StrictNamedArgs{
|
||||||
"file_id": f.ID,
|
"file_id": f.ID,
|
||||||
"tenant_id": scope.GetTenantID(),
|
"tenant_id": scope.GetTenantID(),
|
||||||
|
"organization_id": f.OrganizationID,
|
||||||
"bucket_name": f.BucketName,
|
"bucket_name": f.BucketName,
|
||||||
"mime_type": f.MimeType,
|
"mime_type": f.MimeType,
|
||||||
"file_name": f.FileName,
|
"file_name": f.FileName,
|
||||||
|
|||||||
@@ -21,10 +21,10 @@ import (
|
|||||||
"maps"
|
"maps"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"go.probo.inc/probo/pkg/gid"
|
|
||||||
"go.probo.inc/probo/pkg/page"
|
|
||||||
"github.com/jackc/pgx/v5"
|
"github.com/jackc/pgx/v5"
|
||||||
"go.gearno.de/kit/pg"
|
"go.gearno.de/kit/pg"
|
||||||
|
"go.probo.inc/probo/pkg/gid"
|
||||||
|
"go.probo.inc/probo/pkg/page"
|
||||||
)
|
)
|
||||||
|
|
||||||
type (
|
type (
|
||||||
@@ -33,7 +33,7 @@ type (
|
|||||||
OrganizationID gid.GID `db:"organization_id"`
|
OrganizationID gid.GID `db:"organization_id"`
|
||||||
Email string `db:"email"`
|
Email string `db:"email"`
|
||||||
FullName string `db:"full_name"`
|
FullName string `db:"full_name"`
|
||||||
Role Role `db:"role"`
|
Role MembershipRole `db:"role"`
|
||||||
Status InvitationStatus `db:"status"`
|
Status InvitationStatus `db:"status"`
|
||||||
ExpiresAt time.Time `db:"expires_at"`
|
ExpiresAt time.Time `db:"expires_at"`
|
||||||
AcceptedAt *time.Time `db:"accepted_at"`
|
AcceptedAt *time.Time `db:"accepted_at"`
|
||||||
@@ -47,7 +47,7 @@ type (
|
|||||||
OrganizationID gid.GID `json:"organization_id"`
|
OrganizationID gid.GID `json:"organization_id"`
|
||||||
Email string `json:"email"`
|
Email string `json:"email"`
|
||||||
FullName string `json:"full_name"`
|
FullName string `json:"full_name"`
|
||||||
Role Role `json:"role"`
|
Role MembershipRole `json:"role"`
|
||||||
}
|
}
|
||||||
|
|
||||||
ErrInvitationNotFound struct {
|
ErrInvitationNotFound struct {
|
||||||
|
|||||||
@@ -19,20 +19,19 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Role string
|
type MembershipRole string
|
||||||
|
|
||||||
const (
|
const (
|
||||||
RoleOwner Role = "OWNER"
|
MembershipRoleOwner MembershipRole = "OWNER"
|
||||||
RoleAdmin Role = "ADMIN"
|
MembershipRoleAdmin MembershipRole = "ADMIN"
|
||||||
RoleMember Role = "MEMBER"
|
MembershipRoleViewer MembershipRole = "VIEWER"
|
||||||
RoleViewer Role = "VIEWER"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func (r Role) String() string {
|
func (r MembershipRole) String() string {
|
||||||
return string(r)
|
return string(r)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *Role) Scan(value any) error {
|
func (r *MembershipRole) Scan(value any) error {
|
||||||
var s string
|
var s string
|
||||||
switch v := value.(type) {
|
switch v := value.(type) {
|
||||||
case string:
|
case string:
|
||||||
@@ -40,24 +39,22 @@ func (r *Role) Scan(value any) error {
|
|||||||
case []byte:
|
case []byte:
|
||||||
s = string(v)
|
s = string(v)
|
||||||
default:
|
default:
|
||||||
return fmt.Errorf("unsupported type for Role: %T", value)
|
return fmt.Errorf("unsupported type for MembershipRole: %T", value)
|
||||||
}
|
}
|
||||||
|
|
||||||
switch s {
|
switch s {
|
||||||
case "OWNER":
|
case "OWNER":
|
||||||
*r = RoleOwner
|
*r = MembershipRoleOwner
|
||||||
case "ADMIN":
|
case "ADMIN":
|
||||||
*r = RoleAdmin
|
*r = MembershipRoleAdmin
|
||||||
case "MEMBER":
|
|
||||||
*r = RoleMember
|
|
||||||
case "VIEWER":
|
case "VIEWER":
|
||||||
*r = RoleViewer
|
*r = MembershipRoleViewer
|
||||||
default:
|
default:
|
||||||
return fmt.Errorf("invalid Role value: %q", s)
|
return fmt.Errorf("invalid MembershipRole value: %q", s)
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r Role) Value() (driver.Value, error) {
|
func (r MembershipRole) Value() (driver.Value, error) {
|
||||||
return r.String(), nil
|
return r.String(), nil
|
||||||
}
|
}
|
||||||
@@ -19,13 +19,14 @@ import (
|
|||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"maps"
|
"maps"
|
||||||
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"go.probo.inc/probo/pkg/gid"
|
|
||||||
"go.probo.inc/probo/pkg/page"
|
|
||||||
"github.com/jackc/pgx/v5"
|
"github.com/jackc/pgx/v5"
|
||||||
"github.com/jackc/pgx/v5/pgconn"
|
"github.com/jackc/pgx/v5/pgconn"
|
||||||
"go.gearno.de/kit/pg"
|
"go.gearno.de/kit/pg"
|
||||||
|
"go.probo.inc/probo/pkg/gid"
|
||||||
|
"go.probo.inc/probo/pkg/page"
|
||||||
)
|
)
|
||||||
|
|
||||||
type (
|
type (
|
||||||
@@ -33,7 +34,7 @@ type (
|
|||||||
ID gid.GID `db:"id"`
|
ID gid.GID `db:"id"`
|
||||||
UserID gid.GID `db:"user_id"`
|
UserID gid.GID `db:"user_id"`
|
||||||
OrganizationID gid.GID `db:"organization_id"`
|
OrganizationID gid.GID `db:"organization_id"`
|
||||||
Role Role `db:"role"`
|
Role MembershipRole `db:"role"`
|
||||||
FullName string `db:"full_name"`
|
FullName string `db:"full_name"`
|
||||||
EmailAddress string `db:"email_address"`
|
EmailAddress string `db:"email_address"`
|
||||||
CreatedAt time.Time `db:"created_at"`
|
CreatedAt time.Time `db:"created_at"`
|
||||||
@@ -185,6 +186,83 @@ JOIN
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// LoadRoleByUserAndEntityID loads a user's role by querying any entity to extract its organization_id
|
||||||
|
func (m *Membership) LoadRoleByUserAndEntityID(
|
||||||
|
ctx context.Context,
|
||||||
|
conn pg.Conn,
|
||||||
|
scope Scoper,
|
||||||
|
userID gid.GID,
|
||||||
|
entityID gid.GID,
|
||||||
|
) error {
|
||||||
|
entityType := entityID.EntityType()
|
||||||
|
|
||||||
|
// For organization, the entity ID is the organization ID
|
||||||
|
if entityType == OrganizationEntityType {
|
||||||
|
return m.LoadByUserAndOrg(ctx, conn, scope, userID, entityID)
|
||||||
|
}
|
||||||
|
|
||||||
|
tableName, ok := EntityTable(entityType)
|
||||||
|
if !ok {
|
||||||
|
return fmt.Errorf("unsupported entity type for role lookup: %d", entityType)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build scope fragment with table alias to avoid ambiguity
|
||||||
|
scopeFragment := scope.SQLFragment()
|
||||||
|
// Replace column references with table-qualified versions
|
||||||
|
scopeFragment = strings.ReplaceAll(scopeFragment, "tenant_id =", "m.tenant_id =")
|
||||||
|
|
||||||
|
query := fmt.Sprintf(`
|
||||||
|
SELECT
|
||||||
|
m.id,
|
||||||
|
m.user_id,
|
||||||
|
m.organization_id,
|
||||||
|
m.role,
|
||||||
|
m.created_at,
|
||||||
|
m.updated_at
|
||||||
|
FROM
|
||||||
|
authz_memberships m
|
||||||
|
INNER JOIN %s e ON e.id = @entity_id
|
||||||
|
WHERE
|
||||||
|
%s
|
||||||
|
AND m.user_id = @user_id
|
||||||
|
AND m.organization_id = e.organization_id
|
||||||
|
LIMIT 1;
|
||||||
|
`, tableName, scopeFragment)
|
||||||
|
|
||||||
|
args := pgx.NamedArgs{
|
||||||
|
"user_id": userID,
|
||||||
|
"entity_id": entityID,
|
||||||
|
}
|
||||||
|
maps.Copy(args, scope.SQLArguments())
|
||||||
|
|
||||||
|
rows, err := conn.Query(ctx, query, args)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("cannot query membership by entity: %w", err)
|
||||||
|
}
|
||||||
|
defer rows.Close()
|
||||||
|
|
||||||
|
if !rows.Next() {
|
||||||
|
return &ErrMembershipNotFound{UserID: userID, OrgID: entityID}
|
||||||
|
}
|
||||||
|
|
||||||
|
var membership Membership
|
||||||
|
err = rows.Scan(
|
||||||
|
&membership.ID,
|
||||||
|
&membership.UserID,
|
||||||
|
&membership.OrganizationID,
|
||||||
|
&membership.Role,
|
||||||
|
&membership.CreatedAt,
|
||||||
|
&membership.UpdatedAt,
|
||||||
|
)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("cannot scan membership: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
*m = membership
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
func (m *Membership) LoadByUserAndOrg(
|
func (m *Membership) LoadByUserAndOrg(
|
||||||
ctx context.Context,
|
ctx context.Context,
|
||||||
conn pg.Conn,
|
conn pg.Conn,
|
||||||
@@ -195,17 +273,17 @@ func (m *Membership) LoadByUserAndOrg(
|
|||||||
query := `
|
query := `
|
||||||
WITH mbr AS (
|
WITH mbr AS (
|
||||||
SELECT
|
SELECT
|
||||||
id,
|
am.id,
|
||||||
user_id,
|
am.user_id,
|
||||||
organization_id,
|
am.organization_id,
|
||||||
role,
|
am.role,
|
||||||
created_at,
|
am.created_at,
|
||||||
updated_at
|
am.updated_at
|
||||||
FROM
|
FROM
|
||||||
authz_memberships
|
authz_memberships am
|
||||||
WHERE
|
WHERE
|
||||||
user_id = @user_id
|
am.user_id = @user_id
|
||||||
AND organization_id = @organization_id
|
AND am.organization_id = @organization_id
|
||||||
AND %s
|
AND %s
|
||||||
)
|
)
|
||||||
SELECT
|
SELECT
|
||||||
@@ -223,7 +301,12 @@ JOIN
|
|||||||
users u ON mbr.user_id = u.id
|
users u ON mbr.user_id = u.id
|
||||||
`
|
`
|
||||||
|
|
||||||
query = fmt.Sprintf(query, scope.SQLFragment())
|
// Build scope fragment with table alias
|
||||||
|
scopeFragment := scope.SQLFragment()
|
||||||
|
// Replace column references with table-qualified versions
|
||||||
|
scopeFragment = strings.ReplaceAll(scopeFragment, "tenant_id =", "am.tenant_id =")
|
||||||
|
|
||||||
|
query = fmt.Sprintf(query, scopeFragment)
|
||||||
|
|
||||||
args := pgx.StrictNamedArgs{
|
args := pgx.StrictNamedArgs{
|
||||||
"user_id": userID,
|
"user_id": userID,
|
||||||
@@ -468,67 +551,3 @@ WHERE
|
|||||||
}
|
}
|
||||||
return count, nil
|
return count, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func LoadUserIDsByOrganizationID(
|
|
||||||
ctx context.Context,
|
|
||||||
conn pg.Conn,
|
|
||||||
scope Scoper,
|
|
||||||
organizationID gid.GID,
|
|
||||||
) ([]gid.GID, error) {
|
|
||||||
query := `
|
|
||||||
SELECT user_id
|
|
||||||
FROM authz_memberships
|
|
||||||
WHERE organization_id = @organization_id AND %s
|
|
||||||
`
|
|
||||||
query = fmt.Sprintf(query, scope.SQLFragment())
|
|
||||||
args := pgx.StrictNamedArgs{"organization_id": organizationID}
|
|
||||||
maps.Copy(args, scope.SQLArguments())
|
|
||||||
|
|
||||||
rows, err := conn.Query(ctx, query, args)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("cannot query memberships: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
var userIDs []gid.GID
|
|
||||||
for rows.Next() {
|
|
||||||
var userID gid.GID
|
|
||||||
if err := rows.Scan(&userID); err != nil {
|
|
||||||
rows.Close()
|
|
||||||
return nil, fmt.Errorf("cannot scan user_id: %w", err)
|
|
||||||
}
|
|
||||||
userIDs = append(userIDs, userID)
|
|
||||||
}
|
|
||||||
rows.Close()
|
|
||||||
|
|
||||||
return userIDs, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func UpdateMembershipUserID(
|
|
||||||
ctx context.Context,
|
|
||||||
conn pg.Conn,
|
|
||||||
scope Scoper,
|
|
||||||
oldUserID gid.GID,
|
|
||||||
newUserID gid.GID,
|
|
||||||
organizationID gid.GID,
|
|
||||||
) error {
|
|
||||||
query := `
|
|
||||||
UPDATE authz_memberships
|
|
||||||
SET user_id = @new_user_id, updated_at = @updated_at
|
|
||||||
WHERE user_id = @old_user_id AND organization_id = @organization_id AND %s
|
|
||||||
`
|
|
||||||
query = fmt.Sprintf(query, scope.SQLFragment())
|
|
||||||
args := pgx.StrictNamedArgs{
|
|
||||||
"new_user_id": newUserID,
|
|
||||||
"old_user_id": oldUserID,
|
|
||||||
"organization_id": organizationID,
|
|
||||||
"updated_at": time.Now(),
|
|
||||||
}
|
|
||||||
maps.Copy(args, scope.SQLArguments())
|
|
||||||
|
|
||||||
_, err := conn.Exec(ctx, query, args)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("cannot update membership: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|||||||
210
pkg/coredata/migrations/20251109T214255Z.sql
Normal file
210
pkg/coredata/migrations/20251109T214255Z.sql
Normal file
@@ -0,0 +1,210 @@
|
|||||||
|
-- Set all existing memberships to OWNER role
|
||||||
|
UPDATE authz_memberships SET role = 'OWNER';
|
||||||
|
|
||||||
|
ALTER TABLE controls ADD COLUMN IF NOT EXISTS organization_id TEXT;
|
||||||
|
UPDATE controls
|
||||||
|
SET organization_id = organizations.id
|
||||||
|
FROM organizations
|
||||||
|
WHERE controls.tenant_id = organizations.tenant_id
|
||||||
|
AND controls.organization_id IS NULL;
|
||||||
|
ALTER TABLE controls ALTER COLUMN organization_id SET NOT NULL;
|
||||||
|
|
||||||
|
ALTER TABLE evidences ADD COLUMN IF NOT EXISTS organization_id TEXT;
|
||||||
|
UPDATE evidences
|
||||||
|
SET organization_id = organizations.id
|
||||||
|
FROM organizations
|
||||||
|
WHERE evidences.tenant_id = organizations.tenant_id
|
||||||
|
AND evidences.organization_id IS NULL;
|
||||||
|
ALTER TABLE evidences ALTER COLUMN organization_id SET NOT NULL;
|
||||||
|
|
||||||
|
ALTER TABLE files ADD COLUMN IF NOT EXISTS organization_id TEXT;
|
||||||
|
UPDATE files
|
||||||
|
SET organization_id = organizations.id
|
||||||
|
FROM organizations
|
||||||
|
WHERE files.tenant_id = organizations.tenant_id
|
||||||
|
AND files.organization_id IS NULL;
|
||||||
|
ALTER TABLE files ALTER COLUMN organization_id SET NOT NULL;
|
||||||
|
|
||||||
|
ALTER TABLE document_versions ADD COLUMN IF NOT EXISTS organization_id TEXT;
|
||||||
|
UPDATE document_versions
|
||||||
|
SET organization_id = organizations.id
|
||||||
|
FROM organizations
|
||||||
|
WHERE document_versions.tenant_id = organizations.tenant_id
|
||||||
|
AND document_versions.organization_id IS NULL;
|
||||||
|
ALTER TABLE document_versions ALTER COLUMN organization_id SET NOT NULL;
|
||||||
|
|
||||||
|
ALTER TABLE document_version_signatures ADD COLUMN IF NOT EXISTS organization_id TEXT;
|
||||||
|
UPDATE document_version_signatures
|
||||||
|
SET organization_id = organizations.id
|
||||||
|
FROM organizations
|
||||||
|
WHERE document_version_signatures.tenant_id = organizations.tenant_id
|
||||||
|
AND document_version_signatures.organization_id IS NULL;
|
||||||
|
ALTER TABLE document_version_signatures ALTER COLUMN organization_id SET NOT NULL;
|
||||||
|
|
||||||
|
ALTER TABLE trust_center_references ADD COLUMN IF NOT EXISTS organization_id TEXT;
|
||||||
|
UPDATE trust_center_references
|
||||||
|
SET organization_id = organizations.id
|
||||||
|
FROM organizations
|
||||||
|
WHERE trust_center_references.tenant_id = organizations.tenant_id
|
||||||
|
AND trust_center_references.organization_id IS NULL;
|
||||||
|
ALTER TABLE trust_center_references ALTER COLUMN organization_id SET NOT NULL;
|
||||||
|
|
||||||
|
ALTER TABLE trust_center_accesses ADD COLUMN IF NOT EXISTS organization_id TEXT;
|
||||||
|
UPDATE trust_center_accesses
|
||||||
|
SET organization_id = organizations.id
|
||||||
|
FROM organizations
|
||||||
|
WHERE trust_center_accesses.tenant_id = organizations.tenant_id
|
||||||
|
AND trust_center_accesses.organization_id IS NULL;
|
||||||
|
ALTER TABLE trust_center_accesses ALTER COLUMN organization_id SET NOT NULL;
|
||||||
|
|
||||||
|
ALTER TABLE trust_center_document_accesses ADD COLUMN IF NOT EXISTS organization_id TEXT;
|
||||||
|
UPDATE trust_center_document_accesses
|
||||||
|
SET organization_id = organizations.id
|
||||||
|
FROM organizations
|
||||||
|
WHERE trust_center_document_accesses.tenant_id = organizations.tenant_id
|
||||||
|
AND trust_center_document_accesses.organization_id IS NULL;
|
||||||
|
ALTER TABLE trust_center_document_accesses ALTER COLUMN organization_id SET NOT NULL;
|
||||||
|
|
||||||
|
ALTER TABLE vendor_services ADD COLUMN IF NOT EXISTS organization_id TEXT;
|
||||||
|
UPDATE vendor_services
|
||||||
|
SET organization_id = organizations.id
|
||||||
|
FROM organizations
|
||||||
|
WHERE vendor_services.tenant_id = organizations.tenant_id
|
||||||
|
AND vendor_services.organization_id IS NULL;
|
||||||
|
ALTER TABLE vendor_services ALTER COLUMN organization_id SET NOT NULL;
|
||||||
|
|
||||||
|
ALTER TABLE vendor_contacts ADD COLUMN IF NOT EXISTS organization_id TEXT;
|
||||||
|
UPDATE vendor_contacts
|
||||||
|
SET organization_id = organizations.id
|
||||||
|
FROM organizations
|
||||||
|
WHERE vendor_contacts.tenant_id = organizations.tenant_id
|
||||||
|
AND vendor_contacts.organization_id IS NULL;
|
||||||
|
ALTER TABLE vendor_contacts ALTER COLUMN organization_id SET NOT NULL;
|
||||||
|
|
||||||
|
ALTER TABLE vendor_risk_assessments ADD COLUMN IF NOT EXISTS organization_id TEXT;
|
||||||
|
UPDATE vendor_risk_assessments
|
||||||
|
SET organization_id = organizations.id
|
||||||
|
FROM organizations
|
||||||
|
WHERE vendor_risk_assessments.tenant_id = organizations.tenant_id
|
||||||
|
AND vendor_risk_assessments.organization_id IS NULL;
|
||||||
|
ALTER TABLE vendor_risk_assessments ALTER COLUMN organization_id SET NOT NULL;
|
||||||
|
|
||||||
|
ALTER TABLE reports ADD COLUMN IF NOT EXISTS organization_id TEXT;
|
||||||
|
UPDATE reports
|
||||||
|
SET organization_id = organizations.id
|
||||||
|
FROM organizations
|
||||||
|
WHERE reports.tenant_id = organizations.tenant_id
|
||||||
|
AND reports.organization_id IS NULL;
|
||||||
|
ALTER TABLE reports ALTER COLUMN organization_id SET NOT NULL;
|
||||||
|
|
||||||
|
ALTER TABLE custom_domains ADD COLUMN IF NOT EXISTS organization_id TEXT;
|
||||||
|
UPDATE custom_domains
|
||||||
|
SET organization_id = organizations.id
|
||||||
|
FROM organizations
|
||||||
|
WHERE custom_domains.tenant_id = organizations.tenant_id
|
||||||
|
AND custom_domains.organization_id IS NULL;
|
||||||
|
ALTER TABLE custom_domains ALTER COLUMN organization_id SET NOT NULL;
|
||||||
|
|
||||||
|
ALTER TABLE asset_vendors ADD COLUMN IF NOT EXISTS organization_id TEXT;
|
||||||
|
UPDATE asset_vendors
|
||||||
|
SET organization_id = organizations.id
|
||||||
|
FROM organizations
|
||||||
|
WHERE asset_vendors.tenant_id = organizations.tenant_id
|
||||||
|
AND asset_vendors.organization_id IS NULL;
|
||||||
|
ALTER TABLE asset_vendors ALTER COLUMN organization_id SET NOT NULL;
|
||||||
|
|
||||||
|
ALTER TABLE authz_api_keys_memberships ADD COLUMN IF NOT EXISTS organization_id TEXT;
|
||||||
|
UPDATE authz_api_keys_memberships
|
||||||
|
SET organization_id = organizations.id
|
||||||
|
FROM organizations
|
||||||
|
WHERE authz_api_keys_memberships.tenant_id = organizations.tenant_id
|
||||||
|
AND authz_api_keys_memberships.organization_id IS NULL;
|
||||||
|
ALTER TABLE authz_api_keys_memberships ALTER COLUMN organization_id SET NOT NULL;
|
||||||
|
|
||||||
|
ALTER TABLE controls_audits ADD COLUMN IF NOT EXISTS organization_id TEXT;
|
||||||
|
UPDATE controls_audits
|
||||||
|
SET organization_id = organizations.id
|
||||||
|
FROM organizations
|
||||||
|
WHERE controls_audits.tenant_id = organizations.tenant_id
|
||||||
|
AND controls_audits.organization_id IS NULL;
|
||||||
|
ALTER TABLE controls_audits ALTER COLUMN organization_id SET NOT NULL;
|
||||||
|
|
||||||
|
ALTER TABLE controls_documents ADD COLUMN IF NOT EXISTS organization_id TEXT;
|
||||||
|
UPDATE controls_documents
|
||||||
|
SET organization_id = organizations.id
|
||||||
|
FROM organizations
|
||||||
|
WHERE controls_documents.tenant_id = organizations.tenant_id
|
||||||
|
AND controls_documents.organization_id IS NULL;
|
||||||
|
ALTER TABLE controls_documents ALTER COLUMN organization_id SET NOT NULL;
|
||||||
|
|
||||||
|
ALTER TABLE controls_measures ADD COLUMN IF NOT EXISTS organization_id TEXT;
|
||||||
|
UPDATE controls_measures
|
||||||
|
SET organization_id = organizations.id
|
||||||
|
FROM organizations
|
||||||
|
WHERE controls_measures.tenant_id = organizations.tenant_id
|
||||||
|
AND controls_measures.organization_id IS NULL;
|
||||||
|
ALTER TABLE controls_measures ALTER COLUMN organization_id SET NOT NULL;
|
||||||
|
|
||||||
|
ALTER TABLE controls_snapshots ADD COLUMN IF NOT EXISTS organization_id TEXT;
|
||||||
|
UPDATE controls_snapshots
|
||||||
|
SET organization_id = organizations.id
|
||||||
|
FROM organizations
|
||||||
|
WHERE controls_snapshots.tenant_id = organizations.tenant_id
|
||||||
|
AND controls_snapshots.organization_id IS NULL;
|
||||||
|
ALTER TABLE controls_snapshots ALTER COLUMN organization_id SET NOT NULL;
|
||||||
|
|
||||||
|
ALTER TABLE data_vendors ADD COLUMN IF NOT EXISTS organization_id TEXT;
|
||||||
|
UPDATE data_vendors
|
||||||
|
SET organization_id = organizations.id
|
||||||
|
FROM organizations
|
||||||
|
WHERE data_vendors.tenant_id = organizations.tenant_id
|
||||||
|
AND data_vendors.organization_id IS NULL;
|
||||||
|
ALTER TABLE data_vendors ALTER COLUMN organization_id SET NOT NULL;
|
||||||
|
|
||||||
|
ALTER TABLE export_jobs ADD COLUMN IF NOT EXISTS organization_id TEXT;
|
||||||
|
UPDATE export_jobs
|
||||||
|
SET organization_id = organizations.id
|
||||||
|
FROM organizations
|
||||||
|
WHERE export_jobs.tenant_id = organizations.tenant_id
|
||||||
|
AND export_jobs.organization_id IS NULL;
|
||||||
|
ALTER TABLE export_jobs ALTER COLUMN organization_id SET NOT NULL;
|
||||||
|
|
||||||
|
ALTER TABLE processing_activity_vendors ADD COLUMN IF NOT EXISTS organization_id TEXT;
|
||||||
|
UPDATE processing_activity_vendors
|
||||||
|
SET organization_id = organizations.id
|
||||||
|
FROM organizations
|
||||||
|
WHERE processing_activity_vendors.tenant_id = organizations.tenant_id
|
||||||
|
AND processing_activity_vendors.organization_id IS NULL;
|
||||||
|
ALTER TABLE processing_activity_vendors ALTER COLUMN organization_id SET NOT NULL;
|
||||||
|
|
||||||
|
ALTER TABLE risks_documents ADD COLUMN IF NOT EXISTS organization_id TEXT;
|
||||||
|
UPDATE risks_documents
|
||||||
|
SET organization_id = organizations.id
|
||||||
|
FROM organizations
|
||||||
|
WHERE risks_documents.tenant_id = organizations.tenant_id
|
||||||
|
AND risks_documents.organization_id IS NULL;
|
||||||
|
ALTER TABLE risks_documents ALTER COLUMN organization_id SET NOT NULL;
|
||||||
|
|
||||||
|
ALTER TABLE risks_measures ADD COLUMN IF NOT EXISTS organization_id TEXT;
|
||||||
|
UPDATE risks_measures
|
||||||
|
SET organization_id = organizations.id
|
||||||
|
FROM organizations
|
||||||
|
WHERE risks_measures.tenant_id = organizations.tenant_id
|
||||||
|
AND risks_measures.organization_id IS NULL;
|
||||||
|
ALTER TABLE risks_measures ALTER COLUMN organization_id SET NOT NULL;
|
||||||
|
|
||||||
|
ALTER TABLE risks_obligations ADD COLUMN IF NOT EXISTS organization_id TEXT;
|
||||||
|
UPDATE risks_obligations
|
||||||
|
SET organization_id = organizations.id
|
||||||
|
FROM organizations
|
||||||
|
WHERE risks_obligations.tenant_id = organizations.tenant_id
|
||||||
|
AND risks_obligations.organization_id IS NULL;
|
||||||
|
ALTER TABLE risks_obligations ALTER COLUMN organization_id SET NOT NULL;
|
||||||
|
|
||||||
|
ALTER TABLE vendor_compliance_reports ADD COLUMN IF NOT EXISTS organization_id TEXT;
|
||||||
|
UPDATE vendor_compliance_reports
|
||||||
|
SET organization_id = organizations.id
|
||||||
|
FROM organizations
|
||||||
|
WHERE vendor_compliance_reports.tenant_id = organizations.tenant_id
|
||||||
|
AND vendor_compliance_reports.organization_id IS NULL;
|
||||||
|
ALTER TABLE vendor_compliance_reports ALTER COLUMN organization_id SET NOT NULL;
|
||||||
@@ -237,6 +237,63 @@ ORDER BY
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (o *Organizations) LoadAllByUserIDWithRole(
|
||||||
|
ctx context.Context,
|
||||||
|
conn pg.Conn,
|
||||||
|
userID gid.GID,
|
||||||
|
role MembershipRole,
|
||||||
|
) error {
|
||||||
|
q := `
|
||||||
|
WITH user_org AS (
|
||||||
|
SELECT
|
||||||
|
organization_id
|
||||||
|
FROM
|
||||||
|
authz_memberships
|
||||||
|
WHERE
|
||||||
|
user_id = @user_id
|
||||||
|
AND role = @role
|
||||||
|
)
|
||||||
|
SELECT
|
||||||
|
tenant_id,
|
||||||
|
id,
|
||||||
|
name,
|
||||||
|
description,
|
||||||
|
website_url,
|
||||||
|
email,
|
||||||
|
headquarter_address,
|
||||||
|
custom_domain_id,
|
||||||
|
logo_file_id,
|
||||||
|
horizontal_logo_file_id,
|
||||||
|
created_at,
|
||||||
|
updated_at
|
||||||
|
FROM
|
||||||
|
organizations
|
||||||
|
INNER JOIN
|
||||||
|
user_org ON organizations.id = user_org.organization_id
|
||||||
|
ORDER BY
|
||||||
|
name ASC
|
||||||
|
`
|
||||||
|
|
||||||
|
args := pgx.StrictNamedArgs{
|
||||||
|
"user_id": userID,
|
||||||
|
"role": role,
|
||||||
|
}
|
||||||
|
|
||||||
|
rows, err := conn.Query(ctx, q, args)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("cannot query organizations: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
organizations, err := pgx.CollectRows(rows, pgx.RowToAddrOfStructByName[Organization])
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("cannot collect organizations: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
*o = organizations
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
func (o *Organizations) LoadAllByUserAPIKeyID(
|
func (o *Organizations) LoadAllByUserAPIKeyID(
|
||||||
ctx context.Context,
|
ctx context.Context,
|
||||||
conn pg.Conn,
|
conn pg.Conn,
|
||||||
|
|||||||
@@ -20,15 +20,16 @@ import (
|
|||||||
"maps"
|
"maps"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"go.probo.inc/probo/pkg/gid"
|
|
||||||
"go.probo.inc/probo/pkg/page"
|
|
||||||
"github.com/jackc/pgx/v5"
|
"github.com/jackc/pgx/v5"
|
||||||
"go.gearno.de/kit/pg"
|
"go.gearno.de/kit/pg"
|
||||||
|
"go.probo.inc/probo/pkg/gid"
|
||||||
|
"go.probo.inc/probo/pkg/page"
|
||||||
)
|
)
|
||||||
|
|
||||||
type (
|
type (
|
||||||
Report struct {
|
Report struct {
|
||||||
ID gid.GID `db:"id"`
|
ID gid.GID `db:"id"`
|
||||||
|
OrganizationID gid.GID `db:"organization_id"`
|
||||||
ObjectKey string `db:"object_key"`
|
ObjectKey string `db:"object_key"`
|
||||||
MimeType string `db:"mime_type"`
|
MimeType string `db:"mime_type"`
|
||||||
Filename string `db:"filename"`
|
Filename string `db:"filename"`
|
||||||
@@ -49,6 +50,7 @@ func (r *Report) LoadByID(
|
|||||||
q := `
|
q := `
|
||||||
SELECT
|
SELECT
|
||||||
id,
|
id,
|
||||||
|
organization_id,
|
||||||
object_key,
|
object_key,
|
||||||
mime_type,
|
mime_type,
|
||||||
filename,
|
filename,
|
||||||
@@ -92,6 +94,7 @@ func (r *Report) Insert(
|
|||||||
INSERT INTO reports (
|
INSERT INTO reports (
|
||||||
id,
|
id,
|
||||||
tenant_id,
|
tenant_id,
|
||||||
|
organization_id,
|
||||||
object_key,
|
object_key,
|
||||||
mime_type,
|
mime_type,
|
||||||
filename,
|
filename,
|
||||||
@@ -101,6 +104,7 @@ INSERT INTO reports (
|
|||||||
) VALUES (
|
) VALUES (
|
||||||
@id,
|
@id,
|
||||||
@tenant_id,
|
@tenant_id,
|
||||||
|
@organization_id,
|
||||||
@object_key,
|
@object_key,
|
||||||
@mime_type,
|
@mime_type,
|
||||||
@filename,
|
@filename,
|
||||||
@@ -113,6 +117,7 @@ INSERT INTO reports (
|
|||||||
args := pgx.StrictNamedArgs{
|
args := pgx.StrictNamedArgs{
|
||||||
"id": r.ID,
|
"id": r.ID,
|
||||||
"tenant_id": scope.GetTenantID(),
|
"tenant_id": scope.GetTenantID(),
|
||||||
|
"organization_id": r.OrganizationID,
|
||||||
"object_key": r.ObjectKey,
|
"object_key": r.ObjectKey,
|
||||||
"mime_type": r.MimeType,
|
"mime_type": r.MimeType,
|
||||||
"filename": r.Filename,
|
"filename": r.Filename,
|
||||||
|
|||||||
@@ -29,6 +29,7 @@ type (
|
|||||||
RiskDocument struct {
|
RiskDocument struct {
|
||||||
RiskID gid.GID `db:"risk_id"`
|
RiskID gid.GID `db:"risk_id"`
|
||||||
DocumentID gid.GID `db:"document_id"`
|
DocumentID gid.GID `db:"document_id"`
|
||||||
|
OrganizationID gid.GID `db:"organization_id"`
|
||||||
TenantID gid.TenantID `db:"tenant_id"`
|
TenantID gid.TenantID `db:"tenant_id"`
|
||||||
CreatedAt time.Time `db:"created_at"`
|
CreatedAt time.Time `db:"created_at"`
|
||||||
}
|
}
|
||||||
@@ -46,12 +47,14 @@ INSERT INTO
|
|||||||
risks_documents (
|
risks_documents (
|
||||||
risk_id,
|
risk_id,
|
||||||
document_id,
|
document_id,
|
||||||
|
organization_id,
|
||||||
tenant_id,
|
tenant_id,
|
||||||
created_at
|
created_at
|
||||||
)
|
)
|
||||||
VALUES (
|
VALUES (
|
||||||
@risk_id,
|
@risk_id,
|
||||||
@document_id,
|
@document_id,
|
||||||
|
@organization_id,
|
||||||
@tenant_id,
|
@tenant_id,
|
||||||
@created_at
|
@created_at
|
||||||
);
|
);
|
||||||
@@ -60,6 +63,7 @@ VALUES (
|
|||||||
args := pgx.StrictNamedArgs{
|
args := pgx.StrictNamedArgs{
|
||||||
"risk_id": rp.RiskID,
|
"risk_id": rp.RiskID,
|
||||||
"document_id": rp.DocumentID,
|
"document_id": rp.DocumentID,
|
||||||
|
"organization_id": rp.OrganizationID,
|
||||||
"tenant_id": scope.GetTenantID(),
|
"tenant_id": scope.GetTenantID(),
|
||||||
"created_at": rp.CreatedAt,
|
"created_at": rp.CreatedAt,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,15 +20,16 @@ import (
|
|||||||
"maps"
|
"maps"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"go.probo.inc/probo/pkg/gid"
|
|
||||||
"github.com/jackc/pgx/v5"
|
"github.com/jackc/pgx/v5"
|
||||||
"go.gearno.de/kit/pg"
|
"go.gearno.de/kit/pg"
|
||||||
|
"go.probo.inc/probo/pkg/gid"
|
||||||
)
|
)
|
||||||
|
|
||||||
type (
|
type (
|
||||||
RiskMeasure struct {
|
RiskMeasure struct {
|
||||||
RiskID gid.GID `db:"risk_id"`
|
RiskID gid.GID `db:"risk_id"`
|
||||||
MeasureID gid.GID `db:"measure_id"`
|
MeasureID gid.GID `db:"measure_id"`
|
||||||
|
OrganizationID gid.GID `db:"organization_id"`
|
||||||
TenantID gid.TenantID `db:"tenant_id"`
|
TenantID gid.TenantID `db:"tenant_id"`
|
||||||
CreatedAt time.Time `db:"created_at"`
|
CreatedAt time.Time `db:"created_at"`
|
||||||
}
|
}
|
||||||
@@ -46,12 +47,14 @@ INSERT INTO
|
|||||||
risks_measures (
|
risks_measures (
|
||||||
risk_id,
|
risk_id,
|
||||||
measure_id,
|
measure_id,
|
||||||
|
organization_id,
|
||||||
tenant_id,
|
tenant_id,
|
||||||
created_at
|
created_at
|
||||||
)
|
)
|
||||||
VALUES (
|
VALUES (
|
||||||
@risk_id,
|
@risk_id,
|
||||||
@measure_id,
|
@measure_id,
|
||||||
|
@organization_id,
|
||||||
@tenant_id,
|
@tenant_id,
|
||||||
@created_at
|
@created_at
|
||||||
);
|
);
|
||||||
@@ -60,6 +63,7 @@ VALUES (
|
|||||||
args := pgx.StrictNamedArgs{
|
args := pgx.StrictNamedArgs{
|
||||||
"risk_id": rm.RiskID,
|
"risk_id": rm.RiskID,
|
||||||
"measure_id": rm.MeasureID,
|
"measure_id": rm.MeasureID,
|
||||||
|
"organization_id": rm.OrganizationID,
|
||||||
"tenant_id": scope.GetTenantID(),
|
"tenant_id": scope.GetTenantID(),
|
||||||
"created_at": rm.CreatedAt,
|
"created_at": rm.CreatedAt,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -29,6 +29,7 @@ type (
|
|||||||
RiskObligation struct {
|
RiskObligation struct {
|
||||||
RiskID gid.GID `db:"risk_id"`
|
RiskID gid.GID `db:"risk_id"`
|
||||||
ObligationID gid.GID `db:"obligation_id"`
|
ObligationID gid.GID `db:"obligation_id"`
|
||||||
|
OrganizationID gid.GID `db:"organization_id"`
|
||||||
CreatedAt time.Time `db:"created_at"`
|
CreatedAt time.Time `db:"created_at"`
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -44,11 +45,13 @@ func (ro RiskObligation) Insert(
|
|||||||
INSERT INTO risks_obligations (
|
INSERT INTO risks_obligations (
|
||||||
risk_id,
|
risk_id,
|
||||||
obligation_id,
|
obligation_id,
|
||||||
|
organization_id,
|
||||||
tenant_id,
|
tenant_id,
|
||||||
created_at
|
created_at
|
||||||
) VALUES (
|
) VALUES (
|
||||||
@risk_id,
|
@risk_id,
|
||||||
@obligation_id,
|
@obligation_id,
|
||||||
|
@organization_id,
|
||||||
@tenant_id,
|
@tenant_id,
|
||||||
@created_at
|
@created_at
|
||||||
)
|
)
|
||||||
@@ -57,6 +60,7 @@ INSERT INTO risks_obligations (
|
|||||||
args := pgx.StrictNamedArgs{
|
args := pgx.StrictNamedArgs{
|
||||||
"risk_id": ro.RiskID,
|
"risk_id": ro.RiskID,
|
||||||
"obligation_id": ro.ObligationID,
|
"obligation_id": ro.ObligationID,
|
||||||
|
"organization_id": ro.OrganizationID,
|
||||||
"tenant_id": scope.GetTenantID(),
|
"tenant_id": scope.GetTenantID(),
|
||||||
"created_at": ro.CreatedAt,
|
"created_at": ro.CreatedAt,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -21,9 +21,9 @@ import (
|
|||||||
"maps"
|
"maps"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"go.probo.inc/probo/pkg/gid"
|
|
||||||
"github.com/jackc/pgx/v5"
|
"github.com/jackc/pgx/v5"
|
||||||
"go.gearno.de/kit/pg"
|
"go.gearno.de/kit/pg"
|
||||||
|
"go.probo.inc/probo/pkg/gid"
|
||||||
)
|
)
|
||||||
|
|
||||||
type (
|
type (
|
||||||
|
|||||||
@@ -22,16 +22,17 @@ import (
|
|||||||
"maps"
|
"maps"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"go.probo.inc/probo/pkg/gid"
|
|
||||||
"go.probo.inc/probo/pkg/page"
|
|
||||||
"github.com/jackc/pgx/v5"
|
"github.com/jackc/pgx/v5"
|
||||||
"github.com/jackc/pgx/v5/pgconn"
|
"github.com/jackc/pgx/v5/pgconn"
|
||||||
"go.gearno.de/kit/pg"
|
"go.gearno.de/kit/pg"
|
||||||
|
"go.probo.inc/probo/pkg/gid"
|
||||||
|
"go.probo.inc/probo/pkg/page"
|
||||||
)
|
)
|
||||||
|
|
||||||
type (
|
type (
|
||||||
TrustCenterAccess struct {
|
TrustCenterAccess struct {
|
||||||
ID gid.GID `db:"id"`
|
ID gid.GID `db:"id"`
|
||||||
|
OrganizationID gid.GID `db:"organization_id"`
|
||||||
TenantID gid.TenantID `db:"tenant_id"`
|
TenantID gid.TenantID `db:"tenant_id"`
|
||||||
TrustCenterID gid.GID `db:"trust_center_id"`
|
TrustCenterID gid.GID `db:"trust_center_id"`
|
||||||
Email string `db:"email"`
|
Email string `db:"email"`
|
||||||
@@ -82,6 +83,7 @@ func (tca *TrustCenterAccess) LoadByID(
|
|||||||
q := `
|
q := `
|
||||||
SELECT
|
SELECT
|
||||||
id,
|
id,
|
||||||
|
organization_id,
|
||||||
tenant_id,
|
tenant_id,
|
||||||
trust_center_id,
|
trust_center_id,
|
||||||
email,
|
email,
|
||||||
@@ -135,6 +137,7 @@ func (tca *TrustCenterAccess) LoadByTrustCenterIDAndEmail(
|
|||||||
q := `
|
q := `
|
||||||
SELECT
|
SELECT
|
||||||
id,
|
id,
|
||||||
|
organization_id,
|
||||||
tenant_id,
|
tenant_id,
|
||||||
trust_center_id,
|
trust_center_id,
|
||||||
email,
|
email,
|
||||||
@@ -191,6 +194,7 @@ func (tca *TrustCenterAccess) Insert(
|
|||||||
INSERT INTO trust_center_accesses (
|
INSERT INTO trust_center_accesses (
|
||||||
id,
|
id,
|
||||||
tenant_id,
|
tenant_id,
|
||||||
|
organization_id,
|
||||||
trust_center_id,
|
trust_center_id,
|
||||||
email,
|
email,
|
||||||
name,
|
name,
|
||||||
@@ -201,6 +205,7 @@ INSERT INTO trust_center_accesses (
|
|||||||
) VALUES (
|
) VALUES (
|
||||||
@id,
|
@id,
|
||||||
@tenant_id,
|
@tenant_id,
|
||||||
|
@organization_id,
|
||||||
@trust_center_id,
|
@trust_center_id,
|
||||||
@email,
|
@email,
|
||||||
@name,
|
@name,
|
||||||
@@ -214,6 +219,7 @@ INSERT INTO trust_center_accesses (
|
|||||||
args := pgx.StrictNamedArgs{
|
args := pgx.StrictNamedArgs{
|
||||||
"id": tca.ID,
|
"id": tca.ID,
|
||||||
"tenant_id": tca.TenantID,
|
"tenant_id": tca.TenantID,
|
||||||
|
"organization_id": tca.OrganizationID,
|
||||||
"trust_center_id": tca.TrustCenterID,
|
"trust_center_id": tca.TrustCenterID,
|
||||||
"email": tca.Email,
|
"email": tca.Email,
|
||||||
"name": tca.Name,
|
"name": tca.Name,
|
||||||
@@ -317,6 +323,7 @@ func (tcas *TrustCenterAccesses) LoadByTrustCenterID(
|
|||||||
q := `
|
q := `
|
||||||
SELECT
|
SELECT
|
||||||
id,
|
id,
|
||||||
|
organization_id,
|
||||||
tenant_id,
|
tenant_id,
|
||||||
trust_center_id,
|
trust_center_id,
|
||||||
email,
|
email,
|
||||||
|
|||||||
@@ -21,16 +21,17 @@ import (
|
|||||||
"maps"
|
"maps"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"go.probo.inc/probo/pkg/gid"
|
|
||||||
"go.probo.inc/probo/pkg/page"
|
|
||||||
"github.com/jackc/pgx/v5"
|
"github.com/jackc/pgx/v5"
|
||||||
"github.com/jackc/pgx/v5/pgconn"
|
"github.com/jackc/pgx/v5/pgconn"
|
||||||
"go.gearno.de/kit/pg"
|
"go.gearno.de/kit/pg"
|
||||||
|
"go.probo.inc/probo/pkg/gid"
|
||||||
|
"go.probo.inc/probo/pkg/page"
|
||||||
)
|
)
|
||||||
|
|
||||||
type (
|
type (
|
||||||
TrustCenterDocumentAccess struct {
|
TrustCenterDocumentAccess struct {
|
||||||
ID gid.GID `db:"id"`
|
ID gid.GID `db:"id"`
|
||||||
|
OrganizationID gid.GID `db:"organization_id"`
|
||||||
TrustCenterAccessID gid.GID `db:"trust_center_access_id"`
|
TrustCenterAccessID gid.GID `db:"trust_center_access_id"`
|
||||||
DocumentID *gid.GID `db:"document_id"`
|
DocumentID *gid.GID `db:"document_id"`
|
||||||
ReportID *gid.GID `db:"report_id"`
|
ReportID *gid.GID `db:"report_id"`
|
||||||
@@ -78,6 +79,7 @@ func (tcda *TrustCenterDocumentAccess) LoadByID(
|
|||||||
q := `
|
q := `
|
||||||
SELECT
|
SELECT
|
||||||
id,
|
id,
|
||||||
|
organization_id,
|
||||||
trust_center_access_id,
|
trust_center_access_id,
|
||||||
document_id,
|
document_id,
|
||||||
report_id,
|
report_id,
|
||||||
@@ -127,6 +129,7 @@ func (tcda *TrustCenterDocumentAccess) LoadByTrustCenterAccessIDAndDocumentID(
|
|||||||
q := `
|
q := `
|
||||||
SELECT
|
SELECT
|
||||||
id,
|
id,
|
||||||
|
organization_id,
|
||||||
trust_center_access_id,
|
trust_center_access_id,
|
||||||
document_id,
|
document_id,
|
||||||
report_id,
|
report_id,
|
||||||
@@ -177,6 +180,7 @@ func (tcda *TrustCenterDocumentAccess) LoadByTrustCenterAccessIDAndReportID(
|
|||||||
q := `
|
q := `
|
||||||
SELECT
|
SELECT
|
||||||
id,
|
id,
|
||||||
|
organization_id,
|
||||||
trust_center_access_id,
|
trust_center_access_id,
|
||||||
document_id,
|
document_id,
|
||||||
report_id,
|
report_id,
|
||||||
@@ -226,6 +230,7 @@ func (tcda *TrustCenterDocumentAccess) Insert(
|
|||||||
INSERT INTO trust_center_document_accesses (
|
INSERT INTO trust_center_document_accesses (
|
||||||
id,
|
id,
|
||||||
tenant_id,
|
tenant_id,
|
||||||
|
organization_id,
|
||||||
trust_center_access_id,
|
trust_center_access_id,
|
||||||
document_id,
|
document_id,
|
||||||
report_id,
|
report_id,
|
||||||
@@ -237,6 +242,7 @@ INSERT INTO trust_center_document_accesses (
|
|||||||
) VALUES (
|
) VALUES (
|
||||||
@id,
|
@id,
|
||||||
@tenant_id,
|
@tenant_id,
|
||||||
|
@organization_id,
|
||||||
@trust_center_access_id,
|
@trust_center_access_id,
|
||||||
@document_id,
|
@document_id,
|
||||||
@report_id,
|
@report_id,
|
||||||
@@ -251,6 +257,7 @@ INSERT INTO trust_center_document_accesses (
|
|||||||
args := pgx.StrictNamedArgs{
|
args := pgx.StrictNamedArgs{
|
||||||
"id": tcda.ID,
|
"id": tcda.ID,
|
||||||
"tenant_id": scope.GetTenantID(),
|
"tenant_id": scope.GetTenantID(),
|
||||||
|
"organization_id": tcda.OrganizationID,
|
||||||
"trust_center_access_id": tcda.TrustCenterAccessID,
|
"trust_center_access_id": tcda.TrustCenterAccessID,
|
||||||
"document_id": tcda.DocumentID,
|
"document_id": tcda.DocumentID,
|
||||||
"report_id": tcda.ReportID,
|
"report_id": tcda.ReportID,
|
||||||
@@ -512,6 +519,7 @@ final_items AS (
|
|||||||
SELECT
|
SELECT
|
||||||
COALESCE(tcda.id, ai.item_id) AS id,
|
COALESCE(tcda.id, ai.item_id) AS id,
|
||||||
tcda.tenant_id,
|
tcda.tenant_id,
|
||||||
|
(SELECT organization_id FROM organization) AS organization_id,
|
||||||
@trust_center_access_id AS trust_center_access_id,
|
@trust_center_access_id AS trust_center_access_id,
|
||||||
ai.document_id,
|
ai.document_id,
|
||||||
ai.report_id,
|
ai.report_id,
|
||||||
@@ -532,6 +540,7 @@ final_items AS (
|
|||||||
)
|
)
|
||||||
SELECT
|
SELECT
|
||||||
id,
|
id,
|
||||||
|
organization_id,
|
||||||
trust_center_access_id,
|
trust_center_access_id,
|
||||||
document_id,
|
document_id,
|
||||||
report_id,
|
report_id,
|
||||||
@@ -576,6 +585,7 @@ func (tcdas *TrustCenterDocumentAccesses) LoadAllByTrustCenterAccessID(
|
|||||||
q := `
|
q := `
|
||||||
SELECT
|
SELECT
|
||||||
id,
|
id,
|
||||||
|
organization_id,
|
||||||
trust_center_access_id,
|
trust_center_access_id,
|
||||||
document_id,
|
document_id,
|
||||||
report_id,
|
report_id,
|
||||||
@@ -717,6 +727,7 @@ func (tcdas TrustCenterDocumentAccesses) BulkInsertDocumentAccesses(
|
|||||||
conn pg.Conn,
|
conn pg.Conn,
|
||||||
scope Scoper,
|
scope Scoper,
|
||||||
trustCenterAccessID gid.GID,
|
trustCenterAccessID gid.GID,
|
||||||
|
organizationID gid.GID,
|
||||||
documentIDs []gid.GID,
|
documentIDs []gid.GID,
|
||||||
requested bool,
|
requested bool,
|
||||||
createdAt time.Time,
|
createdAt time.Time,
|
||||||
@@ -730,6 +741,7 @@ WITH document_access_data AS (
|
|||||||
SELECT
|
SELECT
|
||||||
generate_gid(decode_base64_unpadded(@tenant_id), @trust_center_document_access_entity_type) AS id,
|
generate_gid(decode_base64_unpadded(@tenant_id), @trust_center_document_access_entity_type) AS id,
|
||||||
@tenant_id AS tenant_id,
|
@tenant_id AS tenant_id,
|
||||||
|
@organization_id AS organization_id,
|
||||||
@trust_center_access_id AS trust_center_access_id,
|
@trust_center_access_id AS trust_center_access_id,
|
||||||
unnest(@document_ids::text[]) AS document_id,
|
unnest(@document_ids::text[]) AS document_id,
|
||||||
null::text AS report_id,
|
null::text AS report_id,
|
||||||
@@ -740,7 +752,7 @@ WITH document_access_data AS (
|
|||||||
@updated_at::timestamptz AS updated_at
|
@updated_at::timestamptz AS updated_at
|
||||||
)
|
)
|
||||||
INSERT INTO trust_center_document_accesses (
|
INSERT INTO trust_center_document_accesses (
|
||||||
id, tenant_id, trust_center_access_id, document_id, report_id, trust_center_file_id, active, requested, created_at, updated_at
|
id, tenant_id, organization_id, trust_center_access_id, document_id, report_id, trust_center_file_id, active, requested, created_at, updated_at
|
||||||
)
|
)
|
||||||
SELECT * FROM document_access_data
|
SELECT * FROM document_access_data
|
||||||
ON CONFLICT DO NOTHING
|
ON CONFLICT DO NOTHING
|
||||||
@@ -748,6 +760,7 @@ ON CONFLICT DO NOTHING
|
|||||||
|
|
||||||
args := pgx.StrictNamedArgs{
|
args := pgx.StrictNamedArgs{
|
||||||
"tenant_id": scope.GetTenantID(),
|
"tenant_id": scope.GetTenantID(),
|
||||||
|
"organization_id": organizationID,
|
||||||
"trust_center_access_id": trustCenterAccessID,
|
"trust_center_access_id": trustCenterAccessID,
|
||||||
"document_ids": documentIDs,
|
"document_ids": documentIDs,
|
||||||
"trust_center_document_access_entity_type": TrustCenterDocumentAccessEntityType,
|
"trust_center_document_access_entity_type": TrustCenterDocumentAccessEntityType,
|
||||||
@@ -768,6 +781,7 @@ func (tcdas TrustCenterDocumentAccesses) BulkInsertReportAccesses(
|
|||||||
conn pg.Conn,
|
conn pg.Conn,
|
||||||
scope Scoper,
|
scope Scoper,
|
||||||
trustCenterAccessID gid.GID,
|
trustCenterAccessID gid.GID,
|
||||||
|
organizationID gid.GID,
|
||||||
reportIDs []gid.GID,
|
reportIDs []gid.GID,
|
||||||
requested bool,
|
requested bool,
|
||||||
createdAt time.Time,
|
createdAt time.Time,
|
||||||
@@ -781,6 +795,7 @@ WITH report_access_data AS (
|
|||||||
SELECT
|
SELECT
|
||||||
generate_gid(decode_base64_unpadded(@tenant_id), @trust_center_document_access_entity_type) AS id,
|
generate_gid(decode_base64_unpadded(@tenant_id), @trust_center_document_access_entity_type) AS id,
|
||||||
@tenant_id AS tenant_id,
|
@tenant_id AS tenant_id,
|
||||||
|
@organization_id AS organization_id,
|
||||||
@trust_center_access_id AS trust_center_access_id,
|
@trust_center_access_id AS trust_center_access_id,
|
||||||
null::text AS document_id,
|
null::text AS document_id,
|
||||||
unnest(@report_ids::text[]) AS report_id,
|
unnest(@report_ids::text[]) AS report_id,
|
||||||
@@ -791,7 +806,7 @@ WITH report_access_data AS (
|
|||||||
@updated_at::timestamptz AS updated_at
|
@updated_at::timestamptz AS updated_at
|
||||||
)
|
)
|
||||||
INSERT INTO trust_center_document_accesses (
|
INSERT INTO trust_center_document_accesses (
|
||||||
id, tenant_id, trust_center_access_id, document_id, report_id, trust_center_file_id, active, requested, created_at, updated_at
|
id, tenant_id, organization_id, trust_center_access_id, document_id, report_id, trust_center_file_id, active, requested, created_at, updated_at
|
||||||
)
|
)
|
||||||
SELECT * FROM report_access_data
|
SELECT * FROM report_access_data
|
||||||
ON CONFLICT DO NOTHING
|
ON CONFLICT DO NOTHING
|
||||||
@@ -799,6 +814,7 @@ ON CONFLICT DO NOTHING
|
|||||||
|
|
||||||
args := pgx.StrictNamedArgs{
|
args := pgx.StrictNamedArgs{
|
||||||
"tenant_id": scope.GetTenantID(),
|
"tenant_id": scope.GetTenantID(),
|
||||||
|
"organization_id": organizationID,
|
||||||
"trust_center_document_access_entity_type": TrustCenterDocumentAccessEntityType,
|
"trust_center_document_access_entity_type": TrustCenterDocumentAccessEntityType,
|
||||||
"trust_center_access_id": trustCenterAccessID,
|
"trust_center_access_id": trustCenterAccessID,
|
||||||
"report_ids": reportIDs,
|
"report_ids": reportIDs,
|
||||||
@@ -903,6 +919,7 @@ func (tcdas TrustCenterDocumentAccesses) BulkInsertTrustCenterFileAccesses(
|
|||||||
conn pg.Conn,
|
conn pg.Conn,
|
||||||
scope Scoper,
|
scope Scoper,
|
||||||
trustCenterAccessID gid.GID,
|
trustCenterAccessID gid.GID,
|
||||||
|
organizationID gid.GID,
|
||||||
trustCenterFileIDs []gid.GID,
|
trustCenterFileIDs []gid.GID,
|
||||||
requested bool,
|
requested bool,
|
||||||
createdAt time.Time,
|
createdAt time.Time,
|
||||||
@@ -912,6 +929,7 @@ WITH trust_center_file_access_data AS (
|
|||||||
SELECT
|
SELECT
|
||||||
generate_gid(decode_base64_unpadded(@tenant_id), @trust_center_document_access_entity_type) AS id,
|
generate_gid(decode_base64_unpadded(@tenant_id), @trust_center_document_access_entity_type) AS id,
|
||||||
@tenant_id AS tenant_id,
|
@tenant_id AS tenant_id,
|
||||||
|
@organization_id AS organization_id,
|
||||||
@trust_center_access_id AS trust_center_access_id,
|
@trust_center_access_id AS trust_center_access_id,
|
||||||
null::text AS document_id,
|
null::text AS document_id,
|
||||||
null::text AS report_id,
|
null::text AS report_id,
|
||||||
@@ -922,7 +940,7 @@ WITH trust_center_file_access_data AS (
|
|||||||
@updated_at::timestamptz AS updated_at
|
@updated_at::timestamptz AS updated_at
|
||||||
)
|
)
|
||||||
INSERT INTO trust_center_document_accesses (
|
INSERT INTO trust_center_document_accesses (
|
||||||
id, tenant_id, trust_center_access_id, document_id, report_id, trust_center_file_id, active, requested, created_at, updated_at
|
id, tenant_id, organization_id, trust_center_access_id, document_id, report_id, trust_center_file_id, active, requested, created_at, updated_at
|
||||||
)
|
)
|
||||||
SELECT * FROM trust_center_file_access_data
|
SELECT * FROM trust_center_file_access_data
|
||||||
ON CONFLICT DO NOTHING
|
ON CONFLICT DO NOTHING
|
||||||
@@ -930,6 +948,7 @@ ON CONFLICT DO NOTHING
|
|||||||
|
|
||||||
args := pgx.StrictNamedArgs{
|
args := pgx.StrictNamedArgs{
|
||||||
"tenant_id": scope.GetTenantID(),
|
"tenant_id": scope.GetTenantID(),
|
||||||
|
"organization_id": organizationID,
|
||||||
"trust_center_document_access_entity_type": TrustCenterDocumentAccessEntityType,
|
"trust_center_document_access_entity_type": TrustCenterDocumentAccessEntityType,
|
||||||
"trust_center_access_id": trustCenterAccessID,
|
"trust_center_access_id": trustCenterAccessID,
|
||||||
"trust_center_file_ids": trustCenterFileIDs,
|
"trust_center_file_ids": trustCenterFileIDs,
|
||||||
|
|||||||
@@ -31,6 +31,7 @@ import (
|
|||||||
type (
|
type (
|
||||||
TrustCenterReference struct {
|
TrustCenterReference struct {
|
||||||
ID gid.GID `db:"id"`
|
ID gid.GID `db:"id"`
|
||||||
|
OrganizationID gid.GID `db:"organization_id"`
|
||||||
TrustCenterID gid.GID `db:"trust_center_id"`
|
TrustCenterID gid.GID `db:"trust_center_id"`
|
||||||
Name string `db:"name"`
|
Name string `db:"name"`
|
||||||
Description *string `db:"description"`
|
Description *string `db:"description"`
|
||||||
@@ -83,6 +84,7 @@ func (t *TrustCenterReference) LoadByID(
|
|||||||
q := `
|
q := `
|
||||||
SELECT
|
SELECT
|
||||||
id,
|
id,
|
||||||
|
organization_id,
|
||||||
trust_center_id,
|
trust_center_id,
|
||||||
name,
|
name,
|
||||||
description,
|
description,
|
||||||
@@ -128,6 +130,7 @@ INSERT INTO
|
|||||||
trust_center_references (
|
trust_center_references (
|
||||||
tenant_id,
|
tenant_id,
|
||||||
id,
|
id,
|
||||||
|
organization_id,
|
||||||
trust_center_id,
|
trust_center_id,
|
||||||
name,
|
name,
|
||||||
description,
|
description,
|
||||||
@@ -140,6 +143,7 @@ INSERT INTO
|
|||||||
VALUES (
|
VALUES (
|
||||||
@tenant_id,
|
@tenant_id,
|
||||||
@id,
|
@id,
|
||||||
|
@organization_id,
|
||||||
@trust_center_id,
|
@trust_center_id,
|
||||||
@name,
|
@name,
|
||||||
@description,
|
@description,
|
||||||
@@ -155,6 +159,7 @@ RETURNING rank;
|
|||||||
args := pgx.StrictNamedArgs{
|
args := pgx.StrictNamedArgs{
|
||||||
"tenant_id": scope.GetTenantID(),
|
"tenant_id": scope.GetTenantID(),
|
||||||
"id": t.ID,
|
"id": t.ID,
|
||||||
|
"organization_id": t.OrganizationID,
|
||||||
"trust_center_id": t.TrustCenterID,
|
"trust_center_id": t.TrustCenterID,
|
||||||
"name": t.Name,
|
"name": t.Name,
|
||||||
"description": t.Description,
|
"description": t.Description,
|
||||||
@@ -308,6 +313,7 @@ func (t *TrustCenterReferences) LoadByTrustCenterID(
|
|||||||
q := `
|
q := `
|
||||||
SELECT
|
SELECT
|
||||||
id,
|
id,
|
||||||
|
organization_id,
|
||||||
trust_center_id,
|
trust_center_id,
|
||||||
name,
|
name,
|
||||||
description,
|
description,
|
||||||
|
|||||||
@@ -28,16 +28,17 @@ import (
|
|||||||
|
|
||||||
type (
|
type (
|
||||||
VendorComplianceReport struct {
|
VendorComplianceReport struct {
|
||||||
ID gid.GID
|
ID gid.GID `db:"id"`
|
||||||
VendorID gid.GID
|
OrganizationID gid.GID `db:"organization_id"`
|
||||||
ReportDate time.Time
|
VendorID gid.GID `db:"vendor_id"`
|
||||||
ValidUntil *time.Time
|
ReportDate time.Time `db:"report_date"`
|
||||||
ReportName string
|
ValidUntil *time.Time `db:"valid_until"`
|
||||||
ReportFileId *gid.GID
|
ReportName string `db:"report_name"`
|
||||||
SnapshotID *gid.GID
|
ReportFileId *gid.GID `db:"report_file_id"`
|
||||||
SourceID *gid.GID
|
SnapshotID *gid.GID `db:"snapshot_id"`
|
||||||
CreatedAt time.Time
|
SourceID *gid.GID `db:"source_id"`
|
||||||
UpdatedAt time.Time
|
CreatedAt time.Time `db:"created_at"`
|
||||||
|
UpdatedAt time.Time `db:"updated_at"`
|
||||||
}
|
}
|
||||||
|
|
||||||
VendorComplianceReports []*VendorComplianceReport
|
VendorComplianceReports []*VendorComplianceReport
|
||||||
@@ -157,6 +158,7 @@ func (vcr *VendorComplianceReport) Insert(
|
|||||||
INSERT INTO
|
INSERT INTO
|
||||||
vendor_compliance_reports (
|
vendor_compliance_reports (
|
||||||
id,
|
id,
|
||||||
|
organization_id,
|
||||||
tenant_id,
|
tenant_id,
|
||||||
vendor_id,
|
vendor_id,
|
||||||
report_date,
|
report_date,
|
||||||
@@ -168,6 +170,7 @@ INSERT INTO
|
|||||||
)
|
)
|
||||||
VALUES (
|
VALUES (
|
||||||
@id,
|
@id,
|
||||||
|
@organization_id,
|
||||||
@tenant_id,
|
@tenant_id,
|
||||||
@vendor_id,
|
@vendor_id,
|
||||||
@report_date,
|
@report_date,
|
||||||
@@ -180,6 +183,7 @@ VALUES (
|
|||||||
`
|
`
|
||||||
args := pgx.NamedArgs{
|
args := pgx.NamedArgs{
|
||||||
"id": vcr.ID,
|
"id": vcr.ID,
|
||||||
|
"organization_id": vcr.OrganizationID,
|
||||||
"tenant_id": scope.GetTenantID(),
|
"tenant_id": scope.GetTenantID(),
|
||||||
"vendor_id": vcr.VendorID,
|
"vendor_id": vcr.VendorID,
|
||||||
"report_date": vcr.ReportDate,
|
"report_date": vcr.ReportDate,
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user