@@ -350,7 +468,11 @@ export const CreateDynamicSecretLease = ({
animate={{ opacity: 1, translateX: 0 }}
exit={{ opacity: 0, translateX: 30 }}
>
- {renderOutputForm(provider, createDynamicSecretLease.data?.data)}
+ {renderOutputForm(
+ provider,
+ createDynamicSecretLease.data?.data,
+ handleLeaseRegeneration
+ )}
)}
diff --git a/frontend/src/views/SecretMainPage/components/DynamicSecretListView/DynamicSecretListView.tsx b/frontend/src/views/SecretMainPage/components/DynamicSecretListView/DynamicSecretListView.tsx
index 8953ab2bc..22057a90c 100644
--- a/frontend/src/views/SecretMainPage/components/DynamicSecretListView/DynamicSecretListView.tsx
+++ b/frontend/src/views/SecretMainPage/components/DynamicSecretListView/DynamicSecretListView.tsx
@@ -101,10 +101,20 @@ export const DynamicSecretListView = ({
role="button"
tabIndex={0}
onKeyDown={(evt) => {
+ // no lease view for TOTP because it's irrelevant
+ if (secret.type === DynamicSecretProviders.Totp) {
+ return;
+ }
+
if (evt.key === "Enter" && !isRevoking)
handlePopUpOpen("dynamicSecretLeases", secret.id);
}}
onClick={() => {
+ // no lease view for TOTP because it's irrelevant
+ if (secret.type === DynamicSecretProviders.Totp) {
+ return;
+ }
+
if (!isRevoking) {
handlePopUpOpen("dynamicSecretLeases", secret.id);
}
diff --git a/frontend/src/views/SecretMainPage/components/DynamicSecretListView/EditDynamicSecretForm/EditDynamicSecretForm.tsx b/frontend/src/views/SecretMainPage/components/DynamicSecretListView/EditDynamicSecretForm/EditDynamicSecretForm.tsx
index 7c37ed7b2..d260b9f40 100644
--- a/frontend/src/views/SecretMainPage/components/DynamicSecretListView/EditDynamicSecretForm/EditDynamicSecretForm.tsx
+++ b/frontend/src/views/SecretMainPage/components/DynamicSecretListView/EditDynamicSecretForm/EditDynamicSecretForm.tsx
@@ -17,6 +17,7 @@ import { EditDynamicSecretRedisProviderForm } from "./EditDynamicSecretRedisProv
import { EditDynamicSecretSapHanaForm } from "./EditDynamicSecretSapHanaForm";
import { EditDynamicSecretSnowflakeForm } from "./EditDynamicSecretSnowflakeForm";
import { EditDynamicSecretSqlProviderForm } from "./EditDynamicSecretSqlProviderForm";
+import { EditDynamicSecretTotpForm } from "./EditDynamicSecretTotpForm";
type Props = {
onClose: () => void;
@@ -276,6 +277,23 @@ export const EditDynamicSecretForm = ({
/>
)}
+ {dynamicSecretDetails?.type === DynamicSecretProviders.Totp && (
+
+
+
+ )}
);
};
diff --git a/frontend/src/views/SecretMainPage/components/DynamicSecretListView/EditDynamicSecretForm/EditDynamicSecretTotpForm.tsx b/frontend/src/views/SecretMainPage/components/DynamicSecretListView/EditDynamicSecretForm/EditDynamicSecretTotpForm.tsx
new file mode 100644
index 000000000..1e2e01c82
--- /dev/null
+++ b/frontend/src/views/SecretMainPage/components/DynamicSecretListView/EditDynamicSecretForm/EditDynamicSecretTotpForm.tsx
@@ -0,0 +1,318 @@
+import { Controller, useForm } from "react-hook-form";
+import Link from "next/link";
+import { faArrowUpRightFromSquare, faBookOpen } from "@fortawesome/free-solid-svg-icons";
+import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
+import { zodResolver } from "@hookform/resolvers/zod";
+import { z } from "zod";
+
+import { createNotification } from "@app/components/notifications";
+import { Button, FormControl, Input, Select, SelectItem } from "@app/components/v2";
+import { useUpdateDynamicSecret } from "@app/hooks/api";
+import { TDynamicSecret } from "@app/hooks/api/dynamicSecret/types";
+
+enum ConfigType {
+ URL = "url",
+ MANUAL = "manual"
+}
+
+enum TotpAlgorithm {
+ SHA1 = "sha1",
+ SHA256 = "sha256",
+ SHA512 = "sha512"
+}
+
+const formSchema = z.object({
+ inputs: z
+ .discriminatedUnion("configType", [
+ z.object({
+ configType: z.literal(ConfigType.URL),
+ url: z
+ .string()
+ .url()
+ .trim()
+ .min(1)
+ .refine((val) => {
+ const urlObj = new URL(val);
+ const secret = urlObj.searchParams.get("secret");
+
+ return Boolean(secret);
+ }, "OTP URL must contain secret field")
+ }),
+ z.object({
+ configType: z.literal(ConfigType.MANUAL),
+ secret: z
+ .string()
+ .trim()
+ .min(1)
+ .transform((val) => val.replace(/\s+/g, "")),
+ period: z.number().optional(),
+ algorithm: z.nativeEnum(TotpAlgorithm).optional(),
+ digits: z.number().optional()
+ })
+ ])
+ .optional(),
+ newName: z
+ .string()
+ .trim()
+ .min(1)
+ .refine((val) => val.toLowerCase() === val, "Must be lowercase")
+});
+type TForm = z.infer
;
+
+type Props = {
+ onClose: () => void;
+ dynamicSecret: TDynamicSecret & { inputs: unknown };
+ secretPath: string;
+ projectSlug: string;
+ environment: string;
+};
+
+export const EditDynamicSecretTotpForm = ({
+ onClose,
+ dynamicSecret,
+ environment,
+ secretPath,
+ projectSlug
+}: Props) => {
+ const {
+ control,
+ formState: { isSubmitting },
+ watch,
+ handleSubmit
+ } = useForm({
+ resolver: zodResolver(formSchema),
+ values: {
+ newName: dynamicSecret.name,
+ inputs: dynamicSecret.inputs as TForm["inputs"]
+ }
+ });
+
+ const selectedConfigType = watch("inputs.configType");
+ const updateDynamicSecret = useUpdateDynamicSecret();
+
+ const handleUpdateDynamicSecret = async ({ inputs, newName }: TForm) => {
+ // wait till previous request is finished
+ if (updateDynamicSecret.isLoading) return;
+ try {
+ await updateDynamicSecret.mutateAsync({
+ name: dynamicSecret.name,
+ path: secretPath,
+ projectSlug,
+ environmentSlug: environment,
+ data: {
+ inputs,
+ newName: newName === dynamicSecret.name ? undefined : newName
+ }
+ });
+ onClose();
+ createNotification({
+ type: "success",
+ text: "Successfully updated dynamic secret"
+ });
+ } catch (err) {
+ createNotification({
+ type: "error",
+ text: err instanceof Error ? err.message : "Failed to update dynamic secret"
+ });
+ }
+ };
+
+ return (
+
+ );
+};