From b9ae224aef4f1fef39b9c516c2c54ae815be2bf1 Mon Sep 17 00:00:00 2001 From: Tuan Dang Date: Fri, 28 Apr 2023 17:57:50 +0300 Subject: [PATCH] Patch organization invitation emails expiring for existing users and billing logic affected by missing organization populate call --- .../src/controllers/v1/membershipOrgController.ts | 14 +++++++++++--- .../src/controllers/v1/organizationController.ts | 12 ++++++------ backend/src/routes/v1/inviteOrg.ts | 1 + .../templates/organizationInvitation.handlebars | 2 +- frontend/src/pages/api/auth/VerifySignupInvite.ts | 5 ++++- frontend/src/pages/signupinvite.tsx | 4 +++- 6 files changed, 26 insertions(+), 12 deletions(-) diff --git a/backend/src/controllers/v1/membershipOrgController.ts b/backend/src/controllers/v1/membershipOrgController.ts index b25a9b9a7..18247f10f 100644 --- a/backend/src/controllers/v1/membershipOrgController.ts +++ b/backend/src/controllers/v1/membershipOrgController.ts @@ -1,3 +1,4 @@ +import { Types } from 'mongoose'; import { Request, Response } from 'express'; import * as Sentry from '@sentry/node'; import { MembershipOrg, Organization, User } from '../../models'; @@ -139,7 +140,7 @@ export const inviteUserToOrganization = async (req: Request, res: Response) => { inviteEmail: inviteeEmail, organization: organizationId, role: MEMBER, - status: invitee?.publicKey ? ACCEPTED : INVITED + status: INVITED }).save(); } } else { @@ -164,6 +165,7 @@ export const inviteUserToOrganization = async (req: Request, res: Response) => { const organization = await Organization.findOne({ _id: organizationId }); if (organization) { + const token = await TokenService.createToken({ type: TOKEN_EMAIL_ORG_INVITATION, email: inviteeEmail, @@ -179,6 +181,7 @@ export const inviteUserToOrganization = async (req: Request, res: Response) => { inviterEmail: req.user.email, organizationName: organization.name, email: inviteeEmail, + organizationId: organization._id.toString(), token, callback_url: (await getSiteURL()) + '/signupinvite' } @@ -214,13 +217,18 @@ export const inviteUserToOrganization = async (req: Request, res: Response) => { export const verifyUserToOrganization = async (req: Request, res: Response) => { let user, token; try { - const { email, code } = req.body; + const { + email, + organizationId, + code + } = req.body; user = await User.findOne({ email }).select('+publicKey'); const membershipOrg = await MembershipOrg.findOne({ inviteEmail: email, - status: INVITED + status: INVITED, + organization: new Types.ObjectId(organizationId) }); if (!membershipOrg) diff --git a/backend/src/controllers/v1/organizationController.ts b/backend/src/controllers/v1/organizationController.ts index 6b082dac5..17103a7a6 100644 --- a/backend/src/controllers/v1/organizationController.ts +++ b/backend/src/controllers/v1/organizationController.ts @@ -85,7 +85,7 @@ export const createOrganization = async (req: Request, res: Response) => { export const getOrganization = async (req: Request, res: Response) => { let organization; try { - organization = req.membershipOrg.organization; + organization = req.organization } catch (err) { Sentry.setUser({ email: req.user.email }); Sentry.captureException(err); @@ -323,14 +323,14 @@ export const createOrganizationPortalSession = async ( // check if there is a payment method on file const paymentMethods = await stripe.paymentMethods.list({ - customer: req.membershipOrg.organization.customerId, + customer: req.organization.customerId, type: 'card' }); - + if (paymentMethods.data.length < 1) { // case: no payment method on file session = await stripe.checkout.sessions.create({ - customer: req.membershipOrg.organization.customerId, + customer: req.organization.customerId, mode: 'setup', payment_method_types: ['card'], success_url: (await getSiteURL()) + '/dashboard', @@ -338,7 +338,7 @@ export const createOrganizationPortalSession = async ( }); } else { session = await stripe.billingPortal.sessions.create({ - customer: req.membershipOrg.organization.customerId, + customer: req.organization.customerId, return_url: (await getSiteURL()) + '/dashboard' }); } @@ -370,7 +370,7 @@ export const getOrganizationSubscriptions = async ( }); subscriptions = await stripe.subscriptions.list({ - customer: req.membershipOrg.organization.customerId + customer: req.organization.customerId }); } catch (err) { Sentry.setUser({ email: req.user.email }); diff --git a/backend/src/routes/v1/inviteOrg.ts b/backend/src/routes/v1/inviteOrg.ts index 4762711fe..9b4889bc9 100644 --- a/backend/src/routes/v1/inviteOrg.ts +++ b/backend/src/routes/v1/inviteOrg.ts @@ -19,6 +19,7 @@ router.post( router.post( '/verify', body('email').exists().trim().notEmpty(), + body('organizationId').exists().trim().notEmpty(), body('code').exists().trim().notEmpty(), validateRequest, membershipOrgController.verifyUserToOrganization diff --git a/backend/src/templates/organizationInvitation.handlebars b/backend/src/templates/organizationInvitation.handlebars index 045a9602b..b281786f4 100644 --- a/backend/src/templates/organizationInvitation.handlebars +++ b/backend/src/templates/organizationInvitation.handlebars @@ -9,7 +9,7 @@

Join your organization on Infisical

{{inviterFirstName}} ({{inviterEmail}}) has invited you to their Infisical organization — {{organizationName}}

- Join now + Join now

What is Infisical?

Infisical is an easy-to-use end-to-end encrypted tool that enables developers to sync and manage their secrets and configs.

diff --git a/frontend/src/pages/api/auth/VerifySignupInvite.ts b/frontend/src/pages/api/auth/VerifySignupInvite.ts index 71f724654..232e044f3 100644 --- a/frontend/src/pages/api/auth/VerifySignupInvite.ts +++ b/frontend/src/pages/api/auth/VerifySignupInvite.ts @@ -1,22 +1,25 @@ interface Props { email: string; code: string; + organizationId: string; } /** * This route verifies the signup invite link * @param {object} obj * @param {string} obj.email - email that a user is trying to verify + * @param {string} obj.organizationId - id of organization that a user is trying to verify for * @param {string} obj.code - code that a user received to the abovementioned email * @returns */ -const verifySignupInvite = ({ email, code }: Props) => fetch('/api/v1/invite-org/verify', { +const verifySignupInvite = ({ email, organizationId, code }: Props) => fetch('/api/v1/invite-org/verify', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ email, + organizationId, code }) }); diff --git a/frontend/src/pages/signupinvite.tsx b/frontend/src/pages/signupinvite.tsx index abe5d3770..4aa9a0c77 100644 --- a/frontend/src/pages/signupinvite.tsx +++ b/frontend/src/pages/signupinvite.tsx @@ -51,6 +51,7 @@ export default function SignupInvite() { const router = useRouter(); const parsedUrl = queryString.parse(router.asPath.split('?')[1]); const token = parsedUrl.token as string; + const organizationId = parsedUrl.organization_id as string; const email = (parsedUrl.to as string)?.replace(' ', '+').trim(); // Verifies if the information that the users entered (name, workspace) is there, and if the password matched the criteria. @@ -190,7 +191,8 @@ export default function SignupInvite() { onButtonPressed={async () => { const response = await verifySignupInvite({ email, - code: token + code: token, + organizationId }); if (response.status === 200) { const res = await response.json();