import { Request, Response } from 'express'; import * as Sentry from '@sentry/node'; import crypto from 'crypto'; import { SITE_URL, JWT_SIGNUP_LIFETIME, JWT_SIGNUP_SECRET } from '../../config'; import { MembershipOrg, Organization, User, Token } from '../../models'; import { deleteMembershipOrg as deleteMemberFromOrg } from '../../helpers/membershipOrg'; import { checkEmailVerification } from '../../helpers/signup'; import { createToken } from '../../helpers/auth'; import { updateSubscriptionOrgQuantity } from '../../helpers/organization'; import { sendMail } from '../../helpers/nodemailer'; import { OWNER, ADMIN, MEMBER, ACCEPTED, INVITED } from '../../variables'; /** * Delete organization membership with id [membershipOrgId] from organization * @param req * @param res * @returns */ export const deleteMembershipOrg = async (req: Request, res: Response) => { let membershipOrgToDelete; try { const { membershipOrgId } = req.params; // check if organization membership to delete exists membershipOrgToDelete = await MembershipOrg.findOne({ _id: membershipOrgId }).populate('user'); if (!membershipOrgToDelete) { throw new Error( "Failed to delete organization membership that doesn't exist" ); } // check if user is a member and admin of the organization // whose membership we wish to delete const membershipOrg = await MembershipOrg.findOne({ user: req.user._id, organization: membershipOrgToDelete.organization }); if (!membershipOrg) { throw new Error('Failed to validate organization membership'); } if (membershipOrg.role !== OWNER && membershipOrg.role !== ADMIN) { // user is not an admin member of the organization throw new Error('Insufficient role for deleting organization membership'); } // delete organization membership const deletedMembershipOrg = await deleteMemberFromOrg({ membershipOrgId: membershipOrgToDelete._id.toString() }); await updateSubscriptionOrgQuantity({ organizationId: membershipOrg.organization.toString() }); } catch (err) { Sentry.setUser({ email: req.user.email }); Sentry.captureException(err); return res.status(400).send({ message: 'Failed to delete organization membership' }); } return membershipOrgToDelete; }; /** * Change and return organization membership role * @param req * @param res * @returns */ export const changeMembershipOrgRole = async (req: Request, res: Response) => { // change role for (target) organization membership with id // [membershipOrgId] // TODO let membershipToChangeRole; // try { // } catch (err) { // Sentry.setUser({ email: req.user.email }); // Sentry.captureException(err); // return res.status(400).send({ // message: 'Failed to change organization membership role' // }); // } return res.status(200).send({ membershipOrg: membershipToChangeRole }); }; /** * Organization invitation step 1: Send email invitation to user with email [email] * for organization with id [organizationId] containing magic link * @param req * @param res * @returns */ export const inviteUserToOrganization = async (req: Request, res: Response) => { let invitee, inviteeMembershipOrg; try { const { organizationId, inviteeEmail } = req.body; // validate membership const membershipOrg = await MembershipOrg.findOne({ user: req.user._id, organization: organizationId }); if (!membershipOrg) { throw new Error('Failed to validate organization membership'); } invitee = await User.findOne({ email: inviteeEmail }); if (invitee) { // case: invitee is an existing user inviteeMembershipOrg = await MembershipOrg.findOne({ user: invitee._id, organization: organizationId }); if (inviteeMembershipOrg && inviteeMembershipOrg.status === ACCEPTED) { throw new Error( 'Failed to invite an existing member of the organization' ); } if (!inviteeMembershipOrg) { await new MembershipOrg({ user: invitee, inviteEmail: inviteeEmail, organization: organizationId, role: MEMBER, status: invitee?.publicKey ? ACCEPTED : INVITED }).save(); } } else { // check if invitee has been invited before inviteeMembershipOrg = await MembershipOrg.findOne({ inviteEmail: inviteeEmail, organization: organizationId }); if (!inviteeMembershipOrg) { // case: invitee has never been invited before await new MembershipOrg({ inviteEmail: inviteeEmail, organization: organizationId, role: MEMBER, status: INVITED }).save(); } } const organization = await Organization.findOne({ _id: organizationId }); if (organization) { const token = crypto.randomBytes(16).toString('hex'); await Token.findOneAndUpdate( { email: inviteeEmail }, { email: inviteeEmail, token, createdAt: new Date() }, { upsert: true, new: true } ); await sendMail({ template: 'organizationInvitation.handlebars', subjectLine: 'Infisical organization invitation', recipients: [inviteeEmail], substitutions: { inviterFirstName: req.user.firstName, inviterEmail: req.user.email, organizationName: organization.name, email: inviteeEmail, token, callback_url: SITE_URL + '/signupinvite' } }); } await updateSubscriptionOrgQuantity({ organizationId }); } catch (err) { Sentry.setUser(null); Sentry.captureException(err); return res.status(400).send({ message: 'Failed to send organization invite' }); } return res.status(200).send({ message: `Sent an invite link to ${req.body.inviteeEmail}` }); }; /** * Organization invitation step 2: Verify that code [code] was sent to email [email] as part of * magic link and issue a temporary signup token for user to complete setting up their account * @param req * @param res * @returns */ export const verifyUserToOrganization = async (req: Request, res: Response) => { let user, token; try { const { email, code } = req.body; user = await User.findOne({ email }).select('+publicKey'); const membershipOrg = await MembershipOrg.findOne({ inviteEmail: email, status: INVITED }); if (!membershipOrg) throw new Error('Failed to find any invitations for email'); await checkEmailVerification({ email, code }); if (user && user?.publicKey) { // case: user has already completed account // membership can be approved and redirected to login/dashboard membershipOrg.status = ACCEPTED; await membershipOrg.save(); return res.status(200).send({ message: 'Successfully verified email', user, }); } if (!user) { // initialize user account user = await new User({ email }).save(); } // generate temporary signup token token = createToken({ payload: { userId: user._id.toString() }, expiresIn: JWT_SIGNUP_LIFETIME, secret: JWT_SIGNUP_SECRET }); } catch (err) { Sentry.setUser(null); Sentry.captureException(err); return res.status(400).send({ error: 'Failed email magic link verification for organization invitation' }); } return res.status(200).send({ message: 'Successfully verified email', user, token }); };