mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-10-02 22:25:44 +00:00
Finish MFA v1 and refactor all tokens into separate TokenService with modified collection
This commit is contained in:
+3
-1
@@ -1,7 +1,7 @@
|
|||||||
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
||||||
const { patchRouterParam } = require('./utils/patchAsyncRoutes');
|
const { patchRouterParam } = require('./utils/patchAsyncRoutes');
|
||||||
|
|
||||||
import express, { Request, Response } from 'express';
|
import express from 'express';
|
||||||
import helmet from 'helmet';
|
import helmet from 'helmet';
|
||||||
import cors from 'cors';
|
import cors from 'cors';
|
||||||
import cookieParser from 'cookie-parser';
|
import cookieParser from 'cookie-parser';
|
||||||
@@ -42,6 +42,7 @@ import {
|
|||||||
integrationAuth as v1IntegrationAuthRouter
|
integrationAuth as v1IntegrationAuthRouter
|
||||||
} from './routes/v1';
|
} from './routes/v1';
|
||||||
import {
|
import {
|
||||||
|
auth as v2AuthRouter,
|
||||||
users as v2UsersRouter,
|
users as v2UsersRouter,
|
||||||
organizations as v2OrganizationsRouter,
|
organizations as v2OrganizationsRouter,
|
||||||
workspace as v2WorkspaceRouter,
|
workspace as v2WorkspaceRouter,
|
||||||
@@ -109,6 +110,7 @@ app.use('/api/v1/integration', v1IntegrationRouter);
|
|||||||
app.use('/api/v1/integration-auth', v1IntegrationAuthRouter);
|
app.use('/api/v1/integration-auth', v1IntegrationAuthRouter);
|
||||||
|
|
||||||
// v2 routes
|
// v2 routes
|
||||||
|
app.use('/api/v2/auth', v2AuthRouter);
|
||||||
app.use('/api/v2/users', v2UsersRouter);
|
app.use('/api/v2/users', v2UsersRouter);
|
||||||
app.use('/api/v2/organizations', v2OrganizationsRouter);
|
app.use('/api/v2/organizations', v2OrganizationsRouter);
|
||||||
app.use('/api/v2/workspace', v2EnvironmentRouter);
|
app.use('/api/v2/workspace', v2EnvironmentRouter);
|
||||||
|
|||||||
@@ -1,14 +1,13 @@
|
|||||||
import { Request, Response } from 'express';
|
import { Request, Response } from 'express';
|
||||||
import * as Sentry from '@sentry/node';
|
import * as Sentry from '@sentry/node';
|
||||||
import crypto from 'crypto';
|
|
||||||
import { SITE_URL, JWT_SIGNUP_LIFETIME, JWT_SIGNUP_SECRET } from '../../config';
|
import { SITE_URL, JWT_SIGNUP_LIFETIME, JWT_SIGNUP_SECRET } from '../../config';
|
||||||
import { MembershipOrg, Organization, User, Token } from '../../models';
|
import { MembershipOrg, Organization, User } from '../../models';
|
||||||
import { deleteMembershipOrg as deleteMemberFromOrg } from '../../helpers/membershipOrg';
|
import { deleteMembershipOrg as deleteMemberFromOrg } from '../../helpers/membershipOrg';
|
||||||
import { checkEmailVerification } from '../../helpers/signup';
|
|
||||||
import { createToken } from '../../helpers/auth';
|
import { createToken } from '../../helpers/auth';
|
||||||
import { updateSubscriptionOrgQuantity } from '../../helpers/organization';
|
import { updateSubscriptionOrgQuantity } from '../../helpers/organization';
|
||||||
import { sendMail } from '../../helpers/nodemailer';
|
import { sendMail } from '../../helpers/nodemailer';
|
||||||
import { OWNER, ADMIN, MEMBER, ACCEPTED, INVITED } from '../../variables';
|
import { TokenService } from '../../services';
|
||||||
|
import { OWNER, ADMIN, MEMBER, ACCEPTED, INVITED, TOKEN_EMAIL_ORG_INVITATION } from '../../variables';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Delete organization membership with id [membershipOrgId] from organization
|
* Delete organization membership with id [membershipOrgId] from organization
|
||||||
@@ -165,17 +164,11 @@ export const inviteUserToOrganization = async (req: Request, res: Response) => {
|
|||||||
const organization = await Organization.findOne({ _id: organizationId });
|
const organization = await Organization.findOne({ _id: organizationId });
|
||||||
|
|
||||||
if (organization) {
|
if (organization) {
|
||||||
const token = crypto.randomBytes(16).toString('hex');
|
const token = await TokenService.createToken({
|
||||||
|
type: TOKEN_EMAIL_ORG_INVITATION,
|
||||||
await Token.findOneAndUpdate(
|
email: inviteeEmail,
|
||||||
{ email: inviteeEmail },
|
organizationId: organization._id
|
||||||
{
|
});
|
||||||
email: inviteeEmail,
|
|
||||||
token,
|
|
||||||
createdAt: new Date()
|
|
||||||
},
|
|
||||||
{ upsert: true, new: true }
|
|
||||||
);
|
|
||||||
|
|
||||||
await sendMail({
|
await sendMail({
|
||||||
template: 'organizationInvitation.handlebars',
|
template: 'organizationInvitation.handlebars',
|
||||||
@@ -227,10 +220,12 @@ export const verifyUserToOrganization = async (req: Request, res: Response) => {
|
|||||||
|
|
||||||
if (!membershipOrg)
|
if (!membershipOrg)
|
||||||
throw new Error('Failed to find any invitations for email');
|
throw new Error('Failed to find any invitations for email');
|
||||||
|
|
||||||
await checkEmailVerification({
|
await TokenService.validateToken({
|
||||||
|
type: TOKEN_EMAIL_ORG_INVITATION,
|
||||||
email,
|
email,
|
||||||
code
|
organizationId: membershipOrg.organization,
|
||||||
|
token: code
|
||||||
});
|
});
|
||||||
|
|
||||||
if (user && user?.publicKey) {
|
if (user && user?.publicKey) {
|
||||||
|
|||||||
@@ -1,14 +1,14 @@
|
|||||||
import { Request, Response } from 'express';
|
import { Request, Response } from 'express';
|
||||||
import * as Sentry from '@sentry/node';
|
import * as Sentry from '@sentry/node';
|
||||||
import crypto from 'crypto';
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
||||||
const jsrp = require('jsrp');
|
const jsrp = require('jsrp');
|
||||||
import * as bigintConversion from 'bigint-conversion';
|
import * as bigintConversion from 'bigint-conversion';
|
||||||
import { User, Token, BackupPrivateKey } from '../../models';
|
import { User, BackupPrivateKey } from '../../models';
|
||||||
import { checkEmailVerification } from '../../helpers/signup';
|
|
||||||
import { createToken } from '../../helpers/auth';
|
import { createToken } from '../../helpers/auth';
|
||||||
import { sendMail } from '../../helpers/nodemailer';
|
import { sendMail } from '../../helpers/nodemailer';
|
||||||
|
import { TokenService } from '../../services';
|
||||||
import { JWT_SIGNUP_LIFETIME, JWT_SIGNUP_SECRET, SITE_URL } from '../../config';
|
import { JWT_SIGNUP_LIFETIME, JWT_SIGNUP_SECRET, SITE_URL } from '../../config';
|
||||||
|
import { TOKEN_EMAIL_PASSWORD_RESET } from '../../variables';
|
||||||
|
|
||||||
const clientPublicKeys: any = {};
|
const clientPublicKeys: any = {};
|
||||||
|
|
||||||
@@ -33,17 +33,10 @@ export const emailPasswordReset = async (req: Request, res: Response) => {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const token = crypto.randomBytes(16).toString('hex');
|
const token = await TokenService.createToken({
|
||||||
|
type: TOKEN_EMAIL_PASSWORD_RESET,
|
||||||
await Token.findOneAndUpdate(
|
email
|
||||||
{ email },
|
});
|
||||||
{
|
|
||||||
email,
|
|
||||||
token,
|
|
||||||
createdAt: new Date()
|
|
||||||
},
|
|
||||||
{ upsert: true, new: true }
|
|
||||||
);
|
|
||||||
|
|
||||||
await sendMail({
|
await sendMail({
|
||||||
template: 'passwordReset.handlebars',
|
template: 'passwordReset.handlebars',
|
||||||
@@ -55,7 +48,6 @@ export const emailPasswordReset = async (req: Request, res: Response) => {
|
|||||||
callback_url: SITE_URL + '/password-reset'
|
callback_url: SITE_URL + '/password-reset'
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
Sentry.setUser(null);
|
Sentry.setUser(null);
|
||||||
Sentry.captureException(err);
|
Sentry.captureException(err);
|
||||||
@@ -88,10 +80,11 @@ export const emailPasswordResetVerify = async (req: Request, res: Response) => {
|
|||||||
error: 'Failed email verification for password reset'
|
error: 'Failed email verification for password reset'
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
await checkEmailVerification({
|
await TokenService.validateToken({
|
||||||
|
type: TOKEN_EMAIL_PASSWORD_RESET,
|
||||||
email,
|
email,
|
||||||
code
|
token: code
|
||||||
});
|
});
|
||||||
|
|
||||||
// generate temporary password-reset token
|
// generate temporary password-reset token
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { Request, Response } from 'express';
|
import { Request, Response } from 'express';
|
||||||
import * as Sentry from '@sentry/node';
|
import * as Sentry from '@sentry/node';
|
||||||
import { NODE_ENV, JWT_SIGNUP_LIFETIME, JWT_SIGNUP_SECRET } from '../../config';
|
import { JWT_SIGNUP_LIFETIME, JWT_SIGNUP_SECRET } from '../../config';
|
||||||
import { User, MembershipOrg } from '../../models';
|
import { User, MembershipOrg } from '../../models';
|
||||||
import { completeAccount } from '../../helpers/user';
|
import { completeAccount } from '../../helpers/user';
|
||||||
import {
|
import {
|
||||||
|
|||||||
@@ -0,0 +1,216 @@
|
|||||||
|
/* eslint-disable @typescript-eslint/no-var-requires */
|
||||||
|
import { Request, Response } from 'express';
|
||||||
|
import jwt from 'jsonwebtoken';
|
||||||
|
import * as Sentry from '@sentry/node';
|
||||||
|
import * as bigintConversion from 'bigint-conversion';
|
||||||
|
const jsrp = require('jsrp');
|
||||||
|
import { User } from '../../models';
|
||||||
|
import { issueTokens } from '../../helpers/auth';
|
||||||
|
import { sendMail } from '../../helpers/nodemailer';
|
||||||
|
import { TokenService } from '../../services';
|
||||||
|
import {
|
||||||
|
NODE_ENV
|
||||||
|
} from '../../config';
|
||||||
|
import {
|
||||||
|
TOKEN_EMAIL_MFA
|
||||||
|
} from '../../variables';
|
||||||
|
|
||||||
|
declare module 'jsonwebtoken' {
|
||||||
|
export interface UserIDJwtPayload extends jwt.JwtPayload {
|
||||||
|
userId: string;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const clientPublicKeys: any = {};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Log in user step 1: Return [salt] and [serverPublicKey] as part of step 1 of SRP protocol
|
||||||
|
* @param req
|
||||||
|
* @param res
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
export const login1 = async (req: Request, res: Response) => {
|
||||||
|
try {
|
||||||
|
const {
|
||||||
|
email,
|
||||||
|
clientPublicKey
|
||||||
|
}: { email: string; clientPublicKey: string } = req.body;
|
||||||
|
|
||||||
|
const user = await User.findOne({
|
||||||
|
email
|
||||||
|
}).select('+salt +verifier');
|
||||||
|
|
||||||
|
if (!user) throw new Error('Failed to find user');
|
||||||
|
|
||||||
|
const server = new jsrp.server();
|
||||||
|
server.init(
|
||||||
|
{
|
||||||
|
salt: user.salt,
|
||||||
|
verifier: user.verifier
|
||||||
|
},
|
||||||
|
() => {
|
||||||
|
// generate server-side public key
|
||||||
|
const serverPublicKey = server.getPublicKey();
|
||||||
|
clientPublicKeys[email] = {
|
||||||
|
clientPublicKey,
|
||||||
|
serverBInt: bigintConversion.bigintToBuf(server.bInt)
|
||||||
|
};
|
||||||
|
|
||||||
|
return res.status(200).send({
|
||||||
|
serverPublicKey,
|
||||||
|
salt: user.salt
|
||||||
|
});
|
||||||
|
}
|
||||||
|
);
|
||||||
|
} catch (err) {
|
||||||
|
Sentry.setUser(null);
|
||||||
|
Sentry.captureException(err);
|
||||||
|
return res.status(400).send({
|
||||||
|
message: 'Failed to start authentication process'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Log in user step 2: complete step 2 of SRP protocol and return token and their (encrypted)
|
||||||
|
* private key
|
||||||
|
* @param req
|
||||||
|
* @param res
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
export const login2 = async (req: Request, res: Response) => {
|
||||||
|
|
||||||
|
// check to see if user has MFA enabled; if yes then issue MFA-token
|
||||||
|
// TODO: may have to figure out a better token system for tokens with varying expirations
|
||||||
|
// (e.g. for org-invitations vs. auth etc.)
|
||||||
|
try {
|
||||||
|
const { email, clientProof } = req.body;
|
||||||
|
const user = await User.findOne({
|
||||||
|
email
|
||||||
|
}).select('+salt +verifier +publicKey +encryptedPrivateKey +iv +tag');
|
||||||
|
|
||||||
|
if (!user) throw new Error('Failed to find user');
|
||||||
|
|
||||||
|
const server = new jsrp.server();
|
||||||
|
server.init(
|
||||||
|
{
|
||||||
|
salt: user.salt,
|
||||||
|
verifier: user.verifier,
|
||||||
|
b: clientPublicKeys[email].serverBInt
|
||||||
|
},
|
||||||
|
async () => {
|
||||||
|
server.setClientPublicKey(clientPublicKeys[email].clientPublicKey);
|
||||||
|
|
||||||
|
// compare server and client shared keys
|
||||||
|
if (server.checkClientProof(clientProof)) {
|
||||||
|
|
||||||
|
if (user.isMfaEnabled) {
|
||||||
|
// case: user has MFA enabled
|
||||||
|
|
||||||
|
const code = await TokenService.createToken({
|
||||||
|
type: TOKEN_EMAIL_MFA,
|
||||||
|
email
|
||||||
|
});
|
||||||
|
|
||||||
|
// send MFA code [code] to [email]
|
||||||
|
await sendMail({
|
||||||
|
template: 'emailMfa.handlebars',
|
||||||
|
subjectLine: 'Infisical MFA code',
|
||||||
|
recipients: [email],
|
||||||
|
substitutions: {
|
||||||
|
code
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return res.status(200).send({
|
||||||
|
mfaEnabled: true
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// issue tokens
|
||||||
|
const tokens = await issueTokens({ userId: user._id.toString() });
|
||||||
|
|
||||||
|
// store (refresh) token in httpOnly cookie
|
||||||
|
res.cookie('jid', tokens.refreshToken, {
|
||||||
|
httpOnly: true,
|
||||||
|
path: '/',
|
||||||
|
sameSite: 'strict',
|
||||||
|
secure: NODE_ENV === 'production' ? true : false
|
||||||
|
});
|
||||||
|
|
||||||
|
// case: user does not have MFA enabled
|
||||||
|
// return (access) token in response
|
||||||
|
return res.status(200).send({
|
||||||
|
mfaEnabled: false,
|
||||||
|
token: tokens.token,
|
||||||
|
publicKey: user.publicKey,
|
||||||
|
encryptedPrivateKey: user.encryptedPrivateKey,
|
||||||
|
iv: user.iv,
|
||||||
|
tag: user.tag
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return res.status(400).send({
|
||||||
|
message: 'Failed to authenticate. Try again?'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
);
|
||||||
|
} catch (err) {
|
||||||
|
Sentry.setUser(null);
|
||||||
|
Sentry.captureException(err);
|
||||||
|
return res.status(400).send({
|
||||||
|
message: 'Failed to authenticate. Try again?'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Verify MFA token [mfaToken] and issue JWT and refresh tokens if the
|
||||||
|
* MFA token [mfaToken] is valid
|
||||||
|
* @param req
|
||||||
|
* @param res
|
||||||
|
*/
|
||||||
|
export const verifyMfaToken = async (req: Request, res: Response) => {
|
||||||
|
try {
|
||||||
|
const { email, mfaToken } = req.body;
|
||||||
|
|
||||||
|
await TokenService.validateToken({
|
||||||
|
type: TOKEN_EMAIL_MFA,
|
||||||
|
email,
|
||||||
|
token: mfaToken
|
||||||
|
});
|
||||||
|
|
||||||
|
const user = await User.findOne({
|
||||||
|
email
|
||||||
|
}).select('+salt +verifier +publicKey +encryptedPrivateKey +iv +tag');
|
||||||
|
|
||||||
|
if (!user) throw new Error('Failed to find user');
|
||||||
|
|
||||||
|
// issue tokens
|
||||||
|
const tokens = await issueTokens({ userId: user._id.toString() });
|
||||||
|
|
||||||
|
// store (refresh) token in httpOnly cookie
|
||||||
|
res.cookie('jid', tokens.refreshToken, {
|
||||||
|
httpOnly: true,
|
||||||
|
path: '/',
|
||||||
|
sameSite: 'strict',
|
||||||
|
secure: NODE_ENV === 'production' ? true : false
|
||||||
|
});
|
||||||
|
|
||||||
|
// case: user does not have MFA enabled
|
||||||
|
// return (access) token in response
|
||||||
|
return res.status(200).send({
|
||||||
|
token: tokens.token,
|
||||||
|
publicKey: user.publicKey,
|
||||||
|
encryptedPrivateKey: user.encryptedPrivateKey,
|
||||||
|
iv: user.iv,
|
||||||
|
tag: user.tag
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
Sentry.setUser(null);
|
||||||
|
Sentry.captureException(err);
|
||||||
|
return res.status(400).send({
|
||||||
|
message: 'Failed to authenticate. Try again?'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import * as authController from './authController';
|
||||||
import * as usersController from './usersController';
|
import * as usersController from './usersController';
|
||||||
import * as organizationsController from './organizationsController';
|
import * as organizationsController from './organizationsController';
|
||||||
import * as workspaceController from './workspaceController';
|
import * as workspaceController from './workspaceController';
|
||||||
@@ -8,6 +9,7 @@ import * as secretsController from './secretsController';
|
|||||||
import * as environmentController from './environmentController';
|
import * as environmentController from './environmentController';
|
||||||
|
|
||||||
export {
|
export {
|
||||||
|
authController,
|
||||||
usersController,
|
usersController,
|
||||||
organizationsController,
|
organizationsController,
|
||||||
workspaceController,
|
workspaceController,
|
||||||
|
|||||||
@@ -55,6 +55,35 @@ export const getMe = async (req: Request, res: Response) => {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update the current user's MFA-enabled status [isMfaEnabled].
|
||||||
|
* Note: Infisical currently only supports email-based 2FA only; this will expand to
|
||||||
|
* include SMS and authenticator app modes of authentication in the future.
|
||||||
|
* @param req
|
||||||
|
* @param res
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
export const updateMyMfaEnabled = async (req: Request, res: Response) => {
|
||||||
|
let user;
|
||||||
|
try {
|
||||||
|
const { isMfaEnabled }: { isMfaEnabled: boolean } = req.body;
|
||||||
|
req.user.isMfaEnabled = isMfaEnabled;
|
||||||
|
await req.user.save();
|
||||||
|
|
||||||
|
user = req.user;
|
||||||
|
} catch (err) {
|
||||||
|
Sentry.setUser({ email: req.user.email });
|
||||||
|
Sentry.captureException(err);
|
||||||
|
return res.status(400).send({
|
||||||
|
message: "Failed to update current user's MFA status"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return res.status(200).send({
|
||||||
|
user
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Return organizations that the current user is part of.
|
* Return organizations that the current user is part of.
|
||||||
* @param req
|
* @param req
|
||||||
|
|||||||
@@ -1,12 +1,13 @@
|
|||||||
import * as Sentry from '@sentry/node';
|
import * as Sentry from '@sentry/node';
|
||||||
import crypto from 'crypto';
|
import { IUser } from '../models';
|
||||||
import { Token, IToken, IUser } from '../models';
|
|
||||||
import { createOrganization } from './organization';
|
import { createOrganization } from './organization';
|
||||||
import { addMembershipsOrg } from './membershipOrg';
|
import { addMembershipsOrg } from './membershipOrg';
|
||||||
import { createWorkspace } from './workspace';
|
import { createWorkspace } from './workspace';
|
||||||
import { addMemberships } from './membership';
|
import { addMemberships } from './membership';
|
||||||
import { OWNER, ADMIN, ACCEPTED } from '../variables';
|
import { OWNER, ADMIN, ACCEPTED } from '../variables';
|
||||||
import { sendMail } from '../helpers/nodemailer';
|
import { sendMail } from '../helpers/nodemailer';
|
||||||
|
import { TokenService } from '../services';
|
||||||
|
import { TOKEN_EMAIL_CONFIRMATION } from '../variables';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Send magic link to verify email to [email]
|
* Send magic link to verify email to [email]
|
||||||
@@ -14,21 +15,13 @@ import { sendMail } from '../helpers/nodemailer';
|
|||||||
* @param {Object} obj
|
* @param {Object} obj
|
||||||
* @param {String} obj.email - email
|
* @param {String} obj.email - email
|
||||||
* @returns {Boolean} success - whether or not operation was successful
|
* @returns {Boolean} success - whether or not operation was successful
|
||||||
*
|
|
||||||
*/
|
*/
|
||||||
const sendEmailVerification = async ({ email }: { email: string }) => {
|
const sendEmailVerification = async ({ email }: { email: string }) => {
|
||||||
try {
|
try {
|
||||||
const token = String(crypto.randomInt(Math.pow(10, 5), Math.pow(10, 6) - 1));
|
const token = await TokenService.createToken({
|
||||||
|
type: TOKEN_EMAIL_CONFIRMATION,
|
||||||
await Token.findOneAndUpdate(
|
email
|
||||||
{ email },
|
});
|
||||||
{
|
|
||||||
email,
|
|
||||||
token,
|
|
||||||
createdAt: new Date()
|
|
||||||
},
|
|
||||||
{ upsert: true, new: true }
|
|
||||||
);
|
|
||||||
|
|
||||||
// send mail
|
// send mail
|
||||||
await sendMail({
|
await sendMail({
|
||||||
@@ -62,12 +55,11 @@ const checkEmailVerification = async ({
|
|||||||
code: string;
|
code: string;
|
||||||
}) => {
|
}) => {
|
||||||
try {
|
try {
|
||||||
const token = await Token.findOneAndDelete({
|
await TokenService.validateToken({
|
||||||
|
type: TOKEN_EMAIL_CONFIRMATION,
|
||||||
email,
|
email,
|
||||||
token: code
|
token: code
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!token) throw new Error('Failed to find email verification token');
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
Sentry.setUser(null);
|
Sentry.setUser(null);
|
||||||
Sentry.captureException(err);
|
Sentry.captureException(err);
|
||||||
|
|||||||
@@ -0,0 +1,162 @@
|
|||||||
|
import * as Sentry from '@sentry/node';
|
||||||
|
import { Types } from 'mongoose';
|
||||||
|
import { TokenData } from '../models';
|
||||||
|
import crypto from 'crypto';
|
||||||
|
import bcrypt from 'bcrypt';
|
||||||
|
import {
|
||||||
|
TOKEN_EMAIL_CONFIRMATION,
|
||||||
|
TOKEN_EMAIL_MFA,
|
||||||
|
TOKEN_EMAIL_ORG_INVITATION,
|
||||||
|
TOKEN_EMAIL_PASSWORD_RESET
|
||||||
|
} from '../variables';
|
||||||
|
import {
|
||||||
|
SALT_ROUNDS
|
||||||
|
} from '../config';
|
||||||
|
import { UnauthorizedRequestError } from '../utils/errors';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create and store a token in the database for purpose [type]
|
||||||
|
* @param {Object} obj
|
||||||
|
* @param {String} obj.type
|
||||||
|
* @param {String} obj.email
|
||||||
|
* @param {String} obj.phoneNumber
|
||||||
|
* @param {Types.ObjectId} obj.organizationId
|
||||||
|
* @returns {String} token - the created token
|
||||||
|
*/
|
||||||
|
const createTokenHelper = async ({
|
||||||
|
type,
|
||||||
|
email,
|
||||||
|
phoneNumber,
|
||||||
|
organizationId
|
||||||
|
}: {
|
||||||
|
type: 'emailConfirmation' | 'emailMfa' | 'organizationInvitation' | 'passwordReset';
|
||||||
|
email?: string;
|
||||||
|
phoneNumber?: string;
|
||||||
|
organizationId?: Types.ObjectId
|
||||||
|
}) => {
|
||||||
|
let token, expiresAt;
|
||||||
|
try {
|
||||||
|
// generate random token based on specified token use-case
|
||||||
|
// type [type]
|
||||||
|
switch (type) {
|
||||||
|
case TOKEN_EMAIL_CONFIRMATION:
|
||||||
|
// generate random 6-digit code
|
||||||
|
token = String(crypto.randomInt(Math.pow(10, 5), Math.pow(10, 6) - 1));
|
||||||
|
expiresAt = new Date((new Date()).getTime() + 86400000);
|
||||||
|
break;
|
||||||
|
case TOKEN_EMAIL_MFA:
|
||||||
|
// generate random 6-digit code
|
||||||
|
token = String(crypto.randomInt(Math.pow(10, 5), Math.pow(10, 6) - 1));
|
||||||
|
expiresAt = new Date((new Date()).getTime() + 300000);
|
||||||
|
break;
|
||||||
|
case TOKEN_EMAIL_ORG_INVITATION:
|
||||||
|
// generate random hex
|
||||||
|
token = crypto.randomBytes(16).toString('hex');
|
||||||
|
expiresAt = new Date((new Date()).getTime() + 259200000);
|
||||||
|
break;
|
||||||
|
case TOKEN_EMAIL_PASSWORD_RESET:
|
||||||
|
// generate random hex
|
||||||
|
token = crypto.randomBytes(16).toString('hex');
|
||||||
|
expiresAt = new Date((new Date()).getTime() + 86400000);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
token = crypto.randomBytes(16).toString('hex');
|
||||||
|
expiresAt = new Date();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Query {
|
||||||
|
type: string;
|
||||||
|
email?: string;
|
||||||
|
phoneNumber?: string;
|
||||||
|
organization?: Types.ObjectId;
|
||||||
|
}
|
||||||
|
|
||||||
|
const query: Query = { type };
|
||||||
|
|
||||||
|
if (email) { query.email = email; }
|
||||||
|
if (phoneNumber) { query.phoneNumber = phoneNumber; }
|
||||||
|
if (organizationId) { query.organization = organizationId }
|
||||||
|
|
||||||
|
await TokenData.findOneAndUpdate(
|
||||||
|
query,
|
||||||
|
{
|
||||||
|
type,
|
||||||
|
email,
|
||||||
|
phoneNumber,
|
||||||
|
organization: organizationId,
|
||||||
|
tokenHash: await bcrypt.hash(token, SALT_ROUNDS),
|
||||||
|
expiresAt
|
||||||
|
},
|
||||||
|
{
|
||||||
|
new: true,
|
||||||
|
upsert: true
|
||||||
|
}
|
||||||
|
);
|
||||||
|
} catch (err) {
|
||||||
|
Sentry.setUser(null);
|
||||||
|
Sentry.captureException(err);
|
||||||
|
throw new Error(
|
||||||
|
"Failed to create token"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return token;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @param {Object} obj
|
||||||
|
* @param {String} obj.email - email associated with the token
|
||||||
|
* @param {String} obj.token - value of the token
|
||||||
|
*/
|
||||||
|
const validateTokenHelper = async ({
|
||||||
|
type,
|
||||||
|
email,
|
||||||
|
phoneNumber,
|
||||||
|
organizationId,
|
||||||
|
token
|
||||||
|
}: {
|
||||||
|
type: 'emailConfirmation' | 'emailMfa' | 'organizationInvitation' | 'passwordReset';
|
||||||
|
email?: string;
|
||||||
|
phoneNumber?: string;
|
||||||
|
organizationId?: Types.ObjectId;
|
||||||
|
token: string;
|
||||||
|
}) => {
|
||||||
|
try {
|
||||||
|
interface Query {
|
||||||
|
type: string;
|
||||||
|
email?: string;
|
||||||
|
phoneNumber?: string;
|
||||||
|
organization?: Types.ObjectId;
|
||||||
|
}
|
||||||
|
|
||||||
|
const query: Query = { type };
|
||||||
|
|
||||||
|
if (email) { query.email = email; }
|
||||||
|
if (phoneNumber) { query.phoneNumber = phoneNumber; }
|
||||||
|
if (organizationId) { query.organization = organizationId; }
|
||||||
|
|
||||||
|
const tokenData = await TokenData.findOneAndDelete(query);
|
||||||
|
|
||||||
|
if (!tokenData) throw new Error('Failed to find token to validate');
|
||||||
|
|
||||||
|
if (tokenData.expiresAt < new Date()) throw new Error('Token has expired');
|
||||||
|
|
||||||
|
const isValid = await bcrypt.compare(token, tokenData.tokenHash);
|
||||||
|
if (!isValid) throw UnauthorizedRequestError({
|
||||||
|
message: 'Failed token data validation due to incorrect token'
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
Sentry.setUser(null);
|
||||||
|
Sentry.captureException(err);
|
||||||
|
throw new Error(
|
||||||
|
"Failed to validate token data"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export {
|
||||||
|
createTokenHelper,
|
||||||
|
validateTokenHelper
|
||||||
|
}
|
||||||
@@ -10,7 +10,7 @@ import MembershipOrg, { IMembershipOrg } from './membershipOrg';
|
|||||||
import Organization, { IOrganization } from './organization';
|
import Organization, { IOrganization } from './organization';
|
||||||
import Secret, { ISecret } from './secret';
|
import Secret, { ISecret } from './secret';
|
||||||
import ServiceToken, { IServiceToken } from './serviceToken';
|
import ServiceToken, { IServiceToken } from './serviceToken';
|
||||||
import Token, { IToken } from './token';
|
import TokenData, { ITokenData } from './tokenData';
|
||||||
import User, { IUser } from './user';
|
import User, { IUser } from './user';
|
||||||
import UserAction, { IUserAction } from './userAction';
|
import UserAction, { IUserAction } from './userAction';
|
||||||
import Workspace, { IWorkspace } from './workspace';
|
import Workspace, { IWorkspace } from './workspace';
|
||||||
@@ -42,8 +42,8 @@ export {
|
|||||||
ISecret,
|
ISecret,
|
||||||
ServiceToken,
|
ServiceToken,
|
||||||
IServiceToken,
|
IServiceToken,
|
||||||
Token,
|
TokenData,
|
||||||
IToken,
|
ITokenData,
|
||||||
User,
|
User,
|
||||||
IUser,
|
IUser,
|
||||||
UserAction,
|
UserAction,
|
||||||
|
|||||||
@@ -1,33 +0,0 @@
|
|||||||
import { Schema, model } from 'mongoose';
|
|
||||||
import { EMAIL_TOKEN_LIFETIME } from '../config';
|
|
||||||
|
|
||||||
export interface IToken {
|
|
||||||
email: string;
|
|
||||||
token: string;
|
|
||||||
createdAt: Date;
|
|
||||||
}
|
|
||||||
|
|
||||||
const tokenSchema = new Schema<IToken>({
|
|
||||||
email: {
|
|
||||||
type: String,
|
|
||||||
required: true
|
|
||||||
},
|
|
||||||
token: {
|
|
||||||
type: String,
|
|
||||||
required: true
|
|
||||||
},
|
|
||||||
createdAt: {
|
|
||||||
type: Date,
|
|
||||||
default: Date.now
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
tokenSchema.index({
|
|
||||||
createdAt: 1
|
|
||||||
}, {
|
|
||||||
expireAfterSeconds: parseInt(EMAIL_TOKEN_LIFETIME)
|
|
||||||
});
|
|
||||||
|
|
||||||
const Token = model<IToken>('Token', tokenSchema);
|
|
||||||
|
|
||||||
export default Token;
|
|
||||||
@@ -0,0 +1,57 @@
|
|||||||
|
import { Schema, Types, model } from 'mongoose';
|
||||||
|
|
||||||
|
export interface ITokenData {
|
||||||
|
type: string;
|
||||||
|
email?: string;
|
||||||
|
phoneNumber?: string;
|
||||||
|
organization?: Types.ObjectId;
|
||||||
|
tokenHash: string;
|
||||||
|
expiresAt: Date;
|
||||||
|
createdAt: Date;
|
||||||
|
updatedAt: Date;
|
||||||
|
}
|
||||||
|
|
||||||
|
const tokenDataSchema = new Schema<ITokenData>({
|
||||||
|
type: {
|
||||||
|
type: String,
|
||||||
|
enum: [
|
||||||
|
'emailConfirmation',
|
||||||
|
'emailMfa',
|
||||||
|
'organizationInvitation',
|
||||||
|
'passwordReset'
|
||||||
|
],
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
email: {
|
||||||
|
type: String
|
||||||
|
},
|
||||||
|
phoneNumber: {
|
||||||
|
type: String
|
||||||
|
},
|
||||||
|
organization: { // organizationInvitation-specific field
|
||||||
|
type: Schema.Types.ObjectId,
|
||||||
|
ref: 'Organization'
|
||||||
|
},
|
||||||
|
tokenHash: {
|
||||||
|
type: String,
|
||||||
|
select: false,
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
expiresAt: {
|
||||||
|
type: Date,
|
||||||
|
expires: 0,
|
||||||
|
required: true
|
||||||
|
}
|
||||||
|
}, {
|
||||||
|
timestamps: true
|
||||||
|
});
|
||||||
|
|
||||||
|
tokenDataSchema.index({
|
||||||
|
expiresAt: 1
|
||||||
|
}, {
|
||||||
|
expireAfterSeconds: 0
|
||||||
|
});
|
||||||
|
|
||||||
|
const TokenData = model<ITokenData>('TokenData', tokenDataSchema);
|
||||||
|
|
||||||
|
export default TokenData;
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
import { Schema, model, Types } from 'mongoose';
|
import { Schema, model, Types } from 'mongoose';
|
||||||
|
import { MFA_METHOD_EMAIL } from '../variables';
|
||||||
|
|
||||||
export interface IUser {
|
export interface IUser {
|
||||||
_id: Types.ObjectId;
|
_id: Types.ObjectId;
|
||||||
@@ -12,6 +13,7 @@ export interface IUser {
|
|||||||
salt?: string;
|
salt?: string;
|
||||||
verifier?: string;
|
verifier?: string;
|
||||||
refreshVersion?: number;
|
refreshVersion?: number;
|
||||||
|
isMfaEnabled: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
const userSchema = new Schema<IUser>(
|
const userSchema = new Schema<IUser>(
|
||||||
@@ -54,6 +56,10 @@ const userSchema = new Schema<IUser>(
|
|||||||
type: Number,
|
type: Number,
|
||||||
default: 0,
|
default: 0,
|
||||||
select: false
|
select: false
|
||||||
|
},
|
||||||
|
isMfaEnabled: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import { authLimiter } from '../../helpers/rateLimiter';
|
|||||||
|
|
||||||
router.post('/token', validateRequest, authController.getNewToken);
|
router.post('/token', validateRequest, authController.getNewToken);
|
||||||
|
|
||||||
router.post(
|
router.post( // deprecated (moved to api/v2/auth/login1)
|
||||||
'/login1',
|
'/login1',
|
||||||
authLimiter,
|
authLimiter,
|
||||||
body('email').exists().trim().notEmpty(),
|
body('email').exists().trim().notEmpty(),
|
||||||
@@ -16,7 +16,7 @@ router.post(
|
|||||||
authController.login1
|
authController.login1
|
||||||
);
|
);
|
||||||
|
|
||||||
router.post(
|
router.post( // deprecated (moved to api/v2/auth/login2)
|
||||||
'/login2',
|
'/login2',
|
||||||
authLimiter,
|
authLimiter,
|
||||||
body('email').exists().trim().notEmpty(),
|
body('email').exists().trim().notEmpty(),
|
||||||
|
|||||||
@@ -31,7 +31,7 @@ router.patch(
|
|||||||
requireBotAuth({
|
requireBotAuth({
|
||||||
acceptedRoles: [ADMIN, MEMBER]
|
acceptedRoles: [ADMIN, MEMBER]
|
||||||
}),
|
}),
|
||||||
body('isActive').isBoolean(),
|
body('isActive').exists().isBoolean(),
|
||||||
body('botKey'),
|
body('botKey'),
|
||||||
validateRequest,
|
validateRequest,
|
||||||
botController.setBotActiveState
|
botController.setBotActiveState
|
||||||
|
|||||||
@@ -0,0 +1,35 @@
|
|||||||
|
import express from 'express';
|
||||||
|
const router = express.Router();
|
||||||
|
import { body } from 'express-validator';
|
||||||
|
import { validateRequest } from '../../middleware';
|
||||||
|
import { authController } from '../../controllers/v2';
|
||||||
|
import { authLimiter } from '../../helpers/rateLimiter';
|
||||||
|
|
||||||
|
router.post(
|
||||||
|
'/login1',
|
||||||
|
authLimiter,
|
||||||
|
body('email').exists().trim().notEmpty(),
|
||||||
|
body('clientPublicKey').exists().trim().notEmpty(),
|
||||||
|
validateRequest,
|
||||||
|
authController.login1
|
||||||
|
);
|
||||||
|
|
||||||
|
router.post(
|
||||||
|
'/login2',
|
||||||
|
authLimiter,
|
||||||
|
body('email').exists().trim().notEmpty(),
|
||||||
|
body('clientProof').exists().trim().notEmpty(),
|
||||||
|
validateRequest,
|
||||||
|
authController.login2
|
||||||
|
);
|
||||||
|
|
||||||
|
router.post(
|
||||||
|
'/mfa',
|
||||||
|
authLimiter,
|
||||||
|
body('email').exists().trim().notEmpty(),
|
||||||
|
body('mfaToken').exists().trim().notEmpty(),
|
||||||
|
validateRequest,
|
||||||
|
authController.verifyMfaToken
|
||||||
|
);
|
||||||
|
|
||||||
|
export default router;
|
||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import auth from './auth';
|
||||||
import users from './users';
|
import users from './users';
|
||||||
import organizations from './organizations';
|
import organizations from './organizations';
|
||||||
import workspace from './workspace';
|
import workspace from './workspace';
|
||||||
@@ -8,6 +9,7 @@ import apiKeyData from './apiKeyData';
|
|||||||
import environment from "./environment"
|
import environment from "./environment"
|
||||||
|
|
||||||
export {
|
export {
|
||||||
|
auth,
|
||||||
users,
|
users,
|
||||||
organizations,
|
organizations,
|
||||||
workspace,
|
workspace,
|
||||||
|
|||||||
@@ -1,8 +1,10 @@
|
|||||||
import express from 'express';
|
import express from 'express';
|
||||||
const router = express.Router();
|
const router = express.Router();
|
||||||
import {
|
import {
|
||||||
requireAuth
|
requireAuth,
|
||||||
|
validateRequest
|
||||||
} from '../../middleware';
|
} from '../../middleware';
|
||||||
|
import { body, param } from 'express-validator';
|
||||||
import { usersController } from '../../controllers/v2';
|
import { usersController } from '../../controllers/v2';
|
||||||
|
|
||||||
router.get(
|
router.get(
|
||||||
@@ -13,6 +15,16 @@ router.get(
|
|||||||
usersController.getMe
|
usersController.getMe
|
||||||
);
|
);
|
||||||
|
|
||||||
|
router.patch(
|
||||||
|
'/me/mfa',
|
||||||
|
requireAuth({
|
||||||
|
acceptedAuthModes: ['jwt', 'apiKey']
|
||||||
|
}),
|
||||||
|
body('isMfaEnabled').exists().isBoolean(),
|
||||||
|
validateRequest,
|
||||||
|
usersController.updateMyMfaEnabled
|
||||||
|
);
|
||||||
|
|
||||||
router.get(
|
router.get(
|
||||||
'/me/organizations',
|
'/me/organizations',
|
||||||
requireAuth({
|
requireAuth({
|
||||||
|
|||||||
@@ -0,0 +1,69 @@
|
|||||||
|
import { Types } from 'mongoose';
|
||||||
|
import { createTokenHelper, validateTokenHelper } from '../helpers/token';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Class to handle token actions
|
||||||
|
* TODO: elaborate more on this class
|
||||||
|
*/
|
||||||
|
class TokenService {
|
||||||
|
/**
|
||||||
|
* Create a token [token] for type [type] with associated details
|
||||||
|
* @param {Object} obj
|
||||||
|
* @param {String} obj.type - type or context of token (e.g. emailConfirmation)
|
||||||
|
* @param {String} obj.email - email associated with the token
|
||||||
|
* @param {String} obj.phoneNumber - phone number associated with the token
|
||||||
|
* @param {Types.ObjectId} obj.organizationId - id of organization associated with the token
|
||||||
|
* @returns {String} token - the token to create
|
||||||
|
*/
|
||||||
|
static async createToken({
|
||||||
|
type,
|
||||||
|
email,
|
||||||
|
phoneNumber,
|
||||||
|
organizationId
|
||||||
|
}: {
|
||||||
|
type: 'emailConfirmation' | 'emailMfa' | 'organizationInvitation' | 'passwordReset';
|
||||||
|
email?: string;
|
||||||
|
phoneNumber?: string;
|
||||||
|
organizationId?: Types.ObjectId;
|
||||||
|
}) {
|
||||||
|
return await createTokenHelper({
|
||||||
|
type,
|
||||||
|
email,
|
||||||
|
phoneNumber,
|
||||||
|
organizationId
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validate whether or not token [token] and its associated details match a token in the DB
|
||||||
|
* @param {Object} obj
|
||||||
|
* @param {String} obj.type - type or context of token (e.g. emailConfirmation)
|
||||||
|
* @param {String} obj.email - email associated with the token
|
||||||
|
* @param {String} obj.phoneNumber - phone number associated with the token
|
||||||
|
* @param {Types.ObjectId} obj.organizationId - id of organization associated with the token
|
||||||
|
* @param {String} obj.token - the token to validate
|
||||||
|
*/
|
||||||
|
static async validateToken({
|
||||||
|
type,
|
||||||
|
email,
|
||||||
|
phoneNumber,
|
||||||
|
organizationId,
|
||||||
|
token
|
||||||
|
}: {
|
||||||
|
type: 'emailConfirmation' | 'emailMfa' | 'organizationInvitation' | 'passwordReset';
|
||||||
|
email?: string;
|
||||||
|
phoneNumber?: string;
|
||||||
|
organizationId?: Types.ObjectId;
|
||||||
|
token: string;
|
||||||
|
}) {
|
||||||
|
return await validateTokenHelper({
|
||||||
|
type,
|
||||||
|
email,
|
||||||
|
phoneNumber,
|
||||||
|
organizationId,
|
||||||
|
token
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default TokenService;
|
||||||
@@ -3,11 +3,13 @@ import postHogClient from './PostHogClient';
|
|||||||
import BotService from './BotService';
|
import BotService from './BotService';
|
||||||
import EventService from './EventService';
|
import EventService from './EventService';
|
||||||
import IntegrationService from './IntegrationService';
|
import IntegrationService from './IntegrationService';
|
||||||
|
import TokenService from './TokenService';
|
||||||
|
|
||||||
export {
|
export {
|
||||||
DatabaseService,
|
DatabaseService,
|
||||||
postHogClient,
|
postHogClient,
|
||||||
BotService,
|
BotService,
|
||||||
EventService,
|
EventService,
|
||||||
IntegrationService
|
IntegrationService,
|
||||||
|
TokenService
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<meta http-equiv="x-ua-compatible" content="ie=edge">
|
||||||
|
<title>MFA Code</title>
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
<h2>Infisical</h2>
|
||||||
|
<h2>Sign in attempt requires further verification</h2>
|
||||||
|
<p>Your MFA code is below — enter it where you started signing in to Infisical.</p>
|
||||||
|
<h2>{{code}}</h2>
|
||||||
|
<p>The MFA code will be valid for 2 minutes.</p>
|
||||||
|
<p>Not you? Contact Infisical or your administrator immediately.</p>
|
||||||
|
</body>
|
||||||
|
|
||||||
|
</html>
|
||||||
@@ -1,15 +1,19 @@
|
|||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html>
|
<html>
|
||||||
|
|
||||||
<head>
|
<head>
|
||||||
<meta charset="utf-8">
|
<meta charset="utf-8">
|
||||||
<meta http-equiv="x-ua-compatible" content="ie=edge">
|
<meta http-equiv="x-ua-compatible" content="ie=edge">
|
||||||
<title>Email Verification</title>
|
<title></title>
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
<body>
|
<body>
|
||||||
<h2>Infisical</h2>
|
<h2>Infisical</h2>
|
||||||
<h2>Confirm your email address</h2>
|
<h2>Confirm your email address</h2>
|
||||||
<p>Your confirmation code is below — enter it in the browser window where you've started signing up for Infisical.</p>
|
<p>Your confirmation code is below — enter it in the browser window where you've started signing up for Infisical.
|
||||||
|
</p>
|
||||||
<h2>{{code}}</h2>
|
<h2>{{code}}</h2>
|
||||||
<p>Questions about setting up Infisical? Email us at [email protected]</p>
|
<p>Questions about setting up Infisical? Email us at [email protected]</p>
|
||||||
</body>
|
</body>
|
||||||
|
|
||||||
</html>
|
</html>
|
||||||
@@ -42,6 +42,15 @@ import {
|
|||||||
} from './action';
|
} from './action';
|
||||||
import { SMTP_HOST_SENDGRID, SMTP_HOST_MAILGUN } from './smtp';
|
import { SMTP_HOST_SENDGRID, SMTP_HOST_MAILGUN } from './smtp';
|
||||||
import { PLAN_STARTER, PLAN_PRO } from './stripe';
|
import { PLAN_STARTER, PLAN_PRO } from './stripe';
|
||||||
|
import {
|
||||||
|
MFA_METHOD_EMAIL
|
||||||
|
} from './user';
|
||||||
|
import {
|
||||||
|
TOKEN_EMAIL_CONFIRMATION,
|
||||||
|
TOKEN_EMAIL_MFA,
|
||||||
|
TOKEN_EMAIL_ORG_INVITATION,
|
||||||
|
TOKEN_EMAIL_PASSWORD_RESET
|
||||||
|
} from './token';
|
||||||
|
|
||||||
export {
|
export {
|
||||||
OWNER,
|
OWNER,
|
||||||
@@ -84,4 +93,9 @@ export {
|
|||||||
SMTP_HOST_MAILGUN,
|
SMTP_HOST_MAILGUN,
|
||||||
PLAN_STARTER,
|
PLAN_STARTER,
|
||||||
PLAN_PRO,
|
PLAN_PRO,
|
||||||
|
MFA_METHOD_EMAIL,
|
||||||
|
TOKEN_EMAIL_CONFIRMATION,
|
||||||
|
TOKEN_EMAIL_MFA,
|
||||||
|
TOKEN_EMAIL_ORG_INVITATION,
|
||||||
|
TOKEN_EMAIL_PASSWORD_RESET
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -0,0 +1,11 @@
|
|||||||
|
const TOKEN_EMAIL_CONFIRMATION = 'emailConfirmation';
|
||||||
|
const TOKEN_EMAIL_MFA = 'emailMfa';
|
||||||
|
const TOKEN_EMAIL_ORG_INVITATION = 'organizationInvitation';
|
||||||
|
const TOKEN_EMAIL_PASSWORD_RESET = 'passwordReset';
|
||||||
|
|
||||||
|
export {
|
||||||
|
TOKEN_EMAIL_CONFIRMATION,
|
||||||
|
TOKEN_EMAIL_MFA,
|
||||||
|
TOKEN_EMAIL_ORG_INVITATION,
|
||||||
|
TOKEN_EMAIL_PASSWORD_RESET
|
||||||
|
}
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
const MFA_METHOD_EMAIL = 'email';
|
||||||
|
|
||||||
|
export {
|
||||||
|
MFA_METHOD_EMAIL
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user