mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-09-22 13:39:35 +00:00
Merge branch 'main' into checkly-integration
This commit is contained in:
1674
backend/package-lock.json
generated
1674
backend/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -20,6 +20,7 @@
|
|||||||
"crypto-js": "^4.1.1",
|
"crypto-js": "^4.1.1",
|
||||||
"dotenv": "^16.0.1",
|
"dotenv": "^16.0.1",
|
||||||
"express": "^4.18.1",
|
"express": "^4.18.1",
|
||||||
|
"express-async-errors": "^3.1.1",
|
||||||
"express-rate-limit": "^6.7.0",
|
"express-rate-limit": "^6.7.0",
|
||||||
"express-validator": "^6.14.2",
|
"express-validator": "^6.14.2",
|
||||||
"handlebars": "^4.7.7",
|
"handlebars": "^4.7.7",
|
||||||
@@ -31,15 +32,14 @@
|
|||||||
"libsodium-wrappers": "^0.7.10",
|
"libsodium-wrappers": "^0.7.10",
|
||||||
"lodash": "^4.17.21",
|
"lodash": "^4.17.21",
|
||||||
"mongoose": "^6.10.5",
|
"mongoose": "^6.10.5",
|
||||||
"node-cache": "^5.1.2",
|
|
||||||
"nanoid": "^3.3.6",
|
"nanoid": "^3.3.6",
|
||||||
|
"node-cache": "^5.1.2",
|
||||||
"nodemailer": "^6.8.0",
|
"nodemailer": "^6.8.0",
|
||||||
"passport": "^0.6.0",
|
"passport": "^0.6.0",
|
||||||
"passport-google-oauth20": "^2.0.0",
|
"passport-google-oauth20": "^2.0.0",
|
||||||
"posthog-node": "^2.6.0",
|
"posthog-node": "^2.6.0",
|
||||||
"query-string": "^7.1.3",
|
"query-string": "^7.1.3",
|
||||||
"rate-limit-mongo": "^2.3.2",
|
"rate-limit-mongo": "^2.3.2",
|
||||||
"request-ip": "^3.3.0",
|
|
||||||
"rimraf": "^3.0.2",
|
"rimraf": "^3.0.2",
|
||||||
"stripe": "^10.7.0",
|
"stripe": "^10.7.0",
|
||||||
"swagger-autogen": "^2.22.0",
|
"swagger-autogen": "^2.22.0",
|
||||||
@@ -58,7 +58,7 @@
|
|||||||
"start": "node build/index.js",
|
"start": "node build/index.js",
|
||||||
"dev": "nodemon",
|
"dev": "nodemon",
|
||||||
"swagger-autogen": "node ./swagger/index.ts",
|
"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": "eslint . --ext .ts",
|
||||||
"lint-and-fix": "eslint . --ext .ts --fix",
|
"lint-and-fix": "eslint . --ext .ts --fix",
|
||||||
"lint-staged": "lint-staged",
|
"lint-staged": "lint-staged",
|
||||||
|
|||||||
@@ -1,19 +1,28 @@
|
|||||||
import * as Sentry from '@sentry/node';
|
|
||||||
import { Request, Response } from 'express';
|
import { Request, Response } from 'express';
|
||||||
|
import fs from 'fs';
|
||||||
|
import path from 'path';
|
||||||
import jwt from 'jsonwebtoken';
|
import jwt from 'jsonwebtoken';
|
||||||
import * as bigintConversion from 'bigint-conversion';
|
import * as bigintConversion from 'bigint-conversion';
|
||||||
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
||||||
const jsrp = require('jsrp');
|
const jsrp = require('jsrp');
|
||||||
import { User, LoginSRPDetail } from '../../models';
|
import {
|
||||||
|
User,
|
||||||
|
LoginSRPDetail,
|
||||||
|
TokenVersion
|
||||||
|
} from '../../models';
|
||||||
import { createToken, issueAuthTokens, clearTokens } from '../../helpers/auth';
|
import { createToken, issueAuthTokens, clearTokens } from '../../helpers/auth';
|
||||||
import { checkUserDevice } from '../../helpers/user';
|
import { checkUserDevice } from '../../helpers/user';
|
||||||
import {
|
import {
|
||||||
ACTION_LOGIN,
|
ACTION_LOGIN,
|
||||||
ACTION_LOGOUT
|
ACTION_LOGOUT,
|
||||||
|
AUTH_MODE_JWT
|
||||||
} from '../../variables';
|
} from '../../variables';
|
||||||
import { BadRequestError } from '../../utils/errors';
|
import {
|
||||||
|
BadRequestError,
|
||||||
|
UnauthorizedRequestError
|
||||||
|
} from '../../utils/errors';
|
||||||
import { EELogService } from '../../ee/services';
|
import { EELogService } from '../../ee/services';
|
||||||
import { getChannelFromUserAgent } from '../../utils/posthog'; // TODO: move this
|
import { getChannelFromUserAgent } from '../../utils/posthog';
|
||||||
import {
|
import {
|
||||||
getJwtRefreshSecret,
|
getJwtRefreshSecret,
|
||||||
getJwtAuthLifetime,
|
getJwtAuthLifetime,
|
||||||
@@ -24,6 +33,7 @@ import {
|
|||||||
declare module 'jsonwebtoken' {
|
declare module 'jsonwebtoken' {
|
||||||
export interface UserIDJwtPayload extends jwt.JwtPayload {
|
export interface UserIDJwtPayload extends jwt.JwtPayload {
|
||||||
userId: string;
|
userId: string;
|
||||||
|
refreshVersion?: number;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -34,47 +44,39 @@ declare module 'jsonwebtoken' {
|
|||||||
* @returns
|
* @returns
|
||||||
*/
|
*/
|
||||||
export const login1 = async (req: Request, res: Response) => {
|
export const login1 = async (req: Request, res: Response) => {
|
||||||
try {
|
const {
|
||||||
const {
|
email,
|
||||||
email,
|
clientPublicKey
|
||||||
clientPublicKey
|
}: { email: string; clientPublicKey: string } = req.body;
|
||||||
}: { email: string; clientPublicKey: string } = req.body;
|
|
||||||
|
|
||||||
const user = await User.findOne({
|
const user = await User.findOne({
|
||||||
email
|
email
|
||||||
}).select('+salt +verifier');
|
}).select('+salt +verifier');
|
||||||
|
|
||||||
if (!user) throw new Error('Failed to find user');
|
if (!user) throw new Error('Failed to find user');
|
||||||
|
|
||||||
const server = new jsrp.server();
|
const server = new jsrp.server();
|
||||||
server.init(
|
server.init(
|
||||||
{
|
{
|
||||||
salt: user.salt,
|
salt: user.salt,
|
||||||
verifier: user.verifier
|
verifier: user.verifier
|
||||||
},
|
},
|
||||||
async () => {
|
async () => {
|
||||||
// generate server-side public key
|
// generate server-side public key
|
||||||
const serverPublicKey = server.getPublicKey();
|
const serverPublicKey = server.getPublicKey();
|
||||||
|
|
||||||
await LoginSRPDetail.findOneAndReplace({ email: email }, {
|
await LoginSRPDetail.findOneAndReplace({ email: email }, {
|
||||||
email: email,
|
email: email,
|
||||||
clientPublicKey: clientPublicKey,
|
clientPublicKey: clientPublicKey,
|
||||||
serverBInt: bigintConversion.bigintToBuf(server.bInt),
|
serverBInt: bigintConversion.bigintToBuf(server.bInt),
|
||||||
}, { upsert: true, returnNewDocument: false })
|
}, { upsert: true, returnNewDocument: false })
|
||||||
|
|
||||||
return res.status(200).send({
|
return res.status(200).send({
|
||||||
serverPublicKey,
|
serverPublicKey,
|
||||||
salt: user.salt
|
salt: user.salt
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
} catch (err) {
|
|
||||||
Sentry.setUser(null);
|
|
||||||
Sentry.captureException(err);
|
|
||||||
return res.status(400).send({
|
|
||||||
message: 'Failed to start authentication process'
|
|
||||||
});
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -85,84 +87,80 @@ export const login1 = async (req: Request, res: Response) => {
|
|||||||
* @returns
|
* @returns
|
||||||
*/
|
*/
|
||||||
export const login2 = async (req: Request, res: Response) => {
|
export const login2 = async (req: Request, res: Response) => {
|
||||||
try {
|
const { email, clientProof } = req.body;
|
||||||
const { email, clientProof } = req.body;
|
const user = await User.findOne({
|
||||||
const user = await User.findOne({
|
email
|
||||||
email
|
}).select('+salt +verifier +publicKey +encryptedPrivateKey +iv +tag');
|
||||||
}).select('+salt +verifier +publicKey +encryptedPrivateKey +iv +tag');
|
|
||||||
|
|
||||||
if (!user) throw new Error('Failed to find user');
|
if (!user) throw new Error('Failed to find user');
|
||||||
|
|
||||||
const loginSRPDetailFromDB = await LoginSRPDetail.findOneAndDelete({ email: email })
|
const loginSRPDetailFromDB = await LoginSRPDetail.findOneAndDelete({ email: email })
|
||||||
|
|
||||||
if (!loginSRPDetailFromDB) {
|
if (!loginSRPDetailFromDB) {
|
||||||
return BadRequestError(Error("It looks like some details from the first login are not found. Please try login one again"))
|
return BadRequestError(Error("It looks like some details from the first login are not found. Please try login one again"))
|
||||||
}
|
}
|
||||||
|
|
||||||
const server = new jsrp.server();
|
const server = new jsrp.server();
|
||||||
server.init(
|
server.init(
|
||||||
{
|
{
|
||||||
salt: user.salt,
|
salt: user.salt,
|
||||||
verifier: user.verifier,
|
verifier: user.verifier,
|
||||||
b: loginSRPDetailFromDB.serverBInt
|
b: loginSRPDetailFromDB.serverBInt
|
||||||
},
|
},
|
||||||
async () => {
|
async () => {
|
||||||
server.setClientPublicKey(loginSRPDetailFromDB.clientPublicKey);
|
server.setClientPublicKey(loginSRPDetailFromDB.clientPublicKey);
|
||||||
|
|
||||||
// compare server and client shared keys
|
// compare server and client shared keys
|
||||||
if (server.checkClientProof(clientProof)) {
|
if (server.checkClientProof(clientProof)) {
|
||||||
// issue tokens
|
// issue tokens
|
||||||
|
|
||||||
await checkUserDevice({
|
await checkUserDevice({
|
||||||
user,
|
user,
|
||||||
ip: req.ip,
|
ip: req.realIP,
|
||||||
userAgent: req.headers['user-agent'] ?? ''
|
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
|
// store (refresh) token in httpOnly cookie
|
||||||
res.cookie('jid', tokens.refreshToken, {
|
res.cookie('jid', tokens.refreshToken, {
|
||||||
httpOnly: true,
|
httpOnly: true,
|
||||||
path: '/',
|
path: '/',
|
||||||
sameSite: 'strict',
|
sameSite: 'strict',
|
||||||
secure: await getHttpsEnabled()
|
secure: await getHttpsEnabled()
|
||||||
});
|
});
|
||||||
|
|
||||||
const loginAction = await EELogService.createAction({
|
const loginAction = await EELogService.createAction({
|
||||||
name: ACTION_LOGIN,
|
name: ACTION_LOGIN,
|
||||||
userId: user._id
|
userId: user._id
|
||||||
});
|
});
|
||||||
|
|
||||||
loginAction && await EELogService.createLog({
|
loginAction && await EELogService.createLog({
|
||||||
userId: user._id,
|
userId: user._id,
|
||||||
actions: [loginAction],
|
actions: [loginAction],
|
||||||
channel: getChannelFromUserAgent(req.headers['user-agent']),
|
channel: getChannelFromUserAgent(req.headers['user-agent']),
|
||||||
ipAddress: req.ip
|
ipAddress: req.realIP
|
||||||
});
|
});
|
||||||
|
|
||||||
// return (access) token in response
|
// return (access) token in response
|
||||||
return res.status(200).send({
|
return res.status(200).send({
|
||||||
token: tokens.token,
|
token: tokens.token,
|
||||||
publicKey: user.publicKey,
|
publicKey: user.publicKey,
|
||||||
encryptedPrivateKey: user.encryptedPrivateKey,
|
encryptedPrivateKey: user.encryptedPrivateKey,
|
||||||
iv: user.iv,
|
iv: user.iv,
|
||||||
tag: user.tag
|
tag: user.tag
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return res.status(400).send({
|
|
||||||
message: 'Failed to authenticate. Try again?'
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
);
|
|
||||||
} catch (err) {
|
return res.status(400).send({
|
||||||
Sentry.setUser(null);
|
message: 'Failed to authenticate. Try again?'
|
||||||
Sentry.captureException(err);
|
});
|
||||||
return res.status(400).send({
|
}
|
||||||
message: 'Failed to authenticate. Try again?'
|
);
|
||||||
});
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -172,44 +170,59 @@ export const login2 = async (req: Request, res: Response) => {
|
|||||||
* @returns
|
* @returns
|
||||||
*/
|
*/
|
||||||
export const logout = async (req: Request, res: Response) => {
|
export const logout = async (req: Request, res: Response) => {
|
||||||
try {
|
if (req.authData.authMode === AUTH_MODE_JWT && req.authData.authPayload instanceof User && req.authData.tokenVersionId) {
|
||||||
await clearTokens({
|
await clearTokens(req.authData.tokenVersionId)
|
||||||
userId: req.user._id.toString()
|
|
||||||
});
|
|
||||||
|
|
||||||
// clear httpOnly cookie
|
|
||||||
res.cookie('jid', '', {
|
|
||||||
httpOnly: true,
|
|
||||||
path: '/',
|
|
||||||
sameSite: 'strict',
|
|
||||||
secure: (await getHttpsEnabled()) as boolean
|
|
||||||
});
|
|
||||||
|
|
||||||
const logoutAction = await EELogService.createAction({
|
|
||||||
name: ACTION_LOGOUT,
|
|
||||||
userId: req.user._id
|
|
||||||
});
|
|
||||||
|
|
||||||
logoutAction && await EELogService.createLog({
|
|
||||||
userId: req.user._id,
|
|
||||||
actions: [logoutAction],
|
|
||||||
channel: getChannelFromUserAgent(req.headers['user-agent']),
|
|
||||||
ipAddress: req.ip
|
|
||||||
});
|
|
||||||
|
|
||||||
} catch (err) {
|
|
||||||
Sentry.setUser({ email: req.user.email });
|
|
||||||
Sentry.captureException(err);
|
|
||||||
return res.status(400).send({
|
|
||||||
message: 'Failed to logout'
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// clear httpOnly cookie
|
||||||
|
res.cookie('jid', '', {
|
||||||
|
httpOnly: true,
|
||||||
|
path: '/',
|
||||||
|
sameSite: 'strict',
|
||||||
|
secure: (await getHttpsEnabled()) as boolean
|
||||||
|
});
|
||||||
|
|
||||||
|
const logoutAction = await EELogService.createAction({
|
||||||
|
name: ACTION_LOGOUT,
|
||||||
|
userId: req.user._id
|
||||||
|
});
|
||||||
|
|
||||||
|
logoutAction && await EELogService.createLog({
|
||||||
|
userId: req.user._id,
|
||||||
|
actions: [logoutAction],
|
||||||
|
channel: getChannelFromUserAgent(req.headers['user-agent']),
|
||||||
|
ipAddress: req.realIP
|
||||||
|
});
|
||||||
|
|
||||||
return res.status(200).send({
|
return res.status(200).send({
|
||||||
message: 'Successfully logged out.'
|
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
|
* Return user is authenticated
|
||||||
* @param req
|
* @param req
|
||||||
@@ -223,49 +236,53 @@ 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 req
|
||||||
* @param res
|
* @param res
|
||||||
* @returns
|
* @returns
|
||||||
*/
|
*/
|
||||||
export const getNewToken = async (req: Request, res: Response) => {
|
export const getNewToken = async (req: Request, res: Response) => {
|
||||||
try {
|
const refreshToken = req.cookies.jid;
|
||||||
const refreshToken = req.cookies.jid;
|
|
||||||
|
|
||||||
if (!refreshToken) {
|
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>(
|
|
||||||
jwt.verify(refreshToken, await getJwtRefreshSecret())
|
|
||||||
);
|
|
||||||
|
|
||||||
const user = await User.findOne({
|
|
||||||
_id: decodedToken.userId
|
|
||||||
}).select('+publicKey');
|
|
||||||
|
|
||||||
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 token = createToken({
|
|
||||||
payload: {
|
|
||||||
userId: decodedToken.userId
|
|
||||||
},
|
|
||||||
expiresIn: await getJwtAuthLifetime(),
|
|
||||||
secret: await getJwtAuthSecret()
|
|
||||||
});
|
|
||||||
|
|
||||||
return res.status(200).send({
|
|
||||||
token
|
|
||||||
});
|
|
||||||
} catch (err) {
|
|
||||||
Sentry.setUser(null);
|
|
||||||
Sentry.captureException(err);
|
|
||||||
return res.status(400).send({
|
|
||||||
message: 'Invalid request'
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const decodedToken = <jwt.UserIDJwtPayload>(
|
||||||
|
jwt.verify(refreshToken, await getJwtRefreshSecret())
|
||||||
|
);
|
||||||
|
|
||||||
|
const user = await User.findOne({
|
||||||
|
_id: decodedToken.userId
|
||||||
|
}).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,
|
||||||
|
tokenVersionId: tokenVersion._id.toString(),
|
||||||
|
accessVersion: tokenVersion.refreshVersion
|
||||||
|
},
|
||||||
|
expiresIn: await getJwtAuthLifetime(),
|
||||||
|
secret: await getJwtAuthSecret()
|
||||||
|
});
|
||||||
|
|
||||||
|
return res.status(200).send({
|
||||||
|
token
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
export const handleAuthProviderCallback = (req: Request, res: Response) => {
|
export const handleAuthProviderCallback = (req: Request, res: Response) => {
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
import { Request, Response } from 'express';
|
import { Request, Response } from 'express';
|
||||||
import { Types } from 'mongoose';
|
import { Types } from 'mongoose';
|
||||||
import * as Sentry from '@sentry/node';
|
|
||||||
import { Bot, BotKey } from '../../models';
|
import { Bot, BotKey } from '../../models';
|
||||||
import { createBot } from '../../helpers/bot';
|
import { createBot } from '../../helpers/bot';
|
||||||
|
|
||||||
@@ -17,33 +16,24 @@ interface BotKey {
|
|||||||
* @returns
|
* @returns
|
||||||
*/
|
*/
|
||||||
export const getBotByWorkspaceId = async (req: Request, res: Response) => {
|
export const getBotByWorkspaceId = async (req: Request, res: Response) => {
|
||||||
let bot;
|
const { workspaceId } = req.params;
|
||||||
try {
|
|
||||||
const { workspaceId } = req.params;
|
|
||||||
|
|
||||||
bot = await Bot.findOne({
|
let bot = await Bot.findOne({
|
||||||
workspace: workspaceId
|
workspace: workspaceId
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!bot) {
|
if (!bot) {
|
||||||
// case: bot doesn't exist for workspace with id [workspaceId]
|
// case: bot doesn't exist for workspace with id [workspaceId]
|
||||||
// -> create a new bot and return it
|
// -> create a new bot and return it
|
||||||
bot = await createBot({
|
bot = await createBot({
|
||||||
name: 'Infisical Bot',
|
name: 'Infisical Bot',
|
||||||
workspaceId: new Types.ObjectId(workspaceId)
|
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({
|
return res.status(200).send({
|
||||||
bot
|
bot
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -53,54 +43,44 @@ export const getBotByWorkspaceId = async (req: Request, res: Response) => {
|
|||||||
* @returns
|
* @returns
|
||||||
*/
|
*/
|
||||||
export const setBotActiveState = async (req: Request, res: Response) => {
|
export const setBotActiveState = async (req: Request, res: Response) => {
|
||||||
let bot;
|
const { isActive, botKey }: { isActive: boolean, botKey: BotKey } = req.body;
|
||||||
try {
|
|
||||||
const { isActive, botKey }: { isActive: boolean, botKey: BotKey } = req.body;
|
if (isActive) {
|
||||||
|
// bot state set to active -> share workspace key with bot
|
||||||
if (isActive) {
|
if (!botKey?.encryptedKey || !botKey?.nonce) {
|
||||||
// bot state set to active -> share workspace key with bot
|
return res.status(400).send({
|
||||||
if (!botKey?.encryptedKey || !botKey?.nonce) {
|
message: 'Failed to set bot state to active - missing bot key'
|
||||||
return res.status(400).send({
|
|
||||||
message: 'Failed to set bot state to active - missing bot key'
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
await BotKey.findOneAndUpdate({
|
|
||||||
workspace: req.bot.workspace
|
|
||||||
}, {
|
|
||||||
encryptedKey: botKey.encryptedKey,
|
|
||||||
nonce: botKey.nonce,
|
|
||||||
sender: req.user._id,
|
|
||||||
bot: req.bot._id,
|
|
||||||
workspace: req.bot.workspace
|
|
||||||
}, {
|
|
||||||
upsert: true,
|
|
||||||
new: true
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
// case: bot state set to inactive -> delete bot's workspace key
|
|
||||||
await BotKey.deleteOne({
|
|
||||||
bot: req.bot._id
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
bot = await Bot.findOneAndUpdate({
|
await BotKey.findOneAndUpdate({
|
||||||
_id: req.bot._id
|
workspace: req.bot.workspace
|
||||||
}, {
|
}, {
|
||||||
isActive
|
encryptedKey: botKey.encryptedKey,
|
||||||
|
nonce: botKey.nonce,
|
||||||
|
sender: req.user._id,
|
||||||
|
bot: req.bot._id,
|
||||||
|
workspace: req.bot.workspace
|
||||||
}, {
|
}, {
|
||||||
|
upsert: true,
|
||||||
new: true
|
new: true
|
||||||
});
|
});
|
||||||
|
} else {
|
||||||
if (!bot) throw new Error('Failed to update bot active state');
|
// case: bot state set to inactive -> delete bot's workspace key
|
||||||
|
await BotKey.deleteOne({
|
||||||
} catch (err) {
|
bot: req.bot._id
|
||||||
Sentry.setUser({ email: req.user.email });
|
});
|
||||||
Sentry.captureException(err);
|
}
|
||||||
return res.status(400).send({
|
|
||||||
message: 'Failed to update bot active state'
|
let bot = await Bot.findOneAndUpdate({
|
||||||
});
|
_id: req.bot._id
|
||||||
}
|
}, {
|
||||||
|
isActive
|
||||||
|
}, {
|
||||||
|
new: true
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!bot) throw new Error('Failed to update bot active state');
|
||||||
|
|
||||||
return res.status(200).send({
|
return res.status(200).send({
|
||||||
bot
|
bot
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
import { Request, Response } from 'express';
|
import { Request, Response } from 'express';
|
||||||
import { Types } from 'mongoose';
|
import { Types } from 'mongoose';
|
||||||
import * as Sentry from '@sentry/node';
|
|
||||||
import {
|
import {
|
||||||
IntegrationAuth,
|
IntegrationAuth,
|
||||||
Bot
|
Bot
|
||||||
@@ -22,22 +21,13 @@ import { standardRequest } from '../../config/request';
|
|||||||
* Return integration authorization with id [integrationAuthId]
|
* Return integration authorization with id [integrationAuthId]
|
||||||
*/
|
*/
|
||||||
export const getIntegrationAuth = async (req: Request, res: Response) => {
|
export const getIntegrationAuth = async (req: Request, res: Response) => {
|
||||||
let integrationAuth;
|
const { integrationAuthId } = req.params;
|
||||||
try {
|
const integrationAuth = await IntegrationAuth.findById(integrationAuthId);
|
||||||
const { integrationAuthId } = req.params;
|
|
||||||
integrationAuth = await IntegrationAuth.findById(integrationAuthId);
|
if (!integrationAuth) return res.status(400).send({
|
||||||
|
message: 'Failed to find integration authorization'
|
||||||
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({
|
return res.status(200).send({
|
||||||
integrationAuth
|
integrationAuth
|
||||||
});
|
});
|
||||||
@@ -61,33 +51,25 @@ export const oAuthExchange = async (
|
|||||||
req: Request,
|
req: Request,
|
||||||
res: Response
|
res: Response
|
||||||
) => {
|
) => {
|
||||||
try {
|
const { workspaceId, code, integration } = req.body;
|
||||||
const { workspaceId, code, integration } = req.body;
|
if (!INTEGRATION_SET.has(integration))
|
||||||
if (!INTEGRATION_SET.has(integration))
|
throw new Error('Failed to validate integration');
|
||||||
throw new Error('Failed to validate integration');
|
|
||||||
|
const environments = req.membership.workspace?.environments || [];
|
||||||
const environments = req.membership.workspace?.environments || [];
|
if(environments.length === 0){
|
||||||
if(environments.length === 0){
|
throw new Error("Failed to get environments")
|
||||||
throw new Error("Failed to get environments")
|
}
|
||||||
}
|
|
||||||
|
const integrationAuth = await IntegrationService.handleOAuthExchange({
|
||||||
const integrationAuth = await IntegrationService.handleOAuthExchange({
|
workspaceId,
|
||||||
workspaceId,
|
integration,
|
||||||
integration,
|
code,
|
||||||
code,
|
environment: environments[0].slug,
|
||||||
environment: environments[0].slug,
|
});
|
||||||
});
|
|
||||||
|
return res.status(200).send({
|
||||||
return res.status(200).send({
|
integrationAuth
|
||||||
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,55 +86,47 @@ export const saveIntegrationAccessToken = async (
|
|||||||
// TODO: check if access token is valid for each integration
|
// TODO: check if access token is valid for each integration
|
||||||
|
|
||||||
let integrationAuth;
|
let integrationAuth;
|
||||||
try {
|
const {
|
||||||
const {
|
workspaceId,
|
||||||
workspaceId,
|
accessId,
|
||||||
accessId,
|
accessToken,
|
||||||
accessToken,
|
integration
|
||||||
integration
|
}: {
|
||||||
}: {
|
workspaceId: string;
|
||||||
workspaceId: string;
|
accessId: string | null;
|
||||||
accessId: string | null;
|
accessToken: string;
|
||||||
accessToken: string;
|
integration: string;
|
||||||
integration: string;
|
} = req.body;
|
||||||
} = req.body;
|
|
||||||
|
|
||||||
const bot = await Bot.findOne({
|
const bot = await Bot.findOne({
|
||||||
workspace: new Types.ObjectId(workspaceId),
|
workspace: new Types.ObjectId(workspaceId),
|
||||||
isActive: true
|
isActive: true
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!bot) throw new Error('Bot must be enabled to save integration access token');
|
if (!bot) throw new Error('Bot must be enabled to save integration access token');
|
||||||
|
|
||||||
integrationAuth = await IntegrationAuth.findOneAndUpdate({
|
integrationAuth = await IntegrationAuth.findOneAndUpdate({
|
||||||
workspace: new Types.ObjectId(workspaceId),
|
workspace: new Types.ObjectId(workspaceId),
|
||||||
integration
|
integration
|
||||||
}, {
|
}, {
|
||||||
workspace: new Types.ObjectId(workspaceId),
|
workspace: new Types.ObjectId(workspaceId),
|
||||||
integration,
|
integration,
|
||||||
algorithm: ALGORITHM_AES_256_GCM,
|
algorithm: ALGORITHM_AES_256_GCM,
|
||||||
keyEncoding: ENCODING_SCHEME_UTF8
|
keyEncoding: ENCODING_SCHEME_UTF8
|
||||||
}, {
|
}, {
|
||||||
new: true,
|
new: true,
|
||||||
upsert: true
|
upsert: true
|
||||||
});
|
});
|
||||||
|
|
||||||
// encrypt and save integration access details
|
// encrypt and save integration access details
|
||||||
integrationAuth = await IntegrationService.setIntegrationAuthAccess({
|
integrationAuth = await IntegrationService.setIntegrationAuthAccess({
|
||||||
integrationAuthId: integrationAuth._id.toString(),
|
integrationAuthId: integrationAuth._id.toString(),
|
||||||
accessId,
|
accessId,
|
||||||
accessToken,
|
accessToken,
|
||||||
accessExpiresAt: undefined
|
accessExpiresAt: undefined
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!integrationAuth) throw new Error('Failed to save integration access token');
|
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({
|
return res.status(200).send({
|
||||||
integrationAuth
|
integrationAuth
|
||||||
@@ -166,22 +140,13 @@ export const saveIntegrationAccessToken = async (
|
|||||||
* @returns
|
* @returns
|
||||||
*/
|
*/
|
||||||
export const getIntegrationAuthApps = async (req: Request, res: Response) => {
|
export const getIntegrationAuthApps = async (req: Request, res: Response) => {
|
||||||
let apps;
|
const teamId = req.query.teamId as string;
|
||||||
try {
|
|
||||||
const teamId = req.query.teamId as string;
|
const apps = await getApps({
|
||||||
|
integrationAuth: req.integrationAuth,
|
||||||
apps = await getApps({
|
accessToken: req.accessToken,
|
||||||
integrationAuth: req.integrationAuth,
|
...teamId && { teamId }
|
||||||
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({
|
return res.status(200).send({
|
||||||
apps
|
apps
|
||||||
@@ -402,19 +367,10 @@ export const getIntegrationAuthRailwayServices = async (req: Request, res: Respo
|
|||||||
* @returns
|
* @returns
|
||||||
*/
|
*/
|
||||||
export const deleteIntegrationAuth = async (req: Request, res: Response) => {
|
export const deleteIntegrationAuth = async (req: Request, res: Response) => {
|
||||||
let integrationAuth;
|
const integrationAuth = await revokeAccess({
|
||||||
try {
|
integrationAuth: req.integrationAuth,
|
||||||
integrationAuth = await revokeAccess({
|
accessToken: req.accessToken,
|
||||||
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({
|
return res.status(200).send({
|
||||||
integrationAuth,
|
integrationAuth,
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
import { Request, Response } from 'express';
|
import { Request, Response } from 'express';
|
||||||
import { Types } from 'mongoose';
|
import { Types } from 'mongoose';
|
||||||
import * as Sentry from '@sentry/node';
|
|
||||||
import {
|
import {
|
||||||
Integration
|
Integration
|
||||||
} from '../../models';
|
} from '../../models';
|
||||||
@@ -14,62 +13,50 @@ import { eventPushSecrets } from '../../events';
|
|||||||
* @returns
|
* @returns
|
||||||
*/
|
*/
|
||||||
export const createIntegration = async (req: Request, res: Response) => {
|
export const createIntegration = async (req: Request, res: Response) => {
|
||||||
let integration;
|
const {
|
||||||
|
integrationAuthId,
|
||||||
try {
|
app,
|
||||||
const {
|
appId,
|
||||||
integrationAuthId,
|
isActive,
|
||||||
app,
|
sourceEnvironment,
|
||||||
appId,
|
targetEnvironment,
|
||||||
isActive,
|
targetEnvironmentId,
|
||||||
sourceEnvironment,
|
targetService,
|
||||||
targetEnvironment,
|
targetServiceId,
|
||||||
targetEnvironmentId,
|
owner,
|
||||||
targetService,
|
path,
|
||||||
targetServiceId,
|
region
|
||||||
owner,
|
} = req.body;
|
||||||
path,
|
|
||||||
region
|
// TODO: validate [sourceEnvironment] and [targetEnvironment]
|
||||||
} = req.body;
|
|
||||||
|
|
||||||
// TODO: validate [sourceEnvironment] and [targetEnvironment]
|
|
||||||
|
|
||||||
// initialize new integration after saving integration access token
|
|
||||||
integration = await new Integration({
|
|
||||||
workspace: req.integrationAuth.workspace._id,
|
|
||||||
environment: sourceEnvironment,
|
|
||||||
isActive,
|
|
||||||
app,
|
|
||||||
appId,
|
|
||||||
targetEnvironment,
|
|
||||||
targetEnvironmentId,
|
|
||||||
targetService,
|
|
||||||
targetServiceId,
|
|
||||||
owner,
|
|
||||||
path,
|
|
||||||
region,
|
|
||||||
integration: req.integrationAuth.integration,
|
|
||||||
integrationAuth: new Types.ObjectId(integrationAuthId)
|
|
||||||
}).save();
|
|
||||||
|
|
||||||
if (integration) {
|
|
||||||
// trigger event - push secrets
|
|
||||||
EventService.handleEvent({
|
|
||||||
event: eventPushSecrets({
|
|
||||||
workspaceId: integration.workspace,
|
|
||||||
environment: sourceEnvironment
|
|
||||||
})
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
} catch (err) {
|
|
||||||
Sentry.setUser({ email: req.user.email });
|
|
||||||
Sentry.captureException(err);
|
|
||||||
return res.status(400).send({
|
|
||||||
message: 'Failed to create integration'
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
|
// initialize new integration after saving integration access token
|
||||||
|
const integration = await new Integration({
|
||||||
|
workspace: req.integrationAuth.workspace._id,
|
||||||
|
environment: sourceEnvironment,
|
||||||
|
isActive,
|
||||||
|
app,
|
||||||
|
appId,
|
||||||
|
targetEnvironment,
|
||||||
|
targetEnvironmentId,
|
||||||
|
targetService,
|
||||||
|
targetServiceId,
|
||||||
|
owner,
|
||||||
|
path,
|
||||||
|
region,
|
||||||
|
integration: req.integrationAuth.integration,
|
||||||
|
integrationAuth: new Types.ObjectId(integrationAuthId)
|
||||||
|
}).save();
|
||||||
|
|
||||||
|
if (integration) {
|
||||||
|
// trigger event - push secrets
|
||||||
|
EventService.handleEvent({
|
||||||
|
event: eventPushSecrets({
|
||||||
|
workspaceId: integration.workspace,
|
||||||
|
environment: sourceEnvironment
|
||||||
|
})
|
||||||
|
});
|
||||||
|
}
|
||||||
return res.status(200).send({
|
return res.status(200).send({
|
||||||
integration,
|
integration,
|
||||||
});
|
});
|
||||||
@@ -82,52 +69,43 @@ export const createIntegration = async (req: Request, res: Response) => {
|
|||||||
* @returns
|
* @returns
|
||||||
*/
|
*/
|
||||||
export const updateIntegration = async (req: Request, res: Response) => {
|
export const updateIntegration = async (req: Request, res: Response) => {
|
||||||
let integration;
|
|
||||||
|
|
||||||
// TODO: add integration-specific validation to ensure that each
|
// TODO: add integration-specific validation to ensure that each
|
||||||
// integration has the correct fields populated in [Integration]
|
// integration has the correct fields populated in [Integration]
|
||||||
|
|
||||||
try {
|
const {
|
||||||
const {
|
environment,
|
||||||
|
isActive,
|
||||||
|
app,
|
||||||
|
appId,
|
||||||
|
targetEnvironment,
|
||||||
|
owner, // github-specific integration param
|
||||||
|
} = req.body;
|
||||||
|
|
||||||
|
const integration = await Integration.findOneAndUpdate(
|
||||||
|
{
|
||||||
|
_id: req.integration._id,
|
||||||
|
},
|
||||||
|
{
|
||||||
environment,
|
environment,
|
||||||
isActive,
|
isActive,
|
||||||
app,
|
app,
|
||||||
appId,
|
appId,
|
||||||
targetEnvironment,
|
targetEnvironment,
|
||||||
owner, // github-specific integration param
|
owner,
|
||||||
} = req.body;
|
},
|
||||||
|
{
|
||||||
integration = await Integration.findOneAndUpdate(
|
new: true,
|
||||||
{
|
|
||||||
_id: req.integration._id,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
environment,
|
|
||||||
isActive,
|
|
||||||
app,
|
|
||||||
appId,
|
|
||||||
targetEnvironment,
|
|
||||||
owner,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
new: true,
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
if (integration) {
|
|
||||||
// trigger event - push secrets
|
|
||||||
EventService.handleEvent({
|
|
||||||
event: eventPushSecrets({
|
|
||||||
workspaceId: integration.workspace,
|
|
||||||
environment
|
|
||||||
}),
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
} catch (err) {
|
);
|
||||||
Sentry.setUser({ email: req.user.email });
|
|
||||||
Sentry.captureException(err);
|
if (integration) {
|
||||||
return res.status(400).send({
|
// trigger event - push secrets
|
||||||
message: "Failed to update integration",
|
EventService.handleEvent({
|
||||||
|
event: eventPushSecrets({
|
||||||
|
workspaceId: integration.workspace,
|
||||||
|
environment
|
||||||
|
}),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -144,22 +122,13 @@ export const updateIntegration = async (req: Request, res: Response) => {
|
|||||||
* @returns
|
* @returns
|
||||||
*/
|
*/
|
||||||
export const deleteIntegration = async (req: Request, res: Response) => {
|
export const deleteIntegration = async (req: Request, res: Response) => {
|
||||||
let integration;
|
const { integrationId } = req.params;
|
||||||
try {
|
|
||||||
const { integrationId } = req.params;
|
|
||||||
|
|
||||||
integration = await Integration.findOneAndDelete({
|
const integration = await Integration.findOneAndDelete({
|
||||||
_id: integrationId,
|
_id: integrationId,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!integration) throw new Error("Failed to find integration");
|
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({
|
return res.status(200).send({
|
||||||
integration,
|
integration,
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
import { Request, Response } from 'express';
|
import { Request, Response } from 'express';
|
||||||
import * as Sentry from '@sentry/node';
|
|
||||||
import { Key } from '../../models';
|
import { Key } from '../../models';
|
||||||
import { findMembership } from '../../helpers/membership';
|
import { findMembership } from '../../helpers/membership';
|
||||||
|
|
||||||
@@ -11,34 +10,26 @@ import { findMembership } from '../../helpers/membership';
|
|||||||
* @returns
|
* @returns
|
||||||
*/
|
*/
|
||||||
export const uploadKey = async (req: Request, res: Response) => {
|
export const uploadKey = async (req: Request, res: Response) => {
|
||||||
try {
|
const { workspaceId } = req.params;
|
||||||
const { workspaceId } = req.params;
|
const { key } = req.body;
|
||||||
const { key } = req.body;
|
|
||||||
|
|
||||||
// validate membership of receiver
|
// validate membership of receiver
|
||||||
const receiverMembership = await findMembership({
|
const receiverMembership = await findMembership({
|
||||||
user: key.userId,
|
user: key.userId,
|
||||||
workspace: workspaceId
|
workspace: workspaceId
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!receiverMembership) {
|
if (!receiverMembership) {
|
||||||
throw new Error('Failed receiver membership validation for workspace');
|
throw new Error('Failed receiver membership validation for workspace');
|
||||||
}
|
}
|
||||||
|
|
||||||
await new Key({
|
await new Key({
|
||||||
encryptedKey: key.encryptedKey,
|
encryptedKey: key.encryptedKey,
|
||||||
nonce: key.nonce,
|
nonce: key.nonce,
|
||||||
sender: req.user._id,
|
sender: req.user._id,
|
||||||
receiver: key.userId,
|
receiver: key.userId,
|
||||||
workspace: workspaceId
|
workspace: workspaceId
|
||||||
}).save();
|
}).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({
|
return res.status(200).send({
|
||||||
message: 'Successfully uploaded key to workspace'
|
message: 'Successfully uploaded key to workspace'
|
||||||
@@ -52,25 +43,16 @@ export const uploadKey = async (req: Request, res: Response) => {
|
|||||||
* @returns
|
* @returns
|
||||||
*/
|
*/
|
||||||
export const getLatestKey = async (req: Request, res: Response) => {
|
export const getLatestKey = async (req: Request, res: Response) => {
|
||||||
let latestKey;
|
const { workspaceId } = req.params;
|
||||||
try {
|
|
||||||
const { workspaceId } = req.params;
|
|
||||||
|
|
||||||
// get latest key
|
// get latest key
|
||||||
latestKey = await Key.find({
|
const latestKey = await Key.find({
|
||||||
workspace: workspaceId,
|
workspace: workspaceId,
|
||||||
receiver: req.user._id
|
receiver: req.user._id
|
||||||
})
|
})
|
||||||
.sort({ createdAt: -1 })
|
.sort({ createdAt: -1 })
|
||||||
.limit(1)
|
.limit(1)
|
||||||
.populate('sender', '+publicKey');
|
.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 = {};
|
const resObj: any = {};
|
||||||
|
|
||||||
@@ -79,4 +61,4 @@ export const getLatestKey = async (req: Request, res: Response) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return res.status(200).send(resObj);
|
return res.status(200).send(resObj);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
import * as Sentry from '@sentry/node';
|
|
||||||
import { Request, Response } from 'express';
|
import { Request, Response } from 'express';
|
||||||
import { Membership, MembershipOrg, User, Key } from '../../models';
|
import { Membership, MembershipOrg, User, Key } from '../../models';
|
||||||
import {
|
import {
|
||||||
@@ -16,25 +15,16 @@ import { getSiteURL } from '../../config';
|
|||||||
* @returns
|
* @returns
|
||||||
*/
|
*/
|
||||||
export const validateMembership = async (req: Request, res: Response) => {
|
export const validateMembership = async (req: Request, res: Response) => {
|
||||||
try {
|
const { workspaceId } = req.params;
|
||||||
const { workspaceId } = req.params;
|
// validate membership
|
||||||
|
const membership = await findMembership({
|
||||||
|
user: req.user._id,
|
||||||
|
workspace: workspaceId
|
||||||
|
});
|
||||||
|
|
||||||
// validate membership
|
if (!membership) {
|
||||||
const membership = await findMembership({
|
throw new Error('Failed to validate membership');
|
||||||
user: req.user._id,
|
}
|
||||||
workspace: workspaceId
|
|
||||||
});
|
|
||||||
|
|
||||||
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({
|
return res.status(200).send({
|
||||||
message: 'Workspace membership confirmed'
|
message: 'Workspace membership confirmed'
|
||||||
@@ -48,48 +38,39 @@ export const validateMembership = async (req: Request, res: Response) => {
|
|||||||
* @returns
|
* @returns
|
||||||
*/
|
*/
|
||||||
export const deleteMembership = async (req: Request, res: Response) => {
|
export const deleteMembership = async (req: Request, res: Response) => {
|
||||||
let deletedMembership;
|
const { membershipId } = req.params;
|
||||||
try {
|
|
||||||
const { membershipId } = req.params;
|
|
||||||
|
|
||||||
// check if membership to delete exists
|
// check if membership to delete exists
|
||||||
const membershipToDelete = await Membership.findOne({
|
const membershipToDelete = await Membership.findOne({
|
||||||
_id: membershipId
|
_id: membershipId
|
||||||
}).populate('user');
|
}).populate('user');
|
||||||
|
|
||||||
if (!membershipToDelete) {
|
if (!membershipToDelete) {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
"Failed to delete workspace membership that doesn't exist"
|
"Failed to delete workspace membership that doesn't exist"
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// check if user is a member and admin of the workspace
|
// check if user is a member and admin of the workspace
|
||||||
// whose membership we wish to delete
|
// whose membership we wish to delete
|
||||||
const membership = await Membership.findOne({
|
const membership = await Membership.findOne({
|
||||||
user: req.user._id,
|
user: req.user._id,
|
||||||
workspace: membershipToDelete.workspace
|
workspace: membershipToDelete.workspace
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!membership) {
|
if (!membership) {
|
||||||
throw new Error('Failed to validate workspace membership');
|
throw new Error('Failed to validate workspace membership');
|
||||||
}
|
}
|
||||||
|
|
||||||
if (membership.role !== ADMIN) {
|
if (membership.role !== ADMIN) {
|
||||||
// user is not an admin member of the workspace
|
// user is not an admin member of the workspace
|
||||||
throw new Error('Insufficient role for deleting workspace membership');
|
throw new Error('Insufficient role for deleting workspace membership');
|
||||||
}
|
}
|
||||||
|
|
||||||
// delete workspace membership
|
// delete workspace membership
|
||||||
deletedMembership = await deleteMember({
|
const deletedMembership = await deleteMember({
|
||||||
membershipId: membershipToDelete._id.toString()
|
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({
|
return res.status(200).send({
|
||||||
deletedMembership
|
deletedMembership
|
||||||
@@ -103,49 +84,40 @@ export const deleteMembership = async (req: Request, res: Response) => {
|
|||||||
* @returns
|
* @returns
|
||||||
*/
|
*/
|
||||||
export const changeMembershipRole = async (req: Request, res: Response) => {
|
export const changeMembershipRole = async (req: Request, res: Response) => {
|
||||||
let membershipToChangeRole;
|
const { membershipId } = req.params;
|
||||||
try {
|
const { role } = req.body;
|
||||||
const { membershipId } = req.params;
|
|
||||||
const { role } = req.body;
|
|
||||||
|
|
||||||
if (![ADMIN, MEMBER].includes(role)) {
|
if (![ADMIN, MEMBER].includes(role)) {
|
||||||
throw new Error('Failed to validate role');
|
throw new Error('Failed to validate role');
|
||||||
}
|
}
|
||||||
|
|
||||||
// validate target membership
|
// validate target membership
|
||||||
membershipToChangeRole = await findMembership({
|
const membershipToChangeRole = await findMembership({
|
||||||
_id: membershipId
|
_id: membershipId
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!membershipToChangeRole) {
|
if (!membershipToChangeRole) {
|
||||||
throw new Error('Failed to find membership to change role');
|
throw new Error('Failed to find membership to change role');
|
||||||
}
|
}
|
||||||
|
|
||||||
// check if user is a member and admin of target membership's
|
// check if user is a member and admin of target membership's
|
||||||
// workspace
|
// workspace
|
||||||
const membership = await findMembership({
|
const membership = await findMembership({
|
||||||
user: req.user._id,
|
user: req.user._id,
|
||||||
workspace: membershipToChangeRole.workspace
|
workspace: membershipToChangeRole.workspace
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!membership) {
|
if (!membership) {
|
||||||
throw new Error('Failed to validate membership');
|
throw new Error('Failed to validate membership');
|
||||||
}
|
}
|
||||||
|
|
||||||
if (membership.role !== ADMIN) {
|
if (membership.role !== ADMIN) {
|
||||||
// user is not an admin member of the workspace
|
// user is not an admin member of the workspace
|
||||||
throw new Error('Insufficient role for changing member roles');
|
throw new Error('Insufficient role for changing member roles');
|
||||||
}
|
}
|
||||||
|
|
||||||
membershipToChangeRole.role = role;
|
membershipToChangeRole.role = role;
|
||||||
await membershipToChangeRole.save();
|
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({
|
return res.status(200).send({
|
||||||
membership: membershipToChangeRole
|
membership: membershipToChangeRole
|
||||||
@@ -159,75 +131,66 @@ export const changeMembershipRole = async (req: Request, res: Response) => {
|
|||||||
* @returns
|
* @returns
|
||||||
*/
|
*/
|
||||||
export const inviteUserToWorkspace = async (req: Request, res: Response) => {
|
export const inviteUserToWorkspace = async (req: Request, res: Response) => {
|
||||||
let invitee, latestKey;
|
const { workspaceId } = req.params;
|
||||||
try {
|
const { email }: { email: string } = req.body;
|
||||||
const { workspaceId } = req.params;
|
|
||||||
const { email }: { email: string } = req.body;
|
|
||||||
|
|
||||||
invitee = await User.findOne({
|
const invitee = await User.findOne({
|
||||||
email
|
email
|
||||||
}).select('+publicKey');
|
}).select('+publicKey');
|
||||||
|
|
||||||
if (!invitee || !invitee?.publicKey)
|
if (!invitee || !invitee?.publicKey)
|
||||||
throw new Error('Failed to validate invitee');
|
throw new Error('Failed to validate invitee');
|
||||||
|
|
||||||
// validate invitee's workspace membership - ensure member isn't
|
// validate invitee's workspace membership - ensure member isn't
|
||||||
// already a member of the workspace
|
// already a member of the workspace
|
||||||
const inviteeMembership = await Membership.findOne({
|
const inviteeMembership = await Membership.findOne({
|
||||||
user: invitee._id,
|
user: invitee._id,
|
||||||
workspace: workspaceId
|
workspace: workspaceId
|
||||||
});
|
});
|
||||||
|
|
||||||
if (inviteeMembership)
|
if (inviteeMembership)
|
||||||
throw new Error('Failed to add existing member of workspace');
|
throw new Error('Failed to add existing member of workspace');
|
||||||
|
|
||||||
// validate invitee's organization membership - ensure that only
|
// validate invitee's organization membership - ensure that only
|
||||||
// (accepted) organization members can be added to the workspace
|
// (accepted) organization members can be added to the workspace
|
||||||
const membershipOrg = await MembershipOrg.findOne({
|
const membershipOrg = await MembershipOrg.findOne({
|
||||||
user: invitee._id,
|
user: invitee._id,
|
||||||
organization: req.membership.workspace.organization,
|
organization: req.membership.workspace.organization,
|
||||||
status: ACCEPTED
|
status: ACCEPTED
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!membershipOrg)
|
if (!membershipOrg)
|
||||||
throw new Error("Failed to validate invitee's organization membership");
|
throw new Error("Failed to validate invitee's organization membership");
|
||||||
|
|
||||||
// get latest key
|
// get latest key
|
||||||
latestKey = await Key.findOne({
|
const latestKey = await Key.findOne({
|
||||||
workspace: workspaceId,
|
workspace: workspaceId,
|
||||||
receiver: req.user._id
|
receiver: req.user._id
|
||||||
})
|
})
|
||||||
.sort({ createdAt: -1 })
|
.sort({ createdAt: -1 })
|
||||||
.populate('sender', '+publicKey');
|
.populate('sender', '+publicKey');
|
||||||
|
|
||||||
// create new workspace membership
|
// create new workspace membership
|
||||||
const m = await new Membership({
|
const m = await new Membership({
|
||||||
user: invitee._id,
|
user: invitee._id,
|
||||||
workspace: workspaceId,
|
workspace: workspaceId,
|
||||||
role: MEMBER
|
role: MEMBER
|
||||||
}).save();
|
}).save();
|
||||||
|
|
||||||
await sendMail({
|
await sendMail({
|
||||||
template: 'workspaceInvitation.handlebars',
|
template: 'workspaceInvitation.handlebars',
|
||||||
subjectLine: 'Infisical workspace invitation',
|
subjectLine: 'Infisical workspace invitation',
|
||||||
recipients: [invitee.email],
|
recipients: [invitee.email],
|
||||||
substitutions: {
|
substitutions: {
|
||||||
inviterFirstName: req.user.firstName,
|
inviterFirstName: req.user.firstName,
|
||||||
inviterEmail: req.user.email,
|
inviterEmail: req.user.email,
|
||||||
workspaceName: req.membership.workspace.name,
|
workspaceName: req.membership.workspace.name,
|
||||||
callback_url: (await getSiteURL()) + '/login'
|
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({
|
return res.status(200).send({
|
||||||
invitee,
|
invitee,
|
||||||
latestKey
|
latestKey
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,14 +1,15 @@
|
|||||||
import { Types } from 'mongoose';
|
import { Types } from 'mongoose';
|
||||||
import { Request, Response } from 'express';
|
import { Request, Response } from 'express';
|
||||||
import * as Sentry from '@sentry/node';
|
|
||||||
import { MembershipOrg, Organization, User } from '../../models';
|
import { MembershipOrg, Organization, User } from '../../models';
|
||||||
import { deleteMembershipOrg as deleteMemberFromOrg } from '../../helpers/membershipOrg';
|
import { deleteMembershipOrg as deleteMemberFromOrg } from '../../helpers/membershipOrg';
|
||||||
import { createToken } from '../../helpers/auth';
|
import { createToken } from '../../helpers/auth';
|
||||||
import { updateSubscriptionOrgQuantity } from '../../helpers/organization';
|
import { updateSubscriptionOrgQuantity } from '../../helpers/organization';
|
||||||
import { sendMail } from '../../helpers/nodemailer';
|
import { sendMail } from '../../helpers/nodemailer';
|
||||||
import { TokenService } from '../../services';
|
import { TokenService } from '../../services';
|
||||||
|
import { EELicenseService } from '../../ee/services';
|
||||||
import { OWNER, ADMIN, MEMBER, ACCEPTED, INVITED, TOKEN_EMAIL_ORG_INVITATION } from '../../variables';
|
import { OWNER, ADMIN, MEMBER, ACCEPTED, INVITED, TOKEN_EMAIL_ORG_INVITATION } from '../../variables';
|
||||||
import { getSiteURL, getJwtSignupLifetime, getJwtSignupSecret, getSmtpConfigured } from '../../config';
|
import { getSiteURL, getJwtSignupLifetime, getJwtSignupSecret, getSmtpConfigured } from '../../config';
|
||||||
|
import { validateUserEmail } from '../../validation';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Delete organization membership with id [membershipOrgId] from organization
|
* Delete organization membership with id [membershipOrgId] from organization
|
||||||
@@ -17,52 +18,43 @@ import { getSiteURL, getJwtSignupLifetime, getJwtSignupSecret, getSmtpConfigured
|
|||||||
* @returns
|
* @returns
|
||||||
*/
|
*/
|
||||||
export const deleteMembershipOrg = async (req: Request, res: Response) => {
|
export const deleteMembershipOrg = async (req: Request, res: Response) => {
|
||||||
let membershipOrgToDelete;
|
const { membershipOrgId } = req.params;
|
||||||
try {
|
|
||||||
const { membershipOrgId } = req.params;
|
|
||||||
|
|
||||||
// check if organization membership to delete exists
|
// check if organization membership to delete exists
|
||||||
membershipOrgToDelete = await MembershipOrg.findOne({
|
const membershipOrgToDelete = await MembershipOrg.findOne({
|
||||||
_id: membershipOrgId
|
_id: membershipOrgId
|
||||||
}).populate('user');
|
}).populate('user');
|
||||||
|
|
||||||
if (!membershipOrgToDelete) {
|
if (!membershipOrgToDelete) {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
"Failed to delete organization membership that doesn't exist"
|
"Failed to delete organization membership that doesn't exist"
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// check if user is a member and admin of the organization
|
// check if user is a member and admin of the organization
|
||||||
// whose membership we wish to delete
|
// whose membership we wish to delete
|
||||||
const membershipOrg = await MembershipOrg.findOne({
|
const membershipOrg = await MembershipOrg.findOne({
|
||||||
user: req.user._id,
|
user: req.user._id,
|
||||||
organization: membershipOrgToDelete.organization
|
organization: membershipOrgToDelete.organization
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!membershipOrg) {
|
if (!membershipOrg) {
|
||||||
throw new Error('Failed to validate organization membership');
|
throw new Error('Failed to validate organization membership');
|
||||||
}
|
}
|
||||||
|
|
||||||
if (membershipOrg.role !== OWNER && membershipOrg.role !== ADMIN) {
|
if (membershipOrg.role !== OWNER && membershipOrg.role !== ADMIN) {
|
||||||
// user is not an admin member of the organization
|
// user is not an admin member of the organization
|
||||||
throw new Error('Insufficient role for deleting organization membership');
|
throw new Error('Insufficient role for deleting organization membership');
|
||||||
}
|
}
|
||||||
|
|
||||||
// delete organization membership
|
// delete organization membership
|
||||||
const deletedMembershipOrg = await deleteMemberFromOrg({
|
const deletedMembershipOrg = await deleteMemberFromOrg({
|
||||||
membershipOrgId: membershipOrgToDelete._id.toString()
|
membershipOrgId: membershipOrgToDelete._id.toString()
|
||||||
});
|
});
|
||||||
|
|
||||||
await updateSubscriptionOrgQuantity({
|
await updateSubscriptionOrgQuantity({
|
||||||
organizationId: membershipOrg.organization.toString()
|
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;
|
return membershipOrgToDelete;
|
||||||
};
|
};
|
||||||
@@ -78,14 +70,6 @@ export const changeMembershipOrgRole = async (req: Request, res: Response) => {
|
|||||||
// [membershipOrgId]
|
// [membershipOrgId]
|
||||||
|
|
||||||
let membershipToChangeRole;
|
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({
|
return res.status(200).send({
|
||||||
membershipOrg: membershipToChangeRole
|
membershipOrg: membershipToChangeRole
|
||||||
@@ -101,106 +85,114 @@ export const changeMembershipOrgRole = async (req: Request, res: Response) => {
|
|||||||
*/
|
*/
|
||||||
export const inviteUserToOrganization = async (req: Request, res: Response) => {
|
export const inviteUserToOrganization = async (req: Request, res: Response) => {
|
||||||
let invitee, inviteeMembershipOrg, completeInviteLink;
|
let invitee, inviteeMembershipOrg, completeInviteLink;
|
||||||
try {
|
const { organizationId, inviteeEmail } = req.body;
|
||||||
const { organizationId, inviteeEmail } = req.body;
|
const host = req.headers.host;
|
||||||
const host = req.headers.host;
|
const siteUrl = `${req.protocol}://${host}`;
|
||||||
const siteUrl = `${req.protocol}://${host}`;
|
|
||||||
|
|
||||||
// validate membership
|
// validate membership
|
||||||
const membershipOrg = await MembershipOrg.findOne({
|
const membershipOrg = await MembershipOrg.findOne({
|
||||||
user: req.user._id,
|
user: req.user._id,
|
||||||
organization: organizationId
|
organization: organizationId
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!membershipOrg) {
|
if (!membershipOrg) {
|
||||||
throw new Error('Failed to validate organization membership');
|
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({
|
invitee = await User.findOne({
|
||||||
email: inviteeEmail
|
email: inviteeEmail
|
||||||
}).select('+publicKey');
|
}).select('+publicKey');
|
||||||
|
|
||||||
if (invitee) {
|
if (invitee) {
|
||||||
// case: invitee is an existing user
|
// case: invitee is an existing user
|
||||||
|
|
||||||
inviteeMembershipOrg = await MembershipOrg.findOne({
|
inviteeMembershipOrg = await MembershipOrg.findOne({
|
||||||
user: invitee._id,
|
user: invitee._id,
|
||||||
organization: organizationId
|
organization: organizationId
|
||||||
});
|
});
|
||||||
|
|
||||||
if (inviteeMembershipOrg && inviteeMembershipOrg.status === ACCEPTED) {
|
if (inviteeMembershipOrg && inviteeMembershipOrg.status === ACCEPTED) {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
'Failed to invite an existing member of the organization'
|
'Failed to invite an existing member of the organization'
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!inviteeMembershipOrg) {
|
if (!inviteeMembershipOrg) {
|
||||||
|
|
||||||
await new MembershipOrg({
|
await new MembershipOrg({
|
||||||
user: invitee,
|
user: invitee,
|
||||||
inviteEmail: inviteeEmail,
|
inviteEmail: inviteeEmail,
|
||||||
organization: organizationId,
|
organization: organizationId,
|
||||||
role: MEMBER,
|
role: MEMBER,
|
||||||
status: INVITED
|
status: INVITED
|
||||||
}).save();
|
}).save();
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// check if invitee has been invited before
|
// check if invitee has been invited before
|
||||||
inviteeMembershipOrg = await MembershipOrg.findOne({
|
inviteeMembershipOrg = await MembershipOrg.findOne({
|
||||||
inviteEmail: inviteeEmail,
|
inviteEmail: inviteeEmail,
|
||||||
organization: organizationId
|
organization: organizationId
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!inviteeMembershipOrg) {
|
if (!inviteeMembershipOrg) {
|
||||||
// case: invitee has never been invited before
|
// case: invitee has never been invited before
|
||||||
|
|
||||||
|
// validate that email is not disposable
|
||||||
|
validateUserEmail(inviteeEmail);
|
||||||
|
|
||||||
await new MembershipOrg({
|
await new MembershipOrg({
|
||||||
inviteEmail: inviteeEmail,
|
inviteEmail: inviteeEmail,
|
||||||
organization: organizationId,
|
organization: organizationId,
|
||||||
role: MEMBER,
|
role: MEMBER,
|
||||||
status: INVITED
|
status: INVITED
|
||||||
}).save();
|
}).save();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const organization = await Organization.findOne({ _id: organizationId });
|
const organization = await Organization.findOne({ _id: organizationId });
|
||||||
|
|
||||||
if (organization) {
|
if (organization) {
|
||||||
|
|
||||||
const token = await TokenService.createToken({
|
const token = await TokenService.createToken({
|
||||||
type: TOKEN_EMAIL_ORG_INVITATION,
|
type: TOKEN_EMAIL_ORG_INVITATION,
|
||||||
email: inviteeEmail,
|
email: inviteeEmail,
|
||||||
organizationId: organization._id
|
organizationId: organization._id
|
||||||
});
|
});
|
||||||
|
|
||||||
await sendMail({
|
await sendMail({
|
||||||
template: 'organizationInvitation.handlebars',
|
template: 'organizationInvitation.handlebars',
|
||||||
subjectLine: 'Infisical organization invitation',
|
subjectLine: 'Infisical organization invitation',
|
||||||
recipients: [inviteeEmail],
|
recipients: [inviteeEmail],
|
||||||
substitutions: {
|
substitutions: {
|
||||||
inviterFirstName: req.user.firstName,
|
inviterFirstName: req.user.firstName,
|
||||||
inviterEmail: req.user.email,
|
inviterEmail: req.user.email,
|
||||||
organizationName: organization.name,
|
organizationName: organization.name,
|
||||||
email: inviteeEmail,
|
email: inviteeEmail,
|
||||||
organizationId: organization._id.toString(),
|
organizationId: organization._id.toString(),
|
||||||
token,
|
token,
|
||||||
callback_url: (await getSiteURL()) + '/signupinvite'
|
callback_url: (await getSiteURL()) + '/signupinvite'
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!(await getSmtpConfigured())) {
|
if (!(await getSmtpConfigured())) {
|
||||||
completeInviteLink = `${siteUrl + '/signupinvite'}?token=${token}&to=${inviteeEmail}&organization_id=${organization._id}`
|
completeInviteLink = `${siteUrl + '/signupinvite'}?token=${token}&to=${inviteeEmail}&organization_id=${organization._id}`
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
await updateSubscriptionOrgQuantity({ organizationId });
|
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({
|
return res.status(200).send({
|
||||||
message: `Sent an invite link to ${req.body.inviteeEmail}`,
|
message: `Sent an invite link to ${req.body.inviteeEmail}`,
|
||||||
@@ -216,70 +208,62 @@ export const inviteUserToOrganization = async (req: Request, res: Response) => {
|
|||||||
* @returns
|
* @returns
|
||||||
*/
|
*/
|
||||||
export const verifyUserToOrganization = async (req: Request, res: Response) => {
|
export const verifyUserToOrganization = async (req: Request, res: Response) => {
|
||||||
let user, token;
|
let user;
|
||||||
try {
|
const {
|
||||||
const {
|
email,
|
||||||
email,
|
organizationId,
|
||||||
organizationId,
|
code
|
||||||
code
|
} = req.body;
|
||||||
} = req.body;
|
|
||||||
|
|
||||||
user = await User.findOne({ email }).select('+publicKey');
|
user = await User.findOne({ email }).select('+publicKey');
|
||||||
|
|
||||||
const membershipOrg = await MembershipOrg.findOne({
|
const membershipOrg = await MembershipOrg.findOne({
|
||||||
inviteEmail: email,
|
inviteEmail: email,
|
||||||
status: INVITED,
|
status: INVITED,
|
||||||
organization: new Types.ObjectId(organizationId)
|
organization: new Types.ObjectId(organizationId)
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!membershipOrg)
|
if (!membershipOrg)
|
||||||
throw new Error('Failed to find any invitations for email');
|
throw new Error('Failed to find any invitations for email');
|
||||||
|
|
||||||
await TokenService.validateToken({
|
await TokenService.validateToken({
|
||||||
type: TOKEN_EMAIL_ORG_INVITATION,
|
type: TOKEN_EMAIL_ORG_INVITATION,
|
||||||
email,
|
email,
|
||||||
organizationId: membershipOrg.organization,
|
organizationId: membershipOrg.organization,
|
||||||
token: code
|
token: code
|
||||||
});
|
});
|
||||||
|
|
||||||
if (user && user?.publicKey) {
|
if (user && user?.publicKey) {
|
||||||
// case: user has already completed account
|
// case: user has already completed account
|
||||||
// membership can be approved and redirected to login/dashboard
|
// membership can be approved and redirected to login/dashboard
|
||||||
membershipOrg.status = ACCEPTED;
|
membershipOrg.status = ACCEPTED;
|
||||||
await membershipOrg.save();
|
await membershipOrg.save();
|
||||||
|
|
||||||
await updateSubscriptionOrgQuantity({
|
await updateSubscriptionOrgQuantity({
|
||||||
organizationId
|
organizationId
|
||||||
});
|
});
|
||||||
|
|
||||||
return res.status(200).send({
|
return res.status(200).send({
|
||||||
message: 'Successfully verified email',
|
message: 'Successfully verified email',
|
||||||
user,
|
user,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!user) {
|
if (!user) {
|
||||||
// initialize user account
|
// initialize user account
|
||||||
user = await new User({
|
user = await new User({
|
||||||
email
|
email
|
||||||
}).save();
|
}).save();
|
||||||
}
|
}
|
||||||
|
|
||||||
// generate temporary signup token
|
// generate temporary signup token
|
||||||
token = createToken({
|
const token = createToken({
|
||||||
payload: {
|
payload: {
|
||||||
userId: user._id.toString()
|
userId: user._id.toString()
|
||||||
},
|
},
|
||||||
expiresIn: await getJwtSignupLifetime(),
|
expiresIn: await getJwtSignupLifetime(),
|
||||||
secret: await getJwtSignupSecret()
|
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({
|
return res.status(200).send({
|
||||||
message: 'Successfully verified email',
|
message: 'Successfully verified email',
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
import * as Sentry from '@sentry/node';
|
|
||||||
import { Request, Response } from 'express';
|
import { Request, Response } from 'express';
|
||||||
import Stripe from 'stripe';
|
import Stripe from 'stripe';
|
||||||
import {
|
import {
|
||||||
@@ -15,21 +14,12 @@ import _ from 'lodash';
|
|||||||
import { getStripeSecretKey, getSiteURL } from '../../config';
|
import { getStripeSecretKey, getSiteURL } from '../../config';
|
||||||
|
|
||||||
export const getOrganizations = async (req: Request, res: Response) => {
|
export const getOrganizations = async (req: Request, res: Response) => {
|
||||||
let organizations;
|
const organizations = (
|
||||||
try {
|
await MembershipOrg.find({
|
||||||
organizations = (
|
user: req.user._id,
|
||||||
await MembershipOrg.find({
|
status: ACCEPTED
|
||||||
user: req.user._id,
|
}).populate('organization')
|
||||||
status: ACCEPTED
|
).map((m) => m.organization);
|
||||||
}).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({
|
return res.status(200).send({
|
||||||
organizations
|
organizations
|
||||||
@@ -44,33 +34,24 @@ export const getOrganizations = async (req: Request, res: Response) => {
|
|||||||
* @returns
|
* @returns
|
||||||
*/
|
*/
|
||||||
export const createOrganization = async (req: Request, res: Response) => {
|
export const createOrganization = async (req: Request, res: Response) => {
|
||||||
let organization;
|
const { organizationName } = req.body;
|
||||||
try {
|
|
||||||
const { organizationName } = req.body;
|
|
||||||
|
|
||||||
if (organizationName.length < 1) {
|
if (organizationName.length < 1) {
|
||||||
throw new Error('Organization names must be at least 1-character long');
|
throw new Error('Organization names must be at least 1-character long');
|
||||||
}
|
}
|
||||||
|
|
||||||
// create organization and add user as member
|
// create organization and add user as member
|
||||||
organization = await create({
|
const organization = await create({
|
||||||
email: req.user.email,
|
email: req.user.email,
|
||||||
name: organizationName
|
name: organizationName
|
||||||
});
|
});
|
||||||
|
|
||||||
await addMembershipsOrg({
|
await addMembershipsOrg({
|
||||||
userIds: [req.user._id.toString()],
|
userIds: [req.user._id.toString()],
|
||||||
organizationId: organization._id.toString(),
|
organizationId: organization._id.toString(),
|
||||||
roles: [OWNER],
|
roles: [OWNER],
|
||||||
statuses: [ACCEPTED]
|
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({
|
return res.status(200).send({
|
||||||
organization
|
organization
|
||||||
@@ -84,17 +65,7 @@ export const createOrganization = async (req: Request, res: Response) => {
|
|||||||
* @returns
|
* @returns
|
||||||
*/
|
*/
|
||||||
export const getOrganization = async (req: Request, res: Response) => {
|
export const getOrganization = async (req: Request, res: Response) => {
|
||||||
let organization;
|
const organization = req.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'
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return res.status(200).send({
|
return res.status(200).send({
|
||||||
organization
|
organization
|
||||||
});
|
});
|
||||||
@@ -107,20 +78,11 @@ export const getOrganization = async (req: Request, res: Response) => {
|
|||||||
* @returns
|
* @returns
|
||||||
*/
|
*/
|
||||||
export const getOrganizationMembers = async (req: Request, res: Response) => {
|
export const getOrganizationMembers = async (req: Request, res: Response) => {
|
||||||
let users;
|
const { organizationId } = req.params;
|
||||||
try {
|
|
||||||
const { organizationId } = req.params;
|
|
||||||
|
|
||||||
users = await MembershipOrg.find({
|
const users = await MembershipOrg.find({
|
||||||
organization: organizationId
|
organization: organizationId
|
||||||
}).populate('user', '+publicKey');
|
}).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({
|
return res.status(200).send({
|
||||||
users
|
users
|
||||||
@@ -137,35 +99,26 @@ export const getOrganizationWorkspaces = async (
|
|||||||
req: Request,
|
req: Request,
|
||||||
res: Response
|
res: Response
|
||||||
) => {
|
) => {
|
||||||
let workspaces;
|
const { organizationId } = req.params;
|
||||||
try {
|
|
||||||
const { organizationId } = req.params;
|
|
||||||
|
|
||||||
const workspacesSet = new Set(
|
const workspacesSet = new Set(
|
||||||
(
|
(
|
||||||
await Workspace.find(
|
await Workspace.find(
|
||||||
{
|
{
|
||||||
organization: organizationId
|
organization: organizationId
|
||||||
},
|
},
|
||||||
'_id'
|
'_id'
|
||||||
)
|
)
|
||||||
).map((w) => w._id.toString())
|
).map((w) => w._id.toString())
|
||||||
);
|
);
|
||||||
|
|
||||||
workspaces = (
|
const workspaces = (
|
||||||
await Membership.find({
|
await Membership.find({
|
||||||
user: req.user._id
|
user: req.user._id
|
||||||
}).populate('workspace')
|
}).populate('workspace')
|
||||||
)
|
)
|
||||||
.filter((m) => workspacesSet.has(m.workspace._id.toString()))
|
.filter((m) => workspacesSet.has(m.workspace._id.toString()))
|
||||||
.map((m) => m.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 my workspaces'
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return res.status(200).send({
|
return res.status(200).send({
|
||||||
workspaces
|
workspaces
|
||||||
@@ -179,29 +132,20 @@ export const getOrganizationWorkspaces = async (
|
|||||||
* @returns
|
* @returns
|
||||||
*/
|
*/
|
||||||
export const changeOrganizationName = async (req: Request, res: Response) => {
|
export const changeOrganizationName = async (req: Request, res: Response) => {
|
||||||
let organization;
|
const { organizationId } = req.params;
|
||||||
try {
|
const { name } = req.body;
|
||||||
const { organizationId } = req.params;
|
|
||||||
const { name } = req.body;
|
|
||||||
|
|
||||||
organization = await Organization.findOneAndUpdate(
|
const organization = await Organization.findOneAndUpdate(
|
||||||
{
|
{
|
||||||
_id: organizationId
|
_id: organizationId
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name
|
name
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
new: true
|
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({
|
return res.status(200).send({
|
||||||
message: 'Successfully changed organization name',
|
message: 'Successfully changed organization name',
|
||||||
@@ -219,20 +163,11 @@ export const getOrganizationIncidentContacts = async (
|
|||||||
req: Request,
|
req: Request,
|
||||||
res: Response
|
res: Response
|
||||||
) => {
|
) => {
|
||||||
let incidentContactsOrg;
|
const { organizationId } = req.params;
|
||||||
try {
|
|
||||||
const { organizationId } = req.params;
|
|
||||||
|
|
||||||
incidentContactsOrg = await IncidentContactOrg.find({
|
const incidentContactsOrg = await IncidentContactOrg.find({
|
||||||
organization: organizationId
|
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({
|
return res.status(200).send({
|
||||||
incidentContactsOrg
|
incidentContactsOrg
|
||||||
@@ -249,23 +184,14 @@ export const addOrganizationIncidentContact = async (
|
|||||||
req: Request,
|
req: Request,
|
||||||
res: Response
|
res: Response
|
||||||
) => {
|
) => {
|
||||||
let incidentContactOrg;
|
const { organizationId } = req.params;
|
||||||
try {
|
const { email } = req.body;
|
||||||
const { organizationId } = req.params;
|
|
||||||
const { email } = req.body;
|
|
||||||
|
|
||||||
incidentContactOrg = await IncidentContactOrg.findOneAndUpdate(
|
const incidentContactOrg = await IncidentContactOrg.findOneAndUpdate(
|
||||||
{ email, organization: organizationId },
|
{ email, organization: organizationId },
|
||||||
{ email, organization: organizationId },
|
{ email, organization: organizationId },
|
||||||
{ upsert: true, new: true }
|
{ 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({
|
return res.status(200).send({
|
||||||
incidentContactOrg
|
incidentContactOrg
|
||||||
@@ -282,22 +208,13 @@ export const deleteOrganizationIncidentContact = async (
|
|||||||
req: Request,
|
req: Request,
|
||||||
res: Response
|
res: Response
|
||||||
) => {
|
) => {
|
||||||
let incidentContactOrg;
|
const { organizationId } = req.params;
|
||||||
try {
|
const { email } = req.body;
|
||||||
const { organizationId } = req.params;
|
|
||||||
const { email } = req.body;
|
|
||||||
|
|
||||||
incidentContactOrg = await IncidentContactOrg.findOneAndDelete({
|
const incidentContactOrg = await IncidentContactOrg.findOneAndDelete({
|
||||||
email,
|
email,
|
||||||
organization: organizationId
|
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({
|
return res.status(200).send({
|
||||||
message: 'Successfully deleted organization incident contact',
|
message: 'Successfully deleted organization incident contact',
|
||||||
@@ -317,41 +234,33 @@ export const createOrganizationPortalSession = async (
|
|||||||
res: Response
|
res: Response
|
||||||
) => {
|
) => {
|
||||||
let session;
|
let session;
|
||||||
try {
|
const stripe = new Stripe(await getStripeSecretKey(), {
|
||||||
const stripe = new Stripe(await getStripeSecretKey(), {
|
apiVersion: '2022-08-01'
|
||||||
apiVersion: '2022-08-01'
|
});
|
||||||
});
|
|
||||||
|
|
||||||
// check if there is a payment method on file
|
// check if there is a payment method on file
|
||||||
const paymentMethods = await stripe.paymentMethods.list({
|
const paymentMethods = await stripe.paymentMethods.list({
|
||||||
customer: req.organization.customerId,
|
customer: req.organization.customerId,
|
||||||
type: 'card'
|
type: 'card'
|
||||||
});
|
});
|
||||||
|
|
||||||
if (paymentMethods.data.length < 1) {
|
if (paymentMethods.data.length < 1) {
|
||||||
// case: no payment method on file
|
// case: no payment method on file
|
||||||
session = await stripe.checkout.sessions.create({
|
session = await stripe.checkout.sessions.create({
|
||||||
customer: req.organization.customerId,
|
customer: req.organization.customerId,
|
||||||
mode: 'setup',
|
mode: 'setup',
|
||||||
payment_method_types: ['card'],
|
payment_method_types: ['card'],
|
||||||
success_url: (await getSiteURL()) + '/dashboard',
|
success_url: (await getSiteURL()) + '/dashboard',
|
||||||
cancel_url: (await getSiteURL()) + '/dashboard'
|
cancel_url: (await getSiteURL()) + '/dashboard'
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
session = await stripe.billingPortal.sessions.create({
|
session = await stripe.billingPortal.sessions.create({
|
||||||
customer: req.organization.customerId,
|
customer: req.organization.customerId,
|
||||||
return_url: (await getSiteURL()) + '/dashboard'
|
return_url: (await getSiteURL()) + '/dashboard'
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
return res.status(200).send({ url: session.url });
|
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,
|
req: Request,
|
||||||
res: Response
|
res: Response
|
||||||
) => {
|
) => {
|
||||||
let subscriptions;
|
const stripe = new Stripe(await getStripeSecretKey(), {
|
||||||
try {
|
apiVersion: '2022-08-01'
|
||||||
const stripe = new Stripe(await getStripeSecretKey(), {
|
});
|
||||||
apiVersion: '2022-08-01'
|
|
||||||
});
|
const subscriptions = await stripe.subscriptions.list({
|
||||||
|
customer: req.organization.customerId
|
||||||
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({
|
return res.status(200).send({
|
||||||
subscriptions
|
subscriptions
|
||||||
@@ -425,4 +325,4 @@ export const getOrganizationMembersAndTheirWorkspaces = async (
|
|||||||
});
|
});
|
||||||
|
|
||||||
return res.json(userToWorkspaceIds);
|
return res.json(userToWorkspaceIds);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,15 +1,25 @@
|
|||||||
import { Request, Response } from 'express';
|
import { Request, Response } from 'express';
|
||||||
import * as Sentry from '@sentry/node';
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
||||||
const jsrp = require('jsrp');
|
const jsrp = require('jsrp');
|
||||||
import * as bigintConversion from 'bigint-conversion';
|
import * as bigintConversion from 'bigint-conversion';
|
||||||
import { User, BackupPrivateKey, LoginSRPDetail } from '../../models';
|
import { User, BackupPrivateKey, LoginSRPDetail } from '../../models';
|
||||||
import { createToken } from '../../helpers/auth';
|
import {
|
||||||
import { sendMail } from '../../helpers/nodemailer';
|
createToken,
|
||||||
|
sendMail,
|
||||||
|
clearTokens
|
||||||
|
} from '../../helpers';
|
||||||
import { TokenService } from '../../services';
|
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 { 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]
|
* Password reset step 1: Send email verification link to email [email]
|
||||||
@@ -20,43 +30,35 @@ import { getSiteURL, getJwtSignupLifetime, getJwtSignupSecret } from '../../conf
|
|||||||
*/
|
*/
|
||||||
export const emailPasswordReset = async (req: Request, res: Response) => {
|
export const emailPasswordReset = async (req: Request, res: Response) => {
|
||||||
let email: string;
|
let email: string;
|
||||||
try {
|
email = req.body.email;
|
||||||
email = req.body.email;
|
|
||||||
|
|
||||||
const user = await User.findOne({ email }).select('+publicKey');
|
const user = await User.findOne({ email }).select('+publicKey');
|
||||||
if (!user || !user?.publicKey) {
|
if (!user || !user?.publicKey) {
|
||||||
// case: user has already completed account
|
// case: user has already completed account
|
||||||
|
|
||||||
return res.status(403).send({
|
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"
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const token = await TokenService.createToken({
|
const token = await TokenService.createToken({
|
||||||
type: TOKEN_EMAIL_PASSWORD_RESET,
|
type: TOKEN_EMAIL_PASSWORD_RESET,
|
||||||
email
|
email
|
||||||
});
|
});
|
||||||
|
|
||||||
await sendMail({
|
await sendMail({
|
||||||
template: 'passwordReset.handlebars',
|
template: 'passwordReset.handlebars',
|
||||||
subjectLine: 'Infisical password reset',
|
subjectLine: 'Infisical password reset',
|
||||||
recipients: [email],
|
recipients: [email],
|
||||||
substitutions: {
|
substitutions: {
|
||||||
email,
|
email,
|
||||||
token,
|
token,
|
||||||
callback_url: (await getSiteURL()) + '/password-reset'
|
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({
|
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,40 +69,31 @@ export const emailPasswordReset = async (req: Request, res: Response) => {
|
|||||||
* @returns
|
* @returns
|
||||||
*/
|
*/
|
||||||
export const emailPasswordResetVerify = async (req: Request, res: Response) => {
|
export const emailPasswordResetVerify = async (req: Request, res: Response) => {
|
||||||
let user, token;
|
const { email, code } = req.body;
|
||||||
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) {
|
if (!user || !user?.publicKey) {
|
||||||
// case: user doesn't exist with email [email] or
|
// case: user doesn't exist with email [email] or
|
||||||
// hasn't even completed their account
|
// hasn't even completed their account
|
||||||
return res.status(403).send({
|
return res.status(403).send({
|
||||||
error: 'Failed email verification for password reset'
|
error: 'Failed email verification for password reset'
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
await TokenService.validateToken({
|
await TokenService.validateToken({
|
||||||
type: TOKEN_EMAIL_PASSWORD_RESET,
|
type: TOKEN_EMAIL_PASSWORD_RESET,
|
||||||
email,
|
email,
|
||||||
token: code
|
token: code
|
||||||
});
|
});
|
||||||
|
|
||||||
// generate temporary password-reset token
|
// generate temporary password-reset token
|
||||||
token = createToken({
|
const token = createToken({
|
||||||
payload: {
|
payload: {
|
||||||
userId: user._id.toString()
|
userId: user._id.toString()
|
||||||
},
|
},
|
||||||
expiresIn: await getJwtSignupLifetime(),
|
expiresIn: await getJwtSignupLifetime(),
|
||||||
secret: await getJwtSignupSecret()
|
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({
|
return res.status(200).send({
|
||||||
message: 'Successfully verified email',
|
message: 'Successfully verified email',
|
||||||
@@ -117,44 +110,38 @@ export const emailPasswordResetVerify = async (req: Request, res: Response) => {
|
|||||||
*/
|
*/
|
||||||
export const srp1 = async (req: Request, res: Response) => {
|
export const srp1 = async (req: Request, res: Response) => {
|
||||||
// return salt, serverPublicKey as part of first step of SRP protocol
|
// return salt, serverPublicKey as part of first step of SRP protocol
|
||||||
try {
|
|
||||||
const { clientPublicKey } = req.body;
|
const { clientPublicKey } = req.body;
|
||||||
const user = await User.findOne({
|
const user = await User.findOne({
|
||||||
email: req.user.email
|
email: req.user.email
|
||||||
}).select('+salt +verifier');
|
}).select('+salt +verifier');
|
||||||
|
|
||||||
if (!user) throw new Error('Failed to find user');
|
if (!user) throw new Error('Failed to find user');
|
||||||
|
|
||||||
const server = new jsrp.server();
|
const server = new jsrp.server();
|
||||||
server.init(
|
server.init(
|
||||||
{
|
{
|
||||||
salt: user.salt,
|
salt: user.salt,
|
||||||
verifier: user.verifier
|
verifier: user.verifier
|
||||||
},
|
},
|
||||||
async () => {
|
async () => {
|
||||||
// generate server-side public key
|
// generate server-side public key
|
||||||
const serverPublicKey = server.getPublicKey();
|
const serverPublicKey = server.getPublicKey();
|
||||||
|
|
||||||
await LoginSRPDetail.findOneAndReplace({ email: req.user.email }, {
|
await LoginSRPDetail.findOneAndReplace({ email: req.user.email }, {
|
||||||
email: req.user.email,
|
email: req.user.email,
|
||||||
clientPublicKey: clientPublicKey,
|
clientPublicKey: clientPublicKey,
|
||||||
serverBInt: bigintConversion.bigintToBuf(server.bInt),
|
serverBInt: bigintConversion.bigintToBuf(server.bInt),
|
||||||
}, { upsert: true, returnNewDocument: false })
|
}, { upsert: true, returnNewDocument: false })
|
||||||
|
|
||||||
|
return res.status(200).send({
|
||||||
|
serverPublicKey,
|
||||||
|
salt: user.salt
|
||||||
|
});
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return res.status(200).send({
|
|
||||||
serverPublicKey,
|
|
||||||
salt: user.salt
|
|
||||||
});
|
|
||||||
}
|
|
||||||
);
|
|
||||||
} 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
|
* Change account SRP authentication information for user
|
||||||
@@ -165,80 +152,85 @@ export const srp1 = async (req: Request, res: Response) => {
|
|||||||
* @returns
|
* @returns
|
||||||
*/
|
*/
|
||||||
export const changePassword = async (req: Request, res: Response) => {
|
export const changePassword = async (req: Request, res: Response) => {
|
||||||
try {
|
const {
|
||||||
const {
|
clientProof,
|
||||||
clientProof,
|
protectedKey,
|
||||||
protectedKey,
|
protectedKeyIV,
|
||||||
protectedKeyIV,
|
protectedKeyTag,
|
||||||
protectedKeyTag,
|
encryptedPrivateKey,
|
||||||
encryptedPrivateKey,
|
encryptedPrivateKeyIV,
|
||||||
encryptedPrivateKeyIV,
|
encryptedPrivateKeyTag,
|
||||||
encryptedPrivateKeyTag,
|
salt,
|
||||||
salt,
|
verifier
|
||||||
verifier
|
} = req.body;
|
||||||
} = req.body;
|
|
||||||
|
|
||||||
const user = await User.findOne({
|
const user = await User.findOne({
|
||||||
email: req.user.email
|
email: req.user.email
|
||||||
}).select('+salt +verifier');
|
}).select('+salt +verifier');
|
||||||
|
|
||||||
if (!user) throw new Error('Failed to find user');
|
if (!user) throw new Error('Failed to find user');
|
||||||
|
|
||||||
const loginSRPDetailFromDB = await LoginSRPDetail.findOneAndDelete({ email: req.user.email })
|
const loginSRPDetailFromDB = await LoginSRPDetail.findOneAndDelete({ email: req.user.email })
|
||||||
|
|
||||||
if (!loginSRPDetailFromDB) {
|
if (!loginSRPDetailFromDB) {
|
||||||
return BadRequestError(Error("It looks like some details from the first login are not found. Please try login one again"))
|
return BadRequestError(Error("It looks like some details from the first login are not found. Please try login one again"))
|
||||||
}
|
}
|
||||||
|
|
||||||
const server = new jsrp.server();
|
const server = new jsrp.server();
|
||||||
server.init(
|
server.init(
|
||||||
{
|
{
|
||||||
salt: user.salt,
|
salt: user.salt,
|
||||||
verifier: user.verifier,
|
verifier: user.verifier,
|
||||||
b: loginSRPDetailFromDB.serverBInt
|
b: loginSRPDetailFromDB.serverBInt
|
||||||
},
|
},
|
||||||
async () => {
|
async () => {
|
||||||
server.setClientPublicKey(loginSRPDetailFromDB.clientPublicKey);
|
server.setClientPublicKey(loginSRPDetailFromDB.clientPublicKey);
|
||||||
|
|
||||||
// compare server and client shared keys
|
// compare server and client shared keys
|
||||||
if (server.checkClientProof(clientProof)) {
|
if (server.checkClientProof(clientProof)) {
|
||||||
// change password
|
// change password
|
||||||
|
|
||||||
await User.findByIdAndUpdate(
|
await User.findByIdAndUpdate(
|
||||||
req.user._id.toString(),
|
req.user._id.toString(),
|
||||||
{
|
{
|
||||||
encryptionVersion: 2,
|
encryptionVersion: 2,
|
||||||
protectedKey,
|
protectedKey,
|
||||||
protectedKeyIV,
|
protectedKeyIV,
|
||||||
protectedKeyTag,
|
protectedKeyTag,
|
||||||
encryptedPrivateKey,
|
encryptedPrivateKey,
|
||||||
iv: encryptedPrivateKeyIV,
|
iv: encryptedPrivateKeyIV,
|
||||||
tag: encryptedPrivateKeyTag,
|
tag: encryptedPrivateKeyTag,
|
||||||
salt,
|
salt,
|
||||||
verifier
|
verifier
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
new: true
|
new: true
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
|
if (req.authData.authMode === AUTH_MODE_JWT && req.authData.authPayload instanceof User && req.authData.tokenVersionId) {
|
||||||
|
await clearTokens(req.authData.tokenVersionId)
|
||||||
|
}
|
||||||
|
|
||||||
return res.status(200).send({
|
// clear httpOnly cookie
|
||||||
message: 'Successfully changed password'
|
|
||||||
});
|
res.cookie('jid', '', {
|
||||||
}
|
httpOnly: true,
|
||||||
|
path: '/',
|
||||||
|
sameSite: 'strict',
|
||||||
|
secure: (await getHttpsEnabled()) as boolean
|
||||||
|
});
|
||||||
|
|
||||||
return res.status(400).send({
|
return res.status(200).send({
|
||||||
error: 'Failed to change password. Try again?'
|
message: 'Successfully changed password'
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
);
|
|
||||||
} catch (err) {
|
return res.status(400).send({
|
||||||
Sentry.setUser({ email: req.user.email });
|
error: 'Failed to change password. Try again?'
|
||||||
Sentry.captureException(err);
|
});
|
||||||
return res.status(400).send({
|
}
|
||||||
error: 'Failed to change password. Try again?'
|
);
|
||||||
});
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -252,69 +244,61 @@ export const createBackupPrivateKey = async (req: Request, res: Response) => {
|
|||||||
// requires verifying [clientProof] as part of second step of SRP protocol
|
// requires verifying [clientProof] as part of second step of SRP protocol
|
||||||
// as initiated in /srp1
|
// as initiated in /srp1
|
||||||
|
|
||||||
try {
|
const { clientProof, encryptedPrivateKey, iv, tag, salt, verifier } =
|
||||||
const { clientProof, encryptedPrivateKey, iv, tag, salt, verifier } =
|
req.body;
|
||||||
req.body;
|
const user = await User.findOne({
|
||||||
const user = await User.findOne({
|
email: req.user.email
|
||||||
email: req.user.email
|
}).select('+salt +verifier');
|
||||||
}).select('+salt +verifier');
|
|
||||||
|
|
||||||
if (!user) throw new Error('Failed to find user');
|
if (!user) throw new Error('Failed to find user');
|
||||||
|
|
||||||
const loginSRPDetailFromDB = await LoginSRPDetail.findOneAndDelete({ email: req.user.email })
|
const loginSRPDetailFromDB = await LoginSRPDetail.findOneAndDelete({ email: req.user.email })
|
||||||
|
|
||||||
if (!loginSRPDetailFromDB) {
|
if (!loginSRPDetailFromDB) {
|
||||||
return BadRequestError(Error("It looks like some details from the first login are not found. Please try login one again"))
|
return BadRequestError(Error("It looks like some details from the first login are not found. Please try login one again"))
|
||||||
}
|
}
|
||||||
|
|
||||||
const server = new jsrp.server();
|
const server = new jsrp.server();
|
||||||
server.init(
|
server.init(
|
||||||
{
|
{
|
||||||
salt: user.salt,
|
salt: user.salt,
|
||||||
verifier: user.verifier,
|
verifier: user.verifier,
|
||||||
b: loginSRPDetailFromDB.serverBInt
|
b: loginSRPDetailFromDB.serverBInt
|
||||||
},
|
},
|
||||||
async () => {
|
async () => {
|
||||||
server.setClientPublicKey(
|
server.setClientPublicKey(
|
||||||
loginSRPDetailFromDB.clientPublicKey
|
loginSRPDetailFromDB.clientPublicKey
|
||||||
);
|
);
|
||||||
|
|
||||||
// compare server and client shared keys
|
// compare server and client shared keys
|
||||||
if (server.checkClientProof(clientProof)) {
|
if (server.checkClientProof(clientProof)) {
|
||||||
// create new or replace backup private key
|
// create new or replace backup private key
|
||||||
|
|
||||||
const backupPrivateKey = await BackupPrivateKey.findOneAndUpdate(
|
const backupPrivateKey = await BackupPrivateKey.findOneAndUpdate(
|
||||||
{ user: req.user._id },
|
{ user: req.user._id },
|
||||||
{
|
{
|
||||||
user: req.user._id,
|
user: req.user._id,
|
||||||
encryptedPrivateKey,
|
encryptedPrivateKey,
|
||||||
iv,
|
iv,
|
||||||
tag,
|
tag,
|
||||||
salt,
|
salt,
|
||||||
verifier
|
verifier
|
||||||
},
|
},
|
||||||
{ upsert: true, new: true }
|
{ upsert: true, new: true }
|
||||||
).select('+user, encryptedPrivateKey');
|
).select('+user, encryptedPrivateKey');
|
||||||
|
|
||||||
// issue tokens
|
// issue tokens
|
||||||
return res.status(200).send({
|
return res.status(200).send({
|
||||||
message: 'Successfully updated backup private key',
|
message: 'Successfully updated backup private key',
|
||||||
backupPrivateKey
|
backupPrivateKey
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
return res.status(400).send({
|
return res.status(400).send({
|
||||||
message: 'Failed to update backup private key'
|
message: 'Failed to update backup private key'
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
} 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
|
* @returns
|
||||||
*/
|
*/
|
||||||
export const getBackupPrivateKey = async (req: Request, res: Response) => {
|
export const getBackupPrivateKey = async (req: Request, res: Response) => {
|
||||||
let backupPrivateKey;
|
const backupPrivateKey = await BackupPrivateKey.findOne({
|
||||||
try {
|
user: req.user._id
|
||||||
backupPrivateKey = await BackupPrivateKey.findOne({
|
}).select('+encryptedPrivateKey +iv +tag');
|
||||||
user: req.user._id
|
|
||||||
}).select('+encryptedPrivateKey +iv +tag');
|
|
||||||
|
|
||||||
if (!backupPrivateKey) throw new Error('Failed to find backup private key');
|
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({
|
return res.status(200).send({
|
||||||
backupPrivateKey
|
backupPrivateKey
|
||||||
@@ -345,44 +320,36 @@ export const getBackupPrivateKey = async (req: Request, res: Response) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const resetPassword = async (req: Request, res: Response) => {
|
export const resetPassword = async (req: Request, res: Response) => {
|
||||||
try {
|
const {
|
||||||
const {
|
protectedKey,
|
||||||
protectedKey,
|
protectedKeyIV,
|
||||||
protectedKeyIV,
|
protectedKeyTag,
|
||||||
protectedKeyTag,
|
encryptedPrivateKey,
|
||||||
encryptedPrivateKey,
|
encryptedPrivateKeyIV,
|
||||||
encryptedPrivateKeyIV,
|
encryptedPrivateKeyTag,
|
||||||
encryptedPrivateKeyTag,
|
salt,
|
||||||
salt,
|
verifier,
|
||||||
verifier,
|
} = req.body;
|
||||||
} = req.body;
|
|
||||||
|
|
||||||
await User.findByIdAndUpdate(
|
await User.findByIdAndUpdate(
|
||||||
req.user._id.toString(),
|
req.user._id.toString(),
|
||||||
{
|
{
|
||||||
encryptionVersion: 2,
|
encryptionVersion: 2,
|
||||||
protectedKey,
|
protectedKey,
|
||||||
protectedKeyIV,
|
protectedKeyIV,
|
||||||
protectedKeyTag,
|
protectedKeyTag,
|
||||||
encryptedPrivateKey,
|
encryptedPrivateKey,
|
||||||
iv: encryptedPrivateKeyIV,
|
iv: encryptedPrivateKeyIV,
|
||||||
tag: encryptedPrivateKeyTag,
|
tag: encryptedPrivateKeyTag,
|
||||||
salt,
|
salt,
|
||||||
verifier
|
verifier
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
new: true
|
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({
|
return res.status(200).send({
|
||||||
message: 'Successfully reset password'
|
message: 'Successfully reset password'
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
import { Request, Response } from 'express';
|
import { Request, Response } from 'express';
|
||||||
import * as Sentry from '@sentry/node';
|
|
||||||
import { Types } from 'mongoose';
|
import { Types } from 'mongoose';
|
||||||
import { Key, Secret } from '../../models';
|
import { Key, Secret } from '../../models';
|
||||||
import {
|
import {
|
||||||
@@ -37,66 +36,56 @@ interface PushSecret {
|
|||||||
*/
|
*/
|
||||||
export const pushSecrets = async (req: Request, res: Response) => {
|
export const pushSecrets = async (req: Request, res: Response) => {
|
||||||
// upload (encrypted) secrets to workspace with id [workspaceId]
|
// upload (encrypted) secrets to workspace with id [workspaceId]
|
||||||
|
const postHogClient = await TelemetryService.getPostHogClient();
|
||||||
|
let { secrets }: { secrets: PushSecret[] } = req.body;
|
||||||
|
const { keys, environment, channel } = req.body;
|
||||||
|
const { workspaceId } = req.params;
|
||||||
|
|
||||||
try {
|
// validate environment
|
||||||
const postHogClient = await TelemetryService.getPostHogClient();
|
const workspaceEnvs = req.membership.workspace.environments;
|
||||||
let { secrets }: { secrets: PushSecret[] } = req.body;
|
if (!workspaceEnvs.find(({ slug }: { slug: string }) => slug === environment)) {
|
||||||
const { keys, environment, channel } = req.body;
|
throw new Error('Failed to validate environment');
|
||||||
const { workspaceId } = req.params;
|
}
|
||||||
|
|
||||||
// validate environment
|
// sanitize secrets
|
||||||
const workspaceEnvs = req.membership.workspace.environments;
|
secrets = secrets.filter(
|
||||||
if (!workspaceEnvs.find(({ slug }: { slug: string }) => slug === environment)) {
|
(s: PushSecret) => s.ciphertextKey !== '' && s.ciphertextValue !== ''
|
||||||
throw new Error('Failed to validate environment');
|
);
|
||||||
}
|
|
||||||
|
|
||||||
// sanitize secrets
|
await push({
|
||||||
secrets = secrets.filter(
|
userId: req.user._id,
|
||||||
(s: PushSecret) => s.ciphertextKey !== '' && s.ciphertextValue !== ''
|
workspaceId,
|
||||||
);
|
environment,
|
||||||
|
secrets
|
||||||
|
});
|
||||||
|
|
||||||
await push({
|
await pushKeys({
|
||||||
userId: req.user._id,
|
userId: req.user._id,
|
||||||
workspaceId,
|
workspaceId,
|
||||||
environment,
|
keys
|
||||||
secrets
|
});
|
||||||
});
|
|
||||||
|
|
||||||
|
if (postHogClient) {
|
||||||
|
postHogClient.capture({
|
||||||
|
event: 'secrets pushed',
|
||||||
|
distinctId: req.user.email,
|
||||||
|
properties: {
|
||||||
|
numberOfSecrets: secrets.length,
|
||||||
|
environment,
|
||||||
|
workspaceId,
|
||||||
|
channel: channel ? channel : 'cli'
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
await pushKeys({
|
// trigger event - push secrets
|
||||||
userId: req.user._id,
|
EventService.handleEvent({
|
||||||
workspaceId,
|
event: eventPushSecrets({
|
||||||
keys
|
workspaceId: new Types.ObjectId(workspaceId),
|
||||||
});
|
environment
|
||||||
|
})
|
||||||
|
});
|
||||||
if (postHogClient) {
|
|
||||||
postHogClient.capture({
|
|
||||||
event: 'secrets pushed',
|
|
||||||
distinctId: req.user.email,
|
|
||||||
properties: {
|
|
||||||
numberOfSecrets: secrets.length,
|
|
||||||
environment,
|
|
||||||
workspaceId,
|
|
||||||
channel: channel ? channel : 'cli'
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// trigger event - push secrets
|
|
||||||
EventService.handleEvent({
|
|
||||||
event: eventPushSecrets({
|
|
||||||
workspaceId: new Types.ObjectId(workspaceId),
|
|
||||||
environment
|
|
||||||
})
|
|
||||||
});
|
|
||||||
|
|
||||||
} 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({
|
return res.status(200).send({
|
||||||
message: 'Successfully uploaded workspace secrets'
|
message: 'Successfully uploaded workspace secrets'
|
||||||
@@ -113,55 +102,48 @@ export const pushSecrets = async (req: Request, res: Response) => {
|
|||||||
export const pullSecrets = async (req: Request, res: Response) => {
|
export const pullSecrets = async (req: Request, res: Response) => {
|
||||||
let secrets;
|
let secrets;
|
||||||
let key;
|
let key;
|
||||||
try {
|
|
||||||
const postHogClient = await TelemetryService.getPostHogClient();
|
|
||||||
const environment: string = req.query.environment as string;
|
|
||||||
const channel: string = req.query.channel as string;
|
|
||||||
const { workspaceId } = req.params;
|
|
||||||
|
|
||||||
// validate environment
|
const postHogClient = await TelemetryService.getPostHogClient();
|
||||||
const workspaceEnvs = req.membership.workspace.environments;
|
const environment: string = req.query.environment as string;
|
||||||
if (!workspaceEnvs.find(({ slug }: { slug: string }) => slug === environment)) {
|
const channel: string = req.query.channel as string;
|
||||||
throw new Error('Failed to validate environment');
|
const { workspaceId } = req.params;
|
||||||
}
|
|
||||||
|
|
||||||
secrets = await pull({
|
// validate environment
|
||||||
userId: req.user._id.toString(),
|
const workspaceEnvs = req.membership.workspace.environments;
|
||||||
workspaceId,
|
if (!workspaceEnvs.find(({ slug }: { slug: string }) => slug === environment)) {
|
||||||
environment,
|
throw new Error('Failed to validate environment');
|
||||||
channel: channel ? channel : 'cli',
|
}
|
||||||
ipAddress: req.ip
|
|
||||||
});
|
|
||||||
|
|
||||||
key = await Key.findOne({
|
secrets = await pull({
|
||||||
workspace: workspaceId,
|
userId: req.user._id.toString(),
|
||||||
receiver: req.user._id
|
workspaceId,
|
||||||
})
|
environment,
|
||||||
.sort({ createdAt: -1 })
|
channel: channel ? channel : 'cli',
|
||||||
.populate('sender', '+publicKey');
|
ipAddress: req.realIP
|
||||||
|
});
|
||||||
if (channel !== 'cli') {
|
|
||||||
secrets = reformatPullSecrets({ secrets });
|
|
||||||
}
|
|
||||||
|
|
||||||
if (postHogClient) {
|
key = await Key.findOne({
|
||||||
// capture secrets pushed event in production
|
workspace: workspaceId,
|
||||||
postHogClient.capture({
|
receiver: req.user._id
|
||||||
distinctId: req.user.email,
|
})
|
||||||
event: 'secrets pulled',
|
.sort({ createdAt: -1 })
|
||||||
properties: {
|
.populate('sender', '+publicKey');
|
||||||
numberOfSecrets: secrets.length,
|
|
||||||
environment,
|
if (channel !== 'cli') {
|
||||||
workspaceId,
|
secrets = reformatPullSecrets({ secrets });
|
||||||
channel: channel ? channel : 'cli'
|
}
|
||||||
}
|
|
||||||
});
|
if (postHogClient) {
|
||||||
}
|
// capture secrets pushed event in production
|
||||||
} catch (err) {
|
postHogClient.capture({
|
||||||
Sentry.setUser({ email: req.user.email });
|
distinctId: req.user.email,
|
||||||
Sentry.captureException(err);
|
event: 'secrets pulled',
|
||||||
return res.status(400).send({
|
properties: {
|
||||||
message: 'Failed to pull workspace secrets'
|
numberOfSecrets: secrets.length,
|
||||||
|
environment,
|
||||||
|
workspaceId,
|
||||||
|
channel: channel ? channel : 'cli'
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -182,54 +164,47 @@ export const pullSecrets = async (req: Request, res: Response) => {
|
|||||||
export const pullSecretsServiceToken = async (req: Request, res: Response) => {
|
export const pullSecretsServiceToken = async (req: Request, res: Response) => {
|
||||||
let secrets;
|
let secrets;
|
||||||
let key;
|
let key;
|
||||||
try {
|
|
||||||
const postHogClient = await TelemetryService.getPostHogClient();
|
|
||||||
const environment: string = req.query.environment as string;
|
|
||||||
const channel: string = req.query.channel as string;
|
|
||||||
const { workspaceId } = req.params;
|
|
||||||
|
|
||||||
// validate environment
|
const postHogClient = await TelemetryService.getPostHogClient();
|
||||||
const workspaceEnvs = req.membership.workspace.environments;
|
const environment: string = req.query.environment as string;
|
||||||
if (!workspaceEnvs.find(({ slug }: { slug: string }) => slug === environment)) {
|
const channel: string = req.query.channel as string;
|
||||||
throw new Error('Failed to validate environment');
|
const { workspaceId } = req.params;
|
||||||
}
|
|
||||||
|
|
||||||
secrets = await pull({
|
// validate environment
|
||||||
userId: req.serviceToken.user._id.toString(),
|
const workspaceEnvs = req.membership.workspace.environments;
|
||||||
workspaceId,
|
if (!workspaceEnvs.find(({ slug }: { slug: string }) => slug === environment)) {
|
||||||
environment,
|
throw new Error('Failed to validate environment');
|
||||||
channel: 'cli',
|
}
|
||||||
ipAddress: req.ip
|
|
||||||
});
|
|
||||||
|
|
||||||
key = {
|
secrets = await pull({
|
||||||
encryptedKey: req.serviceToken.encryptedKey,
|
userId: req.serviceToken.user._id.toString(),
|
||||||
nonce: req.serviceToken.nonce,
|
workspaceId,
|
||||||
sender: {
|
environment,
|
||||||
publicKey: req.serviceToken.publicKey
|
channel: 'cli',
|
||||||
},
|
ipAddress: req.realIP
|
||||||
receiver: req.serviceToken.user,
|
});
|
||||||
workspace: req.serviceToken.workspace
|
|
||||||
};
|
|
||||||
|
|
||||||
if (postHogClient) {
|
key = {
|
||||||
// capture secrets pulled event in production
|
encryptedKey: req.serviceToken.encryptedKey,
|
||||||
postHogClient.capture({
|
nonce: req.serviceToken.nonce,
|
||||||
distinctId: req.serviceToken.user.email,
|
sender: {
|
||||||
event: 'secrets pulled',
|
publicKey: req.serviceToken.publicKey
|
||||||
properties: {
|
},
|
||||||
numberOfSecrets: secrets.length,
|
receiver: req.serviceToken.user,
|
||||||
environment,
|
workspace: req.serviceToken.workspace
|
||||||
workspaceId,
|
};
|
||||||
channel: channel ? channel : 'cli'
|
|
||||||
}
|
if (postHogClient) {
|
||||||
});
|
// capture secrets pulled event in production
|
||||||
}
|
postHogClient.capture({
|
||||||
} catch (err) {
|
distinctId: req.serviceToken.user.email,
|
||||||
Sentry.setUser({ email: req.serviceToken.user.email });
|
event: 'secrets pulled',
|
||||||
Sentry.captureException(err);
|
properties: {
|
||||||
return res.status(400).send({
|
numberOfSecrets: secrets.length,
|
||||||
message: 'Failed to pull workspace secrets'
|
environment,
|
||||||
|
workspaceId,
|
||||||
|
channel: channel ? channel : 'cli'
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -237,4 +212,4 @@ export const pullSecretsServiceToken = async (req: Request, res: Response) => {
|
|||||||
secrets: reformatPullSecrets({ secrets }),
|
secrets: reformatPullSecrets({ secrets }),
|
||||||
key
|
key
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
import { Request, Response } from 'express';
|
import { Request, Response } from 'express';
|
||||||
import * as Sentry from '@sentry/node';
|
|
||||||
import { User } from '../../models';
|
import { User } from '../../models';
|
||||||
import {
|
import {
|
||||||
sendEmailVerification,
|
sendEmailVerification,
|
||||||
@@ -8,6 +7,7 @@ import {
|
|||||||
import { createToken } from '../../helpers/auth';
|
import { createToken } from '../../helpers/auth';
|
||||||
import { BadRequestError } from '../../utils/errors';
|
import { BadRequestError } from '../../utils/errors';
|
||||||
import { getInviteOnlySignup, getJwtSignupLifetime, getJwtSignupSecret, getSmtpConfigured } from '../../config';
|
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
|
* Signup step 1: Initialize account for user under email [email] and send a verification code
|
||||||
@@ -18,28 +18,23 @@ import { getInviteOnlySignup, getJwtSignupLifetime, getJwtSignupSecret, getSmtpC
|
|||||||
*/
|
*/
|
||||||
export const beginEmailSignup = async (req: Request, res: Response) => {
|
export const beginEmailSignup = async (req: Request, res: Response) => {
|
||||||
let email: string;
|
let email: string;
|
||||||
try {
|
email = req.body.email;
|
||||||
email = req.body.email;
|
|
||||||
|
// validate that email is not disposable
|
||||||
|
validateUserEmail(email);
|
||||||
|
|
||||||
const user = await User.findOne({ email }).select('+publicKey');
|
const user = await User.findOne({ email }).select('+publicKey');
|
||||||
if (user && user?.publicKey) {
|
if (user && user?.publicKey) {
|
||||||
// case: user has already completed account
|
// case: user has already completed account
|
||||||
|
|
||||||
return res.status(403).send({
|
return res.status(403).send({
|
||||||
error: 'Failed to send email verification code for complete account'
|
error: 'Failed to send email verification code for complete account'
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// send send verification email
|
||||||
|
await sendEmailVerification({ email });
|
||||||
|
|
||||||
// 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({
|
return res.status(200).send({
|
||||||
message: `Sent an email verification code to ${email}`
|
message: `Sent an email verification code to ${email}`
|
||||||
});
|
});
|
||||||
@@ -54,55 +49,47 @@ export const beginEmailSignup = async (req: Request, res: Response) => {
|
|||||||
*/
|
*/
|
||||||
export const verifyEmailSignup = async (req: Request, res: Response) => {
|
export const verifyEmailSignup = async (req: Request, res: Response) => {
|
||||||
let user, token;
|
let user, token;
|
||||||
try {
|
const { email, code } = req.body;
|
||||||
const { email, code } = req.body;
|
|
||||||
|
|
||||||
// initialize user account
|
// initialize user account
|
||||||
user = await User.findOne({ email }).select('+publicKey');
|
user = await User.findOne({ email }).select('+publicKey');
|
||||||
if (user && user?.publicKey) {
|
if (user && user?.publicKey) {
|
||||||
// case: user has already completed account
|
// case: user has already completed account
|
||||||
return res.status(403).send({
|
return res.status(403).send({
|
||||||
error: 'Failed email verification for complete user'
|
error: 'Failed email verification for complete user'
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if (await getInviteOnlySignup()) {
|
if (await getInviteOnlySignup()) {
|
||||||
// Only one user can create an account without being invited. The rest need to be invited in order to make an account
|
// Only one user can create an account without being invited. The rest need to be invited in order to make an account
|
||||||
const userCount = await User.countDocuments({})
|
const userCount = await User.countDocuments({})
|
||||||
if (userCount != 0) {
|
if (userCount != 0) {
|
||||||
throw BadRequestError({ message: "New user sign ups are not allowed at this time. You must be invited to sign up." })
|
throw BadRequestError({ message: "New user sign ups are not allowed at this time. You must be invited to sign up." })
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// verify email
|
// verify email
|
||||||
if (await getSmtpConfigured()) {
|
if (await getSmtpConfigured()) {
|
||||||
await checkEmailVerification({
|
await checkEmailVerification({
|
||||||
email,
|
email,
|
||||||
code
|
code
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!user) {
|
if (!user) {
|
||||||
user = await new User({
|
user = await new User({
|
||||||
email
|
email
|
||||||
}).save();
|
}).save();
|
||||||
}
|
}
|
||||||
|
|
||||||
// generate temporary signup token
|
// generate temporary signup token
|
||||||
token = createToken({
|
token = createToken({
|
||||||
payload: {
|
payload: {
|
||||||
userId: user._id.toString()
|
userId: user._id.toString()
|
||||||
},
|
},
|
||||||
expiresIn: await getJwtSignupLifetime(),
|
expiresIn: await getJwtSignupLifetime(),
|
||||||
secret: await getJwtSignupSecret()
|
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({
|
return res.status(200).send({
|
||||||
message: 'Successfuly verified email',
|
message: 'Successfuly verified email',
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
import { Request, Response } from 'express';
|
import { Request, Response } from 'express';
|
||||||
import * as Sentry from '@sentry/node';
|
|
||||||
import Stripe from 'stripe';
|
import Stripe from 'stripe';
|
||||||
import { getStripeSecretKey, getStripeWebhookSecret } from '../../config';
|
import { getStripeSecretKey, getStripeWebhookSecret } from '../../config';
|
||||||
|
|
||||||
@@ -10,26 +9,17 @@ import { getStripeSecretKey, getStripeWebhookSecret } from '../../config';
|
|||||||
* @returns
|
* @returns
|
||||||
*/
|
*/
|
||||||
export const handleWebhook = async (req: Request, res: Response) => {
|
export const handleWebhook = async (req: Request, res: Response) => {
|
||||||
let event;
|
// check request for valid stripe signature
|
||||||
try {
|
const stripe = new Stripe(await getStripeSecretKey(), {
|
||||||
// check request for valid stripe signature
|
apiVersion: '2022-08-01'
|
||||||
const stripe = new Stripe(await getStripeSecretKey(), {
|
});
|
||||||
apiVersion: '2022-08-01'
|
|
||||||
});
|
|
||||||
|
|
||||||
const sig = req.headers['stripe-signature'] as string;
|
const sig = req.headers['stripe-signature'] as string;
|
||||||
event = stripe.webhooks.constructEvent(
|
const event = stripe.webhooks.constructEvent(
|
||||||
req.body,
|
req.body,
|
||||||
sig,
|
sig,
|
||||||
await getStripeWebhookSecret()
|
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) {
|
switch (event.type) {
|
||||||
case '':
|
case '':
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
import { Request, Response } from 'express';
|
import { Request, Response } from 'express';
|
||||||
import * as Sentry from '@sentry/node';
|
|
||||||
import { UserAction } from '../../models';
|
import { UserAction } from '../../models';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -11,28 +10,19 @@ import { UserAction } from '../../models';
|
|||||||
export const addUserAction = async (req: Request, res: Response) => {
|
export const addUserAction = async (req: Request, res: Response) => {
|
||||||
// add/record new action [action] for user with id [req.user._id]
|
// add/record new action [action] for user with id [req.user._id]
|
||||||
|
|
||||||
let userAction;
|
const { action } = req.body;
|
||||||
try {
|
|
||||||
const { action } = req.body;
|
|
||||||
|
|
||||||
userAction = await UserAction.findOneAndUpdate(
|
const userAction = await UserAction.findOneAndUpdate(
|
||||||
{
|
{
|
||||||
user: req.user._id,
|
user: req.user._id,
|
||||||
action
|
action
|
||||||
},
|
},
|
||||||
{ user: req.user._id, action },
|
{ user: req.user._id, action },
|
||||||
{
|
{
|
||||||
new: true,
|
new: true,
|
||||||
upsert: true
|
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({
|
return res.status(200).send({
|
||||||
message: 'Successfully recorded user action',
|
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) => {
|
export const getUserAction = async (req: Request, res: Response) => {
|
||||||
// get user action [action] for user with id [req.user._id]
|
// get user action [action] for user with id [req.user._id]
|
||||||
let userAction;
|
const action: string = req.query.action as string;
|
||||||
try {
|
|
||||||
const action: string = req.query.action as string;
|
|
||||||
|
|
||||||
userAction = await UserAction.findOne({
|
const userAction = await UserAction.findOne({
|
||||||
user: req.user._id,
|
user: req.user._id,
|
||||||
action
|
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({
|
return res.status(200).send({
|
||||||
userAction
|
userAction
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
import { Request, Response } from "express";
|
import { Request, Response } from "express";
|
||||||
import * as Sentry from "@sentry/node";
|
|
||||||
import {
|
import {
|
||||||
Workspace,
|
Workspace,
|
||||||
Membership,
|
Membership,
|
||||||
@@ -14,6 +13,7 @@ import {
|
|||||||
createWorkspace as create,
|
createWorkspace as create,
|
||||||
deleteWorkspace as deleteWork,
|
deleteWorkspace as deleteWork,
|
||||||
} from "../../helpers/workspace";
|
} from "../../helpers/workspace";
|
||||||
|
import { EELicenseService } from '../../ee/services';
|
||||||
import { addMemberships } from "../../helpers/membership";
|
import { addMemberships } from "../../helpers/membership";
|
||||||
import { ADMIN } from "../../variables";
|
import { ADMIN } from "../../variables";
|
||||||
|
|
||||||
@@ -24,27 +24,18 @@ import { ADMIN } from "../../variables";
|
|||||||
* @returns
|
* @returns
|
||||||
*/
|
*/
|
||||||
export const getWorkspacePublicKeys = async (req: Request, res: Response) => {
|
export const getWorkspacePublicKeys = async (req: Request, res: Response) => {
|
||||||
let publicKeys;
|
const { workspaceId } = req.params;
|
||||||
try {
|
|
||||||
const { workspaceId } = req.params;
|
|
||||||
|
|
||||||
publicKeys = (
|
const publicKeys = (
|
||||||
await Membership.find({
|
await Membership.find({
|
||||||
workspace: workspaceId,
|
workspace: workspaceId,
|
||||||
}).populate<{ user: IUser }>("user", "publicKey")
|
}).populate<{ user: IUser }>("user", "publicKey")
|
||||||
).map((member) => {
|
).map((member) => {
|
||||||
return {
|
return {
|
||||||
publicKey: member.user.publicKey,
|
publicKey: member.user.publicKey,
|
||||||
userId: member.user._id,
|
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({
|
return res.status(200).send({
|
||||||
publicKeys,
|
publicKeys,
|
||||||
@@ -58,20 +49,11 @@ export const getWorkspacePublicKeys = async (req: Request, res: Response) => {
|
|||||||
* @returns
|
* @returns
|
||||||
*/
|
*/
|
||||||
export const getWorkspaceMemberships = async (req: Request, res: Response) => {
|
export const getWorkspaceMemberships = async (req: Request, res: Response) => {
|
||||||
let users;
|
const { workspaceId } = req.params;
|
||||||
try {
|
|
||||||
const { workspaceId } = req.params;
|
|
||||||
|
|
||||||
users = await Membership.find({
|
const users = await Membership.find({
|
||||||
workspace: workspaceId,
|
workspace: workspaceId,
|
||||||
}).populate("user", "+publicKey");
|
}).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({
|
return res.status(200).send({
|
||||||
users,
|
users,
|
||||||
@@ -85,20 +67,11 @@ export const getWorkspaceMemberships = async (req: Request, res: Response) => {
|
|||||||
* @returns
|
* @returns
|
||||||
*/
|
*/
|
||||||
export const getWorkspaces = async (req: Request, res: Response) => {
|
export const getWorkspaces = async (req: Request, res: Response) => {
|
||||||
let workspaces;
|
const workspaces = (
|
||||||
try {
|
await Membership.find({
|
||||||
workspaces = (
|
user: req.user._id,
|
||||||
await Membership.find({
|
}).populate("workspace")
|
||||||
user: req.user._id,
|
).map((m) => m.workspace);
|
||||||
}).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({
|
return res.status(200).send({
|
||||||
workspaces,
|
workspaces,
|
||||||
@@ -112,20 +85,11 @@ export const getWorkspaces = async (req: Request, res: Response) => {
|
|||||||
* @returns
|
* @returns
|
||||||
*/
|
*/
|
||||||
export const getWorkspace = async (req: Request, res: Response) => {
|
export const getWorkspace = async (req: Request, res: Response) => {
|
||||||
let workspace;
|
const { workspaceId } = req.params;
|
||||||
try {
|
|
||||||
const { workspaceId } = req.params;
|
|
||||||
|
|
||||||
workspace = await Workspace.findOne({
|
const workspace = await Workspace.findOne({
|
||||||
_id: workspaceId,
|
_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({
|
return res.status(200).send({
|
||||||
workspace,
|
workspace,
|
||||||
@@ -140,43 +104,46 @@ export const getWorkspace = async (req: Request, res: Response) => {
|
|||||||
* @returns
|
* @returns
|
||||||
*/
|
*/
|
||||||
export const createWorkspace = async (req: Request, res: Response) => {
|
export const createWorkspace = async (req: Request, res: Response) => {
|
||||||
let workspace;
|
const { workspaceName, organizationId } = req.body;
|
||||||
try {
|
|
||||||
const { workspaceName, organizationId } = req.body;
|
|
||||||
|
|
||||||
// validate organization membership
|
// validate organization membership
|
||||||
const membershipOrg = await MembershipOrg.findOne({
|
const membershipOrg = await MembershipOrg.findOne({
|
||||||
user: req.user._id,
|
user: req.user._id,
|
||||||
organization: organizationId,
|
organization: organizationId,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!membershipOrg) {
|
if (!membershipOrg) {
|
||||||
throw new Error("Failed to validate organization membership");
|
throw new Error("Failed to validate organization membership");
|
||||||
}
|
|
||||||
|
|
||||||
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({
|
|
||||||
name: workspaceName,
|
|
||||||
organizationId,
|
|
||||||
});
|
|
||||||
|
|
||||||
await addMemberships({
|
|
||||||
userIds: [req.user._id],
|
|
||||||
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",
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
const workspace = await create({
|
||||||
|
name: workspaceName,
|
||||||
|
organizationId,
|
||||||
|
});
|
||||||
|
|
||||||
|
await addMemberships({
|
||||||
|
userIds: [req.user._id],
|
||||||
|
workspaceId: workspace._id.toString(),
|
||||||
|
roles: [ADMIN],
|
||||||
|
});
|
||||||
|
|
||||||
return res.status(200).send({
|
return res.status(200).send({
|
||||||
workspace,
|
workspace,
|
||||||
});
|
});
|
||||||
@@ -189,20 +156,12 @@ export const createWorkspace = async (req: Request, res: Response) => {
|
|||||||
* @returns
|
* @returns
|
||||||
*/
|
*/
|
||||||
export const deleteWorkspace = async (req: Request, res: Response) => {
|
export const deleteWorkspace = async (req: Request, res: Response) => {
|
||||||
try {
|
const { workspaceId } = req.params;
|
||||||
const { workspaceId } = req.params;
|
|
||||||
|
|
||||||
// delete workspace
|
// delete workspace
|
||||||
await deleteWork({
|
await deleteWork({
|
||||||
id: workspaceId,
|
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({
|
return res.status(200).send({
|
||||||
message: "Successfully deleted workspace",
|
message: "Successfully deleted workspace",
|
||||||
@@ -216,29 +175,20 @@ export const deleteWorkspace = async (req: Request, res: Response) => {
|
|||||||
* @returns
|
* @returns
|
||||||
*/
|
*/
|
||||||
export const changeWorkspaceName = async (req: Request, res: Response) => {
|
export const changeWorkspaceName = async (req: Request, res: Response) => {
|
||||||
let workspace;
|
const { workspaceId } = req.params;
|
||||||
try {
|
const { name } = req.body;
|
||||||
const { workspaceId } = req.params;
|
|
||||||
const { name } = req.body;
|
|
||||||
|
|
||||||
workspace = await Workspace.findOneAndUpdate(
|
const workspace = await Workspace.findOneAndUpdate(
|
||||||
{
|
{
|
||||||
_id: workspaceId,
|
_id: workspaceId,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name,
|
name,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
new: true,
|
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({
|
return res.status(200).send({
|
||||||
message: "Successfully changed workspace name",
|
message: "Successfully changed workspace name",
|
||||||
@@ -253,20 +203,11 @@ export const changeWorkspaceName = async (req: Request, res: Response) => {
|
|||||||
* @returns
|
* @returns
|
||||||
*/
|
*/
|
||||||
export const getWorkspaceIntegrations = async (req: Request, res: Response) => {
|
export const getWorkspaceIntegrations = async (req: Request, res: Response) => {
|
||||||
let integrations;
|
const { workspaceId } = req.params;
|
||||||
try {
|
|
||||||
const { workspaceId } = req.params;
|
|
||||||
|
|
||||||
integrations = await Integration.find({
|
const integrations = await Integration.find({
|
||||||
workspace: workspaceId,
|
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({
|
return res.status(200).send({
|
||||||
integrations,
|
integrations,
|
||||||
@@ -283,20 +224,11 @@ export const getWorkspaceIntegrationAuthorizations = async (
|
|||||||
req: Request,
|
req: Request,
|
||||||
res: Response
|
res: Response
|
||||||
) => {
|
) => {
|
||||||
let authorizations;
|
const { workspaceId } = req.params;
|
||||||
try {
|
|
||||||
const { workspaceId } = req.params;
|
|
||||||
|
|
||||||
authorizations = await IntegrationAuth.find({
|
const authorizations = await IntegrationAuth.find({
|
||||||
workspace: workspaceId,
|
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({
|
return res.status(200).send({
|
||||||
authorizations,
|
authorizations,
|
||||||
@@ -313,21 +245,12 @@ export const getWorkspaceServiceTokens = async (
|
|||||||
req: Request,
|
req: Request,
|
||||||
res: Response
|
res: Response
|
||||||
) => {
|
) => {
|
||||||
let serviceTokens;
|
const { workspaceId } = req.params;
|
||||||
try {
|
// ?? FIX.
|
||||||
const { workspaceId } = req.params;
|
const serviceTokens = await ServiceToken.find({
|
||||||
// ?? FIX.
|
user: req.user._id,
|
||||||
serviceTokens = await ServiceToken.find({
|
workspace: workspaceId,
|
||||||
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({
|
return res.status(200).send({
|
||||||
serviceTokens,
|
serviceTokens,
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
import * as Sentry from '@sentry/node';
|
|
||||||
import { Request, Response } from 'express';
|
import { Request, Response } from 'express';
|
||||||
import crypto from 'crypto';
|
import crypto from 'crypto';
|
||||||
import bcrypt from 'bcrypt';
|
import bcrypt from 'bcrypt';
|
||||||
@@ -14,18 +13,9 @@ import { getSaltRounds } from '../../config';
|
|||||||
* @returns
|
* @returns
|
||||||
*/
|
*/
|
||||||
export const getAPIKeyData = async (req: Request, res: Response) => {
|
export const getAPIKeyData = async (req: Request, res: Response) => {
|
||||||
let apiKeyData;
|
const apiKeyData = await APIKeyData.find({
|
||||||
try {
|
user: req.user._id
|
||||||
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({
|
return res.status(200).send({
|
||||||
apiKeyData
|
apiKeyData
|
||||||
@@ -38,39 +28,30 @@ export const getAPIKeyData = async (req: Request, res: Response) => {
|
|||||||
* @param res
|
* @param res
|
||||||
*/
|
*/
|
||||||
export const createAPIKeyData = async (req: Request, res: Response) => {
|
export const createAPIKeyData = async (req: Request, res: Response) => {
|
||||||
let apiKey, apiKeyData;
|
const { name, expiresIn } = req.body;
|
||||||
try {
|
|
||||||
const { name, expiresIn } = req.body;
|
|
||||||
|
|
||||||
const secret = crypto.randomBytes(16).toString('hex');
|
|
||||||
const secretHash = await bcrypt.hash(secret, await getSaltRounds());
|
|
||||||
|
|
||||||
const expiresAt = new Date();
|
|
||||||
expiresAt.setSeconds(expiresAt.getSeconds() + expiresIn);
|
|
||||||
|
|
||||||
apiKeyData = await new APIKeyData({
|
|
||||||
name,
|
|
||||||
lastUsed: new Date(),
|
|
||||||
expiresAt,
|
|
||||||
user: req.user._id,
|
|
||||||
secretHash
|
|
||||||
}).save();
|
|
||||||
|
|
||||||
// return api key data without sensitive data
|
|
||||||
apiKeyData = await APIKeyData.findById(apiKeyData._id);
|
|
||||||
|
|
||||||
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 secret = crypto.randomBytes(16).toString('hex');
|
||||||
|
const secretHash = await bcrypt.hash(secret, await getSaltRounds());
|
||||||
|
|
||||||
|
const expiresAt = new Date();
|
||||||
|
expiresAt.setSeconds(expiresAt.getSeconds() + expiresIn);
|
||||||
|
|
||||||
|
let apiKeyData = await new APIKeyData({
|
||||||
|
name,
|
||||||
|
lastUsed: new Date(),
|
||||||
|
expiresAt,
|
||||||
|
user: req.user._id,
|
||||||
|
secretHash
|
||||||
|
}).save();
|
||||||
|
|
||||||
|
// return api key data without sensitive data
|
||||||
|
// FIX: fix this any
|
||||||
|
apiKeyData = await APIKeyData.findById(apiKeyData._id) as any
|
||||||
|
|
||||||
|
if (!apiKeyData) throw new Error('Failed to find API key data');
|
||||||
|
|
||||||
|
const apiKey = `ak.${apiKeyData._id.toString()}.${secret}`;
|
||||||
|
|
||||||
return res.status(200).send({
|
return res.status(200).send({
|
||||||
apiKey,
|
apiKey,
|
||||||
apiKeyData
|
apiKeyData
|
||||||
@@ -84,21 +65,10 @@ export const createAPIKeyData = async (req: Request, res: Response) => {
|
|||||||
* @returns
|
* @returns
|
||||||
*/
|
*/
|
||||||
export const deleteAPIKeyData = async (req: Request, res: Response) => {
|
export const deleteAPIKeyData = async (req: Request, res: Response) => {
|
||||||
let apiKeyData;
|
const { apiKeyDataId } = req.params;
|
||||||
try {
|
const apiKeyData = await APIKeyData.findByIdAndDelete(apiKeyDataId);
|
||||||
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'
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return res.status(200).send({
|
return res.status(200).send({
|
||||||
apiKeyData
|
apiKeyData
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
/* eslint-disable @typescript-eslint/no-var-requires */
|
/* eslint-disable @typescript-eslint/no-var-requires */
|
||||||
import { Request, Response } from 'express';
|
import { Request, Response } from 'express';
|
||||||
import jwt from 'jsonwebtoken';
|
import jwt from 'jsonwebtoken';
|
||||||
import * as Sentry from '@sentry/node';
|
|
||||||
import * as bigintConversion from 'bigint-conversion';
|
import * as bigintConversion from 'bigint-conversion';
|
||||||
const jsrp = require('jsrp');
|
const jsrp = require('jsrp');
|
||||||
import { User, LoginSRPDetail } from '../../models';
|
import { User, LoginSRPDetail } from '../../models';
|
||||||
@@ -35,47 +34,40 @@ declare module 'jsonwebtoken' {
|
|||||||
* @returns
|
* @returns
|
||||||
*/
|
*/
|
||||||
export const login1 = async (req: Request, res: Response) => {
|
export const login1 = async (req: Request, res: Response) => {
|
||||||
try {
|
const {
|
||||||
const {
|
email,
|
||||||
email,
|
clientPublicKey
|
||||||
clientPublicKey
|
}: { email: string; clientPublicKey: string } = req.body;
|
||||||
}: { email: string; clientPublicKey: string } = req.body;
|
|
||||||
|
|
||||||
const user = await User.findOne({
|
const user = await User.findOne({
|
||||||
email
|
email
|
||||||
}).select('+salt +verifier');
|
}).select('+salt +verifier');
|
||||||
|
|
||||||
if (!user) throw new Error('Failed to find user');
|
if (!user) throw new Error('Failed to find user');
|
||||||
|
|
||||||
const server = new jsrp.server();
|
const server = new jsrp.server();
|
||||||
server.init(
|
server.init(
|
||||||
{
|
{
|
||||||
salt: user.salt,
|
salt: user.salt,
|
||||||
verifier: user.verifier
|
verifier: user.verifier
|
||||||
},
|
},
|
||||||
async () => {
|
async () => {
|
||||||
// generate server-side public key
|
// generate server-side public key
|
||||||
const serverPublicKey = server.getPublicKey();
|
const serverPublicKey = server.getPublicKey();
|
||||||
|
|
||||||
await LoginSRPDetail.findOneAndReplace({ email: email }, {
|
await LoginSRPDetail.findOneAndReplace({ email: email }, {
|
||||||
email: email,
|
email: email,
|
||||||
clientPublicKey: clientPublicKey,
|
clientPublicKey: clientPublicKey,
|
||||||
serverBInt: bigintConversion.bigintToBuf(server.bInt),
|
serverBInt: bigintConversion.bigintToBuf(server.bInt),
|
||||||
}, { upsert: true, returnNewDocument: false });
|
}, { upsert: true, returnNewDocument: false });
|
||||||
|
|
||||||
return res.status(200).send({
|
return res.status(200).send({
|
||||||
serverPublicKey,
|
serverPublicKey,
|
||||||
salt: user.salt
|
salt: user.salt
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
} catch (err) {
|
|
||||||
Sentry.setUser(null);
|
|
||||||
Sentry.captureException(err);
|
|
||||||
return res.status(400).send({
|
|
||||||
message: 'Failed to start authentication process'
|
|
||||||
});
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -86,149 +78,143 @@ export const login1 = async (req: Request, res: Response) => {
|
|||||||
* @returns
|
* @returns
|
||||||
*/
|
*/
|
||||||
export const login2 = async (req: Request, res: Response) => {
|
export const login2 = async (req: Request, res: Response) => {
|
||||||
try {
|
if (!req.headers['user-agent']) throw InternalServerError({ message: 'User-Agent header is required' });
|
||||||
|
|
||||||
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 +devices');
|
||||||
|
|
||||||
const { email, clientProof } = req.body;
|
if (!user) throw new Error('Failed to find user');
|
||||||
const user = await User.findOne({
|
|
||||||
email
|
|
||||||
}).select('+salt +verifier +encryptionVersion +protectedKey +protectedKeyIV +protectedKeyTag +publicKey +encryptedPrivateKey +iv +tag');
|
|
||||||
|
|
||||||
if (!user) throw new Error('Failed to find user');
|
const loginSRPDetail = await LoginSRPDetail.findOneAndDelete({ email: email })
|
||||||
|
|
||||||
const loginSRPDetail = await LoginSRPDetail.findOneAndDelete({ email: email })
|
if (!loginSRPDetail) {
|
||||||
|
return BadRequestError(Error("Failed to find login details for SRP"))
|
||||||
|
}
|
||||||
|
|
||||||
if (!loginSRPDetail) {
|
const server = new jsrp.server();
|
||||||
return BadRequestError(Error("Failed to find login details for SRP"))
|
server.init(
|
||||||
}
|
{
|
||||||
|
salt: user.salt,
|
||||||
|
verifier: user.verifier,
|
||||||
|
b: loginSRPDetail.serverBInt
|
||||||
|
},
|
||||||
|
async () => {
|
||||||
|
server.setClientPublicKey(loginSRPDetail.clientPublicKey);
|
||||||
|
|
||||||
const server = new jsrp.server();
|
// compare server and client shared keys
|
||||||
server.init(
|
if (server.checkClientProof(clientProof)) {
|
||||||
{
|
if (user.isMfaEnabled) {
|
||||||
salt: user.salt,
|
// case: user has MFA enabled
|
||||||
verifier: user.verifier,
|
|
||||||
b: loginSRPDetail.serverBInt
|
|
||||||
},
|
|
||||||
async () => {
|
|
||||||
server.setClientPublicKey(loginSRPDetail.clientPublicKey);
|
|
||||||
|
|
||||||
// compare server and client shared keys
|
// generate temporary MFA token
|
||||||
if (server.checkClientProof(clientProof)) {
|
const token = createToken({
|
||||||
|
payload: {
|
||||||
if (user.isMfaEnabled) {
|
userId: user._id.toString()
|
||||||
// case: user has MFA enabled
|
},
|
||||||
|
expiresIn: await getJwtMfaLifetime(),
|
||||||
// generate temporary MFA token
|
secret: await getJwtMfaSecret()
|
||||||
const token = createToken({
|
|
||||||
payload: {
|
|
||||||
userId: user._id.toString()
|
|
||||||
},
|
|
||||||
expiresIn: await getJwtMfaLifetime(),
|
|
||||||
secret: await getJwtMfaSecret()
|
|
||||||
});
|
|
||||||
|
|
||||||
const code = await TokenService.createToken({
|
|
||||||
type: TOKEN_EMAIL_MFA,
|
|
||||||
email
|
|
||||||
});
|
|
||||||
|
|
||||||
// send MFA code [code] to [email]
|
|
||||||
await sendMail({
|
|
||||||
template: 'emailMfa.handlebars',
|
|
||||||
subjectLine: 'Infisical MFA code',
|
|
||||||
recipients: [email],
|
|
||||||
substitutions: {
|
|
||||||
code
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
return res.status(200).send({
|
|
||||||
mfaEnabled: true,
|
|
||||||
token
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
await checkUserDevice({
|
|
||||||
user,
|
|
||||||
ip: req.ip,
|
|
||||||
userAgent: req.headers['user-agent'] ?? ''
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// issue tokens
|
const code = await TokenService.createToken({
|
||||||
const tokens = await issueAuthTokens({ userId: user._id.toString() });
|
type: TOKEN_EMAIL_MFA,
|
||||||
|
email
|
||||||
// store (refresh) token in httpOnly cookie
|
|
||||||
res.cookie('jid', tokens.refreshToken, {
|
|
||||||
httpOnly: true,
|
|
||||||
path: '/',
|
|
||||||
sameSite: 'strict',
|
|
||||||
secure: await getHttpsEnabled()
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// case: user does not have MFA enablgged
|
// send MFA code [code] to [email]
|
||||||
// return (access) token in response
|
await sendMail({
|
||||||
|
template: 'emailMfa.handlebars',
|
||||||
interface ResponseData {
|
subjectLine: 'Infisical MFA code',
|
||||||
mfaEnabled: boolean;
|
recipients: [email],
|
||||||
encryptionVersion: any;
|
substitutions: {
|
||||||
protectedKey?: string;
|
code
|
||||||
protectedKeyIV?: string;
|
}
|
||||||
protectedKeyTag?: string;
|
|
||||||
token: string;
|
|
||||||
publicKey?: string;
|
|
||||||
encryptedPrivateKey?: string;
|
|
||||||
iv?: string;
|
|
||||||
tag?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
const response: ResponseData = {
|
|
||||||
mfaEnabled: false,
|
|
||||||
encryptionVersion: user.encryptionVersion,
|
|
||||||
token: tokens.token,
|
|
||||||
publicKey: user.publicKey,
|
|
||||||
encryptedPrivateKey: user.encryptedPrivateKey,
|
|
||||||
iv: user.iv,
|
|
||||||
tag: user.tag
|
|
||||||
}
|
|
||||||
|
|
||||||
if (
|
|
||||||
user?.protectedKey &&
|
|
||||||
user?.protectedKeyIV &&
|
|
||||||
user?.protectedKeyTag
|
|
||||||
) {
|
|
||||||
response.protectedKey = user.protectedKey;
|
|
||||||
response.protectedKeyIV = user.protectedKeyIV
|
|
||||||
response.protectedKeyTag = user.protectedKeyTag;
|
|
||||||
}
|
|
||||||
|
|
||||||
const loginAction = await EELogService.createAction({
|
|
||||||
name: ACTION_LOGIN,
|
|
||||||
userId: user._id
|
|
||||||
});
|
});
|
||||||
|
|
||||||
loginAction && await EELogService.createLog({
|
return res.status(200).send({
|
||||||
userId: user._id,
|
mfaEnabled: true,
|
||||||
actions: [loginAction],
|
token
|
||||||
channel: getChannelFromUserAgent(req.headers['user-agent']),
|
|
||||||
ipAddress: req.ip
|
|
||||||
});
|
});
|
||||||
|
|
||||||
return res.status(200).send(response);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return res.status(400).send({
|
await checkUserDevice({
|
||||||
message: 'Failed to authenticate. Try again?'
|
user,
|
||||||
|
ip: req.realIP,
|
||||||
|
userAgent: req.headers['user-agent'] ?? ''
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// issue tokens
|
||||||
|
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, {
|
||||||
|
httpOnly: true,
|
||||||
|
path: '/',
|
||||||
|
sameSite: 'strict',
|
||||||
|
secure: await getHttpsEnabled()
|
||||||
|
});
|
||||||
|
|
||||||
|
// case: user does not have MFA enabled
|
||||||
|
// return (access) token in response
|
||||||
|
|
||||||
|
interface ResponseData {
|
||||||
|
mfaEnabled: boolean;
|
||||||
|
encryptionVersion: any;
|
||||||
|
protectedKey?: string;
|
||||||
|
protectedKeyIV?: string;
|
||||||
|
protectedKeyTag?: string;
|
||||||
|
token: string;
|
||||||
|
publicKey?: string;
|
||||||
|
encryptedPrivateKey?: string;
|
||||||
|
iv?: string;
|
||||||
|
tag?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const response: ResponseData = {
|
||||||
|
mfaEnabled: false,
|
||||||
|
encryptionVersion: user.encryptionVersion,
|
||||||
|
token: tokens.token,
|
||||||
|
publicKey: user.publicKey,
|
||||||
|
encryptedPrivateKey: user.encryptedPrivateKey,
|
||||||
|
iv: user.iv,
|
||||||
|
tag: user.tag
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
user?.protectedKey &&
|
||||||
|
user?.protectedKeyIV &&
|
||||||
|
user?.protectedKeyTag
|
||||||
|
) {
|
||||||
|
response.protectedKey = user.protectedKey;
|
||||||
|
response.protectedKeyIV = user.protectedKeyIV
|
||||||
|
response.protectedKeyTag = user.protectedKeyTag;
|
||||||
|
}
|
||||||
|
|
||||||
|
const loginAction = await EELogService.createAction({
|
||||||
|
name: ACTION_LOGIN,
|
||||||
|
userId: user._id
|
||||||
|
});
|
||||||
|
|
||||||
|
loginAction && await EELogService.createLog({
|
||||||
|
userId: user._id,
|
||||||
|
actions: [loginAction],
|
||||||
|
channel: getChannelFromUserAgent(req.headers['user-agent']),
|
||||||
|
ipAddress: req.ip
|
||||||
|
});
|
||||||
|
|
||||||
|
return res.status(200).send(response);
|
||||||
}
|
}
|
||||||
);
|
|
||||||
} catch (err) {
|
return res.status(400).send({
|
||||||
Sentry.setUser(null);
|
message: 'Failed to authenticate. Try again?'
|
||||||
Sentry.captureException(err);
|
});
|
||||||
return res.status(400).send({
|
}
|
||||||
message: 'Failed to authenticate. Try again?'
|
);
|
||||||
});
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -237,30 +223,22 @@ export const login2 = async (req: Request, res: Response) => {
|
|||||||
* @param res
|
* @param res
|
||||||
*/
|
*/
|
||||||
export const sendMfaToken = async (req: Request, res: Response) => {
|
export const sendMfaToken = async (req: Request, res: Response) => {
|
||||||
try {
|
const { email } = req.body;
|
||||||
const { email } = req.body;
|
|
||||||
|
|
||||||
const code = await TokenService.createToken({
|
const code = await TokenService.createToken({
|
||||||
type: TOKEN_EMAIL_MFA,
|
type: TOKEN_EMAIL_MFA,
|
||||||
email
|
email
|
||||||
});
|
});
|
||||||
|
|
||||||
// send MFA code [code] to [email]
|
// send MFA code [code] to [email]
|
||||||
await sendMail({
|
await sendMail({
|
||||||
template: 'emailMfa.handlebars',
|
template: 'emailMfa.handlebars',
|
||||||
subjectLine: 'Infisical MFA code',
|
subjectLine: 'Infisical MFA code',
|
||||||
recipients: [email],
|
recipients: [email],
|
||||||
substitutions: {
|
substitutions: {
|
||||||
code
|
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({
|
return res.status(200).send({
|
||||||
message: 'Successfully sent new MFA code'
|
message: 'Successfully sent new MFA code'
|
||||||
@@ -284,7 +262,7 @@ export const verifyMfaToken = async (req: Request, res: Response) => {
|
|||||||
|
|
||||||
const user = await User.findOne({
|
const user = await User.findOne({
|
||||||
email
|
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');
|
if (!user) throw new Error('Failed to find user');
|
||||||
|
|
||||||
@@ -292,12 +270,16 @@ export const verifyMfaToken = async (req: Request, res: Response) => {
|
|||||||
|
|
||||||
await checkUserDevice({
|
await checkUserDevice({
|
||||||
user,
|
user,
|
||||||
ip: req.ip,
|
ip: req.realIP,
|
||||||
userAgent: req.headers['user-agent'] ?? ''
|
userAgent: req.headers['user-agent'] ?? ''
|
||||||
});
|
});
|
||||||
|
|
||||||
// issue tokens
|
// 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
|
// store (refresh) token in httpOnly cookie
|
||||||
res.cookie('jid', tokens.refreshToken, {
|
res.cookie('jid', tokens.refreshToken, {
|
||||||
@@ -355,7 +337,7 @@ export const verifyMfaToken = async (req: Request, res: Response) => {
|
|||||||
userId: user._id,
|
userId: user._id,
|
||||||
actions: [loginAction],
|
actions: [loginAction],
|
||||||
channel: getChannelFromUserAgent(req.headers['user-agent']),
|
channel: getChannelFromUserAgent(req.headers['user-agent']),
|
||||||
ipAddress: req.ip
|
ipAddress: req.realIP
|
||||||
});
|
});
|
||||||
|
|
||||||
return res.status(200).send(resObj);
|
return res.status(200).send(resObj);
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
import { Request, Response } from 'express';
|
import { Request, Response } from 'express';
|
||||||
import * as Sentry from '@sentry/node';
|
|
||||||
import {
|
import {
|
||||||
Secret,
|
Secret,
|
||||||
ServiceToken,
|
ServiceToken,
|
||||||
@@ -25,30 +24,22 @@ export const createWorkspaceEnvironment = async (
|
|||||||
) => {
|
) => {
|
||||||
const { workspaceId } = req.params;
|
const { workspaceId } = req.params;
|
||||||
const { environmentName, environmentSlug } = req.body;
|
const { environmentName, environmentSlug } = req.body;
|
||||||
try {
|
const workspace = await Workspace.findById(workspaceId).exec();
|
||||||
const workspace = await Workspace.findById(workspaceId).exec();
|
if (
|
||||||
if (
|
!workspace ||
|
||||||
!workspace ||
|
workspace?.environments.find(
|
||||||
workspace?.environments.find(
|
({ name, slug }) => slug === environmentSlug || environmentName === name
|
||||||
({ name, slug }) => slug === environmentSlug || environmentName === name
|
)
|
||||||
)
|
) {
|
||||||
) {
|
throw new Error('Failed to create workspace environment');
|
||||||
throw new Error('Failed to create workspace environment');
|
|
||||||
}
|
|
||||||
|
|
||||||
workspace?.environments.push({
|
|
||||||
name: environmentName,
|
|
||||||
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',
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
workspace?.environments.push({
|
||||||
|
name: environmentName,
|
||||||
|
slug: environmentSlug.toLowerCase(),
|
||||||
|
});
|
||||||
|
await workspace.save();
|
||||||
|
|
||||||
return res.status(200).send({
|
return res.status(200).send({
|
||||||
message: 'Successfully created new environment',
|
message: 'Successfully created new environment',
|
||||||
workspace: workspaceId,
|
workspace: workspaceId,
|
||||||
@@ -72,75 +63,67 @@ export const renameWorkspaceEnvironment = async (
|
|||||||
) => {
|
) => {
|
||||||
const { workspaceId } = req.params;
|
const { workspaceId } = req.params;
|
||||||
const { environmentName, environmentSlug, oldEnvironmentSlug } = req.body;
|
const { environmentName, environmentSlug, oldEnvironmentSlug } = req.body;
|
||||||
try {
|
// user should pass both new slug and env name
|
||||||
// user should pass both new slug and env name
|
if (!environmentSlug || !environmentName) {
|
||||||
if (!environmentSlug || !environmentName) {
|
throw new Error('Invalid environment given.');
|
||||||
throw new Error('Invalid environment given.');
|
|
||||||
}
|
|
||||||
|
|
||||||
// atomic update the env to avoid conflict
|
|
||||||
const workspace = await Workspace.findById(workspaceId).exec();
|
|
||||||
if (!workspace) {
|
|
||||||
throw new Error('Failed to create workspace environment');
|
|
||||||
}
|
|
||||||
|
|
||||||
const isEnvExist = workspace.environments.some(
|
|
||||||
({ name, slug }) =>
|
|
||||||
slug !== oldEnvironmentSlug &&
|
|
||||||
(name === environmentName || slug === environmentSlug)
|
|
||||||
);
|
|
||||||
if (isEnvExist) {
|
|
||||||
throw new Error('Invalid environment given');
|
|
||||||
}
|
|
||||||
|
|
||||||
const envIndex = workspace?.environments.findIndex(
|
|
||||||
({ slug }) => slug === oldEnvironmentSlug
|
|
||||||
);
|
|
||||||
if (envIndex === -1) {
|
|
||||||
throw new Error('Invalid environment given');
|
|
||||||
}
|
|
||||||
|
|
||||||
workspace.environments[envIndex].name = environmentName;
|
|
||||||
workspace.environments[envIndex].slug = environmentSlug.toLowerCase();
|
|
||||||
|
|
||||||
await workspace.save();
|
|
||||||
await Secret.updateMany(
|
|
||||||
{ workspace: workspaceId, environment: oldEnvironmentSlug },
|
|
||||||
{ environment: environmentSlug }
|
|
||||||
);
|
|
||||||
await SecretVersion.updateMany(
|
|
||||||
{ workspace: workspaceId, environment: oldEnvironmentSlug },
|
|
||||||
{ environment: environmentSlug }
|
|
||||||
);
|
|
||||||
await ServiceToken.updateMany(
|
|
||||||
{ workspace: workspaceId, environment: oldEnvironmentSlug },
|
|
||||||
{ environment: environmentSlug }
|
|
||||||
);
|
|
||||||
await ServiceTokenData.updateMany(
|
|
||||||
{ workspace: workspaceId, environment: oldEnvironmentSlug },
|
|
||||||
{ environment: environmentSlug }
|
|
||||||
);
|
|
||||||
await Integration.updateMany(
|
|
||||||
{ workspace: workspaceId, environment: oldEnvironmentSlug },
|
|
||||||
{ environment: environmentSlug }
|
|
||||||
);
|
|
||||||
await Membership.updateMany(
|
|
||||||
{
|
|
||||||
workspace: workspaceId,
|
|
||||||
"deniedPermissions.environmentSlug": oldEnvironmentSlug
|
|
||||||
},
|
|
||||||
{ $set: { "deniedPermissions.$[element].environmentSlug": environmentSlug } },
|
|
||||||
{ 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',
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// atomic update the env to avoid conflict
|
||||||
|
const workspace = await Workspace.findById(workspaceId).exec();
|
||||||
|
if (!workspace) {
|
||||||
|
throw new Error('Failed to create workspace environment');
|
||||||
|
}
|
||||||
|
|
||||||
|
const isEnvExist = workspace.environments.some(
|
||||||
|
({ name, slug }) =>
|
||||||
|
slug !== oldEnvironmentSlug &&
|
||||||
|
(name === environmentName || slug === environmentSlug)
|
||||||
|
);
|
||||||
|
if (isEnvExist) {
|
||||||
|
throw new Error('Invalid environment given');
|
||||||
|
}
|
||||||
|
|
||||||
|
const envIndex = workspace?.environments.findIndex(
|
||||||
|
({ slug }) => slug === oldEnvironmentSlug
|
||||||
|
);
|
||||||
|
if (envIndex === -1) {
|
||||||
|
throw new Error('Invalid environment given');
|
||||||
|
}
|
||||||
|
|
||||||
|
workspace.environments[envIndex].name = environmentName;
|
||||||
|
workspace.environments[envIndex].slug = environmentSlug.toLowerCase();
|
||||||
|
|
||||||
|
await workspace.save();
|
||||||
|
await Secret.updateMany(
|
||||||
|
{ workspace: workspaceId, environment: oldEnvironmentSlug },
|
||||||
|
{ environment: environmentSlug }
|
||||||
|
);
|
||||||
|
await SecretVersion.updateMany(
|
||||||
|
{ workspace: workspaceId, environment: oldEnvironmentSlug },
|
||||||
|
{ environment: environmentSlug }
|
||||||
|
);
|
||||||
|
await ServiceToken.updateMany(
|
||||||
|
{ workspace: workspaceId, environment: oldEnvironmentSlug },
|
||||||
|
{ environment: environmentSlug }
|
||||||
|
);
|
||||||
|
await ServiceTokenData.updateMany(
|
||||||
|
{ workspace: workspaceId, environment: oldEnvironmentSlug },
|
||||||
|
{ environment: environmentSlug }
|
||||||
|
);
|
||||||
|
await Integration.updateMany(
|
||||||
|
{ workspace: workspaceId, environment: oldEnvironmentSlug },
|
||||||
|
{ environment: environmentSlug }
|
||||||
|
);
|
||||||
|
await Membership.updateMany(
|
||||||
|
{
|
||||||
|
workspace: workspaceId,
|
||||||
|
"deniedPermissions.environmentSlug": oldEnvironmentSlug
|
||||||
|
},
|
||||||
|
{ $set: { "deniedPermissions.$[element].environmentSlug": environmentSlug } },
|
||||||
|
{ arrayFilters: [{ "element.environmentSlug": oldEnvironmentSlug }] }
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
return res.status(200).send({
|
return res.status(200).send({
|
||||||
message: 'Successfully update environment',
|
message: 'Successfully update environment',
|
||||||
workspace: workspaceId,
|
workspace: workspaceId,
|
||||||
@@ -163,57 +146,48 @@ export const deleteWorkspaceEnvironment = async (
|
|||||||
) => {
|
) => {
|
||||||
const { workspaceId } = req.params;
|
const { workspaceId } = req.params;
|
||||||
const { environmentSlug } = req.body;
|
const { environmentSlug } = req.body;
|
||||||
try {
|
// atomic update the env to avoid conflict
|
||||||
// atomic update the env to avoid conflict
|
const workspace = await Workspace.findById(workspaceId).exec();
|
||||||
const workspace = await Workspace.findById(workspaceId).exec();
|
if (!workspace) {
|
||||||
if (!workspace) {
|
throw new Error('Failed to create workspace environment');
|
||||||
throw new Error('Failed to create workspace environment');
|
|
||||||
}
|
|
||||||
|
|
||||||
const envIndex = workspace?.environments.findIndex(
|
|
||||||
({ slug }) => slug === environmentSlug
|
|
||||||
);
|
|
||||||
if (envIndex === -1) {
|
|
||||||
throw new Error('Invalid environment given');
|
|
||||||
}
|
|
||||||
|
|
||||||
workspace.environments.splice(envIndex, 1);
|
|
||||||
await workspace.save();
|
|
||||||
|
|
||||||
// clean up
|
|
||||||
await Secret.deleteMany({
|
|
||||||
workspace: workspaceId,
|
|
||||||
environment: environmentSlug,
|
|
||||||
});
|
|
||||||
await SecretVersion.deleteMany({
|
|
||||||
workspace: workspaceId,
|
|
||||||
environment: environmentSlug,
|
|
||||||
});
|
|
||||||
await ServiceToken.deleteMany({
|
|
||||||
workspace: workspaceId,
|
|
||||||
environment: environmentSlug,
|
|
||||||
});
|
|
||||||
await ServiceTokenData.deleteMany({
|
|
||||||
workspace: workspaceId,
|
|
||||||
environment: environmentSlug,
|
|
||||||
});
|
|
||||||
await Integration.deleteMany({
|
|
||||||
workspace: workspaceId,
|
|
||||||
environment: environmentSlug,
|
|
||||||
});
|
|
||||||
await Membership.updateMany(
|
|
||||||
{ workspace: workspaceId },
|
|
||||||
{ $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',
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const envIndex = workspace?.environments.findIndex(
|
||||||
|
({ slug }) => slug === environmentSlug
|
||||||
|
);
|
||||||
|
if (envIndex === -1) {
|
||||||
|
throw new Error('Invalid environment given');
|
||||||
|
}
|
||||||
|
|
||||||
|
workspace.environments.splice(envIndex, 1);
|
||||||
|
await workspace.save();
|
||||||
|
|
||||||
|
// clean up
|
||||||
|
await Secret.deleteMany({
|
||||||
|
workspace: workspaceId,
|
||||||
|
environment: environmentSlug,
|
||||||
|
});
|
||||||
|
await SecretVersion.deleteMany({
|
||||||
|
workspace: workspaceId,
|
||||||
|
environment: environmentSlug,
|
||||||
|
});
|
||||||
|
await ServiceToken.deleteMany({
|
||||||
|
workspace: workspaceId,
|
||||||
|
environment: environmentSlug,
|
||||||
|
});
|
||||||
|
await ServiceTokenData.deleteMany({
|
||||||
|
workspace: workspaceId,
|
||||||
|
environment: environmentSlug,
|
||||||
|
});
|
||||||
|
await Integration.deleteMany({
|
||||||
|
workspace: workspaceId,
|
||||||
|
environment: environmentSlug,
|
||||||
|
});
|
||||||
|
await Membership.updateMany(
|
||||||
|
{ workspace: workspaceId },
|
||||||
|
{ $pull: { deniedPermissions: { environmentSlug: environmentSlug } } }
|
||||||
|
)
|
||||||
|
|
||||||
return res.status(200).send({
|
return res.status(200).send({
|
||||||
message: 'Successfully deleted environment',
|
message: 'Successfully deleted environment',
|
||||||
workspace: workspaceId,
|
workspace: workspaceId,
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
import { Request, Response } from 'express';
|
import { Request, Response } from 'express';
|
||||||
import * as Sentry from '@sentry/node';
|
|
||||||
import { Types } from 'mongoose';
|
import { Types } from 'mongoose';
|
||||||
import {
|
import {
|
||||||
MembershipOrg,
|
MembershipOrg,
|
||||||
@@ -49,20 +48,11 @@ export const getOrganizationMemberships = async (req: Request, res: Response) =>
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
*/
|
*/
|
||||||
let memberships;
|
const { organizationId } = req.params;
|
||||||
try {
|
|
||||||
const { organizationId } = req.params;
|
|
||||||
|
|
||||||
memberships = await MembershipOrg.find({
|
const memberships = await MembershipOrg.find({
|
||||||
organization: organizationId
|
organization: organizationId
|
||||||
}).populate('user', '+publicKey');
|
}).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({
|
return res.status(200).send({
|
||||||
memberships
|
memberships
|
||||||
@@ -128,26 +118,17 @@ export const updateOrganizationMembership = async (req: Request, res: Response)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
*/
|
*/
|
||||||
let membership;
|
const { membershipId } = req.params;
|
||||||
try {
|
const { role } = req.body;
|
||||||
const { membershipId } = req.params;
|
|
||||||
const { role } = req.body;
|
const membership = await MembershipOrg.findByIdAndUpdate(
|
||||||
|
membershipId,
|
||||||
membership = await MembershipOrg.findByIdAndUpdate(
|
{
|
||||||
membershipId,
|
role
|
||||||
{
|
}, {
|
||||||
role
|
new: true
|
||||||
}, {
|
}
|
||||||
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({
|
return res.status(200).send({
|
||||||
membership
|
membership
|
||||||
@@ -197,25 +178,16 @@ export const deleteOrganizationMembership = async (req: Request, res: Response)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
*/
|
*/
|
||||||
let membership;
|
const { membershipId } = req.params;
|
||||||
try {
|
|
||||||
const { membershipId } = req.params;
|
// delete organization membership
|
||||||
|
const membership = await deleteMembershipOrg({
|
||||||
// delete organization membership
|
membershipOrgId: membershipId
|
||||||
membership = await deleteMembershipOrg({
|
});
|
||||||
membershipOrgId: membershipId
|
|
||||||
});
|
|
||||||
|
|
||||||
await updateSubscriptionOrgQuantity({
|
await updateSubscriptionOrgQuantity({
|
||||||
organizationId: membership.organization.toString()
|
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({
|
return res.status(200).send({
|
||||||
membership
|
membership
|
||||||
@@ -303,4 +275,4 @@ export const getOrganizationServiceAccounts = async (req: Request, res: Response
|
|||||||
return res.status(200).send({
|
return res.status(200).send({
|
||||||
serviceAccounts
|
serviceAccounts
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -297,7 +297,7 @@ export const batchSecrets = async (req: Request, res: Response) => {
|
|||||||
workspaceId: new Types.ObjectId(workspaceId),
|
workspaceId: new Types.ObjectId(workspaceId),
|
||||||
actions,
|
actions,
|
||||||
channel,
|
channel,
|
||||||
ipAddress: req.ip,
|
ipAddress: req.realIP,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -563,7 +563,7 @@ export const createSecrets = async (req: Request, res: Response) => {
|
|||||||
workspaceId: new Types.ObjectId(workspaceId),
|
workspaceId: new Types.ObjectId(workspaceId),
|
||||||
actions: [addAction],
|
actions: [addAction],
|
||||||
channel,
|
channel,
|
||||||
ipAddress: req.ip,
|
ipAddress: req.realIP,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
// (EE) take a secret snapshot
|
// (EE) take a secret snapshot
|
||||||
@@ -785,7 +785,7 @@ export const getSecrets = async (req: Request, res: Response) => {
|
|||||||
workspaceId: new Types.ObjectId(workspaceId as string),
|
workspaceId: new Types.ObjectId(workspaceId as string),
|
||||||
actions: [readAction],
|
actions: [readAction],
|
||||||
channel,
|
channel,
|
||||||
ipAddress: req.ip,
|
ipAddress: req.realIP,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
const postHogClient = await TelemetryService.getPostHogClient();
|
const postHogClient = await TelemetryService.getPostHogClient();
|
||||||
@@ -1020,7 +1020,7 @@ export const updateSecrets = async (req: Request, res: Response) => {
|
|||||||
workspaceId: new Types.ObjectId(key),
|
workspaceId: new Types.ObjectId(key),
|
||||||
actions: [updateAction],
|
actions: [updateAction],
|
||||||
channel,
|
channel,
|
||||||
ipAddress: req.ip,
|
ipAddress: req.realIP,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
// (EE) take a secret snapshot
|
// (EE) take a secret snapshot
|
||||||
@@ -1158,7 +1158,7 @@ export const deleteSecrets = async (req: Request, res: Response) => {
|
|||||||
workspaceId: new Types.ObjectId(key),
|
workspaceId: new Types.ObjectId(key),
|
||||||
actions: [deleteAction],
|
actions: [deleteAction],
|
||||||
channel,
|
channel,
|
||||||
ipAddress: req.ip,
|
ipAddress: req.realIP,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
// (EE) take a secret snapshot
|
// (EE) take a secret snapshot
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
import * as Sentry from '@sentry/node';
|
|
||||||
import { Request, Response } from 'express';
|
import { Request, Response } from 'express';
|
||||||
import crypto from 'crypto';
|
import crypto from 'crypto';
|
||||||
import bcrypt from 'bcrypt';
|
import bcrypt from 'bcrypt';
|
||||||
@@ -144,4 +143,4 @@ export const deleteServiceTokenData = async (req: Request, res: Response) => {
|
|||||||
return res.status(200).send({
|
return res.status(200).send({
|
||||||
serviceTokenData
|
serviceTokenData
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
import { Request, Response } from 'express';
|
import { Request, Response } from 'express';
|
||||||
import * as Sentry from '@sentry/node';
|
|
||||||
import { User, MembershipOrg } from '../../models';
|
import { User, MembershipOrg } from '../../models';
|
||||||
import { completeAccount } from '../../helpers/user';
|
import { completeAccount } from '../../helpers/user';
|
||||||
import {
|
import {
|
||||||
@@ -20,136 +19,130 @@ import { updateSubscriptionOrgQuantity } from '../../helpers/organization';
|
|||||||
*/
|
*/
|
||||||
export const completeAccountSignup = async (req: Request, res: Response) => {
|
export const completeAccountSignup = async (req: Request, res: Response) => {
|
||||||
let user, token, refreshToken;
|
let user, token, refreshToken;
|
||||||
try {
|
const {
|
||||||
const {
|
email,
|
||||||
email,
|
firstName,
|
||||||
firstName,
|
lastName,
|
||||||
lastName,
|
protectedKey,
|
||||||
protectedKey,
|
protectedKeyIV,
|
||||||
protectedKeyIV,
|
protectedKeyTag,
|
||||||
protectedKeyTag,
|
publicKey,
|
||||||
publicKey,
|
encryptedPrivateKey,
|
||||||
encryptedPrivateKey,
|
encryptedPrivateKeyIV,
|
||||||
encryptedPrivateKeyIV,
|
encryptedPrivateKeyTag,
|
||||||
encryptedPrivateKeyTag,
|
salt,
|
||||||
salt,
|
verifier,
|
||||||
verifier,
|
organizationName
|
||||||
organizationName
|
}: {
|
||||||
}: {
|
email: string;
|
||||||
email: string;
|
firstName: string;
|
||||||
firstName: string;
|
lastName: string;
|
||||||
lastName: string;
|
protectedKey: string;
|
||||||
protectedKey: string;
|
protectedKeyIV: string;
|
||||||
protectedKeyIV: string;
|
protectedKeyTag: string;
|
||||||
protectedKeyTag: string;
|
publicKey: string;
|
||||||
publicKey: string;
|
encryptedPrivateKey: string;
|
||||||
encryptedPrivateKey: string;
|
encryptedPrivateKeyIV: string;
|
||||||
encryptedPrivateKeyIV: string;
|
encryptedPrivateKeyTag: string;
|
||||||
encryptedPrivateKeyTag: string;
|
salt: string;
|
||||||
salt: string;
|
verifier: string;
|
||||||
verifier: string;
|
organizationName: string;
|
||||||
organizationName: string;
|
} = req.body;
|
||||||
} = req.body;
|
|
||||||
|
|
||||||
// get user
|
// get user
|
||||||
user = await User.findOne({ email });
|
user = await User.findOne({ email });
|
||||||
|
|
||||||
if (!user || (user && user?.publicKey)) {
|
if (!user || (user && user?.publicKey)) {
|
||||||
// case 1: user doesn't exist.
|
// case 1: user doesn't exist.
|
||||||
// case 2: user has already completed account
|
// case 2: user has already completed account
|
||||||
return res.status(403).send({
|
return res.status(403).send({
|
||||||
error: 'Failed to complete account for complete user'
|
error: 'Failed to complete account for complete user'
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// complete setting up user's account
|
// complete setting up user's account
|
||||||
user = await completeAccount({
|
user = await completeAccount({
|
||||||
userId: user._id.toString(),
|
userId: user._id.toString(),
|
||||||
firstName,
|
firstName,
|
||||||
lastName,
|
lastName,
|
||||||
encryptionVersion: 2,
|
encryptionVersion: 2,
|
||||||
protectedKey,
|
protectedKey,
|
||||||
protectedKeyIV,
|
protectedKeyIV,
|
||||||
protectedKeyTag,
|
protectedKeyTag,
|
||||||
publicKey,
|
publicKey,
|
||||||
encryptedPrivateKey,
|
encryptedPrivateKey,
|
||||||
encryptedPrivateKeyIV,
|
encryptedPrivateKeyIV,
|
||||||
encryptedPrivateKeyTag,
|
encryptedPrivateKeyTag,
|
||||||
salt,
|
salt,
|
||||||
verifier
|
verifier
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!user)
|
if (!user)
|
||||||
throw new Error('Failed to complete account for non-existent user'); // ensure user is non-null
|
throw new Error('Failed to complete account for non-existent user'); // ensure user is non-null
|
||||||
|
|
||||||
// initialize default organization and workspace
|
// initialize default organization and workspace
|
||||||
await initializeDefaultOrg({
|
await initializeDefaultOrg({
|
||||||
organizationName,
|
organizationName,
|
||||||
user
|
user
|
||||||
});
|
});
|
||||||
|
|
||||||
// update organization membership statuses that are
|
// update organization membership statuses that are
|
||||||
// invited to completed with user attached
|
// invited to completed with user attached
|
||||||
const membershipsToUpdate = await MembershipOrg.find({
|
const membershipsToUpdate = await MembershipOrg.find({
|
||||||
inviteEmail: email,
|
inviteEmail: email,
|
||||||
status: INVITED
|
status: INVITED
|
||||||
});
|
});
|
||||||
|
|
||||||
membershipsToUpdate.forEach(async (membership) => {
|
membershipsToUpdate.forEach(async (membership) => {
|
||||||
await updateSubscriptionOrgQuantity({
|
await updateSubscriptionOrgQuantity({
|
||||||
organizationId: membership.organization.toString()
|
organizationId: membership.organization.toString()
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
// update organization membership statuses that are
|
// update organization membership statuses that are
|
||||||
// invited to completed with user attached
|
// invited to completed with user attached
|
||||||
await MembershipOrg.updateMany(
|
await MembershipOrg.updateMany(
|
||||||
{
|
{
|
||||||
inviteEmail: email,
|
inviteEmail: email,
|
||||||
status: INVITED
|
status: INVITED
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
user,
|
user,
|
||||||
status: ACCEPTED
|
status: ACCEPTED
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
// issue tokens
|
// issue tokens
|
||||||
const tokens = await issueAuthTokens({
|
const tokens = await issueAuthTokens({
|
||||||
userId: user._id.toString()
|
userId: user._id,
|
||||||
});
|
ip: req.realIP,
|
||||||
|
userAgent: req.headers['user-agent'] ?? ''
|
||||||
|
});
|
||||||
|
|
||||||
token = tokens.token;
|
token = tokens.token;
|
||||||
|
|
||||||
// sending a welcome email to new users
|
// sending a welcome email to new users
|
||||||
if (await getLoopsApiKey()) {
|
if (await getLoopsApiKey()) {
|
||||||
await standardRequest.post("https://app.loops.so/api/v1/events/send", {
|
await standardRequest.post("https://app.loops.so/api/v1/events/send", {
|
||||||
"email": email,
|
"email": email,
|
||||||
"eventName": "Sign Up",
|
"eventName": "Sign Up",
|
||||||
"firstName": firstName,
|
"firstName": firstName,
|
||||||
"lastName": lastName
|
"lastName": lastName
|
||||||
}, {
|
}, {
|
||||||
headers: {
|
headers: {
|
||||||
"Accept": "application/json",
|
"Accept": "application/json",
|
||||||
"Authorization": "Bearer " + (await getLoopsApiKey())
|
"Authorization": "Bearer " + (await getLoopsApiKey())
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// store (refresh) token in httpOnly cookie
|
// store (refresh) token in httpOnly cookie
|
||||||
res.cookie('jid', tokens.refreshToken, {
|
res.cookie('jid', tokens.refreshToken, {
|
||||||
httpOnly: true,
|
httpOnly: true,
|
||||||
path: '/',
|
path: '/',
|
||||||
sameSite: 'strict',
|
sameSite: 'strict',
|
||||||
secure: await getHttpsEnabled()
|
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({
|
return res.status(200).send({
|
||||||
message: 'Successfully set up account',
|
message: 'Successfully set up account',
|
||||||
@@ -167,109 +160,103 @@ export const completeAccountSignup = async (req: Request, res: Response) => {
|
|||||||
*/
|
*/
|
||||||
export const completeAccountInvite = async (req: Request, res: Response) => {
|
export const completeAccountInvite = async (req: Request, res: Response) => {
|
||||||
let user, token, refreshToken;
|
let user, token, refreshToken;
|
||||||
try {
|
const {
|
||||||
const {
|
email,
|
||||||
email,
|
firstName,
|
||||||
firstName,
|
lastName,
|
||||||
lastName,
|
protectedKey,
|
||||||
protectedKey,
|
protectedKeyIV,
|
||||||
protectedKeyIV,
|
protectedKeyTag,
|
||||||
protectedKeyTag,
|
publicKey,
|
||||||
publicKey,
|
encryptedPrivateKey,
|
||||||
encryptedPrivateKey,
|
encryptedPrivateKeyIV,
|
||||||
encryptedPrivateKeyIV,
|
encryptedPrivateKeyTag,
|
||||||
encryptedPrivateKeyTag,
|
salt,
|
||||||
salt,
|
verifier
|
||||||
verifier
|
} = req.body;
|
||||||
} = req.body;
|
|
||||||
|
|
||||||
// get user
|
// get user
|
||||||
user = await User.findOne({ email });
|
user = await User.findOne({ email });
|
||||||
|
|
||||||
if (!user || (user && user?.publicKey)) {
|
if (!user || (user && user?.publicKey)) {
|
||||||
// case 1: user doesn't exist.
|
// case 1: user doesn't exist.
|
||||||
// case 2: user has already completed account
|
// case 2: user has already completed account
|
||||||
return res.status(403).send({
|
return res.status(403).send({
|
||||||
error: 'Failed to complete account for complete user'
|
error: 'Failed to complete account for complete user'
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const membershipOrg = await MembershipOrg.findOne({
|
const membershipOrg = await MembershipOrg.findOne({
|
||||||
inviteEmail: email,
|
inviteEmail: email,
|
||||||
status: INVITED
|
status: INVITED
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!membershipOrg) throw new Error('Failed to find invitations for email');
|
if (!membershipOrg) throw new Error('Failed to find invitations for email');
|
||||||
|
|
||||||
// complete setting up user's account
|
// complete setting up user's account
|
||||||
user = await completeAccount({
|
user = await completeAccount({
|
||||||
userId: user._id.toString(),
|
userId: user._id.toString(),
|
||||||
firstName,
|
firstName,
|
||||||
lastName,
|
lastName,
|
||||||
encryptionVersion: 2,
|
encryptionVersion: 2,
|
||||||
protectedKey,
|
protectedKey,
|
||||||
protectedKeyIV,
|
protectedKeyIV,
|
||||||
protectedKeyTag,
|
protectedKeyTag,
|
||||||
publicKey,
|
publicKey,
|
||||||
encryptedPrivateKey,
|
encryptedPrivateKey,
|
||||||
encryptedPrivateKeyIV,
|
encryptedPrivateKeyIV,
|
||||||
encryptedPrivateKeyTag,
|
encryptedPrivateKeyTag,
|
||||||
salt,
|
salt,
|
||||||
verifier
|
verifier
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!user)
|
if (!user)
|
||||||
throw new Error('Failed to complete account for non-existent user');
|
throw new Error('Failed to complete account for non-existent user');
|
||||||
|
|
||||||
// update organization membership statuses that are
|
// update organization membership statuses that are
|
||||||
// invited to completed with user attached
|
// invited to completed with user attached
|
||||||
const membershipsToUpdate = await MembershipOrg.find({
|
const membershipsToUpdate = await MembershipOrg.find({
|
||||||
inviteEmail: email,
|
inviteEmail: email,
|
||||||
status: INVITED
|
status: INVITED
|
||||||
});
|
});
|
||||||
|
|
||||||
membershipsToUpdate.forEach(async (membership) => {
|
membershipsToUpdate.forEach(async (membership) => {
|
||||||
await updateSubscriptionOrgQuantity({
|
await updateSubscriptionOrgQuantity({
|
||||||
organizationId: membership.organization.toString()
|
organizationId: membership.organization.toString()
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
await MembershipOrg.updateMany(
|
await MembershipOrg.updateMany(
|
||||||
{
|
{
|
||||||
inviteEmail: email,
|
inviteEmail: email,
|
||||||
status: INVITED
|
status: INVITED
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
user,
|
user,
|
||||||
status: ACCEPTED
|
status: ACCEPTED
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
// issue tokens
|
// issue tokens
|
||||||
const tokens = await issueAuthTokens({
|
const tokens = await issueAuthTokens({
|
||||||
userId: user._id.toString()
|
userId: user._id,
|
||||||
});
|
ip: req.realIP,
|
||||||
|
userAgent: req.headers['user-agent'] ?? ''
|
||||||
|
});
|
||||||
|
|
||||||
token = tokens.token;
|
token = tokens.token;
|
||||||
|
|
||||||
// store (refresh) token in httpOnly cookie
|
// store (refresh) token in httpOnly cookie
|
||||||
res.cookie('jid', tokens.refreshToken, {
|
res.cookie('jid', tokens.refreshToken, {
|
||||||
httpOnly: true,
|
httpOnly: true,
|
||||||
path: '/',
|
path: '/',
|
||||||
sameSite: 'strict',
|
sameSite: 'strict',
|
||||||
secure: await getHttpsEnabled()
|
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({
|
return res.status(200).send({
|
||||||
message: 'Successfully set up account',
|
message: 'Successfully set up account',
|
||||||
user,
|
user,
|
||||||
token
|
token
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
import { Request, Response } from 'express';
|
import { Request, Response } from 'express';
|
||||||
import * as Sentry from '@sentry/node';
|
|
||||||
import { Types } from 'mongoose';
|
import { Types } from 'mongoose';
|
||||||
import {
|
import {
|
||||||
Membership, Secret,
|
Membership, Secret,
|
||||||
@@ -69,4 +68,4 @@ export const getWorkspaceTags = async (req: Request, res: Response) => {
|
|||||||
return res.json({
|
return res.json({
|
||||||
workspaceTags
|
workspaceTags
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
import { Request, Response } from 'express';
|
import { Request, Response } from 'express';
|
||||||
import * as Sentry from '@sentry/node';
|
|
||||||
import {
|
import {
|
||||||
User,
|
User,
|
||||||
MembershipOrg
|
MembershipOrg
|
||||||
@@ -37,18 +36,9 @@ export const getMe = async (req: Request, res: Response) => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
*/
|
*/
|
||||||
let user;
|
const user = await User
|
||||||
try {
|
.findById(req.user._id)
|
||||||
user = await User
|
.select('+salt +publicKey +encryptedPrivateKey +iv +tag +encryptionVersion +protectedKey +protectedKeyIV +protectedKeyTag');
|
||||||
.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({
|
return res.status(200).send({
|
||||||
user
|
user
|
||||||
@@ -64,29 +54,20 @@ export const getMe = async (req: Request, res: Response) => {
|
|||||||
* @returns
|
* @returns
|
||||||
*/
|
*/
|
||||||
export const updateMyMfaEnabled = async (req: Request, res: Response) => {
|
export const updateMyMfaEnabled = async (req: Request, res: Response) => {
|
||||||
let user;
|
const { isMfaEnabled }: { isMfaEnabled: boolean } = req.body;
|
||||||
try {
|
req.user.isMfaEnabled = isMfaEnabled;
|
||||||
const { isMfaEnabled }: { isMfaEnabled: boolean } = req.body;
|
|
||||||
req.user.isMfaEnabled = isMfaEnabled;
|
if (isMfaEnabled) {
|
||||||
|
// TODO: adapt this route/controller
|
||||||
if (isMfaEnabled) {
|
// to work for different forms of MFA
|
||||||
// TODO: adapt this route/controller
|
req.user.mfaMethods = ['email'];
|
||||||
// to work for different forms of MFA
|
} else {
|
||||||
req.user.mfaMethods = ['email'];
|
req.user.mfaMethods = [];
|
||||||
} else {
|
|
||||||
req.user.mfaMethods = [];
|
|
||||||
}
|
|
||||||
|
|
||||||
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"
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
await req.user.save();
|
||||||
|
|
||||||
|
const user = req.user;
|
||||||
|
|
||||||
return res.status(200).send({
|
return res.status(200).send({
|
||||||
user
|
user
|
||||||
@@ -126,22 +107,13 @@ export const getMyOrganizations = async (req: Request, res: Response) => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
*/
|
*/
|
||||||
let organizations;
|
const organizations = (
|
||||||
try {
|
await MembershipOrg.find({
|
||||||
organizations = (
|
user: req.user._id
|
||||||
await MembershipOrg.find({
|
}).populate('organization')
|
||||||
user: req.user._id
|
).map((m) => m.organization);
|
||||||
}).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({
|
return res.status(200).send({
|
||||||
organizations
|
organizations
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
import { Request, Response } from 'express';
|
import { Request, Response } from 'express';
|
||||||
import * as Sentry from '@sentry/node';
|
|
||||||
import { Types } from 'mongoose';
|
import { Types } from 'mongoose';
|
||||||
import {
|
import {
|
||||||
Workspace,
|
Workspace,
|
||||||
@@ -47,66 +46,57 @@ interface V2PushSecret {
|
|||||||
*/
|
*/
|
||||||
export const pushWorkspaceSecrets = async (req: Request, res: Response) => {
|
export const pushWorkspaceSecrets = async (req: Request, res: Response) => {
|
||||||
// upload (encrypted) secrets to workspace with id [workspaceId]
|
// upload (encrypted) secrets to workspace with id [workspaceId]
|
||||||
try {
|
const postHogClient = await TelemetryService.getPostHogClient();
|
||||||
const postHogClient = await TelemetryService.getPostHogClient();
|
let { secrets }: { secrets: V2PushSecret[] } = req.body;
|
||||||
let { secrets }: { secrets: V2PushSecret[] } = req.body;
|
const { keys, environment, channel } = req.body;
|
||||||
const { keys, environment, channel } = req.body;
|
const { workspaceId } = req.params;
|
||||||
const { workspaceId } = req.params;
|
|
||||||
|
|
||||||
// validate environment
|
// validate environment
|
||||||
const workspaceEnvs = req.membership.workspace.environments;
|
const workspaceEnvs = req.membership.workspace.environments;
|
||||||
if (!workspaceEnvs.find(({ slug }: { slug: string }) => slug === environment)) {
|
if (!workspaceEnvs.find(({ slug }: { slug: string }) => slug === environment)) {
|
||||||
throw new Error('Failed to validate environment');
|
throw new Error('Failed to validate environment');
|
||||||
}
|
}
|
||||||
|
|
||||||
// sanitize secrets
|
// sanitize secrets
|
||||||
secrets = secrets.filter(
|
secrets = secrets.filter(
|
||||||
(s: V2PushSecret) => s.secretKeyCiphertext !== '' && s.secretValueCiphertext !== ''
|
(s: V2PushSecret) => s.secretKeyCiphertext !== '' && s.secretValueCiphertext !== ''
|
||||||
);
|
);
|
||||||
|
|
||||||
await push({
|
await push({
|
||||||
userId: req.user._id,
|
userId: req.user._id,
|
||||||
workspaceId,
|
workspaceId,
|
||||||
environment,
|
environment,
|
||||||
secrets,
|
secrets,
|
||||||
channel: channel ? channel : 'cli',
|
channel: channel ? channel : 'cli',
|
||||||
ipAddress: req.ip
|
ipAddress: req.realIP
|
||||||
});
|
});
|
||||||
|
|
||||||
await pushKeys({
|
await pushKeys({
|
||||||
userId: req.user._id,
|
userId: req.user._id,
|
||||||
workspaceId,
|
workspaceId,
|
||||||
keys
|
keys
|
||||||
});
|
});
|
||||||
|
|
||||||
if (postHogClient) {
|
if (postHogClient) {
|
||||||
postHogClient.capture({
|
postHogClient.capture({
|
||||||
event: 'secrets pushed',
|
event: 'secrets pushed',
|
||||||
distinctId: req.user.email,
|
distinctId: req.user.email,
|
||||||
properties: {
|
properties: {
|
||||||
numberOfSecrets: secrets.length,
|
numberOfSecrets: secrets.length,
|
||||||
environment,
|
environment,
|
||||||
workspaceId,
|
workspaceId,
|
||||||
channel: channel ? channel : 'cli'
|
channel: channel ? channel : 'cli'
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// trigger event - push secrets
|
// trigger event - push secrets
|
||||||
EventService.handleEvent({
|
EventService.handleEvent({
|
||||||
event: eventPushSecrets({
|
event: eventPushSecrets({
|
||||||
workspaceId: new Types.ObjectId(workspaceId),
|
workspaceId: new Types.ObjectId(workspaceId),
|
||||||
environment
|
environment
|
||||||
})
|
})
|
||||||
});
|
});
|
||||||
|
|
||||||
} 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({
|
return res.status(200).send({
|
||||||
message: 'Successfully uploaded workspace secrets'
|
message: 'Successfully uploaded workspace secrets'
|
||||||
@@ -122,56 +112,48 @@ export const pushWorkspaceSecrets = async (req: Request, res: Response) => {
|
|||||||
*/
|
*/
|
||||||
export const pullSecrets = async (req: Request, res: Response) => {
|
export const pullSecrets = async (req: Request, res: Response) => {
|
||||||
let secrets;
|
let secrets;
|
||||||
try {
|
const postHogClient = await TelemetryService.getPostHogClient();
|
||||||
const postHogClient = await TelemetryService.getPostHogClient();
|
const environment: string = req.query.environment as string;
|
||||||
const environment: string = req.query.environment as string;
|
const channel: string = req.query.channel as string;
|
||||||
const channel: string = req.query.channel as string;
|
const { workspaceId } = req.params;
|
||||||
const { workspaceId } = req.params;
|
|
||||||
|
|
||||||
let userId;
|
let userId;
|
||||||
if (req.user) {
|
if (req.user) {
|
||||||
userId = req.user._id.toString();
|
userId = req.user._id.toString();
|
||||||
} else if (req.serviceTokenData) {
|
} else if (req.serviceTokenData) {
|
||||||
userId = req.serviceTokenData.user.toString();
|
userId = req.serviceTokenData.user.toString();
|
||||||
}
|
}
|
||||||
// validate environment
|
// validate environment
|
||||||
const workspaceEnvs = req.membership.workspace.environments;
|
const workspaceEnvs = req.membership.workspace.environments;
|
||||||
if (!workspaceEnvs.find(({ slug }: { slug: string }) => slug === environment)) {
|
if (!workspaceEnvs.find(({ slug }: { slug: string }) => slug === environment)) {
|
||||||
throw new Error('Failed to validate environment');
|
throw new Error('Failed to validate environment');
|
||||||
}
|
}
|
||||||
|
|
||||||
secrets = await pull({
|
secrets = await pull({
|
||||||
userId,
|
userId,
|
||||||
workspaceId,
|
workspaceId,
|
||||||
environment,
|
environment,
|
||||||
channel: channel ? channel : 'cli',
|
channel: channel ? channel : 'cli',
|
||||||
ipAddress: req.ip
|
ipAddress: req.realIP
|
||||||
});
|
});
|
||||||
|
|
||||||
if (channel !== 'cli') {
|
if (channel !== 'cli') {
|
||||||
secrets = reformatPullSecrets({ secrets });
|
secrets = reformatPullSecrets({ secrets });
|
||||||
}
|
}
|
||||||
|
|
||||||
if (postHogClient) {
|
if (postHogClient) {
|
||||||
// capture secrets pushed event in production
|
// capture secrets pushed event in production
|
||||||
postHogClient.capture({
|
postHogClient.capture({
|
||||||
distinctId: req.user.email,
|
distinctId: req.user.email,
|
||||||
event: 'secrets pulled',
|
event: 'secrets pulled',
|
||||||
properties: {
|
properties: {
|
||||||
numberOfSecrets: secrets.length,
|
numberOfSecrets: secrets.length,
|
||||||
environment,
|
environment,
|
||||||
workspaceId,
|
workspaceId,
|
||||||
channel: channel ? channel : 'cli'
|
channel: channel ? channel : 'cli'
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
} 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({
|
return res.status(200).send({
|
||||||
secrets
|
secrets
|
||||||
@@ -208,22 +190,14 @@ export const getWorkspaceKey = async (req: Request, res: Response) => {
|
|||||||
}
|
}
|
||||||
*/
|
*/
|
||||||
let key;
|
let key;
|
||||||
try {
|
const { workspaceId } = req.params;
|
||||||
const { workspaceId } = req.params;
|
|
||||||
|
|
||||||
key = await Key.findOne({
|
key = await Key.findOne({
|
||||||
workspace: workspaceId,
|
workspace: workspaceId,
|
||||||
receiver: req.user._id
|
receiver: req.user._id
|
||||||
}).populate('sender', '+publicKey');
|
}).populate('sender', '+publicKey');
|
||||||
|
|
||||||
if (!key) throw new Error('Failed to find workspace key');
|
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);
|
return res.status(200).json(key);
|
||||||
}
|
}
|
||||||
@@ -231,23 +205,13 @@ export const getWorkspaceServiceTokenData = async (
|
|||||||
req: Request,
|
req: Request,
|
||||||
res: Response
|
res: Response
|
||||||
) => {
|
) => {
|
||||||
let serviceTokenData;
|
const { workspaceId } = req.params;
|
||||||
try {
|
|
||||||
const { workspaceId } = req.params;
|
|
||||||
|
|
||||||
serviceTokenData = await ServiceTokenData
|
const serviceTokenData = await ServiceTokenData
|
||||||
.find({
|
.find({
|
||||||
workspace: workspaceId
|
workspace: workspaceId
|
||||||
})
|
})
|
||||||
.select('+encryptedKey +iv +tag');
|
.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({
|
return res.status(200).send({
|
||||||
serviceTokenData
|
serviceTokenData
|
||||||
@@ -294,20 +258,11 @@ export const getWorkspaceMemberships = async (req: Request, res: Response) => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
*/
|
*/
|
||||||
let memberships;
|
const { workspaceId } = req.params;
|
||||||
try {
|
|
||||||
const { workspaceId } = req.params;
|
|
||||||
|
|
||||||
memberships = await Membership.find({
|
const memberships = await Membership.find({
|
||||||
workspace: workspaceId
|
workspace: workspaceId
|
||||||
}).populate('user', '+publicKey');
|
}).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({
|
return res.status(200).send({
|
||||||
memberships
|
memberships
|
||||||
@@ -374,29 +329,20 @@ export const updateWorkspaceMembership = async (req: Request, res: Response) =>
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
*/
|
*/
|
||||||
let membership;
|
const {
|
||||||
try {
|
membershipId
|
||||||
const {
|
} = req.params;
|
||||||
membershipId
|
const { role } = req.body;
|
||||||
} = req.params;
|
|
||||||
const { role } = req.body;
|
const membership = await Membership.findByIdAndUpdate(
|
||||||
|
membershipId,
|
||||||
membership = await Membership.findByIdAndUpdate(
|
{
|
||||||
membershipId,
|
role
|
||||||
{
|
}, {
|
||||||
role
|
new: true
|
||||||
}, {
|
}
|
||||||
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({
|
return res.status(200).send({
|
||||||
membership
|
membership
|
||||||
});
|
});
|
||||||
@@ -445,27 +391,18 @@ export const deleteWorkspaceMembership = async (req: Request, res: Response) =>
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
*/
|
*/
|
||||||
let membership;
|
const {
|
||||||
try {
|
membershipId
|
||||||
const {
|
} = req.params;
|
||||||
membershipId
|
|
||||||
} = req.params;
|
const membership = await Membership.findByIdAndDelete(membershipId);
|
||||||
|
|
||||||
membership = await Membership.findByIdAndDelete(membershipId);
|
if (!membership) throw new Error('Failed to delete workspace membership');
|
||||||
|
|
||||||
if (!membership) throw new Error('Failed to delete workspace membership');
|
await Key.deleteMany({
|
||||||
|
receiver: membership.user,
|
||||||
await Key.deleteMany({
|
workspace: membership.workspace
|
||||||
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({
|
return res.status(200).send({
|
||||||
membership
|
membership
|
||||||
@@ -479,32 +416,23 @@ export const deleteWorkspaceMembership = async (req: Request, res: Response) =>
|
|||||||
* @returns
|
* @returns
|
||||||
*/
|
*/
|
||||||
export const toggleAutoCapitalization = async (req: Request, res: Response) => {
|
export const toggleAutoCapitalization = async (req: Request, res: Response) => {
|
||||||
let workspace;
|
const { workspaceId } = req.params;
|
||||||
try {
|
const { autoCapitalization } = req.body;
|
||||||
const { workspaceId } = req.params;
|
|
||||||
const { autoCapitalization } = req.body;
|
|
||||||
|
|
||||||
workspace = await Workspace.findOneAndUpdate(
|
const workspace = await Workspace.findOneAndUpdate(
|
||||||
{
|
{
|
||||||
_id: workspaceId
|
_id: workspaceId
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
autoCapitalization
|
autoCapitalization
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
new: true
|
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({
|
return res.status(200).send({
|
||||||
message: 'Successfully changed autoCapitalization setting',
|
message: 'Successfully changed autoCapitalization setting',
|
||||||
workspace
|
workspace
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -113,7 +113,7 @@ export const login2 = async (req: Request, res: Response) => {
|
|||||||
|
|
||||||
const user = await User.findOne({
|
const user = await User.findOne({
|
||||||
email,
|
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');
|
if (!user) throw new Error('Failed to find user');
|
||||||
|
|
||||||
@@ -179,12 +179,16 @@ export const login2 = async (req: Request, res: Response) => {
|
|||||||
|
|
||||||
await checkUserDevice({
|
await checkUserDevice({
|
||||||
user,
|
user,
|
||||||
ip: req.ip,
|
ip: req.realIP,
|
||||||
userAgent: req.headers['user-agent'] ?? ''
|
userAgent: req.headers['user-agent'] ?? ''
|
||||||
});
|
});
|
||||||
|
|
||||||
// issue tokens
|
// 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
|
// store (refresh) token in httpOnly cookie
|
||||||
res.cookie('jid', tokens.refreshToken, {
|
res.cookie('jid', tokens.refreshToken, {
|
||||||
@@ -239,7 +243,7 @@ export const login2 = async (req: Request, res: Response) => {
|
|||||||
userId: user._id,
|
userId: user._id,
|
||||||
actions: [loginAction],
|
actions: [loginAction],
|
||||||
channel: getChannelFromUserAgent(req.headers['user-agent']),
|
channel: getChannelFromUserAgent(req.headers['user-agent']),
|
||||||
ipAddress: req.ip
|
ipAddress: req.realIP
|
||||||
});
|
});
|
||||||
|
|
||||||
return res.status(200).send(response);
|
return res.status(200).send(response);
|
||||||
|
|||||||
@@ -137,7 +137,9 @@ export const completeAccountSignup = async (req: Request, res: Response) => {
|
|||||||
|
|
||||||
// issue tokens
|
// issue tokens
|
||||||
const tokens = await issueAuthTokens({
|
const tokens = await issueAuthTokens({
|
||||||
userId: user._id.toString()
|
userId: user._id,
|
||||||
|
ip: req.realIP,
|
||||||
|
userAgent: req.headers['user-agent'] ?? ''
|
||||||
});
|
});
|
||||||
|
|
||||||
token = tokens.token;
|
token = tokens.token;
|
||||||
|
|||||||
1497
backend/src/data/common_passwords.txt
Normal file
1497
backend/src/data/common_passwords.txt
Normal file
File diff suppressed because it is too large
Load Diff
3519
backend/src/data/disposable_emails.txt
Normal file
3519
backend/src/data/disposable_emails.txt
Normal file
File diff suppressed because it is too large
Load Diff
@@ -1,5 +1,4 @@
|
|||||||
import { Request, Response } from 'express';
|
import { Request, Response } from 'express';
|
||||||
import * as Sentry from '@sentry/node';
|
|
||||||
import { Action, SecretVersion } from '../../models';
|
import { Action, SecretVersion } from '../../models';
|
||||||
import { ActionNotFoundError } from '../../../utils/errors';
|
import { ActionNotFoundError } from '../../../utils/errors';
|
||||||
|
|
||||||
@@ -28,4 +27,4 @@ export const getAction = async (req: Request, res: Response) => {
|
|||||||
return res.status(200).send({
|
return res.status(200).send({
|
||||||
action
|
action
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
import * as Sentry from '@sentry/node';
|
|
||||||
import { Request, Response } from 'express';
|
import { Request, Response } from 'express';
|
||||||
import { EELicenseService } from '../../services';
|
import { EELicenseService } from '../../services';
|
||||||
import { getLicenseServerUrl } from '../../../config';
|
import { getLicenseServerUrl } from '../../../config';
|
||||||
@@ -12,23 +11,18 @@ import { licenseServerKeyRequest } from '../../../config/request';
|
|||||||
* @returns
|
* @returns
|
||||||
*/
|
*/
|
||||||
export const getCloudProducts = async (req: Request, res: Response) => {
|
export const getCloudProducts = async (req: Request, res: Response) => {
|
||||||
try {
|
const billingCycle = req.query['billing-cycle'] as string;
|
||||||
const billingCycle = req.query['billing-cycle'] as string;
|
|
||||||
|
|
||||||
if (EELicenseService.instanceType === 'cloud') {
|
if (EELicenseService.instanceType === 'cloud') {
|
||||||
const { data } = await licenseServerKeyRequest.get(
|
const { data } = await licenseServerKeyRequest.get(
|
||||||
`${await getLicenseServerUrl()}/api/license-server/v1/cloud-products?billing-cycle=${billingCycle}`
|
`${await getLicenseServerUrl()}/api/license-server/v1/cloud-products?billing-cycle=${billingCycle}`
|
||||||
);
|
);
|
||||||
|
|
||||||
return res.status(200).send(data);
|
return res.status(200).send(data);
|
||||||
}
|
|
||||||
} catch (err) {
|
|
||||||
Sentry.setUser({ email: req.user.email });
|
|
||||||
Sentry.captureException(err);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return res.status(200).send({
|
return res.status(200).send({
|
||||||
head: [],
|
head: [],
|
||||||
rows: []
|
rows: []
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
import { Request, Response } from "express";
|
import { Request, Response } from "express";
|
||||||
import * as Sentry from "@sentry/node";
|
|
||||||
import { Secret } from "../../../models";
|
import { Secret } from "../../../models";
|
||||||
import { SecretVersion } from "../../models";
|
import { SecretVersion } from "../../models";
|
||||||
import { EESecretService } from "../../services";
|
import { EESecretService } from "../../services";
|
||||||
@@ -55,29 +54,20 @@ export const getSecretVersions = async (req: Request, res: Response) => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
*/
|
*/
|
||||||
let secretVersions;
|
const { secretId, workspaceId, environment, folderId } = req.params;
|
||||||
try {
|
|
||||||
const { secretId, workspaceId, environment, folderId } = req.params;
|
|
||||||
|
|
||||||
const offset: number = parseInt(req.query.offset as string);
|
const offset: number = parseInt(req.query.offset as string);
|
||||||
const limit: number = parseInt(req.query.limit as string);
|
const limit: number = parseInt(req.query.limit as string);
|
||||||
|
|
||||||
secretVersions = await SecretVersion.find({
|
const secretVersions = await SecretVersion.find({
|
||||||
secret: secretId,
|
secret: secretId,
|
||||||
workspace: workspaceId,
|
workspace: workspaceId,
|
||||||
environment,
|
environment,
|
||||||
folder: folderId,
|
folder: folderId,
|
||||||
})
|
})
|
||||||
.sort({ createdAt: -1 })
|
.sort({ createdAt: -1 })
|
||||||
.skip(offset)
|
.skip(offset)
|
||||||
.limit(limit);
|
.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({
|
return res.status(200).send({
|
||||||
secretVersions,
|
secretVersions,
|
||||||
@@ -139,74 +129,45 @@ export const rollbackSecretVersion = async (req: Request, res: Response) => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
*/
|
*/
|
||||||
let secret;
|
const { secretId } = req.params;
|
||||||
try {
|
const { version } = req.body;
|
||||||
const { secretId } = req.params;
|
|
||||||
const { version } = req.body;
|
|
||||||
|
|
||||||
// validate secret version
|
// validate secret version
|
||||||
const oldSecretVersion = await SecretVersion.findOne({
|
const oldSecretVersion = await SecretVersion.findOne({
|
||||||
secret: secretId,
|
secret: secretId,
|
||||||
version,
|
version,
|
||||||
}).select("+secretBlindIndex");
|
}).select("+secretBlindIndex");
|
||||||
|
|
||||||
if (!oldSecretVersion) throw new Error("Failed to find secret version");
|
if (!oldSecretVersion) throw new Error("Failed to find secret version");
|
||||||
|
|
||||||
const {
|
const {
|
||||||
workspace,
|
workspace,
|
||||||
type,
|
type,
|
||||||
user,
|
user,
|
||||||
environment,
|
environment,
|
||||||
secretBlindIndex,
|
secretBlindIndex,
|
||||||
secretKeyCiphertext,
|
secretKeyCiphertext,
|
||||||
secretKeyIV,
|
secretKeyIV,
|
||||||
secretKeyTag,
|
secretKeyTag,
|
||||||
secretValueCiphertext,
|
secretValueCiphertext,
|
||||||
secretValueIV,
|
secretValueIV,
|
||||||
secretValueTag,
|
secretValueTag,
|
||||||
folder,
|
algorithm,
|
||||||
algorithm,
|
folder,
|
||||||
keyEncoding,
|
keyEncoding,
|
||||||
} = oldSecretVersion;
|
} = oldSecretVersion;
|
||||||
|
|
||||||
// update secret
|
// update secret
|
||||||
secret = await Secret.findByIdAndUpdate(
|
const secret = await Secret.findByIdAndUpdate(
|
||||||
secretId,
|
secretId,
|
||||||
{
|
{
|
||||||
$inc: {
|
$inc: {
|
||||||
version: 1,
|
version: 1,
|
||||||
},
|
|
||||||
workspace,
|
|
||||||
type,
|
|
||||||
user,
|
|
||||||
environment,
|
|
||||||
...(secretBlindIndex ? { secretBlindIndex } : {}),
|
|
||||||
secretKeyCiphertext,
|
|
||||||
secretKeyIV,
|
|
||||||
secretKeyTag,
|
|
||||||
secretValueCiphertext,
|
|
||||||
secretValueIV,
|
|
||||||
secretValueTag,
|
|
||||||
folderId: folder,
|
|
||||||
algorithm,
|
|
||||||
keyEncoding,
|
|
||||||
},
|
},
|
||||||
{
|
|
||||||
new: true,
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!secret) throw new Error("Failed to find and update secret");
|
|
||||||
|
|
||||||
// add new secret version
|
|
||||||
await new SecretVersion({
|
|
||||||
secret: secretId,
|
|
||||||
version: secret.version,
|
|
||||||
workspace,
|
workspace,
|
||||||
type,
|
type,
|
||||||
user,
|
user,
|
||||||
environment,
|
environment,
|
||||||
isDeleted: false,
|
|
||||||
...(secretBlindIndex ? { secretBlindIndex } : {}),
|
...(secretBlindIndex ? { secretBlindIndex } : {}),
|
||||||
secretKeyCiphertext,
|
secretKeyCiphertext,
|
||||||
secretKeyIV,
|
secretKeyIV,
|
||||||
@@ -214,24 +175,44 @@ export const rollbackSecretVersion = async (req: Request, res: Response) => {
|
|||||||
secretValueCiphertext,
|
secretValueCiphertext,
|
||||||
secretValueIV,
|
secretValueIV,
|
||||||
secretValueTag,
|
secretValueTag,
|
||||||
folder,
|
folderId: folder,
|
||||||
algorithm,
|
algorithm,
|
||||||
keyEncoding,
|
keyEncoding,
|
||||||
}).save();
|
},
|
||||||
|
{
|
||||||
|
new: true,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
// take secret snapshot
|
if (!secret) throw new Error("Failed to find and update secret");
|
||||||
await EESecretService.takeSecretSnapshot({
|
|
||||||
workspaceId: secret.workspace,
|
// add new secret version
|
||||||
environment,
|
await new SecretVersion({
|
||||||
folderId: folder,
|
secret: secretId,
|
||||||
});
|
version: secret.version,
|
||||||
} catch (err) {
|
workspace,
|
||||||
Sentry.setUser({ email: req.user.email });
|
type,
|
||||||
Sentry.captureException(err);
|
user,
|
||||||
return res.status(400).send({
|
environment,
|
||||||
message: "Failed to roll back secret version",
|
isDeleted: false,
|
||||||
});
|
...(secretBlindIndex ? { secretBlindIndex } : {}),
|
||||||
}
|
secretKeyCiphertext,
|
||||||
|
secretKeyIV,
|
||||||
|
secretKeyTag,
|
||||||
|
secretValueCiphertext,
|
||||||
|
secretValueIV,
|
||||||
|
secretValueTag,
|
||||||
|
folder,
|
||||||
|
algorithm,
|
||||||
|
keyEncoding,
|
||||||
|
}).save();
|
||||||
|
|
||||||
|
// take secret snapshot
|
||||||
|
await EESecretService.takeSecretSnapshot({
|
||||||
|
workspaceId: secret.workspace,
|
||||||
|
environment,
|
||||||
|
folderId: folder,
|
||||||
|
});
|
||||||
|
|
||||||
return res.status(200).send({
|
return res.status(200).send({
|
||||||
secret,
|
secret,
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
import { Request, Response } from "express";
|
import { Request, Response } from "express";
|
||||||
import * as Sentry from "@sentry/node";
|
|
||||||
import {
|
import {
|
||||||
ISecretVersion,
|
ISecretVersion,
|
||||||
SecretSnapshot,
|
SecretSnapshot,
|
||||||
@@ -13,29 +12,21 @@ import {
|
|||||||
* @returns
|
* @returns
|
||||||
*/
|
*/
|
||||||
export const getSecretSnapshot = async (req: Request, res: Response) => {
|
export const getSecretSnapshot = async (req: Request, res: Response) => {
|
||||||
let secretSnapshot;
|
const { secretSnapshotId } = req.params;
|
||||||
try {
|
|
||||||
const { secretSnapshotId } = req.params;
|
|
||||||
|
|
||||||
secretSnapshot = await SecretSnapshot.findById(secretSnapshotId)
|
const secretSnapshot = await SecretSnapshot.findById(secretSnapshotId)
|
||||||
.lean()
|
.lean()
|
||||||
.populate<{ secretVersions: ISecretVersion[] }>({
|
.populate<{ secretVersions: ISecretVersion[] }>({
|
||||||
path: 'secretVersions',
|
path: 'secretVersions',
|
||||||
populate: {
|
populate: {
|
||||||
path: 'tags',
|
path: 'tags',
|
||||||
model: 'Tag'
|
model: 'Tag'
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.populate<{ folderVersion: TFolderRootVersionSchema }>("folderVersion");
|
.populate<{ folderVersion: TFolderRootVersionSchema }>("folderVersion");
|
||||||
|
|
||||||
if (!secretSnapshot) throw new Error("Failed to find secret snapshot");
|
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;
|
const folderId = secretSnapshot.folderId;
|
||||||
// to show only the folder required secrets
|
// to show only the folder required secrets
|
||||||
secretSnapshot.secretVersions = secretSnapshot.secretVersions.filter(
|
secretSnapshot.secretVersions = secretSnapshot.secretVersions.filter(
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
import * as Sentry from '@sentry/node';
|
|
||||||
import { Request, Response } from 'express';
|
import { Request, Response } from 'express';
|
||||||
import Stripe from 'stripe';
|
import Stripe from 'stripe';
|
||||||
import { getStripeSecretKey, getStripeWebhookSecret } from '../../../config';
|
import { getStripeSecretKey, getStripeWebhookSecret } from '../../../config';
|
||||||
@@ -10,26 +9,17 @@ import { getStripeSecretKey, getStripeWebhookSecret } from '../../../config';
|
|||||||
* @returns
|
* @returns
|
||||||
*/
|
*/
|
||||||
export const handleWebhook = async (req: Request, res: Response) => {
|
export const handleWebhook = async (req: Request, res: Response) => {
|
||||||
let event;
|
const stripe = new Stripe(await getStripeSecretKey(), {
|
||||||
try {
|
apiVersion: '2022-08-01'
|
||||||
const stripe = new Stripe(await getStripeSecretKey(), {
|
});
|
||||||
apiVersion: '2022-08-01'
|
|
||||||
});
|
|
||||||
|
|
||||||
// check request for valid stripe signature
|
// check request for valid stripe signature
|
||||||
const sig = req.headers['stripe-signature'] as string;
|
const sig = req.headers['stripe-signature'] as string;
|
||||||
event = stripe.webhooks.constructEvent(
|
const event = stripe.webhooks.constructEvent(
|
||||||
req.body,
|
req.body,
|
||||||
sig,
|
sig,
|
||||||
await getStripeWebhookSecret()
|
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) {
|
switch (event.type) {
|
||||||
case '':
|
case '':
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
import { Request, Response } from "express";
|
import { Request, Response } from "express";
|
||||||
import * as Sentry from "@sentry/node";
|
|
||||||
import { PipelineStage, Types } from "mongoose";
|
import { PipelineStage, Types } from "mongoose";
|
||||||
import { Secret } from "../../../models";
|
import { Secret } from "../../../models";
|
||||||
import {
|
import {
|
||||||
@@ -69,29 +68,20 @@ export const getWorkspaceSecretSnapshots = async (
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
*/
|
*/
|
||||||
let secretSnapshots;
|
const { workspaceId } = req.params;
|
||||||
try {
|
const { environment, folderId } = req.query;
|
||||||
const { workspaceId } = req.params;
|
|
||||||
const { environment, folderId } = req.query;
|
|
||||||
|
|
||||||
const offset: number = parseInt(req.query.offset as string);
|
const offset: number = parseInt(req.query.offset as string);
|
||||||
const limit: number = parseInt(req.query.limit as string);
|
const limit: number = parseInt(req.query.limit as string);
|
||||||
|
|
||||||
secretSnapshots = await SecretSnapshot.find({
|
const secretSnapshots = await SecretSnapshot.find({
|
||||||
workspace: workspaceId,
|
workspace: workspaceId,
|
||||||
environment,
|
environment,
|
||||||
folderId: folderId || "root",
|
folderId: folderId || "root",
|
||||||
})
|
})
|
||||||
.sort({ createdAt: -1 })
|
.sort({ createdAt: -1 })
|
||||||
.skip(offset)
|
.skip(offset)
|
||||||
.limit(limit);
|
.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({
|
return res.status(200).send({
|
||||||
secretSnapshots,
|
secretSnapshots,
|
||||||
@@ -107,23 +97,14 @@ export const getWorkspaceSecretSnapshotsCount = async (
|
|||||||
req: Request,
|
req: Request,
|
||||||
res: Response
|
res: Response
|
||||||
) => {
|
) => {
|
||||||
let count;
|
const { workspaceId } = req.params;
|
||||||
try {
|
const { environment, folderId } = req.query;
|
||||||
const { workspaceId } = req.params;
|
|
||||||
const { environment, folderId } = req.query;
|
|
||||||
|
|
||||||
count = await SecretSnapshot.countDocuments({
|
const count = await SecretSnapshot.countDocuments({
|
||||||
workspace: workspaceId,
|
workspace: workspaceId,
|
||||||
environment,
|
environment,
|
||||||
folderId: folderId || "root",
|
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({
|
return res.status(200).send({
|
||||||
count,
|
count,
|
||||||
@@ -191,324 +172,315 @@ export const rollbackWorkspaceSecretSnapshot = async (
|
|||||||
}
|
}
|
||||||
*/
|
*/
|
||||||
|
|
||||||
let secrets;
|
const { workspaceId } = req.params;
|
||||||
try {
|
const { version, environment, folderId = "root" } = req.body;
|
||||||
const { workspaceId } = req.params;
|
|
||||||
const { version, environment, folderId = "root" } = req.body;
|
|
||||||
|
|
||||||
// validate secret snapshot
|
// validate secret snapshot
|
||||||
const secretSnapshot = await SecretSnapshot.findOne({
|
const secretSnapshot = await SecretSnapshot.findOne({
|
||||||
workspace: workspaceId,
|
workspace: workspaceId,
|
||||||
version,
|
version,
|
||||||
environment,
|
environment,
|
||||||
folderId: folderId,
|
folderId: folderId,
|
||||||
|
})
|
||||||
|
.populate<{ secretVersions: ISecretVersion[] }>({
|
||||||
|
path: "secretVersions",
|
||||||
|
select: "+secretBlindIndex",
|
||||||
})
|
})
|
||||||
.populate<{ secretVersions: ISecretVersion[] }>({
|
.populate<{ folderVersion: TFolderRootVersionSchema }>("folderVersion");
|
||||||
path: "secretVersions",
|
|
||||||
select: "+secretBlindIndex",
|
|
||||||
})
|
|
||||||
.populate<{ folderVersion: TFolderRootVersionSchema }>("folderVersion");
|
|
||||||
|
|
||||||
if (!secretSnapshot) throw new Error("Failed to find secret snapshot");
|
if (!secretSnapshot) throw new Error("Failed to find secret snapshot");
|
||||||
|
|
||||||
const snapshotFolderTree = secretSnapshot.folderVersion;
|
const snapshotFolderTree = secretSnapshot.folderVersion;
|
||||||
const latestFolderTree = await Folder.findOne({
|
const latestFolderTree = await Folder.findOne({
|
||||||
workspace: workspaceId,
|
workspace: workspaceId,
|
||||||
environment,
|
environment,
|
||||||
});
|
});
|
||||||
|
|
||||||
const latestFolderVersion = await FolderVersion.findOne({
|
const latestFolderVersion = await FolderVersion.findOne({
|
||||||
environment,
|
environment,
|
||||||
workspace: workspaceId,
|
workspace: workspaceId,
|
||||||
"nodes.id": folderId,
|
"nodes.id": folderId,
|
||||||
}).sort({ "nodes.version": -1 });
|
}).sort({ "nodes.version": -1 });
|
||||||
|
|
||||||
const oldSecretVersionsObj: Record<string, ISecretVersion> = {};
|
const oldSecretVersionsObj: Record<string, ISecretVersion> = {};
|
||||||
const secretIds: Types.ObjectId[] = [];
|
const secretIds: Types.ObjectId[] = [];
|
||||||
const folderIds: string[] = [folderId];
|
const folderIds: string[] = [folderId];
|
||||||
|
|
||||||
secretSnapshot.secretVersions.forEach((snapSecVer) => {
|
secretSnapshot.secretVersions.forEach((snapSecVer) => {
|
||||||
oldSecretVersionsObj[snapSecVer.secret.toString()] = snapSecVer;
|
oldSecretVersionsObj[snapSecVer.secret.toString()] = snapSecVer;
|
||||||
secretIds.push(snapSecVer.secret);
|
secretIds.push(snapSecVer.secret);
|
||||||
});
|
});
|
||||||
|
|
||||||
// the parent node from current latest one
|
// the parent node from current latest one
|
||||||
// this will be modified according to the snapshot and latest snapshots
|
// this will be modified according to the snapshot and latest snapshots
|
||||||
const newFolderTree =
|
const newFolderTree =
|
||||||
latestFolderTree && searchByFolderId(latestFolderTree.nodes, folderId);
|
latestFolderTree && searchByFolderId(latestFolderTree.nodes, folderId);
|
||||||
|
|
||||||
if (newFolderTree) {
|
if (newFolderTree) {
|
||||||
newFolderTree.children = snapshotFolderTree?.nodes?.children || [];
|
newFolderTree.children = snapshotFolderTree?.nodes?.children || [];
|
||||||
const queue = [newFolderTree];
|
const queue = [newFolderTree];
|
||||||
// a bfs algorithm in which we take the latest snapshots of all the folders in a level
|
// a bfs algorithm in which we take the latest snapshots of all the folders in a level
|
||||||
|
while (queue.length) {
|
||||||
|
const groupByFolderId: Record<string, TFolderSchema> = {};
|
||||||
|
// the original queue is popped out completely to get what ever in a level
|
||||||
|
// subqueue is filled with all the children thus next level folders
|
||||||
|
// subQueue will then be transfered to the oriinal queue
|
||||||
|
const subQueue: TFolderSchema[] = [];
|
||||||
|
// get everything inside a level
|
||||||
while (queue.length) {
|
while (queue.length) {
|
||||||
const groupByFolderId: Record<string, TFolderSchema> = {};
|
const folder = queue.pop() as TFolderSchema;
|
||||||
// the original queue is popped out completely to get what ever in a level
|
folder.children.forEach((el) => {
|
||||||
// subqueue is filled with all the children thus next level folders
|
folderIds.push(el.id); // push ids and data into queu
|
||||||
// subQueue will then be transfered to the oriinal queue
|
subQueue.push(el);
|
||||||
const subQueue: TFolderSchema[] = [];
|
// to modify the original tree very fast we keep a reference object
|
||||||
// get everything inside a level
|
// key with folder id and pointing to the various nodes
|
||||||
while (queue.length) {
|
groupByFolderId[el.id] = el;
|
||||||
const folder = queue.pop() as TFolderSchema;
|
|
||||||
folder.children.forEach((el) => {
|
|
||||||
folderIds.push(el.id); // push ids and data into queu
|
|
||||||
subQueue.push(el);
|
|
||||||
// to modify the original tree very fast we keep a reference object
|
|
||||||
// key with folder id and pointing to the various nodes
|
|
||||||
groupByFolderId[el.id] = el;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
// get latest snapshots of all the folder
|
|
||||||
const matchWsFoldersPipeline = {
|
|
||||||
$match: {
|
|
||||||
workspace: new Types.ObjectId(workspaceId),
|
|
||||||
environment,
|
|
||||||
folderId: {
|
|
||||||
$in: Object.keys(groupByFolderId),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
const sortByFolderIdAndVersion: PipelineStage = {
|
|
||||||
$sort: { folderId: 1, version: -1 },
|
|
||||||
};
|
|
||||||
const pickLatestVersionOfEachFolder = {
|
|
||||||
$group: {
|
|
||||||
_id: "$folderId",
|
|
||||||
latestVersion: { $first: "$version" },
|
|
||||||
doc: {
|
|
||||||
$first: "$$ROOT",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
const populateSecVersion = {
|
|
||||||
$lookup: {
|
|
||||||
from: SecretVersion.collection.name,
|
|
||||||
localField: "doc.secretVersions",
|
|
||||||
foreignField: "_id",
|
|
||||||
as: "doc.secretVersions",
|
|
||||||
},
|
|
||||||
};
|
|
||||||
const populateFolderVersion = {
|
|
||||||
$lookup: {
|
|
||||||
from: FolderVersion.collection.name,
|
|
||||||
localField: "doc.folderVersion",
|
|
||||||
foreignField: "_id",
|
|
||||||
as: "doc.folderVersion",
|
|
||||||
},
|
|
||||||
};
|
|
||||||
const unwindFolderVerField = {
|
|
||||||
$unwind: {
|
|
||||||
path: "$doc.folderVersion",
|
|
||||||
preserveNullAndEmptyArrays: true,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
const latestSnapshotsByFolders: Array<{ doc: typeof secretSnapshot }> =
|
|
||||||
await SecretSnapshot.aggregate([
|
|
||||||
matchWsFoldersPipeline,
|
|
||||||
sortByFolderIdAndVersion,
|
|
||||||
pickLatestVersionOfEachFolder,
|
|
||||||
populateSecVersion,
|
|
||||||
populateFolderVersion,
|
|
||||||
unwindFolderVerField,
|
|
||||||
]);
|
|
||||||
|
|
||||||
// recursive snapshotting each level
|
|
||||||
latestSnapshotsByFolders.forEach((snap) => {
|
|
||||||
// mutate the folder tree to update the nodes to the latest version tree
|
|
||||||
// we are reconstructing the folder tree by latest snapshots here
|
|
||||||
if (groupByFolderId[snap.doc.folderId]) {
|
|
||||||
groupByFolderId[snap.doc.folderId].children =
|
|
||||||
snap.doc?.folderVersion?.nodes?.children || [];
|
|
||||||
}
|
|
||||||
|
|
||||||
// push all children of next level snapshots
|
|
||||||
if (snap.doc.folderVersion?.nodes?.children) {
|
|
||||||
queue.push(...snap.doc.folderVersion.nodes.children);
|
|
||||||
}
|
|
||||||
|
|
||||||
snap.doc.secretVersions.forEach((snapSecVer) => {
|
|
||||||
// record all the secrets
|
|
||||||
oldSecretVersionsObj[snapSecVer.secret.toString()] = snapSecVer;
|
|
||||||
secretIds.push(snapSecVer.secret);
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
queue.push(...subQueue);
|
|
||||||
}
|
}
|
||||||
}
|
// get latest snapshots of all the folder
|
||||||
|
const matchWsFoldersPipeline = {
|
||||||
// TODO: fix any
|
$match: {
|
||||||
const latestSecretVersionIds = await getLatestSecretVersionIds({
|
workspace: new Types.ObjectId(workspaceId),
|
||||||
secretIds,
|
environment,
|
||||||
});
|
folderId: {
|
||||||
|
$in: Object.keys(groupByFolderId),
|
||||||
// TODO: fix any
|
|
||||||
const latestSecretVersions: any = (
|
|
||||||
await SecretVersion.find(
|
|
||||||
{
|
|
||||||
_id: {
|
|
||||||
$in: latestSecretVersionIds.map((s) => s.versionId),
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
"secret version"
|
};
|
||||||
)
|
const sortByFolderIdAndVersion: PipelineStage = {
|
||||||
).reduce(
|
$sort: { folderId: 1, version: -1 },
|
||||||
(accumulator, s) => ({
|
};
|
||||||
...accumulator,
|
const pickLatestVersionOfEachFolder = {
|
||||||
[`${s.secret.toString()}`]: s,
|
$group: {
|
||||||
}),
|
_id: "$folderId",
|
||||||
{}
|
latestVersion: { $first: "$version" },
|
||||||
);
|
doc: {
|
||||||
|
$first: "$$ROOT",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
const populateSecVersion = {
|
||||||
|
$lookup: {
|
||||||
|
from: SecretVersion.collection.name,
|
||||||
|
localField: "doc.secretVersions",
|
||||||
|
foreignField: "_id",
|
||||||
|
as: "doc.secretVersions",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
const populateFolderVersion = {
|
||||||
|
$lookup: {
|
||||||
|
from: FolderVersion.collection.name,
|
||||||
|
localField: "doc.folderVersion",
|
||||||
|
foreignField: "_id",
|
||||||
|
as: "doc.folderVersion",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
const unwindFolderVerField = {
|
||||||
|
$unwind: {
|
||||||
|
path: "$doc.folderVersion",
|
||||||
|
preserveNullAndEmptyArrays: true,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
const latestSnapshotsByFolders: Array<{ doc: typeof secretSnapshot }> =
|
||||||
|
await SecretSnapshot.aggregate([
|
||||||
|
matchWsFoldersPipeline,
|
||||||
|
sortByFolderIdAndVersion,
|
||||||
|
pickLatestVersionOfEachFolder,
|
||||||
|
populateSecVersion,
|
||||||
|
populateFolderVersion,
|
||||||
|
unwindFolderVerField,
|
||||||
|
]);
|
||||||
|
|
||||||
const secDelQuery: Record<string, unknown> = {
|
// recursive snapshotting each level
|
||||||
workspace: workspaceId,
|
latestSnapshotsByFolders.forEach((snap) => {
|
||||||
environment,
|
// mutate the folder tree to update the nodes to the latest version tree
|
||||||
// undefined means root thus collect all secrets
|
// we are reconstructing the folder tree by latest snapshots here
|
||||||
};
|
if (groupByFolderId[snap.doc.folderId]) {
|
||||||
if (folderId !== "root" && folderIds.length)
|
groupByFolderId[snap.doc.folderId].children =
|
||||||
secDelQuery.folder = { $in: folderIds };
|
snap.doc?.folderVersion?.nodes?.children || [];
|
||||||
|
}
|
||||||
|
|
||||||
// delete existing secrets
|
// push all children of next level snapshots
|
||||||
await Secret.deleteMany(secDelQuery);
|
if (snap.doc.folderVersion?.nodes?.children) {
|
||||||
await Folder.deleteOne({
|
queue.push(...snap.doc.folderVersion.nodes.children);
|
||||||
workspace: workspaceId,
|
}
|
||||||
environment,
|
|
||||||
});
|
|
||||||
|
|
||||||
// add secrets
|
snap.doc.secretVersions.forEach((snapSecVer) => {
|
||||||
secrets = await Secret.insertMany(
|
// record all the secrets
|
||||||
Object.keys(oldSecretVersionsObj).map((sv) => {
|
oldSecretVersionsObj[snapSecVer.secret.toString()] = snapSecVer;
|
||||||
const {
|
secretIds.push(snapSecVer.secret);
|
||||||
secret: secretId,
|
});
|
||||||
workspace,
|
|
||||||
type,
|
|
||||||
user,
|
|
||||||
environment,
|
|
||||||
secretBlindIndex,
|
|
||||||
secretKeyCiphertext,
|
|
||||||
secretKeyIV,
|
|
||||||
secretKeyTag,
|
|
||||||
secretValueCiphertext,
|
|
||||||
secretValueIV,
|
|
||||||
secretValueTag,
|
|
||||||
createdAt,
|
|
||||||
algorithm,
|
|
||||||
keyEncoding,
|
|
||||||
folder: secFolderId,
|
|
||||||
} = oldSecretVersionsObj[sv];
|
|
||||||
|
|
||||||
return {
|
|
||||||
_id: secretId,
|
|
||||||
version: latestSecretVersions[secretId.toString()].version + 1,
|
|
||||||
workspace,
|
|
||||||
type,
|
|
||||||
user,
|
|
||||||
environment,
|
|
||||||
secretBlindIndex: secretBlindIndex ?? undefined,
|
|
||||||
secretKeyCiphertext,
|
|
||||||
secretKeyIV,
|
|
||||||
secretKeyTag,
|
|
||||||
secretValueCiphertext,
|
|
||||||
secretValueIV,
|
|
||||||
secretValueTag,
|
|
||||||
secretCommentCiphertext: "",
|
|
||||||
secretCommentIV: "",
|
|
||||||
secretCommentTag: "",
|
|
||||||
createdAt,
|
|
||||||
algorithm,
|
|
||||||
keyEncoding,
|
|
||||||
folder: secFolderId,
|
|
||||||
};
|
|
||||||
})
|
|
||||||
);
|
|
||||||
|
|
||||||
// add secret versions
|
|
||||||
const secretV = await SecretVersion.insertMany(
|
|
||||||
secrets.map(
|
|
||||||
({
|
|
||||||
_id,
|
|
||||||
version,
|
|
||||||
workspace,
|
|
||||||
type,
|
|
||||||
user,
|
|
||||||
environment,
|
|
||||||
secretBlindIndex,
|
|
||||||
secretKeyCiphertext,
|
|
||||||
secretKeyIV,
|
|
||||||
secretKeyTag,
|
|
||||||
secretValueCiphertext,
|
|
||||||
secretValueIV,
|
|
||||||
secretValueTag,
|
|
||||||
algorithm,
|
|
||||||
keyEncoding,
|
|
||||||
folder: secFolderId,
|
|
||||||
}) => ({
|
|
||||||
_id: new Types.ObjectId(),
|
|
||||||
secret: _id,
|
|
||||||
version,
|
|
||||||
workspace,
|
|
||||||
type,
|
|
||||||
user,
|
|
||||||
environment,
|
|
||||||
isDeleted: false,
|
|
||||||
secretBlindIndex: secretBlindIndex ?? undefined,
|
|
||||||
secretKeyCiphertext,
|
|
||||||
secretKeyIV,
|
|
||||||
secretKeyTag,
|
|
||||||
secretValueCiphertext,
|
|
||||||
secretValueIV,
|
|
||||||
secretValueTag,
|
|
||||||
algorithm,
|
|
||||||
keyEncoding,
|
|
||||||
folder: secFolderId,
|
|
||||||
})
|
|
||||||
)
|
|
||||||
);
|
|
||||||
|
|
||||||
if (newFolderTree && latestFolderTree) {
|
|
||||||
// save the updated folder tree to the present one
|
|
||||||
newFolderTree.version = (latestFolderVersion?.nodes?.version || 0) + 1;
|
|
||||||
latestFolderTree._id = new Types.ObjectId();
|
|
||||||
latestFolderTree.isNew = true;
|
|
||||||
await latestFolderTree.save();
|
|
||||||
|
|
||||||
// create new folder version
|
|
||||||
const newFolderVersion = new FolderVersion({
|
|
||||||
workspace: workspaceId,
|
|
||||||
environment,
|
|
||||||
nodes: newFolderTree,
|
|
||||||
});
|
});
|
||||||
await newFolderVersion.save();
|
|
||||||
}
|
|
||||||
|
|
||||||
// update secret versions of restored secrets as not deleted
|
queue.push(...subQueue);
|
||||||
await SecretVersion.updateMany(
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: fix any
|
||||||
|
const latestSecretVersionIds = await getLatestSecretVersionIds({
|
||||||
|
secretIds,
|
||||||
|
});
|
||||||
|
|
||||||
|
// TODO: fix any
|
||||||
|
const latestSecretVersions: any = (
|
||||||
|
await SecretVersion.find(
|
||||||
{
|
{
|
||||||
secret: {
|
_id: {
|
||||||
$in: Object.keys(oldSecretVersionsObj).map(
|
$in: latestSecretVersionIds.map((s) => s.versionId),
|
||||||
(sv) => oldSecretVersionsObj[sv].secret
|
|
||||||
),
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
"secret version"
|
||||||
isDeleted: false,
|
)
|
||||||
}
|
).reduce(
|
||||||
);
|
(accumulator, s) => ({
|
||||||
|
...accumulator,
|
||||||
|
[`${s.secret.toString()}`]: s,
|
||||||
|
}),
|
||||||
|
{}
|
||||||
|
);
|
||||||
|
|
||||||
// take secret snapshot
|
const secDelQuery: Record<string, unknown> = {
|
||||||
await EESecretService.takeSecretSnapshot({
|
workspace: workspaceId,
|
||||||
workspaceId: new Types.ObjectId(workspaceId),
|
environment,
|
||||||
|
// undefined means root thus collect all secrets
|
||||||
|
};
|
||||||
|
if (folderId !== "root" && folderIds.length)
|
||||||
|
secDelQuery.folder = { $in: folderIds };
|
||||||
|
|
||||||
|
// delete existing secrets
|
||||||
|
await Secret.deleteMany(secDelQuery);
|
||||||
|
await Folder.deleteOne({
|
||||||
|
workspace: workspaceId,
|
||||||
|
environment,
|
||||||
|
});
|
||||||
|
|
||||||
|
// add secrets
|
||||||
|
const secrets = await Secret.insertMany(
|
||||||
|
Object.keys(oldSecretVersionsObj).map((sv) => {
|
||||||
|
const {
|
||||||
|
secret: secretId,
|
||||||
|
workspace,
|
||||||
|
type,
|
||||||
|
user,
|
||||||
|
environment,
|
||||||
|
secretBlindIndex,
|
||||||
|
secretKeyCiphertext,
|
||||||
|
secretKeyIV,
|
||||||
|
secretKeyTag,
|
||||||
|
secretValueCiphertext,
|
||||||
|
secretValueIV,
|
||||||
|
secretValueTag,
|
||||||
|
createdAt,
|
||||||
|
algorithm,
|
||||||
|
keyEncoding,
|
||||||
|
folder: secFolderId,
|
||||||
|
} = oldSecretVersionsObj[sv];
|
||||||
|
|
||||||
|
return {
|
||||||
|
_id: secretId,
|
||||||
|
version: latestSecretVersions[secretId.toString()].version + 1,
|
||||||
|
workspace,
|
||||||
|
type,
|
||||||
|
user,
|
||||||
|
environment,
|
||||||
|
secretBlindIndex: secretBlindIndex ?? undefined,
|
||||||
|
secretKeyCiphertext,
|
||||||
|
secretKeyIV,
|
||||||
|
secretKeyTag,
|
||||||
|
secretValueCiphertext,
|
||||||
|
secretValueIV,
|
||||||
|
secretValueTag,
|
||||||
|
secretCommentCiphertext: "",
|
||||||
|
secretCommentIV: "",
|
||||||
|
secretCommentTag: "",
|
||||||
|
createdAt,
|
||||||
|
algorithm,
|
||||||
|
keyEncoding,
|
||||||
|
folder: secFolderId,
|
||||||
|
};
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
// add secret versions
|
||||||
|
const secretV = await SecretVersion.insertMany(
|
||||||
|
secrets.map(
|
||||||
|
({
|
||||||
|
_id,
|
||||||
|
version,
|
||||||
|
workspace,
|
||||||
|
type,
|
||||||
|
user,
|
||||||
|
environment,
|
||||||
|
secretBlindIndex,
|
||||||
|
secretKeyCiphertext,
|
||||||
|
secretKeyIV,
|
||||||
|
secretKeyTag,
|
||||||
|
secretValueCiphertext,
|
||||||
|
secretValueIV,
|
||||||
|
secretValueTag,
|
||||||
|
algorithm,
|
||||||
|
keyEncoding,
|
||||||
|
folder: secFolderId,
|
||||||
|
}) => ({
|
||||||
|
_id: new Types.ObjectId(),
|
||||||
|
secret: _id,
|
||||||
|
version,
|
||||||
|
workspace,
|
||||||
|
type,
|
||||||
|
user,
|
||||||
|
environment,
|
||||||
|
isDeleted: false,
|
||||||
|
secretBlindIndex: secretBlindIndex ?? undefined,
|
||||||
|
secretKeyCiphertext,
|
||||||
|
secretKeyIV,
|
||||||
|
secretKeyTag,
|
||||||
|
secretValueCiphertext,
|
||||||
|
secretValueIV,
|
||||||
|
secretValueTag,
|
||||||
|
algorithm,
|
||||||
|
keyEncoding,
|
||||||
|
folder: secFolderId,
|
||||||
|
})
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
if (newFolderTree && latestFolderTree) {
|
||||||
|
// save the updated folder tree to the present one
|
||||||
|
newFolderTree.version = (latestFolderVersion?.nodes?.version || 0) + 1;
|
||||||
|
latestFolderTree._id = new Types.ObjectId();
|
||||||
|
latestFolderTree.isNew = true;
|
||||||
|
await latestFolderTree.save();
|
||||||
|
|
||||||
|
// create new folder version
|
||||||
|
const newFolderVersion = new FolderVersion({
|
||||||
|
workspace: workspaceId,
|
||||||
environment,
|
environment,
|
||||||
folderId,
|
nodes: newFolderTree,
|
||||||
});
|
|
||||||
} catch (err) {
|
|
||||||
Sentry.setUser({ email: req.user.email });
|
|
||||||
Sentry.captureException(err);
|
|
||||||
return res.status(400).send({
|
|
||||||
message: "Failed to roll back secret snapshot",
|
|
||||||
});
|
});
|
||||||
|
await newFolderVersion.save();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// update secret versions of restored secrets as not deleted
|
||||||
|
await SecretVersion.updateMany(
|
||||||
|
{
|
||||||
|
secret: {
|
||||||
|
$in: Object.keys(oldSecretVersionsObj).map(
|
||||||
|
(sv) => oldSecretVersionsObj[sv].secret
|
||||||
|
),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
isDeleted: false,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
// take secret snapshot
|
||||||
|
await EESecretService.takeSecretSnapshot({
|
||||||
|
workspaceId: new Types.ObjectId(workspaceId),
|
||||||
|
environment,
|
||||||
|
folderId,
|
||||||
|
});
|
||||||
|
|
||||||
return res.status(200).send({
|
return res.status(200).send({
|
||||||
secrets,
|
secrets,
|
||||||
});
|
});
|
||||||
@@ -587,39 +559,30 @@ export const getWorkspaceLogs = async (req: Request, res: Response) => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
*/
|
*/
|
||||||
let logs;
|
const { workspaceId } = req.params;
|
||||||
try {
|
|
||||||
const { workspaceId } = req.params;
|
|
||||||
|
|
||||||
const offset: number = parseInt(req.query.offset as string);
|
const offset: number = parseInt(req.query.offset as string);
|
||||||
const limit: number = parseInt(req.query.limit as string);
|
const limit: number = parseInt(req.query.limit as string);
|
||||||
const sortBy: string = req.query.sortBy as string;
|
const sortBy: string = req.query.sortBy as string;
|
||||||
const userId: string = req.query.userId as string;
|
const userId: string = req.query.userId as string;
|
||||||
const actionNames: string = req.query.actionNames as string;
|
const actionNames: string = req.query.actionNames as string;
|
||||||
|
|
||||||
logs = await Log.find({
|
const logs = await Log.find({
|
||||||
workspace: workspaceId,
|
workspace: workspaceId,
|
||||||
...(userId ? { user: userId } : {}),
|
...(userId ? { user: userId } : {}),
|
||||||
...(actionNames
|
...(actionNames
|
||||||
? {
|
? {
|
||||||
actionNames: {
|
actionNames: {
|
||||||
$in: actionNames.split(","),
|
$in: actionNames.split(","),
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
: {}),
|
: {}),
|
||||||
})
|
})
|
||||||
.sort({ createdAt: sortBy === "recent" ? -1 : 1 })
|
.sort({ createdAt: sortBy === "recent" ? -1 : 1 })
|
||||||
.skip(offset)
|
.skip(offset)
|
||||||
.limit(limit)
|
.limit(limit)
|
||||||
.populate("actions")
|
.populate("actions")
|
||||||
.populate("user serviceAccount serviceTokenData");
|
.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({
|
return res.status(200).send({
|
||||||
logs,
|
logs,
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import NodeCache from 'node-cache';
|
|
||||||
import * as Sentry from '@sentry/node';
|
import * as Sentry from '@sentry/node';
|
||||||
|
import NodeCache from 'node-cache';
|
||||||
import {
|
import {
|
||||||
getLicenseKey,
|
getLicenseKey,
|
||||||
getLicenseServerKey,
|
getLicenseServerKey,
|
||||||
@@ -135,4 +135,4 @@ class EELicenseService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default new EELicenseService();
|
export default new EELicenseService();
|
||||||
|
|||||||
@@ -6,7 +6,9 @@ import {
|
|||||||
User,
|
User,
|
||||||
ServiceTokenData,
|
ServiceTokenData,
|
||||||
ServiceAccount,
|
ServiceAccount,
|
||||||
APIKeyData
|
APIKeyData,
|
||||||
|
TokenVersion,
|
||||||
|
ITokenVersion
|
||||||
} from '../models';
|
} from '../models';
|
||||||
import {
|
import {
|
||||||
AccountNotFoundError,
|
AccountNotFoundError,
|
||||||
@@ -35,7 +37,7 @@ import {
|
|||||||
* @param {Object} obj
|
* @param {Object} obj
|
||||||
* @param {Object} obj.headers - HTTP request headers object
|
* @param {Object} obj.headers - HTTP request headers object
|
||||||
*/
|
*/
|
||||||
const validateAuthMode = ({
|
export const validateAuthMode = ({
|
||||||
headers,
|
headers,
|
||||||
acceptedAuthModes
|
acceptedAuthModes
|
||||||
}: {
|
}: {
|
||||||
@@ -97,7 +99,7 @@ const validateAuthMode = ({
|
|||||||
* @param {String} obj.authTokenValue - JWT token value
|
* @param {String} obj.authTokenValue - JWT token value
|
||||||
* @returns {User} user - user corresponding to JWT token
|
* @returns {User} user - user corresponding to JWT token
|
||||||
*/
|
*/
|
||||||
const getAuthUserPayload = async ({
|
export const getAuthUserPayload = async ({
|
||||||
authTokenValue
|
authTokenValue
|
||||||
}: {
|
}: {
|
||||||
authTokenValue: string;
|
authTokenValue: string;
|
||||||
@@ -107,14 +109,32 @@ const getAuthUserPayload = async ({
|
|||||||
);
|
);
|
||||||
|
|
||||||
const user = await User.findOne({
|
const user = await User.findOne({
|
||||||
_id: decodedToken.userId
|
_id: new Types.ObjectId(decodedToken.userId)
|
||||||
}).select('+publicKey');
|
}).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
|
* @param {String} obj.authTokenValue - service token value
|
||||||
* @returns {ServiceTokenData} serviceTokenData - service token data
|
* @returns {ServiceTokenData} serviceTokenData - service token data
|
||||||
*/
|
*/
|
||||||
const getAuthSTDPayload = async ({
|
export const getAuthSTDPayload = async ({
|
||||||
authTokenValue
|
authTokenValue
|
||||||
}: {
|
}: {
|
||||||
authTokenValue: string;
|
authTokenValue: string;
|
||||||
@@ -169,7 +189,7 @@ const getAuthSTDPayload = async ({
|
|||||||
* @param {String} obj.authTokenValue - service account access token value
|
* @param {String} obj.authTokenValue - service account access token value
|
||||||
* @returns {ServiceAccount} serviceAccount
|
* @returns {ServiceAccount} serviceAccount
|
||||||
*/
|
*/
|
||||||
const getAuthSAAKPayload = async ({
|
export const getAuthSAAKPayload = async ({
|
||||||
authTokenValue
|
authTokenValue
|
||||||
}: {
|
}: {
|
||||||
authTokenValue: string;
|
authTokenValue: string;
|
||||||
@@ -198,7 +218,7 @@ const getAuthSAAKPayload = async ({
|
|||||||
* @param {String} obj.authTokenValue - API key value
|
* @param {String} obj.authTokenValue - API key value
|
||||||
* @returns {APIKeyData} apiKeyData - API key data
|
* @returns {APIKeyData} apiKeyData - API key data
|
||||||
*/
|
*/
|
||||||
const getAuthAPIKeyPayload = async ({
|
export const getAuthAPIKeyPayload = async ({
|
||||||
authTokenValue
|
authTokenValue
|
||||||
}: {
|
}: {
|
||||||
authTokenValue: string;
|
authTokenValue: string;
|
||||||
@@ -255,12 +275,43 @@ const getAuthAPIKeyPayload = async ({
|
|||||||
* @return {String} obj.token - issued JWT token
|
* @return {String} obj.token - issued JWT token
|
||||||
* @return {String} obj.refreshToken - issued refresh 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
|
// issue tokens
|
||||||
const token = createToken({
|
const token = createToken({
|
||||||
payload: {
|
payload: {
|
||||||
userId
|
userId,
|
||||||
|
tokenVersionId: tokenVersion._id.toString(),
|
||||||
|
accessVersion: tokenVersion.accessVersion
|
||||||
},
|
},
|
||||||
expiresIn: await getJwtAuthLifetime(),
|
expiresIn: await getJwtAuthLifetime(),
|
||||||
secret: await getJwtAuthSecret()
|
secret: await getJwtAuthSecret()
|
||||||
@@ -268,7 +319,9 @@ const issueAuthTokens = async ({ userId }: { userId: string }) => {
|
|||||||
|
|
||||||
const refreshToken = createToken({
|
const refreshToken = createToken({
|
||||||
payload: {
|
payload: {
|
||||||
userId
|
userId,
|
||||||
|
tokenVersionId: tokenVersion._id.toString(),
|
||||||
|
refreshVersion: tokenVersion.refreshVersion
|
||||||
},
|
},
|
||||||
expiresIn: await getJwtRefreshLifetime(),
|
expiresIn: await getJwtRefreshLifetime(),
|
||||||
secret: await getJwtRefreshSecret()
|
secret: await getJwtRefreshSecret()
|
||||||
@@ -285,13 +338,15 @@ const issueAuthTokens = async ({ userId }: { userId: string }) => {
|
|||||||
* @param {Object} obj
|
* @param {Object} obj
|
||||||
* @param {String} obj.userId - id of user whose tokens are cleared.
|
* @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
|
// increment refreshVersion on user by 1
|
||||||
User.findOneAndUpdate({
|
|
||||||
_id: userId
|
await TokenVersion.findOneAndUpdate({
|
||||||
|
_id: tokenVersionId
|
||||||
}, {
|
}, {
|
||||||
$inc: {
|
$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.secret - (JWT) secret such as [JWT_AUTH_SECRET]
|
||||||
* @param {String} obj.expiresIn - string describing time span such as '10h' or '7d'
|
* @param {String} obj.expiresIn - string describing time span such as '10h' or '7d'
|
||||||
*/
|
*/
|
||||||
const createToken = ({
|
export const createToken = ({
|
||||||
payload,
|
payload,
|
||||||
expiresIn,
|
expiresIn,
|
||||||
secret
|
secret
|
||||||
@@ -318,7 +373,7 @@ const createToken = ({
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const validateProviderAuthToken = async ({
|
export const validateProviderAuthToken = async ({
|
||||||
email,
|
email,
|
||||||
user,
|
user,
|
||||||
providerAuthToken,
|
providerAuthToken,
|
||||||
@@ -341,16 +396,4 @@ const validateProviderAuthToken = async ({
|
|||||||
) {
|
) {
|
||||||
throw new Error('Invalid authentication credentials.')
|
throw new Error('Invalid authentication credentials.')
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export {
|
|
||||||
validateAuthMode,
|
|
||||||
validateProviderAuthToken,
|
|
||||||
getAuthUserPayload,
|
|
||||||
getAuthSTDPayload,
|
|
||||||
getAuthSAAKPayload,
|
|
||||||
getAuthAPIKeyPayload,
|
|
||||||
createToken,
|
|
||||||
issueAuthTokens,
|
|
||||||
clearTokens
|
|
||||||
};
|
|
||||||
@@ -31,7 +31,7 @@ import { InternalServerError } from "../utils/errors";
|
|||||||
* @param {String} obj.name - name of bot
|
* @param {String} obj.name - name of bot
|
||||||
* @param {String} obj.workspaceId - id of workspace that bot belongs to
|
* @param {String} obj.workspaceId - id of workspace that bot belongs to
|
||||||
*/
|
*/
|
||||||
const createBot = async ({
|
export const createBot = async ({
|
||||||
name,
|
name,
|
||||||
workspaceId,
|
workspaceId,
|
||||||
}: {
|
}: {
|
||||||
@@ -93,7 +93,7 @@ const createBot = async ({
|
|||||||
* @param {String} obj.workspaceId - id of workspace
|
* @param {String} obj.workspaceId - id of workspace
|
||||||
* @param {String} obj.environment - environment
|
* @param {String} obj.environment - environment
|
||||||
*/
|
*/
|
||||||
const getSecretsHelper = async ({
|
export const getSecretsBotHelper = async ({
|
||||||
workspaceId,
|
workspaceId,
|
||||||
environment,
|
environment,
|
||||||
}: {
|
}: {
|
||||||
@@ -136,7 +136,7 @@ const getSecretsHelper = async ({
|
|||||||
* @param {String} obj.workspaceId - id of workspace
|
* @param {String} obj.workspaceId - id of workspace
|
||||||
* @returns {String} key - decrypted workspace key
|
* @returns {String} key - decrypted workspace key
|
||||||
*/
|
*/
|
||||||
const getKey = async ({ workspaceId }: { workspaceId: string }) => {
|
export const getKey = async ({ workspaceId }: { workspaceId: string }) => {
|
||||||
const encryptionKey = await getEncryptionKey();
|
const encryptionKey = await getEncryptionKey();
|
||||||
const rootEncryptionKey = await getRootEncryptionKey();
|
const rootEncryptionKey = await getRootEncryptionKey();
|
||||||
|
|
||||||
@@ -194,7 +194,7 @@ const getKey = async ({ workspaceId }: { workspaceId: string }) => {
|
|||||||
* @param {String} obj1.workspaceId - id of workspace
|
* @param {String} obj1.workspaceId - id of workspace
|
||||||
* @param {String} obj1.plaintext - plaintext to encrypt
|
* @param {String} obj1.plaintext - plaintext to encrypt
|
||||||
*/
|
*/
|
||||||
const encryptSymmetricHelper = async ({
|
export const encryptSymmetricHelper = async ({
|
||||||
workspaceId,
|
workspaceId,
|
||||||
plaintext,
|
plaintext,
|
||||||
}: {
|
}: {
|
||||||
@@ -222,7 +222,7 @@ const encryptSymmetricHelper = async ({
|
|||||||
* @param {String} obj.iv - iv
|
* @param {String} obj.iv - iv
|
||||||
* @param {String} obj.tag - tag
|
* @param {String} obj.tag - tag
|
||||||
*/
|
*/
|
||||||
const decryptSymmetricHelper = async ({
|
export const decryptSymmetricHelper = async ({
|
||||||
workspaceId,
|
workspaceId,
|
||||||
ciphertext,
|
ciphertext,
|
||||||
iv,
|
iv,
|
||||||
@@ -242,11 +242,4 @@ const decryptSymmetricHelper = async ({
|
|||||||
});
|
});
|
||||||
|
|
||||||
return plaintext;
|
return plaintext;
|
||||||
};
|
};
|
||||||
|
|
||||||
export {
|
|
||||||
createBot,
|
|
||||||
getSecretsHelper,
|
|
||||||
encryptSymmetricHelper,
|
|
||||||
decryptSymmetricHelper
|
|
||||||
};
|
|
||||||
@@ -7,7 +7,7 @@ import { getLogger } from '../utils/logger';
|
|||||||
* @param {String} obj.mongoURL - mongo connection string
|
* @param {String} obj.mongoURL - mongo connection string
|
||||||
* @returns
|
* @returns
|
||||||
*/
|
*/
|
||||||
const initDatabaseHelper = async ({
|
export const initDatabaseHelper = async ({
|
||||||
mongoURL
|
mongoURL
|
||||||
}: {
|
}: {
|
||||||
mongoURL: string;
|
mongoURL: string;
|
||||||
@@ -30,7 +30,7 @@ const initDatabaseHelper = async ({
|
|||||||
/**
|
/**
|
||||||
* Close database conection
|
* Close database conection
|
||||||
*/
|
*/
|
||||||
const closeDatabaseHelper = async () => {
|
export const closeDatabaseHelper = async () => {
|
||||||
return Promise.all([
|
return Promise.all([
|
||||||
new Promise((resolve) => {
|
new Promise((resolve) => {
|
||||||
if (mongoose.connection && mongoose.connection.readyState == 1) {
|
if (mongoose.connection && mongoose.connection.readyState == 1) {
|
||||||
@@ -41,9 +41,4 @@ const closeDatabaseHelper = async () => {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
]);
|
]);
|
||||||
}
|
|
||||||
|
|
||||||
export {
|
|
||||||
initDatabaseHelper,
|
|
||||||
closeDatabaseHelper
|
|
||||||
}
|
}
|
||||||
@@ -18,7 +18,7 @@ interface Event {
|
|||||||
* @param {String} obj.event.workspaceId - id of workspace that event is part of
|
* @param {String} obj.event.workspaceId - id of workspace that event is part of
|
||||||
* @param {Object} obj.event.payload - payload of event (depends on event)
|
* @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;
|
const { workspaceId, environment } = event;
|
||||||
|
|
||||||
// TODO: moduralize bot check into separate function
|
// TODO: moduralize bot check into separate function
|
||||||
@@ -37,6 +37,4 @@ const handleEventHelper = async ({ event }: { event: Event }) => {
|
|||||||
});
|
});
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
export { handleEventHelper };
|
|
||||||
17
backend/src/helpers/index.ts
Normal file
17
backend/src/helpers/index.ts
Normal 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';
|
||||||
@@ -1,4 +1,3 @@
|
|||||||
import * as Sentry from '@sentry/node';
|
|
||||||
import { Types } from 'mongoose';
|
import { Types } from 'mongoose';
|
||||||
import {
|
import {
|
||||||
Bot,
|
Bot,
|
||||||
@@ -16,7 +15,6 @@ import {
|
|||||||
import {
|
import {
|
||||||
UnauthorizedRequestError,
|
UnauthorizedRequestError,
|
||||||
} from '../utils/errors';
|
} from '../utils/errors';
|
||||||
import RequestError from '../utils/requestError';
|
|
||||||
|
|
||||||
interface Update {
|
interface Update {
|
||||||
workspace: string;
|
workspace: string;
|
||||||
@@ -37,7 +35,7 @@ interface Update {
|
|||||||
* @param {String} obj.code - code
|
* @param {String} obj.code - code
|
||||||
* @returns {IntegrationAuth} integrationAuth - integration auth after OAuth2 code-token exchange
|
* @returns {IntegrationAuth} integrationAuth - integration auth after OAuth2 code-token exchange
|
||||||
*/
|
*/
|
||||||
const handleOAuthExchangeHelper = async ({
|
export const handleOAuthExchangeHelper = async ({
|
||||||
workspaceId,
|
workspaceId,
|
||||||
integration,
|
integration,
|
||||||
code,
|
code,
|
||||||
@@ -48,66 +46,59 @@ const handleOAuthExchangeHelper = async ({
|
|||||||
code: string;
|
code: string;
|
||||||
environment: string;
|
environment: string;
|
||||||
}) => {
|
}) => {
|
||||||
let integrationAuth;
|
const bot = await Bot.findOne({
|
||||||
try {
|
workspace: workspaceId,
|
||||||
const bot = await Bot.findOne({
|
isActive: true
|
||||||
workspace: workspaceId,
|
});
|
||||||
isActive: true
|
|
||||||
|
if (!bot) throw new Error('Bot must be enabled for OAuth2 code-token exchange');
|
||||||
|
|
||||||
|
// exchange code for access and refresh tokens
|
||||||
|
const res = await exchangeCode({
|
||||||
|
integration,
|
||||||
|
code
|
||||||
|
});
|
||||||
|
|
||||||
|
const update: Update = {
|
||||||
|
workspace: workspaceId,
|
||||||
|
integration
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (integration) {
|
||||||
|
case INTEGRATION_VERCEL:
|
||||||
|
update.teamId = res.teamId;
|
||||||
|
break;
|
||||||
|
case INTEGRATION_NETLIFY:
|
||||||
|
update.accountId = res.accountId;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
const integrationAuth = await IntegrationAuth.findOneAndUpdate({
|
||||||
|
workspace: workspaceId,
|
||||||
|
integration
|
||||||
|
}, update, {
|
||||||
|
new: true,
|
||||||
|
upsert: true
|
||||||
|
});
|
||||||
|
|
||||||
|
if (res.refreshToken) {
|
||||||
|
// case: refresh token returned from exchange
|
||||||
|
// set integration auth refresh token
|
||||||
|
await setIntegrationAuthRefreshHelper({
|
||||||
|
integrationAuthId: integrationAuth._id.toString(),
|
||||||
|
refreshToken: res.refreshToken
|
||||||
});
|
});
|
||||||
|
}
|
||||||
if (!bot) throw new Error('Bot must be enabled for OAuth2 code-token exchange');
|
|
||||||
|
if (res.accessToken) {
|
||||||
// exchange code for access and refresh tokens
|
// case: access token returned from exchange
|
||||||
const res = await exchangeCode({
|
// set integration auth access token
|
||||||
integration,
|
await setIntegrationAuthAccessHelper({
|
||||||
code
|
integrationAuthId: integrationAuth._id.toString(),
|
||||||
|
accessId: null,
|
||||||
|
accessToken: res.accessToken,
|
||||||
|
accessExpiresAt: res.accessExpiresAt
|
||||||
});
|
});
|
||||||
|
|
||||||
const update: Update = {
|
|
||||||
workspace: workspaceId,
|
|
||||||
integration
|
|
||||||
}
|
|
||||||
|
|
||||||
switch (integration) {
|
|
||||||
case INTEGRATION_VERCEL:
|
|
||||||
update.teamId = res.teamId;
|
|
||||||
break;
|
|
||||||
case INTEGRATION_NETLIFY:
|
|
||||||
update.accountId = res.accountId;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
integrationAuth = await IntegrationAuth.findOneAndUpdate({
|
|
||||||
workspace: workspaceId,
|
|
||||||
integration
|
|
||||||
}, update, {
|
|
||||||
new: true,
|
|
||||||
upsert: true
|
|
||||||
});
|
|
||||||
|
|
||||||
if (res.refreshToken) {
|
|
||||||
// case: refresh token returned from exchange
|
|
||||||
// set integration auth refresh token
|
|
||||||
await setIntegrationAuthRefreshHelper({
|
|
||||||
integrationAuthId: integrationAuth._id.toString(),
|
|
||||||
refreshToken: res.refreshToken
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (res.accessToken) {
|
|
||||||
// case: access token returned from exchange
|
|
||||||
// set integration auth access token
|
|
||||||
await setIntegrationAuthAccessHelper({
|
|
||||||
integrationAuthId: integrationAuth._id.toString(),
|
|
||||||
accessId: null,
|
|
||||||
accessToken: res.accessToken,
|
|
||||||
accessExpiresAt: res.accessExpiresAt
|
|
||||||
});
|
|
||||||
}
|
|
||||||
} catch (err) {
|
|
||||||
Sentry.setUser(null);
|
|
||||||
Sentry.captureException(err);
|
|
||||||
throw new Error('Failed to handle OAuth2 code-token exchange')
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return integrationAuth;
|
return integrationAuth;
|
||||||
@@ -118,54 +109,47 @@ const handleOAuthExchangeHelper = async ({
|
|||||||
* @param {Object} obj
|
* @param {Object} obj
|
||||||
* @param {Object} obj.workspaceId - id of workspace
|
* @param {Object} obj.workspaceId - id of workspace
|
||||||
*/
|
*/
|
||||||
const syncIntegrationsHelper = async ({
|
export const syncIntegrationsHelper = async ({
|
||||||
workspaceId,
|
workspaceId,
|
||||||
environment
|
environment
|
||||||
}: {
|
}: {
|
||||||
workspaceId: Types.ObjectId;
|
workspaceId: Types.ObjectId;
|
||||||
environment?: string;
|
environment?: string;
|
||||||
}) => {
|
}) => {
|
||||||
let integrations;
|
const integrations = await Integration.find({
|
||||||
try {
|
workspace: workspaceId,
|
||||||
integrations = await Integration.find({
|
...(environment ? {
|
||||||
workspace: workspaceId,
|
environment
|
||||||
...(environment ? {
|
} : {}),
|
||||||
environment
|
isActive: true,
|
||||||
} : {}),
|
app: { $ne: null }
|
||||||
isActive: true,
|
});
|
||||||
app: { $ne: null }
|
|
||||||
|
// for each workspace integration, sync/push secrets
|
||||||
|
// to that integration
|
||||||
|
for await (const integration of integrations) {
|
||||||
|
// get workspace, environment (shared) secrets
|
||||||
|
const secrets = await BotService.getSecrets({ // issue here?
|
||||||
|
workspaceId: integration.workspace,
|
||||||
|
environment: integration.environment
|
||||||
});
|
});
|
||||||
|
|
||||||
// for each workspace integration, sync/push secrets
|
const integrationAuth = await IntegrationAuth.findById(integration.integrationAuth);
|
||||||
// to that integration
|
if (!integrationAuth) throw new Error('Failed to find integration auth');
|
||||||
for await (const integration of integrations) {
|
|
||||||
// get workspace, environment (shared) secrets
|
// get integration auth access token
|
||||||
const secrets = await BotService.getSecrets({ // issue here?
|
const access = await getIntegrationAuthAccessHelper({
|
||||||
workspaceId: integration.workspace,
|
integrationAuthId: integration.integrationAuth
|
||||||
environment: integration.environment
|
});
|
||||||
});
|
|
||||||
|
|
||||||
const integrationAuth = await IntegrationAuth.findById(integration.integrationAuth);
|
// sync secrets to integration
|
||||||
if (!integrationAuth) throw new Error('Failed to find integration auth');
|
await syncSecrets({
|
||||||
|
integration,
|
||||||
// get integration auth access token
|
integrationAuth,
|
||||||
const access = await getIntegrationAuthAccessHelper({
|
secrets,
|
||||||
integrationAuthId: integration.integrationAuth
|
accessId: access.accessId === undefined ? null : access.accessId,
|
||||||
});
|
accessToken: access.accessToken
|
||||||
|
});
|
||||||
// sync secrets to integration
|
|
||||||
await syncSecrets({
|
|
||||||
integration,
|
|
||||||
integrationAuth,
|
|
||||||
secrets,
|
|
||||||
accessId: access.accessId === undefined ? null : access.accessId,
|
|
||||||
accessToken: access.accessToken
|
|
||||||
});
|
|
||||||
}
|
|
||||||
} catch (err) {
|
|
||||||
Sentry.setUser(null);
|
|
||||||
Sentry.captureException(err);
|
|
||||||
throw new Error('Failed to sync secrets to integrations');
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -177,31 +161,19 @@ const syncIntegrationsHelper = async ({
|
|||||||
* @param {String} obj.integrationAuthId - id of integration auth
|
* @param {String} obj.integrationAuthId - id of integration auth
|
||||||
* @param {String} refreshToken - decrypted refresh token
|
* @param {String} refreshToken - decrypted refresh token
|
||||||
*/
|
*/
|
||||||
const getIntegrationAuthRefreshHelper = async ({ integrationAuthId }: { integrationAuthId: Types.ObjectId }) => {
|
export const getIntegrationAuthRefreshHelper = async ({ integrationAuthId }: { integrationAuthId: Types.ObjectId }) => {
|
||||||
let refreshToken;
|
const integrationAuth = await IntegrationAuth
|
||||||
|
.findById(integrationAuthId)
|
||||||
try {
|
.select('+refreshCiphertext +refreshIV +refreshTag');
|
||||||
const integrationAuth = await IntegrationAuth
|
|
||||||
.findById(integrationAuthId)
|
|
||||||
.select('+refreshCiphertext +refreshIV +refreshTag');
|
|
||||||
|
|
||||||
if (!integrationAuth) throw UnauthorizedRequestError({message: 'Failed to locate Integration Authentication credentials'});
|
if (!integrationAuth) throw UnauthorizedRequestError({message: 'Failed to locate Integration Authentication credentials'});
|
||||||
|
|
||||||
refreshToken = await BotService.decryptSymmetric({
|
const refreshToken = await BotService.decryptSymmetric({
|
||||||
workspaceId: integrationAuth.workspace,
|
workspaceId: integrationAuth.workspace,
|
||||||
ciphertext: integrationAuth.refreshCiphertext as string,
|
ciphertext: integrationAuth.refreshCiphertext as string,
|
||||||
iv: integrationAuth.refreshIV as string,
|
iv: integrationAuth.refreshIV as string,
|
||||||
tag: integrationAuth.refreshTag 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;
|
return refreshToken;
|
||||||
}
|
}
|
||||||
@@ -214,53 +186,43 @@ const syncIntegrationsHelper = async ({
|
|||||||
* @param {String} obj.integrationAuthId - id of integration auth
|
* @param {String} obj.integrationAuthId - id of integration auth
|
||||||
* @returns {String} accessToken - decrypted access token
|
* @returns {String} accessToken - decrypted access token
|
||||||
*/
|
*/
|
||||||
const getIntegrationAuthAccessHelper = async ({ integrationAuthId }: { integrationAuthId: Types.ObjectId }) => {
|
export const getIntegrationAuthAccessHelper = async ({ integrationAuthId }: { integrationAuthId: Types.ObjectId }) => {
|
||||||
let accessId;
|
let accessId;
|
||||||
let accessToken;
|
let accessToken;
|
||||||
try {
|
const integrationAuth = await IntegrationAuth
|
||||||
const integrationAuth = await IntegrationAuth
|
.findById(integrationAuthId)
|
||||||
.findById(integrationAuthId)
|
.select('workspace integration +accessCiphertext +accessIV +accessTag +accessExpiresAt + refreshCiphertext +accessIdCiphertext +accessIdIV +accessIdTag');
|
||||||
.select('workspace integration +accessCiphertext +accessIV +accessTag +accessExpiresAt + refreshCiphertext +accessIdCiphertext +accessIdIV +accessIdTag');
|
|
||||||
|
|
||||||
if (!integrationAuth) throw UnauthorizedRequestError({message: 'Failed to locate Integration Authentication credentials'});
|
if (!integrationAuth) throw UnauthorizedRequestError({message: 'Failed to locate Integration Authentication credentials'});
|
||||||
|
|
||||||
accessToken = await BotService.decryptSymmetric({
|
accessToken = await BotService.decryptSymmetric({
|
||||||
workspaceId: integrationAuth.workspace,
|
workspaceId: integrationAuth.workspace,
|
||||||
ciphertext: integrationAuth.accessCiphertext as string,
|
ciphertext: integrationAuth.accessCiphertext as string,
|
||||||
iv: integrationAuth.accessIV as string,
|
iv: integrationAuth.accessIV as string,
|
||||||
tag: integrationAuth.accessTag as string
|
tag: integrationAuth.accessTag as string
|
||||||
});
|
});
|
||||||
|
|
||||||
if (integrationAuth?.accessExpiresAt && integrationAuth?.refreshCiphertext) {
|
if (integrationAuth?.accessExpiresAt && integrationAuth?.refreshCiphertext) {
|
||||||
// there is a access token expiration date
|
// there is a access token expiration date
|
||||||
// and refresh token to exchange with the OAuth2 server
|
// and refresh token to exchange with the OAuth2 server
|
||||||
|
|
||||||
if (integrationAuth.accessExpiresAt < new Date()) {
|
|
||||||
// access token is expired
|
|
||||||
const refreshToken = await getIntegrationAuthRefreshHelper({ integrationAuthId });
|
|
||||||
accessToken = await exchangeRefresh({
|
|
||||||
integrationAuth,
|
|
||||||
refreshToken
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (integrationAuth?.accessIdCiphertext && integrationAuth?.accessIdIV && integrationAuth?.accessIdTag) {
|
if (integrationAuth.accessExpiresAt < new Date()) {
|
||||||
accessId = await BotService.decryptSymmetric({
|
// access token is expired
|
||||||
workspaceId: integrationAuth.workspace,
|
const refreshToken = await getIntegrationAuthRefreshHelper({ integrationAuthId });
|
||||||
ciphertext: integrationAuth.accessIdCiphertext as string,
|
accessToken = await exchangeRefresh({
|
||||||
iv: integrationAuth.accessIdIV as string,
|
integrationAuth,
|
||||||
tag: integrationAuth.accessIdTag as string
|
refreshToken
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
}
|
||||||
} catch (err) {
|
|
||||||
Sentry.setUser(null);
|
if (integrationAuth?.accessIdCiphertext && integrationAuth?.accessIdIV && integrationAuth?.accessIdTag) {
|
||||||
Sentry.captureException(err);
|
accessId = await BotService.decryptSymmetric({
|
||||||
if(err instanceof RequestError)
|
workspaceId: integrationAuth.workspace,
|
||||||
throw err
|
ciphertext: integrationAuth.accessIdCiphertext as string,
|
||||||
else
|
iv: integrationAuth.accessIdIV as string,
|
||||||
throw new Error('Failed to get integration access token');
|
tag: integrationAuth.accessIdTag as string
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
return ({
|
return ({
|
||||||
@@ -277,7 +239,7 @@ const getIntegrationAuthAccessHelper = async ({ integrationAuthId }: { integrati
|
|||||||
* @param {String} obj.integrationAuthId - id of integration auth
|
* @param {String} obj.integrationAuthId - id of integration auth
|
||||||
* @param {String} obj.refreshToken - refresh token
|
* @param {String} obj.refreshToken - refresh token
|
||||||
*/
|
*/
|
||||||
const setIntegrationAuthRefreshHelper = async ({
|
export const setIntegrationAuthRefreshHelper = async ({
|
||||||
integrationAuthId,
|
integrationAuthId,
|
||||||
refreshToken
|
refreshToken
|
||||||
}: {
|
}: {
|
||||||
@@ -285,34 +247,27 @@ const setIntegrationAuthRefreshHelper = async ({
|
|||||||
refreshToken: string;
|
refreshToken: string;
|
||||||
}) => {
|
}) => {
|
||||||
|
|
||||||
let integrationAuth;
|
let integrationAuth = await IntegrationAuth
|
||||||
try {
|
.findById(integrationAuthId);
|
||||||
integrationAuth = await IntegrationAuth
|
|
||||||
.findById(integrationAuthId);
|
if (!integrationAuth) throw new Error('Failed to find integration auth');
|
||||||
|
|
||||||
if (!integrationAuth) throw new Error('Failed to find integration auth');
|
const obj = await BotService.encryptSymmetric({
|
||||||
|
workspaceId: integrationAuth.workspace,
|
||||||
const obj = await BotService.encryptSymmetric({
|
plaintext: refreshToken
|
||||||
workspaceId: integrationAuth.workspace,
|
});
|
||||||
plaintext: refreshToken
|
|
||||||
});
|
integrationAuth = await IntegrationAuth.findOneAndUpdate({
|
||||||
|
_id: integrationAuthId
|
||||||
integrationAuth = await IntegrationAuth.findOneAndUpdate({
|
}, {
|
||||||
_id: integrationAuthId
|
refreshCiphertext: obj.ciphertext,
|
||||||
}, {
|
refreshIV: obj.iv,
|
||||||
refreshCiphertext: obj.ciphertext,
|
refreshTag: obj.tag,
|
||||||
refreshIV: obj.iv,
|
algorithm: ALGORITHM_AES_256_GCM,
|
||||||
refreshTag: obj.tag,
|
keyEncoding: ENCODING_SCHEME_UTF8
|
||||||
algorithm: ALGORITHM_AES_256_GCM,
|
}, {
|
||||||
keyEncoding: ENCODING_SCHEME_UTF8
|
new: true
|
||||||
}, {
|
});
|
||||||
new: true
|
|
||||||
});
|
|
||||||
} catch (err) {
|
|
||||||
Sentry.setUser(null);
|
|
||||||
Sentry.captureException(err);
|
|
||||||
throw new Error('Failed to set integration auth refresh token');
|
|
||||||
}
|
|
||||||
|
|
||||||
return integrationAuth;
|
return integrationAuth;
|
||||||
}
|
}
|
||||||
@@ -326,7 +281,7 @@ const setIntegrationAuthRefreshHelper = async ({
|
|||||||
* @param {String} obj.accessToken - access token
|
* @param {String} obj.accessToken - access token
|
||||||
* @param {Date} obj.accessExpiresAt - expiration date of access token
|
* @param {Date} obj.accessExpiresAt - expiration date of access token
|
||||||
*/
|
*/
|
||||||
const setIntegrationAuthAccessHelper = async ({
|
export const setIntegrationAuthAccessHelper = async ({
|
||||||
integrationAuthId,
|
integrationAuthId,
|
||||||
accessId,
|
accessId,
|
||||||
accessToken,
|
accessToken,
|
||||||
@@ -337,54 +292,38 @@ const setIntegrationAuthAccessHelper = async ({
|
|||||||
accessToken: string;
|
accessToken: string;
|
||||||
accessExpiresAt: Date | undefined;
|
accessExpiresAt: Date | undefined;
|
||||||
}) => {
|
}) => {
|
||||||
let integrationAuth;
|
let integrationAuth = await IntegrationAuth.findById(integrationAuthId);
|
||||||
try {
|
|
||||||
integrationAuth = await IntegrationAuth.findById(integrationAuthId);
|
if (!integrationAuth) throw new Error('Failed to find integration auth');
|
||||||
|
|
||||||
if (!integrationAuth) throw new Error('Failed to find integration auth');
|
const encryptedAccessTokenObj = await BotService.encryptSymmetric({
|
||||||
|
workspaceId: integrationAuth.workspace,
|
||||||
const encryptedAccessTokenObj = await BotService.encryptSymmetric({
|
plaintext: accessToken
|
||||||
|
});
|
||||||
|
|
||||||
|
let encryptedAccessIdObj;
|
||||||
|
if (accessId) {
|
||||||
|
encryptedAccessIdObj = await BotService.encryptSymmetric({
|
||||||
workspaceId: integrationAuth.workspace,
|
workspaceId: integrationAuth.workspace,
|
||||||
plaintext: accessToken
|
plaintext: accessId
|
||||||
});
|
});
|
||||||
|
|
||||||
let encryptedAccessIdObj;
|
|
||||||
if (accessId) {
|
|
||||||
encryptedAccessIdObj = await BotService.encryptSymmetric({
|
|
||||||
workspaceId: integrationAuth.workspace,
|
|
||||||
plaintext: accessId
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
integrationAuth = await IntegrationAuth.findOneAndUpdate({
|
|
||||||
_id: integrationAuthId
|
|
||||||
}, {
|
|
||||||
accessIdCiphertext: encryptedAccessIdObj?.ciphertext ?? undefined,
|
|
||||||
accessIdIV: encryptedAccessIdObj?.iv ?? undefined,
|
|
||||||
accessIdTag: encryptedAccessIdObj?.tag ?? undefined,
|
|
||||||
accessCiphertext: encryptedAccessTokenObj.ciphertext,
|
|
||||||
accessIV: encryptedAccessTokenObj.iv,
|
|
||||||
accessTag: encryptedAccessTokenObj.tag,
|
|
||||||
accessExpiresAt,
|
|
||||||
algorithm: ALGORITHM_AES_256_GCM,
|
|
||||||
keyEncoding: ENCODING_SCHEME_UTF8
|
|
||||||
}, {
|
|
||||||
new: true
|
|
||||||
});
|
|
||||||
} catch (err) {
|
|
||||||
Sentry.setUser(null);
|
|
||||||
Sentry.captureException(err);
|
|
||||||
throw new Error('Failed to save integration auth access token');
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
integrationAuth = await IntegrationAuth.findOneAndUpdate({
|
||||||
|
_id: integrationAuthId
|
||||||
|
}, {
|
||||||
|
accessIdCiphertext: encryptedAccessIdObj?.ciphertext ?? undefined,
|
||||||
|
accessIdIV: encryptedAccessIdObj?.iv ?? undefined,
|
||||||
|
accessIdTag: encryptedAccessIdObj?.tag ?? undefined,
|
||||||
|
accessCiphertext: encryptedAccessTokenObj.ciphertext,
|
||||||
|
accessIV: encryptedAccessTokenObj.iv,
|
||||||
|
accessTag: encryptedAccessTokenObj.tag,
|
||||||
|
accessExpiresAt,
|
||||||
|
algorithm: ALGORITHM_AES_256_GCM,
|
||||||
|
keyEncoding: ENCODING_SCHEME_UTF8
|
||||||
|
}, {
|
||||||
|
new: true
|
||||||
|
});
|
||||||
|
|
||||||
return integrationAuth;
|
return integrationAuth;
|
||||||
}
|
}
|
||||||
|
|
||||||
export {
|
|
||||||
handleOAuthExchangeHelper,
|
|
||||||
syncIntegrationsHelper,
|
|
||||||
getIntegrationAuthRefreshHelper,
|
|
||||||
getIntegrationAuthAccessHelper,
|
|
||||||
setIntegrationAuthRefreshHelper,
|
|
||||||
setIntegrationAuthAccessHelper
|
|
||||||
}
|
|
||||||
@@ -17,7 +17,7 @@ interface Key {
|
|||||||
* @param {String} obj.keys.nonce - nonce for encryption
|
* @param {String} obj.keys.nonce - nonce for encryption
|
||||||
* @param {String} obj.keys.userId - id of receiver user
|
* @param {String} obj.keys.userId - id of receiver user
|
||||||
*/
|
*/
|
||||||
const pushKeys = async ({
|
export const pushKeys = async ({
|
||||||
userId,
|
userId,
|
||||||
workspaceId,
|
workspaceId,
|
||||||
keys
|
keys
|
||||||
@@ -50,6 +50,4 @@ const pushKeys = async ({
|
|||||||
workspace: workspaceId
|
workspace: workspaceId
|
||||||
}))
|
}))
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export { pushKeys };
|
|
||||||
@@ -1,7 +1,6 @@
|
|||||||
import * as Sentry from '@sentry/node';
|
import { Types } from "mongoose";
|
||||||
import { Types } from 'mongoose';
|
import { Membership, Key } from "../models";
|
||||||
import { Membership, Key } from '../models';
|
import { MembershipNotFoundError, BadRequestError } from "../utils/errors";
|
||||||
import { MembershipNotFoundError, BadRequestError } from '../utils/errors';
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Validate that user with id [userId] is a member of workspace with id [workspaceId]
|
* 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
|
* @param {String} obj.workspaceId - id of workspace
|
||||||
* @returns {Membership} membership - membership of user with id [userId] for workspace with id [workspaceId]
|
* @returns {Membership} membership - membership of user with id [userId] for workspace with id [workspaceId]
|
||||||
*/
|
*/
|
||||||
const validateMembership = async ({
|
export const validateMembership = async ({
|
||||||
userId,
|
userId,
|
||||||
workspaceId,
|
workspaceId,
|
||||||
acceptedRoles,
|
acceptedRoles,
|
||||||
}: {
|
}: {
|
||||||
userId: Types.ObjectId | string;
|
userId: Types.ObjectId | string;
|
||||||
workspaceId: Types.ObjectId | string;
|
workspaceId: Types.ObjectId | string;
|
||||||
acceptedRoles?: Array<'admin' | 'member'>;
|
acceptedRoles?: Array<"admin" | "member">;
|
||||||
}) => {
|
}) => {
|
||||||
const membership = await Membership.findOne({
|
const membership = await Membership.findOne({
|
||||||
user: userId,
|
user: userId,
|
||||||
workspace: workspaceId,
|
workspace: workspaceId,
|
||||||
}).populate('workspace');
|
}).populate("workspace");
|
||||||
|
|
||||||
if (!membership) {
|
if (!membership) {
|
||||||
throw MembershipNotFoundError({
|
throw MembershipNotFoundError({
|
||||||
message: 'Failed to find workspace membership',
|
message: "Failed to find workspace membership",
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if (acceptedRoles) {
|
if (acceptedRoles) {
|
||||||
if (!acceptedRoles.includes(membership.role)) {
|
if (!acceptedRoles.includes(membership.role)) {
|
||||||
throw BadRequestError({
|
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
|
* @param {Object} queryObj - query object
|
||||||
* @return {Object} membership - membership
|
* @return {Object} membership - membership
|
||||||
*/
|
*/
|
||||||
const findMembership = async (queryObj: any) => {
|
export const findMembership = async (queryObj: any) => {
|
||||||
let membership;
|
const membership = await Membership.findOne(queryObj);
|
||||||
try {
|
|
||||||
membership = await Membership.findOne(queryObj);
|
|
||||||
} catch (err) {
|
|
||||||
Sentry.setUser(null);
|
|
||||||
Sentry.captureException(err);
|
|
||||||
throw new Error('Failed to find membership');
|
|
||||||
}
|
|
||||||
|
|
||||||
return membership;
|
return membership;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -68,40 +59,33 @@ const findMembership = async (queryObj: any) => {
|
|||||||
* @param {String} obj.workspaceId - id of workspace.
|
* @param {String} obj.workspaceId - id of workspace.
|
||||||
* @param {String[]} obj.roles - roles of users.
|
* @param {String[]} obj.roles - roles of users.
|
||||||
*/
|
*/
|
||||||
const addMemberships = async ({
|
export const addMemberships = async ({
|
||||||
userIds,
|
userIds,
|
||||||
workspaceId,
|
workspaceId,
|
||||||
roles,
|
roles
|
||||||
}: {
|
}: {
|
||||||
userIds: string[];
|
userIds: string[];
|
||||||
workspaceId: string;
|
workspaceId: string;
|
||||||
roles: string[];
|
roles: string[];
|
||||||
}): Promise<void> => {
|
}): Promise<void> => {
|
||||||
try {
|
const operations = userIds.map((userId, idx) => {
|
||||||
const operations = userIds.map((userId, idx) => {
|
return {
|
||||||
return {
|
updateOne: {
|
||||||
updateOne: {
|
filter: {
|
||||||
filter: {
|
user: userId,
|
||||||
user: userId,
|
workspace: workspaceId,
|
||||||
workspace: workspaceId,
|
role: roles[idx],
|
||||||
role: roles[idx],
|
|
||||||
},
|
|
||||||
update: {
|
|
||||||
user: userId,
|
|
||||||
workspace: workspaceId,
|
|
||||||
role: roles[idx],
|
|
||||||
},
|
|
||||||
upsert: true,
|
|
||||||
},
|
},
|
||||||
};
|
update: {
|
||||||
});
|
user: userId,
|
||||||
|
workspace: workspaceId,
|
||||||
await Membership.bulkWrite(operations as any);
|
role: roles[idx],
|
||||||
} catch (err) {
|
},
|
||||||
Sentry.setUser(null);
|
upsert: true,
|
||||||
Sentry.captureException(err);
|
},
|
||||||
throw new Error('Failed to add users to workspace');
|
};
|
||||||
}
|
});
|
||||||
|
await Membership.bulkWrite(operations as any);
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -109,28 +93,19 @@ const addMemberships = async ({
|
|||||||
* @param {Object} obj
|
* @param {Object} obj
|
||||||
* @param {String} obj.membershipId - id of membership to delete
|
* @param {String} obj.membershipId - id of membership to delete
|
||||||
*/
|
*/
|
||||||
const deleteMembership = async ({ membershipId }: { membershipId: string }) => {
|
export const deleteMembership = async ({ membershipId }: { membershipId: string }) => {
|
||||||
let deletedMembership;
|
const deletedMembership = await Membership.findOneAndDelete({
|
||||||
try {
|
_id: membershipId
|
||||||
deletedMembership = await Membership.findOneAndDelete({
|
});
|
||||||
_id: membershipId,
|
|
||||||
});
|
|
||||||
|
|
||||||
// delete keys associated with the membership
|
// delete keys associated with the membership
|
||||||
if (deletedMembership?.user) {
|
if (deletedMembership?.user) {
|
||||||
// case: membership had a registered user
|
// case: membership had a registered user
|
||||||
await Key.deleteMany({
|
await Key.deleteMany({
|
||||||
receiver: deletedMembership.user,
|
receiver: deletedMembership.user,
|
||||||
workspace: deletedMembership.workspace,
|
workspace: deletedMembership.workspace,
|
||||||
});
|
});
|
||||||
}
|
|
||||||
} catch (err) {
|
|
||||||
Sentry.setUser(null);
|
|
||||||
Sentry.captureException(err);
|
|
||||||
throw new Error('Failed to delete membership');
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return deletedMembership;
|
return deletedMembership;
|
||||||
};
|
};
|
||||||
|
|
||||||
export { validateMembership, addMemberships, findMembership, deleteMembership };
|
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ import {
|
|||||||
* @param {Types.ObjectId} obj.organizationId
|
* @param {Types.ObjectId} obj.organizationId
|
||||||
* @param {String[]} obj.acceptedRoles
|
* @param {String[]} obj.acceptedRoles
|
||||||
*/
|
*/
|
||||||
const validateMembershipOrg = async ({
|
export const validateMembershipOrg = async ({
|
||||||
userId,
|
userId,
|
||||||
organizationId,
|
organizationId,
|
||||||
acceptedRoles,
|
acceptedRoles,
|
||||||
@@ -59,7 +59,7 @@ const validateMembershipOrg = async ({
|
|||||||
* @param {Object} queryObj - query object
|
* @param {Object} queryObj - query object
|
||||||
* @return {Object} membershipOrg - membership
|
* @return {Object} membershipOrg - membership
|
||||||
*/
|
*/
|
||||||
const findMembershipOrg = (queryObj: any) => {
|
export const findMembershipOrg = (queryObj: any) => {
|
||||||
const membershipOrg = MembershipOrg.findOne(queryObj);
|
const membershipOrg = MembershipOrg.findOne(queryObj);
|
||||||
return membershipOrg;
|
return membershipOrg;
|
||||||
};
|
};
|
||||||
@@ -72,7 +72,7 @@ const findMembershipOrg = (queryObj: any) => {
|
|||||||
* @param {String} obj.organizationId - id of organization.
|
* @param {String} obj.organizationId - id of organization.
|
||||||
* @param {String[]} obj.roles - roles of users.
|
* @param {String[]} obj.roles - roles of users.
|
||||||
*/
|
*/
|
||||||
const addMembershipsOrg = async ({
|
export const addMembershipsOrg = async ({
|
||||||
userIds,
|
userIds,
|
||||||
organizationId,
|
organizationId,
|
||||||
roles,
|
roles,
|
||||||
@@ -111,7 +111,7 @@ const addMembershipsOrg = async ({
|
|||||||
* @param {Object} obj
|
* @param {Object} obj
|
||||||
* @param {String} obj.membershipOrgId - id of organization membership to delete
|
* @param {String} obj.membershipOrgId - id of organization membership to delete
|
||||||
*/
|
*/
|
||||||
const deleteMembershipOrg = async ({
|
export const deleteMembershipOrg = async ({
|
||||||
membershipOrgId
|
membershipOrgId
|
||||||
}: {
|
}: {
|
||||||
membershipOrgId: string;
|
membershipOrgId: string;
|
||||||
@@ -148,11 +148,4 @@ const deleteMembershipOrg = async ({
|
|||||||
}
|
}
|
||||||
|
|
||||||
return deletedMembershipOrg;
|
return deletedMembershipOrg;
|
||||||
};
|
};
|
||||||
|
|
||||||
export {
|
|
||||||
validateMembershipOrg,
|
|
||||||
findMembershipOrg,
|
|
||||||
addMembershipsOrg,
|
|
||||||
deleteMembershipOrg
|
|
||||||
};
|
|
||||||
@@ -1,4 +1,3 @@
|
|||||||
import * as Sentry from '@sentry/node';
|
|
||||||
import fs from 'fs';
|
import fs from 'fs';
|
||||||
import path from 'path';
|
import path from 'path';
|
||||||
import handlebars from 'handlebars';
|
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 {String[]} obj.recipients - email addresses of people to send email to
|
||||||
* @param {Object} obj.substitutions - object containing template substitutions
|
* @param {Object} obj.substitutions - object containing template substitutions
|
||||||
*/
|
*/
|
||||||
const sendMail = async ({
|
export const sendMail = async ({
|
||||||
template,
|
template,
|
||||||
subjectLine,
|
subjectLine,
|
||||||
recipients,
|
recipients,
|
||||||
@@ -26,29 +25,22 @@ const sendMail = async ({
|
|||||||
substitutions: any;
|
substitutions: any;
|
||||||
}) => {
|
}) => {
|
||||||
if (await getSmtpConfigured()) {
|
if (await getSmtpConfigured()) {
|
||||||
try {
|
const html = fs.readFileSync(
|
||||||
const html = fs.readFileSync(
|
path.resolve(__dirname, '../templates/' + template),
|
||||||
path.resolve(__dirname, '../templates/' + template),
|
'utf8'
|
||||||
'utf8'
|
);
|
||||||
);
|
const temp = handlebars.compile(html);
|
||||||
const temp = handlebars.compile(html);
|
const htmlToSend = temp(substitutions);
|
||||||
const htmlToSend = temp(substitutions);
|
|
||||||
|
|
||||||
await smtpTransporter.sendMail({
|
await smtpTransporter.sendMail({
|
||||||
from: `"${await getSmtpFromName()}" <${await getSmtpFromAddress()}>`,
|
from: `"${await getSmtpFromName()}" <${await getSmtpFromAddress()}>`,
|
||||||
to: recipients.join(', '),
|
to: recipients.join(', '),
|
||||||
subject: subjectLine,
|
subject: subjectLine,
|
||||||
html: htmlToSend
|
html: htmlToSend
|
||||||
});
|
});
|
||||||
} catch (err) {
|
|
||||||
Sentry.setUser(null);
|
|
||||||
Sentry.captureException(err);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const setTransporter = (transporter: nodemailer.Transporter) => {
|
export const setTransporter = (transporter: nodemailer.Transporter) => {
|
||||||
smtpTransporter = transporter;
|
smtpTransporter = transporter;
|
||||||
};
|
};
|
||||||
|
|
||||||
export { sendMail, setTransporter };
|
|
||||||
@@ -28,7 +28,7 @@ import {
|
|||||||
* @param {String} obj.email - POC email that will receive invoice info
|
* @param {String} obj.email - POC email that will receive invoice info
|
||||||
* @param {Object} organization - new organization
|
* @param {Object} organization - new organization
|
||||||
*/
|
*/
|
||||||
const createOrganization = async ({
|
export const createOrganization = async ({
|
||||||
name,
|
name,
|
||||||
email,
|
email,
|
||||||
}: {
|
}: {
|
||||||
@@ -70,7 +70,7 @@ const createOrganization = async ({
|
|||||||
* @return {Object} obj.stripeSubscription - new stripe subscription
|
* @return {Object} obj.stripeSubscription - new stripe subscription
|
||||||
* @return {Subscription} obj.subscription - new subscription
|
* @return {Subscription} obj.subscription - new subscription
|
||||||
*/
|
*/
|
||||||
const initSubscriptionOrg = async ({
|
export const initSubscriptionOrg = async ({
|
||||||
organizationId,
|
organizationId,
|
||||||
}: {
|
}: {
|
||||||
organizationId: Types.ObjectId;
|
organizationId: Types.ObjectId;
|
||||||
@@ -125,7 +125,7 @@ const initSubscriptionOrg = async ({
|
|||||||
* @param {Object} obj
|
* @param {Object} obj
|
||||||
* @param {Number} obj.organizationId - id of subscription's organization
|
* @param {Number} obj.organizationId - id of subscription's organization
|
||||||
*/
|
*/
|
||||||
const updateSubscriptionOrgQuantity = async ({
|
export const updateSubscriptionOrgQuantity = async ({
|
||||||
organizationId,
|
organizationId,
|
||||||
}: {
|
}: {
|
||||||
organizationId: string;
|
organizationId: string;
|
||||||
@@ -171,10 +171,4 @@ const updateSubscriptionOrgQuantity = async ({
|
|||||||
}
|
}
|
||||||
|
|
||||||
return stripeSubscription;
|
return stripeSubscription;
|
||||||
};
|
};
|
||||||
|
|
||||||
export {
|
|
||||||
createOrganization,
|
|
||||||
initSubscriptionOrg,
|
|
||||||
updateSubscriptionOrgQuantity
|
|
||||||
};
|
|
||||||
@@ -2,7 +2,7 @@ import rateLimit from 'express-rate-limit';
|
|||||||
const MongoStore = require('rate-limit-mongo');
|
const MongoStore = require('rate-limit-mongo');
|
||||||
|
|
||||||
// 200 per minute
|
// 200 per minute
|
||||||
const apiLimiter = rateLimit({
|
export const apiLimiter = rateLimit({
|
||||||
store: new MongoStore({
|
store: new MongoStore({
|
||||||
uri: process.env.MONGO_URL,
|
uri: process.env.MONGO_URL,
|
||||||
expireTimeMs: 1000 * 60,
|
expireTimeMs: 1000 * 60,
|
||||||
@@ -17,7 +17,7 @@ const apiLimiter = rateLimit({
|
|||||||
return request.path === '/healthcheck' || request.path === '/api/status'
|
return request.path === '/healthcheck' || request.path === '/api/status'
|
||||||
},
|
},
|
||||||
keyGenerator: (req, res) => {
|
keyGenerator: (req, res) => {
|
||||||
return req.clientIp
|
return req.realIP
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -34,12 +34,12 @@ const authLimit = rateLimit({
|
|||||||
standardHeaders: true,
|
standardHeaders: true,
|
||||||
legacyHeaders: false,
|
legacyHeaders: false,
|
||||||
keyGenerator: (req, res) => {
|
keyGenerator: (req, res) => {
|
||||||
return req.clientIp
|
return req.realIP
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// 50 requests per 1 hour
|
// 5 requests per 1 hour
|
||||||
const passwordLimiter = rateLimit({
|
export const passwordLimiter = rateLimit({
|
||||||
store: new MongoStore({
|
store: new MongoStore({
|
||||||
uri: process.env.MONGO_URL,
|
uri: process.env.MONGO_URL,
|
||||||
expireTimeMs: 1000 * 60 * 60,
|
expireTimeMs: 1000 * 60 * 60,
|
||||||
@@ -47,24 +47,18 @@ const passwordLimiter = rateLimit({
|
|||||||
collectionName: "expressRateRecords-passwordLimiter",
|
collectionName: "expressRateRecords-passwordLimiter",
|
||||||
}),
|
}),
|
||||||
windowMs: 1000 * 60 * 60,
|
windowMs: 1000 * 60 * 60,
|
||||||
max: 50,
|
max: 5,
|
||||||
standardHeaders: true,
|
standardHeaders: true,
|
||||||
legacyHeaders: false,
|
legacyHeaders: false,
|
||||||
keyGenerator: (req, res) => {
|
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') {
|
if (process.env.NODE_ENV === 'production') {
|
||||||
authLimit(req, res, next);
|
authLimit(req, res, next);
|
||||||
} else {
|
} else {
|
||||||
next();
|
next();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
export {
|
|
||||||
apiLimiter,
|
|
||||||
authLimiter,
|
|
||||||
passwordLimiter
|
|
||||||
};
|
|
||||||
@@ -60,7 +60,7 @@ interface Update {
|
|||||||
* @param {String} obj.environment - environment for secrets
|
* @param {String} obj.environment - environment for secrets
|
||||||
* @param {Object[]} obj.secrets - secrets to push
|
* @param {Object[]} obj.secrets - secrets to push
|
||||||
*/
|
*/
|
||||||
const v1PushSecrets = async ({
|
export const v1PushSecrets = async ({
|
||||||
userId,
|
userId,
|
||||||
workspaceId,
|
workspaceId,
|
||||||
environment,
|
environment,
|
||||||
@@ -304,7 +304,7 @@ const v1PushSecrets = async ({
|
|||||||
* @param {String} obj.channel - channel (web/cli/auto)
|
* @param {String} obj.channel - channel (web/cli/auto)
|
||||||
* @param {String} obj.ipAddress - ip address of request to push secrets
|
* @param {String} obj.ipAddress - ip address of request to push secrets
|
||||||
*/
|
*/
|
||||||
const v2PushSecrets = async ({
|
export const v2PushSecrets = async ({
|
||||||
userId,
|
userId,
|
||||||
workspaceId,
|
workspaceId,
|
||||||
environment,
|
environment,
|
||||||
@@ -530,7 +530,7 @@ const v2PushSecrets = async ({
|
|||||||
* @param {String} obj.workspaceId - id of workspace to pull from
|
* @param {String} obj.workspaceId - id of workspace to pull from
|
||||||
* @param {String} obj.environment - environment for secrets
|
* @param {String} obj.environment - environment for secrets
|
||||||
*/
|
*/
|
||||||
const getSecrets = async ({
|
export const getSecrets = async ({
|
||||||
userId,
|
userId,
|
||||||
workspaceId,
|
workspaceId,
|
||||||
environment,
|
environment,
|
||||||
@@ -570,7 +570,7 @@ const getSecrets = async ({
|
|||||||
* @param {String} obj.channel - channel (web/cli/auto)
|
* @param {String} obj.channel - channel (web/cli/auto)
|
||||||
* @param {String} obj.ipAddress - ip address of request to push secrets
|
* @param {String} obj.ipAddress - ip address of request to push secrets
|
||||||
*/
|
*/
|
||||||
const pullSecrets = async ({
|
export const pullSecrets = async ({
|
||||||
userId,
|
userId,
|
||||||
workspaceId,
|
workspaceId,
|
||||||
environment,
|
environment,
|
||||||
@@ -614,7 +614,7 @@ const pullSecrets = async ({
|
|||||||
* @param {Object} obj
|
* @param {Object} obj
|
||||||
* @param {Object} obj.secrets
|
* @param {Object} obj.secrets
|
||||||
*/
|
*/
|
||||||
const reformatPullSecrets = ({ secrets }: { secrets: ISecret[] }) => {
|
export const reformatPullSecrets = ({ secrets }: { secrets: ISecret[] }) => {
|
||||||
const reformatedSecrets = secrets.map((s) => ({
|
const reformatedSecrets = secrets.map((s) => ({
|
||||||
_id: s._id,
|
_id: s._id,
|
||||||
workspace: s.workspace,
|
workspace: s.workspace,
|
||||||
@@ -644,6 +644,4 @@ const reformatPullSecrets = ({ secrets }: { secrets: ISecret[] }) => {
|
|||||||
}));
|
}));
|
||||||
|
|
||||||
return reformatedSecrets;
|
return reformatedSecrets;
|
||||||
};
|
};
|
||||||
|
|
||||||
export { v1PushSecrets, v2PushSecrets, pullSecrets, reformatPullSecrets };
|
|
||||||
@@ -45,8 +45,8 @@ import {
|
|||||||
* @param {Object} obj
|
* @param {Object} obj
|
||||||
* @param {Types.ObjectId} obj.workspaceId
|
* @param {Types.ObjectId} obj.workspaceId
|
||||||
*/
|
*/
|
||||||
const createSecretBlindIndexDataHelper = async ({
|
export const createSecretBlindIndexDataHelper = async ({
|
||||||
workspaceId,
|
workspaceId
|
||||||
}: {
|
}: {
|
||||||
workspaceId: Types.ObjectId;
|
workspaceId: Types.ObjectId;
|
||||||
}) => {
|
}) => {
|
||||||
@@ -98,8 +98,8 @@ const createSecretBlindIndexDataHelper = async ({
|
|||||||
* @param {Types.ObjectId} obj.workspaceId - id of workspace to get salt for
|
* @param {Types.ObjectId} obj.workspaceId - id of workspace to get salt for
|
||||||
* @returns
|
* @returns
|
||||||
*/
|
*/
|
||||||
const getSecretBlindIndexSaltHelper = async ({
|
export const getSecretBlindIndexSaltHelper = async ({
|
||||||
workspaceId,
|
workspaceId
|
||||||
}: {
|
}: {
|
||||||
workspaceId: Types.ObjectId;
|
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.secretName - name of secret to generate blind index for
|
||||||
* @param {String} obj.salt - base64-salt
|
* @param {String} obj.salt - base64-salt
|
||||||
*/
|
*/
|
||||||
const generateSecretBlindIndexWithSaltHelper = async ({
|
export const generateSecretBlindIndexWithSaltHelper = async ({
|
||||||
secretName,
|
secretName,
|
||||||
salt,
|
salt
|
||||||
}: {
|
}: {
|
||||||
secretName: string;
|
secretName: string;
|
||||||
salt: string;
|
salt: string;
|
||||||
@@ -177,9 +177,9 @@ const generateSecretBlindIndexWithSaltHelper = async ({
|
|||||||
* @param {Stringj} obj.secretName - name of secret to generate blind index for
|
* @param {Stringj} obj.secretName - name of secret to generate blind index for
|
||||||
* @param {Types.ObjectId} obj.workspaceId - id of workspace that secret belongs to
|
* @param {Types.ObjectId} obj.workspaceId - id of workspace that secret belongs to
|
||||||
*/
|
*/
|
||||||
const generateSecretBlindIndexHelper = async ({
|
export const generateSecretBlindIndexHelper = async ({
|
||||||
secretName,
|
secretName,
|
||||||
workspaceId,
|
workspaceId
|
||||||
}: {
|
}: {
|
||||||
secretName: string;
|
secretName: string;
|
||||||
workspaceId: Types.ObjectId;
|
workspaceId: Types.ObjectId;
|
||||||
@@ -247,7 +247,7 @@ const generateSecretBlindIndexHelper = async ({
|
|||||||
* @param {AuthData} obj.authData - authentication data on request
|
* @param {AuthData} obj.authData - authentication data on request
|
||||||
* @returns
|
* @returns
|
||||||
*/
|
*/
|
||||||
const createSecretHelper = async ({
|
export const createSecretHelper = async ({
|
||||||
secretName,
|
secretName,
|
||||||
workspaceId,
|
workspaceId,
|
||||||
environment,
|
environment,
|
||||||
@@ -397,10 +397,10 @@ const createSecretHelper = async ({
|
|||||||
* @param {AuthData} obj.authData - authentication data on request
|
* @param {AuthData} obj.authData - authentication data on request
|
||||||
* @returns
|
* @returns
|
||||||
*/
|
*/
|
||||||
const getSecretsHelper = async ({
|
export const getSecretsHelper = async ({
|
||||||
workspaceId,
|
workspaceId,
|
||||||
environment,
|
environment,
|
||||||
authData,
|
authData
|
||||||
}: GetSecretsParams) => {
|
}: GetSecretsParams) => {
|
||||||
let secrets: ISecret[] = [];
|
let secrets: ISecret[] = [];
|
||||||
|
|
||||||
@@ -472,7 +472,7 @@ const getSecretsHelper = async ({
|
|||||||
* @param {AuthData} obj.authData - authentication data on request
|
* @param {AuthData} obj.authData - authentication data on request
|
||||||
* @returns
|
* @returns
|
||||||
*/
|
*/
|
||||||
const getSecretHelper = async ({
|
export const getSecretHelper = async ({
|
||||||
secretName,
|
secretName,
|
||||||
workspaceId,
|
workspaceId,
|
||||||
environment,
|
environment,
|
||||||
@@ -558,7 +558,8 @@ const getSecretHelper = async ({
|
|||||||
* @param {AuthData} obj.authData - authentication data on request
|
* @param {AuthData} obj.authData - authentication data on request
|
||||||
* @returns
|
* @returns
|
||||||
*/
|
*/
|
||||||
const updateSecretHelper = async ({
|
|
||||||
|
export const updateSecretHelper = async ({
|
||||||
secretName,
|
secretName,
|
||||||
workspaceId,
|
workspaceId,
|
||||||
environment,
|
environment,
|
||||||
@@ -698,7 +699,7 @@ const updateSecretHelper = async ({
|
|||||||
* @param {AuthData} obj.authData - authentication data on request
|
* @param {AuthData} obj.authData - authentication data on request
|
||||||
* @returns
|
* @returns
|
||||||
*/
|
*/
|
||||||
const deleteSecretHelper = async ({
|
export const deleteSecretHelper = async ({
|
||||||
secretName,
|
secretName,
|
||||||
workspaceId,
|
workspaceId,
|
||||||
environment,
|
environment,
|
||||||
@@ -799,16 +800,4 @@ const deleteSecretHelper = async ({
|
|||||||
secrets,
|
secrets,
|
||||||
secret,
|
secret,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
export {
|
|
||||||
createSecretBlindIndexDataHelper,
|
|
||||||
getSecretBlindIndexSaltHelper,
|
|
||||||
generateSecretBlindIndexWithSaltHelper,
|
|
||||||
generateSecretBlindIndexHelper,
|
|
||||||
createSecretHelper,
|
|
||||||
getSecretsHelper,
|
|
||||||
getSecretHelper,
|
|
||||||
updateSecretHelper,
|
|
||||||
deleteSecretHelper,
|
|
||||||
};
|
|
||||||
@@ -1,4 +1,3 @@
|
|||||||
import * as Sentry from '@sentry/node';
|
|
||||||
import { IUser } from '../models';
|
import { IUser } from '../models';
|
||||||
import { createOrganization } from './organization';
|
import { createOrganization } from './organization';
|
||||||
import { addMembershipsOrg } from './membershipOrg';
|
import { addMembershipsOrg } from './membershipOrg';
|
||||||
@@ -14,29 +13,21 @@ import { TOKEN_EMAIL_CONFIRMATION } from '../variables';
|
|||||||
* @param {String} obj.email - email
|
* @param {String} obj.email - email
|
||||||
* @returns {Boolean} success - whether or not operation was successful
|
* @returns {Boolean} success - whether or not operation was successful
|
||||||
*/
|
*/
|
||||||
const sendEmailVerification = async ({ email }: { email: string }) => {
|
export const sendEmailVerification = async ({ email }: { email: string }) => {
|
||||||
try {
|
const token = await TokenService.createToken({
|
||||||
const token = await TokenService.createToken({
|
type: TOKEN_EMAIL_CONFIRMATION,
|
||||||
type: TOKEN_EMAIL_CONFIRMATION,
|
email
|
||||||
email
|
});
|
||||||
});
|
|
||||||
|
|
||||||
// send mail
|
// send mail
|
||||||
await sendMail({
|
await sendMail({
|
||||||
template: 'emailVerification.handlebars',
|
template: 'emailVerification.handlebars',
|
||||||
subjectLine: 'Infisical confirmation code',
|
subjectLine: 'Infisical confirmation code',
|
||||||
recipients: [email],
|
recipients: [email],
|
||||||
substitutions: {
|
substitutions: {
|
||||||
code: token
|
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.email - emai
|
||||||
* @param {String} obj.code - code that was sent to [email]
|
* @param {String} obj.code - code that was sent to [email]
|
||||||
*/
|
*/
|
||||||
const checkEmailVerification = async ({
|
export const checkEmailVerification = async ({
|
||||||
email,
|
email,
|
||||||
code
|
code
|
||||||
}: {
|
}: {
|
||||||
email: string;
|
email: string;
|
||||||
code: string;
|
code: string;
|
||||||
}) => {
|
}) => {
|
||||||
try {
|
await TokenService.validateToken({
|
||||||
await TokenService.validateToken({
|
type: TOKEN_EMAIL_CONFIRMATION,
|
||||||
type: TOKEN_EMAIL_CONFIRMATION,
|
email,
|
||||||
email,
|
token: code
|
||||||
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 {String} obj.organizationName - name of organization to initialize
|
||||||
* @param {IUser} obj.user - user who we are initializing for
|
* @param {IUser} obj.user - user who we are initializing for
|
||||||
*/
|
*/
|
||||||
const initializeDefaultOrg = async ({
|
export const initializeDefaultOrg = async ({
|
||||||
organizationName,
|
organizationName,
|
||||||
user
|
user
|
||||||
}: {
|
}: {
|
||||||
@@ -96,6 +81,4 @@ const initializeDefaultOrg = async ({
|
|||||||
} catch (err) {
|
} catch (err) {
|
||||||
throw new Error(`Failed to initialize default organization and workspace [err=${err}]`);
|
throw new Error(`Failed to initialize default organization and workspace [err=${err}]`);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
export { sendEmailVerification, checkEmailVerification, initializeDefaultOrg };
|
|
||||||
@@ -20,7 +20,7 @@ import { getSaltRounds } from "../config";
|
|||||||
* @param {Types.ObjectId} obj.organizationId
|
* @param {Types.ObjectId} obj.organizationId
|
||||||
* @returns {String} token - the created token
|
* @returns {String} token - the created token
|
||||||
*/
|
*/
|
||||||
const createTokenHelper = async ({
|
export const createTokenHelper = async ({
|
||||||
type,
|
type,
|
||||||
email,
|
email,
|
||||||
phoneNumber,
|
phoneNumber,
|
||||||
@@ -121,7 +121,7 @@ const createTokenHelper = async ({
|
|||||||
* @param {String} obj.email - email associated with the token
|
* @param {String} obj.email - email associated with the token
|
||||||
* @param {String} obj.token - value of the token
|
* @param {String} obj.token - value of the token
|
||||||
*/
|
*/
|
||||||
const validateTokenHelper = async ({
|
export const validateTokenHelper = async ({
|
||||||
type,
|
type,
|
||||||
email,
|
email,
|
||||||
phoneNumber,
|
phoneNumber,
|
||||||
@@ -212,6 +212,4 @@ const validateTokenHelper = async ({
|
|||||||
|
|
||||||
// case: token is valid
|
// case: token is valid
|
||||||
await TokenData.findByIdAndDelete(tokenData._id);
|
await TokenData.findByIdAndDelete(tokenData._id);
|
||||||
};
|
};
|
||||||
|
|
||||||
export { createTokenHelper, validateTokenHelper };
|
|
||||||
@@ -1,4 +1,3 @@
|
|||||||
import * as Sentry from '@sentry/node';
|
|
||||||
import {
|
import {
|
||||||
Workspace,
|
Workspace,
|
||||||
Bot,
|
Bot,
|
||||||
@@ -16,38 +15,31 @@ import { SecretService } from '../services';
|
|||||||
* @param {String} organizationId - id of organization to create workspace in
|
* @param {String} organizationId - id of organization to create workspace in
|
||||||
* @param {Object} workspace - new workspace
|
* @param {Object} workspace - new workspace
|
||||||
*/
|
*/
|
||||||
const createWorkspace = async ({
|
export const createWorkspace = async ({
|
||||||
name,
|
name,
|
||||||
organizationId
|
organizationId
|
||||||
}: {
|
}: {
|
||||||
name: string;
|
name: string;
|
||||||
organizationId: string;
|
organizationId: string;
|
||||||
}) => {
|
}) => {
|
||||||
let workspace;
|
// create workspace
|
||||||
try {
|
const workspace = await new Workspace({
|
||||||
// create workspace
|
name,
|
||||||
workspace = await new Workspace({
|
organization: organizationId,
|
||||||
name,
|
autoCapitalization: true
|
||||||
organization: organizationId,
|
}).save();
|
||||||
autoCapitalization: true
|
|
||||||
}).save();
|
// initialize bot for workspace
|
||||||
|
await createBot({
|
||||||
// initialize bot for workspace
|
name: 'Infisical Bot',
|
||||||
await createBot({
|
workspaceId: workspace._id
|
||||||
name: 'Infisical Bot',
|
});
|
||||||
workspaceId: workspace._id
|
|
||||||
});
|
// initialize blind index salt for workspace
|
||||||
|
await SecretService.createSecretBlindIndexData({
|
||||||
// initialize blind index salt for workspace
|
workspaceId: workspace._id
|
||||||
await SecretService.createSecretBlindIndexData({
|
});
|
||||||
workspaceId: workspace._id
|
|
||||||
});
|
|
||||||
|
|
||||||
} catch (err) {
|
|
||||||
Sentry.setUser(null);
|
|
||||||
Sentry.captureException(err);
|
|
||||||
throw new Error('Failed to create workspace');
|
|
||||||
}
|
|
||||||
|
|
||||||
return workspace;
|
return workspace;
|
||||||
};
|
};
|
||||||
@@ -58,29 +50,18 @@ const createWorkspace = async ({
|
|||||||
* @param {Object} obj
|
* @param {Object} obj
|
||||||
* @param {String} obj.id - id of workspace to delete
|
* @param {String} obj.id - id of workspace to delete
|
||||||
*/
|
*/
|
||||||
const deleteWorkspace = async ({ id }: { id: string }) => {
|
export const deleteWorkspace = async ({ id }: { id: string }) => {
|
||||||
try {
|
await Workspace.deleteOne({ _id: id });
|
||||||
await Workspace.deleteOne({ _id: id });
|
await Bot.deleteOne({
|
||||||
await Bot.deleteOne({
|
workspace: id
|
||||||
workspace: id
|
});
|
||||||
});
|
await Membership.deleteMany({
|
||||||
await Membership.deleteMany({
|
workspace: id
|
||||||
workspace: id
|
});
|
||||||
});
|
await Secret.deleteMany({
|
||||||
await Secret.deleteMany({
|
workspace: id
|
||||||
workspace: id
|
});
|
||||||
});
|
await Key.deleteMany({
|
||||||
await Key.deleteMany({
|
workspace: id
|
||||||
workspace: id
|
});
|
||||||
});
|
|
||||||
} catch (err) {
|
|
||||||
Sentry.setUser(null);
|
|
||||||
Sentry.captureException(err);
|
|
||||||
throw new Error('Failed to delete workspace');
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
export {
|
|
||||||
createWorkspace,
|
|
||||||
deleteWorkspace
|
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
import dotenv from "dotenv";
|
import dotenv from "dotenv";
|
||||||
dotenv.config();
|
dotenv.config();
|
||||||
import express from "express";
|
import express from "express";
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
||||||
|
require('express-async-errors');
|
||||||
import helmet from "helmet";
|
import helmet from "helmet";
|
||||||
import cors from "cors";
|
import cors from "cors";
|
||||||
import { DatabaseService } from "./services";
|
import { DatabaseService } from "./services";
|
||||||
@@ -11,7 +13,6 @@ import swaggerUi = require("swagger-ui-express");
|
|||||||
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
||||||
const swaggerFile = require("../spec.json");
|
const swaggerFile = require("../spec.json");
|
||||||
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
||||||
const requestIp = require("request-ip");
|
|
||||||
import { apiLimiter } from "./helpers/rateLimiter";
|
import { apiLimiter } from "./helpers/rateLimiter";
|
||||||
import {
|
import {
|
||||||
workspace as eeWorkspaceRouter,
|
workspace as eeWorkspaceRouter,
|
||||||
@@ -84,8 +85,6 @@ const main = async () => {
|
|||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
app.use(requestIp.mw());
|
|
||||||
|
|
||||||
if ((await getNodeEnv()) === "production") {
|
if ((await getNodeEnv()) === "production") {
|
||||||
// enable app-wide rate-limiting + helmet security
|
// enable app-wide rate-limiting + helmet security
|
||||||
// in production
|
// in production
|
||||||
@@ -94,6 +93,13 @@ const main = async () => {
|
|||||||
app.use(helmet());
|
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
|
// (EE) routes
|
||||||
app.use("/api/v1/secret", eeSecretRouter);
|
app.use("/api/v1/secret", eeSecretRouter);
|
||||||
app.use("/api/v1/secret-snapshot", eeSecretSnapshotRouter);
|
app.use("/api/v1/secret-snapshot", eeSecretSnapshotRouter);
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -1,3 +1,4 @@
|
|||||||
|
import { Types } from 'mongoose';
|
||||||
import {
|
import {
|
||||||
IUser,
|
IUser,
|
||||||
IServiceAccount,
|
IServiceAccount,
|
||||||
@@ -10,4 +11,5 @@ export interface AuthData {
|
|||||||
authChannel: string;
|
authChannel: string;
|
||||||
authIP: string;
|
authIP: string;
|
||||||
authUserAgent: string;
|
authUserAgent: string;
|
||||||
|
tokenVersionId?: Types.ObjectId;
|
||||||
}
|
}
|
||||||
@@ -71,10 +71,12 @@ const requireAuth = ({
|
|||||||
req.user = authPayload;
|
req.user = authPayload;
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
authPayload = await getAuthUserPayload({
|
const { user, tokenVersionId } = await getAuthUserPayload({
|
||||||
authTokenValue
|
authTokenValue
|
||||||
});
|
});
|
||||||
req.user = authPayload;
|
authPayload = user;
|
||||||
|
req.user = user;
|
||||||
|
req.tokenVersionId = tokenVersionId;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -88,8 +90,9 @@ const requireAuth = ({
|
|||||||
authMode,
|
authMode,
|
||||||
authPayload, // User, ServiceAccount, ServiceTokenData
|
authPayload, // User, ServiceAccount, ServiceTokenData
|
||||||
authChannel: getChannelFromUserAgent(req.headers['user-agent']),
|
authChannel: getChannelFromUserAgent(req.headers['user-agent']),
|
||||||
authIP: req.ip,
|
authIP: req.realIP,
|
||||||
authUserAgent: req.headers['user-agent'] ?? 'other'
|
authUserAgent: req.headers['user-agent'] ?? 'other',
|
||||||
|
tokenVersionId: req.tokenVersionId
|
||||||
}
|
}
|
||||||
|
|
||||||
return next();
|
return next();
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ import Workspace, { IWorkspace } from './workspace';
|
|||||||
import ServiceTokenData, { IServiceTokenData } from './serviceTokenData';
|
import ServiceTokenData, { IServiceTokenData } from './serviceTokenData';
|
||||||
import APIKeyData, { IAPIKeyData } from './apiKeyData';
|
import APIKeyData, { IAPIKeyData } from './apiKeyData';
|
||||||
import LoginSRPDetail, { ILoginSRPDetail } from './loginSRPDetail';
|
import LoginSRPDetail, { ILoginSRPDetail } from './loginSRPDetail';
|
||||||
|
import TokenVersion, { ITokenVersion } from './tokenVersion';
|
||||||
|
|
||||||
export {
|
export {
|
||||||
AuthProvider,
|
AuthProvider,
|
||||||
@@ -72,5 +73,7 @@ export {
|
|||||||
APIKeyData,
|
APIKeyData,
|
||||||
IAPIKeyData,
|
IAPIKeyData,
|
||||||
LoginSRPDetail,
|
LoginSRPDetail,
|
||||||
ILoginSRPDetail
|
ILoginSRPDetail,
|
||||||
|
TokenVersion,
|
||||||
|
ITokenVersion
|
||||||
};
|
};
|
||||||
|
|||||||
47
backend/src/models/tokenVersion.ts
Normal file
47
backend/src/models/tokenVersion.ts
Normal 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;
|
||||||
@@ -21,7 +21,6 @@ export interface IUser extends Document {
|
|||||||
tag?: string;
|
tag?: string;
|
||||||
salt?: string;
|
salt?: string;
|
||||||
verifier?: string;
|
verifier?: string;
|
||||||
refreshVersion?: number;
|
|
||||||
isMfaEnabled: boolean;
|
isMfaEnabled: boolean;
|
||||||
mfaMethods: boolean;
|
mfaMethods: boolean;
|
||||||
devices: {
|
devices: {
|
||||||
@@ -91,11 +90,6 @@ const userSchema = new Schema<IUser>(
|
|||||||
type: String,
|
type: String,
|
||||||
select: false
|
select: false
|
||||||
},
|
},
|
||||||
refreshVersion: {
|
|
||||||
type: Number,
|
|
||||||
default: 0,
|
|
||||||
select: false
|
|
||||||
},
|
|
||||||
isMfaEnabled: {
|
isMfaEnabled: {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
default: false
|
default: false
|
||||||
@@ -108,7 +102,8 @@ const userSchema = new Schema<IUser>(
|
|||||||
ip: String,
|
ip: String,
|
||||||
userAgent: String
|
userAgent: String
|
||||||
}],
|
}],
|
||||||
default: []
|
default: [],
|
||||||
|
select: false
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -44,8 +44,6 @@ router.post(
|
|||||||
authController.checkAuth
|
authController.checkAuth
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
router.get(
|
router.get(
|
||||||
'/redirect/google',
|
'/redirect/google',
|
||||||
authLimiter,
|
authLimiter,
|
||||||
@@ -53,12 +51,27 @@ router.get(
|
|||||||
scope: ['profile', 'email'],
|
scope: ['profile', 'email'],
|
||||||
session: false,
|
session: false,
|
||||||
}),
|
}),
|
||||||
)
|
);
|
||||||
|
|
||||||
router.get(
|
router.get(
|
||||||
'/callback/google',
|
'/callback/google',
|
||||||
passport.authenticate('google', { failureRedirect: '/login/provider/error', session: false }),
|
passport.authenticate('google', { failureRedirect: '/login/provider/error', session: false }),
|
||||||
authController.handleAuthProviderCallback,
|
authController.handleAuthProviderCallback,
|
||||||
)
|
);
|
||||||
|
|
||||||
|
router.get(
|
||||||
|
'/common-passwords',
|
||||||
|
authLimiter,
|
||||||
|
authController.getCommonPasswords
|
||||||
|
);
|
||||||
|
|
||||||
|
router.delete(
|
||||||
|
'/sessions',
|
||||||
|
authLimiter,
|
||||||
|
requireAuth({
|
||||||
|
acceptedAuthModes: [AUTH_MODE_JWT]
|
||||||
|
}),
|
||||||
|
authController.revokeAllSessions
|
||||||
|
);
|
||||||
|
|
||||||
export default router;
|
export default router;
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ router.post(
|
|||||||
router.post(
|
router.post(
|
||||||
'/mfa/send',
|
'/mfa/send',
|
||||||
authLimiter,
|
authLimiter,
|
||||||
body('email').isString().trim().notEmpty(),
|
body('email').isString().trim().notEmpty().isEmail(),
|
||||||
validateRequest,
|
validateRequest,
|
||||||
authController.sendMfaToken
|
authController.sendMfaToken
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { Types } from 'mongoose';
|
import { Types } from 'mongoose';
|
||||||
import {
|
import {
|
||||||
getSecretsHelper,
|
getSecretsBotHelper,
|
||||||
encryptSymmetricHelper,
|
encryptSymmetricHelper,
|
||||||
decryptSymmetricHelper
|
decryptSymmetricHelper
|
||||||
} from '../helpers/bot';
|
} from '../helpers/bot';
|
||||||
@@ -25,7 +25,7 @@ class BotService {
|
|||||||
workspaceId: Types.ObjectId;
|
workspaceId: Types.ObjectId;
|
||||||
environment: string;
|
environment: string;
|
||||||
}) {
|
}) {
|
||||||
return await getSecretsHelper({
|
return await getSecretsBotHelper({
|
||||||
workspaceId,
|
workspaceId,
|
||||||
environment
|
environment
|
||||||
});
|
});
|
||||||
|
|||||||
3
backend/src/types/express/index.d.ts
vendored
3
backend/src/types/express/index.d.ts
vendored
@@ -1,4 +1,5 @@
|
|||||||
import * as express from 'express';
|
import * as express from 'express';
|
||||||
|
import { Types } from 'mongoose';
|
||||||
import {
|
import {
|
||||||
IUser,
|
IUser,
|
||||||
IServiceAccount,
|
IServiceAccount,
|
||||||
@@ -39,7 +40,9 @@ declare global {
|
|||||||
serviceTokenData: any;
|
serviceTokenData: any;
|
||||||
apiKeyData: any;
|
apiKeyData: any;
|
||||||
query?: any;
|
query?: any;
|
||||||
|
tokenVersionId?: Types.ObjectId;
|
||||||
authData: AuthData;
|
authData: AuthData;
|
||||||
|
realIP: string;
|
||||||
requestData: {
|
requestData: {
|
||||||
[key: string]: string
|
[key: string]: string
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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
|
|
||||||
};
|
|
||||||
@@ -5,7 +5,6 @@ import { EELicenseService } from '../../ee/services';
|
|||||||
import { initSmtp } from '../../services/smtp';
|
import { initSmtp } from '../../services/smtp';
|
||||||
import { createTestUserForDevelopment } from '../addDevelopmentUser';
|
import { createTestUserForDevelopment } from '../addDevelopmentUser';
|
||||||
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
||||||
const { patchRouterParam } = require('../patchAsyncRoutes');
|
|
||||||
import { validateEncryptionKeysConfig } from './validateConfig';
|
import { validateEncryptionKeysConfig } from './validateConfig';
|
||||||
import {
|
import {
|
||||||
backfillSecretVersions,
|
backfillSecretVersions,
|
||||||
@@ -38,7 +37,6 @@ import { initializePassport } from '../auth';
|
|||||||
* - Re-encrypting data
|
* - Re-encrypting data
|
||||||
*/
|
*/
|
||||||
export const setup = async () => {
|
export const setup = async () => {
|
||||||
patchRouterParam();
|
|
||||||
await validateEncryptionKeysConfig();
|
await validateEncryptionKeysConfig();
|
||||||
await TelemetryService.logTelemetryMessage();
|
await TelemetryService.logTelemetryMessage();
|
||||||
|
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
export * from './user';
|
||||||
export * from './workspace';
|
export * from './workspace';
|
||||||
export * from './bot';
|
export * from './bot';
|
||||||
export * from './integration';
|
export * from './integration';
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
import fs from 'fs';
|
||||||
|
import path from 'path';
|
||||||
import { Types } from 'mongoose';
|
import { Types } from 'mongoose';
|
||||||
import {
|
import {
|
||||||
IUser,
|
IUser,
|
||||||
@@ -8,7 +10,7 @@ import {
|
|||||||
} from '../models';
|
} from '../models';
|
||||||
import { validateMembership } from '../helpers/membership';
|
import { validateMembership } from '../helpers/membership';
|
||||||
import _ from 'lodash';
|
import _ from 'lodash';
|
||||||
import { BadRequestError, UnauthorizedRequestError } from '../utils/errors';
|
import { BadRequestError, UnauthorizedRequestError, ValidationError } from '../utils/errors';
|
||||||
import {
|
import {
|
||||||
validateMembershipOrg
|
validateMembershipOrg
|
||||||
} from '../helpers/membershipOrg';
|
} from '../helpers/membershipOrg';
|
||||||
@@ -17,6 +19,22 @@ import {
|
|||||||
PERMISSION_WRITE_SECRETS
|
PERMISSION_WRITE_SECRETS
|
||||||
} from '../variables';
|
} 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
|
* Validate that user (client) can access workspace
|
||||||
* with id [workspaceId] and its environment [environment] with required permissions
|
* with id [workspaceId] and its environment [environment] with required permissions
|
||||||
|
|||||||
@@ -121,12 +121,6 @@
|
|||||||
{
|
{
|
||||||
"group": "Self-host Infisical",
|
"group": "Self-host Infisical",
|
||||||
"pages": [
|
"pages": [
|
||||||
{
|
|
||||||
"group": "Authentication",
|
|
||||||
"pages": [
|
|
||||||
"self-hosting/authentication/google"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"group": "Deployment options",
|
"group": "Deployment options",
|
||||||
"pages": [
|
"pages": [
|
||||||
|
|||||||
@@ -3,17 +3,16 @@ title: "Configure email service"
|
|||||||
description: "How to configure your email when self-hosting Infisical."
|
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.
|
- Multi-factor authentication
|
||||||
- Sending invite links for projects.
|
- Sending invite links via email for projects to teammates
|
||||||
- Sending alerts.
|
- Sending alerts such as suspicious login attempts
|
||||||
|
|
||||||
We strongly recommend using an email service to act as your email server and provide examples for common providers.
|
|
||||||
|
|
||||||
## General configuration
|
## 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_HOST`: Hostname to connect to for establishing SMTP connections.
|
||||||
- `SMTP_USERNAME`: Credential to connect to host (e.g. team@infisical.com)
|
- `SMTP_USERNAME`: Credential to connect to host (e.g. team@infisical.com)
|
||||||
|
|||||||
@@ -2,18 +2,19 @@ import crypto from 'crypto';
|
|||||||
|
|
||||||
import React, { useState } from 'react';
|
import React, { useState } from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
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 { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||||
import jsrp from 'jsrp';
|
import jsrp from 'jsrp';
|
||||||
import nacl from 'tweetnacl';
|
import nacl from 'tweetnacl';
|
||||||
import { encodeBase64 } from 'tweetnacl-util';
|
import { encodeBase64 } from 'tweetnacl-util';
|
||||||
|
|
||||||
|
import { useGetCommonPasswords } from '@app/hooks/api';
|
||||||
import completeAccountInformationSignup from '@app/pages/api/auth/CompleteAccountInformationSignup';
|
import completeAccountInformationSignup from '@app/pages/api/auth/CompleteAccountInformationSignup';
|
||||||
import getOrganizations from '@app/pages/api/organization/getOrgs';
|
import getOrganizations from '@app/pages/api/organization/getOrgs';
|
||||||
import ProjectService from '@app/services/ProjectService';
|
import ProjectService from '@app/services/ProjectService';
|
||||||
|
|
||||||
import InputField from '../basic/InputField';
|
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 Aes256Gcm from '../utilities/cryptography/aes-256-gcm';
|
||||||
import { deriveArgonKey } from '../utilities/cryptography/crypto';
|
import { deriveArgonKey } from '../utilities/cryptography/crypto';
|
||||||
import { saveTokenToLocalStorage } from '../utilities/saveTokenToLocalStorage';
|
import { saveTokenToLocalStorage } from '../utilities/saveTokenToLocalStorage';
|
||||||
@@ -37,6 +38,15 @@ interface UserInfoStepProps {
|
|||||||
providerAuthToken?: string;
|
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
|
* This is the step of the sign up flow where people provife their name/surname and password
|
||||||
* @param {object} obj
|
* @param {object} obj
|
||||||
@@ -63,11 +73,11 @@ export default function UserInfoStep({
|
|||||||
setAttributionSource,
|
setAttributionSource,
|
||||||
providerAuthToken,
|
providerAuthToken,
|
||||||
}: UserInfoStepProps): JSX.Element {
|
}: UserInfoStepProps): JSX.Element {
|
||||||
|
const { data: commonPasswords } = useGetCommonPasswords();
|
||||||
const [nameError, setNameError] = useState(false);
|
const [nameError, setNameError] = useState(false);
|
||||||
const [organizationNameError, setOrganizationNameError] = useState(false);
|
const [organizationNameError, setOrganizationNameError] = useState(false);
|
||||||
const [passwordErrorLength, setPasswordErrorLength] = useState(false);
|
|
||||||
const [passwordErrorNumber, setPasswordErrorNumber] = useState(false);
|
const [errors, setErrors] = useState<Errors>({});
|
||||||
const [passwordErrorLowerCase, setPasswordErrorLowerCase] = useState(false);
|
|
||||||
|
|
||||||
const [isLoading, setIsLoading] = useState(false);
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
@@ -89,12 +99,11 @@ export default function UserInfoStep({
|
|||||||
} else {
|
} else {
|
||||||
setOrganizationNameError(false);
|
setOrganizationNameError(false);
|
||||||
}
|
}
|
||||||
errorCheck = passwordCheck({
|
|
||||||
|
errorCheck = checkPassword({
|
||||||
password,
|
password,
|
||||||
setPasswordErrorLength,
|
commonPasswords,
|
||||||
setPasswordErrorNumber,
|
setErrors
|
||||||
setPasswordErrorLowerCase,
|
|
||||||
errorCheck
|
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!errorCheck) {
|
if (!errorCheck) {
|
||||||
@@ -248,59 +257,45 @@ export default function UserInfoStep({
|
|||||||
label={t('section.password.password')}
|
label={t('section.password.password')}
|
||||||
onChangeHandler={(pass: string) => {
|
onChangeHandler={(pass: string) => {
|
||||||
setPassword(pass);
|
setPassword(pass);
|
||||||
passwordCheck({
|
checkPassword({
|
||||||
password: pass,
|
password: pass,
|
||||||
setPasswordErrorLength,
|
commonPasswords,
|
||||||
setPasswordErrorNumber,
|
setErrors
|
||||||
setPasswordErrorLowerCase,
|
|
||||||
errorCheck: false
|
|
||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
type="password"
|
type="password"
|
||||||
value={password}
|
value={password}
|
||||||
isRequired
|
isRequired
|
||||||
error={passwordErrorLength && passwordErrorNumber && passwordErrorLowerCase}
|
error={Object.keys(errors).length > 0}
|
||||||
autoComplete="new-password"
|
autoComplete="new-password"
|
||||||
id="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="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="mb-2 text-sm text-gray-400">{t('section.password.validate-base')}</div>
|
||||||
<div className="ml-1 flex flex-row items-center justify-start">
|
{Object.keys(errors).map((key) => {
|
||||||
{passwordErrorLength ? (
|
if (errors[key as keyof Errors]) {
|
||||||
<FontAwesomeIcon icon={faXmark} className="text-md text-red ml-0.5 mr-2.5" />
|
return (
|
||||||
) : (
|
<div
|
||||||
<FontAwesomeIcon icon={faCheck} className="text-md mr-2 text-primary" />
|
className="ml-1 flex flex-row items-top justify-start"
|
||||||
)}
|
key={key}
|
||||||
<div className={`${passwordErrorLength ? 'text-gray-400' : 'text-gray-600'} text-sm`}>
|
>
|
||||||
{t('section.password.validate-length')}
|
<div>
|
||||||
</div>
|
<FontAwesomeIcon
|
||||||
</div>
|
icon={faXmark}
|
||||||
<div className="ml-1 flex flex-row items-center justify-start">
|
className="text-md text-red ml-0.5 mr-2.5"
|
||||||
{passwordErrorLowerCase ? (
|
/>
|
||||||
<FontAwesomeIcon icon={faXmark} className="text-md text-red ml-0.5 mr-2.5" />
|
</div>
|
||||||
) : (
|
<p className="text-gray-400 text-sm">
|
||||||
<FontAwesomeIcon icon={faCheck} className="text-md mr-2 text-primary" />
|
{errors[key as keyof Errors]}
|
||||||
)}
|
</p>
|
||||||
<div
|
</div>
|
||||||
className={`${passwordErrorLowerCase ? 'text-gray-400' : 'text-gray-600'} text-sm`}
|
);
|
||||||
>
|
}
|
||||||
{t('section.password.validate-case')}
|
|
||||||
</div>
|
return null;
|
||||||
</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')}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
) : (
|
|
||||||
<div className="py-2" />
|
|
||||||
)}
|
)}
|
||||||
</div>
|
</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">
|
<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,
|
setPasswordErrorLowerCase,
|
||||||
errorCheck
|
errorCheck
|
||||||
}: PasswordCheckProps) => {
|
}: PasswordCheckProps) => {
|
||||||
|
|
||||||
if (!password || password.length < 14) {
|
if (!password || password.length < 14) {
|
||||||
setPasswordErrorLength(true);
|
setPasswordErrorLength(true);
|
||||||
errorCheck = true;
|
errorCheck = true;
|
||||||
|
|||||||
72
frontend/src/components/utilities/checks/checkPassword.ts
Normal file
72
frontend/src/components/utilities/checks/checkPassword.ts
Normal file
@@ -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);
|
setPasswordChanged(true);
|
||||||
setCurrentPassword('');
|
setCurrentPassword('');
|
||||||
setNewPassword('');
|
setNewPassword('');
|
||||||
|
|
||||||
|
window.location.href = '/login';
|
||||||
|
|
||||||
|
// move to login page
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
setCurrentPasswordError(true);
|
setCurrentPasswordError(true);
|
||||||
console.log(error);
|
console.log(error);
|
||||||
|
|||||||
@@ -73,10 +73,15 @@ export const DeleteActionModal = ({
|
|||||||
}
|
}
|
||||||
onClose={onClose}
|
onClose={onClose}
|
||||||
>
|
>
|
||||||
<form>
|
<form
|
||||||
|
onSubmit={(evt) => {
|
||||||
|
evt.preventDefault();
|
||||||
|
if (deleteKey === inputData) onDelete();
|
||||||
|
}}
|
||||||
|
>
|
||||||
<FormControl
|
<FormControl
|
||||||
label={
|
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
|
Type <span className="font-bold">{deleteKey}</span> to delete the resource
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,7 @@
|
|||||||
export {
|
export {
|
||||||
useGetAuthToken,
|
useGetAuthToken,
|
||||||
useSendMfaToken,
|
useSendMfaToken,
|
||||||
useVerifyMfaToken} from './queries'
|
useVerifyMfaToken,
|
||||||
|
useRevokeAllSessions,
|
||||||
|
useGetCommonPasswords
|
||||||
|
} from './queries'
|
||||||
|
|||||||
@@ -10,7 +10,8 @@ import {
|
|||||||
VerifyMfaTokenRes} from './types';
|
VerifyMfaTokenRes} from './types';
|
||||||
|
|
||||||
const authKeys = {
|
const authKeys = {
|
||||||
getAuthToken: ['token'] as const
|
getAuthToken: ['token'] as const,
|
||||||
|
commonPasswords: ['common-passwords'] as const
|
||||||
};
|
};
|
||||||
|
|
||||||
export const useSendMfaToken = () => {
|
export const useSendMfaToken = () => {
|
||||||
@@ -49,3 +50,20 @@ export const useGetAuthToken = () =>
|
|||||||
onSuccess: (data) => setAuthToken(data.token),
|
onSuccess: (data) => setAuthToken(data.token),
|
||||||
retry: 0
|
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 });
|
||||||
@@ -2,7 +2,7 @@ import { useEffect, useState } from 'react';
|
|||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import Head from 'next/head';
|
import Head from 'next/head';
|
||||||
import { useRouter } from 'next/router';
|
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 { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||||
|
|
||||||
import Button from '@app/components/basic/buttons/Button';
|
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 ListBox from '@app/components/basic/Listbox';
|
||||||
import ApiKeyTable from '@app/components/basic/table/ApiKeyTable';
|
import ApiKeyTable from '@app/components/basic/table/ApiKeyTable';
|
||||||
import NavHeader from '@app/components/navigation/NavHeader';
|
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 changePassword from '@app/components/utilities/cryptography/changePassword';
|
||||||
import issueBackupKey from '@app/components/utilities/cryptography/issueBackupKey';
|
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 { SecuritySection } from '@app/views/Settings/PersonalSettingsPage/SecuritySection/SecuritySection';
|
||||||
|
|
||||||
import AddApiKeyDialog from '../../../components/basic/dialog/AddApiKeyDialog';
|
import AddApiKeyDialog from '../../../components/basic/dialog/AddApiKeyDialog';
|
||||||
import getAPIKeys from '../../api/apiKey/getAPIKeys';
|
import getAPIKeys from '../../api/apiKey/getAPIKeys';
|
||||||
import getUser from '../../api/user/getUser';
|
import getUser from '../../api/user/getUser';
|
||||||
|
|
||||||
|
type Errors = {
|
||||||
|
length?: string,
|
||||||
|
upperCase?: string,
|
||||||
|
lowerCase?: string,
|
||||||
|
number?: string,
|
||||||
|
specialChar?: string,
|
||||||
|
repeatedChar?: string,
|
||||||
|
};
|
||||||
|
|
||||||
export default function PersonalSettings() {
|
export default function PersonalSettings() {
|
||||||
|
const { data: commonPasswords } = useGetCommonPasswords();
|
||||||
const [personalEmail, setPersonalEmail] = useState('');
|
const [personalEmail, setPersonalEmail] = useState('');
|
||||||
const [personalName, setPersonalName] = 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 [currentPasswordError, setCurrentPasswordError] = useState(false);
|
||||||
const [currentPassword, setCurrentPassword] = useState('');
|
const [currentPassword, setCurrentPassword] = useState('');
|
||||||
const [newPassword, setNewPassword] = useState('');
|
const [newPassword, setNewPassword] = useState('');
|
||||||
@@ -34,6 +44,9 @@ export default function PersonalSettings() {
|
|||||||
const [backupKeyError, setBackupKeyError] = useState(false);
|
const [backupKeyError, setBackupKeyError] = useState(false);
|
||||||
const [isAddApiKeyDialogOpen, setIsAddApiKeyDialogOpen] = useState(false);
|
const [isAddApiKeyDialogOpen, setIsAddApiKeyDialogOpen] = useState(false);
|
||||||
const [apiKeys, setApiKeys] = useState<any[]>([]);
|
const [apiKeys, setApiKeys] = useState<any[]>([]);
|
||||||
|
const [errors, setErrors] = useState<Errors>({});
|
||||||
|
|
||||||
|
const revokeAllSessions = useRevokeAllSessions();
|
||||||
|
|
||||||
const { t, i18n } = useTranslation();
|
const { t, i18n } = useTranslation();
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
@@ -154,78 +167,54 @@ export default function PersonalSettings() {
|
|||||||
label={t('section.password.new') as string}
|
label={t('section.password.new') as string}
|
||||||
onChangeHandler={(password) => {
|
onChangeHandler={(password) => {
|
||||||
setNewPassword(password);
|
setNewPassword(password);
|
||||||
passwordCheck({
|
checkPassword({
|
||||||
password,
|
password,
|
||||||
setPasswordErrorLength,
|
commonPasswords,
|
||||||
setPasswordErrorNumber,
|
setErrors
|
||||||
setPasswordErrorLowerCase,
|
|
||||||
errorCheck: false
|
|
||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
type="password"
|
type="password"
|
||||||
value={newPassword}
|
value={newPassword}
|
||||||
isRequired
|
isRequired
|
||||||
error={passwordErrorLength && passwordErrorLowerCase && passwordErrorNumber}
|
error={Object.keys(errors).length > 0}
|
||||||
autoComplete="new-password"
|
autoComplete="new-password"
|
||||||
id="new-password"
|
id="new-password"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
{passwordErrorLength || passwordErrorLowerCase || passwordErrorNumber ? (
|
{Object.keys(errors).length > 0 && (
|
||||||
<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="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">
|
<div className="mb-2 text-sm text-gray-400">{t('section.password.validate-base')}</div>
|
||||||
{t('section.password.validate-base')}
|
{Object.keys(errors).map((key) => {
|
||||||
</div>
|
if (errors[key as keyof Errors]) {
|
||||||
<div className="ml-1 flex flex-row items-center justify-start">
|
return (
|
||||||
{passwordErrorLength ? (
|
<div className="ml-1 flex flex-row items-top justify-start" key={key}>
|
||||||
<FontAwesomeIcon icon={faX} className="text-md mr-2.5 text-red" />
|
<div>
|
||||||
) : (
|
<FontAwesomeIcon
|
||||||
<FontAwesomeIcon icon={faCheck} className="text-md mr-2 text-primary" />
|
icon={faXmark}
|
||||||
)}
|
className="text-md text-red ml-0.5 mr-2.5"
|
||||||
<div
|
/>
|
||||||
className={`${
|
</div>
|
||||||
passwordErrorLength ? 'text-gray-400' : 'text-gray-600'
|
<p className="text-gray-400 text-sm">
|
||||||
} text-sm`}
|
{errors[key as keyof Errors]}
|
||||||
>
|
</p>
|
||||||
{t('section.password.validate-length')}
|
</div>
|
||||||
</div>
|
);
|
||||||
</div>
|
}
|
||||||
<div className="ml-1 flex flex-row items-center justify-start">
|
|
||||||
{passwordErrorLowerCase ? (
|
return null;
|
||||||
<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>
|
||||||
) : (
|
|
||||||
<div className="py-2" />
|
|
||||||
)}
|
)}
|
||||||
<div className="mt-3 flex w-52 flex-row items-center pr-3">
|
<div className="mt-3 flex w-52 flex-row items-center pr-3">
|
||||||
<Button
|
<Button
|
||||||
text={t('section.password.change') as string}
|
text={t('section.password.change') as string}
|
||||||
onButtonPressed={() => {
|
onButtonPressed={() => {
|
||||||
if (!passwordErrorLength && !passwordErrorLowerCase && !passwordErrorNumber) {
|
const errorCheck = checkPassword({
|
||||||
|
password: newPassword,
|
||||||
|
commonPasswords,
|
||||||
|
setErrors
|
||||||
|
});
|
||||||
|
if (!errorCheck) {
|
||||||
changePassword(
|
changePassword(
|
||||||
personalEmail,
|
personalEmail,
|
||||||
currentPassword,
|
currentPassword,
|
||||||
@@ -237,13 +226,9 @@ export default function PersonalSettings() {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
|
active={Object.keys(errors).length === 0 && currentPassword !== ''}
|
||||||
color="mineshaft"
|
color="mineshaft"
|
||||||
size="md"
|
size="md"
|
||||||
active={
|
|
||||||
newPassword !== '' &&
|
|
||||||
currentPassword !== '' &&
|
|
||||||
!(passwordErrorLength || passwordErrorLowerCase || passwordErrorNumber)
|
|
||||||
}
|
|
||||||
textDisabled={t('section.password.change') as string}
|
textDisabled={t('section.password.change') as string}
|
||||||
/>
|
/>
|
||||||
<FontAwesomeIcon
|
<FontAwesomeIcon
|
||||||
@@ -254,6 +239,28 @@ export default function PersonalSettings() {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</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="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">
|
<div className="flex w-full max-w-5xl flex-row items-center justify-between">
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import Head from 'next/head';
|
|||||||
import Image from 'next/image';
|
import Image from 'next/image';
|
||||||
import Link from 'next/link';
|
import Link from 'next/link';
|
||||||
import { useRouter } from 'next/router';
|
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 { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||||
import jsrp from 'jsrp';
|
import jsrp from 'jsrp';
|
||||||
import queryString from 'query-string';
|
import queryString from 'query-string';
|
||||||
@@ -17,7 +17,9 @@ import { encodeBase64 } from 'tweetnacl-util';
|
|||||||
import Button from '@app/components/basic/buttons/Button';
|
import Button from '@app/components/basic/buttons/Button';
|
||||||
import InputField from '@app/components/basic/InputField';
|
import InputField from '@app/components/basic/InputField';
|
||||||
import attemptLogin from '@app/components/utilities/attemptLogin';
|
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 Aes256Gcm from '@app/components/utilities/cryptography/aes-256-gcm';
|
||||||
import { deriveArgonKey } from '@app/components/utilities/cryptography/crypto';
|
import { deriveArgonKey } from '@app/components/utilities/cryptography/crypto';
|
||||||
import issueBackupKey from '@app/components/utilities/cryptography/issueBackupKey';
|
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 SecurityClient from '@app/components/utilities/SecurityClient';
|
||||||
import getOrganizations from '@app/pages/api/organization/getOrgs';
|
import getOrganizations from '@app/pages/api/organization/getOrgs';
|
||||||
import getOrganizationUserProjects from '@app/pages/api/organization/GetOrgUserProjects';
|
import getOrganizationUserProjects from '@app/pages/api/organization/GetOrgUserProjects';
|
||||||
|
import {
|
||||||
|
useGetCommonPasswords
|
||||||
|
} from '@app/hooks/api';
|
||||||
import completeAccountInformationSignupInvite from './api/auth/CompleteAccountInformationSignupInvite';
|
import completeAccountInformationSignupInvite from './api/auth/CompleteAccountInformationSignupInvite';
|
||||||
import verifySignupInvite from './api/auth/VerifySignupInvite';
|
import verifySignupInvite from './api/auth/VerifySignupInvite';
|
||||||
|
|
||||||
// eslint-disable-next-line new-cap
|
// eslint-disable-next-line new-cap
|
||||||
const client = new jsrp.client();
|
const client = new jsrp.client();
|
||||||
|
|
||||||
|
type Errors = {
|
||||||
|
length?: string,
|
||||||
|
upperCase?: string,
|
||||||
|
lowerCase?: string,
|
||||||
|
number?: string,
|
||||||
|
specialChar?: string,
|
||||||
|
repeatedChar?: string,
|
||||||
|
};
|
||||||
|
|
||||||
export default function SignupInvite() {
|
export default function SignupInvite() {
|
||||||
|
const { data: commonPasswords } = useGetCommonPasswords();
|
||||||
const [password, setPassword] = useState('');
|
const [password, setPassword] = useState('');
|
||||||
const [firstName, setFirstName] = useState('');
|
const [firstName, setFirstName] = useState('');
|
||||||
const [lastName, setLastName] = useState('');
|
const [lastName, setLastName] = useState('');
|
||||||
const [firstNameError, setFirstNameError] = useState(false);
|
const [firstNameError, setFirstNameError] = useState(false);
|
||||||
const [lastNameError, setLastNameError] = 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 [isLoading, setIsLoading] = useState(false);
|
||||||
const [step, setStep] = useState(1);
|
const [step, setStep] = useState(1);
|
||||||
const [backupKeyError, setBackupKeyError] = useState(false);
|
const [backupKeyError, setBackupKeyError] = useState(false);
|
||||||
const [verificationToken, setVerificationToken] = useState('');
|
|
||||||
const [backupKeyIssued, setBackupKeyIssued] = useState(false);
|
const [backupKeyIssued, setBackupKeyIssued] = useState(false);
|
||||||
|
const [errors, setErrors] = useState<Errors>({});
|
||||||
|
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const parsedUrl = queryString.parse(router.asPath.split('?')[1]);
|
const parsedUrl = queryString.parse(router.asPath.split('?')[1]);
|
||||||
@@ -70,12 +80,11 @@ export default function SignupInvite() {
|
|||||||
} else {
|
} else {
|
||||||
setLastNameError(false);
|
setLastNameError(false);
|
||||||
}
|
}
|
||||||
errorCheck = passwordCheck({
|
|
||||||
|
errorCheck = checkPassword({
|
||||||
password,
|
password,
|
||||||
setPasswordErrorLength,
|
commonPasswords,
|
||||||
setPasswordErrorNumber,
|
setErrors
|
||||||
setPasswordErrorLowerCase,
|
|
||||||
errorCheck
|
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!errorCheck) {
|
if (!errorCheck) {
|
||||||
@@ -252,60 +261,43 @@ export default function SignupInvite() {
|
|||||||
label="Password"
|
label="Password"
|
||||||
onChangeHandler={(pass) => {
|
onChangeHandler={(pass) => {
|
||||||
setPassword(pass);
|
setPassword(pass);
|
||||||
passwordCheck({
|
checkPassword({
|
||||||
password: pass,
|
password: pass,
|
||||||
setPasswordErrorLength,
|
commonPasswords,
|
||||||
setPasswordErrorNumber,
|
setErrors
|
||||||
setPasswordErrorLowerCase,
|
|
||||||
errorCheck: false
|
|
||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
type="password"
|
type="password"
|
||||||
value={password}
|
value={password}
|
||||||
isRequired
|
isRequired
|
||||||
error={passwordErrorLength && passwordErrorNumber && passwordErrorLowerCase}
|
error={Object.keys(errors).length > 0}
|
||||||
autoComplete="new-password"
|
autoComplete="new-password"
|
||||||
id="new-password"
|
id="new-password"
|
||||||
/>
|
/>
|
||||||
{passwordErrorLength || passwordErrorLowerCase || passwordErrorNumber ? (
|
{Object.keys(errors).length > 0 && (
|
||||||
<div className="w-full mt-4 bg-white/5 px-2 flex flex-col items-start py-2 rounded-md">
|
<div className="mt-4 flex w-full flex-col items-start rounded-md bg-white/5 px-2 py-2">
|
||||||
<div className="text-gray-400 text-sm mb-1">Password should contain at least:</div>
|
<div className="mb-2 text-sm text-gray-400">Password should contain at least:</div>
|
||||||
<div className="flex flex-row justify-start items-center ml-1">
|
{Object.keys(errors).map((key) => {
|
||||||
{passwordErrorLength ? (
|
if (errors[key as keyof Errors]) {
|
||||||
<FontAwesomeIcon icon={faX} className="text-md text-red mr-2.5" />
|
return (
|
||||||
) : (
|
<div className="ml-1 flex flex-row items-top justify-start" key={key}>
|
||||||
<FontAwesomeIcon icon={faCheck} className="text-md text-primary mr-2" />
|
<div>
|
||||||
)}
|
<FontAwesomeIcon
|
||||||
<div className={`${passwordErrorLength ? 'text-gray-400' : 'text-gray-600'} text-sm`}>
|
icon={faXmark}
|
||||||
14 characters
|
className="text-md text-red ml-0.5 mr-2.5"
|
||||||
</div>
|
/>
|
||||||
|
</div>
|
||||||
|
<p className="text-gray-400 text-sm">
|
||||||
|
{errors[key as keyof Errors]}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
})}
|
||||||
</div>
|
</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
|
|
||||||
</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>
|
||||||
<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">
|
<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">
|
||||||
<Button
|
<Button
|
||||||
|
|||||||
Reference in New Issue
Block a user