From 41c41a647f3daa5b974ef3aeefa099e9642c88e9 Mon Sep 17 00:00:00 2001 From: Tuan Dang Date: Fri, 24 Nov 2023 14:09:56 +0700 Subject: [PATCH] Standardize org members page styling --- .../v3/serviceTokenDataController.ts | 9 +- frontend/src/hooks/api/index.tsx | 3 +- frontend/src/hooks/api/workspace/queries.tsx | 3 + .../src/views/Org/MembersPage/MembersPage.tsx | 24 +- .../OrgMembersTab/OrgMembersTab.tsx | 17 + .../OrgMembersSection/AddOrgMemberModal.tsx | 176 ++++++ .../OrgMembersSection/OrgMembersSection.tsx | 152 +++++ .../OrgMembersSection/OrgMembersTable.tsx | 261 ++++++++ .../components/OrgMembersSection/index.tsx | 1 + .../OrgMembersTab/components/index.tsx | 1 + .../components/OrgMembersTab/index.tsx | 1 + .../OrgMembersTable/OrgMembersTable.tsx | 575 ------------------ .../components/OrgMembersTable/index.tsx | 1 - .../AddServiceTokenV3Modal.tsx | 19 +- .../ServiceTokenV3Section.tsx | 2 +- .../ServiceTokenV3Table.tsx | 118 +++- .../Org/MembersPage/components/index.tsx | 3 + .../AddServiceTokenV3Modal.tsx | 51 +- .../ServiceTokenV3Section.tsx | 3 +- .../ServiceTokenV3Table.tsx | 105 +++- 20 files changed, 884 insertions(+), 641 deletions(-) create mode 100644 frontend/src/views/Org/MembersPage/components/OrgMembersTab/OrgMembersTab.tsx create mode 100644 frontend/src/views/Org/MembersPage/components/OrgMembersTab/components/OrgMembersSection/AddOrgMemberModal.tsx create mode 100644 frontend/src/views/Org/MembersPage/components/OrgMembersTab/components/OrgMembersSection/OrgMembersSection.tsx create mode 100644 frontend/src/views/Org/MembersPage/components/OrgMembersTab/components/OrgMembersSection/OrgMembersTable.tsx create mode 100644 frontend/src/views/Org/MembersPage/components/OrgMembersTab/components/OrgMembersSection/index.tsx create mode 100644 frontend/src/views/Org/MembersPage/components/OrgMembersTab/components/index.tsx create mode 100644 frontend/src/views/Org/MembersPage/components/OrgMembersTab/index.tsx delete mode 100644 frontend/src/views/Org/MembersPage/components/OrgMembersTable/OrgMembersTable.tsx delete mode 100644 frontend/src/views/Org/MembersPage/components/OrgMembersTable/index.tsx create mode 100644 frontend/src/views/Org/MembersPage/components/index.tsx diff --git a/backend/src/ee/controllers/v3/serviceTokenDataController.ts b/backend/src/ee/controllers/v3/serviceTokenDataController.ts index 0cc622064..f3f1874d1 100644 --- a/backend/src/ee/controllers/v3/serviceTokenDataController.ts +++ b/backend/src/ee/controllers/v3/serviceTokenDataController.ts @@ -5,7 +5,8 @@ import { ServiceTokenDataV3, ServiceTokenDataV3Key, // TODO: remove Organization, - ServiceMembershipOrg + ServiceMembershipOrg, + ServiceMembership } from "../../../models"; import { IServiceTokenV3TrustedIp } from "../../../models/serviceTokenDataV3"; import { @@ -426,11 +427,15 @@ export const deleteServiceTokenData = async (req: Request, res: Response) => { if (!serviceMembershipOrg) throw BadRequestError({ message: "Failed to delete service token" }); - + await ServiceTokenDataV3Key.findOneAndDelete({ serviceTokenData: serviceTokenData._id }); + await ServiceMembership.deleteMany({ + service: serviceTokenData._id, + }); + await EEAuditLogService.createAuditLog( req.authData, { diff --git a/frontend/src/hooks/api/index.tsx b/frontend/src/hooks/api/index.tsx index c5e719218..f2e141f71 100644 --- a/frontend/src/hooks/api/index.tsx +++ b/frontend/src/hooks/api/index.tsx @@ -1,5 +1,5 @@ -export * from "./apiKeys"; export * from "./admin" +export * from "./apiKeys"; export * from "./auditLogs"; export * from "./auth"; export * from "./bots"; @@ -16,6 +16,7 @@ export * from "./secretImports"; export * from "./secretRotation"; export * from "./secrets"; export * from "./secretSnapshots"; +export * from "./serverDetails"; export * from "./serviceTokens"; export * from "./ssoConfig"; export * from "./subscriptions"; diff --git a/frontend/src/hooks/api/workspace/queries.tsx b/frontend/src/hooks/api/workspace/queries.tsx index 1ec2890e4..624a7283c 100644 --- a/frontend/src/hooks/api/workspace/queries.tsx +++ b/frontend/src/hooks/api/workspace/queries.tsx @@ -98,6 +98,7 @@ const fetchUserWorkspaceMemberships = async (orgId: string) => { const { data } = await apiRequest.get>( `/api/v1/organization/${orgId}/workspace-memberships` ); + return data; }; @@ -385,6 +386,8 @@ export const useAddServiceToWorkspace = () => { }); }; +// TODO: update + export const useDeleteServiceFromWorkspace = () => { const queryClient = useQueryClient(); return useMutation({ diff --git a/frontend/src/views/Org/MembersPage/MembersPage.tsx b/frontend/src/views/Org/MembersPage/MembersPage.tsx index 2b0e51ad5..907a06b50 100644 --- a/frontend/src/views/Org/MembersPage/MembersPage.tsx +++ b/frontend/src/views/Org/MembersPage/MembersPage.tsx @@ -1,13 +1,13 @@ /* eslint-disable @typescript-eslint/no-unused-vars */ -import { motion } from "framer-motion"; - import { Tab, TabList, TabPanel, Tabs } from "@app/components/v2"; import { OrgPermissionActions, OrgPermissionSubjects } from "@app/context"; import { withPermission } from "@app/hoc"; -import { OrgMembersTable } from "./components/OrgMembersTable"; -import { OrgRoleTabSection } from "./components/OrgRoleTabSection"; -import { OrgServiceTokenTab } from "./components/OrgServiceTokenTab"; +import { + OrgMembersTab, + OrgRoleTabSection, + OrgServiceTokenTab +} from "./components"; enum TabSections { Member = "members", @@ -25,20 +25,12 @@ export const MembersPage = withPermission(

- Members - Service Accounts + People + Machine Identities Roles - - - + diff --git a/frontend/src/views/Org/MembersPage/components/OrgMembersTab/OrgMembersTab.tsx b/frontend/src/views/Org/MembersPage/components/OrgMembersTab/OrgMembersTab.tsx new file mode 100644 index 000000000..0455b4f7d --- /dev/null +++ b/frontend/src/views/Org/MembersPage/components/OrgMembersTab/OrgMembersTab.tsx @@ -0,0 +1,17 @@ +import { motion } from "framer-motion"; + +import { OrgMembersSection } from "./components"; + +export const OrgMembersTab = () => { + return ( + + + + ); +} \ No newline at end of file diff --git a/frontend/src/views/Org/MembersPage/components/OrgMembersTab/components/OrgMembersSection/AddOrgMemberModal.tsx b/frontend/src/views/Org/MembersPage/components/OrgMembersTab/components/OrgMembersSection/AddOrgMemberModal.tsx new file mode 100644 index 000000000..40080fd3f --- /dev/null +++ b/frontend/src/views/Org/MembersPage/components/OrgMembersTab/components/OrgMembersSection/AddOrgMemberModal.tsx @@ -0,0 +1,176 @@ +import { Controller, useForm } from "react-hook-form"; +import { + faCheck, + faCopy, +} from "@fortawesome/free-solid-svg-icons"; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; +import { yupResolver } from "@hookform/resolvers/yup"; +import * as yup from "yup"; + +import { useNotificationContext } from "@app/components/context/Notifications/NotificationProvider"; +import { + Button, + FormControl, + IconButton, + Input, + Modal, + ModalContent, +} from "@app/components/v2"; +import { useOrganization } from "@app/context"; +import { useToggle } from "@app/hooks"; +import { + useAddUserToOrg, + useFetchServerStatus +} from "@app/hooks/api"; +import { UsePopUpState } from "@app/hooks/usePopUp"; + +const addMemberFormSchema = yup.object({ + email: yup.string().email().required().label("Email").trim().lowercase() +}); + +type TAddMemberForm = yup.InferType; + +type Props = { + popUp: UsePopUpState<["addMember"]>; + handlePopUpToggle: (popUpName: keyof UsePopUpState<["addMember"]>, state?: boolean) => void; + completeInviteLink: string; + setCompleteInviteLink: (link: string) => void; +}; + +export const AddOrgMemberModal = ({ + popUp, + handlePopUpToggle, + completeInviteLink, + setCompleteInviteLink +}: Props) => { + const { createNotification } = useNotificationContext(); + const { currentOrg } = useOrganization(); + + const { data: serverDetails } = useFetchServerStatus(); + const { mutateAsync: addUserMutateAsync } = useAddUserToOrg(); + + const [isInviteLinkCopied, setInviteLinkCopied] = useToggle(false); + + const { + control, + handleSubmit, + reset, + formState: { isSubmitting } + } = useForm({ resolver: yupResolver(addMemberFormSchema) }); + + const onAddMember = async ({ email }: TAddMemberForm) => { + if (!currentOrg?._id) return; + + try { + const { data } = await addUserMutateAsync({ + organizationId: currentOrg?._id, + inviteeEmail: email + }); + + setCompleteInviteLink(data?.completeInviteLink ??""); + + // only show this notification when email is configured. + // A [completeInviteLink] will not be sent if smtp is configured + + if (!data.completeInviteLink) { + createNotification({ + text: "Successfully invited user to the organization.", + type: "success" + }); + } + } catch (error) { + console.error(error); + createNotification({ + text: "Failed to invite user to org", + type: "error" + }); + } + + if (serverDetails?.emailConfigured) { + handlePopUpToggle("addMember", false); + } + + reset(); + }; + + const copyTokenToClipboard = () => { + navigator.clipboard.writeText(completeInviteLink as string); + setInviteLinkCopied.on(); + }; + + return ( + { + handlePopUpToggle("addMember", isOpen); + setCompleteInviteLink(undefined); + }} + > + + {!completeInviteLink && ( +
+ An invite is specific to an email address and expires after 1 day. +
+ For security reasons, you will need to separately add members to projects. +
+ )} + {completeInviteLink && + "This Infisical instance does not have a email provider setup. Please share this invite link with the invitee manually"} + + } + > + {!completeInviteLink && ( +
+ ( + + + + )} + /> +
+ + +
+ + )} + {completeInviteLink && ( +
+

{completeInviteLink}

+ + + + click to copy + + +
+ )} +
+
+ ); +} \ No newline at end of file diff --git a/frontend/src/views/Org/MembersPage/components/OrgMembersTab/components/OrgMembersSection/OrgMembersSection.tsx b/frontend/src/views/Org/MembersPage/components/OrgMembersTab/components/OrgMembersSection/OrgMembersSection.tsx new file mode 100644 index 000000000..f987af75d --- /dev/null +++ b/frontend/src/views/Org/MembersPage/components/OrgMembersTab/components/OrgMembersSection/OrgMembersSection.tsx @@ -0,0 +1,152 @@ +import { useState } from "react"; +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, + DeleteActionModal, + EmailServiceSetupModal, + UpgradePlanModal +} from "@app/components/v2"; +import { + OrgPermissionActions, + OrgPermissionSubjects, + useOrganization, + useSubscription, +} from "@app/context"; +import { + useDeleteOrgMembership, + useGetSSOConfig, +} from "@app/hooks/api"; +import { usePopUp } from "@app/hooks/usePopUp"; + +import { AddOrgMemberModal } from "./AddOrgMemberModal"; +import { OrgMembersTable } from "./OrgMembersTable"; + +export const OrgMembersSection = () => { + const { createNotification } = useNotificationContext(); + const { subscription } = useSubscription(); + const { currentOrg } = useOrganization(); + const orgId = currentOrg?._id ?? ""; + + const [completeInviteLink, setCompleteInviteLink] = useState(""); + + const { data: ssoConfig, isLoading: isLoadingSSOConfig } = useGetSSOConfig(orgId); + const { popUp, handlePopUpOpen, handlePopUpClose, handlePopUpToggle } = usePopUp([ + "addMember", + "removeMember", + "upgradePlan", + "setUpEmail" + ] as const); + + const { mutateAsync: deleteMutateAsync } = useDeleteOrgMembership(); + + const isMoreUsersNotAllowed = subscription?.memberLimit + ? subscription.membersUsed >= subscription.memberLimit + : false; + + const handleAddMemberModal = () => { + if (!isLoadingSSOConfig && ssoConfig && ssoConfig.isActive) { + createNotification({ + text: "You cannot invite users when SAML SSO is configured for your organization", + type: "error" + }); + return; + } + + if (isMoreUsersNotAllowed) { + console.log("no more users allowed"); + handlePopUpOpen("upgradePlan"); + } else { + console.log("yes more users allowed"); + handlePopUpOpen("addMember"); + } + } + + const onRemoveMemberSubmit = async (orgMembershipId: string) => { + try { + await deleteMutateAsync({ + orgId, + membershipId: orgMembershipId + }); + + createNotification({ + text: "Successfully removed user from org", + type: "success" + }); + } catch (err) { + console.error(err); + createNotification({ + text: "Failed to remove user from the organization", + type: "error" + }); + } + + handlePopUpClose("removeMember"); + } + + return ( +
+
+

+ Members +

+ + {(isAllowed) => ( + + )} + +
+ + + {/* */} + handlePopUpToggle("removeMember", isOpen)} + deleteKey="confirm" + onDeleteApproved={() => + onRemoveMemberSubmit( + (popUp?.removeMember?.data as { orgMembershipId: string })?.orgMembershipId + ) + } + /> + handlePopUpToggle("upgradePlan", isOpen)} + text="You can add more members if you upgrade your Infisical plan." + /> + handlePopUpToggle("setUpEmail", isOpen)} + /> +
+ ); +} \ No newline at end of file diff --git a/frontend/src/views/Org/MembersPage/components/OrgMembersTab/components/OrgMembersSection/OrgMembersTable.tsx b/frontend/src/views/Org/MembersPage/components/OrgMembersTab/components/OrgMembersSection/OrgMembersTable.tsx new file mode 100644 index 000000000..d475a4792 --- /dev/null +++ b/frontend/src/views/Org/MembersPage/components/OrgMembersTab/components/OrgMembersSection/OrgMembersTable.tsx @@ -0,0 +1,261 @@ +import { useCallback,useMemo, useState } from "react"; +import { + faMagnifyingGlass, + faUsers, + faXmark +} 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, + EmptyState, + IconButton, + Input, + Select, + SelectItem, + Table, + TableContainer, + TableSkeleton, + TBody, + Td, + Th, + THead, + Tr, +} from "@app/components/v2"; +import { + OrgPermissionActions, + OrgPermissionSubjects, + useOrganization, + useUser +} from "@app/context"; +import { + useAddUserToOrg, + useFetchServerStatus, + useGetOrgUsers, + useGetRoles, + useUpdateOrgUserRole +} from "@app/hooks/api"; +import { UsePopUpState } from "@app/hooks/usePopUp"; + +type Props = { + handlePopUpOpen: ( + popUpName: keyof UsePopUpState<["removeMember"]>, + data?: { + orgMembershipId?: string; + email?: string; + } + ) => void; + setCompleteInviteLink: (link: string) => void; +}; + +export const OrgMembersTable = ({ + handlePopUpOpen, + setCompleteInviteLink +}: Props) => { + const { createNotification } = useNotificationContext(); + const { currentOrg } = useOrganization(); + const { user } = useUser(); + const userId = user?._id || ""; + const orgId = currentOrg?._id || ""; + + const { data: roles, isLoading: isRolesLoading } = useGetRoles({ + orgId + }); + + const [searchMemberFilter, setSearchMemberFilter] = useState(""); + + const { data: serverDetails } = useFetchServerStatus(); + const { data: members, isLoading: isMembersLoading } = useGetOrgUsers(orgId); + + const { mutateAsync: addUserMutateAsync } = useAddUserToOrg(); + const { mutateAsync: updateUserOrgRole } = useUpdateOrgUserRole(); + + const onRoleChange = async (membershipId: string, role: string) => { + if (!currentOrg?._id) return; + + try { + await updateUserOrgRole({ + organizationId: currentOrg?._id, + membershipId, role + }); + createNotification({ + text: "Successfully updated user role", + type: "success" + }); + } catch (error) { + console.error(error); + createNotification({ + text: "Failed to update user role", + type: "error" + }); + } + }; + + const onResendInvite = async (email: string) => { + try { + + const { data } = await addUserMutateAsync({ + organizationId: orgId, + inviteeEmail: email + }); + + setCompleteInviteLink(data?.completeInviteLink || ""); + + if (!data.completeInviteLink) { + createNotification({ + text: `Successfully resent invite to ${email}`, + type: "success" + }); + } + } catch (err) { + console.error(err); + createNotification({ + text: `Failed to resend invite to ${email}`, + type: "error" + }); + } + } + + const isLoading = isMembersLoading || IsWsMembershipLoading || isRolesLoading; + + const isIamOwner = useMemo( + () => members?.find(({ user: u }) => userId === u?._id)?.role === "owner", + [userId, members] + ); + + const findRoleFromId = useCallback( + (roleId: string) => { + return (roles || []).find(({ _id: id }) => id === roleId); + }, + [roles] + ); + + const filterdUser = useMemo( + () => + members?.filter( + ({ user: u, inviteEmail }) => + u?.firstName?.toLowerCase().includes(searchMemberFilter) || + u?.lastName?.toLowerCase().includes(searchMemberFilter) || + u?.email?.toLowerCase().includes(searchMemberFilter) || + inviteEmail?.includes(searchMemberFilter) + ), + [members, searchMemberFilter] + ); + + return ( +
+ setSearchMemberFilter(e.target.value)} + leftIcon={} + placeholder="Search members..." + /> + + + + + + + + + + + {isLoading && } + {!isLoading && + filterdUser?.map( + ({ user: u, inviteEmail, role, customRole, _id: orgMembershipId, status }) => { + const name = u ? `${u.firstName} ${u.lastName}` : "-"; + const email = u?.email || inviteEmail; + + return ( + + + + + + + ); + } + )} + +
NameEmailRole +
{name}{email} + + {(isAllowed) => ( + <> + {status === "accepted" && ( + + )} + {(status === "invited" || status === "verified") && + serverDetails?.emailConfigured && ( + + )} + + )} + + + {userId !== u?._id && ( + + {(isAllowed) => ( + { + console.log("TODO: delete logic here"); + handlePopUpOpen("removeMember", { orgMembershipId, email }) + }} + size="lg" + colorSchema="danger" + variant="plain" + ariaLabel="update" + className="ml-4" + isDisabled={!isAllowed} + > + + + )} + + )} +
+ {!isLoading && filterdUser?.length === 0 && ( + + )} +
+ +
+ ); +} \ No newline at end of file diff --git a/frontend/src/views/Org/MembersPage/components/OrgMembersTab/components/OrgMembersSection/index.tsx b/frontend/src/views/Org/MembersPage/components/OrgMembersTab/components/OrgMembersSection/index.tsx new file mode 100644 index 000000000..306158a16 --- /dev/null +++ b/frontend/src/views/Org/MembersPage/components/OrgMembersTab/components/OrgMembersSection/index.tsx @@ -0,0 +1 @@ +export { OrgMembersSection } from "./OrgMembersSection"; \ No newline at end of file diff --git a/frontend/src/views/Org/MembersPage/components/OrgMembersTab/components/index.tsx b/frontend/src/views/Org/MembersPage/components/OrgMembersTab/components/index.tsx new file mode 100644 index 000000000..306158a16 --- /dev/null +++ b/frontend/src/views/Org/MembersPage/components/OrgMembersTab/components/index.tsx @@ -0,0 +1 @@ +export { OrgMembersSection } from "./OrgMembersSection"; \ No newline at end of file diff --git a/frontend/src/views/Org/MembersPage/components/OrgMembersTab/index.tsx b/frontend/src/views/Org/MembersPage/components/OrgMembersTab/index.tsx new file mode 100644 index 000000000..8b20853b9 --- /dev/null +++ b/frontend/src/views/Org/MembersPage/components/OrgMembersTab/index.tsx @@ -0,0 +1 @@ +export { OrgMembersTab } from "./OrgMembersTab"; \ No newline at end of file diff --git a/frontend/src/views/Org/MembersPage/components/OrgMembersTable/OrgMembersTable.tsx b/frontend/src/views/Org/MembersPage/components/OrgMembersTable/OrgMembersTable.tsx deleted file mode 100644 index 7e303554d..000000000 --- a/frontend/src/views/Org/MembersPage/components/OrgMembersTable/OrgMembersTable.tsx +++ /dev/null @@ -1,575 +0,0 @@ -import { useCallback, useEffect, useMemo, useState } from "react"; -import { Controller, useForm } from "react-hook-form"; -import { useRouter } from "next/router"; -import { - faCheck, - faCopy, - faMagnifyingGlass, - faPlus, - faTrash, - faUsers -} from "@fortawesome/free-solid-svg-icons"; -import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; -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 -} from "@app/components/utilities/cryptography/crypto"; -import { - Button, - DeleteActionModal, - EmailServiceSetupModal, - EmptyState, - FormControl, - IconButton, - Input, - Modal, - ModalContent, - Select, - SelectItem, - Table, - TableContainer, - TableSkeleton, - Tag, - TBody, - Td, - Th, - THead, - Tr, - UpgradePlanModal -} from "@app/components/v2"; -import { - OrgPermissionActions, - OrgPermissionSubjects, - useOrganization, - useSubscription, - useUser, - useWorkspace -} from "@app/context"; -import { usePopUp, useToggle } from "@app/hooks"; -import { - useAddUserToOrg, - useDeleteOrgMembership, - useGetOrgUsers, - useGetRoles, - useGetSSOConfig, - useGetUserWorkspaceMemberships, - useGetUserWsKey, - useUpdateOrgUserRole, - useUploadWsKey} from "@app/hooks/api"; -import { useFetchServerStatus } from "@app/hooks/api/serverDetails"; - -const addMemberFormSchema = yup.object({ - email: yup.string().email().required().label("Email").trim().lowercase() -}); - -type TAddMemberForm = yup.InferType; - -export const OrgMembersTable = () => { - const router = useRouter(); - const { createNotification } = useNotificationContext(); - - const { currentOrg } = useOrganization(); - const { workspaces, currentWorkspace } = useWorkspace(); - const { user } = useUser(); - const userId = user?._id || ""; - const orgId = currentOrg?._id || ""; - const workspaceId = currentWorkspace?._id || ""; - - const { data: roles, isLoading: isRolesLoading } = useGetRoles({ - orgId - }); - - const { data: ssoConfig, isLoading: isLoadingSSOConfig } = useGetSSOConfig(orgId); - const [searchMemberFilter, setSearchMemberFilter] = useState(""); - const { data: serverDetails } = useFetchServerStatus(); - - const [isInviteLinkCopied, setInviteLinkCopied] = useToggle(false); - const { handlePopUpToggle, popUp, handlePopUpOpen, handlePopUpClose } = usePopUp([ - "addMember", - "removeMember", - "upgradePlan", - "setUpEmail" - ] as const); - const { subscription } = useSubscription(); - - const { data: members, isLoading: isMembersLoading } = useGetOrgUsers(orgId); - const { data: workspaceMemberships, isLoading: IsWsMembershipLoading } = - useGetUserWorkspaceMemberships(orgId); - const { data: wsKey } = useGetUserWsKey(workspaceId); - - const removeUserOrgMembership = useDeleteOrgMembership(); - const addUserToOrg = useAddUserToOrg(); - const updateOrgUserRole = useUpdateOrgUserRole(); - const uploadWsKey = useUploadWsKey(); - - const [completeInviteLink, setCompleteInviteLink] = useState(""); - - const isMoreUsersNotAllowed = subscription?.memberLimit - ? subscription.membersUsed >= subscription.memberLimit - : false; - - useEffect(() => { - if (router.query.action === "invite") { - handlePopUpOpen("addMember"); - } - }, []); - - const { - control, - handleSubmit, - reset, - formState: { isSubmitting } - } = useForm({ resolver: yupResolver(addMemberFormSchema) }); - - const onAddMember = async ({ email }: TAddMemberForm) => { - if (!currentOrg?._id) return; - - try { - const { data } = await addUserToOrg.mutateAsync({ - organizationId: currentOrg?._id, - inviteeEmail: email - }); - setCompleteInviteLink(data?.completeInviteLink); - // only show this notification when email is configured. - // A [completeInviteLink] will not be sent if smtp is configured - if (!data.completeInviteLink) { - createNotification({ - text: "Successfully invited user to the organization.", - type: "success" - }); - } - } catch (error) { - console.error(error); - createNotification({ - text: "Failed to invite user to org", - type: "error" - }); - } - if (serverDetails?.emailConfigured) { - handlePopUpClose("addMember"); - } - reset(); - }; - - const onAddUserToOrg = async (email: string) => { - if (!currentOrg?._id) return; - - try { - const { data } = await addUserToOrg.mutateAsync({ - organizationId: currentOrg?._id, - inviteeEmail: email - }); - setCompleteInviteLink(data?.completeInviteLink); - - // only show this notification when email is configured. A [completeInviteLink] will not be sent if smtp is configured - if (!data.completeInviteLink) { - createNotification({ - text: "Successfully invited user to the organization.", - type: "success" - }); - } - } catch (error) { - console.error(error); - createNotification({ - text: "Failed to invite user to org", - type: "error" - }); - } - }; - - const onRemoveOrgMemberApproved = async () => { - const membershipId = (popUp?.removeMember?.data as { id: string })?.id; - if (!currentOrg?._id) return; - - try { - await removeUserOrgMembership.mutateAsync({ orgId: currentOrg?._id, membershipId }); - createNotification({ - text: "Successfully removed user from org", - type: "success" - }); - } catch (error) { - console.error(error); - createNotification({ - text: "Failed to remove user from the organization", - type: "error" - }); - } - handlePopUpClose("removeMember"); - }; - - const isIamOwner = useMemo( - () => members?.find(({ user: u }) => userId === u?._id)?.role === "owner", - [userId, members] - ); - - const findRoleFromId = useCallback( - (roleId: string) => { - return (roles || []).find(({ _id: id }) => id === roleId); - }, - [roles] - ); - - const filterdUser = useMemo( - () => - members?.filter( - ({ user: u, inviteEmail }) => - u?.firstName?.toLowerCase().includes(searchMemberFilter) || - u?.lastName?.toLowerCase().includes(searchMemberFilter) || - u?.email?.toLowerCase().includes(searchMemberFilter) || - inviteEmail?.includes(searchMemberFilter) - ), - [members, searchMemberFilter] - ); - - useEffect(() => { - let timer: NodeJS.Timeout; - if (isInviteLinkCopied) { - timer = setTimeout(() => setInviteLinkCopied.off(), 2000); - } - return () => clearTimeout(timer); - }, [isInviteLinkCopied]); - - const onRoleChange = async (membershipId: string, role: string) => { - if (!currentOrg?._id) return; - - try { - await updateOrgUserRole.mutateAsync({ organizationId: currentOrg?._id, membershipId, role }); - createNotification({ - text: "Successfully updated user role", - type: "success" - }); - } catch (error) { - console.error(error); - createNotification({ - text: "Failed to update user role", - type: "error" - }); - } - }; - - const onGrantAccess = async (grantedUserId: string, publicKey: string) => { - try { - const PRIVATE_KEY = localStorage.getItem("PRIVATE_KEY") as string; - if (!PRIVATE_KEY || !wsKey) return; - - // assymmetrically decrypt symmetric key with local private key - const key = decryptAssymmetric({ - ciphertext: wsKey.encryptedKey, - nonce: wsKey.nonce, - publicKey: wsKey.sender.publicKey, - privateKey: PRIVATE_KEY - }); - - const { ciphertext, nonce } = encryptAssymmetric({ - plaintext: key, - publicKey, - privateKey: PRIVATE_KEY - }); - - await uploadWsKey.mutateAsync({ - userId: grantedUserId, - nonce, - encryptedKey: ciphertext, - workspaceId: currentWorkspace?._id || "" - }); - } catch (err) { - console.error(err); - createNotification({ - text: "Failed to grant access to user", - type: "error" - }); - } - }; - - const copyTokenToClipboard = () => { - navigator.clipboard.writeText(completeInviteLink as string); - setInviteLinkCopied.on(); - }; - - const isLoading = isMembersLoading || IsWsMembershipLoading || isRolesLoading; - - return ( -
-
-
- setSearchMemberFilter(e.target.value)} - leftIcon={} - placeholder="Search members..." - /> -
- - {(isAllowed) => ( - - )} - -
-
- - - - - - - - - - - - {isLoading && } - {!isLoading && - filterdUser?.map( - ({ user: u, inviteEmail, role, customRole, _id: orgMembershipId, status }) => { - const name = u ? `${u.firstName} ${u.lastName}` : "-"; - const email = u?.email || inviteEmail; - const userWs = workspaceMemberships?.[u?._id]; - - return ( - - - - - - - - ); - } - )} - -
NameEmailRoleProjects -
{name}{email} - - {(isAllowed) => ( - <> - {status === "accepted" && ( - - )} - {(status === "invited" || status === "verified") && - serverDetails?.emailConfigured && ( - - )} - {status === "completed" && ( - - )} - - )} - - - {userWs ? ( - userWs?.map(({ name: wsName, _id }) => ( - - {wsName} - - )) - ) : ( -
- {(status === "invited" || status === "verified") && - serverDetails?.emailConfigured ? ( - - This user hasn't accepted the invite yet - - ) : ( - - This user isn't part of any projects yet - - )} - {router.query.id !== "undefined" && - !( - (status === "invited" || status === "verified") && - serverDetails?.emailConfigured - ) && ( - - )} -
- )} -
- {userId !== u?._id && ( - - {(isAllowed) => ( - - handlePopUpOpen("removeMember", { id: orgMembershipId }) - } - > - - - )} - - )} -
- {!isLoading && filterdUser?.length === 0 && ( - - )} -
-
- { - handlePopUpToggle("addMember", isOpen); - setCompleteInviteLink(undefined); - }} - > - - {!completeInviteLink && ( -
- An invite is specific to an email address and expires after 1 day. -
- For security reasons, you will need to separately add members to projects. -
- )} - {completeInviteLink && - "This Infisical instance does not have a email provider setup. Please share this invite link with the invitee manually"} -
- } - > - {!completeInviteLink && ( -
- ( - - - - )} - /> -
- - -
- - )} - {completeInviteLink && ( -
-

{completeInviteLink}

- - - - click to copy - - -
- )} - - - handlePopUpToggle("removeMember", isOpen)} - onDeleteApproved={onRemoveOrgMemberApproved} - /> - handlePopUpToggle("upgradePlan", isOpen)} - text="You can add custom environments if you switch to Infisical's Team plan." - /> - handlePopUpToggle("setUpEmail", isOpen)} - /> - - ); -}; diff --git a/frontend/src/views/Org/MembersPage/components/OrgMembersTable/index.tsx b/frontend/src/views/Org/MembersPage/components/OrgMembersTable/index.tsx deleted file mode 100644 index a3d56f438..000000000 --- a/frontend/src/views/Org/MembersPage/components/OrgMembersTable/index.tsx +++ /dev/null @@ -1 +0,0 @@ -export { OrgMembersTable } from "./OrgMembersTable"; diff --git a/frontend/src/views/Org/MembersPage/components/OrgServiceTokenTab/components/ServiceTokenV3Section/AddServiceTokenV3Modal.tsx b/frontend/src/views/Org/MembersPage/components/OrgServiceTokenTab/components/ServiceTokenV3Section/AddServiceTokenV3Modal.tsx index 87e36c53a..7ba34feb5 100644 --- a/frontend/src/views/Org/MembersPage/components/OrgServiceTokenTab/components/ServiceTokenV3Section/AddServiceTokenV3Modal.tsx +++ b/frontend/src/views/Org/MembersPage/components/OrgServiceTokenTab/components/ServiceTokenV3Section/AddServiceTokenV3Modal.tsx @@ -154,6 +154,7 @@ export const AddServiceTokenV3Modal = ({ if (serviceTokenData) { reset({ name: serviceTokenData.name, + expiresIn: "", role: serviceTokenData?.customRole?.slug ?? serviceTokenData.role, trustedIps: serviceTokenData.trustedIps.map(({ ipAddress, @@ -169,6 +170,7 @@ export const AddServiceTokenV3Modal = ({ } else { reset({ name: "", + expiresIn: "", accessTokenTTL: "7200", role: roles[0].slug, trustedIps: [{ @@ -312,7 +314,12 @@ export const AddServiceTokenV3Modal = ({ )} /> - +
+ +
+ )} /> - - - -
{tokenTrustedIps.map(({ id }, index) => (
{popUp?.serviceTokenV3?.data ? "Update" : "Create"} -
diff --git a/frontend/src/views/Org/MembersPage/components/OrgServiceTokenTab/components/ServiceTokenV3Section/ServiceTokenV3Section.tsx b/frontend/src/views/Org/MembersPage/components/OrgServiceTokenTab/components/ServiceTokenV3Section/ServiceTokenV3Section.tsx index a105a10d3..4bafbb09c 100644 --- a/frontend/src/views/Org/MembersPage/components/OrgServiceTokenTab/components/ServiceTokenV3Section/ServiceTokenV3Section.tsx +++ b/frontend/src/views/Org/MembersPage/components/OrgServiceTokenTab/components/ServiceTokenV3Section/ServiceTokenV3Section.tsx @@ -51,7 +51,7 @@ export const ServiceTokenV3Section = withPermission(

- Service Accounts (Beta) + Machines

{ const { createNotification } = useNotificationContext(); const { currentOrg } = useOrganization(); + const orgId = currentOrg?._id || ""; + const { mutateAsync: updateMutateAsync } = useUpdateServiceTokenV3(); const { data, isLoading } = useGetOrgServiceMemberships(currentOrg?._id || ""); - - const handleToggleServiceTokenDataStatus = async ({ + + const { data: roles } = useGetRoles({ + orgId + }); + + const handleChangeRole = async ({ serviceTokenDataId, - isActive + role }: { serviceTokenDataId: string; - isActive: boolean; + role: string; }) => { try { + await updateMutateAsync({ serviceTokenDataId, - isActive + role }); - + createNotification({ - text: `Successfully ${isActive ? "enabled" : "disabled"} service token v3`, + text: "Successfully updated service account role", type: "success" - }); - } catch (err) { - console.log(err); - createNotification({ - text: `Failed to ${isActive ? "enable" : "disable"} service token v3`, - type: "error" }); + } catch (err) { + console.error(err); + createNotification({ + text: "Failed to update service account role", + type: "error" + }); } } + + // const handleToggleStatus = async ({ + // serviceTokenDataId, + // isActive + // }: { + // serviceTokenDataId: string; + // isActive: boolean; + // }) => { + // try { + // await updateMutateAsync({ + // serviceTokenDataId, + // isActive + // }); + + // createNotification({ + // text: `Successfully ${isActive ? "enabled" : "disabled"} service token v3`, + // type: "success" + // }); + // } catch (err) { + // console.log(err); + // createNotification({ + // text: `Failed to ${isActive ? "enable" : "disable"} service token v3`, + // type: "error" + // }); + // } + // } + + const findRoleFromId = useCallback( + (roleId: string) => { + return (roles || []).find(({ _id: id }) => id === roleId); + }, + [roles] + ); return ( @@ -85,7 +128,7 @@ export const ServiceTokenV3Table = ({ Name - Status + {/* Status */} Role {/* Trusted IPs */} {/* Access Token TTL */} @@ -103,7 +146,7 @@ export const ServiceTokenV3Table = ({ service: { _id, name, - isActive, + // isActive, trustedIps, // createdAt, expiresAt, @@ -116,7 +159,7 @@ export const ServiceTokenV3Table = ({ return ( {name} - + {/* ( handleToggleServiceTokenDataStatus({ + onCheckedChange={(value) => handleToggleStatus({ serviceTokenDataId: _id, isActive: value })} @@ -135,8 +178,39 @@ export const ServiceTokenV3Table = ({ )} - - {customRole?.slug ?? role} + */} + + + {(isAllowed) => { + return ( + + ); + }} + + {/* {trustedIps.map(({ _id: trustedIpId, @@ -211,7 +285,7 @@ export const ServiceTokenV3Table = ({ {!isLoading && data && data?.length === 0 && ( - + )} diff --git a/frontend/src/views/Org/MembersPage/components/index.tsx b/frontend/src/views/Org/MembersPage/components/index.tsx new file mode 100644 index 000000000..16f6a981b --- /dev/null +++ b/frontend/src/views/Org/MembersPage/components/index.tsx @@ -0,0 +1,3 @@ +export { OrgMembersTab } from "./OrgMembersTab"; +export { OrgRoleTabSection } from "./OrgRoleTabSection"; +export { OrgServiceTokenTab } from "./OrgServiceTokenTab"; \ No newline at end of file diff --git a/frontend/src/views/Project/MembersPage/components/ServiceAccountTab/components/ServiceTokenV3Section/AddServiceTokenV3Modal.tsx b/frontend/src/views/Project/MembersPage/components/ServiceAccountTab/components/ServiceTokenV3Section/AddServiceTokenV3Modal.tsx index fee5de2e5..124439b50 100644 --- a/frontend/src/views/Project/MembersPage/components/ServiceAccountTab/components/ServiceTokenV3Section/AddServiceTokenV3Modal.tsx +++ b/frontend/src/views/Project/MembersPage/components/ServiceAccountTab/components/ServiceTokenV3Section/AddServiceTokenV3Modal.tsx @@ -19,12 +19,13 @@ import { } from "@app/context"; import { useAddServiceToWorkspace , useGetOrgServiceMemberships, - useGetWorkspaceServiceMemberships -} from "@app/hooks/api"; + useGetRoles, + useGetWorkspaceServiceMemberships} from "@app/hooks/api"; import { UsePopUpState } from "@app/hooks/usePopUp"; const schema = yup.object({ - serviceTokenDataId: yup.string() + serviceTokenDataId: yup.string().required("ST V3 id is required"), + role: yup.string().required("ST V3 role is required") }).required(); export type FormData = yup.InferType; @@ -38,6 +39,7 @@ export const AddServiceTokenV3Modal = ({ popUp, handlePopUpToggle }: Props) => { + const { createNotification } = useNotificationContext(); const { currentOrg } = useOrganization(); const { currentWorkspace } = useWorkspace(); @@ -47,6 +49,11 @@ export const AddServiceTokenV3Modal = ({ const { data: orgServices } = useGetOrgServiceMemberships(orgId); const { data: services } = useGetWorkspaceServiceMemberships(workspaceId); + const { data: roles } = useGetRoles({ + orgId, + workspaceId + }); + const addServiceToWorkspace = useAddServiceToWorkspace(); const filteredOrgServices = useMemo(() => { @@ -71,18 +78,19 @@ export const AddServiceTokenV3Modal = ({ }); const onFormSubmit = async ({ - serviceTokenDataId + serviceTokenDataId, + role }: FormData) => { - try { + await addServiceToWorkspace.mutateAsync({ workspaceId, serviceId: serviceTokenDataId as string, - role: "member" + role }); createNotification({ - text: `Successfully ${popUp?.serviceTokenV3?.data ? "updated" : "created"} ST V3`, + text: "Successfully added service account to project", type: "success" }); @@ -91,7 +99,7 @@ export const AddServiceTokenV3Modal = ({ } catch (err) { console.error(err); createNotification({ - text: `Failed to ${popUp?.serviceTokenV3?.data ? "updated" : "created"} ST V3`, + text: "Failed to add service account to project", type: "error" }); } @@ -117,7 +125,6 @@ export const AddServiceTokenV3Modal = ({ label="Service Account" errorText={error?.message} isError={Boolean(error)} - className="mt-4" > onChange(e)} + className="w-full" + > + {(roles || []).map(({ name, slug }) => ( + + {name} + + ))} + + + )} + />