diff --git a/backend/src/ee/services/dynamic-secret/providers/index.ts b/backend/src/ee/services/dynamic-secret/providers/index.ts index f70985379..e51462be6 100644 --- a/backend/src/ee/services/dynamic-secret/providers/index.ts +++ b/backend/src/ee/services/dynamic-secret/providers/index.ts @@ -13,6 +13,7 @@ import { RabbitMqProvider } from "./rabbit-mq"; import { RedisDatabaseProvider } from "./redis"; import { SapHanaProvider } from "./sap-hana"; import { SqlDatabaseProvider } from "./sql-database"; +import { TotpProvider } from "./totp"; export const buildDynamicSecretProviders = () => ({ [DynamicSecretProviders.SqlDatabase]: SqlDatabaseProvider(), @@ -27,5 +28,6 @@ export const buildDynamicSecretProviders = () => ({ [DynamicSecretProviders.AzureEntraID]: AzureEntraIDProvider(), [DynamicSecretProviders.Ldap]: LdapProvider(), [DynamicSecretProviders.SapHana]: SapHanaProvider(), - [DynamicSecretProviders.Snowflake]: SnowflakeProvider() + [DynamicSecretProviders.Snowflake]: SnowflakeProvider(), + [DynamicSecretProviders.Totp]: TotpProvider() }); diff --git a/backend/src/ee/services/dynamic-secret/providers/models.ts b/backend/src/ee/services/dynamic-secret/providers/models.ts index d98215fd4..d803aab5b 100644 --- a/backend/src/ee/services/dynamic-secret/providers/models.ts +++ b/backend/src/ee/services/dynamic-secret/providers/models.ts @@ -17,6 +17,17 @@ export enum LdapCredentialType { Static = "static" } +export enum TotpConfigType { + URL = "url", + MANUAL = "manual" +} + +export enum TotpAlgorithm { + SHA1 = "sha1", + SHA256 = "sha256", + SHA512 = "sha512" +} + export const DynamicSecretRedisDBSchema = z.object({ host: z.string().trim().toLowerCase(), port: z.number(), @@ -221,6 +232,34 @@ export const LdapSchema = z.union([ }) ]); +export const DynamicSecretTotpSchema = z.discriminatedUnion("configType", [ + z.object({ + configType: z.literal(TotpConfigType.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(TotpConfigType.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() + }) +]); + export enum DynamicSecretProviders { SqlDatabase = "sql-database", Cassandra = "cassandra", @@ -234,7 +273,8 @@ export enum DynamicSecretProviders { AzureEntraID = "azure-entra-id", Ldap = "ldap", SapHana = "sap-hana", - Snowflake = "snowflake" + Snowflake = "snowflake", + Totp = "totp" } export const DynamicSecretProviderSchema = z.discriminatedUnion("type", [ @@ -250,7 +290,8 @@ export const DynamicSecretProviderSchema = z.discriminatedUnion("type", [ z.object({ type: z.literal(DynamicSecretProviders.RabbitMq), inputs: DynamicSecretRabbitMqSchema }), z.object({ type: z.literal(DynamicSecretProviders.AzureEntraID), inputs: AzureEntraIDSchema }), z.object({ type: z.literal(DynamicSecretProviders.Ldap), inputs: LdapSchema }), - z.object({ type: z.literal(DynamicSecretProviders.Snowflake), inputs: DynamicSecretSnowflakeSchema }) + z.object({ type: z.literal(DynamicSecretProviders.Snowflake), inputs: DynamicSecretSnowflakeSchema }), + z.object({ type: z.literal(DynamicSecretProviders.Totp), inputs: DynamicSecretTotpSchema }) ]); export type TDynamicProviderFns = { diff --git a/backend/src/ee/services/dynamic-secret/providers/totp.ts b/backend/src/ee/services/dynamic-secret/providers/totp.ts new file mode 100644 index 000000000..4e3ab6eb2 --- /dev/null +++ b/backend/src/ee/services/dynamic-secret/providers/totp.ts @@ -0,0 +1,92 @@ +import { authenticator } from "otplib"; +import { HashAlgorithms } from "otplib/core"; + +import { BadRequestError } from "@app/lib/errors"; +import { alphaNumericNanoId } from "@app/lib/nanoid"; + +import { DynamicSecretTotpSchema, TDynamicProviderFns, TotpConfigType } from "./models"; + +export const TotpProvider = (): TDynamicProviderFns => { + const validateProviderInputs = async (inputs: unknown) => { + const providerInputs = await DynamicSecretTotpSchema.parseAsync(inputs); + + return providerInputs; + }; + + const validateConnection = async () => { + return true; + }; + + const create = async (inputs: unknown) => { + const providerInputs = await validateProviderInputs(inputs); + + const entityId = alphaNumericNanoId(32); + const authenticatorInstance = authenticator.clone(); + + let secret: string; + let period: number | null | undefined; + let digits: number | null | undefined; + let algorithm: HashAlgorithms | null | undefined; + + if (providerInputs.configType === TotpConfigType.URL) { + const urlObj = new URL(providerInputs.url); + secret = urlObj.searchParams.get("secret") as string; + const periodFromUrl = urlObj.searchParams.get("period"); + const digitsFromUrl = urlObj.searchParams.get("digits"); + const algorithmFromUrl = urlObj.searchParams.get("algorithm"); + + if (periodFromUrl) { + period = +periodFromUrl; + } + + if (digitsFromUrl) { + digits = +digitsFromUrl; + } + + if (algorithmFromUrl) { + algorithm = algorithmFromUrl.toLowerCase() as HashAlgorithms; + } + } else { + secret = providerInputs.secret; + period = providerInputs.period; + digits = providerInputs.digits; + algorithm = providerInputs.algorithm as unknown as HashAlgorithms; + } + + if (digits) { + authenticatorInstance.options = { digits }; + } + + if (algorithm) { + authenticatorInstance.options = { algorithm }; + } + + if (period) { + authenticatorInstance.options = { step: period }; + } + + return { + entityId, + data: { TOTP: authenticatorInstance.generate(secret), TIME_REMAINING: authenticatorInstance.timeRemaining() } + }; + }; + + const revoke = async (_inputs: unknown, entityId: string) => { + return { entityId }; + }; + + // eslint-disable-next-line @typescript-eslint/no-unused-vars + const renew = async (_inputs: unknown, _entityId: string) => { + throw new BadRequestError({ + message: "Lease renewal is not supported for TOTPs" + }); + }; + + return { + validateProviderInputs, + validateConnection, + create, + revoke, + renew + }; +}; diff --git a/docs/documentation/platform/dynamic-secrets/aws-elasticache.mdx b/docs/documentation/platform/dynamic-secrets/aws-elasticache.mdx index 225b884cb..2cf4edc0e 100644 --- a/docs/documentation/platform/dynamic-secrets/aws-elasticache.mdx +++ b/docs/documentation/platform/dynamic-secrets/aws-elasticache.mdx @@ -69,7 +69,7 @@ The Infisical AWS ElastiCache dynamic secret allows you to generate AWS ElastiCa - Default time-to-live for a generated secret (it is possible to modify this value when a secret is generate) + Default time-to-live for a generated secret (it is possible to modify this value after a secret is generated) @@ -131,12 +131,12 @@ The Infisical AWS ElastiCache dynamic secret allows you to generate AWS ElastiCa ## Audit or Revoke Leases Once you have created one or more leases, you will be able to access them by clicking on the respective dynamic secret item on the dashboard. -This will allow you see the expiration time of the lease or delete a lease before it's set time to live. +This will allow you to see the expiration time of the lease or delete a lease before it's set time to live. ![Provision Lease](/images/platform/dynamic-secrets/lease-data.png) ## Renew Leases -To extend the life of the generated dynamic secret leases past its initial time to live, simply click on the **Renew** as illustrated below. +To extend the life of the generated dynamic secret leases past its initial time to live, simply click on the **Renew** button as illustrated below. ![Provision Lease](/images/platform/dynamic-secrets/dynamic-secret-lease-renew.png) diff --git a/docs/documentation/platform/dynamic-secrets/aws-iam.mdx b/docs/documentation/platform/dynamic-secrets/aws-iam.mdx index 7a3976e0f..730e2b287 100644 --- a/docs/documentation/platform/dynamic-secrets/aws-iam.mdx +++ b/docs/documentation/platform/dynamic-secrets/aws-iam.mdx @@ -66,7 +66,7 @@ Replace **\** with your AWS account id and **\** w - Default time-to-live for a generated secret (it is possible to modify this value when a secret is generate) + Default time-to-live for a generated secret (it is possible to modify this value after a secret is generated) @@ -138,12 +138,12 @@ Replace **\** with your AWS account id and **\** w ## Audit or Revoke Leases Once you have created one or more leases, you will be able to access them by clicking on the respective dynamic secret item on the dashboard. -This will allow you see the lease details and delete the lease ahead of its expiration time. +This will allow you to see the lease details and delete the lease ahead of its expiration time. ![Provision Lease](/images/platform/dynamic-secrets/lease-data.png) ## Renew Leases -To extend the life of the generated dynamic secret lease past its initial time to live, simply click on the **Renew** as illustrated below. +To extend the life of the generated dynamic secret lease past its initial time to live, simply click on the **Renew** button as illustrated below. ![Provision Lease](/images/platform/dynamic-secrets/dynamic-secret-lease-renew.png) diff --git a/docs/documentation/platform/dynamic-secrets/azure-entra-id.mdx b/docs/documentation/platform/dynamic-secrets/azure-entra-id.mdx index 8a71772b1..515efabeb 100644 --- a/docs/documentation/platform/dynamic-secrets/azure-entra-id.mdx +++ b/docs/documentation/platform/dynamic-secrets/azure-entra-id.mdx @@ -98,7 +98,7 @@ Click on Add assignments. Search for the application name you created and select - Default time-to-live for a generated secret (it is possible to modify this value when a secret is generate) + Default time-to-live for a generated secret (it is possible to modify this value after a secret is generated) @@ -151,12 +151,12 @@ Click on Add assignments. Search for the application name you created and select ## Audit or Revoke Leases Once you have created one or more leases, you will be able to access them by clicking on the respective dynamic secret item on the dashboard. -This will allow you see the expiration time of the lease or delete a lease before it's set time to live. +This will allow you to see the expiration time of the lease or delete a lease before it's set time to live. ![Provision Lease](/images/platform/dynamic-secrets/lease-data.png) ## Renew Leases -To extend the life of the generated dynamic secret leases past its initial time to live, simply click on the **Renew** as illustrated below. +To extend the life of the generated dynamic secret leases past its initial time to live, simply click on the **Renew** button as illustrated below. ![Provision Lease](/images/platform/dynamic-secrets/dynamic-secret-lease-renew.png) diff --git a/docs/documentation/platform/dynamic-secrets/cassandra.mdx b/docs/documentation/platform/dynamic-secrets/cassandra.mdx index fd46c8288..e7ec4f69d 100644 --- a/docs/documentation/platform/dynamic-secrets/cassandra.mdx +++ b/docs/documentation/platform/dynamic-secrets/cassandra.mdx @@ -39,7 +39,7 @@ The above configuration allows user creation and granting permissions. - Default time-to-live for a generated secret (it is possible to modify this value when a secret is generate) + Default time-to-live for a generated secret (it is possible to modify this value after a secret is generated) @@ -116,12 +116,12 @@ The above configuration allows user creation and granting permissions. ## Audit or Revoke Leases Once you have created one or more leases, you will be able to access them by clicking on the respective dynamic secret item on the dashboard. -This will allow you see the lease details and delete the lease ahead of its expiration time. +This will allow you to see the lease details and delete the lease ahead of its expiration time. ![Provision Lease](/images/platform/dynamic-secrets/lease-data.png) ## Renew Leases -To extend the life of the generated dynamic secret lease past its initial time to live, simply click on the **Renew** as illustrated below. +To extend the life of the generated dynamic secret lease past its initial time to live, simply click on the **Renew** button as illustrated below. ![Provision Lease](/images/platform/dynamic-secrets/dynamic-secret-lease-renew.png) diff --git a/docs/documentation/platform/dynamic-secrets/elastic-search.mdx b/docs/documentation/platform/dynamic-secrets/elastic-search.mdx index 0b2897790..0e1bc5104 100644 --- a/docs/documentation/platform/dynamic-secrets/elastic-search.mdx +++ b/docs/documentation/platform/dynamic-secrets/elastic-search.mdx @@ -34,7 +34,7 @@ The Infisical Elasticsearch dynamic secret allows you to generate Elasticsearch - Default time-to-live for a generated secret (it is possible to modify this value when a secret is generate) + Default time-to-live for a generated secret (it is possible to modify this value after a secret is generated) @@ -114,12 +114,12 @@ The Infisical Elasticsearch dynamic secret allows you to generate Elasticsearch ## Audit or Revoke Leases Once you have created one or more leases, you will be able to access them by clicking on the respective dynamic secret item on the dashboard. -This will allow you see the expiration time of the lease or delete a lease before it's set time to live. +This will allow you to see the expiration time of the lease or delete a lease before it's set time to live. ![Provision Lease](/images/platform/dynamic-secrets/lease-data.png) ## Renew Leases -To extend the life of the generated dynamic secret leases past its initial time to live, simply click on the **Renew** as illustrated below. +To extend the life of the generated dynamic secret leases past its initial time to live, simply click on the **Renew** button as illustrated below. ![Provision Lease](/images/platform/dynamic-secrets/dynamic-secret-lease-renew.png) diff --git a/docs/documentation/platform/dynamic-secrets/ldap.mdx b/docs/documentation/platform/dynamic-secrets/ldap.mdx index ac06a7576..a1731432c 100644 --- a/docs/documentation/platform/dynamic-secrets/ldap.mdx +++ b/docs/documentation/platform/dynamic-secrets/ldap.mdx @@ -31,7 +31,7 @@ The Infisical LDAP dynamic secret allows you to generate user credentials on dem - Default time-to-live for a generated secret (it is possible to modify this value when a secret is generate) + Default time-to-live for a generated secret (it is possible to modify this value after a secret is generated) @@ -171,7 +171,7 @@ The Infisical LDAP dynamic secret allows you to generate user credentials on dem - Default time-to-live for a generated secret (it is possible to modify this value when a secret is generate) + Default time-to-live for a generated secret (it is possible to modify this value after a secret is generated) diff --git a/docs/documentation/platform/dynamic-secrets/mongo-atlas.mdx b/docs/documentation/platform/dynamic-secrets/mongo-atlas.mdx index f9352f2e5..5eda1669e 100644 --- a/docs/documentation/platform/dynamic-secrets/mongo-atlas.mdx +++ b/docs/documentation/platform/dynamic-secrets/mongo-atlas.mdx @@ -30,7 +30,7 @@ Create a project scopped API Key with the required permission in your Mongo Atla - Default time-to-live for a generated secret (it is possible to modify this value when a secret is generate) + Default time-to-live for a generated secret (it is possible to modify this value after a secret is generated) @@ -101,12 +101,12 @@ Create a project scopped API Key with the required permission in your Mongo Atla ## Audit or Revoke Leases Once you have created one or more leases, you will be able to access them by clicking on the respective dynamic secret item on the dashboard. -This will allow you see the expiration time of the lease or delete a lease before it's set time to live. +This will allow you to see the expiration time of the lease or delete a lease before it's set time to live. ![Provision Lease](/images/platform/dynamic-secrets/lease-data.png) ## Renew Leases -To extend the life of the generated dynamic secret leases past its initial time to live, simply click on the **Renew** as illustrated below. +To extend the life of the generated dynamic secret leases past its initial time to live, simply click on the **Renew** button as illustrated below. ![Provision Lease](/images/platform/dynamic-secrets/dynamic-secret-lease-renew.png) diff --git a/docs/documentation/platform/dynamic-secrets/mongo-db.mdx b/docs/documentation/platform/dynamic-secrets/mongo-db.mdx index f34d578dc..ec384f7a9 100644 --- a/docs/documentation/platform/dynamic-secrets/mongo-db.mdx +++ b/docs/documentation/platform/dynamic-secrets/mongo-db.mdx @@ -31,7 +31,7 @@ Create a user with the required permission in your MongoDB instance. This user w - Default time-to-live for a generated secret (it is possible to modify this value when a secret is generate) + Default time-to-live for a generated secret (it is possible to modify this value after a secret is generated) @@ -103,12 +103,12 @@ Create a user with the required permission in your MongoDB instance. This user w ## Audit or Revoke Leases Once you have created one or more leases, you will be able to access them by clicking on the respective dynamic secret item on the dashboard. -This will allow you see the expiration time of the lease or delete a lease before it's set time to live. +This will allow you to see the expiration time of the lease or delete a lease before it's set time to live. ![Provision Lease](/images/platform/dynamic-secrets/lease-data.png) ## Renew Leases -To extend the life of the generated dynamic secret leases past its initial time to live, simply click on the **Renew** as illustrated below. +To extend the life of the generated dynamic secret leases past its initial time to live, simply click on the **Renew** button as illustrated below. ![Provision Lease](/images/platform/dynamic-secrets/dynamic-secret-lease-renew.png) diff --git a/docs/documentation/platform/dynamic-secrets/mssql.mdx b/docs/documentation/platform/dynamic-secrets/mssql.mdx index fb666adca..aca42c5c4 100644 --- a/docs/documentation/platform/dynamic-secrets/mssql.mdx +++ b/docs/documentation/platform/dynamic-secrets/mssql.mdx @@ -28,7 +28,7 @@ Create a user with the required permission in your SQL instance. This user will - Default time-to-live for a generated secret (it is possible to modify this value when a secret is generate) + Default time-to-live for a generated secret (it is possible to modify this value after a secret is generated) @@ -105,12 +105,12 @@ Create a user with the required permission in your SQL instance. This user will ## Audit or Revoke Leases Once you have created one or more leases, you will be able to access them by clicking on the respective dynamic secret item on the dashboard. -This will allow you see the expiration time of the lease or delete the lease before it's set time to live. +This will allow you to see the expiration time of the lease or delete the lease before it's set time to live. ![Provision Lease](/images/platform/dynamic-secrets/lease-data.png) ## Renew Leases -To extend the life of the generated dynamic secret leases past its initial time to live, simply click on the **Renew** as illustrated below. +To extend the life of the generated dynamic secret leases past its initial time to live, simply click on the **Renew** button as illustrated below. ![Provision Lease](/images/platform/dynamic-secrets/dynamic-secret-lease-renew.png) diff --git a/docs/documentation/platform/dynamic-secrets/mysql.mdx b/docs/documentation/platform/dynamic-secrets/mysql.mdx index d85f4b7bb..da39c0a56 100644 --- a/docs/documentation/platform/dynamic-secrets/mysql.mdx +++ b/docs/documentation/platform/dynamic-secrets/mysql.mdx @@ -27,7 +27,7 @@ Create a user with the required permission in your SQL instance. This user will - Default time-to-live for a generated secret (it is possible to modify this value when a secret is generate) + Default time-to-live for a generated secret (it is possible to modify this value after a secret is generated) @@ -102,12 +102,12 @@ Create a user with the required permission in your SQL instance. This user will ## Audit or Revoke Leases Once you have created one or more leases, you will be able to access them by clicking on the respective dynamic secret item on the dashboard. -This will allow you see the expiration time of the lease or delete a lease before it's set time to live. +This will allow you to see the expiration time of the lease or delete a lease before it's set time to live. ![Provision Lease](/images/platform/dynamic-secrets/lease-data.png) ## Renew Leases -To extend the life of the generated dynamic secret leases past its initial time to live, simply click on the **Renew** as illustrated below. +To extend the life of the generated dynamic secret leases past its initial time to live, simply click on the **Renew** button as illustrated below. ![Provision Lease](/images/platform/dynamic-secrets/dynamic-secret-lease-renew.png) diff --git a/docs/documentation/platform/dynamic-secrets/oracle.mdx b/docs/documentation/platform/dynamic-secrets/oracle.mdx index a6fb68913..e8fa86028 100644 --- a/docs/documentation/platform/dynamic-secrets/oracle.mdx +++ b/docs/documentation/platform/dynamic-secrets/oracle.mdx @@ -27,7 +27,7 @@ Create a user with the required permission in your SQL instance. This user will - Default time-to-live for a generated secret (it is possible to modify this value when a secret is generate) + Default time-to-live for a generated secret (it is possible to modify this value after a secret is generated) @@ -102,12 +102,12 @@ Create a user with the required permission in your SQL instance. This user will ## Audit or Revoke Leases Once you have created one or more leases, you will be able to access them by clicking on the respective dynamic secret item on the dashboard. -This will allow you see the expiration time of the lease or delete a lease before it's set time to live. +This will allow you to see the expiration time of the lease or delete a lease before it's set time to live. ![Provision Lease](/images/platform/dynamic-secrets/lease-data.png) ## Renew Leases -To extend the life of the generated dynamic secret leases past its initial time to live, simply click on the **Renew** as illustrated below. +To extend the life of the generated dynamic secret leases past its initial time to live, simply click on the **Renew** button as illustrated below. ![Provision Lease](/images/platform/dynamic-secrets/dynamic-secret-lease-renew.png) diff --git a/docs/documentation/platform/dynamic-secrets/postgresql.mdx b/docs/documentation/platform/dynamic-secrets/postgresql.mdx index ebc19b011..5216d87af 100644 --- a/docs/documentation/platform/dynamic-secrets/postgresql.mdx +++ b/docs/documentation/platform/dynamic-secrets/postgresql.mdx @@ -28,7 +28,7 @@ Create a user with the required permission in your SQL instance. This user will - Default time-to-live for a generated secret (it is possible to modify this value when a secret is generate) + Default time-to-live for a generated secret (it is possible to modify this value after a secret is generated) @@ -105,12 +105,12 @@ Create a user with the required permission in your SQL instance. This user will ## Audit or Revoke Leases Once you have created one or more leases, you will be able to access them by clicking on the respective dynamic secret item on the dashboard. -This will allow you see the expiration time of the lease or delete the lease before it's set time to live. +This will allow you to see the expiration time of the lease or delete the lease before it's set time to live. ![Provision Lease](/images/platform/dynamic-secrets/lease-data.png) ## Renew Leases -To extend the life of the generated dynamic secret leases past its initial time to live, simply click on the **Renew** as illustrated below. +To extend the life of the generated dynamic secret leases past its initial time to live, simply click on the **Renew** button as illustrated below. ![Provision Lease](/images/platform/dynamic-secrets/dynamic-secret-lease-renew.png) diff --git a/docs/documentation/platform/dynamic-secrets/rabbit-mq.mdx b/docs/documentation/platform/dynamic-secrets/rabbit-mq.mdx index f8649b727..6ac5ac069 100644 --- a/docs/documentation/platform/dynamic-secrets/rabbit-mq.mdx +++ b/docs/documentation/platform/dynamic-secrets/rabbit-mq.mdx @@ -28,7 +28,7 @@ The Infisical RabbitMQ dynamic secret allows you to generate RabbitMQ credential - Default time-to-live for a generated secret (it is possible to modify this value when a secret is generate) + Default time-to-live for a generated secret (it is possible to modify this value after a secret is generated) @@ -103,12 +103,12 @@ The Infisical RabbitMQ dynamic secret allows you to generate RabbitMQ credential ## Audit or Revoke Leases Once you have created one or more leases, you will be able to access them by clicking on the respective dynamic secret item on the dashboard. -This will allow you see the expiration time of the lease or delete a lease before it's set time to live. +This will allow you to see the expiration time of the lease or delete a lease before it's set time to live. ![Provision Lease](/images/platform/dynamic-secrets/lease-data.png) ## Renew Leases -To extend the life of the generated dynamic secret leases past its initial time to live, simply click on the **Renew** as illustrated below. +To extend the life of the generated dynamic secret leases past its initial time to live, simply click on the **Renew** button as illustrated below. ![Provision Lease](/images/platform/dynamic-secrets/dynamic-secret-lease-renew.png) diff --git a/docs/documentation/platform/dynamic-secrets/redis.mdx b/docs/documentation/platform/dynamic-secrets/redis.mdx index cb2e6a17e..43fbc6b61 100644 --- a/docs/documentation/platform/dynamic-secrets/redis.mdx +++ b/docs/documentation/platform/dynamic-secrets/redis.mdx @@ -27,7 +27,7 @@ Create a user with the required permission in your Redis instance. This user wil - Default time-to-live for a generated secret (it is possible to modify this value when a secret is generate) + Default time-to-live for a generated secret (it is possible to modify this value after a secret is generated) @@ -93,12 +93,12 @@ Create a user with the required permission in your Redis instance. This user wil ## Audit or Revoke Leases Once you have created one or more leases, you will be able to access them by clicking on the respective dynamic secret item on the dashboard. -This will allow you see the expiration time of the lease or delete a lease before it's set time to live. +This will allow you to see the expiration time of the lease or delete a lease before it's set time to live. ![Provision Lease](/images/platform/dynamic-secrets/lease-data.png) ## Renew Leases -To extend the life of the generated dynamic secret leases past its initial time to live, simply click on the **Renew** as illustrated below. +To extend the life of the generated dynamic secret leases past its initial time to live, simply click on the **Renew** button as illustrated below. ![Provision Lease](/images/platform/dynamic-secrets/dynamic-secret-lease-renew.png) diff --git a/docs/documentation/platform/dynamic-secrets/sap-hana.mdx b/docs/documentation/platform/dynamic-secrets/sap-hana.mdx index 3c2a837d3..668777549 100644 --- a/docs/documentation/platform/dynamic-secrets/sap-hana.mdx +++ b/docs/documentation/platform/dynamic-secrets/sap-hana.mdx @@ -30,7 +30,7 @@ The Infisical SAP HANA dynamic secret allows you to generate SAP HANA database c - Default time-to-live for a generated secret (it is possible to modify this value when a secret is generate) + Default time-to-live for a generated secret (it is possible to modify this value after a secret is generated) @@ -106,13 +106,13 @@ The Infisical SAP HANA dynamic secret allows you to generate SAP HANA database c ## Audit or Revoke Leases Once you have created one or more leases, you will be able to access them by clicking on the respective dynamic secret item on the dashboard. -This will allow you see the lease details and delete the lease ahead of its expiration time. +This will allow you to see the lease details and delete the lease ahead of its expiration time. ![Provision Lease](/images/platform/dynamic-secrets/lease-data.png) ## Renew Leases -To extend the life of the generated dynamic secret lease past its initial time to live, simply click on the **Renew** as illustrated below. +To extend the life of the generated dynamic secret lease past its initial time to live, simply click on the **Renew** button as illustrated below. ![Provision Lease](/images/platform/dynamic-secrets/dynamic-secret-lease-renew.png) diff --git a/docs/documentation/platform/dynamic-secrets/snowflake.mdx b/docs/documentation/platform/dynamic-secrets/snowflake.mdx index f5e06ba76..75db96c8f 100644 --- a/docs/documentation/platform/dynamic-secrets/snowflake.mdx +++ b/docs/documentation/platform/dynamic-secrets/snowflake.mdx @@ -109,7 +109,7 @@ Infisical's Snowflake dynamic secrets allow you to generate Snowflake user crede ## Audit or Revoke Leases Once you have created one or more leases, you will be able to access them by clicking on the respective dynamic secret item on the dashboard. -This will allow you see the lease details and delete the lease ahead of its expiration time. +This will allow you to see the lease details and delete the lease ahead of its expiration time. ![Provision Lease](/images/platform/dynamic-secrets/lease-data.png) diff --git a/docs/documentation/platform/dynamic-secrets/totp.mdx b/docs/documentation/platform/dynamic-secrets/totp.mdx new file mode 100644 index 000000000..201a402e1 --- /dev/null +++ b/docs/documentation/platform/dynamic-secrets/totp.mdx @@ -0,0 +1,70 @@ +--- +title: "TOTP" +description: "Learn how to dynamically generate time-based one-time passwords." +--- + +The Infisical TOTP dynamic secret allows you to generate time-based one-time passwords on demand. + +## Prerequisite + +- Infisical requires either an OTP url or a secret key from a TOTP provider. + +## Set up Dynamic Secrets with TOTP + + + + Open the Secret Overview dashboard and select the environment in which you would like to add a dynamic secret. + + + ![Add Dynamic Secret Button](/images/platform/dynamic-secrets/add-dynamic-secret-button.png) + + + ![Dynamic Secret Modal](/images/platform/dynamic-secrets/dynamic-secret-modal-totp.png) + + + + Name by which you want the secret to be referenced + + + There are two supported configuration types - `url` and `manual`. + + When `url` is selected, you can configure the TOTP generator using the OTP URL. + + When `manual` is selected, you can configure the TOTP generator using the secret key along with other configurations like period, number of digits, and algorithm. + + + OTP URL in `otpauth://` format used to generate TOTP codes. + + + Base32 encoded secret used to generate TOTP codes. + + + Time interval in seconds between generating new TOTP codes. + + + Number of digits to generate in each TOTP code. + + + Hash algorithm to use when generating TOTP codes. The supported algorithms are sha1, sha256, and sha512. + + + ![Dynamic Secret Setup Modal](../../../images/platform/dynamic-secrets/dynamic-secret-setup-modal-totp-url.png) + ![Dynamic Secret Setup Modal](../../../images/platform/dynamic-secrets/dynamic-secret-setup-modal-totp-manual.png) + + + + After submitting the form, you will see a dynamic secret created in the dashboard. + + + + Once you've successfully configured the dynamic secret, you're ready to generate on-demand TOTPs. + To do this, simply click on the 'Generate' button which appears when hovering over the dynamic secret item. + + ![Dynamic Secret](/images/platform/dynamic-secrets/dynamic-secret-generate.png) + + Once you click the `Generate` button, a new secret lease will be generated and the TOTP will be shown to you. + + ![Provision Lease](/images/platform/dynamic-secrets/totp-lease-value.png) + + + diff --git a/docs/images/platform/dynamic-secrets/dynamic-secret-modal-totp.png b/docs/images/platform/dynamic-secrets/dynamic-secret-modal-totp.png new file mode 100644 index 000000000..53326ebe2 Binary files /dev/null and b/docs/images/platform/dynamic-secrets/dynamic-secret-modal-totp.png differ diff --git a/docs/images/platform/dynamic-secrets/dynamic-secret-setup-modal-totp-manual.png b/docs/images/platform/dynamic-secrets/dynamic-secret-setup-modal-totp-manual.png new file mode 100644 index 000000000..788852d85 Binary files /dev/null and b/docs/images/platform/dynamic-secrets/dynamic-secret-setup-modal-totp-manual.png differ diff --git a/docs/images/platform/dynamic-secrets/dynamic-secret-setup-modal-totp-url.png b/docs/images/platform/dynamic-secrets/dynamic-secret-setup-modal-totp-url.png new file mode 100644 index 000000000..ede474a59 Binary files /dev/null and b/docs/images/platform/dynamic-secrets/dynamic-secret-setup-modal-totp-url.png differ diff --git a/docs/images/platform/dynamic-secrets/totp-lease-value.png b/docs/images/platform/dynamic-secrets/totp-lease-value.png new file mode 100644 index 000000000..af2ffe8e1 Binary files /dev/null and b/docs/images/platform/dynamic-secrets/totp-lease-value.png differ diff --git a/docs/mint.json b/docs/mint.json index f070ae88a..e4e487915 100644 --- a/docs/mint.json +++ b/docs/mint.json @@ -189,7 +189,8 @@ "documentation/platform/dynamic-secrets/azure-entra-id", "documentation/platform/dynamic-secrets/ldap", "documentation/platform/dynamic-secrets/sap-hana", - "documentation/platform/dynamic-secrets/snowflake" + "documentation/platform/dynamic-secrets/snowflake", + "documentation/platform/dynamic-secrets/totp" ] }, "documentation/platform/project-templates", diff --git a/frontend/src/hooks/api/dynamicSecret/types.ts b/frontend/src/hooks/api/dynamicSecret/types.ts index 7ac8d4147..35b08df32 100644 --- a/frontend/src/hooks/api/dynamicSecret/types.ts +++ b/frontend/src/hooks/api/dynamicSecret/types.ts @@ -28,7 +28,8 @@ export enum DynamicSecretProviders { AzureEntraId = "azure-entra-id", Ldap = "ldap", SapHana = "sap-hana", - Snowflake = "snowflake" + Snowflake = "snowflake", + Totp = "totp" } export enum SqlProviders { @@ -230,6 +231,21 @@ export type TDynamicSecretProvider = revocationStatement: string; renewStatement?: string; }; + } + | { + type: DynamicSecretProviders.Totp; + inputs: + | { + configType: "url"; + url: string; + } + | { + configType: "manual"; + secret: string; + period?: number; + algorithm?: string; + digits?: number; + }; }; export type TCreateDynamicSecretDTO = { projectSlug: string; diff --git a/frontend/src/views/SecretMainPage/components/ActionBar/CreateDynamicSecretForm/CreateDynamicSecretForm.tsx b/frontend/src/views/SecretMainPage/components/ActionBar/CreateDynamicSecretForm/CreateDynamicSecretForm.tsx index 490ad6f48..44a435958 100644 --- a/frontend/src/views/SecretMainPage/components/ActionBar/CreateDynamicSecretForm/CreateDynamicSecretForm.tsx +++ b/frontend/src/views/SecretMainPage/components/ActionBar/CreateDynamicSecretForm/CreateDynamicSecretForm.tsx @@ -11,7 +11,7 @@ import { SiSnowflake } from "react-icons/si"; import { faAws } from "@fortawesome/free-brands-svg-icons"; -import { faDatabase } from "@fortawesome/free-solid-svg-icons"; +import { faClock, faDatabase } from "@fortawesome/free-solid-svg-icons"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { AnimatePresence, motion } from "framer-motion"; @@ -31,6 +31,7 @@ import { RabbitMqInputForm } from "./RabbitMqInputForm"; import { RedisInputForm } from "./RedisInputForm"; import { SapHanaInputForm } from "./SapHanaInputForm"; import { SqlDatabaseInputForm } from "./SqlDatabaseInputForm"; +import { TotpInputForm } from "./TotpInputForm"; type Props = { isOpen?: boolean; @@ -110,6 +111,11 @@ const DYNAMIC_SECRET_LIST = [ icon: , provider: DynamicSecretProviders.Snowflake, title: "Snowflake" + }, + { + icon: , + provider: DynamicSecretProviders.Totp, + title: "TOTP" } ]; @@ -405,6 +411,24 @@ export const CreateDynamicSecretForm = ({ /> )} + {wizardStep === WizardSteps.ProviderInputs && + selectedProvider === DynamicSecretProviders.Totp && ( + + + + )} diff --git a/frontend/src/views/SecretMainPage/components/ActionBar/CreateDynamicSecretForm/TotpInputForm.tsx b/frontend/src/views/SecretMainPage/components/ActionBar/CreateDynamicSecretForm/TotpInputForm.tsx new file mode 100644 index 000000000..29f4282cc --- /dev/null +++ b/frontend/src/views/SecretMainPage/components/ActionBar/CreateDynamicSecretForm/TotpInputForm.tsx @@ -0,0 +1,314 @@ +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 { useCreateDynamicSecret } from "@app/hooks/api"; +import { DynamicSecretProviders } from "@app/hooks/api/dynamicSecret/types"; + +enum ConfigType { + URL = "url", + MANUAL = "manual" +} + +enum TotpAlgorithm { + SHA1 = "sha1", + SHA256 = "sha256", + SHA512 = "sha512" +} + +const formSchema = z.object({ + provider: 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() + }) + ]), + name: z + .string() + .trim() + .min(1) + .refine((val) => val.toLowerCase() === val, "Must be lowercase") +}); + +type TForm = z.infer; + +type Props = { + onCompleted: () => void; + onCancel: () => void; + secretPath: string; + projectSlug: string; + environment: string; +}; + +export const TotpInputForm = ({ + onCompleted, + onCancel, + environment, + secretPath, + projectSlug +}: Props) => { + const { + control, + watch, + formState: { isSubmitting }, + handleSubmit + } = useForm({ + resolver: zodResolver(formSchema), + defaultValues: { + provider: { + configType: ConfigType.URL + } + } + }); + + const selectedConfigType = watch("provider.configType"); + + const createDynamicSecret = useCreateDynamicSecret(); + + const handleCreateDynamicSecret = async ({ name, provider }: TForm) => { + // wait till previous request is finished + if (createDynamicSecret.isLoading) return; + try { + await createDynamicSecret.mutateAsync({ + provider: { type: DynamicSecretProviders.Totp, inputs: provider }, + maxTTL: "24h", + name, + path: secretPath, + defaultTTL: "1m", + projectSlug, + environmentSlug: environment + }); + onCompleted(); + } catch (err) { + createNotification({ + type: "error", + text: err instanceof Error ? err.message : "Failed to create dynamic secret" + }); + } + }; + + return ( +
+
+
+
+
+ ( + + + + )} + /> +
+
+
+
+ Configuration + + +
+ + Docs + +
+
+ +
+
+ ( + + + + )} + /> + {selectedConfigType === ConfigType.URL && ( + ( + + + + )} + /> + )} + {selectedConfigType === ConfigType.MANUAL && ( + <> + ( + + + + )} + /> +
+ ( + + field.onChange(Number(e.target.value))} + /> + + )} + /> + ( + + field.onChange(Number(e.target.value))} + /> + + )} + /> + ( + + + + )} + /> +
+

+ The period, digits, and algorithm values can remain at their defaults unless + your TOTP provider specifies otherwise. +

+ + )} +
+
+
+
+ + +
+
+
+ ); +}; diff --git a/frontend/src/views/SecretMainPage/components/DynamicSecretListView/CreateDynamicSecretLease.tsx b/frontend/src/views/SecretMainPage/components/DynamicSecretListView/CreateDynamicSecretLease.tsx index d4ba30158..902d157e8 100644 --- a/frontend/src/views/SecretMainPage/components/DynamicSecretListView/CreateDynamicSecretLease.tsx +++ b/frontend/src/views/SecretMainPage/components/DynamicSecretListView/CreateDynamicSecretLease.tsx @@ -1,6 +1,6 @@ -import { ReactNode } 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 { faCheck, faClock, faCopy } from "@fortawesome/free-solid-svg-icons"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { zodResolver } from "@hookform/resolvers/zod"; import { AnimatePresence, motion } from "framer-motion"; @@ -9,8 +9,16 @@ import { z } from "zod"; import { TtlFormLabel } from "@app/components/features"; import { createNotification } from "@app/components/notifications"; -import { Button, FormControl, IconButton, Input, SecretInput, Tooltip } from "@app/components/v2"; -import { useTimedReset } from "@app/hooks"; +import { + Button, + FormControl, + IconButton, + Input, + SecretInput, + Spinner, + Tooltip +} from "@app/components/v2"; +import { useTimedReset, useToggle } from "@app/hooks"; import { useCreateDynamicSecretLease } from "@app/hooks/api"; import { DynamicSecretProviders } from "@app/hooks/api/dynamicSecret/types"; @@ -54,7 +62,76 @@ const OutputDisplay = ({ ); }; -const renderOutputForm = (provider: DynamicSecretProviders, data: unknown) => { +const TotpOutputDisplay = ({ + totp, + remainingSeconds, + triggerLeaseRegeneration +}: { + totp: string; + remainingSeconds: number; + triggerLeaseRegeneration: (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 ? ( +
+ + + Expires in {remainingTime} {remainingTime > 1 ? "seconds" : "second"} + +
+ ) : ( +
+ + Expired +
+ )} + {shouldShowRegenerate && ( + + )} +
+ ); +}; + +const renderOutputForm = ( + provider: DynamicSecretProviders, + data: unknown, + triggerLeaseRegeneration: (details: { ttl?: string }) => Promise +) => { if ( provider === DynamicSecretProviders.SqlDatabase || provider === DynamicSecretProviders.Cassandra || @@ -242,11 +319,29 @@ const renderOutputForm = (provider: DynamicSecretProviders, data: unknown) => { ); } + if (provider === DynamicSecretProviders.Totp) { + const { TOTP, TIME_REMAINING } = data as { + TOTP: string; + TIME_REMAINING: number; + }; + + return ( + + ); + } + return null; }; const formSchema = z.object({ - ttl: z.string().refine((val) => ms(val) > 0, "TTL must be a positive number") + ttl: z + .string() + .refine((val) => ms(val) > 0, "TTL must be a positive number") + .optional() }); type TForm = z.infer; @@ -259,6 +354,8 @@ type Props = { secretPath: string; }; +const PROVIDERS_WITH_AUTOGENERATE_SUPPORT = [DynamicSecretProviders.Totp]; + export const CreateDynamicSecretLease = ({ onClose, projectSlug, @@ -277,6 +374,9 @@ export const CreateDynamicSecretLease = ({ ttl: "1h" } }); + const [isPreloading, setIsPreloading] = useToggle( + PROVIDERS_WITH_AUTOGENERATE_SUPPORT.includes(provider) + ); const createDynamicSecretLease = useCreateDynamicSecretLease(); @@ -290,10 +390,13 @@ export const CreateDynamicSecretLease = ({ ttl, dynamicSecretName }); + createNotification({ type: "success", text: "Successfully leased dynamic secret" }); + + setIsPreloading.off(); } catch (error) { console.log(error); createNotification({ @@ -303,8 +406,23 @@ export const CreateDynamicSecretLease = ({ } }; + const handleLeaseRegeneration = async (data: { ttl?: string }) => { + setIsPreloading.on(); + handleDynamicSecretLeaseCreate(data); + }; + + useEffect(() => { + if (provider === DynamicSecretProviders.Totp) { + handleDynamicSecretLeaseCreate({}); + } + }, [provider]); + const isOutputMode = Boolean(createDynamicSecretLease?.data); + if (isPreloading) { + return ; + } + return (
@@ -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 ( +
+
+
+
+
+ ( + + + + )} + /> +
+
+
+
+ Configuration + + +
+ + Docs + +
+
+ +
+
+ ( + + + + )} + /> + {selectedConfigType === ConfigType.URL && ( + ( + + + + )} + /> + )} + {selectedConfigType === ConfigType.MANUAL && ( + <> + ( + + + + )} + /> +
+ ( + + field.onChange(Number(e.target.value))} + /> + + )} + /> + ( + + field.onChange(Number(e.target.value))} + /> + + )} + /> + ( + + + + )} + /> +
+

+ The period, digits, and algorithm values can remain at their defaults unless + your TOTP provider specifies otherwise. +

+ + )} +
+
+
+
+ + +
+
+
+ ); +};