From 5ef4e4cecbf0b383b7e67b430a64c03432780700 Mon Sep 17 00:00:00 2001 From: Mohammed Date: Fri, 30 Dec 2022 01:03:07 +0100 Subject: [PATCH 1/8] add ability to import secrets with comments --- frontend/components/dashboard/DropZone.tsx | 85 +++++++++++++------- frontend/components/utilities/file.ts | 47 ----------- frontend/components/utilities/parseDotEnv.ts | 66 +++++++++++++++ 3 files changed, 123 insertions(+), 75 deletions(-) delete mode 100644 frontend/components/utilities/file.ts create mode 100644 frontend/components/utilities/parseDotEnv.ts diff --git a/frontend/components/dashboard/DropZone.tsx b/frontend/components/dashboard/DropZone.tsx index 66be964cc..38e293f4e 100644 --- a/frontend/components/dashboard/DropZone.tsx +++ b/frontend/components/dashboard/DropZone.tsx @@ -3,10 +3,11 @@ import Image from "next/image"; import { useTranslation } from "next-i18next"; import { faUpload } from "@fortawesome/free-solid-svg-icons"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; +import { parseDocument, Scalar, YAMLMap } from 'yaml'; import Button from "../basic/buttons/Button"; import Error from "../basic/Error"; -import parse from "../utilities/file"; +import { parseDotEnv } from '../utilities/parseDotEnv'; import guidGenerator from "../utilities/randomId"; interface DropZoneProps { @@ -51,6 +52,53 @@ const DropZone = ({ const [loading, setLoading] = useState(false); + const getSecrets = (file: ArrayBuffer, fileType: string) => { + let secrets; + switch (fileType) { + case 'env': { + const keyPairs = parseDotEnv(file); + secrets = Object.keys(keyPairs).map((key, index) => { + return { + id: guidGenerator(), + pos: numCurrentRows + index, + key: key, + value: keyPairs[key as keyof typeof keyPairs].value, + comment: keyPairs[key as keyof typeof keyPairs].comments.join('\n'), + type: 'shared', + }; + }); + break; + } + case 'yml': { + const parsedFile = parseDocument(file.toString()); + const keyPairs = parsedFile.contents!.toJSON(); + + secrets = Object.keys(keyPairs).map((key, index) => { + const fileContent = parsedFile.contents as YAMLMap; + const comment = + fileContent!.items + .find((item) => item.key.value === key) + ?.key?.commentBefore?.split('\n') + .map((comment) => comment.trim()) + .join('\n') ?? ''; + return { + id: guidGenerator(), + pos: numCurrentRows + index, + key: key, + value: keyPairs[key as keyof typeof keyPairs]?.toString() ?? '', + comment, + type: 'shared', + }; + }); + break; + } + default: + secrets = ''; + break; + } + return secrets; + }; + // This function function immediately parses the file after it is dropped const handleDrop = async (e: DragEvent) => { setLoading(true); @@ -61,20 +109,12 @@ const DropZone = ({ const file = e.dataTransfer.files[0]; const reader = new FileReader(); + const fileType = file.name.split('.')[1]; reader.onload = (event) => { if (event.target === null || event.target.result === null) return; // parse function's argument looks like to be ArrayBuffer - const keyPairs = parse(event.target.result as Buffer); - const newData = Object.keys(keyPairs).map((key, index) => { - return { - id: guidGenerator(), - pos: numCurrentRows + index, - key: key, - value: keyPairs[key as keyof typeof keyPairs], - type: "shared", - }; - }); + const newData = getSecrets(event.target.result as ArrayBuffer, fileType); setData(newData); setButtonReady(true); }; @@ -95,25 +135,14 @@ const DropZone = ({ setTimeout(() => setLoading(false), 5000); if (e.currentTarget.files === null) return; const file = e.currentTarget.files[0]; + const fileType = file.name.split('.')[1]; const reader = new FileReader(); reader.onload = (event) => { if (event.target === null || event.target.result === null) return; const { result } = event.target; - if (typeof result === "string") { - const newData = result - .split("\n") - .map((line: string, index: number) => { - return { - id: guidGenerator(), - pos: numCurrentRows + index, - key: line.split("=")[0], - value: line.split("=").slice(1, line.split("=").length).join("="), - type: "shared", - }; - }); - setData(newData); - setButtonReady(true); - } + const newData = getSecrets(result as ArrayBuffer, fileType); + setData(newData); + setButtonReady(true); }; reader.readAsText(file); }; @@ -139,7 +168,7 @@ const DropZone = ({ id="fileSelect" type="file" className="opacity-0 absolute w-full h-full" - accept=".txt,.env" + accept=".txt,.env,.yml" onChange={handleFileSelect} /> {errorDragAndDrop ? ( @@ -176,7 +205,7 @@ const DropZone = ({ id="fileSelect" type="file" className="opacity-0 absolute w-full h-full" - accept=".txt,.env" + accept=".txt,.env,.yml" onChange={handleFileSelect} />
diff --git a/frontend/components/utilities/file.ts b/frontend/components/utilities/file.ts deleted file mode 100644 index 3784405f9..000000000 --- a/frontend/components/utilities/file.ts +++ /dev/null @@ -1,47 +0,0 @@ -const LINE = - /(?:^|^)\s*(?:export\s+)?([\w.-]+)(?:\s*=\s*?|:\s+?)(\s*'(?:\\'|[^'])*'|\s*"(?:\\"|[^"])*"|\s*`(?:\\`|[^`])*`|[^#\r\n]+)?\s*(?:#.*)?(?:$|$)/gm; - -/** - * Return text that is the buffer parsed - * @param {Buffer} src - source buffer - * @returns {String} text - text of buffer - */ -function parse(src: Buffer) { - const obj: Record = {}; - - // Convert buffer to string - let lines = src.toString(); - - // Convert line breaks to same format - lines = lines.replace(/\r\n?/gm, '\n'); - - let match; - while ((match = LINE.exec(lines)) != null) { - const key = match[1]; - - // Default undefined or null to empty string - let value = match[2] || ''; - - // Remove whitespace - value = value.trim(); - - // Check if double quoted - const maybeQuote = value[0]; - - // Remove surrounding quotes - value = value.replace(/^(['"`])([\s\S]*)\1$/gm, '$2'); - - // Expand newlines if double quoted - if (maybeQuote === '"') { - value = value.replace(/\\n/g, '\n'); - value = value.replace(/\\r/g, '\r'); - } - - // Add to object - obj[key] = value; - } - - return obj; -} - -export default parse; diff --git a/frontend/components/utilities/parseDotEnv.ts b/frontend/components/utilities/parseDotEnv.ts new file mode 100644 index 000000000..56fe3e349 --- /dev/null +++ b/frontend/components/utilities/parseDotEnv.ts @@ -0,0 +1,66 @@ +const LINE = + /(?:^|^)\s*(?:export\s+)?([\w.-]+)(?:\s*=\s*?|:\s+?)(\s*'(?:\\'|[^'])*'|\s*"(?:\\"|[^"])*"|\s*`(?:\\`|[^`])*`|[^#\r\n]+)?\s*(?:#.*)?(?:$|$)/gm; + +/** + * Return text that is the buffer parsed + * @param {ArrayBuffer} src - source buffer + * @returns {String} text - text of buffer + */ +export function parseDotEnv(src: ArrayBuffer) { + const object: { + [key: string]: { value: string; comments: string[] }; + } = {}; + + // Convert buffer to string + let lines = src.toString(); + + // Convert line breaks to same format + lines = lines.replace(/\r\n?/gm, '\n'); + + let comments: string[] = []; + + lines + .split('\n') + .map((line) => { + // collect comments of each env variable + if (line.startsWith('#')) { + comments.push(line.replace('#', '').trim()); + } else if (line) { + let match; + let item: [string, string, string[]] | [] = []; + + while ((match = LINE.exec(line)) !== null) { + const key = match[1]; + + // Default undefined or null to empty string + let value = match[2] || ''; + + // Remove whitespace + value = value.trim(); + + // Check if double quoted + const maybeQuote = value[0]; + + // Remove surrounding quotes + value = value.replace(/^(['"`])([\s\S]*)\1$/gm, '$2'); + + // Expand newlines if double quoted + if (maybeQuote === '"') { + value = value.replace(/\\n/g, '\n'); + value = value.replace(/\\r/g, '\r'); + } + item = [key, value, comments]; + } + comments = []; + return item; + } + return []; + }) + .filter((line) => line.length > 1) + .forEach((line) => { + const [key, value, comments] = line; + object[key as string] = { value, comments }; + }); + + return object; +} From 3715114232cd9ebc461d9761e06c67e3d9437247 Mon Sep 17 00:00:00 2001 From: Mohammed Date: Fri, 30 Dec 2022 01:05:19 +0100 Subject: [PATCH 2/8] add ability to export secrets with comments --- frontend/pages/dashboard/[id].tsx | 132 ++++++++++++++++++++++++++---- 1 file changed, 116 insertions(+), 16 deletions(-) diff --git a/frontend/pages/dashboard/[id].tsx b/frontend/pages/dashboard/[id].tsx index 60fcc7a00..caf1ce82a 100644 --- a/frontend/pages/dashboard/[id].tsx +++ b/frontend/pages/dashboard/[id].tsx @@ -16,6 +16,8 @@ import { faPlus, } from '@fortawesome/free-solid-svg-icons'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; +import { Menu, Transition } from '@headlessui/react'; +import { Document, YAMLSeq } from 'yaml'; import Button from '~/components/basic/buttons/Button'; import ListBox from '~/components/basic/Listbox'; @@ -382,16 +384,75 @@ 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 download = () => { - const file = data! - .map((item: SecretDataProps) => [item.key, item.value].join('=')) + 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] + '.env'; + alink.download = envMapping[env] + '.yml'; alink.click(); }; @@ -541,12 +602,49 @@ export default function Dashboard() { />
-
} {!snapshotData &&
Date: Fri, 6 Jan 2023 17:08:27 -0800 Subject: [PATCH 7/8] Fixed the missing field TS error --- frontend/pages/dashboard/[id].tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/frontend/pages/dashboard/[id].tsx b/frontend/pages/dashboard/[id].tsx index 65362cc54..f46c0cae5 100644 --- a/frontend/pages/dashboard/[id].tsx +++ b/frontend/pages/dashboard/[id].tsx @@ -687,6 +687,7 @@ export default function Dashboard() { color="mineshaft" size="icon-md" icon={faDownload} + onButtonPressed={() => {}} /> Date: Fri, 6 Jan 2023 20:23:26 -0800 Subject: [PATCH 8/8] Trying to fix the telemetry issue in a pr check --- frontend/components/utilities/telemetry/Telemetry.js | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/frontend/components/utilities/telemetry/Telemetry.js b/frontend/components/utilities/telemetry/Telemetry.js index e4f7283f8..8a0f9cc73 100644 --- a/frontend/components/utilities/telemetry/Telemetry.js +++ b/frontend/components/utilities/telemetry/Telemetry.js @@ -29,7 +29,7 @@ class Capturer { } -class Telemetry { +export default class Telemetry { constructor() { if (!Telemetry.instance) { Telemetry.instance = new Capturer(); @@ -40,5 +40,3 @@ class Telemetry { return Telemetry.instance; } } - -module.exports = Telemetry;