mirror of
https://github.com/Infisical/infisical.git
synced 2025-03-25 14:05:03 +00:00
Changed frontend to use the new secrets routes
This commit is contained in:
@ -273,7 +273,7 @@ export const updateSecrets = async (req: Request, res: Response) => {
|
||||
});
|
||||
await Secret.bulkWrite(ops);
|
||||
|
||||
let newSecretsObj: { [key: string]: PatchSecret } = {};
|
||||
const newSecretsObj: { [key: string]: PatchSecret } = {};
|
||||
req.body.secrets.forEach((secret: PatchSecret) => {
|
||||
newSecretsObj[secret.id] = secret;
|
||||
});
|
||||
@ -323,7 +323,7 @@ export const updateSecrets = async (req: Request, res: Response) => {
|
||||
|
||||
// group secrets into workspaces so updated secrets can
|
||||
// be logged and snapshotted separately for each workspace
|
||||
let workspaceSecretObj: any = {};
|
||||
const workspaceSecretObj: any = {};
|
||||
req.secrets.forEach((s: any) => {
|
||||
if (s.workspace.toString() in workspaceSecretObj) {
|
||||
workspaceSecretObj[s.workspace.toString()].push(s);
|
||||
@ -399,7 +399,7 @@ export const deleteSecrets = async (req: Request, res: Response) => {
|
||||
|
||||
// group secrets into workspaces so deleted secrets can
|
||||
// be logged and snapshotted separately for each workspace
|
||||
let workspaceSecretObj: any = {};
|
||||
const workspaceSecretObj: any = {};
|
||||
req.secrets.forEach((s: any) => {
|
||||
if (s.workspace.toString() in workspaceSecretObj) {
|
||||
workspaceSecretObj[s.workspace.toString()].push(s);
|
||||
@ -445,7 +445,7 @@ export const deleteSecrets = async (req: Request, res: Response) => {
|
||||
}
|
||||
});
|
||||
|
||||
return res.status(400).send({
|
||||
return res.status(200).send({
|
||||
secrets: req.secrets
|
||||
});
|
||||
}
|
@ -138,7 +138,7 @@ router.patch(
|
||||
router.delete(
|
||||
'/',
|
||||
[
|
||||
check('secretIds')
|
||||
body('secretIds')
|
||||
.exists()
|
||||
.custom((value) => {
|
||||
// case: delete 1 secret
|
||||
|
@ -7,12 +7,12 @@ import {
|
||||
} from '../config';
|
||||
import { getLogger } from '../utils/logger';
|
||||
|
||||
if(TELEMETRY_ENABLED){
|
||||
if(!TELEMETRY_ENABLED){
|
||||
getLogger("backend-main").info([
|
||||
"",
|
||||
"Infisical collects telemetry data about general usage.",
|
||||
"The data helps us understand how the product is doing and guide our product development to create the best possible platform; it also helps us demonstrate growth for investors as we support Infisical as open-source software.",
|
||||
"To opt out of telemetry, you can set `TELEMETRY_ENABLED=false` within the environment variables",
|
||||
"To improve, Infisical collects telemetry data about general usage.",
|
||||
"This helps us understand how the product is doing and guide our product development to create the best possible platform; it also helps us demonstrate growth as we support Infisical as open-source software.",
|
||||
"To opt into telemetry, you can set `TELEMETRY_ENABLED=true` within the environment variables.",
|
||||
].join('\n'))
|
||||
}
|
||||
|
||||
|
@ -34,7 +34,7 @@ interface ToggleProps {
|
||||
* @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
|
||||
* @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
|
||||
|
@ -6,10 +6,10 @@ import { useTranslation } from "next-i18next";
|
||||
const CommentField = ({ comment, modifyComment, position }: { comment: string; modifyComment: (value: string, posistion: number) => void; position: number;}) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return <div className={`relative mt-4 px-4 pt-4`}>
|
||||
<p className='text-sm text-bunker-300'>{t("dashboard:sidebar.comments")}</p>
|
||||
return <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="bg-bunker-800 h-32 w-full bg-bunker-800 p-2 rounded-md border border-mineshaft-500 text-sm text-bunker-300 outline-none focus:ring-2 ring-primary-800 ring-opacity-70"
|
||||
className="bg-bunker-800 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"
|
||||
value={comment}
|
||||
onChange={(e) => modifyComment(e.target.value, position)}
|
||||
placeholder="Leave any comments here..."
|
||||
|
75
frontend/components/dashboard/DownloadSecretsMenu.tsx
Normal file
75
frontend/components/dashboard/DownloadSecretsMenu.tsx
Normal file
@ -0,0 +1,75 @@
|
||||
import { Fragment } from 'react';
|
||||
import { useTranslation } from "next-i18next";
|
||||
import { faDownload } from '@fortawesome/free-solid-svg-icons';
|
||||
import { Menu, Transition } from '@headlessui/react';
|
||||
|
||||
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
|
||||
* @param {SecretDataProps[]} obj.data - secrets that we want to downlaod
|
||||
* @param {string} obj.env - the environment which we're downloading (used for naming the file)
|
||||
*/
|
||||
const DownloadSecretMenu = ({ data, env }: { data: SecretDataProps[]; env: string; }) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return <Menu
|
||||
as="div"
|
||||
className="relative inline-block text-left"
|
||||
>
|
||||
<Menu.Button
|
||||
as="div"
|
||||
className="inline-flex w-full justify-center text-sm font-medium text-gray-200 rounded-md hover:bg-white/10 duration-200 focus:outline-none focus-visible:ring-2 focus-visible:ring-white focus-visible:ring-opacity-75"
|
||||
>
|
||||
<Button
|
||||
color="mineshaft"
|
||||
size="icon-md"
|
||||
icon={faDownload}
|
||||
onButtonPressed={() => {}}
|
||||
/>
|
||||
</Menu.Button>
|
||||
<Transition
|
||||
as={Fragment}
|
||||
enter="transition ease-out duration-100"
|
||||
enterFrom="transform opacity-0 scale-95"
|
||||
enterTo="transform opacity-100 scale-100"
|
||||
leave="transition ease-in duration-75"
|
||||
leaveFrom="transform opacity-100 scale-100"
|
||||
leaveTo="transform opacity-0 scale-95"
|
||||
>
|
||||
<Menu.Items className="absolute z-50 drop-shadow-xl right-0 mt-0.5 w-[12rem] origin-top-right rounded-md bg-bunker border border-mineshaft-500 shadow-lg ring-1 ring-black ring-opacity-5 focus:outline-none p-2 space-y-2">
|
||||
<Menu.Item>
|
||||
<Button
|
||||
color="mineshaft"
|
||||
onButtonPressed={() => downloadDotEnv({ data, env })}
|
||||
size="md"
|
||||
text="Download as .env"
|
||||
/>
|
||||
</Menu.Item>
|
||||
<Menu.Item>
|
||||
<Button
|
||||
color="mineshaft"
|
||||
onButtonPressed={() => downloadYaml({ data, env })}
|
||||
size="md"
|
||||
text="Download as .yml"
|
||||
/>
|
||||
</Menu.Item>
|
||||
</Menu.Items>
|
||||
</Transition>
|
||||
</Menu>
|
||||
}
|
||||
|
||||
export default DownloadSecretMenu;
|
@ -158,7 +158,7 @@ const SideBar = ({
|
||||
</div>
|
||||
</div>
|
||||
<SecretVersionList secretId={data[0]?.id} />
|
||||
<CommentField comment={data.filter(secret => secret.type == "shared")[0]?.comment} modifyComment={modifyComment} position={data[0]?.pos} />
|
||||
<CommentField comment={data.filter(secret => secret.type == "shared")[0]?.comment} modifyComment={modifyComment} position={data.filter(secret => secret.type == "shared")[0]?.pos} />
|
||||
</div>
|
||||
)}
|
||||
<div className={`flex justify-start max-w-sm mt-4 px-4 mt-full mb-[4.7rem]`}>
|
||||
|
@ -1,35 +1,46 @@
|
||||
import Aes256Gcm from '~/components/utilities/cryptography/aes-256-gcm';
|
||||
import login1 from '~/pages/api/auth/Login1';
|
||||
import login2 from '~/pages/api/auth/Login2';
|
||||
import addSecrets from '~/pages/api/files/AddSecrets';
|
||||
import getOrganizations from '~/pages/api/organization/getOrgs';
|
||||
import getOrganizationUserProjects from '~/pages/api/organization/GetOrgUserProjects';
|
||||
|
||||
import pushKeys from './secrets/pushKeys';
|
||||
import encryptSecrets from './secrets/encryptSecrets';
|
||||
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 nacl = require('tweetnacl');
|
||||
nacl.util = require('tweetnacl-util');
|
||||
const jsrp = require('jsrp');
|
||||
const client = new jsrp.client();
|
||||
|
||||
/**
|
||||
* This function loggs in the user (whether it's right after signup, or a normal login)
|
||||
* @param {*} email
|
||||
* @param {*} password
|
||||
* @param {*} setErrorLogin
|
||||
* This function logs in the user (whether it's right after signup, or a normal login)
|
||||
* @param {string} email - email of the user logging in
|
||||
* @param {string} password - password of the user logging in
|
||||
* @param {function} setErrorLogin - function that visually dispay an error is something is wrong
|
||||
* @param {*} router
|
||||
* @param {*} isSignUp
|
||||
* @param {boolean} isSignUp - whether this log in is a part of signup
|
||||
* @param {boolean} isLogin - ?
|
||||
* @returns
|
||||
*/
|
||||
const attemptLogin = async (
|
||||
email,
|
||||
password,
|
||||
setErrorLogin,
|
||||
router,
|
||||
isSignUp,
|
||||
isLogin
|
||||
email: string,
|
||||
password: string,
|
||||
setErrorLogin: (value: boolean) => void,
|
||||
router: any,
|
||||
isSignUp: boolean,
|
||||
isLogin: boolean
|
||||
) => {
|
||||
try {
|
||||
const telemetry = new Telemetry().getInstance();
|
||||
@ -76,7 +87,7 @@ const attemptLogin = async (
|
||||
});
|
||||
|
||||
const userOrgs = await getOrganizations();
|
||||
const userOrgsData = userOrgs.map((org) => org._id);
|
||||
const userOrgsData = userOrgs.map((org: { _id: string; }) => org._id);
|
||||
|
||||
let orgToLogin;
|
||||
if (userOrgsData.includes(localStorage.getItem('orgData.id'))) {
|
||||
@ -90,7 +101,7 @@ const attemptLogin = async (
|
||||
orgId: orgToLogin
|
||||
});
|
||||
|
||||
orgUserProjects = orgUserProjects?.map((project) => project._id);
|
||||
orgUserProjects = orgUserProjects?.map((project: { _id: string; }) => project._id);
|
||||
let projectToLogin;
|
||||
if (
|
||||
orgUserProjects.includes(localStorage.getItem('projectData.id'))
|
||||
@ -104,26 +115,7 @@ const attemptLogin = async (
|
||||
console.log('ERROR: User likely has no projects. ', error);
|
||||
}
|
||||
}
|
||||
|
||||
// If user is logging in for the first time, add the example keys
|
||||
if (isSignUp) {
|
||||
await pushKeys({
|
||||
obj: {
|
||||
sDATABASE_URL: [
|
||||
'mongodb+srv://${DB_USERNAME}:${DB_PASSWORD}@mongodb.net',
|
||||
'This is an example of secret referencing.'
|
||||
],
|
||||
sDB_USERNAME: ['OVERRIDE_THIS', ''],
|
||||
sDB_PASSWORD: ['OVERRIDE_THIS', ''],
|
||||
pDB_USERNAME: ['user1234', '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.'],
|
||||
pDB_PASSWORD: ['example_password', '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.'],
|
||||
sTWILIO_AUTH_TOKEN: ['example_twillio_token', ''],
|
||||
sWEBSITE_URL: ['http://localhost:3000', ''],
|
||||
},
|
||||
workspaceId: projectToLogin,
|
||||
env: 'Development'
|
||||
});
|
||||
}
|
||||
|
||||
if (email) {
|
||||
telemetry.identify(email);
|
||||
telemetry.capture('User Logged In');
|
||||
@ -133,6 +125,7 @@ const attemptLogin = async (
|
||||
router.push('/dashboard/');
|
||||
}
|
||||
} catch (error) {
|
||||
console.log(error)
|
||||
setErrorLogin(true);
|
||||
console.log('Login response not available');
|
||||
}
|
33
frontend/components/utilities/secrets/checkOverrides.ts
Normal file
33
frontend/components/utilities/secrets/checkOverrides.ts
Normal file
@ -0,0 +1,33 @@
|
||||
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
|
||||
* @param {SecretDataProps[]} obj.data - secrets that we want to check for overrides
|
||||
* @returns
|
||||
*/
|
||||
const checkOverrides = async ({ data }: { data: SecretDataProps[]; }) => {
|
||||
let secrets : SecretDataProps[] = data!.map((secret) => Object.create(secret));
|
||||
const overridenSecrets = data!.filter(
|
||||
(secret) => secret.type === 'personal'
|
||||
);
|
||||
if (overridenSecrets.length) {
|
||||
overridenSecrets.forEach((secret) => {
|
||||
const index = secrets!.findIndex(
|
||||
(_secret) => _secret.key === secret.key && _secret.type === 'shared'
|
||||
);
|
||||
secrets![index].value = secret.value;
|
||||
});
|
||||
secrets = secrets!.filter((secret) => secret.type === 'shared');
|
||||
}
|
||||
return secrets;
|
||||
}
|
||||
|
||||
export default checkOverrides;
|
46
frontend/components/utilities/secrets/downloadDotEnv.ts
Normal file
46
frontend/components/utilities/secrets/downloadDotEnv.ts
Normal file
@ -0,0 +1,46 @@
|
||||
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
|
||||
* @param {SecretDataProps[]} obj.data - secrets that we want to download
|
||||
* @param {string} obj.env - the environment which we're downloading (used for naming the file)
|
||||
*/
|
||||
const downloadDotEnv = async ({ data, env }: { data: SecretDataProps[]; env: string; }) => {
|
||||
if (!data) return;
|
||||
const secrets = await checkOverrides({ data });
|
||||
|
||||
const file = secrets!
|
||||
.map(
|
||||
(item: SecretDataProps) =>
|
||||
`${
|
||||
item.comment
|
||||
? item.comment
|
||||
.split('\n')
|
||||
.map((comment) => '# '.concat(comment))
|
||||
.join('\n') + '\n'
|
||||
: ''
|
||||
}` + [item.key, item.value].join('=')
|
||||
)
|
||||
.join('\n');
|
||||
|
||||
const blob = new Blob([file]);
|
||||
const fileDownloadUrl = URL.createObjectURL(blob);
|
||||
const alink = document.createElement('a');
|
||||
alink.href = fileDownloadUrl;
|
||||
alink.download = envMapping[env] + '.env';
|
||||
alink.click();
|
||||
}
|
||||
|
||||
export default downloadDotEnv;
|
52
frontend/components/utilities/secrets/downloadYaml.ts
Normal file
52
frontend/components/utilities/secrets/downloadYaml.ts
Normal file
@ -0,0 +1,52 @@
|
||||
import YAML from 'yaml';
|
||||
import { YAMLSeq } from 'yaml/types'
|
||||
|
||||
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
|
||||
* @param {SecretDataProps[]} obj.data - secrets that we want to download
|
||||
* @param {string} obj.env - used for naming the file
|
||||
* @returns
|
||||
*/
|
||||
const downloadYaml = async ({ data, env }: { data: SecretDataProps[]; env: string; }) => {
|
||||
if (!data) return;
|
||||
const doc = new YAML.Document();
|
||||
doc.contents = new YAMLSeq()
|
||||
const secrets = await checkOverrides({ data });
|
||||
secrets.forEach((secret) => {
|
||||
const pair = YAML.createNode({ [secret.key]: secret.value });
|
||||
pair.commentBefore = secret.comment
|
||||
.split('\n')
|
||||
.map((line) => (line ? ' '.concat(line) : ''))
|
||||
.join('\n');
|
||||
doc.add(pair);
|
||||
});
|
||||
|
||||
const file = doc
|
||||
.toString()
|
||||
.split('\n')
|
||||
.map((line) => (line.startsWith('-') ? line.replace('- ', '') : line))
|
||||
.join('\n');
|
||||
|
||||
const blob = new Blob([file]);
|
||||
const fileDownloadUrl = URL.createObjectURL(blob);
|
||||
const alink = document.createElement('a');
|
||||
alink.href = fileDownloadUrl;
|
||||
alink.download = envMapping[env] + '.yml';
|
||||
alink.click();
|
||||
}
|
||||
|
||||
export default downloadYaml;
|
116
frontend/components/utilities/secrets/encryptSecrets.ts
Normal file
116
frontend/components/utilities/secrets/encryptSecrets.ts
Normal file
@ -0,0 +1,116 @@
|
||||
import getLatestFileKey from "~/pages/api/workspace/getLatestFileKey";
|
||||
|
||||
const crypto = require("crypto");
|
||||
const {
|
||||
decryptAssymmetric,
|
||||
encryptSymmetric,
|
||||
} = require("../cryptography/crypto");
|
||||
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;
|
||||
environment: string;
|
||||
secretCommentCiphertext: string;
|
||||
secretCommentIV: string;
|
||||
secretCommentTag: string;
|
||||
secretKeyCiphertext: string;
|
||||
secretKeyIV: string;
|
||||
secretKeyTag: string;
|
||||
secretValueCiphertext: string;
|
||||
secretValueIV: string;
|
||||
secretValueTag: string;
|
||||
type: "personal" | "shared";
|
||||
}
|
||||
|
||||
/**
|
||||
* Encypt secrets before pushing the to the DB
|
||||
* @param {object} obj
|
||||
* @param {object} obj.secretsToEncrypt - secrets that we want to encrypt
|
||||
* @param {object} obj.workspaceId - the id of a project in which we are encrypting secrets
|
||||
* @returns
|
||||
*/
|
||||
const encryptSecrets = async ({ secretsToEncrypt, workspaceId, env }: { secretsToEncrypt: SecretDataProps[]; workspaceId: string; env: string; }) => {
|
||||
const sharedKey = await getLatestFileKey({ workspaceId });
|
||||
|
||||
const PRIVATE_KEY = localStorage.getItem("PRIVATE_KEY");
|
||||
|
||||
let randomBytes: string;
|
||||
if (Object.keys(sharedKey).length > 0) {
|
||||
// case: a (shared) key exists for the workspace
|
||||
randomBytes = decryptAssymmetric({
|
||||
ciphertext: sharedKey.latestKey.encryptedKey,
|
||||
nonce: sharedKey.latestKey.nonce,
|
||||
publicKey: sharedKey.latestKey.sender.publicKey,
|
||||
privateKey: PRIVATE_KEY,
|
||||
});
|
||||
} else {
|
||||
// case: a (shared) key does not exist for the workspace
|
||||
randomBytes = crypto.randomBytes(16).toString("hex");
|
||||
}
|
||||
|
||||
const secrets = secretsToEncrypt.map((secret) => {
|
||||
// encrypt key
|
||||
const {
|
||||
ciphertext: secretKeyCiphertext,
|
||||
iv: secretKeyIV,
|
||||
tag: secretKeyTag,
|
||||
} = encryptSymmetric({
|
||||
plaintext: secret.key,
|
||||
key: randomBytes,
|
||||
});
|
||||
|
||||
// encrypt value
|
||||
const {
|
||||
ciphertext: secretValueCiphertext,
|
||||
iv: secretValueIV,
|
||||
tag: secretValueTag,
|
||||
} = encryptSymmetric({
|
||||
plaintext: secret.value,
|
||||
key: randomBytes,
|
||||
});
|
||||
|
||||
// encrypt comment
|
||||
const {
|
||||
ciphertext: secretCommentCiphertext,
|
||||
iv: secretCommentIV,
|
||||
tag: secretCommentTag,
|
||||
} = encryptSymmetric({
|
||||
plaintext: secret.comment ?? '',
|
||||
key: randomBytes,
|
||||
});
|
||||
|
||||
const result: EncryptedSecretProps = {
|
||||
id: secret.id,
|
||||
createdAt: '',
|
||||
environment: env,
|
||||
secretKeyCiphertext,
|
||||
secretKeyIV,
|
||||
secretKeyTag,
|
||||
secretValueCiphertext,
|
||||
secretValueIV,
|
||||
secretValueTag,
|
||||
secretCommentCiphertext,
|
||||
secretCommentIV,
|
||||
secretCommentTag,
|
||||
type: secret.type,
|
||||
};
|
||||
|
||||
return result;
|
||||
});
|
||||
|
||||
return secrets;
|
||||
}
|
||||
|
||||
export default encryptSecrets;
|
@ -1,7 +1,7 @@
|
||||
import getSecrets from '~/pages/api/files/GetSecrets';
|
||||
import getLatestFileKey from '~/pages/api/workspace/getLatestFileKey';
|
||||
|
||||
import { envMapping } from '../../../public/data/frequentConstants';
|
||||
import guidGenerator from '../randomId';
|
||||
|
||||
const {
|
||||
decryptAssymmetric,
|
||||
@ -10,6 +10,22 @@ const {
|
||||
const nacl = require('tweetnacl');
|
||||
nacl.util = require('tweetnacl-util');
|
||||
|
||||
interface EncryptedSecretProps {
|
||||
_id: string;
|
||||
createdAt: string;
|
||||
environment: string;
|
||||
secretCommentCiphertext: string;
|
||||
secretCommentIV: string;
|
||||
secretCommentTag: string;
|
||||
secretKeyCiphertext: string;
|
||||
secretKeyIV: string;
|
||||
secretKeyTag: string;
|
||||
secretValueCiphertext: string;
|
||||
secretValueIV: string;
|
||||
secretValueTag: string;
|
||||
type: "personal" | "shared";
|
||||
}
|
||||
|
||||
interface SecretProps {
|
||||
key: string;
|
||||
value: string;
|
||||
@ -18,107 +34,102 @@ interface SecretProps {
|
||||
id: string;
|
||||
}
|
||||
|
||||
interface Props {
|
||||
interface FunctionProps {
|
||||
env: keyof typeof envMapping;
|
||||
setFileState: any;
|
||||
setIsKeyAvailable: any;
|
||||
setData: any;
|
||||
workspaceId: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the secrets for a certain project
|
||||
* @param {object} obj
|
||||
* @param {string} obj.env - environment for which we are getting secrets
|
||||
* @param {boolean} obj.isKeyAvailable - if a person is able to create new key pairs
|
||||
* @param {function} obj.setData - state function that manages the state of secrets in the dashboard
|
||||
* @param {string} obj.workspaceId - id of a workspace for which we are getting secrets
|
||||
*/
|
||||
const getSecretsForProject = async ({
|
||||
env,
|
||||
setFileState,
|
||||
setIsKeyAvailable,
|
||||
setData,
|
||||
workspaceId
|
||||
}: Props) => {
|
||||
}: FunctionProps) => {
|
||||
try {
|
||||
let file;
|
||||
let encryptedSecrets;
|
||||
try {
|
||||
file = await getSecrets(workspaceId, envMapping[env]);
|
||||
|
||||
setFileState(file);
|
||||
encryptedSecrets = await getSecrets(workspaceId, envMapping[env]);
|
||||
} catch (error) {
|
||||
console.log('ERROR: Not able to access the latest file');
|
||||
console.log('ERROR: Not able to access the latest version of secrets');
|
||||
}
|
||||
|
||||
const latestKey = await getLatestFileKey({ workspaceId })
|
||||
// This is called isKeyAvailable but what it really means is if a person is able to create new key pairs
|
||||
setIsKeyAvailable(!file.key ? file.secrets.length == 0 : true);
|
||||
setIsKeyAvailable(!latestKey ? encryptedSecrets.length == 0 : true);
|
||||
|
||||
const PRIVATE_KEY = localStorage.getItem('PRIVATE_KEY');
|
||||
|
||||
const tempFileState: SecretProps[] = [];
|
||||
if (file.key) {
|
||||
const tempDecryptedSecrets: SecretProps[] = [];
|
||||
if (latestKey) {
|
||||
// assymmetrically decrypt symmetric key with local private key
|
||||
const key = decryptAssymmetric({
|
||||
ciphertext: file.key.encryptedKey,
|
||||
nonce: file.key.nonce,
|
||||
publicKey: file.key.sender.publicKey,
|
||||
ciphertext: latestKey.latestKey.encryptedKey,
|
||||
nonce: latestKey.latestKey.nonce,
|
||||
publicKey: latestKey.latestKey.sender.publicKey,
|
||||
privateKey: PRIVATE_KEY
|
||||
});
|
||||
|
||||
file.secrets.map((secretPair: any) => {
|
||||
// decrypt .env file with symmetric key
|
||||
// decrypt secret keys, values, and comments
|
||||
encryptedSecrets.map((secret: EncryptedSecretProps) => {
|
||||
const plainTextKey = decryptSymmetric({
|
||||
ciphertext: secretPair.secretKey.ciphertext,
|
||||
iv: secretPair.secretKey.iv,
|
||||
tag: secretPair.secretKey.tag,
|
||||
ciphertext: secret.secretKeyCiphertext,
|
||||
iv: secret.secretKeyIV,
|
||||
tag: secret.secretKeyTag,
|
||||
key
|
||||
});
|
||||
|
||||
const plainTextValue = decryptSymmetric({
|
||||
ciphertext: secretPair.secretValue.ciphertext,
|
||||
iv: secretPair.secretValue.iv,
|
||||
tag: secretPair.secretValue.tag,
|
||||
ciphertext: secret.secretValueCiphertext,
|
||||
iv: secret.secretValueIV,
|
||||
tag: secret.secretValueTag,
|
||||
key
|
||||
});
|
||||
|
||||
let plainTextComment;
|
||||
if (secretPair.secretComment.ciphertext) {
|
||||
if (secret.secretCommentCiphertext) {
|
||||
plainTextComment = decryptSymmetric({
|
||||
ciphertext: secretPair.secretComment.ciphertext,
|
||||
iv: secretPair.secretComment.iv,
|
||||
tag: secretPair.secretComment.tag,
|
||||
ciphertext: secret.secretCommentCiphertext,
|
||||
iv: secret.secretCommentIV,
|
||||
tag: secret.secretCommentTag,
|
||||
key
|
||||
});
|
||||
} else {
|
||||
plainTextComment = "";
|
||||
}
|
||||
|
||||
tempFileState.push({
|
||||
id: secretPair._id,
|
||||
tempDecryptedSecrets.push({
|
||||
id: secret._id,
|
||||
key: plainTextKey,
|
||||
value: plainTextValue,
|
||||
type: secretPair.type,
|
||||
type: secret.type,
|
||||
comment: plainTextComment
|
||||
});
|
||||
});
|
||||
}
|
||||
setFileState(tempFileState);
|
||||
|
||||
setData(
|
||||
tempFileState.map((line, index) => {
|
||||
return {
|
||||
id: line['id'],
|
||||
pos: index,
|
||||
key: line['key'],
|
||||
value: line['value'],
|
||||
type: line['type'],
|
||||
comment: line['comment']
|
||||
};
|
||||
})
|
||||
);
|
||||
|
||||
return tempFileState.map((line, index) => {
|
||||
const result = tempDecryptedSecrets.map((secret, index) => {
|
||||
return {
|
||||
id: line['id'],
|
||||
id: secret['id'],
|
||||
pos: index,
|
||||
key: line['key'],
|
||||
value: line['value'],
|
||||
type: line['type'],
|
||||
comment: line['comment']
|
||||
key: secret['key'],
|
||||
value: secret['value'],
|
||||
type: secret['type'],
|
||||
comment: secret['comment']
|
||||
};
|
||||
});
|
||||
|
||||
setData(result);
|
||||
return result;
|
||||
} catch (error) {
|
||||
console.log('Something went wrong during accessing or decripting secrets.');
|
||||
}
|
||||
|
@ -1,126 +0,0 @@
|
||||
import uploadSecrets from "~/pages/api/files/UploadSecrets";
|
||||
import getLatestFileKey from "~/pages/api/workspace/getLatestFileKey";
|
||||
import getWorkspaceKeys from "~/pages/api/workspace/getWorkspaceKeys";
|
||||
|
||||
import { envMapping } from "../../../public/data/frequentConstants";
|
||||
|
||||
const crypto = require("crypto");
|
||||
const {
|
||||
decryptAssymmetric,
|
||||
encryptSymmetric,
|
||||
encryptAssymmetric,
|
||||
} = require("../cryptography/crypto");
|
||||
const nacl = require("tweetnacl");
|
||||
nacl.util = require("tweetnacl-util");
|
||||
|
||||
export interface IK {
|
||||
publicKey: string;
|
||||
userId: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* This function pushes the keys to the database after decrypting them end-to-end
|
||||
* @param {object} obj
|
||||
* @param {object} obj.obj - object with all the key pairs
|
||||
* @param {object} obj.workspaceId - the id of a project to which a user is pushing
|
||||
* @param {object} obj.env - which environment a user is pushing to
|
||||
*/
|
||||
const pushKeys = async({ obj, workspaceId, env }: { obj: object; workspaceId: string; env: string; }) => {
|
||||
const sharedKey = await getLatestFileKey({ workspaceId });
|
||||
|
||||
const PRIVATE_KEY = localStorage.getItem("PRIVATE_KEY");
|
||||
|
||||
let randomBytes: string;
|
||||
if (Object.keys(sharedKey).length > 0) {
|
||||
// case: a (shared) key exists for the workspace
|
||||
randomBytes = decryptAssymmetric({
|
||||
ciphertext: sharedKey.latestKey.encryptedKey,
|
||||
nonce: sharedKey.latestKey.nonce,
|
||||
publicKey: sharedKey.latestKey.sender.publicKey,
|
||||
privateKey: PRIVATE_KEY,
|
||||
});
|
||||
} else {
|
||||
// case: a (shared) key does not exist for the workspace
|
||||
randomBytes = crypto.randomBytes(16).toString("hex");
|
||||
}
|
||||
|
||||
const secrets = Object.keys(obj).map((key) => {
|
||||
// encrypt key
|
||||
const {
|
||||
ciphertext: secretKeyCiphertext,
|
||||
iv: secretKeyIV,
|
||||
tag: secretKeyTag,
|
||||
} = encryptSymmetric({
|
||||
plaintext: key.slice(1),
|
||||
key: randomBytes,
|
||||
});
|
||||
|
||||
// encrypt value
|
||||
const {
|
||||
ciphertext: secretValueCiphertext,
|
||||
iv: secretValueIV,
|
||||
tag: secretValueTag,
|
||||
} = encryptSymmetric({
|
||||
plaintext: obj[key as keyof typeof obj][0],
|
||||
key: randomBytes,
|
||||
});
|
||||
|
||||
// encrypt comment
|
||||
const {
|
||||
ciphertext: secretCommentCiphertext,
|
||||
iv: secretCommentIV,
|
||||
tag: secretCommentTag,
|
||||
} = encryptSymmetric({
|
||||
plaintext: obj[key as keyof typeof obj][1],
|
||||
key: randomBytes,
|
||||
});
|
||||
|
||||
const visibility = key.charAt(0) == "p" ? "personal" : "shared";
|
||||
|
||||
return {
|
||||
secretKeyCiphertext,
|
||||
secretKeyIV,
|
||||
secretKeyTag,
|
||||
secretKeyHash: crypto.createHash("sha256").update(key.slice(1)).digest("hex"),
|
||||
secretValueCiphertext,
|
||||
secretValueIV,
|
||||
secretValueTag,
|
||||
secretValueHash: crypto.createHash("sha256").update(obj[key as keyof typeof obj][0]).digest("hex"),
|
||||
secretCommentCiphertext,
|
||||
secretCommentIV,
|
||||
secretCommentTag,
|
||||
secretCommentHash: crypto.createHash("sha256").update(obj[key as keyof typeof obj][1]).digest("hex"),
|
||||
type: visibility,
|
||||
};
|
||||
});
|
||||
|
||||
// obtain public keys of all receivers (i.e. members in workspace)
|
||||
const publicKeys = await getWorkspaceKeys({
|
||||
workspaceId,
|
||||
});
|
||||
|
||||
// assymmetrically encrypt key with each receiver public keys
|
||||
const keys = publicKeys.map((k: IK) => {
|
||||
const { ciphertext, nonce } = encryptAssymmetric({
|
||||
plaintext: randomBytes,
|
||||
publicKey: k.publicKey,
|
||||
privateKey: PRIVATE_KEY,
|
||||
});
|
||||
|
||||
return {
|
||||
encryptedKey: ciphertext,
|
||||
nonce,
|
||||
userId: k.userId,
|
||||
};
|
||||
});
|
||||
|
||||
// send payload
|
||||
await uploadSecrets({
|
||||
workspaceId,
|
||||
secrets,
|
||||
keys,
|
||||
environment: envMapping[env as keyof typeof envMapping],
|
||||
});
|
||||
};
|
||||
|
||||
export default pushKeys;
|
@ -1,79 +0,0 @@
|
||||
import publicKeyInfical from '~/pages/api/auth/publicKeyInfisical';
|
||||
import changeHerokuConfigVars from '~/pages/api/integrations/ChangeHerokuConfigVars';
|
||||
|
||||
const crypto = require('crypto');
|
||||
const {
|
||||
encryptSymmetric,
|
||||
encryptAssymmetric
|
||||
} = require('../cryptography/crypto');
|
||||
const nacl = require('tweetnacl');
|
||||
nacl.util = require('tweetnacl-util');
|
||||
|
||||
interface Props {
|
||||
obj: Record<string, string>;
|
||||
integrationId: string;
|
||||
}
|
||||
|
||||
const pushKeysIntegration = async ({ obj, integrationId }: Props) => {
|
||||
const PRIVATE_KEY = localStorage.getItem('PRIVATE_KEY');
|
||||
|
||||
const randomBytes = crypto.randomBytes(16).toString('hex');
|
||||
|
||||
const secrets = Object.keys(obj).map((key) => {
|
||||
// encrypt key
|
||||
const {
|
||||
ciphertext: ciphertextKey,
|
||||
iv: ivKey,
|
||||
tag: tagKey
|
||||
} = encryptSymmetric({
|
||||
plaintext: key,
|
||||
key: randomBytes
|
||||
});
|
||||
|
||||
// encrypt value
|
||||
const {
|
||||
ciphertext: ciphertextValue,
|
||||
iv: ivValue,
|
||||
tag: tagValue
|
||||
} = encryptSymmetric({
|
||||
plaintext: obj[key],
|
||||
key: randomBytes
|
||||
});
|
||||
|
||||
const visibility = 'shared';
|
||||
|
||||
return {
|
||||
ciphertextKey,
|
||||
ivKey,
|
||||
tagKey,
|
||||
hashKey: crypto.createHash('sha256').update(key).digest('hex'),
|
||||
ciphertextValue,
|
||||
ivValue,
|
||||
tagValue,
|
||||
hashValue: crypto.createHash('sha256').update(obj[key]).digest('hex'),
|
||||
type: visibility
|
||||
};
|
||||
});
|
||||
|
||||
// obtain public keys of all receivers (i.e. members in workspace)
|
||||
const publicKeyInfisical = await publicKeyInfical();
|
||||
|
||||
const publicKey = (await publicKeyInfisical.json()).publicKey;
|
||||
|
||||
// assymmetrically encrypt key with each receiver public keys
|
||||
|
||||
const { ciphertext, nonce } = encryptAssymmetric({
|
||||
plaintext: randomBytes,
|
||||
publicKey,
|
||||
privateKey: PRIVATE_KEY
|
||||
});
|
||||
|
||||
const key = {
|
||||
encryptedKey: ciphertext,
|
||||
nonce
|
||||
};
|
||||
|
||||
changeHerokuConfigVars({ integrationId, key, secrets });
|
||||
};
|
||||
|
||||
export default pushKeysIntegration;
|
@ -20,7 +20,6 @@ const getActionData = async ({ actionId }: workspaceProps) => {
|
||||
}
|
||||
}
|
||||
).then(async (res) => {
|
||||
console.log(188, res)
|
||||
if (res && res.status == 200) {
|
||||
return (await res.json()).action;
|
||||
} else {
|
||||
|
30
frontend/ee/api/secrets/PerformSecretRollback.ts
Normal file
30
frontend/ee/api/secrets/PerformSecretRollback.ts
Normal file
@ -0,0 +1,30 @@
|
||||
import SecurityClient from '~/utilities/SecurityClient';
|
||||
|
||||
/**
|
||||
* This function performs a rollback of secrets in a certain project
|
||||
* @param {object} obj
|
||||
* @param {string} obj.workspaceId - id of the project for which we are rolling back data
|
||||
* @param {number} obj.version - version to which we are rolling back
|
||||
* @returns
|
||||
*/
|
||||
const performSecretRollback = async ({ workspaceId, version }: { workspaceId: string; version: number; }) => {
|
||||
return SecurityClient.fetchCall(
|
||||
'/api/v1/workspace/' + workspaceId + "/secret-snapshots/rollback", {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({
|
||||
version
|
||||
})
|
||||
}
|
||||
).then(async (res) => {
|
||||
if (res && res.status == 200) {
|
||||
return (await res.json());
|
||||
} else {
|
||||
console.log('Failed to perform the secret rollback');
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
export default performSecretRollback;
|
@ -111,7 +111,7 @@ const PITRecoverySidebar = ({
|
||||
}
|
||||
})
|
||||
|
||||
setSnapshotData({ id: secretSnapshotData._id, createdAt: secretSnapshotData.createdAt, secretVersions: decryptedSecretVersions })
|
||||
setSnapshotData({ id: secretSnapshotData._id, version: secretSnapshotData.version, createdAt: secretSnapshotData.createdAt, secretVersions: decryptedSecretVersions })
|
||||
}
|
||||
|
||||
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`}>
|
||||
|
48
frontend/pages/api/files/AddSecrets.ts
Normal file
48
frontend/pages/api/files/AddSecrets.ts
Normal file
@ -0,0 +1,48 @@
|
||||
import SecurityClient from '~/utilities/SecurityClient';
|
||||
|
||||
interface EncryptedSecretProps {
|
||||
id: string;
|
||||
createdAt: string;
|
||||
environment: string;
|
||||
secretCommentCiphertext: string;
|
||||
secretCommentIV: string;
|
||||
secretCommentTag: string;
|
||||
secretKeyCiphertext: string;
|
||||
secretKeyIV: string;
|
||||
secretKeyTag: string;
|
||||
secretValueCiphertext: string;
|
||||
secretValueIV: string;
|
||||
secretValueTag: string;
|
||||
type: "personal" | "shared";
|
||||
}
|
||||
|
||||
/**
|
||||
* This function adds secrets to a certain project
|
||||
* @param {object} obj
|
||||
* @param {EncryptedSecretProps} obj.secrets - the ids of secrets that we want to add
|
||||
* @param {string} obj.env - the environment to which we are adding secrets
|
||||
* @param {string} obj.workspaceId - the project to which we are adding secrets
|
||||
* @returns
|
||||
*/
|
||||
const addSecrets = async ({ secrets, env, workspaceId }: { secrets: EncryptedSecretProps[]; env: string; workspaceId: string; }) => {
|
||||
return SecurityClient.fetchCall('/api/v2/secrets', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({
|
||||
environment: env,
|
||||
workspaceId,
|
||||
secrets
|
||||
})
|
||||
}
|
||||
).then(async (res) => {
|
||||
if (res && res.status == 200) {
|
||||
return await res.json();
|
||||
} else {
|
||||
console.log('Failed to add certain project secrets');
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
export default addSecrets;
|
27
frontend/pages/api/files/DeleteSecrets.ts
Normal file
27
frontend/pages/api/files/DeleteSecrets.ts
Normal file
@ -0,0 +1,27 @@
|
||||
import SecurityClient from '~/utilities/SecurityClient';
|
||||
|
||||
/**
|
||||
* This function deletes certain secrets from a certain project
|
||||
* @param {string[]} secretIds - the ids of secrets that we want to be deleted
|
||||
* @returns
|
||||
*/
|
||||
const deleteSecrets = async ({ secretIds }: { secretIds: string[] }) => {
|
||||
return SecurityClient.fetchCall('/api/v2/secrets', {
|
||||
method: 'DELETE',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({
|
||||
secretIds
|
||||
})
|
||||
}
|
||||
).then(async (res) => {
|
||||
if (res && res.status == 200) {
|
||||
return await res.json();
|
||||
} else {
|
||||
console.log('Failed to delete certain project secrets');
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
export default deleteSecrets;
|
@ -1,19 +1,17 @@
|
||||
import SecurityClient from '~/utilities/SecurityClient';
|
||||
|
||||
/**
|
||||
* This function fetches the encrypted secrets from the .env file
|
||||
* This function fetches the encrypted secrets for a certain project
|
||||
* @param {string} workspaceId - project is for which a user is trying to get secrets
|
||||
* @param {string} env - environment of a project for which a user is trying ot get secrets
|
||||
* @returns
|
||||
*/
|
||||
const getSecrets = async (workspaceId: string, env: string) => {
|
||||
return SecurityClient.fetchCall(
|
||||
'/api/v1/secret/' +
|
||||
workspaceId +
|
||||
'?' +
|
||||
'/api/v2/secrets?' +
|
||||
new URLSearchParams({
|
||||
environment: env,
|
||||
channel: 'web'
|
||||
workspaceId
|
||||
}),
|
||||
{
|
||||
method: 'GET',
|
||||
@ -23,7 +21,7 @@ const getSecrets = async (workspaceId: string, env: string) => {
|
||||
}
|
||||
).then(async (res) => {
|
||||
if (res && res.status == 200) {
|
||||
return await res.json();
|
||||
return (await res.json()).secrets;
|
||||
} else {
|
||||
console.log('Failed to get project secrets');
|
||||
}
|
||||
|
44
frontend/pages/api/files/UpdateSecrets.ts
Normal file
44
frontend/pages/api/files/UpdateSecrets.ts
Normal file
@ -0,0 +1,44 @@
|
||||
import SecurityClient from '~/utilities/SecurityClient';
|
||||
|
||||
interface EncryptedSecretProps {
|
||||
id: string;
|
||||
createdAt: string;
|
||||
environment: string;
|
||||
secretCommentCiphertext: string;
|
||||
secretCommentIV: string;
|
||||
secretCommentTag: string;
|
||||
secretKeyCiphertext: string;
|
||||
secretKeyIV: string;
|
||||
secretKeyTag: string;
|
||||
secretValueCiphertext: string;
|
||||
secretValueIV: string;
|
||||
secretValueTag: string;
|
||||
type: "personal" | "shared";
|
||||
}
|
||||
|
||||
/**
|
||||
* This function updates certain secrets in a certain project
|
||||
* @param {object} obj
|
||||
* @param {EncryptedSecretProps[]} obj.secrets - the ids of secrets that we want to update
|
||||
* @returns
|
||||
*/
|
||||
const updateSecrets = async ({ secrets }: { secrets: EncryptedSecretProps[] }) => {
|
||||
return SecurityClient.fetchCall('/api/v2/secrets', {
|
||||
method: 'PATCH',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({
|
||||
secrets
|
||||
})
|
||||
}
|
||||
).then(async (res) => {
|
||||
if (res && res.status == 200) {
|
||||
return await res.json();
|
||||
} else {
|
||||
console.log('Failed to update certain project secrets');
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
export default updateSecrets;
|
@ -9,7 +9,6 @@ import {
|
||||
faArrowLeft,
|
||||
faCheck,
|
||||
faClockRotateLeft,
|
||||
faDownload,
|
||||
faEye,
|
||||
faEyeSlash,
|
||||
faFolderOpen,
|
||||
@ -18,28 +17,32 @@ import {
|
||||
} from '@fortawesome/free-solid-svg-icons';
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||
import getProjectSercetSnapshotsCount from 'ee/api/secrets/GetProjectSercetSnapshotsCount';
|
||||
import performSecretRollback from 'ee/api/secrets/PerformSecretRollback';
|
||||
import PITRecoverySidebar from 'ee/components/PITRecoverySidebar';
|
||||
|
||||
import Button from '~/components/basic/buttons/Button';
|
||||
import ListBox from '~/components/basic/Listbox';
|
||||
import BottonRightPopup from '~/components/basic/popups/BottomRightPopup';
|
||||
import { useNotificationContext } from '~/components/context/Notifications/NotificationProvider';
|
||||
import DownloadSecretMenu from '~/components/dashboard/DownloadSecretsMenu';
|
||||
import DropZone from '~/components/dashboard/DropZone';
|
||||
import KeyPair from '~/components/dashboard/KeyPair';
|
||||
import SideBar from '~/components/dashboard/SideBar';
|
||||
import NavHeader from '~/components/navigation/NavHeader';
|
||||
import encryptSecrets from '~/components/utilities/secrets/encryptSecrets';
|
||||
import getSecretsForProject from '~/components/utilities/secrets/getSecretsForProject';
|
||||
import pushKeys from '~/components/utilities/secrets/pushKeys';
|
||||
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';
|
||||
import getUser from '../api/user/getUser';
|
||||
import checkUserAction from '../api/userActions/checkUserAction';
|
||||
import registerUserAction from '../api/userActions/registerUserAction';
|
||||
import getWorkspaces from '../api/workspace/getWorkspaces';
|
||||
|
||||
const queryString = require("query-string");
|
||||
|
||||
interface SecretDataProps {
|
||||
type: 'personal' | 'shared';
|
||||
@ -50,9 +53,18 @@ interface SecretDataProps {
|
||||
comment: string;
|
||||
}
|
||||
|
||||
interface overrideProps {
|
||||
id: string;
|
||||
keyName: string;
|
||||
value: string;
|
||||
pos: number;
|
||||
comment: string;
|
||||
}
|
||||
|
||||
interface SnapshotProps {
|
||||
id: string;
|
||||
createdAt: string;
|
||||
version: number;
|
||||
secretVersions: {
|
||||
id: string;
|
||||
pos: number;
|
||||
@ -87,7 +99,7 @@ function findDuplicates(arr: any[]) {
|
||||
*/
|
||||
export default function Dashboard() {
|
||||
const [data, setData] = useState<SecretDataProps[] | null>();
|
||||
const [fileState, setFileState] = useState<SecretDataProps[]>([]);
|
||||
const [initialData, setInitialData] = useState<SecretDataProps[]>([]);
|
||||
const [buttonReady, setButtonReady] = useState(false);
|
||||
const router = useRouter();
|
||||
const [workspaceId, setWorkspaceId] = useState('');
|
||||
@ -194,11 +206,11 @@ export default function Dashboard() {
|
||||
|
||||
const dataToSort = await getSecretsForProject({
|
||||
env,
|
||||
setFileState,
|
||||
setIsKeyAvailable,
|
||||
setData,
|
||||
workspaceId: String(router.query.id)
|
||||
});
|
||||
setInitialData(dataToSort);
|
||||
reorderRows(dataToSort);
|
||||
|
||||
setSharedToHide(
|
||||
@ -234,14 +246,6 @@ export default function Dashboard() {
|
||||
]);
|
||||
};
|
||||
|
||||
interface overrideProps {
|
||||
id: string;
|
||||
keyName: string;
|
||||
value: string;
|
||||
pos: number;
|
||||
comment: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* This function add an ovverrided version of a certain secret to the current user
|
||||
* @param {object} obj
|
||||
@ -273,12 +277,12 @@ export default function Dashboard() {
|
||||
text: `${secretName} has been deleted. Remember to save changes.`,
|
||||
type: 'error'
|
||||
});
|
||||
setData(data!.filter((row: SecretDataProps) => !ids.includes(row.id)));
|
||||
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 secret to be deleted
|
||||
* @param {string} id - id of a shared secret; the override with the same key should be deleted
|
||||
*/
|
||||
const deleteOverride = (id: string) => {
|
||||
setButtonReady(true);
|
||||
@ -291,7 +295,7 @@ export default function Dashboard() {
|
||||
setSharedToHide(sharedToHide!.filter(tempId => tempId != sharedVersionOfOverride))
|
||||
|
||||
// resort secrets
|
||||
const tempData = data!.filter((row: SecretDataProps) => !(row.id == id && row.type == 'personal'))
|
||||
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")
|
||||
};
|
||||
|
||||
@ -311,14 +315,6 @@ export default function Dashboard() {
|
||||
setButtonReady(true);
|
||||
};
|
||||
|
||||
const modifyVisibility = (value: "shared" | "personal", pos: number) => {
|
||||
setData((oldData) => {
|
||||
oldData![pos].type = value;
|
||||
return [...oldData!];
|
||||
});
|
||||
setButtonReady(true);
|
||||
};
|
||||
|
||||
const modifyComment = (value: string, pos: number) => {
|
||||
setData((oldData) => {
|
||||
oldData![pos].comment = value;
|
||||
@ -336,10 +332,6 @@ export default function Dashboard() {
|
||||
modifyKey(value, pos);
|
||||
}, []);
|
||||
|
||||
const listenChangeVisibility = useCallback((value: "shared" | "personal", pos: number) => {
|
||||
modifyVisibility(value, pos);
|
||||
}, []);
|
||||
|
||||
const listenChangeComment = useCallback((value: string, pos: number) => {
|
||||
modifyComment(value, pos);
|
||||
}, []);
|
||||
@ -347,22 +339,20 @@ export default function Dashboard() {
|
||||
/**
|
||||
* Save the changes of environment variables and push them to the database
|
||||
*/
|
||||
const savePush = async (dataToPush?: any[], envToPush?: string) => {
|
||||
let obj;
|
||||
const savePush = async (dataToPush?: SecretDataProps[]) => {
|
||||
let newData: SecretDataProps[] | null | undefined;
|
||||
// dataToPush is mostly used for rollbacks, otherwise we always take the current state data
|
||||
if ((dataToPush ?? [])?.length > 0) {
|
||||
obj = Object.assign(
|
||||
{},
|
||||
...dataToPush!.map((row: SecretDataProps) => ({ [row.type.charAt(0) + row.key]: [row.value, row.comment ?? ''] }))
|
||||
);
|
||||
newData = dataToPush;
|
||||
} else {
|
||||
// Format the new object with environment variables
|
||||
obj = Object.assign(
|
||||
{},
|
||||
...data!.map((row: SecretDataProps) => ({ [row.type.charAt(0) + row.key]: [row.value, row.comment ?? ''] }))
|
||||
);
|
||||
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))))
|
||||
@ -383,10 +373,37 @@ export default function Dashboard() {
|
||||
});
|
||||
}
|
||||
|
||||
// Once "Save changed is clicked", disable that button
|
||||
// Once "Save changes" is clicked, disable that button
|
||||
setButtonReady(false);
|
||||
console.log(envToPush ? envToPush : env, env, envToPush)
|
||||
pushKeys({ obj, workspaceId: String(router.query.id), env: envToPush ? envToPush : env });
|
||||
|
||||
const secretsToBeDeleted
|
||||
= initialData
|
||||
.filter(initDataPoint => !newData!.map(newDataPoint => newDataPoint.id).includes(initDataPoint.id))
|
||||
.map(secret => secret.id);
|
||||
|
||||
const secretsToBeAdded
|
||||
= newData!
|
||||
.filter(newDataPoint => !initialData.map(initDataPoint => initDataPoint.id).includes(newDataPoint.id));
|
||||
|
||||
const secretsToBeUpdated
|
||||
= 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));
|
||||
|
||||
if (secretsToBeDeleted.length > 0) {
|
||||
await deleteSecrets({ secretIds: secretsToBeDeleted });
|
||||
}
|
||||
if (secretsToBeAdded.length > 0) {
|
||||
const secrets = await encryptSecrets({ secretsToEncrypt: secretsToBeAdded, workspaceId, env: envMapping[env] })
|
||||
await addSecrets({ secrets, env: envMapping[env], workspaceId });
|
||||
}
|
||||
if (secretsToBeUpdated.length > 0) {
|
||||
const secrets = await encryptSecrets({ secretsToEncrypt: secretsToBeUpdated, workspaceId, env: envMapping[env] })
|
||||
await updateSecrets({ secrets });
|
||||
}
|
||||
|
||||
// If this user has never saved environment variables before, show them a prompt to read docs
|
||||
if (!hasUserEverPushed) {
|
||||
@ -425,19 +442,6 @@ export default function Dashboard() {
|
||||
setData(sortedData);
|
||||
};
|
||||
|
||||
// This function downloads the secrets as a .env file
|
||||
const download = () => {
|
||||
const file = data!
|
||||
.map((item: SecretDataProps) => [item.key, item.value].join('='))
|
||||
.join('\n');
|
||||
const blob = new Blob([file]);
|
||||
const fileDownloadUrl = URL.createObjectURL(blob);
|
||||
const alink = document.createElement('a');
|
||||
alink.href = fileDownloadUrl;
|
||||
alink.download = envMapping[env] + '.env';
|
||||
alink.click();
|
||||
};
|
||||
|
||||
const deleteCertainRow = ({ ids, secretName }: { ids: string[]; secretName: string; }) => {
|
||||
deleteRow({ids, secretName});
|
||||
};
|
||||
@ -535,8 +539,6 @@ export default function Dashboard() {
|
||||
<Button
|
||||
text={String(t("Rollback to this snapshot"))}
|
||||
onButtonPressed={async () => {
|
||||
const envsToRollback = snapshotData.secretVersions.map(sv => sv.environment).filter((v, i, a) => a.indexOf(v) === i);
|
||||
|
||||
// Update secrets in the state only for the current environment
|
||||
setData(
|
||||
snapshotData.secretVersions
|
||||
@ -548,18 +550,9 @@ export default function Dashboard() {
|
||||
})
|
||||
);
|
||||
|
||||
// Rollback each of the environments in the snapshot
|
||||
// #TODO: clean up other environments
|
||||
envsToRollback.map(async (envToRollback) => {
|
||||
await savePush(
|
||||
snapshotData.secretVersions
|
||||
.filter(row => row.environment == envToRollback)
|
||||
.map((sv, position) => {
|
||||
return {id: sv.id, pos: position, type: sv.type, key: sv.key, value: sv.value, comment: ''}
|
||||
}),
|
||||
reverseEnvMapping[envToRollback]
|
||||
);
|
||||
});
|
||||
// Perform the rollback globally
|
||||
performSecretRollback({ workspaceId, version: snapshotData.version })
|
||||
|
||||
setSnapshotData(undefined);
|
||||
createNotification({
|
||||
text: `Rollback has been performed successfully.`,
|
||||
@ -614,12 +607,7 @@ export default function Dashboard() {
|
||||
/>
|
||||
</div>}
|
||||
{!snapshotData && <div className="ml-2 min-w-max flex flex-row items-start justify-start">
|
||||
<Button
|
||||
onButtonPressed={download}
|
||||
color="mineshaft"
|
||||
size="icon-md"
|
||||
icon={faDownload}
|
||||
/>
|
||||
<DownloadSecretMenu data={data} env={env} />
|
||||
</div>}
|
||||
<div className="ml-2 min-w-max flex flex-row items-start justify-start">
|
||||
<Button
|
||||
@ -672,7 +660,9 @@ export default function Dashboard() {
|
||||
modifyValue={listenChangeValue}
|
||||
modifyKey={listenChangeKey}
|
||||
isBlurred={blurred}
|
||||
isDuplicate={findDuplicates(data?.map((item) => item.key + item.type))?.includes(keyPair.key + keyPair.type)}
|
||||
isDuplicate={findDuplicates(
|
||||
data?.map((item) => item.key + item.type)
|
||||
)?.includes(keyPair.key + keyPair.type)}
|
||||
toggleSidebar={toggleSidebar}
|
||||
sidebarSecretId={sidebarSecretId}
|
||||
isSnapshot={false}
|
||||
@ -694,7 +684,9 @@ export default function Dashboard() {
|
||||
modifyValue={listenChangeValue}
|
||||
modifyKey={listenChangeKey}
|
||||
isBlurred={blurred}
|
||||
isDuplicate={findDuplicates(data?.map((item) => item.key + item.type))?.includes(keyPair.key + keyPair.type)}
|
||||
isDuplicate={findDuplicates(
|
||||
data?.map((item) => item.key + item.type)
|
||||
)?.includes(keyPair.key + keyPair.type)}
|
||||
toggleSidebar={toggleSidebar}
|
||||
sidebarSecretId={sidebarSecretId}
|
||||
isSnapshot={true}
|
||||
@ -728,7 +720,6 @@ export default function Dashboard() {
|
||||
/>
|
||||
)}
|
||||
{
|
||||
// fileState.message == 'Access needed to pull the latest file' ||
|
||||
(!isKeyAvailable && (
|
||||
<>
|
||||
<FontAwesomeIcon
|
||||
|
Reference in New Issue
Block a user