feat(rbac): implemented granular blocking of actions based on permissions on org level ui

This commit is contained in:
Akhil Mohan
2023-09-08 21:21:43 +05:30
parent 34fb7be1c4
commit aac3168c80
41 changed files with 1992 additions and 1652 deletions
@@ -292,7 +292,7 @@ export const createOrganizationPortalSession = async (req: Request, res: Respons
const { permission } = await getUserOrgPermissions(req.user._id, organizationId);
ForbiddenError.from(permission).throwUnlessCan(
GeneralPermissionActions.Create,
GeneralPermissionActions.Edit,
OrgPermissionSubjects.Billing
);
@@ -0,0 +1,50 @@
import { FunctionComponent, ReactNode } from "react";
import { BoundCanProps, Can } from "@casl/react";
import {
OrgPermissionSubjects,
OrgWorkspacePermissionActions,
TOrgPermission,
useOrgPermission
} from "@app/context/OrgPermissionContext";
import { Tooltip } from "../v2";
type Props = {
label?: ReactNode;
} & BoundCanProps<TOrgPermission>;
export const OrgPermissionCan: FunctionComponent<Props> = ({
label = "Permission Denied. Kindly contact your org admin",
children,
passThrough = true,
...props
}) => {
const permission = useOrgPermission();
return (
<Can
{...props}
passThrough={passThrough}
ability={props?.ability || permission}
I={OrgWorkspacePermissionActions.Read}
a={OrgPermissionSubjects.Sso}
>
{(isAllowed, ability) => {
// akhilmhdh: This is set as type due to error in casl react type.
const finalChild =
typeof children === "function"
? children(isAllowed, ability as TOrgPermission)
: children;
if (!isAllowed && passThrough) {
return <Tooltip content={label}>{finalChild}</Tooltip>;
}
if (!isAllowed) return null;
return finalChild;
}}
</Can>
);
};
@@ -0,0 +1 @@
export { OrgPermissionCan } from "./OrgPermissionCan";
@@ -3,13 +3,13 @@ import { createContext, ReactNode, useContext } from "react";
import { useGetUserOrgPermissions } from "@app/hooks/api";
import { useOrganization } from "../OrganizationContext";
import { TPermission } from "./types";
import { TOrgPermission } from "./types";
type Props = {
children: ReactNode;
};
const PermissionContext = createContext<null | TPermission>(null);
const OrgPermissionContext = createContext<null | TOrgPermission>(null);
export const OrgPermissionProvider = ({ children }: Props): JSX.Element => {
const { currentOrg } = useOrganization();
@@ -37,11 +37,13 @@ export const OrgPermissionProvider = ({ children }: Props): JSX.Element => {
);
}
return <PermissionContext.Provider value={permission}>{children}</PermissionContext.Provider>;
return (
<OrgPermissionContext.Provider value={permission}>{children}</OrgPermissionContext.Provider>
);
};
export const useOrgPermission = () => {
const ctx = useContext(PermissionContext);
const ctx = useContext(OrgPermissionContext);
if (!ctx) {
throw new Error("useOrgPermission to be used within <OrgPermissionProvider>");
}
@@ -1 +1,7 @@
export { OrgPermissionProvider, useOrgPermission } from "./OrgPermissionContext";
export type { TOrgPermission } from "./types";
export {
OrgGeneralPermissionActions,
OrgPermissionSubjects,
OrgWorkspacePermissionActions
} from "./types";
@@ -1,13 +1,13 @@
import { MongoAbility } from "@casl/ability";
export enum GeneralPermissionActions {
export enum OrgGeneralPermissionActions {
Read = "read",
Create = "create",
Edit = "edit",
Delete = "delete"
}
export enum WorkspacePermissionActions {
export enum OrgWorkspacePermissionActions {
Read = "read",
Create = "create"
}
@@ -24,13 +24,13 @@ export enum OrgPermissionSubjects {
}
export type OrgPermissionSet =
| [WorkspacePermissionActions, OrgPermissionSubjects.Workspace]
| [GeneralPermissionActions, OrgPermissionSubjects.Role]
| [GeneralPermissionActions, OrgPermissionSubjects.Member]
| [GeneralPermissionActions, OrgPermissionSubjects.Settings]
| [GeneralPermissionActions, OrgPermissionSubjects.IncidentAccount]
| [GeneralPermissionActions, OrgPermissionSubjects.Sso]
| [GeneralPermissionActions, OrgPermissionSubjects.SecretScanning]
| [GeneralPermissionActions, OrgPermissionSubjects.Billing];
| [OrgWorkspacePermissionActions, OrgPermissionSubjects.Workspace]
| [OrgGeneralPermissionActions, OrgPermissionSubjects.Role]
| [OrgGeneralPermissionActions, OrgPermissionSubjects.Member]
| [OrgGeneralPermissionActions, OrgPermissionSubjects.Settings]
| [OrgGeneralPermissionActions, OrgPermissionSubjects.IncidentAccount]
| [OrgGeneralPermissionActions, OrgPermissionSubjects.Sso]
| [OrgGeneralPermissionActions, OrgPermissionSubjects.SecretScanning]
| [OrgGeneralPermissionActions, OrgPermissionSubjects.Billing];
export type TPermission = MongoAbility<OrgPermissionSet>;
export type TOrgPermission = MongoAbility<OrgPermissionSet>;
+7 -1
View File
@@ -1,6 +1,12 @@
export { AuthProvider } from "./AuthContext";
export { OrgProvider, useOrganization } from "./OrganizationContext";
export { OrgPermissionProvider,useOrgPermission } from "./OrgPermissionContext";
export type { TOrgPermission } from "./OrgPermissionContext";
export {
OrgGeneralPermissionActions,
OrgPermissionSubjects,
OrgWorkspacePermissionActions
} from "./OrgPermissionContext";
export { OrgPermissionProvider, useOrgPermission } from "./OrgPermissionContext";
export { SubscriptionProvider, useSubscription } from "./SubscriptionContext";
export { UserProvider, useUser } from "./UserContext";
export { useWorkspace, WorkspaceProvider } from "./WorkspaceContext";
+1
View File
@@ -0,0 +1 @@
export { withPermission } from "./withPermission";
@@ -0,0 +1 @@
export { withPermission } from "./withPermission";
@@ -0,0 +1,62 @@
import { ComponentType } from "react";
import { Abilities, AbilityTuple, Generics, SubjectType } from "@casl/ability";
import { faLock } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { twMerge } from "tailwind-merge";
import { TOrgPermission, useOrgPermission } from "@app/context";
type Props<T extends Abilities> = (T extends AbilityTuple
? {
action: T[0];
subject: Extract<T[1], SubjectType>;
}
: {
action: string;
subject: string;
}) & { className?: string; containerClassName?: string };
export const withPermission = <T extends {}, J extends TOrgPermission>(
Component: ComponentType<T>,
{ action, subject, className, containerClassName }: Props<Generics<J>["abilities"]>
) => {
const HOC = (hocProps: T) => {
const permission = useOrgPermission();
// akhilmhdh: Set as any due to casl/react ts type bug
// REASON: casl due to its type checking can't seem to union even if union intersection is applied
if (permission.cannot(action as any, subject)) {
return (
<div
className={twMerge(
"container h-full mx-auto flex justify-center items-center",
containerClassName
)}
>
<div
className={twMerge(
"rounded-md bg-mineshaft-800 text-bunker-300 p-16 flex space-x-12 items-end",
className
)}
>
<div>
<FontAwesomeIcon icon={faLock} size="6x" />
</div>
<div>
<div className="text-4xl font-medium mb-2">Permission Denied</div>
<div className="text-sm">
You do not have permission to this page. <br /> Kindly contact your organization
administrator
</div>
</div>
</div>
</div>
);
}
return <Component {...hocProps} />;
};
HOC.displayName = "WithPermission";
return HOC;
};
+10 -3
View File
@@ -1,9 +1,12 @@
import { useTranslation } from "react-i18next";
import Head from "next/head";
import { OrgGeneralPermissionActions, OrgPermissionSubjects, TOrgPermission } from "@app/context";
import { withPermission } from "@app/hoc";
import { BillingSettingsPage } from "@app/views/Settings/BillingSettingsPage";
export default function SettingsBilling() {
const SettingsBilling = withPermission<{}, TOrgPermission>(
() => {
const { t } = useTranslation();
return (
@@ -16,6 +19,10 @@ export default function SettingsBilling() {
<BillingSettingsPage />
</div>
);
}
},
{ action: OrgGeneralPermissionActions.Delete, subject: OrgPermissionSubjects.Billing }
);
SettingsBilling.requireAuth = true;
Object.assign(SettingsBilling, { requireAuth: true });
export default SettingsBilling;
+42 -10
View File
@@ -31,6 +31,7 @@ import * as Tabs from "@radix-ui/react-tabs";
import * as yup from "yup";
import { useNotificationContext } from "@app/components/context/Notifications/NotificationProvider";
import { OrgPermissionCan } from "@app/components/permissions";
import onboardingCheck from "@app/components/utilities/checks/OnboardingCheck";
import {
Button,
@@ -42,9 +43,22 @@ import {
Skeleton,
UpgradePlanModal
} from "@app/components/v2";
import { useSubscription, useUser, useWorkspace } from "@app/context";
import { fetchOrgUsers, useAddUserToWs, useCreateWorkspace, useRegisterUserAction,useUploadWsKey } from "@app/hooks/api";
import { useFetchServerStatus } from "@app/hooks/api/serverDetails";
import {
OrgPermissionSubjects,
OrgWorkspacePermissionActions,
useSubscription,
useUser,
useWorkspace
} from "@app/context";
import { withPermission } from "@app/hoc";
import {
fetchOrgUsers,
useAddUserToWs,
useCreateWorkspace,
useRegisterUserAction,
useUploadWsKey
} from "@app/hooks/api";
import { usePopUp } from "@app/hooks/usePopUp";
import { encryptAssymmetric } from "../../../../components/utilities/cryptography/crypto";
@@ -301,9 +315,7 @@ const LearningItem = ({
tabIndex={0}
onClick={async () => {
if (userAction && userAction !== "first_time_secrets_pushed") {
await registerUserAction.mutateAsync(
userAction
);
await registerUserAction.mutateAsync(userAction);
}
}}
className={`group relative flex h-[5.5rem] w-full items-center justify-between overflow-hidden rounded-md border ${
@@ -446,7 +458,8 @@ type TAddProjectFormData = yup.InferType<typeof formSchema>;
// #TODO: Update all the workspaceIds
export default function Organization() {
const OrganizationPage = withPermission(
() => {
const { t } = useTranslation();
const router = useRouter();
@@ -576,7 +589,13 @@ export default function Organization() {
onChange={(e) => setSearchFilter(e.target.value)}
leftIcon={<FontAwesomeIcon icon={faMagnifyingGlass} />}
/>
<OrgPermissionCan
I={OrgWorkspacePermissionActions.Create}
an={OrgPermissionSubjects.Workspace}
>
{(isAllowed) => (
<Button
isDisabled={!isAllowed}
colorSchema="primary"
leftIcon={<FontAwesomeIcon icon={faPlus} />}
onClick={() => {
@@ -590,6 +609,8 @@ export default function Organization() {
>
Add New Project
</Button>
)}
</OrgPermissionCan>
</div>
<div className="mt-4 grid w-full grid-cols-1 gap-4 lg:grid-cols-2 xl:grid-cols-3 2xl:grid-cols-4">
{isWorkspaceLoading &&
@@ -722,7 +743,9 @@ export default function Organization() {
</div>
</div>
<div
className={`w-28 pr-4 text-right text-sm font-semibold ${false && "text-green"}`}
className={`w-28 pr-4 text-right text-sm font-semibold ${
false && "text-green"
}`}
>
About 2 min
</div>
@@ -759,7 +782,9 @@ export default function Organization() {
{feature.description}
</div>
<div className="flex w-full items-center">
<div className="text-[15px] font-light text-mineshaft-300">Setup time: 20 min</div>
<div className="text-[15px] font-light text-mineshaft-300">
Setup time: 20 min
</div>
<a
target="_blank"
rel="noopener noreferrer"
@@ -850,6 +875,13 @@ export default function Organization() {
{/* <DeleteUserDialog isOpen={isDeleteOpen} closeModal={closeDeleteModal} submitModal={deleteMembership} userIdToBeDeleted={userIdToBeDeleted}/> */}
</div>
);
}
},
{
action: OrgWorkspacePermissionActions.Read,
subject: OrgPermissionSubjects.Workspace
}
);
Organization.requireAuth = true;
Object.assign(OrganizationPage, { requireAuth: true });
export default OrganizationPage;
@@ -1,48 +1,60 @@
import { useEffect, useState } from "react";
import Head from "next/head";
import { useRouter } from "next/router"
import { useRouter } from "next/router";
import { OrgPermissionCan } from "@app/components/permissions";
import { Button } from "@app/components/v2";
import { OrgGeneralPermissionActions, OrgPermissionSubjects } from "@app/context";
import { withPermission } from "@app/hoc";
import { SecretScanningLogsTable } from "@app/views/SecretScanning/components";
import createNewIntegrationSession from "../../../api/secret-scanning/createSecretScanningSession";
import getInstallationStatus from "../../../api/secret-scanning/getInstallationStatus";
import linkGitAppInstallationWithOrganization from "../../../api/secret-scanning/linkGitAppInstallationWithOrganization";
export default function SecretScanning() {
const router = useRouter()
const queryParams = router.query
const [integrationEnabled, setIntegrationStatus] = useState(false)
const SecretScanning = withPermission(
() => {
const router = useRouter();
const queryParams = router.query;
const [integrationEnabled, setIntegrationStatus] = useState(false);
useEffect(()=>{
useEffect(() => {
const linkInstallation = async () => {
if (typeof queryParams.state === "string" && typeof queryParams.installation_id === "string"){
if (
typeof queryParams.state === "string" &&
typeof queryParams.installation_id === "string"
) {
try {
const isLinked = await linkGitAppInstallationWithOrganization(queryParams.installation_id as string, queryParams.state as string)
if (isLinked){
router.reload()
const isLinked = await linkGitAppInstallationWithOrganization(
queryParams.installation_id as string,
queryParams.state as string
);
if (isLinked) {
router.reload();
}
console.log("installation verification complete")
}catch (e){
console.log("app installation is stale, start new session", e)
}
console.log("installation verification complete");
} catch (e) {
console.log("app installation is stale, start new session", e);
}
}
};
const fetchInstallationStatus = async () => {
const status = await getInstallationStatus(String(localStorage.getItem("orgData.id")))
setIntegrationStatus(status)
}
const status = await getInstallationStatus(String(localStorage.getItem("orgData.id")));
setIntegrationStatus(status);
};
fetchInstallationStatus()
linkInstallation()
},[queryParams.state, queryParams.installation_id])
fetchInstallationStatus();
linkInstallation();
}, [queryParams.state, queryParams.installation_id]);
const generateNewIntegrationSession = async () => {
const session = await createNewIntegrationSession(String(localStorage.getItem("orgData.id")))
router.push(`https://github.com/apps/infisical-radar/installations/new?state=${session.sessionId}`)
}
const session = await createNewIntegrationSession(String(localStorage.getItem("orgData.id")));
router.push(
`https://github.com/apps/infisical-radar/installations/new?state=${session.sessionId}`
);
};
return (
<div>
@@ -54,28 +66,56 @@ export default function SecretScanning() {
<div className="flex justify-center bg-bunker-800 text-white w-full h-full">
<div className="max-w-7xl px-6 w-full">
<div className="mt-6 text-3xl font-semibold text-gray-200">Secret Scanning</div>
<div className="mb-6 text-lg text-mineshaft-300">Automatically monitor your GitHub activity and prevent secret leaks</div>
<div className="mb-6 text-lg text-mineshaft-300">
Automatically monitor your GitHub activity and prevent secret leaks
</div>
<div className="relative flex justify-between bg-mineshaft-800 border border-mineshaft-600 rounded-md p-6 mb-6">
<div className="flex flex-col items-start">
<div className="flex flex-row mb-1">Secret Scanning Status: {integrationEnabled ? <p className="text-green ml-1.5 font-semibold">Enabled</p> : <p className="text-red ml-1.5 font-semibold">Not enabled</p>}</div>
<div>{integrationEnabled ? <p className="text-mineshaft-300">Your GitHub organization is connected to Infisical, and is being continuously monitored for secret leaks.</p> : <p className="text-mineshaft-300">Connect your GitHub organization to Infisical.</p>}</div>
<div className="flex flex-row mb-1">
Secret Scanning Status:{" "}
{integrationEnabled ? (
<p className="text-green ml-1.5 font-semibold">Enabled</p>
) : (
<p className="text-red ml-1.5 font-semibold">Not enabled</p>
)}
</div>
<div>
{integrationEnabled ? (
<p className="text-mineshaft-300">
Your GitHub organization is connected to Infisical, and is being continuously
monitored for secret leaks.
</p>
) : (
<p className="text-mineshaft-300">
Connect your GitHub organization to Infisical.
</p>
)}
</div>
</div>
{integrationEnabled ? (
<div>
<div className="absolute right-[2.5rem] top-[2.5rem] animate-ping rounded-full h-6 w-6 bg-green flex items-center justify-center"/>
<div className="absolute right-[2.63rem] top-[2.63rem] animate-ping rounded-full h-5 w-5 bg-green flex items-center justify-center"/>
<div className="absolute right-[2.82rem] top-[2.82rem] animate-ping rounded-full h-3.5 w-3.5 bg-green flex items-center justify-center"/>
<div className="absolute right-[2.5rem] top-[2.5rem] animate-ping rounded-full h-6 w-6 bg-green flex items-center justify-center" />
<div className="absolute right-[2.63rem] top-[2.63rem] animate-ping rounded-full h-5 w-5 bg-green flex items-center justify-center" />
<div className="absolute right-[2.82rem] top-[2.82rem] animate-ping rounded-full h-3.5 w-3.5 bg-green flex items-center justify-center" />
</div>
) : (
<div className="flex items-center h-[3.25rem]">
<OrgPermissionCan
I={OrgGeneralPermissionActions.Create}
a={OrgPermissionSubjects.SecretScanning}
>
{(isAllowed) => (
<Button
variant="solid"
colorSchema="primary"
onClick={generateNewIntegrationSession}
className="py-2 h-min"
isDisabled={!isAllowed}
>
Integrate with GitHub
</Button>
)}
</OrgPermissionCan>
</div>
)}
</div>
@@ -84,6 +124,10 @@ export default function SecretScanning() {
</div>
</div>
);
}
},
{ action: OrgGeneralPermissionActions.Read, subject: OrgPermissionSubjects.SecretScanning }
);
SecretScanning.requireAuth = true;
Object.assign(SecretScanning, { requireAuth: true });
export default SecretScanning;
@@ -3,7 +3,8 @@ import { useTranslation } from "react-i18next";
import { motion } from "framer-motion";
import { Tab, TabList, TabPanel, Tabs } from "@app/components/v2";
import { useOrganization } from "@app/context";
import { OrgGeneralPermissionActions, OrgPermissionSubjects, useOrganization } from "@app/context";
import { withPermission } from "@app/hoc";
import { useGetRoles } from "@app/hooks/api";
import { OrgMembersTable } from "./components/OrgMembersTable";
@@ -14,7 +15,8 @@ enum TabSections {
Roles = "roles"
}
export const MembersPage = () => {
export const MembersPage = withPermission(
() => {
const { t } = useTranslation();
const { currentOrg } = useOrganization();
@@ -55,4 +57,6 @@ export const MembersPage = () => {
</div>
</div>
);
};
},
{ action: OrgGeneralPermissionActions.Read, subject: OrgPermissionSubjects.Member }
);
@@ -14,6 +14,7 @@ import { yupResolver } from "@hookform/resolvers/yup";
import * as yup from "yup";
import { useNotificationContext } from "@app/components/context/Notifications/NotificationProvider";
import { OrgPermissionCan } from "@app/components/permissions";
import {
decryptAssymmetric,
encryptAssymmetric
@@ -41,7 +42,14 @@ import {
Tr,
UpgradePlanModal
} from "@app/components/v2";
import { useOrganization, useSubscription, useUser, useWorkspace } from "@app/context";
import {
OrgGeneralPermissionActions,
OrgPermissionSubjects,
useOrganization,
useSubscription,
useUser,
useWorkspace
} from "@app/context";
import { usePopUp, useToggle } from "@app/hooks";
import {
useAddUserToOrg,
@@ -297,7 +305,10 @@ export const OrgMembersTable = ({ roles = [] }: Props) => {
placeholder="Search members..."
/>
</div>
<OrgPermissionCan I={OrgGeneralPermissionActions.Create} a={OrgPermissionSubjects.Member}>
{(isAllowed) => (
<Button
isDisabled={!isAllowed}
leftIcon={<FontAwesomeIcon icon={faPlus} />}
onClick={() => {
if (!isLoadingSSOConfig && ssoConfig && ssoConfig.isActive) {
@@ -318,6 +329,8 @@ export const OrgMembersTable = ({ roles = [] }: Props) => {
>
Add Member
</Button>
)}
</OrgPermissionCan>
</div>
<div>
<TableContainer>
@@ -345,12 +358,18 @@ export const OrgMembersTable = ({ roles = [] }: Props) => {
<Td>{name}</Td>
<Td>{email}</Td>
<Td>
<OrgPermissionCan
I={OrgGeneralPermissionActions.Edit}
a={OrgPermissionSubjects.Member}
>
{(isAllowed) => (
<>
{status === "accepted" && (
<Select
defaultValue={
role === "custom" ? findRoleFromId(customRole)?.slug : role
}
isDisabled={userId === u?._id}
isDisabled={userId === u?._id || !isAllowed}
className="w-40 bg-mineshaft-600"
dropdownContainerClassName="border border-mineshaft-600 bg-mineshaft-800"
onValueChange={(selectedRole) =>
@@ -371,6 +390,7 @@ export const OrgMembersTable = ({ roles = [] }: Props) => {
{(status === "invited" || status === "verified") &&
serverDetails?.emailConfigured && (
<Button
isDisabled={!isAllowed}
className="w-40"
colorSchema="primary"
variant="outline_bg"
@@ -382,11 +402,15 @@ export const OrgMembersTable = ({ roles = [] }: Props) => {
{status === "completed" && (
<Button
colorSchema="secondary"
isDisabled={!isAllowed}
onClick={() => onGrantAccess(u?._id, u?.publicKey)}
>
Grant Access
</Button>
)}
</>
)}
</OrgPermissionCan>
</Td>
<Td>
{userWs ? (
@@ -428,10 +452,15 @@ export const OrgMembersTable = ({ roles = [] }: Props) => {
</Td>
<Td>
{userId !== u?._id && (
<OrgPermissionCan
I={OrgGeneralPermissionActions.Delete}
a={OrgPermissionSubjects.Member}
>
{(isAllowed) => (
<IconButton
ariaLabel="delete"
colorSchema="danger"
isDisabled={userId === u?._id}
isDisabled={userId === u?._id || !isAllowed}
onClick={() =>
handlePopUpOpen("removeMember", { id: orgMembershipId })
}
@@ -439,6 +468,8 @@ export const OrgMembersTable = ({ roles = [] }: Props) => {
<FontAwesomeIcon icon={faTrash} />
</IconButton>
)}
</OrgPermissionCan>
)}
</Td>
</Tr>
);
@@ -38,10 +38,9 @@ export const BillingPermission = ({ isNonEditable, setValue, control }: Props) =
const [isCustom, setIsCustom] = useToggle();
const selectedPermissionCategory = useMemo(() => {
let score = 0;
const actions = Object.keys(rule || {}) as Array<keyof typeof rule>;
const totalActions = PERMISSIONS.length;
actions.forEach((key) => (score += rule[key] ? 1 : 0));
const score = actions.map((key) => (rule[key] ? 1 : 0)).reduce((a, b) => a + b, 0 as number);
if (isCustom) return Permission.Custom;
if (score === 0) return Permission.NoAccess;
@@ -52,11 +51,14 @@ export const BillingPermission = ({ isNonEditable, setValue, control }: Props) =
}, [rule, isCustom]);
useEffect(() => {
selectedPermissionCategory === Permission.Custom ? setIsCustom.on() : setIsCustom.off();
if (selectedPermissionCategory === Permission.Custom) setIsCustom.on();
else setIsCustom.off();
}, [selectedPermissionCategory]);
const handlePermissionChange = (val: Permission) => {
val === Permission.Custom ? setIsCustom.on() : setIsCustom.off();
if (val === Permission.Custom) setIsCustom.on();
else setIsCustom.off();
switch (val) {
case Permission.NoAccess:
setValue(
@@ -38,10 +38,9 @@ export const IncidentContactPermission = ({ isNonEditable, setValue, control }:
const [isCustom, setIsCustom] = useToggle();
const selectedPermissionCategory = useMemo(() => {
let score = 0;
const actions = Object.keys(rule || {}) as Array<keyof typeof rule>;
const totalActions = PERMISSIONS.length;
actions.forEach((key) => (score += rule[key] ? 1 : 0));
const score = actions.map((key) => (rule[key] ? 1 : 0)).reduce((a, b) => a + b, 0 as number);
if (isCustom) return Permission.Custom;
if (score === 0) return Permission.NoAccess;
@@ -52,14 +51,16 @@ export const IncidentContactPermission = ({ isNonEditable, setValue, control }:
}, [rule, isCustom]);
useEffect(() => {
selectedPermissionCategory === Permission.Custom ? setIsCustom.on() : setIsCustom.off();
if (selectedPermissionCategory === Permission.Custom) setIsCustom.on();
else setIsCustom.off();
}, [selectedPermissionCategory]);
const handlePermissionChange = (val: Permission) => {
val === Permission.Custom ? setIsCustom.on() : setIsCustom.off();
if (val === Permission.Custom) setIsCustom.on();
else setIsCustom.off();
switch (val) {
case Permission.NoAccess:
setIsCustom.off();
setValue(
"permissions.incident-contact",
{ read: false, edit: false, create: false, delete: false },
@@ -67,7 +68,6 @@ export const IncidentContactPermission = ({ isNonEditable, setValue, control }:
);
break;
case Permission.FullAccess:
setIsCustom.off();
setValue(
"permissions.incident-contact",
{ read: true, edit: true, create: true, delete: true },
@@ -75,7 +75,6 @@ export const IncidentContactPermission = ({ isNonEditable, setValue, control }:
);
break;
case Permission.ReadOnly:
setIsCustom.off();
setValue(
"permissions.incident-contact",
{ read: true, edit: false, create: false, delete: false },
@@ -83,7 +82,6 @@ export const IncidentContactPermission = ({ isNonEditable, setValue, control }:
);
break;
default:
setIsCustom.on();
setValue(
"permissions.incident-contact",
{ read: false, edit: false, create: false, delete: false },
@@ -31,31 +31,34 @@ const PERMISSIONS = [
] as const;
export const MemberPermission = ({ isNonEditable, setValue, control }: Props) => {
const memberRule = useWatch({
const rule = useWatch({
control,
name: "permissions.member"
});
const [isCustom, setIsCustom] = useToggle();
const selectedPermissionCategory = useMemo(() => {
let score = 0;
const actions = Object.keys(memberRule || {}) as Array<keyof typeof memberRule>;
const actions = Object.keys(rule || {}) as Array<keyof typeof rule>;
const totalActions = PERMISSIONS.length;
actions.forEach((key) => (score += memberRule[key] ? 1 : 0));
const score = actions.map((key) => (rule[key] ? 1 : 0)).reduce((a, b) => a + b, 0 as number);
if (isCustom) return Permission.Custom;
if (score === 0) return Permission.NoAccess;
if (score === totalActions) return Permission.FullAccess;
if (score === 1 && memberRule.read) return Permission.ReadOnly;
if (score === 1 && rule.read) return Permission.ReadOnly;
return Permission.Custom;
}, [memberRule, isCustom]);
}, [rule, isCustom]);
useEffect(() => {
selectedPermissionCategory === Permission.Custom ? setIsCustom.on() : setIsCustom.off();
if (selectedPermissionCategory === Permission.Custom) setIsCustom.on();
else setIsCustom.off();
}, [selectedPermissionCategory]);
const handlePermissionChange = (val: Permission) => {
if (val === Permission.Custom) setIsCustom.on();
else setIsCustom.off();
switch (val) {
case Permission.NoAccess:
setValue(
@@ -31,34 +31,36 @@ const PERMISSIONS = [
] as const;
export const RolePermission = ({ isNonEditable, setValue, control }: Props) => {
const roleRule = useWatch({
const rule = useWatch({
control,
name: "permissions.role"
});
const [isCustom, setIsCustom] = useToggle();
const selectedPermissionCategory = useMemo(() => {
let score = 0;
const actions = Object.keys(roleRule || {}) as Array<keyof typeof roleRule>;
const actions = Object.keys(rule || {}) as Array<keyof typeof rule>;
const totalActions = PERMISSIONS.length;
actions.forEach((key) => (score += roleRule[key] ? 1 : 0));
const score = actions.map((key) => (rule[key] ? 1 : 0)).reduce((a, b) => a + b, 0 as number);
if (isCustom) return Permission.Custom;
if (score === 0) return Permission.NoAccess;
if (score === totalActions) return Permission.FullAccess;
if (score === 1 && roleRule.read) return Permission.ReadOnly;
if (score === 1 && rule.read) return Permission.ReadOnly;
return Permission.Custom;
}, [roleRule, isCustom]);
}, [rule, isCustom]);
useEffect(() => {
selectedPermissionCategory === Permission.Custom ? setIsCustom.on() : setIsCustom.off();
if (selectedPermissionCategory === Permission.Custom) setIsCustom.on();
else setIsCustom.off();
}, [selectedPermissionCategory]);
const handlePermissionChange = (val: Permission) => {
if (val === Permission.Custom) setIsCustom.on();
else setIsCustom.off();
switch (val) {
case Permission.NoAccess:
setIsCustom.off();
setValue(
"permissions.role",
{ read: false, edit: false, create: false, delete: false },
@@ -66,7 +68,6 @@ export const RolePermission = ({ isNonEditable, setValue, control }: Props) => {
);
break;
case Permission.FullAccess:
setIsCustom.off();
setValue(
"permissions.role",
{ read: true, edit: true, create: true, delete: true },
@@ -74,7 +75,6 @@ export const RolePermission = ({ isNonEditable, setValue, control }: Props) => {
);
break;
case Permission.ReadOnly:
setIsCustom.off();
setValue(
"permissions.role",
{ read: true, edit: false, create: false, delete: false },
@@ -82,7 +82,6 @@ export const RolePermission = ({ isNonEditable, setValue, control }: Props) => {
);
break;
default:
setIsCustom.on();
setValue(
"permissions.role",
{ read: false, edit: false, create: false, delete: false },
@@ -38,10 +38,9 @@ export const SecretScannigPermission = ({ isNonEditable, setValue, control }: Pr
const [isCustom, setIsCustom] = useToggle();
const selectedPermissionCategory = useMemo(() => {
let score = 0;
const actions = Object.keys(rule || {}) as Array<keyof typeof rule>;
const totalActions = PERMISSIONS.length;
actions.forEach((key) => (score += rule[key] ? 1 : 0));
const score = actions.map((key) => (rule[key] ? 1 : 0)).reduce((a, b) => a + b, 0 as number);
if (isCustom) return Permission.Custom;
if (score === 0) return Permission.NoAccess;
@@ -52,13 +51,16 @@ export const SecretScannigPermission = ({ isNonEditable, setValue, control }: Pr
}, [rule, isCustom]);
useEffect(() => {
selectedPermissionCategory === Permission.Custom ? setIsCustom.on() : setIsCustom.off();
if (selectedPermissionCategory === Permission.Custom) setIsCustom.on();
else setIsCustom.off();
}, [selectedPermissionCategory]);
const handlePermissionChange = (val: Permission) => {
if (val === Permission.Custom) setIsCustom.on();
else setIsCustom.off();
switch (val) {
case Permission.NoAccess:
setIsCustom.off();
setValue(
"permissions.secret-scanning",
{ read: false, edit: false, create: false, delete: false },
@@ -66,7 +68,6 @@ export const SecretScannigPermission = ({ isNonEditable, setValue, control }: Pr
);
break;
case Permission.FullAccess:
setIsCustom.off();
setValue(
"permissions.secret-scanning",
{ read: true, edit: true, create: true, delete: true },
@@ -74,7 +75,6 @@ export const SecretScannigPermission = ({ isNonEditable, setValue, control }: Pr
);
break;
case Permission.ReadOnly:
setIsCustom.off();
setValue(
"permissions.secret-scanning",
{ read: true, edit: false, create: false, delete: false },
@@ -82,7 +82,6 @@ export const SecretScannigPermission = ({ isNonEditable, setValue, control }: Pr
);
break;
default:
setIsCustom.on();
setValue(
"permissions.secret-scanning",
{ read: false, edit: false, create: false, delete: false },
@@ -38,10 +38,9 @@ export const SettingsPermission = ({ isNonEditable, setValue, control }: Props)
const [isCustom, setIsCustom] = useToggle();
const selectedPermissionCategory = useMemo(() => {
let score = 0;
const actions = Object.keys(rule || {}) as Array<keyof typeof rule>;
const totalActions = PERMISSIONS.length;
actions.forEach((key) => (score += rule[key] ? 1 : 0));
const score = actions.map((key) => (rule[key] ? 1 : 0)).reduce((a, b) => a + b, 0 as number);
if (isCustom) return Permission.Custom;
if (score === 0) return Permission.NoAccess;
@@ -52,13 +51,16 @@ export const SettingsPermission = ({ isNonEditable, setValue, control }: Props)
}, [rule, isCustom]);
useEffect(() => {
selectedPermissionCategory === Permission.Custom ? setIsCustom.on() : setIsCustom.off();
if (selectedPermissionCategory === Permission.Custom) setIsCustom.on();
else setIsCustom.off();
}, [selectedPermissionCategory]);
const handlePermissionChange = (val: Permission) => {
if (val === Permission.Custom) setIsCustom.on();
else setIsCustom.off();
switch (val) {
case Permission.NoAccess:
setIsCustom.off();
setValue(
"permissions.settings",
{ read: false, edit: false, create: false, delete: false },
@@ -66,7 +68,6 @@ export const SettingsPermission = ({ isNonEditable, setValue, control }: Props)
);
break;
case Permission.FullAccess:
setIsCustom.off();
setValue(
"permissions.settings",
{ read: true, edit: true, create: true, delete: true },
@@ -74,7 +75,6 @@ export const SettingsPermission = ({ isNonEditable, setValue, control }: Props)
);
break;
case Permission.ReadOnly:
setIsCustom.off();
setValue(
"permissions.settings",
{ read: true, edit: false, create: false, delete: false },
@@ -82,7 +82,6 @@ export const SettingsPermission = ({ isNonEditable, setValue, control }: Props)
);
break;
default:
setIsCustom.on();
setValue(
"permissions.settings",
{ read: false, edit: false, create: false, delete: false },
@@ -38,10 +38,9 @@ export const SsoPermission = ({ isNonEditable, setValue, control }: Props) => {
const [isCustom, setIsCustom] = useToggle();
const selectedPermissionCategory = useMemo(() => {
let score = 0;
const actions = Object.keys(rule || {}) as Array<keyof typeof rule>;
const totalActions = PERMISSIONS.length;
actions.forEach((key) => (score += rule[key] ? 1 : 0));
const score = actions.map((key) => (rule[key] ? 1 : 0)).reduce((a, b) => a + b, 0 as number);
if (isCustom) return Permission.Custom;
if (score === 0) return Permission.NoAccess;
@@ -52,13 +51,16 @@ export const SsoPermission = ({ isNonEditable, setValue, control }: Props) => {
}, [rule, isCustom]);
useEffect(() => {
selectedPermissionCategory === Permission.Custom ? setIsCustom.on() : setIsCustom.off();
if (selectedPermissionCategory === Permission.Custom) setIsCustom.on();
else setIsCustom.off();
}, [selectedPermissionCategory]);
const handlePermissionChange = (val: Permission) => {
if (val === Permission.Custom) setIsCustom.on();
else setIsCustom.off();
switch (val) {
case Permission.NoAccess:
setIsCustom.off();
setValue(
"permissions.sso",
{ read: false, edit: false, create: false, delete: false },
@@ -66,7 +68,6 @@ export const SsoPermission = ({ isNonEditable, setValue, control }: Props) => {
);
break;
case Permission.FullAccess:
setIsCustom.off();
setValue(
"permissions.sso",
{ read: true, edit: true, create: true, delete: true },
@@ -74,7 +75,6 @@ export const SsoPermission = ({ isNonEditable, setValue, control }: Props) => {
);
break;
case Permission.ReadOnly:
setIsCustom.off();
setValue(
"permissions.sso",
{ read: true, edit: false, create: false, delete: false },
@@ -82,7 +82,6 @@ export const SsoPermission = ({ isNonEditable, setValue, control }: Props) => {
);
break;
default:
setIsCustom.on();
setValue(
"permissions.sso",
{ read: false, edit: false, create: false, delete: false },
@@ -36,10 +36,9 @@ export const WorkspacePermission = ({ isNonEditable, setValue, control }: Props)
const [isCustom, setIsCustom] = useToggle();
const selectedPermissionCategory = useMemo(() => {
let score = 0;
const actions = Object.keys(rule || {}) as Array<keyof typeof rule>;
const totalActions = PERMISSIONS.length;
actions.forEach((key) => (score += rule[key] ? 1 : 0));
const score = actions.map((key) => (rule[key] ? 1 : 0)).reduce((a, b) => a + b, 0 as number);
if (isCustom) return Permission.Custom;
if (score === 0) return Permission.NoAccess;
@@ -50,11 +49,14 @@ export const WorkspacePermission = ({ isNonEditable, setValue, control }: Props)
}, [rule, isCustom]);
useEffect(() => {
selectedPermissionCategory === Permission.Custom ? setIsCustom.on() : setIsCustom.off();
if (selectedPermissionCategory === Permission.Custom) setIsCustom.on();
else setIsCustom.off();
}, [selectedPermissionCategory]);
const handlePermissionChange = (val: Permission) => {
val === Permission.Custom ? setIsCustom.on() : setIsCustom.off();
if (val === Permission.Custom) setIsCustom.on();
else setIsCustom.off();
switch (val) {
case Permission.NoAccess:
setValue("permissions.workspace", { read: false, create: false }, { shouldDirty: true });
@@ -1,28 +1,47 @@
import { useEffect, useState } from "react";
import { OrgPermissionCan } from "@app/components/permissions";
import { OrgGeneralPermissionActions, OrgPermissionSubjects } from "@app/context";
import updateRiskStatus, { RiskStatus } from "@app/pages/api/secret-scanning/updateRiskStatus";
export const RiskStatusSelection = ({riskId, currentSelection}: {riskId: any, currentSelection: any }) => {
export const RiskStatusSelection = ({
riskId,
currentSelection
}: {
riskId: any;
currentSelection: any;
}) => {
const [selectedRiskStatus, setSelectedRiskStatus] = useState(currentSelection);
useEffect(()=>{
if (currentSelection !== selectedRiskStatus){
const updateSelection = async () =>{
await updateRiskStatus(String(localStorage.getItem("orgData.id")), riskId, selectedRiskStatus)
useEffect(() => {
if (currentSelection !== selectedRiskStatus) {
const updateSelection = async () => {
await updateRiskStatus(
String(localStorage.getItem("orgData.id")),
riskId,
selectedRiskStatus
);
};
updateSelection();
}
updateSelection()
}
},[selectedRiskStatus])
}, [selectedRiskStatus]);
return (
<OrgPermissionCan I={OrgGeneralPermissionActions.Edit} a={OrgPermissionSubjects.SecretScanning}>
{(isAllowed) => (
<select
disabled={!isAllowed}
value={selectedRiskStatus}
onChange={(e) => setSelectedRiskStatus(e.target.value)}
className="block w-full py-2 px-3 rounded-md shadow-sm focus:outline-none focus:ring-blue-500 focus:border-blue-500"
>
<option>Unresolved</option>
<option value={RiskStatus.RESOLVED_FALSE_POSITIVE}>This is a false positive, resolved</option>
<option value={RiskStatus.RESOLVED_FALSE_POSITIVE}>
This is a false positive, resolved
</option>
<option value={RiskStatus.RESOLVED_REVOKED}>I have rotated the secret, resolved</option>
<option value={RiskStatus.RESOLVED_NOT_REVOKED}>No rotate needed, resolved</option>
</select>
)}
</OrgPermissionCan>
);
}
};
@@ -1,5 +1,11 @@
import { OrgPermissionCan } from "@app/components/permissions";
import { Button } from "@app/components/v2";
import { useOrganization,useSubscription } from "@app/context";
import {
OrgGeneralPermissionActions,
OrgPermissionSubjects,
useOrganization,
useSubscription
} from "@app/context";
import {
useCreateCustomerPortalSession,
useGetOrgPlanBillingInfo,
@@ -16,18 +22,16 @@ export const PreviewSection = () => {
const getOrgTrialUrl = useGetOrgTrialUrl();
const createCustomerPortalSession = useCreateCustomerPortalSession();
const { popUp, handlePopUpOpen, handlePopUpToggle } = usePopUp([
"managePlan"
] as const);
const { popUp, handlePopUpOpen, handlePopUpToggle } = usePopUp(["managePlan"] as const);
const formatAmount = (amount: number) => {
const formattedTotal = (Math.floor(amount) / 100).toLocaleString("en-US", {
style: "currency",
currency: "USD",
currency: "USD"
});
return formattedTotal;
}
};
const formatDate = (date: number) => {
const createdDate = new Date(date * 1000);
@@ -37,12 +41,10 @@ export const PreviewSection = () => {
const formattedDate: string = `${day}/${month}/${year}`;
return formattedDate;
}
};
function formatPlanSlug(slug: string) {
return slug
.replace(/(\b[a-z])/g, match => match.toUpperCase())
.replace(/-/g, " ");
return slug.replace(/(\b[a-z])/g, (match) => match.toUpperCase()).replace(/-/g, " ");
}
const handleUpgradeBtnClick = async () => {
@@ -64,23 +66,35 @@ export const PreviewSection = () => {
} catch (err) {
console.error(err);
}
}
};
return (
<div>
{subscription && subscription?.slug !== "enterprise" && subscription?.slug !== "pro" && subscription?.slug !== "pro-annual" && (
<div className="p-4 bg-mineshaft-900 rounded-lg flex-1 border border-mineshaft-600 mb-6 flex items-center bg-mineshaft-600">
{subscription &&
subscription?.slug !== "enterprise" &&
subscription?.slug !== "pro" &&
subscription?.slug !== "pro-annual" && (
<div className="p-4 rounded-lg flex-1 border border-mineshaft-600 mb-6 flex items-center bg-mineshaft-600">
<div className="flex-1">
<h2 className="text-xl font-semibold text-mineshaft-50">Become Infisical</h2>
<p className="text-gray-400 mt-4">Unlimited members, projects, RBAC, smart alerts, and so much more</p>
<p className="text-gray-400 mt-4">
Unlimited members, projects, RBAC, smart alerts, and so much more
</p>
</div>
<OrgPermissionCan
I={OrgGeneralPermissionActions.Create}
a={OrgPermissionSubjects.Billing}
>
{(isAllowed) => (
<Button
// onClick={() => handlePopUpOpen("managePlan")}
onClick={() => handleUpgradeBtnClick()}
color="mineshaft"
isDisabled={!isAllowed}
>
{!subscription.has_used_trial ? "Start Pro Free Trial" : "Upgrade Plan"}
</Button>
)}
</OrgPermissionCan>
</div>
)}
{!isLoading && subscription && data && (
@@ -88,8 +102,15 @@ export const PreviewSection = () => {
<div className="p-4 bg-mineshaft-900 rounded-lg flex-1 mr-4 border border-mineshaft-600">
<p className="mb-2 text-gray-400">Current plan</p>
<p className="text-2xl text-mineshaft-50 font-semibold mb-8">
{`${formatPlanSlug(subscription.slug)} ${subscription.status === "trialing" ? "(Trial)" : ""}`}
{`${formatPlanSlug(subscription.slug)} ${
subscription.status === "trialing" ? "(Trial)" : ""
}`}
</p>
<OrgPermissionCan
I={OrgGeneralPermissionActions.Edit}
a={OrgPermissionSubjects.Billing}
>
{(isAllowed) => (
<button
type="button"
onClick={async () => {
@@ -97,15 +118,20 @@ export const PreviewSection = () => {
const { url } = await createCustomerPortalSession.mutateAsync(currentOrg._id);
window.location.href = url;
}}
disabled={!isAllowed}
className="text-primary"
>
Manage plan &rarr;
</button>
)}
</OrgPermissionCan>
</div>
<div className="p-4 bg-mineshaft-900 rounded-lg flex-1 border border-mineshaft-600 mr-4">
<p className="mb-2 text-gray-400">Price</p>
<p className="text-2xl mb-8 text-mineshaft-50 font-semibold">
{subscription.status === "trialing" ? "$0.00 / month" : `${formatAmount(data.amount)} / ${data.interval}`}
{subscription.status === "trialing"
? "$0.00 / month"
: `${formatAmount(data.amount)} / ${data.interval}`}
</p>
</div>
<div className="p-4 bg-mineshaft-900 rounded-lg flex-1 border border-mineshaft-600">
@@ -116,10 +142,7 @@ export const PreviewSection = () => {
</div>
</div>
)}
<ManagePlansModal
popUp={popUp}
handlePopUpToggle={handlePopUpToggle}
/>
<ManagePlansModal popUp={popUp} handlePopUpToggle={handlePopUpToggle} />
</div>
);
}
};
@@ -4,20 +4,16 @@ import { yupResolver } from "@hookform/resolvers/yup";
import * as yup from "yup";
import { useNotificationContext } from "@app/components/context/Notifications/NotificationProvider";
import {
Button,
FormControl,
Input
} from "@app/components/v2";
import { useOrganization } from "@app/context";
import {
useGetOrgBillingDetails,
useUpdateOrgBillingDetails
} from "@app/hooks/api";
import { OrgPermissionCan } from "@app/components/permissions";
import { Button, FormControl, Input } from "@app/components/v2";
import { OrgGeneralPermissionActions, OrgPermissionSubjects, useOrganization } from "@app/context";
import { useGetOrgBillingDetails, useUpdateOrgBillingDetails } from "@app/hooks/api";
const schema = yup.object({
const schema = yup
.object({
name: yup.string().required("Company name is required")
}).required();
})
.required();
export const CompanyNameSection = () => {
const { createNotification } = useNotificationContext();
@@ -59,40 +55,38 @@ export const CompanyNameSection = () => {
type: "error"
});
}
}
};
return (
<form
onSubmit={handleSubmit(onFormSubmit)}
className="p-4 bg-mineshaft-900 mb-6 rounded-lg border border-mineshaft-600"
>
<h2 className="text-xl font-semibold flex-1 text-mineshaft-100 mb-8">
Business name
</h2>
<h2 className="text-xl font-semibold flex-1 text-mineshaft-100 mb-8">Business name</h2>
<div className="max-w-md">
<Controller
defaultValue=""
render={({ field, fieldState: { error } }) => (
<FormControl isError={Boolean(error)} errorText={error?.message}>
<Input
placeholder="Acme Corp"
{...field}
className="bg-mineshaft-800"
/>
<Input placeholder="Acme Corp" {...field} className="bg-mineshaft-800" />
</FormControl>
)}
control={control}
name="name"
/>
</div>
<OrgPermissionCan I={OrgGeneralPermissionActions.Edit} a={OrgPermissionSubjects.Billing}>
{(isAllowed) => (
<Button
type="submit"
colorSchema="secondary"
isLoading={isLoading}
isDisabled={isLoading}
isDisabled={isLoading || !isAllowed}
>
Save
</Button>
)}
</OrgPermissionCan>
</form>
);
}
};
@@ -4,19 +4,16 @@ import { yupResolver } from "@hookform/resolvers/yup";
import * as yup from "yup";
import { useNotificationContext } from "@app/components/context/Notifications/NotificationProvider";
import {
Button,
FormControl,
Input} from "@app/components/v2";
import { useOrganization } from "@app/context";
import {
useGetOrgBillingDetails,
useUpdateOrgBillingDetails
} from "@app/hooks/api";
import { OrgPermissionCan } from "@app/components/permissions";
import { Button, FormControl, Input } from "@app/components/v2";
import { OrgGeneralPermissionActions, OrgPermissionSubjects, useOrganization } from "@app/context";
import { useGetOrgBillingDetails, useUpdateOrgBillingDetails } from "@app/hooks/api";
const schema = yup.object({
const schema = yup
.object({
email: yup.string().required("Email is required")
}).required();
})
.required();
export const InvoiceEmailSection = () => {
const { createNotification } = useNotificationContext();
@@ -59,39 +56,38 @@ export const InvoiceEmailSection = () => {
type: "error"
});
}
}
};
return (
<form
onSubmit={handleSubmit(onFormSubmit)}
className="p-4 bg-mineshaft-900 mb-6 rounded-lg border border-mineshaft-600"
>
<h2 className="text-xl font-semibold flex-1 text-white mb-8">
Invoice email recipient
</h2>
<h2 className="text-xl font-semibold flex-1 text-white mb-8">Invoice email recipient</h2>
<div className="max-w-md">
<Controller
defaultValue=""
render={({ field, fieldState: { error } }) => (
<FormControl isError={Boolean(error)} errorText={error?.message}>
<Input
placeholder="[email protected]"
{...field}
className="bg-mineshaft-800"
/>
<Input placeholder="[email protected]" {...field} className="bg-mineshaft-800" />
</FormControl>
)}
control={control}
name="email"
/>
</div>
<OrgPermissionCan I={OrgGeneralPermissionActions.Edit} a={OrgPermissionSubjects.Billing}>
{(isAllowed) => (
<Button
type="submit"
colorSchema="secondary"
isLoading={isLoading}
isDisabled={isLoading || !isAllowed}
>
Save
</Button>
)}
</OrgPermissionCan>
</form>
);
}
};
@@ -1,11 +1,9 @@
import { faPlus } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import {
Button
} from "@app/components/v2";
import { useOrganization } from "@app/context";
import { OrgPermissionCan } from "@app/components/permissions";
import { Button } from "@app/components/v2";
import { OrgGeneralPermissionActions, OrgPermissionSubjects, useOrganization } from "@app/context";
import { useAddOrgPmtMethod } from "@app/hooks/api";
import { PmtMethodsTable } from "./PmtMethodsTable";
@@ -23,24 +21,27 @@ export const PmtMethodsSection = () => {
});
window.location.href = url;
}
};
return (
<div className="p-4 bg-mineshaft-900 mb-6 rounded-lg border border-mineshaft-600">
<div className="flex items-center mb-8">
<h2 className="text-xl font-semibold flex-1 text-white">
Payment methods
</h2>
<h2 className="text-xl font-semibold flex-1 text-white">Payment methods</h2>
<OrgPermissionCan I={OrgGeneralPermissionActions.Create} a={OrgPermissionSubjects.Billing}>
{(isAllowed) => (
<Button
onClick={handleAddPmtMethodBtnClick}
colorSchema="secondary"
isLoading={isLoading}
leftIcon={<FontAwesomeIcon icon={faPlus} />}
isDisabled={!isAllowed}
>
Add method
</Button>
)}
</OrgPermissionCan>
</div>
<PmtMethodsTable />
</div>
);
}
};
@@ -1,6 +1,7 @@
import { faCreditCard, faXmark } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { OrgPermissionCan } from "@app/components/permissions";
import {
EmptyState,
IconButton,
@@ -13,7 +14,7 @@ import {
THead,
Tr
} from "@app/components/v2";
import { useOrganization } from "@app/context";
import { OrgGeneralPermissionActions, OrgPermissionSubjects, useOrganization } from "@app/context";
import { useDeleteOrgPmtMethod, useGetOrgPmtMethods } from "@app/hooks/api";
export const PmtMethodsTable = () => {
@@ -52,17 +53,25 @@ export const PmtMethodsTable = () => {
<Td>{last4}</Td>
<Td>{`${exp_month}/${exp_year}`}</Td>
<Td>
<OrgPermissionCan
I={OrgGeneralPermissionActions.Delete}
a={OrgPermissionSubjects.Billing}
>
{(isAllowed) => (
<IconButton
onClick={async () => {
await handleDeletePmtMethodBtnClick(_id);
}}
size="lg"
isDisabled={!isAllowed}
colorSchema="danger"
variant="plain"
ariaLabel="update"
>
<FontAwesomeIcon icon={faXmark} />
</IconButton>
)}
</OrgPermissionCan>
</Td>
</Tr>
))}
@@ -1,8 +1,9 @@
import { faPlus } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { OrgPermissionCan } from "@app/components/permissions";
import { Button } from "@app/components/v2";
import { OrgGeneralPermissionActions, OrgPermissionSubjects } from "@app/context";
import { usePopUp } from "@app/hooks/usePopUp";
import { TaxIDModal } from "./TaxIDModal";
@@ -16,16 +17,19 @@ export const TaxIDSection = () => {
return (
<div className="p-4 bg-mineshaft-900 mb-6 rounded-lg border border-mineshaft-600">
<div className="flex items-center mb-8">
<h2 className="text-xl font-semibold flex-1 text-white">
Tax ID
</h2>
<h2 className="text-xl font-semibold flex-1 text-white">Tax ID</h2>
<OrgPermissionCan I={OrgGeneralPermissionActions.Edit} a={OrgPermissionSubjects.Billing}>
{(isAllowed) => (
<Button
onClick={() => handlePopUpOpen("addTaxID")}
colorSchema="secondary"
isDisabled={!isAllowed}
leftIcon={<FontAwesomeIcon icon={faPlus} />}
>
Add method
</Button>
)}
</OrgPermissionCan>
</div>
<TaxIDTable />
<TaxIDModal
@@ -35,4 +39,4 @@ export const TaxIDSection = () => {
/>
</div>
);
}
};
@@ -1,6 +1,7 @@
import { faFileInvoice, faXmark } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { OrgPermissionCan } from "@app/components/permissions";
import {
EmptyState,
IconButton,
@@ -13,7 +14,7 @@ import {
THead,
Tr
} from "@app/components/v2";
import { useOrganization } from "@app/context";
import { OrgGeneralPermissionActions, OrgPermissionSubjects, useOrganization } from "@app/context";
import { useDeleteOrgTaxId, useGetOrgTaxIds } from "@app/hooks/api";
const taxIDTypeLabelMap: { [key: string]: string } = {
@@ -101,6 +102,11 @@ export const TaxIDTable = () => {
<Td>{taxIDTypeLabelMap[type]}</Td>
<Td>{value}</Td>
<Td>
<OrgPermissionCan
I={OrgGeneralPermissionActions.Delete}
a={OrgPermissionSubjects.Billing}
>
{(isAllowed) => (
<IconButton
onClick={async () => {
await handleDeleteTaxIdBtnClick(_id);
@@ -109,9 +115,12 @@ export const TaxIDTable = () => {
colorSchema="danger"
variant="plain"
ariaLabel="update"
isDisabled={!isAllowed}
>
<FontAwesomeIcon icon={faXmark} />
</IconButton>
)}
</OrgPermissionCan>
</Td>
</Tr>
))}
@@ -1,5 +1,8 @@
import { Fragment } from "react"
import { Tab } from "@headlessui/react"
import { Fragment } from "react";
import { Tab } from "@headlessui/react";
import { OrgGeneralPermissionActions, OrgPermissionSubjects } from "@app/context";
import { withPermission } from "@app/hoc";
import { BillingCloudTab } from "../BillingCloudTab";
import { BillingDetailsTab } from "../BillingDetailsTab";
@@ -13,7 +16,8 @@ const tabs = [
{ name: "Billing details", key: "tab-billing-details" }
];
export const BillingTabGroup = () => {
export const BillingTabGroup = withPermission(
() => {
return (
<Tab.Group>
<Tab.List className="mt-8 mb-6 border-b-2 border-mineshaft-800">
@@ -22,7 +26,9 @@ export const BillingTabGroup = () => {
{({ selected }) => (
<button
type="button"
className={`w-30 py-2 mx-2 mr-4 font-medium text-sm outline-none ${selected ? "border-b border-white text-white" : "text-mineshaft-400"}`}
className={`w-30 py-2 mx-2 mr-4 font-medium text-sm outline-none ${
selected ? "border-b border-white text-white" : "text-mineshaft-400"
}`}
>
{tab.name}
</button>
@@ -46,4 +52,6 @@ export const BillingTabGroup = () => {
</Tab.Panels>
</Tab.Group>
);
}
},
{ action: OrgGeneralPermissionActions.Read, subject: OrgPermissionSubjects.Billing }
);
@@ -1,9 +1,15 @@
import { OrgGeneralPermissionActions, OrgPermissionSubjects } from "@app/context";
import { withPermission } from "@app/hoc";
import { OrgSSOSection } from "./OrgSSOSection";
export const OrgAuthTab = () => {
export const OrgAuthTab = withPermission(
() => {
return (
<div>
<OrgSSOSection />
</div>
);
}
},
{ action: OrgGeneralPermissionActions.Read, subject: OrgPermissionSubjects.Sso }
);
@@ -2,13 +2,15 @@ import { faPlus } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { useNotificationContext } from "@app/components/context/Notifications/NotificationProvider";
import { OrgPermissionCan } from "@app/components/permissions";
import { Button, Switch, UpgradePlanModal } from "@app/components/v2";
import { useOrganization, useSubscription } from "@app/context";
import {
useCreateSSOConfig,
useGetSSOConfig,
useUpdateSSOConfig
} from "@app/hooks/api";
OrgGeneralPermissionActions,
OrgPermissionSubjects,
useOrganization,
useSubscription
} from "@app/context";
import { useCreateSSOConfig, useGetSSOConfig, useUpdateSSOConfig } from "@app/hooks/api";
import { usePopUp } from "@app/hooks/usePopUp";
import { SSOModal } from "./SSOModal";
@@ -17,7 +19,7 @@ const ssoAuthProviderMap: { [key: string]: string } = {
"okta-saml": "Okta SAML",
"azure-saml": "Azure SAML",
"jumpcloud-saml": "JumpCloud SAML"
}
};
export const OrgSSOSection = (): JSX.Element => {
const { currentOrg } = useOrganization();
@@ -52,7 +54,7 @@ export const OrgSSOSection = (): JSX.Element => {
type: "error"
});
}
}
};
const addSSOBtnClick = async () => {
try {
@@ -77,50 +79,62 @@ export const OrgSSOSection = (): JSX.Element => {
} catch (err) {
console.error(err);
}
}
};
return (
<div className="p-4 bg-mineshaft-900 mb-6 rounded-lg border border-mineshaft-600">
<div className="flex items-center mb-8">
<h2 className="text-xl font-semibold flex-1 text-white">
SAML SSO Configuration
</h2>
<h2 className="text-xl font-semibold flex-1 text-white">SAML SSO Configuration</h2>
{!isLoading && (
<OrgPermissionCan I={OrgGeneralPermissionActions.Create} a={OrgPermissionSubjects.Sso}>
{(isAllowed) => (
<Button
onClick={addSSOBtnClick}
colorSchema="secondary"
isDisabled={!isAllowed}
leftIcon={<FontAwesomeIcon icon={faPlus} />}
>
{data ? "Update SAML SSO" : "Set up SAML SSO"}
</Button>
)}
</OrgPermissionCan>
)}
</div>
{data && (
<div className="mb-4">
<OrgPermissionCan I={OrgGeneralPermissionActions.Edit} a={OrgPermissionSubjects.Sso}>
{(isAllowed) => (
<Switch
id="enable-saml-sso"
onCheckedChange={(value) => handleSamlSSOToggle(value)}
isChecked={data ? data.isActive : false}
isDisabled={!isAllowed}
>
Enable SAML SSO
</Switch>
)}
</OrgPermissionCan>
</div>
)}
<div className="mb-4">
<h3 className="text-mineshaft-400 text-sm">SSO identifier</h3>
<p className="text-gray-400 text-md">{(data && data._id !== "") ? data._id : "-"}</p>
<p className="text-gray-400 text-md">{data && data._id !== "" ? data._id : "-"}</p>
</div>
<div className="mb-4">
<h3 className="text-mineshaft-400 text-sm">Type</h3>
<p className="text-gray-400 text-md">{(data && data.authProvider !== "") ? ssoAuthProviderMap[data.authProvider] : "-"}</p>
<p className="text-gray-400 text-md">
{data && data.authProvider !== "" ? ssoAuthProviderMap[data.authProvider] : "-"}
</p>
</div>
<div className="mb-4">
<h3 className="text-mineshaft-400 text-sm">Entrypoint</h3>
<p className="text-gray-400 text-md">{(data && data.entryPoint !== "") ? data.entryPoint : "-"}</p>
<p className="text-gray-400 text-md">
{data && data.entryPoint !== "" ? data.entryPoint : "-"}
</p>
</div>
<div className="mb-4">
<h3 className="text-mineshaft-400 text-sm">Issuer</h3>
<p className="text-gray-400 text-md">{(data && data.issuer !== "") ? data.issuer : "-"}</p>
<p className="text-gray-400 text-md">{data && data.issuer !== "" ? data.issuer : "-"}</p>
</div>
<SSOModal
popUp={popUp}
@@ -6,10 +6,8 @@ export const OrgGeneralTab = () => {
return (
<div>
<OrgNameChangeSection />
<div className="p-4 bg-mineshaft-900 rounded-lg border border-mineshaft-600 mb-6">
<OrgServiceAccountsTable />
</div>
<OrgIncidentContactsSection />
</div>
);
}
};
@@ -3,17 +3,9 @@ import { yupResolver } from "@hookform/resolvers/yup";
import * as yup from "yup";
import { useNotificationContext } from "@app/components/context/Notifications/NotificationProvider";
import {
Button,
FormControl,
Input,
Modal,
ModalContent
} from "@app/components/v2";
import { Button, FormControl, Input, Modal, ModalContent } from "@app/components/v2";
import { useOrganization } from "@app/context";
import {
useAddIncidentContact
} from "@app/hooks/api";
import { useAddIncidentContact } from "@app/hooks/api";
import { useFetchServerStatus } from "@app/hooks/api/serverDetails";
import { UsePopUpState } from "@app/hooks/usePopUp";
@@ -36,12 +28,10 @@ export const AddOrgIncidentContactModal = ({
}: Props) => {
const { createNotification } = useNotificationContext();
const { currentOrg } = useOrganization();
const { data: serverDetails } = useFetchServerStatus()
const {
control,
handleSubmit,
reset
} = useForm<TAddContactForm>({ resolver: yupResolver(addContactFormSchema) });
const { data: serverDetails } = useFetchServerStatus();
const { control, handleSubmit, reset } = useForm<TAddContactForm>({
resolver: yupResolver(addContactFormSchema)
});
const { mutateAsync, isLoading } = useAddIncidentContact();
@@ -59,7 +49,7 @@ export const AddOrgIncidentContactModal = ({
type: "success"
});
if (serverDetails?.emailConfigured){
if (serverDetails?.emailConfigured) {
handlePopUpClose("addContact");
}
@@ -71,7 +61,7 @@ export const AddOrgIncidentContactModal = ({
type: "error"
});
}
}
};
return (
<Modal
@@ -96,13 +86,8 @@ export const AddOrgIncidentContactModal = ({
</FormControl>
)}
/>
<div className="mt-8 flex items-center">
<Button
size="sm"
type="submit"
isLoading={isLoading}
isDisabled={isLoading}
>
<div className="mt-8 flex items-center space-x-4">
<Button size="sm" type="submit" isLoading={isLoading} isDisabled={isLoading}>
Add Incident Contact
</Button>
<Button
@@ -117,4 +102,4 @@ export const AddOrgIncidentContactModal = ({
</ModalContent>
</Modal>
);
}
};
@@ -2,15 +2,17 @@ import { useTranslation } from "react-i18next";
import { faPlus } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import {
Button
} from "@app/components/v2";
import { OrgPermissionCan } from "@app/components/permissions";
import { Button } from "@app/components/v2";
import { OrgGeneralPermissionActions, OrgPermissionSubjects } from "@app/context";
import { withPermission } from "@app/hoc";
import { usePopUp } from "@app/hooks";
import { AddOrgIncidentContactModal } from "./AddOrgIncidentContactModal";
import { OrgIncidentContactsTable } from "./OrgIncidentContactsTable";
export const OrgIncidentContactsSection = () => {
export const OrgIncidentContactsSection = withPermission(
() => {
const { t } = useTranslation();
const { handlePopUpToggle, popUp, handlePopUpOpen, handlePopUpClose } = usePopUp([
"addContact"
@@ -22,14 +24,22 @@ export const OrgIncidentContactsSection = () => {
<p className="min-w-max text-xl font-semibold">
{t("section.incident.incident-contacts")}
</p>
<OrgPermissionCan
I={OrgGeneralPermissionActions.Create}
a={OrgPermissionSubjects.IncidentAccount}
>
{(isAllowed) => (
<Button
colorSchema="secondary"
type="submit"
isDisabled={!isAllowed}
leftIcon={<FontAwesomeIcon icon={faPlus} />}
onClick={() => handlePopUpOpen("addContact")}
>
Add contact
</Button>
)}
</OrgPermissionCan>
</div>
<OrgIncidentContactsTable />
<AddOrgIncidentContactModal
@@ -39,5 +49,6 @@ export const OrgIncidentContactsSection = () => {
/>
</div>
);
}
},
{ action: OrgGeneralPermissionActions.Read, subject: OrgPermissionSubjects.IncidentAccount }
);
@@ -3,6 +3,7 @@ import { faContactBook, faMagnifyingGlass, faTrash } from "@fortawesome/free-sol
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { useNotificationContext } from "@app/components/context/Notifications/NotificationProvider";
import { OrgPermissionCan } from "@app/components/permissions";
import {
DeleteActionModal,
EmptyState,
@@ -17,7 +18,7 @@ import {
THead,
Tr
} from "@app/components/v2";
import { useOrganization } from "@app/context";
import { OrgGeneralPermissionActions, OrgPermissionSubjects, useOrganization } from "@app/context";
import { usePopUp } from "@app/hooks";
import { useDeleteIncidentContact, useGetOrgIncidentContact } from "@app/hooks/api";
@@ -83,13 +84,21 @@ export const OrgIncidentContactsTable = () => {
<Tr key={email}>
<Td className="w-full">{email}</Td>
<Td className="mr-4">
<OrgPermissionCan
I={OrgGeneralPermissionActions.Delete}
an={OrgPermissionSubjects.IncidentAccount}
>
{(isAllowed) => (
<IconButton
ariaLabel="delete"
colorSchema="danger"
onClick={() => handlePopUpOpen("removeContact", { email })}
isDisabled={!isAllowed}
>
<FontAwesomeIcon icon={faTrash} />
</IconButton>
)}
</OrgPermissionCan>
</Td>
</Tr>
))}
@@ -4,8 +4,10 @@ import { yupResolver } from "@hookform/resolvers/yup";
import * as yup from "yup";
import { useNotificationContext } from "@app/components/context/Notifications/NotificationProvider";
import { OrgPermissionCan } from "@app/components/permissions";
import { Button, FormControl, Input } from "@app/components/v2";
import { useOrganization } from "@app/context";
import { OrgGeneralPermissionActions, OrgPermissionSubjects, useOrganization } from "@app/context";
import { withPermission } from "@app/hoc";
import { useRenameOrg } from "@app/hooks/api";
const formSchema = yup.object({
@@ -14,14 +16,13 @@ const formSchema = yup.object({
type FormData = yup.InferType<typeof formSchema>;
export const OrgNameChangeSection = (): JSX.Element => {
export const OrgNameChangeSection = withPermission(
(): JSX.Element => {
const { currentOrg } = useOrganization();
const { createNotification } = useNotificationContext();
const {
handleSubmit,
control,
reset
} = useForm<FormData>({ resolver: yupResolver(formSchema) });
const { handleSubmit, control, reset } = useForm<FormData>({
resolver: yupResolver(formSchema)
});
const { mutateAsync, isLoading } = useRenameOrg();
useEffect(() => {
@@ -54,9 +55,7 @@ export const OrgNameChangeSection = (): JSX.Element => {
onSubmit={handleSubmit(onFormSubmit)}
className="p-4 bg-mineshaft-900 mb-6 rounded-lg border border-mineshaft-600"
>
<p className="text-xl font-semibold text-mineshaft-100 mb-4">
Organization name
</p>
<p className="text-xl font-semibold text-mineshaft-100 mb-4">Organization name</p>
<div className="mb-2 max-w-md">
<Controller
defaultValue=""
@@ -69,14 +68,25 @@ export const OrgNameChangeSection = (): JSX.Element => {
name="name"
/>
</div>
<OrgPermissionCan I={OrgGeneralPermissionActions.Edit} a={OrgPermissionSubjects.Settings}>
{(isAllowed) => (
<Button
isLoading={isLoading}
isDisabled={!isAllowed}
colorSchema="primary"
variant="outline_bg"
type="submit"
>
Save
</Button>
)}
</OrgPermissionCan>
</form>
);
};
},
{
action: OrgGeneralPermissionActions.Read,
subject: OrgPermissionSubjects.Settings,
containerClassName: "mb-4"
}
);
@@ -34,7 +34,13 @@ import {
THead,
Tr
} from "@app/components/v2";
import { useOrganization, useWorkspace } from "@app/context";
import {
OrgGeneralPermissionActions,
OrgPermissionSubjects,
useOrganization,
useWorkspace
} from "@app/context";
import { withPermission } from "@app/hoc";
import { usePopUp, useToggle } from "@app/hooks";
import {
// useCreateServiceAccount,
@@ -62,7 +68,8 @@ import // Controller,
// type TAddServiceAccountForm = yup.InferType<typeof addServiceAccountFormSchema>;
export const OrgServiceAccountsTable = () => {
export const OrgServiceAccountsTable = withPermission(
() => {
const router = useRouter();
const { currentOrg } = useOrganization();
const { currentWorkspace } = useWorkspace();
@@ -139,7 +146,9 @@ export const OrgServiceAccountsTable = () => {
const filteredServiceAccounts = useMemo(
() =>
serviceAccounts.filter(({ name }) => name.toLowerCase().includes(searchServiceAccountFilter)),
serviceAccounts.filter(({ name }) =>
name.toLowerCase().includes(searchServiceAccountFilter)
),
[serviceAccounts, searchServiceAccountFilter]
);
@@ -274,7 +283,7 @@ export const OrgServiceAccountsTable = () => {
};
return (
<div className="w-full">
<div className="p-4 bg-mineshaft-900 rounded-lg border border-mineshaft-600 mb-6">
<div className="mb-4 flex justify-between">
<p className="text-xl font-semibold text-mineshaft-100">Service Accounts</p>
<Button
@@ -371,4 +380,10 @@ export const OrgServiceAccountsTable = () => {
/>
</div>
);
};
},
{
action: OrgGeneralPermissionActions.Read,
subject: OrgPermissionSubjects.Settings,
containerClassName: "mb-4"
}
);
@@ -1,33 +1,14 @@
import { Fragment } from "react"
import { Tab } from "@headlessui/react"
import { useOrganization,useUser } from "@app/context";
import {
useGetOrgUsers
} from "@app/hooks/api";
import { Fragment } from "react";
import { Tab } from "@headlessui/react";
import { OrgAuthTab } from "../OrgAuthTab";
import { OrgGeneralTab } from "../OrgGeneralTab";
export const OrgTabGroup = () => {
const { currentOrg } = useOrganization();
const { user } = useUser();
const { data } = useGetOrgUsers(currentOrg?._id ?? "");
const isRoleSufficient = data?.some((orgUser) => {
return orgUser.role !== "member" && orgUser.user._id === user._id;
});
const tabs = [
const tabs = [
{ name: "General", key: "tab-org-general" },
];
if (isRoleSufficient) {
tabs.push(
{ name: "Authentication", key: "tab-org-auth" }
);
}
];
export const OrgTabGroup = () => {
return (
<Tab.Group>
<Tab.List className="mb-6 border-b-2 border-mineshaft-800 w-full">
@@ -36,7 +17,9 @@ export const OrgTabGroup = () => {
{({ selected }) => (
<button
type="button"
className={`w-30 py-2 mx-2 mr-4 font-medium text-sm outline-none ${selected ? "border-b border-white text-white" : "text-mineshaft-400"}`}
className={`w-30 py-2 mx-2 mr-4 font-medium text-sm outline-none ${
selected ? "border-b border-white text-white" : "text-mineshaft-400"
}`}
>
{tab.name}
</button>
@@ -48,12 +31,10 @@ export const OrgTabGroup = () => {
<Tab.Panel>
<OrgGeneralTab />
</Tab.Panel>
{isRoleSufficient && (
<Tab.Panel>
<OrgAuthTab />
</Tab.Panel>
)}
</Tab.Panels>
</Tab.Group>
);
}
};