diff --git a/backend/src/controllers/v1/secretImportController.ts b/backend/src/controllers/v1/secretImportController.ts index 4b6539930..181248553 100644 --- a/backend/src/controllers/v1/secretImportController.ts +++ b/backend/src/controllers/v1/secretImportController.ts @@ -108,7 +108,7 @@ export const getAllSecretsFromImport = async (req: Request, res: Response) => { }); if (!importSecDoc) { - return res.status(200).json({ secrets: {} }); + return res.status(200).json({ secrets: [] }); } const secrets = await getAllImportedSecrets(workspaceId, environment, folderId); diff --git a/backend/src/helpers/bot.ts b/backend/src/helpers/bot.ts index c7c5904c7..2252dcc64 100644 --- a/backend/src/helpers/bot.ts +++ b/backend/src/helpers/bot.ts @@ -275,3 +275,70 @@ export const decryptSymmetricHelper = async ({ return plaintext; }; + +/** + * Return decrypted comments for workspace secrets with id [workspaceId] + * and [envionment] using bot + * @param {Object} obj + * @param {String} obj.workspaceId - id of workspace + * @param {String} obj.environment - environment + */ +export const getSecretsCommentBotHelper = async ({ + workspaceId, + environment, + secretPath +} : { + workspaceId: Types.ObjectId; + environment: string; + secretPath: string; +}) => { + const content = {} as any; + const key = await getKey({ workspaceId: workspaceId }); + + let folderId = "root"; + const folders = await Folder.findOne({ + workspace: workspaceId, + environment, + }); + + if (!folders && secretPath !== "/") { + throw InternalServerError({ message: "Folder not found" }); + } + + if (folders) { + const folder = getFolderByPath(folders.nodes, secretPath); + if (!folder) { + throw InternalServerError({ message: "Folder not found" }); + } + folderId = folder.id; + } + + const secrets = await Secret.find({ + workspace: workspaceId, + environment, + type: SECRET_SHARED, + folder: folderId, + }); + + secrets.forEach((secret: ISecret) => { + if(secret.secretCommentCiphertext && secret.secretCommentIV && secret.secretCommentTag) { + const secretKey = decryptSymmetric128BitHexKeyUTF8({ + ciphertext: secret.secretKeyCiphertext, + iv: secret.secretKeyIV, + tag: secret.secretKeyTag, + key, + }); + + const commentValue = decryptSymmetric128BitHexKeyUTF8({ + ciphertext: secret.secretCommentCiphertext, + iv: secret.secretCommentIV, + tag: secret.secretCommentTag, + key, + }); + + content[secretKey] = commentValue; + } + }); + + return content; +} \ No newline at end of file diff --git a/backend/src/helpers/integration.ts b/backend/src/helpers/integration.ts index 552e91859..196078533 100644 --- a/backend/src/helpers/integration.ts +++ b/backend/src/helpers/integration.ts @@ -123,7 +123,7 @@ export const syncIntegrationsHelper = async ({ ? { environment, } - : {}), + : {}), isActive: true, app: { $ne: null }, }); @@ -133,17 +133,24 @@ export const syncIntegrationsHelper = async ({ for await (const integration of integrations) { // get workspace, environment (shared) secrets const secrets = await BotService.getSecrets({ - // issue here? workspaceId: integration.workspace, environment: integration.environment, secretPath: integration.secretPath, }); + // get workspace, environment (shared) secrets comments + const secretComments = await BotService.getSecretComments({ + workspaceId: integration.workspace, + environment: integration.environment, + secretPath: integration.secretPath, + }) + const integrationAuth = await IntegrationAuth.findById( integration.integrationAuth ); - if (!integrationAuth) throw new Error("Failed to find integration auth"); + if (!integrationAuth) throw new Error("Failed to find integration auth"); + // get integration auth access token const access = await getIntegrationAuthAccessHelper({ integrationAuthId: integration.integrationAuth, @@ -156,6 +163,7 @@ export const syncIntegrationsHelper = async ({ secrets, accessId: access.accessId === undefined ? null : access.accessId, accessToken: access.accessToken, + secretComments }); } } catch (err) { diff --git a/backend/src/helpers/secrets.ts b/backend/src/helpers/secrets.ts index c770e78fb..04275ed32 100644 --- a/backend/src/helpers/secrets.ts +++ b/backend/src/helpers/secrets.ts @@ -346,6 +346,7 @@ export const createSecretHelper = async ({ workspace: new Types.ObjectId(workspaceId), folder: folderId, type, + environment, ...(type === SECRET_PERSONAL ? getAuthDataPayloadUserObj(authData) : {}) }); @@ -362,6 +363,7 @@ export const createSecretHelper = async ({ secretBlindIndex, folder: folderId, workspace: new Types.ObjectId(workspaceId), + environment, type: SECRET_SHARED }); diff --git a/backend/src/integrations/apps.ts b/backend/src/integrations/apps.ts index fbbc1b759..0e5a9bf33 100644 --- a/backend/src/integrations/apps.ts +++ b/backend/src/integrations/apps.ts @@ -40,7 +40,9 @@ import { INTEGRATION_TRAVISCI, INTEGRATION_TRAVISCI_API_URL, INTEGRATION_VERCEL, - INTEGRATION_VERCEL_API_URL + INTEGRATION_VERCEL_API_URL, + INTEGRATION_WINDMILL, + INTEGRATION_WINDMILL_API_URL, } from "../variables"; import { IIntegrationAuth } from "../models"; import { Octokit } from "@octokit/rest"; @@ -181,6 +183,11 @@ const getApps = async ({ accessToken, }); break; + case INTEGRATION_WINDMILL: + apps = await getAppsWindmill({ + accessToken + }); + break; case INTEGRATION_DIGITAL_OCEAN_APP_PLATFORM: apps = await getAppsDigitalOceanAppPlatform({ accessToken @@ -941,6 +948,106 @@ const getAppsCodefresh = async ({ }; +/** + * Return list of projects for Windmill integration + * @param {Object} obj + * @param {String} obj.accessToken - access token for Windmill API + * @returns {Object[]} apps - names of Windmill workspaces + * @returns {String} apps.name - name of Windmill workspace + */ +const getAppsWindmill = async ({ accessToken }: { accessToken: string }) => { + const { data } = await standardRequest.get( + `${INTEGRATION_WINDMILL_API_URL}/workspaces/list`, + { + headers: { + Authorization: `Bearer ${accessToken}`, + "Accept-Encoding": "application/json", + }, + } + ); + + // check for write access of secrets in windmill workspaces + const writeAccessCheck = data.map(async (app: any) => { + try { + const userPath = "u/user/variable"; + const folderPath = "f/folder/variable"; + + const { data: writeUser } = await standardRequest.post( + `${INTEGRATION_WINDMILL_API_URL}/w/${app.id}/variables/create`, + { + path: userPath, + value: "variable", + is_secret: true, + description: "variable description" + }, + { + headers: { + Authorization: `Bearer ${accessToken}`, + "Accept-Encoding": "application/json", + }, + } + ); + + const { data: writeFolder } = await standardRequest.post( + `${INTEGRATION_WINDMILL_API_URL}/w/${app.id}/variables/create`, + { + path: folderPath, + value: "variable", + is_secret: true, + description: "variable description" + }, + { + headers: { + Authorization: `Bearer ${accessToken}`, + "Accept-Encoding": "application/json", + }, + } + ); + + // is write access is allowed then delete the created secrets from workspace + if (writeUser && writeFolder) { + await standardRequest.delete( + `${INTEGRATION_WINDMILL_API_URL}/w/${app.id}/variables/delete/${userPath}`, + { + headers: { + Authorization: `Bearer ${accessToken}`, + "Accept-Encoding": "application/json", + }, + } + ); + + await standardRequest.delete( + `${INTEGRATION_WINDMILL_API_URL}/w/${app.id}/variables/delete/${folderPath}`, + { + headers: { + Authorization: `Bearer ${accessToken}`, + "Accept-Encoding": "application/json", + }, + } + ); + + return app; + } else { + return { error: "cannot write secret" }; + } + } catch (err: any) { + return { error: err.message }; + } + }); + + const appsWriteResponses = await Promise.all(writeAccessCheck); + const appsWithWriteAccess = appsWriteResponses.filter((appRes: any) => !appRes.error); + + const apps = appsWithWriteAccess.map((a: any) => { + return { + name: a.name, + appId: a.id, + }; + }); + + return apps; +} + /** * Return list of applications for DigitalOcean App Platform integration * @param {Object} obj diff --git a/backend/src/integrations/sync.ts b/backend/src/integrations/sync.ts index 8839c4449..7aa74112f 100644 --- a/backend/src/integrations/sync.ts +++ b/backend/src/integrations/sync.ts @@ -49,7 +49,9 @@ import { INTEGRATION_TRAVISCI, INTEGRATION_TRAVISCI_API_URL, INTEGRATION_VERCEL, - INTEGRATION_VERCEL_API_URL + INTEGRATION_VERCEL_API_URL, + INTEGRATION_WINDMILL, + INTEGRATION_WINDMILL_API_URL, } from "../variables"; import AWS from "aws-sdk"; import { Octokit } from "@octokit/rest"; @@ -65,19 +67,22 @@ import { standardRequest } from "../config/request"; * @param {Object} obj.secrets - secrets to push to integration (object where keys are secret keys and values are secret values) * @param {String} obj.accessId - access id for integration * @param {String} obj.accessToken - access token for integration + * @param {Object} obj.secretComments - secret comments to push to integration (object where keys are secret keys and values are comment values) */ const syncSecrets = async ({ integration, integrationAuth, secrets, accessId, - accessToken + accessToken, + secretComments }: { integration: IIntegration; integrationAuth: IIntegrationAuth; secrets: any; accessId: string | null; accessToken: string; + secretComments: any; }) => { switch (integration.integration) { case INTEGRATION_AZURE_KEY_VAULT: @@ -256,7 +261,15 @@ const syncSecrets = async ({ accessToken }); break; - } + case INTEGRATION_WINDMILL: + await syncSecretsWindmill({ + integration, + secrets, + accessToken, + secretComments + }); + break; + } }; /** @@ -2244,7 +2257,7 @@ const syncSecretsCodefresh = async ({ * @param {IIntegration} obj.integration - integration details * @param {IIntegrationAuth} obj.integrationAuth - integration auth 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 - personal access token for DigitalOcean + * @param {String} obj.accessToken - access token for integration */ const syncSecretsDigitalOceanAppPlatform = async ({ integration, @@ -2272,6 +2285,114 @@ const syncSecretsDigitalOceanAppPlatform = async ({ ); } +/** + * Sync/push [secrets] to Windmill with name [integration.app] + * @param {Object} obj + * @param {IIntegration} obj.integration - integration details + * @param {IIntegrationAuth} obj.integrationAuth - integration auth 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 windmill integration + * @param {Object} obj.secretComments - secret comments to push to integration (object where keys are secret keys and values are comment values) + */ +const syncSecretsWindmill = async ({ + integration, + secrets, + accessToken, + secretComments +}: { + integration: IIntegration; + secrets: any; + accessToken: string; + secretComments: any; +}) => { + interface WindmillSecret { + path: string; + value: string; + is_secret: boolean; + description?: string; + } + + // get secrets stored in windmill workspace + const res = (await standardRequest.get( + `${INTEGRATION_WINDMILL_API_URL}/w/${integration.appId}/variables/list`, + { + headers: { + Authorization: `Bearer ${accessToken}`, + "Accept-Encoding": "application/json", + }, + } + )) + .data + .reduce( + (obj: any, secret: WindmillSecret) => ({ + ...obj, + [secret.path]: secret + }), + {} + ); + + // eslint-disable-next-line no-useless-escape + const pattern = new RegExp("^(u\/|f\/)[a-zA-Z0-9_-]+\/([a-zA-Z0-9_-]+\/)*[a-zA-Z0-9_-]*[^\/]$"); + + for await (const key of Object.keys(secrets)) { + if((key.startsWith("u/") || key.startsWith("f/")) && pattern.test(key)) { + if(!(key in res)) { + // case: secret does not exist in windmill + // -> create secret + + await standardRequest.post( + `${INTEGRATION_WINDMILL_API_URL}/w/${integration.appId}/variables/create`, + { + path: key, + value: secrets[key], + is_secret: true, + description: secretComments[key] || "" + }, + { + headers: { + Authorization: `Bearer ${accessToken}`, + "Accept-Encoding": "application/json", + }, + } + ); + } else { + // -> update secret + await standardRequest.post( + `${INTEGRATION_WINDMILL_API_URL}/w/${integration.appId}/variables/update/${res[key].path}`, + { + path: key, + value: secrets[key], + is_secret: true, + description: secretComments[key] || "" + }, + { + headers: { + Authorization: `Bearer ${accessToken}`, + "Accept-Encoding": "application/json", + }, + } + ); + } + } + } + + for await (const key of Object.keys(res)) { + if (!(key in secrets)) { + // -> delete secret + await standardRequest.delete( + `${INTEGRATION_WINDMILL_API_URL}/w/${integration.appId}/variables/delete/${res[key].path}`, + { + headers: { + Authorization: `Bearer ${accessToken}`, + "Content-Type": "application/json", + "Accept-Encoding": "application/json", + } + } + ); + } + } +} + /** * Sync/push [secrets] to Cloud66 application with name [integration.app] * @param {Object} obj diff --git a/backend/src/models/integration.ts b/backend/src/models/integration.ts index d56c603e5..e6673e77e 100644 --- a/backend/src/models/integration.ts +++ b/backend/src/models/integration.ts @@ -22,7 +22,8 @@ import { INTEGRATION_SUPABASE, INTEGRATION_TERRAFORM_CLOUD, INTEGRATION_TRAVISCI, - INTEGRATION_VERCEL + INTEGRATION_VERCEL, + INTEGRATION_WINDMILL } from "../variables"; import { Schema, Types, model } from "mongoose"; @@ -67,6 +68,7 @@ export interface IIntegration { | "digital-ocean-app-platform" | "cloud-66" | "northflank" + | "windmill"; integrationAuth: Types.ObjectId; } @@ -157,9 +159,10 @@ const integrationSchema = new Schema( INTEGRATION_TERRAFORM_CLOUD, INTEGRATION_HASHICORP_VAULT, INTEGRATION_CLOUDFLARE_PAGES, + INTEGRATION_CODEFRESH, + INTEGRATION_WINDMILL, INTEGRATION_BITBUCKET, INTEGRATION_DIGITAL_OCEAN_APP_PLATFORM, - INTEGRATION_CODEFRESH, INTEGRATION_CLOUD_66, INTEGRATION_NORTHFLANK ], diff --git a/backend/src/models/integrationAuth.ts b/backend/src/models/integrationAuth.ts index dd8d0cd19..6791868cd 100644 --- a/backend/src/models/integrationAuth.ts +++ b/backend/src/models/integrationAuth.ts @@ -24,7 +24,8 @@ import { INTEGRATION_SUPABASE, INTEGRATION_TERRAFORM_CLOUD, INTEGRATION_TRAVISCI, - INTEGRATION_VERCEL + INTEGRATION_VERCEL, + INTEGRATION_WINDMILL } from "../variables"; import { Document, Schema, Types, model } from "mongoose"; @@ -54,7 +55,8 @@ export interface IIntegrationAuth extends Document { | "bitbucket" | "cloud-66" | "terraform-cloud" - | "northflank"; + | "northflank" + | "windmill"; teamId: string; accountId: string; url: string; @@ -101,9 +103,10 @@ const integrationAuthSchema = new Schema( INTEGRATION_TERRAFORM_CLOUD, INTEGRATION_HASHICORP_VAULT, INTEGRATION_CLOUDFLARE_PAGES, + INTEGRATION_CODEFRESH, + INTEGRATION_WINDMILL, INTEGRATION_BITBUCKET, INTEGRATION_DIGITAL_OCEAN_APP_PLATFORM, - INTEGRATION_CODEFRESH, INTEGRATION_CLOUD_66, INTEGRATION_NORTHFLANK ], diff --git a/backend/src/services/BotService.ts b/backend/src/services/BotService.ts index ca31bf103..ca75985d2 100644 --- a/backend/src/services/BotService.ts +++ b/backend/src/services/BotService.ts @@ -5,6 +5,7 @@ import { getIsWorkspaceE2EEHelper, getKey, getSecretsBotHelper, + getSecretsCommentBotHelper, } from "../helpers/bot"; /** @@ -107,6 +108,30 @@ class BotService { tag, }); } + + /** + * Return decrypted secret comments for workspace with id [worskpaceId] and + * environment [environment] shared to bot. + * @param {Object} obj + * @param {String} obj.workspaceId - id of workspace of secrets + * @param {String} obj.environment - environment for secrets + * @returns {Object} secretObj - object where keys are secret keys and values are comments + */ + static async getSecretComments({ + workspaceId, + environment, + secretPath + }: { + workspaceId: Types.ObjectId; + environment: string; + secretPath: string; + }) { + return await getSecretsCommentBotHelper({ + workspaceId, + environment, + secretPath + }); + } } export default BotService; diff --git a/backend/src/variables/integration.ts b/backend/src/variables/integration.ts index 42066e3e5..a649763c0 100644 --- a/backend/src/variables/integration.ts +++ b/backend/src/variables/integration.ts @@ -30,6 +30,7 @@ export const INTEGRATION_HASHICORP_VAULT = "hashicorp-vault"; export const INTEGRATION_CLOUDFLARE_PAGES = "cloudflare-pages"; export const INTEGRATION_BITBUCKET = "bitbucket"; export const INTEGRATION_CODEFRESH = "codefresh"; +export const INTEGRATION_WINDMILL = "windmill"; export const INTEGRATION_DIGITAL_OCEAN_APP_PLATFORM = "digital-ocean-app-platform"; export const INTEGRATION_CLOUD_66 = "cloud-66"; export const INTEGRATION_NORTHFLANK = "northflank"; @@ -50,9 +51,10 @@ export const INTEGRATION_SET = new Set([ INTEGRATION_TERRAFORM_CLOUD, INTEGRATION_HASHICORP_VAULT, INTEGRATION_CLOUDFLARE_PAGES, + INTEGRATION_CODEFRESH, + INTEGRATION_WINDMILL, INTEGRATION_BITBUCKET, INTEGRATION_DIGITAL_OCEAN_APP_PLATFORM, - INTEGRATION_CODEFRESH, INTEGRATION_CLOUD_66, INTEGRATION_NORTHFLANK ]); @@ -88,6 +90,7 @@ export const INTEGRATION_TERRAFORM_CLOUD_API_URL = "https://app.terraform.io"; export const INTEGRATION_CLOUDFLARE_PAGES_API_URL = "https://api.cloudflare.com"; export const INTEGRATION_BITBUCKET_API_URL = "https://api.bitbucket.org"; export const INTEGRATION_CODEFRESH_API_URL = "https://g.codefresh.io/api"; +export const INTEGRATION_WINDMILL_API_URL = "https://app.windmill.dev/api"; export const INTEGRATION_DIGITAL_OCEAN_API_URL = "https://api.digitalocean.com"; export const INTEGRATION_CLOUD_66_API_URL = "https://app.cloud66.com/api"; export const INTEGRATION_NORTHFLANK_API_URL = "https://api.northflank.com"; @@ -293,6 +296,15 @@ export const getIntegrationOptions = async () => { clientId: "", docsLink: "", }, + { + name: "Windmill", + slug: "windmill", + image: "Windmill.png", + isAvailable: true, + type: "pat", + clientId: "", + docsLink: "", + }, { name: "Digital Ocean App Platform", slug: "digital-ocean-app-platform", diff --git a/docs/images/integrations-windmill-auth.png b/docs/images/integrations-windmill-auth.png new file mode 100644 index 000000000..50d60dabe Binary files /dev/null and b/docs/images/integrations-windmill-auth.png differ diff --git a/docs/images/integrations-windmill-create.png b/docs/images/integrations-windmill-create.png new file mode 100644 index 000000000..105d5aadb Binary files /dev/null and b/docs/images/integrations-windmill-create.png differ diff --git a/docs/images/integrations-windmill-dashboard.png b/docs/images/integrations-windmill-dashboard.png new file mode 100644 index 000000000..8664eb013 Binary files /dev/null and b/docs/images/integrations-windmill-dashboard.png differ diff --git a/docs/images/integrations-windmill-token.png b/docs/images/integrations-windmill-token.png new file mode 100644 index 000000000..b943a5268 Binary files /dev/null and b/docs/images/integrations-windmill-token.png differ diff --git a/docs/images/integrations-windmill.png b/docs/images/integrations-windmill.png new file mode 100644 index 000000000..68e5f9016 Binary files /dev/null and b/docs/images/integrations-windmill.png differ diff --git a/docs/images/integrations.png b/docs/images/integrations.png index af2a45125..5ad31d1d1 100644 Binary files a/docs/images/integrations.png and b/docs/images/integrations.png differ diff --git a/docs/integrations/cloud/windmill.mdx b/docs/integrations/cloud/windmill.mdx new file mode 100644 index 000000000..594d483b0 --- /dev/null +++ b/docs/integrations/cloud/windmill.mdx @@ -0,0 +1,46 @@ +--- +title: "Windmill" +description: "How to sync secrets from Infisical to Windmill" +--- + +Prerequisites: + +- Set up and add envars to [Infisical Cloud](https://app.infisical.com) + +## Navigate to your project's integrations tab + +![integrations](../../images/integrations.png) + +## Enter your Windmill Access Token + +Obtain a Windmill access token in Access Tokens + +![integrations windmill dashboard](../../images/integrations-windmill-dashboard.png) +![integrations windmill token](../../images/integrations-windmill-token.png) + +Press on the Windmill tile and input your Windmill access token to grant Infisical access to your Windmill account. + +![integrations windmill authorization](../../images/integrations-windmill-auth.png) + + + If this is your project's first cloud integration, then you'll have to grant + Infisical access to your project's environment variables. Although this step + breaks E2EE, it's necessary for Infisical to sync the environment variables to + the cloud platform. + + +## Start integration + +Select which Infisical environment secrets you want to sync to which Windmill workspace and press create integration to start syncing secrets to Windmill. + +![integrations windmill](../../images/integrations-windmill-create.png) +![integrations windmill](../../images/integrations-windmill.png) + + + Secrets synced to Windmill are subject to the [ownership path + prefix](https://www.windmill.dev/docs/core_concepts/roles_and_permissions) + convention of Windmill. Accordingly, all secrets must be prefixed with either + `u/` or `f/` for user-based and folder-based secret along with the name of the + secret. Put differently, you must use the full path of the secret as its name + in Infisical to be considered valid such as `u/user/FOO/BAR`. + diff --git a/docs/integrations/overview.mdx b/docs/integrations/overview.mdx index 95e77d78a..067f7e039 100644 --- a/docs/integrations/overview.mdx +++ b/docs/integrations/overview.mdx @@ -30,6 +30,7 @@ Missing an integration? [Throw in a request](https://github.com/Infisical/infisi | [AWS Parameter Store](/integrations/cloud/aws-parameter-store) | Cloud | Available | | [AWS Secret Manager](/integrations/cloud/aws-secret-manager) | Cloud | Available | | [Azure Key Vault](/integrations/cloud/azure-key-vault) | Cloud | Available | +| [Windmill](/integrations/cloud/windmill) | Cloud | Available | | [BitBucket](/integrations/cicd/bitbucket) | CI/CD | Available | | [Codefresh](/integrations/cicd/codefresh) | CI/CD | Available | | [GitHub Actions](/integrations/cicd/githubactions) | CI/CD | Available | diff --git a/docs/mint.json b/docs/mint.json index ebe0e7786..c229faed4 100644 --- a/docs/mint.json +++ b/docs/mint.json @@ -237,6 +237,7 @@ "integrations/cloud/hashicorp-vault", "integrations/cloud/azure-key-vault", "integrations/cloud/cloud-66", + "integrations/cloud/windmill", "integrations/cicd/githubactions", "integrations/cicd/gitlab", "integrations/cicd/circleci", diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 5853ff650..d25ff5cf1 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -67,6 +67,7 @@ "react": "^17.0.2", "react-beautiful-dnd": "^13.1.1", "react-code-input": "^3.10.1", + "react-contenteditable": "^3.3.7", "react-dom": "^17.0.2", "react-grid-layout": "^1.3.4", "react-hook-form": "^7.43.0", @@ -75,6 +76,7 @@ "react-markdown": "^8.0.3", "react-redux": "^8.0.2", "react-table": "^7.8.0", + "sanitize-html": "^2.11.0", "set-cookie-parser": "^2.5.1", "sharp": "^0.32.0", "styled-components": "^5.3.7", @@ -99,6 +101,7 @@ "@types/jsrp": "^0.2.4", "@types/node": "18.11.9", "@types/react": "^18.0.26", + "@types/sanitize-html": "^2.9.0", "@typescript-eslint/eslint-plugin": "^5.48.1", "@typescript-eslint/parser": "^5.45.0", "autoprefixer": "^10.4.7", @@ -7922,6 +7925,89 @@ "redux": "^4.0.0" } }, + "node_modules/@types/sanitize-html": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/@types/sanitize-html/-/sanitize-html-2.9.0.tgz", + "integrity": "sha512-4fP/kEcKNj2u39IzrxWYuf/FnCCwwQCpif6wwY6ROUS1EPRIfWJjGkY3HIowY1EX/VbX5e86yq8AAE7UPMgATg==", + "dev": true, + "dependencies": { + "htmlparser2": "^8.0.0" + } + }, + "node_modules/@types/sanitize-html/node_modules/dom-serializer": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-2.0.0.tgz", + "integrity": "sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==", + "dev": true, + "dependencies": { + "domelementtype": "^2.3.0", + "domhandler": "^5.0.2", + "entities": "^4.2.0" + }, + "funding": { + "url": "https://github.com/cheeriojs/dom-serializer?sponsor=1" + } + }, + "node_modules/@types/sanitize-html/node_modules/domhandler": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-5.0.3.tgz", + "integrity": "sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==", + "dev": true, + "dependencies": { + "domelementtype": "^2.3.0" + }, + "engines": { + "node": ">= 4" + }, + "funding": { + "url": "https://github.com/fb55/domhandler?sponsor=1" + } + }, + "node_modules/@types/sanitize-html/node_modules/domutils": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/domutils/-/domutils-3.1.0.tgz", + "integrity": "sha512-H78uMmQtI2AhgDJjWeQmHwJJ2bLPD3GMmO7Zja/ZZh84wkm+4ut+IUnUdRa8uCGX88DiVx1j6FRe1XfxEgjEZA==", + "dev": true, + "dependencies": { + "dom-serializer": "^2.0.0", + "domelementtype": "^2.3.0", + "domhandler": "^5.0.3" + }, + "funding": { + "url": "https://github.com/fb55/domutils?sponsor=1" + } + }, + "node_modules/@types/sanitize-html/node_modules/entities": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", + "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", + "dev": true, + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/@types/sanitize-html/node_modules/htmlparser2": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-8.0.2.tgz", + "integrity": "sha512-GYdjWKDkbRLkZ5geuHs5NY1puJ+PXwP7+fHPRz06Eirsb9ugf6d8kkXav6ADhcODhFFPMIXyxkxSuMf3D6NCFA==", + "dev": true, + "funding": [ + "https://github.com/fb55/htmlparser2?sponsor=1", + { + "type": "github", + "url": "https://github.com/sponsors/fb55" + } + ], + "dependencies": { + "domelementtype": "^2.3.0", + "domhandler": "^5.0.3", + "domutils": "^3.0.1", + "entities": "^4.4.0" + } + }, "node_modules/@types/scheduler": { "version": "0.16.3", "resolved": "https://registry.npmjs.org/@types/scheduler/-/scheduler-0.16.3.tgz", @@ -10745,7 +10831,6 @@ "version": "4.3.1", "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz", "integrity": "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==", - "dev": true, "engines": { "node": ">=0.10.0" } @@ -11203,7 +11288,6 @@ "version": "2.3.0", "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.3.0.tgz", "integrity": "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==", - "dev": true, "funding": [ { "type": "github", @@ -12690,8 +12774,7 @@ "node_modules/fast-deep-equal": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", - "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", - "dev": true + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==" }, "node_modules/fast-diff": { "version": "1.3.0", @@ -17860,6 +17943,11 @@ "node": ">= 0.10" } }, + "node_modules/parse-srcset": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/parse-srcset/-/parse-srcset-1.0.2.tgz", + "integrity": "sha512-/2qh0lav6CmI15FzA3i/2Bzk2zCgQhGMkvhOhKNcBVQ1ldgpbfiNTVslmooUmWJcADi1f1kIeynbDRVzNlfR6Q==" + }, "node_modules/parseurl": { "version": "1.3.3", "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", @@ -18113,7 +18201,6 @@ "version": "8.4.23", "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.23.tgz", "integrity": "sha512-bQ3qMcpF6A/YjR55xtoTr0jGOlnPOKAIMdOWiv0EIT6HVPEaJiJB4NLljSbiHoC2RX7DN5Uvjtpbg1NPdwv1oA==", - "dev": true, "funding": [ { "type": "opencollective", @@ -18936,6 +19023,18 @@ "react-dom": ">=16.8.0" } }, + "node_modules/react-contenteditable": { + "version": "3.3.7", + "resolved": "https://registry.npmjs.org/react-contenteditable/-/react-contenteditable-3.3.7.tgz", + "integrity": "sha512-GA9NbC0DkDdpN3iGvib/OMHWTJzDX2cfkgy5Tt98JJAbA3kLnyrNbBIpsSpPpq7T8d3scD39DHP+j8mAM7BIfQ==", + "dependencies": { + "fast-deep-equal": "^3.1.3", + "prop-types": "^15.7.1" + }, + "peerDependencies": { + "react": ">=16.3" + } + }, "node_modules/react-docgen": { "version": "5.4.3", "resolved": "https://registry.npmjs.org/react-docgen/-/react-docgen-5.4.3.tgz", @@ -20017,6 +20116,88 @@ "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", "dev": true }, + "node_modules/sanitize-html": { + "version": "2.11.0", + "resolved": "https://registry.npmjs.org/sanitize-html/-/sanitize-html-2.11.0.tgz", + "integrity": "sha512-BG68EDHRaGKqlsNjJ2xUB7gpInPA8gVx/mvjO743hZaeMCZ2DwzW7xvsqZ+KNU4QKwj86HJ3uu2liISf2qBBUA==", + "dependencies": { + "deepmerge": "^4.2.2", + "escape-string-regexp": "^4.0.0", + "htmlparser2": "^8.0.0", + "is-plain-object": "^5.0.0", + "parse-srcset": "^1.0.2", + "postcss": "^8.3.11" + } + }, + "node_modules/sanitize-html/node_modules/dom-serializer": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-2.0.0.tgz", + "integrity": "sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==", + "dependencies": { + "domelementtype": "^2.3.0", + "domhandler": "^5.0.2", + "entities": "^4.2.0" + }, + "funding": { + "url": "https://github.com/cheeriojs/dom-serializer?sponsor=1" + } + }, + "node_modules/sanitize-html/node_modules/domhandler": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-5.0.3.tgz", + "integrity": "sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==", + "dependencies": { + "domelementtype": "^2.3.0" + }, + "engines": { + "node": ">= 4" + }, + "funding": { + "url": "https://github.com/fb55/domhandler?sponsor=1" + } + }, + "node_modules/sanitize-html/node_modules/domutils": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/domutils/-/domutils-3.1.0.tgz", + "integrity": "sha512-H78uMmQtI2AhgDJjWeQmHwJJ2bLPD3GMmO7Zja/ZZh84wkm+4ut+IUnUdRa8uCGX88DiVx1j6FRe1XfxEgjEZA==", + "dependencies": { + "dom-serializer": "^2.0.0", + "domelementtype": "^2.3.0", + "domhandler": "^5.0.3" + }, + "funding": { + "url": "https://github.com/fb55/domutils?sponsor=1" + } + }, + "node_modules/sanitize-html/node_modules/entities": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", + "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/sanitize-html/node_modules/htmlparser2": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-8.0.2.tgz", + "integrity": "sha512-GYdjWKDkbRLkZ5geuHs5NY1puJ+PXwP7+fHPRz06Eirsb9ugf6d8kkXav6ADhcODhFFPMIXyxkxSuMf3D6NCFA==", + "funding": [ + "https://github.com/fb55/htmlparser2?sponsor=1", + { + "type": "github", + "url": "https://github.com/sponsors/fb55" + } + ], + "dependencies": { + "domelementtype": "^2.3.0", + "domhandler": "^5.0.3", + "domutils": "^3.0.1", + "entities": "^4.4.0" + } + }, "node_modules/sass-loader": { "version": "12.6.0", "resolved": "https://registry.npmjs.org/sass-loader/-/sass-loader-12.6.0.tgz", @@ -28425,6 +28606,66 @@ "redux": "^4.0.0" } }, + "@types/sanitize-html": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/@types/sanitize-html/-/sanitize-html-2.9.0.tgz", + "integrity": "sha512-4fP/kEcKNj2u39IzrxWYuf/FnCCwwQCpif6wwY6ROUS1EPRIfWJjGkY3HIowY1EX/VbX5e86yq8AAE7UPMgATg==", + "dev": true, + "requires": { + "htmlparser2": "^8.0.0" + }, + "dependencies": { + "dom-serializer": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-2.0.0.tgz", + "integrity": "sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==", + "dev": true, + "requires": { + "domelementtype": "^2.3.0", + "domhandler": "^5.0.2", + "entities": "^4.2.0" + } + }, + "domhandler": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-5.0.3.tgz", + "integrity": "sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==", + "dev": true, + "requires": { + "domelementtype": "^2.3.0" + } + }, + "domutils": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/domutils/-/domutils-3.1.0.tgz", + "integrity": "sha512-H78uMmQtI2AhgDJjWeQmHwJJ2bLPD3GMmO7Zja/ZZh84wkm+4ut+IUnUdRa8uCGX88DiVx1j6FRe1XfxEgjEZA==", + "dev": true, + "requires": { + "dom-serializer": "^2.0.0", + "domelementtype": "^2.3.0", + "domhandler": "^5.0.3" + } + }, + "entities": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", + "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", + "dev": true + }, + "htmlparser2": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-8.0.2.tgz", + "integrity": "sha512-GYdjWKDkbRLkZ5geuHs5NY1puJ+PXwP7+fHPRz06Eirsb9ugf6d8kkXav6ADhcODhFFPMIXyxkxSuMf3D6NCFA==", + "dev": true, + "requires": { + "domelementtype": "^2.3.0", + "domhandler": "^5.0.3", + "domutils": "^3.0.1", + "entities": "^4.4.0" + } + } + } + }, "@types/scheduler": { "version": "0.16.3", "resolved": "https://registry.npmjs.org/@types/scheduler/-/scheduler-0.16.3.tgz", @@ -30633,8 +30874,7 @@ "deepmerge": { "version": "4.3.1", "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz", - "integrity": "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==", - "dev": true + "integrity": "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==" }, "default-browser": { "version": "4.0.0", @@ -30963,8 +31203,7 @@ "domelementtype": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.3.0.tgz", - "integrity": "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==", - "dev": true + "integrity": "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==" }, "domhandler": { "version": "4.3.1", @@ -32126,8 +32365,7 @@ "fast-deep-equal": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", - "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", - "dev": true + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==" }, "fast-diff": { "version": "1.3.0", @@ -35865,6 +36103,11 @@ "dev": true, "peer": true }, + "parse-srcset": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/parse-srcset/-/parse-srcset-1.0.2.tgz", + "integrity": "sha512-/2qh0lav6CmI15FzA3i/2Bzk2zCgQhGMkvhOhKNcBVQ1ldgpbfiNTVslmooUmWJcADi1f1kIeynbDRVzNlfR6Q==" + }, "parseurl": { "version": "1.3.3", "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", @@ -36081,7 +36324,6 @@ "version": "8.4.23", "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.23.tgz", "integrity": "sha512-bQ3qMcpF6A/YjR55xtoTr0jGOlnPOKAIMdOWiv0EIT6HVPEaJiJB4NLljSbiHoC2RX7DN5Uvjtpbg1NPdwv1oA==", - "dev": true, "requires": { "nanoid": "^3.3.6", "picocolors": "^1.0.0", @@ -36653,6 +36895,15 @@ "dev": true, "requires": {} }, + "react-contenteditable": { + "version": "3.3.7", + "resolved": "https://registry.npmjs.org/react-contenteditable/-/react-contenteditable-3.3.7.tgz", + "integrity": "sha512-GA9NbC0DkDdpN3iGvib/OMHWTJzDX2cfkgy5Tt98JJAbA3kLnyrNbBIpsSpPpq7T8d3scD39DHP+j8mAM7BIfQ==", + "requires": { + "fast-deep-equal": "^3.1.3", + "prop-types": "^15.7.1" + } + }, "react-docgen": { "version": "5.4.3", "resolved": "https://registry.npmjs.org/react-docgen/-/react-docgen-5.4.3.tgz", @@ -37421,6 +37672,65 @@ "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", "dev": true }, + "sanitize-html": { + "version": "2.11.0", + "resolved": "https://registry.npmjs.org/sanitize-html/-/sanitize-html-2.11.0.tgz", + "integrity": "sha512-BG68EDHRaGKqlsNjJ2xUB7gpInPA8gVx/mvjO743hZaeMCZ2DwzW7xvsqZ+KNU4QKwj86HJ3uu2liISf2qBBUA==", + "requires": { + "deepmerge": "^4.2.2", + "escape-string-regexp": "^4.0.0", + "htmlparser2": "^8.0.0", + "is-plain-object": "^5.0.0", + "parse-srcset": "^1.0.2", + "postcss": "^8.3.11" + }, + "dependencies": { + "dom-serializer": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-2.0.0.tgz", + "integrity": "sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==", + "requires": { + "domelementtype": "^2.3.0", + "domhandler": "^5.0.2", + "entities": "^4.2.0" + } + }, + "domhandler": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-5.0.3.tgz", + "integrity": "sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==", + "requires": { + "domelementtype": "^2.3.0" + } + }, + "domutils": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/domutils/-/domutils-3.1.0.tgz", + "integrity": "sha512-H78uMmQtI2AhgDJjWeQmHwJJ2bLPD3GMmO7Zja/ZZh84wkm+4ut+IUnUdRa8uCGX88DiVx1j6FRe1XfxEgjEZA==", + "requires": { + "dom-serializer": "^2.0.0", + "domelementtype": "^2.3.0", + "domhandler": "^5.0.3" + } + }, + "entities": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", + "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==" + }, + "htmlparser2": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-8.0.2.tgz", + "integrity": "sha512-GYdjWKDkbRLkZ5geuHs5NY1puJ+PXwP7+fHPRz06Eirsb9ugf6d8kkXav6ADhcODhFFPMIXyxkxSuMf3D6NCFA==", + "requires": { + "domelementtype": "^2.3.0", + "domhandler": "^5.0.3", + "domutils": "^3.0.1", + "entities": "^4.4.0" + } + } + } + }, "sass-loader": { "version": "12.6.0", "resolved": "https://registry.npmjs.org/sass-loader/-/sass-loader-12.6.0.tgz", diff --git a/frontend/package.json b/frontend/package.json index 0430844e7..51028a59b 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -75,6 +75,7 @@ "react": "^17.0.2", "react-beautiful-dnd": "^13.1.1", "react-code-input": "^3.10.1", + "react-contenteditable": "^3.3.7", "react-dom": "^17.0.2", "react-grid-layout": "^1.3.4", "react-hook-form": "^7.43.0", @@ -83,6 +84,7 @@ "react-markdown": "^8.0.3", "react-redux": "^8.0.2", "react-table": "^7.8.0", + "sanitize-html": "^2.11.0", "set-cookie-parser": "^2.5.1", "sharp": "^0.32.0", "styled-components": "^5.3.7", @@ -107,6 +109,7 @@ "@types/jsrp": "^0.2.4", "@types/node": "18.11.9", "@types/react": "^18.0.26", + "@types/sanitize-html": "^2.9.0", "@typescript-eslint/eslint-plugin": "^5.48.1", "@typescript-eslint/parser": "^5.45.0", "autoprefixer": "^10.4.7", diff --git a/frontend/public/data/frequentConstants.ts b/frontend/public/data/frequentConstants.ts index f05c2ade0..3352999f6 100644 --- a/frontend/public/data/frequentConstants.ts +++ b/frontend/public/data/frequentConstants.ts @@ -26,8 +26,9 @@ const integrationSlugNameMapping: Mapping = { "digital-ocean-app-platform": "Digital Ocean App Platform", bitbucket: "BitBucket", "cloud-66": "Cloud 66", - northflank: "Northflank" -}; + northflank: "Northflank", + 'windmill': 'Windmill' +} const envMapping: Mapping = { Development: "dev", diff --git a/frontend/public/images/integrations/Windmill.png b/frontend/public/images/integrations/Windmill.png new file mode 100644 index 000000000..c4297077f Binary files /dev/null and b/frontend/public/images/integrations/Windmill.png differ diff --git a/frontend/src/components/navigation/NavHeader.tsx b/frontend/src/components/navigation/NavHeader.tsx index fac8f4749..d4a5c4352 100644 --- a/frontend/src/components/navigation/NavHeader.tsx +++ b/frontend/src/components/navigation/NavHeader.tsx @@ -58,15 +58,13 @@ export default function NavHeader({ return (
-
+
{currentOrg?.name?.charAt(0)}
- - {currentOrg?.name} + + + {currentOrg?.name} + {isProjectRelated && ( <> @@ -85,7 +83,7 @@ export default function NavHeader({ {pageName} @@ -126,7 +124,11 @@ export default function NavHeader({ {index + 1 === folders?.length ? ( {name} ) : ( - + {name === "root" ? selectedEnv?.name : name} diff --git a/frontend/src/components/utilities/parseDotEnv.ts b/frontend/src/components/utilities/parseDotEnv.ts index e9b14f0fe..683670b33 100644 --- a/frontend/src/components/utilities/parseDotEnv.ts +++ b/frontend/src/components/utilities/parseDotEnv.ts @@ -1,5 +1,5 @@ const LINE = - /(?:^|^)\s*(?:export\s+)?([\w.-]+)(?:\s*=\s*?|:\s+?)(\s*'(?:\\'|[^'])*'|\s*"(?:\\"|[^"])*"|\s*`(?:\\`|[^`])*`|[^#\r\n]+)?\s*(?:#.*)?(?:$|$)/gm; + /(?:^|^)\s*(?:export\s+)?([\w.-:]+)(?:\s*=\s*?|:\s+?)(\s*'(?:\\'|[^'])*'|\s*"(?:\\"|[^"])*"|\s*`(?:\\`|[^`])*`|[^#\r\n]+)?\s*(?:#.*)?(?:$|$)/gm; /** * Return text that is the buffer parsed diff --git a/frontend/src/components/v2/SecretInput/SecretInput.tsx b/frontend/src/components/v2/SecretInput/SecretInput.tsx new file mode 100644 index 000000000..8380eb4a1 --- /dev/null +++ b/frontend/src/components/v2/SecretInput/SecretInput.tsx @@ -0,0 +1,93 @@ +/* eslint-disable react/no-danger */ +import { HTMLAttributes } from "react"; +import ContentEditable from "react-contenteditable"; +import sanitizeHtml from "sanitize-html"; + +import { useToggle } from "@app/hooks"; + +const REGEX = /\${([^}]+)}/g; +const stripSpanTags = (str: string) => str.replace(/<\/?span[^>]*>/g, ""); +const replaceContentWithDot = (str: string) => { + let finalStr = ""; + let isHtml = false; + for (let i = 0; i < str.length; i += 1) { + const char = str.at(i); + + if (char === "<" || char === ">") { + isHtml = char === "<"; + finalStr += char; + } else if (!isHtml && char !== "\n") { + finalStr += "•"; + } else { + finalStr += char; + } + } + return finalStr; +}; + +const syntaxHighlight = (orgContent?: string | null, isVisible?: boolean) => { + if (orgContent === "") return "EMPTY"; + if (!orgContent) return "missing"; + if (!isVisible) return replaceContentWithDot(orgContent); + const content = stripSpanTags(orgContent); + const newContent = content.replace( + REGEX, + (_a, b) => + `${${b}}` + ); + + return newContent; +}; + +const sanitizeConf = { + allowedTags: ["div", "span", "br", "p"] +}; + +type Props = Omit, "onChange" | "onBlur"> & { + value?: string | null; + isVisible?: boolean; + isDisabled?: boolean; + onChange?: (val: string, html: string) => void; + onBlur?: (sanitizedHtml: string) => void; +}; + +export const SecretInput = ({ + value, + isVisible, + onChange, + onBlur, + isDisabled, + ...props +}: Props) => { + const [isSecretFocused, setIsSecretFocused] = useToggle(); + + return ( +
+
+ { + if (onChange) onChange(evt.currentTarget.innerText.trim(), evt.currentTarget.innerHTML); + }} + onFocus={() => setIsSecretFocused.on()} + disabled={isDisabled} + spellCheck={false} + onBlur={(evt) => { + if (onBlur) onBlur(sanitizeHtml(evt.currentTarget.innerHTML || "", sanitizeConf)); + setIsSecretFocused.off(); + }} + html={isVisible || isSecretFocused ? value || "" : syntaxHighlight(value, false)} + {...props} + /> +
+ ); +}; diff --git a/frontend/src/components/v2/SecretInput/index.tsx b/frontend/src/components/v2/SecretInput/index.tsx new file mode 100644 index 000000000..1c322fac4 --- /dev/null +++ b/frontend/src/components/v2/SecretInput/index.tsx @@ -0,0 +1 @@ +export { SecretInput } from "./SecretInput"; diff --git a/frontend/src/components/v2/Table/Table.stories.tsx b/frontend/src/components/v2/Table/Table.stories.tsx index a7b47e315..95be1bb05 100644 --- a/frontend/src/components/v2/Table/Table.stories.tsx +++ b/frontend/src/components/v2/Table/Table.stories.tsx @@ -52,7 +52,7 @@ export const Loading: Story = { - + diff --git a/frontend/src/components/v2/Table/Table.tsx b/frontend/src/components/v2/Table/Table.tsx index d6cd74e45..7c698e2bb 100644 --- a/frontend/src/components/v2/Table/Table.tsx +++ b/frontend/src/components/v2/Table/Table.tsx @@ -33,10 +33,7 @@ export type TableProps = { export const Table = ({ children, className }: TableProps): JSX.Element => ( {children}
@@ -58,11 +55,24 @@ export const THead = ({ children, className }: THeadProps): JSX.Element => ( export type TrProps = { children: ReactNode; className?: string; + isHoverable?: boolean; + isSelectable?: boolean; } & HTMLAttributes; -export const Tr = ({ children, className, ...props }: TrProps): JSX.Element => ( +export const Tr = ({ + children, + className, + isHoverable, + isSelectable, + ...props +}: TrProps): JSX.Element => ( {children} @@ -76,7 +86,14 @@ export type ThProps = { }; export const Th = ({ children, className }: ThProps): JSX.Element => ( - {children} + + {children} + ); // table body @@ -106,15 +123,15 @@ export type TBodyLoader = { columns: number; className?: string; // unique key for mapping - key: string; + innerKey: string; }; -export const TableSkeleton = ({ rows = 3, columns, key, className }: TBodyLoader): JSX.Element => ( +export const TableSkeleton = ({ rows = 3, columns, innerKey, className }: TBodyLoader): JSX.Element => ( <> {Array.apply(0, Array(rows)).map((_x, i) => ( - + {Array.apply(0, Array(columns)).map((_y, j) => ( - + ))} diff --git a/frontend/src/components/v2/Tooltip/Tooltip.tsx b/frontend/src/components/v2/Tooltip/Tooltip.tsx index 4dc9fcbd8..f6f697f38 100644 --- a/frontend/src/components/v2/Tooltip/Tooltip.tsx +++ b/frontend/src/components/v2/Tooltip/Tooltip.tsx @@ -23,7 +23,7 @@ export const Tooltip = ({ ...props }: TooltipProps) => ( { + const folders = useQueries({ + queries: environments.map((env) => ({ + queryKey: queryKeys.getSecretFolders(workspaceId, env, parentFolderPath || parentFolderId), + queryFn: async () => fetchProjectFolders(workspaceId, env, parentFolderId, parentFolderPath), + enabled: Boolean(workspaceId) && Boolean(env) + })) + }); + + const folderNames = useMemo(() => { + const names = new Set(); + folders?.forEach(({ data }) => { + data?.folders.forEach(({ name }) => { + names.add(name); + }); + }); + return [...names]; + }, [(folders || []).map((folder) => folder.data)]); + + const isFolderPresentInEnv = useCallback( + (name: string, env: string) => { + const selectedEnvIndex = environments.indexOf(env); + if (selectedEnvIndex !== -1) { + return Boolean( + folders?.[selectedEnvIndex]?.data?.folders?.find( + ({ name: folderName }) => folderName === name + ) + ); + } + return false; + }, + [(folders || []).map((folder) => folder.data)] + ); + + return { folders, folderNames, isFolderPresentInEnv }; +}; + export const useGetProjectFoldersBatch = ({ folders = [], isPaused, diff --git a/frontend/src/hooks/api/secretFolders/types.ts b/frontend/src/hooks/api/secretFolders/types.ts index d6240666a..c9d707620 100644 --- a/frontend/src/hooks/api/secretFolders/types.ts +++ b/frontend/src/hooks/api/secretFolders/types.ts @@ -17,6 +17,13 @@ export type GetProjectFoldersBatchDTO = { parentFolderPath?: string; }; +export type TGetFoldersByEnvDTO = { + environments: string[]; + workspaceId: string; + parentFolderPath?: string; + parentFolderId?: string; +}; + export type CreateFolderDTO = { workspaceId: string; environment: string; diff --git a/frontend/src/hooks/api/secrets/index.ts b/frontend/src/hooks/api/secrets/index.ts index cad1e65d6..2e31e4f0b 100644 --- a/frontend/src/hooks/api/secrets/index.ts +++ b/frontend/src/hooks/api/secrets/index.ts @@ -1,6 +1,7 @@ +export { useCreateSecretV3, useDeleteSecretV3, useUpdateSecretV3 } from "./mutations"; export { useBatchSecretsOp, useGetProjectSecrets, - useGetProjectSecretsByKey, + useGetProjectSecretsAllEnv, useGetSecretVersion } from "./queries"; diff --git a/frontend/src/hooks/api/secrets/mutations.tsx b/frontend/src/hooks/api/secrets/mutations.tsx new file mode 100644 index 000000000..6df00174c --- /dev/null +++ b/frontend/src/hooks/api/secrets/mutations.tsx @@ -0,0 +1,172 @@ +import crypto from "crypto"; + +import { useMutation, useQueryClient } from "@tanstack/react-query"; + +import { + decryptAssymmetric, + encryptSymmetric +} from "@app/components/utilities/cryptography/crypto"; +import { apiRequest } from "@app/config/request"; + +import { secretKeys } from "./queries"; +import { TCreateSecretsV3DTO, TDeleteSecretsV3DTO, TUpdateSecretsV3DTO } from "./types"; + +const encryptSecret = (randomBytes: string, key: string, value?: string, comment?: string) => { + // encrypt key + const { + ciphertext: secretKeyCiphertext, + iv: secretKeyIV, + tag: secretKeyTag + } = encryptSymmetric({ + plaintext: key, + key: randomBytes + }); + + // encrypt value + const { + ciphertext: secretValueCiphertext, + iv: secretValueIV, + tag: secretValueTag + } = encryptSymmetric({ + plaintext: value ?? "", + key: randomBytes + }); + + // encrypt comment + const { + ciphertext: secretCommentCiphertext, + iv: secretCommentIV, + tag: secretCommentTag + } = encryptSymmetric({ + plaintext: comment ?? "", + key: randomBytes + }); + + return { + secretKeyCiphertext, + secretKeyIV, + secretKeyTag, + secretValueCiphertext, + secretValueIV, + secretValueTag, + secretCommentCiphertext, + secretCommentIV, + secretCommentTag + }; +}; + +export const useCreateSecretV3 = () => { + const queryClient = useQueryClient(); + return useMutation<{}, {}, TCreateSecretsV3DTO>({ + mutationFn: async ({ + secretPath = "/", + type, + environment, + workspaceId, + secretName, + secretValue, + latestFileKey, + secretComment + }) => { + const PRIVATE_KEY = localStorage.getItem("PRIVATE_KEY") as string; + + const randomBytes = latestFileKey + ? decryptAssymmetric({ + ciphertext: latestFileKey.encryptedKey, + nonce: latestFileKey.nonce, + publicKey: latestFileKey.sender.publicKey, + privateKey: PRIVATE_KEY + }) + : crypto.randomBytes(16).toString("hex"); + + const reqBody = { + workspaceId, + environment, + type, + secretPath, + ...encryptSecret(randomBytes, secretName, secretValue, secretComment) + }; + const { data } = await apiRequest.post(`/api/v3/secrets/${secretName}`, reqBody); + return data; + }, + onSuccess: (_, { workspaceId, environment, secretPath }) => { + queryClient.invalidateQueries( + secretKeys.getProjectSecret(workspaceId, environment, secretPath) + ); + } + }); +}; + +export const useUpdateSecretV3 = () => { + const queryClient = useQueryClient(); + return useMutation<{}, {}, TUpdateSecretsV3DTO>({ + mutationFn: async ({ + secretPath = "/", + type, + environment, + workspaceId, + secretName, + secretValue, + latestFileKey + }) => { + const PRIVATE_KEY = localStorage.getItem("PRIVATE_KEY") as string; + + const randomBytes = latestFileKey + ? decryptAssymmetric({ + ciphertext: latestFileKey.encryptedKey, + nonce: latestFileKey.nonce, + publicKey: latestFileKey.sender.publicKey, + privateKey: PRIVATE_KEY + }) + : crypto.randomBytes(16).toString("hex"); + const { secretValueIV, secretValueTag, secretValueCiphertext } = encryptSecret( + randomBytes, + secretName, + secretValue, + "" + ); + + const reqBody = { + workspaceId, + environment, + type, + secretPath, + secretValueIV, + secretValueTag, + secretValueCiphertext + }; + const { data } = await apiRequest.patch(`/api/v3/secrets/${secretName}`, reqBody); + return data; + }, + onSuccess: (_, { workspaceId, environment, secretPath }) => { + queryClient.invalidateQueries( + secretKeys.getProjectSecret(workspaceId, environment, secretPath) + ); + } + }); +}; + +export const useDeleteSecretV3 = () => { + const queryClient = useQueryClient(); + + return useMutation<{}, {}, TDeleteSecretsV3DTO>({ + mutationFn: async ({ secretPath = "/", type, environment, workspaceId, secretName }) => { + const reqBody = { + workspaceId, + environment, + type, + secretPath + }; + + const { data } = await apiRequest.delete(`/api/v3/secrets/${secretName}`, { + data: reqBody + }); + return data; + }, + onSuccess: (_, { workspaceId, environment, secretPath }) => { + queryClient.invalidateQueries( + secretKeys.getProjectSecret(workspaceId, environment, secretPath) + ); + } + }); +}; diff --git a/frontend/src/hooks/api/secrets/queries.tsx b/frontend/src/hooks/api/secrets/queries.tsx index 1541ff753..34ecd7ec4 100644 --- a/frontend/src/hooks/api/secrets/queries.tsx +++ b/frontend/src/hooks/api/secrets/queries.tsx @@ -1,6 +1,6 @@ /* eslint-disable no-param-reassign */ -import { useCallback } from "react"; -import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; +import { useCallback, useMemo } from "react"; +import { useMutation, useQueries, useQuery, useQueryClient } from "@tanstack/react-query"; import { decryptAssymmetric, @@ -15,7 +15,8 @@ import { EncryptedSecret, EncryptedSecretVersion, GetProjectSecretsDTO, - GetSecretVersionsDTO + GetSecretVersionsDTO, + TGetProjectSecretsAllEnvDTO } from "./types"; export const secretKeys = { @@ -37,40 +38,15 @@ const fetchProjectEncryptedSecrets = async ( folderId?: string, secretPath?: string ) => { - if (typeof env === "string") { - const { data } = await apiRequest.get<{ secrets: EncryptedSecret[] }>("/api/v2/secrets", { - params: { - environment: env, - workspaceId, - folderId: folderId || undefined, - secretPath - } - }); - return data.secrets; - } - - if (typeof env === "object") { - let allEnvData: any = []; - - // eslint-disable-next-line no-restricted-syntax - for (const envPoint of env) { - // eslint-disable-next-line no-await-in-loop - const { data } = await apiRequest.get<{ secrets: EncryptedSecret[] }>("/api/v2/secrets", { - params: { - environment: envPoint, - workspaceId, - folderId, - secretPath - } - }); - allEnvData = allEnvData.concat(data.secrets); + const { data } = await apiRequest.get<{ secrets: EncryptedSecret[] }>("/api/v2/secrets", { + params: { + environment: env, + workspaceId, + folderId: folderId || undefined, + secretPath } - - return allEnvData; - // eslint-disable-next-line no-else-return - } else { - return null; - } + }); + return data.secrets; }; export const useGetProjectSecrets = ({ @@ -160,22 +136,19 @@ export const useGetProjectSecrets = ({ ) }); -export const useGetProjectSecretsByKey = ({ +export const useGetProjectSecretsAllEnv = ({ workspaceId, - env, + envs, decryptFileKey, - isPaused, folderId, secretPath -}: GetProjectSecretsDTO) => - useQuery({ - // wait for all values to be available - enabled: Boolean(decryptFileKey && workspaceId && env) && !isPaused, - // right now secretpath is passed as folderid as only this is used in overview - queryKey: secretKeys.getProjectSecret(workspaceId, env, secretPath), - queryFn: () => fetchProjectEncryptedSecrets(workspaceId, env, folderId, secretPath), - select: useCallback( - (data: EncryptedSecret[]) => { +}: TGetProjectSecretsAllEnvDTO) => { + const secrets = useQueries({ + queries: envs.map((env) => ({ + queryKey: secretKeys.getProjectSecret(workspaceId, env, secretPath || folderId), + enabled: Boolean(decryptFileKey && workspaceId && env), + queryFn: () => fetchProjectEncryptedSecrets(workspaceId, env, folderId, secretPath), + select: (data: EncryptedSecret[]) => { const PRIVATE_KEY = localStorage.getItem("PRIVATE_KEY") as string; const latestKey = decryptFileKey; const key = decryptAssymmetric({ @@ -185,12 +158,11 @@ export const useGetProjectSecretsByKey = ({ privateKey: PRIVATE_KEY }); - const sharedSecrets: Record = {}; + const sharedSecrets: Record = {}; const personalSecrets: Record = {}; // this used for add-only mode in dashboard // type won't be there thus only one key is shown const duplicateSecretKey: Record = {}; - const uniqSecKeys: Record = {}; data.forEach((encSecret: EncryptedSecret) => { const secretKey = decryptSymmetric({ ciphertext: encSecret.secretKeyCiphertext, @@ -198,7 +170,6 @@ export const useGetProjectSecretsByKey = ({ tag: encSecret.secretKeyTag, key }); - if (!uniqSecKeys?.[secretKey]) uniqSecKeys[secretKey] = true; const secretValue = decryptSymmetric({ ciphertext: encSecret.secretValueCiphertext, @@ -226,35 +197,65 @@ export const useGetProjectSecretsByKey = ({ }; if (encSecret.type === "personal") { - personalSecrets[`${decryptedSecret.key}-${decryptedSecret.env}`] = { + personalSecrets[decryptedSecret.key] = { id: encSecret._id, value: secretValue }; } else { - if (!duplicateSecretKey?.[`${decryptedSecret.key}-${decryptedSecret.env}`]) { - if (!sharedSecrets?.[secretKey]) sharedSecrets[secretKey] = []; - sharedSecrets[secretKey].push(decryptedSecret); + if (!duplicateSecretKey?.[decryptedSecret.key]) { + sharedSecrets[decryptedSecret.key] = decryptedSecret; } - duplicateSecretKey[`${decryptedSecret.key}-${decryptedSecret.env}`] = true; + duplicateSecretKey[decryptedSecret.key] = true; } }); - Object.keys(sharedSecrets).forEach((secName) => { - sharedSecrets[secName].forEach((val) => { - const dupKey = `${val.key}-${val.env}`; - if (personalSecrets?.[dupKey]) { - val.idOverride = personalSecrets[dupKey].id; - val.valueOverride = personalSecrets[dupKey].value; - val.overrideAction = "modified"; - } - }); - }); - return { secrets: sharedSecrets, uniqueSecCount: Object.keys(uniqSecKeys).length }; - }, - [decryptFileKey] - ) + Object.keys(sharedSecrets).forEach((val) => { + if (personalSecrets?.[val]) { + sharedSecrets[val].idOverride = personalSecrets[val].id; + sharedSecrets[val].valueOverride = personalSecrets[val].value; + sharedSecrets[val].overrideAction = "modified"; + } + }); + return sharedSecrets; + } + })) }); + const secKeys = useMemo(() => { + const keys = new Set(); + secrets?.forEach(({ data }) => { + // TODO(akhilmhdh): find out why this is unknown + Object.keys(data || {}).forEach((key) => keys.add(key)); + }); + return [...keys]; + }, [(secrets || []).map((sec) => sec.data)]); + + const getEnvSecretKeyCount = useCallback( + (env: string) => { + const selectedEnvIndex = envs.indexOf(env); + if (selectedEnvIndex !== -1) { + return Object.keys(secrets[selectedEnvIndex]?.data || {}).length; + } + return 0; + }, + [(secrets || []).map((sec) => sec.data)] + ); + + const getSecretByKey = useCallback( + (env: string, key: string) => { + const selectedEnvIndex = envs.indexOf(env); + if (selectedEnvIndex !== -1) { + const sec = secrets[selectedEnvIndex]?.data?.[key]; + return sec; + } + return undefined; + }, + [(secrets || []).map((sec) => sec.data)] + ); + + return { data: secrets, secKeys, getSecretByKey, getEnvSecretKeyCount }; +}; + const fetchEncryptedSecretVersion = async (secretId: string, offset: number, limit: number) => { const { data } = await apiRequest.get<{ secretVersions: EncryptedSecretVersion[] }>( `/api/v1/secret/${secretId}/secret-versions`, diff --git a/frontend/src/hooks/api/secrets/types.ts b/frontend/src/hooks/api/secrets/types.ts index b36eb47f7..9e3092257 100644 --- a/frontend/src/hooks/api/secrets/types.ts +++ b/frontend/src/hooks/api/secrets/types.ts @@ -103,9 +103,47 @@ export type GetProjectSecretsDTO = { onSuccess?: (data: DecryptedSecret[]) => void; }; +export type TGetProjectSecretsAllEnvDTO = { + workspaceId: string; + envs: string[]; + decryptFileKey: UserWsKeyPair; + folderId?: string; + secretPath?: string; + isPaused?: boolean; +}; + export type GetSecretVersionsDTO = { secretId: string; limit: number; offset: number; decryptFileKey: UserWsKeyPair; }; + +export type TCreateSecretsV3DTO = { + latestFileKey: UserWsKeyPair; + secretName: string; + secretValue: string; + secretComment: string; + secretPath: string; + workspaceId: string; + environment: string; + type: string; +}; + +export type TUpdateSecretsV3DTO = { + latestFileKey: UserWsKeyPair; + workspaceId: string; + environment: string; + type: string; + secretPath: string; + secretName: string; + secretValue: string; +}; + +export type TDeleteSecretsV3DTO = { + workspaceId: string; + environment: string; + type: string; + secretPath: string; + secretName: string; +}; diff --git a/frontend/src/layouts/AppLayout/AppLayout.tsx b/frontend/src/layouts/AppLayout/AppLayout.tsx index 877db215d..fea50dd8a 100644 --- a/frontend/src/layouts/AppLayout/AppLayout.tsx +++ b/frontend/src/layouts/AppLayout/AppLayout.tsx @@ -13,7 +13,18 @@ import { useTranslation } from "react-i18next"; import Link from "next/link"; import { useRouter } from "next/router"; import { faGithub, faSlack } from "@fortawesome/free-brands-svg-icons"; -import { faAngleDown, faArrowLeft, faArrowUpRightFromSquare, faBook, faCheck, faEnvelope, faInfinity, faMobile, faPlus, faQuestion } from "@fortawesome/free-solid-svg-icons"; +import { + faAngleDown, + faArrowLeft, + faArrowUpRightFromSquare, + faBook, + faCheck, + faEnvelope, + faInfinity, + faMobile, + faPlus, + faQuestion +} from "@fortawesome/free-solid-svg-icons"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { yupResolver } from "@hookform/resolvers/yup"; import { DropdownMenuTrigger } from "@radix-ui/react-dropdown-menu"; @@ -41,7 +52,14 @@ import { } from "@app/components/v2"; import { useOrganization, useSubscription, useUser, useWorkspace } from "@app/context"; import { usePopUp } from "@app/hooks"; -import { fetchOrgUsers, useAddUserToWs, useCreateWorkspace, useGetOrgTrialUrl, useLogoutUser, useUploadWsKey } from "@app/hooks/api"; +import { + fetchOrgUsers, + useAddUserToWs, + useCreateWorkspace, + useGetOrgTrialUrl, + useLogoutUser, + useUploadWsKey +} from "@app/hooks/api"; interface LayoutProps { children: React.ReactNode; @@ -89,7 +107,9 @@ export const AppLayout = ({ children }: LayoutProps) => { const { subscription } = useSubscription(); // const [ isLearningNoteOpen, setIsLearningNoteOpen ] = useState(true); - const isAddingProjectsAllowed = subscription?.workspaceLimit ? (subscription.workspacesUsed < subscription.workspaceLimit) : true; + const isAddingProjectsAllowed = subscription?.workspaceLimit + ? subscription.workspacesUsed < subscription.workspaceLimit + : true; const createWs = useCreateWorkspace(); const uploadWsKey = useUploadWsKey(); @@ -110,22 +130,22 @@ export const AppLayout = ({ children }: LayoutProps) => { const { t } = useTranslation(); - useEffect(() => { - const handleRouteChange = () => { - (window).Intercom("update"); - }; - - router.events.on("routeChangeComplete", handleRouteChange); - - return () => { - router.events.off("routeChangeComplete", handleRouteChange); - }; - }, []); + useEffect(() => { + const handleRouteChange = () => { + window.Intercom("update"); + }; + + router.events.on("routeChangeComplete", handleRouteChange); + + return () => { + router.events.off("routeChangeComplete", handleRouteChange); + }; + }, []); const logout = useLogoutUser(); const logOutUser = async () => { try { - console.log("Logging out...") + console.log("Logging out..."); await logout.mutateAsync(); localStorage.removeItem("protectedKey"); localStorage.removeItem("protectedKeyIV"); @@ -145,27 +165,30 @@ export const AppLayout = ({ children }: LayoutProps) => { const changeOrg = async (orgId) => { localStorage.setItem("orgData.id", orgId); - router.push(`/org/${orgId}/overview`) - } + router.push(`/org/${orgId}/overview`); + }; // TODO(akhilmhdh): This entire logic will be rechecked and will try to avoid // Placing the localstorage as much as possible // Wait till tony integrates the azure and its launched useEffect(() => { - // Put a user in an org if they're not in one yet const putUserInOrg = async () => { if (tempLocalStorage("orgData.id") === "") { localStorage.setItem("orgData.id", orgs[0]?._id); } - if (currentOrg && ( - (workspaces?.length === 0 && router.asPath.includes("project")) - || router.asPath.includes("/project/undefined") - || (!orgs?.map(org => org._id)?.includes(router.query.id) && !router.asPath.includes("project") && !router.asPath.includes("personal") && !router.asPath.includes("integration")) - )) { + if ( + currentOrg && + ((workspaces?.length === 0 && router.asPath.includes("project")) || + router.asPath.includes("/project/undefined") || + (!orgs?.map((org) => org._id)?.includes(router.query.id) && + !router.asPath.includes("project") && + !router.asPath.includes("personal") && + !router.asPath.includes("integration"))) + ) { router.push(`/org/${currentOrg?._id}/overview`); - } + } // else if (!router.asPath.includes("org") && !router.asPath.includes("project") && !router.asPath.includes("integrations") && !router.asPath.includes("personal-settings")) { // const pathSegments = router.asPath.split("/").filter((segment) => segment.length > 0); @@ -233,7 +256,7 @@ export const AppLayout = ({ children }: LayoutProps) => { } createNotification({ text: "Workspace created", type: "success" }); handlePopUpClose("addNewWs"); - router.push(`/project/${newWorkspaceId}/secrets`); + router.push(`/project/${newWorkspaceId}/secrets/overview`); } catch (err) { console.error(err); createNotification({ text: "Failed to create workspace", type: "error" }); @@ -244,190 +267,235 @@ export const AppLayout = ({ children }: LayoutProps) => { <>
-