diff --git a/backend/src/server/routes/v3/signup-router.ts b/backend/src/server/routes/v3/signup-router.ts index 59131464a..276a2e8cb 100644 --- a/backend/src/server/routes/v3/signup-router.ts +++ b/backend/src/server/routes/v3/signup-router.ts @@ -179,7 +179,8 @@ export const registerSignupRouter = async (server: FastifyZodProvider) => { encryptedPrivateKeyIV: z.string().trim(), encryptedPrivateKeyTag: z.string().trim(), salt: z.string().trim(), - verifier: z.string().trim() + verifier: z.string().trim(), + metadata: z.string().optional() }), response: { 200: z.object({ diff --git a/backend/src/services/auth-token/auth-token-types.ts b/backend/src/services/auth-token/auth-token-types.ts index 8917bd672..dbe949fd9 100644 --- a/backend/src/services/auth-token/auth-token-types.ts +++ b/backend/src/services/auth-token/auth-token-types.ts @@ -49,3 +49,14 @@ export type TIssueAuthTokenDTO = { ip: string; userAgent: string; }; + +export enum TokenMetadataType { + 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 TTokenMetadata = { + type: TokenMetadataType.InviteToProjects; + projectIds: string[]; + userId: string; +}; diff --git a/backend/src/services/auth/auth-signup-service.ts b/backend/src/services/auth/auth-signup-service.ts index 83a5b27d9..8714f5081 100644 --- a/backend/src/services/auth/auth-signup-service.ts +++ b/backend/src/services/auth/auth-signup-service.ts @@ -1,7 +1,7 @@ import bcrypt from "bcrypt"; import jwt from "jsonwebtoken"; -import { OrgMembershipStatus, TableName } from "@app/db/schemas"; +import { OrgMembershipStatus, ProjectMembershipRole, TableName } from "@app/db/schemas"; import { convertPendingGroupAdditionsToGroupMemberships } from "@app/ee/services/group/group-fns"; import { TUserGroupMembershipDALFactory } from "@app/ee/services/group/user-group-membership-dal"; import { TLicenseServiceFactory } from "@app/ee/services/license/license-service"; @@ -9,7 +9,7 @@ import { isAuthMethodSaml } from "@app/ee/services/permission/permission-fns"; import { getConfig } from "@app/lib/config/env"; import { infisicalSymmetricEncypt } from "@app/lib/crypto/encryption"; import { getUserPrivateKey } from "@app/lib/crypto/srp"; -import { BadRequestError } from "@app/lib/errors"; +import { BadRequestError, UnauthorizedError } from "@app/lib/errors"; import { isDisposableEmail } from "@app/lib/validator"; import { TGroupProjectDALFactory } from "@app/services/group-project/group-project-dal"; import { TProjectDALFactory } from "@app/services/project/project-dal"; @@ -17,9 +17,12 @@ import { TProjectBotDALFactory } from "@app/services/project-bot/project-bot-dal import { TProjectKeyDALFactory } from "@app/services/project-key/project-key-dal"; import { TAuthTokenServiceFactory } from "../auth-token/auth-token-service"; -import { TokenType } from "../auth-token/auth-token-types"; +import { TokenType, TTokenMetadata } from "../auth-token/auth-token-types"; import { TOrgDALFactory } from "../org/org-dal"; import { TOrgServiceFactory } from "../org/org-service"; +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 { SmtpTemplates, TSmtpService } from "../smtp/smtp-service"; import { TUserDALFactory } from "../user/user-dal"; import { TAuthDALFactory } from "./auth-dal"; @@ -32,10 +35,14 @@ type TAuthSignupDep = { userDAL: TUserDALFactory; userGroupMembershipDAL: Pick< TUserGroupMembershipDALFactory, - "find" | "transaction" | "insertMany" | "deletePendingUserGroupMembershipsByUserIds" + | "find" + | "transaction" + | "insertMany" + | "deletePendingUserGroupMembershipsByUserIds" + | "findUserGroupMembershipsInProject" >; projectKeyDAL: Pick; - projectDAL: Pick; + projectDAL: Pick; projectBotDAL: Pick; groupProjectDAL: Pick; orgService: Pick; @@ -43,6 +50,8 @@ type TAuthSignupDep = { tokenService: TAuthTokenServiceFactory; smtpService: TSmtpService; licenseService: Pick; + projectMembershipDAL: Pick; + projectUserMembershipRoleDAL: Pick; }; export type TAuthSignupFactory = ReturnType; @@ -58,6 +67,8 @@ export const authSignupServiceFactory = ({ smtpService, orgService, orgDAL, + projectMembershipDAL, + projectUserMembershipRoleDAL, licenseService }: TAuthSignupDep) => { // first step of signup. create user and send email @@ -301,7 +312,8 @@ export const authSignupServiceFactory = ({ encryptedPrivateKey, encryptedPrivateKeyIV, encryptedPrivateKeyTag, - authorization + authorization, + metadata }: TCompleteAccountInviteDTO) => { const user = await userDAL.findUserByUsername(email); if (!user || (user && user.isAccepted)) { @@ -358,6 +370,38 @@ export const authSignupServiceFactory = ({ tx ); + if (metadata) { + const metadataObj = jwt.verify(metadata, appCfg.AUTH_SECRET) as TTokenMetadata; + + if (user.id !== metadataObj.userId) { + throw new UnauthorizedError({ + message: "Malformed or invalid metadata token" + }); + } + + for await (const projectId of metadataObj.projectIds) { + await addMembersToProject({ + orgDAL, + projectDAL, + projectMembershipDAL, + projectKeyDAL, + userGroupMembershipDAL, + projectBotDAL, + projectUserMembershipRoleDAL, + smtpService + }).addMembersToNonE2EEProject( + { + emails: [user.email!], + usernames: [], + projectId, + projectMembershipRole: ProjectMembershipRole.Member, + sendEmails: false + }, + tx + ); + } + } + const updatedMembersips = await orgDAL.updateMembership( { inviteEmail: email, status: OrgMembershipStatus.Invited }, { userId: us.id, status: OrgMembershipStatus.Accepted }, diff --git a/frontend/src/pages/signupinvite.tsx b/frontend/src/pages/signupinvite.tsx index 725dc6113..9c5c01b32 100644 --- a/frontend/src/pages/signupinvite.tsx +++ b/frontend/src/pages/signupinvite.tsx @@ -64,6 +64,10 @@ export default function SignupInvite() { const email = (parsedUrl.to as string)?.replace(" ", "+").trim(); const { config } = useServerConfig(); + const queryParams = new URLSearchParams(window.location.search); + + const metadata = queryParams.get("metadata") || undefined; + const { mutateAsync: selectOrganization } = useSelectOrganization(); useEffect(() => { @@ -160,7 +164,8 @@ export default function SignupInvite() { encryptedPrivateKeyIV, encryptedPrivateKeyTag, salt: result.salt, - verifier: result.verifier + verifier: result.verifier, + metadata }); // unset temporary signup JWT token and set JWT token