mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-09-22 13:39:35 +00:00
Merge branch 'Infisical:main' into feat/translation-fr
This commit is contained in:
@@ -19,7 +19,7 @@ import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import getOrganizations from "~/pages/api/organization/getOrgs";
|
||||
import getOrganizationUserProjects from "~/pages/api/organization/GetOrgUserProjects";
|
||||
import getOrganizationUsers from "~/pages/api/organization/GetOrgUsers";
|
||||
import checkUserAction from "~/pages/api/userActions/checkUserAction";
|
||||
import getUser from "~/pages/api/user/getUser";
|
||||
import addUserToWorkspace from "~/pages/api/workspace/addUserToWorkspace";
|
||||
import createWorkspace from "~/pages/api/workspace/createWorkspace";
|
||||
import getWorkspaces from "~/pages/api/workspace/getWorkspaces";
|
||||
@@ -39,6 +39,7 @@ import Listbox from "./Listbox";
|
||||
interface LayoutProps {
|
||||
children: React.ReactNode;
|
||||
}
|
||||
const crypto = require("crypto");
|
||||
|
||||
export default function Layout({ children }: LayoutProps) {
|
||||
const router = useRouter();
|
||||
@@ -83,12 +84,31 @@ export default function Layout({ children }: LayoutProps) {
|
||||
});
|
||||
const newWorkspaceId = newWorkspace._id;
|
||||
|
||||
const randomBytes = crypto.randomBytes(16).toString("hex");
|
||||
const PRIVATE_KEY = String(localStorage.getItem("PRIVATE_KEY"));
|
||||
|
||||
const myUser = await getUser();
|
||||
|
||||
const { ciphertext, nonce } = encryptAssymmetric({
|
||||
plaintext: randomBytes,
|
||||
publicKey: myUser.publicKey,
|
||||
privateKey: PRIVATE_KEY,
|
||||
}) as { ciphertext: string; nonce: string };
|
||||
|
||||
await uploadKeys(
|
||||
newWorkspaceId,
|
||||
myUser._id,
|
||||
ciphertext,
|
||||
nonce
|
||||
);
|
||||
|
||||
if (addAllUsers) {
|
||||
console.log('adding other users')
|
||||
const orgUsers = await getOrganizationUsers({
|
||||
orgId: tempLocalStorage("orgData.id"),
|
||||
});
|
||||
orgUsers.map(async (user: any) => {
|
||||
if (user.status == "accepted") {
|
||||
if (user.status == "accepted" && user.email != myUser.email) {
|
||||
const result = await addUserToWorkspace(
|
||||
user.user.email,
|
||||
newWorkspaceId
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -36,7 +36,7 @@ export default function 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-xl absolute bottom-0 right-0 mr-6 mb-6"
|
||||
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"
|
||||
role="alert"
|
||||
>
|
||||
<div className="flex flex-row items-center justify-between w-full border-b border-gray-600/70 pb-3 px-6">
|
||||
|
||||
@@ -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;
|
||||
@@ -163,7 +163,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]`}>
|
||||
|
||||
@@ -23,6 +23,7 @@ import {
|
||||
interface Integration {
|
||||
_id: string;
|
||||
app?: string;
|
||||
target?: string;
|
||||
environment: string;
|
||||
integration: string;
|
||||
integrationAuth: string;
|
||||
@@ -69,7 +70,11 @@ const Integration = ({
|
||||
|
||||
switch (integration.integration) {
|
||||
case "vercel":
|
||||
setIntegrationTarget("Development");
|
||||
setIntegrationTarget(
|
||||
integration?.target
|
||||
? integration.target.charAt(0).toUpperCase() + integration.target.substring(1)
|
||||
: "Development"
|
||||
);
|
||||
break;
|
||||
case "netlify":
|
||||
setIntegrationContext(integration?.context ? contextNetlifyMapping[integration.context] : "Local development");
|
||||
@@ -93,11 +98,11 @@ const Integration = ({
|
||||
</div>
|
||||
<ListBox
|
||||
data={!integration.isActive ? [
|
||||
"Production",
|
||||
"Development",
|
||||
"Preview",
|
||||
"Development"
|
||||
"Production"
|
||||
] : null}
|
||||
selected={"Production"}
|
||||
selected={integrationTarget}
|
||||
onChange={setIntegrationTarget}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -112,9 +112,9 @@ export default function Navbar() {
|
||||
href="https://infisical.com/docs/getting-started/introduction"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="text-gray-200 hover:text-primary duration-200">
|
||||
className="text-gray-200 hover:bg-white/10 px-3 rounded-md duration-200 text-sm mr-4 py-2 flex items-center">
|
||||
<FontAwesomeIcon icon={faBook} className="text-xl mr-2" />
|
||||
Docs
|
||||
<FontAwesomeIcon icon={faUpRightFromSquare} className="text-xs mb-[0.1rem] mr-5 ml-1.5" />
|
||||
</a>
|
||||
<Menu as="div" className="relative inline-block text-left">
|
||||
<div className="mr-4">
|
||||
|
||||
@@ -1,147 +0,0 @@
|
||||
import Aes256Gcm from '~/components/utilities/cryptography/aes-256-gcm';
|
||||
import login1 from '~/pages/api/auth/Login1';
|
||||
import login2 from '~/pages/api/auth/Login2';
|
||||
import getOrganizations from '~/pages/api/organization/getOrgs';
|
||||
import getOrganizationUserProjects from '~/pages/api/organization/GetOrgUserProjects';
|
||||
|
||||
import pushKeys from './secrets/pushKeys';
|
||||
import Telemetry from './telemetry/Telemetry';
|
||||
import { saveTokenToLocalStorage } from './saveTokenToLocalStorage';
|
||||
import SecurityClient from './SecurityClient';
|
||||
|
||||
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
|
||||
* @param {*} router
|
||||
* @param {*} isSignUp
|
||||
* @returns
|
||||
*/
|
||||
const attemptLogin = async (
|
||||
email,
|
||||
password,
|
||||
setErrorLogin,
|
||||
router,
|
||||
isSignUp,
|
||||
isLogin
|
||||
) => {
|
||||
try {
|
||||
const telemetry = new Telemetry().getInstance();
|
||||
|
||||
client.init(
|
||||
{
|
||||
username: email,
|
||||
password: password
|
||||
},
|
||||
async () => {
|
||||
const clientPublicKey = client.getPublicKey();
|
||||
|
||||
try {
|
||||
const { serverPublicKey, salt } = await login1(email, clientPublicKey);
|
||||
|
||||
client.setSalt(salt);
|
||||
client.setServerPublicKey(serverPublicKey);
|
||||
const clientProof = client.getProof(); // called M1
|
||||
|
||||
// if everything works, go the main dashboard page.
|
||||
const { token, publicKey, encryptedPrivateKey, iv, tag } =
|
||||
await login2(email, clientProof);
|
||||
|
||||
SecurityClient.setToken(token);
|
||||
|
||||
const privateKey = Aes256Gcm.decrypt({
|
||||
ciphertext: encryptedPrivateKey,
|
||||
iv,
|
||||
tag,
|
||||
secret: password
|
||||
.slice(0, 32)
|
||||
.padStart(
|
||||
32 + (password.slice(0, 32).length - new Blob([password]).size),
|
||||
'0'
|
||||
)
|
||||
});
|
||||
|
||||
saveTokenToLocalStorage({
|
||||
publicKey,
|
||||
encryptedPrivateKey,
|
||||
iv,
|
||||
tag,
|
||||
privateKey
|
||||
});
|
||||
|
||||
const userOrgs = await getOrganizations();
|
||||
const userOrgsData = userOrgs.map((org) => org._id);
|
||||
|
||||
let orgToLogin;
|
||||
if (userOrgsData.includes(localStorage.getItem('orgData.id'))) {
|
||||
orgToLogin = localStorage.getItem('orgData.id');
|
||||
} else {
|
||||
orgToLogin = userOrgsData[0];
|
||||
localStorage.setItem('orgData.id', orgToLogin);
|
||||
}
|
||||
|
||||
let orgUserProjects = await getOrganizationUserProjects({
|
||||
orgId: orgToLogin
|
||||
});
|
||||
|
||||
orgUserProjects = orgUserProjects?.map((project) => project._id);
|
||||
let projectToLogin;
|
||||
if (
|
||||
orgUserProjects.includes(localStorage.getItem('projectData.id'))
|
||||
) {
|
||||
projectToLogin = localStorage.getItem('projectData.id');
|
||||
} else {
|
||||
try {
|
||||
projectToLogin = orgUserProjects[0];
|
||||
localStorage.setItem('projectData.id', projectToLogin);
|
||||
} catch (error) {
|
||||
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');
|
||||
}
|
||||
|
||||
if (isLogin) {
|
||||
router.push('/dashboard/');
|
||||
}
|
||||
} catch (error) {
|
||||
setErrorLogin(true);
|
||||
console.log('Login response not available');
|
||||
}
|
||||
}
|
||||
);
|
||||
} catch (error) {
|
||||
console.log('Something went wrong during authentication');
|
||||
}
|
||||
return true;
|
||||
};
|
||||
|
||||
export default attemptLogin;
|
||||
217
frontend/components/utilities/attemptLogin.ts
Normal file
217
frontend/components/utilities/attemptLogin.ts
Normal file
@@ -0,0 +1,217 @@
|
||||
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 getUser from '~/pages/api/user/getUser';
|
||||
import uploadKeys from '~/pages/api/workspace/uploadKeys';
|
||||
|
||||
import { encryptAssymmetric } from './cryptography/crypto';
|
||||
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 crypto = require("crypto");
|
||||
const nacl = require('tweetnacl');
|
||||
nacl.util = require('tweetnacl-util');
|
||||
const jsrp = require('jsrp');
|
||||
const client = new jsrp.client();
|
||||
|
||||
/**
|
||||
* 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 {boolean} isSignUp - whether this log in is a part of signup
|
||||
* @param {boolean} isLogin - ?
|
||||
* @returns
|
||||
*/
|
||||
const attemptLogin = async (
|
||||
email: string,
|
||||
password: string,
|
||||
setErrorLogin: (value: boolean) => void,
|
||||
router: any,
|
||||
isSignUp: boolean,
|
||||
isLogin: boolean
|
||||
) => {
|
||||
try {
|
||||
const telemetry = new Telemetry().getInstance();
|
||||
|
||||
client.init(
|
||||
{
|
||||
username: email,
|
||||
password: password
|
||||
},
|
||||
async () => {
|
||||
const clientPublicKey = client.getPublicKey();
|
||||
|
||||
try {
|
||||
const { serverPublicKey, salt } = await login1(email, clientPublicKey);
|
||||
|
||||
client.setSalt(salt);
|
||||
client.setServerPublicKey(serverPublicKey);
|
||||
const clientProof = client.getProof(); // called M1
|
||||
|
||||
// if everything works, go the main dashboard page.
|
||||
const { token, publicKey, encryptedPrivateKey, iv, tag } =
|
||||
await login2(email, clientProof);
|
||||
|
||||
SecurityClient.setToken(token);
|
||||
|
||||
const privateKey = Aes256Gcm.decrypt({
|
||||
ciphertext: encryptedPrivateKey,
|
||||
iv,
|
||||
tag,
|
||||
secret: password
|
||||
.slice(0, 32)
|
||||
.padStart(
|
||||
32 + (password.slice(0, 32).length - new Blob([password]).size),
|
||||
'0'
|
||||
)
|
||||
});
|
||||
|
||||
saveTokenToLocalStorage({
|
||||
publicKey,
|
||||
encryptedPrivateKey,
|
||||
iv,
|
||||
tag,
|
||||
privateKey
|
||||
});
|
||||
|
||||
const userOrgs = await getOrganizations();
|
||||
const userOrgsData = userOrgs.map((org: { _id: string; }) => org._id);
|
||||
|
||||
let orgToLogin;
|
||||
if (userOrgsData.includes(localStorage.getItem('orgData.id'))) {
|
||||
orgToLogin = localStorage.getItem('orgData.id');
|
||||
} else {
|
||||
orgToLogin = userOrgsData[0];
|
||||
localStorage.setItem('orgData.id', orgToLogin);
|
||||
}
|
||||
|
||||
let orgUserProjects = await getOrganizationUserProjects({
|
||||
orgId: orgToLogin
|
||||
});
|
||||
|
||||
orgUserProjects = orgUserProjects?.map((project: { _id: string; }) => project._id);
|
||||
let projectToLogin;
|
||||
if (
|
||||
orgUserProjects.includes(localStorage.getItem('projectData.id'))
|
||||
) {
|
||||
projectToLogin = localStorage.getItem('projectData.id');
|
||||
} else {
|
||||
try {
|
||||
projectToLogin = orgUserProjects[0];
|
||||
localStorage.setItem('projectData.id', projectToLogin);
|
||||
} catch (error) {
|
||||
console.log('ERROR: User likely has no projects. ', error);
|
||||
}
|
||||
}
|
||||
|
||||
if (email) {
|
||||
telemetry.identify(email);
|
||||
telemetry.capture('User Logged In');
|
||||
}
|
||||
|
||||
if (isSignUp) {
|
||||
const randomBytes = crypto.randomBytes(16).toString("hex");
|
||||
const PRIVATE_KEY = String(localStorage.getItem("PRIVATE_KEY"));
|
||||
|
||||
const myUser = await getUser();
|
||||
|
||||
const { ciphertext, nonce } = encryptAssymmetric({
|
||||
plaintext: randomBytes,
|
||||
publicKey: myUser.publicKey,
|
||||
privateKey: PRIVATE_KEY,
|
||||
}) as { ciphertext: string; nonce: string };
|
||||
|
||||
await uploadKeys(
|
||||
projectToLogin,
|
||||
myUser._id,
|
||||
ciphertext,
|
||||
nonce
|
||||
);
|
||||
|
||||
const secretsToBeAdded: SecretDataProps[] = [{
|
||||
type: "shared",
|
||||
pos: 0,
|
||||
key: "DATABASE_URL",
|
||||
value: "mongodb+srv://${DB_USERNAME}:${DB_PASSWORD}@mongodb.net",
|
||||
comment: "This is an example of secret referencing.",
|
||||
id: ''
|
||||
}, {
|
||||
type: "shared",
|
||||
pos: 1,
|
||||
key: "DB_USERNAME",
|
||||
value: "OVERRIDE_THIS",
|
||||
comment: "This is an example of secret overriding. Your team can have a shared value of a secret, while you can override it to whatever value you need",
|
||||
id: ''
|
||||
}, {
|
||||
type: "personal",
|
||||
pos: 2,
|
||||
key: "DB_USERNAME",
|
||||
value: "user1234",
|
||||
comment: "",
|
||||
id: ''
|
||||
}, {
|
||||
type: "shared",
|
||||
pos: 3,
|
||||
key: "DB_PASSWORD",
|
||||
value: "OVERRIDE_THIS",
|
||||
comment: "This is an example of secret overriding. Your team can have a shared value of a secret, while you can override it to whatever value you need",
|
||||
id: ''
|
||||
}, {
|
||||
type: "personal",
|
||||
pos: 4,
|
||||
key: "DB_PASSWORD",
|
||||
value: "example_password",
|
||||
comment: "",
|
||||
id: ''
|
||||
}, {
|
||||
type: "shared",
|
||||
pos: 5,
|
||||
key: "TWILIO_AUTH_TOKEN",
|
||||
value: "example_twillio_token",
|
||||
comment: "This is an example of secret overriding. Your team can have a shared value of a secret, while you can override it to whatever value you need",
|
||||
id: ''
|
||||
}, {
|
||||
type: "shared",
|
||||
pos: 6,
|
||||
key: "WEBSITE_URL",
|
||||
value: "http://localhost:3000",
|
||||
comment: "This is an example of secret overriding. Your team can have a shared value of a secret, while you can override it to whatever value you need",
|
||||
id: ''
|
||||
}]
|
||||
const secrets = await encryptSecrets({ secretsToEncrypt: secretsToBeAdded, workspaceId: String(localStorage.getItem('projectData.id')), env: 'dev' })
|
||||
await addSecrets({ secrets: secrets ?? [], env: "dev", workspaceId: String(localStorage.getItem('projectData.id')) });
|
||||
}
|
||||
|
||||
if (isLogin) {
|
||||
router.push('/dashboard/' + localStorage.getItem('projectData.id'));
|
||||
}
|
||||
} catch (error) {
|
||||
console.log(error)
|
||||
setErrorLogin(true);
|
||||
console.log('Login response not available');
|
||||
}
|
||||
}
|
||||
);
|
||||
} catch (error) {
|
||||
console.log('Something went wrong during authentication');
|
||||
}
|
||||
return true;
|
||||
};
|
||||
|
||||
export default attemptLogin;
|
||||
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;
|
||||
53
frontend/components/utilities/secrets/downloadYaml.ts
Normal file
53
frontend/components/utilities/secrets/downloadYaml.ts
Normal file
@@ -0,0 +1,53 @@
|
||||
// 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();
|
||||
return;
|
||||
}
|
||||
|
||||
export default downloadYaml;
|
||||
122
frontend/components/utilities/secrets/encryptSecrets.ts
Normal file
122
frontend/components/utilities/secrets/encryptSecrets.ts
Normal file
@@ -0,0 +1,122 @@
|
||||
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; }) => {
|
||||
let secrets;
|
||||
try {
|
||||
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");
|
||||
}
|
||||
|
||||
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;
|
||||
});
|
||||
} catch (error) {
|
||||
console.log("Error while encrypting secrets");
|
||||
}
|
||||
|
||||
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;
|
||||
@@ -72,10 +72,8 @@ const ActivityLogsRow = ({ row, toggleSidebar }: { row: logData, toggleSidebar:
|
||||
<td>{String(t("common:timestamp"))}</td>
|
||||
<td>{row.createdAt}</td>
|
||||
</tr>}
|
||||
{payloadOpened &&
|
||||
row.payload?.map((action, index) => {
|
||||
action.secretVersions.length > 0 &&
|
||||
<tr key={index} className="h-9 text-bunker-200 border-mineshaft-700 border-t text-sm">
|
||||
{payloadOpened && row.payload?.map((action, index) => {
|
||||
return action.secretVersions.length > 0 && <tr key={index} className="h-9 text-bunker-200 border-mineshaft-700 border-t text-sm">
|
||||
<td></td>
|
||||
<td className="">{t("activity:event." + action.name)}</td>
|
||||
<td className="text-primary-300 cursor-pointer hover:text-primary duration-200" onClick={() => toggleSidebar(action._id)}>
|
||||
@@ -87,7 +85,7 @@ const ActivityLogsRow = ({ row, toggleSidebar }: { row: logData, toggleSidebar:
|
||||
{payloadOpened &&
|
||||
<tr className='h-9 text-bunker-200 border-mineshaft-700 border-t text-sm'>
|
||||
<td></td>
|
||||
<td>{String(t("common:ip-address"))}</td>
|
||||
<td>{String(t("activity:ip-address"))}</td>
|
||||
<td>{row.ipAddress}</td>
|
||||
</tr>}
|
||||
</>
|
||||
|
||||
@@ -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`}>
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
*/
|
||||
module.exports = {
|
||||
// https://www.i18next.com/overview/configuration-options#logging
|
||||
debug: process.env.NODE_ENV === "development",
|
||||
debug: false,
|
||||
i18n: {
|
||||
defaultLocale: "en",
|
||||
locales: ["en", "ko", "fr"],
|
||||
|
||||
25
frontend/package-lock.json
generated
25
frontend/package-lock.json
generated
@@ -4,6 +4,7 @@
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "frontend",
|
||||
"dependencies": {
|
||||
"@emotion/css": "^11.10.0",
|
||||
"@emotion/server": "^11.10.0",
|
||||
@@ -4639,9 +4640,9 @@
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/json5": {
|
||||
"version": "2.2.1",
|
||||
"resolved": "https://registry.npmjs.org/json5/-/json5-2.2.1.tgz",
|
||||
"integrity": "sha512-1hqLFMSrGHRHxav9q9gNjJ5EXznIxGVO09xQRrwplcS8qs28pZ8s8hupZAmqDwZUmVZ2Qb2jnyPOWcDH8m8dlA==",
|
||||
"version": "2.2.3",
|
||||
"resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz",
|
||||
"integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==",
|
||||
"peer": true,
|
||||
"bin": {
|
||||
"json5": "lib/cli.js"
|
||||
@@ -7454,9 +7455,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/tsconfig-paths/node_modules/json5": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/json5/-/json5-1.0.1.tgz",
|
||||
"integrity": "sha512-aKS4WQjPenRxiQsC93MNfjx+nbF4PAdYzmd/1JIj8HYzqfbu86beTuNgXDzPknWk0n0uARlyewZo4s++ES36Ow==",
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/json5/-/json5-1.0.2.tgz",
|
||||
"integrity": "sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"minimist": "^1.2.0"
|
||||
@@ -11211,9 +11212,9 @@
|
||||
"dev": true
|
||||
},
|
||||
"json5": {
|
||||
"version": "2.2.1",
|
||||
"resolved": "https://registry.npmjs.org/json5/-/json5-2.2.1.tgz",
|
||||
"integrity": "sha512-1hqLFMSrGHRHxav9q9gNjJ5EXznIxGVO09xQRrwplcS8qs28pZ8s8hupZAmqDwZUmVZ2Qb2jnyPOWcDH8m8dlA==",
|
||||
"version": "2.2.3",
|
||||
"resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz",
|
||||
"integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==",
|
||||
"peer": true
|
||||
},
|
||||
"jsonp": {
|
||||
@@ -13118,9 +13119,9 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"json5": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/json5/-/json5-1.0.1.tgz",
|
||||
"integrity": "sha512-aKS4WQjPenRxiQsC93MNfjx+nbF4PAdYzmd/1JIj8HYzqfbu86beTuNgXDzPknWk0n0uARlyewZo4s++ES36Ow==",
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/json5/-/json5-1.0.2.tgz",
|
||||
"integrity": "sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"minimist": "^1.2.0"
|
||||
|
||||
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,
|
||||
@@ -17,31 +16,33 @@ import {
|
||||
faPlus,
|
||||
} from '@fortawesome/free-solid-svg-icons';
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||
import { Menu, Transition } from '@headlessui/react';
|
||||
import getProjectSercetSnapshotsCount from 'ee/api/secrets/GetProjectSercetSnapshotsCount';
|
||||
import performSecretRollback from 'ee/api/secrets/PerformSecretRollback';
|
||||
import PITRecoverySidebar from 'ee/components/PITRecoverySidebar';
|
||||
import { Document, YAMLSeq } from 'yaml';
|
||||
|
||||
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';
|
||||
@@ -52,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;
|
||||
@@ -89,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('');
|
||||
@@ -196,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(
|
||||
@@ -236,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
|
||||
@@ -275,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);
|
||||
@@ -293,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")
|
||||
};
|
||||
|
||||
@@ -313,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;
|
||||
@@ -338,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);
|
||||
}, []);
|
||||
@@ -349,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))))
|
||||
@@ -385,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] })
|
||||
secrets && await addSecrets({ secrets, env: envMapping[env], workspaceId });
|
||||
}
|
||||
if (secretsToBeUpdated.length > 0) {
|
||||
const secrets = await encryptSecrets({ secretsToEncrypt: secretsToBeUpdated, workspaceId, env: envMapping[env] })
|
||||
secrets && await updateSecrets({ secrets });
|
||||
}
|
||||
|
||||
// If this user has never saved environment variables before, show them a prompt to read docs
|
||||
if (!hasUserEverPushed) {
|
||||
@@ -426,79 +441,7 @@ export default function Dashboard() {
|
||||
|
||||
setData(sortedData);
|
||||
};
|
||||
|
||||
// check if there are secrets with an override
|
||||
const checkOverrides = (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;
|
||||
};
|
||||
// This function downloads the secrets as a .env file
|
||||
const downloadDotEnv = () => {
|
||||
if (!data) return;
|
||||
const secrets = 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();
|
||||
};
|
||||
|
||||
// This function downloads the secrets as a .yml file
|
||||
const downloadYaml = () => {
|
||||
if (!data) return;
|
||||
const doc = new Document(new YAMLSeq());
|
||||
const secrets = checkOverrides(data);
|
||||
secrets.forEach((secret) => {
|
||||
const pair = doc.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();
|
||||
};
|
||||
|
||||
|
||||
const deleteCertainRow = ({ ids, secretName }: { ids: string[]; secretName: string; }) => {
|
||||
deleteRow({ids, secretName});
|
||||
};
|
||||
@@ -596,31 +539,29 @@ 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
|
||||
.filter(row => reverseEnvMapping[row.environment] == env)
|
||||
.map((sv, position) => {
|
||||
return {
|
||||
id: sv.id, pos: position, type: sv.type, key: sv.key, value: sv.value, comment: ''
|
||||
}
|
||||
})
|
||||
);
|
||||
|
||||
// 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]
|
||||
);
|
||||
const rolledBackSecrets = snapshotData.secretVersions
|
||||
.filter(row => reverseEnvMapping[row.environment] == env)
|
||||
.map((sv, position) => {
|
||||
return {
|
||||
id: sv.id, pos: position, type: sv.type, key: sv.key, value: sv.value, comment: ''
|
||||
}
|
||||
});
|
||||
setData(rolledBackSecrets);
|
||||
|
||||
setSharedToHide(
|
||||
rolledBackSecrets?.filter(row => (rolledBackSecrets
|
||||
?.map((item) => item.key)
|
||||
.filter(
|
||||
(item, index) =>
|
||||
index !==
|
||||
rolledBackSecrets?.map((item) => item.key).indexOf(item)
|
||||
).includes(row.key) && row.type == 'shared'))?.map((item) => item.id)
|
||||
)
|
||||
|
||||
// Perform the rollback globally
|
||||
performSecretRollback({ workspaceId, version: snapshotData.version })
|
||||
|
||||
setSnapshotData(undefined);
|
||||
createNotification({
|
||||
text: `Rollback has been performed successfully.`,
|
||||
@@ -675,50 +616,7 @@ export default function Dashboard() {
|
||||
/>
|
||||
</div>}
|
||||
{!snapshotData && <div className="ml-2 min-w-max flex flex-row items-start justify-start">
|
||||
<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-[20rem] 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}
|
||||
size="md"
|
||||
text="Download as .env"
|
||||
/>
|
||||
</Menu.Item>
|
||||
<Menu.Item>
|
||||
<Button
|
||||
color="mineshaft"
|
||||
onButtonPressed={downloadYaml}
|
||||
size="md"
|
||||
text="Download as .yml"
|
||||
/>
|
||||
</Menu.Item>
|
||||
</Menu.Items>
|
||||
</Transition>
|
||||
</Menu>
|
||||
<DownloadSecretMenu data={data} env={env} />
|
||||
</div>}
|
||||
<div className="ml-2 min-w-max flex flex-row items-start justify-start">
|
||||
<Button
|
||||
@@ -763,7 +661,7 @@ export default function Dashboard() {
|
||||
className={`max-w-5xl mt-1 max-h-[calc(100vh-280px)] overflow-hidden overflow-y-scroll no-scrollbar no-scrollbar::-webkit-scrollbar`}
|
||||
>
|
||||
<div className="px-1 pt-2 bg-mineshaft-800 rounded-md p-2">
|
||||
{!snapshotData && data?.filter(row => row.key.toUpperCase().includes(searchKeys.toUpperCase()))
|
||||
{!snapshotData && data?.filter(row => row.key?.toUpperCase().includes(searchKeys.toUpperCase()))
|
||||
.filter(row => !(sharedToHide.includes(row.id) && row.type == 'shared')).map((keyPair) => (
|
||||
<KeyPair
|
||||
key={keyPair.id}
|
||||
@@ -831,7 +729,6 @@ export default function Dashboard() {
|
||||
/>
|
||||
)}
|
||||
{
|
||||
// fileState.message == 'Access needed to pull the latest file' ||
|
||||
(!isKeyAvailable && (
|
||||
<>
|
||||
<FontAwesomeIcon
|
||||
|
||||
Reference in New Issue
Block a user