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

File diff suppressed because one or more lines are too long

View File

@@ -35,14 +35,11 @@ export const getIntegrationAuth = async (req: Request, res: Response) => {
}); });
} }
export const getIntegrationOptions = async ( export const getIntegrationOptions = async (req: Request, res: Response) => {
req: Request, return res.status(200).send({
res: Response integrationOptions: INTEGRATION_OPTIONS,
) => { });
return res.status(200).send({ };
integrationOptions: INTEGRATION_OPTIONS
});
}
/** /**
* Perform OAuth2 code-token exchange as part of integration [integration] for workspace with id [workspaceId] * Perform OAuth2 code-token exchange as part of integration [integration] for workspace with id [workspaceId]
@@ -90,8 +87,8 @@ export const oAuthExchange = async (
* @param res * @param res
*/ */
export const saveIntegrationAccessToken = async ( export const saveIntegrationAccessToken = async (
req: Request, req: Request,
res: Response res: Response
) => { ) => {
// TODO: refactor // TODO: refactor
// TODO: check if access token is valid for each integration // TODO: check if access token is valid for each integration
@@ -157,23 +154,23 @@ export const saveIntegrationAccessToken = async (
* @returns * @returns
*/ */
export const getIntegrationAuthApps = async (req: Request, res: Response) => { export const getIntegrationAuthApps = async (req: Request, res: Response) => {
let apps; let apps;
try { try {
apps = await getApps({ apps = await getApps({
integrationAuth: req.integrationAuth, integrationAuth: req.integrationAuth,
accessToken: req.accessToken accessToken: req.accessToken,
}); });
} catch (err) { } catch (err) {
Sentry.setUser({ email: req.user.email }); Sentry.setUser({ email: req.user.email });
Sentry.captureException(err); Sentry.captureException(err);
return res.status(400).send({ return res.status(400).send({
message: 'Failed to get integration authorization applications' message: "Failed to get integration authorization applications",
}); });
} }
return res.status(200).send({ return res.status(200).send({
apps apps,
}); });
}; };
/** /**
@@ -183,21 +180,21 @@ export const getIntegrationAuthApps = async (req: Request, res: Response) => {
* @returns * @returns
*/ */
export const deleteIntegrationAuth = async (req: Request, res: Response) => { export const deleteIntegrationAuth = async (req: Request, res: Response) => {
let integrationAuth; let integrationAuth;
try { try {
integrationAuth = await revokeAccess({ integrationAuth = await revokeAccess({
integrationAuth: req.integrationAuth, integrationAuth: req.integrationAuth,
accessToken: req.accessToken accessToken: req.accessToken,
}); });
} catch (err) { } catch (err) {
Sentry.setUser({ email: req.user.email }); Sentry.setUser({ email: req.user.email });
Sentry.captureException(err); Sentry.captureException(err);
return res.status(400).send({ return res.status(400).send({
message: 'Failed to delete integration authorization' message: "Failed to delete integration authorization",
}); });
} }
return res.status(200).send({ return res.status(200).send({
integrationAuth integrationAuth,
}); });
} };

View File

@@ -12,9 +12,9 @@ import { eventPushSecrets } from '../../events';
/** /**
* Create/initialize an (empty) integration for integration authorization * Create/initialize an (empty) integration for integration authorization
* @param req * @param req
* @param res * @param res
* @returns * @returns
*/ */
export const createIntegration = async (req: Request, res: Response) => { export const createIntegration = async (req: Request, res: Response) => {
let integration; let integration;
@@ -65,10 +65,10 @@ export const createIntegration = async (req: Request, res: Response) => {
}); });
} }
return res.status(200).send({ return res.status(200).send({
integration integration,
}); });
} };
/** /**
* Change environment or name of integration with id [integrationId] * Change environment or name of integration with id [integrationId]
@@ -77,57 +77,57 @@ export const createIntegration = async (req: Request, res: Response) => {
* @returns * @returns
*/ */
export const updateIntegration = async (req: Request, res: Response) => { export const updateIntegration = async (req: Request, res: Response) => {
let integration; let integration;
// TODO: add integration-specific validation to ensure that each
// integration has the correct fields populated in [Integration]
try {
const {
environment,
isActive,
app,
appId,
targetEnvironment,
owner, // github-specific integration param
} = req.body;
integration = await Integration.findOneAndUpdate(
{
_id: req.integration._id
},
{
environment,
isActive,
app,
appId,
targetEnvironment,
owner
},
{
new: true
}
);
if (integration) {
// trigger event - push secrets
EventService.handleEvent({
event: eventPushSecrets({
workspaceId: integration.workspace.toString()
})
});
}
} catch (err) {
Sentry.setUser({ email: req.user.email });
Sentry.captureException(err);
return res.status(400).send({
message: 'Failed to update integration'
});
}
return res.status(200).send({ // TODO: add integration-specific validation to ensure that each
integration // integration has the correct fields populated in [Integration]
});
try {
const {
environment,
isActive,
app,
appId,
targetEnvironment,
owner, // github-specific integration param
} = req.body;
integration = await Integration.findOneAndUpdate(
{
_id: req.integration._id,
},
{
environment,
isActive,
app,
appId,
targetEnvironment,
owner,
},
{
new: true,
}
);
if (integration) {
// trigger event - push secrets
EventService.handleEvent({
event: eventPushSecrets({
workspaceId: integration.workspace.toString(),
}),
});
}
} catch (err) {
Sentry.setUser({ email: req.user.email });
Sentry.captureException(err);
return res.status(400).send({
message: "Failed to update integration",
});
}
return res.status(200).send({
integration,
});
}; };
/** /**
@@ -138,24 +138,24 @@ export const updateIntegration = async (req: Request, res: Response) => {
* @returns * @returns
*/ */
export const deleteIntegration = async (req: Request, res: Response) => { export const deleteIntegration = async (req: Request, res: Response) => {
let integration; let integration;
try { try {
const { integrationId } = req.params; const { integrationId } = req.params;
integration = await Integration.findOneAndDelete({ integration = await Integration.findOneAndDelete({
_id: integrationId _id: integrationId,
}); });
if (!integration) throw new Error('Failed to find integration'); if (!integration) throw new Error("Failed to find integration");
} catch (err) { } catch (err) {
Sentry.setUser({ email: req.user.email }); Sentry.setUser({ email: req.user.email });
Sentry.captureException(err); Sentry.captureException(err);
return res.status(400).send({ return res.status(400).send({
message: 'Failed to delete integration' message: "Failed to delete integration",
}); });
} }
return res.status(200).send({ return res.status(200).send({
integration integration,
}); });
}; };

View File

@@ -1,21 +1,21 @@
import { Request, Response } from 'express'; import { Request, Response } from "express";
import * as Sentry from '@sentry/node'; import * as Sentry from "@sentry/node";
import { import {
Workspace, Workspace,
Membership, Membership,
MembershipOrg, MembershipOrg,
Integration, Integration,
IntegrationAuth, IntegrationAuth,
IUser, IUser,
ServiceToken, ServiceToken,
ServiceTokenData ServiceTokenData,
} from '../../models'; } from "../../models";
import { import {
createWorkspace as create, createWorkspace as create,
deleteWorkspace as deleteWork deleteWorkspace as deleteWork,
} from '../../helpers/workspace'; } from "../../helpers/workspace";
import { addMemberships } from '../../helpers/membership'; import { addMemberships } from "../../helpers/membership";
import { ADMIN } from '../../variables'; import { ADMIN } from "../../variables";
/** /**
* Return public keys of members of workspace with id [workspaceId] * Return public keys of members of workspace with id [workspaceId]
@@ -24,32 +24,31 @@ import { ADMIN } from '../../variables';
* @returns * @returns
*/ */
export const getWorkspacePublicKeys = async (req: Request, res: Response) => { export const getWorkspacePublicKeys = async (req: Request, res: Response) => {
let publicKeys; let publicKeys;
try { try {
const { workspaceId } = req.params; const { workspaceId } = req.params;
publicKeys = ( publicKeys = (
await Membership.find({ await Membership.find({
workspace: workspaceId workspace: workspaceId,
}).populate<{ user: IUser }>('user', 'publicKey') }).populate<{ user: IUser }>("user", "publicKey")
) ).map((member) => {
.map((member) => { return {
return { publicKey: member.user.publicKey,
publicKey: member.user.publicKey, userId: member.user._id,
userId: member.user._id };
}; });
}); } catch (err) {
} catch (err) { Sentry.setUser({ email: req.user.email });
Sentry.setUser({ email: req.user.email }); Sentry.captureException(err);
Sentry.captureException(err); return res.status(400).send({
return res.status(400).send({ message: "Failed to get workspace member public keys",
message: 'Failed to get workspace member public keys' });
}); }
}
return res.status(200).send({ return res.status(200).send({
publicKeys publicKeys,
}); });
}; };
/** /**
@@ -59,24 +58,24 @@ export const getWorkspacePublicKeys = async (req: Request, res: Response) => {
* @returns * @returns
*/ */
export const getWorkspaceMemberships = async (req: Request, res: Response) => { export const getWorkspaceMemberships = async (req: Request, res: Response) => {
let users; let users;
try { try {
const { workspaceId } = req.params; const { workspaceId } = req.params;
users = await Membership.find({ users = await Membership.find({
workspace: workspaceId workspace: workspaceId,
}).populate('user', '+publicKey'); }).populate("user", "+publicKey");
} catch (err) { } catch (err) {
Sentry.setUser({ email: req.user.email }); Sentry.setUser({ email: req.user.email });
Sentry.captureException(err); Sentry.captureException(err);
return res.status(400).send({ return res.status(400).send({
message: 'Failed to get workspace members' message: "Failed to get workspace members",
}); });
} }
return res.status(200).send({ return res.status(200).send({
users users,
}); });
}; };
/** /**
@@ -86,24 +85,24 @@ export const getWorkspaceMemberships = async (req: Request, res: Response) => {
* @returns * @returns
*/ */
export const getWorkspaces = async (req: Request, res: Response) => { export const getWorkspaces = async (req: Request, res: Response) => {
let workspaces; let workspaces;
try { try {
workspaces = ( workspaces = (
await Membership.find({ await Membership.find({
user: req.user._id user: req.user._id,
}).populate('workspace') }).populate("workspace")
).map((m) => m.workspace); ).map((m) => m.workspace);
} catch (err) { } catch (err) {
Sentry.setUser({ email: req.user.email }); Sentry.setUser({ email: req.user.email });
Sentry.captureException(err); Sentry.captureException(err);
return res.status(400).send({ return res.status(400).send({
message: 'Failed to get workspaces' message: "Failed to get workspaces",
}); });
} }
return res.status(200).send({ return res.status(200).send({
workspaces workspaces,
}); });
}; };
/** /**
@@ -113,24 +112,24 @@ export const getWorkspaces = async (req: Request, res: Response) => {
* @returns * @returns
*/ */
export const getWorkspace = async (req: Request, res: Response) => { export const getWorkspace = async (req: Request, res: Response) => {
let workspace; let workspace;
try { try {
const { workspaceId } = req.params; const { workspaceId } = req.params;
workspace = await Workspace.findOne({ workspace = await Workspace.findOne({
_id: workspaceId _id: workspaceId,
}); });
} catch (err) { } catch (err) {
Sentry.setUser({ email: req.user.email }); Sentry.setUser({ email: req.user.email });
Sentry.captureException(err); Sentry.captureException(err);
return res.status(400).send({ return res.status(400).send({
message: 'Failed to get workspace' message: "Failed to get workspace",
}); });
} }
return res.status(200).send({ return res.status(200).send({
workspace workspace,
}); });
}; };
/** /**
@@ -141,46 +140,46 @@ export const getWorkspace = async (req: Request, res: Response) => {
* @returns * @returns
*/ */
export const createWorkspace = async (req: Request, res: Response) => { export const createWorkspace = async (req: Request, res: Response) => {
let workspace; let workspace;
try { try {
const { workspaceName, organizationId } = req.body; const { workspaceName, organizationId } = req.body;
// validate organization membership // validate organization membership
const membershipOrg = await MembershipOrg.findOne({ const membershipOrg = await MembershipOrg.findOne({
user: req.user._id, user: req.user._id,
organization: organizationId organization: organizationId,
}); });
if (!membershipOrg) { if (!membershipOrg) {
throw new Error('Failed to validate organization membership'); throw new Error("Failed to validate organization membership");
} }
if (workspaceName.length < 1) { if (workspaceName.length < 1) {
throw new Error('Workspace names must be at least 1-character long'); throw new Error("Workspace names must be at least 1-character long");
} }
// create workspace and add user as member // create workspace and add user as member
workspace = await create({ workspace = await create({
name: workspaceName, name: workspaceName,
organizationId organizationId,
}); });
await addMemberships({ await addMemberships({
userIds: [req.user._id], userIds: [req.user._id],
workspaceId: workspace._id.toString(), workspaceId: workspace._id.toString(),
roles: [ADMIN] roles: [ADMIN],
}); });
} catch (err) { } catch (err) {
Sentry.setUser({ email: req.user.email }); Sentry.setUser({ email: req.user.email });
Sentry.captureException(err); Sentry.captureException(err);
return res.status(400).send({ return res.status(400).send({
message: 'Failed to create workspace' message: "Failed to create workspace",
}); });
} }
return res.status(200).send({ return res.status(200).send({
workspace workspace,
}); });
}; };
/** /**
@@ -190,24 +189,24 @@ export const createWorkspace = async (req: Request, res: Response) => {
* @returns * @returns
*/ */
export const deleteWorkspace = async (req: Request, res: Response) => { export const deleteWorkspace = async (req: Request, res: Response) => {
try { try {
const { workspaceId } = req.params; const { workspaceId } = req.params;
// delete workspace // delete workspace
await deleteWork({ await deleteWork({
id: workspaceId id: workspaceId,
}); });
} catch (err) { } catch (err) {
Sentry.setUser({ email: req.user.email }); Sentry.setUser({ email: req.user.email });
Sentry.captureException(err); Sentry.captureException(err);
return res.status(400).send({ return res.status(400).send({
message: 'Failed to delete workspace' message: "Failed to delete workspace",
}); });
} }
return res.status(200).send({ return res.status(200).send({
message: 'Successfully deleted workspace' message: "Successfully deleted workspace",
}); });
}; };
/** /**
@@ -217,34 +216,34 @@ export const deleteWorkspace = async (req: Request, res: Response) => {
* @returns * @returns
*/ */
export const changeWorkspaceName = async (req: Request, res: Response) => { export const changeWorkspaceName = async (req: Request, res: Response) => {
let workspace; let workspace;
try { try {
const { workspaceId } = req.params; const { workspaceId } = req.params;
const { name } = req.body; const { name } = req.body;
workspace = await Workspace.findOneAndUpdate( workspace = await Workspace.findOneAndUpdate(
{ {
_id: workspaceId _id: workspaceId,
}, },
{ {
name name,
}, },
{ {
new: true new: true,
} }
); );
} catch (err) { } catch (err) {
Sentry.setUser({ email: req.user.email }); Sentry.setUser({ email: req.user.email });
Sentry.captureException(err); Sentry.captureException(err);
return res.status(400).send({ return res.status(400).send({
message: 'Failed to change workspace name' message: "Failed to change workspace name",
}); });
} }
return res.status(200).send({ return res.status(200).send({
message: 'Successfully changed workspace name', message: "Successfully changed workspace name",
workspace workspace,
}); });
}; };
/** /**
@@ -254,24 +253,24 @@ export const changeWorkspaceName = async (req: Request, res: Response) => {
* @returns * @returns
*/ */
export const getWorkspaceIntegrations = async (req: Request, res: Response) => { export const getWorkspaceIntegrations = async (req: Request, res: Response) => {
let integrations; let integrations;
try { try {
const { workspaceId } = req.params; const { workspaceId } = req.params;
integrations = await Integration.find({ integrations = await Integration.find({
workspace: workspaceId workspace: workspaceId,
}); });
} catch (err) { } catch (err) {
Sentry.setUser({ email: req.user.email }); Sentry.setUser({ email: req.user.email });
Sentry.captureException(err); Sentry.captureException(err);
return res.status(400).send({ return res.status(400).send({
message: 'Failed to get workspace integrations' message: "Failed to get workspace integrations",
}); });
} }
return res.status(200).send({ return res.status(200).send({
integrations integrations,
}); });
}; };
/** /**
@@ -281,56 +280,56 @@ export const getWorkspaceIntegrations = async (req: Request, res: Response) => {
* @returns * @returns
*/ */
export const getWorkspaceIntegrationAuthorizations = async ( export const getWorkspaceIntegrationAuthorizations = async (
req: Request, req: Request,
res: Response res: Response
) => { ) => {
let authorizations; let authorizations;
try { try {
const { workspaceId } = req.params; const { workspaceId } = req.params;
authorizations = await IntegrationAuth.find({ authorizations = await IntegrationAuth.find({
workspace: workspaceId workspace: workspaceId,
}); });
} catch (err) { } catch (err) {
Sentry.setUser({ email: req.user.email }); Sentry.setUser({ email: req.user.email });
Sentry.captureException(err); Sentry.captureException(err);
return res.status(400).send({ return res.status(400).send({
message: 'Failed to get workspace integration authorizations' message: "Failed to get workspace integration authorizations",
}); });
} }
return res.status(200).send({ return res.status(200).send({
authorizations authorizations,
}); });
}; };
/** /**
* Return service service tokens for workspace [workspaceId] belonging to user * Return service service tokens for workspace [workspaceId] belonging to user
* @param req * @param req
* @param res * @param res
* @returns * @returns
*/ */
export const getWorkspaceServiceTokens = async ( export const getWorkspaceServiceTokens = async (
req: Request, req: Request,
res: Response res: Response
) => { ) => {
let serviceTokens; let serviceTokens;
try { try {
const { workspaceId } = req.params; const { workspaceId } = req.params;
// ?? FIX. // ?? FIX.
serviceTokens = await ServiceToken.find({ serviceTokens = await ServiceToken.find({
user: req.user._id, user: req.user._id,
workspace: workspaceId workspace: workspaceId,
}); });
} catch (err) { } catch (err) {
Sentry.setUser({ email: req.user.email }); Sentry.setUser({ email: req.user.email });
Sentry.captureException(err); Sentry.captureException(err);
return res.status(400).send({ return res.status(400).send({
message: 'Failed to get workspace service tokens' message: "Failed to get workspace service tokens",
}); });
} }
return res.status(200).send({ return res.status(200).send({
serviceTokens serviceTokens,
}); });
} };

View File

@@ -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,31 +312,42 @@ 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) {
workspace: workspaceId, const workspaceFromDB = await Tag.find({ workspace: workspaceId })
environment,
$or: [ const tagIds = _.map(tagNamesList, (tagName) => {
{ user: userId }, const tag = _.find(workspaceFromDB, { slug: tagName });
{ user: { $exists: false } } return tag ? tag.id : null;
], });
type: { $in: [SECRET_SHARED, SECRET_PERSONAL] }
} secretQuery = {
) workspace: workspaceId,
.select("secretKeyCiphertext secretKeyIV secretKeyTag") environment,
$or: [
{ user: userId },
{ user: { $exists: false } }
],
tags: { $in: tagIds },
type: { $in: [SECRET_SHARED, SECRET_PERSONAL] }
}
} else { } else {
secrets = await Secret.find( secretQuery = {
{ workspace: workspaceId,
workspace: workspaceId, environment,
environment, $or: [
$or: [ { user: userId },
{ user: userId }, { user: { $exists: false } }
{ user: { $exists: false } } ],
], 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'])

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;
@@ -131,10 +138,10 @@ const getAppsHeroku = async ({ accessToken }: { accessToken: string }) => {
* @returns {Object[]} apps - names of Vercel apps * @returns {Object[]} apps - names of Vercel apps
* @returns {String} apps.name - name of Vercel app * @returns {String} apps.name - name of Vercel app
*/ */
const getAppsVercel = async ({ const getAppsVercel = async ({
integrationAuth, integrationAuth,
accessToken accessToken,
}: { }: {
integrationAuth: IIntegrationAuth; integrationAuth: IIntegrationAuth;
accessToken: string; accessToken: string;
}) => { }) => {
@@ -146,21 +153,23 @@ const getAppsVercel = async ({
Authorization: `Bearer ${accessToken}`, Authorization: `Bearer ${accessToken}`,
'Accept-Encoding': 'application/json' 'Accept-Encoding': 'application/json'
}, },
...( integrationAuth?.teamId ? { ...(integrationAuth?.teamId
params: { ? {
teamId: integrationAuth.teamId params: {
} teamId: integrationAuth.teamId,
} : {}) },
}
: {}),
}) })
).data; ).data;
apps = res.projects.map((a: any) => ({ apps = res.projects.map((a: any) => ({
name: a.name name: a.name,
})); }));
} catch (err) { } catch (err) {
Sentry.setUser(null); Sentry.setUser(null);
Sentry.captureException(err); Sentry.captureException(err);
throw new Error('Failed to get Vercel integration apps'); throw new Error("Failed to get Vercel integration apps");
} }
return apps; return apps;
@@ -173,11 +182,7 @@ const getAppsVercel = async ({
* @returns {Object[]} apps - names of Netlify sites * @returns {Object[]} apps - names of Netlify sites
* @returns {String} apps.name - name of Netlify site * @returns {String} apps.name - name of Netlify site
*/ */
const getAppsNetlify = async ({ const getAppsNetlify = async ({ accessToken }: { accessToken: string }) => {
accessToken
}: {
accessToken: string;
}) => {
let apps; let apps;
try { try {
const res = ( const res = (
@@ -191,12 +196,12 @@ const getAppsNetlify = async ({
apps = res.map((a: any) => ({ apps = res.map((a: any) => ({
name: a.name, name: a.name,
appId: a.site_id appId: a.site_id,
})); }));
} catch (err) { } catch (err) {
Sentry.setUser(null); Sentry.setUser(null);
Sentry.captureException(err); Sentry.captureException(err);
throw new Error('Failed to get Netlify integration apps'); throw new Error("Failed to get Netlify integration apps");
} }
return apps; return apps;
@@ -209,35 +214,32 @@ const getAppsNetlify = async ({
* @returns {Object[]} apps - names of Netlify sites * @returns {Object[]} apps - names of Netlify sites
* @returns {String} apps.name - name of Netlify site * @returns {String} apps.name - name of Netlify site
*/ */
const getAppsGithub = async ({ const getAppsGithub = async ({ accessToken }: { accessToken: string }) => {
accessToken
}: {
accessToken: string;
}) => {
let apps; let apps;
try { try {
const octokit = new Octokit({ const octokit = new Octokit({
auth: accessToken auth: accessToken,
}); });
const repos = (await octokit.request( const repos = (
'GET /user/repos{?visibility,affiliation,type,sort,direction,per_page,page,since,before}', await octokit.request(
{ "GET /user/repos{?visibility,affiliation,type,sort,direction,per_page,page,since,before}",
per_page: 100 {
} per_page: 100,
)).data; }
)
).data;
apps = repos apps = repos
.filter((a:any) => a.permissions.admin === true) .filter((a: any) => a.permissions.admin === true)
.map((a: any) => ({ .map((a: any) => ({
name: a.name, name: a.name,
owner: a.owner.login owner: a.owner.login,
}) }));
);
} catch (err) { } catch (err) {
Sentry.setUser(null); Sentry.setUser(null);
Sentry.captureException(err); Sentry.captureException(err);
throw new Error('Failed to get Github repos'); throw new Error("Failed to get Github repos");
} }
return apps; return apps;
@@ -251,11 +253,7 @@ const getAppsGithub = async ({
* @returns {String} apps.name - name of Render service * @returns {String} apps.name - name of Render service
* @returns {String} apps.appId - id of Render service * @returns {String} apps.appId - id of Render service
*/ */
const getAppsRender = async ({ const getAppsRender = async ({ accessToken }: { accessToken: string }) => {
accessToken
}: {
accessToken: string;
}) => {
let apps: any; let apps: any;
try { try {
const res = ( const res = (
@@ -263,8 +261,8 @@ const getAppsRender = async ({
headers: { headers: {
Authorization: `Bearer ${accessToken}`, Authorization: `Bearer ${accessToken}`,
Accept: 'application/json', Accept: 'application/json',
'Accept-Encoding': 'application/json' 'Accept-Encoding': 'application/json',
} },
}) })
).data; ).data;
@@ -277,11 +275,11 @@ const getAppsRender = async ({
} catch (err) { } catch (err) {
Sentry.setUser(null); Sentry.setUser(null);
Sentry.captureException(err); Sentry.captureException(err);
throw new Error('Failed to get Render services'); throw new Error("Failed to get Render services");
} }
return apps; return apps;
} };
/** /**
* Return list of apps for Fly.io integration * Return list of apps for Fly.io integration
@@ -290,11 +288,7 @@ const getAppsRender = async ({
* @returns {Object[]} apps - names and ids of Fly.io apps * @returns {Object[]} apps - names and ids of Fly.io apps
* @returns {String} apps.name - name of Fly.io apps * @returns {String} apps.name - name of Fly.io apps
*/ */
const getAppsFlyio = async ({ const getAppsFlyio = async ({ accessToken }: { accessToken: string }) => {
accessToken
}: {
accessToken: string;
}) => {
let apps; let apps;
try { try {
const query = ` const query = `
@@ -308,34 +302,71 @@ const getAppsFlyio = async ({
} }
} }
`; `;
const res = (await axios({ const res = (
url: INTEGRATION_FLYIO_API_URL, await axios({
method: 'post', url: INTEGRATION_FLYIO_API_URL,
headers: { method: "post",
'Authorization': 'Bearer ' + accessToken, headers: {
Authorization: "Bearer " + accessToken,
'Accept': 'application/json', 'Accept': 'application/json',
'Accept-Encoding': 'application/json' 'Accept-Encoding': 'application/json',
}, },
data: { data: {
query, query,
variables: { variables: {
role: null role: null,
} },
} },
})).data.data.apps.nodes; })
).data.data.apps.nodes;
apps = res
.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 Fly.io apps'); throw new Error("Failed to get Fly.io 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; return apps;
} };
export { getApps }; export { getApps };

File diff suppressed because it is too large Load Diff

View File

@@ -1,4 +1,4 @@
import { Schema, model, Types } from 'mongoose'; import { Schema, model, Types } from "mongoose";
import { import {
INTEGRATION_AZURE_KEY_VAULT, INTEGRATION_AZURE_KEY_VAULT,
INTEGRATION_AWS_PARAMETER_STORE, INTEGRATION_AWS_PARAMETER_STORE,
@@ -8,8 +8,9 @@ import {
INTEGRATION_NETLIFY, INTEGRATION_NETLIFY,
INTEGRATION_GITHUB, INTEGRATION_GITHUB,
INTEGRATION_RENDER, INTEGRATION_RENDER,
INTEGRATION_FLYIO INTEGRATION_FLYIO,
} from '../variables'; INTEGRATION_CIRCLECI,
} from "../variables";
export interface IIntegration { export interface IIntegration {
_id: Types.ObjectId; _id: Types.ObjectId;
@@ -31,44 +32,47 @@ export interface IIntegration {
| 'netlify' | 'netlify'
| 'github' | 'github'
| 'render' | 'render'
| 'flyio'; | 'flyio'
| 'circleci';
integrationAuth: Types.ObjectId; integrationAuth: Types.ObjectId;
} }
const integrationSchema = new Schema<IIntegration>( const integrationSchema = new Schema<IIntegration>(
{ {
workspace: { workspace: {
type: Schema.Types.ObjectId, type: Schema.Types.ObjectId,
ref: 'Workspace', ref: "Workspace",
required: true required: true,
}, },
environment: { environment: {
type: String, type: String,
required: true required: true,
}, },
isActive: { isActive: {
type: Boolean, type: Boolean,
required: true required: true,
}, },
app: { app: {
// name of app in provider // name of app in provider
type: String, type: String,
default: null default: null,
}, },
appId: { // (new) appId: {
// (new)
// id of app in provider // id of app in provider
type: String, type: String,
default: null default: null,
}, },
targetEnvironment: { // (new) targetEnvironment: {
// target environment // (new)
// 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;

View File

@@ -1,4 +1,4 @@
import { Schema, model, Types } from 'mongoose'; import { Schema, model, Types } from "mongoose";
import { import {
INTEGRATION_AZURE_KEY_VAULT, INTEGRATION_AZURE_KEY_VAULT,
INTEGRATION_AWS_PARAMETER_STORE, INTEGRATION_AWS_PARAMETER_STORE,
@@ -8,24 +8,16 @@ import {
INTEGRATION_NETLIFY, INTEGRATION_NETLIFY,
INTEGRATION_GITHUB, INTEGRATION_GITHUB,
INTEGRATION_RENDER, INTEGRATION_RENDER,
INTEGRATION_FLYIO INTEGRATION_FLYIO,
} from '../variables'; INTEGRATION_CIRCLECI,
} from "../variables";
export interface IIntegrationAuth { export interface IIntegrationAuth {
_id: Types.ObjectId; _id: Types.ObjectId;
workspace: Types.ObjectId; workspace: Types.ObjectId;
integration: integration: 'heroku' | 'vercel' | 'netlify' | 'github' | 'render' | 'flyio' | 'azure-key-vault' | 'circleci' | 'aws-parameter-store' | 'aws-secret-manager';
| 'azure-key-vault' teamId: string;
| 'aws-parameter-store' accountId: string;
| 'aws-secret-manager'
| 'heroku'
| 'vercel'
| 'netlify'
| 'github'
| 'render'
| 'flyio';
teamId: string; // TODO: deprecate (vercel) -> move to accessId
accountId: string; // TODO: deprecate (netlify) -> move to accessId
refreshCiphertext?: string; refreshCiphertext?: string;
refreshIV?: string; refreshIV?: string;
refreshTag?: string; refreshTag?: string;
@@ -41,9 +33,9 @@ export interface IIntegrationAuth {
const integrationAuthSchema = new Schema<IIntegrationAuth>( const integrationAuthSchema = new Schema<IIntegrationAuth>(
{ {
workspace: { workspace: {
type: Schema.Types.ObjectId, type: Schema.Types.ObjectId,
ref: 'Workspace', ref: "Workspace",
required: true required: true,
}, },
integration: { integration: {
type: String, type: String,
@@ -56,29 +48,30 @@ const integrationAuthSchema = new Schema<IIntegrationAuth>(
INTEGRATION_NETLIFY, INTEGRATION_NETLIFY,
INTEGRATION_GITHUB, INTEGRATION_GITHUB,
INTEGRATION_RENDER, INTEGRATION_RENDER,
INTEGRATION_FLYIO INTEGRATION_FLYIO,
INTEGRATION_CIRCLECI,
], ],
required: true required: true,
}, },
teamId: { teamId: {
// vercel-specific integration param // vercel-specific integration param
type: String type: String,
}, },
accountId: { accountId: {
// netlify-specific integration param // netlify-specific integration param
type: String type: String,
}, },
refreshCiphertext: { refreshCiphertext: {
type: String, type: String,
select: false select: false,
}, },
refreshIV: { refreshIV: {
type: String, type: String,
select: false select: false,
}, },
refreshTag: { refreshTag: {
type: String, type: String,
select: false select: false,
}, },
accessIdCiphertext: { accessIdCiphertext: {
type: String, type: String,
@@ -94,28 +87,28 @@ const integrationAuthSchema = new Schema<IIntegrationAuth>(
}, },
accessCiphertext: { accessCiphertext: {
type: String, type: String,
select: false select: false,
}, },
accessIV: { accessIV: {
type: String, type: String,
select: false select: false,
}, },
accessTag: { accessTag: {
type: String, type: String,
select: false select: false,
}, },
accessExpiresAt: { accessExpiresAt: {
type: Date, type: Date,
select: false select: false,
} },
}, },
{ {
timestamps: true timestamps: true,
} }
); );
const IntegrationAuth = model<IIntegrationAuth>( const IntegrationAuth = model<IIntegrationAuth>(
'IntegrationAuth', "IntegrationAuth",
integrationAuthSchema integrationAuthSchema
); );

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: {

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']

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,

View File

@@ -3,50 +3,53 @@ import {
TENANT_ID_AZURE TENANT_ID_AZURE
} from '../config'; } from '../config';
import { import {
CLIENT_ID_HEROKU, CLIENT_ID_HEROKU,
CLIENT_ID_NETLIFY, CLIENT_ID_NETLIFY,
CLIENT_ID_GITHUB, CLIENT_ID_GITHUB,
CLIENT_SLUG_VERCEL CLIENT_SLUG_VERCEL,
} from '../config'; } from "../config";
// integrations // integrations
const INTEGRATION_AZURE_KEY_VAULT = 'azure-key-vault'; const INTEGRATION_AZURE_KEY_VAULT = 'azure-key-vault';
const INTEGRATION_AWS_PARAMETER_STORE = 'aws-parameter-store'; const INTEGRATION_AWS_PARAMETER_STORE = 'aws-parameter-store';
const INTEGRATION_AWS_SECRET_MANAGER = 'aws-secret-manager'; const INTEGRATION_AWS_SECRET_MANAGER = 'aws-secret-manager';
const INTEGRATION_HEROKU = 'heroku'; const INTEGRATION_HEROKU = "heroku";
const INTEGRATION_VERCEL = 'vercel'; const INTEGRATION_VERCEL = "vercel";
const INTEGRATION_NETLIFY = 'netlify'; const INTEGRATION_NETLIFY = "netlify";
const INTEGRATION_GITHUB = 'github'; const INTEGRATION_GITHUB = "github";
const INTEGRATION_RENDER = 'render'; const INTEGRATION_RENDER = "render";
const INTEGRATION_FLYIO = 'flyio'; const INTEGRATION_FLYIO = "flyio";
const INTEGRATION_CIRCLECI = "circleci";
const INTEGRATION_SET = new Set([ const INTEGRATION_SET = new Set([
INTEGRATION_AZURE_KEY_VAULT, INTEGRATION_AZURE_KEY_VAULT,
INTEGRATION_HEROKU, INTEGRATION_HEROKU,
INTEGRATION_VERCEL, INTEGRATION_VERCEL,
INTEGRATION_NETLIFY, INTEGRATION_NETLIFY,
INTEGRATION_GITHUB, INTEGRATION_GITHUB,
INTEGRATION_RENDER, INTEGRATION_RENDER,
INTEGRATION_FLYIO INTEGRATION_FLYIO,
INTEGRATION_CIRCLECI,
]); ]);
// integration types // integration types
const INTEGRATION_OAUTH2 = 'oauth2'; const INTEGRATION_OAUTH2 = "oauth2";
// integration oauth endpoints // integration oauth endpoints
const INTEGRATION_AZURE_TOKEN_URL = `https://login.microsoftonline.com/${TENANT_ID_AZURE}/oauth2/v2.0/token`; const INTEGRATION_AZURE_TOKEN_URL = `https://login.microsoftonline.com/${TENANT_ID_AZURE}/oauth2/v2.0/token`;
const INTEGRATION_HEROKU_TOKEN_URL = 'https://id.heroku.com/oauth/token'; const INTEGRATION_HEROKU_TOKEN_URL = 'https://id.heroku.com/oauth/token';
const INTEGRATION_VERCEL_TOKEN_URL = const INTEGRATION_VERCEL_TOKEN_URL =
'https://api.vercel.com/v2/oauth/access_token'; "https://api.vercel.com/v2/oauth/access_token";
const INTEGRATION_NETLIFY_TOKEN_URL = 'https://api.netlify.com/oauth/token'; const INTEGRATION_NETLIFY_TOKEN_URL = "https://api.netlify.com/oauth/token";
const INTEGRATION_GITHUB_TOKEN_URL = const INTEGRATION_GITHUB_TOKEN_URL =
'https://github.com/login/oauth/access_token'; "https://github.com/login/oauth/access_token";
// integration apps endpoints // integration apps endpoints
const INTEGRATION_HEROKU_API_URL = 'https://api.heroku.com'; const INTEGRATION_HEROKU_API_URL = "https://api.heroku.com";
const INTEGRATION_VERCEL_API_URL = 'https://api.vercel.com'; const INTEGRATION_VERCEL_API_URL = "https://api.vercel.com";
const INTEGRATION_NETLIFY_API_URL = 'https://api.netlify.com'; const INTEGRATION_NETLIFY_API_URL = "https://api.netlify.com";
const INTEGRATION_RENDER_API_URL = 'https://api.render.com'; const INTEGRATION_RENDER_API_URL = "https://api.render.com";
const INTEGRATION_FLYIO_API_URL = 'https://api.fly.io/graphql'; const INTEGRATION_FLYIO_API_URL = "https://api.fly.io/graphql";
const INTEGRATION_CIRCLECI_API_URL = "https://circleci.com/api";
const INTEGRATION_OPTIONS = [ const INTEGRATION_OPTIONS = [
{ {
@@ -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: ''
} }
] ]
@@ -165,23 +168,25 @@ export {
INTEGRATION_AZURE_KEY_VAULT, INTEGRATION_AZURE_KEY_VAULT,
INTEGRATION_AWS_PARAMETER_STORE, INTEGRATION_AWS_PARAMETER_STORE,
INTEGRATION_AWS_SECRET_MANAGER, INTEGRATION_AWS_SECRET_MANAGER,
INTEGRATION_HEROKU, INTEGRATION_HEROKU,
INTEGRATION_VERCEL, INTEGRATION_VERCEL,
INTEGRATION_NETLIFY, INTEGRATION_NETLIFY,
INTEGRATION_GITHUB, INTEGRATION_GITHUB,
INTEGRATION_RENDER, INTEGRATION_RENDER,
INTEGRATION_FLYIO, INTEGRATION_FLYIO,
INTEGRATION_SET, INTEGRATION_CIRCLECI,
INTEGRATION_OAUTH2, INTEGRATION_SET,
INTEGRATION_OAUTH2,
INTEGRATION_AZURE_TOKEN_URL, INTEGRATION_AZURE_TOKEN_URL,
INTEGRATION_HEROKU_TOKEN_URL, INTEGRATION_HEROKU_TOKEN_URL,
INTEGRATION_VERCEL_TOKEN_URL, INTEGRATION_VERCEL_TOKEN_URL,
INTEGRATION_NETLIFY_TOKEN_URL, INTEGRATION_NETLIFY_TOKEN_URL,
INTEGRATION_GITHUB_TOKEN_URL, INTEGRATION_GITHUB_TOKEN_URL,
INTEGRATION_HEROKU_API_URL, INTEGRATION_HEROKU_API_URL,
INTEGRATION_VERCEL_API_URL, INTEGRATION_VERCEL_API_URL,
INTEGRATION_NETLIFY_API_URL, INTEGRATION_NETLIFY_API_URL,
INTEGRATION_RENDER_API_URL, INTEGRATION_RENDER_API_URL,
INTEGRATION_FLYIO_API_URL, INTEGRATION_FLYIO_API_URL,
INTEGRATION_OPTIONS INTEGRATION_CIRCLECI_API_URL,
INTEGRATION_OPTIONS,
}; };

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
}

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
} }

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 {

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

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

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)
} }

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
} }

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]

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 = {

View File

@@ -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 &&

View File

@@ -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>

View File

@@ -45,8 +45,8 @@ type Props = {
handleDeleteIntegration: (args: { integration: Integration }) => void; handleDeleteIntegration: (args: { integration: Integration }) => void;
}; };
const IntegrationTile = ({ const IntegrationTile = ({
integration, integration,
integrations, integrations,
bot, bot,
setBot, setBot,
@@ -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,11 +69,10 @@ 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);
if (integration?.app) { if (integration?.app) {
@@ -90,15 +89,16 @@ const IntegrationTile = ({
case 'vercel': case 'vercel':
setIntegrationTargetEnvironment( setIntegrationTargetEnvironment(
integration?.targetEnvironment integration?.targetEnvironment
? integration.targetEnvironment.charAt(0).toUpperCase() + integration.targetEnvironment.substring(1) ? integration.targetEnvironment.charAt(0).toUpperCase() +
: 'Development' integration.targetEnvironment.substring(1)
: 'Development'
); );
break; break;
case 'netlify': case 'netlify':
setIntegrationTargetEnvironment( setIntegrationTargetEnvironment(
integration?.targetEnvironment integration?.targetEnvironment
? contextNetlifyMapping[integration.targetEnvironment] ? contextNetlifyMapping[integration.targetEnvironment]
: 'Local development' : 'Local development'
); );
break; break;
default: default:
@@ -108,7 +108,7 @@ const IntegrationTile = ({
loadIntegration(); loadIntegration();
}, []); }, []);
const handleStartIntegration = async () => { const handleStartIntegration = async () => {
const reformatTargetEnvironment = (targetEnvironment: string) => { const reformatTargetEnvironment = (targetEnvironment: string) => {
switch (integration.integration) { switch (integration.integration) {
@@ -119,13 +119,13 @@ 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
const appId = siteApp?.appId ?? null; const appId = siteApp?.appId ?? null;
const owner = siteApp?.owner ?? null; const owner = siteApp?.owner ?? null;
// return updated integration // return updated integration
const updatedIntegration = await updateIntegration({ const updatedIntegration = await updateIntegration({
integrationId: integration._id, integrationId: integration._id,
@@ -136,15 +136,15 @@ const IntegrationTile = ({
targetEnvironment: reformatTargetEnvironment(integrationTargetEnvironment), targetEnvironment: reformatTargetEnvironment(integrationTargetEnvironment),
owner owner
}); });
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) => {
try { try {
@@ -152,7 +152,7 @@ const IntegrationTile = ({
case 'vercel': case 'vercel':
return ( return (
<div> <div>
<div className="text-gray-400 text-xs font-semibold mb-2 w-60">ENVIRONMENT</div> <div className="mb-2 w-60 text-xs font-semibold text-gray-400">ENVIRONMENT</div>
<ListBox <ListBox
data={!integration.isActive ? ['Development', 'Preview', 'Production'] : null} data={!integration.isActive ? ['Development', 'Preview', 'Production'] : null}
isSelected={integrationTargetEnvironment} isSelected={integrationTargetEnvironment}
@@ -164,7 +164,7 @@ const IntegrationTile = ({
case 'netlify': case 'netlify':
return ( return (
<div> <div>
<div className="text-gray-400 text-xs font-semibold mb-2">CONTEXT</div> <div className="mb-2 text-xs font-semibold text-gray-400">CONTEXT</div>
<ListBox <ListBox
data={ data={
!integration.isActive !integration.isActive
@@ -189,10 +189,10 @@ const IntegrationTile = ({
if (!integrationApp) return <div />; if (!integrationApp) return <div />;
return ( return (
<div className="max-w-5xl p-6 mx-6 mb-8 rounded-md bg-white/5 flex justify-between"> <div className="mx-6 mb-8 flex max-w-5xl justify-between rounded-md bg-white/5 p-6">
<div className="flex"> <div className="flex">
<div> <div>
<p className="text-gray-400 text-xs font-semibold mb-2">ENVIRONMENT</p> <p className="mb-2 text-xs font-semibold text-gray-400">ENVIRONMENT</p>
<ListBox <ListBox
data={!integration.isActive ? environments.map(({ name }) => name) : null} data={!integration.isActive ? environments.map(({ name }) => name) : null}
isSelected={integrationEnvironment.name} isSelected={integrationEnvironment.name}
@@ -208,7 +208,7 @@ const IntegrationTile = ({
/> />
</div> </div>
<div className="pt-2"> <div className="pt-2">
<FontAwesomeIcon icon={faArrowRight} className="mx-4 text-gray-400 mt-8" /> <FontAwesomeIcon icon={faArrowRight} className="mx-4 mt-8 text-gray-400" />
</div> </div>
<div className="mr-2"> <div className="mr-2">
<p className="text-gray-400 text-xs font-semibold mb-2">INTEGRATION</p> <p className="text-gray-400 text-xs font-semibold mb-2">INTEGRATION</p>
@@ -218,7 +218,7 @@ const IntegrationTile = ({
</div> </div>
</div> </div>
<div className="mr-2"> <div className="mr-2">
<div className="text-gray-400 text-xs font-semibold mb-2">APP</div> <div className="mb-2 text-xs font-semibold text-gray-400">APP</div>
<ListBox <ListBox
data={!integration.isActive ? apps.map((app) => app.name) : null} data={!integration.isActive ? apps.map((app) => app.name) : null}
isSelected={integrationApp} isSelected={integrationApp}
@@ -231,9 +231,9 @@ const IntegrationTile = ({
</div> </div>
<div className="flex items-end"> <div className="flex items-end">
{integration.isActive ? ( {integration.isActive ? (
<div className="max-w-5xl flex flex-row items-center bg-white/5 p-2 rounded-md px-4"> <div className="flex max-w-5xl flex-row items-center rounded-md bg-white/5 p-2 px-4">
<FontAwesomeIcon icon={faRotate} className="text-lg mr-2.5 text-primary animate-spin" /> <FontAwesomeIcon icon={faRotate} className="mr-2.5 animate-spin text-lg text-primary" />
<div className="text-gray-300 font-semibold">In Sync</div> <div className="font-semibold text-gray-300">In Sync</div>
</div> </div>
) : ( ) : (
<Button <Button
@@ -243,11 +243,13 @@ const IntegrationTile = ({
size="md" size="md"
/> />
)} )}
<div className="opacity-50 hover:opacity-100 duration-200 ml-2"> <div className="ml-2 opacity-50 duration-200 hover:opacity-100">
<Button <Button
onButtonPressed={() => handleDeleteIntegration({ onButtonPressed={() =>
integration handleDeleteIntegration({
})} integration
})
}
color="red" color="red"
size="icon-md" size="icon-md"
icon={faX} icon={faX}

View File

@@ -26,7 +26,7 @@ interface Integration {
} }
const ProjectIntegrationSection = ({ const ProjectIntegrationSection = ({
integrations, integrations,
setIntegrations, setIntegrations,
bot, bot,
setBot, setBot,
@@ -35,22 +35,20 @@ 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}
setBot={setBot} setBot={setBot}
setIntegrations={setIntegrations} setIntegrations={setIntegrations}
environments={environments} environments={environments}
handleDeleteIntegration={handleDeleteIntegration} handleDeleteIntegration={handleDeleteIntegration}
/> />
); );
@@ -59,6 +57,6 @@ const ProjectIntegrationSection = ({
) : ( ) : (
<div /> <div />
); );
} };
export default ProjectIntegrationSection; export default ProjectIntegrationSection;

View File

@@ -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',

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',

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}

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?';

View File

@@ -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]

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>

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';

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,
};
}

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,50 +219,58 @@ 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">
<Select Project
defaultValue={currentWorkspace?._id} </p>
value={currentWorkspace?._id} <Select
className="w-full py-2.5 bg-mineshaft-600 font-medium" defaultValue={currentWorkspace?._id}
onValueChange={(value) => { value={currentWorkspace?._id}
router.push(`/dashboard/${value}`); className="w-full py-2.5 bg-mineshaft-600 font-medium"
}} onValueChange={(value) => {
position="popper" router.push(`/dashboard/${value}`);
dropdownContainerClassName="left-0 text-bunker-200 bg-mineshaft-800 border border-mineshaft-600 z-50" }}
> position="popper"
{workspaces.map(({ _id, name }) => ( dropdownContainerClassName="text-bunker-200 bg-mineshaft-800 border border-mineshaft-600 z-50"
<SelectItem key={`ws-layout-list-${_id}`} value={_id} className={`${currentWorkspace?._id === _id && "bg-mineshaft-600"}`}> >
{name} {workspaces.map(({ _id, name }) => (
</SelectItem> <SelectItem
))} key={`ws-layout-list-${_id}`}
<hr className="mt-1 mb-1 h-px border-0 bg-gray-700" /> value={_id}
<div className="w-full"> className={`${currentWorkspace?._id === _id && 'bg-mineshaft-600'}`}
<Button >
className="w-full py-2 text-bunker-200 bg-mineshaft-500 hover:bg-primary/90 hover:text-black" {name}
color="mineshaft" </SelectItem>
size="sm" ))}
onClick={() => handlePopUpOpen('addNewWs')} <hr className="mt-1 mb-1 h-px border-0 bg-gray-700" />
leftIcon={<FontAwesomeIcon icon={faPlus} />} <div className="w-full">
> <Button
Add Project className="w-full py-2 text-bunker-200 bg-mineshaft-500 hover:bg-primary/90 hover:text-black"
</Button> color="mineshaft"
</div> size="sm"
</Select> onClick={() => handlePopUpOpen('addNewWs')}
</div> leftIcon={<FontAwesomeIcon icon={faPlus} />}
: <div className="w-full p-4 mt-3 mb-4"> >
<Button Add Project
className="w-full py-2 text-bunker-200 bg-mineshaft-500 hover:bg-primary/90 hover:text-black" </Button>
color="mineshaft" </div>
size="sm" </Select>
onClick={() => handlePopUpOpen('addNewWs')} </div>
leftIcon={<FontAwesomeIcon icon={faPlus} />} ) : (
> <div className="w-full p-4 mt-3 mb-4">
Add Project <Button
</Button> className="w-full py-2 text-bunker-200 bg-mineshaft-500 hover:bg-primary/90 hover:text-black"
</div>} color="mineshaft"
<div className={`${currentWorkspace ? "block" : "hidden"}`}> size="sm"
onClick={() => handlePopUpOpen('addNewWs')}
leftIcon={<FontAwesomeIcon icon={faPlus} />}
>
Add Project
</Button>
</div>
)}
<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>

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}

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;
} }

View File

@@ -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']);

View File

@@ -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']);