mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-10-07 14:27:30 +00:00
Merge pull request #2380 from Infisical/daniel/invite-multiple-members-to-project
feat: invite multiple members to projects with role assignment
This commit is contained in:
@@ -447,7 +447,9 @@ export const PROJECT_USERS = {
|
|||||||
INVITE_MEMBER: {
|
INVITE_MEMBER: {
|
||||||
projectId: "The ID of the project to invite the member to.",
|
projectId: "The ID of the project to invite the member to.",
|
||||||
emails: "A list of organization member emails to invite to the project.",
|
emails: "A list of organization member emails to invite to the project.",
|
||||||
usernames: "A list of usernames to invite to the project."
|
usernames: "A list of usernames to invite to the project.",
|
||||||
|
roleSlugs:
|
||||||
|
"A list of role slugs to assign to the newly created project membership. If nothing is provided, it will default to the Member role."
|
||||||
},
|
},
|
||||||
REMOVE_MEMBER: {
|
REMOVE_MEMBER: {
|
||||||
projectId: "The ID of the project to remove the member from.",
|
projectId: "The ID of the project to remove the member from.",
|
||||||
|
|||||||
@@ -499,6 +499,7 @@ export const registerRoutes = async (
|
|||||||
tokenService,
|
tokenService,
|
||||||
projectUserAdditionalPrivilegeDAL,
|
projectUserAdditionalPrivilegeDAL,
|
||||||
projectUserMembershipRoleDAL,
|
projectUserMembershipRoleDAL,
|
||||||
|
projectRoleDAL,
|
||||||
projectDAL,
|
projectDAL,
|
||||||
projectMembershipDAL,
|
projectMembershipDAL,
|
||||||
orgMembershipDAL,
|
orgMembershipDAL,
|
||||||
@@ -506,8 +507,7 @@ export const registerRoutes = async (
|
|||||||
smtpService,
|
smtpService,
|
||||||
userDAL,
|
userDAL,
|
||||||
groupDAL,
|
groupDAL,
|
||||||
orgBotDAL,
|
orgBotDAL
|
||||||
projectRoleDAL
|
|
||||||
});
|
});
|
||||||
const signupService = authSignupServiceFactory({
|
const signupService = authSignupServiceFactory({
|
||||||
tokenService,
|
tokenService,
|
||||||
|
|||||||
@@ -26,7 +26,8 @@ export const registerProjectMembershipRouter = async (server: FastifyZodProvider
|
|||||||
}),
|
}),
|
||||||
body: z.object({
|
body: z.object({
|
||||||
emails: z.string().email().array().default([]).describe(PROJECT_USERS.INVITE_MEMBER.emails),
|
emails: z.string().email().array().default([]).describe(PROJECT_USERS.INVITE_MEMBER.emails),
|
||||||
usernames: z.string().array().default([]).describe(PROJECT_USERS.INVITE_MEMBER.usernames)
|
usernames: z.string().array().default([]).describe(PROJECT_USERS.INVITE_MEMBER.usernames),
|
||||||
|
roleSlugs: z.string().array().optional().describe(PROJECT_USERS.INVITE_MEMBER.roleSlugs)
|
||||||
}),
|
}),
|
||||||
response: {
|
response: {
|
||||||
200: z.object({
|
200: z.object({
|
||||||
|
|||||||
@@ -153,7 +153,6 @@ export const orgAdminServiceFactory = ({
|
|||||||
members: [
|
members: [
|
||||||
{
|
{
|
||||||
orgMembershipId: membership.id,
|
orgMembershipId: membership.id,
|
||||||
projectMembershipRole: ProjectMembershipRole.Admin,
|
|
||||||
userPublicKey: userEncryptionKey.publicKey
|
userPublicKey: userEncryptionKey.publicKey
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -114,11 +114,11 @@ export const orgServiceFactory = ({
|
|||||||
tokenService,
|
tokenService,
|
||||||
orgBotDAL,
|
orgBotDAL,
|
||||||
licenseService,
|
licenseService,
|
||||||
|
projectRoleDAL,
|
||||||
samlConfigDAL,
|
samlConfigDAL,
|
||||||
userGroupMembershipDAL,
|
userGroupMembershipDAL,
|
||||||
projectBotDAL,
|
projectBotDAL,
|
||||||
projectUserMembershipRoleDAL,
|
projectUserMembershipRoleDAL
|
||||||
projectRoleDAL
|
|
||||||
}: TOrgServiceFactoryDep) => {
|
}: TOrgServiceFactoryDep) => {
|
||||||
/*
|
/*
|
||||||
* Get organization details by the organization id
|
* Get organization details by the organization id
|
||||||
|
|||||||
@@ -1,190 +0,0 @@
|
|||||||
import { Knex } from "knex";
|
|
||||||
|
|
||||||
import { ProjectMembershipRole, SecretKeyEncoding, TProjectMemberships } from "@app/db/schemas";
|
|
||||||
import { TUserGroupMembershipDALFactory } from "@app/ee/services/group/user-group-membership-dal";
|
|
||||||
import { getConfig } from "@app/lib/config/env";
|
|
||||||
import { infisicalSymmetricDecrypt } from "@app/lib/crypto/encryption";
|
|
||||||
import { BadRequestError } from "@app/lib/errors";
|
|
||||||
import { groupBy } from "@app/lib/fn";
|
|
||||||
|
|
||||||
import { TOrgDALFactory } from "../org/org-dal";
|
|
||||||
import { TProjectDALFactory } from "../project/project-dal";
|
|
||||||
import { assignWorkspaceKeysToMembers } from "../project/project-fns";
|
|
||||||
import { TProjectBotDALFactory } from "../project-bot/project-bot-dal";
|
|
||||||
import { TProjectKeyDALFactory } from "../project-key/project-key-dal";
|
|
||||||
import { SmtpTemplates, TSmtpService } from "../smtp/smtp-service";
|
|
||||||
import { TProjectMembershipDALFactory } from "./project-membership-dal";
|
|
||||||
import { TProjectUserMembershipRoleDALFactory } from "./project-user-membership-role-dal";
|
|
||||||
|
|
||||||
type TAddMembersToProjectArg = {
|
|
||||||
orgDAL: Pick<TOrgDALFactory, "findMembership" | "findOrgMembersByUsername">;
|
|
||||||
projectMembershipDAL: Pick<TProjectMembershipDALFactory, "find" | "transaction" | "insertMany">;
|
|
||||||
projectDAL: Pick<TProjectDALFactory, "findProjectById" | "findProjectGhostUser">;
|
|
||||||
projectKeyDAL: Pick<TProjectKeyDALFactory, "findLatestProjectKey" | "insertMany">;
|
|
||||||
projectBotDAL: Pick<TProjectBotDALFactory, "findOne">;
|
|
||||||
userGroupMembershipDAL: Pick<TUserGroupMembershipDALFactory, "findUserGroupMembershipsInProject">;
|
|
||||||
projectUserMembershipRoleDAL: Pick<TProjectUserMembershipRoleDALFactory, "insertMany">;
|
|
||||||
smtpService: Pick<TSmtpService, "sendMail">;
|
|
||||||
};
|
|
||||||
|
|
||||||
type AddMembersToNonE2EEProjectDTO = {
|
|
||||||
emails: string[];
|
|
||||||
usernames: string[];
|
|
||||||
projectId: string;
|
|
||||||
projectMembershipRole: ProjectMembershipRole;
|
|
||||||
sendEmails?: boolean;
|
|
||||||
};
|
|
||||||
|
|
||||||
type AddMembersToNonE2EEProjectOptions = {
|
|
||||||
tx?: Knex;
|
|
||||||
throwOnProjectNotFound?: boolean;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const addMembersToProject = ({
|
|
||||||
orgDAL,
|
|
||||||
projectDAL,
|
|
||||||
projectMembershipDAL,
|
|
||||||
projectKeyDAL,
|
|
||||||
projectBotDAL,
|
|
||||||
userGroupMembershipDAL,
|
|
||||||
projectUserMembershipRoleDAL,
|
|
||||||
smtpService
|
|
||||||
}: TAddMembersToProjectArg) => {
|
|
||||||
// Can create multiple memberships for a singular project, based on user email / username
|
|
||||||
const addMembersToNonE2EEProject = async (
|
|
||||||
{ emails, usernames, projectId, projectMembershipRole, sendEmails }: AddMembersToNonE2EEProjectDTO,
|
|
||||||
options: AddMembersToNonE2EEProjectOptions = { throwOnProjectNotFound: true }
|
|
||||||
) => {
|
|
||||||
const processTransaction = async (tx: Knex) => {
|
|
||||||
const usernamesAndEmails = [...emails, ...usernames];
|
|
||||||
|
|
||||||
const project = await projectDAL.findProjectById(projectId);
|
|
||||||
if (!project) {
|
|
||||||
if (options.throwOnProjectNotFound) {
|
|
||||||
throw new BadRequestError({ message: "Project not found when attempting to add user to project" });
|
|
||||||
}
|
|
||||||
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
|
|
||||||
const orgMembers = await orgDAL.findOrgMembersByUsername(
|
|
||||||
project.orgId,
|
|
||||||
[...new Set(usernamesAndEmails.map((element) => element.toLowerCase()))],
|
|
||||||
tx
|
|
||||||
);
|
|
||||||
|
|
||||||
if (orgMembers.length !== usernamesAndEmails.length)
|
|
||||||
throw new BadRequestError({ message: "Some users are not part of org" });
|
|
||||||
|
|
||||||
if (!orgMembers.length) return [];
|
|
||||||
|
|
||||||
const existingMembers = await projectMembershipDAL.find({
|
|
||||||
projectId,
|
|
||||||
$in: { userId: orgMembers.map(({ user }) => user.id).filter(Boolean) }
|
|
||||||
});
|
|
||||||
if (existingMembers.length) throw new BadRequestError({ message: "Some users are already part of project" });
|
|
||||||
|
|
||||||
const ghostUser = await projectDAL.findProjectGhostUser(projectId);
|
|
||||||
|
|
||||||
if (!ghostUser) {
|
|
||||||
throw new BadRequestError({
|
|
||||||
message: "Failed to find sudo user"
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
const ghostUserLatestKey = await projectKeyDAL.findLatestProjectKey(ghostUser.id, projectId);
|
|
||||||
|
|
||||||
if (!ghostUserLatestKey) {
|
|
||||||
throw new BadRequestError({
|
|
||||||
message: "Failed to find sudo user latest key"
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
const bot = await projectBotDAL.findOne({ projectId });
|
|
||||||
if (!bot) {
|
|
||||||
throw new BadRequestError({
|
|
||||||
message: "Failed to find bot"
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
const botPrivateKey = infisicalSymmetricDecrypt({
|
|
||||||
keyEncoding: bot.keyEncoding as SecretKeyEncoding,
|
|
||||||
iv: bot.iv,
|
|
||||||
tag: bot.tag,
|
|
||||||
ciphertext: bot.encryptedPrivateKey
|
|
||||||
});
|
|
||||||
|
|
||||||
const newWsMembers = assignWorkspaceKeysToMembers({
|
|
||||||
decryptKey: ghostUserLatestKey,
|
|
||||||
userPrivateKey: botPrivateKey,
|
|
||||||
members: orgMembers.map((membership) => ({
|
|
||||||
orgMembershipId: membership.id,
|
|
||||||
projectMembershipRole,
|
|
||||||
userPublicKey: membership.user.publicKey
|
|
||||||
}))
|
|
||||||
});
|
|
||||||
|
|
||||||
const members: TProjectMemberships[] = [];
|
|
||||||
|
|
||||||
const userIdsToExcludeForProjectKeyAddition = new Set(
|
|
||||||
await userGroupMembershipDAL.findUserGroupMembershipsInProject(usernamesAndEmails, projectId)
|
|
||||||
);
|
|
||||||
const projectMemberships = await projectMembershipDAL.insertMany(
|
|
||||||
orgMembers.map(({ user }) => ({
|
|
||||||
projectId,
|
|
||||||
userId: user.id
|
|
||||||
})),
|
|
||||||
tx
|
|
||||||
);
|
|
||||||
await projectUserMembershipRoleDAL.insertMany(
|
|
||||||
projectMemberships.map(({ id }) => ({ projectMembershipId: id, role: projectMembershipRole })),
|
|
||||||
tx
|
|
||||||
);
|
|
||||||
|
|
||||||
members.push(...projectMemberships);
|
|
||||||
|
|
||||||
const encKeyGroupByOrgMembId = groupBy(newWsMembers, (i) => i.orgMembershipId);
|
|
||||||
await projectKeyDAL.insertMany(
|
|
||||||
orgMembers
|
|
||||||
.filter(({ user }) => !userIdsToExcludeForProjectKeyAddition.has(user.id))
|
|
||||||
.map(({ user, id }) => ({
|
|
||||||
encryptedKey: encKeyGroupByOrgMembId[id][0].workspaceEncryptedKey,
|
|
||||||
nonce: encKeyGroupByOrgMembId[id][0].workspaceEncryptedNonce,
|
|
||||||
senderId: ghostUser.id,
|
|
||||||
receiverId: user.id,
|
|
||||||
projectId
|
|
||||||
})),
|
|
||||||
tx
|
|
||||||
);
|
|
||||||
|
|
||||||
if (sendEmails) {
|
|
||||||
const recipients = orgMembers.filter((i) => i.user.email).map((i) => i.user.email as string);
|
|
||||||
|
|
||||||
const appCfg = getConfig();
|
|
||||||
|
|
||||||
if (recipients.length) {
|
|
||||||
await smtpService.sendMail({
|
|
||||||
template: SmtpTemplates.WorkspaceInvite,
|
|
||||||
subjectLine: "Infisical project invitation",
|
|
||||||
recipients: orgMembers.filter((i) => i.user.email).map((i) => i.user.email as string),
|
|
||||||
substitutions: {
|
|
||||||
workspaceName: project.name,
|
|
||||||
callback_url: `${appCfg.SITE_URL}/login`
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return members;
|
|
||||||
};
|
|
||||||
|
|
||||||
if (options.tx) {
|
|
||||||
return processTransaction(options.tx);
|
|
||||||
}
|
|
||||||
return projectMembershipDAL.transaction(processTransaction);
|
|
||||||
};
|
|
||||||
|
|
||||||
return {
|
|
||||||
addMembersToNonE2EEProject
|
|
||||||
};
|
|
||||||
};
|
|
||||||
@@ -42,7 +42,7 @@ type TProjectMembershipServiceFactoryDep = {
|
|||||||
projectUserMembershipRoleDAL: Pick<TProjectUserMembershipRoleDALFactory, "insertMany" | "find" | "delete">;
|
projectUserMembershipRoleDAL: Pick<TProjectUserMembershipRoleDALFactory, "insertMany" | "find" | "delete">;
|
||||||
userDAL: Pick<TUserDALFactory, "findById" | "findOne" | "findUserByProjectMembershipId" | "find">;
|
userDAL: Pick<TUserDALFactory, "findById" | "findOne" | "findUserByProjectMembershipId" | "find">;
|
||||||
userGroupMembershipDAL: TUserGroupMembershipDALFactory;
|
userGroupMembershipDAL: TUserGroupMembershipDALFactory;
|
||||||
projectRoleDAL: Pick<TProjectRoleDALFactory, "find">;
|
projectRoleDAL: Pick<TProjectRoleDALFactory, "find" | "findOne">;
|
||||||
orgDAL: Pick<TOrgDALFactory, "findMembership" | "findOrgMembersByUsername">;
|
orgDAL: Pick<TOrgDALFactory, "findMembership" | "findOrgMembersByUsername">;
|
||||||
projectDAL: Pick<TProjectDALFactory, "findById" | "findProjectGhostUser" | "transaction" | "findProjectById">;
|
projectDAL: Pick<TProjectDALFactory, "findById" | "findProjectGhostUser" | "transaction" | "findProjectById">;
|
||||||
projectKeyDAL: Pick<TProjectKeyDALFactory, "findLatestProjectKey" | "delete" | "insertMany">;
|
projectKeyDAL: Pick<TProjectKeyDALFactory, "findLatestProjectKey" | "delete" | "insertMany">;
|
||||||
|
|||||||
@@ -53,4 +53,5 @@ export type TAddUsersToWorkspaceNonE2EEDTO = {
|
|||||||
sendEmails?: boolean;
|
sendEmails?: boolean;
|
||||||
emails: string[];
|
emails: string[];
|
||||||
usernames: string[];
|
usernames: string[];
|
||||||
|
roleSlugs?: string[];
|
||||||
} & TProjectPermission;
|
} & TProjectPermission;
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ export const assignWorkspaceKeysToMembers = ({ members, decryptKey, userPrivateK
|
|||||||
privateKey: userPrivateKey
|
privateKey: userPrivateKey
|
||||||
});
|
});
|
||||||
|
|
||||||
const newWsMembers = members.map(({ orgMembershipId, userPublicKey, projectMembershipRole }) => {
|
const newWsMembers = members.map(({ orgMembershipId, userPublicKey }) => {
|
||||||
const { ciphertext: inviteeCipherText, nonce: inviteeNonce } = encryptAsymmetric(
|
const { ciphertext: inviteeCipherText, nonce: inviteeNonce } = encryptAsymmetric(
|
||||||
plaintextProjectKey,
|
plaintextProjectKey,
|
||||||
userPublicKey,
|
userPublicKey,
|
||||||
@@ -25,7 +25,6 @@ export const assignWorkspaceKeysToMembers = ({ members, decryptKey, userPrivateK
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
orgMembershipId,
|
orgMembershipId,
|
||||||
projectRole: projectMembershipRole,
|
|
||||||
workspaceEncryptedKey: inviteeCipherText,
|
workspaceEncryptedKey: inviteeCipherText,
|
||||||
workspaceEncryptedNonce: inviteeNonce
|
workspaceEncryptedNonce: inviteeNonce
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -300,8 +300,7 @@ export const projectQueueFactory = ({
|
|||||||
members: [
|
members: [
|
||||||
{
|
{
|
||||||
userPublicKey: user.publicKey,
|
userPublicKey: user.publicKey,
|
||||||
orgMembershipId: orgMembership.id,
|
orgMembershipId: orgMembership.id
|
||||||
projectMembershipRole: ProjectMembershipRole.Admin
|
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -277,8 +277,7 @@ export const projectServiceFactory = ({
|
|||||||
members: [
|
members: [
|
||||||
{
|
{
|
||||||
userPublicKey: user.publicKey,
|
userPublicKey: user.publicKey,
|
||||||
orgMembershipId: orgMembership.id,
|
orgMembershipId: orgMembership.id
|
||||||
projectMembershipRole: ProjectMembershipRole.Admin
|
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
});
|
});
|
||||||
@@ -292,7 +291,7 @@ export const projectServiceFactory = ({
|
|||||||
tx
|
tx
|
||||||
);
|
);
|
||||||
await projectUserMembershipRoleDAL.create(
|
await projectUserMembershipRoleDAL.create(
|
||||||
{ projectMembershipId: userProjectMembership.id, role: projectAdmin.projectRole },
|
{ projectMembershipId: userProjectMembership.id, role: ProjectMembershipRole.Admin },
|
||||||
tx
|
tx
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { ProjectMembershipRole, TProjectKeys } from "@app/db/schemas";
|
import { TProjectKeys } from "@app/db/schemas";
|
||||||
import { TProjectPermission } from "@app/lib/types";
|
import { TProjectPermission } from "@app/lib/types";
|
||||||
|
|
||||||
import { ActorAuthMethod, ActorType } from "../auth/auth-type";
|
import { ActorAuthMethod, ActorType } from "../auth/auth-type";
|
||||||
@@ -88,7 +88,6 @@ export type AddUserToWsDTO = {
|
|||||||
userPrivateKey: string;
|
userPrivateKey: string;
|
||||||
members: {
|
members: {
|
||||||
orgMembershipId: string;
|
orgMembershipId: string;
|
||||||
projectMembershipRole: ProjectMembershipRole;
|
|
||||||
userPublicKey: string;
|
userPublicKey: string;
|
||||||
}[];
|
}[];
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -60,7 +60,7 @@ export default function TeamInviteStep(): JSX.Element {
|
|||||||
placeholder="[email protected], [email protected]..."
|
placeholder="[email protected], [email protected]..."
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="mx-auto mt-0 mt-2 flex w-full flex-row items-end justify-end text-sm md:mt-4 md:mb-2 md:min-w-[30rem] md:max-w-md">
|
<div className="mx-auto mt-2 flex w-full flex-row items-end justify-end text-sm md:mt-4 md:mb-2 md:min-w-[30rem] md:max-w-md">
|
||||||
<Button
|
<Button
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
if (serverDetails?.emailConfigured) {
|
if (serverDetails?.emailConfigured) {
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import { useMutation, useQueryClient } from "@tanstack/react-query";
|
|||||||
|
|
||||||
import { apiRequest } from "@app/config/request";
|
import { apiRequest } from "@app/config/request";
|
||||||
|
|
||||||
import { userKeys } from "../users/queries";
|
import { userKeys } from "../users";
|
||||||
import {
|
import {
|
||||||
APIKeyDataV2,
|
APIKeyDataV2,
|
||||||
CreateAPIKeyDataV2DTO,
|
CreateAPIKeyDataV2DTO,
|
||||||
|
|||||||
@@ -4,8 +4,8 @@ import { apiRequest } from "@app/config/request";
|
|||||||
|
|
||||||
import { Actor, AuditLog, AuditLogFilters } from "./types";
|
import { Actor, AuditLog, AuditLogFilters } from "./types";
|
||||||
|
|
||||||
export const workspaceKeys = {
|
export const auditLogKeys = {
|
||||||
getAuditLogs: (filters: AuditLogFilters, workspaceId: string | null) =>
|
getAuditLogs: (workspaceId: string | null, filters: AuditLogFilters) =>
|
||||||
[{ workspaceId, filters }, "audit-logs"] as const,
|
[{ workspaceId, filters }, "audit-logs"] as const,
|
||||||
getAuditLogActorFilterOpts: (workspaceId: string) =>
|
getAuditLogActorFilterOpts: (workspaceId: string) =>
|
||||||
[{ workspaceId }, "audit-log-actor-filters"] as const
|
[{ workspaceId }, "audit-log-actor-filters"] as const
|
||||||
@@ -13,9 +13,7 @@ export const workspaceKeys = {
|
|||||||
|
|
||||||
export const useGetAuditLogs = (filters: AuditLogFilters, workspaceId: string | null) => {
|
export const useGetAuditLogs = (filters: AuditLogFilters, workspaceId: string | null) => {
|
||||||
return useInfiniteQuery({
|
return useInfiniteQuery({
|
||||||
queryKey: workspaceKeys.getAuditLogs(filters, workspaceId),
|
queryKey: auditLogKeys.getAuditLogs(workspaceId, filters),
|
||||||
enabled: workspaceId !== "",
|
|
||||||
|
|
||||||
queryFn: async ({ pageParam }) => {
|
queryFn: async ({ pageParam }) => {
|
||||||
const auditLogEndpoint = workspaceId
|
const auditLogEndpoint = workspaceId
|
||||||
? `/api/v1/workspace/${workspaceId}/audit-logs`
|
? `/api/v1/workspace/${workspaceId}/audit-logs`
|
||||||
@@ -37,7 +35,7 @@ export const useGetAuditLogs = (filters: AuditLogFilters, workspaceId: string |
|
|||||||
|
|
||||||
export const useGetAuditLogActorFilterOpts = (workspaceId: string) => {
|
export const useGetAuditLogActorFilterOpts = (workspaceId: string) => {
|
||||||
return useQuery({
|
return useQuery({
|
||||||
queryKey: workspaceKeys.getAuditLogActorFilterOpts(workspaceId),
|
queryKey: auditLogKeys.getAuditLogActorFilterOpts(workspaceId),
|
||||||
queryFn: async () => {
|
queryFn: async () => {
|
||||||
const { data } = await apiRequest.get<{ actors: Actor[] }>(
|
const { data } = await apiRequest.get<{ actors: Actor[] }>(
|
||||||
`/api/v1/workspace/${workspaceId}/audit-logs/filters/actors`
|
`/api/v1/workspace/${workspaceId}/audit-logs/filters/actors`
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import { apiRequest } from "@app/config/request";
|
|||||||
import { setAuthToken } from "@app/reactQuery";
|
import { setAuthToken } from "@app/reactQuery";
|
||||||
|
|
||||||
import { organizationKeys } from "../organization/queries";
|
import { organizationKeys } from "../organization/queries";
|
||||||
import { workspaceKeys } from "../workspace/queries";
|
import { workspaceKeys } from "../workspace";
|
||||||
import {
|
import {
|
||||||
ChangePasswordDTO,
|
ChangePasswordDTO,
|
||||||
CompleteAccountDTO,
|
CompleteAccountDTO,
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import { useMutation, useQueryClient } from "@tanstack/react-query";
|
|||||||
|
|
||||||
import { apiRequest } from "@app/config/request";
|
import { apiRequest } from "@app/config/request";
|
||||||
|
|
||||||
import { workspaceKeys } from "../workspace/queries";
|
import { workspaceKeys } from "../workspace";
|
||||||
import { caKeys } from "./queries";
|
import { caKeys } from "./queries";
|
||||||
import {
|
import {
|
||||||
TCertificateAuthority,
|
TCertificateAuthority,
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import { useMutation, useQueryClient } from "@tanstack/react-query";
|
|||||||
import { apiRequest } from "@app/config/request";
|
import { apiRequest } from "@app/config/request";
|
||||||
|
|
||||||
import { caKeys } from "../ca/queries";
|
import { caKeys } from "../ca/queries";
|
||||||
import { workspaceKeys } from "../workspace/queries";
|
import { workspaceKeys } from "../workspace";
|
||||||
import { certTemplateKeys } from "./queries";
|
import { certTemplateKeys } from "./queries";
|
||||||
import {
|
import {
|
||||||
TCertificateTemplate,
|
TCertificateTemplate,
|
||||||
@@ -54,7 +54,9 @@ export const useDeleteCertTemplate = () => {
|
|||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
return useMutation<TCertificateTemplate, {}, TDeleteCertificateTemplateDTO>({
|
return useMutation<TCertificateTemplate, {}, TDeleteCertificateTemplateDTO>({
|
||||||
mutationFn: async (data) => {
|
mutationFn: async (data) => {
|
||||||
const { data: certificateTemplate } = await apiRequest.delete<TCertificateTemplate>(`/api/v1/pki/certificate-templates/${data.id}`);
|
const { data: certificateTemplate } = await apiRequest.delete<TCertificateTemplate>(
|
||||||
|
`/api/v1/pki/certificate-templates/${data.id}`
|
||||||
|
);
|
||||||
return certificateTemplate;
|
return certificateTemplate;
|
||||||
},
|
},
|
||||||
onSuccess: ({ caId }, { projectId, id }) => {
|
onSuccess: ({ caId }, { projectId, id }) => {
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import { useMutation, useQueryClient } from "@tanstack/react-query";
|
|||||||
|
|
||||||
import { apiRequest } from "@app/config/request";
|
import { apiRequest } from "@app/config/request";
|
||||||
|
|
||||||
import { workspaceKeys } from "../workspace/queries";
|
import { workspaceKeys } from "../workspace";
|
||||||
import { TCertificate, TDeleteCertDTO, TRevokeCertDTO } from "./types";
|
import { TCertificate, TDeleteCertDTO, TRevokeCertDTO } from "./types";
|
||||||
|
|
||||||
export const useDeleteCert = () => {
|
export const useDeleteCert = () => {
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import { useMutation, useQueryClient } from "@tanstack/react-query";
|
|||||||
import { apiRequest } from "@app/config/request";
|
import { apiRequest } from "@app/config/request";
|
||||||
|
|
||||||
import { organizationKeys } from "../organization/queries";
|
import { organizationKeys } from "../organization/queries";
|
||||||
import { userKeys } from "../users/queries";
|
import { userKeys } from "../users/query-keys";
|
||||||
import { groupKeys } from "./queries";
|
import { groupKeys } from "./queries";
|
||||||
import { TGroup } from "./types";
|
import { TGroup } from "./types";
|
||||||
|
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
|||||||
|
|
||||||
import { apiRequest } from "@app/config/request";
|
import { apiRequest } from "@app/config/request";
|
||||||
|
|
||||||
import { workspaceKeys } from "../workspace/queries";
|
import { workspaceKeys } from "../workspace";
|
||||||
import {
|
import {
|
||||||
App,
|
App,
|
||||||
BitBucketWorkspace,
|
BitBucketWorkspace,
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
|||||||
import { createNotification } from "@app/components/notifications";
|
import { createNotification } from "@app/components/notifications";
|
||||||
import { apiRequest } from "@app/config/request";
|
import { apiRequest } from "@app/config/request";
|
||||||
|
|
||||||
import { workspaceKeys } from "../workspace/queries";
|
import { workspaceKeys } from "../workspace";
|
||||||
import { TCloudIntegration } from "./types";
|
import { TCloudIntegration } from "./types";
|
||||||
|
|
||||||
export const integrationQueryKeys = {
|
export const integrationQueryKeys = {
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import { useMutation, useQueryClient } from "@tanstack/react-query";
|
|||||||
|
|
||||||
import { apiRequest } from "@app/config/request";
|
import { apiRequest } from "@app/config/request";
|
||||||
|
|
||||||
import { workspaceKeys } from "../workspace/queries";
|
import { workspaceKeys } from "../workspace";
|
||||||
import { pkiAlertKeys } from "./queries";
|
import { pkiAlertKeys } from "./queries";
|
||||||
import { TCreatePkiAlertDTO, TDeletePkiAlertDTO, TPkiAlert, TUpdatePkiAlertDTO } from "./types";
|
import { TCreatePkiAlertDTO, TDeletePkiAlertDTO, TPkiAlert, TUpdatePkiAlertDTO } from "./types";
|
||||||
|
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import { useMutation, useQueryClient } from "@tanstack/react-query";
|
|||||||
|
|
||||||
import { apiRequest } from "@app/config/request";
|
import { apiRequest } from "@app/config/request";
|
||||||
|
|
||||||
import { workspaceKeys } from "../workspace/queries";
|
import { workspaceKeys } from "../workspace";
|
||||||
import { pkiCollectionKeys } from "./queries";
|
import { pkiCollectionKeys } from "./queries";
|
||||||
import {
|
import {
|
||||||
TAddItemToPkiCollectionDTO,
|
TAddItemToPkiCollectionDTO,
|
||||||
@@ -11,7 +11,8 @@ import {
|
|||||||
TPkiCollection,
|
TPkiCollection,
|
||||||
TPkiCollectionItem,
|
TPkiCollectionItem,
|
||||||
TRemoveItemFromPkiCollectionDTO,
|
TRemoveItemFromPkiCollectionDTO,
|
||||||
TUpdatePkiCollectionTO} from "./types";
|
TUpdatePkiCollectionTO
|
||||||
|
} from "./types";
|
||||||
|
|
||||||
export const useCreatePkiCollection = () => {
|
export const useCreatePkiCollection = () => {
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
|
|||||||
@@ -29,3 +29,4 @@ export {
|
|||||||
useUpdateOrgMembership,
|
useUpdateOrgMembership,
|
||||||
useUpdateUserAuthMethods
|
useUpdateUserAuthMethods
|
||||||
} from "./queries";
|
} from "./queries";
|
||||||
|
export { userKeys } from "./query-keys";
|
||||||
|
|||||||
@@ -6,8 +6,8 @@ import {
|
|||||||
} from "@app/components/utilities/cryptography/crypto";
|
} from "@app/components/utilities/cryptography/crypto";
|
||||||
import { apiRequest } from "@app/config/request";
|
import { apiRequest } from "@app/config/request";
|
||||||
|
|
||||||
import { workspaceKeys } from "../workspace/queries";
|
import { workspaceKeys } from "../workspace";
|
||||||
import { userKeys } from "./queries";
|
import { userKeys } from "./query-keys";
|
||||||
import { AddUserToWsDTOE2EE, AddUserToWsDTONonE2EE } from "./types";
|
import { AddUserToWsDTOE2EE, AddUserToWsDTONonE2EE } from "./types";
|
||||||
|
|
||||||
export const useAddUserToWsE2EE = () => {
|
export const useAddUserToWsE2EE = () => {
|
||||||
@@ -51,9 +51,10 @@ export const useAddUserToWsNonE2EE = () => {
|
|||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
|
|
||||||
return useMutation<{}, {}, AddUserToWsDTONonE2EE>({
|
return useMutation<{}, {}, AddUserToWsDTONonE2EE>({
|
||||||
mutationFn: async ({ projectId, usernames }) => {
|
mutationFn: async ({ projectId, usernames, roleSlugs }) => {
|
||||||
const { data } = await apiRequest.post(`/api/v2/workspace/${projectId}/memberships`, {
|
const { data } = await apiRequest.post(`/api/v2/workspace/${projectId}/memberships`, {
|
||||||
usernames
|
usernames,
|
||||||
|
roleSlugs
|
||||||
});
|
});
|
||||||
return data;
|
return data;
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -6,6 +6,8 @@ import { setAuthToken } from "@app/reactQuery";
|
|||||||
|
|
||||||
import { APIKeyDataV2 } from "../apiKeys/types";
|
import { APIKeyDataV2 } from "../apiKeys/types";
|
||||||
import { TGroupWithProjectMemberships } from "../groups/types";
|
import { TGroupWithProjectMemberships } from "../groups/types";
|
||||||
|
import { workspaceKeys } from "../workspace";
|
||||||
|
import { userKeys } from "./query-keys";
|
||||||
import {
|
import {
|
||||||
AddUserToOrgDTO,
|
AddUserToOrgDTO,
|
||||||
APIKeyData,
|
APIKeyData,
|
||||||
@@ -21,29 +23,6 @@ import {
|
|||||||
UserEnc
|
UserEnc
|
||||||
} from "./types";
|
} from "./types";
|
||||||
|
|
||||||
export const userKeys = {
|
|
||||||
getUser: ["user"] as const,
|
|
||||||
getPrivateKey: ["user"] as const,
|
|
||||||
userAction: ["user-action"] as const,
|
|
||||||
userProjectFavorites: (orgId: string) => [{ orgId }, "user-project-favorites"] as const,
|
|
||||||
getOrgMembership: (orgId: string, orgMembershipId: string) =>
|
|
||||||
[{ orgId, orgMembershipId }, "org-membership"] as const,
|
|
||||||
allOrgMembershipProjectMemberships: (orgId: string) => [orgId, "all-user-memberships"] as const,
|
|
||||||
forOrgMembershipProjectMemberships: (orgId: string, orgMembershipId: string) =>
|
|
||||||
[...userKeys.allOrgMembershipProjectMemberships(orgId), { orgMembershipId }] as const,
|
|
||||||
getOrgMembershipProjectMemberships: (orgId: string, username: string) =>
|
|
||||||
[{ orgId, username }, "org-membership-project-memberships"] as const,
|
|
||||||
getOrgUsers: (orgId: string) => [{ orgId }, "user"],
|
|
||||||
myIp: ["ip"] as const,
|
|
||||||
myAPIKeys: ["api-keys"] as const,
|
|
||||||
myAPIKeysV2: ["api-keys-v2"] as const,
|
|
||||||
mySessions: ["sessions"] as const,
|
|
||||||
listUsers: ["user-list"] as const,
|
|
||||||
listUserGroupMemberships: (username: string) => ["user-group-memberships", username] as const,
|
|
||||||
|
|
||||||
myOrganizationProjects: (orgId: string) => [{ orgId }, "organization-projects"] as const
|
|
||||||
};
|
|
||||||
|
|
||||||
export const fetchUserDetails = async () => {
|
export const fetchUserDetails = async () => {
|
||||||
const { data } = await apiRequest.get<{ user: User & UserEnc }>("/api/v1/user");
|
const { data } = await apiRequest.get<{ user: User & UserEnc }>("/api/v1/user");
|
||||||
|
|
||||||
@@ -175,8 +154,15 @@ export const useAddUsersToOrg = () => {
|
|||||||
mutationFn: (dto) => {
|
mutationFn: (dto) => {
|
||||||
return apiRequest.post("/api/v1/invite-org/signup", dto);
|
return apiRequest.post("/api/v1/invite-org/signup", dto);
|
||||||
},
|
},
|
||||||
onSuccess: (_, { organizationId }) => {
|
onSuccess: (_, { organizationId, projects }) => {
|
||||||
queryClient.invalidateQueries(userKeys.getOrgUsers(organizationId));
|
queryClient.invalidateQueries(userKeys.getOrgUsers(organizationId));
|
||||||
|
|
||||||
|
projects?.forEach((project) => {
|
||||||
|
if (project.slug) {
|
||||||
|
queryClient.invalidateQueries(workspaceKeys.getWorkspaceGroupMemberships(project.slug));
|
||||||
|
}
|
||||||
|
queryClient.invalidateQueries(workspaceKeys.getWorkspaceUsers(project.id));
|
||||||
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -0,0 +1,21 @@
|
|||||||
|
export const userKeys = {
|
||||||
|
getUser: ["user"] as const,
|
||||||
|
getPrivateKey: ["user"] as const,
|
||||||
|
userAction: ["user-action"] as const,
|
||||||
|
userProjectFavorites: (orgId: string) => [{ orgId }, "user-project-favorites"] as const,
|
||||||
|
getOrgMembership: (orgId: string, orgMembershipId: string) =>
|
||||||
|
[{ orgId, orgMembershipId }, "org-membership"] as const,
|
||||||
|
allOrgMembershipProjectMemberships: (orgId: string) => [orgId, "all-user-memberships"] as const,
|
||||||
|
forOrgMembershipProjectMemberships: (orgId: string, orgMembershipId: string) =>
|
||||||
|
[...userKeys.allOrgMembershipProjectMemberships(orgId), { orgMembershipId }] as const,
|
||||||
|
getOrgMembershipProjectMemberships: (orgId: string, username: string) =>
|
||||||
|
[{ orgId, username }, "org-membership-project-memberships"] as const,
|
||||||
|
getOrgUsers: (orgId: string) => [{ orgId }, "user"],
|
||||||
|
myIp: ["ip"] as const,
|
||||||
|
myAPIKeys: ["api-keys"] as const,
|
||||||
|
myAPIKeysV2: ["api-keys-v2"] as const,
|
||||||
|
mySessions: ["sessions"] as const,
|
||||||
|
listUsers: ["user-list"] as const,
|
||||||
|
listUserGroupMemberships: (username: string) => [{ username }, "user-group-memberships"] as const,
|
||||||
|
myOrganizationProjects: (orgId: string) => [{ orgId }, "organization-projects"] as const
|
||||||
|
};
|
||||||
@@ -133,6 +133,7 @@ export type AddUserToWsDTOE2EE = {
|
|||||||
export type AddUserToWsDTONonE2EE = {
|
export type AddUserToWsDTONonE2EE = {
|
||||||
projectId: string;
|
projectId: string;
|
||||||
usernames: string[];
|
usernames: string[];
|
||||||
|
roleSlugs?: string[];
|
||||||
orgId: string;
|
orgId: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -150,9 +151,11 @@ export type DeletOrgMembershipDTO = {
|
|||||||
|
|
||||||
export type AddUserToOrgDTO = {
|
export type AddUserToOrgDTO = {
|
||||||
inviteeEmails: string[];
|
inviteeEmails: string[];
|
||||||
projects?: { id: string; projectRoleSlug: string[] }[];
|
|
||||||
organizationRoleSlug: string;
|
organizationRoleSlug: string;
|
||||||
organizationId: string;
|
organizationId: string;
|
||||||
|
|
||||||
|
// We need the slug in order to invalidate the groups query. `slug` is only used for invalidation purposes.
|
||||||
|
projects?: { id: string; slug?: string; projectRoleSlug: string[] }[];
|
||||||
};
|
};
|
||||||
|
|
||||||
export type CreateAPIKeyRes = {
|
export type CreateAPIKeyRes = {
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import { useMutation, useQueryClient } from "@tanstack/react-query";
|
|||||||
|
|
||||||
import { apiRequest } from "@app/config/request";
|
import { apiRequest } from "@app/config/request";
|
||||||
|
|
||||||
import { workspaceKeys } from "../workspace/queries";
|
import { workspaceKeys } from "../workspace/query-keys";
|
||||||
import { workflowIntegrationKeys } from "./queries";
|
import { workflowIntegrationKeys } from "./queries";
|
||||||
import {
|
import {
|
||||||
TDeleteSlackIntegrationDTO,
|
TDeleteSlackIntegrationDTO,
|
||||||
|
|||||||
@@ -38,3 +38,4 @@ export {
|
|||||||
useUpdateWsEnvironment,
|
useUpdateWsEnvironment,
|
||||||
useUpgradeProject
|
useUpgradeProject
|
||||||
} from "./queries";
|
} from "./queries";
|
||||||
|
export { workspaceKeys } from "./query-keys";
|
||||||
|
|||||||
@@ -2,8 +2,8 @@ import { useMutation, useQueryClient } from "@tanstack/react-query";
|
|||||||
|
|
||||||
import { apiRequest } from "@app/config/request";
|
import { apiRequest } from "@app/config/request";
|
||||||
|
|
||||||
import { userKeys } from "../users/queries";
|
import { userKeys } from "../users/query-keys";
|
||||||
import { workspaceKeys } from "./queries";
|
import { workspaceKeys } from "./query-keys";
|
||||||
import { TUpdateWorkspaceGroupRoleDTO } from "./types";
|
import { TUpdateWorkspaceGroupRoleDTO } from "./types";
|
||||||
|
|
||||||
export const useAddGroupToWorkspace = () => {
|
export const useAddGroupToWorkspace = () => {
|
||||||
|
|||||||
@@ -14,9 +14,10 @@ import { TIntegration } from "../integrations/types";
|
|||||||
import { TPkiAlert } from "../pkiAlerts/types";
|
import { TPkiAlert } from "../pkiAlerts/types";
|
||||||
import { TPkiCollection } from "../pkiCollections/types";
|
import { TPkiCollection } from "../pkiCollections/types";
|
||||||
import { EncryptedSecret } from "../secrets/types";
|
import { EncryptedSecret } from "../secrets/types";
|
||||||
import { userKeys } from "../users/queries";
|
import { userKeys } from "../users/query-keys";
|
||||||
import { TWorkspaceUser } from "../users/types";
|
import { TWorkspaceUser } from "../users/types";
|
||||||
import { ProjectSlackConfig } from "../workflowIntegrations/types";
|
import { ProjectSlackConfig } from "../workflowIntegrations/types";
|
||||||
|
import { workspaceKeys } from "./query-keys";
|
||||||
import {
|
import {
|
||||||
CreateEnvironmentDTO,
|
CreateEnvironmentDTO,
|
||||||
CreateWorkspaceDTO,
|
CreateWorkspaceDTO,
|
||||||
@@ -34,49 +35,6 @@ import {
|
|||||||
Workspace
|
Workspace
|
||||||
} from "./types";
|
} from "./types";
|
||||||
|
|
||||||
export const workspaceKeys = {
|
|
||||||
getWorkspaceById: (workspaceId: string) => [{ workspaceId }, "workspace"] as const,
|
|
||||||
getWorkspaceSecrets: (workspaceId: string) => [{ workspaceId }, "workspace-secrets"] as const,
|
|
||||||
getWorkspaceIndexStatus: (workspaceId: string) =>
|
|
||||||
[{ workspaceId }, "workspace-index-status"] as const,
|
|
||||||
getProjectUpgradeStatus: (workspaceId: string) => [{ workspaceId }, "workspace-upgrade-status"],
|
|
||||||
getWorkspaceMemberships: (orgId: string) => [{ orgId }, "workspace-memberships"],
|
|
||||||
getWorkspaceAuthorization: (workspaceId: string) => [{ workspaceId }, "workspace-authorizations"],
|
|
||||||
getWorkspaceIntegrations: (workspaceId: string) => [{ workspaceId }, "workspace-integrations"],
|
|
||||||
getAllUserWorkspace: ["workspaces"] as const,
|
|
||||||
getWorkspaceAuditLogs: (workspaceId: string) =>
|
|
||||||
[{ workspaceId }, "workspace-audit-logs"] as const,
|
|
||||||
getWorkspaceUsers: (workspaceId: string) => [{ workspaceId }, "workspace-users"] as const,
|
|
||||||
getWorkspaceIdentityMemberships: (workspaceId: string) =>
|
|
||||||
[{ workspaceId }, "workspace-identity-memberships"] as const,
|
|
||||||
getWorkspaceGroupMemberships: (workspaceId: string) =>
|
|
||||||
[{ workspaceId }, "workspace-groups"] as const,
|
|
||||||
getWorkspaceCas: ({ projectSlug }: { projectSlug: string }) =>
|
|
||||||
[{ projectSlug }, "workspace-cas"] as const,
|
|
||||||
specificWorkspaceCas: ({ projectSlug, status }: { projectSlug: string; status?: CaStatus }) =>
|
|
||||||
[...workspaceKeys.getWorkspaceCas({ projectSlug }), { status }] as const,
|
|
||||||
allWorkspaceCertificates: () => ["workspace-certificates"] as const,
|
|
||||||
forWorkspaceCertificates: (slug: string) =>
|
|
||||||
[...workspaceKeys.allWorkspaceCertificates(), slug] as const,
|
|
||||||
specificWorkspaceCertificates: ({
|
|
||||||
slug,
|
|
||||||
offset,
|
|
||||||
limit
|
|
||||||
}: {
|
|
||||||
slug: string;
|
|
||||||
offset: number;
|
|
||||||
limit: number;
|
|
||||||
}) => [...workspaceKeys.forWorkspaceCertificates(slug), { offset, limit }] as const,
|
|
||||||
getWorkspacePkiAlerts: (workspaceId: string) =>
|
|
||||||
[{ workspaceId }, "workspace-pki-alerts"] as const,
|
|
||||||
getWorkspacePkiCollections: (workspaceId: string) =>
|
|
||||||
[{ workspaceId }, "workspace-pki-collections"] as const,
|
|
||||||
getWorkspaceCertificateTemplates: (workspaceId: string) =>
|
|
||||||
[{ workspaceId }, "workspace-certificate-templates"] as const,
|
|
||||||
getWorkspaceSlackConfig: (workspaceId: string) =>
|
|
||||||
[{ workspaceId }, "workspace-slack-config"] as const
|
|
||||||
};
|
|
||||||
|
|
||||||
const fetchWorkspaceById = async (workspaceId: string) => {
|
const fetchWorkspaceById = async (workspaceId: string) => {
|
||||||
const { data } = await apiRequest.get<{ workspace: Workspace }>(
|
const { data } = await apiRequest.get<{ workspace: Workspace }>(
|
||||||
`/api/v1/workspace/${workspaceId}`
|
`/api/v1/workspace/${workspaceId}`
|
||||||
|
|||||||
@@ -0,0 +1,44 @@
|
|||||||
|
import type { CaStatus } from "../ca";
|
||||||
|
|
||||||
|
export const workspaceKeys = {
|
||||||
|
getWorkspaceById: (workspaceId: string) => [{ workspaceId }, "workspace"] as const,
|
||||||
|
getWorkspaceSecrets: (workspaceId: string) => [{ workspaceId }, "workspace-secrets"] as const,
|
||||||
|
getWorkspaceIndexStatus: (workspaceId: string) =>
|
||||||
|
[{ workspaceId }, "workspace-index-status"] as const,
|
||||||
|
getProjectUpgradeStatus: (workspaceId: string) => [{ workspaceId }, "workspace-upgrade-status"],
|
||||||
|
getWorkspaceMemberships: (orgId: string) => [{ orgId }, "workspace-memberships"],
|
||||||
|
getWorkspaceAuthorization: (workspaceId: string) => [{ workspaceId }, "workspace-authorizations"],
|
||||||
|
getWorkspaceIntegrations: (workspaceId: string) => [{ workspaceId }, "workspace-integrations"],
|
||||||
|
getAllUserWorkspace: ["workspaces"] as const,
|
||||||
|
getWorkspaceAuditLogs: (workspaceId: string) =>
|
||||||
|
[{ workspaceId }, "workspace-audit-logs"] as const,
|
||||||
|
getWorkspaceUsers: (workspaceId: string) => [{ workspaceId }, "workspace-users"] as const,
|
||||||
|
getWorkspaceIdentityMemberships: (workspaceId: string) =>
|
||||||
|
[{ workspaceId }, "workspace-identity-memberships"] as const,
|
||||||
|
getWorkspaceGroupMemberships: (workspaceId: string) =>
|
||||||
|
[{ workspaceId }, "workspace-groups"] as const,
|
||||||
|
getWorkspaceCas: ({ projectSlug }: { projectSlug: string }) =>
|
||||||
|
[{ projectSlug }, "workspace-cas"] as const,
|
||||||
|
specificWorkspaceCas: ({ projectSlug, status }: { projectSlug: string; status?: CaStatus }) =>
|
||||||
|
[...workspaceKeys.getWorkspaceCas({ projectSlug }), { status }] as const,
|
||||||
|
allWorkspaceCertificates: () => ["workspace-certificates"] as const,
|
||||||
|
forWorkspaceCertificates: (slug: string) =>
|
||||||
|
[...workspaceKeys.allWorkspaceCertificates(), slug] as const,
|
||||||
|
specificWorkspaceCertificates: ({
|
||||||
|
slug,
|
||||||
|
offset,
|
||||||
|
limit
|
||||||
|
}: {
|
||||||
|
slug: string;
|
||||||
|
offset: number;
|
||||||
|
limit: number;
|
||||||
|
}) => [...workspaceKeys.forWorkspaceCertificates(slug), { offset, limit }] as const,
|
||||||
|
getWorkspacePkiAlerts: (workspaceId: string) =>
|
||||||
|
[{ workspaceId }, "workspace-pki-alerts"] as const,
|
||||||
|
getWorkspacePkiCollections: (workspaceId: string) =>
|
||||||
|
[{ workspaceId }, "workspace-pki-collections"] as const,
|
||||||
|
getWorkspaceCertificateTemplates: (workspaceId: string) =>
|
||||||
|
[{ workspaceId }, "workspace-certificate-templates"] as const,
|
||||||
|
getWorkspaceSlackConfig: (workspaceId: string) =>
|
||||||
|
[{ workspaceId }, "workspace-slack-config"] as const
|
||||||
|
};
|
||||||
@@ -3,7 +3,7 @@ import { NextRouter, useRouter } from "next/router";
|
|||||||
import { useServerConfig } from "@app/context";
|
import { useServerConfig } from "@app/context";
|
||||||
import { useSelectOrganization } from "@app/hooks/api";
|
import { useSelectOrganization } from "@app/hooks/api";
|
||||||
import { fetchOrganizations } from "@app/hooks/api/organization/queries";
|
import { fetchOrganizations } from "@app/hooks/api/organization/queries";
|
||||||
import { userKeys } from "@app/hooks/api/users/queries";
|
import { userKeys } from "@app/hooks/api/users";
|
||||||
import { queryClient } from "@app/reactQuery";
|
import { queryClient } from "@app/reactQuery";
|
||||||
|
|
||||||
export const navigateUserToOrg = async (router: NextRouter, organizationId?: string) => {
|
export const navigateUserToOrg = async (router: NextRouter, organizationId?: string) => {
|
||||||
|
|||||||
+214
-48
@@ -2,24 +2,37 @@ import { useMemo } from "react";
|
|||||||
import { Controller, useForm } from "react-hook-form";
|
import { Controller, useForm } from "react-hook-form";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
|
import { faCheckCircle, faChevronDown } from "@fortawesome/free-solid-svg-icons";
|
||||||
|
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||||
import { zodResolver } from "@hookform/resolvers/zod";
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
|
import { twMerge } from "tailwind-merge";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
|
||||||
import { createNotification } from "@app/components/notifications";
|
import { createNotification } from "@app/components/notifications";
|
||||||
import { Button, FormControl, Modal, ModalContent, Select, SelectItem } from "@app/components/v2";
|
import {
|
||||||
|
Button,
|
||||||
|
DropdownMenu,
|
||||||
|
DropdownMenuContent,
|
||||||
|
DropdownMenuItem,
|
||||||
|
DropdownMenuTrigger,
|
||||||
|
FormControl,
|
||||||
|
Modal,
|
||||||
|
ModalContent
|
||||||
|
} from "@app/components/v2";
|
||||||
import { useOrganization, useWorkspace } from "@app/context";
|
import { useOrganization, useWorkspace } from "@app/context";
|
||||||
import {
|
import {
|
||||||
useAddUserToWsE2EE,
|
useAddUsersToOrg,
|
||||||
useAddUserToWsNonE2EE,
|
|
||||||
useGetOrgUsers,
|
useGetOrgUsers,
|
||||||
useGetUserWsKey,
|
useGetProjectRoles,
|
||||||
useGetWorkspaceUsers
|
useGetWorkspaceUsers
|
||||||
} from "@app/hooks/api";
|
} from "@app/hooks/api";
|
||||||
|
import { ProjectMembershipRole } from "@app/hooks/api/roles/types";
|
||||||
import { ProjectVersion } from "@app/hooks/api/workspace/types";
|
import { ProjectVersion } from "@app/hooks/api/workspace/types";
|
||||||
import { UsePopUpState } from "@app/hooks/usePopUp";
|
import { UsePopUpState } from "@app/hooks/usePopUp";
|
||||||
|
|
||||||
const addMemberFormSchema = z.object({
|
const addMemberFormSchema = z.object({
|
||||||
orgMembershipId: z.string().trim()
|
orgMembershipIds: z.array(z.string().trim()).min(1),
|
||||||
|
projectRoleSlugs: z.array(z.string().trim().min(1)).min(1)
|
||||||
});
|
});
|
||||||
|
|
||||||
type TAddMemberForm = z.infer<typeof addMemberFormSchema>;
|
type TAddMemberForm = z.infer<typeof addMemberFormSchema>;
|
||||||
@@ -37,48 +50,52 @@ export const AddMemberModal = ({ popUp, handlePopUpToggle }: Props) => {
|
|||||||
const orgId = currentOrg?.id || "";
|
const orgId = currentOrg?.id || "";
|
||||||
const workspaceId = currentWorkspace?.id || "";
|
const workspaceId = currentWorkspace?.id || "";
|
||||||
|
|
||||||
const { data: wsKey } = useGetUserWsKey(workspaceId);
|
|
||||||
const { data: members } = useGetWorkspaceUsers(workspaceId);
|
const { data: members } = useGetWorkspaceUsers(workspaceId);
|
||||||
const { data: orgUsers } = useGetOrgUsers(orgId);
|
const { data: orgUsers } = useGetOrgUsers(orgId);
|
||||||
|
|
||||||
const { mutateAsync: addUserToWorkspace } = useAddUserToWsE2EE();
|
const { data: roles } = useGetProjectRoles(currentWorkspace?.slug || "");
|
||||||
const { mutateAsync: addUserToWorkspaceNonE2EE } = useAddUserToWsNonE2EE();
|
|
||||||
|
|
||||||
const {
|
const {
|
||||||
control,
|
control,
|
||||||
handleSubmit,
|
handleSubmit,
|
||||||
reset,
|
reset,
|
||||||
|
watch,
|
||||||
formState: { isSubmitting }
|
formState: { isSubmitting }
|
||||||
} = useForm<TAddMemberForm>({ resolver: zodResolver(addMemberFormSchema) });
|
} = useForm<TAddMemberForm>({
|
||||||
|
resolver: zodResolver(addMemberFormSchema),
|
||||||
|
defaultValues: { orgMembershipIds: [], projectRoleSlugs: [ProjectMembershipRole.Member] }
|
||||||
|
});
|
||||||
|
|
||||||
const onAddMember = async ({ orgMembershipId }: TAddMemberForm) => {
|
const { mutateAsync: addMembersToProject } = useAddUsersToOrg();
|
||||||
|
|
||||||
|
const onAddMember = async ({ orgMembershipIds, projectRoleSlugs }: TAddMemberForm) => {
|
||||||
if (!currentWorkspace) return;
|
if (!currentWorkspace) return;
|
||||||
if (!currentOrg?.id) return;
|
if (!currentOrg?.id) return;
|
||||||
// TODO(akhilmhdh): Move to memory storage
|
|
||||||
const userPrivateKey = localStorage.getItem("PRIVATE_KEY");
|
const selectedMembers = orgMembershipIds.map((orgMembershipId) =>
|
||||||
if (!userPrivateKey || !wsKey) {
|
orgUsers?.find((orgUser) => orgUser.id === orgMembershipId)
|
||||||
createNotification({
|
);
|
||||||
text: "Failed to find private key. Try re-login"
|
|
||||||
});
|
if (!selectedMembers) return;
|
||||||
return;
|
|
||||||
}
|
|
||||||
const orgUser = (orgUsers || []).find(({ id }) => id === orgMembershipId);
|
|
||||||
if (!orgUser) return;
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// TODO: update
|
|
||||||
if (currentWorkspace.version === ProjectVersion.V1) {
|
if (currentWorkspace.version === ProjectVersion.V1) {
|
||||||
await addUserToWorkspace({
|
createNotification({
|
||||||
workspaceId,
|
type: "error",
|
||||||
userPrivateKey,
|
text: "Please upgrade your project to invite new members to the project."
|
||||||
decryptKey: wsKey,
|
|
||||||
members: [{ orgMembershipId, userPublicKey: orgUser.user.publicKey }]
|
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
await addUserToWorkspaceNonE2EE({
|
await addMembersToProject({
|
||||||
projectId: workspaceId,
|
inviteeEmails: selectedMembers.map((member) => member?.user.username!),
|
||||||
usernames: [orgUser.user.username],
|
organizationId: orgId,
|
||||||
orgId
|
organizationRoleSlug: ProjectMembershipRole.Member, // ? This doesn't apply in this case, because we know the users being added are already part of the organization
|
||||||
|
projects: [
|
||||||
|
{
|
||||||
|
slug: currentWorkspace.slug,
|
||||||
|
id: currentWorkspace.id,
|
||||||
|
projectRoleSlug: projectRoleSlugs
|
||||||
|
}
|
||||||
|
]
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
createNotification({
|
createNotification({
|
||||||
@@ -104,6 +121,9 @@ export const AddMemberModal = ({ popUp, handlePopUpToggle }: Props) => {
|
|||||||
return (orgUsers || []).filter(({ user: u }) => !wsUserUsernames.has(u.username));
|
return (orgUsers || []).filter(({ user: u }) => !wsUserUsernames.has(u.username));
|
||||||
}, [orgUsers, members]);
|
}, [orgUsers, members]);
|
||||||
|
|
||||||
|
const selectedOrgMembershipIds = watch("orgMembershipIds");
|
||||||
|
const selectedRoleSlugs = watch("projectRoleSlugs");
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Modal
|
<Modal
|
||||||
isOpen={popUp?.addMember?.isOpen}
|
isOpen={popUp?.addMember?.isOpen}
|
||||||
@@ -115,37 +135,183 @@ export const AddMemberModal = ({ popUp, handlePopUpToggle }: Props) => {
|
|||||||
>
|
>
|
||||||
{filteredOrgUsers.length ? (
|
{filteredOrgUsers.length ? (
|
||||||
<form onSubmit={handleSubmit(onAddMember)}>
|
<form onSubmit={handleSubmit(onAddMember)}>
|
||||||
|
<div className="flex w-full items-center gap-2">
|
||||||
|
<div className="w-full">
|
||||||
<Controller
|
<Controller
|
||||||
control={control}
|
control={control}
|
||||||
defaultValue={filteredOrgUsers?.[0]?.user?.username}
|
name="orgMembershipIds"
|
||||||
name="orgMembershipId"
|
render={({ field }) => (
|
||||||
render={({ field, fieldState: { error } }) => (
|
<FormControl label="Invite users to project">
|
||||||
<FormControl label="Username" isError={Boolean(error)} errorText={error?.message}>
|
<DropdownMenu>
|
||||||
<Select
|
<DropdownMenuTrigger asChild>
|
||||||
position="popper"
|
{filteredOrgUsers && filteredOrgUsers.length > 0 ? (
|
||||||
className="w-full"
|
<div className="inline-flex w-full cursor-pointer items-center justify-between rounded-md border border-mineshaft-600 bg-mineshaft-900 px-3 py-2 font-inter text-sm font-normal text-bunker-200 outline-none data-[placeholder]:text-mineshaft-200">
|
||||||
defaultValue={filteredOrgUsers?.[0]?.user?.username}
|
{/* eslint-disable-next-line no-nested-ternary */}
|
||||||
value={field.value}
|
{selectedOrgMembershipIds.length === 1
|
||||||
onValueChange={field.onChange}
|
? filteredOrgUsers.find(
|
||||||
|
(orgUser) => orgUser.id === selectedOrgMembershipIds[0]
|
||||||
|
)?.user.username
|
||||||
|
: selectedOrgMembershipIds.length === 0
|
||||||
|
? "No users selected"
|
||||||
|
: `${selectedOrgMembershipIds.length} users selected`}
|
||||||
|
<FontAwesomeIcon icon={faChevronDown} className="text-xs" />
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="inline-flex w-full cursor-default items-center justify-between rounded-md border border-mineshaft-600 bg-mineshaft-900 px-3 py-2 font-inter text-sm font-normal text-bunker-200 outline-none data-[placeholder]:text-mineshaft-200">
|
||||||
|
No users found
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</DropdownMenuTrigger>
|
||||||
|
<DropdownMenuContent
|
||||||
|
align="start"
|
||||||
|
className="thin-scrollbar z-[100] max-h-80"
|
||||||
>
|
>
|
||||||
{filteredOrgUsers.map(({ id: orgUserId, user: u }) => (
|
{filteredOrgUsers && filteredOrgUsers.length > 0 ? (
|
||||||
<SelectItem value={orgUserId} key={`org-membership-join-${orgUserId}`}>
|
filteredOrgUsers.map((member) => {
|
||||||
{u?.username}
|
const isSelected = selectedOrgMembershipIds.includes(member.id);
|
||||||
</SelectItem>
|
|
||||||
))}
|
return (
|
||||||
</Select>
|
<DropdownMenuItem
|
||||||
|
onSelect={(event) =>
|
||||||
|
filteredOrgUsers.length > 1 && event.preventDefault()
|
||||||
|
}
|
||||||
|
onClick={() => {
|
||||||
|
if (selectedOrgMembershipIds.includes(String(member.id))) {
|
||||||
|
field.onChange(
|
||||||
|
selectedOrgMembershipIds.filter(
|
||||||
|
(membershipId: string) =>
|
||||||
|
membershipId !== String(member.id)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
field.onChange([
|
||||||
|
...selectedOrgMembershipIds,
|
||||||
|
String(member.id)
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
key={`membership-id-${member.id}`}
|
||||||
|
icon={
|
||||||
|
isSelected ? (
|
||||||
|
<FontAwesomeIcon
|
||||||
|
icon={faCheckCircle}
|
||||||
|
className="pr-0.5 text-primary"
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<div className="pl-[1.01rem]" />
|
||||||
|
)
|
||||||
|
}
|
||||||
|
iconPos="left"
|
||||||
|
className="w-[28.4rem] text-sm"
|
||||||
|
>
|
||||||
|
{member.user.username}
|
||||||
|
</DropdownMenuItem>
|
||||||
|
);
|
||||||
|
})
|
||||||
|
) : (
|
||||||
|
<div />
|
||||||
|
)}
|
||||||
|
</DropdownMenuContent>
|
||||||
|
</DropdownMenu>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex min-w-fit justify-end">
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
|
name="projectRoleSlugs"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormControl
|
||||||
|
label="Select roles"
|
||||||
|
tooltipText="Select the roles that you wish to assign to the users"
|
||||||
|
>
|
||||||
|
<DropdownMenu>
|
||||||
|
<DropdownMenuTrigger asChild>
|
||||||
|
{roles && roles.length > 0 ? (
|
||||||
|
<div className="inline-flex w-full cursor-pointer items-center justify-between rounded-md border border-mineshaft-600 bg-mineshaft-900 px-3 py-2 font-inter text-sm font-normal text-bunker-200 outline-none data-[placeholder]:text-mineshaft-200">
|
||||||
|
{/* eslint-disable-next-line no-nested-ternary */}
|
||||||
|
{selectedRoleSlugs.length === 1
|
||||||
|
? roles.find((role) => role.slug === selectedRoleSlugs[0])?.name
|
||||||
|
: selectedRoleSlugs.length === 0
|
||||||
|
? "Select at least one role"
|
||||||
|
: `${selectedRoleSlugs.length} roles selected`}
|
||||||
|
<FontAwesomeIcon
|
||||||
|
icon={faChevronDown}
|
||||||
|
className={twMerge("ml-2 text-xs")}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="inline-flex w-full cursor-default items-center justify-between rounded-md border border-mineshaft-600 bg-mineshaft-900 px-3 py-2 font-inter text-sm font-normal text-bunker-200 outline-none data-[placeholder]:text-mineshaft-200">
|
||||||
|
No roles found
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</DropdownMenuTrigger>
|
||||||
|
<DropdownMenuContent
|
||||||
|
align="start"
|
||||||
|
className="thin-scrollbar z-[100] max-h-80"
|
||||||
|
>
|
||||||
|
{roles && roles.length > 0 ? (
|
||||||
|
roles.map((role) => {
|
||||||
|
const isSelected = selectedRoleSlugs.includes(role.slug);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<DropdownMenuItem
|
||||||
|
onSelect={(event) => roles.length > 1 && event.preventDefault()}
|
||||||
|
onClick={() => {
|
||||||
|
if (selectedRoleSlugs.includes(String(role.slug))) {
|
||||||
|
field.onChange(
|
||||||
|
selectedRoleSlugs.filter(
|
||||||
|
(roleSlug: string) => roleSlug !== String(role.slug)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
field.onChange([...selectedRoleSlugs, role.slug]);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
key={`role-slug-${role.slug}`}
|
||||||
|
icon={
|
||||||
|
isSelected ? (
|
||||||
|
<FontAwesomeIcon
|
||||||
|
icon={faCheckCircle}
|
||||||
|
className="pr-0.5 text-primary"
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<div className="pl-[1.01rem]" />
|
||||||
|
)
|
||||||
|
}
|
||||||
|
iconPos="left"
|
||||||
|
className="w-[28.4rem] text-sm"
|
||||||
|
>
|
||||||
|
{role.name}
|
||||||
|
</DropdownMenuItem>
|
||||||
|
);
|
||||||
|
})
|
||||||
|
) : (
|
||||||
|
<div />
|
||||||
|
)}
|
||||||
|
</DropdownMenuContent>
|
||||||
|
</DropdownMenu>
|
||||||
|
</FormControl>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div className="mt-8 flex items-center">
|
<div className="mt-8 flex items-center">
|
||||||
<Button
|
<Button
|
||||||
className="mr-4"
|
className="mr-4"
|
||||||
size="sm"
|
size="sm"
|
||||||
type="submit"
|
type="submit"
|
||||||
isLoading={isSubmitting}
|
isLoading={isSubmitting}
|
||||||
isDisabled={isSubmitting}
|
isDisabled={
|
||||||
|
isSubmitting ||
|
||||||
|
selectedOrgMembershipIds.length === 0 ||
|
||||||
|
selectedRoleSlugs.length === 0
|
||||||
|
}
|
||||||
>
|
>
|
||||||
Add Member
|
Add Members
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
colorSchema="secondary"
|
colorSchema="secondary"
|
||||||
|
|||||||
Reference in New Issue
Block a user