diff --git a/backend/src/ee/services/dynamic-secret/providers/totp.ts b/backend/src/ee/services/dynamic-secret/providers/totp.ts index 4dec9abd1..4e3ab6eb2 100644 --- a/backend/src/ee/services/dynamic-secret/providers/totp.ts +++ b/backend/src/ee/services/dynamic-secret/providers/totp.ts @@ -65,7 +65,10 @@ export const TotpProvider = (): TDynamicProviderFns => { authenticatorInstance.options = { step: period }; } - return { entityId, data: { TOTP: authenticatorInstance.generate(secret) } }; + return { + entityId, + data: { TOTP: authenticatorInstance.generate(secret), TIME_REMAINING: authenticatorInstance.timeRemaining() } + }; }; const revoke = async (_inputs: unknown, entityId: string) => { diff --git a/frontend/src/views/SecretMainPage/components/DynamicSecretListView/CreateDynamicSecretLease.tsx b/frontend/src/views/SecretMainPage/components/DynamicSecretListView/CreateDynamicSecretLease.tsx index 69064d03a..011cf74f9 100644 --- a/frontend/src/views/SecretMainPage/components/DynamicSecretListView/CreateDynamicSecretLease.tsx +++ b/frontend/src/views/SecretMainPage/components/DynamicSecretListView/CreateDynamicSecretLease.tsx @@ -1,4 +1,4 @@ -import { ReactNode, useEffect } from "react"; +import { ReactNode, useEffect, useState } from "react"; import { Controller, useForm } from "react-hook-form"; import { faCheck, faCopy } from "@fortawesome/free-solid-svg-icons"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; @@ -62,7 +62,60 @@ const OutputDisplay = ({ ); }; -const renderOutputForm = (provider: DynamicSecretProviders, data: unknown) => { +const TotpOutputDisplay = ({ + totp, + remainingSeconds, + triggerLeaseCreation +}: { + totp: string; + remainingSeconds: number; + triggerLeaseCreation: (details: { ttl?: string }) => Promise; +}) => { + const [remainingTime, setRemainingTime] = useState(remainingSeconds); + const [shouldShowRegenerate, setShouldShowRegenerate] = useToggle(false); + + useEffect(() => { + setRemainingTime(remainingSeconds); + setShouldShowRegenerate.off(); + + // Set up countdown interval + const intervalId = setInterval(() => { + setRemainingTime((prevTime) => { + if (prevTime <= 1) { + clearInterval(intervalId); + setShouldShowRegenerate.on(); + return 0; + } + return prevTime - 1; + }); + }, 1000); + + // Cleanup interval on unmount or when totp changes + return () => clearInterval(intervalId); + }, [totp, remainingSeconds]); + + return ( +
+ + {remainingTime > 0 && ( +
+ Valid for {remainingTime} {remainingTime > 1 ? "seconds" : "second"} +
+ )} + {shouldShowRegenerate && ( + + )} +
+ ); +}; + +const renderOutputForm = ( + provider: DynamicSecretProviders, + data: unknown, + triggerLeaseCreation: (details: { ttl?: string }) => Promise +) => { if ( provider === DynamicSecretProviders.SqlDatabase || provider === DynamicSecretProviders.Cassandra || @@ -251,14 +304,17 @@ const renderOutputForm = (provider: DynamicSecretProviders, data: unknown) => { } if (provider === DynamicSecretProviders.Totp) { - const { TOTP } = data as { + const { TOTP, TIME_REMAINING } = data as { TOTP: string; + TIME_REMAINING: number; }; return ( -
- -
+ ); } @@ -391,7 +447,11 @@ export const CreateDynamicSecretLease = ({ animate={{ opacity: 1, translateX: 0 }} exit={{ opacity: 0, translateX: 30 }} > - {renderOutputForm(provider, createDynamicSecretLease.data?.data)} + {renderOutputForm( + provider, + createDynamicSecretLease.data?.data, + handleDynamicSecretLeaseCreate + )} )}