mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-10-06 10:28:22 +00:00
Merge pull request #2384 from akhilmhdh/feat/org-project-invite
Manager users without waiting for confirmation of mail
This commit is contained in:
@@ -41,10 +41,9 @@ export const userGroupMembershipDALFactory = (db: TDbClient) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
// special query
|
// special query
|
||||||
const findUserGroupMembershipsInProject = async (usernames: string[], projectId: string) => {
|
const findUserGroupMembershipsInProject = async (usernames: string[], projectId: string, tx?: Knex) => {
|
||||||
try {
|
try {
|
||||||
const usernameDocs: string[] = await db
|
const usernameDocs: string[] = await (tx || db.replicaNode())(TableName.UserGroupMembership)
|
||||||
.replicaNode()(TableName.UserGroupMembership)
|
|
||||||
.join(
|
.join(
|
||||||
TableName.GroupProjectMembership,
|
TableName.GroupProjectMembership,
|
||||||
`${TableName.UserGroupMembership}.groupId`,
|
`${TableName.UserGroupMembership}.groupId`,
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import nacl from "tweetnacl";
|
|||||||
import tweetnacl from "tweetnacl-util";
|
import tweetnacl from "tweetnacl-util";
|
||||||
|
|
||||||
import { TUserEncryptionKeys } from "@app/db/schemas";
|
import { TUserEncryptionKeys } from "@app/db/schemas";
|
||||||
|
import { UserEncryption } from "@app/services/user/user-types";
|
||||||
|
|
||||||
import { decryptSymmetric128BitHexKeyUTF8, encryptAsymmetric, encryptSymmetric } from "./encryption";
|
import { decryptSymmetric128BitHexKeyUTF8, encryptAsymmetric, encryptSymmetric } from "./encryption";
|
||||||
|
|
||||||
@@ -36,12 +37,16 @@ export const srpCheckClientProof = async (
|
|||||||
// Ghost user related:
|
// Ghost user related:
|
||||||
// This functionality is intended for ghost user logic. This happens on the frontend when a user is being created.
|
// This functionality is intended for ghost user logic. This happens on the frontend when a user is being created.
|
||||||
// We replicate the same functionality on the backend when creating a ghost user.
|
// We replicate the same functionality on the backend when creating a ghost user.
|
||||||
export const generateUserSrpKeys = async (email: string, password: string) => {
|
export const generateUserSrpKeys = async (
|
||||||
|
email: string,
|
||||||
|
password: string,
|
||||||
|
customKeys?: { publicKey: string; privateKey: string }
|
||||||
|
) => {
|
||||||
const pair = nacl.box.keyPair();
|
const pair = nacl.box.keyPair();
|
||||||
const secretKeyUint8Array = pair.secretKey;
|
const secretKeyUint8Array = pair.secretKey;
|
||||||
const publicKeyUint8Array = pair.publicKey;
|
const publicKeyUint8Array = pair.publicKey;
|
||||||
const privateKey = tweetnacl.encodeBase64(secretKeyUint8Array);
|
const privateKey = customKeys?.privateKey || tweetnacl.encodeBase64(secretKeyUint8Array);
|
||||||
const publicKey = tweetnacl.encodeBase64(publicKeyUint8Array);
|
const publicKey = customKeys?.publicKey || tweetnacl.encodeBase64(publicKeyUint8Array);
|
||||||
|
|
||||||
// eslint-disable-next-line
|
// eslint-disable-next-line
|
||||||
const client = new jsrp.client();
|
const client = new jsrp.client();
|
||||||
@@ -111,7 +116,7 @@ export const getUserPrivateKey = async (
|
|||||||
| "encryptionVersion"
|
| "encryptionVersion"
|
||||||
>
|
>
|
||||||
) => {
|
) => {
|
||||||
if (user.encryptionVersion === 1) {
|
if (user.encryptionVersion === UserEncryption.V1) {
|
||||||
return decryptSymmetric128BitHexKeyUTF8({
|
return decryptSymmetric128BitHexKeyUTF8({
|
||||||
ciphertext: user.encryptedPrivateKey,
|
ciphertext: user.encryptedPrivateKey,
|
||||||
iv: user.iv,
|
iv: user.iv,
|
||||||
@@ -119,7 +124,12 @@ export const getUserPrivateKey = async (
|
|||||||
key: password.slice(0, 32).padStart(32 + (password.slice(0, 32).length - new Blob([password]).size), "0")
|
key: password.slice(0, 32).padStart(32 + (password.slice(0, 32).length - new Blob([password]).size), "0")
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
if (user.encryptionVersion === 2 && user.protectedKey && user.protectedKeyIV && user.protectedKeyTag) {
|
if (
|
||||||
|
user.encryptionVersion === UserEncryption.V2 &&
|
||||||
|
user.protectedKey &&
|
||||||
|
user.protectedKeyIV &&
|
||||||
|
user.protectedKeyTag
|
||||||
|
) {
|
||||||
const derivedKey = await argon2.hash(password, {
|
const derivedKey = await argon2.hash(password, {
|
||||||
salt: Buffer.from(user.salt),
|
salt: Buffer.from(user.salt),
|
||||||
memoryCost: 65536,
|
memoryCost: 65536,
|
||||||
|
|||||||
@@ -1,10 +1,16 @@
|
|||||||
import fs from "fs/promises";
|
import fs from "fs/promises";
|
||||||
import path from "path";
|
import path from "path";
|
||||||
|
|
||||||
export const isDisposableEmail = async (email: string) => {
|
export const isDisposableEmail = async (emails: string | string[]) => {
|
||||||
const emailDomain = email.split("@")[1];
|
|
||||||
const disposableEmails = await fs.readFile(path.join(__dirname, "disposable_emails.txt"), "utf8");
|
const disposableEmails = await fs.readFile(path.join(__dirname, "disposable_emails.txt"), "utf8");
|
||||||
|
if (Array.isArray(emails)) {
|
||||||
|
return emails.some((email) => {
|
||||||
|
const emailDomain = email.split("@")[1];
|
||||||
|
return disposableEmails.split("\n").includes(emailDomain);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const emailDomain = emails.split("@")[1];
|
||||||
if (disposableEmails.split("\n").includes(emailDomain)) return true;
|
if (disposableEmails.split("\n").includes(emailDomain)) return true;
|
||||||
return false;
|
return false;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -495,7 +495,8 @@ export const registerRoutes = async (
|
|||||||
smtpService,
|
smtpService,
|
||||||
userDAL,
|
userDAL,
|
||||||
groupDAL,
|
groupDAL,
|
||||||
orgBotDAL
|
orgBotDAL,
|
||||||
|
projectRoleDAL
|
||||||
});
|
});
|
||||||
const signupService = authSignupServiceFactory({
|
const signupService = authSignupServiceFactory({
|
||||||
tokenService,
|
tokenService,
|
||||||
|
|||||||
@@ -18,9 +18,14 @@ export const registerInviteOrgRouter = async (server: FastifyZodProvider) => {
|
|||||||
body: z.object({
|
body: z.object({
|
||||||
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(),
|
projects: z
|
||||||
projectRoleSlug: z.nativeEnum(ProjectMembershipRole).optional(),
|
.object({
|
||||||
organizationRoleSlug: z.nativeEnum(OrgMembershipRole)
|
id: z.string(),
|
||||||
|
projectRoleSlug: z.string().array().default([ProjectMembershipRole.Member])
|
||||||
|
})
|
||||||
|
.array()
|
||||||
|
.optional(),
|
||||||
|
organizationRoleSlug: z.string().default(OrgMembershipRole.Member)
|
||||||
}),
|
}),
|
||||||
response: {
|
response: {
|
||||||
200: z.object({
|
200: z.object({
|
||||||
@@ -40,12 +45,12 @@ export const registerInviteOrgRouter = async (server: FastifyZodProvider) => {
|
|||||||
handler: async (req) => {
|
handler: async (req) => {
|
||||||
if (req.auth.actor !== ActorType.USER) return;
|
if (req.auth.actor !== ActorType.USER) return;
|
||||||
|
|
||||||
const completeInviteLinks = await server.services.org.inviteUserToOrganization({
|
const { signupTokens: completeInviteLinks } = await server.services.org.inviteUserToOrganization({
|
||||||
orgId: req.body.organizationId,
|
orgId: req.body.organizationId,
|
||||||
userId: req.permission.id,
|
actor: req.permission.type,
|
||||||
|
actorId: req.permission.id,
|
||||||
inviteeEmails: req.body.inviteeEmails,
|
inviteeEmails: req.body.inviteeEmails,
|
||||||
projectIds: req.body.projectIds,
|
projects: req.body.projects,
|
||||||
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,6 @@
|
|||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
|
||||||
import { ProjectMembershipsSchema } from "@app/db/schemas";
|
import { OrgMembershipRole, ProjectMembershipRole, ProjectMembershipsSchema } from "@app/db/schemas";
|
||||||
import { EventType } from "@app/ee/services/audit-log/audit-log-types";
|
import { EventType } from "@app/ee/services/audit-log/audit-log-types";
|
||||||
import { PROJECT_USERS } from "@app/lib/api-docs";
|
import { PROJECT_USERS } from "@app/lib/api-docs";
|
||||||
import { writeLimit } from "@app/server/config/rateLimiter";
|
import { writeLimit } from "@app/server/config/rateLimiter";
|
||||||
@@ -36,14 +36,21 @@ export const registerProjectMembershipRouter = async (server: FastifyZodProvider
|
|||||||
},
|
},
|
||||||
onRequest: verifyAuth([AuthMode.JWT, AuthMode.API_KEY, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
onRequest: verifyAuth([AuthMode.JWT, AuthMode.API_KEY, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||||
handler: async (req) => {
|
handler: async (req) => {
|
||||||
const memberships = await server.services.projectMembership.addUsersToProjectNonE2EE({
|
const usernamesAndEmails = [...req.body.emails, ...req.body.usernames];
|
||||||
projectId: req.params.projectId,
|
const { projectMemberships: memberships } = await server.services.org.inviteUserToOrganization({
|
||||||
actorAuthMethod: req.permission.authMethod,
|
actorAuthMethod: req.permission.authMethod,
|
||||||
actorId: req.permission.id,
|
actorId: req.permission.id,
|
||||||
actorOrgId: req.permission.orgId,
|
actorOrgId: req.permission.orgId,
|
||||||
actor: req.permission.type,
|
actor: req.permission.type,
|
||||||
emails: req.body.emails,
|
inviteeEmails: usernamesAndEmails,
|
||||||
usernames: req.body.usernames
|
orgId: req.permission.orgId,
|
||||||
|
organizationRoleSlug: OrgMembershipRole.NoAccess,
|
||||||
|
projects: [
|
||||||
|
{
|
||||||
|
id: req.params.projectId,
|
||||||
|
projectRoleSlug: [ProjectMembershipRole.Member]
|
||||||
|
}
|
||||||
|
]
|
||||||
});
|
});
|
||||||
|
|
||||||
await server.services.auditLog.createAuditLog({
|
await server.services.auditLog.createAuditLog({
|
||||||
|
|||||||
@@ -1,15 +1,15 @@
|
|||||||
import bcrypt from "bcrypt";
|
import bcrypt from "bcrypt";
|
||||||
import jwt from "jsonwebtoken";
|
import jwt from "jsonwebtoken";
|
||||||
|
|
||||||
import { OrgMembershipStatus, TableName } from "@app/db/schemas";
|
import { OrgMembershipStatus, SecretKeyEncoding, 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";
|
||||||
import { isAuthMethodSaml } from "@app/ee/services/permission/permission-fns";
|
import { isAuthMethodSaml } from "@app/ee/services/permission/permission-fns";
|
||||||
import { getConfig } from "@app/lib/config/env";
|
import { getConfig } from "@app/lib/config/env";
|
||||||
import { infisicalSymmetricEncypt } from "@app/lib/crypto/encryption";
|
import { infisicalSymmetricDecrypt, infisicalSymmetricEncypt } from "@app/lib/crypto/encryption";
|
||||||
import { getUserPrivateKey } from "@app/lib/crypto/srp";
|
import { generateUserSrpKeys, getUserPrivateKey } from "@app/lib/crypto/srp";
|
||||||
import { BadRequestError, UnauthorizedError } from "@app/lib/errors";
|
import { BadRequestError } from "@app/lib/errors";
|
||||||
import { isDisposableEmail } from "@app/lib/validator";
|
import { isDisposableEmail } from "@app/lib/validator";
|
||||||
import { TGroupProjectDALFactory } from "@app/services/group-project/group-project-dal";
|
import { TGroupProjectDALFactory } from "@app/services/group-project/group-project-dal";
|
||||||
import { TProjectDALFactory } from "@app/services/project/project-dal";
|
import { TProjectDALFactory } from "@app/services/project/project-dal";
|
||||||
@@ -17,14 +17,14 @@ 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 { TokenMetadataType, TokenType, TTokenMetadata } from "../auth-token/auth-token-types";
|
import { TokenType } 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";
|
||||||
import { addMembersToProject } from "../project-membership/project-membership-fns";
|
|
||||||
import { TProjectUserMembershipRoleDALFactory } from "../project-membership/project-user-membership-role-dal";
|
import { TProjectUserMembershipRoleDALFactory } from "../project-membership/project-user-membership-role-dal";
|
||||||
import { SmtpTemplates, TSmtpService } from "../smtp/smtp-service";
|
import { SmtpTemplates, TSmtpService } from "../smtp/smtp-service";
|
||||||
import { TUserDALFactory } from "../user/user-dal";
|
import { TUserDALFactory } from "../user/user-dal";
|
||||||
|
import { UserEncryption } from "../user/user-types";
|
||||||
import { TAuthDALFactory } from "./auth-dal";
|
import { TAuthDALFactory } from "./auth-dal";
|
||||||
import { validateProviderAuthToken, validateSignUpAuthorization } from "./auth-fns";
|
import { validateProviderAuthToken, validateSignUpAuthorization } from "./auth-fns";
|
||||||
import { TCompleteAccountInviteDTO, TCompleteAccountSignupDTO } from "./auth-signup-type";
|
import { TCompleteAccountInviteDTO, TCompleteAccountSignupDTO } from "./auth-signup-type";
|
||||||
@@ -67,8 +67,6 @@ export const authSignupServiceFactory = ({
|
|||||||
smtpService,
|
smtpService,
|
||||||
orgService,
|
orgService,
|
||||||
orgDAL,
|
orgDAL,
|
||||||
projectMembershipDAL,
|
|
||||||
projectUserMembershipRoleDAL,
|
|
||||||
licenseService
|
licenseService
|
||||||
}: TAuthSignupDep) => {
|
}: TAuthSignupDep) => {
|
||||||
// first step of signup. create user and send email
|
// first step of signup. create user and send email
|
||||||
@@ -177,32 +175,88 @@ export const authSignupServiceFactory = ({
|
|||||||
encryptedPrivateKey,
|
encryptedPrivateKey,
|
||||||
iv: encryptedPrivateKeyIV,
|
iv: encryptedPrivateKeyIV,
|
||||||
tag: encryptedPrivateKeyTag,
|
tag: encryptedPrivateKeyTag,
|
||||||
encryptionVersion: 2
|
encryptionVersion: UserEncryption.V2
|
||||||
});
|
});
|
||||||
const { tag, encoding, ciphertext, iv } = infisicalSymmetricEncypt(privateKey);
|
const { tag, encoding, ciphertext, iv } = infisicalSymmetricEncypt(privateKey);
|
||||||
const updateduser = await authDAL.transaction(async (tx) => {
|
const updateduser = await authDAL.transaction(async (tx) => {
|
||||||
const us = await userDAL.updateById(user.id, { firstName, lastName, isAccepted: true }, tx);
|
const us = await userDAL.updateById(user.id, { firstName, lastName, isAccepted: true }, tx);
|
||||||
if (!us) throw new Error("User not found");
|
if (!us) throw new Error("User not found");
|
||||||
const userEncKey = await userDAL.upsertUserEncryptionKey(
|
const systemGeneratedUserEncryptionKey = await userDAL.findUserEncKeyByUserId(us.id, tx);
|
||||||
us.id,
|
let userEncKey;
|
||||||
{
|
|
||||||
salt,
|
// below condition is true means this is system generated credentials
|
||||||
verifier,
|
// the private key is actually system generated password
|
||||||
publicKey,
|
// thus we will re-encrypt the system generated private key with the new password
|
||||||
protectedKey,
|
// akhilmhdh: you may find this like why? The reason is simple we are moving away from e2ee and these are pieces of it
|
||||||
protectedKeyIV,
|
// without a dummy key in place some things will break and backward compatiability too. 2025 we will be removing all these things
|
||||||
protectedKeyTag,
|
if (
|
||||||
encryptedPrivateKey,
|
systemGeneratedUserEncryptionKey &&
|
||||||
iv: encryptedPrivateKeyIV,
|
!systemGeneratedUserEncryptionKey.hashedPassword &&
|
||||||
tag: encryptedPrivateKeyTag,
|
systemGeneratedUserEncryptionKey.serverEncryptedPrivateKey &&
|
||||||
hashedPassword,
|
systemGeneratedUserEncryptionKey.serverEncryptedPrivateKeyTag &&
|
||||||
serverEncryptedPrivateKeyEncoding: encoding,
|
systemGeneratedUserEncryptionKey.serverEncryptedPrivateKeyIV &&
|
||||||
serverEncryptedPrivateKeyTag: tag,
|
systemGeneratedUserEncryptionKey.serverEncryptedPrivateKeyEncoding
|
||||||
serverEncryptedPrivateKeyIV: iv,
|
) {
|
||||||
serverEncryptedPrivateKey: ciphertext
|
// get server generated password
|
||||||
},
|
const serverGeneratedPassword = infisicalSymmetricDecrypt({
|
||||||
tx
|
iv: systemGeneratedUserEncryptionKey.serverEncryptedPrivateKeyIV,
|
||||||
);
|
tag: systemGeneratedUserEncryptionKey.serverEncryptedPrivateKeyTag,
|
||||||
|
ciphertext: systemGeneratedUserEncryptionKey.serverEncryptedPrivateKey,
|
||||||
|
keyEncoding: systemGeneratedUserEncryptionKey.serverEncryptedPrivateKeyEncoding as SecretKeyEncoding
|
||||||
|
});
|
||||||
|
const serverGeneratedPrivateKey = await getUserPrivateKey(serverGeneratedPassword, {
|
||||||
|
...systemGeneratedUserEncryptionKey
|
||||||
|
});
|
||||||
|
const encKeys = await generateUserSrpKeys(email, password, {
|
||||||
|
publicKey: systemGeneratedUserEncryptionKey.publicKey,
|
||||||
|
privateKey: serverGeneratedPrivateKey
|
||||||
|
});
|
||||||
|
// now reencrypt server generated key with user provided password
|
||||||
|
userEncKey = await userDAL.upsertUserEncryptionKey(
|
||||||
|
us.id,
|
||||||
|
{
|
||||||
|
encryptionVersion: UserEncryption.V2,
|
||||||
|
protectedKey: encKeys.protectedKey,
|
||||||
|
protectedKeyIV: encKeys.protectedKeyIV,
|
||||||
|
protectedKeyTag: encKeys.protectedKeyTag,
|
||||||
|
publicKey: encKeys.publicKey,
|
||||||
|
encryptedPrivateKey: encKeys.encryptedPrivateKey,
|
||||||
|
iv: encKeys.encryptedPrivateKeyIV,
|
||||||
|
tag: encKeys.encryptedPrivateKeyTag,
|
||||||
|
salt: encKeys.salt,
|
||||||
|
verifier: encKeys.verifier,
|
||||||
|
hashedPassword,
|
||||||
|
serverEncryptedPrivateKeyEncoding: encoding,
|
||||||
|
serverEncryptedPrivateKeyTag: tag,
|
||||||
|
serverEncryptedPrivateKeyIV: iv,
|
||||||
|
serverEncryptedPrivateKey: ciphertext
|
||||||
|
},
|
||||||
|
tx
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
userEncKey = await userDAL.upsertUserEncryptionKey(
|
||||||
|
us.id,
|
||||||
|
{
|
||||||
|
encryptionVersion: UserEncryption.V2,
|
||||||
|
salt,
|
||||||
|
verifier,
|
||||||
|
publicKey,
|
||||||
|
protectedKey,
|
||||||
|
protectedKeyIV,
|
||||||
|
protectedKeyTag,
|
||||||
|
encryptedPrivateKey,
|
||||||
|
iv: encryptedPrivateKeyIV,
|
||||||
|
tag: encryptedPrivateKeyTag,
|
||||||
|
hashedPassword,
|
||||||
|
serverEncryptedPrivateKeyEncoding: encoding,
|
||||||
|
serverEncryptedPrivateKeyTag: tag,
|
||||||
|
serverEncryptedPrivateKeyIV: iv,
|
||||||
|
serverEncryptedPrivateKey: ciphertext
|
||||||
|
},
|
||||||
|
tx
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
// If it's SAML Auth and the organization ID is present, we should check if the user has a pending invite for this org, and accept it
|
// If it's SAML Auth and the organization ID is present, we should check if the user has a pending invite for this org, and accept it
|
||||||
if (
|
if (
|
||||||
(isAuthMethodSaml(authMethod) || [AuthMethod.LDAP, AuthMethod.OIDC].includes(authMethod as AuthMethod)) &&
|
(isAuthMethodSaml(authMethod) || [AuthMethod.LDAP, AuthMethod.OIDC].includes(authMethod as AuthMethod)) &&
|
||||||
@@ -312,8 +366,7 @@ export const authSignupServiceFactory = ({
|
|||||||
encryptedPrivateKey,
|
encryptedPrivateKey,
|
||||||
encryptedPrivateKeyIV,
|
encryptedPrivateKeyIV,
|
||||||
encryptedPrivateKeyTag,
|
encryptedPrivateKeyTag,
|
||||||
authorization,
|
authorization
|
||||||
tokenMetadata
|
|
||||||
}: TCompleteAccountInviteDTO) => {
|
}: TCompleteAccountInviteDTO) => {
|
||||||
const user = await userDAL.findUserByUsername(email);
|
const user = await userDAL.findUserByUsername(email);
|
||||||
if (!user || (user && user.isAccepted)) {
|
if (!user || (user && user.isAccepted)) {
|
||||||
@@ -348,65 +401,76 @@ export const authSignupServiceFactory = ({
|
|||||||
const updateduser = await authDAL.transaction(async (tx) => {
|
const updateduser = await authDAL.transaction(async (tx) => {
|
||||||
const us = await userDAL.updateById(user.id, { firstName, lastName, isAccepted: true }, tx);
|
const us = await userDAL.updateById(user.id, { firstName, lastName, isAccepted: true }, tx);
|
||||||
if (!us) throw new Error("User not found");
|
if (!us) throw new Error("User not found");
|
||||||
const userEncKey = await userDAL.upsertUserEncryptionKey(
|
const systemGeneratedUserEncryptionKey = await userDAL.findUserEncKeyByUserId(us.id, tx);
|
||||||
us.id,
|
let userEncKey;
|
||||||
{
|
// this means this is system generated credentials
|
||||||
salt,
|
// now replace the private key
|
||||||
encryptionVersion: 2,
|
if (
|
||||||
verifier,
|
systemGeneratedUserEncryptionKey &&
|
||||||
publicKey,
|
!systemGeneratedUserEncryptionKey.hashedPassword &&
|
||||||
protectedKey,
|
systemGeneratedUserEncryptionKey.serverEncryptedPrivateKey &&
|
||||||
protectedKeyIV,
|
systemGeneratedUserEncryptionKey.serverEncryptedPrivateKeyTag &&
|
||||||
protectedKeyTag,
|
systemGeneratedUserEncryptionKey.serverEncryptedPrivateKeyIV &&
|
||||||
encryptedPrivateKey,
|
systemGeneratedUserEncryptionKey.serverEncryptedPrivateKeyEncoding
|
||||||
iv: encryptedPrivateKeyIV,
|
) {
|
||||||
tag: encryptedPrivateKeyTag,
|
// get server generated password
|
||||||
hashedPassword,
|
const serverGeneratedPassword = infisicalSymmetricDecrypt({
|
||||||
serverEncryptedPrivateKeyEncoding: encoding,
|
iv: systemGeneratedUserEncryptionKey.serverEncryptedPrivateKeyIV,
|
||||||
serverEncryptedPrivateKeyTag: tag,
|
tag: systemGeneratedUserEncryptionKey.serverEncryptedPrivateKeyTag,
|
||||||
serverEncryptedPrivateKeyIV: iv,
|
ciphertext: systemGeneratedUserEncryptionKey.serverEncryptedPrivateKey,
|
||||||
serverEncryptedPrivateKey: ciphertext
|
keyEncoding: systemGeneratedUserEncryptionKey.serverEncryptedPrivateKeyEncoding as SecretKeyEncoding
|
||||||
},
|
});
|
||||||
tx
|
const serverGeneratedPrivateKey = await getUserPrivateKey(serverGeneratedPassword, {
|
||||||
);
|
...systemGeneratedUserEncryptionKey
|
||||||
|
});
|
||||||
if (tokenMetadata) {
|
const encKeys = await generateUserSrpKeys(email, password, {
|
||||||
const metadataObj = jwt.verify(tokenMetadata, appCfg.AUTH_SECRET) as TTokenMetadata;
|
publicKey: systemGeneratedUserEncryptionKey.publicKey,
|
||||||
|
privateKey: serverGeneratedPrivateKey
|
||||||
if (
|
});
|
||||||
metadataObj?.payload?.userId !== user.id ||
|
// now reencrypt server generated key with user provided password
|
||||||
metadataObj?.payload?.orgId !== orgMembership.orgId ||
|
userEncKey = await userDAL.upsertUserEncryptionKey(
|
||||||
metadataObj?.type !== TokenMetadataType.InviteToProjects
|
us.id,
|
||||||
) {
|
{
|
||||||
throw new UnauthorizedError({
|
encryptionVersion: 2,
|
||||||
message: "Malformed or invalid metadata token"
|
protectedKey: encKeys.protectedKey,
|
||||||
});
|
protectedKeyIV: encKeys.protectedKeyIV,
|
||||||
}
|
protectedKeyTag: encKeys.protectedKeyTag,
|
||||||
|
publicKey: encKeys.publicKey,
|
||||||
for await (const projectId of metadataObj.payload.projectIds) {
|
encryptedPrivateKey: encKeys.encryptedPrivateKey,
|
||||||
await addMembersToProject({
|
iv: encKeys.encryptedPrivateKeyIV,
|
||||||
orgDAL,
|
tag: encKeys.encryptedPrivateKeyTag,
|
||||||
projectDAL,
|
salt: encKeys.salt,
|
||||||
projectMembershipDAL,
|
verifier: encKeys.verifier,
|
||||||
projectKeyDAL,
|
hashedPassword,
|
||||||
userGroupMembershipDAL,
|
serverEncryptedPrivateKeyEncoding: encoding,
|
||||||
projectBotDAL,
|
serverEncryptedPrivateKeyTag: tag,
|
||||||
projectUserMembershipRoleDAL,
|
serverEncryptedPrivateKeyIV: iv,
|
||||||
smtpService
|
serverEncryptedPrivateKey: ciphertext
|
||||||
}).addMembersToNonE2EEProject(
|
},
|
||||||
{
|
tx
|
||||||
emails: [user.email!],
|
);
|
||||||
usernames: [],
|
} else {
|
||||||
projectId,
|
userEncKey = await userDAL.upsertUserEncryptionKey(
|
||||||
projectMembershipRole: metadataObj.payload.projectRoleSlug,
|
us.id,
|
||||||
sendEmails: false
|
{
|
||||||
},
|
encryptionVersion: UserEncryption.V2,
|
||||||
{
|
salt,
|
||||||
tx,
|
verifier,
|
||||||
throwOnProjectNotFound: false
|
publicKey,
|
||||||
}
|
protectedKey,
|
||||||
);
|
protectedKeyIV,
|
||||||
}
|
protectedKeyTag,
|
||||||
|
encryptedPrivateKey,
|
||||||
|
iv: encryptedPrivateKeyIV,
|
||||||
|
tag: encryptedPrivateKeyTag,
|
||||||
|
hashedPassword,
|
||||||
|
serverEncryptedPrivateKeyEncoding: encoding,
|
||||||
|
serverEncryptedPrivateKeyTag: tag,
|
||||||
|
serverEncryptedPrivateKeyIV: iv,
|
||||||
|
serverEncryptedPrivateKey: ciphertext
|
||||||
|
},
|
||||||
|
tx
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const updatedMembersips = await orgDAL.updateMembership(
|
const updatedMembersips = await orgDAL.updateMembership(
|
||||||
|
|||||||
@@ -9,7 +9,10 @@ import {
|
|||||||
OrgMembershipStatus,
|
OrgMembershipStatus,
|
||||||
ProjectMembershipRole,
|
ProjectMembershipRole,
|
||||||
ProjectVersion,
|
ProjectVersion,
|
||||||
|
SecretKeyEncoding,
|
||||||
TableName,
|
TableName,
|
||||||
|
TProjectMemberships,
|
||||||
|
TProjectUserMembershipRolesInsert,
|
||||||
TUsers
|
TUsers
|
||||||
} from "@app/db/schemas";
|
} from "@app/db/schemas";
|
||||||
import { TProjects } from "@app/db/schemas/projects";
|
import { TProjects } from "@app/db/schemas/projects";
|
||||||
@@ -18,13 +21,15 @@ import { TUserGroupMembershipDALFactory } from "@app/ee/services/group/user-grou
|
|||||||
import { TLicenseServiceFactory } from "@app/ee/services/license/license-service";
|
import { TLicenseServiceFactory } from "@app/ee/services/license/license-service";
|
||||||
import { OrgPermissionActions, OrgPermissionSubjects } from "@app/ee/services/permission/org-permission";
|
import { OrgPermissionActions, OrgPermissionSubjects } from "@app/ee/services/permission/org-permission";
|
||||||
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
|
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
|
||||||
|
import { ProjectPermissionActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission";
|
||||||
import { TProjectUserAdditionalPrivilegeDALFactory } from "@app/ee/services/project-user-additional-privilege/project-user-additional-privilege-dal";
|
import { TProjectUserAdditionalPrivilegeDALFactory } from "@app/ee/services/project-user-additional-privilege/project-user-additional-privilege-dal";
|
||||||
import { TSamlConfigDALFactory } from "@app/ee/services/saml-config/saml-config-dal";
|
import { TSamlConfigDALFactory } from "@app/ee/services/saml-config/saml-config-dal";
|
||||||
import { getConfig } from "@app/lib/config/env";
|
import { getConfig } from "@app/lib/config/env";
|
||||||
import { generateAsymmetricKeyPair } from "@app/lib/crypto";
|
import { generateAsymmetricKeyPair } from "@app/lib/crypto";
|
||||||
import { generateSymmetricKey, infisicalSymmetricEncypt } from "@app/lib/crypto/encryption";
|
import { generateSymmetricKey, infisicalSymmetricDecrypt, infisicalSymmetricEncypt } from "@app/lib/crypto/encryption";
|
||||||
import { generateUserSrpKeys } from "@app/lib/crypto/srp";
|
import { generateUserSrpKeys } from "@app/lib/crypto/srp";
|
||||||
import { BadRequestError, NotFoundError, UnauthorizedError } from "@app/lib/errors";
|
import { BadRequestError, NotFoundError, UnauthorizedError } from "@app/lib/errors";
|
||||||
|
import { groupBy } from "@app/lib/fn";
|
||||||
import { alphaNumericNanoId } from "@app/lib/nanoid";
|
import { alphaNumericNanoId } from "@app/lib/nanoid";
|
||||||
import { isDisposableEmail } from "@app/lib/validator";
|
import { isDisposableEmail } from "@app/lib/validator";
|
||||||
import { TOrgMembershipDALFactory } from "@app/services/org-membership/org-membership-dal";
|
import { TOrgMembershipDALFactory } from "@app/services/org-membership/org-membership-dal";
|
||||||
@@ -32,14 +37,14 @@ 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, TTokenMetadata } from "../auth-token/auth-token-types";
|
import { TokenType } from "../auth-token/auth-token-types";
|
||||||
import { TProjectDALFactory } from "../project/project-dal";
|
import { TProjectDALFactory } from "../project/project-dal";
|
||||||
import { verifyProjectVersions } from "../project/project-fns";
|
import { assignWorkspaceKeysToMembers } 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";
|
||||||
import { addMembersToProject } from "../project-membership/project-membership-fns";
|
|
||||||
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 { SmtpTemplates, TSmtpService } from "../smtp/smtp-service";
|
import { SmtpTemplates, TSmtpService } from "../smtp/smtp-service";
|
||||||
import { TUserDALFactory } from "../user/user-dal";
|
import { TUserDALFactory } from "../user/user-dal";
|
||||||
import { TIncidentContactsDALFactory } from "./incident-contacts-dal";
|
import { TIncidentContactsDALFactory } from "./incident-contacts-dal";
|
||||||
@@ -84,6 +89,7 @@ type TOrgServiceFactoryDep = {
|
|||||||
"getPlan" | "updateSubscriptionOrgMemberCount" | "generateOrgCustomerId" | "removeOrgCustomer"
|
"getPlan" | "updateSubscriptionOrgMemberCount" | "generateOrgCustomerId" | "removeOrgCustomer"
|
||||||
>;
|
>;
|
||||||
projectUserAdditionalPrivilegeDAL: Pick<TProjectUserAdditionalPrivilegeDALFactory, "delete">;
|
projectUserAdditionalPrivilegeDAL: Pick<TProjectUserAdditionalPrivilegeDALFactory, "delete">;
|
||||||
|
projectRoleDAL: Pick<TProjectRoleDALFactory, "find">;
|
||||||
userGroupMembershipDAL: Pick<TUserGroupMembershipDALFactory, "findUserGroupMembershipsInProject">;
|
userGroupMembershipDAL: Pick<TUserGroupMembershipDALFactory, "findUserGroupMembershipsInProject">;
|
||||||
projectBotDAL: Pick<TProjectBotDALFactory, "findOne">;
|
projectBotDAL: Pick<TProjectBotDALFactory, "findOne">;
|
||||||
projectUserMembershipRoleDAL: Pick<TProjectUserMembershipRoleDALFactory, "insertMany">;
|
projectUserMembershipRoleDAL: Pick<TProjectUserMembershipRoleDALFactory, "insertMany">;
|
||||||
@@ -111,7 +117,8 @@ export const orgServiceFactory = ({
|
|||||||
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
|
||||||
@@ -440,17 +447,17 @@ export const orgServiceFactory = ({
|
|||||||
*/
|
*/
|
||||||
const inviteUserToOrganization = async ({
|
const inviteUserToOrganization = async ({
|
||||||
orgId,
|
orgId,
|
||||||
userId,
|
actorId,
|
||||||
|
actor,
|
||||||
inviteeEmails,
|
inviteeEmails,
|
||||||
organizationRoleSlug,
|
organizationRoleSlug,
|
||||||
projectRoleSlug,
|
projects: invitedProjects,
|
||||||
projectIds,
|
|
||||||
actorAuthMethod,
|
actorAuthMethod,
|
||||||
actorOrgId
|
actorOrgId
|
||||||
}: TInviteUserToOrgDTO) => {
|
}: TInviteUserToOrgDTO) => {
|
||||||
const appCfg = getConfig();
|
const appCfg = getConfig();
|
||||||
|
|
||||||
const { permission } = await permissionService.getUserOrgPermission(userId, orgId, actorAuthMethod, actorOrgId);
|
const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId, actorAuthMethod, actorOrgId);
|
||||||
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Create, OrgPermissionSubjects.Member);
|
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Create, OrgPermissionSubjects.Member);
|
||||||
|
|
||||||
const org = await orgDAL.findOrgById(orgId);
|
const org = await orgDAL.findOrgById(orgId);
|
||||||
@@ -461,6 +468,13 @@ export const orgServiceFactory = ({
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const isEmailInvalid = await isDisposableEmail(inviteeEmails);
|
||||||
|
if (isEmailInvalid) {
|
||||||
|
throw new BadRequestError({
|
||||||
|
message: "Provided a disposable email",
|
||||||
|
name: "Org invite"
|
||||||
|
});
|
||||||
|
}
|
||||||
const plan = await licenseService.getPlan(orgId);
|
const plan = await licenseService.getPlan(orgId);
|
||||||
if (plan?.memberLimit && plan.membersUsed >= plan.memberLimit) {
|
if (plan?.memberLimit && plan.membersUsed >= plan.memberLimit) {
|
||||||
// limit imposed on number of members allowed / number of members used exceeds the number of members allowed
|
// limit imposed on number of members allowed / number of members used exceeds the number of members allowed
|
||||||
@@ -475,205 +489,331 @@ export const orgServiceFactory = ({
|
|||||||
message: "Failed to invite member due to member limit reached. Upgrade plan to invite more members."
|
message: "Failed to invite member due to member limit reached. Upgrade plan to invite more members."
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
const isCustomOrgRole = !Object.values(OrgMembershipRole).includes(organizationRoleSlug as OrgMembershipRole);
|
||||||
if (projectIds?.length) {
|
if (isCustomOrgRole) {
|
||||||
const projects = await projectDAL.find({
|
if (!plan?.rbac)
|
||||||
orgId,
|
|
||||||
$in: {
|
|
||||||
id: projectIds
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// if its not v3, throw an error
|
|
||||||
if (!verifyProjectVersions(projects, ProjectVersion.V3)) {
|
|
||||||
throw new BadRequestError({
|
throw new BadRequestError({
|
||||||
message: "One or more selected projects are not compatible with this operation. Please upgrade your projects."
|
message: "Failed to assign custom role due to RBAC restriction. Upgrade plan to assign custom role to member."
|
||||||
});
|
});
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const inviteeUsers = await orgDAL.transaction(async (tx) => {
|
const projectsToInvite = invitedProjects?.length
|
||||||
const users: Pick<
|
? await projectDAL.find({
|
||||||
TUsers & { orgId: string },
|
orgId,
|
||||||
"id" | "firstName" | "lastName" | "email" | "orgId" | "username"
|
$in: {
|
||||||
>[] = [];
|
id: invitedProjects?.map(({ id }) => id)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
: [];
|
||||||
|
if (projectsToInvite.length !== invitedProjects?.length) {
|
||||||
|
throw new UnauthorizedError({
|
||||||
|
message: "One or more project doesn't have access to"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (projectsToInvite.some((el) => el.version !== ProjectVersion.V3)) {
|
||||||
|
throw new BadRequestError({
|
||||||
|
message: "One or more selected projects are not compatible with this operation. Please upgrade your projects."
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const mailsForOrgInvitation: { email: string; userId: string; firstName: string; lastName: string }[] = [];
|
||||||
|
const mailsForProjectInvitaion: { email: string[]; projectName: string }[] = [];
|
||||||
|
const newProjectMemberships: TProjectMemberships[] = [];
|
||||||
|
await orgDAL.transaction(async (tx) => {
|
||||||
|
const users: Pick<TUsers, "id" | "firstName" | "lastName" | "email" | "username">[] = [];
|
||||||
|
|
||||||
for await (const inviteeEmail of inviteeEmails) {
|
for await (const inviteeEmail of inviteeEmails) {
|
||||||
const inviteeUser = await userDAL.findUserByUsername(inviteeEmail, tx);
|
let inviteeUser = await userDAL.findUserByUsername(inviteeEmail, tx);
|
||||||
|
|
||||||
if (inviteeUser) {
|
// if the user doesn't exist we create the user with the email
|
||||||
// if user already exist means its already part of infisical
|
if (!inviteeUser) {
|
||||||
// Thus the signup flow is not needed anymore
|
inviteeUser = await userDAL.create(
|
||||||
const [inviteeMembership] = await orgDAL.findMembership(
|
|
||||||
{
|
{
|
||||||
[`${TableName.OrgMembership}.orgId` as "orgId"]: orgId,
|
isAccepted: false,
|
||||||
[`${TableName.OrgMembership}.userId` as "userId"]: inviteeUser.id
|
email: inviteeEmail,
|
||||||
|
username: inviteeEmail,
|
||||||
|
authMethods: [AuthMethod.EMAIL],
|
||||||
|
isGhost: false
|
||||||
},
|
},
|
||||||
{ tx }
|
tx
|
||||||
);
|
);
|
||||||
if (inviteeMembership && inviteeMembership.status === OrgMembershipStatus.Accepted) {
|
|
||||||
throw new BadRequestError({
|
|
||||||
message: `Failed to invite members because ${inviteeEmail} is already part of the organization`,
|
|
||||||
name: "Invite user to org"
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!inviteeMembership) {
|
|
||||||
await orgDAL.createMembership(
|
|
||||||
{
|
|
||||||
userId: inviteeUser.id,
|
|
||||||
inviteEmail: inviteeEmail,
|
|
||||||
orgId,
|
|
||||||
role: OrgMembershipRole.Member,
|
|
||||||
status: OrgMembershipStatus.Invited,
|
|
||||||
isActive: true
|
|
||||||
},
|
|
||||||
tx
|
|
||||||
);
|
|
||||||
|
|
||||||
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"
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
await projectMembershipDAL.insertMany(
|
|
||||||
projectIds.map((id) => ({ projectId: id, userId: inviteeUser.id })),
|
|
||||||
tx
|
|
||||||
);
|
|
||||||
for await (const projectId of projectIds) {
|
|
||||||
await addMembersToProject({
|
|
||||||
orgDAL,
|
|
||||||
projectDAL,
|
|
||||||
projectMembershipDAL,
|
|
||||||
projectKeyDAL,
|
|
||||||
userGroupMembershipDAL,
|
|
||||||
projectBotDAL,
|
|
||||||
projectUserMembershipRoleDAL,
|
|
||||||
smtpService
|
|
||||||
}).addMembersToNonE2EEProject(
|
|
||||||
{
|
|
||||||
emails: [inviteeEmail],
|
|
||||||
usernames: [],
|
|
||||||
projectId,
|
|
||||||
projectMembershipRole: projectRoleSlug,
|
|
||||||
sendEmails: false
|
|
||||||
},
|
|
||||||
{
|
|
||||||
tx
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return [{ ...inviteeUser, orgId }];
|
|
||||||
}
|
}
|
||||||
const isEmailInvalid = await isDisposableEmail(inviteeEmail);
|
|
||||||
if (isEmailInvalid) {
|
const inviteeUserId = inviteeUser?.id;
|
||||||
throw new BadRequestError({
|
const existingEncrytionKey = await userDAL.findUserEncKeyByUserId(inviteeUserId, tx);
|
||||||
message: "Provided a disposable email",
|
|
||||||
name: "Org invite"
|
// when user is missing the encrytion keys
|
||||||
|
// this could happen either if user doesn't exist or user didn't find step 3 of generating the encryption keys of srp
|
||||||
|
// So what we do is we generate a random secure password and then encrypt it with a random pub-private key
|
||||||
|
// Then when user sign in (as login is not possible as isAccepted is false) we rencrypt the private key with the user password
|
||||||
|
if (!inviteeUser || (inviteeUser && !inviteeUser?.isAccepted && !existingEncrytionKey)) {
|
||||||
|
const serverGeneratedPassword = crypto.randomBytes(32).toString("hex");
|
||||||
|
const { tag, encoding, ciphertext, iv } = infisicalSymmetricEncypt(serverGeneratedPassword);
|
||||||
|
const encKeys = await generateUserSrpKeys(inviteeEmail, serverGeneratedPassword);
|
||||||
|
await userDAL.createUserEncryption(
|
||||||
|
{
|
||||||
|
userId: inviteeUserId,
|
||||||
|
encryptionVersion: 2,
|
||||||
|
protectedKey: encKeys.protectedKey,
|
||||||
|
protectedKeyIV: encKeys.protectedKeyIV,
|
||||||
|
protectedKeyTag: encKeys.protectedKeyTag,
|
||||||
|
publicKey: encKeys.publicKey,
|
||||||
|
encryptedPrivateKey: encKeys.encryptedPrivateKey,
|
||||||
|
iv: encKeys.encryptedPrivateKeyIV,
|
||||||
|
tag: encKeys.encryptedPrivateKeyTag,
|
||||||
|
salt: encKeys.salt,
|
||||||
|
verifier: encKeys.verifier,
|
||||||
|
serverEncryptedPrivateKeyEncoding: encoding,
|
||||||
|
serverEncryptedPrivateKeyTag: tag,
|
||||||
|
serverEncryptedPrivateKeyIV: iv,
|
||||||
|
serverEncryptedPrivateKey: ciphertext
|
||||||
|
},
|
||||||
|
tx
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const [inviteeMembership] = await orgDAL.findMembership(
|
||||||
|
{
|
||||||
|
[`${TableName.OrgMembership}.orgId` as "orgId"]: orgId,
|
||||||
|
[`${TableName.OrgMembership}.userId` as "userId"]: inviteeUserId
|
||||||
|
},
|
||||||
|
{ tx }
|
||||||
|
);
|
||||||
|
|
||||||
|
// if there exist no org membership we set is as given by the request
|
||||||
|
if (!inviteeMembership) {
|
||||||
|
let roleId;
|
||||||
|
const orgRole = isCustomOrgRole ? OrgMembershipRole.Custom : organizationRoleSlug;
|
||||||
|
if (isCustomOrgRole) {
|
||||||
|
const customRole = await orgRoleDAL.findOne({ slug: organizationRoleSlug, orgId });
|
||||||
|
if (!customRole)
|
||||||
|
throw new BadRequestError({ name: "Invite membership", message: "Organization role not found" });
|
||||||
|
roleId = customRole.id;
|
||||||
|
}
|
||||||
|
|
||||||
|
await orgDAL.createMembership(
|
||||||
|
{
|
||||||
|
userId: inviteeUser.id,
|
||||||
|
inviteEmail: inviteeEmail,
|
||||||
|
orgId,
|
||||||
|
role: orgRole,
|
||||||
|
status: OrgMembershipStatus.Invited,
|
||||||
|
isActive: true,
|
||||||
|
roleId
|
||||||
|
},
|
||||||
|
tx
|
||||||
|
);
|
||||||
|
mailsForOrgInvitation.push({
|
||||||
|
email: inviteeEmail,
|
||||||
|
userId: inviteeUser.id,
|
||||||
|
firstName: inviteeUser?.firstName || "",
|
||||||
|
lastName: inviteeUser.lastName || ""
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
// not invited before
|
|
||||||
const user = await userDAL.create(
|
|
||||||
{
|
|
||||||
username: inviteeEmail,
|
|
||||||
email: inviteeEmail,
|
|
||||||
isAccepted: false,
|
|
||||||
authMethods: [AuthMethod.EMAIL],
|
|
||||||
isGhost: false
|
|
||||||
},
|
|
||||||
tx
|
|
||||||
);
|
|
||||||
await orgDAL.createMembership(
|
|
||||||
{
|
|
||||||
inviteEmail: inviteeEmail,
|
|
||||||
orgId,
|
|
||||||
userId: user.id,
|
|
||||||
role: organizationRoleSlug,
|
|
||||||
status: OrgMembershipStatus.Invited,
|
|
||||||
isActive: true
|
|
||||||
},
|
|
||||||
tx
|
|
||||||
);
|
|
||||||
|
|
||||||
users.push({
|
users.push(inviteeUser);
|
||||||
...user,
|
}
|
||||||
orgId
|
|
||||||
|
const userIds = users.map(({ id }) => id);
|
||||||
|
const usernames = users.map((el) => el.username);
|
||||||
|
const userEncryptionKeys = await userDAL.findUserEncKeyByUserIdsBatch({ userIds }, tx);
|
||||||
|
// we don't need to spam with email. Thus org invitation doesn't need project invitation again
|
||||||
|
const userIdsWithOrgInvitation = new Set(mailsForOrgInvitation.map((el) => el.userId));
|
||||||
|
|
||||||
|
// if there exist no project membership we set is as given by the request
|
||||||
|
for await (const project of projectsToInvite) {
|
||||||
|
const projectId = project.id;
|
||||||
|
const { permission: projectPermission } = await permissionService.getProjectPermission(
|
||||||
|
actor,
|
||||||
|
actorId,
|
||||||
|
projectId,
|
||||||
|
actorAuthMethod,
|
||||||
|
actorOrgId
|
||||||
|
);
|
||||||
|
ForbiddenError.from(projectPermission).throwUnlessCan(
|
||||||
|
ProjectPermissionActions.Create,
|
||||||
|
ProjectPermissionSub.Member
|
||||||
|
);
|
||||||
|
const existingMembers = await projectMembershipDAL.find(
|
||||||
|
{
|
||||||
|
projectId: project.id,
|
||||||
|
$in: { userId: userIds }
|
||||||
|
},
|
||||||
|
{ tx }
|
||||||
|
);
|
||||||
|
const existingMembersGroupByUserId = groupBy(existingMembers, (i) => i.userId);
|
||||||
|
const userIdsToExcludeAsPartOfGroup = new Set(
|
||||||
|
await userGroupMembershipDAL.findUserGroupMembershipsInProject(usernames, projectId, tx)
|
||||||
|
);
|
||||||
|
const userWithEncryptionKeyInvitedToProject = userEncryptionKeys.filter(
|
||||||
|
(user) => !existingMembersGroupByUserId?.[user.userId] && !userIdsToExcludeAsPartOfGroup.has(user.userId)
|
||||||
|
);
|
||||||
|
// eslint-disable-next-line no-continue
|
||||||
|
if (!userWithEncryptionKeyInvitedToProject.length) continue;
|
||||||
|
|
||||||
|
// validate custom project role
|
||||||
|
const invitedProjectRoles = invitedProjects.find((el) => el.id === project.id)?.projectRoleSlug || [
|
||||||
|
ProjectMembershipRole.Member
|
||||||
|
];
|
||||||
|
|
||||||
|
const customProjectRoles = invitedProjectRoles.filter(
|
||||||
|
(role) => !Object.values(ProjectMembershipRole).includes(role as ProjectMembershipRole)
|
||||||
|
);
|
||||||
|
const hasCustomRole = Boolean(customProjectRoles.length);
|
||||||
|
if (hasCustomRole) {
|
||||||
|
if (!plan?.rbac)
|
||||||
|
throw new BadRequestError({
|
||||||
|
message:
|
||||||
|
"Failed to assign custom role due to RBAC restriction. Upgrade plan to assign custom role to member."
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const customRoles = hasCustomRole
|
||||||
|
? await projectRoleDAL.find({
|
||||||
|
projectId,
|
||||||
|
$in: { slug: customProjectRoles.map((role) => role) }
|
||||||
|
})
|
||||||
|
: [];
|
||||||
|
if (customRoles.length !== customProjectRoles.length)
|
||||||
|
throw new BadRequestError({ message: "Custom role not found" });
|
||||||
|
|
||||||
|
const customRolesGroupBySlug = groupBy(customRoles, ({ slug }) => slug);
|
||||||
|
|
||||||
|
const ghostUser = await projectDAL.findProjectGhostUser(projectId, tx);
|
||||||
|
if (!ghostUser) {
|
||||||
|
throw new BadRequestError({
|
||||||
|
message: "Failed to find sudo user"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const ghostUserLatestKey = await projectKeyDAL.findLatestProjectKey(ghostUser.id, projectId, tx);
|
||||||
|
if (!ghostUserLatestKey) {
|
||||||
|
throw new BadRequestError({
|
||||||
|
message: "Failed to find sudo user latest key"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const bot = await projectBotDAL.findOne({ projectId }, tx);
|
||||||
|
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: userWithEncryptionKeyInvitedToProject.map((userEnc) => ({
|
||||||
|
orgMembershipId: userEnc.userId,
|
||||||
|
projectMembershipRole: ProjectMembershipRole.Admin,
|
||||||
|
userPublicKey: userEnc.publicKey
|
||||||
|
}))
|
||||||
|
});
|
||||||
|
|
||||||
|
const projectMemberships = await projectMembershipDAL.insertMany(
|
||||||
|
userWithEncryptionKeyInvitedToProject.map((userEnc) => ({
|
||||||
|
projectId,
|
||||||
|
userId: userEnc.userId
|
||||||
|
})),
|
||||||
|
tx
|
||||||
|
);
|
||||||
|
newProjectMemberships.push(...projectMemberships);
|
||||||
|
|
||||||
|
const sanitizedProjectMembershipRoles: TProjectUserMembershipRolesInsert[] = [];
|
||||||
|
invitedProjectRoles.forEach((projectRole) => {
|
||||||
|
const isCustomRole = Boolean(customRolesGroupBySlug?.[projectRole]?.[0]);
|
||||||
|
projectMemberships.forEach((membership) => {
|
||||||
|
sanitizedProjectMembershipRoles.push({
|
||||||
|
projectMembershipId: membership.id,
|
||||||
|
role: isCustomRole ? ProjectMembershipRole.Custom : projectRole,
|
||||||
|
customRoleId: customRolesGroupBySlug[projectRole] ? customRolesGroupBySlug[projectRole][0].id : null
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
await projectUserMembershipRoleDAL.insertMany(sanitizedProjectMembershipRoles, tx);
|
||||||
|
|
||||||
|
await projectKeyDAL.insertMany(
|
||||||
|
newWsMembers.map((el) => ({
|
||||||
|
encryptedKey: el.workspaceEncryptedKey,
|
||||||
|
nonce: el.workspaceEncryptedNonce,
|
||||||
|
senderId: ghostUser.id,
|
||||||
|
receiverId: el.orgMembershipId,
|
||||||
|
projectId
|
||||||
|
})),
|
||||||
|
tx
|
||||||
|
);
|
||||||
|
mailsForProjectInvitaion.push({
|
||||||
|
email: userWithEncryptionKeyInvitedToProject
|
||||||
|
.filter((el) => !userIdsWithOrgInvitation.has(el.userId))
|
||||||
|
.map((el) => el.email || el.username),
|
||||||
|
projectName: project.name
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
return users;
|
return users;
|
||||||
});
|
});
|
||||||
|
|
||||||
const user = await userDAL.findById(userId);
|
await licenseService.updateSubscriptionOrgMemberCount(orgId);
|
||||||
|
|
||||||
const signupTokens: { email: string; link: string }[] = [];
|
const signupTokens: { email: string; link: string }[] = [];
|
||||||
if (inviteeUsers) {
|
// send org invite mail
|
||||||
for await (const invitee of inviteeUsers) {
|
await Promise.allSettled(
|
||||||
|
mailsForOrgInvitation.map(async (el) => {
|
||||||
const token = await tokenService.createTokenForUser({
|
const token = await tokenService.createTokenForUser({
|
||||||
type: TokenType.TOKEN_EMAIL_ORG_INVITATION,
|
type: TokenType.TOKEN_EMAIL_ORG_INVITATION,
|
||||||
userId: invitee.id,
|
userId: el.userId,
|
||||||
orgId
|
orgId
|
||||||
});
|
});
|
||||||
|
|
||||||
let inviteMetadata: string = "";
|
|
||||||
if (projectIds && projectIds?.length > 0) {
|
|
||||||
inviteMetadata = jwt.sign(
|
|
||||||
{
|
|
||||||
type: TokenMetadataType.InviteToProjects,
|
|
||||||
payload: {
|
|
||||||
projectIds,
|
|
||||||
projectRoleSlug: projectRoleSlug!, // Implicitly checked inside transaction if projectRoleSlug is undefined
|
|
||||||
userId: invitee.id,
|
|
||||||
orgId
|
|
||||||
}
|
|
||||||
} satisfies TTokenMetadata,
|
|
||||||
appCfg.AUTH_SECRET,
|
|
||||||
{
|
|
||||||
expiresIn: appCfg.JWT_INVITE_LIFETIME
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
signupTokens.push({
|
signupTokens.push({
|
||||||
email: invitee.email || invitee.username,
|
email: el.email,
|
||||||
link: `${appCfg.SITE_URL}/signupinvite?token=${token}${
|
link: `${appCfg.SITE_URL}/signupinvite?token=${token}&to=${el.email}&organization_id=${org?.id}`
|
||||||
inviteMetadata ? `&metadata=${inviteMetadata}` : ""
|
|
||||||
}&to=${invitee.email || invitee.username}&organization_id=${org?.id}`
|
|
||||||
});
|
});
|
||||||
|
|
||||||
await smtpService.sendMail({
|
return smtpService.sendMail({
|
||||||
template: SmtpTemplates.OrgInvite,
|
template: SmtpTemplates.OrgInvite,
|
||||||
subjectLine: "Infisical organization invitation",
|
subjectLine: "Infisical organization invitation",
|
||||||
recipients: [invitee.email || invitee.username],
|
recipients: [el.email],
|
||||||
substitutions: {
|
substitutions: {
|
||||||
metadata: inviteMetadata,
|
inviterFirstName: el.firstName,
|
||||||
inviterFirstName: user.firstName,
|
inviterUsername: el.email,
|
||||||
inviterUsername: user.username,
|
|
||||||
organizationName: org?.name,
|
organizationName: org?.name,
|
||||||
email: invitee.email || invitee.username,
|
email: el.email,
|
||||||
organizationId: org?.id.toString(),
|
organizationId: org?.id.toString(),
|
||||||
token,
|
token,
|
||||||
callback_url: `${appCfg.SITE_URL}/signupinvite`
|
callback_url: `${appCfg.SITE_URL}/signupinvite`
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
})
|
||||||
}
|
);
|
||||||
await licenseService.updateSubscriptionOrgMemberCount(orgId);
|
|
||||||
|
await Promise.allSettled(
|
||||||
|
mailsForProjectInvitaion
|
||||||
|
.filter((el) => Boolean(el.email.length))
|
||||||
|
.map(async (el) => {
|
||||||
|
return smtpService.sendMail({
|
||||||
|
template: SmtpTemplates.WorkspaceInvite,
|
||||||
|
subjectLine: "Infisical project invitation",
|
||||||
|
recipients: el.email,
|
||||||
|
substitutions: {
|
||||||
|
workspaceName: el.projectName,
|
||||||
|
callback_url: `${appCfg.SITE_URL}/login`
|
||||||
|
}
|
||||||
|
});
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
if (!appCfg.isSmtpConfigured) {
|
if (!appCfg.isSmtpConfigured) {
|
||||||
return signupTokens;
|
return { signupTokens, projectMemberships: newProjectMemberships };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return { signupTokens: undefined, projectMemberships: newProjectMemberships };
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
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";
|
||||||
@@ -26,14 +25,17 @@ export type TDeleteOrgMembershipDTO = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export type TInviteUserToOrgDTO = {
|
export type TInviteUserToOrgDTO = {
|
||||||
userId: string;
|
actorId: string;
|
||||||
|
actor: ActorType;
|
||||||
orgId: string;
|
orgId: string;
|
||||||
actorOrgId: string | undefined;
|
actorOrgId: string | undefined;
|
||||||
actorAuthMethod: ActorAuthMethod;
|
actorAuthMethod: ActorAuthMethod;
|
||||||
inviteeEmails: string[];
|
inviteeEmails: string[];
|
||||||
organizationRoleSlug: OrgMembershipRole;
|
organizationRoleSlug: string;
|
||||||
projectIds?: string[];
|
projects?: {
|
||||||
projectRoleSlug?: ProjectMembershipRole;
|
id: string;
|
||||||
|
projectRoleSlug?: string[];
|
||||||
|
}[];
|
||||||
};
|
};
|
||||||
|
|
||||||
export type TVerifyUserToOrgDTO = {
|
export type TVerifyUserToOrgDTO = {
|
||||||
|
|||||||
@@ -22,11 +22,9 @@ import { TProjectRoleDALFactory } from "../project-role/project-role-dal";
|
|||||||
import { SmtpTemplates, TSmtpService } from "../smtp/smtp-service";
|
import { SmtpTemplates, TSmtpService } from "../smtp/smtp-service";
|
||||||
import { TUserDALFactory } from "../user/user-dal";
|
import { TUserDALFactory } from "../user/user-dal";
|
||||||
import { TProjectMembershipDALFactory } from "./project-membership-dal";
|
import { TProjectMembershipDALFactory } from "./project-membership-dal";
|
||||||
import { addMembersToProject } from "./project-membership-fns";
|
|
||||||
import {
|
import {
|
||||||
ProjectUserMembershipTemporaryMode,
|
ProjectUserMembershipTemporaryMode,
|
||||||
TAddUsersToWorkspaceDTO,
|
TAddUsersToWorkspaceDTO,
|
||||||
TAddUsersToWorkspaceNonE2EEDTO,
|
|
||||||
TDeleteProjectMembershipOldDTO,
|
TDeleteProjectMembershipOldDTO,
|
||||||
TDeleteProjectMembershipsDTO,
|
TDeleteProjectMembershipsDTO,
|
||||||
TGetProjectMembershipByUsernameDTO,
|
TGetProjectMembershipByUsernameDTO,
|
||||||
@@ -61,7 +59,6 @@ export const projectMembershipServiceFactory = ({
|
|||||||
projectUserMembershipRoleDAL,
|
projectUserMembershipRoleDAL,
|
||||||
smtpService,
|
smtpService,
|
||||||
projectRoleDAL,
|
projectRoleDAL,
|
||||||
projectBotDAL,
|
|
||||||
orgDAL,
|
orgDAL,
|
||||||
projectUserAdditionalPrivilegeDAL,
|
projectUserAdditionalPrivilegeDAL,
|
||||||
userDAL,
|
userDAL,
|
||||||
@@ -214,52 +211,6 @@ export const projectMembershipServiceFactory = ({
|
|||||||
return orgMembers;
|
return orgMembers;
|
||||||
};
|
};
|
||||||
|
|
||||||
const addUsersToProjectNonE2EE = async ({
|
|
||||||
projectId,
|
|
||||||
actorId,
|
|
||||||
actorAuthMethod,
|
|
||||||
actor,
|
|
||||||
actorOrgId,
|
|
||||||
emails,
|
|
||||||
usernames,
|
|
||||||
sendEmails = true
|
|
||||||
}: TAddUsersToWorkspaceNonE2EEDTO) => {
|
|
||||||
const project = await projectDAL.findById(projectId);
|
|
||||||
if (!project) throw new BadRequestError({ message: "Project not found" });
|
|
||||||
|
|
||||||
if (project.version === ProjectVersion.V1) {
|
|
||||||
throw new BadRequestError({ message: "Please upgrade your project on your dashboard" });
|
|
||||||
}
|
|
||||||
|
|
||||||
const { permission } = await permissionService.getProjectPermission(
|
|
||||||
actor,
|
|
||||||
actorId,
|
|
||||||
projectId,
|
|
||||||
actorAuthMethod,
|
|
||||||
actorOrgId
|
|
||||||
);
|
|
||||||
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Create, ProjectPermissionSub.Member);
|
|
||||||
|
|
||||||
const members = await addMembersToProject({
|
|
||||||
orgDAL,
|
|
||||||
projectDAL,
|
|
||||||
projectMembershipDAL,
|
|
||||||
projectKeyDAL,
|
|
||||||
userGroupMembershipDAL,
|
|
||||||
projectBotDAL,
|
|
||||||
projectUserMembershipRoleDAL,
|
|
||||||
smtpService
|
|
||||||
}).addMembersToNonE2EEProject({
|
|
||||||
emails,
|
|
||||||
usernames,
|
|
||||||
projectId,
|
|
||||||
projectMembershipRole: ProjectMembershipRole.Member,
|
|
||||||
sendEmails
|
|
||||||
});
|
|
||||||
|
|
||||||
return members;
|
|
||||||
};
|
|
||||||
|
|
||||||
const updateProjectMembership = async ({
|
const updateProjectMembership = async ({
|
||||||
actorId,
|
actorId,
|
||||||
actor,
|
actor,
|
||||||
@@ -530,7 +481,6 @@ export const projectMembershipServiceFactory = ({
|
|||||||
getProjectMemberships,
|
getProjectMemberships,
|
||||||
getProjectMembershipByUsername,
|
getProjectMembershipByUsername,
|
||||||
updateProjectMembership,
|
updateProjectMembership,
|
||||||
addUsersToProjectNonE2EE,
|
|
||||||
deleteProjectMemberships,
|
deleteProjectMemberships,
|
||||||
deleteProjectMembership, // TODO: Remove this
|
deleteProjectMembership, // TODO: Remove this
|
||||||
addUsersToProject,
|
addUsersToProject,
|
||||||
|
|||||||
@@ -82,10 +82,9 @@ export const userDALFactory = (db: TDbClient) => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const findUserEncKeyByUserId = async (userId: string) => {
|
const findUserEncKeyByUserId = async (userId: string, tx?: Knex) => {
|
||||||
try {
|
try {
|
||||||
const user = await db
|
const user = await (tx || db.replicaNode())(TableName.Users)
|
||||||
.replicaNode()(TableName.Users)
|
|
||||||
.where(`${TableName.Users}.id`, userId)
|
.where(`${TableName.Users}.id`, userId)
|
||||||
.join(TableName.UserEncryptionKey, `${TableName.Users}.id`, `${TableName.UserEncryptionKey}.userId`)
|
.join(TableName.UserEncryptionKey, `${TableName.Users}.id`, `${TableName.UserEncryptionKey}.userId`)
|
||||||
.first();
|
.first();
|
||||||
|
|||||||
@@ -4,18 +4,14 @@ import { alphaNumericNanoId } from "@app/lib/nanoid";
|
|||||||
import { TUserDALFactory } from "@app/services/user/user-dal";
|
import { TUserDALFactory } from "@app/services/user/user-dal";
|
||||||
|
|
||||||
export const normalizeUsername = async (username: string, userDAL: Pick<TUserDALFactory, "findOne">) => {
|
export const normalizeUsername = async (username: string, userDAL: Pick<TUserDALFactory, "findOne">) => {
|
||||||
let attempt = slugify(`${username}-${alphaNumericNanoId(4)}`);
|
let attempt: string;
|
||||||
|
let user;
|
||||||
|
|
||||||
let user = await userDAL.findOne({ username: attempt });
|
do {
|
||||||
if (!user) return attempt;
|
|
||||||
|
|
||||||
while (true) {
|
|
||||||
attempt = slugify(`${username}-${alphaNumericNanoId(4)}`);
|
attempt = slugify(`${username}-${alphaNumericNanoId(4)}`);
|
||||||
// eslint-disable-next-line no-await-in-loop
|
// eslint-disable-next-line no-await-in-loop
|
||||||
user = await userDAL.findOne({ username: attempt });
|
user = await userDAL.findOne({ username: attempt });
|
||||||
|
} while (user);
|
||||||
|
|
||||||
if (!user) {
|
return attempt;
|
||||||
return attempt;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -0,0 +1,4 @@
|
|||||||
|
export enum UserEncryption {
|
||||||
|
V1 = 1,
|
||||||
|
V2 = 2
|
||||||
|
}
|
||||||
|
|||||||
@@ -150,8 +150,7 @@ export type DeletOrgMembershipDTO = {
|
|||||||
|
|
||||||
export type AddUserToOrgDTO = {
|
export type AddUserToOrgDTO = {
|
||||||
inviteeEmails: string[];
|
inviteeEmails: string[];
|
||||||
projectIds?: string[];
|
projects?: { id: string; projectRoleSlug: string[] }[];
|
||||||
projectRoleSlug?: string;
|
|
||||||
organizationRoleSlug: string;
|
organizationRoleSlug: string;
|
||||||
organizationId: string;
|
organizationId: string;
|
||||||
};
|
};
|
||||||
|
|||||||
+1
-2
@@ -129,8 +129,7 @@ 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,
|
projects: projectIds.map((id) => ({ id, projectRoleSlug: [projectRoleSlug] }))
|
||||||
projectRoleSlug
|
|
||||||
});
|
});
|
||||||
|
|
||||||
setCompleteInviteLinks(data?.completeInviteLinks ?? null);
|
setCompleteInviteLinks(data?.completeInviteLinks ?? null);
|
||||||
|
|||||||
+1
-3
@@ -101,9 +101,7 @@ export const AddMemberModal = ({ popUp, handlePopUpToggle }: Props) => {
|
|||||||
members?.forEach((member) => {
|
members?.forEach((member) => {
|
||||||
wsUserUsernames.set(member.user.username, true);
|
wsUserUsernames.set(member.user.username, true);
|
||||||
});
|
});
|
||||||
return (orgUsers || []).filter(
|
return (orgUsers || []).filter(({ user: u }) => !wsUserUsernames.has(u.username));
|
||||||
({ status, user: u }) => status === "accepted" && !wsUserUsernames.has(u.username)
|
|
||||||
);
|
|
||||||
}, [orgUsers, members]);
|
}, [orgUsers, members]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
+8
-5
@@ -1,10 +1,11 @@
|
|||||||
import { useMemo,useState } from "react";
|
import { useMemo, useState } from "react";
|
||||||
import {
|
import {
|
||||||
faClock,
|
faClock,
|
||||||
faEdit,
|
faEdit,
|
||||||
faMagnifyingGlass,
|
faMagnifyingGlass,
|
||||||
faTrash,
|
faTrash,
|
||||||
faUsers} from "@fortawesome/free-solid-svg-icons";
|
faUsers
|
||||||
|
} from "@fortawesome/free-solid-svg-icons";
|
||||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||||
import { twMerge } from "tailwind-merge";
|
import { twMerge } from "tailwind-merge";
|
||||||
|
|
||||||
@@ -27,12 +28,14 @@ import {
|
|||||||
Th,
|
Th,
|
||||||
THead,
|
THead,
|
||||||
Tooltip,
|
Tooltip,
|
||||||
Tr} from "@app/components/v2";
|
Tr
|
||||||
|
} from "@app/components/v2";
|
||||||
import {
|
import {
|
||||||
ProjectPermissionActions,
|
ProjectPermissionActions,
|
||||||
ProjectPermissionSub,
|
ProjectPermissionSub,
|
||||||
useUser,
|
useUser,
|
||||||
useWorkspace} from "@app/context";
|
useWorkspace
|
||||||
|
} from "@app/context";
|
||||||
import { useGetWorkspaceUsers } from "@app/hooks/api";
|
import { useGetWorkspaceUsers } from "@app/hooks/api";
|
||||||
import { ProjectMembershipRole } from "@app/hooks/api/roles/types";
|
import { ProjectMembershipRole } from "@app/hooks/api/roles/types";
|
||||||
import { TWorkspaceUser } from "@app/hooks/api/types";
|
import { TWorkspaceUser } from "@app/hooks/api/types";
|
||||||
@@ -104,7 +107,7 @@ export const MembersTable = ({ popUp, handlePopUpOpen, handlePopUpToggle }: Prop
|
|||||||
{!isMembersLoading &&
|
{!isMembersLoading &&
|
||||||
filterdUsers?.map((projectMember, index) => {
|
filterdUsers?.map((projectMember, index) => {
|
||||||
const { user: u, inviteEmail, id: membershipId, roles } = projectMember;
|
const { user: u, inviteEmail, id: membershipId, roles } = projectMember;
|
||||||
const name = u ? `${u.firstName} ${u.lastName}` : "-";
|
const name = u.firstName || u.lastName ? `${u.firstName} ${u.lastName || ""}` : "-";
|
||||||
const email = u?.email || inviteEmail;
|
const email = u?.email || inviteEmail;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
Reference in New Issue
Block a user