mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-10-07 21:27:31 +00:00
Feat: Specify organization role and projects when inviting users to org
This commit is contained in:
@@ -688,6 +688,7 @@ export const registerRoutes = async (
|
|||||||
orgDAL,
|
orgDAL,
|
||||||
orgService,
|
orgService,
|
||||||
projectMembershipDAL,
|
projectMembershipDAL,
|
||||||
|
projectRoleDAL,
|
||||||
folderDAL,
|
folderDAL,
|
||||||
licenseService,
|
licenseService,
|
||||||
certificateAuthorityDAL,
|
certificateAuthorityDAL,
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
|
||||||
import { UsersSchema } from "@app/db/schemas";
|
import { OrgMembershipRole, ProjectMembershipRole, UsersSchema } from "@app/db/schemas";
|
||||||
import { inviteUserRateLimit } from "@app/server/config/rateLimiter";
|
import { inviteUserRateLimit } from "@app/server/config/rateLimiter";
|
||||||
import { getTelemetryDistinctId } from "@app/server/lib/telemetry";
|
import { getTelemetryDistinctId } from "@app/server/lib/telemetry";
|
||||||
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
|
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
|
||||||
@@ -19,7 +19,8 @@ export const registerInviteOrgRouter = async (server: FastifyZodProvider) => {
|
|||||||
inviteeEmails: z.array(z.string().trim().email()),
|
inviteeEmails: z.array(z.string().trim().email()),
|
||||||
organizationId: z.string().trim(),
|
organizationId: z.string().trim(),
|
||||||
projectIds: z.array(z.string().trim()).optional(),
|
projectIds: z.array(z.string().trim()).optional(),
|
||||||
organizationRoleSlug: z.string().trim().optional()
|
projectRoleSlug: z.nativeEnum(ProjectMembershipRole).optional(),
|
||||||
|
organizationRoleSlug: z.nativeEnum(OrgMembershipRole)
|
||||||
}),
|
}),
|
||||||
response: {
|
response: {
|
||||||
200: z.object({
|
200: z.object({
|
||||||
@@ -44,6 +45,7 @@ export const registerInviteOrgRouter = async (server: FastifyZodProvider) => {
|
|||||||
userId: req.permission.id,
|
userId: req.permission.id,
|
||||||
inviteeEmails: req.body.inviteeEmails,
|
inviteeEmails: req.body.inviteeEmails,
|
||||||
projectIds: req.body.projectIds,
|
projectIds: req.body.projectIds,
|
||||||
|
projectRoleSlug: req.body.projectRoleSlug,
|
||||||
organizationRoleSlug: req.body.organizationRoleSlug,
|
organizationRoleSlug: req.body.organizationRoleSlug,
|
||||||
actorAuthMethod: req.permission.authMethod,
|
actorAuthMethod: req.permission.authMethod,
|
||||||
actorOrgId: req.permission.orgId
|
actorOrgId: req.permission.orgId
|
||||||
|
|||||||
@@ -1,6 +1,12 @@
|
|||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
|
||||||
import { IntegrationsSchema, ProjectMembershipsSchema, UserEncryptionKeysSchema, UsersSchema } from "@app/db/schemas";
|
import {
|
||||||
|
IntegrationsSchema,
|
||||||
|
ProjectMembershipsSchema,
|
||||||
|
ProjectRolesSchema,
|
||||||
|
UserEncryptionKeysSchema,
|
||||||
|
UsersSchema
|
||||||
|
} from "@app/db/schemas";
|
||||||
import { PROJECTS } from "@app/lib/api-docs";
|
import { PROJECTS } from "@app/lib/api-docs";
|
||||||
import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
|
import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
|
||||||
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
|
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
|
||||||
@@ -122,15 +128,31 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => {
|
|||||||
rateLimit: readLimit
|
rateLimit: readLimit
|
||||||
},
|
},
|
||||||
schema: {
|
schema: {
|
||||||
|
querystring: z.object({
|
||||||
|
includeRoles: z
|
||||||
|
.enum(["true", "false"])
|
||||||
|
.default("false")
|
||||||
|
.transform((value) => value === "true")
|
||||||
|
}),
|
||||||
response: {
|
response: {
|
||||||
200: z.object({
|
200: z.object({
|
||||||
workspaces: projectWithEnv.array()
|
workspaces: projectWithEnv
|
||||||
|
.extend({
|
||||||
|
roles: ProjectRolesSchema.array().optional()
|
||||||
|
})
|
||||||
|
.array()
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
onRequest: verifyAuth([AuthMode.JWT, AuthMode.API_KEY]),
|
onRequest: verifyAuth([AuthMode.JWT, AuthMode.API_KEY]),
|
||||||
handler: async (req) => {
|
handler: async (req) => {
|
||||||
const workspaces = await server.services.project.getProjects(req.permission.id);
|
const workspaces = await server.services.project.getProjects({
|
||||||
|
includeRoles: req.query.includeRoles,
|
||||||
|
actorId: req.permission.id,
|
||||||
|
actorAuthMethod: req.permission.authMethod,
|
||||||
|
actor: req.permission.type,
|
||||||
|
actorOrgId: req.permission.orgId
|
||||||
|
});
|
||||||
return { workspaces };
|
return { workspaces };
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
import { ProjectMembershipRole } from "@app/db/schemas";
|
||||||
|
|
||||||
export enum TokenType {
|
export enum TokenType {
|
||||||
TOKEN_EMAIL_CONFIRMATION = "emailConfirmation",
|
TOKEN_EMAIL_CONFIRMATION = "emailConfirmation",
|
||||||
TOKEN_EMAIL_VERIFICATION = "emailVerification", // unverified -> verified
|
TOKEN_EMAIL_VERIFICATION = "emailVerification", // unverified -> verified
|
||||||
@@ -54,9 +56,14 @@ export enum TokenMetadataType {
|
|||||||
InviteToProjects = "projects-invite"
|
InviteToProjects = "projects-invite"
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: Split the payloads into separate types for better type inference (not needed until we add more types of metadata)
|
export type TTokenInviteToProjectsMetadataPayload = {
|
||||||
|
projectIds: string[];
|
||||||
|
projectRoleSlug: ProjectMembershipRole;
|
||||||
|
userId: string;
|
||||||
|
orgId: string;
|
||||||
|
};
|
||||||
|
|
||||||
export type TTokenMetadata = {
|
export type TTokenMetadata = {
|
||||||
type: TokenMetadataType.InviteToProjects;
|
type: TokenMetadataType.InviteToProjects;
|
||||||
projectIds: string[];
|
payload: TTokenInviteToProjectsMetadataPayload;
|
||||||
userId: string;
|
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import bcrypt from "bcrypt";
|
import bcrypt from "bcrypt";
|
||||||
import jwt from "jsonwebtoken";
|
import jwt from "jsonwebtoken";
|
||||||
|
|
||||||
import { OrgMembershipStatus, ProjectMembershipRole, TableName } from "@app/db/schemas";
|
import { OrgMembershipStatus, TableName } from "@app/db/schemas";
|
||||||
import { convertPendingGroupAdditionsToGroupMemberships } from "@app/ee/services/group/group-fns";
|
import { convertPendingGroupAdditionsToGroupMemberships } from "@app/ee/services/group/group-fns";
|
||||||
import { TUserGroupMembershipDALFactory } from "@app/ee/services/group/user-group-membership-dal";
|
import { TUserGroupMembershipDALFactory } from "@app/ee/services/group/user-group-membership-dal";
|
||||||
import { TLicenseServiceFactory } from "@app/ee/services/license/license-service";
|
import { TLicenseServiceFactory } from "@app/ee/services/license/license-service";
|
||||||
@@ -17,7 +17,7 @@ import { TProjectBotDALFactory } from "@app/services/project-bot/project-bot-dal
|
|||||||
import { TProjectKeyDALFactory } from "@app/services/project-key/project-key-dal";
|
import { TProjectKeyDALFactory } from "@app/services/project-key/project-key-dal";
|
||||||
|
|
||||||
import { TAuthTokenServiceFactory } from "../auth-token/auth-token-service";
|
import { TAuthTokenServiceFactory } from "../auth-token/auth-token-service";
|
||||||
import { TokenType, TTokenMetadata } from "../auth-token/auth-token-types";
|
import { TokenMetadataType, TokenType, TTokenMetadata } from "../auth-token/auth-token-types";
|
||||||
import { TOrgDALFactory } from "../org/org-dal";
|
import { TOrgDALFactory } from "../org/org-dal";
|
||||||
import { TOrgServiceFactory } from "../org/org-service";
|
import { TOrgServiceFactory } from "../org/org-service";
|
||||||
import { TProjectMembershipDALFactory } from "../project-membership/project-membership-dal";
|
import { TProjectMembershipDALFactory } from "../project-membership/project-membership-dal";
|
||||||
@@ -373,13 +373,17 @@ export const authSignupServiceFactory = ({
|
|||||||
if (metadata) {
|
if (metadata) {
|
||||||
const metadataObj = jwt.verify(metadata, appCfg.AUTH_SECRET) as TTokenMetadata;
|
const metadataObj = jwt.verify(metadata, appCfg.AUTH_SECRET) as TTokenMetadata;
|
||||||
|
|
||||||
if (user.id !== metadataObj.userId) {
|
if (
|
||||||
|
metadataObj.payload.userId !== user.id ||
|
||||||
|
metadataObj.payload.orgId !== orgMembership.orgId ||
|
||||||
|
metadataObj.type !== TokenMetadataType.InviteToProjects
|
||||||
|
) {
|
||||||
throw new UnauthorizedError({
|
throw new UnauthorizedError({
|
||||||
message: "Malformed or invalid metadata token"
|
message: "Malformed or invalid metadata token"
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
for await (const projectId of metadataObj.projectIds) {
|
for await (const projectId of metadataObj.payload.projectIds) {
|
||||||
await addMembersToProject({
|
await addMembersToProject({
|
||||||
orgDAL,
|
orgDAL,
|
||||||
projectDAL,
|
projectDAL,
|
||||||
@@ -394,10 +398,13 @@ export const authSignupServiceFactory = ({
|
|||||||
emails: [user.email!],
|
emails: [user.email!],
|
||||||
usernames: [],
|
usernames: [],
|
||||||
projectId,
|
projectId,
|
||||||
projectMembershipRole: ProjectMembershipRole.Member,
|
projectMembershipRole: metadataObj.payload.projectRoleSlug,
|
||||||
sendEmails: false
|
sendEmails: false
|
||||||
},
|
},
|
||||||
tx
|
{
|
||||||
|
tx,
|
||||||
|
throwOnProjectNotFound: false
|
||||||
|
}
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,7 +4,14 @@ import crypto from "crypto";
|
|||||||
import jwt from "jsonwebtoken";
|
import jwt from "jsonwebtoken";
|
||||||
import { Knex } from "knex";
|
import { Knex } from "knex";
|
||||||
|
|
||||||
import { OrgMembershipRole, OrgMembershipStatus, ProjectMembershipRole, TableName, TUsers } from "@app/db/schemas";
|
import {
|
||||||
|
OrgMembershipRole,
|
||||||
|
OrgMembershipStatus,
|
||||||
|
ProjectMembershipRole,
|
||||||
|
ProjectVersion,
|
||||||
|
TableName,
|
||||||
|
TUsers
|
||||||
|
} from "@app/db/schemas";
|
||||||
import { TProjects } from "@app/db/schemas/projects";
|
import { TProjects } from "@app/db/schemas/projects";
|
||||||
import { TGroupDALFactory } from "@app/ee/services/group/group-dal";
|
import { TGroupDALFactory } from "@app/ee/services/group/group-dal";
|
||||||
import { TUserGroupMembershipDALFactory } from "@app/ee/services/group/user-group-membership-dal";
|
import { TUserGroupMembershipDALFactory } from "@app/ee/services/group/user-group-membership-dal";
|
||||||
@@ -25,8 +32,9 @@ import { TUserAliasDALFactory } from "@app/services/user-alias/user-alias-dal";
|
|||||||
|
|
||||||
import { ActorAuthMethod, ActorType, AuthMethod, AuthTokenType } from "../auth/auth-type";
|
import { ActorAuthMethod, ActorType, AuthMethod, AuthTokenType } from "../auth/auth-type";
|
||||||
import { TAuthTokenServiceFactory } from "../auth-token/auth-token-service";
|
import { TAuthTokenServiceFactory } from "../auth-token/auth-token-service";
|
||||||
import { TokenMetadataType, TokenType } from "../auth-token/auth-token-types";
|
import { TokenMetadataType, TokenType, TTokenMetadata } from "../auth-token/auth-token-types";
|
||||||
import { TProjectDALFactory } from "../project/project-dal";
|
import { TProjectDALFactory } from "../project/project-dal";
|
||||||
|
import { verifyProjectVersion } from "../project/project-fns";
|
||||||
import { TProjectBotDALFactory } from "../project-bot/project-bot-dal";
|
import { TProjectBotDALFactory } from "../project-bot/project-bot-dal";
|
||||||
import { TProjectKeyDALFactory } from "../project-key/project-key-dal";
|
import { TProjectKeyDALFactory } from "../project-key/project-key-dal";
|
||||||
import { TProjectMembershipDALFactory } from "../project-membership/project-membership-dal";
|
import { TProjectMembershipDALFactory } from "../project-membership/project-membership-dal";
|
||||||
@@ -435,6 +443,7 @@ export const orgServiceFactory = ({
|
|||||||
userId,
|
userId,
|
||||||
inviteeEmails,
|
inviteeEmails,
|
||||||
organizationRoleSlug,
|
organizationRoleSlug,
|
||||||
|
projectRoleSlug,
|
||||||
projectIds,
|
projectIds,
|
||||||
actorAuthMethod,
|
actorAuthMethod,
|
||||||
actorOrgId
|
actorOrgId
|
||||||
@@ -467,6 +476,22 @@ export const orgServiceFactory = ({
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (projectIds?.length) {
|
||||||
|
const projects = await projectDAL.find({
|
||||||
|
orgId,
|
||||||
|
$in: {
|
||||||
|
id: projectIds
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// if its not v3, throw an error
|
||||||
|
if (!verifyProjectVersion(projects, ProjectVersion.V3)) {
|
||||||
|
throw new BadRequestError({
|
||||||
|
message: "One or more selected projects are not compatible with this operation. Please upgrade your projects."
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const inviteeUsers = await orgDAL.transaction(async (tx) => {
|
const inviteeUsers = await orgDAL.transaction(async (tx) => {
|
||||||
const users: Pick<
|
const users: Pick<
|
||||||
TUsers & { orgId: string },
|
TUsers & { orgId: string },
|
||||||
@@ -474,6 +499,7 @@ export const orgServiceFactory = ({
|
|||||||
>[] = [];
|
>[] = [];
|
||||||
for await (const inviteeEmail of inviteeEmails) {
|
for await (const inviteeEmail of inviteeEmails) {
|
||||||
const inviteeUser = await userDAL.findUserByUsername(inviteeEmail, tx);
|
const inviteeUser = await userDAL.findUserByUsername(inviteeEmail, tx);
|
||||||
|
|
||||||
if (inviteeUser) {
|
if (inviteeUser) {
|
||||||
// if user already exist means its already part of infisical
|
// if user already exist means its already part of infisical
|
||||||
// Thus the signup flow is not needed anymore
|
// Thus the signup flow is not needed anymore
|
||||||
@@ -504,7 +530,22 @@ export const orgServiceFactory = ({
|
|||||||
tx
|
tx
|
||||||
);
|
);
|
||||||
|
|
||||||
if (projectIds) {
|
if (projectIds?.length) {
|
||||||
|
if (
|
||||||
|
organizationRoleSlug === OrgMembershipRole.Custom ||
|
||||||
|
projectRoleSlug === ProjectMembershipRole.Custom
|
||||||
|
) {
|
||||||
|
throw new BadRequestError({
|
||||||
|
message: "Custom roles are not supported for inviting users to projects and organizations"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!projectRoleSlug) {
|
||||||
|
throw new BadRequestError({
|
||||||
|
message: "Selecting a project role is required to invite users to projects"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
for await (const projectId of projectIds) {
|
for await (const projectId of projectIds) {
|
||||||
await projectMembershipDAL.create(
|
await projectMembershipDAL.create(
|
||||||
{
|
{
|
||||||
@@ -528,10 +569,12 @@ export const orgServiceFactory = ({
|
|||||||
emails: [inviteeEmail],
|
emails: [inviteeEmail],
|
||||||
usernames: [],
|
usernames: [],
|
||||||
projectId,
|
projectId,
|
||||||
projectMembershipRole: ProjectMembershipRole.Member,
|
projectMembershipRole: projectRoleSlug,
|
||||||
sendEmails: false
|
sendEmails: false
|
||||||
},
|
},
|
||||||
tx
|
{
|
||||||
|
tx
|
||||||
|
}
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -561,7 +604,7 @@ export const orgServiceFactory = ({
|
|||||||
inviteEmail: inviteeEmail,
|
inviteEmail: inviteeEmail,
|
||||||
orgId,
|
orgId,
|
||||||
userId: user.id,
|
userId: user.id,
|
||||||
role: organizationRoleSlug || OrgMembershipRole.Member,
|
role: organizationRoleSlug,
|
||||||
status: OrgMembershipStatus.Invited,
|
status: OrgMembershipStatus.Invited,
|
||||||
isActive: true
|
isActive: true
|
||||||
},
|
},
|
||||||
@@ -588,13 +631,17 @@ export const orgServiceFactory = ({
|
|||||||
});
|
});
|
||||||
|
|
||||||
let inviteMetadata: string = "";
|
let inviteMetadata: string = "";
|
||||||
if (projectIds && projectIds.length > 0) {
|
if (projectIds && projectIds?.length > 0) {
|
||||||
inviteMetadata = jwt.sign(
|
inviteMetadata = jwt.sign(
|
||||||
{
|
{
|
||||||
type: TokenMetadataType.InviteToProjects,
|
type: TokenMetadataType.InviteToProjects,
|
||||||
projectIds,
|
payload: {
|
||||||
userId: invitee.id
|
projectIds,
|
||||||
},
|
projectRoleSlug: projectRoleSlug!, // Implicitly checked inside transaction if projectRoleSlug is undefined
|
||||||
|
userId: invitee.id,
|
||||||
|
orgId
|
||||||
|
}
|
||||||
|
} satisfies TTokenMetadata,
|
||||||
appCfg.AUTH_SECRET,
|
appCfg.AUTH_SECRET,
|
||||||
{
|
{
|
||||||
expiresIn: appCfg.JWT_INVITE_LIFETIME
|
expiresIn: appCfg.JWT_INVITE_LIFETIME
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { OrgMembershipRole, ProjectMembershipRole } from "@app/db/schemas";
|
||||||
import { TOrgPermission } from "@app/lib/types";
|
import { TOrgPermission } from "@app/lib/types";
|
||||||
|
|
||||||
import { ActorAuthMethod, ActorType } from "../auth/auth-type";
|
import { ActorAuthMethod, ActorType } from "../auth/auth-type";
|
||||||
@@ -30,8 +31,9 @@ export type TInviteUserToOrgDTO = {
|
|||||||
actorOrgId: string | undefined;
|
actorOrgId: string | undefined;
|
||||||
actorAuthMethod: ActorAuthMethod;
|
actorAuthMethod: ActorAuthMethod;
|
||||||
inviteeEmails: string[];
|
inviteeEmails: string[];
|
||||||
organizationRoleSlug?: string;
|
organizationRoleSlug: OrgMembershipRole;
|
||||||
projectIds?: string[];
|
projectIds?: string[];
|
||||||
|
projectRoleSlug?: ProjectMembershipRole;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type TVerifyUserToOrgDTO = {
|
export type TVerifyUserToOrgDTO = {
|
||||||
|
|||||||
@@ -35,6 +35,11 @@ type AddMembersToNonE2EEProjectDTO = {
|
|||||||
sendEmails?: boolean;
|
sendEmails?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
type AddMembersToNonE2EEProjectOptions = {
|
||||||
|
tx?: Knex;
|
||||||
|
throwOnProjectNotFound?: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
export const addMembersToProject = ({
|
export const addMembersToProject = ({
|
||||||
orgDAL,
|
orgDAL,
|
||||||
projectDAL,
|
projectDAL,
|
||||||
@@ -45,15 +50,22 @@ export const addMembersToProject = ({
|
|||||||
projectUserMembershipRoleDAL,
|
projectUserMembershipRoleDAL,
|
||||||
smtpService
|
smtpService
|
||||||
}: TAddMembersToProjectArg) => {
|
}: TAddMembersToProjectArg) => {
|
||||||
|
// Can create multiple memberships for a singular project, based on user email / username
|
||||||
const addMembersToNonE2EEProject = async (
|
const addMembersToNonE2EEProject = async (
|
||||||
{ emails, usernames, projectId, projectMembershipRole, sendEmails }: AddMembersToNonE2EEProjectDTO,
|
{ emails, usernames, projectId, projectMembershipRole, sendEmails }: AddMembersToNonE2EEProjectDTO,
|
||||||
transaction?: Knex
|
options: AddMembersToNonE2EEProjectOptions = { throwOnProjectNotFound: true }
|
||||||
) => {
|
) => {
|
||||||
const processTransaction = async (tx: Knex) => {
|
const processTransaction = async (tx: Knex) => {
|
||||||
const usernamesAndEmails = [...emails, ...usernames];
|
const usernamesAndEmails = [...emails, ...usernames];
|
||||||
|
|
||||||
const project = await projectDAL.findProjectById(projectId);
|
const project = await projectDAL.findProjectById(projectId);
|
||||||
if (!project) throw new BadRequestError({ message: "Project not found" });
|
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(
|
const orgMembers = await orgDAL.findOrgMembersByUsername(
|
||||||
project.orgId,
|
project.orgId,
|
||||||
@@ -61,12 +73,6 @@ export const addMembersToProject = ({
|
|||||||
tx
|
tx
|
||||||
);
|
);
|
||||||
|
|
||||||
// const allorgMembers = await orgDAL.findAllOrgMembers(project.orgId,);
|
|
||||||
|
|
||||||
console.log("usernameandena", usernamesAndEmails);
|
|
||||||
console.log("orgMembers", orgMembers);
|
|
||||||
// console.log("allorgMembers", allorgMembers);
|
|
||||||
|
|
||||||
if (orgMembers.length !== usernamesAndEmails.length)
|
if (orgMembers.length !== usernamesAndEmails.length)
|
||||||
throw new BadRequestError({ message: "Some users are not part of org" });
|
throw new BadRequestError({ message: "Some users are not part of org" });
|
||||||
|
|
||||||
@@ -131,7 +137,7 @@ export const addMembersToProject = ({
|
|||||||
tx
|
tx
|
||||||
);
|
);
|
||||||
await projectUserMembershipRoleDAL.insertMany(
|
await projectUserMembershipRoleDAL.insertMany(
|
||||||
projectMemberships.map(({ id }) => ({ projectMembershipId: id, role: ProjectMembershipRole.Member })),
|
projectMemberships.map(({ id }) => ({ projectMembershipId: id, role: projectMembershipRole })),
|
||||||
tx
|
tx
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -172,8 +178,8 @@ export const addMembersToProject = ({
|
|||||||
return members;
|
return members;
|
||||||
};
|
};
|
||||||
|
|
||||||
if (transaction) {
|
if (options.tx) {
|
||||||
return processTransaction(transaction);
|
return processTransaction(options.tx);
|
||||||
}
|
}
|
||||||
return projectMembershipDAL.transaction(processTransaction);
|
return projectMembershipDAL.transaction(processTransaction);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -0,0 +1,52 @@
|
|||||||
|
import { ProjectMembershipRole } from "@app/db/schemas";
|
||||||
|
import {
|
||||||
|
projectAdminPermissions,
|
||||||
|
projectMemberPermissions,
|
||||||
|
projectNoAccessPermissions,
|
||||||
|
projectViewerPermission
|
||||||
|
} from "@app/ee/services/permission/project-permission";
|
||||||
|
|
||||||
|
export const getPredefinedRoles = (projectId: string, roleFilter?: ProjectMembershipRole) => {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
id: "b11b49a9-09a9-4443-916a-4246f9ff2c69", // dummy userid
|
||||||
|
projectId,
|
||||||
|
name: "Admin",
|
||||||
|
slug: ProjectMembershipRole.Admin,
|
||||||
|
permissions: projectAdminPermissions,
|
||||||
|
description: "Full administrative access over a project",
|
||||||
|
createdAt: new Date(),
|
||||||
|
updatedAt: new Date()
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "b11b49a9-09a9-4443-916a-4246f9ff2c70", // dummy user for zod validation in response
|
||||||
|
projectId,
|
||||||
|
name: "Developer",
|
||||||
|
slug: ProjectMembershipRole.Member,
|
||||||
|
permissions: projectMemberPermissions,
|
||||||
|
description: "Limited read/write role in a project",
|
||||||
|
createdAt: new Date(),
|
||||||
|
updatedAt: new Date()
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "b11b49a9-09a9-4443-916a-4246f9ff2c71", // dummy user for zod validation in response
|
||||||
|
projectId,
|
||||||
|
name: "Viewer",
|
||||||
|
slug: ProjectMembershipRole.Viewer,
|
||||||
|
permissions: projectViewerPermission,
|
||||||
|
description: "Only read role in a project",
|
||||||
|
createdAt: new Date(),
|
||||||
|
updatedAt: new Date()
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "b11b49a9-09a9-4443-916a-4246f9ff2c72", // dummy user for zod validation in response
|
||||||
|
projectId,
|
||||||
|
name: "No Access",
|
||||||
|
slug: ProjectMembershipRole.NoAccess,
|
||||||
|
permissions: projectNoAccessPermissions,
|
||||||
|
description: "No access to any resources in the project",
|
||||||
|
createdAt: new Date(),
|
||||||
|
updatedAt: new Date()
|
||||||
|
}
|
||||||
|
].filter(({ slug }) => !roleFilter || roleFilter.includes(slug));
|
||||||
|
};
|
||||||
@@ -5,13 +5,9 @@ import { ProjectMembershipRole } from "@app/db/schemas";
|
|||||||
import { UnpackedPermissionSchema } from "@app/ee/services/identity-project-additional-privilege/identity-project-additional-privilege-service";
|
import { UnpackedPermissionSchema } from "@app/ee/services/identity-project-additional-privilege/identity-project-additional-privilege-service";
|
||||||
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
|
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
|
||||||
import {
|
import {
|
||||||
projectAdminPermissions,
|
|
||||||
projectMemberPermissions,
|
|
||||||
projectNoAccessPermissions,
|
|
||||||
ProjectPermissionActions,
|
ProjectPermissionActions,
|
||||||
ProjectPermissionSet,
|
ProjectPermissionSet,
|
||||||
ProjectPermissionSub,
|
ProjectPermissionSub
|
||||||
projectViewerPermission
|
|
||||||
} from "@app/ee/services/permission/project-permission";
|
} from "@app/ee/services/permission/project-permission";
|
||||||
import { BadRequestError } from "@app/lib/errors";
|
import { BadRequestError } from "@app/lib/errors";
|
||||||
|
|
||||||
@@ -20,6 +16,7 @@ import { TIdentityProjectMembershipRoleDALFactory } from "../identity-project/id
|
|||||||
import { TProjectDALFactory } from "../project/project-dal";
|
import { TProjectDALFactory } from "../project/project-dal";
|
||||||
import { TProjectUserMembershipRoleDALFactory } from "../project-membership/project-user-membership-role-dal";
|
import { TProjectUserMembershipRoleDALFactory } from "../project-membership/project-user-membership-role-dal";
|
||||||
import { TProjectRoleDALFactory } from "./project-role-dal";
|
import { TProjectRoleDALFactory } from "./project-role-dal";
|
||||||
|
import { getPredefinedRoles } from "./project-role-fns";
|
||||||
import { TCreateRoleDTO, TDeleteRoleDTO, TGetRoleBySlugDTO, TListRolesDTO, TUpdateRoleDTO } from "./project-role-types";
|
import { TCreateRoleDTO, TDeleteRoleDTO, TGetRoleBySlugDTO, TListRolesDTO, TUpdateRoleDTO } from "./project-role-types";
|
||||||
|
|
||||||
type TProjectRoleServiceFactoryDep = {
|
type TProjectRoleServiceFactoryDep = {
|
||||||
@@ -37,51 +34,6 @@ const unpackPermissions = (permissions: unknown) =>
|
|||||||
unpackRules((permissions || []) as PackRule<RawRuleOf<MongoAbility<ProjectPermissionSet>>>[])
|
unpackRules((permissions || []) as PackRule<RawRuleOf<MongoAbility<ProjectPermissionSet>>>[])
|
||||||
);
|
);
|
||||||
|
|
||||||
const getPredefinedRoles = (projectId: string, roleFilter?: ProjectMembershipRole) => {
|
|
||||||
return [
|
|
||||||
{
|
|
||||||
id: "b11b49a9-09a9-4443-916a-4246f9ff2c69", // dummy userid
|
|
||||||
projectId,
|
|
||||||
name: "Admin",
|
|
||||||
slug: ProjectMembershipRole.Admin,
|
|
||||||
permissions: projectAdminPermissions,
|
|
||||||
description: "Full administrative access over a project",
|
|
||||||
createdAt: new Date(),
|
|
||||||
updatedAt: new Date()
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: "b11b49a9-09a9-4443-916a-4246f9ff2c70", // dummy user for zod validation in response
|
|
||||||
projectId,
|
|
||||||
name: "Developer",
|
|
||||||
slug: ProjectMembershipRole.Member,
|
|
||||||
permissions: projectMemberPermissions,
|
|
||||||
description: "Limited read/write role in a project",
|
|
||||||
createdAt: new Date(),
|
|
||||||
updatedAt: new Date()
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: "b11b49a9-09a9-4443-916a-4246f9ff2c71", // dummy user for zod validation in response
|
|
||||||
projectId,
|
|
||||||
name: "Viewer",
|
|
||||||
slug: ProjectMembershipRole.Viewer,
|
|
||||||
permissions: projectViewerPermission,
|
|
||||||
description: "Only read role in a project",
|
|
||||||
createdAt: new Date(),
|
|
||||||
updatedAt: new Date()
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: "b11b49a9-09a9-4443-916a-4246f9ff2c72", // dummy user for zod validation in response
|
|
||||||
projectId,
|
|
||||||
name: "No Access",
|
|
||||||
slug: ProjectMembershipRole.NoAccess,
|
|
||||||
permissions: projectNoAccessPermissions,
|
|
||||||
description: "No access to any resources in the project",
|
|
||||||
createdAt: new Date(),
|
|
||||||
updatedAt: new Date()
|
|
||||||
}
|
|
||||||
].filter(({ slug }) => !roleFilter || roleFilter.includes(slug));
|
|
||||||
};
|
|
||||||
|
|
||||||
export const projectRoleServiceFactory = ({
|
export const projectRoleServiceFactory = ({
|
||||||
projectRoleDAL,
|
projectRoleDAL,
|
||||||
permissionService,
|
permissionService,
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import crypto from "crypto";
|
import crypto from "crypto";
|
||||||
|
|
||||||
|
import { ProjectVersion, TProjects } from "@app/db/schemas";
|
||||||
import { decryptAsymmetric, encryptAsymmetric } from "@app/lib/crypto";
|
import { decryptAsymmetric, encryptAsymmetric } from "@app/lib/crypto";
|
||||||
import { BadRequestError } from "@app/lib/errors";
|
import { BadRequestError } from "@app/lib/errors";
|
||||||
import { TKmsServiceFactory } from "@app/services/kms/kms-service";
|
import { TKmsServiceFactory } from "@app/services/kms/kms-service";
|
||||||
@@ -53,6 +54,16 @@ export const createProjectKey = ({ publicKey, privateKey, plainProjectKey }: TCr
|
|||||||
return { key: encryptedProjectKey, iv: encryptedProjectKeyIv };
|
return { key: encryptedProjectKey, iv: encryptedProjectKeyIv };
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const verifyProjectVersion = (projects: Pick<TProjects, "version">[], version: ProjectVersion) => {
|
||||||
|
for (const project of projects) {
|
||||||
|
if (project.version !== version) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
};
|
||||||
|
|
||||||
export const getProjectKmsCertificateKeyId = async ({
|
export const getProjectKmsCertificateKeyId = async ({
|
||||||
projectId,
|
projectId,
|
||||||
projectDAL,
|
projectDAL,
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import { TKeyStoreFactory } from "@app/keystore/keystore";
|
|||||||
import { isAtLeastAsPrivileged } from "@app/lib/casl";
|
import { isAtLeastAsPrivileged } from "@app/lib/casl";
|
||||||
import { infisicalSymmetricEncypt } from "@app/lib/crypto/encryption";
|
import { infisicalSymmetricEncypt } from "@app/lib/crypto/encryption";
|
||||||
import { BadRequestError, ForbiddenRequestError, NotFoundError } from "@app/lib/errors";
|
import { BadRequestError, ForbiddenRequestError, NotFoundError } from "@app/lib/errors";
|
||||||
|
import { groupBy } from "@app/lib/fn";
|
||||||
import { alphaNumericNanoId } from "@app/lib/nanoid";
|
import { alphaNumericNanoId } from "@app/lib/nanoid";
|
||||||
import { TProjectPermission } from "@app/lib/types";
|
import { TProjectPermission } from "@app/lib/types";
|
||||||
|
|
||||||
@@ -30,6 +31,8 @@ import { TProjectEnvDALFactory } from "../project-env/project-env-dal";
|
|||||||
import { TProjectKeyDALFactory } from "../project-key/project-key-dal";
|
import { TProjectKeyDALFactory } from "../project-key/project-key-dal";
|
||||||
import { TProjectMembershipDALFactory } from "../project-membership/project-membership-dal";
|
import { TProjectMembershipDALFactory } from "../project-membership/project-membership-dal";
|
||||||
import { TProjectUserMembershipRoleDALFactory } from "../project-membership/project-user-membership-role-dal";
|
import { TProjectUserMembershipRoleDALFactory } from "../project-membership/project-user-membership-role-dal";
|
||||||
|
import { TProjectRoleDALFactory } from "../project-role/project-role-dal";
|
||||||
|
import { getPredefinedRoles } from "../project-role/project-role-fns";
|
||||||
import { ROOT_FOLDER_NAME, TSecretFolderDALFactory } from "../secret-folder/secret-folder-dal";
|
import { ROOT_FOLDER_NAME, TSecretFolderDALFactory } from "../secret-folder/secret-folder-dal";
|
||||||
import { TUserDALFactory } from "../user/user-dal";
|
import { TUserDALFactory } from "../user/user-dal";
|
||||||
import { TProjectDALFactory } from "./project-dal";
|
import { TProjectDALFactory } from "./project-dal";
|
||||||
@@ -44,6 +47,7 @@ import {
|
|||||||
TListProjectCasDTO,
|
TListProjectCasDTO,
|
||||||
TListProjectCertificateTemplatesDTO,
|
TListProjectCertificateTemplatesDTO,
|
||||||
TListProjectCertsDTO,
|
TListProjectCertsDTO,
|
||||||
|
TListProjectsDTO,
|
||||||
TLoadProjectKmsBackupDTO,
|
TLoadProjectKmsBackupDTO,
|
||||||
TToggleProjectAutoCapitalizationDTO,
|
TToggleProjectAutoCapitalizationDTO,
|
||||||
TUpdateAuditLogsRetentionDTO,
|
TUpdateAuditLogsRetentionDTO,
|
||||||
@@ -84,6 +88,7 @@ type TProjectServiceFactoryDep = {
|
|||||||
orgDAL: Pick<TOrgDALFactory, "findOne">;
|
orgDAL: Pick<TOrgDALFactory, "findOne">;
|
||||||
keyStore: Pick<TKeyStoreFactory, "deleteItem">;
|
keyStore: Pick<TKeyStoreFactory, "deleteItem">;
|
||||||
projectBotDAL: Pick<TProjectBotDALFactory, "create">;
|
projectBotDAL: Pick<TProjectBotDALFactory, "create">;
|
||||||
|
projectRoleDAL: Pick<TProjectRoleDALFactory, "find">;
|
||||||
kmsService: Pick<
|
kmsService: Pick<
|
||||||
TKmsServiceFactory,
|
TKmsServiceFactory,
|
||||||
| "updateProjectSecretManagerKmsKey"
|
| "updateProjectSecretManagerKmsKey"
|
||||||
@@ -112,6 +117,7 @@ export const projectServiceFactory = ({
|
|||||||
projectEnvDAL,
|
projectEnvDAL,
|
||||||
licenseService,
|
licenseService,
|
||||||
projectUserMembershipRoleDAL,
|
projectUserMembershipRoleDAL,
|
||||||
|
projectRoleDAL,
|
||||||
identityProjectMembershipRoleDAL,
|
identityProjectMembershipRoleDAL,
|
||||||
certificateAuthorityDAL,
|
certificateAuthorityDAL,
|
||||||
certificateDAL,
|
certificateDAL,
|
||||||
@@ -389,8 +395,37 @@ export const projectServiceFactory = ({
|
|||||||
return deletedProject;
|
return deletedProject;
|
||||||
};
|
};
|
||||||
|
|
||||||
const getProjects = async (actorId: string) => {
|
const getProjects = async ({ actorId, includeRoles, actor, actorAuthMethod, actorOrgId }: TListProjectsDTO) => {
|
||||||
const workspaces = await projectDAL.findAllProjects(actorId);
|
const workspaces = await projectDAL.findAllProjects(actorId);
|
||||||
|
|
||||||
|
if (includeRoles) {
|
||||||
|
const customRoles = await projectRoleDAL.find({
|
||||||
|
$in: {
|
||||||
|
projectId: workspaces.map((workspace) => workspace.id)
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const workspaceMappedToRoles = groupBy(customRoles, (role) => role.projectId);
|
||||||
|
|
||||||
|
const workspacesWithRoles = await Promise.all(
|
||||||
|
workspaces.map(async (workspace) => {
|
||||||
|
const { permission } = await permissionService.getProjectPermission(
|
||||||
|
actor,
|
||||||
|
actorId,
|
||||||
|
workspace.id,
|
||||||
|
actorAuthMethod,
|
||||||
|
actorOrgId
|
||||||
|
);
|
||||||
|
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.Role);
|
||||||
|
return {
|
||||||
|
...workspace,
|
||||||
|
roles: [...(workspaceMappedToRoles[workspace.id] || []), ...getPredefinedRoles(workspace.id)]
|
||||||
|
};
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
return workspacesWithRoles;
|
||||||
|
}
|
||||||
return workspaces;
|
return workspaces;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -75,6 +75,10 @@ export type TDeleteProjectDTO = {
|
|||||||
actorOrgId: string | undefined;
|
actorOrgId: string | undefined;
|
||||||
} & Omit<TProjectPermission, "projectId">;
|
} & Omit<TProjectPermission, "projectId">;
|
||||||
|
|
||||||
|
export type TListProjectsDTO = {
|
||||||
|
includeRoles: boolean;
|
||||||
|
} & Omit<TProjectPermission, "projectId">;
|
||||||
|
|
||||||
export type TUpgradeProjectDTO = {
|
export type TUpgradeProjectDTO = {
|
||||||
userPrivateKey: string;
|
userPrivateKey: string;
|
||||||
} & TProjectPermission;
|
} & TProjectPermission;
|
||||||
|
|||||||
@@ -32,7 +32,8 @@ export default function TeamInviteStep(): JSX.Element {
|
|||||||
.map(async (email) => {
|
.map(async (email) => {
|
||||||
mutateAsync({
|
mutateAsync({
|
||||||
inviteeEmails: [email],
|
inviteeEmails: [email],
|
||||||
organizationId: String(localStorage.getItem("orgData.id"))
|
organizationId: String(localStorage.getItem("orgData.id")),
|
||||||
|
organizationRoleSlug: "member"
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -47,7 +47,7 @@ export const roleQueryKeys = {
|
|||||||
["user-project-permissions", { workspaceId }] as const
|
["user-project-permissions", { workspaceId }] as const
|
||||||
};
|
};
|
||||||
|
|
||||||
const getProjectRoles = async (projectId: string) => {
|
export const getProjectRoles = async (projectId: string) => {
|
||||||
const { data } = await apiRequest.get<{ roles: Array<Omit<TProjectRole, "permissions">> }>(
|
const { data } = await apiRequest.get<{ roles: Array<Omit<TProjectRole, "permissions">> }>(
|
||||||
`/api/v1/workspace/${projectId}/roles`
|
`/api/v1/workspace/${projectId}/roles`
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -162,7 +162,7 @@ export const useAddUsersToOrg = () => {
|
|||||||
type Response = {
|
type Response = {
|
||||||
data: {
|
data: {
|
||||||
message: string;
|
message: string;
|
||||||
completeInviteLinks: {
|
completeInviteLinks?: {
|
||||||
email: string;
|
email: string;
|
||||||
link: string;
|
link: string;
|
||||||
}[];
|
}[];
|
||||||
|
|||||||
@@ -151,7 +151,8 @@ export type DeletOrgMembershipDTO = {
|
|||||||
export type AddUserToOrgDTO = {
|
export type AddUserToOrgDTO = {
|
||||||
inviteeEmails: string[];
|
inviteeEmails: string[];
|
||||||
projectIds?: string[];
|
projectIds?: string[];
|
||||||
organizationRoleSlug?: string;
|
projectRoleSlug?: string;
|
||||||
|
organizationRoleSlug: string;
|
||||||
organizationId: string;
|
organizationId: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -138,8 +138,12 @@ export const useGetUpgradeProjectStatus = ({
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const fetchUserWorkspaces = async () => {
|
const fetchUserWorkspaces = async (includeRoles?: boolean) => {
|
||||||
const { data } = await apiRequest.get<{ workspaces: Workspace[] }>("/api/v1/workspace");
|
const { data } = await apiRequest.get<{ workspaces: Workspace[] }>("/api/v1/workspace", {
|
||||||
|
params: {
|
||||||
|
includeRoles
|
||||||
|
}
|
||||||
|
});
|
||||||
return data.workspaces;
|
return data.workspaces;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -171,8 +175,8 @@ export const useGetWorkspaceById = (
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
export const useGetUserWorkspaces = () =>
|
export const useGetUserWorkspaces = (includeRoles?: boolean) =>
|
||||||
useQuery(workspaceKeys.getAllUserWorkspace, fetchUserWorkspaces);
|
useQuery(workspaceKeys.getAllUserWorkspace, () => fetchUserWorkspaces(includeRoles));
|
||||||
|
|
||||||
const fetchUserWorkspaceMemberships = async (orgId: string) => {
|
const fetchUserWorkspaceMemberships = async (orgId: string) => {
|
||||||
const { data } = await apiRequest.get<Record<string, Workspace[]>>(
|
const { data } = await apiRequest.get<Record<string, Workspace[]>>(
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
import { TProjectRole } from "../roles/types";
|
||||||
|
|
||||||
export enum ProjectVersion {
|
export enum ProjectVersion {
|
||||||
V1 = 1,
|
V1 = 1,
|
||||||
V2 = 2,
|
V2 = 2,
|
||||||
@@ -22,6 +24,8 @@ export type Workspace = {
|
|||||||
auditLogsRetentionDays: number;
|
auditLogsRetentionDays: number;
|
||||||
slug: string;
|
slug: string;
|
||||||
createdAt: string;
|
createdAt: string;
|
||||||
|
|
||||||
|
roles?: TProjectRole[];
|
||||||
};
|
};
|
||||||
|
|
||||||
export type WorkspaceEnv = {
|
export type WorkspaceEnv = {
|
||||||
|
|||||||
+130
-81
@@ -1,3 +1,4 @@
|
|||||||
|
import { useMemo } from "react";
|
||||||
import { Controller, useForm } from "react-hook-form";
|
import { Controller, useForm } from "react-hook-form";
|
||||||
import { faCheckCircle, faChevronDown } from "@fortawesome/free-solid-svg-icons";
|
import { faCheckCircle, faChevronDown } from "@fortawesome/free-solid-svg-icons";
|
||||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||||
@@ -25,18 +26,21 @@ import {
|
|||||||
useGetOrgRoles,
|
useGetOrgRoles,
|
||||||
useGetUserWorkspaces
|
useGetUserWorkspaces
|
||||||
} 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 { UsePopUpState } from "@app/hooks/usePopUp";
|
import { UsePopUpState } from "@app/hooks/usePopUp";
|
||||||
|
|
||||||
import { OrgInviteLink } from "./OrgInviteLink";
|
import { OrgInviteLink } from "./OrgInviteLink";
|
||||||
|
|
||||||
const DEFAULT_ORG_MEMBER_ROLE_SLUG = "member";
|
const DEFAULT_ORG_AND_PROJECT_MEMBER_ROLE_SLUG = "member";
|
||||||
|
|
||||||
const EmailSchema = z.string().email().min(1).trim().toLowerCase();
|
const EmailSchema = z.string().email().min(1).trim().toLowerCase();
|
||||||
|
|
||||||
const addMemberFormSchema = z.object({
|
const addMemberFormSchema = z.object({
|
||||||
emails: z.string().min(1).trim().toLowerCase(),
|
emails: z.string().min(1).trim().toLowerCase(),
|
||||||
projectIds: z.array(z.string().min(1).trim().toLowerCase()).default([]),
|
projectIds: z.array(z.string().min(1).trim().toLowerCase()).default([]),
|
||||||
organizationRoleSlug: z.string().min(1).default(DEFAULT_ORG_MEMBER_ROLE_SLUG)
|
projectRoleSlug: z.string().min(1).default(DEFAULT_ORG_AND_PROJECT_MEMBER_ROLE_SLUG),
|
||||||
|
organizationRoleSlug: z.string().min(1).default(DEFAULT_ORG_AND_PROJECT_MEMBER_ROLE_SLUG)
|
||||||
});
|
});
|
||||||
|
|
||||||
type TAddMemberForm = z.infer<typeof addMemberFormSchema>;
|
type TAddMemberForm = z.infer<typeof addMemberFormSchema>;
|
||||||
@@ -71,7 +75,12 @@ export const AddOrgMemberModal = ({
|
|||||||
formState: { isSubmitting }
|
formState: { isSubmitting }
|
||||||
} = useForm<TAddMemberForm>({ resolver: zodResolver(addMemberFormSchema) });
|
} = useForm<TAddMemberForm>({ resolver: zodResolver(addMemberFormSchema) });
|
||||||
|
|
||||||
const onAddMembers = async ({ emails, organizationRoleSlug, projectIds }: TAddMemberForm) => {
|
const onAddMembers = async ({
|
||||||
|
emails,
|
||||||
|
organizationRoleSlug,
|
||||||
|
projectIds,
|
||||||
|
projectRoleSlug
|
||||||
|
}: TAddMemberForm) => {
|
||||||
if (!currentOrg?.id) return;
|
if (!currentOrg?.id) return;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@@ -98,7 +107,8 @@ export const AddOrgMemberModal = ({
|
|||||||
organizationId: currentOrg?.id,
|
organizationId: currentOrg?.id,
|
||||||
inviteeEmails: emails.split(",").map((email) => email.trim()),
|
inviteeEmails: emails.split(",").map((email) => email.trim()),
|
||||||
organizationRoleSlug,
|
organizationRoleSlug,
|
||||||
projectIds
|
projectIds,
|
||||||
|
projectRoleSlug
|
||||||
});
|
});
|
||||||
|
|
||||||
setCompleteInviteLinks(data?.completeInviteLinks ?? null);
|
setCompleteInviteLinks(data?.completeInviteLinks ?? null);
|
||||||
@@ -128,7 +138,12 @@ export const AddOrgMemberModal = ({
|
|||||||
};
|
};
|
||||||
|
|
||||||
const projectIds = watch("projectIds", []);
|
const projectIds = watch("projectIds", []);
|
||||||
const { data: projects } = useGetUserWorkspaces();
|
const { data: projects } = useGetUserWorkspaces(true);
|
||||||
|
|
||||||
|
const filteredProjects = useMemo(
|
||||||
|
() => projects?.filter((project) => project.version === ProjectVersion.V3),
|
||||||
|
[projects]
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Modal
|
<Modal
|
||||||
@@ -179,7 +194,7 @@ export const AddOrgMemberModal = ({
|
|||||||
>
|
>
|
||||||
<div>
|
<div>
|
||||||
<Select
|
<Select
|
||||||
defaultValue={DEFAULT_ORG_MEMBER_ROLE_SLUG}
|
defaultValue={DEFAULT_ORG_AND_PROJECT_MEMBER_ROLE_SLUG}
|
||||||
{...field}
|
{...field}
|
||||||
onValueChange={(val) => field.onChange(val)}
|
onValueChange={(val) => field.onChange(val)}
|
||||||
>
|
>
|
||||||
@@ -195,83 +210,117 @@ export const AddOrgMemberModal = ({
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="-space-y-3">
|
<div className="flex items-center justify-between gap-2">
|
||||||
<Controller
|
<div className="w-full">
|
||||||
control={control}
|
<Controller
|
||||||
name="projectIds"
|
control={control}
|
||||||
render={({ field, fieldState: { error } }) => (
|
name="projectIds"
|
||||||
<FormControl
|
render={({ field, fieldState: { error } }) => (
|
||||||
label="Add user to projects"
|
<FormControl
|
||||||
isError={Boolean(error?.message)}
|
label="Add user to projects"
|
||||||
errorText={error?.message}
|
isError={Boolean(error?.message)}
|
||||||
>
|
errorText={error?.message}
|
||||||
<DropdownMenu>
|
>
|
||||||
<DropdownMenuTrigger asChild>
|
<DropdownMenu>
|
||||||
{projects && projects.length > 0 ? (
|
<DropdownMenuTrigger asChild>
|
||||||
<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">
|
{filteredProjects && filteredProjects.length > 0 ? (
|
||||||
{projectIds.length === 1
|
<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">
|
||||||
? projects.find((project) => project.id === projectIds[0])?.name
|
{projectIds.length === 1
|
||||||
: `${projectIds.length} projects selected`}
|
? filteredProjects.find((project) => project.id === projectIds[0])
|
||||||
<FontAwesomeIcon icon={faChevronDown} className="text-xs" />
|
?.name
|
||||||
</div>
|
: `${projectIds.length} projects selected`}
|
||||||
) : (
|
<FontAwesomeIcon icon={faChevronDown} className="text-xs" />
|
||||||
<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">
|
</div>
|
||||||
No projects found
|
) : (
|
||||||
</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 projects found
|
||||||
</DropdownMenuTrigger>
|
</div>
|
||||||
<DropdownMenuContent
|
)}
|
||||||
align="start"
|
</DropdownMenuTrigger>
|
||||||
className="thin-scrollbar z-[100] max-h-80"
|
<DropdownMenuContent
|
||||||
>
|
align="start"
|
||||||
{projects && projects.length > 0 ? (
|
className="thin-scrollbar z-[100] max-h-80"
|
||||||
projects.map((project) => {
|
>
|
||||||
const isSelected = projectIds.includes(String(project.id));
|
{filteredProjects && filteredProjects.length > 0 ? (
|
||||||
|
filteredProjects.map((project) => {
|
||||||
|
const isSelected = projectIds.includes(String(project.id));
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<DropdownMenuItem
|
<DropdownMenuItem
|
||||||
onSelect={(event) => projects.length > 0 && event.preventDefault()}
|
onSelect={(event) =>
|
||||||
onClick={() => {
|
filteredProjects.length > 0 && event.preventDefault()
|
||||||
if (projectIds.includes(String(project.id))) {
|
|
||||||
field.onChange(
|
|
||||||
projectIds.filter(
|
|
||||||
(projectId: string) => projectId !== String(project.id)
|
|
||||||
)
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
field.onChange([...projectIds, String(project.id)]);
|
|
||||||
}
|
}
|
||||||
}}
|
onClick={() => {
|
||||||
key={`project-id-${project.id}`}
|
if (projectIds.includes(String(project.id))) {
|
||||||
icon={
|
field.onChange(
|
||||||
isSelected ? (
|
projectIds.filter(
|
||||||
<FontAwesomeIcon
|
(projectId: string) => projectId !== String(project.id)
|
||||||
icon={faCheckCircle}
|
)
|
||||||
className="pr-0.5 text-primary"
|
);
|
||||||
/>
|
} else {
|
||||||
) : (
|
field.onChange([...projectIds, String(project.id)]);
|
||||||
<div className="pl-[1.01rem]" />
|
}
|
||||||
)
|
}}
|
||||||
}
|
key={`project-id-${project.id}`}
|
||||||
iconPos="left"
|
icon={
|
||||||
className="w-[28.4rem] text-sm"
|
isSelected ? (
|
||||||
>
|
<FontAwesomeIcon
|
||||||
{project.name}
|
icon={faCheckCircle}
|
||||||
</DropdownMenuItem>
|
className="pr-0.5 text-primary"
|
||||||
);
|
/>
|
||||||
})
|
) : (
|
||||||
) : (
|
<div className="pl-[1.01rem]" />
|
||||||
<div />
|
)
|
||||||
)}
|
}
|
||||||
</DropdownMenuContent>
|
iconPos="left"
|
||||||
</DropdownMenu>
|
className="w-[28.4rem] text-sm"
|
||||||
</FormControl>
|
>
|
||||||
)}
|
{project.name}
|
||||||
/>
|
</DropdownMenuItem>
|
||||||
<p className="ml-0.5 text-xs text-mineshaft-400">
|
);
|
||||||
Members will automatically be assigned the <a className="font-bold">Developer</a>{" "}
|
})
|
||||||
role for the projects you select.
|
) : (
|
||||||
</p>
|
<div />
|
||||||
|
)}
|
||||||
|
</DropdownMenuContent>
|
||||||
|
</DropdownMenu>
|
||||||
|
</FormControl>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="flex min-w-fit justify-end">
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
|
name="projectRoleSlug"
|
||||||
|
render={({ field, fieldState: { error } }) => (
|
||||||
|
<FormControl
|
||||||
|
tooltipText="Select which role to assign to the users in the selected projects."
|
||||||
|
label="Role"
|
||||||
|
isError={Boolean(error)}
|
||||||
|
errorText={error?.message}
|
||||||
|
>
|
||||||
|
<div>
|
||||||
|
<Select
|
||||||
|
isDisabled={projectIds.length === 0}
|
||||||
|
defaultValue={DEFAULT_ORG_AND_PROJECT_MEMBER_ROLE_SLUG}
|
||||||
|
{...field}
|
||||||
|
onValueChange={(val) => field.onChange(val)}
|
||||||
|
>
|
||||||
|
{Object.entries(ProjectMembershipRole).map(
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||||
|
([_, slug]) =>
|
||||||
|
slug !== "custom" && (
|
||||||
|
<SelectItem key={slug} value={slug}>
|
||||||
|
<span className="capitalize">{slug.replace("-", " ")}</span>
|
||||||
|
</SelectItem>
|
||||||
|
)
|
||||||
|
)}
|
||||||
|
</Select>
|
||||||
|
</div>
|
||||||
|
</FormControl>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="mt-8 flex items-center">
|
<div className="mt-8 flex items-center">
|
||||||
|
|||||||
+2
-1
@@ -108,7 +108,8 @@ export const OrgMembersTable = ({ handlePopUpOpen, setCompleteInviteLinks }: Pro
|
|||||||
try {
|
try {
|
||||||
const { data } = await addUsersMutateAsync({
|
const { data } = await addUsersMutateAsync({
|
||||||
organizationId: orgId,
|
organizationId: orgId,
|
||||||
inviteeEmails: [email]
|
inviteeEmails: [email],
|
||||||
|
organizationRoleSlug: "member"
|
||||||
});
|
});
|
||||||
|
|
||||||
setCompleteInviteLinks(data?.completeInviteLinks || null);
|
setCompleteInviteLinks(data?.completeInviteLinks || null);
|
||||||
|
|||||||
@@ -50,7 +50,8 @@ export const UserDetailsSection = ({ membershipId, handlePopUpOpen }: Props) =>
|
|||||||
try {
|
try {
|
||||||
const { data } = await inviteUsers({
|
const { data } = await inviteUsers({
|
||||||
organizationId: orgId,
|
organizationId: orgId,
|
||||||
inviteeEmails: [email]
|
inviteeEmails: [email],
|
||||||
|
organizationRoleSlug: "member"
|
||||||
});
|
});
|
||||||
|
|
||||||
// setCompleteInviteLink(data?.completeInviteLink || "");
|
// setCompleteInviteLink(data?.completeInviteLink || "");
|
||||||
|
|||||||
Reference in New Issue
Block a user