Merge pull request #1152 from akhilmhdh/fix/key-rogue

fix: changed 2 fold operation of member workspace to one api call
This commit is contained in:
Maidul Islam
2023-11-04 12:33:08 -04:00
committed by GitHub
16 changed files with 327 additions and 128 deletions
+14 -12
View File
@@ -9,17 +9,19 @@ import * as secretsController from "./secretsController";
import * as serviceAccountsController from "./serviceAccountsController"; import * as serviceAccountsController from "./serviceAccountsController";
import * as environmentController from "./environmentController"; import * as environmentController from "./environmentController";
import * as tagController from "./tagController"; import * as tagController from "./tagController";
import * as membershipController from "./membershipController";
export { export {
authController, authController,
signupController, signupController,
usersController, usersController,
organizationsController, organizationsController,
workspaceController, workspaceController,
serviceTokenDataController, serviceTokenDataController,
secretController, secretController,
secretsController, secretsController,
serviceAccountsController, serviceAccountsController,
environmentController, environmentController,
tagController, tagController,
} membershipController
};
@@ -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<Record<string, (typeof members)[number]>>(
(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
});
};
+1
View File
@@ -39,6 +39,7 @@ export enum EventType {
UPDATE_ENVIRONMENT = "update-environment", UPDATE_ENVIRONMENT = "update-environment",
DELETE_ENVIRONMENT = "delete-environment", DELETE_ENVIRONMENT = "delete-environment",
ADD_WORKSPACE_MEMBER = "add-workspace-member", ADD_WORKSPACE_MEMBER = "add-workspace-member",
ADD_BATCH_WORKSPACE_MEMBER = "add-workspace-members",
REMOVE_WORKSPACE_MEMBER = "remove-workspace-member", REMOVE_WORKSPACE_MEMBER = "remove-workspace-member",
CREATE_FOLDER = "create-folder", CREATE_FOLDER = "create-folder",
UPDATE_FOLDER = "update-folder", UPDATE_FOLDER = "update-folder",
+9
View File
@@ -287,6 +287,14 @@ interface AddWorkspaceMemberEvent {
}; };
} }
interface AddBatchWorkspaceMemberEvent {
type: EventType.ADD_BATCH_WORKSPACE_MEMBER;
metadata: Array<{
userId: string;
email: string;
}>;
}
interface RemoveWorkspaceMemberEvent { interface RemoveWorkspaceMemberEvent {
type: EventType.REMOVE_WORKSPACE_MEMBER; type: EventType.REMOVE_WORKSPACE_MEMBER;
metadata: { metadata: {
@@ -494,6 +502,7 @@ export type Event =
| UpdateEnvironmentEvent | UpdateEnvironmentEvent
| DeleteEnvironmentEvent | DeleteEnvironmentEvent
| AddWorkspaceMemberEvent | AddWorkspaceMemberEvent
| AddBatchWorkspaceMemberEvent
| RemoveWorkspaceMemberEvent | RemoveWorkspaceMemberEvent
| CreateFolderEvent | CreateFolderEvent
| UpdateFolderEvent | UpdateFolderEvent
+11 -7
View File
@@ -67,7 +67,8 @@ import {
signup as v2SignupRouter, signup as v2SignupRouter,
tags as v2TagsRouter, tags as v2TagsRouter,
users as v2UsersRouter, users as v2UsersRouter,
workspace as v2WorkspaceRouter workspace as v2WorkspaceRouter,
membership as v2MembershipController
} from "./routes/v2"; } from "./routes/v2";
import { import {
auth as v3AuthRouter, auth as v3AuthRouter,
@@ -87,7 +88,7 @@ import {
getSecretScanningPrivateKey, getSecretScanningPrivateKey,
getSecretScanningWebhookProxy, getSecretScanningWebhookProxy,
getSecretScanningWebhookSecret, getSecretScanningWebhookSecret,
getSiteURL, getSiteURL
} from "./config"; } from "./config";
import { setup } from "./utils/setup"; import { setup } from "./utils/setup";
import { syncSecretsToThirdPartyServices } from "./queues/integrations/syncSecretsToThirdPartyServices"; import { syncSecretsToThirdPartyServices } from "./queues/integrations/syncSecretsToThirdPartyServices";
@@ -106,11 +107,13 @@ const main = async () => {
const app = express(); const app = express();
app.enable("trust proxy"); app.enable("trust proxy");
app.use(httpLogger({ app.use(
logger, httpLogger({
autoLogging: false logger,
})); autoLogging: false
})
);
app.use(express.json()); app.use(express.json());
app.use(express.urlencoded({ extended: false })); app.use(express.urlencoded({ extended: false }));
@@ -230,6 +233,7 @@ const main = async () => {
app.use("/api/v2/auth", v2AuthRouter); app.use("/api/v2/auth", v2AuthRouter);
app.use("/api/v2/users", v2UsersRouter); app.use("/api/v2/users", v2UsersRouter);
app.use("/api/v2/organizations", v2OrganizationsRouter); app.use("/api/v2/organizations", v2OrganizationsRouter);
app.use("/api/v2/workspace", v2MembershipController);
app.use("/api/v2/workspace", v2EnvironmentRouter); app.use("/api/v2/workspace", v2EnvironmentRouter);
app.use("/api/v2/workspace", v2TagsRouter); app.use("/api/v2/workspace", v2TagsRouter);
app.use("/api/v2/workspace", v2WorkspaceRouter); app.use("/api/v2/workspace", v2WorkspaceRouter);
+20 -18
View File
@@ -1,25 +1,27 @@
import auth from "./auth"; import auth from "./auth";
import signup from "./signup"; import environment from "./environment";
import users from "./users"; import membership from "./membership";
import organizations from "./organizations"; import organizations from "./organizations";
import workspace from "./workspace";
import secret from "./secret"; // deprecated import secret from "./secret"; // deprecated
import secrets from "./secrets"; import secrets from "./secrets";
import serviceTokenData from "./serviceTokenData";
import serviceAccounts from "./serviceAccounts"; import serviceAccounts from "./serviceAccounts";
import environment from "./environment" import serviceTokenData from "./serviceTokenData";
import tags from "./tags" import signup from "./signup";
import tags from "./tags";
import users from "./users";
import workspace from "./workspace";
export { export {
auth, auth,
signup, signup,
users, users,
organizations, organizations,
workspace, workspace,
secret, secret,
secrets, secrets,
serviceTokenData, serviceTokenData,
serviceAccounts, serviceAccounts,
environment, environment,
tags, tags,
} membership
};
+15
View File
@@ -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;
+16
View File
@@ -78,3 +78,19 @@ export const DenyMembershipPermissionV1 = z.object({
permissions: z.object({}).array() 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)
})
});
+1 -1
View File
@@ -23,7 +23,7 @@ export * from "./secrets/types";
export type { CreateServiceTokenDTO, ServiceToken } from "./serviceTokens/types"; export type { CreateServiceTokenDTO, ServiceToken } from "./serviceTokens/types";
export type { SubscriptionPlan } from "./subscriptions/types"; export type { SubscriptionPlan } from "./subscriptions/types";
export type { WsTag } from "./tags/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 { TWebhook } from "./webhooks/types";
export type { export type {
CreateEnvironmentDTO, CreateEnvironmentDTO,
+2 -2
View File
@@ -1,7 +1,7 @@
export { useAddUserToWs } from "./mutation";
export { export {
fetchOrgUsers, fetchOrgUsers,
useAddUserToOrg, useAddUserToOrg,
useAddUserToWs,
useCreateAPIKey, useCreateAPIKey,
useDeleteAPIKey, useDeleteAPIKey,
useDeleteOrgMembership, useDeleteOrgMembership,
@@ -20,4 +20,4 @@ export {
useUpdateMfaEnabled, useUpdateMfaEnabled,
useUpdateOrgUserRole, useUpdateOrgUserRole,
useUpdateUserAuthMethods useUpdateUserAuthMethods
} from "./queries"; } from "./queries";
+47
View File
@@ -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));
}
});
};
+15 -53
View File
@@ -1,19 +1,11 @@
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
import {
decryptAssymmetric,
encryptAssymmetric
} from "@app/components/utilities/cryptography/crypto";
import { apiRequest } from "@app/config/request"; import { apiRequest } from "@app/config/request";
import { setAuthToken } from "@app/reactQuery"; import { setAuthToken } from "@app/reactQuery";
import { APIKeyDataV2 } from "../apiKeys/types"; import { APIKeyDataV2 } from "../apiKeys/types";
import { useUploadWsKey } from "../keys/queries";
import { workspaceKeys } from "../workspace/queries";
import { import {
AddUserToOrgDTO, AddUserToOrgDTO,
AddUserToWsDTO,
AddUserToWsRes,
APIKeyData, APIKeyData,
AuthMethod, AuthMethod,
CreateAPIKeyRes, CreateAPIKeyRes,
@@ -49,7 +41,9 @@ export const useDeleteUser = () => {
return useMutation({ return useMutation({
mutationFn: async () => { 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; return user;
}, },
onSuccess: () => { onSuccess: () => {
@@ -134,43 +128,7 @@ export const useGetOrgUsers = (orgId: string) =>
}); });
// mutation // mutation
export const useAddUserToWs = () => { // TODO(akhilmhdh): move all mutation to mutation file
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));
}
});
};
export const useAddUserToOrg = () => { export const useAddUserToOrg = () => {
const queryClient = useQueryClient(); const queryClient = useQueryClient();
type Response = { type Response = {
@@ -254,12 +212,11 @@ export const useLogoutUser = () => {
localStorage.removeItem("PRIVATE_KEY"); localStorage.removeItem("PRIVATE_KEY");
localStorage.removeItem("orgData.id"); localStorage.removeItem("orgData.id");
localStorage.removeItem("projectData.id"); localStorage.removeItem("projectData.id");
queryClient.clear(); queryClient.clear();
} }
}); });
} };
export const useGetMyIp = () => { export const useGetMyIp = () => {
return useQuery({ 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({ return useQuery({
queryKey: userKeys.myAPIKeys, queryKey: userKeys.myAPIKeys,
queryFn: async () => { queryFn: async () => {
@@ -287,14 +245,17 @@ export const useGetMyAPIKeysV2 = () => {
return useQuery({ return useQuery({
queryKey: userKeys.myAPIKeysV2, queryKey: userKeys.myAPIKeysV2,
queryFn: async () => { 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; return apiKeyData;
}, },
enabled: true 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(); const queryClient = useQueryClient();
return useMutation({ return useMutation({
mutationFn: async ({ name, expiresIn }: { name: string; expiresIn: number }) => { 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(); const queryClient = useQueryClient();
return useMutation({ return useMutation({
mutationFn: async (apiKeyDataId: string) => { mutationFn: async (apiKeyDataId: string) => {
+9 -9
View File
@@ -5,9 +5,9 @@ export enum AuthMethod {
GOOGLE = "google", GOOGLE = "google",
GITHUB = "github", GITHUB = "github",
GITLAB = "gitlab", GITLAB = "gitlab",
OKTA_SAML = "okta-saml", OKTA_SAML = "okta-saml",
AZURE_SAML = "azure-saml", AZURE_SAML = "azure-saml",
JUMPCLOUD_SAML = "jumpcloud-saml" JUMPCLOUD_SAML = "jumpcloud-saml"
} }
export type User = { export type User = {
@@ -53,12 +53,12 @@ export type TWorkspaceUser = OrgUser;
export type AddUserToWsDTO = { export type AddUserToWsDTO = {
workspaceId: string; workspaceId: string;
email: string; decryptKey: UserWsKeyPair;
}; userPrivateKey: string;
members: {
export type AddUserToWsRes = { orgMembershipId: string;
invitee: OrgUser["user"]; userPublicKey: string;
latestKey: UserWsKeyPair; }[];
}; };
export type UpdateOrgUserRoleDTO = { export type UpdateOrgUserRoleDTO = {
+27 -12
View File
@@ -72,6 +72,7 @@ import {
useRegisterUserAction, useRegisterUserAction,
useUploadWsKey useUploadWsKey
} from "@app/hooks/api"; } from "@app/hooks/api";
import { fetchUserWsKey } from "@app/hooks/api/keys/queries";
import { CreateOrgModal } from "@app/views/Org/components"; import { CreateOrgModal } from "@app/views/Org/components";
interface LayoutProps { interface LayoutProps {
@@ -249,11 +250,19 @@ export const AppLayout = ({ children }: LayoutProps) => {
if (addMembers) { if (addMembers) {
// not using hooks because need at this point only // not using hooks because need at this point only
const orgUsers = await fetchOrgUsers(currentOrg._id); const orgUsers = await fetchOrgUsers(currentOrg._id);
orgUsers.forEach(({ status, user: orgUser }) => { const decryptKey = await fetchUserWsKey(newWorkspaceId);
// skip if status of org user is not accepted await addWsUser.mutateAsync({
// this orgUser is the person who created the ws workspaceId: newWorkspaceId,
if (status !== "accepted" || user.email === orgUser.email) return; decryptKey,
addWsUser.mutate({ email: orgUser.email, workspaceId: newWorkspaceId }); 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" }); createNotification({ text: "Workspace created", type: "success" });
@@ -288,13 +297,16 @@ export const AppLayout = ({ children }: LayoutProps) => {
<div className="flex h-5 w-5 items-center justify-center rounded-md bg-primary text-sm"> <div className="flex h-5 w-5 items-center justify-center rounded-md bg-primary text-sm">
{currentOrg?.name.charAt(0)} {currentOrg?.name.charAt(0)}
</div> </div>
<div className="pl-3 text-sm text-mineshaft-100"> <div
{currentOrg?.name}{" "} className="pl-2 text-sm text-mineshaft-100 text-ellipsis overflow-hidden"
<FontAwesomeIcon style={{ maxWidth: "140px" }}
icon={faAngleDown} >
className="pl-1 pt-1 text-xs text-mineshaft-300" {currentOrg?.name}
/>
</div> </div>
<FontAwesomeIcon
icon={faAngleDown}
className="pl-1 pt-1 text-xs text-mineshaft-300"
/>
</div> </div>
</DropdownMenuTrigger> </DropdownMenuTrigger>
<DropdownMenuContent align="start" className="p-1"> <DropdownMenuContent align="start" className="p-1">
@@ -346,7 +358,10 @@ export const AppLayout = ({ children }: LayoutProps) => {
asChild asChild
className="p-1 hover:bg-primary-400 hover:text-black data-[state=open]:bg-primary-400 data-[state=open]:text-black" className="p-1 hover:bg-primary-400 hover:text-black data-[state=open]:bg-primary-400 data-[state=open]:text-black"
> >
<div className="child flex h-6 w-6 items-center justify-center rounded-full bg-mineshaft pr-1 text-xs text-mineshaft-300 hover:bg-mineshaft-500"> <div
className="child flex items-center justify-center rounded-full bg-mineshaft pr-1 text-mineshaft-300 hover:bg-mineshaft-500"
style={{ fontSize: "11px", width: "26px", height: "26px" }}
>
{user?.firstName?.charAt(0)} {user?.firstName?.charAt(0)}
{user?.lastName && user?.lastName?.charAt(0)} {user?.lastName && user?.lastName?.charAt(0)}
</div> </div>
+19 -8
View File
@@ -33,6 +33,7 @@ import * as yup from "yup";
import { useNotificationContext } from "@app/components/context/Notifications/NotificationProvider"; import { useNotificationContext } from "@app/components/context/Notifications/NotificationProvider";
import { OrgPermissionCan } from "@app/components/permissions"; import { OrgPermissionCan } from "@app/components/permissions";
import onboardingCheck from "@app/components/utilities/checks/OnboardingCheck"; import onboardingCheck from "@app/components/utilities/checks/OnboardingCheck";
import { encryptAssymmetric } from "@app/components/utilities/cryptography/crypto";
import { import {
Button, Button,
Checkbox, Checkbox,
@@ -58,11 +59,10 @@ import {
useRegisterUserAction, useRegisterUserAction,
useUploadWsKey useUploadWsKey
} from "@app/hooks/api"; } from "@app/hooks/api";
import { fetchUserWsKey } from "@app/hooks/api/keys/queries";
import { useFetchServerStatus } from "@app/hooks/api/serverDetails"; import { useFetchServerStatus } from "@app/hooks/api/serverDetails";
import { usePopUp } from "@app/hooks/usePopUp"; import { usePopUp } from "@app/hooks/usePopUp";
import { encryptAssymmetric } from "../../../../components/utilities/cryptography/crypto";
const features = [ const features = [
{ {
_id: 0, _id: 0,
@@ -527,11 +527,19 @@ const OrganizationPage = withPermission(
if (addMembers) { if (addMembers) {
// not using hooks because need at this point only // not using hooks because need at this point only
const orgUsers = await fetchOrgUsers(currentOrg); const orgUsers = await fetchOrgUsers(currentOrg);
orgUsers.forEach(({ status, user: orgUser }) => { const decryptKey = await fetchUserWsKey(newWorkspaceId);
// skip if status of org user is not accepted await addWsUser.mutateAsync({
// this orgUser is the person who created the ws workspaceId: newWorkspaceId,
if (status !== "accepted" || user.email === orgUser.email) return; decryptKey,
addWsUser.mutate({ email: orgUser.email, workspaceId: newWorkspaceId }); 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" }); createNotification({ text: "Workspace created", type: "success" });
@@ -682,7 +690,10 @@ const OrganizationPage = withPermission(
</div> </div>
)} )}
</div> </div>
{!(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
) && (
<div className="mb-4 flex flex-col items-start justify-start px-6 py-6 pb-0 text-3xl"> <div className="mb-4 flex flex-col items-start justify-start px-6 py-6 pb-0 text-3xl">
<p className="mr-4 mb-4 font-semibold text-white">Onboarding Guide</p> <p className="mr-4 mb-4 font-semibold text-white">Onboarding Guide</p>
<div className="mb-3 grid w-full grid-cols-1 gap-3 lg:grid-cols-2 xl:grid-cols-3 2xl:grid-cols-4"> <div className="mb-3 grid w-full grid-cols-1 gap-3 lg:grid-cols-2 xl:grid-cols-3 2xl:grid-cols-4">
@@ -59,7 +59,7 @@ type Props = {
}; };
const addMemberFormSchema = z.object({ const addMemberFormSchema = z.object({
email: z.string().email().trim() orgMembershipId: z.string().trim()
}); });
type TAddMemberForm = z.infer<typeof addMemberFormSchema>; type TAddMemberForm = z.infer<typeof addMemberFormSchema>;
@@ -100,13 +100,25 @@ export const MemberListTab = ({ roles = [], isRolesLoading }: Props) => {
const { mutateAsync: removeUserFromWorkspace } = useDeleteUserFromWorkspace(); const { mutateAsync: removeUserFromWorkspace } = useDeleteUserFromWorkspace();
const { mutateAsync: updateUserWorkspaceRole } = useUpdateUserWorkspaceRole(); const { mutateAsync: updateUserWorkspaceRole } = useUpdateUserWorkspaceRole();
const onAddMember = async ({ email }: TAddMemberForm) => { const onAddMember = async ({ orgMembershipId }: TAddMemberForm) => {
if (!currentOrg?._id) return; 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 { try {
await addUserToWorkspace({ await addUserToWorkspace({
email, workspaceId,
workspaceId userPrivateKey,
decryptKey: wsKey,
members: [{ orgMembershipId, userPublicKey: orgUser.user.publicKey }]
}); });
createNotification({ createNotification({
text: "Successfully invited user to the organization.", text: "Successfully invited user to the organization.",
@@ -365,7 +377,7 @@ export const MemberListTab = ({ roles = [], isRolesLoading }: Props) => {
<Controller <Controller
control={control} control={control}
defaultValue={filteredOrgUsers?.[0]?.user?.email} defaultValue={filteredOrgUsers?.[0]?.user?.email}
name="email" name="orgMembershipId"
render={({ field, fieldState: { error } }) => ( render={({ field, fieldState: { error } }) => (
<FormControl label="Email" isError={Boolean(error)} errorText={error?.message}> <FormControl label="Email" isError={Boolean(error)} errorText={error?.message}>
<Select <Select
@@ -376,7 +388,7 @@ export const MemberListTab = ({ roles = [], isRolesLoading }: Props) => {
onValueChange={field.onChange} onValueChange={field.onChange}
> >
{filteredOrgUsers.map(({ _id: orgUserId, user: u }) => ( {filteredOrgUsers.map(({ _id: orgUserId, user: u }) => (
<SelectItem value={u?.email} key={`org-membership-join-${orgUserId}`}> <SelectItem value={orgUserId} key={`org-membership-join-${orgUserId}`}>
{u?.email} {u?.email}
</SelectItem> </SelectItem>
))} ))}