mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-10-11 01:29:26 +00:00
Merge branch 'main' into fix/typo
This commit is contained in:
@@ -6,7 +6,7 @@ import { ENV, POSTHOG_API_KEY, POSTHOG_HOST } from '../utilities/config';
|
||||
|
||||
export const initPostHog = () => {
|
||||
// @ts-ignore
|
||||
console.log("Init Infisical")
|
||||
console.log("Hi there 👋")
|
||||
try {
|
||||
if (typeof window !== 'undefined') {
|
||||
// @ts-ignore
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import React, { Fragment } from 'react';
|
||||
import { useTranslation } from 'next-i18next';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import {
|
||||
faAngleDown,
|
||||
faEye,
|
||||
@@ -45,16 +45,16 @@ const EventFilter = ({ selected, select }: ListBoxProps): JSX.Element => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<Listbox value={t(`activity:event.${selected}`)} onChange={select}>
|
||||
<Listbox value={t(`activity.event.${selected}`)} onChange={select}>
|
||||
<div className="relative">
|
||||
<Listbox.Button className="bg-mineshaft-800 hover:bg-mineshaft-700 duration-200 cursor-pointer rounded-md h-10 flex items-center justify-between pl-4 pr-2 w-52 text-bunker-200 text-sm">
|
||||
<Listbox.Button className="flex h-10 w-52 cursor-pointer items-center justify-between rounded-md bg-mineshaft-800 pl-4 pr-2 text-sm text-bunker-200 duration-200 hover:bg-mineshaft-700">
|
||||
{selected !== '' ? (
|
||||
<p className="select-none text-bunker-100">{t(`activity:event.${selected}`)}</p>
|
||||
<p className="select-none text-bunker-100">{t(`activity.event.${selected}`)}</p>
|
||||
) : (
|
||||
<p className="select-none">{String(t('common:select-event'))}</p>
|
||||
<p className="select-none">{String(t('common.select-event'))}</p>
|
||||
)}
|
||||
{selected !== '' ? (
|
||||
<FontAwesomeIcon icon={faX} className="pl-2 w-2 p-2" onClick={() => select('')} />
|
||||
<FontAwesomeIcon icon={faX} className="w-2 p-2 pl-2" onClick={() => select('')} />
|
||||
) : (
|
||||
<FontAwesomeIcon icon={faAngleDown} className="pl-4 pr-2" />
|
||||
)}
|
||||
@@ -65,12 +65,12 @@ const EventFilter = ({ selected, select }: ListBoxProps): JSX.Element => {
|
||||
leaveFrom="opacity-100"
|
||||
leaveTo="opacity-0"
|
||||
>
|
||||
<Listbox.Options className="border border-mineshaft-700 z-50 w-52 p-1 absolute mt-1 max-h-60 overflow-auto rounded-md bg-bunker text-base shadow-lg ring-1 ring-black ring-opacity-5 focus:outline-none sm:text-sm">
|
||||
<Listbox.Options className="absolute z-50 mt-1 max-h-60 w-52 overflow-auto rounded-md border border-mineshaft-700 bg-bunker p-1 text-base shadow-lg ring-1 ring-black ring-opacity-5 focus:outline-none sm:text-sm">
|
||||
{eventOptions.map((event, id) => (
|
||||
<Listbox.Option
|
||||
key={`${event.name}.${id + 1}`}
|
||||
className={`px-4 h-10 flex items-center text-sm cursor-pointer hover:bg-mineshaft-700 text-bunker-200 rounded-md ${
|
||||
selected === t(`activity:event.${event.name}`) && 'bg-mineshaft-700'
|
||||
className={`flex h-10 cursor-pointer items-center rounded-md px-4 text-sm text-bunker-200 hover:bg-mineshaft-700 ${
|
||||
selected === t(`activity.event.${event.name}`) && 'bg-mineshaft-700'
|
||||
}`}
|
||||
value={event.name}
|
||||
>
|
||||
@@ -79,7 +79,7 @@ const EventFilter = ({ selected, select }: ListBoxProps): JSX.Element => {
|
||||
className={`block truncate ${isSelected ? 'font-semibold' : 'font-normal'}`}
|
||||
>
|
||||
<FontAwesomeIcon icon={event.icon} className="pr-4" />{' '}
|
||||
{t(`activity:event.${event.name}`)}
|
||||
{t(`activity.event.${event.name}`)}
|
||||
</span>
|
||||
)}
|
||||
</Listbox.Option>
|
||||
|
||||
@@ -61,22 +61,6 @@ const InputField = ({
|
||||
<div className="flex-col w-full">
|
||||
<div className="flex flex-row text-mineshaft-300 items-center mb-0.5">
|
||||
<p className="text-sm font-semibold mr-1">{label}</p>
|
||||
{/* {label === "Password" && router.asPath !== "/login" && (
|
||||
<div className="mb-0.5 relative inline-block text-gray-400 underline hover:text-primary duration-200">
|
||||
<FontAwesomeIcon
|
||||
icon={faCircleExclamation}
|
||||
className={`text-sm peer ${
|
||||
error && "text-red"
|
||||
}`}
|
||||
/>
|
||||
<span className="absolute hidden peer-hover:block duration-200 w-60 -left-28 -top-2 -translate-y-full px-2 py-2 bg-gray-700 rounded-md text-center text-gray-200 text-sm after:content-[''] after:absolute after:left-1/2 after:top-[100%] after:-translate-x-1/2 after:border-8 after:border-x-transparent after:border-b-transparent after:border-t-gray-700">
|
||||
The password should contain at least 8
|
||||
characters including at least 1 lowercase
|
||||
character, uppercase character, number, and a
|
||||
special character.
|
||||
</span>
|
||||
</div>
|
||||
)} */}
|
||||
</div>
|
||||
<div
|
||||
className={`group relative flex flex-col justify-center w-full max-w-2xl border ${
|
||||
@@ -95,7 +79,7 @@ const InputField = ({
|
||||
: ''
|
||||
} ${
|
||||
error ? 'focus:ring-red/50' : 'focus:ring-primary/50'
|
||||
} relative peer bg-bunker-800 rounded-md text-gray-400 text-md p-2 w-full min-w-16 outline-none focus:ring-4 duration-200`}
|
||||
} relative peer bg-mineshaft-900 rounded-md text-gray-400 text-md p-2 w-full min-w-16 outline-none focus:ring-4 duration-200`}
|
||||
name={name}
|
||||
spellCheck="false"
|
||||
autoComplete={autoComplete}
|
||||
|
||||
@@ -4,9 +4,9 @@
|
||||
import crypto from 'crypto';
|
||||
|
||||
import { useEffect, useMemo, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import Link from 'next/link';
|
||||
import { useRouter } from 'next/router';
|
||||
import { useTranslation } from 'next-i18next';
|
||||
import {
|
||||
faBookOpen,
|
||||
faFileLines,
|
||||
@@ -147,17 +147,17 @@ const Layout = ({ children }: LayoutProps) => {
|
||||
() => [
|
||||
{
|
||||
href: `/dashboard/${workspaceMapping[workspaceSelected as any]}`,
|
||||
title: t('nav:menu.secrets'),
|
||||
title: t('nav.menu.secrets'),
|
||||
emoji: <FontAwesomeIcon icon={faKey} />
|
||||
},
|
||||
{
|
||||
href: `/users/${workspaceMapping[workspaceSelected as any]}`,
|
||||
title: t('nav:menu.members'),
|
||||
title: t('nav.menu.members'),
|
||||
emoji: <FontAwesomeIcon icon={faUser} />
|
||||
},
|
||||
{
|
||||
href: `/integrations/${workspaceMapping[workspaceSelected as any]}`,
|
||||
title: t('nav:menu.integrations'),
|
||||
title: t('nav.menu.integrations'),
|
||||
emoji: <FontAwesomeIcon icon={faPlug} />
|
||||
},
|
||||
{
|
||||
@@ -167,7 +167,7 @@ const Layout = ({ children }: LayoutProps) => {
|
||||
},
|
||||
{
|
||||
href: `/settings/project/${workspaceMapping[workspaceSelected as any]}`,
|
||||
title: t('nav:menu.project-settings'),
|
||||
title: t('nav.menu.project-settings'),
|
||||
emoji: <FontAwesomeIcon icon={faGear} />
|
||||
}
|
||||
],
|
||||
@@ -176,6 +176,7 @@ const Layout = ({ children }: LayoutProps) => {
|
||||
|
||||
useEffect(() => {
|
||||
// Put a user in a workspace if they're not in one yet
|
||||
|
||||
const putUserInWorkSpace = async () => {
|
||||
if (tempLocalStorage('orgData.id') === '') {
|
||||
const userOrgs = await getOrganizations();
|
||||
@@ -255,7 +256,7 @@ const Layout = ({ children }: LayoutProps) => {
|
||||
<div>
|
||||
<div className="mt-6 mb-6 flex h-20 w-full flex-col items-center justify-center bg-bunker-600 px-4">
|
||||
<div className="ml-1 mb-1 self-start text-xs font-semibold tracking-wide text-gray-400">
|
||||
{t('nav:menu.project')}
|
||||
{t('nav.menu.project')}
|
||||
</div>
|
||||
{Object.keys(workspaceMapping).length > 0 ? (
|
||||
<Listbox
|
||||
@@ -374,7 +375,7 @@ const Layout = ({ children }: LayoutProps) => {
|
||||
<div className="flex h-screen w-screen flex-col items-center justify-center bg-bunker-800 z-[200] md:hidden">
|
||||
<FontAwesomeIcon icon={faMobile} className="mb-8 text-7xl text-gray-300" />
|
||||
<p className="max-w-sm px-6 text-center text-lg text-gray-200">
|
||||
{` ${t('common:no-mobile')} `}
|
||||
{` ${t('common.no-mobile')} `}
|
||||
</p>
|
||||
</div>
|
||||
</>
|
||||
|
||||
@@ -53,11 +53,11 @@ const Button = ({
|
||||
'group m-auto md:m-0 inline-block rounded-md duration-200',
|
||||
|
||||
// Setting background colors and hover modes
|
||||
color === 'mineshaft' && activityStatus && 'bg-mineshaft-700 hover:bg-primary',
|
||||
color === 'mineshaft' && activityStatus && 'bg-mineshaft-800 border border-mineshaft-600 hover:bg-primary/[0.15] hover:border-primary/60',
|
||||
color === 'mineshaft' && !activityStatus && 'bg-mineshaft',
|
||||
(color === 'primary' || !color) && activityStatus && 'bg-primary hover:opacity-80',
|
||||
(color === 'primary' || !color) && activityStatus && 'bg-primary border border-primary-400 opacity-80 hover:opacity-100',
|
||||
(color === 'primary' || !color) && !activityStatus && 'bg-primary',
|
||||
color === 'red' && 'bg-red',
|
||||
color === 'red' && 'bg-red-800 border border-red',
|
||||
|
||||
// Changing the opacity when active vs when not
|
||||
activityStatus ? 'opacity-100 cursor-pointer' : 'opacity-40',
|
||||
@@ -74,11 +74,11 @@ const Button = ({
|
||||
'relative font-medium flex items-center',
|
||||
|
||||
// Setting the text color for the text and icon
|
||||
color === 'mineshaft' && 'text-gray-400',
|
||||
color === 'mineshaft' && 'text-bunker-200',
|
||||
color !== 'mineshaft' && color !== 'red' && color !== 'none' && 'text-black',
|
||||
color === 'red' && 'text-gray-200',
|
||||
color === 'none' && 'text-gray-200 text-xl',
|
||||
activityStatus && color !== 'red' && color !== 'none' ? 'group-hover:text-black' : '',
|
||||
activityStatus && color !== 'red' && color !== 'mineshaft' && color !== 'none' ? 'group-hover:text-black' : '',
|
||||
|
||||
size === 'icon' && 'flex items-center justify-center'
|
||||
);
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Fragment } from 'react';
|
||||
import { useTranslation } from 'next-i18next';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Dialog, Transition } from '@headlessui/react';
|
||||
|
||||
import Button from '../buttons/Button';
|
||||
@@ -34,10 +34,9 @@ const ActivateBotDialog = ({
|
||||
try {
|
||||
// type check
|
||||
if (!selectedIntegrationOption) return;
|
||||
|
||||
|
||||
// start integration or probe for PAT
|
||||
integrationOptionPress(selectedIntegrationOption);
|
||||
|
||||
} catch (err) {
|
||||
console.log(err);
|
||||
}
|
||||
@@ -71,20 +70,20 @@ const ActivateBotDialog = ({
|
||||
leaveFrom="opacity-100 scale-100"
|
||||
leaveTo="opacity-0 scale-95"
|
||||
>
|
||||
<Dialog.Panel className="w-full max-w-md transform overflow-hidden rounded-md bg-bunker-800 border border-gray-700 p-6 text-left align-middle shadow-xl transition-all">
|
||||
<Dialog.Panel className="w-full max-w-md transform overflow-hidden rounded-md border border-gray-700 bg-bunker-800 p-6 text-left align-middle shadow-xl transition-all">
|
||||
<Dialog.Title as="h3" className="text-lg font-medium leading-6 text-gray-400">
|
||||
{t('integrations:grant-access-to-secrets')}
|
||||
{t('integrations.grant-access-to-secrets')}
|
||||
</Dialog.Title>
|
||||
<div className="mt-2 mb-2">
|
||||
<p className="text-sm text-gray-500">
|
||||
{t('integrations:why-infisical-needs-access')}
|
||||
{t('integrations.why-infisical-needs-access')}
|
||||
</p>
|
||||
</div>
|
||||
<div className="mt-6 max-w-max">
|
||||
<Button
|
||||
onButtonPressed={submit}
|
||||
color="mineshaft"
|
||||
text={t('integrations:grant-access-button') as string}
|
||||
text={t('integrations.grant-access-button') as string}
|
||||
size="md"
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Fragment, useState } from 'react';
|
||||
import { useTranslation } from 'next-i18next';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { faCheck, faCopy } from '@fortawesome/free-solid-svg-icons';
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||
import { Dialog, Transition } from '@headlessui/react';
|
||||
@@ -15,7 +15,7 @@ const expiryMapping = {
|
||||
'7 days': 604800,
|
||||
'1 month': 2592000,
|
||||
'6 months': 15552000,
|
||||
'12 months': 31104000,
|
||||
'12 months': 31104000
|
||||
};
|
||||
|
||||
type Props = {
|
||||
@@ -27,12 +27,7 @@ type Props = {
|
||||
};
|
||||
|
||||
// TODO: convert to TS
|
||||
const AddApiKeyDialog = ({
|
||||
isOpen,
|
||||
closeModal,
|
||||
apiKeys,
|
||||
setApiKeys,
|
||||
}: Props) => {
|
||||
const AddApiKeyDialog = ({ isOpen, closeModal, apiKeys, setApiKeys }: Props) => {
|
||||
const [apiKey, setApiKey] = useState('');
|
||||
const [apiKeyName, setApiKeyName] = useState('');
|
||||
const [apiKeyExpiresIn, setApiKeyExpiresIn] = useState('1 day');
|
||||
@@ -42,7 +37,7 @@ const AddApiKeyDialog = ({
|
||||
const generateAPIKey = async () => {
|
||||
const newApiKey = await addAPIKey({
|
||||
name: apiKeyName,
|
||||
expiresIn: expiryMapping[apiKeyExpiresIn as keyof typeof expiryMapping],
|
||||
expiresIn: expiryMapping[apiKeyExpiresIn as keyof typeof expiryMapping]
|
||||
});
|
||||
|
||||
setApiKeys([...apiKeys, newApiKey.apiKeyData]);
|
||||
@@ -73,143 +68,130 @@ const AddApiKeyDialog = ({
|
||||
};
|
||||
|
||||
return (
|
||||
<div className='z-50'>
|
||||
<div className="z-50">
|
||||
<Transition appear show={isOpen} as={Fragment}>
|
||||
<Dialog as='div' className='relative' onClose={closeModal}>
|
||||
<Dialog as="div" className="relative" onClose={closeModal}>
|
||||
<Transition.Child
|
||||
as={Fragment}
|
||||
enter='ease-out duration-300'
|
||||
enterFrom='opacity-0'
|
||||
enterTo='opacity-100'
|
||||
leave='ease-in duration-200'
|
||||
leaveFrom='opacity-100'
|
||||
leaveTo='opacity-0'
|
||||
enter="ease-out duration-300"
|
||||
enterFrom="opacity-0"
|
||||
enterTo="opacity-100"
|
||||
leave="ease-in duration-200"
|
||||
leaveFrom="opacity-100"
|
||||
leaveTo="opacity-0"
|
||||
>
|
||||
<div className='fixed inset-0 bg-bunker-700 bg-opacity-80' />
|
||||
<div className="fixed inset-0 bg-bunker-700 bg-opacity-80" />
|
||||
</Transition.Child>
|
||||
|
||||
<div className='fixed inset-0 overflow-y-auto'>
|
||||
<div className='flex min-h-full items-center justify-center p-4 text-center'>
|
||||
<div className="fixed inset-0 overflow-y-auto">
|
||||
<div className="flex min-h-full items-center justify-center p-4 text-center">
|
||||
<Transition.Child
|
||||
as={Fragment}
|
||||
enter='ease-out duration-300'
|
||||
enterFrom='opacity-0 scale-95'
|
||||
enterTo='opacity-100 scale-100'
|
||||
leave='ease-in duration-200'
|
||||
leaveFrom='opacity-100 scale-100'
|
||||
leaveTo='opacity-0 scale-95'
|
||||
enter="ease-out duration-300"
|
||||
enterFrom="opacity-0 scale-95"
|
||||
enterTo="opacity-100 scale-100"
|
||||
leave="ease-in duration-200"
|
||||
leaveFrom="opacity-100 scale-100"
|
||||
leaveTo="opacity-0 scale-95"
|
||||
>
|
||||
{apiKey === '' ? (
|
||||
<Dialog.Panel className='w-full max-w-md transform rounded-md bg-bunker-800 border border-gray-700 p-6 text-left align-middle shadow-xl transition-all'>
|
||||
<Dialog.Panel className="w-full max-w-md transform rounded-md border border-gray-700 bg-bunker-800 p-6 text-left align-middle shadow-xl transition-all">
|
||||
<Dialog.Title
|
||||
as='h3'
|
||||
className='text-lg font-medium leading-6 text-gray-400 z-50'
|
||||
as="h3"
|
||||
className="z-50 text-lg font-medium leading-6 text-gray-400"
|
||||
>
|
||||
{t('section-api-key:add-dialog.title')}
|
||||
{t('section.api-key.add-dialog.title')}
|
||||
</Dialog.Title>
|
||||
<div className='mt-2 mb-4'>
|
||||
<div className='flex flex-col'>
|
||||
<p className='text-sm text-gray-500'>
|
||||
{t('section-api-key:add-dialog.description')}
|
||||
<div className="mt-2 mb-4">
|
||||
<div className="flex flex-col">
|
||||
<p className="text-sm text-gray-500">
|
||||
{t('section.api-key.add-dialog.description')}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className='max-h-28 mb-2'>
|
||||
<div className="mb-2 max-h-28">
|
||||
<InputField
|
||||
label={t('section-api-key:add-dialog.name')}
|
||||
label={t('section.api-key.add-dialog.name')}
|
||||
onChangeHandler={setApiKeyName}
|
||||
type='varName'
|
||||
type="varName"
|
||||
value={apiKeyName}
|
||||
placeholder=''
|
||||
placeholder=""
|
||||
isRequired
|
||||
/>
|
||||
</div>
|
||||
<div className='max-h-28'>
|
||||
<div className="max-h-28">
|
||||
<ListBox
|
||||
isSelected={apiKeyExpiresIn}
|
||||
onChange={setApiKeyExpiresIn}
|
||||
data={[
|
||||
'1 day',
|
||||
'7 days',
|
||||
'1 month',
|
||||
'6 months',
|
||||
'12 months',
|
||||
]}
|
||||
data={['1 day', '7 days', '1 month', '6 months', '12 months']}
|
||||
isFull
|
||||
text={`${t('common:expired-in')}: `}
|
||||
text={`${t('common.expired-in')}: `}
|
||||
/>
|
||||
</div>
|
||||
<div className='max-w-max'>
|
||||
<div className='mt-6 flex flex-col justify-start w-max'>
|
||||
<div className="max-w-max">
|
||||
<div className="mt-6 flex w-max flex-col justify-start">
|
||||
<Button
|
||||
onButtonPressed={() => generateAPIKey()}
|
||||
color='mineshaft'
|
||||
text={t('section-api-key:add-dialog.add') as string}
|
||||
textDisabled={
|
||||
t('section-api-key:add-dialog.add') as string
|
||||
}
|
||||
size='md'
|
||||
color="mineshaft"
|
||||
text={t('section.api-key.add-dialog.add') as string}
|
||||
textDisabled={t('section.api-key.add-dialog.add') as string}
|
||||
size="md"
|
||||
active={apiKeyName !== ''}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</Dialog.Panel>
|
||||
) : (
|
||||
<Dialog.Panel className='w-full max-w-md transform rounded-md bg-bunker-800 border border-gray-700 p-6 text-left align-middle shadow-xl transition-all'>
|
||||
<Dialog.Panel className="w-full max-w-md transform rounded-md border border-gray-700 bg-bunker-800 p-6 text-left align-middle shadow-xl transition-all">
|
||||
<Dialog.Title
|
||||
as='h3'
|
||||
className='text-lg font-medium leading-6 text-gray-400 z-50'
|
||||
as="h3"
|
||||
className="z-50 text-lg font-medium leading-6 text-gray-400"
|
||||
>
|
||||
{t('section-api-key:add-dialog.copy-service-token')}
|
||||
{t('section.api-key.add-dialog.copy-service-token')}
|
||||
</Dialog.Title>
|
||||
<div className='mt-2 mb-4'>
|
||||
<div className='flex flex-col'>
|
||||
<p className='text-sm text-gray-500'>
|
||||
{t(
|
||||
'section-api-key:add-dialog.copy-service-token-description'
|
||||
)}
|
||||
<div className="mt-2 mb-4">
|
||||
<div className="flex flex-col">
|
||||
<p className="text-sm text-gray-500">
|
||||
{t('section.api-key.add-dialog.copy-service-token-description')}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className='w-full'>
|
||||
<div className='flex justify-end items-center bg-white/[0.07] text-base mt-2 mr-2 rounded-md text-gray-400 w-full h-20'>
|
||||
<div className="w-full">
|
||||
<div className="mt-2 mr-2 flex h-20 w-full items-center justify-end rounded-md bg-white/[0.07] text-base text-gray-400">
|
||||
<input
|
||||
type='text'
|
||||
type="text"
|
||||
value={apiKey}
|
||||
disabled
|
||||
id='apiKey'
|
||||
className='invisible bg-white/0 text-gray-400 py-2 w-full px-2 min-w-full outline-none'
|
||||
/>
|
||||
<div className='bg-white/0 max-w-md text-sm text-gray-400 py-2 w-full pl-14 pr-2 break-words outline-none'>
|
||||
id="apiKey"
|
||||
className="invisible w-full min-w-full bg-white/0 py-2 px-2 text-gray-400 outline-none"
|
||||
/>
|
||||
<div className="w-full max-w-md break-words bg-white/0 py-2 pl-14 pr-2 text-sm text-gray-400 outline-none">
|
||||
{apiKey}
|
||||
</div>
|
||||
<div className='group font-normal h-full relative inline-block text-gray-400 underline hover:text-primary duration-200'>
|
||||
<div className="group relative inline-block h-full font-normal text-gray-400 underline duration-200 hover:text-primary">
|
||||
<button
|
||||
type="button"
|
||||
onClick={copyToClipboard}
|
||||
className='h-full pl-3.5 pr-4 border-l border-white/20 py-2 hover:bg-white/[0.12] duration-200'
|
||||
className="h-full border-l border-white/20 py-2 pl-3.5 pr-4 duration-200 hover:bg-white/[0.12]"
|
||||
>
|
||||
{apiKeyCopied ? (
|
||||
<FontAwesomeIcon
|
||||
icon={faCheck}
|
||||
className='pr-0.5'
|
||||
/>
|
||||
<FontAwesomeIcon icon={faCheck} className="pr-0.5" />
|
||||
) : (
|
||||
<FontAwesomeIcon icon={faCopy} />
|
||||
)}
|
||||
</button>
|
||||
<span className='absolute hidden group-hover:flex group-hover:animate-popup duration-300 w-28 -left-8 -top-20 translate-y-full px-3 py-2 bg-chicago-900 rounded-md text-center text-gray-400 text-sm'>
|
||||
{t('common:click-to-copy')}
|
||||
<span className="absolute -left-8 -top-20 hidden w-28 translate-y-full rounded-md bg-chicago-900 px-3 py-2 text-center text-sm text-gray-400 duration-300 group-hover:flex group-hover:animate-popup">
|
||||
{t('common.click-to-copy')}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className='mt-6 flex flex-col justify-start w-max'>
|
||||
<div className="mt-6 flex w-max flex-col justify-start">
|
||||
<Button
|
||||
onButtonPressed={() => closeAddApiKeyModal()}
|
||||
color='mineshaft'
|
||||
text='Close'
|
||||
size='md'
|
||||
color="mineshaft"
|
||||
text="Close"
|
||||
size="md"
|
||||
/>
|
||||
</div>
|
||||
</Dialog.Panel>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Fragment, useState } from 'react';
|
||||
import { useTranslation } from 'next-i18next';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Dialog, Transition } from '@headlessui/react';
|
||||
|
||||
import addIncidentContact from '@app/pages/api/organization/addIncidentContact';
|
||||
@@ -18,7 +18,7 @@ const AddIncidentContactDialog = ({
|
||||
isOpen,
|
||||
closeModal,
|
||||
incidentContacts,
|
||||
setIncidentContacts,
|
||||
setIncidentContacts
|
||||
}: Props) => {
|
||||
const [incidentContactEmail, setIncidentContactEmail] = useState('');
|
||||
const { t } = useTranslation();
|
||||
@@ -29,69 +29,61 @@ const AddIncidentContactDialog = ({
|
||||
? incidentContacts.concat([incidentContactEmail])
|
||||
: [incidentContactEmail]
|
||||
);
|
||||
addIncidentContact(
|
||||
localStorage.getItem('orgData.id') as string,
|
||||
incidentContactEmail
|
||||
);
|
||||
addIncidentContact(localStorage.getItem('orgData.id') as string, incidentContactEmail);
|
||||
closeModal();
|
||||
};
|
||||
return (
|
||||
<div>
|
||||
<Transition appear show={isOpen} as={Fragment}>
|
||||
<Dialog as='div' className='relative z-10' onClose={closeModal}>
|
||||
<Dialog as="div" className="relative z-10" onClose={closeModal}>
|
||||
<Transition.Child
|
||||
as={Fragment}
|
||||
enter='ease-out duration-300'
|
||||
enterFrom='opacity-0'
|
||||
enterTo='opacity-100'
|
||||
leave='ease-in duration-200'
|
||||
leaveFrom='opacity-100'
|
||||
leaveTo='opacity-0'
|
||||
enter="ease-out duration-300"
|
||||
enterFrom="opacity-0"
|
||||
enterTo="opacity-100"
|
||||
leave="ease-in duration-200"
|
||||
leaveFrom="opacity-100"
|
||||
leaveTo="opacity-0"
|
||||
>
|
||||
<div className='fixed inset-0 bg-black bg-opacity-70' />
|
||||
<div className="fixed inset-0 bg-black bg-opacity-70" />
|
||||
</Transition.Child>
|
||||
|
||||
<div className='fixed inset-0 overflow-y-auto'>
|
||||
<div className='flex min-h-full items-center justify-center p-4 text-center'>
|
||||
<div className="fixed inset-0 overflow-y-auto">
|
||||
<div className="flex min-h-full items-center justify-center p-4 text-center">
|
||||
<Transition.Child
|
||||
as={Fragment}
|
||||
enter='ease-out duration-300'
|
||||
enterFrom='opacity-0 scale-95'
|
||||
enterTo='opacity-100 scale-100'
|
||||
leave='ease-in duration-200'
|
||||
leaveFrom='opacity-100 scale-100'
|
||||
leaveTo='opacity-0 scale-95'
|
||||
enter="ease-out duration-300"
|
||||
enterFrom="opacity-0 scale-95"
|
||||
enterTo="opacity-100 scale-100"
|
||||
leave="ease-in duration-200"
|
||||
leaveFrom="opacity-100 scale-100"
|
||||
leaveTo="opacity-0 scale-95"
|
||||
>
|
||||
<Dialog.Panel className='w-full max-w-md transform overflow-hidden rounded-md bg-bunker-800 border border-gray-700 p-6 text-left align-middle shadow-xl transition-all'>
|
||||
<Dialog.Title
|
||||
as='h3'
|
||||
className='text-lg font-medium leading-6 text-gray-400'
|
||||
>
|
||||
{t('section-incident:add-dialog.title')}
|
||||
<Dialog.Panel className="w-full max-w-md transform overflow-hidden rounded-md border border-gray-700 bg-bunker-800 p-6 text-left align-middle shadow-xl transition-all">
|
||||
<Dialog.Title as="h3" className="text-lg font-medium leading-6 text-gray-400">
|
||||
{t('section.incident.add-dialog.title')}
|
||||
</Dialog.Title>
|
||||
<div className='mt-2 mb-2'>
|
||||
<p className='text-sm text-gray-500'>
|
||||
{t('section-incident:add-dialog.description')}
|
||||
<div className="mt-2 mb-2">
|
||||
<p className="text-sm text-gray-500">
|
||||
{t('section.incident.add-dialog.description')}
|
||||
</p>
|
||||
</div>
|
||||
<div className='max-h-28'>
|
||||
<div className="max-h-28">
|
||||
<InputField
|
||||
label={t('common:email')}
|
||||
label={t('common.email')}
|
||||
onChangeHandler={setIncidentContactEmail}
|
||||
type='varName'
|
||||
type="varName"
|
||||
value={incidentContactEmail}
|
||||
placeholder=''
|
||||
placeholder=""
|
||||
isRequired
|
||||
/>
|
||||
</div>
|
||||
<div className='mt-6 max-w-max'>
|
||||
<div className="mt-6 max-w-max">
|
||||
<Button
|
||||
onButtonPressed={submit}
|
||||
color='mineshaft'
|
||||
text={
|
||||
t('section-incident:add-dialog.add-incident') as string
|
||||
}
|
||||
size='md'
|
||||
color="mineshaft"
|
||||
text={t('section.incident.add-dialog.add-incident') as string}
|
||||
size="md"
|
||||
/>
|
||||
</div>
|
||||
</Dialog.Panel>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Fragment } from 'react';
|
||||
import { Trans, useTranslation } from 'react-i18next';
|
||||
import { useRouter } from 'next/router';
|
||||
import { Trans, useTranslation } from 'next-i18next';
|
||||
import { Dialog, Transition } from '@headlessui/react';
|
||||
|
||||
import Button from '../buttons/Button';
|
||||
@@ -21,7 +21,7 @@ const AddProjectMemberDialog = ({
|
||||
submitModal,
|
||||
data,
|
||||
email,
|
||||
setEmail,
|
||||
setEmail
|
||||
}: Props) => {
|
||||
const router = useRouter();
|
||||
const { t } = useTranslation();
|
||||
@@ -53,31 +53,31 @@ const AddProjectMemberDialog = ({
|
||||
leaveFrom="opacity-100 scale-100"
|
||||
leaveTo="opacity-0 scale-95"
|
||||
>
|
||||
<Dialog.Panel className="w-full max-w-md transform rounded-md bg-bunker-800 border border-gray-700 p-6 text-left align-middle shadow-xl transition-all">
|
||||
<Dialog.Panel className="w-full max-w-md transform rounded-md border border-mineshaft-600 bg-mineshaft-800 p-6 text-left align-middle shadow-xl transition-all">
|
||||
{data?.length > 0 ? (
|
||||
<Dialog.Title
|
||||
as="h3"
|
||||
className="text-lg font-medium leading-6 text-gray-400 z-50"
|
||||
className="z-50 text-lg font-medium leading-6 text-gray-400"
|
||||
>
|
||||
{t('section-members:add-dialog.add-member-to-project')}
|
||||
{t('section.members.add-dialog.add-member-to-project')}
|
||||
</Dialog.Title>
|
||||
) : (
|
||||
<Dialog.Title
|
||||
as="h3"
|
||||
className="text-lg font-medium leading-6 text-gray-400 z-50"
|
||||
className="z-50 text-lg font-medium text-mineshaft-300 mb-4"
|
||||
>
|
||||
{t('section-members:add-dialog.already-all-invited')}
|
||||
{t('section.members.add-dialog.already-all-invited')}
|
||||
</Dialog.Title>
|
||||
)}
|
||||
<div className="mt-2 mb-4">
|
||||
{data?.length > 0 ? (
|
||||
<div className="flex flex-col">
|
||||
<p className="text-sm text-gray-500">
|
||||
{t('section-members:add-dialog.user-will-email')}
|
||||
{t('section.members.add-dialog.user-will-email')}
|
||||
</p>
|
||||
<div className="">
|
||||
<Trans
|
||||
i18nKey="section-members:add-dialog.looking-add"
|
||||
i18nKey="section.members.add-dialog.looking-add"
|
||||
components={[
|
||||
// eslint-disable-next-line react/jsx-key
|
||||
<button
|
||||
@@ -101,22 +101,27 @@ const AddProjectMemberDialog = ({
|
||||
</div>
|
||||
) : (
|
||||
<p className="text-sm text-gray-500">
|
||||
{t('section-members:add-dialog.add-user-org-first')}
|
||||
{t('section.members.add-dialog.add-user-org-first')}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
<div className="max-h-28">
|
||||
{data?.length > 0 && (
|
||||
<ListBox isSelected={email || data[0]} onChange={setEmail} data={data} isFull />
|
||||
<ListBox
|
||||
isSelected={email || data[0]}
|
||||
onChange={setEmail}
|
||||
data={data}
|
||||
isFull
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
<div className="max-w-max">
|
||||
{data?.length > 0 ? (
|
||||
<div className="mt-6 flex flex-col justify-start w-max">
|
||||
<div className="mt-6 flex w-max flex-col justify-start">
|
||||
<Button
|
||||
onButtonPressed={submitModal}
|
||||
color="mineshaft"
|
||||
text={t('section-members:add-member') as string}
|
||||
text={t('section.members.add-member') as string}
|
||||
size="md"
|
||||
/>
|
||||
</div>
|
||||
@@ -124,7 +129,7 @@ const AddProjectMemberDialog = ({
|
||||
<Button
|
||||
onButtonPressed={() => router.push(`/settings/org/${router.query.id}`)}
|
||||
color="mineshaft"
|
||||
text={t('section-members:add-dialog.add-user-to-org') as string}
|
||||
text={t('section.members.add-dialog.add-user-to-org') as string}
|
||||
size="md"
|
||||
/>
|
||||
)}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import crypto from 'crypto';
|
||||
|
||||
import { Fragment, useState } from 'react';
|
||||
import { useTranslation } from 'next-i18next';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { faCheck, faCopy } from '@fortawesome/free-solid-svg-icons';
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||
import { Dialog, Transition } from '@headlessui/react';
|
||||
@@ -9,10 +9,7 @@ import { Dialog, Transition } from '@headlessui/react';
|
||||
import addServiceToken from '@app/pages/api/serviceToken/addServiceToken';
|
||||
import getLatestFileKey from '@app/pages/api/workspace/getLatestFileKey';
|
||||
|
||||
import {
|
||||
decryptAssymmetric,
|
||||
encryptSymmetric,
|
||||
} from '../../utilities/cryptography/crypto';
|
||||
import { decryptAssymmetric, encryptSymmetric } from '../../utilities/cryptography/crypto';
|
||||
import Button from '../buttons/Button';
|
||||
import InputField from '../InputField';
|
||||
import ListBox from '../Listbox';
|
||||
@@ -22,7 +19,7 @@ const expiryMapping = {
|
||||
'7 days': 604800,
|
||||
'1 month': 2592000,
|
||||
'6 months': 15552000,
|
||||
'12 months': 31104000,
|
||||
'12 months': 31104000
|
||||
};
|
||||
|
||||
type Props = {
|
||||
@@ -42,13 +39,11 @@ const AddServiceTokenDialog = ({
|
||||
workspaceName,
|
||||
serviceTokens,
|
||||
environments,
|
||||
setServiceTokens,
|
||||
setServiceTokens
|
||||
}: Props) => {
|
||||
const [serviceToken, setServiceToken] = useState('');
|
||||
const [serviceTokenName, setServiceTokenName] = useState('');
|
||||
const [selectedServiceTokenEnv, setSelectedServiceTokenEnv] = useState(
|
||||
environments?.[0]
|
||||
);
|
||||
const [selectedServiceTokenEnv, setSelectedServiceTokenEnv] = useState(environments?.[0]);
|
||||
const [serviceTokenExpiresIn, setServiceTokenExpiresIn] = useState('1 day');
|
||||
const [serviceTokenCopied, setServiceTokenCopied] = useState(false);
|
||||
const { t } = useTranslation();
|
||||
@@ -60,40 +55,40 @@ const AddServiceTokenDialog = ({
|
||||
ciphertext: latestFileKey.latestKey.encryptedKey,
|
||||
nonce: latestFileKey.latestKey.nonce,
|
||||
publicKey: latestFileKey.latestKey.sender.publicKey,
|
||||
privateKey: localStorage.getItem('PRIVATE_KEY') as string,
|
||||
privateKey: localStorage.getItem('PRIVATE_KEY') as string
|
||||
});
|
||||
|
||||
const randomBytes = crypto.randomBytes(16).toString('hex');
|
||||
const { ciphertext, iv, tag } = encryptSymmetric({
|
||||
plaintext: key,
|
||||
key: randomBytes,
|
||||
key: randomBytes
|
||||
});
|
||||
|
||||
console.log(1234, selectedServiceTokenEnv, environments, selectedServiceTokenEnv?.slug
|
||||
? selectedServiceTokenEnv.slug
|
||||
: environments[0]?.slug)
|
||||
|
||||
console.log(
|
||||
1234,
|
||||
selectedServiceTokenEnv,
|
||||
environments,
|
||||
selectedServiceTokenEnv?.slug ? selectedServiceTokenEnv.slug : environments[0]?.slug
|
||||
);
|
||||
const newServiceToken = await addServiceToken({
|
||||
name: serviceTokenName,
|
||||
workspaceId,
|
||||
environment: selectedServiceTokenEnv?.slug
|
||||
? selectedServiceTokenEnv.slug
|
||||
: environments[0]?.slug,
|
||||
expiresIn:
|
||||
expiryMapping[serviceTokenExpiresIn as keyof typeof expiryMapping],
|
||||
expiresIn: expiryMapping[serviceTokenExpiresIn as keyof typeof expiryMapping],
|
||||
encryptedKey: ciphertext,
|
||||
iv,
|
||||
tag,
|
||||
tag
|
||||
});
|
||||
|
||||
setServiceTokens(serviceTokens.concat([newServiceToken.serviceTokenData]));
|
||||
setServiceToken(`${newServiceToken.serviceToken }.${ randomBytes}`);
|
||||
setServiceToken(`${newServiceToken.serviceToken}.${randomBytes}`);
|
||||
};
|
||||
|
||||
function copyToClipboard() {
|
||||
// Get the text field
|
||||
const copyText = document.getElementById(
|
||||
'serviceToken'
|
||||
) as HTMLInputElement;
|
||||
const copyText = document.getElementById('serviceToken') as HTMLInputElement;
|
||||
|
||||
// Select the text field
|
||||
copyText.select();
|
||||
@@ -115,60 +110,60 @@ const AddServiceTokenDialog = ({
|
||||
};
|
||||
|
||||
return (
|
||||
<div className='z-50'>
|
||||
<div className="z-50">
|
||||
<Transition appear show={isOpen} as={Fragment}>
|
||||
<Dialog as='div' className='relative' onClose={closeModal}>
|
||||
<Dialog as="div" className="relative" onClose={closeModal}>
|
||||
<Transition.Child
|
||||
as={Fragment}
|
||||
enter='ease-out duration-300'
|
||||
enterFrom='opacity-0'
|
||||
enterTo='opacity-100'
|
||||
leave='ease-in duration-200'
|
||||
leaveFrom='opacity-100'
|
||||
leaveTo='opacity-0'
|
||||
enter="ease-out duration-300"
|
||||
enterFrom="opacity-0"
|
||||
enterTo="opacity-100"
|
||||
leave="ease-in duration-200"
|
||||
leaveFrom="opacity-100"
|
||||
leaveTo="opacity-0"
|
||||
>
|
||||
<div className='fixed inset-0 bg-bunker-700 bg-opacity-80' />
|
||||
<div className="fixed inset-0 bg-bunker-700 bg-opacity-80" />
|
||||
</Transition.Child>
|
||||
|
||||
<div className='fixed inset-0 overflow-y-auto'>
|
||||
<div className='flex min-h-full items-center justify-center p-4 text-center'>
|
||||
<div className="fixed inset-0 overflow-y-auto">
|
||||
<div className="flex min-h-full items-center justify-center p-4 text-center">
|
||||
<Transition.Child
|
||||
as={Fragment}
|
||||
enter='ease-out duration-300'
|
||||
enterFrom='opacity-0 scale-95'
|
||||
enterTo='opacity-100 scale-100'
|
||||
leave='ease-in duration-200'
|
||||
leaveFrom='opacity-100 scale-100'
|
||||
leaveTo='opacity-0 scale-95'
|
||||
enter="ease-out duration-300"
|
||||
enterFrom="opacity-0 scale-95"
|
||||
enterTo="opacity-100 scale-100"
|
||||
leave="ease-in duration-200"
|
||||
leaveFrom="opacity-100 scale-100"
|
||||
leaveTo="opacity-0 scale-95"
|
||||
>
|
||||
{serviceToken === '' ? (
|
||||
<Dialog.Panel className='w-full max-w-md transform rounded-md bg-bunker-800 border border-gray-700 p-6 text-left align-middle shadow-xl transition-all'>
|
||||
<Dialog.Panel className="w-full max-w-md transform rounded-md border border-gray-700 bg-bunker-800 p-6 text-left align-middle shadow-xl transition-all">
|
||||
<Dialog.Title
|
||||
as='h3'
|
||||
className='text-lg font-medium leading-6 text-gray-400 z-50'
|
||||
as="h3"
|
||||
className="z-50 text-lg font-medium leading-6 text-gray-400"
|
||||
>
|
||||
{t('section-token:add-dialog.title', {
|
||||
target: workspaceName,
|
||||
{t('section.token.add-dialog.title', {
|
||||
target: workspaceName
|
||||
})}
|
||||
</Dialog.Title>
|
||||
<div className='mt-2 mb-4'>
|
||||
<div className='flex flex-col'>
|
||||
<p className='text-sm text-gray-500'>
|
||||
{t('section-token:add-dialog.description')}
|
||||
<div className="mt-2 mb-4">
|
||||
<div className="flex flex-col">
|
||||
<p className="text-sm text-gray-500">
|
||||
{t('section.token.add-dialog.description')}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className='max-h-28 mb-2'>
|
||||
<div className="mb-2 max-h-28">
|
||||
<InputField
|
||||
label={t('section-token:add-dialog.name')}
|
||||
label={t('section.token.add-dialog.name')}
|
||||
onChangeHandler={setServiceTokenName}
|
||||
type='varName'
|
||||
type="varName"
|
||||
value={serviceTokenName}
|
||||
placeholder=''
|
||||
placeholder=""
|
||||
isRequired
|
||||
/>
|
||||
</div>
|
||||
<div className='max-h-28 mb-2'>
|
||||
<div className="mb-2 max-h-28">
|
||||
<ListBox
|
||||
isSelected={
|
||||
selectedServiceTokenEnv?.name
|
||||
@@ -178,103 +173,88 @@ const AddServiceTokenDialog = ({
|
||||
data={environments.map(({ name }) => name)}
|
||||
onChange={(envName) =>
|
||||
setSelectedServiceTokenEnv(
|
||||
environments.find(
|
||||
({ name }) => envName === name
|
||||
) || {
|
||||
environments.find(({ name }) => envName === name) || {
|
||||
name: 'unknown',
|
||||
slug: 'unknown',
|
||||
slug: 'unknown'
|
||||
}
|
||||
)
|
||||
}
|
||||
isFull
|
||||
text={`${t('common:environment')}: `}
|
||||
text={`${t('common.environment')}: `}
|
||||
/>
|
||||
</div>
|
||||
<div className='max-h-28'>
|
||||
<div className="max-h-28">
|
||||
<ListBox
|
||||
isSelected={serviceTokenExpiresIn}
|
||||
onChange={setServiceTokenExpiresIn}
|
||||
data={[
|
||||
'1 day',
|
||||
'7 days',
|
||||
'1 month',
|
||||
'6 months',
|
||||
'12 months',
|
||||
]}
|
||||
data={['1 day', '7 days', '1 month', '6 months', '12 months']}
|
||||
isFull
|
||||
text={`${t('common:expired-in')}: `}
|
||||
text={`${t('common.expired-in')}: `}
|
||||
/>
|
||||
</div>
|
||||
<div className='max-w-max'>
|
||||
<div className='mt-6 flex flex-col justify-start w-max'>
|
||||
<div className="max-w-max">
|
||||
<div className="mt-6 flex w-max flex-col justify-start">
|
||||
<Button
|
||||
onButtonPressed={() => generateServiceToken()}
|
||||
color='mineshaft'
|
||||
text={t('section-token:add-dialog.add') as string}
|
||||
textDisabled={
|
||||
t('section-token:add-dialog.add') as string
|
||||
}
|
||||
size='md'
|
||||
color="mineshaft"
|
||||
text={t('section.token.add-dialog.add') as string}
|
||||
textDisabled={t('section.token.add-dialog.add') as string}
|
||||
size="md"
|
||||
active={serviceTokenName !== ''}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</Dialog.Panel>
|
||||
) : (
|
||||
<Dialog.Panel className='w-full max-w-md transform rounded-md bg-bunker-800 border border-gray-700 p-6 text-left align-middle shadow-xl transition-all'>
|
||||
<Dialog.Panel className="w-full max-w-md transform rounded-md border border-gray-700 bg-bunker-800 p-6 text-left align-middle shadow-xl transition-all">
|
||||
<Dialog.Title
|
||||
as='h3'
|
||||
className='text-lg font-medium leading-6 text-gray-400 z-50'
|
||||
as="h3"
|
||||
className="z-50 text-lg font-medium leading-6 text-gray-400"
|
||||
>
|
||||
{t('section-token:add-dialog.copy-service-token')}
|
||||
{t('section.token.add-dialog.copy-service-token')}
|
||||
</Dialog.Title>
|
||||
<div className='mt-2 mb-4'>
|
||||
<div className='flex flex-col'>
|
||||
<p className='text-sm text-gray-500'>
|
||||
{t(
|
||||
'section-token:add-dialog.copy-service-token-description'
|
||||
)}
|
||||
<div className="mt-2 mb-4">
|
||||
<div className="flex flex-col">
|
||||
<p className="text-sm text-gray-500">
|
||||
{t('section.token.add-dialog.copy-service-token-description')}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className='w-full'>
|
||||
<div className='flex justify-end items-center bg-white/[0.07] text-base mt-2 mr-2 rounded-md text-gray-400 w-full h-20'>
|
||||
<div className="w-full">
|
||||
<div className="mt-2 mr-2 flex h-20 w-full items-center justify-end rounded-md bg-white/[0.07] text-base text-gray-400">
|
||||
<input
|
||||
type='text'
|
||||
type="text"
|
||||
value={serviceToken}
|
||||
id='serviceToken'
|
||||
className='invisible bg-white/0 text-gray-400 py-2 w-full px-2 min-w-full outline-none'
|
||||
/>
|
||||
<div className='bg-white/0 max-w-md text-sm text-gray-400 py-2 w-full pl-14 pr-2 break-words outline-none'>
|
||||
id="serviceToken"
|
||||
className="invisible w-full min-w-full bg-white/0 py-2 px-2 text-gray-400 outline-none"
|
||||
/>
|
||||
<div className="w-full max-w-md break-words bg-white/0 py-2 pl-14 pr-2 text-sm text-gray-400 outline-none">
|
||||
{serviceToken}
|
||||
</div>
|
||||
<div className='group font-normal h-full relative inline-block text-gray-400 underline hover:text-primary duration-200'>
|
||||
<div className="group relative inline-block h-full font-normal text-gray-400 underline duration-200 hover:text-primary">
|
||||
<button
|
||||
onClick={copyToClipboard}
|
||||
type="button"
|
||||
className='h-full pl-3.5 pr-4 border-l border-white/20 py-2 hover:bg-white/[0.12] duration-200'
|
||||
className="h-full border-l border-white/20 py-2 pl-3.5 pr-4 duration-200 hover:bg-white/[0.12]"
|
||||
>
|
||||
{serviceTokenCopied ? (
|
||||
<FontAwesomeIcon
|
||||
icon={faCheck}
|
||||
className='pr-0.5'
|
||||
/>
|
||||
<FontAwesomeIcon icon={faCheck} className="pr-0.5" />
|
||||
) : (
|
||||
<FontAwesomeIcon icon={faCopy} />
|
||||
)}
|
||||
</button>
|
||||
<span className='absolute hidden group-hover:flex group-hover:animate-popup duration-300 w-28 -left-8 -top-20 translate-y-full px-3 py-2 bg-chicago-900 rounded-md text-center text-gray-400 text-sm'>
|
||||
{t('common:click-to-copy')}
|
||||
<span className="absolute -left-8 -top-20 hidden w-28 translate-y-full rounded-md bg-chicago-900 px-3 py-2 text-center text-sm text-gray-400 duration-300 group-hover:flex group-hover:animate-popup">
|
||||
{t('common.click-to-copy')}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className='mt-6 flex flex-col justify-start w-max'>
|
||||
<div className="mt-6 flex w-max flex-col justify-start">
|
||||
<Button
|
||||
onButtonPressed={() => closeAddServiceTokenModal()}
|
||||
color='mineshaft'
|
||||
text='Close'
|
||||
size='md'
|
||||
color="mineshaft"
|
||||
text="Close"
|
||||
size="md"
|
||||
/>
|
||||
</div>
|
||||
</Dialog.Panel>
|
||||
|
||||
@@ -25,6 +25,7 @@ type Props = {
|
||||
changeData: (users: any[]) => void;
|
||||
myUser: string;
|
||||
filter: string;
|
||||
isUserListLoading: boolean;
|
||||
};
|
||||
|
||||
type EnvironmentProps = {
|
||||
@@ -38,7 +39,7 @@ type EnvironmentProps = {
|
||||
* @param {*} props
|
||||
* @returns
|
||||
*/
|
||||
const ProjectUsersTable = ({ userData, changeData, myUser, filter }: Props) => {
|
||||
const ProjectUsersTable = ({ userData, changeData, myUser, filter, isUserListLoading }: Props) => {
|
||||
const [roleSelected, setRoleSelected] = useState(
|
||||
Array(userData?.length).fill(userData.map((user) => user.role))
|
||||
);
|
||||
@@ -205,7 +206,7 @@ const ProjectUsersTable = ({ userData, changeData, myUser, filter }: Props) => {
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="table-container relative mb-6 mt-1 min-w-max rounded-md border border-mineshaft-700 bg-bunker">
|
||||
<div className="table-container relative mb-6 mt-1 min-w-max rounded-md border border-mineshaft-600 bg-bunker">
|
||||
<div className="absolute h-[3.1rem] w-full rounded-t-md bg-white/5" />
|
||||
<UpgradePlanModal
|
||||
isOpen={isUpgradeModalOpen}
|
||||
@@ -213,7 +214,7 @@ const ProjectUsersTable = ({ userData, changeData, myUser, filter }: Props) => {
|
||||
text="You can change user permissions if you switch to Infisical's Professional plan."
|
||||
/>
|
||||
<table className="my-0.5 w-full">
|
||||
<thead className="text-xs font-light text-gray-400">
|
||||
<thead className="text-xs font-light text-gray-400 bg-mineshaft-800">
|
||||
<tr>
|
||||
<th className="py-3.5 pl-4 text-left">NAME</th>
|
||||
<th className="py-3.5 pl-4 text-left">EMAIL</th>
|
||||
@@ -231,7 +232,7 @@ const ProjectUsersTable = ({ userData, changeData, myUser, filter }: Props) => {
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{userData?.filter(
|
||||
{!isUserListLoading && userData?.filter(
|
||||
(user) =>
|
||||
user.firstName?.toLowerCase().includes(filter) ||
|
||||
user.lastName?.toLowerCase().includes(filter) ||
|
||||
@@ -245,14 +246,14 @@ const ProjectUsersTable = ({ userData, changeData, myUser, filter }: Props) => {
|
||||
user.email?.toLowerCase().includes(filter)
|
||||
)
|
||||
.map((row, index) => (
|
||||
<tr key={guidGenerator()} className="bg-bunker-600 text-sm hover:bg-bunker-500">
|
||||
<td className="border-t border-mineshaft-700 py-2 pl-4 text-gray-300">
|
||||
<tr key={guidGenerator()} className="bg-mineshaft-800 text-sm">
|
||||
<td className="border-t border-mineshaft-600 py-2 pl-4 text-gray-300">
|
||||
{row.firstName} {row.lastName}
|
||||
</td>
|
||||
<td className="border-t border-mineshaft-700 py-2 pl-4 text-gray-300">
|
||||
<td className="border-t border-mineshaft-600 py-2 pl-4 text-gray-300">
|
||||
{row.email}
|
||||
</td>
|
||||
<td className="border-t border-mineshaft-700 py-2 pl-6 pr-10 text-gray-300">
|
||||
<td className="border-t border-mineshaft-600 py-2 pl-6 pr-10 text-gray-300">
|
||||
<div className="flex h-full flex-row items-center justify-start">
|
||||
<Select
|
||||
className="w-36 bg-mineshaft-700"
|
||||
@@ -285,7 +286,7 @@ const ProjectUsersTable = ({ userData, changeData, myUser, filter }: Props) => {
|
||||
>
|
||||
<Select
|
||||
className="w-16 bg-mineshaft-700"
|
||||
dropdownContainerClassName="bg-mineshaft-700"
|
||||
dropdownContainerClassName="bg-mineshaft-800 border border-mineshaft-600 text-bunker-200"
|
||||
position="item-aligned"
|
||||
// open={isOpen}
|
||||
onValueChange={(val) =>
|
||||
@@ -391,6 +392,10 @@ const ProjectUsersTable = ({ userData, changeData, myUser, filter }: Props) => {
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
{isUserListLoading && <>
|
||||
<tr key={guidGenerator()} className="bg-mineshaft-800 text-sm animate-pulse h-14 w-full"/>
|
||||
<tr key={guidGenerator()} className="bg-mineshaft-800 text-sm animate-pulse h-14 w-full"/>
|
||||
</>}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
@@ -20,8 +20,8 @@ interface Props {
|
||||
export default function Plan({ plan }: Props) {
|
||||
return (
|
||||
<div
|
||||
className={`relative flex flex-col justify-between border-2 min-w-fit w-96 rounded-lg h-68 mr-4 bg-mineshaft-800 ${
|
||||
plan.name !== 'Starter' && plan.current === true ? 'border-primary' : 'border-chicago-700'
|
||||
className={`relative flex flex-col justify-between border min-w-fit w-96 rounded-lg h-68 mr-4 bg-mineshaft-800 ${
|
||||
plan.name !== 'Starter' && plan.current === true ? 'border-2 border-primary' : 'border-mineshaft-600'
|
||||
}
|
||||
`}
|
||||
>
|
||||
@@ -41,13 +41,13 @@ export default function Plan({ plan }: Props) {
|
||||
<>
|
||||
{plan.buttonTextMain === 'Schedule a Demo' ? (
|
||||
<a href="/scheduledemo" target='_blank rel="noopener"'>
|
||||
<div className="relative z-10 mx-5 mt-3 mb-4 py-2 px-4 border border-1 border-gray-600 hover:text-black hover:border-primary text-gray-400 font-semibold hover:bg-primary bg-bunker duration-200 cursor-pointer rounded-md flex w-max">
|
||||
<div className="relative z-10 mx-5 mt-3 mb-4 py-2 px-4 border border-1 border-mineshaft-600 hover:text-black hover:border-primary text-gray-400 font-semibold hover:bg-primary bg-bunker duration-200 cursor-pointer rounded-md flex w-max">
|
||||
{plan.buttonTextMain}
|
||||
</div>
|
||||
</a>
|
||||
) : (
|
||||
<div
|
||||
className={`relative z-10 mx-5 mt-3 mb-4 py-2 px-4 border border-1 border-gray-600 text-gray-400 font-semibold ${
|
||||
className={`relative z-10 mx-5 mt-3 mb-4 py-2 px-4 border border-1 border-mineshaft-600 text-gray-400 font-semibold ${
|
||||
plan.buttonTextMain === 'Downgrade'
|
||||
? 'hover:bg-red hover:text-white hover:border-red'
|
||||
: 'hover:bg-primary hover:text-black hover:border-primary'
|
||||
@@ -74,7 +74,7 @@ export default function Plan({ plan }: Props) {
|
||||
) : (
|
||||
<div
|
||||
className={`h-8 w-full rounded-b-md flex justify-center items-center z-10 ${
|
||||
plan.name !== 'Starter' && plan.current === true ? 'bg-primary' : 'bg-chicago-700'
|
||||
plan.name !== 'Starter' && plan.current === true ? 'bg-primary' : 'bg-mineshaft-400'
|
||||
}`}
|
||||
>
|
||||
<p className="text-xs text-black font-semibold">CURRENT PLAN</p>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useTranslation } from 'next-i18next';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
/**
|
||||
* This is the text field where people can add comments to particular secrets.
|
||||
@@ -16,9 +16,9 @@ const CommentField = ({
|
||||
|
||||
return (
|
||||
<div className="relative mt-4 px-4 pt-6">
|
||||
<p className="text-sm text-bunker-300 pl-0.5">{t('dashboard:sidebar.comments')}</p>
|
||||
<p className="pl-0.5 text-sm text-bunker-300">{t('dashboard.sidebar.comments')}</p>
|
||||
<textarea
|
||||
className="placeholder:text-bunker-400 dark:[color-scheme:dark] h-32 w-full bg-bunker-800 px-2 py-1.5 rounded-md border border-mineshaft-500 text-sm text-bunker-300 outline-none focus:ring-2 ring-primary-800 ring-opacity-70"
|
||||
className="h-32 w-full rounded-md border border-mineshaft-500 bg-bunker-800 px-2 py-1.5 text-sm text-bunker-300 outline-none ring-primary-800 ring-opacity-70 placeholder:text-bunker-400 focus:ring-2 dark:[color-scheme:dark]"
|
||||
value={comment}
|
||||
onChange={(e) => modifyComment(e.target.value, id)}
|
||||
placeholder="Leave any comments here..."
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
import React, { useState } from 'react'
|
||||
import React from 'react'
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { faXmark } from '@fortawesome/free-solid-svg-icons';
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||
|
||||
import Button from '../basic/buttons/Button';
|
||||
import { DeleteEnvVar } from '../basic/dialog/DeleteEnvVar';
|
||||
|
||||
type Props = {
|
||||
onSubmit: () => void;
|
||||
@@ -13,7 +12,6 @@ type Props = {
|
||||
|
||||
export const DeleteActionButton = ({ onSubmit, isPlain }: Props) => {
|
||||
const { t } = useTranslation();
|
||||
const [open, setOpen] = useState(false)
|
||||
|
||||
return (
|
||||
<div className={`${
|
||||
@@ -25,25 +23,17 @@ export const DeleteActionButton = ({ onSubmit, isPlain }: Props) => {
|
||||
onKeyDown={() => null}
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
onClick={() => setOpen(true)}
|
||||
onClick={onSubmit}
|
||||
className="invisible group-hover:visible"
|
||||
>
|
||||
<FontAwesomeIcon className="text-bunker-300 hover:text-red pl-2 pr-6 text-lg mt-0.5" icon={faXmark} />
|
||||
</div>
|
||||
: <Button
|
||||
text={String(t("Delete"))}
|
||||
// onButtonPressed={onSubmit}
|
||||
color="red"
|
||||
size="md"
|
||||
onButtonPressed={() => setOpen(true)}
|
||||
onButtonPressed={onSubmit}
|
||||
/>}
|
||||
<DeleteEnvVar
|
||||
isOpen={open}
|
||||
onClose={() => {
|
||||
setOpen(false)
|
||||
}}
|
||||
onSubmit={onSubmit}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
/* eslint-disable no-nested-ternary */
|
||||
import { type ChangeEvent, type DragEvent, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import Image from 'next/image';
|
||||
import { useTranslation } from 'next-i18next';
|
||||
import { faUpload } from '@fortawesome/free-solid-svg-icons';
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||
import { parseDocument, Scalar, YAMLMap } from 'yaml';
|
||||
@@ -148,7 +148,7 @@ const DropZone = ({
|
||||
setData(newData);
|
||||
setButtonReady(true);
|
||||
} catch (error) {
|
||||
console.log("Error while dropping the file: ", error)
|
||||
console.log('Error while dropping the file: ', error);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -181,12 +181,12 @@ const DropZone = ({
|
||||
};
|
||||
|
||||
return loading ? (
|
||||
<div className="flex items-center justify-center pt-16 mb-16">
|
||||
<div className="mb-16 flex items-center justify-center pt-16">
|
||||
<Image src="/images/loading/loading.gif" height={70} width={120} alt="google logo" />
|
||||
</div>
|
||||
) : keysExist ? (
|
||||
<div
|
||||
className="opacity-60 hover:opacity-100 duration-200 relative bg-mineshaft-900 max-w-[calc(100%-1rem)] w-full outline-dashed outline-chicago-600 rounded-md outline-2 flex flex-col items-center justify-center mb-4 mx-auto mt-1 py-8 px-2"
|
||||
className="relative mx-auto mb-4 mt-1 flex w-full max-w-[calc(100%-1rem)] flex-col items-center justify-center rounded-md bg-mineshaft-900 py-8 px-2 opacity-60 outline-dashed outline-2 outline-chicago-600 duration-200 hover:opacity-100"
|
||||
onDragEnter={handleDragEnter}
|
||||
onDragOver={handleDragOver}
|
||||
onDragLeave={handleDragLeave}
|
||||
@@ -195,14 +195,14 @@ const DropZone = ({
|
||||
<input
|
||||
id="fileSelect"
|
||||
type="file"
|
||||
className="opacity-0 absolute w-full h-full"
|
||||
className="absolute h-full w-full opacity-0"
|
||||
accept=""
|
||||
onChange={handleFileSelect}
|
||||
/>
|
||||
{errorDragAndDrop ? <div className="my-3 max-w-xl opacity-80" /> : <div className="" />}
|
||||
<div className="flex flex-row">
|
||||
<FontAwesomeIcon icon={faUpload} className="text-bunker-300 text-3xl mr-6" />
|
||||
<p className="text-bunker-300 mt-1">{t('common:drop-zone-keys')}</p>
|
||||
<FontAwesomeIcon icon={faUpload} className="mr-6 text-3xl text-bunker-300" />
|
||||
<p className="mt-1 text-bunker-300">{t('common.drop-zone-keys')}</p>
|
||||
</div>
|
||||
{errorDragAndDrop && (
|
||||
<div className="mt-8 max-w-xl opacity-80">
|
||||
@@ -212,30 +212,30 @@ const DropZone = ({
|
||||
</div>
|
||||
) : (
|
||||
<div
|
||||
className="opacity-80 hover:opacity-100 duration-200 relative bg-bunker max-w-2xl w-full outline-dashed outline-gray-700 rounded-md outline-2 flex flex-col items-center justify-center pt-16 mb-16 px-4"
|
||||
className="relative mb-16 flex w-full max-w-2xl flex-col items-center justify-center rounded-md bg-bunker px-4 pt-16 opacity-80 outline-dashed outline-2 outline-gray-700 duration-200 hover:opacity-100"
|
||||
onDragEnter={handleDragEnter}
|
||||
onDragOver={handleDragOver}
|
||||
onDragLeave={handleDragLeave}
|
||||
onDrop={handleDrop}
|
||||
>
|
||||
<FontAwesomeIcon icon={faUpload} className="text-7xl mb-8" />
|
||||
<p className="">{t('common:drop-zone')}</p>
|
||||
<FontAwesomeIcon icon={faUpload} className="mb-8 text-7xl" />
|
||||
<p className="">{t('common.drop-zone')}</p>
|
||||
<input
|
||||
id="fileSelect"
|
||||
type="file"
|
||||
className="opacity-0 absolute w-full h-full"
|
||||
className="absolute h-full w-full opacity-0"
|
||||
accept=".txt,.env,.yml"
|
||||
onChange={handleFileSelect}
|
||||
/>
|
||||
<div className="flex flex-row w-full items-center justify-center mb-6 mt-5">
|
||||
<div className="border-t border-gray-700 w-1/5" />
|
||||
<p className="text-gray-400 text-xs mx-4">OR</p>
|
||||
<div className="border-t border-gray-700 w-1/5" />
|
||||
<div className="mb-6 mt-5 flex w-full flex-row items-center justify-center">
|
||||
<div className="w-1/5 border-t border-gray-700" />
|
||||
<p className="mx-4 text-xs text-gray-400">OR</p>
|
||||
<div className="w-1/5 border-t border-gray-700" />
|
||||
</div>
|
||||
<div className="z-10 mb-6">
|
||||
<Button
|
||||
color="mineshaft"
|
||||
text={String(t('dashboard:add-secret'))}
|
||||
text={String(t('dashboard.add-secret'))}
|
||||
onButtonPressed={createNewFile}
|
||||
size="md"
|
||||
/>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Fragment, useState } from 'react';
|
||||
import { useTranslation } from 'next-i18next';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { faShuffle } from '@fortawesome/free-solid-svg-icons';
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||
import { Menu, Transition } from '@headlessui/react';
|
||||
@@ -21,8 +21,8 @@ const GenerateSecretMenu = ({
|
||||
return (
|
||||
<Menu as="div" className="relative inline-block text-left">
|
||||
<div>
|
||||
<Menu.Button className="inline-flex w-full justify-center text-sm font-medium text-gray-200 rounded-md hover:bg-white/10 duration-200 focus:outline-none focus-visible:ring-2 focus-visible:ring-white focus-visible:ring-opacity-75">
|
||||
<div className="py-1 px-2 rounded-md bg-bunker-800 hover:bg-bunker-500">
|
||||
<Menu.Button className="inline-flex w-full justify-center rounded-md text-sm font-medium text-gray-200 duration-200 hover:bg-white/10 focus:outline-none focus-visible:ring-2 focus-visible:ring-white focus-visible:ring-opacity-75">
|
||||
<div className="rounded-md bg-bunker-800 py-1 px-2 hover:bg-bunker-500">
|
||||
<FontAwesomeIcon icon={faShuffle} className="text-bunker-300" />
|
||||
</div>
|
||||
</Menu.Button>
|
||||
@@ -36,7 +36,7 @@ const GenerateSecretMenu = ({
|
||||
leaveFrom="transform opacity-100 scale-100"
|
||||
leaveTo="transform opacity-0 scale-95"
|
||||
>
|
||||
<Menu.Items className="absolute z-50 drop-shadow-xl right-0 mt-0.5 w-[20rem] origin-top-right rounded-md bg-bunker border border-mineshaft-500 shadow-lg ring-1 ring-black ring-opacity-5 focus:outline-none px-1 py-1">
|
||||
<Menu.Items className="absolute right-0 z-50 mt-0.5 w-[20rem] origin-top-right rounded-md border border-mineshaft-500 bg-bunker px-1 py-1 shadow-lg ring-1 ring-black ring-opacity-5 drop-shadow-xl focus:outline-none">
|
||||
<div
|
||||
onKeyDown={() => null}
|
||||
role="button"
|
||||
@@ -55,20 +55,20 @@ const GenerateSecretMenu = ({
|
||||
);
|
||||
}
|
||||
}}
|
||||
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"
|
||||
className="relative flex w-full cursor-pointer select-none flex-row items-center justify-start rounded-md py-2 px-2 text-gray-400 duration-200 hover:bg-white/10 hover:text-gray-200"
|
||||
>
|
||||
<FontAwesomeIcon className="text-lg pl-1.5 pr-3" icon={faShuffle} />
|
||||
<div className="text-sm justify-between flex flex-row w-full">
|
||||
<p>{t('dashboard:sidebar.generate-random-hex')}</p>
|
||||
<p>{t('dashboard:sidebar.digits')}</p>
|
||||
<FontAwesomeIcon className="pl-1.5 pr-3 text-lg" icon={faShuffle} />
|
||||
<div className="flex w-full flex-row justify-between text-sm">
|
||||
<p>{t('dashboard.sidebar.generate-random-hex')}</p>
|
||||
<p>{t('dashboard.sidebar.digits')}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex flex-row absolute bottom-[0.4rem] right-[3.3rem] w-16 bg-bunker-800 border border-chicago-700 rounded-md text-bunker-200 ">
|
||||
<div className="absolute bottom-[0.4rem] right-[3.3rem] flex w-16 flex-row rounded-md border border-chicago-700 bg-bunker-800 text-bunker-200 ">
|
||||
<div
|
||||
onKeyDown={() => null}
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
className="m-0.5 px-1 cursor-pointer rounded-md hover:bg-chicago-700"
|
||||
className="m-0.5 cursor-pointer rounded-md px-1 hover:bg-chicago-700"
|
||||
onClick={() => {
|
||||
if (randomStringLength > 1) {
|
||||
setRandomStringLength(randomStringLength - 1);
|
||||
@@ -80,14 +80,14 @@ const GenerateSecretMenu = ({
|
||||
<input
|
||||
onChange={(e) => setRandomStringLength(parseInt(e.target.value, 10))}
|
||||
value={randomStringLength}
|
||||
className="text-center z-20 peer text-sm bg-transparent w-full outline-none"
|
||||
className="peer z-20 w-full bg-transparent text-center text-sm outline-none"
|
||||
spellCheck="false"
|
||||
/>
|
||||
<div
|
||||
onKeyDown={() => null}
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
className="m-0.5 px-1 pb-0.5 cursor-pointer rounded-md hover:bg-chicago-700"
|
||||
className="m-0.5 cursor-pointer rounded-md px-1 pb-0.5 hover:bg-chicago-700"
|
||||
onClick={() => {
|
||||
if (randomStringLength < 32) {
|
||||
setRandomStringLength(randomStringLength + 1);
|
||||
|
||||
@@ -101,7 +101,7 @@ const KeyPair = ({
|
||||
}`}
|
||||
>
|
||||
<div className="relative flex flex-row justify-between w-full mr-auto max-h-14 items-center">
|
||||
<div className="w-1/5 border-r border-mineshaft-600 flex flex-row items-center">
|
||||
<div className="w-[23%] border-r border-mineshaft-600 flex flex-row items-center">
|
||||
<div className='text-bunker-400 text-xs flex items-center justify-center w-14 h-10 cursor-default'>{keyPair.pos + 1}</div>
|
||||
<div className="flex items-center max-h-16 w-full">
|
||||
<DashboardInputField
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
/* eslint-disable react/no-unused-prop-types */
|
||||
import { useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import Image from 'next/image';
|
||||
import { useTranslation } from 'next-i18next';
|
||||
import { faXmark } from '@fortawesome/free-solid-svg-icons';
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||
|
||||
@@ -80,9 +80,9 @@ const SideBar = ({
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<div className="absolute border-l border-mineshaft-500 bg-bunker h-full w-full min-w-sm max-w-sm sticky top-0 right-0 z-[70] shadow-xl flex flex-col justify-between">
|
||||
<div className="min-w-sm absolute sticky top-0 right-0 z-[70] flex h-full w-full max-w-sm flex-col justify-between border-l border-mineshaft-500 bg-bunker shadow-xl">
|
||||
{isLoading ? (
|
||||
<div className="flex items-center justify-center h-full w-full">
|
||||
<div className="flex h-full w-full items-center justify-center">
|
||||
<Image
|
||||
src="/images/loading/loading.gif"
|
||||
height={60}
|
||||
@@ -91,9 +91,9 @@ const SideBar = ({
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
<div className="h-min overflow-y-auto w-full">
|
||||
<div className="flex flex-row px-4 py-3 border-b border-mineshaft-500 justify-between items-center">
|
||||
<p className="font-semibold text-lg text-bunker-200">{t('dashboard:sidebar.secret')}</p>
|
||||
<div className="h-min w-full overflow-y-auto">
|
||||
<div className="flex flex-row items-center justify-between border-b border-mineshaft-500 px-4 py-3">
|
||||
<p className="text-lg font-semibold text-bunker-200">{t('dashboard.sidebar.secret')}</p>
|
||||
<div
|
||||
onKeyDown={() => null}
|
||||
role="button"
|
||||
@@ -101,12 +101,12 @@ const SideBar = ({
|
||||
className="p-1"
|
||||
onClick={() => toggleSidebar('None')}
|
||||
>
|
||||
<FontAwesomeIcon icon={faXmark} className="w-4 h-4 text-bunker-300 cursor-pointer" />
|
||||
<FontAwesomeIcon icon={faXmark} className="h-4 w-4 cursor-pointer text-bunker-300" />
|
||||
</div>
|
||||
</div>
|
||||
<div className="mt-4 px-4 pointer-events-none">
|
||||
<p className="text-sm text-bunker-300">{t('dashboard:sidebar.key')}</p>
|
||||
<div className='rounded-md border overflow-hidden border-mineshaft-600 bg-white/5'>
|
||||
<div className="pointer-events-none mt-4 px-4">
|
||||
<p className="text-sm text-bunker-300">{t('dashboard.sidebar.key')}</p>
|
||||
<div className="overflow-hidden rounded-md border border-mineshaft-600 bg-white/5">
|
||||
<DashboardInputField
|
||||
onChangeHandler={modifyKey}
|
||||
type="varName"
|
||||
@@ -117,14 +117,14 @@ const SideBar = ({
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
{(data[0]?.value || data[0]?.value === "") ? (
|
||||
{data[0]?.value || data[0]?.value === '' ? (
|
||||
<div
|
||||
className={`relative mt-2 px-4 ${
|
||||
overrideEnabled && 'opacity-40 pointer-events-none'
|
||||
overrideEnabled && 'pointer-events-none opacity-40'
|
||||
} duration-200`}
|
||||
>
|
||||
<p className="text-sm text-bunker-300">{t('dashboard:sidebar.value')}</p>
|
||||
<div className='rounded-md border overflow-hidden border-mineshaft-600 bg-white/5'>
|
||||
<p className="text-sm text-bunker-300">{t('dashboard.sidebar.value')}</p>
|
||||
<div className="overflow-hidden rounded-md border border-mineshaft-600 bg-white/5">
|
||||
<DashboardInputField
|
||||
onChangeHandler={modifyValue}
|
||||
type="value"
|
||||
@@ -134,22 +134,22 @@ const SideBar = ({
|
||||
blurred
|
||||
/>
|
||||
</div>
|
||||
<div className="absolute bg-bunker-800 right-[1.07rem] top-[1.6rem] z-50">
|
||||
<div className="absolute right-[1.07rem] top-[1.6rem] z-50 bg-bunker-800">
|
||||
<GenerateSecretMenu modifyValue={modifyValue} id={data[0]?.id} />
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className="px-4 text-sm text-bunker-300 pt-4">
|
||||
<span className="py-0.5 px-1 rounded-md bg-primary-200/10 mr-1">
|
||||
{t('common:note')}:
|
||||
<div className="px-4 pt-4 text-sm text-bunker-300">
|
||||
<span className="mr-1 rounded-md bg-primary-200/10 py-0.5 px-1">
|
||||
{t('common.note')}:
|
||||
</span>
|
||||
{t('dashboard:sidebar.personal-explanation')}
|
||||
{t('dashboard.sidebar.personal-explanation')}
|
||||
</div>
|
||||
)}
|
||||
<div className="mt-4 px-4">
|
||||
{(data[0]?.value || data[0]?.value === "") && (
|
||||
<div className="flex flex-row items-center justify-between my-2 pl-1 pr-2">
|
||||
<p className="text-sm text-bunker-300">{t('dashboard:sidebar.override')}</p>
|
||||
{(data[0]?.value || data[0]?.value === '') && (
|
||||
<div className="my-2 flex flex-row items-center justify-between pl-1 pr-2">
|
||||
<p className="text-sm text-bunker-300">{t('dashboard.sidebar.override')}</p>
|
||||
<Toggle
|
||||
enabled={overrideEnabled}
|
||||
setEnabled={setOverrideEnabled}
|
||||
@@ -160,10 +160,10 @@ const SideBar = ({
|
||||
)}
|
||||
<div
|
||||
className={`relative ${
|
||||
!overrideEnabled && 'opacity-40 pointer-events-none'
|
||||
!overrideEnabled && 'pointer-events-none opacity-40'
|
||||
} duration-200`}
|
||||
>
|
||||
<div className='rounded-md border overflow-hidden border-mineshaft-600 bg-white/5'>
|
||||
<div className="overflow-hidden rounded-md border border-mineshaft-600 bg-white/5">
|
||||
<DashboardInputField
|
||||
onChangeHandler={modifyValueOverride}
|
||||
type="value"
|
||||
@@ -179,14 +179,10 @@ const SideBar = ({
|
||||
</div>
|
||||
</div>
|
||||
<SecretVersionList secretId={data[0]?.id} />
|
||||
<CommentField
|
||||
comment={data[0]?.comment}
|
||||
modifyComment={modifyComment}
|
||||
id={data[0]?.id}
|
||||
/>
|
||||
<CommentField comment={data[0]?.comment} modifyComment={modifyComment} id={data[0]?.id} />
|
||||
</div>
|
||||
)}
|
||||
<div className="mt-full w-96 mt-4 mb-4 flex max-w-sm flex-col justify-start space-y-2 px-4">
|
||||
<div className="mt-full mt-4 mb-4 flex w-96 max-w-sm flex-col justify-start space-y-2 px-4">
|
||||
<div>
|
||||
<Button
|
||||
text="Compare secret across environments"
|
||||
@@ -205,7 +201,7 @@ const SideBar = ({
|
||||
</div>
|
||||
<div className="flex">
|
||||
<Button
|
||||
text={String(t('common:save-changes'))}
|
||||
text={String(t('common.save-changes'))}
|
||||
onButtonPressed={savePush}
|
||||
color="primary"
|
||||
size="md"
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import Image from 'next/image';
|
||||
import { faCheck, faX } from '@fortawesome/free-solid-svg-icons';
|
||||
import { faCheck, faXmark } from '@fortawesome/free-solid-svg-icons';
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||
|
||||
import deleteIntegrationAuth from '../../pages/api/integrations/DeleteIntegrationAuth';
|
||||
@@ -44,9 +44,9 @@ const CloudIntegration = ({
|
||||
tabIndex={0}
|
||||
className={`relative ${
|
||||
cloudIntegrationOption.isAvailable
|
||||
? 'cursor-pointer duration-200 hover:bg-white/10'
|
||||
? 'cursor-pointer duration-200 hover:bg-mineshaft-700'
|
||||
: 'opacity-50'
|
||||
} flex h-32 flex-row items-center rounded-md bg-white/5 p-4`}
|
||||
} flex h-32 flex-row items-center rounded-md bg-mineshaft-800 border border-mineshaft-600 p-4`}
|
||||
onClick={() => {
|
||||
if (!cloudIntegrationOption.isAvailable) return;
|
||||
setSelectedIntegrationOption(cloudIntegrationOption);
|
||||
@@ -95,12 +95,12 @@ const CloudIntegration = ({
|
||||
integrationAuth: deletedIntegrationAuth
|
||||
});
|
||||
}}
|
||||
className="flex w-max cursor-pointer flex-row items-center rounded-b-md bg-red py-0.5 px-2 text-xs opacity-0 duration-200 group-hover:opacity-100"
|
||||
className="flex w-max cursor-pointer flex-row items-center rounded-bl-md bg-red py-0.5 px-2 text-xs opacity-30 duration-200 group-hover:opacity-100"
|
||||
>
|
||||
<FontAwesomeIcon icon={faX} className="mr-2 py-px text-xs" />
|
||||
<FontAwesomeIcon icon={faXmark} className="mr-2 text-xs" />
|
||||
Revoke
|
||||
</div>
|
||||
<div className="flex w-max flex-row items-center rounded-bl-md rounded-tr-md bg-primary py-0.5 px-2 text-xs text-black opacity-90 duration-200 group-hover:opacity-100">
|
||||
<div className="flex w-max flex-row items-center rounded-tr-md bg-primary py-0.5 px-2 text-xs text-black opacity-70 duration-200 group-hover:opacity-100">
|
||||
<FontAwesomeIcon icon={faCheck} className="mr-2 text-xs" />
|
||||
Authorized
|
||||
</div>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useTranslation } from 'next-i18next';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import CloudIntegration from './CloudIntegration';
|
||||
|
||||
@@ -40,17 +40,11 @@ const CloudIntegrationSection = ({
|
||||
|
||||
return (
|
||||
<>
|
||||
<div
|
||||
className="flex flex-col justify-between items-start m-4 mt-7 text-xl max-w-5xl px-2"
|
||||
>
|
||||
<h1 className='font-semibold text-3xl'>
|
||||
{t('integrations:cloud-integrations')}
|
||||
</h1>
|
||||
<p className='text-base text-gray-400'>
|
||||
{t('integrations:click-to-start')}
|
||||
</p>
|
||||
<div className="m-4 mt-7 flex max-w-5xl flex-col items-start justify-between px-2 text-xl">
|
||||
<h1 className="text-3xl font-semibold">{t('integrations.cloud-integrations')}</h1>
|
||||
<p className="text-base text-gray-400">{t('integrations.click-to-start')}</p>
|
||||
</div>
|
||||
<div className='grid gap-4 grid-cols-4 grid-rows-2 mx-6 max-w-5xl'>
|
||||
<div className="mx-6 grid max-w-5xl grid-cols-4 grid-rows-2 gap-4">
|
||||
{cloudIntegrationOptions.map((cloudIntegrationOption) => (
|
||||
<CloudIntegration
|
||||
cloudIntegrationOption={cloudIntegrationOption}
|
||||
|
||||
@@ -12,10 +12,10 @@ const FrameworkIntegration = ({ framework }: { framework: Framework }) => (
|
||||
href={framework.docsLink}
|
||||
rel="noopener noreferrer"
|
||||
target="_blank"
|
||||
className="relative flex flex-row justify-center bg-bunker-500 hover:bg-gradient-to-tr duration-200 h-32 rounded-md p-0.5 items-center cursor-pointer"
|
||||
className="relative flex flex-row justify-center duration-200 h-32 rounded-md p-0.5 items-center cursor-pointer"
|
||||
>
|
||||
<div
|
||||
className={`hover:bg-white/10 cursor-pointer font-semibold bg-bunker-500 flex flex-col items-center justify-center h-full w-full rounded-md text-gray-300 group-hover:text-gray-200 duration-200 ${
|
||||
className={`hover:bg-mineshaft-700 cursor-pointer font-semibold bg-mineshaft-800 border border-mineshaft-600 flex flex-col items-center justify-center h-full w-full rounded-md text-gray-300 group-hover:text-gray-200 duration-200 ${
|
||||
framework?.name?.split(' ').length > 1 ? 'text-sm px-1' : 'text-xl px-2'
|
||||
} text-center w-full max-w-xs`}
|
||||
>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useTranslation } from 'next-i18next';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import FrameworkIntegration from './FrameworkIntegration';
|
||||
|
||||
@@ -19,15 +19,11 @@ const FrameworkIntegrationSection = ({ frameworks }: Props) => {
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className='flex flex-col justify-between items-start mx-4 mt-12 mb-4 text-xl max-w-5xl px-2'>
|
||||
<h1 className='font-semibold text-3xl'>
|
||||
{t('integrations:framework-integrations')}
|
||||
</h1>
|
||||
<p className='text-base text-gray-400'>
|
||||
{t('integrations:click-to-setup')}
|
||||
</p>
|
||||
<div className="mx-4 mt-12 mb-4 flex max-w-5xl flex-col items-start justify-between px-2 text-xl">
|
||||
<h1 className="text-3xl font-semibold">{t('integrations.framework-integrations')}</h1>
|
||||
<p className="text-base text-gray-400">{t('integrations.click-to-setup')}</p>
|
||||
</div>
|
||||
<div className='grid gap-4 grid-cols-7 grid-rows-2 mx-6 mt-4 max-w-5xl'>
|
||||
<div className="mx-6 mt-4 grid max-w-5xl grid-cols-7 grid-rows-2 gap-4">
|
||||
{frameworks.map((framework) => (
|
||||
<FrameworkIntegration
|
||||
framework={framework}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
/* 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 { faArrowRight, faCheck, faXmark } from '@fortawesome/free-solid-svg-icons';
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||
// TODO: This needs to be moved from public folder
|
||||
import { contextNetlifyMapping, integrationSlugNameMapping, reverseContextNetlifyMapping } from 'public/data/frequentConstants';
|
||||
@@ -45,6 +45,7 @@ type Props = {
|
||||
handleDeleteIntegration: (args: { integration: Integration }) => void;
|
||||
};
|
||||
|
||||
// TODO: refactor
|
||||
const IntegrationTile = ({
|
||||
integration,
|
||||
integrations,
|
||||
@@ -55,7 +56,6 @@ const IntegrationTile = ({
|
||||
handleDeleteIntegration
|
||||
}: Props) => {
|
||||
|
||||
// set initial environment. This find will only execute when component is mounting
|
||||
const [integrationEnvironment, setIntegrationEnvironment] = useState<Props['environments'][0]>(
|
||||
environments.find(({ slug }) => slug === integration?.environment) || {
|
||||
name: '',
|
||||
@@ -176,6 +176,32 @@ const IntegrationTile = ({
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
case 'railway':
|
||||
return (
|
||||
<div>
|
||||
<div className="mb-2 text-xs font-semibold text-gray-400">ENVIRONMENT</div>
|
||||
<ListBox
|
||||
data={
|
||||
!integration.isActive
|
||||
? ['Production', 'Deploy previews', 'Branch deploys', 'Local development']
|
||||
: null
|
||||
}
|
||||
isSelected={integration.targetEnvironment}
|
||||
onChange={setIntegrationTargetEnvironment}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
case 'gitlab':
|
||||
return (
|
||||
<div>
|
||||
<div className="mb-2 text-xs font-semibold text-gray-400">ENVIRONMENT</div>
|
||||
<ListBox
|
||||
data={null}
|
||||
isSelected={integration.targetEnvironment}
|
||||
onChange={setIntegrationTargetEnvironment}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
default:
|
||||
return <div />;
|
||||
}
|
||||
@@ -186,10 +212,12 @@ const IntegrationTile = ({
|
||||
return <div />;
|
||||
};
|
||||
|
||||
if (!integrationApp) return <div />;
|
||||
if (!integrationApp && integration.integration !== "checkly") return <div />;
|
||||
|
||||
const isSelected = integration.integration === 'hashicorp-vault' ? `${integration.app} - path: ${integration.path}` : integrationApp;
|
||||
|
||||
return (
|
||||
<div className="mx-6 mb-8 flex max-w-5xl justify-between rounded-md bg-white/5 p-6">
|
||||
<div className="mx-6 mb-8 flex max-w-5xl justify-between rounded-md bg-mineshaft-800 border border-mineshaft-600 p-6">
|
||||
<div className="flex">
|
||||
<div>
|
||||
<p className="mb-2 text-xs font-semibold text-gray-400">ENVIRONMENT</p>
|
||||
@@ -212,27 +240,29 @@ const IntegrationTile = ({
|
||||
</div>
|
||||
<div className="mr-2">
|
||||
<p className="text-gray-400 text-xs font-semibold mb-2">INTEGRATION</p>
|
||||
<div className="py-2.5 bg-white/[.07] rounded-md pl-4 pr-10 text-sm font-semibold text-gray-300">
|
||||
<div className="py-2.5 bg-white/[.07] rounded-md pl-4 pr-10 text-sm font-semibold text-gray-300 cursor-default">
|
||||
{/* {integration.integration.charAt(0).toUpperCase() + integration.integration.slice(1)} */}
|
||||
{integrationSlugNameMapping[integration.integration]}
|
||||
</div>
|
||||
</div>
|
||||
<div className="mr-2">
|
||||
<div className="mb-2 text-xs font-semibold text-gray-400">APP</div>
|
||||
<ListBox
|
||||
{integrationApp ? <div title={integrationApp}>
|
||||
<ListBox
|
||||
data={!integration.isActive ? apps.map((app) => app.name) : null}
|
||||
isSelected={integrationApp}
|
||||
isSelected={isSelected}
|
||||
onChange={(app) => {
|
||||
setIntegrationApp(app);
|
||||
}}
|
||||
/>
|
||||
</div> : <div className='w-52 h-10 rounded-md bg-mineshaft-600 animate-pulse px-4 font-bold py-2'>-</div>}
|
||||
</div>
|
||||
{renderIntegrationSpecificParams(integration)}
|
||||
</div>
|
||||
<div className="flex items-end">
|
||||
<div className="flex items-end cursor-default">
|
||||
{integration.isActive ? (
|
||||
<div className="flex max-w-5xl flex-row items-center rounded-md bg-white/5 p-2 px-4">
|
||||
<FontAwesomeIcon icon={faRotate} className="mr-2.5 animate-spin text-lg text-primary" />
|
||||
<div className="flex max-w-5xl flex-row items-center rounded-md bg-mineshaft-600 p-[0.44rem] px-4 border border-mineshaft-500">
|
||||
<FontAwesomeIcon icon={faCheck} className="mr-2.5 text-lg text-primary" />
|
||||
<div className="font-semibold text-gray-300">In Sync</div>
|
||||
</div>
|
||||
) : (
|
||||
@@ -243,7 +273,7 @@ const IntegrationTile = ({
|
||||
size="md"
|
||||
/>
|
||||
)}
|
||||
<div className="ml-2 opacity-50 duration-200 hover:opacity-100">
|
||||
<div className="ml-2 opacity-80 duration-200 hover:opacity-100">
|
||||
<Button
|
||||
onButtonPressed={() =>
|
||||
handleDeleteIntegration({
|
||||
@@ -252,7 +282,7 @@ const IntegrationTile = ({
|
||||
}
|
||||
color="red"
|
||||
size="icon-md"
|
||||
icon={faX}
|
||||
icon={faXmark}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -37,7 +37,7 @@ const ProjectIntegrationSection = ({
|
||||
<div className="mb-12">
|
||||
<div className="mx-4 mb-4 mt-6 flex max-w-5xl flex-col items-start justify-between px-2 text-xl">
|
||||
<h1 className="text-3xl font-semibold">Current Integrations</h1>
|
||||
<p className="text-base text-gray-400">Manage integrations with third-party services.</p>
|
||||
<p className="text-base text-bunker-300">Manage integrations with third-party services.</p>
|
||||
</div>
|
||||
{integrations.map((integration: Integration) => {
|
||||
return (
|
||||
|
||||
@@ -0,0 +1,141 @@
|
||||
import { useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import Link from 'next/link';
|
||||
import { useRouter } from 'next/router';
|
||||
|
||||
import attemptLogin from '@app/components/utilities/attemptLogin';
|
||||
|
||||
import Error from '../basic/Error';
|
||||
// import { faGoogle } from '@fortawesome/free-brands-svg-icons';
|
||||
// import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||
import { Button, Input } from '../v2';
|
||||
|
||||
export default function InitialLoginStep({
|
||||
setStep,
|
||||
email,
|
||||
setEmail,
|
||||
password,
|
||||
setPassword,
|
||||
}: {
|
||||
setStep: (step: number) => void;
|
||||
email: string;
|
||||
setEmail: (email: string) => void;
|
||||
password: string;
|
||||
setPassword: (password: string) => void;
|
||||
}) {
|
||||
|
||||
const router = useRouter();
|
||||
const { t } = useTranslation();
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [loginError, setLoginError] = useState(false);
|
||||
|
||||
const handleLogin = async () => {
|
||||
try {
|
||||
if (!email || !password) {
|
||||
return;
|
||||
}
|
||||
|
||||
setIsLoading(true);
|
||||
const isLoginSuccessful = await attemptLogin({
|
||||
email,
|
||||
password,
|
||||
});
|
||||
if (isLoginSuccessful && isLoginSuccessful.success) {
|
||||
// case: login was successful
|
||||
|
||||
if (isLoginSuccessful.mfaEnabled) {
|
||||
// case: login requires MFA step
|
||||
setStep(2);
|
||||
setIsLoading(false);
|
||||
return;
|
||||
}
|
||||
|
||||
// case: login does not require MFA step
|
||||
router.push(`/dashboard/${localStorage.getItem('projectData.id')}`);
|
||||
}
|
||||
|
||||
} catch (err) {
|
||||
setLoginError(true);
|
||||
}
|
||||
|
||||
setIsLoading(false);
|
||||
}
|
||||
|
||||
return <div className='flex flex-col mx-auto w-full justify-center items-center'>
|
||||
<h1 className='text-xl font-medium text-transparent bg-clip-text bg-gradient-to-b from-white to-bunker-200 text-center mb-8' >Login to Infisical</h1>
|
||||
{/* <div className='lg:w-1/6 w-1/4 min-w-[20rem] rounded-md'>
|
||||
<Button
|
||||
colorSchema="primary"
|
||||
variant="solid"
|
||||
onClick={() => {
|
||||
window.open('/api/v1/auth/redirect/google')
|
||||
}}
|
||||
leftIcon={<FontAwesomeIcon icon={faGoogle} className="mr-1" />}
|
||||
className="h-14 w-full mx-0"
|
||||
>
|
||||
{t('login.continue-with-google')}
|
||||
</Button>
|
||||
</div> */}
|
||||
<div className="relative md:px-1.5 flex items-center justify-center lg:w-1/6 w-1/4 min-w-[20rem] md:min-w-[22rem] mx-auto w-full rounded-lg max-h-24 md:max-h-28">
|
||||
<div className="flex items-center justify-center w-full md:px-2 md:py-1 rounded-lg max-h-24 md:max-h-28">
|
||||
<Input
|
||||
value={email}
|
||||
onChange={(e) => setEmail(e.target.value)}
|
||||
type="email"
|
||||
placeholder="Enter your email..."
|
||||
isRequired
|
||||
autoComplete="username"
|
||||
className="h-12"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="relative pt-2 md:pt-0 md:px-1.5 flex items-center justify-center lg:w-1/6 w-1/4 min-w-[20rem] md:min-w-[22rem] mx-auto w-full rounded-lg max-h-24 md:max-h-28">
|
||||
<div className="flex items-center justify-center w-full md:p-2 rounded-lg max-h-24 md:max-h-28">
|
||||
<Input
|
||||
value={password}
|
||||
onChange={(e) => setPassword(e.target.value)}
|
||||
type="password"
|
||||
placeholder="Enter your password..."
|
||||
isRequired
|
||||
autoComplete="current-password"
|
||||
id="current-password"
|
||||
className="h-12 select:-webkit-autofill:focus"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
{!isLoading && loginError && <Error text={t('login.error-login') ?? ''} />}
|
||||
<div className='lg:w-1/6 w-1/4 min-w-[21.2rem] md:min-w-[20.1rem] text-center rounded-md mt-4'>
|
||||
<Button
|
||||
onClick={async () => handleLogin()}
|
||||
size="sm"
|
||||
isFullWidth
|
||||
className='h-12'
|
||||
colorSchema="primary"
|
||||
variant="solid"
|
||||
isLoading={isLoading}
|
||||
> Login </Button>
|
||||
</div>
|
||||
<div className='lg:w-1/6 w-1/4 min-w-[20rem] flex flex-row items-center mt-4 py-2'>
|
||||
<div className='w-1/2 border-t border-mineshaft-500'/>
|
||||
<span className='px-4 text-sm text-bunker-400'>or</span>
|
||||
<div className='w-1/2 border-t border-mineshaft-500'/>
|
||||
</div>
|
||||
<div className='lg:w-1/6 w-1/4 min-w-[20rem] text-center rounded-md mt-4'>
|
||||
<Button
|
||||
colorSchema="primary"
|
||||
variant="outline_bg"
|
||||
onClick={() => router.push("/saml-sso")}
|
||||
isFullWidth
|
||||
className="h-14 w-full mx-0"
|
||||
>
|
||||
Continue with SAML SSO
|
||||
</Button>
|
||||
</div>
|
||||
<div className="mt-6 text-bunker-400 text-sm flex flex-row">
|
||||
<span className="mr-1">Don't have an acount yet?</span>
|
||||
<Link href="/signup">
|
||||
<span className='hover:underline hover:underline-offset-4 hover:decoration-primary-700 hover:text-bunker-200 duration-200 cursor-pointer'>{t('login.create-account')}</span>
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
@@ -1,15 +1,14 @@
|
||||
import React, { useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import Link from 'next/link';
|
||||
import { useRouter } from 'next/router';
|
||||
import { useTranslation } from 'next-i18next';
|
||||
import { faWarning } from '@fortawesome/free-solid-svg-icons';
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||
|
||||
import Button from '@app/components/basic/buttons/Button';
|
||||
import Error from '@app/components/basic/Error';
|
||||
import InputField from '@app/components/basic/InputField';
|
||||
import attemptLogin from '@app/components/utilities/attemptLogin';
|
||||
import { getTranslatedStaticProps } from '@app/components/utilities/withTranslateProps';
|
||||
|
||||
import { Button, Input } from '../v2';
|
||||
|
||||
/**
|
||||
* 1st step of login - user enters their username and password
|
||||
@@ -21,127 +20,114 @@ import { getTranslatedStaticProps } from '@app/components/utilities/withTranslat
|
||||
* @param {Function} obj.setStep - function to set the login flow step
|
||||
* @returns
|
||||
*/
|
||||
export default function LoginStep ({
|
||||
email,
|
||||
setEmail,
|
||||
password,
|
||||
setPassword,
|
||||
setStep
|
||||
export default function LoginStep({
|
||||
email,
|
||||
setEmail,
|
||||
password,
|
||||
setPassword,
|
||||
setStep
|
||||
}: {
|
||||
email: string;
|
||||
setEmail: (email: string) => void;
|
||||
password: string;
|
||||
setPassword: (password: string) => void;
|
||||
setStep: (step: number) => void;
|
||||
email: string;
|
||||
setEmail: (email: string) => void;
|
||||
password: string;
|
||||
setPassword: (password: string) => void;
|
||||
setStep: (step: number) => void;
|
||||
}) {
|
||||
const router = useRouter();
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [loginError, setLoginError] = useState(false);
|
||||
const router = useRouter();
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [loginError, setLoginError] = useState(false);
|
||||
|
||||
const { t } = useTranslation();
|
||||
const { t } = useTranslation();
|
||||
|
||||
const handleLogin = async () => {
|
||||
try {
|
||||
if (!email || !password) {
|
||||
return;
|
||||
}
|
||||
|
||||
setIsLoading(true);
|
||||
const isLoginSuccessful = await attemptLogin(email, password);
|
||||
if (isLoginSuccessful && isLoginSuccessful.success) {
|
||||
// case: login was successful
|
||||
const handleLogin = async () => {
|
||||
try {
|
||||
if (!email || !password) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (isLoginSuccessful.mfaEnabled) {
|
||||
// case: login requires MFA step
|
||||
setStep(2);
|
||||
setIsLoading(false);
|
||||
return;
|
||||
}
|
||||
setIsLoading(true);
|
||||
const isLoginSuccessful = await attemptLogin({
|
||||
email,
|
||||
password,
|
||||
});
|
||||
if (isLoginSuccessful && isLoginSuccessful.success) {
|
||||
// case: login was successful
|
||||
|
||||
// case: login does not require MFA step
|
||||
router.push(`/dashboard/${localStorage.getItem('projectData.id')}`);
|
||||
}
|
||||
|
||||
} catch (err) {
|
||||
setLoginError(true);
|
||||
if (isLoginSuccessful.mfaEnabled) {
|
||||
// case: login requires MFA step
|
||||
setStep(2);
|
||||
setIsLoading(false);
|
||||
return;
|
||||
}
|
||||
|
||||
setIsLoading(false);
|
||||
|
||||
// case: login does not require MFA step
|
||||
router.push(`/dashboard/${localStorage.getItem('projectData.id')}`);
|
||||
}
|
||||
|
||||
} catch (err) {
|
||||
setLoginError(true);
|
||||
}
|
||||
|
||||
return (
|
||||
<form onSubmit={(e) => e.preventDefault()}>
|
||||
<div className="bg-bunker w-full max-w-md mx-auto h-7/12 py-4 pt-8 px-6 rounded-xl drop-shadow-xl">
|
||||
<p className="text-3xl w-max mx-auto flex justify-center font-semibold text-bunker-100 mb-6">
|
||||
{t('login:login')}
|
||||
</p>
|
||||
<div className="flex items-center justify-center w-full md:p-2 rounded-lg mt-4 md:mt-0 max-h-24 md:max-h-28">
|
||||
<InputField
|
||||
label={t('common:email')}
|
||||
onChangeHandler={setEmail}
|
||||
type="email"
|
||||
value={email}
|
||||
placeholder=""
|
||||
isRequired
|
||||
autoComplete="username"
|
||||
/>
|
||||
</div>
|
||||
<div className="relative flex items-center justify-center w-full md:p-2 rounded-lg md:mt-2 mt-6 max-h-24 md:max-h-28">
|
||||
<InputField
|
||||
label={t('common:password')}
|
||||
onChangeHandler={setPassword}
|
||||
type="password"
|
||||
value={password}
|
||||
placeholder=""
|
||||
isRequired
|
||||
autoComplete="current-password"
|
||||
id="current-password"
|
||||
/>
|
||||
<div className="absolute top-2 right-3 text-primary-700 hover:text-primary duration-200 cursor-pointer text-sm">
|
||||
<Link href="/verify-email">
|
||||
<button
|
||||
type="button"
|
||||
className="text-primary-700 hover:text-primary duration-200 font-normal text-sm underline-offset-4 ml-1.5"
|
||||
>
|
||||
{t('login:forgot-password')}
|
||||
</button>
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
{!isLoading && loginError && <Error text={t('login:error-login') ?? ''} />}
|
||||
<div className="flex flex-col items-center justify-center w-full md:p-2 max-h-20 max-w-md mt-4 mx-auto text-sm">
|
||||
<div className="text-l mt-6 m-8 px-8 py-3 text-lg">
|
||||
<Button
|
||||
type="submit"
|
||||
text={t('login:login') ?? ''}
|
||||
onButtonPressed={async () => handleLogin()}
|
||||
loading={isLoading}
|
||||
size="lg"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{false && (
|
||||
<div className="w-full p-2 flex flex-row items-center bg-white/10 text-gray-300 rounded-md max-w-md mx-auto mt-4">
|
||||
<FontAwesomeIcon icon={faWarning} className="ml-2 mr-6 text-6xl" />
|
||||
{t('common:maintenance-alert')}
|
||||
</div>
|
||||
)}
|
||||
<div className="flex flex-row items-center justify-center md:pb-4 mt-4">
|
||||
<p className="text-sm flex justify-center text-gray-400 w-max">
|
||||
{t('login:need-account')}
|
||||
</p>
|
||||
<Link href="/signup">
|
||||
<button
|
||||
type="button"
|
||||
className="text-primary-700 hover:text-primary duration-200 font-normal text-sm underline-offset-4 ml-1.5"
|
||||
>
|
||||
{t('login:create-account')}
|
||||
</button>
|
||||
</Link>
|
||||
</div>
|
||||
</form>
|
||||
);
|
||||
}
|
||||
setIsLoading(false);
|
||||
}
|
||||
|
||||
export const getStaticProps = getTranslatedStaticProps(['auth', 'login']);
|
||||
return (
|
||||
<form onSubmit={(e) => e.preventDefault()}>
|
||||
<div className="w-full mx-auto h-full px-6">
|
||||
<p className="text-xl w-max mx-auto flex justify-center text-transparent bg-clip-text bg-gradient-to-b from-white to-bunker-200 mb-6">
|
||||
{t('login.login')}
|
||||
</p>
|
||||
<div className="flex items-center justify-center lg:w-1/6 w-1/4 min-w-[22rem] mx-auto w-full md:p-2 rounded-lg mt-4 md:mt-0 max-h-24 md:max-h-28">
|
||||
<Input
|
||||
value={email}
|
||||
onChange={(e) => setEmail(e.target.value)}
|
||||
type="email"
|
||||
placeholder="Enter your email..."
|
||||
isRequired
|
||||
autoComplete="username"
|
||||
className="h-12"
|
||||
/>
|
||||
</div>
|
||||
<div className="relative flex items-center justify-center lg:w-1/6 w-1/4 min-w-[22rem] mx-auto w-full rounded-lg max-h-24 md:max-h-28">
|
||||
<div className="flex items-center justify-center w-full md:p-2 rounded-lg max-h-24 md:max-h-28">
|
||||
<Input
|
||||
value={password}
|
||||
onChange={(e) => setPassword(e.target.value)}
|
||||
type="password"
|
||||
placeholder="Enter your password..."
|
||||
isRequired
|
||||
autoComplete="current-password"
|
||||
id="current-password"
|
||||
className="h-12"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
{!isLoading && loginError && <Error text={t('login.error-login') ?? ''} />}
|
||||
<div className="flex flex-col items-center justify-center lg:w-1/6 w-1/4 min-w-[22rem] px-2 mt-4 max-w-xs md:max-w-md mx-auto text-sm text-center md:text-left">
|
||||
<div className="text-l py-1 text-lg w-full">
|
||||
<Button
|
||||
onClick={async () => handleLogin()}
|
||||
size="sm"
|
||||
isFullWidth
|
||||
className='h-14'
|
||||
colorSchema="primary"
|
||||
variant="outline_bg"
|
||||
isLoading={isLoading}
|
||||
> {String(t('login.login'))} </Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="text-bunker-400 text-sm flex flex-row w-max mx-auto">
|
||||
<Link href="/verify-email">
|
||||
<span className='hover:underline hover:underline-offset-4 hover:decoration-primary-700 hover:text-bunker-200 duration-200 cursor-pointer'>{t('login.forgot-password')}</span>
|
||||
</Link>
|
||||
</div>
|
||||
{false && (
|
||||
<div className="w-full p-2 flex flex-row items-center bg-white/10 text-gray-300 rounded-md max-w-md mx-auto mt-4">
|
||||
<FontAwesomeIcon icon={faWarning} className="ml-2 mr-6 text-6xl" />
|
||||
{t('common.maintenance-alert')}
|
||||
</div>
|
||||
)}
|
||||
</form>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,15 +1,14 @@
|
||||
/* eslint-disable react/jsx-props-no-spreading */
|
||||
import React, { useState } from 'react';
|
||||
import ReactCodeInput from 'react-code-input';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useRouter } from 'next/router';
|
||||
import { useTranslation } from 'next-i18next';
|
||||
|
||||
import attemptLoginMfa from '@app/components/utilities/attemptLoginMfa';
|
||||
import { getTranslatedStaticProps } from '@app/components/utilities/withTranslateProps';
|
||||
import { useSendMfaToken } from '@app/hooks/api/auth';
|
||||
|
||||
import Button from '../basic/buttons/Button';
|
||||
import Error from '../basic/Error';
|
||||
import { Button } from '../v2';
|
||||
|
||||
// The style for the verification code input
|
||||
const props = {
|
||||
@@ -17,10 +16,10 @@ const props = {
|
||||
fontFamily: 'monospace',
|
||||
margin: '4px',
|
||||
MozAppearance: 'textfield',
|
||||
width: '55px',
|
||||
width: '48px',
|
||||
borderRadius: '5px',
|
||||
fontSize: '24px',
|
||||
height: '55px',
|
||||
height: '48px',
|
||||
paddingLeft: '7',
|
||||
backgroundColor: '#0d1117',
|
||||
color: 'white',
|
||||
@@ -37,10 +36,10 @@ interface VerifyMfaTokenError {
|
||||
context: {
|
||||
code: string;
|
||||
triesLeft: number;
|
||||
}
|
||||
},
|
||||
};
|
||||
};
|
||||
status: number;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -53,10 +52,12 @@ interface VerifyMfaTokenError {
|
||||
*/
|
||||
export default function MFAStep({
|
||||
email,
|
||||
password
|
||||
password,
|
||||
providerAuthToken,
|
||||
}: {
|
||||
email: string;
|
||||
password: string;
|
||||
providerAuthToken?: string;
|
||||
}): JSX.Element {
|
||||
const router = useRouter();
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
@@ -73,11 +74,12 @@ export default function MFAStep({
|
||||
if (mfaCode.length !== 6) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
setIsLoading(true);
|
||||
const isLoginSuccessful = await attemptLoginMfa({
|
||||
email,
|
||||
password,
|
||||
providerAuthToken,
|
||||
mfaToken: mfaCode
|
||||
});
|
||||
|
||||
@@ -85,10 +87,9 @@ export default function MFAStep({
|
||||
setIsLoading(false);
|
||||
router.push(`/dashboard/${localStorage.getItem('projectData.id')}`);
|
||||
}
|
||||
|
||||
} catch (err) {
|
||||
const error = err as VerifyMfaTokenError;
|
||||
|
||||
|
||||
if (error?.response?.status === 500) {
|
||||
window.location.reload();
|
||||
} else if (error?.response?.data?.context?.triesLeft) {
|
||||
@@ -100,8 +101,8 @@ export default function MFAStep({
|
||||
|
||||
setIsLoading(false);
|
||||
}
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
const handleResendMfaCode = async () => {
|
||||
try {
|
||||
setIsLoadingResend(true);
|
||||
@@ -111,13 +112,13 @@ export default function MFAStep({
|
||||
console.error(err);
|
||||
setIsLoadingResend(false);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<form className="bg-bunker w-max mx-auto h-7/12 pt-10 pb-4 px-8 rounded-xl drop-shadow-xl mb-64 md:mb-16">
|
||||
<p className="text-l flex justify-center text-bunker-300">{t('mfa:step2-message')}</p>
|
||||
<p className="text-l flex justify-center font-semibold my-2 text-bunker-300">{email} </p>
|
||||
<div className="hidden md:block">
|
||||
<form className="mx-auto w-max md:px-8 pb-4 pt-4 md:mb-16">
|
||||
<p className="text-l flex justify-center text-bunker-300">{t('mfa.step2-message')}</p>
|
||||
<p className="text-l my-1 flex justify-center font-semibold text-bunker-300">{email} </p>
|
||||
<div className="hidden md:block w-max min-w-[20rem] mx-auto">
|
||||
<ReactCodeInput
|
||||
name=""
|
||||
inputMode="tel"
|
||||
@@ -128,35 +129,47 @@ export default function MFAStep({
|
||||
className="mt-6 mb-2"
|
||||
/>
|
||||
</div>
|
||||
{typeof triesLeft === 'number' && <Error text={`${t('mfa:step2-code-error')} ${triesLeft}`} />}
|
||||
<div className="flex max-w-max min-w-28 flex-col items-center justify-center md:p-2 max-h-24 mx-auto text-lg px-4 mt-4 mb-2">
|
||||
<Button
|
||||
text={t('mfa:verify') ?? ''}
|
||||
onButtonPressed={() => handleLoginMfa()}
|
||||
size="lg"
|
||||
<div className="block md:hidden w-max mt-4 mx-auto">
|
||||
<ReactCodeInput
|
||||
name=""
|
||||
inputMode="tel"
|
||||
type="text"
|
||||
fields={6}
|
||||
onChange={setMfaCode}
|
||||
{...props}
|
||||
className="mt-2 mb-2"
|
||||
/>
|
||||
</div>
|
||||
{typeof triesLeft === 'number' && (
|
||||
<Error text={`${t('mfa.step2-code-error')} ${triesLeft}`} />
|
||||
)}
|
||||
<div className="flex flex-col mt-6 items-center justify-center lg:w-[19%] w-1/4 min-w-[20rem] mt-2 max-w-xs md:max-w-md mx-auto text-sm text-center md:text-left">
|
||||
<div className="text-l py-1 text-lg w-full">
|
||||
<Button
|
||||
onClick={() => handleLoginMfa()}
|
||||
size="sm"
|
||||
isFullWidth
|
||||
className='h-14'
|
||||
colorSchema="primary"
|
||||
variant="outline_bg"
|
||||
> {String(t('mfa.verify'))} </Button>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex flex-col items-center justify-center w-full max-h-24 max-w-md mx-auto pt-2">
|
||||
<div className="flex flex-row items-baseline gap-1 text-sm">
|
||||
<span className="text-bunker-400">{t('mfa:step2-resend-alert')}</span>
|
||||
<u
|
||||
className={`font-normal ${
|
||||
isLoadingResend
|
||||
? 'text-bunker-400'
|
||||
: 'text-primary-700 hover:text-primary duration-200'
|
||||
}`}
|
||||
>
|
||||
<button disabled={isLoading} onClick={() => handleResendMfaCode()} type="button">
|
||||
{isLoadingResend
|
||||
? t('mfa:step2-resend-progress')
|
||||
: t('mfa:step2-resend-submit')}
|
||||
<span className="text-bunker-400">{t('signup.step2-resend-alert')}</span>
|
||||
<div className="mt-2 text-bunker-400 text-md flex flex-row">
|
||||
<button disabled={isLoading} onClick={handleResendMfaCode} type="button">
|
||||
<span className='hover:underline hover:underline-offset-4 hover:decoration-primary-700 hover:text-bunker-200 duration-200 cursor-pointer'>
|
||||
{isLoadingResend
|
||||
? t('signup.step2-resend-progress')
|
||||
: t('signup.step2-resend-submit')}
|
||||
</span>
|
||||
</button>
|
||||
</u>
|
||||
</div>
|
||||
</div>
|
||||
<p className="text-sm text-bunker-400 pb-2">{t('mfa:step2-spam-alert')}</p>
|
||||
<p className="text-sm text-bunker-400 pb-2">{t('signup.step2-spam-alert')}</p>
|
||||
</div>
|
||||
</form>
|
||||
);
|
||||
}
|
||||
|
||||
export const getStaticProps = getTranslatedStaticProps(['auth', 'mfa']);
|
||||
@@ -0,0 +1,119 @@
|
||||
import React, { useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import Link from 'next/link';
|
||||
import { useRouter } from 'next/router';
|
||||
|
||||
import Error from '@app/components/basic/Error';
|
||||
import attemptLogin from '@app/components/utilities/attemptLogin';
|
||||
|
||||
import SecurityClient from '../utilities/SecurityClient';
|
||||
import { Button, Input } from '../v2';
|
||||
|
||||
export default function PasswordInputStep({
|
||||
email,
|
||||
password,
|
||||
providerAuthToken,
|
||||
setPassword,
|
||||
setProviderAuthToken,
|
||||
setStep
|
||||
}: {
|
||||
email: string;
|
||||
password: string;
|
||||
providerAuthToken: string;
|
||||
setPassword: (password: string) => void;
|
||||
setProviderAuthToken: (value: string) => void;
|
||||
setStep: (step: number) => void;
|
||||
}) {
|
||||
const router = useRouter();
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [loginError, setLoginError] = useState(false);
|
||||
|
||||
const { t } = useTranslation();
|
||||
|
||||
const handleLogin = async () => {
|
||||
try {
|
||||
setIsLoading(true);
|
||||
const loginAttempt = await attemptLogin({
|
||||
email,
|
||||
password,
|
||||
providerAuthToken,
|
||||
});
|
||||
|
||||
if (loginAttempt && loginAttempt.success) {
|
||||
// case: login was successful
|
||||
|
||||
if (loginAttempt.mfaEnabled) {
|
||||
// case: login requires MFA step
|
||||
setStep(2);
|
||||
setIsLoading(false);
|
||||
return;
|
||||
}
|
||||
|
||||
// case: login does not require MFA step
|
||||
router.push(`/dashboard/${localStorage.getItem('projectData.id')}`);
|
||||
}
|
||||
} catch (err) {
|
||||
setLoginError(true);
|
||||
}
|
||||
|
||||
setIsLoading(false);
|
||||
};
|
||||
|
||||
return (
|
||||
<form onSubmit={(e) => e.preventDefault()}>
|
||||
<div className="h-full mx-auto w-full max-w-md px-6 pt-8">
|
||||
<p className="mx-auto mb-6 flex w-max justify-center text-xl font-medium text-transparent bg-clip-text bg-gradient-to-b from-white to-bunker-200 text-center mb-8">
|
||||
What’s your Infisical Password?
|
||||
</p>
|
||||
<div className="relative flex items-center justify-center lg:w-1/6 w-1/4 min-w-[22rem] mx-auto w-full rounded-lg max-h-24 md:max-h-28">
|
||||
<div className="flex items-center justify-center w-full rounded-lg max-h-24 md:max-h-28">
|
||||
<Input
|
||||
value={password}
|
||||
onChange={(e) => setPassword(e.target.value)}
|
||||
type="password"
|
||||
placeholder="Enter your password..."
|
||||
isRequired
|
||||
autoComplete="current-password"
|
||||
id="current-password"
|
||||
className="h-12"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
{!isLoading && loginError && <Error text={t('login.error-login') ?? ''} />}
|
||||
<div className='lg:w-1/6 w-1/4 w-full mx-auto flex items-center justify-center min-w-[22rem] text-center rounded-md mt-4'>
|
||||
<Button
|
||||
colorSchema="primary"
|
||||
variant="outline_bg"
|
||||
onClick={async () => handleLogin()}
|
||||
isFullWidth
|
||||
isLoading={isLoading}
|
||||
className="h-14"
|
||||
>
|
||||
{t('login.login')}
|
||||
</Button>
|
||||
</div>
|
||||
<div className="text-bunker-400 text-xs flex flex-col items-center w-max mx-auto mt-4">
|
||||
<span className='duration-200 max-w-sm text-center px-4'>
|
||||
Infisical Master Password serves as a decryption mechanism so that even Google is not able to access your secrets.
|
||||
</span>
|
||||
<Link href="/verify-email">
|
||||
<span className='hover:underline mt-2 hover:underline-offset-4 hover:decoration-primary-700 hover:text-bunker-200 duration-200 cursor-pointer'>{t('login.forgot-password')}</span>
|
||||
</Link>
|
||||
</div>
|
||||
<div className="flex flex-row items-center justify-center">
|
||||
<button
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
SecurityClient.setProviderAuthToken('');
|
||||
setProviderAuthToken('');
|
||||
}}
|
||||
type="button"
|
||||
className="text-bunker-400 text-xs hover:underline mt-2 hover:underline-offset-4 hover:decoration-primary-700 hover:text-bunker-200 duration-200 cursor-pointer"
|
||||
>
|
||||
{t('login.other-option')}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
);
|
||||
}
|
||||
@@ -1,9 +1,9 @@
|
||||
/* eslint-disable jsx-a11y/anchor-is-valid */
|
||||
/* eslint-disable react/jsx-key */
|
||||
import { Fragment, useEffect, useMemo, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
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,6 +17,7 @@ import {
|
||||
} from '@fortawesome/free-solid-svg-icons';
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||
import { Menu, Transition } from '@headlessui/react';
|
||||
import {TFunction} from 'i18next'
|
||||
|
||||
import logout from '@app/pages/api/auth/Logout';
|
||||
|
||||
@@ -28,22 +29,22 @@ import guidGenerator from '../utilities/randomId';
|
||||
const supportOptions = (t: TFunction) => [
|
||||
[
|
||||
<FontAwesomeIcon className="text-lg pl-1.5 pr-3" icon={faSlack} />,
|
||||
t('nav:support.slack'),
|
||||
'https://join.slack.com/t/infisical/shared_invite/zt-1dgg63ln8-G7PCNJdCymAT9YF3j1ewVA'
|
||||
t('nav.support.slack'),
|
||||
'https://join.slack.com/t/infisical-users/shared_invite/zt-1wehzfnzn-1aMo5JcGENJiNAC2SD8Jlg'
|
||||
],
|
||||
[
|
||||
<FontAwesomeIcon className="text-lg pl-1.5 pr-3" icon={faBook} />,
|
||||
t('nav:support.docs'),
|
||||
'https://infisical.com/docs/getting-started/introduction'
|
||||
t('nav.support.docs'),
|
||||
'https://infisical.com/docs/documentation/getting-started/introduction'
|
||||
],
|
||||
[
|
||||
<FontAwesomeIcon className="text-lg pl-1.5 pr-3" icon={faGithub} />,
|
||||
t('nav:support.issue'),
|
||||
t('nav.support.issue'),
|
||||
'https://github.com/Infisical/infisical-cli/issues'
|
||||
],
|
||||
[
|
||||
<FontAwesomeIcon className="text-lg pl-1.5 pr-3" icon={faEnvelope} />,
|
||||
t('nav:support.email'),
|
||||
t('nav.support.email'),
|
||||
'mailto:[email protected]'
|
||||
]
|
||||
];
|
||||
@@ -105,7 +106,7 @@ export default function Navbar() {
|
||||
</div>
|
||||
<div className="relative flex justify-start items-center mx-2 z-40">
|
||||
<a
|
||||
href="https://infisical.com/docs/getting-started/introduction"
|
||||
href="https://infisical.com/docs/documentation/getting-started/introduction"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="text-gray-200 hover:bg-white/10 px-3 rounded-md duration-200 text-sm mr-4 py-2 flex items-center"
|
||||
@@ -168,7 +169,7 @@ export default function Navbar() {
|
||||
<Menu.Items className="absolute right-0 mt-0.5 w-64 origin-top-right divide-y divide-gray-700 rounded-md bg-bunker border border-mineshaft-700 shadow-lg ring-1 ring-black z-[999] ring-opacity-5 focus:outline-none">
|
||||
<div className="px-1 py-1 z-[100]">
|
||||
<div className="text-gray-400 self-start ml-2 mt-2 text-xs font-semibold tracking-wide">
|
||||
{t('nav:user.signed-in-as')}
|
||||
{t('nav.user.signed-in-as')}
|
||||
</div>
|
||||
<div
|
||||
onKeyDown={() => null}
|
||||
@@ -197,7 +198,7 @@ export default function Navbar() {
|
||||
</div>
|
||||
<div className="px-2 pt-2">
|
||||
<div className="text-gray-400 self-start ml-2 mt-2 text-xs font-semibold tracking-wide">
|
||||
{t('nav:user.current-organization')}
|
||||
{t('nav.user.current-organization')}
|
||||
</div>
|
||||
<div
|
||||
onKeyDown={() => null}
|
||||
@@ -230,7 +231,7 @@ export default function Navbar() {
|
||||
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"
|
||||
>
|
||||
<FontAwesomeIcon className="text-lg pl-1.5 pr-3" icon={faCoins} />
|
||||
<div className="text-sm">{t('nav:user.usage-billing')}</div>
|
||||
<div className="text-sm">{t('nav.user.usage-billing')}</div>
|
||||
</div>
|
||||
</button>
|
||||
<button
|
||||
@@ -248,14 +249,14 @@ export default function Navbar() {
|
||||
<span className="rounded-lg absolute inset-y-0 left-0 flex items-center pl-3 pr-4">
|
||||
<FontAwesomeIcon icon={faPlus} className="ml-1" />
|
||||
</span>
|
||||
<div className="text-sm ml-1">{t('nav:user.invite')}</div>
|
||||
<div className="text-sm ml-1">{t('nav.user.invite')}</div>
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
{orgs?.length > 1 && (
|
||||
<div className="px-1 pt-1">
|
||||
<div className="text-gray-400 self-start ml-2 mt-2 text-xs font-semibold tracking-wide">
|
||||
{t('nav:user.other-organizations')}
|
||||
{t('nav.user.other-organizations')}
|
||||
</div>
|
||||
<div className="flex flex-col items-start px-1 mt-3 mb-2">
|
||||
{orgs
|
||||
@@ -300,7 +301,7 @@ export default function Navbar() {
|
||||
className="text-lg ml-1.5 mr-3"
|
||||
icon={faRightFromBracket}
|
||||
/>
|
||||
{t('common:logout')}
|
||||
{t('common.logout')}
|
||||
</div>
|
||||
</button>
|
||||
)}
|
||||
|
||||
@@ -1,41 +1,134 @@
|
||||
import { useMemo } from 'react';
|
||||
import Link from 'next/link';
|
||||
import { useRouter } from 'next/router';
|
||||
import { faAngleRight } from '@fortawesome/free-solid-svg-icons';
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||
|
||||
import { useOrganization, useWorkspace } from '@app/context';
|
||||
|
||||
import { Select, SelectItem, Tooltip } from '../v2';
|
||||
|
||||
type Props = {
|
||||
pageName: string;
|
||||
isProjectRelated?: boolean;
|
||||
isOrganizationRelated?: boolean;
|
||||
currentEnv?: string;
|
||||
userAvailableEnvs?: any[];
|
||||
onEnvChange?: (slug: string) => void;
|
||||
folders?: Array<{ id: string; name: string }>;
|
||||
isFolderMode?: boolean;
|
||||
};
|
||||
|
||||
// TODO: make links clickable and clean up
|
||||
|
||||
/**
|
||||
* 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)
|
||||
* @param {object} obj
|
||||
* @param {string} obj.pageName - Name of the page
|
||||
* @param {boolean} obj.isProjectRelated - whether or not this page is related to project (determine if it's 2 or 3 navigation steps)
|
||||
* @param {boolean} obj.isOrganizationRelated - whether or not this page is related to organization (determine if it's 2 or 3 navigation steps)
|
||||
* @param {string} obj.currentEnv - current environment inside a project
|
||||
* @param {string} obj.userAvailableEnvs - environments that are available to a user in this project (used for the dropdown)
|
||||
* @param {string} obj.onEnvChange - the action that happens when an env is changed
|
||||
* @returns
|
||||
*/
|
||||
// TODO(akhilmhdh): simply this header and nav system later
|
||||
export default function NavHeader({
|
||||
pageName,
|
||||
isProjectRelated
|
||||
}: {
|
||||
pageName: string;
|
||||
isProjectRelated?: boolean;
|
||||
}): JSX.Element {
|
||||
isProjectRelated,
|
||||
isOrganizationRelated,
|
||||
currentEnv,
|
||||
userAvailableEnvs = [],
|
||||
onEnvChange,
|
||||
folders = [],
|
||||
isFolderMode
|
||||
}: Props): JSX.Element {
|
||||
const { currentWorkspace } = useWorkspace();
|
||||
const { currentOrg } = useOrganization();
|
||||
const router = useRouter();
|
||||
|
||||
const isInRootFolder = isFolderMode && folders.length <= 1;
|
||||
|
||||
const selectedEnv = useMemo(
|
||||
() => userAvailableEnvs?.find((uae) => uae.name === currentEnv),
|
||||
[userAvailableEnvs, currentEnv]
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="ml-6 flex flex-row items-center pt-8">
|
||||
<div className="ml-6 flex flex-row items-center pt-6">
|
||||
<div className="mr-2 flex h-6 w-6 items-center justify-center rounded-md bg-primary-900 text-mineshaft-100">
|
||||
{currentOrg?.name?.charAt(0)}
|
||||
</div>
|
||||
<div className="text-sm font-semibold text-primary">{currentOrg?.name}</div>
|
||||
<div className="text-sm font-semibold text-bunker-300">{currentOrg?.name}</div>
|
||||
{isProjectRelated && (
|
||||
<>
|
||||
<FontAwesomeIcon icon={faAngleRight} className="ml-3 mr-3 text-sm text-gray-400" />
|
||||
<div className="text-sm font-semibold text-primary">{currentWorkspace?.name}</div>
|
||||
<FontAwesomeIcon icon={faAngleRight} className="ml-3 mr-3 text-xs text-gray-400" />
|
||||
<div className="text-sm font-semibold text-bunker-300">{currentWorkspace?.name}</div>
|
||||
</>
|
||||
)}
|
||||
{isOrganizationRelated && (
|
||||
<>
|
||||
<FontAwesomeIcon icon={faAngleRight} className="ml-3 mr-3 text-xs text-gray-400" />
|
||||
<div className="text-sm font-semibold text-bunker-300">Organization Settings</div>
|
||||
</>
|
||||
)}
|
||||
<FontAwesomeIcon icon={faAngleRight} className="ml-3 mr-3 text-sm text-gray-400" />
|
||||
<div className="text-sm text-gray-400">{pageName}</div>
|
||||
{pageName === 'Secrets' ? (
|
||||
<Link
|
||||
passHref
|
||||
legacyBehavior
|
||||
href={{ pathname: '/dashboard/[id]', query: { id: router.query.id } }}
|
||||
>
|
||||
<a className="text-sm font-semibold text-primary/80 hover:text-primary">{pageName}</a>
|
||||
</Link>
|
||||
) : (
|
||||
<div className="text-sm text-gray-400">{pageName}</div>
|
||||
)}
|
||||
{currentEnv && isInRootFolder && (
|
||||
<>
|
||||
<FontAwesomeIcon icon={faAngleRight} className="ml-3 mr-1.5 text-xs text-gray-400" />
|
||||
<div className="rounded-md pl-3 hover:bg-bunker-100/10">
|
||||
<Tooltip content="Select environment">
|
||||
<Select
|
||||
value={selectedEnv?.slug}
|
||||
onValueChange={(value) => {
|
||||
if (value && onEnvChange) onEnvChange(value);
|
||||
}}
|
||||
className="bg-transparent pl-0 text-sm font-medium text-primary/80 hover:text-primary"
|
||||
dropdownContainerClassName="text-bunker-200 bg-mineshaft-800 border border-mineshaft-600 drop-shadow-2xl"
|
||||
>
|
||||
{userAvailableEnvs?.map(({ name, slug }) => (
|
||||
<SelectItem value={slug} key={slug}>
|
||||
{name}
|
||||
</SelectItem>
|
||||
))}
|
||||
</Select>
|
||||
</Tooltip>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
{isFolderMode &&
|
||||
folders?.map(({ id, name }, index) => {
|
||||
const query = { ...router.query };
|
||||
if (name !== 'root') query.folderId = id;
|
||||
else delete query.folderId;
|
||||
return (
|
||||
<div className="flex items-center space-x-3" key={`breadcrumb-folder-${id}`}>
|
||||
<FontAwesomeIcon icon={faAngleRight} className="ml-3 mr-1.5 text-xs text-gray-400" />
|
||||
{index + 1 === folders?.length ? (
|
||||
<span className="text-sm font-semibold text-bunker-300">{name}</span>
|
||||
) : (
|
||||
<Link passHref legacyBehavior href={{ pathname: '/dashboard/[id]', query }}>
|
||||
<a className="text-sm font-semibold text-primary/80 hover:text-primary">
|
||||
{name === 'root' ? selectedEnv?.name : name}
|
||||
</a>
|
||||
</Link>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,90 @@
|
||||
import { useRouter } from 'next/router';
|
||||
import { faAngleRight } from '@fortawesome/free-solid-svg-icons';
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||
|
||||
import { useOrganization, useWorkspace } from '@app/context';
|
||||
|
||||
import { Select, SelectItem, Tooltip } from '../v2';
|
||||
|
||||
|
||||
/**
|
||||
* 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 {object} obj
|
||||
* @param {string} obj.pageName - Name of the page
|
||||
* @param {boolean} obj.isProjectRelated - whether or not this page is related to project (determine if it's 2 or 3 navigation steps)
|
||||
* @param {boolean} obj.isOrganizationRelated - whether or not this page is related to organization (determine if it's 2 or 3 navigation steps)
|
||||
* @param {string} obj.currentEnv - current environment inside a project
|
||||
* @param {string} obj.userAvailableEnvs - environments that are available to a user in this project (used for the dropdown)
|
||||
* @param {string} obj.onEnvChange - the action that happens when an env is changed
|
||||
* @returns
|
||||
*/
|
||||
export default function NavHeaderSecrets({
|
||||
pageName,
|
||||
isProjectRelated,
|
||||
isOrganizationRelated,
|
||||
isSnapshot,
|
||||
currentEnv,
|
||||
userAvailableEnvs,
|
||||
onEnvChange
|
||||
}: {
|
||||
pageName: string;
|
||||
isProjectRelated?: boolean;
|
||||
isOrganizationRelated?: boolean;
|
||||
isSnapshot: boolean;
|
||||
currentEnv?: string;
|
||||
userAvailableEnvs?: any[];
|
||||
onEnvChange?: (slug: string) => void;
|
||||
}): JSX.Element {
|
||||
const { currentWorkspace } = useWorkspace();
|
||||
const { currentOrg } = useOrganization();
|
||||
const router = useRouter()
|
||||
|
||||
return (
|
||||
<div className={`${!isSnapshot && "absolute"} ml-6 flex flex-row items-center pt-6 cursor-default`}>
|
||||
<div className="mr-3 flex h-6 w-6 items-center justify-center rounded-md bg-primary-900 text-mineshaft-100">
|
||||
{currentOrg?.name?.charAt(0)}
|
||||
</div>
|
||||
<div className="text-md font-medium text-bunker-300">{currentOrg?.name}</div>
|
||||
{isProjectRelated && (
|
||||
<>
|
||||
<FontAwesomeIcon icon={faAngleRight} className="ml-3 mr-3 text-sm text-gray-400" />
|
||||
<div className="text-md font-medium text-bunker-300">{currentWorkspace?.name}</div>
|
||||
</>
|
||||
)}
|
||||
{isOrganizationRelated && (
|
||||
<>
|
||||
<FontAwesomeIcon icon={faAngleRight} className="ml-3 mr-3 text-sm text-gray-400" />
|
||||
<div className="text-md font-medium text-bunker-300">Organization Settings</div>
|
||||
</>
|
||||
)}
|
||||
<FontAwesomeIcon icon={faAngleRight} className="ml-3 mr-3 text-sm text-gray-400" />
|
||||
{pageName === 'Secrets'
|
||||
? <a className="text-md font-medium text-primary/80 hover:text-primary" href={`${router.asPath.split("?")[0]}`}>{pageName}</a>
|
||||
: <div className="text-md text-gray-400">{pageName}</div>}
|
||||
{currentEnv &&
|
||||
<>
|
||||
<FontAwesomeIcon icon={faAngleRight} className="ml-3 mr-1.5 text-sm text-gray-400" />
|
||||
<div className='pl-3 rounded-md hover:bg-bunker-100/10'>
|
||||
<Tooltip content="Select environment">
|
||||
<Select
|
||||
value={userAvailableEnvs?.filter(uae => uae.name === currentEnv)[0]?.slug}
|
||||
onValueChange={(value) => {
|
||||
if (value && onEnvChange) onEnvChange(value);
|
||||
}}
|
||||
className="text-md pl-0 font-medium text-primary/80 hover:text-primary bg-transparent"
|
||||
dropdownContainerClassName="text-bunker-200 bg-mineshaft-800 border border-mineshaft-600 drop-shadow-2xl"
|
||||
>
|
||||
{userAvailableEnvs?.map(({ name, slug }) => (
|
||||
<SelectItem value={slug} key={slug}>
|
||||
{name}
|
||||
</SelectItem>
|
||||
))}
|
||||
</Select>
|
||||
</Tooltip>
|
||||
</div>
|
||||
</>}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,12 +1,12 @@
|
||||
/* eslint-disable react/jsx-props-no-spreading */
|
||||
import React, { useState } from 'react';
|
||||
import ReactCodeInput from 'react-code-input';
|
||||
import { useTranslation } from 'next-i18next';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import sendVerificationEmail from '@app/pages/api/auth/SendVerificationEmail';
|
||||
|
||||
import Button from '../basic/buttons/Button';
|
||||
import Error from '../basic/Error';
|
||||
import { Button } from '../v2';
|
||||
|
||||
// The style for the verification code input
|
||||
const props = {
|
||||
@@ -51,6 +51,7 @@ interface CodeInputStepProps {
|
||||
incrementStep: () => void;
|
||||
setCode: (value: string) => void;
|
||||
codeError: boolean;
|
||||
isCodeInputCheckLoading: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -66,7 +67,8 @@ export default function CodeInputStep({
|
||||
email,
|
||||
incrementStep,
|
||||
setCode,
|
||||
codeError
|
||||
codeError,
|
||||
isCodeInputCheckLoading
|
||||
}: CodeInputStepProps): JSX.Element {
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [isResendingVerificationEmail, setIsResendingVerificationEmail] = useState(false);
|
||||
@@ -83,10 +85,10 @@ export default function CodeInputStep({
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="bg-bunker w-max mx-auto h-7/12 pt-10 pb-4 px-8 rounded-xl drop-shadow-xl mb-64 md:mb-16">
|
||||
<p className="text-l flex justify-center text-bunker-300">{t('signup:step2-message')}</p>
|
||||
<p className="text-l flex justify-center font-semibold my-2 text-bunker-300">{email} </p>
|
||||
<div className="hidden md:block">
|
||||
<div className="mx-auto h-full w-full pb-4 md:px-8">
|
||||
<p className="text-md flex justify-center text-bunker-200">{t('signup.step2-message')}</p>
|
||||
<p className="text-md flex justify-center font-semibold my-1 text-bunker-200">{email} </p>
|
||||
<div className="hidden md:block w-max min-w-[20rem] mx-auto">
|
||||
<ReactCodeInput
|
||||
name=""
|
||||
inputMode="tel"
|
||||
@@ -97,7 +99,7 @@ export default function CodeInputStep({
|
||||
className="mt-6 mb-2"
|
||||
/>
|
||||
</div>
|
||||
<div className="block md:hidden">
|
||||
<div className="block md:hidden w-max mt-4 mx-auto">
|
||||
<ReactCodeInput
|
||||
name=""
|
||||
inputMode="tel"
|
||||
@@ -105,31 +107,37 @@ export default function CodeInputStep({
|
||||
fields={6}
|
||||
onChange={setCode}
|
||||
{...propsPhone}
|
||||
className="mt-2 mb-6"
|
||||
className="mt-2 mb-2"
|
||||
/>
|
||||
</div>
|
||||
{codeError && <Error text={t('signup:step2-code-error')} />}
|
||||
<div className="flex max-w-max min-w-28 flex-col items-center justify-center md:p-2 max-h-24 mx-auto text-lg px-4 mt-4 mb-2">
|
||||
<Button text={t('signup:verify') ?? ''} onButtonPressed={incrementStep} size="lg" />
|
||||
{codeError && <Error text={t('signup.step2-code-error')} />}
|
||||
<div className="flex flex-col items-center justify-center lg:w-[19%] w-1/4 min-w-[20rem] mt-2 max-w-xs md:max-w-md mx-auto text-sm text-center md:text-left">
|
||||
<div className="text-l py-1 text-lg w-full">
|
||||
<Button
|
||||
onClick={incrementStep}
|
||||
size="sm"
|
||||
isFullWidth
|
||||
className='h-14'
|
||||
colorSchema="primary"
|
||||
variant="outline_bg"
|
||||
isLoading={isCodeInputCheckLoading}
|
||||
> {String(t('signup.verify'))} </Button>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex flex-col items-center justify-center w-full max-h-24 max-w-md mx-auto pt-2">
|
||||
<div className="flex flex-row items-baseline gap-1 text-sm">
|
||||
<span className="text-bunker-400">{t('signup:step2-resend-alert')}</span>
|
||||
<u
|
||||
className={`font-normal ${
|
||||
isResendingVerificationEmail
|
||||
? 'text-bunker-400'
|
||||
: 'text-primary-700 hover:text-primary duration-200'
|
||||
}`}
|
||||
>
|
||||
<span className="text-bunker-400">{t('signup.step2-resend-alert')}</span>
|
||||
<div className="mt-2 text-bunker-400 text-md flex flex-row">
|
||||
<button disabled={isLoading} onClick={resendVerificationEmail} type="button">
|
||||
{isResendingVerificationEmail
|
||||
? t('signup:step2-resend-progress')
|
||||
: t('signup:step2-resend-submit')}
|
||||
<span className='hover:underline hover:underline-offset-4 hover:decoration-primary-700 hover:text-bunker-200 duration-200 cursor-pointer'>
|
||||
{isResendingVerificationEmail
|
||||
? t('signup.step2-resend-progress')
|
||||
: t('signup.step2-resend-submit')}
|
||||
</span>
|
||||
</button>
|
||||
</u>
|
||||
</div>
|
||||
</div>
|
||||
<p className="text-sm text-bunker-400 pb-2">{t('signup:step2-spam-alert')}</p>
|
||||
<p className="text-sm text-bunker-400 pb-2">{t('signup.step2-spam-alert')}</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import { useTranslation } from 'next-i18next';
|
||||
import { useTranslation } from 'react-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';
|
||||
import { Button } from '../v2';
|
||||
|
||||
interface DownloadBackupPDFStepProps {
|
||||
incrementStep: () => void;
|
||||
@@ -30,33 +30,35 @@ export default function DonwloadBackupPDFStep({
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<div className="bg-bunker flex flex-col items-center w-full max-w-xs md:max-w-lg h-7/12 py-8 px-4 md:px-6 mx-1 mb-36 md:mb-16 rounded-xl drop-shadow-xl">
|
||||
<p className="text-4xl text-center font-semibold flex justify-center text-primary">
|
||||
{t('signup:step4-message')}
|
||||
<div className="flex flex-col items-center w-full h-full md:px-6 mx-auto mb-36 md:mb-16">
|
||||
<p className="text-xl text-center font-medium flex justify-center text-transparent bg-clip-text bg-gradient-to-b from-white to-bunker-200">
|
||||
<FontAwesomeIcon icon={faWarning} className="ml-2 mr-3 pt-1 text-2xl text-bunker-200" />{t('signup.step4-message')}
|
||||
</p>
|
||||
<div className="flex flex-col items-center justify-center w-full mt-4 md:mt-8 max-w-md text-gray-400 text-md rounded-md px-2">
|
||||
<div>{t('signup:step4-description1')}</div>
|
||||
<div className="mt-3">{t('signup:step4-description2')}</div>
|
||||
</div>
|
||||
<div className="w-full p-2 flex flex-row items-center bg-white/10 text-gray-400 rounded-md max-w-xs md:max-w-md mx-auto mt-4">
|
||||
<FontAwesomeIcon icon={faWarning} className="ml-2 mr-4 text-4xl" />
|
||||
{t('signup:step4-description3')}
|
||||
</div>
|
||||
<div className="flex flex-col items-center justify-center md:px-4 md:py-5 mt-4 px-2 py-3 max-h-24 max-w-max mx-auto text-lg">
|
||||
<Button
|
||||
text="Download PDF"
|
||||
onButtonPressed={async () => {
|
||||
await issueBackupKey({
|
||||
email,
|
||||
password,
|
||||
personalName: name,
|
||||
setBackupKeyError: () => {},
|
||||
setBackupKeyIssued: () => {}
|
||||
});
|
||||
incrementStep();
|
||||
}}
|
||||
size="lg"
|
||||
/>
|
||||
<div className="flex flex-col pb-2 bg-mineshaft-800 border border-mineshaft-600 items-center justify-center text-center lg:w-1/6 w-full md:min-w-[24rem] mt-8 max-w-md text-bunker-300 text-md rounded-md">
|
||||
<div className="w-full mt-4 md:mt-8 flex flex-row text-center items-center m-2 text-bunker-300 rounded-md lg:w-1/6 lg:w-1/6 w-full md:min-w-[23rem] px-3 mx-auto">
|
||||
<span className='mb-2'>{t('signup.step4-description1')} {t('signup.step4-description3')}</span>
|
||||
</div>
|
||||
<div className="flex flex-col items-center px-3 justify-center mt-0 md:mt-4 mb-2 md:mb-4 lg:w-1/6 w-full md:min-w-[20rem] mt-2 md:max-w-md mx-auto text-sm text-center md:text-left">
|
||||
<div className="text-l py-1 text-lg w-full">
|
||||
<Button
|
||||
onClick={async () => {
|
||||
await issueBackupKey({
|
||||
email,
|
||||
password,
|
||||
personalName: name,
|
||||
setBackupKeyError: () => { },
|
||||
setBackupKeyIssued: () => { }
|
||||
});
|
||||
incrementStep();
|
||||
}}
|
||||
size="sm"
|
||||
isFullWidth
|
||||
className='h-12'
|
||||
colorSchema="primary"
|
||||
variant="outline_bg"
|
||||
> Download PDF </Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -1,11 +1,10 @@
|
||||
import React, { useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
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';
|
||||
import { Button, Input } from '../v2';
|
||||
|
||||
interface DownloadBackupPDFStepProps {
|
||||
incrementStep: () => void;
|
||||
@@ -27,7 +26,6 @@ export default function EnterEmailStep({
|
||||
incrementStep
|
||||
}: DownloadBackupPDFStepProps): JSX.Element {
|
||||
const [emailError, setEmailError] = useState(false);
|
||||
const [emailErrorMessage, setEmailErrorMessage] = useState('');
|
||||
const { t } = useTranslation();
|
||||
|
||||
/**
|
||||
@@ -37,11 +35,9 @@ export default function EnterEmailStep({
|
||||
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);
|
||||
@@ -56,41 +52,40 @@ export default function EnterEmailStep({
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className="bg-bunker w-full max-w-md h-7/12 py-8 md:px-6 mx-1 rounded-xl drop-shadow-xl">
|
||||
<p className="text-4xl font-semibold flex justify-center text-primary">
|
||||
{t('signup:step1-start')}
|
||||
<div className="w-full md:px-6 mx-auto">
|
||||
<p className="text-xl font-medium flex justify-center text-transparent bg-clip-text bg-gradient-to-b from-white to-bunker-200">
|
||||
{t('signup.step1-start')}
|
||||
</p>
|
||||
<div className="flex items-center justify-center w-5/6 md:w-full m-auto md:p-2 rounded-lg max-h-24 mt-4">
|
||||
<InputField
|
||||
label={t('common:email') ?? ''}
|
||||
onChangeHandler={setEmail}
|
||||
type="email"
|
||||
<div className="flex flex-col items-center justify-center lg:w-1/6 w-1/4 min-w-[20rem] m-auto rounded-lg mt-8">
|
||||
<Input
|
||||
placeholder="Enter your email address..."
|
||||
onChange={(e) => setEmail(e.target.value)}
|
||||
value={email}
|
||||
placeholder=""
|
||||
isRequired
|
||||
error={emailError}
|
||||
errorText={emailErrorMessage}
|
||||
autoComplete="username"
|
||||
className="h-12"
|
||||
/>
|
||||
{emailError && <p className="text-red-600 text-xs text-left w-full ml-1.5 mt-1.5">Please enter a valid email.</p>}
|
||||
</div>
|
||||
<div className="flex flex-col items-center justify-center w-5/6 md:w-full md:p-2 max-h-28 max-w-xs md:max-w-md mx-auto mt-4 md:mt-4 text-sm text-center md:text-left">
|
||||
<p className="text-gray-400 mt-2 md:mx-0.5">{t('signup:step1-privacy')}</p>
|
||||
<div className="text-l mt-6 m-2 md:m-8 px-8 py-1 text-lg">
|
||||
<div className="flex flex-col items-center justify-center lg:w-1/6 w-1/4 min-w-[20rem] mt-2 max-w-xs md:max-w-md mx-auto text-sm text-center md:text-left">
|
||||
<div className="text-l py-1 text-lg w-full">
|
||||
<Button
|
||||
text={t('signup:step1-submit') ?? ''}
|
||||
type="submit"
|
||||
onButtonPressed={emailCheck}
|
||||
size="lg"
|
||||
/>
|
||||
onClick={emailCheck}
|
||||
size="sm"
|
||||
isFullWidth
|
||||
className='h-14'
|
||||
colorSchema="primary"
|
||||
variant="outline_bg"
|
||||
> {String(t('signup.step1-submit'))} </Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex flex-col items-center justify-center w-full md:pb-2 max-w-md mx-auto pt-2 mb-48 md:mb-16 mt-2">
|
||||
<div className="mx-auto mb-48 mt-2 flex w-full max-w-md flex-col items-center justify-center pt-2 md:mb-16 md:pb-2">
|
||||
<Link href="/login">
|
||||
<button type="button" className="w-max pb-3 hover:opacity-90 duration-200">
|
||||
<u className="font-normal text-sm text-primary-500">
|
||||
{t('signup:already-have-account')}
|
||||
</u>
|
||||
<button type="button" className="w-max pb-3 duration-200 hover:opacity-90">
|
||||
<span className="text-sm text-mineshaft-400 hover:underline hover:underline-offset-4 hover:decoration-primary-700 hover:text-bunker-200 duration-200 cursor-pointer">
|
||||
{t('signup.already-have-account')}
|
||||
</span>
|
||||
</button>
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
@@ -0,0 +1,65 @@
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import Link from 'next/link';
|
||||
import { useRouter } from 'next/router';
|
||||
|
||||
// import { faGoogle } from '@fortawesome/free-brands-svg-icons';
|
||||
// import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||
import { Button } from '../v2';
|
||||
|
||||
export default function InitialSignupStep({
|
||||
setIsSignupWithEmail,
|
||||
}: {
|
||||
setIsSignupWithEmail: (value: boolean) => void
|
||||
}) {
|
||||
const { t } = useTranslation();
|
||||
const router = useRouter();
|
||||
|
||||
return <div className='flex flex-col mx-auto w-full justify-center items-center'>
|
||||
<h1 className='text-xl font-medium text-transparent bg-clip-text bg-gradient-to-b from-white to-bunker-200 text-center mb-8' >{t('signup.initial-title')}</h1>
|
||||
{/* <div className='lg:w-1/6 w-1/4 min-w-[20rem] rounded-md'>
|
||||
<Button
|
||||
colorSchema="primary"
|
||||
variant="solid"
|
||||
onClick={() => {
|
||||
window.open('/api/v1/auth/redirect/google')
|
||||
}}
|
||||
leftIcon={<FontAwesomeIcon icon={faGoogle} className="mr-1" />}
|
||||
className="h-14 w-full mx-0"
|
||||
>
|
||||
{t('signup.continue-with-google')}
|
||||
</Button>
|
||||
</div> */}
|
||||
<div className='lg:w-1/6 w-1/4 min-w-[20rem] text-center rounded-md'>
|
||||
<Button
|
||||
colorSchema="primary"
|
||||
variant="solid"
|
||||
onClick={() => {
|
||||
setIsSignupWithEmail(true);
|
||||
}}
|
||||
isFullWidth
|
||||
className="h-14 w-full mx-0"
|
||||
>
|
||||
Sign Up with email
|
||||
</Button>
|
||||
</div>
|
||||
<div className='lg:w-1/6 w-1/4 min-w-[20rem] text-center rounded-md mt-4'>
|
||||
<Button
|
||||
colorSchema="primary"
|
||||
variant="outline_bg"
|
||||
onClick={() => router.push("/saml-sso")}
|
||||
isFullWidth
|
||||
className="h-14 w-full mx-0"
|
||||
>
|
||||
Continue with SAML SSO
|
||||
</Button>
|
||||
</div>
|
||||
<div className='lg:w-1/6 w-1/4 min-w-[20rem] px-8 text-center mt-6 text-xs text-bunker-400'>
|
||||
{t('signup.create-policy')}
|
||||
</div>
|
||||
<div className="mt-2 text-bunker-400 text-xs flex flex-row">
|
||||
<Link href="/login">
|
||||
<span className='hover:underline hover:underline-offset-4 hover:decoration-primary-700 hover:text-bunker-200 duration-200 cursor-pointer'>{t('signup.already-have-account')}</span>
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
@@ -1,14 +1,13 @@
|
||||
import React, { useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useRouter } from 'next/router';
|
||||
import { useTranslation } from 'next-i18next';
|
||||
|
||||
import { useFetchServerStatus } from '@app/hooks/api/serverDetails';
|
||||
import { usePopUp } from '@app/hooks/usePopUp';
|
||||
import addUserToOrg from '@app/pages/api/organization/addUserToOrg';
|
||||
import getWorkspaces from '@app/pages/api/workspace/getWorkspaces';
|
||||
|
||||
import Button from '../basic/buttons/Button';
|
||||
import { EmailServiceSetupModal } from '../v2';
|
||||
import { Button, EmailServiceSetupModal } from '../v2';
|
||||
|
||||
/**
|
||||
* This is the last step of the signup flow. People can optionally invite their teammates here.
|
||||
@@ -17,16 +16,14 @@ export default function TeamInviteStep(): JSX.Element {
|
||||
const [emails, setEmails] = useState('');
|
||||
const { t } = useTranslation();
|
||||
const router = useRouter();
|
||||
const {data: serverDetails } = useFetchServerStatus()
|
||||
const { handlePopUpToggle, popUp, handlePopUpOpen } = usePopUp([
|
||||
'setUpEmail'
|
||||
] as const);
|
||||
const { data: serverDetails } = useFetchServerStatus();
|
||||
const { handlePopUpToggle, popUp, handlePopUpOpen } = usePopUp(['setUpEmail'] as const);
|
||||
|
||||
// Redirect user to the getting started page
|
||||
const redirectToHome = async () => {
|
||||
const userWorkspaces = await getWorkspaces();
|
||||
const userWorkspace = userWorkspaces[0]._id;
|
||||
router.push(`/home/${userWorkspace}`);
|
||||
router.push(`/dashboard/${userWorkspace}`);
|
||||
};
|
||||
|
||||
const inviteUsers = async ({ emails: inviteEmails }: { emails: string }) => {
|
||||
@@ -39,50 +36,56 @@ export default function TeamInviteStep(): JSX.Element {
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="bg-bunker w-max mx-auto h-7/12 pt-6 pb-4 px-8 rounded-xl drop-shadow-xl mb-64 md:mb-32">
|
||||
<p className="text-4xl font-semibold flex justify-center text-primary">
|
||||
{t('signup:step5-invite-team')}
|
||||
<div className="w-max mx-auto min-w-lg h-full pb-4 px-8 mb-64 md:mb-32">
|
||||
<p className="text-2xl font-semibold flex justify-center text-transparent bg-clip-text bg-gradient-to-b from-white to-bunker-200">
|
||||
{t('signup.step5-invite-team')}
|
||||
</p>
|
||||
<p className="text-center flex justify-center text-bunker-300 max-w-xs md:max-w-sm md:mx-8 mb-6 mt-4">
|
||||
{t('signup:step5-subtitle')}
|
||||
<p className="text-center flex justify-center text-bunker-400 md:mx-8 mb-6 mt-4">
|
||||
{t('signup.step5-subtitle')}
|
||||
</p>
|
||||
<div>
|
||||
<div className="overflow-auto bg-bunker-800">
|
||||
<div className="whitespace-pre-wrap break-words bg-transparent" />
|
||||
<div className="bg-mineshaft-800 border border-mineshaft-500 w-max mx-auto pt-6 pb-4 px-8 rounded-xl drop-shadow-xl mb-6">
|
||||
<div>
|
||||
<div className="text-bunker-300 font-medium pl-1 pb-1 text-sm">
|
||||
<span>Emails</span>
|
||||
</div>
|
||||
<textarea
|
||||
className="bg-mineshaft-900/70 min-w-[30rem] h-20 w-full placeholder:text-bunker-400 py-1 px-2 rounded-md border border-mineshaft-500 text-sm text-bunker-300 outline-none focus:ring-2 ring-primary-800 ring-opacity-70"
|
||||
value={emails}
|
||||
onChange={(e) => setEmails(e.target.value)}
|
||||
placeholder="[email protected], [email protected]..."
|
||||
/>
|
||||
</div>
|
||||
<textarea
|
||||
className="bg-bunker-800 h-20 w-full placeholder:text-bunker-400 py-1 px-2 rounded-md border border-mineshaft-500 text-sm text-bunker-300 outline-none focus:ring-2 ring-primary-800 ring-opacity-70"
|
||||
value={emails}
|
||||
onChange={(e) => setEmails(e.target.value)}
|
||||
placeholder="[email protected], [email protected]..."
|
||||
<div className="flex flex-row items-end justify-end mt-0 md:mt-4 md:mb-2 w-full md:min-w-[30rem] mt-2 md:max-w-md mx-auto text-sm">
|
||||
<Button
|
||||
onClick={() => {
|
||||
if (serverDetails?.emailConfigured) {
|
||||
inviteUsers({ emails })
|
||||
} else {
|
||||
handlePopUpOpen('setUpEmail');
|
||||
}
|
||||
}}
|
||||
size="sm"
|
||||
// isFullWidth
|
||||
className='h-10'
|
||||
colorSchema="primary"
|
||||
variant="solid"
|
||||
> {t('signup.step5-send-invites') ?? ''} </Button>
|
||||
</div>
|
||||
<EmailServiceSetupModal
|
||||
isOpen={popUp.setUpEmail?.isOpen}
|
||||
onOpenChange={(isOpen) => handlePopUpToggle('setUpEmail', isOpen)}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex flex-row max-w-max min-w-28 items-center justify-center md:p-2 max-h-24 mx-auto text-lg px-4 mt-4 mb-2">
|
||||
<div
|
||||
onKeyDown={() => 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')}
|
||||
</div>
|
||||
<div className="flex flex-row max-w-max min-w-28 items-center justify-center md:p-2 min-w-[20rem] max-h-24 mx-auto text-lg px-4 mt-4 mb-2">
|
||||
<Button
|
||||
text={t('signup:step5-send-invites') ?? ''}
|
||||
onButtonPressed={() => {
|
||||
if(serverDetails?.emailConfigured){
|
||||
inviteUsers({ emails })
|
||||
}else{
|
||||
handlePopUpOpen('setUpEmail');
|
||||
}
|
||||
}}
|
||||
size="lg"
|
||||
/>
|
||||
onClick={redirectToHome}
|
||||
size="sm"
|
||||
isFullWidth
|
||||
className='h-12'
|
||||
colorSchema="secondary"
|
||||
variant="outline"
|
||||
> {t('signup.step5-skip') ?? 'Skip'} </Button>
|
||||
</div>
|
||||
<EmailServiceSetupModal
|
||||
isOpen={popUp.setUpEmail?.isOpen}
|
||||
onOpenChange={(isOpen) => handlePopUpToggle('setUpEmail', isOpen)}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,24 +1,25 @@
|
||||
import crypto from 'crypto';
|
||||
|
||||
import React, { useState } from 'react';
|
||||
import { useTranslation } from 'next-i18next';
|
||||
import { faCheck, faX } from '@fortawesome/free-solid-svg-icons';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { faXmark } 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 { useGetCommonPasswords } from '@app/hooks/api';
|
||||
import completeAccountInformationSignup from '@app/pages/api/auth/CompleteAccountInformationSignup';
|
||||
import getOrganizations from '@app/pages/api/organization/getOrgs';
|
||||
import ProjectService from '@app/services/ProjectService';
|
||||
|
||||
import Button from '../basic/buttons/Button';
|
||||
import InputField from '../basic/InputField';
|
||||
import passwordCheck from '../utilities/checks/PasswordCheck';
|
||||
import checkPassword from '../utilities/checks/checkPassword';
|
||||
import Aes256Gcm from '../utilities/cryptography/aes-256-gcm';
|
||||
import { deriveArgonKey } from '../utilities/cryptography/crypto';
|
||||
import { saveTokenToLocalStorage } from '../utilities/saveTokenToLocalStorage';
|
||||
import SecurityClient from '../utilities/SecurityClient';
|
||||
import { Button, Input } from '../v2';
|
||||
|
||||
// eslint-disable-next-line new-cap
|
||||
const client = new jsrp.client();
|
||||
@@ -28,12 +29,24 @@ interface UserInfoStepProps {
|
||||
email: string;
|
||||
password: string;
|
||||
setPassword: (value: string) => void;
|
||||
firstName: string;
|
||||
setFirstName: (value: string) => void;
|
||||
lastName: string;
|
||||
setLastName: (value: string) => void;
|
||||
name: string;
|
||||
setName: (value: string) => void;
|
||||
organizationName: string;
|
||||
setOrganizationName: (value: string) => void;
|
||||
attributionSource: string;
|
||||
setAttributionSource: (value: string) => void;
|
||||
providerAuthToken?: string;
|
||||
}
|
||||
|
||||
type Errors = {
|
||||
length?: string,
|
||||
upperCase?: string,
|
||||
lowerCase?: string,
|
||||
number?: string,
|
||||
specialChar?: string,
|
||||
repeatedChar?: string,
|
||||
};
|
||||
|
||||
/**
|
||||
* This is the step of the sign up flow where people provife their name/surname and password
|
||||
* @param {object} obj
|
||||
@@ -52,16 +65,19 @@ export default function UserInfoStep({
|
||||
email,
|
||||
password,
|
||||
setPassword,
|
||||
firstName,
|
||||
setFirstName,
|
||||
lastName,
|
||||
setLastName
|
||||
name,
|
||||
setName,
|
||||
organizationName,
|
||||
setOrganizationName,
|
||||
attributionSource,
|
||||
setAttributionSource,
|
||||
providerAuthToken,
|
||||
}: 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 { data: commonPasswords } = useGetCommonPasswords();
|
||||
const [nameError, setNameError] = useState(false);
|
||||
const [organizationNameError, setOrganizationNameError] = useState(false);
|
||||
|
||||
const [errors, setErrors] = useState<Errors>({});
|
||||
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const { t } = useTranslation();
|
||||
@@ -71,24 +87,23 @@ export default function UserInfoStep({
|
||||
const signupErrorCheck = async () => {
|
||||
setIsLoading(true);
|
||||
let errorCheck = false;
|
||||
if (!firstName) {
|
||||
setFirstNameError(true);
|
||||
if (!name) {
|
||||
setNameError(true);
|
||||
errorCheck = true;
|
||||
} else {
|
||||
setFirstNameError(false);
|
||||
setNameError(false);
|
||||
}
|
||||
if (!lastName) {
|
||||
setLastNameError(true);
|
||||
if (!organizationName) {
|
||||
setOrganizationNameError(true);
|
||||
errorCheck = true;
|
||||
} else {
|
||||
setLastNameError(false);
|
||||
setOrganizationNameError(false);
|
||||
}
|
||||
errorCheck = passwordCheck({
|
||||
|
||||
errorCheck = checkPassword({
|
||||
password,
|
||||
setPasswordErrorLength,
|
||||
setPasswordErrorNumber,
|
||||
setPasswordErrorLowerCase,
|
||||
errorCheck
|
||||
commonPasswords,
|
||||
setErrors
|
||||
});
|
||||
|
||||
if (!errorCheck) {
|
||||
@@ -108,7 +123,6 @@ export default function UserInfoStep({
|
||||
async () => {
|
||||
client.createVerifier(async (err: any, result: { salt: string; verifier: string }) => {
|
||||
try {
|
||||
|
||||
// TODO: moduralize into KeyService
|
||||
const derivedKey = await deriveArgonKey({
|
||||
password,
|
||||
@@ -118,11 +132,11 @@ export default function UserInfoStep({
|
||||
parallelism: 1,
|
||||
hashLen: 32
|
||||
});
|
||||
|
||||
|
||||
if (!derivedKey) throw new Error('Failed to derive key from password');
|
||||
|
||||
const key = crypto.randomBytes(32);
|
||||
|
||||
|
||||
// create encrypted private key by encrypting the private
|
||||
// key with the symmetric key [key]
|
||||
const {
|
||||
@@ -133,7 +147,7 @@ export default function UserInfoStep({
|
||||
text: privateKey,
|
||||
secret: key
|
||||
});
|
||||
|
||||
|
||||
// create the protected key by encrypting the symmetric key
|
||||
// [key] with the derived key
|
||||
const {
|
||||
@@ -144,11 +158,11 @@ export default function UserInfoStep({
|
||||
text: key.toString('hex'),
|
||||
secret: Buffer.from(derivedKey.hash)
|
||||
});
|
||||
|
||||
|
||||
const response = await completeAccountInformationSignup({
|
||||
email,
|
||||
firstName,
|
||||
lastName,
|
||||
firstName: name.split(" ")[0],
|
||||
lastName: name.split(" ").slice(1).join(' '),
|
||||
protectedKey,
|
||||
protectedKeyIV,
|
||||
protectedKeyTag,
|
||||
@@ -156,14 +170,17 @@ export default function UserInfoStep({
|
||||
encryptedPrivateKey,
|
||||
encryptedPrivateKeyIV,
|
||||
encryptedPrivateKeyTag,
|
||||
providerAuthToken,
|
||||
salt: result.salt,
|
||||
verifier: result.verifier,
|
||||
organizationName: `${firstName}'s organization`
|
||||
organizationName,
|
||||
attributionSource,
|
||||
});
|
||||
|
||||
|
||||
// unset signup JWT token and set JWT token
|
||||
SecurityClient.setSignupToken('');
|
||||
SecurityClient.setToken(response.token);
|
||||
SecurityClient.setProviderAuthToken('');
|
||||
|
||||
saveTokenToLocalStorage({
|
||||
publicKey,
|
||||
@@ -184,7 +201,6 @@ export default function UserInfoStep({
|
||||
localStorage.setItem('projectData.id', project._id);
|
||||
|
||||
incrementStep();
|
||||
|
||||
} catch (error) {
|
||||
setIsLoading(false);
|
||||
console.error(error);
|
||||
@@ -198,110 +214,104 @@ export default function UserInfoStep({
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="bg-bunker w-max mx-auto h-7/12 py-10 px-8 rounded-xl drop-shadow-xl mb-36 md:mb-16">
|
||||
<p className="text-4xl font-bold flex justify-center mb-6 mx-8 md:mx-16 text-primary">
|
||||
{t('signup:step3-message')}
|
||||
<div className="h-full mx-auto mb-36 w-max rounded-xl md:px-8 md:mb-16">
|
||||
<p className="mx-8 mb-6 flex justify-center text-xl font-bold text-medium md:mx-16 text-transparent bg-clip-text bg-gradient-to-b from-white to-bunker-200">
|
||||
{t('signup.step3-message')}
|
||||
</p>
|
||||
<div className="relative z-0 flex items-center justify-end w-full md:p-2 rounded-lg max-h-24">
|
||||
<InputField
|
||||
label={t('common:first-name')}
|
||||
onChangeHandler={setFirstName}
|
||||
type="name"
|
||||
value={firstName}
|
||||
isRequired
|
||||
errorText={
|
||||
t('common:validate-required', {
|
||||
name: t('common:first-name')
|
||||
}) as string
|
||||
}
|
||||
error={firstNameError}
|
||||
autoComplete="given-name"
|
||||
/>
|
||||
</div>
|
||||
<div className="mt-2 flex items-center justify-center w-full md:p-2 rounded-lg max-h-24">
|
||||
<InputField
|
||||
label={t('common:last-name')}
|
||||
onChangeHandler={setLastName}
|
||||
type="name"
|
||||
value={lastName}
|
||||
isRequired
|
||||
errorText={
|
||||
t('common:validate-required', {
|
||||
name: t('common:last-name')
|
||||
}) as string
|
||||
}
|
||||
error={lastNameError}
|
||||
autoComplete="family-name"
|
||||
/>
|
||||
</div>
|
||||
<div className="mt-2 flex flex-col items-center justify-center w-full md:p-2 rounded-lg max-h-60">
|
||||
<InputField
|
||||
label={t('section-password:password')}
|
||||
onChangeHandler={(pass: string) => {
|
||||
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 ? (
|
||||
<div className="w-full mt-4 bg-white/5 px-2 flex flex-col items-start py-2 rounded-md">
|
||||
<div className="text-gray-400 text-sm mb-1">{t('section-password:validate-base')}</div>
|
||||
<div className="flex flex-row justify-start items-center ml-1">
|
||||
{passwordErrorLength ? (
|
||||
<FontAwesomeIcon icon={faX} className="text-md text-red mr-2.5" />
|
||||
) : (
|
||||
<FontAwesomeIcon icon={faCheck} className="text-md text-primary mr-2" />
|
||||
)}
|
||||
<div className={`${passwordErrorLength ? 'text-gray-400' : 'text-gray-600'} text-sm`}>
|
||||
{t('section-password:validate-length')}
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex flex-row justify-start items-center ml-1">
|
||||
{passwordErrorLowerCase ? (
|
||||
<FontAwesomeIcon icon={faX} className="text-md text-red mr-2.5" />
|
||||
) : (
|
||||
<FontAwesomeIcon icon={faCheck} className="text-md text-primary mr-2" />
|
||||
)}
|
||||
<div
|
||||
className={`${passwordErrorLowerCase ? 'text-gray-400' : 'text-gray-600'} text-sm`}
|
||||
>
|
||||
{t('section-password:validate-case')}
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex flex-row justify-start items-center ml-1">
|
||||
{passwordErrorNumber ? (
|
||||
<FontAwesomeIcon icon={faX} className="text-md text-red mr-2.5" />
|
||||
) : (
|
||||
<FontAwesomeIcon icon={faCheck} className="text-md text-primary mr-2" />
|
||||
)}
|
||||
<div className={`${passwordErrorNumber ? 'text-gray-400' : 'text-gray-600'} text-sm`}>
|
||||
{t('section-password:validate-number')}
|
||||
</div>
|
||||
<div className="h-full mx-auto mb-36 w-max rounded-xl py-6 md:px-8 md:mb-16 md:border md:border-mineshaft-600 md:bg-mineshaft-800">
|
||||
<div className="relative z-0 lg:w-1/6 w-1/4 min-w-[20rem] flex flex-col items-center justify-end w-full py-2 rounded-lg">
|
||||
<p className='text-left w-full text-sm text-bunker-300 mb-1 ml-1 font-medium'>Your Name</p>
|
||||
<Input
|
||||
placeholder="Jane Doe"
|
||||
onChange={(e) => setName(e.target.value)}
|
||||
value={name}
|
||||
isRequired
|
||||
autoComplete="given-name"
|
||||
className="h-12"
|
||||
/>
|
||||
{nameError && <p className='text-left w-full text-xs text-red-600 mt-1 ml-1'>Please, specify your name</p>}
|
||||
</div>
|
||||
<div className="relative z-0 lg:w-1/6 w-1/4 min-w-[20rem] flex flex-col items-center justify-end w-full py-2 rounded-lg">
|
||||
<p className='text-left w-full text-sm text-bunker-300 mb-1 ml-1 font-medium'>Organization Name</p>
|
||||
<Input
|
||||
placeholder="Infisical"
|
||||
onChange={(e) => setOrganizationName(e.target.value)}
|
||||
value={organizationName}
|
||||
isRequired
|
||||
className="h-12"
|
||||
/>
|
||||
{organizationNameError && <p className='text-left w-full text-xs text-red-600 mt-1 ml-1'>Please, specify your organization name</p>}
|
||||
</div>
|
||||
<div className="relative z-0 lg:w-1/6 w-1/4 min-w-[20rem] flex flex-col items-center justify-end w-full py-2 rounded-lg">
|
||||
<p className='text-left w-full text-sm text-bunker-300 mb-1 ml-1 font-medium'>Where did you hear about us? <span className="font-light">(optional)</span></p>
|
||||
<Input
|
||||
placeholder=""
|
||||
onChange={(e) => setAttributionSource(e.target.value)}
|
||||
value={attributionSource}
|
||||
isRequired
|
||||
className="h-12"
|
||||
/>
|
||||
</div>
|
||||
<div className="mt-2 flex lg:w-1/6 w-1/4 min-w-[20rem] max-h-60 w-full flex-col items-center justify-center rounded-lg py-2">
|
||||
<InputField
|
||||
label={t('section.password.password')}
|
||||
onChangeHandler={(pass: string) => {
|
||||
setPassword(pass);
|
||||
checkPassword({
|
||||
password: pass,
|
||||
commonPasswords,
|
||||
setErrors
|
||||
});
|
||||
}}
|
||||
type="password"
|
||||
value={password}
|
||||
isRequired
|
||||
error={Object.keys(errors).length > 0}
|
||||
autoComplete="new-password"
|
||||
id="new-password"
|
||||
/>
|
||||
{Object.keys(errors).length > 0 && (
|
||||
<div className="mt-4 flex w-full flex-col items-start rounded-md bg-white/5 px-2 py-2">
|
||||
<div className="mb-2 text-sm text-gray-400">{t('section.password.validate-base')}</div>
|
||||
{Object.keys(errors).map((key) => {
|
||||
if (errors[key as keyof Errors]) {
|
||||
return (
|
||||
<div
|
||||
className="ml-1 flex flex-row items-top justify-start"
|
||||
key={key}
|
||||
>
|
||||
<div>
|
||||
<FontAwesomeIcon
|
||||
icon={faXmark}
|
||||
className="text-md text-red ml-0.5 mr-2.5"
|
||||
/>
|
||||
</div>
|
||||
<p className="text-gray-400 text-sm">
|
||||
{errors[key as keyof Errors]}
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return null;
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex flex-col items-center justify-center lg:w-[19%] w-1/4 min-w-[20rem] mt-2 max-w-xs md:max-w-md mx-auto text-sm text-center md:text-left">
|
||||
<div className="text-l py-1 text-lg w-full">
|
||||
<Button
|
||||
onClick={signupErrorCheck}
|
||||
size="sm"
|
||||
isFullWidth
|
||||
className='h-14'
|
||||
colorSchema="primary"
|
||||
variant="outline_bg"
|
||||
isLoading={isLoading}
|
||||
> {String(t('signup.signup'))} </Button>
|
||||
</div>
|
||||
) : (
|
||||
<div className="py-2" />
|
||||
)}
|
||||
</div>
|
||||
<div className="flex flex-col items-center justify-center md:p-2 max-h-48 max-w-max mx-auto text-lg px-2 py-3">
|
||||
<Button
|
||||
text={t('signup:signup') ?? ''}
|
||||
loading={isLoading}
|
||||
onButtonPressed={signupErrorCheck}
|
||||
size="lg"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,8 +4,20 @@ import {
|
||||
setMfaTempToken,
|
||||
setSignupTempToken} from '@app/reactQuery';
|
||||
|
||||
|
||||
export const PROVIDER_AUTH_TOKEN_KEY = 'infisical__provider-auth-token';
|
||||
|
||||
// depreciated: go for apiRequest module in config/api
|
||||
export default class SecurityClient {
|
||||
|
||||
static setProviderAuthToken(tokenStr: string) {
|
||||
localStorage.setItem(PROVIDER_AUTH_TOKEN_KEY, tokenStr || '')
|
||||
}
|
||||
|
||||
static getProviderAuthToken() {
|
||||
return localStorage.getItem(PROVIDER_AUTH_TOKEN_KEY);
|
||||
}
|
||||
|
||||
static setSignupToken(tokenStr: string) {
|
||||
setSignupTempToken(tokenStr);
|
||||
}
|
||||
|
||||
@@ -26,9 +26,17 @@ interface IsLoginSuccessful {
|
||||
* @param {string} password - password of user to log in
|
||||
*/
|
||||
const attemptLogin = async (
|
||||
email: string,
|
||||
password: string
|
||||
{
|
||||
email,
|
||||
password,
|
||||
providerAuthToken,
|
||||
}: {
|
||||
email: string;
|
||||
password: string;
|
||||
providerAuthToken?: string;
|
||||
}
|
||||
): Promise<IsLoginSuccessful> => {
|
||||
|
||||
const telemetry = new Telemetry().getInstance();
|
||||
return new Promise((resolve, reject) => {
|
||||
client.init(
|
||||
@@ -39,7 +47,11 @@ const attemptLogin = async (
|
||||
async () => {
|
||||
try {
|
||||
const clientPublicKey = client.getPublicKey();
|
||||
const { serverPublicKey, salt } = await login1(email, clientPublicKey);
|
||||
const { serverPublicKey, salt } = await login1({
|
||||
email,
|
||||
clientPublicKey,
|
||||
providerAuthToken,
|
||||
});
|
||||
|
||||
client.setSalt(salt);
|
||||
client.setServerPublicKey(serverPublicKey);
|
||||
@@ -57,8 +69,11 @@ const attemptLogin = async (
|
||||
iv,
|
||||
tag
|
||||
} = await login2(
|
||||
email,
|
||||
clientProof
|
||||
{
|
||||
email,
|
||||
clientProof,
|
||||
providerAuthToken,
|
||||
}
|
||||
);
|
||||
|
||||
if (mfaEnabled) {
|
||||
@@ -80,7 +95,9 @@ const attemptLogin = async (
|
||||
token
|
||||
) {
|
||||
// case: MFA is not enabled
|
||||
|
||||
|
||||
// unset provider auth token in case it was used
|
||||
SecurityClient.setProviderAuthToken('');
|
||||
// set JWT token
|
||||
SecurityClient.setToken(token);
|
||||
|
||||
@@ -120,7 +137,7 @@ const attemptLogin = async (
|
||||
}
|
||||
|
||||
if (email) {
|
||||
telemetry.identify(email);
|
||||
telemetry.identify(email, email);
|
||||
telemetry.capture('User Logged In');
|
||||
}
|
||||
|
||||
@@ -137,4 +154,4 @@ const attemptLogin = async (
|
||||
});
|
||||
};
|
||||
|
||||
export default attemptLogin;
|
||||
export default attemptLogin;
|
||||
|
||||
@@ -23,10 +23,12 @@ const client = new jsrp.client();
|
||||
const attemptLoginMfa = async ({
|
||||
email,
|
||||
password,
|
||||
providerAuthToken,
|
||||
mfaToken
|
||||
}: {
|
||||
email: string;
|
||||
password: string;
|
||||
providerAuthToken?: string,
|
||||
mfaToken: string;
|
||||
}): Promise<Boolean> => {
|
||||
return new Promise((resolve, reject) => {
|
||||
@@ -36,7 +38,11 @@ const attemptLoginMfa = async ({
|
||||
}, async () => {
|
||||
try {
|
||||
const clientPublicKey = client.getPublicKey();
|
||||
const { salt } = await login1(email, clientPublicKey);
|
||||
const { salt } = await login1({
|
||||
email,
|
||||
clientPublicKey,
|
||||
providerAuthToken,
|
||||
});
|
||||
|
||||
const {
|
||||
encryptionVersion,
|
||||
@@ -56,6 +62,7 @@ const attemptLoginMfa = async ({
|
||||
// unset temporary (MFA) JWT token and set JWT token
|
||||
SecurityClient.setMfaToken('');
|
||||
SecurityClient.setToken(token);
|
||||
SecurityClient.setProviderAuthToken('');
|
||||
|
||||
const privateKey = await KeyService.decryptPrivateKey({
|
||||
encryptionVersion,
|
||||
|
||||
@@ -17,6 +17,7 @@ const passwordCheck = ({
|
||||
setPasswordErrorLowerCase,
|
||||
errorCheck
|
||||
}: PasswordCheckProps) => {
|
||||
|
||||
if (!password || password.length < 14) {
|
||||
setPasswordErrorLength(true);
|
||||
errorCheck = true;
|
||||
|
||||
@@ -0,0 +1,72 @@
|
||||
type Errors = {
|
||||
length?: string,
|
||||
upperCase?: string,
|
||||
lowerCase?: string,
|
||||
number?: string,
|
||||
specialChar?: string,
|
||||
repeatedChar?: string,
|
||||
commonPassword?: string
|
||||
};
|
||||
|
||||
interface CheckPasswordParams {
|
||||
password: string;
|
||||
commonPasswords: string[];
|
||||
setErrors: (value: Errors) => void;
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate that the password [password] is at least:
|
||||
* - 8 characters long
|
||||
* - Contains 1 uppercase character (A-Z)
|
||||
* - Contains 1 lowercase character (a-z)
|
||||
* - Contains 1 number (0-9)
|
||||
* - Does not contain 3 repeat, consecutive characters
|
||||
*
|
||||
* The function returns whether or not the password [password]
|
||||
* passes the minimum requirements above. It sets errors on
|
||||
* an erorr object via [setErrors].
|
||||
*
|
||||
* @param {Object} obj
|
||||
* @param {String} obj.password - the password to check
|
||||
* @param {Function} obj.setErrors - set state function to set error object
|
||||
*/
|
||||
const checkPassword = ({
|
||||
password,
|
||||
commonPasswords,
|
||||
setErrors
|
||||
}: CheckPasswordParams): boolean => {
|
||||
const errors: Errors = {};
|
||||
|
||||
if (password.length < 8) {
|
||||
errors.length = "8 characters";
|
||||
}
|
||||
|
||||
if (!/[A-Z]/.test(password)) {
|
||||
errors.upperCase = "1 uppercase character (A-Z)";
|
||||
}
|
||||
|
||||
if (!/[a-z]/.test(password)) {
|
||||
errors.lowerCase = "1 lowercase character (a-z)";
|
||||
}
|
||||
|
||||
if (!/[0-9]/.test(password)) {
|
||||
errors.number = "1 number (0-9)";
|
||||
}
|
||||
|
||||
if (!/[!@#$%^&*(),.?":{}|<>]/.test(password)) {
|
||||
errors.specialChar = "1 special character (!@#$%^&*(),.?)";
|
||||
}
|
||||
|
||||
if (/([A-Za-z0-9])\1\1\1/.test(password)) {
|
||||
errors.repeatedChar = "No 3 repeat, consecutive characters";
|
||||
}
|
||||
|
||||
if (commonPasswords.includes(password)) {
|
||||
errors.commonPassword = "No common passwords";
|
||||
}
|
||||
|
||||
setErrors(errors);
|
||||
return Object.keys(errors).length > 0;
|
||||
}
|
||||
|
||||
export default checkPassword;
|
||||
@@ -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 INTERCOM_ID = process.env.NEXT_PUBLIC_INTERCOM_ID!;
|
||||
|
||||
export {
|
||||
ENV,
|
||||
INTERCOM_ID,
|
||||
POSTHOG_API_KEY,
|
||||
POSTHOG_HOST,
|
||||
STRIPE_PRODUCT_PRO,
|
||||
STRIPE_PRODUCT_STARTER
|
||||
};
|
||||
STRIPE_PRODUCT_STARTER};
|
||||
@@ -125,6 +125,10 @@ const changePassword = async (
|
||||
setPasswordChanged(true);
|
||||
setCurrentPassword('');
|
||||
setNewPassword('');
|
||||
|
||||
window.location.href = '/login';
|
||||
|
||||
// move to login page
|
||||
} catch (error) {
|
||||
setCurrentPasswordError(true);
|
||||
console.log(error);
|
||||
|
||||
@@ -5,12 +5,52 @@ import aes from './aes-256-gcm';
|
||||
const nacl = require('tweetnacl');
|
||||
nacl.util = require('tweetnacl-util');
|
||||
|
||||
/**
|
||||
* Return new base64, NaCl, public-private key pair.
|
||||
* @returns {Object} obj
|
||||
* @returns {String} obj.publicKey - base64, NaCl, public key
|
||||
* @returns {String} obj.privateKey - base64, NaCl, private key
|
||||
*/
|
||||
const generateKeyPair = () => {
|
||||
const pair = nacl.box.keyPair();
|
||||
|
||||
return ({
|
||||
publicKey: nacl.util.encodeBase64(pair.publicKey),
|
||||
privateKey: nacl.util.encodeBase64(pair.secretKey)
|
||||
});
|
||||
}
|
||||
|
||||
type EncryptAsymmetricProps = {
|
||||
plaintext: string;
|
||||
publicKey: string;
|
||||
privateKey: string;
|
||||
};
|
||||
|
||||
/**
|
||||
* Verify that private key [privateKey] is the one that corresponds to
|
||||
* the public key [publicKey]
|
||||
* @param {Object}
|
||||
* @param {String} - base64-encoded Nacl private key
|
||||
* @param {String} - base64-encoded Nacl public key
|
||||
*/
|
||||
const verifyPrivateKey = ({
|
||||
privateKey,
|
||||
publicKey
|
||||
}: {
|
||||
privateKey: string;
|
||||
publicKey: string;
|
||||
}) => {
|
||||
const derivedPublicKey = nacl.util.encodeBase64(
|
||||
nacl.box.keyPair.fromSecretKey(
|
||||
nacl.util.decodeBase64(privateKey)
|
||||
).publicKey
|
||||
);
|
||||
|
||||
if (derivedPublicKey !== publicKey) {
|
||||
throw new Error('Failed to verify private key');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Derive a key from password [password] and salt [salt] using Argon2id
|
||||
* @param {Object} obj
|
||||
@@ -20,7 +60,7 @@ type EncryptAsymmetricProps = {
|
||||
* @param {Number} obj.time - number of iterations
|
||||
* @param {Number} obj.parallelism - desired parallelism
|
||||
* @param {Number} obj.hashLen - desired hash length (i.e. byte-length of derived key)
|
||||
* @returns
|
||||
* @returns
|
||||
*/
|
||||
const deriveArgonKey = async ({
|
||||
password,
|
||||
@@ -39,8 +79,8 @@ const deriveArgonKey = async ({
|
||||
}) => {
|
||||
let derivedKey;
|
||||
try {
|
||||
derivedKey = await argon2.hash({
|
||||
pass: password,
|
||||
derivedKey = await argon2.hash({
|
||||
pass: password,
|
||||
salt,
|
||||
type: argon2.ArgonType.Argon2id,
|
||||
mem,
|
||||
@@ -53,7 +93,7 @@ const deriveArgonKey = async ({
|
||||
}
|
||||
|
||||
return derivedKey;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Return assymmetrically encrypted [plaintext] using [publicKey] where
|
||||
@@ -173,6 +213,7 @@ type DecryptSymmetricProps = {
|
||||
*
|
||||
*/
|
||||
const decryptSymmetric = ({ ciphertext, iv, tag, key }: DecryptSymmetricProps): string => {
|
||||
if (!ciphertext) return '';
|
||||
let plaintext;
|
||||
try {
|
||||
plaintext = aes.decrypt({ ciphertext, iv, tag, secret: key });
|
||||
@@ -184,10 +225,11 @@ const decryptSymmetric = ({ ciphertext, iv, tag, key }: DecryptSymmetricProps):
|
||||
return plaintext;
|
||||
};
|
||||
|
||||
export {
|
||||
decryptAssymmetric,
|
||||
decryptSymmetric,
|
||||
export {
|
||||
decryptAssymmetric,
|
||||
decryptSymmetric,
|
||||
deriveArgonKey,
|
||||
encryptAssymmetric,
|
||||
encryptSymmetric
|
||||
};
|
||||
encryptSymmetric,
|
||||
generateKeyPair,
|
||||
verifyPrivateKey};
|
||||
|
||||
@@ -0,0 +1,64 @@
|
||||
/* eslint-disable prefer-template */
|
||||
/* eslint-disable prefer-rest-params */
|
||||
/* eslint-disable @typescript-eslint/no-unused-expressions */
|
||||
/* eslint-disable no-nested-ternary */
|
||||
/* eslint-disable no-unexpected-multiline */
|
||||
/* eslint-disable react-hooks/exhaustive-deps */
|
||||
/* eslint-disable vars-on-top */
|
||||
/* eslint-disable no-var */
|
||||
/* eslint-disable func-names */
|
||||
// @ts-nocheck
|
||||
|
||||
import { INTERCOM_ID as APP_ID } from '@app/components/utilities/config';
|
||||
|
||||
// Loads Intercom with the snippet
|
||||
// This must be run before boot, it initializes window.Intercom
|
||||
|
||||
// prettier-ignore
|
||||
export const load = () => {
|
||||
(function(){
|
||||
var w=window;
|
||||
var ic=w.Intercom;
|
||||
|
||||
if(typeof ic==="function"){
|
||||
ic('reattach_activator');
|
||||
ic('update',w.intercomSettings);
|
||||
} else {
|
||||
var d=document;
|
||||
var i=function() {
|
||||
i.c(arguments);
|
||||
};
|
||||
i.q=[];
|
||||
i.c=function(args) {
|
||||
i.q.push(args);
|
||||
};
|
||||
w.Intercom=i;
|
||||
var l=function() {
|
||||
var s=d.createElement('script');
|
||||
s.type='text/javascript';
|
||||
s.async=true;
|
||||
s.src='https://widget.intercom.io/widget/' + APP_ID;
|
||||
var x=d.getElementsByTagName('script')[0];
|
||||
x.parentNode.insertBefore(s, x);
|
||||
};
|
||||
if (document.readyState==='complete') {
|
||||
l();
|
||||
} else if (w.attachEvent) {
|
||||
w.attachEvent('onload',l);
|
||||
} else {
|
||||
w.addEventListener('load',l,false);
|
||||
}
|
||||
}
|
||||
})();
|
||||
}
|
||||
|
||||
// Initializes Intercom
|
||||
export const boot = (options = {}) => {
|
||||
window &&
|
||||
window.Intercom &&
|
||||
window.Intercom("boot", { app_id: APP_ID, ...options });
|
||||
};
|
||||
|
||||
export const update = () => {
|
||||
window && window.Intercom && window.Intercom("update");
|
||||
};
|
||||
@@ -0,0 +1,37 @@
|
||||
import { useEffect } from "react";
|
||||
import { useRouter } from "next/router";
|
||||
|
||||
import {
|
||||
boot as bootIntercom,
|
||||
load as loadIntercom,
|
||||
update as updateIntercom,
|
||||
} from "./intercom";
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
export const IntercomProvider = ({ children }: { children: any }) => {
|
||||
const router = useRouter();
|
||||
|
||||
if (typeof window !== "undefined") {
|
||||
loadIntercom();
|
||||
bootIntercom();
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
const handleRouteChange = (url: string) => {
|
||||
if (typeof window !== "undefined") {
|
||||
updateIntercom();
|
||||
}
|
||||
};
|
||||
|
||||
router.events.on("routeChangeStart", handleRouteChange);
|
||||
|
||||
// If the component is unmounted, unsubscribe
|
||||
// from the event with the `off` method:
|
||||
return () => {
|
||||
router.events.off("routeChangeStart", handleRouteChange);
|
||||
};
|
||||
}, [router.events]);
|
||||
|
||||
return children;
|
||||
};
|
||||
@@ -10,6 +10,7 @@ interface EncryptedSecretProps {
|
||||
id: string;
|
||||
createdAt: string;
|
||||
environment: string;
|
||||
secretName: string;
|
||||
secretCommentCiphertext: string;
|
||||
secretCommentIV: string;
|
||||
secretCommentTag: string;
|
||||
@@ -94,6 +95,7 @@ const encryptSecrets = async ({
|
||||
id: secret.id,
|
||||
createdAt: '',
|
||||
environment: env,
|
||||
secretName: secret.key,
|
||||
secretKeyCiphertext,
|
||||
secretKeyIV,
|
||||
secretKeyTag,
|
||||
|
||||
@@ -22,10 +22,12 @@ class Capturer {
|
||||
}
|
||||
}
|
||||
|
||||
identify(id: string) {
|
||||
identify(id: string, email?: string) {
|
||||
if (ENV === 'production' && TELEMETRY_CAPTURING_ENABLED) {
|
||||
try {
|
||||
this.api.identify(id);
|
||||
this.api.identify(id, {
|
||||
email: email
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('PostHog', error);
|
||||
}
|
||||
|
||||
@@ -1,52 +0,0 @@
|
||||
import { GetServerSideProps, GetStaticProps } from "next";
|
||||
import { serverSideTranslations } from "next-i18next/serverSideTranslations";
|
||||
|
||||
const DefaultNamespaces = ["common", "nav"];
|
||||
|
||||
type GetTranslatedStaticProps = (
|
||||
namespaces: string[],
|
||||
getStaticProps?: GetStaticProps<any>
|
||||
) => GetStaticProps;
|
||||
|
||||
type GetTranslatedServerSideProps = (
|
||||
namespaces: string[],
|
||||
getStaticProps?: GetServerSideProps<any>
|
||||
) => 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,
|
||||
},
|
||||
};
|
||||
};
|
||||
@@ -24,20 +24,23 @@ const buttonVariants = cva(
|
||||
{
|
||||
variants: {
|
||||
colorSchema: {
|
||||
primary: ['bg-primary', 'text-black', 'border-primary bg-opacity-80 hover:bg-opacity-100'],
|
||||
primary: ['bg-primary', 'text-black', 'border-primary bg-opacity-90 hover:bg-opacity-100'],
|
||||
secondary: ['bg-mineshaft', 'text-gray-300', 'border-mineshaft hover:bg-opacity-80'],
|
||||
danger: ['bg-red', 'text-white', 'border-red hover:bg-opacity-90']
|
||||
danger: ['bg-red', 'text-white', 'border-red hover:bg-opacity-90'],
|
||||
gray: ['bg-bunker-500', 'text-bunker-200']
|
||||
},
|
||||
variant: {
|
||||
solid: '',
|
||||
outline: ['bg-transparent', 'border-2', 'border-solid'],
|
||||
plain: '',
|
||||
selected: '',
|
||||
outline_bg: '',
|
||||
// a constant color not in use on hover or click goes colorSchema color
|
||||
star: 'text-bunker-200 bg-mineshaft-500'
|
||||
star: 'text-bunker-200 bg-mineshaft-700 border-mineshaft-600'
|
||||
},
|
||||
isDisabled: {
|
||||
true: 'bg-mineshaft opacity-40 cursor-not-allowed',
|
||||
false: ''
|
||||
true: 'bg-mineshaft-700 border border-mineshaft-600 text-white opacity-50 cursor-not-allowed',
|
||||
false: 'border'
|
||||
},
|
||||
isFullWidth: {
|
||||
true: 'w-full',
|
||||
@@ -58,7 +61,27 @@ const buttonVariants = cva(
|
||||
{
|
||||
colorSchema: 'primary',
|
||||
variant: 'star',
|
||||
className: 'hover:bg-primary hover:text-black'
|
||||
className: 'bg-mineshaft-700 border border-mineshaft-600 hover:bg-primary hover:text-black hover:border-primary-400 duration-100'
|
||||
},
|
||||
{
|
||||
colorSchema: 'primary',
|
||||
variant: 'solid',
|
||||
className: 'bg-primary-500 bg-opacity-90 hover:bg-primary-500 hover:text-black'
|
||||
},
|
||||
{
|
||||
colorSchema: 'primary',
|
||||
variant: 'selected',
|
||||
className: 'bg-primary/10 border border-primary/50 text-bunker-200'
|
||||
},
|
||||
{
|
||||
colorSchema: 'primary',
|
||||
variant: 'outline_bg',
|
||||
className: 'bg-mineshaft-600 border border-mineshaft-500 hover:bg-primary/[0.1] hover:border-primary/40 text-bunker-200'
|
||||
},
|
||||
{
|
||||
colorSchema: 'secondary',
|
||||
variant: 'star',
|
||||
className: 'bg-mineshaft-700 border border-mineshaft-600 hover:bg-mineshaft hover:text-white'
|
||||
},
|
||||
{
|
||||
colorSchema: 'danger',
|
||||
@@ -73,7 +96,7 @@ const buttonVariants = cva(
|
||||
{
|
||||
colorSchema: 'secondary',
|
||||
variant: 'outline',
|
||||
className: 'hover:bg-mineshaft'
|
||||
className: 'border-mineshaft-700 hover:border-mineshaft-500'
|
||||
},
|
||||
{
|
||||
colorSchema: 'danger',
|
||||
@@ -85,10 +108,15 @@ const buttonVariants = cva(
|
||||
variant: 'plain',
|
||||
className: 'text-primary'
|
||||
},
|
||||
{
|
||||
colorSchema: 'gray',
|
||||
variant: 'plain',
|
||||
className: 'bg-transparent text-bunker-200'
|
||||
},
|
||||
{
|
||||
colorSchema: 'secondary',
|
||||
variant: 'plain',
|
||||
className: 'text-mineshaft-300'
|
||||
className: 'text-mineshaft-300 hover:text-mineshaft-200 border-none'
|
||||
},
|
||||
{
|
||||
colorSchema: 'danger',
|
||||
@@ -149,32 +177,32 @@ export const Button = forwardRef<HTMLButtonElement, ButtonProps>(
|
||||
>
|
||||
{isLoading && (
|
||||
<img
|
||||
src="/images/loading/loadingblack.gif"
|
||||
src="/images/loading/loading.gif"
|
||||
width={36}
|
||||
alt="loading animation"
|
||||
className="absolute rounded-xl"
|
||||
className="absolute rounded-xl opacity-80"
|
||||
/>
|
||||
)}
|
||||
<span
|
||||
<div
|
||||
className={twMerge(
|
||||
'shrink-0 cursor-pointer transition-all',
|
||||
'inline-flex shrink-0 cursor-pointer items-center justify-center transition-all',
|
||||
loadingToggleClass,
|
||||
leftIcon && 'ml-2',
|
||||
size === 'xs' ? 'mr-1' : 'mr-3'
|
||||
size === 'xs' ? 'mr-1' : 'mr-2'
|
||||
)}
|
||||
>
|
||||
{leftIcon}
|
||||
</span>
|
||||
<span className={twMerge('transition-all', loadingToggleClass)}>{children}</span>
|
||||
<span
|
||||
</div>
|
||||
<span className={twMerge('transition-all', isFullWidth ? 'w-full' : 'w-min', loadingToggleClass)}>{children}</span>
|
||||
<div
|
||||
className={twMerge(
|
||||
'shrink-0 cursor-pointer transition-all',
|
||||
'inline-flex shrink-0 cursor-pointer items-center justify-center transition-all',
|
||||
loadingToggleClass,
|
||||
size === 'xs' ? 'ml-1' : 'ml-3'
|
||||
size === 'xs' ? 'ml-1' : 'ml-2'
|
||||
)}
|
||||
>
|
||||
{rightIcon}
|
||||
</span>
|
||||
</div>
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -10,7 +10,7 @@ export type CardTitleProps = {
|
||||
export const CardTitle = ({ children, className, subTitle }: CardTitleProps) => (
|
||||
<div
|
||||
className={twMerge(
|
||||
'px-6 py-4 mb-5 font-sans text-lg font-normal border-b border-mineshaft-600',
|
||||
'px-6 py-4 mb-5 font-sans text-lg font-normal border-b border-mineshaft-600 break-words',
|
||||
className
|
||||
)}
|
||||
>
|
||||
@@ -25,7 +25,7 @@ export type CardFooterProps = {
|
||||
};
|
||||
|
||||
export const CardFooter = ({ children, className }: CardFooterProps) => (
|
||||
<div className={twMerge('p-6 pt-0', className)}>{children}</div>
|
||||
<div className={twMerge('p-4 pt-0', className)}>{children}</div>
|
||||
);
|
||||
|
||||
export type CardBodyProps = {
|
||||
|
||||
@@ -73,10 +73,15 @@ export const DeleteActionModal = ({
|
||||
}
|
||||
onClose={onClose}
|
||||
>
|
||||
<form>
|
||||
<form
|
||||
onSubmit={(evt) => {
|
||||
evt.preventDefault();
|
||||
if (deleteKey === inputData) onDelete();
|
||||
}}
|
||||
>
|
||||
<FormControl
|
||||
label={
|
||||
<div className="pb-2 text-sm">
|
||||
<div className="break-words pb-2 text-sm">
|
||||
Type <span className="font-bold">{deleteKey}</span> to delete the resource
|
||||
</div>
|
||||
}
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
import type { Meta, StoryObj } from '@storybook/react';
|
||||
|
||||
import { Button } from '../Button';
|
||||
import { Drawer, DrawerContent, DrawerContentProps, DrawerTrigger } from './Drawer';
|
||||
|
||||
const meta: Meta<typeof Drawer> = {
|
||||
title: 'Components/Drawer',
|
||||
component: Drawer,
|
||||
tags: ['v2'],
|
||||
argTypes: {}
|
||||
};
|
||||
|
||||
export default meta;
|
||||
type Story = StoryObj<typeof DrawerContent>;
|
||||
|
||||
const Template = (args: DrawerContentProps) => (
|
||||
<Drawer>
|
||||
<DrawerTrigger asChild>
|
||||
<Button>Open</Button>
|
||||
</DrawerTrigger>
|
||||
<DrawerContent {...args}>Hello world</DrawerContent>
|
||||
</Drawer>
|
||||
);
|
||||
|
||||
export const Basic: Story = {
|
||||
render: (args) => <Template {...args} />,
|
||||
args: {
|
||||
title: 'Title',
|
||||
subTitle: 'Something as subtitle',
|
||||
footerContent: 'footer content'
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,84 @@
|
||||
import { forwardRef, ReactNode } from 'react';
|
||||
import { faTimes } from '@fortawesome/free-solid-svg-icons';
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||
import * as DialogPrimitive from '@radix-ui/react-dialog';
|
||||
import { cva, VariantProps } from 'cva';
|
||||
import { twMerge } from 'tailwind-merge';
|
||||
|
||||
import { Card, CardBody, CardFooter, CardTitle } from '../Card';
|
||||
import { IconButton } from '../IconButton';
|
||||
|
||||
export type DrawerContentProps = DialogPrimitive.DialogContentProps & {
|
||||
title?: ReactNode;
|
||||
subTitle?: ReactNode;
|
||||
footerContent?: ReactNode;
|
||||
onClose?: () => void;
|
||||
} & VariantProps<typeof drawerContentVariation>;
|
||||
|
||||
const drawerContentVariation = cva(
|
||||
'fixed ease-in-out duration-300 z-[90] border border-mineshaft-600 drop-shadow-2xl',
|
||||
{
|
||||
variants: {
|
||||
direction: {
|
||||
right: [
|
||||
'right-0 top-0',
|
||||
'h-full w-96',
|
||||
'data-[state=open]:animate-drawerRightIn data-[state=closed]:animate-drawerRightOut'
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
export const DrawerContent = forwardRef<HTMLDivElement, DrawerContentProps>(
|
||||
(
|
||||
{ children, title, subTitle, className, footerContent, direction = 'right', onClose, ...props },
|
||||
forwardedRef
|
||||
) => (
|
||||
<DialogPrimitive.Portal>
|
||||
<DialogPrimitive.Overlay
|
||||
className="fixed inset-0 z-[70] h-full w-full"
|
||||
style={{ backgroundColor: 'rgba(0, 0, 0, 0.7)' }}
|
||||
/>
|
||||
<DialogPrimitive.Content
|
||||
{...props}
|
||||
ref={forwardedRef}
|
||||
className={twMerge(drawerContentVariation({ direction, className }))}
|
||||
>
|
||||
<Card isRounded={false} className="h-full w-full dark">
|
||||
{title && (
|
||||
<CardTitle subTitle={subTitle} className="px-4">
|
||||
{title}
|
||||
</CardTitle>
|
||||
)}
|
||||
<CardBody className="flex-grow overflow-y-auto overflow-x-hidden px-4 dark:[color-scheme:dark]">
|
||||
{children}
|
||||
</CardBody>
|
||||
{footerContent && <CardFooter>{footerContent}</CardFooter>}{' '}
|
||||
<DialogPrimitive.Close aria-label="Close" asChild onClick={onClose}>
|
||||
<IconButton
|
||||
variant="plain"
|
||||
ariaLabel="close"
|
||||
className="absolute top-4 right-6 rounded text-bunker-400 hover:text-bunker-50"
|
||||
>
|
||||
<FontAwesomeIcon icon={faTimes} size="lg" className="cursor-pointer" />
|
||||
</IconButton>
|
||||
</DialogPrimitive.Close>
|
||||
</Card>
|
||||
</DialogPrimitive.Content>
|
||||
</DialogPrimitive.Portal>
|
||||
)
|
||||
);
|
||||
|
||||
DrawerContent.displayName = 'ModalContent';
|
||||
|
||||
export type DrawerProps = Omit<DialogPrimitive.DialogProps, 'open'> & { isOpen?: boolean };
|
||||
export const Drawer = ({ isOpen, ...props }: DrawerProps) => (
|
||||
<DialogPrimitive.Root open={isOpen} {...props} />
|
||||
);
|
||||
|
||||
export const DrawerTrigger = DialogPrimitive.Trigger;
|
||||
export type DrawerTriggerProps = DialogPrimitive.DialogTriggerProps;
|
||||
|
||||
export const DrawerClose = DialogPrimitive.Close;
|
||||
export type DrawerCloseProps = DialogPrimitive.DialogCloseProps;
|
||||
@@ -0,0 +1 @@
|
||||
export { Drawer, DrawerClose,DrawerContent, DrawerTrigger } from './Drawer';
|
||||
@@ -1,4 +1,5 @@
|
||||
import { ReactNode } from 'react';
|
||||
import { SizeProp } from '@fortawesome/fontawesome-svg-core';
|
||||
import { faCubesStacked, IconDefinition } from '@fortawesome/free-solid-svg-icons';
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||
import { twMerge } from 'tailwind-merge';
|
||||
@@ -8,13 +9,25 @@ type Props = {
|
||||
className?: string;
|
||||
children?: ReactNode;
|
||||
icon?: IconDefinition;
|
||||
iconSize?: SizeProp;
|
||||
};
|
||||
|
||||
export const EmptyState = ({ title, className, children, icon = faCubesStacked }: Props) => (
|
||||
<div className={twMerge('flex w-full bg-bunker-700 flex-col items-center px-2 pt-6 text-bunker-300', className)}>
|
||||
<FontAwesomeIcon icon={icon} size="2x" className='mr-4' />
|
||||
<div className='flex flex-row items-center py-4'>
|
||||
<div className="text-bunker-300 text-sm">{title}</div>
|
||||
export const EmptyState = ({
|
||||
title,
|
||||
className,
|
||||
children,
|
||||
icon = faCubesStacked,
|
||||
iconSize = '2x'
|
||||
}: Props) => (
|
||||
<div
|
||||
className={twMerge(
|
||||
'flex w-full flex-col items-center bg-mineshaft-800 px-2 pt-6 text-bunker-300',
|
||||
className
|
||||
)}
|
||||
>
|
||||
<FontAwesomeIcon icon={icon} size={iconSize} className="mr-4" />
|
||||
<div className="flex flex-row items-center py-4">
|
||||
<div className="text-sm text-bunker-300">{title}</div>
|
||||
<div>{children}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
import type { Meta, StoryObj } from '@storybook/react';
|
||||
|
||||
import { Button } from '../Button';
|
||||
import {
|
||||
HoverCard,
|
||||
HoverCardContent,
|
||||
HoverCardContentProps,
|
||||
HoverCardTrigger
|
||||
} from './HoverCardv2';
|
||||
|
||||
const meta: Meta<typeof HoverCardContent> = {
|
||||
title: 'Components/HoverCard',
|
||||
component: HoverCard,
|
||||
tags: ['v2']
|
||||
};
|
||||
|
||||
export default meta;
|
||||
type Story = StoryObj<typeof HoverCardContent>;
|
||||
|
||||
const Template = (args: HoverCardContentProps) => (
|
||||
<HoverCard>
|
||||
<HoverCardTrigger>
|
||||
<Button>Hello world</Button>
|
||||
</HoverCardTrigger>
|
||||
<HoverCardContent {...args} sideOffset={10}>
|
||||
<div className="bg-mineshaft-600 p-4 text-gray-400">
|
||||
<h1>This is hover card</h1>
|
||||
</div>
|
||||
</HoverCardContent>
|
||||
</HoverCard>
|
||||
);
|
||||
|
||||
// More on writing stories with args: https://storybook.js.org/docs/7.0/react/writing-stories/args
|
||||
export const Primary: Story = {
|
||||
render: (args) => <Template {...args} />
|
||||
};
|
||||
@@ -0,0 +1,26 @@
|
||||
import { ReactNode } from 'react';
|
||||
import * as HoverCardPrimitive from '@radix-ui/react-hover-card';
|
||||
import { twMerge } from 'tailwind-merge';
|
||||
|
||||
export const HoverCardTrigger = HoverCardPrimitive.Trigger;
|
||||
|
||||
export const HoverCard = HoverCardPrimitive.Root;
|
||||
|
||||
export type HoverCardContentProps = {
|
||||
children?: ReactNode;
|
||||
} & HoverCardPrimitive.HoverCardContentProps;
|
||||
|
||||
export const HoverCardContent = ({ children, className, ...props }: HoverCardContentProps) => (
|
||||
<HoverCardPrimitive.Portal>
|
||||
<HoverCardPrimitive.Content
|
||||
{...props}
|
||||
className={twMerge(
|
||||
'relative w-64 rounded-md bg-mineshaft-600 fill-mineshaft-600 p-4 pt-6 font-inter text-gray-200 shadow-md',
|
||||
className
|
||||
)}
|
||||
>
|
||||
{children}
|
||||
<HoverCardPrimitive.Arrow className="fill-inherit stroke-inherit" />
|
||||
</HoverCardPrimitive.Content>
|
||||
</HoverCardPrimitive.Portal>
|
||||
);
|
||||
@@ -0,0 +1 @@
|
||||
export { HoverCard,HoverCardContent, HoverCardTrigger } from './HoverCardv2';
|
||||
@@ -24,12 +24,14 @@ const iconButtonVariants = cva(
|
||||
colorSchema: {
|
||||
primary: ['bg-primary', 'text-black', 'border-primary hover:opacity-80'],
|
||||
secondary: ['bg-mineshaft', 'text-gray-300', 'border-mineshaft hover:bg-bunker-400'],
|
||||
danger: ['bg-[#973939] hover:bg-red', 'text-white', 'border-red']
|
||||
danger: ['bg-[#973939]', 'text-white', 'border-red']
|
||||
},
|
||||
variant: {
|
||||
solid: '',
|
||||
outline: ['bg-transparent', 'border-2', 'border-solid'],
|
||||
plain: ''
|
||||
plain: '',
|
||||
star: 'text-bunker-200 bg-mineshaft-500',
|
||||
outline_bg: ''
|
||||
},
|
||||
isDisabled: {
|
||||
true: 'bg-opacity-70 cursor-not-allowed',
|
||||
@@ -40,13 +42,28 @@ const iconButtonVariants = cva(
|
||||
false: ''
|
||||
},
|
||||
size: {
|
||||
xs: ['text-xs', 'py-2', 'px-2'],
|
||||
xs: ['text-xs', 'rounded-sm', 'py-1.5', 'px-2'],
|
||||
sm: ['text-sm', 'py-3', 'px-3'],
|
||||
md: ['text-md', 'py-4', 'px-4'],
|
||||
lg: ['text-lg', 'py-6', 'px-6']
|
||||
}
|
||||
},
|
||||
compoundVariants: [
|
||||
{
|
||||
colorSchema: 'primary',
|
||||
variant: 'star',
|
||||
className: 'hover:bg-primary hover:text-black'
|
||||
},
|
||||
{
|
||||
colorSchema: 'primary',
|
||||
variant: 'outline_bg',
|
||||
className: 'bg-mineshaft-700 border border-mineshaft-600 hover:bg-primary/[0.15] hover:border-primary/60 text-bunker-200 hover:text-bunker-100 duration-100'
|
||||
},
|
||||
{
|
||||
colorSchema: 'danger',
|
||||
variant: 'star',
|
||||
className: 'hover:bg-red hover:text-white'
|
||||
},
|
||||
{
|
||||
colorSchema: 'primary',
|
||||
variant: 'outline',
|
||||
@@ -65,7 +82,7 @@ const iconButtonVariants = cva(
|
||||
{
|
||||
colorSchema: 'primary',
|
||||
variant: 'plain',
|
||||
className: 'text-primary'
|
||||
className: 'hover:text-primary'
|
||||
},
|
||||
{
|
||||
colorSchema: 'secondary',
|
||||
@@ -75,7 +92,7 @@ const iconButtonVariants = cva(
|
||||
{
|
||||
colorSchema: 'danger',
|
||||
variant: 'plain',
|
||||
className: 'text-red'
|
||||
className: 'hover:text-red'
|
||||
},
|
||||
{
|
||||
colorSchema: ['danger', 'primary', 'secondary'],
|
||||
|
||||
@@ -9,10 +9,11 @@ type Props = {
|
||||
leftIcon?: ReactNode;
|
||||
rightIcon?: ReactNode;
|
||||
isDisabled?: boolean;
|
||||
isReadOnly?: boolean;
|
||||
};
|
||||
|
||||
const inputVariants = cva(
|
||||
'input w-full py-[0.375rem] text-gray-400 placeholder:text-sm placeholder-gray-500 placeholder-opacity-50 outline-none focus:ring-2',
|
||||
'input w-full py-[0.375rem] text-gray-400 placeholder:text-sm placeholder-gray-500 placeholder-opacity-50 outline-none focus:ring-2 hover:ring-[0.05rem] hover:ring-bunker-400/60 duration-100',
|
||||
{
|
||||
variants: {
|
||||
size: {
|
||||
@@ -26,13 +27,13 @@ const inputVariants = cva(
|
||||
false: ''
|
||||
},
|
||||
variant: {
|
||||
filled: ['bg-bunker-800', 'text-gray-400'],
|
||||
filled: ['bg-mineshaft-900', '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'
|
||||
false: 'focus:ring-primary-400/50 focus:ring-1'
|
||||
}
|
||||
},
|
||||
compoundVariants: []
|
||||
@@ -78,6 +79,7 @@ export const Input = forwardRef<HTMLInputElement, InputProps>(
|
||||
rightIcon,
|
||||
variant = 'filled',
|
||||
size = 'md',
|
||||
isReadOnly,
|
||||
...props
|
||||
},
|
||||
ref
|
||||
@@ -89,6 +91,7 @@ export const Input = forwardRef<HTMLInputElement, InputProps>(
|
||||
{...props}
|
||||
required={isRequired}
|
||||
ref={ref}
|
||||
readOnly={isReadOnly}
|
||||
disabled={isDisabled}
|
||||
className={twMerge(
|
||||
leftIcon ? 'pl-10' : 'pl-2.5',
|
||||
|
||||
@@ -1,7 +1,4 @@
|
||||
// eslint-disable-next-line import/no-extraneous-dependencies
|
||||
import { faKey, faUser } from '@fortawesome/free-solid-svg-icons';
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||
// eslint-disable-next-line import/no-extraneous-dependencies
|
||||
import { Meta, StoryObj } from '@storybook/react';
|
||||
|
||||
import { Menu, MenuGroup, MenuItem } from './Menu';
|
||||
@@ -74,14 +71,14 @@ export const WithIcons: Story = {
|
||||
render: (args) => (
|
||||
<Menu {...args}>
|
||||
<MenuGroup title="Group 1">
|
||||
<MenuItem isDisabled icon={<FontAwesomeIcon icon={faKey} />}>
|
||||
<MenuItem isDisabled icon="system-outline-90-lock-closed">
|
||||
Secrets
|
||||
</MenuItem>
|
||||
<MenuItem icon={<FontAwesomeIcon icon={faUser} />}>Members</MenuItem>
|
||||
<MenuItem icon="system-outline-96-groups">Members</MenuItem>
|
||||
</MenuGroup>
|
||||
<MenuGroup title="Group 2">
|
||||
<MenuItem icon={<FontAwesomeIcon icon={faKey} />}>Secrets</MenuItem>
|
||||
<MenuItem icon={<FontAwesomeIcon icon={faKey} />}>Members</MenuItem>
|
||||
<MenuItem icon="system-outline-90-lock-closed">Secrets</MenuItem>
|
||||
<MenuItem icon="system-outline-96-groups">Members</MenuItem>
|
||||
</MenuGroup>
|
||||
</Menu>
|
||||
),
|
||||
@@ -93,12 +90,12 @@ export const WithDescription: Story = {
|
||||
<Menu {...args}>
|
||||
<MenuItem
|
||||
isDisabled
|
||||
icon={<FontAwesomeIcon icon={faKey} />}
|
||||
icon="system-outline-90-lock-closed"
|
||||
description="Some random description"
|
||||
>
|
||||
Secrets
|
||||
</MenuItem>
|
||||
<MenuItem icon={<FontAwesomeIcon icon={faUser} />} description="Some random description">
|
||||
<MenuItem icon="system-outline-96-groups" description="Some random description">
|
||||
Members
|
||||
</MenuItem>
|
||||
</Menu>
|
||||
|
||||
@@ -1,4 +1,9 @@
|
||||
import { ComponentPropsWithRef, ElementType, ReactNode, Ref } from 'react';
|
||||
// @ts-nocheck
|
||||
/* eslint-disable import/no-extraneous-dependencies */
|
||||
/* eslint-disable global-require */
|
||||
import { ComponentPropsWithRef, ElementType, ReactNode, Ref, useRef } from 'react';
|
||||
import { motion } from 'framer-motion'
|
||||
import Lottie from 'lottie-react'
|
||||
import { twMerge } from 'tailwind-merge';
|
||||
|
||||
export type MenuProps = {
|
||||
@@ -14,7 +19,7 @@ export type MenuItemProps<T extends ElementType> = {
|
||||
// Kudos to https://itnext.io/react-polymorphic-components-with-typescript-f7ce72ea7af2
|
||||
as?: T;
|
||||
children: ReactNode;
|
||||
icon?: ReactNode;
|
||||
icon?: string;
|
||||
description?: ReactNode;
|
||||
isDisabled?: boolean;
|
||||
isSelected?: boolean;
|
||||
@@ -33,23 +38,43 @@ export const MenuItem = <T extends ElementType = 'button'>({
|
||||
// wrapping in forward ref with generic component causes the loss of ts definitions on props
|
||||
inputRef,
|
||||
...props
|
||||
}: MenuItemProps<T> & ComponentPropsWithRef<T>): JSX.Element => (
|
||||
<li
|
||||
className={twMerge(
|
||||
'px-1 py-2.5 mt-0.5 font-inter flex flex-col text-sm text-bunker-100 transition-all rounded cursor-pointer hover:bg-mineshaft-600 duration-50',
|
||||
isSelected && 'bg-mineshaft-500',
|
||||
isDisabled && 'hover:bg-transparent cursor-not-allowed',
|
||||
className
|
||||
)}
|
||||
>
|
||||
<Item type="button" role="menuitem" className="flex items-center relative" ref={inputRef} {...props}>
|
||||
<div className={`${isSelected ? "visisble" : "invisible"} absolute w-[0.25rem] rounded-md h-8 bg-primary`}/>
|
||||
{icon && <span className="mr-3 ml-4 w-5">{icon}</span>}
|
||||
<span className="flex-grow text-left">{children}</span>
|
||||
</Item>
|
||||
{description && <span className="mt-2 text-xs">{description}</span>}
|
||||
</li>
|
||||
);
|
||||
}: MenuItemProps<T> & ComponentPropsWithRef<T>): JSX.Element => {
|
||||
const iconRef = useRef()
|
||||
|
||||
return(
|
||||
<a
|
||||
onMouseEnter={() => iconRef.current?.play()}
|
||||
onMouseLeave={() => iconRef.current?.stop()}
|
||||
>
|
||||
<li
|
||||
className={twMerge(
|
||||
'group px-1 py-2.5 mt-0.5 font-inter flex flex-col text-sm text-bunker-100 transition-all rounded cursor-pointer hover:bg-mineshaft-700 duration-50',
|
||||
isSelected && 'bg-mineshaft-600 hover:bg-mineshaft-600',
|
||||
isDisabled && 'hover:bg-transparent cursor-not-allowed',
|
||||
className
|
||||
)}
|
||||
>
|
||||
<motion.span className="w-full flex flex-row items-center justify-start rounded-sm">
|
||||
<Item type="button" role="menuitem" className="flex items-center relative" ref={inputRef} {...props}>
|
||||
<div className={`${isSelected ? "visisble" : "invisible"} absolute w-[0.25rem] rounded-md h-8 bg-primary`}/>
|
||||
{/* {icon && <span className="mr-3 ml-4 w-5 block group-hover:hidden">{icon}</span>} */}
|
||||
<Lottie
|
||||
lottieRef={iconRef}
|
||||
style={{ width: 24, height: 24 }}
|
||||
// eslint-disable-next-line import/no-dynamic-require
|
||||
animationData={require(`../../../../public/lotties/${icon}.json`)}
|
||||
loop={false}
|
||||
autoplay={false}
|
||||
className="my-auto ml-3 mr-3"
|
||||
/>
|
||||
<span className="flex-grow text-left">{children}</span>
|
||||
</Item>
|
||||
{description && <span className="mt-2 text-xs">{description}</span>}
|
||||
</motion.span>
|
||||
</li>
|
||||
</a>
|
||||
)
|
||||
};
|
||||
|
||||
MenuItem.displayName = 'MenuItem';
|
||||
|
||||
|
||||
@@ -12,20 +12,24 @@ export type ModalContentProps = DialogPrimitive.DialogContentProps & {
|
||||
subTitle?: ReactNode;
|
||||
footerContent?: ReactNode;
|
||||
onClose?: () => void;
|
||||
overlayClassName?: string;
|
||||
};
|
||||
|
||||
export const ModalContent = forwardRef<HTMLDivElement, ModalContentProps>(
|
||||
({ children, title, subTitle, className, footerContent, onClose, ...props }, forwardedRef) => (
|
||||
(
|
||||
{ children, title, subTitle, className, overlayClassName, footerContent, onClose, ...props },
|
||||
forwardedRef
|
||||
) => (
|
||||
<DialogPrimitive.Portal>
|
||||
<DialogPrimitive.Overlay
|
||||
className="fixed inset-0 h-full w-full animate-fadeIn z-[70]"
|
||||
className={twMerge('fixed inset-0 z-[70] h-full w-full animate-fadeIn', overlayClassName)}
|
||||
style={{ backgroundColor: 'rgba(0, 0, 0, 0.7)' }}
|
||||
/>
|
||||
<DialogPrimitive.Content {...props} ref={forwardedRef}>
|
||||
<Card
|
||||
isRounded
|
||||
className={twMerge(
|
||||
'fixed top-1/2 left-1/2 max-w-lg border border-mineshaft-600 -translate-y-2/4 -translate-x-2/4 animate-popIn drop-shadow-2xl z-[90]',
|
||||
'fixed top-1/2 left-1/2 z-[90] max-w-lg -translate-y-2/4 -translate-x-2/4 animate-popIn border border-mineshaft-600 drop-shadow-2xl',
|
||||
className
|
||||
)}
|
||||
>
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
import type { Meta, StoryObj } from '@storybook/react';
|
||||
|
||||
import { Button } from '../Button';
|
||||
import { Popover, PopoverContent, PopoverContentProps, PopoverTrigger } from './Popoverv2';
|
||||
|
||||
const meta: Meta<typeof PopoverContent> = {
|
||||
title: 'Components/Popover',
|
||||
component: Popover,
|
||||
tags: ['v2']
|
||||
};
|
||||
|
||||
export default meta;
|
||||
type Story = StoryObj<typeof PopoverContent>;
|
||||
|
||||
const Template = (args: PopoverContentProps) => (
|
||||
<Popover>
|
||||
<PopoverTrigger asChild>
|
||||
<Button>Hello world</Button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent {...args} side="bottom" sideOffset={10}>
|
||||
<h1>This is popper</h1>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
);
|
||||
|
||||
// More on writing stories with args: https://storybook.js.org/docs/7.0/react/writing-stories/args
|
||||
export const Primary: Story = {
|
||||
render: (args) => <Template {...args} />
|
||||
};
|
||||
@@ -0,0 +1,54 @@
|
||||
import { ReactNode } from 'react';
|
||||
import { faTimes } from '@fortawesome/free-solid-svg-icons';
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||
import * as PopoverPrimitive from '@radix-ui/react-popover';
|
||||
import { twMerge } from 'tailwind-merge';
|
||||
|
||||
import { IconButton } from '../IconButton';
|
||||
|
||||
export const PopoverTrigger = PopoverPrimitive.Trigger;
|
||||
|
||||
export const Popover = PopoverPrimitive.Root;
|
||||
|
||||
export type PopoverContentProps = {
|
||||
children?: ReactNode;
|
||||
hideCloseBtn?: boolean;
|
||||
} & PopoverPrimitive.PopoverContentProps;
|
||||
|
||||
export const PopoverContent = ({
|
||||
children,
|
||||
className,
|
||||
hideCloseBtn,
|
||||
...props
|
||||
}: PopoverContentProps) => (
|
||||
<PopoverPrimitive.Portal>
|
||||
<PopoverPrimitive.Content
|
||||
className={twMerge(
|
||||
[
|
||||
'relative z-[100] w-64 rounded-md bg-mineshaft-600 fill-mineshaft-600 p-4 pt-6 font-inter text-gray-200 shadow-md',
|
||||
// animation
|
||||
'data-[state=open]:data-[side=bottom]:animate-slideUpAndFade',
|
||||
'data-[state=open]:data-[side=top]:animate-slideDownAndFade',
|
||||
'data-[state=open]:data-[side=left]:animate-slideRightAndFade',
|
||||
'data-[state=open]:data-[side=right]:animate-slideLeftAndFade'
|
||||
],
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
{!hideCloseBtn && (
|
||||
<PopoverPrimitive.Close aria-label="Close" asChild>
|
||||
<IconButton
|
||||
variant="plain"
|
||||
ariaLabel="close"
|
||||
className="absolute top-0 right-1 rounded text-bunker-400 hover:text-bunker-50"
|
||||
>
|
||||
<FontAwesomeIcon icon={faTimes} size="lg" className="cursor-pointer" />
|
||||
</IconButton>
|
||||
</PopoverPrimitive.Close>
|
||||
)}
|
||||
<PopoverPrimitive.Arrow className="fill-inherit" />
|
||||
</PopoverPrimitive.Content>
|
||||
</PopoverPrimitive.Portal>
|
||||
);
|
||||
@@ -0,0 +1 @@
|
||||
export { Popover,PopoverContent, PopoverTrigger } from './Popoverv2';
|
||||
@@ -1,6 +1,6 @@
|
||||
import { forwardRef, ReactNode } from 'react';
|
||||
import { IconProp } from '@fortawesome/fontawesome-svg-core';
|
||||
import { faCheck, faChevronDown, faChevronUp } from '@fortawesome/free-solid-svg-icons';
|
||||
import { faCaretDown, faCaretUp,faCheck } from '@fortawesome/free-solid-svg-icons';
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||
import * as SelectPrimitive from '@radix-ui/react-select';
|
||||
import { twMerge } from 'tailwind-merge';
|
||||
@@ -40,7 +40,7 @@ export const Select = forwardRef<HTMLButtonElement, SelectProps>(
|
||||
ref={ref}
|
||||
className={twMerge(
|
||||
`inline-flex items-center justify-between rounded-md
|
||||
bg-bunker-800 px-3 py-2 font-inter text-sm font-normal text-bunker-200 outline-none data-[placeholder]:text-gray-500`,
|
||||
bg-mineshaft-900 px-3 py-2 font-inter text-sm font-normal text-bunker-200 outline-none data-[placeholder]:text-gray-500`,
|
||||
className
|
||||
)}
|
||||
>
|
||||
@@ -49,21 +49,23 @@ export const Select = forwardRef<HTMLButtonElement, SelectProps>(
|
||||
</SelectPrimitive.Value>
|
||||
{!isDisabled && (
|
||||
<SelectPrimitive.Icon className="ml-3">
|
||||
<FontAwesomeIcon icon={faChevronDown} size="sm" />
|
||||
<FontAwesomeIcon icon={faCaretDown} size="sm" />
|
||||
</SelectPrimitive.Icon>
|
||||
)}
|
||||
</SelectPrimitive.Trigger>
|
||||
<SelectPrimitive.Portal>
|
||||
<SelectPrimitive.Content
|
||||
className={twMerge(
|
||||
'relative top-1 z-[100] overflow-hidden rounded-md bg-bunker-800 font-inter text-bunker-100 shadow-md',
|
||||
'relative top-1 z-[100] overflow-hidden rounded-md bg-mineshaft-900 border border-mineshaft-600 font-inter text-bunker-100 shadow-md',
|
||||
dropdownContainerClassName
|
||||
)}
|
||||
position={position}
|
||||
style={{ width: 'var(--radix-select-trigger-width)' }}
|
||||
>
|
||||
<SelectPrimitive.ScrollUpButton>
|
||||
<FontAwesomeIcon icon={faChevronUp} size="sm" />
|
||||
<div className="flex items-center justify-center">
|
||||
<FontAwesomeIcon icon={faCaretUp} size="sm" />
|
||||
</div>
|
||||
</SelectPrimitive.ScrollUpButton>
|
||||
<SelectPrimitive.Viewport className="p-1">
|
||||
{isLoading ? (
|
||||
@@ -76,7 +78,9 @@ export const Select = forwardRef<HTMLButtonElement, SelectProps>(
|
||||
)}
|
||||
</SelectPrimitive.Viewport>
|
||||
<SelectPrimitive.ScrollDownButton>
|
||||
<FontAwesomeIcon icon={faChevronDown} size="sm" />
|
||||
<div className="flex items-center justify-center">
|
||||
<FontAwesomeIcon icon={faCaretDown} size="sm" />
|
||||
</div>
|
||||
</SelectPrimitive.ScrollDownButton>
|
||||
</SelectPrimitive.Content>
|
||||
</SelectPrimitive.Portal>
|
||||
|
||||
@@ -19,7 +19,7 @@ export const Switch = ({
|
||||
isRequired,
|
||||
...props
|
||||
}: SwitchProps): JSX.Element => (
|
||||
<div className="flex items-center text-bunker-300 font-inter">
|
||||
<div className="flex items-center font-inter text-bunker-300">
|
||||
<label className="text-sm" htmlFor={id}>
|
||||
{children}
|
||||
{isRequired && <span className="pl-1 text-red">*</span>}
|
||||
@@ -30,13 +30,13 @@ export const Switch = ({
|
||||
checked={isChecked}
|
||||
disabled={isDisabled}
|
||||
className={twMerge(
|
||||
'h-5 ml-3 transition-all rounded-full w-9 bg-bunker-300 data-[state=checked]:bg-bunker-200',
|
||||
'ml-3 h-5 w-9 rounded-full bg-bunker-300 transition-all data-[state=checked]:bg-primary',
|
||||
isDisabled && 'bg-bunker-400 hover:bg-bunker-400',
|
||||
className
|
||||
)}
|
||||
id={id}
|
||||
>
|
||||
<SwitchPrimitive.Thumb className="w-4 h-4 border-none will-change-transform rounded-full shadow bg-black block transition-all translate-x-0.5 data-[state=checked]:translate-x-[18px]" />
|
||||
<SwitchPrimitive.Thumb className="block h-4 w-4 translate-x-0.5 rounded-full border-none bg-black shadow transition-all will-change-transform data-[state=checked]:translate-x-[18px]" />
|
||||
</SwitchPrimitive.Root>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -16,8 +16,8 @@ export const TableContainer = ({
|
||||
}: TableContainerProps): JSX.Element => (
|
||||
<div
|
||||
className={twMerge(
|
||||
'relative w-full overflow-x-auto border border-solid border-mineshaft-700 bg-mineshaft-800 font-inter shadow-md',
|
||||
isRounded && 'rounded-md',
|
||||
'relative w-full overflow-x-auto border border-solid border-mineshaft-700 bg-mineshaft-800 font-inter',
|
||||
isRounded && 'rounded-lg',
|
||||
className
|
||||
)}
|
||||
>
|
||||
@@ -34,7 +34,7 @@ export type TableProps = {
|
||||
export const Table = ({ children, className }: TableProps): JSX.Element => (
|
||||
<table
|
||||
className={twMerge(
|
||||
'w-full rounded-md bg-bunker-800 p-2 text-left text-sm text-gray-300',
|
||||
'w-full bg-mineshaft-800 p-2 text-left text-sm text-gray-300',
|
||||
className
|
||||
)}
|
||||
>
|
||||
@@ -49,7 +49,7 @@ export type THeadProps = {
|
||||
};
|
||||
|
||||
export const THead = ({ children, className }: THeadProps): JSX.Element => (
|
||||
<thead className={twMerge('bg-bunker text-xs uppercase text-bunker-300', className)}>
|
||||
<thead className={twMerge('bg-mineshaft-800 text-xs uppercase text-bunker-300', className)}>
|
||||
{children}
|
||||
</thead>
|
||||
);
|
||||
@@ -62,7 +62,7 @@ export type TrProps = {
|
||||
|
||||
export const Tr = ({ children, className, ...props }: TrProps): JSX.Element => (
|
||||
<tr
|
||||
className={twMerge('border border-solid border-mineshaft-700 hover:bg-bunker-700', className)}
|
||||
className={twMerge('border border-solid border-mineshaft-700 cursor-default', className)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
@@ -76,7 +76,7 @@ export type ThProps = {
|
||||
};
|
||||
|
||||
export const Th = ({ children, className }: ThProps): JSX.Element => (
|
||||
<th className={twMerge('bg-bunker-500 px-5 pt-4 pb-3.5 font-semibold', className)}>{children}</th>
|
||||
<th className={twMerge('bg-mineshaft-800 px-5 pt-4 pb-3.5 font-semibold border-b-2 border-mineshaft-600', className)}>{children}</th>
|
||||
);
|
||||
|
||||
// table body
|
||||
|
||||
@@ -1,24 +1,56 @@
|
||||
import { ReactNode } from 'react';
|
||||
import { faClose } from '@fortawesome/free-solid-svg-icons';
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||
import { cva, VariantProps } from 'cva';
|
||||
import { twMerge } from 'tailwind-merge';
|
||||
|
||||
type Props = {
|
||||
children: ReactNode;
|
||||
className?: string;
|
||||
onClose?: () => void;
|
||||
color?: string;
|
||||
isDisabled?: boolean;
|
||||
} & VariantProps<typeof tagVariants>;
|
||||
|
||||
const tagVariants = cva('inline-flex whitespace-nowrap text-sm rounded-sm mr-1.5 text-bunker-200 ', {
|
||||
variants: {
|
||||
colorSchema: {
|
||||
gray: 'bg-mineshaft-500',
|
||||
red: 'bg-red/80 text-bunker-100'
|
||||
},
|
||||
size: {
|
||||
sm: 'px-1.5 py-0.5'
|
||||
const tagVariants = cva(
|
||||
'inline-flex items-center whitespace-nowrap text-sm rounded-sm mr-1.5 text-bunker-200',
|
||||
{
|
||||
variants: {
|
||||
colorSchema: {
|
||||
gray: 'bg-mineshaft-500',
|
||||
red: 'bg-red/80 text-bunker-100',
|
||||
green: 'bg-primary-800 text-white'
|
||||
},
|
||||
size: {
|
||||
sm: 'px-2 py-0.5'
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
);
|
||||
|
||||
export const Tag = ({ children, className, colorSchema = 'gray', size = 'sm' }: Props) => (
|
||||
<div className={twMerge(tagVariants({ colorSchema, className, size }))}>{children}</div>
|
||||
export const Tag = ({
|
||||
children,
|
||||
className,
|
||||
colorSchema = 'gray',
|
||||
color,
|
||||
isDisabled,
|
||||
size = 'sm',
|
||||
onClose
|
||||
}: Props) => (
|
||||
<div
|
||||
className={twMerge(tagVariants({ colorSchema, className, size }))}
|
||||
style={{ backgroundColor: color }}
|
||||
>
|
||||
{children}
|
||||
{onClose && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={onClose}
|
||||
disabled={isDisabled}
|
||||
className="ml-2 flex items-center justify-center"
|
||||
>
|
||||
<FontAwesomeIcon icon={faClose} />
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -3,6 +3,7 @@ import { cva, VariantProps } from 'cva';
|
||||
import { twMerge } from 'tailwind-merge';
|
||||
|
||||
type Props = {
|
||||
isDisabled?: boolean;
|
||||
placeholder?: string;
|
||||
isFullWidth?: boolean;
|
||||
isRequired?: boolean;
|
||||
@@ -10,7 +11,7 @@ type Props = {
|
||||
};
|
||||
|
||||
const textAreaVariants = cva(
|
||||
'textarea w-full p-2 border border-solid text-gray-400 font-inter placeholder-gray-500 placeholder-opacity-50',
|
||||
'textarea w-full p-2 focus:ring-2 ring-primary-800 outline-none border border-solid text-gray-400 font-inter placeholder-gray-500 placeholder-opacity-50',
|
||||
{
|
||||
variants: {
|
||||
size: {
|
||||
@@ -43,7 +44,7 @@ const textAreaVariants = cva(
|
||||
}
|
||||
);
|
||||
|
||||
export type TextAreaProps = Omit<TextareaHTMLAttributes<HTMLTextAreaElement>, 'size'> &
|
||||
export type TextAreaProps = Omit<TextareaHTMLAttributes<HTMLTextAreaElement>, 'size' | 'disabled'> &
|
||||
VariantProps<typeof textAreaVariants> &
|
||||
Props;
|
||||
|
||||
@@ -52,6 +53,7 @@ export const TextArea = forwardRef<HTMLTextAreaElement, TextAreaProps>(
|
||||
{
|
||||
className,
|
||||
isRounded = true,
|
||||
isDisabled = false,
|
||||
isError = false,
|
||||
isRequired,
|
||||
variant = 'filled',
|
||||
@@ -66,6 +68,7 @@ export const TextArea = forwardRef<HTMLTextAreaElement, TextAreaProps>(
|
||||
style={{ resize: reSize }}
|
||||
required={isRequired}
|
||||
ref={ref}
|
||||
disabled={isDisabled}
|
||||
className={twMerge(textAreaVariants({ className, isError, size, isRounded, variant }))}
|
||||
/>
|
||||
)
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
import type { Meta, StoryObj } from '@storybook/react';
|
||||
|
||||
import { Tooltip, TooltipProps, TooltipProvider } from './Tooltip';
|
||||
|
||||
const meta: Meta<typeof Tooltip> = {
|
||||
title: 'Components/Tooltip',
|
||||
component: Tooltip,
|
||||
tags: ['v2'],
|
||||
args: {
|
||||
content: 'Hi'
|
||||
}
|
||||
};
|
||||
|
||||
export default meta;
|
||||
type Story = StoryObj<typeof Tooltip>;
|
||||
|
||||
const Template = (args: TooltipProps) => (
|
||||
<TooltipProvider>
|
||||
<Tooltip {...args}>
|
||||
<div className="text-white">Hello infisical</div>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
);
|
||||
|
||||
// More on writing stories with args: https://storybook.js.org/docs/7.0/react/writing-stories/args
|
||||
export const Primary: Story = {
|
||||
render: (args) => <Template {...args} />
|
||||
};
|
||||
@@ -0,0 +1,53 @@
|
||||
import { ReactNode } from 'react';
|
||||
import * as TooltipPrimitive from '@radix-ui/react-tooltip';
|
||||
import { twMerge } from 'tailwind-merge';
|
||||
|
||||
export type TooltipProps = {
|
||||
children: ReactNode;
|
||||
content?: ReactNode;
|
||||
isOpen?: boolean;
|
||||
className?: string;
|
||||
asChild?: boolean;
|
||||
onOpenChange?: (isOpen: boolean) => void;
|
||||
defaultOpen?: boolean;
|
||||
} & Omit<TooltipPrimitive.TooltipContentProps, 'open'>;
|
||||
|
||||
export const Tooltip = ({
|
||||
children,
|
||||
content,
|
||||
isOpen,
|
||||
onOpenChange,
|
||||
defaultOpen,
|
||||
className,
|
||||
asChild = true,
|
||||
...props
|
||||
}: TooltipProps) => (
|
||||
<TooltipPrimitive.Root
|
||||
delayDuration={150}
|
||||
open={isOpen}
|
||||
defaultOpen={defaultOpen}
|
||||
onOpenChange={onOpenChange}
|
||||
>
|
||||
<TooltipPrimitive.Trigger asChild={asChild}>{children}</TooltipPrimitive.Trigger>
|
||||
<TooltipPrimitive.Content
|
||||
side="top"
|
||||
align="center"
|
||||
sideOffset={5}
|
||||
{...props}
|
||||
className={twMerge(
|
||||
`z-50 max-w-[15rem] select-none rounded-md border border-mineshaft-600 bg-mineshaft-800 py-2 px-4 text-sm font-light text-bunker-200 shadow-md
|
||||
data-[state=delayed-open]:data-[side=top]:animate-slideDownAndFade
|
||||
data-[state=delayed-open]:data-[side=right]:animate-slideLeftAndFade
|
||||
data-[state=delayed-open]:data-[side=left]:animate-slideRightAndFade
|
||||
data-[state=delayed-open]:data-[side=bottom]:animate-slideUpAndFade
|
||||
`,
|
||||
className
|
||||
)}
|
||||
>
|
||||
{content}
|
||||
<TooltipPrimitive.Arrow width={11} height={5} className="fill-mineshaft-600" />
|
||||
</TooltipPrimitive.Content>
|
||||
</TooltipPrimitive.Root>
|
||||
);
|
||||
|
||||
export const TooltipProvider = TooltipPrimitive.Provider;
|
||||
@@ -0,0 +1 @@
|
||||
export { Tooltip, TooltipProvider } from './Tooltip';
|
||||
@@ -18,7 +18,7 @@ export const UpgradePlanModal = ({ text, isOpen, onOpenChange }: Props): JSX.Ele
|
||||
href={`/settings/billing/${localStorage.getItem('projectData.id') as string}`}
|
||||
key="upgrade-plan"
|
||||
>
|
||||
<Button className="mr-4">Upgrade Plan</Button>
|
||||
<Button className="mr-4 ml-2 mb-2">Upgrade Plan</Button>
|
||||
</Link>,
|
||||
<ModalClose asChild key="upgrade-plan-cancel">
|
||||
<Button colorSchema="secondary" variant="plain">
|
||||
@@ -27,8 +27,8 @@ export const UpgradePlanModal = ({ text, isOpen, onOpenChange }: Props): JSX.Ele
|
||||
</ModalClose>
|
||||
]}
|
||||
>
|
||||
<p className="mb-4 text-bunker-300">{text}</p>
|
||||
<p className="font-medium text-bunker-300">
|
||||
<p className="mb-2 text-bunker-300">{text}</p>
|
||||
<p className="text-bunker-300">
|
||||
Upgrade and get access to this, as well as to other powerful enhancements.
|
||||
</p>
|
||||
</ModalContent>
|
||||
|
||||
@@ -2,14 +2,17 @@ export * from './Button';
|
||||
export * from './Card';
|
||||
export * from './Checkbox';
|
||||
export * from './DeleteActionModal';
|
||||
export * from './Drawer';
|
||||
export * from './Dropdown';
|
||||
export * from './EmailServiceSetupModal'
|
||||
export * from './EmptyState';
|
||||
export * from './FormControl';
|
||||
export * from './HoverCardv2';
|
||||
export * from './IconButton';
|
||||
export * from './Input';
|
||||
export * from './Menu';
|
||||
export * from './Modal';
|
||||
export * from './Popoverv2';
|
||||
export * from './Select';
|
||||
export * from './Skeleton';
|
||||
export * from './Spinner';
|
||||
@@ -17,4 +20,5 @@ export * from './Switch';
|
||||
export * from './Table';
|
||||
export * from './Tag';
|
||||
export * from './TextArea';
|
||||
export * from './Tooltip';
|
||||
export * from './UpgradePlanModal';
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import axios from 'axios';
|
||||
|
||||
import SecurityClient from '@app/components/utilities/SecurityClient';
|
||||
import {
|
||||
getAuthToken,
|
||||
getMfaTempToken,
|
||||
@@ -16,6 +17,7 @@ apiRequest.interceptors.request.use((config) => {
|
||||
const signupTempToken = getSignupTempToken();
|
||||
const mfaTempToken = getMfaTempToken();
|
||||
const token = getAuthToken();
|
||||
const providerAuthToken = SecurityClient.getProviderAuthToken();
|
||||
|
||||
if (signupTempToken && config.headers) {
|
||||
// eslint-disable-next-line no-param-reassign
|
||||
@@ -26,6 +28,9 @@ apiRequest.interceptors.request.use((config) => {
|
||||
} else if (token && config.headers) {
|
||||
// eslint-disable-next-line no-param-reassign
|
||||
config.headers.Authorization = `Bearer ${token}`;
|
||||
} else if(providerAuthToken && config.headers) {
|
||||
// eslint-disable-next-line no-param-reassign
|
||||
config.headers.Authorization = `Bearer ${providerAuthToken}`;
|
||||
}
|
||||
return config;
|
||||
});
|
||||
|
||||
@@ -16,7 +16,10 @@ export const publicPaths = [
|
||||
`/terms`,
|
||||
`/subprocessors`,
|
||||
`/verify-email`,
|
||||
`/password-reset`
|
||||
`/password-reset`,
|
||||
`/saml-sso`,
|
||||
`/login/provider/success`,
|
||||
`/login/provider/error`
|
||||
];
|
||||
|
||||
export const languageMap = {
|
||||
@@ -30,7 +33,7 @@ interface Mapping {
|
||||
}
|
||||
|
||||
const plansDev: Mapping = {
|
||||
starter: 'prod_Mb4ATFT5QAHoPM',
|
||||
starter: 'prod_Nt6kPvYsVBuzVH',
|
||||
team: 'prod_NEpD2WMXUS2eDn',
|
||||
professional: 'prod_Mb4CetZ2jE7jdl',
|
||||
enterprise: 'licence_key_required'
|
||||
|
||||
@@ -1,14 +1,13 @@
|
||||
import { createContext, ReactNode, useContext, useMemo } from 'react';
|
||||
|
||||
import { useGetOrgSubscription } from '@app/hooks/api';
|
||||
import { GetSubscriptionPlan } from '@app/hooks/api/types';
|
||||
import { SubscriptionPlan } from '@app/hooks/api/types';
|
||||
|
||||
import { useWorkspace } from '../WorkspaceContext';
|
||||
// import { Subscription } from '@app/hooks/api/workspace/types';
|
||||
|
||||
type TSubscriptionContext = {
|
||||
subscription?: GetSubscriptionPlan;
|
||||
subscriptionPlan: string;
|
||||
subscription?: SubscriptionPlan;
|
||||
isLoading: boolean;
|
||||
};
|
||||
|
||||
@@ -28,7 +27,6 @@ export const SubscriptionProvider = ({ children }: Props): JSX.Element => {
|
||||
const value = useMemo<TSubscriptionContext>(
|
||||
() => ({
|
||||
subscription: data,
|
||||
subscriptionPlan: data?.data?.[0]?.plan?.product || '',
|
||||
isLoading
|
||||
}),
|
||||
[data, isLoading]
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import Image from 'next/image';
|
||||
import { useRouter } from 'next/router';
|
||||
import { useTranslation } from 'next-i18next';
|
||||
import { faXmark } from '@fortawesome/free-solid-svg-icons';
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||
|
||||
@@ -129,10 +129,10 @@ const ActivitySideBar = ({ toggleSidebar, currentAction }: SideBarProps) => {
|
||||
<div
|
||||
className={`absolute border-l border-mineshaft-500 ${
|
||||
isLoading ? 'bg-bunker-800' : 'bg-bunker'
|
||||
} fixed h-[calc(100vh-56px)] w-96 top-14 right-0 z-40 shadow-xl flex flex-col justify-between`}
|
||||
} fixed top-14 right-0 z-40 flex h-[calc(100vh-56px)] w-96 flex-col justify-between shadow-xl`}
|
||||
>
|
||||
{isLoading ? (
|
||||
<div className="flex items-center justify-center h-full mb-8">
|
||||
<div className="mb-8 flex h-full items-center justify-center">
|
||||
<Image
|
||||
src="/images/loading/loading.gif"
|
||||
height={60}
|
||||
@@ -142,9 +142,9 @@ const ActivitySideBar = ({ toggleSidebar, currentAction }: SideBarProps) => {
|
||||
</div>
|
||||
) : (
|
||||
<div className="h-min">
|
||||
<div className="flex flex-row px-4 py-3 border-b border-mineshaft-500 justify-between items-center">
|
||||
<p className="font-semibold text-lg text-bunker-200">
|
||||
{t(`activity:event.${actionMetaData?.name}`)}
|
||||
<div className="flex flex-row items-center justify-between border-b border-mineshaft-500 px-4 py-3">
|
||||
<p className="text-lg font-semibold text-bunker-200">
|
||||
{t(`activity.event.${actionMetaData?.name}`)}
|
||||
</p>
|
||||
<div
|
||||
className="p-1"
|
||||
@@ -153,31 +153,35 @@ const ActivitySideBar = ({ toggleSidebar, currentAction }: SideBarProps) => {
|
||||
tabIndex={0}
|
||||
onClick={() => toggleSidebar('')}
|
||||
>
|
||||
<FontAwesomeIcon icon={faXmark} className="w-4 h-4 text-bunker-300 cursor-pointer" />
|
||||
<FontAwesomeIcon icon={faXmark} className="h-4 w-4 cursor-pointer text-bunker-300" />
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex flex-col px-4 overflow-y-auto h-[calc(100vh-120px)] overflow-y-autp">
|
||||
<div className="overflow-y-autp flex h-[calc(100vh-120px)] flex-col overflow-y-auto px-4">
|
||||
{(actionMetaData?.name === 'readSecrets' ||
|
||||
actionMetaData?.name === 'addSecrets' ||
|
||||
actionMetaData?.name === 'deleteSecrets') &&
|
||||
actionData?.map((item, id) => (
|
||||
<div key={`secret.${id + 1}`}>
|
||||
<div className="text-xs text-bunker-200 mt-4 pl-1 ph-no-capture">
|
||||
<div className="ph-no-capture mt-4 pl-1 text-xs text-bunker-200">
|
||||
{item.newSecretVersion.key}
|
||||
</div>
|
||||
<div className='w-full font-mono text-sm break-all bg-mineshaft-600 px-2 py-0.5 rounded-md border border-mineshaft-500 text-bunker-200'>
|
||||
{item.newSecretVersion.value ? <span> {item.newSecretVersion.value} </span> : <span className='text-bunker-400'> EMPTY </span>}
|
||||
<div className="w-full break-all rounded-md border border-mineshaft-500 bg-mineshaft-600 px-2 py-0.5 font-mono text-sm text-bunker-200">
|
||||
{item.newSecretVersion.value ? (
|
||||
<span> {item.newSecretVersion.value} </span>
|
||||
) : (
|
||||
<span className="text-bunker-400"> EMPTY </span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
{actionMetaData?.name === 'updateSecrets' &&
|
||||
actionData?.map((item, id) => (
|
||||
<>
|
||||
<div className="text-xs text-bunker-200 mt-4 pl-1">
|
||||
<div className="mt-4 pl-1 text-xs text-bunker-200">
|
||||
{item.newSecretVersion.key}
|
||||
</div>
|
||||
<div className="break-all text-bunker-200 font-mono rounded-md overflow-hidden border border-mineshaft-500">
|
||||
<div className="bg-red/40 px-2 ph-no-capture">
|
||||
<div className="overflow-hidden break-all rounded-md border border-mineshaft-500 font-mono text-bunker-200">
|
||||
<div className="ph-no-capture bg-red/40 px-2">
|
||||
-{' '}
|
||||
{patienceDiff(
|
||||
item.oldSecretVersion.value.split(''),
|
||||
@@ -188,14 +192,16 @@ const ActivitySideBar = ({ toggleSidebar, currentAction }: SideBarProps) => {
|
||||
character.bIndex !== -1 && (
|
||||
<span
|
||||
key={`actionData.${id + 1}.line.${lineId + 1}`}
|
||||
className={`${character.aIndex === -1 && 'text-bunker-100 bg-red-700/80'}`}
|
||||
className={`${
|
||||
character.aIndex === -1 && 'bg-red-700/80 text-bunker-100'
|
||||
}`}
|
||||
>
|
||||
{character.line}
|
||||
</span>
|
||||
)
|
||||
)}
|
||||
</div>
|
||||
<div className="break-all bg-green-500/40 px-2 ph-no-capture">
|
||||
<div className="ph-no-capture break-all bg-green-500/40 px-2">
|
||||
+{' '}
|
||||
{patienceDiff(
|
||||
item.oldSecretVersion.value.split(''),
|
||||
@@ -206,7 +212,9 @@ const ActivitySideBar = ({ toggleSidebar, currentAction }: SideBarProps) => {
|
||||
character.aIndex !== -1 && (
|
||||
<span
|
||||
key={`actionData.${id + 1}.linev2.${lineId + 1}`}
|
||||
className={`${character.bIndex === -1 && 'text-bunker-100 bg-green-700/80'}`}
|
||||
className={`${
|
||||
character.bIndex === -1 && 'bg-green-700/80 text-bunker-100'
|
||||
}`}
|
||||
>
|
||||
{character.line}
|
||||
</span>
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
/* eslint-disable jsx-a11y/no-noninteractive-element-interactions */
|
||||
import React, { useState } from 'react';
|
||||
import Image from 'next/image';
|
||||
import { useTranslation } from 'next-i18next';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { faAngleDown, faAngleRight, faUpRightFromSquare } from '@fortawesome/free-solid-svg-icons';
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||
|
||||
@@ -21,6 +20,12 @@ interface LogData {
|
||||
createdAt: string;
|
||||
ipAddress: string;
|
||||
user: string;
|
||||
serviceAccount: {
|
||||
name: string;
|
||||
};
|
||||
serviceTokenData: {
|
||||
name: string;
|
||||
};
|
||||
payload: PayloadProps[];
|
||||
}
|
||||
|
||||
@@ -41,73 +46,86 @@ const ActivityLogsRow = ({
|
||||
const [payloadOpened, setPayloadOpened] = useState(false);
|
||||
const { t } = useTranslation();
|
||||
|
||||
const renderUser = () => {
|
||||
if (row?.user) return `${row.user}`;
|
||||
if (row?.serviceAccount) return `Service Account: ${row.serviceAccount.name}`;
|
||||
if (row?.serviceTokenData.name) return `Service Token: ${row.serviceTokenData.name}`;
|
||||
|
||||
return '';
|
||||
};
|
||||
return (
|
||||
<>
|
||||
<tr key={guidGenerator()} className="bg-bunker-800 duration-100 w-full text-sm">
|
||||
<td
|
||||
onKeyDown={() => null}
|
||||
<div key={guidGenerator()} className="w-full bg-mineshaft-800 text-sm text-mineshaft-200 duration-100 flex flex-row items-center">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setPayloadOpened(!payloadOpened)}
|
||||
className="border-mineshaft-700 border-t text-gray-300 flex items-center cursor-pointer"
|
||||
className="border-t border-mineshaft-700 pt-[0.58rem]"
|
||||
>
|
||||
<FontAwesomeIcon
|
||||
icon={payloadOpened ? faAngleDown : faAngleRight}
|
||||
className={`mt-2.5 ml-6 text-bunker-100 hover:bg-mineshaft-700 ${
|
||||
payloadOpened && 'bg-mineshaft-500'
|
||||
} p-1 duration-100 h-4 w-4 rounded-md`}
|
||||
className={`ml-6 mb-2 text-mineshaft-300 cursor-pointer ${
|
||||
payloadOpened ? 'bg-mineshaft-500 hover:bg-mineshaft-500' : 'hover:bg-mineshaft-700'
|
||||
} h-4 w-4 rounded-md p-1 duration-100`}
|
||||
/>
|
||||
</td>
|
||||
<td className="py-3 border-mineshaft-700 border-t text-gray-300">
|
||||
</button>
|
||||
<div className="border-t border-mineshaft-700 py-3 w-1/4 pl-6">
|
||||
{row.payload
|
||||
?.map(
|
||||
(action) =>
|
||||
`${String(action.secretVersions.length)} ${t(`activity:event.${action.name}`)}`
|
||||
`${String(action.secretVersions.length)} ${t(`activity.event.${action.name}`)}`
|
||||
)
|
||||
.join(' and ')}
|
||||
</td>
|
||||
<td className="pl-6 py-3 border-mineshaft-700 border-t text-gray-300">{row.user}</td>
|
||||
<td className="pl-6 py-3 border-mineshaft-700 border-t text-gray-300">{row.channel}</td>
|
||||
<td className="pl-6 py-3 border-mineshaft-700 border-t text-gray-300">
|
||||
</div>
|
||||
<div className="border-t border-mineshaft-700 py-3 pl-6 w-1/4">{renderUser()}</div>
|
||||
<div className="border-t border-mineshaft-700 py-3 pl-6 w-1/4">{row.channel}</div>
|
||||
<div className="border-t border-mineshaft-700 py-3 pl-6 w-1/4">
|
||||
{timeSince(new Date(row.createdAt))}
|
||||
</td>
|
||||
</tr>
|
||||
</div>
|
||||
</div>
|
||||
{payloadOpened && (
|
||||
<tr className="h-9 text-bunker-200 border-mineshaft-700 border-t text-sm">
|
||||
<td />
|
||||
<td>{String(t('common:timestamp'))}</td>
|
||||
<td>{row.createdAt}</td>
|
||||
</tr>
|
||||
<div className="h-9 border-t border-mineshaft-700 text-sm text-bunker-200 bg-mineshaft-900/50 w-full flex flex-row items-center">
|
||||
<div className='max-w-xl w-full flex flex-row items-center'>
|
||||
<div className='w-24' />
|
||||
<div className='w-1/2'>{String(t('common.timestamp'))}</div>
|
||||
<div className='w-1/2'>{row.createdAt}</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{payloadOpened &&
|
||||
row.payload?.map(
|
||||
(action) =>
|
||||
action.secretVersions.length > 0 && (
|
||||
<tr
|
||||
key={action._id}
|
||||
className="h-9 text-bunker-200 border-mineshaft-700 border-t text-sm"
|
||||
<div
|
||||
key={action.name}
|
||||
className="h-9 border-t border-mineshaft-700 text-sm text-bunker-200 bg-mineshaft-900/50 w-full flex flex-row items-center"
|
||||
>
|
||||
<td />
|
||||
<td className="">{t(`activity:event.${action.name}`)}</td>
|
||||
<td
|
||||
onKeyDown={() => null}
|
||||
className="text-primary-300 cursor-pointer hover:text-primary duration-200"
|
||||
onClick={() => toggleSidebar(action._id)}
|
||||
>
|
||||
{action.secretVersions.length +
|
||||
(action.secretVersions.length !== 1 ? ' secrets' : ' secret')}
|
||||
<FontAwesomeIcon
|
||||
icon={faUpRightFromSquare}
|
||||
className="ml-2 mb-0.5 font-light w-3 h-3"
|
||||
/>
|
||||
</td>
|
||||
</tr>
|
||||
<div className='max-w-xl w-full flex flex-row items-center'>
|
||||
<div className='w-24' />
|
||||
<div className='w-1/2'>{t(`activity.event.${action.name}`)}</div>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => toggleSidebar(action._id)}
|
||||
className='w-1/2 text-primary-300 hover:text-primary-500 flex flex-row justify-left items-center duration-100'
|
||||
>
|
||||
{action.secretVersions.length +
|
||||
(action.secretVersions.length !== 1 ? ' secrets' : ' secret')}
|
||||
<FontAwesomeIcon
|
||||
icon={faUpRightFromSquare}
|
||||
className="ml-2 mb-0.5 h-3 w-3 font-light"
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
)}
|
||||
{payloadOpened && (
|
||||
<tr className="h-9 text-bunker-200 border-mineshaft-700 border-t text-sm">
|
||||
<td />
|
||||
<td>{String(t('activity:ip-address'))}</td>
|
||||
<td>{row.ipAddress}</td>
|
||||
</tr>
|
||||
<div className="h-9 border-t border-mineshaft-700 text-sm text-bunker-200 bg-mineshaft-900/50 w-full flex flex-row items-center">
|
||||
<div className='max-w-xl w-full flex flex-row items-center'>
|
||||
<div className='w-24' />
|
||||
<div className='w-1/2'>{String(t('activity.ip-address'))}</div>
|
||||
<div className='w-1/2'>{row.ipAddress}</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
@@ -133,48 +151,49 @@ const ActivityTable = ({
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<div className="w-full px-6 mt-8">
|
||||
<div className="table-container w-full bg-bunker rounded-md mb-6 border border-mineshaft-700 relative">
|
||||
<div className="absolute rounded-t-md w-full h-[3rem] bg-white/5" />
|
||||
<table className="w-full my-1">
|
||||
<thead className="text-bunker-300">
|
||||
<tr className="text-sm">
|
||||
<th aria-label="actions" className="text-left pl-6 pt-2.5 pb-3" />
|
||||
<th className="text-left font-semibold pt-2.5 pb-3">
|
||||
{String(t('common:event')).toUpperCase()}
|
||||
</th>
|
||||
<th className="text-left font-semibold pl-6 pt-2.5 pb-3">
|
||||
{String(t('common:user')).toUpperCase()}
|
||||
</th>
|
||||
<th className="text-left font-semibold pl-6 pt-2.5 pb-3">
|
||||
{String(t('common:source')).toUpperCase()}
|
||||
</th>
|
||||
<th className="text-left font-semibold pl-6 pt-2.5 pb-3">
|
||||
{String(t('common:time')).toUpperCase()}
|
||||
</th>
|
||||
<th aria-label="action" />
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{data?.map((row, index) => (
|
||||
<ActivityLogsRow
|
||||
key={`activity.${index + 1}.${row._id}`}
|
||||
row={row}
|
||||
toggleSidebar={toggleSidebar}
|
||||
/>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
<div className="mt-8 w-full px-6">
|
||||
<div className="table-container relative mb-6 w-full rounded-md border border-mineshaft-700 bg-mineshaft-800">
|
||||
{/* <div className="absolute h-[3rem] w-full rounded-t-md bg-white/5" /> */}
|
||||
<div className="my-1 w-full">
|
||||
<div className="text-bunker-300 border-b border-mineshaft-600">
|
||||
<div className="text-sm flex flex-row w-full">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => {}}
|
||||
className="opacity-0"
|
||||
>
|
||||
<FontAwesomeIcon
|
||||
icon={faAngleRight}
|
||||
className="ml-6 mb-2 text-bunker-100 hover:bg-mineshaft-700 cursor-pointer h-4 w-4 rounded-md p-1 duration-100"
|
||||
/>
|
||||
</button>
|
||||
<div className="flex flex-row justify-between w-full">
|
||||
<div className="pt-2.5 pb-3 text-left font-semibold w-1/4 pl-6">
|
||||
{String(t('common.event')).toUpperCase()}
|
||||
</div>
|
||||
<div className="pl-6 pt-2.5 pb-3 text-left font-semibold w-1/4 pl-6">
|
||||
{String(t('common.user')).toUpperCase()}
|
||||
</div>
|
||||
<div className="pl-6 pt-2.5 pb-3 text-left font-semibold w-1/4 pl-6">
|
||||
{String(t('common.source')).toUpperCase()}
|
||||
</div>
|
||||
<div className="pl-6 pt-2.5 pb-3 text-left font-semibold w-1/4 pl-6">
|
||||
{String(t('common.time')).toUpperCase()}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{data?.map((row, index) => (
|
||||
<ActivityLogsRow
|
||||
key={`activity.${index + 1}.${row._id}`}
|
||||
row={row}
|
||||
toggleSidebar={toggleSidebar}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
{isLoading && (
|
||||
<div className="w-full flex justify-center mb-8 mt-4">
|
||||
<Image
|
||||
src="/images/loading/loading.gif"
|
||||
height={60}
|
||||
width={100}
|
||||
alt="loading animation"
|
||||
/>
|
||||
</div>
|
||||
<div className="mb-8 mt-4 bg-mineshaft-800 rounded-md h-60 flex w-full justify-center animate-pulse" />
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import Image from 'next/image';
|
||||
import { useRouter } from 'next/router';
|
||||
import { useTranslation } from 'next-i18next';
|
||||
import { faXmark } from '@fortawesome/free-solid-svg-icons';
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||
import { Tag } from 'public/data/frequentInterfaces';
|
||||
@@ -60,7 +59,6 @@ interface EncrypetedSecretVersionListProps {
|
||||
* @returns the sidebar with the options for point-in-time recovery (commits)
|
||||
*/
|
||||
const PITRecoverySidebar = ({ toggleSidebar, setSnapshotData, chosenSnapshot }: SideBarProps) => {
|
||||
const { t } = useTranslation();
|
||||
const router = useRouter();
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [secretSnapshotsMetadata, setSecretSnapshotsMetadata] = useState<SnaphotProps[]>([]);
|
||||
@@ -102,10 +100,12 @@ const PITRecoverySidebar = ({ toggleSidebar, setSnapshotData, chosenSnapshot }:
|
||||
});
|
||||
}
|
||||
|
||||
const decryptedSecretVersions = secretSnapshotData.secretVersions.filter(
|
||||
(sv: EncrypetedSecretVersionListProps) => (sv.type !== undefined && sv.environment !== undefined)
|
||||
).map(
|
||||
(encryptedSecretVersion: EncrypetedSecretVersionListProps, pos: number) => ({
|
||||
const decryptedSecretVersions = secretSnapshotData.secretVersions
|
||||
.filter(
|
||||
(sv: EncrypetedSecretVersionListProps) =>
|
||||
sv.type !== undefined && sv.environment !== undefined
|
||||
)
|
||||
.map((encryptedSecretVersion: EncrypetedSecretVersionListProps, pos: number) => ({
|
||||
id: encryptedSecretVersion._id,
|
||||
pos,
|
||||
type: encryptedSecretVersion.type,
|
||||
@@ -123,53 +123,59 @@ const PITRecoverySidebar = ({ toggleSidebar, setSnapshotData, chosenSnapshot }:
|
||||
tag: encryptedSecretVersion.secretValueTag,
|
||||
key: decryptedLatestKey
|
||||
})
|
||||
})
|
||||
);
|
||||
}));
|
||||
|
||||
const secretKeys = [
|
||||
...new Set(decryptedSecretVersions.filter((dsv: any) => dsv.type !== undefined || dsv.environemnt !== undefined)
|
||||
.map((secret: SecretDataProps) => secret.key))
|
||||
...new Set(
|
||||
decryptedSecretVersions
|
||||
.filter((dsv: any) => dsv.type !== undefined || dsv.environemnt !== undefined)
|
||||
.map((secret: SecretDataProps) => secret.key)
|
||||
)
|
||||
];
|
||||
|
||||
const result = secretKeys.map((key, index) => (decryptedSecretVersions.filter(
|
||||
const result = secretKeys.map((key, index) =>
|
||||
decryptedSecretVersions.filter(
|
||||
(secret: SecretDataProps) => secret.key === key && secret.type === 'shared'
|
||||
)[0]?.id ? {
|
||||
id: decryptedSecretVersions.filter(
|
||||
(secret: SecretDataProps) => secret.key === key && secret.type === 'shared'
|
||||
)[0].id,
|
||||
pos: index,
|
||||
key,
|
||||
environment: decryptedSecretVersions.filter(
|
||||
(secret: SecretDataProps) => secret.key === key && secret.type === 'shared'
|
||||
)[0].environment,
|
||||
tags: decryptedSecretVersions.filter(
|
||||
(secret: SecretDataProps) => secret.key === key && secret.type === 'shared'
|
||||
)[0].tags,
|
||||
value: decryptedSecretVersions.filter(
|
||||
(secret: SecretDataProps) => secret.key === key && secret.type === 'shared'
|
||||
)[0]?.value,
|
||||
valueOverride: decryptedSecretVersions.filter(
|
||||
(secret: SecretDataProps) => secret.key === key && secret.type === 'personal'
|
||||
)[0]?.value
|
||||
} : {
|
||||
id: decryptedSecretVersions.filter(
|
||||
(secret: SecretDataProps) => secret.key === key && secret.type === 'personal'
|
||||
)[0].id,
|
||||
pos: index,
|
||||
key,
|
||||
environment: decryptedSecretVersions.filter(
|
||||
(secret: SecretDataProps) => secret.key === key && secret.type === 'personal'
|
||||
)[0].environment,
|
||||
tags: decryptedSecretVersions.filter(
|
||||
(secret: SecretDataProps) => secret.key === key && secret.type === 'personal'
|
||||
)[0].tags,
|
||||
value: decryptedSecretVersions.filter(
|
||||
(secret: SecretDataProps) => secret.key === key && secret.type === 'shared'
|
||||
)[0]?.value,
|
||||
valueOverride: decryptedSecretVersions.filter(
|
||||
(secret: SecretDataProps) => secret.key === key && secret.type === 'personal'
|
||||
)[0]?.value
|
||||
}));
|
||||
)[0]?.id
|
||||
? {
|
||||
id: decryptedSecretVersions.filter(
|
||||
(secret: SecretDataProps) => secret.key === key && secret.type === 'shared'
|
||||
)[0].id,
|
||||
pos: index,
|
||||
key,
|
||||
environment: decryptedSecretVersions.filter(
|
||||
(secret: SecretDataProps) => secret.key === key && secret.type === 'shared'
|
||||
)[0].environment,
|
||||
tags: decryptedSecretVersions.filter(
|
||||
(secret: SecretDataProps) => secret.key === key && secret.type === 'shared'
|
||||
)[0].tags,
|
||||
value: decryptedSecretVersions.filter(
|
||||
(secret: SecretDataProps) => secret.key === key && secret.type === 'shared'
|
||||
)[0]?.value,
|
||||
valueOverride: decryptedSecretVersions.filter(
|
||||
(secret: SecretDataProps) => secret.key === key && secret.type === 'personal'
|
||||
)[0]?.value
|
||||
}
|
||||
: {
|
||||
id: decryptedSecretVersions.filter(
|
||||
(secret: SecretDataProps) => secret.key === key && secret.type === 'personal'
|
||||
)[0].id,
|
||||
pos: index,
|
||||
key,
|
||||
environment: decryptedSecretVersions.filter(
|
||||
(secret: SecretDataProps) => secret.key === key && secret.type === 'personal'
|
||||
)[0].environment,
|
||||
tags: decryptedSecretVersions.filter(
|
||||
(secret: SecretDataProps) => secret.key === key && secret.type === 'personal'
|
||||
)[0].tags,
|
||||
value: decryptedSecretVersions.filter(
|
||||
(secret: SecretDataProps) => secret.key === key && secret.type === 'shared'
|
||||
)[0]?.value,
|
||||
valueOverride: decryptedSecretVersions.filter(
|
||||
(secret: SecretDataProps) => secret.key === key && secret.type === 'personal'
|
||||
)[0]?.value
|
||||
}
|
||||
);
|
||||
|
||||
setSnapshotData({
|
||||
id: secretSnapshotData._id,
|
||||
@@ -182,12 +188,12 @@ const PITRecoverySidebar = ({ toggleSidebar, setSnapshotData, chosenSnapshot }:
|
||||
|
||||
return (
|
||||
<div
|
||||
className={`absolute border-l border-mineshaft-500 w-full min-w-sm max-w-sm ${
|
||||
className={`min-w-sm absolute w-full max-w-sm border-l border-mineshaft-500 ${
|
||||
isLoading ? 'bg-bunker-800' : 'bg-bunker'
|
||||
} fixed h-full right-0 z-[40] shadow-xl flex flex-col justify-between sticky top-0`}
|
||||
} fixed sticky right-0 top-0 z-[40] flex h-full flex-col justify-between shadow-xl`}
|
||||
>
|
||||
{isLoading ? (
|
||||
<div className="flex items-center justify-center h-full mb-8">
|
||||
<div className="mb-8 flex h-full items-center justify-center">
|
||||
<Image
|
||||
src="/images/loading/loading.gif"
|
||||
height={60}
|
||||
@@ -197,8 +203,8 @@ const PITRecoverySidebar = ({ toggleSidebar, setSnapshotData, chosenSnapshot }:
|
||||
</div>
|
||||
) : (
|
||||
<div className="h-min">
|
||||
<div className="flex flex-row px-4 py-3 border-b border-mineshaft-500 justify-between items-center">
|
||||
<p className="font-semibold text-lg text-bunker-200">{t('Point-in-time Recovery')}</p>
|
||||
<div className="flex flex-row items-center justify-between border-b border-mineshaft-500 px-4 py-3">
|
||||
<p className="text-lg font-semibold text-bunker-200">Point In Recovery</p>
|
||||
<div
|
||||
onKeyDown={() => null}
|
||||
role="button"
|
||||
@@ -206,11 +212,13 @@ const PITRecoverySidebar = ({ toggleSidebar, setSnapshotData, chosenSnapshot }:
|
||||
className="p-1"
|
||||
onClick={() => toggleSidebar(false)}
|
||||
>
|
||||
<FontAwesomeIcon icon={faXmark} className="w-4 h-4 text-bunker-300 cursor-pointer" />
|
||||
<FontAwesomeIcon icon={faXmark} className="h-4 w-4 cursor-pointer text-bunker-300" />
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex flex-col w-96 px-2 py-2 overflow-y-auto bg-bunker border-l border-mineshaft-600 h-[calc(100vh-115px)]">
|
||||
<span className='px-2 text-bunker-200 pb-2 text-sm'>Note: This will recover secrets for all enviroments in this project.</span>
|
||||
<div className="flex h-[calc(100vh-115px)] w-96 flex-col overflow-y-auto border-l border-mineshaft-600 bg-bunker px-2 py-2">
|
||||
<span className="px-2 pb-2 text-sm text-bunker-200">
|
||||
Note: This will recover secrets for all enviroments in this project.
|
||||
</span>
|
||||
{secretSnapshotsMetadata?.map((snapshot: SnaphotProps, id: number) => (
|
||||
<div
|
||||
onKeyDown={() => null}
|
||||
@@ -220,9 +228,9 @@ const PITRecoverySidebar = ({ toggleSidebar, setSnapshotData, chosenSnapshot }:
|
||||
onClick={() => exploreSnapshot({ snapshotId: snapshot._id })}
|
||||
className={`${
|
||||
chosenSnapshot === snapshot._id || (id === 0 && chosenSnapshot === '')
|
||||
? 'bg-primary text-black pointer-events-none'
|
||||
: 'bg-mineshaft-700 hover:bg-mineshaft-500 duration-200 cursor-pointer'
|
||||
} py-3 px-4 mb-2 rounded-md flex flex-row justify-between items-center`}
|
||||
? 'pointer-events-none bg-primary text-black'
|
||||
: 'cursor-pointer bg-mineshaft-700 duration-200 hover:bg-mineshaft-500'
|
||||
} mb-2 flex flex-row items-center justify-between rounded-md py-3 px-4`}
|
||||
>
|
||||
<div className="flex flex-row items-start">
|
||||
<div
|
||||
@@ -230,7 +238,7 @@ const PITRecoverySidebar = ({ toggleSidebar, setSnapshotData, chosenSnapshot }:
|
||||
chosenSnapshot === snapshot._id || (id === 0 && chosenSnapshot === '')
|
||||
? 'text-bunker-800'
|
||||
: 'text-bunker-200'
|
||||
} text-sm mr-1.5`}
|
||||
} mr-1.5 text-sm`}
|
||||
>
|
||||
{timeSince(new Date(snapshot.createdAt))}
|
||||
</div>
|
||||
@@ -245,8 +253,8 @@ const PITRecoverySidebar = ({ toggleSidebar, setSnapshotData, chosenSnapshot }:
|
||||
<div
|
||||
className={`${
|
||||
chosenSnapshot === snapshot._id || (id === 0 && chosenSnapshot === '')
|
||||
? 'text-bunker-800 pointer-events-none'
|
||||
: 'text-bunker-200 hover:text-primary duration-200 cursor-pointer'
|
||||
? 'pointer-events-none text-bunker-800'
|
||||
: 'cursor-pointer text-bunker-200 duration-200 hover:text-primary'
|
||||
} text-sm`}
|
||||
>
|
||||
{id === 0
|
||||
@@ -257,8 +265,8 @@ const PITRecoverySidebar = ({ toggleSidebar, setSnapshotData, chosenSnapshot }:
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
<div className="flex justify-center w-full mb-14">
|
||||
<div className="items-center w-40">
|
||||
<div className="mb-14 flex w-full justify-center">
|
||||
<div className="w-40 items-center">
|
||||
<Button
|
||||
text="View More"
|
||||
textDisabled="End of History"
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import Image from 'next/image';
|
||||
import { useRouter } from 'next/router';
|
||||
import { useTranslation } from 'next-i18next';
|
||||
import { faCircle, faDotCircle } from '@fortawesome/free-solid-svg-icons';
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||
|
||||
@@ -76,11 +76,11 @@ const SecretVersionList = ({ secretId }: { secretId: string }) => {
|
||||
}, [secretId]);
|
||||
|
||||
return (
|
||||
<div className="w-full min-w-40 h-[12.4rem] px-4 mt-4 text-sm text-bunker-300 overflow-x-none">
|
||||
<p className="">{t('dashboard:sidebar.version-history')}</p>
|
||||
<div className="pl-1 py-0.5 rounded-md bg-bunker-800 border border-mineshaft-500 overflow-x-none h-full">
|
||||
<div className="min-w-40 overflow-x-none dark mt-4 h-[12.4rem] w-full px-4 text-sm text-bunker-300">
|
||||
<p className="">{t('dashboard.sidebar.version-history')}</p>
|
||||
<div className="overflow-x-none h-full rounded-md border border-mineshaft-500 bg-bunker-800 py-0.5 pl-1">
|
||||
{isLoading ? (
|
||||
<div className="flex items-center justify-center h-full">
|
||||
<div className="flex h-full items-center justify-center">
|
||||
<Image
|
||||
src="/images/loading/loading.gif"
|
||||
height={60}
|
||||
@@ -89,20 +89,20 @@ const SecretVersionList = ({ secretId }: { secretId: string }) => {
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
<div className="h-48 overflow-y-auto overflow-x-none dark:[color-scheme:dark]">
|
||||
<div className="overflow-x-none h-48 overflow-y-auto dark:[color-scheme:dark]">
|
||||
{secretVersions ? (
|
||||
secretVersions
|
||||
?.sort((a, b) => b.createdAt.localeCompare(a.createdAt))
|
||||
.map((version: DecryptedSecretVersionListProps, index: number) => (
|
||||
<div key={`${version.createdAt}.${index + 1}`} className="flex flex-row">
|
||||
<div className="pr-1 flex flex-col items-center">
|
||||
<div className="flex flex-col items-center pr-1">
|
||||
<div className="p-1">
|
||||
<FontAwesomeIcon icon={index === 0 ? faDotCircle : faCircle} />
|
||||
</div>
|
||||
<div className="w-0 h-full border-l border-bunker-300 mt-1" />
|
||||
<div className="mt-1 h-full w-0 border-l border-bunker-300" />
|
||||
</div>
|
||||
<div className="flex flex-col w-full max-w-[calc(100%-2.3rem)]">
|
||||
<div className="pr-2 pt-1 text-bunker-300/90">
|
||||
<div className="flex w-full max-w-[calc(100%-2.3rem)] flex-col">
|
||||
<div className="pr-2 text-bunker-300/90">
|
||||
{new Date(version.createdAt).toLocaleDateString('en-US', {
|
||||
year: 'numeric',
|
||||
month: '2-digit',
|
||||
@@ -113,18 +113,18 @@ const SecretVersionList = ({ secretId }: { secretId: string }) => {
|
||||
})}
|
||||
</div>
|
||||
<div className="">
|
||||
<p className="break-words ph-no-capture">
|
||||
<span className="py-0.5 px-1 rounded-sm bg-primary-500/30 mr-1.5">
|
||||
<p className="ph-no-capture break-words">
|
||||
<span className="mr-1.5 rounded-sm bg-primary-500/30 py-0.5 px-1">
|
||||
Value:
|
||||
</span>
|
||||
<span className='font-mono'>{version.value}</span>
|
||||
<span className="font-mono">{version.value}</span>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))
|
||||
) : (
|
||||
<div className="w-full h-full flex items-center justify-center text-bunker-400">
|
||||
<div className="flex h-full w-full items-center justify-center text-bunker-400">
|
||||
No version history yet.
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -93,6 +93,7 @@ const initProjectHelper = async ({
|
||||
}) => {
|
||||
let project;
|
||||
try {
|
||||
|
||||
// create new project
|
||||
project = await createWorkspace({
|
||||
workspaceName: projectName,
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
export {
|
||||
useGetAuthToken,
|
||||
useSendMfaToken,
|
||||
useVerifyMfaToken} from './queries'
|
||||
useVerifyMfaToken,
|
||||
useRevokeAllSessions,
|
||||
useGetCommonPasswords
|
||||
} from './queries'
|
||||
|
||||
@@ -10,7 +10,8 @@ import {
|
||||
VerifyMfaTokenRes} from './types';
|
||||
|
||||
const authKeys = {
|
||||
getAuthToken: ['token'] as const
|
||||
getAuthToken: ['token'] as const,
|
||||
commonPasswords: ['common-passwords'] as const
|
||||
};
|
||||
|
||||
export const useSendMfaToken = () => {
|
||||
@@ -49,3 +50,20 @@ export const useGetAuthToken = () =>
|
||||
onSuccess: (data) => setAuthToken(data.token),
|
||||
retry: 0
|
||||
});
|
||||
|
||||
export const useRevokeAllSessions = () => {
|
||||
return useMutation({
|
||||
mutationFn: async () => {
|
||||
const { data } = await apiRequest.delete('/api/v1/auth/sessions');
|
||||
return data;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
const fetchCommonPasswords = async () => {
|
||||
const { data } = await apiRequest.get('/api/v1/auth/common-passwords');
|
||||
return data || [];
|
||||
};
|
||||
|
||||
export const useGetCommonPasswords = () =>
|
||||
useQuery({ queryKey: authKeys.commonPasswords, queryFn: fetchCommonPasswords });
|
||||
@@ -2,6 +2,10 @@ export * from './auth';
|
||||
export * from './incidentContacts';
|
||||
export * from './keys';
|
||||
export * from './organization';
|
||||
export * from './secretFolders';
|
||||
export * from './secrets';
|
||||
export * from './secretSnapshots';
|
||||
export * from './serviceAccounts';
|
||||
export * from './serviceTokens';
|
||||
export * from './subscriptions';
|
||||
export * from './tags';
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
export {
|
||||
useGetIntegrationAuthApps,
|
||||
useGetIntegrationAuthById,
|
||||
useGetIntegrationAuthTeams} from './queries';
|
||||
useGetIntegrationAuthRailwayEnvironments,
|
||||
useGetIntegrationAuthRailwayServices,
|
||||
useGetIntegrationAuthTeams,
|
||||
useGetIntegrationAuthVercelBranches} from './queries';
|
||||
@@ -4,13 +4,37 @@ import { apiRequest } from "@app/config/request";
|
||||
|
||||
import {
|
||||
App,
|
||||
Environment,
|
||||
IntegrationAuth,
|
||||
Team} from './types';
|
||||
Service,
|
||||
Team
|
||||
} from './types';
|
||||
|
||||
const integrationAuthKeys = {
|
||||
getIntegrationAuthById: (integrationAuthId: string) => [{ integrationAuthId }, 'integrationAuth'] as const,
|
||||
getIntegrationAuthApps: (integrationAuthId: string, teamId?: string) => [{ integrationAuthId, teamId }, 'integrationAuthApps'] as const,
|
||||
getIntegrationAuthTeams: (integrationAuthId: string) => [{ integrationAuthId }, 'integrationAuthTeams'] as const
|
||||
getIntegrationAuthTeams: (integrationAuthId: string) => [{ integrationAuthId }, 'integrationAuthTeams'] as const,
|
||||
getIntegrationAuthVercelBranches: ({
|
||||
integrationAuthId,
|
||||
appId,
|
||||
}: {
|
||||
integrationAuthId: string;
|
||||
appId: string;
|
||||
}) => [{ integrationAuthId, appId }, 'integrationAuthVercelBranches'] as const,
|
||||
getIntegrationAuthRailwayEnvironments: ({
|
||||
integrationAuthId,
|
||||
appId
|
||||
}: {
|
||||
integrationAuthId: string;
|
||||
appId: string;
|
||||
}) => [{ integrationAuthId, appId }, 'integrationAuthRailwayEnvironments'] as const,
|
||||
getIntegrationAuthRailwayServices: ({
|
||||
integrationAuthId,
|
||||
appId
|
||||
}: {
|
||||
integrationAuthId: string;
|
||||
appId: string;
|
||||
}) => [{ integrationAuthId, appId }, 'integrationAuthRailwayServices'] as const
|
||||
}
|
||||
|
||||
const fetchIntegrationAuthById = async (integrationAuthId: string) => {
|
||||
@@ -38,6 +62,54 @@ const fetchIntegrationAuthTeams = async (integrationAuthId: string) => {
|
||||
return data.teams;
|
||||
}
|
||||
|
||||
const fetchIntegrationAuthVercelBranches = async ({
|
||||
integrationAuthId,
|
||||
appId
|
||||
}: {
|
||||
integrationAuthId: string;
|
||||
appId: string;
|
||||
}) => {
|
||||
const { data: { branches } } = await apiRequest.get<{ branches: string[] }>(`/api/v1/integration-auth/${integrationAuthId}/vercel/branches`, {
|
||||
params: {
|
||||
appId
|
||||
}
|
||||
});
|
||||
|
||||
return branches;
|
||||
};
|
||||
|
||||
const fetchIntegrationAuthRailwayEnvironments = async ({
|
||||
integrationAuthId,
|
||||
appId
|
||||
}: {
|
||||
integrationAuthId: string;
|
||||
appId: string;
|
||||
}) => {
|
||||
const { data: { environments } } = await apiRequest.get<{ environments: Environment[] }>(`/api/v1/integration-auth/${integrationAuthId}/railway/environments`, {
|
||||
params: {
|
||||
appId
|
||||
}
|
||||
});
|
||||
|
||||
return environments;
|
||||
}
|
||||
|
||||
const fetchIntegrationAuthRailwayServices = async ({
|
||||
integrationAuthId,
|
||||
appId
|
||||
}: {
|
||||
integrationAuthId: string;
|
||||
appId: string;
|
||||
}) => {
|
||||
const { data: { services } } = await apiRequest.get<{ services: Service[] }>(`/api/v1/integration-auth/${integrationAuthId}/railway/services`, {
|
||||
params: {
|
||||
appId
|
||||
}
|
||||
});
|
||||
|
||||
return services;
|
||||
}
|
||||
|
||||
export const useGetIntegrationAuthById = (integrationAuthId: string) => {
|
||||
return useQuery({
|
||||
queryKey: integrationAuthKeys.getIntegrationAuthById(integrationAuthId),
|
||||
@@ -46,7 +118,6 @@ export const useGetIntegrationAuthById = (integrationAuthId: string) => {
|
||||
});
|
||||
}
|
||||
|
||||
// TODO: fix to teamId
|
||||
export const useGetIntegrationAuthApps = ({
|
||||
integrationAuthId,
|
||||
teamId
|
||||
@@ -70,4 +141,64 @@ export const useGetIntegrationAuthTeams = (integrationAuthId: string) => {
|
||||
queryFn: () => fetchIntegrationAuthTeams(integrationAuthId),
|
||||
enabled: true
|
||||
});
|
||||
}
|
||||
|
||||
export const useGetIntegrationAuthVercelBranches = ({
|
||||
integrationAuthId,
|
||||
appId,
|
||||
}: {
|
||||
integrationAuthId: string;
|
||||
appId: string;
|
||||
}) => {
|
||||
return useQuery({
|
||||
queryKey: integrationAuthKeys.getIntegrationAuthVercelBranches({
|
||||
integrationAuthId,
|
||||
appId,
|
||||
}),
|
||||
queryFn: () => fetchIntegrationAuthVercelBranches({
|
||||
integrationAuthId,
|
||||
appId,
|
||||
}),
|
||||
enabled: true
|
||||
});
|
||||
}
|
||||
|
||||
export const useGetIntegrationAuthRailwayEnvironments = ({
|
||||
integrationAuthId,
|
||||
appId
|
||||
}: {
|
||||
integrationAuthId: string;
|
||||
appId: string;
|
||||
}) => {
|
||||
return useQuery({
|
||||
queryKey: integrationAuthKeys.getIntegrationAuthRailwayEnvironments({
|
||||
integrationAuthId,
|
||||
appId,
|
||||
}),
|
||||
queryFn: () => fetchIntegrationAuthRailwayEnvironments({
|
||||
integrationAuthId,
|
||||
appId,
|
||||
}),
|
||||
enabled: true
|
||||
});
|
||||
}
|
||||
|
||||
export const useGetIntegrationAuthRailwayServices = ({
|
||||
integrationAuthId,
|
||||
appId
|
||||
}: {
|
||||
integrationAuthId: string;
|
||||
appId: string;
|
||||
}) => {
|
||||
return useQuery({
|
||||
queryKey: integrationAuthKeys.getIntegrationAuthRailwayServices({
|
||||
integrationAuthId,
|
||||
appId,
|
||||
}),
|
||||
queryFn: () => fetchIntegrationAuthRailwayServices({
|
||||
integrationAuthId,
|
||||
appId,
|
||||
}),
|
||||
enabled: true
|
||||
});
|
||||
}
|
||||
@@ -15,4 +15,14 @@ export type App = {
|
||||
export type Team = {
|
||||
name: string;
|
||||
teamId: string;
|
||||
}
|
||||
|
||||
export type Environment = {
|
||||
name: string;
|
||||
environmentId: string;
|
||||
}
|
||||
|
||||
export type Service = {
|
||||
name: string;
|
||||
serviceId: string;
|
||||
}
|
||||
@@ -5,7 +5,7 @@ import { apiRequest } from '@app/config/request';
|
||||
import { UploadWsKeyDTO, UserWsKeyPair } from './types';
|
||||
|
||||
const encKeyKeys = {
|
||||
getUserWorkspaceKey: (workspaceID: string) => ['worksapce-key-pair', { workspaceID }] as const
|
||||
getUserWorkspaceKey: (workspaceID: string) => ['workspace-key-pair', { workspaceID }] as const
|
||||
};
|
||||
|
||||
const fetchUserWsKey = async (workspaceID: string) => {
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
export { useCreateFolder, useDeleteFolder, useGetProjectFolders, useUpdateFolder } from './queries';
|
||||
@@ -0,0 +1,129 @@
|
||||
import { useCallback } from 'react';
|
||||
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
|
||||
|
||||
import { apiRequest } from '@app/config/request';
|
||||
|
||||
import { secretSnapshotKeys } from '../secretSnapshots/queries';
|
||||
import {
|
||||
CreateFolderDTO,
|
||||
DeleteFolderDTO,
|
||||
GetProjectFoldersDTO,
|
||||
TSecretFolder,
|
||||
UpdateFolderDTO
|
||||
} from './types';
|
||||
|
||||
const queryKeys = {
|
||||
getSecretFolders: (workspaceId: string, environment: string, parentFolderId?: string) =>
|
||||
['secret-folders', { workspaceId, environment, parentFolderId }] as const
|
||||
};
|
||||
|
||||
export const useGetProjectFolders = ({
|
||||
workspaceId,
|
||||
parentFolderId,
|
||||
environment,
|
||||
isPaused,
|
||||
sortDir
|
||||
}: GetProjectFoldersDTO) =>
|
||||
useQuery({
|
||||
queryKey: queryKeys.getSecretFolders(workspaceId, environment, parentFolderId),
|
||||
enabled: Boolean(workspaceId) && Boolean(environment) && !isPaused,
|
||||
queryFn: async () => {
|
||||
const { data } = await apiRequest.get<{ folders: TSecretFolder[]; dir: TSecretFolder[] }>(
|
||||
'/api/v1/folders',
|
||||
{
|
||||
params: {
|
||||
workspaceId,
|
||||
environment,
|
||||
parentFolderId
|
||||
}
|
||||
}
|
||||
);
|
||||
return data;
|
||||
},
|
||||
select: useCallback(
|
||||
({ folders, dir }: { folders: TSecretFolder[]; dir: TSecretFolder[] }) => ({
|
||||
dir,
|
||||
folders: folders.sort((a, b) =>
|
||||
sortDir === 'asc'
|
||||
? a?.name?.localeCompare(b?.name || '')
|
||||
: b?.name?.localeCompare(a?.name || '')
|
||||
)
|
||||
}),
|
||||
[sortDir]
|
||||
)
|
||||
});
|
||||
|
||||
export const useCreateFolder = () => {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutation<{}, {}, CreateFolderDTO>({
|
||||
mutationFn: async (dto) => {
|
||||
const { data } = await apiRequest.post('/api/v1/folders', dto);
|
||||
return data;
|
||||
},
|
||||
onSuccess: (_, { workspaceId, environment, parentFolderId }) => {
|
||||
queryClient.invalidateQueries(
|
||||
queryKeys.getSecretFolders(workspaceId, environment, parentFolderId)
|
||||
);
|
||||
queryClient.invalidateQueries(
|
||||
secretSnapshotKeys.count(workspaceId, environment, parentFolderId)
|
||||
);
|
||||
queryClient.invalidateQueries(
|
||||
secretSnapshotKeys.list(workspaceId, environment, parentFolderId)
|
||||
);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
export const useUpdateFolder = (parentFolderId: string) => {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutation<{}, {}, UpdateFolderDTO>({
|
||||
mutationFn: async ({ folderId, name, environment, workspaceId }) => {
|
||||
const { data } = await apiRequest.patch(`/api/v1/folders/${folderId}`, {
|
||||
name,
|
||||
environment,
|
||||
workspaceId
|
||||
});
|
||||
return data;
|
||||
},
|
||||
onSuccess: (_, { workspaceId, environment }) => {
|
||||
queryClient.invalidateQueries(
|
||||
queryKeys.getSecretFolders(workspaceId, environment, parentFolderId)
|
||||
);
|
||||
queryClient.invalidateQueries(
|
||||
secretSnapshotKeys.count(workspaceId, environment, parentFolderId)
|
||||
);
|
||||
queryClient.invalidateQueries(
|
||||
secretSnapshotKeys.list(workspaceId, environment, parentFolderId)
|
||||
);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
export const useDeleteFolder = (parentFolderId: string) => {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutation<{}, {}, DeleteFolderDTO>({
|
||||
mutationFn: async ({ folderId, environment, workspaceId }) => {
|
||||
const { data } = await apiRequest.delete(`/api/v1/folders/${folderId}`, {
|
||||
data: {
|
||||
environment,
|
||||
workspaceId
|
||||
}
|
||||
});
|
||||
return data;
|
||||
},
|
||||
onSuccess: (_, { workspaceId, environment }) => {
|
||||
queryClient.invalidateQueries(
|
||||
queryKeys.getSecretFolders(workspaceId, environment, parentFolderId)
|
||||
);
|
||||
queryClient.invalidateQueries(
|
||||
secretSnapshotKeys.count(workspaceId, environment, parentFolderId)
|
||||
);
|
||||
queryClient.invalidateQueries(
|
||||
secretSnapshotKeys.list(workspaceId, environment, parentFolderId)
|
||||
);
|
||||
}
|
||||
});
|
||||
};
|
||||
@@ -0,0 +1,32 @@
|
||||
export type TSecretFolder = {
|
||||
id: string;
|
||||
name: string;
|
||||
};
|
||||
|
||||
export type GetProjectFoldersDTO = {
|
||||
workspaceId: string;
|
||||
environment: string;
|
||||
parentFolderId?: string;
|
||||
isPaused?: boolean;
|
||||
sortDir?: 'asc' | 'desc';
|
||||
};
|
||||
|
||||
export type CreateFolderDTO = {
|
||||
workspaceId: string;
|
||||
environment: string;
|
||||
folderName: string;
|
||||
parentFolderId?: string;
|
||||
};
|
||||
|
||||
export type UpdateFolderDTO = {
|
||||
workspaceId: string;
|
||||
environment: string;
|
||||
name: string;
|
||||
folderId: string;
|
||||
};
|
||||
|
||||
export type DeleteFolderDTO = {
|
||||
workspaceId: string;
|
||||
environment: string;
|
||||
folderId: string;
|
||||
};
|
||||
@@ -0,0 +1,6 @@
|
||||
export {
|
||||
useGetSnapshotSecrets,
|
||||
useGetWorkspaceSecretSnapshots,
|
||||
useGetWsSnapshotCount,
|
||||
usePerformSecretRollback
|
||||
} from './queries';
|
||||
@@ -0,0 +1,188 @@
|
||||
/* eslint-disable no-param-reassign */
|
||||
import { useInfiniteQuery, useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
|
||||
|
||||
import {
|
||||
decryptAssymmetric,
|
||||
decryptSymmetric
|
||||
} from '@app/components/utilities/cryptography/crypto';
|
||||
import { apiRequest } from '@app/config/request';
|
||||
|
||||
import { DecryptedSecret } from '../secrets/types';
|
||||
import {
|
||||
GetWorkspaceSecretSnapshotsDTO,
|
||||
TSecretRollbackDTO,
|
||||
TSnapshotSecret,
|
||||
TSnapshotSecretProps,
|
||||
TWorkspaceSecretSnapshot
|
||||
} from './types';
|
||||
|
||||
export const secretSnapshotKeys = {
|
||||
list: (workspaceId: string, env: string, folderId?: string) =>
|
||||
[{ workspaceId, env, folderId }, 'secret-snapshot'] as const,
|
||||
snapshotSecrets: (snapshotId: string) => [{ snapshotId }, 'secret-snapshot'] as const,
|
||||
count: (workspaceId: string, env: string, folderId?: string) => [
|
||||
{ workspaceId, env, folderId },
|
||||
'count',
|
||||
'secret-snapshot'
|
||||
]
|
||||
};
|
||||
|
||||
const fetchWorkspaceSecretSnaphots = async (
|
||||
workspaceId: string,
|
||||
environment: string,
|
||||
folderId?: string,
|
||||
limit = 10,
|
||||
offset = 0
|
||||
) => {
|
||||
const res = await apiRequest.get<{ secretSnapshots: TWorkspaceSecretSnapshot[] }>(
|
||||
`/api/v1/workspace/${workspaceId}/secret-snapshots`,
|
||||
{
|
||||
params: {
|
||||
limit,
|
||||
offset,
|
||||
environment,
|
||||
folderId
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
return res.data.secretSnapshots;
|
||||
};
|
||||
|
||||
export const useGetWorkspaceSecretSnapshots = (dto: GetWorkspaceSecretSnapshotsDTO) =>
|
||||
useInfiniteQuery({
|
||||
enabled: Boolean(dto.workspaceId && dto.environment),
|
||||
queryKey: secretSnapshotKeys.list(dto.workspaceId, dto.environment, dto?.folder),
|
||||
queryFn: ({ pageParam }) =>
|
||||
fetchWorkspaceSecretSnaphots(
|
||||
dto.workspaceId,
|
||||
dto.environment,
|
||||
dto?.folder,
|
||||
dto.limit,
|
||||
pageParam
|
||||
),
|
||||
getNextPageParam: (lastPage, pages) =>
|
||||
lastPage.length !== 0 ? pages.length * dto.limit : undefined
|
||||
});
|
||||
|
||||
const fetchSnapshotEncSecrets = async (snapshotId: string) => {
|
||||
const res = await apiRequest.get<{ secretSnapshot: TSnapshotSecret }>(
|
||||
`/api/v1/secret-snapshot/${snapshotId}`
|
||||
);
|
||||
return res.data.secretSnapshot;
|
||||
};
|
||||
|
||||
export const useGetSnapshotSecrets = ({ decryptFileKey, env, snapshotId }: TSnapshotSecretProps) =>
|
||||
useQuery({
|
||||
queryKey: secretSnapshotKeys.snapshotSecrets(snapshotId),
|
||||
enabled: Boolean(snapshotId && decryptFileKey),
|
||||
queryFn: () => fetchSnapshotEncSecrets(snapshotId),
|
||||
select: (data) => {
|
||||
const PRIVATE_KEY = localStorage.getItem('PRIVATE_KEY') as string;
|
||||
const latestKey = decryptFileKey;
|
||||
const key = decryptAssymmetric({
|
||||
ciphertext: latestKey.encryptedKey,
|
||||
nonce: latestKey.nonce,
|
||||
publicKey: latestKey.sender.publicKey,
|
||||
privateKey: PRIVATE_KEY
|
||||
});
|
||||
|
||||
const sharedSecrets: DecryptedSecret[] = [];
|
||||
const personalSecrets: Record<string, { id: string; value: string }> = {};
|
||||
data.secretVersions
|
||||
.filter(({ environment }) => environment === env)
|
||||
.forEach((encSecret) => {
|
||||
const secretKey = decryptSymmetric({
|
||||
ciphertext: encSecret.secretKeyCiphertext,
|
||||
iv: encSecret.secretKeyIV,
|
||||
tag: encSecret.secretKeyTag,
|
||||
key
|
||||
});
|
||||
|
||||
const secretValue = decryptSymmetric({
|
||||
ciphertext: encSecret.secretValueCiphertext,
|
||||
iv: encSecret.secretValueIV,
|
||||
tag: encSecret.secretValueTag,
|
||||
key
|
||||
});
|
||||
|
||||
const secretComment = '';
|
||||
|
||||
const decryptedSecret = {
|
||||
_id: encSecret.secret,
|
||||
env: encSecret.environment,
|
||||
key: secretKey,
|
||||
value: secretValue,
|
||||
tags: encSecret.tags,
|
||||
comment: secretComment,
|
||||
createdAt: encSecret.createdAt,
|
||||
updatedAt: encSecret.updatedAt,
|
||||
type: 'modified'
|
||||
};
|
||||
|
||||
if (encSecret.type === 'personal') {
|
||||
personalSecrets[decryptedSecret.key] = { id: encSecret.secret, value: secretValue };
|
||||
} else {
|
||||
sharedSecrets.push(decryptedSecret);
|
||||
}
|
||||
});
|
||||
|
||||
sharedSecrets.forEach((val) => {
|
||||
if (personalSecrets?.[val.key]) {
|
||||
val.idOverride = personalSecrets[val.key].id;
|
||||
val.valueOverride = personalSecrets[val.key].value;
|
||||
val.overrideAction = 'modified';
|
||||
}
|
||||
});
|
||||
|
||||
return {
|
||||
version: data.version,
|
||||
secrets: sharedSecrets,
|
||||
createdAt: data.createdAt,
|
||||
folders: data.folderVersion
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
const fetchWorkspaceSecretSnaphotCount = async (
|
||||
workspaceId: string,
|
||||
environment: string,
|
||||
folderId?: string
|
||||
) => {
|
||||
const res = await apiRequest.get<{ count: number }>(
|
||||
`/api/v1/workspace/${workspaceId}/secret-snapshots/count`,
|
||||
{
|
||||
params: {
|
||||
environment,
|
||||
folderId
|
||||
}
|
||||
}
|
||||
);
|
||||
return res.data.count;
|
||||
};
|
||||
|
||||
export const useGetWsSnapshotCount = (workspaceId: string, env: string, folderId?: string) =>
|
||||
useQuery({
|
||||
enabled: Boolean(workspaceId && env),
|
||||
queryKey: secretSnapshotKeys.count(workspaceId, env, folderId),
|
||||
queryFn: () => fetchWorkspaceSecretSnaphotCount(workspaceId, env, folderId)
|
||||
});
|
||||
|
||||
export const usePerformSecretRollback = () => {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutation<{}, {}, TSecretRollbackDTO>({
|
||||
mutationFn: async ({ workspaceId, ...dto }) => {
|
||||
const { data } = await apiRequest.post(
|
||||
`/api/v1/workspace/${workspaceId}/secret-snapshots/rollback`,
|
||||
dto
|
||||
);
|
||||
return data;
|
||||
},
|
||||
onSuccess: (_, { workspaceId, environment, folderId }) => {
|
||||
queryClient.invalidateQueries([{ workspaceId, environment }, 'secrets']);
|
||||
queryClient.invalidateQueries(secretSnapshotKeys.list(workspaceId, environment, folderId));
|
||||
queryClient.invalidateQueries(secretSnapshotKeys.count(workspaceId, environment, folderId));
|
||||
}
|
||||
});
|
||||
};
|
||||
@@ -0,0 +1,37 @@
|
||||
import { UserWsKeyPair } from '../keys/types';
|
||||
import { EncryptedSecretVersion } from '../secrets/types';
|
||||
|
||||
export type TWorkspaceSecretSnapshot = {
|
||||
_id: string;
|
||||
workspace: string;
|
||||
version: number;
|
||||
secretVersions: string[];
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
__v: number;
|
||||
};
|
||||
|
||||
export type TSnapshotSecret = Omit<TWorkspaceSecretSnapshot, 'secretVersions'> & {
|
||||
secretVersions: EncryptedSecretVersion[];
|
||||
folderVersion: Array<{ name: string; id: string }>;
|
||||
};
|
||||
|
||||
export type TSnapshotSecretProps = {
|
||||
snapshotId: string;
|
||||
env: string;
|
||||
decryptFileKey: UserWsKeyPair;
|
||||
};
|
||||
|
||||
export type GetWorkspaceSecretSnapshotsDTO = {
|
||||
workspaceId: string;
|
||||
limit: number;
|
||||
environment: string;
|
||||
folder?: string;
|
||||
};
|
||||
|
||||
export type TSecretRollbackDTO = {
|
||||
workspaceId: string;
|
||||
version: number;
|
||||
environment: string;
|
||||
folderId?: string;
|
||||
};
|
||||
@@ -0,0 +1,6 @@
|
||||
export {
|
||||
useBatchSecretsOp,
|
||||
useGetProjectSecrets,
|
||||
useGetProjectSecretsByKey,
|
||||
useGetSecretVersion
|
||||
} from './queries';
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user