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 ( export const getIntegrationOptions = async (req: Request, res: Response) => {
req: Request, return res.status(200).send({
res: Response integrationOptions: INTEGRATION_OPTIONS,
) => { });
return res.status(200).send({ };
integrationOptions: INTEGRATION_OPTIONS
});
}
/** /**
* Perform OAuth2 code-token exchange as part of integration [integration] for workspace with id [workspaceId] * Perform OAuth2 code-token exchange as part of integration [integration] for workspace with id [workspaceId]
@@ -90,8 +87,8 @@ export const oAuthExchange = async (
* @param res * @param res
*/ */
export const saveIntegrationAccessToken = async ( export const saveIntegrationAccessToken = async (
req: Request, req: Request,
res: Response res: Response
) => { ) => {
// TODO: refactor // TODO: refactor
// TODO: check if access token is valid for each integration // TODO: check if access token is valid for each integration
@@ -157,23 +154,23 @@ export const saveIntegrationAccessToken = async (
* @returns * @returns
*/ */
export const getIntegrationAuthApps = async (req: Request, res: Response) => { export const getIntegrationAuthApps = async (req: Request, res: Response) => {
let apps; let apps;
try { try {
apps = await getApps({ apps = await getApps({
integrationAuth: req.integrationAuth, integrationAuth: req.integrationAuth,
accessToken: req.accessToken accessToken: req.accessToken,
}); });
} catch (err) { } catch (err) {
Sentry.setUser({ email: req.user.email }); Sentry.setUser({ email: req.user.email });
Sentry.captureException(err); Sentry.captureException(err);
return res.status(400).send({ return res.status(400).send({
message: 'Failed to get integration authorization applications' message: "Failed to get integration authorization applications",
}); });
} }
return res.status(200).send({ return res.status(200).send({
apps apps,
}); });
}; };
/** /**
@@ -183,21 +180,21 @@ export const getIntegrationAuthApps = async (req: Request, res: Response) => {
* @returns * @returns
*/ */
export const deleteIntegrationAuth = async (req: Request, res: Response) => { export const deleteIntegrationAuth = async (req: Request, res: Response) => {
let integrationAuth; let integrationAuth;
try { try {
integrationAuth = await revokeAccess({ integrationAuth = await revokeAccess({
integrationAuth: req.integrationAuth, integrationAuth: req.integrationAuth,
accessToken: req.accessToken accessToken: req.accessToken,
}); });
} catch (err) { } catch (err) {
Sentry.setUser({ email: req.user.email }); Sentry.setUser({ email: req.user.email });
Sentry.captureException(err); Sentry.captureException(err);
return res.status(400).send({ return res.status(400).send({
message: 'Failed to delete integration authorization' message: "Failed to delete integration authorization",
}); });
} }
return res.status(200).send({ return res.status(200).send({
integrationAuth integrationAuth,
}); });
} };
@@ -65,10 +65,10 @@ export const createIntegration = async (req: Request, res: Response) => {
}); });
} }
return res.status(200).send({ return res.status(200).send({
integration integration,
}); });
} };
/** /**
* Change environment or name of integration with id [integrationId] * Change environment or name of integration with id [integrationId]
@@ -77,57 +77,57 @@ export const createIntegration = async (req: Request, res: Response) => {
* @returns * @returns
*/ */
export const updateIntegration = async (req: Request, res: Response) => { export const updateIntegration = async (req: Request, res: Response) => {
let integration; let integration;
// TODO: add integration-specific validation to ensure that each // TODO: add integration-specific validation to ensure that each
// integration has the correct fields populated in [Integration] // integration has the correct fields populated in [Integration]
try { try {
const { const {
environment, environment,
isActive, isActive,
app, app,
appId, appId,
targetEnvironment, targetEnvironment,
owner, // github-specific integration param owner, // github-specific integration param
} = req.body; } = req.body;
integration = await Integration.findOneAndUpdate( integration = await Integration.findOneAndUpdate(
{ {
_id: req.integration._id _id: req.integration._id,
}, },
{ {
environment, environment,
isActive, isActive,
app, app,
appId, appId,
targetEnvironment, targetEnvironment,
owner owner,
}, },
{ {
new: true new: true,
} }
); );
if (integration) { if (integration) {
// trigger event - push secrets // trigger event - push secrets
EventService.handleEvent({ EventService.handleEvent({
event: eventPushSecrets({ event: eventPushSecrets({
workspaceId: integration.workspace.toString() workspaceId: integration.workspace.toString(),
}) }),
}); });
} }
} catch (err) { } catch (err) {
Sentry.setUser({ email: req.user.email }); Sentry.setUser({ email: req.user.email });
Sentry.captureException(err); Sentry.captureException(err);
return res.status(400).send({ return res.status(400).send({
message: 'Failed to update integration' message: "Failed to update integration",
}); });
} }
return res.status(200).send({ return res.status(200).send({
integration integration,
}); });
}; };
/** /**
@@ -138,24 +138,24 @@ export const updateIntegration = async (req: Request, res: Response) => {
* @returns * @returns
*/ */
export const deleteIntegration = async (req: Request, res: Response) => { export const deleteIntegration = async (req: Request, res: Response) => {
let integration; let integration;
try { try {
const { integrationId } = req.params; const { integrationId } = req.params;
integration = await Integration.findOneAndDelete({ 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) { } catch (err) {
Sentry.setUser({ email: req.user.email }); Sentry.setUser({ email: req.user.email });
Sentry.captureException(err); Sentry.captureException(err);
return res.status(400).send({ return res.status(400).send({
message: 'Failed to delete integration' message: "Failed to delete integration",
}); });
} }
return res.status(200).send({ return res.status(200).send({
integration integration,
}); });
}; };
+219 -220
View File
@@ -1,21 +1,21 @@
import { Request, Response } from 'express'; import { Request, Response } from "express";
import * as Sentry from '@sentry/node'; import * as Sentry from "@sentry/node";
import { import {
Workspace, Workspace,
Membership, Membership,
MembershipOrg, MembershipOrg,
Integration, Integration,
IntegrationAuth, IntegrationAuth,
IUser, IUser,
ServiceToken, ServiceToken,
ServiceTokenData ServiceTokenData,
} from '../../models'; } from "../../models";
import { import {
createWorkspace as create, createWorkspace as create,
deleteWorkspace as deleteWork deleteWorkspace as deleteWork,
} from '../../helpers/workspace'; } from "../../helpers/workspace";
import { addMemberships } from '../../helpers/membership'; import { addMemberships } from "../../helpers/membership";
import { ADMIN } from '../../variables'; import { ADMIN } from "../../variables";
/** /**
* Return public keys of members of workspace with id [workspaceId] * Return public keys of members of workspace with id [workspaceId]
@@ -24,32 +24,31 @@ import { ADMIN } from '../../variables';
* @returns * @returns
*/ */
export const getWorkspacePublicKeys = async (req: Request, res: Response) => { export const getWorkspacePublicKeys = async (req: Request, res: Response) => {
let publicKeys; let publicKeys;
try { try {
const { workspaceId } = req.params; const { workspaceId } = req.params;
publicKeys = ( publicKeys = (
await Membership.find({ await Membership.find({
workspace: workspaceId workspace: workspaceId,
}).populate<{ user: IUser }>('user', 'publicKey') }).populate<{ user: IUser }>("user", "publicKey")
) ).map((member) => {
.map((member) => { return {
return { publicKey: member.user.publicKey,
publicKey: member.user.publicKey, userId: member.user._id,
userId: member.user._id };
}; });
}); } catch (err) {
} catch (err) { Sentry.setUser({ email: req.user.email });
Sentry.setUser({ email: req.user.email }); Sentry.captureException(err);
Sentry.captureException(err); return res.status(400).send({
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({ return res.status(200).send({
publicKeys publicKeys,
}); });
}; };
/** /**
@@ -59,24 +58,24 @@ export const getWorkspacePublicKeys = async (req: Request, res: Response) => {
* @returns * @returns
*/ */
export const getWorkspaceMemberships = async (req: Request, res: Response) => { export const getWorkspaceMemberships = async (req: Request, res: Response) => {
let users; let users;
try { try {
const { workspaceId } = req.params; const { workspaceId } = req.params;
users = await Membership.find({ users = await Membership.find({
workspace: workspaceId workspace: workspaceId,
}).populate('user', '+publicKey'); }).populate("user", "+publicKey");
} catch (err) { } catch (err) {
Sentry.setUser({ email: req.user.email }); Sentry.setUser({ email: req.user.email });
Sentry.captureException(err); Sentry.captureException(err);
return res.status(400).send({ return res.status(400).send({
message: 'Failed to get workspace members' message: "Failed to get workspace members",
}); });
} }
return res.status(200).send({ return res.status(200).send({
users users,
}); });
}; };
/** /**
@@ -86,24 +85,24 @@ export const getWorkspaceMemberships = async (req: Request, res: Response) => {
* @returns * @returns
*/ */
export const getWorkspaces = async (req: Request, res: Response) => { export const getWorkspaces = async (req: Request, res: Response) => {
let workspaces; let workspaces;
try { try {
workspaces = ( workspaces = (
await Membership.find({ await Membership.find({
user: req.user._id user: req.user._id,
}).populate('workspace') }).populate("workspace")
).map((m) => m.workspace); ).map((m) => m.workspace);
} catch (err) { } catch (err) {
Sentry.setUser({ email: req.user.email }); Sentry.setUser({ email: req.user.email });
Sentry.captureException(err); Sentry.captureException(err);
return res.status(400).send({ return res.status(400).send({
message: 'Failed to get workspaces' message: "Failed to get workspaces",
}); });
} }
return res.status(200).send({ return res.status(200).send({
workspaces workspaces,
}); });
}; };
/** /**
@@ -113,24 +112,24 @@ export const getWorkspaces = async (req: Request, res: Response) => {
* @returns * @returns
*/ */
export const getWorkspace = async (req: Request, res: Response) => { export const getWorkspace = async (req: Request, res: Response) => {
let workspace; let workspace;
try { try {
const { workspaceId } = req.params; const { workspaceId } = req.params;
workspace = await Workspace.findOne({ workspace = await Workspace.findOne({
_id: workspaceId _id: workspaceId,
}); });
} catch (err) { } catch (err) {
Sentry.setUser({ email: req.user.email }); Sentry.setUser({ email: req.user.email });
Sentry.captureException(err); Sentry.captureException(err);
return res.status(400).send({ return res.status(400).send({
message: 'Failed to get workspace' message: "Failed to get workspace",
}); });
} }
return res.status(200).send({ return res.status(200).send({
workspace workspace,
}); });
}; };
/** /**
@@ -141,46 +140,46 @@ export const getWorkspace = async (req: Request, res: Response) => {
* @returns * @returns
*/ */
export const createWorkspace = async (req: Request, res: Response) => { export const createWorkspace = async (req: Request, res: Response) => {
let workspace; let workspace;
try { try {
const { workspaceName, organizationId } = req.body; const { workspaceName, organizationId } = req.body;
// validate organization membership // validate organization membership
const membershipOrg = await MembershipOrg.findOne({ const membershipOrg = await MembershipOrg.findOne({
user: req.user._id, user: req.user._id,
organization: organizationId organization: organizationId,
}); });
if (!membershipOrg) { if (!membershipOrg) {
throw new Error('Failed to validate organization membership'); throw new Error("Failed to validate organization membership");
} }
if (workspaceName.length < 1) { 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 // create workspace and add user as member
workspace = await create({ workspace = await create({
name: workspaceName, name: workspaceName,
organizationId organizationId,
}); });
await addMemberships({ await addMemberships({
userIds: [req.user._id], userIds: [req.user._id],
workspaceId: workspace._id.toString(), workspaceId: workspace._id.toString(),
roles: [ADMIN] roles: [ADMIN],
}); });
} catch (err) { } catch (err) {
Sentry.setUser({ email: req.user.email }); Sentry.setUser({ email: req.user.email });
Sentry.captureException(err); Sentry.captureException(err);
return res.status(400).send({ return res.status(400).send({
message: 'Failed to create workspace' message: "Failed to create workspace",
}); });
} }
return res.status(200).send({ return res.status(200).send({
workspace workspace,
}); });
}; };
/** /**
@@ -190,24 +189,24 @@ export const createWorkspace = async (req: Request, res: Response) => {
* @returns * @returns
*/ */
export const deleteWorkspace = async (req: Request, res: Response) => { export const deleteWorkspace = async (req: Request, res: Response) => {
try { try {
const { workspaceId } = req.params; const { workspaceId } = req.params;
// delete workspace // delete workspace
await deleteWork({ await deleteWork({
id: workspaceId id: workspaceId,
}); });
} catch (err) { } catch (err) {
Sentry.setUser({ email: req.user.email }); Sentry.setUser({ email: req.user.email });
Sentry.captureException(err); Sentry.captureException(err);
return res.status(400).send({ return res.status(400).send({
message: 'Failed to delete workspace' message: "Failed to delete workspace",
}); });
} }
return res.status(200).send({ return res.status(200).send({
message: 'Successfully deleted workspace' message: "Successfully deleted workspace",
}); });
}; };
/** /**
@@ -217,34 +216,34 @@ export const deleteWorkspace = async (req: Request, res: Response) => {
* @returns * @returns
*/ */
export const changeWorkspaceName = async (req: Request, res: Response) => { export const changeWorkspaceName = async (req: Request, res: Response) => {
let workspace; let workspace;
try { try {
const { workspaceId } = req.params; const { workspaceId } = req.params;
const { name } = req.body; const { name } = req.body;
workspace = await Workspace.findOneAndUpdate( workspace = await Workspace.findOneAndUpdate(
{ {
_id: workspaceId _id: workspaceId,
}, },
{ {
name name,
}, },
{ {
new: true new: true,
} }
); );
} catch (err) { } catch (err) {
Sentry.setUser({ email: req.user.email }); Sentry.setUser({ email: req.user.email });
Sentry.captureException(err); Sentry.captureException(err);
return res.status(400).send({ return res.status(400).send({
message: 'Failed to change workspace name' message: "Failed to change workspace name",
}); });
} }
return res.status(200).send({ return res.status(200).send({
message: 'Successfully changed workspace name', message: "Successfully changed workspace name",
workspace workspace,
}); });
}; };
/** /**
@@ -254,24 +253,24 @@ export const changeWorkspaceName = async (req: Request, res: Response) => {
* @returns * @returns
*/ */
export const getWorkspaceIntegrations = async (req: Request, res: Response) => { export const getWorkspaceIntegrations = async (req: Request, res: Response) => {
let integrations; let integrations;
try { try {
const { workspaceId } = req.params; const { workspaceId } = req.params;
integrations = await Integration.find({ integrations = await Integration.find({
workspace: workspaceId workspace: workspaceId,
}); });
} catch (err) { } catch (err) {
Sentry.setUser({ email: req.user.email }); Sentry.setUser({ email: req.user.email });
Sentry.captureException(err); Sentry.captureException(err);
return res.status(400).send({ return res.status(400).send({
message: 'Failed to get workspace integrations' message: "Failed to get workspace integrations",
}); });
} }
return res.status(200).send({ return res.status(200).send({
integrations integrations,
}); });
}; };
/** /**
@@ -281,27 +280,27 @@ export const getWorkspaceIntegrations = async (req: Request, res: Response) => {
* @returns * @returns
*/ */
export const getWorkspaceIntegrationAuthorizations = async ( export const getWorkspaceIntegrationAuthorizations = async (
req: Request, req: Request,
res: Response res: Response
) => { ) => {
let authorizations; let authorizations;
try { try {
const { workspaceId } = req.params; const { workspaceId } = req.params;
authorizations = await IntegrationAuth.find({ authorizations = await IntegrationAuth.find({
workspace: workspaceId workspace: workspaceId,
}); });
} catch (err) { } catch (err) {
Sentry.setUser({ email: req.user.email }); Sentry.setUser({ email: req.user.email });
Sentry.captureException(err); Sentry.captureException(err);
return res.status(400).send({ return res.status(400).send({
message: 'Failed to get workspace integration authorizations' message: "Failed to get workspace integration authorizations",
}); });
} }
return res.status(200).send({ return res.status(200).send({
authorizations authorizations,
}); });
}; };
/** /**
@@ -311,26 +310,26 @@ export const getWorkspaceIntegrationAuthorizations = async (
* @returns * @returns
*/ */
export const getWorkspaceServiceTokens = async ( export const getWorkspaceServiceTokens = async (
req: Request, req: Request,
res: Response res: Response
) => { ) => {
let serviceTokens; let serviceTokens;
try { try {
const { workspaceId } = req.params; const { workspaceId } = req.params;
// ?? FIX. // ?? FIX.
serviceTokens = await ServiceToken.find({ serviceTokens = await ServiceToken.find({
user: req.user._id, user: req.user._id,
workspace: workspaceId workspace: workspaceId,
}); });
} catch (err) { } catch (err) {
Sentry.setUser({ email: req.user.email }); Sentry.setUser({ email: req.user.email });
Sentry.captureException(err); Sentry.captureException(err);
return res.status(400).send({ return res.status(400).send({
message: 'Failed to get workspace service tokens' message: "Failed to get workspace service tokens",
}); });
} }
return res.status(200).send({ return res.status(200).send({
serviceTokens serviceTokens,
}); });
} };
+114 -86
View File
@@ -1,7 +1,7 @@
import axios from 'axios'; import axios from "axios";
import * as Sentry from '@sentry/node'; import * as Sentry from "@sentry/node";
import { Octokit } from '@octokit/rest'; import { Octokit } from "@octokit/rest";
import { IIntegrationAuth } from '../models'; import { IIntegrationAuth } from "../models";
import { import {
INTEGRATION_AZURE_KEY_VAULT, INTEGRATION_AZURE_KEY_VAULT,
INTEGRATION_AWS_PARAMETER_STORE, INTEGRATION_AWS_PARAMETER_STORE,
@@ -12,12 +12,14 @@ import {
INTEGRATION_GITHUB, INTEGRATION_GITHUB,
INTEGRATION_RENDER, INTEGRATION_RENDER,
INTEGRATION_FLYIO, INTEGRATION_FLYIO,
INTEGRATION_CIRCLECI,
INTEGRATION_HEROKU_API_URL, INTEGRATION_HEROKU_API_URL,
INTEGRATION_VERCEL_API_URL, INTEGRATION_VERCEL_API_URL,
INTEGRATION_NETLIFY_API_URL, INTEGRATION_NETLIFY_API_URL,
INTEGRATION_RENDER_API_URL, INTEGRATION_RENDER_API_URL,
INTEGRATION_FLYIO_API_URL INTEGRATION_FLYIO_API_URL,
} from '../variables'; INTEGRATION_CIRCLECI_API_URL,
} from "../variables";
/** /**
* Return list of names of apps for integration named [integration] * Return list of names of apps for integration named [integration]
@@ -29,7 +31,7 @@ import {
*/ */
const getApps = async ({ const getApps = async ({
integrationAuth, integrationAuth,
accessToken accessToken,
}: { }: {
integrationAuth: IIntegrationAuth; integrationAuth: IIntegrationAuth;
accessToken: string; accessToken: string;
@@ -54,40 +56,45 @@ const getApps = async ({
break; break;
case INTEGRATION_HEROKU: case INTEGRATION_HEROKU:
apps = await getAppsHeroku({ apps = await getAppsHeroku({
accessToken accessToken,
}); });
break; break;
case INTEGRATION_VERCEL: case INTEGRATION_VERCEL:
apps = await getAppsVercel({ apps = await getAppsVercel({
integrationAuth, integrationAuth,
accessToken accessToken,
}); });
break; break;
case INTEGRATION_NETLIFY: case INTEGRATION_NETLIFY:
apps = await getAppsNetlify({ apps = await getAppsNetlify({
accessToken accessToken,
}); });
break; break;
case INTEGRATION_GITHUB: case INTEGRATION_GITHUB:
apps = await getAppsGithub({ apps = await getAppsGithub({
accessToken accessToken,
}); });
break; break;
case INTEGRATION_RENDER: case INTEGRATION_RENDER:
apps = await getAppsRender({ apps = await getAppsRender({
accessToken accessToken,
}); });
break; break;
case INTEGRATION_FLYIO: case INTEGRATION_FLYIO:
apps = await getAppsFlyio({ apps = await getAppsFlyio({
accessToken accessToken,
});
break;
case INTEGRATION_CIRCLECI:
apps = await getAppsCircleci({
accessToken,
}); });
break; break;
} }
} catch (err) { } catch (err) {
Sentry.setUser(null); Sentry.setUser(null);
Sentry.captureException(err); Sentry.captureException(err);
throw new Error('Failed to get integration apps'); throw new Error("Failed to get integration apps");
} }
return apps; return apps;
@@ -106,19 +113,19 @@ const getAppsHeroku = async ({ accessToken }: { accessToken: string }) => {
const res = ( const res = (
await axios.get(`${INTEGRATION_HEROKU_API_URL}/apps`, { await axios.get(`${INTEGRATION_HEROKU_API_URL}/apps`, {
headers: { headers: {
Accept: 'application/vnd.heroku+json; version=3', Accept: "application/vnd.heroku+json; version=3",
Authorization: `Bearer ${accessToken}` Authorization: `Bearer ${accessToken}`,
} },
}) })
).data; ).data;
apps = res.map((a: any) => ({ apps = res.map((a: any) => ({
name: a.name name: a.name,
})); }));
} catch (err) { } catch (err) {
Sentry.setUser(null); Sentry.setUser(null);
Sentry.captureException(err); Sentry.captureException(err);
throw new Error('Failed to get Heroku integration apps'); throw new Error("Failed to get Heroku integration apps");
} }
return apps; return apps;
@@ -133,7 +140,7 @@ const getAppsHeroku = async ({ accessToken }: { accessToken: string }) => {
*/ */
const getAppsVercel = async ({ const getAppsVercel = async ({
integrationAuth, integrationAuth,
accessToken accessToken,
}: { }: {
integrationAuth: IIntegrationAuth; integrationAuth: IIntegrationAuth;
accessToken: string; accessToken: string;
@@ -146,21 +153,23 @@ const getAppsVercel = async ({
Authorization: `Bearer ${accessToken}`, Authorization: `Bearer ${accessToken}`,
'Accept-Encoding': 'application/json' 'Accept-Encoding': 'application/json'
}, },
...( integrationAuth?.teamId ? { ...(integrationAuth?.teamId
params: { ? {
teamId: integrationAuth.teamId params: {
} teamId: integrationAuth.teamId,
} : {}) },
}
: {}),
}) })
).data; ).data;
apps = res.projects.map((a: any) => ({ apps = res.projects.map((a: any) => ({
name: a.name name: a.name,
})); }));
} catch (err) { } catch (err) {
Sentry.setUser(null); Sentry.setUser(null);
Sentry.captureException(err); Sentry.captureException(err);
throw new Error('Failed to get Vercel integration apps'); throw new Error("Failed to get Vercel integration apps");
} }
return apps; return apps;
@@ -173,11 +182,7 @@ const getAppsVercel = async ({
* @returns {Object[]} apps - names of Netlify sites * @returns {Object[]} apps - names of Netlify sites
* @returns {String} apps.name - name of Netlify site * @returns {String} apps.name - name of Netlify site
*/ */
const getAppsNetlify = async ({ const getAppsNetlify = async ({ accessToken }: { accessToken: string }) => {
accessToken
}: {
accessToken: string;
}) => {
let apps; let apps;
try { try {
const res = ( const res = (
@@ -191,12 +196,12 @@ const getAppsNetlify = async ({
apps = res.map((a: any) => ({ apps = res.map((a: any) => ({
name: a.name, name: a.name,
appId: a.site_id appId: a.site_id,
})); }));
} catch (err) { } catch (err) {
Sentry.setUser(null); Sentry.setUser(null);
Sentry.captureException(err); Sentry.captureException(err);
throw new Error('Failed to get Netlify integration apps'); throw new Error("Failed to get Netlify integration apps");
} }
return apps; return apps;
@@ -209,35 +214,32 @@ const getAppsNetlify = async ({
* @returns {Object[]} apps - names of Netlify sites * @returns {Object[]} apps - names of Netlify sites
* @returns {String} apps.name - name of Netlify site * @returns {String} apps.name - name of Netlify site
*/ */
const getAppsGithub = async ({ const getAppsGithub = async ({ accessToken }: { accessToken: string }) => {
accessToken
}: {
accessToken: string;
}) => {
let apps; let apps;
try { try {
const octokit = new Octokit({ const octokit = new Octokit({
auth: accessToken auth: accessToken,
}); });
const repos = (await octokit.request( const repos = (
'GET /user/repos{?visibility,affiliation,type,sort,direction,per_page,page,since,before}', 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 apps = repos
.filter((a:any) => a.permissions.admin === true) .filter((a: any) => a.permissions.admin === true)
.map((a: any) => ({ .map((a: any) => ({
name: a.name, name: a.name,
owner: a.owner.login owner: a.owner.login,
}) }));
);
} catch (err) { } catch (err) {
Sentry.setUser(null); Sentry.setUser(null);
Sentry.captureException(err); Sentry.captureException(err);
throw new Error('Failed to get Github repos'); throw new Error("Failed to get Github repos");
} }
return apps; return apps;
@@ -251,11 +253,7 @@ const getAppsGithub = async ({
* @returns {String} apps.name - name of Render service * @returns {String} apps.name - name of Render service
* @returns {String} apps.appId - id of Render service * @returns {String} apps.appId - id of Render service
*/ */
const getAppsRender = async ({ const getAppsRender = async ({ accessToken }: { accessToken: string }) => {
accessToken
}: {
accessToken: string;
}) => {
let apps: any; let apps: any;
try { try {
const res = ( const res = (
@@ -263,8 +261,8 @@ const getAppsRender = async ({
headers: { headers: {
Authorization: `Bearer ${accessToken}`, Authorization: `Bearer ${accessToken}`,
Accept: 'application/json', Accept: 'application/json',
'Accept-Encoding': 'application/json' 'Accept-Encoding': 'application/json',
} },
}) })
).data; ).data;
@@ -277,11 +275,11 @@ const getAppsRender = async ({
} catch (err) { } catch (err) {
Sentry.setUser(null); Sentry.setUser(null);
Sentry.captureException(err); Sentry.captureException(err);
throw new Error('Failed to get Render services'); throw new Error("Failed to get Render services");
} }
return apps; return apps;
} };
/** /**
* Return list of apps for Fly.io integration * 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 {Object[]} apps - names and ids of Fly.io apps
* @returns {String} apps.name - name of Fly.io apps * @returns {String} apps.name - name of Fly.io apps
*/ */
const getAppsFlyio = async ({ const getAppsFlyio = async ({ accessToken }: { accessToken: string }) => {
accessToken
}: {
accessToken: string;
}) => {
let apps; let apps;
try { try {
const query = ` const query = `
@@ -309,33 +303,67 @@ const getAppsFlyio = async ({
} }
`; `;
const res = (await axios({ const res = (
url: INTEGRATION_FLYIO_API_URL, await axios({
method: 'post', url: INTEGRATION_FLYIO_API_URL,
headers: { method: "post",
'Authorization': 'Bearer ' + accessToken, headers: {
Authorization: "Bearer " + accessToken,
'Accept': 'application/json', 'Accept': 'application/json',
'Accept-Encoding': 'application/json' 'Accept-Encoding': 'application/json',
}, },
data: { data: {
query, query,
variables: { variables: {
role: null role: null,
} },
} },
})).data.data.apps.nodes; })
).data.data.apps.nodes;
apps = res apps = res.map((a: any) => ({
.map((a: any) => ({ name: a.name,
name: a.name }));
}));
} catch (err) { } catch (err) {
Sentry.setUser(null); Sentry.setUser(null);
Sentry.captureException(err); Sentry.captureException(err);
throw new Error('Failed to get Fly.io apps'); throw new Error("Failed to get Fly.io apps");
} }
return 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 }; export { getApps };
File diff suppressed because it is too large Load Diff
+27 -22
View File
@@ -1,4 +1,4 @@
import { Schema, model, Types } from 'mongoose'; import { Schema, model, Types } from "mongoose";
import { import {
INTEGRATION_AZURE_KEY_VAULT, INTEGRATION_AZURE_KEY_VAULT,
INTEGRATION_AWS_PARAMETER_STORE, INTEGRATION_AWS_PARAMETER_STORE,
@@ -8,8 +8,9 @@ import {
INTEGRATION_NETLIFY, INTEGRATION_NETLIFY,
INTEGRATION_GITHUB, INTEGRATION_GITHUB,
INTEGRATION_RENDER, INTEGRATION_RENDER,
INTEGRATION_FLYIO INTEGRATION_FLYIO,
} from '../variables'; INTEGRATION_CIRCLECI,
} from "../variables";
export interface IIntegration { export interface IIntegration {
_id: Types.ObjectId; _id: Types.ObjectId;
@@ -31,44 +32,47 @@ export interface IIntegration {
| 'netlify' | 'netlify'
| 'github' | 'github'
| 'render' | 'render'
| 'flyio'; | 'flyio'
| 'circleci';
integrationAuth: Types.ObjectId; integrationAuth: Types.ObjectId;
} }
const integrationSchema = new Schema<IIntegration>( const integrationSchema = new Schema<IIntegration>(
{ {
workspace: { workspace: {
type: Schema.Types.ObjectId, type: Schema.Types.ObjectId,
ref: 'Workspace', ref: "Workspace",
required: true required: true,
}, },
environment: { environment: {
type: String, type: String,
required: true required: true,
}, },
isActive: { isActive: {
type: Boolean, type: Boolean,
required: true required: true,
}, },
app: { app: {
// name of app in provider // name of app in provider
type: String, type: String,
default: null default: null,
}, },
appId: { // (new) appId: {
// (new)
// id of app in provider // id of app in provider
type: String, type: String,
default: null default: null,
}, },
targetEnvironment: { // (new) targetEnvironment: {
// (new)
// target environment // target environment
type: String, type: String,
default: null default: null,
}, },
owner: { owner: {
// github-specific repo owner-login // github-specific repo owner-login
type: String, type: String,
default: null default: null,
}, },
path: { path: {
// aws-parameter-store-specific path // aws-parameter-store-specific path
@@ -91,21 +95,22 @@ const integrationSchema = new Schema<IIntegration>(
INTEGRATION_NETLIFY, INTEGRATION_NETLIFY,
INTEGRATION_GITHUB, INTEGRATION_GITHUB,
INTEGRATION_RENDER, INTEGRATION_RENDER,
INTEGRATION_FLYIO INTEGRATION_FLYIO,
INTEGRATION_CIRCLECI,
], ],
required: true required: true,
}, },
integrationAuth: { integrationAuth: {
type: Schema.Types.ObjectId, type: Schema.Types.ObjectId,
ref: 'IntegrationAuth', ref: "IntegrationAuth",
required: true required: true,
} },
}, },
{ {
timestamps: true timestamps: true,
} }
); );
const Integration = model<IIntegration>('Integration', integrationSchema); const Integration = model<IIntegration>("Integration", integrationSchema);
export default Integration; export default Integration;
+25 -32
View File
@@ -1,4 +1,4 @@
import { Schema, model, Types } from 'mongoose'; import { Schema, model, Types } from "mongoose";
import { import {
INTEGRATION_AZURE_KEY_VAULT, INTEGRATION_AZURE_KEY_VAULT,
INTEGRATION_AWS_PARAMETER_STORE, INTEGRATION_AWS_PARAMETER_STORE,
@@ -8,24 +8,16 @@ import {
INTEGRATION_NETLIFY, INTEGRATION_NETLIFY,
INTEGRATION_GITHUB, INTEGRATION_GITHUB,
INTEGRATION_RENDER, INTEGRATION_RENDER,
INTEGRATION_FLYIO INTEGRATION_FLYIO,
} from '../variables'; INTEGRATION_CIRCLECI,
} from "../variables";
export interface IIntegrationAuth { export interface IIntegrationAuth {
_id: Types.ObjectId; _id: Types.ObjectId;
workspace: Types.ObjectId; workspace: Types.ObjectId;
integration: integration: 'heroku' | 'vercel' | 'netlify' | 'github' | 'render' | 'flyio' | 'azure-key-vault' | 'circleci' | 'aws-parameter-store' | 'aws-secret-manager';
| 'azure-key-vault' teamId: string;
| 'aws-parameter-store' accountId: string;
| '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
refreshCiphertext?: string; refreshCiphertext?: string;
refreshIV?: string; refreshIV?: string;
refreshTag?: string; refreshTag?: string;
@@ -41,9 +33,9 @@ export interface IIntegrationAuth {
const integrationAuthSchema = new Schema<IIntegrationAuth>( const integrationAuthSchema = new Schema<IIntegrationAuth>(
{ {
workspace: { workspace: {
type: Schema.Types.ObjectId, type: Schema.Types.ObjectId,
ref: 'Workspace', ref: "Workspace",
required: true required: true,
}, },
integration: { integration: {
type: String, type: String,
@@ -56,29 +48,30 @@ const integrationAuthSchema = new Schema<IIntegrationAuth>(
INTEGRATION_NETLIFY, INTEGRATION_NETLIFY,
INTEGRATION_GITHUB, INTEGRATION_GITHUB,
INTEGRATION_RENDER, INTEGRATION_RENDER,
INTEGRATION_FLYIO INTEGRATION_FLYIO,
INTEGRATION_CIRCLECI,
], ],
required: true required: true,
}, },
teamId: { teamId: {
// vercel-specific integration param // vercel-specific integration param
type: String type: String,
}, },
accountId: { accountId: {
// netlify-specific integration param // netlify-specific integration param
type: String type: String,
}, },
refreshCiphertext: { refreshCiphertext: {
type: String, type: String,
select: false select: false,
}, },
refreshIV: { refreshIV: {
type: String, type: String,
select: false select: false,
}, },
refreshTag: { refreshTag: {
type: String, type: String,
select: false select: false,
}, },
accessIdCiphertext: { accessIdCiphertext: {
type: String, type: String,
@@ -94,28 +87,28 @@ const integrationAuthSchema = new Schema<IIntegrationAuth>(
}, },
accessCiphertext: { accessCiphertext: {
type: String, type: String,
select: false select: false,
}, },
accessIV: { accessIV: {
type: String, type: String,
select: false select: false,
}, },
accessTag: { accessTag: {
type: String, type: String,
select: false select: false,
}, },
accessExpiresAt: { accessExpiresAt: {
type: Date, type: Date,
select: false select: false,
} },
}, },
{ {
timestamps: true timestamps: true,
} }
); );
const IntegrationAuth = model<IIntegrationAuth>( const IntegrationAuth = model<IIntegrationAuth>(
'IntegrationAuth', "IntegrationAuth",
integrationAuthSchema integrationAuthSchema
); );
+15 -17
View File
@@ -3,8 +3,8 @@ import {
ENV_TESTING, ENV_TESTING,
ENV_STAGING, ENV_STAGING,
ENV_PROD, ENV_PROD,
ENV_SET ENV_SET,
} from './environment'; } from "./environment";
import { import {
INTEGRATION_AZURE_KEY_VAULT, INTEGRATION_AZURE_KEY_VAULT,
INTEGRATION_AWS_PARAMETER_STORE, INTEGRATION_AWS_PARAMETER_STORE,
@@ -15,6 +15,7 @@ import {
INTEGRATION_GITHUB, INTEGRATION_GITHUB,
INTEGRATION_RENDER, INTEGRATION_RENDER,
INTEGRATION_FLYIO, INTEGRATION_FLYIO,
INTEGRATION_CIRCLECI,
INTEGRATION_SET, INTEGRATION_SET,
INTEGRATION_OAUTH2, INTEGRATION_OAUTH2,
INTEGRATION_AZURE_TOKEN_URL, INTEGRATION_AZURE_TOKEN_URL,
@@ -27,27 +28,22 @@ import {
INTEGRATION_NETLIFY_API_URL, INTEGRATION_NETLIFY_API_URL,
INTEGRATION_RENDER_API_URL, INTEGRATION_RENDER_API_URL,
INTEGRATION_FLYIO_API_URL, INTEGRATION_FLYIO_API_URL,
INTEGRATION_OPTIONS INTEGRATION_CIRCLECI_API_URL,
} from './integration'; INTEGRATION_OPTIONS,
import { } from "./integration";
OWNER, import { OWNER, ADMIN, MEMBER, INVITED, ACCEPTED } from "./organization";
ADMIN, import { SECRET_SHARED, SECRET_PERSONAL } from "./secret";
MEMBER, import { EVENT_PUSH_SECRETS, EVENT_PULL_SECRETS } from "./event";
INVITED,
ACCEPTED,
} from './organization';
import { SECRET_SHARED, SECRET_PERSONAL } from './secret';
import { EVENT_PUSH_SECRETS, EVENT_PULL_SECRETS } from './event';
import { import {
ACTION_LOGIN, ACTION_LOGIN,
ACTION_LOGOUT, ACTION_LOGOUT,
ACTION_ADD_SECRETS, ACTION_ADD_SECRETS,
ACTION_UPDATE_SECRETS, ACTION_UPDATE_SECRETS,
ACTION_DELETE_SECRETS, ACTION_DELETE_SECRETS,
ACTION_READ_SECRETS ACTION_READ_SECRETS,
} from './action'; } from "./action";
import { SMTP_HOST_SENDGRID, SMTP_HOST_MAILGUN } from './smtp'; import { SMTP_HOST_SENDGRID, SMTP_HOST_MAILGUN } from "./smtp";
import { PLAN_STARTER, PLAN_PRO } from './stripe'; import { PLAN_STARTER, PLAN_PRO } from "./stripe";
export { export {
OWNER, OWNER,
@@ -71,6 +67,7 @@ export {
INTEGRATION_GITHUB, INTEGRATION_GITHUB,
INTEGRATION_RENDER, INTEGRATION_RENDER,
INTEGRATION_FLYIO, INTEGRATION_FLYIO,
INTEGRATION_CIRCLECI,
INTEGRATION_SET, INTEGRATION_SET,
INTEGRATION_OAUTH2, INTEGRATION_OAUTH2,
INTEGRATION_AZURE_TOKEN_URL, INTEGRATION_AZURE_TOKEN_URL,
@@ -83,6 +80,7 @@ export {
INTEGRATION_NETLIFY_API_URL, INTEGRATION_NETLIFY_API_URL,
INTEGRATION_RENDER_API_URL, INTEGRATION_RENDER_API_URL,
INTEGRATION_FLYIO_API_URL, INTEGRATION_FLYIO_API_URL,
INTEGRATION_CIRCLECI_API_URL,
EVENT_PUSH_SECRETS, EVENT_PUSH_SECRETS,
EVENT_PULL_SECRETS, EVENT_PULL_SECRETS,
ACTION_LOGIN, ACTION_LOGIN,
+51 -46
View File
@@ -3,50 +3,53 @@ import {
TENANT_ID_AZURE TENANT_ID_AZURE
} from '../config'; } from '../config';
import { import {
CLIENT_ID_HEROKU, CLIENT_ID_HEROKU,
CLIENT_ID_NETLIFY, CLIENT_ID_NETLIFY,
CLIENT_ID_GITHUB, CLIENT_ID_GITHUB,
CLIENT_SLUG_VERCEL CLIENT_SLUG_VERCEL,
} from '../config'; } from "../config";
// integrations // integrations
const INTEGRATION_AZURE_KEY_VAULT = 'azure-key-vault'; const INTEGRATION_AZURE_KEY_VAULT = 'azure-key-vault';
const INTEGRATION_AWS_PARAMETER_STORE = 'aws-parameter-store'; const INTEGRATION_AWS_PARAMETER_STORE = 'aws-parameter-store';
const INTEGRATION_AWS_SECRET_MANAGER = 'aws-secret-manager'; const INTEGRATION_AWS_SECRET_MANAGER = 'aws-secret-manager';
const INTEGRATION_HEROKU = 'heroku'; const INTEGRATION_HEROKU = "heroku";
const INTEGRATION_VERCEL = 'vercel'; const INTEGRATION_VERCEL = "vercel";
const INTEGRATION_NETLIFY = 'netlify'; const INTEGRATION_NETLIFY = "netlify";
const INTEGRATION_GITHUB = 'github'; const INTEGRATION_GITHUB = "github";
const INTEGRATION_RENDER = 'render'; const INTEGRATION_RENDER = "render";
const INTEGRATION_FLYIO = 'flyio'; const INTEGRATION_FLYIO = "flyio";
const INTEGRATION_CIRCLECI = "circleci";
const INTEGRATION_SET = new Set([ const INTEGRATION_SET = new Set([
INTEGRATION_AZURE_KEY_VAULT, INTEGRATION_AZURE_KEY_VAULT,
INTEGRATION_HEROKU, INTEGRATION_HEROKU,
INTEGRATION_VERCEL, INTEGRATION_VERCEL,
INTEGRATION_NETLIFY, INTEGRATION_NETLIFY,
INTEGRATION_GITHUB, INTEGRATION_GITHUB,
INTEGRATION_RENDER, INTEGRATION_RENDER,
INTEGRATION_FLYIO INTEGRATION_FLYIO,
INTEGRATION_CIRCLECI,
]); ]);
// integration types // integration types
const INTEGRATION_OAUTH2 = 'oauth2'; const INTEGRATION_OAUTH2 = "oauth2";
// integration oauth endpoints // integration oauth endpoints
const INTEGRATION_AZURE_TOKEN_URL = `https://login.microsoftonline.com/${TENANT_ID_AZURE}/oauth2/v2.0/token`; 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_HEROKU_TOKEN_URL = 'https://id.heroku.com/oauth/token';
const INTEGRATION_VERCEL_TOKEN_URL = const INTEGRATION_VERCEL_TOKEN_URL =
'https://api.vercel.com/v2/oauth/access_token'; "https://api.vercel.com/v2/oauth/access_token";
const INTEGRATION_NETLIFY_TOKEN_URL = 'https://api.netlify.com/oauth/token'; const INTEGRATION_NETLIFY_TOKEN_URL = "https://api.netlify.com/oauth/token";
const INTEGRATION_GITHUB_TOKEN_URL = const INTEGRATION_GITHUB_TOKEN_URL =
'https://github.com/login/oauth/access_token'; "https://github.com/login/oauth/access_token";
// integration apps endpoints // integration apps endpoints
const INTEGRATION_HEROKU_API_URL = 'https://api.heroku.com'; const INTEGRATION_HEROKU_API_URL = "https://api.heroku.com";
const INTEGRATION_VERCEL_API_URL = 'https://api.vercel.com'; const INTEGRATION_VERCEL_API_URL = "https://api.vercel.com";
const INTEGRATION_NETLIFY_API_URL = 'https://api.netlify.com'; const INTEGRATION_NETLIFY_API_URL = "https://api.netlify.com";
const INTEGRATION_RENDER_API_URL = 'https://api.render.com'; const INTEGRATION_RENDER_API_URL = "https://api.render.com";
const INTEGRATION_FLYIO_API_URL = 'https://api.fly.io/graphql'; const INTEGRATION_FLYIO_API_URL = "https://api.fly.io/graphql";
const INTEGRATION_CIRCLECI_API_URL = "https://circleci.com/api";
const INTEGRATION_OPTIONS = [ const INTEGRATION_OPTIONS = [
{ {
@@ -154,8 +157,8 @@ const INTEGRATION_OPTIONS = [
name: 'Circle CI', name: 'Circle CI',
slug: 'circleci', slug: 'circleci',
image: 'Circle CI.png', image: 'Circle CI.png',
isAvailable: false, isAvailable: true,
type: '', type: 'pat',
clientId: '', clientId: '',
docsLink: '' docsLink: ''
} }
@@ -165,23 +168,25 @@ export {
INTEGRATION_AZURE_KEY_VAULT, INTEGRATION_AZURE_KEY_VAULT,
INTEGRATION_AWS_PARAMETER_STORE, INTEGRATION_AWS_PARAMETER_STORE,
INTEGRATION_AWS_SECRET_MANAGER, INTEGRATION_AWS_SECRET_MANAGER,
INTEGRATION_HEROKU, INTEGRATION_HEROKU,
INTEGRATION_VERCEL, INTEGRATION_VERCEL,
INTEGRATION_NETLIFY, INTEGRATION_NETLIFY,
INTEGRATION_GITHUB, INTEGRATION_GITHUB,
INTEGRATION_RENDER, INTEGRATION_RENDER,
INTEGRATION_FLYIO, INTEGRATION_FLYIO,
INTEGRATION_SET, INTEGRATION_CIRCLECI,
INTEGRATION_OAUTH2, INTEGRATION_SET,
INTEGRATION_OAUTH2,
INTEGRATION_AZURE_TOKEN_URL, INTEGRATION_AZURE_TOKEN_URL,
INTEGRATION_HEROKU_TOKEN_URL, INTEGRATION_HEROKU_TOKEN_URL,
INTEGRATION_VERCEL_TOKEN_URL, INTEGRATION_VERCEL_TOKEN_URL,
INTEGRATION_NETLIFY_TOKEN_URL, INTEGRATION_NETLIFY_TOKEN_URL,
INTEGRATION_GITHUB_TOKEN_URL, INTEGRATION_GITHUB_TOKEN_URL,
INTEGRATION_HEROKU_API_URL, INTEGRATION_HEROKU_API_URL,
INTEGRATION_VERCEL_API_URL, INTEGRATION_VERCEL_API_URL,
INTEGRATION_NETLIFY_API_URL, INTEGRATION_NETLIFY_API_URL,
INTEGRATION_RENDER_API_URL, INTEGRATION_RENDER_API_URL,
INTEGRATION_FLYIO_API_URL, INTEGRATION_FLYIO_API_URL,
INTEGRATION_OPTIONS INTEGRATION_CIRCLECI_API_URL,
INTEGRATION_OPTIONS,
}; };
+8 -16
View File
@@ -1,24 +1,16 @@
// membership roles // membership roles
const OWNER = 'owner'; const OWNER = "owner";
const ADMIN = 'admin'; const ADMIN = "admin";
const MEMBER = 'member'; const MEMBER = "member";
// membership statuses // membership statuses
const INVITED = 'invited'; const INVITED = "invited";
// membership permissions ability // membership permissions ability
const ABILITY_READ = 'read'; const ABILITY_READ = "read";
const ABILITY_WRITE = 'write'; const ABILITY_WRITE = "write";
// -- organization // -- organization
const ACCEPTED = 'accepted'; const ACCEPTED = "accepted";
export { export { OWNER, ADMIN, MEMBER, INVITED, ACCEPTED, ABILITY_READ, ABILITY_WRITE };
OWNER,
ADMIN,
MEMBER,
INVITED,
ACCEPTED,
ABILITY_READ,
ABILITY_WRITE
}
+2 -1
View File
@@ -11,7 +11,8 @@ const integrationSlugNameMapping: Mapping = {
'netlify': 'Netlify', 'netlify': 'Netlify',
'github': 'GitHub', 'github': 'GitHub',
'render': 'Render', 'render': 'Render',
'flyio': 'Fly.io' 'flyio': 'Fly.io',
"circleci": 'CircleCI'
} }
const envMapping: Mapping = { const envMapping: Mapping = {
@@ -44,9 +44,9 @@ const CloudIntegration = ({
tabIndex={0} tabIndex={0}
className={`relative ${ className={`relative ${
cloudIntegrationOption.isAvailable cloudIntegrationOption.isAvailable
? 'hover:bg-white/10 duration-200 cursor-pointer' ? 'cursor-pointer duration-200 hover:bg-white/10'
: 'opacity-50' : '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={() => { onClick={() => {
if (!cloudIntegrationOption.isAvailable) return; if (!cloudIntegrationOption.isAvailable) return;
setSelectedIntegrationOption(cloudIntegrationOption); setSelectedIntegrationOption(cloudIntegrationOption);
@@ -61,22 +61,22 @@ const CloudIntegration = ({
alt="integration logo" alt="integration logo"
/> />
{cloudIntegrationOption.name.split(' ').length > 2 ? ( {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>{cloudIntegrationOption.name.split(' ')[0]}</div>
<div className="text-base"> <div className="text-base">
{cloudIntegrationOption.name.split(' ')[1]} {cloudIntegrationOption.name.split(' ')[2]} {cloudIntegrationOption.name.split(' ')[1]} {cloudIntegrationOption.name.split(' ')[2]}
</div> </div>
</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} {cloudIntegrationOption.name}
</div> </div>
)} )}
{cloudIntegrationOption.isAvailable && {cloudIntegrationOption.isAvailable &&
integrationAuths integrationAuths
.map((authorization) => authorization.integration) .map((authorization) => authorization?.integration)
.includes(cloudIntegrationOption.slug) && ( .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 <div
onKeyDown={() => null} onKeyDown={() => null}
role="button" role="button"
@@ -86,8 +86,7 @@ const CloudIntegration = ({
const deletedIntegrationAuth = await deleteIntegrationAuth({ const deletedIntegrationAuth = await deleteIntegrationAuth({
integrationAuthId: integrationAuths integrationAuthId: integrationAuths
.filter( .filter(
(authorization) => (authorization) => authorization.integration === cloudIntegrationOption.slug
authorization.integration === cloudIntegrationOption.slug
) )
.map((authorization) => authorization._id)[0] .map((authorization) => authorization._id)[0]
}); });
@@ -96,20 +95,20 @@ const CloudIntegration = ({
integrationAuth: deletedIntegrationAuth 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 Revoke
</div> </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"> <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="text-xs mr-2" /> <FontAwesomeIcon icon={faCheck} className="mr-2 text-xs" />
Authorized Authorized
</div> </div>
</div> </div>
)} )}
{!cloudIntegrationOption.isAvailable && ( {!cloudIntegrationOption.isAvailable && (
<div className="absolute group z-50 top-0 right-0 flex flex-row"> <div className="group absolute top-0 right-0 z-50 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="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 Coming Soon
</div> </div>
</div> </div>
@@ -57,7 +57,7 @@ const IntegrationTile = ({
// set initial environment. This find will only execute when component is mounting // set initial environment. This find will only execute when component is mounting
const [integrationEnvironment, setIntegrationEnvironment] = useState<Props['environments'][0]>( const [integrationEnvironment, setIntegrationEnvironment] = useState<Props['environments'][0]>(
environments.find(({ slug }) => slug === integration.environment) || { environments.find(({ slug }) => slug === integration?.environment) || {
name: '', name: '',
slug: '' slug: ''
} }
@@ -69,9 +69,8 @@ const IntegrationTile = ({
useEffect(() => { useEffect(() => {
const loadIntegration = async () => { const loadIntegration = async () => {
const tempApps: [IntegrationApp] = await getIntegrationApps({ const tempApps: [IntegrationApp] = await getIntegrationApps({
integrationAuthId: integration.integrationAuth integrationAuthId: integration?.integrationAuth
}); });
setApps(tempApps); setApps(tempApps);
@@ -90,15 +89,16 @@ const IntegrationTile = ({
case 'vercel': case 'vercel':
setIntegrationTargetEnvironment( setIntegrationTargetEnvironment(
integration?.targetEnvironment integration?.targetEnvironment
? integration.targetEnvironment.charAt(0).toUpperCase() + integration.targetEnvironment.substring(1) ? integration.targetEnvironment.charAt(0).toUpperCase() +
: 'Development' integration.targetEnvironment.substring(1)
: 'Development'
); );
break; break;
case 'netlify': case 'netlify':
setIntegrationTargetEnvironment( setIntegrationTargetEnvironment(
integration?.targetEnvironment integration?.targetEnvironment
? contextNetlifyMapping[integration.targetEnvironment] ? contextNetlifyMapping[integration.targetEnvironment]
: 'Local development' : 'Local development'
); );
break; break;
default: default:
@@ -119,7 +119,7 @@ const IntegrationTile = ({
default: default:
return null; return null;
} }
} };
try { try {
const siteApp = apps.find((app) => app.name === integrationApp); // obj or undefined const siteApp = apps.find((app) => app.name === integrationApp); // obj or undefined
@@ -138,12 +138,12 @@ const IntegrationTile = ({
}); });
setIntegrations( setIntegrations(
integrations.map((i) => i._id === updatedIntegration._id ? updatedIntegration : i) integrations.map((i) => (i._id === updatedIntegration._id ? updatedIntegration : i))
); );
} catch (err) { } catch (err) {
console.error(err); console.error(err);
} }
} };
// eslint-disable-next-line @typescript-eslint/no-shadow // eslint-disable-next-line @typescript-eslint/no-shadow
const renderIntegrationSpecificParams = (integration: Integration) => { const renderIntegrationSpecificParams = (integration: Integration) => {
@@ -152,7 +152,7 @@ const IntegrationTile = ({
case 'vercel': case 'vercel':
return ( return (
<div> <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 <ListBox
data={!integration.isActive ? ['Development', 'Preview', 'Production'] : null} data={!integration.isActive ? ['Development', 'Preview', 'Production'] : null}
isSelected={integrationTargetEnvironment} isSelected={integrationTargetEnvironment}
@@ -164,7 +164,7 @@ const IntegrationTile = ({
case 'netlify': case 'netlify':
return ( return (
<div> <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 <ListBox
data={ data={
!integration.isActive !integration.isActive
@@ -189,10 +189,10 @@ const IntegrationTile = ({
if (!integrationApp) return <div />; if (!integrationApp) return <div />;
return ( 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 className="flex">
<div> <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 <ListBox
data={!integration.isActive ? environments.map(({ name }) => name) : null} data={!integration.isActive ? environments.map(({ name }) => name) : null}
isSelected={integrationEnvironment.name} isSelected={integrationEnvironment.name}
@@ -208,7 +208,7 @@ const IntegrationTile = ({
/> />
</div> </div>
<div className="pt-2"> <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>
<div className="mr-2"> <div className="mr-2">
<p className="text-gray-400 text-xs font-semibold mb-2">INTEGRATION</p> <p className="text-gray-400 text-xs font-semibold mb-2">INTEGRATION</p>
@@ -218,7 +218,7 @@ const IntegrationTile = ({
</div> </div>
</div> </div>
<div className="mr-2"> <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 <ListBox
data={!integration.isActive ? apps.map((app) => app.name) : null} data={!integration.isActive ? apps.map((app) => app.name) : null}
isSelected={integrationApp} isSelected={integrationApp}
@@ -231,9 +231,9 @@ const IntegrationTile = ({
</div> </div>
<div className="flex items-end"> <div className="flex items-end">
{integration.isActive ? ( {integration.isActive ? (
<div className="max-w-5xl flex flex-row items-center bg-white/5 p-2 rounded-md px-4"> <div className="flex max-w-5xl flex-row items-center rounded-md bg-white/5 p-2 px-4">
<FontAwesomeIcon icon={faRotate} className="text-lg mr-2.5 text-primary animate-spin" /> <FontAwesomeIcon icon={faRotate} className="mr-2.5 animate-spin text-lg text-primary" />
<div className="text-gray-300 font-semibold">In Sync</div> <div className="font-semibold text-gray-300">In Sync</div>
</div> </div>
) : ( ) : (
<Button <Button
@@ -243,11 +243,13 @@ const IntegrationTile = ({
size="md" 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 <Button
onButtonPressed={() => handleDeleteIntegration({ onButtonPressed={() =>
integration handleDeleteIntegration({
})} integration
})
}
color="red" color="red"
size="icon-md" size="icon-md"
icon={faX} icon={faX}
@@ -35,16 +35,14 @@ const ProjectIntegrationSection = ({
}: Props) => { }: Props) => {
return integrations.length > 0 ? ( return integrations.length > 0 ? (
<div className="mb-12"> <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"> <div className="mx-4 mb-4 mt-6 flex max-w-5xl flex-col items-start justify-between px-2 text-xl">
<h1 className="font-semibold text-3xl">Current Integrations</h1> <h1 className="text-3xl font-semibold">Current Integrations</h1>
<p className="text-base text-gray-400"> <p className="text-base text-gray-400">Manage integrations with third-party services.</p>
Manage integrations with third-party services.
</p>
</div> </div>
{integrations.map((integration: Integration) => { {integrations.map((integration: Integration) => {
return ( return (
<IntegrationTile <IntegrationTile
key={`integration-${integration._id.toString()}`} key={`integration-${integration?._id.toString()}`}
integration={integration} integration={integration}
integrations={integrations} integrations={integrations}
bot={bot} bot={bot}
@@ -59,6 +57,6 @@ const ProjectIntegrationSection = ({
) : ( ) : (
<div /> <div />
); );
} };
export default ProjectIntegrationSection; export default ProjectIntegrationSection;
+6
View File
@@ -198,6 +198,9 @@ export default function Integrations() {
case 'flyio': case 'flyio':
link = `${window.location.origin}/integrations/flyio/authorize` link = `${window.location.origin}/integrations/flyio/authorize`
break; break;
case 'circleci':
link = `${window.location.origin}/integrations/circleci/authorize`
break;
default: default:
break; break;
} }
@@ -241,6 +244,9 @@ export default function Integrations() {
case 'flyio': case 'flyio':
link = `${window.location.origin}/integrations/flyio/create?integrationAuthId=${integrationAuth._id}`; link = `${window.location.origin}/integrations/flyio/create?integrationAuthId=${integrationAuth._id}`;
break; break;
case 'circleci':
link = `${window.location.origin}/integrations/circleci/create?integrationAuthId=${integrationAuth._id}`;
break;
default: default:
break; 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']);