diff --git a/frontend/src/components/basic/table/ProjectUsersTable.tsx b/frontend/src/components/basic/table/ProjectUsersTable.tsx
index 27346195c..438517aa5 100644
--- a/frontend/src/components/basic/table/ProjectUsersTable.tsx
+++ b/frontend/src/components/basic/table/ProjectUsersTable.tsx
@@ -30,7 +30,7 @@ type Props = {
type EnvironmentProps = {
name: string;
slug: string;
-}
+};
/**
* This is the component that shows the users of a certin project
@@ -91,26 +91,38 @@ const ProjectUsersTable = ({ userData, changeData, myUser, filter }: Props) => {
});
};
- const handlePermissionUpdate = (index: number, val: string, membershipId: string, slug: string ) => {
- let denials: { ability: string; environmentSlug: string; }[];
- if (val === "Read Only") {
- denials = [{
- ability: "write",
- environmentSlug: slug
- }];
- } else if (val === "No Access") {
- denials = [{
- ability: "write",
- environmentSlug: slug
- }, {
- ability: "read",
- environmentSlug: slug
- }];
- } else if (val === "Add Only") {
- denials = [{
- ability: "read",
- environmentSlug: slug
- }];
+ const handlePermissionUpdate = (
+ index: number,
+ val: string,
+ membershipId: string,
+ slug: string
+ ) => {
+ let denials: { ability: string; environmentSlug: string }[];
+ if (val === 'Read Only') {
+ denials = [
+ {
+ ability: 'write',
+ environmentSlug: slug
+ }
+ ];
+ } else if (val === 'No Access') {
+ denials = [
+ {
+ ability: 'write',
+ environmentSlug: slug
+ },
+ {
+ ability: 'read',
+ environmentSlug: slug
+ }
+ ];
+ } else if (val === 'Add Only') {
+ denials = [
+ {
+ ability: 'read',
+ environmentSlug: slug
+ }
+ ];
} else {
denials = [];
}
@@ -118,8 +130,12 @@ const ProjectUsersTable = ({ userData, changeData, myUser, filter }: Props) => {
if (currentPlan !== plans.professional && host === 'https://app.infisical.com') {
setIsUpgradeModalOpen(true);
} else {
- const allDenials = userData[index].deniedPermissions.filter((perm: { ability: string; environmentSlug: string; }) => perm.environmentSlug !== slug).concat(denials);
- updateUserProjectPermission({ membershipId, denials: allDenials});
+ const allDenials = userData[index].deniedPermissions
+ .filter(
+ (perm: { ability: string; environmentSlug: string }) => perm.environmentSlug !== slug
+ )
+ .concat(denials);
+ updateUserProjectPermission({ membershipId, denials: allDenials });
changeData([
...userData.slice(0, index),
...[
@@ -156,7 +172,7 @@ const ProjectUsersTable = ({ userData, changeData, myUser, filter }: Props) => {
orgId
});
if (subscriptions) {
- setCurrentPlan(subscriptions.data[0].plan.product)
+ setCurrentPlan(subscriptions.data[0].plan.product);
}
})();
}, [userData, myUser]);
@@ -186,25 +202,28 @@ const ProjectUsersTable = ({ userData, changeData, myUser, filter }: Props) => {
const closeUpgradeModal = () => {
setIsUpgradeModalOpen(false);
- }
+ };
return (
-
-
+
+
-
-
+
+
- NAME
- EMAIL
- ROLE
- {workspaceEnvs.map(env => (
-
- {env.slug.toUpperCase()}
+ NAME
+ EMAIL
+ ROLE
+ {workspaceEnvs.map((env) => (
+
+
+ {env.slug.toUpperCase()}
+
+
{/* PERMISSION */}
))}
@@ -227,28 +246,28 @@ const ProjectUsersTable = ({ userData, changeData, myUser, filter }: Props) => {
)
.map((row, index) => (
-
+
{row.firstName} {row.lastName}
-
+
{row.email}
-
-
-
+
+
handleRoleUpdate(index, e)}
value={row.role}
- disabled={myRole !== 'admin' || myUser === row.email}
+ isDisabled={myRole !== 'admin' || myUser === row.email}
// onOpenChange={(open) => setIsOpen(open)}
>
Admin
Member
{row.status === 'completed' && myUser !== row.email && (
-
+
grantAccess(row.userId, row.publicKey)}
color="mineshaft"
@@ -259,43 +278,106 @@ const ProjectUsersTable = ({ userData, changeData, myUser, filter }: Props) => {
)}
- {workspaceEnvs.map((env) =>
- handlePermissionUpdate(index, val, row.membershipId, env.slug)}
- value={
- // eslint-disable-next-line no-nested-ternary
- (row.deniedPermissions.filter((perm: any) => perm.environmentSlug === env.slug).map((perm: {ability: string}) => perm.ability).includes("write") && row.deniedPermissions.filter((perm: any) => perm.environmentSlug === env.slug).map((perm: {ability: string}) => perm.ability).includes("read"))
- ? "No Access"
- // eslint-disable-next-line no-nested-ternary
- : (row.deniedPermissions.filter((perm: any) => perm.environmentSlug === env.slug).map((perm: {ability: string}) => perm.ability).includes("write") && !row.deniedPermissions.filter((perm: any) => perm.environmentSlug === env.slug).map((perm: {ability: string}) => perm.ability).includes("read") ? "Read Only"
- : !row.deniedPermissions.filter((perm: any) => perm.environmentSlug === env.slug).map((perm: {ability: string}) => perm.ability).includes("write") && row.deniedPermissions.filter((perm: any) => perm.environmentSlug === env.slug).map((perm: {ability: string}) => perm.ability).includes("read") ? "Add Only" : "Read & Write")
- }
- icon={
- // eslint-disable-next-line no-nested-ternary
- (row.deniedPermissions.filter((perm: any) => perm.environmentSlug === env.slug).map((perm: {ability: string}) => perm.ability).includes("write") && row.deniedPermissions.filter((perm: any) => perm.environmentSlug === env.slug).map((perm: {ability: string}) => perm.ability).includes("read"))
- ? faEyeSlash
- // eslint-disable-next-line no-nested-ternary
- : (row.deniedPermissions.filter((perm: any) => perm.environmentSlug === env.slug).map((perm: {ability: string}) => perm.ability).includes("write") && !row.deniedPermissions.filter((perm: any) => perm.environmentSlug === env.slug).map((perm: {ability: string}) => perm.ability).includes("read") ? faEye
- : !row.deniedPermissions.filter((perm: any) => perm.environmentSlug === env.slug).map((perm: {ability: string}) => perm.ability).includes("write") && row.deniedPermissions.filter((perm: any) => perm.environmentSlug === env.slug).map((perm: {ability: string}) => perm.ability).includes("read") ? faPlus : faPenToSquare)
- }
- disabled={myRole !== 'admin'}
- // onOpenChange={(open) => setIsOpen(open)}
+ {workspaceEnvs.map((env) => (
+
- No Access
- Read Only
- Add Only
- Read & Write
-
- )}
-
+
+ handlePermissionUpdate(index, val, row.membershipId, env.slug)
+ }
+ value={
+ // eslint-disable-next-line no-nested-ternary
+ row.deniedPermissions
+ .filter((perm: any) => perm.environmentSlug === env.slug)
+ .map((perm: { ability: string }) => perm.ability)
+ .includes('write') &&
+ row.deniedPermissions
+ .filter((perm: any) => perm.environmentSlug === env.slug)
+ .map((perm: { ability: string }) => perm.ability)
+ .includes('read')
+ ? 'No Access'
+ : // eslint-disable-next-line no-nested-ternary
+ row.deniedPermissions
+ .filter((perm: any) => perm.environmentSlug === env.slug)
+ .map((perm: { ability: string }) => perm.ability)
+ .includes('write') &&
+ !row.deniedPermissions
+ .filter((perm: any) => perm.environmentSlug === env.slug)
+ .map((perm: { ability: string }) => perm.ability)
+ .includes('read')
+ ? 'Read Only'
+ : !row.deniedPermissions
+ .filter((perm: any) => perm.environmentSlug === env.slug)
+ .map((perm: { ability: string }) => perm.ability)
+ .includes('write') &&
+ row.deniedPermissions
+ .filter((perm: any) => perm.environmentSlug === env.slug)
+ .map((perm: { ability: string }) => perm.ability)
+ .includes('read')
+ ? 'Add Only'
+ : 'Read & Write'
+ }
+ icon={
+ // eslint-disable-next-line no-nested-ternary
+ row.deniedPermissions
+ .filter((perm: any) => perm.environmentSlug === env.slug)
+ .map((perm: { ability: string }) => perm.ability)
+ .includes('write') &&
+ row.deniedPermissions
+ .filter((perm: any) => perm.environmentSlug === env.slug)
+ .map((perm: { ability: string }) => perm.ability)
+ .includes('read')
+ ? faEyeSlash
+ : // eslint-disable-next-line no-nested-ternary
+ row.deniedPermissions
+ .filter((perm: any) => perm.environmentSlug === env.slug)
+ .map((perm: { ability: string }) => perm.ability)
+ .includes('write') &&
+ !row.deniedPermissions
+ .filter((perm: any) => perm.environmentSlug === env.slug)
+ .map((perm: { ability: string }) => perm.ability)
+ .includes('read')
+ ? faEye
+ : !row.deniedPermissions
+ .filter((perm: any) => perm.environmentSlug === env.slug)
+ .map((perm: { ability: string }) => perm.ability)
+ .includes('write') &&
+ row.deniedPermissions
+ .filter((perm: any) => perm.environmentSlug === env.slug)
+ .map((perm: { ability: string }) => perm.ability)
+ .includes('read')
+ ? faPlus
+ : faPenToSquare
+ }
+ isDisabled={myRole !== 'admin'}
+ // onOpenChange={(open) => setIsOpen(open)}
+ >
+
+ No Access
+
+
+ Read Only
+
+
+ Add Only
+
+
+ Read & Write
+
+
+
+ ))}
+
{myUser !== row.email &&
// row.role !== "admin" &&
myRole !== 'member' ? (
-
+
handleDelete(row.membershipId, index)}
color="red"
@@ -304,7 +386,7 @@ const ProjectUsersTable = ({ userData, changeData, myUser, filter }: Props) => {
/>
) : (
-
+
)}
diff --git a/frontend/src/pages/settings/org/[id].tsx b/frontend/src/pages/settings/org/[id].tsx
index 562c45c9b..59c8c373c 100644
--- a/frontend/src/pages/settings/org/[id].tsx
+++ b/frontend/src/pages/settings/org/[id].tsx
@@ -1,374 +1,21 @@
/* eslint-disable @typescript-eslint/no-unused-vars */
-import { useEffect, useState } from 'react';
import Head from 'next/head';
-import { useRouter } from 'next/router';
import { useTranslation } from 'next-i18next';
-import { faCheck, faMagnifyingGlass, faPlus, faX } from '@fortawesome/free-solid-svg-icons';
-import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
-import { plans } from 'public/data/frequentConstants';
-import Button from '@app/components/basic/buttons/Button';
-import AddIncidentContactDialog from '@app/components/basic/dialog/AddIncidentContactDialog';
-import AddUserDialog from '@app/components/basic/dialog/AddUserDialog';
-import UpgradePlanModal from '@app/components/basic/dialog/UpgradePlan';
-import InputField from '@app/components/basic/InputField';
-import UserTable from '@app/components/basic/table/UserTable';
-import NavHeader from '@app/components/navigation/NavHeader';
-import guidGenerator from '@app/components/utilities/randomId';
import { getTranslatedServerSideProps } from '@app/components/utilities/withTranslateProps';
-
-import addUserToOrg from '../../api/organization/addUserToOrg';
-import deleteIncidentContact from '../../api/organization/deleteIncidentContact';
-import getIncidentContacts from '../../api/organization/getIncidentContacts';
-import getOrganization from '../../api/organization/GetOrg';
-import getOrganizationSubscriptions from '../../api/organization/GetOrgSubscription';
-import getOrganizationUsers from '../../api/organization/GetOrgUsers';
-import renameOrg from '../../api/organization/renameOrg';
-import getUser from '../../api/user/getUser';
-import deleteWorkspace from '../../api/workspace/deleteWorkspace';
-import getWorkspaces from '../../api/workspace/getWorkspaces';
+import { OrgSettingsPage } from '@app/views/Settings/OrgSettingsPage';
export default function SettingsOrg() {
- const [buttonReady, setButtonReady] = useState(false);
- const router = useRouter();
- const host = window.location.origin;
- const [orgName, setOrgName] = useState('');
- const [emailUser, setEmailUser] = useState('');
- const [workspaceToBeDeletedName, setWorkspaceToBeDeletedName] = useState('');
- const [searchUsers, setSearchUsers] = useState('');
- const [isAddIncidentContactOpen, setIsAddIncidentContactOpen] = useState(false);
- const [isAddUserOpen, setIsAddUserOpen] = useState(router.asPath.split('?')[1] === 'invite');
- const [incidentContacts, setIncidentContacts] = useState([]);
- const [searchIncidentContact, setSearchIncidentContact] = useState('');
- const [userList, setUserList] = useState([]);
- const [personalEmail, setPersonalEmail] = useState('');
- const [email, setEmail] = useState('');
- const [currentPlan, setCurrentPlan] = useState('');
-
- const workspaceId = router.query.id as string;
-
const { t } = useTranslation();
- useEffect(() => {
- (async () => {
- const orgId = localStorage.getItem('orgData.id') as string;
- const org = await getOrganization({
- orgId
- });
-
- setOrgName(org.name);
- const incidentContactsData = await getIncidentContacts(
- localStorage.getItem('orgData.id') as string
- );
-
- setIncidentContacts(incidentContactsData?.map((contact) => contact.email));
-
- const user = await getUser();
- setPersonalEmail(user.email);
-
- const orgUsers = await getOrganizationUsers({
- orgId
- });
-
- setUserList(
- orgUsers.map((orgUser) => ({
- key: guidGenerator(),
- firstName: orgUser.user?.firstName,
- lastName: orgUser.user?.lastName,
- email: orgUser.user?.email == null ? orgUser.inviteEmail : orgUser.user?.email,
- role: orgUser?.role,
- status: orgUser?.status,
- userId: orgUser.user?._id,
- membershipId: orgUser._id,
- publicKey: orgUser.user?.publicKey
- }))
- );
-
- const subscriptions = await getOrganizationSubscriptions({
- orgId
- });
- if (subscriptions) {
- setCurrentPlan(subscriptions.data[0].plan.product);
- }
- })();
- }, []);
-
- const modifyOrgName = (newName: string) => {
- setButtonReady(true);
- setOrgName(newName);
- };
-
- const submitChanges = (newOrgName: string) => {
- renameOrg(localStorage.getItem('orgData.id') as string, newOrgName);
- setButtonReady(false);
- };
-
- const closeAddUserModal = () => {
- setIsAddUserOpen(false);
- };
-
- const closeAddIncidentContactModal = () => {
- setIsAddIncidentContactOpen(false);
- };
-
- const openAddUserModal = () => {
- setIsAddUserOpen(true);
- };
-
- const openAddIncidentContactModal = () => {
- setIsAddIncidentContactOpen(true);
- };
-
- const submitAddUserModal = async (newUserEmail: string) => {
- await addUserToOrg(newUserEmail, localStorage.getItem('orgData.id') as string);
- setEmail('');
- setIsAddUserOpen(false);
- router.reload();
- };
-
- const deleteIncidentContactFully = (incidentContact: string) => {
- setIncidentContacts(incidentContacts.filter((contact) => contact !== incidentContact));
- deleteIncidentContact(localStorage.getItem('orgData.id') as string, incidentContact);
- };
-
- /**
- * This function deleted a workspace.
- * It first checks if there is more than one workspace aviable. Otherwise, it doesn't delete
- * It then checks if the name of the workspace to be deleted is correct. Otherwise, it doesn't delete.
- * It then deletes the workspace and forwards the user to another aviable workspace.
- */
- const executeDeletingWorkspace = async () => {
- const userWorkspaces = await getWorkspaces();
-
- if (userWorkspaces.length > 1) {
- if (
- userWorkspaces.filter((workspace) => workspace._id === workspaceId)[0].name ===
- workspaceToBeDeletedName
- ) {
- await deleteWorkspace(workspaceId);
- const ws = await getWorkspaces();
- router.push(`/dashboard/${ws[0]._id}`);
- }
- }
- };
-
return (
-
+ <>
{t('common:head-title', { title: t('settings-org:title') })}
-
-
-
-
-
-
-
{t('settings-org:title')}
-
- {t('settings-org:description')}
-
-
-
-
-
-
-
-
-
- {t('common:display-name')}
-
-
-
-
-
- submitChanges(orgName)}
- color="mineshaft"
- size="md"
- active={buttonReady}
- iconDisabled={faCheck}
- textDisabled={t('common:saved') as string}
- />
-
-
-
-
-
-
-
- {t('section-members:org-members')}
-
-
- {t('section-members:org-members-description')}
-
-
-
= 5 && currentPlan === plans.starter && host === 'https://app.infisical.com'}
- onClose={closeAddUserModal}
- text="You can add more members if you switch to Infisical's Team plan."
- />
- {/* */}
-
-
-
- setSearchUsers(e.target.value)}
- placeholder={t('section-members:search-members') as string}
- />
-
-
-
-
-
- {userList && (
-
-
-
- )}
-
-
-
-
-
-
- {t('section-incident:incident-contacts')}
-
-
- {t('section-incident:incident-contacts-description')}
-
-
-
-
-
-
-
-
- setSearchIncidentContact(e.target.value)}
- placeholder={t('common:search') as string}
- />
-
- {incidentContacts?.filter((incidentEmail) =>
- incidentEmail.includes(searchIncidentContact)
- ).length > 0 ? (
- incidentContacts
- .filter((incidentEmail) => incidentEmail.includes(searchIncidentContact))
- .map((contact) => (
-
-
{contact}
-
- deleteIncidentContactFully(contact)}
- color="red"
- size="icon-sm"
- icon={faX}
- />
-
-
- ))
- ) : (
-
-
{t('section-incident:no-incident-contacts')}
-
- )}
-
-
- {/*
-
- Danger Zone
-
-
- As soon as you delete an organization, you will
- not be able to undo it. This will immediately
- remove all organization members and cancel your
- subscription. If you still want to do that,
- please enter the name of the organization below.
-
-
-
-
-
- Delete Project
-
-
- Note: You can only delete a project in case you
- have more than one.
-
-
*/}
-
-
-
-
+
+ >
);
}
diff --git a/frontend/src/views/Settings/OrgSettingsPage/OrgSettingsPage.tsx b/frontend/src/views/Settings/OrgSettingsPage/OrgSettingsPage.tsx
new file mode 100644
index 000000000..26c06a9d2
--- /dev/null
+++ b/frontend/src/views/Settings/OrgSettingsPage/OrgSettingsPage.tsx
@@ -0,0 +1,308 @@
+/* eslint-disable @typescript-eslint/no-unused-vars */
+import { useTranslation } from 'next-i18next';
+import { plans } from 'public/data/frequentConstants';
+
+import { useNotificationContext } from '@app/components/context/Notifications/NotificationProvider';
+import NavHeader from '@app/components/navigation/NavHeader';
+import {
+ decryptAssymmetric,
+ encryptAssymmetric
+} from '@app/components/utilities/cryptography/crypto';
+import { useOrganization, useSubscription, useUser, useWorkspace } from '@app/context';
+import {
+ useAddIncidentContact,
+ useAddUserToOrg,
+ useDeleteIncidentContact,
+ useDeleteOrgMembership,
+ useGetOrgIncidentContact,
+ useGetOrgUsers,
+ useGetUserWorkspaceMemberships,
+ useGetUserWsKey,
+ useRenameOrg,
+ useUpdateOrgUserRole,
+ useUploadWsKey
+} from '@app/hooks/api';
+
+import { OrgIncidentContactsTable, OrgMembersTable, OrgNameChangeSection } from './components';
+
+export const OrgSettingsPage = () => {
+ const host = window.location.origin;
+
+ const { t } = useTranslation();
+ const { currentOrg } = useOrganization();
+ const { currentWorkspace } = useWorkspace();
+ const { user } = useUser();
+ const { subscriptionPlan } = useSubscription();
+ const { createNotification } = useNotificationContext();
+
+ const orgId = currentOrg?._id || '';
+ const { data: orgUsers } = useGetOrgUsers(orgId);
+ const { data: workspaceMemberships } = useGetUserWorkspaceMemberships(orgId);
+ const { data: wsKey } = useGetUserWsKey(currentWorkspace?._id || '');
+ const { data: incidentContact } = useGetOrgIncidentContact(orgId);
+
+ const renameOrg = useRenameOrg();
+ const removeUserOrgMembership = useDeleteOrgMembership();
+ const addUserToOrg = useAddUserToOrg();
+ const updateOrgUserRole = useUpdateOrgUserRole();
+ const uploadWsKey = useUploadWsKey();
+ const addIncidentContact = useAddIncidentContact();
+ const removeIncidentContact = useDeleteIncidentContact();
+
+ const isMoreUsersNotAllowed =
+ (orgUsers || []).length >= 5 &&
+ subscriptionPlan === plans.starter &&
+ host === 'https://app.infisical.com';
+
+ const onRenameOrg = async (name: string) => {
+ if (!currentOrg?._id) return;
+
+ try {
+ await renameOrg.mutateAsync({ orgId: currentOrg?._id, newOrgName: name });
+ createNotification({
+ text: 'Successfully renamed organization',
+ type: 'success'
+ });
+ } catch (error) {
+ console.error(error);
+ createNotification({
+ text: 'Failed to rename organization',
+ type: 'error'
+ });
+ }
+ };
+
+ const onRemoveUserOrgMembership = async (membershipId: string) => {
+ if (!currentOrg?._id) return;
+
+ try {
+ await removeUserOrgMembership.mutateAsync({ orgId: currentOrg?._id, membershipId });
+ createNotification({
+ text: 'Successfully removed used from org',
+ type: 'success'
+ });
+ } catch (error) {
+ console.error(error);
+ createNotification({
+ text: 'Failed to remove user from org',
+ type: 'error'
+ });
+ }
+ };
+ const onAddUserToOrg = async (email: string) => {
+ if (!currentOrg?._id) return;
+
+ try {
+ await addUserToOrg.mutateAsync({ organizationId: currentOrg?._id, inviteeEmail: email });
+ createNotification({
+ text: 'Successfully invited user to org',
+ type: 'success'
+ });
+ } catch (error) {
+ console.error(error);
+ createNotification({
+ text: 'Failed to invite user to org',
+ type: 'error'
+ });
+ }
+ };
+
+ const onUpdateOrgUserRole = 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 onGrantUserAccess = async (userId: 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,
+ nonce,
+ encryptedKey: ciphertext,
+ workspaceId: currentWorkspace?._id || ''
+ });
+ } catch (err) {
+ console.error(err);
+ createNotification({
+ text: 'Failed to grant access to user',
+ type: 'error'
+ });
+ }
+ };
+
+ const onAddIncidentContact = async (email: string) => {
+ if (!currentOrg?._id) return;
+
+ try {
+ await addIncidentContact.mutateAsync({ orgId, email });
+ createNotification({
+ text: 'Successfully added incident contact',
+ type: 'success'
+ });
+ } catch (error) {
+ console.error(error);
+ createNotification({
+ text: 'Failed to add incident contact',
+ type: 'error'
+ });
+ }
+ };
+
+ const onRemoveIncidentContact = async (email: string) => {
+ if (!currentOrg?._id) return;
+
+ try {
+ await removeIncidentContact.mutateAsync({ orgId, email });
+ createNotification({
+ text: 'Successfully removed incident contact',
+ type: 'success'
+ });
+ } catch (error) {
+ console.error(error);
+ createNotification({
+ text: 'Failed to remove incident contact',
+ type: 'error'
+ });
+ }
+ };
+
+ /**
+ * This function deleted a workspace.
+ * It first checks if there is more than one workspace aviable. Otherwise, it doesn't delete
+ * It then checks if the name of the workspace to be deleted is correct. Otherwise, it doesn't delete.
+ * It then deletes the workspace and forwards the user to another aviable workspace.
+ */
+ // const executeDeletingWorkspace = async () => {
+ // const userWorkspaces = await getWorkspaces();
+ //
+ // if (userWorkspaces.length > 1) {
+ // if (
+ // userWorkspaces.filter((workspace) => workspace._id === workspaceId)[0].name ===
+ // workspaceToBeDeletedName
+ // ) {
+ // await deleteWorkspace(workspaceId);
+ // const ws = await getWorkspaces();
+ // router.push(`/dashboard/${ws[0]._id}`);
+ // }
+ // }
+ // };
+ //
+ return (
+
+
+
+
+
{t('settings-org:title')}
+
+ {t('settings-org:description')}
+
+
+
+
+
+
+
+ {t('section-members:org-members')}
+
+
+ {t('section-members:org-members-description')}
+
+
+
+
+
+
+
+ {t('section-incident:incident-contacts')}
+
+
+ {t('section-incident:incident-contacts-description')}
+
+
+
+
+
+
+
+ {/*
+
+ Danger Zone
+
+
+ As soon as you delete an organization, you will
+ not be able to undo it. This will immediately
+ remove all organization members and cancel your
+ subscription. If you still want to do that,
+ please enter the name of the organization below.
+
+
+
+
+
+ Delete Project
+
+
+ Note: You can only delete a project in case you
+ have more than one.
+
+
*/}
+
+
+ );
+};
diff --git a/frontend/src/views/Settings/OrgSettingsPage/components/OrgIncidentContactsTable/OrgIncidentContactsTable.tsx b/frontend/src/views/Settings/OrgSettingsPage/components/OrgIncidentContactsTable/OrgIncidentContactsTable.tsx
new file mode 100644
index 000000000..296e5031d
--- /dev/null
+++ b/frontend/src/views/Settings/OrgSettingsPage/components/OrgIncidentContactsTable/OrgIncidentContactsTable.tsx
@@ -0,0 +1,171 @@
+import { useState } from 'react';
+import { Controller, useForm } from 'react-hook-form';
+import { faMagnifyingGlass, faPlus, faTrash } from '@fortawesome/free-solid-svg-icons';
+import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
+import { yupResolver } from '@hookform/resolvers/yup';
+import * as yup from 'yup';
+
+import {
+ Button,
+ DeleteActionModal,
+ FormControl,
+ IconButton,
+ Input,
+ Modal,
+ ModalContent,
+ Table,
+ TableContainer,
+ TBody,
+ Td,
+ Th,
+ THead,
+ Tr
+} from '@app/components/v2';
+import { usePopUp } from '@app/hooks';
+import { IncidentContact } from '@app/hooks/api/types';
+
+type Props = {
+ contacts?: IncidentContact[];
+ onRemoveContact: (email: string) => Promise;
+ onAddContact: (email: string) => Promise;
+};
+
+const addContactFormSchema = yup.object({
+ email: yup.string().email().required().label('Email').trim()
+});
+
+type TAddContactForm = yup.InferType;
+
+export const OrgIncidentContactsTable = ({
+ contacts = [],
+ onAddContact,
+ onRemoveContact
+}: Props) => {
+ const [searchContact, setSearchContact] = useState('');
+ const { handlePopUpToggle, popUp, handlePopUpOpen, handlePopUpClose } = usePopUp([
+ 'addContact',
+ 'removeContact'
+ ] as const);
+
+ const {
+ control,
+ handleSubmit,
+ reset,
+ formState: { isSubmitting }
+ } = useForm({ resolver: yupResolver(addContactFormSchema) });
+
+ const onAddIncidentContact = ({ email }: TAddContactForm) => {
+ onAddContact(email);
+ handlePopUpClose('addContact');
+ reset();
+ };
+
+ const onRemoveIncidentContact = async () => {
+ const incidentContactEmail = (popUp?.removeContact?.data as { email: string })?.email;
+ await onRemoveContact(incidentContactEmail);
+ handlePopUpClose('removeContact');
+ };
+
+ return (
+
+
+
+ setSearchContact(e.target.value)}
+ leftIcon={ }
+ placeholder="Search incident contact by email..."
+ />
+
+
+ }
+ onClick={() => handlePopUpOpen('addContact')}
+ >
+ Add Contact
+
+
+
+
+
+
+
+
+ Email
+
+
+
+
+ {contacts
+ ?.filter(({ email }) => email.toLocaleLowerCase().includes(searchContact))
+ ?.map(({ email }) => (
+
+ {email}
+
+ handlePopUpOpen('removeContact', { email })}
+ >
+
+
+
+
+ ))}
+
+
+
+
+
{
+ handlePopUpToggle('addContact', isOpen);
+ reset();
+ }}
+ >
+
+
+
+
+
handlePopUpToggle('removeContact', isOpen)}
+ onDeleteApproved={onRemoveIncidentContact}
+ />
+
+ );
+};
diff --git a/frontend/src/views/Settings/OrgSettingsPage/components/OrgIncidentContactsTable/index.tsx b/frontend/src/views/Settings/OrgSettingsPage/components/OrgIncidentContactsTable/index.tsx
new file mode 100644
index 000000000..8fbc24611
--- /dev/null
+++ b/frontend/src/views/Settings/OrgSettingsPage/components/OrgIncidentContactsTable/index.tsx
@@ -0,0 +1 @@
+export { OrgIncidentContactsTable } from './OrgIncidentContactsTable';
diff --git a/frontend/src/views/Settings/OrgSettingsPage/components/OrgMembersTable/OrgMembersTable.tsx b/frontend/src/views/Settings/OrgSettingsPage/components/OrgMembersTable/OrgMembersTable.tsx
new file mode 100644
index 000000000..e23bf42ba
--- /dev/null
+++ b/frontend/src/views/Settings/OrgSettingsPage/components/OrgMembersTable/OrgMembersTable.tsx
@@ -0,0 +1,274 @@
+import { useMemo, useState } from 'react';
+import { Controller, useForm } from 'react-hook-form';
+import { faMagnifyingGlass, faPlus, faTrash } from '@fortawesome/free-solid-svg-icons';
+import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
+import { yupResolver } from '@hookform/resolvers/yup';
+import * as yup from 'yup';
+
+import {
+ Button,
+ DeleteActionModal,
+ FormControl,
+ IconButton,
+ Input,
+ Modal,
+ ModalContent,
+ Select,
+ SelectItem,
+ Table,
+ TableContainer,
+ Tag,
+ TBody,
+ Td,
+ Th,
+ THead,
+ Tr,
+ UpgradePlanModal
+} from '@app/components/v2';
+import { usePopUp } from '@app/hooks';
+import { OrgUser, Workspace } from '@app/hooks/api/types';
+
+type Props = {
+ members?: OrgUser[];
+ workspaceMemberships?: Record;
+ orgName: string;
+ isMoreUserNotAllowed: boolean;
+ onRemoveMember: (userId: string) => Promise;
+ onInviteMember: (email: string) => Promise;
+ onRoleChange: (membershipId: string, role: string) => Promise;
+ onGrantAccess: (userId: string, publicKey: string) => Promise;
+ // the current user id to block remove org button
+ userId: string;
+};
+
+const addMemberFormSchema = yup.object({
+ email: yup.string().email().required().label('Email').trim()
+});
+
+type TAddMemberForm = yup.InferType;
+
+export const OrgMembersTable = ({
+ members = [],
+ workspaceMemberships = {},
+ orgName,
+ isMoreUserNotAllowed,
+ onRemoveMember,
+ onInviteMember,
+ onGrantAccess,
+ onRoleChange,
+ userId
+}: Props) => {
+ const [searchMemberFilter, setSearchMemberFilter] = useState('');
+ const { handlePopUpToggle, popUp, handlePopUpOpen, handlePopUpClose } = usePopUp([
+ 'addMember',
+ 'removeMember',
+ 'upgradePlan'
+ ] as const);
+
+ const {
+ control,
+ handleSubmit,
+ reset,
+ formState: { isSubmitting }
+ } = useForm({ resolver: yupResolver(addMemberFormSchema) });
+
+ const onAddMember = ({ email }: TAddMemberForm) => {
+ onInviteMember(email);
+ handlePopUpClose('addMember');
+ reset();
+ };
+
+ const onRemoveOrgMemberApproved = async () => {
+ const orgMembershipId = (popUp?.removeMember?.data as { id: string })?.id;
+ await onRemoveMember(orgMembershipId);
+ handlePopUpClose('removeMember');
+ };
+
+ const isIamOwner = useMemo(
+ () => members.find(({ user }) => userId === user?._id)?.role === 'owner',
+ [userId, members]
+ );
+
+ const filterdUser = useMemo(
+ () =>
+ members.filter(
+ ({ user, inviteEmail }) =>
+ user?.firstName?.toLowerCase().includes(searchMemberFilter) ||
+ user?.lastName?.toLowerCase().includes(searchMemberFilter) ||
+ user?.email?.toLowerCase().includes(searchMemberFilter) ||
+ inviteEmail?.includes(searchMemberFilter)
+ ),
+ [members, searchMemberFilter]
+ );
+
+ return (
+
+
+
+ setSearchMemberFilter(e.target.value)}
+ leftIcon={ }
+ placeholder="Search members..."
+ />
+
+
+ }
+ onClick={() => {
+ if (isMoreUserNotAllowed) {
+ handlePopUpOpen('upgradePlan');
+ } else {
+ handlePopUpOpen('addMember');
+ }
+ }}
+ >
+ Add Member
+
+
+
+
+
+
+
+
+ Name
+ Email
+ Role
+ Projects
+
+
+
+
+ {filterdUser.map(({ user, inviteEmail, role, _id: orgMembershipId, status }) => {
+ const name = user ? `${user.firstName} ${user.lastName}` : '-';
+ const email = user?.email || inviteEmail;
+ const userWs = workspaceMemberships?.[user?._id];
+
+ return (
+
+ {name}
+ {email}
+
+ {status === 'accepted' && (
+
+ onRoleChange(orgMembershipId, selectedRole)
+ }
+ >
+ {(isIamOwner || role === 'owner') && (
+ owner
+ )}
+ admin
+ member
+
+ )}
+ {(status === 'invited' || status === 'verified') && (
+ onInviteMember(email)}>
+ Resent Invite
+
+ )}
+ {status === 'completed' && (
+ onGrantAccess(user?._id, user?.publicKey)}
+ >
+ Grant Access
+
+ )}
+
+
+ {userWs ? (
+ userWs?.map(({ name: wsName, _id }) => (
+
+ {wsName}
+
+ ))
+ ) : (
+ This user isn't part of any projects yet
+ )}
+
+
+ handlePopUpOpen('removeMember', { id: orgMembershipId })}
+ >
+
+
+
+
+ );
+ })}
+
+
+
+
+
{
+ handlePopUpToggle('addMember', isOpen);
+ reset();
+ }}
+ >
+
+ 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.
+ >
+ }
+ >
+
+
+
+
handlePopUpToggle('removeMember', isOpen)}
+ onDeleteApproved={onRemoveOrgMemberApproved}
+ />
+ handlePopUpToggle('upgradePlan', isOpen)}
+ text="You can add custom environments if you switch to Infisical's Team plan."
+ />
+
+ );
+};
diff --git a/frontend/src/views/Settings/OrgSettingsPage/components/OrgMembersTable/index.tsx b/frontend/src/views/Settings/OrgSettingsPage/components/OrgMembersTable/index.tsx
new file mode 100644
index 000000000..729d06592
--- /dev/null
+++ b/frontend/src/views/Settings/OrgSettingsPage/components/OrgMembersTable/index.tsx
@@ -0,0 +1 @@
+export { OrgMembersTable } from './OrgMembersTable';
diff --git a/frontend/src/views/Settings/OrgSettingsPage/components/OrgNameChangeSection/OrgNameChangeSection.tsx b/frontend/src/views/Settings/OrgSettingsPage/components/OrgNameChangeSection/OrgNameChangeSection.tsx
new file mode 100644
index 000000000..9b18285c0
--- /dev/null
+++ b/frontend/src/views/Settings/OrgSettingsPage/components/OrgNameChangeSection/OrgNameChangeSection.tsx
@@ -0,0 +1,68 @@
+import { useEffect } from 'react';
+import { Controller, useForm } from 'react-hook-form';
+import { useTranslation } from 'react-i18next';
+import { faCheck } from '@fortawesome/free-solid-svg-icons';
+import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
+import { yupResolver } from '@hookform/resolvers/yup';
+import * as yup from 'yup';
+
+import { Button, FormControl, Input } from '@app/components/v2';
+
+type Props = {
+ orgName?: string;
+ onOrgNameChange: (name: string) => Promise;
+};
+
+const formSchema = yup.object({
+ name: yup.string().required().label('Project Name')
+});
+
+type FormData = yup.InferType;
+
+export const OrgNameChangeSection = ({ onOrgNameChange, orgName }: Props): JSX.Element => {
+ const {
+ handleSubmit,
+ control,
+ reset,
+ formState: { isDirty, isSubmitting }
+ } = useForm({ resolver: yupResolver(formSchema) });
+ const { t } = useTranslation();
+
+ useEffect(() => {
+ reset({ name: orgName });
+ }, [orgName]);
+
+ const onFormSubmit = async ({ name }: FormData) => {
+ await onOrgNameChange(name);
+ };
+
+ return (
+
+ );
+};
diff --git a/frontend/src/views/Settings/OrgSettingsPage/components/OrgNameChangeSection/index.tsx b/frontend/src/views/Settings/OrgSettingsPage/components/OrgNameChangeSection/index.tsx
new file mode 100644
index 000000000..b668d3732
--- /dev/null
+++ b/frontend/src/views/Settings/OrgSettingsPage/components/OrgNameChangeSection/index.tsx
@@ -0,0 +1 @@
+export { OrgNameChangeSection } from './OrgNameChangeSection';
diff --git a/frontend/src/views/Settings/OrgSettingsPage/components/index.tsx b/frontend/src/views/Settings/OrgSettingsPage/components/index.tsx
new file mode 100644
index 000000000..15a09b0d2
--- /dev/null
+++ b/frontend/src/views/Settings/OrgSettingsPage/components/index.tsx
@@ -0,0 +1,3 @@
+export { OrgIncidentContactsTable } from './OrgIncidentContactsTable';
+export { OrgMembersTable } from './OrgMembersTable';
+export { OrgNameChangeSection } from './OrgNameChangeSection';
diff --git a/frontend/src/views/Settings/OrgSettingsPage/index.tsx b/frontend/src/views/Settings/OrgSettingsPage/index.tsx
new file mode 100644
index 000000000..3ea2e3b0f
--- /dev/null
+++ b/frontend/src/views/Settings/OrgSettingsPage/index.tsx
@@ -0,0 +1 @@
+export { OrgSettingsPage } from './OrgSettingsPage';