Merge branch 'main' into auto-capitalization-toggle

This commit is contained in:
Hasan Mansoor
2023-02-08 05:23:41 +03:00
committed by GitHub
82 changed files with 2924 additions and 682 deletions

View File

@@ -171,7 +171,9 @@ We're currently setting the foundation and building [integrations](https://infis
🔜 GCP SM (https://github.com/Infisical/infisical/issues/285)
</td>
<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 align="left" valign="middle">
🔜 CircleCI (https://github.com/Infisical/infisical/issues/91)

View File

@@ -10,13 +10,13 @@
"license": "ISC",
"dependencies": {
"@godaddy/terminus": "^4.11.2",
"@sentry/node": "^7.21.1",
"@octokit/rest": "^19.0.5",
"@sentry/tracing": "^7.21.1",
"@sentry/node": "^7.14.0",
"@sentry/tracing": "^7.19.0",
"@types/crypto-js": "^4.1.1",
"axios": "^1.2.0",
"@types/libsodium-wrappers": "^0.7.10",
"await-to-js": "^3.0.0",
"axios": "^1.1.3",
"bcrypt": "^5.1.0",
"bigint-conversion": "^2.2.2",
"builder-pattern": "^2.2.0",
@@ -32,9 +32,9 @@
"js-yaml": "^4.1.0",
"jsonwebtoken": "^9.0.0",
"jsrp": "^0.2.4",
"mongoose": "^6.7.3",
"libsodium-wrappers": "^0.7.10",
"lodash": "^4.17.21",
"mongoose": "^6.7.2",
"nodemailer": "^6.8.0",
"posthog-node": "^2.2.2",
"query-string": "^7.1.3",
@@ -2838,19 +2838,6 @@
"@maxmind/geoip2-node": "^3.4.0"
}
},
"node_modules/@sentry/core": {
"version": "7.21.1",
"resolved": "https://registry.npmjs.org/@sentry/core/-/core-7.21.1.tgz",
"integrity": "sha512-Og5wEEsy24fNvT/T7IKjcV4EvVK5ryY2kxbJzKY6GU2eX+i+aBl+n/vp7U0Es351C/AlTkS+0NOUsp2TQQFxZA==",
"dependencies": {
"@sentry/types": "7.21.1",
"@sentry/utils": "7.21.1",
"tslib": "^1.9.3"
},
"engines": {
"node": ">=8"
}
},
"node_modules/@protobufjs/aspromise": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/@protobufjs/aspromise/-/aspromise-1.1.2.tgz",
@@ -2905,27 +2892,10 @@
"resolved": "https://registry.npmjs.org/@protobufjs/utf8/-/utf8-1.1.0.tgz",
"integrity": "sha512-Vvn3zZrhQZkkBE8LSuW3em98c0FwgO4nxzv6OdSxPKJIEKY2bGbHn+mhGIPerzI4twdxaP8/0+06HBpwf345Lw=="
},
"node_modules/@sentry/node": {
"version": "7.19.0",
"resolved": "https://registry.npmjs.org/@sentry/node/-/node-7.19.0.tgz",
"integrity": "sha512-yG7Tx32WqOkEHVotFLrumCcT9qlaSDTkFNZ+yLSvZXx74ifsE781DzBA9W7K7bBdYO3op+p2YdsOKzf3nPpAyQ==",
"dependencies": {
"@sentry/core": "7.19.0",
"@sentry/types": "7.19.0",
"@sentry/utils": "7.19.0",
"cookie": "^0.4.1",
"https-proxy-agent": "^5.0.0",
"lru_map": "^0.3.3",
"tslib": "^1.9.3"
},
"engines": {
"node": ">=8"
}
},
"node_modules/@sentry/node/node_modules/@sentry/core": {
"version": "7.19.0",
"resolved": "https://registry.npmjs.org/@sentry/core/-/core-7.19.0.tgz",
"integrity": "sha512-YF9cTBcAnO4R44092BJi5Wa2/EO02xn2ziCtmNgAVTN2LD31a/YVGxGBt/FDr4Y6yeuVehaqijVVvtpSmXrGJw==",
"node_modules/@sentry/core": {
"version": "7.21.1",
"resolved": "https://registry.npmjs.org/@sentry/core/-/core-7.21.1.tgz",
"integrity": "sha512-Og5wEEsy24fNvT/T7IKjcV4EvVK5ryY2kxbJzKY6GU2eX+i+aBl+n/vp7U0Es351C/AlTkS+0NOUsp2TQQFxZA==",
"dependencies": {
"@sentry/types": "7.21.1",
"@sentry/utils": "7.21.1",
@@ -2986,26 +2956,6 @@
"node": ">=8"
}
},
"node_modules/@sentry/types": {
"version": "7.21.1",
"resolved": "https://registry.npmjs.org/@sentry/types/-/types-7.21.1.tgz",
"integrity": "sha512-3/IKnd52Ol21amQvI+kz+WB76s8/LR5YvFJzMgIoI2S8d82smIr253zGijRXxHPEif8kMLX4Yt+36VzrLxg6+A==",
"engines": {
"node": ">=8"
}
},
"node_modules/@sentry/utils": {
"version": "7.21.1",
"resolved": "https://registry.npmjs.org/@sentry/utils/-/utils-7.21.1.tgz",
"integrity": "sha512-F0W0AAi8tgtTx6ApZRI2S9HbXEA9ENX1phTZgdNNWcMFm1BNbc21XEwLqwXBNjub5nlA6CE8xnjXRgdZKx4kzQ==",
"dependencies": {
"@sentry/types": "7.21.1",
"tslib": "^1.9.3"
},
"engines": {
"node": ">=8"
}
},
"node_modules/@sinclair/typebox": {
"version": "0.24.51",
"resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.24.51.tgz",
@@ -14306,16 +14256,6 @@
"@maxmind/geoip2-node": "^3.4.0"
}
},
"@sentry/core": {
"version": "7.21.1",
"resolved": "https://registry.npmjs.org/@sentry/core/-/core-7.21.1.tgz",
"integrity": "sha512-Og5wEEsy24fNvT/T7IKjcV4EvVK5ryY2kxbJzKY6GU2eX+i+aBl+n/vp7U0Es351C/AlTkS+0NOUsp2TQQFxZA==",
"requires": {
"@sentry/types": "7.21.1",
"@sentry/utils": "7.21.1",
"tslib": "^1.9.3"
}
},
"@protobufjs/aspromise": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/@protobufjs/aspromise/-/aspromise-1.1.2.tgz",
@@ -14370,6 +14310,16 @@
"resolved": "https://registry.npmjs.org/@protobufjs/utf8/-/utf8-1.1.0.tgz",
"integrity": "sha512-Vvn3zZrhQZkkBE8LSuW3em98c0FwgO4nxzv6OdSxPKJIEKY2bGbHn+mhGIPerzI4twdxaP8/0+06HBpwf345Lw=="
},
"@sentry/core": {
"version": "7.21.1",
"resolved": "https://registry.npmjs.org/@sentry/core/-/core-7.21.1.tgz",
"integrity": "sha512-Og5wEEsy24fNvT/T7IKjcV4EvVK5ryY2kxbJzKY6GU2eX+i+aBl+n/vp7U0Es351C/AlTkS+0NOUsp2TQQFxZA==",
"requires": {
"@sentry/types": "7.21.1",
"@sentry/utils": "7.21.1",
"tslib": "^1.9.3"
}
},
"@sentry/node": {
"version": "7.21.1",
"resolved": "https://registry.npmjs.org/@sentry/node/-/node-7.21.1.tgz",
@@ -14409,20 +14359,6 @@
"tslib": "^1.9.3"
}
},
"@sentry/types": {
"version": "7.21.1",
"resolved": "https://registry.npmjs.org/@sentry/types/-/types-7.21.1.tgz",
"integrity": "sha512-3/IKnd52Ol21amQvI+kz+WB76s8/LR5YvFJzMgIoI2S8d82smIr253zGijRXxHPEif8kMLX4Yt+36VzrLxg6+A=="
},
"@sentry/utils": {
"version": "7.21.1",
"resolved": "https://registry.npmjs.org/@sentry/utils/-/utils-7.21.1.tgz",
"integrity": "sha512-F0W0AAi8tgtTx6ApZRI2S9HbXEA9ENX1phTZgdNNWcMFm1BNbc21XEwLqwXBNjub5nlA6CE8xnjXRgdZKx4kzQ==",
"requires": {
"@sentry/types": "7.21.1",
"tslib": "^1.9.3"
}
},
"@sinclair/typebox": {
"version": "0.24.51",
"resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.24.51.tgz",

View File

@@ -13,10 +13,13 @@ const MONGO_URL = process.env.MONGO_URL!;
const NODE_ENV = process.env.NODE_ENV! || 'production';
const VERBOSE_ERROR_OUTPUT = process.env.VERBOSE_ERROR_OUTPUT! === 'true' && true;
const LOKI_HOST = process.env.LOKI_HOST || undefined;
const CLIENT_ID_AZURE = process.env.CLIENT_ID_AZURE!;
const TENANT_ID_AZURE = process.env.TENANT_ID_AZURE!;
const CLIENT_ID_HEROKU = process.env.CLIENT_ID_HEROKU!;
const CLIENT_ID_VERCEL = process.env.CLIENT_ID_VERCEL!;
const CLIENT_ID_NETLIFY = process.env.CLIENT_ID_NETLIFY!;
const CLIENT_ID_GITHUB = process.env.CLIENT_ID_GITHUB!;
const CLIENT_SECRET_AZURE = process.env.CLIENT_SECRET_AZURE!;
const CLIENT_SECRET_HEROKU = process.env.CLIENT_SECRET_HEROKU!;
const CLIENT_SECRET_VERCEL = process.env.CLIENT_SECRET_VERCEL!;
const CLIENT_SECRET_NETLIFY = process.env.CLIENT_SECRET_NETLIFY!;
@@ -60,10 +63,13 @@ export {
NODE_ENV,
VERBOSE_ERROR_OUTPUT,
LOKI_HOST,
CLIENT_ID_AZURE,
TENANT_ID_AZURE,
CLIENT_ID_HEROKU,
CLIENT_ID_VERCEL,
CLIENT_ID_NETLIFY,
CLIENT_ID_GITHUB,
CLIENT_SECRET_AZURE,
CLIENT_SECRET_HEROKU,
CLIENT_SECRET_VERCEL,
CLIENT_SECRET_NETLIFY,

View File

@@ -10,6 +10,31 @@ import { INTEGRATION_SET, INTEGRATION_OPTIONS } from '../../variables';
import { IntegrationService } from '../../services';
import { getApps, revokeAccess } from '../../integrations';
/***
* Return integration authorization with id [integrationAuthId]
*/
export const getIntegrationAuth = async (req: Request, res: Response) => {
let integrationAuth;
try {
const { integrationAuthId } = req.params;
integrationAuth = await IntegrationAuth.findById(integrationAuthId);
if (!integrationAuth) return res.status(400).send({
message: 'Failed to find integration authorization'
});
} catch (err) {
Sentry.setUser({ email: req.user.email });
Sentry.captureException(err);
return res.status(400).send({
message: 'Failed to get integration authorization'
});
}
return res.status(200).send({
integrationAuth
});
}
export const getIntegrationOptions = async (
req: Request,
res: Response
@@ -31,7 +56,6 @@ export const oAuthExchange = async (
) => {
try {
const { workspaceId, code, integration } = req.body;
if (!INTEGRATION_SET.has(integration))
throw new Error('Failed to validate integration');
@@ -40,12 +64,16 @@ export const oAuthExchange = async (
throw new Error("Failed to get environments")
}
await IntegrationService.handleOAuthExchange({
const integrationAuth = await IntegrationService.handleOAuthExchange({
workspaceId,
integration,
code,
environment: environments[0].slug,
});
return res.status(200).send({
integrationAuth
});
} catch (err) {
Sentry.setUser({ email: req.user.email });
Sentry.captureException(err);
@@ -53,10 +81,6 @@ export const oAuthExchange = async (
message: 'Failed to get OAuth2 code-token exchange'
});
}
return res.status(200).send({
message: 'Successfully enabled integration authorization'
});
};
/**
@@ -81,6 +105,13 @@ export const saveIntegrationAccessToken = async (
integration: string;
} = req.body;
const bot = await Bot.findOne({
workspace: new Types.ObjectId(workspaceId),
isActive: true
});
if (!bot) throw new Error('Bot must be enabled to save integration access token');
integrationAuth = await IntegrationAuth.findOneAndUpdate({
workspace: new Types.ObjectId(workspaceId),
integration
@@ -91,13 +122,6 @@ export const saveIntegrationAccessToken = async (
new: true,
upsert: true
});
const bot = await Bot.findOne({
workspace: new Types.ObjectId(workspaceId),
isActive: true
});
if (!bot) throw new Error('Bot must be enabled to save integration access token');
// encrypt and save integration access token
integrationAuth = await IntegrationService.setIntegrationAuthAccess({

View File

@@ -1,4 +1,5 @@
import { Request, Response } from 'express';
import { Types } from 'mongoose';
import * as Sentry from '@sentry/node';
import {
Integration,
@@ -18,15 +19,40 @@ import { eventPushSecrets } from '../../events';
export const createIntegration = async (req: Request, res: Response) => {
let integration;
try {
const {
integrationAuthId,
app,
appId,
isActive,
sourceEnvironment,
targetEnvironment,
owner
} = req.body;
// TODO: validate [sourceEnvironment] and [targetEnvironment]
// initialize new integration after saving integration access token
integration = await new Integration({
workspace: req.integrationAuth.workspace._id,
isActive: false,
app: null,
environment: req.integrationAuth.workspace?.environments[0].slug,
environment: sourceEnvironment,
isActive,
app,
appId,
targetEnvironment,
owner,
integration: req.integrationAuth.integration,
integrationAuth: req.integrationAuth._id
integrationAuth: new Types.ObjectId(integrationAuthId)
}).save();
if (integration) {
// trigger event - push secrets
EventService.handleEvent({
event: eventPushSecrets({
workspaceId: integration.workspace.toString()
})
});
}
} catch (err) {
Sentry.setUser({ email: req.user.email });
Sentry.captureException(err);

View File

@@ -103,6 +103,9 @@ export const createSecrets = async (req: Request, res: Response) => {
secretValueCiphertext: string;
secretValueIV: string;
secretValueTag: string;
secretCommentCiphertext: string;
secretCommentIV: string;
secretCommentTag: string;
tags: string[]
}
@@ -115,6 +118,9 @@ export const createSecrets = async (req: Request, res: Response) => {
secretValueCiphertext,
secretValueIV,
secretValueTag,
secretCommentCiphertext,
secretCommentIV,
secretCommentTag,
tags
}: secretsToCreateType) => {
return ({
@@ -129,6 +135,9 @@ export const createSecrets = async (req: Request, res: Response) => {
secretValueCiphertext,
secretValueIV,
secretValueTag,
secretCommentCiphertext,
secretCommentIV,
secretCommentTag,
tags
});
})
@@ -160,6 +169,9 @@ export const createSecrets = async (req: Request, res: Response) => {
secretValueIV,
secretValueTag,
secretValueHash,
secretCommentCiphertext,
secretCommentIV,
secretCommentTag,
tags
}) => ({
_id: new Types.ObjectId(),
@@ -178,6 +190,9 @@ export const createSecrets = async (req: Request, res: Response) => {
secretValueIV,
secretValueTag,
secretValueHash,
secretCommentCiphertext,
secretCommentIV,
secretCommentTag,
tags
}))
});

View File

@@ -52,9 +52,9 @@ export const deleteWorkspaceTag = async (req: Request, res: Response) => {
UnauthorizedRequestError({ message: 'Failed to validate membership' });
}
await Tag.findByIdAndDelete(tagId)
const result = await Tag.findByIdAndDelete(tagId);
res.sendStatus(200)
res.json(result);
}
export const getWorkspaceTags = async (req: Request, res: Response) => {

View File

@@ -15,7 +15,13 @@ export const getSecretSnapshot = async (req: Request, res: Response) => {
secretSnapshot = await SecretSnapshot
.findById(secretSnapshotId)
.populate('secretVersions');
.populate({
path: 'secretVersions',
populate: {
path: 'tags',
model: 'Tag',
}
});
if (!secretSnapshot) throw new Error('Failed to find secret snapshot');

View File

@@ -30,6 +30,7 @@ interface Update {
* @param {String} obj.workspaceId - id of workspace
* @param {String} obj.integration - name of integration
* @param {String} obj.code - code
* @returns {IntegrationAuth} integrationAuth - integration auth after OAuth2 code-token exchange
*/
const handleOAuthExchangeHelper = async ({
workspaceId,
@@ -42,7 +43,6 @@ const handleOAuthExchangeHelper = async ({
code: string;
environment: string;
}) => {
let action;
let integrationAuth;
try {
const bot = await Bot.findOne({
@@ -98,21 +98,13 @@ const handleOAuthExchangeHelper = async ({
accessExpiresAt: res.accessExpiresAt
});
}
// initialize new integration after exchange
await new Integration({
workspace: workspaceId,
isActive: false,
app: null,
environment,
integration,
integrationAuth: integrationAuth._id
}).save();
} catch (err) {
Sentry.setUser(null);
Sentry.captureException(err);
throw new Error('Failed to handle OAuth2 code-token exchange')
}
return integrationAuth;
}
/**
* Sync/push environment variables in workspace with id [workspaceId] to

View File

@@ -3,6 +3,7 @@ import * as Sentry from '@sentry/node';
import { Octokit } from '@octokit/rest';
import { IIntegrationAuth } from '../models';
import {
INTEGRATION_AZURE_KEY_VAULT,
INTEGRATION_HEROKU,
INTEGRATION_VERCEL,
INTEGRATION_NETLIFY,
@@ -40,6 +41,11 @@ const getApps = async ({
let apps: App[];
try {
switch (integrationAuth.integration) {
case INTEGRATION_AZURE_KEY_VAULT:
apps = await getAppsAzureKeyVault({
accessToken
});
break;
case INTEGRATION_HEROKU:
apps = await getAppsHeroku({
accessToken
@@ -81,6 +87,15 @@ const getApps = async ({
return apps;
};
const getAppsAzureKeyVault = async ({
accessToken
}: {
accessToken: string;
}) => {
// TODO
return [];
}
/**
* Return list of apps for Heroku integration
* @param {Object} obj
@@ -247,7 +262,9 @@ const getAppsRender = async ({
const res = (
await axios.get(`${INTEGRATION_RENDER_API_URL}/v1/services`, {
headers: {
Authorization: `Bearer ${accessToken}`
Authorization: `Bearer ${accessToken}`,
Accept: 'application/json',
'Accept-Encoding': 'application/json'
}
})
).data;
@@ -257,6 +274,7 @@ const getAppsRender = async ({
name: a.service.name,
appId: a.service.id
}));
} catch (err) {
Sentry.setUser(null);
Sentry.captureException(err);
@@ -296,7 +314,9 @@ const getAppsFlyio = async ({
url: INTEGRATION_FLYIO_API_URL,
method: 'post',
headers: {
'Authorization': 'Bearer ' + accessToken
'Authorization': 'Bearer ' + accessToken,
'Accept': 'application/json',
'Accept-Encoding': 'application/json'
},
data: {
query,

View File

@@ -1,10 +1,12 @@
import axios from 'axios';
import * as Sentry from '@sentry/node';
import {
INTEGRATION_AZURE_KEY_VAULT,
INTEGRATION_HEROKU,
INTEGRATION_VERCEL,
INTEGRATION_NETLIFY,
INTEGRATION_GITHUB,
INTEGRATION_AZURE_TOKEN_URL,
INTEGRATION_HEROKU_TOKEN_URL,
INTEGRATION_VERCEL_TOKEN_URL,
INTEGRATION_NETLIFY_TOKEN_URL,
@@ -12,15 +14,27 @@ import {
} from '../variables';
import {
SITE_URL,
CLIENT_ID_AZURE,
CLIENT_ID_VERCEL,
CLIENT_ID_NETLIFY,
CLIENT_ID_GITHUB,
CLIENT_SECRET_AZURE,
CLIENT_SECRET_HEROKU,
CLIENT_SECRET_VERCEL,
CLIENT_SECRET_NETLIFY,
CLIENT_SECRET_GITHUB
} from '../config';
interface ExchangeCodeAzureResponse {
token_type: string;
scope: string;
expires_in: number;
ext_expires_in: number;
access_token: string;
refresh_token: string;
id_token: string;
}
interface ExchangeCodeHerokuResponse {
token_type: string;
access_token: string;
@@ -75,6 +89,11 @@ const exchangeCode = async ({
try {
switch (integration) {
case INTEGRATION_AZURE_KEY_VAULT:
obj = await exchangeCodeAzure({
code
});
break;
case INTEGRATION_HEROKU:
obj = await exchangeCodeHeroku({
code
@@ -105,6 +124,46 @@ const exchangeCode = async ({
return obj;
};
/**
* Return [accessToken] for Azure OAuth2 code-token exchange
* @param param0
*/
const exchangeCodeAzure = async ({
code
}: {
code: string;
}) => {
const accessExpiresAt = new Date();
let res: ExchangeCodeAzureResponse;
try {
res = (await axios.post(
INTEGRATION_AZURE_TOKEN_URL,
new URLSearchParams({
grant_type: 'authorization_code',
code: code,
scope: 'https://vault.azure.net/.default openid offline_access', // TODO: do we need all these permissions?
client_id: CLIENT_ID_AZURE,
client_secret: CLIENT_SECRET_AZURE,
redirect_uri: `${SITE_URL}/integrations/azure-key-vault/oauth2/callback`
} as any)
)).data;
accessExpiresAt.setSeconds(
accessExpiresAt.getSeconds() + res.expires_in
);
} catch (err: any) {
Sentry.setUser(null);
Sentry.captureException(err);
throw new Error('Failed OAuth2 code-token exchange with Azure');
}
return ({
accessToken: res.access_token,
refreshToken: res.refresh_token,
accessExpiresAt
});
}
/**
* Return [accessToken], [accessExpiresAt], and [refreshToken] for Heroku
* OAuth2 code-token exchange
@@ -168,7 +227,7 @@ const exchangeCodeVercel = async ({ code }: { code: string }) => {
code: code,
client_id: CLIENT_ID_VERCEL,
client_secret: CLIENT_SECRET_VERCEL,
redirect_uri: `${SITE_URL}/vercel`
redirect_uri: `${SITE_URL}/integrations/vercel/oauth2/callback`
} as any)
)
).data;
@@ -208,7 +267,7 @@ const exchangeCodeNetlify = async ({ code }: { code: string }) => {
code: code,
client_id: CLIENT_ID_NETLIFY,
client_secret: CLIENT_SECRET_NETLIFY,
redirect_uri: `${SITE_URL}/netlify`
redirect_uri: `${SITE_URL}/integrations/netlify/oauth2/callback`
} as any)
)
).data;
@@ -260,10 +319,11 @@ const exchangeCodeGithub = async ({ code }: { code: string }) => {
client_id: CLIENT_ID_GITHUB,
client_secret: CLIENT_SECRET_GITHUB,
code: code,
redirect_uri: `${SITE_URL}/github`
redirect_uri: `${SITE_URL}/integrations/github/oauth2/callback`
},
headers: {
Accept: 'application/json'
'Accept': 'application/json',
'Accept-Encoding': 'application/json'
}
})
).data;

View File

@@ -1,13 +1,26 @@
import axios from 'axios';
import * as Sentry from '@sentry/node';
import { INTEGRATION_HEROKU } from '../variables';
import { INTEGRATION_AZURE_KEY_VAULT, INTEGRATION_HEROKU } from '../variables';
import {
CLIENT_SECRET_HEROKU
SITE_URL,
CLIENT_ID_AZURE,
CLIENT_SECRET_AZURE,
CLIENT_SECRET_HEROKU
} from '../config';
import {
INTEGRATION_HEROKU_TOKEN_URL
INTEGRATION_AZURE_TOKEN_URL,
INTEGRATION_HEROKU_TOKEN_URL
} from '../variables';
interface RefreshTokenAzureResponse {
token_type: string;
scope: string;
expires_in: number;
ext_expires_in: 4871;
access_token: string;
refresh_token: string;
}
/**
* Return new access token by exchanging refresh token [refreshToken] for integration
* named [integration]
@@ -25,6 +38,11 @@ const exchangeRefresh = async ({
let accessToken;
try {
switch (integration) {
case INTEGRATION_AZURE_KEY_VAULT:
accessToken = await exchangeRefreshAzure({
refreshToken
});
break;
case INTEGRATION_HEROKU:
accessToken = await exchangeRefreshHeroku({
refreshToken
@@ -40,6 +58,38 @@ const exchangeRefresh = async ({
return accessToken;
};
/**
* Return new access token by exchanging refresh token [refreshToken] for the
* Azure integration
* @param {Object} obj
* @param {String} obj.refreshToken - refresh token to use to get new access token for Azure
* @returns
*/
const exchangeRefreshAzure = async ({
refreshToken
}: {
refreshToken: string;
}) => {
try {
const res: RefreshTokenAzureResponse = (await axios.post(
INTEGRATION_AZURE_TOKEN_URL,
new URLSearchParams({
client_id: CLIENT_ID_AZURE,
scope: 'openid offline_access',
refresh_token: refreshToken,
grant_type: 'refresh_token',
client_secret: CLIENT_SECRET_AZURE
} as any)
)).data;
return res.access_token;
} catch (err) {
Sentry.setUser(null);
Sentry.captureException(err);
throw new Error('Failed to get refresh OAuth2 access token for Azure');
}
}
/**
* Return new access token by exchanging refresh token [refreshToken] for the
* Heroku integration
@@ -52,23 +102,23 @@ const exchangeRefreshHeroku = async ({
}: {
refreshToken: string;
}) => {
let accessToken;
//TODO: Refactor code to take advantage of using RequestError. It's possible to create new types of errors for more detailed errors
try {
const res = await axios.post(
INTEGRATION_HEROKU_TOKEN_URL,
new URLSearchParams({
grant_type: 'refresh_token',
refresh_token: refreshToken,
client_secret: CLIENT_SECRET_HEROKU
} as any)
);
let accessToken;
try {
const res = await axios.post(
INTEGRATION_HEROKU_TOKEN_URL,
new URLSearchParams({
grant_type: 'refresh_token',
refresh_token: refreshToken,
client_secret: CLIENT_SECRET_HEROKU
} as any)
);
accessToken = res.data.access_token;
} catch (err) {
Sentry.setUser(null);
Sentry.captureException(err);
throw new Error('Failed to get new OAuth2 access token for Heroku');
throw new Error('Failed to refresh OAuth2 access token for Heroku');
}
return accessToken;

View File

@@ -1,11 +1,10 @@
import axios from 'axios';
import * as Sentry from '@sentry/node';
import { Octokit } from '@octokit/rest';
// import * as sodium from 'libsodium-wrappers';
import sodium from 'libsodium-wrappers';
// const sodium = require('libsodium-wrappers');
import { IIntegration, IIntegrationAuth } from '../models';
import {
INTEGRATION_AZURE_KEY_VAULT,
INTEGRATION_HEROKU,
INTEGRATION_VERCEL,
INTEGRATION_NETLIFY,
@@ -18,7 +17,6 @@ import {
INTEGRATION_RENDER_API_URL,
INTEGRATION_FLYIO_API_URL
} from '../variables';
import { access, appendFile } from 'fs';
/**
* Sync/push [secrets] to [app] in integration named [integration]
@@ -41,6 +39,13 @@ const syncSecrets = async ({
}) => {
try {
switch (integration.integration) {
case INTEGRATION_AZURE_KEY_VAULT:
await syncSecretsAzureKeyVault({
integration,
secrets,
accessToken
});
break;
case INTEGRATION_HEROKU:
await syncSecretsHeroku({
integration,
@@ -93,6 +98,151 @@ const syncSecrets = async ({
}
};
/**
* Sync/push [secrets] to Azure Key Vault with vault URI [integration.app]
* @param {Object} obj
* @param {IIntegration} obj.integration - integration details
* @param {Object} obj.secrets - secrets to push to integration (object where keys are secret keys and values are secret values)
* @param {String} obj.accessToken - access token for Azure Key Vault integration
*/
const syncSecretsAzureKeyVault = async ({
integration,
secrets,
accessToken
}: {
integration: IIntegration;
secrets: any;
accessToken: string;
}) => {
try {
interface GetAzureKeyVaultSecret {
id: string; // secret URI
attributes: {
enabled: true,
created: number;
updated: number;
recoveryLevel: string;
recoverableDays: number;
}
}
interface AzureKeyVaultSecret extends GetAzureKeyVaultSecret {
key: string;
}
/**
* Return all secrets from Azure Key Vault by paginating through URL [url]
* @param {String} url - pagination URL to get next set of secrets from Azure Key Vault
* @returns
*/
const paginateAzureKeyVaultSecrets = async (url: string) => {
let result: GetAzureKeyVaultSecret[] = [];
while (url) {
const res = await axios.get(url, {
headers: {
Authorization: `Bearer ${accessToken}`
}
});
result = result.concat(res.data.value);
url = res.data.nextLink;
}
return result;
}
const getAzureKeyVaultSecrets = await paginateAzureKeyVaultSecrets(`${integration.app}/secrets?api-version=7.3`);
let lastSlashIndex: number;
const res = (await Promise.all(getAzureKeyVaultSecrets.map(async (getAzureKeyVaultSecret) => {
if (!lastSlashIndex) {
lastSlashIndex = getAzureKeyVaultSecret.id.lastIndexOf('/');
}
const azureKeyVaultSecret = await axios.get(`${getAzureKeyVaultSecret.id}?api-version=7.3`, {
headers: {
'Authorization': `Bearer ${accessToken}`
}
});
return ({
...azureKeyVaultSecret.data,
key: getAzureKeyVaultSecret.id.substring(lastSlashIndex + 1),
});
})))
.reduce((obj: any, secret: any) => ({
...obj,
[secret.key]: secret
}), {});
const setSecrets: {
key: string;
value: string;
}[] = [];
Object.keys(secrets).forEach((key) => {
const hyphenatedKey = key.replace(/_/g, '-');
if (!(hyphenatedKey in res)) {
// case: secret has been created
setSecrets.push({
key: hyphenatedKey,
value: secrets[key]
});
} else {
if (secrets[key] !== res[hyphenatedKey].value) {
// case: secret has been updated
setSecrets.push({
key: hyphenatedKey,
value: secrets[key]
});
}
}
});
const deleteSecrets: AzureKeyVaultSecret[] = [];
Object.keys(res).forEach((key) => {
const underscoredKey = key.replace(/-/g, '_');
if (!(underscoredKey in secrets)) {
deleteSecrets.push(res[key]);
}
});
// Sync/push set secrets
if (setSecrets.length > 0) {
setSecrets.forEach(async ({ key, value }) => {
await axios.put(
`${integration.app}/secrets/${key}?api-version=7.3`,
{
value
},
{
headers: {
Authorization: `Bearer ${accessToken}`
}
}
);
});
}
if (deleteSecrets.length > 0) {
deleteSecrets.forEach(async (secret) => {
await axios.delete(`${integration.app}/secrets/${secret.key}?api-version=7.3`, {
headers: {
'Authorization': `Bearer ${accessToken}`
}
});
});
}
} catch (err) {
Sentry.setUser(null);
Sentry.captureException(err);
throw new Error('Failed to sync secrets to Azure Key Vault');
}
};
/**
* Sync/push [secrets] to Heroku app named [integration.app]
* @param {Object} obj

View File

@@ -1,5 +1,6 @@
import { Schema, model, Types } from 'mongoose';
import {
INTEGRATION_AZURE_KEY_VAULT,
INTEGRATION_HEROKU,
INTEGRATION_VERCEL,
INTEGRATION_NETLIFY,
@@ -17,7 +18,7 @@ export interface IIntegration {
owner: string;
targetEnvironment: string;
appId: string;
integration: 'heroku' | 'vercel' | 'netlify' | 'github' | 'render' | 'flyio';
integration: 'heroku' | 'vercel' | 'netlify' | 'github' | 'render' | 'flyio' | 'azure-key-vault';
integrationAuth: Types.ObjectId;
}
@@ -59,6 +60,7 @@ const integrationSchema = new Schema<IIntegration>(
integration: {
type: String,
enum: [
INTEGRATION_AZURE_KEY_VAULT,
INTEGRATION_HEROKU,
INTEGRATION_VERCEL,
INTEGRATION_NETLIFY,

View File

@@ -1,5 +1,6 @@
import { Schema, model, Types } from 'mongoose';
import {
INTEGRATION_AZURE_KEY_VAULT,
INTEGRATION_HEROKU,
INTEGRATION_VERCEL,
INTEGRATION_NETLIFY,
@@ -9,7 +10,7 @@ import {
export interface IIntegrationAuth {
_id: Types.ObjectId;
workspace: Types.ObjectId;
integration: 'heroku' | 'vercel' | 'netlify' | 'github' | 'render' | 'flyio';
integration: 'heroku' | 'vercel' | 'netlify' | 'github' | 'render' | 'flyio' | 'azure-key-vault';
teamId: string;
accountId: string;
refreshCiphertext?: string;
@@ -31,6 +32,7 @@ const integrationAuthSchema = new Schema<IIntegrationAuth>(
integration: {
type: String,
enum: [
INTEGRATION_AZURE_KEY_VAULT,
INTEGRATION_HEROKU,
INTEGRATION_VERCEL,
INTEGRATION_NETLIFY,

View File

@@ -10,7 +10,7 @@ import { ADMIN, MEMBER } from '../../variables';
import { body, param } from 'express-validator';
import { integrationController } from '../../controllers/v1';
router.post( // new: add new integration
router.post( // new: add new integration for integration auth
'/',
requireAuth({
acceptedAuthModes: ['jwt', 'apiKey']
@@ -19,7 +19,13 @@ router.post( // new: add new integration
acceptedRoles: [ADMIN, MEMBER],
location: 'body'
}),
body('integrationAuthId').exists().trim(),
body('integrationAuthId').exists().isString().trim(),
body('app').isString().trim(),
body('isActive').exists().isBoolean(),
body('appId').trim(),
body('sourceEnvironment').trim(),
body('targetEnvironment').trim(),
body('owner').trim(),
validateRequest,
integrationController.createIntegration
);

View File

@@ -18,6 +18,19 @@ router.get(
integrationAuthController.getIntegrationOptions
);
router.get(
'/:integrationAuthId',
requireAuth({
acceptedAuthModes: ['jwt']
}),
requireIntegrationAuthorizationAuth({
acceptedRoles: [ADMIN, MEMBER]
}),
param('integrationAuthId'),
validateRequest,
integrationAuthController.getIntegrationAuth
);
router.post(
'/oauth-token',
requireAuth({

View File

@@ -1,7 +1,3 @@
import * as Sentry from '@sentry/node';
import {
Integration
} from '../models';
import {
handleOAuthExchangeHelper,
syncIntegrationsHelper,
@@ -10,7 +6,6 @@ import {
setIntegrationAuthRefreshHelper,
setIntegrationAuthAccessHelper,
} from '../helpers/integration';
import { exchangeCode } from '../integrations';
// should sync stuff be here too? Probably.
// TODO: move bot functions to IntegrationService.
@@ -26,11 +21,12 @@ class IntegrationService {
* - Store integration access and refresh tokens returned from the OAuth2 code-token exchange
* - Add placeholder inactive integration
* - Create bot sequence for integration
* @param {Object} obj
* @param {String} obj.workspaceId - id of workspace
* @param {String} obj.environment - workspace environment
* @param {String} obj.integration - name of integration
* @param {String} obj.code - code
* @param {Object} obj1
* @param {String} obj1.workspaceId - id of workspace
* @param {String} obj1.environment - workspace environment
* @param {String} obj1.integration - name of integration
* @param {String} obj1.code - code
* @returns {IntegrationAuth} integrationAuth - integration authorization after OAuth2 code-token exchange
*/
static async handleOAuthExchange({
workspaceId,
@@ -43,7 +39,7 @@ class IntegrationService {
code: string;
environment: string;
}) {
await handleOAuthExchangeHelper({
return await handleOAuthExchangeHelper({
workspaceId,
integration,
code,

View File

@@ -6,6 +6,7 @@ import {
ENV_SET
} from './environment';
import {
INTEGRATION_AZURE_KEY_VAULT,
INTEGRATION_HEROKU,
INTEGRATION_VERCEL,
INTEGRATION_NETLIFY,
@@ -14,6 +15,7 @@ import {
INTEGRATION_FLYIO,
INTEGRATION_SET,
INTEGRATION_OAUTH2,
INTEGRATION_AZURE_TOKEN_URL,
INTEGRATION_HEROKU_TOKEN_URL,
INTEGRATION_VERCEL_TOKEN_URL,
INTEGRATION_NETLIFY_TOKEN_URL,
@@ -58,6 +60,7 @@ export {
ENV_STAGING,
ENV_PROD,
ENV_SET,
INTEGRATION_AZURE_KEY_VAULT,
INTEGRATION_HEROKU,
INTEGRATION_VERCEL,
INTEGRATION_NETLIFY,
@@ -66,6 +69,7 @@ export {
INTEGRATION_FLYIO,
INTEGRATION_SET,
INTEGRATION_OAUTH2,
INTEGRATION_AZURE_TOKEN_URL,
INTEGRATION_HEROKU_TOKEN_URL,
INTEGRATION_VERCEL_TOKEN_URL,
INTEGRATION_NETLIFY_TOKEN_URL,

View File

@@ -1,3 +1,7 @@
import {
CLIENT_ID_AZURE,
TENANT_ID_AZURE
} from '../config';
import {
CLIENT_ID_HEROKU,
CLIENT_ID_NETLIFY,
@@ -6,6 +10,7 @@ import {
} from '../config';
// integrations
const INTEGRATION_AZURE_KEY_VAULT = 'azure-key-vault';
const INTEGRATION_HEROKU = 'heroku';
const INTEGRATION_VERCEL = 'vercel';
const INTEGRATION_NETLIFY = 'netlify';
@@ -13,6 +18,7 @@ const INTEGRATION_GITHUB = 'github';
const INTEGRATION_RENDER = 'render';
const INTEGRATION_FLYIO = 'flyio';
const INTEGRATION_SET = new Set([
INTEGRATION_AZURE_KEY_VAULT,
INTEGRATION_HEROKU,
INTEGRATION_VERCEL,
INTEGRATION_NETLIFY,
@@ -25,6 +31,7 @@ const INTEGRATION_SET = new Set([
const INTEGRATION_OAUTH2 = 'oauth2';
// integration oauth endpoints
const INTEGRATION_AZURE_TOKEN_URL = `https://login.microsoftonline.com/${TENANT_ID_AZURE}/oauth2/v2.0/token`;
const INTEGRATION_HEROKU_TOKEN_URL = 'https://id.heroku.com/oauth/token';
const INTEGRATION_VERCEL_TOKEN_URL =
'https://api.vercel.com/v2/oauth/access_token';
@@ -40,6 +47,16 @@ const INTEGRATION_RENDER_API_URL = 'https://api.render.com';
const INTEGRATION_FLYIO_API_URL = 'https://api.fly.io/graphql';
const INTEGRATION_OPTIONS = [
{
name: 'Azure Key Vault',
slug: 'azure-key-vault',
image: 'Microsoft Azure.png',
isAvailable: true,
type: 'oauth',
clientId: CLIENT_ID_AZURE,
tenantId: TENANT_ID_AZURE,
docsLink: ''
},
{
name: 'Heroku',
slug: 'heroku',
@@ -143,6 +160,7 @@ const INTEGRATION_OPTIONS = [
]
export {
INTEGRATION_AZURE_KEY_VAULT,
INTEGRATION_HEROKU,
INTEGRATION_VERCEL,
INTEGRATION_NETLIFY,
@@ -151,6 +169,7 @@ export {
INTEGRATION_FLYIO,
INTEGRATION_SET,
INTEGRATION_OAUTH2,
INTEGRATION_AZURE_TOKEN_URL,
INTEGRATION_HEROKU_TOKEN_URL,
INTEGRATION_VERCEL_TOKEN_URL,
INTEGRATION_NETLIFY_TOKEN_URL,

BIN
docs/images/k8-diagram.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 131 KiB

View File

@@ -3,10 +3,12 @@ title: 'Kubernetes'
description: "This page explains how to use Infisical to inject secrets into Kubernetes clusters."
---
The Infisical Secrets Operator is a custom Kubernetes controller that helps keep secrets in a cluster up to date by synchronizing them.
It is installed in its own namespace within the cluster and follows strict RBAC policies.
The operator uses InfisicalSecret custom resources to identify which secrets to sync and where to store them.
It is responsible for continuously updating managed secrets, and in the future may also automatically reload deployments that use them as needed.
![title](../../images/k8-diagram.png)
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

View File

@@ -15,6 +15,7 @@
"@fortawesome/react-fontawesome": "^0.1.19",
"@headlessui/react": "^1.6.6",
"@hookform/resolvers": "^2.9.10",
"@octokit/rest": "^19.0.7",
"@radix-ui/react-accordion": "^1.1.0",
"@radix-ui/react-alert-dialog": "^1.0.2",
"@radix-ui/react-checkbox": "^1.0.1",
@@ -3521,6 +3522,153 @@
"node": ">= 8"
}
},
"node_modules/@octokit/auth-token": {
"version": "3.0.3",
"resolved": "https://registry.npmjs.org/@octokit/auth-token/-/auth-token-3.0.3.tgz",
"integrity": "sha512-/aFM2M4HVDBT/jjDBa84sJniv1t9Gm/rLkalaz9htOm+L+8JMj1k9w0CkUdcxNyNxZPlTxKPVko+m1VlM58ZVA==",
"dependencies": {
"@octokit/types": "^9.0.0"
},
"engines": {
"node": ">= 14"
}
},
"node_modules/@octokit/core": {
"version": "4.2.0",
"resolved": "https://registry.npmjs.org/@octokit/core/-/core-4.2.0.tgz",
"integrity": "sha512-AgvDRUg3COpR82P7PBdGZF/NNqGmtMq2NiPqeSsDIeCfYFOZ9gddqWNQHnFdEUf+YwOj4aZYmJnlPp7OXmDIDg==",
"dependencies": {
"@octokit/auth-token": "^3.0.0",
"@octokit/graphql": "^5.0.0",
"@octokit/request": "^6.0.0",
"@octokit/request-error": "^3.0.0",
"@octokit/types": "^9.0.0",
"before-after-hook": "^2.2.0",
"universal-user-agent": "^6.0.0"
},
"engines": {
"node": ">= 14"
}
},
"node_modules/@octokit/endpoint": {
"version": "7.0.5",
"resolved": "https://registry.npmjs.org/@octokit/endpoint/-/endpoint-7.0.5.tgz",
"integrity": "sha512-LG4o4HMY1Xoaec87IqQ41TQ+glvIeTKqfjkCEmt5AIwDZJwQeVZFIEYXrYY6yLwK+pAScb9Gj4q+Nz2qSw1roA==",
"dependencies": {
"@octokit/types": "^9.0.0",
"is-plain-object": "^5.0.0",
"universal-user-agent": "^6.0.0"
},
"engines": {
"node": ">= 14"
}
},
"node_modules/@octokit/graphql": {
"version": "5.0.5",
"resolved": "https://registry.npmjs.org/@octokit/graphql/-/graphql-5.0.5.tgz",
"integrity": "sha512-Qwfvh3xdqKtIznjX9lz2D458r7dJPP8l6r4GQkIdWQouZwHQK0mVT88uwiU2bdTU2OtT1uOlKpRciUWldpG0yQ==",
"dependencies": {
"@octokit/request": "^6.0.0",
"@octokit/types": "^9.0.0",
"universal-user-agent": "^6.0.0"
},
"engines": {
"node": ">= 14"
}
},
"node_modules/@octokit/openapi-types": {
"version": "16.0.0",
"resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-16.0.0.tgz",
"integrity": "sha512-JbFWOqTJVLHZSUUoF4FzAZKYtqdxWu9Z5m2QQnOyEa04fOFljvyh7D3GYKbfuaSWisqehImiVIMG4eyJeP5VEA=="
},
"node_modules/@octokit/plugin-paginate-rest": {
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/@octokit/plugin-paginate-rest/-/plugin-paginate-rest-6.0.0.tgz",
"integrity": "sha512-Sq5VU1PfT6/JyuXPyt04KZNVsFOSBaYOAq2QRZUwzVlI10KFvcbUo8lR258AAQL1Et60b0WuVik+zOWKLuDZxw==",
"dependencies": {
"@octokit/types": "^9.0.0"
},
"engines": {
"node": ">= 14"
},
"peerDependencies": {
"@octokit/core": ">=4"
}
},
"node_modules/@octokit/plugin-request-log": {
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/@octokit/plugin-request-log/-/plugin-request-log-1.0.4.tgz",
"integrity": "sha512-mLUsMkgP7K/cnFEw07kWqXGF5LKrOkD+lhCrKvPHXWDywAwuDUeDwWBpc69XK3pNX0uKiVt8g5z96PJ6z9xCFA==",
"peerDependencies": {
"@octokit/core": ">=3"
}
},
"node_modules/@octokit/plugin-rest-endpoint-methods": {
"version": "7.0.1",
"resolved": "https://registry.npmjs.org/@octokit/plugin-rest-endpoint-methods/-/plugin-rest-endpoint-methods-7.0.1.tgz",
"integrity": "sha512-pnCaLwZBudK5xCdrR823xHGNgqOzRnJ/mpC/76YPpNP7DybdsJtP7mdOwh+wYZxK5jqeQuhu59ogMI4NRlBUvA==",
"dependencies": {
"@octokit/types": "^9.0.0",
"deprecation": "^2.3.1"
},
"engines": {
"node": ">= 14"
},
"peerDependencies": {
"@octokit/core": ">=3"
}
},
"node_modules/@octokit/request": {
"version": "6.2.3",
"resolved": "https://registry.npmjs.org/@octokit/request/-/request-6.2.3.tgz",
"integrity": "sha512-TNAodj5yNzrrZ/VxP+H5HiYaZep0H3GU0O7PaF+fhDrt8FPrnkei9Aal/txsN/1P7V3CPiThG0tIvpPDYUsyAA==",
"dependencies": {
"@octokit/endpoint": "^7.0.0",
"@octokit/request-error": "^3.0.0",
"@octokit/types": "^9.0.0",
"is-plain-object": "^5.0.0",
"node-fetch": "^2.6.7",
"universal-user-agent": "^6.0.0"
},
"engines": {
"node": ">= 14"
}
},
"node_modules/@octokit/request-error": {
"version": "3.0.3",
"resolved": "https://registry.npmjs.org/@octokit/request-error/-/request-error-3.0.3.tgz",
"integrity": "sha512-crqw3V5Iy2uOU5Np+8M/YexTlT8zxCfI+qu+LxUB7SZpje4Qmx3mub5DfEKSO8Ylyk0aogi6TYdf6kxzh2BguQ==",
"dependencies": {
"@octokit/types": "^9.0.0",
"deprecation": "^2.0.0",
"once": "^1.4.0"
},
"engines": {
"node": ">= 14"
}
},
"node_modules/@octokit/rest": {
"version": "19.0.7",
"resolved": "https://registry.npmjs.org/@octokit/rest/-/rest-19.0.7.tgz",
"integrity": "sha512-HRtSfjrWmWVNp2uAkEpQnuGMJsu/+dBr47dRc5QVgsCbnIc1+GFEaoKBWkYG+zjrsHpSqcAElMio+n10c0b5JA==",
"dependencies": {
"@octokit/core": "^4.1.0",
"@octokit/plugin-paginate-rest": "^6.0.0",
"@octokit/plugin-request-log": "^1.0.4",
"@octokit/plugin-rest-endpoint-methods": "^7.0.0"
},
"engines": {
"node": ">= 14"
}
},
"node_modules/@octokit/types": {
"version": "9.0.0",
"resolved": "https://registry.npmjs.org/@octokit/types/-/types-9.0.0.tgz",
"integrity": "sha512-LUewfj94xCMH2rbD5YJ+6AQ4AVjFYTgpp6rboWM5T7N3IsIF65SBEOVcYMGAEzO/kKNiNaW4LoWtoThOhH06gw==",
"dependencies": {
"@octokit/openapi-types": "^16.0.0"
}
},
"node_modules/@pkgr/utils": {
"version": "2.3.1",
"resolved": "https://registry.npmjs.org/@pkgr/utils/-/utils-2.3.1.tgz",
@@ -8789,6 +8937,11 @@
}
]
},
"node_modules/before-after-hook": {
"version": "2.2.3",
"resolved": "https://registry.npmjs.org/before-after-hook/-/before-after-hook-2.2.3.tgz",
"integrity": "sha512-NzUnlZexiaH/46WDhANlyR2bXRopNg4F/zuSA3OpZnllCUgRaOF2znDioDWrmbNVsuZk6l9pMquQB38cfBZwkQ=="
},
"node_modules/better-opn": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/better-opn/-/better-opn-2.1.1.tgz",
@@ -10371,6 +10524,11 @@
"node": ">= 0.8"
}
},
"node_modules/deprecation": {
"version": "2.3.1",
"resolved": "https://registry.npmjs.org/deprecation/-/deprecation-2.3.1.tgz",
"integrity": "sha512-xmHIy4F3scKVwMsQ4WnVaS8bHOx0DmVwRywosKhaILI0ywMDWPtBSku2HNxRvF7jtwDRsoEwYQSfbxj8b7RlJQ=="
},
"node_modules/dequal": {
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz",
@@ -14099,7 +14257,6 @@
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-5.0.0.tgz",
"integrity": "sha512-VRSzKkbMm5jMDoKLbltAkFQ5Qr7VDiTFGXxYFXXowVj387GeGNOCsOH6Msy00SGZ3Fp84b1Naa1psqgcCIEP5Q==",
"dev": true,
"engines": {
"node": ">=0.10.0"
}
@@ -16455,7 +16612,6 @@
"version": "2.6.8",
"resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.8.tgz",
"integrity": "sha512-RZ6dBYuj8dRSfxpUSu+NsdF1dpPpluJxwOp+6IoDp/sH2QNDSvurYsAa+F1WxY2RjA1iP93xhcsUoYbF2XBqVg==",
"dev": true,
"dependencies": {
"whatwg-url": "^5.0.0"
},
@@ -21217,8 +21373,7 @@
"node_modules/tr46": {
"version": "0.0.3",
"resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz",
"integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==",
"dev": true
"integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw=="
},
"node_modules/trim-lines": {
"version": "3.0.1",
@@ -21664,6 +21819,11 @@
"url": "https://opencollective.com/unified"
}
},
"node_modules/universal-user-agent": {
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/universal-user-agent/-/universal-user-agent-6.0.0.tgz",
"integrity": "sha512-isyNax3wXoKaulPDZWHQqbmIx1k2tb9fb3GGDBRxCscfYV2Ch7WxPArBsFEG8s/safwXTT7H4QGhaIkTp9447w=="
},
"node_modules/universalify": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.0.tgz",
@@ -22068,8 +22228,7 @@
"node_modules/webidl-conversions": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz",
"integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==",
"dev": true
"integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ=="
},
"node_modules/webpack": {
"version": "5.75.0",
@@ -22267,7 +22426,6 @@
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz",
"integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==",
"dev": true,
"dependencies": {
"tr46": "~0.0.3",
"webidl-conversions": "^3.0.0"
@@ -24829,6 +24987,118 @@
"fastq": "^1.6.0"
}
},
"@octokit/auth-token": {
"version": "3.0.3",
"resolved": "https://registry.npmjs.org/@octokit/auth-token/-/auth-token-3.0.3.tgz",
"integrity": "sha512-/aFM2M4HVDBT/jjDBa84sJniv1t9Gm/rLkalaz9htOm+L+8JMj1k9w0CkUdcxNyNxZPlTxKPVko+m1VlM58ZVA==",
"requires": {
"@octokit/types": "^9.0.0"
}
},
"@octokit/core": {
"version": "4.2.0",
"resolved": "https://registry.npmjs.org/@octokit/core/-/core-4.2.0.tgz",
"integrity": "sha512-AgvDRUg3COpR82P7PBdGZF/NNqGmtMq2NiPqeSsDIeCfYFOZ9gddqWNQHnFdEUf+YwOj4aZYmJnlPp7OXmDIDg==",
"requires": {
"@octokit/auth-token": "^3.0.0",
"@octokit/graphql": "^5.0.0",
"@octokit/request": "^6.0.0",
"@octokit/request-error": "^3.0.0",
"@octokit/types": "^9.0.0",
"before-after-hook": "^2.2.0",
"universal-user-agent": "^6.0.0"
}
},
"@octokit/endpoint": {
"version": "7.0.5",
"resolved": "https://registry.npmjs.org/@octokit/endpoint/-/endpoint-7.0.5.tgz",
"integrity": "sha512-LG4o4HMY1Xoaec87IqQ41TQ+glvIeTKqfjkCEmt5AIwDZJwQeVZFIEYXrYY6yLwK+pAScb9Gj4q+Nz2qSw1roA==",
"requires": {
"@octokit/types": "^9.0.0",
"is-plain-object": "^5.0.0",
"universal-user-agent": "^6.0.0"
}
},
"@octokit/graphql": {
"version": "5.0.5",
"resolved": "https://registry.npmjs.org/@octokit/graphql/-/graphql-5.0.5.tgz",
"integrity": "sha512-Qwfvh3xdqKtIznjX9lz2D458r7dJPP8l6r4GQkIdWQouZwHQK0mVT88uwiU2bdTU2OtT1uOlKpRciUWldpG0yQ==",
"requires": {
"@octokit/request": "^6.0.0",
"@octokit/types": "^9.0.0",
"universal-user-agent": "^6.0.0"
}
},
"@octokit/openapi-types": {
"version": "16.0.0",
"resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-16.0.0.tgz",
"integrity": "sha512-JbFWOqTJVLHZSUUoF4FzAZKYtqdxWu9Z5m2QQnOyEa04fOFljvyh7D3GYKbfuaSWisqehImiVIMG4eyJeP5VEA=="
},
"@octokit/plugin-paginate-rest": {
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/@octokit/plugin-paginate-rest/-/plugin-paginate-rest-6.0.0.tgz",
"integrity": "sha512-Sq5VU1PfT6/JyuXPyt04KZNVsFOSBaYOAq2QRZUwzVlI10KFvcbUo8lR258AAQL1Et60b0WuVik+zOWKLuDZxw==",
"requires": {
"@octokit/types": "^9.0.0"
}
},
"@octokit/plugin-request-log": {
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/@octokit/plugin-request-log/-/plugin-request-log-1.0.4.tgz",
"integrity": "sha512-mLUsMkgP7K/cnFEw07kWqXGF5LKrOkD+lhCrKvPHXWDywAwuDUeDwWBpc69XK3pNX0uKiVt8g5z96PJ6z9xCFA==",
"requires": {}
},
"@octokit/plugin-rest-endpoint-methods": {
"version": "7.0.1",
"resolved": "https://registry.npmjs.org/@octokit/plugin-rest-endpoint-methods/-/plugin-rest-endpoint-methods-7.0.1.tgz",
"integrity": "sha512-pnCaLwZBudK5xCdrR823xHGNgqOzRnJ/mpC/76YPpNP7DybdsJtP7mdOwh+wYZxK5jqeQuhu59ogMI4NRlBUvA==",
"requires": {
"@octokit/types": "^9.0.0",
"deprecation": "^2.3.1"
}
},
"@octokit/request": {
"version": "6.2.3",
"resolved": "https://registry.npmjs.org/@octokit/request/-/request-6.2.3.tgz",
"integrity": "sha512-TNAodj5yNzrrZ/VxP+H5HiYaZep0H3GU0O7PaF+fhDrt8FPrnkei9Aal/txsN/1P7V3CPiThG0tIvpPDYUsyAA==",
"requires": {
"@octokit/endpoint": "^7.0.0",
"@octokit/request-error": "^3.0.0",
"@octokit/types": "^9.0.0",
"is-plain-object": "^5.0.0",
"node-fetch": "^2.6.7",
"universal-user-agent": "^6.0.0"
}
},
"@octokit/request-error": {
"version": "3.0.3",
"resolved": "https://registry.npmjs.org/@octokit/request-error/-/request-error-3.0.3.tgz",
"integrity": "sha512-crqw3V5Iy2uOU5Np+8M/YexTlT8zxCfI+qu+LxUB7SZpje4Qmx3mub5DfEKSO8Ylyk0aogi6TYdf6kxzh2BguQ==",
"requires": {
"@octokit/types": "^9.0.0",
"deprecation": "^2.0.0",
"once": "^1.4.0"
}
},
"@octokit/rest": {
"version": "19.0.7",
"resolved": "https://registry.npmjs.org/@octokit/rest/-/rest-19.0.7.tgz",
"integrity": "sha512-HRtSfjrWmWVNp2uAkEpQnuGMJsu/+dBr47dRc5QVgsCbnIc1+GFEaoKBWkYG+zjrsHpSqcAElMio+n10c0b5JA==",
"requires": {
"@octokit/core": "^4.1.0",
"@octokit/plugin-paginate-rest": "^6.0.0",
"@octokit/plugin-request-log": "^1.0.4",
"@octokit/plugin-rest-endpoint-methods": "^7.0.0"
}
},
"@octokit/types": {
"version": "9.0.0",
"resolved": "https://registry.npmjs.org/@octokit/types/-/types-9.0.0.tgz",
"integrity": "sha512-LUewfj94xCMH2rbD5YJ+6AQ4AVjFYTgpp6rboWM5T7N3IsIF65SBEOVcYMGAEzO/kKNiNaW4LoWtoThOhH06gw==",
"requires": {
"@octokit/openapi-types": "^16.0.0"
}
},
"@pkgr/utils": {
"version": "2.3.1",
"resolved": "https://registry.npmjs.org/@pkgr/utils/-/utils-2.3.1.tgz",
@@ -28742,6 +29012,11 @@
"resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz",
"integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA=="
},
"before-after-hook": {
"version": "2.2.3",
"resolved": "https://registry.npmjs.org/before-after-hook/-/before-after-hook-2.2.3.tgz",
"integrity": "sha512-NzUnlZexiaH/46WDhANlyR2bXRopNg4F/zuSA3OpZnllCUgRaOF2znDioDWrmbNVsuZk6l9pMquQB38cfBZwkQ=="
},
"better-opn": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/better-opn/-/better-opn-2.1.1.tgz",
@@ -29938,6 +30213,11 @@
"resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz",
"integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw=="
},
"deprecation": {
"version": "2.3.1",
"resolved": "https://registry.npmjs.org/deprecation/-/deprecation-2.3.1.tgz",
"integrity": "sha512-xmHIy4F3scKVwMsQ4WnVaS8bHOx0DmVwRywosKhaILI0ywMDWPtBSku2HNxRvF7jtwDRsoEwYQSfbxj8b7RlJQ=="
},
"dequal": {
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz",
@@ -32732,8 +33012,7 @@
"is-plain-object": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-5.0.0.tgz",
"integrity": "sha512-VRSzKkbMm5jMDoKLbltAkFQ5Qr7VDiTFGXxYFXXowVj387GeGNOCsOH6Msy00SGZ3Fp84b1Naa1psqgcCIEP5Q==",
"dev": true
"integrity": "sha512-VRSzKkbMm5jMDoKLbltAkFQ5Qr7VDiTFGXxYFXXowVj387GeGNOCsOH6Msy00SGZ3Fp84b1Naa1psqgcCIEP5Q=="
},
"is-regex": {
"version": "1.1.4",
@@ -34410,7 +34689,6 @@
"version": "2.6.8",
"resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.8.tgz",
"integrity": "sha512-RZ6dBYuj8dRSfxpUSu+NsdF1dpPpluJxwOp+6IoDp/sH2QNDSvurYsAa+F1WxY2RjA1iP93xhcsUoYbF2XBqVg==",
"dev": true,
"requires": {
"whatwg-url": "^5.0.0"
}
@@ -37863,8 +38141,7 @@
"tr46": {
"version": "0.0.3",
"resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz",
"integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==",
"dev": true
"integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw=="
},
"trim-lines": {
"version": "3.0.1",
@@ -38186,6 +38463,11 @@
"unist-util-is": "^5.0.0"
}
},
"universal-user-agent": {
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/universal-user-agent/-/universal-user-agent-6.0.0.tgz",
"integrity": "sha512-isyNax3wXoKaulPDZWHQqbmIx1k2tb9fb3GGDBRxCscfYV2Ch7WxPArBsFEG8s/safwXTT7H4QGhaIkTp9447w=="
},
"universalify": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.0.tgz",
@@ -38479,8 +38761,7 @@
"webidl-conversions": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz",
"integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==",
"dev": true
"integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ=="
},
"webpack": {
"version": "5.75.0",
@@ -38626,7 +38907,6 @@
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz",
"integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==",
"dev": true,
"requires": {
"tr46": "~0.0.3",
"webidl-conversions": "^3.0.0"

View File

@@ -22,6 +22,7 @@
"@fortawesome/react-fontawesome": "^0.1.19",
"@headlessui/react": "^1.6.6",
"@hookform/resolvers": "^2.9.10",
"@octokit/rest": "^19.0.7",
"@radix-ui/react-accordion": "^1.1.0",
"@radix-ui/react-alert-dialog": "^1.0.2",
"@radix-ui/react-checkbox": "^1.0.1",

View File

@@ -2,6 +2,16 @@ interface Mapping {
[key: string]: string;
}
const integrationSlugNameMapping: Mapping = {
'azure-key-vault': 'Azure Key Vault',
'heroku': 'Heroku',
'vercel': 'Vercel',
'netlify': 'Netlify',
'github': 'GitHub',
'render': 'Render',
'flyio': 'Fly.io'
}
const envMapping: Mapping = {
Development: "dev",
Staging: "staging",
@@ -19,7 +29,7 @@ const reverseEnvMapping: Mapping = {
const contextNetlifyMapping: Mapping = {
"dev": "Local development",
"branch-deploy": "Branch deploys",
"deploy-review": "Deploy Previews",
"deploy-preview": "Deploy Previews",
"production": "Production"
}
@@ -49,6 +59,7 @@ const plans = plansProd || plansDev;
export {
contextNetlifyMapping,
envMapping,
integrationSlugNameMapping,
plans,
reverseContextNetlifyMapping,
reverseEnvMapping}

View File

@@ -1,3 +1,12 @@
export interface Tag {
_id: string;
name: string;
slug: string;
user: string;
workspace: string;
createdAt: string;
}
export interface SecretDataProps {
pos: number;
key: string;
@@ -5,4 +14,5 @@ export interface SecretDataProps {
valueOverride: string | undefined;
id: string;
comment: string;
tags: Tag[];
}

View File

@@ -199,13 +199,13 @@ const Layout = ({ children }: LayoutProps) => {
.split('/')
[router.asPath.split('/').length - 1].split('?')[0];
if (!['heroku', 'vercel', 'github', 'netlify'].includes(intendedWorkspaceId)) {
if (!['callback', 'create', 'authorize'].includes(intendedWorkspaceId)) {
localStorage.setItem('projectData.id', intendedWorkspaceId);
}
// If a user is not a member of a workspace they are trying to access, just push them to one of theirs
if (
!['heroku', 'vercel', 'github', 'netlify'].includes(intendedWorkspaceId) &&
!['callback', 'create', 'authorize'].includes(intendedWorkspaceId) &&
!userWorkspaces
.map((workspace: { _id: string }) => workspace._id)
.includes(intendedWorkspaceId)

View File

@@ -36,7 +36,7 @@ const AddWorkspaceDialog = ({
return (
<div>
<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
as={Fragment}
enter="ease-out duration-300"
@@ -49,7 +49,7 @@ const AddWorkspaceDialog = ({
<div className="fixed inset-0 bg-black bg-opacity-70" />
</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">
<Transition.Child
as={Fragment}
@@ -60,7 +60,7 @@ const AddWorkspaceDialog = ({
leaveFrom="opacity-100 scale-100"
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
as="h3"
className="text-lg font-medium leading-6 text-gray-400"
@@ -69,7 +69,7 @@ const AddWorkspaceDialog = ({
</Dialog.Title>
<div className="mt-2">
<p className="text-sm text-gray-500">
This project will contain your environmental variables.
This project will contain your secrets and configs.
</p>
</div>
<div className="max-h-28 mt-4">

View File

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

View File

@@ -83,7 +83,7 @@ const EnvironmentTable = ({ data = [], onCreateEnv, onDeleteEnv, onUpdateEnv }:
</div>
<div className="w-48">
<Button
text="Add New Env"
text="Add New Environment"
onButtonPressed={() => {
if (plan !== plans.starter || host !== 'https://app.infisical.com') {
handlePopUpOpen('createUpdateEnv');

View File

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

View File

@@ -3,7 +3,6 @@ import { faCircle, faExclamationCircle, faEye, faLayerGroup } from '@fortawesome
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import guidGenerator from '../utilities/randomId';
import { Button } from '../v2';
import { HoverObject } from '../v2/HoverCard';
const REGEX = /([$]{.*?})/g;
@@ -101,8 +100,8 @@ const DashboardInputField = ({
)}
{!error && <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'
} cursor-pointer duration-0`}>
<Button variant="plain" onClick={() => {
} cursor-pointer duration-0 h-10 flex items-center px-2`}>
<button type="button" onClick={() => {
if (modifyValueOverride) {
if (overrideEnabled === false) {
modifyValueOverride('', position);
@@ -116,7 +115,7 @@ const DashboardInputField = ({
icon={faLayerGroup}
color={overrideEnabled ? 'primary' : 'bunker-400'}
/>
</Button>
</button>
</div>}
</div>
);

View File

@@ -19,7 +19,7 @@ export const DeleteActionButton = ({ onSubmit, isPlain }: Props) => {
<div className={`${
!isPlain
? '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
? <div
onKeyDown={() => null}

View File

@@ -64,7 +64,8 @@ const DropZone = ({
key,
value: keyPairs[key as keyof typeof keyPairs].value,
comment: keyPairs[key as keyof typeof keyPairs].comments.join('\n'),
type: 'shared'
type: 'shared',
tags: []
}));
break;
}
@@ -86,7 +87,8 @@ const DropZone = ({
key,
value: keyPairs[key as keyof typeof keyPairs]?.toString() ?? '',
comment,
type: 'shared'
type: 'shared',
tags: []
};
});
break;

View File

@@ -1,7 +1,8 @@
import { faEllipsis } from '@fortawesome/free-solid-svg-icons';
import { faEllipsis, faXmark } from '@fortawesome/free-solid-svg-icons';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { SecretDataProps } from 'public/data/frequentInterfaces';
import { SecretDataProps, Tag } from 'public/data/frequentInterfaces';
import AddTagsMenu from './AddTagsMenu';
import DashboardInputField from './DashboardInputField';
import { DeleteActionButton } from './DeleteActionButton';
@@ -11,6 +12,7 @@ interface KeyPairProps {
modifyValue: (value: string, position: number) => void;
modifyValueOverride: (value: string | undefined, position: number) => void;
modifyComment: (value: string, position: number) => void;
modifyTags: (value: Tag[], position: number) => void;
isBlurred: boolean;
isDuplicate: boolean;
toggleSidebar: (id: string) => void;
@@ -18,6 +20,8 @@ interface KeyPairProps {
isSnapshot: boolean;
isCapitalized: boolean;
deleteRow?: (props: DeleteRowFunctionProps) => void;
tags: Tag[];
togglePITSidebar?: (value: boolean) => void;
}
export interface DeleteRowFunctionProps {
@@ -25,6 +29,23 @@ export interface DeleteRowFunctionProps {
secretName: string;
}
const colors = [
'bg-[#f1c40f]/40',
'bg-[#cb1c8d]/40',
'bg-[#badc58]/40',
'bg-[#ff5400]/40',
'bg-[#00bbf9]/40'
]
const colorsText = [
'text-[#fcf0c3]/70',
'text-[#f2c6e3]/70',
'text-[#eef6d5]/70',
'text-[#ffddcc]/70',
'text-[#f0fffd]/70'
]
/**
* This component represent a single row for an environemnt variable on the dashboard
* @param {object} obj
@@ -33,12 +54,15 @@ export interface DeleteRowFunctionProps {
* @param {function} obj.modifyValue - modify the value of a certain environment variable
* @param {function} obj.modifyValueOverride - modify the value of a certain environment variable if it is overriden
* @param {function} obj.modifyComment - modify the comment of a certain environment variable
* @param {function} obj.modifyTags - modify the tags of a certain environment variable
* @param {boolean} obj.isBlurred - if the blurring setting is turned on
* @param {boolean} obj.isDuplicate - list of all the duplicates secret names on the dashboard
* @param {function} obj.toggleSidebar - open/close/switch sidebar
* @param {string} obj.sidebarSecretId - the id of a secret for the side bar is displayed
* @param {boolean} obj.isSnapshot - whether this keyPair is in a snapshot. If so, it won't have some features like sidebar
* @param {function} obj.deleteRow - a function to delete a certain keyPair
* @param {function} obj.togglePITSidebar - open or close the Point-in-time recovery sidebar
* @param {Tag[]} obj.tags - tags for a certain secret
* @returns
*/
const KeyPair = ({
@@ -47,22 +71,32 @@ const KeyPair = ({
modifyValue,
modifyValueOverride,
modifyComment,
modifyTags,
isBlurred,
isDuplicate,
toggleSidebar,
sidebarSecretId,
isCapitalized,
isSnapshot,
deleteRow
}: KeyPairProps) => (
deleteRow,
togglePITSidebar,
tags
}: KeyPairProps) => {
const tagData = (tags.map((tag, index) => {return {
...tag,
color: colors[index%colors.length],
colorText: colorsText[index%colorsText.length]
}}));
return (
<div
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'
}`}
>
<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-80 border-r border-mineshaft-600">
<div className="w-2/12 border-r border-mineshaft-600 flex flex-row 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="flex items-center max-h-16">
<DashboardInputField
isCapitalized = {isCapitalized}
@@ -77,7 +111,7 @@ const KeyPair = ({
/>
</div>
</div>
<div className="w-full border-r border-mineshaft-600">
<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'
>
@@ -92,7 +126,7 @@ const KeyPair = ({
/>
</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">
<DashboardInputField
onChangeHandler={modifyComment}
@@ -104,27 +138,42 @@ const KeyPair = ({
/>
</div>
</div>
{!isSnapshot && (
<div
onKeyDown={() => null}
role="button"
tabIndex={0}
onClick={() => toggleSidebar(keyPair.id)}
className="cursor-pointer w-[2.35rem] h-[2.35rem] px-6 rounded-md invisible group-hover:visible flex flex-row justify-center items-center"
>
<FontAwesomeIcon className="text-bunker-300 hover:text-primary text-lg" icon={faEllipsis} />
<div className="w-2/12 h-10 flex items-center overflow-visible overflow-r-scroll no-scrollbar no-scrollbar::-webkit-scrollbar">
<div className="flex items-center max-h-16">
{keyPair.tags.map((tag, index) => (
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`}>
<span className='mb-0.5 cursor-default'>{tag.name}</span>
<FontAwesomeIcon icon={faXmark} className="ml-1 cursor-pointer p-1" onClick={() => modifyTags(keyPair.tags.filter(ttag => ttag._id !== tag._id), keyPair.pos)}/>
</div>
))}
<AddTagsMenu allTags={tags} currentTags={keyPair.tags} modifyTags={modifyTags} position={keyPair.pos} />
</div>
)}
{!isSnapshot && (
</div>
<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
onSubmit={() => { if (deleteRow) {
deleteRow({ ids: [keyPair.id], secretName: keyPair?.key })
}}}
isPlain
/>
)}
</div>
</div>
</div>
);
)};
export default KeyPair;

View File

@@ -80,7 +80,7 @@ const SideBar = ({
const { t } = useTranslation();
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 ? (
<div className="flex items-center justify-center h-full">
<Image

View File

@@ -4,7 +4,7 @@ import { useRouter } from 'next/router';
import { faArrowRight, faRotate, faX } from '@fortawesome/free-solid-svg-icons';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
// 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 ListBox from '@app/components/basic/Listbox';
@@ -52,6 +52,7 @@ const IntegrationTile = ({
environments = [],
handleDeleteIntegration
}: Props) => {
// set initial environment. This find will only execute when component is mounting
const [integrationEnvironment, setIntegrationEnvironment] = useState<Props['environments'][0]>(
environments.find(({ slug }) => slug === integration.environment) || {
@@ -72,7 +73,14 @@ const IntegrationTile = ({
});
setApps(tempApps);
setIntegrationApp(integration.app ? integration.app : tempApps[0].name);
if (integration?.app) {
setIntegrationApp(integration.app);
} else if (tempApps.length > 0) {
setIntegrationApp(tempApps[0].name)
} else {
setIntegrationApp('');
}
switch (integration.integration) {
case 'vercel':
@@ -174,7 +182,7 @@ const IntegrationTile = ({
return <div />;
};
if (!integrationApp || apps.length === 0) return <div />;
if (!integrationApp) return <div />;
return (
<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">
<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">
{integration.integration.charAt(0).toUpperCase() + integration.integration.slice(1)}
{/* {integration.integration.charAt(0).toUpperCase() + integration.integration.slice(1)} */}
{integrationSlugNameMapping[integration.integration]}
</div>
</div>
<div className="mr-2">

View File

@@ -138,7 +138,8 @@ const attemptLogin = async (
value: 'mongodb+srv://${DB_USERNAME}:${DB_PASSWORD}@mongodb.net',
valueOverride: undefined,
comment: 'This is an example of secret referencing.',
id: ''
id: '',
tags: []
},
{
pos: 1,
@@ -147,7 +148,8 @@ const attemptLogin = async (
valueOverride: undefined,
comment:
'This is an example of secret overriding. Your team can have a shared value of a secret, while you can override it to whatever value you need',
id: ''
id: '',
tags: []
},
{
pos: 2,
@@ -156,7 +158,8 @@ const attemptLogin = async (
valueOverride: undefined,
comment:
'This is an example of secret overriding. Your team can have a shared value of a secret, while you can override it to whatever value you need',
id: ''
id: '',
tags: []
},
{
pos: 3,
@@ -164,7 +167,8 @@ const attemptLogin = async (
value: 'user1234',
valueOverride: 'user1234',
comment: '',
id: ''
id: '',
tags: []
},
{
pos: 4,
@@ -172,7 +176,8 @@ const attemptLogin = async (
value: 'example_password',
valueOverride: 'example_password',
comment: '',
id: ''
id: '',
tags: []
},
{
pos: 5,
@@ -180,7 +185,8 @@ const attemptLogin = async (
value: 'example_twillio_token',
valueOverride: undefined,
comment: '',
id: ''
id: '',
tags: []
},
{
pos: 6,
@@ -188,7 +194,8 @@ const attemptLogin = async (
value: 'http://localhost:3000',
valueOverride: undefined,
comment: '',
id: ''
id: '',
tags: []
}
];
const secrets = await encryptSecrets({

View File

@@ -1,6 +1,6 @@
import crypto from 'crypto';
import { SecretDataProps } from 'public/data/frequentInterfaces';
import { SecretDataProps, Tag } from 'public/data/frequentInterfaces';
import getLatestFileKey from '@app/pages/api/workspace/getLatestFileKey';
@@ -20,6 +20,7 @@ interface EncryptedSecretProps {
secretValueIV: string;
secretValueTag: string;
type: 'personal' | 'shared';
tags: Tag[];
}
/**
@@ -105,7 +106,8 @@ const encryptSecrets = async ({
type:
secret.valueOverride === undefined || secret?.value !== secret?.valueOverride
? 'shared'
: 'personal'
: 'personal',
tags: secret.tags
};
return result;

View File

@@ -1,3 +1,5 @@
import { Tag } from 'public/data/frequentInterfaces';
import getSecrets from '@app/pages/api/files/GetSecrets';
import getLatestFileKey from '@app/pages/api/workspace/getLatestFileKey';
@@ -17,6 +19,7 @@ interface EncryptedSecretProps {
secretValueIV: string;
secretValueTag: string;
type: 'personal' | 'shared';
tags: Tag[];
}
interface SecretProps {
@@ -25,6 +28,7 @@ interface SecretProps {
type: 'personal' | 'shared';
comment: string;
id: string;
tags: Tag[];
}
interface FunctionProps {
@@ -107,7 +111,8 @@ const getSecretsForProject = async ({
key: plainTextKey,
value: plainTextValue,
type: secret.type,
comment: plainTextComment
comment: plainTextComment,
tags: secret.tags
});
});
}
@@ -130,7 +135,10 @@ const getSecretsForProject = async ({
)[0]?.value,
comment: tempDecryptedSecrets.filter(
(secret) => secret.key === key && secret.type === 'shared'
)[0]?.comment
)[0]?.comment,
tags: tempDecryptedSecrets.filter(
(secret) => secret.key === key && secret.type === 'shared'
)[0]?.tags
}));
if (typeof setData === 'function') {

View File

@@ -52,7 +52,7 @@ export const DeleteActionModal = ({
>
<ModalContent
title={title}
subTitle="This action is irreversible!!!"
subTitle="This action is irreversible!"
footerContent={
<div className="flex items-center">
<Button

View File

@@ -41,7 +41,7 @@ export const Select = forwardRef<HTMLButtonElement, SelectProps>(
<SelectPrimitive.Portal>
<SelectPrimitive.Content
className={twMerge(
'relative left-4 top-1 overflow-hidden rounded-md bg-bunker-800 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
)}
>

View File

@@ -5,6 +5,7 @@ import { useRouter } from 'next/router';
import { useTranslation } from 'next-i18next';
import { faXmark } from '@fortawesome/free-solid-svg-icons';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { Tag } from 'public/data/frequentInterfaces';
import Button from '@app/components/basic/buttons/Button';
import {
@@ -48,6 +49,7 @@ interface EncrypetedSecretVersionListProps {
secretKeyTag: string;
environment: string;
type: 'personal' | 'shared';
tags: Tag[];
}
/**
@@ -106,6 +108,7 @@ const PITRecoverySidebar = ({ toggleSidebar, setSnapshotData, chosenSnapshot }:
pos,
type: encryptedSecretVersion.type,
environment: encryptedSecretVersion.environment,
tags: encryptedSecretVersion.tags,
key: decryptSymmetric({
ciphertext: encryptedSecretVersion.secretKeyCiphertext,
iv: encryptedSecretVersion.secretKeyIV,
@@ -134,6 +137,9 @@ const PITRecoverySidebar = ({ toggleSidebar, setSnapshotData, chosenSnapshot }:
environment: decryptedSecretVersions.filter(
(secret: SecretDataProps) => secret.key === key && secret.type === 'shared'
)[0].environment,
tags: decryptedSecretVersions.filter(
(secret: SecretDataProps) => secret.key === key && secret.type === 'shared'
)[0].tags,
value: decryptedSecretVersions.filter(
(secret: SecretDataProps) => secret.key === key && secret.type === 'shared'
)[0]?.value,
@@ -155,7 +161,7 @@ const PITRecoverySidebar = ({ toggleSidebar, setSnapshotData, chosenSnapshot }:
<div
className={`absolute border-l border-mineshaft-500 ${
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 ? (
<div className="flex items-center justify-center h-full mb-8">

View File

@@ -2,4 +2,5 @@ export * from './auth';
export * from './keys';
export * from './serviceTokens';
export * from './subscriptions';
export * from './tags';
export * from './workspace';

View File

@@ -0,0 +1,3 @@
export {
useGetIntegrationAuthApps,
useGetIntegrationAuthById} from './queries';

View File

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

View File

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

View File

@@ -0,0 +1,3 @@
export { useGetWsTags } from './queries';
export { useCreateWsTag } from './queries';
export { useDeleteWsTag } from './queries';

View File

@@ -0,0 +1,62 @@
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
import { apiRequest } from '@app/config/request';
import {
CreateTagDTO,
CreateTagRes,
DeleteTagDTO,
DeleteWsTagRes,
UserWsTags
} from './types';
const workspaceTags = {
getWsTags: (workspaceID: string) => ['workspace-tags', { workspaceID }] as const
};
const fetchWsTag = async (workspaceID: string) => {
const { data } = await apiRequest.get<{ workspaceTags: UserWsTags }>(
`/api/v2/workspace/${workspaceID}/tags`
);
return data.workspaceTags;
};
export const useGetWsTags = (workspaceID: string) =>
useQuery({
queryKey: workspaceTags.getWsTags(workspaceID),
queryFn: () => fetchWsTag(workspaceID),
enabled: Boolean(workspaceID)
});
export const useCreateWsTag = () => {
const queryClient = useQueryClient();
return useMutation<CreateTagRes, {}, CreateTagDTO>({
mutationFn: async ({ workspaceID, tagName, tagSlug }) => {
const { data } = await apiRequest.post(`/api/v2/workspace/${workspaceID}/tags`, {
name: tagName,
slug: tagSlug
})
return data;
},
onSuccess: (tagData) => {
queryClient.invalidateQueries(workspaceTags.getWsTags(tagData?.workspace));
}
});
};
export const useDeleteWsTag = () => {
const queryClient = useQueryClient();
return useMutation<DeleteWsTagRes, {}, DeleteTagDTO>({
mutationFn: async ({ tagID }) => {
const { data } = await apiRequest.delete(`/api/v2/workspace/tags/${tagID}`);
return data
},
onSuccess: (tagData) => {
queryClient.invalidateQueries(workspaceTags.getWsTags(tagData?.workspace));
}
});
};

View File

@@ -0,0 +1,39 @@
export type UserWsTags = WsTag[];
export type WsTag = {
_id: string;
name: string;
slug: string;
workspace: string;
createdAt: string;
updatedAt: string;
__v: number;
}
export type WorkspaceTag = { _id: string; name: string; slug: string };
export type CreateTagDTO = {
workspaceID: string;
tagSlug: string;
tagName: string;
};
export type CreateTagRes = {
name: string;
slug: string;
workspace: string;
createdAt: string;
user: string;
_id: string;
};
export type DeleteTagDTO = { tagID: string; };
export type DeleteWsTagRes = {
name: string;
slug: string;
workspace: string;
createdAt: string;
user: string;
_id: string;
};

View File

@@ -9,5 +9,6 @@ export type {
RenameWorkspaceDTO,
UpdateEnvironmentDTO,
Workspace,
WorkspaceEnv
WorkspaceEnv,
WorkspaceTag
} from './workspace/types';

View File

@@ -3,7 +3,8 @@ export {
useDeleteWorkspace,
useDeleteWsEnvironment,
useGetUserWorkspaces,
useGetWorkspaceById,
useRenameWorkspace,
useToggleAutoCapitalization,
useUpdateWsEnvironment
} from './queries';
useUpdateWsEnvironment} from './queries';

View File

@@ -12,15 +12,30 @@ import {
Workspace
} from './types';
const workspaceKeys = {
getWorkspaceById: (workspaceId: string) => [{ workspaceId }, 'workspace'] as const,
getAllUserWorkspace: ['workspaces'] as const
};
const fetchWorkspaceById = async (workspaceId: string) => {
const { data } = await apiRequest.get<{ workspace: Workspace }>(`/api/v1/workspace/${workspaceId}`);
return data.workspace;
}
const fetchUserWorkspaces = async () => {
const { data } = await apiRequest.get<{ workspaces: Workspace[] }>('/api/v1/workspace');
return data.workspaces;
};
export const useGetWorkspaceById = (workspaceId: string) => {
return useQuery({
queryKey: workspaceKeys.getWorkspaceById(workspaceId),
queryFn: () => fetchWorkspaceById(workspaceId),
enabled: true
});
};
export const useGetUserWorkspaces = () =>
useQuery(workspaceKeys.getAllUserWorkspace, fetchUserWorkspaces);

View File

@@ -8,6 +8,7 @@ export type Workspace = {
};
export type WorkspaceEnv = { name: string; slug: string };
export type WorkspaceTag = { _id: string; name: string; slug: string };
// mutation dto
export type RenameWorkspaceDTO = { workspaceID: string; newWorkspaceName: string };

View File

@@ -26,7 +26,7 @@ const AuthorizeIntegration = ({ workspaceId, code, integration }: Props) =>
})
}).then(async (res) => {
if (res && res.status === 200) {
return res;
return (await res.json()).integrationAuth;
}
console.log('Failed to authorize the integration');
return undefined;

View File

@@ -1,7 +1,13 @@
import SecurityClient from '@app/components/utilities/SecurityClient';
interface Props {
integrationAuthId: string;
integrationAuthId: string;
isActive: boolean;
app: string | null;
appId: string | null;
sourceEnvironment: string;
targetEnvironment: string | null;
owner: string | null;
}
/**
* This route creates a new integration based on the integration authorization with id [integrationAuthId]
@@ -10,7 +16,13 @@ interface Props {
* @returns
*/
const createIntegration = ({
integrationAuthId
integrationAuthId,
isActive,
app,
appId,
sourceEnvironment,
targetEnvironment,
owner
}: Props) =>
SecurityClient.fetchCall('/api/v1/integration', {
method: 'POST',
@@ -18,7 +30,13 @@ const createIntegration = ({
'Content-Type': 'application/json'
},
body: JSON.stringify({
integrationAuthId
integrationAuthId,
isActive,
app,
appId,
sourceEnvironment,
targetEnvironment,
owner
})
}).then(async (res) => {
if (res && res.status === 200) {

View File

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

View File

@@ -14,9 +14,11 @@ import {
faEyeSlash,
faFolderOpen,
faMagnifyingGlass,
faPlus
faPlus,
faXmark
} from '@fortawesome/free-solid-svg-icons';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { Tag } from 'public/data/frequentInterfaces';
import Button from '@app/components/basic/buttons/Button';
import ListBox from '@app/components/basic/Listbox';
@@ -44,6 +46,7 @@ import checkUserAction from '../api/userActions/checkUserAction';
import registerUserAction from '../api/userActions/registerUserAction';
import getWorkspaceEnvironments from '../api/workspace/getWorkspaceEnvironments';
import getWorkspaces from '../api/workspace/getWorkspaces';
import getWorkspaceTags from '../api/workspace/getWorkspaceTags';
type WorkspaceEnv = {
name: string;
@@ -59,6 +62,7 @@ interface SecretDataProps {
id: string;
idOverride: string | undefined;
comment: string;
tags: Tag[];
}
// eslint-disable-next-line @typescript-eslint/no-unused-vars
@@ -82,6 +86,7 @@ interface SnapshotProps {
value: string;
valueOverride: string;
comment: string;
tags: Tag[];
}[];
}
@@ -127,6 +132,7 @@ export default function Dashboard() {
const [numSnapshots, setNumSnapshots] = useState<number>();
const [saveLoading, setSaveLoading] = useState(false);
const [autoCapitalization, setAutoCapitalization] = useState(false);
const [projectTags, setProjectTags] = useState<Tag[]>([]);
const { t } = useTranslation();
const { createNotification } = useNotificationContext();
@@ -235,6 +241,8 @@ export default function Dashboard() {
action: 'first_time_secrets_pushed'
});
setHasUserEverPushed(!!userAction);
setProjectTags(await getWorkspaceTags({ workspaceId }));
} catch (error) {
console.log('Error', error);
setData(undefined);
@@ -277,7 +285,8 @@ export default function Dashboard() {
key: '',
value: '',
valueOverride: undefined,
comment: ''
comment: '',
tags: []
},
...data!
]);
@@ -297,7 +306,8 @@ export default function Dashboard() {
key: '',
value: '',
valueOverride: undefined,
comment: ''
comment: '',
tags: []
},
]);
};
@@ -335,6 +345,11 @@ export default function Dashboard() {
setButtonReady(true);
};
const modifyTags = (tags: Tag[], pos: number) => {
setData((oldData) => oldData?.map((e) => (e.pos === pos ? { ...e, tags } : e)));
setButtonReady(true);
};
// For speed purposes and better perforamance, we are using useCallback
const listenChangeValue = useCallback((value: string, pos: number) => {
modifyValue(value, pos);
@@ -352,6 +367,10 @@ export default function Dashboard() {
modifyComment(value, pos);
}, []);
const listenChangeTags = useCallback((value: Tag[], pos: number) => {
modifyTags(value, pos);
}, []);
/**
* Save the changes of environment variables and push them to the database
*/
@@ -424,7 +443,9 @@ export default function Dashboard() {
newData!.filter((dataPoint) => dataPoint.id === initDataPoint.id)[0].key !==
initDataPoint.key ||
newData!.filter((dataPoint) => dataPoint.id === initDataPoint.id)[0].comment !==
initDataPoint.comment)
initDataPoint.comment) ||
newData!.filter((dataPoint) => dataPoint.id === initDataPoint.id)[0].tags !==
initDataPoint.tags
)
.map((secret) => secret.id)
.includes(newDataPoint.id)
@@ -458,7 +479,8 @@ export default function Dashboard() {
valueOverride: override.valueOverride,
comment: '',
id: String(override.idOverride),
idOverride: String(override.idOverride)
idOverride: String(override.idOverride),
tags: override.tags
}));
console.log('override add', overridesToBeAdded.length);
@@ -473,7 +495,9 @@ export default function Dashboard() {
newOverrides!.filter((dataPoint) => dataPoint.id === initDataPoint.id)[0].key !==
initDataPoint.key ||
newOverrides!.filter((dataPoint) => dataPoint.id === initDataPoint.id)[0]
.comment !== initDataPoint.comment)
.comment !== initDataPoint.comment ||
newOverrides!.filter((dataPoint) => dataPoint.id === initDataPoint.id)[0]
.tags !== initDataPoint.tags)
)
.map((secret) => secret.id)
.includes(newDataPoint.id)
@@ -485,7 +509,8 @@ export default function Dashboard() {
valueOverride: override.valueOverride,
comment: '',
id: String(override.idOverride),
idOverride: String(override.idOverride)
idOverride: String(override.idOverride),
tags: override.tags
}));
console.log('override update', overridesToBeUpdated.length);
@@ -546,34 +571,6 @@ export default function Dashboard() {
<meta name="og:description" content={String(t('dashboard:og-description'))} />
</Head>
<div className="flex flex-row h-full dark:[color-scheme:dark]">
{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]">
<NavHeader pageName={t('dashboard:title')} isProjectRelated />
{checkDocsPopUpVisible && (
@@ -628,7 +625,10 @@ export default function Dashboard() {
<div className="flex justify-start max-w-sm mt-1 mr-2">
<Button
text={String(`${numSnapshots} ${t('Commits')}`)}
onButtonPressed={() => togglePITSidebar(true)}
onButtonPressed={() => {
toggleSidebar('None');
togglePITSidebar(true)
}}
color="mineshaft"
size="md"
icon={faClockRotateLeft}
@@ -663,7 +663,8 @@ export default function Dashboard() {
valueOverride: sv.valueOverride,
key: sv.key,
value: sv.value,
comment: ''
comment: '',
tags: sv.tags
}));
setData(rolledBackSecrets);
@@ -781,27 +782,58 @@ export default function Dashboard() {
className="mt-1 max-h-[calc(100vh-280px)] overflow-hidden overflow-y-scroll no-scrollbar no-scrollbar::-webkit-scrollbar border border-mineshaft-600 rounded-md"
>
<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 className='w-14'/>
<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>
{!snapshotData && <IconButton
ariaLabel="copy icon"
variant="plain"
className="group relative ml-2"
onClick={() => reorderRows(1)}
>
{sortMethod === 'alphabetical' ? <FontAwesomeIcon icon={faArrowUp} /> : <FontAwesomeIcon icon={faArrowDown} />}
</IconButton>}
<div
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="relative flex flex-row justify-between w-full mr-auto max-h-14 items-center">
<div className="w-2/12 border-r border-mineshaft-600 flex flex-row items-center">
<div className='text-transparent text-xs flex items-center justify-center w-14 h-10 cursor-default'>0</div>
<span className='px-2 text-bunker-300 font-semibold'>Key</span>
{!snapshotData && <IconButton
ariaLabel="copy icon"
variant="plain"
className="group relative ml-2"
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 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 className="bg-mineshaft-800 rounded-b-md border-bunker-600">
{!snapshotData &&
data
?.filter((row) => row.key?.toUpperCase().includes(searchKeys.toUpperCase()))
?.filter((row) => row.key?.toUpperCase().includes(searchKeys.toUpperCase()) || row.tags?.map(tag => tag.name).join(" ")?.toUpperCase().includes(searchKeys.toUpperCase()))
.filter((row) => !sharedToHide.includes(row.id))
.map((keyPair) => (
<KeyPair
@@ -812,14 +844,17 @@ export default function Dashboard() {
modifyValueOverride={listenChangeValueOverride}
modifyKey={listenChangeKey}
modifyComment={listenChangeComment}
modifyTags={listenChangeTags}
isBlurred={blurred}
isDuplicate={findDuplicates(data?.map((item) => item.key))?.includes(
keyPair.key
)}
toggleSidebar={toggleSidebar}
togglePITSidebar={togglePITSidebar}
sidebarSecretId={sidebarSecretId}
isSnapshot={false}
deleteRow={deleteCertainRow}
tags={projectTags}
/>
))}
{snapshotData &&
@@ -852,6 +887,7 @@ export default function Dashboard() {
modifyValueOverride={listenChangeValueOverride}
modifyKey={listenChangeKey}
modifyComment={listenChangeComment}
modifyTags={listenChangeTags}
isBlurred={blurred}
isDuplicate={findDuplicates(data?.map((item) => item.key))?.includes(
keyPair.key
@@ -859,6 +895,7 @@ export default function Dashboard() {
toggleSidebar={toggleSidebar}
sidebarSecretId={sidebarSecretId}
isSnapshot
tags={projectTags}
/>
))}
<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]'>
@@ -912,6 +949,34 @@ export default function Dashboard() {
)}
</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>
) : (

View File

@@ -1,41 +0,0 @@
import { useEffect } from 'react';
import { useRouter } from 'next/router';
import queryString from 'query-string';
import AuthorizeIntegration from './api/integrations/authorizeIntegration';
export default function Github() {
const router = useRouter();
const parsedUrl = queryString.parse(router.asPath.split('?')[1]);
const {code} = parsedUrl;
const {state} = parsedUrl;
/**
* Here we forward to the default workspace if a user opens this url
*/
// eslint-disable-next-line react-hooks/exhaustive-deps
useEffect(() => {
(async () => {
try {
if (state === localStorage.getItem('latestCSRFToken')) {
localStorage.removeItem('latestCSRFToken');
await AuthorizeIntegration({
workspaceId: localStorage.getItem('projectData.id') as string,
code: code as string,
integration: 'github',
});
router.push(
`/integrations/${ localStorage.getItem('projectData.id')}`
);
}
} catch (error) {
console.error('Github integration error: ', error);
}
})();
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
return <div />;
}
Github.requireAuth = true;

View File

@@ -1,41 +0,0 @@
import { useEffect } from 'react';
import { useRouter } from 'next/router';
import queryString from 'query-string';
import AuthorizeIntegration from './api/integrations/authorizeIntegration';
export default function Heroku() {
const router = useRouter();
const parsedUrl = queryString.parse(router.asPath.split('?')[1]);
const {code} = parsedUrl;
const {state} = parsedUrl;
/**
* Here we forward to the default workspace if a user opens this url
*/
// eslint-disable-next-line react-hooks/exhaustive-deps
useEffect(() => {
(async () => {
try {
if (state === localStorage.getItem('latestCSRFToken')) {
localStorage.removeItem('latestCSRFToken');
await AuthorizeIntegration({
workspaceId: localStorage.getItem('projectData.id') as string,
code: code as string,
integration: 'heroku',
});
router.push(
`/integrations/${ localStorage.getItem('projectData.id')}`
);
}
} catch (error) {
console.error('Heroku integration error: ', error);
}
})();
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
return <div />;
}
Heroku.requireAuth = true;

View File

@@ -7,7 +7,6 @@ import { useTranslation } from 'next-i18next';
import frameworkIntegrationOptions from 'public/json/frameworkIntegrations.json';
import ActivateBotDialog from '@app/components/basic/dialog/ActivateBotDialog';
import IntegrationAccessTokenDialog from '@app/components/basic/dialog/IntegrationAccessTokenDialog';
import CloudIntegrationSection from '@app/components/integrations/CloudIntegrationSection';
import FrameworkIntegrationSection from '@app/components/integrations/FrameworkIntegrationSection';
import IntegrationSection from '@app/components/integrations/IntegrationSection';
@@ -20,12 +19,10 @@ import {
} from '../../components/utilities/cryptography/crypto';
import getBot from '../api/bot/getBot';
import setBotActiveStatus from '../api/bot/setBotActiveStatus';
import createIntegration from '../api/integrations/createIntegration';
import deleteIntegration from '../api/integrations/DeleteIntegration';
import getIntegrationOptions from '../api/integrations/GetIntegrationOptions';
import getWorkspaceAuthorizations from '../api/integrations/getWorkspaceAuthorizations';
import getWorkspaceIntegrations from '../api/integrations/getWorkspaceIntegrations';
import saveIntegrationAccessToken from '../api/integrations/saveIntegrationAccessToken';
import getAWorkspace from '../api/workspace/getAWorkspace';
import getLatestFileKey from '../api/workspace/getLatestFileKey';
@@ -52,6 +49,7 @@ interface Integration {
}
interface IntegrationOption {
tenantId?: string;
clientId: string;
clientSlug?: string; // vercel-integration specific
docsLink: string;
@@ -75,7 +73,6 @@ export default function Integrations() {
// TODO: These will have its type when migratiing towards react-query
const [bot, setBot] = useState<any>(null);
const [isActivateBotDialogOpen, setIsActivateBotDialogOpen] = useState(false);
const [isIntegrationAccessTokenDialogOpen, setIntegrationAccessTokenDialogOpen] = useState(false);
const [selectedIntegrationOption, setSelectedIntegrationOption] = useState<IntegrationOption | null>(null);
const router = useRouter();
@@ -166,80 +163,83 @@ export default function Integrations() {
}
};
/**
* Handle integration option authorization for a given integration option [integrationOption]
* @param {Object} obj
* @param {Object} obj.integrationOption - an integration option
* @param {String} obj.name
* @param {String} obj.type
* @param {String} obj.docsLink
* @returns
*/
const handleIntegrationOption = async ({
integrationOption,
accessToken
}: {
integrationOption: IntegrationOption,
accessToken?: string;
}) => {
const handleUnauthorizedIntegrationOptionPress = (integrationOption: IntegrationOption) => {
try {
if (!bot.isActive) {
await handleBotActivate();
// generate CSRF token for OAuth2 code-token exchange integrations
const state = crypto.randomBytes(16).toString('hex');
localStorage.setItem('latestCSRFToken', state);
let link = '';
switch (integrationOption.slug) {
case 'azure-key-vault':
link = `https://login.microsoftonline.com/${integrationOption.tenantId}/oauth2/v2.0/authorize?client_id=${integrationOption.clientId}&response_type=code&redirect_uri=${window.location.origin}/integrations/azure-key-vault/oauth2/callback&response_mode=query&scope=https://vault.azure.net/.default openid offline_access&state=${state}`;
break;
case 'heroku':
link = `https://id.heroku.com/oauth/authorize?client_id=${integrationOption.clientId}&response_type=code&scope=write-protected&state=${state}`;
break;
case 'vercel':
link = `https://vercel.com/integrations/${integrationOption.clientSlug}/new?state=${state}`;
break;
case 'netlify':
link = `https://app.netlify.com/authorize?client_id=${integrationOption.clientId}&response_type=code&state=${state}&redirect_uri=${window.location.origin}/integrations/netlify/oauth2/callback`;
break;
case 'github':
link = `https://github.com/login/oauth/authorize?client_id=${integrationOption.clientId}&response_type=code&scope=repo&redirect_uri=${window.location.origin}/integrations/github/oauth2/callback&state=${state}`;
break;
case 'render':
link = `${window.location.origin}/integrations/render/authorize`
break;
case 'flyio':
link = `${window.location.origin}/integrations/flyio/authorize`
break;
default:
break;
}
if (integrationOption.type === 'oauth') {
// integration is of type OAuth
// generate CSRF token for OAuth2 code-token exchange integrations
const state = crypto.randomBytes(16).toString('hex');
localStorage.setItem('latestCSRFToken', state);
switch (integrationOption.slug) {
case 'heroku':
window.location.assign(
`https://id.heroku.com/oauth/authorize?client_id=${integrationOption.clientId}&response_type=code&scope=write-protected&state=${state}`
);
break;
case 'vercel':
window.location.assign(
`https://vercel.com/integrations/${integrationOption.clientSlug}/new?state=${state}`
);
break;
case 'netlify':
window.location.assign(
`https://app.netlify.com/authorize?client_id=${integrationOption.clientId}&response_type=code&state=${state}&redirect_uri=${window.location.origin}/netlify`
);
break;
case 'github':
window.location.assign(
`https://github.com/login/oauth/authorize?client_id=${integrationOption.clientId}&response_type=code&scope=repo&redirect_uri=${window.location.origin}/github&state=${state}`
);
break;
default:
break;
}
return;
} if (integrationOption.type === 'pat') {
// integration is of type personal access token
const integrationAuth = await saveIntegrationAccessToken({
workspaceId: localStorage.getItem('projectData.id'),
integration: integrationOption.slug,
accessToken: accessToken ?? ''
});
setIntegrationAuths([...integrationAuths, integrationAuth])
const integration = await createIntegration({
integrationAuthId: integrationAuth._id
});
setIntegrations([...integrations, integration]);
return;
if (link !== '') {
window.location.assign(link);
}
} catch (err) {
console.error(err);
}
};
}
const handleAuthorizedIntegrationOptionPress = (integrationAuth: IntegrationAuth) => {
try {
let link = '';
switch (integrationAuth.integration) {
case 'azure-key-vault':
link = `${window.location.origin}/integrations/azure-key-vault/create?integrationAuthId=${integrationAuth._id}`;
break;
case 'heroku':
link = `${window.location.origin}/integrations/heroku/create?integrationAuthId=${integrationAuth._id}`;
break;
case 'vercel':
link = `${window.location.origin}/integrations/vercel/create?integrationAuthId=${integrationAuth._id}`;
break;
case 'netlify':
link = `${window.location.origin}/integrations/netlify/create?integrationAuthId=${integrationAuth._id}`;
break;
case 'github':
link = `${window.location.origin}/integrations/github/create?integrationAuthId=${integrationAuth._id}`;
break;
case 'render':
link = `${window.location.origin}/integrations/render/create?integrationAuthId=${integrationAuth._id}`;
break;
case 'flyio':
link = `${window.location.origin}/integrations/flyio/create?integrationAuthId=${integrationAuth._id}`;
break;
default:
break;
}
if (link !== '') {
window.location.assign(link);
}
} catch (err) {
console.error(err);
}
}
/**
* Open dialog to activate bot if bot is not active.
@@ -251,34 +251,20 @@ export default function Integrations() {
* @returns
*/
const integrationOptionPress = async (integrationOption: IntegrationOption) => {
// consider: don't start integration until at [handleIntegrationOption] step
try {
const integrationAuthX = integrationAuths.find((integrationAuth) => integrationAuth.integration === integrationOption.slug);
if (!integrationAuthX) {
// case: integration has not been authorized before
if (integrationOption.type === 'pat') {
// case: integration requires user to input their personal access token for that integration
setIntegrationAccessTokenDialogOpen(true);
return;
}
// case: integration does not require user to input their personal access token (i.e. it's an OAuth2 integration)
handleIntegrationOption({ integrationOption });
return;
}
if (!bot.isActive) {
await handleBotActivate();
}
// case: integration has been authorized before
// -> create new integration
const integration = await createIntegration({
integrationAuthId: integrationAuthX._id
});
setIntegrations([...integrations, integration]);
if (!integrationAuthX) {
// case: integration has not been authorized
handleUnauthorizedIntegrationOptionPress(integrationOption);
return;
}
handleAuthorizedIntegrationOptionPress(integrationAuthX);
} catch (err) {
console.error(err);
}
@@ -360,12 +346,6 @@ export default function Integrations() {
selectedIntegrationOption={selectedIntegrationOption}
integrationOptionPress={integrationOptionPress}
/>
<IntegrationAccessTokenDialog
isOpen={isIntegrationAccessTokenDialogOpen}
closeModal={() => setIntegrationAccessTokenDialogOpen(false)}
selectedIntegrationOption={selectedIntegrationOption}
handleIntegrationOption={handleIntegrationOption}
/>
<IntegrationSection
integrations={integrations}
setIntegrations={setIntegrations}

View File

@@ -0,0 +1,124 @@
import { useEffect, useState } from 'react';
import { useRouter } from 'next/router';
import queryString from 'query-string';
import { getTranslatedServerSideProps } from '../../../components/utilities/withTranslateProps';
import {
Button,
Card,
CardTitle,
FormControl,
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']);

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,46 +0,0 @@
import { useEffect } from 'react';
import { useRouter } from 'next/router';
import queryString from 'query-string';
import AuthorizeIntegration from './api/integrations/authorizeIntegration';
export default function Netlify() {
const router = useRouter();
const parsedUrl = queryString.parse(router.asPath.split('?')[1]);
const {code} = parsedUrl;
const {state} = parsedUrl;
// modify comment here
/**
* Here we forward to the default workspace if a user opens this url
*/
// eslint-disable-next-line react-hooks/exhaustive-deps
useEffect(() => {
(async () => {
try {
if (!code) throw new Error('Code not found');
if (state === localStorage.getItem('latestCSRFToken')) {
localStorage.removeItem('latestCSRFToken');
await AuthorizeIntegration({
workspaceId: localStorage.getItem('projectData.id') as string,
code: code as string,
integration: 'netlify',
});
router.push(
`/integrations/${ localStorage.getItem('projectData.id')}`
);
}
} catch (err) {
console.error('Netlify integration error: ', err);
}
})();
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
return <div />;
}
Netlify.requireAuth = true;

View File

@@ -1,46 +0,0 @@
import { useEffect } from 'react';
import { useRouter } from 'next/router';
import queryString from 'query-string';
import AuthorizeIntegration from './api/integrations/authorizeIntegration';
export default function Vercel() {
const router = useRouter();
const parsedUrl = queryString.parse(router.asPath.split('?')[1]);
const {code} = parsedUrl;
const {state} = parsedUrl;
/**
* Here we forward to the default workspace if a user opens this url
*/
// eslint-disable-next-line react-hooks/exhaustive-deps
useEffect(() => {
(async () => {
try {
// type check
if (!code) throw new Error('Code not found');
if (state === localStorage.getItem('latestCSRFToken')) {
localStorage.removeItem('latestCSRFToken');
await AuthorizeIntegration({
workspaceId: localStorage.getItem('projectData.id') as string,
code: code as string,
integration: 'vercel',
});
router.push(
`/integrations/${ localStorage.getItem('projectData.id')}`
);
}
} catch (err) {
console.error('Vercel integration error: ', err);
}
})();
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
return <div />;
}
Vercel.requireAuth = true;

View File

@@ -18,21 +18,29 @@ import { useToggle } from '@app/hooks';
import {
useCreateServiceToken,
useCreateWsEnvironment,
useCreateWsTag,
useDeleteServiceToken,
useDeleteWorkspace,
useDeleteWsEnvironment,
useDeleteWsTag,
useGetUserWsKey,
useGetUserWsServiceTokens,
useGetWsTags,
useRenameWorkspace,
useToggleAutoCapitalization,
useUpdateWsEnvironment
} from '@app/hooks/api';
import { AutoCapitalizationSection } from './components/AutoCapitalizationSection/AutoCapitalizationSection';
import { SecretTagsSection } from './components/SecretTagsSection';
import {
CopyProjectIDSection,
CreateServiceToken,
CreateUpdateEnvFormData,
CreateWsTag,
EnvironmentSection,
ProjectNameChangeSection,
ServiceTokenSection
@@ -65,6 +73,11 @@ export const ProjectSettingsPage = () => {
const createServiceToken = useCreateServiceToken();
const deleteServiceToken = useDeleteServiceToken();
// tag
const { data: wsTags } = useGetWsTags(workspaceID);
const createWsTag = useCreateWsTag();
const deleteWsTag = useDeleteWsTag();
// get user subscription
const { subscriptionPlan } = useSubscription();
const host = window.location.origin;
@@ -231,6 +244,44 @@ export const ProjectSettingsPage = () => {
return '';
};
const onCreateWsTag = async ({ name }: CreateWsTag) => {
try {
const res = await createWsTag.mutateAsync({
workspaceID,
tagName: name,
tagSlug: name.replace(" ", "_")
});
createNotification({
text: 'Successfully created a tag',
type: 'success'
});
return res.name;
} catch (error) {
console.error(error);
createNotification({
text: 'Failed to create a tag',
type: 'error'
});
}
return '';
};
const onDeleteTag = async (tagID: string) => {
try {
await deleteWsTag.mutateAsync({ tagID });
createNotification({
text: 'Successfully deleted tag',
type: 'success'
});
} catch (error) {
console.error(error);
createNotification({
text: 'Failed to delete the tag',
type: 'error'
});
}
};
const onDeleteServiceToken = async (tokenID: string) => {
try {
await deleteServiceToken.mutateAsync(tokenID);
@@ -248,7 +299,7 @@ export const ProjectSettingsPage = () => {
};
return (
<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 */}
<div className="relative right-5">
<NavHeader pageName={t('settings-project:title')} isProjectRelated />
@@ -284,6 +335,12 @@ export const ProjectSettingsPage = () => {
workspaceName={currentWorkspace?.name || ''}
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">
<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>

View File

@@ -102,7 +102,7 @@ export const EnvironmentSection = ({
colorSchema="primary"
leftIcon={<FontAwesomeIcon icon={faPlus} />}
>
Add New Env
Add New Environment
</Button>
</div>
</div>

View File

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

View File

@@ -0,0 +1 @@
export {SecretTagsSection} from './SecretTagsSection'

View File

@@ -2,5 +2,6 @@ export { CopyProjectIDSection } from './CopyProjectIDSection';
export { EnvironmentSection } from './EnvironmentSection';
export type { CreateUpdateEnvFormData } from './EnvironmentSection/EnvironmentSection';
export { ProjectNameChangeSection } from './ProjectNameChangeSection';
export type { CreateWsTag } from './SecretTagsSection/SecretTagsSection';
export { ServiceTokenSection } from './ServiceTokenSection';
export type { CreateServiceToken } from './ServiceTokenSection/ServiceTokenSection';