diff --git a/.github/values.yaml b/.github/values.yaml new file mode 100644 index 0000000000..f1551d61a0 --- /dev/null +++ b/.github/values.yaml @@ -0,0 +1,36 @@ +frontend: + replicaCount: 1 + image: + repository: + pullPolicy: Always + tag: "latest" + kubeSecretRef: managed-secret-frontend + +backend: + replicaCount: 1 + image: + repository: + pullPolicy: Always + tag: "latest" + kubeSecretRef: managed-backend-secret + +ingress: + enabled: true + annotations: + kubernetes.io/ingress.class: "nginx" + cert-manager.io/cluster-issuer: "letsencrypt-prod" + hostName: gamma.infisical.com + frontend: + path: / + pathType: Prefix + backend: + path: /api + pathType: Prefix + tls: + - secretName: echo-tls + hosts: + - gamma.infisical.com + +backendEnvironmentVariables: + +frontendEnvironmentVariables: \ No newline at end of file diff --git a/.github/workflows/docker-image.yml b/.github/workflows/docker-image.yml index ab7b939e7b..b4f9546ae0 100644 --- a/.github/workflows/docker-image.yml +++ b/.github/workflows/docker-image.yml @@ -1,5 +1,4 @@ -name: Push frontend and backend to Dockerhub - +name: Build, Publish and Deploy to Gamma on: [workflow_dispatch] jobs: @@ -99,4 +98,41 @@ jobs: infisical/frontend:latest platforms: linux/amd64,linux/arm64 build-args: | - POSTHOG_API_KEY=${{ secrets.PUBLIC_POSTHOG_API_KEY }} \ No newline at end of file + POSTHOG_API_KEY=${{ secrets.PUBLIC_POSTHOG_API_KEY }} + gamma-deployment: + name: Deploy to gamma + runs-on: ubuntu-latest + needs: [frontend-image, backend-image] + steps: + - name: ☁️ Checkout source + uses: actions/checkout@v3 + - name: Install Helm + uses: azure/setup-helm@v3 + with: + version: v3.10.0 + - name: Install infisical helm chart + run: | + helm repo add infisical-helm-charts 'https://dl.cloudsmith.io/public/infisical/helm-charts/helm/charts/' + helm repo update + - name: Install kubectl + uses: azure/setup-kubectl@v3 + - name: Install doctl + uses: digitalocean/action-doctl@v2 + with: + token: ${{ secrets.DIGITALOCEAN_ACCESS_TOKEN }} + - name: Save DigitalOcean kubeconfig with short-lived credentials + run: doctl kubernetes cluster kubeconfig save --expiry-seconds 600 k8s-1-25-4-do-0-nyc1-1670645170179 + - name: switch to gamma namespace + run: kubectl config set-context --current --namespace=gamma + - name: test kubectl + run: kubectl get ingress + - name: Download helm values to file and upgrade gamma deploy + run: | + wget https://raw.githubusercontent.com/Infisical/infisical/main/.github/values.yaml + helm upgrade infisical infisical-helm-charts/infisical --values values.yaml --recreate-pods + if [[ $(helm status infisical) == *"FAILED"* ]]; then + echo "Helm upgrade failed" + exit 1 + else + echo "Helm upgrade was successful" + fi \ No newline at end of file diff --git a/Makefile b/Makefile index 266eaf9b59..9e1f5c3f61 100644 --- a/Makefile +++ b/Makefile @@ -7,6 +7,9 @@ push: up-dev: docker-compose -f docker-compose.dev.yml up --build +i-dev: + infisical export && infisical export > .env && docker-compose -f docker-compose.dev.yml up --build + up-prod: docker-compose -f docker-compose.yml up --build diff --git a/README.md b/README.md index 329c862aee..3a3860d2c0 100644 --- a/README.md +++ b/README.md @@ -3,7 +3,7 @@ infisical

-

Open-source, E2EE, simple tool to manage and sync environment variables across your team and infrastructure.

+

Open-source, E2EE, simple tool to manage secrets and configs across your team and infrastructure.

@@ -34,13 +34,13 @@ Dashboard -**[Infisical](https://infisical.com)** is an open source, E2EE tool to help teams manage and sync environment variables across their development workflow and infrastructure. It's designed to be simple and take minutes to get going. +**[Infisical](https://infisical.com)** is an open source, E2EE tool to help teams manage and sync secrets and configs across their development workflow and infrastructure. It's designed to be simple and take minutes to get going. -- **[User-Friendly Dashboard](https://infisical.com/docs/getting-started/dashboard/project)** to manage your team's environment variables within projects -- **[Language-Agnostic CLI](https://infisical.com/docs/cli/overview)** that pulls and injects environment variables into your local workflow +- **[User-Friendly Dashboard](https://infisical.com/docs/getting-started/dashboard/project)** to manage your team's secrets and configs within projects +- **[Language-Agnostic CLI](https://infisical.com/docs/cli/overview)** that pulls and injects esecrets and configs into your local workflow - **[Complete control over your data](https://infisical.com/docs/self-hosting/overview)** - host it yourself on any infrastructure - **Navigate Multiple Environments** per project (e.g. development, staging, production, etc.) -- **Personal overrides** for environment variables +- **Personal overrides** for secrets and configs - **[Integrations](https://infisical.com/docs/integrations/overview)** with CI/CD and production infrastructure - **[Secret Versioning](https://infisical.com/docs/getting-started/dashboard/versioning)** to view the change history for any secret - **[Activity Logs](https://infisical.com/docs/getting-started/dashboard/audit-logs)** to record every action taken in a project. @@ -333,7 +333,8 @@ Infisical officially launched as v.1.0 on November 21st, 2022. There are a lot o - + + ## 🌎 Translations diff --git a/backend/src/app.ts b/backend/src/app.ts index 8cefd31ac2..d32e83be77 100644 --- a/backend/src/app.ts +++ b/backend/src/app.ts @@ -47,6 +47,7 @@ import { workspace as v2WorkspaceRouter, serviceTokenData as v2ServiceTokenDataRouter, apiKeyData as v2APIKeyDataRouter, + environment as v2EnvironmentRouter, } from './routes/v2'; import { healthCheck } from './routes/status'; @@ -105,6 +106,7 @@ app.use('/api/v1/integration-auth', v1IntegrationAuthRouter); // v2 routes app.use('/api/v2/users', v2UsersRouter); +app.use('/api/v2/workspace', v2EnvironmentRouter); app.use('/api/v2/workspace', v2WorkspaceRouter); // TODO: turn into plural route app.use('/api/v2/secret', v2SecretRouter); // stop supporting, TODO: revise app.use('/api/v2/secrets', v2SecretsRouter); diff --git a/backend/src/controllers/v1/integrationAuthController.ts b/backend/src/controllers/v1/integrationAuthController.ts index 95d0066ae7..5df8b4e910 100644 --- a/backend/src/controllers/v1/integrationAuthController.ts +++ b/backend/src/controllers/v1/integrationAuthController.ts @@ -3,7 +3,7 @@ import * as Sentry from '@sentry/node'; import axios from 'axios'; import { readFileSync } from 'fs'; import { IntegrationAuth, Integration } from '../../models'; -import { INTEGRATION_SET, INTEGRATION_OPTIONS, ENV_DEV } from '../../variables'; +import { INTEGRATION_SET, INTEGRATION_OPTIONS } from '../../variables'; import { IntegrationService } from '../../services'; import { getApps, revokeAccess } from '../../integrations'; @@ -31,11 +31,17 @@ export const oAuthExchange = async ( 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 + code, + environment: environments[0].slug, }); } catch (err) { Sentry.setUser(null); diff --git a/backend/src/controllers/v1/secretController.ts b/backend/src/controllers/v1/secretController.ts index 1b756ecc70..c76e5e8833 100644 --- a/backend/src/controllers/v1/secretController.ts +++ b/backend/src/controllers/v1/secretController.ts @@ -9,7 +9,6 @@ import { import { pushKeys } from '../../helpers/key'; import { eventPushSecrets } from '../../events'; import { EventService } from '../../services'; -import { ENV_SET } from '../../variables'; import { postHogClient } from '../../services'; interface PushSecret { @@ -44,7 +43,8 @@ export const pushSecrets = async (req: Request, res: Response) => { const { workspaceId } = req.params; // validate environment - if (!ENV_SET.has(environment)) { + const workspaceEnvs = req.membership.workspace.environments; + if (!workspaceEnvs.find(({ slug }: { slug: string }) => slug === environment)) { throw new Error('Failed to validate environment'); } @@ -116,7 +116,8 @@ export const pullSecrets = async (req: Request, res: Response) => { const { workspaceId } = req.params; // validate environment - if (!ENV_SET.has(environment)) { + const workspaceEnvs = req.membership.workspace.environments; + if (!workspaceEnvs.find(({ slug }: { slug: string }) => slug === environment)) { throw new Error('Failed to validate environment'); } @@ -183,7 +184,8 @@ export const pullSecretsServiceToken = async (req: Request, res: Response) => { const { workspaceId } = req.params; // validate environment - if (!ENV_SET.has(environment)) { + const workspaceEnvs = req.membership.workspace.environments; + if (!workspaceEnvs.find(({ slug }: { slug: string }) => slug === environment)) { throw new Error('Failed to validate environment'); } diff --git a/backend/src/controllers/v1/serviceTokenController.ts b/backend/src/controllers/v1/serviceTokenController.ts index 244a587837..3fafb90433 100644 --- a/backend/src/controllers/v1/serviceTokenController.ts +++ b/backend/src/controllers/v1/serviceTokenController.ts @@ -1,7 +1,6 @@ import { Request, Response } from 'express'; import { ServiceToken } from '../../models'; import { createToken } from '../../helpers/auth'; -import { ENV_SET } from '../../variables'; import { JWT_SERVICE_SECRET } from '../../config'; /** @@ -36,7 +35,8 @@ export const createServiceToken = async (req: Request, res: Response) => { } = req.body; // validate environment - if (!ENV_SET.has(environment)) { + const workspaceEnvs = req.membership.workspace.environments; + if (!workspaceEnvs.find(({ slug }: { slug: string }) => slug === environment)) { throw new Error('Failed to validate environment'); } diff --git a/backend/src/controllers/v2/environmentController.ts b/backend/src/controllers/v2/environmentController.ts new file mode 100644 index 0000000000..1d1ffb6a69 --- /dev/null +++ b/backend/src/controllers/v2/environmentController.ts @@ -0,0 +1,204 @@ +import { Request, Response } from 'express'; +import * as Sentry from '@sentry/node'; +import { + Secret, + ServiceToken, + Workspace, + Integration, + ServiceTokenData, +} from '../../models'; +import { SecretVersion } from '../../ee/models'; + +/** + * Create new workspace environment named [environmentName] under workspace with id + * @param req + * @param res + * @returns + */ +export const createWorkspaceEnvironment = async ( + req: Request, + res: Response +) => { + const { workspaceId } = req.params; + const { environmentName, environmentSlug } = req.body; + try { + const workspace = await Workspace.findById(workspaceId).exec(); + if ( + !workspace || + workspace?.environments.find( + ({ name, slug }) => slug === environmentSlug || environmentName === name + ) + ) { + throw new Error('Failed to create workspace environment'); + } + + workspace?.environments.push({ + name: environmentName.toLowerCase(), + slug: environmentSlug.toLowerCase(), + }); + await workspace.save(); + } catch (err) { + Sentry.setUser({ email: req.user.email }); + Sentry.captureException(err); + return res.status(400).send({ + message: 'Failed to create new workspace environment', + }); + } + + return res.status(200).send({ + message: 'Successfully created new environment', + workspace: workspaceId, + environment: { + name: environmentName, + slug: environmentSlug, + }, + }); +}; + +/** + * Rename workspace environment with new name and slug of a workspace with [workspaceId] + * Old slug [oldEnvironmentSlug] must be provided + * @param req + * @param res + * @returns + */ +export const renameWorkspaceEnvironment = async ( + req: Request, + res: Response +) => { + const { workspaceId } = req.params; + const { environmentName, environmentSlug, oldEnvironmentSlug } = req.body; + try { + // user should pass both new slug and env name + if (!environmentSlug || !environmentName) { + throw new Error('Invalid environment given.'); + } + + // atomic update the env to avoid conflict + const workspace = await Workspace.findById(workspaceId).exec(); + if (!workspace) { + throw new Error('Failed to create workspace environment'); + } + + const isEnvExist = workspace.environments.some( + ({ name, slug }) => + slug !== oldEnvironmentSlug && + (name === environmentName || slug === environmentSlug) + ); + if (isEnvExist) { + throw new Error('Invalid environment given'); + } + + const envIndex = workspace?.environments.findIndex( + ({ slug }) => slug === oldEnvironmentSlug + ); + if (envIndex === -1) { + throw new Error('Invalid environment given'); + } + + workspace.environments[envIndex].name = environmentName.toLowerCase(); + workspace.environments[envIndex].slug = environmentSlug.toLowerCase(); + + await workspace.save(); + await Secret.updateMany( + { workspace: workspaceId, environment: oldEnvironmentSlug }, + { environment: environmentSlug } + ); + await SecretVersion.updateMany( + { workspace: workspaceId, environment: oldEnvironmentSlug }, + { environment: environmentSlug } + ); + await ServiceToken.updateMany( + { workspace: workspaceId, environment: oldEnvironmentSlug }, + { environment: environmentSlug } + ); + await ServiceTokenData.updateMany( + { workspace: workspaceId, environment: oldEnvironmentSlug }, + { environment: environmentSlug } + ); + await Integration.updateMany( + { workspace: workspaceId, environment: oldEnvironmentSlug }, + { environment: environmentSlug } + ); + } catch (err) { + Sentry.setUser({ email: req.user.email }); + Sentry.captureException(err); + return res.status(400).send({ + message: 'Failed to update workspace environment', + }); + } + + return res.status(200).send({ + message: 'Successfully update environment', + workspace: workspaceId, + environment: { + name: environmentName, + slug: environmentSlug, + }, + }); +}; + +/** + * Delete workspace environment by [environmentSlug] of workspace [workspaceId] and do the clean up + * @param req + * @param res + * @returns + */ +export const deleteWorkspaceEnvironment = async ( + req: Request, + res: Response +) => { + const { workspaceId } = req.params; + const { environmentSlug } = req.body; + try { + // atomic update the env to avoid conflict + const workspace = await Workspace.findById(workspaceId).exec(); + if (!workspace) { + throw new Error('Failed to create workspace environment'); + } + + const envIndex = workspace?.environments.findIndex( + ({ slug }) => slug === environmentSlug + ); + if (envIndex === -1) { + throw new Error('Invalid environment given'); + } + + workspace.environments.splice(envIndex, 1); + await workspace.save(); + + // clean up + await Secret.deleteMany({ + workspace: workspaceId, + environment: environmentSlug, + }); + await SecretVersion.deleteMany({ + workspace: workspaceId, + environment: environmentSlug, + }); + await ServiceToken.deleteMany({ + workspace: workspaceId, + environment: environmentSlug, + }); + await ServiceTokenData.deleteMany({ + workspace: workspaceId, + environment: environmentSlug, + }); + await Integration.deleteMany({ + workspace: workspaceId, + environment: environmentSlug, + }); + } catch (err) { + Sentry.setUser({ email: req.user.email }); + Sentry.captureException(err); + return res.status(400).send({ + message: 'Failed to delete workspace environment', + }); + } + + return res.status(200).send({ + message: 'Successfully deleted environment', + workspace: workspaceId, + environment: environmentSlug, + }); +}; diff --git a/backend/src/controllers/v2/index.ts b/backend/src/controllers/v2/index.ts index bc68485901..b2b245d9cc 100644 --- a/backend/src/controllers/v2/index.ts +++ b/backend/src/controllers/v2/index.ts @@ -4,6 +4,7 @@ import * as serviceTokenDataController from './serviceTokenDataController'; import * as apiKeyDataController from './apiKeyDataController'; import * as secretController from './secretController'; import * as secretsController from './secretsController'; +import * as environmentController from './environmentController'; export { usersController, @@ -11,5 +12,6 @@ export { serviceTokenDataController, apiKeyDataController, secretController, - secretsController + secretsController, + environmentController } diff --git a/backend/src/controllers/v2/secretController.ts b/backend/src/controllers/v2/secretController.ts index c2cf45f9ad..89567e616d 100644 --- a/backend/src/controllers/v2/secretController.ts +++ b/backend/src/controllers/v2/secretController.ts @@ -7,7 +7,7 @@ const { ValidationError } = mongoose.Error; import { BadRequestError, InternalServerError, UnauthorizedRequestError, ValidationError as RouteValidationError } from '../../utils/errors'; import { AnyBulkWriteOperation } from 'mongodb'; import { SECRET_PERSONAL, SECRET_SHARED } from "../../variables"; -// import { postHogClient } from '../../services'; +import { postHogClient } from '../../services'; /** * Create secret for workspace with id [workspaceId] and environment [environment] @@ -42,19 +42,19 @@ export const createSecret = async (req: Request, res: Response) => { throw RouteValidationError({ message: error.message, stack: error.stack }) } - // if (postHogClient) { - // postHogClient.capture({ - // event: 'secrets added', - // distinctId: req.user.email, - // properties: { - // numberOfSecrets: 1, - // workspaceId, - // environment, - // channel: req.headers?.['user-agent']?.toLowerCase().includes('mozilla') ? 'web' : 'cli', - // userAgent: req.headers?.['user-agent'] - // } - // }); - // } + if (postHogClient) { + postHogClient.capture({ + event: 'secrets added', + distinctId: req.user.email, + properties: { + numberOfSecrets: 1, + workspaceId, + environment, + channel: req.headers?.['user-agent']?.toLowerCase().includes('mozilla') ? 'web' : 'cli', + userAgent: req.headers?.['user-agent'] + } + }); + } res.status(200).send({ secret @@ -103,19 +103,19 @@ export const createSecrets = async (req: Request, res: Response) => { throw InternalServerError({ message: "Unable to process your batch create request. Please try again", stack: bulkCreateError.stack }) } - // if (postHogClient) { - // postHogClient.capture({ - // event: 'secrets added', - // distinctId: req.user.email, - // properties: { - // numberOfSecrets: (secretsToCreate ?? []).length, - // workspaceId, - // environment, - // channel: req.headers?.['user-agent']?.toLowerCase().includes('mozilla') ? 'web' : 'cli', - // userAgent: req.headers?.['user-agent'] - // } - // }); - // } + if (postHogClient) { + postHogClient.capture({ + event: 'secrets added', + distinctId: req.user.email, + properties: { + numberOfSecrets: (secretsToCreate ?? []).length, + workspaceId, + environment, + channel: req.headers?.['user-agent']?.toLowerCase().includes('mozilla') ? 'web' : 'cli', + userAgent: req.headers?.['user-agent'] + } + }); + } res.status(200).send({ secrets @@ -158,19 +158,19 @@ export const deleteSecrets = async (req: Request, res: Response) => { throw InternalServerError() } - // if (postHogClient) { - // postHogClient.capture({ - // event: 'secrets deleted', - // distinctId: req.user.email, - // properties: { - // numberOfSecrets: numSecretsDeleted, - // environment: environmentName, - // workspaceId, - // channel: req.headers?.['user-agent']?.toLowerCase().includes('mozilla') ? 'web' : 'cli', - // userAgent: req.headers?.['user-agent'] - // } - // }); - // } + if (postHogClient) { + postHogClient.capture({ + event: 'secrets deleted', + distinctId: req.user.email, + properties: { + numberOfSecrets: numSecretsDeleted, + environment: environmentName, + workspaceId, + channel: req.headers?.['user-agent']?.toLowerCase().includes('mozilla') ? 'web' : 'cli', + userAgent: req.headers?.['user-agent'] + } + }); + } res.status(200).send() } @@ -183,19 +183,19 @@ export const deleteSecrets = async (req: Request, res: Response) => { export const deleteSecret = async (req: Request, res: Response) => { await Secret.findByIdAndDelete(req._secret._id) - // if (postHogClient) { - // postHogClient.capture({ - // event: 'secrets deleted', - // distinctId: req.user.email, - // properties: { - // numberOfSecrets: 1, - // workspaceId: req._secret.workspace.toString(), - // environment: req._secret.environment, - // channel: req.headers?.['user-agent']?.toLowerCase().includes('mozilla') ? 'web' : 'cli', - // userAgent: req.headers?.['user-agent'] - // } - // }); - // } + if (postHogClient) { + postHogClient.capture({ + event: 'secrets deleted', + distinctId: req.user.email, + properties: { + numberOfSecrets: 1, + workspaceId: req._secret.workspace.toString(), + environment: req._secret.environment, + channel: req.headers?.['user-agent']?.toLowerCase().includes('mozilla') ? 'web' : 'cli', + userAgent: req.headers?.['user-agent'] + } + }); + } res.status(200).send({ secret: req._secret @@ -252,19 +252,19 @@ export const updateSecrets = async (req: Request, res: Response) => { throw InternalServerError() } - // if (postHogClient) { - // postHogClient.capture({ - // event: 'secrets modified', - // distinctId: req.user.email, - // properties: { - // numberOfSecrets: (secretsModificationsRequested ?? []).length, - // environment: environmentName, - // workspaceId, - // channel: req.headers?.['user-agent']?.toLowerCase().includes('mozilla') ? 'web' : 'cli', - // userAgent: req.headers?.['user-agent'] - // } - // }); - // } + if (postHogClient) { + postHogClient.capture({ + event: 'secrets modified', + distinctId: req.user.email, + properties: { + numberOfSecrets: (secretsModificationsRequested ?? []).length, + environment: environmentName, + workspaceId, + channel: req.headers?.['user-agent']?.toLowerCase().includes('mozilla') ? 'web' : 'cli', + userAgent: req.headers?.['user-agent'] + } + }); + } return res.status(200).send() } @@ -304,19 +304,19 @@ export const updateSecret = async (req: Request, res: Response) => { throw RouteValidationError({ message: "Unable to apply modifications, please try again", stack: error.stack }) } - // if (postHogClient) { - // postHogClient.capture({ - // event: 'secrets modified', - // distinctId: req.user.email, - // properties: { - // numberOfSecrets: 1, - // environment: environmentName, - // workspaceId, - // channel: req.headers?.['user-agent']?.toLowerCase().includes('mozilla') ? 'web' : 'cli', - // userAgent: req.headers?.['user-agent'] - // } - // }); - // } + if (postHogClient) { + postHogClient.capture({ + event: 'secrets modified', + distinctId: req.user.email, + properties: { + numberOfSecrets: 1, + environment: environmentName, + workspaceId, + channel: req.headers?.['user-agent']?.toLowerCase().includes('mozilla') ? 'web' : 'cli', + userAgent: req.headers?.['user-agent'] + } + }); + } return res.status(200).send(singleModificationUpdate) } @@ -332,13 +332,16 @@ export const getSecrets = async (req: Request, res: Response) => { const { environment } = req.query; const { workspaceId } = req.params; - let userId: string | undefined = undefined // used for getting personal secrets for user + let userId: Types.ObjectId | undefined = undefined // used for getting personal secrets for user + let userEmail: Types.ObjectId | undefined = undefined // used for posthog if (req.user) { - userId = req.user._id.toString(); + userId = req.user._id; + userEmail = req.user.email; } if (req.serviceTokenData) { userId = req.serviceTokenData.user._id + userEmail = req.serviceTokenData.user.email; } const [err, secrets] = await to(Secret.find( @@ -354,19 +357,19 @@ export const getSecrets = async (req: Request, res: Response) => { throw RouteValidationError({ message: "Failed to get secrets, please try again", stack: err.stack }) } - // if (postHogClient) { - // postHogClient.capture({ - // event: 'secrets pulled', - // distinctId: req.user.email, - // properties: { - // numberOfSecrets: (secrets ?? []).length, - // environment, - // workspaceId, - // channel: req.headers?.['user-agent']?.toLowerCase().includes('mozilla') ? 'web' : 'cli', - // userAgent: req.headers?.['user-agent'] - // } - // }); - // } + if (postHogClient) { + postHogClient.capture({ + event: 'secrets pulled', + distinctId: userEmail, + properties: { + numberOfSecrets: (secrets ?? []).length, + environment, + workspaceId, + channel: req.headers?.['user-agent']?.toLowerCase().includes('mozilla') ? 'web' : 'cli', + userAgent: req.headers?.['user-agent'] + } + }); + } return res.json(secrets) } diff --git a/backend/src/controllers/v2/secretsController.ts b/backend/src/controllers/v2/secretsController.ts index 43434df63a..77a318fc64 100644 --- a/backend/src/controllers/v2/secretsController.ts +++ b/backend/src/controllers/v2/secretsController.ts @@ -2,8 +2,8 @@ import to from 'await-to-js'; import { Types } from 'mongoose'; import { Request, Response } from 'express'; import { ISecret, Secret } from '../../models'; -import { - SECRET_PERSONAL, +import { + SECRET_PERSONAL, SECRET_SHARED, ACTION_ADD_SECRETS, ACTION_READ_SECRETS, @@ -11,6 +11,8 @@ import { ACTION_DELETE_SECRETS } from '../../variables'; import { ValidationError } from '../../utils/errors'; +import { EventService } from '../../services'; +import { eventPushSecrets } from '../../events'; import { EESecretService, EELogService } from '../../ee/services'; import { postHogClient } from '../../services'; import { BadRequestError } from '../../utils/errors'; @@ -73,9 +75,9 @@ export const createSecrets = async (req: Request, res: Response) => { } } */ - const channel = req.headers?.['user-agent']?.toLowerCase().includes('mozilla') ? 'web' : 'cli'; + const channel = req.headers?.['user-agent']?.toLowerCase().includes('mozilla') ? 'web' : 'cli'; const { workspaceId, environment } = req.body; - + let toAdd; if (Array.isArray(req.body.secrets)) { // case: create multiple secrets @@ -84,7 +86,7 @@ export const createSecrets = async (req: Request, res: Response) => { // case: create 1 secret toAdd = [req.body.secrets]; } - + const newSecrets = await Secret.insertMany( toAdd.map(({ type, @@ -116,9 +118,18 @@ export const createSecrets = async (req: Request, res: Response) => { secretValueTag })) ); - + + setTimeout(async () => { + // trigger event - push secrets + await EventService.handleEvent({ + event: eventPushSecrets({ + workspaceId + }) + }); + }, 5000); + // (EE) add secret versions for new secrets - EESecretService.addSecretVersions({ + await EESecretService.addSecretVersions({ secretVersions: newSecrets.map(({ _id, version, @@ -242,22 +253,25 @@ export const getSecrets = async (req: Request, res: Response) => { } */ const { workspaceId, environment } = req.query; - + let userId: Types.ObjectId | undefined = undefined // used for getting personal secrets for user + let userEmail: Types.ObjectId | undefined = undefined // used for posthog if (req.user) { userId = req.user._id; + userEmail = req.user.email; } if (req.serviceTokenData) { userId = req.serviceTokenData.user._id + userEmail = req.serviceTokenData.user.email; } - + const [err, secrets] = await to(Secret.find( { workspace: workspaceId, environment, $or: [ - { user: userId }, + { user: userId }, { user: { $exists: false } } ], type: { $in: [SECRET_SHARED, SECRET_PERSONAL] } @@ -265,9 +279,9 @@ export const getSecrets = async (req: Request, res: Response) => { ).then()) if (err) throw ValidationError({ message: 'Failed to get secrets', stack: err.stack }); - + const channel = req.headers?.['user-agent']?.toLowerCase().includes('mozilla') ? 'web' : 'cli'; - + const readAction = await EELogService.createActionSecret({ name: ACTION_READ_SECRETS, userId: req.user._id.toString(), @@ -285,8 +299,8 @@ export const getSecrets = async (req: Request, res: Response) => { if (postHogClient) { postHogClient.capture({ - event: 'secrets deleted', - distinctId: req.user.email, + event: 'secrets pulled', + distinctId: userEmail, properties: { numberOfSecrets: secrets.length, environment, @@ -296,7 +310,7 @@ export const getSecrets = async (req: Request, res: Response) => { } }); } - + return res.status(200).send({ secrets }); @@ -352,8 +366,8 @@ export const updateSecrets = async (req: Request, res: Response) => { } } */ - const channel = req.headers?.['user-agent']?.toLowerCase().includes('mozilla') ? 'web' : 'cli'; - + const channel = req.headers?.['user-agent']?.toLowerCase().includes('mozilla') ? 'web' : 'cli'; + // TODO: move type interface PatchSecret { id: string; @@ -368,7 +382,7 @@ export const updateSecrets = async (req: Request, res: Response) => { secretCommentTag: string; } - const ops = req.body.secrets.map((secret: PatchSecret) => { + const updateOperationsToPerform = req.body.secrets.map((secret: PatchSecret) => { const { secretKeyCiphertext, secretKeyIV, @@ -380,6 +394,7 @@ export const updateSecrets = async (req: Request, res: Response) => { secretCommentIV, secretCommentTag } = secret; + return ({ updateOne: { filter: { _id: new Types.ObjectId(secret.id) }, @@ -394,8 +409,8 @@ export const updateSecrets = async (req: Request, res: Response) => { secretValueIV, secretValueTag, ...(( - secretCommentCiphertext && - secretCommentIV && + secretCommentCiphertext && + secretCommentIV && secretCommentTag ) ? { secretCommentCiphertext, @@ -406,15 +421,17 @@ export const updateSecrets = async (req: Request, res: Response) => { } }); }); - await Secret.bulkWrite(ops); - - const newSecretsObj: { [key: string]: PatchSecret } = {}; + + await Secret.bulkWrite(updateOperationsToPerform); + + const secretModificationsBySecretId: { [key: string]: PatchSecret } = {}; req.body.secrets.forEach((secret: PatchSecret) => { - newSecretsObj[secret.id] = secret; + secretModificationsBySecretId[secret.id] = secret; }); - await EESecretService.addSecretVersions({ - secretVersions: req.secrets.map((secret: ISecret) => { + const ListOfSecretsBeforeModifications = req.secrets + const secretVersions = { + secretVersions: ListOfSecretsBeforeModifications.map((secret: ISecret) => { const { secretKeyCiphertext, secretKeyIV, @@ -424,37 +441,30 @@ export const updateSecrets = async (req: Request, res: Response) => { secretValueTag, secretCommentCiphertext, secretCommentIV, - secretCommentTag - } = newSecretsObj[secret._id.toString()] + secretCommentTag, + } = secretModificationsBySecretId[secret._id.toString()] + return ({ secret: secret._id, version: secret.version + 1, workspace: secret.workspace, type: secret.type, environment: secret.environment, - isDeleted: false, - secretKeyCiphertext, - secretKeyIV, - secretKeyTag, - secretValueCiphertext, - secretValueIV, - secretValueTag, - ...(( - secretCommentCiphertext && - secretCommentIV && - secretCommentTag - ) ? { - secretCommentCiphertext, - secretCommentIV, - secretCommentTag - } : { - secretCommentCiphertext: '', - secretCommentIV: '', - secretCommentTag: '' - }) + secretKeyCiphertext: secretKeyCiphertext ? secretKeyCiphertext : secret.secretKeyCiphertext, + secretKeyIV: secretKeyIV ? secretKeyIV : secret.secretKeyIV, + secretKeyTag: secretKeyTag ? secretKeyTag : secret.secretKeyTag, + secretValueCiphertext: secretValueCiphertext ? secretValueCiphertext : secret.secretValueCiphertext, + secretValueIV: secretValueIV ? secretValueIV : secret.secretValueIV, + secretValueTag: secretValueTag ? secretValueTag : secret.secretValueTag, + secretCommentCiphertext: secretCommentCiphertext ? secretCommentCiphertext : secret.secretCommentCiphertext, + secretCommentIV: secretCommentIV ? secretCommentIV : secret.secretCommentIV, + secretCommentTag: secretCommentTag ? secretCommentTag : secret.secretCommentTag, }); }) - }); + } + + await EESecretService.addSecretVersions(secretVersions); + // group secrets into workspaces so updated secrets can // be logged and snapshotted separately for each workspace @@ -468,12 +478,21 @@ export const updateSecrets = async (req: Request, res: Response) => { }); Object.keys(workspaceSecretObj).forEach(async (key) => { + // trigger event - push secrets + setTimeout(async () => { + await EventService.handleEvent({ + event: eventPushSecrets({ + workspaceId: key + }) + }); + }, 10000); + const updateAction = await EELogService.createActionSecret({ name: ACTION_UPDATE_SECRETS, userId: req.user._id.toString(), workspaceId: key, secretIds: workspaceSecretObj[key].map((secret: ISecret) => secret._id) - }); + }); // (EE) create (audit) log updateAction && await EELogService.createLog({ @@ -485,9 +504,9 @@ export const updateSecrets = async (req: Request, res: Response) => { }); // (EE) take a secret snapshot - await EESecretService.takeSecretSnapshot({ - workspaceId: key - }) + await EESecretService.takeSecretSnapshot({ + workspaceId: key + }) if (postHogClient) { postHogClient.capture({ @@ -503,7 +522,7 @@ export const updateSecrets = async (req: Request, res: Response) => { }); } }); - + return res.status(200).send({ secrets: await Secret.find({ _id: { @@ -563,15 +582,15 @@ export const deleteSecrets = async (req: Request, res: Response) => { } } */ - const channel = req.headers?.['user-agent']?.toLowerCase().includes('mozilla') ? 'web' : 'cli'; + const channel = req.headers?.['user-agent']?.toLowerCase().includes('mozilla') ? 'web' : 'cli'; const toDelete = req.secrets.map((s: any) => s._id); - + await Secret.deleteMany({ _id: { $in: toDelete } }); - + await EESecretService.markDeletedSecretVersions({ secretIds: toDelete }); @@ -588,12 +607,18 @@ export const deleteSecrets = async (req: Request, res: Response) => { }); Object.keys(workspaceSecretObj).forEach(async (key) => { + // trigger event - push secrets + await EventService.handleEvent({ + event: eventPushSecrets({ + workspaceId: key + }) + }); const deleteAction = await EELogService.createActionSecret({ name: ACTION_DELETE_SECRETS, userId: req.user._id.toString(), workspaceId: key, secretIds: workspaceSecretObj[key].map((secret: ISecret) => secret._id) - }); + }); // (EE) create (audit) log deleteAction && await EELogService.createLog({ @@ -605,9 +630,9 @@ export const deleteSecrets = async (req: Request, res: Response) => { }); // (EE) take a secret snapshot - await EESecretService.takeSecretSnapshot({ - workspaceId: key - }) + await EESecretService.takeSecretSnapshot({ + workspaceId: key + }) if (postHogClient) { postHogClient.capture({ @@ -623,7 +648,7 @@ export const deleteSecrets = async (req: Request, res: Response) => { }); } }); - + return res.status(200).send({ secrets: req.secrets }); diff --git a/backend/src/controllers/v2/workspaceController.ts b/backend/src/controllers/v2/workspaceController.ts index 2b3aeda1dc..3f929ee4a5 100644 --- a/backend/src/controllers/v2/workspaceController.ts +++ b/backend/src/controllers/v2/workspaceController.ts @@ -21,7 +21,6 @@ import { import { pushKeys } from '../../helpers/key'; import { postHogClient, EventService } from '../../services'; import { eventPushSecrets } from '../../events'; -import { ENV_SET } from '../../variables'; interface V2PushSecret { type: string; // personal or shared @@ -54,7 +53,8 @@ export const pushWorkspaceSecrets = async (req: Request, res: Response) => { const { workspaceId } = req.params; // validate environment - if (!ENV_SET.has(environment)) { + const workspaceEnvs = req.membership.workspace.environments; + if (!workspaceEnvs.find(({ slug }: { slug: string }) => slug === environment)) { throw new Error('Failed to validate environment'); } @@ -131,6 +131,11 @@ export const pullSecrets = async (req: Request, res: Response) => { } else if (req.serviceTokenData) { userId = req.serviceTokenData.user._id } + // validate environment + const workspaceEnvs = req.membership.workspace.environments; + if (!workspaceEnvs.find(({ slug }: { slug: string }) => slug === environment)) { + throw new Error('Failed to validate environment'); + } secrets = await pull({ userId, diff --git a/backend/src/ee/helpers/secret.ts b/backend/src/ee/helpers/secret.ts index 529c9a9801..7edee915f0 100644 --- a/backend/src/ee/helpers/secret.ts +++ b/backend/src/ee/helpers/secret.ts @@ -1,11 +1,11 @@ import { Types } from 'mongoose'; import * as Sentry from '@sentry/node'; import { - Secret, + Secret, ISecret } from '../../models'; import { - SecretSnapshot, + SecretSnapshot, SecretVersion, ISecretVersion } from '../models'; @@ -18,24 +18,24 @@ import { * @param {String} obj.workspaceId * @returns {SecretSnapshot} secretSnapshot - new secret snapshot */ - const takeSecretSnapshotHelper = async ({ +const takeSecretSnapshotHelper = async ({ workspaceId }: { workspaceId: string; }) => { - + let secretSnapshot; try { const secretIds = (await Secret.find({ workspace: workspaceId }, '_id')).map((s) => s._id); - + const latestSecretVersions = (await SecretVersion.aggregate([ { - $match: { - secret: { - $in: secretIds - } + $match: { + secret: { + $in: secretIds + } } }, { @@ -48,14 +48,14 @@ import { { $sort: { version: -1 } } - ]) + ]) .exec()) .map((s) => s.versionId); - + const latestSecretSnapshot = await SecretSnapshot.findOne({ workspace: workspaceId }).sort({ version: -1 }); - + secretSnapshot = await new SecretSnapshot({ workspace: workspaceId, version: latestSecretSnapshot ? latestSecretSnapshot.version + 1 : 1, @@ -66,7 +66,7 @@ import { Sentry.captureException(err); throw new Error('Failed to take a secret snapshot'); } - + return secretSnapshot; } @@ -87,9 +87,9 @@ const addSecretVersionsHelper = async ({ } catch (err) { Sentry.setUser(null); Sentry.captureException(err); - throw new Error('Failed to add secret versions'); + throw new Error(`Failed to add secret versions [err=${err}]`); } - + return newSecretVersions; } @@ -120,39 +120,39 @@ const markDeletedSecretVersionsHelper = async ({ const initSecretVersioningHelper = async () => { try { - await Secret.updateMany( + await Secret.updateMany( { version: { $exists: false } }, { $set: { version: 1 } } ); - - const unversionedSecrets: ISecret[] = await Secret.aggregate([ - { - $lookup: { - from: 'secretversions', - localField: '_id', - foreignField: 'secret', - as: 'versions', - }, - }, - { - $match: { - versions: { $size: 0 }, - }, - }, - ]); - - if (unversionedSecrets.length > 0) { - await addSecretVersionsHelper({ - secretVersions: unversionedSecrets.map((s, idx) => ({ - ...s, - secret: s._id, - version: s.version ? s.version : 1, - isDeleted: false, - workspace: s.workspace, - environment: s.environment - })) - }); - } + + const unversionedSecrets: ISecret[] = await Secret.aggregate([ + { + $lookup: { + from: 'secretversions', + localField: '_id', + foreignField: 'secret', + as: 'versions', + }, + }, + { + $match: { + versions: { $size: 0 }, + }, + }, + ]); + + if (unversionedSecrets.length > 0) { + await addSecretVersionsHelper({ + secretVersions: unversionedSecrets.map((s, idx) => ({ + ...s, + secret: s._id, + version: s.version ? s.version : 1, + isDeleted: false, + workspace: s.workspace, + environment: s.environment + })) + }); + } } catch (err) { Sentry.setUser(null); @@ -162,7 +162,7 @@ const initSecretVersioningHelper = async () => { } export { - takeSecretSnapshotHelper, + takeSecretSnapshotHelper, addSecretVersionsHelper, markDeletedSecretVersionsHelper, initSecretVersioningHelper diff --git a/backend/src/ee/models/secretVersion.ts b/backend/src/ee/models/secretVersion.ts index 616d44fbdc..1af4aff2c3 100644 --- a/backend/src/ee/models/secretVersion.ts +++ b/backend/src/ee/models/secretVersion.ts @@ -2,22 +2,18 @@ import { Schema, model, Types } from 'mongoose'; import { SECRET_SHARED, SECRET_PERSONAL, - ENV_DEV, - ENV_TESTING, - ENV_STAGING, - ENV_PROD } from '../../variables'; export interface ISecretVersion { _id: Types.ObjectId; - secret: Types.ObjectId; - version: number; + secret: Types.ObjectId; + version: number; workspace: Types.ObjectId; // new type: string; // new user: Types.ObjectId; // new environment: string; // new - isDeleted: boolean; - secretKeyCiphertext: string; + isDeleted: boolean; + secretKeyCiphertext: string; secretKeyIV: string; secretKeyTag: string; secretKeyHash: string; @@ -28,17 +24,17 @@ export interface ISecretVersion { } const secretVersionSchema = new Schema( - { - secret: { // could be deleted - type: Schema.Types.ObjectId, - ref: 'Secret', - required: true - }, - version: { - type: Number, - default: 1, - required: true - }, + { + secret: { // could be deleted + type: Schema.Types.ObjectId, + ref: 'Secret', + required: true + }, + version: { + type: Number, + default: 1, + required: true + }, workspace: { type: Schema.Types.ObjectId, ref: 'Workspace', @@ -56,15 +52,14 @@ const secretVersionSchema = new Schema( }, environment: { type: String, - enum: [ENV_DEV, ENV_TESTING, ENV_STAGING, ENV_PROD], required: true }, - isDeleted: { // consider removing field - type: Boolean, - default: false, - required: true - }, - secretKeyCiphertext: { + isDeleted: { // consider removing field + type: Boolean, + default: false, + required: true + }, + secretKeyCiphertext: { type: String, required: true }, @@ -94,10 +89,10 @@ const secretVersionSchema = new Schema( secretValueHash: { type: String } - }, - { - timestamps: true - } + }, + { + timestamps: true + } ); const SecretVersion = model('SecretVersion', secretVersionSchema); diff --git a/backend/src/helpers/integration.ts b/backend/src/helpers/integration.ts index d92156ece0..f602f17291 100644 --- a/backend/src/helpers/integration.ts +++ b/backend/src/helpers/integration.ts @@ -7,8 +7,6 @@ import { import { exchangeCode, exchangeRefresh, syncSecrets } from '../integrations'; import { BotService } from '../services'; import { - ENV_DEV, - EVENT_PUSH_SECRETS, INTEGRATION_VERCEL, INTEGRATION_NETLIFY } from '../variables'; @@ -36,11 +34,13 @@ interface Update { const handleOAuthExchangeHelper = async ({ workspaceId, integration, - code + code, + environment }: { workspaceId: string; integration: string; code: string; + environment: string; }) => { let action; let integrationAuth; @@ -102,9 +102,9 @@ const handleOAuthExchangeHelper = async ({ // initialize new integration after exchange await new Integration({ workspace: workspaceId, - environment: ENV_DEV, isActive: false, app: null, + environment, integration, integrationAuth: integrationAuth._id }).save(); diff --git a/backend/src/helpers/secret.ts b/backend/src/helpers/secret.ts index 59d72f4695..74ba062bda 100644 --- a/backend/src/helpers/secret.ts +++ b/backend/src/helpers/secret.ts @@ -39,7 +39,7 @@ const validateSecrets = async ({ try { secrets = await Secret.find({ _id: { - $in: secretIds + $in: secretIds.map((secretId: string) => new Types.ObjectId(secretId)) } }); diff --git a/backend/src/integrations/apps.ts b/backend/src/integrations/apps.ts index e3b78c4811..02a1904c70 100644 --- a/backend/src/integrations/apps.ts +++ b/backend/src/integrations/apps.ts @@ -9,14 +9,9 @@ import { INTEGRATION_GITHUB, INTEGRATION_HEROKU_API_URL, INTEGRATION_VERCEL_API_URL, - INTEGRATION_NETLIFY_API_URL, - INTEGRATION_GITHUB_API_URL + INTEGRATION_NETLIFY_API_URL } from '../variables'; -interface GitHubApp { - name: string; -} - /** * Return list of names of apps for integration named [integration] * @param {Object} obj @@ -47,6 +42,7 @@ const getApps = async ({ break; case INTEGRATION_VERCEL: apps = await getAppsVercel({ + integrationAuth, accessToken }); break; @@ -110,17 +106,28 @@ const getAppsHeroku = async ({ accessToken }: { accessToken: string }) => { * @returns {Object[]} apps - names of Vercel apps * @returns {String} apps.name - name of Vercel app */ -const getAppsVercel = async ({ accessToken }: { accessToken: string }) => { +const getAppsVercel = async ({ + integrationAuth, + accessToken +}: { + integrationAuth: IIntegrationAuth; + accessToken: string; +}) => { let apps; try { const res = ( await axios.get(`${INTEGRATION_VERCEL_API_URL}/v9/projects`, { headers: { Authorization: `Bearer ${accessToken}` + }, + ...( integrationAuth?.teamId ? { + params: { + teamId: integrationAuth.teamId } + } : {}) }) ).data; - + apps = res.projects.map((a: any) => ({ name: a.name })); diff --git a/backend/src/integrations/exchange.ts b/backend/src/integrations/exchange.ts index 3e70761187..26aca5fdb2 100644 --- a/backend/src/integrations/exchange.ts +++ b/backend/src/integrations/exchange.ts @@ -8,8 +8,7 @@ import { INTEGRATION_HEROKU_TOKEN_URL, INTEGRATION_VERCEL_TOKEN_URL, INTEGRATION_NETLIFY_TOKEN_URL, - INTEGRATION_GITHUB_TOKEN_URL, - INTEGRATION_GITHUB_API_URL + INTEGRATION_GITHUB_TOKEN_URL } from '../variables'; import { SITE_URL, diff --git a/backend/src/integrations/sync.ts b/backend/src/integrations/sync.ts index 30628fb9a5..0ae57dc594 100644 --- a/backend/src/integrations/sync.ts +++ b/backend/src/integrations/sync.ts @@ -1,4 +1,4 @@ -import axios from 'axios'; +import axios, { AxiosError } from 'axios'; import * as Sentry from '@sentry/node'; import { Octokit } from '@octokit/rest'; // import * as sodium from 'libsodium-wrappers'; @@ -12,14 +12,10 @@ import { INTEGRATION_GITHUB, INTEGRATION_HEROKU_API_URL, INTEGRATION_VERCEL_API_URL, - INTEGRATION_NETLIFY_API_URL, - INTEGRATION_GITHUB_API_URL + INTEGRATION_NETLIFY_API_URL } from '../variables'; import { access, appendFile } from 'fs'; -// TODO: need a helper function in the future to handle integration -// envar priorities (i.e. prioritize secrets within integration or those on Infisical) - /** * Sync/push [secrets] to [app] in integration named [integration] * @param {Object} obj @@ -53,6 +49,7 @@ const syncSecrets = async ({ case INTEGRATION_VERCEL: await syncSecretsVercel({ integration, + integrationAuth, secrets, accessToken }); @@ -139,14 +136,15 @@ const syncSecretsHeroku = async ({ */ const syncSecretsVercel = async ({ integration, + integrationAuth, secrets, accessToken }: { integration: IIntegration, + integrationAuth: IIntegrationAuth, secrets: any; accessToken: string; }) => { - interface VercelSecret { id?: string; type: string; @@ -156,129 +154,135 @@ const syncSecretsVercel = async ({ } try { - // Get all (decrypted) secrets back from Vercel in - // decrypted format - const params = new URLSearchParams({ - decrypt: "true" - }); - - const res = (await Promise.all((await axios.get( - `${INTEGRATION_VERCEL_API_URL}/v9/projects/${integration.app}/env`, - { + // Get all (decrypted) secrets back from Vercel in + // decrypted format + const params: { [key: string]: string } = { + decrypt: 'true', + ...( integrationAuth?.teamId ? { + teamId: integrationAuth.teamId + } : {}) + } + + const res = (await Promise.all((await axios.get( + `${INTEGRATION_VERCEL_API_URL}/v9/projects/${integration.app}/env`, + { + params, + headers: { + Authorization: `Bearer ${accessToken}` + } + } + )) + .data + .envs + .filter((secret: VercelSecret) => secret.target.includes(integration.target)) + .map(async (secret: VercelSecret) => (await axios.get( + `${INTEGRATION_VERCEL_API_URL}/v9/projects/${integration.app}/env/${secret.id}`, + { params, headers: { Authorization: `Bearer ${accessToken}` } - } - )) - .data - .envs - .filter((secret: VercelSecret) => secret.target.includes(integration.target)) - .map(async (secret: VercelSecret) => (await axios.get( - `${INTEGRATION_VERCEL_API_URL}/v9/projects/${integration.app}/env/${secret.id}`, - { - headers: { - Authorization: `Bearer ${accessToken}` - } - - } - )).data) - )).reduce((obj: any, secret: any) => ({ - ...obj, - [secret.key]: secret - }), {}); - - const updateSecrets: VercelSecret[] = []; - const deleteSecrets: VercelSecret[] = []; - const newSecrets: VercelSecret[] = []; + } + )).data) + )).reduce((obj: any, secret: any) => ({ + ...obj, + [secret.key]: secret + }), {}); + + const updateSecrets: VercelSecret[] = []; + const deleteSecrets: VercelSecret[] = []; + const newSecrets: VercelSecret[] = []; - // Identify secrets to create - Object.keys(secrets).map((key) => { - if (!(key in res)) { - // case: secret has been created - newSecrets.push({ - key: key, - value: secrets[key], - type: 'encrypted', - target: [integration.target] - }); - } - }); - - // Identify secrets to update and delete - Object.keys(res).map((key) => { - if (key in secrets) { - if (res[key].value !== secrets[key]) { - // case: secret value has changed - updateSecrets.push({ - id: res[key].id, - key: key, - value: secrets[key], - type: 'encrypted', - target: [integration.target] - }); - } - } else { - // case: secret has been deleted - deleteSecrets.push({ - id: res[key].id, - key: key, - value: res[key].value, - type: 'encrypted', - target: [integration.target], - }); - } - }); + // Identify secrets to create + Object.keys(secrets).map((key) => { + if (!(key in res)) { + // case: secret has been created + newSecrets.push({ + key: key, + value: secrets[key], + type: 'encrypted', + target: [integration.target] + }); + } + }); + + // Identify secrets to update and delete + Object.keys(res).map((key) => { + if (key in secrets) { + if (res[key].value !== secrets[key]) { + // case: secret value has changed + updateSecrets.push({ + id: res[key].id, + key: key, + value: secrets[key], + type: 'encrypted', + target: [integration.target] + }); + } + } else { + // case: secret has been deleted + deleteSecrets.push({ + id: res[key].id, + key: key, + value: res[key].value, + type: 'encrypted', + target: [integration.target], + }); + } + }); - // Sync/push new secrets - if (newSecrets.length > 0) { - await axios.post( - `${INTEGRATION_VERCEL_API_URL}/v10/projects/${integration.app}/env`, - newSecrets, - { - headers: { - Authorization: `Bearer ${accessToken}` - } + // Sync/push new secrets + if (newSecrets.length > 0) { + await axios.post( + `${INTEGRATION_VERCEL_API_URL}/v10/projects/${integration.app}/env`, + newSecrets, + { + params, + headers: { + Authorization: `Bearer ${accessToken}` } - ); - } + } + ); + } - // Sync/push updated secrets - if (updateSecrets.length > 0) { - updateSecrets.forEach(async (secret: VercelSecret) => { - const { - id, - ...updatedSecret - } = secret; - await axios.patch( - `${INTEGRATION_VERCEL_API_URL}/v9/projects/${integration.app}/env/${secret.id}`, - updatedSecret, - { - headers: { - Authorization: `Bearer ${accessToken}` - } + // Sync/push updated secrets + if (updateSecrets.length > 0) { + updateSecrets.forEach(async (secret: VercelSecret) => { + const { + id, + ...updatedSecret + } = secret; + await axios.patch( + `${INTEGRATION_VERCEL_API_URL}/v9/projects/${integration.app}/env/${secret.id}`, + updatedSecret, + { + params, + headers: { + Authorization: `Bearer ${accessToken}` } - ); - }); - } + } + ); + }); + } - // Delete secrets - if (deleteSecrets.length > 0) { - deleteSecrets.forEach(async (secret: VercelSecret) => { - await axios.delete( - `${INTEGRATION_VERCEL_API_URL}/v9/projects/${integration.app}/env/${secret.id}`, - { - headers: { - Authorization: `Bearer ${accessToken}` - } + // Delete secrets + if (deleteSecrets.length > 0) { + deleteSecrets.forEach(async (secret: VercelSecret) => { + await axios.delete( + `${INTEGRATION_VERCEL_API_URL}/v9/projects/${integration.app}/env/${secret.id}`, + { + params, + headers: { + Authorization: `Bearer ${accessToken}` } - ); - }); - } + } + ); + }); + } } catch (err) { - Sentry.setUser(null); - Sentry.captureException(err); - throw new Error('Failed to sync secrets to Vercel'); + Sentry.setUser(null); + Sentry.captureException(err); + throw new Error('Failed to sync secrets to Vercel'); } } @@ -302,188 +306,188 @@ const syncSecretsNetlify = async ({ }) => { try { - interface NetlifyValue { - id?: string; - context: string; // 'dev' | 'branch-deploy' | 'deploy-preview' | 'production', - value: string; - } - - interface NetlifySecret { - key: string; - values: NetlifyValue[]; - } - - interface NetlifySecretsRes { - [index: string]: NetlifySecret; - } - - const getParams = new URLSearchParams({ - context_name: 'all', // integration.context or all - site_id: integration.siteId - }); - - const res = (await axios.get( - `${INTEGRATION_NETLIFY_API_URL}/api/v1/accounts/${integrationAuth.accountId}/env`, - { - params: getParams, - headers: { - Authorization: `Bearer ${accessToken}` - } - } - )) - .data - .reduce((obj: any, secret: any) => ({ - ...obj, - [secret.key]: secret - }), {}); - - const newSecrets: NetlifySecret[] = []; // createEnvVars - const deleteSecrets: string[] = []; // deleteEnvVar - const deleteSecretValues: NetlifySecret[] = []; // deleteEnvVarValue - const updateSecrets: NetlifySecret[] = []; // setEnvVarValue - - // identify secrets to create and update - Object.keys(secrets).map((key) => { - if (!(key in res)) { - // case: Infisical secret does not exist in Netlify -> create secret - newSecrets.push({ - key, - values: [{ - value: secrets[key], - context: integration.context - }] - }); - } else { - // case: Infisical secret exists in Netlify - const contexts = res[key].values - .reduce((obj: any, value: NetlifyValue) => ({ - ...obj, - [value.context]: value - }), {}); - - if (integration.context in contexts) { - // case: Netlify secret value exists in integration context - if (secrets[key] !== contexts[integration.context].value) { - // case: Infisical and Netlify secret values are different - // -> update Netlify secret context and value - updateSecrets.push({ - key, - values: [{ - context: integration.context, - value: secrets[key] - }] - }); - } - } else { - // case: Netlify secret value does not exist in integration context - // -> add the new Netlify secret context and value - updateSecrets.push({ - key, - values: [{ - context: integration.context, - value: secrets[key] - }] - }); - } - } - }) - - // identify secrets to delete - // TODO: revise (patch case where 1 context was deleted but others still there - Object.keys(res).map((key) => { - // loop through each key's context - if (!(key in secrets)) { - // case: Netlify secret does not exist in Infisical - - const numberOfValues = res[key].values.length; - - res[key].values.forEach((value: NetlifyValue) => { - if (value.context === integration.context) { - if (numberOfValues <= 1) { - // case: Netlify secret value has less than 1 context -> delete secret - deleteSecrets.push(key); - } else { - // case: Netlify secret value has more than 1 context -> delete secret value context - deleteSecretValues.push({ - key, - values: [{ - id: value.id, - context: integration.context, - value: value.value - }] - }); - } - } - }); - } - }); + interface NetlifyValue { + id?: string; + context: string; // 'dev' | 'branch-deploy' | 'deploy-preview' | 'production', + value: string; + } + + interface NetlifySecret { + key: string; + values: NetlifyValue[]; + } + + interface NetlifySecretsRes { + [index: string]: NetlifySecret; + } + + const getParams = new URLSearchParams({ + context_name: 'all', // integration.context or all + site_id: integration.siteId + }); + + const res = (await axios.get( + `${INTEGRATION_NETLIFY_API_URL}/api/v1/accounts/${integrationAuth.accountId}/env`, + { + params: getParams, + headers: { + Authorization: `Bearer ${accessToken}` + } + } + )) + .data + .reduce((obj: any, secret: any) => ({ + ...obj, + [secret.key]: secret + }), {}); + + const newSecrets: NetlifySecret[] = []; // createEnvVars + const deleteSecrets: string[] = []; // deleteEnvVar + const deleteSecretValues: NetlifySecret[] = []; // deleteEnvVarValue + const updateSecrets: NetlifySecret[] = []; // setEnvVarValue + + // identify secrets to create and update + Object.keys(secrets).map((key) => { + if (!(key in res)) { + // case: Infisical secret does not exist in Netlify -> create secret + newSecrets.push({ + key, + values: [{ + value: secrets[key], + context: integration.context + }] + }); + } else { + // case: Infisical secret exists in Netlify + const contexts = res[key].values + .reduce((obj: any, value: NetlifyValue) => ({ + ...obj, + [value.context]: value + }), {}); + + if (integration.context in contexts) { + // case: Netlify secret value exists in integration context + if (secrets[key] !== contexts[integration.context].value) { + // case: Infisical and Netlify secret values are different + // -> update Netlify secret context and value + updateSecrets.push({ + key, + values: [{ + context: integration.context, + value: secrets[key] + }] + }); + } + } else { + // case: Netlify secret value does not exist in integration context + // -> add the new Netlify secret context and value + updateSecrets.push({ + key, + values: [{ + context: integration.context, + value: secrets[key] + }] + }); + } + } + }) + + // identify secrets to delete + // TODO: revise (patch case where 1 context was deleted but others still there + Object.keys(res).map((key) => { + // loop through each key's context + if (!(key in secrets)) { + // case: Netlify secret does not exist in Infisical + + const numberOfValues = res[key].values.length; + + res[key].values.forEach((value: NetlifyValue) => { + if (value.context === integration.context) { + if (numberOfValues <= 1) { + // case: Netlify secret value has less than 1 context -> delete secret + deleteSecrets.push(key); + } else { + // case: Netlify secret value has more than 1 context -> delete secret value context + deleteSecretValues.push({ + key, + values: [{ + id: value.id, + context: integration.context, + value: value.value + }] + }); + } + } + }); + } + }); - const syncParams = new URLSearchParams({ - site_id: integration.siteId - }); + const syncParams = new URLSearchParams({ + site_id: integration.siteId + }); - if (newSecrets.length > 0) { - await axios.post( - `${INTEGRATION_NETLIFY_API_URL}/api/v1/accounts/${integrationAuth.accountId}/env`, - newSecrets, - { - params: syncParams, - headers: { - Authorization: `Bearer ${accessToken}` - } - } - ); - } + if (newSecrets.length > 0) { + await axios.post( + `${INTEGRATION_NETLIFY_API_URL}/api/v1/accounts/${integrationAuth.accountId}/env`, + newSecrets, + { + params: syncParams, + headers: { + Authorization: `Bearer ${accessToken}` + } + } + ); + } - if (updateSecrets.length > 0) { - updateSecrets.forEach(async (secret: NetlifySecret) => { - await axios.patch( - `${INTEGRATION_NETLIFY_API_URL}/api/v1/accounts/${integrationAuth.accountId}/env/${secret.key}`, - { - context: secret.values[0].context, - value: secret.values[0].value - }, - { - params: syncParams, - headers: { - Authorization: `Bearer ${accessToken}` - } - } - ); - }); - } + if (updateSecrets.length > 0) { + updateSecrets.forEach(async (secret: NetlifySecret) => { + await axios.patch( + `${INTEGRATION_NETLIFY_API_URL}/api/v1/accounts/${integrationAuth.accountId}/env/${secret.key}`, + { + context: secret.values[0].context, + value: secret.values[0].value + }, + { + params: syncParams, + headers: { + Authorization: `Bearer ${accessToken}` + } + } + ); + }); + } - if (deleteSecrets.length > 0) { - deleteSecrets.forEach(async (key: string) => { - await axios.delete( - `${INTEGRATION_NETLIFY_API_URL}/api/v1/accounts/${integrationAuth.accountId}/env/${key}`, - { - params: syncParams, - headers: { - Authorization: `Bearer ${accessToken}` - } - } - ); - }); - } + if (deleteSecrets.length > 0) { + deleteSecrets.forEach(async (key: string) => { + await axios.delete( + `${INTEGRATION_NETLIFY_API_URL}/api/v1/accounts/${integrationAuth.accountId}/env/${key}`, + { + params: syncParams, + headers: { + Authorization: `Bearer ${accessToken}` + } + } + ); + }); + } - if (deleteSecretValues.length > 0) { - deleteSecretValues.forEach(async (secret: NetlifySecret) => { - await axios.delete( - `${INTEGRATION_NETLIFY_API_URL}/api/v1/accounts/${integrationAuth.accountId}/env/${secret.key}/value/${secret.values[0].id}`, - { - params: syncParams, - headers: { - Authorization: `Bearer ${accessToken}` - } - } - ); - }); - } + if (deleteSecretValues.length > 0) { + deleteSecretValues.forEach(async (secret: NetlifySecret) => { + await axios.delete( + `${INTEGRATION_NETLIFY_API_URL}/api/v1/accounts/${integrationAuth.accountId}/env/${secret.key}/value/${secret.values[0].id}`, + { + params: syncParams, + headers: { + Authorization: `Bearer ${accessToken}` + } + } + ); + }); + } } catch (err) { - Sentry.setUser(null); - Sentry.captureException(err); - throw new Error('Failed to sync secrets to Heroku'); + Sentry.setUser(null); + Sentry.captureException(err); + throw new Error('Failed to sync secrets to Heroku'); } } diff --git a/backend/src/models/integration.ts b/backend/src/models/integration.ts index 6da699216e..98e4934d90 100644 --- a/backend/src/models/integration.ts +++ b/backend/src/models/integration.ts @@ -1,9 +1,5 @@ import { Schema, model, Types } from 'mongoose'; import { - ENV_DEV, - ENV_TESTING, - ENV_STAGING, - ENV_PROD, INTEGRATION_HEROKU, INTEGRATION_VERCEL, INTEGRATION_NETLIFY, @@ -13,7 +9,7 @@ import { export interface IIntegration { _id: Types.ObjectId; workspace: Types.ObjectId; - environment: 'dev' | 'test' | 'staging' | 'prod'; + environment: string; isActive: boolean; app: string; target: string; @@ -32,7 +28,6 @@ const integrationSchema = new Schema( }, environment: { type: String, - enum: [ENV_DEV, ENV_TESTING, ENV_STAGING, ENV_PROD], required: true }, isActive: { diff --git a/backend/src/models/secret.ts b/backend/src/models/secret.ts index a01b92d80e..6887c8b0f6 100644 --- a/backend/src/models/secret.ts +++ b/backend/src/models/secret.ts @@ -2,10 +2,6 @@ import { Schema, model, Types } from 'mongoose'; import { SECRET_SHARED, SECRET_PERSONAL, - ENV_DEV, - ENV_TESTING, - ENV_STAGING, - ENV_PROD } from '../variables'; export interface ISecret { @@ -53,7 +49,6 @@ const secretSchema = new Schema( }, environment: { type: String, - enum: [ENV_DEV, ENV_TESTING, ENV_STAGING, ENV_PROD], required: true }, secretKeyCiphertext: { diff --git a/backend/src/models/serviceToken.ts b/backend/src/models/serviceToken.ts index b5a2f4ec97..9d91b076ea 100644 --- a/backend/src/models/serviceToken.ts +++ b/backend/src/models/serviceToken.ts @@ -1,7 +1,4 @@ import { Schema, model, Types } from 'mongoose'; -import { ENV_DEV, ENV_TESTING, ENV_STAGING, ENV_PROD } from '../variables'; - -// TODO: deprecate export interface IServiceToken { _id: Types.ObjectId; name: string; @@ -33,7 +30,6 @@ const serviceTokenSchema = new Schema( }, environment: { type: String, - enum: [ENV_DEV, ENV_TESTING, ENV_STAGING, ENV_PROD], required: true }, expiresAt: { diff --git a/backend/src/models/workspace.ts b/backend/src/models/workspace.ts index 1e886c5239..fa7dbc8b58 100644 --- a/backend/src/models/workspace.ts +++ b/backend/src/models/workspace.ts @@ -4,6 +4,10 @@ export interface IWorkspace { _id: Types.ObjectId; name: string; organization: Types.ObjectId; + environments: Array<{ + name: string; + slug: string; + }>; } const workspaceSchema = new Schema({ @@ -15,7 +19,33 @@ const workspaceSchema = new Schema({ type: Schema.Types.ObjectId, ref: 'Organization', required: true - } + }, + environments: { + type: [ + { + name: String, + slug: String, + }, + ], + default: [ + { + name: "development", + slug: "dev" + }, + { + name: "test", + slug: "test" + }, + { + name: "staging", + slug: "staging" + }, + { + name: "production", + slug: "prod" + } + ], + }, }); const Workspace = model('Workspace', workspaceSchema); diff --git a/backend/src/routes/v2/environment.ts b/backend/src/routes/v2/environment.ts new file mode 100644 index 0000000000..924db18fcd --- /dev/null +++ b/backend/src/routes/v2/environment.ts @@ -0,0 +1,57 @@ +import express, { Response, Request } from 'express'; +const router = express.Router(); +import { body, param } from 'express-validator'; +import { environmentController } from '../../controllers/v2'; +import { + requireAuth, + requireWorkspaceAuth, + validateRequest, +} from '../../middleware'; +import { ADMIN, MEMBER } from '../../variables'; + +router.post( + '/:workspaceId/environments', + requireAuth({ + acceptedAuthModes: ['jwt'], + }), + requireWorkspaceAuth({ + acceptedRoles: [ADMIN, MEMBER], + }), + param('workspaceId').exists().trim(), + body('environmentSlug').exists().trim(), + body('environmentName').exists().trim(), + validateRequest, + environmentController.createWorkspaceEnvironment +); + +router.put( + '/:workspaceId/environments', + requireAuth({ + acceptedAuthModes: ['jwt'], + }), + requireWorkspaceAuth({ + acceptedRoles: [ADMIN, MEMBER], + }), + param('workspaceId').exists().trim(), + body('environmentSlug').exists().trim(), + body('environmentName').exists().trim(), + body('oldEnvironmentSlug').exists().trim(), + validateRequest, + environmentController.renameWorkspaceEnvironment +); + +router.delete( + '/:workspaceId/environments', + requireAuth({ + acceptedAuthModes: ['jwt'], + }), + requireWorkspaceAuth({ + acceptedRoles: [ADMIN], + }), + param('workspaceId').exists().trim(), + body('environmentSlug').exists().trim(), + validateRequest, + environmentController.deleteWorkspaceEnvironment +); + +export default router; diff --git a/backend/src/routes/v2/index.ts b/backend/src/routes/v2/index.ts index a30c72642a..cc9375fb72 100644 --- a/backend/src/routes/v2/index.ts +++ b/backend/src/routes/v2/index.ts @@ -4,6 +4,7 @@ import secrets from './secrets'; import workspace from './workspace'; import serviceTokenData from './serviceTokenData'; import apiKeyData from './apiKeyData'; +import environment from "./environment" export { users, @@ -11,5 +12,6 @@ export { secrets, workspace, serviceTokenData, - apiKeyData -} + apiKeyData, + environment +} \ No newline at end of file diff --git a/backend/src/routes/v2/secrets.ts b/backend/src/routes/v2/secrets.ts index 29eac042fe..ccdce53215 100644 --- a/backend/src/routes/v2/secrets.ts +++ b/backend/src/routes/v2/secrets.ts @@ -8,8 +8,8 @@ import { } from '../../middleware'; import { query, check, body } from 'express-validator'; import { secretsController } from '../../controllers/v2'; -import { - ADMIN, +import { + ADMIN, MEMBER, SECRET_PERSONAL, SECRET_SHARED @@ -18,7 +18,7 @@ import { router.post( '/', body('workspaceId').exists().isString().trim(), - body('environment').exists().isString().trim().isIn(['dev', 'staging', 'prod', 'test']), + body('environment').exists().isString().trim(), body('secrets') .exists() .custom((value) => { @@ -27,7 +27,7 @@ router.post( if (value.length === 0) throw new Error('secrets cannot be an empty array') for (const secret of value) { if ( - !secret.type || + !secret.type || !(secret.type === SECRET_PERSONAL || secret.type === SECRET_SHARED) || !secret.secretKeyCiphertext || !secret.secretKeyIV || @@ -42,7 +42,7 @@ router.post( } else if (typeof value === 'object') { // case: update 1 secret if ( - !value.type || + !value.type || !(value.type === SECRET_PERSONAL || value.type === SECRET_SHARED) || !value.secretKeyCiphertext || !value.secretKeyIV || @@ -52,13 +52,13 @@ router.post( !value.secretValueTag ) { throw new Error('secrets object is missing required secret properties'); - } + } } else { throw new Error('secrets must be an object or an array of objects') } - + return true; - }), + }), validateRequest, requireAuth({ acceptedAuthModes: ['jwt'] @@ -73,7 +73,7 @@ router.post( router.get( '/', query('workspaceId').exists().trim(), - query('environment').exists().trim().isIn(['dev', 'staging', 'prod', 'test']), + query('environment').exists().trim(), validateRequest, requireAuth({ acceptedAuthModes: ['jwt', 'serviceToken'] @@ -95,36 +95,24 @@ router.patch( if (value.length === 0) throw new Error('secrets cannot be an empty array') for (const secret of value) { if ( - !secret.id || - !secret.secretKeyCiphertext || - !secret.secretKeyIV || - !secret.secretKeyTag || - !secret.secretValueCiphertext || - !secret.secretValueIV || - !secret.secretValueTag + !secret.id ) { - throw new Error('secrets array must contain objects that have required secret properties'); + throw new Error('Each secret must contain a ID property'); } } } else if (typeof value === 'object') { // case: update 1 secret if ( - !value.id || - !value.secretKeyCiphertext || - !value.secretKeyIV || - !value.secretKeyTag || - !value.secretValueCiphertext || - !value.secretValueIV || - !value.secretValueTag + !value.id ) { - throw new Error('secrets object is missing required secret properties'); - } + throw new Error('secret must contain a ID property'); + } } else { throw new Error('secrets must be an object or an array of objects') } - + return true; - }), + }), validateRequest, requireAuth({ acceptedAuthModes: ['jwt'] @@ -142,13 +130,13 @@ router.delete( .custom((value) => { // case: delete 1 secret if (typeof value === 'string') return true; - + if (Array.isArray(value)) { // case: delete multiple secrets if (value.length === 0) throw new Error('secrets cannot be an empty array'); return value.every((id: string) => typeof id === 'string') } - + throw new Error('secretIds must be a string or an array of strings'); }) .not() diff --git a/backend/src/services/IntegrationService.ts b/backend/src/services/IntegrationService.ts index 32f5f5a88b..43746aee4a 100644 --- a/backend/src/services/IntegrationService.ts +++ b/backend/src/services/IntegrationService.ts @@ -11,10 +11,6 @@ import { setIntegrationAuthAccessHelper, } from '../helpers/integration'; import { exchangeCode } from '../integrations'; -import { - ENV_DEV, - EVENT_PUSH_SECRETS -} from '../variables'; // should sync stuff be here too? Probably. // TODO: move bot functions to IntegrationService. @@ -32,22 +28,26 @@ class IntegrationService { * - 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 */ static async handleOAuthExchange({ workspaceId, integration, - code + code, + environment }: { workspaceId: string; integration: string; code: string; + environment: string; }) { await handleOAuthExchangeHelper({ workspaceId, integration, - code + code, + environment }); } diff --git a/backend/src/services/smtp.ts b/backend/src/services/smtp.ts index 12841eee7d..f960f6bae3 100644 --- a/backend/src/services/smtp.ts +++ b/backend/src/services/smtp.ts @@ -28,7 +28,13 @@ if (SMTP_SECURE) { } break; default: - mailOpts.secure = true; + if (SMTP_HOST.includes('amazonaws.com')) { + mailOpts.tls = { + ciphers: 'TLSv1.2' + } + } else { + mailOpts.secure = true; + } break; } } diff --git a/backend/src/variables/index.ts b/backend/src/variables/index.ts index 16c068925c..4f7ffd8b0f 100644 --- a/backend/src/variables/index.ts +++ b/backend/src/variables/index.ts @@ -19,7 +19,6 @@ import { INTEGRATION_HEROKU_API_URL, INTEGRATION_VERCEL_API_URL, INTEGRATION_NETLIFY_API_URL, - INTEGRATION_GITHUB_API_URL, INTEGRATION_OPTIONS } from './integration'; import { @@ -66,7 +65,6 @@ export { INTEGRATION_HEROKU_API_URL, INTEGRATION_VERCEL_API_URL, INTEGRATION_NETLIFY_API_URL, - INTEGRATION_GITHUB_API_URL, EVENT_PUSH_SECRETS, EVENT_PULL_SECRETS, ACTION_ADD_SECRETS, diff --git a/backend/src/variables/integration.ts b/backend/src/variables/integration.ts index ed18c5a2ac..b298a784a0 100644 --- a/backend/src/variables/integration.ts +++ b/backend/src/variables/integration.ts @@ -11,10 +11,10 @@ const INTEGRATION_VERCEL = 'vercel'; const INTEGRATION_NETLIFY = 'netlify'; const INTEGRATION_GITHUB = 'github'; const INTEGRATION_SET = new Set([ - INTEGRATION_HEROKU, - INTEGRATION_VERCEL, - INTEGRATION_NETLIFY, - INTEGRATION_GITHUB + INTEGRATION_HEROKU, + INTEGRATION_VERCEL, + INTEGRATION_NETLIFY, + INTEGRATION_GITHUB ]); // integration types @@ -23,22 +23,21 @@ const INTEGRATION_OAUTH2 = 'oauth2'; // integration oauth endpoints const INTEGRATION_HEROKU_TOKEN_URL = 'https://id.heroku.com/oauth/token'; const INTEGRATION_VERCEL_TOKEN_URL = - 'https://api.vercel.com/v2/oauth/access_token'; + 'https://api.vercel.com/v2/oauth/access_token'; const INTEGRATION_NETLIFY_TOKEN_URL = 'https://api.netlify.com/oauth/token'; const INTEGRATION_GITHUB_TOKEN_URL = - 'https://github.com/login/oauth/access_token'; + 'https://github.com/login/oauth/access_token'; // integration apps endpoints const INTEGRATION_HEROKU_API_URL = 'https://api.heroku.com'; const INTEGRATION_VERCEL_API_URL = 'https://api.vercel.com'; const INTEGRATION_NETLIFY_API_URL = 'https://api.netlify.com'; -const INTEGRATION_GITHUB_API_URL = 'https://api.github.com'; const INTEGRATION_OPTIONS = [ { name: 'Heroku', slug: 'heroku', - image: 'Heroku', + image: 'Heroku', isAvailable: true, type: 'oauth2', clientId: CLIENT_ID_HEROKU, @@ -47,8 +46,8 @@ const INTEGRATION_OPTIONS = [ { name: 'Vercel', slug: 'vercel', - image: 'Vercel', - isAvailable: false, + image: 'Vercel', + isAvailable: true, type: 'vercel', clientId: '', clientSlug: CLIENT_SLUG_VERCEL, @@ -57,7 +56,7 @@ const INTEGRATION_OPTIONS = [ { name: 'Netlify', slug: 'netlify', - image: 'Netlify', + image: 'Netlify', isAvailable: false, type: 'oauth2', clientId: CLIENT_ID_NETLIFY, @@ -66,17 +65,17 @@ const INTEGRATION_OPTIONS = [ { name: 'GitHub', slug: 'github', - image: 'GitHub', + image: 'GitHub', isAvailable: false, type: 'oauth2', clientId: CLIENT_ID_GITHUB, docsLink: '' - + }, { name: 'Google Cloud Platform', slug: 'gcp', - image: 'Google Cloud Platform', + image: 'Google Cloud Platform', isAvailable: false, type: '', clientId: '', @@ -85,7 +84,7 @@ const INTEGRATION_OPTIONS = [ { name: 'Amazon Web Services', slug: 'aws', - image: 'Amazon Web Services', + image: 'Amazon Web Services', isAvailable: false, type: '', clientId: '', @@ -94,7 +93,7 @@ const INTEGRATION_OPTIONS = [ { name: 'Microsoft Azure', slug: 'azure', - image: 'Microsoft Azure', + image: 'Microsoft Azure', isAvailable: false, type: '', clientId: '', @@ -103,7 +102,7 @@ const INTEGRATION_OPTIONS = [ { name: 'Travis CI', slug: 'travisci', - image: 'Travis CI', + image: 'Travis CI', isAvailable: false, type: '', clientId: '', @@ -112,7 +111,7 @@ const INTEGRATION_OPTIONS = [ { name: 'Circle CI', slug: 'circleci', - image: 'Circle CI', + image: 'Circle CI', isAvailable: false, type: '', clientId: '', @@ -121,19 +120,18 @@ const INTEGRATION_OPTIONS = [ ] export { - INTEGRATION_HEROKU, - INTEGRATION_VERCEL, - INTEGRATION_NETLIFY, - INTEGRATION_GITHUB, - INTEGRATION_SET, - INTEGRATION_OAUTH2, - INTEGRATION_HEROKU_TOKEN_URL, - INTEGRATION_VERCEL_TOKEN_URL, - INTEGRATION_NETLIFY_TOKEN_URL, - INTEGRATION_GITHUB_TOKEN_URL, - INTEGRATION_HEROKU_API_URL, - INTEGRATION_VERCEL_API_URL, - INTEGRATION_NETLIFY_API_URL, - INTEGRATION_GITHUB_API_URL, - INTEGRATION_OPTIONS + INTEGRATION_HEROKU, + INTEGRATION_VERCEL, + INTEGRATION_NETLIFY, + INTEGRATION_GITHUB, + INTEGRATION_SET, + INTEGRATION_OAUTH2, + INTEGRATION_HEROKU_TOKEN_URL, + INTEGRATION_VERCEL_TOKEN_URL, + INTEGRATION_NETLIFY_TOKEN_URL, + INTEGRATION_GITHUB_TOKEN_URL, + INTEGRATION_HEROKU_API_URL, + INTEGRATION_VERCEL_API_URL, + INTEGRATION_NETLIFY_API_URL, + INTEGRATION_OPTIONS }; diff --git a/cli/packages/cmd/export.go b/cli/packages/cmd/export.go index fd04e9ce2c..689f507d38 100644 --- a/cli/packages/cmd/export.go +++ b/cli/packages/cmd/export.go @@ -16,10 +16,11 @@ import ( ) const ( - FormatDotenv string = "dotenv" - FormatJson string = "json" - FormatCSV string = "csv" - FormatYaml string = "yaml" + FormatDotenv string = "dotenv" + FormatJson string = "json" + FormatCSV string = "csv" + FormatYaml string = "yaml" + FormatDotEnvExport string = "dotenv-export" ) // exportCmd represents the export command @@ -85,6 +86,8 @@ func formatEnvs(envs []models.SingleEnvironmentVariable, format string) (string, switch strings.ToLower(format) { case FormatDotenv: return formatAsDotEnv(envs), nil + case FormatDotEnvExport: + return formatAsDotEnvExport(envs), nil case FormatJson: return formatAsJson(envs), nil case FormatCSV: @@ -92,7 +95,7 @@ func formatEnvs(envs []models.SingleEnvironmentVariable, format string) (string, case FormatYaml: return formatAsYaml(envs), nil default: - return "", fmt.Errorf("invalid format type: %s. Available format types are [%s]", format, []string{FormatDotenv, FormatJson, FormatCSV, FormatYaml}) + return "", fmt.Errorf("invalid format type: %s. Available format types are [%s]", format, []string{FormatDotenv, FormatJson, FormatCSV, FormatYaml, FormatDotEnvExport}) } } @@ -117,6 +120,15 @@ func formatAsDotEnv(envs []models.SingleEnvironmentVariable) string { return dotenv } +// Format environment variables as a dotenv file with export at the beginning +func formatAsDotEnvExport(envs []models.SingleEnvironmentVariable) string { + var dotenv string + for _, env := range envs { + dotenv += fmt.Sprintf("export %s='%s'\n", env.Key, env.Value) + } + return dotenv +} + func formatAsYaml(envs []models.SingleEnvironmentVariable) string { var dotenv string for _, env := range envs { diff --git a/cli/packages/cmd/root.go b/cli/packages/cmd/root.go index b1b53d2ff2..708982a22b 100644 --- a/cli/packages/cmd/root.go +++ b/cli/packages/cmd/root.go @@ -15,7 +15,7 @@ var rootCmd = &cobra.Command{ Short: "Infisical CLI is used to inject environment variables into any process", Long: `Infisical is a simple, end-to-end encrypted service that enables teams to sync and manage their environment variables across their development life cycle.`, CompletionOptions: cobra.CompletionOptions{HiddenDefaultCmd: true}, - Version: "0.2.0", + Version: "0.2.2", } // Execute adds all child commands to the root command and sets flags appropriately. diff --git a/cli/packages/util/secrets.go b/cli/packages/util/secrets.go index c0a3ad9526..131a8e66db 100644 --- a/cli/packages/util/secrets.go +++ b/cli/packages/util/secrets.go @@ -117,11 +117,9 @@ func GetAllEnvironmentVariables(envName string) ([]models.SingleEnvironmentVaria secrets, err := GetPlainTextSecretsViaJTW(loggedInUserDetails.UserCredentials.JTWToken, loggedInUserDetails.UserCredentials.PrivateKey, workspaceFile.WorkspaceId, envName) return secrets, err - } else if infisicalToken != "" { + } else { log.Debug("Trying to fetch secrets using service token") return GetPlainTextSecretsViaServiceToken(infisicalToken) - } else { - return nil, fmt.Errorf("unable to fetch secrets because we could not find a service token or a logged in user") } } diff --git a/docs/cli/commands/export.mdx b/docs/cli/commands/export.mdx index b80fb74703..bcbc89e896 100644 --- a/docs/cli/commands/export.mdx +++ b/docs/cli/commands/export.mdx @@ -12,12 +12,12 @@ Export environment variables from the platform into a file format. ## Options -| Option | Description | Default value | -| ------------- | -------------------------------------------------------------------------------------------------------------------------------------------- | ------------- | -| `--env` | Used to set the environment that secrets are pulled from. Accepted values: `dev`, `staging`, `test`, `prod` | `dev` | -| `--projectId` | Only required if injecting via the [service token method](../token). If you are not using service token, the project id will be automatically retrieved from the `.infisical.json` located at the root of your local project. | `None` | -| `--expand` | Parse shell parameter expansions in your secrets (e.g., `${DOMAIN}`) | `true` | -| `--format` | Format of the output file. Accepted values: `dotenv`, `csv` and `json` | `dotenv` | +| Option | Description | Default value | +| ------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------- | +| `--env` | Used to set the environment that secrets are pulled from. Accepted values: `dev`, `staging`, `test`, `prod` | `dev` | +| `--projectId` | Only required if injecting via the [service token method](../token). If you are not using service token, the project id will be automatically retrieved from the `.infisical.json` located at the root of your local project. | `None` | +| `--expand` | Parse shell parameter expansions in your secrets (e.g., `${DOMAIN}`) | `true` | +| `--format` | Format of the output file. Accepted values: `dotenv`, `dotenv-export`, `csv` and `json` | `dotenv` | ## Examples @@ -25,6 +25,9 @@ Export environment variables from the platform into a file format. # Export variables to a .env file infisical export > .env +# Export variables to a .env file (with export keyword) +infisical export --format=dotenv-export > .env + # Export variables to a CSV file infisical export --format=csv > secrets.csv @@ -33,4 +36,5 @@ infisical export --format=json > secrets.json # Export variables to a YAML file infisical export --format=yaml > secrets.yaml + ``` diff --git a/docs/images/email-aws-ses-console.png b/docs/images/email-aws-ses-console.png new file mode 100644 index 0000000000..2882ba4d5f Binary files /dev/null and b/docs/images/email-aws-ses-console.png differ diff --git a/docs/images/email-aws-ses-user.png b/docs/images/email-aws-ses-user.png new file mode 100644 index 0000000000..f740e58a5e Binary files /dev/null and b/docs/images/email-aws-ses-user.png differ diff --git a/docs/self-hosting/configuration/email.mdx b/docs/self-hosting/configuration/email.mdx index b1e911fb67..666ca76224 100644 --- a/docs/self-hosting/configuration/email.mdx +++ b/docs/self-hosting/configuration/email.mdx @@ -48,7 +48,7 @@ SMTP_FROM_NAME=Infisical ``` - Remember that you will need to restart Infisical for this to work properly. + Remember that you will need to restart Infisical for this to work properly. ## Mailgun @@ -70,6 +70,28 @@ SMTP_FROM_ADDRESS=hey@example.com # your email address being used to send out em SMTP_FROM_NAME=Infisical ``` +## AWS SES + +1. Create an account and [configure AWS SES](https://aws.amazon.com/premiumsupport/knowledge-center/ses-set-up-connect-smtp/) to send emails in the Amazon SES console. +2. Create an IAM user for SMTP authentication and obtain SMTP credentials in SMTP settings > Create SMTP credentials + +![opening AWS SES console](../../images/email-aws-ses-console.png) + +![creating AWS IAM SES user](../../images/email-aws-ses-user.png) + +3. With your AWS SES SMTP credentials, you can now set up your SMTP environment variables: + +``` +SMTP_HOST=smtp.mailgun.org # obtained from credentials page +SMTP_HOST=email-smtp.ap-northeast-1.amazonaws.com # SMTP endpoint obtained from SMTP settings +SMTP_USERNAME=xxx # your SMTP username +SMTP_PASSWORD=xxx # your SMTP password +SMTP_PORT=587 +SMTP_SECURE=true +SMTP_FROM_ADDRESS=hey@example.com # your email address being used to send out emails +SMTP_FROM_NAME=Infisical +``` + - Remember that you will need to restart Infisical for this to work properly. - \ No newline at end of file + Remember that you will need to restart Infisical for this to work properly. + diff --git a/frontend/components/RouteGuard.js b/frontend/components/RouteGuard.js index c5f6feb35e..f3693abec4 100644 --- a/frontend/components/RouteGuard.js +++ b/frontend/components/RouteGuard.js @@ -68,18 +68,5 @@ export default function RouteGuard({ children }) { } } - if (authorized) { - return children; - } else { - return ( -
- google logo -
- ); - } + return children; } diff --git a/frontend/components/basic/Listbox.tsx b/frontend/components/basic/Listbox.tsx index 95e3f33c64..a65135dfad 100644 --- a/frontend/components/basic/Listbox.tsx +++ b/frontend/components/basic/Listbox.tsx @@ -46,7 +46,7 @@ export default function ListBox({ >
{text} - + {' '} {selected} @@ -69,7 +69,7 @@ export default function ListBox({ - `my-0.5 relative cursor-default select-none py-2 pl-10 pr-4 rounded-md ${ + `my-0.5 relative cursor-default select-none py-2 pl-10 pr-4 rounded-md capitalize ${ selected ? 'bg-white/10 text-gray-400 font-bold' : '' } ${ active && !selected diff --git a/frontend/components/basic/Toggle.tsx b/frontend/components/basic/Toggle.tsx index c9957cc3c5..1a5ddaa826 100644 --- a/frontend/components/basic/Toggle.tsx +++ b/frontend/components/basic/Toggle.tsx @@ -1,27 +1,11 @@ import React from "react"; import { Switch } from "@headlessui/react"; - -interface OverrideProps { - id: string; - keyName: string; - value: string; - pos: number; - comment: string; -} - interface ToggleProps { enabled: boolean; setEnabled: (value: boolean) => void; - addOverride: (value: OverrideProps) => void; - keyName: string; - value: string; + addOverride: (value: string | undefined, pos: number) => void; pos: number; - id: string; - comment: string; - deleteOverride: (id: string) => void; - sharedToHide: string[]; - setSharedToHide: (values: string[]) => void; } /** @@ -30,41 +14,23 @@ interface ToggleProps { * @param {boolean} obj.enabled - whether the toggle is turned on or off * @param {function} obj.setEnabled - change the state of the toggle * @param {function} obj.addOverride - a function that adds an override to a certain secret - * @param {string} obj.keyName - key of a certain secret - * @param {string} obj.value - value of a certain secret * @param {number} obj.pos - position of a certain secret - #TODO: make the secret id persistent? - * @param {string} obj.id - id of a certain secret (NOTE: THIS IS THE ID OF THE MAIN SECRET - NOT OF AN OVERRIDE) - * @param {function} obj.deleteOverride - a function that deleted an override for a certain secret - * @param {string[]} obj.sharedToHide - an array of shared secrets that we want to hide visually because they are overriden. - * @param {function} obj.setSharedToHide - a function that updates the array of secrets that we want to hide visually * @returns */ export default function Toggle ({ enabled, setEnabled, addOverride, - keyName, - value, - pos, - id, - comment, - deleteOverride, - sharedToHide, - setSharedToHide + pos }: ToggleProps): JSX.Element { return ( { if (enabled == false) { - addOverride({ id, keyName, value, pos, comment }); - setSharedToHide([ - ...sharedToHide!, - id - ]) + addOverride('', pos); } else { - deleteOverride(id); + addOverride(undefined, pos); } setEnabled(!enabled); }} diff --git a/frontend/components/basic/buttons/Button.tsx b/frontend/components/basic/buttons/Button.tsx index 939d9b17de..2405abce77 100644 --- a/frontend/components/basic/buttons/Button.tsx +++ b/frontend/components/basic/buttons/Button.tsx @@ -3,7 +3,6 @@ import Image from "next/image"; import { IconProp } from "@fortawesome/fontawesome-svg-core"; import { FontAwesomeIcon, - FontAwesomeIconProps, } from "@fortawesome/react-fontawesome"; const classNames = require("classnames"); @@ -101,7 +100,7 @@ export default function Button(props: ButtonProps): JSX.Element {
{ const [serviceToken, setServiceToken] = useState(""); const [serviceTokenName, setServiceTokenName] = useState(""); - const [serviceTokenEnv, setServiceTokenEnv] = useState("Development"); + const [selectedServiceTokenEnv, setSelectedServiceTokenEnv] = useState(environments?.[0]); const [serviceTokenExpiresIn, setServiceTokenExpiresIn] = useState("1 day"); const [serviceTokenCopied, setServiceTokenCopied] = useState(false); const { t } = useTranslation(); @@ -66,7 +66,7 @@ const AddServiceTokenDialog = ({ let newServiceToken = await addServiceToken({ name: serviceTokenName, workspaceId, - environment: envMapping[serviceTokenEnv], + environment: selectedServiceTokenEnv?.slug ? selectedServiceTokenEnv.slug : environments[0]?.name, expiresIn: expiryMapping[serviceTokenExpiresIn], encryptedKey: ciphertext, iv, @@ -101,155 +101,159 @@ const AddServiceTokenDialog = ({ }; return ( -
+
- + -
+
-
-
+
+
- {serviceToken == "" ? ( - + {serviceToken == '' ? ( + - {t("section-token:add-dialog.title", { + {t('section-token:add-dialog.title', { target: workspaceName, })} -
-
-

- {t("section-token:add-dialog.description")} +

+
+

+ {t('section-token:add-dialog.description')}

-
+
-
+
name)} + onChange={(envName) => + setSelectedServiceTokenEnv( + environments.find( + ({ name }) => envName === name + ) || { + name: 'unknown', + slug: 'unknown', + } + ) + } isFull={true} - text={`${t("common:environment")}: `} + text={`${t('common:environment')}: `} />
-
+
-
-
+
+
) : ( - + - {t("section-token:add-dialog.copy-service-token")} + {t('section-token:add-dialog.copy-service-token')} -
-
-

+

+
+

{t( - "section-token:add-dialog.copy-service-token-description" + 'section-token:add-dialog.copy-service-token-description' )}

-
-
+
+
-
+
{serviceToken}
-
+
- - {t("common:click-to-copy")} + + {t('common:click-to-copy')}
-
+
diff --git a/frontend/components/basic/dialog/AddUpdateEnvironmentDialog.tsx b/frontend/components/basic/dialog/AddUpdateEnvironmentDialog.tsx new file mode 100644 index 0000000000..9476dd8f65 --- /dev/null +++ b/frontend/components/basic/dialog/AddUpdateEnvironmentDialog.tsx @@ -0,0 +1,145 @@ +import { FormEventHandler, Fragment, useEffect, useState } from 'react'; +import { Dialog, Transition } from '@headlessui/react'; + +import Button from '../buttons/Button'; +import InputField from '../InputField'; + +type FormFields = { name: string; slug: string }; + +type Props = { + isOpen?: boolean; + isEditMode?: boolean; + // on edit mode load up initial values + initialValues?: FormFields; + onClose: () => void; + onCreateSubmit: (data: FormFields) => void; + onEditSubmit: (data: FormFields) => void; +}; + +// TODO: Migrate to better form management and validation. Preferable react-hook-form + yup +/** + * The dialog modal for when the user wants to create a new workspace + * @param {*} param0 + * @returns + */ +export const AddUpdateEnvironmentDialog = ({ + isOpen, + onClose, + onCreateSubmit, + onEditSubmit, + initialValues, + isEditMode, +}: Props) => { + const [formInput, setFormInput] = useState({ + name: '', + slug: '', + }); + + // This use effect can be removed when the unmount is happening from outside the component + // When unmount happens outside state gets unmounted also + useEffect(() => { + setFormInput(initialValues || { name: '', slug: '' }); + }, [isOpen]); + + // REFACTOR: Move to react-hook-form with yup for better form management + const onInputChange = (fieldName: string, fieldValue: string) => { + setFormInput((state) => ({ ...state, [fieldName]: fieldValue })); + }; + + const onFormSubmit: FormEventHandler = (e) => { + e.preventDefault(); + const data = { + name: formInput.name.toLowerCase(), + slug: formInput.slug.toLowerCase(), + }; + if (isEditMode) { + onEditSubmit(data); + return; + } + onCreateSubmit(data); + }; + + return ( +
+ + + +
+ + +
+
+ + + + {isEditMode + ? 'Update environment' + : 'Create a new environment'} + +
+
+ onInputChange('name', val)} + type='varName' + value={formInput.name} + placeholder='' + isRequired + // error={error.length > 0} + // errorText={error} + /> +
+
+ onInputChange('slug', val)} + type='varName' + value={formInput.slug} + placeholder='' + isRequired + // error={error.length > 0} + // errorText={error} + /> +
+

+ Slugs are shorthands used in cli to access environment +

+
+
+
+
+
+
+
+
+
+
+ ); +}; diff --git a/frontend/components/basic/dialog/DeleteActionModal.tsx b/frontend/components/basic/dialog/DeleteActionModal.tsx new file mode 100644 index 0000000000..9a1b7a46af --- /dev/null +++ b/frontend/components/basic/dialog/DeleteActionModal.tsx @@ -0,0 +1,104 @@ +import { Fragment, useEffect, useState } from 'react'; +import { Dialog, Transition } from '@headlessui/react'; + +import InputField from '../InputField'; + +// REFACTOR: Move all these modals into one reusable one +type Props = { + isOpen?: boolean; + onClose: ()=>void; + title: string; + onSubmit:()=>void; + deleteKey?:string; +} + +const DeleteActionModal = ({ + isOpen, + onClose, + title, + onSubmit, + deleteKey +}:Props) => { + const [deleteInputField, setDeleteInputField] = useState("") + + useEffect(() => { + setDeleteInputField(""); + }, [isOpen]); + + return ( +
+ + + +
+ +
+
+ + + + {title} + +
+

+ This action is irrevertible. +

+
+
+ setDeleteInputField(val)} + value={deleteInputField} + type='text' + /> +
+
+ + +
+
+
+
+
+
+
+
+ ); +}; + +export default DeleteActionModal; diff --git a/frontend/components/basic/table/EnvironmentsTable.tsx b/frontend/components/basic/table/EnvironmentsTable.tsx new file mode 100644 index 0000000000..56536b679e --- /dev/null +++ b/frontend/components/basic/table/EnvironmentsTable.tsx @@ -0,0 +1,167 @@ +import { faPencil, faPlus, faX } from '@fortawesome/free-solid-svg-icons'; + +import { usePopUp } from '../../../hooks/usePopUp'; +import Button from '../buttons/Button'; +import { AddUpdateEnvironmentDialog } from '../dialog/AddUpdateEnvironmentDialog'; +import DeleteActionModal from '../dialog/DeleteActionModal'; + +type Env = { name: string; slug: string }; + +type Props = { + data: Env[]; + onCreateEnv: (arg0: Env) => Promise; + onUpdateEnv: (oldSlug: string, arg0: Env) => Promise; + onDeleteEnv: (slug: string) => Promise; +}; + +const EnvironmentTable = ({ + data = [], + onCreateEnv, + onDeleteEnv, + onUpdateEnv, +}: Props) => { + const { popUp, handlePopUpOpen, handlePopUpClose } = usePopUp([ + 'createUpdateEnv', + 'deleteEnv', + ] as const); + + const onEnvCreateCB = async (env: Env) => { + try { + await onCreateEnv(env); + handlePopUpClose('createUpdateEnv'); + } catch (error) { + console.error(error); + } + }; + + const onEnvUpdateCB = async (env: Env) => { + try { + await onUpdateEnv( + (popUp.createUpdateEnv?.data as Pick)?.slug, + env + ); + handlePopUpClose('createUpdateEnv'); + } catch (error) { + console.error(error); + } + }; + + const onEnvDeleteCB = async () => { + try { + await onDeleteEnv( + (popUp.deleteEnv?.data as Pick)?.slug + ); + handlePopUpClose('deleteEnv'); + } catch (error) { + console.error(error); + } + }; + + 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. +

+
+
+
+
+
+
+ + + + + + + + + + {data?.length > 0 ? ( + data.map(({ name, slug }) => { + return ( + + + + + + ); + }) + ) : ( + + + + )} + +
NameSlug
+ {name} + + {slug} + +
+
+
+
+
+ No environmants found +
+ handlePopUpClose('deleteEnv')} + onSubmit={onEnvDeleteCB} + /> + handlePopUpClose('createUpdateEnv')} + onCreateSubmit={onEnvCreateCB} + onEditSubmit={onEnvUpdateCB} + /> +
+ + ); +}; + +export default EnvironmentTable; diff --git a/frontend/components/basic/table/ServiceTokenTable.tsx b/frontend/components/basic/table/ServiceTokenTable.tsx index 412a0dbfbc..4d30b30139 100644 --- a/frontend/components/basic/table/ServiceTokenTable.tsx +++ b/frontend/components/basic/table/ServiceTokenTable.tsx @@ -3,7 +3,6 @@ import { faX } from '@fortawesome/free-solid-svg-icons'; import { useNotificationContext } from '~/components/context/Notifications/NotificationProvider'; import deleteServiceToken from "../../../pages/api/serviceToken/deleteServiceToken"; -import { reverseEnvMapping } from '../../../public/data/frequentConstants'; import guidGenerator from '../../utilities/randomId'; import Button from '../buttons/Button'; @@ -60,7 +59,7 @@ const ServiceTokenTable = ({ data, workspaceName, setServiceTokens }: ServiceTok {workspaceName} - {reverseEnvMapping[row.environment]} + {row.environment} {new Date(row.expiresAt).toUTCString()} diff --git a/frontend/components/dashboard/DashboardInputField.tsx b/frontend/components/dashboard/DashboardInputField.tsx index a667c39d0f..c19f6f4f95 100644 --- a/frontend/components/dashboard/DashboardInputField.tsx +++ b/frontend/components/dashboard/DashboardInputField.tsx @@ -9,7 +9,7 @@ const REGEX = /([$]{.*?})/g; interface DashboardInputFieldProps { position: number; onChangeHandler: (value: string, position: number) => void; - value: string; + value: string | undefined; type: 'varName' | 'value'; blurred?: boolean; isDuplicate?: boolean; @@ -47,7 +47,7 @@ const DashboardInputField = ({ }; if (type === 'varName') { - const startsWithNumber = !isNaN(Number(value.charAt(0))) && value != ''; + const startsWithNumber = !isNaN(Number(value?.charAt(0))) && value != ''; const error = startsWithNumber || isDuplicate; return ( @@ -141,7 +141,7 @@ const DashboardInputField = ({ {blurred && (
- {value.split('').map(() => ( + {value?.split('').map(() => ( void; modifyValue: (value: string, position: number) => void; + modifyValueOverride: (value: string, position: number) => void; isBlurred: boolean; isDuplicate: boolean; toggleSidebar: (id: string) => void; @@ -30,6 +23,7 @@ interface KeyPairProps { * @param {String[]} obj.keyPair - data related to the environment variable (id, pos, key, value, public/private) * @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 {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 @@ -41,6 +35,7 @@ const KeyPair = ({ keyPair, modifyKey, modifyValue, + modifyValueOverride, isBlurred, isDuplicate, toggleSidebar, @@ -50,7 +45,7 @@ const KeyPair = ({ return (
- {keyPair.type == "personal" &&
+ {keyPair.valueOverride &&
This secret is overriden @@ -70,12 +65,12 @@ const KeyPair = ({
diff --git a/frontend/components/dashboard/SideBar.tsx b/frontend/components/dashboard/SideBar.tsx index 33b936f521..32cdf7341d 100644 --- a/frontend/components/dashboard/SideBar.tsx +++ b/frontend/components/dashboard/SideBar.tsx @@ -16,18 +16,15 @@ import GenerateSecretMenu from './GenerateSecretMenu'; interface SecretProps { key: string; value: string; + valueOverride: string | undefined; pos: number; - type: string; id: string; comment: string; } interface OverrideProps { id: string; - keyName: string; - value: string; - pos: number; - comment: string; + valueOverride: string; } export interface DeleteRowFunctionProps { ids: string[]; @@ -39,9 +36,8 @@ interface SideBarProps { data: SecretProps[]; modifyKey: (value: string, position: number) => void; modifyValue: (value: string, position: number) => void; + modifyValueOverride: (value: string | undefined, position: number) => void; modifyComment: (value: string, position: number) => void; - addOverride: (value: OverrideProps) => void; - deleteOverride: (id: string) => void; buttonReady: boolean; savePush: () => void; sharedToHide: string[]; @@ -55,12 +51,9 @@ interface SideBarProps { * @param {SecretProps[]} obj.data - data of a certain key valeu pair * @param {function} obj.modifyKey - function that modifies the secret key * @param {function} obj.modifyValue - function that modifies the secret value - * @param {function} obj.addOverride - override a certain secret - * @param {function} obj.deleteOverride - delete the personal override for a certain secret + * @param {function} obj.modifyValueOverride - function that modifies the secret value if it is an override * @param {boolean} obj.buttonReady - is the button for saving chagnes active * @param {function} obj.savePush - save changes andp ush secrets - * @param {string[]} obj.sharedToHide - an array of shared secrets that we want to hide visually because they are overriden. - * @param {function} obj.setSharedToHide - a function that updates the array of secrets that we want to hide visually * @param {function} obj.deleteRow - a function to delete a certain keyPair * @returns the sidebar with 'secret's settings' */ @@ -69,17 +62,14 @@ const SideBar = ({ data, modifyKey, modifyValue, + modifyValueOverride, modifyComment, - addOverride, - deleteOverride, buttonReady, savePush, - sharedToHide, - setSharedToHide, deleteRow }: SideBarProps) => { const [isLoading, setIsLoading] = useState(false); - const [overrideEnabled, setOverrideEnabled] = useState(data.map(secret => secret.type).includes("personal")); + const [overrideEnabled, setOverrideEnabled] = useState(data[0].valueOverride != undefined); const { t } = useTranslation(); return
@@ -111,19 +101,19 @@ const SideBar = ({ blurred={false} />
- {data.filter(secret => secret.type == "shared")[0]?.value + {data[0]?.value ?

{t("dashboard:sidebar.value")}

secret.type == "shared")[0]?.pos} - value={data.filter(secret => secret.type == "shared")[0]?.value} + position={data[0].pos} + value={data[0]?.value} isDuplicate={false} blurred={true} />
- secret.type == "shared")[0]?.pos} /> +
:
@@ -131,39 +121,32 @@ const SideBar = ({ {t("dashboard:sidebar.personal-explanation")}
}
- {data.filter(secret => secret.type == "shared")[0]?.value && + {data[0]?.value &&

{t("dashboard:sidebar.override")}

}
secret.type == "personal")[0]?.pos : data[0]?.pos} - value={overrideEnabled ? data.filter(secret => secret.type == "personal")[0]?.value : data[0]?.value} + position={data[0]?.pos} + value={overrideEnabled ? data[0]?.valueOverride : data[0]?.value} isDuplicate={false} blurred={true} />
- secret.type == "personal")[0]?.pos : data[0]?.pos} /> +
- secret.type == "shared")[0]?.comment} modifyComment={modifyComment} position={data.filter(secret => secret.type == "shared")[0]?.pos} /> +
)}
@@ -176,7 +159,7 @@ const SideBar = ({ textDisabled="Saved" /> deleteRow({ ids: overrideEnabled ? data.map(secret => secret.id) : [data.filter(secret => secret.type == "shared")[0]?.id], secretName: data[0]?.key })} + onSubmit={() => deleteRow({ ids: data.map(secret => secret.id), secretName: data[0]?.key })} />
diff --git a/frontend/components/integrations/CloudIntegration.tsx b/frontend/components/integrations/CloudIntegration.tsx index 75a8019a5e..dd1b13f47e 100644 --- a/frontend/components/integrations/CloudIntegration.tsx +++ b/frontend/components/integrations/CloudIntegration.tsx @@ -74,7 +74,7 @@ const CloudIntegration = ({ integrationAuths .map((authorization) => authorization.integration) .includes(cloudIntegrationOption.name.toLowerCase()) && ( -
+
{ event.stopPropagation(); diff --git a/frontend/components/integrations/Integration.tsx b/frontend/components/integrations/Integration.tsx index 11edf7cb70..bbc8861dc6 100644 --- a/frontend/components/integrations/Integration.tsx +++ b/frontend/components/integrations/Integration.tsx @@ -15,9 +15,7 @@ import getIntegrationApps from "../../pages/api/integrations/GetIntegrationApps" import updateIntegration from "../../pages/api/integrations/updateIntegration" import { contextNetlifyMapping, - envMapping, reverseContextNetlifyMapping, - reverseEnvMapping, } from "../../public/data/frequentConstants"; interface Integration { @@ -36,13 +34,23 @@ interface IntegrationApp { siteId: string; } -const Integration = ({ - integration -}: { +type Props = { integration: Integration; -}) => { - const [integrationEnvironment, setIntegrationEnvironment] = useState( - reverseEnvMapping[integration.environment] + environments: Array<{ name: string; slug: string }>; +}; + +const Integration = ({ + integration, + environments = [] +}:Props ) => { + // set initial environment. This find will only execute when component is mounting + const [integrationEnvironment, setIntegrationEnvironment] = useState< + Props['environments'][0] + >( + environments.find(({ slug }) => slug === integration.environment) || { + name: '', + slug: '', + } ); const [fileState, setFileState] = useState([]); const router = useRouter(); @@ -93,7 +101,7 @@ const Integration = ({ case "vercel": return (
-
+
ENVIRONMENT
); @@ -136,42 +145,47 @@ const Integration = ({ if (!integrationApp || apps.length === 0) return
return ( -
-
+
+
-

ENVIRONMENT

- { - setIntegrationEnvironment(environment); - }} +

+ ENVIRONMENT +

+ name) + : null + } + selected={integrationEnvironment.name} + onChange={(envName) => + setIntegrationEnvironment( + environments.find(({ name }) => envName === name) || { + name: 'unknown', + slug: 'unknown', + } + ) + } isFull={true} />
-
+
+ className='mx-4 text-gray-400 mt-8' + />
-
-

+

+

INTEGRATION

-
+
{integration.integration.charAt(0).toUpperCase() + integration.integration.slice(1)}
-
-
- APP -
+
+
APP
app.name) : null} selected={integrationApp} @@ -182,52 +196,55 @@ const Integration = ({
{renderIntegrationSpecificParams(integration)}
-
- {integration.isActive ? ( -
- -
In Sync
-
- ) : ( -
); diff --git a/frontend/components/integrations/IntegrationSection.tsx b/frontend/components/integrations/IntegrationSection.tsx index 52d5565ff5..633a747afb 100644 --- a/frontend/components/integrations/IntegrationSection.tsx +++ b/frontend/components/integrations/IntegrationSection.tsx @@ -5,7 +5,8 @@ import guidGenerator from "~/utilities/randomId"; import Integration from "./Integration"; interface Props { - integrations: any + integrations: any; + environments: Array<{ name: string; slug: string }>; } interface IntegrationType { @@ -19,7 +20,8 @@ interface IntegrationType { } const ProjectIntegrationSection = ({ - integrations + integrations, + environments = [], }: Props) => { return integrations.length > 0 ? (
@@ -33,6 +35,7 @@ const ProjectIntegrationSection = ({ ))}
diff --git a/frontend/components/signup/CodeInputStep.tsx b/frontend/components/signup/CodeInputStep.tsx index 05c4973a87..9cb1aa5502 100644 --- a/frontend/components/signup/CodeInputStep.tsx +++ b/frontend/components/signup/CodeInputStep.tsx @@ -81,7 +81,7 @@ export default function CodeInputStep({ email, incrementStep, setCode, codeError return (

- {"We've"} sent a verification email to{" "} + {t("signup:step2-message")}

{email}{" "} @@ -119,11 +119,11 @@ export default function CodeInputStep({ email, incrementStep, setCode, codeError

- Not seeing an email? + {t("signup:step2-resend-alert")}
diff --git a/frontend/components/signup/EnterEmailStep.tsx b/frontend/components/signup/EnterEmailStep.tsx index 5f98710b75..1b9177580f 100644 --- a/frontend/components/signup/EnterEmailStep.tsx +++ b/frontend/components/signup/EnterEmailStep.tsx @@ -59,11 +59,11 @@ export default function EnterEmailStep({ email, setEmail, incrementStep }: Downl

- {'Let\''}s get started + {t("signup:step1-start")}

-
diff --git a/frontend/components/utilities/attemptLogin.ts b/frontend/components/utilities/attemptLogin.ts index c40dbb5481..f1aa03ffb9 100644 --- a/frontend/components/utilities/attemptLogin.ts +++ b/frontend/components/utilities/attemptLogin.ts @@ -1,3 +1,5 @@ +import { SecretDataProps } from 'public/data/frequentInterfaces'; + import Aes256Gcm from '~/components/utilities/cryptography/aes-256-gcm'; import login1 from '~/pages/api/auth/Login1'; import login2 from '~/pages/api/auth/Login2'; @@ -13,14 +15,6 @@ import Telemetry from './telemetry/Telemetry'; import { saveTokenToLocalStorage } from './saveTokenToLocalStorage'; import SecurityClient from './SecurityClient'; -interface SecretDataProps { - type: 'personal' | 'shared'; - pos: number; - key: string; - value: string; - id: string; - comment: string; -} const crypto = require("crypto"); const nacl = require('tweetnacl'); @@ -145,53 +139,53 @@ const attemptLogin = async ( ); const secretsToBeAdded: SecretDataProps[] = [{ - type: "shared", pos: 0, key: "DATABASE_URL", value: "mongodb+srv://${DB_USERNAME}:${DB_PASSWORD}@mongodb.net", + valueOverride: undefined, comment: "This is an example of secret referencing.", id: '' }, { - type: "shared", pos: 1, key: "DB_USERNAME", 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: '' }, { - type: "personal", pos: 2, - key: "DB_USERNAME", - value: "user1234", - comment: "", - id: '' - }, { - type: "shared", - pos: 3, key: "DB_PASSWORD", 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: '' }, { - type: "personal", + pos: 3, + key: "DB_USERNAME", + value: "user1234", + valueOverride: "user1234", + comment: "", + id: '' + }, { pos: 4, key: "DB_PASSWORD", value: "example_password", + valueOverride: "example_password", comment: "", id: '' }, { - type: "shared", pos: 5, key: "TWILIO_AUTH_TOKEN", value: "example_twillio_token", - 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", + valueOverride: undefined, + comment: "", id: '' }, { - type: "shared", pos: 6, key: "WEBSITE_URL", value: "http://localhost:3000", - 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", + valueOverride: undefined, + comment: "", id: '' }] const secrets = await encryptSecrets({ secretsToEncrypt: secretsToBeAdded, workspaceId: String(localStorage.getItem('projectData.id')), env: 'dev' }) diff --git a/frontend/components/utilities/secrets/checkOverrides.ts b/frontend/components/utilities/secrets/checkOverrides.ts index fae465d88c..83a779a85b 100644 --- a/frontend/components/utilities/secrets/checkOverrides.ts +++ b/frontend/components/utilities/secrets/checkOverrides.ts @@ -1,11 +1,4 @@ -interface SecretDataProps { - type: 'personal' | 'shared'; - pos: number; - key: string; - value: string; - id: string; - comment: string; -} +import { SecretDataProps } from "public/data/frequentInterfaces"; /** * This function downloads the secrets as a .env file @@ -16,16 +9,16 @@ interface SecretDataProps { const checkOverrides = async ({ data }: { data: SecretDataProps[]; }) => { let secrets : SecretDataProps[] = data!.map((secret) => Object.create(secret)); const overridenSecrets = data!.filter( - (secret) => secret.type === 'personal' + (secret) => (secret.valueOverride == undefined || secret?.value != secret?.valueOverride) ? 'shared' : 'personal' ); if (overridenSecrets.length) { overridenSecrets.forEach((secret) => { const index = secrets!.findIndex( - (_secret) => _secret.key === secret.key && _secret.type === 'shared' + (_secret) => _secret.key === secret.key && (secret.valueOverride == undefined || secret?.value != secret?.valueOverride) ); secrets![index].value = secret.value; }); - secrets = secrets!.filter((secret) => secret.type === 'shared'); + secrets = secrets!.filter((secret) => (secret.valueOverride == undefined || secret?.value != secret?.valueOverride)); } return secrets; } diff --git a/frontend/components/utilities/secrets/downloadDotEnv.ts b/frontend/components/utilities/secrets/downloadDotEnv.ts index dbb29497b1..8770d098be 100644 --- a/frontend/components/utilities/secrets/downloadDotEnv.ts +++ b/frontend/components/utilities/secrets/downloadDotEnv.ts @@ -1,16 +1,9 @@ +import { SecretDataProps } from "public/data/frequentInterfaces"; + import { envMapping } from "../../../public/data/frequentConstants"; import checkOverrides from './checkOverrides'; -interface SecretDataProps { - type: 'personal' | 'shared'; - pos: number; - key: string; - value: string; - id: string; - comment: string; -} - /** * This function downloads the secrets as a .env file * @param {object} obj @@ -39,7 +32,7 @@ const downloadDotEnv = async ({ data, env }: { data: SecretDataProps[]; env: str const fileDownloadUrl = URL.createObjectURL(blob); const alink = document.createElement('a'); alink.href = fileDownloadUrl; - alink.download = envMapping[env] + '.env'; + alink.download = env + '.env'; alink.click(); } diff --git a/frontend/components/utilities/secrets/downloadYaml.ts b/frontend/components/utilities/secrets/downloadYaml.ts index 1c17b14109..26fca5f006 100644 --- a/frontend/components/utilities/secrets/downloadYaml.ts +++ b/frontend/components/utilities/secrets/downloadYaml.ts @@ -1,19 +1,12 @@ // import YAML from 'yaml'; // import { YAMLSeq } from 'yaml/types'; +import { SecretDataProps } from "public/data/frequentInterfaces"; + // import { envMapping } from "../../../public/data/frequentConstants"; // import checkOverrides from './checkOverrides'; -interface SecretDataProps { - type: 'personal' | 'shared'; - pos: number; - key: string; - value: string; - id: string; - comment: string; -} - /** * This function downloads the secrets as a .yml file * @param {object} obj diff --git a/frontend/components/utilities/secrets/encryptSecrets.ts b/frontend/components/utilities/secrets/encryptSecrets.ts index 46c3150ef5..a0f56990a2 100644 --- a/frontend/components/utilities/secrets/encryptSecrets.ts +++ b/frontend/components/utilities/secrets/encryptSecrets.ts @@ -1,3 +1,5 @@ +import { SecretDataProps } from "public/data/frequentInterfaces"; + import getLatestFileKey from "~/pages/api/workspace/getLatestFileKey"; const crypto = require("crypto"); @@ -9,15 +11,6 @@ const nacl = require("tweetnacl"); nacl.util = require("tweetnacl-util"); -interface SecretDataProps { - type: 'personal' | 'shared'; - pos: number; - key: string; - value: string; - id: string; - comment: string; -} - interface EncryptedSecretProps { id: string; createdAt: string; @@ -106,7 +99,7 @@ const encryptSecrets = async ({ secretsToEncrypt, workspaceId, env }: { secretsT secretCommentCiphertext, secretCommentIV, secretCommentTag, - type: secret.type, + type: (secret.valueOverride == undefined || secret?.value != secret?.valueOverride) ? 'shared' : 'personal', }; return result; diff --git a/frontend/components/utilities/secrets/getSecretsForProject.ts b/frontend/components/utilities/secrets/getSecretsForProject.ts index 81de27be09..86f6db0e01 100644 --- a/frontend/components/utilities/secrets/getSecretsForProject.ts +++ b/frontend/components/utilities/secrets/getSecretsForProject.ts @@ -1,8 +1,6 @@ import getSecrets from '~/pages/api/files/GetSecrets'; import getLatestFileKey from '~/pages/api/workspace/getLatestFileKey'; -import { envMapping } from '../../../public/data/frequentConstants'; - const { decryptAssymmetric, decryptSymmetric @@ -35,7 +33,7 @@ interface SecretProps { } interface FunctionProps { - env: keyof typeof envMapping; + env: string; setIsKeyAvailable: any; setData: any; workspaceId: string; @@ -58,7 +56,7 @@ const getSecretsForProject = async ({ try { let encryptedSecrets; try { - encryptedSecrets = await getSecrets(workspaceId, envMapping[env]); + encryptedSecrets = await getSecrets(workspaceId, env); } catch (error) { console.log('ERROR: Not able to access the latest version of secrets'); } @@ -117,15 +115,19 @@ const getSecretsForProject = async ({ }); } - const result = tempDecryptedSecrets.map((secret, index) => { + const secretKeys = [...new Set(tempDecryptedSecrets.map(secret => secret.key))]; + + + const result = secretKeys.map((key, index) => { return { - id: secret['id'], + id: tempDecryptedSecrets.filter(secret => secret.key == key && secret.type == 'shared')[0]?.id, + idOverride: tempDecryptedSecrets.filter(secret => secret.key == key && secret.type == 'personal')[0]?.id, pos: index, - key: secret['key'], - value: secret['value'], - type: secret['type'], - comment: secret['comment'] - }; + key: key, + value: tempDecryptedSecrets.filter(secret => secret.key == key && secret.type == 'shared')[0]?.value, + valueOverride: tempDecryptedSecrets.filter(secret => secret.key == key && secret.type == 'personal')[0]?.value, + comment: tempDecryptedSecrets.filter(secret => secret.key == key && secret.type == 'shared')[0]?.comment, + } }); setData(result); diff --git a/frontend/const.js b/frontend/const.js index 195d54a6de..0e5ed9516e 100644 --- a/frontend/const.js +++ b/frontend/const.js @@ -20,4 +20,5 @@ export const publicPaths = [ export const languageMap = { en: "English", ko: "한국어", + fr: "Français", }; diff --git a/frontend/ee/components/PITRecoverySidebar.tsx b/frontend/ee/components/PITRecoverySidebar.tsx index 085c483ed9..e8a1041cd2 100644 --- a/frontend/ee/components/PITRecoverySidebar.tsx +++ b/frontend/ee/components/PITRecoverySidebar.tsx @@ -13,6 +13,15 @@ import { decryptAssymmetric, decryptSymmetric } from "~/components/utilities/cry import getLatestFileKey from "~/pages/api/workspace/getLatestFileKey"; +export interface SecretDataProps { + pos: number; + key: string; + value: string; + type: string; + id: string; + environment: string; +} + interface SideBarProps { toggleSidebar: (value: boolean) => void; setSnapshotData: (value: any) => void; @@ -43,8 +52,6 @@ interface EncrypetedSecretVersionListProps { * @param {function} obj.toggleSidebar - function that opens or closes the sidebar * @param {function} obj.setSnapshotData - state manager for snapshot data * @param {string} obj.chosenSnaphshot - the snapshot id which is currently selected - * - * * @returns the sidebar with the options for point-in-time recovery (commits) */ const PITRecoverySidebar = ({ @@ -111,7 +118,21 @@ const PITRecoverySidebar = ({ } }) - setSnapshotData({ id: secretSnapshotData._id, version: secretSnapshotData.version, createdAt: secretSnapshotData.createdAt, secretVersions: decryptedSecretVersions }) + + const secretKeys = [...new Set(decryptedSecretVersions.map((secret: SecretDataProps) => secret.key))]; + + const result = secretKeys.map((key, index) => { + return { + id: decryptedSecretVersions.filter((secret: SecretDataProps) => secret.key == key && secret.type == 'shared')[0].id, + pos: index, + key: key, + environment: decryptedSecretVersions.filter((secret: SecretDataProps) => secret.key == key && secret.type == 'shared')[0].environment, + value: decryptedSecretVersions.filter((secret: SecretDataProps) => secret.key == key && secret.type == 'shared')[0]?.value, + valueOverride: decryptedSecretVersions.filter((secret: SecretDataProps) => secret.key == key && secret.type == 'personal')[0]?.value, + } + }); + + setSnapshotData({ id: secretSnapshotData._id, version: secretSnapshotData.version, createdAt: secretSnapshotData.createdAt, secretVersions: result, comment: '' }) } return
@@ -125,31 +146,35 @@ const PITRecoverySidebar = ({ >
) : ( -
+

{t("Point-in-time Recovery")}

toggleSidebar(false)}>
-
- {secretSnapshotsMetadata?.map((snapshot: SnaphotProps, id: number) =>
-
-
{timeSince(new Date(snapshot.createdAt))}
-
{" - " + snapshot.secretVersions.length + " Secrets"}
-
+
+ {secretSnapshotsMetadata?.map((snapshot: SnaphotProps, id: number) =>
exploreSnapshot({ snapshotId: snapshot._id })} - className={`${chosenSnapshot == snapshot._id || (id == 0 && chosenSnapshot === "") ? "text-bunker-800 pointer-events-none" : "text-bunker-200 hover:text-primary duration-200 cursor-pointer"} text-sm`}> - {id == 0 ? "Current Version" : chosenSnapshot == snapshot._id ? "Currently Viewing" : "Explore"} + key={snapshot._id} + onClick={() => exploreSnapshot({ snapshotId: snapshot._id })} + className={`${chosenSnapshot == snapshot._id || (id == 0 && chosenSnapshot === "") ? "bg-primary text-black pointer-events-none" : "bg-mineshaft-700 hover:bg-mineshaft-500 duration-200 cursor-pointer"} py-3 px-4 mb-2 rounded-md flex flex-row justify-between items-center`} + > +
+
{timeSince(new Date(snapshot.createdAt))}
+
{" - " + snapshot.secretVersions.length + " Secrets"}
+
+
+ {id == 0 ? "Current Version" : chosenSnapshot == snapshot._id ? "Currently Viewing" : "Explore"} +
+
)} +
+
+
-
)} -
-
-
-
)}
diff --git a/frontend/ee/components/SecretVersionList.tsx b/frontend/ee/components/SecretVersionList.tsx index 3eb40005f9..d07198dc06 100644 --- a/frontend/ee/components/SecretVersionList.tsx +++ b/frontend/ee/components/SecretVersionList.tsx @@ -52,7 +52,7 @@ const SecretVersionList = ({ secretId }: { secretId: string; }) => { }); } - const decryptedSecretVersions = encryptedSecretVersions.secretVersions.map((encryptedSecretVersion: EncrypetedSecretVersionListProps) => { + const decryptedSecretVersions = encryptedSecretVersions?.secretVersions.map((encryptedSecretVersion: EncrypetedSecretVersionListProps) => { return { createdAt: encryptedSecretVersion.createdAt, value: decryptSymmetric({ @@ -87,28 +87,33 @@ const SecretVersionList = ({ secretId }: { secretId: string; }) => {
) : (
- {secretVersions?.sort((a, b) => b.createdAt.localeCompare(a.createdAt)) - .map((version: DecryptedSecretVersionListProps, index: number) => -
-
-
-
-
-
-
- {(new Date(version.createdAt)).toLocaleDateString('en-US', { - year: 'numeric', - month: '2-digit', - day: '2-digit', - hour: '2-digit', - minute: '2-digit', - second: '2-digit' - })} + {secretVersions + ? secretVersions?.sort((a, b) => b.createdAt.localeCompare(a.createdAt)) + .map((version: DecryptedSecretVersionListProps, index: number) => +
+
+
+
+
+
+
+ {(new Date(version.createdAt)).toLocaleDateString('en-US', { + year: 'numeric', + month: '2-digit', + day: '2-digit', + hour: '2-digit', + minute: '2-digit', + second: '2-digit' + })} +
+

Value:{version.value}

-

Value:{version.value}

-
- )} + ) + : ( +
No version history yet.
+ ) + }
)}
diff --git a/frontend/hooks/index.ts b/frontend/hooks/index.ts new file mode 100644 index 0000000000..dcea2eb7c4 --- /dev/null +++ b/frontend/hooks/index.ts @@ -0,0 +1 @@ +export { usePopUp } from './usePopUp'; diff --git a/frontend/hooks/usePopUp.tsx b/frontend/hooks/usePopUp.tsx new file mode 100644 index 0000000000..eb0835b495 --- /dev/null +++ b/frontend/hooks/usePopUp.tsx @@ -0,0 +1,69 @@ +import { useCallback, useState } from 'react'; + +interface usePopUpProps { + name: Readonly; + isOpen: boolean; +} + +/** + * to provide better intellisense + * checks which type of inputProps were given and converts them into key-names + * SIDENOTE: On inputting give it as const and not string with (as const) + */ +type usePopUpState | usePopUpProps[]> = { + [P in T extends usePopUpProps[] ? T[number]['name'] : T[number]]: { + isOpen: boolean; + data?: unknown; + }; +}; + +interface usePopUpReturn | usePopUpProps[]> { + popUp: usePopUpState; + handlePopUpOpen: (popUpName: keyof usePopUpState, data?: unknown) => void; + handlePopUpClose: (popUpName: keyof usePopUpState) => void; + handlePopUpToggle: (popUpName: keyof usePopUpState) => void; +} + +/** + * This hook is used to manage multiple popUps/modal/dialog in a page + * Provides api to open,close,toggle and also store temporary data for the popUp + * @param popUpNames: the names of popUp containers eg: ["popUp1","second"] or [{name:"popUp2",isOpen:bool}] + */ +export const usePopUp = | usePopUpProps[]>( + popUpNames: T +): usePopUpReturn => { + const [popUp, setPopUp] = useState>( + Object.fromEntries( + popUpNames.map((popUpName) => + typeof popUpName === 'string' + ? [popUpName, { isOpen: false }] + : [popUpName.name, { isOpen: popUpName.isOpen }] + ) // convert into an array of [[popUpName,state]] then into Object + ) as usePopUpState // to override generic string return type of the function + ); + + const handlePopUpOpen = useCallback( + (popUpName: keyof usePopUpState, data?: unknown) => { + setPopUp((popUp) => ({ ...popUp, [popUpName]: { isOpen: true, data } })); + }, + [] + ); + + const handlePopUpClose = useCallback((popUpName: keyof usePopUpState) => { + setPopUp((popUp) => ({ ...popUp, [popUpName]: { isOpen: false } })); + }, []); + + const handlePopUpToggle = useCallback((popUpName: keyof usePopUpState) => { + setPopUp((popUp) => ({ + ...popUp, + [popUpName]: { isOpen: !popUp[popUpName].isOpen }, + })); + }, []); + + return { + popUp, + handlePopUpOpen, + handlePopUpClose, + handlePopUpToggle, + }; +}; diff --git a/frontend/next-i18next.config.js b/frontend/next-i18next.config.js index ff84258827..ea9fa3463b 100644 --- a/frontend/next-i18next.config.js +++ b/frontend/next-i18next.config.js @@ -8,7 +8,7 @@ module.exports = { debug: false, i18n: { defaultLocale: "en", - locales: ["en", "ko"], + locales: ["en", "ko", "fr", "pt-BR"], }, fallbackLng: { default: ["en"], diff --git a/frontend/pages/api/environments/createEnvironment.ts b/frontend/pages/api/environments/createEnvironment.ts new file mode 100644 index 0000000000..2d0ecfa379 --- /dev/null +++ b/frontend/pages/api/environments/createEnvironment.ts @@ -0,0 +1,29 @@ +import SecurityClient from '~/utilities/SecurityClient'; + +type NewEnvironmentInfo = { + environmentSlug: string; + environmentName: string; +}; + +/** + * This route deletes a specified workspace. + * @param {*} workspaceId + * @returns + */ +const createEnvironment = (workspaceId:string, newEnv: NewEnvironmentInfo) => { + return SecurityClient.fetchCall(`/api/v2/workspace/${workspaceId}/environments`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify(newEnv) + }).then(async (res) => { + if (res && res.status == 200) { + return res; + } else { + console.log('Failed to create environment'); + } + }); +}; + +export default createEnvironment; diff --git a/frontend/pages/api/environments/deleteEnvironment.ts b/frontend/pages/api/environments/deleteEnvironment.ts new file mode 100644 index 0000000000..de86115321 --- /dev/null +++ b/frontend/pages/api/environments/deleteEnvironment.ts @@ -0,0 +1,26 @@ +import SecurityClient from '~/utilities/SecurityClient'; +/** + * This route deletes a specified env. + * @param {*} workspaceId + * @returns + */ +const deleteEnvironment = (workspaceId: string, environmentSlug: string) => { + return SecurityClient.fetchCall( + `/api/v2/workspace/${workspaceId}/environments`, + { + method: 'DELETE', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ environmentSlug }), + } + ).then(async (res) => { + if (res && res.status == 200) { + return res; + } else { + console.log('Failed to delete environment'); + } + }); +}; + +export default deleteEnvironment; diff --git a/frontend/pages/api/environments/updateEnvironment.ts b/frontend/pages/api/environments/updateEnvironment.ts new file mode 100644 index 0000000000..65fb449a8b --- /dev/null +++ b/frontend/pages/api/environments/updateEnvironment.ts @@ -0,0 +1,33 @@ +import SecurityClient from '~/utilities/SecurityClient'; + +type EnvironmentInfo = { + oldEnvironmentSlug: string; + environmentSlug: string; + environmentName: string; +}; + +/** + * This route updates a specified environment. + * @param {*} workspaceId + * @returns + */ +const updateEnvironment = (workspaceId: string, env: EnvironmentInfo) => { + return SecurityClient.fetchCall( + `/api/v2/workspace/${workspaceId}/environments`, + { + method: 'PUT', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify(env), + } + ).then(async (res) => { + if (res && res.status == 200) { + return res; + } else { + console.log('Failed to update environment'); + } + }); +}; + +export default updateEnvironment; diff --git a/frontend/pages/api/workspace/getAWorkspace.ts b/frontend/pages/api/workspace/getAWorkspace.ts new file mode 100644 index 0000000000..71c5036eb4 --- /dev/null +++ b/frontend/pages/api/workspace/getAWorkspace.ts @@ -0,0 +1,31 @@ +import SecurityClient from '~/utilities/SecurityClient'; + +interface Workspace { + __v: number; + _id: string; + name: string; + organization: string; + environments: Array<{ name: string; slug: string }>; +} + +/** + * This route lets us get the workspaces of a certain user + * @returns + */ +const getAWorkspace = (workspaceID:string) => { + return SecurityClient.fetchCall(`/api/v1/workspace/${workspaceID}`, { + method: 'GET', + headers: { + 'Content-Type': 'application/json', + }, + }).then(async (res) => { + if (res?.status == 200) { + const data = (await res.json()) as unknown as { workspace: Workspace }; + return data.workspace; + } + + throw new Error('Failed to get workspace'); + }); +}; + +export default getAWorkspace; diff --git a/frontend/pages/api/workspace/getWorkspaces.ts b/frontend/pages/api/workspace/getWorkspaces.ts index 1bbb42c7a7..d77c4c431b 100644 --- a/frontend/pages/api/workspace/getWorkspaces.ts +++ b/frontend/pages/api/workspace/getWorkspaces.ts @@ -5,6 +5,7 @@ interface Workspace { _id: string; name: string; organization: string; + environments: Array<{name:string, slug:string}> } /** diff --git a/frontend/pages/dashboard/[id].tsx b/frontend/pages/dashboard/[id].tsx index cb3717f683..1a9cd41da1 100644 --- a/frontend/pages/dashboard/[id].tsx +++ b/frontend/pages/dashboard/[id].tsx @@ -2,7 +2,7 @@ import { Fragment, useCallback, useEffect, useState } from 'react'; import Head from 'next/head'; import Image from 'next/image'; import { useRouter } from 'next/router'; -import { useTranslation } from "next-i18next"; +import { useTranslation } from 'next-i18next'; import { faArrowDownAZ, faArrowDownZA, @@ -34,7 +34,6 @@ import getSecretsForProject from '~/components/utilities/secrets/getSecretsForPr import { getTranslatedServerSideProps } from '~/components/utilities/withTranslateProps'; import guidGenerator from '~/utilities/randomId'; -import { envMapping, reverseEnvMapping } from '../../public/data/frequentConstants'; import addSecrets from '../api/files/AddSecrets'; import deleteSecrets from '../api/files/DeleteSecrets'; import updateSecrets from '../api/files/UpdateSecrets'; @@ -43,13 +42,18 @@ import checkUserAction from '../api/userActions/checkUserAction'; import registerUserAction from '../api/userActions/registerUserAction'; import getWorkspaces from '../api/workspace/getWorkspaces'; +type WorkspaceEnv = { + name: string; + slug: string; +}; interface SecretDataProps { - type: 'personal' | 'shared'; pos: number; key: string; value: string; + valueOverride: string | undefined; id: string; + idOverride: string | undefined; comment: string; } @@ -68,10 +72,11 @@ interface SnapshotProps { secretVersions: { id: string; pos: number; - type: "personal" | "shared"; environment: string; key: string; value: string; + valueOverride: string; + comment: string; }[]; } @@ -99,14 +104,11 @@ function findDuplicates(arr: any[]) { */ export default function Dashboard() { const [data, setData] = useState(); - const [initialData, setInitialData] = useState([]); + const [initialData, setInitialData] = useState([]); const [buttonReady, setButtonReady] = useState(false); const router = useRouter(); - const [workspaceId, setWorkspaceId] = useState(''); const [blurred, setBlurred] = useState(true); const [isKeyAvailable, setIsKeyAvailable] = useState(true); - const [env, setEnv] = useState('Development'); - const [snapshotEnv, setSnapshotEnv] = useState('Development'); const [isNew, setIsNew] = useState(false); const [isLoading, setIsLoading] = useState(false); const [searchKeys, setSearchKeys] = useState(''); @@ -114,15 +116,26 @@ export default function Dashboard() { const [sortMethod, setSortMethod] = useState('alphabetical'); const [checkDocsPopUpVisible, setCheckDocsPopUpVisible] = useState(false); const [hasUserEverPushed, setHasUserEverPushed] = useState(false); - const [sidebarSecretId, toggleSidebar] = useState("None"); + const [sidebarSecretId, toggleSidebar] = useState('None'); const [PITSidebarOpen, togglePITSidebar] = useState(false); const [sharedToHide, setSharedToHide] = useState([]); const [snapshotData, setSnapshotData] = useState(); const [numSnapshots, setNumSnapshots] = useState(); + const [saveLoading, setSaveLoading] = useState(false); const { t } = useTranslation(); const { createNotification } = useNotificationContext(); + const workspaceId = router.query.id as string; + const [workspaceEnvs, setWorkspaceEnvs] = useState([]); + + const [selectedSnapshotEnv, setSelectedSnapshotEnv] = + useState(); + const [selectedEnv, setSelectedEnv] = useState({ + name: '', + slug: '', + }); + // #TODO: fix save message for changing reroutes // const beforeRouteHandler = (url) => { // const warningText = @@ -169,25 +182,37 @@ export default function Dashboard() { useEffect(() => { (async () => { try { - const tempNumSnapshots = await getProjectSercetSnapshotsCount({ workspaceId: String(router.query.id) }) + const tempNumSnapshots = await getProjectSercetSnapshotsCount({ + workspaceId, + }); setNumSnapshots(tempNumSnapshots); const userWorkspaces = await getWorkspaces(); - const listWorkspaces = userWorkspaces.map((workspace) => workspace._id); - if ( - !listWorkspaces.includes(router.asPath.split('/')[2]) - ) { - router.push('/dashboard/' + listWorkspaces[0]); + const workspace = userWorkspaces.find( + (workspace) => workspace._id === workspaceId + ); + if (!workspace) { + router.push('/dashboard/' + userWorkspaces?.[0]?._id); } + setWorkspaceEnvs(workspace?.environments || []); + // set env + const env = workspace?.environments?.[0] || { + name: 'unknown', + slug: 'unkown', + }; + setSelectedEnv(env); + setSelectedSnapshotEnv(env); const user = await getUser(); setIsNew( - (Date.parse(String(new Date())) - Date.parse(user.createdAt)) / 60000 < 3 + (Date.parse(String(new Date())) - Date.parse(user.createdAt)) / + 60000 < + 3 ? true : false ); const userAction = await checkUserAction({ - action: 'first_time_secrets_pushed' + action: 'first_time_secrets_pushed', }); setHasUserEverPushed(userAction ? true : false); } catch (error) { @@ -195,41 +220,33 @@ export default function Dashboard() { setData(undefined); } })(); - }, []); + }, [workspaceId]); useEffect(() => { (async () => { try { setIsLoading(true); setBlurred(true); - setWorkspaceId(String(router.query.id)); - + // ENV const dataToSort = await getSecretsForProject({ - env, + env: selectedEnv.slug, setIsKeyAvailable, setData, - workspaceId: String(router.query.id) + workspaceId, }); setInitialData(dataToSort); reorderRows(dataToSort); - setSharedToHide( - dataToSort?.filter(row => (dataToSort - ?.map((item) => item.key) - .filter( - (item, index) => - index !== - dataToSort?.map((item) => item.key).indexOf(item) - ).includes(row.key) && row.type == 'shared'))?.map((item) => item.id) - ) - setIsLoading(false); + setTimeout( + () => setIsLoading(false) + , 700); } catch (error) { console.log('Error', error); setData(undefined); } })(); // eslint-disable-next-line react-hooks/exhaustive-deps - }, [env]); + }, [selectedEnv]); const addRow = () => { setIsNew(false); @@ -237,71 +254,41 @@ export default function Dashboard() { ...data!, { id: guidGenerator(), + idOverride: guidGenerator(), pos: data!.length, key: '', value: '', - type: 'shared', + valueOverride: undefined, comment: '', - } + }, ]); }; - /** - * This function add an ovverrided version of a certain secret to the current user - * @param {object} obj - * @param {string} obj.id - if of this secret that is about to be overriden - * @param {string} obj.keyName - key name of this secret - * @param {string} obj.value - value of this secret - * @param {string} obj.pos - position of this secret on the dashboard - */ - const addOverride = ({ id, keyName, value, pos, comment }: overrideProps) => { - setIsNew(false); - const tempdata: SecretDataProps[] | 1 = [ - ...data!, - { - id: id, - pos: pos, - key: keyName, - value: value, - type: 'personal', - comment: comment - } - ]; - sortValuesHandler(tempdata, sortMethod == "alhpabetical" ? "-alphabetical" : "alphabetical"); - }; const deleteRow = ({ ids, secretName }: { ids: string[]; secretName: string; }) => { setButtonReady(true); - toggleSidebar("None"); + toggleSidebar('None'); createNotification({ text: `${secretName} has been deleted. Remember to save changes.`, - type: 'error' + type: 'error', }); - sortValuesHandler(data!.filter((row: SecretDataProps) => !ids.includes(row.id)), sortMethod == "alhpabetical" ? "-alphabetical" : "alphabetical"); + sortValuesHandler( + data!.filter((row: SecretDataProps) => !ids.includes(row.id)), + sortMethod == 'alhpabetical' ? '-alphabetical' : 'alphabetical' + ); }; - /** - * This function deleted the override of a certain secrer - * @param {string} id - id of a shared secret; the override with the same key should be deleted - */ - const deleteOverride = (id: string) => { + const modifyValue = (value: string, pos: number) => { + setData((oldData) => { + oldData![pos].value = value; + return [...oldData!]; + }); setButtonReady(true); - - // find which shared secret corresponds to the overriden version - const sharedVersionOfOverride = data!.filter(secret => secret.type == "shared" && secret.key == data!.filter(row => row.id == id)[0]?.key)[0]?.id; - - // change the sidebar to this shared secret; and unhide it - toggleSidebar(sharedVersionOfOverride) - setSharedToHide(sharedToHide!.filter(tempId => tempId != sharedVersionOfOverride)) - - // resort secrets - const tempData = data!.filter((row: SecretDataProps) => !(row.key == data!.filter(row => row.id == id)[0]?.key && row.type == 'personal')) - sortValuesHandler(tempData, sortMethod == "alhpabetical" ? "-alphabetical" : "alphabetical") }; - const modifyValue = (value: string, pos: number) => { + const modifyValueOverride = (value: string | undefined, pos: number) => { setData((oldData) => { - oldData![pos].value = value; + oldData![pos].valueOverride = value; return [...oldData!]; }); setButtonReady(true); @@ -328,6 +315,10 @@ export default function Dashboard() { modifyValue(value, pos); }, []); + const listenChangeValueOverride = useCallback((value: string | undefined, pos: number) => { + modifyValueOverride(value, pos); + }, []); + const listenChangeKey = useCallback((value: string, pos: number) => { modifyKey(value, pos); }, []); @@ -340,6 +331,7 @@ export default function Dashboard() { * Save the changes of environment variables and push them to the database */ const savePush = async (dataToPush?: SecretDataProps[]) => { + setSaveLoading(true); let newData: SecretDataProps[] | null | undefined; // dataToPush is mostly used for rollbacks, otherwise we always take the current state data if ((dataToPush ?? [])?.length > 0) { @@ -348,28 +340,23 @@ export default function Dashboard() { newData = data; } - const obj = Object.assign( - {}, - ...newData!.map((row: SecretDataProps) => ({ [row.type.charAt(0) + row.key]: [row.value, row.comment ?? ''] })) - ); - // Checking if any of the secret keys start with a number - if so, don't do anything - const nameErrors = !Object.keys(obj) - .map((key) => !isNaN(Number(key[0].charAt(0)))) + const nameErrors = !newData! + .map((secret) => !isNaN(Number(secret.key.charAt(0)))) .every((v) => v === false); - const duplicatesExist = findDuplicates(data!.map((item: SecretDataProps) => item.key + item.type)).length > 0; + const duplicatesExist = findDuplicates(data!.map((item: SecretDataProps) => item.key)).length > 0; if (nameErrors) { return createNotification({ text: 'Solve all name errors before saving secrets.', - type: 'error' + type: 'error', }); } if (duplicatesExist) { return createNotification({ text: 'Remove duplicated secret names before saving.', - type: 'error' + type: 'error', }); } @@ -377,34 +364,64 @@ export default function Dashboard() { setButtonReady(false); const secretsToBeDeleted - = initialData + = initialData! .filter(initDataPoint => !newData!.map(newDataPoint => newDataPoint.id).includes(initDataPoint.id)) .map(secret => secret.id); + console.log('delete', secretsToBeDeleted.length) const secretsToBeAdded = newData! - .filter(newDataPoint => !initialData.map(initDataPoint => initDataPoint.id).includes(newDataPoint.id)); + .filter(newDataPoint => !initialData!.map(initDataPoint => initDataPoint.id).includes(newDataPoint.id)); + console.log('add', secretsToBeAdded.length) const secretsToBeUpdated - = newData!.filter(newDataPoint => initialData + = newData!.filter(newDataPoint => initialData! .filter(initDataPoint => newData!.map(newDataPoint => newDataPoint.id).includes(initDataPoint.id) && (newData!.filter(newDataPoint => newDataPoint.id == initDataPoint.id)[0].value != initDataPoint.value || newData!.filter(newDataPoint => newDataPoint.id == initDataPoint.id)[0].key != initDataPoint.key || newData!.filter(newDataPoint => newDataPoint.id == initDataPoint.id)[0].comment != initDataPoint.comment)) .map(secret => secret.id).includes(newDataPoint.id)); + console.log('update', secretsToBeUpdated.length) + + const newOverrides = newData!.filter(newDataPoint => newDataPoint.valueOverride != undefined) + const initOverrides = initialData!.filter(initDataPoint => initDataPoint.valueOverride != undefined) + + const overridesToBeDeleted + = initOverrides + .filter(initDataPoint => !newOverrides!.map(newDataPoint => newDataPoint.id).includes(initDataPoint.id)) + .map(secret => String(secret.idOverride)); + console.log('override delete', overridesToBeDeleted.length) + + const overridesToBeAdded + = newOverrides! + .filter(newDataPoint => !initOverrides.map(initDataPoint => initDataPoint.id).includes(newDataPoint.id)) + .map(override => ({pos: override.pos, key: override.key, value: String(override.valueOverride), valueOverride: override.valueOverride, comment: '', id: String(override.idOverride), idOverride: String(override.idOverride)})); + console.log('override add', overridesToBeAdded.length) + + const overridesToBeUpdated + = newOverrides!.filter(newDataPoint => initOverrides + .filter(initDataPoint => newOverrides!.map(newDataPoint => newDataPoint.id).includes(initDataPoint.id) + && (newOverrides!.filter(newDataPoint => newDataPoint.id == initDataPoint.id)[0].valueOverride != initDataPoint.valueOverride + || newOverrides!.filter(newDataPoint => newDataPoint.id == initDataPoint.id)[0].key != initDataPoint.key + || newOverrides!.filter(newDataPoint => newDataPoint.id == initDataPoint.id)[0].comment != initDataPoint.comment)) + .map(secret => secret.id).includes(newDataPoint.id)) + .map(override => ({pos: override.pos, key: override.key, value: String(override.valueOverride), valueOverride: override.valueOverride, comment: '', id: String(override.idOverride), idOverride: String(override.idOverride)})); + console.log('override update', overridesToBeUpdated.length) - if (secretsToBeDeleted.length > 0) { - await deleteSecrets({ secretIds: secretsToBeDeleted }); + if (secretsToBeDeleted.concat(overridesToBeDeleted).length > 0) { + await deleteSecrets({ secretIds: secretsToBeDeleted.concat(overridesToBeDeleted) }); } - if (secretsToBeAdded.length > 0) { - const secrets = await encryptSecrets({ secretsToEncrypt: secretsToBeAdded, workspaceId, env: envMapping[env] }) - secrets && await addSecrets({ secrets, env: envMapping[env], workspaceId }); + if (secretsToBeAdded.concat(overridesToBeAdded).length > 0) { + const secrets = await encryptSecrets({ secretsToEncrypt: secretsToBeAdded.concat(overridesToBeAdded), workspaceId, env: selectedEnv.slug }); + secrets && await addSecrets({ secrets, env: selectedEnv.slug, workspaceId }); } - if (secretsToBeUpdated.length > 0) { - const secrets = await encryptSecrets({ secretsToEncrypt: secretsToBeUpdated, workspaceId, env: envMapping[env] }) + if (secretsToBeUpdated.concat(overridesToBeUpdated).length > 0) { + const secrets = await encryptSecrets({ secretsToEncrypt: secretsToBeUpdated.concat(overridesToBeUpdated), workspaceId, env: selectedEnv.slug }); secrets && await updateSecrets({ secrets }); } + setInitialData(structuredClone(newData)); + // If this user has never saved environment variables before, show them a prompt to read docs if (!hasUserEverPushed) { setCheckDocsPopUpVisible(true); @@ -413,6 +430,7 @@ export default function Dashboard() { // increasing the number of project commits setNumSnapshots((numSnapshots ?? 0) + 1); + setSaveLoading(false); }; const addData = (newData: SecretDataProps[]) => { @@ -424,36 +442,49 @@ export default function Dashboard() { setBlurred(!blurred); }; - const sortValuesHandler = (dataToSort: SecretDataProps[] | 1, specificSortMethod?: 'alphabetical' | '-alphabetical') => { - const howToSort = specificSortMethod == undefined ? sortMethod : specificSortMethod; + const sortValuesHandler = ( + dataToSort: SecretDataProps[] | 1, + specificSortMethod?: 'alphabetical' | '-alphabetical' + ) => { + const howToSort = + specificSortMethod == undefined ? sortMethod : specificSortMethod; const sortedData = (dataToSort != 1 ? dataToSort : data)! - .sort((a, b) => - howToSort == 'alphabetical' - ? a.key.localeCompare(b.key) - : b.key.localeCompare(a.key) - ) - .map((item: SecretDataProps, index: number) => { - return { - ...item, - pos: index - }; - }); + .sort((a, b) => + howToSort == 'alphabetical' + ? a.key.localeCompare(b.key) + : b.key.localeCompare(a.key) + ) + .map((item: SecretDataProps, index: number) => { + return { + ...item, + pos: index, + }; + }); setData(sortedData); }; - - const deleteCertainRow = ({ ids, secretName }: { ids: string[]; secretName: string; }) => { - deleteRow({ids, secretName}); + + const deleteCertainRow = ({ + ids, + secretName, + }: { + ids: string[]; + secretName: string; + }) => { + deleteRow({ ids, secretName }); }; return data ? ( -
+
- {t("common:head-title", { title: t("dashboard:title") })} - - - - + {t('common:head-title', { title: t('dashboard:title') })} + + + +
{sidebarSecretId != "None" && row.key == data.filter(row => row.id == sidebarSecretId)[0]?.key)} modifyKey={listenChangeKey} modifyValue={listenChangeValue} + modifyValueOverride={listenChangeValueOverride} modifyComment={listenChangeComment} - addOverride={addOverride} - deleteOverride={deleteOverride} buttonReady={buttonReady} savePush={savePush} sharedToHide={sharedToHide} @@ -479,59 +509,72 @@ export default function Dashboard() { {checkDocsPopUpVisible && ( )} -
- {snapshotData && -
-
} -
-
-

{snapshotData ? "Secret Snapshot" : t("dashboard:title")}

- {snapshotData && {new Date(snapshotData.createdAt).toLocaleString()}} +
+ {snapshotData && ( +
+
+ )} +
+
+

{snapshotData ? 'Secret Snapshot' : t('dashboard:title')}

+ {snapshotData && ( + + {new Date(snapshotData.createdAt).toLocaleString()} + + )}
{!snapshotData && data?.length == 0 && ( name)} + onChange={(envName) => + setSelectedEnv( + workspaceEnvs.find(({ name }) => envName === name) || { + name: 'unknown', + slug: 'unknown', + } + ) + } /> )}
-
+
{(data?.length !== 0 || buttonReady) && !snapshotData && (
)} @@ -541,24 +584,14 @@ export default function Dashboard() { onButtonPressed={async () => { // Update secrets in the state only for the current environment const rolledBackSecrets = snapshotData.secretVersions - .filter(row => reverseEnvMapping[row.environment] == env) + .filter(row => row.environment == selectedEnv.slug) .map((sv, position) => { return { - id: sv.id, pos: position, type: sv.type, key: sv.key, value: sv.value, comment: '' + id: sv.id, idOverride: sv.id, pos: position, valueOverride: sv.valueOverride, key: sv.key, value: sv.value, comment: '' } }); setData(rolledBackSecrets); - setSharedToHide( - rolledBackSecrets?.filter(row => (rolledBackSecrets - ?.map((item) => item.key) - .filter( - (item, index) => - index !== - rolledBackSecrets?.map((item) => item.key).indexOf(item) - ).includes(row.key) && row.type == 'shared'))?.map((item) => item.id) - ) - // Perform the rollback globally performSecretRollback({ workspaceId, version: snapshotData.version }) @@ -575,148 +608,183 @@ export default function Dashboard() {
}
-
-
-
+
+
+
{(snapshotData || data?.length !== 0) && ( <> - {!snapshotData - ? - : } -
+ {!snapshotData ? ( + name)} + onChange={(envName) => + setSelectedEnv( + workspaceEnvs.find( + ({ name }) => envName === name + ) || { + name: 'unknown', + slug: 'unknown', + } + ) + } + /> + ) : ( + name)} + onChange={(envName) => + setSelectedSnapshotEnv( + workspaceEnvs.find( + ({ name }) => envName === name + ) || { + name: 'unknown', + slug: 'unknown', + } + ) + } + /> + )} +
setSearchKeys(e.target.value)} - placeholder={String(t("dashboard:search-keys"))} + placeholder={String(t('dashboard:search-keys'))} />
- {!snapshotData &&
-
} - {!snapshotData &&
- -
} -
+ {!snapshotData && ( +
+
+ )} + {!snapshotData && ( +
+ +
+ )} +
- {!snapshotData &&
-
} + {!snapshotData && ( +
+
+ )} )}
{isLoading ? ( -
- infisical loading indicator -
- ) : ( - data?.length !== 0 ? ( -
+
+ infisical loading indicator +
+ ) : data?.length !== 0 ? ( +
{!snapshotData && data?.filter(row => row.key?.toUpperCase().includes(searchKeys.toUpperCase())) - .filter(row => !(sharedToHide.includes(row.id) && row.type == 'shared')).map((keyPair) => ( + .filter(row => !sharedToHide.includes(row.id)).map((keyPair) => ( item.key + item.type) - )?.includes(keyPair.key + keyPair.type)} + data?.map((item) => item.key) + )?.includes(keyPair.key)} toggleSidebar={toggleSidebar} sidebarSecretId={sidebarSecretId} isSnapshot={false} /> ))} {snapshotData && snapshotData.secretVersions?.sort((a, b) => a.key.localeCompare(b.key)) - .filter(row => reverseEnvMapping[row.environment] == snapshotEnv) + .filter(row => row.environment == selectedSnapshotEnv?.slug) .filter(row => row.key.toUpperCase().includes(searchKeys.toUpperCase())) - .filter(row => !(snapshotData.secretVersions?.filter(row => (snapshotData.secretVersions + .filter( + row => !(snapshotData.secretVersions?.filter(row => (snapshotData.secretVersions ?.map((item) => item.key) .filter( (item, index) => index !== snapshotData.secretVersions?.map((item) => item.key).indexOf(item) - ).includes(row.key) && row.type == 'shared'))?.map((item) => item.id).includes(row.id) && row.type == 'shared')).map((keyPair) => ( + ).includes(row.key)))?.map((item) => item.id).includes(row.id)) + ) + .map((keyPair) => ( item.key + item.type) - )?.includes(keyPair.key + keyPair.type)} + data?.map((item) => item.key) + )?.includes(keyPair.key)} toggleSidebar={toggleSidebar} sidebarSecretId={sidebarSecretId} isSnapshot={true} /> ))}
- {!snapshotData &&
- -
} + {!snapshotData && ( +
+ +
+ )}
) : ( -
+
{isKeyAvailable && !snapshotData && ( )} - { - (!isKeyAvailable && ( - <> - -

- To view this file, contact your administrator for - permission. -

-

- They need to grant you access in the team tab. -

- - ))} + {!isKeyAvailable && ( + <> + +

+ To view this file, contact your administrator for + permission. +

+

+ They need to grant you access in the team tab. +

+ + )}
- ))} + )}
) : ( -
-
+
+
loading animation
); @@ -765,4 +832,4 @@ export default function Dashboard() { Dashboard.requireAuth = true; -export const getServerSideProps = getTranslatedServerSideProps(["dashboard"]); +export const getServerSideProps = getTranslatedServerSideProps(['dashboard']); diff --git a/frontend/pages/integrations/[id].js b/frontend/pages/integrations/[id].js index 32ff8d2d94..8a11bc3c44 100644 --- a/frontend/pages/integrations/[id].js +++ b/frontend/pages/integrations/[id].js @@ -24,6 +24,7 @@ import setBotActiveStatus from "../api/bot/setBotActiveStatus"; import getIntegrationOptions from "../api/integrations/GetIntegrationOptions"; import getWorkspaceAuthorizations from "../api/integrations/getWorkspaceAuthorizations"; import getWorkspaceIntegrations from "../api/integrations/getWorkspaceIntegrations"; +import getAWorkspace from "../api/workspace/getAWorkspace"; import getLatestFileKey from "../api/workspace/getLatestFileKey"; const { decryptAssymmetric, @@ -34,6 +35,7 @@ const crypto = require("crypto"); export default function Integrations() { const [cloudIntegrationOptions, setCloudIntegrationOptions] = useState([]); const [integrationAuths, setIntegrationAuths] = useState([]); + const [environments,setEnvironments] = useState([]) const [integrations, setIntegrations] = useState([]); const [bot, setBot] = useState(null); const [isActivateBotDialogOpen, setIsActivateBotDialogOpen] = useState(false); @@ -41,11 +43,15 @@ export default function Integrations() { const [selectedIntegrationOption, setSelectedIntegrationOption] = useState(null); const router = useRouter(); + const workspaceId = router.query.id; const { t } = useTranslation(); useEffect(async () => { try { + const workspace = await getAWorkspace(workspaceId); + setEnvironments(workspace.environments); + // get cloud integration options setCloudIntegrationOptions( await getIntegrationOptions() @@ -54,23 +60,19 @@ export default function Integrations() { // get project integration authorizations setIntegrationAuths( await getWorkspaceAuthorizations({ - workspaceId: router.query.id, + workspaceId }) ); // get project integrations setIntegrations( await getWorkspaceIntegrations({ - workspaceId: router.query.id, + workspaceId, }) ); // get project bot - setBot( - await getBot({ - workspaceId: router.query.id - } - )); + setBot(await getBot({ workspaceId })); } catch (err) { console.log(err); @@ -90,7 +92,7 @@ export default function Integrations() { if (bot) { // case: there is a bot - const key = await getLatestFileKey({ workspaceId: router.query.id }); + const key = await getLatestFileKey({ workspaceId }); const PRIVATE_KEY = localStorage.getItem('PRIVATE_KEY'); const WORKSPACE_KEY = decryptAssymmetric({ @@ -132,9 +134,6 @@ export default function Integrations() { * @returns */ const handleIntegrationOption = async ({ integrationOption }) => { - - console.log('handleIntegrationOption', integrationOption); - try { // generate CSRF token for OAuth2 code-token exchange integrations const state = crypto.randomBytes(16).toString("hex"); @@ -217,8 +216,8 @@ export default function Integrations() { handleBotActivate={handleBotActivate} handleIntegrationOption={handleIntegrationOption} /> */} - - {cloudIntegrationOptions.length > 0 ? ( + + {(cloudIntegrationOptions.length > 0 && bot) ? (
- Forgot password? + + +
{!isLoading && errorLogin && ( - + )}
@@ -160,7 +164,7 @@ export default function Login() { diff --git a/frontend/pages/settings/personal/[id].js b/frontend/pages/settings/personal/[id].js index 5eebb83283..a592bf45be 100644 --- a/frontend/pages/settings/personal/[id].js +++ b/frontend/pages/settings/personal/[id].js @@ -126,7 +126,7 @@ export default function PersonalSettings() { diff --git a/frontend/pages/settings/project/[id].js b/frontend/pages/settings/project/[id].js deleted file mode 100644 index 3449df346a..0000000000 --- a/frontend/pages/settings/project/[id].js +++ /dev/null @@ -1,306 +0,0 @@ -import { useEffect, useRef, useState } from "react"; -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 "~/components/basic/buttons/Button"; -import AddServiceTokenDialog from "~/components/basic/dialog/AddServiceTokenDialog"; -import InputField from "~/components/basic/InputField"; -import ServiceTokenTable from "~/components/basic/table/ServiceTokenTable.tsx"; -import NavHeader from "~/components/navigation/NavHeader"; -import { getTranslatedServerSideProps } from "~/utilities/withTranslateProps"; - -import getServiceTokens from "../../api/serviceToken/getServiceTokens"; -import deleteWorkspace from "../../api/workspace/deleteWorkspace"; -import getWorkspaces from "../../api/workspace/getWorkspaces"; -import renameWorkspace from "../../api/workspace/renameWorkspace"; - - -export default function SettingsBasic() { - const [buttonReady, setButtonReady] = useState(false); - const router = useRouter(); - const [workspaceName, setWorkspaceName] = useState(""); - const [serviceTokens, setServiceTokens] = useState([]); - const [workspaceToBeDeletedName, setWorkspaceToBeDeletedName] = useState(""); - const [workspaceId, setWorkspaceId] = useState(""); - const [isAddOpen, setIsAddOpen] = useState(false); - let [isAddServiceTokenDialogOpen, setIsAddServiceTokenDialogOpen] = - useState(false); - const [projectIdCopied, setProjectIdCopied] = useState(false); - - const { t } = useTranslation(); - - /** - * This function copies the project id to the clipboard - */ - function copyToClipboard() { - // const copyText = document.getElementById('myInput') as HTMLInputElement; - const copyText = document.getElementById('myInput') - - if (copyText) { - copyText.select(); - copyText.setSelectionRange(0, 99999); // For mobile devices - - navigator.clipboard.writeText(copyText.value); - - setProjectIdCopied(true); - setTimeout(() => setProjectIdCopied(false), 2000); - } - } - - useEffect(async () => { - let userWorkspaces = await getWorkspaces(); - userWorkspaces.map((userWorkspace) => { - if (userWorkspace._id == router.query.id) { - setWorkspaceName(userWorkspace.name); - } - }); - let tempServiceTokens = await getServiceTokens({ - workspaceId: router.query.id, - }); - setServiceTokens(tempServiceTokens); - }, []); - - const modifyWorkspaceName = (newName) => { - setButtonReady(true); - setWorkspaceName(newName); - }; - - const submitChanges = (newWorkspaceName) => { - renameWorkspace(router.query.id, newWorkspaceName); - setButtonReady(false); - }; - - useEffect(async () => { - setWorkspaceId(router.query.id); - }, []); - - function closeAddModal() { - setIsAddOpen(false); - } - - function openAddModal() { - setIsAddOpen(true); - } - - 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 () => { - let userWorkspaces = await getWorkspaces(); - - if (userWorkspaces.length > 1) { - if ( - userWorkspaces.filter( - (workspace) => workspace._id == router.query.id - )[0].name == workspaceToBeDeletedName - ) { - await deleteWorkspace(router.query.id); - let userWorkspaces = await getWorkspaces(); - router.push("/dashboard/" + userWorkspaces[0]._id); - } - } - }; - - return ( -
- - - {t("common:head-title", { title: t("settings-project:title") })} - - - - -
-
- -
-
-

- {t("settings-project:title")} -

-

- {t("settings-project:description")} -

-
-
-
-
-
-
-

- {t("common:display-name")} -

-
- -
-
-
-
-
-
-
-

- {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" - )}:`}

- -
- - - {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 - . -

-
-
-
-
- -
-
-
-
-

- {t("settings-project:danger-zone")} -

-

- {t("settings-project:danger-zone-note")} -

-
- -
- -

- {t("settings-project:delete-project-note")} -

-
-
-
-
-
- ); -} - -SettingsBasic.requireAuth = true; - -export const getServerSideProps = getTranslatedServerSideProps([ - "settings", - "settings-project", - "section-token", -]); diff --git a/frontend/pages/settings/project/[id].tsx b/frontend/pages/settings/project/[id].tsx new file mode 100644 index 0000000000..5b9e3f5de9 --- /dev/null +++ b/frontend/pages/settings/project/[id].tsx @@ -0,0 +1,358 @@ +import { useEffect, useState } from 'react'; +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 '~/components/basic/buttons/Button'; +import AddServiceTokenDialog from '~/components/basic/dialog/AddServiceTokenDialog'; +import InputField from '~/components/basic/InputField'; +import EnvironmentTable from '~/components/basic/table/EnvironmentsTable'; +import ServiceTokenTable from '~/components/basic/table/ServiceTokenTable'; +import NavHeader from '~/components/navigation/NavHeader'; +import deleteEnvironment from '~/pages/api/environments/deleteEnvironment'; +import updateEnvironment from '~/pages/api/environments/updateEnvironment'; +import { getTranslatedServerSideProps } from '~/utilities/withTranslateProps'; + +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; + + 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 userWorkspaces = await getWorkspaces(); + router.push('/dashboard/' + userWorkspaces[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')} +

+
+ +
+
+
+
+
+
+
+

+ {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' + )}:`}

+ +
+ + + {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 + + . +

+
+
+
+
+ +
+
+
+
+

+ {t('settings-project:danger-zone')} +

+

+ {t('settings-project:danger-zone-note')} +

+
+ +
+ +

+ {t('settings-project:delete-project-note')} +

+
+
+
+
+
+ ); +} + +SettingsBasic.requireAuth = true; + +export const getServerSideProps = getTranslatedServerSideProps([ + 'settings', + 'settings-project', + 'section-token', +]); diff --git a/frontend/public/data/frequentInterfaces.ts b/frontend/public/data/frequentInterfaces.ts new file mode 100644 index 0000000000..c2fa797fd5 --- /dev/null +++ b/frontend/public/data/frequentInterfaces.ts @@ -0,0 +1,8 @@ +export interface SecretDataProps { + pos: number; + key: string; + value: string; + valueOverride: string | undefined; + id: string; + comment: string; +} \ No newline at end of file diff --git a/frontend/public/locales/en/login.json b/frontend/public/locales/en/login.json index ee097fd920..75b56a014f 100644 --- a/frontend/public/locales/en/login.json +++ b/frontend/public/locales/en/login.json @@ -4,5 +4,7 @@ "og-description": "Infisical a simple end-to-end encrypted platform that enables teams to sync and manage their .env files.", "login": "Log In", "need-account": "Need an Infisical account?", - "create-account": "Create an account" + "create-account": "Create an account", + "forgot-password": "Forgot your password?", + "error-login": "Wrong credentials." } diff --git a/frontend/public/locales/en/signup.json b/frontend/public/locales/en/signup.json index 0cdbe310b1..bee993727f 100644 --- a/frontend/public/locales/en/signup.json +++ b/frontend/public/locales/en/signup.json @@ -9,8 +9,11 @@ "step1-start": "Let's get started", "step1-privacy": "By creating an account, you agree to our Terms and have read and acknowledged the Privacy Policy.", "step1-submit": "Get Started", - "step2-message": "We've sent a verification email to{{email}}", + "step2-message": "We've sent a verification email to", "step2-code-error": "Oops. Your code is wrong. Please try again.", + "step2-resend-alert": "Don't see the email?", + "step2-resend-submit": "Resend", + "step2-resend-progress": "Resending...", "step2-spam-alert": "Make sure to check your spam inbox.", "step3-message": "Almost there!", "step4-message": "Save your Emergency Kit", diff --git a/frontend/public/locales/fr/activity.json b/frontend/public/locales/fr/activity.json new file mode 100644 index 0000000000..4360d7d0c9 --- /dev/null +++ b/frontend/public/locales/fr/activity.json @@ -0,0 +1,11 @@ +{ + "title": "Journaux d'activité", + "subtitle": "Historique des événements pour ce projet Infisical.", + "event": { + "readSecrets": "Secrets Visualisés", + "updateSecrets": "Secrets Mis à jour", + "addSecrets": "Secrets Ajoutés", + "deleteSecrets": "Secrets Supprimés" + }, + "ip-address": "Adresse IP" +} diff --git a/frontend/public/locales/fr/billing.json b/frontend/public/locales/fr/billing.json new file mode 100644 index 0000000000..1acb44339e --- /dev/null +++ b/frontend/public/locales/fr/billing.json @@ -0,0 +1,28 @@ +{ + "title": "Utilisation et Facturation", + "description": "Voir et gérer l'abonnement de votre organisation ici", + "subscription": "Abonnement", + "starter": { + "name": "Starter", + "price-explanation": "jusqu'à 5 membres de l'équipe", + "text": "Gérez n'importe quel projet jusqu'à 5 membres gratuitement!", + "subtext": "$5 par membre / mois par la suite." + }, + "professional": { + "name": "Professionnel", + "price-explanation": "/membre/mois", + "subtext": "Comprend des projets et des membres illimités.", + "text": "Suivez la gestion clé à mesure que vous grandissez." + }, + "enterprise": { + "name": "Entreprise", + "text": "Suivez la gestion clé à mesure que vous grandissez." + }, + "current-usage": "Utilisation actuelle", + "free": "Gratuit", + "downgrade": "Rétrograder", + "upgrade": "Améliorer", + "learn-more": "En savoir plus", + "custom-pricing": "Prix personnalisés", + "schedule-demo": "Planifier une démo" +} diff --git a/frontend/public/locales/fr/common.json b/frontend/public/locales/fr/common.json new file mode 100644 index 0000000000..8552668aa9 --- /dev/null +++ b/frontend/public/locales/fr/common.json @@ -0,0 +1,34 @@ +{ + "head-title": "{{title}} | Infiscal", + "error_project-already-exists": "Un projet avec ce nom existe déjà.", + "no-mobile": " Pour utiliser Infisical, veuillez vous connecter avec un appareil avec des dimensions plus grandes. ", + "email": "Email", + "password": "Mot de passe", + "first-name": "Prénom", + "last-name": "Nom", + "logout": "Déconnexion", + "validate-required": "Veuillez saisir votre {{name}}", + "maintenance-alert": "Nous rencontrons des difficultés techniques mineures. Nous travaillons sur leurs résolution dès maintenant. Revenez dans quelques minutes.", + "click-to-copy": "Cliquez pour copiez", + "project-id": "Identifiant du Projet", + "save-changes": "Sauvegarder les modifications", + "saved": "Enregistrée", + "drop-zone": "Glissez et déposez un fichier .env ou .yml ici.", + "drop-zone-keys": "Glissez et déposez un fichier .env ou .yml ici pour ajouter plus de clés.", + "role": "Rôle", + "role_admin": "administrateur", + "display-name": "Nom d'affichage", + "environment": "Environnement", + "expired-in": "Expire dans", + "language": "Langue", + "search": "Recherche...", + "note": "Note", + "view-more": "Voir plus", + "end-of-history": "Fin de l'historique", + "select-event": "Sélectionnez un événement", + "event": "Événement", + "user": "Utilisateur", + "source": "Source", + "time": "Heure", + "timestamp": "Horodatage" +} diff --git a/frontend/public/locales/fr/dashboard.json b/frontend/public/locales/fr/dashboard.json new file mode 100644 index 0000000000..f90bf1fc21 --- /dev/null +++ b/frontend/public/locales/fr/dashboard.json @@ -0,0 +1,36 @@ +{ + "title": "Secrets", + "og-title": "Gérez vos fichiers .env rapidement", + "og-description": "Infisical une plate-forme simple et chiffré de bout en bout qui permet aux équipes de synchroniser et de gérer leurs fichiers .env.", + "search-keys": "Recherche les clefs...", + "add-key": "Ajouter une clef", + "personal": "Personnel", + "personal-description": "Les clés personnelles ne sont visibles que pour vous", + "shared": "Partagé", + "shared-description": "Les clés partagées sont visibles à toute votre équipe", + "make-shared": "Rendre Partagé", + "make-personal": "Rendre Personnel", + "add-secret": "Ajouter un nouveau secret", + "check-docs": { + "button": "Vérifier la documentation", + "title": "Bon travail!", + "line1": "Félicitations pour avoir ajouté plus de secrets.", + "line2": "Voici comment les connecter à votre base de code." + }, + "sidebar": { + "secret": "Secret", + "key": "Clef", + "value": "Valeur", + "override": "Remplacer la valeur avec une valeur personnelle", + "version-history": "Historique des versions", + "comments": "Commentaires & Notes", + "personal-explanation": "Ce secret est personnel. Il n'est partagé avec aucun de vos coéquipiers.", + "generate-random-hex": "Générer un Hex aléatoire", + "digits": "chiffres", + "delete-key-dialog": { + "title": "Supprimer la clef", + "confirm-delete-message": "Êtes-vous sûr de vouloir supprimer ce secret? Cela ne peut pas être annulé." + } + } + +} diff --git a/frontend/public/locales/fr/integrations.json b/frontend/public/locales/fr/integrations.json new file mode 100644 index 0000000000..887e79dfd6 --- /dev/null +++ b/frontend/public/locales/fr/integrations.json @@ -0,0 +1,16 @@ +{ + "title": "Intégrations de Projet", + "description": "Gérez vos intégrations d'Infisical avec des services tiers.", + "no-integrations1": "Vous n'avez pas encore d'intégration. Quand vous en aurez, elles apparaîtront ici.", + "no-integrations2": "Pour commencer, cliquez sur l'une des options ci-dessous. La configuration se fait en 5 clics.", + "available": "Intégrations de plate-forme et cloud", + "available-text1": "Cliquez sur l'intégration que vous souhaitez connecter. Cela permettra à vos variables d'environnement de circuler automatiquement dans les services tiers sélectionnés.", + "available-text2": "Remarque: Lors d'une intégration avec Heroku, pour des raisons de sécurité, il est impossible de maintenir le chiffrage de bout en bout. En théorie, cela permet à Infisical de déchiffrer les variables d'environnement. En pratique, nous pouvons vous assurer que cela ne sera jamais fait, et cela nous permet de protéger vos secrets des mauvais acteurs en ligne. Le service Infisical de base restera toujours chiffré de bout en bout. Pour toutes vos intérogations, contactez support@infisical.com.", + "cloud-integrations": "Intégrations Cloud", + "framework-integrations": "Intégrations Framework", + "click-to-start": "Cliquez sur une intégration pour commencer à synchroniser les secrets avec elle.", + "click-to-setup": "Cliquez sur un framework pour obtenir les instructions de configuration.", + "grant-access-to-secrets": "Accordez un accès Infisical à vos secrets", + "why-infisical-needs-access": "La plupart des intégrations cloud nécessitent qu'Infisical puisse déchiffrer vos secrets afin qu'ils puissent être transmis.", + "grant-access-button": "Autoriser l'accès" +} diff --git a/frontend/public/locales/fr/login.json b/frontend/public/locales/fr/login.json new file mode 100644 index 0000000000..8c93da9fbd --- /dev/null +++ b/frontend/public/locales/fr/login.json @@ -0,0 +1,10 @@ +{ + "title": "Connexion", + "og-title": "Connectez-vous à Infisical", + "og-description": "Infisical, une plate-forme simple et chiffré de bout en bout permettant aux équipes de synchroniser et de gérer leurs fichiers .env.", + "login": "Se connecter", + "need-account": "Besoin d'un compte Infisical?", + "create-account": "Créer un compte", + "forgot-password": "Mot de passe oublié?", + "error-login": "Mauvais identifiants." +} diff --git a/frontend/public/locales/fr/nav.json b/frontend/public/locales/fr/nav.json new file mode 100644 index 0000000000..1fda27af97 --- /dev/null +++ b/frontend/public/locales/fr/nav.json @@ -0,0 +1,22 @@ +{ + "support": { + "slack": "[NEW] Rejoignez le forum Slack", + "docs": "Lire les documentations", + "issue": "Ouvrir une issue Github", + "email": "Envoyez-nous un email" + }, + "user": { + "signed-in-as": "CONNECTÉ EN TANT QUE", + "current-organization": "ORGANISATION ACTUELLE", + "usage-billing": "Utilisation & Facturation", + "invite": "Inviter des membres", + "other-organizations": "AUTRE ORGANISATION" + }, + "menu": { + "project": "PROJET", + "secrets": "Secrets", + "members": "Membres", + "integrations": "Intégrations", + "project-settings": "Paramètres du Projet" + } +} diff --git a/frontend/public/locales/fr/section-incident.json b/frontend/public/locales/fr/section-incident.json new file mode 100644 index 0000000000..95b77ea13a --- /dev/null +++ b/frontend/public/locales/fr/section-incident.json @@ -0,0 +1,11 @@ +{ + "incident-contacts": "Contacts Incidents", + "incident-contacts-description": "Ces contacts seront informés dans le cas improbable d'un incident grave.", + "no-incident-contacts": "Aucun contact incident trouvé.", + "add-contact": "Ajouter un contact", + "add-dialog": { + "title": "Ajouter un contact incident", + "description": "Ce contact sera informé dans le cas improbable d'un incident grave.", + "add-incident": "Ajouter un contact incident" + } +} diff --git a/frontend/public/locales/fr/section-members.json b/frontend/public/locales/fr/section-members.json new file mode 100644 index 0000000000..8faed80e76 --- /dev/null +++ b/frontend/public/locales/fr/section-members.json @@ -0,0 +1,14 @@ +{ + "add-member": "Ajouter un Membre", + "org-members": "Membres de l'organisation", + "org-members-description": "Gérer les membres de votre organisation. Ces utilisateurs pourraient ensuite être répartis en projets.", + "search-members": "Recherche des membres...", + "add-dialog": { + "add-member-to-project": "Ajoutez un membre à votre projet", + "already-all-invited": "Tous les utilisateurs de votre organisation sont déjà invités.", + "add-user-org-first": "Ajoutez d'abord plus d'utilisateurs à l'organisation.", + "user-will-email": "L'utilisateur recevra un email avec les instructions.", + "looking-add": "<0>Si vous cherchez à ajouter des utilisateurs à votre organisation,<1>cliquez ici", + "add-user-to-org": "Ajouter des Utilisateurs à l'Organisation" + } +} diff --git a/frontend/public/locales/fr/section-password.json b/frontend/public/locales/fr/section-password.json new file mode 100644 index 0000000000..0928233e64 --- /dev/null +++ b/frontend/public/locales/fr/section-password.json @@ -0,0 +1,11 @@ +{ + "password": "Mot de passe", + "change": "Changer le mot de passe", + "current": "Mot de passe actuel", + "current-wrong": "Le mot de passe actuel peut être érroné", + "new": "Nouveau mot de passe", + "validate-base": "Le mot de passe doit contenir au moins:", + "validate-length": "14 caractères", + "validate-case": "1 caractère miniscule", + "validate-number": "1 chiffre" +} diff --git a/frontend/public/locales/fr/section-token.json b/frontend/public/locales/fr/section-token.json new file mode 100644 index 0000000000..1f2a3b2332 --- /dev/null +++ b/frontend/public/locales/fr/section-token.json @@ -0,0 +1,13 @@ +{ + "service-tokens": "Jetons de service", + "service-tokens-description": "Chaque jeton de service vous est spécifique, à un certain projet et à un certain environnement dans ce projet.", + "add-new": "Ajouter un nouveau jeton", + "add-dialog": { + "title": "Ajouter un jeton de service pour {{target}}", + "description": "Spécifiez le nom, l'environnement et la période d'expiration. Lorsqu'un jeton est généré, vous ne pourrez le voir qu'une seule fois avant qu'il ne disparaisse. Assurez-vous de le sauvegarder quelque part.", + "name": "Nom du jeton de service", + "add": "Ajouter un jeton de service", + "copy-service-token": "Copiez votre jeton de service", + "copy-service-token-description": "Une fois que vous aurez fermé cette fenêtre, vous ne reverrez plus jamais votre jeton de service" + } +} diff --git a/frontend/public/locales/fr/settings-members.json b/frontend/public/locales/fr/settings-members.json new file mode 100644 index 0000000000..9f34c7a68a --- /dev/null +++ b/frontend/public/locales/fr/settings-members.json @@ -0,0 +1,4 @@ +{ + "title": "Membres du projet", + "description": "Cette page affiche les membres du projet sélectionné." +} diff --git a/frontend/public/locales/fr/settings-org.json b/frontend/public/locales/fr/settings-org.json new file mode 100644 index 0000000000..952bd68c8f --- /dev/null +++ b/frontend/public/locales/fr/settings-org.json @@ -0,0 +1,4 @@ +{ + "title": "Paramètres d'Organisation", + "description": "Gérer les membres de votre organisation. Ces utilisateurs pourraient ensuite être répartis en projets." +} diff --git a/frontend/public/locales/fr/settings-personal.json b/frontend/public/locales/fr/settings-personal.json new file mode 100644 index 0000000000..30a3d6fa3b --- /dev/null +++ b/frontend/public/locales/fr/settings-personal.json @@ -0,0 +1,11 @@ +{ + "title": "Paramètres Personnels", + "description": "Consultez et gérez vos informations personnelles ici.", + "emergency": { + "name": "Kit d'urgence", + "text1": "Votre kit d'urgence contient les informations dont vous aurez besoin pour vous connecter à votre compte Infisical.", + "text2": "Seul le dernier kit d'urgence émis reste valide. Pour obtenir un nouveau kit d'urgence, vérifiez votre mot de passe.", + "download": "Télécharger le kit d'urgence" + }, + "change-language": "Changer de langue" +} diff --git a/frontend/public/locales/fr/settings-project.json b/frontend/public/locales/fr/settings-project.json new file mode 100644 index 0000000000..a5a3f1ee35 --- /dev/null +++ b/frontend/public/locales/fr/settings-project.json @@ -0,0 +1,13 @@ +{ + "title": "Paramètres du Projet", + "description": "Ces paramètres ne s'appliquent qu'au Projet actuellement sélectionné.", + "danger-zone": "Zone de danger", + "delete-project": "Supprimer le Projet", + "project-to-delete": "Projet à Supprimer", + "danger-zone-note": "Dès que vous supprimez ce projet, vous ne pourrez plus revenir en arrière. Cela supprimera immédiatement toutes les clefs. Si vous voulez toujours le faire, veuillez saisir le nom du projet ci-dessous.", + "delete-project-note": "Remarque: Vous ne pouvez supprimer qu'un projet que si vous en avez plus d'un.", + "project-id-description": "Pour intégrer Infisical dans votre base de code et obtenir une injection automatique de variables d'environnement, vous devez utiliser l'ID du projet suivant.", + "project-id-description2": "Pour plus de conseils, y compris des extraits de code pour diverses langues et frameworks, voir ", + "auto-generated": "Ceci est l'identifiant unique généré automatiquement pour votre projet. Il ne peut pas être modifié.", + "docs": "Documentation Infisical" +} diff --git a/frontend/public/locales/fr/signup.json b/frontend/public/locales/fr/signup.json new file mode 100644 index 0000000000..48b36e2030 --- /dev/null +++ b/frontend/public/locales/fr/signup.json @@ -0,0 +1,28 @@ +{ + "title": "S'inscrire", + "og-title": "Remplacez les fichiers .env par 1 ligne de code. Inscrivez-vous à Infisical en 3 minutes.", + "og-description": "Infisical, une plate-forme simple et chiffré de bout en bout qui permet aux équipes de synchroniser et de gérer des clefs API et des variables d'environnement. Fonctionne avec Node.js, Next.js, Gatsby, Nest.js ...", + "signup": "S'inscrire", + "already-have-account": "Déjà inscris? Se connecter", + "forgot-password": "Mot de passe oublié?", + "verify": "Vérifier", + "step1-start": "Bon, on commence!", + "step1-privacy": "En créant votre compte, vous acceptez nos conditions et avez lu et reconnu notre politique de confidentialité.", + "step1-submit": "C'est parti", + "step2-message": "Nous avons envoyé un email de vérification à", + "step2-code-error": "Oops. Votre code est faux. Veuillez réessayer.", + "step2-resend-alert": "Vous ne voyez pas l'email?", + "step2-resend-submit": "Renvoyer", + "step2-resend-progress": "Envoie en cours...", + "step2-spam-alert": "Assurez-vous de vérifier vos spams.", + "step3-message": "Nous y sommes presque!", + "step4-message": "Enregistrez votre kit d'urgence", + "step4-description1": "Si vous n'arrivez plus à vous connecter à votre compte, votre kit d'urgence est le seul moyen d'y arriver.", + "step4-description2": "Nous vous recommandons de le télécharger et de le garder en sécurité.", + "step4-description3": "Il contient votre clef secrète que nous ne pouvons pas récupérer pour vous si vous la perdez.", + "step4-download": "Téléchargez le PDF", + "step5-send-invites": "Envoyer les invitations", + "step5-invite-team": "Invitez votre équipe", + "step5-subtitle": "Infisical a pour but d'être utilisé avec vos coéquipiers. Invitez-les à le tester.", + "step5-skip": "Passer" +} diff --git a/frontend/public/locales/pt-BR/activity.json b/frontend/public/locales/pt-BR/activity.json new file mode 100644 index 0000000000..dfc5821aeb --- /dev/null +++ b/frontend/public/locales/pt-BR/activity.json @@ -0,0 +1,8 @@ +{ + "event": { + "readSecrets": "Segredos Visualizados", + "updateSecrets": "Segredos Atualizados", + "addSecrets": "Segredos Adicionados", + "deleteSecrets": "Segredos Excluídos" + } +} \ No newline at end of file diff --git a/frontend/public/locales/pt-BR/billing.json b/frontend/public/locales/pt-BR/billing.json new file mode 100644 index 0000000000..1c920c053a --- /dev/null +++ b/frontend/public/locales/pt-BR/billing.json @@ -0,0 +1,28 @@ +{ + "title": "Uso & Faturamento", + "description": "Visualize e gerencie a assinatura da sua organização aqui", + "subscription": "Inscrição", + "starter": { + "name": "Iniciante", + "price-explanation": "Até 5 membros da equipe", + "text": "Gerencie qualquer projeto com 5 membros gratuitamente!", + "subtext": "$5 por membro / mês depois." + }, + "professional": { + "name": "Profissional", + "price-explanation": "/membro/mês", + "subtext": "Inclui projetos e membros ilimitados.", + "text": "Acompanhe o gerenciamento de chaves à medida que você cresce." + }, + "enterprise": { + "name": "Empreendimento", + "text": "Acompanhe o gerenciamento de chaves à medida que você cresce." + }, + "current-usage": "Uso atual", + "free": "Grátis", + "downgrade": "Reduzir", + "upgrade": "Melhoria", + "learn-more": "Saber Mais", + "custom-pricing": "Preço Personalizado", + "schedule-demo": "Agende uma Demonstração" +} \ No newline at end of file diff --git a/frontend/public/locales/pt-BR/common.json b/frontend/public/locales/pt-BR/common.json new file mode 100644 index 0000000000..12e657d7cc --- /dev/null +++ b/frontend/public/locales/pt-BR/common.json @@ -0,0 +1,26 @@ +{ + "head-title": "{{title}} | Infiscal", + "error_project-already-exists": "Já exite um projeto com este nome.", + "no-mobile": "Para usar o Infisical, faça o login através de um dispositivo com dimensões maiores.", + "email": "Email", + "password": "Senha", + "first-name": "Primeiro Nome", + "last-name": "Ultimo Nome", + "logout": "Sair", + "validate-required": "Por favor insira o seu {{name}}", + "maintenance-alert": "Estamos passando por pequenas dificuldades técnicas. Estamos trabalhando para resolvê-lo agora. Por favor, volte em alguns minutos.", + "click-to-copy": "Clique para copiar", + "project-id": "ID do Projeto", + "save-changes": "Salvar Alterações", + "saved": "Salvou", + "drop-zone": "Arraste e solte seu arquivo .env aqui.", + "drop-zone-keys": "Arraste e solte seu arquivo .env aqui para adicionar mais chaves.", + "role": "Role", + "role_admin": "admin", + "display-name": "Nome de exibição", + "environment": "Ambiente", + "expired-in": "Expira em", + "language": "Linguagem", + "search": "Procurar...", + "note": "Note" +} \ No newline at end of file diff --git a/frontend/public/locales/pt-BR/dashboard.json b/frontend/public/locales/pt-BR/dashboard.json new file mode 100644 index 0000000000..3edeacc891 --- /dev/null +++ b/frontend/public/locales/pt-BR/dashboard.json @@ -0,0 +1,30 @@ +{ + "title": "Segredos", + "og-title": "Gerencie seus arquivos .env em segundos", + "og-description": "Infisical é uma plataforma simples e criptografada de ponta a ponta que permite que as equipes sincronizem e gerenciem seus arquivos .env.", + "search-keys": "Pesquisar chaves...", + "add-key": "Adicionar Chave", + "personal": "Pessoal", + "personal-description": "As chaves pessoais são visíveis apenas para você", + "shared": "Compartilhado", + "shared-description": "As chaves compartilhadas ficam visíveis para toda a sua equipe", + "make-shared": "Tornar Compartilhado", + "make-personal": "Tornar individual", + "check-docs": { + "button": "Checkar Documentação", + "title": "Bom trabalho!!", + "line1": "Parabéns por adicionar mais segredos.", + "line2": "Veja como conectá-los à sua base de código." + }, + "sidebar": { + "secret": "Segredo", + "key": "Chave", + "value": "Valor", + "override": "Substitua o valor por um valor pessoal", + "version-history": "Histórico da versão", + "comments": "Comentários e Notas", + "personal-explanation": "Este segredo é pessoal. Não é compartilhado com nenhum de seus colegas de equipe.", + "generate-random-hex": "Gerar Hex Aleatório", + "digits": "Digitos" + } +} \ No newline at end of file diff --git a/frontend/public/locales/pt-BR/integrations.json b/frontend/public/locales/pt-BR/integrations.json new file mode 100644 index 0000000000..3e1533582f --- /dev/null +++ b/frontend/public/locales/pt-BR/integrations.json @@ -0,0 +1,16 @@ +{ + "title": "Integrações de Projetos", + "description": "Gerencie suas integrações da Infisical com serviços de terceiros.", + "no-integrations1": "Você ainda não tem integrações configuradas. Quando você fizer isso, eles aparecerão aqui.", + "no-integrations2": "Para começar, clique em qualquer uma das opções abaixo. Leva 5 cliques para configurar.", + "available": "Integrações de Plataforma e Nuvem", + "available-text1": "Clique na integração que deseja conectar. Isso permitirá que suas variáveis de ambiente fluam automaticamente para serviços de terceiros selecionados.", + "available-text2": "Observação: durante uma integração com o Heroku, por questões de segurança, é impossível manter a criptografia de ponta a ponta. Em teoria, isso permite que o Infisical descriptografe suas variáveis de ambiente. Na prática, podemos garantir que isso nunca será feito e nos permite proteger seus segredos de pessoas mal-intencionadas online. O serviço básico da Infisical sempre permanecerá criptografado de ponta a ponta. Em caso de dúvidas, entre em contato com support@infisical.com.", + "cloud-integrations": "Integrações na Nuvem", + "framework-integrations": "Integrações de framework", + "click-to-start": "Clique em uma integração para começar a sincronizar segredos com ela.", + "click-to-setup": "Clique em uma estrutura para obter as instruções de configuração.", + "grant-access-to-secrets": "Conceda acesso Infisical aos seus segredos", + "why-infisical-needs-access": "A maioria das integrações em nuvem exige que o Infisical seja capaz de descriptografar seus segredos para que possam ser encaminhados.", + "grant-access-button": "Garantir acesso" +} \ No newline at end of file diff --git a/frontend/public/locales/pt-BR/login.json b/frontend/public/locales/pt-BR/login.json new file mode 100644 index 0000000000..9510d1a22d --- /dev/null +++ b/frontend/public/locales/pt-BR/login.json @@ -0,0 +1,8 @@ +{ + "title": "Entrar", + "og-title": "Entrar no Infisical", + "og-description": "Infisical é uma plataforma simples e criptografada de ponta a ponta que permite que as equipes sincronizem e gerenciem seus arquivos .env.", + "login": "Entrar", + "need-account": "Precisa de uma conta Infisical?", + "create-account": "Criar uma conta" +} \ No newline at end of file diff --git a/frontend/public/locales/pt-BR/nav.json b/frontend/public/locales/pt-BR/nav.json new file mode 100644 index 0000000000..7922312dde --- /dev/null +++ b/frontend/public/locales/pt-BR/nav.json @@ -0,0 +1,22 @@ +{ + "support": { + "slack": "[NEW] Participe do fórum do Slack", + "docs": "Leia a Documentação", + "issue": "Abra uma Issue no Github", + "email": "Envie-nos um e-mail" + }, + "user": { + "signed-in-as": "ASSINADO COMO", + "current-organization": "ORGANIZAÇÃO ATUAL", + "usage-billing": "Uso & Faturamento", + "invite": "Convide Membros", + "other-organizations": "OUTRA ORGANIZAÇÃO" + }, + "menu": { + "project": "PROJETO", + "secrets": "Segredos", + "members": "Membros", + "integrations": "Integrações", + "project-settings": "Configurações do Projeto" + } +} \ No newline at end of file diff --git a/frontend/public/locales/pt-BR/section-incident.json b/frontend/public/locales/pt-BR/section-incident.json new file mode 100644 index 0000000000..bfa55d2ee0 --- /dev/null +++ b/frontend/public/locales/pt-BR/section-incident.json @@ -0,0 +1,11 @@ +{ + "incident-contacts": "Contatos do Incidente", + "incident-contacts-description": "Esses contatos serão notificados no caso improvável de um incidente grave.", + "no-incident-contacts": "Nenhum contato de incidente encontrado.", + "add-contact": "Adicionar contato", + "add-dialog": { + "title": "Adicionar um contato de incidente", + "description": "Este contato será notificado no caso improvável de um incidente grave.", + "add-incident": "Adicionar contato de incidente" + } +} \ No newline at end of file diff --git a/frontend/public/locales/pt-BR/section-members.json b/frontend/public/locales/pt-BR/section-members.json new file mode 100644 index 0000000000..67ef10ff0b --- /dev/null +++ b/frontend/public/locales/pt-BR/section-members.json @@ -0,0 +1,14 @@ +{ + "add-member": "Adicionar membro", + "org-members": "Membros da Organização", + "org-members-description": "Gerencie os membros da sua organização. Esses usuários poderiam posteriormente ser formados em projetos.", + "search-members": "Pesquisar membros...", + "add-dialog": { + "add-member-to-project": "Adicionar um membro ao seu projeto", + "already-all-invited": "Todos os usuários da sua organização já foram convidados.", + "add-user-org-first": "Adicione mais usuários à organização primeiro.", + "user-will-email": "O usuário receberá um e-mail com as instruções.", + "looking-add": "<0>Se você deseja adicionar usuários à sua organização,<1>clique aqui", + "add-user-to-org": "Adicionar usuários à organização" + } +} \ No newline at end of file diff --git a/frontend/public/locales/pt-BR/section-password.json b/frontend/public/locales/pt-BR/section-password.json new file mode 100644 index 0000000000..7715817682 --- /dev/null +++ b/frontend/public/locales/pt-BR/section-password.json @@ -0,0 +1,11 @@ +{ + "password": "Senha", + "change": "Mudar senha", + "current": "Senha atual", + "current-wrong": "A senha atual pode estar errada", + "new": "Nova Senha", + "validate-base": "A senha deve conter pelo menos:", + "validate-length": "14 caracteres", + "validate-case": "1 caractere minúsculo", + "validate-number": "1 número" +} \ No newline at end of file diff --git a/frontend/public/locales/pt-BR/section-token.json b/frontend/public/locales/pt-BR/section-token.json new file mode 100644 index 0000000000..1713f8118d --- /dev/null +++ b/frontend/public/locales/pt-BR/section-token.json @@ -0,0 +1,13 @@ +{ + "service-tokens": "Tokens de Serviço", + "service-tokens-description": "Cada token de serviço é específico para você, um determinado projeto e um determinado ambiente dentro deste projeto.", + "add-new": "Adicionar novo token", + "add-dialog": { + "title": "Adicione um token de serviço para {{target}}", + "description": "Especifique o nome, o ambiente e o período de expiração. Quando um token é gerado, você só poderá vê-lo uma vez antes que ele desapareça. Certifique-se de salvá-lo em algum lugar.", + "name": "Nome do token de serviço", + "add": "Adicionar token de serviço", + "copy-service-token": "Copie seu token de serviço", + "copy-service-token-description": "Depois de fechar este pop-up, você nunca mais verá seu token de serviço" + } +} \ No newline at end of file diff --git a/frontend/public/locales/pt-BR/settings-members.json b/frontend/public/locales/pt-BR/settings-members.json new file mode 100644 index 0000000000..f48913547f --- /dev/null +++ b/frontend/public/locales/pt-BR/settings-members.json @@ -0,0 +1,4 @@ +{ + "title": "Membros do Projeto", + "description": "Esta página mostra os membros do projeto selecionado." +} \ No newline at end of file diff --git a/frontend/public/locales/pt-BR/settings-org.json b/frontend/public/locales/pt-BR/settings-org.json new file mode 100644 index 0000000000..e017c2aee3 --- /dev/null +++ b/frontend/public/locales/pt-BR/settings-org.json @@ -0,0 +1,4 @@ +{ + "title": "Configurações da organização", + "description": "Gerencie os membros da sua organização. Esses usuários poderiam posteriormente ser formados em projetos." +} \ No newline at end of file diff --git a/frontend/public/locales/pt-BR/settings-personal.json b/frontend/public/locales/pt-BR/settings-personal.json new file mode 100644 index 0000000000..9d5dd7f50f --- /dev/null +++ b/frontend/public/locales/pt-BR/settings-personal.json @@ -0,0 +1,11 @@ +{ + "title": "Configurações Pessoais", + "description": "Visualize e gerencie suas informações pessoais aqui.", + "emergency": { + "name": "Kit de emergência", + "text1": "Seu Kit de Emergência contém as informações necessárias para acessar sua conta Infisical.", + "text2": "Apenas o último kit de emergência emitido permanece válido. Para obter um novo Kit de emergência, verifique sua senha.", + "download": "Baixe o kit de emergência" + }, + "change-language": "Mudar idioma" +} \ No newline at end of file diff --git a/frontend/public/locales/pt-BR/settings-project.json b/frontend/public/locales/pt-BR/settings-project.json new file mode 100644 index 0000000000..f25af8fd43 --- /dev/null +++ b/frontend/public/locales/pt-BR/settings-project.json @@ -0,0 +1,13 @@ +{ + "title": "Configurações do Projeto", + "description": "Essas configurações se aplicam apenas ao projeto atualmente selecionado.", + "danger-zone": "Zona de perigo", + "delete-project": "Excluir projeto", + "project-to-delete": "Projeto a ser deletado", + "danger-zone-note": "Assim que você excluir este projeto, não poderá desfazê-lo. Isso removerá imediatamente todas as chaves. Se você ainda quiser fazer isso, digite o nome do projeto abaixo.", + "delete-project-note": "Observação: você só pode excluir um projeto caso tenha mais de um", + "project-id-description": "Para integrar Infisical em sua base de código e obter injeção automática de variáveis ambientais, você deve usar o seguinte ID do projeto.", + "project-id-description2": "Para obter mais orientações, incluindo trechos de código para várias linguagens e estruturas, consulte", + "auto-generated": "Este é o identificador exclusivo gerado automaticamente do seu projeto. Não pode ser alterado.", + "docs": "Documentação do Infisical" +} \ No newline at end of file diff --git a/frontend/public/locales/pt-BR/signup.json b/frontend/public/locales/pt-BR/signup.json new file mode 100644 index 0000000000..b86ac1eff1 --- /dev/null +++ b/frontend/public/locales/pt-BR/signup.json @@ -0,0 +1,21 @@ +{ + "title": "Inscrever-se", + "og-title": "Substitua os arquivos .env por 1 linha de código. Cadastre-se no Infisical em 3 minutos.", + "og-description": "Infisical é uma plataforma criptografada de ponta a ponta simples que permite que as equipes sincronizem e gerenciem chaves de API e variáveis ambientais. Funciona com Node.js, Next.js, Gatsby, Nest.js...", + "signup": "Inscrever-se", + "already-have-account": "Possui uma conta? Conecte-se", + "forgot-password": "Esqueceu sua senha?", + "verify": "Verificar", + "step1-start": "Vamos começar", + "step1-privacy": "Ao criar uma conta, você concorda com nossos Termos e leu e reconheceu a Política de Privacidade.", + "step1-submit": "Iniciar", + "step2-message": "Enviamos um e-mail de verificação para{{email}}", + "step2-code-error": "Ops. Seu código está errado. Por favor, tente novamente.", + "step2-spam-alert": "Certifique-se de verificar sua caixa de entrada de spam.", + "step3-message": "Quase lá!", + "step4-message": "Guarde o seu Kit de Emergência", + "step4-description1": "Se sua conta for bloqueada, seu Kit de emergência é a única maneira de fazer login.", + "step4-description2": "Recomendamos que você faça o download e guarde-o em algum lugar seguro.", + "step4-description3": "Ele contém sua chave secreta que não podemos acessar ou recuperar para você se você a perder.", + "step4-download": "Baixar PDF" +} \ No newline at end of file diff --git a/frontend/tsconfig.json b/frontend/tsconfig.json index e936a69a2c..f51378ec30 100644 --- a/frontend/tsconfig.json +++ b/frontend/tsconfig.json @@ -3,6 +3,7 @@ "baseUrl": ".", "paths": { "~/components/*": ["components/*"], + "~/hooks/*": ["hooks/*"], "~/utilities/*": ["components/utilities/*"], "~/*": ["const"], "~/pages/*": ["pages/*"]