Merge branch 'main' into feat/merge-env

This commit is contained in:
mv-turtle
2023-02-07 19:57:50 -08:00
committed by GitHub
82 changed files with 2920 additions and 683 deletions
+3 -1
View File
@@ -171,7 +171,9 @@ We're currently setting the foundation and building [integrations](https://infis
🔜 GCP SM (https://github.com/Infisical/infisical/issues/285) 🔜 GCP SM (https://github.com/Infisical/infisical/issues/285)
</td> </td>
<td align="left" valign="middle"> <td align="left" valign="middle">
🔜 GitLab CI/CD (https://github.com/Infisical/infisical/issues/134) <a href="https://infisical.com/docs/integrations/cicd/gitlab">
✔️ GitLab CI/CD
</a>
</td> </td>
<td align="left" valign="middle"> <td align="left" valign="middle">
🔜 CircleCI (https://github.com/Infisical/infisical/issues/91) 🔜 CircleCI (https://github.com/Infisical/infisical/issues/91)
+18 -82
View File
@@ -10,13 +10,13 @@
"license": "ISC", "license": "ISC",
"dependencies": { "dependencies": {
"@godaddy/terminus": "^4.11.2", "@godaddy/terminus": "^4.11.2",
"@sentry/node": "^7.21.1",
"@octokit/rest": "^19.0.5", "@octokit/rest": "^19.0.5",
"@sentry/tracing": "^7.21.1", "@sentry/node": "^7.14.0",
"@sentry/tracing": "^7.19.0",
"@types/crypto-js": "^4.1.1", "@types/crypto-js": "^4.1.1",
"axios": "^1.2.0",
"@types/libsodium-wrappers": "^0.7.10", "@types/libsodium-wrappers": "^0.7.10",
"await-to-js": "^3.0.0", "await-to-js": "^3.0.0",
"axios": "^1.1.3",
"bcrypt": "^5.1.0", "bcrypt": "^5.1.0",
"bigint-conversion": "^2.2.2", "bigint-conversion": "^2.2.2",
"builder-pattern": "^2.2.0", "builder-pattern": "^2.2.0",
@@ -32,9 +32,9 @@
"js-yaml": "^4.1.0", "js-yaml": "^4.1.0",
"jsonwebtoken": "^9.0.0", "jsonwebtoken": "^9.0.0",
"jsrp": "^0.2.4", "jsrp": "^0.2.4",
"mongoose": "^6.7.3",
"libsodium-wrappers": "^0.7.10", "libsodium-wrappers": "^0.7.10",
"lodash": "^4.17.21", "lodash": "^4.17.21",
"mongoose": "^6.7.2",
"nodemailer": "^6.8.0", "nodemailer": "^6.8.0",
"posthog-node": "^2.2.2", "posthog-node": "^2.2.2",
"query-string": "^7.1.3", "query-string": "^7.1.3",
@@ -2838,19 +2838,6 @@
"@maxmind/geoip2-node": "^3.4.0" "@maxmind/geoip2-node": "^3.4.0"
} }
}, },
"node_modules/@sentry/core": {
"version": "7.21.1",
"resolved": "https://registry.npmjs.org/@sentry/core/-/core-7.21.1.tgz",
"integrity": "sha512-Og5wEEsy24fNvT/T7IKjcV4EvVK5ryY2kxbJzKY6GU2eX+i+aBl+n/vp7U0Es351C/AlTkS+0NOUsp2TQQFxZA==",
"dependencies": {
"@sentry/types": "7.21.1",
"@sentry/utils": "7.21.1",
"tslib": "^1.9.3"
},
"engines": {
"node": ">=8"
}
},
"node_modules/@protobufjs/aspromise": { "node_modules/@protobufjs/aspromise": {
"version": "1.1.2", "version": "1.1.2",
"resolved": "https://registry.npmjs.org/@protobufjs/aspromise/-/aspromise-1.1.2.tgz", "resolved": "https://registry.npmjs.org/@protobufjs/aspromise/-/aspromise-1.1.2.tgz",
@@ -2905,27 +2892,10 @@
"resolved": "https://registry.npmjs.org/@protobufjs/utf8/-/utf8-1.1.0.tgz", "resolved": "https://registry.npmjs.org/@protobufjs/utf8/-/utf8-1.1.0.tgz",
"integrity": "sha512-Vvn3zZrhQZkkBE8LSuW3em98c0FwgO4nxzv6OdSxPKJIEKY2bGbHn+mhGIPerzI4twdxaP8/0+06HBpwf345Lw==" "integrity": "sha512-Vvn3zZrhQZkkBE8LSuW3em98c0FwgO4nxzv6OdSxPKJIEKY2bGbHn+mhGIPerzI4twdxaP8/0+06HBpwf345Lw=="
}, },
"node_modules/@sentry/node": { "node_modules/@sentry/core": {
"version": "7.19.0", "version": "7.21.1",
"resolved": "https://registry.npmjs.org/@sentry/node/-/node-7.19.0.tgz", "resolved": "https://registry.npmjs.org/@sentry/core/-/core-7.21.1.tgz",
"integrity": "sha512-yG7Tx32WqOkEHVotFLrumCcT9qlaSDTkFNZ+yLSvZXx74ifsE781DzBA9W7K7bBdYO3op+p2YdsOKzf3nPpAyQ==", "integrity": "sha512-Og5wEEsy24fNvT/T7IKjcV4EvVK5ryY2kxbJzKY6GU2eX+i+aBl+n/vp7U0Es351C/AlTkS+0NOUsp2TQQFxZA==",
"dependencies": {
"@sentry/core": "7.19.0",
"@sentry/types": "7.19.0",
"@sentry/utils": "7.19.0",
"cookie": "^0.4.1",
"https-proxy-agent": "^5.0.0",
"lru_map": "^0.3.3",
"tslib": "^1.9.3"
},
"engines": {
"node": ">=8"
}
},
"node_modules/@sentry/node/node_modules/@sentry/core": {
"version": "7.19.0",
"resolved": "https://registry.npmjs.org/@sentry/core/-/core-7.19.0.tgz",
"integrity": "sha512-YF9cTBcAnO4R44092BJi5Wa2/EO02xn2ziCtmNgAVTN2LD31a/YVGxGBt/FDr4Y6yeuVehaqijVVvtpSmXrGJw==",
"dependencies": { "dependencies": {
"@sentry/types": "7.21.1", "@sentry/types": "7.21.1",
"@sentry/utils": "7.21.1", "@sentry/utils": "7.21.1",
@@ -2986,26 +2956,6 @@
"node": ">=8" "node": ">=8"
} }
}, },
"node_modules/@sentry/types": {
"version": "7.21.1",
"resolved": "https://registry.npmjs.org/@sentry/types/-/types-7.21.1.tgz",
"integrity": "sha512-3/IKnd52Ol21amQvI+kz+WB76s8/LR5YvFJzMgIoI2S8d82smIr253zGijRXxHPEif8kMLX4Yt+36VzrLxg6+A==",
"engines": {
"node": ">=8"
}
},
"node_modules/@sentry/utils": {
"version": "7.21.1",
"resolved": "https://registry.npmjs.org/@sentry/utils/-/utils-7.21.1.tgz",
"integrity": "sha512-F0W0AAi8tgtTx6ApZRI2S9HbXEA9ENX1phTZgdNNWcMFm1BNbc21XEwLqwXBNjub5nlA6CE8xnjXRgdZKx4kzQ==",
"dependencies": {
"@sentry/types": "7.21.1",
"tslib": "^1.9.3"
},
"engines": {
"node": ">=8"
}
},
"node_modules/@sinclair/typebox": { "node_modules/@sinclair/typebox": {
"version": "0.24.51", "version": "0.24.51",
"resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.24.51.tgz", "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.24.51.tgz",
@@ -14306,16 +14256,6 @@
"@maxmind/geoip2-node": "^3.4.0" "@maxmind/geoip2-node": "^3.4.0"
} }
}, },
"@sentry/core": {
"version": "7.21.1",
"resolved": "https://registry.npmjs.org/@sentry/core/-/core-7.21.1.tgz",
"integrity": "sha512-Og5wEEsy24fNvT/T7IKjcV4EvVK5ryY2kxbJzKY6GU2eX+i+aBl+n/vp7U0Es351C/AlTkS+0NOUsp2TQQFxZA==",
"requires": {
"@sentry/types": "7.21.1",
"@sentry/utils": "7.21.1",
"tslib": "^1.9.3"
}
},
"@protobufjs/aspromise": { "@protobufjs/aspromise": {
"version": "1.1.2", "version": "1.1.2",
"resolved": "https://registry.npmjs.org/@protobufjs/aspromise/-/aspromise-1.1.2.tgz", "resolved": "https://registry.npmjs.org/@protobufjs/aspromise/-/aspromise-1.1.2.tgz",
@@ -14370,6 +14310,16 @@
"resolved": "https://registry.npmjs.org/@protobufjs/utf8/-/utf8-1.1.0.tgz", "resolved": "https://registry.npmjs.org/@protobufjs/utf8/-/utf8-1.1.0.tgz",
"integrity": "sha512-Vvn3zZrhQZkkBE8LSuW3em98c0FwgO4nxzv6OdSxPKJIEKY2bGbHn+mhGIPerzI4twdxaP8/0+06HBpwf345Lw==" "integrity": "sha512-Vvn3zZrhQZkkBE8LSuW3em98c0FwgO4nxzv6OdSxPKJIEKY2bGbHn+mhGIPerzI4twdxaP8/0+06HBpwf345Lw=="
}, },
"@sentry/core": {
"version": "7.21.1",
"resolved": "https://registry.npmjs.org/@sentry/core/-/core-7.21.1.tgz",
"integrity": "sha512-Og5wEEsy24fNvT/T7IKjcV4EvVK5ryY2kxbJzKY6GU2eX+i+aBl+n/vp7U0Es351C/AlTkS+0NOUsp2TQQFxZA==",
"requires": {
"@sentry/types": "7.21.1",
"@sentry/utils": "7.21.1",
"tslib": "^1.9.3"
}
},
"@sentry/node": { "@sentry/node": {
"version": "7.21.1", "version": "7.21.1",
"resolved": "https://registry.npmjs.org/@sentry/node/-/node-7.21.1.tgz", "resolved": "https://registry.npmjs.org/@sentry/node/-/node-7.21.1.tgz",
@@ -14409,20 +14359,6 @@
"tslib": "^1.9.3" "tslib": "^1.9.3"
} }
}, },
"@sentry/types": {
"version": "7.21.1",
"resolved": "https://registry.npmjs.org/@sentry/types/-/types-7.21.1.tgz",
"integrity": "sha512-3/IKnd52Ol21amQvI+kz+WB76s8/LR5YvFJzMgIoI2S8d82smIr253zGijRXxHPEif8kMLX4Yt+36VzrLxg6+A=="
},
"@sentry/utils": {
"version": "7.21.1",
"resolved": "https://registry.npmjs.org/@sentry/utils/-/utils-7.21.1.tgz",
"integrity": "sha512-F0W0AAi8tgtTx6ApZRI2S9HbXEA9ENX1phTZgdNNWcMFm1BNbc21XEwLqwXBNjub5nlA6CE8xnjXRgdZKx4kzQ==",
"requires": {
"@sentry/types": "7.21.1",
"tslib": "^1.9.3"
}
},
"@sinclair/typebox": { "@sinclair/typebox": {
"version": "0.24.51", "version": "0.24.51",
"resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.24.51.tgz", "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.24.51.tgz",
+6
View File
@@ -13,10 +13,13 @@ const MONGO_URL = process.env.MONGO_URL!;
const NODE_ENV = process.env.NODE_ENV! || 'production'; const NODE_ENV = process.env.NODE_ENV! || 'production';
const VERBOSE_ERROR_OUTPUT = process.env.VERBOSE_ERROR_OUTPUT! === 'true' && true; const VERBOSE_ERROR_OUTPUT = process.env.VERBOSE_ERROR_OUTPUT! === 'true' && true;
const LOKI_HOST = process.env.LOKI_HOST || undefined; const LOKI_HOST = process.env.LOKI_HOST || undefined;
const CLIENT_ID_AZURE = process.env.CLIENT_ID_AZURE!;
const TENANT_ID_AZURE = process.env.TENANT_ID_AZURE!;
const CLIENT_ID_HEROKU = process.env.CLIENT_ID_HEROKU!; const CLIENT_ID_HEROKU = process.env.CLIENT_ID_HEROKU!;
const CLIENT_ID_VERCEL = process.env.CLIENT_ID_VERCEL!; const CLIENT_ID_VERCEL = process.env.CLIENT_ID_VERCEL!;
const CLIENT_ID_NETLIFY = process.env.CLIENT_ID_NETLIFY!; const CLIENT_ID_NETLIFY = process.env.CLIENT_ID_NETLIFY!;
const CLIENT_ID_GITHUB = process.env.CLIENT_ID_GITHUB!; const CLIENT_ID_GITHUB = process.env.CLIENT_ID_GITHUB!;
const CLIENT_SECRET_AZURE = process.env.CLIENT_SECRET_AZURE!;
const CLIENT_SECRET_HEROKU = process.env.CLIENT_SECRET_HEROKU!; const CLIENT_SECRET_HEROKU = process.env.CLIENT_SECRET_HEROKU!;
const CLIENT_SECRET_VERCEL = process.env.CLIENT_SECRET_VERCEL!; const CLIENT_SECRET_VERCEL = process.env.CLIENT_SECRET_VERCEL!;
const CLIENT_SECRET_NETLIFY = process.env.CLIENT_SECRET_NETLIFY!; const CLIENT_SECRET_NETLIFY = process.env.CLIENT_SECRET_NETLIFY!;
@@ -60,10 +63,13 @@ export {
NODE_ENV, NODE_ENV,
VERBOSE_ERROR_OUTPUT, VERBOSE_ERROR_OUTPUT,
LOKI_HOST, LOKI_HOST,
CLIENT_ID_AZURE,
TENANT_ID_AZURE,
CLIENT_ID_HEROKU, CLIENT_ID_HEROKU,
CLIENT_ID_VERCEL, CLIENT_ID_VERCEL,
CLIENT_ID_NETLIFY, CLIENT_ID_NETLIFY,
CLIENT_ID_GITHUB, CLIENT_ID_GITHUB,
CLIENT_SECRET_AZURE,
CLIENT_SECRET_HEROKU, CLIENT_SECRET_HEROKU,
CLIENT_SECRET_VERCEL, CLIENT_SECRET_VERCEL,
CLIENT_SECRET_NETLIFY, CLIENT_SECRET_NETLIFY,
@@ -10,6 +10,31 @@ import { INTEGRATION_SET, INTEGRATION_OPTIONS } from '../../variables';
import { IntegrationService } from '../../services'; import { IntegrationService } from '../../services';
import { getApps, revokeAccess } from '../../integrations'; import { getApps, revokeAccess } from '../../integrations';
/***
* Return integration authorization with id [integrationAuthId]
*/
export const getIntegrationAuth = async (req: Request, res: Response) => {
let integrationAuth;
try {
const { integrationAuthId } = req.params;
integrationAuth = await IntegrationAuth.findById(integrationAuthId);
if (!integrationAuth) return res.status(400).send({
message: 'Failed to find integration authorization'
});
} catch (err) {
Sentry.setUser({ email: req.user.email });
Sentry.captureException(err);
return res.status(400).send({
message: 'Failed to get integration authorization'
});
}
return res.status(200).send({
integrationAuth
});
}
export const getIntegrationOptions = async ( export const getIntegrationOptions = async (
req: Request, req: Request,
res: Response res: Response
@@ -31,7 +56,6 @@ export const oAuthExchange = async (
) => { ) => {
try { try {
const { workspaceId, code, integration } = req.body; const { workspaceId, code, integration } = req.body;
if (!INTEGRATION_SET.has(integration)) if (!INTEGRATION_SET.has(integration))
throw new Error('Failed to validate integration'); throw new Error('Failed to validate integration');
@@ -40,12 +64,16 @@ export const oAuthExchange = async (
throw new Error("Failed to get environments") throw new Error("Failed to get environments")
} }
await IntegrationService.handleOAuthExchange({ const integrationAuth = await IntegrationService.handleOAuthExchange({
workspaceId, workspaceId,
integration, integration,
code, code,
environment: environments[0].slug, environment: environments[0].slug,
}); });
return res.status(200).send({
integrationAuth
});
} catch (err) { } catch (err) {
Sentry.setUser({ email: req.user.email }); Sentry.setUser({ email: req.user.email });
Sentry.captureException(err); Sentry.captureException(err);
@@ -53,10 +81,6 @@ export const oAuthExchange = async (
message: 'Failed to get OAuth2 code-token exchange' message: 'Failed to get OAuth2 code-token exchange'
}); });
} }
return res.status(200).send({
message: 'Successfully enabled integration authorization'
});
}; };
/** /**
@@ -81,6 +105,13 @@ export const saveIntegrationAccessToken = async (
integration: string; integration: string;
} = req.body; } = req.body;
const bot = await Bot.findOne({
workspace: new Types.ObjectId(workspaceId),
isActive: true
});
if (!bot) throw new Error('Bot must be enabled to save integration access token');
integrationAuth = await IntegrationAuth.findOneAndUpdate({ integrationAuth = await IntegrationAuth.findOneAndUpdate({
workspace: new Types.ObjectId(workspaceId), workspace: new Types.ObjectId(workspaceId),
integration integration
@@ -92,13 +123,6 @@ export const saveIntegrationAccessToken = async (
upsert: true upsert: true
}); });
const bot = await Bot.findOne({
workspace: new Types.ObjectId(workspaceId),
isActive: true
});
if (!bot) throw new Error('Bot must be enabled to save integration access token');
// encrypt and save integration access token // encrypt and save integration access token
integrationAuth = await IntegrationService.setIntegrationAuthAccess({ integrationAuth = await IntegrationService.setIntegrationAuthAccess({
integrationAuthId: integrationAuth._id.toString(), integrationAuthId: integrationAuth._id.toString(),
@@ -1,4 +1,5 @@
import { Request, Response } from 'express'; import { Request, Response } from 'express';
import { Types } from 'mongoose';
import * as Sentry from '@sentry/node'; import * as Sentry from '@sentry/node';
import { import {
Integration, Integration,
@@ -18,15 +19,40 @@ import { eventPushSecrets } from '../../events';
export const createIntegration = async (req: Request, res: Response) => { export const createIntegration = async (req: Request, res: Response) => {
let integration; let integration;
try { try {
const {
integrationAuthId,
app,
appId,
isActive,
sourceEnvironment,
targetEnvironment,
owner
} = req.body;
// TODO: validate [sourceEnvironment] and [targetEnvironment]
// initialize new integration after saving integration access token // initialize new integration after saving integration access token
integration = await new Integration({ integration = await new Integration({
workspace: req.integrationAuth.workspace._id, workspace: req.integrationAuth.workspace._id,
isActive: false, environment: sourceEnvironment,
app: null, isActive,
environment: req.integrationAuth.workspace?.environments[0].slug, app,
appId,
targetEnvironment,
owner,
integration: req.integrationAuth.integration, integration: req.integrationAuth.integration,
integrationAuth: req.integrationAuth._id integrationAuth: new Types.ObjectId(integrationAuthId)
}).save(); }).save();
if (integration) {
// trigger event - push secrets
EventService.handleEvent({
event: eventPushSecrets({
workspaceId: integration.workspace.toString()
})
});
}
} catch (err) { } catch (err) {
Sentry.setUser({ email: req.user.email }); Sentry.setUser({ email: req.user.email });
Sentry.captureException(err); Sentry.captureException(err);
@@ -103,6 +103,9 @@ export const createSecrets = async (req: Request, res: Response) => {
secretValueCiphertext: string; secretValueCiphertext: string;
secretValueIV: string; secretValueIV: string;
secretValueTag: string; secretValueTag: string;
secretCommentCiphertext: string;
secretCommentIV: string;
secretCommentTag: string;
tags: string[] tags: string[]
} }
@@ -115,6 +118,9 @@ export const createSecrets = async (req: Request, res: Response) => {
secretValueCiphertext, secretValueCiphertext,
secretValueIV, secretValueIV,
secretValueTag, secretValueTag,
secretCommentCiphertext,
secretCommentIV,
secretCommentTag,
tags tags
}: secretsToCreateType) => { }: secretsToCreateType) => {
return ({ return ({
@@ -129,6 +135,9 @@ export const createSecrets = async (req: Request, res: Response) => {
secretValueCiphertext, secretValueCiphertext,
secretValueIV, secretValueIV,
secretValueTag, secretValueTag,
secretCommentCiphertext,
secretCommentIV,
secretCommentTag,
tags tags
}); });
}) })
@@ -160,6 +169,9 @@ export const createSecrets = async (req: Request, res: Response) => {
secretValueIV, secretValueIV,
secretValueTag, secretValueTag,
secretValueHash, secretValueHash,
secretCommentCiphertext,
secretCommentIV,
secretCommentTag,
tags tags
}) => ({ }) => ({
_id: new Types.ObjectId(), _id: new Types.ObjectId(),
@@ -178,6 +190,9 @@ export const createSecrets = async (req: Request, res: Response) => {
secretValueIV, secretValueIV,
secretValueTag, secretValueTag,
secretValueHash, secretValueHash,
secretCommentCiphertext,
secretCommentIV,
secretCommentTag,
tags tags
})) }))
}); });
+2 -2
View File
@@ -52,9 +52,9 @@ export const deleteWorkspaceTag = async (req: Request, res: Response) => {
UnauthorizedRequestError({ message: 'Failed to validate membership' }); UnauthorizedRequestError({ message: 'Failed to validate membership' });
} }
await Tag.findByIdAndDelete(tagId) const result = await Tag.findByIdAndDelete(tagId);
res.sendStatus(200) res.json(result);
} }
export const getWorkspaceTags = async (req: Request, res: Response) => { export const getWorkspaceTags = async (req: Request, res: Response) => {
@@ -15,7 +15,13 @@ export const getSecretSnapshot = async (req: Request, res: Response) => {
secretSnapshot = await SecretSnapshot secretSnapshot = await SecretSnapshot
.findById(secretSnapshotId) .findById(secretSnapshotId)
.populate('secretVersions'); .populate({
path: 'secretVersions',
populate: {
path: 'tags',
model: 'Tag',
}
});
if (!secretSnapshot) throw new Error('Failed to find secret snapshot'); if (!secretSnapshot) throw new Error('Failed to find secret snapshot');
+3 -11
View File
@@ -30,6 +30,7 @@ interface Update {
* @param {String} obj.workspaceId - id of workspace * @param {String} obj.workspaceId - id of workspace
* @param {String} obj.integration - name of integration * @param {String} obj.integration - name of integration
* @param {String} obj.code - code * @param {String} obj.code - code
* @returns {IntegrationAuth} integrationAuth - integration auth after OAuth2 code-token exchange
*/ */
const handleOAuthExchangeHelper = async ({ const handleOAuthExchangeHelper = async ({
workspaceId, workspaceId,
@@ -42,7 +43,6 @@ const handleOAuthExchangeHelper = async ({
code: string; code: string;
environment: string; environment: string;
}) => { }) => {
let action;
let integrationAuth; let integrationAuth;
try { try {
const bot = await Bot.findOne({ const bot = await Bot.findOne({
@@ -98,21 +98,13 @@ const handleOAuthExchangeHelper = async ({
accessExpiresAt: res.accessExpiresAt accessExpiresAt: res.accessExpiresAt
}); });
} }
// initialize new integration after exchange
await new Integration({
workspace: workspaceId,
isActive: false,
app: null,
environment,
integration,
integrationAuth: integrationAuth._id
}).save();
} catch (err) { } catch (err) {
Sentry.setUser(null); Sentry.setUser(null);
Sentry.captureException(err); Sentry.captureException(err);
throw new Error('Failed to handle OAuth2 code-token exchange') throw new Error('Failed to handle OAuth2 code-token exchange')
} }
return integrationAuth;
} }
/** /**
* Sync/push environment variables in workspace with id [workspaceId] to * Sync/push environment variables in workspace with id [workspaceId] to
+22 -2
View File
@@ -3,6 +3,7 @@ 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_HEROKU, INTEGRATION_HEROKU,
INTEGRATION_VERCEL, INTEGRATION_VERCEL,
INTEGRATION_NETLIFY, INTEGRATION_NETLIFY,
@@ -40,6 +41,11 @@ const getApps = async ({
let apps: App[]; let apps: App[];
try { try {
switch (integrationAuth.integration) { switch (integrationAuth.integration) {
case INTEGRATION_AZURE_KEY_VAULT:
apps = await getAppsAzureKeyVault({
accessToken
});
break;
case INTEGRATION_HEROKU: case INTEGRATION_HEROKU:
apps = await getAppsHeroku({ apps = await getAppsHeroku({
accessToken accessToken
@@ -81,6 +87,15 @@ const getApps = async ({
return apps; return apps;
}; };
const getAppsAzureKeyVault = async ({
accessToken
}: {
accessToken: string;
}) => {
// TODO
return [];
}
/** /**
* Return list of apps for Heroku integration * Return list of apps for Heroku integration
* @param {Object} obj * @param {Object} obj
@@ -247,7 +262,9 @@ const getAppsRender = async ({
const res = ( const res = (
await axios.get(`${INTEGRATION_RENDER_API_URL}/v1/services`, { await axios.get(`${INTEGRATION_RENDER_API_URL}/v1/services`, {
headers: { headers: {
Authorization: `Bearer ${accessToken}` Authorization: `Bearer ${accessToken}`,
Accept: 'application/json',
'Accept-Encoding': 'application/json'
} }
}) })
).data; ).data;
@@ -257,6 +274,7 @@ const getAppsRender = async ({
name: a.service.name, name: a.service.name,
appId: a.service.id appId: a.service.id
})); }));
} catch (err) { } catch (err) {
Sentry.setUser(null); Sentry.setUser(null);
Sentry.captureException(err); Sentry.captureException(err);
@@ -296,7 +314,9 @@ const getAppsFlyio = async ({
url: INTEGRATION_FLYIO_API_URL, url: INTEGRATION_FLYIO_API_URL,
method: 'post', method: 'post',
headers: { headers: {
'Authorization': 'Bearer ' + accessToken 'Authorization': 'Bearer ' + accessToken,
'Accept': 'application/json',
'Accept-Encoding': 'application/json'
}, },
data: { data: {
query, query,
+64 -4
View File
@@ -1,10 +1,12 @@
import axios from 'axios'; import axios from 'axios';
import * as Sentry from '@sentry/node'; import * as Sentry from '@sentry/node';
import { import {
INTEGRATION_AZURE_KEY_VAULT,
INTEGRATION_HEROKU, INTEGRATION_HEROKU,
INTEGRATION_VERCEL, INTEGRATION_VERCEL,
INTEGRATION_NETLIFY, INTEGRATION_NETLIFY,
INTEGRATION_GITHUB, INTEGRATION_GITHUB,
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,
@@ -12,15 +14,27 @@ import {
} from '../variables'; } from '../variables';
import { import {
SITE_URL, SITE_URL,
CLIENT_ID_AZURE,
CLIENT_ID_VERCEL, CLIENT_ID_VERCEL,
CLIENT_ID_NETLIFY, CLIENT_ID_NETLIFY,
CLIENT_ID_GITHUB, CLIENT_ID_GITHUB,
CLIENT_SECRET_AZURE,
CLIENT_SECRET_HEROKU, CLIENT_SECRET_HEROKU,
CLIENT_SECRET_VERCEL, CLIENT_SECRET_VERCEL,
CLIENT_SECRET_NETLIFY, CLIENT_SECRET_NETLIFY,
CLIENT_SECRET_GITHUB CLIENT_SECRET_GITHUB
} from '../config'; } from '../config';
interface ExchangeCodeAzureResponse {
token_type: string;
scope: string;
expires_in: number;
ext_expires_in: number;
access_token: string;
refresh_token: string;
id_token: string;
}
interface ExchangeCodeHerokuResponse { interface ExchangeCodeHerokuResponse {
token_type: string; token_type: string;
access_token: string; access_token: string;
@@ -75,6 +89,11 @@ const exchangeCode = async ({
try { try {
switch (integration) { switch (integration) {
case INTEGRATION_AZURE_KEY_VAULT:
obj = await exchangeCodeAzure({
code
});
break;
case INTEGRATION_HEROKU: case INTEGRATION_HEROKU:
obj = await exchangeCodeHeroku({ obj = await exchangeCodeHeroku({
code code
@@ -105,6 +124,46 @@ const exchangeCode = async ({
return obj; return obj;
}; };
/**
* Return [accessToken] for Azure OAuth2 code-token exchange
* @param param0
*/
const exchangeCodeAzure = async ({
code
}: {
code: string;
}) => {
const accessExpiresAt = new Date();
let res: ExchangeCodeAzureResponse;
try {
res = (await axios.post(
INTEGRATION_AZURE_TOKEN_URL,
new URLSearchParams({
grant_type: 'authorization_code',
code: code,
scope: 'https://vault.azure.net/.default openid offline_access', // TODO: do we need all these permissions?
client_id: CLIENT_ID_AZURE,
client_secret: CLIENT_SECRET_AZURE,
redirect_uri: `${SITE_URL}/integrations/azure-key-vault/oauth2/callback`
} as any)
)).data;
accessExpiresAt.setSeconds(
accessExpiresAt.getSeconds() + res.expires_in
);
} catch (err: any) {
Sentry.setUser(null);
Sentry.captureException(err);
throw new Error('Failed OAuth2 code-token exchange with Azure');
}
return ({
accessToken: res.access_token,
refreshToken: res.refresh_token,
accessExpiresAt
});
}
/** /**
* Return [accessToken], [accessExpiresAt], and [refreshToken] for Heroku * Return [accessToken], [accessExpiresAt], and [refreshToken] for Heroku
* OAuth2 code-token exchange * OAuth2 code-token exchange
@@ -168,7 +227,7 @@ const exchangeCodeVercel = async ({ code }: { code: string }) => {
code: code, code: code,
client_id: CLIENT_ID_VERCEL, client_id: CLIENT_ID_VERCEL,
client_secret: CLIENT_SECRET_VERCEL, client_secret: CLIENT_SECRET_VERCEL,
redirect_uri: `${SITE_URL}/vercel` redirect_uri: `${SITE_URL}/integrations/vercel/oauth2/callback`
} as any) } as any)
) )
).data; ).data;
@@ -208,7 +267,7 @@ const exchangeCodeNetlify = async ({ code }: { code: string }) => {
code: code, code: code,
client_id: CLIENT_ID_NETLIFY, client_id: CLIENT_ID_NETLIFY,
client_secret: CLIENT_SECRET_NETLIFY, client_secret: CLIENT_SECRET_NETLIFY,
redirect_uri: `${SITE_URL}/netlify` redirect_uri: `${SITE_URL}/integrations/netlify/oauth2/callback`
} as any) } as any)
) )
).data; ).data;
@@ -260,10 +319,11 @@ const exchangeCodeGithub = async ({ code }: { code: string }) => {
client_id: CLIENT_ID_GITHUB, client_id: CLIENT_ID_GITHUB,
client_secret: CLIENT_SECRET_GITHUB, client_secret: CLIENT_SECRET_GITHUB,
code: code, code: code,
redirect_uri: `${SITE_URL}/github` redirect_uri: `${SITE_URL}/integrations/github/oauth2/callback`
}, },
headers: { headers: {
Accept: 'application/json' 'Accept': 'application/json',
'Accept-Encoding': 'application/json'
} }
}) })
).data; ).data;
+65 -15
View File
@@ -1,13 +1,26 @@
import axios from 'axios'; import axios from 'axios';
import * as Sentry from '@sentry/node'; import * as Sentry from '@sentry/node';
import { INTEGRATION_HEROKU } from '../variables'; import { INTEGRATION_AZURE_KEY_VAULT, INTEGRATION_HEROKU } from '../variables';
import { import {
CLIENT_SECRET_HEROKU SITE_URL,
CLIENT_ID_AZURE,
CLIENT_SECRET_AZURE,
CLIENT_SECRET_HEROKU
} from '../config'; } from '../config';
import { import {
INTEGRATION_HEROKU_TOKEN_URL INTEGRATION_AZURE_TOKEN_URL,
INTEGRATION_HEROKU_TOKEN_URL
} from '../variables'; } from '../variables';
interface RefreshTokenAzureResponse {
token_type: string;
scope: string;
expires_in: number;
ext_expires_in: 4871;
access_token: string;
refresh_token: string;
}
/** /**
* Return new access token by exchanging refresh token [refreshToken] for integration * Return new access token by exchanging refresh token [refreshToken] for integration
* named [integration] * named [integration]
@@ -25,6 +38,11 @@ const exchangeRefresh = async ({
let accessToken; let accessToken;
try { try {
switch (integration) { switch (integration) {
case INTEGRATION_AZURE_KEY_VAULT:
accessToken = await exchangeRefreshAzure({
refreshToken
});
break;
case INTEGRATION_HEROKU: case INTEGRATION_HEROKU:
accessToken = await exchangeRefreshHeroku({ accessToken = await exchangeRefreshHeroku({
refreshToken refreshToken
@@ -40,6 +58,38 @@ const exchangeRefresh = async ({
return accessToken; return accessToken;
}; };
/**
* Return new access token by exchanging refresh token [refreshToken] for the
* Azure integration
* @param {Object} obj
* @param {String} obj.refreshToken - refresh token to use to get new access token for Azure
* @returns
*/
const exchangeRefreshAzure = async ({
refreshToken
}: {
refreshToken: string;
}) => {
try {
const res: RefreshTokenAzureResponse = (await axios.post(
INTEGRATION_AZURE_TOKEN_URL,
new URLSearchParams({
client_id: CLIENT_ID_AZURE,
scope: 'openid offline_access',
refresh_token: refreshToken,
grant_type: 'refresh_token',
client_secret: CLIENT_SECRET_AZURE
} as any)
)).data;
return res.access_token;
} catch (err) {
Sentry.setUser(null);
Sentry.captureException(err);
throw new Error('Failed to get refresh OAuth2 access token for Azure');
}
}
/** /**
* Return new access token by exchanging refresh token [refreshToken] for the * Return new access token by exchanging refresh token [refreshToken] for the
* Heroku integration * Heroku integration
@@ -52,23 +102,23 @@ const exchangeRefreshHeroku = async ({
}: { }: {
refreshToken: string; refreshToken: string;
}) => { }) => {
let accessToken;
//TODO: Refactor code to take advantage of using RequestError. It's possible to create new types of errors for more detailed errors let accessToken;
try { try {
const res = await axios.post( const res = await axios.post(
INTEGRATION_HEROKU_TOKEN_URL, INTEGRATION_HEROKU_TOKEN_URL,
new URLSearchParams({ new URLSearchParams({
grant_type: 'refresh_token', grant_type: 'refresh_token',
refresh_token: refreshToken, refresh_token: refreshToken,
client_secret: CLIENT_SECRET_HEROKU client_secret: CLIENT_SECRET_HEROKU
} as any) } as any)
); );
accessToken = res.data.access_token; accessToken = res.data.access_token;
} catch (err) { } catch (err) {
Sentry.setUser(null); Sentry.setUser(null);
Sentry.captureException(err); Sentry.captureException(err);
throw new Error('Failed to get new OAuth2 access token for Heroku'); throw new Error('Failed to refresh OAuth2 access token for Heroku');
} }
return accessToken; return accessToken;
+153 -3
View File
@@ -1,11 +1,10 @@
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 * as sodium from 'libsodium-wrappers';
import sodium from 'libsodium-wrappers'; import sodium from 'libsodium-wrappers';
// const sodium = require('libsodium-wrappers');
import { IIntegration, IIntegrationAuth } from '../models'; import { IIntegration, IIntegrationAuth } from '../models';
import { import {
INTEGRATION_AZURE_KEY_VAULT,
INTEGRATION_HEROKU, INTEGRATION_HEROKU,
INTEGRATION_VERCEL, INTEGRATION_VERCEL,
INTEGRATION_NETLIFY, INTEGRATION_NETLIFY,
@@ -18,7 +17,6 @@ import {
INTEGRATION_RENDER_API_URL, INTEGRATION_RENDER_API_URL,
INTEGRATION_FLYIO_API_URL INTEGRATION_FLYIO_API_URL
} from '../variables'; } from '../variables';
import { access, appendFile } from 'fs';
/** /**
* Sync/push [secrets] to [app] in integration named [integration] * Sync/push [secrets] to [app] in integration named [integration]
@@ -41,6 +39,13 @@ const syncSecrets = async ({
}) => { }) => {
try { try {
switch (integration.integration) { switch (integration.integration) {
case INTEGRATION_AZURE_KEY_VAULT:
await syncSecretsAzureKeyVault({
integration,
secrets,
accessToken
});
break;
case INTEGRATION_HEROKU: case INTEGRATION_HEROKU:
await syncSecretsHeroku({ await syncSecretsHeroku({
integration, integration,
@@ -93,6 +98,151 @@ const syncSecrets = async ({
} }
}; };
/**
* Sync/push [secrets] to Azure Key Vault with vault URI [integration.app]
* @param {Object} obj
* @param {IIntegration} obj.integration - integration details
* @param {Object} obj.secrets - secrets to push to integration (object where keys are secret keys and values are secret values)
* @param {String} obj.accessToken - access token for Azure Key Vault integration
*/
const syncSecretsAzureKeyVault = async ({
integration,
secrets,
accessToken
}: {
integration: IIntegration;
secrets: any;
accessToken: string;
}) => {
try {
interface GetAzureKeyVaultSecret {
id: string; // secret URI
attributes: {
enabled: true,
created: number;
updated: number;
recoveryLevel: string;
recoverableDays: number;
}
}
interface AzureKeyVaultSecret extends GetAzureKeyVaultSecret {
key: string;
}
/**
* Return all secrets from Azure Key Vault by paginating through URL [url]
* @param {String} url - pagination URL to get next set of secrets from Azure Key Vault
* @returns
*/
const paginateAzureKeyVaultSecrets = async (url: string) => {
let result: GetAzureKeyVaultSecret[] = [];
while (url) {
const res = await axios.get(url, {
headers: {
Authorization: `Bearer ${accessToken}`
}
});
result = result.concat(res.data.value);
url = res.data.nextLink;
}
return result;
}
const getAzureKeyVaultSecrets = await paginateAzureKeyVaultSecrets(`${integration.app}/secrets?api-version=7.3`);
let lastSlashIndex: number;
const res = (await Promise.all(getAzureKeyVaultSecrets.map(async (getAzureKeyVaultSecret) => {
if (!lastSlashIndex) {
lastSlashIndex = getAzureKeyVaultSecret.id.lastIndexOf('/');
}
const azureKeyVaultSecret = await axios.get(`${getAzureKeyVaultSecret.id}?api-version=7.3`, {
headers: {
'Authorization': `Bearer ${accessToken}`
}
});
return ({
...azureKeyVaultSecret.data,
key: getAzureKeyVaultSecret.id.substring(lastSlashIndex + 1),
});
})))
.reduce((obj: any, secret: any) => ({
...obj,
[secret.key]: secret
}), {});
const setSecrets: {
key: string;
value: string;
}[] = [];
Object.keys(secrets).forEach((key) => {
const hyphenatedKey = key.replace(/_/g, '-');
if (!(hyphenatedKey in res)) {
// case: secret has been created
setSecrets.push({
key: hyphenatedKey,
value: secrets[key]
});
} else {
if (secrets[key] !== res[hyphenatedKey].value) {
// case: secret has been updated
setSecrets.push({
key: hyphenatedKey,
value: secrets[key]
});
}
}
});
const deleteSecrets: AzureKeyVaultSecret[] = [];
Object.keys(res).forEach((key) => {
const underscoredKey = key.replace(/-/g, '_');
if (!(underscoredKey in secrets)) {
deleteSecrets.push(res[key]);
}
});
// Sync/push set secrets
if (setSecrets.length > 0) {
setSecrets.forEach(async ({ key, value }) => {
await axios.put(
`${integration.app}/secrets/${key}?api-version=7.3`,
{
value
},
{
headers: {
Authorization: `Bearer ${accessToken}`
}
}
);
});
}
if (deleteSecrets.length > 0) {
deleteSecrets.forEach(async (secret) => {
await axios.delete(`${integration.app}/secrets/${secret.key}?api-version=7.3`, {
headers: {
'Authorization': `Bearer ${accessToken}`
}
});
});
}
} catch (err) {
Sentry.setUser(null);
Sentry.captureException(err);
throw new Error('Failed to sync secrets to Azure Key Vault');
}
};
/** /**
* Sync/push [secrets] to Heroku app named [integration.app] * Sync/push [secrets] to Heroku app named [integration.app]
* @param {Object} obj * @param {Object} obj
+3 -1
View File
@@ -1,5 +1,6 @@
import { Schema, model, Types } from 'mongoose'; import { Schema, model, Types } from 'mongoose';
import { import {
INTEGRATION_AZURE_KEY_VAULT,
INTEGRATION_HEROKU, INTEGRATION_HEROKU,
INTEGRATION_VERCEL, INTEGRATION_VERCEL,
INTEGRATION_NETLIFY, INTEGRATION_NETLIFY,
@@ -17,7 +18,7 @@ export interface IIntegration {
owner: string; owner: string;
targetEnvironment: string; targetEnvironment: string;
appId: string; appId: string;
integration: 'heroku' | 'vercel' | 'netlify' | 'github' | 'render' | 'flyio'; integration: 'heroku' | 'vercel' | 'netlify' | 'github' | 'render' | 'flyio' | 'azure-key-vault';
integrationAuth: Types.ObjectId; integrationAuth: Types.ObjectId;
} }
@@ -59,6 +60,7 @@ const integrationSchema = new Schema<IIntegration>(
integration: { integration: {
type: String, type: String,
enum: [ enum: [
INTEGRATION_AZURE_KEY_VAULT,
INTEGRATION_HEROKU, INTEGRATION_HEROKU,
INTEGRATION_VERCEL, INTEGRATION_VERCEL,
INTEGRATION_NETLIFY, INTEGRATION_NETLIFY,
+3 -1
View File
@@ -1,5 +1,6 @@
import { Schema, model, Types } from 'mongoose'; import { Schema, model, Types } from 'mongoose';
import { import {
INTEGRATION_AZURE_KEY_VAULT,
INTEGRATION_HEROKU, INTEGRATION_HEROKU,
INTEGRATION_VERCEL, INTEGRATION_VERCEL,
INTEGRATION_NETLIFY, INTEGRATION_NETLIFY,
@@ -9,7 +10,7 @@ import {
export interface IIntegrationAuth { export interface IIntegrationAuth {
_id: Types.ObjectId; _id: Types.ObjectId;
workspace: Types.ObjectId; workspace: Types.ObjectId;
integration: 'heroku' | 'vercel' | 'netlify' | 'github' | 'render' | 'flyio'; integration: 'heroku' | 'vercel' | 'netlify' | 'github' | 'render' | 'flyio' | 'azure-key-vault';
teamId: string; teamId: string;
accountId: string; accountId: string;
refreshCiphertext?: string; refreshCiphertext?: string;
@@ -31,6 +32,7 @@ const integrationAuthSchema = new Schema<IIntegrationAuth>(
integration: { integration: {
type: String, type: String,
enum: [ enum: [
INTEGRATION_AZURE_KEY_VAULT,
INTEGRATION_HEROKU, INTEGRATION_HEROKU,
INTEGRATION_VERCEL, INTEGRATION_VERCEL,
INTEGRATION_NETLIFY, INTEGRATION_NETLIFY,
+8 -2
View File
@@ -10,7 +10,7 @@ import { ADMIN, MEMBER } from '../../variables';
import { body, param } from 'express-validator'; import { body, param } from 'express-validator';
import { integrationController } from '../../controllers/v1'; import { integrationController } from '../../controllers/v1';
router.post( // new: add new integration router.post( // new: add new integration for integration auth
'/', '/',
requireAuth({ requireAuth({
acceptedAuthModes: ['jwt', 'apiKey'] acceptedAuthModes: ['jwt', 'apiKey']
@@ -19,7 +19,13 @@ router.post( // new: add new integration
acceptedRoles: [ADMIN, MEMBER], acceptedRoles: [ADMIN, MEMBER],
location: 'body' location: 'body'
}), }),
body('integrationAuthId').exists().trim(), body('integrationAuthId').exists().isString().trim(),
body('app').isString().trim(),
body('isActive').exists().isBoolean(),
body('appId').trim(),
body('sourceEnvironment').trim(),
body('targetEnvironment').trim(),
body('owner').trim(),
validateRequest, validateRequest,
integrationController.createIntegration integrationController.createIntegration
); );
+13
View File
@@ -18,6 +18,19 @@ router.get(
integrationAuthController.getIntegrationOptions integrationAuthController.getIntegrationOptions
); );
router.get(
'/:integrationAuthId',
requireAuth({
acceptedAuthModes: ['jwt']
}),
requireIntegrationAuthorizationAuth({
acceptedRoles: [ADMIN, MEMBER]
}),
param('integrationAuthId'),
validateRequest,
integrationAuthController.getIntegrationAuth
);
router.post( router.post(
'/oauth-token', '/oauth-token',
requireAuth({ requireAuth({
+7 -11
View File
@@ -1,7 +1,3 @@
import * as Sentry from '@sentry/node';
import {
Integration
} from '../models';
import { import {
handleOAuthExchangeHelper, handleOAuthExchangeHelper,
syncIntegrationsHelper, syncIntegrationsHelper,
@@ -10,7 +6,6 @@ import {
setIntegrationAuthRefreshHelper, setIntegrationAuthRefreshHelper,
setIntegrationAuthAccessHelper, setIntegrationAuthAccessHelper,
} from '../helpers/integration'; } from '../helpers/integration';
import { exchangeCode } from '../integrations';
// should sync stuff be here too? Probably. // should sync stuff be here too? Probably.
// TODO: move bot functions to IntegrationService. // TODO: move bot functions to IntegrationService.
@@ -26,11 +21,12 @@ class IntegrationService {
* - Store integration access and refresh tokens returned from the OAuth2 code-token exchange * - Store integration access and refresh tokens returned from the OAuth2 code-token exchange
* - Add placeholder inactive integration * - Add placeholder inactive integration
* - Create bot sequence for integration * - Create bot sequence for integration
* @param {Object} obj * @param {Object} obj1
* @param {String} obj.workspaceId - id of workspace * @param {String} obj1.workspaceId - id of workspace
* @param {String} obj.environment - workspace environment * @param {String} obj1.environment - workspace environment
* @param {String} obj.integration - name of integration * @param {String} obj1.integration - name of integration
* @param {String} obj.code - code * @param {String} obj1.code - code
* @returns {IntegrationAuth} integrationAuth - integration authorization after OAuth2 code-token exchange
*/ */
static async handleOAuthExchange({ static async handleOAuthExchange({
workspaceId, workspaceId,
@@ -43,7 +39,7 @@ class IntegrationService {
code: string; code: string;
environment: string; environment: string;
}) { }) {
await handleOAuthExchangeHelper({ return await handleOAuthExchangeHelper({
workspaceId, workspaceId,
integration, integration,
code, code,
+4
View File
@@ -6,6 +6,7 @@ import {
ENV_SET ENV_SET
} from './environment'; } from './environment';
import { import {
INTEGRATION_AZURE_KEY_VAULT,
INTEGRATION_HEROKU, INTEGRATION_HEROKU,
INTEGRATION_VERCEL, INTEGRATION_VERCEL,
INTEGRATION_NETLIFY, INTEGRATION_NETLIFY,
@@ -14,6 +15,7 @@ import {
INTEGRATION_FLYIO, INTEGRATION_FLYIO,
INTEGRATION_SET, INTEGRATION_SET,
INTEGRATION_OAUTH2, INTEGRATION_OAUTH2,
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,
@@ -58,6 +60,7 @@ export {
ENV_STAGING, ENV_STAGING,
ENV_PROD, ENV_PROD,
ENV_SET, ENV_SET,
INTEGRATION_AZURE_KEY_VAULT,
INTEGRATION_HEROKU, INTEGRATION_HEROKU,
INTEGRATION_VERCEL, INTEGRATION_VERCEL,
INTEGRATION_NETLIFY, INTEGRATION_NETLIFY,
@@ -66,6 +69,7 @@ export {
INTEGRATION_FLYIO, INTEGRATION_FLYIO,
INTEGRATION_SET, INTEGRATION_SET,
INTEGRATION_OAUTH2, INTEGRATION_OAUTH2,
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,
+19
View File
@@ -1,3 +1,7 @@
import {
CLIENT_ID_AZURE,
TENANT_ID_AZURE
} from '../config';
import { import {
CLIENT_ID_HEROKU, CLIENT_ID_HEROKU,
CLIENT_ID_NETLIFY, CLIENT_ID_NETLIFY,
@@ -6,6 +10,7 @@ import {
} from '../config'; } from '../config';
// integrations // integrations
const INTEGRATION_AZURE_KEY_VAULT = 'azure-key-vault';
const INTEGRATION_HEROKU = 'heroku'; const INTEGRATION_HEROKU = 'heroku';
const INTEGRATION_VERCEL = 'vercel'; const INTEGRATION_VERCEL = 'vercel';
const INTEGRATION_NETLIFY = 'netlify'; const INTEGRATION_NETLIFY = 'netlify';
@@ -13,6 +18,7 @@ const INTEGRATION_GITHUB = 'github';
const INTEGRATION_RENDER = 'render'; const INTEGRATION_RENDER = 'render';
const INTEGRATION_FLYIO = 'flyio'; const INTEGRATION_FLYIO = 'flyio';
const INTEGRATION_SET = new Set([ const INTEGRATION_SET = new Set([
INTEGRATION_AZURE_KEY_VAULT,
INTEGRATION_HEROKU, INTEGRATION_HEROKU,
INTEGRATION_VERCEL, INTEGRATION_VERCEL,
INTEGRATION_NETLIFY, INTEGRATION_NETLIFY,
@@ -25,6 +31,7 @@ const INTEGRATION_SET = new Set([
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_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';
@@ -40,6 +47,16 @@ 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_OPTIONS = [ const INTEGRATION_OPTIONS = [
{
name: 'Azure Key Vault',
slug: 'azure-key-vault',
image: 'Microsoft Azure.png',
isAvailable: true,
type: 'oauth',
clientId: CLIENT_ID_AZURE,
tenantId: TENANT_ID_AZURE,
docsLink: ''
},
{ {
name: 'Heroku', name: 'Heroku',
slug: 'heroku', slug: 'heroku',
@@ -143,6 +160,7 @@ const INTEGRATION_OPTIONS = [
] ]
export { export {
INTEGRATION_AZURE_KEY_VAULT,
INTEGRATION_HEROKU, INTEGRATION_HEROKU,
INTEGRATION_VERCEL, INTEGRATION_VERCEL,
INTEGRATION_NETLIFY, INTEGRATION_NETLIFY,
@@ -151,6 +169,7 @@ export {
INTEGRATION_FLYIO, INTEGRATION_FLYIO,
INTEGRATION_SET, INTEGRATION_SET,
INTEGRATION_OAUTH2, INTEGRATION_OAUTH2,
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,
Binary file not shown.

After

Width:  |  Height:  |  Size: 131 KiB

+6 -4
View File
@@ -3,10 +3,12 @@ title: 'Kubernetes'
description: "This page explains how to use Infisical to inject secrets into Kubernetes clusters." description: "This page explains how to use Infisical to inject secrets into Kubernetes clusters."
--- ---
The Infisical Secrets Operator is a custom Kubernetes controller that helps keep secrets in a cluster up to date by synchronizing them. ![title](../../images/k8-diagram.png)
It is installed in its own namespace within the cluster and follows strict RBAC policies.
The operator uses InfisicalSecret custom resources to identify which secrets to sync and where to store them.
It is responsible for continuously updating managed secrets, and in the future may also automatically reload deployments that use them as needed. The Infisical Secrets Operator is a Kubernetes controller that retrieves secrets from Infisical and stores them in a designated cluster.
It uses an `InfisicalSecret` resource to specify authentication and storage methods.
The operator continuously updates secrets and can also reload dependent deployments automatically.
## Install Operator ## Install Operator
+295 -15
View File
@@ -15,6 +15,7 @@
"@fortawesome/react-fontawesome": "^0.1.19", "@fortawesome/react-fontawesome": "^0.1.19",
"@headlessui/react": "^1.6.6", "@headlessui/react": "^1.6.6",
"@hookform/resolvers": "^2.9.10", "@hookform/resolvers": "^2.9.10",
"@octokit/rest": "^19.0.7",
"@radix-ui/react-accordion": "^1.1.0", "@radix-ui/react-accordion": "^1.1.0",
"@radix-ui/react-alert-dialog": "^1.0.2", "@radix-ui/react-alert-dialog": "^1.0.2",
"@radix-ui/react-checkbox": "^1.0.1", "@radix-ui/react-checkbox": "^1.0.1",
@@ -3521,6 +3522,153 @@
"node": ">= 8" "node": ">= 8"
} }
}, },
"node_modules/@octokit/auth-token": {
"version": "3.0.3",
"resolved": "https://registry.npmjs.org/@octokit/auth-token/-/auth-token-3.0.3.tgz",
"integrity": "sha512-/aFM2M4HVDBT/jjDBa84sJniv1t9Gm/rLkalaz9htOm+L+8JMj1k9w0CkUdcxNyNxZPlTxKPVko+m1VlM58ZVA==",
"dependencies": {
"@octokit/types": "^9.0.0"
},
"engines": {
"node": ">= 14"
}
},
"node_modules/@octokit/core": {
"version": "4.2.0",
"resolved": "https://registry.npmjs.org/@octokit/core/-/core-4.2.0.tgz",
"integrity": "sha512-AgvDRUg3COpR82P7PBdGZF/NNqGmtMq2NiPqeSsDIeCfYFOZ9gddqWNQHnFdEUf+YwOj4aZYmJnlPp7OXmDIDg==",
"dependencies": {
"@octokit/auth-token": "^3.0.0",
"@octokit/graphql": "^5.0.0",
"@octokit/request": "^6.0.0",
"@octokit/request-error": "^3.0.0",
"@octokit/types": "^9.0.0",
"before-after-hook": "^2.2.0",
"universal-user-agent": "^6.0.0"
},
"engines": {
"node": ">= 14"
}
},
"node_modules/@octokit/endpoint": {
"version": "7.0.5",
"resolved": "https://registry.npmjs.org/@octokit/endpoint/-/endpoint-7.0.5.tgz",
"integrity": "sha512-LG4o4HMY1Xoaec87IqQ41TQ+glvIeTKqfjkCEmt5AIwDZJwQeVZFIEYXrYY6yLwK+pAScb9Gj4q+Nz2qSw1roA==",
"dependencies": {
"@octokit/types": "^9.0.0",
"is-plain-object": "^5.0.0",
"universal-user-agent": "^6.0.0"
},
"engines": {
"node": ">= 14"
}
},
"node_modules/@octokit/graphql": {
"version": "5.0.5",
"resolved": "https://registry.npmjs.org/@octokit/graphql/-/graphql-5.0.5.tgz",
"integrity": "sha512-Qwfvh3xdqKtIznjX9lz2D458r7dJPP8l6r4GQkIdWQouZwHQK0mVT88uwiU2bdTU2OtT1uOlKpRciUWldpG0yQ==",
"dependencies": {
"@octokit/request": "^6.0.0",
"@octokit/types": "^9.0.0",
"universal-user-agent": "^6.0.0"
},
"engines": {
"node": ">= 14"
}
},
"node_modules/@octokit/openapi-types": {
"version": "16.0.0",
"resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-16.0.0.tgz",
"integrity": "sha512-JbFWOqTJVLHZSUUoF4FzAZKYtqdxWu9Z5m2QQnOyEa04fOFljvyh7D3GYKbfuaSWisqehImiVIMG4eyJeP5VEA=="
},
"node_modules/@octokit/plugin-paginate-rest": {
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/@octokit/plugin-paginate-rest/-/plugin-paginate-rest-6.0.0.tgz",
"integrity": "sha512-Sq5VU1PfT6/JyuXPyt04KZNVsFOSBaYOAq2QRZUwzVlI10KFvcbUo8lR258AAQL1Et60b0WuVik+zOWKLuDZxw==",
"dependencies": {
"@octokit/types": "^9.0.0"
},
"engines": {
"node": ">= 14"
},
"peerDependencies": {
"@octokit/core": ">=4"
}
},
"node_modules/@octokit/plugin-request-log": {
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/@octokit/plugin-request-log/-/plugin-request-log-1.0.4.tgz",
"integrity": "sha512-mLUsMkgP7K/cnFEw07kWqXGF5LKrOkD+lhCrKvPHXWDywAwuDUeDwWBpc69XK3pNX0uKiVt8g5z96PJ6z9xCFA==",
"peerDependencies": {
"@octokit/core": ">=3"
}
},
"node_modules/@octokit/plugin-rest-endpoint-methods": {
"version": "7.0.1",
"resolved": "https://registry.npmjs.org/@octokit/plugin-rest-endpoint-methods/-/plugin-rest-endpoint-methods-7.0.1.tgz",
"integrity": "sha512-pnCaLwZBudK5xCdrR823xHGNgqOzRnJ/mpC/76YPpNP7DybdsJtP7mdOwh+wYZxK5jqeQuhu59ogMI4NRlBUvA==",
"dependencies": {
"@octokit/types": "^9.0.0",
"deprecation": "^2.3.1"
},
"engines": {
"node": ">= 14"
},
"peerDependencies": {
"@octokit/core": ">=3"
}
},
"node_modules/@octokit/request": {
"version": "6.2.3",
"resolved": "https://registry.npmjs.org/@octokit/request/-/request-6.2.3.tgz",
"integrity": "sha512-TNAodj5yNzrrZ/VxP+H5HiYaZep0H3GU0O7PaF+fhDrt8FPrnkei9Aal/txsN/1P7V3CPiThG0tIvpPDYUsyAA==",
"dependencies": {
"@octokit/endpoint": "^7.0.0",
"@octokit/request-error": "^3.0.0",
"@octokit/types": "^9.0.0",
"is-plain-object": "^5.0.0",
"node-fetch": "^2.6.7",
"universal-user-agent": "^6.0.0"
},
"engines": {
"node": ">= 14"
}
},
"node_modules/@octokit/request-error": {
"version": "3.0.3",
"resolved": "https://registry.npmjs.org/@octokit/request-error/-/request-error-3.0.3.tgz",
"integrity": "sha512-crqw3V5Iy2uOU5Np+8M/YexTlT8zxCfI+qu+LxUB7SZpje4Qmx3mub5DfEKSO8Ylyk0aogi6TYdf6kxzh2BguQ==",
"dependencies": {
"@octokit/types": "^9.0.0",
"deprecation": "^2.0.0",
"once": "^1.4.0"
},
"engines": {
"node": ">= 14"
}
},
"node_modules/@octokit/rest": {
"version": "19.0.7",
"resolved": "https://registry.npmjs.org/@octokit/rest/-/rest-19.0.7.tgz",
"integrity": "sha512-HRtSfjrWmWVNp2uAkEpQnuGMJsu/+dBr47dRc5QVgsCbnIc1+GFEaoKBWkYG+zjrsHpSqcAElMio+n10c0b5JA==",
"dependencies": {
"@octokit/core": "^4.1.0",
"@octokit/plugin-paginate-rest": "^6.0.0",
"@octokit/plugin-request-log": "^1.0.4",
"@octokit/plugin-rest-endpoint-methods": "^7.0.0"
},
"engines": {
"node": ">= 14"
}
},
"node_modules/@octokit/types": {
"version": "9.0.0",
"resolved": "https://registry.npmjs.org/@octokit/types/-/types-9.0.0.tgz",
"integrity": "sha512-LUewfj94xCMH2rbD5YJ+6AQ4AVjFYTgpp6rboWM5T7N3IsIF65SBEOVcYMGAEzO/kKNiNaW4LoWtoThOhH06gw==",
"dependencies": {
"@octokit/openapi-types": "^16.0.0"
}
},
"node_modules/@pkgr/utils": { "node_modules/@pkgr/utils": {
"version": "2.3.1", "version": "2.3.1",
"resolved": "https://registry.npmjs.org/@pkgr/utils/-/utils-2.3.1.tgz", "resolved": "https://registry.npmjs.org/@pkgr/utils/-/utils-2.3.1.tgz",
@@ -8789,6 +8937,11 @@
} }
] ]
}, },
"node_modules/before-after-hook": {
"version": "2.2.3",
"resolved": "https://registry.npmjs.org/before-after-hook/-/before-after-hook-2.2.3.tgz",
"integrity": "sha512-NzUnlZexiaH/46WDhANlyR2bXRopNg4F/zuSA3OpZnllCUgRaOF2znDioDWrmbNVsuZk6l9pMquQB38cfBZwkQ=="
},
"node_modules/better-opn": { "node_modules/better-opn": {
"version": "2.1.1", "version": "2.1.1",
"resolved": "https://registry.npmjs.org/better-opn/-/better-opn-2.1.1.tgz", "resolved": "https://registry.npmjs.org/better-opn/-/better-opn-2.1.1.tgz",
@@ -10371,6 +10524,11 @@
"node": ">= 0.8" "node": ">= 0.8"
} }
}, },
"node_modules/deprecation": {
"version": "2.3.1",
"resolved": "https://registry.npmjs.org/deprecation/-/deprecation-2.3.1.tgz",
"integrity": "sha512-xmHIy4F3scKVwMsQ4WnVaS8bHOx0DmVwRywosKhaILI0ywMDWPtBSku2HNxRvF7jtwDRsoEwYQSfbxj8b7RlJQ=="
},
"node_modules/dequal": { "node_modules/dequal": {
"version": "2.0.3", "version": "2.0.3",
"resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz", "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz",
@@ -14099,7 +14257,6 @@
"version": "5.0.0", "version": "5.0.0",
"resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-5.0.0.tgz", "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-5.0.0.tgz",
"integrity": "sha512-VRSzKkbMm5jMDoKLbltAkFQ5Qr7VDiTFGXxYFXXowVj387GeGNOCsOH6Msy00SGZ3Fp84b1Naa1psqgcCIEP5Q==", "integrity": "sha512-VRSzKkbMm5jMDoKLbltAkFQ5Qr7VDiTFGXxYFXXowVj387GeGNOCsOH6Msy00SGZ3Fp84b1Naa1psqgcCIEP5Q==",
"dev": true,
"engines": { "engines": {
"node": ">=0.10.0" "node": ">=0.10.0"
} }
@@ -16455,7 +16612,6 @@
"version": "2.6.8", "version": "2.6.8",
"resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.8.tgz", "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.8.tgz",
"integrity": "sha512-RZ6dBYuj8dRSfxpUSu+NsdF1dpPpluJxwOp+6IoDp/sH2QNDSvurYsAa+F1WxY2RjA1iP93xhcsUoYbF2XBqVg==", "integrity": "sha512-RZ6dBYuj8dRSfxpUSu+NsdF1dpPpluJxwOp+6IoDp/sH2QNDSvurYsAa+F1WxY2RjA1iP93xhcsUoYbF2XBqVg==",
"dev": true,
"dependencies": { "dependencies": {
"whatwg-url": "^5.0.0" "whatwg-url": "^5.0.0"
}, },
@@ -21217,8 +21373,7 @@
"node_modules/tr46": { "node_modules/tr46": {
"version": "0.0.3", "version": "0.0.3",
"resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz",
"integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==", "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw=="
"dev": true
}, },
"node_modules/trim-lines": { "node_modules/trim-lines": {
"version": "3.0.1", "version": "3.0.1",
@@ -21664,6 +21819,11 @@
"url": "https://opencollective.com/unified" "url": "https://opencollective.com/unified"
} }
}, },
"node_modules/universal-user-agent": {
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/universal-user-agent/-/universal-user-agent-6.0.0.tgz",
"integrity": "sha512-isyNax3wXoKaulPDZWHQqbmIx1k2tb9fb3GGDBRxCscfYV2Ch7WxPArBsFEG8s/safwXTT7H4QGhaIkTp9447w=="
},
"node_modules/universalify": { "node_modules/universalify": {
"version": "2.0.0", "version": "2.0.0",
"resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.0.tgz", "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.0.tgz",
@@ -22068,8 +22228,7 @@
"node_modules/webidl-conversions": { "node_modules/webidl-conversions": {
"version": "3.0.1", "version": "3.0.1",
"resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz",
"integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==", "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ=="
"dev": true
}, },
"node_modules/webpack": { "node_modules/webpack": {
"version": "5.75.0", "version": "5.75.0",
@@ -22267,7 +22426,6 @@
"version": "5.0.0", "version": "5.0.0",
"resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz",
"integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==",
"dev": true,
"dependencies": { "dependencies": {
"tr46": "~0.0.3", "tr46": "~0.0.3",
"webidl-conversions": "^3.0.0" "webidl-conversions": "^3.0.0"
@@ -24829,6 +24987,118 @@
"fastq": "^1.6.0" "fastq": "^1.6.0"
} }
}, },
"@octokit/auth-token": {
"version": "3.0.3",
"resolved": "https://registry.npmjs.org/@octokit/auth-token/-/auth-token-3.0.3.tgz",
"integrity": "sha512-/aFM2M4HVDBT/jjDBa84sJniv1t9Gm/rLkalaz9htOm+L+8JMj1k9w0CkUdcxNyNxZPlTxKPVko+m1VlM58ZVA==",
"requires": {
"@octokit/types": "^9.0.0"
}
},
"@octokit/core": {
"version": "4.2.0",
"resolved": "https://registry.npmjs.org/@octokit/core/-/core-4.2.0.tgz",
"integrity": "sha512-AgvDRUg3COpR82P7PBdGZF/NNqGmtMq2NiPqeSsDIeCfYFOZ9gddqWNQHnFdEUf+YwOj4aZYmJnlPp7OXmDIDg==",
"requires": {
"@octokit/auth-token": "^3.0.0",
"@octokit/graphql": "^5.0.0",
"@octokit/request": "^6.0.0",
"@octokit/request-error": "^3.0.0",
"@octokit/types": "^9.0.0",
"before-after-hook": "^2.2.0",
"universal-user-agent": "^6.0.0"
}
},
"@octokit/endpoint": {
"version": "7.0.5",
"resolved": "https://registry.npmjs.org/@octokit/endpoint/-/endpoint-7.0.5.tgz",
"integrity": "sha512-LG4o4HMY1Xoaec87IqQ41TQ+glvIeTKqfjkCEmt5AIwDZJwQeVZFIEYXrYY6yLwK+pAScb9Gj4q+Nz2qSw1roA==",
"requires": {
"@octokit/types": "^9.0.0",
"is-plain-object": "^5.0.0",
"universal-user-agent": "^6.0.0"
}
},
"@octokit/graphql": {
"version": "5.0.5",
"resolved": "https://registry.npmjs.org/@octokit/graphql/-/graphql-5.0.5.tgz",
"integrity": "sha512-Qwfvh3xdqKtIznjX9lz2D458r7dJPP8l6r4GQkIdWQouZwHQK0mVT88uwiU2bdTU2OtT1uOlKpRciUWldpG0yQ==",
"requires": {
"@octokit/request": "^6.0.0",
"@octokit/types": "^9.0.0",
"universal-user-agent": "^6.0.0"
}
},
"@octokit/openapi-types": {
"version": "16.0.0",
"resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-16.0.0.tgz",
"integrity": "sha512-JbFWOqTJVLHZSUUoF4FzAZKYtqdxWu9Z5m2QQnOyEa04fOFljvyh7D3GYKbfuaSWisqehImiVIMG4eyJeP5VEA=="
},
"@octokit/plugin-paginate-rest": {
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/@octokit/plugin-paginate-rest/-/plugin-paginate-rest-6.0.0.tgz",
"integrity": "sha512-Sq5VU1PfT6/JyuXPyt04KZNVsFOSBaYOAq2QRZUwzVlI10KFvcbUo8lR258AAQL1Et60b0WuVik+zOWKLuDZxw==",
"requires": {
"@octokit/types": "^9.0.0"
}
},
"@octokit/plugin-request-log": {
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/@octokit/plugin-request-log/-/plugin-request-log-1.0.4.tgz",
"integrity": "sha512-mLUsMkgP7K/cnFEw07kWqXGF5LKrOkD+lhCrKvPHXWDywAwuDUeDwWBpc69XK3pNX0uKiVt8g5z96PJ6z9xCFA==",
"requires": {}
},
"@octokit/plugin-rest-endpoint-methods": {
"version": "7.0.1",
"resolved": "https://registry.npmjs.org/@octokit/plugin-rest-endpoint-methods/-/plugin-rest-endpoint-methods-7.0.1.tgz",
"integrity": "sha512-pnCaLwZBudK5xCdrR823xHGNgqOzRnJ/mpC/76YPpNP7DybdsJtP7mdOwh+wYZxK5jqeQuhu59ogMI4NRlBUvA==",
"requires": {
"@octokit/types": "^9.0.0",
"deprecation": "^2.3.1"
}
},
"@octokit/request": {
"version": "6.2.3",
"resolved": "https://registry.npmjs.org/@octokit/request/-/request-6.2.3.tgz",
"integrity": "sha512-TNAodj5yNzrrZ/VxP+H5HiYaZep0H3GU0O7PaF+fhDrt8FPrnkei9Aal/txsN/1P7V3CPiThG0tIvpPDYUsyAA==",
"requires": {
"@octokit/endpoint": "^7.0.0",
"@octokit/request-error": "^3.0.0",
"@octokit/types": "^9.0.0",
"is-plain-object": "^5.0.0",
"node-fetch": "^2.6.7",
"universal-user-agent": "^6.0.0"
}
},
"@octokit/request-error": {
"version": "3.0.3",
"resolved": "https://registry.npmjs.org/@octokit/request-error/-/request-error-3.0.3.tgz",
"integrity": "sha512-crqw3V5Iy2uOU5Np+8M/YexTlT8zxCfI+qu+LxUB7SZpje4Qmx3mub5DfEKSO8Ylyk0aogi6TYdf6kxzh2BguQ==",
"requires": {
"@octokit/types": "^9.0.0",
"deprecation": "^2.0.0",
"once": "^1.4.0"
}
},
"@octokit/rest": {
"version": "19.0.7",
"resolved": "https://registry.npmjs.org/@octokit/rest/-/rest-19.0.7.tgz",
"integrity": "sha512-HRtSfjrWmWVNp2uAkEpQnuGMJsu/+dBr47dRc5QVgsCbnIc1+GFEaoKBWkYG+zjrsHpSqcAElMio+n10c0b5JA==",
"requires": {
"@octokit/core": "^4.1.0",
"@octokit/plugin-paginate-rest": "^6.0.0",
"@octokit/plugin-request-log": "^1.0.4",
"@octokit/plugin-rest-endpoint-methods": "^7.0.0"
}
},
"@octokit/types": {
"version": "9.0.0",
"resolved": "https://registry.npmjs.org/@octokit/types/-/types-9.0.0.tgz",
"integrity": "sha512-LUewfj94xCMH2rbD5YJ+6AQ4AVjFYTgpp6rboWM5T7N3IsIF65SBEOVcYMGAEzO/kKNiNaW4LoWtoThOhH06gw==",
"requires": {
"@octokit/openapi-types": "^16.0.0"
}
},
"@pkgr/utils": { "@pkgr/utils": {
"version": "2.3.1", "version": "2.3.1",
"resolved": "https://registry.npmjs.org/@pkgr/utils/-/utils-2.3.1.tgz", "resolved": "https://registry.npmjs.org/@pkgr/utils/-/utils-2.3.1.tgz",
@@ -28742,6 +29012,11 @@
"resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz",
"integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==" "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA=="
}, },
"before-after-hook": {
"version": "2.2.3",
"resolved": "https://registry.npmjs.org/before-after-hook/-/before-after-hook-2.2.3.tgz",
"integrity": "sha512-NzUnlZexiaH/46WDhANlyR2bXRopNg4F/zuSA3OpZnllCUgRaOF2znDioDWrmbNVsuZk6l9pMquQB38cfBZwkQ=="
},
"better-opn": { "better-opn": {
"version": "2.1.1", "version": "2.1.1",
"resolved": "https://registry.npmjs.org/better-opn/-/better-opn-2.1.1.tgz", "resolved": "https://registry.npmjs.org/better-opn/-/better-opn-2.1.1.tgz",
@@ -29938,6 +30213,11 @@
"resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz",
"integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==" "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw=="
}, },
"deprecation": {
"version": "2.3.1",
"resolved": "https://registry.npmjs.org/deprecation/-/deprecation-2.3.1.tgz",
"integrity": "sha512-xmHIy4F3scKVwMsQ4WnVaS8bHOx0DmVwRywosKhaILI0ywMDWPtBSku2HNxRvF7jtwDRsoEwYQSfbxj8b7RlJQ=="
},
"dequal": { "dequal": {
"version": "2.0.3", "version": "2.0.3",
"resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz", "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz",
@@ -32732,8 +33012,7 @@
"is-plain-object": { "is-plain-object": {
"version": "5.0.0", "version": "5.0.0",
"resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-5.0.0.tgz", "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-5.0.0.tgz",
"integrity": "sha512-VRSzKkbMm5jMDoKLbltAkFQ5Qr7VDiTFGXxYFXXowVj387GeGNOCsOH6Msy00SGZ3Fp84b1Naa1psqgcCIEP5Q==", "integrity": "sha512-VRSzKkbMm5jMDoKLbltAkFQ5Qr7VDiTFGXxYFXXowVj387GeGNOCsOH6Msy00SGZ3Fp84b1Naa1psqgcCIEP5Q=="
"dev": true
}, },
"is-regex": { "is-regex": {
"version": "1.1.4", "version": "1.1.4",
@@ -34410,7 +34689,6 @@
"version": "2.6.8", "version": "2.6.8",
"resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.8.tgz", "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.8.tgz",
"integrity": "sha512-RZ6dBYuj8dRSfxpUSu+NsdF1dpPpluJxwOp+6IoDp/sH2QNDSvurYsAa+F1WxY2RjA1iP93xhcsUoYbF2XBqVg==", "integrity": "sha512-RZ6dBYuj8dRSfxpUSu+NsdF1dpPpluJxwOp+6IoDp/sH2QNDSvurYsAa+F1WxY2RjA1iP93xhcsUoYbF2XBqVg==",
"dev": true,
"requires": { "requires": {
"whatwg-url": "^5.0.0" "whatwg-url": "^5.0.0"
} }
@@ -37863,8 +38141,7 @@
"tr46": { "tr46": {
"version": "0.0.3", "version": "0.0.3",
"resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz",
"integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==", "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw=="
"dev": true
}, },
"trim-lines": { "trim-lines": {
"version": "3.0.1", "version": "3.0.1",
@@ -38186,6 +38463,11 @@
"unist-util-is": "^5.0.0" "unist-util-is": "^5.0.0"
} }
}, },
"universal-user-agent": {
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/universal-user-agent/-/universal-user-agent-6.0.0.tgz",
"integrity": "sha512-isyNax3wXoKaulPDZWHQqbmIx1k2tb9fb3GGDBRxCscfYV2Ch7WxPArBsFEG8s/safwXTT7H4QGhaIkTp9447w=="
},
"universalify": { "universalify": {
"version": "2.0.0", "version": "2.0.0",
"resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.0.tgz", "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.0.tgz",
@@ -38479,8 +38761,7 @@
"webidl-conversions": { "webidl-conversions": {
"version": "3.0.1", "version": "3.0.1",
"resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz",
"integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==", "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ=="
"dev": true
}, },
"webpack": { "webpack": {
"version": "5.75.0", "version": "5.75.0",
@@ -38626,7 +38907,6 @@
"version": "5.0.0", "version": "5.0.0",
"resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz",
"integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==",
"dev": true,
"requires": { "requires": {
"tr46": "~0.0.3", "tr46": "~0.0.3",
"webidl-conversions": "^3.0.0" "webidl-conversions": "^3.0.0"
+1
View File
@@ -22,6 +22,7 @@
"@fortawesome/react-fontawesome": "^0.1.19", "@fortawesome/react-fontawesome": "^0.1.19",
"@headlessui/react": "^1.6.6", "@headlessui/react": "^1.6.6",
"@hookform/resolvers": "^2.9.10", "@hookform/resolvers": "^2.9.10",
"@octokit/rest": "^19.0.7",
"@radix-ui/react-accordion": "^1.1.0", "@radix-ui/react-accordion": "^1.1.0",
"@radix-ui/react-alert-dialog": "^1.0.2", "@radix-ui/react-alert-dialog": "^1.0.2",
"@radix-ui/react-checkbox": "^1.0.1", "@radix-ui/react-checkbox": "^1.0.1",
+12 -1
View File
@@ -2,6 +2,16 @@ interface Mapping {
[key: string]: string; [key: string]: string;
} }
const integrationSlugNameMapping: Mapping = {
'azure-key-vault': 'Azure Key Vault',
'heroku': 'Heroku',
'vercel': 'Vercel',
'netlify': 'Netlify',
'github': 'GitHub',
'render': 'Render',
'flyio': 'Fly.io'
}
const envMapping: Mapping = { const envMapping: Mapping = {
Development: "dev", Development: "dev",
Staging: "staging", Staging: "staging",
@@ -19,7 +29,7 @@ const reverseEnvMapping: Mapping = {
const contextNetlifyMapping: Mapping = { const contextNetlifyMapping: Mapping = {
"dev": "Local development", "dev": "Local development",
"branch-deploy": "Branch deploys", "branch-deploy": "Branch deploys",
"deploy-review": "Deploy Previews", "deploy-preview": "Deploy Previews",
"production": "Production" "production": "Production"
} }
@@ -49,6 +59,7 @@ const plans = plansProd || plansDev;
export { export {
contextNetlifyMapping, contextNetlifyMapping,
envMapping, envMapping,
integrationSlugNameMapping,
plans, plans,
reverseContextNetlifyMapping, reverseContextNetlifyMapping,
reverseEnvMapping} reverseEnvMapping}
@@ -1,3 +1,12 @@
export interface Tag {
_id: string;
name: string;
slug: string;
user: string;
workspace: string;
createdAt: string;
}
export interface SecretDataProps { export interface SecretDataProps {
pos: number; pos: number;
key: string; key: string;
@@ -5,4 +14,5 @@ export interface SecretDataProps {
valueOverride: string | undefined; valueOverride: string | undefined;
id: string; id: string;
comment: string; comment: string;
tags: Tag[];
} }
+2 -2
View File
@@ -199,13 +199,13 @@ const Layout = ({ children }: LayoutProps) => {
.split('/') .split('/')
[router.asPath.split('/').length - 1].split('?')[0]; [router.asPath.split('/').length - 1].split('?')[0];
if (!['heroku', 'vercel', 'github', 'netlify'].includes(intendedWorkspaceId)) { if (!['callback', 'create', 'authorize'].includes(intendedWorkspaceId)) {
localStorage.setItem('projectData.id', intendedWorkspaceId); localStorage.setItem('projectData.id', intendedWorkspaceId);
} }
// 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 (
!['heroku', 'vercel', 'github', 'netlify'].includes(intendedWorkspaceId) && !['callback', 'create', 'authorize'].includes(intendedWorkspaceId) &&
!userWorkspaces !userWorkspaces
.map((workspace: { _id: string }) => workspace._id) .map((workspace: { _id: string }) => workspace._id)
.includes(intendedWorkspaceId) .includes(intendedWorkspaceId)
@@ -36,7 +36,7 @@ const AddWorkspaceDialog = ({
return ( return (
<div> <div>
<Transition appear show={isOpen} as={Fragment}> <Transition appear show={isOpen} as={Fragment}>
<Dialog as="div" className="relative z-20" onClose={closeModal}> <Dialog as="div" className="relative z-[100]" onClose={closeModal}>
<Transition.Child <Transition.Child
as={Fragment} as={Fragment}
enter="ease-out duration-300" enter="ease-out duration-300"
@@ -49,7 +49,7 @@ const AddWorkspaceDialog = ({
<div className="fixed inset-0 bg-black bg-opacity-70" /> <div className="fixed inset-0 bg-black bg-opacity-70" />
</Transition.Child> </Transition.Child>
<div className="fixed inset-0 overflow-y-auto z-50"> <div className="fixed inset-0 overflow-y-auto">
<div className="flex min-h-full items-center justify-center p-4 text-center"> <div className="flex min-h-full items-center justify-center p-4 text-center">
<Transition.Child <Transition.Child
as={Fragment} as={Fragment}
@@ -60,7 +60,7 @@ const AddWorkspaceDialog = ({
leaveFrom="opacity-100 scale-100" leaveFrom="opacity-100 scale-100"
leaveTo="opacity-0 scale-95" leaveTo="opacity-0 scale-95"
> >
<Dialog.Panel className="w-full max-w-md transform overflow-hidden rounded-2xl bg-bunker-800 border border-gray-700 p-6 text-left align-middle shadow-xl transition-all"> <Dialog.Panel className="w-full max-w-md transform overflow-hidden rounded-md bg-bunker-800 border border-gray-700 p-6 text-left align-middle shadow-xl transition-all">
<Dialog.Title <Dialog.Title
as="h3" as="h3"
className="text-lg font-medium leading-6 text-gray-400" className="text-lg font-medium leading-6 text-gray-400"
@@ -69,7 +69,7 @@ const AddWorkspaceDialog = ({
</Dialog.Title> </Dialog.Title>
<div className="mt-2"> <div className="mt-2">
<p className="text-sm text-gray-500"> <p className="text-sm text-gray-500">
This project will contain your environmental variables. This project will contain your secrets and configs.
</p> </p>
</div> </div>
<div className="max-h-28 mt-4"> <div className="max-h-28 mt-4">
@@ -1,120 +0,0 @@
import { Fragment, useState } from "react";
import { Dialog, Transition } from "@headlessui/react";
import Button from "../buttons/Button";
import InputField from "../InputField";
interface IntegrationOption {
clientId: string;
clientSlug?: string; // vercel-integration specific
docsLink: string;
image: string;
isAvailable: boolean;
name: string;
slug: string;
type: string;
}
type Props = {
isOpen: boolean;
closeModal: () => void;
selectedIntegrationOption: IntegrationOption | null
handleIntegrationOption: (arg:{
integrationOption: IntegrationOption,
accessToken?: string;
})=>void;
};
const IntegrationAccessTokenDialog = ({
isOpen,
closeModal,
selectedIntegrationOption,
handleIntegrationOption
}:Props) => {
const [accessToken, setAccessToken] = useState('');
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const submit = async () => {
try {
if (selectedIntegrationOption && accessToken !== '') {
handleIntegrationOption({
integrationOption: selectedIntegrationOption,
accessToken
});
closeModal();
setAccessToken('');
}
} catch (err) {
console.log(err);
}
}
return (
<div>
<Transition appear show={isOpen} as={Fragment}>
<Dialog as="div" className="relative z-10" onClose={() => {
closeModal();
}}>
<Transition.Child
as={Fragment}
enter="ease-out duration-300"
enterFrom="opacity-0"
enterTo="opacity-100"
leave="ease-in duration-200"
leaveFrom="opacity-100"
leaveTo="opacity-0"
>
<div className="fixed inset-0 bg-black bg-opacity-70" />
</Transition.Child>
<div className="fixed inset-0 overflow-y-auto">
<div className="flex min-h-full items-center justify-center p-4 text-center">
<Transition.Child
as={Fragment}
enter="ease-out duration-300"
enterFrom="opacity-0 scale-95"
enterTo="opacity-100 scale-100"
leave="ease-in duration-200"
leaveFrom="opacity-100 scale-100"
leaveTo="opacity-0 scale-95"
>
<Dialog.Panel className="w-full max-w-md transform overflow-hidden rounded-md bg-bunker-800 border border-gray-700 p-6 text-left align-middle shadow-xl transition-all">
<Dialog.Title
as="h3"
className="text-lg font-medium leading-6 text-gray-400"
>
{`Enter your ${selectedIntegrationOption?.name} API Key`}
</Dialog.Title>
<div className="mt-2 mb-2">
<p className="text-sm text-gray-500">
{`This integration requires you to obtain an API key from ${selectedIntegrationOption?.name ?? ''} and store it with Infisical. `}
You can learn how to do this <a target="_blank" rel="noreferrer" className="text-primary cursor-pointer underline underline-offset-2" href="https://infisical.com/docs/integrations/cloud/render">here</a>.
</p>
</div>
<div className="mt-4 max-w-full">
<InputField
label="API Key"
onChangeHandler={setAccessToken}
type="varName"
value={accessToken}
placeholder=""
isRequired
/>
<div className="mt-4 max-w-[5.5rem]">
<Button
onButtonPressed={submit}
color="mineshaft"
text="Connect"
size="md"
/>
</div>
</div>
</Dialog.Panel>
</Transition.Child>
</div>
</div>
</Dialog>
</Transition>
</div>
);
}
export default IntegrationAccessTokenDialog;
@@ -83,7 +83,7 @@ const EnvironmentTable = ({ data = [], onCreateEnv, onDeleteEnv, onUpdateEnv }:
</div> </div>
<div className="w-48"> <div className="w-48">
<Button <Button
text="Add New Env" text="Add New Environment"
onButtonPressed={() => { onButtonPressed={() => {
if (plan !== plans.starter || host !== 'https://app.infisical.com') { if (plan !== plans.starter || host !== 'https://app.infisical.com') {
handlePopUpOpen('createUpdateEnv'); handlePopUpOpen('createUpdateEnv');
@@ -0,0 +1,61 @@
import { Fragment } from 'react';
import { useRouter } from 'next/router';
import { faCheckSquare, faPlus, faSquare } from '@fortawesome/free-solid-svg-icons';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { Menu, Transition } from '@headlessui/react';
import { Tag } from 'public/data/frequentInterfaces';
/**
* This is the menu that is used to download secrets as .env ad .yml files (in future we may have more options)
* @param {object} obj
* @param {SecretDataProps[]} obj.data -
*
*/
const AddTagsMenu = ({ allTags, currentTags, modifyTags, position }: { allTags: Tag[]; currentTags: Tag[]; modifyTags: (value: Tag[], position: number) => void; position: number; }) => {
const router = useRouter();
return (
<Menu as="div" className="ml-2 relative inline-block text-left">
<Menu.Button
as="div"
className="flex justify-center items-center font-medium focus:outline-none focus-visible:ring-2 focus-visible:ring-white focus-visible:ring-opacity-75"
>
<div className='bg-mineshaft/30 cursor-pointer rounded-sm text-sm text-mineshaft-200/50 hover:bg-mineshaft/70 duration-200 flex items-center'>
<FontAwesomeIcon icon={faPlus} className="p-[0.28rem]"/>
{currentTags.length > 2 && <span className='pr-2'>{currentTags.length - 2}</span>}
</div>
</Menu.Button>
<Transition
as={Fragment}
enter="transition ease-out duration-100"
enterFrom="transform opacity-0 scale-95"
enterTo="transform opacity-100 scale-100"
leave="transition ease-in duration-75"
leaveFrom="transform opacity-100 scale-100"
leaveTo="transform opacity-0 scale-95"
>
<Menu.Items className="absolute z-[90] text-sm drop-shadow-xl right-0 mt-0.5 w-[12rem] origin-top-right rounded-md bg-mineshaft-600 border border-mineshaft-500 shadow-lg ring-1 ring-black ring-opacity-5 focus:outline-none p-1 space-y-1">
{allTags?.map((tag) => { return (
<Menu.Item key={tag._id}>
<button
type="button"
className={`${currentTags.map(currentTag => currentTag.name).includes(tag.name) ? "opacity-30 cursor-default" : "hover:bg-mineshaft-700"} w-full text-left bg-mineshaft-800 px-2 py-0.5 text-bunker-200 rounded-sm flex items-center`}
onClick={() => {if (!currentTags.map(currentTag => currentTag.name).includes(tag.name)) {modifyTags(currentTags.concat([tag]), position)}}}
>
{currentTags.map(currentTag => currentTag.name).includes(tag.name) ? <FontAwesomeIcon icon={faCheckSquare} className="text-xs mr-2 text-primary"/> : <FontAwesomeIcon icon={faSquare} className="text-xs mr-2"/>} {tag.name}
</button>
</Menu.Item>
)})}
<button
type="button"
className='w-full text-left bg-mineshaft-800 hover:bg-primary hover:text-black duration-200 px-2 py-0.5 text-bunker-200 rounded-sm'
onClick={() => router.push(`/settings/project/${String(router.query.id)}`)}
>
<FontAwesomeIcon icon={faPlus} className="mr-2 text-xs" />Add more tags
</button>
</Menu.Items>
</Transition>
</Menu>
);
};
export default AddTagsMenu;
@@ -3,7 +3,6 @@ import { faCircle, faExclamationCircle, faEye, faLayerGroup } from '@fortawesome
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import guidGenerator from '../utilities/randomId'; import guidGenerator from '../utilities/randomId';
import { Button } from '../v2';
import { HoverObject } from '../v2/HoverCard'; import { HoverObject } from '../v2/HoverCard';
const REGEX = /([$]{.*?})/g; const REGEX = /([$]{.*?})/g;
@@ -99,8 +98,8 @@ const DashboardInputField = ({
)} )}
{!error && <div className={`absolute right-0 top-0 text-red z-50 ${ {!error && <div className={`absolute right-0 top-0 text-red z-50 ${
overrideEnabled ? 'visible group-hover:bg-mineshaft-700' : 'invisible group-hover:visible bg-mineshaft-700' overrideEnabled ? 'visible group-hover:bg-mineshaft-700' : 'invisible group-hover:visible bg-mineshaft-700'
} cursor-pointer duration-0`}> } cursor-pointer duration-0 h-10 flex items-center px-2`}>
<Button variant="plain" onClick={() => { <button type="button" onClick={() => {
if (modifyValueOverride) { if (modifyValueOverride) {
if (overrideEnabled === false) { if (overrideEnabled === false) {
modifyValueOverride('', position); modifyValueOverride('', position);
@@ -114,7 +113,7 @@ const DashboardInputField = ({
icon={faLayerGroup} icon={faLayerGroup}
color={overrideEnabled ? 'primary' : 'bunker-400'} color={overrideEnabled ? 'primary' : 'bunker-400'}
/> />
</Button> </button>
</div>} </div>}
</div> </div>
); );
@@ -19,7 +19,7 @@ export const DeleteActionButton = ({ onSubmit, isPlain }: Props) => {
<div className={`${ <div className={`${
!isPlain !isPlain
? 'bg-[#9B3535] opacity-70 hover:opacity-100 w-[4.5rem] h-[2.5rem] rounded-md duration-200 ml-2' ? 'bg-[#9B3535] opacity-70 hover:opacity-100 w-[4.5rem] h-[2.5rem] rounded-md duration-200 ml-2'
: 'cursor-pointer w-[2.35rem] h-[2.35rem] flex items-center justfy-center'}`}> : 'cursor-pointer w-[1.5rem] h-[2.35rem] mr-2 flex items-center justfy-center'}`}>
{isPlain {isPlain
? <div ? <div
onKeyDown={() => null} onKeyDown={() => null}
@@ -64,7 +64,8 @@ const DropZone = ({
key, key,
value: keyPairs[key as keyof typeof keyPairs].value, value: keyPairs[key as keyof typeof keyPairs].value,
comment: keyPairs[key as keyof typeof keyPairs].comments.join('\n'), comment: keyPairs[key as keyof typeof keyPairs].comments.join('\n'),
type: 'shared' type: 'shared',
tags: []
})); }));
break; break;
} }
@@ -86,7 +87,8 @@ const DropZone = ({
key, key,
value: keyPairs[key as keyof typeof keyPairs]?.toString() ?? '', value: keyPairs[key as keyof typeof keyPairs]?.toString() ?? '',
comment, comment,
type: 'shared' type: 'shared',
tags: []
}; };
}); });
break; break;
+70 -21
View File
@@ -1,7 +1,8 @@
import { faEllipsis } from '@fortawesome/free-solid-svg-icons'; import { faEllipsis, faXmark } from '@fortawesome/free-solid-svg-icons';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { SecretDataProps } from 'public/data/frequentInterfaces'; import { SecretDataProps, Tag } from 'public/data/frequentInterfaces';
import AddTagsMenu from './AddTagsMenu';
import DashboardInputField from './DashboardInputField'; import DashboardInputField from './DashboardInputField';
import { DeleteActionButton } from './DeleteActionButton'; import { DeleteActionButton } from './DeleteActionButton';
@@ -11,12 +12,15 @@ interface KeyPairProps {
modifyValue: (value: string, position: number) => void; modifyValue: (value: string, position: number) => void;
modifyValueOverride: (value: string | undefined, position: number) => void; modifyValueOverride: (value: string | undefined, position: number) => void;
modifyComment: (value: string, position: number) => void; modifyComment: (value: string, position: number) => void;
modifyTags: (value: Tag[], position: number) => void;
isBlurred: boolean; isBlurred: boolean;
isDuplicate: boolean; isDuplicate: boolean;
toggleSidebar: (id: string) => void; toggleSidebar: (id: string) => void;
sidebarSecretId: string; sidebarSecretId: string;
isSnapshot: boolean; isSnapshot: boolean;
deleteRow?: (props: DeleteRowFunctionProps) => void; deleteRow?: (props: DeleteRowFunctionProps) => void;
tags: Tag[];
togglePITSidebar?: (value: boolean) => void;
} }
export interface DeleteRowFunctionProps { export interface DeleteRowFunctionProps {
@@ -24,6 +28,23 @@ export interface DeleteRowFunctionProps {
secretName: string; secretName: string;
} }
const colors = [
'bg-[#f1c40f]/40',
'bg-[#cb1c8d]/40',
'bg-[#badc58]/40',
'bg-[#ff5400]/40',
'bg-[#00bbf9]/40'
]
const colorsText = [
'text-[#fcf0c3]/70',
'text-[#f2c6e3]/70',
'text-[#eef6d5]/70',
'text-[#ffddcc]/70',
'text-[#f0fffd]/70'
]
/** /**
* This component represent a single row for an environemnt variable on the dashboard * This component represent a single row for an environemnt variable on the dashboard
* @param {object} obj * @param {object} obj
@@ -32,12 +53,15 @@ export interface DeleteRowFunctionProps {
* @param {function} obj.modifyValue - modify the value of a certain environment variable * @param {function} obj.modifyValue - modify the value of a certain environment variable
* @param {function} obj.modifyValueOverride - modify the value of a certain environment variable if it is overriden * @param {function} obj.modifyValueOverride - modify the value of a certain environment variable if it is overriden
* @param {function} obj.modifyComment - modify the comment of a certain environment variable * @param {function} obj.modifyComment - modify the comment of a certain environment variable
* @param {function} obj.modifyTags - modify the tags of a certain environment variable
* @param {boolean} obj.isBlurred - if the blurring setting is turned on * @param {boolean} obj.isBlurred - if the blurring setting is turned on
* @param {boolean} obj.isDuplicate - list of all the duplicates secret names on the dashboard * @param {boolean} obj.isDuplicate - list of all the duplicates secret names on the dashboard
* @param {function} obj.toggleSidebar - open/close/switch sidebar * @param {function} obj.toggleSidebar - open/close/switch sidebar
* @param {string} obj.sidebarSecretId - the id of a secret for the side bar is displayed * @param {string} obj.sidebarSecretId - the id of a secret for the side bar is displayed
* @param {boolean} obj.isSnapshot - whether this keyPair is in a snapshot. If so, it won't have some features like sidebar * @param {boolean} obj.isSnapshot - whether this keyPair is in a snapshot. If so, it won't have some features like sidebar
* @param {function} obj.deleteRow - a function to delete a certain keyPair * @param {function} obj.deleteRow - a function to delete a certain keyPair
* @param {function} obj.togglePITSidebar - open or close the Point-in-time recovery sidebar
* @param {Tag[]} obj.tags - tags for a certain secret
* @returns * @returns
*/ */
const KeyPair = ({ const KeyPair = ({
@@ -46,21 +70,31 @@ const KeyPair = ({
modifyValue, modifyValue,
modifyValueOverride, modifyValueOverride,
modifyComment, modifyComment,
modifyTags,
isBlurred, isBlurred,
isDuplicate, isDuplicate,
toggleSidebar, toggleSidebar,
sidebarSecretId, sidebarSecretId,
isSnapshot, isSnapshot,
deleteRow deleteRow,
}: KeyPairProps) => ( togglePITSidebar,
tags
}: KeyPairProps) => {
const tagData = (tags.map((tag, index) => {return {
...tag,
color: colors[index%colors.length],
colorText: colorsText[index%colorsText.length]
}}));
return (
<div <div
className={`group flex flex-col items-center border-b border-mineshaft-500 hover:bg-white/[0.03] duration-100 ${isSnapshot && 'pointer-events-none'} ${ className={`group flex flex-col items-center border-b border-mineshaft-500 hover:bg-white/[0.03] duration-100 ${isSnapshot && 'pointer-events-none'} ${
keyPair.id === sidebarSecretId && 'bg-mineshaft-700 duration-200' keyPair.id === sidebarSecretId && 'bg-mineshaft-700 duration-200'
}`} }`}
> >
<div className="relative flex flex-row justify-between w-full mr-auto max-h-14 items-center"> <div className="relative flex flex-row justify-between w-full mr-auto max-h-14 items-center">
<div className='text-bunker-400 text-xs flex items-center justify-center w-14 h-10 cursor-default'>{keyPair.pos + 1}</div> <div className="w-2/12 border-r border-mineshaft-600 flex flex-row items-center">
<div className="w-80 border-r border-mineshaft-600"> <div className='text-bunker-400 text-xs flex items-center justify-center w-14 h-10 cursor-default'>{keyPair.pos + 1}</div>
<div className="flex items-center max-h-16"> <div className="flex items-center max-h-16">
<DashboardInputField <DashboardInputField
onChangeHandler={modifyKey} onChangeHandler={modifyKey}
@@ -74,7 +108,7 @@ const KeyPair = ({
/> />
</div> </div>
</div> </div>
<div className="w-full border-r border-mineshaft-600"> <div className="w-5/12 border-r border-mineshaft-600">
<div <div
className='flex items-center rounded-lg mt-4 md:mt-0 max-h-10' className='flex items-center rounded-lg mt-4 md:mt-0 max-h-10'
> >
@@ -89,7 +123,7 @@ const KeyPair = ({
/> />
</div> </div>
</div> </div>
<div className="w-96 border-r border-mineshaft-600"> <div className="w-2/12 border-r border-mineshaft-600">
<div className="flex items-center max-h-16"> <div className="flex items-center max-h-16">
<DashboardInputField <DashboardInputField
onChangeHandler={modifyComment} onChangeHandler={modifyComment}
@@ -101,27 +135,42 @@ const KeyPair = ({
/> />
</div> </div>
</div> </div>
{!isSnapshot && ( <div className="w-2/12 h-10 flex items-center overflow-visible overflow-r-scroll no-scrollbar no-scrollbar::-webkit-scrollbar">
<div <div className="flex items-center max-h-16">
onKeyDown={() => null} {keyPair.tags.map((tag, index) => (
role="button" index < 2 && <div key={keyPair.pos} className={`ml-2 px-1.5 ${tagData.filter(tagDp => tagDp._id === tag._id)[0]?.color} rounded-sm text-sm ${tagData.filter(tagDp => tagDp._id === tag._id)[0]?.colorText} flex items-center`}>
tabIndex={0} <span className='mb-0.5 cursor-default'>{tag.name}</span>
onClick={() => toggleSidebar(keyPair.id)} <FontAwesomeIcon icon={faXmark} className="ml-1 cursor-pointer p-1" onClick={() => modifyTags(keyPair.tags.filter(ttag => ttag._id !== tag._id), keyPair.pos)}/>
className="cursor-pointer w-[2.35rem] h-[2.35rem] px-6 rounded-md invisible group-hover:visible flex flex-row justify-center items-center" </div>
> ))}
<FontAwesomeIcon className="text-bunker-300 hover:text-primary text-lg" icon={faEllipsis} />
<AddTagsMenu allTags={tags} currentTags={keyPair.tags} modifyTags={modifyTags} position={keyPair.pos} />
</div> </div>
)} </div>
{!isSnapshot && ( <div
onKeyDown={() => null}
role="button"
tabIndex={0}
onClick={() => {
if (togglePITSidebar) {
togglePITSidebar(false);
}
toggleSidebar(keyPair.id)
}}
className={`cursor-pointer w-[1.5rem] h-[2.35rem] ml-auto group-hover:bg-mineshaft-700 z-50 rounded-md invisible group-hover:visible flex flex-row justify-center items-center ${isSnapshot ?? 'invisible'}`}
>
<FontAwesomeIcon className="text-bunker-300 hover:text-primary text-lg" icon={faEllipsis} />
</div>
<div className={`group-hover:bg-mineshaft-700 z-50 ${isSnapshot ?? 'invisible'}`}>
<DeleteActionButton <DeleteActionButton
onSubmit={() => { if (deleteRow) { onSubmit={() => { if (deleteRow) {
deleteRow({ ids: [keyPair.id], secretName: keyPair?.key }) deleteRow({ ids: [keyPair.id], secretName: keyPair?.key })
}}} }}}
isPlain isPlain
/> />
)} </div>
</div> </div>
</div> </div>
); )};
export default KeyPair; export default KeyPair;
@@ -80,7 +80,7 @@ const SideBar = ({
const { t } = useTranslation(); const { t } = useTranslation();
return ( return (
<div className="absolute border-l border-mineshaft-500 bg-bunker h-full w-96 right-0 z-[70] shadow-xl flex flex-col justify-between"> <div className="absolute border-l border-mineshaft-500 bg-bunker h-full w-[28rem] sticky top-0 right-0 z-[70] shadow-xl flex flex-col justify-between">
{isLoading ? ( {isLoading ? (
<div className="flex items-center justify-center h-full"> <div className="flex items-center justify-center h-full">
<Image <Image
@@ -4,7 +4,7 @@ import { useRouter } from 'next/router';
import { faArrowRight, faRotate, faX } from '@fortawesome/free-solid-svg-icons'; import { faArrowRight, faRotate, faX } from '@fortawesome/free-solid-svg-icons';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
// TODO: This needs to be moved from public folder // TODO: This needs to be moved from public folder
import { contextNetlifyMapping, reverseContextNetlifyMapping } from 'public/data/frequentConstants'; import { contextNetlifyMapping, integrationSlugNameMapping, reverseContextNetlifyMapping } from 'public/data/frequentConstants';
import Button from '@app/components/basic/buttons/Button'; import Button from '@app/components/basic/buttons/Button';
import ListBox from '@app/components/basic/Listbox'; import ListBox from '@app/components/basic/Listbox';
@@ -52,6 +52,7 @@ const IntegrationTile = ({
environments = [], environments = [],
handleDeleteIntegration handleDeleteIntegration
}: Props) => { }: Props) => {
// 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) || {
@@ -72,7 +73,14 @@ const IntegrationTile = ({
}); });
setApps(tempApps); setApps(tempApps);
setIntegrationApp(integration.app ? integration.app : tempApps[0].name);
if (integration?.app) {
setIntegrationApp(integration.app);
} else if (tempApps.length > 0) {
setIntegrationApp(tempApps[0].name)
} else {
setIntegrationApp('');
}
switch (integration.integration) { switch (integration.integration) {
case 'vercel': case 'vercel':
@@ -174,7 +182,7 @@ const IntegrationTile = ({
return <div />; return <div />;
}; };
if (!integrationApp || apps.length === 0) 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="max-w-5xl p-6 mx-6 mb-8 rounded-md bg-white/5 flex justify-between">
@@ -201,7 +209,8 @@ const IntegrationTile = ({
<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>
<div className="py-2.5 bg-white/[.07] rounded-md pl-4 pr-10 text-sm font-semibold text-gray-300"> <div className="py-2.5 bg-white/[.07] rounded-md pl-4 pr-10 text-sm font-semibold text-gray-300">
{integration.integration.charAt(0).toUpperCase() + integration.integration.slice(1)} {/* {integration.integration.charAt(0).toUpperCase() + integration.integration.slice(1)} */}
{integrationSlugNameMapping[integration.integration]}
</div> </div>
</div> </div>
<div className="mr-2"> <div className="mr-2">
@@ -138,7 +138,8 @@ const attemptLogin = async (
value: 'mongodb+srv://${DB_USERNAME}:${DB_PASSWORD}@mongodb.net', value: 'mongodb+srv://${DB_USERNAME}:${DB_PASSWORD}@mongodb.net',
valueOverride: undefined, valueOverride: undefined,
comment: 'This is an example of secret referencing.', comment: 'This is an example of secret referencing.',
id: '' id: '',
tags: []
}, },
{ {
pos: 1, pos: 1,
@@ -147,7 +148,8 @@ const attemptLogin = async (
valueOverride: undefined, valueOverride: undefined,
comment: comment:
'This is an example of secret overriding. Your team can have a shared value of a secret, while you can override it to whatever value you need', 'This is an example of secret overriding. Your team can have a shared value of a secret, while you can override it to whatever value you need',
id: '' id: '',
tags: []
}, },
{ {
pos: 2, pos: 2,
@@ -156,7 +158,8 @@ const attemptLogin = async (
valueOverride: undefined, valueOverride: undefined,
comment: comment:
'This is an example of secret overriding. Your team can have a shared value of a secret, while you can override it to whatever value you need', 'This is an example of secret overriding. Your team can have a shared value of a secret, while you can override it to whatever value you need',
id: '' id: '',
tags: []
}, },
{ {
pos: 3, pos: 3,
@@ -164,7 +167,8 @@ const attemptLogin = async (
value: 'user1234', value: 'user1234',
valueOverride: 'user1234', valueOverride: 'user1234',
comment: '', comment: '',
id: '' id: '',
tags: []
}, },
{ {
pos: 4, pos: 4,
@@ -172,7 +176,8 @@ const attemptLogin = async (
value: 'example_password', value: 'example_password',
valueOverride: 'example_password', valueOverride: 'example_password',
comment: '', comment: '',
id: '' id: '',
tags: []
}, },
{ {
pos: 5, pos: 5,
@@ -180,7 +185,8 @@ const attemptLogin = async (
value: 'example_twillio_token', value: 'example_twillio_token',
valueOverride: undefined, valueOverride: undefined,
comment: '', comment: '',
id: '' id: '',
tags: []
}, },
{ {
pos: 6, pos: 6,
@@ -188,7 +194,8 @@ const attemptLogin = async (
value: 'http://localhost:3000', value: 'http://localhost:3000',
valueOverride: undefined, valueOverride: undefined,
comment: '', comment: '',
id: '' id: '',
tags: []
} }
]; ];
const secrets = await encryptSecrets({ const secrets = await encryptSecrets({
@@ -1,6 +1,6 @@
import crypto from 'crypto'; import crypto from 'crypto';
import { SecretDataProps } from 'public/data/frequentInterfaces'; import { SecretDataProps, Tag } from 'public/data/frequentInterfaces';
import getLatestFileKey from '@app/pages/api/workspace/getLatestFileKey'; import getLatestFileKey from '@app/pages/api/workspace/getLatestFileKey';
@@ -20,6 +20,7 @@ interface EncryptedSecretProps {
secretValueIV: string; secretValueIV: string;
secretValueTag: string; secretValueTag: string;
type: 'personal' | 'shared'; type: 'personal' | 'shared';
tags: Tag[];
} }
/** /**
@@ -105,7 +106,8 @@ const encryptSecrets = async ({
type: type:
secret.valueOverride === undefined || secret?.value !== secret?.valueOverride secret.valueOverride === undefined || secret?.value !== secret?.valueOverride
? 'shared' ? 'shared'
: 'personal' : 'personal',
tags: secret.tags
}; };
return result; return result;
@@ -1,3 +1,5 @@
import { Tag } from 'public/data/frequentInterfaces';
import getSecrets from '@app/pages/api/files/GetSecrets'; import getSecrets from '@app/pages/api/files/GetSecrets';
import getLatestFileKey from '@app/pages/api/workspace/getLatestFileKey'; import getLatestFileKey from '@app/pages/api/workspace/getLatestFileKey';
@@ -17,6 +19,7 @@ interface EncryptedSecretProps {
secretValueIV: string; secretValueIV: string;
secretValueTag: string; secretValueTag: string;
type: 'personal' | 'shared'; type: 'personal' | 'shared';
tags: Tag[];
} }
interface SecretProps { interface SecretProps {
@@ -25,6 +28,7 @@ interface SecretProps {
type: 'personal' | 'shared'; type: 'personal' | 'shared';
comment: string; comment: string;
id: string; id: string;
tags: Tag[];
} }
interface FunctionProps { interface FunctionProps {
@@ -107,7 +111,8 @@ const getSecretsForProject = async ({
key: plainTextKey, key: plainTextKey,
value: plainTextValue, value: plainTextValue,
type: secret.type, type: secret.type,
comment: plainTextComment comment: plainTextComment,
tags: secret.tags
}); });
}); });
} }
@@ -130,7 +135,10 @@ const getSecretsForProject = async ({
)[0]?.value, )[0]?.value,
comment: tempDecryptedSecrets.filter( comment: tempDecryptedSecrets.filter(
(secret) => secret.key === key && secret.type === 'shared' (secret) => secret.key === key && secret.type === 'shared'
)[0]?.comment )[0]?.comment,
tags: tempDecryptedSecrets.filter(
(secret) => secret.key === key && secret.type === 'shared'
)[0]?.tags
})); }));
if (typeof setData === 'function') { if (typeof setData === 'function') {
@@ -52,7 +52,7 @@ export const DeleteActionModal = ({
> >
<ModalContent <ModalContent
title={title} title={title}
subTitle="This action is irreversible!!!" subTitle="This action is irreversible!"
footerContent={ footerContent={
<div className="flex items-center"> <div className="flex items-center">
<Button <Button
+1 -1
View File
@@ -41,7 +41,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 font-inter text-bunker-100 shadow-md', 'relative left-4 top-1 overflow-hidden rounded-md bg-bunker-800 font-inter text-bunker-100 shadow-md z-[100]',
dropdownContainerClassName dropdownContainerClassName
)} )}
> >
@@ -5,6 +5,7 @@ import { useRouter } from 'next/router';
import { useTranslation } from 'next-i18next'; import { useTranslation } from 'next-i18next';
import { faXmark } from '@fortawesome/free-solid-svg-icons'; import { faXmark } from '@fortawesome/free-solid-svg-icons';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { Tag } from 'public/data/frequentInterfaces';
import Button from '@app/components/basic/buttons/Button'; import Button from '@app/components/basic/buttons/Button';
import { import {
@@ -48,6 +49,7 @@ interface EncrypetedSecretVersionListProps {
secretKeyTag: string; secretKeyTag: string;
environment: string; environment: string;
type: 'personal' | 'shared'; type: 'personal' | 'shared';
tags: Tag[];
} }
/** /**
@@ -106,6 +108,7 @@ const PITRecoverySidebar = ({ toggleSidebar, setSnapshotData, chosenSnapshot }:
pos, pos,
type: encryptedSecretVersion.type, type: encryptedSecretVersion.type,
environment: encryptedSecretVersion.environment, environment: encryptedSecretVersion.environment,
tags: encryptedSecretVersion.tags,
key: decryptSymmetric({ key: decryptSymmetric({
ciphertext: encryptedSecretVersion.secretKeyCiphertext, ciphertext: encryptedSecretVersion.secretKeyCiphertext,
iv: encryptedSecretVersion.secretKeyIV, iv: encryptedSecretVersion.secretKeyIV,
@@ -134,6 +137,9 @@ const PITRecoverySidebar = ({ toggleSidebar, setSnapshotData, chosenSnapshot }:
environment: decryptedSecretVersions.filter( environment: decryptedSecretVersions.filter(
(secret: SecretDataProps) => secret.key === key && secret.type === 'shared' (secret: SecretDataProps) => secret.key === key && secret.type === 'shared'
)[0].environment, )[0].environment,
tags: decryptedSecretVersions.filter(
(secret: SecretDataProps) => secret.key === key && secret.type === 'shared'
)[0].tags,
value: decryptedSecretVersions.filter( value: decryptedSecretVersions.filter(
(secret: SecretDataProps) => secret.key === key && secret.type === 'shared' (secret: SecretDataProps) => secret.key === key && secret.type === 'shared'
)[0]?.value, )[0]?.value,
@@ -155,7 +161,7 @@ const PITRecoverySidebar = ({ toggleSidebar, setSnapshotData, chosenSnapshot }:
<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 right-0 z-[70] shadow-xl flex flex-col justify-between`} } fixed h-full w-[28rem] right-0 z-[70] shadow-xl flex flex-col justify-between sticky top-0`}
> >
{isLoading ? ( {isLoading ? (
<div className="flex items-center justify-center h-full mb-8"> <div className="flex items-center justify-center h-full mb-8">
+1
View File
@@ -2,4 +2,5 @@ export * from './auth';
export * from './keys'; export * from './keys';
export * from './serviceTokens'; export * from './serviceTokens';
export * from './subscriptions'; export * from './subscriptions';
export * from './tags';
export * from './workspace'; export * from './workspace';
@@ -0,0 +1,3 @@
export {
useGetIntegrationAuthApps,
useGetIntegrationAuthById} from './queries';
@@ -0,0 +1,38 @@
import { useQuery } from "@tanstack/react-query";
import { apiRequest } from "@app/config/request";
import {
App,
IntegrationAuth} from './types';
const integrationAuthKeys = {
getIntegrationAuthById: (integrationAuthId: string) => [{ integrationAuthId }, 'integrationAuth'] as const,
getIntegrationAuthApps: (integrationAuthId: string) => [{ integrationAuthId }, 'integrationAuthApps'] as const,
}
const fetchIntegrationAuthById = async (integrationAuthId: string) => {
const { data } = await apiRequest.get<{ integrationAuth: IntegrationAuth }>(`/api/v1/integration-auth/${integrationAuthId}`);
return data.integrationAuth;
}
const fetchIntegrationAuthApps = async (integrationAuthId: string) => {
const { data } = await apiRequest.get<{ apps: App[] }>(`/api/v1/integration-auth/${integrationAuthId}/apps`);
return data.apps;
}
export const useGetIntegrationAuthById = (integrationAuthId: string) => {
return useQuery({
queryKey: integrationAuthKeys.getIntegrationAuthById(integrationAuthId),
queryFn: () => fetchIntegrationAuthById(integrationAuthId),
enabled: true
});
}
export const useGetIntegrationAuthApps = (integrationAuthId: string) => {
return useQuery({
queryKey: integrationAuthKeys.getIntegrationAuthApps(integrationAuthId),
queryFn: () => fetchIntegrationAuthApps(integrationAuthId),
enabled: true
});
}
@@ -0,0 +1,13 @@
export type IntegrationAuth = {
_id: string;
workspace: string;
integration: string;
teamId?: string;
accountId?: string;
}
export type App = {
name: string;
appId?: string;
owner?: string;
}
+3
View File
@@ -0,0 +1,3 @@
export { useGetWsTags } from './queries';
export { useCreateWsTag } from './queries';
export { useDeleteWsTag } from './queries';
+62
View File
@@ -0,0 +1,62 @@
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
import { apiRequest } from '@app/config/request';
import {
CreateTagDTO,
CreateTagRes,
DeleteTagDTO,
DeleteWsTagRes,
UserWsTags
} from './types';
const workspaceTags = {
getWsTags: (workspaceID: string) => ['workspace-tags', { workspaceID }] as const
};
const fetchWsTag = async (workspaceID: string) => {
const { data } = await apiRequest.get<{ workspaceTags: UserWsTags }>(
`/api/v2/workspace/${workspaceID}/tags`
);
return data.workspaceTags;
};
export const useGetWsTags = (workspaceID: string) =>
useQuery({
queryKey: workspaceTags.getWsTags(workspaceID),
queryFn: () => fetchWsTag(workspaceID),
enabled: Boolean(workspaceID)
});
export const useCreateWsTag = () => {
const queryClient = useQueryClient();
return useMutation<CreateTagRes, {}, CreateTagDTO>({
mutationFn: async ({ workspaceID, tagName, tagSlug }) => {
const { data } = await apiRequest.post(`/api/v2/workspace/${workspaceID}/tags`, {
name: tagName,
slug: tagSlug
})
return data;
},
onSuccess: (tagData) => {
queryClient.invalidateQueries(workspaceTags.getWsTags(tagData?.workspace));
}
});
};
export const useDeleteWsTag = () => {
const queryClient = useQueryClient();
return useMutation<DeleteWsTagRes, {}, DeleteTagDTO>({
mutationFn: async ({ tagID }) => {
const { data } = await apiRequest.delete(`/api/v2/workspace/tags/${tagID}`);
return data
},
onSuccess: (tagData) => {
queryClient.invalidateQueries(workspaceTags.getWsTags(tagData?.workspace));
}
});
};
+39
View File
@@ -0,0 +1,39 @@
export type UserWsTags = WsTag[];
export type WsTag = {
_id: string;
name: string;
slug: string;
workspace: string;
createdAt: string;
updatedAt: string;
__v: number;
}
export type WorkspaceTag = { _id: string; name: string; slug: string };
export type CreateTagDTO = {
workspaceID: string;
tagSlug: string;
tagName: string;
};
export type CreateTagRes = {
name: string;
slug: string;
workspace: string;
createdAt: string;
user: string;
_id: string;
};
export type DeleteTagDTO = { tagID: string; };
export type DeleteWsTagRes = {
name: string;
slug: string;
workspace: string;
createdAt: string;
user: string;
_id: string;
};
+2 -1
View File
@@ -9,5 +9,6 @@ export type {
RenameWorkspaceDTO, RenameWorkspaceDTO,
UpdateEnvironmentDTO, UpdateEnvironmentDTO,
Workspace, Workspace,
WorkspaceEnv WorkspaceEnv,
WorkspaceTag
} from './workspace/types'; } from './workspace/types';
+2 -2
View File
@@ -3,6 +3,6 @@ export {
useDeleteWorkspace, useDeleteWorkspace,
useDeleteWsEnvironment, useDeleteWsEnvironment,
useGetUserWorkspaces, useGetUserWorkspaces,
useGetWorkspaceById,
useRenameWorkspace, useRenameWorkspace,
useUpdateWsEnvironment useUpdateWsEnvironment} from './queries';
} from './queries';
+15 -1
View File
@@ -11,16 +11,30 @@ import {
Workspace Workspace
} from './types'; } from './types';
const workspaceKeys = { const workspaceKeys = {
getWorkspaceById: (workspaceId: string) => [{ workspaceId }, 'workspace'] as const,
getAllUserWorkspace: ['workspaces'] as const getAllUserWorkspace: ['workspaces'] as const
}; };
const fetchWorkspaceById = async (workspaceId: string) => {
const { data } = await apiRequest.get<{ workspace: Workspace }>(`/api/v1/workspace/${workspaceId}`);
return data.workspace;
}
const fetchUserWorkspaces = async () => { const fetchUserWorkspaces = async () => {
const { data } = await apiRequest.get<{ workspaces: Workspace[] }>('/api/v1/workspace'); const { data } = await apiRequest.get<{ workspaces: Workspace[] }>('/api/v1/workspace');
return data.workspaces; return data.workspaces;
}; };
export const useGetWorkspaceById = (workspaceId: string) => {
return useQuery({
queryKey: workspaceKeys.getWorkspaceById(workspaceId),
queryFn: () => fetchWorkspaceById(workspaceId),
enabled: true
});
};
export const useGetUserWorkspaces = () => export const useGetUserWorkspaces = () =>
useQuery(workspaceKeys.getAllUserWorkspace, fetchUserWorkspaces); useQuery(workspaceKeys.getAllUserWorkspace, fetchUserWorkspaces);
@@ -7,6 +7,7 @@ export type Workspace = {
}; };
export type WorkspaceEnv = { name: string; slug: string }; export type WorkspaceEnv = { name: string; slug: string };
export type WorkspaceTag = { _id: string; name: string; slug: string };
// mutation dto // mutation dto
export type RenameWorkspaceDTO = { workspaceID: string; newWorkspaceName: string }; export type RenameWorkspaceDTO = { workspaceID: string; newWorkspaceName: string };
@@ -26,7 +26,7 @@ const AuthorizeIntegration = ({ workspaceId, code, integration }: Props) =>
}) })
}).then(async (res) => { }).then(async (res) => {
if (res && res.status === 200) { if (res && res.status === 200) {
return res; return (await res.json()).integrationAuth;
} }
console.log('Failed to authorize the integration'); console.log('Failed to authorize the integration');
return undefined; return undefined;
@@ -1,7 +1,13 @@
import SecurityClient from '@app/components/utilities/SecurityClient'; import SecurityClient from '@app/components/utilities/SecurityClient';
interface Props { interface Props {
integrationAuthId: string; integrationAuthId: string;
isActive: boolean;
app: string | null;
appId: string | null;
sourceEnvironment: string;
targetEnvironment: string | null;
owner: string | null;
} }
/** /**
* This route creates a new integration based on the integration authorization with id [integrationAuthId] * This route creates a new integration based on the integration authorization with id [integrationAuthId]
@@ -10,7 +16,13 @@ interface Props {
* @returns * @returns
*/ */
const createIntegration = ({ const createIntegration = ({
integrationAuthId integrationAuthId,
isActive,
app,
appId,
sourceEnvironment,
targetEnvironment,
owner
}: Props) => }: Props) =>
SecurityClient.fetchCall('/api/v1/integration', { SecurityClient.fetchCall('/api/v1/integration', {
method: 'POST', method: 'POST',
@@ -18,7 +30,13 @@ const createIntegration = ({
'Content-Type': 'application/json' 'Content-Type': 'application/json'
}, },
body: JSON.stringify({ body: JSON.stringify({
integrationAuthId integrationAuthId,
isActive,
app,
appId,
sourceEnvironment,
targetEnvironment,
owner
}) })
}).then(async (res) => { }).then(async (res) => {
if (res && res.status === 200) { if (res && res.status === 200) {
@@ -0,0 +1,22 @@
import SecurityClient from '@app/components/utilities/SecurityClient';
/**
* This route lets us get the tags for a certain project
* @param {string} workspaceId
* @returns
*/
const getWorkspaceTags = ({ workspaceId }: { workspaceId: string }) =>
SecurityClient.fetchCall(`/api/v2/workspace/${workspaceId}/tags`, {
method: 'GET',
headers: {
'Content-Type': 'application/json'
}
}).then(async (res) => {
if (res?.status === 200) {
return (await res.json()).workspaceTags;
}
console.log('Failed to get the tags available in a certain project');
return undefined;
});
export default getWorkspaceTags;
+118 -53
View File
@@ -14,9 +14,11 @@ import {
faEyeSlash, faEyeSlash,
faFolderOpen, faFolderOpen,
faMagnifyingGlass, faMagnifyingGlass,
faPlus faPlus,
faXmark
} from '@fortawesome/free-solid-svg-icons'; } from '@fortawesome/free-solid-svg-icons';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { Tag } from 'public/data/frequentInterfaces';
import Button from '@app/components/basic/buttons/Button'; import Button from '@app/components/basic/buttons/Button';
import ListBox from '@app/components/basic/Listbox'; import ListBox from '@app/components/basic/Listbox';
@@ -45,6 +47,7 @@ import checkUserAction from '../api/userActions/checkUserAction';
import registerUserAction from '../api/userActions/registerUserAction'; import registerUserAction from '../api/userActions/registerUserAction';
import getWorkspaceEnvironments from '../api/workspace/getWorkspaceEnvironments'; import getWorkspaceEnvironments from '../api/workspace/getWorkspaceEnvironments';
import getWorkspaces from '../api/workspace/getWorkspaces'; import getWorkspaces from '../api/workspace/getWorkspaces';
import getWorkspaceTags from '../api/workspace/getWorkspaceTags';
type WorkspaceEnv = { type WorkspaceEnv = {
name: string; name: string;
@@ -60,6 +63,7 @@ interface SecretDataProps {
id: string; id: string;
idOverride: string | undefined; idOverride: string | undefined;
comment: string; comment: string;
tags: Tag[];
} }
// eslint-disable-next-line @typescript-eslint/no-unused-vars // eslint-disable-next-line @typescript-eslint/no-unused-vars
@@ -83,6 +87,7 @@ interface SnapshotProps {
value: string; value: string;
valueOverride: string; valueOverride: string;
comment: string; comment: string;
tags: Tag[];
}[]; }[];
} }
@@ -128,6 +133,7 @@ export default function Dashboard() {
const [numSnapshots, setNumSnapshots] = useState<number>(); const [numSnapshots, setNumSnapshots] = useState<number>();
const [saveLoading, setSaveLoading] = useState(false); const [saveLoading, setSaveLoading] = useState(false);
const [dropZoneData, setDropZoneData] = useState<SecretDataProps[]>(); const [dropZoneData, setDropZoneData] = useState<SecretDataProps[]>();
const [projectTags, setProjectTags] = useState<Tag[]>([]);
const { t } = useTranslation(); const { t } = useTranslation();
const { createNotification } = useNotificationContext(); const { createNotification } = useNotificationContext();
@@ -235,6 +241,8 @@ export default function Dashboard() {
action: 'first_time_secrets_pushed' action: 'first_time_secrets_pushed'
}); });
setHasUserEverPushed(!!userAction); setHasUserEverPushed(!!userAction);
setProjectTags(await getWorkspaceTags({ workspaceId }));
} catch (error) { } catch (error) {
console.log('Error', error); console.log('Error', error);
setData(undefined); setData(undefined);
@@ -277,7 +285,8 @@ export default function Dashboard() {
key: '', key: '',
value: '', value: '',
valueOverride: undefined, valueOverride: undefined,
comment: '' comment: '',
tags: []
}, },
...data! ...data!
]); ]);
@@ -297,7 +306,8 @@ export default function Dashboard() {
key: '', key: '',
value: '', value: '',
valueOverride: undefined, valueOverride: undefined,
comment: '' comment: '',
tags: []
}, },
]); ]);
}; };
@@ -335,6 +345,11 @@ export default function Dashboard() {
setButtonReady(true); setButtonReady(true);
}; };
const modifyTags = (tags: Tag[], pos: number) => {
setData((oldData) => oldData?.map((e) => (e.pos === pos ? { ...e, tags } : e)));
setButtonReady(true);
};
// For speed purposes and better perforamance, we are using useCallback // For speed purposes and better perforamance, we are using useCallback
const listenChangeValue = useCallback((value: string, pos: number) => { const listenChangeValue = useCallback((value: string, pos: number) => {
modifyValue(value, pos); modifyValue(value, pos);
@@ -352,6 +367,10 @@ export default function Dashboard() {
modifyComment(value, pos); modifyComment(value, pos);
}, []); }, []);
const listenChangeTags = useCallback((value: Tag[], pos: number) => {
modifyTags(value, pos);
}, []);
/** /**
* Save the changes of environment variables and push them to the database * Save the changes of environment variables and push them to the database
*/ */
@@ -424,7 +443,9 @@ export default function Dashboard() {
newData!.filter((dataPoint) => dataPoint.id === initDataPoint.id)[0].key !== newData!.filter((dataPoint) => dataPoint.id === initDataPoint.id)[0].key !==
initDataPoint.key || initDataPoint.key ||
newData!.filter((dataPoint) => dataPoint.id === initDataPoint.id)[0].comment !== newData!.filter((dataPoint) => dataPoint.id === initDataPoint.id)[0].comment !==
initDataPoint.comment) initDataPoint.comment) ||
newData!.filter((dataPoint) => dataPoint.id === initDataPoint.id)[0].tags !==
initDataPoint.tags
) )
.map((secret) => secret.id) .map((secret) => secret.id)
.includes(newDataPoint.id) .includes(newDataPoint.id)
@@ -458,7 +479,8 @@ export default function Dashboard() {
valueOverride: override.valueOverride, valueOverride: override.valueOverride,
comment: '', comment: '',
id: String(override.idOverride), id: String(override.idOverride),
idOverride: String(override.idOverride) idOverride: String(override.idOverride),
tags: override.tags
})); }));
console.log('override add', overridesToBeAdded.length); console.log('override add', overridesToBeAdded.length);
@@ -473,7 +495,9 @@ export default function Dashboard() {
newOverrides!.filter((dataPoint) => dataPoint.id === initDataPoint.id)[0].key !== newOverrides!.filter((dataPoint) => dataPoint.id === initDataPoint.id)[0].key !==
initDataPoint.key || initDataPoint.key ||
newOverrides!.filter((dataPoint) => dataPoint.id === initDataPoint.id)[0] newOverrides!.filter((dataPoint) => dataPoint.id === initDataPoint.id)[0]
.comment !== initDataPoint.comment) .comment !== initDataPoint.comment ||
newOverrides!.filter((dataPoint) => dataPoint.id === initDataPoint.id)[0]
.tags !== initDataPoint.tags)
) )
.map((secret) => secret.id) .map((secret) => secret.id)
.includes(newDataPoint.id) .includes(newDataPoint.id)
@@ -485,7 +509,8 @@ export default function Dashboard() {
valueOverride: override.valueOverride, valueOverride: override.valueOverride,
comment: '', comment: '',
id: String(override.idOverride), id: String(override.idOverride),
idOverride: String(override.idOverride) idOverride: String(override.idOverride),
tags: override.tags
})); }));
console.log('override update', overridesToBeUpdated.length); console.log('override update', overridesToBeUpdated.length);
@@ -583,34 +608,6 @@ export default function Dashboard() {
.map((duplicate) => duplicate.key) ?? [] .map((duplicate) => duplicate.key) ?? []
} }
/> />
{sidebarSecretId !== 'None' && (
<SideBar
toggleSidebar={toggleSidebar}
data={data.filter(
(row: SecretDataProps) =>
row.key === data.filter((r) => r.id === sidebarSecretId)[0]?.key
)}
modifyKey={listenChangeKey}
modifyValue={listenChangeValue}
modifyValueOverride={listenChangeValueOverride}
modifyComment={listenChangeComment}
buttonReady={buttonReady}
workspaceEnvs={workspaceEnvs}
selectedEnv={selectedEnv!}
workspaceId={workspaceId}
savePush={savePush}
sharedToHide={sharedToHide}
setSharedToHide={setSharedToHide}
deleteRow={deleteCertainRow}
/>
)}
{PITSidebarOpen && (
<PITRecoverySidebar
toggleSidebar={togglePITSidebar}
chosenSnapshot={String(snapshotData?.id ? snapshotData.id : '')}
setSnapshotData={setSnapshotData}
/>
)}
<div className="w-full max-h-96 pb-2 dark:[color-scheme:dark]"> <div className="w-full max-h-96 pb-2 dark:[color-scheme:dark]">
<NavHeader pageName={t('dashboard:title')} isProjectRelated /> <NavHeader pageName={t('dashboard:title')} isProjectRelated />
{checkDocsPopUpVisible && ( {checkDocsPopUpVisible && (
@@ -665,7 +662,10 @@ export default function Dashboard() {
<div className="flex justify-start max-w-sm mt-1 mr-2"> <div className="flex justify-start max-w-sm mt-1 mr-2">
<Button <Button
text={String(`${numSnapshots} ${t('Commits')}`)} text={String(`${numSnapshots} ${t('Commits')}`)}
onButtonPressed={() => togglePITSidebar(true)} onButtonPressed={() => {
toggleSidebar('None');
togglePITSidebar(true)
}}
color="mineshaft" color="mineshaft"
size="md" size="md"
icon={faClockRotateLeft} icon={faClockRotateLeft}
@@ -700,7 +700,8 @@ export default function Dashboard() {
valueOverride: sv.valueOverride, valueOverride: sv.valueOverride,
key: sv.key, key: sv.key,
value: sv.value, value: sv.value,
comment: '' comment: '',
tags: sv.tags
})); }));
setData(rolledBackSecrets); setData(rolledBackSecrets);
@@ -818,27 +819,58 @@ export default function Dashboard() {
className="mt-1 max-h-[calc(100vh-280px)] overflow-hidden overflow-y-scroll no-scrollbar no-scrollbar::-webkit-scrollbar border border-mineshaft-600 rounded-md" className="mt-1 max-h-[calc(100vh-280px)] overflow-hidden overflow-y-scroll no-scrollbar no-scrollbar::-webkit-scrollbar border border-mineshaft-600 rounded-md"
> >
<div ref={secretsTop} /> <div ref={secretsTop} />
<div className='bg-mineshaft-800 text-sm rounded-t-md h-10 w-full flex flex-row items-center border-b-2 border-mineshaft-500 sticky top-0 z-[60]'> <div
<div className='w-14'/> className='group flex flex-col items-center bg-mineshaft-800 border-b-2 border-mineshaft-500 duration-100 sticky top-0 z-[60]'
<div className='text-bunker-300 relative font-semibold h-10 flex items-center w-80 pl-2 border-r border-mineshaft-600'> >
<span>Key</span> <div className="relative flex flex-row justify-between w-full mr-auto max-h-14 items-center">
{!snapshotData && <IconButton <div className="w-2/12 border-r border-mineshaft-600 flex flex-row items-center">
ariaLabel="copy icon" <div className='text-transparent text-xs flex items-center justify-center w-14 h-10 cursor-default'>0</div>
variant="plain" <span className='px-2 text-bunker-300 font-semibold'>Key</span>
className="group relative ml-2" {!snapshotData && <IconButton
onClick={() => reorderRows(1)} ariaLabel="copy icon"
> variant="plain"
{sortMethod === 'alphabetical' ? <FontAwesomeIcon icon={faArrowUp} /> : <FontAwesomeIcon icon={faArrowDown} />} className="group relative ml-2"
</IconButton>} onClick={() => reorderRows(1)}
>
{sortMethod === 'alphabetical' ? <FontAwesomeIcon icon={faArrowUp} /> : <FontAwesomeIcon icon={faArrowDown} />}
</IconButton>}
</div>
<div className="w-5/12 border-r border-mineshaft-600">
<div
className='flex items-center rounded-lg mt-4 md:mt-0 max-h-10'
>
<div className='text-bunker-300 px-2 font-semibold h-10 flex items-center w-7/12'>Value</div>
</div>
</div>
<div className="w-2/12 border-r border-mineshaft-600">
<div className="flex items-center max-h-16">
<div className='text-bunker-300 px-2 font-semibold h-10 flex items-center w-3/12'>Comment</div>
</div>
</div>
<div className="w-2/12">
<div className="flex items-center max-h-16">
<div className='text-bunker-300 px-2 font-semibold h-10 flex items-center w-3/12'>Tags</div>
</div>
</div>
<div
className="w-[1.5rem] h-[2.35rem] ml-auto rounded-md flex flex-row justify-center items-center"
/>
<div className='w-[1.5rem] h-[2.35rem] mr-2 flex items-center justfy-center'>
<div
onKeyDown={() => null}
role="none"
onClick={() => {}}
className="invisible group-hover:visible"
>
<FontAwesomeIcon className="text-bunker-300 hover:text-red pl-2 pr-6 text-lg mt-0.5 invisible" icon={faXmark} />
</div>
</div>
</div> </div>
<div className='text-bunker-300 pl-2 font-semibold h-10 flex items-center w-full border-r border-mineshaft-600'>Value</div>
<div className='text-bunker-300 pl-2 font-semibold h-10 flex items-center w-96'>Comment</div>
{!snapshotData && <div className='w-[9.3rem]'/>}
</div> </div>
<div className="bg-mineshaft-800 rounded-b-md border-bunker-600"> <div className="bg-mineshaft-800 rounded-b-md border-bunker-600">
{!snapshotData && {!snapshotData &&
data data
?.filter((row) => row.key?.toUpperCase().includes(searchKeys.toUpperCase())) ?.filter((row) => row.key?.toUpperCase().includes(searchKeys.toUpperCase()) || row.tags?.map(tag => tag.name).join(" ")?.toUpperCase().includes(searchKeys.toUpperCase()))
.filter((row) => !sharedToHide.includes(row.id)) .filter((row) => !sharedToHide.includes(row.id))
.map((keyPair) => ( .map((keyPair) => (
<KeyPair <KeyPair
@@ -848,14 +880,17 @@ export default function Dashboard() {
modifyValueOverride={listenChangeValueOverride} modifyValueOverride={listenChangeValueOverride}
modifyKey={listenChangeKey} modifyKey={listenChangeKey}
modifyComment={listenChangeComment} modifyComment={listenChangeComment}
modifyTags={listenChangeTags}
isBlurred={blurred} isBlurred={blurred}
isDuplicate={findDuplicates(data?.map((item) => item.key))?.includes( isDuplicate={findDuplicates(data?.map((item) => item.key))?.includes(
keyPair.key keyPair.key
)} )}
toggleSidebar={toggleSidebar} toggleSidebar={toggleSidebar}
togglePITSidebar={togglePITSidebar}
sidebarSecretId={sidebarSecretId} sidebarSecretId={sidebarSecretId}
isSnapshot={false} isSnapshot={false}
deleteRow={deleteCertainRow} deleteRow={deleteCertainRow}
tags={projectTags}
/> />
))} ))}
{snapshotData && {snapshotData &&
@@ -887,6 +922,7 @@ export default function Dashboard() {
modifyValueOverride={listenChangeValueOverride} modifyValueOverride={listenChangeValueOverride}
modifyKey={listenChangeKey} modifyKey={listenChangeKey}
modifyComment={listenChangeComment} modifyComment={listenChangeComment}
modifyTags={listenChangeTags}
isBlurred={blurred} isBlurred={blurred}
isDuplicate={findDuplicates(data?.map((item) => item.key))?.includes( isDuplicate={findDuplicates(data?.map((item) => item.key))?.includes(
keyPair.key keyPair.key
@@ -894,6 +930,7 @@ export default function Dashboard() {
toggleSidebar={toggleSidebar} toggleSidebar={toggleSidebar}
sidebarSecretId={sidebarSecretId} sidebarSecretId={sidebarSecretId}
isSnapshot isSnapshot
tags={projectTags}
/> />
))} ))}
<div className='bg-mineshaft-800 text-sm rounded-t-md hover:bg-mineshaft-700 h-10 w-full flex flex-row items-center border-b-2 border-mineshaft-500 sticky top-0 z-[60]'> <div className='bg-mineshaft-800 text-sm rounded-t-md hover:bg-mineshaft-700 h-10 w-full flex flex-row items-center border-b-2 border-mineshaft-500 sticky top-0 z-[60]'>
@@ -947,6 +984,34 @@ export default function Dashboard() {
)} )}
</div> </div>
</div> </div>
{sidebarSecretId !== 'None' && (
<SideBar
toggleSidebar={toggleSidebar}
data={data.filter(
(row: SecretDataProps) =>
row.key === data.filter((r) => r.id === sidebarSecretId)[0]?.key
)}
modifyKey={listenChangeKey}
modifyValue={listenChangeValue}
modifyValueOverride={listenChangeValueOverride}
modifyComment={listenChangeComment}
buttonReady={buttonReady}
workspaceEnvs={workspaceEnvs}
selectedEnv={selectedEnv!}
workspaceId={workspaceId}
savePush={savePush}
sharedToHide={sharedToHide}
setSharedToHide={setSharedToHide}
deleteRow={deleteCertainRow}
/>
)}
{PITSidebarOpen && (
<PITRecoverySidebar
toggleSidebar={togglePITSidebar}
chosenSnapshot={String(snapshotData?.id ? snapshotData.id : '')}
setSnapshotData={setSnapshotData}
/>
)}
</div> </div>
</div> </div>
) : ( ) : (
-41
View File
@@ -1,41 +0,0 @@
import { useEffect } from 'react';
import { useRouter } from 'next/router';
import queryString from 'query-string';
import AuthorizeIntegration from './api/integrations/authorizeIntegration';
export default function Github() {
const router = useRouter();
const parsedUrl = queryString.parse(router.asPath.split('?')[1]);
const {code} = parsedUrl;
const {state} = parsedUrl;
/**
* Here we forward to the default workspace if a user opens this url
*/
// eslint-disable-next-line react-hooks/exhaustive-deps
useEffect(() => {
(async () => {
try {
if (state === localStorage.getItem('latestCSRFToken')) {
localStorage.removeItem('latestCSRFToken');
await AuthorizeIntegration({
workspaceId: localStorage.getItem('projectData.id') as string,
code: code as string,
integration: 'github',
});
router.push(
`/integrations/${ localStorage.getItem('projectData.id')}`
);
}
} catch (error) {
console.error('Github integration error: ', error);
}
})();
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
return <div />;
}
Github.requireAuth = true;
-41
View File
@@ -1,41 +0,0 @@
import { useEffect } from 'react';
import { useRouter } from 'next/router';
import queryString from 'query-string';
import AuthorizeIntegration from './api/integrations/authorizeIntegration';
export default function Heroku() {
const router = useRouter();
const parsedUrl = queryString.parse(router.asPath.split('?')[1]);
const {code} = parsedUrl;
const {state} = parsedUrl;
/**
* Here we forward to the default workspace if a user opens this url
*/
// eslint-disable-next-line react-hooks/exhaustive-deps
useEffect(() => {
(async () => {
try {
if (state === localStorage.getItem('latestCSRFToken')) {
localStorage.removeItem('latestCSRFToken');
await AuthorizeIntegration({
workspaceId: localStorage.getItem('projectData.id') as string,
code: code as string,
integration: 'heroku',
});
router.push(
`/integrations/${ localStorage.getItem('projectData.id')}`
);
}
} catch (error) {
console.error('Heroku integration error: ', error);
}
})();
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
return <div />;
}
Heroku.requireAuth = true;
+78 -98
View File
@@ -7,7 +7,6 @@ import { useTranslation } from 'next-i18next';
import frameworkIntegrationOptions from 'public/json/frameworkIntegrations.json'; import frameworkIntegrationOptions from 'public/json/frameworkIntegrations.json';
import ActivateBotDialog from '@app/components/basic/dialog/ActivateBotDialog'; import ActivateBotDialog from '@app/components/basic/dialog/ActivateBotDialog';
import IntegrationAccessTokenDialog from '@app/components/basic/dialog/IntegrationAccessTokenDialog';
import CloudIntegrationSection from '@app/components/integrations/CloudIntegrationSection'; import CloudIntegrationSection from '@app/components/integrations/CloudIntegrationSection';
import FrameworkIntegrationSection from '@app/components/integrations/FrameworkIntegrationSection'; import FrameworkIntegrationSection from '@app/components/integrations/FrameworkIntegrationSection';
import IntegrationSection from '@app/components/integrations/IntegrationSection'; import IntegrationSection from '@app/components/integrations/IntegrationSection';
@@ -20,12 +19,10 @@ import {
} from '../../components/utilities/cryptography/crypto'; } from '../../components/utilities/cryptography/crypto';
import getBot from '../api/bot/getBot'; import getBot from '../api/bot/getBot';
import setBotActiveStatus from '../api/bot/setBotActiveStatus'; import setBotActiveStatus from '../api/bot/setBotActiveStatus';
import createIntegration from '../api/integrations/createIntegration';
import deleteIntegration from '../api/integrations/DeleteIntegration'; import deleteIntegration from '../api/integrations/DeleteIntegration';
import getIntegrationOptions from '../api/integrations/GetIntegrationOptions'; import getIntegrationOptions from '../api/integrations/GetIntegrationOptions';
import getWorkspaceAuthorizations from '../api/integrations/getWorkspaceAuthorizations'; import getWorkspaceAuthorizations from '../api/integrations/getWorkspaceAuthorizations';
import getWorkspaceIntegrations from '../api/integrations/getWorkspaceIntegrations'; import getWorkspaceIntegrations from '../api/integrations/getWorkspaceIntegrations';
import saveIntegrationAccessToken from '../api/integrations/saveIntegrationAccessToken';
import getAWorkspace from '../api/workspace/getAWorkspace'; import getAWorkspace from '../api/workspace/getAWorkspace';
import getLatestFileKey from '../api/workspace/getLatestFileKey'; import getLatestFileKey from '../api/workspace/getLatestFileKey';
@@ -52,6 +49,7 @@ interface Integration {
} }
interface IntegrationOption { interface IntegrationOption {
tenantId?: string;
clientId: string; clientId: string;
clientSlug?: string; // vercel-integration specific clientSlug?: string; // vercel-integration specific
docsLink: string; docsLink: string;
@@ -75,7 +73,6 @@ export default function Integrations() {
// TODO: These will have its type when migratiing towards react-query // TODO: These will have its type when migratiing towards react-query
const [bot, setBot] = useState<any>(null); const [bot, setBot] = useState<any>(null);
const [isActivateBotDialogOpen, setIsActivateBotDialogOpen] = useState(false); const [isActivateBotDialogOpen, setIsActivateBotDialogOpen] = useState(false);
const [isIntegrationAccessTokenDialogOpen, setIntegrationAccessTokenDialogOpen] = useState(false);
const [selectedIntegrationOption, setSelectedIntegrationOption] = useState<IntegrationOption | null>(null); const [selectedIntegrationOption, setSelectedIntegrationOption] = useState<IntegrationOption | null>(null);
const router = useRouter(); const router = useRouter();
@@ -166,80 +163,83 @@ export default function Integrations() {
} }
}; };
/** const handleUnauthorizedIntegrationOptionPress = (integrationOption: IntegrationOption) => {
* Handle integration option authorization for a given integration option [integrationOption]
* @param {Object} obj
* @param {Object} obj.integrationOption - an integration option
* @param {String} obj.name
* @param {String} obj.type
* @param {String} obj.docsLink
* @returns
*/
const handleIntegrationOption = async ({
integrationOption,
accessToken
}: {
integrationOption: IntegrationOption,
accessToken?: string;
}) => {
try { try {
if (!bot.isActive) { // generate CSRF token for OAuth2 code-token exchange integrations
await handleBotActivate(); const state = crypto.randomBytes(16).toString('hex');
localStorage.setItem('latestCSRFToken', state);
let link = '';
switch (integrationOption.slug) {
case 'azure-key-vault':
link = `https://login.microsoftonline.com/${integrationOption.tenantId}/oauth2/v2.0/authorize?client_id=${integrationOption.clientId}&response_type=code&redirect_uri=${window.location.origin}/integrations/azure-key-vault/oauth2/callback&response_mode=query&scope=https://vault.azure.net/.default openid offline_access&state=${state}`;
break;
case 'heroku':
link = `https://id.heroku.com/oauth/authorize?client_id=${integrationOption.clientId}&response_type=code&scope=write-protected&state=${state}`;
break;
case 'vercel':
link = `https://vercel.com/integrations/${integrationOption.clientSlug}/new?state=${state}`;
break;
case 'netlify':
link = `https://app.netlify.com/authorize?client_id=${integrationOption.clientId}&response_type=code&state=${state}&redirect_uri=${window.location.origin}/integrations/netlify/oauth2/callback`;
break;
case 'github':
link = `https://github.com/login/oauth/authorize?client_id=${integrationOption.clientId}&response_type=code&scope=repo&redirect_uri=${window.location.origin}/integrations/github/oauth2/callback&state=${state}`;
break;
case 'render':
link = `${window.location.origin}/integrations/render/authorize`
break;
case 'flyio':
link = `${window.location.origin}/integrations/flyio/authorize`
break;
default:
break;
} }
if (integrationOption.type === 'oauth') { if (link !== '') {
// integration is of type OAuth window.location.assign(link);
// generate CSRF token for OAuth2 code-token exchange integrations
const state = crypto.randomBytes(16).toString('hex');
localStorage.setItem('latestCSRFToken', state);
switch (integrationOption.slug) {
case 'heroku':
window.location.assign(
`https://id.heroku.com/oauth/authorize?client_id=${integrationOption.clientId}&response_type=code&scope=write-protected&state=${state}`
);
break;
case 'vercel':
window.location.assign(
`https://vercel.com/integrations/${integrationOption.clientSlug}/new?state=${state}`
);
break;
case 'netlify':
window.location.assign(
`https://app.netlify.com/authorize?client_id=${integrationOption.clientId}&response_type=code&state=${state}&redirect_uri=${window.location.origin}/netlify`
);
break;
case 'github':
window.location.assign(
`https://github.com/login/oauth/authorize?client_id=${integrationOption.clientId}&response_type=code&scope=repo&redirect_uri=${window.location.origin}/github&state=${state}`
);
break;
default:
break;
}
return;
} if (integrationOption.type === 'pat') {
// integration is of type personal access token
const integrationAuth = await saveIntegrationAccessToken({
workspaceId: localStorage.getItem('projectData.id'),
integration: integrationOption.slug,
accessToken: accessToken ?? ''
});
setIntegrationAuths([...integrationAuths, integrationAuth])
const integration = await createIntegration({
integrationAuthId: integrationAuth._id
});
setIntegrations([...integrations, integration]);
return;
} }
} catch (err) { } catch (err) {
console.error(err); console.error(err);
} }
}; }
const handleAuthorizedIntegrationOptionPress = (integrationAuth: IntegrationAuth) => {
try {
let link = '';
switch (integrationAuth.integration) {
case 'azure-key-vault':
link = `${window.location.origin}/integrations/azure-key-vault/create?integrationAuthId=${integrationAuth._id}`;
break;
case 'heroku':
link = `${window.location.origin}/integrations/heroku/create?integrationAuthId=${integrationAuth._id}`;
break;
case 'vercel':
link = `${window.location.origin}/integrations/vercel/create?integrationAuthId=${integrationAuth._id}`;
break;
case 'netlify':
link = `${window.location.origin}/integrations/netlify/create?integrationAuthId=${integrationAuth._id}`;
break;
case 'github':
link = `${window.location.origin}/integrations/github/create?integrationAuthId=${integrationAuth._id}`;
break;
case 'render':
link = `${window.location.origin}/integrations/render/create?integrationAuthId=${integrationAuth._id}`;
break;
case 'flyio':
link = `${window.location.origin}/integrations/flyio/create?integrationAuthId=${integrationAuth._id}`;
break;
default:
break;
}
if (link !== '') {
window.location.assign(link);
}
} catch (err) {
console.error(err);
}
}
/** /**
* Open dialog to activate bot if bot is not active. * Open dialog to activate bot if bot is not active.
@@ -251,34 +251,20 @@ export default function Integrations() {
* @returns * @returns
*/ */
const integrationOptionPress = async (integrationOption: IntegrationOption) => { const integrationOptionPress = async (integrationOption: IntegrationOption) => {
// consider: don't start integration until at [handleIntegrationOption] step
try { try {
const integrationAuthX = integrationAuths.find((integrationAuth) => integrationAuth.integration === integrationOption.slug); const integrationAuthX = integrationAuths.find((integrationAuth) => integrationAuth.integration === integrationOption.slug);
if (!integrationAuthX) {
// case: integration has not been authorized before
if (integrationOption.type === 'pat') {
// case: integration requires user to input their personal access token for that integration
setIntegrationAccessTokenDialogOpen(true);
return;
}
// case: integration does not require user to input their personal access token (i.e. it's an OAuth2 integration)
handleIntegrationOption({ integrationOption });
return;
}
if (!bot.isActive) { if (!bot.isActive) {
await handleBotActivate(); await handleBotActivate();
} }
// case: integration has been authorized before if (!integrationAuthX) {
// -> create new integration // case: integration has not been authorized
const integration = await createIntegration({ handleUnauthorizedIntegrationOptionPress(integrationOption);
integrationAuthId: integrationAuthX._id return;
}); }
setIntegrations([...integrations, integration]);
handleAuthorizedIntegrationOptionPress(integrationAuthX);
} catch (err) { } catch (err) {
console.error(err); console.error(err);
} }
@@ -360,12 +346,6 @@ export default function Integrations() {
selectedIntegrationOption={selectedIntegrationOption} selectedIntegrationOption={selectedIntegrationOption}
integrationOptionPress={integrationOptionPress} integrationOptionPress={integrationOptionPress}
/> />
<IntegrationAccessTokenDialog
isOpen={isIntegrationAccessTokenDialogOpen}
closeModal={() => setIntegrationAccessTokenDialogOpen(false)}
selectedIntegrationOption={selectedIntegrationOption}
handleIntegrationOption={handleIntegrationOption}
/>
<IntegrationSection <IntegrationSection
integrations={integrations} integrations={integrations}
setIntegrations={setIntegrations} setIntegrations={setIntegrations}
@@ -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,
Input,
Select,
SelectItem
} from '../../../components/v2';
import { useGetIntegrationAuthById } from '../../../hooks/api/integrationAuth';
import { useGetWorkspaceById } from '../../../hooks/api/workspace';
import createIntegration from "../../api/integrations/createIntegration";
export default function AzureKeyVaultCreateIntegrationPage() {
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 [selectedSourceEnvironment, setSelectedSourceEnvironment] = useState('');
const [vaultBaseUrl, setVaultBaseUrl] = useState('');
const [vaultBaseUrlErrorText, setVaultBaseUrlErrorText] = useState('');
const [isLoading, setIsLoading] = useState(false);
useEffect(() => {
if (workspace) {
setSelectedSourceEnvironment(workspace.environments[0].slug);
}
}, [workspace]);
const handleButtonClick = async () => {
try {
if (vaultBaseUrl.length === 0) {
setVaultBaseUrlErrorText('Vault URI cannot be blank');
return;
}
if (
!vaultBaseUrl.startsWith('https://')
|| !vaultBaseUrl.endsWith('vault.azure.net')
) {
setVaultBaseUrlErrorText('Vault URI must be like https://<vault_name>.vault.azure.net');
return;
}
if (!integrationAuth?._id) return;
setIsLoading(true);
await createIntegration({
integrationAuthId: integrationAuth?._id,
isActive: true,
app: vaultBaseUrl,
appId: null,
sourceEnvironment: selectedSourceEnvironment,
targetEnvironment: null,
owner: null
});
setIsLoading(false);
router.push(
`/integrations/${localStorage.getItem('projectData.id')}`
);
} catch (err) {
console.error(err);
}
}
return (integrationAuth && workspace && selectedSourceEnvironment) ? (
<div className="h-full w-full flex justify-center items-center">
<Card className="max-w-md p-8 rounded-md">
<CardTitle className='text-center'>Azure Key Vault 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="Vault URI"
errorText={vaultBaseUrlErrorText}
isError={vaultBaseUrlErrorText !== '' ?? false}
>
<Input
placeholder='https://example.vault.azure.net'
value={vaultBaseUrl}
onChange={(e) => setVaultBaseUrl(e.target.value)}
/>
</FormControl>
<Button
onClick={handleButtonClick}
color="mineshaft"
className='mt-4'
isLoading={isLoading}
>
Create Integration
</Button>
</Card>
</div>
) : <div />
}
AzureKeyVaultCreateIntegrationPage.requireAuth = true;
export const getServerSideProps = getTranslatedServerSideProps(['integrations']);
@@ -0,0 +1,41 @@
import { useEffect } from 'react';
import { useRouter } from 'next/router';
import queryString from 'query-string';
import { getTranslatedServerSideProps } from '../../../../components/utilities/withTranslateProps';
import AuthorizeIntegration from "../../../api/integrations/authorizeIntegration";
export default function AzureKeyVaultOAuth2CallbackPage() {
const router = useRouter();
const { code, state } = queryString.parse(router.asPath.split('?')[1]);
useEffect(() => {
(async () => {
try {
// validate state
if (state !== localStorage.getItem('latestCSRFToken')) return;
localStorage.removeItem('latestCSRFToken');
const integrationAuth = await AuthorizeIntegration({
workspaceId: localStorage.getItem('projectData.id') as string,
code: code as string,
integration: 'azure-key-vault'
});
router.push(
`/integrations/azure-key-vault/create?integrationAuthId=${integrationAuth._id}`
);
} catch (err) {
console.error(err);
}
})();
}, []);
return <div />
}
AzureKeyVaultOAuth2CallbackPage.requireAuth = true;
export const getServerSideProps = getTranslatedServerSideProps(['integrations']);
@@ -0,0 +1,76 @@
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 FlyioCreateIntegrationPage() {
const router = useRouter();
const [accessToken, setAccessToken] = useState('');
const [accessTokenErrorText, setAccessTokenErrorText] = useState('');
const [isLoading, setIsLoading] = useState(false);
const handleButtonClick = async () => {
try {
setAccessTokenErrorText('');
if (accessToken.length === 0) {
setAccessTokenErrorText('Access token cannot be blank');
return;
}
setIsLoading(true);
const integrationAuth = await saveIntegrationAccessToken({
workspaceId: localStorage.getItem('projectData.id'),
integration: 'flyio',
accessToken
});
setIsLoading(false);
router.push(
`/integrations/flyio/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'>Render Integration</CardTitle>
<FormControl
label="Fly.io Access Token"
errorText={accessTokenErrorText}
isError={accessTokenErrorText !== '' ?? false}
>
<Input
placeholder=''
value={accessToken}
onChange={(e) => setAccessToken(e.target.value)}
/>
</FormControl>
<Button
onClick={handleButtonClick}
color="mineshaft"
className='mt-4'
isLoading={isLoading}
>
Connect to Fly.io
</Button>
</Card>
</div>
)
}
FlyioCreateIntegrationPage.requireAuth = true;
export const getServerSideProps = getTranslatedServerSideProps(['integrations']);
@@ -0,0 +1,122 @@
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 FlyioCreateIntegrationPage() {
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: null,
sourceEnvironment: selectedSourceEnvironment,
targetEnvironment: null,
owner: 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'>Fly.io 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={`flyio-environment-${sourceEnvironment.slug}`}>
{sourceEnvironment.name}
</SelectItem>
))}
</Select>
</FormControl>
<FormControl
label="Fly.io App"
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 />
}
FlyioCreateIntegrationPage.requireAuth = true;
export const getServerSideProps = getTranslatedServerSideProps(['integrations']);
@@ -0,0 +1,123 @@
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 GitHubCreateIntegrationPage() {
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 [owner, setOwner] = useState<string | null>(null);
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);
setOwner(integrationAuthApps[0]?.owner ?? null);
}
}, [integrationAuthApps]);
const handleButtonClick = async () => {
try {
setIsLoading(true);
if (!integrationAuth?._id) return;
await createIntegration({
integrationAuthId: integrationAuth?._id,
isActive: true,
app: targetApp,
appId: null,
sourceEnvironment: selectedSourceEnvironment,
targetEnvironment: null,
owner
});
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'>GitHub 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="GitHub Repo"
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={`github-environment-${integrationAuthApp.name}`}>
{integrationAuthApp.name}
</SelectItem>
))}
</Select>
</FormControl>
<Button
onClick={handleButtonClick}
color="mineshaft"
className='mt-4'
isLoading={isLoading}
>
Create Integration
</Button>
</Card>
</div>
) : <div />
}
GitHubCreateIntegrationPage.requireAuth = true;
export const getServerSideProps = getTranslatedServerSideProps(['integrations']);
@@ -0,0 +1,40 @@
import { useEffect } from 'react';
import { useRouter } from 'next/router';
import queryString from 'query-string';
import { getTranslatedServerSideProps } from '../../../../components/utilities/withTranslateProps';
import AuthorizeIntegration from "../../../api/integrations/authorizeIntegration";
export default function GitHubOAuth2CallbackPage() {
const router = useRouter();
const { code, state } = queryString.parse(router.asPath.split('?')[1]);
useEffect(() => {
(async () => {
try {
// validate state
if (state !== localStorage.getItem('latestCSRFToken')) return;
localStorage.removeItem('latestCSRFToken');
const integrationAuth = await AuthorizeIntegration({
workspaceId: localStorage.getItem('projectData.id') as string,
code: code as string,
integration: 'github'
});
router.push(
`/integrations/github/create?integrationAuthId=${integrationAuth._id}`
);
} catch (err) {
console.error(err);
}
})();
}, []);
return <div />
}
GitHubOAuth2CallbackPage.requireAuth = true;
export const getServerSideProps = getTranslatedServerSideProps(['integrations']);
@@ -0,0 +1,121 @@
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 HerokuCreateIntegrationPage() {
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 {
setIsLoading(true);
if (!integrationAuth?._id) return;
await createIntegration({
integrationAuthId: integrationAuth?._id,
isActive: true,
app: targetApp,
appId: null,
sourceEnvironment: selectedSourceEnvironment,
targetEnvironment: null,
owner: 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'>Heroku 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="Heroku App"
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={`heroku-environment-${integrationAuthApp.name}`}>
{integrationAuthApp.name}
</SelectItem>
))}
</Select>
</FormControl>
<Button
onClick={handleButtonClick}
color="mineshaft"
className='mt-4'
isLoading={isLoading}
>
Create Integration
</Button>
</Card>
</div>
) : <div />
}
HerokuCreateIntegrationPage.requireAuth = true;
export const getServerSideProps = getTranslatedServerSideProps(['integrations']);
@@ -0,0 +1,40 @@
import { useEffect } from 'react';
import { useRouter } from 'next/router';
import queryString from 'query-string';
import { getTranslatedServerSideProps } from '../../../../components/utilities/withTranslateProps';
import AuthorizeIntegration from "../../../api/integrations/authorizeIntegration";
export default function HerokuOAuth2CallbackPage() {
const router = useRouter();
const { code, state } = queryString.parse(router.asPath.split('?')[1]);
useEffect(() => {
(async () => {
try {
// validate state
if (state !== localStorage.getItem('latestCSRFToken')) return;
localStorage.removeItem('latestCSRFToken');
const integrationAuth = await AuthorizeIntegration({
workspaceId: localStorage.getItem('projectData.id') as string,
code: code as string,
integration: 'heroku'
});
router.push(
`/integrations/heroku/create?integrationAuthId=${integrationAuth._id}`
);
} catch (err) {
console.error(err);
}
})();
}, []);
return <div />
}
HerokuOAuth2CallbackPage.requireAuth = true;
export const getServerSideProps = getTranslatedServerSideProps(['integrations']);
@@ -0,0 +1,144 @@
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";
const netlifyEnvironments = [
{ name: 'Local development', slug: 'dev' },
{ name: 'Branch deploys', slug: 'branch-deploy' },
{ name: 'Deploy previews', slug: 'deploy-preview' },
{ name: 'Production', slug: 'production' }
]
export default function NetlifyCreateIntegrationPage() {
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 [targetEnvironment, setTargetEnvironment] = useState('');
const [isLoading, setIsLoading] = useState(false);
useEffect(() => {
if (workspace) {
setSelectedSourceEnvironment(workspace.environments[0].slug);
setTargetEnvironment(netlifyEnvironments[0].slug);
}
}, [workspace]);
useEffect(() => {
// TODO: handle case where apps can be empty
if (integrationAuthApps) {
console.log(integrationAuthApps)
setTargetApp(integrationAuthApps[0].name);
}
}, [integrationAuthApps]);
const handleButtonClick = async () => {
try {
setIsLoading(true);
if (!integrationAuth?._id) return;
await createIntegration({
integrationAuthId: integrationAuth?._id,
isActive: true,
app: targetApp,
appId: (integrationAuthApps?.find((integrationAuthApp) => integrationAuthApp.name === targetApp))?.appId ?? null,
sourceEnvironment: selectedSourceEnvironment,
targetEnvironment,
owner: null
});
setIsLoading(false);
router.push(
`/integrations/${localStorage.getItem('projectData.id')}`
);
} catch (err) {
console.error(err);
}
}
return (integrationAuth && workspace && selectedSourceEnvironment && integrationAuthApps && targetApp && targetEnvironment) ? (
<div className="h-full w-full flex justify-center items-center">
<Card className="max-w-md p-8 rounded-md">
<CardTitle className='text-center'>Netlify Integration</CardTitle>
<FormControl
label="Project Environment"
>
<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="Netlify Site"
>
<Select
value={targetApp}
onValueChange={(val) => setTargetApp(val)}
className='w-full border border-mineshaft-500'
>
{integrationAuthApps.map((integrationAuthApp) => (
<SelectItem value={integrationAuthApp.name} key={`heroku-environment-${integrationAuthApp.name}`}>
{integrationAuthApp.name}
</SelectItem>
))}
</Select>
</FormControl>
<FormControl
label="Netlify Context"
>
<Select
value={targetEnvironment}
onValueChange={(val) => setTargetEnvironment(val)}
className='w-full border border-mineshaft-500'
>
{netlifyEnvironments.map((netlifyEnvironment) => (
<SelectItem value={netlifyEnvironment.slug} key={`netlify-environment-${netlifyEnvironment.slug}`}>
{netlifyEnvironment.name}
</SelectItem>
))}
</Select>
</FormControl>
<Button
onClick={handleButtonClick}
color="mineshaft"
className='mt-4'
isLoading={isLoading}
>
Create Integration
</Button>
</Card>
</div>
) : <div />
}
NetlifyCreateIntegrationPage.requireAuth = true;
export const getServerSideProps = getTranslatedServerSideProps(['integrations']);
@@ -0,0 +1,41 @@
import { useEffect } from 'react';
import { useRouter } from 'next/router';
import queryString from 'query-string';
import { getTranslatedServerSideProps } from '../../../../components/utilities/withTranslateProps';
import AuthorizeIntegration from "../../../api/integrations/authorizeIntegration";
export default function NetlifyOAuth2CallbackPage() {
const router = useRouter();
const { code, state } = queryString.parse(router.asPath.split('?')[1]);
useEffect(() => {
(async () => {
try {
// validate state
if (state !== localStorage.getItem('latestCSRFToken')) return;
localStorage.removeItem('latestCSRFToken');
const integrationAuth = await AuthorizeIntegration({
workspaceId: localStorage.getItem('projectData.id') as string,
code: code as string,
integration: 'netlify'
});
router.push(
`/integrations/netlify/create?integrationAuthId=${integrationAuth._id}`
);
} catch (err) {
console.error(err);
}
})();
}, []);
return <div />
}
NetlifyOAuth2CallbackPage.requireAuth = true;
export const getServerSideProps = getTranslatedServerSideProps(['integrations']);
@@ -0,0 +1,76 @@
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 RenderCreateIntegrationPage() {
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: 'render',
accessToken: apiKey
});
setIsLoading(false);
router.push(
`/integrations/render/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'>Render Integration</CardTitle>
<FormControl
label="Render API Key"
errorText={apiKeyErrorText}
isError={apiKeyErrorText !== '' ?? false}
>
<Input
placeholder='rnd_xxx'
value={apiKey}
onChange={(e) => setApiKey(e.target.value)}
/>
</FormControl>
<Button
onClick={handleButtonClick}
color="mineshaft"
className='mt-4'
isLoading={isLoading}
>
Connect to Render
</Button>
</Card>
</div>
)
}
RenderCreateIntegrationPage.requireAuth = true;
export const getServerSideProps = getTranslatedServerSideProps(['integrations']);
@@ -0,0 +1,122 @@
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 RenderCreateIntegrationPage() {
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: null,
sourceEnvironment: selectedSourceEnvironment,
targetEnvironment: null,
owner: 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'>Render 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="Render 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 />
}
RenderCreateIntegrationPage.requireAuth = true;
export const getServerSideProps = getTranslatedServerSideProps(['integrations']);
@@ -0,0 +1,142 @@
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";
const vercelEnvironments = [
{ name: 'Development', slug: 'development' },
{ name: 'Preview', slug: 'preview' },
{ name: 'Production', slug: 'production' }
]
export default function VercelCreateIntegrationPage() {
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 [targetEnvironment, setTargetEnvironemnt] = 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);
setTargetEnvironemnt(vercelEnvironments[0].slug);
}
}, [integrationAuthApps]);
const handleButtonClick = async () => {
try {
if (!integrationAuth?._id) return;
setIsLoading(true);
await createIntegration({
integrationAuthId: integrationAuth?._id,
isActive: true,
app: targetApp,
appId: null,
sourceEnvironment: selectedSourceEnvironment,
targetEnvironment,
owner: null
});
setIsLoading(false);
router.push(
`/integrations/${localStorage.getItem('projectData.id')}`
);
} catch (err) {
console.error(err);
}
}
return (integrationAuth && workspace && selectedSourceEnvironment && integrationAuthApps && targetApp && targetEnvironment) ? (
<div className="h-full w-full flex justify-center items-center">
<Card className="max-w-md p-8 rounded-md">
<CardTitle className='text-center'>Vercel 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="Vercel App"
>
<Select
value={targetApp}
onValueChange={(val) => setTargetApp(val)}
className='w-full border border-mineshaft-500'
>
{integrationAuthApps.map((integrationAuthApp) => (
<SelectItem value={integrationAuthApp.name} key={`heroku-environment-${integrationAuthApp.name}`}>
{integrationAuthApp.name}
</SelectItem>
))}
</Select>
</FormControl>
<FormControl
label="Vercel App Environment"
>
<Select
value={targetEnvironment}
onValueChange={(val) => setTargetEnvironemnt(val)}
className='w-full border border-mineshaft-500'
>
{vercelEnvironments.map((vercelEnvironment) => (
<SelectItem value={vercelEnvironment.slug} key={`vercel-environment-${vercelEnvironment.slug}`}>
{vercelEnvironment.name}
</SelectItem>
))}
</Select>
</FormControl>
<Button
onClick={handleButtonClick}
color="mineshaft"
className='mt-4'
isLoading={isLoading}
>
Create Integration
</Button>
</Card>
</div>
) : <div />
}
VercelCreateIntegrationPage.requireAuth = true;
export const getServerSideProps = getTranslatedServerSideProps(['integrations']);
@@ -0,0 +1,41 @@
import { useEffect } from 'react';
import { useRouter } from 'next/router';
import queryString from 'query-string';
import { getTranslatedServerSideProps } from '../../../../components/utilities/withTranslateProps';
import AuthorizeIntegration from "../../../api/integrations/authorizeIntegration";
export default function VercelOAuth2CallbackPage() {
const router = useRouter();
const { code, state } = queryString.parse(router.asPath.split('?')[1]);
useEffect(() => {
(async () => {
try {
// validate state
if (state !== localStorage.getItem('latestCSRFToken')) return;
localStorage.removeItem('latestCSRFToken');
const integrationAuth = await AuthorizeIntegration({
workspaceId: localStorage.getItem('projectData.id') as string,
code: code as string,
integration: 'vercel'
});
router.push(
`/integrations/vercel/create?integrationAuthId=${integrationAuth._id}`
);
} catch (err) {
console.error(err);
}
})();
}, []);
return <div />
}
VercelOAuth2CallbackPage.requireAuth = true;
export const getServerSideProps = getTranslatedServerSideProps(['integrations']);
-46
View File
@@ -1,46 +0,0 @@
import { useEffect } from 'react';
import { useRouter } from 'next/router';
import queryString from 'query-string';
import AuthorizeIntegration from './api/integrations/authorizeIntegration';
export default function Netlify() {
const router = useRouter();
const parsedUrl = queryString.parse(router.asPath.split('?')[1]);
const {code} = parsedUrl;
const {state} = parsedUrl;
// modify comment here
/**
* Here we forward to the default workspace if a user opens this url
*/
// eslint-disable-next-line react-hooks/exhaustive-deps
useEffect(() => {
(async () => {
try {
if (!code) throw new Error('Code not found');
if (state === localStorage.getItem('latestCSRFToken')) {
localStorage.removeItem('latestCSRFToken');
await AuthorizeIntegration({
workspaceId: localStorage.getItem('projectData.id') as string,
code: code as string,
integration: 'netlify',
});
router.push(
`/integrations/${ localStorage.getItem('projectData.id')}`
);
}
} catch (err) {
console.error('Netlify integration error: ', err);
}
})();
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
return <div />;
}
Netlify.requireAuth = true;
-46
View File
@@ -1,46 +0,0 @@
import { useEffect } from 'react';
import { useRouter } from 'next/router';
import queryString from 'query-string';
import AuthorizeIntegration from './api/integrations/authorizeIntegration';
export default function Vercel() {
const router = useRouter();
const parsedUrl = queryString.parse(router.asPath.split('?')[1]);
const {code} = parsedUrl;
const {state} = parsedUrl;
/**
* Here we forward to the default workspace if a user opens this url
*/
// eslint-disable-next-line react-hooks/exhaustive-deps
useEffect(() => {
(async () => {
try {
// type check
if (!code) throw new Error('Code not found');
if (state === localStorage.getItem('latestCSRFToken')) {
localStorage.removeItem('latestCSRFToken');
await AuthorizeIntegration({
workspaceId: localStorage.getItem('projectData.id') as string,
code: code as string,
integration: 'vercel',
});
router.push(
`/integrations/${ localStorage.getItem('projectData.id')}`
);
}
} catch (err) {
console.error('Vercel integration error: ', err);
}
})();
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
return <div />;
}
Vercel.requireAuth = true;
@@ -18,19 +18,24 @@ import { useToggle } from '@app/hooks';
import { import {
useCreateServiceToken, useCreateServiceToken,
useCreateWsEnvironment, useCreateWsEnvironment,
useCreateWsTag,
useDeleteServiceToken, useDeleteServiceToken,
useDeleteWorkspace, useDeleteWorkspace,
useDeleteWsEnvironment, useDeleteWsEnvironment,
useDeleteWsTag,
useGetUserWsKey, useGetUserWsKey,
useGetUserWsServiceTokens, useGetUserWsServiceTokens,
useGetWsTags,
useRenameWorkspace, useRenameWorkspace,
useUpdateWsEnvironment useUpdateWsEnvironment
} from '@app/hooks/api'; } from '@app/hooks/api';
import { SecretTagsSection } from './components/SecretTagsSection';
import { import {
CopyProjectIDSection, CopyProjectIDSection,
CreateServiceToken, CreateServiceToken,
CreateUpdateEnvFormData, CreateUpdateEnvFormData,
CreateWsTag,
EnvironmentSection, EnvironmentSection,
ProjectNameChangeSection, ProjectNameChangeSection,
ServiceTokenSection ServiceTokenSection
@@ -61,6 +66,11 @@ export const ProjectSettingsPage = () => {
const createServiceToken = useCreateServiceToken(); const createServiceToken = useCreateServiceToken();
const deleteServiceToken = useDeleteServiceToken(); const deleteServiceToken = useDeleteServiceToken();
// tag
const { data: wsTags } = useGetWsTags(workspaceID);
const createWsTag = useCreateWsTag();
const deleteWsTag = useDeleteWsTag();
// get user subscription // get user subscription
const { subscriptionPlan } = useSubscription(); const { subscriptionPlan } = useSubscription();
const host = window.location.origin; const host = window.location.origin;
@@ -207,6 +217,44 @@ export const ProjectSettingsPage = () => {
return ''; return '';
}; };
const onCreateWsTag = async ({ name }: CreateWsTag) => {
try {
const res = await createWsTag.mutateAsync({
workspaceID,
tagName: name,
tagSlug: name.replace(" ", "_")
});
createNotification({
text: 'Successfully created a tag',
type: 'success'
});
return res.name;
} catch (error) {
console.error(error);
createNotification({
text: 'Failed to create a tag',
type: 'error'
});
}
return '';
};
const onDeleteTag = async (tagID: string) => {
try {
await deleteWsTag.mutateAsync({ tagID });
createNotification({
text: 'Successfully deleted tag',
type: 'success'
});
} catch (error) {
console.error(error);
createNotification({
text: 'Failed to delete the tag',
type: 'error'
});
}
};
const onDeleteServiceToken = async (tokenID: string) => { const onDeleteServiceToken = async (tokenID: string) => {
try { try {
await deleteServiceToken.mutateAsync(tokenID); await deleteServiceToken.mutateAsync(tokenID);
@@ -224,7 +272,7 @@ export const ProjectSettingsPage = () => {
}; };
return ( return (
<div className="container mx-auto flex flex-col px-8 text-mineshaft-50"> <div className="container mx-auto flex flex-col px-8 text-mineshaft-50 dark dark:[color-scheme:dark]">
{/* TODO(akhilmhdh): Remove this right when layout is refactored */} {/* TODO(akhilmhdh): Remove this right when layout is refactored */}
<div className="relative right-5"> <div className="relative right-5">
<NavHeader pageName={t('settings-project:title')} isProjectRelated /> <NavHeader pageName={t('settings-project:title')} isProjectRelated />
@@ -256,6 +304,12 @@ export const ProjectSettingsPage = () => {
workspaceName={currentWorkspace?.name || ''} workspaceName={currentWorkspace?.name || ''}
onCreateToken={onCreateServiceToken} onCreateToken={onCreateServiceToken}
/> />
<SecretTagsSection
tags={wsTags || []}
onDeleteTag={onDeleteTag}
workspaceName={currentWorkspace?.name || ''}
onCreateTag={onCreateWsTag}
/>
<div className="mb-6 mt-4 flex w-full flex-col items-start rounded-md border-l border-red bg-white/5 px-6 pl-6 pb-4 pt-4"> <div className="mb-6 mt-4 flex w-full flex-col items-start rounded-md border-l border-red bg-white/5 px-6 pl-6 pb-4 pt-4">
<p className="text-xl font-bold text-red">{t('settings-project:danger-zone')}</p> <p className="text-xl font-bold text-red">{t('settings-project:danger-zone')}</p>
<p className="text-md mt-2 text-gray-400">{t('settings-project:danger-zone-note')}</p> <p className="text-md mt-2 text-gray-400">{t('settings-project:danger-zone-note')}</p>
@@ -102,7 +102,7 @@ export const EnvironmentSection = ({
colorSchema="primary" colorSchema="primary"
leftIcon={<FontAwesomeIcon icon={faPlus} />} leftIcon={<FontAwesomeIcon icon={faPlus} />}
> >
Add New Env Add New Environment
</Button> </Button>
</div> </div>
</div> </div>
@@ -0,0 +1,186 @@
import { Controller, useForm } from 'react-hook-form';
import { faPlus, faTrashCan } from '@fortawesome/free-solid-svg-icons';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { yupResolver } from '@hookform/resolvers/yup';
import * as yup from 'yup';
import {
Button,
DeleteActionModal,
FormControl,
IconButton,
Input,
Modal,
ModalClose,
ModalContent,
ModalTrigger,
Table,
TableContainer,
TBody,
Td,
Th,
THead,
Tr,
} from '@app/components/v2';
import { usePopUp } from '@app/hooks';
import { WorkspaceTag } from '@app/hooks/api/types';
const createTagSchema = yup.object({
name: yup.string().required().label('Tag Name'),
});
export type CreateWsTag = yup.InferType<typeof createTagSchema>;
type Props = {
tags: WorkspaceTag[];
workspaceName: string;
onDeleteTag: (tagID: string) => Promise<void>;
onCreateTag: (data: CreateWsTag) => Promise<string>;
};
type DeleteModalData = { name: string; id: string };
export const SecretTagsSection = ({
tags = [],
onDeleteTag,
workspaceName,
onCreateTag
}: Props): JSX.Element => {
const { popUp, handlePopUpToggle, handlePopUpClose, handlePopUpOpen } = usePopUp([
'CreateSecretTag',
'deleteTagConfirmation'
] as const);
const {
control,
reset,
handleSubmit,
formState: { isSubmitting }
} = useForm<CreateWsTag>({
resolver: yupResolver(createTagSchema)
});
const onFormSubmit = async (data: CreateWsTag) => {
console.log(19191, data);
await onCreateTag(data);
handlePopUpClose('CreateSecretTag');
};
const onDeleteApproved = async () => {
await onDeleteTag((popUp?.deleteTagConfirmation?.data as DeleteModalData)?.id);
handlePopUpClose('deleteTagConfirmation');
};
return (
<div className="mt-4 mb-4 flex w-full flex-col items-start rounded-md bg-white/5 p-6">
<div className="flex w-full flex-row justify-between">
<div className="flex w-full flex-col">
<p className="mb-3 text-xl font-semibold">Secret Tags</p>
<p className="text-sm text-gray-400">Every secret can be assigned to one or more tags. Here you can add and remove tags for the current project.</p>
</div>
<div>
<Modal
isOpen={popUp?.CreateSecretTag?.isOpen}
onOpenChange={(open) => {
handlePopUpToggle('CreateSecretTag', open);
reset();
}}
>
<ModalTrigger asChild>
<Button color="mineshaft" leftIcon={<FontAwesomeIcon icon={faPlus} />}>
Add New Tag
</Button>
</ModalTrigger>
<ModalContent
title={`Add a tag for ${ workspaceName}`}
subTitle='Specify your tag name, and the slug will be created automatically.'
>
<form onSubmit={handleSubmit(onFormSubmit)}>
<Controller
control={control}
name="name"
defaultValue=""
render={({ field, fieldState: { error } }) => (
<FormControl
label='Tag Name'
isError={Boolean(error)}
errorText={error?.message}
>
<Input {...field} placeholder="Type your tag name" />
</FormControl>
)}
/>
<div className="mt-8 flex items-center">
<Button
className="mr-4"
type="submit"
isDisabled={isSubmitting}
isLoading={isSubmitting}
>
Create
</Button>
<ModalClose asChild>
<Button variant="plain" colorSchema="secondary">
Cancel
</Button>
</ModalClose>
</div>
</form>
</ModalContent>
</Modal>
</div>
</div>
<TableContainer className='mt-4'>
<Table>
<THead>
<Tr>
<Th>Tag</Th>
<Th>Slug</Th>
<Th aria-label="button" />
</Tr>
</THead>
<TBody>
{tags?.length > 0 ? (
tags.map(({ _id, name, slug }) => (
<Tr key={name}>
<Td>{name}</Td>
<Td>{slug}</Td>
<Td className="flex items-center justify-end">
<IconButton
onClick={() =>
handlePopUpOpen('deleteTagConfirmation', {
name,
id: _id
})
}
colorSchema="danger"
ariaLabel="update"
>
<FontAwesomeIcon icon={faTrashCan} />
</IconButton>
</Td>
</Tr>
))
) : (
<Tr>
<Td colSpan={4} className="py-6 text-center text-bunker-400">
No tags found for this project
</Td>
</Tr>
)}
</TBody>
</Table>
</TableContainer>
<DeleteActionModal
isOpen={popUp.deleteTagConfirmation.isOpen}
title={`Delete ${
(popUp?.deleteTagConfirmation?.data as DeleteModalData)?.name || ' '
} api key?`}
onChange={(isOpen) => handlePopUpToggle('deleteTagConfirmation', isOpen)}
deleteKey={(popUp?.deleteTagConfirmation?.data as DeleteModalData)?.name}
onClose={() => handlePopUpClose('deleteTagConfirmation')}
onDeleteApproved={onDeleteApproved}
/>
</div>
);
};
@@ -0,0 +1 @@
export {SecretTagsSection} from './SecretTagsSection'
@@ -2,5 +2,6 @@ export { CopyProjectIDSection } from './CopyProjectIDSection';
export { EnvironmentSection } from './EnvironmentSection'; export { EnvironmentSection } from './EnvironmentSection';
export type { CreateUpdateEnvFormData } from './EnvironmentSection/EnvironmentSection'; export type { CreateUpdateEnvFormData } from './EnvironmentSection/EnvironmentSection';
export { ProjectNameChangeSection } from './ProjectNameChangeSection'; export { ProjectNameChangeSection } from './ProjectNameChangeSection';
export type { CreateWsTag } from './SecretTagsSection/SecretTagsSection';
export { ServiceTokenSection } from './ServiceTokenSection'; export { ServiceTokenSection } from './ServiceTokenSection';
export type { CreateServiceToken } from './ServiceTokenSection/ServiceTokenSection'; export type { CreateServiceToken } from './ServiceTokenSection/ServiceTokenSection';