Merge remote-tracking branch 'refs/remotes/origin/main' into circleci-integration-branch
3308
backend/package-lock.json
generated
@@ -1,11 +1,18 @@
|
||||
{
|
||||
"dependencies": {
|
||||
"@aws-sdk/client-secrets-manager": "^3.267.0",
|
||||
"@godaddy/terminus": "^4.11.2",
|
||||
"@sentry/node": "^7.21.1",
|
||||
"@sentry/tracing": "^7.21.1",
|
||||
"@octokit/rest": "^19.0.5",
|
||||
"@sentry/node": "^7.14.0",
|
||||
"@sentry/tracing": "^7.19.0",
|
||||
"@types/crypto-js": "^4.1.1",
|
||||
"axios": "^1.2.0",
|
||||
"@types/libsodium-wrappers": "^0.7.10",
|
||||
"await-to-js": "^3.0.0",
|
||||
"aws-sdk": "^2.1311.0",
|
||||
"axios": "^1.1.3",
|
||||
"bcrypt": "^5.1.0",
|
||||
"bigint-conversion": "^2.2.2",
|
||||
"builder-pattern": "^2.2.0",
|
||||
"cookie-parser": "^1.4.6",
|
||||
"cors": "^2.8.5",
|
||||
"crypto-js": "^4.1.1",
|
||||
@@ -15,17 +22,26 @@
|
||||
"express-validator": "^6.14.2",
|
||||
"handlebars": "^4.7.7",
|
||||
"helmet": "^5.1.1",
|
||||
"jsonwebtoken": "^8.5.1",
|
||||
"js-yaml": "^4.1.0",
|
||||
"jsonwebtoken": "^9.0.0",
|
||||
"jsrp": "^0.2.4",
|
||||
"mongoose": "^6.7.3",
|
||||
"libsodium-wrappers": "^0.7.10",
|
||||
"lodash": "^4.17.21",
|
||||
"mongoose": "^6.7.2",
|
||||
"nodemailer": "^6.8.0",
|
||||
"posthog-node": "^2.1.0",
|
||||
"posthog-node": "^2.2.2",
|
||||
"query-string": "^7.1.3",
|
||||
"request-ip": "^3.3.0",
|
||||
"rimraf": "^3.0.2",
|
||||
"stripe": "^10.7.0",
|
||||
"swagger-autogen": "^2.22.0",
|
||||
"swagger-ui-express": "^4.6.0",
|
||||
"tweetnacl": "^1.0.3",
|
||||
"tweetnacl-util": "^0.15.1",
|
||||
"typescript": "^4.9.3"
|
||||
"typescript": "^4.9.3",
|
||||
"utility-types": "^3.10.0",
|
||||
"winston": "^3.8.2",
|
||||
"winston-loki": "^6.0.6"
|
||||
},
|
||||
"name": "infisical-api",
|
||||
"version": "1.0.0",
|
||||
@@ -102,47 +118,5 @@
|
||||
"suiteNameTemplate": "{filepath}",
|
||||
"classNameTemplate": "{classname}",
|
||||
"titleTemplate": "{title}"
|
||||
},
|
||||
"dependencies": {
|
||||
"@godaddy/terminus": "^4.11.2",
|
||||
"@octokit/rest": "^19.0.5",
|
||||
"@sentry/node": "^7.14.0",
|
||||
"@sentry/tracing": "^7.19.0",
|
||||
"@types/crypto-js": "^4.1.1",
|
||||
"@types/libsodium-wrappers": "^0.7.10",
|
||||
"await-to-js": "^3.0.0",
|
||||
"axios": "^1.1.3",
|
||||
"bcrypt": "^5.1.0",
|
||||
"bigint-conversion": "^2.2.2",
|
||||
"builder-pattern": "^2.2.0",
|
||||
"cookie-parser": "^1.4.6",
|
||||
"cors": "^2.8.5",
|
||||
"crypto-js": "^4.1.1",
|
||||
"dotenv": "^16.0.1",
|
||||
"express": "^4.18.1",
|
||||
"express-rate-limit": "^6.7.0",
|
||||
"express-validator": "^6.14.2",
|
||||
"handlebars": "^4.7.7",
|
||||
"helmet": "^5.1.1",
|
||||
"js-yaml": "^4.1.0",
|
||||
"jsonwebtoken": "^9.0.0",
|
||||
"jsrp": "^0.2.4",
|
||||
"libsodium-wrappers": "^0.7.10",
|
||||
"lodash": "^4.17.21",
|
||||
"mongoose": "^6.7.2",
|
||||
"nodemailer": "^6.8.0",
|
||||
"posthog-node": "^2.2.2",
|
||||
"query-string": "^7.1.3",
|
||||
"request-ip": "^3.3.0",
|
||||
"rimraf": "^3.0.2",
|
||||
"stripe": "^10.7.0",
|
||||
"swagger-autogen": "^2.22.0",
|
||||
"swagger-ui-express": "^4.6.0",
|
||||
"tweetnacl": "^1.0.3",
|
||||
"tweetnacl-util": "^0.15.1",
|
||||
"typescript": "^4.9.3",
|
||||
"utility-types": "^3.10.0",
|
||||
"winston": "^3.8.2",
|
||||
"winston-loki": "^6.0.6"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -81,26 +81,31 @@ export const oAuthExchange = async (
|
||||
};
|
||||
|
||||
/**
|
||||
* Save integration access token as part of integration [integration] for workspace with id [workspaceId]
|
||||
* @param req
|
||||
* @param res
|
||||
* Save integration access token and (optionally) access id as part of integration
|
||||
* [integration] for workspace with id [workspaceId]
|
||||
* @param req
|
||||
* @param res
|
||||
*/
|
||||
export const saveIntegrationAccessToken = async (
|
||||
req: Request,
|
||||
res: Response
|
||||
) => {
|
||||
// TODO: refactor
|
||||
let integrationAuth;
|
||||
try {
|
||||
const {
|
||||
workspaceId,
|
||||
accessToken,
|
||||
integration,
|
||||
}: {
|
||||
workspaceId: string;
|
||||
accessToken: string;
|
||||
integration: string;
|
||||
} = req.body;
|
||||
// TODO: refactor
|
||||
// TODO: check if access token is valid for each integration
|
||||
|
||||
let integrationAuth;
|
||||
try {
|
||||
const {
|
||||
workspaceId,
|
||||
accessId,
|
||||
accessToken,
|
||||
integration
|
||||
}: {
|
||||
workspaceId: string;
|
||||
accessId: string | null;
|
||||
accessToken: string;
|
||||
integration: string;
|
||||
} = req.body;
|
||||
|
||||
const bot = await Bot.findOne({
|
||||
workspace: new Types.ObjectId(workspaceId),
|
||||
@@ -120,9 +125,10 @@ export const saveIntegrationAccessToken = async (
|
||||
upsert: true
|
||||
});
|
||||
|
||||
// encrypt and save integration access token
|
||||
// encrypt and save integration access details
|
||||
integrationAuth = await IntegrationService.setIntegrationAuthAccess({
|
||||
integrationAuthId: integrationAuth._id.toString(),
|
||||
accessId,
|
||||
accessToken,
|
||||
accessExpiresAt: undefined
|
||||
});
|
||||
|
||||
@@ -26,7 +26,9 @@ export const createIntegration = async (req: Request, res: Response) => {
|
||||
isActive,
|
||||
sourceEnvironment,
|
||||
targetEnvironment,
|
||||
owner
|
||||
owner,
|
||||
path,
|
||||
region
|
||||
} = req.body;
|
||||
|
||||
// TODO: validate [sourceEnvironment] and [targetEnvironment]
|
||||
@@ -40,6 +42,8 @@ export const createIntegration = async (req: Request, res: Response) => {
|
||||
appId,
|
||||
targetEnvironment,
|
||||
owner,
|
||||
path,
|
||||
region,
|
||||
integration: req.integrationAuth.integration,
|
||||
integrationAuth: new Types.ObjectId(integrationAuthId)
|
||||
}).save();
|
||||
|
||||
@@ -452,7 +452,7 @@ export const updateSecrets = async (req: Request, res: Response) => {
|
||||
secretValueTag,
|
||||
tags,
|
||||
...((
|
||||
secretCommentCiphertext &&
|
||||
secretCommentCiphertext !== undefined &&
|
||||
secretCommentIV &&
|
||||
secretCommentTag
|
||||
) ? {
|
||||
|
||||
@@ -2,7 +2,7 @@ import { Request, Response } from 'express';
|
||||
import * as Sentry from '@sentry/node';
|
||||
import { Types } from 'mongoose';
|
||||
import {
|
||||
Membership,
|
||||
Membership, Secret,
|
||||
} from '../../models';
|
||||
import Tag, { ITag } from '../../models/tag';
|
||||
import { Builder } from "builder-pattern"
|
||||
@@ -54,6 +54,12 @@ export const deleteWorkspaceTag = async (req: Request, res: Response) => {
|
||||
|
||||
const result = await Tag.findByIdAndDelete(tagId);
|
||||
|
||||
// remove the tag from secrets
|
||||
await Secret.updateMany(
|
||||
{ tags: { $in: [tagId] } },
|
||||
{ $pull: { tags: tagId } }
|
||||
);
|
||||
|
||||
res.json(result);
|
||||
}
|
||||
|
||||
|
||||
@@ -94,6 +94,7 @@ const handleOAuthExchangeHelper = async ({
|
||||
// set integration auth access token
|
||||
await setIntegrationAuthAccessHelper({
|
||||
integrationAuthId: integrationAuth._id.toString(),
|
||||
accessId: null,
|
||||
accessToken: res.accessToken,
|
||||
accessExpiresAt: res.accessExpiresAt
|
||||
});
|
||||
@@ -138,7 +139,7 @@ const syncIntegrationsHelper = async ({
|
||||
if (!integrationAuth) throw new Error('Failed to find integration auth');
|
||||
|
||||
// get integration auth access token
|
||||
const accessToken = await getIntegrationAuthAccessHelper({
|
||||
const access = await getIntegrationAuthAccessHelper({
|
||||
integrationAuthId: integration.integrationAuth.toString()
|
||||
});
|
||||
|
||||
@@ -147,7 +148,8 @@ const syncIntegrationsHelper = async ({
|
||||
integration,
|
||||
integrationAuth,
|
||||
secrets,
|
||||
accessToken
|
||||
accessId: access.accessId,
|
||||
accessToken: access.accessToken
|
||||
});
|
||||
}
|
||||
} catch (err) {
|
||||
@@ -203,12 +205,12 @@ const syncIntegrationsHelper = async ({
|
||||
* @returns {String} accessToken - decrypted access token
|
||||
*/
|
||||
const getIntegrationAuthAccessHelper = async ({ integrationAuthId }: { integrationAuthId: string }) => {
|
||||
let accessId;
|
||||
let accessToken;
|
||||
|
||||
try {
|
||||
const integrationAuth = await IntegrationAuth
|
||||
.findById(integrationAuthId)
|
||||
.select('workspace integration +accessCiphertext +accessIV +accessTag +accessExpiresAt + refreshCiphertext');
|
||||
.select('workspace integration +accessCiphertext +accessIV +accessTag +accessExpiresAt + refreshCiphertext +accessIdCiphertext +accessIdIV +accessIdTag');
|
||||
|
||||
if (!integrationAuth) throw UnauthorizedRequestError({message: 'Failed to locate Integration Authentication credentials'});
|
||||
|
||||
@@ -232,6 +234,15 @@ const getIntegrationAuthAccessHelper = async ({ integrationAuthId }: { integrati
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (integrationAuth?.accessIdCiphertext && integrationAuth?.accessIdIV && integrationAuth?.accessIdTag) {
|
||||
accessId = await BotService.decryptSymmetric({
|
||||
workspaceId: integrationAuth.workspace.toString(),
|
||||
ciphertext: integrationAuth.accessIdCiphertext as string,
|
||||
iv: integrationAuth.accessIdIV as string,
|
||||
tag: integrationAuth.accessIdTag as string
|
||||
});
|
||||
}
|
||||
|
||||
} catch (err) {
|
||||
Sentry.setUser(null);
|
||||
@@ -242,7 +253,10 @@ const getIntegrationAuthAccessHelper = async ({ integrationAuthId }: { integrati
|
||||
throw new Error('Failed to get integration access token');
|
||||
}
|
||||
|
||||
return accessToken;
|
||||
return ({
|
||||
accessId,
|
||||
accessToken
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -292,9 +306,9 @@ const setIntegrationAuthRefreshHelper = async ({
|
||||
}
|
||||
|
||||
/**
|
||||
* Encrypt access token [accessToken] using the bot's copy
|
||||
* of the workspace key for workspace belonging to integration auth
|
||||
* with id [integrationAuthId] and store it along with [accessExpiresAt]
|
||||
* Encrypt access token [accessToken] and (optionally) access id [accessId]
|
||||
* using the bot's copy of the workspace key for workspace belonging to
|
||||
* integration auth with id [integrationAuthId] and store it along with [accessExpiresAt]
|
||||
* @param {Object} obj
|
||||
* @param {String} obj.integrationAuthId - id of integration auth
|
||||
* @param {String} obj.accessToken - access token
|
||||
@@ -302,10 +316,12 @@ const setIntegrationAuthRefreshHelper = async ({
|
||||
*/
|
||||
const setIntegrationAuthAccessHelper = async ({
|
||||
integrationAuthId,
|
||||
accessId,
|
||||
accessToken,
|
||||
accessExpiresAt
|
||||
}: {
|
||||
integrationAuthId: string;
|
||||
accessId: string | null;
|
||||
accessToken: string;
|
||||
accessExpiresAt: Date | undefined;
|
||||
}) => {
|
||||
@@ -315,17 +331,28 @@ const setIntegrationAuthAccessHelper = async ({
|
||||
|
||||
if (!integrationAuth) throw new Error('Failed to find integration auth');
|
||||
|
||||
const obj = await BotService.encryptSymmetric({
|
||||
const encryptedAccessTokenObj = await BotService.encryptSymmetric({
|
||||
workspaceId: integrationAuth.workspace.toString(),
|
||||
plaintext: accessToken
|
||||
});
|
||||
|
||||
let encryptedAccessIdObj;
|
||||
if (accessId) {
|
||||
encryptedAccessIdObj = await BotService.encryptSymmetric({
|
||||
workspaceId: integrationAuth.workspace.toString(),
|
||||
plaintext: accessId
|
||||
});
|
||||
}
|
||||
|
||||
integrationAuth = await IntegrationAuth.findOneAndUpdate({
|
||||
_id: integrationAuthId
|
||||
}, {
|
||||
accessCiphertext: obj.ciphertext,
|
||||
accessIV: obj.iv,
|
||||
accessTag: obj.tag,
|
||||
accessIdCiphertext: encryptedAccessIdObj?.ciphertext ?? undefined,
|
||||
accessIdIV: encryptedAccessIdObj?.iv ?? undefined,
|
||||
accessIdTag: encryptedAccessIdObj?.tag ?? undefined,
|
||||
accessCiphertext: encryptedAccessTokenObj.ciphertext,
|
||||
accessIV: encryptedAccessTokenObj.iv,
|
||||
accessTag: encryptedAccessTokenObj.tag,
|
||||
accessExpiresAt
|
||||
}, {
|
||||
new: true
|
||||
|
||||
@@ -4,6 +4,8 @@ import { Octokit } from "@octokit/rest";
|
||||
import { IIntegrationAuth } from "../models";
|
||||
import {
|
||||
INTEGRATION_AZURE_KEY_VAULT,
|
||||
INTEGRATION_AWS_PARAMETER_STORE,
|
||||
INTEGRATION_AWS_SECRET_MANAGER,
|
||||
INTEGRATION_HEROKU,
|
||||
INTEGRATION_VERCEL,
|
||||
INTEGRATION_NETLIFY,
|
||||
@@ -44,10 +46,14 @@ const getApps = async ({
|
||||
try {
|
||||
switch (integrationAuth.integration) {
|
||||
case INTEGRATION_AZURE_KEY_VAULT:
|
||||
apps = await getAppsAzureKeyVault({
|
||||
accessToken
|
||||
});
|
||||
break;
|
||||
apps = [];
|
||||
break;
|
||||
case INTEGRATION_AWS_PARAMETER_STORE:
|
||||
apps = [];
|
||||
break;
|
||||
case INTEGRATION_AWS_SECRET_MANAGER:
|
||||
apps = [];
|
||||
break;
|
||||
case INTEGRATION_HEROKU:
|
||||
apps = await getAppsHeroku({
|
||||
accessToken,
|
||||
@@ -94,15 +100,6 @@ const getApps = async ({
|
||||
return apps;
|
||||
};
|
||||
|
||||
const getAppsAzureKeyVault = async ({
|
||||
accessToken
|
||||
}: {
|
||||
accessToken: string;
|
||||
}) => {
|
||||
// TODO
|
||||
return [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Return list of apps for Heroku integration
|
||||
* @param {Object} obj
|
||||
@@ -154,6 +151,7 @@ const getAppsVercel = async ({
|
||||
await axios.get(`${INTEGRATION_VERCEL_API_URL}/v9/projects`, {
|
||||
headers: {
|
||||
Authorization: `Bearer ${accessToken}`,
|
||||
'Accept-Encoding': 'application/json'
|
||||
},
|
||||
...(integrationAuth?.teamId
|
||||
? {
|
||||
@@ -191,7 +189,8 @@ const getAppsNetlify = async ({ accessToken }: { accessToken: string }) => {
|
||||
await axios.get(`${INTEGRATION_NETLIFY_API_URL}/api/v1/sites`, {
|
||||
headers: {
|
||||
Authorization: `Bearer ${accessToken}`,
|
||||
},
|
||||
'Accept-Encoding': 'application/json'
|
||||
}
|
||||
})
|
||||
).data;
|
||||
|
||||
|
||||
@@ -141,7 +141,7 @@ const exchangeCodeAzure = async ({
|
||||
new URLSearchParams({
|
||||
grant_type: 'authorization_code',
|
||||
code: code,
|
||||
scope: 'https://vault.azure.net/.default openid offline_access', // TODO: do we need all these permissions?
|
||||
scope: 'https://vault.azure.net/.default openid offline_access',
|
||||
client_id: CLIENT_ID_AZURE,
|
||||
client_secret: CLIENT_SECRET_AZURE,
|
||||
redirect_uri: `${SITE_URL}/integrations/azure-key-vault/oauth2/callback`
|
||||
|
||||
@@ -1,10 +1,21 @@
|
||||
import axios from "axios";
|
||||
import * as Sentry from "@sentry/node";
|
||||
import _ from 'lodash';
|
||||
import AWS from 'aws-sdk';
|
||||
import {
|
||||
SecretsManagerClient,
|
||||
UpdateSecretCommand,
|
||||
CreateSecretCommand,
|
||||
GetSecretValueCommand,
|
||||
ResourceNotFoundException
|
||||
} from '@aws-sdk/client-secrets-manager';
|
||||
import { Octokit } from "@octokit/rest";
|
||||
import sodium from "libsodium-wrappers";
|
||||
import { IIntegration, IIntegrationAuth } from "../models";
|
||||
import {
|
||||
INTEGRATION_AZURE_KEY_VAULT,
|
||||
INTEGRATION_AWS_PARAMETER_STORE,
|
||||
INTEGRATION_AWS_SECRET_MANAGER,
|
||||
INTEGRATION_HEROKU,
|
||||
INTEGRATION_VERCEL,
|
||||
INTEGRATION_NETLIFY,
|
||||
@@ -27,17 +38,20 @@ import { access, appendFile } from "fs";
|
||||
* @param {IIntegration} obj.integration - integration details
|
||||
* @param {IIntegrationAuth} obj.integrationAuth - integration auth details
|
||||
* @param {Object} obj.secrets - secrets to push to integration (object where keys are secret keys and values are secret values)
|
||||
* @param {String} obj.accessId - access id for integration
|
||||
* @param {String} obj.accessToken - access token for integration
|
||||
*/
|
||||
const syncSecrets = async ({
|
||||
integration,
|
||||
integrationAuth,
|
||||
secrets,
|
||||
accessId,
|
||||
accessToken,
|
||||
}: {
|
||||
integration: IIntegration;
|
||||
integrationAuth: IIntegrationAuth;
|
||||
secrets: any;
|
||||
accessId: string | null;
|
||||
accessToken: string;
|
||||
}) => {
|
||||
try {
|
||||
@@ -49,6 +63,22 @@ const syncSecrets = async ({
|
||||
accessToken
|
||||
});
|
||||
break;
|
||||
case INTEGRATION_AWS_PARAMETER_STORE:
|
||||
await syncSecretsAWSParameterStore({
|
||||
integration,
|
||||
secrets,
|
||||
accessId,
|
||||
accessToken
|
||||
});
|
||||
break;
|
||||
case INTEGRATION_AWS_SECRET_MANAGER:
|
||||
await syncSecretsAWSSecretManager({
|
||||
integration,
|
||||
secrets,
|
||||
accessId,
|
||||
accessToken
|
||||
});
|
||||
break;
|
||||
case INTEGRATION_HEROKU:
|
||||
await syncSecretsHeroku({
|
||||
integration,
|
||||
@@ -252,6 +282,188 @@ const syncSecretsAzureKeyVault = async ({
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Sync/push [secrets] to AWS parameter store
|
||||
* @param {Object} obj
|
||||
* @param {IIntegration} obj.integration - integration details
|
||||
* @param {Object} obj.secrets - secrets to push to integration (object where keys are secret keys and values are secret values)
|
||||
* @param {String} obj.accessId - access id for AWS parameter store integration
|
||||
* @param {String} obj.accessToken - access token for AWS parameter store integration
|
||||
*/
|
||||
const syncSecretsAWSParameterStore = async ({
|
||||
integration,
|
||||
secrets,
|
||||
accessId,
|
||||
accessToken
|
||||
}: {
|
||||
integration: IIntegration;
|
||||
secrets: any;
|
||||
accessId: string | null;
|
||||
accessToken: string;
|
||||
}) => {
|
||||
try {
|
||||
if (!accessId) return;
|
||||
|
||||
AWS.config.update({
|
||||
region: integration.region,
|
||||
accessKeyId: accessId,
|
||||
secretAccessKey: accessToken
|
||||
});
|
||||
|
||||
const ssm = new AWS.SSM({
|
||||
apiVersion: '2014-11-06',
|
||||
region: integration.region
|
||||
});
|
||||
|
||||
const params = {
|
||||
Path: integration.path,
|
||||
Recursive: true,
|
||||
WithDecryption: true
|
||||
};
|
||||
|
||||
const parameterList = (await ssm.getParametersByPath(params).promise()).Parameters
|
||||
|
||||
let awsParameterStoreSecretsObj: {
|
||||
[key: string]: any // TODO: fix type
|
||||
} = {};
|
||||
|
||||
if (parameterList) {
|
||||
awsParameterStoreSecretsObj = parameterList.reduce((obj: any, secret: any) => ({
|
||||
...obj,
|
||||
[secret.Name.split("/").pop()]: secret
|
||||
}), {});
|
||||
}
|
||||
|
||||
// Identify secrets to create
|
||||
Object.keys(secrets).map(async (key) => {
|
||||
if (!(key in awsParameterStoreSecretsObj)) {
|
||||
// case: secret does not exist in AWS parameter store
|
||||
// -> create secret
|
||||
await ssm.putParameter({
|
||||
Name: `${integration.path}${key}`,
|
||||
Type: 'SecureString',
|
||||
Value: secrets[key],
|
||||
Overwrite: true
|
||||
}).promise();
|
||||
} else {
|
||||
// case: secret exists in AWS parameter store
|
||||
|
||||
if (awsParameterStoreSecretsObj[key].Value !== secrets[key]) {
|
||||
// case: secret value doesn't match one in AWS parameter store
|
||||
// -> update secret
|
||||
await ssm.putParameter({
|
||||
Name: `${integration.path}${key}`,
|
||||
Type: 'SecureString',
|
||||
Value: secrets[key],
|
||||
Overwrite: true
|
||||
}).promise();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Identify secrets to delete
|
||||
Object.keys(awsParameterStoreSecretsObj).map(async (key) => {
|
||||
if (!(key in secrets)) {
|
||||
// case:
|
||||
// -> delete secret
|
||||
await ssm.deleteParameter({
|
||||
Name: awsParameterStoreSecretsObj[key].Name
|
||||
}).promise();
|
||||
}
|
||||
});
|
||||
|
||||
AWS.config.update({
|
||||
region: undefined,
|
||||
accessKeyId: undefined,
|
||||
secretAccessKey: undefined
|
||||
});
|
||||
} catch (err) {
|
||||
Sentry.setUser(null);
|
||||
Sentry.captureException(err);
|
||||
throw new Error('Failed to sync secrets to AWS Parameter Store');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sync/push [secrets] to AWS secret manager
|
||||
* @param {Object} obj
|
||||
* @param {IIntegration} obj.integration - integration details
|
||||
* @param {Object} obj.secrets - secrets to push to integration (object where keys are secret keys and values are secret values)
|
||||
* @param {String} obj.accessId - access id for AWS secret manager integration
|
||||
* @param {String} obj.accessToken - access token for AWS secret manager integration
|
||||
*/
|
||||
const syncSecretsAWSSecretManager = async ({
|
||||
integration,
|
||||
secrets,
|
||||
accessId,
|
||||
accessToken
|
||||
}: {
|
||||
integration: IIntegration;
|
||||
secrets: any;
|
||||
accessId: string | null;
|
||||
accessToken: string;
|
||||
}) => {
|
||||
let secretsManager;
|
||||
try {
|
||||
if (!accessId) return;
|
||||
|
||||
AWS.config.update({
|
||||
region: integration.region,
|
||||
accessKeyId: accessId,
|
||||
secretAccessKey: accessToken
|
||||
});
|
||||
|
||||
secretsManager = new SecretsManagerClient({
|
||||
region: integration.region,
|
||||
credentials: {
|
||||
accessKeyId: accessId,
|
||||
secretAccessKey: accessToken
|
||||
}
|
||||
});
|
||||
|
||||
const awsSecretManagerSecret = await secretsManager.send(
|
||||
new GetSecretValueCommand({
|
||||
SecretId: integration.app
|
||||
})
|
||||
);
|
||||
|
||||
let awsSecretManagerSecretObj: { [key: string]: any } = {};
|
||||
|
||||
if (awsSecretManagerSecret?.SecretString) {
|
||||
awsSecretManagerSecretObj = JSON.parse(awsSecretManagerSecret.SecretString);
|
||||
}
|
||||
|
||||
if (!_.isEqual(awsSecretManagerSecretObj, secrets)) {
|
||||
await secretsManager.send(new UpdateSecretCommand({
|
||||
SecretId: integration.app,
|
||||
SecretString: JSON.stringify(secrets)
|
||||
}));
|
||||
}
|
||||
|
||||
AWS.config.update({
|
||||
region: undefined,
|
||||
accessKeyId: undefined,
|
||||
secretAccessKey: undefined
|
||||
});
|
||||
} catch (err) {
|
||||
if (err instanceof ResourceNotFoundException && secretsManager) {
|
||||
await secretsManager.send(new CreateSecretCommand({
|
||||
Name: integration.app,
|
||||
SecretString: JSON.stringify(secrets)
|
||||
}));
|
||||
} else {
|
||||
Sentry.setUser(null);
|
||||
Sentry.captureException(err);
|
||||
throw new Error('Failed to sync secrets to AWS Secret Manager');
|
||||
}
|
||||
AWS.config.update({
|
||||
region: undefined,
|
||||
accessKeyId: undefined,
|
||||
secretAccessKey: undefined
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sync/push [secrets] to Heroku app named [integration.app]
|
||||
* @param {Object} obj
|
||||
@@ -349,40 +561,30 @@ const syncSecretsVercel = async ({
|
||||
{
|
||||
params,
|
||||
headers: {
|
||||
Authorization: `Bearer ${accessToken}`,
|
||||
},
|
||||
}
|
||||
)
|
||||
).data.envs
|
||||
.filter((secret: VercelSecret) =>
|
||||
secret.target.includes(integration.targetEnvironment)
|
||||
)
|
||||
.map(
|
||||
async (secret: VercelSecret) =>
|
||||
(
|
||||
await axios.get(
|
||||
`${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,
|
||||
}),
|
||||
{}
|
||||
);
|
||||
Authorization: `Bearer ${accessToken}`
|
||||
}
|
||||
}
|
||||
))
|
||||
.data
|
||||
.envs
|
||||
.filter((secret: VercelSecret) => secret.target.includes(integration.targetEnvironment))
|
||||
.map(async (secret: VercelSecret) => (await axios.get(
|
||||
`${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[] = [];
|
||||
const updateSecrets: VercelSecret[] = [];
|
||||
const deleteSecrets: VercelSecret[] = [];
|
||||
const newSecrets: VercelSecret[] = [];
|
||||
|
||||
// Identify secrets to create
|
||||
Object.keys(secrets).map((key) => {
|
||||
|
||||
@@ -45,9 +45,10 @@ const requireIntegrationAuthorizationAuth = ({
|
||||
|
||||
req.integrationAuth = integrationAuth;
|
||||
if (attachAccessToken) {
|
||||
req.accessToken = await IntegrationService.getIntegrationAuthAccess({
|
||||
const access = await IntegrationService.getIntegrationAuthAccess({
|
||||
integrationAuthId: integrationAuth._id.toString()
|
||||
});
|
||||
req.accessToken = access.accessToken;
|
||||
}
|
||||
|
||||
return next();
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import { Schema, model, Types } from "mongoose";
|
||||
import {
|
||||
INTEGRATION_AZURE_KEY_VAULT,
|
||||
INTEGRATION_AWS_PARAMETER_STORE,
|
||||
INTEGRATION_AWS_SECRET_MANAGER,
|
||||
INTEGRATION_HEROKU,
|
||||
INTEGRATION_VERCEL,
|
||||
INTEGRATION_NETLIFY,
|
||||
@@ -19,15 +21,19 @@ export interface IIntegration {
|
||||
owner: string;
|
||||
targetEnvironment: string;
|
||||
appId: string;
|
||||
path: string;
|
||||
region: string;
|
||||
integration:
|
||||
| "heroku"
|
||||
| "vercel"
|
||||
| "netlify"
|
||||
| "github"
|
||||
| "render"
|
||||
| "flyio"
|
||||
| "azure-key-vault"
|
||||
| "circleci";
|
||||
| 'azure-key-vault'
|
||||
| 'aws-parameter-store'
|
||||
| 'aws-secret-manager'
|
||||
| 'heroku'
|
||||
| 'vercel'
|
||||
| 'netlify'
|
||||
| 'github'
|
||||
| 'render'
|
||||
| 'flyio'
|
||||
| 'circleci';
|
||||
integrationAuth: Types.ObjectId;
|
||||
}
|
||||
|
||||
@@ -68,10 +74,32 @@ const integrationSchema = new Schema<IIntegration>(
|
||||
type: String,
|
||||
default: null,
|
||||
},
|
||||
path: {
|
||||
// aws-parameter-store-specific path
|
||||
type: String,
|
||||
default: null
|
||||
},
|
||||
region: {
|
||||
// aws-parameter-store-specific path
|
||||
type: String,
|
||||
default: null
|
||||
},
|
||||
path: {
|
||||
// aws-parameter-store-specific path
|
||||
type: String,
|
||||
default: null
|
||||
},
|
||||
region: {
|
||||
// aws-parameter-store-specific path
|
||||
type: String,
|
||||
default: null
|
||||
},
|
||||
integration: {
|
||||
type: String,
|
||||
enum: [
|
||||
INTEGRATION_AZURE_KEY_VAULT,
|
||||
INTEGRATION_AWS_PARAMETER_STORE,
|
||||
INTEGRATION_AWS_SECRET_MANAGER,
|
||||
INTEGRATION_HEROKU,
|
||||
INTEGRATION_VERCEL,
|
||||
INTEGRATION_NETLIFY,
|
||||
|
||||
@@ -1,30 +1,29 @@
|
||||
import { Schema, model, Types } from "mongoose";
|
||||
import {
|
||||
INTEGRATION_AZURE_KEY_VAULT,
|
||||
INTEGRATION_AWS_PARAMETER_STORE,
|
||||
INTEGRATION_AWS_SECRET_MANAGER,
|
||||
INTEGRATION_HEROKU,
|
||||
INTEGRATION_VERCEL,
|
||||
INTEGRATION_NETLIFY,
|
||||
INTEGRATION_GITHUB,
|
||||
INTEGRATION_RENDER,
|
||||
INTEGRATION_FLYIO,
|
||||
INTEGRATION_CIRCLECI,
|
||||
} from "../variables";
|
||||
|
||||
export interface IIntegrationAuth {
|
||||
_id: Types.ObjectId;
|
||||
workspace: Types.ObjectId;
|
||||
integration:
|
||||
| "heroku"
|
||||
| "vercel"
|
||||
| "netlify"
|
||||
| "github"
|
||||
| "render"
|
||||
| "flyio"
|
||||
| "azure-key-vault"
|
||||
| "circleci";
|
||||
integration: 'heroku' | 'vercel' | 'netlify' | 'github' | 'render' | 'flyio' | 'azure-key-vault' | 'circleci' | 'aws-parameter-store' | 'aws-secret-manager';
|
||||
teamId: string;
|
||||
accountId: string;
|
||||
refreshCiphertext?: string;
|
||||
refreshIV?: string;
|
||||
refreshTag?: string;
|
||||
accessIdCiphertext?: string; // new
|
||||
accessIdIV?: string; // new
|
||||
accessIdTag?: string; // new
|
||||
accessCiphertext?: string;
|
||||
accessIV?: string;
|
||||
accessTag?: string;
|
||||
@@ -42,10 +41,14 @@ const integrationAuthSchema = new Schema<IIntegrationAuth>(
|
||||
type: String,
|
||||
enum: [
|
||||
INTEGRATION_AZURE_KEY_VAULT,
|
||||
INTEGRATION_AWS_PARAMETER_STORE,
|
||||
INTEGRATION_AWS_SECRET_MANAGER,
|
||||
INTEGRATION_HEROKU,
|
||||
INTEGRATION_VERCEL,
|
||||
INTEGRATION_NETLIFY,
|
||||
INTEGRATION_GITHUB,
|
||||
INTEGRATION_RENDER,
|
||||
INTEGRATION_FLYIO,
|
||||
INTEGRATION_CIRCLECI,
|
||||
],
|
||||
required: true,
|
||||
@@ -70,6 +73,30 @@ const integrationAuthSchema = new Schema<IIntegrationAuth>(
|
||||
type: String,
|
||||
select: false,
|
||||
},
|
||||
accessIdCiphertext: {
|
||||
type: String,
|
||||
select: false
|
||||
},
|
||||
accessIdIV: {
|
||||
type: String,
|
||||
select: false
|
||||
},
|
||||
accessIdTag: {
|
||||
type: String,
|
||||
select: false
|
||||
},
|
||||
accessIdCiphertext: {
|
||||
type: String,
|
||||
select: false
|
||||
},
|
||||
accessIdIV: {
|
||||
type: String,
|
||||
select: false
|
||||
},
|
||||
accessIdTag: {
|
||||
type: String,
|
||||
select: false
|
||||
},
|
||||
accessCiphertext: {
|
||||
type: String,
|
||||
select: false,
|
||||
|
||||
@@ -109,6 +109,9 @@ const secretSchema = new Schema<ISecret>(
|
||||
}
|
||||
);
|
||||
|
||||
|
||||
secretSchema.index({ tags: 1 }, { background: true })
|
||||
|
||||
const Secret = model<ISecret>('Secret', secretSchema);
|
||||
|
||||
export default Secret;
|
||||
|
||||
@@ -20,12 +20,14 @@ router.post( // new: add new integration for integration auth
|
||||
location: 'body'
|
||||
}),
|
||||
body('integrationAuthId').exists().isString().trim(),
|
||||
body('app').isString().trim(),
|
||||
body('app').trim(),
|
||||
body('isActive').exists().isBoolean(),
|
||||
body('appId').trim(),
|
||||
body('sourceEnvironment').trim(),
|
||||
body('targetEnvironment').trim(),
|
||||
body('owner').trim(),
|
||||
body('path').trim(),
|
||||
body('region').trim(),
|
||||
validateRequest,
|
||||
integrationController.createIntegration
|
||||
);
|
||||
|
||||
@@ -57,6 +57,7 @@ router.post(
|
||||
location: 'body'
|
||||
}),
|
||||
body('workspaceId').exists().trim().notEmpty(),
|
||||
body('accessId').trim(),
|
||||
body('accessToken').exists().trim().notEmpty(),
|
||||
body('integration').exists().trim().notEmpty(),
|
||||
validateRequest,
|
||||
|
||||
@@ -112,26 +112,30 @@ class IntegrationService {
|
||||
}
|
||||
|
||||
/**
|
||||
* Encrypt access token [accessToken] using the bot's copy
|
||||
* of the workspace key for workspace belonging to integration auth
|
||||
* Encrypt access token [accessToken] and (optionally) access id using the
|
||||
* bot's copy of the workspace key for workspace belonging to integration auth
|
||||
* with id [integrationAuthId]
|
||||
* @param {Object} obj
|
||||
* @param {String} obj.integrationAuthId - id of integration auth
|
||||
* @param {String} obj.accessId - access id
|
||||
* @param {String} obj.accessToken - access token
|
||||
* @param {Date} obj.accessExpiresAt - expiration date of access token
|
||||
* @returns {IntegrationAuth} - updated integration auth
|
||||
*/
|
||||
static async setIntegrationAuthAccess({
|
||||
integrationAuthId,
|
||||
accessId,
|
||||
accessToken,
|
||||
accessExpiresAt
|
||||
}: {
|
||||
integrationAuthId: string;
|
||||
accessId: string | null;
|
||||
accessToken: string;
|
||||
accessExpiresAt: Date | undefined;
|
||||
}) {
|
||||
return await setIntegrationAuthAccessHelper({
|
||||
integrationAuthId,
|
||||
accessId,
|
||||
accessToken,
|
||||
accessExpiresAt
|
||||
});
|
||||
|
||||
@@ -6,10 +6,9 @@
|
||||
<title>Email Verification</title>
|
||||
</head>
|
||||
<body>
|
||||
<h2>Infisical</h2>
|
||||
<h2>Confirm your email address</h2>
|
||||
<p>Your confirmation code is below — enter it in the browser window where you've started signing up for Infisical.</p>
|
||||
<h2>{{code}}</h2>
|
||||
<h1>{{code}}</h1>
|
||||
<p>Questions about setting up Infisical? Email us at support@infisical.com</p>
|
||||
</body>
|
||||
</html>
|
||||
@@ -7,12 +7,10 @@
|
||||
<title>Organization Invitation</title>
|
||||
</head>
|
||||
<body>
|
||||
<h2>Infisical</h2>
|
||||
<h2>Join your team on Infisical</h2>
|
||||
<p>{{inviterFirstName}}({{inviterEmail}}) has invited you to their Infisical organization — {{organizationName}}</p>
|
||||
<h2>Join your organization on Infisical</h2>
|
||||
<p>{{inviterFirstName}} ({{inviterEmail}}) has invited you to their Infisical organization — {{organizationName}}</p>
|
||||
<a href="{{callback_url}}?token={{token}}&to={{email}}">Join now</a>
|
||||
<h3>What is Infisical?</h3>
|
||||
<p>Infisical is a simple end-to-end encrypted solution that enables teams to sync and manage their environment
|
||||
variables.</p>
|
||||
<p>Infisical is an easy-to-use end-to-end encrypted tool that enables developers to sync and manage their secrets and configs.</p>
|
||||
</body>
|
||||
</html>
|
||||
@@ -6,7 +6,6 @@
|
||||
<title>Account Recovery</title>
|
||||
</head>
|
||||
<body>
|
||||
<h2>Infisical</h2>
|
||||
<h2>Reset your password</h2>
|
||||
<p>Someone requested a password reset.</p>
|
||||
<a href="{{callback_url}}?token={{token}}&to={{email}}">Reset password</a>
|
||||
|
||||
@@ -6,11 +6,10 @@
|
||||
<title>Project Invitation</title>
|
||||
</head>
|
||||
<body>
|
||||
<h2>Infisical</h2>
|
||||
<h2>Join your team on Infisical</h2>
|
||||
<p>{{inviterFirstName}}({{inviterEmail}}) has invited you to their Infisical workspace — {{workspaceName}}</p>
|
||||
<p>{{inviterFirstName}} ({{inviterEmail}}) has invited you to their Infisical project — {{workspaceName}}</p>
|
||||
<a href="{{callback_url}}">Join now</a>
|
||||
<h3>What is Infisical?</h3>
|
||||
<p>Infisical is a simple end-to-end encrypted solution that enables teams to sync and manage their environment variables.</p>
|
||||
<p>Infisical is an easy-to-use end-to-end encrypted tool that enables developers to sync and manage their secrets and configs.</p>
|
||||
</body>
|
||||
</html>
|
||||
@@ -7,6 +7,8 @@ import {
|
||||
} from "./environment";
|
||||
import {
|
||||
INTEGRATION_AZURE_KEY_VAULT,
|
||||
INTEGRATION_AWS_PARAMETER_STORE,
|
||||
INTEGRATION_AWS_SECRET_MANAGER,
|
||||
INTEGRATION_HEROKU,
|
||||
INTEGRATION_VERCEL,
|
||||
INTEGRATION_NETLIFY,
|
||||
@@ -57,6 +59,8 @@ export {
|
||||
ENV_PROD,
|
||||
ENV_SET,
|
||||
INTEGRATION_AZURE_KEY_VAULT,
|
||||
INTEGRATION_AWS_PARAMETER_STORE,
|
||||
INTEGRATION_AWS_SECRET_MANAGER,
|
||||
INTEGRATION_HEROKU,
|
||||
INTEGRATION_VERCEL,
|
||||
INTEGRATION_NETLIFY,
|
||||
|
||||
@@ -11,6 +11,8 @@ import {
|
||||
|
||||
// integrations
|
||||
const INTEGRATION_AZURE_KEY_VAULT = 'azure-key-vault';
|
||||
const INTEGRATION_AWS_PARAMETER_STORE = 'aws-parameter-store';
|
||||
const INTEGRATION_AWS_SECRET_MANAGER = 'aws-secret-manager';
|
||||
const INTEGRATION_HEROKU = "heroku";
|
||||
const INTEGRATION_VERCEL = "vercel";
|
||||
const INTEGRATION_NETLIFY = "netlify";
|
||||
@@ -50,6 +52,79 @@ const INTEGRATION_FLYIO_API_URL = "https://api.fly.io/graphql";
|
||||
const INTEGRATION_CIRCLECI_API_URL = "https://circleci.com/api";
|
||||
|
||||
const INTEGRATION_OPTIONS = [
|
||||
{
|
||||
name: 'Heroku',
|
||||
slug: 'heroku',
|
||||
image: 'Heroku.png',
|
||||
isAvailable: true,
|
||||
type: 'oauth',
|
||||
clientId: CLIENT_ID_HEROKU,
|
||||
docsLink: ''
|
||||
},
|
||||
{
|
||||
name: 'Vercel',
|
||||
slug: 'vercel',
|
||||
image: 'Vercel.png',
|
||||
isAvailable: true,
|
||||
type: 'oauth',
|
||||
clientId: '',
|
||||
clientSlug: CLIENT_SLUG_VERCEL,
|
||||
docsLink: ''
|
||||
},
|
||||
{
|
||||
name: 'Netlify',
|
||||
slug: 'netlify',
|
||||
image: 'Netlify.png',
|
||||
isAvailable: true,
|
||||
type: 'oauth',
|
||||
clientId: CLIENT_ID_NETLIFY,
|
||||
docsLink: ''
|
||||
},
|
||||
{
|
||||
name: 'GitHub',
|
||||
slug: 'github',
|
||||
image: 'GitHub.png',
|
||||
isAvailable: true,
|
||||
type: 'oauth',
|
||||
clientId: CLIENT_ID_GITHUB,
|
||||
docsLink: ''
|
||||
},
|
||||
{
|
||||
name: 'Render',
|
||||
slug: 'render',
|
||||
image: 'Render.png',
|
||||
isAvailable: true,
|
||||
type: 'pat',
|
||||
clientId: '',
|
||||
docsLink: ''
|
||||
},
|
||||
{
|
||||
name: 'Fly.io',
|
||||
slug: 'flyio',
|
||||
image: 'Flyio.svg',
|
||||
isAvailable: true,
|
||||
type: 'pat',
|
||||
clientId: '',
|
||||
docsLink: ''
|
||||
},
|
||||
{
|
||||
name: 'AWS Parameter Store',
|
||||
slug: 'aws-parameter-store',
|
||||
image: 'Amazon Web Services.png',
|
||||
isAvailable: true,
|
||||
type: 'custom',
|
||||
clientId: '',
|
||||
docsLink: ''
|
||||
},
|
||||
{
|
||||
name: 'AWS Secret Manager',
|
||||
slug: 'aws-secret-manager',
|
||||
image: 'Amazon Web Services.png',
|
||||
isAvailable: true,
|
||||
type: 'custom',
|
||||
clientId: '',
|
||||
docsLink: ''
|
||||
},
|
||||
{
|
||||
name: 'Azure Key Vault',
|
||||
slug: 'azure-key-vault',
|
||||
@@ -60,110 +135,39 @@ const INTEGRATION_OPTIONS = [
|
||||
tenantId: TENANT_ID_AZURE,
|
||||
docsLink: ''
|
||||
},
|
||||
{
|
||||
name: "Heroku",
|
||||
slug: "heroku",
|
||||
image: "Heroku.png",
|
||||
isAvailable: true,
|
||||
type: "oauth",
|
||||
clientId: CLIENT_ID_HEROKU,
|
||||
docsLink: "",
|
||||
},
|
||||
{
|
||||
name: "Vercel",
|
||||
slug: "vercel",
|
||||
image: "Vercel.png",
|
||||
isAvailable: true,
|
||||
type: "oauth",
|
||||
clientId: "",
|
||||
clientSlug: CLIENT_SLUG_VERCEL,
|
||||
docsLink: "",
|
||||
},
|
||||
{
|
||||
name: "Netlify",
|
||||
slug: "netlify",
|
||||
image: "Netlify.png",
|
||||
isAvailable: true,
|
||||
type: "oauth",
|
||||
clientId: CLIENT_ID_NETLIFY,
|
||||
docsLink: "",
|
||||
},
|
||||
{
|
||||
name: "GitHub",
|
||||
slug: "github",
|
||||
image: "GitHub.png",
|
||||
isAvailable: true,
|
||||
type: "oauth",
|
||||
clientId: CLIENT_ID_GITHUB,
|
||||
docsLink: "",
|
||||
},
|
||||
{
|
||||
name: "Render",
|
||||
slug: "render",
|
||||
image: "Render.png",
|
||||
isAvailable: true,
|
||||
type: "pat",
|
||||
clientId: "",
|
||||
docsLink: "",
|
||||
},
|
||||
{
|
||||
name: "Fly.io",
|
||||
slug: "flyio",
|
||||
image: "Flyio.svg",
|
||||
isAvailable: true,
|
||||
type: "pat",
|
||||
clientId: "",
|
||||
docsLink: "",
|
||||
},
|
||||
{
|
||||
name: "Google Cloud Platform",
|
||||
slug: "gcp",
|
||||
image: "Google Cloud Platform.png",
|
||||
isAvailable: false,
|
||||
type: "",
|
||||
clientId: "",
|
||||
docsLink: "",
|
||||
},
|
||||
{
|
||||
name: "Amazon Web Services",
|
||||
slug: "aws",
|
||||
image: "Amazon Web Services.png",
|
||||
isAvailable: false,
|
||||
type: "",
|
||||
clientId: "",
|
||||
docsLink: "",
|
||||
},
|
||||
{
|
||||
name: "Microsoft Azure",
|
||||
slug: "azure",
|
||||
image: "Microsoft Azure.png",
|
||||
isAvailable: false,
|
||||
type: "",
|
||||
clientId: "",
|
||||
docsLink: "",
|
||||
},
|
||||
{
|
||||
name: "Travis CI",
|
||||
slug: "travisci",
|
||||
image: "Travis CI.png",
|
||||
isAvailable: false,
|
||||
type: "",
|
||||
clientId: "",
|
||||
docsLink: "",
|
||||
},
|
||||
{
|
||||
name: "Circle CI",
|
||||
slug: "circleci",
|
||||
image: "Circle CI.png",
|
||||
isAvailable: true,
|
||||
type: "pat",
|
||||
clientId: "",
|
||||
docsLink: "",
|
||||
},
|
||||
];
|
||||
{
|
||||
name: 'Google Cloud Platform',
|
||||
slug: 'gcp',
|
||||
image: 'Google Cloud Platform.png',
|
||||
isAvailable: false,
|
||||
type: '',
|
||||
clientId: '',
|
||||
docsLink: ''
|
||||
},
|
||||
{
|
||||
name: 'Travis CI',
|
||||
slug: 'travisci',
|
||||
image: 'Travis CI.png',
|
||||
isAvailable: false,
|
||||
type: '',
|
||||
clientId: '',
|
||||
docsLink: ''
|
||||
},
|
||||
{
|
||||
name: 'Circle CI',
|
||||
slug: 'circleci',
|
||||
image: 'Circle CI.png',
|
||||
isAvailable: true,
|
||||
type: 'pat',
|
||||
clientId: '',
|
||||
docsLink: ''
|
||||
}
|
||||
]
|
||||
|
||||
export {
|
||||
INTEGRATION_AZURE_KEY_VAULT,
|
||||
INTEGRATION_AWS_PARAMETER_STORE,
|
||||
INTEGRATION_AWS_SECRET_MANAGER,
|
||||
INTEGRATION_HEROKU,
|
||||
INTEGRATION_VERCEL,
|
||||
INTEGRATION_NETLIFY,
|
||||
|
||||
@@ -166,3 +166,24 @@ func CallIsAuthenticated(httpClient *resty.Client) bool {
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
func CallGetAccessibleEnvironments(httpClient *resty.Client, request GetAccessibleEnvironmentsRequest) (GetAccessibleEnvironmentsResponse, error) {
|
||||
var accessibleEnvironmentsResponse GetAccessibleEnvironmentsResponse
|
||||
response, err := httpClient.
|
||||
R().
|
||||
SetResult(&accessibleEnvironmentsResponse).
|
||||
SetHeader("User-Agent", USER_AGENT).
|
||||
Get(fmt.Sprintf("%v/v2/workspace/%s/environments", config.INFISICAL_URL, request.WorkspaceId))
|
||||
|
||||
log.Debugln(fmt.Errorf("CallGetAccessibleEnvironments: Unsuccessful response: [response=%v]", response))
|
||||
|
||||
if err != nil {
|
||||
return GetAccessibleEnvironmentsResponse{}, err
|
||||
}
|
||||
|
||||
if response.IsError() {
|
||||
return GetAccessibleEnvironmentsResponse{}, fmt.Errorf("CallGetAccessibleEnvironments: Unsuccessful response: [response=%v]", response)
|
||||
}
|
||||
|
||||
return accessibleEnvironmentsResponse, nil
|
||||
}
|
||||
|
||||
@@ -250,3 +250,15 @@ type GetServiceTokenDetailsResponse struct {
|
||||
UpdatedAt time.Time `json:"updatedAt"`
|
||||
V int `json:"__v"`
|
||||
}
|
||||
|
||||
type GetAccessibleEnvironmentsRequest struct {
|
||||
WorkspaceId string `json:"workspaceId"`
|
||||
}
|
||||
|
||||
type GetAccessibleEnvironmentsResponse struct {
|
||||
AccessibleEnvironments []struct {
|
||||
Name string `json:"name"`
|
||||
Slug string `json:"slug"`
|
||||
IsWriteDenied bool `json:"isWriteDenied"`
|
||||
} `json:"accessibleEnvironments"`
|
||||
}
|
||||
|
||||
45
cli/packages/cmd/reset.go
Normal file
@@ -0,0 +1,45 @@
|
||||
/*
|
||||
Copyright (c) 2023 Infisical Inc.
|
||||
*/
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"os"
|
||||
|
||||
"github.com/Infisical/infisical-merge/packages/util"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
var resetCmd = &cobra.Command{
|
||||
Use: "reset",
|
||||
Short: "Used delete all Infisical related data on your machine",
|
||||
DisableFlagsInUseLine: true,
|
||||
Example: "infisical reset",
|
||||
Args: cobra.NoArgs,
|
||||
PreRun: func(cmd *cobra.Command, args []string) {
|
||||
toggleDebug(cmd, args)
|
||||
},
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
// delete config
|
||||
_, pathToDir, err := util.GetFullConfigFilePath()
|
||||
if err != nil {
|
||||
util.HandleError(err)
|
||||
}
|
||||
|
||||
os.RemoveAll(pathToDir)
|
||||
|
||||
// delete keyring
|
||||
keyringInstance, err := util.GetKeyRing()
|
||||
if err != nil {
|
||||
util.HandleError(err)
|
||||
}
|
||||
|
||||
keyringInstance.Remove(util.KEYRING_SERVICE_NAME)
|
||||
|
||||
util.PrintSuccessMessage("Reset successful")
|
||||
},
|
||||
}
|
||||
|
||||
func init() {
|
||||
rootCmd.AddCommand(resetCmd)
|
||||
}
|
||||
@@ -64,10 +64,6 @@ var runCmd = &cobra.Command{
|
||||
util.HandleError(err, "Unable to parse flag")
|
||||
}
|
||||
|
||||
// if !util.IsSecretEnvironmentValid(envName) {
|
||||
// util.PrintMessageAndExit("Invalid environment name passed. Environment names can only be prod, dev, test or staging")
|
||||
// }
|
||||
|
||||
secretOverriding, err := cmd.Flags().GetBool("secret-overriding")
|
||||
if err != nil {
|
||||
util.HandleError(err, "Unable to parse flag")
|
||||
|
||||
@@ -512,7 +512,7 @@ func generateExampleEnv(cmd *cobra.Command, args []string) {
|
||||
if len(listOfTagNames) == 0 {
|
||||
fmt.Printf("\n%s \n", strings.Join(listOfKeyValue, "\n \n"))
|
||||
} else {
|
||||
fmt.Printf("\n\n\n%s\n \n%s \n", heading, strings.Join(listOfKeyValue, "\n \n"))
|
||||
fmt.Printf("\n\n\n%s \n%s \n", heading, strings.Join(listOfKeyValue, "\n \n"))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -27,6 +27,10 @@ func PrintWarning(message string) {
|
||||
color.New(color.FgYellow).Fprintf(os.Stderr, "Warning: %v \n", message)
|
||||
}
|
||||
|
||||
func PrintSuccessMessage(message string) {
|
||||
color.New(color.FgGreen).Println(message)
|
||||
}
|
||||
|
||||
func PrintMessageAndExit(messages ...string) {
|
||||
if len(messages) > 0 {
|
||||
for _, message := range messages {
|
||||
|
||||
@@ -130,6 +130,12 @@ func GetAllEnvironmentVariables(params models.GetAllSecretsParameters) ([]models
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Verify environment
|
||||
err = ValidateEnvironmentName(params.Environment, workspaceFile.WorkspaceId, loggedInUserDetails.UserCredentials)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("unable to validate environment name because [err=%s]", err)
|
||||
}
|
||||
|
||||
secretsToReturn, errorToReturn = GetPlainTextSecretsViaJTW(loggedInUserDetails.UserCredentials.JTWToken, loggedInUserDetails.UserCredentials.PrivateKey, workspaceFile.WorkspaceId, params.Environment)
|
||||
log.Debugf("GetAllEnvironmentVariables: Trying to fetch secrets JTW token [err=%s]", errorToReturn)
|
||||
|
||||
@@ -156,6 +162,33 @@ func GetAllEnvironmentVariables(params models.GetAllSecretsParameters) ([]models
|
||||
return secretsToReturn, errorToReturn
|
||||
}
|
||||
|
||||
func ValidateEnvironmentName(environmentName string, workspaceId string, userLoggedInDetails models.UserCredentials) error {
|
||||
httpClient := resty.New()
|
||||
httpClient.SetAuthToken(userLoggedInDetails.JTWToken).
|
||||
SetHeader("Accept", "application/json")
|
||||
|
||||
response, err := api.CallGetAccessibleEnvironments(httpClient, api.GetAccessibleEnvironmentsRequest{WorkspaceId: workspaceId})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
listOfEnvSlugs := []string{}
|
||||
mapOfEnvSlugs := make(map[string]interface{})
|
||||
|
||||
for _, environment := range response.AccessibleEnvironments {
|
||||
listOfEnvSlugs = append(listOfEnvSlugs, environment.Slug)
|
||||
mapOfEnvSlugs[environment.Slug] = environment
|
||||
}
|
||||
|
||||
_, exists := mapOfEnvSlugs[environmentName]
|
||||
if !exists {
|
||||
HandleError(fmt.Errorf("the environment [%s] does not exist in project with [id=%s]. Only [%s] are available", environmentName, workspaceId, strings.Join(listOfEnvSlugs, ",")))
|
||||
}
|
||||
|
||||
return nil
|
||||
|
||||
}
|
||||
|
||||
func getExpandedEnvVariable(secrets []models.SingleEnvironmentVariable, variableWeAreLookingFor string, hashMapOfCompleteVariables map[string]string, hashMapOfSelfRefs map[string]string) string {
|
||||
if value, found := hashMapOfCompleteVariables[variableWeAreLookingFor]; found {
|
||||
return value
|
||||
|
||||
11
docs/cli/commands/reset.mdx
Normal file
@@ -0,0 +1,11 @@
|
||||
---
|
||||
title: "infisical reset"
|
||||
description: "Reset Infisical"
|
||||
---
|
||||
|
||||
```bash
|
||||
infisical reset
|
||||
```
|
||||
|
||||
## Description
|
||||
This command provides a way to clear all Infisical-generated configuration data, effectively resetting the software to its default settings. This can be an effective way to address any persistent issues that arise while using the CLI.
|
||||
@@ -87,4 +87,25 @@ $ infisical secrets set STRIPE_API_KEY=sjdgwkeudyjwe DOMAIN=example.com HASH=jeb
|
||||
|
||||
Default value: `dev`
|
||||
</Accordion>
|
||||
</Accordion>
|
||||
</Accordion>
|
||||
|
||||
<Accordion title="infisical secrets generate-example-env">
|
||||
This command allows you to generate an example .env file from your secrets and with their associated comments and tags. This is useful when you would like to let
|
||||
others who work on the project but do not use Infisical become aware of the required environment variables and their intended values.
|
||||
|
||||
To place default values in your example .env file, you can simply include the syntax `DEFAULT:<value>` within your secret's comment in Infisical. This will result in the specified value being extracted and utilized as the default.
|
||||
|
||||
```bash
|
||||
$ infisical secrets generate-example-env
|
||||
|
||||
## Example
|
||||
$ infisical secrets generate-example-env > .example-env
|
||||
```
|
||||
|
||||
### Flags
|
||||
<Accordion title="--env">
|
||||
Used to select the environment name on which actions should be taken on
|
||||
|
||||
Default value: `dev`
|
||||
</Accordion>
|
||||
</Accordion>
|
||||
|
||||
BIN
docs/images/integrations-aws-access-key-1.png
Normal file
|
After Width: | Height: | Size: 374 KiB |
BIN
docs/images/integrations-aws-access-key-2.png
Normal file
|
After Width: | Height: | Size: 364 KiB |
BIN
docs/images/integrations-aws-access-key-3.png
Normal file
|
After Width: | Height: | Size: 290 KiB |
BIN
docs/images/integrations-aws-iam-1.png
Normal file
|
After Width: | Height: | Size: 323 KiB |
BIN
docs/images/integrations-aws-parameter-store-auth.png
Normal file
|
After Width: | Height: | Size: 181 KiB |
BIN
docs/images/integrations-aws-parameter-store-create.png
Normal file
|
After Width: | Height: | Size: 199 KiB |
BIN
docs/images/integrations-aws-parameter-store-iam-2.png
Normal file
|
After Width: | Height: | Size: 343 KiB |
BIN
docs/images/integrations-aws-parameter-store-iam-3.png
Normal file
|
After Width: | Height: | Size: 219 KiB |
BIN
docs/images/integrations-aws-parameter-store.png
Normal file
|
After Width: | Height: | Size: 377 KiB |
BIN
docs/images/integrations-aws-secret-manager-auth.png
Normal file
|
After Width: | Height: | Size: 180 KiB |
BIN
docs/images/integrations-aws-secret-manager-create.png
Normal file
|
After Width: | Height: | Size: 204 KiB |
BIN
docs/images/integrations-aws-secret-manager-iam-2.png
Normal file
|
After Width: | Height: | Size: 343 KiB |
BIN
docs/images/integrations-aws-secret-manager-iam-3.png
Normal file
|
After Width: | Height: | Size: 220 KiB |
BIN
docs/images/integrations-aws-secret-manager.png
Normal file
|
After Width: | Height: | Size: 377 KiB |
|
Before Width: | Height: | Size: 350 KiB After Width: | Height: | Size: 162 KiB |
BIN
docs/images/integrations-flyio-create.png
Normal file
|
After Width: | Height: | Size: 179 KiB |
|
Before Width: | Height: | Size: 399 KiB After Width: | Height: | Size: 373 KiB |
BIN
docs/images/integrations-heroku-create.png
Normal file
|
After Width: | Height: | Size: 179 KiB |
|
Before Width: | Height: | Size: 402 KiB After Width: | Height: | Size: 371 KiB |
BIN
docs/images/integrations-netlify-create.png
Normal file
|
After Width: | Height: | Size: 196 KiB |
|
Before Width: | Height: | Size: 404 KiB After Width: | Height: | Size: 380 KiB |
|
Before Width: | Height: | Size: 351 KiB After Width: | Height: | Size: 165 KiB |
BIN
docs/images/integrations-render-create.png
Normal file
|
After Width: | Height: | Size: 182 KiB |
|
Before Width: | Height: | Size: 398 KiB After Width: | Height: | Size: 371 KiB |
BIN
docs/images/integrations-vercel-create.png
Normal file
|
After Width: | Height: | Size: 192 KiB |
|
Before Width: | Height: | Size: 403 KiB After Width: | Height: | Size: 378 KiB |
|
Before Width: | Height: | Size: 418 KiB After Width: | Height: | Size: 421 KiB |
75
docs/integrations/cloud/aws-parameter-store.mdx
Normal file
@@ -0,0 +1,75 @@
|
||||
---
|
||||
title: "AWS Parameter Store"
|
||||
description: "How to automatically sync secrets from Infisical to your AWS Parameter Store."
|
||||
---
|
||||
|
||||
Prerequisites:
|
||||
|
||||
- Set up and add envars to [Infisical Cloud](https://app.infisical.com)
|
||||
- Set up AWS and have/create an IAM user
|
||||
|
||||
## Grant the IAM user permissions to access AWS Parameter Store
|
||||
|
||||
Navigate to your IAM user permissions and add a permission policy to grant access to AWS Parameter Store.
|
||||
|
||||

|
||||

|
||||

|
||||
|
||||
For better security, here's a custom policy containing the minimum permissions required by Infisical to sync secrets to AWS Parameter Store for the IAM user that you can use:
|
||||
|
||||
```json
|
||||
{
|
||||
"Version": "2012-10-17",
|
||||
"Statement": [
|
||||
{
|
||||
"Sid": "AllowSSMAccess",
|
||||
"Effect": "Allow",
|
||||
"Action": [
|
||||
"ssm:PutParameter",
|
||||
"ssm:DeleteParameter",
|
||||
"ssm:GetParametersByPath",
|
||||
"ssm:DeleteParameters"
|
||||
],
|
||||
"Resource": "*"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
## Navigate to your project's integrations tab
|
||||
|
||||

|
||||
|
||||
## Authorize Infisical for AWS Parameter store
|
||||
|
||||
Obtain a AWS access key ID and secret access key for your IAM user in IAM > Users > User > Security credentials > Access keys
|
||||
|
||||

|
||||

|
||||

|
||||
|
||||
Press on the AWS Parameter Store tile and input your AWS access key ID and secret access key from the previous step.
|
||||
|
||||

|
||||
|
||||
<Info>
|
||||
If this is your project's first cloud integration, then you'll have to grant
|
||||
Infisical access to your project's environment variables. Although this step
|
||||
breaks E2EE, it's necessary for Infisical to sync the environment variables to
|
||||
the cloud platform.
|
||||
</Info>
|
||||
|
||||
## Start integration
|
||||
|
||||
Select which Infisical environment secrets you want to sync to which AWS Parameter Store region and indicate the path for your secrets. Then, press create integration to start syncing secrets to AWS Parameter Store.
|
||||
|
||||

|
||||
|
||||
<Tip>
|
||||
Infisical requires you to add a path for your secrets to be stored in AWS
|
||||
Parameter Store and recommends setting the path structure to
|
||||
`/[project_name]/[environment]/` according to best practices. This enables a
|
||||
secret like `TEST` to be stored as `/[project_name]/[environment]/TEST` in AWS
|
||||
Parameter Store.
|
||||
</Tip>
|
||||
73
docs/integrations/cloud/aws-secret-manager.mdx
Normal file
@@ -0,0 +1,73 @@
|
||||
---
|
||||
title: "AWS Secret Manager"
|
||||
description: "How to automatically sync secrets from Infisical to your AWS Secret Manager."
|
||||
---
|
||||
|
||||
Prerequisites:
|
||||
|
||||
- Set up and add envars to [Infisical Cloud](https://app.infisical.com)
|
||||
- Set up AWS and have/create an IAM user
|
||||
|
||||
## Grant the IAM user permissions to access AWS Secret Manager
|
||||
|
||||
Navigate to your IAM user permissions and add a permission policy to grant access to AWS Secret Manager.
|
||||
|
||||

|
||||

|
||||

|
||||
|
||||
For better security, here's a custom policy containing the minimum permissions required by Infisical to sync secrets to AWS Secret Manager for the IAM user that you can use:
|
||||
|
||||
```json
|
||||
{
|
||||
"Version": "2012-10-17",
|
||||
"Statement": [
|
||||
{
|
||||
"Sid": "AllowSecretsManagerAccess",
|
||||
"Effect": "Allow",
|
||||
"Action": [
|
||||
"secretsmanager:GetSecretValue",
|
||||
"secretsmanager:CreateSecret",
|
||||
"secretsmanager:UpdateSecret"
|
||||
],
|
||||
"Resource": "*"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
## Navigate to your project's integrations tab
|
||||
|
||||

|
||||
|
||||
## Authorize Infisical for AWS Secret Manager
|
||||
|
||||
Obtain a AWS access key ID and secret access key for your IAM user in IAM > Users > User > Security credentials > Access keys
|
||||
|
||||

|
||||

|
||||

|
||||
|
||||
Press on the AWS Secret Manager tile and input your AWS access key ID and secret access key from the previous step.
|
||||
|
||||

|
||||
|
||||
<Info>
|
||||
If this is your project's first cloud integration, then you'll have to grant
|
||||
Infisical access to your project's environment variables. Although this step
|
||||
breaks E2EE, it's necessary for Infisical to sync the environment variables to
|
||||
the cloud platform.
|
||||
</Info>
|
||||
|
||||
## Start integration
|
||||
|
||||
Select which Infisical environment secrets you want to sync to which AWS Secret Manager region and under which secret name. Then, press create integration to start syncing secrets to AWS Secret Manager.
|
||||
|
||||

|
||||
|
||||
<Info>
|
||||
Infisical currently syncs environment variables to AWS Secret Manager as
|
||||
key-value pairs under one secret. We're actively exploring ways to help users
|
||||
group environment variable key-pairs under multiple secrets for greater
|
||||
control.
|
||||
</Info>
|
||||
@@ -31,6 +31,7 @@ Press on the Fly.io tile and input your Fly.io access token to grant Infisical a
|
||||
|
||||
## Start integration
|
||||
|
||||
Select which Infisical environment secrets you want to sync to which Fly.io app and press start integration to start syncing secrets to Fly.io.
|
||||
Select which Infisical environment secrets you want to sync to which Fly.io app and press create integration to start syncing secrets to Fly.io.
|
||||
|
||||

|
||||

|
||||
|
||||
@@ -18,13 +18,15 @@ Press on the Heroku tile and grant Infisical access to your Heroku account.
|
||||

|
||||
|
||||
<Info>
|
||||
If this is your project's first cloud integration, then you'll have to grant Infisical access to your project's environment variables.
|
||||
Although this step breaks E2EE, it's necessary for Infisical to sync the environment variables to the cloud platform.
|
||||
If this is your project's first cloud integration, then you'll have to grant
|
||||
Infisical access to your project's environment variables. Although this step
|
||||
breaks E2EE, it's necessary for Infisical to sync the environment variables to
|
||||
the cloud platform.
|
||||
</Info>
|
||||
|
||||
## Start integration
|
||||
|
||||
Select which Infisical environment secrets you want to sync to which Heroku app and press start integration to start syncing secrets to Heroku.
|
||||
Select which Infisical environment secrets you want to sync to which Heroku app and press create integration to start syncing secrets to Heroku.
|
||||
|
||||

|
||||

|
||||
|
||||
|
||||
@@ -4,7 +4,9 @@ description: "How to automatically sync secrets from Infisical into your Netlify
|
||||
---
|
||||
|
||||
<Warning>
|
||||
Infisical integrates with Netlify's new environment variable experience. If your site uses Netlify's old environment variable experience, you'll have to upgrade it to the new one to use this integration.
|
||||
Infisical integrates with Netlify's new environment variable experience. If
|
||||
your site uses Netlify's old environment variable experience, you'll have to
|
||||
upgrade it to the new one to use this integration.
|
||||
</Warning>
|
||||
|
||||
Prerequisites:
|
||||
@@ -22,12 +24,15 @@ Press on the Netlify tile and grant Infisical access to your Netlify account.
|
||||

|
||||
|
||||
<Info>
|
||||
If this is your project's first cloud integration, then you'll have to grant Infisical access to your project's environment variables.
|
||||
Although this step breaks E2EE, it's necessary for Infisical to sync the environment variables to the cloud platform.
|
||||
If this is your project's first cloud integration, then you'll have to grant
|
||||
Infisical access to your project's environment variables. Although this step
|
||||
breaks E2EE, it's necessary for Infisical to sync the environment variables to
|
||||
the cloud platform.
|
||||
</Info>
|
||||
|
||||
## Start integration
|
||||
|
||||
Select which Infisical environment secrets you want to sync to which Netlify app and context. Lastly, press start integration to start syncing secrets to Netlify.
|
||||
Select which Infisical environment secrets you want to sync to which Netlify app and context. Lastly, press create integration to start syncing secrets to Netlify.
|
||||
|
||||

|
||||

|
||||

|
||||
|
||||
@@ -31,6 +31,7 @@ Press on the Render tile and input your Render API Key to grant Infisical access
|
||||
|
||||
## Start integration
|
||||
|
||||
Select which Infisical environment secrets you want to sync to which Render service and press start integration to start syncing secrets to Render.
|
||||
Select which Infisical environment secrets you want to sync to which Render service and press create integration to start syncing secrets to Render.
|
||||
|
||||

|
||||

|
||||

|
||||
|
||||
@@ -17,8 +17,16 @@ Press on the Vercel tile and grant Infisical access to your Vercel account.
|
||||
|
||||

|
||||
|
||||
<Info>
|
||||
If this is your project's first cloud integration, then you'll have to grant
|
||||
Infisical access to your project's environment variables. Although this step
|
||||
breaks E2EE, it's necessary for Infisical to sync the environment variables to
|
||||
the cloud platform.
|
||||
</Info>
|
||||
|
||||
## Start integration
|
||||
|
||||
Select which Infisical environment secrets you want to sync to which Vercel app and environment. Lastly, press start integration to start syncing secrets to Vercel.
|
||||
Select which Infisical environment secrets you want to sync to which Vercel app and environment. Lastly, press create integration to start syncing secrets to Vercel.
|
||||
|
||||

|
||||

|
||||
|
||||
@@ -7,38 +7,40 @@ Integrations allow environment variables to be synced from Infisical into your l
|
||||
|
||||
Missing an integration? Throw in a [request](https://github.com/Infisical/infisical/issues).
|
||||
|
||||
| Integration | Type | Status |
|
||||
| -------------------------------------------------------- | --------- | ----------- |
|
||||
| [Docker](/integrations/platforms/docker) | Platform | Available |
|
||||
| [Docker-Compose](/integrations/platforms/docker-compose) | Platform | Available |
|
||||
| [Kubernetes](/integrations/platforms/kubernetes) | Platform | Available |
|
||||
| [PM2](/integrations/platforms/pm2) | Platform | Available |
|
||||
| [Heroku](/integrations/cloud/heroku) | Cloud | Available |
|
||||
| [Vercel](/integrations/cloud/vercel) | Cloud | Available |
|
||||
| [Netlify](/integrations/cloud/netlify) | Cloud | Available |
|
||||
| [Render](/integrations/cloud/render) | Cloud | Available |
|
||||
| [Fly.io](/integrations/cloud/flyio) | Cloud | Available |
|
||||
| [GitHub Actions](/integrations/cicd/githubactions) | CI/CD | Available |
|
||||
| [React](/integrations/frameworks/react) | Framework | Available |
|
||||
| [Vue](/integrations/frameworks/vue) | Framework | Available |
|
||||
| [Express](/integrations/frameworks/express) | Framework | Available |
|
||||
| [Next.js](/integrations/frameworks/nextjs) | Framework | Available |
|
||||
| [NestJS](/integrations/frameworks/nestjs) | Framework | Available |
|
||||
| [Nuxt](/integrations/frameworks/nuxt) | Framework | Available |
|
||||
| [Gatsby](/integrations/frameworks/gatsby) | Framework | Available |
|
||||
| [Remix](/integrations/frameworks/remix) | Framework | Available |
|
||||
| [Vite](/integrations/frameworks/vite) | Framework | Available |
|
||||
| [Fiber](/integrations/frameworks/fiber) | Framework | Available |
|
||||
| [Django](/integrations/frameworks/django) | Framework | Available |
|
||||
| [Flask](/integrations/frameworks/flask) | Framework | Available |
|
||||
| [Laravel](/integrations/frameworks/laravel) | Framework | Available |
|
||||
| [Ruby on Rails](/integrations/frameworks/rails) | Framework | Available |
|
||||
| AWS | Cloud | Coming soon |
|
||||
| GCP | Cloud | Coming soon |
|
||||
| Azure | Cloud | Coming soon |
|
||||
| DigitalOcean | Cloud | Coming soon |
|
||||
| [GitLab Pipeline](/integrations/cicd/gitlab) | CI/CD | Available |
|
||||
| [CircleCI](/integrations/cicd/circleci) | CI/CD | Coming soon |
|
||||
| TravisCI | CI/CD | Coming soon |
|
||||
| GitHub Actions | CI/CD | Coming soon |
|
||||
| Jenkins | CI/CD | Coming soon |
|
||||
| Integration | Type | Status |
|
||||
| -------------------------------------------------------------- | --------- | ----------- |
|
||||
| [Docker](/integrations/platforms/docker) | Platform | Available |
|
||||
| [Docker-Compose](/integrations/platforms/docker-compose) | Platform | Available |
|
||||
| [Kubernetes](/integrations/platforms/kubernetes) | Platform | Available |
|
||||
| [PM2](/integrations/platforms/pm2) | Platform | Available |
|
||||
| [Heroku](/integrations/cloud/heroku) | Cloud | Available |
|
||||
| [Vercel](/integrations/cloud/vercel) | Cloud | Available |
|
||||
| [Netlify](/integrations/cloud/netlify) | Cloud | Available |
|
||||
| [Render](/integrations/cloud/render) | Cloud | Available |
|
||||
| [Fly.io](/integrations/cloud/flyio) | Cloud | Available |
|
||||
| [AWS Parameter Store](/integrations/cloud/aws-parameter-store) | Cloud | Available |
|
||||
| [AWS Secret Manager](/integrations/cloud/aws-secret-manager) | Cloud | Available |
|
||||
| [GitHub Actions](/integrations/cicd/githubactions) | CI/CD | Available |
|
||||
| [React](/integrations/frameworks/react) | Framework | Available |
|
||||
| [Vue](/integrations/frameworks/vue) | Framework | Available |
|
||||
| [Express](/integrations/frameworks/express) | Framework | Available |
|
||||
| [Next.js](/integrations/frameworks/nextjs) | Framework | Available |
|
||||
| [NestJS](/integrations/frameworks/nestjs) | Framework | Available |
|
||||
| [Nuxt](/integrations/frameworks/nuxt) | Framework | Available |
|
||||
| [Gatsby](/integrations/frameworks/gatsby) | Framework | Available |
|
||||
| [Remix](/integrations/frameworks/remix) | Framework | Available |
|
||||
| [Vite](/integrations/frameworks/vite) | Framework | Available |
|
||||
| [Fiber](/integrations/frameworks/fiber) | Framework | Available |
|
||||
| [Django](/integrations/frameworks/django) | Framework | Available |
|
||||
| [Flask](/integrations/frameworks/flask) | Framework | Available |
|
||||
| [Laravel](/integrations/frameworks/laravel) | Framework | Available |
|
||||
| [Ruby on Rails](/integrations/frameworks/rails) | Framework | Available |
|
||||
| AWS | Cloud | Coming soon |
|
||||
| GCP | Cloud | Coming soon |
|
||||
| Azure | Cloud | Coming soon |
|
||||
| DigitalOcean | Cloud | Coming soon |
|
||||
| [GitLab Pipeline](/integrations/cicd/gitlab) | CI/CD | Available |
|
||||
| [CircleCI](/integrations/cicd/circleci) | CI/CD | Coming soon |
|
||||
| TravisCI | CI/CD | Coming soon |
|
||||
| GitHub Actions | CI/CD | Coming soon |
|
||||
| Jenkins | CI/CD | Coming soon |
|
||||
|
||||
@@ -114,7 +114,8 @@
|
||||
"cli/commands/run",
|
||||
"cli/commands/secrets",
|
||||
"cli/commands/export",
|
||||
"cli/commands/vault"
|
||||
"cli/commands/vault",
|
||||
"cli/commands/reset"
|
||||
]
|
||||
},
|
||||
"cli/faq"
|
||||
@@ -220,7 +221,9 @@
|
||||
"integrations/cloud/vercel",
|
||||
"integrations/cloud/netlify",
|
||||
"integrations/cloud/render",
|
||||
"integrations/cloud/flyio"
|
||||
"integrations/cloud/flyio",
|
||||
"integrations/cloud/aws-parameter-store",
|
||||
"integrations/cloud/aws-secret-manager"
|
||||
]
|
||||
},
|
||||
{
|
||||
|
||||
@@ -4,6 +4,8 @@ interface Mapping {
|
||||
|
||||
const integrationSlugNameMapping: Mapping = {
|
||||
'azure-key-vault': 'Azure Key Vault',
|
||||
'aws-parameter-store': 'AWS Parameter Store',
|
||||
'aws-secret-manager': 'AWS Secret Manager',
|
||||
'heroku': 'Heroku',
|
||||
'vercel': 'Vercel',
|
||||
'netlify': 'Netlify',
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"support": {
|
||||
"slack": "[NEW] Join Slack Forum",
|
||||
"slack": "Join Slack Forum",
|
||||
"docs": "Read Docs",
|
||||
"issue": "Open a Github Issue",
|
||||
"email": "Send us an Email"
|
||||
|
||||
@@ -246,9 +246,9 @@ const Layout = ({ children }: LayoutProps) => {
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="flex h-screen w-full flex-col overflow-x-hidden">
|
||||
<div className="h-screen w-full flex-col overflow-x-hidden hidden md:flex">
|
||||
<NavBarDashboard />
|
||||
<div className="flex flex-grow flex-col overflow-y-hidden md:flex-row">
|
||||
<div className="flex flex-grow flex-col overflow-y-hidden md:flex-row dark">
|
||||
<aside className="w-full border-r border-mineshaft-500 bg-bunker-600 md:w-60">
|
||||
<nav className="items-between flex h-full flex-col justify-between">
|
||||
{/* <div className="py-6"></div> */}
|
||||
@@ -368,10 +368,10 @@ const Layout = ({ children }: LayoutProps) => {
|
||||
error={error}
|
||||
loading={loading}
|
||||
/>
|
||||
<main className="flex-1 overflow-y-auto overflow-x-hidden bg-bunker-800">{children}</main>
|
||||
<main className="flex-1 overflow-y-auto overflow-x-hidden bg-bunker-800 dark:[color-scheme:dark]">{children}</main>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex h-screen w-screen flex-col items-center justify-center bg-bunker-800 md:hidden">
|
||||
<div className="flex h-screen w-screen flex-col items-center justify-center bg-bunker-800 z-[200] md:hidden">
|
||||
<FontAwesomeIcon icon={faMobile} className="mb-8 text-7xl text-gray-300" />
|
||||
<p className="max-w-sm px-6 text-center text-lg text-gray-200">
|
||||
{` ${t('common:no-mobile')} `}
|
||||
|
||||
@@ -34,7 +34,7 @@ const BottonRightPopup = ({
|
||||
}: PopupProps): JSX.Element => {
|
||||
return (
|
||||
<div
|
||||
className="z-50 drop-shadow-xl border-gray-600/50 border flex flex-col items-start bg-bunker max-w-xl text-gray-200 pt-3 pb-4 rounded-md absolute bottom-0 right-0 mr-6 mb-6"
|
||||
className="z-[100] drop-shadow-xl border-gray-600/50 border flex flex-col items-start bg-bunker max-w-xl text-gray-200 pt-3 pb-4 rounded-md absolute bottom-0 right-0 mr-6 mb-6"
|
||||
role="alert"
|
||||
>
|
||||
<div className="flex flex-row items-center justify-between w-full border-b border-gray-600/70 pb-3 px-6">
|
||||
|
||||
@@ -18,7 +18,7 @@ const CommentField = ({
|
||||
<div className="relative mt-4 px-4 pt-6">
|
||||
<p className="text-sm text-bunker-300 pl-0.5">{t('dashboard:sidebar.comments')}</p>
|
||||
<textarea
|
||||
className="placeholder:text-bunker-400 h-32 w-full bg-bunker-800 px-2 py-1.5 rounded-md border border-mineshaft-500 text-sm text-bunker-300 outline-none focus:ring-2 ring-primary-800 ring-opacity-70"
|
||||
className="placeholder:text-bunker-400 dark:[color-scheme:dark] h-32 w-full bg-bunker-800 px-2 py-1.5 rounded-md border border-mineshaft-500 text-sm text-bunker-300 outline-none focus:ring-2 ring-primary-800 ring-opacity-70"
|
||||
value={comment}
|
||||
onChange={(e) => modifyComment(e.target.value, position)}
|
||||
placeholder="Leave any comments here..."
|
||||
|
||||
@@ -125,7 +125,7 @@ const DashboardInputField = ({
|
||||
const error = startsWithNumber || isDuplicate;
|
||||
|
||||
return (
|
||||
<div className={`relative flex-col w-full h-10 ${
|
||||
<div title={value} className={`relative flex-col w-full h-10 ${
|
||||
isSideBarOpen && 'bg-mineshaft-700 duration-200'
|
||||
}`}>
|
||||
<div
|
||||
@@ -137,7 +137,7 @@ const DashboardInputField = ({
|
||||
onChange={(e) => onChangeHandler(e.target.value, position)}
|
||||
type={type}
|
||||
value={value}
|
||||
className='z-10 peer font-mono ph-no-capture bg-transparent py-2.5 caret-bunker-200 text-sm px-2 w-full min-w-16 outline-none text-bunker-300 focus:text-bunker-100 placeholder:text-bunker-400 placeholder:focus:text-transparent placeholder duration-200'
|
||||
className='z-10 peer ph-no-capture bg-transparent py-2.5 caret-bunker-200 text-sm px-2 w-full min-w-16 outline-none text-bunker-300 focus:text-bunker-100 placeholder:text-bunker-400 placeholder:focus:text-transparent placeholder duration-200'
|
||||
spellCheck="false"
|
||||
placeholder='–'
|
||||
/>
|
||||
@@ -209,7 +209,7 @@ const DashboardInputField = ({
|
||||
{value?.split('').map(() => (
|
||||
<FontAwesomeIcon
|
||||
key={guidGenerator()}
|
||||
className="text-xxs mx-0.5"
|
||||
className="text-xxs mr-0.5"
|
||||
icon={faCircle}
|
||||
/>
|
||||
))}
|
||||
|
||||
@@ -34,7 +34,10 @@ const colors = [
|
||||
'bg-[#cb1c8d]/40',
|
||||
'bg-[#badc58]/40',
|
||||
'bg-[#ff5400]/40',
|
||||
'bg-[#00bbf9]/40'
|
||||
'bg-[#3AB0FF]/40',
|
||||
'bg-[#6F1AB6]/40',
|
||||
'bg-[#C40B13]/40',
|
||||
'bg-[#332FD0]/40'
|
||||
]
|
||||
|
||||
|
||||
@@ -43,7 +46,10 @@ const colorsText = [
|
||||
'text-[#f2c6e3]/70',
|
||||
'text-[#eef6d5]/70',
|
||||
'text-[#ffddcc]/70',
|
||||
'text-[#f0fffd]/70'
|
||||
'text-[#f0fffd]/70',
|
||||
'text-[#FFE5F1]/70',
|
||||
'text-[#FFDEDE]/70',
|
||||
'text-[#DFF6FF]/70'
|
||||
]
|
||||
|
||||
/**
|
||||
@@ -95,9 +101,9 @@ const KeyPair = ({
|
||||
}`}
|
||||
>
|
||||
<div className="relative flex flex-row justify-between w-full mr-auto max-h-14 items-center">
|
||||
<div className="w-2/12 border-r border-mineshaft-600 flex flex-row items-center">
|
||||
<div className="w-1/5 border-r border-mineshaft-600 flex flex-row items-center">
|
||||
<div className='text-bunker-400 text-xs flex items-center justify-center w-14 h-10 cursor-default'>{keyPair.pos + 1}</div>
|
||||
<div className="flex items-center max-h-16">
|
||||
<div className="flex items-center max-h-16 w-full">
|
||||
<DashboardInputField
|
||||
isCapitalized = {isCapitalized}
|
||||
onChangeHandler={modifyKey}
|
||||
|
||||
@@ -18,6 +18,8 @@ interface Integration {
|
||||
isActive: boolean;
|
||||
app: string | null;
|
||||
appId: string | null;
|
||||
path: string | null;
|
||||
region: string | null;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
environment: string;
|
||||
@@ -75,6 +77,8 @@ const IntegrationTile = ({
|
||||
|
||||
if (integration?.app) {
|
||||
setIntegrationApp(integration.app);
|
||||
} else if (integration?.path && integration?.region) {
|
||||
setIntegrationApp(`${integration.path} (${integration.region})`);
|
||||
} else if (tempApps.length > 0) {
|
||||
setIntegrationApp(tempApps[0].name)
|
||||
} else {
|
||||
|
||||
@@ -14,6 +14,8 @@ interface Integration {
|
||||
isActive: boolean;
|
||||
app: string | null;
|
||||
appId: string | null;
|
||||
path: string | null;
|
||||
region: string | null;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
environment: string;
|
||||
|
||||
@@ -93,7 +93,7 @@ export default function Navbar() {
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex flex-row justify-between w-full bg-bunker text-white border-b border-mineshaft-500 z-50">
|
||||
<div className="flex flex-row justify-between w-full bg-bunker text-white border-b border-mineshaft-500 z-[61]">
|
||||
<div className="m-auto flex justify-start items-center mx-4">
|
||||
<div className="flex flex-row items-center">
|
||||
<div className="flex justify-center py-4">
|
||||
@@ -166,8 +166,8 @@ export default function Navbar() {
|
||||
leaveFrom="transform opacity-100 scale-100"
|
||||
leaveTo="transform opacity-0 scale-95"
|
||||
>
|
||||
<Menu.Items className="absolute right-0 mt-0.5 w-64 origin-top-right divide-y divide-gray-700 rounded-md bg-bunker border border-mineshaft-700 shadow-lg ring-1 ring-black z-20 ring-opacity-5 focus:outline-none">
|
||||
<div className="px-1 py-1 ">
|
||||
<Menu.Items className="absolute right-0 mt-0.5 w-64 origin-top-right divide-y divide-gray-700 rounded-md bg-bunker border border-mineshaft-700 shadow-lg ring-1 ring-black z-[65] ring-opacity-5 focus:outline-none">
|
||||
<div className="px-1 py-1 z-[100]">
|
||||
<div className="text-gray-400 self-start ml-2 mt-2 text-xs font-semibold tracking-wide">
|
||||
{t('nav:user.signed-in-as')}
|
||||
</div>
|
||||
|
||||
@@ -1,10 +1,7 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useRouter } from 'next/router';
|
||||
import { faAngleRight } from '@fortawesome/free-solid-svg-icons';
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||
|
||||
import { useWorkspace } from '@app/context';
|
||||
import getOrganization from '@app/pages/api/organization/GetOrg';
|
||||
import { useOrganization, useWorkspace } from '@app/context';
|
||||
|
||||
/**
|
||||
* This is the component at the top of almost every page.
|
||||
@@ -22,28 +19,15 @@ export default function NavHeader({
|
||||
pageName: string;
|
||||
isProjectRelated?: boolean;
|
||||
}): JSX.Element {
|
||||
const [orgName, setOrgName] = useState('');
|
||||
const router = useRouter();
|
||||
const projectId = String(router.query.id);
|
||||
const { currentWorkspace } = useWorkspace();
|
||||
|
||||
useEffect(() => {
|
||||
(async () => {
|
||||
const orgId = localStorage.getItem('orgData.id');
|
||||
const org = await getOrganization({
|
||||
orgId: orgId || ''
|
||||
});
|
||||
setOrgName(org.name);
|
||||
})();
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [projectId]);
|
||||
const { currentOrg } = useOrganization();
|
||||
|
||||
return (
|
||||
<div className="ml-6 flex flex-row items-center pt-8">
|
||||
<div className="mr-2 flex h-6 w-6 items-center justify-center rounded-md bg-primary-900 text-mineshaft-100">
|
||||
{orgName?.charAt(0)}
|
||||
{currentOrg?.name?.charAt(0)}
|
||||
</div>
|
||||
<div className="text-sm font-semibold text-primary">{orgName}</div>
|
||||
<div className="text-sm font-semibold text-primary">{currentOrg?.name}</div>
|
||||
{isProjectRelated && (
|
||||
<>
|
||||
<FontAwesomeIcon icon={faAngleRight} className="ml-3 mr-3 text-sm text-gray-400" />
|
||||
|
||||
@@ -4,7 +4,7 @@ const POSTHOG_HOST =
|
||||
process.env.NEXT_PUBLIC_POSTHOG_HOST! || "https://app.posthog.com";
|
||||
const STRIPE_PRODUCT_PRO = process.env.NEXT_PUBLIC_STRIPE_PRODUCT_PRO!;
|
||||
const STRIPE_PRODUCT_STARTER = process.env.NEXT_PUBLIC_STRIPE_PRODUCT_STARTER!;
|
||||
const {SITE_URL} = process.env
|
||||
const SITE_URL = "https://app.infisical.com";
|
||||
|
||||
export {
|
||||
ENV,
|
||||
@@ -12,4 +12,5 @@ export {
|
||||
POSTHOG_HOST,
|
||||
SITE_URL,
|
||||
STRIPE_PRODUCT_PRO,
|
||||
STRIPE_PRODUCT_STARTER};
|
||||
STRIPE_PRODUCT_STARTER
|
||||
};
|
||||
@@ -24,7 +24,7 @@ const buttonVariants = cva(
|
||||
{
|
||||
variants: {
|
||||
colorSchema: {
|
||||
primary: ['bg-primary', 'text-black', 'border-primary hover:bg-opacity-80'],
|
||||
primary: ['bg-primary', 'text-black', 'border-primary bg-opacity-80 hover:bg-opacity-100'],
|
||||
secondary: ['bg-mineshaft', 'text-gray-300', 'border-mineshaft hover:bg-opacity-80'],
|
||||
danger: ['bg-red', 'text-white', 'border-red hover:bg-opacity-90']
|
||||
},
|
||||
|
||||
@@ -8,9 +8,9 @@ export type CardTitleProps = {
|
||||
};
|
||||
|
||||
export const CardTitle = ({ children, className, subTitle }: CardTitleProps) => (
|
||||
<div className={twMerge('px-6 py-4 font-sans text-xl font-medium', className)}>
|
||||
<div className={twMerge('px-6 py-4 mb-5 font-sans text-lg font-normal border-b border-mineshaft-600', className)}>
|
||||
{children}
|
||||
{subTitle && <p className="py-1 text-sm font-normal text-gray-400">{subTitle}</p>}
|
||||
{subTitle && <p className="pt-0.5 text-sm font-normal text-gray-400">{subTitle}</p>}
|
||||
</div>
|
||||
);
|
||||
|
||||
|
||||
@@ -28,8 +28,9 @@ export const Checkbox = ({
|
||||
<div className="flex items-center font-inter text-bunker-300">
|
||||
<CheckboxPrimitive.Root
|
||||
className={twMerge(
|
||||
'flex items-center justify-center w-5 h-5 mr-3 transition-all rounded shadow hover:bg-bunker-200 bg-bunker-300',
|
||||
'flex items-center justify-center w-4 h-4 mr-3 transition-all rounded shadow border border-mineshaft-400 hover:bg-mineshaft-500 bg-mineshaft-600',
|
||||
isDisabled && 'bg-bunker-400 hover:bg-bunker-400',
|
||||
isChecked && 'bg-primary hover:bg-primary',
|
||||
className
|
||||
)}
|
||||
required={isRequired}
|
||||
|
||||
@@ -24,7 +24,7 @@ export const HoverObject = ({
|
||||
</a>
|
||||
</HoverCard.Trigger>
|
||||
<HoverCard.Portal>
|
||||
<HoverCard.Content className="HoverCardContent z-[50]" sideOffset={5}>
|
||||
<HoverCard.Content className="HoverCardContent z-[300]" sideOffset={5}>
|
||||
<div className='bg-bunker-700 border border-mineshaft-600 p-2 rounded-md drop-shadow-xl text-bunker-300'>
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: 15 }}>
|
||||
<div>
|
||||
|
||||
@@ -31,18 +31,20 @@ export const MenuItem = <T extends ElementType = 'button'>({
|
||||
as: Item = 'button',
|
||||
description,
|
||||
// wrapping in forward ref with generic component causes the loss of ts definitions on props
|
||||
inputRef
|
||||
inputRef,
|
||||
...props
|
||||
}: MenuItemProps<T> & ComponentPropsWithRef<T>): JSX.Element => (
|
||||
<li
|
||||
className={twMerge(
|
||||
'px-2 py-3 font-inter flex flex-col text-sm text-white transition-all rounded cursor-pointer hover:bg-gray-700',
|
||||
isSelected && 'text-primary',
|
||||
isDisabled && 'text-gray-500 hover:bg-transparent cursor-not-allowed',
|
||||
'px-1 py-2.5 mt-0.5 font-inter flex flex-col text-sm text-bunker-100 transition-all rounded cursor-pointer hover:bg-mineshaft-600 duration-50',
|
||||
isSelected && 'bg-mineshaft-500',
|
||||
isDisabled && 'hover:bg-transparent cursor-not-allowed',
|
||||
className
|
||||
)}
|
||||
>
|
||||
<Item type="button" role="menuitem" class="flex items-center" ref={inputRef}>
|
||||
{icon && <span className="mr-2">{icon}</span>}
|
||||
<Item type="button" role="menuitem" className="flex items-center relative" ref={inputRef} {...props}>
|
||||
<div className={`${isSelected ? "visisble" : "invisible"} absolute w-[0.25rem] rounded-md h-8 bg-primary`}/>
|
||||
{icon && <span className="mr-3 ml-4 w-5">{icon}</span>}
|
||||
<span className="flex-grow text-left">{children}</span>
|
||||
</Item>
|
||||
{description && <span className="mt-2 text-xs">{description}</span>}
|
||||
|
||||
@@ -25,7 +25,7 @@ export const ModalContent = forwardRef<HTMLDivElement, ModalContentProps>(
|
||||
<Card
|
||||
isRounded
|
||||
className={twMerge(
|
||||
'fixed top-1/2 left-1/2 max-w-lg -translate-y-2/4 -translate-x-2/4 animate-popIn drop-shadow-2xl z-[90]',
|
||||
'fixed top-1/2 left-1/2 max-w-lg border border-mineshaft-600 -translate-y-2/4 -translate-x-2/4 animate-popIn drop-shadow-2xl z-[90]',
|
||||
className
|
||||
)}
|
||||
>
|
||||
@@ -36,7 +36,7 @@ export const ModalContent = forwardRef<HTMLDivElement, ModalContentProps>(
|
||||
<IconButton
|
||||
variant="plain"
|
||||
ariaLabel="close"
|
||||
className="absolute top-3 right-3 rounded text-white hover:bg-gray-600"
|
||||
className="absolute top-4 right-6 rounded text-bunker-400 hover:text-bunker-50"
|
||||
>
|
||||
<FontAwesomeIcon icon={faTimes} size="lg" className="cursor-pointer" />
|
||||
</IconButton>
|
||||
|
||||
@@ -12,13 +12,14 @@ type Props = {
|
||||
className?: string;
|
||||
dropdownContainerClassName?: string;
|
||||
isLoading?: boolean;
|
||||
position?: 'item-aligned' | 'popper';
|
||||
};
|
||||
|
||||
export type SelectProps = SelectPrimitive.SelectProps & Props;
|
||||
|
||||
export const Select = forwardRef<HTMLButtonElement, SelectProps>(
|
||||
(
|
||||
{ children, placeholder, className, isLoading, dropdownContainerClassName, ...props },
|
||||
{ children, placeholder, className, isLoading, dropdownContainerClassName, position, ...props },
|
||||
ref
|
||||
): JSX.Element => {
|
||||
return (
|
||||
@@ -44,11 +45,13 @@ export const Select = forwardRef<HTMLButtonElement, SelectProps>(
|
||||
'relative left-4 top-1 overflow-hidden rounded-md bg-bunker-800 font-inter text-bunker-100 shadow-md z-[100]',
|
||||
dropdownContainerClassName
|
||||
)}
|
||||
position={position}
|
||||
style={{ width: 'var(--radix-select-trigger-width)' }}
|
||||
>
|
||||
<SelectPrimitive.ScrollUpButton>
|
||||
<FontAwesomeIcon icon={faChevronUp} size="sm" />
|
||||
</SelectPrimitive.ScrollUpButton>
|
||||
<SelectPrimitive.Viewport className="p-1.5">
|
||||
<SelectPrimitive.Viewport className="p-1">
|
||||
{isLoading ? (
|
||||
<div className="flex items-center justify-center">
|
||||
<Spinner size="xs" />
|
||||
@@ -82,7 +85,7 @@ export const SelectItem = forwardRef<HTMLDivElement, SelectItemProps>(
|
||||
{...props}
|
||||
className={twMerge(
|
||||
`relative flex cursor-pointer
|
||||
select-none items-center rounded-md py-2 pl-10 pr-4 text-sm
|
||||
select-none items-center rounded-md py-2 pl-10 pr-4 mb-0.5 text-sm
|
||||
outline-none transition-all hover:bg-mineshaft-500`,
|
||||
isSelected && 'bg-primary',
|
||||
isDisabled && 'cursor-not-allowed text-gray-600 hover:bg-transparent hover:text-gray-600',
|
||||
@@ -90,8 +93,8 @@ export const SelectItem = forwardRef<HTMLDivElement, SelectItemProps>(
|
||||
)}
|
||||
ref={forwardedRef}
|
||||
>
|
||||
<SelectPrimitive.ItemIndicator className="absolute left-3.5">
|
||||
<FontAwesomeIcon icon={faCheck} size="sm" />
|
||||
<SelectPrimitive.ItemIndicator className="absolute left-3.5 text-primary">
|
||||
<FontAwesomeIcon icon={faCheck} />
|
||||
</SelectPrimitive.ItemIndicator>
|
||||
<SelectPrimitive.ItemText className="">{children}</SelectPrimitive.ItemText>
|
||||
</SelectPrimitive.Item>
|
||||
|
||||
@@ -0,0 +1,46 @@
|
||||
import { createContext, ReactNode, useContext, useMemo } from 'react';
|
||||
|
||||
import { useGetOrganization } from '@app/hooks/api';
|
||||
import { Organization } from '@app/hooks/api/types';
|
||||
|
||||
import { useWorkspace } from '../WorkspaceContext';
|
||||
|
||||
type TOrgContext = {
|
||||
orgs?: Organization[];
|
||||
currentOrg?: Organization;
|
||||
isLoading: boolean;
|
||||
};
|
||||
|
||||
const OrgContext = createContext<TOrgContext | null>(null);
|
||||
|
||||
type Props = {
|
||||
children: ReactNode;
|
||||
};
|
||||
|
||||
export const OrgProvider = ({ children }: Props): JSX.Element => {
|
||||
const { currentWorkspace } = useWorkspace();
|
||||
const { data: userOrgs, isLoading } = useGetOrganization();
|
||||
|
||||
const currentWsOrgID = currentWorkspace?.organization;
|
||||
|
||||
// memorize the workspace details for the context
|
||||
const value = useMemo<TOrgContext>(
|
||||
() => ({
|
||||
orgs: userOrgs,
|
||||
currentOrg: (userOrgs || []).find(({ _id }) => _id === currentWsOrgID),
|
||||
isLoading
|
||||
}),
|
||||
[currentWsOrgID, userOrgs, isLoading]
|
||||
);
|
||||
|
||||
return <OrgContext.Provider value={value}>{children}</OrgContext.Provider>;
|
||||
};
|
||||
|
||||
export const useOrganization = () => {
|
||||
const ctx = useContext(OrgContext);
|
||||
if (!ctx) {
|
||||
throw new Error('useOrganization to be used within <OrgContext.Provider>');
|
||||
}
|
||||
|
||||
return ctx;
|
||||
};
|
||||
1
frontend/src/context/OrganizationContext/index.tsx
Normal file
@@ -0,0 +1 @@
|
||||
export { OrgProvider, useOrganization } from './OrganizationContext';
|
||||
38
frontend/src/context/UserContext/UserContext.tsx
Normal file
@@ -0,0 +1,38 @@
|
||||
import { createContext, ReactNode, useContext, useMemo } from 'react';
|
||||
|
||||
import { useGetUser } from '@app/hooks/api';
|
||||
import { User } from '@app/hooks/api/types';
|
||||
|
||||
type TUserContext = {
|
||||
user: User;
|
||||
isLoading: boolean;
|
||||
};
|
||||
|
||||
const UserContext = createContext<TUserContext | null>(null);
|
||||
|
||||
type Props = {
|
||||
children: ReactNode;
|
||||
};
|
||||
|
||||
export const UserProvider = ({ children }: Props): JSX.Element => {
|
||||
const { data, isLoading } = useGetUser();
|
||||
|
||||
// memorize the workspace details for the context
|
||||
const value = useMemo<TUserContext>(() => {
|
||||
return {
|
||||
user: data!,
|
||||
isLoading
|
||||
};
|
||||
}, [data, isLoading]);
|
||||
|
||||
return <UserContext.Provider value={value}>{children}</UserContext.Provider>;
|
||||
};
|
||||
|
||||
export const useUser = () => {
|
||||
const ctx = useContext(UserContext);
|
||||
if (!ctx) {
|
||||
throw new Error('useUser has to be used within <UserContext.Provider>');
|
||||
}
|
||||
|
||||
return ctx;
|
||||
};
|
||||
1
frontend/src/context/UserContext/index.tsx
Normal file
@@ -0,0 +1 @@
|
||||
export { UserProvider, useUser } from './UserContext';
|
||||
@@ -23,9 +23,10 @@ export const WorkspaceProvider = ({ children }: Props): JSX.Element => {
|
||||
|
||||
// memorize the workspace details for the context
|
||||
const value = useMemo<TWorkspaceContext>(() => {
|
||||
const wsId = workspaceId || localStorage.getItem('projectData.id');
|
||||
return {
|
||||
workspaces: ws || [],
|
||||
currentWorkspace: (ws || []).find(({ _id: id }) => id === workspaceId),
|
||||
currentWorkspace: (ws || []).find(({ _id: id }) => id === wsId),
|
||||
isLoading
|
||||
};
|
||||
}, [ws, workspaceId, isLoading]);
|
||||
|
||||
@@ -1,2 +1,5 @@
|
||||
export { AuthProvider } from './AuthContext';
|
||||
export { OrgProvider, useOrganization } from './OrganizationContext';
|
||||
export { SubscriptionProvider, useSubscription } from './SubscriptionContext';
|
||||
export { UserProvider, useUser } from './UserContext';
|
||||
export { useWorkspace, WorkspaceProvider } from './WorkspaceContext';
|
||||
|
||||
@@ -2,7 +2,7 @@ import { useEffect, useState } from 'react';
|
||||
import Image from 'next/image';
|
||||
import { useRouter } from 'next/router';
|
||||
import { useTranslation } from 'next-i18next';
|
||||
import { faX } from '@fortawesome/free-solid-svg-icons';
|
||||
import { faXmark } from '@fortawesome/free-solid-svg-icons';
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||
|
||||
import getActionData from '@app/ee/api/secrets/GetActionData';
|
||||
@@ -154,7 +154,7 @@ const ActivitySideBar = ({ toggleSidebar, currentAction }: SideBarProps) => {
|
||||
tabIndex={0}
|
||||
onClick={() => toggleSidebar('')}
|
||||
>
|
||||
<FontAwesomeIcon icon={faX} className="w-4 h-4 text-bunker-300 cursor-pointer" />
|
||||
<FontAwesomeIcon icon={faXmark} className="w-4 h-4 text-bunker-300 cursor-pointer" />
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex flex-col px-4">
|
||||
@@ -163,7 +163,7 @@ const ActivitySideBar = ({ toggleSidebar, currentAction }: SideBarProps) => {
|
||||
actionMetaData?.name === 'deleteSecrets') &&
|
||||
actionData?.map((item, id) => (
|
||||
<div key={`secret.${id + 1}`}>
|
||||
<div className="text-xs text-bunker-200 mt-4 pl-1">
|
||||
<div className="text-xs text-bunker-200 mt-4 pl-1 ph-no-capture">
|
||||
{item.newSecretVersion.key}
|
||||
</div>
|
||||
<DashboardInputField
|
||||
@@ -183,7 +183,7 @@ const ActivitySideBar = ({ toggleSidebar, currentAction }: SideBarProps) => {
|
||||
{item.newSecretVersion.key}
|
||||
</div>
|
||||
<div className="text-bunker-100 font-mono rounded-md overflow-hidden">
|
||||
<div className="bg-red/30 px-2">
|
||||
<div className="bg-red/30 px-2 ph-no-capture">
|
||||
-{' '}
|
||||
{patienceDiff(
|
||||
item.oldSecretVersion.value.split(''),
|
||||
@@ -201,7 +201,7 @@ const ActivitySideBar = ({ toggleSidebar, currentAction }: SideBarProps) => {
|
||||
)
|
||||
)}
|
||||
</div>
|
||||
<div className="bg-green-500/30 px-2">
|
||||
<div className="bg-green-500/30 px-2 ph-no-capture">
|
||||
+{' '}
|
||||
{patienceDiff(
|
||||
item.oldSecretVersion.value.split(''),
|
||||
|
||||
@@ -113,7 +113,7 @@ const SecretVersionList = ({ secretId }: { secretId: string }) => {
|
||||
})}
|
||||
</div>
|
||||
<div className="">
|
||||
<p className="break-words">
|
||||
<p className="break-words ph-no-capture">
|
||||
<span className="py-0.5 px-1 rounded-md bg-primary-200/10 mr-1.5">
|
||||
Value:
|
||||
</span>
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
export * from './auth';
|
||||
export * from './keys';
|
||||
export * from './organization';
|
||||
export * from './serviceTokens';
|
||||
export * from './subscriptions';
|
||||
export * from './tags';
|
||||
export * from './users';
|
||||
export * from './workspace';
|
||||
|
||||
@@ -1 +1 @@
|
||||
export { useGetUserWsKey } from './queries';
|
||||
export { useGetUserWsKey, useUploadWsKey } from './queries';
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import { useMutation, useQuery } from '@tanstack/react-query';
|
||||
|
||||
import { apiRequest } from '@app/config/request';
|
||||
|
||||
import { UserWsKeyPair } from './types';
|
||||
import { UploadWsKeyDTO, UserWsKeyPair } from './types';
|
||||
|
||||
const encKeyKeys = {
|
||||
getUserWorkspaceKey: (workspaceID: string) => ['worksapce-key-pair', { workspaceID }] as const
|
||||
@@ -22,3 +22,10 @@ export const useGetUserWsKey = (workspaceID: string) =>
|
||||
queryFn: () => fetchUserWsKey(workspaceID),
|
||||
enabled: Boolean(workspaceID)
|
||||
});
|
||||
|
||||
// mutations
|
||||
export const useUploadWsKey = () =>
|
||||
useMutation<{}, {}, UploadWsKeyDTO>({
|
||||
mutationFn: ({ encryptedKey, nonce, userId, workspaceId }) =>
|
||||
apiRequest.post(`/api/v1/key/${workspaceId}`, { key: { userId, encryptedKey, nonce } })
|
||||
});
|
||||
|
||||
@@ -20,3 +20,10 @@ export type Sender = {
|
||||
lastName: string;
|
||||
publicKey: string;
|
||||
};
|
||||
|
||||
export type UploadWsKeyDTO = {
|
||||
userId: string;
|
||||
encryptedKey: string;
|
||||
nonce: string;
|
||||
workspaceId: string;
|
||||
};
|
||||
|
||||
1
frontend/src/hooks/api/organization/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export { useGetOrganization } from './queries';
|
||||