From b521d9fa3a7ce0039c83036558dc167b9a305490 Mon Sep 17 00:00:00 2001 From: Akhil Mohan Date: Fri, 3 Nov 2023 15:17:42 +0530 Subject: [PATCH] fix: changed 2 fold operation of member workspace to one time operation and as batch --- backend/src/controllers/v2/index.ts | 26 +++-- .../controllers/v2/membershipController.ts | 103 ++++++++++++++++++ backend/src/ee/models/auditLog/enums.ts | 1 + backend/src/ee/models/auditLog/types.ts | 9 ++ backend/src/index.ts | 18 +-- backend/src/routes/v2/index.ts | 38 ++++--- backend/src/routes/v2/membership.ts | 15 +++ backend/src/validation/membership.ts | 16 +++ frontend/src/hooks/api/types.ts | 2 +- frontend/src/hooks/api/users/index.tsx | 4 +- frontend/src/hooks/api/users/mutation.tsx | 47 ++++++++ frontend/src/hooks/api/users/queries.tsx | 68 +++--------- frontend/src/hooks/api/users/types.ts | 18 +-- frontend/src/layouts/AppLayout/AppLayout.tsx | 39 +++++-- .../src/pages/org/[id]/overview/index.tsx | 27 +++-- .../MemberListTab/MemberListTab.tsx | 24 +++- 16 files changed, 327 insertions(+), 128 deletions(-) create mode 100644 backend/src/controllers/v2/membershipController.ts create mode 100644 backend/src/routes/v2/membership.ts create mode 100644 frontend/src/hooks/api/users/mutation.tsx diff --git a/backend/src/controllers/v2/index.ts b/backend/src/controllers/v2/index.ts index a5955fef4..6ec40f8ce 100644 --- a/backend/src/controllers/v2/index.ts +++ b/backend/src/controllers/v2/index.ts @@ -9,17 +9,19 @@ import * as secretsController from "./secretsController"; import * as serviceAccountsController from "./serviceAccountsController"; import * as environmentController from "./environmentController"; import * as tagController from "./tagController"; +import * as membershipController from "./membershipController"; export { - authController, - signupController, - usersController, - organizationsController, - workspaceController, - serviceTokenDataController, - secretController, - secretsController, - serviceAccountsController, - environmentController, - tagController, -} + authController, + signupController, + usersController, + organizationsController, + workspaceController, + serviceTokenDataController, + secretController, + secretsController, + serviceAccountsController, + environmentController, + tagController, + membershipController +}; diff --git a/backend/src/controllers/v2/membershipController.ts b/backend/src/controllers/v2/membershipController.ts new file mode 100644 index 000000000..537134963 --- /dev/null +++ b/backend/src/controllers/v2/membershipController.ts @@ -0,0 +1,103 @@ +import { ForbiddenError } from "@casl/ability"; +import { Request, Response } from "express"; +import { Types } from "mongoose"; + +import { getSiteURL } from "../../config"; +import { EventType } from "../../ee/models"; +import { EEAuditLogService } from "../../ee/services"; +import { + ProjectPermissionActions, + ProjectPermissionSub, + getUserProjectPermissions +} from "../../ee/services/ProjectRoleService"; +import { sendMail } from "../../helpers"; +import { validateRequest } from "../../helpers/validation"; +import { IUser, Key, Membership, MembershipOrg, Workspace } from "../../models"; +import { BadRequestError } from "../../utils/errors"; +import * as reqValidator from "../../validation/membership"; +import { ACCEPTED, MEMBER } from "../../variables"; + +export const addUserToWorkspace = async (req: Request, res: Response) => { + const { + params: { workspaceId }, + body: { members } + } = await validateRequest(reqValidator.AddUserToWorkspaceV2, req); + // check workspace + const workspace = await Workspace.findById(workspaceId); + if (!workspace) throw new Error("Failed to find workspace"); + + // check permission + const { permission } = await getUserProjectPermissions(req.user._id, workspaceId); + ForbiddenError.from(permission).throwUnlessCan( + ProjectPermissionActions.Create, + ProjectPermissionSub.Member + ); + + // validate members are part of the organization + const orgMembers = await MembershipOrg.find({ + status: ACCEPTED, + _id: { $in: members.map(({ orgMembershipId }) => orgMembershipId) }, + organization: workspace.organization + }) + .populate<{ user: IUser }>("user") + .select({ _id: 1, user: 1 }) + .lean(); + if (orgMembers.length !== members.length) + throw BadRequestError({ message: "Org member not found" }); + + const existingMember = await Membership.find({ + workspace: workspaceId, + user: { $in: orgMembers.map(({ user }) => user) } + }); + if (existingMember?.length) + throw BadRequestError({ message: "Some users are already part of workspace" }); + + await Membership.insertMany( + orgMembers.map(({ user }) => ({ user: user._id, workspace: workspaceId, role: MEMBER })) + ); + + const encKeyGroupedByOrgMemberId = members.reduce>( + (prev, curr) => ({ ...prev, [curr.orgMembershipId]: curr }), + {} + ); + await Key.insertMany( + orgMembers.map(({ user, _id: id }) => ({ + encryptedKey: encKeyGroupedByOrgMemberId[id.toString()].workspaceEncryptedKey, + nonce: encKeyGroupedByOrgMemberId[id.toString()].workspaceEncryptedNonce, + sender: req.user._id, + receiver: user._id, + workspace: workspaceId + })) + ); + + await sendMail({ + template: "workspaceInvitation.handlebars", + subjectLine: "Infisical workspace invitation", + recipients: orgMembers.map(({ user }) => user.email), + substitutions: { + inviterFirstName: req.user.firstName, + inviterEmail: req.user.email, + workspaceName: workspace.name, + callback_url: (await getSiteURL()) + "/login" + } + }); + + await EEAuditLogService.createAuditLog( + req.authData, + { + type: EventType.ADD_BATCH_WORKSPACE_MEMBER, + metadata: orgMembers.map(({ user }) => ({ + userId: user._id.toString(), + email: user.email + })) + }, + { + workspaceId: new Types.ObjectId(workspaceId) + } + ); + + return res.status(200).send({ + success: true, + data: orgMembers + }); +}; diff --git a/backend/src/ee/models/auditLog/enums.ts b/backend/src/ee/models/auditLog/enums.ts index 45ba56345..6f9971ef7 100644 --- a/backend/src/ee/models/auditLog/enums.ts +++ b/backend/src/ee/models/auditLog/enums.ts @@ -39,6 +39,7 @@ export enum EventType { UPDATE_ENVIRONMENT = "update-environment", DELETE_ENVIRONMENT = "delete-environment", ADD_WORKSPACE_MEMBER = "add-workspace-member", + ADD_BATCH_WORKSPACE_MEMBER = "add-workspace-members", REMOVE_WORKSPACE_MEMBER = "remove-workspace-member", CREATE_FOLDER = "create-folder", UPDATE_FOLDER = "update-folder", diff --git a/backend/src/ee/models/auditLog/types.ts b/backend/src/ee/models/auditLog/types.ts index 24796c70a..51f94fdb0 100644 --- a/backend/src/ee/models/auditLog/types.ts +++ b/backend/src/ee/models/auditLog/types.ts @@ -287,6 +287,14 @@ interface AddWorkspaceMemberEvent { }; } +interface AddBatchWorkspaceMemberEvent { + type: EventType.ADD_BATCH_WORKSPACE_MEMBER; + metadata: Array<{ + userId: string; + email: string; + }>; +} + interface RemoveWorkspaceMemberEvent { type: EventType.REMOVE_WORKSPACE_MEMBER; metadata: { @@ -494,6 +502,7 @@ export type Event = | UpdateEnvironmentEvent | DeleteEnvironmentEvent | AddWorkspaceMemberEvent + | AddBatchWorkspaceMemberEvent | RemoveWorkspaceMemberEvent | CreateFolderEvent | UpdateFolderEvent diff --git a/backend/src/index.ts b/backend/src/index.ts index c1e0baab9..d7fb17178 100644 --- a/backend/src/index.ts +++ b/backend/src/index.ts @@ -67,7 +67,8 @@ import { signup as v2SignupRouter, tags as v2TagsRouter, users as v2UsersRouter, - workspace as v2WorkspaceRouter + workspace as v2WorkspaceRouter, + membership as v2MembershipController } from "./routes/v2"; import { auth as v3AuthRouter, @@ -87,7 +88,7 @@ import { getSecretScanningPrivateKey, getSecretScanningWebhookProxy, getSecretScanningWebhookSecret, - getSiteURL, + getSiteURL } from "./config"; import { setup } from "./utils/setup"; import { syncSecretsToThirdPartyServices } from "./queues/integrations/syncSecretsToThirdPartyServices"; @@ -106,11 +107,13 @@ const main = async () => { const app = express(); app.enable("trust proxy"); - - app.use(httpLogger({ - logger, - autoLogging: false - })); + + app.use( + httpLogger({ + logger, + autoLogging: false + }) + ); app.use(express.json()); app.use(express.urlencoded({ extended: false })); @@ -230,6 +233,7 @@ const main = async () => { app.use("/api/v2/auth", v2AuthRouter); app.use("/api/v2/users", v2UsersRouter); app.use("/api/v2/organizations", v2OrganizationsRouter); + app.use("/api/v2/workspace", v2MembershipController); app.use("/api/v2/workspace", v2EnvironmentRouter); app.use("/api/v2/workspace", v2TagsRouter); app.use("/api/v2/workspace", v2WorkspaceRouter); diff --git a/backend/src/routes/v2/index.ts b/backend/src/routes/v2/index.ts index 807760268..597dbafa4 100644 --- a/backend/src/routes/v2/index.ts +++ b/backend/src/routes/v2/index.ts @@ -1,25 +1,27 @@ import auth from "./auth"; -import signup from "./signup"; -import users from "./users"; +import environment from "./environment"; +import membership from "./membership"; import organizations from "./organizations"; -import workspace from "./workspace"; import secret from "./secret"; // deprecated import secrets from "./secrets"; -import serviceTokenData from "./serviceTokenData"; import serviceAccounts from "./serviceAccounts"; -import environment from "./environment" -import tags from "./tags" +import serviceTokenData from "./serviceTokenData"; +import signup from "./signup"; +import tags from "./tags"; +import users from "./users"; +import workspace from "./workspace"; export { - auth, - signup, - users, - organizations, - workspace, - secret, - secrets, - serviceTokenData, - serviceAccounts, - environment, - tags, -} \ No newline at end of file + auth, + signup, + users, + organizations, + workspace, + secret, + secrets, + serviceTokenData, + serviceAccounts, + environment, + tags, + membership +}; diff --git a/backend/src/routes/v2/membership.ts b/backend/src/routes/v2/membership.ts new file mode 100644 index 000000000..a91af00f2 --- /dev/null +++ b/backend/src/routes/v2/membership.ts @@ -0,0 +1,15 @@ +import express from "express"; +const router = express.Router(); +import { membershipController } from "../../controllers/v2"; +import { requireAuth } from "../../middleware"; +import { AuthMode } from "../../variables"; + +router.post( + "/:workspaceId/memberships", + requireAuth({ + acceptedAuthModes: [AuthMode.JWT, AuthMode.API_KEY] + }), + membershipController.addUserToWorkspace +); + +export default router; diff --git a/backend/src/validation/membership.ts b/backend/src/validation/membership.ts index 233805545..373a09aa0 100644 --- a/backend/src/validation/membership.ts +++ b/backend/src/validation/membership.ts @@ -78,3 +78,19 @@ export const DenyMembershipPermissionV1 = z.object({ permissions: z.object({}).array() }) }); + +export const AddUserToWorkspaceV2 = z.object({ + params: z.object({ + workspaceId: z.string().trim() + }), + body: z.object({ + members: z + .object({ + orgMembershipId: z.string().trim(), + workspaceEncryptedKey: z.string().trim(), + workspaceEncryptedNonce: z.string().trim() + }) + .array() + .min(1) + }) +}); diff --git a/frontend/src/hooks/api/types.ts b/frontend/src/hooks/api/types.ts index 422219adf..eae8fa0d7 100644 --- a/frontend/src/hooks/api/types.ts +++ b/frontend/src/hooks/api/types.ts @@ -23,7 +23,7 @@ export * from "./secrets/types"; export type { CreateServiceTokenDTO, ServiceToken } from "./serviceTokens/types"; export type { SubscriptionPlan } from "./subscriptions/types"; export type { WsTag } from "./tags/types"; -export type { AddUserToWsDTO, AddUserToWsRes, OrgUser, TWorkspaceUser, User } from "./users/types"; +export type { AddUserToWsDTO, OrgUser, TWorkspaceUser, User } from "./users/types"; export type { TWebhook } from "./webhooks/types"; export type { CreateEnvironmentDTO, diff --git a/frontend/src/hooks/api/users/index.tsx b/frontend/src/hooks/api/users/index.tsx index c04f7aa9f..75dd36cc5 100644 --- a/frontend/src/hooks/api/users/index.tsx +++ b/frontend/src/hooks/api/users/index.tsx @@ -1,7 +1,7 @@ +export { useAddUserToWs } from "./mutation"; export { fetchOrgUsers, useAddUserToOrg, - useAddUserToWs, useCreateAPIKey, useDeleteAPIKey, useDeleteOrgMembership, @@ -20,4 +20,4 @@ export { useUpdateMfaEnabled, useUpdateOrgUserRole, useUpdateUserAuthMethods -} from "./queries"; \ No newline at end of file +} from "./queries"; diff --git a/frontend/src/hooks/api/users/mutation.tsx b/frontend/src/hooks/api/users/mutation.tsx new file mode 100644 index 000000000..4d92202a8 --- /dev/null +++ b/frontend/src/hooks/api/users/mutation.tsx @@ -0,0 +1,47 @@ +import { useMutation, useQueryClient } from "@tanstack/react-query"; + +import { + decryptAssymmetric, + encryptAssymmetric +} from "@app/components/utilities/cryptography/crypto"; +import { apiRequest } from "@app/config/request"; + +import { workspaceKeys } from "../workspace/queries"; +import { AddUserToWsDTO } from "./types"; + +export const useAddUserToWs = () => { + const queryClient = useQueryClient(); + + return useMutation<{}, {}, AddUserToWsDTO>({ + mutationFn: async ({ workspaceId, members, decryptKey, userPrivateKey }) => { + // assymmetrically decrypt symmetric key with local private key + const key = decryptAssymmetric({ + ciphertext: decryptKey.encryptedKey, + nonce: decryptKey.nonce, + publicKey: decryptKey.sender.publicKey, + privateKey: userPrivateKey + }); + + const newWsMembers = members.map(({ orgMembershipId, userPublicKey }) => { + const { ciphertext: inviteeCipherText, nonce: inviteeNonce } = encryptAssymmetric({ + plaintext: key, + publicKey: userPublicKey, + privateKey: userPrivateKey + }); + + return { + orgMembershipId, + workspaceEncryptedKey: inviteeCipherText, + workspaceEncryptedNonce: inviteeNonce + }; + }); + const { data } = await apiRequest.post(`/api/v2/workspace/${workspaceId}/memberships`, { + members: newWsMembers + }); + return data; + }, + onSuccess: (_, { workspaceId }) => { + queryClient.invalidateQueries(workspaceKeys.getWorkspaceUsers(workspaceId)); + } + }); +}; diff --git a/frontend/src/hooks/api/users/queries.tsx b/frontend/src/hooks/api/users/queries.tsx index 45d8c323c..b865f8758 100644 --- a/frontend/src/hooks/api/users/queries.tsx +++ b/frontend/src/hooks/api/users/queries.tsx @@ -1,19 +1,11 @@ import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; -import { - decryptAssymmetric, - encryptAssymmetric -} from "@app/components/utilities/cryptography/crypto"; import { apiRequest } from "@app/config/request"; import { setAuthToken } from "@app/reactQuery"; import { APIKeyDataV2 } from "../apiKeys/types"; -import { useUploadWsKey } from "../keys/queries"; -import { workspaceKeys } from "../workspace/queries"; import { AddUserToOrgDTO, - AddUserToWsDTO, - AddUserToWsRes, APIKeyData, AuthMethod, CreateAPIKeyRes, @@ -49,7 +41,9 @@ export const useDeleteUser = () => { return useMutation({ mutationFn: async () => { - const { data: { user } } = await apiRequest.delete<{ user: User }>("/api/v2/users/me"); + const { + data: { user } + } = await apiRequest.delete<{ user: User }>("/api/v2/users/me"); return user; }, onSuccess: () => { @@ -134,43 +128,7 @@ export const useGetOrgUsers = (orgId: string) => }); // mutation -export const useAddUserToWs = () => { - const uploadWsKey = useUploadWsKey(); - const queryClient = useQueryClient(); - - return useMutation<{ data: AddUserToWsRes }, {}, AddUserToWsDTO>({ - mutationFn: ({ email, workspaceId }) => - apiRequest.post(`/api/v1/workspace/${workspaceId}/invite-signup`, { email }), - onSuccess: ({ data }, { workspaceId }) => { - const PRIVATE_KEY = localStorage.getItem("PRIVATE_KEY"); - if (!PRIVATE_KEY) return; - - // assymmetrically decrypt symmetric key with local private key - const key = decryptAssymmetric({ - ciphertext: data.latestKey.encryptedKey, - nonce: data.latestKey.nonce, - publicKey: data.latestKey.sender.publicKey, - privateKey: PRIVATE_KEY - }); - - const { ciphertext: inviteeCipherText, nonce: inviteeNonce } = encryptAssymmetric({ - plaintext: key, - publicKey: data.invitee.publicKey, - privateKey: PRIVATE_KEY - }); - - uploadWsKey.mutate({ - encryptedKey: inviteeCipherText, - nonce: inviteeNonce, - userId: data.invitee._id, - workspaceId - }); - - queryClient.invalidateQueries(workspaceKeys.getWorkspaceUsers(workspaceId)); - } - }); -}; - +// TODO(akhilmhdh): move all mutation to mutation file export const useAddUserToOrg = () => { const queryClient = useQueryClient(); type Response = { @@ -254,12 +212,11 @@ export const useLogoutUser = () => { localStorage.removeItem("PRIVATE_KEY"); localStorage.removeItem("orgData.id"); localStorage.removeItem("projectData.id"); - + queryClient.clear(); } }); -} - +}; export const useGetMyIp = () => { return useQuery({ @@ -272,7 +229,8 @@ export const useGetMyIp = () => { }); }; -export const useGetMyAPIKeys = () => { // TODO: deprecate (moving to API Key V2) +export const useGetMyAPIKeys = () => { + // TODO: deprecate (moving to API Key V2) return useQuery({ queryKey: userKeys.myAPIKeys, queryFn: async () => { @@ -287,14 +245,17 @@ export const useGetMyAPIKeysV2 = () => { return useQuery({ queryKey: userKeys.myAPIKeysV2, queryFn: async () => { - const { data: { apiKeyData } } = await apiRequest.get<{ apiKeyData: APIKeyDataV2[] }>("/api/v3/users/me/api-keys"); + const { + data: { apiKeyData } + } = await apiRequest.get<{ apiKeyData: APIKeyDataV2[] }>("/api/v3/users/me/api-keys"); return apiKeyData; }, enabled: true }); }; -export const useCreateAPIKey = () => { // TODO: deprecate (moving to API Key V2) +export const useCreateAPIKey = () => { + // TODO: deprecate (moving to API Key V2) const queryClient = useQueryClient(); return useMutation({ mutationFn: async ({ name, expiresIn }: { name: string; expiresIn: number }) => { @@ -311,7 +272,8 @@ export const useCreateAPIKey = () => { // TODO: deprecate (moving to API Key V2) }); }; -export const useDeleteAPIKey = () => { // TODO: deprecate (moving to API Key V2) +export const useDeleteAPIKey = () => { + // TODO: deprecate (moving to API Key V2) const queryClient = useQueryClient(); return useMutation({ mutationFn: async (apiKeyDataId: string) => { diff --git a/frontend/src/hooks/api/users/types.ts b/frontend/src/hooks/api/users/types.ts index 6917bcdc7..c6b790592 100644 --- a/frontend/src/hooks/api/users/types.ts +++ b/frontend/src/hooks/api/users/types.ts @@ -5,9 +5,9 @@ export enum AuthMethod { GOOGLE = "google", GITHUB = "github", GITLAB = "gitlab", - OKTA_SAML = "okta-saml", - AZURE_SAML = "azure-saml", - JUMPCLOUD_SAML = "jumpcloud-saml" + OKTA_SAML = "okta-saml", + AZURE_SAML = "azure-saml", + JUMPCLOUD_SAML = "jumpcloud-saml" } export type User = { @@ -53,12 +53,12 @@ export type TWorkspaceUser = OrgUser; export type AddUserToWsDTO = { workspaceId: string; - email: string; -}; - -export type AddUserToWsRes = { - invitee: OrgUser["user"]; - latestKey: UserWsKeyPair; + decryptKey: UserWsKeyPair; + userPrivateKey: string; + members: { + orgMembershipId: string; + userPublicKey: string; + }[]; }; export type UpdateOrgUserRoleDTO = { diff --git a/frontend/src/layouts/AppLayout/AppLayout.tsx b/frontend/src/layouts/AppLayout/AppLayout.tsx index 632449273..16d7df68f 100644 --- a/frontend/src/layouts/AppLayout/AppLayout.tsx +++ b/frontend/src/layouts/AppLayout/AppLayout.tsx @@ -72,6 +72,7 @@ import { useRegisterUserAction, useUploadWsKey } from "@app/hooks/api"; +import { fetchUserWsKey } from "@app/hooks/api/keys/queries"; import { CreateOrgModal } from "@app/views/Org/components"; interface LayoutProps { @@ -249,11 +250,19 @@ export const AppLayout = ({ children }: LayoutProps) => { if (addMembers) { // not using hooks because need at this point only const orgUsers = await fetchOrgUsers(currentOrg._id); - orgUsers.forEach(({ status, user: orgUser }) => { - // skip if status of org user is not accepted - // this orgUser is the person who created the ws - if (status !== "accepted" || user.email === orgUser.email) return; - addWsUser.mutate({ email: orgUser.email, workspaceId: newWorkspaceId }); + const decryptKey = await fetchUserWsKey(newWorkspaceId); + await addWsUser.mutateAsync({ + workspaceId: newWorkspaceId, + decryptKey, + userPrivateKey: PRIVATE_KEY, + members: orgUsers + .filter( + ({ status, user: orgUser }) => status === "accepted" && user.email !== orgUser.email + ) + .map(({ user: orgUser, _id: orgMembershipId }) => ({ + userPublicKey: orgUser.publicKey, + orgMembershipId + })) }); } createNotification({ text: "Workspace created", type: "success" }); @@ -288,13 +297,16 @@ export const AppLayout = ({ children }: LayoutProps) => {
{currentOrg?.name.charAt(0)}
-
- {currentOrg?.name}{" "} - +
+ {currentOrg?.name}
+
@@ -346,7 +358,10 @@ export const AppLayout = ({ children }: LayoutProps) => { asChild className="p-1 hover:bg-primary-400 hover:text-black data-[state=open]:bg-primary-400 data-[state=open]:text-black" > -
+
{user?.firstName?.charAt(0)} {user?.lastName && user?.lastName?.charAt(0)}
diff --git a/frontend/src/pages/org/[id]/overview/index.tsx b/frontend/src/pages/org/[id]/overview/index.tsx index 61a22d967..9298b7837 100644 --- a/frontend/src/pages/org/[id]/overview/index.tsx +++ b/frontend/src/pages/org/[id]/overview/index.tsx @@ -33,6 +33,7 @@ import * as yup from "yup"; import { useNotificationContext } from "@app/components/context/Notifications/NotificationProvider"; import { OrgPermissionCan } from "@app/components/permissions"; import onboardingCheck from "@app/components/utilities/checks/OnboardingCheck"; +import { encryptAssymmetric } from "@app/components/utilities/cryptography/crypto"; import { Button, Checkbox, @@ -58,11 +59,10 @@ import { useRegisterUserAction, useUploadWsKey } from "@app/hooks/api"; +import { fetchUserWsKey } from "@app/hooks/api/keys/queries"; import { useFetchServerStatus } from "@app/hooks/api/serverDetails"; import { usePopUp } from "@app/hooks/usePopUp"; -import { encryptAssymmetric } from "../../../../components/utilities/cryptography/crypto"; - const features = [ { _id: 0, @@ -527,11 +527,19 @@ const OrganizationPage = withPermission( if (addMembers) { // not using hooks because need at this point only const orgUsers = await fetchOrgUsers(currentOrg); - orgUsers.forEach(({ status, user: orgUser }) => { - // skip if status of org user is not accepted - // this orgUser is the person who created the ws - if (status !== "accepted" || user.email === orgUser.email) return; - addWsUser.mutate({ email: orgUser.email, workspaceId: newWorkspaceId }); + const decryptKey = await fetchUserWsKey(newWorkspaceId); + await addWsUser.mutateAsync({ + workspaceId: newWorkspaceId, + decryptKey, + userPrivateKey: PRIVATE_KEY, + members: orgUsers + .filter( + ({ status, user: orgUser }) => status === "accepted" && user.email !== orgUser.email + ) + .map(({ user: orgUser, _id: orgMembershipId }) => ({ + userPublicKey: orgUser.publicKey, + orgMembershipId + })) }); } createNotification({ text: "Workspace created", type: "success" }); @@ -682,7 +690,10 @@ const OrganizationPage = withPermission(
)} - {!(new Date().getTime() - new Date(user?.createdAt).getTime() < 30 * 24 * 60 * 60 * 1000) && ( + {!( + new Date().getTime() - new Date(user?.createdAt).getTime() < + 30 * 24 * 60 * 60 * 1000 + ) && (

Onboarding Guide

diff --git a/frontend/src/views/Project/MembersPage/components/MemberListTab/MemberListTab.tsx b/frontend/src/views/Project/MembersPage/components/MemberListTab/MemberListTab.tsx index 601400f34..f35b578e1 100644 --- a/frontend/src/views/Project/MembersPage/components/MemberListTab/MemberListTab.tsx +++ b/frontend/src/views/Project/MembersPage/components/MemberListTab/MemberListTab.tsx @@ -59,7 +59,7 @@ type Props = { }; const addMemberFormSchema = z.object({ - email: z.string().email().trim() + orgMembershipId: z.string().trim() }); type TAddMemberForm = z.infer; @@ -100,13 +100,25 @@ export const MemberListTab = ({ roles = [], isRolesLoading }: Props) => { const { mutateAsync: removeUserFromWorkspace } = useDeleteUserFromWorkspace(); const { mutateAsync: updateUserWorkspaceRole } = useUpdateUserWorkspaceRole(); - const onAddMember = async ({ email }: TAddMemberForm) => { + const onAddMember = async ({ orgMembershipId }: TAddMemberForm) => { if (!currentOrg?._id) return; + // TODO(akhilmhdh): Move to memory storage + const userPrivateKey = localStorage.getItem("PRIVATE_KEY"); + if (!userPrivateKey || !wsKey) { + createNotification({ + text: "Failed to find private key. Try re-login" + }); + return; + } + const orgUser = (orgUsers || []).find(({ _id }) => _id === orgMembershipId); + if (!orgUser) return; try { await addUserToWorkspace({ - email, - workspaceId + workspaceId, + userPrivateKey, + decryptKey: wsKey, + members: [{ orgMembershipId, userPublicKey: orgUser.user.publicKey }] }); createNotification({ text: "Successfully invited user to the organization.", @@ -365,7 +377,7 @@ export const MemberListTab = ({ roles = [], isRolesLoading }: Props) => { (