diff --git a/README.md b/README.md
index 1ec8cff50..64d643e7f 100644
--- a/README.md
+++ b/README.md
@@ -171,7 +171,9 @@ We're currently setting the foundation and building [integrations](https://infis
🔜 GCP SM (https://github.com/Infisical/infisical/issues/285)
- 🔜 GitLab CI/CD (https://github.com/Infisical/infisical/issues/134)
+
+ ✔️ GitLab CI/CD
+
🔜 CircleCI (https://github.com/Infisical/infisical/issues/91)
diff --git a/backend/package-lock.json b/backend/package-lock.json
index 6703e198f..189b1eccd 100644
--- a/backend/package-lock.json
+++ b/backend/package-lock.json
@@ -10,13 +10,13 @@
"license": "ISC",
"dependencies": {
"@godaddy/terminus": "^4.11.2",
- "@sentry/node": "^7.21.1",
"@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",
- "axios": "^1.2.0",
"@types/libsodium-wrappers": "^0.7.10",
"await-to-js": "^3.0.0",
+ "axios": "^1.1.3",
"bcrypt": "^5.1.0",
"bigint-conversion": "^2.2.2",
"builder-pattern": "^2.2.0",
@@ -32,9 +32,9 @@
"js-yaml": "^4.1.0",
"jsonwebtoken": "^9.0.0",
"jsrp": "^0.2.4",
- "mongoose": "^6.7.3",
"libsodium-wrappers": "^0.7.10",
"lodash": "^4.17.21",
+ "mongoose": "^6.7.2",
"nodemailer": "^6.8.0",
"posthog-node": "^2.2.2",
"query-string": "^7.1.3",
@@ -2838,19 +2838,6 @@
"@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": {
"version": "1.1.2",
"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",
"integrity": "sha512-Vvn3zZrhQZkkBE8LSuW3em98c0FwgO4nxzv6OdSxPKJIEKY2bGbHn+mhGIPerzI4twdxaP8/0+06HBpwf345Lw=="
},
- "node_modules/@sentry/node": {
- "version": "7.19.0",
- "resolved": "https://registry.npmjs.org/@sentry/node/-/node-7.19.0.tgz",
- "integrity": "sha512-yG7Tx32WqOkEHVotFLrumCcT9qlaSDTkFNZ+yLSvZXx74ifsE781DzBA9W7K7bBdYO3op+p2YdsOKzf3nPpAyQ==",
- "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==",
+ "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",
@@ -2986,26 +2956,6 @@
"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": {
"version": "0.24.51",
"resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.24.51.tgz",
@@ -14306,16 +14256,6 @@
"@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": {
"version": "1.1.2",
"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",
"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": {
"version": "7.21.1",
"resolved": "https://registry.npmjs.org/@sentry/node/-/node-7.21.1.tgz",
@@ -14409,20 +14359,6 @@
"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": {
"version": "0.24.51",
"resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.24.51.tgz",
diff --git a/backend/src/config/index.ts b/backend/src/config/index.ts
index 2ecd1af8c..ad1acb1a6 100644
--- a/backend/src/config/index.ts
+++ b/backend/src/config/index.ts
@@ -13,10 +13,13 @@ const MONGO_URL = process.env.MONGO_URL!;
const NODE_ENV = process.env.NODE_ENV! || 'production';
const VERBOSE_ERROR_OUTPUT = process.env.VERBOSE_ERROR_OUTPUT! === 'true' && true;
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_VERCEL = process.env.CLIENT_ID_VERCEL!;
const CLIENT_ID_NETLIFY = process.env.CLIENT_ID_NETLIFY!;
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_VERCEL = process.env.CLIENT_SECRET_VERCEL!;
const CLIENT_SECRET_NETLIFY = process.env.CLIENT_SECRET_NETLIFY!;
@@ -60,10 +63,13 @@ export {
NODE_ENV,
VERBOSE_ERROR_OUTPUT,
LOKI_HOST,
+ CLIENT_ID_AZURE,
+ TENANT_ID_AZURE,
CLIENT_ID_HEROKU,
CLIENT_ID_VERCEL,
CLIENT_ID_NETLIFY,
CLIENT_ID_GITHUB,
+ CLIENT_SECRET_AZURE,
CLIENT_SECRET_HEROKU,
CLIENT_SECRET_VERCEL,
CLIENT_SECRET_NETLIFY,
diff --git a/backend/src/controllers/v1/integrationAuthController.ts b/backend/src/controllers/v1/integrationAuthController.ts
index b030b79d6..be42647de 100644
--- a/backend/src/controllers/v1/integrationAuthController.ts
+++ b/backend/src/controllers/v1/integrationAuthController.ts
@@ -10,6 +10,31 @@ import { INTEGRATION_SET, INTEGRATION_OPTIONS } from '../../variables';
import { IntegrationService } from '../../services';
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 (
req: Request,
res: Response
@@ -31,7 +56,6 @@ export const oAuthExchange = async (
) => {
try {
const { workspaceId, code, integration } = req.body;
-
if (!INTEGRATION_SET.has(integration))
throw new Error('Failed to validate integration');
@@ -40,12 +64,16 @@ export const oAuthExchange = async (
throw new Error("Failed to get environments")
}
- await IntegrationService.handleOAuthExchange({
+ const integrationAuth = await IntegrationService.handleOAuthExchange({
workspaceId,
integration,
code,
environment: environments[0].slug,
});
+
+ return res.status(200).send({
+ integrationAuth
+ });
} catch (err) {
Sentry.setUser({ email: req.user.email });
Sentry.captureException(err);
@@ -53,10 +81,6 @@ export const oAuthExchange = async (
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;
} = 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({
workspace: new Types.ObjectId(workspaceId),
integration
@@ -91,13 +122,6 @@ export const saveIntegrationAccessToken = async (
new: 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
integrationAuth = await IntegrationService.setIntegrationAuthAccess({
diff --git a/backend/src/controllers/v1/integrationController.ts b/backend/src/controllers/v1/integrationController.ts
index 4e66ce0aa..779cb3cb7 100644
--- a/backend/src/controllers/v1/integrationController.ts
+++ b/backend/src/controllers/v1/integrationController.ts
@@ -1,4 +1,5 @@
import { Request, Response } from 'express';
+import { Types } from 'mongoose';
import * as Sentry from '@sentry/node';
import {
Integration,
@@ -18,15 +19,40 @@ import { eventPushSecrets } from '../../events';
export const createIntegration = async (req: Request, res: Response) => {
let integration;
try {
+ const {
+ integrationAuthId,
+ app,
+ appId,
+ isActive,
+ sourceEnvironment,
+ targetEnvironment,
+ owner
+ } = req.body;
+
+ // TODO: validate [sourceEnvironment] and [targetEnvironment]
+
// initialize new integration after saving integration access token
integration = await new Integration({
workspace: req.integrationAuth.workspace._id,
- isActive: false,
- app: null,
- environment: req.integrationAuth.workspace?.environments[0].slug,
+ environment: sourceEnvironment,
+ isActive,
+ app,
+ appId,
+ targetEnvironment,
+ owner,
integration: req.integrationAuth.integration,
- integrationAuth: req.integrationAuth._id
+ integrationAuth: new Types.ObjectId(integrationAuthId)
}).save();
+
+ if (integration) {
+ // trigger event - push secrets
+ EventService.handleEvent({
+ event: eventPushSecrets({
+ workspaceId: integration.workspace.toString()
+ })
+ });
+ }
+
} catch (err) {
Sentry.setUser({ email: req.user.email });
Sentry.captureException(err);
diff --git a/backend/src/controllers/v2/secretsController.ts b/backend/src/controllers/v2/secretsController.ts
index 5ac86e903..ff39791ac 100644
--- a/backend/src/controllers/v2/secretsController.ts
+++ b/backend/src/controllers/v2/secretsController.ts
@@ -103,6 +103,9 @@ export const createSecrets = async (req: Request, res: Response) => {
secretValueCiphertext: string;
secretValueIV: string;
secretValueTag: string;
+ secretCommentCiphertext: string;
+ secretCommentIV: string;
+ secretCommentTag: string;
tags: string[]
}
@@ -115,6 +118,9 @@ export const createSecrets = async (req: Request, res: Response) => {
secretValueCiphertext,
secretValueIV,
secretValueTag,
+ secretCommentCiphertext,
+ secretCommentIV,
+ secretCommentTag,
tags
}: secretsToCreateType) => {
return ({
@@ -129,6 +135,9 @@ export const createSecrets = async (req: Request, res: Response) => {
secretValueCiphertext,
secretValueIV,
secretValueTag,
+ secretCommentCiphertext,
+ secretCommentIV,
+ secretCommentTag,
tags
});
})
@@ -160,6 +169,9 @@ export const createSecrets = async (req: Request, res: Response) => {
secretValueIV,
secretValueTag,
secretValueHash,
+ secretCommentCiphertext,
+ secretCommentIV,
+ secretCommentTag,
tags
}) => ({
_id: new Types.ObjectId(),
@@ -178,6 +190,9 @@ export const createSecrets = async (req: Request, res: Response) => {
secretValueIV,
secretValueTag,
secretValueHash,
+ secretCommentCiphertext,
+ secretCommentIV,
+ secretCommentTag,
tags
}))
});
diff --git a/backend/src/controllers/v2/tagController.ts b/backend/src/controllers/v2/tagController.ts
index 250ee08a5..b737eb41a 100644
--- a/backend/src/controllers/v2/tagController.ts
+++ b/backend/src/controllers/v2/tagController.ts
@@ -52,9 +52,9 @@ export const deleteWorkspaceTag = async (req: Request, res: Response) => {
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) => {
diff --git a/backend/src/ee/controllers/v1/secretSnapshotController.ts b/backend/src/ee/controllers/v1/secretSnapshotController.ts
index 6e8605c2f..ae3af7958 100644
--- a/backend/src/ee/controllers/v1/secretSnapshotController.ts
+++ b/backend/src/ee/controllers/v1/secretSnapshotController.ts
@@ -15,7 +15,13 @@ export const getSecretSnapshot = async (req: Request, res: Response) => {
secretSnapshot = await SecretSnapshot
.findById(secretSnapshotId)
- .populate('secretVersions');
+ .populate({
+ path: 'secretVersions',
+ populate: {
+ path: 'tags',
+ model: 'Tag',
+ }
+ });
if (!secretSnapshot) throw new Error('Failed to find secret snapshot');
diff --git a/backend/src/helpers/integration.ts b/backend/src/helpers/integration.ts
index 1618e5e67..17338ee68 100644
--- a/backend/src/helpers/integration.ts
+++ b/backend/src/helpers/integration.ts
@@ -30,6 +30,7 @@ interface Update {
* @param {String} obj.workspaceId - id of workspace
* @param {String} obj.integration - name of integration
* @param {String} obj.code - code
+ * @returns {IntegrationAuth} integrationAuth - integration auth after OAuth2 code-token exchange
*/
const handleOAuthExchangeHelper = async ({
workspaceId,
@@ -42,7 +43,6 @@ const handleOAuthExchangeHelper = async ({
code: string;
environment: string;
}) => {
- let action;
let integrationAuth;
try {
const bot = await Bot.findOne({
@@ -98,21 +98,13 @@ const handleOAuthExchangeHelper = async ({
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) {
Sentry.setUser(null);
Sentry.captureException(err);
throw new Error('Failed to handle OAuth2 code-token exchange')
}
+
+ return integrationAuth;
}
/**
* Sync/push environment variables in workspace with id [workspaceId] to
diff --git a/backend/src/integrations/apps.ts b/backend/src/integrations/apps.ts
index d1252954e..3d834d876 100644
--- a/backend/src/integrations/apps.ts
+++ b/backend/src/integrations/apps.ts
@@ -3,6 +3,7 @@ import * as Sentry from '@sentry/node';
import { Octokit } from '@octokit/rest';
import { IIntegrationAuth } from '../models';
import {
+ INTEGRATION_AZURE_KEY_VAULT,
INTEGRATION_HEROKU,
INTEGRATION_VERCEL,
INTEGRATION_NETLIFY,
@@ -40,6 +41,11 @@ const getApps = async ({
let apps: App[];
try {
switch (integrationAuth.integration) {
+ case INTEGRATION_AZURE_KEY_VAULT:
+ apps = await getAppsAzureKeyVault({
+ accessToken
+ });
+ break;
case INTEGRATION_HEROKU:
apps = await getAppsHeroku({
accessToken
@@ -81,6 +87,15 @@ const getApps = async ({
return apps;
};
+const getAppsAzureKeyVault = async ({
+ accessToken
+}: {
+ accessToken: string;
+}) => {
+ // TODO
+ return [];
+}
+
/**
* Return list of apps for Heroku integration
* @param {Object} obj
@@ -247,7 +262,9 @@ const getAppsRender = async ({
const res = (
await axios.get(`${INTEGRATION_RENDER_API_URL}/v1/services`, {
headers: {
- Authorization: `Bearer ${accessToken}`
+ Authorization: `Bearer ${accessToken}`,
+ Accept: 'application/json',
+ 'Accept-Encoding': 'application/json'
}
})
).data;
@@ -257,6 +274,7 @@ const getAppsRender = async ({
name: a.service.name,
appId: a.service.id
}));
+
} catch (err) {
Sentry.setUser(null);
Sentry.captureException(err);
@@ -296,7 +314,9 @@ const getAppsFlyio = async ({
url: INTEGRATION_FLYIO_API_URL,
method: 'post',
headers: {
- 'Authorization': 'Bearer ' + accessToken
+ 'Authorization': 'Bearer ' + accessToken,
+ 'Accept': 'application/json',
+ 'Accept-Encoding': 'application/json'
},
data: {
query,
diff --git a/backend/src/integrations/exchange.ts b/backend/src/integrations/exchange.ts
index 26aca5fdb..8650b31a0 100644
--- a/backend/src/integrations/exchange.ts
+++ b/backend/src/integrations/exchange.ts
@@ -1,10 +1,12 @@
import axios from 'axios';
import * as Sentry from '@sentry/node';
import {
+ INTEGRATION_AZURE_KEY_VAULT,
INTEGRATION_HEROKU,
INTEGRATION_VERCEL,
INTEGRATION_NETLIFY,
INTEGRATION_GITHUB,
+ INTEGRATION_AZURE_TOKEN_URL,
INTEGRATION_HEROKU_TOKEN_URL,
INTEGRATION_VERCEL_TOKEN_URL,
INTEGRATION_NETLIFY_TOKEN_URL,
@@ -12,15 +14,27 @@ import {
} from '../variables';
import {
SITE_URL,
+ CLIENT_ID_AZURE,
CLIENT_ID_VERCEL,
CLIENT_ID_NETLIFY,
CLIENT_ID_GITHUB,
+ CLIENT_SECRET_AZURE,
CLIENT_SECRET_HEROKU,
CLIENT_SECRET_VERCEL,
CLIENT_SECRET_NETLIFY,
CLIENT_SECRET_GITHUB
} 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 {
token_type: string;
access_token: string;
@@ -75,6 +89,11 @@ const exchangeCode = async ({
try {
switch (integration) {
+ case INTEGRATION_AZURE_KEY_VAULT:
+ obj = await exchangeCodeAzure({
+ code
+ });
+ break;
case INTEGRATION_HEROKU:
obj = await exchangeCodeHeroku({
code
@@ -105,6 +124,46 @@ const exchangeCode = async ({
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
* OAuth2 code-token exchange
@@ -168,7 +227,7 @@ const exchangeCodeVercel = async ({ code }: { code: string }) => {
code: code,
client_id: CLIENT_ID_VERCEL,
client_secret: CLIENT_SECRET_VERCEL,
- redirect_uri: `${SITE_URL}/vercel`
+ redirect_uri: `${SITE_URL}/integrations/vercel/oauth2/callback`
} as any)
)
).data;
@@ -208,7 +267,7 @@ const exchangeCodeNetlify = async ({ code }: { code: string }) => {
code: code,
client_id: CLIENT_ID_NETLIFY,
client_secret: CLIENT_SECRET_NETLIFY,
- redirect_uri: `${SITE_URL}/netlify`
+ redirect_uri: `${SITE_URL}/integrations/netlify/oauth2/callback`
} as any)
)
).data;
@@ -260,10 +319,11 @@ const exchangeCodeGithub = async ({ code }: { code: string }) => {
client_id: CLIENT_ID_GITHUB,
client_secret: CLIENT_SECRET_GITHUB,
code: code,
- redirect_uri: `${SITE_URL}/github`
+ redirect_uri: `${SITE_URL}/integrations/github/oauth2/callback`
},
headers: {
- Accept: 'application/json'
+ 'Accept': 'application/json',
+ 'Accept-Encoding': 'application/json'
}
})
).data;
diff --git a/backend/src/integrations/refresh.ts b/backend/src/integrations/refresh.ts
index ea232f1e5..4fdbcdbbb 100644
--- a/backend/src/integrations/refresh.ts
+++ b/backend/src/integrations/refresh.ts
@@ -1,13 +1,26 @@
import axios from 'axios';
import * as Sentry from '@sentry/node';
-import { INTEGRATION_HEROKU } from '../variables';
+import { INTEGRATION_AZURE_KEY_VAULT, INTEGRATION_HEROKU } from '../variables';
import {
- CLIENT_SECRET_HEROKU
+ SITE_URL,
+ CLIENT_ID_AZURE,
+ CLIENT_SECRET_AZURE,
+ CLIENT_SECRET_HEROKU
} from '../config';
import {
- INTEGRATION_HEROKU_TOKEN_URL
+ INTEGRATION_AZURE_TOKEN_URL,
+ INTEGRATION_HEROKU_TOKEN_URL
} 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
* named [integration]
@@ -25,6 +38,11 @@ const exchangeRefresh = async ({
let accessToken;
try {
switch (integration) {
+ case INTEGRATION_AZURE_KEY_VAULT:
+ accessToken = await exchangeRefreshAzure({
+ refreshToken
+ });
+ break;
case INTEGRATION_HEROKU:
accessToken = await exchangeRefreshHeroku({
refreshToken
@@ -40,6 +58,38 @@ const exchangeRefresh = async ({
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
* Heroku integration
@@ -52,23 +102,23 @@ const exchangeRefreshHeroku = async ({
}: {
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
- try {
- const res = await axios.post(
- INTEGRATION_HEROKU_TOKEN_URL,
- new URLSearchParams({
- grant_type: 'refresh_token',
- refresh_token: refreshToken,
- client_secret: CLIENT_SECRET_HEROKU
- } as any)
- );
+
+ let accessToken;
+ try {
+ const res = await axios.post(
+ INTEGRATION_HEROKU_TOKEN_URL,
+ new URLSearchParams({
+ grant_type: 'refresh_token',
+ refresh_token: refreshToken,
+ client_secret: CLIENT_SECRET_HEROKU
+ } as any)
+ );
accessToken = res.data.access_token;
} catch (err) {
Sentry.setUser(null);
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;
diff --git a/backend/src/integrations/sync.ts b/backend/src/integrations/sync.ts
index 9954943b8..401c7f9a1 100644
--- a/backend/src/integrations/sync.ts
+++ b/backend/src/integrations/sync.ts
@@ -1,11 +1,10 @@
import axios from 'axios';
import * as Sentry from '@sentry/node';
import { Octokit } from '@octokit/rest';
-// import * as sodium from 'libsodium-wrappers';
import sodium from 'libsodium-wrappers';
-// const sodium = require('libsodium-wrappers');
import { IIntegration, IIntegrationAuth } from '../models';
import {
+ INTEGRATION_AZURE_KEY_VAULT,
INTEGRATION_HEROKU,
INTEGRATION_VERCEL,
INTEGRATION_NETLIFY,
@@ -18,7 +17,6 @@ import {
INTEGRATION_RENDER_API_URL,
INTEGRATION_FLYIO_API_URL
} from '../variables';
-import { access, appendFile } from 'fs';
/**
* Sync/push [secrets] to [app] in integration named [integration]
@@ -41,6 +39,13 @@ const syncSecrets = async ({
}) => {
try {
switch (integration.integration) {
+ case INTEGRATION_AZURE_KEY_VAULT:
+ await syncSecretsAzureKeyVault({
+ integration,
+ secrets,
+ accessToken
+ });
+ break;
case INTEGRATION_HEROKU:
await syncSecretsHeroku({
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]
* @param {Object} obj
diff --git a/backend/src/models/integration.ts b/backend/src/models/integration.ts
index 01e1d7ee3..9c1214fb5 100644
--- a/backend/src/models/integration.ts
+++ b/backend/src/models/integration.ts
@@ -1,5 +1,6 @@
import { Schema, model, Types } from 'mongoose';
import {
+ INTEGRATION_AZURE_KEY_VAULT,
INTEGRATION_HEROKU,
INTEGRATION_VERCEL,
INTEGRATION_NETLIFY,
@@ -17,7 +18,7 @@ export interface IIntegration {
owner: string;
targetEnvironment: string;
appId: string;
- integration: 'heroku' | 'vercel' | 'netlify' | 'github' | 'render' | 'flyio';
+ integration: 'heroku' | 'vercel' | 'netlify' | 'github' | 'render' | 'flyio' | 'azure-key-vault';
integrationAuth: Types.ObjectId;
}
@@ -59,6 +60,7 @@ const integrationSchema = new Schema(
integration: {
type: String,
enum: [
+ INTEGRATION_AZURE_KEY_VAULT,
INTEGRATION_HEROKU,
INTEGRATION_VERCEL,
INTEGRATION_NETLIFY,
diff --git a/backend/src/models/integrationAuth.ts b/backend/src/models/integrationAuth.ts
index bff56f09a..0eaa8c9e1 100644
--- a/backend/src/models/integrationAuth.ts
+++ b/backend/src/models/integrationAuth.ts
@@ -1,5 +1,6 @@
import { Schema, model, Types } from 'mongoose';
import {
+ INTEGRATION_AZURE_KEY_VAULT,
INTEGRATION_HEROKU,
INTEGRATION_VERCEL,
INTEGRATION_NETLIFY,
@@ -9,7 +10,7 @@ import {
export interface IIntegrationAuth {
_id: Types.ObjectId;
workspace: Types.ObjectId;
- integration: 'heroku' | 'vercel' | 'netlify' | 'github' | 'render' | 'flyio';
+ integration: 'heroku' | 'vercel' | 'netlify' | 'github' | 'render' | 'flyio' | 'azure-key-vault';
teamId: string;
accountId: string;
refreshCiphertext?: string;
@@ -31,6 +32,7 @@ const integrationAuthSchema = new Schema(
integration: {
type: String,
enum: [
+ INTEGRATION_AZURE_KEY_VAULT,
INTEGRATION_HEROKU,
INTEGRATION_VERCEL,
INTEGRATION_NETLIFY,
diff --git a/backend/src/routes/v1/integration.ts b/backend/src/routes/v1/integration.ts
index d587bd2e5..90cea3a38 100644
--- a/backend/src/routes/v1/integration.ts
+++ b/backend/src/routes/v1/integration.ts
@@ -10,7 +10,7 @@ import { ADMIN, MEMBER } from '../../variables';
import { body, param } from 'express-validator';
import { integrationController } from '../../controllers/v1';
-router.post( // new: add new integration
+router.post( // new: add new integration for integration auth
'/',
requireAuth({
acceptedAuthModes: ['jwt', 'apiKey']
@@ -19,7 +19,13 @@ router.post( // new: add new integration
acceptedRoles: [ADMIN, MEMBER],
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,
integrationController.createIntegration
);
diff --git a/backend/src/routes/v1/integrationAuth.ts b/backend/src/routes/v1/integrationAuth.ts
index 3f88aa7e5..1918140c2 100644
--- a/backend/src/routes/v1/integrationAuth.ts
+++ b/backend/src/routes/v1/integrationAuth.ts
@@ -18,6 +18,19 @@ router.get(
integrationAuthController.getIntegrationOptions
);
+router.get(
+ '/:integrationAuthId',
+ requireAuth({
+ acceptedAuthModes: ['jwt']
+ }),
+ requireIntegrationAuthorizationAuth({
+ acceptedRoles: [ADMIN, MEMBER]
+ }),
+ param('integrationAuthId'),
+ validateRequest,
+ integrationAuthController.getIntegrationAuth
+);
+
router.post(
'/oauth-token',
requireAuth({
diff --git a/backend/src/services/IntegrationService.ts b/backend/src/services/IntegrationService.ts
index 4ee991cdd..5b0195427 100644
--- a/backend/src/services/IntegrationService.ts
+++ b/backend/src/services/IntegrationService.ts
@@ -1,7 +1,3 @@
-import * as Sentry from '@sentry/node';
-import {
- Integration
-} from '../models';
import {
handleOAuthExchangeHelper,
syncIntegrationsHelper,
@@ -10,7 +6,6 @@ import {
setIntegrationAuthRefreshHelper,
setIntegrationAuthAccessHelper,
} from '../helpers/integration';
-import { exchangeCode } from '../integrations';
// should sync stuff be here too? Probably.
// 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
* - Add placeholder inactive integration
* - Create bot sequence for integration
- * @param {Object} obj
- * @param {String} obj.workspaceId - id of workspace
- * @param {String} obj.environment - workspace environment
- * @param {String} obj.integration - name of integration
- * @param {String} obj.code - code
+ * @param {Object} obj1
+ * @param {String} obj1.workspaceId - id of workspace
+ * @param {String} obj1.environment - workspace environment
+ * @param {String} obj1.integration - name of integration
+ * @param {String} obj1.code - code
+ * @returns {IntegrationAuth} integrationAuth - integration authorization after OAuth2 code-token exchange
*/
static async handleOAuthExchange({
workspaceId,
@@ -43,7 +39,7 @@ class IntegrationService {
code: string;
environment: string;
}) {
- await handleOAuthExchangeHelper({
+ return await handleOAuthExchangeHelper({
workspaceId,
integration,
code,
diff --git a/backend/src/variables/index.ts b/backend/src/variables/index.ts
index fafab6939..cf061520c 100644
--- a/backend/src/variables/index.ts
+++ b/backend/src/variables/index.ts
@@ -6,6 +6,7 @@ import {
ENV_SET
} from './environment';
import {
+ INTEGRATION_AZURE_KEY_VAULT,
INTEGRATION_HEROKU,
INTEGRATION_VERCEL,
INTEGRATION_NETLIFY,
@@ -14,6 +15,7 @@ import {
INTEGRATION_FLYIO,
INTEGRATION_SET,
INTEGRATION_OAUTH2,
+ INTEGRATION_AZURE_TOKEN_URL,
INTEGRATION_HEROKU_TOKEN_URL,
INTEGRATION_VERCEL_TOKEN_URL,
INTEGRATION_NETLIFY_TOKEN_URL,
@@ -58,6 +60,7 @@ export {
ENV_STAGING,
ENV_PROD,
ENV_SET,
+ INTEGRATION_AZURE_KEY_VAULT,
INTEGRATION_HEROKU,
INTEGRATION_VERCEL,
INTEGRATION_NETLIFY,
@@ -66,6 +69,7 @@ export {
INTEGRATION_FLYIO,
INTEGRATION_SET,
INTEGRATION_OAUTH2,
+ INTEGRATION_AZURE_TOKEN_URL,
INTEGRATION_HEROKU_TOKEN_URL,
INTEGRATION_VERCEL_TOKEN_URL,
INTEGRATION_NETLIFY_TOKEN_URL,
diff --git a/backend/src/variables/integration.ts b/backend/src/variables/integration.ts
index 7cecb54c2..0cac80984 100644
--- a/backend/src/variables/integration.ts
+++ b/backend/src/variables/integration.ts
@@ -1,3 +1,7 @@
+import {
+ CLIENT_ID_AZURE,
+ TENANT_ID_AZURE
+} from '../config';
import {
CLIENT_ID_HEROKU,
CLIENT_ID_NETLIFY,
@@ -6,6 +10,7 @@ import {
} from '../config';
// integrations
+const INTEGRATION_AZURE_KEY_VAULT = 'azure-key-vault';
const INTEGRATION_HEROKU = 'heroku';
const INTEGRATION_VERCEL = 'vercel';
const INTEGRATION_NETLIFY = 'netlify';
@@ -13,6 +18,7 @@ const INTEGRATION_GITHUB = 'github';
const INTEGRATION_RENDER = 'render';
const INTEGRATION_FLYIO = 'flyio';
const INTEGRATION_SET = new Set([
+ INTEGRATION_AZURE_KEY_VAULT,
INTEGRATION_HEROKU,
INTEGRATION_VERCEL,
INTEGRATION_NETLIFY,
@@ -25,6 +31,7 @@ const INTEGRATION_SET = new Set([
const INTEGRATION_OAUTH2 = 'oauth2';
// integration oauth endpoints
+const INTEGRATION_AZURE_TOKEN_URL = `https://login.microsoftonline.com/${TENANT_ID_AZURE}/oauth2/v2.0/token`;
const INTEGRATION_HEROKU_TOKEN_URL = 'https://id.heroku.com/oauth/token';
const INTEGRATION_VERCEL_TOKEN_URL =
'https://api.vercel.com/v2/oauth/access_token';
@@ -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_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',
slug: 'heroku',
@@ -143,6 +160,7 @@ const INTEGRATION_OPTIONS = [
]
export {
+ INTEGRATION_AZURE_KEY_VAULT,
INTEGRATION_HEROKU,
INTEGRATION_VERCEL,
INTEGRATION_NETLIFY,
@@ -151,6 +169,7 @@ export {
INTEGRATION_FLYIO,
INTEGRATION_SET,
INTEGRATION_OAUTH2,
+ INTEGRATION_AZURE_TOKEN_URL,
INTEGRATION_HEROKU_TOKEN_URL,
INTEGRATION_VERCEL_TOKEN_URL,
INTEGRATION_NETLIFY_TOKEN_URL,
diff --git a/docs/images/k8-diagram.png b/docs/images/k8-diagram.png
new file mode 100644
index 000000000..f45d301bc
Binary files /dev/null and b/docs/images/k8-diagram.png differ
diff --git a/docs/integrations/platforms/kubernetes.mdx b/docs/integrations/platforms/kubernetes.mdx
index 189dc9d7b..07ce0aae3 100644
--- a/docs/integrations/platforms/kubernetes.mdx
+++ b/docs/integrations/platforms/kubernetes.mdx
@@ -3,10 +3,12 @@ title: 'Kubernetes'
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.
-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
diff --git a/frontend/package-lock.json b/frontend/package-lock.json
index 7f52e04ae..b0317ff7c 100644
--- a/frontend/package-lock.json
+++ b/frontend/package-lock.json
@@ -15,6 +15,7 @@
"@fortawesome/react-fontawesome": "^0.1.19",
"@headlessui/react": "^1.6.6",
"@hookform/resolvers": "^2.9.10",
+ "@octokit/rest": "^19.0.7",
"@radix-ui/react-accordion": "^1.1.0",
"@radix-ui/react-alert-dialog": "^1.0.2",
"@radix-ui/react-checkbox": "^1.0.1",
@@ -3521,6 +3522,153 @@
"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": {
"version": "2.3.1",
"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": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/better-opn/-/better-opn-2.1.1.tgz",
@@ -10371,6 +10524,11 @@
"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": {
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz",
@@ -14099,7 +14257,6 @@
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-5.0.0.tgz",
"integrity": "sha512-VRSzKkbMm5jMDoKLbltAkFQ5Qr7VDiTFGXxYFXXowVj387GeGNOCsOH6Msy00SGZ3Fp84b1Naa1psqgcCIEP5Q==",
- "dev": true,
"engines": {
"node": ">=0.10.0"
}
@@ -16455,7 +16612,6 @@
"version": "2.6.8",
"resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.8.tgz",
"integrity": "sha512-RZ6dBYuj8dRSfxpUSu+NsdF1dpPpluJxwOp+6IoDp/sH2QNDSvurYsAa+F1WxY2RjA1iP93xhcsUoYbF2XBqVg==",
- "dev": true,
"dependencies": {
"whatwg-url": "^5.0.0"
},
@@ -21217,8 +21373,7 @@
"node_modules/tr46": {
"version": "0.0.3",
"resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz",
- "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==",
- "dev": true
+ "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw=="
},
"node_modules/trim-lines": {
"version": "3.0.1",
@@ -21664,6 +21819,11 @@
"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": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.0.tgz",
@@ -22068,8 +22228,7 @@
"node_modules/webidl-conversions": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz",
- "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==",
- "dev": true
+ "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ=="
},
"node_modules/webpack": {
"version": "5.75.0",
@@ -22267,7 +22426,6 @@
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz",
"integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==",
- "dev": true,
"dependencies": {
"tr46": "~0.0.3",
"webidl-conversions": "^3.0.0"
@@ -24829,6 +24987,118 @@
"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": {
"version": "2.3.1",
"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",
"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": {
"version": "2.1.1",
"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",
"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": {
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz",
@@ -32732,8 +33012,7 @@
"is-plain-object": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-5.0.0.tgz",
- "integrity": "sha512-VRSzKkbMm5jMDoKLbltAkFQ5Qr7VDiTFGXxYFXXowVj387GeGNOCsOH6Msy00SGZ3Fp84b1Naa1psqgcCIEP5Q==",
- "dev": true
+ "integrity": "sha512-VRSzKkbMm5jMDoKLbltAkFQ5Qr7VDiTFGXxYFXXowVj387GeGNOCsOH6Msy00SGZ3Fp84b1Naa1psqgcCIEP5Q=="
},
"is-regex": {
"version": "1.1.4",
@@ -34410,7 +34689,6 @@
"version": "2.6.8",
"resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.8.tgz",
"integrity": "sha512-RZ6dBYuj8dRSfxpUSu+NsdF1dpPpluJxwOp+6IoDp/sH2QNDSvurYsAa+F1WxY2RjA1iP93xhcsUoYbF2XBqVg==",
- "dev": true,
"requires": {
"whatwg-url": "^5.0.0"
}
@@ -37863,8 +38141,7 @@
"tr46": {
"version": "0.0.3",
"resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz",
- "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==",
- "dev": true
+ "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw=="
},
"trim-lines": {
"version": "3.0.1",
@@ -38186,6 +38463,11 @@
"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": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.0.tgz",
@@ -38479,8 +38761,7 @@
"webidl-conversions": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz",
- "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==",
- "dev": true
+ "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ=="
},
"webpack": {
"version": "5.75.0",
@@ -38626,7 +38907,6 @@
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz",
"integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==",
- "dev": true,
"requires": {
"tr46": "~0.0.3",
"webidl-conversions": "^3.0.0"
diff --git a/frontend/package.json b/frontend/package.json
index db8153a0a..4e2e3e585 100644
--- a/frontend/package.json
+++ b/frontend/package.json
@@ -22,6 +22,7 @@
"@fortawesome/react-fontawesome": "^0.1.19",
"@headlessui/react": "^1.6.6",
"@hookform/resolvers": "^2.9.10",
+ "@octokit/rest": "^19.0.7",
"@radix-ui/react-accordion": "^1.1.0",
"@radix-ui/react-alert-dialog": "^1.0.2",
"@radix-ui/react-checkbox": "^1.0.1",
diff --git a/frontend/public/data/frequentConstants.ts b/frontend/public/data/frequentConstants.ts
index 2ff525e54..f25da26d3 100644
--- a/frontend/public/data/frequentConstants.ts
+++ b/frontend/public/data/frequentConstants.ts
@@ -2,6 +2,16 @@ interface Mapping {
[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 = {
Development: "dev",
Staging: "staging",
@@ -19,7 +29,7 @@ const reverseEnvMapping: Mapping = {
const contextNetlifyMapping: Mapping = {
"dev": "Local development",
"branch-deploy": "Branch deploys",
- "deploy-review": "Deploy Previews",
+ "deploy-preview": "Deploy Previews",
"production": "Production"
}
@@ -49,6 +59,7 @@ const plans = plansProd || plansDev;
export {
contextNetlifyMapping,
envMapping,
+ integrationSlugNameMapping,
plans,
reverseContextNetlifyMapping,
reverseEnvMapping}
diff --git a/frontend/public/data/frequentInterfaces.ts b/frontend/public/data/frequentInterfaces.ts
index c2fa797fd..9865d9909 100644
--- a/frontend/public/data/frequentInterfaces.ts
+++ b/frontend/public/data/frequentInterfaces.ts
@@ -1,3 +1,12 @@
+export interface Tag {
+ _id: string;
+ name: string;
+ slug: string;
+ user: string;
+ workspace: string;
+ createdAt: string;
+}
+
export interface SecretDataProps {
pos: number;
key: string;
@@ -5,4 +14,5 @@ export interface SecretDataProps {
valueOverride: string | undefined;
id: string;
comment: string;
+ tags: Tag[];
}
\ No newline at end of file
diff --git a/frontend/src/components/basic/Layout.tsx b/frontend/src/components/basic/Layout.tsx
index ba4cf73b2..c6e234058 100644
--- a/frontend/src/components/basic/Layout.tsx
+++ b/frontend/src/components/basic/Layout.tsx
@@ -199,13 +199,13 @@ const Layout = ({ children }: LayoutProps) => {
.split('/')
[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);
}
-
+
// If a user is not a member of a workspace they are trying to access, just push them to one of theirs
if (
- !['heroku', 'vercel', 'github', 'netlify'].includes(intendedWorkspaceId) &&
+ !['callback', 'create', 'authorize'].includes(intendedWorkspaceId) &&
!userWorkspaces
.map((workspace: { _id: string }) => workspace._id)
.includes(intendedWorkspaceId)
diff --git a/frontend/src/components/basic/dialog/AddWorkspaceDialog.tsx b/frontend/src/components/basic/dialog/AddWorkspaceDialog.tsx
index d9cf17645..88dc7cb78 100644
--- a/frontend/src/components/basic/dialog/AddWorkspaceDialog.tsx
+++ b/frontend/src/components/basic/dialog/AddWorkspaceDialog.tsx
@@ -36,7 +36,7 @@ const AddWorkspaceDialog = ({
return (
-
+
-
+
-
+
- This project will contain your environmental variables.
+ This project will contain your secrets and configs.
diff --git a/frontend/src/components/basic/dialog/IntegrationAccessTokenDialog.tsx b/frontend/src/components/basic/dialog/IntegrationAccessTokenDialog.tsx
deleted file mode 100644
index 0a09f21e9..000000000
--- a/frontend/src/components/basic/dialog/IntegrationAccessTokenDialog.tsx
+++ /dev/null
@@ -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 (
-
-
- {
- closeModal();
- }}>
-
-
-
-
-
-
-
-
- {`Enter your ${selectedIntegrationOption?.name} API Key`}
-
-
-
- {`This integration requires you to obtain an API key from ${selectedIntegrationOption?.name ?? ''} and store it with Infisical. `}
- You can learn how to do this here .
-
-
-
-
-
-
-
-
-
-
- );
-}
-
-export default IntegrationAccessTokenDialog;
\ No newline at end of file
diff --git a/frontend/src/components/basic/table/EnvironmentsTable.tsx b/frontend/src/components/basic/table/EnvironmentsTable.tsx
index 37411c366..f4e02a3bb 100644
--- a/frontend/src/components/basic/table/EnvironmentsTable.tsx
+++ b/frontend/src/components/basic/table/EnvironmentsTable.tsx
@@ -83,7 +83,7 @@ const EnvironmentTable = ({ data = [], onCreateEnv, onDeleteEnv, onUpdateEnv }:
{
if (plan !== plans.starter || host !== 'https://app.infisical.com') {
handlePopUpOpen('createUpdateEnv');
diff --git a/frontend/src/components/dashboard/AddTagsMenu.tsx b/frontend/src/components/dashboard/AddTagsMenu.tsx
new file mode 100644
index 000000000..1b0790079
--- /dev/null
+++ b/frontend/src/components/dashboard/AddTagsMenu.tsx
@@ -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 (
+
+
+
+
+ {currentTags.length > 2 && {currentTags.length - 2} }
+
+
+
+
+ {allTags?.map((tag) => { return (
+
+ 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) ? : } {tag.name}
+
+
+ )})}
+ router.push(`/settings/project/${String(router.query.id)}`)}
+ >
+ Add more tags
+
+
+
+
+ );
+};
+
+export default AddTagsMenu;
diff --git a/frontend/src/components/dashboard/DashboardInputField.tsx b/frontend/src/components/dashboard/DashboardInputField.tsx
index 00c9c0de7..2b01fa62e 100644
--- a/frontend/src/components/dashboard/DashboardInputField.tsx
+++ b/frontend/src/components/dashboard/DashboardInputField.tsx
@@ -3,7 +3,6 @@ import { faCircle, faExclamationCircle, faEye, faLayerGroup } from '@fortawesome
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import guidGenerator from '../utilities/randomId';
-import { Button } from '../v2';
import { HoverObject } from '../v2/HoverCard';
const REGEX = /([$]{.*?})/g;
@@ -101,8 +100,8 @@ const DashboardInputField = ({
)}
{!error &&
- {
+ } cursor-pointer duration-0 h-10 flex items-center px-2`}>
+ {
if (modifyValueOverride) {
if (overrideEnabled === false) {
modifyValueOverride('', position);
@@ -116,7 +115,7 @@ const DashboardInputField = ({
icon={faLayerGroup}
color={overrideEnabled ? 'primary' : 'bunker-400'}
/>
-
+
}
);
diff --git a/frontend/src/components/dashboard/DeleteActionButton.tsx b/frontend/src/components/dashboard/DeleteActionButton.tsx
index d0708f239..91819b43c 100644
--- a/frontend/src/components/dashboard/DeleteActionButton.tsx
+++ b/frontend/src/components/dashboard/DeleteActionButton.tsx
@@ -19,7 +19,7 @@ export const DeleteActionButton = ({ onSubmit, isPlain }: Props) => {
+ : 'cursor-pointer w-[1.5rem] h-[2.35rem] mr-2 flex items-center justfy-center'}`}>
{isPlain
?
null}
diff --git a/frontend/src/components/dashboard/DropZone.tsx b/frontend/src/components/dashboard/DropZone.tsx
index 5b1c0cd45..cd6d8650b 100644
--- a/frontend/src/components/dashboard/DropZone.tsx
+++ b/frontend/src/components/dashboard/DropZone.tsx
@@ -64,7 +64,8 @@ const DropZone = ({
key,
value: keyPairs[key as keyof typeof keyPairs].value,
comment: keyPairs[key as keyof typeof keyPairs].comments.join('\n'),
- type: 'shared'
+ type: 'shared',
+ tags: []
}));
break;
}
@@ -86,7 +87,8 @@ const DropZone = ({
key,
value: keyPairs[key as keyof typeof keyPairs]?.toString() ?? '',
comment,
- type: 'shared'
+ type: 'shared',
+ tags: []
};
});
break;
diff --git a/frontend/src/components/dashboard/KeyPair.tsx b/frontend/src/components/dashboard/KeyPair.tsx
index c2f8debef..03d22d39c 100644
--- a/frontend/src/components/dashboard/KeyPair.tsx
+++ b/frontend/src/components/dashboard/KeyPair.tsx
@@ -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 { SecretDataProps } from 'public/data/frequentInterfaces';
+import { SecretDataProps, Tag } from 'public/data/frequentInterfaces';
+import AddTagsMenu from './AddTagsMenu';
import DashboardInputField from './DashboardInputField';
import { DeleteActionButton } from './DeleteActionButton';
@@ -11,6 +12,7 @@ interface KeyPairProps {
modifyValue: (value: string, position: number) => void;
modifyValueOverride: (value: string | undefined, position: number) => void;
modifyComment: (value: string, position: number) => void;
+ modifyTags: (value: Tag[], position: number) => void;
isBlurred: boolean;
isDuplicate: boolean;
toggleSidebar: (id: string) => void;
@@ -18,6 +20,8 @@ interface KeyPairProps {
isSnapshot: boolean;
isCapitalized: boolean;
deleteRow?: (props: DeleteRowFunctionProps) => void;
+ tags: Tag[];
+ togglePITSidebar?: (value: boolean) => void;
}
export interface DeleteRowFunctionProps {
@@ -25,6 +29,23 @@ export interface DeleteRowFunctionProps {
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
* @param {object} obj
@@ -33,12 +54,15 @@ export interface DeleteRowFunctionProps {
* @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.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.isDuplicate - list of all the duplicates secret names on the dashboard
* @param {function} obj.toggleSidebar - open/close/switch sidebar
* @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 {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
*/
const KeyPair = ({
@@ -47,22 +71,32 @@ const KeyPair = ({
modifyValue,
modifyValueOverride,
modifyComment,
+ modifyTags,
isBlurred,
isDuplicate,
toggleSidebar,
sidebarSecretId,
isCapitalized,
isSnapshot,
- deleteRow
-}: KeyPairProps) => (
+ deleteRow,
+ togglePITSidebar,
+ tags
+}: KeyPairProps) => {
+ const tagData = (tags.map((tag, index) => {return {
+ ...tag,
+ color: colors[index%colors.length],
+ colorText: colorsText[index%colorsText.length]
+ }}));
+
+ return (
-
{keyPair.pos + 1}
-
+
-
+
@@ -92,7 +126,7 @@ const KeyPair = ({
/>
-
+
- {!isSnapshot && (
-
null}
- role="button"
- tabIndex={0}
- onClick={() => toggleSidebar(keyPair.id)}
- className="cursor-pointer w-[2.35rem] h-[2.35rem] px-6 rounded-md invisible group-hover:visible flex flex-row justify-center items-center"
- >
-
+
+
+ {keyPair.tags.map((tag, index) => (
+ index < 2 &&
tagDp._id === tag._id)[0]?.color} rounded-sm text-sm ${tagData.filter(tagDp => tagDp._id === tag._id)[0]?.colorText} flex items-center`}>
+ {tag.name}
+ modifyTags(keyPair.tags.filter(ttag => ttag._id !== tag._id), keyPair.pos)}/>
+
+ ))}
+
+
- )}
- {!isSnapshot && (
+
+
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'}`}
+ >
+
+
+
{ if (deleteRow) {
deleteRow({ ids: [keyPair.id], secretName: keyPair?.key })
}}}
isPlain
/>
- )}
+
-);
+)};
export default KeyPair;
diff --git a/frontend/src/components/dashboard/SideBar.tsx b/frontend/src/components/dashboard/SideBar.tsx
index a40dc1634..1d0996376 100644
--- a/frontend/src/components/dashboard/SideBar.tsx
+++ b/frontend/src/components/dashboard/SideBar.tsx
@@ -80,7 +80,7 @@ const SideBar = ({
const { t } = useTranslation();
return (
-
+
{isLoading ? (
{
+
// set initial environment. This find will only execute when component is mounting
const [integrationEnvironment, setIntegrationEnvironment] = useState(
environments.find(({ slug }) => slug === integration.environment) || {
@@ -72,7 +73,14 @@ const IntegrationTile = ({
});
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) {
case 'vercel':
@@ -174,7 +182,7 @@ const IntegrationTile = ({
return
;
};
- if (!integrationApp || apps.length === 0) return
;
+ if (!integrationApp) return
;
return (
@@ -201,7 +209,8 @@ const IntegrationTile = ({
INTEGRATION
- {integration.integration.charAt(0).toUpperCase() + integration.integration.slice(1)}
+ {/* {integration.integration.charAt(0).toUpperCase() + integration.integration.slice(1)} */}
+ {integrationSlugNameMapping[integration.integration]}
diff --git a/frontend/src/components/utilities/attemptLogin.ts b/frontend/src/components/utilities/attemptLogin.ts
index acc2b67fe..a47b2c1f0 100644
--- a/frontend/src/components/utilities/attemptLogin.ts
+++ b/frontend/src/components/utilities/attemptLogin.ts
@@ -138,7 +138,8 @@ const attemptLogin = async (
value: 'mongodb+srv://${DB_USERNAME}:${DB_PASSWORD}@mongodb.net',
valueOverride: undefined,
comment: 'This is an example of secret referencing.',
- id: ''
+ id: '',
+ tags: []
},
{
pos: 1,
@@ -147,7 +148,8 @@ const attemptLogin = async (
valueOverride: undefined,
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',
- id: ''
+ id: '',
+ tags: []
},
{
pos: 2,
@@ -156,7 +158,8 @@ const attemptLogin = async (
valueOverride: undefined,
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',
- id: ''
+ id: '',
+ tags: []
},
{
pos: 3,
@@ -164,7 +167,8 @@ const attemptLogin = async (
value: 'user1234',
valueOverride: 'user1234',
comment: '',
- id: ''
+ id: '',
+ tags: []
},
{
pos: 4,
@@ -172,7 +176,8 @@ const attemptLogin = async (
value: 'example_password',
valueOverride: 'example_password',
comment: '',
- id: ''
+ id: '',
+ tags: []
},
{
pos: 5,
@@ -180,7 +185,8 @@ const attemptLogin = async (
value: 'example_twillio_token',
valueOverride: undefined,
comment: '',
- id: ''
+ id: '',
+ tags: []
},
{
pos: 6,
@@ -188,7 +194,8 @@ const attemptLogin = async (
value: 'http://localhost:3000',
valueOverride: undefined,
comment: '',
- id: ''
+ id: '',
+ tags: []
}
];
const secrets = await encryptSecrets({
diff --git a/frontend/src/components/utilities/secrets/encryptSecrets.ts b/frontend/src/components/utilities/secrets/encryptSecrets.ts
index 53ce0066d..11d9e8d22 100644
--- a/frontend/src/components/utilities/secrets/encryptSecrets.ts
+++ b/frontend/src/components/utilities/secrets/encryptSecrets.ts
@@ -1,6 +1,6 @@
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';
@@ -20,6 +20,7 @@ interface EncryptedSecretProps {
secretValueIV: string;
secretValueTag: string;
type: 'personal' | 'shared';
+ tags: Tag[];
}
/**
@@ -105,7 +106,8 @@ const encryptSecrets = async ({
type:
secret.valueOverride === undefined || secret?.value !== secret?.valueOverride
? 'shared'
- : 'personal'
+ : 'personal',
+ tags: secret.tags
};
return result;
diff --git a/frontend/src/components/utilities/secrets/getSecretsForProject.ts b/frontend/src/components/utilities/secrets/getSecretsForProject.ts
index 62c1e2e2d..28491a3e4 100644
--- a/frontend/src/components/utilities/secrets/getSecretsForProject.ts
+++ b/frontend/src/components/utilities/secrets/getSecretsForProject.ts
@@ -1,3 +1,5 @@
+import { Tag } from 'public/data/frequentInterfaces';
+
import getSecrets from '@app/pages/api/files/GetSecrets';
import getLatestFileKey from '@app/pages/api/workspace/getLatestFileKey';
@@ -17,6 +19,7 @@ interface EncryptedSecretProps {
secretValueIV: string;
secretValueTag: string;
type: 'personal' | 'shared';
+ tags: Tag[];
}
interface SecretProps {
@@ -25,6 +28,7 @@ interface SecretProps {
type: 'personal' | 'shared';
comment: string;
id: string;
+ tags: Tag[];
}
interface FunctionProps {
@@ -107,7 +111,8 @@ const getSecretsForProject = async ({
key: plainTextKey,
value: plainTextValue,
type: secret.type,
- comment: plainTextComment
+ comment: plainTextComment,
+ tags: secret.tags
});
});
}
@@ -130,7 +135,10 @@ const getSecretsForProject = async ({
)[0]?.value,
comment: tempDecryptedSecrets.filter(
(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') {
diff --git a/frontend/src/components/v2/DeleteActionModal/DeleteActionModal.tsx b/frontend/src/components/v2/DeleteActionModal/DeleteActionModal.tsx
index 7678af3cf..1d072cc89 100644
--- a/frontend/src/components/v2/DeleteActionModal/DeleteActionModal.tsx
+++ b/frontend/src/components/v2/DeleteActionModal/DeleteActionModal.tsx
@@ -52,7 +52,7 @@ export const DeleteActionModal = ({
>
(
diff --git a/frontend/src/ee/components/PITRecoverySidebar.tsx b/frontend/src/ee/components/PITRecoverySidebar.tsx
index b13e3b66a..204973228 100644
--- a/frontend/src/ee/components/PITRecoverySidebar.tsx
+++ b/frontend/src/ee/components/PITRecoverySidebar.tsx
@@ -5,6 +5,7 @@ import { useRouter } from 'next/router';
import { useTranslation } from 'next-i18next';
import { faXmark } from '@fortawesome/free-solid-svg-icons';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
+import { Tag } from 'public/data/frequentInterfaces';
import Button from '@app/components/basic/buttons/Button';
import {
@@ -48,6 +49,7 @@ interface EncrypetedSecretVersionListProps {
secretKeyTag: string;
environment: string;
type: 'personal' | 'shared';
+ tags: Tag[];
}
/**
@@ -106,6 +108,7 @@ const PITRecoverySidebar = ({ toggleSidebar, setSnapshotData, chosenSnapshot }:
pos,
type: encryptedSecretVersion.type,
environment: encryptedSecretVersion.environment,
+ tags: encryptedSecretVersion.tags,
key: decryptSymmetric({
ciphertext: encryptedSecretVersion.secretKeyCiphertext,
iv: encryptedSecretVersion.secretKeyIV,
@@ -134,6 +137,9 @@ const PITRecoverySidebar = ({ toggleSidebar, setSnapshotData, chosenSnapshot }:
environment: decryptedSecretVersions.filter(
(secret: SecretDataProps) => secret.key === key && secret.type === 'shared'
)[0].environment,
+ tags: decryptedSecretVersions.filter(
+ (secret: SecretDataProps) => secret.key === key && secret.type === 'shared'
+ )[0].tags,
value: decryptedSecretVersions.filter(
(secret: SecretDataProps) => secret.key === key && secret.type === 'shared'
)[0]?.value,
@@ -155,7 +161,7 @@ const PITRecoverySidebar = ({ toggleSidebar, setSnapshotData, chosenSnapshot }:
{isLoading ? (
diff --git a/frontend/src/hooks/api/index.tsx b/frontend/src/hooks/api/index.tsx
index 424fb514a..263f7e152 100644
--- a/frontend/src/hooks/api/index.tsx
+++ b/frontend/src/hooks/api/index.tsx
@@ -2,4 +2,5 @@ export * from './auth';
export * from './keys';
export * from './serviceTokens';
export * from './subscriptions';
+export * from './tags';
export * from './workspace';
diff --git a/frontend/src/hooks/api/integrationAuth/index.tsx b/frontend/src/hooks/api/integrationAuth/index.tsx
new file mode 100644
index 000000000..43920b65f
--- /dev/null
+++ b/frontend/src/hooks/api/integrationAuth/index.tsx
@@ -0,0 +1,3 @@
+export {
+ useGetIntegrationAuthApps,
+ useGetIntegrationAuthById} from './queries';
\ No newline at end of file
diff --git a/frontend/src/hooks/api/integrationAuth/queries.tsx b/frontend/src/hooks/api/integrationAuth/queries.tsx
new file mode 100644
index 000000000..508afc741
--- /dev/null
+++ b/frontend/src/hooks/api/integrationAuth/queries.tsx
@@ -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
+ });
+}
\ No newline at end of file
diff --git a/frontend/src/hooks/api/integrationAuth/types.ts b/frontend/src/hooks/api/integrationAuth/types.ts
new file mode 100644
index 000000000..bf193eafe
--- /dev/null
+++ b/frontend/src/hooks/api/integrationAuth/types.ts
@@ -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;
+}
\ No newline at end of file
diff --git a/frontend/src/hooks/api/tags/index.tsx b/frontend/src/hooks/api/tags/index.tsx
new file mode 100644
index 000000000..c37b75a2c
--- /dev/null
+++ b/frontend/src/hooks/api/tags/index.tsx
@@ -0,0 +1,3 @@
+export { useGetWsTags } from './queries';
+export { useCreateWsTag } from './queries';
+export { useDeleteWsTag } from './queries';
diff --git a/frontend/src/hooks/api/tags/queries.tsx b/frontend/src/hooks/api/tags/queries.tsx
new file mode 100644
index 000000000..17bd8092a
--- /dev/null
+++ b/frontend/src/hooks/api/tags/queries.tsx
@@ -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
({
+ 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({
+ mutationFn: async ({ tagID }) => {
+ const { data } = await apiRequest.delete(`/api/v2/workspace/tags/${tagID}`);
+ return data
+ },
+ onSuccess: (tagData) => {
+ queryClient.invalidateQueries(workspaceTags.getWsTags(tagData?.workspace));
+ }
+ });
+};
diff --git a/frontend/src/hooks/api/tags/types.ts b/frontend/src/hooks/api/tags/types.ts
new file mode 100644
index 000000000..56b7171bf
--- /dev/null
+++ b/frontend/src/hooks/api/tags/types.ts
@@ -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;
+};
\ No newline at end of file
diff --git a/frontend/src/hooks/api/types.ts b/frontend/src/hooks/api/types.ts
index cc7498b72..a0475e24e 100644
--- a/frontend/src/hooks/api/types.ts
+++ b/frontend/src/hooks/api/types.ts
@@ -9,5 +9,6 @@ export type {
RenameWorkspaceDTO,
UpdateEnvironmentDTO,
Workspace,
- WorkspaceEnv
+ WorkspaceEnv,
+ WorkspaceTag
} from './workspace/types';
diff --git a/frontend/src/hooks/api/workspace/index.tsx b/frontend/src/hooks/api/workspace/index.tsx
index 23d9e42a3..51f8c38eb 100644
--- a/frontend/src/hooks/api/workspace/index.tsx
+++ b/frontend/src/hooks/api/workspace/index.tsx
@@ -3,7 +3,8 @@ export {
useDeleteWorkspace,
useDeleteWsEnvironment,
useGetUserWorkspaces,
+ useGetWorkspaceById,
useRenameWorkspace,
useToggleAutoCapitalization,
- useUpdateWsEnvironment
-} from './queries';
+ useUpdateWsEnvironment} from './queries';
+
\ No newline at end of file
diff --git a/frontend/src/hooks/api/workspace/queries.tsx b/frontend/src/hooks/api/workspace/queries.tsx
index 650058d5d..bae1db874 100644
--- a/frontend/src/hooks/api/workspace/queries.tsx
+++ b/frontend/src/hooks/api/workspace/queries.tsx
@@ -12,15 +12,30 @@ import {
Workspace
} from './types';
+
const workspaceKeys = {
+ getWorkspaceById: (workspaceId: string) => [{ workspaceId }, 'workspace'] 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 { data } = await apiRequest.get<{ workspaces: Workspace[] }>('/api/v1/workspace');
return data.workspaces;
};
+export const useGetWorkspaceById = (workspaceId: string) => {
+ return useQuery({
+ queryKey: workspaceKeys.getWorkspaceById(workspaceId),
+ queryFn: () => fetchWorkspaceById(workspaceId),
+ enabled: true
+ });
+};
+
export const useGetUserWorkspaces = () =>
useQuery(workspaceKeys.getAllUserWorkspace, fetchUserWorkspaces);
diff --git a/frontend/src/hooks/api/workspace/types.ts b/frontend/src/hooks/api/workspace/types.ts
index fd408999b..312a811d9 100644
--- a/frontend/src/hooks/api/workspace/types.ts
+++ b/frontend/src/hooks/api/workspace/types.ts
@@ -8,6 +8,7 @@ export type Workspace = {
};
export type WorkspaceEnv = { name: string; slug: string };
+export type WorkspaceTag = { _id: string; name: string; slug: string };
// mutation dto
export type RenameWorkspaceDTO = { workspaceID: string; newWorkspaceName: string };
diff --git a/frontend/src/pages/api/integrations/authorizeIntegration.ts b/frontend/src/pages/api/integrations/authorizeIntegration.ts
index 94f1d54f5..9c70796b7 100644
--- a/frontend/src/pages/api/integrations/authorizeIntegration.ts
+++ b/frontend/src/pages/api/integrations/authorizeIntegration.ts
@@ -26,7 +26,7 @@ const AuthorizeIntegration = ({ workspaceId, code, integration }: Props) =>
})
}).then(async (res) => {
if (res && res.status === 200) {
- return res;
+ return (await res.json()).integrationAuth;
}
console.log('Failed to authorize the integration');
return undefined;
diff --git a/frontend/src/pages/api/integrations/createIntegration.ts b/frontend/src/pages/api/integrations/createIntegration.ts
index 6b37f1281..6b83d3fdc 100644
--- a/frontend/src/pages/api/integrations/createIntegration.ts
+++ b/frontend/src/pages/api/integrations/createIntegration.ts
@@ -1,7 +1,13 @@
import SecurityClient from '@app/components/utilities/SecurityClient';
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]
@@ -10,7 +16,13 @@ interface Props {
* @returns
*/
const createIntegration = ({
- integrationAuthId
+ integrationAuthId,
+ isActive,
+ app,
+ appId,
+ sourceEnvironment,
+ targetEnvironment,
+ owner
}: Props) =>
SecurityClient.fetchCall('/api/v1/integration', {
method: 'POST',
@@ -18,7 +30,13 @@ const createIntegration = ({
'Content-Type': 'application/json'
},
body: JSON.stringify({
- integrationAuthId
+ integrationAuthId,
+ isActive,
+ app,
+ appId,
+ sourceEnvironment,
+ targetEnvironment,
+ owner
})
}).then(async (res) => {
if (res && res.status === 200) {
diff --git a/frontend/src/pages/api/workspace/getWorkspaceTags.ts b/frontend/src/pages/api/workspace/getWorkspaceTags.ts
new file mode 100644
index 000000000..9a27a8f90
--- /dev/null
+++ b/frontend/src/pages/api/workspace/getWorkspaceTags.ts
@@ -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;
diff --git a/frontend/src/pages/dashboard/[id].tsx b/frontend/src/pages/dashboard/[id].tsx
index 9aa2c53e8..b9580a8f4 100644
--- a/frontend/src/pages/dashboard/[id].tsx
+++ b/frontend/src/pages/dashboard/[id].tsx
@@ -14,9 +14,11 @@ import {
faEyeSlash,
faFolderOpen,
faMagnifyingGlass,
- faPlus
+ faPlus,
+ faXmark
} from '@fortawesome/free-solid-svg-icons';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
+import { Tag } from 'public/data/frequentInterfaces';
import Button from '@app/components/basic/buttons/Button';
import ListBox from '@app/components/basic/Listbox';
@@ -44,6 +46,7 @@ import checkUserAction from '../api/userActions/checkUserAction';
import registerUserAction from '../api/userActions/registerUserAction';
import getWorkspaceEnvironments from '../api/workspace/getWorkspaceEnvironments';
import getWorkspaces from '../api/workspace/getWorkspaces';
+import getWorkspaceTags from '../api/workspace/getWorkspaceTags';
type WorkspaceEnv = {
name: string;
@@ -59,6 +62,7 @@ interface SecretDataProps {
id: string;
idOverride: string | undefined;
comment: string;
+ tags: Tag[];
}
// eslint-disable-next-line @typescript-eslint/no-unused-vars
@@ -82,6 +86,7 @@ interface SnapshotProps {
value: string;
valueOverride: string;
comment: string;
+ tags: Tag[];
}[];
}
@@ -127,6 +132,7 @@ export default function Dashboard() {
const [numSnapshots, setNumSnapshots] = useState();
const [saveLoading, setSaveLoading] = useState(false);
const [autoCapitalization, setAutoCapitalization] = useState(false);
+ const [projectTags, setProjectTags] = useState([]);
const { t } = useTranslation();
const { createNotification } = useNotificationContext();
@@ -235,6 +241,8 @@ export default function Dashboard() {
action: 'first_time_secrets_pushed'
});
setHasUserEverPushed(!!userAction);
+
+ setProjectTags(await getWorkspaceTags({ workspaceId }));
} catch (error) {
console.log('Error', error);
setData(undefined);
@@ -277,7 +285,8 @@ export default function Dashboard() {
key: '',
value: '',
valueOverride: undefined,
- comment: ''
+ comment: '',
+ tags: []
},
...data!
]);
@@ -297,7 +306,8 @@ export default function Dashboard() {
key: '',
value: '',
valueOverride: undefined,
- comment: ''
+ comment: '',
+ tags: []
},
]);
};
@@ -335,6 +345,11 @@ export default function Dashboard() {
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
const listenChangeValue = useCallback((value: string, pos: number) => {
modifyValue(value, pos);
@@ -352,6 +367,10 @@ export default function Dashboard() {
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
*/
@@ -424,7 +443,9 @@ export default function Dashboard() {
newData!.filter((dataPoint) => dataPoint.id === initDataPoint.id)[0].key !==
initDataPoint.key ||
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)
.includes(newDataPoint.id)
@@ -458,7 +479,8 @@ export default function Dashboard() {
valueOverride: override.valueOverride,
comment: '',
id: String(override.idOverride),
- idOverride: String(override.idOverride)
+ idOverride: String(override.idOverride),
+ tags: override.tags
}));
console.log('override add', overridesToBeAdded.length);
@@ -473,7 +495,9 @@ export default function Dashboard() {
newOverrides!.filter((dataPoint) => dataPoint.id === initDataPoint.id)[0].key !==
initDataPoint.key ||
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)
.includes(newDataPoint.id)
@@ -485,7 +509,8 @@ export default function Dashboard() {
valueOverride: override.valueOverride,
comment: '',
id: String(override.idOverride),
- idOverride: String(override.idOverride)
+ idOverride: String(override.idOverride),
+ tags: override.tags
}));
console.log('override update', overridesToBeUpdated.length);
@@ -546,34 +571,6 @@ export default function Dashboard() {
- {sidebarSecretId !== 'None' && (
-
- 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 && (
-
- )}
{checkDocsPopUpVisible && (
@@ -628,7 +625,10 @@ export default function Dashboard() {
togglePITSidebar(true)}
+ onButtonPressed={() => {
+ toggleSidebar('None');
+ togglePITSidebar(true)
+ }}
color="mineshaft"
size="md"
icon={faClockRotateLeft}
@@ -663,7 +663,8 @@ export default function Dashboard() {
valueOverride: sv.valueOverride,
key: sv.key,
value: sv.value,
- comment: ''
+ comment: '',
+ tags: sv.tags
}));
setData(rolledBackSecrets);
@@ -781,27 +782,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"
>
-
-
-
-
Key
- {!snapshotData &&
reorderRows(1)}
- >
- {sortMethod === 'alphabetical' ? : }
- }
+
+
+
+
0
+
Key
+ {!snapshotData &&
reorderRows(1)}
+ >
+ {sortMethod === 'alphabetical' ? : }
+ }
+
+
+
+
+
+
+
null}
+ role="none"
+ onClick={() => {}}
+ className="invisible group-hover:visible"
+ >
+
+
+
-
Value
-
Comment
- {!snapshotData &&
}
{!snapshotData &&
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))
.map((keyPair) => (
item.key))?.includes(
keyPair.key
)}
toggleSidebar={toggleSidebar}
+ togglePITSidebar={togglePITSidebar}
sidebarSecretId={sidebarSecretId}
isSnapshot={false}
deleteRow={deleteCertainRow}
+ tags={projectTags}
/>
))}
{snapshotData &&
@@ -852,6 +887,7 @@ export default function Dashboard() {
modifyValueOverride={listenChangeValueOverride}
modifyKey={listenChangeKey}
modifyComment={listenChangeComment}
+ modifyTags={listenChangeTags}
isBlurred={blurred}
isDuplicate={findDuplicates(data?.map((item) => item.key))?.includes(
keyPair.key
@@ -859,6 +895,7 @@ export default function Dashboard() {
toggleSidebar={toggleSidebar}
sidebarSecretId={sidebarSecretId}
isSnapshot
+ tags={projectTags}
/>
))}
@@ -912,6 +949,34 @@ export default function Dashboard() {
)}
+ {sidebarSecretId !== 'None' && (
+
+ 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 && (
+
+ )}
) : (
diff --git a/frontend/src/pages/github.tsx b/frontend/src/pages/github.tsx
deleted file mode 100644
index 897027246..000000000
--- a/frontend/src/pages/github.tsx
+++ /dev/null
@@ -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
;
-}
-
-Github.requireAuth = true;
diff --git a/frontend/src/pages/heroku.tsx b/frontend/src/pages/heroku.tsx
deleted file mode 100644
index ff4f2263e..000000000
--- a/frontend/src/pages/heroku.tsx
+++ /dev/null
@@ -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
;
-}
-
-Heroku.requireAuth = true;
diff --git a/frontend/src/pages/integrations/[id].tsx b/frontend/src/pages/integrations/[id].tsx
index a4bf83a89..d802e81a8 100644
--- a/frontend/src/pages/integrations/[id].tsx
+++ b/frontend/src/pages/integrations/[id].tsx
@@ -7,7 +7,6 @@ import { useTranslation } from 'next-i18next';
import frameworkIntegrationOptions from 'public/json/frameworkIntegrations.json';
import ActivateBotDialog from '@app/components/basic/dialog/ActivateBotDialog';
-import IntegrationAccessTokenDialog from '@app/components/basic/dialog/IntegrationAccessTokenDialog';
import CloudIntegrationSection from '@app/components/integrations/CloudIntegrationSection';
import FrameworkIntegrationSection from '@app/components/integrations/FrameworkIntegrationSection';
import IntegrationSection from '@app/components/integrations/IntegrationSection';
@@ -20,12 +19,10 @@ import {
} from '../../components/utilities/cryptography/crypto';
import getBot from '../api/bot/getBot';
import setBotActiveStatus from '../api/bot/setBotActiveStatus';
-import createIntegration from '../api/integrations/createIntegration';
import deleteIntegration from '../api/integrations/DeleteIntegration';
import getIntegrationOptions from '../api/integrations/GetIntegrationOptions';
import getWorkspaceAuthorizations from '../api/integrations/getWorkspaceAuthorizations';
import getWorkspaceIntegrations from '../api/integrations/getWorkspaceIntegrations';
-import saveIntegrationAccessToken from '../api/integrations/saveIntegrationAccessToken';
import getAWorkspace from '../api/workspace/getAWorkspace';
import getLatestFileKey from '../api/workspace/getLatestFileKey';
@@ -52,6 +49,7 @@ interface Integration {
}
interface IntegrationOption {
+ tenantId?: string;
clientId: string;
clientSlug?: string; // vercel-integration specific
docsLink: string;
@@ -75,7 +73,6 @@ export default function Integrations() {
// TODO: These will have its type when migratiing towards react-query
const [bot, setBot] = useState(null);
const [isActivateBotDialogOpen, setIsActivateBotDialogOpen] = useState(false);
- const [isIntegrationAccessTokenDialogOpen, setIntegrationAccessTokenDialogOpen] = useState(false);
const [selectedIntegrationOption, setSelectedIntegrationOption] = useState(null);
const router = useRouter();
@@ -166,80 +163,83 @@ export default function Integrations() {
}
};
- /**
- * 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;
- }) => {
+ const handleUnauthorizedIntegrationOptionPress = (integrationOption: IntegrationOption) => {
try {
- if (!bot.isActive) {
- await handleBotActivate();
+ // generate CSRF token for OAuth2 code-token exchange integrations
+ 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') {
- // integration is of type OAuth
- // 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;
+ if (link !== '') {
+ window.location.assign(link);
}
} catch (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.
@@ -251,34 +251,20 @@ export default function Integrations() {
* @returns
*/
const integrationOptionPress = async (integrationOption: IntegrationOption) => {
- // consider: don't start integration until at [handleIntegrationOption] step
try {
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) {
await handleBotActivate();
}
- // case: integration has been authorized before
- // -> create new integration
- const integration = await createIntegration({
- integrationAuthId: integrationAuthX._id
- });
- setIntegrations([...integrations, integration]);
+ if (!integrationAuthX) {
+ // case: integration has not been authorized
+ handleUnauthorizedIntegrationOptionPress(integrationOption);
+ return;
+ }
+
+ handleAuthorizedIntegrationOptionPress(integrationAuthX);
} catch (err) {
console.error(err);
}
@@ -360,12 +346,6 @@ export default function Integrations() {
selectedIntegrationOption={selectedIntegrationOption}
integrationOptionPress={integrationOptionPress}
/>
- setIntegrationAccessTokenDialogOpen(false)}
- selectedIntegrationOption={selectedIntegrationOption}
- handleIntegrationOption={handleIntegrationOption}
- />
{
+ 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.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) ? (
+
+
+ Azure Key Vault Integration
+
+ setSelectedSourceEnvironment(val)}
+ className='w-full border border-mineshaft-500'
+ >
+ {workspace?.environments.map((sourceEnvironment) => (
+
+ {sourceEnvironment.name}
+
+ ))}
+
+
+
+ setVaultBaseUrl(e.target.value)}
+ />
+
+
+ Create Integration
+
+
+
+ ) :
+}
+
+AzureKeyVaultCreateIntegrationPage.requireAuth = true;
+
+export const getServerSideProps = getTranslatedServerSideProps(['integrations']);
\ No newline at end of file
diff --git a/frontend/src/pages/integrations/azure-key-vault/oauth2/callback.tsx b/frontend/src/pages/integrations/azure-key-vault/oauth2/callback.tsx
new file mode 100644
index 000000000..14d92d9c2
--- /dev/null
+++ b/frontend/src/pages/integrations/azure-key-vault/oauth2/callback.tsx
@@ -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
+}
+
+AzureKeyVaultOAuth2CallbackPage.requireAuth = true;
+
+export const getServerSideProps = getTranslatedServerSideProps(['integrations']);
\ No newline at end of file
diff --git a/frontend/src/pages/integrations/flyio/authorize.tsx b/frontend/src/pages/integrations/flyio/authorize.tsx
new file mode 100644
index 000000000..5b00e562a
--- /dev/null
+++ b/frontend/src/pages/integrations/flyio/authorize.tsx
@@ -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 (
+
+
+ Render Integration
+
+ setAccessToken(e.target.value)}
+ />
+
+
+ Connect to Fly.io
+
+
+
+ )
+}
+
+FlyioCreateIntegrationPage.requireAuth = true;
+
+export const getServerSideProps = getTranslatedServerSideProps(['integrations']);
\ No newline at end of file
diff --git a/frontend/src/pages/integrations/flyio/create.tsx b/frontend/src/pages/integrations/flyio/create.tsx
new file mode 100644
index 000000000..2e185d9fe
--- /dev/null
+++ b/frontend/src/pages/integrations/flyio/create.tsx
@@ -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) ? (
+
+
+ Fly.io Integration
+
+ setSelectedSourceEnvironment(val)}
+ className='w-full border border-mineshaft-500'
+ >
+ {workspace?.environments.map((sourceEnvironment) => (
+
+ {sourceEnvironment.name}
+
+ ))}
+
+
+
+ setTargetApp(val)}
+ className='w-full border border-mineshaft-500'
+ >
+ {integrationAuthApps.map((integrationAuthApp) => (
+
+ {integrationAuthApp.name}
+
+ ))}
+
+
+
+ Create Integration
+
+
+
+ ) :
+}
+
+FlyioCreateIntegrationPage.requireAuth = true;
+
+export const getServerSideProps = getTranslatedServerSideProps(['integrations']);
\ No newline at end of file
diff --git a/frontend/src/pages/integrations/github/create.tsx b/frontend/src/pages/integrations/github/create.tsx
new file mode 100644
index 000000000..3b39fa2d1
--- /dev/null
+++ b/frontend/src/pages/integrations/github/create.tsx
@@ -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(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) ? (
+
+
+ GitHub Integration
+
+ setSelectedSourceEnvironment(val)}
+ className='w-full border border-mineshaft-500'
+ >
+ {workspace?.environments.map((sourceEnvironment) => (
+
+ {sourceEnvironment.name}
+
+ ))}
+
+
+
+ setTargetApp(val)}
+ className='w-full border border-mineshaft-500'
+ >
+ {integrationAuthApps.map((integrationAuthApp) => (
+
+ {integrationAuthApp.name}
+
+ ))}
+
+
+
+ Create Integration
+
+
+
+ ) :
+}
+
+GitHubCreateIntegrationPage.requireAuth = true;
+
+export const getServerSideProps = getTranslatedServerSideProps(['integrations']);
\ No newline at end of file
diff --git a/frontend/src/pages/integrations/github/oauth2/callback.tsx b/frontend/src/pages/integrations/github/oauth2/callback.tsx
new file mode 100644
index 000000000..a3fd7e84c
--- /dev/null
+++ b/frontend/src/pages/integrations/github/oauth2/callback.tsx
@@ -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
+}
+
+GitHubOAuth2CallbackPage.requireAuth = true;
+
+export const getServerSideProps = getTranslatedServerSideProps(['integrations']);
\ No newline at end of file
diff --git a/frontend/src/pages/integrations/heroku/create.tsx b/frontend/src/pages/integrations/heroku/create.tsx
new file mode 100644
index 000000000..46e379462
--- /dev/null
+++ b/frontend/src/pages/integrations/heroku/create.tsx
@@ -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) ? (
+
+
+ Heroku Integration
+
+ setSelectedSourceEnvironment(val)}
+ className='w-full border border-mineshaft-500'
+ >
+ {workspace?.environments.map((sourceEnvironment) => (
+
+ {sourceEnvironment.name}
+
+ ))}
+
+
+
+ setTargetApp(val)}
+ className='w-full border border-mineshaft-500'
+ >
+ {integrationAuthApps.map((integrationAuthApp) => (
+
+ {integrationAuthApp.name}
+
+ ))}
+
+
+
+ Create Integration
+
+
+
+ ) :
+}
+
+HerokuCreateIntegrationPage.requireAuth = true;
+
+export const getServerSideProps = getTranslatedServerSideProps(['integrations']);
\ No newline at end of file
diff --git a/frontend/src/pages/integrations/heroku/oauth2/callback.tsx b/frontend/src/pages/integrations/heroku/oauth2/callback.tsx
new file mode 100644
index 000000000..321255c9f
--- /dev/null
+++ b/frontend/src/pages/integrations/heroku/oauth2/callback.tsx
@@ -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
+}
+
+HerokuOAuth2CallbackPage.requireAuth = true;
+
+export const getServerSideProps = getTranslatedServerSideProps(['integrations']);
\ No newline at end of file
diff --git a/frontend/src/pages/integrations/netlify/create.tsx b/frontend/src/pages/integrations/netlify/create.tsx
new file mode 100644
index 000000000..a8b6bcd05
--- /dev/null
+++ b/frontend/src/pages/integrations/netlify/create.tsx
@@ -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) ? (
+
+
+ Netlify Integration
+
+ setSelectedSourceEnvironment(val)}
+ className='w-full border border-mineshaft-500'
+ >
+ {workspace?.environments.map((sourceEnvironment) => (
+
+ {sourceEnvironment.name}
+
+ ))}
+
+
+
+ setTargetApp(val)}
+ className='w-full border border-mineshaft-500'
+ >
+ {integrationAuthApps.map((integrationAuthApp) => (
+
+ {integrationAuthApp.name}
+
+ ))}
+
+
+
+ setTargetEnvironment(val)}
+ className='w-full border border-mineshaft-500'
+ >
+ {netlifyEnvironments.map((netlifyEnvironment) => (
+
+ {netlifyEnvironment.name}
+
+ ))}
+
+
+
+ Create Integration
+
+
+
+ ) :
+}
+
+NetlifyCreateIntegrationPage.requireAuth = true;
+
+export const getServerSideProps = getTranslatedServerSideProps(['integrations']);
\ No newline at end of file
diff --git a/frontend/src/pages/integrations/netlify/oauth2/callback.tsx b/frontend/src/pages/integrations/netlify/oauth2/callback.tsx
new file mode 100644
index 000000000..db884282a
--- /dev/null
+++ b/frontend/src/pages/integrations/netlify/oauth2/callback.tsx
@@ -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
+}
+
+NetlifyOAuth2CallbackPage.requireAuth = true;
+
+export const getServerSideProps = getTranslatedServerSideProps(['integrations']);
\ No newline at end of file
diff --git a/frontend/src/pages/integrations/render/authorize.tsx b/frontend/src/pages/integrations/render/authorize.tsx
new file mode 100644
index 000000000..bd7a877c3
--- /dev/null
+++ b/frontend/src/pages/integrations/render/authorize.tsx
@@ -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 (
+
+
+ Render Integration
+
+ setApiKey(e.target.value)}
+ />
+
+
+ Connect to Render
+
+
+
+ )
+}
+
+RenderCreateIntegrationPage.requireAuth = true;
+
+export const getServerSideProps = getTranslatedServerSideProps(['integrations']);
\ No newline at end of file
diff --git a/frontend/src/pages/integrations/render/create.tsx b/frontend/src/pages/integrations/render/create.tsx
new file mode 100644
index 000000000..3429def89
--- /dev/null
+++ b/frontend/src/pages/integrations/render/create.tsx
@@ -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) ? (
+
+
+ Render Integration
+
+ setSelectedSourceEnvironment(val)}
+ className='w-full border border-mineshaft-500'
+ >
+ {workspace?.environments.map((sourceEnvironment) => (
+
+ {sourceEnvironment.name}
+
+ ))}
+
+
+
+ setTargetApp(val)}
+ className='w-full border border-mineshaft-500'
+ >
+ {integrationAuthApps.map((integrationAuthApp) => (
+
+ {integrationAuthApp.name}
+
+ ))}
+
+
+
+ Create Integration
+
+
+
+ ) :
+}
+
+RenderCreateIntegrationPage.requireAuth = true;
+
+export const getServerSideProps = getTranslatedServerSideProps(['integrations']);
\ No newline at end of file
diff --git a/frontend/src/pages/integrations/vercel/create.tsx b/frontend/src/pages/integrations/vercel/create.tsx
new file mode 100644
index 000000000..5e0e8ad7e
--- /dev/null
+++ b/frontend/src/pages/integrations/vercel/create.tsx
@@ -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) ? (
+
+
+ Vercel Integration
+
+ setSelectedSourceEnvironment(val)}
+ className='w-full border border-mineshaft-500'
+ >
+ {workspace?.environments.map((sourceEnvironment) => (
+
+ {sourceEnvironment.name}
+
+ ))}
+
+
+
+ setTargetApp(val)}
+ className='w-full border border-mineshaft-500'
+ >
+ {integrationAuthApps.map((integrationAuthApp) => (
+
+ {integrationAuthApp.name}
+
+ ))}
+
+
+
+ setTargetEnvironemnt(val)}
+ className='w-full border border-mineshaft-500'
+ >
+ {vercelEnvironments.map((vercelEnvironment) => (
+
+ {vercelEnvironment.name}
+
+ ))}
+
+
+
+ Create Integration
+
+
+
+ ) :
+}
+
+VercelCreateIntegrationPage.requireAuth = true;
+
+export const getServerSideProps = getTranslatedServerSideProps(['integrations']);
\ No newline at end of file
diff --git a/frontend/src/pages/integrations/vercel/oauth2/callback.tsx b/frontend/src/pages/integrations/vercel/oauth2/callback.tsx
new file mode 100644
index 000000000..1acf01b02
--- /dev/null
+++ b/frontend/src/pages/integrations/vercel/oauth2/callback.tsx
@@ -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
+}
+
+VercelOAuth2CallbackPage.requireAuth = true;
+
+export const getServerSideProps = getTranslatedServerSideProps(['integrations']);
\ No newline at end of file
diff --git a/frontend/src/pages/netlify.tsx b/frontend/src/pages/netlify.tsx
deleted file mode 100644
index 532d6044a..000000000
--- a/frontend/src/pages/netlify.tsx
+++ /dev/null
@@ -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
;
-}
-
-Netlify.requireAuth = true;
diff --git a/frontend/src/pages/vercel.tsx b/frontend/src/pages/vercel.tsx
deleted file mode 100644
index 8e8245736..000000000
--- a/frontend/src/pages/vercel.tsx
+++ /dev/null
@@ -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
;
-}
-
-Vercel.requireAuth = true;
diff --git a/frontend/src/views/Settings/ProjectSettingsPage/ProjectSettingsPage.tsx b/frontend/src/views/Settings/ProjectSettingsPage/ProjectSettingsPage.tsx
index da89e9882..1b30eb4e9 100644
--- a/frontend/src/views/Settings/ProjectSettingsPage/ProjectSettingsPage.tsx
+++ b/frontend/src/views/Settings/ProjectSettingsPage/ProjectSettingsPage.tsx
@@ -18,21 +18,29 @@ import { useToggle } from '@app/hooks';
import {
useCreateServiceToken,
useCreateWsEnvironment,
+ useCreateWsTag,
useDeleteServiceToken,
useDeleteWorkspace,
useDeleteWsEnvironment,
+ useDeleteWsTag,
useGetUserWsKey,
useGetUserWsServiceTokens,
+ useGetWsTags,
useRenameWorkspace,
useToggleAutoCapitalization,
useUpdateWsEnvironment
} from '@app/hooks/api';
+
import { AutoCapitalizationSection } from './components/AutoCapitalizationSection/AutoCapitalizationSection';
+
+import { SecretTagsSection } from './components/SecretTagsSection';
+
import {
CopyProjectIDSection,
CreateServiceToken,
CreateUpdateEnvFormData,
+ CreateWsTag,
EnvironmentSection,
ProjectNameChangeSection,
ServiceTokenSection
@@ -65,6 +73,11 @@ export const ProjectSettingsPage = () => {
const createServiceToken = useCreateServiceToken();
const deleteServiceToken = useDeleteServiceToken();
+ // tag
+ const { data: wsTags } = useGetWsTags(workspaceID);
+ const createWsTag = useCreateWsTag();
+ const deleteWsTag = useDeleteWsTag();
+
// get user subscription
const { subscriptionPlan } = useSubscription();
const host = window.location.origin;
@@ -231,6 +244,44 @@ export const ProjectSettingsPage = () => {
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) => {
try {
await deleteServiceToken.mutateAsync(tokenID);
@@ -248,7 +299,7 @@ export const ProjectSettingsPage = () => {
};
return (
-
+
{/* TODO(akhilmhdh): Remove this right when layout is refactored */}
@@ -284,6 +335,12 @@ export const ProjectSettingsPage = () => {
workspaceName={currentWorkspace?.name || ''}
onCreateToken={onCreateServiceToken}
/>
+
{t('settings-project:danger-zone')}
{t('settings-project:danger-zone-note')}
diff --git a/frontend/src/views/Settings/ProjectSettingsPage/components/EnvironmentSection/EnvironmentSection.tsx b/frontend/src/views/Settings/ProjectSettingsPage/components/EnvironmentSection/EnvironmentSection.tsx
index 521394f9f..ddcf7f4b3 100644
--- a/frontend/src/views/Settings/ProjectSettingsPage/components/EnvironmentSection/EnvironmentSection.tsx
+++ b/frontend/src/views/Settings/ProjectSettingsPage/components/EnvironmentSection/EnvironmentSection.tsx
@@ -102,7 +102,7 @@ export const EnvironmentSection = ({
colorSchema="primary"
leftIcon={
}
>
- Add New Env
+ Add New Environment
diff --git a/frontend/src/views/Settings/ProjectSettingsPage/components/SecretTagsSection/SecretTagsSection.tsx b/frontend/src/views/Settings/ProjectSettingsPage/components/SecretTagsSection/SecretTagsSection.tsx
new file mode 100644
index 000000000..6aa4d26be
--- /dev/null
+++ b/frontend/src/views/Settings/ProjectSettingsPage/components/SecretTagsSection/SecretTagsSection.tsx
@@ -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
;
+
+type Props = {
+ tags: WorkspaceTag[];
+ workspaceName: string;
+ onDeleteTag: (tagID: string) => Promise;
+ onCreateTag: (data: CreateWsTag) => Promise;
+};
+
+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({
+ 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 (
+
+
+
+
Secret Tags
+
Every secret can be assigned to one or more tags. Here you can add and remove tags for the current project.
+
+
+
{
+ handlePopUpToggle('CreateSecretTag', open);
+ reset();
+ }}
+ >
+
+ }>
+ Add New Tag
+
+
+
+
+
+
+
+
+
+
+
+
+ Tag
+ Slug
+
+
+
+
+ {tags?.length > 0 ? (
+ tags.map(({ _id, name, slug }) => (
+
+ {name}
+ {slug}
+
+
+ handlePopUpOpen('deleteTagConfirmation', {
+ name,
+ id: _id
+ })
+ }
+ colorSchema="danger"
+ ariaLabel="update"
+ >
+
+
+
+
+ ))
+ ) : (
+
+
+ No tags found for this project
+
+
+ )}
+
+
+
+
handlePopUpToggle('deleteTagConfirmation', isOpen)}
+ deleteKey={(popUp?.deleteTagConfirmation?.data as DeleteModalData)?.name}
+ onClose={() => handlePopUpClose('deleteTagConfirmation')}
+ onDeleteApproved={onDeleteApproved}
+ />
+
+ );
+};
diff --git a/frontend/src/views/Settings/ProjectSettingsPage/components/SecretTagsSection/index.tsx b/frontend/src/views/Settings/ProjectSettingsPage/components/SecretTagsSection/index.tsx
new file mode 100644
index 000000000..0d0615293
--- /dev/null
+++ b/frontend/src/views/Settings/ProjectSettingsPage/components/SecretTagsSection/index.tsx
@@ -0,0 +1 @@
+export {SecretTagsSection} from './SecretTagsSection'
\ No newline at end of file
diff --git a/frontend/src/views/Settings/ProjectSettingsPage/components/index.tsx b/frontend/src/views/Settings/ProjectSettingsPage/components/index.tsx
index 163483278..98160b5b7 100644
--- a/frontend/src/views/Settings/ProjectSettingsPage/components/index.tsx
+++ b/frontend/src/views/Settings/ProjectSettingsPage/components/index.tsx
@@ -2,5 +2,6 @@ export { CopyProjectIDSection } from './CopyProjectIDSection';
export { EnvironmentSection } from './EnvironmentSection';
export type { CreateUpdateEnvFormData } from './EnvironmentSection/EnvironmentSection';
export { ProjectNameChangeSection } from './ProjectNameChangeSection';
+export type { CreateWsTag } from './SecretTagsSection/SecretTagsSection';
export { ServiceTokenSection } from './ServiceTokenSection';
export type { CreateServiceToken } from './ServiceTokenSection/ServiceTokenSection';