Merge branch 'main' into checkly-integration

This commit is contained in:
BlackMagiq
2023-06-08 10:53:10 +01:00
committed by GitHub
81 changed files with 10283 additions and 6004 deletions
+867 -807
View File
File diff suppressed because it is too large Load Diff
+3 -3
View File
@@ -20,6 +20,7 @@
"crypto-js": "^4.1.1",
"dotenv": "^16.0.1",
"express": "^4.18.1",
"express-async-errors": "^3.1.1",
"express-rate-limit": "^6.7.0",
"express-validator": "^6.14.2",
"handlebars": "^4.7.7",
@@ -31,15 +32,14 @@
"libsodium-wrappers": "^0.7.10",
"lodash": "^4.17.21",
"mongoose": "^6.10.5",
"node-cache": "^5.1.2",
"nanoid": "^3.3.6",
"node-cache": "^5.1.2",
"nodemailer": "^6.8.0",
"passport": "^0.6.0",
"passport-google-oauth20": "^2.0.0",
"posthog-node": "^2.6.0",
"query-string": "^7.1.3",
"rate-limit-mongo": "^2.3.2",
"request-ip": "^3.3.0",
"rimraf": "^3.0.2",
"stripe": "^10.7.0",
"swagger-autogen": "^2.22.0",
@@ -58,7 +58,7 @@
"start": "node build/index.js",
"dev": "nodemon",
"swagger-autogen": "node ./swagger/index.ts",
"build": "rimraf ./build && tsc && cp -R ./src/templates ./build",
"build": "rimraf ./build && tsc && cp -R ./src/templates ./build && cp -R ./src/data ./build",
"lint": "eslint . --ext .ts",
"lint-and-fix": "eslint . --ext .ts --fix",
"lint-staged": "lint-staged",
+66 -49
View File
@@ -1,19 +1,28 @@
import * as Sentry from '@sentry/node';
import { Request, Response } from 'express';
import fs from 'fs';
import path from 'path';
import jwt from 'jsonwebtoken';
import * as bigintConversion from 'bigint-conversion';
// eslint-disable-next-line @typescript-eslint/no-var-requires
const jsrp = require('jsrp');
import { User, LoginSRPDetail } from '../../models';
import {
User,
LoginSRPDetail,
TokenVersion
} from '../../models';
import { createToken, issueAuthTokens, clearTokens } from '../../helpers/auth';
import { checkUserDevice } from '../../helpers/user';
import {
ACTION_LOGIN,
ACTION_LOGOUT
ACTION_LOGOUT,
AUTH_MODE_JWT
} from '../../variables';
import { BadRequestError } from '../../utils/errors';
import {
BadRequestError,
UnauthorizedRequestError
} from '../../utils/errors';
import { EELogService } from '../../ee/services';
import { getChannelFromUserAgent } from '../../utils/posthog'; // TODO: move this
import { getChannelFromUserAgent } from '../../utils/posthog';
import {
getJwtRefreshSecret,
getJwtAuthLifetime,
@@ -24,6 +33,7 @@ import {
declare module 'jsonwebtoken' {
export interface UserIDJwtPayload extends jwt.JwtPayload {
userId: string;
refreshVersion?: number;
}
}
@@ -34,7 +44,6 @@ declare module 'jsonwebtoken' {
* @returns
*/
export const login1 = async (req: Request, res: Response) => {
try {
const {
email,
clientPublicKey
@@ -68,13 +77,6 @@ export const login1 = async (req: Request, res: Response) => {
});
}
);
} catch (err) {
Sentry.setUser(null);
Sentry.captureException(err);
return res.status(400).send({
message: 'Failed to start authentication process'
});
}
};
/**
@@ -85,7 +87,6 @@ export const login1 = async (req: Request, res: Response) => {
* @returns
*/
export const login2 = async (req: Request, res: Response) => {
try {
const { email, clientProof } = req.body;
const user = await User.findOne({
email
@@ -115,11 +116,15 @@ export const login2 = async (req: Request, res: Response) => {
await checkUserDevice({
user,
ip: req.ip,
ip: req.realIP,
userAgent: req.headers['user-agent'] ?? ''
});
const tokens = await issueAuthTokens({ userId: user._id.toString() });
const tokens = await issueAuthTokens({
userId: user._id,
ip: req.realIP,
userAgent: req.headers['user-agent'] ?? ''
});
// store (refresh) token in httpOnly cookie
res.cookie('jid', tokens.refreshToken, {
@@ -138,7 +143,7 @@ export const login2 = async (req: Request, res: Response) => {
userId: user._id,
actions: [loginAction],
channel: getChannelFromUserAgent(req.headers['user-agent']),
ipAddress: req.ip
ipAddress: req.realIP
});
// return (access) token in response
@@ -156,13 +161,6 @@ export const login2 = async (req: Request, res: Response) => {
});
}
);
} catch (err) {
Sentry.setUser(null);
Sentry.captureException(err);
return res.status(400).send({
message: 'Failed to authenticate. Try again?'
});
}
};
/**
@@ -172,10 +170,9 @@ export const login2 = async (req: Request, res: Response) => {
* @returns
*/
export const logout = async (req: Request, res: Response) => {
try {
await clearTokens({
userId: req.user._id.toString()
});
if (req.authData.authMode === AUTH_MODE_JWT && req.authData.authPayload instanceof User && req.authData.tokenVersionId) {
await clearTokens(req.authData.tokenVersionId)
}
// clear httpOnly cookie
res.cookie('jid', '', {
@@ -194,22 +191,38 @@ export const logout = async (req: Request, res: Response) => {
userId: req.user._id,
actions: [logoutAction],
channel: getChannelFromUserAgent(req.headers['user-agent']),
ipAddress: req.ip
ipAddress: req.realIP
});
} catch (err) {
Sentry.setUser({ email: req.user.email });
Sentry.captureException(err);
return res.status(400).send({
message: 'Failed to logout'
});
}
return res.status(200).send({
message: 'Successfully logged out.'
});
};
export const getCommonPasswords = async (req: Request, res: Response) => {
const commonPasswords = fs.readFileSync(
path.resolve(__dirname, '../../data/' + 'common_passwords.txt'),
'utf8'
).split('\n');
return res.status(200).send(commonPasswords);
}
export const revokeAllSessions = async (req: Request, res: Response) => {
await TokenVersion.updateMany({
user: req.user._id
}, {
$inc: {
refreshVersion: 1,
accessVersion: 1
}
});
return res.status(200).send({
message: 'Successfully revoked all sessions.'
});
}
/**
* Return user is authenticated
* @param req
@@ -223,17 +236,16 @@ export const checkAuth = async (req: Request, res: Response) => {
}
/**
* Return new token by redeeming refresh token
* Return new JWT access token by first validating the refresh token
* @param req
* @param res
* @returns
*/
export const getNewToken = async (req: Request, res: Response) => {
try {
const refreshToken = req.cookies.jid;
if (!refreshToken) {
throw new Error('Failed to find token in request cookies');
throw new Error('Failed to find refresh token in request cookies');
}
const decodedToken = <jwt.UserIDJwtPayload>(
@@ -242,15 +254,27 @@ export const getNewToken = async (req: Request, res: Response) => {
const user = await User.findOne({
_id: decodedToken.userId
}).select('+publicKey');
}).select('+publicKey +refreshVersion +accessVersion');
if (!user) throw new Error('Failed to authenticate unfound user');
if (!user?.publicKey)
throw new Error('Failed to authenticate not fully set up account');
const tokenVersion = await TokenVersion.findById(decodedToken.tokenVersionId);
if (!tokenVersion) throw UnauthorizedRequestError({
message: 'Failed to validate refresh token'
});
if (decodedToken.refreshVersion !== tokenVersion.refreshVersion) throw BadRequestError({
message: 'Failed to validate refresh token'
});
const token = createToken({
payload: {
userId: decodedToken.userId
userId: decodedToken.userId,
tokenVersionId: tokenVersion._id.toString(),
accessVersion: tokenVersion.refreshVersion
},
expiresIn: await getJwtAuthLifetime(),
secret: await getJwtAuthSecret()
@@ -259,13 +283,6 @@ export const getNewToken = async (req: Request, res: Response) => {
return res.status(200).send({
token
});
} catch (err) {
Sentry.setUser(null);
Sentry.captureException(err);
return res.status(400).send({
message: 'Invalid request'
});
}
};
export const handleAuthProviderCallback = (req: Request, res: Response) => {
+2 -22
View File
@@ -1,6 +1,5 @@
import { Request, Response } from 'express';
import { Types } from 'mongoose';
import * as Sentry from '@sentry/node';
import { Bot, BotKey } from '../../models';
import { createBot } from '../../helpers/bot';
@@ -17,11 +16,9 @@ interface BotKey {
* @returns
*/
export const getBotByWorkspaceId = async (req: Request, res: Response) => {
let bot;
try {
const { workspaceId } = req.params;
bot = await Bot.findOne({
let bot = await Bot.findOne({
workspace: workspaceId
});
@@ -33,13 +30,6 @@ export const getBotByWorkspaceId = async (req: Request, res: Response) => {
workspaceId: new Types.ObjectId(workspaceId)
});
}
} catch (err) {
Sentry.setUser({ email: req.user.email });
Sentry.captureException(err);
return res.status(400).send({
message: 'Failed to get bot for workspace'
});
}
return res.status(200).send({
bot
@@ -53,8 +43,6 @@ export const getBotByWorkspaceId = async (req: Request, res: Response) => {
* @returns
*/
export const setBotActiveState = async (req: Request, res: Response) => {
let bot;
try {
const { isActive, botKey }: { isActive: boolean, botKey: BotKey } = req.body;
if (isActive) {
@@ -84,7 +72,7 @@ export const setBotActiveState = async (req: Request, res: Response) => {
});
}
bot = await Bot.findOneAndUpdate({
let bot = await Bot.findOneAndUpdate({
_id: req.bot._id
}, {
isActive
@@ -94,14 +82,6 @@ export const setBotActiveState = async (req: Request, res: Response) => {
if (!bot) throw new Error('Failed to update bot active state');
} catch (err) {
Sentry.setUser({ email: req.user.email });
Sentry.captureException(err);
return res.status(400).send({
message: 'Failed to update bot active state'
});
}
return res.status(200).send({
bot
});
@@ -1,6 +1,5 @@
import { Request, Response } from 'express';
import { Types } from 'mongoose';
import * as Sentry from '@sentry/node';
import {
IntegrationAuth,
Bot
@@ -22,21 +21,12 @@ import { standardRequest } from '../../config/request';
* Return integration authorization with id [integrationAuthId]
*/
export const getIntegrationAuth = async (req: Request, res: Response) => {
let integrationAuth;
try {
const { integrationAuthId } = req.params;
integrationAuth = await IntegrationAuth.findById(integrationAuthId);
const integrationAuth = await IntegrationAuth.findById(integrationAuthId);
if (!integrationAuth) return res.status(400).send({
message: 'Failed to find integration authorization'
});
} catch (err) {
Sentry.setUser({ email: req.user.email });
Sentry.captureException(err);
return res.status(400).send({
message: 'Failed to get integration authorization'
});
}
return res.status(200).send({
integrationAuth
@@ -61,7 +51,6 @@ export const oAuthExchange = async (
req: Request,
res: Response
) => {
try {
const { workspaceId, code, integration } = req.body;
if (!INTEGRATION_SET.has(integration))
throw new Error('Failed to validate integration');
@@ -81,13 +70,6 @@ export const oAuthExchange = async (
return res.status(200).send({
integrationAuth
});
} catch (err) {
Sentry.setUser({ email: req.user.email });
Sentry.captureException(err);
return res.status(400).send({
message: 'Failed to get OAuth2 code-token exchange'
});
}
};
/**
@@ -104,7 +86,6 @@ export const saveIntegrationAccessToken = async (
// TODO: check if access token is valid for each integration
let integrationAuth;
try {
const {
workspaceId,
accessId,
@@ -146,13 +127,6 @@ export const saveIntegrationAccessToken = async (
});
if (!integrationAuth) throw new Error('Failed to save integration access token');
} catch (err) {
Sentry.setUser({ email: req.user.email });
Sentry.captureException(err);
return res.status(400).send({
message: 'Failed to save access token for integration'
});
}
return res.status(200).send({
integrationAuth
@@ -166,22 +140,13 @@ export const saveIntegrationAccessToken = async (
* @returns
*/
export const getIntegrationAuthApps = async (req: Request, res: Response) => {
let apps;
try {
const teamId = req.query.teamId as string;
apps = await getApps({
const apps = await getApps({
integrationAuth: req.integrationAuth,
accessToken: req.accessToken,
...teamId && { teamId }
});
} catch (err) {
Sentry.setUser({ email: req.user.email });
Sentry.captureException(err);
return res.status(400).send({
message: "Failed to get integration authorization applications",
});
}
return res.status(200).send({
apps
@@ -402,19 +367,10 @@ export const getIntegrationAuthRailwayServices = async (req: Request, res: Respo
* @returns
*/
export const deleteIntegrationAuth = async (req: Request, res: Response) => {
let integrationAuth;
try {
integrationAuth = await revokeAccess({
const integrationAuth = await revokeAccess({
integrationAuth: req.integrationAuth,
accessToken: req.accessToken,
});
} catch (err) {
Sentry.setUser({ email: req.user.email });
Sentry.captureException(err);
return res.status(400).send({
message: "Failed to delete integration authorization",
});
}
return res.status(200).send({
integrationAuth,
@@ -1,6 +1,5 @@
import { Request, Response } from 'express';
import { Types } from 'mongoose';
import * as Sentry from '@sentry/node';
import {
Integration
} from '../../models';
@@ -14,9 +13,6 @@ import { eventPushSecrets } from '../../events';
* @returns
*/
export const createIntegration = async (req: Request, res: Response) => {
let integration;
try {
const {
integrationAuthId,
app,
@@ -35,7 +31,7 @@ export const createIntegration = async (req: Request, res: Response) => {
// TODO: validate [sourceEnvironment] and [targetEnvironment]
// initialize new integration after saving integration access token
integration = await new Integration({
const integration = await new Integration({
workspace: req.integrationAuth.workspace._id,
environment: sourceEnvironment,
isActive,
@@ -61,15 +57,6 @@ export const createIntegration = async (req: Request, res: Response) => {
})
});
}
} catch (err) {
Sentry.setUser({ email: req.user.email });
Sentry.captureException(err);
return res.status(400).send({
message: 'Failed to create integration'
});
}
return res.status(200).send({
integration,
});
@@ -82,12 +69,10 @@ export const createIntegration = async (req: Request, res: Response) => {
* @returns
*/
export const updateIntegration = async (req: Request, res: Response) => {
let integration;
// TODO: add integration-specific validation to ensure that each
// integration has the correct fields populated in [Integration]
try {
const {
environment,
isActive,
@@ -97,7 +82,7 @@ export const updateIntegration = async (req: Request, res: Response) => {
owner, // github-specific integration param
} = req.body;
integration = await Integration.findOneAndUpdate(
const integration = await Integration.findOneAndUpdate(
{
_id: req.integration._id,
},
@@ -123,13 +108,6 @@ export const updateIntegration = async (req: Request, res: Response) => {
}),
});
}
} catch (err) {
Sentry.setUser({ email: req.user.email });
Sentry.captureException(err);
return res.status(400).send({
message: "Failed to update integration",
});
}
return res.status(200).send({
integration,
@@ -144,22 +122,13 @@ export const updateIntegration = async (req: Request, res: Response) => {
* @returns
*/
export const deleteIntegration = async (req: Request, res: Response) => {
let integration;
try {
const { integrationId } = req.params;
integration = await Integration.findOneAndDelete({
const integration = await Integration.findOneAndDelete({
_id: integrationId,
});
if (!integration) throw new Error("Failed to find integration");
} catch (err) {
Sentry.setUser({ email: req.user.email });
Sentry.captureException(err);
return res.status(400).send({
message: "Failed to delete integration",
});
}
return res.status(200).send({
integration,
+1 -19
View File
@@ -1,5 +1,4 @@
import { Request, Response } from 'express';
import * as Sentry from '@sentry/node';
import { Key } from '../../models';
import { findMembership } from '../../helpers/membership';
@@ -11,7 +10,6 @@ import { findMembership } from '../../helpers/membership';
* @returns
*/
export const uploadKey = async (req: Request, res: Response) => {
try {
const { workspaceId } = req.params;
const { key } = req.body;
@@ -32,13 +30,6 @@ export const uploadKey = async (req: Request, res: Response) => {
receiver: key.userId,
workspace: workspaceId
}).save();
} catch (err) {
Sentry.setUser({ email: req.user.email });
Sentry.captureException(err);
return res.status(400).send({
message: 'Failed to upload key to workspace'
});
}
return res.status(200).send({
message: 'Successfully uploaded key to workspace'
@@ -52,25 +43,16 @@ export const uploadKey = async (req: Request, res: Response) => {
* @returns
*/
export const getLatestKey = async (req: Request, res: Response) => {
let latestKey;
try {
const { workspaceId } = req.params;
// get latest key
latestKey = await Key.find({
const latestKey = await Key.find({
workspace: workspaceId,
receiver: req.user._id
})
.sort({ createdAt: -1 })
.limit(1)
.populate('sender', '+publicKey');
} catch (err) {
Sentry.setUser({ email: req.user.email });
Sentry.captureException(err);
return res.status(400).send({
message: 'Failed to get latest key'
});
}
const resObj: any = {};
@@ -1,4 +1,3 @@
import * as Sentry from '@sentry/node';
import { Request, Response } from 'express';
import { Membership, MembershipOrg, User, Key } from '../../models';
import {
@@ -16,9 +15,7 @@ import { getSiteURL } from '../../config';
* @returns
*/
export const validateMembership = async (req: Request, res: Response) => {
try {
const { workspaceId } = req.params;
// validate membership
const membership = await findMembership({
user: req.user._id,
@@ -28,13 +25,6 @@ export const validateMembership = async (req: Request, res: Response) => {
if (!membership) {
throw new Error('Failed to validate membership');
}
} catch (err) {
Sentry.setUser({ email: req.user.email });
Sentry.captureException(err);
return res.status(400).send({
message: 'Failed workspace connection check'
});
}
return res.status(200).send({
message: 'Workspace membership confirmed'
@@ -48,8 +38,6 @@ export const validateMembership = async (req: Request, res: Response) => {
* @returns
*/
export const deleteMembership = async (req: Request, res: Response) => {
let deletedMembership;
try {
const { membershipId } = req.params;
// check if membership to delete exists
@@ -80,16 +68,9 @@ export const deleteMembership = async (req: Request, res: Response) => {
}
// delete workspace membership
deletedMembership = await deleteMember({
const deletedMembership = await deleteMember({
membershipId: membershipToDelete._id.toString()
});
} catch (err) {
Sentry.setUser({ email: req.user.email });
Sentry.captureException(err);
return res.status(400).send({
message: 'Failed to delete membership'
});
}
return res.status(200).send({
deletedMembership
@@ -103,8 +84,6 @@ export const deleteMembership = async (req: Request, res: Response) => {
* @returns
*/
export const changeMembershipRole = async (req: Request, res: Response) => {
let membershipToChangeRole;
try {
const { membershipId } = req.params;
const { role } = req.body;
@@ -113,7 +92,7 @@ export const changeMembershipRole = async (req: Request, res: Response) => {
}
// validate target membership
membershipToChangeRole = await findMembership({
const membershipToChangeRole = await findMembership({
_id: membershipId
});
@@ -139,13 +118,6 @@ export const changeMembershipRole = async (req: Request, res: Response) => {
membershipToChangeRole.role = role;
await membershipToChangeRole.save();
} catch (err) {
Sentry.setUser({ email: req.user.email });
Sentry.captureException(err);
return res.status(400).send({
message: 'Failed to change membership role'
});
}
return res.status(200).send({
membership: membershipToChangeRole
@@ -159,12 +131,10 @@ export const changeMembershipRole = async (req: Request, res: Response) => {
* @returns
*/
export const inviteUserToWorkspace = async (req: Request, res: Response) => {
let invitee, latestKey;
try {
const { workspaceId } = req.params;
const { email }: { email: string } = req.body;
invitee = await User.findOne({
const invitee = await User.findOne({
email
}).select('+publicKey');
@@ -193,7 +163,7 @@ export const inviteUserToWorkspace = async (req: Request, res: Response) => {
throw new Error("Failed to validate invitee's organization membership");
// get latest key
latestKey = await Key.findOne({
const latestKey = await Key.findOne({
workspace: workspaceId,
receiver: req.user._id
})
@@ -218,13 +188,6 @@ export const inviteUserToWorkspace = async (req: Request, res: Response) => {
callback_url: (await getSiteURL()) + '/login'
}
});
} catch (err) {
Sentry.setUser({ email: req.user.email });
Sentry.captureException(err);
return res.status(400).send({
message: 'Failed to invite user to workspace'
});
}
return res.status(200).send({
invitee,
@@ -1,14 +1,15 @@
import { Types } from 'mongoose';
import { Request, Response } from 'express';
import * as Sentry from '@sentry/node';
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
@@ -17,12 +18,10 @@ import { getSiteURL, getJwtSignupLifetime, getJwtSignupSecret, getSmtpConfigured
* @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({
const membershipOrgToDelete = await MembershipOrg.findOne({
_id: membershipOrgId
}).populate('user');
@@ -56,13 +55,6 @@ export const deleteMembershipOrg = async (req: Request, res: Response) => {
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;
};
@@ -78,14 +70,6 @@ export const changeMembershipOrgRole = async (req: Request, res: Response) => {
// [membershipOrgId]
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
@@ -101,7 +85,6 @@ export const changeMembershipOrgRole = async (req: Request, res: Response) => {
*/
export const inviteUserToOrganization = async (req: Request, res: Response) => {
let invitee, inviteeMembershipOrg, completeInviteLink;
try {
const { organizationId, inviteeEmail } = req.body;
const host = req.headers.host;
const siteUrl = `${req.protocol}://${host}`;
@@ -116,6 +99,19 @@ export const inviteUserToOrganization = async (req: Request, res: Response) => {
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');
@@ -154,6 +150,9 @@ export const inviteUserToOrganization = async (req: Request, res: Response) => {
if (!inviteeMembershipOrg) {
// case: invitee has never been invited before
// validate that email is not disposable
validateUserEmail(inviteeEmail);
await new MembershipOrg({
inviteEmail: inviteeEmail,
organization: organizationId,
@@ -194,13 +193,6 @@ export const inviteUserToOrganization = async (req: Request, res: Response) => {
}
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}`,
@@ -216,8 +208,7 @@ export const inviteUserToOrganization = async (req: Request, res: Response) => {
* @returns
*/
export const verifyUserToOrganization = async (req: Request, res: Response) => {
let user, token;
try {
let user;
const {
email,
organizationId,
@@ -266,20 +257,13 @@ export const verifyUserToOrganization = async (req: Request, res: Response) => {
}
// generate temporary signup token
token = createToken({
const token = createToken({
payload: {
userId: user._id.toString()
},
expiresIn: await getJwtSignupLifetime(),
secret: await getJwtSignupSecret()
});
} 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',
@@ -1,4 +1,3 @@
import * as Sentry from '@sentry/node';
import { Request, Response } from 'express';
import Stripe from 'stripe';
import {
@@ -15,21 +14,12 @@ import _ from 'lodash';
import { getStripeSecretKey, getSiteURL } from '../../config';
export const getOrganizations = async (req: Request, res: Response) => {
let organizations;
try {
organizations = (
const organizations = (
await MembershipOrg.find({
user: req.user._id,
status: ACCEPTED
}).populate('organization')
).map((m) => m.organization);
} catch (err) {
Sentry.setUser({ email: req.user.email });
Sentry.captureException(err);
return res.status(400).send({
message: 'Failed to get organizations'
});
}
return res.status(200).send({
organizations
@@ -44,8 +34,6 @@ export const getOrganizations = async (req: Request, res: Response) => {
* @returns
*/
export const createOrganization = async (req: Request, res: Response) => {
let organization;
try {
const { organizationName } = req.body;
if (organizationName.length < 1) {
@@ -53,7 +41,7 @@ export const createOrganization = async (req: Request, res: Response) => {
}
// create organization and add user as member
organization = await create({
const organization = await create({
email: req.user.email,
name: organizationName
});
@@ -64,13 +52,6 @@ export const createOrganization = async (req: Request, res: Response) => {
roles: [OWNER],
statuses: [ACCEPTED]
});
} catch (err) {
Sentry.setUser({ email: req.user.email });
Sentry.captureException(err);
return res.status(400).send({
message: 'Failed to create organization'
});
}
return res.status(200).send({
organization
@@ -84,17 +65,7 @@ export const createOrganization = async (req: Request, res: Response) => {
* @returns
*/
export const getOrganization = async (req: Request, res: Response) => {
let organization;
try {
organization = req.organization
} catch (err) {
Sentry.setUser({ email: req.user.email });
Sentry.captureException(err);
return res.status(400).send({
message: 'Failed to find organization'
});
}
const organization = req.organization
return res.status(200).send({
organization
});
@@ -107,20 +78,11 @@ export const getOrganization = async (req: Request, res: Response) => {
* @returns
*/
export const getOrganizationMembers = async (req: Request, res: Response) => {
let users;
try {
const { organizationId } = req.params;
users = await MembershipOrg.find({
const users = await MembershipOrg.find({
organization: organizationId
}).populate('user', '+publicKey');
} catch (err) {
Sentry.setUser({ email: req.user.email });
Sentry.captureException(err);
return res.status(400).send({
message: 'Failed to get organization members'
});
}
return res.status(200).send({
users
@@ -137,8 +99,6 @@ export const getOrganizationWorkspaces = async (
req: Request,
res: Response
) => {
let workspaces;
try {
const { organizationId } = req.params;
const workspacesSet = new Set(
@@ -152,20 +112,13 @@ export const getOrganizationWorkspaces = async (
).map((w) => w._id.toString())
);
workspaces = (
const workspaces = (
await Membership.find({
user: req.user._id
}).populate('workspace')
)
.filter((m) => workspacesSet.has(m.workspace._id.toString()))
.map((m) => m.workspace);
} catch (err) {
Sentry.setUser({ email: req.user.email });
Sentry.captureException(err);
return res.status(400).send({
message: 'Failed to get my workspaces'
});
}
return res.status(200).send({
workspaces
@@ -179,12 +132,10 @@ export const getOrganizationWorkspaces = async (
* @returns
*/
export const changeOrganizationName = async (req: Request, res: Response) => {
let organization;
try {
const { organizationId } = req.params;
const { name } = req.body;
organization = await Organization.findOneAndUpdate(
const organization = await Organization.findOneAndUpdate(
{
_id: organizationId
},
@@ -195,13 +146,6 @@ export const changeOrganizationName = async (req: Request, res: Response) => {
new: true
}
);
} catch (err) {
Sentry.setUser({ email: req.user.email });
Sentry.captureException(err);
return res.status(400).send({
message: 'Failed to change organization name'
});
}
return res.status(200).send({
message: 'Successfully changed organization name',
@@ -219,20 +163,11 @@ export const getOrganizationIncidentContacts = async (
req: Request,
res: Response
) => {
let incidentContactsOrg;
try {
const { organizationId } = req.params;
incidentContactsOrg = await IncidentContactOrg.find({
const incidentContactsOrg = await IncidentContactOrg.find({
organization: organizationId
});
} catch (err) {
Sentry.setUser({ email: req.user.email });
Sentry.captureException(err);
return res.status(400).send({
message: 'Failed to get organization incident contacts'
});
}
return res.status(200).send({
incidentContactsOrg
@@ -249,23 +184,14 @@ export const addOrganizationIncidentContact = async (
req: Request,
res: Response
) => {
let incidentContactOrg;
try {
const { organizationId } = req.params;
const { email } = req.body;
incidentContactOrg = await IncidentContactOrg.findOneAndUpdate(
const incidentContactOrg = await IncidentContactOrg.findOneAndUpdate(
{ email, organization: organizationId },
{ email, organization: organizationId },
{ upsert: true, new: true }
);
} catch (err) {
Sentry.setUser({ email: req.user.email });
Sentry.captureException(err);
return res.status(400).send({
message: 'Failed to add incident contact for organization'
});
}
return res.status(200).send({
incidentContactOrg
@@ -282,22 +208,13 @@ export const deleteOrganizationIncidentContact = async (
req: Request,
res: Response
) => {
let incidentContactOrg;
try {
const { organizationId } = req.params;
const { email } = req.body;
incidentContactOrg = await IncidentContactOrg.findOneAndDelete({
const incidentContactOrg = await IncidentContactOrg.findOneAndDelete({
email,
organization: organizationId
});
} catch (err) {
Sentry.setUser({ email: req.user.email });
Sentry.captureException(err);
return res.status(400).send({
message: 'Failed to delete organization incident contact'
});
}
return res.status(200).send({
message: 'Successfully deleted organization incident contact',
@@ -317,7 +234,6 @@ export const createOrganizationPortalSession = async (
res: Response
) => {
let session;
try {
const stripe = new Stripe(await getStripeSecretKey(), {
apiVersion: '2022-08-01'
});
@@ -345,13 +261,6 @@ export const createOrganizationPortalSession = async (
}
return res.status(200).send({ url: session.url });
} catch (err) {
Sentry.setUser({ email: req.user.email });
Sentry.captureException(err);
return res.status(400).send({
message: 'Failed to redirect to organization billing portal'
});
}
};
/**
@@ -364,22 +273,13 @@ export const getOrganizationSubscriptions = async (
req: Request,
res: Response
) => {
let subscriptions;
try {
const stripe = new Stripe(await getStripeSecretKey(), {
apiVersion: '2022-08-01'
});
subscriptions = await stripe.subscriptions.list({
const subscriptions = await stripe.subscriptions.list({
customer: req.organization.customerId
});
} catch (err) {
Sentry.setUser({ email: req.user.email });
Sentry.captureException(err);
return res.status(400).send({
message: 'Failed to get organization subscriptions'
});
}
return res.status(200).send({
subscriptions
@@ -1,15 +1,25 @@
import { Request, Response } from 'express';
import * as Sentry from '@sentry/node';
// eslint-disable-next-line @typescript-eslint/no-var-requires
const jsrp = require('jsrp');
import * as bigintConversion from 'bigint-conversion';
import { User, BackupPrivateKey, LoginSRPDetail } from '../../models';
import { createToken } from '../../helpers/auth';
import { sendMail } from '../../helpers/nodemailer';
import {
createToken,
sendMail,
clearTokens
} from '../../helpers';
import { TokenService } from '../../services';
import { TOKEN_EMAIL_PASSWORD_RESET } from '../../variables';
import {
TOKEN_EMAIL_PASSWORD_RESET,
AUTH_MODE_JWT
} from '../../variables';
import { BadRequestError } from '../../utils/errors';
import { getSiteURL, getJwtSignupLifetime, getJwtSignupSecret } from '../../config';
import {
getSiteURL,
getJwtSignupLifetime,
getJwtSignupSecret,
getHttpsEnabled
} from '../../config';
/**
* Password reset step 1: Send email verification link to email [email]
@@ -20,7 +30,6 @@ import { getSiteURL, getJwtSignupLifetime, getJwtSignupSecret } from '../../conf
*/
export const emailPasswordReset = async (req: Request, res: Response) => {
let email: string;
try {
email = req.body.email;
const user = await User.findOne({ email }).select('+publicKey');
@@ -28,7 +37,7 @@ export const emailPasswordReset = async (req: Request, res: Response) => {
// case: user has already completed account
return res.status(403).send({
error: 'Failed to send email verification for password reset'
message: "If an account exists with this email, a password reset link has been sent"
});
}
@@ -47,16 +56,9 @@ export const emailPasswordReset = async (req: Request, res: Response) => {
callback_url: (await getSiteURL()) + '/password-reset'
}
});
} catch (err) {
Sentry.setUser(null);
Sentry.captureException(err);
return res.status(400).send({
message: 'Failed to send email for account recovery'
});
}
return res.status(200).send({
message: `Sent an email for account recovery to ${email}`
message:"If an account exists with this email, a password reset link has been sent"
});
}
@@ -67,11 +69,9 @@ export const emailPasswordReset = async (req: Request, res: Response) => {
* @returns
*/
export const emailPasswordResetVerify = async (req: Request, res: Response) => {
let user, token;
try {
const { email, code } = req.body;
user = await User.findOne({ email }).select('+publicKey');
const user = await User.findOne({ email }).select('+publicKey');
if (!user || !user?.publicKey) {
// case: user doesn't exist with email [email] or
// hasn't even completed their account
@@ -87,20 +87,13 @@ export const emailPasswordResetVerify = async (req: Request, res: Response) => {
});
// generate temporary password-reset token
token = createToken({
const token = createToken({
payload: {
userId: user._id.toString()
},
expiresIn: await getJwtSignupLifetime(),
secret: await getJwtSignupSecret()
});
} catch (err) {
Sentry.setUser(null);
Sentry.captureException(err);
return res.status(400).send({
message: 'Failed email verification for password reset'
});
}
return res.status(200).send({
message: 'Successfully verified email',
@@ -117,7 +110,7 @@ export const emailPasswordResetVerify = async (req: Request, res: Response) => {
*/
export const srp1 = async (req: Request, res: Response) => {
// return salt, serverPublicKey as part of first step of SRP protocol
try {
const { clientPublicKey } = req.body;
const user = await User.findOne({
email: req.user.email
@@ -147,14 +140,8 @@ export const srp1 = async (req: Request, res: Response) => {
});
}
);
} catch (err) {
Sentry.setUser({ email: req.user.email });
Sentry.captureException(err);
return res.status(400).send({
error: 'Failed to start change password process'
});
}
};
/**
* Change account SRP authentication information for user
@@ -165,7 +152,6 @@ export const srp1 = async (req: Request, res: Response) => {
* @returns
*/
export const changePassword = async (req: Request, res: Response) => {
try {
const {
clientProof,
protectedKey,
@@ -222,6 +208,19 @@ export const changePassword = async (req: Request, res: Response) => {
}
);
if (req.authData.authMode === AUTH_MODE_JWT && req.authData.authPayload instanceof User && req.authData.tokenVersionId) {
await clearTokens(req.authData.tokenVersionId)
}
// clear httpOnly cookie
res.cookie('jid', '', {
httpOnly: true,
path: '/',
sameSite: 'strict',
secure: (await getHttpsEnabled()) as boolean
});
return res.status(200).send({
message: 'Successfully changed password'
});
@@ -232,13 +231,6 @@ export const changePassword = async (req: Request, res: Response) => {
});
}
);
} catch (err) {
Sentry.setUser({ email: req.user.email });
Sentry.captureException(err);
return res.status(400).send({
error: 'Failed to change password. Try again?'
});
}
};
/**
@@ -252,7 +244,6 @@ export const createBackupPrivateKey = async (req: Request, res: Response) => {
// requires verifying [clientProof] as part of second step of SRP protocol
// as initiated in /srp1
try {
const { clientProof, encryptedPrivateKey, iv, tag, salt, verifier } =
req.body;
const user = await User.findOne({
@@ -308,13 +299,6 @@ export const createBackupPrivateKey = async (req: Request, res: Response) => {
});
}
);
} catch (err) {
Sentry.setUser({ email: req.user.email });
Sentry.captureException(err);
return res.status(400).send({
message: 'Failed to update backup private key'
});
}
};
/**
@@ -324,20 +308,11 @@ export const createBackupPrivateKey = async (req: Request, res: Response) => {
* @returns
*/
export const getBackupPrivateKey = async (req: Request, res: Response) => {
let backupPrivateKey;
try {
backupPrivateKey = await BackupPrivateKey.findOne({
const backupPrivateKey = await BackupPrivateKey.findOne({
user: req.user._id
}).select('+encryptedPrivateKey +iv +tag');
if (!backupPrivateKey) throw new Error('Failed to find backup private key');
} catch (err) {
Sentry.setUser({ email: req.user.email });
Sentry.captureException(err);
return res.status(400).send({
message: 'Failed to get backup private key'
});
}
return res.status(200).send({
backupPrivateKey
@@ -345,7 +320,6 @@ export const getBackupPrivateKey = async (req: Request, res: Response) => {
}
export const resetPassword = async (req: Request, res: Response) => {
try {
const {
protectedKey,
protectedKeyIV,
@@ -374,13 +348,6 @@ export const resetPassword = async (req: Request, res: Response) => {
new: true
}
);
} catch (err) {
Sentry.setUser({ email: req.user.email });
Sentry.captureException(err);
return res.status(400).send({
message: 'Failed to get backup private key'
});
}
return res.status(200).send({
message: 'Successfully reset password'
+4 -29
View File
@@ -1,5 +1,4 @@
import { Request, Response } from 'express';
import * as Sentry from '@sentry/node';
import { Types } from 'mongoose';
import { Key, Secret } from '../../models';
import {
@@ -37,8 +36,6 @@ interface PushSecret {
*/
export const pushSecrets = async (req: Request, res: Response) => {
// upload (encrypted) secrets to workspace with id [workspaceId]
try {
const postHogClient = await TelemetryService.getPostHogClient();
let { secrets }: { secrets: PushSecret[] } = req.body;
const { keys, environment, channel } = req.body;
@@ -90,14 +87,6 @@ export const pushSecrets = async (req: Request, res: Response) => {
})
});
} catch (err) {
Sentry.setUser({ email: req.user.email });
Sentry.captureException(err);
return res.status(400).send({
message: 'Failed to upload workspace secrets'
});
}
return res.status(200).send({
message: 'Successfully uploaded workspace secrets'
});
@@ -113,7 +102,7 @@ export const pushSecrets = async (req: Request, res: Response) => {
export const pullSecrets = async (req: Request, res: Response) => {
let secrets;
let key;
try {
const postHogClient = await TelemetryService.getPostHogClient();
const environment: string = req.query.environment as string;
const channel: string = req.query.channel as string;
@@ -130,7 +119,7 @@ export const pullSecrets = async (req: Request, res: Response) => {
workspaceId,
environment,
channel: channel ? channel : 'cli',
ipAddress: req.ip
ipAddress: req.realIP
});
key = await Key.findOne({
@@ -157,13 +146,6 @@ export const pullSecrets = async (req: Request, res: Response) => {
}
});
}
} catch (err) {
Sentry.setUser({ email: req.user.email });
Sentry.captureException(err);
return res.status(400).send({
message: 'Failed to pull workspace secrets'
});
}
return res.status(200).send({
secrets,
@@ -182,7 +164,7 @@ export const pullSecrets = async (req: Request, res: Response) => {
export const pullSecretsServiceToken = async (req: Request, res: Response) => {
let secrets;
let key;
try {
const postHogClient = await TelemetryService.getPostHogClient();
const environment: string = req.query.environment as string;
const channel: string = req.query.channel as string;
@@ -199,7 +181,7 @@ export const pullSecretsServiceToken = async (req: Request, res: Response) => {
workspaceId,
environment,
channel: 'cli',
ipAddress: req.ip
ipAddress: req.realIP
});
key = {
@@ -225,13 +207,6 @@ export const pullSecretsServiceToken = async (req: Request, res: Response) => {
}
});
}
} catch (err) {
Sentry.setUser({ email: req.serviceToken.user.email });
Sentry.captureException(err);
return res.status(400).send({
message: 'Failed to pull workspace secrets'
});
}
return res.status(200).send({
secrets: reformatPullSecrets({ secrets }),
+4 -17
View File
@@ -1,5 +1,4 @@
import { Request, Response } from 'express';
import * as Sentry from '@sentry/node';
import { User } from '../../models';
import {
sendEmailVerification,
@@ -8,6 +7,7 @@ import {
import { createToken } from '../../helpers/auth';
import { BadRequestError } from '../../utils/errors';
import { getInviteOnlySignup, getJwtSignupLifetime, getJwtSignupSecret, getSmtpConfigured } from '../../config';
import { validateUserEmail } from '../../validation';
/**
* Signup step 1: Initialize account for user under email [email] and send a verification code
@@ -18,9 +18,11 @@ import { getInviteOnlySignup, getJwtSignupLifetime, getJwtSignupSecret, getSmtpC
*/
export const beginEmailSignup = async (req: Request, res: Response) => {
let email: string;
try {
email = req.body.email;
// validate that email is not disposable
validateUserEmail(email);
const user = await User.findOne({ email }).select('+publicKey');
if (user && user?.publicKey) {
// case: user has already completed account
@@ -32,13 +34,6 @@ export const beginEmailSignup = async (req: Request, res: Response) => {
// send send verification email
await sendEmailVerification({ email });
} catch (err) {
Sentry.setUser(null);
Sentry.captureException(err);
return res.status(400).send({
error: 'Failed to send email verification code'
});
}
return res.status(200).send({
message: `Sent an email verification code to ${email}`
@@ -54,7 +49,6 @@ export const beginEmailSignup = async (req: Request, res: Response) => {
*/
export const verifyEmailSignup = async (req: Request, res: Response) => {
let user, token;
try {
const { email, code } = req.body;
// initialize user account
@@ -96,13 +90,6 @@ export const verifyEmailSignup = async (req: Request, res: Response) => {
expiresIn: await getJwtSignupLifetime(),
secret: await getJwtSignupSecret()
});
} catch (err) {
Sentry.setUser(null);
Sentry.captureException(err);
return res.status(400).send({
error: 'Failed email verification'
});
}
return res.status(200).send({
message: 'Successfuly verified email',
+1 -11
View File
@@ -1,5 +1,4 @@
import { Request, Response } from 'express';
import * as Sentry from '@sentry/node';
import Stripe from 'stripe';
import { getStripeSecretKey, getStripeWebhookSecret } from '../../config';
@@ -10,26 +9,17 @@ import { getStripeSecretKey, getStripeWebhookSecret } from '../../config';
* @returns
*/
export const handleWebhook = async (req: Request, res: Response) => {
let event;
try {
// check request for valid stripe signature
const stripe = new Stripe(await getStripeSecretKey(), {
apiVersion: '2022-08-01'
});
const sig = req.headers['stripe-signature'] as string;
event = stripe.webhooks.constructEvent(
const event = stripe.webhooks.constructEvent(
req.body,
sig,
await getStripeWebhookSecret()
);
} catch (err) {
Sentry.setUser({ email: req.user.email });
Sentry.captureException(err);
return res.status(400).send({
error: 'Failed to process webhook'
});
}
switch (event.type) {
case '':
@@ -1,5 +1,4 @@
import { Request, Response } from 'express';
import * as Sentry from '@sentry/node';
import { UserAction } from '../../models';
/**
@@ -11,11 +10,9 @@ import { UserAction } from '../../models';
export const addUserAction = async (req: Request, res: Response) => {
// add/record new action [action] for user with id [req.user._id]
let userAction;
try {
const { action } = req.body;
userAction = await UserAction.findOneAndUpdate(
const userAction = await UserAction.findOneAndUpdate(
{
user: req.user._id,
action
@@ -26,13 +23,6 @@ export const addUserAction = async (req: Request, res: Response) => {
upsert: true
}
);
} catch (err) {
Sentry.setUser({ email: req.user.email });
Sentry.captureException(err);
return res.status(400).send({
message: 'Failed to record user action'
});
}
return res.status(200).send({
message: 'Successfully recorded user action',
@@ -48,21 +38,12 @@ export const addUserAction = async (req: Request, res: Response) => {
*/
export const getUserAction = async (req: Request, res: Response) => {
// get user action [action] for user with id [req.user._id]
let userAction;
try {
const action: string = req.query.action as string;
userAction = await UserAction.findOne({
const userAction = await UserAction.findOne({
user: req.user._id,
action
});
} catch (err) {
Sentry.setUser({ email: req.user.email });
Sentry.captureException(err);
return res.status(400).send({
message: 'Failed to get user action'
});
}
return res.status(200).send({
userAction
@@ -1,5 +1,4 @@
import { Request, Response } from "express";
import * as Sentry from "@sentry/node";
import {
Workspace,
Membership,
@@ -14,6 +13,7 @@ import {
createWorkspace as create,
deleteWorkspace as deleteWork,
} from "../../helpers/workspace";
import { EELicenseService } from '../../ee/services';
import { addMemberships } from "../../helpers/membership";
import { ADMIN } from "../../variables";
@@ -24,11 +24,9 @@ import { ADMIN } from "../../variables";
* @returns
*/
export const getWorkspacePublicKeys = async (req: Request, res: Response) => {
let publicKeys;
try {
const { workspaceId } = req.params;
publicKeys = (
const publicKeys = (
await Membership.find({
workspace: workspaceId,
}).populate<{ user: IUser }>("user", "publicKey")
@@ -38,13 +36,6 @@ export const getWorkspacePublicKeys = async (req: Request, res: Response) => {
userId: member.user._id,
};
});
} catch (err) {
Sentry.setUser({ email: req.user.email });
Sentry.captureException(err);
return res.status(400).send({
message: "Failed to get workspace member public keys",
});
}
return res.status(200).send({
publicKeys,
@@ -58,20 +49,11 @@ export const getWorkspacePublicKeys = async (req: Request, res: Response) => {
* @returns
*/
export const getWorkspaceMemberships = async (req: Request, res: Response) => {
let users;
try {
const { workspaceId } = req.params;
users = await Membership.find({
const users = await Membership.find({
workspace: workspaceId,
}).populate("user", "+publicKey");
} catch (err) {
Sentry.setUser({ email: req.user.email });
Sentry.captureException(err);
return res.status(400).send({
message: "Failed to get workspace members",
});
}
return res.status(200).send({
users,
@@ -85,20 +67,11 @@ export const getWorkspaceMemberships = async (req: Request, res: Response) => {
* @returns
*/
export const getWorkspaces = async (req: Request, res: Response) => {
let workspaces;
try {
workspaces = (
const workspaces = (
await Membership.find({
user: req.user._id,
}).populate("workspace")
).map((m) => m.workspace);
} catch (err) {
Sentry.setUser({ email: req.user.email });
Sentry.captureException(err);
return res.status(400).send({
message: "Failed to get workspaces",
});
}
return res.status(200).send({
workspaces,
@@ -112,20 +85,11 @@ export const getWorkspaces = async (req: Request, res: Response) => {
* @returns
*/
export const getWorkspace = async (req: Request, res: Response) => {
let workspace;
try {
const { workspaceId } = req.params;
workspace = await Workspace.findOne({
const workspace = await Workspace.findOne({
_id: workspaceId,
});
} catch (err) {
Sentry.setUser({ email: req.user.email });
Sentry.captureException(err);
return res.status(400).send({
message: "Failed to get workspace",
});
}
return res.status(200).send({
workspace,
@@ -140,8 +104,6 @@ export const getWorkspace = async (req: Request, res: Response) => {
* @returns
*/
export const createWorkspace = async (req: Request, res: Response) => {
let workspace;
try {
const { workspaceName, organizationId } = req.body;
// validate organization membership
@@ -154,12 +116,24 @@ export const createWorkspace = async (req: Request, res: Response) => {
throw new Error("Failed to validate organization membership");
}
const plan = await EELicenseService.getOrganizationPlan(organizationId);
if (plan.workspaceLimit !== null) {
// case: limit imposed on number of workspaces allowed
if (plan.workspacesUsed >= plan.workspaceLimit) {
// case: number of workspaces used exceeds the number of workspaces allowed
return res.status(400).send({
message: 'Failed to create workspace due to plan limit reached. Upgrade plan to add more workspaces.'
});
}
}
if (workspaceName.length < 1) {
throw new Error("Workspace names must be at least 1-character long");
}
// create workspace and add user as member
workspace = await create({
const workspace = await create({
name: workspaceName,
organizationId,
});
@@ -169,13 +143,6 @@ export const createWorkspace = async (req: Request, res: Response) => {
workspaceId: workspace._id.toString(),
roles: [ADMIN],
});
} catch (err) {
Sentry.setUser({ email: req.user.email });
Sentry.captureException(err);
return res.status(400).send({
message: "Failed to create workspace",
});
}
return res.status(200).send({
workspace,
@@ -189,20 +156,12 @@ export const createWorkspace = async (req: Request, res: Response) => {
* @returns
*/
export const deleteWorkspace = async (req: Request, res: Response) => {
try {
const { workspaceId } = req.params;
// delete workspace
await deleteWork({
id: workspaceId,
});
} catch (err) {
Sentry.setUser({ email: req.user.email });
Sentry.captureException(err);
return res.status(400).send({
message: "Failed to delete workspace",
});
}
return res.status(200).send({
message: "Successfully deleted workspace",
@@ -216,12 +175,10 @@ export const deleteWorkspace = async (req: Request, res: Response) => {
* @returns
*/
export const changeWorkspaceName = async (req: Request, res: Response) => {
let workspace;
try {
const { workspaceId } = req.params;
const { name } = req.body;
workspace = await Workspace.findOneAndUpdate(
const workspace = await Workspace.findOneAndUpdate(
{
_id: workspaceId,
},
@@ -232,13 +189,6 @@ export const changeWorkspaceName = async (req: Request, res: Response) => {
new: true,
}
);
} catch (err) {
Sentry.setUser({ email: req.user.email });
Sentry.captureException(err);
return res.status(400).send({
message: "Failed to change workspace name",
});
}
return res.status(200).send({
message: "Successfully changed workspace name",
@@ -253,20 +203,11 @@ export const changeWorkspaceName = async (req: Request, res: Response) => {
* @returns
*/
export const getWorkspaceIntegrations = async (req: Request, res: Response) => {
let integrations;
try {
const { workspaceId } = req.params;
integrations = await Integration.find({
const integrations = await Integration.find({
workspace: workspaceId,
});
} catch (err) {
Sentry.setUser({ email: req.user.email });
Sentry.captureException(err);
return res.status(400).send({
message: "Failed to get workspace integrations",
});
}
return res.status(200).send({
integrations,
@@ -283,20 +224,11 @@ export const getWorkspaceIntegrationAuthorizations = async (
req: Request,
res: Response
) => {
let authorizations;
try {
const { workspaceId } = req.params;
authorizations = await IntegrationAuth.find({
const authorizations = await IntegrationAuth.find({
workspace: workspaceId,
});
} catch (err) {
Sentry.setUser({ email: req.user.email });
Sentry.captureException(err);
return res.status(400).send({
message: "Failed to get workspace integration authorizations",
});
}
return res.status(200).send({
authorizations,
@@ -313,21 +245,12 @@ export const getWorkspaceServiceTokens = async (
req: Request,
res: Response
) => {
let serviceTokens;
try {
const { workspaceId } = req.params;
// ?? FIX.
serviceTokens = await ServiceToken.find({
const serviceTokens = await ServiceToken.find({
user: req.user._id,
workspace: workspaceId,
});
} catch (err) {
Sentry.setUser({ email: req.user.email });
Sentry.captureException(err);
return res.status(400).send({
message: "Failed to get workspace service tokens",
});
}
return res.status(200).send({
serviceTokens,
@@ -1,4 +1,3 @@
import * as Sentry from '@sentry/node';
import { Request, Response } from 'express';
import crypto from 'crypto';
import bcrypt from 'bcrypt';
@@ -14,18 +13,9 @@ import { getSaltRounds } from '../../config';
* @returns
*/
export const getAPIKeyData = async (req: Request, res: Response) => {
let apiKeyData;
try {
apiKeyData = await APIKeyData.find({
const apiKeyData = await APIKeyData.find({
user: req.user._id
});
} catch (err) {
Sentry.setUser({ email: req.user.email });
Sentry.captureException(err);
return res.status(400).send({
message: 'Failed to get API key data'
});
}
return res.status(200).send({
apiKeyData
@@ -38,8 +28,6 @@ export const getAPIKeyData = async (req: Request, res: Response) => {
* @param res
*/
export const createAPIKeyData = async (req: Request, res: Response) => {
let apiKey, apiKeyData;
try {
const { name, expiresIn } = req.body;
const secret = crypto.randomBytes(16).toString('hex');
@@ -48,7 +36,7 @@ export const createAPIKeyData = async (req: Request, res: Response) => {
const expiresAt = new Date();
expiresAt.setSeconds(expiresAt.getSeconds() + expiresIn);
apiKeyData = await new APIKeyData({
let apiKeyData = await new APIKeyData({
name,
lastUsed: new Date(),
expiresAt,
@@ -57,19 +45,12 @@ export const createAPIKeyData = async (req: Request, res: Response) => {
}).save();
// return api key data without sensitive data
apiKeyData = await APIKeyData.findById(apiKeyData._id);
// FIX: fix this any
apiKeyData = await APIKeyData.findById(apiKeyData._id) as any
if (!apiKeyData) throw new Error('Failed to find API key data');
apiKey = `ak.${apiKeyData._id.toString()}.${secret}`;
} catch (err) {
Sentry.setUser({ email: req.user.email });
Sentry.captureException(err);
return res.status(400).send({
message: 'Failed to API key data'
});
}
const apiKey = `ak.${apiKeyData._id.toString()}.${secret}`;
return res.status(200).send({
apiKey,
@@ -84,19 +65,8 @@ export const createAPIKeyData = async (req: Request, res: Response) => {
* @returns
*/
export const deleteAPIKeyData = async (req: Request, res: Response) => {
let apiKeyData;
try {
const { apiKeyDataId } = req.params;
apiKeyData = await APIKeyData.findByIdAndDelete(apiKeyDataId);
} catch (err) {
Sentry.setUser({ email: req.user.email });
Sentry.captureException(err);
return res.status(400).send({
message: 'Failed to delete API key data'
});
}
const apiKeyData = await APIKeyData.findByIdAndDelete(apiKeyDataId);
return res.status(200).send({
apiKeyData
+17 -35
View File
@@ -1,7 +1,6 @@
/* 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, LoginSRPDetail } from '../../models';
@@ -35,7 +34,6 @@ declare module 'jsonwebtoken' {
* @returns
*/
export const login1 = async (req: Request, res: Response) => {
try {
const {
email,
clientPublicKey
@@ -69,13 +67,7 @@ export const login1 = async (req: Request, res: Response) => {
});
}
);
} catch (err) {
Sentry.setUser(null);
Sentry.captureException(err);
return res.status(400).send({
message: 'Failed to start authentication process'
});
}
};
/**
@@ -86,14 +78,12 @@ export const login1 = async (req: Request, res: Response) => {
* @returns
*/
export const login2 = async (req: Request, res: Response) => {
try {
if (!req.headers['user-agent']) throw InternalServerError({ message: 'User-Agent header is required' });
const { email, clientProof } = req.body;
const user = await User.findOne({
email
}).select('+salt +verifier +encryptionVersion +protectedKey +protectedKeyIV +protectedKeyTag +publicKey +encryptedPrivateKey +iv +tag');
}).select('+salt +verifier +encryptionVersion +protectedKey +protectedKeyIV +protectedKeyTag +publicKey +encryptedPrivateKey +iv +tag +devices');
if (!user) throw new Error('Failed to find user');
@@ -115,7 +105,6 @@ export const login2 = async (req: Request, res: Response) => {
// compare server and client shared keys
if (server.checkClientProof(clientProof)) {
if (user.isMfaEnabled) {
// case: user has MFA enabled
@@ -151,12 +140,16 @@ export const login2 = async (req: Request, res: Response) => {
await checkUserDevice({
user,
ip: req.ip,
ip: req.realIP,
userAgent: req.headers['user-agent'] ?? ''
});
// issue tokens
const tokens = await issueAuthTokens({ userId: user._id.toString() });
const tokens = await issueAuthTokens({
userId: user._id,
ip: req.realIP,
userAgent: req.headers['user-agent'] ?? ''
});
// store (refresh) token in httpOnly cookie
res.cookie('jid', tokens.refreshToken, {
@@ -166,7 +159,7 @@ export const login2 = async (req: Request, res: Response) => {
secure: await getHttpsEnabled()
});
// case: user does not have MFA enablgged
// case: user does not have MFA enabled
// return (access) token in response
interface ResponseData {
@@ -222,13 +215,6 @@ export const login2 = async (req: Request, res: Response) => {
});
}
);
} catch (err) {
Sentry.setUser(null);
Sentry.captureException(err);
return res.status(400).send({
message: 'Failed to authenticate. Try again?'
});
}
};
/**
@@ -237,7 +223,6 @@ export const login2 = async (req: Request, res: Response) => {
* @param res
*/
export const sendMfaToken = async (req: Request, res: Response) => {
try {
const { email } = req.body;
const code = await TokenService.createToken({
@@ -254,13 +239,6 @@ export const sendMfaToken = async (req: Request, res: Response) => {
code
}
});
} catch (err) {
Sentry.setUser(null);
Sentry.captureException(err);
return res.status(400).send({
message: 'Failed to send MFA code'
});
}
return res.status(200).send({
message: 'Successfully sent new MFA code'
@@ -284,7 +262,7 @@ export const verifyMfaToken = async (req: Request, res: Response) => {
const user = await User.findOne({
email
}).select('+salt +verifier +encryptionVersion +protectedKey +protectedKeyIV +protectedKeyTag +publicKey +encryptedPrivateKey +iv +tag');
}).select('+salt +verifier +encryptionVersion +protectedKey +protectedKeyIV +protectedKeyTag +publicKey +encryptedPrivateKey +iv +tag +devices');
if (!user) throw new Error('Failed to find user');
@@ -292,12 +270,16 @@ export const verifyMfaToken = async (req: Request, res: Response) => {
await checkUserDevice({
user,
ip: req.ip,
ip: req.realIP,
userAgent: req.headers['user-agent'] ?? ''
});
// issue tokens
const tokens = await issueAuthTokens({ userId: user._id.toString() });
const tokens = await issueAuthTokens({
userId: user._id,
ip: req.realIP,
userAgent: req.headers['user-agent'] ?? ''
});
// store (refresh) token in httpOnly cookie
res.cookie('jid', tokens.refreshToken, {
@@ -355,7 +337,7 @@ export const verifyMfaToken = async (req: Request, res: Response) => {
userId: user._id,
actions: [loginAction],
channel: getChannelFromUserAgent(req.headers['user-agent']),
ipAddress: req.ip
ipAddress: req.realIP
});
return res.status(200).send(resObj);
@@ -1,5 +1,4 @@
import { Request, Response } from 'express';
import * as Sentry from '@sentry/node';
import {
Secret,
ServiceToken,
@@ -25,7 +24,6 @@ export const createWorkspaceEnvironment = async (
) => {
const { workspaceId } = req.params;
const { environmentName, environmentSlug } = req.body;
try {
const workspace = await Workspace.findById(workspaceId).exec();
if (
!workspace ||
@@ -41,13 +39,6 @@ export const createWorkspaceEnvironment = async (
slug: environmentSlug.toLowerCase(),
});
await workspace.save();
} catch (err) {
Sentry.setUser({ email: req.user.email });
Sentry.captureException(err);
return res.status(400).send({
message: 'Failed to create new workspace environment',
});
}
return res.status(200).send({
message: 'Successfully created new environment',
@@ -72,7 +63,6 @@ export const renameWorkspaceEnvironment = async (
) => {
const { workspaceId } = req.params;
const { environmentName, environmentSlug, oldEnvironmentSlug } = req.body;
try {
// user should pass both new slug and env name
if (!environmentSlug || !environmentName) {
throw new Error('Invalid environment given.');
@@ -133,13 +123,6 @@ export const renameWorkspaceEnvironment = async (
{ arrayFilters: [{ "element.environmentSlug": oldEnvironmentSlug }] }
)
} catch (err) {
Sentry.setUser({ email: req.user.email });
Sentry.captureException(err);
return res.status(400).send({
message: 'Failed to update workspace environment',
});
}
return res.status(200).send({
message: 'Successfully update environment',
@@ -163,7 +146,6 @@ export const deleteWorkspaceEnvironment = async (
) => {
const { workspaceId } = req.params;
const { environmentSlug } = req.body;
try {
// atomic update the env to avoid conflict
const workspace = await Workspace.findById(workspaceId).exec();
if (!workspace) {
@@ -206,14 +188,6 @@ export const deleteWorkspaceEnvironment = async (
{ $pull: { deniedPermissions: { environmentSlug: environmentSlug } } }
)
} catch (err) {
Sentry.setUser({ email: req.user.email });
Sentry.captureException(err);
return res.status(400).send({
message: 'Failed to delete workspace environment',
});
}
return res.status(200).send({
message: 'Successfully deleted environment',
workspace: workspaceId,
@@ -1,5 +1,4 @@
import { Request, Response } from 'express';
import * as Sentry from '@sentry/node';
import { Types } from 'mongoose';
import {
MembershipOrg,
@@ -49,20 +48,11 @@ export const getOrganizationMemberships = async (req: Request, res: Response) =>
}
}
*/
let memberships;
try {
const { organizationId } = req.params;
memberships = await MembershipOrg.find({
const memberships = await MembershipOrg.find({
organization: organizationId
}).populate('user', '+publicKey');
} catch (err) {
Sentry.setUser({ email: req.user.email });
Sentry.captureException(err);
return res.status(400).send({
message: 'Failed to get organization memberships'
});
}
return res.status(200).send({
memberships
@@ -128,12 +118,10 @@ export const updateOrganizationMembership = async (req: Request, res: Response)
}
}
*/
let membership;
try {
const { membershipId } = req.params;
const { role } = req.body;
membership = await MembershipOrg.findByIdAndUpdate(
const membership = await MembershipOrg.findByIdAndUpdate(
membershipId,
{
role
@@ -141,13 +129,6 @@ export const updateOrganizationMembership = async (req: Request, res: Response)
new: true
}
);
} catch (err) {
Sentry.setUser({ email: req.user.email });
Sentry.captureException(err);
return res.status(400).send({
message: 'Failed to update organization membership'
});
}
return res.status(200).send({
membership
@@ -197,25 +178,16 @@ export const deleteOrganizationMembership = async (req: Request, res: Response)
}
}
*/
let membership;
try {
const { membershipId } = req.params;
// delete organization membership
membership = await deleteMembershipOrg({
const membership = await deleteMembershipOrg({
membershipOrgId: membershipId
});
await updateSubscriptionOrgQuantity({
organizationId: membership.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 res.status(200).send({
membership
@@ -297,7 +297,7 @@ export const batchSecrets = async (req: Request, res: Response) => {
workspaceId: new Types.ObjectId(workspaceId),
actions,
channel,
ipAddress: req.ip,
ipAddress: req.realIP,
});
}
@@ -563,7 +563,7 @@ export const createSecrets = async (req: Request, res: Response) => {
workspaceId: new Types.ObjectId(workspaceId),
actions: [addAction],
channel,
ipAddress: req.ip,
ipAddress: req.realIP,
}));
// (EE) take a secret snapshot
@@ -785,7 +785,7 @@ export const getSecrets = async (req: Request, res: Response) => {
workspaceId: new Types.ObjectId(workspaceId as string),
actions: [readAction],
channel,
ipAddress: req.ip,
ipAddress: req.realIP,
}));
const postHogClient = await TelemetryService.getPostHogClient();
@@ -1020,7 +1020,7 @@ export const updateSecrets = async (req: Request, res: Response) => {
workspaceId: new Types.ObjectId(key),
actions: [updateAction],
channel,
ipAddress: req.ip,
ipAddress: req.realIP,
}));
// (EE) take a secret snapshot
@@ -1158,7 +1158,7 @@ export const deleteSecrets = async (req: Request, res: Response) => {
workspaceId: new Types.ObjectId(key),
actions: [deleteAction],
channel,
ipAddress: req.ip,
ipAddress: req.realIP,
}));
// (EE) take a secret snapshot
@@ -1,4 +1,3 @@
import * as Sentry from '@sentry/node';
import { Request, Response } from 'express';
import crypto from 'crypto';
import bcrypt from 'bcrypt';
+6 -19
View File
@@ -1,5 +1,4 @@
import { Request, Response } from 'express';
import * as Sentry from '@sentry/node';
import { User, MembershipOrg } from '../../models';
import { completeAccount } from '../../helpers/user';
import {
@@ -20,7 +19,6 @@ import { updateSubscriptionOrgQuantity } from '../../helpers/organization';
*/
export const completeAccountSignup = async (req: Request, res: Response) => {
let user, token, refreshToken;
try {
const {
email,
firstName,
@@ -116,7 +114,9 @@ export const completeAccountSignup = async (req: Request, res: Response) => {
// issue tokens
const tokens = await issueAuthTokens({
userId: user._id.toString()
userId: user._id,
ip: req.realIP,
userAgent: req.headers['user-agent'] ?? ''
});
token = tokens.token;
@@ -143,13 +143,6 @@ export const completeAccountSignup = async (req: Request, res: Response) => {
sameSite: 'strict',
secure: await getHttpsEnabled()
});
} catch (err) {
Sentry.setUser(null);
Sentry.captureException(err);
return res.status(400).send({
message: 'Failed to complete account setup'
});
}
return res.status(200).send({
message: 'Successfully set up account',
@@ -167,7 +160,6 @@ export const completeAccountSignup = async (req: Request, res: Response) => {
*/
export const completeAccountInvite = async (req: Request, res: Response) => {
let user, token, refreshToken;
try {
const {
email,
firstName,
@@ -247,7 +239,9 @@ export const completeAccountInvite = async (req: Request, res: Response) => {
// issue tokens
const tokens = await issueAuthTokens({
userId: user._id.toString()
userId: user._id,
ip: req.realIP,
userAgent: req.headers['user-agent'] ?? ''
});
token = tokens.token;
@@ -259,13 +253,6 @@ export const completeAccountInvite = async (req: Request, res: Response) => {
sameSite: 'strict',
secure: await getHttpsEnabled()
});
} catch (err) {
Sentry.setUser(null);
Sentry.captureException(err);
return res.status(400).send({
message: 'Failed to complete account setup'
});
}
return res.status(200).send({
message: 'Successfully set up account',
@@ -1,5 +1,4 @@
import { Request, Response } from 'express';
import * as Sentry from '@sentry/node';
import { Types } from 'mongoose';
import {
Membership, Secret,
+3 -31
View File
@@ -1,5 +1,4 @@
import { Request, Response } from 'express';
import * as Sentry from '@sentry/node';
import {
User,
MembershipOrg
@@ -37,18 +36,9 @@ export const getMe = async (req: Request, res: Response) => {
}
}
*/
let user;
try {
user = await User
const user = await User
.findById(req.user._id)
.select('+salt +publicKey +encryptedPrivateKey +iv +tag +encryptionVersion +protectedKey +protectedKeyIV +protectedKeyTag');
} catch (err) {
Sentry.setUser({ email: req.user.email });
Sentry.captureException(err);
return res.status(400).send({
message: 'Failed to get current user'
});
}
return res.status(200).send({
user
@@ -64,8 +54,6 @@ export const getMe = async (req: Request, res: Response) => {
* @returns
*/
export const updateMyMfaEnabled = async (req: Request, res: Response) => {
let user;
try {
const { isMfaEnabled }: { isMfaEnabled: boolean } = req.body;
req.user.isMfaEnabled = isMfaEnabled;
@@ -79,14 +67,7 @@ export const updateMyMfaEnabled = async (req: Request, res: Response) => {
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"
});
}
const user = req.user;
return res.status(200).send({
user
@@ -126,20 +107,11 @@ export const getMyOrganizations = async (req: Request, res: Response) => {
}
}
*/
let organizations;
try {
organizations = (
const organizations = (
await MembershipOrg.find({
user: req.user._id
}).populate('organization')
).map((m) => m.organization);
} catch (err) {
Sentry.setUser({ email: req.user.email });
Sentry.captureException(err);
return res.status(400).send({
message: "Failed to get current user's organizations"
});
}
return res.status(200).send({
organizations
@@ -1,5 +1,4 @@
import { Request, Response } from 'express';
import * as Sentry from '@sentry/node';
import { Types } from 'mongoose';
import {
Workspace,
@@ -47,7 +46,6 @@ interface V2PushSecret {
*/
export const pushWorkspaceSecrets = async (req: Request, res: Response) => {
// upload (encrypted) secrets to workspace with id [workspaceId]
try {
const postHogClient = await TelemetryService.getPostHogClient();
let { secrets }: { secrets: V2PushSecret[] } = req.body;
const { keys, environment, channel } = req.body;
@@ -70,7 +68,7 @@ export const pushWorkspaceSecrets = async (req: Request, res: Response) => {
environment,
secrets,
channel: channel ? channel : 'cli',
ipAddress: req.ip
ipAddress: req.realIP
});
await pushKeys({
@@ -100,14 +98,6 @@ export const pushWorkspaceSecrets = async (req: Request, res: Response) => {
})
});
} catch (err) {
Sentry.setUser({ email: req.user.email });
Sentry.captureException(err);
return res.status(400).send({
message: 'Failed to upload workspace secrets'
});
}
return res.status(200).send({
message: 'Successfully uploaded workspace secrets'
});
@@ -122,7 +112,6 @@ export const pushWorkspaceSecrets = async (req: Request, res: Response) => {
*/
export const pullSecrets = async (req: Request, res: Response) => {
let secrets;
try {
const postHogClient = await TelemetryService.getPostHogClient();
const environment: string = req.query.environment as string;
const channel: string = req.query.channel as string;
@@ -145,7 +134,7 @@ export const pullSecrets = async (req: Request, res: Response) => {
workspaceId,
environment,
channel: channel ? channel : 'cli',
ipAddress: req.ip
ipAddress: req.realIP
});
if (channel !== 'cli') {
@@ -165,13 +154,6 @@ export const pullSecrets = async (req: Request, res: Response) => {
}
});
}
} catch (err) {
Sentry.setUser({ email: req.user.email });
Sentry.captureException(err);
return res.status(400).send({
message: 'Failed to pull workspace secrets'
});
}
return res.status(200).send({
secrets
@@ -208,7 +190,6 @@ export const getWorkspaceKey = async (req: Request, res: Response) => {
}
*/
let key;
try {
const { workspaceId } = req.params;
key = await Key.findOne({
@@ -217,13 +198,6 @@ export const getWorkspaceKey = async (req: Request, res: Response) => {
}).populate('sender', '+publicKey');
if (!key) throw new Error('Failed to find workspace key');
} catch (err) {
Sentry.setUser({ email: req.user.email });
Sentry.captureException(err);
return res.status(400).send({
message: 'Failed to get workspace key'
});
}
return res.status(200).json(key);
}
@@ -231,24 +205,14 @@ export const getWorkspaceServiceTokenData = async (
req: Request,
res: Response
) => {
let serviceTokenData;
try {
const { workspaceId } = req.params;
serviceTokenData = await ServiceTokenData
const serviceTokenData = await ServiceTokenData
.find({
workspace: workspaceId
})
.select('+encryptedKey +iv +tag');
} catch (err) {
Sentry.setUser({ email: req.user.email });
Sentry.captureException(err);
return res.status(400).send({
message: 'Failed to get workspace service token data'
});
}
return res.status(200).send({
serviceTokenData
});
@@ -294,20 +258,11 @@ export const getWorkspaceMemberships = async (req: Request, res: Response) => {
}
}
*/
let memberships;
try {
const { workspaceId } = req.params;
memberships = await Membership.find({
const memberships = await Membership.find({
workspace: workspaceId
}).populate('user', '+publicKey');
} catch (err) {
Sentry.setUser({ email: req.user.email });
Sentry.captureException(err);
return res.status(400).send({
message: 'Failed to get workspace memberships'
});
}
return res.status(200).send({
memberships
@@ -374,14 +329,12 @@ export const updateWorkspaceMembership = async (req: Request, res: Response) =>
}
}
*/
let membership;
try {
const {
membershipId
} = req.params;
const { role } = req.body;
membership = await Membership.findByIdAndUpdate(
const membership = await Membership.findByIdAndUpdate(
membershipId,
{
role
@@ -389,13 +342,6 @@ export const updateWorkspaceMembership = async (req: Request, res: Response) =>
new: true
}
);
} catch (err) {
Sentry.setUser({ email: req.user.email });
Sentry.captureException(err);
return res.status(400).send({
message: 'Failed to update workspace membership'
});
}
return res.status(200).send({
membership
@@ -445,13 +391,11 @@ export const deleteWorkspaceMembership = async (req: Request, res: Response) =>
}
}
*/
let membership;
try {
const {
membershipId
} = req.params;
membership = await Membership.findByIdAndDelete(membershipId);
const membership = await Membership.findByIdAndDelete(membershipId);
if (!membership) throw new Error('Failed to delete workspace membership');
@@ -459,13 +403,6 @@ export const deleteWorkspaceMembership = async (req: Request, res: Response) =>
receiver: membership.user,
workspace: membership.workspace
});
} catch (err) {
Sentry.setUser({ email: req.user.email });
Sentry.captureException(err);
return res.status(400).send({
message: 'Failed to delete workspace membership'
});
}
return res.status(200).send({
membership
@@ -479,12 +416,10 @@ export const deleteWorkspaceMembership = async (req: Request, res: Response) =>
* @returns
*/
export const toggleAutoCapitalization = async (req: Request, res: Response) => {
let workspace;
try {
const { workspaceId } = req.params;
const { autoCapitalization } = req.body;
workspace = await Workspace.findOneAndUpdate(
const workspace = await Workspace.findOneAndUpdate(
{
_id: workspaceId
},
@@ -495,13 +430,6 @@ export const toggleAutoCapitalization = async (req: Request, res: Response) => {
new: true
}
);
} catch (err) {
Sentry.setUser({ email: req.user.email });
Sentry.captureException(err);
return res.status(400).send({
message: 'Failed to change autoCapitalization setting'
});
}
return res.status(200).send({
message: 'Successfully changed autoCapitalization setting',
+8 -4
View File
@@ -113,7 +113,7 @@ export const login2 = async (req: Request, res: Response) => {
const user = await User.findOne({
email,
}).select('+salt +verifier +encryptionVersion +protectedKey +protectedKeyIV +protectedKeyTag +publicKey +encryptedPrivateKey +iv +tag');
}).select('+salt +verifier +encryptionVersion +protectedKey +protectedKeyIV +protectedKeyTag +publicKey +encryptedPrivateKey +iv +tag +devices');
if (!user) throw new Error('Failed to find user');
@@ -179,12 +179,16 @@ export const login2 = async (req: Request, res: Response) => {
await checkUserDevice({
user,
ip: req.ip,
ip: req.realIP,
userAgent: req.headers['user-agent'] ?? ''
});
// issue tokens
const tokens = await issueAuthTokens({ userId: user._id.toString() });
const tokens = await issueAuthTokens({
userId: user._id,
ip: req.realIP,
userAgent: req.headers['user-agent'] ?? ''
});
// store (refresh) token in httpOnly cookie
res.cookie('jid', tokens.refreshToken, {
@@ -239,7 +243,7 @@ export const login2 = async (req: Request, res: Response) => {
userId: user._id,
actions: [loginAction],
channel: getChannelFromUserAgent(req.headers['user-agent']),
ipAddress: req.ip
ipAddress: req.realIP
});
return res.status(200).send(response);
@@ -137,7 +137,9 @@ export const completeAccountSignup = async (req: Request, res: Response) => {
// issue tokens
const tokens = await issueAuthTokens({
userId: user._id.toString()
userId: user._id,
ip: req.realIP,
userAgent: req.headers['user-agent'] ?? ''
});
token = tokens.token;
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
@@ -1,5 +1,4 @@
import { Request, Response } from 'express';
import * as Sentry from '@sentry/node';
import { Action, SecretVersion } from '../../models';
import { ActionNotFoundError } from '../../../utils/errors';
@@ -1,4 +1,3 @@
import * as Sentry from '@sentry/node';
import { Request, Response } from 'express';
import { EELicenseService } from '../../services';
import { getLicenseServerUrl } from '../../../config';
@@ -12,7 +11,6 @@ import { licenseServerKeyRequest } from '../../../config/request';
* @returns
*/
export const getCloudProducts = async (req: Request, res: Response) => {
try {
const billingCycle = req.query['billing-cycle'] as string;
if (EELicenseService.instanceType === 'cloud') {
@@ -22,10 +20,6 @@ export const getCloudProducts = async (req: Request, res: Response) => {
return res.status(200).send(data);
}
} catch (err) {
Sentry.setUser({ email: req.user.email });
Sentry.captureException(err);
}
return res.status(200).send({
head: [],
@@ -1,5 +1,4 @@
import { Request, Response } from "express";
import * as Sentry from "@sentry/node";
import { Secret } from "../../../models";
import { SecretVersion } from "../../models";
import { EESecretService } from "../../services";
@@ -55,14 +54,12 @@ export const getSecretVersions = async (req: Request, res: Response) => {
}
}
*/
let secretVersions;
try {
const { secretId, workspaceId, environment, folderId } = req.params;
const offset: number = parseInt(req.query.offset as string);
const limit: number = parseInt(req.query.limit as string);
secretVersions = await SecretVersion.find({
const secretVersions = await SecretVersion.find({
secret: secretId,
workspace: workspaceId,
environment,
@@ -71,13 +68,6 @@ export const getSecretVersions = async (req: Request, res: Response) => {
.sort({ createdAt: -1 })
.skip(offset)
.limit(limit);
} catch (err) {
Sentry.setUser({ email: req.user.email });
Sentry.captureException(err);
return res.status(400).send({
message: "Failed to get secret versions",
});
}
return res.status(200).send({
secretVersions,
@@ -139,8 +129,6 @@ export const rollbackSecretVersion = async (req: Request, res: Response) => {
}
}
*/
let secret;
try {
const { secretId } = req.params;
const { version } = req.body;
@@ -164,13 +152,13 @@ export const rollbackSecretVersion = async (req: Request, res: Response) => {
secretValueCiphertext,
secretValueIV,
secretValueTag,
folder,
algorithm,
folder,
keyEncoding,
} = oldSecretVersion;
// update secret
secret = await Secret.findByIdAndUpdate(
const secret = await Secret.findByIdAndUpdate(
secretId,
{
$inc: {
@@ -225,13 +213,6 @@ export const rollbackSecretVersion = async (req: Request, res: Response) => {
environment,
folderId: folder,
});
} catch (err) {
Sentry.setUser({ email: req.user.email });
Sentry.captureException(err);
return res.status(400).send({
message: "Failed to roll back secret version",
});
}
return res.status(200).send({
secret,
@@ -1,5 +1,4 @@
import { Request, Response } from "express";
import * as Sentry from "@sentry/node";
import {
ISecretVersion,
SecretSnapshot,
@@ -13,11 +12,9 @@ import {
* @returns
*/
export const getSecretSnapshot = async (req: Request, res: Response) => {
let secretSnapshot;
try {
const { secretSnapshotId } = req.params;
secretSnapshot = await SecretSnapshot.findById(secretSnapshotId)
const secretSnapshot = await SecretSnapshot.findById(secretSnapshotId)
.lean()
.populate<{ secretVersions: ISecretVersion[] }>({
path: 'secretVersions',
@@ -29,13 +26,7 @@ export const getSecretSnapshot = async (req: Request, res: Response) => {
.populate<{ folderVersion: TFolderRootVersionSchema }>("folderVersion");
if (!secretSnapshot) throw new Error("Failed to find secret snapshot");
} catch (err) {
Sentry.setUser({ email: req.user.email });
Sentry.captureException(err);
return res.status(400).send({
message: "Failed to get secret snapshot",
});
}
const folderId = secretSnapshot.folderId;
// to show only the folder required secrets
secretSnapshot.secretVersions = secretSnapshot.secretVersions.filter(
@@ -1,4 +1,3 @@
import * as Sentry from '@sentry/node';
import { Request, Response } from 'express';
import Stripe from 'stripe';
import { getStripeSecretKey, getStripeWebhookSecret } from '../../../config';
@@ -10,26 +9,17 @@ import { getStripeSecretKey, getStripeWebhookSecret } from '../../../config';
* @returns
*/
export const handleWebhook = async (req: Request, res: Response) => {
let event;
try {
const stripe = new Stripe(await getStripeSecretKey(), {
apiVersion: '2022-08-01'
});
// check request for valid stripe signature
const sig = req.headers['stripe-signature'] as string;
event = stripe.webhooks.constructEvent(
const event = stripe.webhooks.constructEvent(
req.body,
sig,
await getStripeWebhookSecret()
);
} catch (err) {
Sentry.setUser({ email: req.user.email });
Sentry.captureException(err);
return res.status(400).send({
error: 'Failed to process webhook'
});
}
switch (event.type) {
case '':
@@ -1,5 +1,4 @@
import { Request, Response } from "express";
import * as Sentry from "@sentry/node";
import { PipelineStage, Types } from "mongoose";
import { Secret } from "../../../models";
import {
@@ -69,15 +68,13 @@ export const getWorkspaceSecretSnapshots = async (
}
}
*/
let secretSnapshots;
try {
const { workspaceId } = req.params;
const { environment, folderId } = req.query;
const offset: number = parseInt(req.query.offset as string);
const limit: number = parseInt(req.query.limit as string);
secretSnapshots = await SecretSnapshot.find({
const secretSnapshots = await SecretSnapshot.find({
workspace: workspaceId,
environment,
folderId: folderId || "root",
@@ -85,13 +82,6 @@ export const getWorkspaceSecretSnapshots = async (
.sort({ createdAt: -1 })
.skip(offset)
.limit(limit);
} catch (err) {
Sentry.setUser({ email: req.user.email });
Sentry.captureException(err);
return res.status(400).send({
message: "Failed to get secret snapshots",
});
}
return res.status(200).send({
secretSnapshots,
@@ -107,23 +97,14 @@ export const getWorkspaceSecretSnapshotsCount = async (
req: Request,
res: Response
) => {
let count;
try {
const { workspaceId } = req.params;
const { environment, folderId } = req.query;
count = await SecretSnapshot.countDocuments({
const count = await SecretSnapshot.countDocuments({
workspace: workspaceId,
environment,
folderId: folderId || "root",
});
} catch (err) {
Sentry.setUser({ email: req.user.email });
Sentry.captureException(err);
return res.status(400).send({
message: "Failed to count number of secret snapshots",
});
}
return res.status(200).send({
count,
@@ -191,8 +172,6 @@ export const rollbackWorkspaceSecretSnapshot = async (
}
*/
let secrets;
try {
const { workspaceId } = req.params;
const { version, environment, folderId = "root" } = req.body;
@@ -376,7 +355,7 @@ export const rollbackWorkspaceSecretSnapshot = async (
});
// add secrets
secrets = await Secret.insertMany(
const secrets = await Secret.insertMany(
Object.keys(oldSecretVersionsObj).map((sv) => {
const {
secret: secretId,
@@ -501,13 +480,6 @@ export const rollbackWorkspaceSecretSnapshot = async (
environment,
folderId,
});
} catch (err) {
Sentry.setUser({ email: req.user.email });
Sentry.captureException(err);
return res.status(400).send({
message: "Failed to roll back secret snapshot",
});
}
return res.status(200).send({
secrets,
@@ -587,8 +559,6 @@ export const getWorkspaceLogs = async (req: Request, res: Response) => {
}
}
*/
let logs;
try {
const { workspaceId } = req.params;
const offset: number = parseInt(req.query.offset as string);
@@ -597,7 +567,7 @@ export const getWorkspaceLogs = async (req: Request, res: Response) => {
const userId: string = req.query.userId as string;
const actionNames: string = req.query.actionNames as string;
logs = await Log.find({
const logs = await Log.find({
workspace: workspaceId,
...(userId ? { user: userId } : {}),
...(actionNames
@@ -613,13 +583,6 @@ export const getWorkspaceLogs = async (req: Request, res: Response) => {
.limit(limit)
.populate("actions")
.populate("user serviceAccount serviceTokenData");
} catch (err) {
Sentry.setUser({ email: req.user.email });
Sentry.captureException(err);
return res.status(400).send({
message: "Failed to get workspace logs",
});
}
return res.status(200).send({
logs,
+1 -1
View File
@@ -1,5 +1,5 @@
import NodeCache from 'node-cache';
import * as Sentry from '@sentry/node';
import NodeCache from 'node-cache';
import {
getLicenseKey,
getLicenseServerKey,
+75 -32
View File
@@ -6,7 +6,9 @@ import {
User,
ServiceTokenData,
ServiceAccount,
APIKeyData
APIKeyData,
TokenVersion,
ITokenVersion
} from '../models';
import {
AccountNotFoundError,
@@ -35,7 +37,7 @@ import {
* @param {Object} obj
* @param {Object} obj.headers - HTTP request headers object
*/
const validateAuthMode = ({
export const validateAuthMode = ({
headers,
acceptedAuthModes
}: {
@@ -97,7 +99,7 @@ const validateAuthMode = ({
* @param {String} obj.authTokenValue - JWT token value
* @returns {User} user - user corresponding to JWT token
*/
const getAuthUserPayload = async ({
export const getAuthUserPayload = async ({
authTokenValue
}: {
authTokenValue: string;
@@ -107,14 +109,32 @@ const getAuthUserPayload = async ({
);
const user = await User.findOne({
_id: decodedToken.userId
}).select('+publicKey');
_id: new Types.ObjectId(decodedToken.userId)
}).select('+publicKey +accessVersion');
if (!user) throw AccountNotFoundError({ message: 'Failed to find User' });
if (!user) throw AccountNotFoundError({ message: 'Failed to find user' });
if (!user?.publicKey) throw UnauthorizedRequestError({ message: 'Failed to authenticate User with partially set up account' });
if (!user?.publicKey) throw UnauthorizedRequestError({ message: 'Failed to authenticate user with partially set up account' });
return user;
const tokenVersion = await TokenVersion.findOneAndUpdate({
_id: new Types.ObjectId(decodedToken.tokenVersionId),
user: user._id
}, {
lastUsed: new Date()
});
if (!tokenVersion) throw UnauthorizedRequestError({
message: 'Failed to validate access token'
});
if (decodedToken.accessVersion !== tokenVersion.accessVersion) throw UnauthorizedRequestError({
message: 'Failed to validate access token'
});
return ({
user,
tokenVersionId: tokenVersion._id
});
}
/**
@@ -123,7 +143,7 @@ const getAuthUserPayload = async ({
* @param {String} obj.authTokenValue - service token value
* @returns {ServiceTokenData} serviceTokenData - service token data
*/
const getAuthSTDPayload = async ({
export const getAuthSTDPayload = async ({
authTokenValue
}: {
authTokenValue: string;
@@ -169,7 +189,7 @@ const getAuthSTDPayload = async ({
* @param {String} obj.authTokenValue - service account access token value
* @returns {ServiceAccount} serviceAccount
*/
const getAuthSAAKPayload = async ({
export const getAuthSAAKPayload = async ({
authTokenValue
}: {
authTokenValue: string;
@@ -198,7 +218,7 @@ const getAuthSAAKPayload = async ({
* @param {String} obj.authTokenValue - API key value
* @returns {APIKeyData} apiKeyData - API key data
*/
const getAuthAPIKeyPayload = async ({
export const getAuthAPIKeyPayload = async ({
authTokenValue
}: {
authTokenValue: string;
@@ -255,12 +275,43 @@ const getAuthAPIKeyPayload = async ({
* @return {String} obj.token - issued JWT token
* @return {String} obj.refreshToken - issued refresh token
*/
const issueAuthTokens = async ({ userId }: { userId: string }) => {
export const issueAuthTokens = async ({
userId,
ip,
userAgent
}: {
userId: Types.ObjectId;
ip: string;
userAgent: string;
}) => {
let tokenVersion: ITokenVersion | null;
// continue with (session) token version matching existing ip and user agent
tokenVersion = await TokenVersion.findOne({
user: userId,
ip,
userAgent
});
if (!tokenVersion) {
// case: no existing ip and user agent exists
// -> create new (session) token version for ip and user agent
tokenVersion = await new TokenVersion({
user: userId,
refreshVersion: 0,
accessVersion: 0,
ip,
userAgent,
lastUsed: new Date()
}).save();
}
// issue tokens
const token = createToken({
payload: {
userId
userId,
tokenVersionId: tokenVersion._id.toString(),
accessVersion: tokenVersion.accessVersion
},
expiresIn: await getJwtAuthLifetime(),
secret: await getJwtAuthSecret()
@@ -268,7 +319,9 @@ const issueAuthTokens = async ({ userId }: { userId: string }) => {
const refreshToken = createToken({
payload: {
userId
userId,
tokenVersionId: tokenVersion._id.toString(),
refreshVersion: tokenVersion.refreshVersion
},
expiresIn: await getJwtRefreshLifetime(),
secret: await getJwtRefreshSecret()
@@ -285,13 +338,15 @@ const issueAuthTokens = async ({ userId }: { userId: string }) => {
* @param {Object} obj
* @param {String} obj.userId - id of user whose tokens are cleared.
*/
const clearTokens = async ({ userId }: { userId: string }): Promise<void> => {
export const clearTokens = async (tokenVersionId: Types.ObjectId): Promise<void> => {
// increment refreshVersion on user by 1
User.findOneAndUpdate({
_id: userId
await TokenVersion.findOneAndUpdate({
_id: tokenVersionId
}, {
$inc: {
refreshVersion: 1
refreshVersion: 1,
accessVersion: 1
}
});
};
@@ -304,7 +359,7 @@ const clearTokens = async ({ userId }: { userId: string }): Promise<void> => {
* @param {String} obj.secret - (JWT) secret such as [JWT_AUTH_SECRET]
* @param {String} obj.expiresIn - string describing time span such as '10h' or '7d'
*/
const createToken = ({
export const createToken = ({
payload,
expiresIn,
secret
@@ -318,7 +373,7 @@ const createToken = ({
});
};
const validateProviderAuthToken = async ({
export const validateProviderAuthToken = async ({
email,
user,
providerAuthToken,
@@ -342,15 +397,3 @@ const validateProviderAuthToken = async ({
throw new Error('Invalid authentication credentials.')
}
}
export {
validateAuthMode,
validateProviderAuthToken,
getAuthUserPayload,
getAuthSTDPayload,
getAuthSAAKPayload,
getAuthAPIKeyPayload,
createToken,
issueAuthTokens,
clearTokens
};
+5 -12
View File
@@ -31,7 +31,7 @@ import { InternalServerError } from "../utils/errors";
* @param {String} obj.name - name of bot
* @param {String} obj.workspaceId - id of workspace that bot belongs to
*/
const createBot = async ({
export const createBot = async ({
name,
workspaceId,
}: {
@@ -93,7 +93,7 @@ const createBot = async ({
* @param {String} obj.workspaceId - id of workspace
* @param {String} obj.environment - environment
*/
const getSecretsHelper = async ({
export const getSecretsBotHelper = async ({
workspaceId,
environment,
}: {
@@ -136,7 +136,7 @@ const getSecretsHelper = async ({
* @param {String} obj.workspaceId - id of workspace
* @returns {String} key - decrypted workspace key
*/
const getKey = async ({ workspaceId }: { workspaceId: string }) => {
export const getKey = async ({ workspaceId }: { workspaceId: string }) => {
const encryptionKey = await getEncryptionKey();
const rootEncryptionKey = await getRootEncryptionKey();
@@ -194,7 +194,7 @@ const getKey = async ({ workspaceId }: { workspaceId: string }) => {
* @param {String} obj1.workspaceId - id of workspace
* @param {String} obj1.plaintext - plaintext to encrypt
*/
const encryptSymmetricHelper = async ({
export const encryptSymmetricHelper = async ({
workspaceId,
plaintext,
}: {
@@ -222,7 +222,7 @@ const encryptSymmetricHelper = async ({
* @param {String} obj.iv - iv
* @param {String} obj.tag - tag
*/
const decryptSymmetricHelper = async ({
export const decryptSymmetricHelper = async ({
workspaceId,
ciphertext,
iv,
@@ -243,10 +243,3 @@ const decryptSymmetricHelper = async ({
return plaintext;
};
export {
createBot,
getSecretsHelper,
encryptSymmetricHelper,
decryptSymmetricHelper
};
+2 -7
View File
@@ -7,7 +7,7 @@ import { getLogger } from '../utils/logger';
* @param {String} obj.mongoURL - mongo connection string
* @returns
*/
const initDatabaseHelper = async ({
export const initDatabaseHelper = async ({
mongoURL
}: {
mongoURL: string;
@@ -30,7 +30,7 @@ const initDatabaseHelper = async ({
/**
* Close database conection
*/
const closeDatabaseHelper = async () => {
export const closeDatabaseHelper = async () => {
return Promise.all([
new Promise((resolve) => {
if (mongoose.connection && mongoose.connection.readyState == 1) {
@@ -42,8 +42,3 @@ const closeDatabaseHelper = async () => {
})
]);
}
export {
initDatabaseHelper,
closeDatabaseHelper
}
+1 -3
View File
@@ -18,7 +18,7 @@ interface Event {
* @param {String} obj.event.workspaceId - id of workspace that event is part of
* @param {Object} obj.event.payload - payload of event (depends on event)
*/
const handleEventHelper = async ({ event }: { event: Event }) => {
export const handleEventHelper = async ({ event }: { event: Event }) => {
const { workspaceId, environment } = event;
// TODO: moduralize bot check into separate function
@@ -38,5 +38,3 @@ const handleEventHelper = async ({ event }: { event: Event }) => {
break;
}
};
export { handleEventHelper };
+17
View File
@@ -0,0 +1,17 @@
export * from './auth';
export * from './bot';
export * from './database';
export * from './event';
export * from './integration';
export * from './key';
export * from './membership';
export * from './membershipOrg';
export * from './nodemailer';
export * from './organization';
export * from './rateLimiter';
export * from './secret';
export * from './secrets';
export * from './signup';
export * from './token';
export * from './user';
export * from './workspace';
+11 -72
View File
@@ -1,4 +1,3 @@
import * as Sentry from '@sentry/node';
import { Types } from 'mongoose';
import {
Bot,
@@ -16,7 +15,6 @@ import {
import {
UnauthorizedRequestError,
} from '../utils/errors';
import RequestError from '../utils/requestError';
interface Update {
workspace: string;
@@ -37,7 +35,7 @@ interface Update {
* @param {String} obj.code - code
* @returns {IntegrationAuth} integrationAuth - integration auth after OAuth2 code-token exchange
*/
const handleOAuthExchangeHelper = async ({
export const handleOAuthExchangeHelper = async ({
workspaceId,
integration,
code,
@@ -48,8 +46,6 @@ const handleOAuthExchangeHelper = async ({
code: string;
environment: string;
}) => {
let integrationAuth;
try {
const bot = await Bot.findOne({
workspace: workspaceId,
isActive: true
@@ -77,7 +73,7 @@ const handleOAuthExchangeHelper = async ({
break;
}
integrationAuth = await IntegrationAuth.findOneAndUpdate({
const integrationAuth = await IntegrationAuth.findOneAndUpdate({
workspace: workspaceId,
integration
}, update, {
@@ -104,11 +100,6 @@ const handleOAuthExchangeHelper = async ({
accessExpiresAt: res.accessExpiresAt
});
}
} catch (err) {
Sentry.setUser(null);
Sentry.captureException(err);
throw new Error('Failed to handle OAuth2 code-token exchange')
}
return integrationAuth;
}
@@ -118,16 +109,14 @@ const handleOAuthExchangeHelper = async ({
* @param {Object} obj
* @param {Object} obj.workspaceId - id of workspace
*/
const syncIntegrationsHelper = async ({
export const syncIntegrationsHelper = async ({
workspaceId,
environment
}: {
workspaceId: Types.ObjectId;
environment?: string;
}) => {
let integrations;
try {
integrations = await Integration.find({
const integrations = await Integration.find({
workspace: workspaceId,
...(environment ? {
environment
@@ -162,11 +151,6 @@ const syncIntegrationsHelper = async ({
accessToken: access.accessToken
});
}
} catch (err) {
Sentry.setUser(null);
Sentry.captureException(err);
throw new Error('Failed to sync secrets to integrations');
}
}
/**
@@ -177,32 +161,20 @@ const syncIntegrationsHelper = async ({
* @param {String} obj.integrationAuthId - id of integration auth
* @param {String} refreshToken - decrypted refresh token
*/
const getIntegrationAuthRefreshHelper = async ({ integrationAuthId }: { integrationAuthId: Types.ObjectId }) => {
let refreshToken;
try {
export const getIntegrationAuthRefreshHelper = async ({ integrationAuthId }: { integrationAuthId: Types.ObjectId }) => {
const integrationAuth = await IntegrationAuth
.findById(integrationAuthId)
.select('+refreshCiphertext +refreshIV +refreshTag');
if (!integrationAuth) throw UnauthorizedRequestError({message: 'Failed to locate Integration Authentication credentials'});
refreshToken = await BotService.decryptSymmetric({
const refreshToken = await BotService.decryptSymmetric({
workspaceId: integrationAuth.workspace,
ciphertext: integrationAuth.refreshCiphertext as string,
iv: integrationAuth.refreshIV as string,
tag: integrationAuth.refreshTag as string
});
} catch (err) {
Sentry.setUser(null);
Sentry.captureException(err);
if(err instanceof RequestError)
throw err
else
throw new Error('Failed to get integration refresh token');
}
return refreshToken;
}
@@ -214,10 +186,9 @@ const syncIntegrationsHelper = async ({
* @param {String} obj.integrationAuthId - id of integration auth
* @returns {String} accessToken - decrypted access token
*/
const getIntegrationAuthAccessHelper = async ({ integrationAuthId }: { integrationAuthId: Types.ObjectId }) => {
export const getIntegrationAuthAccessHelper = async ({ integrationAuthId }: { integrationAuthId: Types.ObjectId }) => {
let accessId;
let accessToken;
try {
const integrationAuth = await IntegrationAuth
.findById(integrationAuthId)
.select('workspace integration +accessCiphertext +accessIV +accessTag +accessExpiresAt + refreshCiphertext +accessIdCiphertext +accessIdIV +accessIdTag');
@@ -254,15 +225,6 @@ const getIntegrationAuthAccessHelper = async ({ integrationAuthId }: { integrati
});
}
} catch (err) {
Sentry.setUser(null);
Sentry.captureException(err);
if(err instanceof RequestError)
throw err
else
throw new Error('Failed to get integration access token');
}
return ({
accessId,
accessToken
@@ -277,7 +239,7 @@ const getIntegrationAuthAccessHelper = async ({ integrationAuthId }: { integrati
* @param {String} obj.integrationAuthId - id of integration auth
* @param {String} obj.refreshToken - refresh token
*/
const setIntegrationAuthRefreshHelper = async ({
export const setIntegrationAuthRefreshHelper = async ({
integrationAuthId,
refreshToken
}: {
@@ -285,9 +247,7 @@ const setIntegrationAuthRefreshHelper = async ({
refreshToken: string;
}) => {
let integrationAuth;
try {
integrationAuth = await IntegrationAuth
let integrationAuth = await IntegrationAuth
.findById(integrationAuthId);
if (!integrationAuth) throw new Error('Failed to find integration auth');
@@ -308,11 +268,6 @@ const setIntegrationAuthRefreshHelper = async ({
}, {
new: true
});
} catch (err) {
Sentry.setUser(null);
Sentry.captureException(err);
throw new Error('Failed to set integration auth refresh token');
}
return integrationAuth;
}
@@ -326,7 +281,7 @@ const setIntegrationAuthRefreshHelper = async ({
* @param {String} obj.accessToken - access token
* @param {Date} obj.accessExpiresAt - expiration date of access token
*/
const setIntegrationAuthAccessHelper = async ({
export const setIntegrationAuthAccessHelper = async ({
integrationAuthId,
accessId,
accessToken,
@@ -337,9 +292,7 @@ const setIntegrationAuthAccessHelper = async ({
accessToken: string;
accessExpiresAt: Date | undefined;
}) => {
let integrationAuth;
try {
integrationAuth = await IntegrationAuth.findById(integrationAuthId);
let integrationAuth = await IntegrationAuth.findById(integrationAuthId);
if (!integrationAuth) throw new Error('Failed to find integration auth');
@@ -371,20 +324,6 @@ const setIntegrationAuthAccessHelper = async ({
}, {
new: true
});
} catch (err) {
Sentry.setUser(null);
Sentry.captureException(err);
throw new Error('Failed to save integration auth access token');
}
return integrationAuth;
}
export {
handleOAuthExchangeHelper,
syncIntegrationsHelper,
getIntegrationAuthRefreshHelper,
getIntegrationAuthAccessHelper,
setIntegrationAuthRefreshHelper,
setIntegrationAuthAccessHelper
}
+1 -3
View File
@@ -17,7 +17,7 @@ interface Key {
* @param {String} obj.keys.nonce - nonce for encryption
* @param {String} obj.keys.userId - id of receiver user
*/
const pushKeys = async ({
export const pushKeys = async ({
userId,
workspaceId,
keys
@@ -51,5 +51,3 @@ const pushKeys = async ({
}))
);
};
export { pushKeys };
+15 -40
View File
@@ -1,7 +1,6 @@
import * as Sentry from '@sentry/node';
import { Types } from 'mongoose';
import { Membership, Key } from '../models';
import { MembershipNotFoundError, BadRequestError } from '../utils/errors';
import { Types } from "mongoose";
import { Membership, Key } from "../models";
import { MembershipNotFoundError, BadRequestError } from "../utils/errors";
/**
* Validate that user with id [userId] is a member of workspace with id [workspaceId]
@@ -11,30 +10,30 @@ import { MembershipNotFoundError, BadRequestError } from '../utils/errors';
* @param {String} obj.workspaceId - id of workspace
* @returns {Membership} membership - membership of user with id [userId] for workspace with id [workspaceId]
*/
const validateMembership = async ({
export const validateMembership = async ({
userId,
workspaceId,
acceptedRoles,
}: {
userId: Types.ObjectId | string;
workspaceId: Types.ObjectId | string;
acceptedRoles?: Array<'admin' | 'member'>;
acceptedRoles?: Array<"admin" | "member">;
}) => {
const membership = await Membership.findOne({
user: userId,
workspace: workspaceId,
}).populate('workspace');
}).populate("workspace");
if (!membership) {
throw MembershipNotFoundError({
message: 'Failed to find workspace membership',
message: "Failed to find workspace membership",
});
}
if (acceptedRoles) {
if (!acceptedRoles.includes(membership.role)) {
throw BadRequestError({
message: 'Failed authorization for membership role',
message: "Failed authorization for membership role",
});
}
}
@@ -47,16 +46,8 @@ const validateMembership = async ({
* @param {Object} queryObj - query object
* @return {Object} membership - membership
*/
const findMembership = async (queryObj: any) => {
let membership;
try {
membership = await Membership.findOne(queryObj);
} catch (err) {
Sentry.setUser(null);
Sentry.captureException(err);
throw new Error('Failed to find membership');
}
export const findMembership = async (queryObj: any) => {
const membership = await Membership.findOne(queryObj);
return membership;
};
@@ -68,16 +59,15 @@ const findMembership = async (queryObj: any) => {
* @param {String} obj.workspaceId - id of workspace.
* @param {String[]} obj.roles - roles of users.
*/
const addMemberships = async ({
export const addMemberships = async ({
userIds,
workspaceId,
roles,
roles
}: {
userIds: string[];
workspaceId: string;
roles: string[];
}): Promise<void> => {
try {
const operations = userIds.map((userId, idx) => {
return {
updateOne: {
@@ -95,13 +85,7 @@ const addMemberships = async ({
},
};
});
await Membership.bulkWrite(operations as any);
} catch (err) {
Sentry.setUser(null);
Sentry.captureException(err);
throw new Error('Failed to add users to workspace');
}
};
/**
@@ -109,11 +93,9 @@ const addMemberships = async ({
* @param {Object} obj
* @param {String} obj.membershipId - id of membership to delete
*/
const deleteMembership = async ({ membershipId }: { membershipId: string }) => {
let deletedMembership;
try {
deletedMembership = await Membership.findOneAndDelete({
_id: membershipId,
export const deleteMembership = async ({ membershipId }: { membershipId: string }) => {
const deletedMembership = await Membership.findOneAndDelete({
_id: membershipId
});
// delete keys associated with the membership
@@ -124,13 +106,6 @@ const deleteMembership = async ({ membershipId }: { membershipId: string }) => {
workspace: deletedMembership.workspace,
});
}
} catch (err) {
Sentry.setUser(null);
Sentry.captureException(err);
throw new Error('Failed to delete membership');
}
return deletedMembership;
};
export { validateMembership, addMemberships, findMembership, deleteMembership };
+4 -11
View File
@@ -18,7 +18,7 @@ import {
* @param {Types.ObjectId} obj.organizationId
* @param {String[]} obj.acceptedRoles
*/
const validateMembershipOrg = async ({
export const validateMembershipOrg = async ({
userId,
organizationId,
acceptedRoles,
@@ -59,7 +59,7 @@ const validateMembershipOrg = async ({
* @param {Object} queryObj - query object
* @return {Object} membershipOrg - membership
*/
const findMembershipOrg = (queryObj: any) => {
export const findMembershipOrg = (queryObj: any) => {
const membershipOrg = MembershipOrg.findOne(queryObj);
return membershipOrg;
};
@@ -72,7 +72,7 @@ const findMembershipOrg = (queryObj: any) => {
* @param {String} obj.organizationId - id of organization.
* @param {String[]} obj.roles - roles of users.
*/
const addMembershipsOrg = async ({
export const addMembershipsOrg = async ({
userIds,
organizationId,
roles,
@@ -111,7 +111,7 @@ const addMembershipsOrg = async ({
* @param {Object} obj
* @param {String} obj.membershipOrgId - id of organization membership to delete
*/
const deleteMembershipOrg = async ({
export const deleteMembershipOrg = async ({
membershipOrgId
}: {
membershipOrgId: string;
@@ -149,10 +149,3 @@ const deleteMembershipOrg = async ({
return deletedMembershipOrg;
};
export {
validateMembershipOrg,
findMembershipOrg,
addMembershipsOrg,
deleteMembershipOrg
};
+2 -10
View File
@@ -1,4 +1,3 @@
import * as Sentry from '@sentry/node';
import fs from 'fs';
import path from 'path';
import handlebars from 'handlebars';
@@ -14,7 +13,7 @@ let smtpTransporter: nodemailer.Transporter;
* @param {String[]} obj.recipients - email addresses of people to send email to
* @param {Object} obj.substitutions - object containing template substitutions
*/
const sendMail = async ({
export const sendMail = async ({
template,
subjectLine,
recipients,
@@ -26,7 +25,6 @@ const sendMail = async ({
substitutions: any;
}) => {
if (await getSmtpConfigured()) {
try {
const html = fs.readFileSync(
path.resolve(__dirname, '../templates/' + template),
'utf8'
@@ -40,15 +38,9 @@ const sendMail = async ({
subject: subjectLine,
html: htmlToSend
});
} catch (err) {
Sentry.setUser(null);
Sentry.captureException(err);
}
}
};
const setTransporter = (transporter: nodemailer.Transporter) => {
export const setTransporter = (transporter: nodemailer.Transporter) => {
smtpTransporter = transporter;
};
export { sendMail, setTransporter };
+3 -9
View File
@@ -28,7 +28,7 @@ import {
* @param {String} obj.email - POC email that will receive invoice info
* @param {Object} organization - new organization
*/
const createOrganization = async ({
export const createOrganization = async ({
name,
email,
}: {
@@ -70,7 +70,7 @@ const createOrganization = async ({
* @return {Object} obj.stripeSubscription - new stripe subscription
* @return {Subscription} obj.subscription - new subscription
*/
const initSubscriptionOrg = async ({
export const initSubscriptionOrg = async ({
organizationId,
}: {
organizationId: Types.ObjectId;
@@ -125,7 +125,7 @@ const initSubscriptionOrg = async ({
* @param {Object} obj
* @param {Number} obj.organizationId - id of subscription's organization
*/
const updateSubscriptionOrgQuantity = async ({
export const updateSubscriptionOrgQuantity = async ({
organizationId,
}: {
organizationId: string;
@@ -172,9 +172,3 @@ const updateSubscriptionOrgQuantity = async ({
return stripeSubscription;
};
export {
createOrganization,
initSubscriptionOrg,
updateSubscriptionOrgQuantity
};
+8 -14
View File
@@ -2,7 +2,7 @@ import rateLimit from 'express-rate-limit';
const MongoStore = require('rate-limit-mongo');
// 200 per minute
const apiLimiter = rateLimit({
export const apiLimiter = rateLimit({
store: new MongoStore({
uri: process.env.MONGO_URL,
expireTimeMs: 1000 * 60,
@@ -17,7 +17,7 @@ const apiLimiter = rateLimit({
return request.path === '/healthcheck' || request.path === '/api/status'
},
keyGenerator: (req, res) => {
return req.clientIp
return req.realIP
}
});
@@ -34,12 +34,12 @@ const authLimit = rateLimit({
standardHeaders: true,
legacyHeaders: false,
keyGenerator: (req, res) => {
return req.clientIp
return req.realIP
}
});
// 50 requests per 1 hour
const passwordLimiter = rateLimit({
// 5 requests per 1 hour
export const passwordLimiter = rateLimit({
store: new MongoStore({
uri: process.env.MONGO_URL,
expireTimeMs: 1000 * 60 * 60,
@@ -47,24 +47,18 @@ const passwordLimiter = rateLimit({
collectionName: "expressRateRecords-passwordLimiter",
}),
windowMs: 1000 * 60 * 60,
max: 50,
max: 5,
standardHeaders: true,
legacyHeaders: false,
keyGenerator: (req, res) => {
return req.clientIp
return req.realIP
}
});
const authLimiter = (req: any, res: any, next: any) => {
export const authLimiter = (req: any, res: any, next: any) => {
if (process.env.NODE_ENV === 'production') {
authLimit(req, res, next);
} else {
next();
}
};
export {
apiLimiter,
authLimiter,
passwordLimiter
};
+5 -7
View File
@@ -60,7 +60,7 @@ interface Update {
* @param {String} obj.environment - environment for secrets
* @param {Object[]} obj.secrets - secrets to push
*/
const v1PushSecrets = async ({
export const v1PushSecrets = async ({
userId,
workspaceId,
environment,
@@ -304,7 +304,7 @@ const v1PushSecrets = async ({
* @param {String} obj.channel - channel (web/cli/auto)
* @param {String} obj.ipAddress - ip address of request to push secrets
*/
const v2PushSecrets = async ({
export const v2PushSecrets = async ({
userId,
workspaceId,
environment,
@@ -530,7 +530,7 @@ const v2PushSecrets = async ({
* @param {String} obj.workspaceId - id of workspace to pull from
* @param {String} obj.environment - environment for secrets
*/
const getSecrets = async ({
export const getSecrets = async ({
userId,
workspaceId,
environment,
@@ -570,7 +570,7 @@ const getSecrets = async ({
* @param {String} obj.channel - channel (web/cli/auto)
* @param {String} obj.ipAddress - ip address of request to push secrets
*/
const pullSecrets = async ({
export const pullSecrets = async ({
userId,
workspaceId,
environment,
@@ -614,7 +614,7 @@ const pullSecrets = async ({
* @param {Object} obj
* @param {Object} obj.secrets
*/
const reformatPullSecrets = ({ secrets }: { secrets: ISecret[] }) => {
export const reformatPullSecrets = ({ secrets }: { secrets: ISecret[] }) => {
const reformatedSecrets = secrets.map((s) => ({
_id: s._id,
workspace: s.workspace,
@@ -645,5 +645,3 @@ const reformatPullSecrets = ({ secrets }: { secrets: ISecret[] }) => {
return reformatedSecrets;
};
export { v1PushSecrets, v2PushSecrets, pullSecrets, reformatPullSecrets };
+15 -26
View File
@@ -45,8 +45,8 @@ import {
* @param {Object} obj
* @param {Types.ObjectId} obj.workspaceId
*/
const createSecretBlindIndexDataHelper = async ({
workspaceId,
export const createSecretBlindIndexDataHelper = async ({
workspaceId
}: {
workspaceId: Types.ObjectId;
}) => {
@@ -98,8 +98,8 @@ const createSecretBlindIndexDataHelper = async ({
* @param {Types.ObjectId} obj.workspaceId - id of workspace to get salt for
* @returns
*/
const getSecretBlindIndexSaltHelper = async ({
workspaceId,
export const getSecretBlindIndexSaltHelper = async ({
workspaceId
}: {
workspaceId: Types.ObjectId;
}) => {
@@ -147,9 +147,9 @@ const getSecretBlindIndexSaltHelper = async ({
* @param {String} obj.secretName - name of secret to generate blind index for
* @param {String} obj.salt - base64-salt
*/
const generateSecretBlindIndexWithSaltHelper = async ({
export const generateSecretBlindIndexWithSaltHelper = async ({
secretName,
salt,
salt
}: {
secretName: string;
salt: string;
@@ -177,9 +177,9 @@ const generateSecretBlindIndexWithSaltHelper = async ({
* @param {Stringj} obj.secretName - name of secret to generate blind index for
* @param {Types.ObjectId} obj.workspaceId - id of workspace that secret belongs to
*/
const generateSecretBlindIndexHelper = async ({
export const generateSecretBlindIndexHelper = async ({
secretName,
workspaceId,
workspaceId
}: {
secretName: string;
workspaceId: Types.ObjectId;
@@ -247,7 +247,7 @@ const generateSecretBlindIndexHelper = async ({
* @param {AuthData} obj.authData - authentication data on request
* @returns
*/
const createSecretHelper = async ({
export const createSecretHelper = async ({
secretName,
workspaceId,
environment,
@@ -397,10 +397,10 @@ const createSecretHelper = async ({
* @param {AuthData} obj.authData - authentication data on request
* @returns
*/
const getSecretsHelper = async ({
export const getSecretsHelper = async ({
workspaceId,
environment,
authData,
authData
}: GetSecretsParams) => {
let secrets: ISecret[] = [];
@@ -472,7 +472,7 @@ const getSecretsHelper = async ({
* @param {AuthData} obj.authData - authentication data on request
* @returns
*/
const getSecretHelper = async ({
export const getSecretHelper = async ({
secretName,
workspaceId,
environment,
@@ -558,7 +558,8 @@ const getSecretHelper = async ({
* @param {AuthData} obj.authData - authentication data on request
* @returns
*/
const updateSecretHelper = async ({
export const updateSecretHelper = async ({
secretName,
workspaceId,
environment,
@@ -698,7 +699,7 @@ const updateSecretHelper = async ({
* @param {AuthData} obj.authData - authentication data on request
* @returns
*/
const deleteSecretHelper = async ({
export const deleteSecretHelper = async ({
secretName,
workspaceId,
environment,
@@ -800,15 +801,3 @@ const deleteSecretHelper = async ({
secret,
};
};
export {
createSecretBlindIndexDataHelper,
getSecretBlindIndexSaltHelper,
generateSecretBlindIndexWithSaltHelper,
generateSecretBlindIndexHelper,
createSecretHelper,
getSecretsHelper,
getSecretHelper,
updateSecretHelper,
deleteSecretHelper,
};
+3 -20
View File
@@ -1,4 +1,3 @@
import * as Sentry from '@sentry/node';
import { IUser } from '../models';
import { createOrganization } from './organization';
import { addMembershipsOrg } from './membershipOrg';
@@ -14,8 +13,7 @@ import { TOKEN_EMAIL_CONFIRMATION } from '../variables';
* @param {String} obj.email - email
* @returns {Boolean} success - whether or not operation was successful
*/
const sendEmailVerification = async ({ email }: { email: string }) => {
try {
export const sendEmailVerification = async ({ email }: { email: string }) => {
const token = await TokenService.createToken({
type: TOKEN_EMAIL_CONFIRMATION,
email
@@ -30,13 +28,6 @@ const sendEmailVerification = async ({ email }: { email: string }) => {
code: token
}
});
} catch (err) {
Sentry.setUser(null);
Sentry.captureException(err);
throw new Error(
"Ouch. We weren't able to send your email verification code"
);
}
};
/**
@@ -45,24 +36,18 @@ const sendEmailVerification = async ({ email }: { email: string }) => {
* @param {String} obj.email - emai
* @param {String} obj.code - code that was sent to [email]
*/
const checkEmailVerification = async ({
export const checkEmailVerification = async ({
email,
code
}: {
email: string;
code: string;
}) => {
try {
await TokenService.validateToken({
type: TOKEN_EMAIL_CONFIRMATION,
email,
token: code
});
} catch (err) {
Sentry.setUser(null);
Sentry.captureException(err);
throw new Error("Oops. We weren't able to verify");
}
};
/**
@@ -72,7 +57,7 @@ const checkEmailVerification = async ({
* @param {String} obj.organizationName - name of organization to initialize
* @param {IUser} obj.user - user who we are initializing for
*/
const initializeDefaultOrg = async ({
export const initializeDefaultOrg = async ({
organizationName,
user
}: {
@@ -97,5 +82,3 @@ const initializeDefaultOrg = async ({
throw new Error(`Failed to initialize default organization and workspace [err=${err}]`);
}
};
export { sendEmailVerification, checkEmailVerification, initializeDefaultOrg };
View File
+2 -4
View File
@@ -20,7 +20,7 @@ import { getSaltRounds } from "../config";
* @param {Types.ObjectId} obj.organizationId
* @returns {String} token - the created token
*/
const createTokenHelper = async ({
export const createTokenHelper = async ({
type,
email,
phoneNumber,
@@ -121,7 +121,7 @@ const createTokenHelper = async ({
* @param {String} obj.email - email associated with the token
* @param {String} obj.token - value of the token
*/
const validateTokenHelper = async ({
export const validateTokenHelper = async ({
type,
email,
phoneNumber,
@@ -213,5 +213,3 @@ const validateTokenHelper = async ({
// case: token is valid
await TokenData.findByIdAndDelete(tokenData._id);
};
export { createTokenHelper, validateTokenHelper };
+3 -22
View File
@@ -1,4 +1,3 @@
import * as Sentry from '@sentry/node';
import {
Workspace,
Bot,
@@ -16,17 +15,15 @@ import { SecretService } from '../services';
* @param {String} organizationId - id of organization to create workspace in
* @param {Object} workspace - new workspace
*/
const createWorkspace = async ({
export const createWorkspace = async ({
name,
organizationId
}: {
name: string;
organizationId: string;
}) => {
let workspace;
try {
// create workspace
workspace = await new Workspace({
const workspace = await new Workspace({
name,
organization: organizationId,
autoCapitalization: true
@@ -43,11 +40,6 @@ const createWorkspace = async ({
workspaceId: workspace._id
});
} catch (err) {
Sentry.setUser(null);
Sentry.captureException(err);
throw new Error('Failed to create workspace');
}
return workspace;
};
@@ -58,8 +50,7 @@ const createWorkspace = async ({
* @param {Object} obj
* @param {String} obj.id - id of workspace to delete
*/
const deleteWorkspace = async ({ id }: { id: string }) => {
try {
export const deleteWorkspace = async ({ id }: { id: string }) => {
await Workspace.deleteOne({ _id: id });
await Bot.deleteOne({
workspace: id
@@ -73,14 +64,4 @@ const deleteWorkspace = async ({ id }: { id: string }) => {
await Key.deleteMany({
workspace: id
});
} catch (err) {
Sentry.setUser(null);
Sentry.captureException(err);
throw new Error('Failed to delete workspace');
}
};
export {
createWorkspace,
deleteWorkspace
};
+9 -3
View File
@@ -1,6 +1,8 @@
import dotenv from "dotenv";
dotenv.config();
import express from "express";
// eslint-disable-next-line @typescript-eslint/no-var-requires
require('express-async-errors');
import helmet from "helmet";
import cors from "cors";
import { DatabaseService } from "./services";
@@ -11,7 +13,6 @@ import swaggerUi = require("swagger-ui-express");
// eslint-disable-next-line @typescript-eslint/no-var-requires
const swaggerFile = require("../spec.json");
// eslint-disable-next-line @typescript-eslint/no-var-requires
const requestIp = require("request-ip");
import { apiLimiter } from "./helpers/rateLimiter";
import {
workspace as eeWorkspaceRouter,
@@ -84,8 +85,6 @@ const main = async () => {
})
);
app.use(requestIp.mw());
if ((await getNodeEnv()) === "production") {
// enable app-wide rate-limiting + helmet security
// in production
@@ -94,6 +93,13 @@ const main = async () => {
app.use(helmet());
}
app.use((req, res, next) => {
// default to IP address provided by Cloudflare
const cfIp = req.headers['cf-connecting-ip'];
req.realIP = Array.isArray(cfIp) ? cfIp[0] : (cfIp as string) || req.ip;
next();
});
// (EE) routes
app.use("/api/v1/secret", eeSecretRouter);
app.use("/api/v1/secret-snapshot", eeSecretSnapshotRouter);
+28 -96
View File
@@ -1,4 +1,3 @@
import * as Sentry from "@sentry/node";
import _ from 'lodash';
import AWS from 'aws-sdk';
import {
@@ -63,7 +62,6 @@ const syncSecrets = async ({
accessId: string | null;
accessToken: string;
}) => {
try {
switch (integration.integration) {
case INTEGRATION_AZURE_KEY_VAULT:
await syncSecretsAzureKeyVault({
@@ -160,6 +158,34 @@ const syncSecrets = async ({
accessToken,
});
break;
case INTEGRATION_SUPABASE:
await syncSecretsSupabase({
integration,
secrets,
accessToken
});
break;
case INTEGRATION_FLYIO:
await syncSecretsFlyio({
integration,
secrets,
accessToken,
});
break;
case INTEGRATION_CIRCLECI:
await syncSecretsCircleCI({
integration,
secrets,
accessToken,
});
break;
case INTEGRATION_TRAVISCI:
await syncSecretsTravisCI({
integration,
secrets,
accessToken,
});
break;
case INTEGRATION_SUPABASE:
await syncSecretsSupabase({
integration,
@@ -178,7 +204,6 @@ const syncSecrets = async ({
} catch (err) {
Sentry.setUser(null);
Sentry.captureException(err);
throw new Error('Failed to sync secrets to integration');
}
};
@@ -198,7 +223,6 @@ const syncSecretsAzureKeyVault = async ({
secrets: any;
accessToken: string;
}) => {
try {
interface GetAzureKeyVaultSecret {
id: string; // secret URI
attributes: {
@@ -221,7 +245,6 @@ const syncSecretsAzureKeyVault = async ({
*/
const paginateAzureKeyVaultSecrets = async (url: string) => {
let result: GetAzureKeyVaultSecret[] = [];
try {
while (url) {
const res = await standardRequest.get(url, {
headers: {
@@ -234,11 +257,6 @@ const syncSecretsAzureKeyVault = async ({
url = res.data.nextLink;
}
} catch (err) {
Sentry.setUser(null);
Sentry.captureException(err);
}
return result;
}
@@ -369,11 +387,6 @@ const syncSecretsAzureKeyVault = async ({
}
});
}
} catch (err) {
Sentry.setUser(null);
Sentry.captureException(err);
throw new Error('Failed to sync secrets to Azure Key Vault');
}
};
/**
@@ -395,7 +408,6 @@ const syncSecretsAWSParameterStore = async ({
accessId: string | null;
accessToken: string;
}) => {
try {
if (!accessId) return;
AWS.config.update({
@@ -471,11 +483,6 @@ const syncSecretsAWSParameterStore = async ({
accessKeyId: undefined,
secretAccessKey: undefined
});
} catch (err) {
Sentry.setUser(null);
Sentry.captureException(err);
throw new Error('Failed to sync secrets to AWS Parameter Store');
}
}
/**
@@ -545,10 +552,6 @@ const syncSecretsAWSSecretManager = async ({
Name: integration.app,
SecretString: JSON.stringify(secrets)
}));
} else {
Sentry.setUser(null);
Sentry.captureException(err);
throw new Error('Failed to sync secrets to AWS Secret Manager');
}
AWS.config.update({
region: undefined,
@@ -574,7 +577,6 @@ const syncSecretsHeroku = async ({
secrets: any;
accessToken: string;
}) => {
try {
const herokuSecrets = (
await standardRequest.get(
`${INTEGRATION_HEROKU_API_URL}/apps/${integration.app}/config-vars`,
@@ -605,11 +607,6 @@ const syncSecretsHeroku = async ({
},
}
);
} catch (err) {
Sentry.setUser(null);
Sentry.captureException(err);
throw new Error("Failed to sync secrets to Heroku");
}
};
/**
@@ -637,8 +634,6 @@ const syncSecretsVercel = async ({
target: string[];
gitBranch?: string;
}
try {
// Get all (decrypted) secrets back from Vercel in
// decrypted format
const params: { [key: string]: string } = {
@@ -797,11 +792,6 @@ const syncSecretsVercel = async ({
}
);
}
} catch (err) {
Sentry.setUser(null);
Sentry.captureException(err);
throw new Error("Failed to sync secrets to Vercel");
}
};
/**
@@ -823,7 +813,6 @@ const syncSecretsNetlify = async ({
secrets: any;
accessToken: string;
}) => {
try {
interface NetlifyValue {
id?: string;
context: string; // 'dev' | 'branch-deploy' | 'deploy-preview' | 'production',
@@ -1020,11 +1009,6 @@ const syncSecretsNetlify = async ({
);
});
}
} catch (err) {
Sentry.setUser(null);
Sentry.captureException(err);
throw new Error("Failed to sync secrets to Heroku");
}
};
/**
@@ -1044,7 +1028,6 @@ const syncSecretsGitHub = async ({
secrets: any;
accessToken: string;
}) => {
try {
interface GitHubRepoKey {
key_id: string;
key: string;
@@ -1135,11 +1118,6 @@ const syncSecretsGitHub = async ({
);
});
});
} catch (err) {
Sentry.setUser(null);
Sentry.captureException(err);
throw new Error("Failed to sync secrets to GitHub");
}
};
/**
@@ -1158,7 +1136,6 @@ const syncSecretsRender = async ({
secrets: any;
accessToken: string;
}) => {
try {
await standardRequest.put(
`${INTEGRATION_RENDER_API_URL}/v1/services/${integration.appId}/env-vars`,
Object.keys(secrets).map((key) => ({
@@ -1172,11 +1149,6 @@ const syncSecretsRender = async ({
},
}
);
} catch (err) {
Sentry.setUser(null);
Sentry.captureException(err);
throw new Error("Failed to sync secrets to Render");
}
};
/**
@@ -1195,8 +1167,6 @@ const syncSecretsRailway = async ({
secrets: any;
accessToken: string;
}) => {
try {
const query = `
mutation UpsertVariables($input: VariableCollectionUpsertInput!) {
variableCollectionUpsert(input: $input)
@@ -1223,12 +1193,6 @@ const syncSecretsRailway = async ({
'Accept-Encoding': 'application/json'
},
});
} catch (err) {
Sentry.setUser(null);
Sentry.captureException(err);
throw new Error("Failed to sync secrets to Railway");
}
}
/**
@@ -1247,7 +1211,6 @@ const syncSecretsFlyio = async ({
secrets: any;
accessToken: string;
}) => {
try {
// set secrets
const SetSecrets = `
mutation($input: SetSecretsInput!) {
@@ -1355,12 +1318,6 @@ const syncSecretsFlyio = async ({
'Accept-Encoding': 'application/json',
},
});
} catch (err) {
Sentry.setUser(null);
Sentry.captureException(err);
throw new Error("Failed to sync secrets to Fly.io");
}
};
/**
@@ -1379,7 +1336,6 @@ const syncSecretsCircleCI = async ({
secrets: any;
accessToken: string;
}) => {
try {
const circleciOrganizationDetail = (
await standardRequest.get(`${INTEGRATION_CIRCLECI_API_URL}/v2/me/collaborations`, {
headers: {
@@ -1436,11 +1392,6 @@ const syncSecretsCircleCI = async ({
);
}
});
} catch (err) {
Sentry.setUser(null);
Sentry.captureException(err);
throw new Error("Failed to sync secrets to CircleCI");
}
};
/**
@@ -1459,7 +1410,6 @@ const syncSecretsTravisCI = async ({
secrets: any;
accessToken: string;
}) => {
try {
// get secrets from travis-ci
const getSecretsRes = (
await standardRequest.get(
@@ -1537,11 +1487,6 @@ const syncSecretsTravisCI = async ({
);
}
}
} catch (err) {
Sentry.setUser(null);
Sentry.captureException(err);
throw new Error("Failed to sync secrets to GitLab");
}
}
/**
@@ -1561,7 +1506,6 @@ const syncSecretsGitLab = async ({
secrets: any;
accessToken: string;
}) => {
try {
interface GitLabSecret {
key: string;
value: string;
@@ -1655,12 +1599,6 @@ const syncSecretsGitLab = async ({
);
}
}
} catch (err) {
Sentry.setUser(null);
Sentry.captureException(err);
throw new Error("Failed to sync secrets to GitLab");
}
}
/**
@@ -1680,7 +1618,6 @@ const syncSecretsSupabase = async ({
secrets: any;
accessToken: string;
}) => {
try {
const { data: getSecretsRes } = await standardRequest.get(
`${INTEGRATION_SUPABASE_API_URL}/v1/projects/${integration.appId}/secrets`,
{
@@ -1730,11 +1667,6 @@ const syncSecretsSupabase = async ({
data: secretsToDelete
}
);
} catch (err) {
Sentry.setUser(null);
Sentry.captureException(err);
throw new Error('Failed to sync secrets to Supabase');
}
};
@@ -1,3 +1,4 @@
import { Types } from 'mongoose';
import {
IUser,
IServiceAccount,
@@ -10,4 +11,5 @@ export interface AuthData {
authChannel: string;
authIP: string;
authUserAgent: string;
tokenVersionId?: Types.ObjectId;
}
+7 -4
View File
@@ -71,10 +71,12 @@ const requireAuth = ({
req.user = authPayload;
break;
default:
authPayload = await getAuthUserPayload({
const { user, tokenVersionId } = await getAuthUserPayload({
authTokenValue
});
req.user = authPayload;
authPayload = user;
req.user = user;
req.tokenVersionId = tokenVersionId;
break;
}
@@ -88,8 +90,9 @@ const requireAuth = ({
authMode,
authPayload, // User, ServiceAccount, ServiceTokenData
authChannel: getChannelFromUserAgent(req.headers['user-agent']),
authIP: req.ip,
authUserAgent: req.headers['user-agent'] ?? 'other'
authIP: req.realIP,
authUserAgent: req.headers['user-agent'] ?? 'other',
tokenVersionId: req.tokenVersionId
}
return next();
+4 -1
View File
@@ -22,6 +22,7 @@ import Workspace, { IWorkspace } from './workspace';
import ServiceTokenData, { IServiceTokenData } from './serviceTokenData';
import APIKeyData, { IAPIKeyData } from './apiKeyData';
import LoginSRPDetail, { ILoginSRPDetail } from './loginSRPDetail';
import TokenVersion, { ITokenVersion } from './tokenVersion';
export {
AuthProvider,
@@ -72,5 +73,7 @@ export {
APIKeyData,
IAPIKeyData,
LoginSRPDetail,
ILoginSRPDetail
ILoginSRPDetail,
TokenVersion,
ITokenVersion
};
+47
View File
@@ -0,0 +1,47 @@
import { Schema, model, Types, Document } from 'mongoose';
export interface ITokenVersion extends Document {
user: Types.ObjectId;
ip: string;
userAgent: string;
refreshVersion: number;
accessVersion: number;
lastUsed: Date;
}
const tokenVersionSchema = new Schema<ITokenVersion>(
{
user: {
type: Schema.Types.ObjectId,
ref: 'User',
required: true
},
ip: {
type: String,
required: true
},
userAgent: {
type: String,
required: true
},
refreshVersion: {
type: Number,
required: true
},
accessVersion: {
type: Number,
required: true
},
lastUsed: {
type: Date,
required: true
}
},
{
timestamps: true
}
);
const TokenVersion = model<ITokenVersion>('TokenVersion', tokenVersionSchema);
export default TokenVersion;
+2 -7
View File
@@ -21,7 +21,6 @@ export interface IUser extends Document {
tag?: string;
salt?: string;
verifier?: string;
refreshVersion?: number;
isMfaEnabled: boolean;
mfaMethods: boolean;
devices: {
@@ -91,11 +90,6 @@ const userSchema = new Schema<IUser>(
type: String,
select: false
},
refreshVersion: {
type: Number,
default: 0,
select: false
},
isMfaEnabled: {
type: Boolean,
default: false
@@ -108,7 +102,8 @@ const userSchema = new Schema<IUser>(
ip: String,
userAgent: String
}],
default: []
default: [],
select: false
}
},
{
+17 -4
View File
@@ -44,8 +44,6 @@ router.post(
authController.checkAuth
);
router.get(
'/redirect/google',
authLimiter,
@@ -53,12 +51,27 @@ router.get(
scope: ['profile', 'email'],
session: false,
}),
)
);
router.get(
'/callback/google',
passport.authenticate('google', { failureRedirect: '/login/provider/error', session: false }),
authController.handleAuthProviderCallback,
)
);
router.get(
'/common-passwords',
authLimiter,
authController.getCommonPasswords
);
router.delete(
'/sessions',
authLimiter,
requireAuth({
acceptedAuthModes: [AUTH_MODE_JWT]
}),
authController.revokeAllSessions
);
export default router;
+1 -1
View File
@@ -26,7 +26,7 @@ router.post(
router.post(
'/mfa/send',
authLimiter,
body('email').isString().trim().notEmpty(),
body('email').isString().trim().notEmpty().isEmail(),
validateRequest,
authController.sendMfaToken
);
+2 -2
View File
@@ -1,6 +1,6 @@
import { Types } from 'mongoose';
import {
getSecretsHelper,
getSecretsBotHelper,
encryptSymmetricHelper,
decryptSymmetricHelper
} from '../helpers/bot';
@@ -25,7 +25,7 @@ class BotService {
workspaceId: Types.ObjectId;
environment: string;
}) {
return await getSecretsHelper({
return await getSecretsBotHelper({
workspaceId,
environment
});
+3
View File
@@ -1,4 +1,5 @@
import * as express from 'express';
import { Types } from 'mongoose';
import {
IUser,
IServiceAccount,
@@ -39,7 +40,9 @@ declare global {
serviceTokenData: any;
apiKeyData: any;
query?: any;
tokenVersionId?: Types.ObjectId;
authData: AuthData;
realIP: string;
requestData: {
[key: string]: string
};
-69
View File
@@ -1,69 +0,0 @@
/*
Original work Copyright (c) 2016, Nikolay Nemshilov <nemshilov@gmail.com>
Modified work Copyright (c) 2016, David Banham <david@banham.id.au>
Permission to use, copy, modify, and/or distribute this software for any purpose
with or without fee is hereby granted, provided that the above copyright notice
and this permission notice appear in all copies.
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND
FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS
OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER
TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF
THIS SOFTWARE.
*/
/* eslint-disable @typescript-eslint/no-var-requires */
/* eslint-env node */
const Layer = require('express/lib/router/layer');
const Router = require('express/lib/router');
const last = (arr = []) => arr[arr.length - 1];
const noop = Function.prototype;
function copyFnProps(oldFn, newFn) {
Object.keys(oldFn).forEach((key) => {
newFn[key] = oldFn[key];
});
return newFn;
}
function wrap(fn) {
const newFn = function newFn(...args) {
const ret = fn.apply(this, args);
const next = (args.length === 5 ? args[2] : last(args)) || noop;
if (ret && ret.catch) ret.catch(err => next(err));
return ret;
};
Object.defineProperty(newFn, 'length', {
value: fn.length,
writable: false,
});
return copyFnProps(fn, newFn);
}
function patchRouterParam() {
const originalParam = Router.prototype.constructor.param;
Router.prototype.constructor.param = function param(name, fn) {
fn = wrap(fn);
return originalParam.call(this, name, fn);
};
}
Object.defineProperty(Layer.prototype, 'handle', {
enumerable: true,
get() {
return this.__handle;
},
set(fn) {
fn = wrap(fn);
this.__handle = fn;
},
});
module.exports = {
patchRouterParam
};
-2
View File
@@ -5,7 +5,6 @@ import { EELicenseService } from '../../ee/services';
import { initSmtp } from '../../services/smtp';
import { createTestUserForDevelopment } from '../addDevelopmentUser';
// eslint-disable-next-line @typescript-eslint/no-var-requires
const { patchRouterParam } = require('../patchAsyncRoutes');
import { validateEncryptionKeysConfig } from './validateConfig';
import {
backfillSecretVersions,
@@ -38,7 +37,6 @@ import { initializePassport } from '../auth';
* - Re-encrypting data
*/
export const setup = async () => {
patchRouterParam();
await validateEncryptionKeysConfig();
await TelemetryService.logTelemetryMessage();
+1
View File
@@ -1,3 +1,4 @@
export * from './user';
export * from './workspace';
export * from './bot';
export * from './integration';
+19 -1
View File
@@ -1,3 +1,5 @@
import fs from 'fs';
import path from 'path';
import { Types } from 'mongoose';
import {
IUser,
@@ -8,7 +10,7 @@ import {
} from '../models';
import { validateMembership } from '../helpers/membership';
import _ from 'lodash';
import { BadRequestError, UnauthorizedRequestError } from '../utils/errors';
import { BadRequestError, UnauthorizedRequestError, ValidationError } from '../utils/errors';
import {
validateMembershipOrg
} from '../helpers/membershipOrg';
@@ -17,6 +19,22 @@ import {
PERMISSION_WRITE_SECRETS
} from '../variables';
/**
* Validate that email [email] is not disposable
* @param email - email to validate
*/
export const validateUserEmail = (email: string) => {
const emailDomain = email.split('@')[1];
const disposableEmails = fs.readFileSync(
path.resolve(__dirname, '../data/' + 'disposable_emails.txt'),
'utf8'
).split('\n');
if (disposableEmails.includes(emailDomain)) throw ValidationError({
message: 'Failed to validate email as non-disposable'
});
}
/**
* Validate that user (client) can access workspace
* with id [workspaceId] and its environment [environment] with required permissions
-6
View File
@@ -121,12 +121,6 @@
{
"group": "Self-host Infisical",
"pages": [
{
"group": "Authentication",
"pages": [
"self-hosting/authentication/google"
]
},
{
"group": "Deployment options",
"pages": [
+6 -7
View File
@@ -3,17 +3,16 @@ title: "Configure email service"
description: "How to configure your email when self-hosting Infisical."
---
Infisical requires you to configure your own SMTP server for certain functionality like:
By default, the core functions of Infisical work without any email service configuration. Without email service, basic sign up/login and secret operations will function without any issue.
However, the following functionality will be disabled.
- Sending email confirmation links to sign up.
- Sending invite links for projects.
- Sending alerts.
We strongly recommend using an email service to act as your email server and provide examples for common providers.
- Multi-factor authentication
- Sending invite links via email for projects to teammates
- Sending alerts such as suspicious login attempts
## General configuration
By default, you need to configure the following SMTP [environment variables](https://infisical.com/docs/self-hosting/configuration/envars):
If you choose to setup email service, you need to configure the following SMTP [environment variables](https://infisical.com/docs/self-hosting/configuration/envars):
- `SMTP_HOST`: Hostname to connect to for establishing SMTP connections.
- `SMTP_USERNAME`: Credential to connect to host (e.g. [email protected])
+43 -48
View File
@@ -2,18 +2,19 @@ import crypto from 'crypto';
import React, { useState } from 'react';
import { useTranslation } from 'react-i18next';
import { faCheck, faXmark } from '@fortawesome/free-solid-svg-icons';
import { faXmark } from '@fortawesome/free-solid-svg-icons';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import jsrp from 'jsrp';
import nacl from 'tweetnacl';
import { encodeBase64 } from 'tweetnacl-util';
import { useGetCommonPasswords } from '@app/hooks/api';
import completeAccountInformationSignup from '@app/pages/api/auth/CompleteAccountInformationSignup';
import getOrganizations from '@app/pages/api/organization/getOrgs';
import ProjectService from '@app/services/ProjectService';
import InputField from '../basic/InputField';
import passwordCheck from '../utilities/checks/PasswordCheck';
import checkPassword from '../utilities/checks/checkPassword';
import Aes256Gcm from '../utilities/cryptography/aes-256-gcm';
import { deriveArgonKey } from '../utilities/cryptography/crypto';
import { saveTokenToLocalStorage } from '../utilities/saveTokenToLocalStorage';
@@ -37,6 +38,15 @@ interface UserInfoStepProps {
providerAuthToken?: string;
}
type Errors = {
length?: string,
upperCase?: string,
lowerCase?: string,
number?: string,
specialChar?: string,
repeatedChar?: string,
};
/**
* This is the step of the sign up flow where people provife their name/surname and password
* @param {object} obj
@@ -63,11 +73,11 @@ export default function UserInfoStep({
setAttributionSource,
providerAuthToken,
}: UserInfoStepProps): JSX.Element {
const { data: commonPasswords } = useGetCommonPasswords();
const [nameError, setNameError] = useState(false);
const [organizationNameError, setOrganizationNameError] = useState(false);
const [passwordErrorLength, setPasswordErrorLength] = useState(false);
const [passwordErrorNumber, setPasswordErrorNumber] = useState(false);
const [passwordErrorLowerCase, setPasswordErrorLowerCase] = useState(false);
const [errors, setErrors] = useState<Errors>({});
const [isLoading, setIsLoading] = useState(false);
const { t } = useTranslation();
@@ -89,12 +99,11 @@ export default function UserInfoStep({
} else {
setOrganizationNameError(false);
}
errorCheck = passwordCheck({
errorCheck = checkPassword({
password,
setPasswordErrorLength,
setPasswordErrorNumber,
setPasswordErrorLowerCase,
errorCheck
commonPasswords,
setErrors
});
if (!errorCheck) {
@@ -248,59 +257,45 @@ export default function UserInfoStep({
label={t('section.password.password')}
onChangeHandler={(pass: string) => {
setPassword(pass);
passwordCheck({
checkPassword({
password: pass,
setPasswordErrorLength,
setPasswordErrorNumber,
setPasswordErrorLowerCase,
errorCheck: false
commonPasswords,
setErrors
});
}}
type="password"
value={password}
isRequired
error={passwordErrorLength && passwordErrorNumber && passwordErrorLowerCase}
error={Object.keys(errors).length > 0}
autoComplete="new-password"
id="new-password"
/>
{passwordErrorLength || passwordErrorLowerCase || passwordErrorNumber ? (
{Object.keys(errors).length > 0 && (
<div className="mt-4 flex w-full flex-col items-start rounded-md bg-white/5 px-2 py-2">
<div className="mb-1 text-sm text-gray-400">{t('section.password.validate-base')}</div>
<div className="ml-1 flex flex-row items-center justify-start">
{passwordErrorLength ? (
<FontAwesomeIcon icon={faXmark} className="text-md text-red ml-0.5 mr-2.5" />
) : (
<FontAwesomeIcon icon={faCheck} className="text-md mr-2 text-primary" />
)}
<div className={`${passwordErrorLength ? 'text-gray-400' : 'text-gray-600'} text-sm`}>
{t('section.password.validate-length')}
</div>
</div>
<div className="ml-1 flex flex-row items-center justify-start">
{passwordErrorLowerCase ? (
<FontAwesomeIcon icon={faXmark} className="text-md text-red ml-0.5 mr-2.5" />
) : (
<FontAwesomeIcon icon={faCheck} className="text-md mr-2 text-primary" />
)}
<div className="mb-2 text-sm text-gray-400">{t('section.password.validate-base')}</div>
{Object.keys(errors).map((key) => {
if (errors[key as keyof Errors]) {
return (
<div
className={`${passwordErrorLowerCase ? 'text-gray-400' : 'text-gray-600'} text-sm`}
className="ml-1 flex flex-row items-top justify-start"
key={key}
>
{t('section.password.validate-case')}
<div>
<FontAwesomeIcon
icon={faXmark}
className="text-md text-red ml-0.5 mr-2.5"
/>
</div>
<p className="text-gray-400 text-sm">
{errors[key as keyof Errors]}
</p>
</div>
<div className="ml-1 flex flex-row items-center justify-start">
{passwordErrorNumber ? (
<FontAwesomeIcon icon={faXmark} className="text-md text-red ml-0.5 mr-2.5" />
) : (
<FontAwesomeIcon icon={faCheck} className="text-md mr-2 text-primary" />
)}
<div className={`${passwordErrorNumber ? 'text-gray-400' : 'text-gray-600'} text-sm`}>
{t('section.password.validate-number')}
);
}
return null;
})}
</div>
</div>
</div>
) : (
<div className="py-2" />
)}
</div>
<div className="flex flex-col items-center justify-center lg:w-[19%] w-1/4 min-w-[20rem] mt-2 max-w-xs md:max-w-md mx-auto text-sm text-center md:text-left">
@@ -17,6 +17,7 @@ const passwordCheck = ({
setPasswordErrorLowerCase,
errorCheck
}: PasswordCheckProps) => {
if (!password || password.length < 14) {
setPasswordErrorLength(true);
errorCheck = true;
@@ -0,0 +1,72 @@
type Errors = {
length?: string,
upperCase?: string,
lowerCase?: string,
number?: string,
specialChar?: string,
repeatedChar?: string,
commonPassword?: string
};
interface CheckPasswordParams {
password: string;
commonPasswords: string[];
setErrors: (value: Errors) => void;
}
/**
* Validate that the password [password] is at least:
* - 8 characters long
* - Contains 1 uppercase character (A-Z)
* - Contains 1 lowercase character (a-z)
* - Contains 1 number (0-9)
* - Does not contain 3 repeat, consecutive characters
*
* The function returns whether or not the password [password]
* passes the minimum requirements above. It sets errors on
* an erorr object via [setErrors].
*
* @param {Object} obj
* @param {String} obj.password - the password to check
* @param {Function} obj.setErrors - set state function to set error object
*/
const checkPassword = ({
password,
commonPasswords,
setErrors
}: CheckPasswordParams): boolean => {
const errors: Errors = {};
if (password.length < 8) {
errors.length = "8 characters";
}
if (!/[A-Z]/.test(password)) {
errors.upperCase = "1 uppercase character (A-Z)";
}
if (!/[a-z]/.test(password)) {
errors.lowerCase = "1 lowercase character (a-z)";
}
if (!/[0-9]/.test(password)) {
errors.number = "1 number (0-9)";
}
if (!/[!@#$%^&*(),.?":{}|<>]/.test(password)) {
errors.specialChar = "1 special character (!@#$%^&*(),.?)";
}
if (/([A-Za-z0-9])\1\1\1/.test(password)) {
errors.repeatedChar = "No 3 repeat, consecutive characters";
}
if (commonPasswords.includes(password)) {
errors.commonPassword = "No common passwords";
}
setErrors(errors);
return Object.keys(errors).length > 0;
}
export default checkPassword;
@@ -125,6 +125,10 @@ const changePassword = async (
setPasswordChanged(true);
setCurrentPassword('');
setNewPassword('');
window.location.href = '/login';
// move to login page
} catch (error) {
setCurrentPasswordError(true);
console.log(error);
@@ -73,10 +73,15 @@ export const DeleteActionModal = ({
}
onClose={onClose}
>
<form>
<form
onSubmit={(evt) => {
evt.preventDefault();
if (deleteKey === inputData) onDelete();
}}
>
<FormControl
label={
<div className="pb-2 text-sm break-words">
<div className="break-words pb-2 text-sm">
Type <span className="font-bold">{deleteKey}</span> to delete the resource
</div>
}
+4 -1
View File
@@ -1,4 +1,7 @@
export {
useGetAuthToken,
useSendMfaToken,
useVerifyMfaToken} from './queries'
useVerifyMfaToken,
useRevokeAllSessions,
useGetCommonPasswords
} from './queries'
+19 -1
View File
@@ -10,7 +10,8 @@ import {
VerifyMfaTokenRes} from './types';
const authKeys = {
getAuthToken: ['token'] as const
getAuthToken: ['token'] as const,
commonPasswords: ['common-passwords'] as const
};
export const useSendMfaToken = () => {
@@ -49,3 +50,20 @@ export const useGetAuthToken = () =>
onSuccess: (data) => setAuthToken(data.token),
retry: 0
});
export const useRevokeAllSessions = () => {
return useMutation({
mutationFn: async () => {
const { data } = await apiRequest.delete('/api/v1/auth/sessions');
return data;
}
});
}
const fetchCommonPasswords = async () => {
const { data } = await apiRequest.get('/api/v1/auth/common-passwords');
return data || [];
};
export const useGetCommonPasswords = () =>
useQuery({ queryKey: authKeys.commonPasswords, queryFn: fetchCommonPasswords });
+71 -64
View File
@@ -2,7 +2,7 @@ import { useEffect, useState } from 'react';
import { useTranslation } from 'react-i18next';
import Head from 'next/head';
import { useRouter } from 'next/router';
import { faCheck, faPlus, faX } from '@fortawesome/free-solid-svg-icons';
import { faBan,faCheck, faPlus, faXmark } from '@fortawesome/free-solid-svg-icons';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import Button from '@app/components/basic/buttons/Button';
@@ -10,21 +10,31 @@ import InputField from '@app/components/basic/InputField';
import ListBox from '@app/components/basic/Listbox';
import ApiKeyTable from '@app/components/basic/table/ApiKeyTable';
import NavHeader from '@app/components/navigation/NavHeader';
import passwordCheck from '@app/components/utilities/checks/PasswordCheck';
import checkPassword from '@app/components/utilities/checks/checkPassword';
import changePassword from '@app/components/utilities/cryptography/changePassword';
import issueBackupKey from '@app/components/utilities/cryptography/issueBackupKey';
import {
useGetCommonPasswords,
useRevokeAllSessions} from '@app/hooks/api';
import { SecuritySection } from '@app/views/Settings/PersonalSettingsPage/SecuritySection/SecuritySection';
import AddApiKeyDialog from '../../../components/basic/dialog/AddApiKeyDialog';
import getAPIKeys from '../../api/apiKey/getAPIKeys';
import getUser from '../../api/user/getUser';
type Errors = {
length?: string,
upperCase?: string,
lowerCase?: string,
number?: string,
specialChar?: string,
repeatedChar?: string,
};
export default function PersonalSettings() {
const { data: commonPasswords } = useGetCommonPasswords();
const [personalEmail, setPersonalEmail] = useState('');
const [personalName, setPersonalName] = useState('');
const [passwordErrorLength, setPasswordErrorLength] = useState(false);
const [passwordErrorNumber, setPasswordErrorNumber] = useState(false);
const [passwordErrorLowerCase, setPasswordErrorLowerCase] = useState(false);
const [currentPasswordError, setCurrentPasswordError] = useState(false);
const [currentPassword, setCurrentPassword] = useState('');
const [newPassword, setNewPassword] = useState('');
@@ -34,6 +44,9 @@ export default function PersonalSettings() {
const [backupKeyError, setBackupKeyError] = useState(false);
const [isAddApiKeyDialogOpen, setIsAddApiKeyDialogOpen] = useState(false);
const [apiKeys, setApiKeys] = useState<any[]>([]);
const [errors, setErrors] = useState<Errors>({});
const revokeAllSessions = useRevokeAllSessions();
const { t, i18n } = useTranslation();
const router = useRouter();
@@ -154,78 +167,54 @@ export default function PersonalSettings() {
label={t('section.password.new') as string}
onChangeHandler={(password) => {
setNewPassword(password);
passwordCheck({
checkPassword({
password,
setPasswordErrorLength,
setPasswordErrorNumber,
setPasswordErrorLowerCase,
errorCheck: false
commonPasswords,
setErrors
});
}}
type="password"
value={newPassword}
isRequired
error={passwordErrorLength && passwordErrorLowerCase && passwordErrorNumber}
error={Object.keys(errors).length > 0}
autoComplete="new-password"
id="new-password"
/>
</div>
{passwordErrorLength || passwordErrorLowerCase || passwordErrorNumber ? (
<div className="mt-3 mb-2 flex w-full max-w-xl flex-col items-start rounded-md bg-white/5 px-2 py-2">
<div className="mb-1 text-sm text-gray-400">
{t('section.password.validate-base')}
{Object.keys(errors).length > 0 && (
<div className="mt-4 flex w-full flex-col items-start rounded-md bg-white/5 px-2 py-2">
<div className="mb-2 text-sm text-gray-400">{t('section.password.validate-base')}</div>
{Object.keys(errors).map((key) => {
if (errors[key as keyof Errors]) {
return (
<div className="ml-1 flex flex-row items-top justify-start" key={key}>
<div>
<FontAwesomeIcon
icon={faXmark}
className="text-md text-red ml-0.5 mr-2.5"
/>
</div>
<div className="ml-1 flex flex-row items-center justify-start">
{passwordErrorLength ? (
<FontAwesomeIcon icon={faX} className="text-md mr-2.5 text-red" />
) : (
<FontAwesomeIcon icon={faCheck} className="text-md mr-2 text-primary" />
)}
<div
className={`${
passwordErrorLength ? 'text-gray-400' : 'text-gray-600'
} text-sm`}
>
{t('section.password.validate-length')}
<p className="text-gray-400 text-sm">
{errors[key as keyof Errors]}
</p>
</div>
);
}
return null;
})}
</div>
<div className="ml-1 flex flex-row items-center justify-start">
{passwordErrorLowerCase ? (
<FontAwesomeIcon icon={faX} className="text-md mr-2.5 text-red" />
) : (
<FontAwesomeIcon icon={faCheck} className="text-md mr-2 text-primary" />
)}
<div
className={`${
passwordErrorLowerCase ? 'text-gray-400' : 'text-gray-600'
} text-sm`}
>
{t('section.password.validate-case')}
</div>
</div>
<div className="ml-1 flex flex-row items-center justify-start">
{passwordErrorNumber ? (
<FontAwesomeIcon icon={faX} className="text-md mr-2.5 text-red" />
) : (
<FontAwesomeIcon icon={faCheck} className="text-md mr-2 text-primary" />
)}
<div
className={`${
passwordErrorNumber ? 'text-gray-400' : 'text-gray-600'
} text-sm`}
>
{t('section.password.validate-number')}
</div>
</div>
</div>
) : (
<div className="py-2" />
)}
<div className="mt-3 flex w-52 flex-row items-center pr-3">
<Button
text={t('section.password.change') as string}
onButtonPressed={() => {
if (!passwordErrorLength && !passwordErrorLowerCase && !passwordErrorNumber) {
const errorCheck = checkPassword({
password: newPassword,
commonPasswords,
setErrors
});
if (!errorCheck) {
changePassword(
personalEmail,
currentPassword,
@@ -237,13 +226,9 @@ export default function PersonalSettings() {
);
}
}}
active={Object.keys(errors).length === 0 && currentPassword !== ''}
color="mineshaft"
size="md"
active={
newPassword !== '' &&
currentPassword !== '' &&
!(passwordErrorLength || passwordErrorLowerCase || passwordErrorNumber)
}
textDisabled={t('section.password.change') as string}
/>
<FontAwesomeIcon
@@ -254,6 +239,28 @@ export default function PersonalSettings() {
/>
</div>
</div>
<div className="mb-6 mt-2 flex w-full flex-col items-start rounded-md bg-white/5 px-6 pb-6 pt-2">
<div className="my-4 flex w-full flex-row justify-between">
<p className="text-xl font-semibold w-full">
Sessions
</p>
<div className="w-40">
<Button
text="Revoke all"
onButtonPressed={async () => {
await revokeAllSessions.mutateAsync();
router.push('/login');
}}
color="mineshaft"
icon={faBan}
size="md"
/>
</div>
</div>
<p className="mb-5 text-sm text-mineshaft-300">
Logging into Infisical via browser or CLI creates a session. Revoking all sessions logs your account out all active sessions across all browsers and CLIs.
</p>
</div>
<div className="mt-2 mb-6 flex w-full flex-col items-start rounded-md bg-white/5 px-6 pt-5 pb-6">
<div className="flex w-full max-w-5xl flex-row items-center justify-between">
+46 -54
View File
@@ -7,7 +7,7 @@ import Head from 'next/head';
import Image from 'next/image';
import Link from 'next/link';
import { useRouter } from 'next/router';
import { faCheck, faWarning, faX } from '@fortawesome/free-solid-svg-icons';
import { faCheck, faWarning, faXmark } from '@fortawesome/free-solid-svg-icons';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import jsrp from 'jsrp';
import queryString from 'query-string';
@@ -17,7 +17,9 @@ import { encodeBase64 } from 'tweetnacl-util';
import Button from '@app/components/basic/buttons/Button';
import InputField from '@app/components/basic/InputField';
import attemptLogin from '@app/components/utilities/attemptLogin';
import passwordCheck from '@app/components/utilities/checks/PasswordCheck';
import checkPassword from '@app/components/utilities/checks/checkPassword';
import Aes256Gcm from '@app/components/utilities/cryptography/aes-256-gcm';
import { deriveArgonKey } from '@app/components/utilities/cryptography/crypto';
import issueBackupKey from '@app/components/utilities/cryptography/issueBackupKey';
@@ -25,28 +27,36 @@ import { saveTokenToLocalStorage } from '@app/components/utilities/saveTokenToLo
import SecurityClient from '@app/components/utilities/SecurityClient';
import getOrganizations from '@app/pages/api/organization/getOrgs';
import getOrganizationUserProjects from '@app/pages/api/organization/GetOrgUserProjects';
import {
useGetCommonPasswords
} from '@app/hooks/api';
import completeAccountInformationSignupInvite from './api/auth/CompleteAccountInformationSignupInvite';
import verifySignupInvite from './api/auth/VerifySignupInvite';
// eslint-disable-next-line new-cap
const client = new jsrp.client();
type Errors = {
length?: string,
upperCase?: string,
lowerCase?: string,
number?: string,
specialChar?: string,
repeatedChar?: string,
};
export default function SignupInvite() {
const { data: commonPasswords } = useGetCommonPasswords();
const [password, setPassword] = useState('');
const [firstName, setFirstName] = useState('');
const [lastName, setLastName] = useState('');
const [firstNameError, setFirstNameError] = useState(false);
const [lastNameError, setLastNameError] = useState(false);
const [passwordErrorLength, setPasswordErrorLength] = useState(false);
const [passwordErrorNumber, setPasswordErrorNumber] = useState(false);
const [passwordErrorLowerCase, setPasswordErrorLowerCase] = useState(false);
const [errorLogin, setErrorLogin] = useState(false);
const [isLoading, setIsLoading] = useState(false);
const [step, setStep] = useState(1);
const [backupKeyError, setBackupKeyError] = useState(false);
const [verificationToken, setVerificationToken] = useState('');
const [backupKeyIssued, setBackupKeyIssued] = useState(false);
const [errors, setErrors] = useState<Errors>({});
const router = useRouter();
const parsedUrl = queryString.parse(router.asPath.split('?')[1]);
@@ -70,12 +80,11 @@ export default function SignupInvite() {
} else {
setLastNameError(false);
}
errorCheck = passwordCheck({
errorCheck = checkPassword({
password,
setPasswordErrorLength,
setPasswordErrorNumber,
setPasswordErrorLowerCase,
errorCheck
commonPasswords,
setErrors
});
if (!errorCheck) {
@@ -252,59 +261,42 @@ export default function SignupInvite() {
label="Password"
onChangeHandler={(pass) => {
setPassword(pass);
passwordCheck({
checkPassword({
password: pass,
setPasswordErrorLength,
setPasswordErrorNumber,
setPasswordErrorLowerCase,
errorCheck: false
commonPasswords,
setErrors
});
}}
type="password"
value={password}
isRequired
error={passwordErrorLength && passwordErrorNumber && passwordErrorLowerCase}
error={Object.keys(errors).length > 0}
autoComplete="new-password"
id="new-password"
/>
{passwordErrorLength || passwordErrorLowerCase || passwordErrorNumber ? (
<div className="w-full mt-4 bg-white/5 px-2 flex flex-col items-start py-2 rounded-md">
<div className="text-gray-400 text-sm mb-1">Password should contain at least:</div>
<div className="flex flex-row justify-start items-center ml-1">
{passwordErrorLength ? (
<FontAwesomeIcon icon={faX} className="text-md text-red mr-2.5" />
) : (
<FontAwesomeIcon icon={faCheck} className="text-md text-primary mr-2" />
)}
<div className={`${passwordErrorLength ? 'text-gray-400' : 'text-gray-600'} text-sm`}>
14 characters
{Object.keys(errors).length > 0 && (
<div className="mt-4 flex w-full flex-col items-start rounded-md bg-white/5 px-2 py-2">
<div className="mb-2 text-sm text-gray-400">Password should contain at least:</div>
{Object.keys(errors).map((key) => {
if (errors[key as keyof Errors]) {
return (
<div className="ml-1 flex flex-row items-top justify-start" key={key}>
<div>
<FontAwesomeIcon
icon={faXmark}
className="text-md text-red ml-0.5 mr-2.5"
/>
</div>
<p className="text-gray-400 text-sm">
{errors[key as keyof Errors]}
</p>
</div>
<div className="flex flex-row justify-start items-center ml-1">
{passwordErrorLowerCase ? (
<FontAwesomeIcon icon={faX} className="text-md text-red mr-2.5" />
) : (
<FontAwesomeIcon icon={faCheck} className="text-md text-primary mr-2" />
)}
<div
className={`${passwordErrorLowerCase ? 'text-gray-400' : 'text-gray-600'} text-sm`}
>
1 lowercase character
);
}
return null;
})}
</div>
</div>
<div className="flex flex-row justify-start items-center ml-1">
{passwordErrorNumber ? (
<FontAwesomeIcon icon={faX} className="text-md text-red mr-2.5" />
) : (
<FontAwesomeIcon icon={faCheck} className="text-md text-primary mr-2" />
)}
<div className={`${passwordErrorNumber ? 'text-gray-400' : 'text-gray-600'} text-sm`}>
1 number
</div>
</div>
</div>
) : (
<div className="py-2" />
)}
</div>
<div className="flex flex-col items-center justify-center md:px-4 md:py-5 mt-2 px-2 py-3 max-h-24 max-w-max mx-auto text-lg">