diff --git a/README.md b/README.md
index 1ec8cff50..64d643e7f 100644
--- a/README.md
+++ b/README.md
@@ -171,7 +171,9 @@ We're currently setting the foundation and building [integrations](https://infis
🔜 GCP SM (https://github.com/Infisical/infisical/issues/285)
- 🔜 GitLab CI/CD (https://github.com/Infisical/infisical/issues/134)
+
+ ✔️ GitLab CI/CD
+
🔜 CircleCI (https://github.com/Infisical/infisical/issues/91)
diff --git a/backend/package-lock.json b/backend/package-lock.json
index 6703e198f..189b1eccd 100644
--- a/backend/package-lock.json
+++ b/backend/package-lock.json
@@ -10,13 +10,13 @@
"license": "ISC",
"dependencies": {
"@godaddy/terminus": "^4.11.2",
- "@sentry/node": "^7.21.1",
"@octokit/rest": "^19.0.5",
- "@sentry/tracing": "^7.21.1",
+ "@sentry/node": "^7.14.0",
+ "@sentry/tracing": "^7.19.0",
"@types/crypto-js": "^4.1.1",
- "axios": "^1.2.0",
"@types/libsodium-wrappers": "^0.7.10",
"await-to-js": "^3.0.0",
+ "axios": "^1.1.3",
"bcrypt": "^5.1.0",
"bigint-conversion": "^2.2.2",
"builder-pattern": "^2.2.0",
@@ -32,9 +32,9 @@
"js-yaml": "^4.1.0",
"jsonwebtoken": "^9.0.0",
"jsrp": "^0.2.4",
- "mongoose": "^6.7.3",
"libsodium-wrappers": "^0.7.10",
"lodash": "^4.17.21",
+ "mongoose": "^6.7.2",
"nodemailer": "^6.8.0",
"posthog-node": "^2.2.2",
"query-string": "^7.1.3",
@@ -2838,19 +2838,6 @@
"@maxmind/geoip2-node": "^3.4.0"
}
},
- "node_modules/@sentry/core": {
- "version": "7.21.1",
- "resolved": "https://registry.npmjs.org/@sentry/core/-/core-7.21.1.tgz",
- "integrity": "sha512-Og5wEEsy24fNvT/T7IKjcV4EvVK5ryY2kxbJzKY6GU2eX+i+aBl+n/vp7U0Es351C/AlTkS+0NOUsp2TQQFxZA==",
- "dependencies": {
- "@sentry/types": "7.21.1",
- "@sentry/utils": "7.21.1",
- "tslib": "^1.9.3"
- },
- "engines": {
- "node": ">=8"
- }
- },
"node_modules/@protobufjs/aspromise": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/@protobufjs/aspromise/-/aspromise-1.1.2.tgz",
@@ -2905,27 +2892,10 @@
"resolved": "https://registry.npmjs.org/@protobufjs/utf8/-/utf8-1.1.0.tgz",
"integrity": "sha512-Vvn3zZrhQZkkBE8LSuW3em98c0FwgO4nxzv6OdSxPKJIEKY2bGbHn+mhGIPerzI4twdxaP8/0+06HBpwf345Lw=="
},
- "node_modules/@sentry/node": {
- "version": "7.19.0",
- "resolved": "https://registry.npmjs.org/@sentry/node/-/node-7.19.0.tgz",
- "integrity": "sha512-yG7Tx32WqOkEHVotFLrumCcT9qlaSDTkFNZ+yLSvZXx74ifsE781DzBA9W7K7bBdYO3op+p2YdsOKzf3nPpAyQ==",
- "dependencies": {
- "@sentry/core": "7.19.0",
- "@sentry/types": "7.19.0",
- "@sentry/utils": "7.19.0",
- "cookie": "^0.4.1",
- "https-proxy-agent": "^5.0.0",
- "lru_map": "^0.3.3",
- "tslib": "^1.9.3"
- },
- "engines": {
- "node": ">=8"
- }
- },
- "node_modules/@sentry/node/node_modules/@sentry/core": {
- "version": "7.19.0",
- "resolved": "https://registry.npmjs.org/@sentry/core/-/core-7.19.0.tgz",
- "integrity": "sha512-YF9cTBcAnO4R44092BJi5Wa2/EO02xn2ziCtmNgAVTN2LD31a/YVGxGBt/FDr4Y6yeuVehaqijVVvtpSmXrGJw==",
+ "node_modules/@sentry/core": {
+ "version": "7.21.1",
+ "resolved": "https://registry.npmjs.org/@sentry/core/-/core-7.21.1.tgz",
+ "integrity": "sha512-Og5wEEsy24fNvT/T7IKjcV4EvVK5ryY2kxbJzKY6GU2eX+i+aBl+n/vp7U0Es351C/AlTkS+0NOUsp2TQQFxZA==",
"dependencies": {
"@sentry/types": "7.21.1",
"@sentry/utils": "7.21.1",
@@ -2986,26 +2956,6 @@
"node": ">=8"
}
},
- "node_modules/@sentry/types": {
- "version": "7.21.1",
- "resolved": "https://registry.npmjs.org/@sentry/types/-/types-7.21.1.tgz",
- "integrity": "sha512-3/IKnd52Ol21amQvI+kz+WB76s8/LR5YvFJzMgIoI2S8d82smIr253zGijRXxHPEif8kMLX4Yt+36VzrLxg6+A==",
- "engines": {
- "node": ">=8"
- }
- },
- "node_modules/@sentry/utils": {
- "version": "7.21.1",
- "resolved": "https://registry.npmjs.org/@sentry/utils/-/utils-7.21.1.tgz",
- "integrity": "sha512-F0W0AAi8tgtTx6ApZRI2S9HbXEA9ENX1phTZgdNNWcMFm1BNbc21XEwLqwXBNjub5nlA6CE8xnjXRgdZKx4kzQ==",
- "dependencies": {
- "@sentry/types": "7.21.1",
- "tslib": "^1.9.3"
- },
- "engines": {
- "node": ">=8"
- }
- },
"node_modules/@sinclair/typebox": {
"version": "0.24.51",
"resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.24.51.tgz",
@@ -14306,16 +14256,6 @@
"@maxmind/geoip2-node": "^3.4.0"
}
},
- "@sentry/core": {
- "version": "7.21.1",
- "resolved": "https://registry.npmjs.org/@sentry/core/-/core-7.21.1.tgz",
- "integrity": "sha512-Og5wEEsy24fNvT/T7IKjcV4EvVK5ryY2kxbJzKY6GU2eX+i+aBl+n/vp7U0Es351C/AlTkS+0NOUsp2TQQFxZA==",
- "requires": {
- "@sentry/types": "7.21.1",
- "@sentry/utils": "7.21.1",
- "tslib": "^1.9.3"
- }
- },
"@protobufjs/aspromise": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/@protobufjs/aspromise/-/aspromise-1.1.2.tgz",
@@ -14370,6 +14310,16 @@
"resolved": "https://registry.npmjs.org/@protobufjs/utf8/-/utf8-1.1.0.tgz",
"integrity": "sha512-Vvn3zZrhQZkkBE8LSuW3em98c0FwgO4nxzv6OdSxPKJIEKY2bGbHn+mhGIPerzI4twdxaP8/0+06HBpwf345Lw=="
},
+ "@sentry/core": {
+ "version": "7.21.1",
+ "resolved": "https://registry.npmjs.org/@sentry/core/-/core-7.21.1.tgz",
+ "integrity": "sha512-Og5wEEsy24fNvT/T7IKjcV4EvVK5ryY2kxbJzKY6GU2eX+i+aBl+n/vp7U0Es351C/AlTkS+0NOUsp2TQQFxZA==",
+ "requires": {
+ "@sentry/types": "7.21.1",
+ "@sentry/utils": "7.21.1",
+ "tslib": "^1.9.3"
+ }
+ },
"@sentry/node": {
"version": "7.21.1",
"resolved": "https://registry.npmjs.org/@sentry/node/-/node-7.21.1.tgz",
@@ -14409,20 +14359,6 @@
"tslib": "^1.9.3"
}
},
- "@sentry/types": {
- "version": "7.21.1",
- "resolved": "https://registry.npmjs.org/@sentry/types/-/types-7.21.1.tgz",
- "integrity": "sha512-3/IKnd52Ol21amQvI+kz+WB76s8/LR5YvFJzMgIoI2S8d82smIr253zGijRXxHPEif8kMLX4Yt+36VzrLxg6+A=="
- },
- "@sentry/utils": {
- "version": "7.21.1",
- "resolved": "https://registry.npmjs.org/@sentry/utils/-/utils-7.21.1.tgz",
- "integrity": "sha512-F0W0AAi8tgtTx6ApZRI2S9HbXEA9ENX1phTZgdNNWcMFm1BNbc21XEwLqwXBNjub5nlA6CE8xnjXRgdZKx4kzQ==",
- "requires": {
- "@sentry/types": "7.21.1",
- "tslib": "^1.9.3"
- }
- },
"@sinclair/typebox": {
"version": "0.24.51",
"resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.24.51.tgz",
diff --git a/backend/src/app.ts b/backend/src/app.ts
index 79561d00c..cf44804e9 100644
--- a/backend/src/app.ts
+++ b/backend/src/app.ts
@@ -50,6 +50,7 @@ import {
serviceTokenData as v2ServiceTokenDataRouter,
apiKeyData as v2APIKeyDataRouter,
environment as v2EnvironmentRouter,
+ tags as v2TagsRouter,
} from './routes/v2';
import { healthCheck } from './routes/status';
@@ -112,6 +113,7 @@ app.use('/api/v1/integration-auth', v1IntegrationAuthRouter);
app.use('/api/v2/users', v2UsersRouter);
app.use('/api/v2/organizations', v2OrganizationsRouter);
app.use('/api/v2/workspace', v2EnvironmentRouter);
+app.use('/api/v2/workspace', v2TagsRouter);
app.use('/api/v2/workspace', v2WorkspaceRouter);
app.use('/api/v2/secret', v2SecretRouter); // deprecated
app.use('/api/v2/secrets', v2SecretsRouter);
diff --git a/backend/src/config/index.ts b/backend/src/config/index.ts
index 2ecd1af8c..ad1acb1a6 100644
--- a/backend/src/config/index.ts
+++ b/backend/src/config/index.ts
@@ -13,10 +13,13 @@ const MONGO_URL = process.env.MONGO_URL!;
const NODE_ENV = process.env.NODE_ENV! || 'production';
const VERBOSE_ERROR_OUTPUT = process.env.VERBOSE_ERROR_OUTPUT! === 'true' && true;
const LOKI_HOST = process.env.LOKI_HOST || undefined;
+const CLIENT_ID_AZURE = process.env.CLIENT_ID_AZURE!;
+const TENANT_ID_AZURE = process.env.TENANT_ID_AZURE!;
const CLIENT_ID_HEROKU = process.env.CLIENT_ID_HEROKU!;
const CLIENT_ID_VERCEL = process.env.CLIENT_ID_VERCEL!;
const CLIENT_ID_NETLIFY = process.env.CLIENT_ID_NETLIFY!;
const CLIENT_ID_GITHUB = process.env.CLIENT_ID_GITHUB!;
+const CLIENT_SECRET_AZURE = process.env.CLIENT_SECRET_AZURE!;
const CLIENT_SECRET_HEROKU = process.env.CLIENT_SECRET_HEROKU!;
const CLIENT_SECRET_VERCEL = process.env.CLIENT_SECRET_VERCEL!;
const CLIENT_SECRET_NETLIFY = process.env.CLIENT_SECRET_NETLIFY!;
@@ -60,10 +63,13 @@ export {
NODE_ENV,
VERBOSE_ERROR_OUTPUT,
LOKI_HOST,
+ CLIENT_ID_AZURE,
+ TENANT_ID_AZURE,
CLIENT_ID_HEROKU,
CLIENT_ID_VERCEL,
CLIENT_ID_NETLIFY,
CLIENT_ID_GITHUB,
+ CLIENT_SECRET_AZURE,
CLIENT_SECRET_HEROKU,
CLIENT_SECRET_VERCEL,
CLIENT_SECRET_NETLIFY,
diff --git a/backend/src/controllers/v1/authController.ts b/backend/src/controllers/v1/authController.ts
index 19850680e..6e2de53b3 100644
--- a/backend/src/controllers/v1/authController.ts
+++ b/backend/src/controllers/v1/authController.ts
@@ -4,16 +4,21 @@ import jwt from 'jsonwebtoken';
import * as Sentry from '@sentry/node';
import * as bigintConversion from 'bigint-conversion';
const jsrp = require('jsrp');
-import { User } from '../../models';
+import { User, LoginSRPDetail } from '../../models';
import { createToken, issueTokens, clearTokens } from '../../helpers/auth';
+import {
+ ACTION_LOGIN,
+ ACTION_LOGOUT
+} from '../../variables';
import {
NODE_ENV,
JWT_AUTH_LIFETIME,
JWT_AUTH_SECRET,
JWT_REFRESH_SECRET
} from '../../config';
-import LoginSRPDetail from '../../models/LoginSRPDetail';
import { BadRequestError } from '../../utils/errors';
+import { EELogService } from '../../ee/services';
+import { getChannelFromUserAgent } from '../../utils/posthog'; // TODO: move this
declare module 'jsonwebtoken' {
export interface UserIDJwtPayload extends jwt.JwtPayload {
@@ -116,6 +121,18 @@ export const login2 = async (req: Request, res: Response) => {
secure: NODE_ENV === 'production' ? true : false
});
+ const loginAction = await EELogService.createAction({
+ name: ACTION_LOGIN,
+ userId: user._id
+ });
+
+ loginAction && await EELogService.createLog({
+ userId: user._id,
+ actions: [loginAction],
+ channel: getChannelFromUserAgent(req.headers['user-agent']),
+ ipAddress: req.ip
+ });
+
// return (access) token in response
return res.status(200).send({
token: tokens.token,
@@ -159,6 +176,19 @@ export const logout = async (req: Request, res: Response) => {
sameSite: 'strict',
secure: NODE_ENV === 'production' ? true : false
});
+
+ const logoutAction = await EELogService.createAction({
+ name: ACTION_LOGOUT,
+ userId: req.user._id
+ });
+
+ logoutAction && await EELogService.createLog({
+ userId: req.user._id,
+ actions: [logoutAction],
+ channel: getChannelFromUserAgent(req.headers['user-agent']),
+ ipAddress: req.ip
+ });
+
} catch (err) {
Sentry.setUser({ email: req.user.email });
Sentry.captureException(err);
diff --git a/backend/src/controllers/v1/integrationAuthController.ts b/backend/src/controllers/v1/integrationAuthController.ts
index cb5952986..215353445 100644
--- a/backend/src/controllers/v1/integrationAuthController.ts
+++ b/backend/src/controllers/v1/integrationAuthController.ts
@@ -1,10 +1,39 @@
-import { Request, Response } from "express";
-import { Types } from "mongoose";
-import * as Sentry from "@sentry/node";
-import { Integration, IntegrationAuth, Bot } from "../../models";
-import { INTEGRATION_SET, INTEGRATION_OPTIONS } from "../../variables";
-import { IntegrationService } from "../../services";
-import { getApps, revokeAccess } from "../../integrations";
+import { Request, Response } from 'express';
+import { Types } from 'mongoose';
+import * as Sentry from '@sentry/node';
+import {
+ Integration,
+ IntegrationAuth,
+ Bot
+} from '../../models';
+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) => {
return res.status(200).send({
@@ -18,35 +47,37 @@ export const getIntegrationOptions = async (req: Request, res: Response) => {
* @param res
* @returns
*/
-export const oAuthExchange = async (req: Request, res: Response) => {
- try {
- const { workspaceId, code, integration } = req.body;
-
- if (!INTEGRATION_SET.has(integration))
- throw new Error("Failed to validate integration");
-
- const environments = req.membership.workspace?.environments || [];
- if (environments.length === 0) {
- throw new Error("Failed to get environments");
- }
-
- await IntegrationService.handleOAuthExchange({
- workspaceId,
- integration,
- code,
- environment: environments[0].slug,
- });
- } catch (err) {
- Sentry.setUser({ email: req.user.email });
- Sentry.captureException(err);
- return res.status(400).send({
- message: "Failed to get OAuth2 code-token exchange",
- });
- }
-
- return res.status(200).send({
- message: "Successfully enabled integration authorization",
- });
+export const oAuthExchange = async (
+ req: Request,
+ res: Response
+) => {
+ try {
+ const { workspaceId, code, integration } = req.body;
+ if (!INTEGRATION_SET.has(integration))
+ throw new Error('Failed to validate integration');
+
+ const environments = req.membership.workspace?.environments || [];
+ if(environments.length === 0){
+ throw new Error("Failed to get environments")
+ }
+
+ 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);
+ return res.status(400).send({
+ message: 'Failed to get OAuth2 code-token exchange'
+ });
+ }
};
/**
@@ -71,50 +102,44 @@ export const saveIntegrationAccessToken = async (
integration: string;
} = req.body;
- integrationAuth = await IntegrationAuth.findOneAndUpdate(
- {
- workspace: new Types.ObjectId(workspaceId),
- integration,
- },
- {
- workspace: new Types.ObjectId(workspaceId),
- integration,
- },
- {
- 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');
- 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({
- integrationAuthId: integrationAuth._id.toString(),
- accessToken,
- accessExpiresAt: undefined,
- });
-
- if (!integrationAuth)
- throw new Error("Failed to save integration access token");
- } catch (err) {
- Sentry.setUser({ email: req.user.email });
- Sentry.captureException(err);
- return res.status(400).send({
- message: "Failed to save access token for integration",
- });
- }
-
- return res.status(200).send({
- integrationAuth,
- });
-};
+ integrationAuth = await IntegrationAuth.findOneAndUpdate({
+ workspace: new Types.ObjectId(workspaceId),
+ integration
+ }, {
+ workspace: new Types.ObjectId(workspaceId),
+ integration
+ }, {
+ new: true,
+ upsert: true
+ });
+
+ // encrypt and save integration access token
+ integrationAuth = await IntegrationService.setIntegrationAuthAccess({
+ integrationAuthId: integrationAuth._id.toString(),
+ accessToken,
+ accessExpiresAt: undefined
+ });
+
+ if (!integrationAuth) throw new Error('Failed to save integration access token');
+ } catch (err) {
+ Sentry.setUser({ email: req.user.email });
+ Sentry.captureException(err);
+ return res.status(400).send({
+ message: 'Failed to save access token for integration'
+ });
+ }
+
+ return res.status(200).send({
+ integrationAuth
+ });
+}
/**
* Return list of applications allowed for integration with integration authorization id [integrationAuthId]
diff --git a/backend/src/controllers/v1/integrationController.ts b/backend/src/controllers/v1/integrationController.ts
index cdd30e7a7..631d035ca 100644
--- a/backend/src/controllers/v1/integrationController.ts
+++ b/backend/src/controllers/v1/integrationController.ts
@@ -1,8 +1,14 @@
-import { Request, Response } from "express";
-import * as Sentry from "@sentry/node";
-import { Integration, Workspace, Bot, BotKey } from "../../models";
-import { EventService } from "../../services";
-import { eventPushSecrets } from "../../events";
+import { Request, Response } from 'express';
+import { Types } from 'mongoose';
+import * as Sentry from '@sentry/node';
+import {
+ Integration,
+ Workspace,
+ Bot,
+ BotKey
+} from '../../models';
+import { EventService } from '../../services';
+import { eventPushSecrets } from '../../events';
/**
* Create/initialize an (empty) integration for integration authorization
@@ -11,24 +17,49 @@ import { eventPushSecrets } from "../../events";
* @returns
*/
export const createIntegration = async (req: Request, res: Response) => {
- let integration;
- try {
- // 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,
- integration: req.integrationAuth.integration,
- integrationAuth: req.integrationAuth._id,
- }).save();
- } catch (err) {
- Sentry.setUser({ email: req.user.email });
- Sentry.captureException(err);
- return res.status(400).send({
- message: "Failed to create integration",
- });
- }
+ 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,
+ environment: sourceEnvironment,
+ isActive,
+ app,
+ appId,
+ targetEnvironment,
+ owner,
+ integration: req.integrationAuth.integration,
+ 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);
+ return res.status(400).send({
+ message: 'Failed to create integration'
+ });
+ }
return res.status(200).send({
integration,
diff --git a/backend/src/controllers/v1/membershipOrgController.ts b/backend/src/controllers/v1/membershipOrgController.ts
index f3703b889..0040b583e 100644
--- a/backend/src/controllers/v1/membershipOrgController.ts
+++ b/backend/src/controllers/v1/membershipOrgController.ts
@@ -77,8 +77,6 @@ export const changeMembershipOrgRole = async (req: Request, res: Response) => {
// change role for (target) organization membership with id
// [membershipOrgId]
- // TODO
-
let membershipToChangeRole;
// try {
// } catch (err) {
diff --git a/backend/src/controllers/v1/passwordController.ts b/backend/src/controllers/v1/passwordController.ts
index d429cbf41..0c5530f58 100644
--- a/backend/src/controllers/v1/passwordController.ts
+++ b/backend/src/controllers/v1/passwordController.ts
@@ -4,12 +4,11 @@ import crypto from 'crypto';
// eslint-disable-next-line @typescript-eslint/no-var-requires
const jsrp = require('jsrp');
import * as bigintConversion from 'bigint-conversion';
-import { User, Token, BackupPrivateKey } from '../../models';
+import { User, Token, BackupPrivateKey, LoginSRPDetail } from '../../models';
import { checkEmailVerification } from '../../helpers/signup';
import { createToken } from '../../helpers/auth';
import { sendMail } from '../../helpers/nodemailer';
import { JWT_SIGNUP_LIFETIME, JWT_SIGNUP_SECRET, SITE_URL } from '../../config';
-import LoginSRPDetail from '../../models/LoginSRPDetail';
import { BadRequestError } from '../../utils/errors';
/**
diff --git a/backend/src/controllers/v2/index.ts b/backend/src/controllers/v2/index.ts
index 936f5e281..3183ac60f 100644
--- a/backend/src/controllers/v2/index.ts
+++ b/backend/src/controllers/v2/index.ts
@@ -6,6 +6,7 @@ import * as apiKeyDataController from './apiKeyDataController';
import * as secretController from './secretController';
import * as secretsController from './secretsController';
import * as environmentController from './environmentController';
+import * as tagController from './tagController';
export {
usersController,
@@ -15,5 +16,6 @@ export {
apiKeyDataController,
secretController,
secretsController,
- environmentController
+ environmentController,
+ tagController
}
diff --git a/backend/src/controllers/v2/secretsController.ts b/backend/src/controllers/v2/secretsController.ts
index 90eb8e48d..ff39791ac 100644
--- a/backend/src/controllers/v2/secretsController.ts
+++ b/backend/src/controllers/v2/secretsController.ts
@@ -79,24 +79,38 @@ export const createSecrets = async (req: Request, res: Response) => {
*/
const channel = getChannelFromUserAgent(req.headers['user-agent'])
- const { workspaceId, environment } = req.body;
+ const { workspaceId, environment }: { workspaceId: string, environment: string } = req.body;
const hasAccess = await userHasWorkspaceAccess(req.user, workspaceId, environment, ABILITY_WRITE)
if (!hasAccess) {
throw UnauthorizedRequestError({ message: "You do not have the necessary permission(s) perform this action" })
}
- let toAdd;
+ let listOfSecretsToCreate;
if (Array.isArray(req.body.secrets)) {
// case: create multiple secrets
- toAdd = req.body.secrets;
+ listOfSecretsToCreate = req.body.secrets;
} else if (typeof req.body.secrets === 'object') {
// case: create 1 secret
- toAdd = [req.body.secrets];
+ listOfSecretsToCreate = [req.body.secrets];
}
- const newSecrets = await Secret.insertMany(
- toAdd.map(({
+ type secretsToCreateType = {
+ type: string;
+ secretKeyCiphertext: string;
+ secretKeyIV: string;
+ secretKeyTag: string;
+ secretValueCiphertext: string;
+ secretValueIV: string;
+ secretValueTag: string;
+ secretCommentCiphertext: string;
+ secretCommentIV: string;
+ secretCommentTag: string;
+ tags: string[]
+ }
+
+ const newlyCreatedSecrets = await Secret.insertMany(
+ listOfSecretsToCreate.map(({
type,
secretKeyCiphertext,
secretKeyIV,
@@ -104,15 +118,11 @@ export const createSecrets = async (req: Request, res: Response) => {
secretValueCiphertext,
secretValueIV,
secretValueTag,
- }: {
- type: string;
- secretKeyCiphertext: string;
- secretKeyIV: string;
- secretKeyTag: string;
- secretValueCiphertext: string;
- secretValueIV: string;
- secretValueTag: string;
- }) => {
+ secretCommentCiphertext,
+ secretCommentIV,
+ secretCommentTag,
+ tags
+ }: secretsToCreateType) => {
return ({
version: 1,
workspace: new Types.ObjectId(workspaceId),
@@ -124,7 +134,11 @@ export const createSecrets = async (req: Request, res: Response) => {
secretKeyTag,
secretValueCiphertext,
secretValueIV,
- secretValueTag
+ secretValueTag,
+ secretCommentCiphertext,
+ secretCommentIV,
+ secretCommentTag,
+ tags
});
})
);
@@ -140,7 +154,7 @@ export const createSecrets = async (req: Request, res: Response) => {
// (EE) add secret versions for new secrets
await EESecretService.addSecretVersions({
- secretVersions: newSecrets.map(({
+ secretVersions: newlyCreatedSecrets.map(({
_id,
version,
workspace,
@@ -154,7 +168,11 @@ export const createSecrets = async (req: Request, res: Response) => {
secretValueCiphertext,
secretValueIV,
secretValueTag,
- secretValueHash
+ secretValueHash,
+ secretCommentCiphertext,
+ secretCommentIV,
+ secretCommentTag,
+ tags
}) => ({
_id: new Types.ObjectId(),
secret: _id,
@@ -171,21 +189,25 @@ export const createSecrets = async (req: Request, res: Response) => {
secretValueCiphertext,
secretValueIV,
secretValueTag,
- secretValueHash
+ secretValueHash,
+ secretCommentCiphertext,
+ secretCommentIV,
+ secretCommentTag,
+ tags
}))
});
- const addAction = await EELogService.createActionSecret({
+ const addAction = await EELogService.createAction({
name: ACTION_ADD_SECRETS,
- userId: req.user._id.toString(),
- workspaceId,
- secretIds: newSecrets.map((n) => n._id)
+ userId: req.user._id,
+ workspaceId: new Types.ObjectId(workspaceId),
+ secretIds: newlyCreatedSecrets.map((n) => n._id)
});
// (EE) create (audit) log
addAction && await EELogService.createLog({
userId: req.user._id.toString(),
- workspaceId,
+ workspaceId: new Types.ObjectId(workspaceId),
actions: [addAction],
channel,
ipAddress: req.ip
@@ -201,7 +223,7 @@ export const createSecrets = async (req: Request, res: Response) => {
event: 'secrets added',
distinctId: req.user.email,
properties: {
- numberOfSecrets: toAdd.length,
+ numberOfSecrets: listOfSecretsToCreate.length,
environment,
workspaceId,
channel: channel,
@@ -211,7 +233,7 @@ export const createSecrets = async (req: Request, res: Response) => {
}
return res.status(200).send({
- secrets: newSecrets
+ secrets: newlyCreatedSecrets
});
}
@@ -294,22 +316,22 @@ export const getSecrets = async (req: Request, res: Response) => {
],
type: { $in: [SECRET_SHARED, SECRET_PERSONAL] }
}
- ).then())
+ ).populate("tags").then())
if (err) throw ValidationError({ message: 'Failed to get secrets', stack: err.stack });
const channel = getChannelFromUserAgent(req.headers['user-agent'])
- const readAction = await EELogService.createActionSecret({
+ const readAction = await EELogService.createAction({
name: ACTION_READ_SECRETS,
- userId: userId,
- workspaceId: workspaceId as string,
+ userId: new Types.ObjectId(userId),
+ workspaceId: new Types.ObjectId(workspaceId as string),
secretIds: secrets.map((n: any) => n._id)
});
readAction && await EELogService.createLog({
- userId: userId,
- workspaceId: workspaceId as string,
+ userId: new Types.ObjectId(userId),
+ workspaceId: new Types.ObjectId(workspaceId as string),
actions: [readAction],
channel,
ipAddress: req.ip
@@ -398,6 +420,7 @@ export const updateSecrets = async (req: Request, res: Response) => {
secretCommentCiphertext: string;
secretCommentIV: string;
secretCommentTag: string;
+ tags: string[]
}
const updateOperationsToPerform = req.body.secrets.map((secret: PatchSecret) => {
@@ -410,7 +433,8 @@ export const updateSecrets = async (req: Request, res: Response) => {
secretValueTag,
secretCommentCiphertext,
secretCommentIV,
- secretCommentTag
+ secretCommentTag,
+ tags
} = secret;
return ({
@@ -426,6 +450,7 @@ export const updateSecrets = async (req: Request, res: Response) => {
secretValueCiphertext,
secretValueIV,
secretValueTag,
+ tags,
...((
secretCommentCiphertext &&
secretCommentIV &&
@@ -460,6 +485,7 @@ export const updateSecrets = async (req: Request, res: Response) => {
secretCommentCiphertext,
secretCommentIV,
secretCommentTag,
+ tags
} = secretModificationsBySecretId[secret._id.toString()]
return ({
@@ -477,6 +503,7 @@ export const updateSecrets = async (req: Request, res: Response) => {
secretCommentCiphertext: secretCommentCiphertext ? secretCommentCiphertext : secret.secretCommentCiphertext,
secretCommentIV: secretCommentIV ? secretCommentIV : secret.secretCommentIV,
secretCommentTag: secretCommentTag ? secretCommentTag : secret.secretCommentTag,
+ tags: tags ? tags : secret.tags
});
})
}
@@ -505,17 +532,17 @@ export const updateSecrets = async (req: Request, res: Response) => {
});
}, 10000);
- const updateAction = await EELogService.createActionSecret({
+ const updateAction = await EELogService.createAction({
name: ACTION_UPDATE_SECRETS,
- userId: req.user._id.toString(),
- workspaceId: key,
+ userId: req.user._id,
+ workspaceId: new Types.ObjectId(key),
secretIds: workspaceSecretObj[key].map((secret: ISecret) => secret._id)
});
// (EE) create (audit) log
updateAction && await EELogService.createLog({
userId: req.user._id.toString(),
- workspaceId: key,
+ workspaceId: new Types.ObjectId(key),
actions: [updateAction],
channel,
ipAddress: req.ip
@@ -631,17 +658,17 @@ export const deleteSecrets = async (req: Request, res: Response) => {
workspaceId: key
})
});
- const deleteAction = await EELogService.createActionSecret({
+ const deleteAction = await EELogService.createAction({
name: ACTION_DELETE_SECRETS,
- userId: req.user._id.toString(),
- workspaceId: key,
+ userId: req.user._id,
+ workspaceId: new Types.ObjectId(key),
secretIds: workspaceSecretObj[key].map((secret: ISecret) => secret._id)
});
// (EE) create (audit) log
deleteAction && await EELogService.createLog({
userId: req.user._id.toString(),
- workspaceId: key,
+ workspaceId: new Types.ObjectId(key),
actions: [deleteAction],
channel,
ipAddress: req.ip
diff --git a/backend/src/controllers/v2/tagController.ts b/backend/src/controllers/v2/tagController.ts
new file mode 100644
index 000000000..b737eb41a
--- /dev/null
+++ b/backend/src/controllers/v2/tagController.ts
@@ -0,0 +1,66 @@
+import { Request, Response } from 'express';
+import * as Sentry from '@sentry/node';
+import { Types } from 'mongoose';
+import {
+ Membership,
+} from '../../models';
+import Tag, { ITag } from '../../models/tag';
+import { Builder } from "builder-pattern"
+import to from 'await-to-js';
+import { BadRequestError, UnauthorizedRequestError } from '../../utils/errors';
+import { MongoError } from 'mongodb';
+import { userHasWorkspaceAccess } from '../../ee/helpers/checkMembershipPermissions';
+
+export const createWorkspaceTag = async (req: Request, res: Response) => {
+ const { workspaceId } = req.params
+ const { name, slug } = req.body
+ const sanitizedTagToCreate = Builder()
+ .name(name)
+ .workspace(new Types.ObjectId(workspaceId))
+ .slug(slug)
+ .user(new Types.ObjectId(req.user._id))
+ .build();
+
+ const [err, createdTag] = await to(Tag.create(sanitizedTagToCreate))
+
+ if (err) {
+ if ((err as MongoError).code === 11000) {
+ throw BadRequestError({ message: "Tags must be unique in a workspace" })
+ }
+
+ throw err
+ }
+
+ res.json(createdTag)
+}
+
+export const deleteWorkspaceTag = async (req: Request, res: Response) => {
+ const { tagId } = req.params
+
+ const tagFromDB = await Tag.findById(tagId)
+ if (!tagFromDB) {
+ throw BadRequestError()
+ }
+
+ // can only delete if the request user is one that belongs to the same workspace as the tag
+ const membership = await Membership.findOne({
+ user: req.user,
+ workspace: tagFromDB.workspace
+ });
+
+ if (!membership) {
+ UnauthorizedRequestError({ message: 'Failed to validate membership' });
+ }
+
+ const result = await Tag.findByIdAndDelete(tagId);
+
+ res.json(result);
+}
+
+export const getWorkspaceTags = async (req: Request, res: Response) => {
+ const { workspaceId } = req.params
+ const workspaceTags = await Tag.find({ workspace: workspaceId })
+ return res.json({
+ workspaceTags
+ })
+}
\ No newline at end of file
diff --git a/backend/src/controllers/v2/workspaceController.ts b/backend/src/controllers/v2/workspaceController.ts
index 90781b58c..55cd02fff 100644
--- a/backend/src/controllers/v2/workspaceController.ts
+++ b/backend/src/controllers/v2/workspaceController.ts
@@ -467,4 +467,42 @@ export const deleteWorkspaceMembership = async (req: Request, res: Response) =>
return res.status(200).send({
membership
});
-}
\ No newline at end of file
+}
+
+/**
+ * Change autoCapitilzation Rule of workspace
+ * @param req
+ * @param res
+ * @returns
+ */
+export const toggleAutoCapitalization = async (req: Request, res: Response) => {
+ let workspace;
+ try {
+ const { workspaceId } = req.params;
+ const { autoCapitalization } = req.body;
+
+ workspace = await Workspace.findOneAndUpdate(
+ {
+ _id: workspaceId
+ },
+ {
+ autoCapitalization
+ },
+ {
+ new: true
+ }
+ );
+ } catch (err) {
+ Sentry.setUser({ email: req.user.email });
+ Sentry.captureException(err);
+ return res.status(400).send({
+ message: 'Failed to change autoCapitalization setting'
+ });
+ }
+
+ return res.status(200).send({
+ message: 'Successfully changed autoCapitalization setting',
+ workspace
+ });
+};
+
diff --git a/backend/src/ee/controllers/v1/secretSnapshotController.ts b/backend/src/ee/controllers/v1/secretSnapshotController.ts
index 6e8605c2f..ae3af7958 100644
--- a/backend/src/ee/controllers/v1/secretSnapshotController.ts
+++ b/backend/src/ee/controllers/v1/secretSnapshotController.ts
@@ -15,7 +15,13 @@ export const getSecretSnapshot = async (req: Request, res: Response) => {
secretSnapshot = await SecretSnapshot
.findById(secretSnapshotId)
- .populate('secretVersions');
+ .populate({
+ path: 'secretVersions',
+ populate: {
+ path: 'tags',
+ model: 'Tag',
+ }
+ });
if (!secretSnapshot) throw new Error('Failed to find secret snapshot');
diff --git a/backend/src/ee/helpers/action.ts b/backend/src/ee/helpers/action.ts
index 389cfedbf..eb94810f4 100644
--- a/backend/src/ee/helpers/action.ts
+++ b/backend/src/ee/helpers/action.ts
@@ -1,39 +1,40 @@
import * as Sentry from '@sentry/node';
import { Types } from 'mongoose';
-import { SecretVersion, Action } from '../models';
+import { Action } from '../models';
import {
getLatestSecretVersionIds,
getLatestNSecretSecretVersionIds
} from '../helpers/secretVersion';
-import { ACTION_UPDATE_SECRETS } from '../../variables';
+import {
+ ACTION_LOGIN,
+ ACTION_LOGOUT,
+ ACTION_ADD_SECRETS,
+ ACTION_READ_SECRETS,
+ ACTION_DELETE_SECRETS,
+ ACTION_UPDATE_SECRETS,
+} from '../../variables';
/**
- * Create an (audit) action for secrets including
- * add, delete, update, and read actions.
+ * Create an (audit) action for updating secrets
* @param {Object} obj
* @param {String} obj.name - name of action
- * @param {ObjectId[]} obj.secretIds - ids of relevant secrets
+ * @param {Types.ObjectId} obj.secretIds - ids of relevant secrets
* @returns {Action} action - new action
*/
-const createActionSecretHelper = async ({
+const createActionUpdateSecret = async ({
name,
userId,
workspaceId,
secretIds
}: {
name: string;
- userId: string;
- workspaceId: string;
+ userId: Types.ObjectId;
+ workspaceId: Types.ObjectId;
secretIds: Types.ObjectId[];
}) => {
-
let action;
- let latestSecretVersions;
try {
- if (name === ACTION_UPDATE_SECRETS) {
- // case: action is updating secrets
- // -> add old and new secret versions
- latestSecretVersions = (await getLatestNSecretSecretVersionIds({
+ const latestSecretVersions = (await getLatestNSecretSecretVersionIds({
secretIds,
n: 2
}))
@@ -41,17 +42,7 @@ const createActionSecretHelper = async ({
oldSecretVersion: s.versions[0]._id,
newSecretVersion: s.versions[1]._id
}));
- } else {
- // case: action is adding, deleting, or reading secrets
- // -> add new secret versions
- latestSecretVersions = (await getLatestSecretVersionIds({
- secretIds
- }))
- .map((s) => ({
- newSecretVersion: s.versionId
- }));
- }
-
+
action = await new Action({
name,
user: userId,
@@ -64,10 +55,148 @@ const createActionSecretHelper = async ({
} catch (err) {
Sentry.setUser(null);
Sentry.captureException(err);
+ throw new Error('Failed to create update secret action');
+ }
+
+ return action;
+}
+
+/**
+ * Create an (audit) action for creating, reading, and deleting
+ * secrets
+ * @param {Object} obj
+ * @param {String} obj.name - name of action
+ * @param {Types.ObjectId} obj.secretIds - ids of relevant secrets
+ * @returns {Action} action - new action
+ */
+const createActionSecret = async ({
+ name,
+ userId,
+ workspaceId,
+ secretIds
+}: {
+ name: string;
+ userId: Types.ObjectId;
+ workspaceId: Types.ObjectId;
+ secretIds: Types.ObjectId[];
+}) => {
+ let action;
+ try {
+ // case: action is adding, deleting, or reading secrets
+ // -> add new secret versions
+ const latestSecretVersions = (await getLatestSecretVersionIds({
+ secretIds
+ }))
+ .map((s) => ({
+ newSecretVersion: s.versionId
+ }));
+
+ action = await new Action({
+ name,
+ user: userId,
+ workspace: workspaceId,
+ payload: {
+ secretVersions: latestSecretVersions
+ }
+ }).save();
+
+ } catch (err) {
+ Sentry.setUser(null);
+ Sentry.captureException(err);
+ throw new Error('Failed to create action create/read/delete secret action');
+ }
+
+ return action;
+}
+
+/**
+ * Create an (audit) action for user with id [userId]
+ * @param {Object} obj
+ * @param {String} obj.name - name of action
+ * @param {String} obj.userId - id of user associated with action
+ * @returns
+ */
+const createActionUser = ({
+ name,
+ userId
+}: {
+ name: string;
+ userId: Types.ObjectId;
+}) => {
+ let action;
+ try {
+ action = new Action({
+ name,
+ user: userId
+ }).save();
+ } catch (err) {
+ Sentry.setUser(null);
+ Sentry.captureException(err);
+ throw new Error('Failed to create user action');
+ }
+
+ return action;
+}
+
+/**
+ * Create an (audit) action.
+ * @param {Object} obj
+ * @param {Object} obj.name - name of action
+ * @param {Types.ObjectId} obj.userId - id of user associated with action
+ * @param {Types.ObjectId} obj.workspaceId - id of workspace associated with action
+ * @param {Types.ObjectId[]} obj.secretIds - ids of secrets associated with action
+*/
+const createActionHelper = async ({
+ name,
+ userId,
+ workspaceId,
+ secretIds,
+}: {
+ name: string;
+ userId: Types.ObjectId;
+ workspaceId?: Types.ObjectId;
+ secretIds?: Types.ObjectId[];
+}) => {
+ let action;
+ try {
+ switch (name) {
+ case ACTION_LOGIN:
+ case ACTION_LOGOUT:
+ action = await createActionUser({
+ name,
+ userId
+ });
+ break;
+ case ACTION_ADD_SECRETS:
+ case ACTION_READ_SECRETS:
+ case ACTION_DELETE_SECRETS:
+ if (!workspaceId || !secretIds) throw new Error('Missing required params workspace id or secret ids to create action secret');
+ action = await createActionSecret({
+ name,
+ userId,
+ workspaceId,
+ secretIds
+ });
+ break;
+ case ACTION_UPDATE_SECRETS:
+ if (!workspaceId || !secretIds) throw new Error('Missing required params workspace id or secret ids to create action secret');
+ action = await createActionUpdateSecret({
+ name,
+ userId,
+ workspaceId,
+ secretIds
+ });
+ break;
+ }
+ } catch (err) {
+ Sentry.setUser(null);
+ Sentry.captureException(err);
throw new Error('Failed to create action');
}
return action;
}
-export { createActionSecretHelper };
\ No newline at end of file
+export {
+ createActionHelper
+};
\ No newline at end of file
diff --git a/backend/src/ee/helpers/log.ts b/backend/src/ee/helpers/log.ts
index c357c9818..bdb0a2380 100644
--- a/backend/src/ee/helpers/log.ts
+++ b/backend/src/ee/helpers/log.ts
@@ -1,9 +1,19 @@
import * as Sentry from '@sentry/node';
+import { Types } from 'mongoose';
import {
Log,
IAction
} from '../models';
-
+/**
+ * Create an (audit) log
+ * @param {Object} obj
+ * @param {Types.ObjectId} obj.userId - id of user associated with the log
+ * @param {Types.ObjectId} obj.workspaceId - id of workspace associated with the log
+ * @param {IAction[]} obj.actions - actions to include in log
+ * @param {String} obj.channel - channel (web/cli/auto) associated with the log
+ * @param {String} obj.ipAddress - ip address associated with the log
+ * @returns {Log} log - new audit log
+ */
const createLogHelper = async ({
userId,
workspaceId,
@@ -11,8 +21,8 @@ const createLogHelper = async ({
channel,
ipAddress
}: {
- userId: string;
- workspaceId: string;
+ userId: Types.ObjectId;
+ workspaceId?: Types.ObjectId;
actions: IAction[];
channel: string;
ipAddress: string;
@@ -21,7 +31,7 @@ const createLogHelper = async ({
try {
log = await new Log({
user: userId,
- workspace: workspaceId,
+ workspace: workspaceId ?? undefined,
actionNames: actions.map((a) => a.name),
actions,
channel,
diff --git a/backend/src/ee/models/action.ts b/backend/src/ee/models/action.ts
index 3d48aa04d..7cdd7a355 100644
--- a/backend/src/ee/models/action.ts
+++ b/backend/src/ee/models/action.ts
@@ -1,10 +1,18 @@
import { Schema, model, Types } from 'mongoose';
+import {
+ ACTION_LOGIN,
+ ACTION_LOGOUT,
+ ACTION_ADD_SECRETS,
+ ACTION_UPDATE_SECRETS,
+ ACTION_READ_SECRETS,
+ ACTION_DELETE_SECRETS
+} from '../../variables';
export interface IAction {
name: string;
user?: Types.ObjectId,
workspace?: Types.ObjectId,
- payload: {
+ payload?: {
secretVersions?: Types.ObjectId[]
}
}
@@ -13,7 +21,15 @@ const actionSchema = new Schema(
{
name: {
type: String,
- required: true
+ required: true,
+ enum: [
+ ACTION_LOGIN,
+ ACTION_LOGOUT,
+ ACTION_ADD_SECRETS,
+ ACTION_UPDATE_SECRETS,
+ ACTION_READ_SECRETS,
+ ACTION_DELETE_SECRETS
+ ]
},
user: {
type: Schema.Types.ObjectId,
diff --git a/backend/src/ee/models/log.ts b/backend/src/ee/models/log.ts
index b9eadc016..47be2e58f 100644
--- a/backend/src/ee/models/log.ts
+++ b/backend/src/ee/models/log.ts
@@ -1,5 +1,7 @@
import { Schema, model, Types } from 'mongoose';
import {
+ ACTION_LOGIN,
+ ACTION_LOGOUT,
ACTION_ADD_SECRETS,
ACTION_UPDATE_SECRETS,
ACTION_READ_SECRETS,
@@ -29,6 +31,8 @@ const logSchema = new Schema(
actionNames: {
type: [String],
enum: [
+ ACTION_LOGIN,
+ ACTION_LOGOUT,
ACTION_ADD_SECRETS,
ACTION_UPDATE_SECRETS,
ACTION_READ_SECRETS,
diff --git a/backend/src/ee/models/secretVersion.ts b/backend/src/ee/models/secretVersion.ts
index 1af4aff2c..efa042765 100644
--- a/backend/src/ee/models/secretVersion.ts
+++ b/backend/src/ee/models/secretVersion.ts
@@ -21,6 +21,7 @@ export interface ISecretVersion {
secretValueIV: string;
secretValueTag: string;
secretValueHash: string;
+ tags?: string[];
}
const secretVersionSchema = new Schema(
@@ -88,7 +89,12 @@ const secretVersionSchema = new Schema(
},
secretValueHash: {
type: String
- }
+ },
+ tags: {
+ ref: 'Tag',
+ type: [Schema.Types.ObjectId],
+ default: []
+ },
},
{
timestamps: true
diff --git a/backend/src/ee/services/EELogService.ts b/backend/src/ee/services/EELogService.ts
index c1b2da6fb..bbe03e09e 100644
--- a/backend/src/ee/services/EELogService.ts
+++ b/backend/src/ee/services/EELogService.ts
@@ -1,14 +1,12 @@
import { Types } from 'mongoose';
import {
- Log,
- Action,
IAction
} from '../models';
import {
createLogHelper
} from '../helpers/log';
import {
- createActionSecretHelper
+ createActionHelper
} from '../helpers/action';
import EELicenseService from './EELicenseService';
@@ -33,8 +31,8 @@ class EELogService {
channel,
ipAddress
}: {
- userId: string;
- workspaceId: string;
+ userId: Types.ObjectId;
+ workspaceId?: Types.ObjectId;
actions: IAction[];
channel: string;
ipAddress: string;
@@ -50,26 +48,26 @@ class EELogService {
}
/**
- * Create an (audit) action for secrets including
- * add, delete, update, and read actions.
+ * Create an (audit) action
* @param {Object} obj
* @param {String} obj.name - name of action
- * @param {ObjectId[]} obj.secretIds - secret ids
+ * @param {Types.ObjectId} obj.userId - id of user associated with the action
+ * @param {Types.ObjectId} obj.workspaceId - id of workspace associated with the action
+ * @param {ObjectId[]} obj.secretIds - ids of secrets associated with the action
* @returns {Action} action - new action
*/
- static async createActionSecret({
+ static async createAction({
name,
userId,
workspaceId,
secretIds
}: {
name: string;
- userId: string;
- workspaceId: string;
- secretIds: Types.ObjectId[];
+ userId: Types.ObjectId;
+ workspaceId?: Types.ObjectId;
+ secretIds?: Types.ObjectId[];
}) {
- if (!EELicenseService.isLicenseValid) return null;
- return await createActionSecretHelper({
+ return await createActionHelper({
name,
userId,
workspaceId,
diff --git a/backend/src/helpers/integration.ts b/backend/src/helpers/integration.ts
index 1618e5e67..17338ee68 100644
--- a/backend/src/helpers/integration.ts
+++ b/backend/src/helpers/integration.ts
@@ -30,6 +30,7 @@ interface Update {
* @param {String} obj.workspaceId - id of workspace
* @param {String} obj.integration - name of integration
* @param {String} obj.code - code
+ * @returns {IntegrationAuth} integrationAuth - integration auth after OAuth2 code-token exchange
*/
const handleOAuthExchangeHelper = async ({
workspaceId,
@@ -42,7 +43,6 @@ const handleOAuthExchangeHelper = async ({
code: string;
environment: string;
}) => {
- let action;
let integrationAuth;
try {
const bot = await Bot.findOne({
@@ -98,21 +98,13 @@ const handleOAuthExchangeHelper = async ({
accessExpiresAt: res.accessExpiresAt
});
}
-
- // initialize new integration after exchange
- await new Integration({
- workspace: workspaceId,
- isActive: false,
- app: null,
- environment,
- integration,
- integrationAuth: integrationAuth._id
- }).save();
} catch (err) {
Sentry.setUser(null);
Sentry.captureException(err);
throw new Error('Failed to handle OAuth2 code-token exchange')
}
+
+ return integrationAuth;
}
/**
* Sync/push environment variables in workspace with id [workspaceId] to
diff --git a/backend/src/helpers/secret.ts b/backend/src/helpers/secret.ts
index a16d114c6..9466cf7aa 100644
--- a/backend/src/helpers/secret.ts
+++ b/backend/src/helpers/secret.ts
@@ -406,10 +406,10 @@ const v2PushSecrets = async ({
secretIds: toDelete
});
- const deleteAction = await EELogService.createActionSecret({
+ const deleteAction = await EELogService.createAction({
name: ACTION_DELETE_SECRETS,
- userId,
- workspaceId,
+ userId: new Types.ObjectId(userId),
+ workspaceId: new Types.ObjectId(userId),
secretIds: toDelete
});
@@ -499,10 +499,10 @@ const v2PushSecrets = async ({
})
});
- const updateAction = await EELogService.createActionSecret({
+ const updateAction = await EELogService.createAction({
name: ACTION_UPDATE_SECRETS,
- userId,
- workspaceId,
+ userId: new Types.ObjectId(userId),
+ workspaceId: new Types.ObjectId(workspaceId),
secretIds: toUpdate.map((u) => u._id)
});
@@ -536,10 +536,10 @@ const v2PushSecrets = async ({
})
});
- const addAction = await EELogService.createActionSecret({
+ const addAction = await EELogService.createAction({
name: ACTION_ADD_SECRETS,
- userId,
- workspaceId,
+ userId: new Types.ObjectId(userId),
+ workspaceId: new Types.ObjectId(workspaceId),
secretIds: newSecrets.map((n) => n._id)
});
addAction && actions.push(addAction);
@@ -553,8 +553,8 @@ const v2PushSecrets = async ({
// (EE) create (audit) log
if (actions.length > 0) {
await EELogService.createLog({
- userId,
- workspaceId,
+ userId: new Types.ObjectId(userId),
+ workspaceId: new Types.ObjectId(workspaceId),
actions,
channel,
ipAddress
@@ -645,16 +645,16 @@ const pullSecrets = async ({
environment
})
- const readAction = await EELogService.createActionSecret({
+ const readAction = await EELogService.createAction({
name: ACTION_READ_SECRETS,
- userId,
- workspaceId,
+ userId: new Types.ObjectId(userId),
+ workspaceId: new Types.ObjectId(workspaceId),
secretIds: secrets.map((n: any) => n._id)
});
readAction && await EELogService.createLog({
- userId,
- workspaceId,
+ userId: new Types.ObjectId(userId),
+ workspaceId: new Types.ObjectId(workspaceId),
actions: [readAction],
channel,
ipAddress
diff --git a/backend/src/integrations/apps.ts b/backend/src/integrations/apps.ts
index 2369a44e6..ef0e36455 100644
--- a/backend/src/integrations/apps.ts
+++ b/backend/src/integrations/apps.ts
@@ -3,6 +3,7 @@ import * as Sentry from "@sentry/node";
import { Octokit } from "@octokit/rest";
import { IIntegrationAuth } from "../models";
import {
+ INTEGRATION_AZURE_KEY_VAULT,
INTEGRATION_HEROKU,
INTEGRATION_VERCEL,
INTEGRATION_NETLIFY,
@@ -42,6 +43,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,
@@ -88,6 +94,15 @@ const getApps = async ({
return apps;
};
+const getAppsAzureKeyVault = async ({
+ accessToken
+}: {
+ accessToken: string;
+}) => {
+ // TODO
+ return [];
+}
+
/**
* Return list of apps for Heroku integration
* @param {Object} obj
@@ -246,14 +261,18 @@ const getAppsRender = async ({ accessToken }: { accessToken: string }) => {
await axios.get(`${INTEGRATION_RENDER_API_URL}/v1/services`, {
headers: {
Authorization: `Bearer ${accessToken}`,
+ Accept: 'application/json',
+ 'Accept-Encoding': 'application/json',
},
})
).data;
-
- apps = res.map((a: any) => ({
- name: a.service.name,
- appId: a.service.id,
- }));
+
+ apps = res
+ .map((a: any) => ({
+ name: a.service.name,
+ appId: a.service.id
+ }));
+
} catch (err) {
Sentry.setUser(null);
Sentry.captureException(err);
@@ -291,6 +310,8 @@ const getAppsFlyio = async ({ accessToken }: { accessToken: string }) => {
method: "post",
headers: {
Authorization: "Bearer " + accessToken,
+ 'Accept': 'application/json',
+ 'Accept-Encoding': 'application/json',
},
data: {
query,
diff --git a/backend/src/integrations/exchange.ts b/backend/src/integrations/exchange.ts
index 26aca5fdb..8650b31a0 100644
--- a/backend/src/integrations/exchange.ts
+++ b/backend/src/integrations/exchange.ts
@@ -1,10 +1,12 @@
import axios from 'axios';
import * as Sentry from '@sentry/node';
import {
+ INTEGRATION_AZURE_KEY_VAULT,
INTEGRATION_HEROKU,
INTEGRATION_VERCEL,
INTEGRATION_NETLIFY,
INTEGRATION_GITHUB,
+ INTEGRATION_AZURE_TOKEN_URL,
INTEGRATION_HEROKU_TOKEN_URL,
INTEGRATION_VERCEL_TOKEN_URL,
INTEGRATION_NETLIFY_TOKEN_URL,
@@ -12,15 +14,27 @@ import {
} from '../variables';
import {
SITE_URL,
+ CLIENT_ID_AZURE,
CLIENT_ID_VERCEL,
CLIENT_ID_NETLIFY,
CLIENT_ID_GITHUB,
+ CLIENT_SECRET_AZURE,
CLIENT_SECRET_HEROKU,
CLIENT_SECRET_VERCEL,
CLIENT_SECRET_NETLIFY,
CLIENT_SECRET_GITHUB
} from '../config';
+interface ExchangeCodeAzureResponse {
+ token_type: string;
+ scope: string;
+ expires_in: number;
+ ext_expires_in: number;
+ access_token: string;
+ refresh_token: string;
+ id_token: string;
+}
+
interface ExchangeCodeHerokuResponse {
token_type: string;
access_token: string;
@@ -75,6 +89,11 @@ const exchangeCode = async ({
try {
switch (integration) {
+ case INTEGRATION_AZURE_KEY_VAULT:
+ obj = await exchangeCodeAzure({
+ code
+ });
+ break;
case INTEGRATION_HEROKU:
obj = await exchangeCodeHeroku({
code
@@ -105,6 +124,46 @@ const exchangeCode = async ({
return obj;
};
+/**
+ * Return [accessToken] for Azure OAuth2 code-token exchange
+ * @param param0
+ */
+const exchangeCodeAzure = async ({
+ code
+}: {
+ code: string;
+}) => {
+ const accessExpiresAt = new Date();
+ let res: ExchangeCodeAzureResponse;
+ try {
+ res = (await axios.post(
+ INTEGRATION_AZURE_TOKEN_URL,
+ new URLSearchParams({
+ grant_type: 'authorization_code',
+ code: code,
+ scope: 'https://vault.azure.net/.default openid offline_access', // TODO: do we need all these permissions?
+ client_id: CLIENT_ID_AZURE,
+ client_secret: CLIENT_SECRET_AZURE,
+ redirect_uri: `${SITE_URL}/integrations/azure-key-vault/oauth2/callback`
+ } as any)
+ )).data;
+
+ accessExpiresAt.setSeconds(
+ accessExpiresAt.getSeconds() + res.expires_in
+ );
+ } catch (err: any) {
+ Sentry.setUser(null);
+ Sentry.captureException(err);
+ throw new Error('Failed OAuth2 code-token exchange with Azure');
+ }
+
+ return ({
+ accessToken: res.access_token,
+ refreshToken: res.refresh_token,
+ accessExpiresAt
+ });
+}
+
/**
* Return [accessToken], [accessExpiresAt], and [refreshToken] for Heroku
* OAuth2 code-token exchange
@@ -168,7 +227,7 @@ const exchangeCodeVercel = async ({ code }: { code: string }) => {
code: code,
client_id: CLIENT_ID_VERCEL,
client_secret: CLIENT_SECRET_VERCEL,
- redirect_uri: `${SITE_URL}/vercel`
+ redirect_uri: `${SITE_URL}/integrations/vercel/oauth2/callback`
} as any)
)
).data;
@@ -208,7 +267,7 @@ const exchangeCodeNetlify = async ({ code }: { code: string }) => {
code: code,
client_id: CLIENT_ID_NETLIFY,
client_secret: CLIENT_SECRET_NETLIFY,
- redirect_uri: `${SITE_URL}/netlify`
+ redirect_uri: `${SITE_URL}/integrations/netlify/oauth2/callback`
} as any)
)
).data;
@@ -260,10 +319,11 @@ const exchangeCodeGithub = async ({ code }: { code: string }) => {
client_id: CLIENT_ID_GITHUB,
client_secret: CLIENT_SECRET_GITHUB,
code: code,
- redirect_uri: `${SITE_URL}/github`
+ redirect_uri: `${SITE_URL}/integrations/github/oauth2/callback`
},
headers: {
- Accept: 'application/json'
+ 'Accept': 'application/json',
+ 'Accept-Encoding': 'application/json'
}
})
).data;
diff --git a/backend/src/integrations/refresh.ts b/backend/src/integrations/refresh.ts
index ea232f1e5..4fdbcdbbb 100644
--- a/backend/src/integrations/refresh.ts
+++ b/backend/src/integrations/refresh.ts
@@ -1,13 +1,26 @@
import axios from 'axios';
import * as Sentry from '@sentry/node';
-import { INTEGRATION_HEROKU } from '../variables';
+import { INTEGRATION_AZURE_KEY_VAULT, INTEGRATION_HEROKU } from '../variables';
import {
- CLIENT_SECRET_HEROKU
+ SITE_URL,
+ CLIENT_ID_AZURE,
+ CLIENT_SECRET_AZURE,
+ CLIENT_SECRET_HEROKU
} from '../config';
import {
- INTEGRATION_HEROKU_TOKEN_URL
+ INTEGRATION_AZURE_TOKEN_URL,
+ INTEGRATION_HEROKU_TOKEN_URL
} from '../variables';
+interface RefreshTokenAzureResponse {
+ token_type: string;
+ scope: string;
+ expires_in: number;
+ ext_expires_in: 4871;
+ access_token: string;
+ refresh_token: string;
+}
+
/**
* Return new access token by exchanging refresh token [refreshToken] for integration
* named [integration]
@@ -25,6 +38,11 @@ const exchangeRefresh = async ({
let accessToken;
try {
switch (integration) {
+ case INTEGRATION_AZURE_KEY_VAULT:
+ accessToken = await exchangeRefreshAzure({
+ refreshToken
+ });
+ break;
case INTEGRATION_HEROKU:
accessToken = await exchangeRefreshHeroku({
refreshToken
@@ -40,6 +58,38 @@ const exchangeRefresh = async ({
return accessToken;
};
+/**
+ * Return new access token by exchanging refresh token [refreshToken] for the
+ * Azure integration
+ * @param {Object} obj
+ * @param {String} obj.refreshToken - refresh token to use to get new access token for Azure
+ * @returns
+ */
+const exchangeRefreshAzure = async ({
+ refreshToken
+}: {
+ refreshToken: string;
+}) => {
+ try {
+ const res: RefreshTokenAzureResponse = (await axios.post(
+ INTEGRATION_AZURE_TOKEN_URL,
+ new URLSearchParams({
+ client_id: CLIENT_ID_AZURE,
+ scope: 'openid offline_access',
+ refresh_token: refreshToken,
+ grant_type: 'refresh_token',
+ client_secret: CLIENT_SECRET_AZURE
+ } as any)
+ )).data;
+
+ return res.access_token;
+ } catch (err) {
+ Sentry.setUser(null);
+ Sentry.captureException(err);
+ throw new Error('Failed to get refresh OAuth2 access token for Azure');
+ }
+}
+
/**
* Return new access token by exchanging refresh token [refreshToken] for the
* Heroku integration
@@ -52,23 +102,23 @@ const exchangeRefreshHeroku = async ({
}: {
refreshToken: string;
}) => {
- let accessToken;
- //TODO: Refactor code to take advantage of using RequestError. It's possible to create new types of errors for more detailed errors
- try {
- const res = await axios.post(
- INTEGRATION_HEROKU_TOKEN_URL,
- new URLSearchParams({
- grant_type: 'refresh_token',
- refresh_token: refreshToken,
- client_secret: CLIENT_SECRET_HEROKU
- } as any)
- );
+
+ let accessToken;
+ try {
+ const res = await axios.post(
+ INTEGRATION_HEROKU_TOKEN_URL,
+ new URLSearchParams({
+ grant_type: 'refresh_token',
+ refresh_token: refreshToken,
+ client_secret: CLIENT_SECRET_HEROKU
+ } as any)
+ );
accessToken = res.data.access_token;
} catch (err) {
Sentry.setUser(null);
Sentry.captureException(err);
- throw new Error('Failed to get new OAuth2 access token for Heroku');
+ throw new Error('Failed to refresh OAuth2 access token for Heroku');
}
return accessToken;
diff --git a/backend/src/integrations/sync.ts b/backend/src/integrations/sync.ts
index f56157205..bfa559c74 100644
--- a/backend/src/integrations/sync.ts
+++ b/backend/src/integrations/sync.ts
@@ -1,11 +1,10 @@
import axios from "axios";
import * as Sentry from "@sentry/node";
import { Octokit } from "@octokit/rest";
-// import * as sodium from 'libsodium-wrappers';
import sodium from "libsodium-wrappers";
-// const sodium = require('libsodium-wrappers');
import { IIntegration, IIntegrationAuth } from "../models";
import {
+ INTEGRATION_AZURE_KEY_VAULT,
INTEGRATION_HEROKU,
INTEGRATION_VERCEL,
INTEGRATION_NETLIFY,
@@ -43,6 +42,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,
@@ -97,7 +103,152 @@ const syncSecrets = async ({
} catch (err) {
Sentry.setUser(null);
Sentry.captureException(err);
- throw new Error("Failed to sync secrets to integration");
+ throw new Error('Failed to sync secrets to integration');
+ }
+};
+
+/**
+ * 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');
}
};
@@ -772,8 +923,9 @@ const syncSecretsFlyio = async ({
method: "post",
url: INTEGRATION_FLYIO_API_URL,
headers: {
- Authorization: "Bearer " + accessToken,
- "Content-Type": "application/json",
+ 'Authorization': 'Bearer ' + accessToken,
+ 'Content-Type': 'application/json',
+ 'Accept-Encoding': 'application/json'
},
data: {
query: GetSecrets,
diff --git a/backend/src/models/LoginSRPDetail.ts b/backend/src/models/LoginSRPDetail.ts
deleted file mode 100644
index f65e779c3..000000000
--- a/backend/src/models/LoginSRPDetail.ts
+++ /dev/null
@@ -1,23 +0,0 @@
-import mongoose, { Schema, model } from 'mongoose';
-
-const LoginSRPDetailSchema = new Schema(
- {
- clientPublicKey: {
- type: String,
- required: true
- },
- email: {
- type: String,
- required: true,
- unique: true
- },
- serverBInt: { type: mongoose.Schema.Types.Buffer },
- expireAt: { type: Date }
- }
-);
-
-const LoginSRPDetail = model('LoginSRPDetail', LoginSRPDetailSchema);
-
-// LoginSRPDetailSchema.index({ "expireAt": 1 }, { expireAfterSeconds: 0 });
-
-export default LoginSRPDetail;
diff --git a/backend/src/models/index.ts b/backend/src/models/index.ts
index 72ffca607..ac5f102c0 100644
--- a/backend/src/models/index.ts
+++ b/backend/src/models/index.ts
@@ -16,6 +16,7 @@ import UserAction, { IUserAction } from './userAction';
import Workspace, { IWorkspace } from './workspace';
import ServiceTokenData, { IServiceTokenData } from './serviceTokenData';
import APIKeyData, { IAPIKeyData } from './apiKeyData';
+import LoginSRPDetail, { ILoginSRPDetail } from './loginSRPDetail';
export {
BackupPrivateKey,
@@ -53,5 +54,7 @@ export {
ServiceTokenData,
IServiceTokenData,
APIKeyData,
- IAPIKeyData
+ IAPIKeyData,
+ LoginSRPDetail,
+ ILoginSRPDetail
};
diff --git a/backend/src/models/integration.ts b/backend/src/models/integration.ts
index 6417f2beb..95ed9f4a9 100644
--- a/backend/src/models/integration.ts
+++ b/backend/src/models/integration.ts
@@ -1,5 +1,6 @@
import { Schema, model, Types } from "mongoose";
import {
+ INTEGRATION_AZURE_KEY_VAULT,
INTEGRATION_HEROKU,
INTEGRATION_VERCEL,
INTEGRATION_NETLIFY,
@@ -25,6 +26,7 @@ export interface IIntegration {
| "github"
| "render"
| "flyio"
+ | "azure-key-vault"
| "circleci";
integrationAuth: Types.ObjectId;
}
@@ -69,6 +71,7 @@ const integrationSchema = new Schema(
integration: {
type: String,
enum: [
+ INTEGRATION_AZURE_KEY_VAULT,
INTEGRATION_HEROKU,
INTEGRATION_VERCEL,
INTEGRATION_NETLIFY,
diff --git a/backend/src/models/integrationAuth.ts b/backend/src/models/integrationAuth.ts
index c219d3292..858a2e414 100644
--- a/backend/src/models/integrationAuth.ts
+++ b/backend/src/models/integrationAuth.ts
@@ -1,5 +1,6 @@
import { Schema, model, Types } from "mongoose";
import {
+ INTEGRATION_AZURE_KEY_VAULT,
INTEGRATION_HEROKU,
INTEGRATION_VERCEL,
INTEGRATION_NETLIFY,
@@ -16,7 +17,8 @@ export interface IIntegrationAuth {
| "netlify"
| "github"
| "render"
- | "flyio"
+ | "flyio"
+ | "azure-key-vault"
| "circleci";
teamId: string;
accountId: string;
@@ -39,6 +41,7 @@ const integrationAuthSchema = new Schema(
integration: {
type: String,
enum: [
+ INTEGRATION_AZURE_KEY_VAULT,
INTEGRATION_HEROKU,
INTEGRATION_VERCEL,
INTEGRATION_NETLIFY,
diff --git a/backend/src/models/loginSRPDetail.ts b/backend/src/models/loginSRPDetail.ts
new file mode 100644
index 000000000..6f3b579bd
--- /dev/null
+++ b/backend/src/models/loginSRPDetail.ts
@@ -0,0 +1,29 @@
+import mongoose, { Schema, model, Types } from 'mongoose';
+
+export interface ILoginSRPDetail {
+ _id: Types.ObjectId;
+ clientPublicKey: string;
+ email: string;
+ serverBInt: mongoose.Schema.Types.Buffer;
+ expireAt: Date;
+}
+
+const loginSRPDetailSchema = new Schema(
+ {
+ clientPublicKey: {
+ type: String,
+ required: true
+ },
+ email: {
+ type: String,
+ required: true,
+ unique: true
+ },
+ serverBInt: { type: mongoose.Schema.Types.Buffer },
+ expireAt: { type: Date }
+ }
+);
+
+const LoginSRPDetail = model('LoginSRPDetail', loginSRPDetailSchema);
+
+export default LoginSRPDetail;
diff --git a/backend/src/models/secret.ts b/backend/src/models/secret.ts
index 6887c8b0f..4ac6c768d 100644
--- a/backend/src/models/secret.ts
+++ b/backend/src/models/secret.ts
@@ -23,6 +23,7 @@ export interface ISecret {
secretCommentIV?: string;
secretCommentTag?: string;
secretCommentHash?: string;
+ tags?: string[];
}
const secretSchema = new Schema(
@@ -47,6 +48,11 @@ const secretSchema = new Schema(
type: Schema.Types.ObjectId,
ref: 'User'
},
+ tags: {
+ ref: 'Tag',
+ type: [Schema.Types.ObjectId],
+ default: []
+ },
environment: {
type: String,
required: true
diff --git a/backend/src/models/tag.ts b/backend/src/models/tag.ts
new file mode 100644
index 000000000..6b02c8b1b
--- /dev/null
+++ b/backend/src/models/tag.ts
@@ -0,0 +1,49 @@
+import { Schema, model, Types } from 'mongoose';
+
+export interface ITag {
+ _id: Types.ObjectId;
+ name: string;
+ slug: string;
+ user: Types.ObjectId;
+ workspace: Types.ObjectId;
+}
+
+const tagSchema = new Schema(
+ {
+ name: {
+ type: String,
+ required: true,
+ trim: true,
+ },
+ slug: {
+ type: String,
+ required: true,
+ trim: true,
+ lowercase: true,
+ validate: [
+ function (value: any) {
+ return value.indexOf(' ') === -1;
+ },
+ 'slug cannot contain spaces'
+ ]
+ },
+ user: {
+ type: Schema.Types.ObjectId,
+ ref: 'User'
+ },
+ workspace: {
+ type: Schema.Types.ObjectId,
+ ref: 'Workspace'
+ },
+ },
+ {
+ timestamps: true
+ }
+);
+
+tagSchema.index({ slug: 1, workspace: 1 }, { unique: true })
+tagSchema.index({ workspace: 1 })
+
+const Tag = model('Tag', tagSchema);
+
+export default Tag;
diff --git a/backend/src/models/user.ts b/backend/src/models/user.ts
index 7ea988c9d..ba9a59bc5 100644
--- a/backend/src/models/user.ts
+++ b/backend/src/models/user.ts
@@ -12,6 +12,7 @@ export interface IUser {
salt?: string;
verifier?: string;
refreshVersion?: number;
+ seenIps: [string];
}
const userSchema = new Schema(
@@ -54,7 +55,8 @@ const userSchema = new Schema(
type: Number,
default: 0,
select: false
- }
+ },
+ seenIps: [String]
},
{
timestamps: true
diff --git a/backend/src/models/workspace.ts b/backend/src/models/workspace.ts
index a70b64340..d68ae9416 100644
--- a/backend/src/models/workspace.ts
+++ b/backend/src/models/workspace.ts
@@ -8,6 +8,7 @@ export interface IWorkspace {
name: string;
slug: string;
}>;
+ autoCapitalization: boolean;
}
const workspaceSchema = new Schema({
@@ -15,6 +16,10 @@ const workspaceSchema = new Schema({
type: String,
required: true
},
+ autoCapitalization: {
+ type: Boolean,
+ default: true,
+ },
organization: {
type: Schema.Types.ObjectId,
ref: 'Organization',
diff --git a/backend/src/routes/v1/integration.ts b/backend/src/routes/v1/integration.ts
index d587bd2e5..90cea3a38 100644
--- a/backend/src/routes/v1/integration.ts
+++ b/backend/src/routes/v1/integration.ts
@@ -10,7 +10,7 @@ import { ADMIN, MEMBER } from '../../variables';
import { body, param } from 'express-validator';
import { integrationController } from '../../controllers/v1';
-router.post( // new: add new integration
+router.post( // new: add new integration for integration auth
'/',
requireAuth({
acceptedAuthModes: ['jwt', 'apiKey']
@@ -19,7 +19,13 @@ router.post( // new: add new integration
acceptedRoles: [ADMIN, MEMBER],
location: 'body'
}),
- body('integrationAuthId').exists().trim(),
+ body('integrationAuthId').exists().isString().trim(),
+ body('app').isString().trim(),
+ body('isActive').exists().isBoolean(),
+ body('appId').trim(),
+ body('sourceEnvironment').trim(),
+ body('targetEnvironment').trim(),
+ body('owner').trim(),
validateRequest,
integrationController.createIntegration
);
diff --git a/backend/src/routes/v1/integrationAuth.ts b/backend/src/routes/v1/integrationAuth.ts
index 3f88aa7e5..1918140c2 100644
--- a/backend/src/routes/v1/integrationAuth.ts
+++ b/backend/src/routes/v1/integrationAuth.ts
@@ -18,6 +18,19 @@ router.get(
integrationAuthController.getIntegrationOptions
);
+router.get(
+ '/:integrationAuthId',
+ requireAuth({
+ acceptedAuthModes: ['jwt']
+ }),
+ requireIntegrationAuthorizationAuth({
+ acceptedRoles: [ADMIN, MEMBER]
+ }),
+ param('integrationAuthId'),
+ validateRequest,
+ integrationAuthController.getIntegrationAuth
+);
+
router.post(
'/oauth-token',
requireAuth({
diff --git a/backend/src/routes/v2/index.ts b/backend/src/routes/v2/index.ts
index 6f698e316..dfc9ee617 100644
--- a/backend/src/routes/v2/index.ts
+++ b/backend/src/routes/v2/index.ts
@@ -6,6 +6,7 @@ import secrets from './secrets';
import serviceTokenData from './serviceTokenData';
import apiKeyData from './apiKeyData';
import environment from "./environment"
+import tags from "./tags"
export {
users,
@@ -15,5 +16,6 @@ export {
secrets,
serviceTokenData,
apiKeyData,
- environment
+ environment,
+ tags
}
\ No newline at end of file
diff --git a/backend/src/routes/v2/tags.ts b/backend/src/routes/v2/tags.ts
new file mode 100644
index 000000000..d78e1e0f1
--- /dev/null
+++ b/backend/src/routes/v2/tags.ts
@@ -0,0 +1,50 @@
+import express, { Response, Request } from 'express';
+const router = express.Router();
+import { body, param } from 'express-validator';
+import { tagController } from '../../controllers/v2';
+import {
+ requireAuth,
+ requireWorkspaceAuth,
+ validateRequest,
+} from '../../middleware';
+import { ADMIN, MEMBER } from '../../variables';
+
+router.get(
+ '/:workspaceId/tags',
+ requireAuth({
+ acceptedAuthModes: ['jwt'],
+ }),
+ requireWorkspaceAuth({
+ acceptedRoles: [MEMBER, ADMIN],
+ }),
+ param('workspaceId').exists().trim(),
+ validateRequest,
+ tagController.getWorkspaceTags
+);
+
+router.delete(
+ '/tags/:tagId',
+ requireAuth({
+ acceptedAuthModes: ['jwt'],
+ }),
+ param('tagId').exists().trim(),
+ validateRequest,
+ tagController.deleteWorkspaceTag
+);
+
+router.post(
+ '/:workspaceId/tags',
+ requireAuth({
+ acceptedAuthModes: ['jwt'],
+ }),
+ requireWorkspaceAuth({
+ acceptedRoles: [MEMBER, ADMIN],
+ }),
+ param('workspaceId').exists().trim(),
+ body('name').exists().trim(),
+ body('slug').exists().trim(),
+ validateRequest,
+ tagController.createWorkspaceTag
+);
+
+export default router;
diff --git a/backend/src/routes/v2/workspace.ts b/backend/src/routes/v2/workspace.ts
index 55299e575..9a753badd 100644
--- a/backend/src/routes/v2/workspace.ts
+++ b/backend/src/routes/v2/workspace.ts
@@ -118,4 +118,19 @@ router.delete( // TODO - rewire dashboard to this route
workspaceController.deleteWorkspaceMembership
);
+
+router.patch(
+ '/:workspaceId/auto-capitalization',
+ requireAuth({
+ acceptedAuthModes: ['jwt']
+ }),
+ requireWorkspaceAuth({
+ acceptedRoles: [ADMIN, MEMBER]
+ }),
+ param('workspaceId').exists().trim(),
+ body('autoCapitalization').exists().trim().notEmpty(),
+ validateRequest,
+ workspaceController.toggleAutoCapitalization
+);
+
export default router;
diff --git a/backend/src/services/IntegrationService.ts b/backend/src/services/IntegrationService.ts
index 4ee991cdd..5b0195427 100644
--- a/backend/src/services/IntegrationService.ts
+++ b/backend/src/services/IntegrationService.ts
@@ -1,7 +1,3 @@
-import * as Sentry from '@sentry/node';
-import {
- Integration
-} from '../models';
import {
handleOAuthExchangeHelper,
syncIntegrationsHelper,
@@ -10,7 +6,6 @@ import {
setIntegrationAuthRefreshHelper,
setIntegrationAuthAccessHelper,
} from '../helpers/integration';
-import { exchangeCode } from '../integrations';
// should sync stuff be here too? Probably.
// TODO: move bot functions to IntegrationService.
@@ -26,11 +21,12 @@ class IntegrationService {
* - Store integration access and refresh tokens returned from the OAuth2 code-token exchange
* - Add placeholder inactive integration
* - Create bot sequence for integration
- * @param {Object} obj
- * @param {String} obj.workspaceId - id of workspace
- * @param {String} obj.environment - workspace environment
- * @param {String} obj.integration - name of integration
- * @param {String} obj.code - code
+ * @param {Object} obj1
+ * @param {String} obj1.workspaceId - id of workspace
+ * @param {String} obj1.environment - workspace environment
+ * @param {String} obj1.integration - name of integration
+ * @param {String} obj1.code - code
+ * @returns {IntegrationAuth} integrationAuth - integration authorization after OAuth2 code-token exchange
*/
static async handleOAuthExchange({
workspaceId,
@@ -43,7 +39,7 @@ class IntegrationService {
code: string;
environment: string;
}) {
- await handleOAuthExchangeHelper({
+ return await handleOAuthExchangeHelper({
workspaceId,
integration,
code,
diff --git a/backend/src/variables/action.ts b/backend/src/variables/action.ts
index 512eb8e8d..682b357a7 100644
--- a/backend/src/variables/action.ts
+++ b/backend/src/variables/action.ts
@@ -1,9 +1,13 @@
+const ACTION_LOGIN = 'login';
+const ACTION_LOGOUT = 'logout';
const ACTION_ADD_SECRETS = 'addSecrets';
const ACTION_DELETE_SECRETS = 'deleteSecrets';
const ACTION_UPDATE_SECRETS = 'updateSecrets';
const ACTION_READ_SECRETS = 'readSecrets';
export {
+ ACTION_LOGIN,
+ ACTION_LOGOUT,
ACTION_ADD_SECRETS,
ACTION_DELETE_SECRETS,
ACTION_UPDATE_SECRETS,
diff --git a/backend/src/variables/index.ts b/backend/src/variables/index.ts
index 4196a374f..33307af4b 100644
--- a/backend/src/variables/index.ts
+++ b/backend/src/variables/index.ts
@@ -6,6 +6,7 @@ import {
ENV_SET,
} from "./environment";
import {
+ INTEGRATION_AZURE_KEY_VAULT,
INTEGRATION_HEROKU,
INTEGRATION_VERCEL,
INTEGRATION_NETLIFY,
@@ -15,6 +16,7 @@ import {
INTEGRATION_CIRCLECI,
INTEGRATION_SET,
INTEGRATION_OAUTH2,
+ INTEGRATION_AZURE_TOKEN_URL,
INTEGRATION_HEROKU_TOKEN_URL,
INTEGRATION_VERCEL_TOKEN_URL,
INTEGRATION_NETLIFY_TOKEN_URL,
@@ -31,6 +33,8 @@ import { OWNER, ADMIN, MEMBER, INVITED, ACCEPTED } from "./organization";
import { SECRET_SHARED, SECRET_PERSONAL } from "./secret";
import { EVENT_PUSH_SECRETS, EVENT_PULL_SECRETS } from "./event";
import {
+ ACTION_LOGIN,
+ ACTION_LOGOUT,
ACTION_ADD_SECRETS,
ACTION_UPDATE_SECRETS,
ACTION_DELETE_SECRETS,
@@ -52,6 +56,7 @@ export {
ENV_STAGING,
ENV_PROD,
ENV_SET,
+ INTEGRATION_AZURE_KEY_VAULT,
INTEGRATION_HEROKU,
INTEGRATION_VERCEL,
INTEGRATION_NETLIFY,
@@ -61,6 +66,7 @@ export {
INTEGRATION_CIRCLECI,
INTEGRATION_SET,
INTEGRATION_OAUTH2,
+ INTEGRATION_AZURE_TOKEN_URL,
INTEGRATION_HEROKU_TOKEN_URL,
INTEGRATION_VERCEL_TOKEN_URL,
INTEGRATION_NETLIFY_TOKEN_URL,
@@ -73,6 +79,8 @@ export {
INTEGRATION_CIRCLECI_API_URL,
EVENT_PUSH_SECRETS,
EVENT_PULL_SECRETS,
+ ACTION_LOGIN,
+ ACTION_LOGOUT,
ACTION_ADD_SECRETS,
ACTION_UPDATE_SECRETS,
ACTION_DELETE_SECRETS,
diff --git a/backend/src/variables/integration.ts b/backend/src/variables/integration.ts
index 883e8a730..77b3a80a1 100644
--- a/backend/src/variables/integration.ts
+++ b/backend/src/variables/integration.ts
@@ -1,3 +1,7 @@
+import {
+ CLIENT_ID_AZURE,
+ TENANT_ID_AZURE
+} from '../config';
import {
CLIENT_ID_HEROKU,
CLIENT_ID_NETLIFY,
@@ -6,6 +10,7 @@ import {
} from "../config";
// integrations
+const INTEGRATION_AZURE_KEY_VAULT = 'azure-key-vault';
const INTEGRATION_HEROKU = "heroku";
const INTEGRATION_VERCEL = "vercel";
const INTEGRATION_NETLIFY = "netlify";
@@ -14,6 +19,7 @@ const INTEGRATION_RENDER = "render";
const INTEGRATION_FLYIO = "flyio";
const INTEGRATION_CIRCLECI = "circleci";
const INTEGRATION_SET = new Set([
+ INTEGRATION_AZURE_KEY_VAULT,
INTEGRATION_HEROKU,
INTEGRATION_VERCEL,
INTEGRATION_NETLIFY,
@@ -27,7 +33,8 @@ const INTEGRATION_SET = new Set([
const INTEGRATION_OAUTH2 = "oauth2";
// integration oauth endpoints
-const INTEGRATION_HEROKU_TOKEN_URL = "https://id.heroku.com/oauth/token";
+const INTEGRATION_AZURE_TOKEN_URL = `https://login.microsoftonline.com/${TENANT_ID_AZURE}/oauth2/v2.0/token`;
+const INTEGRATION_HEROKU_TOKEN_URL = 'https://id.heroku.com/oauth/token';
const INTEGRATION_VERCEL_TOKEN_URL =
"https://api.vercel.com/v2/oauth/access_token";
const INTEGRATION_NETLIFY_TOKEN_URL = "https://api.netlify.com/oauth/token";
@@ -43,6 +50,16 @@ const INTEGRATION_FLYIO_API_URL = "https://api.fly.io/graphql";
const INTEGRATION_CIRCLECI_API_URL = "https://circleci.com/api";
const INTEGRATION_OPTIONS = [
+ {
+ name: 'Azure Key Vault',
+ slug: 'azure-key-vault',
+ image: 'Microsoft Azure.png',
+ isAvailable: false,
+ type: 'oauth',
+ clientId: CLIENT_ID_AZURE,
+ tenantId: TENANT_ID_AZURE,
+ docsLink: ''
+ },
{
name: "Heroku",
slug: "heroku",
@@ -146,6 +163,7 @@ const INTEGRATION_OPTIONS = [
];
export {
+ INTEGRATION_AZURE_KEY_VAULT,
INTEGRATION_HEROKU,
INTEGRATION_VERCEL,
INTEGRATION_NETLIFY,
@@ -155,6 +173,7 @@ export {
INTEGRATION_CIRCLECI,
INTEGRATION_SET,
INTEGRATION_OAUTH2,
+ INTEGRATION_AZURE_TOKEN_URL,
INTEGRATION_HEROKU_TOKEN_URL,
INTEGRATION_VERCEL_TOKEN_URL,
INTEGRATION_NETLIFY_TOKEN_URL,
diff --git a/cli/packages/api/model.go b/cli/packages/api/model.go
index 698d41408..8f929bf69 100644
--- a/cli/packages/api/model.go
+++ b/cli/packages/api/model.go
@@ -201,21 +201,30 @@ type GetEncryptedSecretsV2Request struct {
type GetEncryptedSecretsV2Response struct {
Secrets []struct {
- ID string `json:"_id"`
- Version int `json:"version"`
- Workspace string `json:"workspace"`
- Type string `json:"type"`
- Environment string `json:"environment"`
- SecretKeyCiphertext string `json:"secretKeyCiphertext"`
- SecretKeyIV string `json:"secretKeyIV"`
- SecretKeyTag string `json:"secretKeyTag"`
- SecretValueCiphertext string `json:"secretValueCiphertext"`
- SecretValueIV string `json:"secretValueIV"`
- SecretValueTag string `json:"secretValueTag"`
- V int `json:"__v"`
- CreatedAt time.Time `json:"createdAt"`
- UpdatedAt time.Time `json:"updatedAt"`
- User string `json:"user,omitempty"`
+ ID string `json:"_id"`
+ Version int `json:"version"`
+ Workspace string `json:"workspace"`
+ Type string `json:"type"`
+ Environment string `json:"environment"`
+ SecretKeyCiphertext string `json:"secretKeyCiphertext"`
+ SecretKeyIV string `json:"secretKeyIV"`
+ SecretKeyTag string `json:"secretKeyTag"`
+ SecretValueCiphertext string `json:"secretValueCiphertext"`
+ SecretValueIV string `json:"secretValueIV"`
+ SecretValueTag string `json:"secretValueTag"`
+ SecretCommentCiphertext string `json:"secretCommentCiphertext"`
+ SecretCommentIV string `json:"secretCommentIV"`
+ SecretCommentTag string `json:"secretCommentTag"`
+ V int `json:"__v"`
+ CreatedAt time.Time `json:"createdAt"`
+ UpdatedAt time.Time `json:"updatedAt"`
+ User string `json:"user,omitempty"`
+ Tags []struct {
+ ID string `json:"_id"`
+ Name string `json:"name"`
+ Slug string `json:"slug"`
+ Workspace string `json:"workspace"`
+ } `json:"tags"`
} `json:"secrets"`
}
diff --git a/cli/packages/cmd/export.go b/cli/packages/cmd/export.go
index 41e170f70..c2963f6bd 100644
--- a/cli/packages/cmd/export.go
+++ b/cli/packages/cmd/export.go
@@ -56,7 +56,12 @@ var exportCmd = &cobra.Command{
util.HandleError(err, "Unable to parse flag")
}
- secrets, err := util.GetAllEnvironmentVariables(envName)
+ infisicalToken, err := cmd.Flags().GetString("token")
+ if err != nil {
+ util.HandleError(err, "Unable to parse flag")
+ }
+
+ secrets, err := util.GetAllEnvironmentVariables(models.GetAllSecretsParameters{Environment: envName, InfisicalToken: infisicalToken})
if err != nil {
util.HandleError(err, "Unable to fetch secrets")
}
@@ -91,6 +96,7 @@ func init() {
exportCmd.Flags().Bool("expand", true, "Parse shell parameter expansions in your secrets")
exportCmd.Flags().StringP("format", "f", "dotenv", "Set the format of the output file (dotenv, json, csv)")
exportCmd.Flags().Bool("secret-overriding", true, "Prioritizes personal secrets, if any, with the same name over shared secrets")
+ exportCmd.Flags().String("token", "", "Fetch secrets using the Infisical Token")
}
// Format according to the format flag
diff --git a/cli/packages/cmd/login.go b/cli/packages/cmd/login.go
index 6a1dc2313..c37bdda08 100644
--- a/cli/packages/cmd/login.go
+++ b/cli/packages/cmd/login.go
@@ -101,6 +101,9 @@ var loginCmd = &cobra.Command{
util.HandleError(err, "Unable to write write to Infisical Config file. Please try again")
}
+ // clear backed up secrets from prev account
+ util.DeleteBackupSecrets()
+
color.Green("Nice! You are logged in as: %v", email)
},
diff --git a/cli/packages/cmd/run.go b/cli/packages/cmd/run.go
index d31981b02..bd1be4300 100644
--- a/cli/packages/cmd/run.go
+++ b/cli/packages/cmd/run.go
@@ -12,6 +12,7 @@ import (
"strings"
"syscall"
+ "github.com/Infisical/infisical-merge/packages/models"
"github.com/Infisical/infisical-merge/packages/util"
"github.com/fatih/color"
log "github.com/sirupsen/logrus"
@@ -58,6 +59,11 @@ var runCmd = &cobra.Command{
util.HandleError(err, "Unable to parse flag")
}
+ infisicalToken, err := cmd.Flags().GetString("token")
+ if err != nil {
+ util.HandleError(err, "Unable to parse flag")
+ }
+
// if !util.IsSecretEnvironmentValid(envName) {
// util.PrintMessageAndExit("Invalid environment name passed. Environment names can only be prod, dev, test or staging")
// }
@@ -72,7 +78,8 @@ var runCmd = &cobra.Command{
util.HandleError(err, "Unable to parse flag")
}
- secrets, err := util.GetAllEnvironmentVariables(envName)
+ secrets, err := util.GetAllEnvironmentVariables(models.GetAllSecretsParameters{Environment: envName, InfisicalToken: infisicalToken})
+
if err != nil {
util.HandleError(err, "Could not fetch secrets", "If you are using a service token to fetch secrets, please ensure it is valid")
}
@@ -140,6 +147,7 @@ var runCmd = &cobra.Command{
func init() {
rootCmd.AddCommand(runCmd)
+ runCmd.Flags().String("token", "", "Fetch secrets using the Infisical Token")
runCmd.Flags().StringP("env", "e", "dev", "Set the environment (dev, prod, etc.) from which your secrets should be pulled from")
runCmd.Flags().Bool("expand", true, "Parse shell parameter expansions in your secrets")
runCmd.Flags().Bool("secret-overriding", true, "Prioritizes personal secrets, if any, with the same name over shared secrets")
diff --git a/cli/packages/cmd/secrets.go b/cli/packages/cmd/secrets.go
index 060a69a45..a00cacaea 100644
--- a/cli/packages/cmd/secrets.go
+++ b/cli/packages/cmd/secrets.go
@@ -6,6 +6,8 @@ package cmd
import (
"encoding/base64"
"fmt"
+ "regexp"
+ "sort"
"strings"
"unicode"
@@ -22,7 +24,7 @@ import (
)
var secretsCmd = &cobra.Command{
- Example: `infisical secrets"`,
+ Example: `infisical secrets`,
Short: "Used to create, read update and delete secrets",
Use: "secrets",
DisableFlagsInUseLine: true,
@@ -34,12 +36,17 @@ var secretsCmd = &cobra.Command{
util.HandleError(err)
}
+ infisicalToken, err := cmd.Flags().GetString("token")
+ if err != nil {
+ util.HandleError(err, "Unable to parse flag")
+ }
+
shouldExpandSecrets, err := cmd.Flags().GetBool("expand")
if err != nil {
util.HandleError(err)
}
- secrets, err := util.GetAllEnvironmentVariables(environmentName)
+ secrets, err := util.GetAllEnvironmentVariables(models.GetAllSecretsParameters{Environment: environmentName, InfisicalToken: infisicalToken})
if err != nil {
util.HandleError(err)
}
@@ -62,6 +69,16 @@ var secretsGetCmd = &cobra.Command{
Run: getSecretsByNames,
}
+var secretsGenerateExampleEnvCmd = &cobra.Command{
+ Example: `secrets generate-example-env > .example-env`,
+ Short: "Used to generate a example .env file",
+ Use: "generate-example-env",
+ DisableFlagsInUseLine: true,
+ Args: cobra.NoArgs,
+ PreRun: toggleDebug,
+ Run: generateExampleEnv,
+}
+
var secretsSetCmd = &cobra.Command{
Example: `secrets set ..."`,
Short: "Used set secrets",
@@ -111,7 +128,7 @@ var secretsSetCmd = &cobra.Command{
plainTextEncryptionKey := crypto.DecryptAsymmetric(encryptedWorkspaceKey, encryptedWorkspaceKeyNonce, encryptedWorkspaceKeySenderPublicKey, currentUsersPrivateKey)
// pull current secrets
- secrets, err := util.GetAllEnvironmentVariables(environmentName)
+ secrets, err := util.GetAllEnvironmentVariables(models.GetAllSecretsParameters{Environment: environmentName})
if err != nil {
util.HandleError(err, "unable to retrieve secrets")
}
@@ -267,7 +284,7 @@ var secretsDeleteCmd = &cobra.Command{
util.HandleError(err, "Unable to get local project details")
}
- secrets, err := util.GetAllEnvironmentVariables(environmentName)
+ secrets, err := util.GetAllEnvironmentVariables(models.GetAllSecretsParameters{Environment: environmentName})
if err != nil {
util.HandleError(err, "Unable to fetch secrets")
}
@@ -309,30 +326,6 @@ var secretsDeleteCmd = &cobra.Command{
},
}
-func init() {
- secretsCmd.AddCommand(secretsGetCmd)
- secretsGetCmd.PersistentPreRun = func(cmd *cobra.Command, args []string) {
- util.RequireLogin()
- util.RequireLocalWorkspaceFile()
- }
-
- secretsCmd.AddCommand(secretsSetCmd)
- secretsSetCmd.PersistentPreRun = func(cmd *cobra.Command, args []string) {
- util.RequireLogin()
- util.RequireLocalWorkspaceFile()
- }
-
- secretsCmd.AddCommand(secretsDeleteCmd)
- secretsDeleteCmd.PersistentPreRun = func(cmd *cobra.Command, args []string) {
- util.RequireLogin()
- util.RequireLocalWorkspaceFile()
- }
-
- secretsCmd.PersistentFlags().String("env", "dev", "Used to select the environment name on which actions should be taken on")
- secretsCmd.Flags().Bool("expand", true, "Parse shell parameter expansions in your secrets")
- rootCmd.AddCommand(secretsCmd)
-}
-
func getSecretsByNames(cmd *cobra.Command, args []string) {
environmentName, err := cmd.Flags().GetString("env")
if err != nil {
@@ -344,7 +337,12 @@ func getSecretsByNames(cmd *cobra.Command, args []string) {
util.HandleError(err, "Unable to parse flag")
}
- secrets, err := util.GetAllEnvironmentVariables(environmentName)
+ infisicalToken, err := cmd.Flags().GetString("token")
+ if err != nil {
+ util.HandleError(err, "Unable to parse flag")
+ }
+
+ secrets, err := util.GetAllEnvironmentVariables(models.GetAllSecretsParameters{Environment: environmentName, InfisicalToken: infisicalToken})
if err != nil {
util.HandleError(err, "To fetch all secrets")
}
@@ -371,6 +369,171 @@ func getSecretsByNames(cmd *cobra.Command, args []string) {
visualize.PrintAllSecretDetails(requestedSecrets)
}
+func generateExampleEnv(cmd *cobra.Command, args []string) {
+ environmentName, err := cmd.Flags().GetString("env")
+ if err != nil {
+ util.HandleError(err, "Unable to parse flag")
+ }
+
+ workspaceFileExists := util.WorkspaceConfigFileExistsInCurrentPath()
+ if !workspaceFileExists {
+ util.HandleError(err, "Unable to parse flag")
+ }
+
+ infisicalToken, err := cmd.Flags().GetString("token")
+ if err != nil {
+ util.HandleError(err, "Unable to parse flag")
+ }
+
+ secrets, err := util.GetAllEnvironmentVariables(models.GetAllSecretsParameters{Environment: environmentName, InfisicalToken: infisicalToken})
+ if err != nil {
+ util.HandleError(err, "To fetch all secrets")
+ }
+
+ tagsHashToSecretKey := make(map[string]int)
+
+ type TagsAndSecrets struct {
+ Secrets []models.SingleEnvironmentVariable
+ Tags []struct {
+ ID string `json:"_id"`
+ Name string `json:"name"`
+ Slug string `json:"slug"`
+ Workspace string `json:"workspace"`
+ }
+ }
+
+ // sort secrets by associated tags (most number of tags to least tags)
+ sort.Slice(secrets, func(i, j int) bool {
+ return len(secrets[i].Tags) > len(secrets[j].Tags)
+ })
+
+ for _, secret := range secrets {
+ listOfTagSlugs := []string{}
+
+ for _, tag := range secret.Tags {
+ listOfTagSlugs = append(listOfTagSlugs, tag.Slug)
+ }
+ sort.Strings(listOfTagSlugs)
+
+ tagsHash := util.GetHashFromStringList(listOfTagSlugs)
+
+ tagsHashToSecretKey[tagsHash] += 1
+ }
+
+ finalTagHashToSecretKey := make(map[string]TagsAndSecrets)
+
+ for _, secret := range secrets {
+ listOfTagSlugs := []string{}
+ for _, tag := range secret.Tags {
+ listOfTagSlugs = append(listOfTagSlugs, tag.Slug)
+ }
+
+ // sort the slug so we get the same hash each time
+ sort.Strings(listOfTagSlugs)
+
+ tagsHash := util.GetHashFromStringList(listOfTagSlugs)
+ occurrence, exists := tagsHashToSecretKey[tagsHash]
+ if exists && occurrence > 0 {
+
+ value, exists2 := finalTagHashToSecretKey[tagsHash]
+ allSecretsForTags := append(value.Secrets, secret)
+
+ // sort the the secrets by keys so that they can later be sorted by the first item in the secrets array
+ sort.Slice(allSecretsForTags, func(i, j int) bool {
+ return allSecretsForTags[i].Key < allSecretsForTags[j].Key
+ })
+
+ if exists2 {
+ finalTagHashToSecretKey[tagsHash] = TagsAndSecrets{
+ Tags: secret.Tags,
+ Secrets: allSecretsForTags,
+ }
+ } else {
+ finalTagHashToSecretKey[tagsHash] = TagsAndSecrets{
+ Tags: secret.Tags,
+ Secrets: []models.SingleEnvironmentVariable{secret},
+ }
+ }
+
+ tagsHashToSecretKey[tagsHash] -= 1
+ }
+ }
+
+ // sort the fianl result by secret key fo consistent print order
+ listOfsecretDetails := make([]TagsAndSecrets, 0, len(finalTagHashToSecretKey))
+ for _, secretDetails := range finalTagHashToSecretKey {
+ listOfsecretDetails = append(listOfsecretDetails, secretDetails)
+ }
+
+ // sort the order of the headings by the order of the secrets
+ sort.Slice(listOfsecretDetails, func(i, j int) bool {
+ return len(listOfsecretDetails[i].Tags) < len(listOfsecretDetails[j].Tags)
+ })
+
+ for _, secretDetails := range listOfsecretDetails {
+ listOfKeyValue := []string{}
+
+ for _, secret := range secretDetails.Secrets {
+ re := regexp.MustCompile(`(?s)(.*)DEFAULT:(.*)`)
+ match := re.FindStringSubmatch(secret.Comment)
+ defaultValue := ""
+ comment := secret.Comment
+
+ // Case: Only has default value
+ if len(match) == 2 {
+ defaultValue = strings.TrimSpace(match[1])
+ }
+
+ // Case: has a comment and a default value
+ if len(match) == 3 {
+ comment = match[1]
+ defaultValue = match[2]
+ }
+
+ row := ""
+ if comment != "" {
+ comment = addHash(comment)
+ row = fmt.Sprintf("%s \n%s=%s", strings.TrimSpace(comment), strings.TrimSpace(secret.Key), strings.TrimSpace(defaultValue))
+ } else {
+ row = fmt.Sprintf("%s=%s", strings.TrimSpace(secret.Key), strings.TrimSpace(defaultValue))
+ }
+
+ // each secret row to be added to the file
+ listOfKeyValue = append(listOfKeyValue, row)
+ }
+
+ listOfTagNames := []string{}
+ for _, tag := range secretDetails.Tags {
+ listOfTagNames = append(listOfTagNames, tag.Name)
+ }
+
+ heading := CenterString(strings.Join(listOfTagNames, " & "), 80)
+
+ if len(listOfTagNames) == 0 {
+ fmt.Printf("\n%s \n", strings.Join(listOfKeyValue, "\n \n"))
+ } else {
+ fmt.Printf("\n\n\n%s\n \n%s \n", heading, strings.Join(listOfKeyValue, "\n \n"))
+ }
+ }
+}
+
+func CenterString(s string, numStars int) string {
+ stars := strings.Repeat("*", numStars)
+ padding := (numStars - len(s)) / 2
+ cenetredTextWithStar := stars[:padding] + " " + strings.ToUpper(s) + " " + stars[padding:]
+
+ hashes := strings.Repeat("#", len(cenetredTextWithStar)+2)
+ return fmt.Sprintf("%s \n# %s \n%s", hashes, cenetredTextWithStar, hashes)
+}
+
+func addHash(input string) string {
+ lines := strings.Split(input, "\n")
+ for i, line := range lines {
+ lines[i] = "# " + line
+ }
+ return strings.Join(lines, "\n")
+}
+
func getSecretsByKeys(secrets []models.SingleEnvironmentVariable) map[string]models.SingleEnvironmentVariable {
secretMapByName := make(map[string]models.SingleEnvironmentVariable)
@@ -380,3 +543,29 @@ func getSecretsByKeys(secrets []models.SingleEnvironmentVariable) map[string]mod
return secretMapByName
}
+
+func init() {
+
+ secretsGenerateExampleEnvCmd.Flags().String("token", "", "Fetch secrets using the Infisical Token")
+ secretsCmd.AddCommand(secretsGenerateExampleEnvCmd)
+
+ secretsGetCmd.Flags().String("token", "", "Fetch secrets using the Infisical Token")
+ secretsCmd.AddCommand(secretsGetCmd)
+
+ secretsCmd.AddCommand(secretsSetCmd)
+ secretsSetCmd.PersistentPreRun = func(cmd *cobra.Command, args []string) {
+ util.RequireLogin()
+ util.RequireLocalWorkspaceFile()
+ }
+
+ secretsCmd.AddCommand(secretsDeleteCmd)
+ secretsDeleteCmd.PersistentPreRun = func(cmd *cobra.Command, args []string) {
+ util.RequireLogin()
+ util.RequireLocalWorkspaceFile()
+ }
+
+ secretsCmd.Flags().String("token", "", "Fetch secrets using the Infisical Token")
+ secretsCmd.PersistentFlags().String("env", "dev", "Used to select the environment name on which actions should be taken on")
+ secretsCmd.Flags().Bool("expand", true, "Parse shell parameter expansions in your secrets")
+ rootCmd.AddCommand(secretsCmd)
+}
diff --git a/cli/packages/crypto/crypto.go b/cli/packages/crypto/crypto.go
index f9589347a..2f507ed4c 100644
--- a/cli/packages/crypto/crypto.go
+++ b/cli/packages/crypto/crypto.go
@@ -12,6 +12,11 @@ import (
// will decrypt cipher text to plain text using iv and tag
func DecryptSymmetric(key []byte, cipherText []byte, tag []byte, iv []byte) ([]byte, error) {
+ // Case: empty string
+ if len(cipherText) == 0 && len(tag) == 0 && len(iv) == 0 {
+ return []byte{}, nil
+ }
+
block, err := aes.NewCipher(key)
if err != nil {
return nil, err
diff --git a/cli/packages/models/cli.go b/cli/packages/models/cli.go
index 7bc31cab2..3ffc1cc20 100644
--- a/cli/packages/models/cli.go
+++ b/cli/packages/models/cli.go
@@ -1,6 +1,8 @@
package models
-import "github.com/99designs/keyring"
+import (
+ "github.com/99designs/keyring"
+)
type UserCredentials struct {
Email string `json:"email"`
@@ -19,6 +21,13 @@ type SingleEnvironmentVariable struct {
Value string `json:"value"`
Type string `json:"type"`
ID string `json:"_id"`
+ Tags []struct {
+ ID string `json:"_id"`
+ Name string `json:"name"`
+ Slug string `json:"slug"`
+ Workspace string `json:"workspace"`
+ } `json:"tags"`
+ Comment string `json:"comment"`
}
type Workspace struct {
@@ -34,7 +43,12 @@ type WorkspaceConfigFile struct {
}
type SymmetricEncryptionResult struct {
- CipherText []byte
- Nonce []byte
- AuthTag []byte
+ CipherText []byte `json:"CipherText"`
+ Nonce []byte `json:"Nonce"`
+ AuthTag []byte `json:"AuthTag"`
+}
+
+type GetAllSecretsParameters struct {
+ Environment string
+ InfisicalToken string
}
diff --git a/cli/packages/util/check-for-update.go b/cli/packages/util/check-for-update.go
index 5fbf39a1e..a23b90ad1 100644
--- a/cli/packages/util/check-for-update.go
+++ b/cli/packages/util/check-for-update.go
@@ -2,10 +2,10 @@ package util
import (
"encoding/json"
+ "errors"
"fmt"
"io/ioutil"
"net/http"
- "errors"
)
func CheckForUpdate() {
@@ -42,5 +42,5 @@ func getLatestTag(repoOwner string, repoName string) (string, error) {
json.Unmarshal(body, &tags)
- return tags[0].Name, nil
+ return tags[0].Name[1:], nil
}
diff --git a/cli/packages/util/common.go b/cli/packages/util/common.go
index 2d420ac6f..13e76046d 100644
--- a/cli/packages/util/common.go
+++ b/cli/packages/util/common.go
@@ -2,6 +2,7 @@ package util
import (
"fmt"
+ "net/http"
"os"
)
@@ -19,3 +20,11 @@ func WriteToFile(fileName string, dataToWrite []byte, filePerm os.FileMode) erro
return nil
}
+
+func CheckIsConnectedToInternet() (ok bool) {
+ _, err := http.Get("http://clients3.google.com/generate_204")
+ if err != nil {
+ return false
+ }
+ return true
+}
diff --git a/cli/packages/util/helper.go b/cli/packages/util/helper.go
index a6e7e35fc..3fd8853c4 100644
--- a/cli/packages/util/helper.go
+++ b/cli/packages/util/helper.go
@@ -1,6 +1,7 @@
package util
import (
+ "crypto/sha256"
"encoding/base64"
"fmt"
"os"
@@ -98,3 +99,14 @@ func RequireLocalWorkspaceFile() {
PrintMessageAndExit("Your project id is missing in your local config file. Please add it or run again [infisical init]")
}
}
+
+func GetHashFromStringList(list []string) string {
+ hash := sha256.New()
+
+ for _, item := range list {
+ hash.Write([]byte(item))
+ }
+
+ sum := sha256.Sum256(hash.Sum(nil))
+ return fmt.Sprintf("%x", sum)
+}
diff --git a/cli/packages/util/log.go b/cli/packages/util/log.go
index a701987fc..b191f33c8 100644
--- a/cli/packages/util/log.go
+++ b/cli/packages/util/log.go
@@ -24,7 +24,7 @@ func PrintErrorAndExit(exitCode int, err error, messages ...string) {
}
func PrintWarning(message string) {
- color.Yellow("Warning: %v", message)
+ color.New(color.FgYellow).Fprintf(os.Stderr, "Warning: %v \n", message)
}
func PrintMessageAndExit(messages ...string) {
diff --git a/cli/packages/util/secrets.go b/cli/packages/util/secrets.go
index 1cf8a7ab5..7a4bc09db 100644
--- a/cli/packages/util/secrets.go
+++ b/cli/packages/util/secrets.go
@@ -2,6 +2,8 @@ package util
import (
"encoding/base64"
+ "encoding/json"
+ "errors"
"fmt"
"os"
"regexp"
@@ -97,13 +99,26 @@ func GetPlainTextSecretsViaJTW(JTWToken string, receiversPrivateKey string, work
return plainTextSecrets, nil
}
-func GetAllEnvironmentVariables(envName string) ([]models.SingleEnvironmentVariable, error) {
- infisicalToken := os.Getenv(INFISICAL_TOKEN_NAME)
+func GetAllEnvironmentVariables(params models.GetAllSecretsParameters) ([]models.SingleEnvironmentVariable, error) {
+ var infisicalToken string
+ if params.InfisicalToken == "" {
+ infisicalToken = os.Getenv(INFISICAL_TOKEN_NAME)
+ } else {
+ infisicalToken = params.InfisicalToken
+ }
+
+ isConnected := CheckIsConnectedToInternet()
+ var secretsToReturn []models.SingleEnvironmentVariable
+ var errorToReturn error
if infisicalToken == "" {
- RequireLocalWorkspaceFile()
- RequireLogin()
- log.Debug("Trying to fetch secrets using logged in details")
+ if isConnected {
+ log.Debug("GetAllEnvironmentVariables: Connected to internet, checking logged in creds")
+ RequireLocalWorkspaceFile()
+ RequireLogin()
+ }
+
+ log.Debug("GetAllEnvironmentVariables: Trying to fetch secrets using logged in details")
loggedInUserDetails, err := GetCurrentLoggedInUserDetails()
if err != nil {
@@ -115,13 +130,30 @@ func GetAllEnvironmentVariables(envName string) ([]models.SingleEnvironmentVaria
return nil, err
}
- secrets, err := GetPlainTextSecretsViaJTW(loggedInUserDetails.UserCredentials.JTWToken, loggedInUserDetails.UserCredentials.PrivateKey, workspaceFile.WorkspaceId, envName)
- return secrets, err
+ secretsToReturn, errorToReturn = GetPlainTextSecretsViaJTW(loggedInUserDetails.UserCredentials.JTWToken, loggedInUserDetails.UserCredentials.PrivateKey, workspaceFile.WorkspaceId, params.Environment)
+ log.Debugf("GetAllEnvironmentVariables: Trying to fetch secrets JTW token [err=%s]", errorToReturn)
+
+ backupSecretsEncryptionKey := []byte(loggedInUserDetails.UserCredentials.PrivateKey)[0:32]
+ if errorToReturn == nil {
+ WriteBackupSecrets(workspaceFile.WorkspaceId, params.Environment, backupSecretsEncryptionKey, secretsToReturn)
+ }
+
+ // only attempt to serve cached secrets if no internet connection and if at least one secret cached
+ if !isConnected {
+ backedSecrets, err := ReadBackupSecrets(workspaceFile.WorkspaceId, params.Environment, backupSecretsEncryptionKey)
+ if len(backedSecrets) > 0 {
+ PrintWarning("Unable to fetch latest secret(s) due to connection error, serving secrets from last successful fetch. For more info, run with --debug")
+ secretsToReturn = backedSecrets
+ errorToReturn = err
+ }
+ }
} else {
log.Debug("Trying to fetch secrets using service token")
- return GetPlainTextSecretsViaServiceToken(infisicalToken)
+ secretsToReturn, errorToReturn = GetPlainTextSecretsViaServiceToken(infisicalToken)
}
+
+ return secretsToReturn, errorToReturn
}
func getExpandedEnvVariable(secrets []models.SingleEnvironmentVariable, variableWeAreLookingFor string, hashMapOfCompleteVariables map[string]string, hashMapOfSelfRefs map[string]string) string {
@@ -283,11 +315,34 @@ func GetPlainTextSecrets(key []byte, encryptedSecrets api.GetEncryptedSecretsV2R
return nil, fmt.Errorf("unable to symmetrically decrypt secret value")
}
+ // Decrypt comment
+ comment_iv, err := base64.StdEncoding.DecodeString(secret.SecretCommentIV)
+ if err != nil {
+ return nil, fmt.Errorf("unable to decode secret IV for secret value")
+ }
+
+ comment_tag, err := base64.StdEncoding.DecodeString(secret.SecretCommentTag)
+ if err != nil {
+ return nil, fmt.Errorf("unable to decode secret authentication tag for secret value")
+ }
+
+ comment_ciphertext, _ := base64.StdEncoding.DecodeString(secret.SecretCommentCiphertext)
+ if err != nil {
+ return nil, fmt.Errorf("unable to decode secret cipher text for secret key")
+ }
+
+ plainTextComment, err := crypto.DecryptSymmetric(key, comment_ciphertext, comment_tag, comment_iv)
+ if err != nil {
+ return nil, fmt.Errorf("unable to symmetrically decrypt secret comment")
+ }
+
plainTextSecret := models.SingleEnvironmentVariable{
- Key: string(plainTextKey),
- Value: string(plainTextValue),
- Type: string(secret.Type),
- ID: secret.ID,
+ Key: string(plainTextKey),
+ Value: string(plainTextValue),
+ Type: string(secret.Type),
+ ID: secret.ID,
+ Tags: secret.Tags,
+ Comment: string(plainTextComment),
}
plainTextSecrets = append(plainTextSecrets, plainTextSecret)
@@ -295,3 +350,100 @@ func GetPlainTextSecrets(key []byte, encryptedSecrets api.GetEncryptedSecretsV2R
return plainTextSecrets, nil
}
+
+func WriteBackupSecrets(workspace string, environment string, encryptionKey []byte, secrets []models.SingleEnvironmentVariable) error {
+ fileName := fmt.Sprintf("secrets_%s_%s", workspace, environment)
+ secrets_backup_folder_name := "secrets-backup"
+
+ _, fullConfigFileDirPath, err := GetFullConfigFilePath()
+ if err != nil {
+ return fmt.Errorf("WriteBackupSecrets: unable to get full config folder path [err=%s]", err)
+ }
+
+ // create secrets backup directory
+ fullPathToSecretsBackupFolder := fmt.Sprintf("%s/%s", fullConfigFileDirPath, secrets_backup_folder_name)
+ if _, err := os.Stat(fullPathToSecretsBackupFolder); errors.Is(err, os.ErrNotExist) {
+ err := os.Mkdir(fullPathToSecretsBackupFolder, os.ModePerm)
+ if err != nil {
+ return err
+ }
+ }
+
+ var encryptedSecrets []models.SymmetricEncryptionResult
+ for _, secret := range secrets {
+ marshaledSecrets, _ := json.Marshal(secret)
+ result, err := crypto.EncryptSymmetric(marshaledSecrets, encryptionKey)
+ if err != nil {
+ return err
+ }
+
+ encryptedSecrets = append(encryptedSecrets, result)
+ }
+
+ listOfSecretsMarshalled, _ := json.Marshal(encryptedSecrets)
+ err = os.WriteFile(fmt.Sprintf("%s/%s", fullPathToSecretsBackupFolder, fileName), listOfSecretsMarshalled, os.ModePerm)
+ if err != nil {
+ return fmt.Errorf("WriteBackupSecrets: Unable to write backup secrets to file [err=%s]", err)
+ }
+
+ return nil
+}
+
+func ReadBackupSecrets(workspace string, environment string, encryptionKey []byte) ([]models.SingleEnvironmentVariable, error) {
+ fileName := fmt.Sprintf("secrets_%s_%s", workspace, environment)
+ secrets_backup_folder_name := "secrets-backup"
+
+ _, fullConfigFileDirPath, err := GetFullConfigFilePath()
+ if err != nil {
+ return nil, fmt.Errorf("ReadBackupSecrets: unable to write config file because an error occurred when getting config file path [err=%s]", err)
+ }
+
+ fullPathToSecretsBackupFolder := fmt.Sprintf("%s/%s", fullConfigFileDirPath, secrets_backup_folder_name)
+ if _, err := os.Stat(fullPathToSecretsBackupFolder); errors.Is(err, os.ErrNotExist) {
+ return nil, nil
+ }
+
+ encryptedBackupSecretsFilePath := fmt.Sprintf("%s/%s", fullPathToSecretsBackupFolder, fileName)
+
+ encryptedBackupSecretsAsBytes, err := os.ReadFile(encryptedBackupSecretsFilePath)
+ if err != nil {
+ return nil, err
+ }
+
+ var listOfEncryptedBackupSecrets []models.SymmetricEncryptionResult
+
+ _ = json.Unmarshal(encryptedBackupSecretsAsBytes, &listOfEncryptedBackupSecrets)
+
+ var plainTextSecrets []models.SingleEnvironmentVariable
+ for _, encryptedSecret := range listOfEncryptedBackupSecrets {
+ result, err := crypto.DecryptSymmetric(encryptionKey, encryptedSecret.CipherText, encryptedSecret.AuthTag, encryptedSecret.Nonce)
+ if err != nil {
+ return nil, err
+ }
+
+ var plainTextSecret models.SingleEnvironmentVariable
+
+ err = json.Unmarshal(result, &plainTextSecret)
+ if err != nil {
+ return nil, err
+ }
+
+ plainTextSecrets = append(plainTextSecrets, plainTextSecret)
+ }
+
+ return plainTextSecrets, nil
+
+}
+
+func DeleteBackupSecrets() error {
+ secrets_backup_folder_name := "secrets-backup"
+
+ _, fullConfigFileDirPath, err := GetFullConfigFilePath()
+ if err != nil {
+ return fmt.Errorf("ReadBackupSecrets: unable to write config file because an error occurred when getting config file path [err=%s]", err)
+ }
+
+ fullPathToSecretsBackupFolder := fmt.Sprintf("%s/%s", fullConfigFileDirPath, secrets_backup_folder_name)
+
+ return os.RemoveAll(fullPathToSecretsBackupFolder)
+}
diff --git a/cli/packages/util/vault.go b/cli/packages/util/vault.go
index 7e4ce1b31..0852e5f48 100644
--- a/cli/packages/util/vault.go
+++ b/cli/packages/util/vault.go
@@ -57,7 +57,7 @@ func fileKeyringPassphrasePrompt(prompt string) (string, error) {
if password, ok := os.LookupEnv("INFISICAL_VAULT_FILE_PASSPHRASE"); ok {
return password, nil
} else {
- fmt.Println("You may set the `INFISICAL_VAULT_FILE_PASSPHRASE` environment variable to avoid typing password")
+ fmt.Println("You may set the environment variable `INFISICAL_VAULT_FILE_PASSPHRASE` with your password to avoid typing it")
}
fmt.Fprintf(os.Stderr, "%s:", prompt)
@@ -65,6 +65,7 @@ func fileKeyringPassphrasePrompt(prompt string) (string, error) {
if err != nil {
return "", err
}
+
fmt.Println("")
return string(b), nil
}
diff --git a/docs/cli/commands/init.mdx b/docs/cli/commands/init.mdx
index d477541b8..ef33e1d04 100644
--- a/docs/cli/commands/init.mdx
+++ b/docs/cli/commands/init.mdx
@@ -9,6 +9,8 @@ infisical init
## Description
-Link a local project to the platform
+Link a local project to your Infisical project. Once connected, you can then access the secrets locally from the connected Infisical project.
-The command creates a `infisical.json` file containing your Project ID.
+
+This command creates a `infisical.json` file containing your Project ID.
+
diff --git a/docs/cli/commands/run.mdx b/docs/cli/commands/run.mdx
index a3f02cf44..fcc9be549 100644
--- a/docs/cli/commands/run.mdx
+++ b/docs/cli/commands/run.mdx
@@ -25,13 +25,58 @@ description: "The command that injects your secrets into local environment"
## Description
-Inject environment variables from the platform into an application process.
+Inject secrets from Infisical into your application process.
-## Options
-| Option | Description | Default value |
-| -------------- | ----------------------------------------------------------------------------------------------------------- | ------------- |
-| `--env` | Used to set the environment that secrets are pulled from. Accepted values: `dev`, `staging`, `test`, `prod` | `dev` |
-| `--expand` | Parse shell parameter expansions in your secrets (e.g., `${DOMAIN}`) | `true` |
-| `--command` | Pass secrets into chained commands (e.g., `"first-command && second-command; more-commands..."`) | None |
-| `--secret-overriding`| Prioritizes personal secrets with the same name over shared secrets | `true` |
+## Subcommands & flags
+
+
+ Use this command to inject secrets into your applications process
+
+ ```bash
+ $ infisical run --
+
+ # Example
+ $ infisical run -- npm run dev
+ ```
+
+ ### flags
+
+ Pass secrets into multiple commands at once
+
+ ```bash
+ # Example
+ infisical run --command="npm run build && npm run dev; more-commands..."
+ ```
+
+
+
+ If you are using a [service token](../../getting-started/dashboard/token) to authenticate, you can pass the token as a flag
+
+ ```bash
+ # Example
+ infisical run --token="st.63e03c4a97cb4a747186c71e.ed5b46a34c078a8f94e8228f4ab0ff97.4f7f38034811995997d72badf44b42ec" -- npm run start
+ ```
+
+ You may also expose the token to the CLI by setting the environment variable `INFISICAL_TOKEN` before executing the run command. This will have the same effect as setting the token with `--token` flag
+
+
+
+ Turn on or off the shell parameter expansion in your secrets. If you have used shell parameters in your secret(s), activating this feature will populate them before injecting them into your application process.
+
+ Default value: `true`
+
+
+
+ This is used to specify the environment from which secrets should be retrieved. The accepted values are the environment slugs defined for your project, such as `dev`, `staging`, `test`, and `prod`.
+
+ Default value: `dev`
+
+
+
+ Prioritizes personal secrets with the same name over shared secrets
+
+ Default value: `true`
+
+
+
diff --git a/docs/cli/commands/secrets.mdx b/docs/cli/commands/secrets.mdx
index 0556e1866..0e4190291 100644
--- a/docs/cli/commands/secrets.mdx
+++ b/docs/cli/commands/secrets.mdx
@@ -14,17 +14,8 @@ This command enables you to perform CRUD (create, read, update, delete) operatio
Use this command to print out all of the secrets in your project
- ```
+ ```bash
$ infisical secrets
-
- ## Example
- $ infisical secrets
- ┌─────────────┬──────────────┬─────────────┐
- │ SECRET NAME │ SECRET VALUE │ SECRET TYPE │
- ├─────────────┼──────────────┼─────────────┤
- │ DOMAIN │ example.com │ shared │
- │ HASH │ jebhfbwe │ shared │
- └─────────────┴──────────────┴─────────────┘
```
### flags
@@ -45,16 +36,11 @@ This command enables you to perform CRUD (create, read, update, delete) operatio
This command allows you selectively print the requested secrets by name
- ```
+ ```bash
$ infisical secrets get ...
# Example
$ infisical secrets get DOMAIN
- ┌─────────────┬──────────────┬─────────────┐
- │ SECRET NAME │ SECRET VALUE │ SECRET TYPE │
- ├─────────────┼──────────────┼─────────────┤
- │ DOMAIN │ example.com │ shared │
- └─────────────┴──────────────┴─────────────┘
```
@@ -70,18 +56,11 @@ This command enables you to perform CRUD (create, read, update, delete) operatio
This command allows you to set or update secrets in your environment. If the secret key provided already exists, its value will be updated with the new value.
If the secret key does not exist, a new secret will be created using both the key and value provided.
-```
+```bash
$ infisical secrets set ...
## Example
$ infisical secrets set STRIPE_API_KEY=sjdgwkeudyjwe DOMAIN=example.com HASH=jebhfbwe
-┌────────────────┬───────────────┬────────────────────────┐
-│ SECRET NAME │ SECRET VALUE │ STATUS │
-├────────────────┼───────────────┼────────────────────────┤
-│ STRIPE_API_KEY │ sjdgwkeudyjwe │ SECRET VALUE UNCHANGED │
-│ DOMAIN │ example.com │ SECRET VALUE MODIFIED │
-│ HASH │ jebhfbwe │ SECRET CREATED │
-└────────────────┴───────────────┴────────────────────────┘
```
### Flags
@@ -95,12 +74,11 @@ $ infisical secrets set STRIPE_API_KEY=sjdgwkeudyjwe DOMAIN=example.com HASH=jeb
This command allows you to delete secrets by their name(s).
- ```
+ ```bash
$ infisical secrets delete ...
## Example
$ infisical secrets delete STRIPE_API_KEY DOMAIN HASH
- secret name(s) [STRIPE_API_KEY, DOMAIN, HASH] have been deleted from your project
```
### Flags
diff --git a/docs/cli/faq.mdx b/docs/cli/faq.mdx
index 6a98d8dc3..7eb73258e 100644
--- a/docs/cli/faq.mdx
+++ b/docs/cli/faq.mdx
@@ -13,4 +13,9 @@ If none of the available stores work for you, you can try using the `file` store
If you are still experiencing trouble, please seek support.
[Learn more about vault command](./commands/vault)
+
+
+
+Yes. If you have previously retrieved secrets for a specific project and environment (such as dev, staging, or prod), the `run`/`secret` command will utilize the saved secrets, even when offline, on subsequent fetch attempts.
+
\ No newline at end of file
diff --git a/docs/images/k8-diagram.png b/docs/images/k8-diagram.png
new file mode 100644
index 000000000..f45d301bc
Binary files /dev/null and b/docs/images/k8-diagram.png differ
diff --git a/docs/integrations/cicd/gitlab.mdx b/docs/integrations/cicd/gitlab.mdx
new file mode 100644
index 000000000..b84d90e07
--- /dev/null
+++ b/docs/integrations/cicd/gitlab.mdx
@@ -0,0 +1,34 @@
+---
+title: "Gitlab Pipeline"
+---
+
+To integrate Infisical secrets into your Gitlab CI/CD setup, three steps are required.
+
+## Generate service token
+To expose Infisical secrets in Gitlab CI/CD, you must generate a service token for the specific project and environment in Infisical. For instructions on how to generate a service token, refer to [this page](../../getting-started/dashboard/token)
+
+## Set Infisical service token in Gitlab
+To provide Infisical CLI with the service token generated in the previous step, go to **Settings > CI/CD > Variables** in Gitlab and create a new **INFISICAL_TOKEN** variable. Enter the generated service token as its value.
+
+## Configure Infisical in your pipeline
+Edit your .gitlab-ci.yml to include the installation of the Infisical CLI. This will allow you to use the CLI for fetching and injecting secrets into any script or command within your Gitlab CI/CD process.
+
+#### Example
+```yaml
+image: ubuntu
+
+stages:
+ - build
+ - test
+ - deploy
+
+build-job:
+ stage: build
+ script:
+ - apt update && apt install -y curl
+ - curl -1sLf 'https://dl.cloudsmith.io/public/infisical/infisical-cli/setup.deb.sh' | bash
+ - apt-get update && apt-get install -y infisical
+ - infisical run -- npm run build
+
+...
+```
\ No newline at end of file
diff --git a/docs/integrations/overview.mdx b/docs/integrations/overview.mdx
index 4f1e99227..eb99ca79d 100644
--- a/docs/integrations/overview.mdx
+++ b/docs/integrations/overview.mdx
@@ -12,6 +12,7 @@ Missing an integration? Throw in a [request](https://github.com/Infisical/infisi
| [Docker](/integrations/platforms/docker) | Platform | Available |
| [Docker-Compose](/integrations/platforms/docker-compose) | Platform | Available |
| [Kubernetes](/integrations/platforms/kubernetes) | Platform | Available |
+| [PM2](/integrations/platforms/pm2) | Platform | Available |
| [Heroku](/integrations/cloud/heroku) | Cloud | Available |
| [Vercel](/integrations/cloud/vercel) | Cloud | Available |
| [Netlify](/integrations/cloud/netlify) | Cloud | Available |
@@ -36,7 +37,7 @@ Missing an integration? Throw in a [request](https://github.com/Infisical/infisi
| GCP | Cloud | Coming soon |
| Azure | Cloud | Coming soon |
| DigitalOcean | Cloud | Coming soon |
-| GitLab | CI/CD | Coming soon |
+| [GitLab Pipeline](/integrations/cicd/gitlab) | CI/CD | Available |
| [CircleCI](/integrations/cicd/circleci) | CI/CD | Coming soon |
| TravisCI | CI/CD | Coming soon |
| GitHub Actions | CI/CD | Coming soon |
diff --git a/docs/integrations/platforms/docker-compose.mdx b/docs/integrations/platforms/docker-compose.mdx
index 698316582..e030869e3 100644
--- a/docs/integrations/platforms/docker-compose.mdx
+++ b/docs/integrations/platforms/docker-compose.mdx
@@ -1,45 +1,38 @@
---
title: "Docker Compose"
-description: "How to use Infisical to inject environment variables into container defined in your Docker Compose file."
+description: "How to use Infisical to inject environment variables into services defined in your Docker Compose file."
---
-The Docker Compose integration enables you to inject environment variables from Infisical into the containers defined in your compose file.
+Prerequisites:
-## Add the CLI to your Dockerfile(s) start command
+- Set up and add envars to [Infisical Cloud](https://app.infisical.com)
-Follow the [guide to configure Infisical CLI](./docker) in your your Dockerfile first.
+## Configure the Infisical CLI for each service
-## Generate Infisical Token
+Follow this [guide](./docker) to configure the Infisical CLI for each service that you wish to inject environment variables into; you'll have to update the Dockerfile of each service.
-In order for Infisical CLI to authenticate and retrieve your project's secrets without exposing your login credentials, you must generate a Infisical Token.
-To learn how, visit [Infisical Token](../../getting-started/dashboard/token). Once you have generated the token, keep it handy.
+## Generate Infisical Tokens
-
- If you have multiple services and they do not use the same secrets, you will
- have to generate a Infisical Token for each service.
-
+Generate a unique [Infisical Token](https://infisical.com/docs/getting-started/dashboard/token) for each service.
-## Tell Docker Compose your Infisical Token
+## Add Infisical Tokens to your Docker Compose file
-For each service you want to inject secrets into, set an environment variable called `INFISICAL_TOKEN` equal to a helpful identifier variable.
-This will ensure that you can set Infisical Tokens for multiple services.
+For each service you want to inject secrets into, set an environment variable called `INFISICAL_TOKEN` equal to a unique identifier variable.
-For the example below, we have set `INFISICAL_TOKEN_FOR_WEB` and `INFISICAL_TOKEN_FOR_API` as the `INFISICAL_TOKEN` for the corresponding service.
+In the example below, we set `INFISICAL_TOKEN_FOR_WEB` and `INFISICAL_TOKEN_FOR_API` as the `INFISICAL_TOKEN` for the services.
```yaml
# Example Docker Compose file
services:
web:
build: .
- image: auledge-frontend
- container_name: auledge-frontend
+ image: example-service-1
environment:
- INFISICAL_TOKEN=${INFISICAL_TOKEN_FOR_WEB}
api:
build: .
- image: auledge-backend
- container_name: auledge-backend
+ image: example-service-2
environment:
- INFISICAL_TOKEN=${INFISICAL_TOKEN_FOR_API}
```
diff --git a/docs/integrations/platforms/docker.mdx b/docs/integrations/platforms/docker.mdx
index 8f53336ed..b4224d6d0 100644
--- a/docs/integrations/platforms/docker.mdx
+++ b/docs/integrations/platforms/docker.mdx
@@ -3,9 +3,11 @@ title: "Docker"
description: "How to use Infisical to inject environment variables into a Docker container."
---
-Infisical can be used in a Dockerfile to inject environment variables into a Docker container.
+Prerequisites:
-## Add the CLI to your Dockerfile
+- Set up and add envars to [Infisical Cloud](https://app.infisical.com)
+
+## Add the Infisical CLI to your Dockerfile
@@ -32,32 +34,32 @@ Infisical can be used in a Dockerfile to inject environment variables into a Doc
- ## Modify your Dockerfile start command
+## Modify the start command in your Dockerfile
- To make your Docker container consume Infisical secrets, you can start your application with Infisical.
- This will automatically pull the necessary secrets and make them available to your application as if they were natively exposed within the container.
+Starting your service with the Infisical CLI pulls your secrets from Infisical and injects them into your service.
- ```dockerfile
- CMD ["infisical", "run", "---", "[your application start command]"]
+```dockerfile
+CMD ["infisical", "run", "---", "[your service start command]"]
- # example with single single command
- CMD ["infisical", "run", "---", "npm run start"]
+# example with single single command
+CMD ["infisical", "run", "---", "npm run start"]
- # example with multiple commands
- CMD ["infisical", "run", "--command", "npm run start && ..."]
- ```
+# example with multiple commands
+CMD ["infisical", "run", "--command", "npm run start && ..."]
+```
- View more options for the `run` command [here](../../cli/commands/run)
+## Generate an Infisical Token
+
+Head to your project settings in Infisical Cloud to generate an [Infisical Token](https://infisical.com/docs/getting-started/dashboard/token).
## Feed Docker your Infisical Token
-The CLI looks out for an environment variable called `INFISICAL_TOKEN`. If the token is detected, the CLI will authenticate, retrieve, and inject the environment variables which the token is authorized for.
-
```bash
docker run --env INFISICAL_TOKEN=[token]...
```
-## Generate an Infisical Token
+
-[Generate an Infisical Token](../../getting-started/dashboard/token) and keep it handy.
+The Infisical CLI uses the detected `INFISICAL_TOKEN` environment variable to authenticate, retrieve, and inject the environment variables which the token is authorized for.
+
diff --git a/docs/integrations/platforms/kubernetes.mdx b/docs/integrations/platforms/kubernetes.mdx
index 189dc9d7b..07ce0aae3 100644
--- a/docs/integrations/platforms/kubernetes.mdx
+++ b/docs/integrations/platforms/kubernetes.mdx
@@ -3,10 +3,12 @@ title: 'Kubernetes'
description: "This page explains how to use Infisical to inject secrets into Kubernetes clusters."
---
-The Infisical Secrets Operator is a custom Kubernetes controller that helps keep secrets in a cluster up to date by synchronizing them.
-It is installed in its own namespace within the cluster and follows strict RBAC policies.
-The operator uses InfisicalSecret custom resources to identify which secrets to sync and where to store them.
-It is responsible for continuously updating managed secrets, and in the future may also automatically reload deployments that use them as needed.
+
+
+
+The Infisical Secrets Operator is a Kubernetes controller that retrieves secrets from Infisical and stores them in a designated cluster.
+It uses an `InfisicalSecret` resource to specify authentication and storage methods.
+The operator continuously updates secrets and can also reload dependent deployments automatically.
## Install Operator
diff --git a/docs/integrations/platforms/pm2.mdx b/docs/integrations/platforms/pm2.mdx
new file mode 100644
index 000000000..e9b04bbb2
--- /dev/null
+++ b/docs/integrations/platforms/pm2.mdx
@@ -0,0 +1,52 @@
+---
+title: "PM2"
+description: "How to use Infisical to inject environment variables and secrets with PM2 into a Node.js app"
+---
+
+Prerequisites:
+
+- Set up and add envars to [Infisical Cloud](https://app.infisical.com)
+- [Install the CLI](/cli/overview)
+
+## Initialize Infisical for your Node.js app
+
+```bash
+# navigate to the root of your of your project
+cd /path/to/project
+
+# then initialize infisical
+infisical init
+```
+
+## Create a bash or js script
+
+
+
+```bash infisical-run.sh
+infisical run -- npm start
+```
+
+```js infisical-run.js
+const spawn = require("child_process").spawn;
+
+const infisical = spawn("infisical", ["run", "--", "npm", "start"]);
+
+infisical.stdout.on("data", (data) => console.log(`${data}`));
+infisical.stderr.on("data", (data) => console.error(`${data}`));
+```
+
+
+
+## Start your application as usual but with the script
+
+
+
+```bash infisical-run.sh
+pm2 start infisical-run.sh
+```
+
+```bash infisical-run.js
+pm2 start infisical-run.js
+```
+
+
diff --git a/docs/mint.json b/docs/mint.json
index 4ce1d6c03..fd334b5ab 100644
--- a/docs/mint.json
+++ b/docs/mint.json
@@ -209,7 +209,8 @@
"pages": [
"integrations/platforms/docker",
"integrations/platforms/docker-compose",
- "integrations/platforms/kubernetes"
+ "integrations/platforms/kubernetes",
+ "integrations/platforms/pm2"
]
},
{
@@ -226,6 +227,7 @@
"group": "CI/CD",
"pages": [
"integrations/cicd/githubactions",
+ "integrations/cicd/gitlab",
"integrations/cicd/circleci"
]
},
diff --git a/frontend/package-lock.json b/frontend/package-lock.json
index f5bb934fe..b0317ff7c 100644
--- a/frontend/package-lock.json
+++ b/frontend/package-lock.json
@@ -14,11 +14,14 @@
"@fortawesome/free-solid-svg-icons": "^6.1.2",
"@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",
"@radix-ui/react-dialog": "^1.0.2",
"@radix-ui/react-dropdown-menu": "^2.0.2",
+ "@radix-ui/react-hover-card": "^1.0.3",
"@radix-ui/react-label": "^2.0.0",
"@radix-ui/react-popover": "^1.0.3",
"@radix-ui/react-progress": "^1.0.1",
@@ -51,6 +54,7 @@
"react-code-input": "^3.10.1",
"react-dom": "^17.0.2",
"react-grid-layout": "^1.3.4",
+ "react-hook-form": "^7.43.0",
"react-i18next": "^12.1.1",
"react-mailchimp-subscribe": "^2.1.3",
"react-markdown": "^8.0.3",
@@ -64,7 +68,8 @@
"tweetnacl-util": "^0.15.1",
"uuid": "^8.3.2",
"uuidv4": "^6.2.13",
- "yaml": "^2.2.0"
+ "yaml": "^2.2.0",
+ "yup": "^0.32.11"
},
"devDependencies": {
"@storybook/addon-essentials": "^7.0.0-beta.30",
@@ -108,7 +113,6 @@
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.2.0.tgz",
"integrity": "sha512-qRmjj8nj9qmLTQXXmaR1cck3UXSRMPrbsLJAasZpF+t3riI71BXed5ebIOYwQntykeZuhjsdweEc9BxH5Jc26w==",
- "dev": true,
"dependencies": {
"@jridgewell/gen-mapping": "^0.1.0",
"@jridgewell/trace-mapping": "^0.3.9"
@@ -121,7 +125,6 @@
"version": "0.1.1",
"resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.1.1.tgz",
"integrity": "sha512-sQXCasFk+U8lWYEe66WxRDOE9PjVz4vSM51fTu3Hw+ClTpUSQb718772vH3pyS5pShp6lvQM7SxgIDXXXmOX7w==",
- "dev": true,
"dependencies": {
"@jridgewell/set-array": "^1.0.0",
"@jridgewell/sourcemap-codec": "^1.4.10"
@@ -157,7 +160,6 @@
"version": "7.20.5",
"resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.20.5.tgz",
"integrity": "sha512-KZXo2t10+/jxmkhNXc7pZTqRvSOIvVv/+lJwHS+B2rErwOyjuVRh60yVpb7liQ1U5t7lLJ1bz+t8tSypUZdm0g==",
- "dev": true,
"engines": {
"node": ">=6.9.0"
}
@@ -166,7 +168,6 @@
"version": "7.20.5",
"resolved": "https://registry.npmjs.org/@babel/core/-/core-7.20.5.tgz",
"integrity": "sha512-UdOWmk4pNWTm/4DlPUl/Pt4Gz4rcEMb7CY0Y3eJl5Yz1vI8ZJGmHWaVE55LoxRjdpx0z259GE9U5STA9atUinQ==",
- "dev": true,
"dependencies": {
"@ampproject/remapping": "^2.1.0",
"@babel/code-frame": "^7.18.6",
@@ -196,7 +197,6 @@
"version": "4.3.4",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz",
"integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==",
- "dev": true,
"dependencies": {
"ms": "2.1.2"
},
@@ -212,14 +212,12 @@
"node_modules/@babel/core/node_modules/ms": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
- "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==",
- "dev": true
+ "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w=="
},
"node_modules/@babel/core/node_modules/semver": {
"version": "6.3.0",
"resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz",
"integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==",
- "dev": true,
"bin": {
"semver": "bin/semver.js"
}
@@ -265,7 +263,6 @@
"version": "7.20.7",
"resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.20.7.tgz",
"integrity": "sha512-4tGORmfQcrc+bvrjb5y3dG9Mx1IOZjsHqQVUz7XCNHO+iTmqxWnVg3KRygjGmpRLJGdQSKuvFinbIb0CnZwHAQ==",
- "dev": true,
"dependencies": {
"@babel/compat-data": "^7.20.5",
"@babel/helper-validator-option": "^7.18.6",
@@ -284,7 +281,6 @@
"version": "5.1.1",
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz",
"integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==",
- "dev": true,
"dependencies": {
"yallist": "^3.0.2"
}
@@ -293,7 +289,6 @@
"version": "6.3.0",
"resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz",
"integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==",
- "dev": true,
"bin": {
"semver": "bin/semver.js"
}
@@ -301,8 +296,7 @@
"node_modules/@babel/helper-compilation-targets/node_modules/yallist": {
"version": "3.1.1",
"resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz",
- "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==",
- "dev": true
+ "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g=="
},
"node_modules/@babel/helper-create-class-features-plugin": {
"version": "7.20.12",
@@ -461,7 +455,6 @@
"version": "7.20.11",
"resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.20.11.tgz",
"integrity": "sha512-uRy78kN4psmji1s2QtbtcCSaj/LILFDp0f/ymhpQH5QY3nljUZCaNWz9X1dEj/8MBdBEFECs7yRhKn8i7NjZgg==",
- "dev": true,
"dependencies": {
"@babel/helper-environment-visitor": "^7.18.9",
"@babel/helper-module-imports": "^7.18.6",
@@ -535,7 +528,6 @@
"version": "7.20.2",
"resolved": "https://registry.npmjs.org/@babel/helper-simple-access/-/helper-simple-access-7.20.2.tgz",
"integrity": "sha512-+0woI/WPq59IrqDYbVGfshjT5Dmk/nnbdpcF8SnMhhXObpTq2KNBdLFRFrkVdbDOyUmHBCxzm5FHV1rACIkIbA==",
- "dev": true,
"dependencies": {
"@babel/types": "^7.20.2"
},
@@ -586,7 +578,6 @@
"version": "7.18.6",
"resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.18.6.tgz",
"integrity": "sha512-XO7gESt5ouv/LRJdrVjkShckw6STTaB7l9BrpBaAHDeF5YZT+01PCwmR0SJHnkW6i8OwW/EVWRShfi4j2x+KQw==",
- "dev": true,
"engines": {
"node": ">=6.9.0"
}
@@ -610,7 +601,6 @@
"version": "7.20.6",
"resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.20.6.tgz",
"integrity": "sha512-Pf/OjgfgFRW5bApskEz5pvidpim7tEDPlFtKcNRXWmfHGn9IEI2W2flqRQXTFb7gIPTyK++N6rVHuwKut4XK6w==",
- "dev": true,
"dependencies": {
"@babel/template": "^7.18.10",
"@babel/traverse": "^7.20.5",
@@ -2897,6 +2887,14 @@
"react-dom": "^16 || ^17 || ^18"
}
},
+ "node_modules/@hookform/resolvers": {
+ "version": "2.9.10",
+ "resolved": "https://registry.npmjs.org/@hookform/resolvers/-/resolvers-2.9.10.tgz",
+ "integrity": "sha512-JIL1DgJIlH9yuxcNGtyhsWX/PgNltz+5Gr6+8SX9fhXc/hPbEIk6wPI82nhgvp3uUb6ZfAM5mqg/x7KR7NAb+A==",
+ "peerDependencies": {
+ "react-hook-form": "^7.0.0"
+ }
+ },
"node_modules/@humanwhocodes/config-array": {
"version": "0.11.8",
"resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.8.tgz",
@@ -3524,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",
@@ -3862,6 +4007,27 @@
"react-dom": "^16.8 || ^17.0 || ^18.0"
}
},
+ "node_modules/@radix-ui/react-hover-card": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-hover-card/-/react-hover-card-1.0.3.tgz",
+ "integrity": "sha512-rr2+DxPlMhR57IPcNvZ85X8chytdfj7kyVToyR5Ge0r4IJEFiyPs0Cs8/K8oe5zt+yo0F8f29vtC8tNNK+ZIkA==",
+ "dependencies": {
+ "@babel/runtime": "^7.13.10",
+ "@radix-ui/primitive": "1.0.0",
+ "@radix-ui/react-compose-refs": "1.0.0",
+ "@radix-ui/react-context": "1.0.0",
+ "@radix-ui/react-dismissable-layer": "1.0.2",
+ "@radix-ui/react-popper": "1.1.0",
+ "@radix-ui/react-portal": "1.0.1",
+ "@radix-ui/react-presence": "1.0.0",
+ "@radix-ui/react-primitive": "1.0.1",
+ "@radix-ui/react-use-controllable-state": "1.0.0"
+ },
+ "peerDependencies": {
+ "react": "^16.8 || ^17.0 || ^18.0",
+ "react-dom": "^16.8 || ^17.0 || ^18.0"
+ }
+ },
"node_modules/@radix-ui/react-id": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/@radix-ui/react-id/-/react-id-1.0.0.tgz",
@@ -7005,8 +7171,7 @@
"node_modules/@types/lodash": {
"version": "4.14.191",
"resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.14.191.tgz",
- "integrity": "sha512-BdZ5BCCvho3EIXw6wUCXHe7rS53AIDPLE+JzwgT+OsJk53oBfbSmZZ7CX4VaRoN78N+TJpFi9QPlfIVNmJYWxQ==",
- "dev": true
+ "integrity": "sha512-BdZ5BCCvho3EIXw6wUCXHe7rS53AIDPLE+JzwgT+OsJk53oBfbSmZZ7CX4VaRoN78N+TJpFi9QPlfIVNmJYWxQ=="
},
"node_modules/@types/mdast": {
"version": "3.0.10",
@@ -8772,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",
@@ -8997,7 +9167,6 @@
"version": "4.21.4",
"resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.21.4.tgz",
"integrity": "sha512-CBHJJdDmgjl3daYjN5Cp5kbTf1mUhZoS+beLklHIvkOWscs83YAhLlF3Wsh/lciQYAcbBJgTOD44VtG31ZM4Hw==",
- "dev": true,
"funding": [
{
"type": "opencollective",
@@ -10355,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",
@@ -10680,8 +10854,7 @@
"node_modules/electron-to-chromium": {
"version": "1.4.284",
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.284.tgz",
- "integrity": "sha512-M8WEXFuKXMYMVr45fo8mq0wUrrJHheiKZf6BArTKk9ZBYCKJEOU5H8cdWgDT+qCVZf7Na4lVUaZsA+h6uA9+PA==",
- "dev": true
+ "integrity": "sha512-M8WEXFuKXMYMVr45fo8mq0wUrrJHheiKZf6BArTKk9ZBYCKJEOU5H8cdWgDT+qCVZf7Na4lVUaZsA+h6uA9+PA=="
},
"node_modules/emoji-regex": {
"version": "9.2.2",
@@ -10977,7 +11150,6 @@
"version": "3.1.1",
"resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz",
"integrity": "sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==",
- "dev": true,
"engines": {
"node": ">=6"
}
@@ -12818,7 +12990,6 @@
"version": "1.0.0-beta.2",
"resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz",
"integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==",
- "dev": true,
"engines": {
"node": ">=6.9.0"
}
@@ -14086,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"
}
@@ -14991,7 +15161,6 @@
"version": "2.2.3",
"resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz",
"integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==",
- "dev": true,
"bin": {
"json5": "lib/cli.js"
},
@@ -15210,6 +15379,11 @@
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz",
"integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg=="
},
+ "node_modules/lodash-es": {
+ "version": "4.17.21",
+ "resolved": "https://registry.npmjs.org/lodash-es/-/lodash-es-4.17.21.tgz",
+ "integrity": "sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw=="
+ },
"node_modules/lodash.castarray": {
"version": "4.4.0",
"resolved": "https://registry.npmjs.org/lodash.castarray/-/lodash.castarray-4.4.0.tgz",
@@ -16183,6 +16357,11 @@
"object-assign": "^4.1.0"
}
},
+ "node_modules/nanoclone": {
+ "version": "0.2.1",
+ "resolved": "https://registry.npmjs.org/nanoclone/-/nanoclone-0.2.1.tgz",
+ "integrity": "sha512-wynEP02LmIbLpcYw8uBKpcfF6dmg2vcpKqxeH5UcoKEYdExslsdUA4ugFauuaeYdTB76ez6gJW8XAZ6CgkXYxA=="
+ },
"node_modules/nanoid": {
"version": "3.3.4",
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.4.tgz",
@@ -16433,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"
},
@@ -16464,8 +16642,7 @@
"node_modules/node-releases": {
"version": "2.0.6",
"resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.6.tgz",
- "integrity": "sha512-PiVXnNuFm5+iYkLBNeq5211hvO38y63T0i2KKh2KnUs3RpzJ+JtODFjkD8yjLwnDkTYF1eKXheUwdssR+NRZdg==",
- "dev": true
+ "integrity": "sha512-PiVXnNuFm5+iYkLBNeq5211hvO38y63T0i2KKh2KnUs3RpzJ+JtODFjkD8yjLwnDkTYF1eKXheUwdssR+NRZdg=="
},
"node_modules/normalize-package-data": {
"version": "2.5.0",
@@ -17625,6 +17802,11 @@
"react-is": "^16.13.1"
}
},
+ "node_modules/property-expr": {
+ "version": "2.0.5",
+ "resolved": "https://registry.npmjs.org/property-expr/-/property-expr-2.0.5.tgz",
+ "integrity": "sha512-IJUkICM5dP5znhCckHSv30Q4b5/JA5enCtkRHYaOVOAocnH/1BQEYTC5NMfT3AVl/iXKdr3aqQbQn9DxyWknwA=="
+ },
"node_modules/property-information": {
"version": "6.1.1",
"resolved": "https://registry.npmjs.org/property-information/-/property-information-6.1.1.tgz",
@@ -18142,6 +18324,21 @@
"react-dom": ">= 16.3.0"
}
},
+ "node_modules/react-hook-form": {
+ "version": "7.43.0",
+ "resolved": "https://registry.npmjs.org/react-hook-form/-/react-hook-form-7.43.0.tgz",
+ "integrity": "sha512-/rVEz7T0gLdSFwPqutJ1kn2e0sQNyb9ci/hmwEYr2YG0KF/LSuRLvNrf9QWJM+gj88CjDpDW5Bh/1AD7B2+z9Q==",
+ "engines": {
+ "node": ">=12.22.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/react-hook-form"
+ },
+ "peerDependencies": {
+ "react": "^16.8.0 || ^17 || ^18"
+ }
+ },
"node_modules/react-i18next": {
"version": "12.1.1",
"resolved": "https://registry.npmjs.org/react-i18next/-/react-i18next-12.1.1.tgz",
@@ -21168,11 +21365,15 @@
"node": ">=0.6"
}
},
+ "node_modules/toposort": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/toposort/-/toposort-2.0.2.tgz",
+ "integrity": "sha512-0a5EOkAUp8D4moMi2W8ZF8jcga7BgZd91O/yabJCFY8az+XSzeGyTKs0Aoo897iV1Nj6guFq8orWDS96z91oGg=="
+ },
"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",
@@ -21404,7 +21605,7 @@
"version": "4.9.3",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-4.9.3.tgz",
"integrity": "sha512-CIfGzTelbKNEnLpLdGFgdyKhG23CKdKgQPOBc+OUNrkJ2vr+KSzsSV5kq5iWhEQbok+quxgGzrAtGWCyU7tHnA==",
- "dev": true,
+ "devOptional": true,
"bin": {
"tsc": "bin/tsc",
"tsserver": "bin/tsserver"
@@ -21618,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",
@@ -21730,7 +21936,6 @@
"version": "1.0.10",
"resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.0.10.tgz",
"integrity": "sha512-OztqDenkfFkbSG+tRxBeAnCVPckDBcvibKd35yDONx6OU8N7sqgwc7rCbkJ/WcYtVRZ4ba68d6byhC21GFh7sQ==",
- "dev": true,
"funding": [
{
"type": "opencollective",
@@ -22023,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",
@@ -22222,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"
@@ -22480,6 +22683,23 @@
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
+ },
+ "node_modules/yup": {
+ "version": "0.32.11",
+ "resolved": "https://registry.npmjs.org/yup/-/yup-0.32.11.tgz",
+ "integrity": "sha512-Z2Fe1bn+eLstG8DRR6FTavGD+MeAwyfmouhHsIUgaADz8jvFKbO/fXc2trJKZg+5EBjh4gGm3iU/t3onKlXHIg==",
+ "dependencies": {
+ "@babel/runtime": "^7.15.4",
+ "@types/lodash": "^4.14.175",
+ "lodash": "^4.17.21",
+ "lodash-es": "^4.17.21",
+ "nanoclone": "^0.2.1",
+ "property-expr": "^2.0.4",
+ "toposort": "^2.0.2"
+ },
+ "engines": {
+ "node": ">=10"
+ }
}
},
"dependencies": {
@@ -22487,7 +22707,6 @@
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.2.0.tgz",
"integrity": "sha512-qRmjj8nj9qmLTQXXmaR1cck3UXSRMPrbsLJAasZpF+t3riI71BXed5ebIOYwQntykeZuhjsdweEc9BxH5Jc26w==",
- "dev": true,
"requires": {
"@jridgewell/gen-mapping": "^0.1.0",
"@jridgewell/trace-mapping": "^0.3.9"
@@ -22497,7 +22716,6 @@
"version": "0.1.1",
"resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.1.1.tgz",
"integrity": "sha512-sQXCasFk+U8lWYEe66WxRDOE9PjVz4vSM51fTu3Hw+ClTpUSQb718772vH3pyS5pShp6lvQM7SxgIDXXXmOX7w==",
- "dev": true,
"requires": {
"@jridgewell/set-array": "^1.0.0",
"@jridgewell/sourcemap-codec": "^1.4.10"
@@ -22525,14 +22743,12 @@
"@babel/compat-data": {
"version": "7.20.5",
"resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.20.5.tgz",
- "integrity": "sha512-KZXo2t10+/jxmkhNXc7pZTqRvSOIvVv/+lJwHS+B2rErwOyjuVRh60yVpb7liQ1U5t7lLJ1bz+t8tSypUZdm0g==",
- "dev": true
+ "integrity": "sha512-KZXo2t10+/jxmkhNXc7pZTqRvSOIvVv/+lJwHS+B2rErwOyjuVRh60yVpb7liQ1U5t7lLJ1bz+t8tSypUZdm0g=="
},
"@babel/core": {
"version": "7.20.5",
"resolved": "https://registry.npmjs.org/@babel/core/-/core-7.20.5.tgz",
"integrity": "sha512-UdOWmk4pNWTm/4DlPUl/Pt4Gz4rcEMb7CY0Y3eJl5Yz1vI8ZJGmHWaVE55LoxRjdpx0z259GE9U5STA9atUinQ==",
- "dev": true,
"requires": {
"@ampproject/remapping": "^2.1.0",
"@babel/code-frame": "^7.18.6",
@@ -22555,7 +22771,6 @@
"version": "4.3.4",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz",
"integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==",
- "dev": true,
"requires": {
"ms": "2.1.2"
}
@@ -22563,14 +22778,12 @@
"ms": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
- "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==",
- "dev": true
+ "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w=="
},
"semver": {
"version": "6.3.0",
"resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz",
- "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==",
- "dev": true
+ "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw=="
}
}
},
@@ -22606,7 +22819,6 @@
"version": "7.20.7",
"resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.20.7.tgz",
"integrity": "sha512-4tGORmfQcrc+bvrjb5y3dG9Mx1IOZjsHqQVUz7XCNHO+iTmqxWnVg3KRygjGmpRLJGdQSKuvFinbIb0CnZwHAQ==",
- "dev": true,
"requires": {
"@babel/compat-data": "^7.20.5",
"@babel/helper-validator-option": "^7.18.6",
@@ -22619,7 +22831,6 @@
"version": "5.1.1",
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz",
"integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==",
- "dev": true,
"requires": {
"yallist": "^3.0.2"
}
@@ -22627,14 +22838,12 @@
"semver": {
"version": "6.3.0",
"resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz",
- "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==",
- "dev": true
+ "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw=="
},
"yallist": {
"version": "3.1.1",
"resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz",
- "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==",
- "dev": true
+ "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g=="
}
}
},
@@ -22753,7 +22962,6 @@
"version": "7.20.11",
"resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.20.11.tgz",
"integrity": "sha512-uRy78kN4psmji1s2QtbtcCSaj/LILFDp0f/ymhpQH5QY3nljUZCaNWz9X1dEj/8MBdBEFECs7yRhKn8i7NjZgg==",
- "dev": true,
"requires": {
"@babel/helper-environment-visitor": "^7.18.9",
"@babel/helper-module-imports": "^7.18.6",
@@ -22809,7 +23017,6 @@
"version": "7.20.2",
"resolved": "https://registry.npmjs.org/@babel/helper-simple-access/-/helper-simple-access-7.20.2.tgz",
"integrity": "sha512-+0woI/WPq59IrqDYbVGfshjT5Dmk/nnbdpcF8SnMhhXObpTq2KNBdLFRFrkVdbDOyUmHBCxzm5FHV1rACIkIbA==",
- "dev": true,
"requires": {
"@babel/types": "^7.20.2"
}
@@ -22844,8 +23051,7 @@
"@babel/helper-validator-option": {
"version": "7.18.6",
"resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.18.6.tgz",
- "integrity": "sha512-XO7gESt5ouv/LRJdrVjkShckw6STTaB7l9BrpBaAHDeF5YZT+01PCwmR0SJHnkW6i8OwW/EVWRShfi4j2x+KQw==",
- "dev": true
+ "integrity": "sha512-XO7gESt5ouv/LRJdrVjkShckw6STTaB7l9BrpBaAHDeF5YZT+01PCwmR0SJHnkW6i8OwW/EVWRShfi4j2x+KQw=="
},
"@babel/helper-wrap-function": {
"version": "7.20.5",
@@ -22863,7 +23069,6 @@
"version": "7.20.6",
"resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.20.6.tgz",
"integrity": "sha512-Pf/OjgfgFRW5bApskEz5pvidpim7tEDPlFtKcNRXWmfHGn9IEI2W2flqRQXTFb7gIPTyK++N6rVHuwKut4XK6w==",
- "dev": true,
"requires": {
"@babel/template": "^7.18.10",
"@babel/traverse": "^7.20.5",
@@ -24061,7 +24266,8 @@
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/@emotion/use-insertion-effect-with-fallbacks/-/use-insertion-effect-with-fallbacks-1.0.0.tgz",
"integrity": "sha512-1eEgUGmkaljiBnRMTdksDV1W4kUnmwgp7X9G8B++9GYwl1lUdqSndSriIrTJ0N7LQaoauY9JJ2yhiOYK5+NI4A==",
- "dev": true
+ "dev": true,
+ "requires": {}
},
"@emotion/utils": {
"version": "1.2.0",
@@ -24366,7 +24572,14 @@
"@headlessui/react": {
"version": "1.6.6",
"resolved": "https://registry.npmjs.org/@headlessui/react/-/react-1.6.6.tgz",
- "integrity": "sha512-MFJtmj9Xh/hhBMhLccGbBoSk+sk61BlP6sJe4uQcVMtXZhCgGqd2GyIQzzmsdPdTEWGSF434CBi8mnhR6um46Q=="
+ "integrity": "sha512-MFJtmj9Xh/hhBMhLccGbBoSk+sk61BlP6sJe4uQcVMtXZhCgGqd2GyIQzzmsdPdTEWGSF434CBi8mnhR6um46Q==",
+ "requires": {}
+ },
+ "@hookform/resolvers": {
+ "version": "2.9.10",
+ "resolved": "https://registry.npmjs.org/@hookform/resolvers/-/resolvers-2.9.10.tgz",
+ "integrity": "sha512-JIL1DgJIlH9yuxcNGtyhsWX/PgNltz+5Gr6+8SX9fhXc/hPbEIk6wPI82nhgvp3uUb6ZfAM5mqg/x7KR7NAb+A==",
+ "requires": {}
},
"@humanwhocodes/config-array": {
"version": "0.11.8",
@@ -24774,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",
@@ -25017,6 +25342,23 @@
"@radix-ui/react-use-callback-ref": "1.0.0"
}
},
+ "@radix-ui/react-hover-card": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-hover-card/-/react-hover-card-1.0.3.tgz",
+ "integrity": "sha512-rr2+DxPlMhR57IPcNvZ85X8chytdfj7kyVToyR5Ge0r4IJEFiyPs0Cs8/K8oe5zt+yo0F8f29vtC8tNNK+ZIkA==",
+ "requires": {
+ "@babel/runtime": "^7.13.10",
+ "@radix-ui/primitive": "1.0.0",
+ "@radix-ui/react-compose-refs": "1.0.0",
+ "@radix-ui/react-context": "1.0.0",
+ "@radix-ui/react-dismissable-layer": "1.0.2",
+ "@radix-ui/react-popper": "1.1.0",
+ "@radix-ui/react-portal": "1.0.1",
+ "@radix-ui/react-presence": "1.0.0",
+ "@radix-ui/react-primitive": "1.0.1",
+ "@radix-ui/react-use-controllable-state": "1.0.0"
+ }
+ },
"@radix-ui/react-id": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/@radix-ui/react-id/-/react-id-1.0.0.tgz",
@@ -27340,8 +27682,7 @@
"@types/lodash": {
"version": "4.14.191",
"resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.14.191.tgz",
- "integrity": "sha512-BdZ5BCCvho3EIXw6wUCXHe7rS53AIDPLE+JzwgT+OsJk53oBfbSmZZ7CX4VaRoN78N+TJpFi9QPlfIVNmJYWxQ==",
- "dev": true
+ "integrity": "sha512-BdZ5BCCvho3EIXw6wUCXHe7rS53AIDPLE+JzwgT+OsJk53oBfbSmZZ7CX4VaRoN78N+TJpFi9QPlfIVNmJYWxQ=="
},
"@types/mdast": {
"version": "3.0.10",
@@ -28016,7 +28357,8 @@
"version": "5.3.2",
"resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz",
"integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==",
- "dev": true
+ "dev": true,
+ "requires": {}
},
"acorn-node": {
"version": "1.8.2",
@@ -28150,7 +28492,8 @@
"version": "3.5.2",
"resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-3.5.2.tgz",
"integrity": "sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ==",
- "dev": true
+ "dev": true,
+ "requires": {}
},
"ansi-align": {
"version": "3.0.1",
@@ -28444,7 +28787,8 @@
"axios-auth-refresh": {
"version": "3.3.3",
"resolved": "https://registry.npmjs.org/axios-auth-refresh/-/axios-auth-refresh-3.3.3.tgz",
- "integrity": "sha512-2IbDhJ/h6ddNBBnnzn1VFK/qx17pE9aVqiafB8rx5LVHsJ1HtFpUGkbXY7PzTG+8P9HJWcyA3fNZl9BikSuilg=="
+ "integrity": "sha512-2IbDhJ/h6ddNBBnnzn1VFK/qx17pE9aVqiafB8rx5LVHsJ1HtFpUGkbXY7PzTG+8P9HJWcyA3fNZl9BikSuilg==",
+ "requires": {}
},
"axobject-query": {
"version": "3.1.1",
@@ -28459,7 +28803,8 @@
"version": "7.0.0-bridge.0",
"resolved": "https://registry.npmjs.org/babel-core/-/babel-core-7.0.0-bridge.0.tgz",
"integrity": "sha512-poPX9mZH/5CSanm50Q+1toVci6pv5KSRv/5TWCwtzQS5XEwn40BcCrgIeMFWP9CKKIniKXNxoIOnOq4VVlGXhg==",
- "dev": true
+ "dev": true,
+ "requires": {}
},
"babel-loader": {
"version": "9.1.2",
@@ -28667,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",
@@ -28838,7 +29188,6 @@
"version": "4.21.4",
"resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.21.4.tgz",
"integrity": "sha512-CBHJJdDmgjl3daYjN5Cp5kbTf1mUhZoS+beLklHIvkOWscs83YAhLlF3Wsh/lciQYAcbBJgTOD44VtG31ZM4Hw==",
- "dev": true,
"requires": {
"caniuse-lite": "^1.0.30001400",
"electron-to-chromium": "^1.4.251",
@@ -29668,7 +30017,8 @@
"cva": {
"version": "npm:class-variance-authority@0.4.0",
"resolved": "https://registry.npmjs.org/class-variance-authority/-/class-variance-authority-0.4.0.tgz",
- "integrity": "sha512-74enNN8O9ZNieycac/y8FxqgyzZhZbxmCitAtAeUrLPlxjSd5zA7LfpprmxEcOmQBnaGs5hYhiSGnJ0mqrtBLQ=="
+ "integrity": "sha512-74enNN8O9ZNieycac/y8FxqgyzZhZbxmCitAtAeUrLPlxjSd5zA7LfpprmxEcOmQBnaGs5hYhiSGnJ0mqrtBLQ==",
+ "requires": {}
},
"damerau-levenshtein": {
"version": "1.0.8",
@@ -29863,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",
@@ -30121,8 +30476,7 @@
"electron-to-chromium": {
"version": "1.4.284",
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.284.tgz",
- "integrity": "sha512-M8WEXFuKXMYMVr45fo8mq0wUrrJHheiKZf6BArTKk9ZBYCKJEOU5H8cdWgDT+qCVZf7Na4lVUaZsA+h6uA9+PA==",
- "dev": true
+ "integrity": "sha512-M8WEXFuKXMYMVr45fo8mq0wUrrJHheiKZf6BArTKk9ZBYCKJEOU5H8cdWgDT+qCVZf7Na4lVUaZsA+h6uA9+PA=="
},
"emoji-regex": {
"version": "9.2.2",
@@ -30368,8 +30722,7 @@
"escalade": {
"version": "3.1.1",
"resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz",
- "integrity": "sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==",
- "dev": true
+ "integrity": "sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw=="
},
"escape-html": {
"version": "1.0.3",
@@ -30632,7 +30985,8 @@
"version": "8.6.0",
"resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-8.6.0.tgz",
"integrity": "sha512-bAF0eLpLVqP5oEVUFKpMA+NnRFICwn9X8B5jrR9FcqnYBuPbqWEjTEspPWMj5ye6czoSLDweCzSo3Ko7gGrZaA==",
- "dev": true
+ "dev": true,
+ "requires": {}
},
"eslint-import-resolver-node": {
"version": "0.3.7",
@@ -30890,13 +31244,15 @@
"version": "4.6.0",
"resolved": "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-4.6.0.tgz",
"integrity": "sha512-oFc7Itz9Qxh2x4gNHStv3BqJq54ExXmfC+a1NjAta66IAN87Wu0R/QArgIS9qKzX3dXKPI9H5crl9QchNMY9+g==",
- "dev": true
+ "dev": true,
+ "requires": {}
},
"eslint-plugin-simple-import-sort": {
"version": "8.0.0",
"resolved": "https://registry.npmjs.org/eslint-plugin-simple-import-sort/-/eslint-plugin-simple-import-sort-8.0.0.tgz",
"integrity": "sha512-bXgJQ+lqhtQBCuWY/FUWdB27j4+lqcvXv5rUARkzbeWLwea+S5eBZEQrhnO+WgX3ZoJHVj0cn943iyXwByHHQw==",
- "dev": true
+ "dev": true,
+ "requires": {}
},
"eslint-plugin-storybook": {
"version": "0.6.10",
@@ -31760,8 +32116,7 @@
"gensync": {
"version": "1.0.0-beta.2",
"resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz",
- "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==",
- "dev": true
+ "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg=="
},
"get-caller-file": {
"version": "2.0.5",
@@ -32325,7 +32680,8 @@
"version": "5.1.0",
"resolved": "https://registry.npmjs.org/icss-utils/-/icss-utils-5.1.0.tgz",
"integrity": "sha512-soFhflCVWLfRNOPU3iv5Z9VUdT44xFRbzjLsEzSr5AQmgqPMTHdU3PMT1Cf1ssx8fLNJDA1juftYl+PUcv3MqA==",
- "dev": true
+ "dev": true,
+ "requires": {}
},
"ieee754": {
"version": "1.2.1",
@@ -32656,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",
@@ -33339,8 +33694,7 @@
"json5": {
"version": "2.2.3",
"resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz",
- "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==",
- "dev": true
+ "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg=="
},
"jsonfile": {
"version": "6.1.0",
@@ -33508,6 +33862,11 @@
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz",
"integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg=="
},
+ "lodash-es": {
+ "version": "4.17.21",
+ "resolved": "https://registry.npmjs.org/lodash-es/-/lodash-es-4.17.21.tgz",
+ "integrity": "sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw=="
+ },
"lodash.castarray": {
"version": "4.4.0",
"resolved": "https://registry.npmjs.org/lodash.castarray/-/lodash.castarray-4.4.0.tgz",
@@ -33638,7 +33997,8 @@
"version": "7.1.8",
"resolved": "https://registry.npmjs.org/markdown-to-jsx/-/markdown-to-jsx-7.1.8.tgz",
"integrity": "sha512-rRSa1aFmFnpDRFAhv5vIkWM4nPaoB9vnzIjuIKa1wGupfn2hdCNeaQHKpu4/muoc8n8J7yowjTP2oncA4/Rbgg==",
- "dev": true
+ "dev": true,
+ "requires": {}
},
"md5.js": {
"version": "1.3.5",
@@ -34155,6 +34515,11 @@
"object-assign": "^4.1.0"
}
},
+ "nanoclone": {
+ "version": "0.2.1",
+ "resolved": "https://registry.npmjs.org/nanoclone/-/nanoclone-0.2.1.tgz",
+ "integrity": "sha512-wynEP02LmIbLpcYw8uBKpcfF6dmg2vcpKqxeH5UcoKEYdExslsdUA4ugFauuaeYdTB76ez6gJW8XAZ6CgkXYxA=="
+ },
"nanoid": {
"version": "3.3.4",
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.4.tgz",
@@ -34324,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"
}
@@ -34344,8 +34708,7 @@
"node-releases": {
"version": "2.0.6",
"resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.6.tgz",
- "integrity": "sha512-PiVXnNuFm5+iYkLBNeq5211hvO38y63T0i2KKh2KnUs3RpzJ+JtODFjkD8yjLwnDkTYF1eKXheUwdssR+NRZdg==",
- "dev": true
+ "integrity": "sha512-PiVXnNuFm5+iYkLBNeq5211hvO38y63T0i2KKh2KnUs3RpzJ+JtODFjkD8yjLwnDkTYF1eKXheUwdssR+NRZdg=="
},
"normalize-package-data": {
"version": "2.5.0",
@@ -34935,7 +35298,8 @@
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/postcss-modules-extract-imports/-/postcss-modules-extract-imports-3.0.0.tgz",
"integrity": "sha512-bdHleFnP3kZ4NYDhuGlVK+CMrQ/pqUm8bx/oGL93K6gVwiclvX5x0n76fYMKuIGKzlABOy13zsvqjb0f92TEXw==",
- "dev": true
+ "dev": true,
+ "requires": {}
},
"postcss-modules-local-by-default": {
"version": "4.0.0",
@@ -35044,7 +35408,8 @@
"version": "0.2.2",
"resolved": "https://registry.npmjs.org/prettier-plugin-tailwindcss/-/prettier-plugin-tailwindcss-0.2.2.tgz",
"integrity": "sha512-5RjUbWRe305pUpc48MosoIp6uxZvZxrM6GyOgsbGLTce+ehePKNm7ziW2dLG2air9aXbGuXlHVSQQw4Lbosq3w==",
- "dev": true
+ "dev": true,
+ "requires": {}
},
"pretty-error": {
"version": "4.0.0",
@@ -35132,6 +35497,11 @@
"react-is": "^16.13.1"
}
},
+ "property-expr": {
+ "version": "2.0.5",
+ "resolved": "https://registry.npmjs.org/property-expr/-/property-expr-2.0.5.tgz",
+ "integrity": "sha512-IJUkICM5dP5znhCckHSv30Q4b5/JA5enCtkRHYaOVOAocnH/1BQEYTC5NMfT3AVl/iXKdr3aqQbQn9DxyWknwA=="
+ },
"property-information": {
"version": "6.1.1",
"resolved": "https://registry.npmjs.org/property-information/-/property-information-6.1.1.tgz",
@@ -35437,7 +35807,8 @@
"version": "5.6.1",
"resolved": "https://registry.npmjs.org/react-colorful/-/react-colorful-5.6.1.tgz",
"integrity": "sha512-1exovf0uGTGyq5mXQT0zgQ80uvj2PCwvF8zY1RN9/vbJVSjSo3fsB/4L3ObbF7u70NduSiK4xu4Y6q1MHoUGEw==",
- "dev": true
+ "dev": true,
+ "requires": {}
},
"react-docgen": {
"version": "5.4.3",
@@ -35469,7 +35840,8 @@
"version": "2.2.2",
"resolved": "https://registry.npmjs.org/react-docgen-typescript/-/react-docgen-typescript-2.2.2.tgz",
"integrity": "sha512-tvg2ZtOpOi6QDwsb3GZhOjDkkX0h8Z2gipvTg6OVMUyoYoURhEiRNePT8NZItTVCDh39JJHnLdfCOkzoLbFnTg==",
- "dev": true
+ "dev": true,
+ "requires": {}
},
"react-dom": {
"version": "17.0.2",
@@ -35521,6 +35893,12 @@
"react-resizable": "^3.0.4"
}
},
+ "react-hook-form": {
+ "version": "7.43.0",
+ "resolved": "https://registry.npmjs.org/react-hook-form/-/react-hook-form-7.43.0.tgz",
+ "integrity": "sha512-/rVEz7T0gLdSFwPqutJ1kn2e0sQNyb9ci/hmwEYr2YG0KF/LSuRLvNrf9QWJM+gj88CjDpDW5Bh/1AD7B2+z9Q==",
+ "requires": {}
+ },
"react-i18next": {
"version": "12.1.1",
"resolved": "https://registry.npmjs.org/react-i18next/-/react-i18next-12.1.1.tgz",
@@ -35534,7 +35912,8 @@
"version": "6.0.1",
"resolved": "https://registry.npmjs.org/react-inspector/-/react-inspector-6.0.1.tgz",
"integrity": "sha512-cxKSeFTf7jpSSVddm66sKdolG90qURAX3g1roTeaN6x0YEbtWc8JpmFN9+yIqLNH2uEkYerWLtJZIXRIFuBKrg==",
- "dev": true
+ "dev": true,
+ "requires": {}
},
"react-is": {
"version": "16.13.1",
@@ -35649,7 +36028,8 @@
"react-table": {
"version": "7.8.0",
"resolved": "https://registry.npmjs.org/react-table/-/react-table-7.8.0.tgz",
- "integrity": "sha512-hNaz4ygkZO4bESeFfnfOft73iBUj8K5oKi1EcSHPAibEydfsX2MyU6Z8KCr3mv3C9Kqqh71U+DhZkFvibbnPbA=="
+ "integrity": "sha512-hNaz4ygkZO4bESeFfnfOft73iBUj8K5oKi1EcSHPAibEydfsX2MyU6Z8KCr3mv3C9Kqqh71U+DhZkFvibbnPbA==",
+ "requires": {}
},
"read-cache": {
"version": "1.0.0",
@@ -35805,7 +36185,8 @@
"redux-thunk": {
"version": "2.4.1",
"resolved": "https://registry.npmjs.org/redux-thunk/-/redux-thunk-2.4.1.tgz",
- "integrity": "sha512-OOYGNY5Jy2TWvTL1KgAlVy6dcx3siPJ1wTq741EPyUKfn6W6nChdICjZwCd0p8AZBs5kWpZlbkXW2nE/zjUa+Q=="
+ "integrity": "sha512-OOYGNY5Jy2TWvTL1KgAlVy6dcx3siPJ1wTq741EPyUKfn6W6nChdICjZwCd0p8AZBs5kWpZlbkXW2nE/zjUa+Q==",
+ "requires": {}
},
"regenerate": {
"version": "1.4.2",
@@ -37258,7 +37639,8 @@
"version": "3.3.1",
"resolved": "https://registry.npmjs.org/style-loader/-/style-loader-3.3.1.tgz",
"integrity": "sha512-GPcQ+LDJbrcxHORTRes6Jy2sfvK2kS6hpSfI/fXhPt+spVzxF6LJ1dHLN9zIGmVaaP044YKaIatFaufENRiDoQ==",
- "dev": true
+ "dev": true,
+ "requires": {}
},
"style-to-object": {
"version": "0.3.0",
@@ -37288,7 +37670,8 @@
"styled-jsx": {
"version": "5.0.7",
"resolved": "https://registry.npmjs.org/styled-jsx/-/styled-jsx-5.0.7.tgz",
- "integrity": "sha512-b3sUzamS086YLRuvnaDigdAewz1/EFYlHpYBP5mZovKEdQQOIIYq8lApylub3HHZ6xFjV051kkGU7cudJmrXEA=="
+ "integrity": "sha512-b3sUzamS086YLRuvnaDigdAewz1/EFYlHpYBP5mZovKEdQQOIIYq8lApylub3HHZ6xFjV051kkGU7cudJmrXEA==",
+ "requires": {}
},
"stylis": {
"version": "4.0.13",
@@ -37750,11 +38133,15 @@
"integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==",
"dev": true
},
+ "toposort": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/toposort/-/toposort-2.0.2.tgz",
+ "integrity": "sha512-0a5EOkAUp8D4moMi2W8ZF8jcga7BgZd91O/yabJCFY8az+XSzeGyTKs0Aoo897iV1Nj6guFq8orWDS96z91oGg=="
+ },
"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",
@@ -37928,7 +38315,7 @@
"version": "4.9.3",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-4.9.3.tgz",
"integrity": "sha512-CIfGzTelbKNEnLpLdGFgdyKhG23CKdKgQPOBc+OUNrkJ2vr+KSzsSV5kq5iWhEQbok+quxgGzrAtGWCyU7tHnA==",
- "dev": true
+ "devOptional": true
},
"uc.micro": {
"version": "1.0.6",
@@ -38076,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",
@@ -38164,7 +38556,6 @@
"version": "1.0.10",
"resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.0.10.tgz",
"integrity": "sha512-OztqDenkfFkbSG+tRxBeAnCVPckDBcvibKd35yDONx6OU8N7sqgwc7rCbkJ/WcYtVRZ4ba68d6byhC21GFh7sQ==",
- "dev": true,
"requires": {
"escalade": "^3.1.1",
"picocolors": "^1.0.0"
@@ -38202,12 +38593,14 @@
"use-isomorphic-layout-effect": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/use-isomorphic-layout-effect/-/use-isomorphic-layout-effect-1.1.2.tgz",
- "integrity": "sha512-49L8yCO3iGT/ZF9QttjwLF/ZD9Iwto5LnH5LmEdk/6cFmXddqi2ulF0edxTwjj+7mqvpVVGQWvbXZdn32wRSHA=="
+ "integrity": "sha512-49L8yCO3iGT/ZF9QttjwLF/ZD9Iwto5LnH5LmEdk/6cFmXddqi2ulF0edxTwjj+7mqvpVVGQWvbXZdn32wRSHA==",
+ "requires": {}
},
"use-memo-one": {
"version": "1.1.3",
"resolved": "https://registry.npmjs.org/use-memo-one/-/use-memo-one-1.1.3.tgz",
- "integrity": "sha512-g66/K7ZQGYrI6dy8GLpVcMsBp4s17xNkYJVSMvTEevGy3nDxHOfE6z8BVE22+5G5x7t3+bhzrlTDB7ObrEE0cQ=="
+ "integrity": "sha512-g66/K7ZQGYrI6dy8GLpVcMsBp4s17xNkYJVSMvTEevGy3nDxHOfE6z8BVE22+5G5x7t3+bhzrlTDB7ObrEE0cQ==",
+ "requires": {}
},
"use-sidecar": {
"version": "1.1.2",
@@ -38221,7 +38614,8 @@
"use-sync-external-store": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.2.0.tgz",
- "integrity": "sha512-eEgnFxGQ1Ife9bzYs6VLi8/4X6CObHMw9Qr9tPY43iKwsPw8xE8+EFsf/2cFZ5S3esXgpWgtSCtLNS41F+sKPA=="
+ "integrity": "sha512-eEgnFxGQ1Ife9bzYs6VLi8/4X6CObHMw9Qr9tPY43iKwsPw8xE8+EFsf/2cFZ5S3esXgpWgtSCtLNS41F+sKPA==",
+ "requires": {}
},
"util": {
"version": "0.12.5",
@@ -38367,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",
@@ -38412,7 +38805,8 @@
"version": "1.8.0",
"resolved": "https://registry.npmjs.org/acorn-import-assertions/-/acorn-import-assertions-1.8.0.tgz",
"integrity": "sha512-m7VZ3jwz4eK6A4Vtt8Ew1/mNbP24u0FhdyfA7fSvnJR6LMdfOYnmuIrrJAgrYfYJ10F/otaHTtrtrtmHdMNzEw==",
- "dev": true
+ "dev": true,
+ "requires": {}
},
"eslint-scope": {
"version": "5.1.1",
@@ -38513,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"
@@ -38638,7 +39031,8 @@
"version": "8.12.0",
"resolved": "https://registry.npmjs.org/ws/-/ws-8.12.0.tgz",
"integrity": "sha512-kU62emKIdKVeEIOIKVegvqpXMSTAMLJozpHZaJNDYqBjzlSYXQGviYwN1osDLJ9av68qHd4a2oSjd7yD4pacig==",
- "dev": true
+ "dev": true,
+ "requires": {}
},
"xtend": {
"version": "4.0.2",
@@ -38698,6 +39092,20 @@
"resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz",
"integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==",
"dev": true
+ },
+ "yup": {
+ "version": "0.32.11",
+ "resolved": "https://registry.npmjs.org/yup/-/yup-0.32.11.tgz",
+ "integrity": "sha512-Z2Fe1bn+eLstG8DRR6FTavGD+MeAwyfmouhHsIUgaADz8jvFKbO/fXc2trJKZg+5EBjh4gGm3iU/t3onKlXHIg==",
+ "requires": {
+ "@babel/runtime": "^7.15.4",
+ "@types/lodash": "^4.14.175",
+ "lodash": "^4.17.21",
+ "lodash-es": "^4.17.21",
+ "nanoclone": "^0.2.1",
+ "property-expr": "^2.0.4",
+ "toposort": "^2.0.2"
+ }
}
}
}
diff --git a/frontend/package.json b/frontend/package.json
index f97edd68f..4e2e3e585 100644
--- a/frontend/package.json
+++ b/frontend/package.json
@@ -21,11 +21,14 @@
"@fortawesome/free-solid-svg-icons": "^6.1.2",
"@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",
"@radix-ui/react-dialog": "^1.0.2",
"@radix-ui/react-dropdown-menu": "^2.0.2",
+ "@radix-ui/react-hover-card": "^1.0.3",
"@radix-ui/react-label": "^2.0.0",
"@radix-ui/react-popover": "^1.0.3",
"@radix-ui/react-progress": "^1.0.1",
@@ -58,6 +61,7 @@
"react-code-input": "^3.10.1",
"react-dom": "^17.0.2",
"react-grid-layout": "^1.3.4",
+ "react-hook-form": "^7.43.0",
"react-i18next": "^12.1.1",
"react-mailchimp-subscribe": "^2.1.3",
"react-markdown": "^8.0.3",
@@ -71,7 +75,8 @@
"tweetnacl-util": "^0.15.1",
"uuid": "^8.3.2",
"uuidv4": "^6.2.13",
- "yaml": "^2.2.0"
+ "yaml": "^2.2.0",
+ "yup": "^0.32.11"
},
"devDependencies": {
"@storybook/addon-essentials": "^7.0.0-beta.30",
diff --git a/frontend/public/data/frequentConstants.ts b/frontend/public/data/frequentConstants.ts
index 2ff525e54..f25da26d3 100644
--- a/frontend/public/data/frequentConstants.ts
+++ b/frontend/public/data/frequentConstants.ts
@@ -2,6 +2,16 @@ interface Mapping {
[key: string]: string;
}
+const integrationSlugNameMapping: Mapping = {
+ 'azure-key-vault': 'Azure Key Vault',
+ 'heroku': 'Heroku',
+ 'vercel': 'Vercel',
+ 'netlify': 'Netlify',
+ 'github': 'GitHub',
+ 'render': 'Render',
+ 'flyio': 'Fly.io'
+}
+
const envMapping: Mapping = {
Development: "dev",
Staging: "staging",
@@ -19,7 +29,7 @@ const reverseEnvMapping: Mapping = {
const contextNetlifyMapping: Mapping = {
"dev": "Local development",
"branch-deploy": "Branch deploys",
- "deploy-review": "Deploy Previews",
+ "deploy-preview": "Deploy Previews",
"production": "Production"
}
@@ -49,6 +59,7 @@ const plans = plansProd || plansDev;
export {
contextNetlifyMapping,
envMapping,
+ integrationSlugNameMapping,
plans,
reverseContextNetlifyMapping,
reverseEnvMapping}
diff --git a/frontend/public/data/frequentInterfaces.ts b/frontend/public/data/frequentInterfaces.ts
index c2fa797fd..9865d9909 100644
--- a/frontend/public/data/frequentInterfaces.ts
+++ b/frontend/public/data/frequentInterfaces.ts
@@ -1,3 +1,12 @@
+export interface Tag {
+ _id: string;
+ name: string;
+ slug: string;
+ user: string;
+ workspace: string;
+ createdAt: string;
+}
+
export interface SecretDataProps {
pos: number;
key: string;
@@ -5,4 +14,5 @@ export interface SecretDataProps {
valueOverride: string | undefined;
id: string;
comment: string;
+ tags: Tag[];
}
\ No newline at end of file
diff --git a/frontend/public/locales/en/common.json b/frontend/public/locales/en/common.json
index e02c4f474..3d840c663 100644
--- a/frontend/public/locales/en/common.json
+++ b/frontend/public/locales/en/common.json
@@ -14,7 +14,7 @@
"save-changes": "Save Changes",
"saved": "Saved",
"drop-zone": "Drag and drop a .env or .yml file here.",
- "drop-zone-keys": "Drag and drop a .env or .yml file here to add more keys.",
+ "drop-zone-keys": "Drag and drop a .env or .yml file here to add more secrets.",
"role": "Role",
"role_admin": "admin",
"display-name": "Display Name",
diff --git a/frontend/public/locales/en/section-token.json b/frontend/public/locales/en/section-token.json
index d12bd202c..ce468b1af 100644
--- a/frontend/public/locales/en/section-token.json
+++ b/frontend/public/locales/en/section-token.json
@@ -4,7 +4,7 @@
"add-new": "Add New Token",
"add-dialog": {
"title": "Add a service token for {{target}}",
- "description": "Specify the name, environment, and expiry period. When a token is generated, you will only be able to see it once before it disappears. Make sure to save it somewhere.",
+ "description": "When a token is generated, you will only be able to see it once before it disappears. Make sure to save it somewhere.",
"name": "Service Token Name",
"add": "Add Service Token",
"copy-service-token": "Copy your service token",
diff --git a/frontend/public/locales/en/settings-project.json b/frontend/public/locales/en/settings-project.json
index 03e4b1459..3f3401f2e 100644
--- a/frontend/public/locales/en/settings-project.json
+++ b/frontend/public/locales/en/settings-project.json
@@ -9,5 +9,7 @@
"project-id-description": "To integrate Infisical into your code base and get automatic injection of environmental variables, you should use the following Project ID.",
"project-id-description2": "For more guidance, including code snipets for various languages and frameworks, see ",
"auto-generated": "This is your project's auto-generated unique identifier. It can't be changed.",
- "docs": "Infisical Docs"
+ "docs": "Infisical Docs",
+ "auto-capitalization": "Auto Capitalization",
+ "auto-capitalization-description": "According to standards, Infisical will automatically capitalize your keys. If you want to disable this feature, you can do so here."
}
diff --git a/frontend/src/components/basic/Layout.tsx b/frontend/src/components/basic/Layout.tsx
index a3d6092a1..c6e234058 100644
--- a/frontend/src/components/basic/Layout.tsx
+++ b/frontend/src/components/basic/Layout.tsx
@@ -187,10 +187,10 @@ const Layout = ({ children }: LayoutProps) => {
});
const userWorkspaces = orgUserProjects;
if (
- userWorkspaces.length === 0 &&
- router.asPath !== '/noprojects' &&
- !router.asPath.includes('home') &&
- !router.asPath.includes('settings') ||
+ (userWorkspaces.length === 0 &&
+ router.asPath !== '/noprojects' &&
+ !router.asPath.includes('home') &&
+ !router.asPath.includes('settings')) ||
router.asPath === '/dashboard/undefined'
) {
router.push('/noprojects');
@@ -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)
@@ -246,16 +246,15 @@ const Layout = ({ children }: LayoutProps) => {
return (
<>
-
-
+
-
-
-
+
+
+
{/*
*/}
-
-
+
+
{t('nav:menu.project')}
{Object.keys(workspaceMapping).length > 0 ? (
@@ -279,29 +278,29 @@ const Layout = ({ children }: LayoutProps) => {
{Object.keys(workspaceMapping).length > 0 &&
menuItems.map(({ href, title, emoji }) => (
-
+
{router.asPath.split('/')[1] === href.split('/')[1] &&
(['project', 'billing', 'org', 'personal'].includes(
router.asPath.split('/')[2]
)
? router.asPath.split('/')[2] === href.split('/')[2]
: true) ? (
-
-
-
+
) : router.asPath === '/noprojects' ? (
-
-
{emoji}
+
) : (
-
-
+
+
{emoji}
{title}
@@ -312,11 +311,11 @@ const Layout = ({ children }: LayoutProps) => {
))}
-
+
{router.asPath.split('/')[1] === 'home' ? (
-
-
-
+
+
+
Infisical Guide
@@ -336,8 +335,8 @@ const Layout = ({ children }: LayoutProps) => {
) : (
-
-
+
+
Infisical Guide
@@ -369,12 +368,12 @@ const Layout = ({ children }: LayoutProps) => {
error={error}
loading={loading}
/>
-
{children}
+
{children}
-
-
-
+
+
+
{` ${t('common:no-mobile')} `}
diff --git a/frontend/src/components/basic/Listbox.tsx b/frontend/src/components/basic/Listbox.tsx
index a2f08d9b9..19bdd1dd5 100644
--- a/frontend/src/components/basic/Listbox.tsx
+++ b/frontend/src/components/basic/Listbox.tsx
@@ -58,7 +58,7 @@ const ListBox = ({
leaveFrom="opacity-100"
leaveTo="opacity-0"
>
-
+
{data.map((person, personIdx) => (
-
+
-
+
-
+
- This project will contain your environmental variables.
+ This project will contain your secrets and configs.
diff --git a/frontend/src/components/basic/dialog/DeleteEnvVar.tsx b/frontend/src/components/basic/dialog/DeleteEnvVar.tsx
index d455a792c..48aa93c11 100644
--- a/frontend/src/components/basic/dialog/DeleteEnvVar.tsx
+++ b/frontend/src/components/basic/dialog/DeleteEnvVar.tsx
@@ -15,7 +15,7 @@ export const DeleteEnvVar = ({ isOpen, onClose, onSubmit }: Props) => {
return (
- {}}>
+ {}}>
void;
- selectedIntegrationOption: IntegrationOption | null
- handleIntegrationOption: (arg:{
- integrationOption: IntegrationOption,
- accessToken?: string;
-})=>void;
-};
-
-const IntegrationAccessTokenDialog = ({
- isOpen,
- closeModal,
- selectedIntegrationOption,
- handleIntegrationOption
-}:Props) => {
- const [accessToken, setAccessToken] = useState('');
- // eslint-disable-next-line @typescript-eslint/no-unused-vars
- const submit = async () => {
- try {
- if (selectedIntegrationOption && accessToken !== '') {
- handleIntegrationOption({
- integrationOption: selectedIntegrationOption,
- accessToken
- });
- closeModal();
- setAccessToken('');
- }
- } catch (err) {
- console.log(err);
- }
- }
-
- return (
-
-
- {
- closeModal();
- }}>
-
-
-
-
-
-
-
-
- {`Enter your ${selectedIntegrationOption?.name} API Key`}
-
-
-
- {`This integration requires you to obtain an API key from ${selectedIntegrationOption?.name ?? ''} and store it with Infisical. `}
- You can learn how to do this here .
-
-
-
-
-
-
-
-
-
-
- );
-}
-
-export default IntegrationAccessTokenDialog;
\ No newline at end of file
diff --git a/frontend/src/components/basic/table/EnvironmentsTable.tsx b/frontend/src/components/basic/table/EnvironmentsTable.tsx
index df0d06ef5..f4e02a3bb 100644
--- a/frontend/src/components/basic/table/EnvironmentsTable.tsx
+++ b/frontend/src/components/basic/table/EnvironmentsTable.tsx
@@ -41,7 +41,6 @@ const EnvironmentTable = ({ data = [], onCreateEnv, onDeleteEnv, onUpdateEnv }:
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
-
const onEnvCreateCB = async (env: Env) => {
try {
await onCreateEnv(env);
@@ -71,25 +70,25 @@ const EnvironmentTable = ({ data = [], onCreateEnv, onDeleteEnv, onUpdateEnv }:
return (
<>
-
-
-
Project Environments
-
+
+
+
Project Environments
+
Choose which environments will show up in your dashboard like development, staging,
production
-
+
Note: the text in slugs shows how these environmant should be accessed in CLI.
{
- if (plan !== plans.starter || host !== "https://app.infisical.com") {
- handlePopUpOpen('createUpdateEnv')
+ if (plan !== plans.starter || host !== 'https://app.infisical.com') {
+ handlePopUpOpen('createUpdateEnv');
} else {
- handlePopUpOpen('upgradePlan')
+ handlePopUpOpen('upgradePlan');
}
}}
color="mineshaft"
@@ -98,32 +97,30 @@ const EnvironmentTable = ({ data = [], onCreateEnv, onDeleteEnv, onUpdateEnv }:
/>
-
-
-
+
+
+
- Name
- Slug
+ Name
+ Slug
{data?.length > 0 ? (
data.map(({ name, slug }) => (
-
-
- {name}
-
- {slug}
-
-
+
+ {name}
+ {slug}
+
+
{
- if (plan !== plans.starter || host !== "https://app.infisical.com") {
- handlePopUpOpen('createUpdateEnv', { name, slug })
+ if (plan !== plans.starter || host !== 'https://app.infisical.com') {
+ handlePopUpOpen('createUpdateEnv', { name, slug });
} else {
- handlePopUpOpen('upgradePlan')
+ handlePopUpOpen('upgradePlan');
}
}}
color="mineshaft"
@@ -131,13 +128,13 @@ const EnvironmentTable = ({ data = [], onCreateEnv, onDeleteEnv, onUpdateEnv }:
icon={faPencil}
/>
-
+
{
- if (plan !== plans.starter || host !== "https://app.infisical.com") {
- handlePopUpOpen('deleteEnv', { name, slug })
+ if (plan !== plans.starter || host !== 'https://app.infisical.com') {
+ handlePopUpOpen('deleteEnv', { name, slug });
} else {
- handlePopUpOpen('upgradePlan')
+ handlePopUpOpen('upgradePlan');
}
}}
color="red"
@@ -150,8 +147,8 @@ const EnvironmentTable = ({ data = [], onCreateEnv, onDeleteEnv, onUpdateEnv }:
))
) : (
-
- No environmants found
+
+ No environments found
)}
@@ -174,7 +171,7 @@ const EnvironmentTable = ({ data = [], onCreateEnv, onDeleteEnv, onUpdateEnv }:
onCreateSubmit={onEnvCreateCB}
onEditSubmit={onEnvUpdateCB}
/>
- handlePopUpClose('upgradePlan')}
text="You can add custom environments if you switch to Infisical's Team plan."
diff --git a/frontend/src/components/dashboard/AddTagsMenu.tsx b/frontend/src/components/dashboard/AddTagsMenu.tsx
new file mode 100644
index 000000000..b0f6b1a22
--- /dev/null
+++ b/frontend/src/components/dashboard/AddTagsMenu.tsx
@@ -0,0 +1,63 @@
+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 add more tags to a secret
+ * @param {object} obj
+ * @param {Tag[]} obj.allTags - all available tags for a vertain project
+ * @param {Tag[]} obj.currentTags - currently selected tags for a certain secret
+ * @param {function} obj.modifyTags - modify tags for a certain secret
+ * @param {Tag[]} obj.position - currently selected tags for a certain secret
+ */
+const AddTagsMenu = ({ allTags, currentTags, modifyTags, position }: { allTags: Tag[]; currentTags: Tag[]; modifyTags: (value: Tag[], position: number) => void; position: number; }) => {
+ const router = useRouter();
+ return (
+
+
+
+
+ {currentTags?.length > 2 && {currentTags.length - 2} }
+
+
+
+
+ {allTags?.map((tag) => { return (
+
+ currentTag.name).includes(tag.name) ? "opacity-30 cursor-default" : "hover:bg-mineshaft-700"} w-full text-left bg-mineshaft-800 px-2 py-0.5 text-bunker-200 rounded-sm flex items-center`}
+ onClick={() => {if (!currentTags?.map(currentTag => currentTag.name).includes(tag.name)) {modifyTags(currentTags.concat([tag]), position)}}}
+ >
+ {currentTags?.map(currentTag => currentTag.name).includes(tag.name) ? : } {tag.name}
+
+
+ )})}
+ router.push(`/settings/project/${String(router.query.id)}`)}
+ >
+ Add more tags
+
+
+
+
+ );
+};
+
+export default AddTagsMenu;
diff --git a/frontend/src/components/dashboard/CompareSecretsModal.tsx b/frontend/src/components/dashboard/CompareSecretsModal.tsx
new file mode 100644
index 000000000..b5d81bc04
--- /dev/null
+++ b/frontend/src/components/dashboard/CompareSecretsModal.tsx
@@ -0,0 +1,88 @@
+import { SetStateAction, useEffect, useState } from 'react';
+import Image from 'next/image';
+
+import { WorkspaceEnv } from '@app/hooks/api/types';
+
+import getSecretsForProject from '../utilities/secrets/getSecretsForProject';
+import { Modal, ModalContent } from '../v2';
+
+interface Secrets {
+ label: string;
+ secret: string;
+}
+
+interface CompareSecretsModalProps {
+ compareModal: boolean;
+ setCompareModal: React.Dispatch>;
+ selectedEnv: WorkspaceEnv;
+ workspaceEnvs: WorkspaceEnv[];
+ workspaceId: string;
+ currentSecret: {
+ key: string;
+ value: string;
+ };
+}
+
+const CompareSecretsModal = ({
+ compareModal,
+ setCompareModal,
+ selectedEnv,
+ workspaceEnvs,
+ workspaceId,
+ currentSecret
+}: CompareSecretsModalProps) => {
+ const [secrets, setSecrets] = useState([]);
+
+ const getEnvSecrets = async () => {
+ const workspaceEnvironments = workspaceEnvs?.filter((env) => env !== selectedEnv);
+ const newSecrets = await Promise.all(
+ workspaceEnvironments.map(async (env) => {
+ // #TODO: optimize this query somehow...
+ const allSecrets = await getSecretsForProject({ env: env.slug, workspaceId });
+ const secret =
+ allSecrets.find((item) => item.key === currentSecret.key)?.value ?? 'Not found';
+ return { label: env.name, secret };
+ })
+ );
+ setSecrets([{ label: selectedEnv.name, secret: currentSecret.value }, ...newSecrets]);
+ };
+
+ useEffect(() => {
+ if (compareModal) {
+ (async () => {
+ await getEnvSecrets();
+ })();
+ }
+ }, [compareModal]);
+
+ return (
+
+ e.preventDefault()}>
+
+ {secrets.length === 0 ? (
+
+
+
+ ) : (
+ secrets.map((item) => (
+
+ ))
+ )}
+
+
+
+ );
+};
+export default CompareSecretsModal;
diff --git a/frontend/src/components/dashboard/ConfirmEnvOverwriteModal.tsx b/frontend/src/components/dashboard/ConfirmEnvOverwriteModal.tsx
new file mode 100644
index 000000000..d3fad6511
--- /dev/null
+++ b/frontend/src/components/dashboard/ConfirmEnvOverwriteModal.tsx
@@ -0,0 +1,42 @@
+import { Button, Modal, ModalContent } from '../v2';
+
+type Props = {
+ isOpen: boolean;
+ onClose: () => void;
+ onOverwriteConfirm: (preserve: 'old' | 'new') => void;
+ duplicateKeys: string[];
+};
+
+const ConfirmEnvOverwriteModal = ({
+ isOpen,
+ onClose,
+ duplicateKeys,
+ onOverwriteConfirm
+}: Props): JSX.Element => {
+ return (
+
+
+ onOverwriteConfirm('old')}>
+ Keep Old
+
+ onOverwriteConfirm('new')}>
+ Overwrite
+
+
+ }
+ onClose={onClose}
+ >
+
+
Your file contains the following duplicate secrets:
+
{duplicateKeys.join(', ')}
+
Are you sure you want to overwrite these secrets?
+
+
+
+ );
+};
+
+export default ConfirmEnvOverwriteModal;
diff --git a/frontend/src/components/dashboard/DashboardInputField.tsx b/frontend/src/components/dashboard/DashboardInputField.tsx
index 0e292d65d..b02ecceca 100644
--- a/frontend/src/components/dashboard/DashboardInputField.tsx
+++ b/frontend/src/components/dashboard/DashboardInputField.tsx
@@ -1,8 +1,9 @@
import { memo, SyntheticEvent, useRef } from 'react';
-import { faCircle } from '@fortawesome/free-solid-svg-icons';
+import { faCircle, faExclamationCircle, faEye, faLayerGroup } from '@fortawesome/free-solid-svg-icons';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import guidGenerator from '../utilities/randomId';
+import { HoverObject } from '../v2/HoverCard';
const REGEX = /([$]{.*?})/g;
@@ -10,10 +11,13 @@ interface DashboardInputFieldProps {
position: number;
onChangeHandler: (value: string, position: number) => void;
value: string | undefined;
- type: 'varName' | 'value';
+ type: 'varName' | 'value' | 'comment';
blurred?: boolean;
isDuplicate?: boolean;
- override?: boolean;
+ isCapitalized?: boolean;
+ overrideEnabled?: boolean;
+ modifyValueOverride?: (value: string | undefined, position: number) => void;
+ isSideBarOpen?: boolean;
}
/**
@@ -26,6 +30,8 @@ interface DashboardInputFieldProps {
* @param {boolean} obj.blurred - whether the input field should be blurred (behind the gray dots) or not; this can be turned on/off in the dashboard
* @param {boolean} obj.isDuplicate - if the key name is duplicated
* @param {boolean} obj.override - whether a secret/row should be displalyed as overriden
+ *
+ *
* @returns
*/
@@ -36,7 +42,10 @@ const DashboardInputField = ({
value,
blurred,
isDuplicate,
- override
+ isCapitalized,
+ overrideEnabled,
+ modifyValueOverride,
+ isSideBarOpen
}: DashboardInputFieldProps) => {
const ref = useRef
(null);
const syncScroll = (e: SyntheticEvent) => {
@@ -51,41 +60,97 @@ const DashboardInputField = ({
const error = startsWithNumber || isDuplicate;
return (
-
+
onChangeHandler(e.target.value.toUpperCase(), position)}
+ onChange={(e) => onChangeHandler(isCapitalized ? e.target.value.toUpperCase() : e.target.value, position)}
type={type}
value={value}
- className={`z-10 peer font-mono ph-no-capture bg-bunker-800 rounded-md caret-white text-gray-400 text-md px-2 py-1.5 w-full min-w-16 outline-none focus:ring-2 ${
- error ? 'focus:ring-red/50' : 'focus:ring-primary/50'
+ className={`z-10 peer font-mono ph-no-capture bg-transparent h-full caret-bunker-200 text-sm px-2 w-full min-w-16 outline-none ${
+ error ? 'text-red-600 focus:text-red-500' : 'text-bunker-300 focus:text-bunker-100'
} duration-200`}
spellCheck="false"
/>
{startsWithNumber && (
-
- Should not start with a number
-
+
+
+
)}
- {isDuplicate && !startsWithNumber && (
-
- Secret names should be unique
-
+ {isDuplicate && value !== '' && !startsWithNumber && (
+
+
+
)}
+ {!error &&
+ {
+ if (modifyValueOverride) {
+ if (overrideEnabled === false) {
+ modifyValueOverride('', position);
+ } else {
+ modifyValueOverride(undefined, position);
+ }
+ }
+ }}>
+
+
+
}
+
+ );
+ }
+ if (type === 'comment') {
+ const startsWithNumber = !Number.isNaN(Number(value?.charAt(0))) && value !== '';
+ const error = startsWithNumber || isDuplicate;
+
+ return (
+
);
}
if (type === 'value') {
return (
-
- {override === true && (
-
+
+ {overrideEnabled === true && (
+
Override enabled
)}
@@ -95,19 +160,19 @@ const DashboardInputField = ({
onScroll={syncScroll}
className={`${
blurred
- ? 'text-transparent group-hover:text-transparent focus:text-transparent active:text-transparent'
+ ? 'text-transparent focus:text-transparent active:text-transparent'
: ''
- } z-10 peer font-mono ph-no-capture bg-transparent rounded-md caret-white text-transparent text-md px-2 py-1.5 w-full min-w-16 outline-none focus:ring-2 focus:ring-primary/50 duration-200 no-scrollbar no-scrollbar::-webkit-scrollbar`}
+ } z-10 peer font-mono ph-no-capture bg-transparent caret-white text-transparent text-sm px-2 py-2 w-full min-w-16 outline-none duration-200 no-scrollbar no-scrollbar::-webkit-scrollbar`}
spellCheck="false"
/>
{value?.split(REGEX).map((word, id) => {
if (word.match(REGEX) !== null) {
@@ -137,7 +202,9 @@ const DashboardInputField = ({
})}
{blurred && (
-
+
{value?.split('').map(() => (
))}
+ {value?.split('').length === 0 && EMPTY }
+
)}
@@ -163,8 +232,10 @@ function inputPropsAreEqual(prev: DashboardInputFieldProps, next: DashboardInput
prev.type === next.type &&
prev.position === next.position &&
prev.blurred === next.blurred &&
- prev.override === next.override &&
- prev.isDuplicate === next.isDuplicate
+ prev.isCapitalized === next.isCapitalized &&
+ prev.overrideEnabled === next.overrideEnabled &&
+ prev.isDuplicate === next.isDuplicate &&
+ prev.isSideBarOpen === next.isSideBarOpen
);
}
diff --git a/frontend/src/components/dashboard/DeleteActionButton.tsx b/frontend/src/components/dashboard/DeleteActionButton.tsx
index e4d56dd95..91819b43c 100644
--- a/frontend/src/components/dashboard/DeleteActionButton.tsx
+++ b/frontend/src/components/dashboard/DeleteActionButton.tsx
@@ -1,26 +1,42 @@
import React, { useState } from 'react'
import { useTranslation } from 'react-i18next';
+import { faXmark } from '@fortawesome/free-solid-svg-icons';
+import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import Button from '../basic/buttons/Button';
import { DeleteEnvVar } from '../basic/dialog/DeleteEnvVar';
type Props = {
- onSubmit: () => void
+ onSubmit: () => void;
+ isPlain?: boolean;
}
-export const DeleteActionButton = ({ onSubmit }: Props) => {
+export const DeleteActionButton = ({ onSubmit, isPlain }: Props) => {
const { t } = useTranslation();
const [open, setOpen] = useState(false)
return (
-
-
+ {isPlain
+ ? null}
+ role="button"
+ tabIndex={0}
+ onClick={() => setOpen(true)}
+ className="invisible group-hover:visible"
+ >
+
+
+ : setOpen(true)}
- />
+ />}
{
diff --git a/frontend/src/components/dashboard/DownloadSecretsMenu.tsx b/frontend/src/components/dashboard/DownloadSecretsMenu.tsx
index 19b300365..84c759756 100644
--- a/frontend/src/components/dashboard/DownloadSecretsMenu.tsx
+++ b/frontend/src/components/dashboard/DownloadSecretsMenu.tsx
@@ -31,7 +31,7 @@ const DownloadSecretMenu = ({ data, env }: { data: SecretDataProps[]; env: strin
leaveFrom="transform opacity-100 scale-100"
leaveTo="transform opacity-0 scale-95"
>
-
+
) : keysExist ? (
void;
modifyValue: (value: string, position: number) => void;
- modifyValueOverride: (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;
sidebarSecretId: string;
isSnapshot: boolean;
+ isCapitalized: boolean;
+ deleteRow?: (props: DeleteRowFunctionProps) => void;
+ tags: Tag[];
+ togglePITSidebar?: (value: boolean) => void;
}
+export interface DeleteRowFunctionProps {
+ ids: string[];
+ secretName: string;
+}
+
+const colors = [
+ 'bg-[#f1c40f]/40',
+ 'bg-[#cb1c8d]/40',
+ 'bg-[#badc58]/40',
+ 'bg-[#ff5400]/40',
+ 'bg-[#00bbf9]/40'
+]
+
+
+const colorsText = [
+ 'text-[#fcf0c3]/70',
+ 'text-[#f2c6e3]/70',
+ 'text-[#eef6d5]/70',
+ 'text-[#ffddcc]/70',
+ 'text-[#f0fffd]/70'
+]
+
/**
* This component represent a single row for an environemnt variable on the dashboard
* @param {object} obj
@@ -23,11 +53,16 @@ interface KeyPairProps {
* @param {function} obj.modifyKey - modify the key of a certain environment variable
* @param {function} obj.modifyValue - modify the value of a certain environment variable
* @param {function} obj.modifyValueOverride - modify the value of a certain environment variable if it is overriden
+ * @param {function} obj.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 = ({
@@ -35,66 +70,110 @@ const KeyPair = ({
modifyKey,
modifyValue,
modifyValueOverride,
+ modifyComment,
+ modifyTags,
isBlurred,
isDuplicate,
toggleSidebar,
sidebarSecretId,
- isSnapshot
-}: KeyPairProps) => (
+ isCapitalized,
+ isSnapshot,
+ deleteRow,
+ togglePITSidebar,
+ tags
+}: KeyPairProps) => {
+ const tagData = (tags.map((tag, index) => {return {
+ ...tag,
+ color: colors[index%colors.length],
+ colorText: colorsText[index%colorsText.length]
+ }}));
+
+ return (
-
- {keyPair.valueOverride && (
-
-
-
- This secret is overriden
-
-
- )}
-
-
+
+
-
+
- {!isSnapshot && (
-
null}
- role="button"
- tabIndex={0}
- onClick={() => toggleSidebar(keyPair.id)}
- className="cursor-pointer w-[2.35rem] h-[2.35rem] bg-mineshaft-700 hover:bg-chicago-700 rounded-md flex flex-row justify-center items-center duration-200"
- >
-
+
+
+
+ {keyPair.tags?.map((tag, index) => (
+ index < 2 &&
tagDp._id === tag._id)[0]?.color} rounded-sm text-sm ${tagData.filter(tagDp => tagDp._id === tag._id)[0]?.colorText} flex items-center`}>
+ {tag.name}
+ modifyTags(keyPair.tags.filter(ttag => ttag._id !== tag._id), keyPair.pos)}/>
+
+ ))}
+
+
+
+
+
null}
+ role="button"
+ tabIndex={0}
+ onClick={() => {
+ if (togglePITSidebar) {
+ togglePITSidebar(false);
+ }
+ toggleSidebar(keyPair.id)
+ }}
+ className={`cursor-pointer w-[1.5rem] h-[2.35rem] ml-auto group-hover:bg-mineshaft-700 z-50 rounded-md invisible group-hover:visible flex flex-row justify-center items-center ${isSnapshot ?? 'invisible'}`}
+ >
+
+
+
+ { if (deleteRow) {
+ deleteRow({ ids: [keyPair.id], secretName: keyPair?.key })
+ }}}
+ isPlain
+ />
+
-);
+)};
export default KeyPair;
diff --git a/frontend/src/components/dashboard/SideBar.tsx b/frontend/src/components/dashboard/SideBar.tsx
index 1a3f99a29..1d0996376 100644
--- a/frontend/src/components/dashboard/SideBar.tsx
+++ b/frontend/src/components/dashboard/SideBar.tsx
@@ -2,14 +2,16 @@
import { useState } from 'react';
import Image from 'next/image';
import { useTranslation } from 'next-i18next';
-import { faX } from '@fortawesome/free-solid-svg-icons';
+import { faXmark } from '@fortawesome/free-solid-svg-icons';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import SecretVersionList from '@app/ee/components/SecretVersionList';
+import { WorkspaceEnv } from '@app/hooks/api/types';
import Button from '../basic/buttons/Button';
import Toggle from '../basic/Toggle';
import CommentField from './CommentField';
+import CompareSecretsModal from './CompareSecretsModal';
import DashboardInputField from './DashboardInputField';
import { DeleteActionButton } from './DeleteActionButton';
import GenerateSecretMenu from './GenerateSecretMenu';
@@ -40,6 +42,9 @@ interface SideBarProps {
sharedToHide: string[];
setSharedToHide: (values: string[]) => void;
deleteRow: (props: DeleteRowFunctionProps) => void;
+ workspaceEnvs: WorkspaceEnv[];
+ selectedEnv: WorkspaceEnv;
+ workspaceId: string;
}
/**
@@ -63,15 +68,19 @@ const SideBar = ({
modifyComment,
buttonReady,
savePush,
- deleteRow
+ deleteRow,
+ workspaceEnvs,
+ selectedEnv,
+ workspaceId
}: SideBarProps) => {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const [isLoading, setIsLoading] = useState(false);
- const [overrideEnabled, setOverrideEnabled] = useState(data[0].valueOverride !== undefined);
+ const [overrideEnabled, setOverrideEnabled] = useState(data[0]?.valueOverride !== undefined);
+ const [compareModal, setCompareModal] = useState(false);
const { t } = useTranslation();
return (
-
+
{isLoading ? (
toggleSidebar('None')}
>
-
+
{t('dashboard:sidebar.key')}
-
+
+
+
{(data[0]?.value || data[0]?.value === "") ? (
{t('dashboard:sidebar.value')}
-
+
+
+
@@ -150,14 +163,16 @@ const SideBar = ({
!overrideEnabled && 'opacity-40 pointer-events-none'
} duration-200`}
>
-
+
+
+
@@ -171,20 +186,38 @@ const SideBar = ({
/>
)}
-
-
-
- deleteRow({ ids: data.map((secret) => secret.id), secretName: data[0]?.key })
- }
- />
+
+
+ setCompareModal(true)}
+ />
+
+
+
+
+
+ deleteRow({ ids: data.map((secret) => secret.id), secretName: data[0]?.key })
+ }
+ />
+
);
diff --git a/frontend/src/components/integrations/Integration.tsx b/frontend/src/components/integrations/Integration.tsx
index e177f462c..0d2371b31 100644
--- a/frontend/src/components/integrations/Integration.tsx
+++ b/frontend/src/components/integrations/Integration.tsx
@@ -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
(
environments.find(({ slug }) => slug === integration?.environment) || {
@@ -71,7 +72,14 @@ const IntegrationTile = ({
});
setApps(tempApps);
- setIntegrationApp(integration?.app ? integration.app : tempApps[0].name);
+
+ if (integration?.app) {
+ setIntegrationApp(integration.app);
+ } else if (tempApps.length > 0) {
+ setIntegrationApp(tempApps[0].name)
+ } else {
+ setIntegrationApp('');
+ }
switch (integration.integration) {
case 'vercel':
@@ -174,7 +182,7 @@ const IntegrationTile = ({
return
;
};
- if (!integrationApp || apps.length === 0) return
;
+ if (!integrationApp) return
;
return (
@@ -199,9 +207,10 @@ const IntegrationTile = ({
-
INTEGRATION
-
- {integration.integration.charAt(0).toUpperCase() + integration.integration.slice(1)}
+
INTEGRATION
+
+ {/* {integration.integration.charAt(0).toUpperCase() + integration.integration.slice(1)} */}
+ {integrationSlugNameMapping[integration.integration]}
diff --git a/frontend/src/components/navigation/NavBarDashboard.tsx b/frontend/src/components/navigation/NavBarDashboard.tsx
index d681930ac..81db93cda 100644
--- a/frontend/src/components/navigation/NavBarDashboard.tsx
+++ b/frontend/src/components/navigation/NavBarDashboard.tsx
@@ -93,7 +93,7 @@ export default function Navbar() {
};
return (
-
+
diff --git a/frontend/src/components/navigation/NavHeader.tsx b/frontend/src/components/navigation/NavHeader.tsx
index 247d4ac3d..bc3876750 100644
--- a/frontend/src/components/navigation/NavHeader.tsx
+++ b/frontend/src/components/navigation/NavHeader.tsx
@@ -3,8 +3,8 @@ import { useRouter } from 'next/router';
import { faAngleRight } from '@fortawesome/free-solid-svg-icons';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
+import { useWorkspace } from '@app/context';
import getOrganization from '@app/pages/api/organization/GetOrg';
-import getProjectInfo from '@app/pages/api/workspace/getProjectInfo';
/**
* This is the component at the top of almost every page.
@@ -23,9 +23,9 @@ export default function NavHeader({
isProjectRelated?: boolean;
}): JSX.Element {
const [orgName, setOrgName] = useState('');
- const [workspaceName, setWorkspaceName] = useState('');
const router = useRouter();
const projectId = String(router.query.id);
+ const { currentWorkspace } = useWorkspace();
useEffect(() => {
(async () => {
@@ -34,29 +34,24 @@ export default function NavHeader({
orgId: orgId || ''
});
setOrgName(org.name);
-
- const workspace = await getProjectInfo({
- projectId
- });
- setWorkspaceName(workspace.name);
})();
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [projectId]);
return (
-
-
+
+
{orgName?.charAt(0)}
-
{orgName}
+
{orgName}
{isProjectRelated && (
<>
-
-
{workspaceName}
+
+
{currentWorkspace?.name}
>
)}
-
-
{pageName}
+
+
{pageName}
);
}
diff --git a/frontend/src/components/utilities/attemptLogin.ts b/frontend/src/components/utilities/attemptLogin.ts
index acc2b67fe..e1a95381e 100644
--- a/frontend/src/components/utilities/attemptLogin.ts
+++ b/frontend/src/components/utilities/attemptLogin.ts
@@ -137,8 +137,9 @@ const attemptLogin = async (
// eslint-disable-next-line no-template-curly-in-string
value: 'mongodb+srv://${DB_USERNAME}:${DB_PASSWORD}@mongodb.net',
valueOverride: undefined,
- comment: 'This is an example of secret referencing.',
- id: ''
+ comment: 'Secret referencing example',
+ id: '',
+ tags: []
},
{
pos: 1,
@@ -146,8 +147,9 @@ const attemptLogin = async (
value: 'OVERRIDE_THIS',
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: ''
+ 'Override secrets with personal value',
+ id: '',
+ tags: []
},
{
pos: 2,
@@ -155,8 +157,9 @@ const attemptLogin = async (
value: 'OVERRIDE_THIS',
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: ''
+ 'Another secret override',
+ id: '',
+ tags: []
},
{
pos: 3,
@@ -164,7 +167,8 @@ const attemptLogin = async (
value: 'user1234',
valueOverride: 'user1234',
comment: '',
- id: ''
+ id: '',
+ tags: []
},
{
pos: 4,
@@ -172,7 +176,8 @@ const attemptLogin = async (
value: 'example_password',
valueOverride: 'example_password',
comment: '',
- id: ''
+ id: '',
+ tags: []
},
{
pos: 5,
@@ -180,7 +185,8 @@ const attemptLogin = async (
value: 'example_twillio_token',
valueOverride: undefined,
comment: '',
- id: ''
+ id: '',
+ tags: []
},
{
pos: 6,
@@ -188,7 +194,8 @@ const attemptLogin = async (
value: 'http://localhost:3000',
valueOverride: undefined,
comment: '',
- id: ''
+ id: '',
+ tags: []
}
];
const secrets = await encryptSecrets({
diff --git a/frontend/src/components/utilities/secrets/encryptSecrets.ts b/frontend/src/components/utilities/secrets/encryptSecrets.ts
index 53ce0066d..11d9e8d22 100644
--- a/frontend/src/components/utilities/secrets/encryptSecrets.ts
+++ b/frontend/src/components/utilities/secrets/encryptSecrets.ts
@@ -1,6 +1,6 @@
import crypto from 'crypto';
-import { SecretDataProps } from 'public/data/frequentInterfaces';
+import { SecretDataProps, Tag } from 'public/data/frequentInterfaces';
import getLatestFileKey from '@app/pages/api/workspace/getLatestFileKey';
@@ -20,6 +20,7 @@ interface EncryptedSecretProps {
secretValueIV: string;
secretValueTag: string;
type: 'personal' | 'shared';
+ tags: Tag[];
}
/**
@@ -105,7 +106,8 @@ const encryptSecrets = async ({
type:
secret.valueOverride === undefined || secret?.value !== secret?.valueOverride
? 'shared'
- : 'personal'
+ : 'personal',
+ tags: secret.tags
};
return result;
diff --git a/frontend/src/components/utilities/secrets/getSecretsForProject.ts b/frontend/src/components/utilities/secrets/getSecretsForProject.ts
index c00f13d04..28491a3e4 100644
--- a/frontend/src/components/utilities/secrets/getSecretsForProject.ts
+++ b/frontend/src/components/utilities/secrets/getSecretsForProject.ts
@@ -1,3 +1,5 @@
+import { Tag } from 'public/data/frequentInterfaces';
+
import getSecrets from '@app/pages/api/files/GetSecrets';
import getLatestFileKey from '@app/pages/api/workspace/getLatestFileKey';
@@ -17,6 +19,7 @@ interface EncryptedSecretProps {
secretValueIV: string;
secretValueTag: string;
type: 'personal' | 'shared';
+ tags: Tag[];
}
interface SecretProps {
@@ -25,12 +28,13 @@ interface SecretProps {
type: 'personal' | 'shared';
comment: string;
id: string;
+ tags: Tag[];
}
interface FunctionProps {
env: string;
- setIsKeyAvailable: any;
- setData: any;
+ setIsKeyAvailable?: any;
+ setData?: any;
workspaceId: string;
}
@@ -58,7 +62,9 @@ const getSecretsForProject = async ({
const latestKey = await getLatestFileKey({ workspaceId });
// This is called isKeyAvailable but what it really means is if a person is able to create new key pairs
- setIsKeyAvailable(!latestKey ? encryptedSecrets.length === 0 : true);
+ if (typeof setIsKeyAvailable === 'function') {
+ setIsKeyAvailable(!latestKey ? encryptedSecrets.length === 0 : true);
+ }
const PRIVATE_KEY = localStorage.getItem('PRIVATE_KEY') as string;
@@ -105,7 +111,8 @@ const getSecretsForProject = async ({
key: plainTextKey,
value: plainTextValue,
type: secret.type,
- comment: plainTextComment
+ comment: plainTextComment,
+ tags: secret.tags
});
});
}
@@ -128,10 +135,16 @@ 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
}));
- setData(result);
+ if (typeof setData === 'function') {
+ setData(result);
+ }
+
return result;
} catch (error) {
console.log('Something went wrong during accessing or decripting secrets.');
diff --git a/frontend/src/components/v2/Button/Button.tsx b/frontend/src/components/v2/Button/Button.tsx
index 21548d0e3..df12c25f7 100644
--- a/frontend/src/components/v2/Button/Button.tsx
+++ b/frontend/src/components/v2/Button/Button.tsx
@@ -18,14 +18,15 @@ const buttonVariants = cva(
'font-inter font-medium',
'cursor-pointer',
'inline-flex items-center justify-center',
- 'relative'
+ 'relative',
+ 'whitespace-nowrap'
],
{
variants: {
colorSchema: {
primary: ['bg-primary', 'text-black', 'border-primary hover:bg-opacity-80'],
secondary: ['bg-mineshaft', 'text-gray-300', 'border-mineshaft hover:bg-opacity-80'],
- danger: ['bg-red', 'text-white', 'border-red']
+ danger: ['bg-red', 'text-white', 'border-red hover:bg-opacity-90']
},
variant: {
solid: '',
@@ -33,7 +34,7 @@ const buttonVariants = cva(
plain: ''
},
isDisabled: {
- true: 'bg-opacity-70 cursor-not-allowed',
+ true: 'bg-mineshaft opacity-40 cursor-not-allowed',
false: ''
},
isFullWidth: {
@@ -45,9 +46,9 @@ const buttonVariants = cva(
false: ''
},
size: {
- xs: ['text-xs', 'py-1', 'px-2'],
- sm: ['text-sm', 'py-2', 'px-4'],
- md: ['text-md', 'py-2', 'px-6'],
+ xs: ['text-xs', 'py-1', 'px-1'],
+ sm: ['text-sm', 'py-2', 'px-2'],
+ md: ['text-md', 'py-2', 'px-4'],
lg: ['text-lg', 'py-2', 'px-8']
}
},
@@ -75,7 +76,7 @@ const buttonVariants = cva(
{
colorSchema: 'secondary',
variant: 'plain',
- className: 'text-mineshaft'
+ className: 'text-mineshaft-300'
},
{
colorSchema: 'danger',
@@ -101,7 +102,7 @@ export const Button = forwardRef
(
children,
isDisabled = false,
className = '',
- size = 'md',
+ size = 'sm',
variant = 'solid',
isFullWidth,
isRounded = true,
@@ -144,9 +145,10 @@ export const Button = forwardRef(
)}
{leftIcon}
@@ -154,9 +156,9 @@ export const Button = forwardRef(
{children}
{rightIcon}
diff --git a/frontend/src/components/v2/Card/Card.tsx b/frontend/src/components/v2/Card/Card.tsx
index 9e658aaf7..1f8448a03 100644
--- a/frontend/src/components/v2/Card/Card.tsx
+++ b/frontend/src/components/v2/Card/Card.tsx
@@ -8,7 +8,7 @@ export type CardTitleProps = {
};
export const CardTitle = ({ children, className, subTitle }: CardTitleProps) => (
-
+
{children}
{subTitle &&
{subTitle}
}
@@ -29,7 +29,7 @@ export type CardBodyProps = {
};
export const CardBody = ({ children, className }: CardBodyProps) => (
-
{children}
+
{children}
);
export type CardProps = {
@@ -47,7 +47,7 @@ export const Card = forwardRef
(
void;
+ onChange?: (isOpen: boolean) => void;
+ deleteKey: string;
+ title: string;
+ onDeleteApproved: () => Promise;
+};
+
+export const DeleteActionModal = ({
+ isOpen,
+ onClose,
+ onChange,
+ deleteKey,
+ onDeleteApproved,
+ title
+}: Props): JSX.Element => {
+ const [inputData, setInputData] = useState('');
+ const [isLoading, setIsLoading] = useToggle();
+
+ useEffect(() => {
+ setInputData('');
+ }, [isOpen]);
+
+ const onDelete = async () => {
+ setIsLoading.on();
+ try {
+ await onDeleteApproved();
+ } catch {
+ setIsLoading.off();
+ } finally {
+ setIsLoading.off();
+ }
+ };
+
+ return (
+ {
+ setInputData('');
+ if (onChange) onChange(isOpenState);
+ }}
+ >
+
+
+ Delete
+
+
+ Cancel
+
+
+ }
+ onClose={onClose}
+ >
+
+ }
+ className="mb-4"
+ >
+ setInputData(e.target.value)} />
+
+
+
+
+ );
+};
diff --git a/frontend/src/components/v2/DeleteActionModal/index.tsx b/frontend/src/components/v2/DeleteActionModal/index.tsx
new file mode 100644
index 000000000..b69763984
--- /dev/null
+++ b/frontend/src/components/v2/DeleteActionModal/index.tsx
@@ -0,0 +1 @@
+export { DeleteActionModal } from './DeleteActionModal';
diff --git a/frontend/src/components/v2/FormControl/FormControl.tsx b/frontend/src/components/v2/FormControl/FormControl.tsx
index 14e58d5be..5a545acec 100644
--- a/frontend/src/components/v2/FormControl/FormControl.tsx
+++ b/frontend/src/components/v2/FormControl/FormControl.tsx
@@ -11,7 +11,7 @@ export type FormLabelProps = {
};
export const FormLabel = ({ id, label, isRequired }: FormLabelProps) => (
-
+
{label}
{isRequired && * }
@@ -25,7 +25,7 @@ export type FormHelperTextProps = {
export const FormHelperText = ({ isError, text }: FormHelperTextProps) => (
@@ -46,6 +46,7 @@ export type FormControlProps = {
helperText?: ReactNode;
errorText?: ReactNode;
children: JSX.Element;
+ className?: string;
};
export const FormControl = ({
@@ -55,10 +56,11 @@ export const FormControl = ({
helperText,
errorText,
id,
- isError
+ isError,
+ className
}: FormControlProps): JSX.Element => {
return (
-
+
{typeof label === 'string' ? (
) : (
diff --git a/frontend/src/components/v2/HoverCard/HoverCard.tsx b/frontend/src/components/v2/HoverCard/HoverCard.tsx
new file mode 100644
index 000000000..703e7dbd0
--- /dev/null
+++ b/frontend/src/components/v2/HoverCard/HoverCard.tsx
@@ -0,0 +1,42 @@
+import { IconProp } from '@fortawesome/fontawesome-svg-core';
+import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
+import * as HoverCard from '@radix-ui/react-hover-card';
+
+type Props = {
+ text: string;
+ icon: IconProp;
+ color: string;
+};
+
+export type HoverCardProps = Props;
+
+export const HoverObject = ({
+ text,
+ icon,
+ color
+ }: Props): JSX.Element => (
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+);
+
+HoverObject.displayName = 'HoverCard';
diff --git a/frontend/src/components/v2/HoverCard/index.tsx b/frontend/src/components/v2/HoverCard/index.tsx
new file mode 100644
index 000000000..fec7b1538
--- /dev/null
+++ b/frontend/src/components/v2/HoverCard/index.tsx
@@ -0,0 +1,2 @@
+export type { HoverCardProps } from './HoverCard';
+export { HoverObject } from './HoverCard';
diff --git a/frontend/src/components/v2/IconButton/IconButton.tsx b/frontend/src/components/v2/IconButton/IconButton.tsx
index b1a1f4813..98e81b089 100644
--- a/frontend/src/components/v2/IconButton/IconButton.tsx
+++ b/frontend/src/components/v2/IconButton/IconButton.tsx
@@ -14,7 +14,7 @@ const iconButtonVariants = cva(
[
'button',
'transition-all',
- 'font-inter font-medium',
+ 'font-inter font-medium user-select-none',
'cursor-pointer',
'inline-flex items-center justify-center',
'relative'
@@ -22,9 +22,9 @@ const iconButtonVariants = cva(
{
variants: {
colorSchema: {
- primary: ['bg-primary', 'text-black', 'border-primary hover:bg-opacity-80'],
- secondary: ['bg-mineshaft', 'text-gray-300', 'border-mineshaft hover:bg-opacity-80'],
- danger: ['bg-red', 'text-white', 'border-red']
+ primary: ['bg-primary', 'text-black', 'border-primary hover:opacity-80'],
+ secondary: ['bg-mineshaft', 'text-gray-300', 'border-mineshaft hover:bg-bunker-400'],
+ danger: ['bg-[#973939] hover:bg-red', 'text-white', 'border-red']
},
variant: {
solid: '',
@@ -40,10 +40,10 @@ const iconButtonVariants = cva(
false: ''
},
size: {
- xs: ['text-xs', 'py-1', 'px-2'],
- sm: ['text-sm', 'py-2', 'px-3'],
- md: ['text-md', 'py-3', 'px-4'],
- lg: ['text-lg', 'py-3', 'px-6']
+ xs: ['text-xs', 'py-2', 'px-2'],
+ sm: ['text-sm', 'py-3', 'px-3'],
+ md: ['text-md', 'py-4', 'px-4'],
+ lg: ['text-lg', 'py-6', 'px-6']
}
},
compoundVariants: [
@@ -80,7 +80,7 @@ const iconButtonVariants = cva(
{
colorSchema: ['danger', 'primary', 'secondary'],
variant: ['plain'],
- className: 'bg-transparent py-1 px-1'
+ className: 'bg-transparent py-1 px-1 text-bunker-300'
}
]
}
@@ -97,7 +97,7 @@ export const IconButton = forwardRef
(
ariaLabel,
isDisabled = false,
className,
- size = 'md',
+ size = 'sm',
variant = 'solid',
isRounded = true,
colorSchema = 'primary',
diff --git a/frontend/src/components/v2/Input/Input.tsx b/frontend/src/components/v2/Input/Input.tsx
index 4f502e31d..28524c72d 100644
--- a/frontend/src/components/v2/Input/Input.tsx
+++ b/frontend/src/components/v2/Input/Input.tsx
@@ -8,10 +8,11 @@ type Props = {
isRequired?: boolean;
leftIcon?: ReactNode;
rightIcon?: ReactNode;
+ isDisabled?: boolean;
};
const inputVariants = cva(
- 'input w-full py-2 text-gray-400 placeholder-gray-500 placeholder-opacity-50',
+ 'input w-full py-2 text-gray-400 placeholder-gray-500 placeholder-opacity-50 outline-none focus:ring-2',
{
variants: {
size: {
@@ -46,7 +47,7 @@ const inputParentContainerVariants = cva('inline-flex font-inter items-center bo
},
isError: {
true: 'border-red',
- false: 'border-mineshaft-400'
+ false: 'border-mineshaft-500'
},
isFullWidth: {
true: 'w-full',
@@ -70,6 +71,7 @@ export const Input = forwardRef(
className,
isRounded = true,
isFullWidth = true,
+ isDisabled,
isError = false,
isRequired,
leftIcon,
@@ -87,9 +89,10 @@ export const Input = forwardRef(
{...props}
required={isRequired}
ref={ref}
+ disabled={isDisabled}
className={twMerge(
- leftIcon ? 'pl-9' : 'pl-4',
- rightIcon ? 'pr-9' : 'pr-4',
+ leftIcon ? 'pl-9' : 'pl-2.5',
+ rightIcon ? 'pr-9' : 'pr-2.5',
inputVariants({ className, isError, size, isRounded, variant })
)}
/>
diff --git a/frontend/src/components/v2/Modal/Modal.tsx b/frontend/src/components/v2/Modal/Modal.tsx
index c7d42541d..efc6ba3a3 100644
--- a/frontend/src/components/v2/Modal/Modal.tsx
+++ b/frontend/src/components/v2/Modal/Modal.tsx
@@ -11,33 +11,34 @@ export type ModalContentProps = DialogPrimitive.DialogContentProps & {
title?: ReactNode;
subTitle?: string;
footerContent?: ReactNode;
+ onClose?: () => void;
};
export const ModalContent = forwardRef(
- ({ children, title, subTitle, className, footerContent, ...props }, forwardedRef) => (
+ ({ children, title, subTitle, className, footerContent, onClose, ...props }, forwardedRef) => (
{title && {title} }
{children}
{footerContent && {footerContent} }
-
+
-
+
@@ -55,3 +56,6 @@ export const Modal = ({ isOpen, ...props }: ModalProps) => (
export const ModalTrigger = DialogPrimitive.Trigger;
export type ModalTriggerProps = DialogPrimitive.DialogTriggerProps;
+
+export const ModalClose = DialogPrimitive.Close;
+export type ModalCloseProps = DialogPrimitive.DialogCloseProps;
diff --git a/frontend/src/components/v2/Modal/index.tsx b/frontend/src/components/v2/Modal/index.tsx
index c97a564fb..7517b3ec1 100644
--- a/frontend/src/components/v2/Modal/index.tsx
+++ b/frontend/src/components/v2/Modal/index.tsx
@@ -1,2 +1,2 @@
-export type { ModalContentProps, ModalProps, ModalTriggerProps } from './Modal';
-export { Modal, ModalContent, ModalTrigger } from './Modal';
+export type { ModalCloseProps, ModalContentProps, ModalProps, ModalTriggerProps } from './Modal';
+export { Modal, ModalClose, ModalContent, ModalTrigger } from './Modal';
diff --git a/frontend/src/components/v2/Select/Select.tsx b/frontend/src/components/v2/Select/Select.tsx
index c093b8b70..86ccc1079 100644
--- a/frontend/src/components/v2/Select/Select.tsx
+++ b/frontend/src/components/v2/Select/Select.tsx
@@ -10,34 +10,40 @@ type Props = {
children: ReactNode;
placeholder?: string;
className?: string;
+ dropdownContainerClassName?: string;
isLoading?: boolean;
};
export type SelectProps = SelectPrimitive.SelectProps & Props;
export const Select = forwardRef(
- ({ children, placeholder, className, isLoading, ...props }, ref): JSX.Element => {
+ (
+ { children, placeholder, className, isLoading, dropdownContainerClassName, ...props },
+ ref
+ ): JSX.Element => {
return (
- {!props.disabled &&
-
- }
+ {!props.disabled && (
+
+
+
+ )}
@@ -75,11 +81,11 @@ export const SelectItem = forwardRef(
(
(
@@ -47,7 +47,7 @@ export type THeadProps = {
};
export const THead = ({ children, className }: THeadProps): JSX.Element => (
-
+
{children}
);
@@ -56,20 +56,22 @@ export const THead = ({ children, className }: THeadProps): JSX.Element => (
export type TrProps = {
children: ReactNode;
className?: string;
-};
+} & HTMLAttributes;
-export const Tr = ({ children, className }: TrProps): JSX.Element => (
- {children}
+export const Tr = ({ children, className, ...props }: TrProps): JSX.Element => (
+
+ {children}
+
);
// table head columns
export type ThProps = {
- children: ReactNode;
+ children?: ReactNode;
className?: string;
};
export const Th = ({ children, className }: ThProps): JSX.Element => (
- {children}
+ {children}
);
// table body
@@ -84,10 +86,12 @@ export const TBody = ({ children, className }: TBodyProps): JSX.Element => (
// table body columns
export type TdProps = {
- children: ReactNode;
+ children?: ReactNode;
className?: string;
-};
+} & TdHTMLAttributes;
-export const Td = ({ children, className }: TdProps): JSX.Element => (
- {children}
+export const Td = ({ children, className, ...props }: TdProps): JSX.Element => (
+
+ {children}
+
);
diff --git a/frontend/src/components/v2/UpgradePlanModal/UpgradePlanModal.tsx b/frontend/src/components/v2/UpgradePlanModal/UpgradePlanModal.tsx
new file mode 100644
index 000000000..e991ca3a8
--- /dev/null
+++ b/frontend/src/components/v2/UpgradePlanModal/UpgradePlanModal.tsx
@@ -0,0 +1,36 @@
+import Link from 'next/link';
+
+import { Button } from '../Button';
+import { Modal, ModalClose, ModalContent } from '../Modal';
+
+type Props = {
+ isOpen?: boolean;
+ onOpenChange?: (isOpen: boolean) => void;
+ text: string;
+};
+
+export const UpgradePlanModal = ({ text, isOpen, onOpenChange }: Props): JSX.Element => (
+
+
+ Upgrade Plan
+ ,
+
+
+ Cancel
+
+
+ ]}
+ >
+ {text}
+
+ Upgrade and get access to this, as well as to other powerful enhancements.
+
+
+
+);
diff --git a/frontend/src/components/v2/UpgradePlanModal/index.tsx b/frontend/src/components/v2/UpgradePlanModal/index.tsx
new file mode 100644
index 000000000..e4649c2df
--- /dev/null
+++ b/frontend/src/components/v2/UpgradePlanModal/index.tsx
@@ -0,0 +1 @@
+export { UpgradePlanModal } from './UpgradePlanModal';
diff --git a/frontend/src/components/v2/index.tsx b/frontend/src/components/v2/index.tsx
index 437114047..0f89a3a2b 100644
--- a/frontend/src/components/v2/index.tsx
+++ b/frontend/src/components/v2/index.tsx
@@ -1,6 +1,7 @@
export * from './Button';
export * from './Card';
export * from './Checkbox';
+export * from './DeleteActionModal';
export * from './Dropdown';
export * from './FormControl';
export * from './IconButton';
@@ -12,3 +13,4 @@ export * from './Spinner';
export * from './Switch';
export * from './Table';
export * from './TextArea';
+export * from './UpgradePlanModal';
diff --git a/frontend/src/const.ts b/frontend/src/const.ts
index 6ec3941a4..4583d1dbb 100644
--- a/frontend/src/const.ts
+++ b/frontend/src/const.ts
@@ -16,11 +16,31 @@ export const publicPaths = [
`/terms`,
`/subprocessors`,
`/verify-email`,
- `/password-reset`,
+ `/password-reset`
];
export const languageMap = {
- en: "English",
- ko: "한국어",
- fr: "Français",
+ en: 'English',
+ ko: '한국어',
+ fr: 'Français'
};
+
+interface Mapping {
+ [key: string]: string;
+}
+
+const plansDev: Mapping = {
+ starter: 'prod_Mb4ATFT5QAHoPM',
+ team: 'prod_NEpD2WMXUS2eDn',
+ professional: 'prod_Mb4CetZ2jE7jdl',
+ enterprise: 'licence_key_required'
+};
+
+const plansProd: Mapping = {
+ starter: 'prod_Mb8oR5XNwyFTul',
+ team: 'prod_NEp7fAB3UJWK6A',
+ professional: 'prod_Mb8pUIpA0OUi5N',
+ enterprise: 'licence_key_required'
+};
+
+export const plans = plansProd || plansDev;
diff --git a/frontend/src/context/SubscriptionContext/SubscriptionContext.tsx b/frontend/src/context/SubscriptionContext/SubscriptionContext.tsx
new file mode 100644
index 000000000..ae9910224
--- /dev/null
+++ b/frontend/src/context/SubscriptionContext/SubscriptionContext.tsx
@@ -0,0 +1,47 @@
+import { createContext, ReactNode, useContext, useMemo } from 'react';
+
+import { useGetOrgSubscription } from '@app/hooks/api';
+import { GetSubscriptionPlan } from '@app/hooks/api/types';
+
+import { useWorkspace } from '../WorkspaceContext';
+// import { Subscription } from '@app/hooks/api/workspace/types';
+
+type TSubscriptionContext = {
+ subscription?: GetSubscriptionPlan;
+ subscriptionPlan: string;
+ isLoading: boolean;
+};
+
+const SubscriptionContext = createContext(null);
+
+type Props = {
+ children: ReactNode;
+};
+
+export const SubscriptionProvider = ({ children }: Props): JSX.Element => {
+ const { currentWorkspace } = useWorkspace();
+ const { data, isLoading } = useGetOrgSubscription({
+ orgID: currentWorkspace?.organization || ''
+ });
+
+ // memorize the workspace details for the context
+ const value = useMemo(
+ () => ({
+ subscription: data,
+ subscriptionPlan: data?.data?.[0]?.plan?.product || '',
+ isLoading
+ }),
+ [data, isLoading]
+ );
+
+ return {children} ;
+};
+
+export const useSubscription = () => {
+ const ctx = useContext(SubscriptionContext);
+ if (!ctx) {
+ throw new Error('useSubscription has to be used within ');
+ }
+
+ return ctx;
+};
diff --git a/frontend/src/context/SubscriptionContext/index.tsx b/frontend/src/context/SubscriptionContext/index.tsx
new file mode 100644
index 000000000..9a2b07b15
--- /dev/null
+++ b/frontend/src/context/SubscriptionContext/index.tsx
@@ -0,0 +1 @@
+export { SubscriptionProvider, useSubscription } from './SubscriptionContext';
diff --git a/frontend/src/context/WorkspaceContext/WorkspaceContext.tsx b/frontend/src/context/WorkspaceContext/WorkspaceContext.tsx
new file mode 100644
index 000000000..4edef8e6a
--- /dev/null
+++ b/frontend/src/context/WorkspaceContext/WorkspaceContext.tsx
@@ -0,0 +1,43 @@
+import { createContext, ReactNode, useContext, useMemo } from 'react';
+import { useRouter } from 'next/router';
+
+import { useGetUserWorkspaces } from '@app/hooks/api';
+import { Workspace } from '@app/hooks/api/workspace/types';
+
+type TWorkspaceContext = {
+ workspaces: Workspace[];
+ currentWorkspace?: Workspace;
+ isLoading: boolean;
+};
+
+const WorkspaceContext = createContext(null);
+
+type Props = {
+ children: ReactNode;
+};
+
+export const WorkspaceProvider = ({ children }: Props): JSX.Element => {
+ const { data: ws, isLoading } = useGetUserWorkspaces();
+ const router = useRouter();
+ const workspaceId = router.query.id;
+
+ // memorize the workspace details for the context
+ const value = useMemo(() => {
+ return {
+ workspaces: ws || [],
+ currentWorkspace: (ws || []).find(({ _id: id }) => id === workspaceId),
+ isLoading
+ };
+ }, [ws, workspaceId, isLoading]);
+
+ return {children} ;
+};
+
+export const useWorkspace = () => {
+ const ctx = useContext(WorkspaceContext);
+ if (!ctx) {
+ throw new Error('useWorkspace has to be used within ');
+ }
+
+ return ctx;
+};
diff --git a/frontend/src/context/WorkspaceContext/index.tsx b/frontend/src/context/WorkspaceContext/index.tsx
new file mode 100644
index 000000000..3496038c3
--- /dev/null
+++ b/frontend/src/context/WorkspaceContext/index.tsx
@@ -0,0 +1 @@
+export { useWorkspace, WorkspaceProvider } from './WorkspaceContext';
diff --git a/frontend/src/context/index.tsx b/frontend/src/context/index.tsx
new file mode 100644
index 000000000..1bf59f9ac
--- /dev/null
+++ b/frontend/src/context/index.tsx
@@ -0,0 +1,2 @@
+export { SubscriptionProvider, useSubscription } from './SubscriptionContext';
+export { useWorkspace, WorkspaceProvider } from './WorkspaceContext';
diff --git a/frontend/src/ee/components/PITRecoverySidebar.tsx b/frontend/src/ee/components/PITRecoverySidebar.tsx
index d9cc47a7e..204973228 100644
--- a/frontend/src/ee/components/PITRecoverySidebar.tsx
+++ b/frontend/src/ee/components/PITRecoverySidebar.tsx
@@ -3,8 +3,9 @@ import { useEffect, useState } from 'react';
import Image from 'next/image';
import { useRouter } from 'next/router';
import { useTranslation } from 'next-i18next';
-import { faX } from '@fortawesome/free-solid-svg-icons';
+import { faXmark } from '@fortawesome/free-solid-svg-icons';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
+import { Tag } from 'public/data/frequentInterfaces';
import Button from '@app/components/basic/buttons/Button';
import {
@@ -48,6 +49,7 @@ interface EncrypetedSecretVersionListProps {
secretKeyTag: string;
environment: string;
type: 'personal' | 'shared';
+ tags: Tag[];
}
/**
@@ -106,6 +108,7 @@ const PITRecoverySidebar = ({ toggleSidebar, setSnapshotData, chosenSnapshot }:
pos,
type: encryptedSecretVersion.type,
environment: encryptedSecretVersion.environment,
+ tags: encryptedSecretVersion.tags,
key: decryptSymmetric({
ciphertext: encryptedSecretVersion.secretKeyCiphertext,
iv: encryptedSecretVersion.secretKeyIV,
@@ -134,6 +137,9 @@ const PITRecoverySidebar = ({ toggleSidebar, setSnapshotData, chosenSnapshot }:
environment: decryptedSecretVersions.filter(
(secret: SecretDataProps) => secret.key === key && secret.type === 'shared'
)[0].environment,
+ tags: decryptedSecretVersions.filter(
+ (secret: SecretDataProps) => secret.key === key && secret.type === 'shared'
+ )[0].tags,
value: decryptedSecretVersions.filter(
(secret: SecretDataProps) => secret.key === key && secret.type === 'shared'
)[0]?.value,
@@ -155,7 +161,7 @@ const PITRecoverySidebar = ({ toggleSidebar, setSnapshotData, chosenSnapshot }:
{isLoading ? (
@@ -177,7 +183,7 @@ const PITRecoverySidebar = ({ toggleSidebar, setSnapshotData, chosenSnapshot }:
className="p-1"
onClick={() => toggleSidebar(false)}
>
-
+
diff --git a/frontend/src/ee/components/SecretVersionList.tsx b/frontend/src/ee/components/SecretVersionList.tsx
index 4df9c38a7..fd4a3916c 100644
--- a/frontend/src/ee/components/SecretVersionList.tsx
+++ b/frontend/src/ee/components/SecretVersionList.tsx
@@ -89,7 +89,7 @@ const SecretVersionList = ({ secretId }: { secretId: string }) => {
/>
) : (
-
+
{secretVersions ? (
secretVersions
?.sort((a, b) => b.createdAt.localeCompare(a.createdAt))
diff --git a/frontend/src/hooks/api/index.tsx b/frontend/src/hooks/api/index.tsx
index 9df70dcf0..263f7e152 100644
--- a/frontend/src/hooks/api/index.tsx
+++ b/frontend/src/hooks/api/index.tsx
@@ -1 +1,6 @@
-export { useGetAuthToken } from './auth';
+export * from './auth';
+export * from './keys';
+export * from './serviceTokens';
+export * from './subscriptions';
+export * from './tags';
+export * from './workspace';
diff --git a/frontend/src/hooks/api/integrationAuth/index.tsx b/frontend/src/hooks/api/integrationAuth/index.tsx
new file mode 100644
index 000000000..43920b65f
--- /dev/null
+++ b/frontend/src/hooks/api/integrationAuth/index.tsx
@@ -0,0 +1,3 @@
+export {
+ useGetIntegrationAuthApps,
+ useGetIntegrationAuthById} from './queries';
\ No newline at end of file
diff --git a/frontend/src/hooks/api/integrationAuth/queries.tsx b/frontend/src/hooks/api/integrationAuth/queries.tsx
new file mode 100644
index 000000000..508afc741
--- /dev/null
+++ b/frontend/src/hooks/api/integrationAuth/queries.tsx
@@ -0,0 +1,38 @@
+import { useQuery } from "@tanstack/react-query";
+
+import { apiRequest } from "@app/config/request";
+
+import {
+ App,
+ IntegrationAuth} from './types';
+
+const integrationAuthKeys = {
+ getIntegrationAuthById: (integrationAuthId: string) => [{ integrationAuthId }, 'integrationAuth'] as const,
+ getIntegrationAuthApps: (integrationAuthId: string) => [{ integrationAuthId }, 'integrationAuthApps'] as const,
+}
+
+const fetchIntegrationAuthById = async (integrationAuthId: string) => {
+ const { data } = await apiRequest.get<{ integrationAuth: IntegrationAuth }>(`/api/v1/integration-auth/${integrationAuthId}`);
+ return data.integrationAuth;
+}
+
+const fetchIntegrationAuthApps = async (integrationAuthId: string) => {
+ const { data } = await apiRequest.get<{ apps: App[] }>(`/api/v1/integration-auth/${integrationAuthId}/apps`);
+ return data.apps;
+}
+
+export const useGetIntegrationAuthById = (integrationAuthId: string) => {
+ return useQuery({
+ queryKey: integrationAuthKeys.getIntegrationAuthById(integrationAuthId),
+ queryFn: () => fetchIntegrationAuthById(integrationAuthId),
+ enabled: true
+ });
+}
+
+export const useGetIntegrationAuthApps = (integrationAuthId: string) => {
+ return useQuery({
+ queryKey: integrationAuthKeys.getIntegrationAuthApps(integrationAuthId),
+ queryFn: () => fetchIntegrationAuthApps(integrationAuthId),
+ enabled: true
+ });
+}
\ No newline at end of file
diff --git a/frontend/src/hooks/api/integrationAuth/types.ts b/frontend/src/hooks/api/integrationAuth/types.ts
new file mode 100644
index 000000000..bf193eafe
--- /dev/null
+++ b/frontend/src/hooks/api/integrationAuth/types.ts
@@ -0,0 +1,13 @@
+export type IntegrationAuth = {
+ _id: string;
+ workspace: string;
+ integration: string;
+ teamId?: string;
+ accountId?: string;
+}
+
+export type App = {
+ name: string;
+ appId?: string;
+ owner?: string;
+}
\ No newline at end of file
diff --git a/frontend/src/hooks/api/keys/index.tsx b/frontend/src/hooks/api/keys/index.tsx
new file mode 100644
index 000000000..23ffdf8d1
--- /dev/null
+++ b/frontend/src/hooks/api/keys/index.tsx
@@ -0,0 +1 @@
+export { useGetUserWsKey } from './queries';
diff --git a/frontend/src/hooks/api/keys/queries.tsx b/frontend/src/hooks/api/keys/queries.tsx
new file mode 100644
index 000000000..d37c73f13
--- /dev/null
+++ b/frontend/src/hooks/api/keys/queries.tsx
@@ -0,0 +1,24 @@
+import { useQuery } from '@tanstack/react-query';
+
+import { apiRequest } from '@app/config/request';
+
+import { UserWsKeyPair } from './types';
+
+const encKeyKeys = {
+ getUserWorkspaceKey: (workspaceID: string) => ['worksapce-key-pair', { workspaceID }] as const
+};
+
+const fetchUserWsKey = async (workspaceID: string) => {
+ const { data } = await apiRequest.get<{ latestKey: UserWsKeyPair }>(
+ `/api/v1/key/${workspaceID}/latest`
+ );
+
+ return data.latestKey;
+};
+
+export const useGetUserWsKey = (workspaceID: string) =>
+ useQuery({
+ queryKey: encKeyKeys.getUserWorkspaceKey(workspaceID),
+ queryFn: () => fetchUserWsKey(workspaceID),
+ enabled: Boolean(workspaceID)
+ });
diff --git a/frontend/src/hooks/api/keys/types.ts b/frontend/src/hooks/api/keys/types.ts
new file mode 100644
index 000000000..bad61b5dd
--- /dev/null
+++ b/frontend/src/hooks/api/keys/types.ts
@@ -0,0 +1,22 @@
+export type UserWsKeyPair = {
+ _id: string;
+ encryptedKey: string;
+ nonce: string;
+ sender: Sender;
+ receiver: string;
+ workspace: string;
+ createdAt: string;
+ updatedAt: string;
+ __v: number;
+};
+
+export type Sender = {
+ _id: string;
+ email: string;
+ createdAt: string;
+ updatedAt: string;
+ __v: number;
+ firstName: string;
+ lastName: string;
+ publicKey: string;
+};
diff --git a/frontend/src/hooks/api/serviceTokens/index.ts b/frontend/src/hooks/api/serviceTokens/index.ts
new file mode 100644
index 000000000..548ad313a
--- /dev/null
+++ b/frontend/src/hooks/api/serviceTokens/index.ts
@@ -0,0 +1 @@
+export { useCreateServiceToken, useDeleteServiceToken, useGetUserWsServiceTokens } from './queries';
diff --git a/frontend/src/hooks/api/serviceTokens/queries.tsx b/frontend/src/hooks/api/serviceTokens/queries.tsx
new file mode 100644
index 000000000..70e0736d3
--- /dev/null
+++ b/frontend/src/hooks/api/serviceTokens/queries.tsx
@@ -0,0 +1,60 @@
+import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
+
+import { apiRequest } from '@app/config/request';
+
+import {
+ CreateServiceTokenDTO,
+ CreateServiceTokenRes,
+ DeleteServiceTokenRes,
+ ServiceToken
+} from './types';
+
+const serviceTokenKeys = {
+ getAllWorkspaceServiceToken: (workspaceID: string) => [{ workspaceID }, 'service-tokens'] as const
+};
+
+const fetchWorkspaceServiceTokens = async (workspaceID: string) => {
+ const { data } = await apiRequest.get<{ serviceTokenData: ServiceToken[] }>(
+ `/api/v2/workspace/${workspaceID}/service-token-data`
+ );
+
+ return data.serviceTokenData;
+};
+
+type UseGetWorkspaceServiceTokensProps = { workspaceID: string };
+
+export const useGetUserWsServiceTokens = ({ workspaceID }: UseGetWorkspaceServiceTokensProps) =>
+ useQuery({
+ queryKey: serviceTokenKeys.getAllWorkspaceServiceToken(workspaceID),
+ queryFn: () => fetchWorkspaceServiceTokens(workspaceID),
+ enabled: Boolean(workspaceID)
+ });
+
+// mutation
+export const useCreateServiceToken = () => {
+ const queryClient = useQueryClient();
+
+ return useMutation
({
+ mutationFn: async (body) => {
+ const { data } = await apiRequest.post('/api/v2/service-token/', body);
+ return data;
+ },
+ onSuccess: ({ serviceTokenData: { workspace } }) => {
+ queryClient.invalidateQueries(serviceTokenKeys.getAllWorkspaceServiceToken(workspace));
+ }
+ });
+};
+
+export const useDeleteServiceToken = () => {
+ const queryClient = useQueryClient();
+
+ return useMutation({
+ mutationFn: async (serviceTokenId) => {
+ const { data } = await apiRequest.delete(`/api/v2/service-token/${serviceTokenId}`);
+ return data;
+ },
+ onSuccess: ({ serviceTokenData: { workspace } }) => {
+ queryClient.invalidateQueries(serviceTokenKeys.getAllWorkspaceServiceToken(workspace));
+ }
+ });
+};
diff --git a/frontend/src/hooks/api/serviceTokens/types.ts b/frontend/src/hooks/api/serviceTokens/types.ts
new file mode 100644
index 000000000..a1eb73743
--- /dev/null
+++ b/frontend/src/hooks/api/serviceTokens/types.ts
@@ -0,0 +1,28 @@
+export type ServiceToken = {
+ _id: string;
+ name: string;
+ workspace: string;
+ environment: string;
+ user: string;
+ expiresAt: string;
+ createdAt: string;
+ updatedAt: string;
+ __v: number;
+};
+
+export type CreateServiceTokenDTO = {
+ name: string;
+ workspaceId: string;
+ environment: string;
+ expiresIn: number;
+ encryptedKey: string;
+ iv: string;
+ tag: string;
+};
+
+export type CreateServiceTokenRes = {
+ serviceToken: string;
+ serviceTokenData: ServiceToken;
+};
+
+export type DeleteServiceTokenRes = { serviceTokenData: ServiceToken };
diff --git a/frontend/src/hooks/api/subscriptions/index.tsx b/frontend/src/hooks/api/subscriptions/index.tsx
new file mode 100644
index 000000000..4bb98e85a
--- /dev/null
+++ b/frontend/src/hooks/api/subscriptions/index.tsx
@@ -0,0 +1 @@
+export { useGetOrgSubscription } from './queries';
diff --git a/frontend/src/hooks/api/subscriptions/queries.tsx b/frontend/src/hooks/api/subscriptions/queries.tsx
new file mode 100644
index 000000000..4a65912ef
--- /dev/null
+++ b/frontend/src/hooks/api/subscriptions/queries.tsx
@@ -0,0 +1,30 @@
+import { useQuery } from '@tanstack/react-query';
+
+import { apiRequest } from '@app/config/request';
+
+import { GetSubscriptionPlan } from './types';
+
+// import { Workspace } from './types';
+
+const subscriptionKeys = {
+ getOrgSubsription: (orgID: string) => ['subscription', { orgID }] as const
+};
+
+const fetchOrgSubscription = async (orgID: string) => {
+ const { data } = await apiRequest.get<{ subscriptions: GetSubscriptionPlan }>(
+ `/api/v1/organization/${orgID}/subscriptions`
+ );
+
+ return data.subscriptions;
+};
+
+type UseGetOrgSubscriptionProps = {
+ orgID: string;
+};
+
+export const useGetOrgSubscription = ({ orgID }: UseGetOrgSubscriptionProps) =>
+ useQuery({
+ queryKey: subscriptionKeys.getOrgSubsription(orgID),
+ queryFn: () => fetchOrgSubscription(orgID),
+ enabled: Boolean(orgID)
+ });
diff --git a/frontend/src/hooks/api/subscriptions/types.ts b/frontend/src/hooks/api/subscriptions/types.ts
new file mode 100644
index 000000000..f9dfcbf0b
--- /dev/null
+++ b/frontend/src/hooks/api/subscriptions/types.ts
@@ -0,0 +1,25 @@
+export type GetSubscriptionPlan = {
+ data: { plan: SubscriptionPlan }[];
+};
+
+export type SubscriptionPlan = {
+ id: string;
+ object: string;
+ active: boolean;
+ aggregate_usage: unknown;
+ amount: 1400;
+ amount_decimal: 1400;
+ billing_scheme: string;
+ created: 1674833546;
+ currency: string;
+ interval: string;
+ interval_count: 1;
+ livemode: false;
+ metadata: {};
+ nickname: null;
+ product: string;
+ tiers_mode: unknown;
+ transform_usage: unknown;
+ trial_period_days: unknown;
+ usage_type: string;
+};
diff --git a/frontend/src/hooks/api/tags/index.tsx b/frontend/src/hooks/api/tags/index.tsx
new file mode 100644
index 000000000..c37b75a2c
--- /dev/null
+++ b/frontend/src/hooks/api/tags/index.tsx
@@ -0,0 +1,3 @@
+export { useGetWsTags } from './queries';
+export { useCreateWsTag } from './queries';
+export { useDeleteWsTag } from './queries';
diff --git a/frontend/src/hooks/api/tags/queries.tsx b/frontend/src/hooks/api/tags/queries.tsx
new file mode 100644
index 000000000..17bd8092a
--- /dev/null
+++ b/frontend/src/hooks/api/tags/queries.tsx
@@ -0,0 +1,62 @@
+import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
+
+import { apiRequest } from '@app/config/request';
+
+import {
+ CreateTagDTO,
+ CreateTagRes,
+ DeleteTagDTO,
+ DeleteWsTagRes,
+ UserWsTags
+} from './types';
+
+
+const workspaceTags = {
+ getWsTags: (workspaceID: string) => ['workspace-tags', { workspaceID }] as const
+};
+
+const fetchWsTag = async (workspaceID: string) => {
+ const { data } = await apiRequest.get<{ workspaceTags: UserWsTags }>(
+ `/api/v2/workspace/${workspaceID}/tags`
+ );
+
+ return data.workspaceTags;
+};
+
+export const useGetWsTags = (workspaceID: string) =>
+ useQuery({
+ queryKey: workspaceTags.getWsTags(workspaceID),
+ queryFn: () => fetchWsTag(workspaceID),
+ enabled: Boolean(workspaceID)
+ });
+
+export const useCreateWsTag = () => {
+ const queryClient = useQueryClient();
+
+ return useMutation({
+ mutationFn: async ({ workspaceID, tagName, tagSlug }) => {
+ const { data } = await apiRequest.post(`/api/v2/workspace/${workspaceID}/tags`, {
+ name: tagName,
+ slug: tagSlug
+ })
+ return data;
+ },
+ onSuccess: (tagData) => {
+ queryClient.invalidateQueries(workspaceTags.getWsTags(tagData?.workspace));
+ }
+ });
+};
+
+export const useDeleteWsTag = () => {
+ const queryClient = useQueryClient();
+
+ return useMutation({
+ mutationFn: async ({ tagID }) => {
+ const { data } = await apiRequest.delete(`/api/v2/workspace/tags/${tagID}`);
+ return data
+ },
+ onSuccess: (tagData) => {
+ queryClient.invalidateQueries(workspaceTags.getWsTags(tagData?.workspace));
+ }
+ });
+};
diff --git a/frontend/src/hooks/api/tags/types.ts b/frontend/src/hooks/api/tags/types.ts
new file mode 100644
index 000000000..56b7171bf
--- /dev/null
+++ b/frontend/src/hooks/api/tags/types.ts
@@ -0,0 +1,39 @@
+export type UserWsTags = WsTag[];
+
+export type WsTag = {
+ _id: string;
+ name: string;
+ slug: string;
+ workspace: string;
+ createdAt: string;
+ updatedAt: string;
+ __v: number;
+}
+
+export type WorkspaceTag = { _id: string; name: string; slug: string };
+
+export type CreateTagDTO = {
+ workspaceID: string;
+ tagSlug: string;
+ tagName: string;
+};
+
+export type CreateTagRes = {
+ name: string;
+ slug: string;
+ workspace: string;
+ createdAt: string;
+ user: string;
+ _id: string;
+};
+
+export type DeleteTagDTO = { tagID: string; };
+
+export type DeleteWsTagRes = {
+ name: string;
+ slug: string;
+ workspace: string;
+ createdAt: string;
+ user: string;
+ _id: string;
+};
\ No newline at end of file
diff --git a/frontend/src/hooks/api/types.ts b/frontend/src/hooks/api/types.ts
new file mode 100644
index 000000000..a0475e24e
--- /dev/null
+++ b/frontend/src/hooks/api/types.ts
@@ -0,0 +1,14 @@
+export type { GetAuthTokenAPI } from './auth/types';
+export type { UserWsKeyPair } from './keys/types';
+export type { CreateServiceTokenDTO, ServiceToken } from './serviceTokens/types';
+export type { GetSubscriptionPlan, SubscriptionPlan } from './subscriptions/types';
+export type {
+ CreateEnvironmentDTO,
+ DeleteEnvironmentDTO,
+ DeleteWorkspaceDTO,
+ RenameWorkspaceDTO,
+ UpdateEnvironmentDTO,
+ Workspace,
+ WorkspaceEnv,
+ WorkspaceTag
+} from './workspace/types';
diff --git a/frontend/src/hooks/api/workspace/index.tsx b/frontend/src/hooks/api/workspace/index.tsx
new file mode 100644
index 000000000..51f8c38eb
--- /dev/null
+++ b/frontend/src/hooks/api/workspace/index.tsx
@@ -0,0 +1,10 @@
+export {
+ useCreateWsEnvironment,
+ useDeleteWorkspace,
+ useDeleteWsEnvironment,
+ useGetUserWorkspaces,
+ useGetWorkspaceById,
+ useRenameWorkspace,
+ useToggleAutoCapitalization,
+ useUpdateWsEnvironment} from './queries';
+
\ No newline at end of file
diff --git a/frontend/src/hooks/api/workspace/queries.tsx b/frontend/src/hooks/api/workspace/queries.tsx
new file mode 100644
index 000000000..bae1db874
--- /dev/null
+++ b/frontend/src/hooks/api/workspace/queries.tsx
@@ -0,0 +1,121 @@
+import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
+
+import { apiRequest } from '@app/config/request';
+
+import {
+ CreateEnvironmentDTO,
+ DeleteEnvironmentDTO,
+ DeleteWorkspaceDTO,
+ RenameWorkspaceDTO,
+ ToggleAutoCapitalizationDTO,
+ UpdateEnvironmentDTO,
+ 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);
+
+// mutation
+export const useRenameWorkspace = () => {
+ const queryClient = useQueryClient();
+
+ return useMutation<{}, {}, RenameWorkspaceDTO>({
+ mutationFn: ({ workspaceID, newWorkspaceName }) =>
+ apiRequest.post(`/api/v1/workspace/${workspaceID}/name`, { name: newWorkspaceName }),
+ onSuccess: () => {
+ queryClient.invalidateQueries(workspaceKeys.getAllUserWorkspace);
+ }
+ });
+};
+
+export const useToggleAutoCapitalization = () => {
+ const queryClient = useQueryClient();
+
+ return useMutation<{}, {}, ToggleAutoCapitalizationDTO>({
+ mutationFn: ({ workspaceID, state }) =>
+ apiRequest.patch(`/api/v2/workspace/${workspaceID}/auto-capitalization`, { autoCapitalization: state }),
+ onSuccess: () => {
+ queryClient.invalidateQueries(workspaceKeys.getAllUserWorkspace);
+ }
+ });
+};
+
+export const useDeleteWorkspace = () => {
+ const queryClient = useQueryClient();
+
+ return useMutation<{}, {}, DeleteWorkspaceDTO>({
+ mutationFn: ({ workspaceID }) => apiRequest.delete(`/api/v1/workspace/${workspaceID}`),
+ onSuccess: () => {
+ queryClient.invalidateQueries(workspaceKeys.getAllUserWorkspace);
+ }
+ });
+};
+
+export const useCreateWsEnvironment = () => {
+ const queryClient = useQueryClient();
+
+ return useMutation<{}, {}, CreateEnvironmentDTO>({
+ mutationFn: ({ workspaceID, environmentName, environmentSlug }) =>
+ apiRequest.post(`/api/v2/workspace/${workspaceID}/environments`, {
+ environmentName,
+ environmentSlug
+ }),
+ onSuccess: () => {
+ queryClient.invalidateQueries(workspaceKeys.getAllUserWorkspace);
+ }
+ });
+};
+
+export const useUpdateWsEnvironment = () => {
+ const queryClient = useQueryClient();
+
+ return useMutation<{}, {}, UpdateEnvironmentDTO>({
+ mutationFn: ({ workspaceID, environmentName, environmentSlug, oldEnvironmentSlug }) =>
+ apiRequest.put(`/api/v2/workspace/${workspaceID}/environments`, {
+ environmentName,
+ environmentSlug,
+ oldEnvironmentSlug
+ }),
+ onSuccess: () => {
+ queryClient.invalidateQueries(workspaceKeys.getAllUserWorkspace);
+ }
+ });
+};
+
+export const useDeleteWsEnvironment = () => {
+ const queryClient = useQueryClient();
+
+ return useMutation<{}, {}, DeleteEnvironmentDTO>({
+ mutationFn: ({ workspaceID, environmentSlug }) =>
+ apiRequest.delete(`/api/v2/workspace/${workspaceID}/environments`, {
+ data: { environmentSlug }
+ }),
+ onSuccess: () => {
+ queryClient.invalidateQueries(workspaceKeys.getAllUserWorkspace);
+ }
+ });
+};
diff --git a/frontend/src/hooks/api/workspace/types.ts b/frontend/src/hooks/api/workspace/types.ts
new file mode 100644
index 000000000..312a811d9
--- /dev/null
+++ b/frontend/src/hooks/api/workspace/types.ts
@@ -0,0 +1,32 @@
+export type Workspace = {
+ __v: number;
+ _id: string;
+ name: string;
+ organization: string;
+ autoCapitalization: boolean;
+ environments: WorkspaceEnv[];
+};
+
+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 };
+export type ToggleAutoCapitalizationDTO = { workspaceID: string; state: boolean };
+
+export type DeleteWorkspaceDTO = { workspaceID: string };
+
+export type CreateEnvironmentDTO = {
+ workspaceID: string;
+ environmentSlug: string;
+ environmentName: string;
+};
+
+export type UpdateEnvironmentDTO = {
+ workspaceID: string;
+ oldEnvironmentSlug: string;
+ environmentSlug: string;
+ environmentName: string;
+};
+
+export type DeleteEnvironmentDTO = { workspaceID: string; environmentSlug: string };
diff --git a/frontend/src/hooks/usePopUp.tsx b/frontend/src/hooks/usePopUp.tsx
index b0c4163be..52839142d 100644
--- a/frontend/src/hooks/usePopUp.tsx
+++ b/frontend/src/hooks/usePopUp.tsx
@@ -21,7 +21,7 @@ interface UsePopUpReturn | UsePopUpProps[]> {
popUp: UsePopUpState;
handlePopUpOpen: (popUpName: keyof UsePopUpState, data?: unknown) => void;
handlePopUpClose: (popUpName: keyof UsePopUpState) => void;
- handlePopUpToggle: (popUpName: keyof UsePopUpState) => void;
+ handlePopUpToggle: (popUpName: keyof UsePopUpState, state?: boolean) => void;
}
/**
@@ -50,10 +50,10 @@ export const usePopUp = | UsePopUpProps[]>(
setPopUp((oldState) => ({ ...oldState, [popUpName]: { isOpen: false } }));
}, []);
- const handlePopUpToggle = useCallback((popUpName: keyof UsePopUpState) => {
+ const handlePopUpToggle = useCallback((popUpName: keyof UsePopUpState, state?: boolean) => {
setPopUp((oldState) => ({
...oldState,
- [popUpName]: { isOpen: !oldState[popUpName].isOpen }
+ [popUpName]: { isOpen: typeof state === 'undefined' ? !oldState[popUpName].isOpen : state }
}));
}, []);
diff --git a/frontend/src/pages/_app.tsx b/frontend/src/pages/_app.tsx
index 5bfaaa1e3..0d5809093 100644
--- a/frontend/src/pages/_app.tsx
+++ b/frontend/src/pages/_app.tsx
@@ -10,7 +10,9 @@ import Layout from '@app/components/basic/Layout';
import NotificationProvider from '@app/components/context/Notifications/NotificationProvider';
import Telemetry from '@app/components/utilities/telemetry/Telemetry';
import { publicPaths } from '@app/const';
+import { SubscriptionProvider } from '@app/context';
import { AuthProvider } from '@app/context/AuthContext';
+import { WorkspaceProvider } from '@app/context/WorkspaceContext';
import { queryClient } from '@app/reactQuery';
import '@fortawesome/fontawesome-svg-core/styles.css';
@@ -68,11 +70,15 @@ const App = ({ Component, pageProps, ...appProps }: NextAppProp): JSX.Element =>
return (
-
-
-
-
-
+
+
+
+
+
+
+
+
+
);
diff --git a/frontend/src/pages/activity/[id].tsx b/frontend/src/pages/activity/[id].tsx
index 55e5b1924..181efb4b2 100644
--- a/frontend/src/pages/activity/[id].tsx
+++ b/frontend/src/pages/activity/[id].tsx
@@ -124,7 +124,7 @@ export default function Activity() {
};
return (
-
+
{currentSidebarAction && (
diff --git a/frontend/src/pages/api/integrations/authorizeIntegration.ts b/frontend/src/pages/api/integrations/authorizeIntegration.ts
index 94f1d54f5..9c70796b7 100644
--- a/frontend/src/pages/api/integrations/authorizeIntegration.ts
+++ b/frontend/src/pages/api/integrations/authorizeIntegration.ts
@@ -26,7 +26,7 @@ const AuthorizeIntegration = ({ workspaceId, code, integration }: Props) =>
})
}).then(async (res) => {
if (res && res.status === 200) {
- return res;
+ return (await res.json()).integrationAuth;
}
console.log('Failed to authorize the integration');
return undefined;
diff --git a/frontend/src/pages/api/integrations/createIntegration.ts b/frontend/src/pages/api/integrations/createIntegration.ts
index 6b37f1281..6b83d3fdc 100644
--- a/frontend/src/pages/api/integrations/createIntegration.ts
+++ b/frontend/src/pages/api/integrations/createIntegration.ts
@@ -1,7 +1,13 @@
import SecurityClient from '@app/components/utilities/SecurityClient';
interface Props {
- integrationAuthId: string;
+ integrationAuthId: string;
+ isActive: boolean;
+ app: string | null;
+ appId: string | null;
+ sourceEnvironment: string;
+ targetEnvironment: string | null;
+ owner: string | null;
}
/**
* This route creates a new integration based on the integration authorization with id [integrationAuthId]
@@ -10,7 +16,13 @@ interface Props {
* @returns
*/
const createIntegration = ({
- integrationAuthId
+ integrationAuthId,
+ isActive,
+ app,
+ appId,
+ sourceEnvironment,
+ targetEnvironment,
+ owner
}: Props) =>
SecurityClient.fetchCall('/api/v1/integration', {
method: 'POST',
@@ -18,7 +30,13 @@ const createIntegration = ({
'Content-Type': 'application/json'
},
body: JSON.stringify({
- integrationAuthId
+ integrationAuthId,
+ isActive,
+ app,
+ appId,
+ sourceEnvironment,
+ targetEnvironment,
+ owner
})
}).then(async (res) => {
if (res && res.status === 200) {
diff --git a/frontend/src/pages/api/workspace/getWorkspaceTags.ts b/frontend/src/pages/api/workspace/getWorkspaceTags.ts
new file mode 100644
index 000000000..9a27a8f90
--- /dev/null
+++ b/frontend/src/pages/api/workspace/getWorkspaceTags.ts
@@ -0,0 +1,22 @@
+import SecurityClient from '@app/components/utilities/SecurityClient';
+
+/**
+ * This route lets us get the tags for a certain project
+ * @param {string} workspaceId
+ * @returns
+ */
+const getWorkspaceTags = ({ workspaceId }: { workspaceId: string }) =>
+ SecurityClient.fetchCall(`/api/v2/workspace/${workspaceId}/tags`, {
+ method: 'GET',
+ headers: {
+ 'Content-Type': 'application/json'
+ }
+ }).then(async (res) => {
+ if (res?.status === 200) {
+ return (await res.json()).workspaceTags;
+ }
+ console.log('Failed to get the tags available in a certain project');
+ return undefined;
+ });
+
+export default getWorkspaceTags;
diff --git a/frontend/src/pages/api/workspace/getWorkspaces.ts b/frontend/src/pages/api/workspace/getWorkspaces.ts
index 95c977008..168f6df62 100644
--- a/frontend/src/pages/api/workspace/getWorkspaces.ts
+++ b/frontend/src/pages/api/workspace/getWorkspaces.ts
@@ -4,6 +4,7 @@ interface Workspace {
__v: number;
_id: string;
name: string;
+ autoCapitalization: boolean;
organization: string;
environments: Array<{ name: string; slug: string }>;
}
diff --git a/frontend/src/pages/dashboard/[id].tsx b/frontend/src/pages/dashboard/[id].tsx
index b199790a9..8951004ce 100644
--- a/frontend/src/pages/dashboard/[id].tsx
+++ b/frontend/src/pages/dashboard/[id].tsx
@@ -5,23 +5,26 @@ import Image from 'next/image';
import { useRouter } from 'next/router';
import { useTranslation } from 'next-i18next';
import {
- faArrowDownAZ,
- faArrowDownZA,
+ faArrowDown,
faArrowLeft,
+ faArrowUp,
faCheck,
faClockRotateLeft,
faEye,
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';
import BottonRightPopup from '@app/components/basic/popups/BottomRightPopup';
import { useNotificationContext } from '@app/components/context/Notifications/NotificationProvider';
+import ConfirmEnvOverwriteModal from '@app/components/dashboard/ConfirmEnvOverwriteModal';
import DownloadSecretMenu from '@app/components/dashboard/DownloadSecretsMenu';
import DropZone from '@app/components/dashboard/DropZone';
import KeyPair from '@app/components/dashboard/KeyPair';
@@ -31,6 +34,7 @@ import guidGenerator from '@app/components/utilities/randomId';
import encryptSecrets from '@app/components/utilities/secrets/encryptSecrets';
import getSecretsForProject from '@app/components/utilities/secrets/getSecretsForProject';
import { getTranslatedServerSideProps } from '@app/components/utilities/withTranslateProps';
+import { IconButton } from '@app/components/v2';
import getProjectSercetSnapshotsCount from '@app/ee/api/secrets/GetProjectSercetSnapshotsCount';
import performSecretRollback from '@app/ee/api/secrets/PerformSecretRollback';
import PITRecoverySidebar from '@app/ee/components/PITRecoverySidebar';
@@ -43,6 +47,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;
@@ -58,6 +63,7 @@ interface SecretDataProps {
id: string;
idOverride: string | undefined;
comment: string;
+ tags: Tag[];
}
// eslint-disable-next-line @typescript-eslint/no-unused-vars
@@ -81,6 +87,7 @@ interface SnapshotProps {
value: string;
valueOverride: string;
comment: string;
+ tags: Tag[];
}[];
}
@@ -113,7 +120,7 @@ export default function Dashboard() {
const [blurred, setBlurred] = useState(true);
const [isKeyAvailable, setIsKeyAvailable] = useState(true);
const [isNew, setIsNew] = useState(false);
- const [isLoading, setIsLoading] = useState(false);
+ const [isLoading, setIsLoading] = useState(true);
const [searchKeys, setSearchKeys] = useState('');
const [errorDragAndDrop, setErrorDragAndDrop] = useState(false);
const [sortMethod, setSortMethod] = useState('alphabetical');
@@ -125,6 +132,9 @@ export default function Dashboard() {
const [snapshotData, setSnapshotData] = useState
();
const [numSnapshots, setNumSnapshots] = useState();
const [saveLoading, setSaveLoading] = useState(false);
+ const [autoCapitalization, setAutoCapitalization] = useState(false);
+ const [dropZoneData, setDropZoneData] = useState();
+ const [projectTags, setProjectTags] = useState([]);
const { t } = useTranslation();
const { createNotification } = useNotificationContext();
@@ -133,11 +143,7 @@ export default function Dashboard() {
const [workspaceEnvs, setWorkspaceEnvs] = useState([]);
const [selectedSnapshotEnv, setSelectedSnapshotEnv] = useState();
- const [selectedEnv, setSelectedEnv] = useState({
- name: '',
- slug: '',
- isWriteDenied: false
- });
+ const [selectedEnv, setSelectedEnv] = useState();
const [atSecretAreaTop, setAtSecretsAreaTop] = useState(true);
const secretsTop = useRef(null);
@@ -193,15 +199,18 @@ export default function Dashboard() {
}));
setData(sortedData);
+ setIsLoading(false);
};
/**
* Reorder rows alphabetically or in the opprosite order
*/
const reorderRows = (dataToReorder: SecretDataProps[] | 1) => {
- setSortMethod((prevSort) => (prevSort === 'alphabetical' ? '-alphabetical' : 'alphabetical'));
+ if (dataToReorder) {
+ setSortMethod((prevSort) => (prevSort === 'alphabetical' ? '-alphabetical' : 'alphabetical'));
- sortValuesHandler(dataToReorder, undefined);
+ sortValuesHandler(dataToReorder, undefined);
+ }
};
useEffect(() => {
@@ -216,6 +225,7 @@ export default function Dashboard() {
if (!workspace) {
router.push(`/dashboard/${userWorkspaces?.[0]?._id}`);
}
+ setAutoCapitalization(workspace?.autoCapitalization ?? true);
const accessibleEnvironments = await getWorkspaceEnvironments({ workspaceId });
setWorkspaceEnvs(accessibleEnvironments || []);
@@ -233,6 +243,8 @@ export default function Dashboard() {
action: 'first_time_secrets_pushed'
});
setHasUserEverPushed(!!userAction);
+
+ setProjectTags(await getWorkspaceTags({ workspaceId }));
} catch (error) {
console.log('Error', error);
setData(undefined);
@@ -246,16 +258,17 @@ export default function Dashboard() {
setIsLoading(true);
setBlurred(true);
// ENV
- const dataToSort = await getSecretsForProject({
- env: selectedEnv.slug,
- setIsKeyAvailable,
- setData,
- workspaceId
- });
- setInitialData(dataToSort);
- reorderRows(dataToSort);
-
- setTimeout(() => setIsLoading(false), 700);
+ let dataToSort;
+ if (selectedEnv) {
+ dataToSort = await getSecretsForProject({
+ env: selectedEnv.slug,
+ setIsKeyAvailable,
+ setData,
+ workspaceId
+ });
+ setInitialData(dataToSort);
+ reorderRows(dataToSort);
+ }
} catch (error) {
console.log('Error', error);
setData(undefined);
@@ -274,7 +287,8 @@ export default function Dashboard() {
key: '',
value: '',
valueOverride: undefined,
- comment: ''
+ comment: '',
+ tags: []
},
...data!
]);
@@ -283,6 +297,23 @@ export default function Dashboard() {
}
};
+ const addRowToBottom = () => {
+ setIsNew(false);
+ setData([
+ ...data!,
+ {
+ id: guidGenerator(),
+ idOverride: guidGenerator(),
+ pos: data!.length,
+ key: '',
+ value: '',
+ valueOverride: undefined,
+ comment: '',
+ tags: []
+ },
+ ]);
+ };
+
const deleteRow = ({ ids, secretName }: { ids: string[]; secretName: string }) => {
setButtonReady(true);
toggleSidebar('None');
@@ -316,6 +347,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);
@@ -333,6 +369,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
*/
@@ -370,7 +410,7 @@ export default function Dashboard() {
});
}
- if (selectedEnv.isWriteDenied) {
+ if (selectedEnv?.isWriteDenied) {
setSaveLoading(false);
return createNotification({
text: 'You are not allowed to edit this environment',
@@ -405,7 +445,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)
@@ -439,7 +481,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);
@@ -454,7 +497,8 @@ 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)
@@ -466,14 +510,15 @@ 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);
if (secretsToBeDeleted.concat(overridesToBeDeleted).length > 0) {
await deleteSecrets({ secretIds: secretsToBeDeleted.concat(overridesToBeDeleted) });
}
- if (secretsToBeAdded.concat(overridesToBeAdded).length > 0) {
+ if (selectedEnv && secretsToBeAdded.concat(overridesToBeAdded).length > 0) {
const secrets = await encryptSecrets({
secretsToEncrypt: secretsToBeAdded.concat(overridesToBeAdded),
workspaceId,
@@ -481,7 +526,7 @@ export default function Dashboard() {
});
if (secrets) await addSecrets({ secrets, env: selectedEnv.slug, workspaceId });
}
- if (secretsToBeUpdated.concat(overridesToBeUpdated).length > 0) {
+ if (selectedEnv && secretsToBeUpdated.concat(overridesToBeUpdated).length > 0) {
const secrets = await encryptSecrets({
secretsToEncrypt: secretsToBeUpdated.concat(overridesToBeUpdated),
workspaceId,
@@ -504,11 +549,33 @@ export default function Dashboard() {
return undefined;
};
- const addData = (newData: SecretDataProps[]) => {
- setData(data!.concat(newData));
+ const addDataWithMerge = (newData: SecretDataProps[], preserve?: 'old' | 'new') => {
+ setData((oldData) => {
+ let filteredOldData = oldData!;
+ let filteredNewData = newData;
+ if (preserve === 'new')
+ filteredOldData = oldData!.filter(
+ (oldDataPoint) => !newData.find((newDataPoint) => newDataPoint.key === oldDataPoint.key)
+ );
+ if (preserve === 'old')
+ filteredNewData = newData.filter(
+ (newDataPoint) => !oldData?.find((oldDataPoint) => oldDataPoint.key === newDataPoint.key)
+ );
+ return filteredOldData.concat(filteredNewData);
+ });
setButtonReady(true);
};
+ const addData = (newData: SecretDataProps[]) => {
+ if (
+ newData.some((newDataPoint) => data?.find((dataPoint) => dataPoint.key === newDataPoint.key)) // if newData contains duplicates
+ ) {
+ setDropZoneData(newData);
+ return;
+ }
+ addDataWithMerge(newData);
+ };
+
const changeBlurred = () => {
setBlurred(!blurred);
};
@@ -518,7 +585,7 @@ export default function Dashboard() {
};
return data ? (
-
+
{t('common:head-title', { title: t('dashboard:title') })}
@@ -526,33 +593,23 @@ export default function Dashboard() {
-
- {sidebarSecretId !== 'None' && (
-
- row.key === data.filter((r) => r.id === sidebarSecretId)[0]?.key
- )}
- modifyKey={listenChangeKey}
- modifyValue={listenChangeValue}
- modifyValueOverride={listenChangeValueOverride}
- modifyComment={listenChangeComment}
- buttonReady={buttonReady}
- savePush={savePush}
- sharedToHide={sharedToHide}
- setSharedToHide={setSharedToHide}
- deleteRow={deleteCertainRow}
- />
- )}
- {PITSidebarOpen && (
-
- )}
-
+
+
setDropZoneData(undefined)}
+ onOverwriteConfirm={(preserve) => {
+ addDataWithMerge(dropZoneData!, preserve);
+ setDropZoneData(undefined);
+ }}
+ duplicateKeys={
+ dropZoneData
+ ?.filter((newDataPoint) =>
+ data?.find((dataPoint) => dataPoint.key === newDataPoint.key)
+ )
+ .map((duplicate) => duplicate.key) ?? []
+ }
+ />
+
{checkDocsPopUpVisible && (
)}
-
+
{snapshotData && (
)}
- {!snapshotData && data?.length === 0 && (
+ {!snapshotData && data?.length === 0 && selectedEnv && (
name)}
@@ -606,7 +663,10 @@ export default function Dashboard() {
togglePITSidebar(true)}
+ onButtonPressed={() => {
+ toggleSidebar('None');
+ togglePITSidebar(true)
+ }}
color="mineshaft"
size="md"
icon={faClockRotateLeft}
@@ -626,7 +686,7 @@ export default function Dashboard() {
/>
)}
- {snapshotData && (
+ {snapshotData && selectedEnv && (
-
+
- {(snapshotData || data?.length !== 0) && (
+ {(snapshotData || data?.length !== 0) && selectedEnv && (
<>
{!snapshotData ? (
)}
-
+
setSearchKeys(e.target.value)}
placeholder={String(t('dashboard:search-keys'))}
/>
- {!snapshotData && (
-
- reorderRows(1)}
- color="mineshaft"
- size="icon-md"
- icon={sortMethod === 'alphabetical' ? faArrowDownAZ : faArrowDownZA}
- />
-
- )}
{!snapshotData && (
@@ -762,31 +814,85 @@ export default function Dashboard() {
/>
) : data?.length !== 0 ? (
-
+
-
+
+
+
+
0
+
Key
+ {!snapshotData &&
reorderRows(1)}
+ >
+ {sortMethod === 'alphabetical' ? : }
+ }
+
+
+
+
+
+
+
null}
+ role="none"
+ onClick={() => {}}
+ className="invisible group-hover:visible"
+ >
+
+
+
+
+
+
{!snapshotData &&
data
- ?.filter((row) => row.key?.toUpperCase().includes(searchKeys.toUpperCase()))
+ ?.filter((row) => row.key?.toUpperCase().includes(searchKeys.toUpperCase()) || row.tags?.map(tag => tag.name).join(" ")?.toUpperCase().includes(searchKeys.toUpperCase()))
.filter((row) => !sharedToHide.includes(row.id))
.map((keyPair) => (
item.key))?.includes(
keyPair.key
)}
toggleSidebar={toggleSidebar}
+ togglePITSidebar={togglePITSidebar}
sidebarSecretId={sidebarSecretId}
isSnapshot={false}
+ deleteRow={deleteCertainRow}
+ tags={projectTags}
/>
))}
{snapshotData &&
@@ -812,11 +918,14 @@ export default function Dashboard() {
)
.map((keyPair) => (
item.key))?.includes(
keyPair.key
@@ -824,11 +933,23 @@ export default function Dashboard() {
toggleSidebar={toggleSidebar}
sidebarSecretId={sidebarSecretId}
isSnapshot
+ tags={projectTags}
/>
))}
+
{!snapshotData && (
-
+
) : (
-
+
{isKeyAvailable && !snapshotData && (
+ {sidebarSecretId !== 'None' && (
+
+ row.key === data.filter((r) => r.id === sidebarSecretId)[0]?.key
+ )}
+ modifyKey={listenChangeKey}
+ modifyValue={listenChangeValue}
+ modifyValueOverride={listenChangeValueOverride}
+ modifyComment={listenChangeComment}
+ buttonReady={buttonReady}
+ workspaceEnvs={workspaceEnvs}
+ selectedEnv={selectedEnv!}
+ workspaceId={workspaceId}
+ savePush={savePush}
+ sharedToHide={sharedToHide}
+ setSharedToHide={setSharedToHide}
+ deleteRow={deleteCertainRow}
+ />
+ )}
+ {PITSidebarOpen && (
+
+ )}
) : (
);
diff --git a/frontend/src/pages/github.tsx b/frontend/src/pages/github.tsx
deleted file mode 100644
index 897027246..000000000
--- a/frontend/src/pages/github.tsx
+++ /dev/null
@@ -1,41 +0,0 @@
-import { useEffect } from 'react';
-import { useRouter } from 'next/router';
-import queryString from 'query-string';
-
-import AuthorizeIntegration from './api/integrations/authorizeIntegration';
-
-export default function Github() {
- const router = useRouter();
- const parsedUrl = queryString.parse(router.asPath.split('?')[1]);
- const {code} = parsedUrl;
- const {state} = parsedUrl;
-
- /**
- * Here we forward to the default workspace if a user opens this url
- */
- // eslint-disable-next-line react-hooks/exhaustive-deps
- useEffect(() => {
- (async () => {
- try {
- if (state === localStorage.getItem('latestCSRFToken')) {
- localStorage.removeItem('latestCSRFToken');
- await AuthorizeIntegration({
- workspaceId: localStorage.getItem('projectData.id') as string,
- code: code as string,
- integration: 'github',
- });
- router.push(
- `/integrations/${ localStorage.getItem('projectData.id')}`
- );
- }
- } catch (error) {
- console.error('Github integration error: ', error);
- }
- })();
- // eslint-disable-next-line react-hooks/exhaustive-deps
- }, []);
-
- return
;
-}
-
-Github.requireAuth = true;
diff --git a/frontend/src/pages/heroku.tsx b/frontend/src/pages/heroku.tsx
deleted file mode 100644
index ff4f2263e..000000000
--- a/frontend/src/pages/heroku.tsx
+++ /dev/null
@@ -1,41 +0,0 @@
-import { useEffect } from 'react';
-import { useRouter } from 'next/router';
-import queryString from 'query-string';
-
-import AuthorizeIntegration from './api/integrations/authorizeIntegration';
-
-export default function Heroku() {
- const router = useRouter();
- const parsedUrl = queryString.parse(router.asPath.split('?')[1]);
- const {code} = parsedUrl;
- const {state} = parsedUrl;
-
- /**
- * Here we forward to the default workspace if a user opens this url
- */
- // eslint-disable-next-line react-hooks/exhaustive-deps
- useEffect(() => {
- (async () => {
- try {
- if (state === localStorage.getItem('latestCSRFToken')) {
- localStorage.removeItem('latestCSRFToken');
- await AuthorizeIntegration({
- workspaceId: localStorage.getItem('projectData.id') as string,
- code: code as string,
- integration: 'heroku',
- });
- router.push(
- `/integrations/${ localStorage.getItem('projectData.id')}`
- );
- }
- } catch (error) {
- console.error('Heroku integration error: ', error);
- }
- })();
- // eslint-disable-next-line react-hooks/exhaustive-deps
- }, []);
-
- return
;
-}
-
-Heroku.requireAuth = true;
diff --git a/frontend/src/pages/integrations/[id].tsx b/frontend/src/pages/integrations/[id].tsx
index a4bf83a89..d802e81a8 100644
--- a/frontend/src/pages/integrations/[id].tsx
+++ b/frontend/src/pages/integrations/[id].tsx
@@ -7,7 +7,6 @@ import { useTranslation } from 'next-i18next';
import frameworkIntegrationOptions from 'public/json/frameworkIntegrations.json';
import ActivateBotDialog from '@app/components/basic/dialog/ActivateBotDialog';
-import IntegrationAccessTokenDialog from '@app/components/basic/dialog/IntegrationAccessTokenDialog';
import CloudIntegrationSection from '@app/components/integrations/CloudIntegrationSection';
import FrameworkIntegrationSection from '@app/components/integrations/FrameworkIntegrationSection';
import IntegrationSection from '@app/components/integrations/IntegrationSection';
@@ -20,12 +19,10 @@ import {
} from '../../components/utilities/cryptography/crypto';
import getBot from '../api/bot/getBot';
import setBotActiveStatus from '../api/bot/setBotActiveStatus';
-import createIntegration from '../api/integrations/createIntegration';
import deleteIntegration from '../api/integrations/DeleteIntegration';
import getIntegrationOptions from '../api/integrations/GetIntegrationOptions';
import getWorkspaceAuthorizations from '../api/integrations/getWorkspaceAuthorizations';
import getWorkspaceIntegrations from '../api/integrations/getWorkspaceIntegrations';
-import saveIntegrationAccessToken from '../api/integrations/saveIntegrationAccessToken';
import getAWorkspace from '../api/workspace/getAWorkspace';
import getLatestFileKey from '../api/workspace/getLatestFileKey';
@@ -52,6 +49,7 @@ interface Integration {
}
interface IntegrationOption {
+ tenantId?: string;
clientId: string;
clientSlug?: string; // vercel-integration specific
docsLink: string;
@@ -75,7 +73,6 @@ export default function Integrations() {
// TODO: These will have its type when migratiing towards react-query
const [bot, setBot] = useState
(null);
const [isActivateBotDialogOpen, setIsActivateBotDialogOpen] = useState(false);
- const [isIntegrationAccessTokenDialogOpen, setIntegrationAccessTokenDialogOpen] = useState(false);
const [selectedIntegrationOption, setSelectedIntegrationOption] = useState(null);
const router = useRouter();
@@ -166,80 +163,83 @@ export default function Integrations() {
}
};
- /**
- * Handle integration option authorization for a given integration option [integrationOption]
- * @param {Object} obj
- * @param {Object} obj.integrationOption - an integration option
- * @param {String} obj.name
- * @param {String} obj.type
- * @param {String} obj.docsLink
- * @returns
- */
- const handleIntegrationOption = async ({
- integrationOption,
- accessToken
- }: {
- integrationOption: IntegrationOption,
- accessToken?: string;
- }) => {
+ const handleUnauthorizedIntegrationOptionPress = (integrationOption: IntegrationOption) => {
try {
- if (!bot.isActive) {
- await handleBotActivate();
+ // generate CSRF token for OAuth2 code-token exchange integrations
+ const state = crypto.randomBytes(16).toString('hex');
+ localStorage.setItem('latestCSRFToken', state);
+
+ let link = '';
+ switch (integrationOption.slug) {
+ case 'azure-key-vault':
+ link = `https://login.microsoftonline.com/${integrationOption.tenantId}/oauth2/v2.0/authorize?client_id=${integrationOption.clientId}&response_type=code&redirect_uri=${window.location.origin}/integrations/azure-key-vault/oauth2/callback&response_mode=query&scope=https://vault.azure.net/.default openid offline_access&state=${state}`;
+ break;
+ case 'heroku':
+ link = `https://id.heroku.com/oauth/authorize?client_id=${integrationOption.clientId}&response_type=code&scope=write-protected&state=${state}`;
+ break;
+ case 'vercel':
+ link = `https://vercel.com/integrations/${integrationOption.clientSlug}/new?state=${state}`;
+ break;
+ case 'netlify':
+ link = `https://app.netlify.com/authorize?client_id=${integrationOption.clientId}&response_type=code&state=${state}&redirect_uri=${window.location.origin}/integrations/netlify/oauth2/callback`;
+ break;
+ case 'github':
+ link = `https://github.com/login/oauth/authorize?client_id=${integrationOption.clientId}&response_type=code&scope=repo&redirect_uri=${window.location.origin}/integrations/github/oauth2/callback&state=${state}`;
+ break;
+ case 'render':
+ link = `${window.location.origin}/integrations/render/authorize`
+ break;
+ case 'flyio':
+ link = `${window.location.origin}/integrations/flyio/authorize`
+ break;
+ default:
+ break;
}
-
- if (integrationOption.type === 'oauth') {
- // integration is of type OAuth
- // generate CSRF token for OAuth2 code-token exchange integrations
- const state = crypto.randomBytes(16).toString('hex');
- localStorage.setItem('latestCSRFToken', state);
-
- switch (integrationOption.slug) {
- case 'heroku':
- window.location.assign(
- `https://id.heroku.com/oauth/authorize?client_id=${integrationOption.clientId}&response_type=code&scope=write-protected&state=${state}`
- );
- break;
- case 'vercel':
- window.location.assign(
- `https://vercel.com/integrations/${integrationOption.clientSlug}/new?state=${state}`
- );
- break;
- case 'netlify':
- window.location.assign(
- `https://app.netlify.com/authorize?client_id=${integrationOption.clientId}&response_type=code&state=${state}&redirect_uri=${window.location.origin}/netlify`
- );
- break;
- case 'github':
- window.location.assign(
- `https://github.com/login/oauth/authorize?client_id=${integrationOption.clientId}&response_type=code&scope=repo&redirect_uri=${window.location.origin}/github&state=${state}`
- );
- break;
- default:
- break;
- }
- return;
- } if (integrationOption.type === 'pat') {
- // integration is of type personal access token
- const integrationAuth = await saveIntegrationAccessToken({
- workspaceId: localStorage.getItem('projectData.id'),
- integration: integrationOption.slug,
- accessToken: accessToken ?? ''
- });
-
- setIntegrationAuths([...integrationAuths, integrationAuth])
-
- const integration = await createIntegration({
- integrationAuthId: integrationAuth._id
- });
-
- setIntegrations([...integrations, integration]);
- return;
+ if (link !== '') {
+ window.location.assign(link);
}
} catch (err) {
console.error(err);
}
- };
+ }
+
+ const handleAuthorizedIntegrationOptionPress = (integrationAuth: IntegrationAuth) => {
+ try {
+ let link = '';
+ switch (integrationAuth.integration) {
+ case 'azure-key-vault':
+ link = `${window.location.origin}/integrations/azure-key-vault/create?integrationAuthId=${integrationAuth._id}`;
+ break;
+ case 'heroku':
+ link = `${window.location.origin}/integrations/heroku/create?integrationAuthId=${integrationAuth._id}`;
+ break;
+ case 'vercel':
+ link = `${window.location.origin}/integrations/vercel/create?integrationAuthId=${integrationAuth._id}`;
+ break;
+ case 'netlify':
+ link = `${window.location.origin}/integrations/netlify/create?integrationAuthId=${integrationAuth._id}`;
+ break;
+ case 'github':
+ link = `${window.location.origin}/integrations/github/create?integrationAuthId=${integrationAuth._id}`;
+ break;
+ case 'render':
+ link = `${window.location.origin}/integrations/render/create?integrationAuthId=${integrationAuth._id}`;
+ break;
+ case 'flyio':
+ link = `${window.location.origin}/integrations/flyio/create?integrationAuthId=${integrationAuth._id}`;
+ break;
+ default:
+ break;
+ }
+
+ if (link !== '') {
+ window.location.assign(link);
+ }
+ } catch (err) {
+ console.error(err);
+ }
+ }
/**
* Open dialog to activate bot if bot is not active.
@@ -251,34 +251,20 @@ export default function Integrations() {
* @returns
*/
const integrationOptionPress = async (integrationOption: IntegrationOption) => {
- // consider: don't start integration until at [handleIntegrationOption] step
try {
const integrationAuthX = integrationAuths.find((integrationAuth) => integrationAuth.integration === integrationOption.slug);
- if (!integrationAuthX) {
- // case: integration has not been authorized before
-
- if (integrationOption.type === 'pat') {
- // case: integration requires user to input their personal access token for that integration
- setIntegrationAccessTokenDialogOpen(true);
- return;
- }
-
- // case: integration does not require user to input their personal access token (i.e. it's an OAuth2 integration)
- handleIntegrationOption({ integrationOption });
- return;
- }
-
if (!bot.isActive) {
await handleBotActivate();
}
- // case: integration has been authorized before
- // -> create new integration
- const integration = await createIntegration({
- integrationAuthId: integrationAuthX._id
- });
- setIntegrations([...integrations, integration]);
+ if (!integrationAuthX) {
+ // case: integration has not been authorized
+ handleUnauthorizedIntegrationOptionPress(integrationOption);
+ return;
+ }
+
+ handleAuthorizedIntegrationOptionPress(integrationAuthX);
} catch (err) {
console.error(err);
}
@@ -360,12 +346,6 @@ export default function Integrations() {
selectedIntegrationOption={selectedIntegrationOption}
integrationOptionPress={integrationOptionPress}
/>
- setIntegrationAccessTokenDialogOpen(false)}
- selectedIntegrationOption={selectedIntegrationOption}
- handleIntegrationOption={handleIntegrationOption}
- />
{
+ if (workspace) {
+ setSelectedSourceEnvironment(workspace.environments[0].slug);
+ }
+ }, [workspace]);
+
+ const handleButtonClick = async () => {
+ try {
+ if (vaultBaseUrl.length === 0) {
+ setVaultBaseUrlErrorText('Vault URI cannot be blank');
+ return;
+ }
+
+ if (
+ !vaultBaseUrl.startsWith('https://')
+ || !vaultBaseUrl.endsWith('vault.azure.net')
+ ) {
+ setVaultBaseUrlErrorText('Vault URI must be like https://.vault.azure.net');
+ return;
+ }
+
+ if (!integrationAuth?._id) return;
+
+ setIsLoading(true);
+ await createIntegration({
+ integrationAuthId: integrationAuth?._id,
+ isActive: true,
+ app: vaultBaseUrl,
+ appId: null,
+ sourceEnvironment: selectedSourceEnvironment,
+ targetEnvironment: null,
+ owner: null
+ });
+ setIsLoading(false);
+
+ router.push(
+ `/integrations/${localStorage.getItem('projectData.id')}`
+ );
+
+ } catch (err) {
+ console.error(err);
+ }
+ }
+
+ return (integrationAuth && workspace && selectedSourceEnvironment) ? (
+
+
+ Azure Key Vault Integration
+
+ setSelectedSourceEnvironment(val)}
+ className='w-full border border-mineshaft-500'
+ >
+ {workspace?.environments.map((sourceEnvironment) => (
+
+ {sourceEnvironment.name}
+
+ ))}
+
+
+
+ setVaultBaseUrl(e.target.value)}
+ />
+
+
+ Create Integration
+
+
+
+ ) :
+}
+
+AzureKeyVaultCreateIntegrationPage.requireAuth = true;
+
+export const getServerSideProps = getTranslatedServerSideProps(['integrations']);
\ No newline at end of file
diff --git a/frontend/src/pages/integrations/azure-key-vault/oauth2/callback.tsx b/frontend/src/pages/integrations/azure-key-vault/oauth2/callback.tsx
new file mode 100644
index 000000000..14d92d9c2
--- /dev/null
+++ b/frontend/src/pages/integrations/azure-key-vault/oauth2/callback.tsx
@@ -0,0 +1,41 @@
+import { useEffect } from 'react';
+import { useRouter } from 'next/router';
+import queryString from 'query-string';
+
+import { getTranslatedServerSideProps } from '../../../../components/utilities/withTranslateProps';
+import AuthorizeIntegration from "../../../api/integrations/authorizeIntegration";
+
+export default function AzureKeyVaultOAuth2CallbackPage() {
+ const router = useRouter();
+
+ const { code, state } = queryString.parse(router.asPath.split('?')[1]);
+
+ useEffect(() => {
+ (async () => {
+ try {
+ // validate state
+ if (state !== localStorage.getItem('latestCSRFToken')) return;
+ localStorage.removeItem('latestCSRFToken');
+
+ const integrationAuth = await AuthorizeIntegration({
+ workspaceId: localStorage.getItem('projectData.id') as string,
+ code: code as string,
+ integration: 'azure-key-vault'
+ });
+
+ router.push(
+ `/integrations/azure-key-vault/create?integrationAuthId=${integrationAuth._id}`
+ );
+
+ } catch (err) {
+ console.error(err);
+ }
+ })();
+ }, []);
+
+ return
+}
+
+AzureKeyVaultOAuth2CallbackPage.requireAuth = true;
+
+export const getServerSideProps = getTranslatedServerSideProps(['integrations']);
\ No newline at end of file
diff --git a/frontend/src/pages/integrations/flyio/authorize.tsx b/frontend/src/pages/integrations/flyio/authorize.tsx
new file mode 100644
index 000000000..5b00e562a
--- /dev/null
+++ b/frontend/src/pages/integrations/flyio/authorize.tsx
@@ -0,0 +1,76 @@
+import { useState } from 'react';
+import { useRouter } from 'next/router';
+
+import { getTranslatedServerSideProps } from '../../../components/utilities/withTranslateProps';
+import {
+ Button,
+ Card,
+ CardTitle,
+ FormControl,
+ Input,
+} from '../../../components/v2';
+import saveIntegrationAccessToken from "../../api/integrations/saveIntegrationAccessToken";
+
+export default function FlyioCreateIntegrationPage() {
+ const router = useRouter();
+ const [accessToken, setAccessToken] = useState('');
+ const [accessTokenErrorText, setAccessTokenErrorText] = useState('');
+ const [isLoading, setIsLoading] = useState(false);
+
+ const handleButtonClick = async () => {
+ try {
+ setAccessTokenErrorText('');
+ if (accessToken.length === 0) {
+ setAccessTokenErrorText('Access token cannot be blank');
+ return;
+ }
+
+ setIsLoading(true);
+
+ const integrationAuth = await saveIntegrationAccessToken({
+ workspaceId: localStorage.getItem('projectData.id'),
+ integration: 'flyio',
+ accessToken
+ });
+
+ setIsLoading(false);
+
+ router.push(
+ `/integrations/flyio/create?integrationAuthId=${integrationAuth._id}`
+ );
+ } catch (err) {
+ console.error(err);
+ }
+ }
+
+ return (
+
+
+ Render Integration
+
+ setAccessToken(e.target.value)}
+ />
+
+
+ Connect to Fly.io
+
+
+
+ )
+}
+
+FlyioCreateIntegrationPage.requireAuth = true;
+
+export const getServerSideProps = getTranslatedServerSideProps(['integrations']);
\ No newline at end of file
diff --git a/frontend/src/pages/integrations/flyio/create.tsx b/frontend/src/pages/integrations/flyio/create.tsx
new file mode 100644
index 000000000..2e185d9fe
--- /dev/null
+++ b/frontend/src/pages/integrations/flyio/create.tsx
@@ -0,0 +1,122 @@
+import { useEffect, useState } from 'react';
+import { useRouter } from 'next/router';
+import queryString from 'query-string';
+
+import { getTranslatedServerSideProps } from '../../../components/utilities/withTranslateProps';
+import {
+ Button,
+ Card,
+ CardTitle,
+ FormControl,
+ Select,
+ SelectItem
+} from '../../../components/v2';
+import { useGetIntegrationAuthApps,useGetIntegrationAuthById } from '../../../hooks/api/integrationAuth';
+import { useGetWorkspaceById } from '../../../hooks/api/workspace';
+import createIntegration from "../../api/integrations/createIntegration";
+
+export default function FlyioCreateIntegrationPage() {
+ const router = useRouter();
+
+ const { integrationAuthId } = queryString.parse(router.asPath.split('?')[1]);
+
+ const { data: workspace } = useGetWorkspaceById(localStorage.getItem('projectData.id') ?? '');
+ const { data: integrationAuth } = useGetIntegrationAuthById(integrationAuthId as string ?? '');
+ const { data: integrationAuthApps } = useGetIntegrationAuthApps(integrationAuthId as string ?? '');
+
+ const [selectedSourceEnvironment, setSelectedSourceEnvironment] = useState('');
+ const [targetApp, setTargetApp] = useState('');
+
+ const [isLoading, setIsLoading] = useState(false);
+
+ useEffect(() => {
+ if (workspace) {
+ setSelectedSourceEnvironment(workspace.environments[0].slug);
+ }
+ }, [workspace]);
+
+ useEffect(() => {
+ // TODO: handle case where apps can be empty
+ if (integrationAuthApps) {
+ setTargetApp(integrationAuthApps[0].name);
+ }
+ }, [integrationAuthApps]);
+
+ const handleButtonClick = async () => {
+ try {
+ if (!integrationAuth?._id) return;
+
+ setIsLoading(true);
+
+ await createIntegration({
+ integrationAuthId: integrationAuth?._id,
+ isActive: true,
+ app: targetApp,
+ appId: null,
+ sourceEnvironment: selectedSourceEnvironment,
+ targetEnvironment: null,
+ owner: null
+ });
+
+ setIsLoading(false);
+
+ router.push(
+ `/integrations/${localStorage.getItem('projectData.id')}`
+ );
+ } catch (err) {
+ console.error(err);
+ }
+ }
+
+ return (integrationAuth && workspace && selectedSourceEnvironment && integrationAuthApps && targetApp) ? (
+
+
+ Fly.io Integration
+
+ setSelectedSourceEnvironment(val)}
+ className='w-full border border-mineshaft-500'
+ >
+ {workspace?.environments.map((sourceEnvironment) => (
+
+ {sourceEnvironment.name}
+
+ ))}
+
+
+
+ setTargetApp(val)}
+ className='w-full border border-mineshaft-500'
+ >
+ {integrationAuthApps.map((integrationAuthApp) => (
+
+ {integrationAuthApp.name}
+
+ ))}
+
+
+
+ Create Integration
+
+
+
+ ) :
+}
+
+FlyioCreateIntegrationPage.requireAuth = true;
+
+export const getServerSideProps = getTranslatedServerSideProps(['integrations']);
\ No newline at end of file
diff --git a/frontend/src/pages/integrations/github/create.tsx b/frontend/src/pages/integrations/github/create.tsx
new file mode 100644
index 000000000..3b39fa2d1
--- /dev/null
+++ b/frontend/src/pages/integrations/github/create.tsx
@@ -0,0 +1,123 @@
+import { useEffect, useState } from 'react';
+import { useRouter } from 'next/router';
+import queryString from 'query-string';
+
+import { getTranslatedServerSideProps } from '../../../components/utilities/withTranslateProps';
+import {
+ Button,
+ Card,
+ CardTitle,
+ FormControl,
+ Select,
+ SelectItem
+} from '../../../components/v2';
+import { useGetIntegrationAuthApps,useGetIntegrationAuthById } from '../../../hooks/api/integrationAuth';
+import { useGetWorkspaceById } from '../../../hooks/api/workspace';
+import createIntegration from "../../api/integrations/createIntegration";
+
+export default function GitHubCreateIntegrationPage() {
+ const router = useRouter();
+
+ const { integrationAuthId } = queryString.parse(router.asPath.split('?')[1]);
+
+ const { data: workspace } = useGetWorkspaceById(localStorage.getItem('projectData.id') ?? '');
+ const { data: integrationAuth } = useGetIntegrationAuthById(integrationAuthId as string ?? '');
+ const { data: integrationAuthApps } = useGetIntegrationAuthApps(integrationAuthId as string ?? '');
+
+ const [selectedSourceEnvironment, setSelectedSourceEnvironment] = useState('');
+ const [owner, setOwner] = useState(null);
+ const [targetApp, setTargetApp] = useState('');
+
+ const [isLoading, setIsLoading] = useState(false);
+
+ useEffect(() => {
+ if (workspace) {
+ setSelectedSourceEnvironment(workspace.environments[0].slug);
+ }
+ }, [workspace]);
+
+ useEffect(() => {
+ // TODO: handle case where apps can be empty
+ if (integrationAuthApps) {
+ setTargetApp(integrationAuthApps[0].name);
+ setOwner(integrationAuthApps[0]?.owner ?? null);
+ }
+ }, [integrationAuthApps]);
+
+ const handleButtonClick = async () => {
+ try {
+ setIsLoading(true);
+
+ if (!integrationAuth?._id) return;
+
+ await createIntegration({
+ integrationAuthId: integrationAuth?._id,
+ isActive: true,
+ app: targetApp,
+ appId: null,
+ sourceEnvironment: selectedSourceEnvironment,
+ targetEnvironment: null,
+ owner
+ });
+
+ setIsLoading(false);
+ router.push(
+ `/integrations/${localStorage.getItem('projectData.id')}`
+ );
+ } catch (err) {
+ console.error(err);
+ }
+ }
+
+ return (integrationAuth && workspace && selectedSourceEnvironment && integrationAuthApps && targetApp) ? (
+
+
+ GitHub Integration
+
+ setSelectedSourceEnvironment(val)}
+ className='w-full border border-mineshaft-500'
+ >
+ {workspace?.environments.map((sourceEnvironment) => (
+
+ {sourceEnvironment.name}
+
+ ))}
+
+
+
+ setTargetApp(val)}
+ className='w-full border border-mineshaft-500'
+ >
+ {integrationAuthApps.map((integrationAuthApp) => (
+
+ {integrationAuthApp.name}
+
+ ))}
+
+
+
+ Create Integration
+
+
+
+ ) :
+}
+
+GitHubCreateIntegrationPage.requireAuth = true;
+
+export const getServerSideProps = getTranslatedServerSideProps(['integrations']);
\ No newline at end of file
diff --git a/frontend/src/pages/integrations/github/oauth2/callback.tsx b/frontend/src/pages/integrations/github/oauth2/callback.tsx
new file mode 100644
index 000000000..a3fd7e84c
--- /dev/null
+++ b/frontend/src/pages/integrations/github/oauth2/callback.tsx
@@ -0,0 +1,40 @@
+import { useEffect } from 'react';
+import { useRouter } from 'next/router';
+import queryString from 'query-string';
+
+import { getTranslatedServerSideProps } from '../../../../components/utilities/withTranslateProps';
+import AuthorizeIntegration from "../../../api/integrations/authorizeIntegration";
+
+export default function GitHubOAuth2CallbackPage() {
+ const router = useRouter();
+ const { code, state } = queryString.parse(router.asPath.split('?')[1]);
+
+ useEffect(() => {
+ (async () => {
+ try {
+ // validate state
+ if (state !== localStorage.getItem('latestCSRFToken')) return;
+ localStorage.removeItem('latestCSRFToken');
+
+ const integrationAuth = await AuthorizeIntegration({
+ workspaceId: localStorage.getItem('projectData.id') as string,
+ code: code as string,
+ integration: 'github'
+ });
+
+ router.push(
+ `/integrations/github/create?integrationAuthId=${integrationAuth._id}`
+ );
+
+ } catch (err) {
+ console.error(err);
+ }
+ })();
+ }, []);
+
+ return
+}
+
+GitHubOAuth2CallbackPage.requireAuth = true;
+
+export const getServerSideProps = getTranslatedServerSideProps(['integrations']);
\ No newline at end of file
diff --git a/frontend/src/pages/integrations/heroku/create.tsx b/frontend/src/pages/integrations/heroku/create.tsx
new file mode 100644
index 000000000..46e379462
--- /dev/null
+++ b/frontend/src/pages/integrations/heroku/create.tsx
@@ -0,0 +1,121 @@
+import { useEffect, useState } from 'react';
+import { useRouter } from 'next/router';
+import queryString from 'query-string';
+
+import { getTranslatedServerSideProps } from '../../../components/utilities/withTranslateProps';
+import {
+ Button,
+ Card,
+ CardTitle,
+ FormControl,
+ Select,
+ SelectItem
+} from '../../../components/v2';
+import { useGetIntegrationAuthApps,useGetIntegrationAuthById } from '../../../hooks/api/integrationAuth';
+import { useGetWorkspaceById } from '../../../hooks/api/workspace';
+import createIntegration from "../../api/integrations/createIntegration";
+
+export default function HerokuCreateIntegrationPage() {
+ const router = useRouter();
+
+ const { integrationAuthId } = queryString.parse(router.asPath.split('?')[1]);
+
+ const { data: workspace } = useGetWorkspaceById(localStorage.getItem('projectData.id') ?? '');
+ const { data: integrationAuth } = useGetIntegrationAuthById(integrationAuthId as string ?? '');
+ const { data: integrationAuthApps } = useGetIntegrationAuthApps(integrationAuthId as string ?? '');
+
+ const [selectedSourceEnvironment, setSelectedSourceEnvironment] = useState('');
+ const [targetApp, setTargetApp] = useState('');
+
+ const [isLoading, setIsLoading] = useState(false);
+
+ useEffect(() => {
+ if (workspace) {
+ setSelectedSourceEnvironment(workspace.environments[0].slug);
+ }
+ }, [workspace]);
+
+ useEffect(() => {
+ // TODO: handle case where apps can be empty
+ if (integrationAuthApps) {
+ setTargetApp(integrationAuthApps[0].name);
+ }
+ }, [integrationAuthApps]);
+
+ const handleButtonClick = async () => {
+ try {
+ setIsLoading(true);
+
+ if (!integrationAuth?._id) return;
+
+ await createIntegration({
+ integrationAuthId: integrationAuth?._id,
+ isActive: true,
+ app: targetApp,
+ appId: null,
+ sourceEnvironment: selectedSourceEnvironment,
+ targetEnvironment: null,
+ owner: null
+ });
+
+ setIsLoading(false);
+ router.push(
+ `/integrations/${localStorage.getItem('projectData.id')}`
+ );
+ } catch (err) {
+ console.error(err);
+ }
+ }
+
+ return (integrationAuth && workspace && selectedSourceEnvironment && integrationAuthApps && targetApp) ? (
+
+
+ Heroku Integration
+
+ setSelectedSourceEnvironment(val)}
+ className='w-full border border-mineshaft-500'
+ >
+ {workspace?.environments.map((sourceEnvironment) => (
+
+ {sourceEnvironment.name}
+
+ ))}
+
+
+
+ setTargetApp(val)}
+ className='w-full border border-mineshaft-500'
+ >
+ {integrationAuthApps.map((integrationAuthApp) => (
+
+ {integrationAuthApp.name}
+
+ ))}
+
+
+
+ Create Integration
+
+
+
+ ) :
+}
+
+HerokuCreateIntegrationPage.requireAuth = true;
+
+export const getServerSideProps = getTranslatedServerSideProps(['integrations']);
\ No newline at end of file
diff --git a/frontend/src/pages/integrations/heroku/oauth2/callback.tsx b/frontend/src/pages/integrations/heroku/oauth2/callback.tsx
new file mode 100644
index 000000000..321255c9f
--- /dev/null
+++ b/frontend/src/pages/integrations/heroku/oauth2/callback.tsx
@@ -0,0 +1,40 @@
+import { useEffect } from 'react';
+import { useRouter } from 'next/router';
+import queryString from 'query-string';
+
+import { getTranslatedServerSideProps } from '../../../../components/utilities/withTranslateProps';
+import AuthorizeIntegration from "../../../api/integrations/authorizeIntegration";
+
+export default function HerokuOAuth2CallbackPage() {
+ const router = useRouter();
+
+ const { code, state } = queryString.parse(router.asPath.split('?')[1]);
+
+ useEffect(() => {
+ (async () => {
+ try {
+ // validate state
+ if (state !== localStorage.getItem('latestCSRFToken')) return;
+ localStorage.removeItem('latestCSRFToken');
+ const integrationAuth = await AuthorizeIntegration({
+ workspaceId: localStorage.getItem('projectData.id') as string,
+ code: code as string,
+ integration: 'heroku'
+ });
+
+ router.push(
+ `/integrations/heroku/create?integrationAuthId=${integrationAuth._id}`
+ );
+
+ } catch (err) {
+ console.error(err);
+ }
+ })();
+ }, []);
+
+ return
+}
+
+HerokuOAuth2CallbackPage.requireAuth = true;
+
+export const getServerSideProps = getTranslatedServerSideProps(['integrations']);
\ No newline at end of file
diff --git a/frontend/src/pages/integrations/netlify/create.tsx b/frontend/src/pages/integrations/netlify/create.tsx
new file mode 100644
index 000000000..7f00394d7
--- /dev/null
+++ b/frontend/src/pages/integrations/netlify/create.tsx
@@ -0,0 +1,143 @@
+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) {
+ setTargetApp(integrationAuthApps[0].name);
+ }
+ }, [integrationAuthApps]);
+
+ const handleButtonClick = async () => {
+ try {
+ setIsLoading(true);
+
+ if (!integrationAuth?._id) return;
+
+ await createIntegration({
+ integrationAuthId: integrationAuth?._id,
+ isActive: true,
+ app: targetApp,
+ appId: (integrationAuthApps?.find((integrationAuthApp) => integrationAuthApp.name === targetApp))?.appId ?? null,
+ sourceEnvironment: selectedSourceEnvironment,
+ targetEnvironment,
+ owner: null
+ });
+
+ setIsLoading(false);
+ router.push(
+ `/integrations/${localStorage.getItem('projectData.id')}`
+ );
+ } catch (err) {
+ console.error(err);
+ }
+ }
+
+ return (integrationAuth && workspace && selectedSourceEnvironment && integrationAuthApps && targetApp && targetEnvironment) ? (
+
+
+ Netlify Integration
+
+ setSelectedSourceEnvironment(val)}
+ className='w-full border border-mineshaft-500'
+ >
+ {workspace?.environments.map((sourceEnvironment) => (
+
+ {sourceEnvironment.name}
+
+ ))}
+
+
+
+ setTargetApp(val)}
+ className='w-full border border-mineshaft-500'
+ >
+ {integrationAuthApps.map((integrationAuthApp) => (
+
+ {integrationAuthApp.name}
+
+ ))}
+
+
+
+ setTargetEnvironment(val)}
+ className='w-full border border-mineshaft-500'
+ >
+ {netlifyEnvironments.map((netlifyEnvironment) => (
+
+ {netlifyEnvironment.name}
+
+ ))}
+
+
+
+ Create Integration
+
+
+
+ ) :
+}
+
+NetlifyCreateIntegrationPage.requireAuth = true;
+
+export const getServerSideProps = getTranslatedServerSideProps(['integrations']);
\ No newline at end of file
diff --git a/frontend/src/pages/integrations/netlify/oauth2/callback.tsx b/frontend/src/pages/integrations/netlify/oauth2/callback.tsx
new file mode 100644
index 000000000..db884282a
--- /dev/null
+++ b/frontend/src/pages/integrations/netlify/oauth2/callback.tsx
@@ -0,0 +1,41 @@
+import { useEffect } from 'react';
+import { useRouter } from 'next/router';
+import queryString from 'query-string';
+
+import { getTranslatedServerSideProps } from '../../../../components/utilities/withTranslateProps';
+import AuthorizeIntegration from "../../../api/integrations/authorizeIntegration";
+
+export default function NetlifyOAuth2CallbackPage() {
+ const router = useRouter();
+
+ const { code, state } = queryString.parse(router.asPath.split('?')[1]);
+
+ useEffect(() => {
+ (async () => {
+ try {
+ // validate state
+ if (state !== localStorage.getItem('latestCSRFToken')) return;
+ localStorage.removeItem('latestCSRFToken');
+
+ const integrationAuth = await AuthorizeIntegration({
+ workspaceId: localStorage.getItem('projectData.id') as string,
+ code: code as string,
+ integration: 'netlify'
+ });
+
+ router.push(
+ `/integrations/netlify/create?integrationAuthId=${integrationAuth._id}`
+ );
+
+ } catch (err) {
+ console.error(err);
+ }
+ })();
+ }, []);
+
+ return
+}
+
+NetlifyOAuth2CallbackPage.requireAuth = true;
+
+export const getServerSideProps = getTranslatedServerSideProps(['integrations']);
\ No newline at end of file
diff --git a/frontend/src/pages/integrations/render/authorize.tsx b/frontend/src/pages/integrations/render/authorize.tsx
new file mode 100644
index 000000000..bd7a877c3
--- /dev/null
+++ b/frontend/src/pages/integrations/render/authorize.tsx
@@ -0,0 +1,76 @@
+import { useState } from 'react';
+import { useRouter } from 'next/router';
+
+import { getTranslatedServerSideProps } from '../../../components/utilities/withTranslateProps';
+import {
+ Button,
+ Card,
+ CardTitle,
+ FormControl,
+ Input,
+} from '../../../components/v2';
+import saveIntegrationAccessToken from "../../api/integrations/saveIntegrationAccessToken";
+
+export default function RenderCreateIntegrationPage() {
+ const router = useRouter();
+ const [apiKey, setApiKey] = useState('');
+ const [apiKeyErrorText, setApiKeyErrorText] = useState('');
+ const [isLoading, setIsLoading] = useState(false);
+
+ const handleButtonClick = async () => {
+ try {
+ setApiKeyErrorText('');
+ if (apiKey.length === 0) {
+ setApiKeyErrorText('API Key cannot be blank');
+ return;
+ }
+
+ setIsLoading(true);
+
+ const integrationAuth = await saveIntegrationAccessToken({
+ workspaceId: localStorage.getItem('projectData.id'),
+ integration: 'render',
+ accessToken: apiKey
+ });
+
+ setIsLoading(false);
+
+ router.push(
+ `/integrations/render/create?integrationAuthId=${integrationAuth._id}`
+ );
+ } catch (err) {
+ console.error(err);
+ }
+ }
+
+ return (
+
+
+ Render Integration
+
+ setApiKey(e.target.value)}
+ />
+
+
+ Connect to Render
+
+
+
+ )
+}
+
+RenderCreateIntegrationPage.requireAuth = true;
+
+export const getServerSideProps = getTranslatedServerSideProps(['integrations']);
\ No newline at end of file
diff --git a/frontend/src/pages/integrations/render/create.tsx b/frontend/src/pages/integrations/render/create.tsx
new file mode 100644
index 000000000..b37319312
--- /dev/null
+++ b/frontend/src/pages/integrations/render/create.tsx
@@ -0,0 +1,122 @@
+import { useEffect, useState } from 'react';
+import { useRouter } from 'next/router';
+import queryString from 'query-string';
+
+import { getTranslatedServerSideProps } from '../../../components/utilities/withTranslateProps';
+import {
+ Button,
+ Card,
+ CardTitle,
+ FormControl,
+ Select,
+ SelectItem
+} from '../../../components/v2';
+import { useGetIntegrationAuthApps,useGetIntegrationAuthById } from '../../../hooks/api/integrationAuth';
+import { useGetWorkspaceById } from '../../../hooks/api/workspace';
+import createIntegration from "../../api/integrations/createIntegration";
+
+export default function RenderCreateIntegrationPage() {
+ const router = useRouter();
+
+ const { integrationAuthId } = queryString.parse(router.asPath.split('?')[1]);
+
+ const { data: workspace } = useGetWorkspaceById(localStorage.getItem('projectData.id') ?? '');
+ const { data: integrationAuth } = useGetIntegrationAuthById(integrationAuthId as string ?? '');
+ const { data: integrationAuthApps } = useGetIntegrationAuthApps(integrationAuthId as string ?? '');
+
+ const [selectedSourceEnvironment, setSelectedSourceEnvironment] = useState('');
+ const [targetApp, setTargetApp] = useState('');
+
+ const [isLoading, setIsLoading] = useState(false);
+
+ useEffect(() => {
+ if (workspace) {
+ setSelectedSourceEnvironment(workspace.environments[0].slug);
+ }
+ }, [workspace]);
+
+ useEffect(() => {
+ // TODO: handle case where apps can be empty
+ if (integrationAuthApps) {
+ setTargetApp(integrationAuthApps[0].name);
+ }
+ }, [integrationAuthApps]);
+
+ const handleButtonClick = async () => {
+ try {
+ if (!integrationAuth?._id) return;
+
+ setIsLoading(true);
+
+ await createIntegration({
+ integrationAuthId: integrationAuth?._id,
+ isActive: true,
+ app: targetApp,
+ appId: (integrationAuthApps?.find((integrationAuthApp) => integrationAuthApp.name === targetApp))?.appId ?? null,
+ sourceEnvironment: selectedSourceEnvironment,
+ targetEnvironment: null,
+ owner: null
+ });
+
+ setIsLoading(false);
+
+ router.push(
+ `/integrations/${localStorage.getItem('projectData.id')}`
+ );
+ } catch (err) {
+ console.error(err);
+ }
+ }
+
+ return (integrationAuth && workspace && selectedSourceEnvironment && integrationAuthApps && targetApp) ? (
+
+
+ Render Integration
+
+ setSelectedSourceEnvironment(val)}
+ className='w-full border border-mineshaft-500'
+ >
+ {workspace?.environments.map((sourceEnvironment) => (
+
+ {sourceEnvironment.name}
+
+ ))}
+
+
+
+ setTargetApp(val)}
+ className='w-full border border-mineshaft-500'
+ >
+ {integrationAuthApps.map((integrationAuthApp) => (
+
+ {integrationAuthApp.name}
+
+ ))}
+
+
+
+ Create Integration
+
+
+
+ ) :
+}
+
+RenderCreateIntegrationPage.requireAuth = true;
+
+export const getServerSideProps = getTranslatedServerSideProps(['integrations']);
\ No newline at end of file
diff --git a/frontend/src/pages/integrations/vercel/create.tsx b/frontend/src/pages/integrations/vercel/create.tsx
new file mode 100644
index 000000000..5e0e8ad7e
--- /dev/null
+++ b/frontend/src/pages/integrations/vercel/create.tsx
@@ -0,0 +1,142 @@
+import { useEffect, useState } from 'react';
+import { useRouter } from 'next/router';
+import queryString from 'query-string';
+
+import { getTranslatedServerSideProps } from '../../../components/utilities/withTranslateProps';
+import {
+ Button,
+ Card,
+ CardTitle,
+ FormControl,
+ Select,
+ SelectItem
+} from '../../../components/v2';
+import { useGetIntegrationAuthApps,useGetIntegrationAuthById } from '../../../hooks/api/integrationAuth';
+import { useGetWorkspaceById } from '../../../hooks/api/workspace';
+import createIntegration from "../../api/integrations/createIntegration";
+
+const vercelEnvironments = [
+ { name: 'Development', slug: 'development' },
+ { name: 'Preview', slug: 'preview' },
+ { name: 'Production', slug: 'production' }
+]
+
+export default function VercelCreateIntegrationPage() {
+ const router = useRouter();
+
+ const { integrationAuthId } = queryString.parse(router.asPath.split('?')[1]);
+
+ const { data: workspace } = useGetWorkspaceById(localStorage.getItem('projectData.id') ?? '');
+ const { data: integrationAuth } = useGetIntegrationAuthById(integrationAuthId as string ?? '');
+ const { data: integrationAuthApps } = useGetIntegrationAuthApps(integrationAuthId as string ?? '');
+
+ const [selectedSourceEnvironment, setSelectedSourceEnvironment] = useState('');
+ const [targetApp, setTargetApp] = useState('');
+ const [targetEnvironment, setTargetEnvironemnt] = useState('');
+
+ const [isLoading, setIsLoading] = useState(false);
+
+ useEffect(() => {
+ if (workspace) {
+ setSelectedSourceEnvironment(workspace.environments[0].slug);
+ }
+ }, [workspace]);
+
+ useEffect(() => {
+ // TODO: handle case where apps can be empty
+ if (integrationAuthApps) {
+ setTargetApp(integrationAuthApps[0].name);
+ setTargetEnvironemnt(vercelEnvironments[0].slug);
+ }
+ }, [integrationAuthApps]);
+
+ const handleButtonClick = async () => {
+ try {
+ if (!integrationAuth?._id) return;
+
+ setIsLoading(true);
+ await createIntegration({
+ integrationAuthId: integrationAuth?._id,
+ isActive: true,
+ app: targetApp,
+ appId: null,
+ sourceEnvironment: selectedSourceEnvironment,
+ targetEnvironment,
+ owner: null
+ });
+
+ setIsLoading(false);
+ router.push(
+ `/integrations/${localStorage.getItem('projectData.id')}`
+ );
+ } catch (err) {
+ console.error(err);
+ }
+ }
+
+ return (integrationAuth && workspace && selectedSourceEnvironment && integrationAuthApps && targetApp && targetEnvironment) ? (
+
+
+ Vercel Integration
+
+ setSelectedSourceEnvironment(val)}
+ className='w-full border border-mineshaft-500'
+ >
+ {workspace?.environments.map((sourceEnvironment) => (
+
+ {sourceEnvironment.name}
+
+ ))}
+
+
+
+ setTargetApp(val)}
+ className='w-full border border-mineshaft-500'
+ >
+ {integrationAuthApps.map((integrationAuthApp) => (
+
+ {integrationAuthApp.name}
+
+ ))}
+
+
+
+ setTargetEnvironemnt(val)}
+ className='w-full border border-mineshaft-500'
+ >
+ {vercelEnvironments.map((vercelEnvironment) => (
+
+ {vercelEnvironment.name}
+
+ ))}
+
+
+
+ Create Integration
+
+
+
+ ) :
+}
+
+VercelCreateIntegrationPage.requireAuth = true;
+
+export const getServerSideProps = getTranslatedServerSideProps(['integrations']);
\ No newline at end of file
diff --git a/frontend/src/pages/integrations/vercel/oauth2/callback.tsx b/frontend/src/pages/integrations/vercel/oauth2/callback.tsx
new file mode 100644
index 000000000..1acf01b02
--- /dev/null
+++ b/frontend/src/pages/integrations/vercel/oauth2/callback.tsx
@@ -0,0 +1,41 @@
+import { useEffect } from 'react';
+import { useRouter } from 'next/router';
+import queryString from 'query-string';
+
+import { getTranslatedServerSideProps } from '../../../../components/utilities/withTranslateProps';
+import AuthorizeIntegration from "../../../api/integrations/authorizeIntegration";
+
+export default function VercelOAuth2CallbackPage() {
+ const router = useRouter();
+
+ const { code, state } = queryString.parse(router.asPath.split('?')[1]);
+
+ useEffect(() => {
+ (async () => {
+ try {
+ // validate state
+ if (state !== localStorage.getItem('latestCSRFToken')) return;
+ localStorage.removeItem('latestCSRFToken');
+
+ const integrationAuth = await AuthorizeIntegration({
+ workspaceId: localStorage.getItem('projectData.id') as string,
+ code: code as string,
+ integration: 'vercel'
+ });
+
+ router.push(
+ `/integrations/vercel/create?integrationAuthId=${integrationAuth._id}`
+ );
+
+ } catch (err) {
+ console.error(err);
+ }
+ })();
+ }, []);
+
+ return
+}
+
+VercelOAuth2CallbackPage.requireAuth = true;
+
+export const getServerSideProps = getTranslatedServerSideProps(['integrations']);
\ No newline at end of file
diff --git a/frontend/src/pages/netlify.tsx b/frontend/src/pages/netlify.tsx
deleted file mode 100644
index 532d6044a..000000000
--- a/frontend/src/pages/netlify.tsx
+++ /dev/null
@@ -1,46 +0,0 @@
-import { useEffect } from 'react';
-import { useRouter } from 'next/router';
-import queryString from 'query-string';
-
-import AuthorizeIntegration from './api/integrations/authorizeIntegration';
-
-export default function Netlify() {
- const router = useRouter();
- const parsedUrl = queryString.parse(router.asPath.split('?')[1]);
- const {code} = parsedUrl;
- const {state} = parsedUrl;
- // modify comment here
-
- /**
- * Here we forward to the default workspace if a user opens this url
- */
- // eslint-disable-next-line react-hooks/exhaustive-deps
- useEffect(() => {
- (async () => {
- try {
- if (!code) throw new Error('Code not found');
-
- if (state === localStorage.getItem('latestCSRFToken')) {
- localStorage.removeItem('latestCSRFToken');
-
- await AuthorizeIntegration({
- workspaceId: localStorage.getItem('projectData.id') as string,
- code: code as string,
- integration: 'netlify',
- });
-
- router.push(
- `/integrations/${ localStorage.getItem('projectData.id')}`
- );
- }
- } catch (err) {
- console.error('Netlify integration error: ', err);
- }
- })();
- // eslint-disable-next-line react-hooks/exhaustive-deps
- }, []);
-
- return
;
-}
-
-Netlify.requireAuth = true;
diff --git a/frontend/src/pages/settings/project/[id].tsx b/frontend/src/pages/settings/project/[id].tsx
index 14b2a3785..0d08eca45 100644
--- a/frontend/src/pages/settings/project/[id].tsx
+++ b/frontend/src/pages/settings/project/[id].tsx
@@ -1,332 +1,29 @@
-import { useEffect, useState } from 'react';
+import { useTranslation } from 'react-i18next';
import Head from 'next/head';
-import { useRouter } from 'next/router';
-import { useTranslation } from 'next-i18next';
-import { faCheck, faCopy, faPlus } from '@fortawesome/free-solid-svg-icons';
-import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
-import Button from '@app/components/basic/buttons/Button';
-import AddServiceTokenDialog from '@app/components/basic/dialog/AddServiceTokenDialog';
-import InputField from '@app/components/basic/InputField';
-import EnvironmentTable from '@app/components/basic/table/EnvironmentsTable';
-import ServiceTokenTable from '@app/components/basic/table/ServiceTokenTable';
-import NavHeader from '@app/components/navigation/NavHeader';
-import { getTranslatedServerSideProps } from '@app/components/utilities/withTranslateProps';
-import deleteEnvironment from '@app/pages/api/environments/deleteEnvironment';
-import updateEnvironment from '@app/pages/api/environments/updateEnvironment';
-
-import createEnvironment from '../../api/environments/createEnvironment';
-import getServiceTokens from '../../api/serviceToken/getServiceTokens';
-import deleteWorkspace from '../../api/workspace/deleteWorkspace';
-import getWorkspaces from '../../api/workspace/getWorkspaces';
-import renameWorkspace from '../../api/workspace/renameWorkspace';
-
-type EnvData = {
- name: string;
- slug: string;
-};
-
-export default function SettingsBasic() {
- const [buttonReady, setButtonReady] = useState(false);
- const router = useRouter();
- const [workspaceName, setWorkspaceName] = useState('');
- const [serviceTokens, setServiceTokens] = useState([]);
- const [environments, setEnvironments] = useState>([]);
- const [workspaceToBeDeletedName, setWorkspaceToBeDeletedName] = useState('');
- // const [isAddOpen, setIsAddOpen] = useState(false);
- const [isAddServiceTokenDialogOpen, setIsAddServiceTokenDialogOpen] = useState(false);
- const [projectIdCopied, setProjectIdCopied] = useState(false);
- const workspaceId = router.query.id as string;
+import { getTranslatedStaticProps } from '@app/components/utilities/withTranslateProps';
+import { ProjectSettingsPage } from '@app/views/Settings/ProjectSettingsPage';
+const ProjectSettings = () => {
const { t } = useTranslation();
- /**
- * This function copies the project id to the clipboard
- */
- function copyToClipboard() {
- const copyText = document.getElementById('myInput') as HTMLInputElement;
-
- if (copyText) {
- copyText.select();
- copyText.setSelectionRange(0, 99999); // For mobile devices
-
- navigator.clipboard.writeText(copyText.value);
-
- setProjectIdCopied(true);
- setTimeout(() => setProjectIdCopied(false), 2000);
- }
- }
-
- useEffect(() => {
- const load = async () => {
- const userWorkspaces = await getWorkspaces();
- userWorkspaces.forEach((userWorkspace) => {
- if (userWorkspace._id === workspaceId) {
- setWorkspaceName(userWorkspace.name);
- setEnvironments(userWorkspace.environments);
- }
- });
- const tempServiceTokens = await getServiceTokens({
- workspaceId
- });
- setServiceTokens(tempServiceTokens);
- };
-
- load();
- }, []);
-
- const modifyWorkspaceName = (newName: string) => {
- setButtonReady(true);
- setWorkspaceName(newName);
- };
-
- const submitChanges = (newWorkspaceName: string) => {
- renameWorkspace(workspaceId, newWorkspaceName);
- setButtonReady(false);
- };
-
- const closeAddServiceTokenModal = () => {
- setIsAddServiceTokenDialogOpen(false);
- };
-
- /**
- * This function deleted a workspace.
- * It first checks if there is more than one workspace aviable. Otherwise, it doesn't delete
- * It then checks if the name of the workspace to be deleted is correct. Otherwise, it doesn't delete.
- * It then deletes the workspace and forwards the user to another aviable workspace.
- */
- const executeDeletingWorkspace = async () => {
- const userWorkspaces = await getWorkspaces();
-
- if (userWorkspaces.length > 1) {
- if (
- userWorkspaces.filter((workspace) => workspace._id === workspaceId)[0].name ===
- workspaceToBeDeletedName
- ) {
- await deleteWorkspace(workspaceId);
- const ws = await getWorkspaces();
- router.push(`/dashboard/${ws[0]._id}`);
- }
- }
- };
-
- const onCreateEnvironment = async ({ name, slug }: EnvData) => {
- const res = await createEnvironment(workspaceId, {
- environmentName: name,
- environmentSlug: slug
- });
- if (res) {
- // TODO: on react-query migration do an api call to resync
- setEnvironments((env) => [...env, { name, slug }]);
- }
- };
-
- const onUpdateEnvironment = async (oldSlug: string, { name, slug }: EnvData) => {
- const res = await updateEnvironment(workspaceId, {
- oldEnvironmentSlug: oldSlug,
- environmentName: name,
- environmentSlug: slug
- });
- // TODO: on react-query migration do an api call to resync
- if (res) {
- setEnvironments((env) => env.map((el) => (el.slug === oldSlug ? { name, slug } : el)));
- }
- };
-
- const onDeleteEnvironment = async (slugToBeDelete: string) => {
- const res = await deleteEnvironment(workspaceId, slugToBeDelete);
- // TODO: on react-query migration do an api call to resync
- if (res) {
- setEnvironments((env) => env.filter(({ slug }) => slug !== slugToBeDelete));
- }
- };
-
return (
-
+ <>
{t('common:head-title', { title: t('settings-project:title') })}
-
-
-
-
-
-
-
{t('settings-project:title')}
-
- {t('settings-project:description')}
-
-
-
-
-
-
-
-
{t('common:display-name')}
-
-
-
-
-
- submitChanges(workspaceName)}
- color="mineshaft"
- size="md"
- active={buttonReady}
- iconDisabled={faCheck}
- textDisabled="Saved"
- />
-
-
-
-
-
{t('common:project-id')}
-
- {t('settings-project:project-id-description')}
-
-
- {t('settings-project:project-id-description2')}
- {/* eslint-disable-next-line react/jsx-no-target-blank */}
-
- {t('settings-project:docs')}
-
-
-
- {t('settings-project:auto-generated')}
-
-
-
{`${t('common:project-id')}:`}
-
-
-
- {projectIdCopied ? (
-
- ) : (
-
- )}
-
-
- {t('common:click-to-copy')}
-
-
-
-
-
-
-
-
-
-
-
- {t('section-token:service-tokens')}
-
-
- {t('section-token:service-tokens-description')}
-
-
- Please, make sure you are on the
-
- latest version of CLI
-
- .
-
-
-
- {
- setIsAddServiceTokenDialogOpen(true);
- }}
- color="mineshaft"
- icon={faPlus}
- size="md"
- />
-
-
-
-
-
-
-
-
{t('settings-project:danger-zone')}
-
{t('settings-project:danger-zone-note')}
-
-
-
-
- {t('settings-project:delete-project')}
-
-
- {t('settings-project:delete-project-note')}
-
-
-
-
-
-
+
+
+ >
);
-}
+};
-SettingsBasic.requireAuth = true;
+export default ProjectSettings;
-export const getServerSideProps = getTranslatedServerSideProps([
+ProjectSettings.requireAuth = true;
+
+export const getServerSideProps = getTranslatedStaticProps([
'settings',
'settings-project',
'section-token'
diff --git a/frontend/src/pages/users/[id].tsx b/frontend/src/pages/users/[id].tsx
index abd1cfc6f..27b531444 100644
--- a/frontend/src/pages/users/[id].tsx
+++ b/frontend/src/pages/users/[id].tsx
@@ -207,7 +207,6 @@ export default function Users() {
) : (
);
diff --git a/frontend/src/pages/vercel.tsx b/frontend/src/pages/vercel.tsx
deleted file mode 100644
index 8e8245736..000000000
--- a/frontend/src/pages/vercel.tsx
+++ /dev/null
@@ -1,46 +0,0 @@
-import { useEffect } from 'react';
-import { useRouter } from 'next/router';
-import queryString from 'query-string';
-
-import AuthorizeIntegration from './api/integrations/authorizeIntegration';
-
-export default function Vercel() {
- const router = useRouter();
- const parsedUrl = queryString.parse(router.asPath.split('?')[1]);
- const {code} = parsedUrl;
- const {state} = parsedUrl;
-
- /**
- * Here we forward to the default workspace if a user opens this url
- */
- // eslint-disable-next-line react-hooks/exhaustive-deps
- useEffect(() => {
- (async () => {
- try {
- // type check
- if (!code) throw new Error('Code not found');
-
- if (state === localStorage.getItem('latestCSRFToken')) {
- localStorage.removeItem('latestCSRFToken');
-
- await AuthorizeIntegration({
- workspaceId: localStorage.getItem('projectData.id') as string,
- code: code as string,
- integration: 'vercel',
- });
-
- router.push(
- `/integrations/${ localStorage.getItem('projectData.id')}`
- );
- }
- } catch (err) {
- console.error('Vercel integration error: ', err);
- }
- })();
- // eslint-disable-next-line react-hooks/exhaustive-deps
- }, []);
-
- return
;
-}
-
-Vercel.requireAuth = true;
diff --git a/frontend/src/views/Settings/ProjectSettingsPage/ProjectSettingsPage.tsx b/frontend/src/views/Settings/ProjectSettingsPage/ProjectSettingsPage.tsx
new file mode 100644
index 000000000..8e5dcf78b
--- /dev/null
+++ b/frontend/src/views/Settings/ProjectSettingsPage/ProjectSettingsPage.tsx
@@ -0,0 +1,374 @@
+import crypto from 'crypto';
+
+import { useState } from 'react';
+import { useTranslation } from 'react-i18next';
+import { useRouter } from 'next/router';
+
+import { useNotificationContext } from '@app/components/context/Notifications/NotificationProvider';
+import NavHeader from '@app/components/navigation/NavHeader';
+// TODO(akhilmhdh):Refactor this into a better utility module package
+import {
+ decryptAssymmetric,
+ encryptSymmetric
+} from '@app/components/utilities/cryptography/crypto';
+import { Button, FormControl, Input } from '@app/components/v2';
+import { plans } from '@app/const';
+import { useSubscription, useWorkspace } from '@app/context';
+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
+} from './components';
+
+export const ProjectSettingsPage = () => {
+ const { t } = useTranslation();
+ const { currentWorkspace, workspaces } = useWorkspace();
+ const router = useRouter();
+ const { data: serviceTokens } = useGetUserWsServiceTokens({
+ workspaceID: currentWorkspace?._id || ''
+ });
+ const workspaceID = currentWorkspace?._id || '';
+ const { createNotification } = useNotificationContext();
+ // delete action worksapce
+ const [deleteProjectInput, setDeleteProjectInput] = useState('');
+ const [isDeleting, setIsDeleting] = useToggle();
+
+ const renameWorkspace = useRenameWorkspace();
+ const toggleAutoCapitalization = useToggleAutoCapitalization();
+
+ const deleteWorkspace = useDeleteWorkspace();
+ // env crud operation
+ const createWsEnv = useCreateWsEnvironment();
+ const updateWsEnv = useUpdateWsEnvironment();
+ const deleteWsEnv = useDeleteWsEnvironment();
+
+ // service token
+ const { data: latestFileKey } = useGetUserWsKey(workspaceID);
+ 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;
+ const isEnvServiceAllowed =
+ subscriptionPlan !== plans.starter || host !== 'https://app.infisical.com';
+
+ const onRenameWorkspace = async (name: string) => {
+ try {
+ await renameWorkspace.mutateAsync({ workspaceID, newWorkspaceName: name });
+ createNotification({
+ text: 'Successfully renamed workspace',
+ type: 'success'
+ });
+ } catch (error) {
+ console.error(error);
+ createNotification({
+ text: 'Failed to rename workspace',
+ type: 'error'
+ });
+ }
+ };
+
+ const onAutoCapitalizationToggle = async (state: boolean) => {
+ try {
+ await toggleAutoCapitalization.mutateAsync({
+ workspaceID,
+ state
+ });
+ const text = `Successfully ${state ? 'enabled' : 'disabled'} auto capitalization`;
+ createNotification({
+ text,
+ type: 'success'
+ });
+ } catch (error) {
+ console.error(error);
+ createNotification({
+ text: 'Failed to update auto capitalization',
+ type: 'error'
+ });
+ }
+ };
+
+ const onDeleteWorkspace = async () => {
+ setIsDeleting.on();
+ try {
+ await deleteWorkspace.mutateAsync({ workspaceID });
+ // redirect user to first workspace user is part of
+ const ws = workspaces.find(({ _id }) => _id !== workspaceID);
+ router.push(`/dashboard/${ws?._id}`);
+ createNotification({
+ text: 'Successfully deleted workspace',
+ type: 'success'
+ });
+ } catch (error) {
+ console.error(error);
+ createNotification({
+ text: 'Failed to delete workspace',
+ type: 'error'
+ });
+ } finally {
+ setIsDeleting.off();
+ }
+ };
+
+ // workspace environment operation
+ const onCreateWsEnv = async ({ environmentName, environmentSlug }: CreateUpdateEnvFormData) => {
+ try {
+ await createWsEnv.mutateAsync({ workspaceID, environmentName, environmentSlug });
+ createNotification({
+ text: 'Successfully created environment',
+ type: 'success'
+ });
+ } catch (error) {
+ console.error(error);
+ createNotification({
+ text: 'Failed to create environment',
+ type: 'error'
+ });
+ }
+ };
+
+ const onUpdateWsEnv = async (
+ oldEnvironmentSlug: string,
+ { environmentName, environmentSlug }: CreateUpdateEnvFormData
+ ) => {
+ try {
+ await updateWsEnv.mutateAsync({
+ workspaceID,
+ environmentName,
+ environmentSlug,
+ oldEnvironmentSlug
+ });
+ createNotification({
+ text: 'Successfully updated environment',
+ type: 'success'
+ });
+ } catch (error) {
+ console.error(error);
+ createNotification({
+ text: 'Failed to update environment',
+ type: 'error'
+ });
+ }
+ };
+
+ const onDeleteWsEnv = async (environmentSlug: string) => {
+ try {
+ await deleteWsEnv.mutateAsync({
+ workspaceID,
+ environmentSlug
+ });
+ createNotification({
+ text: 'Successfully deleted environment',
+ type: 'success'
+ });
+ } catch (error) {
+ console.error(error);
+ createNotification({
+ text: 'Failed to delete environment',
+ type: 'error'
+ });
+ }
+ };
+
+ const onCreateServiceToken = async ({ environment, expiresIn, name }: CreateServiceToken) => {
+ // type guard
+ if (!latestFileKey) return '';
+ try {
+ // crypo calculation to generate the key
+ const key = decryptAssymmetric({
+ ciphertext: latestFileKey.encryptedKey,
+ nonce: latestFileKey.nonce,
+ publicKey: latestFileKey.sender.publicKey,
+ privateKey: localStorage.getItem('PRIVATE_KEY') as string
+ });
+ const randomBytes = crypto.randomBytes(16).toString('hex');
+ const { ciphertext, iv, tag } = encryptSymmetric({
+ plaintext: key,
+ key: randomBytes
+ });
+
+ const res = await createServiceToken.mutateAsync({
+ encryptedKey: ciphertext,
+ iv,
+ tag,
+ environment,
+ expiresIn: Number(expiresIn),
+ name,
+ workspaceId: workspaceID
+ });
+ console.log(res);
+ createNotification({
+ text: 'Successfully created a service token',
+ type: 'success'
+ });
+ return res.serviceToken;
+ } catch (error) {
+ console.error(error);
+ createNotification({
+ text: 'Failed to create a service token',
+ type: 'error'
+ });
+ }
+ 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);
+ createNotification({
+ text: 'Successfully revoked service token',
+ type: 'success'
+ });
+ } catch (error) {
+ console.error(error);
+ createNotification({
+ text: 'Failed to delete service token',
+ type: 'error'
+ });
+ }
+ };
+
+ return (
+
+ {/* TODO(akhilmhdh): Remove this right when layout is refactored */}
+
+
+
+
+
+
{t('settings-project:title')}
+
+ {t('settings-project:description')}
+
+
+
+
+
+
+
+
+
+
+
{t('settings-project:danger-zone')}
+
{t('settings-project:danger-zone-note')}
+
+
+ Type {currentWorkspace?.name} to delete the
+ workspace
+
+ }
+ >
+
setDeleteProjectInput(e.target.value)}
+ value={deleteProjectInput}
+ placeholder="Type the project name to delete"
+ />
+
+
+
+ {t('settings-project:delete-project')}
+
+
+ {t('settings-project:delete-project-note')}
+
+
+
+ );
+};
diff --git a/frontend/src/views/Settings/ProjectSettingsPage/components/AutoCapitalizationSection/AutoCapitalizationSection.tsx b/frontend/src/views/Settings/ProjectSettingsPage/components/AutoCapitalizationSection/AutoCapitalizationSection.tsx
new file mode 100644
index 000000000..a398db343
--- /dev/null
+++ b/frontend/src/views/Settings/ProjectSettingsPage/components/AutoCapitalizationSection/AutoCapitalizationSection.tsx
@@ -0,0 +1,34 @@
+import { useTranslation } from 'react-i18next';
+
+import { Checkbox } from '@app/components/v2';
+
+type Props = {
+ workspaceAutoCapitalization?: boolean;
+ onAutoCapitalizationChange: (state: boolean) => Promise
;
+};
+
+export const AutoCapitalizationSection = ({
+ workspaceAutoCapitalization,
+ onAutoCapitalizationChange
+}: Props) => {
+ const { t } = useTranslation();
+ return (
+
+ );
+};
diff --git a/frontend/src/views/Settings/ProjectSettingsPage/components/AutoCapitalizationSection/index.tsx b/frontend/src/views/Settings/ProjectSettingsPage/components/AutoCapitalizationSection/index.tsx
new file mode 100644
index 000000000..2b851428e
--- /dev/null
+++ b/frontend/src/views/Settings/ProjectSettingsPage/components/AutoCapitalizationSection/index.tsx
@@ -0,0 +1 @@
+export { AutoCapitalizationSection } from './AutoCapitalizationSection';
\ No newline at end of file
diff --git a/frontend/src/views/Settings/ProjectSettingsPage/components/CopyProjectIDSection/CopyProjectIDSection.tsx b/frontend/src/views/Settings/ProjectSettingsPage/components/CopyProjectIDSection/CopyProjectIDSection.tsx
new file mode 100644
index 000000000..6e18a88f7
--- /dev/null
+++ b/frontend/src/views/Settings/ProjectSettingsPage/components/CopyProjectIDSection/CopyProjectIDSection.tsx
@@ -0,0 +1,66 @@
+import { useEffect } from 'react';
+import { useTranslation } from 'react-i18next';
+import { faCheck, faCopy } from '@fortawesome/free-solid-svg-icons';
+import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
+
+import { IconButton } from '@app/components/v2';
+import { useToggle } from '@app/hooks';
+
+type Props = {
+ workspaceID: string;
+};
+
+export const CopyProjectIDSection = ({ workspaceID }: Props): JSX.Element => {
+ const { t } = useTranslation();
+ const [isProjectIdCopied, setIsProjectIdCopied] = useToggle(false);
+
+ useEffect(() => {
+ let timer: NodeJS.Timeout;
+ if (isProjectIdCopied) {
+ timer = setTimeout(() => setIsProjectIdCopied.off(), 2000);
+ }
+ return () => clearTimeout(timer);
+ }, [isProjectIdCopied]);
+
+ const copyProjectIdToClipboard = () => {
+ navigator.clipboard.writeText(workspaceID);
+ setIsProjectIdCopied.on();
+ };
+
+ return (
+
+
{t('common:project-id')}
+
+ {t('settings-project:project-id-description')}
+
+
+ {t('settings-project:project-id-description2')}
+ {/* eslint-disable-next-line react/jsx-no-target-blank */}
+
+ {t('settings-project:docs')}
+
+
+
{t('settings-project:auto-generated')}
+
+
{`${t('common:project-id')}:`}
+
{workspaceID}
+
copyProjectIdToClipboard()}
+ >
+
+
+ {t('common:click-to-copy')}
+
+
+
+
+ );
+};
diff --git a/frontend/src/views/Settings/ProjectSettingsPage/components/CopyProjectIDSection/index.tsx b/frontend/src/views/Settings/ProjectSettingsPage/components/CopyProjectIDSection/index.tsx
new file mode 100644
index 000000000..1e56a1747
--- /dev/null
+++ b/frontend/src/views/Settings/ProjectSettingsPage/components/CopyProjectIDSection/index.tsx
@@ -0,0 +1 @@
+export { CopyProjectIDSection } from './CopyProjectIDSection';
diff --git a/frontend/src/views/Settings/ProjectSettingsPage/components/EnvironmentSection/EnvironmentSection.tsx b/frontend/src/views/Settings/ProjectSettingsPage/components/EnvironmentSection/EnvironmentSection.tsx
new file mode 100644
index 000000000..ddcf7f4b3
--- /dev/null
+++ b/frontend/src/views/Settings/ProjectSettingsPage/components/EnvironmentSection/EnvironmentSection.tsx
@@ -0,0 +1,240 @@
+import { Controller, useForm } from 'react-hook-form';
+import { faPencil, 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,
+ ModalContent,
+ Table,
+ TableContainer,
+ TBody,
+ Td,
+ Th,
+ THead,
+ Tr,
+ UpgradePlanModal
+} from '@app/components/v2';
+import { usePopUp } from '@app/hooks/usePopUp';
+
+type Props = {
+ environments: Array<{ name: string; slug: string }>;
+ isEnvServiceAllowed: boolean;
+ onCreate: (data: CreateUpdateEnvFormData) => Promise;
+ onUpdate: (oldEnvSlug: string, data: CreateUpdateEnvFormData) => Promise;
+ onDelete: (envSlug: string) => Promise;
+};
+
+const createUpdateEnvSchema = yup.object({
+ environmentName: yup.string().label('Environment Name').required(),
+ environmentSlug: yup.string().label('Environment Slug').required()
+});
+
+export type CreateUpdateEnvFormData = yup.InferType;
+
+export const EnvironmentSection = ({
+ environments,
+ isEnvServiceAllowed,
+ onCreate,
+ onDelete,
+ onUpdate
+}: Props): JSX.Element => {
+ const { popUp, handlePopUpOpen, handlePopUpClose, handlePopUpToggle } = usePopUp([
+ 'createUpdateEnv',
+ 'deleteEnv',
+ 'upgradePlan'
+ ] as const);
+
+ const {
+ control,
+ handleSubmit,
+ reset,
+ formState: { isSubmitting }
+ } = useForm({
+ resolver: yupResolver(createUpdateEnvSchema)
+ });
+
+ const isEnvUpdate = Boolean(popUp?.createUpdateEnv?.data);
+ const oldEnvSlug = (popUp?.createUpdateEnv?.data as { slug: string })?.slug;
+
+ const onEnvModalSubmit = async (data: CreateUpdateEnvFormData) => {
+ if (isEnvUpdate) {
+ await onUpdate(oldEnvSlug, data);
+ } else {
+ await onCreate(data);
+ }
+ handlePopUpClose('createUpdateEnv');
+ };
+
+ const onEnvDeleteSubmit = async (envSlug: string) => {
+ await onDelete(envSlug);
+ handlePopUpClose('deleteEnv');
+ };
+
+ return (
+
+
+
+
Project Environments
+
+ Choose which environments will show up in your dashboard like development, staging,
+ production
+
+
+ Note: the text in slugs shows how these environmant should be accessed in CLI.
+
+
+
+ {
+ if (isEnvServiceAllowed) {
+ handlePopUpOpen('createUpdateEnv');
+ } else {
+ handlePopUpOpen('upgradePlan');
+ }
+ }}
+ colorSchema="primary"
+ leftIcon={ }
+ >
+ Add New Environment
+
+
+
+
+
+
+
+ Name
+ Slug
+
+
+
+
+ {environments?.length > 0 ? (
+ environments.map(({ name, slug }) => (
+
+ {name}
+ {slug}
+
+ {
+ if (isEnvServiceAllowed) {
+ handlePopUpOpen('createUpdateEnv', { name, slug });
+ reset({ environmentName: name, environmentSlug: slug });
+ } else {
+ handlePopUpOpen('upgradePlan');
+ }
+ }}
+ colorSchema="secondary"
+ ariaLabel="update"
+ >
+
+
+ {
+ if (isEnvServiceAllowed) {
+ handlePopUpOpen('deleteEnv', { name, slug });
+ } else {
+ handlePopUpOpen('upgradePlan');
+ }
+ }}
+ colorSchema="danger"
+ ariaLabel="update"
+ >
+
+
+
+
+ ))
+ ) : (
+
+
+ No environments found
+
+
+ )}
+
+
+
+
{
+ handlePopUpToggle('createUpdateEnv', isOpen);
+ reset();
+ }}
+ >
+
+
+
+
+
handlePopUpToggle('deleteEnv', isOpen)}
+ deleteKey={(popUp?.deleteEnv?.data as { slug: string })?.slug || ''}
+ onDeleteApproved={() =>
+ onEnvDeleteSubmit((popUp?.deleteEnv?.data as { slug: string })?.slug)
+ }
+ />
+ handlePopUpToggle('upgradePlan', isOpen)}
+ text="You can add custom environments if you switch to Infisical's Team plan."
+ />
+
+ );
+};
diff --git a/frontend/src/views/Settings/ProjectSettingsPage/components/EnvironmentSection/index.tsx b/frontend/src/views/Settings/ProjectSettingsPage/components/EnvironmentSection/index.tsx
new file mode 100644
index 000000000..239cfd747
--- /dev/null
+++ b/frontend/src/views/Settings/ProjectSettingsPage/components/EnvironmentSection/index.tsx
@@ -0,0 +1 @@
+export { EnvironmentSection } from './EnvironmentSection';
diff --git a/frontend/src/views/Settings/ProjectSettingsPage/components/ProjectNameChangeSection/ProjectNameChangeSection.tsx b/frontend/src/views/Settings/ProjectSettingsPage/components/ProjectNameChangeSection/ProjectNameChangeSection.tsx
new file mode 100644
index 000000000..02b488b55
--- /dev/null
+++ b/frontend/src/views/Settings/ProjectSettingsPage/components/ProjectNameChangeSection/ProjectNameChangeSection.tsx
@@ -0,0 +1,71 @@
+import { useEffect } from 'react';
+import { Controller, useForm } from 'react-hook-form';
+import { useTranslation } from 'react-i18next';
+import { faCheck } 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, FormControl, Input } from '@app/components/v2';
+
+type Props = {
+ workspaceName?: string;
+ onProjectNameChange: (name: string) => Promise;
+};
+
+const formSchema = yup.object({
+ name: yup.string().required().label('Project Name')
+});
+
+type FormData = yup.InferType;
+
+export const ProjectNameChangeSection = ({
+ workspaceName,
+ onProjectNameChange
+}: Props): JSX.Element => {
+ const {
+ handleSubmit,
+ control,
+ reset,
+ formState: { isDirty, isSubmitting }
+ } = useForm({ resolver: yupResolver(formSchema) });
+ const { t } = useTranslation();
+
+ useEffect(() => {
+ reset({ name: workspaceName });
+ }, [workspaceName]);
+
+ const onFormSubmit = async ({ name }: FormData) => {
+ await onProjectNameChange(name);
+ };
+
+ return (
+
+ );
+};
diff --git a/frontend/src/views/Settings/ProjectSettingsPage/components/ProjectNameChangeSection/index.tsx b/frontend/src/views/Settings/ProjectSettingsPage/components/ProjectNameChangeSection/index.tsx
new file mode 100644
index 000000000..c50fe4757
--- /dev/null
+++ b/frontend/src/views/Settings/ProjectSettingsPage/components/ProjectNameChangeSection/index.tsx
@@ -0,0 +1 @@
+export { ProjectNameChangeSection } from './ProjectNameChangeSection';
diff --git a/frontend/src/views/Settings/ProjectSettingsPage/components/SecretTagsSection/SecretTagsSection.tsx b/frontend/src/views/Settings/ProjectSettingsPage/components/SecretTagsSection/SecretTagsSection.tsx
new file mode 100644
index 000000000..6aa4d26be
--- /dev/null
+++ b/frontend/src/views/Settings/ProjectSettingsPage/components/SecretTagsSection/SecretTagsSection.tsx
@@ -0,0 +1,186 @@
+import { Controller, useForm } from 'react-hook-form';
+import { faPlus, faTrashCan } from '@fortawesome/free-solid-svg-icons';
+import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
+import { yupResolver } from '@hookform/resolvers/yup';
+import * as yup from 'yup';
+
+import {
+ Button,
+ DeleteActionModal,
+ FormControl,
+ IconButton,
+ Input,
+ Modal,
+ ModalClose,
+ ModalContent,
+ ModalTrigger,
+ Table,
+ TableContainer,
+ TBody,
+ Td,
+ Th,
+ THead,
+ Tr,
+} from '@app/components/v2';
+import { usePopUp } from '@app/hooks';
+import { WorkspaceTag } from '@app/hooks/api/types';
+
+const createTagSchema = yup.object({
+ name: yup.string().required().label('Tag Name'),
+});
+
+export type CreateWsTag = yup.InferType;
+
+type Props = {
+ tags: WorkspaceTag[];
+ workspaceName: string;
+ onDeleteTag: (tagID: string) => Promise;
+ onCreateTag: (data: CreateWsTag) => Promise;
+};
+
+type DeleteModalData = { name: string; id: string };
+
+export const SecretTagsSection = ({
+ tags = [],
+ onDeleteTag,
+ workspaceName,
+ onCreateTag
+}: Props): JSX.Element => {
+ const { popUp, handlePopUpToggle, handlePopUpClose, handlePopUpOpen } = usePopUp([
+ 'CreateSecretTag',
+ 'deleteTagConfirmation'
+ ] as const);
+
+ const {
+ control,
+ reset,
+ handleSubmit,
+ formState: { isSubmitting }
+ } = useForm({
+ resolver: yupResolver(createTagSchema)
+ });
+
+ const onFormSubmit = async (data: CreateWsTag) => {
+ console.log(19191, data);
+ await onCreateTag(data);
+ handlePopUpClose('CreateSecretTag');
+ };
+
+ const onDeleteApproved = async () => {
+ await onDeleteTag((popUp?.deleteTagConfirmation?.data as DeleteModalData)?.id);
+ handlePopUpClose('deleteTagConfirmation');
+ };
+
+ return (
+
+
+
+
Secret Tags
+
Every secret can be assigned to one or more tags. Here you can add and remove tags for the current project.
+
+
+
{
+ handlePopUpToggle('CreateSecretTag', open);
+ reset();
+ }}
+ >
+
+ }>
+ Add New Tag
+
+
+
+
+
+
+
+
+
+
+
+
+ Tag
+ Slug
+
+
+
+
+ {tags?.length > 0 ? (
+ tags.map(({ _id, name, slug }) => (
+
+ {name}
+ {slug}
+
+
+ handlePopUpOpen('deleteTagConfirmation', {
+ name,
+ id: _id
+ })
+ }
+ colorSchema="danger"
+ ariaLabel="update"
+ >
+
+
+
+
+ ))
+ ) : (
+
+
+ No tags found for this project
+
+
+ )}
+
+
+
+
handlePopUpToggle('deleteTagConfirmation', isOpen)}
+ deleteKey={(popUp?.deleteTagConfirmation?.data as DeleteModalData)?.name}
+ onClose={() => handlePopUpClose('deleteTagConfirmation')}
+ onDeleteApproved={onDeleteApproved}
+ />
+
+ );
+};
diff --git a/frontend/src/views/Settings/ProjectSettingsPage/components/SecretTagsSection/index.tsx b/frontend/src/views/Settings/ProjectSettingsPage/components/SecretTagsSection/index.tsx
new file mode 100644
index 000000000..0d0615293
--- /dev/null
+++ b/frontend/src/views/Settings/ProjectSettingsPage/components/SecretTagsSection/index.tsx
@@ -0,0 +1 @@
+export {SecretTagsSection} from './SecretTagsSection'
\ No newline at end of file
diff --git a/frontend/src/views/Settings/ProjectSettingsPage/components/ServiceTokenSection/ServiceTokenSection.tsx b/frontend/src/views/Settings/ProjectSettingsPage/components/ServiceTokenSection/ServiceTokenSection.tsx
new file mode 100644
index 000000000..052fdc678
--- /dev/null
+++ b/frontend/src/views/Settings/ProjectSettingsPage/components/ServiceTokenSection/ServiceTokenSection.tsx
@@ -0,0 +1,306 @@
+import { useEffect, useState } from 'react';
+import { Controller, useForm } from 'react-hook-form';
+import { useTranslation } from 'react-i18next';
+import { faCheck, faCopy, 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,
+ Select,
+ SelectItem,
+ Table,
+ TableContainer,
+ TBody,
+ Td,
+ Th,
+ THead,
+ Tr
+} from '@app/components/v2';
+import { usePopUp, useToggle } from '@app/hooks';
+import { ServiceToken, WorkspaceEnv } from '@app/hooks/api/types';
+
+const apiTokenExpiry = [
+ { label: '1 Day', value: 86400 },
+ { label: '7 Days', value: 604800 },
+ { label: '1 Month', value: 2592000 },
+ { label: '6 months', value: 15552000 },
+ { label: '12 months', value: 31104000 }
+];
+
+const createServiceTokenSchema = yup.object({
+ name: yup.string().required().label('Service Token Name'),
+ environment: yup.string().required().label('Environment'),
+ expiresIn: yup.string().required().label('Service Token Name')
+});
+
+export type CreateServiceToken = yup.InferType;
+
+type Props = {
+ tokens: ServiceToken[];
+ workspaceName: string;
+ environments: WorkspaceEnv[];
+ onDeleteToken: (serviceTokenID: string) => Promise;
+ onCreateToken: (data: CreateServiceToken) => Promise;
+};
+
+type DeleteModalData = { name: string; id: string };
+
+export const ServiceTokenSection = ({
+ tokens = [],
+ onDeleteToken,
+ workspaceName,
+ environments = [],
+ onCreateToken
+}: Props): JSX.Element => {
+ const [newToken, setToken] = useState('');
+ const { t } = useTranslation();
+ const [isTokenCopied, setIsTokenCopied] = useToggle(false);
+
+ useEffect(() => {
+ let timer: NodeJS.Timeout;
+ if (isTokenCopied) {
+ timer = setTimeout(() => setIsTokenCopied.off(), 2000);
+ }
+ return () => clearTimeout(timer);
+ }, [isTokenCopied]);
+
+ const copyTokenToClipboard = () => {
+ navigator.clipboard.writeText(newToken);
+ setIsTokenCopied.on();
+ };
+
+ const { popUp, handlePopUpToggle, handlePopUpClose, handlePopUpOpen } = usePopUp([
+ 'createAPIToken',
+ 'deleteAPITokenConfirmation'
+ ] as const);
+
+ const {
+ control,
+ reset,
+ handleSubmit,
+ formState: { isSubmitting }
+ } = useForm({
+ resolver: yupResolver(createServiceTokenSchema)
+ });
+
+ const hasServiceToken = Boolean(newToken);
+
+ const onFormSubmit = async (data: CreateServiceToken) => {
+ const token = await onCreateToken(data);
+ setToken(token);
+ };
+
+ const onDeleteApproved = async () => {
+ await onDeleteToken((popUp?.deleteAPITokenConfirmation?.data as DeleteModalData)?.id);
+ handlePopUpClose('deleteAPITokenConfirmation');
+ };
+
+ return (
+
+
+
+
{t('section-token:service-tokens')}
+
{t('section-token:service-tokens-description')}
+
+ Please, make sure you are on the
+
+ latest version of CLI
+
+ .
+
+
+
+
{
+ handlePopUpToggle('createAPIToken', open);
+ reset();
+ setToken('');
+ }}
+ >
+
+ }>
+ {t('section-token:add-new')}
+
+
+
+ {!hasServiceToken ? (
+
+ ) : (
+
+
{newToken}
+
+
+
+ {t('common:click-to-copy')}
+
+
+
+ )}
+
+
+
+
+
handlePopUpToggle('deleteAPITokenConfirmation', isOpen)}
+ deleteKey={(popUp?.deleteAPITokenConfirmation?.data as DeleteModalData)?.name}
+ onClose={() => handlePopUpClose('deleteAPITokenConfirmation')}
+ onDeleteApproved={onDeleteApproved}
+ />
+
+
+
+
+ Token Name
+ Environment
+ Valid Until
+
+
+
+
+ {tokens?.length > 0 ? (
+ tokens.map((row) => (
+
+ {row.name}
+ {row.environment}
+ {new Date(row.expiresAt).toUTCString()}
+
+
+ handlePopUpOpen('deleteAPITokenConfirmation', {
+ name: row.name,
+ id: row._id
+ })
+ }
+ colorSchema="danger"
+ ariaLabel="delete"
+ >
+
+
+
+
+ ))
+ ) : (
+
+
+ No service tokens found
+
+
+ )}
+
+
+
+
+ );
+};
diff --git a/frontend/src/views/Settings/ProjectSettingsPage/components/ServiceTokenSection/index.tsx b/frontend/src/views/Settings/ProjectSettingsPage/components/ServiceTokenSection/index.tsx
new file mode 100644
index 000000000..d683dc3b8
--- /dev/null
+++ b/frontend/src/views/Settings/ProjectSettingsPage/components/ServiceTokenSection/index.tsx
@@ -0,0 +1 @@
+export {ServiceTokenSection} from './ServiceTokenSection'
\ No newline at end of file
diff --git a/frontend/src/views/Settings/ProjectSettingsPage/components/index.tsx b/frontend/src/views/Settings/ProjectSettingsPage/components/index.tsx
new file mode 100644
index 000000000..98160b5b7
--- /dev/null
+++ b/frontend/src/views/Settings/ProjectSettingsPage/components/index.tsx
@@ -0,0 +1,7 @@
+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';
diff --git a/frontend/src/views/Settings/ProjectSettingsPage/index.tsx b/frontend/src/views/Settings/ProjectSettingsPage/index.tsx
new file mode 100644
index 000000000..4c4efbdb7
--- /dev/null
+++ b/frontend/src/views/Settings/ProjectSettingsPage/index.tsx
@@ -0,0 +1 @@
+export { ProjectSettingsPage } from './ProjectSettingsPage';