Added commenting functionality (#135)

This commit is contained in:
Vladyslav Matsiiako
2022-12-26 21:28:55 -05:00
parent ab3f3600e5
commit f3e3a9edf1
10 changed files with 172 additions and 62 deletions
@@ -21,6 +21,10 @@ interface PushSecret {
ivValue: string; ivValue: string;
tagValue: string; tagValue: string;
hashValue: string; hashValue: string;
ciphertextComment: string;
ivComment: string;
tagComment: string;
hashComment: string;
type: 'shared' | 'personal'; type: 'shared' | 'personal';
} }
+28 -4
View File
@@ -25,6 +25,10 @@ interface PushSecret {
ivValue: string; ivValue: string;
tagValue: string; tagValue: string;
hashValue: string; hashValue: string;
ciphertextComment: string;
ivComment: string;
tagComment: string;
hashComment: string;
type: 'shared' | 'personal'; type: 'shared' | 'personal';
} }
@@ -93,7 +97,8 @@ const pushSecrets = async ({
const toUpdate = oldSecrets const toUpdate = oldSecrets
.filter((s) => { .filter((s) => {
if (`${s.type}-${s.secretKeyHash}` in newSecretsObj) { if (`${s.type}-${s.secretKeyHash}` in newSecretsObj) {
if (s.secretValueHash !== newSecretsObj[`${s.type}-${s.secretKeyHash}`].hashValue) { if (s.secretValueHash !== newSecretsObj[`${s.type}-${s.secretKeyHash}`].hashValue
|| s.secretCommentHash !== newSecretsObj[`${s.type}-${s.secretKeyHash}`].hashComment) {
// case: filter secrets where value changed // case: filter secrets where value changed
return true; return true;
} }
@@ -113,14 +118,22 @@ const pushSecrets = async ({
ciphertextValue, ciphertextValue,
ivValue, ivValue,
tagValue, tagValue,
hashValue hashValue,
ciphertextComment,
ivComment,
tagComment,
hashComment
} = newSecretsObj[`${s.type}-${s.secretKeyHash}`]; } = newSecretsObj[`${s.type}-${s.secretKeyHash}`];
const update: Update = { const update: Update = {
secretValueCiphertext: ciphertextValue, secretValueCiphertext: ciphertextValue,
secretValueIV: ivValue, secretValueIV: ivValue,
secretValueTag: tagValue, secretValueTag: tagValue,
secretValueHash: hashValue secretValueHash: hashValue,
secretCommentCiphertext: ciphertextComment,
secretCommentIV: ivComment,
secretCommentTag: tagComment,
secretCommentHash: hashComment,
} }
if (!s.version) { if (!s.version) {
@@ -192,7 +205,11 @@ const pushSecrets = async ({
secretValueCiphertext: s.ciphertextValue, secretValueCiphertext: s.ciphertextValue,
secretValueIV: s.ivValue, secretValueIV: s.ivValue,
secretValueTag: s.tagValue, secretValueTag: s.tagValue,
secretValueHash: s.hashValue secretValueHash: s.hashValue,
secretCommentCiphertext: s.ciphertextComment,
secretCommentIV: s.ivComment,
secretCommentTag: s.tagComment,
secretCommentHash: s.hashComment
}; };
if (toAdd[idx].type === 'personal') { if (toAdd[idx].type === 'personal') {
@@ -315,6 +332,13 @@ const reformatPullSecrets = ({ secrets }: { secrets: ISecret[] }) => {
iv: s.secretValueIV, iv: s.secretValueIV,
tag: s.secretValueTag, tag: s.secretValueTag,
hash: s.secretValueHash hash: s.secretValueHash
},
secretComment: {
workspace: s.workspace,
ciphertext: s.secretCommentCiphertext,
iv: s.secretCommentIV,
tag: s.secretCommentTag,
hash: s.secretCommentHash
} }
})); }));
} catch (err) { } catch (err) {
+20
View File
@@ -23,6 +23,10 @@ export interface ISecret {
secretValueIV: string; secretValueIV: string;
secretValueTag: string; secretValueTag: string;
secretValueHash: string; secretValueHash: string;
secretCommentCiphertext?: string;
secretCommentIV?: string;
secretCommentTag?: string;
secretCommentHash?: string;
} }
const secretSchema = new Schema<ISecret>( const secretSchema = new Schema<ISecret>(
@@ -82,6 +86,22 @@ const secretSchema = new Schema<ISecret>(
secretValueHash: { secretValueHash: {
type: String, type: String,
required: true required: true
},
secretCommentCiphertext: {
type: String,
required: false
},
secretCommentIV: {
type: String, // symmetric
required: false
},
secretCommentTag: {
type: String, // symmetric
required: false
},
secretCommentHash: {
type: String,
required: false
} }
}, },
{ {
+4 -1
View File
@@ -7,6 +7,7 @@ interface OverrideProps {
keyName: string; keyName: string;
value: string; value: string;
pos: number; pos: number;
comment: string;
} }
interface ToggleProps { interface ToggleProps {
@@ -17,6 +18,7 @@ interface ToggleProps {
value: string; value: string;
pos: number; pos: number;
id: string; id: string;
comment: string;
deleteOverride: (id: string) => void; deleteOverride: (id: string) => void;
sharedToHide: string[]; sharedToHide: string[];
setSharedToHide: (values: string[]) => void; setSharedToHide: (values: string[]) => void;
@@ -46,6 +48,7 @@ export default function Toggle ({
value, value,
pos, pos,
id, id,
comment,
deleteOverride, deleteOverride,
sharedToHide, sharedToHide,
setSharedToHide setSharedToHide
@@ -55,7 +58,7 @@ export default function Toggle ({
checked={enabled} checked={enabled}
onChange={() => { onChange={() => {
if (enabled == false) { if (enabled == false) {
addOverride({ id, keyName, value, pos }); addOverride({ id, keyName, value, pos, comment });
setSharedToHide([ setSharedToHide([
...sharedToHide!, ...sharedToHide!,
id id
@@ -0,0 +1,16 @@
/**
* This is the text field where people can add comments to particular secrets.
*/
const CommentField = ({ comment, modifyComment, position }: { comment: string; modifyComment: (value: string, posistion: number) => void; position: number;}) => {
return <div className={`relative mt-4 px-4 pt-4`}>
<p className='text-sm text-bunker-300'>Comments & notes</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"
value={comment}
onChange={(e) => modifyComment(e.target.value, position)}
placeholder="Leave any comments here..."
/>
</div>
}
export default CommentField;
+3 -3
View File
@@ -126,7 +126,7 @@ const DropZone = ({
</div> </div>
) : keysExist ? ( ) : keysExist ? (
<div <div
className="opacity-60 hover:opacity-100 duration-200 relative bg-bunker outline max-w-[calc(100%-1rem)] w-full outline-dashed outline-gray-600 rounded-md outline-2 flex flex-col items-center justify-center mb-16 mx-auto mt-1 py-8 px-2" className="opacity-60 hover:opacity-100 duration-200 relative bg-mineshaft-900 outline max-w-[calc(100%-1rem)] w-full outline-dashed outline-chicago-600 rounded-md outline-2 flex flex-col items-center justify-center mb-16 mx-auto mt-1 py-8 px-2"
onDragEnter={handleDragEnter} onDragEnter={handleDragEnter}
onDragOver={handleDragOver} onDragOver={handleDragOver}
onDragLeave={handleDragLeave} onDragLeave={handleDragLeave}
@@ -147,9 +147,9 @@ const DropZone = ({
<div className="flex flex-row"> <div className="flex flex-row">
<FontAwesomeIcon <FontAwesomeIcon
icon={faUpload} icon={faUpload}
className="text-gray-300 text-3xl mr-6" className="text-bunker-300 text-3xl mr-6"
/> />
<p className="text-gray-300 mt-1"> <p className="text-bunker-300 mt-1">
Drag and drop your .env file here to add more keys. Drag and drop your .env file here to add more keys.
</p> </p>
</div> </div>
+7 -11
View File
@@ -5,6 +5,7 @@ import SecretVersionList from 'ee/components/SecretVersionList';
import Button from '../basic/buttons/Button'; import Button from '../basic/buttons/Button';
import Toggle from '../basic/Toggle'; import Toggle from '../basic/Toggle';
import CommentField from './CommentField';
import DashboardInputField from './DashboardInputField'; import DashboardInputField from './DashboardInputField';
import GenerateSecretMenu from './GenerateSecretMenu'; import GenerateSecretMenu from './GenerateSecretMenu';
@@ -15,6 +16,7 @@ interface SecretProps {
pos: number; pos: number;
type: string; type: string;
id: string; id: string;
comment: string;
} }
interface OverrideProps { interface OverrideProps {
@@ -22,6 +24,7 @@ interface OverrideProps {
keyName: string; keyName: string;
value: string; value: string;
pos: number; pos: number;
comment: string;
} }
interface SideBarProps { interface SideBarProps {
@@ -29,6 +32,7 @@ interface SideBarProps {
data: SecretProps[]; data: SecretProps[];
modifyKey: (value: string, position: number) => void; modifyKey: (value: string, position: number) => void;
modifyValue: (value: string, position: number) => void; modifyValue: (value: string, position: number) => void;
modifyComment: (value: string, position: number) => void;
addOverride: (value: OverrideProps) => void; addOverride: (value: OverrideProps) => void;
deleteOverride: (id: string) => void; deleteOverride: (id: string) => void;
buttonReady: boolean; buttonReady: boolean;
@@ -56,6 +60,7 @@ const SideBar = ({
data, data,
modifyKey, modifyKey,
modifyValue, modifyValue,
modifyComment,
addOverride, addOverride,
deleteOverride, deleteOverride,
buttonReady, buttonReady,
@@ -115,6 +120,7 @@ const SideBar = ({
value={data[0].value} value={data[0].value}
pos={data[0].pos} pos={data[0].pos}
id={data[0].id} id={data[0].id}
comment={data[0].comment}
deleteOverride={deleteOverride} deleteOverride={deleteOverride}
sharedToHide={sharedToHide} sharedToHide={sharedToHide}
setSharedToHide={setSharedToHide} setSharedToHide={setSharedToHide}
@@ -143,17 +149,7 @@ const SideBar = ({
isFull={true} isFull={true}
/> />
</div> */} </div> */}
<div className={`relative mt-4 px-4 pt-4`}> <CommentField comment={data.filter(secret => secret.type == "shared")[0]?.comment} modifyComment={modifyComment} position={data[0].pos} />
<div className='flex flex-row justify-between'>
<p className='text-sm text-bunker-300'>Comments & notes</p>
<div className="bg-yellow rounded-md h-min">
<p className="relative text-black text-xs px-1.5 h-min">Coming soon!</p>
</div>
</div>
<div className='h-32 opacity-50 w-full bg-bunker-800 p-2 rounded-md border border-mineshaft-500 rounded-md text-sm text-bunker-300'>
Leave your comment here...
</div>
</div>
</div> </div>
<div className={`flex justify-start max-w-sm mt-4 px-4 mt-full mb-[4.7rem]`}> <div className={`flex justify-start max-w-sm mt-4 px-4 mt-full mb-[4.7rem]`}>
<Button <Button
@@ -10,6 +10,13 @@ const {
const nacl = require('tweetnacl'); const nacl = require('tweetnacl');
nacl.util = require('tweetnacl-util'); nacl.util = require('tweetnacl-util');
interface SecretProps {
key: string;
value: string;
type: 'personal' | 'shared';
comment: string;
}
interface Props { interface Props {
env: keyof typeof envMapping; env: keyof typeof envMapping;
setFileState: any; setFileState: any;
@@ -34,12 +41,12 @@ const getSecretsForProject = async ({
} catch (error) { } catch (error) {
console.log('ERROR: Not able to access the latest file'); console.log('ERROR: Not able to access the latest file');
} }
// This is called isKeyAvilable but what it really means is if a person is able to create new key pairs // 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(!file.key ? file.secrets.length == 0 : true);
const PRIVATE_KEY = localStorage.getItem('PRIVATE_KEY'); const PRIVATE_KEY = localStorage.getItem('PRIVATE_KEY');
const tempFileState: { key: string; value: string; type: 'personal' | 'shared'; }[] = []; const tempFileState: SecretProps[] = [];
if (file.key) { if (file.key) {
// assymmetrically decrypt symmetric key with local private key // assymmetrically decrypt symmetric key with local private key
const key = decryptAssymmetric({ const key = decryptAssymmetric({
@@ -64,10 +71,24 @@ const getSecretsForProject = async ({
tag: secretPair.secretValue.tag, tag: secretPair.secretValue.tag,
key key
}); });
let plainTextComment;
if (secretPair.secretComment.ciphertext) {
plainTextComment = decryptSymmetric({
ciphertext: secretPair.secretComment.ciphertext,
iv: secretPair.secretComment.iv,
tag: secretPair.secretComment.tag,
key
});
} else {
plainTextComment = "";
}
tempFileState.push({ tempFileState.push({
key: plainTextKey, key: plainTextKey,
value: plainTextValue, value: plainTextValue,
type: secretPair.type type: secretPair.type,
comment: plainTextComment
}); });
}); });
} }
@@ -80,7 +101,8 @@ const getSecretsForProject = async ({
pos: index, pos: index,
key: line['key'], key: line['key'],
value: line['value'], value: line['value'],
type: line['type'] type: line['type'],
comment: line['comment']
}; };
}) })
); );
@@ -91,7 +113,8 @@ const getSecretsForProject = async ({
pos: index, pos: index,
key: line['key'], key: line['key'],
value: line['value'], value: line['value'],
type: line['type'] type: line['type'],
comment: line['comment']
}; };
}); });
} catch (error) { } catch (error) {
@@ -65,6 +65,16 @@ const pushKeys = async({ obj, workspaceId, env }: { obj: object; workspaceId: st
key: randomBytes, key: randomBytes,
}); });
// encrypt comment
const {
ciphertext: ciphertextComment,
iv: ivComment,
tag: tagComment,
} = encryptSymmetric({
plaintext: obj[key as keyof typeof obj][1],
key: randomBytes,
});
const visibility = key.charAt(0) == "p" ? "personal" : "shared"; const visibility = key.charAt(0) == "p" ? "personal" : "shared";
return { return {
@@ -76,6 +86,10 @@ const pushKeys = async({ obj, workspaceId, env }: { obj: object; workspaceId: st
ivValue, ivValue,
tagValue, tagValue,
hashValue: crypto.createHash("sha256").update(obj[key as keyof typeof obj][0]).digest("hex"), hashValue: crypto.createHash("sha256").update(obj[key as keyof typeof obj][0]).digest("hex"),
ciphertextComment,
ivComment,
tagComment,
hashComment: crypto.createHash("sha256").update(obj[key as keyof typeof obj][1]).digest("hex"),
type: visibility, type: visibility,
}; };
}); });
+48 -38
View File
@@ -41,6 +41,7 @@ interface SecretDataProps {
key: string; key: string;
value: string; value: string;
id: string; id: string;
comment: string;
} }
/** /**
@@ -199,7 +200,8 @@ export default function Dashboard() {
pos: data!.length, pos: data!.length,
key: '', key: '',
value: '', value: '',
type: 'shared' type: 'shared',
comment: '',
} }
]); ]);
}; };
@@ -209,6 +211,7 @@ export default function Dashboard() {
keyName: string; keyName: string;
value: string; value: string;
pos: number; pos: number;
comment: string;
} }
/** /**
@@ -219,7 +222,7 @@ export default function Dashboard() {
* @param {string} obj.value - value of this secret * @param {string} obj.value - value of this secret
* @param {string} obj.pos - position of this secret on the dashboard * @param {string} obj.pos - position of this secret on the dashboard
*/ */
const addOverride = ({ id, keyName, value, pos }: overrideProps) => { const addOverride = ({ id, keyName, value, pos, comment }: overrideProps) => {
setIsNew(false); setIsNew(false);
const tempdata: SecretDataProps[] | 1 = [ const tempdata: SecretDataProps[] | 1 = [
...data!, ...data!,
@@ -228,7 +231,8 @@ export default function Dashboard() {
pos: pos, pos: pos,
key: keyName, key: keyName,
value: value, value: value,
type: 'personal' type: 'personal',
comment: comment
} }
]; ];
sortValuesHandler(tempdata, sortMethod == "alhpabetical" ? "-alphabetical" : "alphabetical"); sortValuesHandler(tempdata, sortMethod == "alhpabetical" ? "-alphabetical" : "alphabetical");
@@ -273,6 +277,14 @@ export default function Dashboard() {
setButtonReady(true); setButtonReady(true);
}; };
const modifyComment = (value: string, pos: number) => {
setData((oldData) => {
oldData![pos].comment = value;
return [...oldData!];
});
setButtonReady(true);
};
// For speed purposes and better perforamance, we are using useCallback // For speed purposes and better perforamance, we are using useCallback
const listenChangeValue = useCallback((value: string, pos: number) => { const listenChangeValue = useCallback((value: string, pos: number) => {
modifyValue(value, pos); modifyValue(value, pos);
@@ -286,6 +298,10 @@ export default function Dashboard() {
modifyVisibility(value, pos); modifyVisibility(value, pos);
}, []); }, []);
const listenChangeComment = useCallback((value: string, pos: number) => {
modifyComment(value, pos);
}, []);
/** /**
* Save the changes of environment variables and push them to the database * Save the changes of environment variables and push them to the database
*/ */
@@ -293,7 +309,7 @@ export default function Dashboard() {
// Format the new object with environment variables // Format the new object with environment variables
const obj = Object.assign( const obj = Object.assign(
{}, {},
...data!.map((row: SecretDataProps) => ({ [row.type.charAt(0) + row.key]: [row.value, row.type] })) ...data!.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 // Checking if any of the secret keys start with a number - if so, don't do anything
@@ -316,8 +332,6 @@ export default function Dashboard() {
}); });
} }
console.log('pushing', obj)
// Once "Save changed is clicked", disable that button // Once "Save changed is clicked", disable that button
setButtonReady(false); setButtonReady(false);
pushKeys({ obj, workspaceId: String(router.query.id), env }); pushKeys({ obj, workspaceId: String(router.query.id), env });
@@ -352,7 +366,6 @@ export default function Dashboard() {
pos: index pos: index
}; };
}); });
console.log('override', sortedData)
setData(sortedData); setData(sortedData);
}; };
@@ -409,6 +422,7 @@ export default function Dashboard() {
data={data.filter((row: SecretDataProps) => row.id == sidebarSecretId)} data={data.filter((row: SecretDataProps) => row.id == sidebarSecretId)}
modifyKey={listenChangeKey} modifyKey={listenChangeKey}
modifyValue={listenChangeValue} modifyValue={listenChangeValue}
modifyComment={listenChangeComment}
addOverride={addOverride} addOverride={addOverride}
deleteOverride={deleteOverride} deleteOverride={deleteOverride}
buttonReady={buttonReady} buttonReady={buttonReady}
@@ -436,7 +450,6 @@ export default function Dashboard() {
<ListBox <ListBox
selected={env} selected={env}
data={['Development', 'Staging', 'Production', 'Testing']} data={['Development', 'Staging', 'Production', 'Testing']}
// ref={useRef(123)}
onChange={setEnv} onChange={setEnv}
/> />
)} )}
@@ -490,7 +503,6 @@ export default function Dashboard() {
<ListBox <ListBox
selected={env} selected={env}
data={['Development', 'Staging', 'Production', 'Testing']} data={['Development', 'Staging', 'Production', 'Testing']}
// ref={useRef(123)}
onChange={setEnv} onChange={setEnv}
/> />
<div className="h-10 w-full bg-white/5 hover:bg-white/10 ml-2 flex items-center rounded-md flex flex-row items-center"> <div className="h-10 w-full bg-white/5 hover:bg-white/10 ml-2 flex items-center rounded-md flex flex-row items-center">
@@ -554,36 +566,34 @@ export default function Dashboard() {
</div> </div>
{data?.length !== 0 ? ( {data?.length !== 0 ? (
<div className="flex flex-col w-full mt-1 mb-2"> <div className="flex flex-col w-full mt-1 mb-2">
<div className='bg-mineshaft-800 rounded-md px-2 py-2 max-w-5xl'> <div
<div className={`max-w-5xl mt-1 max-h-[calc(100vh-280px)] overflow-hidden overflow-y-scroll no-scrollbar no-scrollbar::-webkit-scrollbar`}
className={`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">
<div className="px-1 pt-2"> {data?.filter(row => !(sharedToHide.includes(row.id) && row.type == 'shared')).map((keyPair) => (
{data?.filter(row => !(sharedToHide.includes(row.id) && row.type == 'shared')).map((keyPair) => ( <KeyPair
<KeyPair key={keyPair.id}
key={keyPair.id} keyPair={keyPair}
keyPair={keyPair} deleteRow={deleteCertainRow}
deleteRow={deleteCertainRow} modifyValue={listenChangeValue}
modifyValue={listenChangeValue} modifyKey={listenChangeKey}
modifyKey={listenChangeKey} isBlurred={blurred}
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}
toggleSidebar={toggleSidebar} sidebarSecretId={sidebarSecretId}
sidebarSecretId={sidebarSecretId}
/>
))}
</div>
<div className="w-full max-w-5xl px-2 pt-2">
<DropZone
setData={addData}
setErrorDragAndDrop={setErrorDragAndDrop}
createNewFile={addRow}
errorDragAndDrop={errorDragAndDrop}
setButtonReady={setButtonReady}
keysExist={true}
numCurrentRows={data.length}
/> />
</div> ))}
</div>
<div className="w-full max-w-5xl px-2 pt-3">
<DropZone
setData={addData}
setErrorDragAndDrop={setErrorDragAndDrop}
createNewFile={addRow}
errorDragAndDrop={errorDragAndDrop}
setButtonReady={setButtonReady}
keysExist={true}
numCurrentRows={data.length}
/>
</div> </div>
</div> </div>
</div> </div>