Complete v1 API reference docs, pre-launch

This commit is contained in:
Tuan Dang
2023-01-14 19:06:43 +07:00
115 changed files with 3290 additions and 1668 deletions

36
.github/values.yaml vendored Normal file
View File

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

View File

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

View File

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

View File

@@ -3,7 +3,7 @@
<img width="300" src="/img/logoname-white.svg#gh-dark-mode-only" alt="infisical">
</h1>
<p align="center">
<p align="center">Open-source, E2EE, simple tool to manage and sync environment variables across your team and infrastructure.</p>
<p align="center">Open-source, E2EE, simple tool to manage secrets and configs across your team and infrastructure.</p>
</p>
<h4 align="center">
@@ -34,13 +34,13 @@
<img src="/img/infisical_github_repo.png" width="100%" alt="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
<!-- prettier-ignore-start -->
<!-- markdownlint-disable -->
<a href="https://github.com/dangtony98"><img src="https://avatars.githubusercontent.com/u/25857006?v=4" width="50" height="50" alt=""/></a> <a href="https://github.com/mv-turtle"><img src="https://avatars.githubusercontent.com/u/78047717?s=96&v=4" width="50" height="50" alt=""/></a> <a href="https://github.com/maidul98"><img src="https://avatars.githubusercontent.com/u/9300960?v=4" width="50" height="50" alt=""/></a> <a href="https://github.com/gangjun06"><img src="https://avatars.githubusercontent.com/u/50910815?v=4" width="50" height="50" alt=""/></a> <a href="https://github.com/reginaldbondoc"><img src="https://avatars.githubusercontent.com/u/7693108?v=4" width="50" height="50" alt=""/></a> <a href="https://github.com/SH5H"><img src="https://avatars.githubusercontent.com/u/25437192?v=4" width="50" height="50" alt=""/></a> <a href="https://github.com/gmgale"><img src="https://avatars.githubusercontent.com/u/62303146?v=4" width="50" height="50" alt=""/></a> <a href="https://github.com/asharonbaltazar"><img src="https://avatars.githubusercontent.com/u/58940073?v=4" width="50" height="50" alt=""/></a> <a href="https://github.com/JoaoVictor6"><img src="https://avatars.githubusercontent.com/u/68869379?v=4" width="50" height="50" alt=""/></a> <a href="https://github.com/mocherfaoui"><img src="https://avatars.githubusercontent.com/u/37941426?v=4" width="50" height="50" alt=""/></a> <a href="https://github.com/jon4hz"><img src="https://avatars.githubusercontent.com/u/26183582?v=4" width="50" height="50" alt=""/></a> <a href="https://github.com/edgarrmondragon"><img src="https://avatars.githubusercontent.com/u/16805946?v=4" width="50" height="50" alt=""/></a> <a href="https://github.com/arjunyel"><img src="https://avatars.githubusercontent.com/u/11153289?v=4" width="50" height="50" alt=""/></a> <a href="https://github.com/LemmyMwaura"><img src="https://avatars.githubusercontent.com/u/20738858?v=4" width="50" height="50" alt=""/></a> <a href="https://github.com/Zamion101"><img src="https://avatars.githubusercontent.com/u/8071263?v=4" width="50" height="50" alt=""/></a> <a href="https://github.com/akhilmhdh"><img src="https://avatars.githubusercontent.com/u/31166322?v=4" width="50" height="50" alt=""/></a> <a href="https://github.com/naorpeled"><img src="https://avatars.githubusercontent.com/u/6171622?v=4" width="50" height="50" alt=""/></a> <a href="https://github.com/jonerrr"><img src="https://avatars.githubusercontent.com/u/73760377?v=4" width="50" height="50" alt=""/></a> <a href="https://github.com/adrianmarinwork"><img src="https://avatars.githubusercontent.com/u/118568289?v=4" width="50" height="50" alt=""/></a> <a href="https://github.com/arthurzenika"><img src="https://avatars.githubusercontent.com/u/445200?v=4" width="50" height="50" alt=""/></a> <a href="https://github.com/hanywang2"><img src="https://avatars.githubusercontent.com/u/44352119?v=4" width="50" height="50" alt=""/></a> <a href="https://github.com/tobias-mintlify"><img src="https://avatars.githubusercontent.com/u/110702161?v=4" width="50" height="50" alt=""/></a> <a href="https://github.com/wjhurley"><img src="https://avatars.githubusercontent.com/u/15939055?v=4" width="50" height="50" alt=""/></a> <a href="https://github.com/0xflotus"><img src="https://avatars.githubusercontent.com/u/26602940?v=4" width="50" height="50" alt=""/></a> <a href="https://github.com/wanjohiryan"><img src="https://avatars.githubusercontent.com/u/71614375?v=4" width="50" height="50" alt=""/></a>
<a href="https://github.com/dangtony98"><img src="https://avatars.githubusercontent.com/u/25857006?v=4" width="50" height="50" alt=""/></a> <a href="https://github.com/mv-turtle"><img src="https://avatars.githubusercontent.com/u/78047717?s=96&v=4" width="50" height="50" alt=""/></a> <a href="https://github.com/maidul98"><img src="https://avatars.githubusercontent.com/u/9300960?v=4" width="50" height="50" alt=""/></a> <a href="https://github.com/gangjun06"><img src="https://avatars.githubusercontent.com/u/50910815?v=4" width="50" height="50" alt=""/></a> <a href="https://github.com/reginaldbondoc"><img src="https://avatars.githubusercontent.com/u/7693108?v=4" width="50" height="50" alt=""/></a> <a href="https://github.com/SH5H"><img src="https://avatars.githubusercontent.com/u/25437192?v=4" width="50" height="50" alt=""/></a> <a href="https://github.com/gmgale"><img src="https://avatars.githubusercontent.com/u/62303146?v=4" width="50" height="50" alt=""/></a> <a href="https://github.com/asharonbaltazar"><img src="https://avatars.githubusercontent.com/u/58940073?v=4" width="50" height="50" alt=""/></a> <a href="https://github.com/JoaoVictor6"><img src="https://avatars.githubusercontent.com/u/68869379?v=4" width="50" height="50" alt=""/></a> <a href="https://github.com/mocherfaoui"><img src="https://avatars.githubusercontent.com/u/37941426?v=4" width="50" height="50" alt=""/></a> <a href="https://github.com/jon4hz"><img src="https://avatars.githubusercontent.com/u/26183582?v=4" width="50" height="50" alt=""/></a> <a href="https://github.com/edgarrmondragon"><img src="https://avatars.githubusercontent.com/u/16805946?v=4" width="50" height="50" alt=""/></a> <a href="https://github.com/arjunyel"><img src="https://avatars.githubusercontent.com/u/11153289?v=4" width="50" height="50" alt=""/></a> <a href="https://github.com/LemmyMwaura"><img src="https://avatars.githubusercontent.com/u/20738858?v=4" width="50" height="50" alt=""/></a> <a href="https://github.com/Zamion101"><img src="https://avatars.githubusercontent.com/u/8071263?v=4" width="50" height="50" alt=""/></a> <a href="https://github.com/Grraahaam"><img src="https://avatars.githubusercontent.com/u/72856427?v=4" width="50" height="50" alt=""/></a> <a href="https://github.com/Gabriellopes232"><img src="https://avatars.githubusercontent.com/u/74881862?v=4" width="50" height="50" alt=""/></a> <a href="https://github.com/akhilmhdh"><img src="https://avatars.githubusercontent.com/u/31166322?v=4" width="50" height="50" alt=""/></a> <a href="https://github.com/naorpeled"><img src="https://avatars.githubusercontent.com/u/6171622?v=4" width="50" height="50" alt=""/></a> <a href="https://github.com/jonerrr"><img src="https://avatars.githubusercontent.com/u/73760377?v=4" width="50" height="50" alt=""/></a> <a href="https://github.com/cerrussell"><img src="https://avatars.githubusercontent.com/u/80227828?v=4" width="50" height="50" alt=""/></a> <a href="https://github.com/imakecodes"><img src="https://avatars.githubusercontent.com/u/35536648?v=4" width="50" height="50" alt=""/></a> <a href="https://github.com/adrianmarinwork"><img src="https://avatars.githubusercontent.com/u/118568289?v=4" width="50" height="50" alt=""/></a> <a href="https://github.com/arthurzenika"><img src="https://avatars.githubusercontent.com/u/445200?v=4" width="50" height="50" alt=""/></a> <a href="https://github.com/hanywang2"><img src="https://avatars.githubusercontent.com/u/44352119?v=4" width="50" height="50" alt=""/></a> <a href="https://github.com/tobias-mintlify"><img src="https://avatars.githubusercontent.com/u/110702161?v=4" width="50" height="50" alt=""/></a> <a href="https://github.com/wjhurley"><img src="https://avatars.githubusercontent.com/u/15939055?v=4" width="50" height="50" alt=""/></a> <a href="https://github.com/0xflotus"><img src="https://avatars.githubusercontent.com/u/26602940?v=4" width="50" height="50" alt=""/></a> <a href="https://github.com/wanjohiryan"><img src="https://avatars.githubusercontent.com/u/71614375?v=4" width="50" height="50" alt=""/></a>
## 🌎 Translations

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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<ISecretVersion>(
{
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<ISecretVersion>(
},
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<ISecretVersion>(
secretValueHash: {
type: String
}
},
{
timestamps: true
}
},
{
timestamps: true
}
);
const SecretVersion = model<ISecretVersion>('SecretVersion', secretVersionSchema);

View File

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

View File

@@ -39,7 +39,7 @@ const validateSecrets = async ({
try {
secrets = await Secret.find({
_id: {
$in: secretIds
$in: secretIds.map((secretId: string) => new Types.ObjectId(secretId))
}
});

View File

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

View File

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

View File

@@ -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}`,
{
}
)).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],
});
}
});
// 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,
{
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}`,
{
params,
headers: {
Authorization: `Bearer ${accessToken}`
}
}
)).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],
});
}
});
// 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 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}`
}
}
);
});
}
// 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}`
}
}
);
});
}
}
);
});
}
} 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
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
}]
});
}
}
});
}
});
// 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');
}
}

View File

@@ -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<IIntegration>(
},
environment: {
type: String,
enum: [ENV_DEV, ENV_TESTING, ENV_STAGING, ENV_PROD],
required: true
},
isActive: {

View File

@@ -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<ISecret>(
},
environment: {
type: String,
enum: [ENV_DEV, ENV_TESTING, ENV_STAGING, ENV_PROD],
required: true
},
secretKeyCiphertext: {

View File

@@ -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<IServiceToken>(
},
environment: {
type: String,
enum: [ENV_DEV, ENV_TESTING, ENV_STAGING, ENV_PROD],
required: true
},
expiresAt: {

View File

@@ -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<IWorkspace>({
@@ -15,7 +19,33 @@ const workspaceSchema = new Schema<IWorkspace>({
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<IWorkspace>('Workspace', workspaceSchema);

View File

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

View File

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

View File

@@ -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()

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 370 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 172 KiB

View File

@@ -48,7 +48,7 @@ SMTP_FROM_NAME=Infisical
```
<Info>
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.
</Info>
## 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
```
<Info>
Remember that you will need to restart Infisical for this to work properly.
</Info>
Remember that you will need to restart Infisical for this to work properly.
</Info>

View File

@@ -68,18 +68,5 @@ export default function RouteGuard({ children }) {
}
}
if (authorized) {
return children;
} else {
return (
<div className="w-screen h-screen bg-bunker-800 flex items-center justify-center">
<Image
src="/images/loading/loading.gif"
height={70}
width={120}
alt="google logo"
></Image>
</div>
);
}
return children;
}

View File

@@ -46,7 +46,7 @@ export default function ListBox({
>
<div className="flex flex-row">
{text}
<span className="ml-1 cursor-pointer block truncate font-semibold text-gray-300">
<span className="ml-1 cursor-pointer block truncate font-semibold text-gray-300 capitalize">
{' '}
{selected}
</span>
@@ -69,7 +69,7 @@ export default function ListBox({
<Listbox.Option
key={personIdx}
className={({ active, selected }) =>
`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

View File

@@ -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 (
<Switch
checked={enabled}
onChange={() => {
if (enabled == false) {
addOverride({ id, keyName, value, pos, comment });
setSharedToHide([
...sharedToHide!,
id
])
addOverride('', pos);
} else {
deleteOverride(id);
addOverride(undefined, pos);
}
setEnabled(!enabled);
}}

View File

@@ -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 {
<div
className={`${
props.loading == true ? "opacity-100" : "opacity-0"
} absolute flex items-center px-2 duration-200`}
} absolute flex items-center px-3 bg-primary duration-200 w-full`}
>
<Image
src="/images/loading/loadingblack.gif"

View File

@@ -8,7 +8,6 @@ import nacl from "tweetnacl";
import addServiceToken from "~/pages/api/serviceToken/addServiceToken";
import getLatestFileKey from "~/pages/api/workspace/getLatestFileKey";
import { envMapping } from "../../../public/data/frequentConstants";
import {
decryptAssymmetric,
encryptAssymmetric,
@@ -34,11 +33,12 @@ const AddServiceTokenDialog = ({
workspaceId,
workspaceName,
serviceTokens,
environments,
setServiceTokens
}) => {
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 (
<div className="z-50">
<div className='z-50'>
<Transition appear show={isOpen} as={Fragment}>
<Dialog as="div" className="relative" onClose={closeModal}>
<Dialog as='div' className='relative' onClose={closeModal}>
<Transition.Child
as={Fragment}
enter="ease-out duration-300"
enterFrom="opacity-0"
enterTo="opacity-100"
leave="ease-in duration-200"
leaveFrom="opacity-100"
leaveTo="opacity-0"
enter='ease-out duration-300'
enterFrom='opacity-0'
enterTo='opacity-100'
leave='ease-in duration-200'
leaveFrom='opacity-100'
leaveTo='opacity-0'
>
<div className="fixed inset-0 bg-bunker-700 bg-opacity-80" />
<div className='fixed inset-0 bg-bunker-700 bg-opacity-80' />
</Transition.Child>
<div className="fixed inset-0 overflow-y-auto">
<div className="flex min-h-full items-center justify-center p-4 text-center">
<div className='fixed inset-0 overflow-y-auto'>
<div className='flex min-h-full items-center justify-center p-4 text-center'>
<Transition.Child
as={Fragment}
enter="ease-out duration-300"
enterFrom="opacity-0 scale-95"
enterTo="opacity-100 scale-100"
leave="ease-in duration-200"
leaveFrom="opacity-100 scale-100"
leaveTo="opacity-0 scale-95"
enter='ease-out duration-300'
enterFrom='opacity-0 scale-95'
enterTo='opacity-100 scale-100'
leave='ease-in duration-200'
leaveFrom='opacity-100 scale-100'
leaveTo='opacity-0 scale-95'
>
{serviceToken == "" ? (
<Dialog.Panel className="w-full max-w-md transform rounded-md bg-bunker-800 border border-gray-700 p-6 text-left align-middle shadow-xl transition-all">
{serviceToken == '' ? (
<Dialog.Panel className='w-full max-w-md transform rounded-md bg-bunker-800 border border-gray-700 p-6 text-left align-middle shadow-xl transition-all'>
<Dialog.Title
as="h3"
className="text-lg font-medium leading-6 text-gray-400 z-50"
as='h3'
className='text-lg font-medium leading-6 text-gray-400 z-50'
>
{t("section-token:add-dialog.title", {
{t('section-token:add-dialog.title', {
target: workspaceName,
})}
</Dialog.Title>
<div className="mt-2 mb-4">
<div className="flex flex-col">
<p className="text-sm text-gray-500">
{t("section-token:add-dialog.description")}
<div className='mt-2 mb-4'>
<div className='flex flex-col'>
<p className='text-sm text-gray-500'>
{t('section-token:add-dialog.description')}
</p>
</div>
</div>
<div className="max-h-28 mb-2">
<div className='max-h-28 mb-2'>
<InputField
label={t("section-token:add-dialog.name")}
label={t('section-token:add-dialog.name')}
onChangeHandler={setServiceTokenName}
type="varName"
type='varName'
value={serviceTokenName}
placeholder=""
placeholder=''
isRequired
/>
</div>
<div className="max-h-28 mb-2">
<div className='max-h-28 mb-2'>
<ListBox
selected={serviceTokenEnv}
onChange={setServiceTokenEnv}
data={[
"Development",
"Staging",
"Production",
"Testing",
]}
selected={selectedServiceTokenEnv?.name ? selectedServiceTokenEnv?.name : environments[0]?.name}
data={environments.map(({ name }) => name)}
onChange={(envName) =>
setSelectedServiceTokenEnv(
environments.find(
({ name }) => envName === name
) || {
name: 'unknown',
slug: 'unknown',
}
)
}
isFull={true}
text={`${t("common:environment")}: `}
text={`${t('common:environment')}: `}
/>
</div>
<div className="max-h-28">
<div className='max-h-28'>
<ListBox
selected={serviceTokenExpiresIn}
onChange={setServiceTokenExpiresIn}
data={[
"1 day",
"7 days",
"1 month",
"6 months",
"12 months",
'1 day',
'7 days',
'1 month',
'6 months',
'12 months',
]}
isFull={true}
text={`${t("common:expired-in")}: `}
text={`${t('common:expired-in')}: `}
/>
</div>
<div className="max-w-max">
<div className="mt-6 flex flex-col justify-start w-max">
<div className='max-w-max'>
<div className='mt-6 flex flex-col justify-start w-max'>
<Button
onButtonPressed={() => generateServiceToken()}
color="mineshaft"
text={t("section-token:add-dialog.add")}
textDisabled={t("section-token:add-dialog.add")}
size="md"
active={serviceTokenName == "" ? false : true}
color='mineshaft'
text={t('section-token:add-dialog.add')}
textDisabled={t('section-token:add-dialog.add')}
size='md'
active={serviceTokenName == '' ? false : true}
/>
</div>
</div>
</Dialog.Panel>
) : (
<Dialog.Panel className="w-full max-w-md transform rounded-md bg-bunker-800 border border-gray-700 p-6 text-left align-middle shadow-xl transition-all">
<Dialog.Panel className='w-full max-w-md transform rounded-md bg-bunker-800 border border-gray-700 p-6 text-left align-middle shadow-xl transition-all'>
<Dialog.Title
as="h3"
className="text-lg font-medium leading-6 text-gray-400 z-50"
as='h3'
className='text-lg font-medium leading-6 text-gray-400 z-50'
>
{t("section-token:add-dialog.copy-service-token")}
{t('section-token:add-dialog.copy-service-token')}
</Dialog.Title>
<div className="mt-2 mb-4">
<div className="flex flex-col">
<p className="text-sm text-gray-500">
<div className='mt-2 mb-4'>
<div className='flex flex-col'>
<p className='text-sm text-gray-500'>
{t(
"section-token:add-dialog.copy-service-token-description"
'section-token:add-dialog.copy-service-token-description'
)}
</p>
</div>
</div>
<div className="w-full">
<div className="flex justify-end items-center bg-white/[0.07] text-base mt-2 mr-2 rounded-md text-gray-400 w-full h-20">
<div className='w-full'>
<div className='flex justify-end items-center bg-white/[0.07] text-base mt-2 mr-2 rounded-md text-gray-400 w-full h-20'>
<input
type="text"
type='text'
value={serviceToken}
id="serviceToken"
className="invisible bg-white/0 text-gray-400 py-2 w-full px-2 min-w-full outline-none"
id='serviceToken'
className='invisible bg-white/0 text-gray-400 py-2 w-full px-2 min-w-full outline-none'
></input>
<div className="bg-white/0 max-w-md text-sm text-gray-400 py-2 w-full pl-14 pr-2 break-words outline-none">
<div className='bg-white/0 max-w-md text-sm text-gray-400 py-2 w-full pl-14 pr-2 break-words outline-none'>
{serviceToken}
</div>
<div className="group font-normal h-full relative inline-block text-gray-400 underline hover:text-primary duration-200">
<div className='group font-normal h-full relative inline-block text-gray-400 underline hover:text-primary duration-200'>
<button
onClick={copyToClipboard}
className="h-full pl-3.5 pr-4 border-l border-white/20 py-2 hover:bg-white/[0.12] duration-200"
className='h-full pl-3.5 pr-4 border-l border-white/20 py-2 hover:bg-white/[0.12] duration-200'
>
{serviceTokenCopied ? (
<FontAwesomeIcon
icon={faCheck}
className="pr-0.5"
className='pr-0.5'
/>
) : (
<FontAwesomeIcon icon={faCopy} />
)}
</button>
<span className="absolute hidden group-hover:flex group-hover:animate-popup duration-300 w-28 -left-8 -top-20 translate-y-full px-3 py-2 bg-chicago-900 rounded-md text-center text-gray-400 text-sm">
{t("common:click-to-copy")}
<span className='absolute hidden group-hover:flex group-hover:animate-popup duration-300 w-28 -left-8 -top-20 translate-y-full px-3 py-2 bg-chicago-900 rounded-md text-center text-gray-400 text-sm'>
{t('common:click-to-copy')}
</span>
</div>
</div>
</div>
<div className="mt-6 flex flex-col justify-start w-max">
<div className='mt-6 flex flex-col justify-start w-max'>
<Button
onButtonPressed={() => closeAddServiceTokenModal()}
color="mineshaft"
text="Close"
size="md"
color='mineshaft'
text='Close'
size='md'
/>
</div>
</Dialog.Panel>

View File

@@ -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<FormFields>({
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 (
<div>
<Transition appear show={isOpen} as={Fragment}>
<Dialog as='div' className='relative z-20' onClose={onClose}>
<Transition.Child
as={Fragment}
enter='ease-out duration-300'
enterFrom='opacity-0'
enterTo='opacity-100'
leave='ease-out duration-150'
leaveFrom='opacity-100'
leaveTo='opacity-0'
>
<div className='fixed inset-0 bg-black bg-opacity-70' />
</Transition.Child>
<div className='fixed inset-0 overflow-y-auto z-50'>
<div className='flex min-h-full items-center justify-center p-4 text-center'>
<Transition.Child
as={Fragment}
enter='ease-out duration-300'
enterFrom='opacity-0 scale-95'
enterTo='opacity-100 scale-100'
leave='ease-in duration-200'
leaveFrom='opacity-100 scale-100'
leaveTo='opacity-0 scale-95'
>
<Dialog.Panel className='w-full max-w-md transform overflow-hidden rounded-2xl bg-bunker-800 border border-gray-700 p-6 text-left align-middle shadow-xl transition-all'>
<Dialog.Title
as='h3'
className='text-lg font-medium leading-6 text-gray-400'
>
{isEditMode
? 'Update environment'
: 'Create a new environment'}
</Dialog.Title>
<form onSubmit={onFormSubmit}>
<div className='max-h-28 mt-4'>
<InputField
label='Environment Name'
onChangeHandler={(val) => onInputChange('name', val)}
type='varName'
value={formInput.name}
placeholder=''
isRequired
// error={error.length > 0}
// errorText={error}
/>
</div>
<div className='max-h-28 mt-4'>
<InputField
label='Environment Slug'
onChangeHandler={(val) => onInputChange('slug', val)}
type='varName'
value={formInput.slug}
placeholder=''
isRequired
// error={error.length > 0}
// errorText={error}
/>
</div>
<p className='text-xs text-gray-500 mt-2'>
Slugs are shorthands used in cli to access environment
</p>
<div className='mt-4 max-w-min'>
<Button
onButtonPressed={() => null}
type='submit'
color='mineshaft'
text={isEditMode ? 'Update' : 'Create'}
active={formInput.name !== '' && formInput.slug !== ''}
size='md'
/>
</div>
</form>
</Dialog.Panel>
</Transition.Child>
</div>
</div>
</Dialog>
</Transition>
</div>
);
};

View File

@@ -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 (
<div>
<Transition appear show={isOpen} as={Fragment}>
<Dialog as='div' className='relative z-10' onClose={onClose}>
<Transition.Child
as={Fragment}
enter='ease-out duration-300'
enterFrom='opacity-0'
enterTo='opacity-100'
leave='ease-in duration-150'
leaveFrom='opacity-100'
leaveTo='opacity-0'
>
<div className='fixed inset-0 bg-black bg-opacity-25' />
</Transition.Child>
<div className='fixed inset-0 overflow-y-auto'>
<div className='flex min-h-full items-center justify-center p-4 text-center'>
<Transition.Child
as={Fragment}
enter='ease-out duration-300'
enterFrom='opacity-0 scale-95'
enterTo='opacity-100 scale-100'
leave='ease-in duration-200'
leaveFrom='opacity-100 scale-100'
leaveTo='opacity-0 scale-95'
>
<Dialog.Panel className='w-full max-w-md transform overflow-hidden rounded-2xl bg-grey border border-gray-700 p-6 text-left align-middle shadow-xl transition-all'>
<Dialog.Title
as='h3'
className='text-lg font-medium leading-6 text-gray-400'
>
{title}
</Dialog.Title>
<div className='mt-2'>
<p className='text-sm text-gray-500'>
This action is irrevertible.
</p>
</div>
<div className='mt-2'>
<InputField
isRequired
label={`Type ${deleteKey} to delete the resource`}
onChangeHandler={(val) => setDeleteInputField(val)}
value={deleteInputField}
type='text'
/>
</div>
<div className='mt-6'>
<button
type='button'
className='inline-flex justify-center rounded-md border border-transparent bg-gray-800 px-4 py-2 text-sm font-medium text-gray-400 hover:bg-alizarin hover:text-white hover:text-semibold duration-200 focus:outline-none focus-visible:ring-2 focus-visible:ring-blue-500 focus-visible:ring-offset-2'
onClick={onSubmit}
disabled={
Boolean(deleteKey) && deleteInputField !== deleteKey
}
>
Delete
</button>
<button
type='button'
className='ml-2 inline-flex justify-center rounded-md border border-transparent bg-gray-800 px-4 py-2 text-sm font-medium text-gray-400 hover:border-white hover:text-white hover:text-semibold duration-200 focus:outline-none focus-visible:ring-2 focus-visible:ring-blue-500 focus-visible:ring-offset-2'
onClick={onClose}
>
Cancel
</button>
</div>
</Dialog.Panel>
</Transition.Child>
</div>
</div>
</Dialog>
</Transition>
</div>
);
};
export default DeleteActionModal;

View File

@@ -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<void>;
onUpdateEnv: (oldSlug: string, arg0: Env) => Promise<void>;
onDeleteEnv: (slug: string) => Promise<void>;
};
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<Env, 'slug'>)?.slug,
env
);
handlePopUpClose('createUpdateEnv');
} catch (error) {
console.error(error);
}
};
const onEnvDeleteCB = async () => {
try {
await onDeleteEnv(
(popUp.deleteEnv?.data as Pick<Env, 'slug'>)?.slug
);
handlePopUpClose('deleteEnv');
} catch (error) {
console.error(error);
}
};
return (
<>
<div className='flex flex-row justify-between w-full'>
<div className='flex flex-col w-full'>
<p className='text-xl font-semibold mb-3'>Project Environments</p>
<p className='text-base text-gray-400 mb-4'>
Choose which environments will show up in your dashboard like
development, staging, production
</p>
<p className='text-sm mr-1 text-gray-500 self-start'>
Note: the text in slugs shows how these environmant should be
accessed in CLI.
</p>
</div>
<div className='w-48'>
<Button
text='Add New Env'
onButtonPressed={() => handlePopUpOpen('createUpdateEnv')}
color='mineshaft'
icon={faPlus}
size='md'
/>
</div>
</div>
<div className='table-container w-full bg-bunker rounded-md mb-6 border border-mineshaft-700 relative mt-1'>
<div className='absolute rounded-t-md w-full h-12 bg-white/5'></div>
<table className='w-full my-1'>
<thead className='text-bunker-300'>
<tr>
<th className='text-left pl-6 pt-2.5 pb-2'>Name</th>
<th className='text-left pl-6 pt-2.5 pb-2'>Slug</th>
<th></th>
</tr>
</thead>
<tbody>
{data?.length > 0 ? (
data.map(({ name, slug }) => {
return (
<tr
key={name}
className='bg-bunker-800 hover:bg-bunker-800/5 duration-100'
>
<td className='pl-6 py-2 border-mineshaft-700 border-t text-gray-300 capitalize'>
{name}
</td>
<td className='pl-6 py-2 border-mineshaft-700 border-t text-gray-300'>
{slug}
</td>
<td className='py-2 border-mineshaft-700 border-t flex'>
<div className='opacity-50 hover:opacity-100 duration-200 flex items-center mr-8'>
<Button
onButtonPressed={() =>
handlePopUpOpen('createUpdateEnv', { name, slug })
}
color='red'
size='icon-sm'
icon={faPencil}
/>
</div>
<div className='opacity-50 hover:opacity-100 duration-200 flex items-center'>
<Button
onButtonPressed={() =>
handlePopUpOpen('deleteEnv', { name, slug })
}
color='red'
size='icon-sm'
icon={faX}
/>
</div>
</td>
</tr>
);
})
) : (
<tr>
<td
colSpan={4}
className='text-center pt-7 pb-4 text-bunker-400'
>
No environmants found
</td>
</tr>
)}
</tbody>
</table>
<DeleteActionModal
isOpen={popUp['deleteEnv'].isOpen}
title={`Are you sure want to delete ${
(popUp?.deleteEnv?.data as { name: string })?.name || ' '
}?`}
deleteKey={(popUp?.deleteEnv?.data as { slug: string })?.slug || ''}
onClose={() => handlePopUpClose('deleteEnv')}
onSubmit={onEnvDeleteCB}
/>
<AddUpdateEnvironmentDialog
isOpen={popUp.createUpdateEnv.isOpen}
isEditMode={Boolean(popUp.createUpdateEnv?.data)}
initialValues={popUp?.createUpdateEnv?.data as any}
onClose={() => handlePopUpClose('createUpdateEnv')}
onCreateSubmit={onEnvCreateCB}
onEditSubmit={onEnvUpdateCB}
/>
</div>
</>
);
};
export default EnvironmentTable;

View File

@@ -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}
</td>
<td className="pl-6 py-2 border-mineshaft-700 border-t text-gray-300">
{reverseEnvMapping[row.environment]}
{row.environment}
</td>
<td className="pl-6 py-2 border-mineshaft-700 border-t text-gray-300">
{new Date(row.expiresAt).toUTCString()}

View File

@@ -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 && (
<div className="absolute flex flex-row items-center z-20 peer pr-2 bg-bunker-800 group-hover:hidden peer-hover:hidden peer-focus:hidden peer-active:invisible h-9 w-full rounded-md text-gray-400/50 text-clip">
<div className="px-2 flex flex-row items-center overflow-x-scroll no-scrollbar no-scrollbar::-webkit-scrollbar">
{value.split('').map(() => (
{value?.split('').map(() => (
<FontAwesomeIcon
key={guidGenerator()}
className="text-xxs mx-0.5"

View File

@@ -2,21 +2,12 @@ import { Fragment } from 'react';
import { useTranslation } from "next-i18next";
import { faDownload } from '@fortawesome/free-solid-svg-icons';
import { Menu, Transition } from '@headlessui/react';
import { SecretDataProps } from 'public/data/frequentInterfaces';
import Button from '../basic/buttons/Button';
import downloadDotEnv from '../utilities/secrets/downloadDotEnv';
import downloadYaml from '../utilities/secrets/downloadYaml';
interface SecretDataProps {
type: 'personal' | 'shared';
pos: number;
key: string;
value: string;
id: string;
comment: string;
}
/**
* This is the menu that is used to download secrets as .env ad .yml files (in future we may have more options)
* @param {object} obj

View File

@@ -1,22 +1,15 @@
import React from 'react';
import { faEllipsis, faShuffle, faX } from '@fortawesome/free-solid-svg-icons';
import { faEllipsis } from '@fortawesome/free-solid-svg-icons';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { SecretDataProps } from 'public/data/frequentInterfaces';
import Button from '../basic/buttons/Button';
import DashboardInputField from './DashboardInputField';
interface SecretDataProps {
type: 'personal' | 'shared';
pos: number;
key: string;
value: string;
id: string;
}
interface KeyPairProps {
keyPair: SecretDataProps;
modifyKey: (value: string, position: number) => 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 (
<div className={`mx-1 flex flex-col items-center ml-1 ${isSnapshot && "pointer-events-none"} ${keyPair.id == sidebarSecretId && "bg-mineshaft-500 duration-200"} rounded-md`}>
<div className="relative flex flex-row justify-between w-full max-w-5xl mr-auto max-h-14 my-1 items-start px-1">
{keyPair.type == "personal" && <div className="group font-normal group absolute top-[1rem] left-[0.2rem] z-40 inline-block text-gray-300 underline hover:text-primary duration-200">
{keyPair.valueOverride && <div className="group font-normal group absolute top-[1rem] left-[0.2rem] z-40 inline-block text-gray-300 underline hover:text-primary duration-200">
<div className='w-1 h-1 rounded-full bg-primary z-40'></div>
<span className="absolute z-50 hidden group-hover:flex group-hover:animate-popdown duration-200 w-[10.5rem] -left-[0.4rem] -top-[1.7rem] translate-y-full px-2 py-2 bg-mineshaft-500 rounded-b-md rounded-r-md text-center text-gray-100 text-sm after:content-[''] after:absolute after:left-0 after:bottom-[100%] after:-translate-x-0 after:border-8 after:border-x-transparent after:border-t-transparent after:border-b-mineshaft-500">
This secret is overriden
@@ -70,12 +65,12 @@ const KeyPair = ({
<div className="w-full min-w-xl">
<div className={`flex min-w-xl items-center ${!isSnapshot && "pr-1.5"} rounded-lg mt-4 md:mt-0 max-h-10`}>
<DashboardInputField
onChangeHandler={modifyValue}
onChangeHandler={keyPair.valueOverride ? modifyValueOverride : modifyValue}
type="value"
position={keyPair.pos}
value={keyPair.value}
value={keyPair.valueOverride ? keyPair.valueOverride : keyPair.value}
blurred={isBlurred}
override={keyPair.type == "personal"}
override={Boolean(keyPair.valueOverride)}
/>
</div>
</div>

View File

@@ -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 <div className='absolute border-l border-mineshaft-500 bg-bunker fixed h-full w-96 top-14 right-0 z-40 shadow-xl flex flex-col justify-between'>
@@ -111,19 +101,19 @@ const SideBar = ({
blurred={false}
/>
</div>
{data.filter(secret => secret.type == "shared")[0]?.value
{data[0]?.value
? <div className={`relative mt-2 px-4 ${overrideEnabled && "opacity-40 pointer-events-none"} duration-200`}>
<p className='text-sm text-bunker-300'>{t("dashboard:sidebar.value")}</p>
<DashboardInputField
onChangeHandler={modifyValue}
type="value"
position={data.filter(secret => 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}
/>
<div className='absolute bg-bunker-800 right-[1.07rem] top-[1.6rem] z-50'>
<GenerateSecretMenu modifyValue={modifyValue} position={data.filter(secret => secret.type == "shared")[0]?.pos} />
<GenerateSecretMenu modifyValue={modifyValue} position={data[0]?.pos} />
</div>
</div>
: <div className='px-4 text-sm text-bunker-300 pt-4'>
@@ -131,39 +121,32 @@ const SideBar = ({
{t("dashboard:sidebar.personal-explanation")}
</div>}
<div className='mt-4 px-4'>
{data.filter(secret => secret.type == "shared")[0]?.value &&
{data[0]?.value &&
<div className='flex flex-row items-center justify-between my-2 pl-1 pr-2'>
<p className='text-sm text-bunker-300'>{t("dashboard:sidebar.override")}</p>
<Toggle
enabled={overrideEnabled}
setEnabled={setOverrideEnabled}
addOverride={addOverride}
keyName={data[0]?.key}
value={data[0]?.value}
addOverride={modifyValueOverride}
pos={data[0]?.pos}
id={data[0]?.id}
comment={data[0]?.comment}
deleteOverride={deleteOverride}
sharedToHide={sharedToHide}
setSharedToHide={setSharedToHide}
/>
</div>}
<div className={`relative ${!overrideEnabled && "opacity-40 pointer-events-none"} duration-200`}>
<DashboardInputField
onChangeHandler={modifyValue}
onChangeHandler={modifyValueOverride}
type="value"
position={overrideEnabled ? data.filter(secret => 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}
/>
<div className='absolute right-[0.57rem] top-[0.3rem] z-50'>
<GenerateSecretMenu modifyValue={modifyValue} position={overrideEnabled ? data.filter(secret => secret.type == "personal")[0]?.pos : data[0]?.pos} />
<GenerateSecretMenu modifyValue={modifyValueOverride} position={data[0]?.pos} />
</div>
</div>
</div>
<SecretVersionList secretId={data[0]?.id} />
<CommentField comment={data.filter(secret => secret.type == "shared")[0]?.comment} modifyComment={modifyComment} position={data.filter(secret => secret.type == "shared")[0]?.pos} />
<CommentField comment={data[0]?.comment} modifyComment={modifyComment} position={data[0]?.pos} />
</div>
)}
<div className={`flex justify-start max-w-sm mt-4 px-4 mt-full mb-[4.7rem]`}>
@@ -176,7 +159,7 @@ const SideBar = ({
textDisabled="Saved"
/>
<DeleteActionButton
onSubmit={() => 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 })}
/>
</div>
</div>

View File

@@ -74,7 +74,7 @@ const CloudIntegration = ({
integrationAuths
.map((authorization) => authorization.integration)
.includes(cloudIntegrationOption.name.toLowerCase()) && (
<div className="absolute group z-50 top-0 right-0 flex flex-row">
<div className="absolute group z-40 top-0 right-0 flex flex-row">
<div
onClick={(event) => {
event.stopPropagation();

View File

@@ -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 (
<div>
<div className="text-gray-400 text-xs font-semibold mb-2">
<div className="text-gray-400 text-xs font-semibold mb-2 w-60">
ENVIRONMENT
</div>
<ListBox
@@ -104,6 +112,7 @@ const Integration = ({
] : null}
selected={integrationTarget}
onChange={setIntegrationTarget}
isFull={true}
/>
</div>
);
@@ -136,42 +145,47 @@ const Integration = ({
if (!integrationApp || apps.length === 0) return <div></div>
return (
<div className="max-w-5xl p-6 mx-6 mb-8 rounded-md bg-white/5 flex justify-between">
<div className="flex">
<div className='max-w-5xl p-6 mx-6 mb-8 rounded-md bg-white/5 flex justify-between'>
<div className='flex'>
<div>
<p className="text-gray-400 text-xs font-semibold mb-2">ENVIRONMENT</p>
<ListBox data={!integration.isActive ? [
"Development",
"Staging",
"Testing",
"Production",
] : null}
selected={integrationEnvironment}
onChange={(environment) => {
setIntegrationEnvironment(environment);
}}
<p className='text-gray-400 text-xs font-semibold mb-2'>
ENVIRONMENT
</p>
<ListBox
data={
!integration.isActive
? environments.map(({ name }) => name)
: null
}
selected={integrationEnvironment.name}
onChange={(envName) =>
setIntegrationEnvironment(
environments.find(({ name }) => envName === name) || {
name: 'unknown',
slug: 'unknown',
}
)
}
isFull={true}
/>
</div>
<div className="pt-2">
<div className='pt-2'>
<FontAwesomeIcon
icon={faArrowRight}
className="mx-4 text-gray-400 mt-8"
/>
className='mx-4 text-gray-400 mt-8'
/>
</div>
<div className="mr-2">
<p className="text-gray-400 text-xs font-semibold mb-2">
<div className='mr-2'>
<p className='text-gray-400 text-xs font-semibold mb-2'>
INTEGRATION
</p>
<div className="py-2.5 bg-white/[.07] rounded-md pl-4 pr-10 text-sm font-semibold text-gray-300">
<div className='py-2.5 bg-white/[.07] rounded-md pl-4 pr-10 text-sm font-semibold text-gray-300'>
{integration.integration.charAt(0).toUpperCase() +
integration.integration.slice(1)}
</div>
</div>
<div className="mr-2">
<div className="text-gray-400 text-xs font-semibold mb-2">
APP
</div>
<div className='mr-2'>
<div className='text-gray-400 text-xs font-semibold mb-2'>APP</div>
<ListBox
data={!integration.isActive ? apps.map((app) => app.name) : null}
selected={integrationApp}
@@ -182,52 +196,55 @@ const Integration = ({
</div>
{renderIntegrationSpecificParams(integration)}
</div>
<div className="flex items-end">
{integration.isActive ? (
<div className="max-w-5xl flex flex-row items-center bg-white/5 p-2 rounded-md px-4">
<FontAwesomeIcon
icon={faRotate}
className="text-lg mr-2.5 text-primary animate-spin"
/>
<div className="text-gray-300 font-semibold">In Sync</div>
</div>
) : (
<Button
text="Start Integration"
onButtonPressed={async () => {
const siteApp = apps.find((app) => app.name === integrationApp); // obj or undefined
const siteId = siteApp?.siteId ? siteApp.siteId : null;
const result = await updateIntegration({
integrationId: integration._id,
environment: envMapping[integrationEnvironment],
app: integrationApp,
isActive: true,
target: integrationTarget ? integrationTarget.toLowerCase() : null,
context: integrationContext ? reverseContextNetlifyMapping[integrationContext] : null,
siteId
});
router.reload();
}}
color="mineshaft"
size="md"
/>
)}
<div className="opacity-50 hover:opacity-100 duration-200 ml-2">
<Button
onButtonPressed={async () => {
await deleteIntegration({
integrationId: integration._id,
});
router.reload();
}}
color="red"
size="icon-md"
icon={faX}
<div className='flex items-end'>
{integration.isActive ? (
<div className='max-w-5xl flex flex-row items-center bg-white/5 p-2 rounded-md px-4'>
<FontAwesomeIcon
icon={faRotate}
className='text-lg mr-2.5 text-primary animate-spin'
/>
<div className='text-gray-300 font-semibold'>In Sync</div>
</div>
) : (
<Button
text='Start Integration'
onButtonPressed={async () => {
const siteApp = apps.find((app) => app.name === integrationApp); // obj or undefined
const siteId = siteApp?.siteId ? siteApp.siteId : null;
await updateIntegration({
integrationId: integration._id,
environment: integrationEnvironment.slug,
app: integrationApp,
isActive: true,
target: integrationTarget
? integrationTarget.toLowerCase()
: null,
context: integrationContext
? reverseContextNetlifyMapping[integrationContext]
: null,
siteId,
});
router.reload();
}}
color='mineshaft'
size='md'
/>
)}
<div className='opacity-50 hover:opacity-100 duration-200 ml-2'>
<Button
onButtonPressed={async () => {
await deleteIntegration({
integrationId: integration._id,
});
router.reload();
}}
color='red'
size='icon-md'
icon={faX}
/>
</div>
</div>
</div>
);

View File

@@ -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 ? (
<div className="mb-12">
@@ -33,6 +35,7 @@ const ProjectIntegrationSection = ({
<Integration
key={guidGenerator()}
integration={integration}
environments={environments}
/>
))}
</div>

View File

@@ -81,7 +81,7 @@ export default function CodeInputStep({ email, incrementStep, setCode, codeError
return (
<div className="bg-bunker w-max mx-auto h-7/12 pt-10 pb-4 px-8 rounded-xl drop-shadow-xl mb-64 md:mb-16">
<p className="text-l flex justify-center text-bunker-300">
{"We've"} sent a verification email to{" "}
{t("signup:step2-message")}
</p>
<p className="text-l flex justify-center font-semibold my-2 text-bunker-300">
{email}{" "}
@@ -119,11 +119,11 @@ export default function CodeInputStep({ email, incrementStep, setCode, codeError
<div className="flex flex-col items-center justify-center w-full max-h-24 max-w-md mx-auto pt-2">
<div className="flex flex-row items-baseline gap-1 text-sm">
<span className="text-bunker-400">
Not seeing an email?
{t("signup:step2-resend-alert")}
</span>
<u className={`font-normal ${isResendingVerificationEmail ? 'text-bunker-400' : 'text-primary-700 hover:text-primary duration-200'}`}>
<button disabled={isLoading} onClick={resendVerificationEmail}>
{isResendingVerificationEmail ? "Resending..." : "Resend"}
{isResendingVerificationEmail ? t("signup:step2-resend-progress") : t("signup:step2-resend-submit")}
</button>
</u>
</div>

View File

@@ -59,11 +59,11 @@ export default function EnterEmailStep({ email, setEmail, incrementStep }: Downl
<div>
<div className="bg-bunker w-full max-w-md mx-auto h-7/12 py-8 md:px-6 mx-1 rounded-xl drop-shadow-xl">
<p className="text-4xl font-semibold flex justify-center text-primary">
{'Let\''}s get started
{t("signup:step1-start")}
</p>
<div className="flex items-center justify-center w-5/6 md:w-full m-auto md:p-2 rounded-lg max-h-24 mt-4">
<InputField
label="Email"
label={t("common:email") ?? ""}
onChangeHandler={setEmail}
type="email"
value={email}
@@ -79,7 +79,7 @@ export default function EnterEmailStep({ email, setEmail, incrementStep }: Downl
{t("signup:step1-privacy")}
</p>
<div className="text-l mt-6 m-2 md:m-8 px-8 py-1 text-lg">
<Button text="Get Started" type="submit" onButtonPressed={emailCheck} size="lg" />
<Button text={t("signup:step1-submit") ?? ""} type="submit" onButtonPressed={emailCheck} size="lg" />
</div>
</div>
</div>

View File

@@ -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_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: ''
}, {
pos: 3,
key: "DB_USERNAME",
value: "user1234",
valueOverride: "user1234",
comment: "",
id: ''
}, {
type: "shared",
pos: 3,
key: "DB_PASSWORD",
value: "OVERRIDE_THIS",
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: 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' })

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -20,4 +20,5 @@ export const publicPaths = [
export const languageMap = {
en: "English",
ko: "한국어",
fr: "Français",
};

View File

@@ -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 <div className={`absolute border-l border-mineshaft-500 ${isLoading ? "bg-bunker-800" : "bg-bunker"} fixed h-full w-96 top-14 right-0 z-40 shadow-xl flex flex-col justify-between`}>
@@ -125,31 +146,35 @@ const PITRecoverySidebar = ({
></Image>
</div>
) : (
<div className='h-min overflow-y-auto'>
<div className='h-min'>
<div className="flex flex-row px-4 py-3 border-b border-mineshaft-500 justify-between items-center">
<p className="font-semibold text-lg text-bunker-200">{t("Point-in-time Recovery")}</p>
<div className='p-1' onClick={() => toggleSidebar(false)}>
<FontAwesomeIcon icon={faX} className='w-4 h-4 text-bunker-300 cursor-pointer'/>
</div>
</div>
<div className='flex flex-col px-2 py-2'>
{secretSnapshotsMetadata?.map((snapshot: SnaphotProps, id: number) => <div key={snapshot._id} className={`${chosenSnapshot == snapshot._id || (id == 0 && chosenSnapshot === "") ? "bg-primary text-black" : "bg-mineshaft-700"} py-3 px-4 mb-2 rounded-md flex flex-row justify-between items-center`}>
<div className="flex flex-row items-start">
<div className={`${chosenSnapshot == snapshot._id || (id == 0 && chosenSnapshot === "") ? "text-bunker-800" : "text-bunker-200"} text-sm mr-1.5`}>{timeSince(new Date(snapshot.createdAt))}</div>
<div className={`${chosenSnapshot == snapshot._id || (id == 0 && chosenSnapshot === "") ? "text-bunker-900" : "text-bunker-300"} text-sm `}>{" - " + snapshot.secretVersions.length + " Secrets"}</div>
</div>
<div className='flex flex-col px-2 py-2 overflow-y-auto h-[92vh]'>
{secretSnapshotsMetadata?.map((snapshot: SnaphotProps, id: number) =>
<div
onClick={() => 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`}
>
<div className="flex flex-row items-start">
<div className={`${chosenSnapshot == snapshot._id || (id == 0 && chosenSnapshot === "") ? "text-bunker-800" : "text-bunker-200"} text-sm mr-1.5`}>{timeSince(new Date(snapshot.createdAt))}</div>
<div className={`${chosenSnapshot == snapshot._id || (id == 0 && chosenSnapshot === "") ? "text-bunker-900" : "text-bunker-300"} text-sm `}>{" - " + snapshot.secretVersions.length + " Secrets"}</div>
</div>
<div
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"}
</div>
</div>)}
<div className='flex justify-center w-full mb-14'>
<div className='items-center w-40'>
<Button text="View More" textDisabled="End of History" active={secretSnapshotsMetadata.length % 15 == 0 ? true : false} onButtonPressed={loadMoreSnapshots} size="md" color="mineshaft"/>
</div>
</div>)}
<div className='flex justify-center w-full mb-14'>
<div className='items-center w-40'>
<Button text="View More" textDisabled="End of History" active={secretSnapshotsMetadata.length % 15 == 0 ? true : false} onButtonPressed={loadMoreSnapshots} size="md" color="mineshaft"/>
</div>
</div>
</div>
</div>
)}
</div>

View File

@@ -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; }) => {
</div>
) : (
<div className='h-48 overflow-y-auto overflow-x-none'>
{secretVersions?.sort((a, b) => b.createdAt.localeCompare(a.createdAt))
.map((version: DecryptedSecretVersionListProps, index: number) =>
<div key={index} className='flex flex-row'>
<div className='pr-1 flex flex-col items-center'>
<div className='p-1'><FontAwesomeIcon icon={index == 0 ? faDotCircle : faCircle} /></div>
<div className='w-0 h-full border-l mt-1'></div>
</div>
<div className='flex flex-col w-full max-w-[calc(100%-2.3rem)]'>
<div className='pr-2 pt-1'>
{(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) =>
<div key={index} className='flex flex-row'>
<div className='pr-1 flex flex-col items-center'>
<div className='p-1'><FontAwesomeIcon icon={index == 0 ? faDotCircle : faCircle} /></div>
<div className='w-0 h-full border-l mt-1'></div>
</div>
<div className='flex flex-col w-full max-w-[calc(100%-2.3rem)]'>
<div className='pr-2 pt-1'>
{(new Date(version.createdAt)).toLocaleDateString('en-US', {
year: 'numeric',
month: '2-digit',
day: '2-digit',
hour: '2-digit',
minute: '2-digit',
second: '2-digit'
})}
</div>
<div className=''><p className='break-words'><span className='py-0.5 px-1 rounded-md bg-primary-200/10 mr-1.5'>Value:</span>{version.value}</p></div>
</div>
<div className=''><p className='break-words'><span className='py-0.5 px-1 rounded-md bg-primary-200/10 mr-1.5'>Value:</span>{version.value}</p></div>
</div>
</div>
)}
)
: (
<div className='w-full h-full flex items-center justify-center text-bunker-400'>No version history yet.</div>
)
}
</div>
)}
</div>

1
frontend/hooks/index.ts Normal file
View File

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

View File

@@ -0,0 +1,69 @@
import { useCallback, useState } from 'react';
interface usePopUpProps {
name: Readonly<string>;
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<T extends Readonly<string[]> | usePopUpProps[]> = {
[P in T extends usePopUpProps[] ? T[number]['name'] : T[number]]: {
isOpen: boolean;
data?: unknown;
};
};
interface usePopUpReturn<T extends Readonly<string[]> | usePopUpProps[]> {
popUp: usePopUpState<T>;
handlePopUpOpen: (popUpName: keyof usePopUpState<T>, data?: unknown) => void;
handlePopUpClose: (popUpName: keyof usePopUpState<T>) => void;
handlePopUpToggle: (popUpName: keyof usePopUpState<T>) => 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 = <T extends Readonly<string[]> | usePopUpProps[]>(
popUpNames: T
): usePopUpReturn<T> => {
const [popUp, setPopUp] = useState<usePopUpState<T>>(
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<T> // to override generic string return type of the function
);
const handlePopUpOpen = useCallback(
(popUpName: keyof usePopUpState<T>, data?: unknown) => {
setPopUp((popUp) => ({ ...popUp, [popUpName]: { isOpen: true, data } }));
},
[]
);
const handlePopUpClose = useCallback((popUpName: keyof usePopUpState<T>) => {
setPopUp((popUp) => ({ ...popUp, [popUpName]: { isOpen: false } }));
}, []);
const handlePopUpToggle = useCallback((popUpName: keyof usePopUpState<T>) => {
setPopUp((popUp) => ({
...popUp,
[popUpName]: { isOpen: !popUp[popUpName].isOpen },
}));
}, []);
return {
popUp,
handlePopUpOpen,
handlePopUpClose,
handlePopUpToggle,
};
};

View File

@@ -8,7 +8,7 @@ module.exports = {
debug: false,
i18n: {
defaultLocale: "en",
locales: ["en", "ko"],
locales: ["en", "ko", "fr", "pt-BR"],
},
fallbackLng: {
default: ["en"],

View File

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

View File

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

View File

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

View File

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

View File

@@ -5,6 +5,7 @@ interface Workspace {
_id: string;
name: string;
organization: string;
environments: Array<{name:string, slug:string}>
}
/**

View File

@@ -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<SecretDataProps[] | null>();
const [initialData, setInitialData] = useState<SecretDataProps[]>([]);
const [initialData, setInitialData] = useState<SecretDataProps[] | null | undefined>([]);
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<string[]>([]);
const [snapshotData, setSnapshotData] = useState<SnapshotProps>();
const [numSnapshots, setNumSnapshots] = useState<number>();
const [saveLoading, setSaveLoading] = useState(false);
const { t } = useTranslation();
const { createNotification } = useNotificationContext();
const workspaceId = router.query.id as string;
const [workspaceEnvs, setWorkspaceEnvs] = useState<WorkspaceEnv[]>([]);
const [selectedSnapshotEnv, setSelectedSnapshotEnv] =
useState<WorkspaceEnv>();
const [selectedEnv, setSelectedEnv] = useState<WorkspaceEnv>({
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,66 +254,28 @@ 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");
};
/**
* 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) => {
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")
sortValuesHandler(
data!.filter((row: SecretDataProps) => !ids.includes(row.id)),
sortMethod == 'alhpabetical' ? '-alphabetical' : 'alphabetical'
);
};
const modifyValue = (value: string, pos: number) => {
@@ -307,6 +286,14 @@ export default function Dashboard() {
setButtonReady(true);
};
const modifyValueOverride = (value: string | undefined, pos: number) => {
setData((oldData) => {
oldData![pos].valueOverride = value;
return [...oldData!];
});
setButtonReady(true);
};
const modifyKey = (value: string, pos: number) => {
setData((oldData) => {
oldData![pos].key = value;
@@ -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 ? (
<div className="bg-bunker-800 max-h-screen flex flex-col justify-between text-white">
<div className='bg-bunker-800 max-h-screen flex flex-col justify-between text-white'>
<Head>
<title>{t("common:head-title", { title: t("dashboard:title") })}</title>
<link rel="icon" href="/infisical.ico" />
<meta property="og:image" content="/images/message.png" />
<meta property="og:title" content={String(t("dashboard:og-title"))} />
<meta name="og:description" content={String(t("dashboard:og-description"))} />
<title>{t('common:head-title', { title: t('dashboard:title') })}</title>
<link rel='icon' href='/infisical.ico' />
<meta property='og:image' content='/images/message.png' />
<meta property='og:title' content={String(t('dashboard:og-title'))} />
<meta
name='og:description'
content={String(t('dashboard:og-description'))}
/>
</Head>
<div className="flex flex-row">
{sidebarSecretId != "None" && <SideBar
@@ -461,9 +492,8 @@ export default function Dashboard() {
data={data.filter((row: SecretDataProps) => 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() {
<NavHeader pageName={t("dashboard:title")} isProjectRelated={true} />
{checkDocsPopUpVisible && (
<BottonRightPopup
buttonText={t("dashboard:check-docs.button")}
buttonLink="https://infisical.com/docs/getting-started/introduction"
titleText={t("dashboard:check-docs.title")}
emoji="🎉"
textLine1={t("dashboard:check-docs.line1")}
textLine2={t("dashboard:check-docs.line2")}
buttonText={t('dashboard:check-docs.button')}
buttonLink='https://infisical.com/docs/getting-started/introduction'
titleText={t('dashboard:check-docs.title')}
emoji='🎉'
textLine1={t('dashboard:check-docs.line1')}
textLine2={t('dashboard:check-docs.line2')}
setCheckDocsPopUpVisible={setCheckDocsPopUpVisible}
/>
)}
<div className="flex flex-row justify-between items-center mx-6 mt-6 mb-3 text-xl max-w-5xl">
{snapshotData &&
<div className={`flex justify-start max-w-sm mt-1 mr-2`}>
<Button
text={String(t("Go back to current"))}
onButtonPressed={() => setSnapshotData(undefined)}
color="mineshaft"
size="md"
icon={faArrowLeft}
/>
</div>}
<div className="flex flex-row justify-start items-center text-3xl">
<div className="font-semibold mr-4 mt-1 flex flex-row items-center">
<p>{snapshotData ? "Secret Snapshot" : t("dashboard:title")}</p>
{snapshotData && <span className='bg-primary-800 text-sm ml-4 mt-1 px-1.5 rounded-md'>{new Date(snapshotData.createdAt).toLocaleString()}</span>}
<div className='flex flex-row justify-between items-center mx-6 mt-6 mb-3 text-xl max-w-5xl'>
{snapshotData && (
<div className={`flex justify-start max-w-sm mt-1 mr-2`}>
<Button
text={String(t('Go back to current'))}
onButtonPressed={() => setSnapshotData(undefined)}
color='mineshaft'
size='md'
icon={faArrowLeft}
/>
</div>
)}
<div className='flex flex-row justify-start items-center text-3xl'>
<div className='font-semibold mr-4 mt-1 flex flex-row items-center'>
<p>{snapshotData ? 'Secret Snapshot' : t('dashboard:title')}</p>
{snapshotData && (
<span className='bg-primary-800 text-sm ml-4 mt-1 px-1.5 rounded-md'>
{new Date(snapshotData.createdAt).toLocaleString()}
</span>
)}
</div>
{!snapshotData && data?.length == 0 && (
<ListBox
selected={env}
data={['Development', 'Staging', 'Production', 'Testing']}
onChange={setEnv}
selected={selectedEnv.name}
data={workspaceEnvs.map(({ name }) => name)}
onChange={(envName) =>
setSelectedEnv(
workspaceEnvs.find(({ name }) => envName === name) || {
name: 'unknown',
slug: 'unknown',
}
)
}
/>
)}
</div>
<div className="flex flex-row">
<div className='flex flex-row'>
<div className={`flex justify-start max-w-sm mt-1 mr-2`}>
<Button
text={String(numSnapshots + " " + t("Commits"))}
text={String(numSnapshots + ' ' + t('Commits'))}
onButtonPressed={() => togglePITSidebar(true)}
color="mineshaft"
size="md"
color='mineshaft'
size='md'
icon={faClockRotateLeft}
/>
</div>
{(data?.length !== 0 || buttonReady) && !snapshotData && (
<div className={`flex justify-start max-w-sm mt-1`}>
<Button
text={String(t("common:save-changes"))}
text={String(t('common:save-changes'))}
onButtonPressed={savePush}
color="primary"
size="md"
color='primary'
size='md'
active={buttonReady}
iconDisabled={faCheck}
textDisabled={String(t("common:saved"))}
loading={saveLoading}
/>
</div>
)}
@@ -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() {
</div>}
</div>
</div>
<div className="mx-6 w-full pr-12">
<div className="flex flex-col max-w-5xl pb-1">
<div className="w-full flex flex-row items-start">
<div className='mx-6 w-full pr-12'>
<div className='flex flex-col max-w-5xl pb-1'>
<div className='w-full flex flex-row items-start'>
{(snapshotData || data?.length !== 0) && (
<>
{!snapshotData
? <ListBox
selected={env}
data={['Development', 'Staging', 'Production', 'Testing']}
onChange={setEnv}
/>
: <ListBox
selected={snapshotEnv}
data={['Development', 'Staging', 'Production', 'Testing']}
onChange={setSnapshotEnv}
/>}
<div className="h-10 w-full bg-white/5 hover:bg-white/10 ml-2 flex items-center rounded-md flex flex-row items-center">
{!snapshotData ? (
<ListBox
selected={selectedEnv.name}
data={workspaceEnvs.map(({ name }) => name)}
onChange={(envName) =>
setSelectedEnv(
workspaceEnvs.find(
({ name }) => envName === name
) || {
name: 'unknown',
slug: 'unknown',
}
)
}
/>
) : (
<ListBox
selected={selectedSnapshotEnv?.name || ''}
data={workspaceEnvs.map(({ name }) => name)}
onChange={(envName) =>
setSelectedSnapshotEnv(
workspaceEnvs.find(
({ name }) => envName === name
) || {
name: 'unknown',
slug: 'unknown',
}
)
}
/>
)}
<div className='h-10 w-full bg-white/5 hover:bg-white/10 ml-2 flex items-center rounded-md flex flex-row items-center'>
<FontAwesomeIcon
className="bg-white/5 rounded-l-md py-3 pl-4 pr-2 text-gray-400"
className='bg-white/5 rounded-l-md py-3 pl-4 pr-2 text-gray-400'
icon={faMagnifyingGlass}
/>
<input
className="pl-2 text-gray-400 rounded-r-md bg-white/5 w-full h-full outline-none"
className='pl-2 text-gray-400 rounded-r-md bg-white/5 w-full h-full outline-none'
value={searchKeys}
onChange={(e) => setSearchKeys(e.target.value)}
placeholder={String(t("dashboard:search-keys"))}
placeholder={String(t('dashboard:search-keys'))}
/>
</div>
{!snapshotData && <div className="ml-2 min-w-max flex flex-row items-start justify-start">
<Button
onButtonPressed={() => reorderRows(1)}
color="mineshaft"
size="icon-md"
icon={
sortMethod == 'alphabetical'
? faArrowDownAZ
: faArrowDownZA
}
/>
</div>}
{!snapshotData && <div className="ml-2 min-w-max flex flex-row items-start justify-start">
<DownloadSecretMenu data={data} env={env} />
</div>}
<div className="ml-2 min-w-max flex flex-row items-start justify-start">
{!snapshotData && (
<div className='ml-2 min-w-max flex flex-row items-start justify-start'>
<Button
onButtonPressed={() => reorderRows(1)}
color='mineshaft'
size='icon-md'
icon={
sortMethod == 'alphabetical'
? faArrowDownAZ
: faArrowDownZA
}
/>
</div>
)}
{!snapshotData && (
<div className='ml-2 min-w-max flex flex-row items-start justify-start'>
<DownloadSecretMenu
data={data}
env={selectedEnv.slug}
/>
</div>
)}
<div className='ml-2 min-w-max flex flex-row items-start justify-start'>
<Button
onButtonPressed={changeBlurred}
color="mineshaft"
size="icon-md"
color='mineshaft'
size='icon-md'
icon={blurred ? faEye : faEyeSlash}
/>
</div>
{!snapshotData && <div className="relative ml-2 min-w-max flex flex-row items-start justify-end">
<Button
text={String(t("dashboard:add-key"))}
onButtonPressed={addRow}
color="mineshaft"
icon={faPlus}
size="md"
/>
{isNew && (
<span className="absolute right-0 flex h-3 w-3 items-center justify-center ml-4 mb-4">
<span className="animate-ping absolute inline-flex h-full w-full rounded-full bg-primary/50 opacity-75 h-4 w-4"></span>
<span className="relative inline-flex rounded-full h-3 w-3 bg-primary"></span>
</span>
)}
</div>}
{!snapshotData && (
<div className='relative ml-2 min-w-max flex flex-row items-start justify-end'>
<Button
text={String(t('dashboard:add-key'))}
onButtonPressed={addRow}
color='mineshaft'
icon={faPlus}
size='md'
/>
{isNew && (
<span className='absolute right-0 flex h-3 w-3 items-center justify-center ml-4 mb-4'>
<span className='animate-ping absolute inline-flex h-full w-full rounded-full bg-primary/50 opacity-75 h-4 w-4'></span>
<span className='relative inline-flex rounded-full h-3 w-3 bg-primary'></span>
</span>
)}
</div>
)}
</>
)}
</div>
</div>
{isLoading ? (
<div className="flex items-center justify-center h-full my-48">
<Image
src="/images/loading/loading.gif"
height={60}
width={100}
alt="infisical loading indicator"
></Image>
</div>
) : (
data?.length !== 0 ? (
<div className="flex flex-col w-full mt-1 mb-2">
<div className='flex items-center justify-center h-full my-48'>
<Image
src='/images/loading/loading.gif'
height={60}
width={100}
alt='infisical loading indicator'
></Image>
</div>
) : data?.length !== 0 ? (
<div className='flex flex-col w-full mt-1 mb-2'>
<div
className={`max-w-5xl mt-1 max-h-[calc(100vh-280px)] overflow-hidden overflow-y-scroll no-scrollbar no-scrollbar::-webkit-scrollbar`}
>
<div className="px-1 pt-2 bg-mineshaft-800 rounded-md p-2">
{!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) => (
<KeyPair
key={keyPair.id}
keyPair={keyPair}
modifyValue={listenChangeValue}
modifyValueOverride={listenChangeValueOverride}
modifyKey={listenChangeKey}
isBlurred={blurred}
isDuplicate={findDuplicates(
data?.map((item) => 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) => (
<KeyPair
key={keyPair.id}
keyPair={keyPair}
modifyValue={listenChangeValue}
modifyValueOverride={listenChangeValueOverride}
modifyKey={listenChangeKey}
isBlurred={blurred}
isDuplicate={findDuplicates(
data?.map((item) => item.key + item.type)
)?.includes(keyPair.key + keyPair.type)}
data?.map((item) => item.key)
)?.includes(keyPair.key)}
toggleSidebar={toggleSidebar}
sidebarSecretId={sidebarSecretId}
isSnapshot={true}
/>
))}
</div>
{!snapshotData && <div className="w-full max-w-5xl px-2 pt-3">
<DropZone
setData={addData}
setErrorDragAndDrop={setErrorDragAndDrop}
createNewFile={addRow}
errorDragAndDrop={errorDragAndDrop}
setButtonReady={setButtonReady}
keysExist={true}
numCurrentRows={data.length}
/>
</div>}
{!snapshotData && (
<div className='w-full max-w-5xl px-2 pt-3'>
<DropZone
setData={addData}
setErrorDragAndDrop={setErrorDragAndDrop}
createNewFile={addRow}
errorDragAndDrop={errorDragAndDrop}
setButtonReady={setButtonReady}
keysExist={true}
numCurrentRows={data.length}
/>
</div>
)}
</div>
</div>
) : (
<div className="flex flex-col items-center justify-center h-full text-xl text-gray-400 max-w-5xl mt-28">
<div className='flex flex-col items-center justify-center h-full text-xl text-gray-400 max-w-5xl mt-28'>
{isKeyAvailable && !snapshotData && (
<DropZone
setData={setData}
@@ -728,36 +796,35 @@ export default function Dashboard() {
keysExist={false}
/>
)}
{
(!isKeyAvailable && (
<>
<FontAwesomeIcon
className="text-7xl mt-20 mb-8"
icon={faFolderOpen}
/>
<p>
To view this file, contact your administrator for
permission.
</p>
<p className="mt-1">
They need to grant you access in the team tab.
</p>
</>
))}
{!isKeyAvailable && (
<>
<FontAwesomeIcon
className='text-7xl mt-20 mb-8'
icon={faFolderOpen}
/>
<p>
To view this file, contact your administrator for
permission.
</p>
<p className='mt-1'>
They need to grant you access in the team tab.
</p>
</>
)}
</div>
))}
)}
</div>
</div>
</div>
</div>
) : (
<div className="relative z-10 w-10/12 mr-auto h-full ml-2 bg-bunker-800 flex flex-col items-center justify-center">
<div className="absolute top-0 bg-bunker h-14 border-b border-mineshaft-700 w-full"></div>
<div className='relative z-10 w-10/12 mr-auto h-full ml-2 bg-bunker-800 flex flex-col items-center justify-center'>
<div className='absolute top-0 bg-bunker h-14 border-b border-mineshaft-700 w-full'></div>
<Image
src="/images/loading/loading.gif"
src='/images/loading/loading.gif'
height={70}
width={120}
alt="loading animation"
alt='loading animation'
></Image>
</div>
);
@@ -765,4 +832,4 @@ export default function Dashboard() {
Dashboard.requireAuth = true;
export const getServerSideProps = getTranslatedServerSideProps(["dashboard"]);
export const getServerSideProps = getTranslatedServerSideProps(['dashboard']);

View File

@@ -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}
/> */}
<IntegrationSection integrations={integrations} />
{cloudIntegrationOptions.length > 0 ? (
<IntegrationSection integrations={integrations} environments={environments} />
{(cloudIntegrationOptions.length > 0 && bot) ? (
<CloudIntegrationSection
cloudIntegrationOptions={cloudIntegrationOptions}
setSelectedIntegrationOption={setSelectedIntegrationOption}

View File

@@ -117,11 +117,15 @@ export default function Login() {
id="current-password"
/>
<div className="absolute top-2 right-3 text-primary-700 hover:text-primary duration-200 cursor-pointer text-sm">
<Link href="/verify-email">Forgot password?</Link>
<Link href="/verify-email">
<button className="text-primary-700 hover:text-primary duration-200 font-normal text-sm underline-offset-4 ml-1.5">
{t("login:forgot-password")}
</button>
</Link>
</div>
</div>
{!isLoading && errorLogin && (
<Error text="Your email and/or password are wrong." />
<Error text={t("login:error-login") ?? ""} />
)}
<div className="flex flex-col items-center justify-center w-full md:p-2 max-h-20 max-w-md mt-4 mx-auto text-sm">
<div className="text-l mt-6 m-8 px-8 py-3 text-lg">
@@ -160,7 +164,7 @@ export default function Login() {
<ListBox
selected={lang}
onChange={setLanguage}
data={["en", "ko"]}
data={["en", "ko", "fr", "pt-BR"]}
isFull
text={`${t("common:language")}: `}
/>

View File

@@ -126,7 +126,7 @@ export default function PersonalSettings() {
<ListBox
selected={lang}
onChange={setLanguage}
data={["en", "ko"]}
data={["en", "ko", "fr"]}
width="full"
text={`${t("common:language")}: `}
/>

View File

@@ -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 (
<div className="bg-bunker-800 max-h-screen flex flex-col justify-between text-white">
<Head>
<title>
{t("common:head-title", { title: t("settings-project:title") })}
</title>
<link rel="icon" href="/infisical.ico" />
</Head>
<AddServiceTokenDialog
isOpen={isAddServiceTokenDialogOpen}
workspaceId={router.query.id}
closeModal={closeAddServiceTokenModal}
workspaceName={workspaceName}
serviceTokens={serviceTokens}
setServiceTokens={setServiceTokens}
/>
<div className="flex flex-row mr-6 max-w-5xl">
<div className="w-full max-h-screen pb-2 overflow-y-auto">
<NavHeader
pageName={t("settings-project:title")}
isProjectRelated={true}
/>
<div className="flex flex-row justify-between items-center ml-6 my-8 text-xl max-w-5xl">
<div className="flex flex-col justify-start items-start text-3xl">
<p className="font-semibold mr-4 text-gray-200">
{t("settings-project:title")}
</p>
<p className="font-normal mr-4 text-gray-400 text-base">
{t("settings-project:description")}
</p>
</div>
</div>
<div className="flex flex-col ml-6 text-mineshaft-50">
<div className="flex flex-col">
<div className="min-w-md mt-2 flex flex-col items-start">
<div className="bg-white/5 rounded-md px-6 pt-6 pb-4 flex flex-col items-start flex flex-col items-start w-full mb-6 pt-2">
<p className="text-xl font-semibold mb-4 mt-2">
{t("common:display-name")}
</p>
<div className="max-h-28 w-full max-w-md mr-auto">
<InputField
onChangeHandler={modifyWorkspaceName}
type="varName"
value={workspaceName}
placeholder=""
isRequired
/>
</div>
<div className="flex justify-start w-full">
<div className={`flex justify-start max-w-sm mt-4 mb-2`}>
<Button
text={t("common:save-changes")}
onButtonPressed={() => submitChanges(workspaceName)}
color="mineshaft"
size="md"
active={buttonReady}
iconDisabled={faCheck}
textDisabled="Saved"
/>
</div>
</div>
</div>
<div className="bg-white/5 rounded-md px-6 pt-4 pb-2 flex flex-col items-start flex flex-col items-start w-full mb-6 mt-4">
<p className="text-xl font-semibold self-start">
{t("common:project-id")}
</p>
<p className="text-base text-gray-400 font-normal self-start mt-4">
{t("settings-project:project-id-description")}
</p>
<p className="text-base text-gray-400 font-normal self-start">
{t("settings-project:project-id-description2")}
{/* eslint-disable-next-line react/jsx-no-target-blank */}
<a
href="https://infisical.com/docs/getting-started/introduction"
target="_blank"
rel="noopener"
className="text-primary hover:opacity-80 duration-200"
>
{t("settings-project:docs")}
</a>
</p>
<p className="mt-4 text-xs text-bunker-300">{t("settings-project:auto-generated")}</p>
<div className="flex justify-end items-center bg-white/[0.07] text-base mt-2 mb-3 mr-2 rounded-md text-gray-400">
<p className="mr-2 font-bold pl-4">{`${t(
"common:project-id"
)}:`}</p>
<input
type="text"
value={workspaceId}
id="myInput"
className="bg-white/0 text-gray-400 py-2 w-60 px-2 min-w-md outline-none"
disabled
></input>
<div className="group font-normal group relative inline-block text-gray-400 underline hover:text-primary duration-200">
<button
onClick={copyToClipboard}
className="pl-4 pr-4 border-l border-white/20 py-2 hover:bg-white/[0.12] duration-200"
>
{projectIdCopied ? (
<FontAwesomeIcon icon={faCheck} className="pr-0.5" />
) : (
<FontAwesomeIcon icon={faCopy} />
)}
</button>
<span className="absolute hidden group-hover:flex group-hover:animate-popup duration-300 w-28 -left-8 -top-20 translate-y-full pl-3 py-2 bg-bunker-800 rounded-md text-center text-gray-400 text-sm">
{t("common:click-to-copy")}
</span>
</div>
</div>
</div>
<div className="bg-white/5 rounded-md px-6 pt-4 flex flex-col items-start flex flex-col items-start w-full mt-4 mb-4 pt-2">
<div className="flex flex-row justify-between w-full">
<div className="flex flex-col w-full">
<p className="text-xl font-semibold mb-3">
{t("section-token:service-tokens")}
</p>
<p className="text-sm text-gray-400">
{t("section-token:service-tokens-description")}
</p>
<p className="text-sm text-gray-400 mb-4">
Please, make sure you are on the
<a
className="text-primary underline underline-offset-2 ml-1"
href="https://infisical.com/docs/cli/overview"
target="_blank"
rel="noreferrer"
>
latest version of CLI
</a>.
</p>
</div>
<div className="w-48 mt-2">
<Button
text={t("section-token:add-new")}
onButtonPressed={() => {
setIsAddServiceTokenDialogOpen(true);
}}
color="mineshaft"
icon={faPlus}
size="md"
/>
</div>
</div>
<ServiceTokenTable
data={serviceTokens}
workspaceName={workspaceName}
setServiceTokens={setServiceTokens}
/>
</div>
</div>
</div>
<div className="bg-white/5 rounded-md px-6 pt-4 pb-6 border-l border-red pl-6 flex flex-col items-start flex flex-col items-start w-full mb-6 mt-4 pb-4 pt-2">
<p className="text-xl font-bold text-red">
{t("settings-project:danger-zone")}
</p>
<p className="mt-2 text-md text-gray-400">
{t("settings-project:danger-zone-note")}
</p>
<div className="max-h-28 w-full max-w-md mr-auto mt-4">
<InputField
label={t("settings-project:project-to-delete")}
onChangeHandler={setWorkspaceToBeDeletedName}
type="varName"
value={workspaceToBeDeletedName}
placeholder=""
isRequired
/>
</div>
<button
type="button"
className="max-w-md mt-6 w-full inline-flex justify-center rounded-md border border-transparent bg-gray-800 px-4 py-2.5 text-sm font-medium text-gray-400 hover:bg-red hover:text-white hover:font-semibold hover:text-semibold duration-200 focus:outline-none focus-visible:ring-2 focus-visible:ring-blue-500 focus-visible:ring-offset-2"
onClick={executeDeletingWorkspace}
>
{t("settings-project:delete-project")}
</button>
<p className="mt-0.5 ml-1 text-xs text-gray-500">
{t("settings-project:delete-project-note")}
</p>
</div>
</div>
</div>
</div>
</div>
);
}
SettingsBasic.requireAuth = true;
export const getServerSideProps = getTranslatedServerSideProps([
"settings",
"settings-project",
"section-token",
]);

View File

@@ -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<Array<EnvData>>([]);
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 (
<div className='bg-bunker-800 max-h-screen flex flex-col justify-between text-white'>
<Head>
<title>
{t('common:head-title', { title: t('settings-project:title') })}
</title>
<link rel='icon' href='/infisical.ico' />
</Head>
<AddServiceTokenDialog
isOpen={isAddServiceTokenDialogOpen}
workspaceId={workspaceId}
environments={environments}
closeModal={closeAddServiceTokenModal}
workspaceName={workspaceName}
serviceTokens={serviceTokens}
setServiceTokens={setServiceTokens}
/>
<div className='flex flex-row mr-6 max-w-5xl'>
<div className='w-full max-h-screen pb-2 overflow-y-auto'>
<NavHeader
pageName={t('settings-project:title')}
isProjectRelated={true}
/>
<div className='flex flex-row justify-between items-center ml-6 my-8 text-xl max-w-5xl'>
<div className='flex flex-col justify-start items-start text-3xl'>
<p className='font-semibold mr-4 text-gray-200'>
{t('settings-project:title')}
</p>
<p className='font-normal mr-4 text-gray-400 text-base'>
{t('settings-project:description')}
</p>
</div>
</div>
<div className='flex flex-col ml-6 text-mineshaft-50'>
<div className='flex flex-col'>
<div className='min-w-md mt-2 flex flex-col items-start'>
<div className='bg-white/5 rounded-md px-6 pt-6 pb-4 flex flex-col items-start flex flex-col items-start w-full mb-6 pt-2'>
<p className='text-xl font-semibold mb-4 mt-2'>
{t('common:display-name')}
</p>
<div className='max-h-28 w-full max-w-md mr-auto'>
<InputField
label=''
onChangeHandler={modifyWorkspaceName}
type='varName'
value={workspaceName}
placeholder=''
isRequired
/>
</div>
<div className='flex justify-start w-full'>
<div className={`flex justify-start max-w-sm mt-4 mb-2`}>
<Button
text={t('common:save-changes') as string}
onButtonPressed={() => submitChanges(workspaceName)}
color='mineshaft'
size='md'
active={buttonReady}
iconDisabled={faCheck}
textDisabled='Saved'
/>
</div>
</div>
</div>
<div className='bg-white/5 rounded-md px-6 pt-4 pb-2 flex flex-col items-start w-full mb-6 mt-4'>
<p className='text-xl font-semibold self-start'>
{t('common:project-id')}
</p>
<p className='text-base text-gray-400 font-normal self-start mt-4'>
{t('settings-project:project-id-description')}
</p>
<p className='text-base text-gray-400 font-normal self-start'>
{t('settings-project:project-id-description2')}
{/* eslint-disable-next-line react/jsx-no-target-blank */}
<a
href='https://infisical.com/docs/getting-started/introduction'
target='_blank'
rel='noopener'
className='text-primary hover:opacity-80 duration-200'
>
{t('settings-project:docs')}
</a>
</p>
<p className='mt-4 text-xs text-bunker-300'>
{t('settings-project:auto-generated')}
</p>
<div className='flex justify-end items-center bg-white/[0.07] text-base mt-2 mb-3 mr-2 rounded-md text-gray-400'>
<p className='mr-2 font-bold pl-4'>{`${t(
'common:project-id'
)}:`}</p>
<input
type='text'
value={workspaceId}
id='myInput'
className='bg-white/0 text-gray-400 py-2 w-60 px-2 min-w-md outline-none'
disabled
></input>
<div className='group font-normal group relative inline-block text-gray-400 underline hover:text-primary duration-200'>
<button
onClick={copyToClipboard}
className='pl-4 pr-4 border-l border-white/20 py-2 hover:bg-white/[0.12] duration-200'
>
{projectIdCopied ? (
<FontAwesomeIcon icon={faCheck} className='pr-0.5' />
) : (
<FontAwesomeIcon icon={faCopy} />
)}
</button>
<span className='absolute hidden group-hover:flex group-hover:animate-popup duration-300 w-28 -left-8 -top-20 translate-y-full pl-3 py-2 bg-bunker-800 rounded-md text-center text-gray-400 text-sm'>
{t('common:click-to-copy')}
</span>
</div>
</div>
</div>
<div className='bg-white/5 rounded-md px-6 pt-6 flex flex-col items-start w-full mt-4 mb-4'>
<EnvironmentTable
data={environments}
onCreateEnv={onCreateEnvironment}
onUpdateEnv={onUpdateEnvironment}
onDeleteEnv={onDeleteEnvironment}
/>
</div>
<div className='bg-white/5 rounded-md px-6 flex flex-col items-start w-full mt-4 mb-4 pt-2'>
<div className='flex flex-row justify-between w-full'>
<div className='flex flex-col w-full'>
<p className='text-xl font-semibold mb-3'>
{t('section-token:service-tokens')}
</p>
<p className='text-sm text-gray-400'>
{t('section-token:service-tokens-description')}
</p>
<p className='text-sm text-gray-400 mb-4'>
Please, make sure you are on the
<a
className='text-primary underline underline-offset-2 ml-1'
href='https://infisical.com/docs/cli/overview'
target='_blank'
rel='noreferrer'
>
latest version of CLI
</a>
.
</p>
</div>
<div className='w-48 mt-2'>
<Button
text={t('section-token:add-new') as string}
onButtonPressed={() => {
setIsAddServiceTokenDialogOpen(true);
}}
color='mineshaft'
icon={faPlus}
size='md'
/>
</div>
</div>
<ServiceTokenTable
data={serviceTokens}
workspaceName={workspaceName}
setServiceTokens={setServiceTokens as any}
/>
</div>
</div>
</div>
<div className='bg-white/5 rounded-md px-6 border-l border-red pl-6 flex flex-col items-start w-full mb-6 mt-4 pb-4 pt-2'>
<p className='text-xl font-bold text-red'>
{t('settings-project:danger-zone')}
</p>
<p className='mt-2 text-md text-gray-400'>
{t('settings-project:danger-zone-note')}
</p>
<div className='max-h-28 w-full max-w-md mr-auto mt-4'>
<InputField
label={t('settings-project:project-to-delete')}
onChangeHandler={setWorkspaceToBeDeletedName}
type='varName'
value={workspaceToBeDeletedName}
placeholder=''
isRequired
/>
</div>
<button
type='button'
className='max-w-md mt-6 w-full inline-flex justify-center rounded-md border border-transparent bg-gray-800 px-4 py-2.5 text-sm font-medium text-gray-400 hover:bg-red hover:text-white hover:font-semibold hover:text-semibold duration-200 focus:outline-none focus-visible:ring-2 focus-visible:ring-blue-500 focus-visible:ring-offset-2'
onClick={executeDeletingWorkspace}
>
{t('settings-project:delete-project')}
</button>
<p className='mt-0.5 ml-1 text-xs text-gray-500'>
{t('settings-project:delete-project-note')}
</p>
</div>
</div>
</div>
</div>
</div>
);
}
SettingsBasic.requireAuth = true;
export const getServerSideProps = getTranslatedServerSideProps([
'settings',
'settings-project',
'section-token',
]);

View File

@@ -0,0 +1,8 @@
export interface SecretDataProps {
pos: number;
key: string;
value: string;
valueOverride: string | undefined;
id: string;
comment: string;
}

View File

@@ -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."
}

View File

@@ -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": "<wrapper>We've sent a verification email to</wrapper><email>{{email}}</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",

View File

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

View File

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

View File

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

View File

@@ -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é."
}
}
}

View File

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

View File

@@ -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."
}

View File

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

View File

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

View File

@@ -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,</0><1>cliquez ici</1>",
"add-user-to-org": "Ajouter des Utilisateurs à l'Organisation"
}
}

View File

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

View File

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

View File

@@ -0,0 +1,4 @@
{
"title": "Membres du projet",
"description": "Cette page affiche les membres du projet sélectionné."
}

View File

@@ -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."
}

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,8 @@
{
"event": {
"readSecrets": "Segredos Visualizados",
"updateSecrets": "Segredos Atualizados",
"addSecrets": "Segredos Adicionados",
"deleteSecrets": "Segredos Excluídos"
}
}

View File

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

Some files were not shown because too many files have changed in this diff Show More