mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-10-06 02:26:40 +00:00
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:
@@ -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
|
||||||
|
});
|
||||||
|
};
|
||||||
@@ -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",
|
||||||
|
|||||||
@@ -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
@@ -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);
|
||||||
|
|||||||
@@ -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
|
||||||
|
};
|
||||||
|
|||||||
@@ -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;
|
||||||
@@ -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)
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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";
|
||||||
|
|||||||
@@ -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));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
@@ -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) => {
|
||||||
|
|||||||
@@ -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 = {
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
))}
|
))}
|
||||||
|
|||||||
Reference in New Issue
Block a user