Drag and drop your .env file here.
diff --git a/frontend/src/components/dashboard/GenerateSecretMenu.tsx b/frontend/src/components/dashboard/GenerateSecretMenu.tsx
new file mode 100644
index 000000000..f87b12bec
--- /dev/null
+++ b/frontend/src/components/dashboard/GenerateSecretMenu.tsx
@@ -0,0 +1,106 @@
+import { Fragment, useState } from 'react';
+import { useTranslation } from 'next-i18next';
+import { faShuffle } from '@fortawesome/free-solid-svg-icons';
+import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
+import { Menu, Transition } from '@headlessui/react';
+
+/**
+ * This is the menu that is used to (re)generate secrets (currently we only have ranom hex, in future we will have more options)
+ * @returns the popup-menu for randomly generating secrets
+ */
+const GenerateSecretMenu = ({
+ modifyValue,
+ position
+}: {
+ modifyValue: (value: string, position: number) => void;
+ position: number;
+}) => {
+ const [randomStringLength, setRandomStringLength] = useState(32);
+ const { t } = useTranslation();
+
+ return (
+
+
+
+
+ null}
+ role="button"
+ tabIndex={0}
+ onClick={() => {
+ if (randomStringLength > 32) {
+ setRandomStringLength(32);
+ } else if (randomStringLength < 2) {
+ setRandomStringLength(2);
+ } else {
+ modifyValue(
+ [...Array(randomStringLength)]
+ .map(() => Math.floor(Math.random() * 16).toString(16))
+ .join(''),
+ position
+ );
+ }
+ }}
+ className="relative flex flex-row justify-start items-center cursor-pointer select-none py-2 px-2 rounded-md text-gray-400 hover:bg-white/10 duration-200 hover:text-gray-200 w-full"
+ >
+
+
+
{t('dashboard:sidebar.generate-random-hex')}
+
{t('dashboard:sidebar.digits')}
+
+
+
+
null}
+ role="button"
+ tabIndex={0}
+ className="m-0.5 px-1 cursor-pointer rounded-md hover:bg-chicago-700"
+ onClick={() => {
+ if (randomStringLength > 1) {
+ setRandomStringLength(randomStringLength - 1);
+ }
+ }}
+ >
+ -
+
+
setRandomStringLength(parseInt(e.target.value, 10))}
+ value={randomStringLength}
+ className="text-center z-20 peer text-sm bg-transparent w-full outline-none"
+ spellCheck="false"
+ />
+
null}
+ role="button"
+ tabIndex={0}
+ className="m-0.5 px-1 pb-0.5 cursor-pointer rounded-md hover:bg-chicago-700"
+ onClick={() => {
+ if (randomStringLength < 32) {
+ setRandomStringLength(randomStringLength + 1);
+ }
+ }}
+ >
+ +
+
+
+
+
+
+ );
+};
+
+export default GenerateSecretMenu;
diff --git a/frontend/src/components/dashboard/KeyPair.tsx b/frontend/src/components/dashboard/KeyPair.tsx
new file mode 100644
index 000000000..7de4edf82
--- /dev/null
+++ b/frontend/src/components/dashboard/KeyPair.tsx
@@ -0,0 +1,100 @@
+import { faEllipsis } from '@fortawesome/free-solid-svg-icons';
+import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
+import { SecretDataProps } from 'public/data/frequentInterfaces';
+
+import DashboardInputField from './DashboardInputField';
+
+interface KeyPairProps {
+ keyPair: SecretDataProps;
+ modifyKey: (value: string, position: number) => void;
+ modifyValue: (value: string, position: number) => void;
+ modifyValueOverride: (value: string, position: number) => void;
+ isBlurred: boolean;
+ isDuplicate: boolean;
+ toggleSidebar: (id: string) => void;
+ sidebarSecretId: string;
+ isSnapshot: boolean;
+}
+
+/**
+ * This component represent a single row for an environemnt variable on the dashboard
+ * @param {object} obj
+ * @param {String[]} obj.keyPair - data related to the environment variable (id, pos, key, value, public/private)
+ * @param {function} obj.modifyKey - modify the key of a certain environment variable
+ * @param {function} obj.modifyValue - modify the value of a certain environment variable
+ * @param {function} obj.modifyValueOverride - modify the value of a certain environment variable if it is overriden
+ * @param {boolean} obj.isBlurred - if the blurring setting is turned on
+ * @param {boolean} obj.isDuplicate - list of all the duplicates secret names on the dashboard
+ * @param {function} obj.toggleSidebar - open/close/switch sidebar
+ * @param {string} obj.sidebarSecretId - the id of a secret for the side bar is displayed
+ * @param {boolean} obj.isSnapshot - whether this keyPair is in a snapshot. If so, it won't have some features like sidebar
+ * @returns
+ */
+const KeyPair = ({
+ keyPair,
+ modifyKey,
+ modifyValue,
+ modifyValueOverride,
+ isBlurred,
+ isDuplicate,
+ toggleSidebar,
+ sidebarSecretId,
+ isSnapshot
+}: KeyPairProps) => (
+
+
+ {keyPair.valueOverride && (
+
+
+
+ This secret is overriden
+
+
+ )}
+
+
+ {!isSnapshot && (
+
null}
+ role="button"
+ tabIndex={0}
+ onClick={() => toggleSidebar(keyPair.id)}
+ className="cursor-pointer w-[2.35rem] h-[2.35rem] bg-mineshaft-700 hover:bg-chicago-700 rounded-md flex flex-row justify-center items-center duration-200"
+ >
+
+
+ )}
+
+
+);
+
+export default KeyPair;
diff --git a/frontend/src/components/dashboard/SideBar.tsx b/frontend/src/components/dashboard/SideBar.tsx
new file mode 100644
index 000000000..1a3f99a29
--- /dev/null
+++ b/frontend/src/components/dashboard/SideBar.tsx
@@ -0,0 +1,193 @@
+/* eslint-disable react/no-unused-prop-types */
+import { useState } from 'react';
+import Image from 'next/image';
+import { useTranslation } from 'next-i18next';
+import { faX } from '@fortawesome/free-solid-svg-icons';
+import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
+
+import SecretVersionList from '@app/ee/components/SecretVersionList';
+
+import Button from '../basic/buttons/Button';
+import Toggle from '../basic/Toggle';
+import CommentField from './CommentField';
+import DashboardInputField from './DashboardInputField';
+import { DeleteActionButton } from './DeleteActionButton';
+import GenerateSecretMenu from './GenerateSecretMenu';
+
+interface SecretProps {
+ key: string;
+ value: string;
+ valueOverride: string | undefined;
+ pos: number;
+ id: string;
+ comment: string;
+}
+
+export interface DeleteRowFunctionProps {
+ ids: string[];
+ secretName: string;
+}
+
+interface SideBarProps {
+ toggleSidebar: (value: string) => void;
+ data: SecretProps[];
+ modifyKey: (value: string, position: number) => void;
+ modifyValue: (value: string, position: number) => void;
+ modifyValueOverride: (value: string | undefined, position: number) => void;
+ modifyComment: (value: string, position: number) => void;
+ buttonReady: boolean;
+ savePush: () => void;
+ sharedToHide: string[];
+ setSharedToHide: (values: string[]) => void;
+ deleteRow: (props: DeleteRowFunctionProps) => void;
+}
+
+/**
+ * @param {object} obj
+ * @param {function} obj.toggleSidebar - function that opens or closes the sidebar
+ * @param {SecretProps[]} obj.data - data of a certain key valeu pair
+ * @param {function} obj.modifyKey - function that modifies the secret key
+ * @param {function} obj.modifyValue - function that modifies the secret value
+ * @param {function} obj.modifyValueOverride - function that modifies the secret value if it is an override
+ * @param {boolean} obj.buttonReady - is the button for saving chagnes active
+ * @param {function} obj.savePush - save changes andp ush secrets
+ * @param {function} obj.deleteRow - a function to delete a certain keyPair
+ * @returns the sidebar with 'secret's settings'
+ */
+const SideBar = ({
+ toggleSidebar,
+ data,
+ modifyKey,
+ modifyValue,
+ modifyValueOverride,
+ modifyComment,
+ buttonReady,
+ savePush,
+ deleteRow
+}: SideBarProps) => {
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
+ const [isLoading, setIsLoading] = useState(false);
+ const [overrideEnabled, setOverrideEnabled] = useState(data[0].valueOverride !== undefined);
+ const { t } = useTranslation();
+
+ return (
+
+ {isLoading ? (
+
+
+
+ ) : (
+
+
+
{t('dashboard:sidebar.secret')}
+
null}
+ role="button"
+ tabIndex={0}
+ className="p-1"
+ onClick={() => toggleSidebar('None')}
+ >
+
+
+
+
+
{t('dashboard:sidebar.key')}
+
+
+ {(data[0]?.value || data[0]?.value === "") ? (
+
+
{t('dashboard:sidebar.value')}
+
+
+
+
+
+ ) : (
+
+
+ {t('common:note')}:
+
+ {t('dashboard:sidebar.personal-explanation')}
+
+ )}
+
+ {(data[0]?.value || data[0]?.value === "") && (
+
+
{t('dashboard:sidebar.override')}
+
+
+ )}
+
+
+
+
+
+ )}
+
+
+
+ deleteRow({ ids: data.map((secret) => secret.id), secretName: data[0]?.key })
+ }
+ />
+
+
+ );
+};
+
+export default SideBar;
diff --git a/frontend/src/components/integrations/CloudIntegration.tsx b/frontend/src/components/integrations/CloudIntegration.tsx
new file mode 100644
index 000000000..cc06bdd8e
--- /dev/null
+++ b/frontend/src/components/integrations/CloudIntegration.tsx
@@ -0,0 +1,123 @@
+import Image from 'next/image';
+import { faCheck, faX } from '@fortawesome/free-solid-svg-icons';
+import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
+
+import deleteIntegrationAuth from '../../pages/api/integrations/DeleteIntegrationAuth';
+
+interface IntegrationOption {
+ clientId: string;
+ clientSlug?: string; // vercel-integration specific
+ docsLink: string;
+ image: string;
+ isAvailable: boolean;
+ name: string;
+ slug: string;
+ type: string;
+}
+interface IntegrationAuth {
+ _id: string;
+ integration: string;
+ workspace: string;
+ createdAt: string;
+ updatedAt: string;
+}
+
+interface Props {
+ cloudIntegrationOption: IntegrationOption;
+ setSelectedIntegrationOption: (cloudIntegration: IntegrationOption) => void;
+ integrationOptionPress: (cloudIntegrationOption: IntegrationOption) => void;
+ integrationAuths: IntegrationAuth[];
+ handleDeleteIntegrationAuth: (args: { integrationAuth: IntegrationAuth }) => void;
+}
+
+const CloudIntegration = ({
+ cloudIntegrationOption,
+ setSelectedIntegrationOption,
+ integrationOptionPress,
+ integrationAuths,
+ handleDeleteIntegrationAuth
+}: Props) => {
+ return integrationAuths ? (
+
null}
+ role="button"
+ tabIndex={0}
+ className={`relative ${
+ cloudIntegrationOption.isAvailable
+ ? 'hover:bg-white/10 duration-200 cursor-pointer'
+ : 'opacity-50'
+ } flex flex-row bg-white/5 h-32 rounded-md p-4 items-center`}
+ onClick={() => {
+ if (!cloudIntegrationOption.isAvailable) return;
+ setSelectedIntegrationOption(cloudIntegrationOption);
+ integrationOptionPress(cloudIntegrationOption);
+ }}
+ key={cloudIntegrationOption.name}
+ >
+
+ {cloudIntegrationOption.name.split(' ').length > 2 ? (
+
+
{cloudIntegrationOption.name.split(' ')[0]}
+
+ {cloudIntegrationOption.name.split(' ')[1]} {cloudIntegrationOption.name.split(' ')[2]}
+
+
+ ) : (
+
+ {cloudIntegrationOption.name}
+
+ )}
+ {cloudIntegrationOption.isAvailable &&
+ integrationAuths
+ .map((authorization) => authorization.integration)
+ .includes(cloudIntegrationOption.slug) && (
+
+
null}
+ role="button"
+ tabIndex={0}
+ onClick={async (event) => {
+ event.stopPropagation();
+ const deletedIntegrationAuth = await deleteIntegrationAuth({
+ integrationAuthId: integrationAuths
+ .filter(
+ (authorization) =>
+ authorization.integration === cloudIntegrationOption.slug
+ )
+ .map((authorization) => authorization._id)[0]
+ });
+
+ handleDeleteIntegrationAuth({
+ integrationAuth: deletedIntegrationAuth
+ });
+ }}
+ className="cursor-pointer w-max bg-red py-0.5 px-2 rounded-b-md text-xs flex flex-row items-center opacity-0 group-hover:opacity-100 duration-200"
+ >
+
+ Revoke
+
+
+
+ Authorized
+
+
+ )}
+ {!cloudIntegrationOption.isAvailable && (
+
+ )}
+
+ ) : (
+
+ );
+};
+
+export default CloudIntegration;
diff --git a/frontend/src/components/integrations/CloudIntegrationSection.tsx b/frontend/src/components/integrations/CloudIntegrationSection.tsx
new file mode 100644
index 000000000..aadae1308
--- /dev/null
+++ b/frontend/src/components/integrations/CloudIntegrationSection.tsx
@@ -0,0 +1,69 @@
+import { useTranslation } from 'next-i18next';
+
+import CloudIntegration from './CloudIntegration';
+
+interface IntegrationOption {
+ clientId: string;
+ clientSlug?: string; // vercel-integration specific
+ docsLink: string;
+ image: string;
+ isAvailable: boolean;
+ name: string;
+ slug: string;
+ type: string;
+}
+
+interface IntegrationAuth {
+ _id: string;
+ integration: string;
+ workspace: string;
+ createdAt: string;
+ updatedAt: string;
+}
+
+interface Props {
+ cloudIntegrationOptions: IntegrationOption[];
+ setSelectedIntegrationOption: () => void;
+ integrationOptionPress: (integrationOption: IntegrationOption) => void;
+ integrationAuths: IntegrationAuth[];
+ handleDeleteIntegrationAuth: (args: { integrationAuth: IntegrationAuth }) => void;
+}
+
+const CloudIntegrationSection = ({
+ cloudIntegrationOptions,
+ setSelectedIntegrationOption,
+ integrationOptionPress,
+ integrationAuths,
+ handleDeleteIntegrationAuth
+}: Props) => {
+ const { t } = useTranslation();
+
+ return (
+ <>
+
+
+ {t('integrations:cloud-integrations')}
+
+
+ {t('integrations:click-to-start')}
+
+
+
+ {cloudIntegrationOptions.map((cloudIntegrationOption) => (
+
+ ))}
+
+ >
+ );
+};
+
+export default CloudIntegrationSection;
diff --git a/frontend/src/components/integrations/FrameworkIntegration.tsx b/frontend/src/components/integrations/FrameworkIntegration.tsx
new file mode 100644
index 000000000..cf8503827
--- /dev/null
+++ b/frontend/src/components/integrations/FrameworkIntegration.tsx
@@ -0,0 +1,35 @@
+import Image from 'next/image';
+
+interface Framework {
+ name: string;
+ slug: string;
+ image: string;
+ docsLink: string;
+}
+
+const FrameworkIntegration = ({ framework }: { framework: Framework }) => (
+
+ 1 ? 'text-sm px-1' : 'text-xl px-2'
+ } text-center w-full max-w-xs`}
+ >
+ {framework?.image && (
+
+ )}
+ {framework?.name && framework?.image &&
}
+ {framework?.name && framework.name}
+
+
+);
+
+export default FrameworkIntegration;
diff --git a/frontend/src/components/integrations/FrameworkIntegrationSection.tsx b/frontend/src/components/integrations/FrameworkIntegrationSection.tsx
new file mode 100644
index 000000000..31bac0043
--- /dev/null
+++ b/frontend/src/components/integrations/FrameworkIntegrationSection.tsx
@@ -0,0 +1,42 @@
+import { useTranslation } from 'next-i18next';
+
+import FrameworkIntegration from './FrameworkIntegration';
+
+interface Framework {
+ name: string;
+ image: string;
+ link: string;
+ slug: string;
+ docsLink: string;
+}
+
+interface Props {
+ frameworks: [Framework];
+}
+
+const FrameworkIntegrationSection = ({ frameworks }: Props) => {
+ const { t } = useTranslation();
+
+ return (
+ <>
+
+
+ {t('integrations:framework-integrations')}
+
+
+ {t('integrations:click-to-setup')}
+
+
+
+ {frameworks.map((framework) => (
+
+ ))}
+
+ >
+ );
+};
+
+export default FrameworkIntegrationSection;
diff --git a/frontend/src/components/integrations/Integration.tsx b/frontend/src/components/integrations/Integration.tsx
new file mode 100644
index 000000000..abec74f9d
--- /dev/null
+++ b/frontend/src/components/integrations/Integration.tsx
@@ -0,0 +1,248 @@
+/* eslint-disable @typescript-eslint/no-unused-vars */
+import { useEffect, useState } from 'react';
+import { useRouter } from 'next/router';
+import { faArrowRight, faRotate, faX } from '@fortawesome/free-solid-svg-icons';
+import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
+// TODO: This needs to be moved from public folder
+import { contextNetlifyMapping, reverseContextNetlifyMapping } from 'public/data/frequentConstants';
+
+import Button from '@app/components/basic/buttons/Button';
+import ListBox from '@app/components/basic/Listbox';
+
+import deleteIntegration from '../../pages/api/integrations/DeleteIntegration';
+import getIntegrationApps from '../../pages/api/integrations/GetIntegrationApps';
+import updateIntegration from '../../pages/api/integrations/updateIntegration';
+
+interface Integration {
+ _id: string;
+ isActive: boolean;
+ app: string | null;
+ appId: string | null;
+ createdAt: string;
+ updatedAt: string;
+ environment: string;
+ integration: string;
+ targetEnvironment: string;
+ workspace: string;
+ integrationAuth: string;
+}
+
+interface IntegrationApp {
+ name: string;
+ appId?: string;
+ owner?: string;
+}
+
+type Props = {
+ integration: Integration;
+ integrations: Integration[];
+ setIntegrations: any;
+ bot: any;
+ setBot: any;
+ environments: Array<{ name: string; slug: string }>;
+ handleDeleteIntegration: (args: { integration: Integration }) => void;
+};
+
+const IntegrationTile = ({
+ integration,
+ integrations,
+ bot,
+ setBot,
+ setIntegrations,
+ environments = [],
+ handleDeleteIntegration
+}: Props) => {
+ // set initial environment. This find will only execute when component is mounting
+ const [integrationEnvironment, setIntegrationEnvironment] = useState
(
+ environments.find(({ slug }) => slug === integration.environment) || {
+ name: '',
+ slug: ''
+ }
+ );
+ const router = useRouter();
+ const [apps, setApps] = useState([]); // integration app objects
+ const [integrationApp, setIntegrationApp] = useState(''); // integration app name
+ const [integrationTargetEnvironment, setIntegrationTargetEnvironment] = useState('');
+
+ useEffect(() => {
+ const loadIntegration = async () => {
+
+ const tempApps: [IntegrationApp] = await getIntegrationApps({
+ integrationAuthId: integration.integrationAuth
+ });
+
+ setApps(tempApps);
+ setIntegrationApp(integration.app ? integration.app : tempApps[0].name);
+
+ switch (integration.integration) {
+ case 'vercel':
+ setIntegrationTargetEnvironment(
+ integration?.targetEnvironment
+ ? integration.targetEnvironment.charAt(0).toUpperCase() + integration.targetEnvironment.substring(1)
+ : 'Development'
+ );
+ break;
+ case 'netlify':
+ setIntegrationTargetEnvironment(
+ integration?.targetEnvironment
+ ? contextNetlifyMapping[integration.targetEnvironment]
+ : 'Local development'
+ );
+ break;
+ default:
+ break;
+ }
+ };
+
+ loadIntegration();
+ }, []);
+
+ const handleStartIntegration = async () => {
+ const reformatTargetEnvironment = (targetEnvironment: string) => {
+ switch (integration.integration) {
+ case 'vercel':
+ return targetEnvironment.toLowerCase();
+ case 'netlify':
+ return reverseContextNetlifyMapping[targetEnvironment];
+ default:
+ return null;
+ }
+ }
+
+ try {
+ const siteApp = apps.find((app) => app.name === integrationApp); // obj or undefined
+ const appId = siteApp?.appId ?? null;
+ const owner = siteApp?.owner ?? null;
+
+ // return updated integration
+ const updatedIntegration = await updateIntegration({
+ integrationId: integration._id,
+ environment: integrationEnvironment.slug,
+ isActive: true,
+ app: integrationApp,
+ appId,
+ targetEnvironment: reformatTargetEnvironment(integrationTargetEnvironment),
+ owner
+ });
+
+ setIntegrations(
+ integrations.map((i) => i._id === updatedIntegration._id ? updatedIntegration : i)
+ );
+ } catch (err) {
+ console.error(err);
+ }
+ }
+
+ // eslint-disable-next-line @typescript-eslint/no-shadow
+ const renderIntegrationSpecificParams = (integration: Integration) => {
+ try {
+ switch (integration.integration) {
+ case 'vercel':
+ return (
+
+ );
+ case 'netlify':
+ return (
+
+ );
+ default:
+ return
;
+ }
+ } catch (err) {
+ console.error(err);
+ }
+
+ return
;
+ };
+
+ if (!integrationApp || apps.length === 0) return
;
+
+ return (
+
+
+
+
ENVIRONMENT
+
name) : null}
+ isSelected={integrationEnvironment.name}
+ onChange={(envName) =>
+ setIntegrationEnvironment(
+ environments.find(({ name }) => envName === name) || {
+ name: 'unknown',
+ slug: 'unknown'
+ }
+ )
+ }
+ isFull
+ />
+
+
+
+
+
+
INTEGRATION
+
+ {integration.integration.charAt(0).toUpperCase() + integration.integration.slice(1)}
+
+
+
+
APP
+
app.name) : null}
+ isSelected={integrationApp}
+ onChange={(app) => {
+ setIntegrationApp(app);
+ }}
+ />
+
+ {renderIntegrationSpecificParams(integration)}
+
+
+ {integration.isActive ? (
+
+ ) : (
+
handleStartIntegration()}
+ color="mineshaft"
+ size="md"
+ />
+ )}
+
+ handleDeleteIntegration({
+ integration
+ })}
+ color="red"
+ size="icon-md"
+ icon={faX}
+ />
+
+
+
+ );
+};
+
+export default IntegrationTile;
diff --git a/frontend/src/components/integrations/IntegrationSection.tsx b/frontend/src/components/integrations/IntegrationSection.tsx
new file mode 100644
index 000000000..6fe2413d0
--- /dev/null
+++ b/frontend/src/components/integrations/IntegrationSection.tsx
@@ -0,0 +1,62 @@
+import IntegrationTile from './Integration';
+
+interface Props {
+ integrations: any;
+ setIntegrations: any;
+ bot: any;
+ setBot: any;
+ environments: Array<{ name: string; slug: string }>;
+ handleDeleteIntegration: (args: { integration: Integration }) => void;
+}
+
+interface Integration {
+ _id: string;
+ isActive: boolean;
+ app: string | null;
+ appId: string | null;
+ createdAt: string;
+ updatedAt: string;
+ environment: string;
+ integration: string;
+ targetEnvironment: string;
+ workspace: string;
+ integrationAuth: string;
+}
+
+const ProjectIntegrationSection = ({
+ integrations,
+ setIntegrations,
+ bot,
+ setBot,
+ environments = [],
+ handleDeleteIntegration
+}: Props) => {
+ return integrations.length > 0 ? (
+
+
+
Current Integrations
+
+ Manage integrations with third-party services.
+
+
+ {integrations.map((integration: Integration) => {
+ return (
+
+ );
+ })}
+
+ ) : (
+
+ );
+}
+
+export default ProjectIntegrationSection;
\ No newline at end of file
diff --git a/frontend/components/navigation/NavBarDashboard.tsx b/frontend/src/components/navigation/NavBarDashboard.tsx
similarity index 72%
rename from frontend/components/navigation/NavBarDashboard.tsx
rename to frontend/src/components/navigation/NavBarDashboard.tsx
index 1834d8117..d681930ac 100644
--- a/frontend/components/navigation/NavBarDashboard.tsx
+++ b/frontend/src/components/navigation/NavBarDashboard.tsx
@@ -1,8 +1,9 @@
-/* eslint-disable react-hooks/exhaustive-deps */
+/* eslint-disable jsx-a11y/anchor-is-valid */
/* eslint-disable react/jsx-key */
-import React, { Fragment, useEffect, useState } from 'react';
+import { Fragment, useEffect, useMemo, useState } from 'react';
import Image from 'next/image';
import { useRouter } from 'next/router';
+import { TFunction, useTranslation } from 'next-i18next';
import { faGithub, faSlack } from '@fortawesome/free-brands-svg-icons';
import { faCircleQuestion } from '@fortawesome/free-regular-svg-icons';
import {
@@ -17,32 +18,32 @@ import {
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { Menu, Transition } from '@headlessui/react';
-import logout from '~/pages/api/auth/Logout';
-import getOrganization from '~/pages/api/organization/GetOrg';
-import getOrganizations from '~/pages/api/organization/getOrgs';
-import getUser from '~/pages/api/user/getUser';
+import logout from '@app/pages/api/auth/Logout';
+import getOrganization from '../../pages/api/organization/GetOrg';
+import getOrganizations from '../../pages/api/organization/getOrgs';
+import getUser from '../../pages/api/user/getUser';
import guidGenerator from '../utilities/randomId';
-const supportOptions = [
+const supportOptions = (t: TFunction) => [
[
,
- 'Join Slack Forum',
- 'https://join.slack.com/t/infisical-users/shared_invite/zt-1kdbk07ro-RtoyEt_9E~fyzGo_xQYP6g'
+ t('nav:support.slack'),
+ 'https://join.slack.com/t/infisical/shared_invite/zt-1dgg63ln8-G7PCNJdCymAT9YF3j1ewVA'
],
[
,
- 'Read Docs',
+ t('nav:support.docs'),
'https://infisical.com/docs/getting-started/introduction'
],
[
,
- 'Open a GitHub Issue',
+ t('nav:support.issue'),
'https://github.com/Infisical/infisical-cli/issues'
],
[
,
- 'Send us an Email',
+ t('nav:support.email'),
'mailto:support@infisical.com'
]
];
@@ -68,16 +69,20 @@ export default function Navbar() {
const [orgs, setOrgs] = useState([]);
const [currentOrg, setCurrentOrg] = useState();
+ const { t } = useTranslation();
+
+ const supportOptionsList = useMemo(() => supportOptions(t), [t]);
+
useEffect(() => {
(async () => {
const userData = await getUser();
setUser(userData);
const orgsData = await getOrganizations();
setOrgs(orgsData);
- const currentOrg = await getOrganization({
+ const currentUserOrg = await getOrganization({
orgId: String(localStorage.getItem('orgData.id'))
});
- setCurrentOrg(currentOrg);
+ setCurrentOrg(currentUserOrg);
})();
}, []);
@@ -92,20 +97,26 @@ export default function Navbar() {
+
+
+ Docs
+
-
+
@@ -119,18 +130,17 @@ export default function Navbar() {
leaveTo="transform opacity-0 scale-95"
>
- {supportOptions.map((option) => (
- // eslint-disable-next-line react/jsx-no-target-blank
+ {supportOptionsList.map(([icon, text, url]) => (
- {option[0]}
-
{option[1]}
+ {icon}
+
{text}
))}
@@ -139,7 +149,7 @@ export default function Navbar() {
-
+
{user?.firstName} {user?.lastName}
- SIGNED IN AS
+ {t('nav:user.signed-in-as')}
- router.push('/settings/personal/' + router.query.id)
- }
+ onKeyDown={() => null}
+ role="button"
+ tabIndex={0}
+ onClick={() => router.push(`/settings/personal/${router.query.id}`)}
className="flex flex-row items-center px-1 mx-1 my-1 hover:bg-white/5 cursor-pointer rounded-md"
>
@@ -176,10 +187,7 @@ export default function Navbar() {
{' '}
{user?.firstName} {user?.lastName}
-
- {' '}
- {user?.email}
-
+
{user?.email}
- CURRENT ORGANIZATION
+ {t('nav:user.current-organization')}
- router.push('/settings/org/' + router.query.id)
- }
+ onKeyDown={() => null}
+ role="button"
+ tabIndex={0}
+ onClick={() => router.push(`/settings/org/${router.query.id}`)}
className="flex flex-row items-center px-2 mt-2 py-1 hover:bg-white/5 cursor-pointer rounded-md"
>
{currentOrg?.name?.charAt(0)}
-
- {currentOrg?.name}
-
+
{currentOrg?.name}
- router.push('/settings/billing/' + router.query.id)
- }
+ onKeyDown={() => null}
+ role="button"
+ tabIndex={0}
+ onClick={() => router.push(`/settings/billing/${router.query.id}`)}
className="mt-1 relative flex justify-start cursor-pointer select-none py-2 px-2 rounded-md text-gray-400 hover:bg-white/5 duration-200 hover:text-gray-200"
>
-
-
Usage & Billing
+
+
{t('nav:user.usage-billing')}
- router.push(
- '/settings/org/' + router.query.id + '?invite'
- )
- }
+ onKeyDown={() => null}
+ role="button"
+ tabIndex={0}
+ onClick={() => router.push(`/settings/org/${router.query.id}?invite`)}
className="relative flex justify-start cursor-pointer select-none py-2 pl-10 pr-4 rounded-md text-gray-400 hover:bg-primary/100 duration-200 hover:text-black hover:font-semibold mt-1"
>
-
Invite Members
+
{t('nav:user.invite')}
{orgs?.length > 1 && (
- OTHER ORGANIZATIONS
+ {t('nav:user.other-organizations')}
{orgs
.filter(
- (org: { _id: string }) =>
- org._id != localStorage.getItem('orgData.id')
+ (org: { _id: string }) => org._id !== localStorage.getItem('orgData.id')
)
.map((org: { _id: string; name: string }) => (
null}
+ role="button"
+ tabIndex={0}
key={guidGenerator()}
onClick={() => {
localStorage.setItem('orgData.id', org._id);
@@ -271,9 +279,7 @@ export default function Navbar() {
{org.name.charAt(0)}
-
- {org.name}
-
+
{org.name}
))}
@@ -284,11 +290,10 @@ export default function Navbar() {
{({ active }) => (
@@ -296,7 +301,7 @@ export default function Navbar() {
className="text-lg ml-1.5 mr-3"
icon={faRightFromBracket}
/>
- Log Out
+ {t('common:logout')}
)}
diff --git a/frontend/src/components/navigation/NavHeader.tsx b/frontend/src/components/navigation/NavHeader.tsx
new file mode 100644
index 000000000..247d4ac3d
--- /dev/null
+++ b/frontend/src/components/navigation/NavHeader.tsx
@@ -0,0 +1,62 @@
+import { useEffect, useState } from 'react';
+import { useRouter } from 'next/router';
+import { faAngleRight } from '@fortawesome/free-solid-svg-icons';
+import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
+
+import getOrganization from '@app/pages/api/organization/GetOrg';
+import getProjectInfo from '@app/pages/api/workspace/getProjectInfo';
+
+/**
+ * This is the component at the top of almost every page.
+ * It shows how to navigate to a certain page.
+ * It future these links should also be clickable and hoverable
+ * @param obj
+ * @param obj.pageName - Name of the page
+ * @param obj.isProjectRelated - whether this page is related to project or now (determine if it's 2 or 3 navigation steps)
+ * @returns
+ */
+export default function NavHeader({
+ pageName,
+ isProjectRelated
+}: {
+ pageName: string;
+ isProjectRelated?: boolean;
+}): JSX.Element {
+ const [orgName, setOrgName] = useState('');
+ const [workspaceName, setWorkspaceName] = useState('');
+ const router = useRouter();
+ const projectId = String(router.query.id);
+
+ useEffect(() => {
+ (async () => {
+ const orgId = localStorage.getItem('orgData.id');
+ const org = await getOrganization({
+ orgId: orgId || ''
+ });
+ setOrgName(org.name);
+
+ const workspace = await getProjectInfo({
+ projectId
+ });
+ setWorkspaceName(workspace.name);
+ })();
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ }, [projectId]);
+
+ return (
+
+
+ {orgName?.charAt(0)}
+
+
{orgName}
+ {isProjectRelated && (
+ <>
+
+
{workspaceName}
+ >
+ )}
+
+
{pageName}
+
+ );
+}
diff --git a/frontend/src/components/signup/CodeInputStep.tsx b/frontend/src/components/signup/CodeInputStep.tsx
new file mode 100644
index 000000000..67b4e530c
--- /dev/null
+++ b/frontend/src/components/signup/CodeInputStep.tsx
@@ -0,0 +1,136 @@
+/* eslint-disable react/jsx-props-no-spreading */
+import React, { useState } from 'react';
+import ReactCodeInput from 'react-code-input';
+import { useTranslation } from 'next-i18next';
+
+import sendVerificationEmail from '@app/pages/api/auth/SendVerificationEmail';
+
+import Button from '../basic/buttons/Button';
+import Error from '../basic/Error';
+
+// The style for the verification code input
+const props = {
+ inputStyle: {
+ fontFamily: 'monospace',
+ margin: '4px',
+ MozAppearance: 'textfield',
+ width: '55px',
+ borderRadius: '5px',
+ fontSize: '24px',
+ height: '55px',
+ paddingLeft: '7',
+ backgroundColor: '#0d1117',
+ color: 'white',
+ border: '1px solid #2d2f33',
+ textAlign: 'center',
+ outlineColor: '#8ca542',
+ borderColor: '#2d2f33'
+ }
+} as const;
+const propsPhone = {
+ inputStyle: {
+ fontFamily: 'monospace',
+ margin: '4px',
+ MozAppearance: 'textfield',
+ width: '40px',
+ borderRadius: '5px',
+ fontSize: '24px',
+ height: '40px',
+ paddingLeft: '7',
+ backgroundColor: '#0d1117',
+ color: 'white',
+ border: '1px solid #2d2f33',
+ textAlign: 'center',
+ outlineColor: '#8ca542',
+ borderColor: '#2d2f33'
+ }
+} as const;
+
+interface CodeInputStepProps {
+ email: string;
+ incrementStep: () => void;
+ setCode: (value: string) => void;
+ codeError: boolean;
+}
+
+/**
+ * This is the second step of sign up where users need to verify their email
+ * @param {object} obj
+ * @param {string} obj.email - user's email to which we just sent a verification email
+ * @param {function} obj.incrementStep - goes to the next step of signup
+ * @param {function} obj.setCode - state updating function that set the current value of the emai verification code
+ * @param {boolean} obj.codeError - whether the code was inputted wrong or now
+ * @returns
+ */
+export default function CodeInputStep({
+ email,
+ incrementStep,
+ setCode,
+ codeError
+}: CodeInputStepProps): JSX.Element {
+ const [isLoading, setIsLoading] = useState(false);
+ const [isResendingVerificationEmail, setIsResendingVerificationEmail] = useState(false);
+ const { t } = useTranslation();
+
+ const resendVerificationEmail = async () => {
+ setIsResendingVerificationEmail(true);
+ setIsLoading(true);
+ sendVerificationEmail(email);
+ setTimeout(() => {
+ setIsLoading(false);
+ setIsResendingVerificationEmail(false);
+ }, 2000);
+ };
+
+ return (
+
+
{t('signup:step2-message')}
+
{email}
+
+
+
+
+
+
+ {codeError &&
}
+
+
+
+
+
+ {t('signup:step2-resend-alert')}
+
+
+ {isResendingVerificationEmail
+ ? t('signup:step2-resend-progress')
+ : t('signup:step2-resend-submit')}
+
+
+
+
{t('signup:step2-spam-alert')}
+
+
+ );
+}
diff --git a/frontend/src/components/signup/DonwloadBackupPDFStep.tsx b/frontend/src/components/signup/DonwloadBackupPDFStep.tsx
new file mode 100644
index 000000000..664420c3b
--- /dev/null
+++ b/frontend/src/components/signup/DonwloadBackupPDFStep.tsx
@@ -0,0 +1,63 @@
+import { useTranslation } from 'next-i18next';
+import { faWarning } from '@fortawesome/free-solid-svg-icons';
+import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
+
+import Button from '../basic/buttons/Button';
+import issueBackupKey from '../utilities/cryptography/issueBackupKey';
+
+interface DownloadBackupPDFStepProps {
+ incrementStep: () => void;
+ email: string;
+ password: string;
+ name: string;
+}
+
+/**
+ * This is the step of the signup flow where the user downloads the backup pdf
+ * @param {object} obj
+ * @param {function} obj.incrementStep - function that moves the user on to the next stage of signup
+ * @param {string} obj.email - user's email
+ * @param {string} obj.password - user's password
+ * @param {string} obj.name - user's name
+ * @returns
+ */
+export default function DonwloadBackupPDFStep({
+ incrementStep,
+ email,
+ password,
+ name
+}: DownloadBackupPDFStepProps): JSX.Element {
+ const { t } = useTranslation();
+
+ return (
+
+
+ {t('signup:step4-message')}
+
+
+
{t('signup:step4-description1')}
+
{t('signup:step4-description2')}
+
+
+
+ {t('signup:step4-description3')}
+
+
+ {
+ await issueBackupKey({
+ email,
+ password,
+ personalName: name,
+ setBackupKeyError: () => {},
+ setBackupKeyIssued: () => {}
+ });
+ incrementStep();
+ }}
+ size="lg"
+ />
+
+
+ );
+}
diff --git a/frontend/src/components/signup/EnterEmailStep.tsx b/frontend/src/components/signup/EnterEmailStep.tsx
new file mode 100644
index 000000000..d7114946e
--- /dev/null
+++ b/frontend/src/components/signup/EnterEmailStep.tsx
@@ -0,0 +1,99 @@
+import React, { useState } from 'react';
+import Link from 'next/link';
+import { useTranslation } from 'next-i18next';
+
+import sendVerificationEmail from '@app/pages/api/auth/SendVerificationEmail';
+
+import Button from '../basic/buttons/Button';
+import InputField from '../basic/InputField';
+
+interface DownloadBackupPDFStepProps {
+ incrementStep: () => void;
+ email: string;
+ setEmail: (value: string) => void;
+}
+
+/**
+ * This is the first step of the sign up process - users need to enter their email
+ * @param {object} obj
+ * @param {string} obj.email - email of a user signing up
+ * @param {function} obj.setEmail - funciton that manages the state of the email variable
+ * @param {function} obj.incrementStep - function to go to the next step of the signup flow
+ * @returns
+ */
+export default function EnterEmailStep({
+ email,
+ setEmail,
+ incrementStep
+}: DownloadBackupPDFStepProps): JSX.Element {
+ const [emailError, setEmailError] = useState(false);
+ const [emailErrorMessage, setEmailErrorMessage] = useState('');
+ const { t } = useTranslation();
+
+ /**
+ * Verifies if the entered email "looks" correct
+ */
+ const emailCheck = () => {
+ let emailCheckBool = false;
+ if (!email) {
+ setEmailError(true);
+ setEmailErrorMessage('Please enter your email.');
+ emailCheckBool = true;
+ } else if (!email.includes('@') || !email.includes('.') || !/[a-z]/.test(email)) {
+ setEmailError(true);
+ setEmailErrorMessage('Please enter a valid email.');
+ emailCheckBool = true;
+ } else {
+ setEmailError(false);
+ }
+
+ // If everything is correct, go to the next step
+ if (!emailCheckBool) {
+ sendVerificationEmail(email);
+ incrementStep();
+ }
+ };
+
+ return (
+
+
+
+ {t('signup:step1-start')}
+
+
+
+
+
+
{t('signup:step1-privacy')}
+
+
+
+
+
+
+
+
+
+ {t('signup:already-have-account')}
+
+
+
+
+
+ );
+}
diff --git a/frontend/src/components/signup/TeamInviteStep.tsx b/frontend/src/components/signup/TeamInviteStep.tsx
new file mode 100644
index 000000000..29cb95076
--- /dev/null
+++ b/frontend/src/components/signup/TeamInviteStep.tsx
@@ -0,0 +1,71 @@
+import React, { useState } from 'react';
+import { useRouter } from 'next/router';
+import { useTranslation } from 'next-i18next';
+
+import addUserToOrg from '@app/pages/api/organization/addUserToOrg';
+import getWorkspaces from '@app/pages/api/workspace/getWorkspaces';
+
+import Button from '../basic/buttons/Button';
+
+/**
+ * This is the last step of the signup flow. People can optionally invite their teammates here.
+ */
+export default function TeamInviteStep(): JSX.Element {
+ const [emails, setEmails] = useState('');
+ const { t } = useTranslation();
+ const router = useRouter();
+
+ // Redirect user to the getting started page
+ const redirectToHome = async () => {
+ const userWorkspaces = await getWorkspaces();
+ const userWorkspace = userWorkspaces[0]._id;
+ router.push(`/home/${userWorkspace}`);
+ };
+
+ const inviteUsers = async ({ emails: inviteEmails }: { emails: string }) => {
+ inviteEmails
+ .split(',')
+ .map((email) => email.trim())
+ .map(async (email) => addUserToOrg(email, String(localStorage.getItem('orgData.id'))));
+
+ await redirectToHome();
+ };
+
+ return (
+
+
+ {t('signup:step5-invite-team')}
+
+
+ {t('signup:step5-subtitle')}
+
+
+
+
null}
+ role="button"
+ tabIndex={0}
+ className="text-md md:text-sm mx-3 text-bunker-300 bg-mineshaft-700 py-3 md:py-3.5 px-5 rounded-md cursor-pointer hover:bg-mineshaft-500 duration-200"
+ onClick={redirectToHome}
+ >
+ {t('signup:step5-skip')}
+
+
inviteUsers({ emails })}
+ size="lg"
+ />
+
+
+ );
+}
diff --git a/frontend/src/components/signup/UserInfoStep.tsx b/frontend/src/components/signup/UserInfoStep.tsx
new file mode 100644
index 000000000..bf9fa1edb
--- /dev/null
+++ b/frontend/src/components/signup/UserInfoStep.tsx
@@ -0,0 +1,261 @@
+import React, { useState } from 'react';
+import { useRouter } from 'next/router';
+import { useTranslation } from 'next-i18next';
+import { faCheck, faX } from '@fortawesome/free-solid-svg-icons';
+import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
+import jsrp from 'jsrp';
+import nacl from 'tweetnacl';
+import { encodeBase64 } from 'tweetnacl-util';
+
+import completeAccountInformationSignup from '@app/pages/api/auth/CompleteAccountInformationSignup';
+
+import Button from '../basic/buttons/Button';
+import InputField from '../basic/InputField';
+import attemptLogin from '../utilities/attemptLogin';
+import passwordCheck from '../utilities/checks/PasswordCheck';
+import Aes256Gcm from '../utilities/cryptography/aes-256-gcm';
+
+// eslint-disable-next-line new-cap
+const client = new jsrp.client();
+
+interface UserInfoStepProps {
+ verificationToken: string;
+ incrementStep: () => void;
+ email: string;
+ password: string;
+ setPassword: (value: string) => void;
+ firstName: string;
+ setFirstName: (value: string) => void;
+ lastName: string;
+ setLastName: (value: string) => void;
+}
+
+/**
+ * This is the step of the sign up flow where people provife their name/surname and password
+ * @param {object} obj
+ * @param {string} obj.verificationToken - the token which we use to verify the legitness of a user
+ * @param {string} obj.incrementStep - a function to move to the next signup step
+ * @param {string} obj.email - email of a user who is signing up
+ * @param {string} obj.password - user's password
+ * @param {string} obj.setPassword - function managing the state of user's password
+ * @param {string} obj.firstName - user's first name
+ * @param {string} obj.setFirstName - function managing the state of user's first name
+ * @param {string} obj.lastName - user's lastName
+ * @param {string} obj.setLastName - function managing the state of user's last name
+ */
+export default function UserInfoStep({
+ verificationToken,
+ incrementStep,
+ email,
+ password,
+ setPassword,
+ firstName,
+ setFirstName,
+ lastName,
+ setLastName
+}: UserInfoStepProps): JSX.Element {
+ const [firstNameError, setFirstNameError] = useState(false);
+ const [lastNameError, setLastNameError] = useState(false);
+ const [passwordErrorLength, setPasswordErrorLength] = useState(false);
+ const [passwordErrorNumber, setPasswordErrorNumber] = useState(false);
+ const [passwordErrorLowerCase, setPasswordErrorLowerCase] = useState(false);
+
+ const [isLoading, setIsLoading] = useState(false);
+ const { t } = useTranslation();
+ const router = useRouter();
+
+ // Verifies if the information that the users entered (name, workspace)
+ // is there, and if the password matches the criteria.
+ const signupErrorCheck = async () => {
+ setIsLoading(true);
+ let errorCheck = false;
+ if (!firstName) {
+ setFirstNameError(true);
+ errorCheck = true;
+ } else {
+ setFirstNameError(false);
+ }
+ if (!lastName) {
+ setLastNameError(true);
+ errorCheck = true;
+ } else {
+ setLastNameError(false);
+ }
+ errorCheck = passwordCheck({
+ password,
+ setPasswordErrorLength,
+ setPasswordErrorNumber,
+ setPasswordErrorLowerCase,
+ errorCheck
+ });
+
+ if (!errorCheck) {
+ // Generate a random pair of a public and a private key
+ const pair = nacl.box.keyPair();
+ const secretKeyUint8Array = pair.secretKey;
+ const publicKeyUint8Array = pair.publicKey;
+ const PRIVATE_KEY = encodeBase64(secretKeyUint8Array);
+ const PUBLIC_KEY = encodeBase64(publicKeyUint8Array);
+
+ const { ciphertext, iv, tag } = Aes256Gcm.encrypt({
+ text: PRIVATE_KEY,
+ secret: password
+ .slice(0, 32)
+ .padStart(32 + (password.slice(0, 32).length - new Blob([password]).size), '0')
+ }) as { ciphertext: string; iv: string; tag: string };
+
+ localStorage.setItem('PRIVATE_KEY', PRIVATE_KEY);
+
+ client.init(
+ {
+ username: email,
+ password
+ },
+ async () => {
+ client.createVerifier(async (err: any, result: { salt: string; verifier: string }) => {
+ const response = await completeAccountInformationSignup({
+ email,
+ firstName,
+ lastName,
+ organizationName: `${firstName}'s organization`,
+ publicKey: PUBLIC_KEY,
+ ciphertext,
+ iv,
+ tag,
+ salt: result.salt,
+ verifier: result.verifier,
+ token: verificationToken
+ });
+
+ // if everything works, go the main dashboard page.
+ if (response.status === 200) {
+ // response = await response.json();
+
+ localStorage.setItem('publicKey', PUBLIC_KEY);
+ localStorage.setItem('encryptedPrivateKey', ciphertext);
+ localStorage.setItem('iv', iv);
+ localStorage.setItem('tag', tag);
+
+ try {
+ await attemptLogin(email, password, () => {}, router, true, false);
+ incrementStep();
+ } catch (error) {
+ setIsLoading(false);
+ }
+ }
+ });
+ }
+ );
+ } else {
+ setIsLoading(false);
+ }
+ };
+
+ return (
+
+
+ {t('signup:step3-message')}
+
+
+
+
+
+
+
+
+
{
+ setPassword(pass);
+ passwordCheck({
+ password: pass,
+ setPasswordErrorLength,
+ setPasswordErrorNumber,
+ setPasswordErrorLowerCase,
+ errorCheck: false
+ });
+ }}
+ type="password"
+ value={password}
+ isRequired
+ error={passwordErrorLength && passwordErrorNumber && passwordErrorLowerCase}
+ autoComplete="new-password"
+ id="new-password"
+ />
+ {passwordErrorLength || passwordErrorLowerCase || passwordErrorNumber ? (
+
+
{t('section-password:validate-base')}
+
+ {passwordErrorLength ? (
+
+ ) : (
+
+ )}
+
+ {t('section-password:validate-length')}
+
+
+
+ {passwordErrorLowerCase ? (
+
+ ) : (
+
+ )}
+
+ {t('section-password:validate-case')}
+
+
+
+ {passwordErrorNumber ? (
+
+ ) : (
+
+ )}
+
+ {t('section-password:validate-number')}
+
+
+
+ ) : (
+
+ )}
+
+
+
+
+
+ );
+}
diff --git a/frontend/src/components/utilities/SecurityClient.ts b/frontend/src/components/utilities/SecurityClient.ts
new file mode 100644
index 000000000..fa1ec0bb0
--- /dev/null
+++ b/frontend/src/components/utilities/SecurityClient.ts
@@ -0,0 +1,20 @@
+import { getAuthToken, setAuthToken } from '@app/reactQuery';
+
+// depreciated: go for apiRequest module in config/api
+export default class SecurityClient {
+ static setToken(tokenStr: string) {
+ setAuthToken(tokenStr);
+ }
+
+ static async fetchCall(resource: RequestInfo, options?: RequestInit | undefined) {
+ const req = new Request(resource, options);
+
+ const token = getAuthToken();
+
+ if (token) {
+ req.headers.set('Authorization', `Bearer ${token}`);
+ }
+
+ return fetch(req);
+ }
+}
diff --git a/frontend/src/components/utilities/attemptLogin.ts b/frontend/src/components/utilities/attemptLogin.ts
new file mode 100644
index 000000000..acc2b67fe
--- /dev/null
+++ b/frontend/src/components/utilities/attemptLogin.ts
@@ -0,0 +1,226 @@
+/* eslint-disable prefer-destructuring */
+import crypto from 'crypto';
+
+import jsrp from 'jsrp';
+import { SecretDataProps } from 'public/data/frequentInterfaces';
+
+import Aes256Gcm from '@app/components/utilities/cryptography/aes-256-gcm';
+import login1 from '@app/pages/api/auth/Login1';
+import login2 from '@app/pages/api/auth/Login2';
+import addSecrets from '@app/pages/api/files/AddSecrets';
+import getOrganizations from '@app/pages/api/organization/getOrgs';
+import getOrganizationUserProjects from '@app/pages/api/organization/GetOrgUserProjects';
+import getUser from '@app/pages/api/user/getUser';
+import uploadKeys from '@app/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';
+
+// eslint-disable-next-line new-cap
+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
+ },
+ 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[] = [
+ {
+ pos: 0,
+ key: 'DATABASE_URL',
+ // eslint-disable-next-line no-template-curly-in-string
+ value: 'mongodb+srv://${DB_USERNAME}:${DB_PASSWORD}@mongodb.net',
+ valueOverride: undefined,
+ comment: 'This is an example of secret referencing.',
+ id: ''
+ },
+ {
+ pos: 1,
+ key: 'DB_USERNAME',
+ value: 'OVERRIDE_THIS',
+ valueOverride: undefined,
+ 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: ''
+ },
+ {
+ pos: 2,
+ key: 'DB_PASSWORD',
+ value: 'OVERRIDE_THIS',
+ valueOverride: undefined,
+ 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: ''
+ },
+ {
+ pos: 3,
+ key: 'DB_USERNAME',
+ value: 'user1234',
+ valueOverride: 'user1234',
+ comment: '',
+ id: ''
+ },
+ {
+ pos: 4,
+ key: 'DB_PASSWORD',
+ value: 'example_password',
+ valueOverride: 'example_password',
+ comment: '',
+ id: ''
+ },
+ {
+ pos: 5,
+ key: 'TWILIO_AUTH_TOKEN',
+ value: 'example_twillio_token',
+ valueOverride: undefined,
+ comment: '',
+ id: ''
+ },
+ {
+ pos: 6,
+ key: 'WEBSITE_URL',
+ value: 'http://localhost:3000',
+ valueOverride: undefined,
+ comment: '',
+ 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) {
+ if (localStorage.getItem('projectData.id') !== "undefined") {
+ router.push(`/dashboard/${localStorage.getItem('projectData.id')}`);
+ } else {
+ router.push("/noprojects");
+ }
+ }
+ } 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;
diff --git a/frontend/components/utilities/checks/OnboardingCheck.ts b/frontend/src/components/utilities/checks/OnboardingCheck.ts
similarity index 61%
rename from frontend/components/utilities/checks/OnboardingCheck.ts
rename to frontend/src/components/utilities/checks/OnboardingCheck.ts
index 343efadb5..ccdf03dff 100644
--- a/frontend/components/utilities/checks/OnboardingCheck.ts
+++ b/frontend/src/components/utilities/checks/OnboardingCheck.ts
@@ -1,5 +1,5 @@
-import getOrganizationUsers from '~/pages/api/organization/GetOrgUsers';
-import checkUserAction from '~/pages/api/userActions/checkUserAction';
+import getOrganizationUsers from '@app/pages/api/organization/GetOrgUsers';
+import checkUserAction from '@app/pages/api/userActions/checkUserAction';
interface OnboardingCheckProps {
setTotalOnboardingActionsDone?: (value: number) => void;
@@ -26,46 +26,43 @@ const onboardingCheck = async ({
action: 'slack_cta_clicked'
});
if (userActionSlack) {
- countActions = countActions + 1;
+ countActions += 1;
}
- setHasUserClickedSlack &&
- setHasUserClickedSlack(userActionSlack ? true : false);
+ if (setHasUserClickedSlack) setHasUserClickedSlack(!!userActionSlack);
const userActionSecrets = await checkUserAction({
action: 'first_time_secrets_pushed'
});
if (userActionSecrets) {
- countActions = countActions + 1;
+ countActions += 1;
}
- setHasUserPushedSecrets &&
- setHasUserPushedSecrets(userActionSecrets ? true : false);
+ if (setHasUserPushedSecrets) setHasUserPushedSecrets(!!userActionSecrets);
const userActionIntro = await checkUserAction({
action: 'intro_cta_clicked'
});
if (userActionIntro) {
- countActions = countActions + 1;
+ countActions += 1;
}
- setHasUserClickedIntro &&
- setHasUserClickedIntro(userActionIntro ? true : false);
+ if (setHasUserClickedIntro) setHasUserClickedIntro(!!userActionIntro);
const userActionStar = await checkUserAction({
action: 'star_cta_clicked'
});
if (userActionStar) {
- countActions = countActions + 1;
+ countActions += 1;
}
- setHasUserStarred && setHasUserStarred(userActionStar ? true : false);
+ if (setHasUserStarred) setHasUserStarred(!!userActionStar);
const orgId = localStorage.getItem('orgData.id');
const orgUsers = await getOrganizationUsers({
- orgId: orgId ? orgId : ''
+ orgId: orgId || ''
});
if (orgUsers.length > 1) {
- countActions = countActions + 1;
+ countActions += 1;
}
- setUsersInOrg && setUsersInOrg(orgUsers.length > 1);
- setTotalOnboardingActionsDone && setTotalOnboardingActionsDone(countActions);
+ if (setUsersInOrg) setUsersInOrg(orgUsers.length > 1);
+ if (setTotalOnboardingActionsDone) setTotalOnboardingActionsDone(countActions);
};
export default onboardingCheck;
diff --git a/frontend/components/utilities/checks/PasswordCheck.ts b/frontend/src/components/utilities/checks/PasswordCheck.ts
similarity index 95%
rename from frontend/components/utilities/checks/PasswordCheck.ts
rename to frontend/src/components/utilities/checks/PasswordCheck.ts
index 10240b747..e84cba761 100644
--- a/frontend/components/utilities/checks/PasswordCheck.ts
+++ b/frontend/src/components/utilities/checks/PasswordCheck.ts
@@ -1,6 +1,7 @@
+/* eslint-disable no-param-reassign */
interface PasswordCheckProps {
password: string;
- currentErrorCheck: boolean;
+ errorCheck: boolean;
setPasswordErrorLength: (value: boolean) => void;
setPasswordErrorNumber: (value: boolean) => void;
setPasswordErrorLowerCase: (value: boolean) => void;
@@ -14,9 +15,8 @@ const passwordCheck = ({
setPasswordErrorLength,
setPasswordErrorNumber,
setPasswordErrorLowerCase,
- currentErrorCheck,
+ errorCheck
}: PasswordCheckProps) => {
- let errorCheck = currentErrorCheck;
if (!password || password.length < 14) {
setPasswordErrorLength(true);
errorCheck = true;
diff --git a/frontend/components/utilities/checks/tempLocalStorage.ts b/frontend/src/components/utilities/checks/tempLocalStorage.ts
similarity index 100%
rename from frontend/components/utilities/checks/tempLocalStorage.ts
rename to frontend/src/components/utilities/checks/tempLocalStorage.ts
diff --git a/frontend/components/utilities/config/index.ts b/frontend/src/components/utilities/config/index.ts
similarity index 86%
rename from frontend/components/utilities/config/index.ts
rename to frontend/src/components/utilities/config/index.ts
index d0ffed00c..7ecfb9d4d 100644
--- a/frontend/components/utilities/config/index.ts
+++ b/frontend/src/components/utilities/config/index.ts
@@ -4,11 +4,12 @@ const POSTHOG_HOST =
process.env.NEXT_PUBLIC_POSTHOG_HOST! || "https://app.posthog.com";
const STRIPE_PRODUCT_PRO = process.env.NEXT_PUBLIC_STRIPE_PRODUCT_PRO!;
const STRIPE_PRODUCT_STARTER = process.env.NEXT_PUBLIC_STRIPE_PRODUCT_STARTER!;
+const {SITE_URL} = process.env
export {
ENV,
POSTHOG_API_KEY,
POSTHOG_HOST,
+ SITE_URL,
STRIPE_PRODUCT_PRO,
- STRIPE_PRODUCT_STARTER,
-};
+ STRIPE_PRODUCT_STARTER};
\ No newline at end of file
diff --git a/frontend/components/utilities/cryptography/aes-256-gcm.ts b/frontend/src/components/utilities/cryptography/aes-256-gcm.ts
similarity index 93%
rename from frontend/components/utilities/cryptography/aes-256-gcm.ts
rename to frontend/src/components/utilities/cryptography/aes-256-gcm.ts
index aa4986dc4..5a2301c25 100644
--- a/frontend/components/utilities/cryptography/aes-256-gcm.ts
+++ b/frontend/src/components/utilities/cryptography/aes-256-gcm.ts
@@ -32,7 +32,6 @@ class Aes256Gcm {
/**
* No need to run the constructor. The class only has static methods.
*/
- constructor() {}
/**
* Encrypts text with AES 256 GCM.
@@ -65,11 +64,7 @@ class Aes256Gcm {
* @returns {string}
*/
static decrypt({ ciphertext, iv, tag, secret }: DecryptProps): string {
- const decipher = crypto.createDecipheriv(
- ALGORITHM,
- secret,
- Buffer.from(iv, 'base64')
- );
+ const decipher = crypto.createDecipheriv(ALGORITHM, secret, Buffer.from(iv, 'base64'));
decipher.setAuthTag(Buffer.from(tag, 'base64'));
let cleartext = decipher.update(ciphertext, 'base64', 'utf8');
diff --git a/frontend/components/utilities/cryptography/changePassword.js b/frontend/src/components/utilities/cryptography/changePassword.ts
similarity index 76%
rename from frontend/components/utilities/cryptography/changePassword.js
rename to frontend/src/components/utilities/cryptography/changePassword.ts
index c173b3cdf..db72856ed 100644
--- a/frontend/components/utilities/cryptography/changePassword.js
+++ b/frontend/src/components/utilities/cryptography/changePassword.ts
@@ -1,11 +1,11 @@
-import changePassword2 from '~/pages/api/auth/ChangePassword2';
-import SRP1 from '~/pages/api/auth/SRP1';
+/* eslint-disable new-cap */
+import jsrp from 'jsrp';
+
+import changePassword2 from '@app/pages/api/auth/ChangePassword2';
+import SRP1 from '@app/pages/api/auth/SRP1';
import Aes256Gcm from './aes-256-gcm';
-const nacl = require('tweetnacl');
-nacl.util = require('tweetnacl-util');
-const jsrp = require('jsrp');
const clientOldPassword = new jsrp.client();
const clientNewPassword = new jsrp.client();
@@ -19,13 +19,13 @@ const clientNewPassword = new jsrp.client();
* @returns
*/
const changePassword = async (
- email,
- currentPassword,
- newPassword,
- setCurrentPasswordError,
- setPasswordChanged,
- setCurrentPassword,
- setNewPassword
+ email: string,
+ currentPassword: string,
+ newPassword: string,
+ setCurrentPasswordError: (arg: boolean) => void,
+ setPasswordChanged: (arg: boolean) => void,
+ setCurrentPassword: (arg: string) => void,
+ setNewPassword: (arg: string) => void
) => {
try {
setPasswordChanged(false);
@@ -39,10 +39,11 @@ const changePassword = async (
async () => {
const clientPublicKey = clientOldPassword.getPublicKey();
- let serverPublicKey, salt;
+ let serverPublicKey;
+ let salt;
try {
const res = await SRP1({
- clientPublicKey: clientPublicKey
+ clientPublicKey
});
serverPublicKey = res.serverPublicKey;
salt = res.salt;
@@ -64,13 +65,11 @@ const changePassword = async (
clientNewPassword.createVerifier(async (err, result) => {
// The Blob part here is needed to account for symbols that count as 2+ bytes (e.g., รฉ, รฅ, รธ)
const { ciphertext, iv, tag } = Aes256Gcm.encrypt({
- text: localStorage.getItem('PRIVATE_KEY'),
+ text: localStorage.getItem('PRIVATE_KEY') as string,
secret: newPassword
.slice(0, 32)
.padStart(
- 32 +
- (newPassword.slice(0, 32).length -
- new Blob([newPassword]).size),
+ 32 + (newPassword.slice(0, 32).length - new Blob([newPassword]).size),
'0'
)
});
@@ -90,16 +89,16 @@ const changePassword = async (
verifier: result.verifier,
clientProof
});
- if (res.status == 400) {
+ if (res && res.status === 400) {
setCurrentPasswordError(true);
- } else if (res.status == 200) {
+ } else if (res && res.status === 200) {
setPasswordChanged(true);
setCurrentPassword('');
setNewPassword('');
}
- } catch (err) {
+ } catch (error) {
setCurrentPasswordError(true);
- console.log(err);
+ console.log(error);
}
}
});
diff --git a/frontend/components/utilities/cryptography/crypto.ts b/frontend/src/components/utilities/cryptography/crypto.ts
similarity index 81%
rename from frontend/components/utilities/cryptography/crypto.ts
rename to frontend/src/components/utilities/cryptography/crypto.ts
index 409b447fc..d1db995ee 100644
--- a/frontend/components/utilities/cryptography/crypto.ts
+++ b/frontend/src/components/utilities/cryptography/crypto.ts
@@ -1,8 +1,9 @@
-const nacl = require('tweetnacl');
-nacl.util = require('tweetnacl-util');
import aes from './aes-256-gcm';
-type encryptAsymmetricProps = {
+const nacl = require('tweetnacl');
+nacl.util = require('tweetnacl-util');
+
+type EncryptAsymmetricProps = {
plaintext: string;
publicKey: string;
privateKey: string;
@@ -23,7 +24,10 @@ const encryptAssymmetric = ({
plaintext,
publicKey,
privateKey
-}: encryptAsymmetricProps): object => {
+}: EncryptAsymmetricProps): {
+ ciphertext: string;
+ nonce: string;
+} => {
const nonce = nacl.randomBytes(24);
const ciphertext = nacl.box(
nacl.util.decodeUTF8(plaintext),
@@ -38,7 +42,7 @@ const encryptAssymmetric = ({
};
};
-type decryptAsymmetricProps = {
+type DecryptAsymmetricProps = {
ciphertext: string;
nonce: string;
publicKey: string;
@@ -59,7 +63,7 @@ const decryptAssymmetric = ({
nonce,
publicKey,
privateKey
-}: decryptAsymmetricProps): string => {
+}: DecryptAsymmetricProps): string => {
const plaintext = nacl.box.open(
nacl.util.decodeBase64(ciphertext),
nacl.util.decodeBase64(nonce),
@@ -70,22 +74,23 @@ const decryptAssymmetric = ({
return nacl.util.encodeUTF8(plaintext);
};
-type encryptSymmetricProps = {
+type EncryptSymmetricProps = {
plaintext: string;
key: string;
};
+type EncryptSymmetricReturn = {
+ ciphertext: string;
+ iv: string;
+ tag: string;
+};
/**
* Return symmetrically encrypted [plaintext] using [key].
- * @param {Object} obj
- * @param {String} obj.plaintext - plaintext to encrypt
- * @param {String} obj.key - 16-byte hex key
*/
-const encryptSymmetric = ({
- plaintext,
- key
-}: encryptSymmetricProps): object => {
- let ciphertext, iv, tag;
+const encryptSymmetric = ({ plaintext, key }: EncryptSymmetricProps): EncryptSymmetricReturn => {
+ let ciphertext;
+ let iv;
+ let tag;
try {
const obj = aes.encrypt({ text: plaintext, secret: key });
ciphertext = obj.ciphertext;
@@ -104,7 +109,7 @@ const encryptSymmetric = ({
};
};
-type decryptSymmetricProps = {
+type DecryptSymmetricProps = {
ciphertext: string;
iv: string;
tag: string;
@@ -121,12 +126,7 @@ type decryptSymmetricProps = {
* @param {String} obj.key - 32-byte hex key
*
*/
-const decryptSymmetric = ({
- ciphertext,
- iv,
- tag,
- key
-}: decryptSymmetricProps): string => {
+const decryptSymmetric = ({ ciphertext, iv, tag, key }: DecryptSymmetricProps): string => {
let plaintext;
try {
plaintext = aes.decrypt({ ciphertext, iv, tag, secret: key });
@@ -138,9 +138,4 @@ const decryptSymmetric = ({
return plaintext;
};
-export {
- decryptAssymmetric,
- decryptSymmetric,
- encryptAssymmetric,
- encryptSymmetric
-};
+export { decryptAssymmetric, decryptSymmetric, encryptAssymmetric, encryptSymmetric };
diff --git a/frontend/components/utilities/cryptography/issueBackupKey.ts b/frontend/src/components/utilities/cryptography/issueBackupKey.ts
similarity index 86%
rename from frontend/components/utilities/cryptography/issueBackupKey.ts
rename to frontend/src/components/utilities/cryptography/issueBackupKey.ts
index 147377b77..49ea5400c 100644
--- a/frontend/components/utilities/cryptography/issueBackupKey.ts
+++ b/frontend/src/components/utilities/cryptography/issueBackupKey.ts
@@ -1,15 +1,16 @@
-import issueBackupPrivateKey from '~/pages/api/auth/IssueBackupPrivateKey';
-import SRP1 from '~/pages/api/auth/SRP1';
+/* eslint-disable new-cap */
+import crypto from 'crypto';
+
+import jsrp from 'jsrp';
+
+import issueBackupPrivateKey from '@app/pages/api/auth/IssueBackupPrivateKey';
+import SRP1 from '@app/pages/api/auth/SRP1';
import generateBackupPDF from '../generateBackupPDF';
import Aes256Gcm from './aes-256-gcm';
-const nacl = require('tweetnacl');
-nacl.util = require('tweetnacl-util');
-const jsrp = require('jsrp');
const clientPassword = new jsrp.client();
const clientKey = new jsrp.client();
-const crypto = require('crypto');
interface BackupKeyProps {
email: string;
@@ -42,15 +43,16 @@ const issueBackupKey = async ({
clientPassword.init(
{
username: email,
- password: password
+ password
},
async () => {
const clientPublicKey = clientPassword.getPublicKey();
- let serverPublicKey, salt;
+ let serverPublicKey;
+ let salt;
try {
const res = await SRP1({
- clientPublicKey: clientPublicKey
+ clientPublicKey
});
serverPublicKey = res.serverPublicKey;
salt = res.salt;
@@ -87,9 +89,9 @@ const issueBackupKey = async ({
clientProof
});
- if (res?.status == 400) {
+ if (res?.status === 400) {
setBackupKeyError(true);
- } else if (res?.status == 200) {
+ } else if (res?.status === 200) {
generateBackupPDF({
personalName,
personalEmail: email,
diff --git a/frontend/components/utilities/generateBackupPDF.ts b/frontend/src/components/utilities/generateBackupPDF.ts
similarity index 98%
rename from frontend/components/utilities/generateBackupPDF.ts
rename to frontend/src/components/utilities/generateBackupPDF.ts
index d47bdfe73..e51dfaa0a 100644
--- a/frontend/components/utilities/generateBackupPDF.ts
+++ b/frontend/src/components/utilities/generateBackupPDF.ts
@@ -1,5 +1,7 @@
import { jsPDF } from 'jspdf';
+import { SITE_URL } from './config';
+
interface PDFProps {
personalName: string;
personalEmail: string;
@@ -9,22 +11,19 @@ interface PDFProps {
/**
* This function generate a pdf with a secret key for a user.
*/
-function generateBackupPDF({
- personalName,
- personalEmail,
- generatedKey
-}: PDFProps) {
+function generateBackupPDF({ personalName, personalEmail, generatedKey }: PDFProps) {
const imgData =
'data:image/png;base64,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';
// new jsPDF('p', 'mm', [297, 210]);
const today = new Date();
const dd = String(today.getDate()).padStart(2, '0');
- const mm = String(today.getMonth() + 1).padStart(2, '0'); //January is 0!
+ const mm = String(today.getMonth() + 1).padStart(2, '0'); // January is 0!
const yyyy = today.getFullYear();
- const todayFormatted = mm + '/' + dd + '/' + yyyy;
+ const todayFormatted = `${mm}/${dd}/${yyyy}`;
+ // eslint-disable-next-line new-cap
const doc = new jsPDF('p', 'pt');
doc.setFillColor(255, 255, 255);
doc.rect(0, 0, 600, 900, 'F');
@@ -32,12 +31,7 @@ function generateBackupPDF({
doc.addImage(imgData, 'png', 30, 35, 535, 72);
doc.addFont('helvetica', 'normal', '');
doc.setFontSize(12);
- doc.text(
- 'Created for ' + personalName + ' on ' + todayFormatted + '.',
- 290,
- 130,
- { align: 'center' }
- );
+ doc.text(`Created for ${personalName} on ${todayFormatted}.`, 290, 130, { align: 'center' });
doc.setFontSize(14);
doc.text(
'In case you get locked out of you Infisical account, you`ll need these account details',
@@ -46,11 +40,7 @@ function generateBackupPDF({
);
doc.text('to sign in โ', 32, 200);
doc.setFont('helvetica', 'bold');
- doc.text(
- 'including your Secret Key, which we absolutely cannot access or',
- 110,
- 200
- );
+ doc.text('including your Secret Key, which we absolutely cannot access or', 110, 200);
doc.text('recover for you. ', 32, 220);
doc.setFont('helvetica', 'normal');
doc.text('Recommendations:', 32, 250);
@@ -85,7 +75,7 @@ function generateBackupPDF({
doc.roundedRect(170, 488, 375, 35, 5, 5, 'F');
doc.setTextColor(23, 23, 23);
doc.setFontSize(14);
- doc.text('https://app.infisical.com/login', 180, 420);
+ doc.text(`${SITE_URL}/login`, 180, 420);
doc.text(personalEmail, 180, 465);
doc.text(generatedKey, 180, 510);
doc.text('Need help? Contact us at support@infisical.com', 32, 575);
diff --git a/frontend/src/components/utilities/parseDotEnv.ts b/frontend/src/components/utilities/parseDotEnv.ts
new file mode 100644
index 000000000..5d8869b60
--- /dev/null
+++ b/frontend/src/components/utilities/parseDotEnv.ts
@@ -0,0 +1,67 @@
+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[]] | [] = [];
+
+ // eslint-disable-next-line no-cond-assign
+ 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, cmnts] = line;
+ object[key as string] = { value, comments: cmnts };
+ });
+
+ return object;
+}
diff --git a/frontend/components/utilities/randomId.ts b/frontend/src/components/utilities/randomId.ts
similarity index 55%
rename from frontend/components/utilities/randomId.ts
rename to frontend/src/components/utilities/randomId.ts
index 8e1c7f972..9c9382c14 100644
--- a/frontend/components/utilities/randomId.ts
+++ b/frontend/src/components/utilities/randomId.ts
@@ -3,23 +3,11 @@
* @returns
*/
const guidGenerator = () => {
- const S4 = function () {
+ const S4 = () => {
+ // eslint-disable-next-line no-bitwise
return (((1 + Math.random()) * 0x10000) | 0).toString(16).substring(1);
};
- return (
- S4() +
- S4() +
- '-' +
- S4() +
- '-' +
- S4() +
- '-' +
- S4() +
- '-' +
- S4() +
- S4() +
- S4()
- );
+ return `${S4() + S4()}-${S4()}-${S4()}-${S4()}-${S4()}${S4()}${S4()}`;
};
export default guidGenerator;
diff --git a/frontend/components/utilities/saveTokenToLocalStorage.ts b/frontend/src/components/utilities/saveTokenToLocalStorage.ts
similarity index 89%
rename from frontend/components/utilities/saveTokenToLocalStorage.ts
rename to frontend/src/components/utilities/saveTokenToLocalStorage.ts
index 35b336181..b624babd6 100644
--- a/frontend/components/utilities/saveTokenToLocalStorage.ts
+++ b/frontend/src/components/utilities/saveTokenToLocalStorage.ts
@@ -22,7 +22,7 @@ export const saveTokenToLocalStorage = ({
} catch (err) {
if (err instanceof Error) {
throw new Error(
- "Unable to send the tokens in local storage:" + err.message
+ `Unable to send the tokens in local storage:${ err.message}`
);
}
}
diff --git a/frontend/src/components/utilities/secrets/checkOverrides.ts b/frontend/src/components/utilities/secrets/checkOverrides.ts
new file mode 100644
index 000000000..356f0025e
--- /dev/null
+++ b/frontend/src/components/utilities/secrets/checkOverrides.ts
@@ -0,0 +1,32 @@
+import { SecretDataProps } from 'public/data/frequentInterfaces';
+
+/**
+ * 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.valueOverride === undefined || secret?.value !== secret?.valueOverride
+ ? 'shared'
+ : 'personal'
+ );
+ if (overridenSecrets.length) {
+ overridenSecrets.forEach((secret) => {
+ const index = secrets!.findIndex(
+ (_secret) =>
+ _secret.key === secret.key &&
+ (secret.valueOverride === undefined || secret?.value !== secret?.valueOverride)
+ );
+ secrets![index].value = secret.value;
+ });
+ secrets = secrets!.filter(
+ (secret) => secret.valueOverride === undefined || secret?.value !== secret?.valueOverride
+ );
+ }
+ return secrets;
+};
+
+export default checkOverrides;
diff --git a/frontend/src/components/utilities/secrets/downloadDotEnv.ts b/frontend/src/components/utilities/secrets/downloadDotEnv.ts
new file mode 100644
index 000000000..49702fb70
--- /dev/null
+++ b/frontend/src/components/utilities/secrets/downloadDotEnv.ts
@@ -0,0 +1,37 @@
+import { SecretDataProps } from 'public/data/frequentInterfaces';
+
+import checkOverrides from './checkOverrides';
+
+/**
+ * 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 = `${env}.env`;
+ alink.click();
+};
+
+export default downloadDotEnv;
diff --git a/frontend/src/components/utilities/secrets/downloadYaml.ts b/frontend/src/components/utilities/secrets/downloadYaml.ts
new file mode 100644
index 000000000..5af38f9b1
--- /dev/null
+++ b/frontend/src/components/utilities/secrets/downloadYaml.ts
@@ -0,0 +1,43 @@
+// import YAML from 'yaml';
+// import { YAMLSeq } from 'yaml/types';
+
+import { SecretDataProps } from 'public/data/frequentInterfaces';
+
+// import { envMapping } from "../../../public/data/frequentConstants";
+// import checkOverrides from './checkOverrides';
+
+/**
+ * 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
+ */
+// eslint-disable-next-line @typescript-eslint/no-unused-vars
+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();
+};
+
+export default downloadYaml;
diff --git a/frontend/src/components/utilities/secrets/encryptSecrets.ts b/frontend/src/components/utilities/secrets/encryptSecrets.ts
new file mode 100644
index 000000000..53ce0066d
--- /dev/null
+++ b/frontend/src/components/utilities/secrets/encryptSecrets.ts
@@ -0,0 +1,120 @@
+import crypto from 'crypto';
+
+import { SecretDataProps } from 'public/data/frequentInterfaces';
+
+import getLatestFileKey from '@app/pages/api/workspace/getLatestFileKey';
+
+import { decryptAssymmetric, encryptSymmetric } from '../cryptography/crypto';
+
+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') as string;
+
+ 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.valueOverride === undefined || secret?.value !== secret?.valueOverride
+ ? 'shared'
+ : 'personal'
+ };
+
+ return result;
+ });
+ } catch (error) {
+ console.log('Error while encrypting secrets');
+ }
+
+ return secrets;
+};
+
+export default encryptSecrets;
diff --git a/frontend/src/components/utilities/secrets/getSecretsForProject.ts b/frontend/src/components/utilities/secrets/getSecretsForProject.ts
new file mode 100644
index 000000000..c00f13d04
--- /dev/null
+++ b/frontend/src/components/utilities/secrets/getSecretsForProject.ts
@@ -0,0 +1,142 @@
+import getSecrets from '@app/pages/api/files/GetSecrets';
+import getLatestFileKey from '@app/pages/api/workspace/getLatestFileKey';
+
+import { decryptAssymmetric, decryptSymmetric } from '../cryptography/crypto';
+
+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;
+ type: 'personal' | 'shared';
+ comment: string;
+ id: string;
+}
+
+interface FunctionProps {
+ env: string;
+ 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,
+ setIsKeyAvailable,
+ setData,
+ workspaceId
+}: FunctionProps) => {
+ try {
+ let encryptedSecrets;
+ try {
+ encryptedSecrets = await getSecrets(workspaceId, env);
+ } catch (error) {
+ 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(!latestKey ? encryptedSecrets.length === 0 : true);
+
+ const PRIVATE_KEY = localStorage.getItem('PRIVATE_KEY') as string;
+
+ const tempDecryptedSecrets: SecretProps[] = [];
+ if (latestKey) {
+ // assymmetrically decrypt symmetric key with local private key
+ const key = decryptAssymmetric({
+ ciphertext: latestKey.latestKey.encryptedKey,
+ nonce: latestKey.latestKey.nonce,
+ publicKey: latestKey.latestKey.sender.publicKey,
+ privateKey: PRIVATE_KEY
+ });
+
+ // decrypt secret keys, values, and comments
+ encryptedSecrets.forEach((secret: EncryptedSecretProps) => {
+ const plainTextKey = decryptSymmetric({
+ ciphertext: secret.secretKeyCiphertext,
+ iv: secret.secretKeyIV,
+ tag: secret.secretKeyTag,
+ key
+ });
+
+ const plainTextValue = decryptSymmetric({
+ ciphertext: secret.secretValueCiphertext,
+ iv: secret.secretValueIV,
+ tag: secret.secretValueTag,
+ key
+ });
+
+ let plainTextComment;
+ if (secret.secretCommentCiphertext) {
+ plainTextComment = decryptSymmetric({
+ ciphertext: secret.secretCommentCiphertext,
+ iv: secret.secretCommentIV,
+ tag: secret.secretCommentTag,
+ key
+ });
+ } else {
+ plainTextComment = '';
+ }
+
+ tempDecryptedSecrets.push({
+ id: secret._id,
+ key: plainTextKey,
+ value: plainTextValue,
+ type: secret.type,
+ comment: plainTextComment
+ });
+ });
+ }
+
+ const secretKeys = [...new Set(tempDecryptedSecrets.map((secret) => secret.key))];
+
+ const result = secretKeys.map((key, index) => ({
+ id: tempDecryptedSecrets.filter((secret) => secret.key === key && secret.type === 'shared')[0]
+ ?.id,
+ idOverride: tempDecryptedSecrets.filter(
+ (secret) => secret.key === key && secret.type === 'personal'
+ )[0]?.id,
+ pos: index,
+ key,
+ value: tempDecryptedSecrets.filter(
+ (secret) => secret.key === key && secret.type === 'shared'
+ )[0]?.value,
+ valueOverride: tempDecryptedSecrets.filter(
+ (secret) => secret.key === key && secret.type === 'personal'
+ )[0]?.value,
+ comment: tempDecryptedSecrets.filter(
+ (secret) => secret.key === key && secret.type === 'shared'
+ )[0]?.comment
+ }));
+
+ setData(result);
+ return result;
+ } catch (error) {
+ console.log('Something went wrong during accessing or decripting secrets.');
+ }
+ return [];
+};
+
+export default getSecretsForProject;
diff --git a/frontend/src/components/utilities/telemetry/Telemetry.ts b/frontend/src/components/utilities/telemetry/Telemetry.ts
new file mode 100644
index 000000000..b31007f1c
--- /dev/null
+++ b/frontend/src/components/utilities/telemetry/Telemetry.ts
@@ -0,0 +1,48 @@
+/* eslint-disable */
+import { PostHog } from 'posthog-js';
+import { initPostHog } from '@app/components/analytics/posthog';
+import { ENV } from '@app/components/utilities/config';
+
+declare let TELEMETRY_CAPTURING_ENABLED: any;
+
+class Capturer {
+ api: PostHog;
+
+ constructor() {
+ this.api = initPostHog()!;
+ }
+
+ capture(item: string) {
+ if (ENV === 'production' && TELEMETRY_CAPTURING_ENABLED) {
+ try {
+ this.api.capture(item);
+ } catch (error) {
+ console.error('PostHog', error);
+ }
+ }
+ }
+
+ identify(id: string) {
+ if (ENV === 'production' && TELEMETRY_CAPTURING_ENABLED) {
+ try {
+ this.api.identify(id);
+ } catch (error) {
+ console.error('PostHog', error);
+ }
+ }
+ }
+}
+
+export default class Telemetry {
+ static instance: Capturer;
+
+ constructor() {
+ if (!Telemetry.instance) {
+ Telemetry.instance = new Capturer();
+ }
+ }
+
+ getInstance() {
+ return Telemetry.instance;
+ }
+}
diff --git a/frontend/src/components/utilities/withTranslateProps.ts b/frontend/src/components/utilities/withTranslateProps.ts
new file mode 100644
index 000000000..6dc9abd10
--- /dev/null
+++ b/frontend/src/components/utilities/withTranslateProps.ts
@@ -0,0 +1,52 @@
+import { GetServerSideProps, GetStaticProps } from "next";
+import { serverSideTranslations } from "next-i18next/serverSideTranslations";
+
+const DefaultNamespaces = ["common", "nav"];
+
+type GetTranslatedStaticProps = (
+ namespaces: string[],
+ getStaticProps?: GetStaticProps
+) => GetStaticProps;
+
+type GetTranslatedServerSideProps = (
+ namespaces: string[],
+ getStaticProps?: GetServerSideProps
+) => GetServerSideProps;
+
+export const getTranslatedStaticProps: GetTranslatedStaticProps =
+ (namespaces, getStaticProps) =>
+ async ({ locale, ...context }) => {
+ let staticProps = { props: {} };
+ if (typeof getStaticProps === "function") {
+ staticProps = (await getStaticProps(context)) as any;
+ }
+ return {
+ ...staticProps,
+ props: {
+ ...(await serverSideTranslations(locale ?? "en", [
+ ...DefaultNamespaces,
+ ...namespaces,
+ ])),
+ ...staticProps.props,
+ },
+ };
+ };
+
+export const getTranslatedServerSideProps: GetTranslatedServerSideProps =
+ (namespaces, getServerSideProps) =>
+ async ({ locale, ...context }) => {
+ let staticProps = { props: {} };
+ if (typeof getServerSideProps === "function") {
+ staticProps = (await getServerSideProps(context)) as any;
+ }
+ return {
+ ...staticProps,
+ props: {
+ ...(await serverSideTranslations(locale ?? "en", [
+ ...DefaultNamespaces,
+ ...namespaces,
+ ])),
+ ...staticProps.props,
+ },
+ };
+ };
diff --git a/frontend/src/components/v2/Button/Button.stories.tsx b/frontend/src/components/v2/Button/Button.stories.tsx
new file mode 100644
index 000000000..51e58d0da
--- /dev/null
+++ b/frontend/src/components/v2/Button/Button.stories.tsx
@@ -0,0 +1,89 @@
+import { faPlus } from '@fortawesome/free-solid-svg-icons';
+import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
+import type { Meta, StoryObj } from '@storybook/react';
+
+import { Button } from './Button';
+
+const meta: Meta = {
+ title: 'Components/Button',
+ component: Button,
+ tags: ['v2'],
+ argTypes: {
+ isRounded: {
+ defaultValue: true,
+ type: 'boolean'
+ },
+ isFullWidth: {
+ defaultValue: false,
+ type: 'boolean'
+ }
+ }
+};
+
+export default meta;
+type Story = StoryObj;
+
+// More on writing stories with args: https://storybook.js.org/docs/7.0/react/writing-stories/args
+export const Primary: Story = {
+ args: {
+ children: 'Hello Infisical'
+ }
+};
+
+export const Secondary: Story = {
+ args: {
+ children: 'Hello Infisical',
+ colorSchema: 'secondary',
+ variant: 'outline'
+ }
+};
+
+export const Danger: Story = {
+ args: {
+ children: 'Hello Infisical',
+ colorSchema: 'danger',
+ variant: 'solid'
+ }
+};
+
+export const Plain: Story = {
+ args: {
+ children: 'Hello Infisical',
+ variant: 'plain'
+ }
+};
+
+export const Disabled: Story = {
+ args: {
+ children: 'Hello Infisical',
+ disabled: true
+ }
+};
+
+export const FullWidth: Story = {
+ args: {
+ children: 'Hello Infisical',
+ isFullWidth: true
+ }
+};
+
+export const Loading: Story = {
+ args: {
+ children: 'Hello Infisical',
+ isLoading: true
+ }
+};
+
+export const LeftIcon: Story = {
+ args: {
+ children: 'Hello Infisical',
+ leftIcon:
+ }
+};
+
+export const RightIcon: Story = {
+ args: {
+ children: 'Hello Infisical',
+ rightIcon:
+ }
+};
diff --git a/frontend/src/components/v2/Button/Button.tsx b/frontend/src/components/v2/Button/Button.tsx
new file mode 100644
index 000000000..21548d0e3
--- /dev/null
+++ b/frontend/src/components/v2/Button/Button.tsx
@@ -0,0 +1,169 @@
+import { ButtonHTMLAttributes, forwardRef, ReactNode } from 'react';
+import { cva, VariantProps } from 'cva';
+import { twMerge } from 'tailwind-merge';
+
+type Props = {
+ children: ReactNode;
+ isDisabled?: boolean;
+ leftIcon?: ReactNode;
+ rightIcon?: ReactNode;
+ // loading state
+ isLoading?: boolean;
+};
+
+const buttonVariants = cva(
+ [
+ 'button',
+ 'transition-all',
+ 'font-inter font-medium',
+ 'cursor-pointer',
+ 'inline-flex items-center justify-center',
+ 'relative'
+ ],
+ {
+ variants: {
+ colorSchema: {
+ primary: ['bg-primary', 'text-black', 'border-primary hover:bg-opacity-80'],
+ secondary: ['bg-mineshaft', 'text-gray-300', 'border-mineshaft hover:bg-opacity-80'],
+ danger: ['bg-red', 'text-white', 'border-red']
+ },
+ variant: {
+ solid: '',
+ outline: ['bg-transparent', 'border-2', 'border-solid'],
+ plain: ''
+ },
+ isDisabled: {
+ true: 'bg-opacity-70 cursor-not-allowed',
+ false: ''
+ },
+ isFullWidth: {
+ true: 'w-full',
+ false: ''
+ },
+ isRounded: {
+ true: 'rounded-md',
+ false: ''
+ },
+ size: {
+ xs: ['text-xs', 'py-1', 'px-2'],
+ sm: ['text-sm', 'py-2', 'px-4'],
+ md: ['text-md', 'py-2', 'px-6'],
+ lg: ['text-lg', 'py-2', 'px-8']
+ }
+ },
+ compoundVariants: [
+ {
+ colorSchema: 'primary',
+ variant: 'outline',
+ className: 'text-primary hover:bg-primary hover:text-black'
+ },
+ {
+ colorSchema: 'secondary',
+ variant: 'outline',
+ className: 'hover:bg-mineshaft'
+ },
+ {
+ colorSchema: 'danger',
+ variant: 'outline',
+ className: 'text-red hover:bg-red hover:text-black'
+ },
+ {
+ colorSchema: 'primary',
+ variant: 'plain',
+ className: 'text-primary'
+ },
+ {
+ colorSchema: 'secondary',
+ variant: 'plain',
+ className: 'text-mineshaft'
+ },
+ {
+ colorSchema: 'danger',
+ variant: 'plain',
+ className: 'text-red'
+ },
+ {
+ colorSchema: ['danger', 'primary', 'secondary'],
+ variant: ['plain'],
+ className: 'bg-transparent py-1 px-1'
+ }
+ ]
+ }
+);
+
+export type ButtonProps = ButtonHTMLAttributes &
+ VariantProps &
+ Props;
+
+export const Button = forwardRef(
+ (
+ {
+ children,
+ isDisabled = false,
+ className = '',
+ size = 'md',
+ variant = 'solid',
+ isFullWidth,
+ isRounded = true,
+ leftIcon,
+ rightIcon,
+ isLoading,
+ colorSchema = 'primary',
+ ...props
+ },
+ ref
+ ): JSX.Element => {
+ const loadingToggleClass = isLoading ? 'opacity-0' : 'opacity-100';
+
+ return (
+
+ {isLoading && (
+
+ )}
+
+ {leftIcon}
+
+ {children}
+
+ {rightIcon}
+
+
+ );
+ }
+);
+
+Button.displayName = 'Button';
diff --git a/frontend/src/components/v2/Button/index.tsx b/frontend/src/components/v2/Button/index.tsx
new file mode 100644
index 000000000..9cad84da1
--- /dev/null
+++ b/frontend/src/components/v2/Button/index.tsx
@@ -0,0 +1,2 @@
+export type { ButtonProps } from './Button';
+export { Button } from './Button';
diff --git a/frontend/src/components/v2/Card/Card.stories.tsx b/frontend/src/components/v2/Card/Card.stories.tsx
new file mode 100644
index 000000000..e21625cb4
--- /dev/null
+++ b/frontend/src/components/v2/Card/Card.stories.tsx
@@ -0,0 +1,40 @@
+import type { Meta, StoryObj } from '@storybook/react';
+
+import { Card, CardBody, CardFooter, CardProps, CardTitle } from './Card';
+
+const meta: Meta = {
+ title: 'Components/Card',
+ component: Card,
+ tags: ['v2'],
+ argTypes: {
+ isRounded: {
+ type: 'boolean',
+ defaultValue: true
+ }
+ }
+};
+
+export default meta;
+type Story = StoryObj;
+
+// More on writing stories with args: https://storybook.js.org/docs/7.0/react/writing-stories/args
+const Template = (args: CardProps) => (
+
+
+ Title
+ Content
+ Footer
+
+
+);
+
+export const Basic: Story = {
+ render: (args) =>
+};
+
+export const Hoverable: Story = {
+ render: (args) => ,
+ args: {
+ isHoverable: true
+ }
+};
diff --git a/frontend/src/components/v2/Card/Card.tsx b/frontend/src/components/v2/Card/Card.tsx
new file mode 100644
index 000000000..9e658aaf7
--- /dev/null
+++ b/frontend/src/components/v2/Card/Card.tsx
@@ -0,0 +1,64 @@
+import { forwardRef, ReactNode } from 'react';
+import { twMerge } from 'tailwind-merge';
+
+export type CardTitleProps = {
+ children: ReactNode;
+ subTitle?: string;
+ className?: string;
+};
+
+export const CardTitle = ({ children, className, subTitle }: CardTitleProps) => (
+
+ {children}
+ {subTitle &&
{subTitle}
}
+
+);
+
+export type CardFooterProps = {
+ children: ReactNode;
+ className?: string;
+};
+
+export const CardFooter = ({ children, className }: CardFooterProps) => (
+ {children}
+);
+
+export type CardBodyProps = {
+ children: ReactNode;
+ className?: string;
+};
+
+export const CardBody = ({ children, className }: CardBodyProps) => (
+ {children}
+);
+
+export type CardProps = {
+ children: ReactNode;
+ className?: string;
+ isFullHeight?: boolean;
+ isRounded?: boolean;
+ isPlain?: boolean;
+ isHoverable?: boolean;
+};
+
+export const Card = forwardRef(
+ ({ children, isFullHeight, isRounded, isHoverable, isPlain, className }, ref): JSX.Element => {
+ return (
+
+ {children}
+
+ );
+ }
+);
+
+Card.displayName = 'Card';
diff --git a/frontend/src/components/v2/Card/index.tsx b/frontend/src/components/v2/Card/index.tsx
new file mode 100644
index 000000000..35276dda6
--- /dev/null
+++ b/frontend/src/components/v2/Card/index.tsx
@@ -0,0 +1,2 @@
+export type { CardBodyProps, CardFooterProps, CardProps, CardTitleProps } from './Card';
+export { Card, CardBody, CardFooter, CardTitle } from './Card';
diff --git a/frontend/src/components/v2/Checkbox/Checkbox.stories.tsx b/frontend/src/components/v2/Checkbox/Checkbox.stories.tsx
new file mode 100644
index 000000000..0dfe1b4a1
--- /dev/null
+++ b/frontend/src/components/v2/Checkbox/Checkbox.stories.tsx
@@ -0,0 +1,34 @@
+import type { Meta, StoryObj } from '@storybook/react';
+
+import { Checkbox } from './Checkbox';
+
+const meta: Meta = {
+ title: 'Components/Checkbox',
+ component: Checkbox,
+ tags: ['v2'],
+ argTypes: {}
+};
+
+export default meta;
+type Story = StoryObj;
+
+// More on writing stories with args: https://storybook.js.org/docs/7.0/react/writing-stories/args
+export const Simple: Story = {
+ args: {
+ children: 'Accept the condition'
+ }
+};
+
+export const Disabled: Story = {
+ args: {
+ children: 'Accept the condition',
+ isDisabled: true
+ }
+};
+
+export const Required: Story = {
+ args: {
+ children: 'Accept the condition',
+ isRequired: true
+ }
+};
diff --git a/frontend/src/components/v2/Checkbox/Checkbox.tsx b/frontend/src/components/v2/Checkbox/Checkbox.tsx
new file mode 100644
index 000000000..5b30fb458
--- /dev/null
+++ b/frontend/src/components/v2/Checkbox/Checkbox.tsx
@@ -0,0 +1,51 @@
+import { ReactNode } from 'react';
+import { faCheck } from '@fortawesome/free-solid-svg-icons';
+import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
+import * as CheckboxPrimitive from '@radix-ui/react-checkbox';
+import { twMerge } from 'tailwind-merge';
+
+export type CheckboxProps = Omit<
+ CheckboxPrimitive.CheckboxProps,
+ 'checked' | 'disabled' | 'required'
+> & {
+ children: ReactNode;
+ id: string;
+ isDisabled?: boolean;
+ isChecked?: boolean;
+ isRequired?: boolean;
+};
+
+export const Checkbox = ({
+ children,
+ className,
+ id,
+ isChecked,
+ isDisabled,
+ isRequired,
+ ...props
+}: CheckboxProps): JSX.Element => {
+ return (
+
+
+
+
+
+
+
+ {children}
+ {isRequired && * }
+
+
+ );
+};
diff --git a/frontend/src/components/v2/Checkbox/index.tsx b/frontend/src/components/v2/Checkbox/index.tsx
new file mode 100644
index 000000000..fcd016662
--- /dev/null
+++ b/frontend/src/components/v2/Checkbox/index.tsx
@@ -0,0 +1,2 @@
+export type { CheckboxProps } from './Checkbox';
+export { Checkbox } from './Checkbox';
diff --git a/frontend/src/components/v2/Dropdown/Dropdown.stories.tsx b/frontend/src/components/v2/Dropdown/Dropdown.stories.tsx
new file mode 100644
index 000000000..35e7fd664
--- /dev/null
+++ b/frontend/src/components/v2/Dropdown/Dropdown.stories.tsx
@@ -0,0 +1,111 @@
+import { faPlus, faTrash, faUndo } from '@fortawesome/free-solid-svg-icons';
+import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
+import type { Meta, StoryObj } from '@storybook/react';
+
+import { IconButton } from '../IconButton';
+import {
+ DropdownMenu,
+ DropdownMenuContent,
+ DropdownMenuGroup,
+ DropdownMenuItem,
+ DropdownMenuLabel,
+ DropdownMenuSeparator,
+ DropdownMenuTrigger
+} from './Dropdown';
+
+const meta: Meta = {
+ title: 'Components/DropdownMenu',
+ component: DropdownMenu,
+ tags: ['v2'],
+ argTypes: {}
+};
+
+export default meta;
+type Story = StoryObj;
+
+export const Basic: Story = {
+ render: (args) => (
+
+
+
+
+
+
+
+
+ Delete
+ Undo
+
+
+
+ )
+};
+
+export const Icons: Story = {
+ render: (args) => (
+
+
+
+
+
+
+
+
+ }>
+ Delete
+
+ }>
+ Undo
+
+
+
+
+ )
+};
+
+export const WithDivider: Story = {
+ render: (args) => (
+
+
+
+
+
+
+
+
+ Delete
+
+ Undo
+
+ Redo
+
+
+
+ )
+};
+
+export const Group: Story = {
+ render: (args) => (
+
+
+
+
+
+
+
+
+
+ Group
+ Undo
+ Delete
+
+
+ Group#2
+ Undo
+ Delete
+
+
+
+
+ )
+};
diff --git a/frontend/src/components/v2/Dropdown/Dropdown.tsx b/frontend/src/components/v2/Dropdown/Dropdown.tsx
new file mode 100644
index 000000000..57a206493
--- /dev/null
+++ b/frontend/src/components/v2/Dropdown/Dropdown.tsx
@@ -0,0 +1,97 @@
+import { ComponentPropsWithRef, ElementType, forwardRef, ReactNode, Ref } from 'react';
+import * as DropdownMenuPrimitive from '@radix-ui/react-dropdown-menu';
+import { twMerge } from 'tailwind-merge';
+
+// Main menu or parent container
+export type DropdownMenuProps = DropdownMenuPrimitive.DropdownMenuProps;
+export const DropdownMenu = DropdownMenuPrimitive.Root;
+
+// trigger
+export type DropdownMenuTriggerProps = DropdownMenuPrimitive.DropdownMenuTriggerProps;
+export const DropdownMenuTrigger = DropdownMenuPrimitive.Trigger;
+
+// item container
+export type DropdownMenuContentProps = DropdownMenuPrimitive.MenuContentProps;
+export const DropdownMenuContent = forwardRef(
+ ({ children, className, ...props }, forwardedRef) => {
+ return (
+
+
+ {children}
+
+
+ );
+ }
+);
+
+DropdownMenuContent.displayName = 'DropdownMenuContent';
+
+// item label component
+export type DropdownLabelProps = DropdownMenuPrimitive.MenuLabelProps;
+export const DropdownMenuLabel = ({ className, ...props }: DropdownLabelProps) => (
+
+);
+
+// dropdown items
+export type DropdownMenuItemProps =
+ DropdownMenuPrimitive.MenuContentProps & {
+ icon?: ReactNode;
+ as?: T;
+ inputRef?: Ref;
+ };
+
+export const DropdownMenuItem = ({
+ children,
+ inputRef,
+ className,
+ icon,
+ as: Item = 'button',
+ ...props
+}: DropdownMenuItemProps & ComponentPropsWithRef) => (
+
+ -
+ {icon &&
{icon} }
+ {children}
+
+
+);
+
+// grouping items into 1
+export type DropdownMenuGroupProps = DropdownMenuPrimitive.DropdownMenuGroupProps;
+
+export const DropdownMenuGroup = forwardRef(
+ ({ ...props }, ref) =>
+);
+
+DropdownMenuGroup.displayName = 'DropdownMenuGroup';
+
+// Divider
+export const DropdownMenuSeparator = forwardRef<
+ HTMLDivElement,
+ DropdownMenuPrimitive.DropdownMenuSeparatorProps
+>(({ className, ...props }, ref) => (
+
+));
+
+DropdownMenuSeparator.displayName = 'DropdownMenuSeperator';
diff --git a/frontend/src/components/v2/Dropdown/index.tsx b/frontend/src/components/v2/Dropdown/index.tsx
new file mode 100644
index 000000000..15a77a417
--- /dev/null
+++ b/frontend/src/components/v2/Dropdown/index.tsx
@@ -0,0 +1,17 @@
+export type {
+ DropdownLabelProps,
+ DropdownMenuContentProps,
+ DropdownMenuGroupProps,
+ DropdownMenuItemProps,
+ DropdownMenuProps,
+ DropdownMenuTriggerProps
+} from './Dropdown';
+export {
+ DropdownMenu,
+ DropdownMenuContent,
+ DropdownMenuGroup,
+ DropdownMenuItem,
+ DropdownMenuLabel,
+ DropdownMenuSeparator,
+ DropdownMenuTrigger
+} from './Dropdown';
diff --git a/frontend/src/components/v2/FormControl/FormControl.stories.tsx b/frontend/src/components/v2/FormControl/FormControl.stories.tsx
new file mode 100644
index 000000000..2e3692228
--- /dev/null
+++ b/frontend/src/components/v2/FormControl/FormControl.stories.tsx
@@ -0,0 +1,40 @@
+import type { Meta, StoryObj } from '@storybook/react';
+
+// Be careful on dep cycle
+import { Input } from '../Input/Input';
+import { FormControl } from './FormControl';
+
+const meta: Meta = {
+ title: 'Components/FormControl',
+ component: FormControl,
+ tags: ['v2'],
+ argTypes: {}
+};
+
+export default meta;
+type Story = StoryObj;
+
+// More on writing stories with args: https://storybook.js.org/docs/7.0/react/writing-stories/args
+export const Basic: Story = {
+ args: {
+ children: ,
+ label: 'Email',
+ id: 'email',
+ helperText: 'Type something..'
+ }
+};
+
+export const RequiredInput: Story = {
+ args: {
+ ...Basic.args,
+ isRequired: true
+ }
+};
+
+export const ErrorInput: Story = {
+ args: {
+ ...Basic.args,
+ errorText: 'Some random error',
+ isError: true
+ }
+};
diff --git a/frontend/src/components/v2/FormControl/FormControl.tsx b/frontend/src/components/v2/FormControl/FormControl.tsx
new file mode 100644
index 000000000..14e58d5be
--- /dev/null
+++ b/frontend/src/components/v2/FormControl/FormControl.tsx
@@ -0,0 +1,72 @@
+import { cloneElement, ReactNode } from 'react';
+import { faExclamationTriangle } from '@fortawesome/free-solid-svg-icons';
+import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
+import * as Label from '@radix-ui/react-label';
+import { twMerge } from 'tailwind-merge';
+
+export type FormLabelProps = {
+ id?: string;
+ isRequired?: boolean;
+ label?: ReactNode;
+};
+
+export const FormLabel = ({ id, label, isRequired }: FormLabelProps) => (
+
+ {label}
+ {isRequired && * }
+
+);
+
+export type FormHelperTextProps = {
+ isError?: boolean;
+ text?: ReactNode;
+};
+
+export const FormHelperText = ({ isError, text }: FormHelperTextProps) => (
+
+ {isError && (
+
+
+
+ )}
+ {text}
+
+);
+
+export type FormControlProps = {
+ id?: string;
+ isRequired?: boolean;
+ isError?: boolean;
+ label?: ReactNode;
+ helperText?: ReactNode;
+ errorText?: ReactNode;
+ children: JSX.Element;
+};
+
+export const FormControl = ({
+ children,
+ isRequired,
+ label,
+ helperText,
+ errorText,
+ id,
+ isError
+}: FormControlProps): JSX.Element => {
+ return (
+
+ {typeof label === 'string' ? (
+
+ ) : (
+ label
+ )}
+ {cloneElement(children, { isRequired, 'data-required': isRequired, isError })}
+ {!isError && helperText && }
+ {isError && errorText && }
+
+ );
+};
diff --git a/frontend/src/components/v2/FormControl/index.tsx b/frontend/src/components/v2/FormControl/index.tsx
new file mode 100644
index 000000000..806103759
--- /dev/null
+++ b/frontend/src/components/v2/FormControl/index.tsx
@@ -0,0 +1,2 @@
+export type { FormControlProps, FormHelperTextProps, FormLabelProps } from './FormControl';
+export { FormControl, FormHelperText, FormLabel } from './FormControl';
diff --git a/frontend/src/components/v2/IconButton/IconButton.stories.tsx b/frontend/src/components/v2/IconButton/IconButton.stories.tsx
new file mode 100644
index 000000000..d9abf35ec
--- /dev/null
+++ b/frontend/src/components/v2/IconButton/IconButton.stories.tsx
@@ -0,0 +1,60 @@
+import { faPlus } from '@fortawesome/free-solid-svg-icons';
+import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
+import type { Meta, StoryObj } from '@storybook/react';
+
+import { IconButton } from './IconButton';
+
+const meta: Meta = {
+ title: 'Components/IconButton',
+ component: IconButton,
+ tags: ['v2'],
+ argTypes: {
+ isRounded: {
+ defaultValue: true,
+ type: 'boolean'
+ },
+ ariaLabel: {
+ defaultValue: 'Some buttons...'
+ }
+ }
+};
+
+export default meta;
+type Story = StoryObj;
+
+// More on writing stories with args: https://storybook.js.org/docs/7.0/react/writing-stories/args
+export const Primary: Story = {
+ args: {
+ children:
+ }
+};
+
+export const Secondary: Story = {
+ args: {
+ children: ,
+ colorSchema: 'secondary',
+ variant: 'outline'
+ }
+};
+
+export const Danger: Story = {
+ args: {
+ children: ,
+ colorSchema: 'danger',
+ variant: 'solid'
+ }
+};
+
+export const Plain: Story = {
+ args: {
+ children: ,
+ variant: 'plain'
+ }
+};
+
+export const Disabled: Story = {
+ args: {
+ children: ,
+ disabled: true
+ }
+};
diff --git a/frontend/src/components/v2/IconButton/IconButton.tsx b/frontend/src/components/v2/IconButton/IconButton.tsx
new file mode 100644
index 000000000..b1a1f4813
--- /dev/null
+++ b/frontend/src/components/v2/IconButton/IconButton.tsx
@@ -0,0 +1,131 @@
+import { ButtonHTMLAttributes, forwardRef, ReactNode } from 'react';
+import { cva, VariantProps } from 'cva';
+import { twMerge } from 'tailwind-merge';
+
+type Props = {
+ children: ReactNode;
+ // This is kept as required because by accessibility convention and eslint
+ // when button doesn't have text an aria-label needs to be passed
+ ariaLabel: string;
+ isDisabled?: boolean;
+};
+
+const iconButtonVariants = cva(
+ [
+ 'button',
+ 'transition-all',
+ 'font-inter font-medium',
+ 'cursor-pointer',
+ 'inline-flex items-center justify-center',
+ 'relative'
+ ],
+ {
+ variants: {
+ colorSchema: {
+ primary: ['bg-primary', 'text-black', 'border-primary hover:bg-opacity-80'],
+ secondary: ['bg-mineshaft', 'text-gray-300', 'border-mineshaft hover:bg-opacity-80'],
+ danger: ['bg-red', 'text-white', 'border-red']
+ },
+ variant: {
+ solid: '',
+ outline: ['bg-transparent', 'border-2', 'border-solid'],
+ plain: ''
+ },
+ isDisabled: {
+ true: 'bg-opacity-70 cursor-not-allowed',
+ false: ''
+ },
+ isRounded: {
+ true: 'rounded-md',
+ false: ''
+ },
+ size: {
+ xs: ['text-xs', 'py-1', 'px-2'],
+ sm: ['text-sm', 'py-2', 'px-3'],
+ md: ['text-md', 'py-3', 'px-4'],
+ lg: ['text-lg', 'py-3', 'px-6']
+ }
+ },
+ compoundVariants: [
+ {
+ colorSchema: 'primary',
+ variant: 'outline',
+ className: 'text-primary hover:bg-primary hover:text-black'
+ },
+ {
+ colorSchema: 'secondary',
+ variant: 'outline',
+ className: 'hover:bg-mineshaft'
+ },
+ {
+ colorSchema: 'danger',
+ variant: 'outline',
+ className: 'text-red hover:bg-red hover:text-black'
+ },
+ {
+ colorSchema: 'primary',
+ variant: 'plain',
+ className: 'text-primary'
+ },
+ {
+ colorSchema: 'secondary',
+ variant: 'plain',
+ className: 'text-mineshaft'
+ },
+ {
+ colorSchema: 'danger',
+ variant: 'plain',
+ className: 'text-red'
+ },
+ {
+ colorSchema: ['danger', 'primary', 'secondary'],
+ variant: ['plain'],
+ className: 'bg-transparent py-1 px-1'
+ }
+ ]
+ }
+);
+
+export type IconButtonProps = Omit, 'aria-label'> &
+ VariantProps &
+ Props;
+
+export const IconButton = forwardRef(
+ (
+ {
+ children,
+ ariaLabel,
+ isDisabled = false,
+ className,
+ size = 'md',
+ variant = 'solid',
+ isRounded = true,
+ colorSchema = 'primary',
+ ...props
+ },
+ ref
+ ): JSX.Element => (
+
+ {children}
+
+ )
+);
+
+IconButton.displayName = 'IconButton';
diff --git a/frontend/src/components/v2/IconButton/index.tsx b/frontend/src/components/v2/IconButton/index.tsx
new file mode 100644
index 000000000..e920c3d6a
--- /dev/null
+++ b/frontend/src/components/v2/IconButton/index.tsx
@@ -0,0 +1,2 @@
+export type { IconButtonProps } from './IconButton';
+export { IconButton } from './IconButton';
diff --git a/frontend/src/components/v2/Input/Input.stories.tsx b/frontend/src/components/v2/Input/Input.stories.tsx
new file mode 100644
index 000000000..288805b21
--- /dev/null
+++ b/frontend/src/components/v2/Input/Input.stories.tsx
@@ -0,0 +1,66 @@
+import { faEye, faMailBulk } from '@fortawesome/free-solid-svg-icons';
+import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
+import type { Meta, StoryObj } from '@storybook/react';
+
+import { Input } from './Input';
+
+const meta: Meta = {
+ title: 'Components/Input',
+ component: Input,
+ tags: ['v2'],
+ argTypes: {
+ isRounded: {
+ defaultValue: true,
+ type: 'boolean'
+ },
+ placeholder: {
+ defaultValue: 'Type anything',
+ type: 'string'
+ }
+ }
+};
+
+export default meta;
+type Story = StoryObj;
+
+// More on writing stories with args: https://storybook.js.org/docs/7.0/react/writing-stories/args
+export const Filled: Story = {
+ args: {}
+};
+
+export const Outline: Story = {
+ args: {
+ variant: 'outline'
+ }
+};
+
+export const Plain: Story = {
+ args: {
+ variant: 'plain'
+ }
+};
+
+export const Error: Story = {
+ args: {
+ isError: true
+ }
+};
+
+export const AutoWidth: Story = {
+ args: {
+ isFullWidth: false,
+ className: 'w-auto'
+ }
+};
+
+export const RightIcon: Story = {
+ args: {
+ rightIcon:
+ }
+};
+
+export const LeftIcon: Story = {
+ args: {
+ leftIcon:
+ }
+};
diff --git a/frontend/src/components/v2/Input/Input.tsx b/frontend/src/components/v2/Input/Input.tsx
new file mode 100644
index 000000000..4f502e31d
--- /dev/null
+++ b/frontend/src/components/v2/Input/Input.tsx
@@ -0,0 +1,102 @@
+import { forwardRef, InputHTMLAttributes, ReactNode } from 'react';
+import { cva, VariantProps } from 'cva';
+import { twMerge } from 'tailwind-merge';
+
+type Props = {
+ placeholder?: string;
+ isFullWidth?: boolean;
+ isRequired?: boolean;
+ leftIcon?: ReactNode;
+ rightIcon?: ReactNode;
+};
+
+const inputVariants = cva(
+ 'input w-full py-2 text-gray-400 placeholder-gray-500 placeholder-opacity-50',
+ {
+ variants: {
+ size: {
+ xs: ['text-xs'],
+ sm: ['text-sm'],
+ md: ['text-md'],
+ lg: ['text-lg']
+ },
+ isRounded: {
+ true: ['rounded-md'],
+ false: ''
+ },
+ variant: {
+ filled: ['bg-bunker-800', 'text-gray-400'],
+ outline: ['bg-transparent'],
+ plain: 'bg-transparent outline-none'
+ },
+ isError: {
+ true: 'focus:ring-red/50 placeholder-red-300',
+ false: 'focus:ring-primary/50'
+ }
+ },
+ compoundVariants: []
+ }
+);
+
+const inputParentContainerVariants = cva('inline-flex font-inter items-center border relative', {
+ variants: {
+ isRounded: {
+ true: ['rounded-md'],
+ false: ''
+ },
+ isError: {
+ true: 'border-red',
+ false: 'border-mineshaft-400'
+ },
+ isFullWidth: {
+ true: 'w-full',
+ false: ''
+ },
+ variant: {
+ filled: ['bg-bunker-800', 'text-gray-400'],
+ outline: ['bg-transparent'],
+ plain: 'border-none'
+ }
+ }
+});
+
+export type InputProps = Omit, 'size'> &
+ VariantProps