mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-10-10 19:28:51 +00:00
Merge pull request #298 from Aashish-Upadhyay-101/circleci-integration-branch
Circleci integration branch
This commit is contained in:
@@ -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,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
@@ -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
@@ -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 };
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
);
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
|
||||
@@ -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 };
|
||||
|
||||
@@ -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;
|
||||
@@ -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']);
|
||||
Reference in New Issue
Block a user