From 810554e13c8fe4c67d80787b741719ec8102d26e Mon Sep 17 00:00:00 2001 From: Vladyslav Matsiiako Date: Wed, 12 Apr 2023 13:41:12 -0700 Subject: [PATCH 1/7] First commit of env overview --- frontend/src/components/v2/Button/Button.tsx | 6 + frontend/src/components/v2/Table/Table.tsx | 2 +- .../src/ee/components/SecretVersionList.tsx | 4 +- frontend/src/hooks/api/secrets/queries.tsx | 71 ++++- frontend/src/hooks/api/secrets/types.ts | 2 +- frontend/src/layouts/AppLayout/AppLayout.tsx | 7 +- frontend/src/pages/dashboard/[id].tsx | 6 +- .../DashboardPage/DashboardEnvOverview.tsx | 269 ++++++++++++++++++ .../EnvComparisonHeader/EnvComparison.tsx | 22 ++ .../components/EnvComparisonHeader/index.tsx | 1 + .../EnvComparisonRow/EnvComparisonRow.tsx | 153 ++++++++++ .../components/EnvComparisonRow/index.tsx | 1 + 12 files changed, 524 insertions(+), 20 deletions(-) create mode 100644 frontend/src/views/DashboardPage/DashboardEnvOverview.tsx create mode 100644 frontend/src/views/DashboardPage/components/EnvComparisonHeader/EnvComparison.tsx create mode 100644 frontend/src/views/DashboardPage/components/EnvComparisonHeader/index.tsx create mode 100644 frontend/src/views/DashboardPage/components/EnvComparisonRow/EnvComparisonRow.tsx create mode 100644 frontend/src/views/DashboardPage/components/EnvComparisonRow/index.tsx diff --git a/frontend/src/components/v2/Button/Button.tsx b/frontend/src/components/v2/Button/Button.tsx index c4a89023c..17ae7b867 100644 --- a/frontend/src/components/v2/Button/Button.tsx +++ b/frontend/src/components/v2/Button/Button.tsx @@ -34,6 +34,7 @@ const buttonVariants = cva( 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' }, @@ -67,6 +68,11 @@ const buttonVariants = cva( variant: 'selected', className: 'bg-primary/10 border border-primary/50 text-bunker-200' }, + { + colorSchema: 'primary', + variant: 'outline_bg', + className: 'bg-mineshaft-800 border border-mineshaft-600 hover:bg-primary/[0.15] hover:border-primary/60 text-bunker-200' + }, { colorSchema: 'secondary', variant: 'star', diff --git a/frontend/src/components/v2/Table/Table.tsx b/frontend/src/components/v2/Table/Table.tsx index dd4bb4e7b..411fffab1 100644 --- a/frontend/src/components/v2/Table/Table.tsx +++ b/frontend/src/components/v2/Table/Table.tsx @@ -34,7 +34,7 @@ export type TableProps = { export const Table = ({ children, className }: TableProps): JSX.Element => ( diff --git a/frontend/src/ee/components/SecretVersionList.tsx b/frontend/src/ee/components/SecretVersionList.tsx index 3f16a228d..dbf7c5bf2 100644 --- a/frontend/src/ee/components/SecretVersionList.tsx +++ b/frontend/src/ee/components/SecretVersionList.tsx @@ -76,7 +76,7 @@ const SecretVersionList = ({ secretId }: { secretId: string }) => { }, [secretId]); return ( -
+

{t('dashboard:sidebar.version-history')}

{isLoading ? ( @@ -102,7 +102,7 @@ const SecretVersionList = ({ secretId }: { secretId: string }) => {
-
+
{new Date(version.createdAt).toLocaleDateString('en-US', { year: 'numeric', month: '2-digit', diff --git a/frontend/src/hooks/api/secrets/queries.tsx b/frontend/src/hooks/api/secrets/queries.tsx index 9b9c49df5..bc4c5e515 100644 --- a/frontend/src/hooks/api/secrets/queries.tsx +++ b/frontend/src/hooks/api/secrets/queries.tsx @@ -19,18 +19,66 @@ import { export const secretKeys = { // this is also used in secretSnapshot part - getProjectSecret: (workspaceId: string, env: string) => [{ workspaceId, env }, 'secrets'], + getProjectSecret: (workspaceId: string, env: string | string[]) => [{ workspaceId, env }, 'secrets'], getSecretVersion: (secretId: string) => [{ secretId }, 'secret-versions'] }; -const fetchProjectEncryptedSecrets = async (workspaceId: string, env: string) => { - const { data } = await apiRequest.get<{ secrets: EncryptedSecret[] }>('/api/v2/secrets', { - params: { - environment: env, - workspaceId +const fetchProjectEncryptedSecrets = async (workspaceId: string, env: string | string[]) => { + if (typeof env === 'string') { + const { data } = await apiRequest.get<{ secrets: EncryptedSecret[] }>('/api/v2/secrets', { + params: { + environment: env, + workspaceId + } + }); + return data.secrets; + } + + if (typeof env === 'object') { + let allEnvData: any = []; + // env.map(async (envPoint: string) => { + // const { data } = await apiRequest.get<{ secrets: EncryptedSecret[] }>('/api/v2/secrets', { + // params: { + // environment: envPoint, + // workspaceId + // } + // }); + // console.log(111, envPoint, data.secrets) + // allEnvData = allEnvData.concat(data.secrets); + // // await allEnvData.push(...data.secrets) + // console.log(222, allEnvData) + // }) + // eslint-disable-next-line no-restricted-syntax + for (const envPoint of env) { + // eslint-disable-next-line no-await-in-loop + const { data } = await apiRequest.get<{ secrets: EncryptedSecret[] }>('/api/v2/secrets', { + params: { + environment: envPoint, + workspaceId + } + }); + allEnvData = allEnvData.concat(data.secrets); } - }); - return data.secrets; + // const { data: data1 } = await apiRequest.get<{ secrets: EncryptedSecret[] }>('/api/v2/secrets', { + // params: { + // environment: env[0], + // workspaceId + // } + // }); + // const { data: data2 } = await apiRequest.get<{ secrets: EncryptedSecret[] }>('/api/v2/secrets', { + // params: { + // environment: env[1], + // workspaceId + // } + // }); + // allEnvData = data1.secrets.concat(data2.secrets); + + return allEnvData; + // eslint-disable-next-line no-else-return + } else { + return null; + } + }; export const useGetProjectSecrets = ({ @@ -45,6 +93,7 @@ export const useGetProjectSecrets = ({ queryKey: secretKeys.getProjectSecret(workspaceId, env), queryFn: () => fetchProjectEncryptedSecrets(workspaceId, env), select: (data) => { + console.log(878787878, data) const PRIVATE_KEY = localStorage.getItem('PRIVATE_KEY') as string; const latestKey = decryptFileKey; const key = decryptAssymmetric({ @@ -93,12 +142,12 @@ export const useGetProjectSecrets = ({ }; if (encSecret.type === 'personal') { - personalSecrets[decryptedSecret.key] = { id: encSecret._id, value: secretValue }; + personalSecrets[`${decryptedSecret.key}-${decryptedSecret.env}`] = { id: encSecret._id, value: secretValue }; } else { - if (!duplicateSecretKey?.[decryptedSecret.key]) { + if (!duplicateSecretKey?.[`${decryptedSecret.key}-${decryptedSecret.env}`]) { sharedSecrets.push(decryptedSecret); } - duplicateSecretKey[decryptedSecret.key] = true; + duplicateSecretKey[`${decryptedSecret.key}-${decryptedSecret.env}`] = true; } }); sharedSecrets.forEach((val) => { diff --git a/frontend/src/hooks/api/secrets/types.ts b/frontend/src/hooks/api/secrets/types.ts index 7888e64b3..567fecc9a 100644 --- a/frontend/src/hooks/api/secrets/types.ts +++ b/frontend/src/hooks/api/secrets/types.ts @@ -90,7 +90,7 @@ export type BatchSecretDTO = { export type GetProjectSecretsDTO = { workspaceId: string; - env: string; + env: string | string[]; decryptFileKey: UserWsKeyPair; isPaused?: boolean; onSuccess?: (data: DecryptedSecret[]) => void; diff --git a/frontend/src/layouts/AppLayout/AppLayout.tsx b/frontend/src/layouts/AppLayout/AppLayout.tsx index 93ed5b860..3e3641a0a 100644 --- a/frontend/src/layouts/AppLayout/AppLayout.tsx +++ b/frontend/src/layouts/AppLayout/AppLayout.tsx @@ -271,11 +271,12 @@ export const AppLayout = ({ children }: LayoutProps) => { {name} ))} -
+ {/*
*/}
+ + + {[... new Set(secrets?.secrets.map((secret: any) => secret.key))].map((key, index) => ( + secret.key === key)} + isReadOnly={isReadOnly} + isAddOnly={isAddOnly} + index={index} + isSecretValueHidden={isSecretValueHidden} + userAvailableEnvs={userAvailableEnvs} + /> + ))} + + + + + + {userAvailableEnvs?.map(env => { + return <> + + + + })} + + +
0
+
1
+
+
{0}
+
+ +
+ + )} + {/*
+ +
*/} + + + { + handlePopUpToggle('addTag', open); + }} + > + + + + + + + ); +}; diff --git a/frontend/src/views/DashboardPage/components/EnvComparisonHeader/EnvComparison.tsx b/frontend/src/views/DashboardPage/components/EnvComparisonHeader/EnvComparison.tsx new file mode 100644 index 000000000..136085d26 --- /dev/null +++ b/frontend/src/views/DashboardPage/components/EnvComparisonHeader/EnvComparison.tsx @@ -0,0 +1,22 @@ +export const EnvComparisonHeader = ({ userAvailableEnvs }: { userAvailableEnvs?: any[] }): JSX.Element => ( + + + +
{0}
+ + +
+
Secret
+
+ + {userAvailableEnvs?.map(env => { + return <> + +
{0}
+ +
{env.name}
+ + })} + + +); diff --git a/frontend/src/views/DashboardPage/components/EnvComparisonHeader/index.tsx b/frontend/src/views/DashboardPage/components/EnvComparisonHeader/index.tsx new file mode 100644 index 000000000..0bba0fb5f --- /dev/null +++ b/frontend/src/views/DashboardPage/components/EnvComparisonHeader/index.tsx @@ -0,0 +1 @@ +export { EnvComparisonHeader } from './EnvComparison'; diff --git a/frontend/src/views/DashboardPage/components/EnvComparisonRow/EnvComparisonRow.tsx b/frontend/src/views/DashboardPage/components/EnvComparisonRow/EnvComparisonRow.tsx new file mode 100644 index 000000000..8dbe32bf3 --- /dev/null +++ b/frontend/src/views/DashboardPage/components/EnvComparisonRow/EnvComparisonRow.tsx @@ -0,0 +1,153 @@ +/* eslint-disable react/jsx-no-useless-fragment */ +import { SyntheticEvent, useRef } from 'react'; +import { useFormContext, useWatch } from 'react-hook-form'; +import { faCircle } from '@fortawesome/free-solid-svg-icons'; +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; + +import guidGenerator from '@app/components/utilities/randomId'; +import { Input } from '@app/components/v2'; + +import { FormData, SecretActionType } from '../../DashboardPage.utils'; + +type Props = { + index: number; + secrets: any[] | undefined; + // permission and external state's that decided to hide or show + isReadOnly?: boolean; + isAddOnly?: boolean; + isSecretValueHidden: boolean; + userAvailableEnvs?: any[]; +}; + +const REGEX = /([$]{.*?})/g; + +const DashboardInput = ({ isOverridden, isSecretValueHidden, isAddOnly, isReadOnly, secret, shouldBeBlockedInAddOnly, index }: { isOverridden: boolean, isSecretValueHidden: boolean, isAddOnly?: boolean, isReadOnly?: boolean, secret: any, shouldBeBlockedInAddOnly?: boolean, index: number } ): JSX.Element => { + const ref = useRef(null); + const syncScroll = (e: SyntheticEvent) => { + if (ref.current === null) return; + + ref.current.scrollTop = e.currentTarget.scrollTop; + ref.current.scrollLeft = e.currentTarget.scrollLeft; + }; + console.log(33333333, secret) + + return +
+ +
+ {(isOverridden ? secret.valueOverride : secret?.value || '-')?.split('').length === 0 && EMPTY} + {(isOverridden ? secret.valueOverride : secret?.value || '-')?.split(REGEX).map((word: string) => { + if (word.match(REGEX) !== null) { + return ( + + {word.slice(0, 2)} + + {word.slice(2, word.length - 1)} + + {word.slice(word.length - 1, word.length) === '}' ? ( + + {word.slice(word.length - 1, word.length)} + + ) : ( + + {word.slice(word.length - 1, word.length)} + + )} + + ); + } + return ( + + {word} + + ); + })} +
+ {(isSecretValueHidden && secret?.value) && ( +
+
+ {(isOverridden ? secret.valueOverride : secret?.value || '-')?.split('').map(() => ( + + ))} + {(isOverridden ? secret.valueOverride : secret?.value || '-')?.split('').length === 0 && EMPTY} +
+
+ )} +
+ +} + +export const EnvComparisonRow = ({ + index, + secrets, + isSecretValueHidden, + isReadOnly, + isAddOnly, + userAvailableEnvs +}: Props): JSX.Element => { + const { + // register, setValue, + control } = useFormContext(); + console.log(1282828822, userAvailableEnvs) + + console.log('index', index) + // to get details on a secret + const secret = useWatch({ name: `secrets.${index}`, control }); + + // when secret is override by personal values + const isOverridden = + secret.overrideAction === SecretActionType.Created || + secret.overrideAction === SecretActionType.Modified; + + const isCreatedSecret = !secret?._id; + const shouldBeBlockedInAddOnly = !isCreatedSecret && isAddOnly; + console.log(893892749827097, secrets) + + return ( + +
{index + 1}
+ +
+ +
+ + {userAvailableEnvs?.map(env => { + return <> + +
{0}
+ + sec.env === env.slug)[0]} shouldBeBlockedInAddOnly={shouldBeBlockedInAddOnly} index={index} /> + + })} + + ); +}; diff --git a/frontend/src/views/DashboardPage/components/EnvComparisonRow/index.tsx b/frontend/src/views/DashboardPage/components/EnvComparisonRow/index.tsx new file mode 100644 index 000000000..e0c9f8847 --- /dev/null +++ b/frontend/src/views/DashboardPage/components/EnvComparisonRow/index.tsx @@ -0,0 +1 @@ +export { EnvComparisonRow } from './EnvComparisonRow'; From 6b1f704a44c7fe83f365ccda7a8b38f64e979aac Mon Sep 17 00:00:00 2001 From: mv-turtle <78047717+mv-turtle@users.noreply.github.com> Date: Fri, 14 Apr 2023 12:49:12 -0700 Subject: [PATCH 2/7] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 449b8cce6..6e6d40855 100644 --- a/README.md +++ b/README.md @@ -25,7 +25,7 @@ git commit activity - Cloudsmith downloads + Cloudsmith downloads Slack community channel From 6d8b16fc851391950884450d16b4450a37ebcc78 Mon Sep 17 00:00:00 2001 From: Maidul Islam Date: Fri, 14 Apr 2023 16:16:37 -0700 Subject: [PATCH 3/7] mark smtp fields as not required --- .env.example | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/.env.example b/.env.example index 79fca5686..df316753e 100644 --- a/.env.example +++ b/.env.example @@ -31,12 +31,12 @@ MONGO_PASSWORD=example SITE_URL=http://localhost:8080 # Mail/SMTP -SMTP_HOST= # required -SMTP_USERNAME= # required -SMTP_PASSWORD= # required +SMTP_HOST= +SMTP_USERNAME= +SMTP_PASSWORD= SMTP_PORT=587 SMTP_SECURE=false -SMTP_FROM_ADDRESS= # required +SMTP_FROM_ADDRESS= SMTP_FROM_NAME=Infisical # Integration @@ -66,4 +66,4 @@ STRIPE_WEBHOOK_SECRET= STRIPE_PRODUCT_STARTER= STRIPE_PRODUCT_TEAM= STRIPE_PRODUCT_PRO= -NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY= \ No newline at end of file +NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY= From 903560a2d17fe22814a10f6eb57ba58d83d4794c Mon Sep 17 00:00:00 2001 From: Vladyslav Matsiiako Date: Fri, 14 Apr 2023 18:20:19 -0700 Subject: [PATCH 4/7] Finished the env overview feature --- frontend/src/components/analytics/posthog.ts | 2 +- .../src/components/navigation/NavHeader.tsx | 59 ++++- frontend/src/components/v2/Select/Select.tsx | 6 +- frontend/src/components/v2/Table/Table.tsx | 2 +- frontend/src/hooks/api/secrets/queries.tsx | 29 +-- frontend/src/pages/dashboard/[id].tsx | 13 +- .../DashboardPage/DashboardEnvOverview.tsx | 236 +++++++++--------- .../src/views/DashboardPage/DashboardPage.tsx | 34 +-- .../EnvComparisonHeader/EnvComparison.tsx | 22 -- .../components/EnvComparisonHeader/index.tsx | 1 - .../EnvComparisonRow/EnvComparisonRow.tsx | 68 ++--- .../SecretInputRow/SecretInputRow.tsx | 4 +- .../SecretTagsSection/SecretTagsSection.tsx | 1 - 13 files changed, 222 insertions(+), 255 deletions(-) delete mode 100644 frontend/src/views/DashboardPage/components/EnvComparisonHeader/EnvComparison.tsx delete mode 100644 frontend/src/views/DashboardPage/components/EnvComparisonHeader/index.tsx diff --git a/frontend/src/components/analytics/posthog.ts b/frontend/src/components/analytics/posthog.ts index 29208a795..b6f3e4341 100644 --- a/frontend/src/components/analytics/posthog.ts +++ b/frontend/src/components/analytics/posthog.ts @@ -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 diff --git a/frontend/src/components/navigation/NavHeader.tsx b/frontend/src/components/navigation/NavHeader.tsx index e1ee09080..c8af1ce46 100644 --- a/frontend/src/components/navigation/NavHeader.tsx +++ b/frontend/src/components/navigation/NavHeader.tsx @@ -1,52 +1,89 @@ +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'; + // 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 or not this page is related to project (determine if it's 2 or 3 navigation steps) - * @param obj.isOrganizationRelated - whether or not this page is related to organization (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 */ export default function NavHeader({ pageName, isProjectRelated, - isOrganizationRelated + isOrganizationRelated, + currentEnv, + userAvailableEnvs, + onEnvChange }: { pageName: string; isProjectRelated?: boolean; isOrganizationRelated?: boolean; + currentEnv?: string; + userAvailableEnvs?: any[]; + onEnvChange?: (slug: string) => void; }): JSX.Element { const { currentWorkspace } = useWorkspace(); const { currentOrg } = useOrganization(); + const router = useRouter() return (
{currentOrg?.name?.charAt(0)}
-
{currentOrg?.name}
+
{currentOrg?.name}
{isProjectRelated && ( <> - -
{currentWorkspace?.name}
+ +
{currentWorkspace?.name}
)} {isOrganizationRelated && ( <> - -
Organization Settings
+ +
Organization Settings
)} -
{pageName}
+ {pageName === 'Secrets' + ?
{pageName} + :
{pageName}
} + {currentEnv && + <> + +
+ + + +
+ }
); } diff --git a/frontend/src/components/v2/Select/Select.tsx b/frontend/src/components/v2/Select/Select.tsx index ffeaf3c3f..46cd3fb03 100644 --- a/frontend/src/components/v2/Select/Select.tsx +++ b/frontend/src/components/v2/Select/Select.tsx @@ -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, faCheck, faChevronUp } 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'; @@ -49,7 +49,7 @@ export const Select = forwardRef( {!isDisabled && ( - + )} @@ -76,7 +76,7 @@ export const Select = forwardRef( )} - + diff --git a/frontend/src/components/v2/Table/Table.tsx b/frontend/src/components/v2/Table/Table.tsx index 411fffab1..dd4bb4e7b 100644 --- a/frontend/src/components/v2/Table/Table.tsx +++ b/frontend/src/components/v2/Table/Table.tsx @@ -34,7 +34,7 @@ export type TableProps = { export const Table = ({ children, className }: TableProps): JSX.Element => ( diff --git a/frontend/src/hooks/api/secrets/queries.tsx b/frontend/src/hooks/api/secrets/queries.tsx index bc4c5e515..031c661d1 100644 --- a/frontend/src/hooks/api/secrets/queries.tsx +++ b/frontend/src/hooks/api/secrets/queries.tsx @@ -36,18 +36,7 @@ const fetchProjectEncryptedSecrets = async (workspaceId: string, env: string | s if (typeof env === 'object') { let allEnvData: any = []; - // env.map(async (envPoint: string) => { - // const { data } = await apiRequest.get<{ secrets: EncryptedSecret[] }>('/api/v2/secrets', { - // params: { - // environment: envPoint, - // workspaceId - // } - // }); - // console.log(111, envPoint, data.secrets) - // allEnvData = allEnvData.concat(data.secrets); - // // await allEnvData.push(...data.secrets) - // console.log(222, allEnvData) - // }) + // eslint-disable-next-line no-restricted-syntax for (const envPoint of env) { // eslint-disable-next-line no-await-in-loop @@ -59,19 +48,6 @@ const fetchProjectEncryptedSecrets = async (workspaceId: string, env: string | s }); allEnvData = allEnvData.concat(data.secrets); } - // const { data: data1 } = await apiRequest.get<{ secrets: EncryptedSecret[] }>('/api/v2/secrets', { - // params: { - // environment: env[0], - // workspaceId - // } - // }); - // const { data: data2 } = await apiRequest.get<{ secrets: EncryptedSecret[] }>('/api/v2/secrets', { - // params: { - // environment: env[1], - // workspaceId - // } - // }); - // allEnvData = data1.secrets.concat(data2.secrets); return allEnvData; // eslint-disable-next-line no-else-return @@ -93,7 +69,6 @@ export const useGetProjectSecrets = ({ queryKey: secretKeys.getProjectSecret(workspaceId, env), queryFn: () => fetchProjectEncryptedSecrets(workspaceId, env), select: (data) => { - console.log(878787878, data) const PRIVATE_KEY = localStorage.getItem('PRIVATE_KEY') as string; const latestKey = decryptFileKey; const key = decryptAssymmetric({ @@ -108,7 +83,7 @@ export const useGetProjectSecrets = ({ // this used for add-only mode in dashboard // type won't be there thus only one key is shown const duplicateSecretKey: Record = {}; - data.forEach((encSecret) => { + data.forEach((encSecret: EncryptedSecret) => { const secretKey = decryptSymmetric({ ciphertext: encSecret.secretKeyCiphertext, iv: encSecret.secretKeyIV, diff --git a/frontend/src/pages/dashboard/[id].tsx b/frontend/src/pages/dashboard/[id].tsx index e73d3033e..bb81245ec 100644 --- a/frontend/src/pages/dashboard/[id].tsx +++ b/frontend/src/pages/dashboard/[id].tsx @@ -1,12 +1,18 @@ import Head from 'next/head'; +import { useRouter } from 'next/router'; import { useTranslation } from 'next-i18next'; +import queryString from 'query-string'; import { getTranslatedServerSideProps } from '@app/components/utilities/withTranslateProps'; -// import { DashboardPage } from '@app/views/DashboardPage'; +import { DashboardPage } from '@app/views/DashboardPage'; import { DashboardEnvOverview } from '@app/views/DashboardPage/DashboardEnvOverview'; const Dashboard = () => { const { t } = useTranslation(); + const router = useRouter(); + + const env = queryString.parse(router.asPath.split('?')[1])?.env; + return ( <> @@ -16,8 +22,9 @@ const Dashboard = () => { - {/* */} - + {env + ? + : } ); }; diff --git a/frontend/src/views/DashboardPage/DashboardEnvOverview.tsx b/frontend/src/views/DashboardPage/DashboardEnvOverview.tsx index 75f79db9f..a7545fb1c 100644 --- a/frontend/src/views/DashboardPage/DashboardEnvOverview.tsx +++ b/frontend/src/views/DashboardPage/DashboardEnvOverview.tsx @@ -1,5 +1,5 @@ import { useEffect, useState } from 'react'; -import { FormProvider, useFieldArray, useForm, useWatch } from 'react-hook-form'; +import { FormProvider, useForm, useWatch } from 'react-hook-form'; import { useTranslation } from 'react-i18next'; import { useRouter } from 'next/router'; import { yupResolver } from '@hookform/resolvers/yup'; @@ -10,10 +10,11 @@ import { Button, Modal, ModalContent, - TableContainer + TableContainer, + Tooltip } from '@app/components/v2'; import { useWorkspace } from '@app/context'; -import { usePopUp, useToggle } from '@app/hooks'; +import { usePopUp } from '@app/hooks'; import { useCreateWsTag, useGetProjectSecrets, @@ -23,26 +24,13 @@ import { import { WorkspaceEnv } from '@app/hooks/api/types'; import { CreateTagModal } from './components/CreateTagModal'; -import { EnvComparisonHeader } from './components/EnvComparisonHeader'; import { EnvComparisonRow } from './components/EnvComparisonRow'; import { - DEFAULT_SECRET_VALUE, FormData, schema } from './DashboardPage.utils'; -/* - * Some imp aspects to consider. Here there are multiple stats changing - * Thus ideally we need to use a context. But instead we rely on react hook form - * React hook form provides context and high performance proxy based rendering - * It also handles error handling and transferring states between inputs - * - * Another thing is the purpose of overrideAction - * Before we would remove the value for personal secret when user toggle and user couldn't get it back - * They have to reload the browser or go back all over again - * Instead when user delete we raise a flag so if user decides to go back to toggle personal before saving - * They will get it back - */ + export const DashboardEnvOverview = () => { const { t } = useTranslation(); const router = useRouter(); @@ -57,11 +45,7 @@ export const DashboardEnvOverview = () => { 'uploadedSecOpts', 'compareSecrets' ] as const); - const [isSecretValueHidden, setIsSecretValueHidden] = useToggle(true); - const [snapshotId, setSnaphotId] = useState(null); - console.log(setIsSecretValueHidden, setSnaphotId) const [selectedEnv, setSelectedEnv] = useState(null); - // const [sortDir, setSortDir] = useState<'asc' | 'desc'>('asc'); const { currentWorkspace, isLoading } = useWorkspace(); const workspaceId = currentWorkspace?._id as string; @@ -77,26 +61,25 @@ export const DashboardEnvOverview = () => { workspaceId, onSuccess: (data) => { // get an env with one of the access available - const env = data.find(({ isReadDenied, isWriteDenied }) => !isWriteDenied || !isReadDenied); + const env = data.find(({ isReadDenied }) => !isReadDenied); if (env) { setSelectedEnv(env); } } }); + + const userAvailableEnvs = wsEnv?.filter( + ({ isReadDenied }) => !isReadDenied + ); const { data: secrets, isLoading: isSecretsLoading } = useGetProjectSecrets({ workspaceId, - env: wsEnv?.map(env => env.slug) ?? [], + env: userAvailableEnvs?.map(env => env.slug) ?? [], decryptFileKey: latestFileKey!, - isPaused: Boolean(snapshotId) + isPaused: false }); - console.log(333333, secrets, [... new Set(secrets?.secrets.map((secret: any) => secret.key))]) - // mutation calls - // const { mutateAsync: batchSecretOp } = useBatchSecretsOp(); - // const { mutateAsync: performSecretRollback } = usePerformSecretRollback(); - // const { mutateAsync: registerUserAction } = useRegisterUserAction(); const { mutateAsync: createWsTag } = useCreateWsTag(); const method = useForm({ @@ -116,27 +99,8 @@ export const DashboardEnvOverview = () => { // reset } = method; const formSecrets = useWatch({ control, name: 'secrets' }); - console.log(formSecrets) - const { fields, prepend, - // append, remove, update - } = useFieldArray({ control, name: 'secrets' }); - console.log(987, fields, secrets?.secrets.map((secret: any) => secret.key)) - const isRollbackMode = Boolean(snapshotId); const isReadOnly = selectedEnv?.isWriteDenied; - const isAddOnly = selectedEnv?.isReadDenied && !selectedEnv?.isWriteDenied; - // const canDoRollback = !isReadOnly && !isAddOnly; - - - // const onSortSecrets = () => { - // const dir = sortDir === 'asc' ? 'desc' : 'asc'; - // const sec = getValues('secrets') || []; - // const sortedSec = sec.sort((a, b) => - // dir === 'asc' ? a?.key?.localeCompare(b?.key || '') : b?.key?.localeCompare(a?.key || '') - // ); - // setValue('secrets', sortedSec); - // setSortDir(dir); - // }; const onCreateWsTag = async (tagName: string) => { try { @@ -169,11 +133,8 @@ export const DashboardEnvOverview = () => { // when secrets is not loading and secrets list is empty const isDashboardSecretEmpty = !isSecretsLoading && !formSecrets?.length; - const isSecretEmpty = (!isRollbackMode && isDashboardSecretEmpty); - const userAvailableEnvs = wsEnv?.filter( - ({ isReadDenied, isWriteDenied }) => !isReadDenied || !isWriteDenied - ); + const numSecretsMissingPerEnv = userAvailableEnvs?.map(envir => ({[envir.slug]: [... new Set(secrets?.secrets.map((secret: any) => secret.key))].length - [... new Set(secrets?.secrets.filter(s => s.env === envir.slug).map((secret: any) => secret.key))].length})).reduce((acc, cur) => ({ ...acc, ...cur }), {}) return (
@@ -183,71 +144,116 @@ export const DashboardEnvOverview = () => {
-
+

Secrets Overview

-

Put your secrets to work with the Infisical CLI

-
-
- {!isSecretEmpty && ( - -
- - - {[... new Set(secrets?.secrets.map((secret: any) => secret.key))].map((key, index) => ( - secret.key === key)} - isReadOnly={isReadOnly} - isAddOnly={isAddOnly} - index={index} - isSecretValueHidden={isSecretValueHidden} - userAvailableEnvs={userAvailableEnvs} - /> - ))} - - - - - - {userAvailableEnvs?.map(env => { - return <> - - - - })} - - -
0
-
1
-
-
{0}
-
- -
- - )} - {/*
- -
*/} + Infisical CLI + + or + + Infisical SDKs +

+ +
+
+
+
{0}
+
+
+
+
Secret
+
+
+ {numSecretsMissingPerEnv && userAvailableEnvs?.map(env => { + return
+
+ {env.name} + {numSecretsMissingPerEnv[env.slug] > 0 &&
+ {numSecretsMissingPerEnv[env.slug]} +
} +
+
+ })} +
+
+ {!isDashboardSecretEmpty && ( + + + + {[... new Set(secrets?.secrets.map((secret: any) => secret.key))].map((key, index) => ( + secret.key === key)} + isReadOnly={isReadOnly} + index={index} + isSecretValueHidden + userAvailableEnvs={userAvailableEnvs} + /> + ))} + +
+
+ )} + {isDashboardSecretEmpty && +
+
+
{0}
+
+
+
+
Secret
+
+
+
+ No secrets are available in this project yet. + You can go into any environment to add secrets there. +
+
} + {/* In future, we should add an option to add environments here +
+ +
*/} +
+
+
0
+
+ 0 + +
+ {userAvailableEnvs?.map(env => { + return
+ +
+ })} +
{ +export const DashboardPage = ({ envFromTop }: { envFromTop: string }) => { const { t } = useTranslation(); const router = useRouter(); const { createNotification } = useNotificationContext(); @@ -126,8 +124,8 @@ export const DashboardPage = () => { onSuccess: (data) => { // get an env with one of the access available const env = data.find(({ isReadDenied, isWriteDenied }) => !isWriteDenied || !isReadDenied); - if (env) { - setSelectedEnv(env); + if (env && data?.map(wsenv => wsenv.slug).includes(envFromTop)) { + setSelectedEnv(data?.filter(dp => dp.slug === envFromTop)[0]); } } }); @@ -357,6 +355,7 @@ export const DashboardPage = () => { } const env = wsEnv?.find((el) => el.slug === slug); if (env) setSelectedEnv(env); + router.push(`${router.asPath.split("?")[0]}?env=${slug}`) }; // record all deleted ids @@ -417,7 +416,13 @@ export const DashboardPage = () => {
{/* breadcrumb row */}
- + envir.slug === envFromTop)[0].name || ''} + isProjectRelated + userAvailableEnvs={userAvailableEnvs} + onEnvChange={onEnvChange} + />
{/* Secrets, commit and save button section */}
@@ -468,23 +473,6 @@ export const DashboardPage = () => {
{/* Environment, search and other action row */}
-
- - - -
( - - - -
{0}
- - -
-
Secret
-
- - {userAvailableEnvs?.map(env => { - return <> - -
{0}
- -
{env.name}
- - })} - - -); diff --git a/frontend/src/views/DashboardPage/components/EnvComparisonHeader/index.tsx b/frontend/src/views/DashboardPage/components/EnvComparisonHeader/index.tsx deleted file mode 100644 index 0bba0fb5f..000000000 --- a/frontend/src/views/DashboardPage/components/EnvComparisonHeader/index.tsx +++ /dev/null @@ -1 +0,0 @@ -export { EnvComparisonHeader } from './EnvComparison'; diff --git a/frontend/src/views/DashboardPage/components/EnvComparisonRow/EnvComparisonRow.tsx b/frontend/src/views/DashboardPage/components/EnvComparisonRow/EnvComparisonRow.tsx index 8dbe32bf3..acadf600d 100644 --- a/frontend/src/views/DashboardPage/components/EnvComparisonRow/EnvComparisonRow.tsx +++ b/frontend/src/views/DashboardPage/components/EnvComparisonRow/EnvComparisonRow.tsx @@ -1,27 +1,25 @@ /* eslint-disable react/jsx-no-useless-fragment */ -import { SyntheticEvent, useRef } from 'react'; +import { SyntheticEvent, useRef, useState } from 'react'; import { useFormContext, useWatch } from 'react-hook-form'; -import { faCircle } from '@fortawesome/free-solid-svg-icons'; +import { faCircle, faEye, faEyeSlash } from '@fortawesome/free-solid-svg-icons'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import guidGenerator from '@app/components/utilities/randomId'; -import { Input } from '@app/components/v2'; -import { FormData, SecretActionType } from '../../DashboardPage.utils'; +import { FormData } from '../../DashboardPage.utils'; type Props = { index: number; secrets: any[] | undefined; // permission and external state's that decided to hide or show isReadOnly?: boolean; - isAddOnly?: boolean; isSecretValueHidden: boolean; userAvailableEnvs?: any[]; }; const REGEX = /([$]{.*?})/g; -const DashboardInput = ({ isOverridden, isSecretValueHidden, isAddOnly, isReadOnly, secret, shouldBeBlockedInAddOnly, index }: { isOverridden: boolean, isSecretValueHidden: boolean, isAddOnly?: boolean, isReadOnly?: boolean, secret: any, shouldBeBlockedInAddOnly?: boolean, index: number } ): JSX.Element => { +const DashboardInput = ({ isOverridden, isSecretValueHidden, isReadOnly, secret, index }: { isOverridden: boolean, isSecretValueHidden: boolean, isReadOnly?: boolean, secret: any, index: number } ): JSX.Element => { const ref = useRef(null); const syncScroll = (e: SyntheticEvent) => { if (ref.current === null) return; @@ -29,20 +27,19 @@ const DashboardInput = ({ isOverridden, isSecretValueHidden, isAddOnly, isReadOn ref.current.scrollTop = e.currentTarget.scrollTop; ref.current.scrollLeft = e.currentTarget.scrollLeft; }; - console.log(33333333, secret) - return + return
- {(isOverridden ? secret.valueOverride : secret?.value || '-')?.split('').length === 0 && EMPTY} - {(isOverridden ? secret.valueOverride : secret?.value || '-')?.split(REGEX).map((word: string) => { + {(secret?.value || secret?.value === '') && (isOverridden ? secret.valueOverride : secret?.value)?.split('').length === 0 && EMPTY} + {(secret?.value || secret?.value === '') && (isOverridden ? secret.valueOverride : secret?.value)?.split(REGEX).map((word: string) => { if (word.match(REGEX) !== null) { return ( - + {word.slice(0, 2)} {word.slice(2, word.length - 1)} @@ -81,18 +78,19 @@ const DashboardInput = ({ isOverridden, isSecretValueHidden, isAddOnly, isReadOn ); })} + {!(secret?.value || secret?.value === '') && missing}
{(isSecretValueHidden && secret?.value) && (
- {(isOverridden ? secret.valueOverride : secret?.value || '-')?.split('').map(() => ( + {(isOverridden ? secret.valueOverride : secret?.value || '')?.split('').map(() => ( ))} - {(isOverridden ? secret.valueOverride : secret?.value || '-')?.split('').length === 0 && EMPTY} + {(isOverridden ? secret.valueOverride : secret?.value || '')?.split('').length === 0 && EMPTY}
)} @@ -105,48 +103,28 @@ export const EnvComparisonRow = ({ secrets, isSecretValueHidden, isReadOnly, - isAddOnly, userAvailableEnvs }: Props): JSX.Element => { const { // register, setValue, control } = useFormContext(); - console.log(1282828822, userAvailableEnvs) - console.log('index', index) // to get details on a secret const secret = useWatch({ name: `secrets.${index}`, control }); - // when secret is override by personal values - const isOverridden = - secret.overrideAction === SecretActionType.Created || - secret.overrideAction === SecretActionType.Modified; - - const isCreatedSecret = !secret?._id; - const shouldBeBlockedInAddOnly = !isCreatedSecret && isAddOnly; - console.log(893892749827097, secrets) + const [areValuesHiddenThisRow, setAreValuesHiddenThisRow] = useState(true); return ( - -
{index + 1}
- -
- -
+ +
{index + 1}
+ +
{secret?.key || ''}
+ {userAvailableEnvs?.map(env => { - return <> - -
{0}
- - sec.env === env.slug)[0]} shouldBeBlockedInAddOnly={shouldBeBlockedInAddOnly} index={index} /> - + return sec.env === env.slug)[0]} index={index} /> })} ); diff --git a/frontend/src/views/DashboardPage/components/SecretInputRow/SecretInputRow.tsx b/frontend/src/views/DashboardPage/components/SecretInputRow/SecretInputRow.tsx index 850e8866c..f0b2cd092 100644 --- a/frontend/src/views/DashboardPage/components/SecretInputRow/SecretInputRow.tsx +++ b/frontend/src/views/DashboardPage/components/SecretInputRow/SecretInputRow.tsx @@ -156,7 +156,7 @@ export const SecretInputRow = ({ } return ( - +
{index + 1}
{ if (word.match(REGEX) !== null) { return ( - + {word.slice(0, 2)} {word.slice(2, word.length - 1)} diff --git a/frontend/src/views/Settings/ProjectSettingsPage/components/SecretTagsSection/SecretTagsSection.tsx b/frontend/src/views/Settings/ProjectSettingsPage/components/SecretTagsSection/SecretTagsSection.tsx index 98019e0ad..cbfc1aeed 100644 --- a/frontend/src/views/Settings/ProjectSettingsPage/components/SecretTagsSection/SecretTagsSection.tsx +++ b/frontend/src/views/Settings/ProjectSettingsPage/components/SecretTagsSection/SecretTagsSection.tsx @@ -64,7 +64,6 @@ export const SecretTagsSection = ({ }); const onFormSubmit = async (data: CreateWsTag) => { - console.log(19191, data); await onCreateTag(data); handlePopUpClose('CreateSecretTag'); }; From cfc9470a6f4a2bb09e7fe5a5a8a2d1d82ea67078 Mon Sep 17 00:00:00 2001 From: Vladyslav Matsiiako Date: Fri, 14 Apr 2023 19:33:25 -0700 Subject: [PATCH 5/7] Fixed merge conflicts --- frontend/src/pages/dashboard/[id].tsx | 63 ++++++++++----------------- 1 file changed, 24 insertions(+), 39 deletions(-) diff --git a/frontend/src/pages/dashboard/[id].tsx b/frontend/src/pages/dashboard/[id].tsx index 0e77bd99c..8aa480c81 100644 --- a/frontend/src/pages/dashboard/[id].tsx +++ b/frontend/src/pages/dashboard/[id].tsx @@ -2,7 +2,6 @@ import { UIEvent, useCallback, useEffect, useRef, useState } from 'react'; import Head from 'next/head'; import Image from 'next/image'; -import queryString from 'query-string'; import { useRouter } from 'next/router'; import { useTranslation } from 'next-i18next'; import { @@ -20,9 +19,9 @@ import { } from '@fortawesome/free-solid-svg-icons'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { Tag } from 'public/data/frequentInterfaces'; +import queryString from 'query-string'; import Button from '@app/components/basic/buttons/Button'; -import ListBox from '@app/components/basic/Listbox'; import BottonRightPopup from '@app/components/basic/popups/BottomRightPopup'; import { useNotificationContext } from '@app/components/context/Notifications/NotificationProvider'; import ConfirmEnvOverwriteModal from '@app/components/dashboard/ConfirmEnvOverwriteModal'; @@ -36,14 +35,14 @@ import guidGenerator from '@app/components/utilities/randomId'; import encryptSecrets from '@app/components/utilities/secrets/encryptSecrets'; import getSecretsForProject from '@app/components/utilities/secrets/getSecretsForProject'; import { getTranslatedServerSideProps } from '@app/components/utilities/withTranslateProps'; -import { DashboardPage } from '@app/views/DashboardPage'; -import { DashboardEnvOverview } from '@app/views/DashboardPage/DashboardEnvOverview'; import { IconButton } from '@app/components/v2'; import { leaveConfirmDefaultMessage } from '@app/const'; import getProjectSercetSnapshotsCount from '@app/ee/api/secrets/GetProjectSercetSnapshotsCount'; import performSecretRollback from '@app/ee/api/secrets/PerformSecretRollback'; import PITRecoverySidebar from '@app/ee/components/PITRecoverySidebar'; import { useLeaveConfirm } from '@app/hooks'; +// import { DashboardPage } from '@app/views/DashboardPage'; +import { DashboardEnvOverview } from '@app/views/DashboardPage/DashboardEnvOverview'; // import addSecrets from '../api/files/AddSecrets'; // import deleteSecrets from '../api/files/DeleteSecrets'; @@ -150,7 +149,6 @@ function findDuplicates(arr: any[]) { export default function Dashboard() { const [data, setData] = useState(); const [initialData, setInitialData] = useState([]); - const router = useRouter(); const [blurred, setBlurred] = useState(true); const [isKeyAvailable, setIsKeyAvailable] = useState(true); const [isNew, setIsNew] = useState(false); @@ -175,7 +173,7 @@ export default function Dashboard() { const { createNotification } = useNotificationContext(); const router = useRouter(); - const env = queryString.parse(router.asPath.split('?')[1])?.env; + const envInURL = queryString.parse(router.asPath.split('?')[1])?.env; const workspaceId = router.query.id as string; const [workspaceEnvs, setWorkspaceEnvs] = useState([]); @@ -789,13 +787,13 @@ export default function Dashboard() { deleteRow({ ids, secretName }); }; - const handleOnEnvironmentChange = (envName: string) => { + const handleOnEnvironmentChange = (envSlug: string) => { if (hasUnsavedChanges) { if (!window.confirm(leaveConfirmDefaultMessage)) return; } const selectedWorkspaceEnv = workspaceEnvs.find( - ({ name }: { name: string }) => envName === name + ({ slug }: { slug: string }) => envSlug === slug ) || { name: 'unknown', slug: 'unknown', @@ -803,6 +801,8 @@ export default function Dashboard() { isReadDenied: false }; + console.log(124, envSlug, selectedWorkspaceEnv) + if (selectedWorkspaceEnv) { if (snapshotData) setSelectedSnapshotEnv(selectedWorkspaceEnv); else setSelectedEnv(selectedWorkspaceEnv); @@ -815,10 +815,10 @@ export default function Dashboard() { }) }; - return <> - {!env + return
+ {!envInURL ? - : data ? ( + : (data ? (
{t('common:head-title', { title: t('dashboard:title') })} @@ -844,7 +844,13 @@ export default function Dashboard() { } />
- + envir.slug === envInURL)[0].name || ''} + isProjectRelated + userAvailableEnvs={workspaceEnvs} + onEnvChange={handleOnEnvironmentChange} + /> {checkDocsPopUpVisible && (
@@ -969,20 +968,7 @@ export default function Dashboard() {
{(snapshotData || data?.length !== 0) && selectedEnv && ( <> - {!snapshotData ? ( - name)} - onChange={handleOnEnvironmentChange} - /> - ) : ( - name)} - onChange={handleOnEnvironmentChange} - /> - )} -
+
infisical loading indicator @@ -1232,11 +1218,10 @@ export default function Dashboard() {
) : ( -
+
loading animation
- ) - + ))}
} Dashboard.requireAuth = true; From 56c35293ebf525c5600b87d99dde4eadde7c0a0e Mon Sep 17 00:00:00 2001 From: Vladyslav Matsiiako Date: Fri, 14 Apr 2023 19:39:30 -0700 Subject: [PATCH 6/7] hotfix: choose env when opening a dashboard link --- frontend/src/layouts/AppLayout/AppLayout.tsx | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/frontend/src/layouts/AppLayout/AppLayout.tsx b/frontend/src/layouts/AppLayout/AppLayout.tsx index 3e3641a0a..85828c3c5 100644 --- a/frontend/src/layouts/AppLayout/AppLayout.tsx +++ b/frontend/src/layouts/AppLayout/AppLayout.tsx @@ -20,6 +20,7 @@ import { } from '@fortawesome/free-solid-svg-icons'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { yupResolver } from '@hookform/resolvers/yup'; +import queryString from 'query-string'; import * as yup from 'yup'; import { useNotificationContext } from '@app/components/context/Notifications/NotificationProvider'; @@ -158,7 +159,10 @@ export const AppLayout = ({ children }: LayoutProps) => { .map((workspace: { _id: string }) => workspace._id) .includes(intendedWorkspaceId) ) { - router.push(`/dashboard/${userWorkspaces[0]._id}`); + const { env } = queryString.parse(router.asPath.split('?')[1]); + if (!env) { + router.push(`/dashboard/${userWorkspaces[0]._id}`); + } } else { setWorkspaceMapping( Object.fromEntries( From c1f39b866f0c70c85f3ae364949c8b3b226ec4dd Mon Sep 17 00:00:00 2001 From: Sheen Date: Sat, 15 Apr 2023 15:50:06 +0800 Subject: [PATCH 7/7] [Feature][Sheen] added never expire service token --- backend/src/controllers/v2/serviceTokenDataController.ts | 7 +++++-- .../ServiceTokenSection/ServiceTokenSection.tsx | 9 +++++---- 2 files changed, 10 insertions(+), 6 deletions(-) diff --git a/backend/src/controllers/v2/serviceTokenDataController.ts b/backend/src/controllers/v2/serviceTokenDataController.ts index 6b3d24dfb..328749472 100644 --- a/backend/src/controllers/v2/serviceTokenDataController.ts +++ b/backend/src/controllers/v2/serviceTokenDataController.ts @@ -75,8 +75,11 @@ export const createServiceTokenData = async (req: Request, res: Response) => { const secret = crypto.randomBytes(16).toString('hex'); const secretHash = await bcrypt.hash(secret, getSaltRounds()); - const expiresAt = new Date(); - expiresAt.setSeconds(expiresAt.getSeconds() + expiresIn); + let expiresAt; + if (!!expiresIn) { + expiresAt = new Date() + expiresAt.setSeconds(expiresAt.getSeconds() + expiresIn); + } let user, serviceAccount; diff --git a/frontend/src/views/Settings/ProjectSettingsPage/components/ServiceTokenSection/ServiceTokenSection.tsx b/frontend/src/views/Settings/ProjectSettingsPage/components/ServiceTokenSection/ServiceTokenSection.tsx index b857a3224..10c95ee55 100644 --- a/frontend/src/views/Settings/ProjectSettingsPage/components/ServiceTokenSection/ServiceTokenSection.tsx +++ b/frontend/src/views/Settings/ProjectSettingsPage/components/ServiceTokenSection/ServiceTokenSection.tsx @@ -37,13 +37,14 @@ const apiTokenExpiry = [ { label: '7 Days', value: 604800 }, { label: '1 Month', value: 2592000 }, { label: '6 months', value: 15552000 }, - { label: '12 months', value: 31104000 } + { label: '12 months', value: 31104000 }, + { label: 'Never', value: null }, ]; const createServiceTokenSchema = yup.object({ name: yup.string().required().label('Service Token Name'), environment: yup.string().required().label('Environment'), - expiresIn: yup.string().required().label('Service Token Expiration'), + expiresIn: yup.string().optional().label('Service Token Expiration'), permissions: yup.object().shape({ read: yup.boolean().required(), write: yup.boolean().required() @@ -213,7 +214,7 @@ export const ServiceTokenSection = ({ className="w-full" > {apiTokenExpiry.map(({ label, value }) => ( - + {label} ))} @@ -332,7 +333,7 @@ export const ServiceTokenSection = ({ {row.name} {row.environment} - {new Date(row.expiresAt).toUTCString()} + {row.expiresAt && new Date(row.expiresAt).toUTCString()}