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

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