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) => (
+ }
+ onClick={() => handleAddMemberModal()}
+ isDisabled={!isAllowed}
+ >
+ Add Member
+
+ )}
+
+
+
+
+ {/*
*/}
+
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..."
+ />
+
+
+
+
+ Name
+ Email
+ Role
+
+
+
+
+ {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 (
+
+ {name}
+ {email}
+
+
+ {(isAllowed) => (
+ <>
+ {status === "accepted" && (
+
+ onRoleChange(orgMembershipId, selectedRole)
+ }
+ >
+ {(roles || [])
+ .filter(({ slug }) =>
+ slug === "owner" ? isIamOwner || role === "owner" : true
+ )
+ .map(({ slug, name: roleName }) => (
+
+ {roleName}
+
+ ))}
+
+ )}
+ {(status === "invited" || status === "verified") &&
+ serverDetails?.emailConfigured && (
+ onResendInvite(email)}
+ >
+ Resend Invite
+
+ )}
+ >
+ )}
+
+
+
+ {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) => (
- }
- onClick={() => {
- if (!isLoadingSSOConfig && ssoConfig && ssoConfig.isActive) {
- createNotification({
- text: "You cannot invite users when SAML SSO is configured for your organization",
- type: "error"
- });
-
- return;
- }
-
- if (isMoreUsersNotAllowed) {
- handlePopUpOpen("upgradePlan");
- } else {
- handlePopUpOpen("addMember");
- }
- }}
- >
- Add Member
-
- )}
-
-
-
-
-
-
-
- Name
- Email
- Role
- Projects
-
-
-
-
- {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 (
-
- {name}
- {email}
-
-
- {(isAllowed) => (
- <>
- {status === "accepted" && (
-
- onRoleChange(orgMembershipId, selectedRole)
- }
- >
- {(roles || [])
- .filter(({ slug }) =>
- slug === "owner" ? isIamOwner || role === "owner" : true
- )
- .map(({ slug, name: roleName }) => (
-
- {roleName}
-
- ))}
-
- )}
- {(status === "invited" || status === "verified") &&
- serverDetails?.emailConfigured && (
- onAddUserToOrg(email)}
- >
- Resend Invite
-
- )}
- {status === "completed" && (
- onGrantAccess(u?._id, u?.publicKey)}
- >
- Grant Access
-
- )}
- >
- )}
-
-
-
- {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
- ) && (
-
- router.push(`/project/${workspaces[0]?._id}/members`)
- }
- className="w-max cursor-pointer rounded-sm bg-mineshaft px-1.5 py-0.5 text-sm duration-200 hover:bg-primary hover:text-black"
- >
-
- Add to projects
-
- )}
-
- )}
-
-
- {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"}
-
+ handlePopUpToggle("serviceTokenV3", false)}
+ >
Cancel
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 (
+
+ handleChangeRole({
+ serviceTokenDataId: _id,
+ role: selectedRole
+ })
+ }
+ >
+ {(roles || [])
+ .map(({ slug, name: roleName }) => (
+
+ {roleName}
+
+ ))}
+
+ );
+ }}
+
+
{/*
{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}
+
+ ))}
+
+
+ )}
+ />
- Service Accounts (Beta)
+ Service Accounts
void;
- };
+};
+// TODO: update roles thing here
export const ServiceTokenV3Table = ({
handlePopUpOpen
}: Props) => {
+ const { createNotification } = useNotificationContext();
+ const { currentOrg } = useOrganization();
const { currentWorkspace } = useWorkspace();
+ const orgId = currentOrg?._id || "";
+ const workspaceId = currentWorkspace?._id || "";
+
const { data, isLoading } = useGetWorkspaceServiceMemberships(currentWorkspace?._id || "");
+
+ const { data: roles } = useGetRoles({
+ orgId,
+ workspaceId
+ });
+
+ const handleChangeRole = async ({
+ serviceTokenDataId,
+ role
+ }: {
+ serviceTokenDataId: string;
+ role: string;
+ }) => {
+
+ try {
+
+ console.log("handle project-level role change vals: ", {
+ serviceTokenDataId,
+ role
+ });
+
+ // await updateMutateAsync({
+ // serviceTokenDataId,
+ // role
+ // });
+
+ createNotification({
+ text: "Successfully updated service account role",
+ type: "success"
+ });
+ } catch (err) {
+ console.error(err);
+ createNotification({
+ text: "Failed to update service account role",
+ type: "error"
+ });
+ }
+ }
+
+ const findRoleFromId = useCallback(
+ (roleId: string) => {
+ return (roles || []).find(({ _id: id }) => id === roleId);
+ },
+ [roles]
+ );
return (
@@ -72,7 +134,38 @@ export const ServiceTokenV3Table = ({
return (
{name}
- {customRole?.slug ?? role}
+
+
+ {(isAllowed) => {
+ return (
+
+ handleChangeRole({
+ serviceTokenDataId: _id,
+ role: selectedRole
+ })
+ }
+ >
+ {(roles || [])
+ .map(({ slug, name: roleName }) => (
+
+ {roleName}
+
+ ))}
+
+ );
+ }}
+
+
{format(new Date(createdAt), "yyyy-MM-dd")}
-
+
)}