diff --git a/frontend/components/basic/dialog/AddApiKeyDialog.js b/frontend/components/basic/dialog/AddApiKeyDialog.js new file mode 100644 index 0000000000..3c9c359385 --- /dev/null +++ b/frontend/components/basic/dialog/AddApiKeyDialog.js @@ -0,0 +1,264 @@ +import { Fragment, useState } from "react"; +import { useTranslation } from "next-i18next"; +import { faCheck, faCopy } from "@fortawesome/free-solid-svg-icons"; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; +import { Dialog, Transition } from "@headlessui/react"; + +import addServiceToken from "~/pages/api/serviceToken/addServiceToken"; +import getLatestFileKey from "~/pages/api/workspace/getLatestFileKey"; + +import { envMapping } from "../../../public/data/frequentConstants"; +import { + decryptAssymmetric, + encryptSymmetric, +} from "../../utilities/cryptography/crypto"; +import Button from "../buttons/Button"; +import InputField from "../InputField"; +import ListBox from "../Listbox"; + +const expiryMapping = { + "1 day": 86400, + "7 days": 604800, + "1 month": 2592000, + "6 months": 15552000, + "12 months": 31104000, +}; + +const crypto = require('crypto'); + +const AddApiKeyDialog = ({ + isOpen, + closeModal, + workspaceId, + workspaceName, + serviceTokens, + setServiceTokens +}) => { + const [serviceToken, setServiceToken] = useState(""); + const [serviceTokenName, setServiceTokenName] = useState(""); + const [serviceTokenEnv, setServiceTokenEnv] = useState("Development"); + const [serviceTokenExpiresIn, setServiceTokenExpiresIn] = useState("1 day"); + const [serviceTokenCopied, setServiceTokenCopied] = useState(false); + const { t } = useTranslation(); + + const generateServiceToken = async () => { + const latestFileKey = await getLatestFileKey({ workspaceId }); + + const key = decryptAssymmetric({ + ciphertext: latestFileKey.latestKey.encryptedKey, + nonce: latestFileKey.latestKey.nonce, + publicKey: latestFileKey.latestKey.sender.publicKey, + privateKey: localStorage.getItem("PRIVATE_KEY"), + }); + + const randomBytes = crypto.randomBytes(16).toString('hex'); + const { + ciphertext, + iv, + tag, + } = encryptSymmetric({ + plaintext: key, + key: randomBytes, + }); + + let newServiceToken = await addServiceToken({ + name: serviceTokenName, + workspaceId, + environment: envMapping[serviceTokenEnv], + expiresIn: expiryMapping[serviceTokenExpiresIn], + encryptedKey: ciphertext, + iv, + tag + }); + + setServiceTokens(serviceTokens.concat([newServiceToken.serviceTokenData])); + setServiceToken(newServiceToken.serviceToken + "." + randomBytes); + }; + + function copyToClipboard() { + // Get the text field + var copyText = document.getElementById("serviceToken"); + + // Select the text field + copyText.select(); + copyText.setSelectionRange(0, 99999); // For mobile devices + + // Copy the text inside the text field + navigator.clipboard.writeText(copyText.value); + + setServiceTokenCopied(true); + setTimeout(() => setServiceTokenCopied(false), 2000); + // Alert the copied text + // alert("Copied the text: " + copyText.value); + } + + const closeAddServiceTokenModal = () => { + closeModal(); + setServiceTokenName(""); + setServiceToken(""); + }; + + return ( +
+ + + +
+ + +
+
+ + {serviceToken == "" ? ( + + + {t("section-token:add-dialog.title", { + target: workspaceName, + })} + +
+
+

+ {t("section-token:add-dialog.description")} +

+
+
+
+ +
+
+ +
+
+ +
+
+
+
+
+
+ ) : ( + + + {t("section-token:add-dialog.copy-service-token")} + +
+
+

+ {t( + "section-token:add-dialog.copy-service-token-description" + )} +

+
+
+
+
+ +
+ {serviceToken} +
+
+ + + {t("common:click-to-copy")} + +
+
+
+
+
+
+ )} +
+
+
+
+
+
+ ); +}; + +export default AddApiKeyDialog; diff --git a/frontend/components/basic/table/ApiKeyTable.tsx b/frontend/components/basic/table/ApiKeyTable.tsx new file mode 100644 index 0000000000..a7549e083c --- /dev/null +++ b/frontend/components/basic/table/ApiKeyTable.tsx @@ -0,0 +1,94 @@ +import { faX } from '@fortawesome/free-solid-svg-icons'; + +import { useNotificationContext } from '~/components/context/Notifications/NotificationProvider'; + +import deleteServiceToken from "../../../pages/api/serviceToken/deleteServiceToken"; +import { reverseEnvMapping } from '../../../public/data/frequentConstants'; +import guidGenerator from '../../utilities/randomId'; +import Button from '../buttons/Button'; + +interface TokenProps { + _id: string; + name: string; + environment: string; + expiresAt: string; +} + +interface ServiceTokensProps { + data: TokenProps[]; + setServiceTokens: (value: TokenProps[]) => void; +} + +/** + * This is the component that we utilize for the api key table + * @param {object} obj + * @param {any[]} obj.data - current state of the api key table + * @param {function} obj.setServiceTokens - updating the state of the api key table + * @returns + */ +const ApiKeyTable = ({ data, setServiceTokens }: ServiceTokensProps) => { + const { createNotification } = useNotificationContext(); + + return ( +
+
+ + + + + + + + + + + {data?.length > 0 ? ( + data?.map((row) => { + return ( + + + + + + + ); + }) + ) : ( + + + + )} + +
API KEY NAMEENVIRONMENTVAILD UNTIL
+ {row.name} + + {reverseEnvMapping[row.environment]} + + {new Date(row.expiresAt).toUTCString()} + +
+
+
+ No API keys yet +
+
+ ); +}; + +export default ApiKeyTable; diff --git a/frontend/pages/settings/personal/[id].js b/frontend/pages/settings/personal/[id].js index a592bf45be..d7441ec544 100644 --- a/frontend/pages/settings/personal/[id].js +++ b/frontend/pages/settings/personal/[id].js @@ -2,18 +2,20 @@ import { useEffect, useState } from "react"; import Head from "next/head"; import { useRouter } from "next/router"; import { useTranslation } from "next-i18next"; -import { faCheck, faX } from "@fortawesome/free-solid-svg-icons"; +import { faCheck, faPlus, faX } from "@fortawesome/free-solid-svg-icons"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import Button from "~/components/basic/buttons/Button"; import InputField from "~/components/basic/InputField"; import ListBox from "~/components/basic/Listbox"; +import ApiKeyTable from "~/components/basic/table/ApiKeyTable.tsx"; import NavHeader from "~/components/navigation/NavHeader"; import changePassword from "~/components/utilities/cryptography/changePassword"; import issueBackupKey from "~/components/utilities/cryptography/issueBackupKey"; import passwordCheck from "~/utilities/checks/PasswordCheck"; import { getTranslatedServerSideProps } from "~/utilities/withTranslateProps"; +import AddApiKeyDialog from "../../../components/basic/dialog/AddApiKeyDialog"; import getUser from "../../api/user/getUser"; export default function PersonalSettings() { @@ -29,6 +31,8 @@ export default function PersonalSettings() { const [passwordChanged, setPasswordChanged] = useState(false); const [backupKeyIssued, setBackupKeyIssued] = useState(false); const [backupKeyError, setBackupKeyError] = useState(false); + const [isAddApiKeyDialogOpen, setIsAddApiKeyDialogOpen] = useState(false) + const [apiKeys, setApiKeys] = useState([]); const { t } = useTranslation(); const router = useRouter(); @@ -45,6 +49,10 @@ export default function PersonalSettings() { setPersonalName(user.firstName + " " + user.lastName); }, []); + const closeAddApiKeyModal = () => { + setIsAddApiKeyDialogOpen(false); + }; + return (
@@ -53,6 +61,13 @@ export default function PersonalSettings() { +
-
-
- {/*
-
-

- Display Name -

- -
-
-
- {buttonReady ? ( - - ) : ( -
- -

- Saved -

-
- )} -
-
-
*/} -
-

{t("settings-personal:change-language")} @@ -132,6 +99,44 @@ export default function PersonalSettings() { />

+
+
+
+

+ {t("settings-personal:api-keys.title")} +

+

+ {t("settings-personal:api-keys.description")} +

+

+ Please, make sure you are on the + + latest version of CLI + . +

+
+
+
+
+ +
@@ -295,7 +300,7 @@ export default function PersonalSettings() {
-
+

diff --git a/frontend/public/locales/en/settings-personal.json b/frontend/public/locales/en/settings-personal.json index 8759c20f07..66a90b6c35 100644 --- a/frontend/public/locales/en/settings-personal.json +++ b/frontend/public/locales/en/settings-personal.json @@ -7,5 +7,10 @@ "text2": "Only the latest issued Emergency Kit remains valid. To get a new Emergency Kit, verify your password.", "download": "Download Emergency Kit" }, - "change-language": "Change Language" + "change-language": "Change Language", + "api-keys": { + "title": "API Keys", + "description": "Manage your personal API Keys to access the Infisical API.", + "add-new": "Add new" + } }