Merge pull request #298 from Aashish-Upadhyay-101/circleci-integration-branch

Circleci integration branch
This commit is contained in:
BlackMagiq
2023-02-13 18:03:22 +07:00
committed by GitHub
17 changed files with 1376 additions and 1052 deletions
@@ -35,14 +35,11 @@ export const getIntegrationAuth = async (req: Request, res: Response) => {
});
}
export const getIntegrationOptions = async (
req: Request,
res: Response
) => {
export const getIntegrationOptions = async (req: Request, res: Response) => {
return res.status(200).send({
integrationOptions: INTEGRATION_OPTIONS
integrationOptions: INTEGRATION_OPTIONS,
});
}
};
/**
* Perform OAuth2 code-token exchange as part of integration [integration] for workspace with id [workspaceId]
@@ -161,18 +158,18 @@ export const getIntegrationAuthApps = async (req: Request, res: Response) => {
try {
apps = await getApps({
integrationAuth: req.integrationAuth,
accessToken: req.accessToken
accessToken: req.accessToken,
});
} catch (err) {
Sentry.setUser({ email: req.user.email });
Sentry.captureException(err);
return res.status(400).send({
message: 'Failed to get integration authorization applications'
message: "Failed to get integration authorization applications",
});
}
return res.status(200).send({
apps
apps,
});
};
@@ -187,17 +184,17 @@ export const deleteIntegrationAuth = async (req: Request, res: Response) => {
try {
integrationAuth = await revokeAccess({
integrationAuth: req.integrationAuth,
accessToken: req.accessToken
accessToken: req.accessToken,
});
} catch (err) {
Sentry.setUser({ email: req.user.email });
Sentry.captureException(err);
return res.status(400).send({
message: 'Failed to delete integration authorization'
message: "Failed to delete integration authorization",
});
}
return res.status(200).send({
integrationAuth
integrationAuth,
});
}
};
@@ -66,9 +66,9 @@ export const createIntegration = async (req: Request, res: Response) => {
}
return res.status(200).send({
integration
integration,
});
}
};
/**
* Change environment or name of integration with id [integrationId]
@@ -94,7 +94,7 @@ export const updateIntegration = async (req: Request, res: Response) => {
integration = await Integration.findOneAndUpdate(
{
_id: req.integration._id
_id: req.integration._id,
},
{
environment,
@@ -102,10 +102,10 @@ export const updateIntegration = async (req: Request, res: Response) => {
app,
appId,
targetEnvironment,
owner
owner,
},
{
new: true
new: true,
}
);
@@ -113,20 +113,20 @@ export const updateIntegration = async (req: Request, res: Response) => {
// trigger event - push secrets
EventService.handleEvent({
event: eventPushSecrets({
workspaceId: integration.workspace.toString()
})
workspaceId: integration.workspace.toString(),
}),
});
}
} catch (err) {
Sentry.setUser({ email: req.user.email });
Sentry.captureException(err);
return res.status(400).send({
message: 'Failed to update integration'
message: "Failed to update integration",
});
}
return res.status(200).send({
integration
integration,
});
};
@@ -143,19 +143,19 @@ export const deleteIntegration = async (req: Request, res: Response) => {
const { integrationId } = req.params;
integration = await Integration.findOneAndDelete({
_id: integrationId
_id: integrationId,
});
if (!integration) throw new Error('Failed to find integration');
if (!integration) throw new Error("Failed to find integration");
} catch (err) {
Sentry.setUser({ email: req.user.email });
Sentry.captureException(err);
return res.status(400).send({
message: 'Failed to delete integration'
message: "Failed to delete integration",
});
}
return res.status(200).send({
integration
integration,
});
};
@@ -1,5 +1,5 @@
import { Request, Response } from 'express';
import * as Sentry from '@sentry/node';
import { Request, Response } from "express";
import * as Sentry from "@sentry/node";
import {
Workspace,
Membership,
@@ -8,14 +8,14 @@ import {
IntegrationAuth,
IUser,
ServiceToken,
ServiceTokenData
} from '../../models';
ServiceTokenData,
} from "../../models";
import {
createWorkspace as create,
deleteWorkspace as deleteWork
} from '../../helpers/workspace';
import { addMemberships } from '../../helpers/membership';
import { ADMIN } from '../../variables';
deleteWorkspace as deleteWork,
} from "../../helpers/workspace";
import { addMemberships } from "../../helpers/membership";
import { ADMIN } from "../../variables";
/**
* Return public keys of members of workspace with id [workspaceId]
@@ -30,25 +30,24 @@ export const getWorkspacePublicKeys = async (req: Request, res: Response) => {
publicKeys = (
await Membership.find({
workspace: workspaceId
}).populate<{ user: IUser }>('user', 'publicKey')
)
.map((member) => {
workspace: workspaceId,
}).populate<{ user: IUser }>("user", "publicKey")
).map((member) => {
return {
publicKey: member.user.publicKey,
userId: member.user._id
userId: member.user._id,
};
});
} catch (err) {
Sentry.setUser({ email: req.user.email });
Sentry.captureException(err);
return res.status(400).send({
message: 'Failed to get workspace member public keys'
message: "Failed to get workspace member public keys",
});
}
return res.status(200).send({
publicKeys
publicKeys,
});
};
@@ -64,18 +63,18 @@ export const getWorkspaceMemberships = async (req: Request, res: Response) => {
const { workspaceId } = req.params;
users = await Membership.find({
workspace: workspaceId
}).populate('user', '+publicKey');
workspace: workspaceId,
}).populate("user", "+publicKey");
} catch (err) {
Sentry.setUser({ email: req.user.email });
Sentry.captureException(err);
return res.status(400).send({
message: 'Failed to get workspace members'
message: "Failed to get workspace members",
});
}
return res.status(200).send({
users
users,
});
};
@@ -90,19 +89,19 @@ export const getWorkspaces = async (req: Request, res: Response) => {
try {
workspaces = (
await Membership.find({
user: req.user._id
}).populate('workspace')
user: req.user._id,
}).populate("workspace")
).map((m) => m.workspace);
} catch (err) {
Sentry.setUser({ email: req.user.email });
Sentry.captureException(err);
return res.status(400).send({
message: 'Failed to get workspaces'
message: "Failed to get workspaces",
});
}
return res.status(200).send({
workspaces
workspaces,
});
};
@@ -118,18 +117,18 @@ export const getWorkspace = async (req: Request, res: Response) => {
const { workspaceId } = req.params;
workspace = await Workspace.findOne({
_id: workspaceId
_id: workspaceId,
});
} catch (err) {
Sentry.setUser({ email: req.user.email });
Sentry.captureException(err);
return res.status(400).send({
message: 'Failed to get workspace'
message: "Failed to get workspace",
});
}
return res.status(200).send({
workspace
workspace,
});
};
@@ -148,38 +147,38 @@ export const createWorkspace = async (req: Request, res: Response) => {
// validate organization membership
const membershipOrg = await MembershipOrg.findOne({
user: req.user._id,
organization: organizationId
organization: organizationId,
});
if (!membershipOrg) {
throw new Error('Failed to validate organization membership');
throw new Error("Failed to validate organization membership");
}
if (workspaceName.length < 1) {
throw new Error('Workspace names must be at least 1-character long');
throw new Error("Workspace names must be at least 1-character long");
}
// create workspace and add user as member
workspace = await create({
name: workspaceName,
organizationId
organizationId,
});
await addMemberships({
userIds: [req.user._id],
workspaceId: workspace._id.toString(),
roles: [ADMIN]
roles: [ADMIN],
});
} catch (err) {
Sentry.setUser({ email: req.user.email });
Sentry.captureException(err);
return res.status(400).send({
message: 'Failed to create workspace'
message: "Failed to create workspace",
});
}
return res.status(200).send({
workspace
workspace,
});
};
@@ -195,18 +194,18 @@ export const deleteWorkspace = async (req: Request, res: Response) => {
// delete workspace
await deleteWork({
id: workspaceId
id: workspaceId,
});
} catch (err) {
Sentry.setUser({ email: req.user.email });
Sentry.captureException(err);
return res.status(400).send({
message: 'Failed to delete workspace'
message: "Failed to delete workspace",
});
}
return res.status(200).send({
message: 'Successfully deleted workspace'
message: "Successfully deleted workspace",
});
};
@@ -224,26 +223,26 @@ export const changeWorkspaceName = async (req: Request, res: Response) => {
workspace = await Workspace.findOneAndUpdate(
{
_id: workspaceId
_id: workspaceId,
},
{
name
name,
},
{
new: true
new: true,
}
);
} catch (err) {
Sentry.setUser({ email: req.user.email });
Sentry.captureException(err);
return res.status(400).send({
message: 'Failed to change workspace name'
message: "Failed to change workspace name",
});
}
return res.status(200).send({
message: 'Successfully changed workspace name',
workspace
message: "Successfully changed workspace name",
workspace,
});
};
@@ -259,18 +258,18 @@ export const getWorkspaceIntegrations = async (req: Request, res: Response) => {
const { workspaceId } = req.params;
integrations = await Integration.find({
workspace: workspaceId
workspace: workspaceId,
});
} catch (err) {
Sentry.setUser({ email: req.user.email });
Sentry.captureException(err);
return res.status(400).send({
message: 'Failed to get workspace integrations'
message: "Failed to get workspace integrations",
});
}
return res.status(200).send({
integrations
integrations,
});
};
@@ -289,18 +288,18 @@ export const getWorkspaceIntegrationAuthorizations = async (
const { workspaceId } = req.params;
authorizations = await IntegrationAuth.find({
workspace: workspaceId
workspace: workspaceId,
});
} catch (err) {
Sentry.setUser({ email: req.user.email });
Sentry.captureException(err);
return res.status(400).send({
message: 'Failed to get workspace integration authorizations'
message: "Failed to get workspace integration authorizations",
});
}
return res.status(200).send({
authorizations
authorizations,
});
};
@@ -320,17 +319,17 @@ export const getWorkspaceServiceTokens = async (
// ?? FIX.
serviceTokens = await ServiceToken.find({
user: req.user._id,
workspace: workspaceId
workspace: workspaceId,
});
} catch (err) {
Sentry.setUser({ email: req.user.email });
Sentry.captureException(err);
return res.status(400).send({
message: 'Failed to get workspace service tokens'
message: "Failed to get workspace service tokens",
});
}
return res.status(200).send({
serviceTokens
serviceTokens,
});
}
};
+102 -74
View File
@@ -1,7 +1,7 @@
import axios from 'axios';
import * as Sentry from '@sentry/node';
import { Octokit } from '@octokit/rest';
import { IIntegrationAuth } from '../models';
import axios from "axios";
import * as Sentry from "@sentry/node";
import { Octokit } from "@octokit/rest";
import { IIntegrationAuth } from "../models";
import {
INTEGRATION_AZURE_KEY_VAULT,
INTEGRATION_AWS_PARAMETER_STORE,
@@ -12,12 +12,14 @@ import {
INTEGRATION_GITHUB,
INTEGRATION_RENDER,
INTEGRATION_FLYIO,
INTEGRATION_CIRCLECI,
INTEGRATION_HEROKU_API_URL,
INTEGRATION_VERCEL_API_URL,
INTEGRATION_NETLIFY_API_URL,
INTEGRATION_RENDER_API_URL,
INTEGRATION_FLYIO_API_URL
} from '../variables';
INTEGRATION_FLYIO_API_URL,
INTEGRATION_CIRCLECI_API_URL,
} from "../variables";
/**
* Return list of names of apps for integration named [integration]
@@ -29,7 +31,7 @@ import {
*/
const getApps = async ({
integrationAuth,
accessToken
accessToken,
}: {
integrationAuth: IIntegrationAuth;
accessToken: string;
@@ -54,40 +56,45 @@ const getApps = async ({
break;
case INTEGRATION_HEROKU:
apps = await getAppsHeroku({
accessToken
accessToken,
});
break;
case INTEGRATION_VERCEL:
apps = await getAppsVercel({
integrationAuth,
accessToken
accessToken,
});
break;
case INTEGRATION_NETLIFY:
apps = await getAppsNetlify({
accessToken
accessToken,
});
break;
case INTEGRATION_GITHUB:
apps = await getAppsGithub({
accessToken
accessToken,
});
break;
case INTEGRATION_RENDER:
apps = await getAppsRender({
accessToken
accessToken,
});
break;
case INTEGRATION_FLYIO:
apps = await getAppsFlyio({
accessToken
accessToken,
});
break;
case INTEGRATION_CIRCLECI:
apps = await getAppsCircleci({
accessToken,
});
break;
}
} catch (err) {
Sentry.setUser(null);
Sentry.captureException(err);
throw new Error('Failed to get integration apps');
throw new Error("Failed to get integration apps");
}
return apps;
@@ -106,19 +113,19 @@ const getAppsHeroku = async ({ accessToken }: { accessToken: string }) => {
const res = (
await axios.get(`${INTEGRATION_HEROKU_API_URL}/apps`, {
headers: {
Accept: 'application/vnd.heroku+json; version=3',
Authorization: `Bearer ${accessToken}`
}
Accept: "application/vnd.heroku+json; version=3",
Authorization: `Bearer ${accessToken}`,
},
})
).data;
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');
throw new Error("Failed to get Heroku integration apps");
}
return apps;
@@ -133,7 +140,7 @@ const getAppsHeroku = async ({ accessToken }: { accessToken: string }) => {
*/
const getAppsVercel = async ({
integrationAuth,
accessToken
accessToken,
}: {
integrationAuth: IIntegrationAuth;
accessToken: string;
@@ -146,21 +153,23 @@ const getAppsVercel = async ({
Authorization: `Bearer ${accessToken}`,
'Accept-Encoding': 'application/json'
},
...( integrationAuth?.teamId ? {
...(integrationAuth?.teamId
? {
params: {
teamId: integrationAuth.teamId
teamId: integrationAuth.teamId,
},
}
} : {})
: {}),
})
).data;
apps = res.projects.map((a: any) => ({
name: a.name
name: a.name,
}));
} catch (err) {
Sentry.setUser(null);
Sentry.captureException(err);
throw new Error('Failed to get Vercel integration apps');
throw new Error("Failed to get Vercel integration apps");
}
return apps;
@@ -173,11 +182,7 @@ const getAppsVercel = async ({
* @returns {Object[]} apps - names of Netlify sites
* @returns {String} apps.name - name of Netlify site
*/
const getAppsNetlify = async ({
accessToken
}: {
accessToken: string;
}) => {
const getAppsNetlify = async ({ accessToken }: { accessToken: string }) => {
let apps;
try {
const res = (
@@ -191,12 +196,12 @@ const getAppsNetlify = async ({
apps = res.map((a: any) => ({
name: a.name,
appId: a.site_id
appId: a.site_id,
}));
} catch (err) {
Sentry.setUser(null);
Sentry.captureException(err);
throw new Error('Failed to get Netlify integration apps');
throw new Error("Failed to get Netlify integration apps");
}
return apps;
@@ -209,35 +214,32 @@ const getAppsNetlify = async ({
* @returns {Object[]} apps - names of Netlify sites
* @returns {String} apps.name - name of Netlify site
*/
const getAppsGithub = async ({
accessToken
}: {
accessToken: string;
}) => {
const getAppsGithub = async ({ accessToken }: { accessToken: string }) => {
let apps;
try {
const octokit = new Octokit({
auth: accessToken
auth: accessToken,
});
const repos = (await octokit.request(
'GET /user/repos{?visibility,affiliation,type,sort,direction,per_page,page,since,before}',
const repos = (
await octokit.request(
"GET /user/repos{?visibility,affiliation,type,sort,direction,per_page,page,since,before}",
{
per_page: 100
per_page: 100,
}
)).data;
)
).data;
apps = repos
.filter((a:any) => a.permissions.admin === true)
.filter((a: any) => a.permissions.admin === true)
.map((a: any) => ({
name: a.name,
owner: a.owner.login
})
);
owner: a.owner.login,
}));
} catch (err) {
Sentry.setUser(null);
Sentry.captureException(err);
throw new Error('Failed to get Github repos');
throw new Error("Failed to get Github repos");
}
return apps;
@@ -251,11 +253,7 @@ const getAppsGithub = async ({
* @returns {String} apps.name - name 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;
try {
const res = (
@@ -263,8 +261,8 @@ const getAppsRender = async ({
headers: {
Authorization: `Bearer ${accessToken}`,
Accept: 'application/json',
'Accept-Encoding': 'application/json'
}
'Accept-Encoding': 'application/json',
},
})
).data;
@@ -277,11 +275,11 @@ const getAppsRender = async ({
} catch (err) {
Sentry.setUser(null);
Sentry.captureException(err);
throw new Error('Failed to get Render services');
throw new Error("Failed to get Render services");
}
return apps;
}
};
/**
* Return list of apps for Fly.io integration
@@ -290,11 +288,7 @@ const getAppsRender = async ({
* @returns {Object[]} apps - names and ids 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;
try {
const query = `
@@ -309,33 +303,67 @@ const getAppsFlyio = async ({
}
`;
const res = (await axios({
const res = (
await axios({
url: INTEGRATION_FLYIO_API_URL,
method: 'post',
method: "post",
headers: {
'Authorization': 'Bearer ' + accessToken,
Authorization: "Bearer " + accessToken,
'Accept': 'application/json',
'Accept-Encoding': 'application/json'
'Accept-Encoding': 'application/json',
},
data: {
query,
variables: {
role: null
}
}
})).data.data.apps.nodes;
role: null,
},
},
})
).data.data.apps.nodes;
apps = res
.map((a: any) => ({
name: a.name
apps = res.map((a: any) => ({
name: a.name,
}));
} catch (err) {
Sentry.setUser(null);
Sentry.captureException(err);
throw new Error('Failed to get Fly.io apps');
throw new Error("Failed to get Fly.io apps");
}
return apps;
}
};
const getAppsCircleci = async ({ accessToken }: { accessToken: string }) => {
// in place of accessToken we have to send Circle-Token i.e. Personal API token from CircleCi
let apps: any;
try {
let res = (
await axios.get(
`${INTEGRATION_CIRCLECI_API_URL}/v1.1/projects`,
{
headers: {
"Circle-Token": accessToken,
"Accept-Encoding": "application/json",
},
}
)
).data
apps = res?.map((a: any) => {
return {
name: a?.reponame
}
})
} catch (err) {
console.log(err);
Sentry.setUser(null);
Sentry.captureException(err);
throw new Error("Failed to get Render services");
}
console.log("hello apps");
return apps;
};
export { getApps };
+240 -140
View File
@@ -1,5 +1,5 @@
import axios from 'axios';
import * as Sentry from '@sentry/node';
import axios from "axios";
import * as Sentry from "@sentry/node";
import _ from 'lodash';
import AWS from 'aws-sdk';
import {
@@ -9,9 +9,9 @@ import {
GetSecretValueCommand,
ResourceNotFoundException
} from '@aws-sdk/client-secrets-manager';
import { Octokit } from '@octokit/rest';
import sodium from 'libsodium-wrappers';
import { IIntegration, IIntegrationAuth } from '../models';
import { Octokit } from "@octokit/rest";
import sodium from "libsodium-wrappers";
import { IIntegration, IIntegrationAuth } from "../models";
import {
INTEGRATION_AZURE_KEY_VAULT,
INTEGRATION_AWS_PARAMETER_STORE,
@@ -22,12 +22,15 @@ import {
INTEGRATION_GITHUB,
INTEGRATION_RENDER,
INTEGRATION_FLYIO,
INTEGRATION_CIRCLECI,
INTEGRATION_HEROKU_API_URL,
INTEGRATION_VERCEL_API_URL,
INTEGRATION_NETLIFY_API_URL,
INTEGRATION_RENDER_API_URL,
INTEGRATION_FLYIO_API_URL
} from '../variables';
INTEGRATION_FLYIO_API_URL,
INTEGRATION_CIRCLECI_API_URL,
} from "../variables";
import { access, appendFile } from "fs";
/**
* Sync/push [secrets] to [app] in integration named [integration]
@@ -43,7 +46,7 @@ const syncSecrets = async ({
integrationAuth,
secrets,
accessId,
accessToken
accessToken,
}: {
integration: IIntegration;
integrationAuth: IIntegrationAuth;
@@ -80,7 +83,7 @@ const syncSecrets = async ({
await syncSecretsHeroku({
integration,
secrets,
accessToken
accessToken,
});
break;
case INTEGRATION_VERCEL:
@@ -88,7 +91,7 @@ const syncSecrets = async ({
integration,
integrationAuth,
secrets,
accessToken
accessToken,
});
break;
case INTEGRATION_NETLIFY:
@@ -96,30 +99,36 @@ const syncSecrets = async ({
integration,
integrationAuth,
secrets,
accessToken
accessToken,
});
break;
case INTEGRATION_GITHUB:
await syncSecretsGitHub({
integration,
secrets,
accessToken
accessToken,
});
break;
case INTEGRATION_RENDER:
await syncSecretsRender({
integration,
secrets,
accessToken
accessToken,
});
break;
case INTEGRATION_FLYIO:
await syncSecretsFlyio({
integration,
secrets,
accessToken
accessToken,
});
break;
case INTEGRATION_CIRCLECI:
await syncSecretsCircleci({
integration,
secrets,
accessToken,
});
}
} catch (err) {
Sentry.setUser(null);
@@ -465,7 +474,7 @@ const syncSecretsAWSSecretManager = async ({
const syncSecretsHeroku = async ({
integration,
secrets,
accessToken
accessToken,
}: {
integration: IIntegration;
secrets: any;
@@ -477,9 +486,9 @@ const syncSecretsHeroku = async ({
`${INTEGRATION_HEROKU_API_URL}/apps/${integration.app}/config-vars`,
{
headers: {
Accept: 'application/vnd.heroku+json; version=3',
Authorization: `Bearer ${accessToken}`
}
Accept: "application/vnd.heroku+json; version=3",
Authorization: `Bearer ${accessToken}`,
},
}
)
).data;
@@ -495,15 +504,15 @@ const syncSecretsHeroku = async ({
secrets,
{
headers: {
Accept: 'application/vnd.heroku+json; version=3',
Authorization: `Bearer ${accessToken}`
}
Accept: "application/vnd.heroku+json; version=3",
Authorization: `Bearer ${accessToken}`,
},
}
);
} catch (err) {
Sentry.setUser(null);
Sentry.captureException(err);
throw new Error('Failed to sync secrets to Heroku');
throw new Error("Failed to sync secrets to Heroku");
}
};
@@ -517,10 +526,10 @@ const syncSecretsVercel = async ({
integration,
integrationAuth,
secrets,
accessToken
accessToken,
}: {
integration: IIntegration,
integrationAuth: IIntegrationAuth,
integration: IIntegration;
integrationAuth: IIntegrationAuth;
secrets: any;
accessToken: string;
}) => {
@@ -536,13 +545,18 @@ const syncSecretsVercel = async ({
// Get all (decrypted) secrets back from Vercel in
// decrypted format
const params: { [key: string]: string } = {
decrypt: 'true',
...( integrationAuth?.teamId ? {
teamId: integrationAuth.teamId
} : {})
decrypt: "true",
...(integrationAuth?.teamId
? {
teamId: integrationAuth.teamId,
}
: {}),
};
const res = (await Promise.all((await axios.get(
const res = (
await Promise.all(
(
await axios.get(
`${INTEGRATION_VERCEL_API_URL}/v9/projects/${integration.app}/env`,
{
params,
@@ -579,8 +593,8 @@ const syncSecretsVercel = async ({
newSecrets.push({
key: key,
value: secrets[key],
type: 'encrypted',
target: [integration.targetEnvironment]
type: "encrypted",
target: [integration.targetEnvironment],
});
}
});
@@ -594,8 +608,8 @@ const syncSecretsVercel = async ({
id: res[key].id,
key: key,
value: secrets[key],
type: 'encrypted',
target: [integration.targetEnvironment]
type: "encrypted",
target: [integration.targetEnvironment],
});
}
} else {
@@ -604,7 +618,7 @@ const syncSecretsVercel = async ({
id: res[key].id,
key: key,
value: res[key].value,
type: 'encrypted',
type: "encrypted",
target: [integration.targetEnvironment],
});
}
@@ -618,8 +632,8 @@ const syncSecretsVercel = async ({
{
params,
headers: {
Authorization: `Bearer ${accessToken}`
}
Authorization: `Bearer ${accessToken}`,
},
}
);
}
@@ -627,18 +641,15 @@ const syncSecretsVercel = async ({
// Sync/push updated secrets
if (updateSecrets.length > 0) {
updateSecrets.forEach(async (secret: VercelSecret) => {
const {
id,
...updatedSecret
} = secret;
const { id, ...updatedSecret } = secret;
await axios.patch(
`${INTEGRATION_VERCEL_API_URL}/v9/projects/${integration.app}/env/${secret.id}`,
updatedSecret,
{
params,
headers: {
Authorization: `Bearer ${accessToken}`
}
Authorization: `Bearer ${accessToken}`,
},
}
);
});
@@ -652,8 +663,8 @@ const syncSecretsVercel = async ({
{
params,
headers: {
Authorization: `Bearer ${accessToken}`
}
Authorization: `Bearer ${accessToken}`,
},
}
);
});
@@ -661,9 +672,9 @@ const syncSecretsVercel = async ({
} catch (err) {
Sentry.setUser(null);
Sentry.captureException(err);
throw new Error('Failed to sync secrets to Vercel');
throw new Error("Failed to sync secrets to Vercel");
}
}
};
/**
* Sync/push [secrets] to Netlify site with id [integration.appId]
@@ -677,7 +688,7 @@ const syncSecretsNetlify = async ({
integration,
integrationAuth,
secrets,
accessToken
accessToken,
}: {
integration: IIntegration;
integrationAuth: IIntegrationAuth;
@@ -685,7 +696,6 @@ const syncSecretsNetlify = async ({
accessToken: string;
}) => {
try {
interface NetlifyValue {
id?: string;
context: string; // 'dev' | 'branch-deploy' | 'deploy-preview' | 'production',
@@ -702,24 +712,27 @@ const syncSecretsNetlify = async ({
}
const getParams = new URLSearchParams({
context_name: 'all', // integration.context or all
site_id: integration.appId
context_name: "all", // integration.context or all
site_id: integration.appId,
});
const res = (await axios.get(
const res = (
await axios.get(
`${INTEGRATION_NETLIFY_API_URL}/api/v1/accounts/${integrationAuth.accountId}/env`,
{
params: getParams,
headers: {
Authorization: `Bearer ${accessToken}`
Authorization: `Bearer ${accessToken}`,
},
}
}
))
.data
.reduce((obj: any, secret: any) => ({
)
).data.reduce(
(obj: any, secret: any) => ({
...obj,
[secret.key]: secret
}), {});
[secret.key]: secret,
}),
{}
);
const newSecrets: NetlifySecret[] = []; // createEnvVars
const deleteSecrets: string[] = []; // deleteEnvVar
@@ -732,18 +745,22 @@ const syncSecretsNetlify = async ({
// case: Infisical secret does not exist in Netlify -> create secret
newSecrets.push({
key,
values: [{
values: [
{
value: secrets[key],
context: integration.targetEnvironment
}]
context: integration.targetEnvironment,
},
],
});
} else {
// case: Infisical secret exists in Netlify
const contexts = res[key].values
.reduce((obj: any, value: NetlifyValue) => ({
const contexts = res[key].values.reduce(
(obj: any, value: NetlifyValue) => ({
...obj,
[value.context]: value
}), {});
[value.context]: value,
}),
{}
);
if (integration.targetEnvironment in contexts) {
// case: Netlify secret value exists in integration context
@@ -752,10 +769,12 @@ const syncSecretsNetlify = async ({
// -> update Netlify secret context and value
updateSecrets.push({
key,
values: [{
values: [
{
context: integration.targetEnvironment,
value: secrets[key]
}]
value: secrets[key],
},
],
});
}
} else {
@@ -763,14 +782,16 @@ const syncSecretsNetlify = async ({
// -> add the new Netlify secret context and value
updateSecrets.push({
key,
values: [{
values: [
{
context: integration.targetEnvironment,
value: secrets[key]
}]
value: secrets[key],
},
],
});
}
}
})
});
// identify secrets to delete
// TODO: revise (patch case where 1 context was deleted but others still there
@@ -790,11 +811,13 @@ const syncSecretsNetlify = async ({
// case: Netlify secret value has more than 1 context -> delete secret value context
deleteSecretValues.push({
key,
values: [{
values: [
{
id: value.id,
context: integration.targetEnvironment,
value: value.value
}]
value: value.value,
},
],
});
}
}
@@ -803,7 +826,7 @@ const syncSecretsNetlify = async ({
});
const syncParams = new URLSearchParams({
site_id: integration.appId
site_id: integration.appId,
});
if (newSecrets.length > 0) {
@@ -813,8 +836,8 @@ const syncSecretsNetlify = async ({
{
params: syncParams,
headers: {
Authorization: `Bearer ${accessToken}`
}
Authorization: `Bearer ${accessToken}`,
},
}
);
}
@@ -825,13 +848,13 @@ const syncSecretsNetlify = async ({
`${INTEGRATION_NETLIFY_API_URL}/api/v1/accounts/${integrationAuth.accountId}/env/${secret.key}`,
{
context: secret.values[0].context,
value: secret.values[0].value
value: secret.values[0].value,
},
{
params: syncParams,
headers: {
Authorization: `Bearer ${accessToken}`
}
Authorization: `Bearer ${accessToken}`,
},
}
);
});
@@ -844,8 +867,8 @@ const syncSecretsNetlify = async ({
{
params: syncParams,
headers: {
Authorization: `Bearer ${accessToken}`
}
Authorization: `Bearer ${accessToken}`,
},
}
);
});
@@ -858,8 +881,8 @@ const syncSecretsNetlify = async ({
{
params: syncParams,
headers: {
Authorization: `Bearer ${accessToken}`
}
Authorization: `Bearer ${accessToken}`,
},
}
);
});
@@ -867,9 +890,9 @@ const syncSecretsNetlify = async ({
} catch (err) {
Sentry.setUser(null);
Sentry.captureException(err);
throw new Error('Failed to sync secrets to Heroku');
throw new Error("Failed to sync secrets to Heroku");
}
}
};
/**
* Sync/push [secrets] to GitHub repo with name [integration.app]
@@ -882,14 +905,13 @@ const syncSecretsNetlify = async ({
const syncSecretsGitHub = async ({
integration,
secrets,
accessToken
accessToken,
}: {
integration: IIntegration;
secrets: any;
accessToken: string;
}) => {
try {
interface GitHubRepoKey {
key_id: string;
key: string;
@@ -908,41 +930,42 @@ const syncSecretsGitHub = async ({
const deleteSecrets: GitHubSecret[] = [];
const octokit = new Octokit({
auth: accessToken
auth: accessToken,
});
// const user = (await octokit.request('GET /user', {})).data;
const repoPublicKey: GitHubRepoKey = (await octokit.request(
'GET /repos/{owner}/{repo}/actions/secrets/public-key',
const repoPublicKey: GitHubRepoKey = (
await octokit.request(
"GET /repos/{owner}/{repo}/actions/secrets/public-key",
{
owner: integration.owner,
repo: integration.app
repo: integration.app,
}
)).data;
)
).data;
// Get local copy of decrypted secrets. We cannot decrypt them as we dont have access to GH private key
const encryptedSecrets: GitHubSecretRes = (await octokit.request(
'GET /repos/{owner}/{repo}/actions/secrets',
{
const encryptedSecrets: GitHubSecretRes = (
await octokit.request("GET /repos/{owner}/{repo}/actions/secrets", {
owner: integration.owner,
repo: integration.app
}
))
.data
.secrets
.reduce((obj: any, secret: any) => ({
repo: integration.app,
})
).data.secrets.reduce(
(obj: any, secret: any) => ({
...obj,
[secret.name]: secret
}), {});
[secret.name]: secret,
}),
{}
);
Object.keys(encryptedSecrets).map(async (key) => {
if (!(key in secrets)) {
await octokit.request(
'DELETE /repos/{owner}/{repo}/actions/secrets/{secret_name}',
"DELETE /repos/{owner}/{repo}/actions/secrets/{secret_name}",
{
owner: integration.owner,
repo: integration.app,
secret_name: key
secret_name: key,
}
);
}
@@ -968,13 +991,13 @@ const syncSecretsGitHub = async ({
);
await octokit.request(
'PUT /repos/{owner}/{repo}/actions/secrets/{secret_name}',
"PUT /repos/{owner}/{repo}/actions/secrets/{secret_name}",
{
owner: integration.owner,
repo: integration.app,
secret_name: key,
encrypted_value: encryptedSecret,
key_id: repoPublicKey.key_id
key_id: repoPublicKey.key_id,
}
);
});
@@ -982,7 +1005,7 @@ const syncSecretsGitHub = async ({
} catch (err) {
Sentry.setUser(null);
Sentry.captureException(err);
throw new Error('Failed to sync secrets to GitHub');
throw new Error("Failed to sync secrets to GitHub");
}
};
@@ -996,7 +1019,7 @@ const syncSecretsGitHub = async ({
const syncSecretsRender = async ({
integration,
secrets,
accessToken
accessToken,
}: {
integration: IIntegration;
secrets: any;
@@ -1007,20 +1030,20 @@ const syncSecretsRender = async ({
`${INTEGRATION_RENDER_API_URL}/v1/services/${integration.appId}/env-vars`,
Object.keys(secrets).map((key) => ({
key,
value: secrets[key]
value: secrets[key],
})),
{
headers: {
Authorization: `Bearer ${accessToken}`
}
Authorization: `Bearer ${accessToken}`,
},
}
);
} catch (err) {
Sentry.setUser(null);
Sentry.captureException(err);
throw new Error('Failed to sync secrets to Render');
throw new Error("Failed to sync secrets to Render");
}
}
};
/**
* Sync/push [secrets] to Fly.io app
@@ -1032,7 +1055,7 @@ const syncSecretsRender = async ({
const syncSecretsFlyio = async ({
integration,
secrets,
accessToken
accessToken,
}: {
integration: IIntegration;
secrets: any;
@@ -1062,19 +1085,22 @@ const syncSecretsFlyio = async ({
await axios({
url: INTEGRATION_FLYIO_API_URL,
method: 'post',
method: "post",
headers: {
'Authorization': 'Bearer ' + accessToken
Authorization: "Bearer " + accessToken,
},
data: {
query: SetSecrets,
variables: {
input: {
appId: integration.app,
secrets: Object.entries(secrets).map(([key, value]) => ({ key, value }))
}
}
}
secrets: Object.entries(secrets).map(([key, value]) => ({
key,
value,
})),
},
},
},
});
// get secrets
@@ -1094,8 +1120,9 @@ const syncSecretsFlyio = async ({
}
}`;
const getSecretsRes = (await axios({
method: 'post',
const getSecretsRes = (
await axios({
method: "post",
url: INTEGRATION_FLYIO_API_URL,
headers: {
'Authorization': 'Bearer ' + accessToken,
@@ -1105,10 +1132,11 @@ const syncSecretsFlyio = async ({
data: {
query: GetSecrets,
variables: {
appName: integration.app
}
}
})).data.data.app.secrets;
appName: integration.app,
},
},
})
).data.data.app.secrets;
const deleteSecretsKeys = getSecretsRes
.filter((secret: FlyioSecret) => !(secret.name in secrets))
@@ -1134,28 +1162,100 @@ const syncSecretsFlyio = async ({
}`;
await axios({
method: 'post',
method: "post",
url: INTEGRATION_FLYIO_API_URL,
headers: {
'Authorization': 'Bearer ' + accessToken,
'Content-Type': 'application/json'
Authorization: "Bearer " + accessToken,
"Content-Type": "application/json",
},
data: {
query: DeleteSecrets,
variables: {
input: {
appId: integration.app,
keys: deleteSecretsKeys
}
}
}
keys: deleteSecretsKeys,
},
},
},
});
} catch (err) {
Sentry.setUser(null);
Sentry.captureException(err);
throw new Error('Failed to sync secrets to Fly.io');
throw new Error("Failed to sync secrets to Fly.io");
}
}
};
const syncSecretsCircleci = async ({
integration,
secrets,
accessToken,
}: {
integration: IIntegration;
secrets: any;
accessToken: string;
}) => {
try {
const circleciOrganizationDetail = (
await axios.get(`${INTEGRATION_CIRCLECI_API_URL}/v2/me/collaborations`, {
headers: {
"Circle-Token": accessToken,
"Accept-Encoding": "application/json",
},
})
).data[0];
const { slug } = circleciOrganizationDetail;
// inject secrets to CircleCI (one by one)
Object.keys(secrets).forEach(
async (key) =>
await axios.post(
`${INTEGRATION_CIRCLECI_API_URL}/v2/project/${slug}/${integration.app}/envvar`,
{
name: key,
value: secrets[key],
},
{
headers: {
"Circle-Token": accessToken,
"Content-Type": "application/json",
},
}
)
);
// get secrets from CircleCI
const getSecretsRes = (
await axios.get(
`${INTEGRATION_CIRCLECI_API_URL}/v2/project/${slug}/${integration.app}/envvar`,
{
headers: {
"Circle-Token": accessToken,
"Accept-Encoding": "application/json",
},
}
)
).data?.items;
// delete secrets from CircleCI
getSecretsRes.forEach(async (sec: any) => {
if (!(sec.name in secrets)) {
await axios.delete(
`${INTEGRATION_CIRCLECI_API_URL}/v2/project/${slug}/${integration.app}/envvar/${sec.name}`,
{
headers: {
"Circle-Token": accessToken,
"Content-Type": "application/json",
},
}
);
}
});
} catch (err) {
Sentry.setUser(null);
Sentry.captureException(err);
throw new Error("Failed to sync secrets to CircleCI");
}
};
export { syncSecrets };
+26 -21
View File
@@ -1,4 +1,4 @@
import { Schema, model, Types } from 'mongoose';
import { Schema, model, Types } from "mongoose";
import {
INTEGRATION_AZURE_KEY_VAULT,
INTEGRATION_AWS_PARAMETER_STORE,
@@ -8,8 +8,9 @@ import {
INTEGRATION_NETLIFY,
INTEGRATION_GITHUB,
INTEGRATION_RENDER,
INTEGRATION_FLYIO
} from '../variables';
INTEGRATION_FLYIO,
INTEGRATION_CIRCLECI,
} from "../variables";
export interface IIntegration {
_id: Types.ObjectId;
@@ -31,7 +32,8 @@ export interface IIntegration {
| 'netlify'
| 'github'
| 'render'
| 'flyio';
| 'flyio'
| 'circleci';
integrationAuth: Types.ObjectId;
}
@@ -39,36 +41,38 @@ const integrationSchema = new Schema<IIntegration>(
{
workspace: {
type: Schema.Types.ObjectId,
ref: 'Workspace',
required: true
ref: "Workspace",
required: true,
},
environment: {
type: String,
required: true
required: true,
},
isActive: {
type: Boolean,
required: true
required: true,
},
app: {
// name of app in provider
type: String,
default: null
default: null,
},
appId: { // (new)
appId: {
// (new)
// id of app in provider
type: String,
default: null
default: null,
},
targetEnvironment: { // (new)
targetEnvironment: {
// (new)
// target environment
type: String,
default: null
default: null,
},
owner: {
// github-specific repo owner-login
type: String,
default: null
default: null,
},
path: {
// aws-parameter-store-specific path
@@ -91,21 +95,22 @@ const integrationSchema = new Schema<IIntegration>(
INTEGRATION_NETLIFY,
INTEGRATION_GITHUB,
INTEGRATION_RENDER,
INTEGRATION_FLYIO
INTEGRATION_FLYIO,
INTEGRATION_CIRCLECI,
],
required: true
required: true,
},
integrationAuth: {
type: Schema.Types.ObjectId,
ref: 'IntegrationAuth',
required: true
}
ref: "IntegrationAuth",
required: true,
},
},
{
timestamps: true
timestamps: true,
}
);
const Integration = model<IIntegration>('Integration', integrationSchema);
const Integration = model<IIntegration>("Integration", integrationSchema);
export default Integration;
+24 -31
View File
@@ -1,4 +1,4 @@
import { Schema, model, Types } from 'mongoose';
import { Schema, model, Types } from "mongoose";
import {
INTEGRATION_AZURE_KEY_VAULT,
INTEGRATION_AWS_PARAMETER_STORE,
@@ -8,24 +8,16 @@ import {
INTEGRATION_NETLIFY,
INTEGRATION_GITHUB,
INTEGRATION_RENDER,
INTEGRATION_FLYIO
} from '../variables';
INTEGRATION_FLYIO,
INTEGRATION_CIRCLECI,
} from "../variables";
export interface IIntegrationAuth {
_id: Types.ObjectId;
workspace: Types.ObjectId;
integration:
| 'azure-key-vault'
| 'aws-parameter-store'
| 'aws-secret-manager'
| 'heroku'
| 'vercel'
| 'netlify'
| 'github'
| 'render'
| 'flyio';
teamId: string; // TODO: deprecate (vercel) -> move to accessId
accountId: string; // TODO: deprecate (netlify) -> move to accessId
integration: 'heroku' | 'vercel' | 'netlify' | 'github' | 'render' | 'flyio' | 'azure-key-vault' | 'circleci' | 'aws-parameter-store' | 'aws-secret-manager';
teamId: string;
accountId: string;
refreshCiphertext?: string;
refreshIV?: string;
refreshTag?: string;
@@ -42,8 +34,8 @@ const integrationAuthSchema = new Schema<IIntegrationAuth>(
{
workspace: {
type: Schema.Types.ObjectId,
ref: 'Workspace',
required: true
ref: "Workspace",
required: true,
},
integration: {
type: String,
@@ -56,29 +48,30 @@ const integrationAuthSchema = new Schema<IIntegrationAuth>(
INTEGRATION_NETLIFY,
INTEGRATION_GITHUB,
INTEGRATION_RENDER,
INTEGRATION_FLYIO
INTEGRATION_FLYIO,
INTEGRATION_CIRCLECI,
],
required: true
required: true,
},
teamId: {
// vercel-specific integration param
type: String
type: String,
},
accountId: {
// netlify-specific integration param
type: String
type: String,
},
refreshCiphertext: {
type: String,
select: false
select: false,
},
refreshIV: {
type: String,
select: false
select: false,
},
refreshTag: {
type: String,
select: false
select: false,
},
accessIdCiphertext: {
type: String,
@@ -94,28 +87,28 @@ const integrationAuthSchema = new Schema<IIntegrationAuth>(
},
accessCiphertext: {
type: String,
select: false
select: false,
},
accessIV: {
type: String,
select: false
select: false,
},
accessTag: {
type: String,
select: false
select: false,
},
accessExpiresAt: {
type: Date,
select: false
}
select: false,
},
},
{
timestamps: true
timestamps: true,
}
);
const IntegrationAuth = model<IIntegrationAuth>(
'IntegrationAuth',
"IntegrationAuth",
integrationAuthSchema
);
+15 -17
View File
@@ -3,8 +3,8 @@ import {
ENV_TESTING,
ENV_STAGING,
ENV_PROD,
ENV_SET
} from './environment';
ENV_SET,
} from "./environment";
import {
INTEGRATION_AZURE_KEY_VAULT,
INTEGRATION_AWS_PARAMETER_STORE,
@@ -15,6 +15,7 @@ import {
INTEGRATION_GITHUB,
INTEGRATION_RENDER,
INTEGRATION_FLYIO,
INTEGRATION_CIRCLECI,
INTEGRATION_SET,
INTEGRATION_OAUTH2,
INTEGRATION_AZURE_TOKEN_URL,
@@ -27,27 +28,22 @@ import {
INTEGRATION_NETLIFY_API_URL,
INTEGRATION_RENDER_API_URL,
INTEGRATION_FLYIO_API_URL,
INTEGRATION_OPTIONS
} from './integration';
import {
OWNER,
ADMIN,
MEMBER,
INVITED,
ACCEPTED,
} from './organization';
import { SECRET_SHARED, SECRET_PERSONAL } from './secret';
import { EVENT_PUSH_SECRETS, EVENT_PULL_SECRETS } from './event';
INTEGRATION_CIRCLECI_API_URL,
INTEGRATION_OPTIONS,
} from "./integration";
import { OWNER, ADMIN, MEMBER, INVITED, ACCEPTED } from "./organization";
import { SECRET_SHARED, SECRET_PERSONAL } from "./secret";
import { EVENT_PUSH_SECRETS, EVENT_PULL_SECRETS } from "./event";
import {
ACTION_LOGIN,
ACTION_LOGOUT,
ACTION_ADD_SECRETS,
ACTION_UPDATE_SECRETS,
ACTION_DELETE_SECRETS,
ACTION_READ_SECRETS
} from './action';
import { SMTP_HOST_SENDGRID, SMTP_HOST_MAILGUN } from './smtp';
import { PLAN_STARTER, PLAN_PRO } from './stripe';
ACTION_READ_SECRETS,
} from "./action";
import { SMTP_HOST_SENDGRID, SMTP_HOST_MAILGUN } from "./smtp";
import { PLAN_STARTER, PLAN_PRO } from "./stripe";
export {
OWNER,
@@ -71,6 +67,7 @@ export {
INTEGRATION_GITHUB,
INTEGRATION_RENDER,
INTEGRATION_FLYIO,
INTEGRATION_CIRCLECI,
INTEGRATION_SET,
INTEGRATION_OAUTH2,
INTEGRATION_AZURE_TOKEN_URL,
@@ -83,6 +80,7 @@ export {
INTEGRATION_NETLIFY_API_URL,
INTEGRATION_RENDER_API_URL,
INTEGRATION_FLYIO_API_URL,
INTEGRATION_CIRCLECI_API_URL,
EVENT_PUSH_SECRETS,
EVENT_PULL_SECRETS,
ACTION_LOGIN,
+26 -21
View File
@@ -6,19 +6,20 @@ import {
CLIENT_ID_HEROKU,
CLIENT_ID_NETLIFY,
CLIENT_ID_GITHUB,
CLIENT_SLUG_VERCEL
} from '../config';
CLIENT_SLUG_VERCEL,
} from "../config";
// integrations
const INTEGRATION_AZURE_KEY_VAULT = 'azure-key-vault';
const INTEGRATION_AWS_PARAMETER_STORE = 'aws-parameter-store';
const INTEGRATION_AWS_SECRET_MANAGER = 'aws-secret-manager';
const INTEGRATION_HEROKU = 'heroku';
const INTEGRATION_VERCEL = 'vercel';
const INTEGRATION_NETLIFY = 'netlify';
const INTEGRATION_GITHUB = 'github';
const INTEGRATION_RENDER = 'render';
const INTEGRATION_FLYIO = 'flyio';
const INTEGRATION_HEROKU = "heroku";
const INTEGRATION_VERCEL = "vercel";
const INTEGRATION_NETLIFY = "netlify";
const INTEGRATION_GITHUB = "github";
const INTEGRATION_RENDER = "render";
const INTEGRATION_FLYIO = "flyio";
const INTEGRATION_CIRCLECI = "circleci";
const INTEGRATION_SET = new Set([
INTEGRATION_AZURE_KEY_VAULT,
INTEGRATION_HEROKU,
@@ -26,27 +27,29 @@ const INTEGRATION_SET = new Set([
INTEGRATION_NETLIFY,
INTEGRATION_GITHUB,
INTEGRATION_RENDER,
INTEGRATION_FLYIO
INTEGRATION_FLYIO,
INTEGRATION_CIRCLECI,
]);
// integration types
const INTEGRATION_OAUTH2 = 'oauth2';
const INTEGRATION_OAUTH2 = "oauth2";
// integration oauth endpoints
const INTEGRATION_AZURE_TOKEN_URL = `https://login.microsoftonline.com/${TENANT_ID_AZURE}/oauth2/v2.0/token`;
const INTEGRATION_HEROKU_TOKEN_URL = 'https://id.heroku.com/oauth/token';
const INTEGRATION_VERCEL_TOKEN_URL =
'https://api.vercel.com/v2/oauth/access_token';
const INTEGRATION_NETLIFY_TOKEN_URL = 'https://api.netlify.com/oauth/token';
"https://api.vercel.com/v2/oauth/access_token";
const INTEGRATION_NETLIFY_TOKEN_URL = "https://api.netlify.com/oauth/token";
const INTEGRATION_GITHUB_TOKEN_URL =
'https://github.com/login/oauth/access_token';
"https://github.com/login/oauth/access_token";
// integration apps endpoints
const INTEGRATION_HEROKU_API_URL = 'https://api.heroku.com';
const INTEGRATION_VERCEL_API_URL = 'https://api.vercel.com';
const INTEGRATION_NETLIFY_API_URL = 'https://api.netlify.com';
const INTEGRATION_RENDER_API_URL = 'https://api.render.com';
const INTEGRATION_FLYIO_API_URL = 'https://api.fly.io/graphql';
const INTEGRATION_HEROKU_API_URL = "https://api.heroku.com";
const INTEGRATION_VERCEL_API_URL = "https://api.vercel.com";
const INTEGRATION_NETLIFY_API_URL = "https://api.netlify.com";
const INTEGRATION_RENDER_API_URL = "https://api.render.com";
const INTEGRATION_FLYIO_API_URL = "https://api.fly.io/graphql";
const INTEGRATION_CIRCLECI_API_URL = "https://circleci.com/api";
const INTEGRATION_OPTIONS = [
{
@@ -154,8 +157,8 @@ const INTEGRATION_OPTIONS = [
name: 'Circle CI',
slug: 'circleci',
image: 'Circle CI.png',
isAvailable: false,
type: '',
isAvailable: true,
type: 'pat',
clientId: '',
docsLink: ''
}
@@ -171,6 +174,7 @@ export {
INTEGRATION_GITHUB,
INTEGRATION_RENDER,
INTEGRATION_FLYIO,
INTEGRATION_CIRCLECI,
INTEGRATION_SET,
INTEGRATION_OAUTH2,
INTEGRATION_AZURE_TOKEN_URL,
@@ -183,5 +187,6 @@ export {
INTEGRATION_NETLIFY_API_URL,
INTEGRATION_RENDER_API_URL,
INTEGRATION_FLYIO_API_URL,
INTEGRATION_OPTIONS
INTEGRATION_CIRCLECI_API_URL,
INTEGRATION_OPTIONS,
};
+8 -16
View File
@@ -1,24 +1,16 @@
// membership roles
const OWNER = 'owner';
const ADMIN = 'admin';
const MEMBER = 'member';
const OWNER = "owner";
const ADMIN = "admin";
const MEMBER = "member";
// membership statuses
const INVITED = 'invited';
const INVITED = "invited";
// membership permissions ability
const ABILITY_READ = 'read';
const ABILITY_WRITE = 'write';
const ABILITY_READ = "read";
const ABILITY_WRITE = "write";
// -- organization
const ACCEPTED = 'accepted';
const ACCEPTED = "accepted";
export {
OWNER,
ADMIN,
MEMBER,
INVITED,
ACCEPTED,
ABILITY_READ,
ABILITY_WRITE
}
export { OWNER, ADMIN, MEMBER, INVITED, ACCEPTED, ABILITY_READ, ABILITY_WRITE };
+2 -1
View File
@@ -11,7 +11,8 @@ const integrationSlugNameMapping: Mapping = {
'netlify': 'Netlify',
'github': 'GitHub',
'render': 'Render',
'flyio': 'Fly.io'
'flyio': 'Fly.io',
"circleci": 'CircleCI'
}
const envMapping: Mapping = {
@@ -44,9 +44,9 @@ const CloudIntegration = ({
tabIndex={0}
className={`relative ${
cloudIntegrationOption.isAvailable
? 'hover:bg-white/10 duration-200 cursor-pointer'
? 'cursor-pointer duration-200 hover:bg-white/10'
: 'opacity-50'
} flex flex-row bg-white/5 h-32 rounded-md p-4 items-center`}
} flex h-32 flex-row items-center rounded-md bg-white/5 p-4`}
onClick={() => {
if (!cloudIntegrationOption.isAvailable) return;
setSelectedIntegrationOption(cloudIntegrationOption);
@@ -61,22 +61,22 @@ const CloudIntegration = ({
alt="integration logo"
/>
{cloudIntegrationOption.name.split(' ').length > 2 ? (
<div className="font-semibold text-gray-300 group-hover:text-gray-200 duration-200 text-3xl ml-4 max-w-xs">
<div className="ml-4 max-w-xs text-3xl font-semibold text-gray-300 duration-200 group-hover:text-gray-200">
<div>{cloudIntegrationOption.name.split(' ')[0]}</div>
<div className="text-base">
{cloudIntegrationOption.name.split(' ')[1]} {cloudIntegrationOption.name.split(' ')[2]}
</div>
</div>
) : (
<div className="font-semibold text-gray-300 group-hover:text-gray-200 duration-200 text-xl ml-4 max-w-xs">
<div className="ml-4 max-w-xs text-xl font-semibold text-gray-300 duration-200 group-hover:text-gray-200">
{cloudIntegrationOption.name}
</div>
)}
{cloudIntegrationOption.isAvailable &&
integrationAuths
.map((authorization) => authorization.integration)
.map((authorization) => authorization?.integration)
.includes(cloudIntegrationOption.slug) && (
<div className="absolute group z-40 top-0 right-0 flex flex-row">
<div className="group absolute top-0 right-0 z-40 flex flex-row">
<div
onKeyDown={() => null}
role="button"
@@ -86,8 +86,7 @@ const CloudIntegration = ({
const deletedIntegrationAuth = await deleteIntegrationAuth({
integrationAuthId: integrationAuths
.filter(
(authorization) =>
authorization.integration === cloudIntegrationOption.slug
(authorization) => authorization.integration === cloudIntegrationOption.slug
)
.map((authorization) => authorization._id)[0]
});
@@ -96,20 +95,20 @@ const CloudIntegration = ({
integrationAuth: deletedIntegrationAuth
});
}}
className="cursor-pointer w-max bg-red py-0.5 px-2 rounded-b-md text-xs flex flex-row items-center opacity-0 group-hover:opacity-100 duration-200"
className="flex w-max cursor-pointer flex-row items-center rounded-b-md bg-red py-0.5 px-2 text-xs opacity-0 duration-200 group-hover:opacity-100"
>
<FontAwesomeIcon icon={faX} className="text-xs mr-2 py-px" />
<FontAwesomeIcon icon={faX} className="mr-2 py-px text-xs" />
Revoke
</div>
<div className="w-max bg-primary py-0.5 px-2 rounded-bl-md rounded-tr-md text-xs flex flex-row items-center text-black opacity-90 group-hover:opacity-100 duration-200">
<FontAwesomeIcon icon={faCheck} className="text-xs mr-2" />
<div className="flex w-max flex-row items-center rounded-bl-md rounded-tr-md bg-primary py-0.5 px-2 text-xs text-black opacity-90 duration-200 group-hover:opacity-100">
<FontAwesomeIcon icon={faCheck} className="mr-2 text-xs" />
Authorized
</div>
</div>
)}
{!cloudIntegrationOption.isAvailable && (
<div className="absolute group z-50 top-0 right-0 flex flex-row">
<div className="w-max bg-yellow py-0.5 px-2 rounded-bl-md rounded-tr-md text-xs flex flex-row items-center text-black opacity-90">
<div className="group absolute top-0 right-0 z-50 flex flex-row">
<div className="flex w-max flex-row items-center rounded-bl-md rounded-tr-md bg-yellow py-0.5 px-2 text-xs text-black opacity-90">
Coming Soon
</div>
</div>
@@ -57,7 +57,7 @@ const IntegrationTile = ({
// set initial environment. This find will only execute when component is mounting
const [integrationEnvironment, setIntegrationEnvironment] = useState<Props['environments'][0]>(
environments.find(({ slug }) => slug === integration.environment) || {
environments.find(({ slug }) => slug === integration?.environment) || {
name: '',
slug: ''
}
@@ -69,9 +69,8 @@ const IntegrationTile = ({
useEffect(() => {
const loadIntegration = async () => {
const tempApps: [IntegrationApp] = await getIntegrationApps({
integrationAuthId: integration.integrationAuth
integrationAuthId: integration?.integrationAuth
});
setApps(tempApps);
@@ -90,7 +89,8 @@ const IntegrationTile = ({
case 'vercel':
setIntegrationTargetEnvironment(
integration?.targetEnvironment
? integration.targetEnvironment.charAt(0).toUpperCase() + integration.targetEnvironment.substring(1)
? integration.targetEnvironment.charAt(0).toUpperCase() +
integration.targetEnvironment.substring(1)
: 'Development'
);
break;
@@ -119,7 +119,7 @@ const IntegrationTile = ({
default:
return null;
}
}
};
try {
const siteApp = apps.find((app) => app.name === integrationApp); // obj or undefined
@@ -138,12 +138,12 @@ const IntegrationTile = ({
});
setIntegrations(
integrations.map((i) => i._id === updatedIntegration._id ? updatedIntegration : i)
integrations.map((i) => (i._id === updatedIntegration._id ? updatedIntegration : i))
);
} catch (err) {
console.error(err);
}
}
};
// eslint-disable-next-line @typescript-eslint/no-shadow
const renderIntegrationSpecificParams = (integration: Integration) => {
@@ -152,7 +152,7 @@ const IntegrationTile = ({
case 'vercel':
return (
<div>
<div className="text-gray-400 text-xs font-semibold mb-2 w-60">ENVIRONMENT</div>
<div className="mb-2 w-60 text-xs font-semibold text-gray-400">ENVIRONMENT</div>
<ListBox
data={!integration.isActive ? ['Development', 'Preview', 'Production'] : null}
isSelected={integrationTargetEnvironment}
@@ -164,7 +164,7 @@ const IntegrationTile = ({
case 'netlify':
return (
<div>
<div className="text-gray-400 text-xs font-semibold mb-2">CONTEXT</div>
<div className="mb-2 text-xs font-semibold text-gray-400">CONTEXT</div>
<ListBox
data={
!integration.isActive
@@ -189,10 +189,10 @@ const IntegrationTile = ({
if (!integrationApp) return <div />;
return (
<div className="max-w-5xl p-6 mx-6 mb-8 rounded-md bg-white/5 flex justify-between">
<div className="mx-6 mb-8 flex max-w-5xl justify-between rounded-md bg-white/5 p-6">
<div className="flex">
<div>
<p className="text-gray-400 text-xs font-semibold mb-2">ENVIRONMENT</p>
<p className="mb-2 text-xs font-semibold text-gray-400">ENVIRONMENT</p>
<ListBox
data={!integration.isActive ? environments.map(({ name }) => name) : null}
isSelected={integrationEnvironment.name}
@@ -208,7 +208,7 @@ const IntegrationTile = ({
/>
</div>
<div className="pt-2">
<FontAwesomeIcon icon={faArrowRight} className="mx-4 text-gray-400 mt-8" />
<FontAwesomeIcon icon={faArrowRight} className="mx-4 mt-8 text-gray-400" />
</div>
<div className="mr-2">
<p className="text-gray-400 text-xs font-semibold mb-2">INTEGRATION</p>
@@ -218,7 +218,7 @@ const IntegrationTile = ({
</div>
</div>
<div className="mr-2">
<div className="text-gray-400 text-xs font-semibold mb-2">APP</div>
<div className="mb-2 text-xs font-semibold text-gray-400">APP</div>
<ListBox
data={!integration.isActive ? apps.map((app) => app.name) : null}
isSelected={integrationApp}
@@ -231,9 +231,9 @@ const IntegrationTile = ({
</div>
<div className="flex items-end">
{integration.isActive ? (
<div className="max-w-5xl flex flex-row items-center bg-white/5 p-2 rounded-md px-4">
<FontAwesomeIcon icon={faRotate} className="text-lg mr-2.5 text-primary animate-spin" />
<div className="text-gray-300 font-semibold">In Sync</div>
<div className="flex max-w-5xl flex-row items-center rounded-md bg-white/5 p-2 px-4">
<FontAwesomeIcon icon={faRotate} className="mr-2.5 animate-spin text-lg text-primary" />
<div className="font-semibold text-gray-300">In Sync</div>
</div>
) : (
<Button
@@ -243,11 +243,13 @@ const IntegrationTile = ({
size="md"
/>
)}
<div className="opacity-50 hover:opacity-100 duration-200 ml-2">
<div className="ml-2 opacity-50 duration-200 hover:opacity-100">
<Button
onButtonPressed={() => handleDeleteIntegration({
onButtonPressed={() =>
handleDeleteIntegration({
integration
})}
})
}
color="red"
size="icon-md"
icon={faX}
@@ -35,16 +35,14 @@ const ProjectIntegrationSection = ({
}: Props) => {
return integrations.length > 0 ? (
<div className="mb-12">
<div className="flex flex-col justify-between items-start mx-4 mb-4 mt-6 text-xl max-w-5xl px-2">
<h1 className="font-semibold text-3xl">Current Integrations</h1>
<p className="text-base text-gray-400">
Manage integrations with third-party services.
</p>
<div className="mx-4 mb-4 mt-6 flex max-w-5xl flex-col items-start justify-between px-2 text-xl">
<h1 className="text-3xl font-semibold">Current Integrations</h1>
<p className="text-base text-gray-400">Manage integrations with third-party services.</p>
</div>
{integrations.map((integration: Integration) => {
return (
<IntegrationTile
key={`integration-${integration._id.toString()}`}
key={`integration-${integration?._id.toString()}`}
integration={integration}
integrations={integrations}
bot={bot}
@@ -59,6 +57,6 @@ const ProjectIntegrationSection = ({
) : (
<div />
);
}
};
export default ProjectIntegrationSection;
+6
View File
@@ -198,6 +198,9 @@ export default function Integrations() {
case 'flyio':
link = `${window.location.origin}/integrations/flyio/authorize`
break;
case 'circleci':
link = `${window.location.origin}/integrations/circleci/authorize`
break;
default:
break;
}
@@ -241,6 +244,9 @@ export default function Integrations() {
case 'flyio':
link = `${window.location.origin}/integrations/flyio/create?integrationAuthId=${integrationAuth._id}`;
break;
case 'circleci':
link = `${window.location.origin}/integrations/circleci/create?integrationAuthId=${integrationAuth._id}`;
break;
default:
break;
}
@@ -0,0 +1,77 @@
import { useState } from 'react';
import { useRouter } from 'next/router';
import { getTranslatedServerSideProps } from '../../../components/utilities/withTranslateProps';
import {
Button,
Card,
CardTitle,
FormControl,
Input,
} from '../../../components/v2';
import saveIntegrationAccessToken from "../../api/integrations/saveIntegrationAccessToken";
export default function CircleCICreateIntegrationPage() {
const router = useRouter();
const [apiKey, setApiKey] = useState('');
const [apiKeyErrorText, setApiKeyErrorText] = useState('');
const [isLoading, setIsLoading] = useState(false);
const handleButtonClick = async () => {
try {
setApiKeyErrorText('');
if (apiKey.length === 0) {
setApiKeyErrorText('API Key cannot be blank');
return;
}
setIsLoading(true);
const integrationAuth = await saveIntegrationAccessToken({
workspaceId: localStorage.getItem('projectData.id'),
integration: 'circleci',
accessToken: apiKey,
accessId: null,
});
setIsLoading(false);
router.push(
`/integrations/circleci/create?integrationAuthId=${integrationAuth._id}`
);
} catch (err) {
console.error(err);
}
}
return (
<div className="h-full w-full flex justify-center items-center">
<Card className="max-w-md p-8 rounded-md">
<CardTitle className='text-center'>CircleCI Integration</CardTitle>
<FormControl
label="CircleCI API Key"
errorText={apiKeyErrorText}
isError={apiKeyErrorText !== '' ?? false}
>
<Input
placeholder='rnd_xxx'
value={apiKey}
onChange={(e) => setApiKey(e.target.value)}
/>
</FormControl>
<Button
onClick={handleButtonClick}
color="mineshaft"
className='mt-4'
isLoading={isLoading}
>
Connect to CircleCI
</Button>
</Card>
</div>
)
}
CircleCICreateIntegrationPage.requireAuth = true;
export const getServerSideProps = getTranslatedServerSideProps(['integrations']);
@@ -0,0 +1,124 @@
import { useEffect, useState } from 'react';
import { useRouter } from 'next/router';
import queryString from 'query-string';
import { getTranslatedServerSideProps } from '../../../components/utilities/withTranslateProps';
import {
Button,
Card,
CardTitle,
FormControl,
Select,
SelectItem
} from '../../../components/v2';
import { useGetIntegrationAuthApps,useGetIntegrationAuthById } from '../../../hooks/api/integrationAuth';
import { useGetWorkspaceById } from '../../../hooks/api/workspace';
import createIntegration from "../../api/integrations/createIntegration";
export default function CircleCICreateIntegrationPage() {
const router = useRouter();
const { integrationAuthId } = queryString.parse(router.asPath.split('?')[1]);
const { data: workspace } = useGetWorkspaceById(localStorage.getItem('projectData.id') ?? '');
const { data: integrationAuth } = useGetIntegrationAuthById(integrationAuthId as string ?? '');
const { data: integrationAuthApps } = useGetIntegrationAuthApps(integrationAuthId as string ?? '');
const [selectedSourceEnvironment, setSelectedSourceEnvironment] = useState('');
const [targetApp, setTargetApp] = useState('');
const [isLoading, setIsLoading] = useState(false);
useEffect(() => {
if (workspace) {
setSelectedSourceEnvironment(workspace.environments[0].slug);
}
}, [workspace]);
useEffect(() => {
// TODO: handle case where apps can be empty
if (integrationAuthApps) {
setTargetApp(integrationAuthApps[0]?.name);
}
}, [integrationAuthApps]);
const handleButtonClick = async () => {
try {
if (!integrationAuth?._id) return;
setIsLoading(true);
await createIntegration({
integrationAuthId: integrationAuth?._id,
isActive: true,
app: targetApp,
appId: (integrationAuthApps?.find((integrationAuthApp) => integrationAuthApp.name === targetApp))?.appId ?? null,
sourceEnvironment: selectedSourceEnvironment,
targetEnvironment: null,
owner: null,
path: null,
region: null,
});
setIsLoading(false);
router.push(
`/integrations/${localStorage.getItem('projectData.id')}`
);
} catch (err) {
console.error(err);
}
}
return (integrationAuth && workspace && selectedSourceEnvironment && integrationAuthApps && targetApp) ? (
<div className="h-full w-full flex justify-center items-center">
<Card className="max-w-md p-8 rounded-md">
<CardTitle className='text-center'>CircleCI Integration</CardTitle>
<FormControl
label="Project Environment"
className='mt-4'
>
<Select
value={selectedSourceEnvironment}
onValueChange={(val) => setSelectedSourceEnvironment(val)}
className='w-full border border-mineshaft-500'
>
{workspace?.environments.map((sourceEnvironment) => (
<SelectItem value={sourceEnvironment.slug} key={`azure-key-vault-environment-${sourceEnvironment.slug}`}>
{sourceEnvironment.name}
</SelectItem>
))}
</Select>
</FormControl>
<FormControl
label="CircleCI Service"
className='mt-4'
>
<Select
value={targetApp}
onValueChange={(val) => setTargetApp(val)}
className='w-full border border-mineshaft-500'
>
{integrationAuthApps.map((integrationAuthApp) => (
<SelectItem value={integrationAuthApp.name} key={`render-environment-${integrationAuthApp.name}`}>
{integrationAuthApp.name}
</SelectItem>
))}
</Select>
</FormControl>
<Button
onClick={handleButtonClick}
color="mineshaft"
className='mt-4'
isLoading={isLoading}
>
Create Integration
</Button>
</Card>
</div>
) : <div />
}
CircleCICreateIntegrationPage.requireAuth = true;
export const getServerSideProps = getTranslatedServerSideProps(['integrations']);