diff --git a/backend/src/services/external-migration/external-migration-fns.ts b/backend/src/services/external-migration/external-migration-fns.ts index 4da9278e3..1e5b2cde1 100644 --- a/backend/src/services/external-migration/external-migration-fns.ts +++ b/backend/src/services/external-migration/external-migration-fns.ts @@ -13,7 +13,7 @@ export const decryptEnvKeyData = async (decryptionKey: string, encryptedJson: { const decrypted = secretbox.open(encryptedData, nonce, key); if (!decrypted) { - throw new Error("Decryption failed"); + throw new Error("Decryption failed, please check the entered encryption key"); } const decryptedJson = encodeUTF8(decrypted); diff --git a/frontend/src/hooks/api/migration/mutations.tsx b/frontend/src/hooks/api/migration/mutations.tsx new file mode 100644 index 000000000..83408bba3 --- /dev/null +++ b/frontend/src/hooks/api/migration/mutations.tsx @@ -0,0 +1,31 @@ +import { useMutation } from "@tanstack/react-query"; +import { AxiosError } from "axios"; + +import { apiRequest } from "@app/config/request"; + +export const useImportEnvKey = () => { + return useMutation({ + mutationFn: async ({ encryptedJson, decryptionKey }: { encryptedJson: { + nonce: string, + data: string + }, decryptionKey: string }) : Promise<{ success: boolean, message:string }>=> { + try{ + const { data } = await apiRequest.post<{ + success: boolean, + message: string + }>("/api/v3/migrate/envkey/", { + encryptedJson, + decryptionKey + }); + return data; + } catch (err) { + if ((err as AxiosError<{ + message: string + }>).response) { + return { success: false, message: (err as AxiosError<{message: string}>).response?.data?.message as string}; + } + } + return { success: false, message: "Something went wrong" }; + } + }); +}; \ No newline at end of file diff --git a/frontend/src/views/Settings/OrgSettingsPage/components/ImportTab/ImportTab.tsx b/frontend/src/views/Settings/OrgSettingsPage/components/ImportTab/ImportTab.tsx new file mode 100644 index 000000000..86af16d44 --- /dev/null +++ b/frontend/src/views/Settings/OrgSettingsPage/components/ImportTab/ImportTab.tsx @@ -0,0 +1,199 @@ +import { useEffect, useRef } from "react"; +import { Controller, useForm } from "react-hook-form"; +import { faUpload } from "@fortawesome/free-solid-svg-icons"; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; +import { zodResolver } from "@hookform/resolvers/zod"; +import { z } from "zod"; + +import { createNotification } from "@app/components/notifications"; +import { Button, FormControl, IconButton } from "@app/components/v2"; +import { useImportEnvKey } from "@app/hooks/api/migration/mutations"; + +const formSchema = z.object({ + decryptionKey: z.string().min(1), + file: z.unknown(), + encryptedJson: z.object({ + nonce: z.string().min(1), + data: z.string().min(1) + }) +}); + +type TForm = z.infer; + +export const ImportTab = () => { + const fileUploadRef = useRef(null); + +const { mutateAsync: importEnvKey +} = useImportEnvKey(); + + const { + handleSubmit, + control, + watch, + setError, + setValue, + reset, + trigger, + formState: { isSubmitting, isValid } + } = useForm({ + resolver: zodResolver(formSchema), + values: { + decryptionKey: "", + encryptedJson: { + nonce: "", + data: "" + }, + file: undefined + } + }); + + const parseJson = (src: ArrayBuffer) => { + console.log("here") + const file = src.toString(); + const formatedData: Record = JSON.parse(file); + if (Object.keys(formatedData).includes("nonce") && Object.keys(formatedData).includes("data")) { + const data = { + nonce: formatedData.nonce, + data: formatedData.data + }; + setValue("encryptedJson", data); + trigger("encryptedJson"); + console.log(data); + } else { + setValue("encryptedJson", { + nonce: "", + data: "" + }); + if (fileUploadRef.current) { + fileUploadRef.current.value = ""; + } + createNotification({ + text: "Improper file format, please upload the EnvKey export.", + type: "error" + }); + } + }; + + const parseFile = (file?: File) => { + const reader = new FileReader(); + if (!file) { + createNotification({ + text: "No file selected.", + type: "error" + }); + return; + } + reader.onload = (event) => { + if (!event?.target?.result) return; + // parse function's argument looks like to be ArrayBuffer + parseJson(event.target.result as ArrayBuffer); + } + reader.readAsText(file); + } + + const submitExport = async (data: TForm) => { + if (!data.encryptedJson) { + setError("encryptedJson", { + type: "required", + message: "File is required" + }); + return; + } + + const res = await importEnvKey({ encryptedJson: data.encryptedJson, decryptionKey: data.decryptionKey }); + if (res.success) { + createNotification({ + text: "Data imported successfully.", + type: "success" + }); + reset(); + if (fileUploadRef.current) { + fileUploadRef.current.value = ""; + } + } else { + createNotification({ + text: res.message, + type: "error" + }); + } + } + + const watchEncryptedJsonFile: any = watch("file"); + useEffect(() => { + if (watchEncryptedJsonFile) { + parseFile(watchEncryptedJsonFile?.[0]); + } + }, [watchEncryptedJsonFile]); + + return ( +
+

Import from external source

+

+ Import data from another secret manager to Infisical. +

+
+

Import from EnvKey

+
+
+ ( + + + + )} + name="decryptionKey" + control={control} + /> +
+ ( + + <> + field.onChange(e.target.files)} + ref={fileUploadRef} + /> + { + fileUploadRef?.current?.click(); + }} + > + + + + + )} /> + +
+
+ +
+ +
+
+ ); +}; diff --git a/frontend/src/views/Settings/OrgSettingsPage/components/ImportTab/index.tsx b/frontend/src/views/Settings/OrgSettingsPage/components/ImportTab/index.tsx new file mode 100644 index 000000000..0679ab33f --- /dev/null +++ b/frontend/src/views/Settings/OrgSettingsPage/components/ImportTab/index.tsx @@ -0,0 +1 @@ +export { ImportTab } from "./ImportTab"; \ No newline at end of file diff --git a/frontend/src/views/Settings/OrgSettingsPage/components/OrgTabGroup/OrgTabGroup.tsx b/frontend/src/views/Settings/OrgSettingsPage/components/OrgTabGroup/OrgTabGroup.tsx index 090fd63e0..875f8cdf4 100644 --- a/frontend/src/views/Settings/OrgSettingsPage/components/OrgTabGroup/OrgTabGroup.tsx +++ b/frontend/src/views/Settings/OrgSettingsPage/components/OrgTabGroup/OrgTabGroup.tsx @@ -2,7 +2,11 @@ import { Fragment, useEffect, useState } from "react"; import { useRouter } from "next/router"; import { Tab } from "@headlessui/react"; +import { OrgPermissionCan } from "@app/components/permissions"; +import { OrgPermissionActions, OrgPermissionSubjects } from "@app/context"; + import { AuditLogStreamsTab } from "../AuditLogStreamTab"; +import { ImportTab } from "../ImportTab"; import { OrgAuthTab } from "../OrgAuthTab"; import { OrgEncryptionTab } from "../OrgEncryptionTab"; import { OrgGeneralTab } from "../OrgGeneralTab"; @@ -13,7 +17,8 @@ const tabs = [ { name: "Security", key: "tab-org-security" }, { name: "Encryption", key: "tab-org-encryption" }, { name: "Workflow Integrations", key: "workflow-integrations" }, - { name: "Audit Log Streams", key: "tag-audit-log-streams" } + { name: "Audit Log Streams", key: "tag-audit-log-streams" }, + { name: "Import", key: "tab-import" } ]; export const OrgTabGroup = () => { const { query } = useRouter(); @@ -63,6 +68,11 @@ export const OrgTabGroup = () => { + + + + + );