mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-10-07 09:28:06 +00:00
274 lines
7.6 KiB
TypeScript
274 lines
7.6 KiB
TypeScript
import { Types } from 'mongoose';
|
|
import { Request, Response } from 'express';
|
|
import { MembershipOrg, Organization, User } from '../../models';
|
|
import { deleteMembershipOrg as deleteMemberFromOrg } from '../../helpers/membershipOrg';
|
|
import { createToken } from '../../helpers/auth';
|
|
import { updateSubscriptionOrgQuantity } from '../../helpers/organization';
|
|
import { sendMail } from '../../helpers/nodemailer';
|
|
import { TokenService } from '../../services';
|
|
import { EELicenseService } from '../../ee/services';
|
|
import { OWNER, ADMIN, MEMBER, ACCEPTED, INVITED, TOKEN_EMAIL_ORG_INVITATION } from '../../variables';
|
|
import { getSiteURL, getJwtSignupLifetime, getJwtSignupSecret, getSmtpConfigured } from '../../config';
|
|
import { validateUserEmail } from '../../validation';
|
|
|
|
/**
|
|
* Delete organization membership with id [membershipOrgId] from organization
|
|
* @param req
|
|
* @param res
|
|
* @returns
|
|
*/
|
|
export const deleteMembershipOrg = async (req: Request, res: Response) => {
|
|
const { membershipOrgId } = req.params;
|
|
|
|
// check if organization membership to delete exists
|
|
const 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()
|
|
});
|
|
|
|
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]
|
|
|
|
let membershipToChangeRole;
|
|
|
|
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, completeInviteLink;
|
|
const { organizationId, inviteeEmail } = req.body;
|
|
const host = req.headers.host;
|
|
const siteUrl = `${req.protocol}://${host}`;
|
|
|
|
// validate membership
|
|
const membershipOrg = await MembershipOrg.findOne({
|
|
user: req.user._id,
|
|
organization: organizationId
|
|
});
|
|
|
|
if (!membershipOrg) {
|
|
throw new Error('Failed to validate organization membership');
|
|
}
|
|
|
|
const plan = await EELicenseService.getOrganizationPlan(organizationId);
|
|
|
|
if (plan.memberLimit !== null) {
|
|
// case: limit imposed on number of members allowed
|
|
|
|
if (plan.membersUsed >= plan.memberLimit) {
|
|
// case: number of members used exceeds the number of members allowed
|
|
return res.status(400).send({
|
|
message: 'Failed to invite member due to member limit reached. Upgrade plan to invite more members.'
|
|
});
|
|
}
|
|
}
|
|
|
|
invitee = await User.findOne({
|
|
email: inviteeEmail
|
|
}).select('+publicKey');
|
|
|
|
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: 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
|
|
|
|
// validate that email is not disposable
|
|
validateUserEmail(inviteeEmail);
|
|
|
|
await new MembershipOrg({
|
|
inviteEmail: inviteeEmail,
|
|
organization: organizationId,
|
|
role: MEMBER,
|
|
status: INVITED
|
|
}).save();
|
|
}
|
|
}
|
|
|
|
const organization = await Organization.findOne({ _id: organizationId });
|
|
|
|
if (organization) {
|
|
|
|
const token = await TokenService.createToken({
|
|
type: TOKEN_EMAIL_ORG_INVITATION,
|
|
email: inviteeEmail,
|
|
organizationId: organization._id
|
|
});
|
|
|
|
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,
|
|
organizationId: organization._id.toString(),
|
|
token,
|
|
callback_url: (await getSiteURL()) + '/signupinvite'
|
|
}
|
|
});
|
|
|
|
if (!(await getSmtpConfigured())) {
|
|
completeInviteLink = `${siteUrl + '/signupinvite'}?token=${token}&to=${inviteeEmail}&organization_id=${organization._id}`
|
|
}
|
|
}
|
|
|
|
await updateSubscriptionOrgQuantity({ organizationId });
|
|
|
|
return res.status(200).send({
|
|
message: `Sent an invite link to ${req.body.inviteeEmail}`,
|
|
completeInviteLink
|
|
});
|
|
};
|
|
|
|
/**
|
|
* 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;
|
|
const {
|
|
email,
|
|
organizationId,
|
|
code
|
|
} = req.body;
|
|
|
|
user = await User.findOne({ email }).select('+publicKey');
|
|
|
|
const membershipOrg = await MembershipOrg.findOne({
|
|
inviteEmail: email,
|
|
status: INVITED,
|
|
organization: new Types.ObjectId(organizationId)
|
|
});
|
|
|
|
if (!membershipOrg)
|
|
throw new Error('Failed to find any invitations for email');
|
|
|
|
await TokenService.validateToken({
|
|
type: TOKEN_EMAIL_ORG_INVITATION,
|
|
email,
|
|
organizationId: membershipOrg.organization,
|
|
token: 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();
|
|
|
|
await updateSubscriptionOrgQuantity({
|
|
organizationId
|
|
});
|
|
|
|
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
|
|
const token = createToken({
|
|
payload: {
|
|
userId: user._id.toString()
|
|
},
|
|
expiresIn: await getJwtSignupLifetime(),
|
|
secret: await getJwtSignupSecret()
|
|
});
|
|
|
|
return res.status(200).send({
|
|
message: 'Successfully verified email',
|
|
user,
|
|
token
|
|
});
|
|
};
|