Fix merge conflicts

This commit is contained in:
Tuan Dang
2023-02-14 17:40:42 +07:00
39 changed files with 1681 additions and 1253 deletions
+1 -2
View File
File diff suppressed because one or more lines are too long
@@ -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,
res: Response
) => {
return res.status(200).send({ 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] * 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 { 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,
}); });
}; };
@@ -187,17 +184,17 @@ export const deleteIntegrationAuth = async (req: Request, res: Response) => {
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,
}); });
} };
@@ -66,9 +66,9 @@ 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]
@@ -94,7 +94,7 @@ export const updateIntegration = async (req: Request, res: Response) => {
integration = await Integration.findOneAndUpdate( integration = await Integration.findOneAndUpdate(
{ {
_id: req.integration._id _id: req.integration._id,
}, },
{ {
environment, environment,
@@ -102,10 +102,10 @@ export const updateIntegration = async (req: Request, res: Response) => {
app, app,
appId, appId,
targetEnvironment, targetEnvironment,
owner owner,
}, },
{ {
new: true new: true,
} }
); );
@@ -113,20 +113,20 @@ export const updateIntegration = async (req: Request, res: Response) => {
// 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,
}); });
}; };
@@ -143,19 +143,19 @@ export const deleteIntegration = async (req: Request, res: Response) => {
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,
}); });
}; };
@@ -1,5 +1,5 @@
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,
@@ -8,14 +8,14 @@ import {
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]
@@ -30,25 +30,24 @@ export const getWorkspacePublicKeys = async (req: Request, res: Response) => {
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,
}); });
}; };
@@ -64,18 +63,18 @@ export const getWorkspaceMemberships = async (req: Request, res: Response) => {
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,
}); });
}; };
@@ -90,19 +89,19 @@ export const getWorkspaces = async (req: Request, res: Response) => {
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,
}); });
}; };
@@ -118,18 +117,18 @@ export const getWorkspace = async (req: Request, res: Response) => {
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,
}); });
}; };
@@ -148,38 +147,38 @@ export const createWorkspace = async (req: Request, res: Response) => {
// 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,
}); });
}; };
@@ -195,18 +194,18 @@ export const deleteWorkspace = async (req: Request, res: Response) => {
// 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",
}); });
}; };
@@ -224,26 +223,26 @@ export const changeWorkspaceName = async (req: Request, res: Response) => {
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,
}); });
}; };
@@ -259,18 +258,18 @@ export const getWorkspaceIntegrations = async (req: Request, res: Response) => {
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,
}); });
}; };
@@ -289,18 +288,18 @@ export const getWorkspaceIntegrationAuthorizations = async (
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,
}); });
}; };
@@ -320,17 +319,17 @@ export const getWorkspaceServiceTokens = async (
// ?? 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,
}); });
} };
@@ -18,6 +18,8 @@ import { postHogClient } from '../../services';
import { getChannelFromUserAgent } from '../../utils/posthog'; import { getChannelFromUserAgent } from '../../utils/posthog';
import { ABILITY_READ, ABILITY_WRITE } from '../../variables/organization'; import { ABILITY_READ, ABILITY_WRITE } from '../../variables/organization';
import { userHasNoAbility, userHasWorkspaceAccess, userHasWriteOnlyAbility } from '../../ee/helpers/checkMembershipPermissions'; import { userHasNoAbility, userHasWorkspaceAccess, userHasWriteOnlyAbility } from '../../ee/helpers/checkMembershipPermissions';
import Tag from '../../models/tag';
import _ from 'lodash';
/** /**
* Create secret(s) for workspace with id [workspaceId] and environment [environment] * Create secret(s) for workspace with id [workspaceId] and environment [environment]
@@ -284,8 +286,10 @@ export const getSecrets = async (req: Request, res: Response) => {
} }
} }
*/ */
const { workspaceId, environment } = req.query;
const { workspaceId, environment, tagSlugs } = req.query;
const tagNamesList = typeof tagSlugs === 'string' && tagSlugs !== '' ? tagSlugs.split(',') : [];
let userId = "" // used for getting personal secrets for user let userId = "" // used for getting personal secrets for user
let userEmail = "" // used for posthog let userEmail = "" // used for posthog
if (req.user) { if (req.user) {
@@ -308,22 +312,28 @@ export const getSecrets = async (req: Request, res: Response) => {
} }
} }
let secrets: any let secrets: any
if (hasWriteOnlyAccess) { let secretQuery: any
secrets = await Secret.find(
{ if (tagNamesList != undefined && tagNamesList.length != 0) {
const workspaceFromDB = await Tag.find({ workspace: workspaceId })
const tagIds = _.map(tagNamesList, (tagName) => {
const tag = _.find(workspaceFromDB, { slug: tagName });
return tag ? tag.id : null;
});
secretQuery = {
workspace: workspaceId, workspace: workspaceId,
environment, environment,
$or: [ $or: [
{ user: userId }, { user: userId },
{ user: { $exists: false } } { user: { $exists: false } }
], ],
tags: { $in: tagIds },
type: { $in: [SECRET_SHARED, SECRET_PERSONAL] } type: { $in: [SECRET_SHARED, SECRET_PERSONAL] }
} }
)
.select("secretKeyCiphertext secretKeyIV secretKeyTag")
} else { } else {
secrets = await Secret.find( secretQuery = {
{
workspace: workspaceId, workspace: workspaceId,
environment, environment,
$or: [ $or: [
@@ -332,7 +342,12 @@ export const getSecrets = async (req: Request, res: Response) => {
], ],
type: { $in: [SECRET_SHARED, SECRET_PERSONAL] } type: { $in: [SECRET_SHARED, SECRET_PERSONAL] }
} }
).populate("tags") }
if (hasWriteOnlyAccess) {
secrets = await Secret.find(secretQuery).select("secretKeyCiphertext secretKeyIV secretKeyTag")
} else {
secrets = await Secret.find(secretQuery).populate("tags")
} }
const channel = getChannelFromUserAgent(req.headers['user-agent']) const channel = getChannelFromUserAgent(req.headers['user-agent'])
+105 -74
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: { params: {
teamId: integrationAuth.teamId 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,70 @@ const getAppsFlyio = async ({
} }
`; `;
const res = (await axios({ const res = (
await axios({
url: INTEGRATION_FLYIO_API_URL, url: INTEGRATION_FLYIO_API_URL,
method: 'post', method: "post",
headers: { headers: {
'Authorization': 'Bearer ' + accessToken, 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;
} };
/**
* Return list of projects for CircleCI integration
* @param {Object} obj
* @param {String} obj.accessToken - access token for CircleCI API
* @returns {Object[]} apps -
* @returns {String} apps.name - name of CircleCI apps
*/
const getAppsCircleCI = async ({ accessToken }: { accessToken: string }) => {
let apps: any;
try {
const 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) {
Sentry.setUser(null);
Sentry.captureException(err);
throw new Error("Failed to get CircleCI projects");
}
return apps;
};
export { getApps }; export { getApps };
+247 -140
View File
@@ -1,5 +1,5 @@
import axios from 'axios'; import axios from "axios";
import * as Sentry from '@sentry/node'; import * as Sentry from "@sentry/node";
import _ from 'lodash'; import _ from 'lodash';
import AWS from 'aws-sdk'; import AWS from 'aws-sdk';
import { import {
@@ -9,9 +9,9 @@ import {
GetSecretValueCommand, GetSecretValueCommand,
ResourceNotFoundException ResourceNotFoundException
} from '@aws-sdk/client-secrets-manager'; } from '@aws-sdk/client-secrets-manager';
import { Octokit } from '@octokit/rest'; import { Octokit } from "@octokit/rest";
import sodium from 'libsodium-wrappers'; import sodium from "libsodium-wrappers";
import { IIntegration, IIntegrationAuth } from '../models'; import { IIntegration, IIntegrationAuth } from "../models";
import { import {
INTEGRATION_AZURE_KEY_VAULT, INTEGRATION_AZURE_KEY_VAULT,
INTEGRATION_AWS_PARAMETER_STORE, INTEGRATION_AWS_PARAMETER_STORE,
@@ -22,12 +22,15 @@ 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";
import { access, appendFile } from "fs";
/** /**
* Sync/push [secrets] to [app] in integration named [integration] * Sync/push [secrets] to [app] in integration named [integration]
@@ -43,7 +46,7 @@ const syncSecrets = async ({
integrationAuth, integrationAuth,
secrets, secrets,
accessId, accessId,
accessToken accessToken,
}: { }: {
integration: IIntegration; integration: IIntegration;
integrationAuth: IIntegrationAuth; integrationAuth: IIntegrationAuth;
@@ -80,7 +83,7 @@ const syncSecrets = async ({
await syncSecretsHeroku({ await syncSecretsHeroku({
integration, integration,
secrets, secrets,
accessToken accessToken,
}); });
break; break;
case INTEGRATION_VERCEL: case INTEGRATION_VERCEL:
@@ -88,7 +91,7 @@ const syncSecrets = async ({
integration, integration,
integrationAuth, integrationAuth,
secrets, secrets,
accessToken accessToken,
}); });
break; break;
case INTEGRATION_NETLIFY: case INTEGRATION_NETLIFY:
@@ -96,30 +99,36 @@ const syncSecrets = async ({
integration, integration,
integrationAuth, integrationAuth,
secrets, secrets,
accessToken accessToken,
}); });
break; break;
case INTEGRATION_GITHUB: case INTEGRATION_GITHUB:
await syncSecretsGitHub({ await syncSecretsGitHub({
integration, integration,
secrets, secrets,
accessToken accessToken,
}); });
break; break;
case INTEGRATION_RENDER: case INTEGRATION_RENDER:
await syncSecretsRender({ await syncSecretsRender({
integration, integration,
secrets, secrets,
accessToken accessToken,
}); });
break; break;
case INTEGRATION_FLYIO: case INTEGRATION_FLYIO:
await syncSecretsFlyio({ await syncSecretsFlyio({
integration, integration,
secrets, secrets,
accessToken accessToken,
}); });
break; break;
case INTEGRATION_CIRCLECI:
await syncSecretsCircleCI({
integration,
secrets,
accessToken,
});
} }
} catch (err) { } catch (err) {
Sentry.setUser(null); Sentry.setUser(null);
@@ -465,7 +474,7 @@ const syncSecretsAWSSecretManager = async ({
const syncSecretsHeroku = async ({ const syncSecretsHeroku = async ({
integration, integration,
secrets, secrets,
accessToken accessToken,
}: { }: {
integration: IIntegration; integration: IIntegration;
secrets: any; secrets: any;
@@ -477,9 +486,9 @@ const syncSecretsHeroku = async ({
`${INTEGRATION_HEROKU_API_URL}/apps/${integration.app}/config-vars`, `${INTEGRATION_HEROKU_API_URL}/apps/${integration.app}/config-vars`,
{ {
headers: { headers: {
Accept: 'application/vnd.heroku+json; version=3', Accept: "application/vnd.heroku+json; version=3",
Authorization: `Bearer ${accessToken}` Authorization: `Bearer ${accessToken}`,
} },
} }
) )
).data; ).data;
@@ -495,15 +504,15 @@ const syncSecretsHeroku = async ({
secrets, secrets,
{ {
headers: { headers: {
Accept: 'application/vnd.heroku+json; version=3', Accept: "application/vnd.heroku+json; version=3",
Authorization: `Bearer ${accessToken}` Authorization: `Bearer ${accessToken}`,
} },
} }
); );
} catch (err) { } catch (err) {
Sentry.setUser(null); Sentry.setUser(null);
Sentry.captureException(err); 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, integration,
integrationAuth, integrationAuth,
secrets, secrets,
accessToken accessToken,
}: { }: {
integration: IIntegration, integration: IIntegration;
integrationAuth: IIntegrationAuth, integrationAuth: IIntegrationAuth;
secrets: any; secrets: any;
accessToken: string; accessToken: string;
}) => { }) => {
@@ -536,13 +545,18 @@ const syncSecretsVercel = async ({
// Get all (decrypted) secrets back from Vercel in // Get all (decrypted) secrets back from Vercel in
// decrypted format // decrypted format
const params: { [key: string]: string } = { const params: { [key: string]: string } = {
decrypt: 'true', decrypt: "true",
...( integrationAuth?.teamId ? { ...(integrationAuth?.teamId
teamId: 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`, `${INTEGRATION_VERCEL_API_URL}/v9/projects/${integration.app}/env`,
{ {
params, params,
@@ -579,8 +593,8 @@ const syncSecretsVercel = async ({
newSecrets.push({ newSecrets.push({
key: key, key: key,
value: secrets[key], value: secrets[key],
type: 'encrypted', type: "encrypted",
target: [integration.targetEnvironment] target: [integration.targetEnvironment],
}); });
} }
}); });
@@ -594,8 +608,8 @@ const syncSecretsVercel = async ({
id: res[key].id, id: res[key].id,
key: key, key: key,
value: secrets[key], value: secrets[key],
type: 'encrypted', type: "encrypted",
target: [integration.targetEnvironment] target: [integration.targetEnvironment],
}); });
} }
} else { } else {
@@ -604,7 +618,7 @@ const syncSecretsVercel = async ({
id: res[key].id, id: res[key].id,
key: key, key: key,
value: res[key].value, value: res[key].value,
type: 'encrypted', type: "encrypted",
target: [integration.targetEnvironment], target: [integration.targetEnvironment],
}); });
} }
@@ -618,8 +632,8 @@ const syncSecretsVercel = async ({
{ {
params, params,
headers: { headers: {
Authorization: `Bearer ${accessToken}` Authorization: `Bearer ${accessToken}`,
} },
} }
); );
} }
@@ -627,18 +641,15 @@ const syncSecretsVercel = async ({
// Sync/push updated secrets // Sync/push updated secrets
if (updateSecrets.length > 0) { if (updateSecrets.length > 0) {
updateSecrets.forEach(async (secret: VercelSecret) => { updateSecrets.forEach(async (secret: VercelSecret) => {
const { const { id, ...updatedSecret } = secret;
id,
...updatedSecret
} = secret;
await axios.patch( await axios.patch(
`${INTEGRATION_VERCEL_API_URL}/v9/projects/${integration.app}/env/${secret.id}`, `${INTEGRATION_VERCEL_API_URL}/v9/projects/${integration.app}/env/${secret.id}`,
updatedSecret, updatedSecret,
{ {
params, params,
headers: { headers: {
Authorization: `Bearer ${accessToken}` Authorization: `Bearer ${accessToken}`,
} },
} }
); );
}); });
@@ -652,8 +663,8 @@ const syncSecretsVercel = async ({
{ {
params, params,
headers: { headers: {
Authorization: `Bearer ${accessToken}` Authorization: `Bearer ${accessToken}`,
} },
} }
); );
}); });
@@ -661,9 +672,9 @@ const syncSecretsVercel = async ({
} catch (err) { } catch (err) {
Sentry.setUser(null); Sentry.setUser(null);
Sentry.captureException(err); 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] * Sync/push [secrets] to Netlify site with id [integration.appId]
@@ -677,7 +688,7 @@ const syncSecretsNetlify = async ({
integration, integration,
integrationAuth, integrationAuth,
secrets, secrets,
accessToken accessToken,
}: { }: {
integration: IIntegration; integration: IIntegration;
integrationAuth: IIntegrationAuth; integrationAuth: IIntegrationAuth;
@@ -685,7 +696,6 @@ const syncSecretsNetlify = async ({
accessToken: string; accessToken: string;
}) => { }) => {
try { try {
interface NetlifyValue { interface NetlifyValue {
id?: string; id?: string;
context: string; // 'dev' | 'branch-deploy' | 'deploy-preview' | 'production', context: string; // 'dev' | 'branch-deploy' | 'deploy-preview' | 'production',
@@ -702,24 +712,27 @@ const syncSecretsNetlify = async ({
} }
const getParams = new URLSearchParams({ const getParams = new URLSearchParams({
context_name: 'all', // integration.context or all context_name: "all", // integration.context or all
site_id: integration.appId site_id: integration.appId,
}); });
const res = (await axios.get( const res = (
await axios.get(
`${INTEGRATION_NETLIFY_API_URL}/api/v1/accounts/${integrationAuth.accountId}/env`, `${INTEGRATION_NETLIFY_API_URL}/api/v1/accounts/${integrationAuth.accountId}/env`,
{ {
params: getParams, params: getParams,
headers: { headers: {
Authorization: `Bearer ${accessToken}` Authorization: `Bearer ${accessToken}`,
},
} }
} )
)) ).data.reduce(
.data (obj: any, secret: any) => ({
.reduce((obj: any, secret: any) => ({
...obj, ...obj,
[secret.key]: secret [secret.key]: secret,
}), {}); }),
{}
);
const newSecrets: NetlifySecret[] = []; // createEnvVars const newSecrets: NetlifySecret[] = []; // createEnvVars
const deleteSecrets: string[] = []; // deleteEnvVar const deleteSecrets: string[] = []; // deleteEnvVar
@@ -732,18 +745,22 @@ const syncSecretsNetlify = async ({
// case: Infisical secret does not exist in Netlify -> create secret // case: Infisical secret does not exist in Netlify -> create secret
newSecrets.push({ newSecrets.push({
key, key,
values: [{ values: [
{
value: secrets[key], value: secrets[key],
context: integration.targetEnvironment context: integration.targetEnvironment,
}] },
],
}); });
} else { } else {
// case: Infisical secret exists in Netlify // case: Infisical secret exists in Netlify
const contexts = res[key].values const contexts = res[key].values.reduce(
.reduce((obj: any, value: NetlifyValue) => ({ (obj: any, value: NetlifyValue) => ({
...obj, ...obj,
[value.context]: value [value.context]: value,
}), {}); }),
{}
);
if (integration.targetEnvironment in contexts) { if (integration.targetEnvironment in contexts) {
// case: Netlify secret value exists in integration context // case: Netlify secret value exists in integration context
@@ -752,10 +769,12 @@ const syncSecretsNetlify = async ({
// -> update Netlify secret context and value // -> update Netlify secret context and value
updateSecrets.push({ updateSecrets.push({
key, key,
values: [{ values: [
{
context: integration.targetEnvironment, context: integration.targetEnvironment,
value: secrets[key] value: secrets[key],
}] },
],
}); });
} }
} else { } else {
@@ -763,14 +782,16 @@ const syncSecretsNetlify = async ({
// -> add the new Netlify secret context and value // -> add the new Netlify secret context and value
updateSecrets.push({ updateSecrets.push({
key, key,
values: [{ values: [
{
context: integration.targetEnvironment, context: integration.targetEnvironment,
value: secrets[key] value: secrets[key],
}] },
],
}); });
} }
} }
}) });
// identify secrets to delete // identify secrets to delete
// TODO: revise (patch case where 1 context was deleted but others still there // 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 // case: Netlify secret value has more than 1 context -> delete secret value context
deleteSecretValues.push({ deleteSecretValues.push({
key, key,
values: [{ values: [
{
id: value.id, id: value.id,
context: integration.targetEnvironment, context: integration.targetEnvironment,
value: value.value value: value.value,
}] },
],
}); });
} }
} }
@@ -803,7 +826,7 @@ const syncSecretsNetlify = async ({
}); });
const syncParams = new URLSearchParams({ const syncParams = new URLSearchParams({
site_id: integration.appId site_id: integration.appId,
}); });
if (newSecrets.length > 0) { if (newSecrets.length > 0) {
@@ -813,8 +836,8 @@ const syncSecretsNetlify = async ({
{ {
params: syncParams, params: syncParams,
headers: { 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}`, `${INTEGRATION_NETLIFY_API_URL}/api/v1/accounts/${integrationAuth.accountId}/env/${secret.key}`,
{ {
context: secret.values[0].context, context: secret.values[0].context,
value: secret.values[0].value value: secret.values[0].value,
}, },
{ {
params: syncParams, params: syncParams,
headers: { headers: {
Authorization: `Bearer ${accessToken}` Authorization: `Bearer ${accessToken}`,
} },
} }
); );
}); });
@@ -844,8 +867,8 @@ const syncSecretsNetlify = async ({
{ {
params: syncParams, params: syncParams,
headers: { headers: {
Authorization: `Bearer ${accessToken}` Authorization: `Bearer ${accessToken}`,
} },
} }
); );
}); });
@@ -858,8 +881,8 @@ const syncSecretsNetlify = async ({
{ {
params: syncParams, params: syncParams,
headers: { headers: {
Authorization: `Bearer ${accessToken}` Authorization: `Bearer ${accessToken}`,
} },
} }
); );
}); });
@@ -867,9 +890,9 @@ const syncSecretsNetlify = async ({
} catch (err) { } catch (err) {
Sentry.setUser(null); Sentry.setUser(null);
Sentry.captureException(err); 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] * Sync/push [secrets] to GitHub repo with name [integration.app]
@@ -882,14 +905,13 @@ const syncSecretsNetlify = async ({
const syncSecretsGitHub = async ({ const syncSecretsGitHub = async ({
integration, integration,
secrets, secrets,
accessToken accessToken,
}: { }: {
integration: IIntegration; integration: IIntegration;
secrets: any; secrets: any;
accessToken: string; accessToken: string;
}) => { }) => {
try { try {
interface GitHubRepoKey { interface GitHubRepoKey {
key_id: string; key_id: string;
key: string; key: string;
@@ -908,41 +930,42 @@ const syncSecretsGitHub = async ({
const deleteSecrets: GitHubSecret[] = []; const deleteSecrets: GitHubSecret[] = [];
const octokit = new Octokit({ const octokit = new Octokit({
auth: accessToken auth: accessToken,
}); });
// const user = (await octokit.request('GET /user', {})).data; // const user = (await octokit.request('GET /user', {})).data;
const repoPublicKey: GitHubRepoKey = (await octokit.request( const repoPublicKey: GitHubRepoKey = (
'GET /repos/{owner}/{repo}/actions/secrets/public-key', await octokit.request(
"GET /repos/{owner}/{repo}/actions/secrets/public-key",
{ {
owner: integration.owner, 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 // 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( const encryptedSecrets: GitHubSecretRes = (
'GET /repos/{owner}/{repo}/actions/secrets', await octokit.request("GET /repos/{owner}/{repo}/actions/secrets", {
{
owner: integration.owner, owner: integration.owner,
repo: integration.app repo: integration.app,
} })
)) ).data.secrets.reduce(
.data (obj: any, secret: any) => ({
.secrets
.reduce((obj: any, secret: any) => ({
...obj, ...obj,
[secret.name]: secret [secret.name]: secret,
}), {}); }),
{}
);
Object.keys(encryptedSecrets).map(async (key) => { Object.keys(encryptedSecrets).map(async (key) => {
if (!(key in secrets)) { if (!(key in secrets)) {
await octokit.request( await octokit.request(
'DELETE /repos/{owner}/{repo}/actions/secrets/{secret_name}', "DELETE /repos/{owner}/{repo}/actions/secrets/{secret_name}",
{ {
owner: integration.owner, owner: integration.owner,
repo: integration.app, repo: integration.app,
secret_name: key secret_name: key,
} }
); );
} }
@@ -968,13 +991,13 @@ const syncSecretsGitHub = async ({
); );
await octokit.request( await octokit.request(
'PUT /repos/{owner}/{repo}/actions/secrets/{secret_name}', "PUT /repos/{owner}/{repo}/actions/secrets/{secret_name}",
{ {
owner: integration.owner, owner: integration.owner,
repo: integration.app, repo: integration.app,
secret_name: key, secret_name: key,
encrypted_value: encryptedSecret, encrypted_value: encryptedSecret,
key_id: repoPublicKey.key_id key_id: repoPublicKey.key_id,
} }
); );
}); });
@@ -982,7 +1005,7 @@ const syncSecretsGitHub = async ({
} catch (err) { } catch (err) {
Sentry.setUser(null); Sentry.setUser(null);
Sentry.captureException(err); 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 ({ const syncSecretsRender = async ({
integration, integration,
secrets, secrets,
accessToken accessToken,
}: { }: {
integration: IIntegration; integration: IIntegration;
secrets: any; secrets: any;
@@ -1007,20 +1030,20 @@ const syncSecretsRender = async ({
`${INTEGRATION_RENDER_API_URL}/v1/services/${integration.appId}/env-vars`, `${INTEGRATION_RENDER_API_URL}/v1/services/${integration.appId}/env-vars`,
Object.keys(secrets).map((key) => ({ Object.keys(secrets).map((key) => ({
key, key,
value: secrets[key] value: secrets[key],
})), })),
{ {
headers: { headers: {
Authorization: `Bearer ${accessToken}` Authorization: `Bearer ${accessToken}`,
} },
} }
); );
} catch (err) { } catch (err) {
Sentry.setUser(null); Sentry.setUser(null);
Sentry.captureException(err); 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 * Sync/push [secrets] to Fly.io app
@@ -1032,7 +1055,7 @@ const syncSecretsRender = async ({
const syncSecretsFlyio = async ({ const syncSecretsFlyio = async ({
integration, integration,
secrets, secrets,
accessToken accessToken,
}: { }: {
integration: IIntegration; integration: IIntegration;
secrets: any; secrets: any;
@@ -1062,19 +1085,22 @@ const syncSecretsFlyio = async ({
await axios({ await axios({
url: INTEGRATION_FLYIO_API_URL, url: INTEGRATION_FLYIO_API_URL,
method: 'post', method: "post",
headers: { headers: {
'Authorization': 'Bearer ' + accessToken Authorization: "Bearer " + accessToken,
}, },
data: { data: {
query: SetSecrets, query: SetSecrets,
variables: { variables: {
input: { input: {
appId: integration.app, appId: integration.app,
secrets: Object.entries(secrets).map(([key, value]) => ({ key, value })) secrets: Object.entries(secrets).map(([key, value]) => ({
} key,
} value,
} })),
},
},
},
}); });
// get secrets // get secrets
@@ -1094,8 +1120,9 @@ const syncSecretsFlyio = async ({
} }
}`; }`;
const getSecretsRes = (await axios({ const getSecretsRes = (
method: 'post', await axios({
method: "post",
url: INTEGRATION_FLYIO_API_URL, url: INTEGRATION_FLYIO_API_URL,
headers: { headers: {
'Authorization': 'Bearer ' + accessToken, 'Authorization': 'Bearer ' + accessToken,
@@ -1105,10 +1132,11 @@ const syncSecretsFlyio = async ({
data: { data: {
query: GetSecrets, query: GetSecrets,
variables: { variables: {
appName: integration.app appName: integration.app,
} },
} },
})).data.data.app.secrets; })
).data.data.app.secrets;
const deleteSecretsKeys = getSecretsRes const deleteSecretsKeys = getSecretsRes
.filter((secret: FlyioSecret) => !(secret.name in secrets)) .filter((secret: FlyioSecret) => !(secret.name in secrets))
@@ -1134,28 +1162,107 @@ const syncSecretsFlyio = async ({
}`; }`;
await axios({ await axios({
method: 'post', method: "post",
url: INTEGRATION_FLYIO_API_URL, url: INTEGRATION_FLYIO_API_URL,
headers: { headers: {
'Authorization': 'Bearer ' + accessToken, Authorization: "Bearer " + accessToken,
'Content-Type': 'application/json' "Content-Type": "application/json",
}, },
data: { data: {
query: DeleteSecrets, query: DeleteSecrets,
variables: { variables: {
input: { input: {
appId: integration.app, appId: integration.app,
keys: deleteSecretsKeys keys: deleteSecretsKeys,
} },
} },
} },
}); });
} catch (err) { } catch (err) {
Sentry.setUser(null); Sentry.setUser(null);
Sentry.captureException(err); Sentry.captureException(err);
throw new Error('Failed to sync secrets to Fly.io'); throw new Error("Failed to sync secrets to Fly.io");
} }
} };
/**
* Sync/push [secrets] to CircleCI project
* @param {Object} obj
* @param {IIntegration} obj.integration - integration details
* @param {Object} obj.secrets - secrets to push to integration (object where keys are secret keys and values are secret values)
* @param {String} obj.accessToken - access token for CircleCI integration
*/
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;
// sync secrets to CircleCI
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 }; export { syncSecrets };
+26 -21
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,7 +32,8 @@ export interface IIntegration {
| 'netlify' | 'netlify'
| 'github' | 'github'
| 'render' | 'render'
| 'flyio'; | 'flyio'
| 'circleci';
integrationAuth: Types.ObjectId; integrationAuth: Types.ObjectId;
} }
@@ -39,36 +41,38 @@ 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;
+24 -31
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;
@@ -42,8 +34,8 @@ 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
); );
+3 -4
View File
@@ -17,20 +17,20 @@ interface IApprover {
status: ApprovalStatus; status: ApprovalStatus;
} }
enum ApprovalStatus { export enum ApprovalStatus {
PENDING = 'pending', PENDING = 'pending',
APPROVED = 'approved', APPROVED = 'approved',
REJECTED = 'rejected' REJECTED = 'rejected'
} }
enum RequestType { export enum RequestType {
UPDATE = 'update', UPDATE = 'update',
DELETE = 'delete', DELETE = 'delete',
CREATE = 'create' CREATE = 'create'
} }
const approverSchema = new mongoose.Schema({ const approverSchema = new mongoose.Schema({
userId: { user: {
type: mongoose.Schema.Types.ObjectId, type: mongoose.Schema.Types.ObjectId,
ref: 'User', ref: 'User',
required: true required: true
@@ -42,7 +42,6 @@ const approverSchema = new mongoose.Schema({
} }
}); });
const secretApprovalRequestSchema = new Schema<ISecretApprovalRequest>( const secretApprovalRequestSchema = new Schema<ISecretApprovalRequest>(
{ {
secret: { secret: {
+1
View File
@@ -74,6 +74,7 @@ router.get(
'/', '/',
query('workspaceId').exists().trim(), query('workspaceId').exists().trim(),
query('environment').exists().trim(), query('environment').exists().trim(),
query('tagSlugs'),
validateRequest, validateRequest,
requireAuth({ requireAuth({
acceptedAuthModes: ['jwt', 'apiKey', 'serviceToken'] acceptedAuthModes: ['jwt', 'apiKey', 'serviceToken']
+11 -13
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,17 +28,12 @@ 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,
@@ -80,6 +76,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,
@@ -92,6 +89,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,
+33 -28
View File
@@ -6,19 +6,20 @@ 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,
@@ -26,27 +27,29 @@ const INTEGRATION_SET = new Set([
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 = [
{ {
@@ -122,6 +125,15 @@ const INTEGRATION_OPTIONS = [
clientId: '', clientId: '',
docsLink: '' docsLink: ''
}, },
{
name: 'Circle CI',
slug: 'circleci',
image: 'Circle CI.png',
isAvailable: true,
type: 'pat',
clientId: '',
docsLink: ''
},
{ {
name: 'Azure Key Vault', name: 'Azure Key Vault',
slug: 'azure-key-vault', slug: 'azure-key-vault',
@@ -149,15 +161,6 @@ const INTEGRATION_OPTIONS = [
type: '', type: '',
clientId: '', clientId: '',
docsLink: '' docsLink: ''
},
{
name: 'Circle CI',
slug: 'circleci',
image: 'Circle CI.png',
isAvailable: false,
type: '',
clientId: '',
docsLink: ''
} }
] ]
@@ -171,6 +174,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,
@@ -183,5 +187,6 @@ 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_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 -4
View File
@@ -114,6 +114,7 @@ func CallGetSecretsV2(httpClient *resty.Client, request GetEncryptedSecretsV2Req
SetHeader("User-Agent", USER_AGENT). SetHeader("User-Agent", USER_AGENT).
SetQueryParam("environment", request.Environment). SetQueryParam("environment", request.Environment).
SetQueryParam("workspaceId", request.WorkspaceId). SetQueryParam("workspaceId", request.WorkspaceId).
SetQueryParam("tagSlugs", request.TagSlugs).
Get(fmt.Sprintf("%v/v2/secrets", config.INFISICAL_URL)) Get(fmt.Sprintf("%v/v2/secrets", config.INFISICAL_URL))
if err != nil { if err != nil {
@@ -154,13 +155,12 @@ func CallIsAuthenticated(httpClient *resty.Client) bool {
SetHeader("User-Agent", USER_AGENT). SetHeader("User-Agent", USER_AGENT).
Post(fmt.Sprintf("%v/v1/auth/checkAuth", config.INFISICAL_URL)) Post(fmt.Sprintf("%v/v1/auth/checkAuth", config.INFISICAL_URL))
log.Debugln(fmt.Errorf("CallIsAuthenticated: Unsuccessful response: [response=%v]", response))
if err != nil { if err != nil {
return false return false
} }
if response.IsError() { if response.IsError() {
log.Debugln(fmt.Errorf("CallIsAuthenticated: Unsuccessful response: [response=%v]", response))
return false return false
} }
@@ -175,8 +175,6 @@ func CallGetAccessibleEnvironments(httpClient *resty.Client, request GetAccessib
SetHeader("User-Agent", USER_AGENT). SetHeader("User-Agent", USER_AGENT).
Get(fmt.Sprintf("%v/v2/workspace/%s/environments", config.INFISICAL_URL, request.WorkspaceId)) Get(fmt.Sprintf("%v/v2/workspace/%s/environments", config.INFISICAL_URL, request.WorkspaceId))
log.Debugln(fmt.Errorf("CallGetAccessibleEnvironments: Unsuccessful response: [response=%v]", response))
if err != nil { if err != nil {
return GetAccessibleEnvironmentsResponse{}, err return GetAccessibleEnvironmentsResponse{}, err
} }
+1
View File
@@ -197,6 +197,7 @@ type GetSecretsByWorkspaceIdAndEnvironmentRequest struct {
type GetEncryptedSecretsV2Request struct { type GetEncryptedSecretsV2Request struct {
Environment string `json:"environment"` Environment string `json:"environment"`
WorkspaceId string `json:"workspaceId"` WorkspaceId string `json:"workspaceId"`
TagSlugs string `json:"tagSlugs"`
} }
type GetEncryptedSecretsV2Response struct { type GetEncryptedSecretsV2Response struct {
+7 -1
View File
@@ -61,7 +61,12 @@ var exportCmd = &cobra.Command{
util.HandleError(err, "Unable to parse flag") util.HandleError(err, "Unable to parse flag")
} }
secrets, err := util.GetAllEnvironmentVariables(models.GetAllSecretsParameters{Environment: envName, InfisicalToken: infisicalToken}) tagSlugs, err := cmd.Flags().GetString("tags")
if err != nil {
util.HandleError(err, "Unable to parse flag")
}
secrets, err := util.GetAllEnvironmentVariables(models.GetAllSecretsParameters{Environment: envName, InfisicalToken: infisicalToken, TagSlugs: tagSlugs})
if err != nil { if err != nil {
util.HandleError(err, "Unable to fetch secrets") util.HandleError(err, "Unable to fetch secrets")
} }
@@ -97,6 +102,7 @@ func init() {
exportCmd.Flags().StringP("format", "f", "dotenv", "Set the format of the output file (dotenv, json, csv)") exportCmd.Flags().StringP("format", "f", "dotenv", "Set the format of the output file (dotenv, json, csv)")
exportCmd.Flags().Bool("secret-overriding", true, "Prioritizes personal secrets, if any, with the same name over shared secrets") exportCmd.Flags().Bool("secret-overriding", true, "Prioritizes personal secrets, if any, with the same name over shared secrets")
exportCmd.Flags().String("token", "", "Fetch secrets using the Infisical Token") exportCmd.Flags().String("token", "", "Fetch secrets using the Infisical Token")
exportCmd.Flags().StringP("tags", "t", "", "filter secrets by tag slugs")
} }
// Format according to the format flag // Format according to the format flag
+7 -1
View File
@@ -74,7 +74,12 @@ var runCmd = &cobra.Command{
util.HandleError(err, "Unable to parse flag") util.HandleError(err, "Unable to parse flag")
} }
secrets, err := util.GetAllEnvironmentVariables(models.GetAllSecretsParameters{Environment: envName, InfisicalToken: infisicalToken}) tagSlugs, err := cmd.Flags().GetString("tags")
if err != nil {
util.HandleError(err, "Unable to parse flag")
}
secrets, err := util.GetAllEnvironmentVariables(models.GetAllSecretsParameters{Environment: envName, InfisicalToken: infisicalToken, TagSlugs: tagSlugs})
if err != nil { if err != nil {
util.HandleError(err, "Could not fetch secrets", "If you are using a service token to fetch secrets, please ensure it is valid") util.HandleError(err, "Could not fetch secrets", "If you are using a service token to fetch secrets, please ensure it is valid")
@@ -148,6 +153,7 @@ func init() {
runCmd.Flags().Bool("expand", true, "Parse shell parameter expansions in your secrets") runCmd.Flags().Bool("expand", true, "Parse shell parameter expansions in your secrets")
runCmd.Flags().Bool("secret-overriding", true, "Prioritizes personal secrets, if any, with the same name over shared secrets") runCmd.Flags().Bool("secret-overriding", true, "Prioritizes personal secrets, if any, with the same name over shared secrets")
runCmd.Flags().StringP("command", "c", "", "chained commands to execute (e.g. \"npm install && npm run dev; echo ...\")") runCmd.Flags().StringP("command", "c", "", "chained commands to execute (e.g. \"npm install && npm run dev; echo ...\")")
runCmd.Flags().StringP("tags", "t", "", "filter secrets by tag slugs ")
} }
// Will execute a single command and pass in the given secrets into the process // Will execute a single command and pass in the given secrets into the process
+19 -3
View File
@@ -46,7 +46,12 @@ var secretsCmd = &cobra.Command{
util.HandleError(err) util.HandleError(err)
} }
secrets, err := util.GetAllEnvironmentVariables(models.GetAllSecretsParameters{Environment: environmentName, InfisicalToken: infisicalToken}) tagSlugs, err := cmd.Flags().GetString("tags")
if err != nil {
util.HandleError(err, "Unable to parse flag")
}
secrets, err := util.GetAllEnvironmentVariables(models.GetAllSecretsParameters{Environment: environmentName, InfisicalToken: infisicalToken, TagSlugs: tagSlugs})
if err != nil { if err != nil {
util.HandleError(err) util.HandleError(err)
} }
@@ -342,7 +347,12 @@ func getSecretsByNames(cmd *cobra.Command, args []string) {
util.HandleError(err, "Unable to parse flag") util.HandleError(err, "Unable to parse flag")
} }
secrets, err := util.GetAllEnvironmentVariables(models.GetAllSecretsParameters{Environment: environmentName, InfisicalToken: infisicalToken}) tagSlugs, err := cmd.Flags().GetString("tags")
if err != nil {
util.HandleError(err, "Unable to parse flag")
}
secrets, err := util.GetAllEnvironmentVariables(models.GetAllSecretsParameters{Environment: environmentName, InfisicalToken: infisicalToken, TagSlugs: tagSlugs})
if err != nil { if err != nil {
util.HandleError(err, "To fetch all secrets") util.HandleError(err, "To fetch all secrets")
} }
@@ -385,7 +395,12 @@ func generateExampleEnv(cmd *cobra.Command, args []string) {
util.HandleError(err, "Unable to parse flag") util.HandleError(err, "Unable to parse flag")
} }
secrets, err := util.GetAllEnvironmentVariables(models.GetAllSecretsParameters{Environment: environmentName, InfisicalToken: infisicalToken}) tagSlugs, err := cmd.Flags().GetString("tags")
if err != nil {
util.HandleError(err, "Unable to parse flag")
}
secrets, err := util.GetAllEnvironmentVariables(models.GetAllSecretsParameters{Environment: environmentName, InfisicalToken: infisicalToken, TagSlugs: tagSlugs})
if err != nil { if err != nil {
util.HandleError(err, "To fetch all secrets") util.HandleError(err, "To fetch all secrets")
} }
@@ -567,5 +582,6 @@ func init() {
secretsCmd.Flags().String("token", "", "Fetch secrets using the Infisical Token") secretsCmd.Flags().String("token", "", "Fetch secrets using the Infisical Token")
secretsCmd.PersistentFlags().String("env", "dev", "Used to select the environment name on which actions should be taken on") secretsCmd.PersistentFlags().String("env", "dev", "Used to select the environment name on which actions should be taken on")
secretsCmd.Flags().Bool("expand", true, "Parse shell parameter expansions in your secrets") secretsCmd.Flags().Bool("expand", true, "Parse shell parameter expansions in your secrets")
secretsCmd.PersistentFlags().StringP("tags", "t", "", "filter secrets by tag slugs")
rootCmd.AddCommand(secretsCmd) rootCmd.AddCommand(secretsCmd)
} }
+1
View File
@@ -51,4 +51,5 @@ type SymmetricEncryptionResult struct {
type GetAllSecretsParameters struct { type GetAllSecretsParameters struct {
Environment string Environment string
InfisicalToken string InfisicalToken string
TagSlugs string
} }
+3 -2
View File
@@ -62,7 +62,7 @@ func GetPlainTextSecretsViaServiceToken(fullServiceToken string) ([]models.Singl
return plainTextSecrets, nil return plainTextSecrets, nil
} }
func GetPlainTextSecretsViaJTW(JTWToken string, receiversPrivateKey string, workspaceId string, environmentName string) ([]models.SingleEnvironmentVariable, error) { func GetPlainTextSecretsViaJTW(JTWToken string, receiversPrivateKey string, workspaceId string, environmentName string, tagSlugs string) ([]models.SingleEnvironmentVariable, error) {
httpClient := resty.New() httpClient := resty.New()
httpClient.SetAuthToken(JTWToken). httpClient.SetAuthToken(JTWToken).
SetHeader("Accept", "application/json") SetHeader("Accept", "application/json")
@@ -85,6 +85,7 @@ func GetPlainTextSecretsViaJTW(JTWToken string, receiversPrivateKey string, work
encryptedSecrets, err := api.CallGetSecretsV2(httpClient, api.GetEncryptedSecretsV2Request{ encryptedSecrets, err := api.CallGetSecretsV2(httpClient, api.GetEncryptedSecretsV2Request{
WorkspaceId: workspaceId, WorkspaceId: workspaceId,
Environment: environmentName, Environment: environmentName,
TagSlugs: tagSlugs,
}) })
if err != nil { if err != nil {
@@ -136,7 +137,7 @@ func GetAllEnvironmentVariables(params models.GetAllSecretsParameters) ([]models
return nil, fmt.Errorf("unable to validate environment name because [err=%s]", err) return nil, fmt.Errorf("unable to validate environment name because [err=%s]", err)
} }
secretsToReturn, errorToReturn = GetPlainTextSecretsViaJTW(loggedInUserDetails.UserCredentials.JTWToken, loggedInUserDetails.UserCredentials.PrivateKey, workspaceFile.WorkspaceId, params.Environment) secretsToReturn, errorToReturn = GetPlainTextSecretsViaJTW(loggedInUserDetails.UserCredentials.JTWToken, loggedInUserDetails.UserCredentials.PrivateKey, workspaceFile.WorkspaceId, params.Environment, params.TagSlugs)
log.Debugf("GetAllEnvironmentVariables: Trying to fetch secrets JTW token [err=%s]", errorToReturn) log.Debugf("GetAllEnvironmentVariables: Trying to fetch secrets JTW token [err=%s]", errorToReturn)
backupSecretsEncryptionKey := []byte(loggedInUserDetails.UserCredentials.PrivateKey)[0:32] backupSecretsEncryptionKey := []byte(loggedInUserDetails.UserCredentials.PrivateKey)[0:32]
+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 = {
@@ -38,7 +38,6 @@ const UserTable = ({ userData, changeData, myUser, filter, resendInvite, isOrg }
const router = useRouter(); const router = useRouter();
const [myRole, setMyRole] = useState('member'); const [myRole, setMyRole] = useState('member');
const [userProjectMemberships, setUserProjectMemberships] = useState<any[]>([]); const [userProjectMemberships, setUserProjectMemberships] = useState<any[]>([]);
console.log(123, userData)
const workspaceId = router.query.id as string; const workspaceId = router.query.id as string;
// Delete the row in the table (e.g. a user) // Delete the row in the table (e.g. a user)
@@ -198,15 +197,15 @@ const UserTable = ({ userData, changeData, myUser, filter, resendInvite, isOrg }
</div> </div>
</td> </td>
<td className="pl-4 py-2 border-mineshaft-700 border-t text-gray-300"> <td className="pl-4 py-2 border-mineshaft-700 border-t text-gray-300">
<td className="flex items-center max-h-16 overflow-x-auto w-full max-w-xl"> <div className="flex items-center max-h-16 overflow-x-auto w-full max-w-xl break-all">
{userProjectMemberships[row.userId] {userProjectMemberships[row.userId]
? userProjectMemberships[row.userId]?.map((project: any) => ( ? userProjectMemberships[row.userId]?.map((project: any) => (
<div key={project.id} className='mx-1 min-w-max px-1.5 bg-mineshaft-500 rounded-sm text-sm text-bunker-200 flex items-center'> <div key={project._id} className='mx-1 min-w-max px-1.5 bg-mineshaft-500 rounded-sm text-sm text-bunker-200 flex items-center'>
<span className='mb-0.5 cursor-default'>{project.name}</span> <span className='mb-0.5 cursor-default'>{project.name}</span>
</div> </div>
)) ))
: <span className='ml-1 text-bunker-100 rounded-sm px-1 py-0.5 text-sm bg-red/80'>This user isn&apos;t part of any projects yet.</span>} : <span className='ml-1 text-bunker-100 rounded-sm px-1 py-0.5 text-sm bg-red/80'>This user isn&apos;t part of any projects yet.</span>}
</td> </div>
</td> </td>
<td className="flex flex-row justify-end pl-8 pr-8 py-2 border-t border-0.5 border-mineshaft-700"> <td className="flex flex-row justify-end pl-8 pr-8 py-2 border-t border-0.5 border-mineshaft-700">
{myUser !== row.email && {myUser !== row.email &&
@@ -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,7 +89,8 @@ 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() +
integration.targetEnvironment.substring(1)
: 'Development' : 'Development'
); );
break; break;
@@ -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={() =>
handleDeleteIntegration({
integration 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;
@@ -38,6 +38,13 @@ export const Secondary: Story = {
} }
}; };
export const Star: Story = {
args: {
children: 'Hello Infisical',
variant: 'star'
}
};
export const Danger: Story = { export const Danger: Story = {
args: { args: {
children: 'Hello Infisical', children: 'Hello Infisical',
+13 -1
View File
@@ -31,7 +31,9 @@ const buttonVariants = cva(
variant: { variant: {
solid: '', solid: '',
outline: ['bg-transparent', 'border-2', 'border-solid'], outline: ['bg-transparent', 'border-2', 'border-solid'],
plain: '' plain: '',
// a constant color not in use on hover or click goes colorSchema color
star: 'text-bunker-200 bg-mineshaft-500'
}, },
isDisabled: { isDisabled: {
true: 'bg-mineshaft opacity-40 cursor-not-allowed', true: 'bg-mineshaft opacity-40 cursor-not-allowed',
@@ -53,6 +55,16 @@ const buttonVariants = cva(
} }
}, },
compoundVariants: [ compoundVariants: [
{
colorSchema: 'primary',
variant: 'star',
className: 'hover:bg-primary hover:text-black'
},
{
colorSchema: 'danger',
variant: 'star',
className: 'hover:bg-red hover:text-white'
},
{ {
colorSchema: 'primary', colorSchema: 'primary',
variant: 'outline', variant: 'outline',
+1 -1
View File
@@ -46,7 +46,7 @@ export const Select = forwardRef<HTMLButtonElement, SelectProps>(
<SelectPrimitive.Portal> <SelectPrimitive.Portal>
<SelectPrimitive.Content <SelectPrimitive.Content
className={twMerge( className={twMerge(
'relative left-4 top-1 overflow-hidden rounded-md bg-bunker-800 border border-mineshaft-500 drop-shadow-xl font-inter text-bunker-100 shadow-md z-[100]', 'relative top-1 overflow-hidden rounded-md bg-bunker-800 font-inter text-bunker-100 shadow-md z-[100]',
dropdownContainerClassName dropdownContainerClassName
)} )}
position={position} position={position}
+2
View File
@@ -44,3 +44,5 @@ const plansProd: Mapping = {
}; };
export const plans = plansProd || plansDev; export const plans = plansProd || plansDev;
export const leaveConfirmDefaultMessage = 'Do you want to save your results before leaving this page?';
@@ -27,7 +27,7 @@ export const OrgProvider = ({ children }: Props): JSX.Element => {
const value = useMemo<TOrgContext>( const value = useMemo<TOrgContext>(
() => ({ () => ({
orgs: userOrgs, orgs: userOrgs,
currentOrg: (userOrgs || []).find(({ _id }) => _id === currentWsOrgID), currentOrg: (userOrgs || []).find(({ _id }) => _id === currentWsOrgID) || (userOrgs || [])[0],
isLoading isLoading
}), }),
[currentWsOrgID, userOrgs, isLoading] [currentWsOrgID, userOrgs, isLoading]
+11 -17
View File
@@ -9,7 +9,6 @@ import getActionData from '@app/ee/api/secrets/GetActionData';
import patienceDiff from '@app/ee/utilities/findTextDifferences'; import patienceDiff from '@app/ee/utilities/findTextDifferences';
import getLatestFileKey from '@app/pages/api/workspace/getLatestFileKey'; import getLatestFileKey from '@app/pages/api/workspace/getLatestFileKey';
import DashboardInputField from '../../components/dashboard/DashboardInputField';
import { import {
decryptAssymmetric, decryptAssymmetric,
decryptSymmetric decryptSymmetric
@@ -130,7 +129,7 @@ const ActivitySideBar = ({ toggleSidebar, currentAction }: SideBarProps) => {
<div <div
className={`absolute border-l border-mineshaft-500 ${ className={`absolute border-l border-mineshaft-500 ${
isLoading ? 'bg-bunker-800' : 'bg-bunker' isLoading ? 'bg-bunker-800' : 'bg-bunker'
} fixed h-full w-96 top-14 right-0 z-40 shadow-xl flex flex-col justify-between`} } fixed h-[calc(100vh-56px)] w-96 top-14 right-0 z-40 shadow-xl flex flex-col justify-between`}
> >
{isLoading ? ( {isLoading ? (
<div className="flex items-center justify-center h-full mb-8"> <div className="flex items-center justify-center h-full mb-8">
@@ -142,7 +141,7 @@ const ActivitySideBar = ({ toggleSidebar, currentAction }: SideBarProps) => {
/> />
</div> </div>
) : ( ) : (
<div className="h-min overflow-y-auto"> <div className="h-min">
<div className="flex flex-row px-4 py-3 border-b border-mineshaft-500 justify-between items-center"> <div className="flex flex-row px-4 py-3 border-b border-mineshaft-500 justify-between items-center">
<p className="font-semibold text-lg text-bunker-200"> <p className="font-semibold text-lg text-bunker-200">
{t(`activity:event.${actionMetaData?.name}`)} {t(`activity:event.${actionMetaData?.name}`)}
@@ -157,7 +156,7 @@ const ActivitySideBar = ({ toggleSidebar, currentAction }: SideBarProps) => {
<FontAwesomeIcon icon={faXmark} className="w-4 h-4 text-bunker-300 cursor-pointer" /> <FontAwesomeIcon icon={faXmark} className="w-4 h-4 text-bunker-300 cursor-pointer" />
</div> </div>
</div> </div>
<div className="flex flex-col px-4"> <div className="flex flex-col px-4 overflow-y-auto h-[calc(100vh-120px)] overflow-y-autp">
{(actionMetaData?.name === 'readSecrets' || {(actionMetaData?.name === 'readSecrets' ||
actionMetaData?.name === 'addSecrets' || actionMetaData?.name === 'addSecrets' ||
actionMetaData?.name === 'deleteSecrets') && actionMetaData?.name === 'deleteSecrets') &&
@@ -166,14 +165,9 @@ const ActivitySideBar = ({ toggleSidebar, currentAction }: SideBarProps) => {
<div className="text-xs text-bunker-200 mt-4 pl-1 ph-no-capture"> <div className="text-xs text-bunker-200 mt-4 pl-1 ph-no-capture">
{item.newSecretVersion.key} {item.newSecretVersion.key}
</div> </div>
<DashboardInputField <div className='w-full font-mono text-sm break-all bg-mineshaft-600 px-2 py-0.5 rounded-md border border-mineshaft-500 text-bunker-200'>
onChangeHandler={() => {}} {item.newSecretVersion.value ? <span> {item.newSecretVersion.value} </span> : <span className='text-bunker-400'> EMPTY </span>}
type="value" </div>
position={1}
value={item.newSecretVersion.value}
isDuplicate={false}
blurred={false}
/>
</div> </div>
))} ))}
{actionMetaData?.name === 'updateSecrets' && {actionMetaData?.name === 'updateSecrets' &&
@@ -182,8 +176,8 @@ const ActivitySideBar = ({ toggleSidebar, currentAction }: SideBarProps) => {
<div className="text-xs text-bunker-200 mt-4 pl-1"> <div className="text-xs text-bunker-200 mt-4 pl-1">
{item.newSecretVersion.key} {item.newSecretVersion.key}
</div> </div>
<div className="text-bunker-100 font-mono rounded-md overflow-hidden"> <div className="break-all text-bunker-200 font-mono rounded-md overflow-hidden border border-mineshaft-500">
<div className="bg-red/30 px-2 ph-no-capture"> <div className="bg-red/40 px-2 ph-no-capture">
-{' '} -{' '}
{patienceDiff( {patienceDiff(
item.oldSecretVersion.value.split(''), item.oldSecretVersion.value.split(''),
@@ -194,14 +188,14 @@ const ActivitySideBar = ({ toggleSidebar, currentAction }: SideBarProps) => {
character.bIndex !== -1 && ( character.bIndex !== -1 && (
<span <span
key={`actionData.${id + 1}.line.${lineId + 1}`} key={`actionData.${id + 1}.line.${lineId + 1}`}
className={`${character.aIndex === -1 && 'bg-red-700/80'}`} className={`${character.aIndex === -1 && 'text-bunker-100 bg-red-700/80'}`}
> >
{character.line} {character.line}
</span> </span>
) )
)} )}
</div> </div>
<div className="bg-green-500/30 px-2 ph-no-capture"> <div className="break-all bg-green-500/40 px-2 ph-no-capture">
+{' '} +{' '}
{patienceDiff( {patienceDiff(
item.oldSecretVersion.value.split(''), item.oldSecretVersion.value.split(''),
@@ -212,7 +206,7 @@ const ActivitySideBar = ({ toggleSidebar, currentAction }: SideBarProps) => {
character.aIndex !== -1 && ( character.aIndex !== -1 && (
<span <span
key={`actionData.${id + 1}.linev2.${lineId + 1}`} key={`actionData.${id + 1}.linev2.${lineId + 1}`}
className={`${character.bIndex === -1 && 'bg-green-700/80'}`} className={`${character.bIndex === -1 && 'text-bunker-100 bg-green-700/80'}`}
> >
{character.line} {character.line}
</span> </span>
+1
View File
@@ -1,2 +1,3 @@
export { useLeaveConfirm } from './useLeaveConfirm';
export { usePopUp } from './usePopUp'; export { usePopUp } from './usePopUp';
export { useToggle } from './useToggle'; export { useToggle } from './useToggle';
+55
View File
@@ -0,0 +1,55 @@
import { Dispatch, SetStateAction, useCallback, useEffect, useState } from 'react';
import { useRouter } from 'next/router';
import { leaveConfirmDefaultMessage } from '@app/const';
type LeaveConfirmProps = {
initialValue: boolean,
message?: string
}
interface LeaveConfirmReturn {
hasUnsavedChanges: boolean,
setHasUnsavedChanges: Dispatch<SetStateAction<boolean>>,
}
export function useLeaveConfirm({
initialValue,
message = leaveConfirmDefaultMessage,
}: LeaveConfirmProps): LeaveConfirmReturn {
const router = useRouter()
const [hasUnsavedChanges, setHasUnsavedChanges] = useState<boolean>(initialValue);
const onRouteChangeStart = useCallback(() => {
if (hasUnsavedChanges) {
if (window.confirm(message)) {
return true
}
throw new Error("Abort route change by user's confirmation.")
}
return false;
}, [hasUnsavedChanges])
const handleWindowClose = useCallback((e: any) => {
if (!hasUnsavedChanges) {
return;
}
e.preventDefault();
e.returnValue = message;
}, []);
useEffect(() => {
router.events.on("routeChangeStart", onRouteChangeStart);
window.addEventListener('beforeunload', handleWindowClose);
return () => {
router.events.off("routeChangeStart", onRouteChangeStart);
window.removeEventListener('beforeunload', handleWindowClose);
}
}, [onRouteChangeStart, handleWindowClose]);
return {
hasUnsavedChanges,
setHasUnsavedChanges,
};
}
+29 -13
View File
@@ -64,7 +64,7 @@ export const AppLayout = ({ children }: LayoutProps) => {
// eslint-disable-next-line prefer-const // eslint-disable-next-line prefer-const
let { workspaces, currentWorkspace } = useWorkspace(); let { workspaces, currentWorkspace } = useWorkspace();
const { currentOrg } = useOrganization(); const { currentOrg } = useOrganization();
workspaces = workspaces.filter(ws => ws.organization === currentOrg?._id) workspaces = workspaces.filter((ws) => ws.organization === currentOrg?._id);
const { user } = useUser(); const { user } = useUser();
const createWs = useCreateWorkspace(); const createWs = useCreateWorkspace();
@@ -98,7 +98,7 @@ export const AppLayout = ({ children }: LayoutProps) => {
const putUserInWorkSpace = async () => { const putUserInWorkSpace = async () => {
if (tempLocalStorage('orgData.id') === '') { if (tempLocalStorage('orgData.id') === '') {
const userOrgs = await getOrganizations(); const userOrgs = await getOrganizations();
localStorage.setItem('orgData.id', userOrgs[0]._id); localStorage.setItem('orgData.id', userOrgs[0]?._id);
} }
const orgUserProjects = await getOrganizationUserProjects({ const orgUserProjects = await getOrganizationUserProjects({
@@ -124,7 +124,7 @@ export const AppLayout = ({ children }: LayoutProps) => {
// If a user is not a member of a workspace they are trying to access, just push them to one of theirs // If a user is not a member of a workspace they are trying to access, just push them to one of theirs
if ( if (
!['callback', 'create', 'authorize'].includes(intendedWorkspaceId) && !['callback', 'create', 'authorize'].includes(intendedWorkspaceId) && userWorkspaces[0]?._id !== undefined &&
!userWorkspaces !userWorkspaces
.map((workspace: { _id: string }) => workspace._id) .map((workspace: { _id: string }) => workspace._id)
.includes(intendedWorkspaceId) .includes(intendedWorkspaceId)
@@ -219,9 +219,11 @@ export const AppLayout = ({ children }: LayoutProps) => {
<aside className="w-full border-r border-mineshaft-500 bg-mineshaft-900 md:w-60"> <aside className="w-full border-r border-mineshaft-500 bg-mineshaft-900 md:w-60">
<nav className="items-between flex h-full flex-col justify-between"> <nav className="items-between flex h-full flex-col justify-between">
<div> <div>
{currentWorkspace {currentWorkspace ? (
? <div className="w-full p-4 mt-3 mb-4"> <div className="w-full p-4 mt-3 mb-4">
<p className="text-xs font-semibold ml-1.5 mb-1 uppercase text-gray-400">Project</p> <p className="text-xs font-semibold ml-1.5 mb-1 uppercase text-gray-400">
Project
</p>
<Select <Select
defaultValue={currentWorkspace?._id} defaultValue={currentWorkspace?._id}
value={currentWorkspace?._id} value={currentWorkspace?._id}
@@ -230,10 +232,14 @@ export const AppLayout = ({ children }: LayoutProps) => {
router.push(`/dashboard/${value}`); router.push(`/dashboard/${value}`);
}} }}
position="popper" position="popper"
dropdownContainerClassName="left-0 text-bunker-200 bg-mineshaft-800 border border-mineshaft-600 z-50" dropdownContainerClassName="text-bunker-200 bg-mineshaft-800 border border-mineshaft-600 z-50"
> >
{workspaces.map(({ _id, name }) => ( {workspaces.map(({ _id, name }) => (
<SelectItem key={`ws-layout-list-${_id}`} value={_id} className={`${currentWorkspace?._id === _id && "bg-mineshaft-600"}`}> <SelectItem
key={`ws-layout-list-${_id}`}
value={_id}
className={`${currentWorkspace?._id === _id && 'bg-mineshaft-600'}`}
>
{name} {name}
</SelectItem> </SelectItem>
))} ))}
@@ -251,7 +257,8 @@ export const AppLayout = ({ children }: LayoutProps) => {
</div> </div>
</Select> </Select>
</div> </div>
: <div className="w-full p-4 mt-3 mb-4"> ) : (
<div className="w-full p-4 mt-3 mb-4">
<Button <Button
className="w-full py-2 text-bunker-200 bg-mineshaft-500 hover:bg-primary/90 hover:text-black" className="w-full py-2 text-bunker-200 bg-mineshaft-500 hover:bg-primary/90 hover:text-black"
color="mineshaft" color="mineshaft"
@@ -261,8 +268,9 @@ export const AppLayout = ({ children }: LayoutProps) => {
> >
Add Project Add Project
</Button> </Button>
</div>} </div>
<div className={`${currentWorkspace ? "block" : "hidden"}`}> )}
<div className={`${currentWorkspace ? 'block' : 'hidden'}`}>
<Menu> <Menu>
<Link href={`/dashboard/${currentWorkspace?._id}`} passHref> <Link href={`/dashboard/${currentWorkspace?._id}`} passHref>
<a> <a>
@@ -393,7 +401,7 @@ export const AppLayout = ({ children }: LayoutProps) => {
</FormControl> </FormControl>
)} )}
/> />
<div className='pl-1 mt-4'> <div className="pl-1 mt-4">
<Controller <Controller
control={control} control={control}
name="addMembers" name="addMembers"
@@ -415,11 +423,19 @@ export const AppLayout = ({ children }: LayoutProps) => {
isDisabled={isSubmitting} isDisabled={isSubmitting}
isLoading={isSubmitting} isLoading={isSubmitting}
key="layout-create-project-submit" key="layout-create-project-submit"
className="" className="mr-4"
type="submit" type="submit"
> >
Create Project Create Project
</Button> </Button>
<Button
key="layout-cancel-create-project"
onClick={() => handlePopUpClose('addNewWs')}
variant="plain"
colorSchema="secondary"
>
Cancel
</Button>
</div> </div>
</form> </form>
</ModalContent> </ModalContent>
+41 -76
View File
@@ -35,9 +35,11 @@ import encryptSecrets from '@app/components/utilities/secrets/encryptSecrets';
import getSecretsForProject from '@app/components/utilities/secrets/getSecretsForProject'; import getSecretsForProject from '@app/components/utilities/secrets/getSecretsForProject';
import { getTranslatedServerSideProps } from '@app/components/utilities/withTranslateProps'; import { getTranslatedServerSideProps } from '@app/components/utilities/withTranslateProps';
import { IconButton } from '@app/components/v2'; import { IconButton } from '@app/components/v2';
import { leaveConfirmDefaultMessage } from '@app/const';
import getProjectSercetSnapshotsCount from '@app/ee/api/secrets/GetProjectSercetSnapshotsCount'; import getProjectSercetSnapshotsCount from '@app/ee/api/secrets/GetProjectSercetSnapshotsCount';
import performSecretRollback from '@app/ee/api/secrets/PerformSecretRollback'; import performSecretRollback from '@app/ee/api/secrets/PerformSecretRollback';
import PITRecoverySidebar from '@app/ee/components/PITRecoverySidebar'; import PITRecoverySidebar from '@app/ee/components/PITRecoverySidebar';
import { useLeaveConfirm } from '@app/hooks';
import addSecrets from '../api/files/AddSecrets'; import addSecrets from '../api/files/AddSecrets';
import deleteSecrets from '../api/files/DeleteSecrets'; import deleteSecrets from '../api/files/DeleteSecrets';
@@ -116,7 +118,6 @@ function findDuplicates(arr: any[]) {
export default function Dashboard() { export default function Dashboard() {
const [data, setData] = useState<SecretDataProps[] | null>(); const [data, setData] = useState<SecretDataProps[] | null>();
const [initialData, setInitialData] = useState<SecretDataProps[] | null | undefined>([]); const [initialData, setInitialData] = useState<SecretDataProps[] | null | undefined>([]);
const [buttonReady, setButtonReady] = useState(false);
const router = useRouter(); const router = useRouter();
const [blurred, setBlurred] = useState(true); const [blurred, setBlurred] = useState(true);
const [isKeyAvailable, setIsKeyAvailable] = useState(true); const [isKeyAvailable, setIsKeyAvailable] = useState(true);
@@ -137,6 +138,7 @@ export default function Dashboard() {
const [dropZoneData, setDropZoneData] = useState<SecretDataProps[]>(); const [dropZoneData, setDropZoneData] = useState<SecretDataProps[]>();
const [projectTags, setProjectTags] = useState<Tag[]>([]); const [projectTags, setProjectTags] = useState<Tag[]>([]);
const { hasUnsavedChanges, setHasUnsavedChanges } = useLeaveConfirm({initialValue: false});
const { t } = useTranslation(); const { t } = useTranslation();
const { createNotification } = useNotificationContext(); const { createNotification } = useNotificationContext();
@@ -153,36 +155,6 @@ export default function Dashboard() {
setAtSecretsAreaTop(false); setAtSecretsAreaTop(false);
} }
}; };
// #TODO: fix save message for changing reroutes
// const beforeRouteHandler = (url) => {
// const warningText =
// "Do you want to save your results bfore leaving this page?";
// if (!buttonReady) return;
// if (router.asPath !== url && !confirm(warningText)) {
// // router.events.emit('routeChangeError');
// // setData(data)
// savePush();
// throw `Route change to "${url}" was aborted (this error can be safely ignored).`;
// } else {
// setButtonReady(false);
// }
// };
// prompt the user if they try and leave with unsaved changes
useEffect(() => {
const warningText = 'Do you want to save your results before leaving this page?';
const handleWindowClose = (e: any) => {
if (!buttonReady) return;
e.preventDefault();
e.returnValue = warningText;
};
window.addEventListener('beforeunload', handleWindowClose);
// router.events.on('routeChangeStart', beforeRouteHandler);
return () => {
window.removeEventListener('beforeunload', handleWindowClose);
// router.events.off('routeChangeStart', beforeRouteHandler);
};
}, [buttonReady]);
// TODO(akhilmhdh): change to FP // TODO(akhilmhdh): change to FP
const sortValuesHandler = ( const sortValuesHandler = (
@@ -318,7 +290,7 @@ export default function Dashboard() {
}; };
const deleteRow = ({ ids, secretName }: { ids: string[]; secretName: string }) => { const deleteRow = ({ ids, secretName }: { ids: string[]; secretName: string }) => {
setButtonReady(true); setHasUnsavedChanges(true);
toggleSidebar('None'); toggleSidebar('None');
createNotification({ createNotification({
text: `${secretName || 'Secret'} has been deleted. Remember to save changes.`, text: `${secretName || 'Secret'} has been deleted. Remember to save changes.`,
@@ -332,27 +304,27 @@ export default function Dashboard() {
const modifyValue = (value: string, pos: number) => { const modifyValue = (value: string, pos: number) => {
setData((oldData) => oldData?.map((e) => (e.pos === pos ? { ...e, value } : e))); setData((oldData) => oldData?.map((e) => (e.pos === pos ? { ...e, value } : e)));
setButtonReady(true); setHasUnsavedChanges(true);
}; };
const modifyValueOverride = (valueOverride: string | undefined, pos: number) => { const modifyValueOverride = (valueOverride: string | undefined, pos: number) => {
setData((oldData) => oldData?.map((e) => (e.pos === pos ? { ...e, valueOverride } : e))); setData((oldData) => oldData?.map((e) => (e.pos === pos ? { ...e, valueOverride } : e)));
setButtonReady(true); setHasUnsavedChanges(true);
}; };
const modifyKey = (key: string, pos: number) => { const modifyKey = (key: string, pos: number) => {
setData((oldData) => oldData?.map((e) => (e.pos === pos ? { ...e, key } : e))); setData((oldData) => oldData?.map((e) => (e.pos === pos ? { ...e, key } : e)));
setButtonReady(true); setHasUnsavedChanges(true);
}; };
const modifyComment = (comment: string, pos: number) => { const modifyComment = (comment: string, pos: number) => {
setData((oldData) => oldData?.map((e) => (e.pos === pos ? { ...e, comment } : e))); setData((oldData) => oldData?.map((e) => (e.pos === pos ? { ...e, comment } : e)));
setButtonReady(true); setHasUnsavedChanges(true);
}; };
const modifyTags = (tags: Tag[], pos: number) => { const modifyTags = (tags: Tag[], pos: number) => {
setData((oldData) => oldData?.map((e) => (e.pos === pos ? { ...e, tags } : e))); setData((oldData) => oldData?.map((e) => (e.pos === pos ? { ...e, tags } : e)));
setButtonReady(true); setHasUnsavedChanges(true);
}; };
// For speed purposes and better perforamance, we are using useCallback // For speed purposes and better perforamance, we are using useCallback
@@ -422,7 +394,7 @@ export default function Dashboard() {
} }
// Once "Save changes" is clicked, disable that button // Once "Save changes" is clicked, disable that button
setButtonReady(false); setHasUnsavedChanges(false);
const secretsToBeDeleted = initialData! const secretsToBeDeleted = initialData!
.filter( .filter(
@@ -566,7 +538,7 @@ export default function Dashboard() {
); );
return filteredOldData.concat(filteredNewData); return filteredOldData.concat(filteredNewData);
}); });
setButtonReady(true); setHasUnsavedChanges(true);
}; };
const addData = (newData: SecretDataProps[]) => { const addData = (newData: SecretDataProps[]) => {
@@ -587,6 +559,26 @@ export default function Dashboard() {
deleteRow({ ids, secretName }); deleteRow({ ids, secretName });
}; };
const handleOnEnvironmentChange = (envName: string) => {
if(hasUnsavedChanges) {
if (!window.confirm(leaveConfirmDefaultMessage)) return;
}
const selectedWorkspaceEnv = workspaceEnvs.find(({ name }: { name: string }) => envName === name) || {
name: 'unknown',
slug: 'unknown',
isWriteDenied: false,
isReadDenied: false
};
if (selectedWorkspaceEnv) {
if (snapshotData) setSelectedSnapshotEnv(selectedWorkspaceEnv);
else setSelectedEnv(selectedWorkspaceEnv);
}
setHasUnsavedChanges(false);
};
return data ? ( return data ? (
<div className="bg-bunker-800 max-h-screen h-full relative flex flex-col justify-between text-white dark"> <div className="bg-bunker-800 max-h-screen h-full relative flex flex-col justify-between text-white dark">
<Head> <Head>
@@ -655,16 +647,7 @@ export default function Dashboard() {
<ListBox <ListBox
isSelected={selectedEnv.name} isSelected={selectedEnv.name}
data={workspaceEnvs.map(({ name }) => name)} data={workspaceEnvs.map(({ name }) => name)}
onChange={(envName) => onChange={handleOnEnvironmentChange}
setSelectedEnv(
workspaceEnvs.find(({ name }) => envName === name) || {
name: 'unknown',
slug: 'unknown',
isWriteDenied: false,
isReadDenied: false
}
)
}
/> />
)} )}
</div> </div>
@@ -681,14 +664,14 @@ export default function Dashboard() {
icon={faClockRotateLeft} icon={faClockRotateLeft}
/>} />}
</div> </div>
{(data?.length !== 0 || buttonReady) && !snapshotData && ( {(data?.length !== 0 || hasUnsavedChanges) && !snapshotData && (
<div className="flex justify-start max-w-sm mt-1"> <div className="flex justify-start max-w-sm mt-1">
<Button <Button
text={String(t('common:save-changes'))} text={String(t('common:save-changes'))}
onButtonPressed={savePush} onButtonPressed={savePush}
color="primary" color="primary"
size="md" size="md"
active={buttonReady} active={hasUnsavedChanges}
iconDisabled={faCheck} iconDisabled={faCheck}
textDisabled={String(t('common:saved'))} textDisabled={String(t('common:saved'))}
loading={saveLoading} loading={saveLoading}
@@ -723,11 +706,11 @@ export default function Dashboard() {
text: `Rollback has been performed successfully.`, text: `Rollback has been performed successfully.`,
type: 'success' type: 'success'
}); });
setButtonReady(false); setHasUnsavedChanges(false);
}} }}
color="primary" color="primary"
size="md" size="md"
active={buttonReady} active={hasUnsavedChanges}
/> />
</div> </div>
)} )}
@@ -742,31 +725,13 @@ export default function Dashboard() {
<ListBox <ListBox
isSelected={selectedEnv.name} isSelected={selectedEnv.name}
data={workspaceEnvs.map(({ name }) => name)} data={workspaceEnvs.map(({ name }) => name)}
onChange={(envName) => onChange={handleOnEnvironmentChange}
setSelectedEnv(
workspaceEnvs.find(({ name }) => envName === name) || {
name: 'unknown',
slug: 'unknown',
isWriteDenied: false,
isReadDenied: false
}
)
}
/> />
) : ( ) : (
<ListBox <ListBox
isSelected={selectedSnapshotEnv?.name || ''} isSelected={selectedSnapshotEnv?.name || ''}
data={workspaceEnvs.map(({ name }) => name)} data={workspaceEnvs.map(({ name }) => name)}
onChange={(envName) => onChange={handleOnEnvironmentChange}
setSelectedSnapshotEnv(
workspaceEnvs.find(({ name }) => envName === name) || {
name: 'unknown',
slug: 'unknown',
isWriteDenied: false,
isReadDenied: false
}
)
}
/> />
)} )}
<div className="h-10 w-full bg-mineshaft-700 hover:bg-white/10 ml-2 rounded-md flex flex-row items-center"> <div className="h-10 w-full bg-mineshaft-700 hover:bg-white/10 ml-2 rounded-md flex flex-row items-center">
@@ -970,7 +935,7 @@ export default function Dashboard() {
setErrorDragAndDrop={setErrorDragAndDrop} setErrorDragAndDrop={setErrorDragAndDrop}
createNewFile={addRow} createNewFile={addRow}
errorDragAndDrop={errorDragAndDrop} errorDragAndDrop={errorDragAndDrop}
setButtonReady={setButtonReady} setButtonReady={setHasUnsavedChanges}
keysExist keysExist
numCurrentRows={data.length} numCurrentRows={data.length}
/> />
@@ -986,7 +951,7 @@ export default function Dashboard() {
setErrorDragAndDrop={setErrorDragAndDrop} setErrorDragAndDrop={setErrorDragAndDrop}
createNewFile={addRow} createNewFile={addRow}
errorDragAndDrop={errorDragAndDrop} errorDragAndDrop={errorDragAndDrop}
setButtonReady={setButtonReady} setButtonReady={setHasUnsavedChanges}
numCurrentRows={data.length} numCurrentRows={data.length}
keysExist={false} keysExist={false}
/> />
@@ -1013,7 +978,7 @@ export default function Dashboard() {
modifyValue={listenChangeValue} modifyValue={listenChangeValue}
modifyValueOverride={listenChangeValueOverride} modifyValueOverride={listenChangeValueOverride}
modifyComment={listenChangeComment} modifyComment={listenChangeComment}
buttonReady={buttonReady} buttonReady={hasUnsavedChanges}
workspaceEnvs={workspaceEnvs} workspaceEnvs={workspaceEnvs}
selectedEnv={selectedEnv!} selectedEnv={selectedEnv!}
workspaceId={workspaceId} workspaceId={workspaceId}
+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=''
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']);