diff --git a/frontend/src/components/notifications/Notifications.tsx b/frontend/src/components/notifications/Notifications.tsx index befe79e4f..23b4eebaa 100644 --- a/frontend/src/components/notifications/Notifications.tsx +++ b/frontend/src/components/notifications/Notifications.tsx @@ -4,13 +4,15 @@ import { Id, toast, ToastContainer, ToastOptions, TypeOptions } from "react-toas export type TNotification = { title?: string; text: ReactNode; + children?: ReactNode; }; -export const NotificationContent = ({ title, text }: TNotification) => { +export const NotificationContent = ({ title, text, children }: TNotification) => { return (
{title &&
{title}
} -
{text}
+
{text}
+ {children &&
{children}
}
); }; @@ -23,7 +25,13 @@ export const createNotification = ( position: "bottom-right", ...toastProps, theme: "dark", - type: myProps?.type || "info", + type: myProps?.type || "info" }); -export const NotificationContainer = () => ; +export const NotificationContainer = () => ( + +); diff --git a/frontend/src/context/ProjectPermissionContext/types.ts b/frontend/src/context/ProjectPermissionContext/types.ts index 307ef74af..818a5d0e6 100644 --- a/frontend/src/context/ProjectPermissionContext/types.ts +++ b/frontend/src/context/ProjectPermissionContext/types.ts @@ -33,6 +33,15 @@ export enum PermissionConditionOperators { $GLOB = "$glob" } +export const formatedConditionsOperatorNames: { [K in PermissionConditionOperators]: string } = { + [PermissionConditionOperators.$EQ]: "equal", + [PermissionConditionOperators.$IN]: "containing", + [PermissionConditionOperators.$ALL]: "contains all", + [PermissionConditionOperators.$NEQ]: "not equal", + [PermissionConditionOperators.$GLOB]: "glob matching", + [PermissionConditionOperators.$REGEX]: "regex" +}; + export type TPermissionConditionOperators = { [PermissionConditionOperators.$IN]: string[]; [PermissionConditionOperators.$ALL]: string[]; diff --git a/frontend/src/hooks/api/types.ts b/frontend/src/hooks/api/types.ts index 516a5d7cf..34120a15d 100644 --- a/frontend/src/hooks/api/types.ts +++ b/frontend/src/hooks/api/types.ts @@ -1,3 +1,4 @@ +import { PureAbility } from "@casl/ability"; import { ZodIssue } from "zod"; export type { TAccessApprovalPolicy } from "./accessApproval/types"; @@ -52,9 +53,14 @@ export type TApiErrors = | { error: ApiErrorTypes.ValidationError; message: ZodIssue[]; + statusCode: 401; + } + | { + error: ApiErrorTypes.ForbiddenError; + message: string; + details: PureAbility["rules"]; statusCode: 403; } - | { error: ApiErrorTypes.ForbiddenError; message: string; statusCode: 401 } | { statusCode: 400; message: string; diff --git a/frontend/src/reactQuery.tsx b/frontend/src/reactQuery.tsx index bf764d2d7..5083f9db3 100644 --- a/frontend/src/reactQuery.tsx +++ b/frontend/src/reactQuery.tsx @@ -3,6 +3,23 @@ import axios from "axios"; import { createNotification } from "@app/components/notifications"; +import { + Button, + Modal, + ModalContent, + ModalTrigger, + Table, + TableContainer, + TBody, + Td, + Th, + THead, + Tr +} from "./components/v2"; +import { + formatedConditionsOperatorNames, + PermissionConditionOperators +} from "./context/ProjectPermissionContext/types"; import { ApiErrorTypes, TApiErrors } from "./hooks/api/types"; // this is saved in react-query cache @@ -10,35 +27,142 @@ export const SIGNUP_TEMP_TOKEN_CACHE_KEY = ["infisical__signup-temp-token"]; export const MFA_TEMP_TOKEN_CACHE_KEY = ["infisical__mfa-temp-token"]; export const AUTH_TOKEN_CACHE_KEY = ["infisical__auth-token"]; +const camelCaseToSpaces = (input: string) => { + return input.replace(/([a-z])([A-Z])/g, "$1 $2"); +}; + export const queryClient = new QueryClient({ mutationCache: new MutationCache({ onError: (error) => { if (axios.isAxiosError(error)) { const serverResponse = error.response?.data as TApiErrors; if (serverResponse?.error === ApiErrorTypes.ValidationError) { - createNotification({ - title: "Validation Error", - type: "error", - text: ( -
- {serverResponse.message?.map(({ message, path }) => ( -
-
- Field {path.join(".")} {message.toLowerCase()} -
-
- ))} -
- ) - }); + createNotification( + { + title: "Validation Error", + type: "error", + text: "Please check the input and try again.", + children: ( + + + + + + + + + + + + + + + {serverResponse.message?.map(({ message, path }) => ( + + + + + ))} + +
FieldIssue
{path.join(".")}{message.toLowerCase()}
+
+
+
+ ) + }, + { closeOnClick: false } + ); return; } - if (serverResponse.statusCode === 401) { - createNotification({ - title: "Forbidden Access", - type: "error", - text: serverResponse.message - }); + if (serverResponse?.error === ApiErrorTypes.ForbiddenError) { + createNotification( + { + title: "Forbidden Access", + type: "error", + text: serverResponse.message, + children: serverResponse?.details?.length ? ( + + + + + +
+ {serverResponse.details?.map((el, index) => { + const hasConditions = Object.keys(el.conditions || {}).length; + return ( +
+
+ {el.inverted ? "Cannot" : "Can"}{" "} + {el.action.toString()}{" "} + {el.subject.toString()} {hasConditions && "with conditions"} +
+ {hasConditions && ( +
+ {Object.keys(el.conditions || {}).flatMap((field, fieldIndex) => { + const operators = ( + el.conditions as Record< + string, + | string + | { [K in PermissionConditionOperators]: string | string[] } + > + )[field]; + + const formattedFieldName = camelCaseToSpaces(field).toLowerCase(); + if (typeof operators === "string") { + return ( +
+ {formattedFieldName} equal{" "} + {operators} +
+ ); + } + + return Object.keys(operators).map((operator, operatorIndex) => ( +
+ {formattedFieldName}{" "} + { + formatedConditionsOperatorNames[ + operator as PermissionConditionOperators + ] + }{" "} + + {operators[ + operator as PermissionConditionOperators + ].toString()} + +
+ )); + })} +
+ )} +
+ ); + })} +
+
+
+ ) : undefined + }, + { closeOnClick: false } + ); return; } createNotification({ title: "Bad Request", type: "error", text: serverResponse.message }); diff --git a/frontend/src/styles/globals.css b/frontend/src/styles/globals.css index 0ccfbc466..916244a5a 100644 --- a/frontend/src/styles/globals.css +++ b/frontend/src/styles/globals.css @@ -13,6 +13,14 @@ html { @apply rounded-md; } +.Toastify__toast-body { + @apply items-start; +} + +.Toastify__toast-icon { + @apply w-4 pt-1; +} + .rdp-day, .rdp-nav_button { @apply rounded-md hover:text-mineshaft-500;