mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-10-06 03:26:46 +00:00
feat: remove try-catch blocks for handling errors in middleware
This commit is contained in:
@@ -1,4 +1,3 @@
|
|||||||
import * as Sentry from '@sentry/node';
|
|
||||||
import { Types } from 'mongoose';
|
import { Types } from 'mongoose';
|
||||||
import { Action } from '../models';
|
import { Action } from '../models';
|
||||||
import {
|
import {
|
||||||
@@ -36,33 +35,25 @@ const createActionUpdateSecret = async ({
|
|||||||
workspaceId: Types.ObjectId;
|
workspaceId: Types.ObjectId;
|
||||||
secretIds: Types.ObjectId[];
|
secretIds: Types.ObjectId[];
|
||||||
}) => {
|
}) => {
|
||||||
let action;
|
const latestSecretVersions = (await getLatestNSecretSecretVersionIds({
|
||||||
try {
|
secretIds,
|
||||||
const latestSecretVersions = (await getLatestNSecretSecretVersionIds({
|
n: 2
|
||||||
secretIds,
|
}))
|
||||||
n: 2
|
.map((s) => ({
|
||||||
}))
|
oldSecretVersion: s.versions[0]._id,
|
||||||
.map((s) => ({
|
newSecretVersion: s.versions[1]._id
|
||||||
oldSecretVersion: s.versions[0]._id,
|
}));
|
||||||
newSecretVersion: s.versions[1]._id
|
|
||||||
}));
|
const action = await new Action({
|
||||||
|
name,
|
||||||
action = await new Action({
|
user: userId,
|
||||||
name,
|
serviceAccount: serviceAccountId,
|
||||||
user: userId,
|
serviceTokenData: serviceTokenDataId,
|
||||||
serviceAccount: serviceAccountId,
|
workspace: workspaceId,
|
||||||
serviceTokenData: serviceTokenDataId,
|
payload: {
|
||||||
workspace: workspaceId,
|
secretVersions: latestSecretVersions
|
||||||
payload: {
|
}
|
||||||
secretVersions: latestSecretVersions
|
}).save();
|
||||||
}
|
|
||||||
}).save();
|
|
||||||
|
|
||||||
} catch (err) {
|
|
||||||
Sentry.setUser(null);
|
|
||||||
Sentry.captureException(err);
|
|
||||||
throw new Error('Failed to create update secret action');
|
|
||||||
}
|
|
||||||
|
|
||||||
return action;
|
return action;
|
||||||
}
|
}
|
||||||
@@ -90,33 +81,25 @@ const createActionSecret = async ({
|
|||||||
workspaceId: Types.ObjectId;
|
workspaceId: Types.ObjectId;
|
||||||
secretIds: Types.ObjectId[];
|
secretIds: Types.ObjectId[];
|
||||||
}) => {
|
}) => {
|
||||||
let action;
|
// case: action is adding, deleting, or reading secrets
|
||||||
try {
|
// -> add new secret versions
|
||||||
// case: action is adding, deleting, or reading secrets
|
const latestSecretVersions = (await getLatestSecretVersionIds({
|
||||||
// -> add new secret versions
|
secretIds
|
||||||
const latestSecretVersions = (await getLatestSecretVersionIds({
|
}))
|
||||||
secretIds
|
.map((s) => ({
|
||||||
}))
|
newSecretVersion: s.versionId
|
||||||
.map((s) => ({
|
}));
|
||||||
newSecretVersion: s.versionId
|
|
||||||
}));
|
const action = await new Action({
|
||||||
|
name,
|
||||||
action = await new Action({
|
user: userId,
|
||||||
name,
|
serviceAccount: serviceAccountId,
|
||||||
user: userId,
|
serviceTokenData: serviceTokenDataId,
|
||||||
serviceAccount: serviceAccountId,
|
workspace: workspaceId,
|
||||||
serviceTokenData: serviceTokenDataId,
|
payload: {
|
||||||
workspace: workspaceId,
|
secretVersions: latestSecretVersions
|
||||||
payload: {
|
}
|
||||||
secretVersions: latestSecretVersions
|
}).save();
|
||||||
}
|
|
||||||
}).save();
|
|
||||||
|
|
||||||
} catch (err) {
|
|
||||||
Sentry.setUser(null);
|
|
||||||
Sentry.captureException(err);
|
|
||||||
throw new Error('Failed to create action create/read/delete secret action');
|
|
||||||
}
|
|
||||||
|
|
||||||
return action;
|
return action;
|
||||||
}
|
}
|
||||||
@@ -140,19 +123,12 @@ const createActionClient = ({
|
|||||||
serviceAccountId?: Types.ObjectId;
|
serviceAccountId?: Types.ObjectId;
|
||||||
serviceTokenDataId?: Types.ObjectId;
|
serviceTokenDataId?: Types.ObjectId;
|
||||||
}) => {
|
}) => {
|
||||||
let action;
|
const action = new Action({
|
||||||
try {
|
name,
|
||||||
action = new Action({
|
user: userId,
|
||||||
name,
|
serviceAccount: serviceAccountId,
|
||||||
user: userId,
|
serviceTokenData: serviceTokenDataId
|
||||||
serviceAccount: serviceAccountId,
|
}).save();
|
||||||
serviceTokenData: serviceTokenDataId
|
|
||||||
}).save();
|
|
||||||
} catch (err) {
|
|
||||||
Sentry.setUser(null);
|
|
||||||
Sentry.captureException(err);
|
|
||||||
throw new Error('Failed to create client action');
|
|
||||||
}
|
|
||||||
|
|
||||||
return action;
|
return action;
|
||||||
}
|
}
|
||||||
@@ -181,40 +157,34 @@ const createActionHelper = async ({
|
|||||||
secretIds?: Types.ObjectId[];
|
secretIds?: Types.ObjectId[];
|
||||||
}) => {
|
}) => {
|
||||||
let action;
|
let action;
|
||||||
try {
|
switch (name) {
|
||||||
switch (name) {
|
case ACTION_LOGIN:
|
||||||
case ACTION_LOGIN:
|
case ACTION_LOGOUT:
|
||||||
case ACTION_LOGOUT:
|
action = await createActionClient({
|
||||||
action = await createActionClient({
|
name,
|
||||||
name,
|
userId
|
||||||
userId
|
});
|
||||||
});
|
break;
|
||||||
break;
|
case ACTION_ADD_SECRETS:
|
||||||
case ACTION_ADD_SECRETS:
|
case ACTION_READ_SECRETS:
|
||||||
case ACTION_READ_SECRETS:
|
case ACTION_DELETE_SECRETS:
|
||||||
case ACTION_DELETE_SECRETS:
|
if (!workspaceId || !secretIds) throw new Error('Missing required params workspace id or secret ids to create action secret');
|
||||||
if (!workspaceId || !secretIds) throw new Error('Missing required params workspace id or secret ids to create action secret');
|
action = await createActionSecret({
|
||||||
action = await createActionSecret({
|
name,
|
||||||
name,
|
userId,
|
||||||
userId,
|
workspaceId,
|
||||||
workspaceId,
|
secretIds
|
||||||
secretIds
|
});
|
||||||
});
|
break;
|
||||||
break;
|
case ACTION_UPDATE_SECRETS:
|
||||||
case ACTION_UPDATE_SECRETS:
|
if (!workspaceId || !secretIds) throw new Error('Missing required params workspace id or secret ids to create action secret');
|
||||||
if (!workspaceId || !secretIds) throw new Error('Missing required params workspace id or secret ids to create action secret');
|
action = await createActionUpdateSecret({
|
||||||
action = await createActionUpdateSecret({
|
name,
|
||||||
name,
|
userId,
|
||||||
userId,
|
workspaceId,
|
||||||
workspaceId,
|
secretIds
|
||||||
secretIds
|
});
|
||||||
});
|
break;
|
||||||
break;
|
|
||||||
}
|
|
||||||
} catch (err) {
|
|
||||||
Sentry.setUser(null);
|
|
||||||
Sentry.captureException(err);
|
|
||||||
throw new Error('Failed to create action');
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return action;
|
return action;
|
||||||
@@ -222,4 +192,4 @@ const createActionHelper = async ({
|
|||||||
|
|
||||||
export {
|
export {
|
||||||
createActionHelper
|
createActionHelper
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
import * as Sentry from '@sentry/node';
|
|
||||||
import { Types } from 'mongoose';
|
import { Types } from 'mongoose';
|
||||||
import {
|
import {
|
||||||
Log,
|
Log,
|
||||||
@@ -32,27 +31,20 @@ const createLogHelper = async ({
|
|||||||
channel: string;
|
channel: string;
|
||||||
ipAddress: string;
|
ipAddress: string;
|
||||||
}) => {
|
}) => {
|
||||||
let log;
|
const log = await new Log({
|
||||||
try {
|
user: userId,
|
||||||
log = await new Log({
|
serviceAccount: serviceAccountId,
|
||||||
user: userId,
|
serviceTokenData: serviceTokenDataId,
|
||||||
serviceAccount: serviceAccountId,
|
workspace: workspaceId ?? undefined,
|
||||||
serviceTokenData: serviceTokenDataId,
|
actionNames: actions.map((a) => a.name),
|
||||||
workspace: workspaceId ?? undefined,
|
actions,
|
||||||
actionNames: actions.map((a) => a.name),
|
channel,
|
||||||
actions,
|
ipAddress
|
||||||
channel,
|
}).save();
|
||||||
ipAddress
|
|
||||||
}).save();
|
|
||||||
} catch (err) {
|
|
||||||
Sentry.setUser(null);
|
|
||||||
Sentry.captureException(err);
|
|
||||||
throw new Error('Failed to create log');
|
|
||||||
}
|
|
||||||
|
|
||||||
return log;
|
return log;
|
||||||
}
|
}
|
||||||
|
|
||||||
export {
|
export {
|
||||||
createLogHelper
|
createLogHelper
|
||||||
}
|
}
|
||||||
|
|||||||
+103
-128
@@ -1,14 +1,6 @@
|
|||||||
import { Types } from 'mongoose';
|
import { Types } from "mongoose";
|
||||||
import * as Sentry from '@sentry/node';
|
import { Secret, ISecret } from "../../models";
|
||||||
import {
|
import { SecretSnapshot, SecretVersion, ISecretVersion } from "../models";
|
||||||
Secret,
|
|
||||||
ISecret,
|
|
||||||
} from '../../models';
|
|
||||||
import {
|
|
||||||
SecretSnapshot,
|
|
||||||
SecretVersion,
|
|
||||||
ISecretVersion
|
|
||||||
} from '../models';
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Save a secret snapshot that is a copy of the current state of secrets in workspace with id
|
* Save a secret snapshot that is a copy of the current state of secrets in workspace with id
|
||||||
@@ -19,56 +11,53 @@ import {
|
|||||||
* @returns {SecretSnapshot} secretSnapshot - new secret snapshot
|
* @returns {SecretSnapshot} secretSnapshot - new secret snapshot
|
||||||
*/
|
*/
|
||||||
const takeSecretSnapshotHelper = async ({
|
const takeSecretSnapshotHelper = async ({
|
||||||
workspaceId
|
workspaceId,
|
||||||
}: {
|
}: {
|
||||||
workspaceId: Types.ObjectId;
|
workspaceId: Types.ObjectId;
|
||||||
}) => {
|
}) => {
|
||||||
|
const secretIds = (
|
||||||
|
await Secret.find(
|
||||||
|
{
|
||||||
|
workspace: workspaceId,
|
||||||
|
},
|
||||||
|
"_id"
|
||||||
|
)
|
||||||
|
).map((s) => s._id);
|
||||||
|
|
||||||
let secretSnapshot;
|
const latestSecretVersions = (
|
||||||
try {
|
await SecretVersion.aggregate([
|
||||||
const secretIds = (await Secret.find({
|
{
|
||||||
workspace: workspaceId
|
$match: {
|
||||||
}, '_id')).map((s) => s._id);
|
secret: {
|
||||||
|
$in: secretIds,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
$group: {
|
||||||
|
_id: "$secret",
|
||||||
|
version: { $max: "$version" },
|
||||||
|
versionId: { $max: "$_id" }, // secret version id
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
$sort: { version: -1 },
|
||||||
|
},
|
||||||
|
]).exec()
|
||||||
|
).map((s) => s.versionId);
|
||||||
|
|
||||||
const latestSecretVersions = (await SecretVersion.aggregate([
|
const latestSecretSnapshot = await SecretSnapshot.findOne({
|
||||||
{
|
workspace: workspaceId,
|
||||||
$match: {
|
}).sort({ version: -1 });
|
||||||
secret: {
|
|
||||||
$in: secretIds
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
$group: {
|
|
||||||
_id: '$secret',
|
|
||||||
version: { $max: '$version' },
|
|
||||||
versionId: { $max: '$_id' } // secret version id
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
$sort: { version: -1 }
|
|
||||||
}
|
|
||||||
])
|
|
||||||
.exec())
|
|
||||||
.map((s) => s.versionId);
|
|
||||||
|
|
||||||
const latestSecretSnapshot = await SecretSnapshot.findOne({
|
const secretSnapshot = await new SecretSnapshot({
|
||||||
workspace: workspaceId
|
workspace: workspaceId,
|
||||||
}).sort({ version: -1 });
|
version: latestSecretSnapshot ? latestSecretSnapshot.version + 1 : 1,
|
||||||
|
secretVersions: latestSecretVersions,
|
||||||
|
}).save();
|
||||||
|
|
||||||
secretSnapshot = await new SecretSnapshot({
|
return secretSnapshot;
|
||||||
workspace: workspaceId,
|
};
|
||||||
version: latestSecretSnapshot ? latestSecretSnapshot.version + 1 : 1,
|
|
||||||
secretVersions: latestSecretVersions
|
|
||||||
}).save();
|
|
||||||
} catch (err) {
|
|
||||||
Sentry.setUser(null);
|
|
||||||
Sentry.captureException(err);
|
|
||||||
throw new Error('Failed to take a secret snapshot');
|
|
||||||
}
|
|
||||||
|
|
||||||
return secretSnapshot;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Add secret versions [secretVersions] to the SecretVersion collection.
|
* Add secret versions [secretVersions] to the SecretVersion collection.
|
||||||
@@ -77,93 +66,79 @@ const takeSecretSnapshotHelper = async ({
|
|||||||
* @returns {SecretVersion[]} newSecretVersions - new secret versions
|
* @returns {SecretVersion[]} newSecretVersions - new secret versions
|
||||||
*/
|
*/
|
||||||
const addSecretVersionsHelper = async ({
|
const addSecretVersionsHelper = async ({
|
||||||
secretVersions
|
secretVersions,
|
||||||
}: {
|
}: {
|
||||||
secretVersions: ISecretVersion[]
|
secretVersions: ISecretVersion[];
|
||||||
}) => {
|
}) => {
|
||||||
let newSecretVersions;
|
const newSecretVersions = await SecretVersion.insertMany(secretVersions);
|
||||||
try {
|
|
||||||
newSecretVersions = await SecretVersion.insertMany(secretVersions);
|
|
||||||
} catch (err) {
|
|
||||||
Sentry.setUser(null);
|
|
||||||
Sentry.captureException(err);
|
|
||||||
throw new Error(`Failed to add secret versions [err=${err}]`);
|
|
||||||
}
|
|
||||||
|
|
||||||
return newSecretVersions;
|
return newSecretVersions;
|
||||||
}
|
};
|
||||||
|
|
||||||
const markDeletedSecretVersionsHelper = async ({
|
const markDeletedSecretVersionsHelper = async ({
|
||||||
secretIds
|
secretIds,
|
||||||
}: {
|
}: {
|
||||||
secretIds: Types.ObjectId[];
|
secretIds: Types.ObjectId[];
|
||||||
}) => {
|
}) => {
|
||||||
try {
|
await SecretVersion.updateMany(
|
||||||
await SecretVersion.updateMany({
|
{
|
||||||
secret: { $in: secretIds }
|
secret: { $in: secretIds },
|
||||||
}, {
|
},
|
||||||
isDeleted: true
|
{
|
||||||
}, {
|
isDeleted: true,
|
||||||
new: true
|
},
|
||||||
});
|
{
|
||||||
} catch (err) {
|
new: true,
|
||||||
Sentry.setUser(null);
|
}
|
||||||
Sentry.captureException(err);
|
);
|
||||||
throw new Error('Failed to mark secret versions as deleted');
|
};
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Initialize secret versioning by setting previously unversioned
|
* Initialize secret versioning by setting previously unversioned
|
||||||
* secrets to version 1 and begin populating secret versions.
|
* secrets to version 1 and begin populating secret versions.
|
||||||
*/
|
*/
|
||||||
const initSecretVersioningHelper = async () => {
|
const initSecretVersioningHelper = async () => {
|
||||||
try {
|
await Secret.updateMany(
|
||||||
|
{ version: { $exists: false } },
|
||||||
|
{ $set: { version: 1 } }
|
||||||
|
);
|
||||||
|
|
||||||
await Secret.updateMany(
|
const unversionedSecrets: ISecret[] = await Secret.aggregate([
|
||||||
{ version: { $exists: false } },
|
{
|
||||||
{ $set: { version: 1 } }
|
$lookup: {
|
||||||
);
|
from: "secretversions",
|
||||||
|
localField: "_id",
|
||||||
|
foreignField: "secret",
|
||||||
|
as: "versions",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
$match: {
|
||||||
|
versions: { $size: 0 },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
const unversionedSecrets: ISecret[] = await Secret.aggregate([
|
if (unversionedSecrets.length > 0) {
|
||||||
{
|
await addSecretVersionsHelper({
|
||||||
$lookup: {
|
secretVersions: unversionedSecrets.map(
|
||||||
from: 'secretversions',
|
(s, idx) =>
|
||||||
localField: '_id',
|
new SecretVersion({
|
||||||
foreignField: 'secret',
|
...s,
|
||||||
as: 'versions',
|
secret: s._id,
|
||||||
},
|
version: s.version ? s.version : 1,
|
||||||
},
|
isDeleted: false,
|
||||||
{
|
workspace: s.workspace,
|
||||||
$match: {
|
environment: s.environment,
|
||||||
versions: { $size: 0 },
|
})
|
||||||
},
|
),
|
||||||
},
|
});
|
||||||
]);
|
}
|
||||||
|
};
|
||||||
if (unversionedSecrets.length > 0) {
|
|
||||||
await addSecretVersionsHelper({
|
|
||||||
secretVersions: unversionedSecrets.map((s, idx) => new SecretVersion({
|
|
||||||
...s,
|
|
||||||
secret: s._id,
|
|
||||||
version: s.version ? s.version : 1,
|
|
||||||
isDeleted: false,
|
|
||||||
workspace: s.workspace,
|
|
||||||
environment: s.environment
|
|
||||||
}))
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
} catch (err) {
|
|
||||||
Sentry.setUser(null);
|
|
||||||
Sentry.captureException(err);
|
|
||||||
throw new Error('Failed to ensure that secrets are versioned');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export {
|
export {
|
||||||
takeSecretSnapshotHelper,
|
takeSecretSnapshotHelper,
|
||||||
addSecretVersionsHelper,
|
addSecretVersionsHelper,
|
||||||
markDeletedSecretVersionsHelper,
|
markDeletedSecretVersionsHelper,
|
||||||
initSecretVersioningHelper
|
initSecretVersioningHelper,
|
||||||
}
|
};
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
import * as Sentry from '@sentry/node';
|
|
||||||
import { Types } from 'mongoose';
|
import { Types } from 'mongoose';
|
||||||
import { SecretVersion } from '../models';
|
import { SecretVersion } from '../models';
|
||||||
|
|
||||||
@@ -13,41 +12,32 @@ const getLatestSecretVersionIds = async ({
|
|||||||
}: {
|
}: {
|
||||||
secretIds: Types.ObjectId[];
|
secretIds: Types.ObjectId[];
|
||||||
}) => {
|
}) => {
|
||||||
|
|
||||||
interface LatestSecretVersionId {
|
interface LatestSecretVersionId {
|
||||||
_id: Types.ObjectId;
|
_id: Types.ObjectId;
|
||||||
version: number;
|
version: number;
|
||||||
versionId: Types.ObjectId;
|
versionId: Types.ObjectId;
|
||||||
}
|
}
|
||||||
|
|
||||||
let latestSecretVersionIds: LatestSecretVersionId[];
|
const latestSecretVersionIds = (await SecretVersion.aggregate([
|
||||||
try {
|
{
|
||||||
latestSecretVersionIds = (await SecretVersion.aggregate([
|
$match: {
|
||||||
{
|
secret: {
|
||||||
$match: {
|
$in: secretIds
|
||||||
secret: {
|
}
|
||||||
$in: secretIds
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
$group: {
|
|
||||||
_id: '$secret',
|
|
||||||
version: { $max: '$version' },
|
|
||||||
versionId: { $max: '$_id' } // id of latest secret version
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
$sort: { version: -1 }
|
|
||||||
}
|
}
|
||||||
])
|
},
|
||||||
.exec());
|
{
|
||||||
|
$group: {
|
||||||
} catch (err) {
|
_id: '$secret',
|
||||||
Sentry.setUser(null);
|
version: { $max: '$version' },
|
||||||
Sentry.captureException(err);
|
versionId: { $max: '$_id' } // id of latest secret version
|
||||||
throw new Error('Failed to get latest secret versions');
|
}
|
||||||
}
|
},
|
||||||
|
{
|
||||||
|
$sort: { version: -1 }
|
||||||
|
}
|
||||||
|
])
|
||||||
|
.exec());
|
||||||
|
|
||||||
return latestSecretVersionIds;
|
return latestSecretVersionIds;
|
||||||
}
|
}
|
||||||
@@ -66,40 +56,32 @@ const getLatestNSecretSecretVersionIds = async ({
|
|||||||
secretIds: Types.ObjectId[];
|
secretIds: Types.ObjectId[];
|
||||||
n: number;
|
n: number;
|
||||||
}) => {
|
}) => {
|
||||||
|
|
||||||
// TODO: optimize query
|
// TODO: optimize query
|
||||||
let latestNSecretVersions;
|
const latestNSecretVersions = (await SecretVersion.aggregate([
|
||||||
try {
|
{
|
||||||
latestNSecretVersions = (await SecretVersion.aggregate([
|
$match: {
|
||||||
{
|
secret: {
|
||||||
$match: {
|
$in: secretIds,
|
||||||
secret: {
|
|
||||||
$in: secretIds,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
{
|
|
||||||
$sort: { version: -1 },
|
|
||||||
},
|
},
|
||||||
{
|
},
|
||||||
$group: {
|
{
|
||||||
_id: "$secret",
|
$sort: { version: -1 },
|
||||||
versions: { $push: "$$ROOT" },
|
},
|
||||||
},
|
{
|
||||||
|
$group: {
|
||||||
|
_id: "$secret",
|
||||||
|
versions: { $push: "$$ROOT" },
|
||||||
},
|
},
|
||||||
{
|
},
|
||||||
$project: {
|
{
|
||||||
_id: 0,
|
$project: {
|
||||||
secret: "$_id",
|
_id: 0,
|
||||||
versions: { $slice: ["$versions", n] },
|
secret: "$_id",
|
||||||
},
|
versions: { $slice: ["$versions", n] },
|
||||||
}
|
},
|
||||||
]));
|
}
|
||||||
} catch (err) {
|
]));
|
||||||
Sentry.setUser(null);
|
|
||||||
Sentry.captureException(err);
|
|
||||||
throw new Error('Failed to get latest n secret versions');
|
|
||||||
}
|
|
||||||
|
|
||||||
return latestNSecretVersions;
|
return latestNSecretVersions;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
import * as Sentry from '@sentry/node';
|
|
||||||
import { Types } from 'mongoose';
|
import { Types } from 'mongoose';
|
||||||
import jwt from 'jsonwebtoken';
|
import jwt from 'jsonwebtoken';
|
||||||
import bcrypt from 'bcrypt';
|
import bcrypt from 'bcrypt';
|
||||||
|
|||||||
+212
-243
@@ -1,41 +1,34 @@
|
|||||||
import * as Sentry from '@sentry/node';
|
import { Types } from "mongoose";
|
||||||
import { Types } from 'mongoose';
|
|
||||||
import {
|
import {
|
||||||
Bot,
|
Bot,
|
||||||
BotKey,
|
BotKey,
|
||||||
Secret,
|
Secret,
|
||||||
ISecret,
|
ISecret,
|
||||||
IUser,
|
IUser,
|
||||||
User,
|
User,
|
||||||
IServiceAccount,
|
IServiceAccount,
|
||||||
ServiceAccount,
|
ServiceAccount,
|
||||||
IServiceTokenData,
|
IServiceTokenData,
|
||||||
ServiceTokenData
|
ServiceTokenData,
|
||||||
} from '../models';
|
} from "../models";
|
||||||
import {
|
|
||||||
generateKeyPair,
|
|
||||||
encryptSymmetric,
|
|
||||||
decryptSymmetric,
|
|
||||||
decryptAsymmetric
|
|
||||||
} from '../utils/crypto';
|
|
||||||
import {
|
import {
|
||||||
SECRET_SHARED,
|
generateKeyPair,
|
||||||
AUTH_MODE_JWT,
|
encryptSymmetric,
|
||||||
AUTH_MODE_SERVICE_ACCOUNT,
|
decryptSymmetric,
|
||||||
AUTH_MODE_SERVICE_TOKEN,
|
decryptAsymmetric,
|
||||||
AUTH_MODE_API_KEY
|
} from "../utils/crypto";
|
||||||
} from '../variables';
|
|
||||||
import { getEncryptionKey } from '../config';
|
|
||||||
import { BotNotFoundError, UnauthorizedRequestError } from '../utils/errors';
|
|
||||||
import {
|
import {
|
||||||
validateMembership
|
SECRET_SHARED,
|
||||||
} from '../helpers/membership';
|
AUTH_MODE_JWT,
|
||||||
import {
|
AUTH_MODE_SERVICE_ACCOUNT,
|
||||||
validateUserClientForWorkspace
|
AUTH_MODE_SERVICE_TOKEN,
|
||||||
} from '../helpers/user';
|
AUTH_MODE_API_KEY,
|
||||||
import {
|
} from "../variables";
|
||||||
validateServiceAccountClientForWorkspace
|
import { getEncryptionKey } from "../config";
|
||||||
} from '../helpers/serviceAccount';
|
import { BotNotFoundError, UnauthorizedRequestError } from "../utils/errors";
|
||||||
|
import { validateMembership } from "../helpers/membership";
|
||||||
|
import { validateUserClientForWorkspace } from "../helpers/user";
|
||||||
|
import { validateServiceAccountClientForWorkspace } from "../helpers/serviceAccount";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Validate authenticated clients for bot with id [botId] based
|
* Validate authenticated clients for bot with id [botId] based
|
||||||
@@ -46,99 +39,104 @@ import {
|
|||||||
* @param {Array<'admin' | 'member'>} obj.acceptedRoles - accepted workspace roles
|
* @param {Array<'admin' | 'member'>} obj.acceptedRoles - accepted workspace roles
|
||||||
*/
|
*/
|
||||||
const validateClientForBot = async ({
|
const validateClientForBot = async ({
|
||||||
authData,
|
authData,
|
||||||
botId,
|
botId,
|
||||||
acceptedRoles
|
acceptedRoles,
|
||||||
}: {
|
}: {
|
||||||
authData: {
|
authData: {
|
||||||
authMode: string;
|
authMode: string;
|
||||||
authPayload: IUser | IServiceAccount | IServiceTokenData;
|
authPayload: IUser | IServiceAccount | IServiceTokenData;
|
||||||
};
|
};
|
||||||
botId: Types.ObjectId;
|
botId: Types.ObjectId;
|
||||||
acceptedRoles: Array<'admin' | 'member'>;
|
acceptedRoles: Array<"admin" | "member">;
|
||||||
}) => {
|
}) => {
|
||||||
const bot = await Bot.findById(botId);
|
const bot = await Bot.findById(botId);
|
||||||
|
|
||||||
if (!bot) throw BotNotFoundError();
|
|
||||||
|
|
||||||
if (authData.authMode === AUTH_MODE_JWT && authData.authPayload instanceof User) {
|
|
||||||
await validateUserClientForWorkspace({
|
|
||||||
user: authData.authPayload,
|
|
||||||
workspaceId: bot.workspace,
|
|
||||||
acceptedRoles
|
|
||||||
});
|
|
||||||
|
|
||||||
return bot;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (authData.authMode === AUTH_MODE_SERVICE_ACCOUNT && authData.authPayload instanceof ServiceAccount) {
|
if (!bot) throw BotNotFoundError();
|
||||||
await validateServiceAccountClientForWorkspace({
|
|
||||||
serviceAccount: authData.authPayload,
|
|
||||||
workspaceId: bot.workspace
|
|
||||||
});
|
|
||||||
|
|
||||||
return bot;
|
if (
|
||||||
}
|
authData.authMode === AUTH_MODE_JWT &&
|
||||||
|
authData.authPayload instanceof User
|
||||||
if (authData.authMode === AUTH_MODE_SERVICE_TOKEN && authData.authPayload instanceof ServiceTokenData) {
|
) {
|
||||||
throw UnauthorizedRequestError({
|
await validateUserClientForWorkspace({
|
||||||
message: 'Failed service token authorization for bot'
|
user: authData.authPayload,
|
||||||
});
|
workspaceId: bot.workspace,
|
||||||
}
|
acceptedRoles,
|
||||||
|
|
||||||
if (authData.authMode === AUTH_MODE_API_KEY && authData.authPayload instanceof User) {
|
|
||||||
await validateUserClientForWorkspace({
|
|
||||||
user: authData.authPayload,
|
|
||||||
workspaceId: bot.workspace,
|
|
||||||
acceptedRoles
|
|
||||||
});
|
|
||||||
|
|
||||||
return bot;
|
|
||||||
}
|
|
||||||
|
|
||||||
throw BotNotFoundError({
|
|
||||||
message: 'Failed client authorization for bot'
|
|
||||||
});
|
});
|
||||||
}
|
|
||||||
|
return bot;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
authData.authMode === AUTH_MODE_SERVICE_ACCOUNT &&
|
||||||
|
authData.authPayload instanceof ServiceAccount
|
||||||
|
) {
|
||||||
|
await validateServiceAccountClientForWorkspace({
|
||||||
|
serviceAccount: authData.authPayload,
|
||||||
|
workspaceId: bot.workspace,
|
||||||
|
});
|
||||||
|
|
||||||
|
return bot;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
authData.authMode === AUTH_MODE_SERVICE_TOKEN &&
|
||||||
|
authData.authPayload instanceof ServiceTokenData
|
||||||
|
) {
|
||||||
|
throw UnauthorizedRequestError({
|
||||||
|
message: "Failed service token authorization for bot",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
authData.authMode === AUTH_MODE_API_KEY &&
|
||||||
|
authData.authPayload instanceof User
|
||||||
|
) {
|
||||||
|
await validateUserClientForWorkspace({
|
||||||
|
user: authData.authPayload,
|
||||||
|
workspaceId: bot.workspace,
|
||||||
|
acceptedRoles,
|
||||||
|
});
|
||||||
|
|
||||||
|
return bot;
|
||||||
|
}
|
||||||
|
|
||||||
|
throw BotNotFoundError({
|
||||||
|
message: "Failed client authorization for bot",
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Create an inactive bot with name [name] for workspace with id [workspaceId]
|
* Create an inactive bot with name [name] for workspace with id [workspaceId]
|
||||||
* @param {Object} obj
|
* @param {Object} obj
|
||||||
* @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 ({
|
const createBot = async ({
|
||||||
name,
|
name,
|
||||||
workspaceId,
|
workspaceId,
|
||||||
}: {
|
}: {
|
||||||
name: string;
|
name: string;
|
||||||
workspaceId: Types.ObjectId;
|
workspaceId: Types.ObjectId;
|
||||||
}) => {
|
}) => {
|
||||||
let bot;
|
const { publicKey, privateKey } = generateKeyPair();
|
||||||
try {
|
const { ciphertext, iv, tag } = encryptSymmetric({
|
||||||
const { publicKey, privateKey } = generateKeyPair();
|
plaintext: privateKey,
|
||||||
const { ciphertext, iv, tag } = encryptSymmetric({
|
key: await getEncryptionKey(),
|
||||||
plaintext: privateKey,
|
});
|
||||||
key: await getEncryptionKey()
|
|
||||||
});
|
|
||||||
|
|
||||||
bot = await new Bot({
|
const bot = await new Bot({
|
||||||
name,
|
name,
|
||||||
workspace: workspaceId,
|
workspace: workspaceId,
|
||||||
isActive: false,
|
isActive: false,
|
||||||
publicKey,
|
publicKey,
|
||||||
encryptedPrivateKey: ciphertext,
|
encryptedPrivateKey: ciphertext,
|
||||||
iv,
|
iv,
|
||||||
tag
|
tag,
|
||||||
}).save();
|
}).save();
|
||||||
} catch (err) {
|
|
||||||
Sentry.setUser(null);
|
return bot;
|
||||||
Sentry.captureException(err);
|
};
|
||||||
throw new Error('Failed to create bot');
|
|
||||||
}
|
|
||||||
|
|
||||||
return bot;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Return decrypted secrets for workspace with id [workspaceId]
|
* Return decrypted secrets for workspace with id [workspaceId]
|
||||||
@@ -148,125 +146,105 @@ const createBot = async ({
|
|||||||
* @param {String} obj.environment - environment
|
* @param {String} obj.environment - environment
|
||||||
*/
|
*/
|
||||||
const getSecretsHelper = async ({
|
const getSecretsHelper = async ({
|
||||||
workspaceId,
|
workspaceId,
|
||||||
environment
|
environment,
|
||||||
}: {
|
}: {
|
||||||
workspaceId: Types.ObjectId;
|
workspaceId: Types.ObjectId;
|
||||||
environment: string;
|
environment: string;
|
||||||
}) => {
|
}) => {
|
||||||
const content = {} as any;
|
const content = {} as any;
|
||||||
try {
|
const key = await getKey({ workspaceId: workspaceId.toString() });
|
||||||
const key = await getKey({ workspaceId });
|
const secrets = await Secret.find({
|
||||||
const secrets = await Secret.find({
|
workspace: workspaceId,
|
||||||
workspace: workspaceId,
|
environment,
|
||||||
environment,
|
type: SECRET_SHARED,
|
||||||
type: SECRET_SHARED
|
});
|
||||||
});
|
|
||||||
|
|
||||||
secrets.forEach((secret: ISecret) => {
|
|
||||||
const secretKey = decryptSymmetric({
|
|
||||||
ciphertext: secret.secretKeyCiphertext,
|
|
||||||
iv: secret.secretKeyIV,
|
|
||||||
tag: secret.secretKeyTag,
|
|
||||||
key
|
|
||||||
});
|
|
||||||
|
|
||||||
const secretValue = decryptSymmetric({
|
secrets.forEach((secret: ISecret) => {
|
||||||
ciphertext: secret.secretValueCiphertext,
|
const secretKey = decryptSymmetric({
|
||||||
iv: secret.secretValueIV,
|
ciphertext: secret.secretKeyCiphertext,
|
||||||
tag: secret.secretValueTag,
|
iv: secret.secretKeyIV,
|
||||||
key
|
tag: secret.secretKeyTag,
|
||||||
});
|
key,
|
||||||
|
});
|
||||||
|
|
||||||
content[secretKey] = secretValue;
|
const secretValue = decryptSymmetric({
|
||||||
});
|
ciphertext: secret.secretValueCiphertext,
|
||||||
} catch (err) {
|
iv: secret.secretValueIV,
|
||||||
Sentry.setUser(null);
|
tag: secret.secretValueTag,
|
||||||
Sentry.captureException(err);
|
key,
|
||||||
throw new Error('Failed to get secrets');
|
});
|
||||||
}
|
|
||||||
|
|
||||||
return content;
|
content[secretKey] = secretValue;
|
||||||
}
|
});
|
||||||
|
|
||||||
|
return content;
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Return bot's copy of the workspace key for workspace
|
* Return bot's copy of the workspace key for workspace
|
||||||
* with id [workspaceId]
|
* with id [workspaceId]
|
||||||
* @param {Object} obj
|
* @param {Object} obj
|
||||||
* @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: Types.ObjectId }) => {
|
const getKey = async ({ workspaceId }: { workspaceId: string }) => {
|
||||||
let key;
|
const botKey = await BotKey.findOne({
|
||||||
try {
|
workspace: workspaceId,
|
||||||
const botKey = await BotKey.findOne({
|
}).populate<{ sender: IUser }>("sender", "publicKey");
|
||||||
workspace: workspaceId
|
|
||||||
}).populate<{ sender: IUser }>('sender', 'publicKey');
|
if (!botKey) throw new Error("Failed to find bot key");
|
||||||
|
|
||||||
if (!botKey) throw new Error('Failed to find bot key');
|
const bot = await Bot.findOne({
|
||||||
|
workspace: workspaceId,
|
||||||
const bot = await Bot.findOne({
|
}).select("+encryptedPrivateKey +iv +tag");
|
||||||
workspace: workspaceId
|
|
||||||
}).select('+encryptedPrivateKey +iv +tag');
|
if (!bot) throw new Error("Failed to find bot");
|
||||||
|
if (!bot.isActive) throw new Error("Bot is not active");
|
||||||
if (!bot) throw new Error('Failed to find bot');
|
|
||||||
if (!bot.isActive) throw new Error('Bot is not active');
|
const privateKeyBot = decryptSymmetric({
|
||||||
|
ciphertext: bot.encryptedPrivateKey,
|
||||||
const privateKeyBot = decryptSymmetric({
|
iv: bot.iv,
|
||||||
ciphertext: bot.encryptedPrivateKey,
|
tag: bot.tag,
|
||||||
iv: bot.iv,
|
key: await getEncryptionKey(),
|
||||||
tag: bot.tag,
|
});
|
||||||
key: await getEncryptionKey()
|
|
||||||
});
|
const key = decryptAsymmetric({
|
||||||
|
ciphertext: botKey.encryptedKey,
|
||||||
key = decryptAsymmetric({
|
nonce: botKey.nonce,
|
||||||
ciphertext: botKey.encryptedKey,
|
publicKey: botKey.sender.publicKey as string,
|
||||||
nonce: botKey.nonce,
|
privateKey: privateKeyBot,
|
||||||
publicKey: botKey.sender.publicKey as string,
|
});
|
||||||
privateKey: privateKeyBot
|
|
||||||
});
|
return key;
|
||||||
} catch (err) {
|
};
|
||||||
Sentry.setUser(null);
|
|
||||||
Sentry.captureException(err);
|
|
||||||
throw new Error('Failed to get workspace key');
|
|
||||||
}
|
|
||||||
|
|
||||||
return key;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Return symmetrically encrypted [plaintext] using the
|
* Return symmetrically encrypted [plaintext] using the
|
||||||
* key for workspace with id [workspaceId]
|
* key for workspace with id [workspaceId]
|
||||||
* @param {Object} obj1
|
* @param {Object} obj1
|
||||||
* @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 ({
|
const encryptSymmetricHelper = async ({
|
||||||
workspaceId,
|
workspaceId,
|
||||||
plaintext
|
plaintext,
|
||||||
}: {
|
}: {
|
||||||
workspaceId: Types.ObjectId;
|
workspaceId: Types.ObjectId;
|
||||||
plaintext: string;
|
plaintext: string;
|
||||||
}) => {
|
}) => {
|
||||||
|
const key = await getKey({ workspaceId: workspaceId.toString() });
|
||||||
try {
|
const { ciphertext, iv, tag } = encryptSymmetric({
|
||||||
const key = await getKey({ workspaceId });
|
plaintext,
|
||||||
const { ciphertext, iv, tag } = encryptSymmetric({
|
key,
|
||||||
plaintext,
|
});
|
||||||
key
|
|
||||||
});
|
return {
|
||||||
|
ciphertext,
|
||||||
return ({
|
iv,
|
||||||
ciphertext,
|
tag,
|
||||||
iv,
|
};
|
||||||
tag
|
};
|
||||||
});
|
|
||||||
} catch (err) {
|
|
||||||
Sentry.setUser(null);
|
|
||||||
Sentry.captureException(err);
|
|
||||||
throw new Error('Failed to perform symmetric encryption with bot');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
/**
|
/**
|
||||||
* Return symmetrically decrypted [ciphertext] using the
|
* Return symmetrically decrypted [ciphertext] using the
|
||||||
* key for workspace with id [workspaceId]
|
* key for workspace with id [workspaceId]
|
||||||
@@ -277,40 +255,31 @@ const encryptSymmetricHelper = async ({
|
|||||||
* @param {String} obj.tag - tag
|
* @param {String} obj.tag - tag
|
||||||
*/
|
*/
|
||||||
const decryptSymmetricHelper = async ({
|
const decryptSymmetricHelper = async ({
|
||||||
workspaceId,
|
workspaceId,
|
||||||
|
ciphertext,
|
||||||
|
iv,
|
||||||
|
tag,
|
||||||
|
}: {
|
||||||
|
workspaceId: Types.ObjectId;
|
||||||
|
ciphertext: string;
|
||||||
|
iv: string;
|
||||||
|
tag: string;
|
||||||
|
}) => {
|
||||||
|
const key = await getKey({ workspaceId: workspaceId.toString() });
|
||||||
|
const plaintext = decryptSymmetric({
|
||||||
ciphertext,
|
ciphertext,
|
||||||
iv,
|
iv,
|
||||||
tag
|
tag,
|
||||||
}: {
|
key,
|
||||||
workspaceId: Types.ObjectId;
|
});
|
||||||
ciphertext: string;
|
|
||||||
iv: string;
|
return plaintext;
|
||||||
tag: string;
|
};
|
||||||
}) => {
|
|
||||||
let plaintext;
|
|
||||||
try {
|
|
||||||
const key = await getKey({ workspaceId });
|
|
||||||
const plaintext = decryptSymmetric({
|
|
||||||
ciphertext,
|
|
||||||
iv,
|
|
||||||
tag,
|
|
||||||
key
|
|
||||||
});
|
|
||||||
|
|
||||||
return plaintext;
|
|
||||||
} catch (err) {
|
|
||||||
Sentry.setUser(null);
|
|
||||||
Sentry.captureException(err);
|
|
||||||
throw new Error('Failed to perform symmetric decryption with bot');
|
|
||||||
}
|
|
||||||
|
|
||||||
return plaintext;
|
|
||||||
}
|
|
||||||
|
|
||||||
export {
|
export {
|
||||||
validateClientForBot,
|
validateClientForBot,
|
||||||
createBot,
|
createBot,
|
||||||
getSecretsHelper,
|
getSecretsHelper,
|
||||||
encryptSymmetricHelper,
|
encryptSymmetricHelper,
|
||||||
decryptSymmetricHelper
|
decryptSymmetricHelper,
|
||||||
}
|
};
|
||||||
|
|||||||
@@ -1,14 +1,13 @@
|
|||||||
import { Types } from 'mongoose';
|
import { Types } from "mongoose";
|
||||||
import * as Sentry from '@sentry/node';
|
import { Bot, IBot } from "../models";
|
||||||
import { Bot, IBot } from '../models';
|
import { EVENT_PUSH_SECRETS } from "../variables";
|
||||||
import { EVENT_PUSH_SECRETS } from '../variables';
|
import { IntegrationService } from "../services";
|
||||||
import { IntegrationService } from '../services';
|
|
||||||
|
|
||||||
interface Event {
|
interface Event {
|
||||||
name: string;
|
name: string;
|
||||||
workspaceId: Types.ObjectId;
|
workspaceId: Types.ObjectId;
|
||||||
environment?: string;
|
environment?: string;
|
||||||
payload: any;
|
payload: any;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -19,39 +18,25 @@ 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 ({
|
const handleEventHelper = async ({ event }: { event: Event }) => {
|
||||||
event
|
const { workspaceId, environment } = event;
|
||||||
}: {
|
|
||||||
event: Event;
|
|
||||||
}) => {
|
|
||||||
const {
|
|
||||||
workspaceId,
|
|
||||||
environment
|
|
||||||
} = event;
|
|
||||||
|
|
||||||
// TODO: moduralize bot check into separate function
|
|
||||||
const bot = await Bot.findOne({
|
|
||||||
workspace: workspaceId,
|
|
||||||
isActive: true
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!bot) return;
|
|
||||||
|
|
||||||
try {
|
|
||||||
switch (event.name) {
|
|
||||||
case EVENT_PUSH_SECRETS:
|
|
||||||
IntegrationService.syncIntegrations({
|
|
||||||
workspaceId,
|
|
||||||
environment
|
|
||||||
});
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
} catch (err) {
|
|
||||||
Sentry.setUser(null);
|
|
||||||
Sentry.captureException(err);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export {
|
// TODO: moduralize bot check into separate function
|
||||||
handleEventHelper
|
const bot = await Bot.findOne({
|
||||||
}
|
workspace: workspaceId,
|
||||||
|
isActive: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!bot) return;
|
||||||
|
|
||||||
|
switch (event.name) {
|
||||||
|
case EVENT_PUSH_SECRETS:
|
||||||
|
IntegrationService.syncIntegrations({
|
||||||
|
workspaceId,
|
||||||
|
environment,
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export { handleEventHelper };
|
||||||
|
|||||||
@@ -256,7 +256,7 @@ const syncIntegrationsHelper = async ({
|
|||||||
integration,
|
integration,
|
||||||
integrationAuth,
|
integrationAuth,
|
||||||
secrets,
|
secrets,
|
||||||
accessId: access.accessId,
|
accessId: access.accessId === undefined ? null : access.accessId,
|
||||||
accessToken: access.accessToken
|
accessToken: access.accessToken
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -482,4 +482,4 @@ export {
|
|||||||
getIntegrationAuthAccessHelper,
|
getIntegrationAuthAccessHelper,
|
||||||
setIntegrationAuthRefreshHelper,
|
setIntegrationAuthRefreshHelper,
|
||||||
setIntegrationAuthAccessHelper
|
setIntegrationAuthAccessHelper
|
||||||
}
|
}
|
||||||
|
|||||||
+22
-29
@@ -1,4 +1,3 @@
|
|||||||
import * as Sentry from '@sentry/node';
|
|
||||||
import { Key, IKey } from '../models';
|
import { Key, IKey } from '../models';
|
||||||
|
|
||||||
interface Key {
|
interface Key {
|
||||||
@@ -27,36 +26,30 @@ const pushKeys = async ({
|
|||||||
workspaceId: string;
|
workspaceId: string;
|
||||||
keys: Key[];
|
keys: Key[];
|
||||||
}): Promise<void> => {
|
}): Promise<void> => {
|
||||||
try {
|
// filter out already-inserted keys
|
||||||
// filter out already-inserted keys
|
const keysSet = new Set(
|
||||||
const keysSet = new Set(
|
(
|
||||||
(
|
await Key.find(
|
||||||
await Key.find(
|
{
|
||||||
{
|
workspace: workspaceId
|
||||||
workspace: workspaceId
|
},
|
||||||
},
|
'receiver'
|
||||||
'receiver'
|
)
|
||||||
)
|
).map((k: IKey) => k.receiver.toString())
|
||||||
).map((k: IKey) => k.receiver.toString())
|
);
|
||||||
);
|
|
||||||
|
|
||||||
keys = keys.filter((key) => !keysSet.has(key.userId));
|
keys = keys.filter((key) => !keysSet.has(key.userId));
|
||||||
|
|
||||||
// add new shared keys only
|
// add new shared keys only
|
||||||
await Key.insertMany(
|
await Key.insertMany(
|
||||||
keys.map((k) => ({
|
keys.map((k) => ({
|
||||||
encryptedKey: k.encryptedKey,
|
encryptedKey: k.encryptedKey,
|
||||||
nonce: k.nonce,
|
nonce: k.nonce,
|
||||||
sender: userId,
|
sender: userId,
|
||||||
receiver: k.userId,
|
receiver: k.userId,
|
||||||
workspace: workspaceId
|
workspace: workspaceId
|
||||||
}))
|
}))
|
||||||
);
|
);
|
||||||
} catch (err) {
|
|
||||||
Sentry.setUser(null);
|
|
||||||
Sentry.captureException(err);
|
|
||||||
throw new Error('Failed to push access keys');
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export { pushKeys };
|
export { pushKeys };
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
import * as Sentry from '@sentry/node';
|
|
||||||
import { Types } from 'mongoose';
|
import { Types } from 'mongoose';
|
||||||
import {
|
import {
|
||||||
MembershipOrg,
|
MembershipOrg,
|
||||||
@@ -144,15 +143,7 @@ const validateMembershipOrg = async ({
|
|||||||
* @return {Object} membershipOrg - membership
|
* @return {Object} membershipOrg - membership
|
||||||
*/
|
*/
|
||||||
const findMembershipOrg = (queryObj: any) => {
|
const findMembershipOrg = (queryObj: any) => {
|
||||||
let membershipOrg;
|
const membershipOrg = MembershipOrg.findOne(queryObj);
|
||||||
try {
|
|
||||||
membershipOrg = MembershipOrg.findOne(queryObj);
|
|
||||||
} catch (err) {
|
|
||||||
Sentry.setUser(null);
|
|
||||||
Sentry.captureException(err);
|
|
||||||
throw new Error('Failed to find organization membership');
|
|
||||||
}
|
|
||||||
|
|
||||||
return membershipOrg;
|
return membershipOrg;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -175,33 +166,27 @@ const addMembershipsOrg = async ({
|
|||||||
roles: string[];
|
roles: string[];
|
||||||
statuses: string[];
|
statuses: string[];
|
||||||
}) => {
|
}) => {
|
||||||
try {
|
const operations = userIds.map((userId, idx) => {
|
||||||
const operations = userIds.map((userId, idx) => {
|
return {
|
||||||
return {
|
updateOne: {
|
||||||
updateOne: {
|
filter: {
|
||||||
filter: {
|
user: userId,
|
||||||
user: userId,
|
organization: organizationId,
|
||||||
organization: organizationId,
|
role: roles[idx],
|
||||||
role: roles[idx],
|
status: statuses[idx]
|
||||||
status: statuses[idx]
|
},
|
||||||
},
|
update: {
|
||||||
update: {
|
user: userId,
|
||||||
user: userId,
|
organization: organizationId,
|
||||||
organization: organizationId,
|
role: roles[idx],
|
||||||
role: roles[idx],
|
status: statuses[idx]
|
||||||
status: statuses[idx]
|
},
|
||||||
},
|
upsert: true
|
||||||
upsert: true
|
}
|
||||||
}
|
};
|
||||||
};
|
});
|
||||||
});
|
|
||||||
|
|
||||||
await MembershipOrg.bulkWrite(operations as any);
|
await MembershipOrg.bulkWrite(operations as any);
|
||||||
} catch (err) {
|
|
||||||
Sentry.setUser(null);
|
|
||||||
Sentry.captureException(err);
|
|
||||||
throw new Error('Failed to add users to organization');
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -214,43 +199,36 @@ const deleteMembershipOrg = async ({
|
|||||||
}: {
|
}: {
|
||||||
membershipOrgId: string;
|
membershipOrgId: string;
|
||||||
}) => {
|
}) => {
|
||||||
let deletedMembershipOrg;
|
const deletedMembershipOrg = await MembershipOrg.findOneAndDelete({
|
||||||
try {
|
_id: membershipOrgId
|
||||||
deletedMembershipOrg = await MembershipOrg.findOneAndDelete({
|
});
|
||||||
_id: membershipOrgId
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!deletedMembershipOrg) throw new Error('Failed to delete organization membership');
|
if (!deletedMembershipOrg) throw new Error('Failed to delete organization membership');
|
||||||
|
|
||||||
// delete keys associated with organization membership
|
// delete keys associated with organization membership
|
||||||
if (deletedMembershipOrg?.user) {
|
if (deletedMembershipOrg?.user) {
|
||||||
// case: organization membership had a registered user
|
// case: organization membership had a registered user
|
||||||
|
|
||||||
const workspaces = (
|
const workspaces = (
|
||||||
await Workspace.find({
|
await Workspace.find({
|
||||||
organization: deletedMembershipOrg.organization
|
organization: deletedMembershipOrg.organization
|
||||||
})
|
})
|
||||||
).map((w) => w._id.toString());
|
).map((w) => w._id.toString());
|
||||||
|
|
||||||
await Membership.deleteMany({
|
await Membership.deleteMany({
|
||||||
user: deletedMembershipOrg.user,
|
user: deletedMembershipOrg.user,
|
||||||
workspace: {
|
workspace: {
|
||||||
$in: workspaces
|
$in: workspaces
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
await Key.deleteMany({
|
await Key.deleteMany({
|
||||||
receiver: deletedMembershipOrg.user,
|
receiver: deletedMembershipOrg.user,
|
||||||
workspace: {
|
workspace: {
|
||||||
$in: workspaces
|
$in: workspaces
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
} catch (err) {
|
|
||||||
Sentry.setUser(null);
|
|
||||||
Sentry.captureException(err);
|
|
||||||
throw new Error('Failed to delete organization membership');
|
|
||||||
}
|
|
||||||
|
|
||||||
return deletedMembershipOrg;
|
return deletedMembershipOrg;
|
||||||
};
|
};
|
||||||
|
|||||||
+197
-207
@@ -1,39 +1,34 @@
|
|||||||
import * as Sentry from '@sentry/node';
|
import Stripe from "stripe";
|
||||||
import Stripe from 'stripe';
|
import { Types } from "mongoose";
|
||||||
import { Types } from 'mongoose';
|
|
||||||
import {
|
import {
|
||||||
IUser,
|
IUser,
|
||||||
User,
|
User,
|
||||||
IServiceAccount,
|
IServiceAccount,
|
||||||
ServiceAccount,
|
ServiceAccount,
|
||||||
IServiceTokenData,
|
IServiceTokenData,
|
||||||
ServiceTokenData
|
ServiceTokenData,
|
||||||
} from '../models';
|
} from "../models";
|
||||||
import { Organization, MembershipOrg } from '../models';
|
import { Organization, MembershipOrg } from "../models";
|
||||||
import {
|
|
||||||
ACCEPTED,
|
|
||||||
AUTH_MODE_JWT,
|
|
||||||
AUTH_MODE_SERVICE_ACCOUNT,
|
|
||||||
AUTH_MODE_SERVICE_TOKEN,
|
|
||||||
AUTH_MODE_API_KEY,
|
|
||||||
OWNER
|
|
||||||
} from '../variables';
|
|
||||||
import {
|
|
||||||
getStripeSecretKey,
|
|
||||||
getStripeProductPro,
|
|
||||||
getStripeProductTeam,
|
|
||||||
getStripeProductStarter
|
|
||||||
} from '../config';
|
|
||||||
import {
|
import {
|
||||||
UnauthorizedRequestError,
|
ACCEPTED,
|
||||||
OrganizationNotFoundError
|
AUTH_MODE_JWT,
|
||||||
} from '../utils/errors';
|
AUTH_MODE_SERVICE_ACCOUNT,
|
||||||
|
AUTH_MODE_SERVICE_TOKEN,
|
||||||
|
AUTH_MODE_API_KEY,
|
||||||
|
OWNER,
|
||||||
|
} from "../variables";
|
||||||
import {
|
import {
|
||||||
validateUserClientForOrganization
|
getStripeSecretKey,
|
||||||
} from '../helpers/user';
|
getStripeProductPro,
|
||||||
|
getStripeProductTeam,
|
||||||
|
getStripeProductStarter,
|
||||||
|
} from "../config";
|
||||||
import {
|
import {
|
||||||
validateServiceAccountClientForOrganization
|
UnauthorizedRequestError,
|
||||||
} from '../helpers/serviceAccount';
|
OrganizationNotFoundError,
|
||||||
|
} from "../utils/errors";
|
||||||
|
import { validateUserClientForOrganization } from "../helpers/user";
|
||||||
|
import { validateServiceAccountClientForOrganization } from "../helpers/serviceAccount";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Validate accepted clients for organization with id [organizationId]
|
* Validate accepted clients for organization with id [organizationId]
|
||||||
@@ -42,69 +37,80 @@ import {
|
|||||||
* @param {Types.ObjectId} obj.organizationId - id of organization to validate against
|
* @param {Types.ObjectId} obj.organizationId - id of organization to validate against
|
||||||
*/
|
*/
|
||||||
const validateClientForOrganization = async ({
|
const validateClientForOrganization = async ({
|
||||||
authData,
|
authData,
|
||||||
organizationId,
|
organizationId,
|
||||||
acceptedRoles,
|
acceptedRoles,
|
||||||
acceptedStatuses
|
acceptedStatuses,
|
||||||
}: {
|
}: {
|
||||||
authData: {
|
authData: {
|
||||||
authMode: string;
|
authMode: string;
|
||||||
authPayload: IUser | IServiceAccount | IServiceTokenData;
|
authPayload: IUser | IServiceAccount | IServiceTokenData;
|
||||||
},
|
};
|
||||||
organizationId: Types.ObjectId;
|
organizationId: Types.ObjectId;
|
||||||
acceptedRoles: Array<'owner' | 'admin' | 'member'>;
|
acceptedRoles: Array<"owner" | "admin" | "member">;
|
||||||
acceptedStatuses: Array<'invited' | 'accepted'>;
|
acceptedStatuses: Array<"invited" | "accepted">;
|
||||||
}) => {
|
}) => {
|
||||||
|
const organization = await Organization.findById(organizationId);
|
||||||
const organization = await Organization.findById(organizationId);
|
|
||||||
|
|
||||||
if (!organization) {
|
|
||||||
throw OrganizationNotFoundError({
|
|
||||||
message: 'Failed to find organization'
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (authData.authMode === AUTH_MODE_JWT && authData.authPayload instanceof User) {
|
|
||||||
const membershipOrg = await validateUserClientForOrganization({
|
|
||||||
user: authData.authPayload,
|
|
||||||
organization,
|
|
||||||
acceptedRoles,
|
|
||||||
acceptedStatuses
|
|
||||||
});
|
|
||||||
|
|
||||||
return ({ organization, membershipOrg });
|
|
||||||
}
|
|
||||||
|
|
||||||
if (authData.authMode === AUTH_MODE_SERVICE_ACCOUNT && authData.authPayload instanceof ServiceAccount) {
|
if (!organization) {
|
||||||
await validateServiceAccountClientForOrganization({
|
throw OrganizationNotFoundError({
|
||||||
serviceAccount: authData.authPayload,
|
message: "Failed to find organization",
|
||||||
organization
|
});
|
||||||
});
|
}
|
||||||
|
|
||||||
return ({ organization });
|
|
||||||
}
|
|
||||||
|
|
||||||
if (authData.authMode === AUTH_MODE_SERVICE_TOKEN && authData.authPayload instanceof ServiceTokenData) {
|
if (
|
||||||
throw UnauthorizedRequestError({
|
authData.authMode === AUTH_MODE_JWT &&
|
||||||
message: 'Failed service token authorization for organization'
|
authData.authPayload instanceof User
|
||||||
});
|
) {
|
||||||
}
|
const membershipOrg = await validateUserClientForOrganization({
|
||||||
|
user: authData.authPayload,
|
||||||
|
organization,
|
||||||
|
acceptedRoles,
|
||||||
|
acceptedStatuses,
|
||||||
|
});
|
||||||
|
|
||||||
if (authData.authMode === AUTH_MODE_API_KEY && authData.authPayload instanceof User) {
|
return { organization, membershipOrg };
|
||||||
const membershipOrg = await validateUserClientForOrganization({
|
}
|
||||||
user: authData.authPayload,
|
|
||||||
organization,
|
if (
|
||||||
acceptedRoles,
|
authData.authMode === AUTH_MODE_SERVICE_ACCOUNT &&
|
||||||
acceptedStatuses
|
authData.authPayload instanceof ServiceAccount
|
||||||
});
|
) {
|
||||||
|
await validateServiceAccountClientForOrganization({
|
||||||
return ({ organization, membershipOrg });
|
serviceAccount: authData.authPayload,
|
||||||
}
|
organization,
|
||||||
|
});
|
||||||
throw UnauthorizedRequestError({
|
|
||||||
message: 'Failed client authorization for organization'
|
return { organization };
|
||||||
});
|
}
|
||||||
}
|
|
||||||
|
if (
|
||||||
|
authData.authMode === AUTH_MODE_SERVICE_TOKEN &&
|
||||||
|
authData.authPayload instanceof ServiceTokenData
|
||||||
|
) {
|
||||||
|
throw UnauthorizedRequestError({
|
||||||
|
message: "Failed service token authorization for organization",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
authData.authMode === AUTH_MODE_API_KEY &&
|
||||||
|
authData.authPayload instanceof User
|
||||||
|
) {
|
||||||
|
const membershipOrg = await validateUserClientForOrganization({
|
||||||
|
user: authData.authPayload,
|
||||||
|
organization,
|
||||||
|
acceptedRoles,
|
||||||
|
acceptedStatuses,
|
||||||
|
});
|
||||||
|
|
||||||
|
return { organization, membershipOrg };
|
||||||
|
}
|
||||||
|
|
||||||
|
throw UnauthorizedRequestError({
|
||||||
|
message: "Failed client authorization for organization",
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Create an organization with name [name]
|
* Create an organization with name [name]
|
||||||
@@ -114,43 +120,37 @@ const validateClientForOrganization = async ({
|
|||||||
* @param {Object} organization - new organization
|
* @param {Object} organization - new organization
|
||||||
*/
|
*/
|
||||||
const createOrganization = async ({
|
const createOrganization = async ({
|
||||||
name,
|
name,
|
||||||
email
|
email,
|
||||||
}: {
|
}: {
|
||||||
name: string;
|
name: string;
|
||||||
email: string;
|
email: string;
|
||||||
}) => {
|
}) => {
|
||||||
let organization;
|
let organization;
|
||||||
try {
|
// register stripe account
|
||||||
// register stripe account
|
const stripe = new Stripe(await getStripeSecretKey(), {
|
||||||
const stripe = new Stripe(await getStripeSecretKey(), {
|
apiVersion: "2022-08-01",
|
||||||
apiVersion: '2022-08-01'
|
});
|
||||||
});
|
|
||||||
|
|
||||||
if (await getStripeSecretKey()) {
|
if (await getStripeSecretKey()) {
|
||||||
const customer = await stripe.customers.create({
|
const customer = await stripe.customers.create({
|
||||||
email,
|
email,
|
||||||
description: name
|
description: name,
|
||||||
});
|
});
|
||||||
|
|
||||||
organization = await new Organization({
|
organization = await new Organization({
|
||||||
name,
|
name,
|
||||||
customerId: customer.id
|
customerId: customer.id,
|
||||||
}).save();
|
}).save();
|
||||||
} else {
|
} else {
|
||||||
organization = await new Organization({
|
organization = await new Organization({
|
||||||
name
|
name,
|
||||||
}).save();
|
}).save();
|
||||||
}
|
}
|
||||||
|
|
||||||
await initSubscriptionOrg({ organizationId: organization._id });
|
await initSubscriptionOrg({ organizationId: organization._id });
|
||||||
} catch (err) {
|
|
||||||
Sentry.setUser({ email });
|
|
||||||
Sentry.captureException(err);
|
|
||||||
throw new Error(`Failed to create organization [err=${err}]`);
|
|
||||||
}
|
|
||||||
|
|
||||||
return organization;
|
return organization;
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -162,57 +162,52 @@ const createOrganization = async ({
|
|||||||
* @return {Subscription} obj.subscription - new subscription
|
* @return {Subscription} obj.subscription - new subscription
|
||||||
*/
|
*/
|
||||||
const initSubscriptionOrg = async ({
|
const initSubscriptionOrg = async ({
|
||||||
organizationId
|
organizationId,
|
||||||
}: {
|
}: {
|
||||||
organizationId: Types.ObjectId;
|
organizationId: Types.ObjectId;
|
||||||
}) => {
|
}) => {
|
||||||
let stripeSubscription;
|
let stripeSubscription;
|
||||||
let subscription;
|
let subscription;
|
||||||
try {
|
|
||||||
// find organization
|
|
||||||
const organization = await Organization.findOne({
|
|
||||||
_id: organizationId
|
|
||||||
});
|
|
||||||
|
|
||||||
if (organization) {
|
// find organization
|
||||||
if (organization.customerId) {
|
const organization = await Organization.findOne({
|
||||||
// initialize starter subscription with quantity of 0
|
_id: organizationId,
|
||||||
const stripe = new Stripe(await getStripeSecretKey(), {
|
});
|
||||||
apiVersion: '2022-08-01'
|
|
||||||
});
|
|
||||||
|
|
||||||
const productToPriceMap = {
|
if (organization) {
|
||||||
starter: await getStripeProductStarter(),
|
if (organization.customerId) {
|
||||||
team: await getStripeProductTeam(),
|
// initialize starter subscription with quantity of 0
|
||||||
pro: await getStripeProductPro()
|
const stripe = new Stripe(await getStripeSecretKey(), {
|
||||||
};
|
apiVersion: "2022-08-01",
|
||||||
|
});
|
||||||
|
|
||||||
stripeSubscription = await stripe.subscriptions.create({
|
const productToPriceMap = {
|
||||||
customer: organization.customerId,
|
starter: await getStripeProductStarter(),
|
||||||
items: [
|
team: await getStripeProductTeam(),
|
||||||
{
|
pro: await getStripeProductPro(),
|
||||||
price: productToPriceMap['starter'],
|
};
|
||||||
quantity: 1
|
|
||||||
}
|
|
||||||
],
|
|
||||||
payment_behavior: 'default_incomplete',
|
|
||||||
proration_behavior: 'none',
|
|
||||||
expand: ['latest_invoice.payment_intent']
|
|
||||||
});
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
throw new Error('Failed to initialize free organization subscription');
|
|
||||||
}
|
|
||||||
} catch (err) {
|
|
||||||
Sentry.setUser(null);
|
|
||||||
Sentry.captureException(err);
|
|
||||||
throw new Error('Failed to initialize free organization subscription');
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
stripeSubscription = await stripe.subscriptions.create({
|
||||||
stripeSubscription,
|
customer: organization.customerId,
|
||||||
subscription
|
items: [
|
||||||
};
|
{
|
||||||
|
price: productToPriceMap["starter"],
|
||||||
|
quantity: 1,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
payment_behavior: "default_incomplete",
|
||||||
|
proration_behavior: "none",
|
||||||
|
expand: ["latest_invoice.payment_intent"],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
throw new Error("Failed to initialize free organization subscription");
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
stripeSubscription,
|
||||||
|
subscription,
|
||||||
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -222,54 +217,49 @@ const initSubscriptionOrg = async ({
|
|||||||
* @param {Number} obj.organizationId - id of subscription's organization
|
* @param {Number} obj.organizationId - id of subscription's organization
|
||||||
*/
|
*/
|
||||||
const updateSubscriptionOrgQuantity = async ({
|
const updateSubscriptionOrgQuantity = async ({
|
||||||
organizationId
|
organizationId,
|
||||||
}: {
|
}: {
|
||||||
organizationId: string;
|
organizationId: string;
|
||||||
}) => {
|
}) => {
|
||||||
let stripeSubscription;
|
let stripeSubscription;
|
||||||
try {
|
// find organization
|
||||||
// find organization
|
const organization = await Organization.findOne({
|
||||||
const organization = await Organization.findOne({
|
_id: organizationId,
|
||||||
_id: organizationId
|
});
|
||||||
});
|
|
||||||
|
|
||||||
if (organization && organization.customerId) {
|
if (organization && organization.customerId) {
|
||||||
const quantity = await MembershipOrg.countDocuments({
|
const quantity = await MembershipOrg.countDocuments({
|
||||||
organization: organizationId,
|
organization: organizationId,
|
||||||
status: ACCEPTED
|
status: ACCEPTED,
|
||||||
});
|
});
|
||||||
|
|
||||||
const stripe = new Stripe(await getStripeSecretKey(), {
|
const stripe = new Stripe(await getStripeSecretKey(), {
|
||||||
apiVersion: '2022-08-01'
|
apiVersion: "2022-08-01",
|
||||||
});
|
});
|
||||||
|
|
||||||
const subscription = (
|
const subscription = (
|
||||||
await stripe.subscriptions.list({
|
await stripe.subscriptions.list({
|
||||||
customer: organization.customerId
|
customer: organization.customerId,
|
||||||
})
|
})
|
||||||
).data[0];
|
).data[0];
|
||||||
|
|
||||||
stripeSubscription = await stripe.subscriptions.update(subscription.id, {
|
stripeSubscription = await stripe.subscriptions.update(subscription.id, {
|
||||||
items: [
|
items: [
|
||||||
{
|
{
|
||||||
id: subscription.items.data[0].id,
|
id: subscription.items.data[0].id,
|
||||||
price: subscription.items.data[0].price.id,
|
price: subscription.items.data[0].price.id,
|
||||||
quantity
|
quantity,
|
||||||
}
|
},
|
||||||
]
|
],
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
} catch (err) {
|
|
||||||
Sentry.setUser(null);
|
|
||||||
Sentry.captureException(err);
|
|
||||||
}
|
|
||||||
|
|
||||||
return stripeSubscription;
|
return stripeSubscription;
|
||||||
};
|
};
|
||||||
|
|
||||||
export {
|
export {
|
||||||
validateClientForOrganization,
|
validateClientForOrganization,
|
||||||
createOrganization,
|
createOrganization,
|
||||||
initSubscriptionOrg,
|
initSubscriptionOrg,
|
||||||
updateSubscriptionOrgQuantity
|
updateSubscriptionOrgQuantity,
|
||||||
};
|
};
|
||||||
|
|||||||
+505
-538
File diff suppressed because it is too large
Load Diff
+183
-181
@@ -1,16 +1,15 @@
|
|||||||
import * as Sentry from '@sentry/node';
|
import { Types } from "mongoose";
|
||||||
import { Types } from 'mongoose';
|
import { TokenData } from "../models";
|
||||||
import { TokenData } from '../models';
|
import crypto from "crypto";
|
||||||
import crypto from 'crypto';
|
import bcrypt from "bcrypt";
|
||||||
import bcrypt from 'bcrypt';
|
|
||||||
import {
|
import {
|
||||||
TOKEN_EMAIL_CONFIRMATION,
|
TOKEN_EMAIL_CONFIRMATION,
|
||||||
TOKEN_EMAIL_MFA,
|
TOKEN_EMAIL_MFA,
|
||||||
TOKEN_EMAIL_ORG_INVITATION,
|
TOKEN_EMAIL_ORG_INVITATION,
|
||||||
TOKEN_EMAIL_PASSWORD_RESET
|
TOKEN_EMAIL_PASSWORD_RESET,
|
||||||
} from '../variables';
|
} from "../variables";
|
||||||
import { UnauthorizedRequestError } from '../utils/errors';
|
import { UnauthorizedRequestError } from "../utils/errors";
|
||||||
import { getSaltRounds } from '../config';
|
import { getSaltRounds } from "../config";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Create and store a token in the database for purpose [type]
|
* Create and store a token in the database for purpose [type]
|
||||||
@@ -22,194 +21,197 @@ import { getSaltRounds } from '../config';
|
|||||||
* @returns {String} token - the created token
|
* @returns {String} token - the created token
|
||||||
*/
|
*/
|
||||||
const createTokenHelper = async ({
|
const createTokenHelper = async ({
|
||||||
type,
|
type,
|
||||||
email,
|
email,
|
||||||
phoneNumber,
|
phoneNumber,
|
||||||
organizationId
|
organizationId,
|
||||||
}: {
|
}: {
|
||||||
type: 'emailConfirmation' | 'emailMfa' | 'organizationInvitation' | 'passwordReset';
|
type:
|
||||||
|
| "emailConfirmation"
|
||||||
|
| "emailMfa"
|
||||||
|
| "organizationInvitation"
|
||||||
|
| "passwordReset";
|
||||||
|
email?: string;
|
||||||
|
phoneNumber?: string;
|
||||||
|
organizationId?: Types.ObjectId;
|
||||||
|
}) => {
|
||||||
|
let token, expiresAt, triesLeft;
|
||||||
|
// generate random token based on specified token use-case
|
||||||
|
// type [type]
|
||||||
|
switch (type) {
|
||||||
|
case TOKEN_EMAIL_CONFIRMATION:
|
||||||
|
// generate random 6-digit code
|
||||||
|
token = String(crypto.randomInt(Math.pow(10, 5), Math.pow(10, 6) - 1));
|
||||||
|
expiresAt = new Date(new Date().getTime() + 86400000);
|
||||||
|
break;
|
||||||
|
case TOKEN_EMAIL_MFA:
|
||||||
|
// generate random 6-digit code
|
||||||
|
token = String(crypto.randomInt(Math.pow(10, 5), Math.pow(10, 6) - 1));
|
||||||
|
triesLeft = 5;
|
||||||
|
expiresAt = new Date(new Date().getTime() + 300000);
|
||||||
|
break;
|
||||||
|
case TOKEN_EMAIL_ORG_INVITATION:
|
||||||
|
// generate random hex
|
||||||
|
token = crypto.randomBytes(16).toString("hex");
|
||||||
|
expiresAt = new Date(new Date().getTime() + 259200000);
|
||||||
|
break;
|
||||||
|
case TOKEN_EMAIL_PASSWORD_RESET:
|
||||||
|
// generate random hex
|
||||||
|
token = crypto.randomBytes(16).toString("hex");
|
||||||
|
expiresAt = new Date(new Date().getTime() + 86400000);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
token = crypto.randomBytes(16).toString("hex");
|
||||||
|
expiresAt = new Date();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface TokenDataQuery {
|
||||||
|
type: string;
|
||||||
email?: string;
|
email?: string;
|
||||||
phoneNumber?: string;
|
phoneNumber?: string;
|
||||||
organizationId?: Types.ObjectId
|
organization?: Types.ObjectId;
|
||||||
}) => {
|
}
|
||||||
let token, expiresAt, triesLeft;
|
|
||||||
try {
|
|
||||||
// generate random token based on specified token use-case
|
|
||||||
// type [type]
|
|
||||||
switch (type) {
|
|
||||||
case TOKEN_EMAIL_CONFIRMATION:
|
|
||||||
// generate random 6-digit code
|
|
||||||
token = String(crypto.randomInt(Math.pow(10, 5), Math.pow(10, 6) - 1));
|
|
||||||
expiresAt = new Date((new Date()).getTime() + 86400000);
|
|
||||||
break;
|
|
||||||
case TOKEN_EMAIL_MFA:
|
|
||||||
// generate random 6-digit code
|
|
||||||
token = String(crypto.randomInt(Math.pow(10, 5), Math.pow(10, 6) - 1));
|
|
||||||
triesLeft = 5;
|
|
||||||
expiresAt = new Date((new Date()).getTime() + 300000);
|
|
||||||
break;
|
|
||||||
case TOKEN_EMAIL_ORG_INVITATION:
|
|
||||||
// generate random hex
|
|
||||||
token = crypto.randomBytes(16).toString('hex');
|
|
||||||
expiresAt = new Date((new Date()).getTime() + 259200000);
|
|
||||||
break;
|
|
||||||
case TOKEN_EMAIL_PASSWORD_RESET:
|
|
||||||
// generate random hex
|
|
||||||
token = crypto.randomBytes(16).toString('hex');
|
|
||||||
expiresAt = new Date((new Date()).getTime() + 86400000);
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
token = crypto.randomBytes(16).toString('hex');
|
|
||||||
expiresAt = new Date();
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface TokenDataQuery {
|
|
||||||
type: string;
|
|
||||||
email?: string;
|
|
||||||
phoneNumber?: string;
|
|
||||||
organization?: Types.ObjectId;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface TokenDataUpdate {
|
|
||||||
type: string;
|
|
||||||
email?: string;
|
|
||||||
phoneNumber?: string;
|
|
||||||
organization?: Types.ObjectId;
|
|
||||||
tokenHash: string;
|
|
||||||
triesLeft?: number;
|
|
||||||
expiresAt: Date;
|
|
||||||
}
|
|
||||||
|
|
||||||
const query: TokenDataQuery = { type };
|
interface TokenDataUpdate {
|
||||||
const update: TokenDataUpdate = {
|
type: string;
|
||||||
type,
|
email?: string;
|
||||||
tokenHash: await bcrypt.hash(token, await getSaltRounds()),
|
phoneNumber?: string;
|
||||||
expiresAt
|
organization?: Types.ObjectId;
|
||||||
}
|
tokenHash: string;
|
||||||
|
triesLeft?: number;
|
||||||
|
expiresAt: Date;
|
||||||
|
}
|
||||||
|
|
||||||
if (email) {
|
const query: TokenDataQuery = { type };
|
||||||
query.email = email;
|
const update: TokenDataUpdate = {
|
||||||
update.email = email;
|
type,
|
||||||
}
|
tokenHash: await bcrypt.hash(token, await getSaltRounds()),
|
||||||
if (phoneNumber) {
|
expiresAt,
|
||||||
query.phoneNumber = phoneNumber;
|
};
|
||||||
update.phoneNumber = phoneNumber;
|
|
||||||
}
|
if (email) {
|
||||||
if (organizationId) {
|
query.email = email;
|
||||||
query.organization = organizationId
|
update.email = email;
|
||||||
update.organization = organizationId
|
}
|
||||||
}
|
if (phoneNumber) {
|
||||||
|
query.phoneNumber = phoneNumber;
|
||||||
if (triesLeft) {
|
update.phoneNumber = phoneNumber;
|
||||||
update.triesLeft = triesLeft;
|
}
|
||||||
}
|
if (organizationId) {
|
||||||
|
query.organization = organizationId;
|
||||||
await TokenData.findOneAndUpdate(
|
update.organization = organizationId;
|
||||||
query,
|
}
|
||||||
update,
|
|
||||||
{
|
if (triesLeft) {
|
||||||
new: true,
|
update.triesLeft = triesLeft;
|
||||||
upsert: true
|
}
|
||||||
}
|
|
||||||
);
|
await TokenData.findOneAndUpdate(query, update, {
|
||||||
} catch (err) {
|
new: true,
|
||||||
Sentry.setUser(null);
|
upsert: true,
|
||||||
Sentry.captureException(err);
|
});
|
||||||
throw new Error(
|
|
||||||
"Failed to create token"
|
return token;
|
||||||
);
|
};
|
||||||
}
|
|
||||||
|
|
||||||
return token;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
*
|
*
|
||||||
* @param {Object} obj
|
* @param {Object} obj
|
||||||
* @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 ({
|
const validateTokenHelper = async ({
|
||||||
type,
|
type,
|
||||||
email,
|
email,
|
||||||
phoneNumber,
|
phoneNumber,
|
||||||
organizationId,
|
organizationId,
|
||||||
token
|
token,
|
||||||
}: {
|
}: {
|
||||||
type: 'emailConfirmation' | 'emailMfa' | 'organizationInvitation' | 'passwordReset';
|
type:
|
||||||
|
| "emailConfirmation"
|
||||||
|
| "emailMfa"
|
||||||
|
| "organizationInvitation"
|
||||||
|
| "passwordReset";
|
||||||
|
email?: string;
|
||||||
|
phoneNumber?: string;
|
||||||
|
organizationId?: Types.ObjectId;
|
||||||
|
token: string;
|
||||||
|
}) => {
|
||||||
|
interface Query {
|
||||||
|
type: string;
|
||||||
email?: string;
|
email?: string;
|
||||||
phoneNumber?: string;
|
phoneNumber?: string;
|
||||||
organizationId?: Types.ObjectId;
|
organization?: Types.ObjectId;
|
||||||
token: string;
|
}
|
||||||
}) => {
|
|
||||||
interface Query {
|
|
||||||
type: string;
|
|
||||||
email?: string;
|
|
||||||
phoneNumber?: string;
|
|
||||||
organization?: Types.ObjectId;
|
|
||||||
}
|
|
||||||
|
|
||||||
const query: Query = { type };
|
const query: Query = { type };
|
||||||
|
|
||||||
if (email) { query.email = email; }
|
if (email) {
|
||||||
if (phoneNumber) { query.phoneNumber = phoneNumber; }
|
query.email = email;
|
||||||
if (organizationId) { query.organization = organizationId; }
|
}
|
||||||
|
if (phoneNumber) {
|
||||||
|
query.phoneNumber = phoneNumber;
|
||||||
|
}
|
||||||
|
if (organizationId) {
|
||||||
|
query.organization = organizationId;
|
||||||
|
}
|
||||||
|
|
||||||
const tokenData = await TokenData.findOne(query).select('+tokenHash');
|
const tokenData = await TokenData.findOne(query).select("+tokenHash");
|
||||||
|
|
||||||
if (!tokenData) throw new Error('Failed to find token to validate');
|
|
||||||
|
|
||||||
if (tokenData.expiresAt < new Date()) {
|
|
||||||
// case: token expired
|
|
||||||
await TokenData.findByIdAndDelete(tokenData._id);
|
|
||||||
throw UnauthorizedRequestError({
|
|
||||||
message: 'MFA session expired. Please log in again',
|
|
||||||
context: {
|
|
||||||
code: 'mfa_expired'
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
const isValid = await bcrypt.compare(token, tokenData.tokenHash);
|
if (!tokenData) throw new Error("Failed to find token to validate");
|
||||||
if (!isValid) {
|
|
||||||
// case: token is not valid
|
|
||||||
if (tokenData?.triesLeft !== undefined) {
|
|
||||||
// case: token has a try-limit
|
|
||||||
if (tokenData.triesLeft === 1) {
|
|
||||||
// case: token is out of tries
|
|
||||||
await TokenData.findByIdAndDelete(tokenData._id);
|
|
||||||
} else {
|
|
||||||
// case: token has more than 1 try left
|
|
||||||
await TokenData.findByIdAndUpdate(tokenData._id, {
|
|
||||||
triesLeft: tokenData.triesLeft - 1
|
|
||||||
}, {
|
|
||||||
new: true
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
throw UnauthorizedRequestError({
|
if (tokenData.expiresAt < new Date()) {
|
||||||
message: 'MFA code is invalid',
|
// case: token expired
|
||||||
context: {
|
|
||||||
code: 'mfa_invalid',
|
|
||||||
triesLeft: tokenData.triesLeft - 1
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
throw UnauthorizedRequestError({
|
|
||||||
message: 'MFA code is invalid',
|
|
||||||
context: {
|
|
||||||
code: 'mfa_invalid'
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// case: token is valid
|
|
||||||
await TokenData.findByIdAndDelete(tokenData._id);
|
await TokenData.findByIdAndDelete(tokenData._id);
|
||||||
}
|
throw UnauthorizedRequestError({
|
||||||
|
message: "MFA session expired. Please log in again",
|
||||||
|
context: {
|
||||||
|
code: "mfa_expired",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
export {
|
const isValid = await bcrypt.compare(token, tokenData.tokenHash);
|
||||||
createTokenHelper,
|
if (!isValid) {
|
||||||
validateTokenHelper
|
// case: token is not valid
|
||||||
}
|
if (tokenData?.triesLeft !== undefined) {
|
||||||
|
// case: token has a try-limit
|
||||||
|
if (tokenData.triesLeft === 1) {
|
||||||
|
// case: token is out of tries
|
||||||
|
await TokenData.findByIdAndDelete(tokenData._id);
|
||||||
|
} else {
|
||||||
|
// case: token has more than 1 try left
|
||||||
|
await TokenData.findByIdAndUpdate(
|
||||||
|
tokenData._id,
|
||||||
|
{
|
||||||
|
triesLeft: tokenData.triesLeft - 1,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
new: true,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
throw UnauthorizedRequestError({
|
||||||
|
message: "MFA code is invalid",
|
||||||
|
context: {
|
||||||
|
code: "mfa_invalid",
|
||||||
|
triesLeft: tokenData.triesLeft - 1,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
throw UnauthorizedRequestError({
|
||||||
|
message: "MFA code is invalid",
|
||||||
|
context: {
|
||||||
|
code: "mfa_invalid",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// case: token is valid
|
||||||
|
await TokenData.findByIdAndDelete(tokenData._id);
|
||||||
|
};
|
||||||
|
|
||||||
|
export { createTokenHelper, validateTokenHelper };
|
||||||
|
|||||||
+24
-39
@@ -1,4 +1,3 @@
|
|||||||
import * as Sentry from '@sentry/node';
|
|
||||||
import { Types } from 'mongoose';
|
import { Types } from 'mongoose';
|
||||||
import {
|
import {
|
||||||
IUser,
|
IUser,
|
||||||
@@ -28,16 +27,9 @@ import {
|
|||||||
* @returns {Object} user - the initialized user
|
* @returns {Object} user - the initialized user
|
||||||
*/
|
*/
|
||||||
const setupAccount = async ({ email }: { email: string }) => {
|
const setupAccount = async ({ email }: { email: string }) => {
|
||||||
let user;
|
const user = await new User({
|
||||||
try {
|
email
|
||||||
user = await new User({
|
}).save();
|
||||||
email
|
|
||||||
}).save();
|
|
||||||
} catch (err) {
|
|
||||||
Sentry.setUser({ email });
|
|
||||||
Sentry.captureException(err);
|
|
||||||
throw new Error('Failed to set up account');
|
|
||||||
}
|
|
||||||
|
|
||||||
return user;
|
return user;
|
||||||
};
|
};
|
||||||
@@ -89,34 +81,27 @@ const completeAccount = async ({
|
|||||||
salt: string;
|
salt: string;
|
||||||
verifier: string;
|
verifier: string;
|
||||||
}) => {
|
}) => {
|
||||||
let user;
|
const options = {
|
||||||
try {
|
new: true
|
||||||
const options = {
|
};
|
||||||
new: true
|
const user = await User.findByIdAndUpdate(
|
||||||
};
|
userId,
|
||||||
user = await User.findByIdAndUpdate(
|
{
|
||||||
userId,
|
firstName,
|
||||||
{
|
lastName,
|
||||||
firstName,
|
encryptionVersion,
|
||||||
lastName,
|
protectedKey,
|
||||||
encryptionVersion,
|
protectedKeyIV,
|
||||||
protectedKey,
|
protectedKeyTag,
|
||||||
protectedKeyIV,
|
publicKey,
|
||||||
protectedKeyTag,
|
encryptedPrivateKey,
|
||||||
publicKey,
|
iv: encryptedPrivateKeyIV,
|
||||||
encryptedPrivateKey,
|
tag: encryptedPrivateKeyTag,
|
||||||
iv: encryptedPrivateKeyIV,
|
salt,
|
||||||
tag: encryptedPrivateKeyTag,
|
verifier
|
||||||
salt,
|
},
|
||||||
verifier
|
options
|
||||||
},
|
);
|
||||||
options
|
|
||||||
);
|
|
||||||
} catch (err) {
|
|
||||||
Sentry.setUser(null);
|
|
||||||
Sentry.captureException(err);
|
|
||||||
throw new Error('Failed to complete account set up');
|
|
||||||
}
|
|
||||||
|
|
||||||
return user;
|
return user;
|
||||||
};
|
};
|
||||||
|
|||||||
+371
-453
@@ -1,7 +1,6 @@
|
|||||||
import * as Sentry from "@sentry/node";
|
|
||||||
import { Octokit } from "@octokit/rest";
|
import { Octokit } from "@octokit/rest";
|
||||||
import { IIntegrationAuth } from "../models";
|
import { IIntegrationAuth } from "../models";
|
||||||
import request from '../config/request';
|
import request from "../config/request";
|
||||||
import {
|
import {
|
||||||
INTEGRATION_AZURE_KEY_VAULT,
|
INTEGRATION_AZURE_KEY_VAULT,
|
||||||
INTEGRATION_AWS_PARAMETER_STORE,
|
INTEGRATION_AWS_PARAMETER_STORE,
|
||||||
@@ -26,7 +25,7 @@ import {
|
|||||||
INTEGRATION_FLYIO_API_URL,
|
INTEGRATION_FLYIO_API_URL,
|
||||||
INTEGRATION_CIRCLECI_API_URL,
|
INTEGRATION_CIRCLECI_API_URL,
|
||||||
INTEGRATION_TRAVISCI_API_URL,
|
INTEGRATION_TRAVISCI_API_URL,
|
||||||
INTEGRATION_SUPABASE_API_URL
|
INTEGRATION_SUPABASE_API_URL,
|
||||||
} from "../variables";
|
} from "../variables";
|
||||||
|
|
||||||
interface App {
|
interface App {
|
||||||
@@ -47,87 +46,80 @@ interface App {
|
|||||||
const getApps = async ({
|
const getApps = async ({
|
||||||
integrationAuth,
|
integrationAuth,
|
||||||
accessToken,
|
accessToken,
|
||||||
teamId
|
teamId,
|
||||||
}: {
|
}: {
|
||||||
integrationAuth: IIntegrationAuth;
|
integrationAuth: IIntegrationAuth;
|
||||||
accessToken: string;
|
accessToken: string;
|
||||||
teamId?: string;
|
teamId?: string;
|
||||||
}) => {
|
}) => {
|
||||||
|
|
||||||
let apps: App[] = [];
|
let apps: App[] = [];
|
||||||
try {
|
switch (integrationAuth.integration) {
|
||||||
switch (integrationAuth.integration) {
|
case INTEGRATION_AZURE_KEY_VAULT:
|
||||||
case INTEGRATION_AZURE_KEY_VAULT:
|
apps = [];
|
||||||
apps = [];
|
break;
|
||||||
break;
|
case INTEGRATION_AWS_PARAMETER_STORE:
|
||||||
case INTEGRATION_AWS_PARAMETER_STORE:
|
apps = [];
|
||||||
apps = [];
|
break;
|
||||||
break;
|
case INTEGRATION_AWS_SECRET_MANAGER:
|
||||||
case INTEGRATION_AWS_SECRET_MANAGER:
|
apps = [];
|
||||||
apps = [];
|
break;
|
||||||
break;
|
case INTEGRATION_HEROKU:
|
||||||
case INTEGRATION_HEROKU:
|
apps = await getAppsHeroku({
|
||||||
apps = await getAppsHeroku({
|
accessToken,
|
||||||
accessToken,
|
});
|
||||||
});
|
break;
|
||||||
break;
|
case INTEGRATION_VERCEL:
|
||||||
case INTEGRATION_VERCEL:
|
apps = await getAppsVercel({
|
||||||
apps = await getAppsVercel({
|
integrationAuth,
|
||||||
integrationAuth,
|
accessToken,
|
||||||
accessToken,
|
});
|
||||||
});
|
break;
|
||||||
break;
|
case INTEGRATION_NETLIFY:
|
||||||
case INTEGRATION_NETLIFY:
|
apps = await getAppsNetlify({
|
||||||
apps = await getAppsNetlify({
|
accessToken,
|
||||||
accessToken,
|
});
|
||||||
});
|
break;
|
||||||
break;
|
case INTEGRATION_GITHUB:
|
||||||
case INTEGRATION_GITHUB:
|
apps = await getAppsGithub({
|
||||||
apps = await getAppsGithub({
|
accessToken,
|
||||||
accessToken,
|
});
|
||||||
});
|
break;
|
||||||
break;
|
case INTEGRATION_GITLAB:
|
||||||
case INTEGRATION_GITLAB:
|
apps = await getAppsGitlab({
|
||||||
apps = await getAppsGitlab({
|
accessToken,
|
||||||
accessToken,
|
teamId,
|
||||||
teamId
|
});
|
||||||
});
|
break;
|
||||||
break;
|
case INTEGRATION_RENDER:
|
||||||
case INTEGRATION_RENDER:
|
apps = await getAppsRender({
|
||||||
apps = await getAppsRender({
|
accessToken,
|
||||||
accessToken,
|
});
|
||||||
});
|
break;
|
||||||
break;
|
case INTEGRATION_RAILWAY:
|
||||||
case INTEGRATION_RAILWAY:
|
apps = await getAppsRailway({
|
||||||
apps = await getAppsRailway({
|
accessToken,
|
||||||
accessToken
|
});
|
||||||
});
|
break;
|
||||||
break;
|
case INTEGRATION_FLYIO:
|
||||||
case INTEGRATION_FLYIO:
|
apps = await getAppsFlyio({
|
||||||
apps = await getAppsFlyio({
|
accessToken,
|
||||||
accessToken,
|
});
|
||||||
});
|
break;
|
||||||
break;
|
case INTEGRATION_CIRCLECI:
|
||||||
case INTEGRATION_CIRCLECI:
|
apps = await getAppsCircleCI({
|
||||||
apps = await getAppsCircleCI({
|
accessToken,
|
||||||
accessToken,
|
});
|
||||||
});
|
break;
|
||||||
break;
|
case INTEGRATION_TRAVISCI:
|
||||||
case INTEGRATION_TRAVISCI:
|
apps = await getAppsTravisCI({
|
||||||
apps = await getAppsTravisCI({
|
accessToken,
|
||||||
accessToken,
|
});
|
||||||
})
|
break;
|
||||||
break;
|
case INTEGRATION_SUPABASE:
|
||||||
case INTEGRATION_SUPABASE:
|
apps = await getAppsSupabase({
|
||||||
apps = await getAppsSupabase({
|
accessToken,
|
||||||
accessToken
|
});
|
||||||
});
|
break;
|
||||||
break;
|
|
||||||
}
|
|
||||||
} catch (err) {
|
|
||||||
Sentry.setUser(null);
|
|
||||||
Sentry.captureException(err);
|
|
||||||
throw new Error("Failed to get integration apps");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return apps;
|
return apps;
|
||||||
@@ -141,25 +133,18 @@ const getApps = async ({
|
|||||||
* @returns {String} apps.name - name of Heroku app
|
* @returns {String} apps.name - name of Heroku app
|
||||||
*/
|
*/
|
||||||
const getAppsHeroku = async ({ accessToken }: { accessToken: string }) => {
|
const getAppsHeroku = async ({ accessToken }: { accessToken: string }) => {
|
||||||
let apps;
|
const res = (
|
||||||
try {
|
await request.get(`${INTEGRATION_HEROKU_API_URL}/apps`, {
|
||||||
const res = (
|
headers: {
|
||||||
await request.get(`${INTEGRATION_HEROKU_API_URL}/apps`, {
|
Accept: "application/vnd.heroku+json; version=3",
|
||||||
headers: {
|
Authorization: `Bearer ${accessToken}`,
|
||||||
Accept: "application/vnd.heroku+json; version=3",
|
},
|
||||||
Authorization: `Bearer ${accessToken}`,
|
})
|
||||||
},
|
).data;
|
||||||
})
|
|
||||||
).data;
|
|
||||||
|
|
||||||
apps = res.map((a: any) => ({
|
const apps = res.map((a: any) => ({
|
||||||
name: a.name,
|
name: a.name,
|
||||||
}));
|
}));
|
||||||
} catch (err) {
|
|
||||||
Sentry.setUser(null);
|
|
||||||
Sentry.captureException(err);
|
|
||||||
throw new Error("Failed to get Heroku integration apps");
|
|
||||||
}
|
|
||||||
|
|
||||||
return apps;
|
return apps;
|
||||||
};
|
};
|
||||||
@@ -178,33 +163,26 @@ const getAppsVercel = async ({
|
|||||||
integrationAuth: IIntegrationAuth;
|
integrationAuth: IIntegrationAuth;
|
||||||
accessToken: string;
|
accessToken: string;
|
||||||
}) => {
|
}) => {
|
||||||
let apps;
|
const res = (
|
||||||
try {
|
await request.get(`${INTEGRATION_VERCEL_API_URL}/v9/projects`, {
|
||||||
const res = (
|
headers: {
|
||||||
await request.get(`${INTEGRATION_VERCEL_API_URL}/v9/projects`, {
|
Authorization: `Bearer ${accessToken}`,
|
||||||
headers: {
|
"Accept-Encoding": "application/json",
|
||||||
Authorization: `Bearer ${accessToken}`,
|
},
|
||||||
'Accept-Encoding': 'application/json'
|
...(integrationAuth?.teamId
|
||||||
},
|
? {
|
||||||
...(integrationAuth?.teamId
|
params: {
|
||||||
? {
|
teamId: integrationAuth.teamId,
|
||||||
params: {
|
},
|
||||||
teamId: integrationAuth.teamId,
|
}
|
||||||
},
|
: {}),
|
||||||
}
|
})
|
||||||
: {}),
|
).data;
|
||||||
})
|
|
||||||
).data;
|
|
||||||
|
|
||||||
apps = res.projects.map((a: any) => ({
|
const apps = res.projects.map((a: any) => ({
|
||||||
name: a.name,
|
name: a.name,
|
||||||
appId: a.id
|
appId: a.id,
|
||||||
}));
|
}));
|
||||||
} catch (err) {
|
|
||||||
Sentry.setUser(null);
|
|
||||||
Sentry.captureException(err);
|
|
||||||
throw new Error("Failed to get Vercel integration apps");
|
|
||||||
}
|
|
||||||
|
|
||||||
return apps;
|
return apps;
|
||||||
};
|
};
|
||||||
@@ -218,43 +196,40 @@ const getAppsVercel = async ({
|
|||||||
*/
|
*/
|
||||||
const getAppsNetlify = async ({ accessToken }: { accessToken: string }) => {
|
const getAppsNetlify = async ({ accessToken }: { accessToken: string }) => {
|
||||||
const apps: any = [];
|
const apps: any = [];
|
||||||
try {
|
let page = 1;
|
||||||
let page = 1;
|
const perPage = 10;
|
||||||
const perPage = 10;
|
let hasMorePages = true;
|
||||||
let hasMorePages = true;
|
|
||||||
|
|
||||||
// paginate through all sites
|
|
||||||
while (hasMorePages) {
|
|
||||||
const params = new URLSearchParams({
|
|
||||||
page: String(page),
|
|
||||||
per_page: String(perPage)
|
|
||||||
});
|
|
||||||
|
|
||||||
const { data } = await request.get(`${INTEGRATION_NETLIFY_API_URL}/api/v1/sites`, {
|
// paginate through all sites
|
||||||
|
while (hasMorePages) {
|
||||||
|
const params = new URLSearchParams({
|
||||||
|
page: String(page),
|
||||||
|
per_page: String(perPage),
|
||||||
|
});
|
||||||
|
|
||||||
|
const { data } = await request.get(
|
||||||
|
`${INTEGRATION_NETLIFY_API_URL}/api/v1/sites`,
|
||||||
|
{
|
||||||
params,
|
params,
|
||||||
headers: {
|
headers: {
|
||||||
Authorization: `Bearer ${accessToken}`,
|
Authorization: `Bearer ${accessToken}`,
|
||||||
'Accept-Encoding': 'application/json'
|
"Accept-Encoding": "application/json",
|
||||||
}
|
},
|
||||||
});
|
|
||||||
|
|
||||||
data.map((a: any) => {
|
|
||||||
apps.push({
|
|
||||||
name: a.name,
|
|
||||||
appId: a.site_id
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
if (data.length < perPage) {
|
|
||||||
hasMorePages = false;
|
|
||||||
}
|
}
|
||||||
|
);
|
||||||
|
|
||||||
page++;
|
data.map((a: any) => {
|
||||||
|
apps.push({
|
||||||
|
name: a.name,
|
||||||
|
appId: a.site_id,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
if (data.length < perPage) {
|
||||||
|
hasMorePages = false;
|
||||||
}
|
}
|
||||||
} catch (err) {
|
|
||||||
Sentry.setUser(null);
|
page++;
|
||||||
Sentry.captureException(err);
|
|
||||||
throw new Error("Failed to get Netlify integration apps");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return apps;
|
return apps;
|
||||||
@@ -268,67 +243,59 @@ const getAppsNetlify = async ({ accessToken }: { accessToken: string }) => {
|
|||||||
* @returns {String} apps.name - name of Github site
|
* @returns {String} apps.name - name of Github site
|
||||||
*/
|
*/
|
||||||
const getAppsGithub = async ({ accessToken }: { accessToken: string }) => {
|
const getAppsGithub = async ({ accessToken }: { accessToken: string }) => {
|
||||||
let apps;
|
interface GitHubApp {
|
||||||
try {
|
id: string;
|
||||||
interface GitHubApp {
|
name: string;
|
||||||
id: string;
|
permissions: {
|
||||||
name: string;
|
admin: boolean;
|
||||||
permissions: {
|
};
|
||||||
admin: boolean;
|
owner: {
|
||||||
};
|
login: string;
|
||||||
owner: {
|
};
|
||||||
login: string;
|
}
|
||||||
|
|
||||||
|
const octokit = new Octokit({
|
||||||
|
auth: accessToken,
|
||||||
|
});
|
||||||
|
|
||||||
|
const getAllRepos = async () => {
|
||||||
|
let repos: GitHubApp[] = [];
|
||||||
|
let page = 1;
|
||||||
|
const per_page = 100;
|
||||||
|
let hasMore = true;
|
||||||
|
|
||||||
|
while (hasMore) {
|
||||||
|
const response = await octokit.request(
|
||||||
|
"GET /user/repos{?visibility,affiliation,type,sort,direction,per_page,page,since,before}",
|
||||||
|
{
|
||||||
|
per_page,
|
||||||
|
page,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
if (response.data.length > 0) {
|
||||||
|
repos = repos.concat(response.data);
|
||||||
|
page++;
|
||||||
|
} else {
|
||||||
|
hasMore = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const octokit = new Octokit({
|
return repos;
|
||||||
auth: accessToken,
|
};
|
||||||
|
|
||||||
|
const repos = await getAllRepos();
|
||||||
|
|
||||||
|
const apps = repos
|
||||||
|
.filter((a: GitHubApp) => a.permissions.admin === true)
|
||||||
|
.map((a: GitHubApp) => {
|
||||||
|
return {
|
||||||
|
appId: a.id,
|
||||||
|
name: a.name,
|
||||||
|
owner: a.owner.login,
|
||||||
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
const getAllRepos = async () => {
|
|
||||||
let repos: GitHubApp[] = [];
|
|
||||||
let page = 1;
|
|
||||||
const per_page = 100;
|
|
||||||
let hasMore = true;
|
|
||||||
|
|
||||||
while (hasMore) {
|
|
||||||
const response = await octokit.request(
|
|
||||||
"GET /user/repos{?visibility,affiliation,type,sort,direction,per_page,page,since,before}",
|
|
||||||
{
|
|
||||||
per_page,
|
|
||||||
page,
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
if (response.data.length > 0) {
|
|
||||||
repos = repos.concat(response.data);
|
|
||||||
page++;
|
|
||||||
} else {
|
|
||||||
hasMore = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return repos;
|
|
||||||
};
|
|
||||||
|
|
||||||
const repos = await getAllRepos();
|
|
||||||
|
|
||||||
apps = repos
|
|
||||||
.filter((a: GitHubApp) => a.permissions.admin === true)
|
|
||||||
.map((a: GitHubApp) => {
|
|
||||||
return {
|
|
||||||
appId: a.id,
|
|
||||||
name: a.name,
|
|
||||||
owner: a.owner.login,
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
} catch (err) {
|
|
||||||
Sentry.setUser(null);
|
|
||||||
Sentry.captureException(err);
|
|
||||||
throw new Error("Failed to get Github repos");
|
|
||||||
}
|
|
||||||
|
|
||||||
return apps;
|
return apps;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -341,29 +308,20 @@ const getAppsGithub = async ({ accessToken }: { accessToken: string }) => {
|
|||||||
* @returns {String} apps.appId - id of Render service
|
* @returns {String} apps.appId - id of Render service
|
||||||
*/
|
*/
|
||||||
const getAppsRender = async ({ accessToken }: { accessToken: string }) => {
|
const getAppsRender = async ({ accessToken }: { accessToken: string }) => {
|
||||||
let apps: any;
|
const res = (
|
||||||
try {
|
await request.get(`${INTEGRATION_RENDER_API_URL}/v1/services`, {
|
||||||
const res = (
|
headers: {
|
||||||
await request.get(`${INTEGRATION_RENDER_API_URL}/v1/services`, {
|
Authorization: `Bearer ${accessToken}`,
|
||||||
headers: {
|
Accept: "application/json",
|
||||||
Authorization: `Bearer ${accessToken}`,
|
"Accept-Encoding": "application/json",
|
||||||
Accept: 'application/json',
|
},
|
||||||
'Accept-Encoding': 'application/json',
|
})
|
||||||
},
|
).data;
|
||||||
})
|
|
||||||
).data;
|
const apps = res.map((a: any) => ({
|
||||||
|
name: a.service.name,
|
||||||
apps = res
|
appId: a.service.id,
|
||||||
.map((a: any) => ({
|
}));
|
||||||
name: a.service.name,
|
|
||||||
appId: a.service.id
|
|
||||||
}));
|
|
||||||
|
|
||||||
} catch (err) {
|
|
||||||
Sentry.setUser(null);
|
|
||||||
Sentry.captureException(err);
|
|
||||||
throw new Error("Failed to get Render services");
|
|
||||||
}
|
|
||||||
|
|
||||||
return apps;
|
return apps;
|
||||||
};
|
};
|
||||||
@@ -376,49 +334,51 @@ const getAppsRender = async ({ accessToken }: { accessToken: string }) => {
|
|||||||
* @returns {String} apps.name - name of Railway project
|
* @returns {String} apps.name - name of Railway project
|
||||||
* @returns {String} apps.appId - id of Railway project
|
* @returns {String} apps.appId - id of Railway project
|
||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
const getAppsRailway = async ({ accessToken }: { accessToken: string }) => {
|
const getAppsRailway = async ({ accessToken }: { accessToken: string }) => {
|
||||||
let apps: any[] = [];
|
const query = `
|
||||||
try {
|
query GetProjects($userId: String, $teamId: String) {
|
||||||
const query = `
|
projects(userId: $userId, teamId: $teamId) {
|
||||||
query GetProjects($userId: String, $teamId: String) {
|
edges {
|
||||||
projects(userId: $userId, teamId: $teamId) {
|
node {
|
||||||
edges {
|
id
|
||||||
node {
|
name
|
||||||
id
|
|
||||||
name
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
`;
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
const variables = {};
|
const variables = {};
|
||||||
|
|
||||||
const { data: { data: { projects: { edges }}} } = await request.post(INTEGRATION_RAILWAY_API_URL, {
|
const {
|
||||||
|
data: {
|
||||||
|
data: {
|
||||||
|
projects: { edges },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
} = await request.post(
|
||||||
|
INTEGRATION_RAILWAY_API_URL,
|
||||||
|
{
|
||||||
query,
|
query,
|
||||||
variables,
|
variables,
|
||||||
}, {
|
},
|
||||||
|
{
|
||||||
headers: {
|
headers: {
|
||||||
'Authorization': `Bearer ${accessToken}`,
|
Authorization: `Bearer ${accessToken}`,
|
||||||
'Content-Type': 'application/json',
|
"Content-Type": "application/json",
|
||||||
'Accept-Encoding': 'application/json'
|
"Accept-Encoding": "application/json",
|
||||||
},
|
},
|
||||||
});
|
}
|
||||||
|
);
|
||||||
apps = edges.map((e: any) => ({
|
|
||||||
name: e.node.name,
|
const apps = edges.map((e: any) => ({
|
||||||
appId: e.node.id
|
name: e.node.name,
|
||||||
}));
|
appId: e.node.id,
|
||||||
|
}));
|
||||||
|
|
||||||
} catch (err) {
|
|
||||||
Sentry.setUser(null);
|
|
||||||
Sentry.captureException(err);
|
|
||||||
throw new Error("Failed to get Railway services");
|
|
||||||
}
|
|
||||||
|
|
||||||
return apps;
|
return apps;
|
||||||
}
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Return list of apps for Fly.io integration
|
* Return list of apps for Fly.io integration
|
||||||
@@ -428,41 +388,40 @@ const getAppsRailway = async ({ accessToken }: { accessToken: string }) => {
|
|||||||
* @returns {String} apps.name - name of Fly.io apps
|
* @returns {String} apps.name - name of Fly.io apps
|
||||||
*/
|
*/
|
||||||
const getAppsFlyio = async ({ accessToken }: { accessToken: string }) => {
|
const getAppsFlyio = async ({ accessToken }: { accessToken: string }) => {
|
||||||
let apps;
|
const query = `
|
||||||
try {
|
query($role: String) {
|
||||||
const query = `
|
apps(type: "container", first: 400, role: $role) {
|
||||||
query($role: String) {
|
nodes {
|
||||||
apps(type: "container", first: 400, role: $role) {
|
id
|
||||||
nodes {
|
name
|
||||||
id
|
hostname
|
||||||
name
|
|
||||||
hostname
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
`;
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
const res = (await request.post(INTEGRATION_FLYIO_API_URL, {
|
const res = (
|
||||||
query,
|
await request.post(
|
||||||
variables: {
|
INTEGRATION_FLYIO_API_URL,
|
||||||
role: null,
|
{
|
||||||
|
query,
|
||||||
|
variables: {
|
||||||
|
role: null,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
}, {
|
{
|
||||||
headers: {
|
headers: {
|
||||||
Authorization: "Bearer " + accessToken,
|
Authorization: "Bearer " + accessToken,
|
||||||
'Accept': 'application/json',
|
Accept: "application/json",
|
||||||
'Accept-Encoding': 'application/json',
|
"Accept-Encoding": "application/json",
|
||||||
},
|
},
|
||||||
})).data.data.apps.nodes;
|
}
|
||||||
|
)
|
||||||
|
).data.data.apps.nodes;
|
||||||
|
|
||||||
apps = res.map((a: any) => ({
|
const apps = res.map((a: any) => ({
|
||||||
name: a.name,
|
name: a.name,
|
||||||
}));
|
}));
|
||||||
} catch (err) {
|
|
||||||
Sentry.setUser(null);
|
|
||||||
Sentry.captureException(err);
|
|
||||||
throw new Error("Failed to get Fly.io apps");
|
|
||||||
}
|
|
||||||
|
|
||||||
return apps;
|
return apps;
|
||||||
};
|
};
|
||||||
@@ -475,63 +434,43 @@ const getAppsFlyio = async ({ accessToken }: { accessToken: string }) => {
|
|||||||
* @returns {String} apps.name - name of CircleCI apps
|
* @returns {String} apps.name - name of CircleCI apps
|
||||||
*/
|
*/
|
||||||
const getAppsCircleCI = async ({ accessToken }: { accessToken: string }) => {
|
const getAppsCircleCI = async ({ accessToken }: { accessToken: string }) => {
|
||||||
let apps: any;
|
const res = (
|
||||||
try {
|
await request.get(`${INTEGRATION_CIRCLECI_API_URL}/v1.1/projects`, {
|
||||||
const res = (
|
headers: {
|
||||||
await request.get(
|
"Circle-Token": accessToken,
|
||||||
`${INTEGRATION_CIRCLECI_API_URL}/v1.1/projects`,
|
"Accept-Encoding": "application/json",
|
||||||
{
|
},
|
||||||
headers: {
|
})
|
||||||
"Circle-Token": accessToken,
|
).data;
|
||||||
"Accept-Encoding": "application/json",
|
|
||||||
},
|
const apps = res?.map((a: any) => {
|
||||||
}
|
return {
|
||||||
)
|
name: a?.reponame,
|
||||||
).data
|
};
|
||||||
|
});
|
||||||
|
|
||||||
apps = res?.map((a: any) => {
|
|
||||||
return {
|
|
||||||
name: a?.reponame
|
|
||||||
}
|
|
||||||
});
|
|
||||||
} catch (err) {
|
|
||||||
Sentry.setUser(null);
|
|
||||||
Sentry.captureException(err);
|
|
||||||
throw new Error("Failed to get CircleCI projects");
|
|
||||||
}
|
|
||||||
|
|
||||||
return apps;
|
return apps;
|
||||||
};
|
};
|
||||||
|
|
||||||
const getAppsTravisCI = async ({ accessToken }: { accessToken: string }) => {
|
const getAppsTravisCI = async ({ accessToken }: { accessToken: string }) => {
|
||||||
let apps: any;
|
const res = (
|
||||||
try {
|
await request.get(`${INTEGRATION_TRAVISCI_API_URL}/repos`, {
|
||||||
const res = (
|
headers: {
|
||||||
await request.get(
|
Authorization: `token ${accessToken}`,
|
||||||
`${INTEGRATION_TRAVISCI_API_URL}/repos`,
|
"Accept-Encoding": "application/json",
|
||||||
{
|
},
|
||||||
headers: {
|
})
|
||||||
"Authorization": `token ${accessToken}`,
|
).data;
|
||||||
"Accept-Encoding": "application/json",
|
|
||||||
},
|
const apps = res?.map((a: any) => {
|
||||||
}
|
return {
|
||||||
)
|
name: a?.slug?.split("/")[1],
|
||||||
).data;
|
appId: a?.id,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
apps = res?.map((a: any) => {
|
|
||||||
return {
|
|
||||||
name: a?.slug?.split("/")[1],
|
|
||||||
appId: a?.id,
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}catch (err) {
|
|
||||||
Sentry.setUser(null);
|
|
||||||
Sentry.captureException(err);
|
|
||||||
throw new Error("Failed to get TravisCI projects");
|
|
||||||
}
|
|
||||||
|
|
||||||
return apps;
|
return apps;
|
||||||
}
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Return list of repositories for GitLab integration
|
* Return list of repositories for GitLab integration
|
||||||
@@ -540,112 +479,98 @@ const getAppsTravisCI = async ({ accessToken }: { accessToken: string }) => {
|
|||||||
* @returns {Object[]} apps - names of GitLab sites
|
* @returns {Object[]} apps - names of GitLab sites
|
||||||
* @returns {String} apps.name - name of GitLab site
|
* @returns {String} apps.name - name of GitLab site
|
||||||
*/
|
*/
|
||||||
const getAppsGitlab = async ({
|
const getAppsGitlab = async ({
|
||||||
accessToken,
|
accessToken,
|
||||||
teamId
|
teamId,
|
||||||
}: {
|
}: {
|
||||||
accessToken: string;
|
accessToken: string;
|
||||||
teamId?: string;
|
teamId?: string;
|
||||||
}) => {
|
}) => {
|
||||||
const apps: App[] = [];
|
const apps: App[] = [];
|
||||||
|
|
||||||
let page = 1;
|
let page = 1;
|
||||||
const perPage = 10;
|
const perPage = 10;
|
||||||
let hasMorePages = true;
|
let hasMorePages = true;
|
||||||
try {
|
|
||||||
|
|
||||||
if (teamId) {
|
if (teamId) {
|
||||||
// case: fetch projects for group with id [teamId] in GitLab
|
// case: fetch projects for group with id [teamId] in GitLab
|
||||||
|
|
||||||
while (hasMorePages) {
|
|
||||||
const params = new URLSearchParams({
|
|
||||||
page: String(page),
|
|
||||||
per_page: String(perPage)
|
|
||||||
});
|
|
||||||
|
|
||||||
const { data } = (
|
while (hasMorePages) {
|
||||||
await request.get(
|
const params = new URLSearchParams({
|
||||||
`${INTEGRATION_GITLAB_API_URL}/v4/groups/${teamId}/projects`,
|
page: String(page),
|
||||||
{
|
per_page: String(perPage),
|
||||||
params,
|
});
|
||||||
headers: {
|
|
||||||
"Authorization": `Bearer ${accessToken}`,
|
|
||||||
"Accept-Encoding": "application/json",
|
|
||||||
},
|
|
||||||
}
|
|
||||||
)
|
|
||||||
);
|
|
||||||
|
|
||||||
data.map((a: any) => {
|
const { data } = await request.get(
|
||||||
apps.push({
|
`${INTEGRATION_GITLAB_API_URL}/v4/groups/${teamId}/projects`,
|
||||||
name: a.name,
|
{
|
||||||
appId: a.id
|
params,
|
||||||
});
|
headers: {
|
||||||
});
|
Authorization: `Bearer ${accessToken}`,
|
||||||
|
"Accept-Encoding": "application/json",
|
||||||
if (data.length < perPage) {
|
},
|
||||||
hasMorePages = false;
|
|
||||||
}
|
}
|
||||||
|
);
|
||||||
page++;
|
|
||||||
}
|
data.map((a: any) => {
|
||||||
} else {
|
apps.push({
|
||||||
// case: fetch projects for individual in GitLab
|
name: a.name,
|
||||||
|
appId: a.id,
|
||||||
const { id } = (
|
|
||||||
await request.get(
|
|
||||||
`${INTEGRATION_GITLAB_API_URL}/v4/user`,
|
|
||||||
{
|
|
||||||
headers: {
|
|
||||||
"Authorization": `Bearer ${accessToken}`,
|
|
||||||
"Accept-Encoding": "application/json",
|
|
||||||
},
|
|
||||||
}
|
|
||||||
)
|
|
||||||
).data;
|
|
||||||
|
|
||||||
while (hasMorePages) {
|
|
||||||
const params = new URLSearchParams({
|
|
||||||
page: String(page),
|
|
||||||
per_page: String(perPage)
|
|
||||||
});
|
});
|
||||||
|
});
|
||||||
|
|
||||||
const { data } = (
|
if (data.length < perPage) {
|
||||||
await request.get(
|
hasMorePages = false;
|
||||||
`${INTEGRATION_GITLAB_API_URL}/v4/users/${id}/projects`,
|
|
||||||
{
|
|
||||||
params,
|
|
||||||
headers: {
|
|
||||||
"Authorization": `Bearer ${accessToken}`,
|
|
||||||
"Accept-Encoding": "application/json",
|
|
||||||
},
|
|
||||||
}
|
|
||||||
)
|
|
||||||
);
|
|
||||||
|
|
||||||
data.map((a: any) => {
|
|
||||||
apps.push({
|
|
||||||
name: a.name,
|
|
||||||
appId: a.id
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
if (data.length < perPage) {
|
|
||||||
hasMorePages = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
page++;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
page++;
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} else {
|
||||||
Sentry.setUser(null);
|
// case: fetch projects for individual in GitLab
|
||||||
Sentry.captureException(err);
|
|
||||||
throw new Error("Failed to get GitLab projects");
|
|
||||||
}
|
|
||||||
|
|
||||||
return apps;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
const { id } = (
|
||||||
|
await request.get(`${INTEGRATION_GITLAB_API_URL}/v4/user`, {
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${accessToken}`,
|
||||||
|
"Accept-Encoding": "application/json",
|
||||||
|
},
|
||||||
|
})
|
||||||
|
).data;
|
||||||
|
|
||||||
|
while (hasMorePages) {
|
||||||
|
const params = new URLSearchParams({
|
||||||
|
page: String(page),
|
||||||
|
per_page: String(perPage),
|
||||||
|
});
|
||||||
|
|
||||||
|
const { data } = await request.get(
|
||||||
|
`${INTEGRATION_GITLAB_API_URL}/v4/users/${id}/projects`,
|
||||||
|
{
|
||||||
|
params,
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${accessToken}`,
|
||||||
|
"Accept-Encoding": "application/json",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
data.map((a: any) => {
|
||||||
|
apps.push({
|
||||||
|
name: a.name,
|
||||||
|
appId: a.id,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
if (data.length < perPage) {
|
||||||
|
hasMorePages = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
page++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return apps;
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Return list of projects for Supabase integration
|
* Return list of projects for Supabase integration
|
||||||
@@ -655,30 +580,23 @@ const getAppsGitlab = async ({
|
|||||||
* @returns {String} apps.name - name of Supabase app
|
* @returns {String} apps.name - name of Supabase app
|
||||||
*/
|
*/
|
||||||
const getAppsSupabase = async ({ accessToken }: { accessToken: string }) => {
|
const getAppsSupabase = async ({ accessToken }: { accessToken: string }) => {
|
||||||
let apps: any;
|
const { data } = await request.get(
|
||||||
try {
|
`${INTEGRATION_SUPABASE_API_URL}/v1/projects`,
|
||||||
const { data } = await request.get(
|
{
|
||||||
`${INTEGRATION_SUPABASE_API_URL}/v1/projects`,
|
headers: {
|
||||||
{
|
Authorization: `Bearer ${accessToken}`,
|
||||||
headers: {
|
"Accept-Encoding": "application/json",
|
||||||
Authorization: `Bearer ${accessToken}`,
|
},
|
||||||
'Accept-Encoding': 'application/json'
|
}
|
||||||
}
|
);
|
||||||
}
|
|
||||||
);
|
const apps = data.map((a: any) => {
|
||||||
|
return {
|
||||||
|
name: a.name,
|
||||||
|
appId: a.id,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
apps = data.map((a: any) => {
|
|
||||||
return {
|
|
||||||
name: a.name,
|
|
||||||
appId: a.id
|
|
||||||
};
|
|
||||||
});
|
|
||||||
} catch (err) {
|
|
||||||
Sentry.setUser(null);
|
|
||||||
Sentry.captureException(err);
|
|
||||||
throw new Error('Failed to get Supabase projects');
|
|
||||||
}
|
|
||||||
|
|
||||||
return apps;
|
return apps;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
import * as Sentry from '@sentry/node';
|
import request from "../config/request";
|
||||||
import request from '../config/request';
|
|
||||||
import {
|
import {
|
||||||
INTEGRATION_AZURE_KEY_VAULT,
|
INTEGRATION_AZURE_KEY_VAULT,
|
||||||
INTEGRATION_HEROKU,
|
INTEGRATION_HEROKU,
|
||||||
@@ -12,8 +11,8 @@ import {
|
|||||||
INTEGRATION_VERCEL_TOKEN_URL,
|
INTEGRATION_VERCEL_TOKEN_URL,
|
||||||
INTEGRATION_NETLIFY_TOKEN_URL,
|
INTEGRATION_NETLIFY_TOKEN_URL,
|
||||||
INTEGRATION_GITHUB_TOKEN_URL,
|
INTEGRATION_GITHUB_TOKEN_URL,
|
||||||
INTEGRATION_GITLAB_TOKEN_URL
|
INTEGRATION_GITLAB_TOKEN_URL,
|
||||||
} from '../variables';
|
} from "../variables";
|
||||||
import {
|
import {
|
||||||
getSiteURL,
|
getSiteURL,
|
||||||
getClientIdAzure,
|
getClientIdAzure,
|
||||||
@@ -26,8 +25,8 @@ import {
|
|||||||
getClientIdGitHub,
|
getClientIdGitHub,
|
||||||
getClientSecretGitHub,
|
getClientSecretGitHub,
|
||||||
getClientIdGitLab,
|
getClientIdGitLab,
|
||||||
getClientSecretGitLab
|
getClientSecretGitLab,
|
||||||
} from '../config';
|
} from "../config";
|
||||||
|
|
||||||
interface ExchangeCodeAzureResponse {
|
interface ExchangeCodeAzureResponse {
|
||||||
token_type: string;
|
token_type: string;
|
||||||
@@ -93,49 +92,43 @@ interface ExchangeCodeGitlabResponse {
|
|||||||
*/
|
*/
|
||||||
const exchangeCode = async ({
|
const exchangeCode = async ({
|
||||||
integration,
|
integration,
|
||||||
code
|
code,
|
||||||
}: {
|
}: {
|
||||||
integration: string;
|
integration: string;
|
||||||
code: string;
|
code: string;
|
||||||
}) => {
|
}) => {
|
||||||
let obj = {} as any;
|
let obj = {} as any;
|
||||||
|
|
||||||
try {
|
switch (integration) {
|
||||||
switch (integration) {
|
case INTEGRATION_AZURE_KEY_VAULT:
|
||||||
case INTEGRATION_AZURE_KEY_VAULT:
|
obj = await exchangeCodeAzure({
|
||||||
obj = await exchangeCodeAzure({
|
code,
|
||||||
code
|
});
|
||||||
});
|
break;
|
||||||
break;
|
case INTEGRATION_HEROKU:
|
||||||
case INTEGRATION_HEROKU:
|
obj = await exchangeCodeHeroku({
|
||||||
obj = await exchangeCodeHeroku({
|
code,
|
||||||
code
|
});
|
||||||
});
|
break;
|
||||||
break;
|
case INTEGRATION_VERCEL:
|
||||||
case INTEGRATION_VERCEL:
|
obj = await exchangeCodeVercel({
|
||||||
obj = await exchangeCodeVercel({
|
code,
|
||||||
code
|
});
|
||||||
});
|
break;
|
||||||
break;
|
case INTEGRATION_NETLIFY:
|
||||||
case INTEGRATION_NETLIFY:
|
obj = await exchangeCodeNetlify({
|
||||||
obj = await exchangeCodeNetlify({
|
code,
|
||||||
code
|
});
|
||||||
});
|
break;
|
||||||
break;
|
case INTEGRATION_GITHUB:
|
||||||
case INTEGRATION_GITHUB:
|
obj = await exchangeCodeGithub({
|
||||||
obj = await exchangeCodeGithub({
|
code,
|
||||||
code
|
});
|
||||||
});
|
break;
|
||||||
break;
|
case INTEGRATION_GITLAB:
|
||||||
case INTEGRATION_GITLAB:
|
obj = await exchangeCodeGitlab({
|
||||||
obj = await exchangeCodeGitlab({
|
code,
|
||||||
code
|
});
|
||||||
});
|
|
||||||
}
|
|
||||||
} catch (err) {
|
|
||||||
Sentry.setUser(null);
|
|
||||||
Sentry.captureException(err);
|
|
||||||
throw new Error('Failed OAuth2 code-token exchange');
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return obj;
|
return obj;
|
||||||
@@ -143,43 +136,33 @@ const exchangeCode = async ({
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Return [accessToken] for Azure OAuth2 code-token exchange
|
* Return [accessToken] for Azure OAuth2 code-token exchange
|
||||||
* @param param0
|
* @param param0
|
||||||
*/
|
*/
|
||||||
const exchangeCodeAzure = async ({
|
const exchangeCodeAzure = async ({ code }: { code: string }) => {
|
||||||
code
|
|
||||||
}: {
|
|
||||||
code: string;
|
|
||||||
}) => {
|
|
||||||
const accessExpiresAt = new Date();
|
const accessExpiresAt = new Date();
|
||||||
let res: ExchangeCodeAzureResponse;
|
|
||||||
try {
|
const res: ExchangeCodeAzureResponse = (
|
||||||
res = (await request.post(
|
await request.post(
|
||||||
INTEGRATION_AZURE_TOKEN_URL,
|
INTEGRATION_AZURE_TOKEN_URL,
|
||||||
new URLSearchParams({
|
new URLSearchParams({
|
||||||
grant_type: 'authorization_code',
|
grant_type: "authorization_code",
|
||||||
code: code,
|
code: code,
|
||||||
scope: 'https://vault.azure.net/.default openid offline_access',
|
scope: "https://vault.azure.net/.default openid offline_access",
|
||||||
client_id: await getClientIdAzure(),
|
client_id: await getClientIdAzure(),
|
||||||
client_secret: await getClientSecretAzure(),
|
client_secret: await getClientSecretAzure(),
|
||||||
redirect_uri: `${await getSiteURL()}/integrations/azure-key-vault/oauth2/callback`
|
redirect_uri: `${await getSiteURL()}/integrations/azure-key-vault/oauth2/callback`,
|
||||||
} as any)
|
} as any)
|
||||||
)).data;
|
)
|
||||||
|
).data;
|
||||||
|
|
||||||
accessExpiresAt.setSeconds(
|
accessExpiresAt.setSeconds(accessExpiresAt.getSeconds() + res.expires_in);
|
||||||
accessExpiresAt.getSeconds() + res.expires_in
|
|
||||||
);
|
|
||||||
} catch (err) {
|
|
||||||
Sentry.setUser(null);
|
|
||||||
Sentry.captureException(err);
|
|
||||||
throw new Error('Failed OAuth2 code-token exchange with Azure');
|
|
||||||
}
|
|
||||||
|
|
||||||
return ({
|
return {
|
||||||
accessToken: res.access_token,
|
accessToken: res.access_token,
|
||||||
refreshToken: res.refresh_token,
|
refreshToken: res.refresh_token,
|
||||||
accessExpiresAt
|
accessExpiresAt,
|
||||||
});
|
};
|
||||||
}
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Return [accessToken], [accessExpiresAt], and [refreshToken] for Heroku
|
* Return [accessToken], [accessExpiresAt], and [refreshToken] for Heroku
|
||||||
@@ -191,38 +174,28 @@ const exchangeCodeAzure = async ({
|
|||||||
* @returns {String} obj2.refreshToken - refresh token for Heroku API
|
* @returns {String} obj2.refreshToken - refresh token for Heroku API
|
||||||
* @returns {Date} obj2.accessExpiresAt - date of expiration for access token
|
* @returns {Date} obj2.accessExpiresAt - date of expiration for access token
|
||||||
*/
|
*/
|
||||||
const exchangeCodeHeroku = async ({
|
const exchangeCodeHeroku = async ({ code }: { code: string }) => {
|
||||||
code
|
|
||||||
}: {
|
|
||||||
code: string;
|
|
||||||
}) => {
|
|
||||||
let res: ExchangeCodeHerokuResponse;
|
|
||||||
const accessExpiresAt = new Date();
|
const accessExpiresAt = new Date();
|
||||||
try {
|
|
||||||
res = (await request.post(
|
const res: ExchangeCodeHerokuResponse = (
|
||||||
|
await request.post(
|
||||||
INTEGRATION_HEROKU_TOKEN_URL,
|
INTEGRATION_HEROKU_TOKEN_URL,
|
||||||
new URLSearchParams({
|
new URLSearchParams({
|
||||||
grant_type: 'authorization_code',
|
grant_type: "authorization_code",
|
||||||
code: code,
|
code: code,
|
||||||
client_secret: await getClientSecretHeroku()
|
client_secret: await getClientSecretHeroku(),
|
||||||
} as any)
|
} as any)
|
||||||
)).data;
|
)
|
||||||
|
).data;
|
||||||
|
|
||||||
accessExpiresAt.setSeconds(
|
accessExpiresAt.setSeconds(accessExpiresAt.getSeconds() + res.expires_in);
|
||||||
accessExpiresAt.getSeconds() + res.expires_in
|
|
||||||
);
|
|
||||||
} catch (err) {
|
|
||||||
Sentry.setUser(null);
|
|
||||||
Sentry.captureException(err);
|
|
||||||
throw new Error('Failed OAuth2 code-token exchange with Heroku');
|
|
||||||
}
|
|
||||||
|
|
||||||
return ({
|
return {
|
||||||
accessToken: res.access_token,
|
accessToken: res.access_token,
|
||||||
refreshToken: res.refresh_token,
|
refreshToken: res.refresh_token,
|
||||||
accessExpiresAt
|
accessExpiresAt,
|
||||||
});
|
};
|
||||||
}
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Return [accessToken], [accessExpiresAt], and [refreshToken] for Vercel
|
* Return [accessToken], [accessExpiresAt], and [refreshToken] for Vercel
|
||||||
@@ -235,30 +208,23 @@ const exchangeCodeHeroku = async ({
|
|||||||
* @returns {Date} obj2.accessExpiresAt - date of expiration for access token
|
* @returns {Date} obj2.accessExpiresAt - date of expiration for access token
|
||||||
*/
|
*/
|
||||||
const exchangeCodeVercel = async ({ code }: { code: string }) => {
|
const exchangeCodeVercel = async ({ code }: { code: string }) => {
|
||||||
let res: ExchangeCodeVercelResponse;
|
const res: ExchangeCodeVercelResponse = (
|
||||||
try {
|
await request.post(
|
||||||
res = (
|
INTEGRATION_VERCEL_TOKEN_URL,
|
||||||
await request.post(
|
new URLSearchParams({
|
||||||
INTEGRATION_VERCEL_TOKEN_URL,
|
code: code,
|
||||||
new URLSearchParams({
|
client_id: await getClientIdVercel(),
|
||||||
code: code,
|
client_secret: await getClientSecretVercel(),
|
||||||
client_id: await getClientIdVercel(),
|
redirect_uri: `${await getSiteURL()}/integrations/vercel/oauth2/callback`,
|
||||||
client_secret: await getClientSecretVercel(),
|
} as any)
|
||||||
redirect_uri: `${await getSiteURL()}/integrations/vercel/oauth2/callback`
|
)
|
||||||
} as any)
|
).data;
|
||||||
)
|
|
||||||
).data;
|
|
||||||
} catch (err) {
|
|
||||||
Sentry.setUser(null);
|
|
||||||
Sentry.captureException(err);
|
|
||||||
throw new Error(`Failed OAuth2 code-token exchange with Vercel [err=${err}]`);
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
accessToken: res.access_token,
|
accessToken: res.access_token,
|
||||||
refreshToken: null,
|
refreshToken: null,
|
||||||
accessExpiresAt: null,
|
accessExpiresAt: null,
|
||||||
teamId: res.team_id
|
teamId: res.team_id,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -273,47 +239,39 @@ const exchangeCodeVercel = async ({ code }: { code: string }) => {
|
|||||||
* @returns {Date} obj2.accessExpiresAt - date of expiration for access token
|
* @returns {Date} obj2.accessExpiresAt - date of expiration for access token
|
||||||
*/
|
*/
|
||||||
const exchangeCodeNetlify = async ({ code }: { code: string }) => {
|
const exchangeCodeNetlify = async ({ code }: { code: string }) => {
|
||||||
let res: ExchangeCodeNetlifyResponse;
|
const res: ExchangeCodeNetlifyResponse = (
|
||||||
let accountId;
|
await request.post(
|
||||||
try {
|
INTEGRATION_NETLIFY_TOKEN_URL,
|
||||||
res = (
|
new URLSearchParams({
|
||||||
await request.post(
|
grant_type: "authorization_code",
|
||||||
INTEGRATION_NETLIFY_TOKEN_URL,
|
code: code,
|
||||||
new URLSearchParams({
|
client_id: await getClientIdNetlify(),
|
||||||
grant_type: 'authorization_code',
|
client_secret: await getClientSecretNetlify(),
|
||||||
code: code,
|
redirect_uri: `${await getSiteURL()}/integrations/netlify/oauth2/callback`,
|
||||||
client_id: await getClientIdNetlify(),
|
} as any)
|
||||||
client_secret: await getClientSecretNetlify(),
|
)
|
||||||
redirect_uri: `${await getSiteURL()}/integrations/netlify/oauth2/callback`
|
).data;
|
||||||
} as any)
|
|
||||||
)
|
|
||||||
).data;
|
|
||||||
|
|
||||||
const res2 = await request.get('https://api.netlify.com/api/v1/sites', {
|
const res2 = await request.get("https://api.netlify.com/api/v1/sites", {
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${res.access_token}`,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const res3 = (
|
||||||
|
await request.get("https://api.netlify.com/api/v1/accounts", {
|
||||||
headers: {
|
headers: {
|
||||||
Authorization: `Bearer ${res.access_token}`
|
Authorization: `Bearer ${res.access_token}`,
|
||||||
}
|
},
|
||||||
});
|
})
|
||||||
|
).data;
|
||||||
|
|
||||||
const res3 = (
|
const accountId = res3[0].id;
|
||||||
await request.get('https://api.netlify.com/api/v1/accounts', {
|
|
||||||
headers: {
|
|
||||||
Authorization: `Bearer ${res.access_token}`
|
|
||||||
}
|
|
||||||
})
|
|
||||||
).data;
|
|
||||||
|
|
||||||
accountId = res3[0].id;
|
|
||||||
} catch (err) {
|
|
||||||
Sentry.setUser(null);
|
|
||||||
Sentry.captureException(err);
|
|
||||||
throw new Error('Failed OAuth2 code-token exchange with Netlify');
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
accessToken: res.access_token,
|
accessToken: res.access_token,
|
||||||
refreshToken: res.refresh_token,
|
refreshToken: res.refresh_token,
|
||||||
accountId
|
accountId,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -328,33 +286,25 @@ const exchangeCodeNetlify = async ({ code }: { code: string }) => {
|
|||||||
* @returns {Date} obj2.accessExpiresAt - date of expiration for access token
|
* @returns {Date} obj2.accessExpiresAt - date of expiration for access token
|
||||||
*/
|
*/
|
||||||
const exchangeCodeGithub = async ({ code }: { code: string }) => {
|
const exchangeCodeGithub = async ({ code }: { code: string }) => {
|
||||||
let res: ExchangeCodeGithubResponse;
|
const res: ExchangeCodeGithubResponse = (
|
||||||
try {
|
await request.get(INTEGRATION_GITHUB_TOKEN_URL, {
|
||||||
res = (
|
params: {
|
||||||
await request.get(INTEGRATION_GITHUB_TOKEN_URL, {
|
client_id: await getClientIdGitHub(),
|
||||||
params: {
|
client_secret: await getClientSecretGitHub(),
|
||||||
client_id: await getClientIdGitHub(),
|
code: code,
|
||||||
client_secret: await getClientSecretGitHub(),
|
redirect_uri: `${await getSiteURL()}/integrations/github/oauth2/callback`,
|
||||||
code: code,
|
},
|
||||||
redirect_uri: `${await getSiteURL()}/integrations/github/oauth2/callback`
|
headers: {
|
||||||
},
|
Accept: "application/json",
|
||||||
headers: {
|
"Accept-Encoding": "application/json",
|
||||||
'Accept': 'application/json',
|
},
|
||||||
'Accept-Encoding': 'application/json'
|
})
|
||||||
}
|
).data;
|
||||||
})
|
|
||||||
).data;
|
|
||||||
|
|
||||||
} catch (err) {
|
|
||||||
Sentry.setUser(null);
|
|
||||||
Sentry.captureException(err);
|
|
||||||
throw new Error('Failed OAuth2 code-token exchange with Github');
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
accessToken: res.access_token,
|
accessToken: res.access_token,
|
||||||
refreshToken: null,
|
refreshToken: null,
|
||||||
accessExpiresAt: null
|
accessExpiresAt: null,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -369,42 +319,32 @@ const exchangeCodeGithub = async ({ code }: { code: string }) => {
|
|||||||
* @returns {Date} obj2.accessExpiresAt - date of expiration for access token
|
* @returns {Date} obj2.accessExpiresAt - date of expiration for access token
|
||||||
*/
|
*/
|
||||||
const exchangeCodeGitlab = async ({ code }: { code: string }) => {
|
const exchangeCodeGitlab = async ({ code }: { code: string }) => {
|
||||||
let res: ExchangeCodeGitlabResponse;
|
|
||||||
const accessExpiresAt = new Date();
|
const accessExpiresAt = new Date();
|
||||||
|
const res: ExchangeCodeGitlabResponse = (
|
||||||
try {
|
await request.post(
|
||||||
res = (
|
INTEGRATION_GITLAB_TOKEN_URL,
|
||||||
await request.post(
|
new URLSearchParams({
|
||||||
INTEGRATION_GITLAB_TOKEN_URL,
|
grant_type: "authorization_code",
|
||||||
new URLSearchParams({
|
code: code,
|
||||||
grant_type: 'authorization_code',
|
client_id: await getClientIdGitLab(),
|
||||||
code: code,
|
client_secret: await getClientSecretGitLab(),
|
||||||
client_id: await getClientIdGitLab(),
|
redirect_uri: `${await getSiteURL()}/integrations/gitlab/oauth2/callback`,
|
||||||
client_secret: await getClientSecretGitLab(),
|
} as any),
|
||||||
redirect_uri: `${await getSiteURL()}/integrations/gitlab/oauth2/callback`
|
{
|
||||||
} as any),
|
headers: {
|
||||||
{
|
"Accept-Encoding": "application/json",
|
||||||
headers: {
|
},
|
||||||
"Accept-Encoding": "application/json",
|
}
|
||||||
}
|
)
|
||||||
}
|
).data;
|
||||||
)
|
|
||||||
).data;
|
accessExpiresAt.setSeconds(accessExpiresAt.getSeconds() + res.expires_in);
|
||||||
|
|
||||||
accessExpiresAt.setSeconds(
|
|
||||||
accessExpiresAt.getSeconds() + res.expires_in
|
|
||||||
);
|
|
||||||
} catch (err) {
|
|
||||||
Sentry.setUser(null);
|
|
||||||
Sentry.captureException(err);
|
|
||||||
throw new Error('Failed OAuth2 code-token exchange with Gitlab');
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
accessToken: res.access_token,
|
accessToken: res.access_token,
|
||||||
refreshToken: res.refresh_token,
|
refreshToken: res.refresh_token,
|
||||||
accessExpiresAt
|
accessExpiresAt,
|
||||||
};
|
};
|
||||||
}
|
};
|
||||||
|
|
||||||
export { exchangeCode };
|
export { exchangeCode };
|
||||||
|
|||||||
+114
-145
@@ -1,29 +1,24 @@
|
|||||||
import * as Sentry from '@sentry/node';
|
import request from "../config/request";
|
||||||
import request from '../config/request';
|
import { IIntegrationAuth } from "../models";
|
||||||
import {
|
import {
|
||||||
IIntegrationAuth
|
INTEGRATION_AZURE_KEY_VAULT,
|
||||||
} from '../models';
|
|
||||||
import {
|
|
||||||
INTEGRATION_AZURE_KEY_VAULT,
|
|
||||||
INTEGRATION_HEROKU,
|
INTEGRATION_HEROKU,
|
||||||
INTEGRATION_GITLAB,
|
INTEGRATION_GITLAB,
|
||||||
} from '../variables';
|
} from "../variables";
|
||||||
import {
|
import {
|
||||||
INTEGRATION_AZURE_TOKEN_URL,
|
INTEGRATION_AZURE_TOKEN_URL,
|
||||||
INTEGRATION_HEROKU_TOKEN_URL,
|
INTEGRATION_HEROKU_TOKEN_URL,
|
||||||
INTEGRATION_GITLAB_TOKEN_URL
|
INTEGRATION_GITLAB_TOKEN_URL,
|
||||||
} from '../variables';
|
} from "../variables";
|
||||||
import {
|
import { IntegrationService } from "../services";
|
||||||
IntegrationService
|
|
||||||
} from '../services';
|
|
||||||
import {
|
import {
|
||||||
getSiteURL,
|
getSiteURL,
|
||||||
getClientIdAzure,
|
getClientIdAzure,
|
||||||
getClientSecretAzure,
|
getClientSecretAzure,
|
||||||
getClientSecretHeroku,
|
getClientSecretHeroku,
|
||||||
getClientIdGitLab,
|
getClientIdGitLab,
|
||||||
getClientSecretGitLab
|
getClientSecretGitLab,
|
||||||
} from '../config';
|
} from "../config";
|
||||||
|
|
||||||
interface RefreshTokenAzureResponse {
|
interface RefreshTokenAzureResponse {
|
||||||
token_type: string;
|
token_type: string;
|
||||||
@@ -60,60 +55,57 @@ interface RefreshTokenGitLabResponse {
|
|||||||
*/
|
*/
|
||||||
const exchangeRefresh = async ({
|
const exchangeRefresh = async ({
|
||||||
integrationAuth,
|
integrationAuth,
|
||||||
refreshToken
|
refreshToken,
|
||||||
}: {
|
}: {
|
||||||
integrationAuth: IIntegrationAuth;
|
integrationAuth: IIntegrationAuth;
|
||||||
refreshToken: string;
|
refreshToken: string;
|
||||||
}) => {
|
}) => {
|
||||||
|
|
||||||
interface TokenDetails {
|
interface TokenDetails {
|
||||||
accessToken: string;
|
accessToken: string;
|
||||||
refreshToken: string;
|
refreshToken: string;
|
||||||
accessExpiresAt: Date;
|
accessExpiresAt: Date;
|
||||||
}
|
}
|
||||||
|
|
||||||
let tokenDetails: TokenDetails;
|
let tokenDetails: TokenDetails;
|
||||||
try {
|
switch (integrationAuth.integration) {
|
||||||
switch (integrationAuth.integration) {
|
case INTEGRATION_AZURE_KEY_VAULT:
|
||||||
case INTEGRATION_AZURE_KEY_VAULT:
|
tokenDetails = await exchangeRefreshAzure({
|
||||||
tokenDetails = await exchangeRefreshAzure({
|
refreshToken,
|
||||||
refreshToken
|
|
||||||
});
|
|
||||||
break;
|
|
||||||
case INTEGRATION_HEROKU:
|
|
||||||
tokenDetails = await exchangeRefreshHeroku({
|
|
||||||
refreshToken
|
|
||||||
});
|
|
||||||
break;
|
|
||||||
case INTEGRATION_GITLAB:
|
|
||||||
tokenDetails = await exchangeRefreshGitLab({
|
|
||||||
refreshToken
|
|
||||||
});
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
throw new Error('Failed to exchange token for incompatible integration');
|
|
||||||
}
|
|
||||||
|
|
||||||
if (tokenDetails?.accessToken && tokenDetails?.refreshToken && tokenDetails?.accessExpiresAt) {
|
|
||||||
await IntegrationService.setIntegrationAuthAccess({
|
|
||||||
integrationAuthId: integrationAuth._id.toString(),
|
|
||||||
accessId: null,
|
|
||||||
accessToken: tokenDetails.accessToken,
|
|
||||||
accessExpiresAt: tokenDetails.accessExpiresAt
|
|
||||||
});
|
});
|
||||||
|
break;
|
||||||
await IntegrationService.setIntegrationAuthRefresh({
|
case INTEGRATION_HEROKU:
|
||||||
integrationAuthId: integrationAuth._id.toString(),
|
tokenDetails = await exchangeRefreshHeroku({
|
||||||
refreshToken: tokenDetails.refreshToken
|
refreshToken,
|
||||||
});
|
});
|
||||||
}
|
break;
|
||||||
|
case INTEGRATION_GITLAB:
|
||||||
return tokenDetails.accessToken;
|
tokenDetails = await exchangeRefreshGitLab({
|
||||||
} catch (err) {
|
refreshToken,
|
||||||
Sentry.setUser(null);
|
});
|
||||||
Sentry.captureException(err);
|
break;
|
||||||
throw new Error('Failed to get new OAuth2 access token');
|
default:
|
||||||
|
throw new Error("Failed to exchange token for incompatible integration");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
tokenDetails?.accessToken &&
|
||||||
|
tokenDetails?.refreshToken &&
|
||||||
|
tokenDetails?.accessExpiresAt
|
||||||
|
) {
|
||||||
|
await IntegrationService.setIntegrationAuthAccess({
|
||||||
|
integrationAuthId: integrationAuth._id.toString(),
|
||||||
|
accessId: null,
|
||||||
|
accessToken: tokenDetails.accessToken,
|
||||||
|
accessExpiresAt: tokenDetails.accessExpiresAt,
|
||||||
|
});
|
||||||
|
|
||||||
|
await IntegrationService.setIntegrationAuthRefresh({
|
||||||
|
integrationAuthId: integrationAuth._id.toString(),
|
||||||
|
refreshToken: tokenDetails.refreshToken,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return tokenDetails.accessToken;
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -124,38 +116,30 @@ const exchangeRefresh = async ({
|
|||||||
* @returns
|
* @returns
|
||||||
*/
|
*/
|
||||||
const exchangeRefreshAzure = async ({
|
const exchangeRefreshAzure = async ({
|
||||||
refreshToken
|
refreshToken,
|
||||||
}: {
|
}: {
|
||||||
refreshToken: string;
|
refreshToken: string;
|
||||||
}) => {
|
}) => {
|
||||||
try {
|
const accessExpiresAt = new Date();
|
||||||
const accessExpiresAt = new Date();
|
const { data }: { data: RefreshTokenAzureResponse } = await request.post(
|
||||||
const { data }: { data: RefreshTokenAzureResponse } = await request.post(
|
INTEGRATION_AZURE_TOKEN_URL,
|
||||||
INTEGRATION_AZURE_TOKEN_URL,
|
new URLSearchParams({
|
||||||
new URLSearchParams({
|
client_id: await getClientIdAzure(),
|
||||||
client_id: await getClientIdAzure(),
|
scope: "openid offline_access",
|
||||||
scope: 'openid offline_access',
|
refresh_token: refreshToken,
|
||||||
refresh_token: refreshToken,
|
grant_type: "refresh_token",
|
||||||
grant_type: 'refresh_token',
|
client_secret: await getClientSecretAzure(),
|
||||||
client_secret: await getClientSecretAzure()
|
} as any)
|
||||||
} as any)
|
);
|
||||||
);
|
|
||||||
|
|
||||||
accessExpiresAt.setSeconds(
|
|
||||||
accessExpiresAt.getSeconds() + data.expires_in
|
|
||||||
);
|
|
||||||
|
|
||||||
return ({
|
accessExpiresAt.setSeconds(accessExpiresAt.getSeconds() + data.expires_in);
|
||||||
accessToken: data.access_token,
|
|
||||||
refreshToken: data.refresh_token,
|
return {
|
||||||
accessExpiresAt
|
accessToken: data.access_token,
|
||||||
});
|
refreshToken: data.refresh_token,
|
||||||
} catch (err) {
|
accessExpiresAt,
|
||||||
Sentry.setUser(null);
|
};
|
||||||
Sentry.captureException(err);
|
};
|
||||||
throw new Error('Failed to get refresh OAuth2 access token for Azure');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Return new access token by exchanging refresh token [refreshToken] for the
|
* Return new access token by exchanging refresh token [refreshToken] for the
|
||||||
@@ -165,39 +149,31 @@ const exchangeRefreshAzure = async ({
|
|||||||
* @returns
|
* @returns
|
||||||
*/
|
*/
|
||||||
const exchangeRefreshHeroku = async ({
|
const exchangeRefreshHeroku = async ({
|
||||||
refreshToken
|
refreshToken,
|
||||||
}: {
|
}: {
|
||||||
refreshToken: string;
|
refreshToken: string;
|
||||||
}) => {
|
}) => {
|
||||||
try {
|
const accessExpiresAt = new Date();
|
||||||
const accessExpiresAt = new Date();
|
const {
|
||||||
const {
|
data,
|
||||||
data
|
}: {
|
||||||
}: {
|
data: RefreshTokenHerokuResponse;
|
||||||
data: RefreshTokenHerokuResponse
|
} = await request.post(
|
||||||
} = await request.post(
|
INTEGRATION_HEROKU_TOKEN_URL,
|
||||||
INTEGRATION_HEROKU_TOKEN_URL,
|
new URLSearchParams({
|
||||||
new URLSearchParams({
|
grant_type: "refresh_token",
|
||||||
grant_type: 'refresh_token',
|
refresh_token: refreshToken,
|
||||||
refresh_token: refreshToken,
|
client_secret: await getClientSecretHeroku(),
|
||||||
client_secret: await getClientSecretHeroku()
|
} as any)
|
||||||
} as any)
|
);
|
||||||
);
|
|
||||||
|
|
||||||
accessExpiresAt.setSeconds(
|
accessExpiresAt.setSeconds(accessExpiresAt.getSeconds() + data.expires_in);
|
||||||
accessExpiresAt.getSeconds() + data.expires_in
|
|
||||||
);
|
|
||||||
|
|
||||||
return ({
|
return {
|
||||||
accessToken: data.access_token,
|
accessToken: data.access_token,
|
||||||
refreshToken: data.refresh_token,
|
refreshToken: data.refresh_token,
|
||||||
accessExpiresAt
|
accessExpiresAt,
|
||||||
});
|
};
|
||||||
} catch (err) {
|
|
||||||
Sentry.setUser(null);
|
|
||||||
Sentry.captureException(err);
|
|
||||||
throw new Error('Failed to refresh OAuth2 access token for Heroku');
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -208,45 +184,38 @@ const exchangeRefreshHeroku = async ({
|
|||||||
* @returns
|
* @returns
|
||||||
*/
|
*/
|
||||||
const exchangeRefreshGitLab = async ({
|
const exchangeRefreshGitLab = async ({
|
||||||
refreshToken
|
refreshToken,
|
||||||
}: {
|
}: {
|
||||||
refreshToken: string;
|
refreshToken: string;
|
||||||
}) => {
|
}) => {
|
||||||
try {
|
const accessExpiresAt = new Date();
|
||||||
const accessExpiresAt = new Date();
|
const {
|
||||||
const {
|
data,
|
||||||
data
|
}: {
|
||||||
}: {
|
data: RefreshTokenGitLabResponse;
|
||||||
data: RefreshTokenGitLabResponse
|
} = await request.post(
|
||||||
} = await request.post(
|
INTEGRATION_GITLAB_TOKEN_URL,
|
||||||
INTEGRATION_GITLAB_TOKEN_URL,
|
new URLSearchParams({
|
||||||
new URLSearchParams({
|
grant_type: "refresh_token",
|
||||||
grant_type: 'refresh_token',
|
refresh_token: refreshToken,
|
||||||
refresh_token: refreshToken,
|
client_id: await getClientIdGitLab,
|
||||||
client_id: await getClientIdGitLab,
|
client_secret: await getClientSecretGitLab(),
|
||||||
client_secret: await getClientSecretGitLab(),
|
redirect_uri: `${await getSiteURL()}/integrations/gitlab/oauth2/callback`,
|
||||||
redirect_uri: `${await getSiteURL()}/integrations/gitlab/oauth2/callback`
|
} as any),
|
||||||
} as any),
|
{
|
||||||
{
|
headers: {
|
||||||
headers: {
|
"Accept-Encoding": "application/json",
|
||||||
"Accept-Encoding": "application/json",
|
},
|
||||||
}
|
}
|
||||||
});
|
);
|
||||||
|
|
||||||
accessExpiresAt.setSeconds(
|
accessExpiresAt.setSeconds(accessExpiresAt.getSeconds() + data.expires_in);
|
||||||
accessExpiresAt.getSeconds() + data.expires_in
|
|
||||||
);
|
|
||||||
|
|
||||||
return ({
|
return {
|
||||||
accessToken: data.access_token,
|
accessToken: data.access_token,
|
||||||
refreshToken: data.refresh_token,
|
refreshToken: data.refresh_token,
|
||||||
accessExpiresAt
|
accessExpiresAt,
|
||||||
});
|
};
|
||||||
} catch (err) {
|
|
||||||
Sentry.setUser(null);
|
|
||||||
Sentry.captureException(err);
|
|
||||||
throw new Error('Failed to refresh OAuth2 access token for GitLab');
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export { exchangeRefresh };
|
export { exchangeRefresh };
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
import * as Sentry from '@sentry/node';
|
|
||||||
import {
|
import {
|
||||||
IIntegrationAuth,
|
IIntegrationAuth,
|
||||||
IntegrationAuth,
|
IntegrationAuth,
|
||||||
@@ -22,34 +21,28 @@ const revokeAccess = async ({
|
|||||||
accessToken: string;
|
accessToken: string;
|
||||||
}) => {
|
}) => {
|
||||||
let deletedIntegrationAuth;
|
let deletedIntegrationAuth;
|
||||||
try {
|
// add any integration-specific revocation logic
|
||||||
// add any integration-specific revocation logic
|
switch (integrationAuth.integration) {
|
||||||
switch (integrationAuth.integration) {
|
case INTEGRATION_HEROKU:
|
||||||
case INTEGRATION_HEROKU:
|
break;
|
||||||
break;
|
case INTEGRATION_VERCEL:
|
||||||
case INTEGRATION_VERCEL:
|
break;
|
||||||
break;
|
case INTEGRATION_NETLIFY:
|
||||||
case INTEGRATION_NETLIFY:
|
break;
|
||||||
break;
|
case INTEGRATION_GITHUB:
|
||||||
case INTEGRATION_GITHUB:
|
break;
|
||||||
break;
|
case INTEGRATION_GITLAB:
|
||||||
case INTEGRATION_GITLAB:
|
break;
|
||||||
break;
|
}
|
||||||
}
|
|
||||||
|
|
||||||
deletedIntegrationAuth = await IntegrationAuth.findOneAndDelete({
|
deletedIntegrationAuth = await IntegrationAuth.findOneAndDelete({
|
||||||
_id: integrationAuth._id
|
_id: integrationAuth._id
|
||||||
|
});
|
||||||
|
|
||||||
|
if (deletedIntegrationAuth) {
|
||||||
|
await Integration.deleteMany({
|
||||||
|
integrationAuth: deletedIntegrationAuth._id
|
||||||
});
|
});
|
||||||
|
|
||||||
if (deletedIntegrationAuth) {
|
|
||||||
await Integration.deleteMany({
|
|
||||||
integrationAuth: deletedIntegrationAuth._id
|
|
||||||
});
|
|
||||||
}
|
|
||||||
} catch (err) {
|
|
||||||
Sentry.setUser(null);
|
|
||||||
Sentry.captureException(err);
|
|
||||||
throw new Error('Failed to delete integration authorization');
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return deletedIntegrationAuth;
|
return deletedIntegrationAuth;
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
import * as Sentry from "@sentry/node";
|
|
||||||
import {
|
import {
|
||||||
IIntegrationAuth
|
IIntegrationAuth
|
||||||
} from '../models';
|
} from '../models';
|
||||||
@@ -31,21 +30,15 @@ const getTeams = async ({
|
|||||||
}) => {
|
}) => {
|
||||||
|
|
||||||
let teams: Team[] = [];
|
let teams: Team[] = [];
|
||||||
try {
|
|
||||||
switch (integrationAuth.integration) {
|
switch (integrationAuth.integration) {
|
||||||
case INTEGRATION_GITLAB:
|
case INTEGRATION_GITLAB:
|
||||||
teams = await getTeamsGitLab({
|
teams = await getTeamsGitLab({
|
||||||
accessToken
|
accessToken
|
||||||
});
|
});
|
||||||
break;
|
break;
|
||||||
}
|
|
||||||
} catch (err) {
|
|
||||||
Sentry.setUser(null);
|
|
||||||
Sentry.captureException(err);
|
|
||||||
throw new Error('Failed to get integration teams');
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
return teams;
|
return teams;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -63,30 +56,24 @@ const getTeamsGitLab = async ({
|
|||||||
accessToken: string;
|
accessToken: string;
|
||||||
}) => {
|
}) => {
|
||||||
let teams: Team[] = [];
|
let teams: Team[] = [];
|
||||||
try {
|
const res = (await request.get(
|
||||||
const res = (await request.get(
|
`${INTEGRATION_GITLAB_API_URL}/v4/groups`,
|
||||||
`${INTEGRATION_GITLAB_API_URL}/v4/groups`,
|
{
|
||||||
{
|
headers: {
|
||||||
headers: {
|
Authorization: `Bearer ${accessToken}`,
|
||||||
Authorization: `Bearer ${accessToken}`,
|
"Accept-Encoding": "application/json"
|
||||||
"Accept-Encoding": "application/json"
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
)).data;
|
}
|
||||||
|
)).data;
|
||||||
teams = res.map((t: any) => ({
|
|
||||||
name: t.name,
|
teams = res.map((t: any) => ({
|
||||||
teamId: t.id
|
name: t.name,
|
||||||
}));
|
teamId: t.id
|
||||||
} catch (err) {
|
}));
|
||||||
Sentry.setUser(null);
|
|
||||||
Sentry.captureException(err);
|
|
||||||
throw new Error("Failed to get GitLab integration teams");
|
|
||||||
}
|
|
||||||
|
|
||||||
return teams;
|
return teams;
|
||||||
}
|
}
|
||||||
|
|
||||||
export {
|
export {
|
||||||
getTeams
|
getTeams
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
import * as Sentry from '@sentry/node';
|
|
||||||
import { Types } from 'mongoose';
|
import { Types } from 'mongoose';
|
||||||
import { Request, Response, NextFunction } from 'express';
|
import { Request, Response, NextFunction } from 'express';
|
||||||
import { IntegrationAuth, IWorkspace } from '../models';
|
import { IntegrationAuth, IWorkspace } from '../models';
|
||||||
|
|||||||
+16
-48
@@ -1,7 +1,6 @@
|
|||||||
import nacl from 'tweetnacl';
|
import nacl from 'tweetnacl';
|
||||||
import util from 'tweetnacl-util';
|
import util from 'tweetnacl-util';
|
||||||
import AesGCM from './aes-gcm';
|
import AesGCM from './aes-gcm';
|
||||||
import * as Sentry from '@sentry/node';
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Return new base64, NaCl, public-private key pair.
|
* Return new base64, NaCl, public-private key pair.
|
||||||
@@ -38,20 +37,13 @@ const encryptAsymmetric = ({
|
|||||||
publicKey: string;
|
publicKey: string;
|
||||||
privateKey: string;
|
privateKey: string;
|
||||||
}) => {
|
}) => {
|
||||||
let nonce, ciphertext;
|
const nonce = nacl.randomBytes(24);
|
||||||
try {
|
const ciphertext = nacl.box(
|
||||||
nonce = nacl.randomBytes(24);
|
util.decodeUTF8(plaintext),
|
||||||
ciphertext = nacl.box(
|
nonce,
|
||||||
util.decodeUTF8(plaintext),
|
util.decodeBase64(publicKey),
|
||||||
nonce,
|
util.decodeBase64(privateKey)
|
||||||
util.decodeBase64(publicKey),
|
);
|
||||||
util.decodeBase64(privateKey)
|
|
||||||
);
|
|
||||||
} catch (err) {
|
|
||||||
Sentry.setUser(null);
|
|
||||||
Sentry.captureException(err);
|
|
||||||
throw new Error('Failed to perform asymmetric encryption');
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
ciphertext: util.encodeBase64(ciphertext),
|
ciphertext: util.encodeBase64(ciphertext),
|
||||||
@@ -80,19 +72,12 @@ const decryptAsymmetric = ({
|
|||||||
publicKey: string;
|
publicKey: string;
|
||||||
privateKey: string;
|
privateKey: string;
|
||||||
}): string => {
|
}): string => {
|
||||||
let plaintext: any;
|
const plaintext: any = nacl.box.open(
|
||||||
try {
|
util.decodeBase64(ciphertext),
|
||||||
plaintext = nacl.box.open(
|
util.decodeBase64(nonce),
|
||||||
util.decodeBase64(ciphertext),
|
util.decodeBase64(publicKey),
|
||||||
util.decodeBase64(nonce),
|
util.decodeBase64(privateKey)
|
||||||
util.decodeBase64(publicKey),
|
);
|
||||||
util.decodeBase64(privateKey)
|
|
||||||
);
|
|
||||||
} catch (err) {
|
|
||||||
Sentry.setUser(null);
|
|
||||||
Sentry.captureException(err);
|
|
||||||
throw new Error('Failed to perform asymmetric decryption');
|
|
||||||
}
|
|
||||||
|
|
||||||
return util.encodeUTF8(plaintext);
|
return util.encodeUTF8(plaintext);
|
||||||
};
|
};
|
||||||
@@ -110,17 +95,8 @@ const encryptSymmetric = ({
|
|||||||
plaintext: string;
|
plaintext: string;
|
||||||
key: string;
|
key: string;
|
||||||
}) => {
|
}) => {
|
||||||
let ciphertext, iv, tag;
|
const obj = AesGCM.encrypt(plaintext, key);
|
||||||
try {
|
const { ciphertext, iv, tag } = obj;
|
||||||
const obj = AesGCM.encrypt(plaintext, key);
|
|
||||||
ciphertext = obj.ciphertext;
|
|
||||||
iv = obj.iv;
|
|
||||||
tag = obj.tag;
|
|
||||||
} catch (err) {
|
|
||||||
Sentry.setUser(null);
|
|
||||||
Sentry.captureException(err);
|
|
||||||
throw new Error('Failed to perform symmetric encryption');
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
ciphertext,
|
ciphertext,
|
||||||
@@ -150,15 +126,7 @@ const decryptSymmetric = ({
|
|||||||
tag: string;
|
tag: string;
|
||||||
key: string;
|
key: string;
|
||||||
}): string => {
|
}): string => {
|
||||||
let plaintext;
|
const plaintext = AesGCM.decrypt(ciphertext, iv, tag, key);
|
||||||
try {
|
|
||||||
plaintext = AesGCM.decrypt(ciphertext, iv, tag, key);
|
|
||||||
} catch (err) {
|
|
||||||
Sentry.setUser(null);
|
|
||||||
Sentry.captureException(err);
|
|
||||||
throw new Error('Failed to perform symmetric decryption');
|
|
||||||
}
|
|
||||||
|
|
||||||
return plaintext;
|
return plaintext;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -28,14 +28,14 @@ describe('Crypto', () => {
|
|||||||
test('should throw error if publicKey is undefined', () => {
|
test('should throw error if publicKey is undefined', () => {
|
||||||
expect(() => {
|
expect(() => {
|
||||||
encryptAsymmetric({ plaintext, publicKey, privateKey });
|
encryptAsymmetric({ plaintext, publicKey, privateKey });
|
||||||
}).toThrowError('Failed to perform asymmetric encryption');
|
}).toThrowError('invalid encoding');
|
||||||
});
|
});
|
||||||
|
|
||||||
test('should throw error if publicKey is empty string', () => {
|
test('should throw error if publicKey is empty string', () => {
|
||||||
publicKey = '';
|
publicKey = '';
|
||||||
expect(() => {
|
expect(() => {
|
||||||
encryptAsymmetric({ plaintext, publicKey, privateKey });
|
encryptAsymmetric({ plaintext, publicKey, privateKey });
|
||||||
}).toThrowError('Failed to perform asymmetric encryption');
|
}).toThrowError('bad public key size');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -47,14 +47,14 @@ describe('Crypto', () => {
|
|||||||
test('should throw error if privateKey is undefined', () => {
|
test('should throw error if privateKey is undefined', () => {
|
||||||
expect(() => {
|
expect(() => {
|
||||||
encryptAsymmetric({ plaintext, publicKey, privateKey });
|
encryptAsymmetric({ plaintext, publicKey, privateKey });
|
||||||
}).toThrowError('Failed to perform asymmetric encryption');
|
}).toThrowError('invalid encoding');
|
||||||
});
|
});
|
||||||
|
|
||||||
test('should throw error if privateKey is empty string', () => {
|
test('should throw error if privateKey is empty string', () => {
|
||||||
privateKey = '';
|
privateKey = '';
|
||||||
expect(() => {
|
expect(() => {
|
||||||
encryptAsymmetric({ plaintext, publicKey, privateKey });
|
encryptAsymmetric({ plaintext, publicKey, privateKey });
|
||||||
}).toThrowError('Failed to perform asymmetric encryption');
|
}).toThrowError('bad secret key size');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -66,7 +66,7 @@ describe('Crypto', () => {
|
|||||||
test('should throw error if plaintext is undefined', () => {
|
test('should throw error if plaintext is undefined', () => {
|
||||||
expect(() => {
|
expect(() => {
|
||||||
encryptAsymmetric({ plaintext, publicKey, privateKey });
|
encryptAsymmetric({ plaintext, publicKey, privateKey });
|
||||||
}).toThrowError('Failed to perform asymmetric encryption');
|
}).toThrowError('expected string');
|
||||||
});
|
});
|
||||||
|
|
||||||
test('should encrypt plaintext containing special characters', () => {
|
test('should encrypt plaintext containing special characters', () => {
|
||||||
@@ -130,7 +130,7 @@ describe('Crypto', () => {
|
|||||||
publicKey,
|
publicKey,
|
||||||
privateKey
|
privateKey
|
||||||
});
|
});
|
||||||
}).toThrowError('Failed to perform asymmetric decryption');
|
}).toThrowError('invalid encoding');
|
||||||
});
|
});
|
||||||
|
|
||||||
test('should throw error if nonce is modified', () => {
|
test('should throw error if nonce is modified', () => {
|
||||||
@@ -149,7 +149,7 @@ describe('Crypto', () => {
|
|||||||
publicKey,
|
publicKey,
|
||||||
privateKey
|
privateKey
|
||||||
});
|
});
|
||||||
}).toThrowError('Failed to perform asymmetric decryption');
|
}).toThrowError('invalid encoding');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -170,7 +170,7 @@ describe('Crypto', () => {
|
|||||||
const invalidKey = 'invalid-key';
|
const invalidKey = 'invalid-key';
|
||||||
expect(() => {
|
expect(() => {
|
||||||
encryptSymmetric({ plaintext, key: invalidKey });
|
encryptSymmetric({ plaintext, key: invalidKey });
|
||||||
}).toThrowError('Failed to perform symmetric encryption');
|
}).toThrowError('Invalid key length');
|
||||||
});
|
});
|
||||||
|
|
||||||
test('should throw an error when invalid key is provided', () => {
|
test('should throw an error when invalid key is provided', () => {
|
||||||
@@ -179,7 +179,7 @@ describe('Crypto', () => {
|
|||||||
|
|
||||||
expect(() => {
|
expect(() => {
|
||||||
encryptSymmetric({ plaintext, key: invalidKey });
|
encryptSymmetric({ plaintext, key: invalidKey });
|
||||||
}).toThrowError('Failed to perform symmetric encryption');
|
}).toThrowError('Invalid key length');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -209,7 +209,7 @@ describe('Crypto', () => {
|
|||||||
tag,
|
tag,
|
||||||
key
|
key
|
||||||
});
|
});
|
||||||
}).toThrowError('Failed to perform symmetric decryption');
|
}).toThrowError('Unsupported state or unable to authenticate data');
|
||||||
});
|
});
|
||||||
|
|
||||||
test('should fail if iv is modified', () => {
|
test('should fail if iv is modified', () => {
|
||||||
@@ -221,7 +221,7 @@ describe('Crypto', () => {
|
|||||||
tag,
|
tag,
|
||||||
key
|
key
|
||||||
});
|
});
|
||||||
}).toThrowError('Failed to perform symmetric decryption');
|
}).toThrowError('Unsupported state or unable to authenticate data');
|
||||||
});
|
});
|
||||||
|
|
||||||
test('should fail if tag is modified', () => {
|
test('should fail if tag is modified', () => {
|
||||||
@@ -233,7 +233,7 @@ describe('Crypto', () => {
|
|||||||
tag: modifiedTag,
|
tag: modifiedTag,
|
||||||
key
|
key
|
||||||
});
|
});
|
||||||
}).toThrowError('Failed to perform symmetric decryption');
|
}).toThrowError(/Invalid authentication tag length: \d+/);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('should throw an error when decryption fails', () => {
|
test('should throw an error when decryption fails', () => {
|
||||||
@@ -245,7 +245,7 @@ describe('Crypto', () => {
|
|||||||
tag,
|
tag,
|
||||||
key: invalidKey
|
key: invalidKey
|
||||||
});
|
});
|
||||||
}).toThrowError('Failed to perform symmetric decryption');
|
}).toThrowError('Invalid key length');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user