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 (
req: Request,
res: Response
) => {
return res.status(200).send({
integrationOptions: INTEGRATION_OPTIONS
});
}
export const getIntegrationOptions = async (req: Request, res: Response) => {
return res.status(200).send({
integrationOptions: INTEGRATION_OPTIONS,
});
};
/**
* 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
*/
export const saveIntegrationAccessToken = async (
req: Request,
res: Response
req: Request,
res: Response
) => {
// TODO: refactor
// TODO: check if access token is valid for each integration
@@ -157,23 +154,23 @@ export const saveIntegrationAccessToken = async (
* @returns
*/
export const getIntegrationAuthApps = async (req: Request, res: Response) => {
let apps;
try {
apps = await getApps({
integrationAuth: req.integrationAuth,
accessToken: req.accessToken
});
} catch (err) {
Sentry.setUser({ email: req.user.email });
Sentry.captureException(err);
return res.status(400).send({
message: 'Failed to get integration authorization applications'
});
}
let apps;
try {
apps = await getApps({
integrationAuth: req.integrationAuth,
accessToken: req.accessToken,
});
} catch (err) {
Sentry.setUser({ email: req.user.email });
Sentry.captureException(err);
return res.status(400).send({
message: "Failed to get integration authorization applications",
});
}
return res.status(200).send({
apps
});
return res.status(200).send({
apps,
});
};
/**
@@ -183,21 +180,21 @@ export const getIntegrationAuthApps = async (req: Request, res: Response) => {
* @returns
*/
export const deleteIntegrationAuth = async (req: Request, res: Response) => {
let integrationAuth;
try {
integrationAuth = await revokeAccess({
integrationAuth: req.integrationAuth,
accessToken: req.accessToken
});
} catch (err) {
Sentry.setUser({ email: req.user.email });
Sentry.captureException(err);
return res.status(400).send({
message: 'Failed to delete integration authorization'
});
}
return res.status(200).send({
integrationAuth
});
}
let integrationAuth;
try {
integrationAuth = await revokeAccess({
integrationAuth: req.integrationAuth,
accessToken: req.accessToken,
});
} catch (err) {
Sentry.setUser({ email: req.user.email });
Sentry.captureException(err);
return res.status(400).send({
message: "Failed to delete integration authorization",
});
}
return res.status(200).send({
integrationAuth,
});
};

View File

@@ -12,9 +12,9 @@ import { eventPushSecrets } from '../../events';
/**
* Create/initialize an (empty) integration for integration authorization
* @param req
* @param res
* @returns
* @param req
* @param res
* @returns
*/
export const createIntegration = async (req: Request, res: Response) => {
let integration;
@@ -65,10 +65,10 @@ export const createIntegration = async (req: Request, res: Response) => {
});
}
return res.status(200).send({
integration
});
}
return res.status(200).send({
integration,
});
};
/**
* Change environment or name of integration with id [integrationId]
@@ -77,57 +77,57 @@ export const createIntegration = async (req: Request, res: Response) => {
* @returns
*/
export const updateIntegration = async (req: Request, res: Response) => {
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'
});
}
let integration;
return res.status(200).send({
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({
integration,
});
};
/**
@@ -138,24 +138,24 @@ export const updateIntegration = async (req: Request, res: Response) => {
* @returns
*/
export const deleteIntegration = async (req: Request, res: Response) => {
let integration;
try {
const { integrationId } = req.params;
let integration;
try {
const { integrationId } = req.params;
integration = await Integration.findOneAndDelete({
_id: integrationId
});
if (!integration) throw new Error('Failed to find integration');
} catch (err) {
Sentry.setUser({ email: req.user.email });
Sentry.captureException(err);
return res.status(400).send({
message: 'Failed to delete integration'
});
}
return res.status(200).send({
integration
});
integration = await Integration.findOneAndDelete({
_id: integrationId,
});
if (!integration) throw new Error("Failed to find integration");
} catch (err) {
Sentry.setUser({ email: req.user.email });
Sentry.captureException(err);
return res.status(400).send({
message: "Failed to delete integration",
});
}
return res.status(200).send({
integration,
});
};

View File

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

View File

@@ -18,6 +18,8 @@ import { postHogClient } from '../../services';
import { getChannelFromUserAgent } from '../../utils/posthog';
import { ABILITY_READ, ABILITY_WRITE } from '../../variables/organization';
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]
@@ -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 userEmail = "" // used for posthog
if (req.user) {
@@ -308,31 +312,42 @@ export const getSecrets = async (req: Request, res: Response) => {
}
}
let secrets: any
if (hasWriteOnlyAccess) {
secrets = await Secret.find(
{
workspace: workspaceId,
environment,
$or: [
{ user: userId },
{ user: { $exists: false } }
],
type: { $in: [SECRET_SHARED, SECRET_PERSONAL] }
}
)
.select("secretKeyCiphertext secretKeyIV secretKeyTag")
let secretQuery: any
if (tagNamesList != undefined && tagNamesList.length != 0) {
const workspaceFromDB = await Tag.find({ workspace: workspaceId })
const tagIds = _.map(tagNamesList, (tagName) => {
const tag = _.find(workspaceFromDB, { slug: tagName });
return tag ? tag.id : null;
});
secretQuery = {
workspace: workspaceId,
environment,
$or: [
{ user: userId },
{ user: { $exists: false } }
],
tags: { $in: tagIds },
type: { $in: [SECRET_SHARED, SECRET_PERSONAL] }
}
} else {
secrets = await Secret.find(
{
workspace: workspaceId,
environment,
$or: [
{ user: userId },
{ user: { $exists: false } }
],
type: { $in: [SECRET_SHARED, SECRET_PERSONAL] }
}
).populate("tags")
secretQuery = {
workspace: workspaceId,
environment,
$or: [
{ user: userId },
{ user: { $exists: false } }
],
type: { $in: [SECRET_SHARED, SECRET_PERSONAL] }
}
}
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'])

View File

@@ -1,7 +1,7 @@
import axios from 'axios';
import * as Sentry from '@sentry/node';
import { Octokit } from '@octokit/rest';
import { IIntegrationAuth } from '../models';
import axios from "axios";
import * as Sentry from "@sentry/node";
import { Octokit } from "@octokit/rest";
import { IIntegrationAuth } from "../models";
import {
INTEGRATION_AZURE_KEY_VAULT,
INTEGRATION_AWS_PARAMETER_STORE,
@@ -12,12 +12,14 @@ import {
INTEGRATION_GITHUB,
INTEGRATION_RENDER,
INTEGRATION_FLYIO,
INTEGRATION_CIRCLECI,
INTEGRATION_HEROKU_API_URL,
INTEGRATION_VERCEL_API_URL,
INTEGRATION_NETLIFY_API_URL,
INTEGRATION_RENDER_API_URL,
INTEGRATION_FLYIO_API_URL
} from '../variables';
INTEGRATION_FLYIO_API_URL,
INTEGRATION_CIRCLECI_API_URL,
} from "../variables";
/**
* Return list of names of apps for integration named [integration]
@@ -29,7 +31,7 @@ import {
*/
const getApps = async ({
integrationAuth,
accessToken
accessToken,
}: {
integrationAuth: IIntegrationAuth;
accessToken: string;
@@ -54,40 +56,45 @@ const getApps = async ({
break;
case INTEGRATION_HEROKU:
apps = await getAppsHeroku({
accessToken
accessToken,
});
break;
case INTEGRATION_VERCEL:
apps = await getAppsVercel({
integrationAuth,
accessToken
accessToken,
});
break;
case INTEGRATION_NETLIFY:
apps = await getAppsNetlify({
accessToken
accessToken,
});
break;
case INTEGRATION_GITHUB:
apps = await getAppsGithub({
accessToken
accessToken,
});
break;
case INTEGRATION_RENDER:
apps = await getAppsRender({
accessToken
accessToken,
});
break;
case INTEGRATION_FLYIO:
apps = await getAppsFlyio({
accessToken
accessToken,
});
break;
case INTEGRATION_CIRCLECI:
apps = await getAppsCircleCI({
accessToken,
});
break;
}
} catch (err) {
Sentry.setUser(null);
Sentry.captureException(err);
throw new Error('Failed to get integration apps');
throw new Error("Failed to get integration apps");
}
return apps;
@@ -106,19 +113,19 @@ const getAppsHeroku = async ({ accessToken }: { accessToken: string }) => {
const res = (
await axios.get(`${INTEGRATION_HEROKU_API_URL}/apps`, {
headers: {
Accept: 'application/vnd.heroku+json; version=3',
Authorization: `Bearer ${accessToken}`
}
Accept: "application/vnd.heroku+json; version=3",
Authorization: `Bearer ${accessToken}`,
},
})
).data;
apps = res.map((a: any) => ({
name: a.name
name: a.name,
}));
} catch (err) {
Sentry.setUser(null);
Sentry.captureException(err);
throw new Error('Failed to get Heroku integration apps');
throw new Error("Failed to get Heroku integration apps");
}
return apps;
@@ -131,10 +138,10 @@ const getAppsHeroku = async ({ accessToken }: { accessToken: string }) => {
* @returns {Object[]} apps - names of Vercel apps
* @returns {String} apps.name - name of Vercel app
*/
const getAppsVercel = async ({
const getAppsVercel = async ({
integrationAuth,
accessToken
}: {
accessToken,
}: {
integrationAuth: IIntegrationAuth;
accessToken: string;
}) => {
@@ -146,21 +153,23 @@ const getAppsVercel = async ({
Authorization: `Bearer ${accessToken}`,
'Accept-Encoding': 'application/json'
},
...( integrationAuth?.teamId ? {
params: {
teamId: integrationAuth.teamId
}
} : {})
...(integrationAuth?.teamId
? {
params: {
teamId: integrationAuth.teamId,
},
}
: {}),
})
).data;
apps = res.projects.map((a: any) => ({
name: a.name
name: a.name,
}));
} catch (err) {
Sentry.setUser(null);
Sentry.captureException(err);
throw new Error('Failed to get Vercel integration apps');
throw new Error("Failed to get Vercel integration apps");
}
return apps;
@@ -173,11 +182,7 @@ const getAppsVercel = async ({
* @returns {Object[]} apps - names of Netlify sites
* @returns {String} apps.name - name of Netlify site
*/
const getAppsNetlify = async ({
accessToken
}: {
accessToken: string;
}) => {
const getAppsNetlify = async ({ accessToken }: { accessToken: string }) => {
let apps;
try {
const res = (
@@ -191,12 +196,12 @@ const getAppsNetlify = async ({
apps = res.map((a: any) => ({
name: a.name,
appId: a.site_id
appId: a.site_id,
}));
} catch (err) {
Sentry.setUser(null);
Sentry.captureException(err);
throw new Error('Failed to get Netlify integration apps');
throw new Error("Failed to get Netlify integration apps");
}
return apps;
@@ -209,35 +214,32 @@ const getAppsNetlify = async ({
* @returns {Object[]} apps - names of Netlify sites
* @returns {String} apps.name - name of Netlify site
*/
const getAppsGithub = async ({
accessToken
}: {
accessToken: string;
}) => {
const getAppsGithub = async ({ accessToken }: { accessToken: string }) => {
let apps;
try {
const octokit = new Octokit({
auth: accessToken
auth: accessToken,
});
const repos = (await octokit.request(
'GET /user/repos{?visibility,affiliation,type,sort,direction,per_page,page,since,before}',
{
per_page: 100
}
)).data;
const repos = (
await octokit.request(
"GET /user/repos{?visibility,affiliation,type,sort,direction,per_page,page,since,before}",
{
per_page: 100,
}
)
).data;
apps = repos
.filter((a:any) => a.permissions.admin === true)
.filter((a: any) => a.permissions.admin === true)
.map((a: any) => ({
name: a.name,
owner: a.owner.login
})
);
name: a.name,
owner: a.owner.login,
}));
} catch (err) {
Sentry.setUser(null);
Sentry.captureException(err);
throw new Error('Failed to get Github repos');
throw new Error("Failed to get Github repos");
}
return apps;
@@ -251,11 +253,7 @@ const getAppsGithub = async ({
* @returns {String} apps.name - name of Render service
* @returns {String} apps.appId - id of Render service
*/
const getAppsRender = async ({
accessToken
}: {
accessToken: string;
}) => {
const getAppsRender = async ({ accessToken }: { accessToken: string }) => {
let apps: any;
try {
const res = (
@@ -263,8 +261,8 @@ const getAppsRender = async ({
headers: {
Authorization: `Bearer ${accessToken}`,
Accept: 'application/json',
'Accept-Encoding': 'application/json'
}
'Accept-Encoding': 'application/json',
},
})
).data;
@@ -277,11 +275,11 @@ const getAppsRender = async ({
} catch (err) {
Sentry.setUser(null);
Sentry.captureException(err);
throw new Error('Failed to get Render services');
throw new Error("Failed to get Render services");
}
return apps;
}
};
/**
* Return list of apps for Fly.io integration
@@ -290,11 +288,7 @@ const getAppsRender = async ({
* @returns {Object[]} apps - names and ids of Fly.io apps
* @returns {String} apps.name - name of Fly.io apps
*/
const getAppsFlyio = async ({
accessToken
}: {
accessToken: string;
}) => {
const getAppsFlyio = async ({ accessToken }: { accessToken: string }) => {
let apps;
try {
const query = `
@@ -308,34 +302,71 @@ const getAppsFlyio = async ({
}
}
`;
const res = (await axios({
url: INTEGRATION_FLYIO_API_URL,
method: 'post',
headers: {
'Authorization': 'Bearer ' + accessToken,
const res = (
await axios({
url: INTEGRATION_FLYIO_API_URL,
method: "post",
headers: {
Authorization: "Bearer " + accessToken,
'Accept': 'application/json',
'Accept-Encoding': 'application/json'
},
data: {
query,
variables: {
role: null
}
}
})).data.data.apps.nodes;
apps = res
.map((a: any) => ({
name: a.name
}));
'Accept-Encoding': 'application/json',
},
data: {
query,
variables: {
role: null,
},
},
})
).data.data.apps.nodes;
apps = res.map((a: any) => ({
name: a.name,
}));
} catch (err) {
Sentry.setUser(null);
Sentry.captureException(err);
throw new Error('Failed to get Fly.io apps');
throw new Error("Failed to get Fly.io apps");
}
return apps;
};
/**
* Return list of projects for CircleCI integration
* @param {Object} obj
* @param {String} obj.accessToken - access token for CircleCI API
* @returns {Object[]} apps -
* @returns {String} apps.name - name of CircleCI apps
*/
const getAppsCircleCI = async ({ accessToken }: { accessToken: string }) => {
let apps: any;
try {
const res = (
await axios.get(
`${INTEGRATION_CIRCLECI_API_URL}/v1.1/projects`,
{
headers: {
"Circle-Token": accessToken,
"Accept-Encoding": "application/json",
},
}
)
).data
apps = res?.map((a: any) => {
return {
name: a?.reponame
}
});
} catch (err) {
Sentry.setUser(null);
Sentry.captureException(err);
throw new Error("Failed to get CircleCI projects");
}
return apps;
}
};
export { getApps };

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

View File

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

View File

@@ -17,20 +17,20 @@ interface IApprover {
status: ApprovalStatus;
}
enum ApprovalStatus {
export enum ApprovalStatus {
PENDING = 'pending',
APPROVED = 'approved',
REJECTED = 'rejected'
}
enum RequestType {
export enum RequestType {
UPDATE = 'update',
DELETE = 'delete',
CREATE = 'create'
}
const approverSchema = new mongoose.Schema({
userId: {
user: {
type: mongoose.Schema.Types.ObjectId,
ref: 'User',
required: true
@@ -42,7 +42,6 @@ const approverSchema = new mongoose.Schema({
}
});
const secretApprovalRequestSchema = new Schema<ISecretApprovalRequest>(
{
secret: {

View File

@@ -74,6 +74,7 @@ router.get(
'/',
query('workspaceId').exists().trim(),
query('environment').exists().trim(),
query('tagSlugs'),
validateRequest,
requireAuth({
acceptedAuthModes: ['jwt', 'apiKey', 'serviceToken']

View File

@@ -3,8 +3,8 @@ import {
ENV_TESTING,
ENV_STAGING,
ENV_PROD,
ENV_SET
} from './environment';
ENV_SET,
} from "./environment";
import {
INTEGRATION_AZURE_KEY_VAULT,
INTEGRATION_AWS_PARAMETER_STORE,
@@ -15,6 +15,7 @@ import {
INTEGRATION_GITHUB,
INTEGRATION_RENDER,
INTEGRATION_FLYIO,
INTEGRATION_CIRCLECI,
INTEGRATION_SET,
INTEGRATION_OAUTH2,
INTEGRATION_AZURE_TOKEN_URL,
@@ -27,17 +28,12 @@ import {
INTEGRATION_NETLIFY_API_URL,
INTEGRATION_RENDER_API_URL,
INTEGRATION_FLYIO_API_URL,
INTEGRATION_OPTIONS
} from './integration';
import {
OWNER,
ADMIN,
MEMBER,
INVITED,
ACCEPTED,
} from './organization';
import { SECRET_SHARED, SECRET_PERSONAL } from './secret';
import { EVENT_PUSH_SECRETS, EVENT_PULL_SECRETS } from './event';
INTEGRATION_CIRCLECI_API_URL,
INTEGRATION_OPTIONS,
} from "./integration";
import { OWNER, ADMIN, MEMBER, INVITED, ACCEPTED } from "./organization";
import { SECRET_SHARED, SECRET_PERSONAL } from "./secret";
import { EVENT_PUSH_SECRETS, EVENT_PULL_SECRETS } from "./event";
import {
ACTION_LOGIN,
ACTION_LOGOUT,
@@ -80,6 +76,7 @@ export {
INTEGRATION_GITHUB,
INTEGRATION_RENDER,
INTEGRATION_FLYIO,
INTEGRATION_CIRCLECI,
INTEGRATION_SET,
INTEGRATION_OAUTH2,
INTEGRATION_AZURE_TOKEN_URL,
@@ -92,6 +89,7 @@ export {
INTEGRATION_NETLIFY_API_URL,
INTEGRATION_RENDER_API_URL,
INTEGRATION_FLYIO_API_URL,
INTEGRATION_CIRCLECI_API_URL,
EVENT_PUSH_SECRETS,
EVENT_PULL_SECRETS,
ACTION_LOGIN,

View File

@@ -3,50 +3,53 @@ import {
TENANT_ID_AZURE
} from '../config';
import {
CLIENT_ID_HEROKU,
CLIENT_ID_NETLIFY,
CLIENT_ID_GITHUB,
CLIENT_SLUG_VERCEL
} from '../config';
CLIENT_ID_HEROKU,
CLIENT_ID_NETLIFY,
CLIENT_ID_GITHUB,
CLIENT_SLUG_VERCEL,
} from "../config";
// integrations
const INTEGRATION_AZURE_KEY_VAULT = 'azure-key-vault';
const INTEGRATION_AWS_PARAMETER_STORE = 'aws-parameter-store';
const INTEGRATION_AWS_SECRET_MANAGER = 'aws-secret-manager';
const INTEGRATION_HEROKU = 'heroku';
const INTEGRATION_VERCEL = 'vercel';
const INTEGRATION_NETLIFY = 'netlify';
const INTEGRATION_GITHUB = 'github';
const INTEGRATION_RENDER = 'render';
const INTEGRATION_FLYIO = 'flyio';
const INTEGRATION_HEROKU = "heroku";
const INTEGRATION_VERCEL = "vercel";
const INTEGRATION_NETLIFY = "netlify";
const INTEGRATION_GITHUB = "github";
const INTEGRATION_RENDER = "render";
const INTEGRATION_FLYIO = "flyio";
const INTEGRATION_CIRCLECI = "circleci";
const INTEGRATION_SET = new Set([
INTEGRATION_AZURE_KEY_VAULT,
INTEGRATION_HEROKU,
INTEGRATION_VERCEL,
INTEGRATION_NETLIFY,
INTEGRATION_GITHUB,
INTEGRATION_RENDER,
INTEGRATION_FLYIO
INTEGRATION_HEROKU,
INTEGRATION_VERCEL,
INTEGRATION_NETLIFY,
INTEGRATION_GITHUB,
INTEGRATION_RENDER,
INTEGRATION_FLYIO,
INTEGRATION_CIRCLECI,
]);
// integration types
const INTEGRATION_OAUTH2 = 'oauth2';
const INTEGRATION_OAUTH2 = "oauth2";
// integration oauth endpoints
const INTEGRATION_AZURE_TOKEN_URL = `https://login.microsoftonline.com/${TENANT_ID_AZURE}/oauth2/v2.0/token`;
const INTEGRATION_HEROKU_TOKEN_URL = 'https://id.heroku.com/oauth/token';
const INTEGRATION_VERCEL_TOKEN_URL =
'https://api.vercel.com/v2/oauth/access_token';
const INTEGRATION_NETLIFY_TOKEN_URL = 'https://api.netlify.com/oauth/token';
"https://api.vercel.com/v2/oauth/access_token";
const INTEGRATION_NETLIFY_TOKEN_URL = "https://api.netlify.com/oauth/token";
const INTEGRATION_GITHUB_TOKEN_URL =
'https://github.com/login/oauth/access_token';
"https://github.com/login/oauth/access_token";
// integration apps endpoints
const INTEGRATION_HEROKU_API_URL = 'https://api.heroku.com';
const INTEGRATION_VERCEL_API_URL = 'https://api.vercel.com';
const INTEGRATION_NETLIFY_API_URL = 'https://api.netlify.com';
const INTEGRATION_RENDER_API_URL = 'https://api.render.com';
const INTEGRATION_FLYIO_API_URL = 'https://api.fly.io/graphql';
const INTEGRATION_HEROKU_API_URL = "https://api.heroku.com";
const INTEGRATION_VERCEL_API_URL = "https://api.vercel.com";
const INTEGRATION_NETLIFY_API_URL = "https://api.netlify.com";
const INTEGRATION_RENDER_API_URL = "https://api.render.com";
const INTEGRATION_FLYIO_API_URL = "https://api.fly.io/graphql";
const INTEGRATION_CIRCLECI_API_URL = "https://circleci.com/api";
const INTEGRATION_OPTIONS = [
{
@@ -122,6 +125,15 @@ const INTEGRATION_OPTIONS = [
clientId: '',
docsLink: ''
},
{
name: 'Circle CI',
slug: 'circleci',
image: 'Circle CI.png',
isAvailable: true,
type: 'pat',
clientId: '',
docsLink: ''
},
{
name: 'Azure Key Vault',
slug: 'azure-key-vault',
@@ -149,15 +161,6 @@ const INTEGRATION_OPTIONS = [
type: '',
clientId: '',
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_AWS_PARAMETER_STORE,
INTEGRATION_AWS_SECRET_MANAGER,
INTEGRATION_HEROKU,
INTEGRATION_VERCEL,
INTEGRATION_NETLIFY,
INTEGRATION_GITHUB,
INTEGRATION_RENDER,
INTEGRATION_FLYIO,
INTEGRATION_SET,
INTEGRATION_OAUTH2,
INTEGRATION_HEROKU,
INTEGRATION_VERCEL,
INTEGRATION_NETLIFY,
INTEGRATION_GITHUB,
INTEGRATION_RENDER,
INTEGRATION_FLYIO,
INTEGRATION_CIRCLECI,
INTEGRATION_SET,
INTEGRATION_OAUTH2,
INTEGRATION_AZURE_TOKEN_URL,
INTEGRATION_HEROKU_TOKEN_URL,
INTEGRATION_VERCEL_TOKEN_URL,
INTEGRATION_NETLIFY_TOKEN_URL,
INTEGRATION_GITHUB_TOKEN_URL,
INTEGRATION_HEROKU_API_URL,
INTEGRATION_VERCEL_API_URL,
INTEGRATION_NETLIFY_API_URL,
INTEGRATION_RENDER_API_URL,
INTEGRATION_FLYIO_API_URL,
INTEGRATION_OPTIONS
INTEGRATION_HEROKU_TOKEN_URL,
INTEGRATION_VERCEL_TOKEN_URL,
INTEGRATION_NETLIFY_TOKEN_URL,
INTEGRATION_GITHUB_TOKEN_URL,
INTEGRATION_HEROKU_API_URL,
INTEGRATION_VERCEL_API_URL,
INTEGRATION_NETLIFY_API_URL,
INTEGRATION_RENDER_API_URL,
INTEGRATION_FLYIO_API_URL,
INTEGRATION_CIRCLECI_API_URL,
INTEGRATION_OPTIONS,
};

View File

@@ -1,24 +1,16 @@
// membership roles
const OWNER = 'owner';
const ADMIN = 'admin';
const MEMBER = 'member';
const OWNER = "owner";
const ADMIN = "admin";
const MEMBER = "member";
// membership statuses
const INVITED = 'invited';
const INVITED = "invited";
// membership permissions ability
const ABILITY_READ = 'read';
const ABILITY_WRITE = 'write';
const ABILITY_READ = "read";
const ABILITY_WRITE = "write";
// -- organization
const ACCEPTED = 'accepted';
const ACCEPTED = "accepted";
export {
OWNER,
ADMIN,
MEMBER,
INVITED,
ACCEPTED,
ABILITY_READ,
ABILITY_WRITE
}
export { OWNER, ADMIN, MEMBER, INVITED, ACCEPTED, ABILITY_READ, ABILITY_WRITE };

View File

@@ -114,6 +114,7 @@ func CallGetSecretsV2(httpClient *resty.Client, request GetEncryptedSecretsV2Req
SetHeader("User-Agent", USER_AGENT).
SetQueryParam("environment", request.Environment).
SetQueryParam("workspaceId", request.WorkspaceId).
SetQueryParam("tagSlugs", request.TagSlugs).
Get(fmt.Sprintf("%v/v2/secrets", config.INFISICAL_URL))
if err != nil {
@@ -154,13 +155,12 @@ func CallIsAuthenticated(httpClient *resty.Client) bool {
SetHeader("User-Agent", USER_AGENT).
Post(fmt.Sprintf("%v/v1/auth/checkAuth", config.INFISICAL_URL))
log.Debugln(fmt.Errorf("CallIsAuthenticated: Unsuccessful response: [response=%v]", response))
if err != nil {
return false
}
if response.IsError() {
log.Debugln(fmt.Errorf("CallIsAuthenticated: Unsuccessful response: [response=%v]", response))
return false
}
@@ -175,8 +175,6 @@ func CallGetAccessibleEnvironments(httpClient *resty.Client, request GetAccessib
SetHeader("User-Agent", USER_AGENT).
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 {
return GetAccessibleEnvironmentsResponse{}, err
}

View File

@@ -197,6 +197,7 @@ type GetSecretsByWorkspaceIdAndEnvironmentRequest struct {
type GetEncryptedSecretsV2Request struct {
Environment string `json:"environment"`
WorkspaceId string `json:"workspaceId"`
TagSlugs string `json:"tagSlugs"`
}
type GetEncryptedSecretsV2Response struct {

View File

@@ -61,7 +61,12 @@ var exportCmd = &cobra.Command{
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 {
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().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().StringP("tags", "t", "", "filter secrets by tag slugs")
}
// Format according to the format flag

View File

@@ -74,7 +74,12 @@ var runCmd = &cobra.Command{
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 {
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("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("tags", "t", "", "filter secrets by tag slugs ")
}
// 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)
}
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 {
util.HandleError(err)
}
@@ -342,7 +347,12 @@ func getSecretsByNames(cmd *cobra.Command, args []string) {
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 {
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")
}
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 {
util.HandleError(err, "To fetch all secrets")
}
@@ -567,5 +582,6 @@ func init() {
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.Flags().Bool("expand", true, "Parse shell parameter expansions in your secrets")
secretsCmd.PersistentFlags().StringP("tags", "t", "", "filter secrets by tag slugs")
rootCmd.AddCommand(secretsCmd)
}

View File

@@ -51,4 +51,5 @@ type SymmetricEncryptionResult struct {
type GetAllSecretsParameters struct {
Environment string
InfisicalToken string
TagSlugs string
}

View File

@@ -62,7 +62,7 @@ func GetPlainTextSecretsViaServiceToken(fullServiceToken string) ([]models.Singl
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.SetAuthToken(JTWToken).
SetHeader("Accept", "application/json")
@@ -85,6 +85,7 @@ func GetPlainTextSecretsViaJTW(JTWToken string, receiversPrivateKey string, work
encryptedSecrets, err := api.CallGetSecretsV2(httpClient, api.GetEncryptedSecretsV2Request{
WorkspaceId: workspaceId,
Environment: environmentName,
TagSlugs: tagSlugs,
})
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)
}
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)
backupSecretsEncryptionKey := []byte(loggedInUserDetails.UserCredentials.PrivateKey)[0:32]

View File

@@ -11,7 +11,8 @@ const integrationSlugNameMapping: Mapping = {
'netlify': 'Netlify',
'github': 'GitHub',
'render': 'Render',
'flyio': 'Fly.io'
'flyio': 'Fly.io',
"circleci": 'CircleCI'
}
const envMapping: Mapping = {

View File

@@ -38,7 +38,6 @@ const UserTable = ({ userData, changeData, myUser, filter, resendInvite, isOrg }
const router = useRouter();
const [myRole, setMyRole] = useState('member');
const [userProjectMemberships, setUserProjectMemberships] = useState<any[]>([]);
console.log(123, userData)
const workspaceId = router.query.id as string;
// Delete the row in the table (e.g. a user)
@@ -198,15 +197,15 @@ const UserTable = ({ userData, changeData, myUser, filter, resendInvite, isOrg }
</div>
</td>
<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">
{userProjectMemberships[row.userId]
? 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'>
<span className='mb-0.5 cursor-default'>{project.name}</span>
</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>}
</td>
<div className="flex items-center max-h-16 overflow-x-auto w-full max-w-xl break-all">
{userProjectMemberships[row.userId]
? 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'>
<span className='mb-0.5 cursor-default'>{project.name}</span>
</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>}
</div>
</td>
<td className="flex flex-row justify-end pl-8 pr-8 py-2 border-t border-0.5 border-mineshaft-700">
{myUser !== row.email &&

View File

@@ -44,9 +44,9 @@ const CloudIntegration = ({
tabIndex={0}
className={`relative ${
cloudIntegrationOption.isAvailable
? 'hover:bg-white/10 duration-200 cursor-pointer'
? 'cursor-pointer duration-200 hover:bg-white/10'
: 'opacity-50'
} flex flex-row bg-white/5 h-32 rounded-md p-4 items-center`}
} flex h-32 flex-row items-center rounded-md bg-white/5 p-4`}
onClick={() => {
if (!cloudIntegrationOption.isAvailable) return;
setSelectedIntegrationOption(cloudIntegrationOption);
@@ -61,22 +61,22 @@ const CloudIntegration = ({
alt="integration logo"
/>
{cloudIntegrationOption.name.split(' ').length > 2 ? (
<div className="font-semibold text-gray-300 group-hover:text-gray-200 duration-200 text-3xl ml-4 max-w-xs">
<div className="ml-4 max-w-xs text-3xl font-semibold text-gray-300 duration-200 group-hover:text-gray-200">
<div>{cloudIntegrationOption.name.split(' ')[0]}</div>
<div className="text-base">
{cloudIntegrationOption.name.split(' ')[1]} {cloudIntegrationOption.name.split(' ')[2]}
</div>
</div>
) : (
<div className="font-semibold text-gray-300 group-hover:text-gray-200 duration-200 text-xl ml-4 max-w-xs">
<div className="ml-4 max-w-xs text-xl font-semibold text-gray-300 duration-200 group-hover:text-gray-200">
{cloudIntegrationOption.name}
</div>
)}
{cloudIntegrationOption.isAvailable &&
integrationAuths
.map((authorization) => authorization.integration)
.map((authorization) => authorization?.integration)
.includes(cloudIntegrationOption.slug) && (
<div className="absolute group z-40 top-0 right-0 flex flex-row">
<div className="group absolute top-0 right-0 z-40 flex flex-row">
<div
onKeyDown={() => null}
role="button"
@@ -86,8 +86,7 @@ const CloudIntegration = ({
const deletedIntegrationAuth = await deleteIntegrationAuth({
integrationAuthId: integrationAuths
.filter(
(authorization) =>
authorization.integration === cloudIntegrationOption.slug
(authorization) => authorization.integration === cloudIntegrationOption.slug
)
.map((authorization) => authorization._id)[0]
});
@@ -96,20 +95,20 @@ const CloudIntegration = ({
integrationAuth: deletedIntegrationAuth
});
}}
className="cursor-pointer w-max bg-red py-0.5 px-2 rounded-b-md text-xs flex flex-row items-center opacity-0 group-hover:opacity-100 duration-200"
className="flex w-max cursor-pointer flex-row items-center rounded-b-md bg-red py-0.5 px-2 text-xs opacity-0 duration-200 group-hover:opacity-100"
>
<FontAwesomeIcon icon={faX} className="text-xs mr-2 py-px" />
<FontAwesomeIcon icon={faX} className="mr-2 py-px text-xs" />
Revoke
</div>
<div className="w-max bg-primary py-0.5 px-2 rounded-bl-md rounded-tr-md text-xs flex flex-row items-center text-black opacity-90 group-hover:opacity-100 duration-200">
<FontAwesomeIcon icon={faCheck} className="text-xs mr-2" />
<div className="flex w-max flex-row items-center rounded-bl-md rounded-tr-md bg-primary py-0.5 px-2 text-xs text-black opacity-90 duration-200 group-hover:opacity-100">
<FontAwesomeIcon icon={faCheck} className="mr-2 text-xs" />
Authorized
</div>
</div>
)}
{!cloudIntegrationOption.isAvailable && (
<div className="absolute group z-50 top-0 right-0 flex flex-row">
<div className="w-max bg-yellow py-0.5 px-2 rounded-bl-md rounded-tr-md text-xs flex flex-row items-center text-black opacity-90">
<div className="group absolute top-0 right-0 z-50 flex flex-row">
<div className="flex w-max flex-row items-center rounded-bl-md rounded-tr-md bg-yellow py-0.5 px-2 text-xs text-black opacity-90">
Coming Soon
</div>
</div>

View File

@@ -45,8 +45,8 @@ type Props = {
handleDeleteIntegration: (args: { integration: Integration }) => void;
};
const IntegrationTile = ({
integration,
const IntegrationTile = ({
integration,
integrations,
bot,
setBot,
@@ -57,7 +57,7 @@ const IntegrationTile = ({
// set initial environment. This find will only execute when component is mounting
const [integrationEnvironment, setIntegrationEnvironment] = useState<Props['environments'][0]>(
environments.find(({ slug }) => slug === integration.environment) || {
environments.find(({ slug }) => slug === integration?.environment) || {
name: '',
slug: ''
}
@@ -69,11 +69,10 @@ const IntegrationTile = ({
useEffect(() => {
const loadIntegration = async () => {
const tempApps: [IntegrationApp] = await getIntegrationApps({
integrationAuthId: integration.integrationAuth
integrationAuthId: integration?.integrationAuth
});
setApps(tempApps);
if (integration?.app) {
@@ -90,15 +89,16 @@ const IntegrationTile = ({
case 'vercel':
setIntegrationTargetEnvironment(
integration?.targetEnvironment
? integration.targetEnvironment.charAt(0).toUpperCase() + integration.targetEnvironment.substring(1)
: 'Development'
? integration.targetEnvironment.charAt(0).toUpperCase() +
integration.targetEnvironment.substring(1)
: 'Development'
);
break;
case 'netlify':
setIntegrationTargetEnvironment(
integration?.targetEnvironment
? contextNetlifyMapping[integration.targetEnvironment]
: 'Local development'
integration?.targetEnvironment
? contextNetlifyMapping[integration.targetEnvironment]
: 'Local development'
);
break;
default:
@@ -108,7 +108,7 @@ const IntegrationTile = ({
loadIntegration();
}, []);
const handleStartIntegration = async () => {
const reformatTargetEnvironment = (targetEnvironment: string) => {
switch (integration.integration) {
@@ -119,13 +119,13 @@ const IntegrationTile = ({
default:
return null;
}
}
};
try {
const siteApp = apps.find((app) => app.name === integrationApp); // obj or undefined
const appId = siteApp?.appId ?? null;
const owner = siteApp?.owner ?? null;
// return updated integration
const updatedIntegration = await updateIntegration({
integrationId: integration._id,
@@ -136,15 +136,15 @@ const IntegrationTile = ({
targetEnvironment: reformatTargetEnvironment(integrationTargetEnvironment),
owner
});
setIntegrations(
integrations.map((i) => i._id === updatedIntegration._id ? updatedIntegration : i)
integrations.map((i) => (i._id === updatedIntegration._id ? updatedIntegration : i))
);
} catch (err) {
console.error(err);
}
}
};
// eslint-disable-next-line @typescript-eslint/no-shadow
const renderIntegrationSpecificParams = (integration: Integration) => {
try {
@@ -152,7 +152,7 @@ const IntegrationTile = ({
case 'vercel':
return (
<div>
<div className="text-gray-400 text-xs font-semibold mb-2 w-60">ENVIRONMENT</div>
<div className="mb-2 w-60 text-xs font-semibold text-gray-400">ENVIRONMENT</div>
<ListBox
data={!integration.isActive ? ['Development', 'Preview', 'Production'] : null}
isSelected={integrationTargetEnvironment}
@@ -164,7 +164,7 @@ const IntegrationTile = ({
case 'netlify':
return (
<div>
<div className="text-gray-400 text-xs font-semibold mb-2">CONTEXT</div>
<div className="mb-2 text-xs font-semibold text-gray-400">CONTEXT</div>
<ListBox
data={
!integration.isActive
@@ -189,10 +189,10 @@ const IntegrationTile = ({
if (!integrationApp) return <div />;
return (
<div className="max-w-5xl p-6 mx-6 mb-8 rounded-md bg-white/5 flex justify-between">
<div className="mx-6 mb-8 flex max-w-5xl justify-between rounded-md bg-white/5 p-6">
<div className="flex">
<div>
<p className="text-gray-400 text-xs font-semibold mb-2">ENVIRONMENT</p>
<p className="mb-2 text-xs font-semibold text-gray-400">ENVIRONMENT</p>
<ListBox
data={!integration.isActive ? environments.map(({ name }) => name) : null}
isSelected={integrationEnvironment.name}
@@ -208,7 +208,7 @@ const IntegrationTile = ({
/>
</div>
<div className="pt-2">
<FontAwesomeIcon icon={faArrowRight} className="mx-4 text-gray-400 mt-8" />
<FontAwesomeIcon icon={faArrowRight} className="mx-4 mt-8 text-gray-400" />
</div>
<div className="mr-2">
<p className="text-gray-400 text-xs font-semibold mb-2">INTEGRATION</p>
@@ -218,7 +218,7 @@ const IntegrationTile = ({
</div>
</div>
<div className="mr-2">
<div className="text-gray-400 text-xs font-semibold mb-2">APP</div>
<div className="mb-2 text-xs font-semibold text-gray-400">APP</div>
<ListBox
data={!integration.isActive ? apps.map((app) => app.name) : null}
isSelected={integrationApp}
@@ -231,9 +231,9 @@ const IntegrationTile = ({
</div>
<div className="flex items-end">
{integration.isActive ? (
<div className="max-w-5xl flex flex-row items-center bg-white/5 p-2 rounded-md px-4">
<FontAwesomeIcon icon={faRotate} className="text-lg mr-2.5 text-primary animate-spin" />
<div className="text-gray-300 font-semibold">In Sync</div>
<div className="flex max-w-5xl flex-row items-center rounded-md bg-white/5 p-2 px-4">
<FontAwesomeIcon icon={faRotate} className="mr-2.5 animate-spin text-lg text-primary" />
<div className="font-semibold text-gray-300">In Sync</div>
</div>
) : (
<Button
@@ -243,11 +243,13 @@ const IntegrationTile = ({
size="md"
/>
)}
<div className="opacity-50 hover:opacity-100 duration-200 ml-2">
<div className="ml-2 opacity-50 duration-200 hover:opacity-100">
<Button
onButtonPressed={() => handleDeleteIntegration({
integration
})}
onButtonPressed={() =>
handleDeleteIntegration({
integration
})
}
color="red"
size="icon-md"
icon={faX}

View File

@@ -26,7 +26,7 @@ interface Integration {
}
const ProjectIntegrationSection = ({
integrations,
integrations,
setIntegrations,
bot,
setBot,
@@ -35,22 +35,20 @@ const ProjectIntegrationSection = ({
}: Props) => {
return integrations.length > 0 ? (
<div className="mb-12">
<div className="flex flex-col justify-between items-start mx-4 mb-4 mt-6 text-xl max-w-5xl px-2">
<h1 className="font-semibold text-3xl">Current Integrations</h1>
<p className="text-base text-gray-400">
Manage integrations with third-party services.
</p>
<div className="mx-4 mb-4 mt-6 flex max-w-5xl flex-col items-start justify-between px-2 text-xl">
<h1 className="text-3xl font-semibold">Current Integrations</h1>
<p className="text-base text-gray-400">Manage integrations with third-party services.</p>
</div>
{integrations.map((integration: Integration) => {
return (
<IntegrationTile
key={`integration-${integration._id.toString()}`}
integration={integration}
key={`integration-${integration?._id.toString()}`}
integration={integration}
integrations={integrations}
bot={bot}
setBot={setBot}
setIntegrations={setIntegrations}
environments={environments}
environments={environments}
handleDeleteIntegration={handleDeleteIntegration}
/>
);
@@ -59,6 +57,6 @@ const ProjectIntegrationSection = ({
) : (
<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 = {
args: {
children: 'Hello Infisical',

View File

@@ -31,7 +31,9 @@ const buttonVariants = cva(
variant: {
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: {
true: 'bg-mineshaft opacity-40 cursor-not-allowed',
@@ -53,6 +55,16 @@ const buttonVariants = cva(
}
},
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',
variant: 'outline',

View File

@@ -46,7 +46,7 @@ export const Select = forwardRef<HTMLButtonElement, SelectProps>(
<SelectPrimitive.Portal>
<SelectPrimitive.Content
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
)}
position={position}

View File

@@ -44,3 +44,5 @@ const plansProd: Mapping = {
};
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>(
() => ({
orgs: userOrgs,
currentOrg: (userOrgs || []).find(({ _id }) => _id === currentWsOrgID),
currentOrg: (userOrgs || []).find(({ _id }) => _id === currentWsOrgID) || (userOrgs || [])[0],
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 getLatestFileKey from '@app/pages/api/workspace/getLatestFileKey';
import DashboardInputField from '../../components/dashboard/DashboardInputField';
import {
decryptAssymmetric,
decryptSymmetric
@@ -130,7 +129,7 @@ const ActivitySideBar = ({ toggleSidebar, currentAction }: SideBarProps) => {
<div
className={`absolute border-l border-mineshaft-500 ${
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 ? (
<div className="flex items-center justify-center h-full mb-8">
@@ -142,7 +141,7 @@ const ActivitySideBar = ({ toggleSidebar, currentAction }: SideBarProps) => {
/>
</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">
<p className="font-semibold text-lg text-bunker-200">
{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" />
</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 === 'addSecrets' ||
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">
{item.newSecretVersion.key}
</div>
<DashboardInputField
onChangeHandler={() => {}}
type="value"
position={1}
value={item.newSecretVersion.value}
isDuplicate={false}
blurred={false}
/>
<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'>
{item.newSecretVersion.value ? <span> {item.newSecretVersion.value} </span> : <span className='text-bunker-400'> EMPTY </span>}
</div>
</div>
))}
{actionMetaData?.name === 'updateSecrets' &&
@@ -182,8 +176,8 @@ const ActivitySideBar = ({ toggleSidebar, currentAction }: SideBarProps) => {
<div className="text-xs text-bunker-200 mt-4 pl-1">
{item.newSecretVersion.key}
</div>
<div className="text-bunker-100 font-mono rounded-md overflow-hidden">
<div className="bg-red/30 px-2 ph-no-capture">
<div className="break-all text-bunker-200 font-mono rounded-md overflow-hidden border border-mineshaft-500">
<div className="bg-red/40 px-2 ph-no-capture">
-{' '}
{patienceDiff(
item.oldSecretVersion.value.split(''),
@@ -194,14 +188,14 @@ const ActivitySideBar = ({ toggleSidebar, currentAction }: SideBarProps) => {
character.bIndex !== -1 && (
<span
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}
</span>
)
)}
</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(
item.oldSecretVersion.value.split(''),
@@ -212,7 +206,7 @@ const ActivitySideBar = ({ toggleSidebar, currentAction }: SideBarProps) => {
character.aIndex !== -1 && (
<span
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}
</span>

View File

@@ -1,2 +1,3 @@
export { useLeaveConfirm } from './useLeaveConfirm';
export { usePopUp } from './usePopUp';
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
let { workspaces, currentWorkspace } = useWorkspace();
const { currentOrg } = useOrganization();
workspaces = workspaces.filter(ws => ws.organization === currentOrg?._id)
workspaces = workspaces.filter((ws) => ws.organization === currentOrg?._id);
const { user } = useUser();
const createWs = useCreateWorkspace();
@@ -98,7 +98,7 @@ export const AppLayout = ({ children }: LayoutProps) => {
const putUserInWorkSpace = async () => {
if (tempLocalStorage('orgData.id') === '') {
const userOrgs = await getOrganizations();
localStorage.setItem('orgData.id', userOrgs[0]._id);
localStorage.setItem('orgData.id', userOrgs[0]?._id);
}
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 (
!['callback', 'create', 'authorize'].includes(intendedWorkspaceId) &&
!['callback', 'create', 'authorize'].includes(intendedWorkspaceId) && userWorkspaces[0]?._id !== undefined &&
!userWorkspaces
.map((workspace: { _id: string }) => workspace._id)
.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">
<nav className="items-between flex h-full flex-col justify-between">
<div>
{currentWorkspace
? <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>
<Select
defaultValue={currentWorkspace?._id}
value={currentWorkspace?._id}
className="w-full py-2.5 bg-mineshaft-600 font-medium"
onValueChange={(value) => {
router.push(`/dashboard/${value}`);
}}
position="popper"
dropdownContainerClassName="left-0 text-bunker-200 bg-mineshaft-800 border border-mineshaft-600 z-50"
>
{workspaces.map(({ _id, name }) => (
<SelectItem key={`ws-layout-list-${_id}`} value={_id} className={`${currentWorkspace?._id === _id && "bg-mineshaft-600"}`}>
{name}
</SelectItem>
))}
<hr className="mt-1 mb-1 h-px border-0 bg-gray-700" />
<div className="w-full">
<Button
className="w-full py-2 text-bunker-200 bg-mineshaft-500 hover:bg-primary/90 hover:text-black"
color="mineshaft"
size="sm"
onClick={() => handlePopUpOpen('addNewWs')}
leftIcon={<FontAwesomeIcon icon={faPlus} />}
>
Add Project
</Button>
</div>
</Select>
</div>
: <div className="w-full p-4 mt-3 mb-4">
<Button
className="w-full py-2 text-bunker-200 bg-mineshaft-500 hover:bg-primary/90 hover:text-black"
color="mineshaft"
size="sm"
onClick={() => handlePopUpOpen('addNewWs')}
leftIcon={<FontAwesomeIcon icon={faPlus} />}
>
Add Project
</Button>
</div>}
<div className={`${currentWorkspace ? "block" : "hidden"}`}>
{currentWorkspace ? (
<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>
<Select
defaultValue={currentWorkspace?._id}
value={currentWorkspace?._id}
className="w-full py-2.5 bg-mineshaft-600 font-medium"
onValueChange={(value) => {
router.push(`/dashboard/${value}`);
}}
position="popper"
dropdownContainerClassName="text-bunker-200 bg-mineshaft-800 border border-mineshaft-600 z-50"
>
{workspaces.map(({ _id, name }) => (
<SelectItem
key={`ws-layout-list-${_id}`}
value={_id}
className={`${currentWorkspace?._id === _id && 'bg-mineshaft-600'}`}
>
{name}
</SelectItem>
))}
<hr className="mt-1 mb-1 h-px border-0 bg-gray-700" />
<div className="w-full">
<Button
className="w-full py-2 text-bunker-200 bg-mineshaft-500 hover:bg-primary/90 hover:text-black"
color="mineshaft"
size="sm"
onClick={() => handlePopUpOpen('addNewWs')}
leftIcon={<FontAwesomeIcon icon={faPlus} />}
>
Add Project
</Button>
</div>
</Select>
</div>
) : (
<div className="w-full p-4 mt-3 mb-4">
<Button
className="w-full py-2 text-bunker-200 bg-mineshaft-500 hover:bg-primary/90 hover:text-black"
color="mineshaft"
size="sm"
onClick={() => handlePopUpOpen('addNewWs')}
leftIcon={<FontAwesomeIcon icon={faPlus} />}
>
Add Project
</Button>
</div>
)}
<div className={`${currentWorkspace ? 'block' : 'hidden'}`}>
<Menu>
<Link href={`/dashboard/${currentWorkspace?._id}`} passHref>
<a>
@@ -393,7 +401,7 @@ export const AppLayout = ({ children }: LayoutProps) => {
</FormControl>
)}
/>
<div className='pl-1 mt-4'>
<div className="pl-1 mt-4">
<Controller
control={control}
name="addMembers"
@@ -415,11 +423,19 @@ export const AppLayout = ({ children }: LayoutProps) => {
isDisabled={isSubmitting}
isLoading={isSubmitting}
key="layout-create-project-submit"
className=""
className="mr-4"
type="submit"
>
Create Project
</Button>
<Button
key="layout-cancel-create-project"
onClick={() => handlePopUpClose('addNewWs')}
variant="plain"
colorSchema="secondary"
>
Cancel
</Button>
</div>
</form>
</ModalContent>

View File

@@ -35,9 +35,11 @@ import encryptSecrets from '@app/components/utilities/secrets/encryptSecrets';
import getSecretsForProject from '@app/components/utilities/secrets/getSecretsForProject';
import { getTranslatedServerSideProps } from '@app/components/utilities/withTranslateProps';
import { IconButton } from '@app/components/v2';
import { leaveConfirmDefaultMessage } from '@app/const';
import getProjectSercetSnapshotsCount from '@app/ee/api/secrets/GetProjectSercetSnapshotsCount';
import performSecretRollback from '@app/ee/api/secrets/PerformSecretRollback';
import PITRecoverySidebar from '@app/ee/components/PITRecoverySidebar';
import { useLeaveConfirm } from '@app/hooks';
import addSecrets from '../api/files/AddSecrets';
import deleteSecrets from '../api/files/DeleteSecrets';
@@ -116,7 +118,6 @@ function findDuplicates(arr: any[]) {
export default function Dashboard() {
const [data, setData] = useState<SecretDataProps[] | null>();
const [initialData, setInitialData] = useState<SecretDataProps[] | null | undefined>([]);
const [buttonReady, setButtonReady] = useState(false);
const router = useRouter();
const [blurred, setBlurred] = useState(true);
const [isKeyAvailable, setIsKeyAvailable] = useState(true);
@@ -137,6 +138,7 @@ export default function Dashboard() {
const [dropZoneData, setDropZoneData] = useState<SecretDataProps[]>();
const [projectTags, setProjectTags] = useState<Tag[]>([]);
const { hasUnsavedChanges, setHasUnsavedChanges } = useLeaveConfirm({initialValue: false});
const { t } = useTranslation();
const { createNotification } = useNotificationContext();
@@ -153,36 +155,6 @@ export default function Dashboard() {
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
const sortValuesHandler = (
@@ -318,7 +290,7 @@ export default function Dashboard() {
};
const deleteRow = ({ ids, secretName }: { ids: string[]; secretName: string }) => {
setButtonReady(true);
setHasUnsavedChanges(true);
toggleSidebar('None');
createNotification({
text: `${secretName || 'Secret'} has been deleted. Remember to save changes.`,
@@ -332,27 +304,27 @@ export default function Dashboard() {
const modifyValue = (value: string, pos: number) => {
setData((oldData) => oldData?.map((e) => (e.pos === pos ? { ...e, value } : e)));
setButtonReady(true);
setHasUnsavedChanges(true);
};
const modifyValueOverride = (valueOverride: string | undefined, pos: number) => {
setData((oldData) => oldData?.map((e) => (e.pos === pos ? { ...e, valueOverride } : e)));
setButtonReady(true);
setHasUnsavedChanges(true);
};
const modifyKey = (key: string, pos: number) => {
setData((oldData) => oldData?.map((e) => (e.pos === pos ? { ...e, key } : e)));
setButtonReady(true);
setHasUnsavedChanges(true);
};
const modifyComment = (comment: string, pos: number) => {
setData((oldData) => oldData?.map((e) => (e.pos === pos ? { ...e, comment } : e)));
setButtonReady(true);
setHasUnsavedChanges(true);
};
const modifyTags = (tags: Tag[], pos: number) => {
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
@@ -422,7 +394,7 @@ export default function Dashboard() {
}
// Once "Save changes" is clicked, disable that button
setButtonReady(false);
setHasUnsavedChanges(false);
const secretsToBeDeleted = initialData!
.filter(
@@ -566,7 +538,7 @@ export default function Dashboard() {
);
return filteredOldData.concat(filteredNewData);
});
setButtonReady(true);
setHasUnsavedChanges(true);
};
const addData = (newData: SecretDataProps[]) => {
@@ -587,6 +559,26 @@ export default function Dashboard() {
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 ? (
<div className="bg-bunker-800 max-h-screen h-full relative flex flex-col justify-between text-white dark">
<Head>
@@ -655,16 +647,7 @@ export default function Dashboard() {
<ListBox
isSelected={selectedEnv.name}
data={workspaceEnvs.map(({ name }) => name)}
onChange={(envName) =>
setSelectedEnv(
workspaceEnvs.find(({ name }) => envName === name) || {
name: 'unknown',
slug: 'unknown',
isWriteDenied: false,
isReadDenied: false
}
)
}
onChange={handleOnEnvironmentChange}
/>
)}
</div>
@@ -681,14 +664,14 @@ export default function Dashboard() {
icon={faClockRotateLeft}
/>}
</div>
{(data?.length !== 0 || buttonReady) && !snapshotData && (
{(data?.length !== 0 || hasUnsavedChanges) && !snapshotData && (
<div className="flex justify-start max-w-sm mt-1">
<Button
text={String(t('common:save-changes'))}
onButtonPressed={savePush}
color="primary"
size="md"
active={buttonReady}
active={hasUnsavedChanges}
iconDisabled={faCheck}
textDisabled={String(t('common:saved'))}
loading={saveLoading}
@@ -723,11 +706,11 @@ export default function Dashboard() {
text: `Rollback has been performed successfully.`,
type: 'success'
});
setButtonReady(false);
setHasUnsavedChanges(false);
}}
color="primary"
size="md"
active={buttonReady}
active={hasUnsavedChanges}
/>
</div>
)}
@@ -742,31 +725,13 @@ export default function Dashboard() {
<ListBox
isSelected={selectedEnv.name}
data={workspaceEnvs.map(({ name }) => name)}
onChange={(envName) =>
setSelectedEnv(
workspaceEnvs.find(({ name }) => envName === name) || {
name: 'unknown',
slug: 'unknown',
isWriteDenied: false,
isReadDenied: false
}
)
}
onChange={handleOnEnvironmentChange}
/>
) : (
<ListBox
isSelected={selectedSnapshotEnv?.name || ''}
data={workspaceEnvs.map(({ name }) => name)}
onChange={(envName) =>
setSelectedSnapshotEnv(
workspaceEnvs.find(({ name }) => envName === name) || {
name: 'unknown',
slug: 'unknown',
isWriteDenied: false,
isReadDenied: false
}
)
}
onChange={handleOnEnvironmentChange}
/>
)}
<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}
createNewFile={addRow}
errorDragAndDrop={errorDragAndDrop}
setButtonReady={setButtonReady}
setButtonReady={setHasUnsavedChanges}
keysExist
numCurrentRows={data.length}
/>
@@ -986,7 +951,7 @@ export default function Dashboard() {
setErrorDragAndDrop={setErrorDragAndDrop}
createNewFile={addRow}
errorDragAndDrop={errorDragAndDrop}
setButtonReady={setButtonReady}
setButtonReady={setHasUnsavedChanges}
numCurrentRows={data.length}
keysExist={false}
/>
@@ -1013,7 +978,7 @@ export default function Dashboard() {
modifyValue={listenChangeValue}
modifyValueOverride={listenChangeValueOverride}
modifyComment={listenChangeComment}
buttonReady={buttonReady}
buttonReady={hasUnsavedChanges}
workspaceEnvs={workspaceEnvs}
selectedEnv={selectedEnv!}
workspaceId={workspaceId}

View File

@@ -198,6 +198,9 @@ export default function Integrations() {
case 'flyio':
link = `${window.location.origin}/integrations/flyio/authorize`
break;
case 'circleci':
link = `${window.location.origin}/integrations/circleci/authorize`
break;
default:
break;
}
@@ -241,6 +244,9 @@ export default function Integrations() {
case 'flyio':
link = `${window.location.origin}/integrations/flyio/create?integrationAuthId=${integrationAuth._id}`;
break;
case 'circleci':
link = `${window.location.origin}/integrations/circleci/create?integrationAuthId=${integrationAuth._id}`;
break;
default:
break;
}

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