mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-10-14 21:51:56 +00:00
Merge remote-tracking branch 'origin' into groups-phase-2b
This commit is contained in:
@@ -16,7 +16,8 @@ export type TDynamicSecret = {
|
||||
};
|
||||
|
||||
export enum DynamicSecretProviders {
|
||||
SqlDatabase = "sql-database"
|
||||
SqlDatabase = "sql-database",
|
||||
Cassandra = "cassandra"
|
||||
}
|
||||
|
||||
export enum SqlProviders {
|
||||
@@ -25,21 +26,37 @@ export enum SqlProviders {
|
||||
Oracle = "oracledb"
|
||||
}
|
||||
|
||||
export type TDynamicSecretProvider = {
|
||||
type: DynamicSecretProviders;
|
||||
inputs: {
|
||||
client: SqlProviders;
|
||||
host: string;
|
||||
port: number;
|
||||
database: string;
|
||||
username: string;
|
||||
password: string;
|
||||
creationStatement: string;
|
||||
revocationStatement: string;
|
||||
renewStatement?: string;
|
||||
ca?: string | undefined;
|
||||
export type TDynamicSecretProvider =
|
||||
| {
|
||||
type: DynamicSecretProviders.SqlDatabase;
|
||||
inputs: {
|
||||
client: SqlProviders;
|
||||
host: string;
|
||||
port: number;
|
||||
database: string;
|
||||
username: string;
|
||||
password: string;
|
||||
creationStatement: string;
|
||||
revocationStatement: string;
|
||||
renewStatement?: string;
|
||||
ca?: string | undefined;
|
||||
};
|
||||
}
|
||||
| {
|
||||
type: DynamicSecretProviders.Cassandra;
|
||||
inputs: {
|
||||
host: string;
|
||||
port: number;
|
||||
keyspace?: string;
|
||||
localDataCenter: string;
|
||||
username: string;
|
||||
password: string;
|
||||
creationStatement: string;
|
||||
revocationStatement: string;
|
||||
renewStatement?: string;
|
||||
ca?: string | undefined;
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
export type TCreateDynamicSecretDTO = {
|
||||
projectSlug: string;
|
||||
|
||||
@@ -10,6 +10,7 @@ import {
|
||||
Environment,
|
||||
HerokuPipelineCoupling,
|
||||
IntegrationAuth,
|
||||
KmsKey,
|
||||
NorthflankSecretGroup,
|
||||
Org,
|
||||
Project,
|
||||
@@ -43,6 +44,14 @@ const integrationAuthKeys = {
|
||||
[{ integrationAuthId }, "integrationAuthGithubOrgs"] as const,
|
||||
getIntegrationAuthGithubEnvs: (integrationAuthId: string, repoName: string, repoOwner: string) =>
|
||||
[{ integrationAuthId, repoName, repoOwner }, "integrationAuthGithubOrgs"] as const,
|
||||
getIntegrationAuthAwsKmsKeys: ({
|
||||
integrationAuthId,
|
||||
region
|
||||
}: {
|
||||
integrationAuthId: string,
|
||||
region: string
|
||||
}) =>
|
||||
[{ integrationAuthId, region }, "integrationAuthAwsKmsKeyIds"] as const,
|
||||
getIntegrationAuthQoveryOrgs: (integrationAuthId: string) =>
|
||||
[{ integrationAuthId }, "integrationAuthQoveryOrgs"] as const,
|
||||
getIntegrationAuthQoveryProjects: ({
|
||||
@@ -217,6 +226,27 @@ const fetchIntegrationAuthQoveryOrgs = async (integrationAuthId: string) => {
|
||||
return orgs;
|
||||
};
|
||||
|
||||
const fetchIntegrationAuthAwsKmsKeys = async ({
|
||||
integrationAuthId,
|
||||
region
|
||||
}: {
|
||||
integrationAuthId: string;
|
||||
region: string;
|
||||
}) => {
|
||||
const {
|
||||
data: { kmsKeys }
|
||||
} = await apiRequest.get<{ kmsKeys: KmsKey[] }>(
|
||||
`/api/v1/integration-auth/${integrationAuthId}/aws-secrets-manager/kms-keys`,
|
||||
{
|
||||
params: {
|
||||
region
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
return kmsKeys;
|
||||
};
|
||||
|
||||
const fetchIntegrationAuthQoveryProjects = async ({
|
||||
integrationAuthId,
|
||||
orgId
|
||||
@@ -544,6 +574,27 @@ export const useGetIntegrationAuthQoveryOrgs = (integrationAuthId: string) => {
|
||||
});
|
||||
};
|
||||
|
||||
export const useGetIntegrationAuthAwsKmsKeys = ({
|
||||
integrationAuthId,
|
||||
region
|
||||
}: {
|
||||
integrationAuthId: string;
|
||||
region: string;
|
||||
}) => {
|
||||
return useQuery({
|
||||
queryKey: integrationAuthKeys.getIntegrationAuthAwsKmsKeys({
|
||||
integrationAuthId,
|
||||
region
|
||||
}),
|
||||
queryFn: () =>
|
||||
fetchIntegrationAuthAwsKmsKeys({
|
||||
integrationAuthId,
|
||||
region
|
||||
}),
|
||||
enabled: true
|
||||
});
|
||||
};
|
||||
|
||||
export const useGetIntegrationAuthQoveryProjects = ({
|
||||
integrationAuthId,
|
||||
orgId
|
||||
|
||||
@@ -58,6 +58,11 @@ export type Project = {
|
||||
projectId: string;
|
||||
};
|
||||
|
||||
export type KmsKey = {
|
||||
id: string;
|
||||
alias: string;
|
||||
};
|
||||
|
||||
export type Service = {
|
||||
name: string;
|
||||
serviceId: string;
|
||||
|
||||
@@ -66,7 +66,8 @@ export const useCreateIntegration = () => {
|
||||
secretAWSTag?: {
|
||||
key: string;
|
||||
value: string;
|
||||
};
|
||||
}[];
|
||||
kmsKeyId?: string;
|
||||
};
|
||||
}) => {
|
||||
const {
|
||||
|
||||
@@ -128,10 +128,10 @@ export default function AWSParameterStoreCreateIntegrationPage() {
|
||||
metadata: {
|
||||
...(shouldTag
|
||||
? {
|
||||
secretAWSTag: {
|
||||
secretAWSTag: [{
|
||||
key: tagKey,
|
||||
value: tagValue
|
||||
}
|
||||
}]
|
||||
}
|
||||
: {})
|
||||
}
|
||||
@@ -279,7 +279,7 @@ export default function AWSParameterStoreCreateIntegrationPage() {
|
||||
label="Tag Value"
|
||||
>
|
||||
<Input
|
||||
placeholder="managed-by"
|
||||
placeholder="infisical"
|
||||
value={tagValue}
|
||||
onChange={(e) => setTagValue(e.target.value)}
|
||||
/>
|
||||
|
||||
@@ -14,6 +14,7 @@ import { motion } from "framer-motion";
|
||||
import queryString from "query-string";
|
||||
|
||||
import { useCreateIntegration } from "@app/hooks/api";
|
||||
import { useGetIntegrationAuthAwsKmsKeys } from "@app/hooks/api/integrationAuth/queries";
|
||||
|
||||
import {
|
||||
Button,
|
||||
@@ -87,6 +88,7 @@ export default function AWSSecretManagerCreateIntegrationPage() {
|
||||
const [targetSecretNameErrorText, setTargetSecretNameErrorText] = useState("");
|
||||
const [tagKey, setTagKey] = useState("");
|
||||
const [tagValue, setTagValue] = useState("");
|
||||
const [kmsKeyId, setKmsKeyId] = useState("");
|
||||
|
||||
// const [path, setPath] = useState('');
|
||||
// const [pathErrorText, setPathErrorText] = useState('');
|
||||
@@ -94,6 +96,19 @@ export default function AWSSecretManagerCreateIntegrationPage() {
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [shouldTag, setShouldTag] = useState(false);
|
||||
|
||||
|
||||
const { data: integrationAuthAwsKmsKeys, isLoading: isIntegrationAuthAwsKmsKeysLoading } =
|
||||
useGetIntegrationAuthAwsKmsKeys({
|
||||
integrationAuthId: String(integrationAuthId),
|
||||
region: selectedAWSRegion
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (integrationAuthAwsKmsKeys) {
|
||||
setKmsKeyId(String(integrationAuthAwsKmsKeys?.filter(key => key.alias === "alias/aws/secretsmanager")[0]?.id))
|
||||
}
|
||||
}, [integrationAuthAwsKmsKeys])
|
||||
|
||||
useEffect(() => {
|
||||
if (workspace) {
|
||||
setSelectedSourceEnvironment(workspace.environments[0].slug);
|
||||
@@ -127,12 +142,16 @@ export default function AWSSecretManagerCreateIntegrationPage() {
|
||||
metadata: {
|
||||
...(shouldTag
|
||||
? {
|
||||
secretAWSTag: {
|
||||
secretAWSTag: [{
|
||||
key: tagKey,
|
||||
value: tagValue
|
||||
}
|
||||
}]
|
||||
}
|
||||
: {})
|
||||
: {}),
|
||||
...((kmsKeyId && integrationAuthAwsKmsKeys?.filter(key => key.id === kmsKeyId)[0]?.alias !== "alias/aws/secretsmanager") ?
|
||||
{
|
||||
kmsKeyId
|
||||
}: {})
|
||||
}
|
||||
});
|
||||
|
||||
@@ -145,7 +164,7 @@ export default function AWSSecretManagerCreateIntegrationPage() {
|
||||
}
|
||||
};
|
||||
|
||||
return integrationAuth && workspace && selectedSourceEnvironment ? (
|
||||
return (integrationAuth && workspace && selectedSourceEnvironment && !isIntegrationAuthAwsKmsKeysLoading) ? (
|
||||
<div className="flex h-full w-full flex-col items-center justify-center">
|
||||
<Head>
|
||||
<title>Set Up AWS Secrets Manager Integration</title>
|
||||
@@ -278,13 +297,38 @@ export default function AWSSecretManagerCreateIntegrationPage() {
|
||||
label="Tag Value"
|
||||
>
|
||||
<Input
|
||||
placeholder="managed-by"
|
||||
placeholder="infisical"
|
||||
value={tagValue}
|
||||
onChange={(e) => setTagValue(e.target.value)}
|
||||
/>
|
||||
</FormControl>
|
||||
</div>
|
||||
)}
|
||||
<FormControl label="Encryption Key" className="mt-4">
|
||||
<Select
|
||||
value={kmsKeyId}
|
||||
onValueChange={(e) => {
|
||||
setKmsKeyId(e)
|
||||
}}
|
||||
className="w-full border border-mineshaft-500"
|
||||
>
|
||||
{integrationAuthAwsKmsKeys?.length ? (
|
||||
integrationAuthAwsKmsKeys.map((key) => {
|
||||
return (
|
||||
<SelectItem
|
||||
value={key.id as string}
|
||||
key={`repo-id-${key.id}`}
|
||||
className="w-[28.4rem] text-sm"
|
||||
>
|
||||
{key.alias}
|
||||
</SelectItem>
|
||||
);
|
||||
})
|
||||
) : (
|
||||
<div />
|
||||
)}
|
||||
</Select>
|
||||
</FormControl>
|
||||
</motion.div>
|
||||
</TabPanel>
|
||||
</Tabs>
|
||||
@@ -317,7 +361,7 @@ export default function AWSSecretManagerCreateIntegrationPage() {
|
||||
<title>Set Up AWS Secrets Manager Integration</title>
|
||||
<link rel="icon" href="/infisical.ico" />
|
||||
</Head>
|
||||
{isintegrationAuthLoading ? (
|
||||
{(isintegrationAuthLoading || isIntegrationAuthAwsKmsKeysLoading) ? (
|
||||
<img
|
||||
src="/images/loading/loading.gif"
|
||||
height={70}
|
||||
|
||||
@@ -1,5 +1,10 @@
|
||||
import { useState } from "react";
|
||||
import Head from "next/head";
|
||||
import Image from "next/image";
|
||||
import Link from "next/link";
|
||||
import { useRouter } from "next/router";
|
||||
import { faArrowUpRightFromSquare, faBookOpen } from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
|
||||
import { useSaveIntegrationAccessToken } from "@app/hooks/api";
|
||||
|
||||
@@ -49,12 +54,56 @@ export default function TerraformCloudCreateIntegrationPage() {
|
||||
|
||||
return (
|
||||
<div className="flex h-full w-full items-center justify-center">
|
||||
<Card className="max-w-md rounded-md p-8">
|
||||
<CardTitle className="text-center">Terraform Cloud Integration</CardTitle>
|
||||
<Head>
|
||||
<title>Authorize Terraform Cloud Integration</title>
|
||||
<link rel="icon" href="/infisical.ico" />
|
||||
</Head>
|
||||
<Card className="max-w-lg rounded-md border border-mineshaft-600">
|
||||
<CardTitle
|
||||
className="px-6 text-left text-xl"
|
||||
subTitle="After adding the details below, you will be prompted to set up an integration for a particular Infisical project and environment."
|
||||
>
|
||||
<div className="flex flex-row items-center">
|
||||
<div className="inline flex items-center">
|
||||
<Image
|
||||
src="/images/integrations/Terraform.png"
|
||||
height={35}
|
||||
width={35}
|
||||
alt="Terraform logo"
|
||||
/>
|
||||
</div>
|
||||
<span className="ml-1.5">Terraform Cloud Integration </span>
|
||||
<Link href="https://infisical.com/docs/integrations/cloud/terraform-cloud" passHref>
|
||||
<a target="_blank" rel="noopener noreferrer">
|
||||
<div className="ml-2 mb-1 inline-block cursor-default rounded-md bg-yellow/20 px-1.5 pb-[0.03rem] pt-[0.04rem] text-sm text-yellow opacity-80 hover:opacity-100">
|
||||
<FontAwesomeIcon icon={faBookOpen} className="mr-1.5" />
|
||||
Docs
|
||||
<FontAwesomeIcon
|
||||
icon={faArrowUpRightFromSquare}
|
||||
className="ml-1.5 mb-[0.07rem] text-xxs"
|
||||
/>
|
||||
</div>
|
||||
</a>
|
||||
</Link>
|
||||
</div>
|
||||
</CardTitle>
|
||||
<FormControl
|
||||
label="Terraform Cloud Workspace ID"
|
||||
errorText={workspacesIdErrorText}
|
||||
isError={workspacesIdErrorText !== "" ?? false}
|
||||
className="px-6"
|
||||
>
|
||||
<Input
|
||||
placeholder="Workspace Id"
|
||||
value={workspacesId}
|
||||
onChange={(e) => setWorkSpacesId(e.target.value)}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormControl
|
||||
label="Terraform Cloud API Token"
|
||||
errorText={apiKeyErrorText}
|
||||
isError={apiKeyErrorText !== "" ?? false}
|
||||
className="px-6"
|
||||
>
|
||||
<Input
|
||||
placeholder="API Token"
|
||||
@@ -64,21 +113,11 @@ export default function TerraformCloudCreateIntegrationPage() {
|
||||
onChange={(e) => setApiKey(e.target.value)}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormControl
|
||||
label="Terraform Cloud Workspace ID"
|
||||
errorText={workspacesIdErrorText}
|
||||
isError={workspacesIdErrorText !== "" ?? false}
|
||||
>
|
||||
<Input
|
||||
placeholder="Workspace Id"
|
||||
value={workspacesId}
|
||||
onChange={(e) => setWorkSpacesId(e.target.value)}
|
||||
/>
|
||||
</FormControl>
|
||||
<Button
|
||||
onClick={handleButtonClick}
|
||||
color="mineshaft"
|
||||
className="mt-4"
|
||||
colorSchema="primary"
|
||||
variant="outline_bg"
|
||||
className="mb-6 mt-2 ml-auto mr-6 w-min"
|
||||
isLoading={isLoading}
|
||||
>
|
||||
Connect to Terraform Cloud
|
||||
|
||||
@@ -1,8 +1,14 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import Head from "next/head";
|
||||
import Image from "next/image";
|
||||
import Link from "next/link";
|
||||
import { useRouter } from "next/router";
|
||||
import { faArrowUpRightFromSquare, faBookOpen } from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import queryString from "query-string";
|
||||
|
||||
import { useCreateIntegration } from "@app/hooks/api";
|
||||
import { IntegrationSyncBehavior } from "@app/hooks/api/integrations/types";
|
||||
|
||||
import {
|
||||
Button,
|
||||
@@ -19,6 +25,15 @@ import {
|
||||
} from "../../../hooks/api/integrationAuth";
|
||||
import { useGetWorkspaceById } from "../../../hooks/api/workspace";
|
||||
|
||||
const initialSyncBehaviors = [
|
||||
{
|
||||
label: "No Import - Overwrite all values in Terraform Cloud",
|
||||
value: IntegrationSyncBehavior.OVERWRITE_TARGET
|
||||
},
|
||||
{ label: "Import non-sensitive - Prefer values from Terraform Cloud", value: IntegrationSyncBehavior.PREFER_TARGET },
|
||||
{ label: "Import non-sensitive - Prefer values from Infisical", value: IntegrationSyncBehavior.PREFER_SOURCE }
|
||||
];
|
||||
|
||||
const variableTypes = [{ name: "env" }, { name: "terraform" }];
|
||||
|
||||
export default function TerraformCloudCreateIntegrationPage() {
|
||||
@@ -39,6 +54,7 @@ export default function TerraformCloudCreateIntegrationPage() {
|
||||
const [variableType, setVariableType] = useState("");
|
||||
const [variableTypeErrorText, setVariableTypeErrorText] = useState("");
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [initialSyncBehavior, setInitialSyncBehavior] = useState("prefer-source")
|
||||
|
||||
useEffect(() => {
|
||||
if (workspace) {
|
||||
@@ -78,7 +94,10 @@ export default function TerraformCloudCreateIntegrationPage() {
|
||||
)?.appId,
|
||||
sourceEnvironment: selectedSourceEnvironment,
|
||||
targetService: variableType,
|
||||
secretPath
|
||||
secretPath,
|
||||
metadata: {
|
||||
initialSyncBehavior
|
||||
}
|
||||
});
|
||||
|
||||
setIsLoading(false);
|
||||
@@ -95,9 +114,40 @@ export default function TerraformCloudCreateIntegrationPage() {
|
||||
integrationAuthApps &&
|
||||
targetApp ? (
|
||||
<div className="flex h-full w-full items-center justify-center">
|
||||
<Card className="max-w-md rounded-md p-8">
|
||||
<CardTitle className="text-center">Terraform Cloud Integration</CardTitle>
|
||||
<FormControl label="Project Environment" className="mt-4">
|
||||
<Head>
|
||||
<title>Create Terraform Cloud Integration</title>
|
||||
<link rel="icon" href="/infisical.ico" />
|
||||
</Head>
|
||||
<Card className="max-w-lg rounded-md border border-mineshaft-600">
|
||||
<CardTitle
|
||||
className="px-6 text-left text-xl"
|
||||
subTitle="Specify the encironment and path within Infisical that you want to push to which project in Terraform."
|
||||
>
|
||||
<div className="flex flex-row items-center">
|
||||
<div className="inline flex items-center">
|
||||
<Image
|
||||
src="/images/integrations/Terraform.png"
|
||||
height={35}
|
||||
width={35}
|
||||
alt="Terraform logo"
|
||||
/>
|
||||
</div>
|
||||
<span className="ml-1.5">Terraform Cloud Integration </span>
|
||||
<Link href="https://infisical.com/docs/integrations/cloud/terraform-cloud" passHref>
|
||||
<a target="_blank" rel="noopener noreferrer">
|
||||
<div className="ml-2 mb-1 inline-block cursor-default rounded-md bg-yellow/20 px-1.5 pb-[0.03rem] pt-[0.04rem] text-sm text-yellow opacity-80 hover:opacity-100">
|
||||
<FontAwesomeIcon icon={faBookOpen} className="mr-1.5" />
|
||||
Docs
|
||||
<FontAwesomeIcon
|
||||
icon={faArrowUpRightFromSquare}
|
||||
className="ml-1.5 mb-[0.07rem] text-xxs"
|
||||
/>
|
||||
</div>
|
||||
</a>
|
||||
</Link>
|
||||
</div>
|
||||
</CardTitle>
|
||||
<FormControl label="Project Environment" className="px-6">
|
||||
<Select
|
||||
value={selectedSourceEnvironment}
|
||||
onValueChange={(val) => setSelectedSourceEnvironment(val)}
|
||||
@@ -113,7 +163,7 @@ export default function TerraformCloudCreateIntegrationPage() {
|
||||
))}
|
||||
</Select>
|
||||
</FormControl>
|
||||
<FormControl label="Secrets Path">
|
||||
<FormControl label="Secrets Path" className="px-6">
|
||||
<Input
|
||||
value={secretPath}
|
||||
onChange={(evt) => setSecretPath(evt.target.value)}
|
||||
@@ -122,7 +172,7 @@ export default function TerraformCloudCreateIntegrationPage() {
|
||||
</FormControl>
|
||||
<FormControl
|
||||
label="Category"
|
||||
className="mt-4"
|
||||
className="px-6"
|
||||
errorText={variableTypeErrorText}
|
||||
isError={variableTypeErrorText !== "" ?? false}
|
||||
>
|
||||
@@ -138,7 +188,7 @@ export default function TerraformCloudCreateIntegrationPage() {
|
||||
))}
|
||||
</Select>
|
||||
</FormControl>
|
||||
<FormControl label="Terraform Cloud Project" className="mt-4">
|
||||
<FormControl label="Terraform Cloud Project" className="px-6">
|
||||
<Select
|
||||
value={targetApp}
|
||||
onValueChange={(val) => setTargetApp(val)}
|
||||
@@ -161,10 +211,22 @@ export default function TerraformCloudCreateIntegrationPage() {
|
||||
)}
|
||||
</Select>
|
||||
</FormControl>
|
||||
<FormControl label="Initial Sync Behavior" className="px-6">
|
||||
<Select value={initialSyncBehavior} onValueChange={(e) => setInitialSyncBehavior(e)} className="w-full border border-mineshaft-600">
|
||||
{initialSyncBehaviors.map((b) => {
|
||||
return (
|
||||
<SelectItem value={b.value} key={`sync-behavior-${b.value}`}>
|
||||
{b.label}
|
||||
</SelectItem>
|
||||
);
|
||||
})}
|
||||
</Select>
|
||||
</FormControl>
|
||||
<Button
|
||||
onClick={handleButtonClick}
|
||||
color="mineshaft"
|
||||
className="mt-4"
|
||||
colorSchema="primary"
|
||||
variant="outline_bg"
|
||||
className="mb-6 mt-2 ml-auto mr-6 w-min"
|
||||
isLoading={isLoading}
|
||||
isDisabled={integrationAuthApps.length === 0}
|
||||
>
|
||||
|
||||
+13
-1
@@ -74,7 +74,7 @@ export const IntegrationsSection = ({
|
||||
</div>
|
||||
)}
|
||||
{!isLoading && isBotActive && (
|
||||
<div className="flex flex-col space-y-4 p-6 pt-0">
|
||||
<div className="flex flex-col min-w-max space-y-4 p-6 pt-0">
|
||||
{integrations?.map((integration) => (
|
||||
<div
|
||||
className="max-w-8xl flex justify-between rounded-md border border-mineshaft-600 bg-mineshaft-800 p-3"
|
||||
@@ -128,6 +128,9 @@ export const IntegrationsSection = ({
|
||||
<FormLabel
|
||||
label={
|
||||
(integration.integration === "qovery" && integration?.scope) ||
|
||||
(integration.integration === "aws-secret-manager" && "Secret") ||
|
||||
(integration.integration === "aws-parameter-store" && "Path") ||
|
||||
(integration?.integration === "terraform-cloud" && "Project") ||
|
||||
(integration?.scope === "github-org" && "Organization") ||
|
||||
(["github-repo", "github-env"].includes(integration?.scope as string) &&
|
||||
"Repository") ||
|
||||
@@ -138,6 +141,7 @@ export const IntegrationsSection = ({
|
||||
{(integration.integration === "hashicorp-vault" &&
|
||||
`${integration.app} - path: ${integration.path}`) ||
|
||||
(integration.scope === "github-org" && `${integration.owner}`) ||
|
||||
(integration.integration === "aws-parameter-store" && `${integration.path}`) ||
|
||||
(integration.scope?.startsWith("github-") &&
|
||||
`${integration.owner}/${integration.app}`) ||
|
||||
integration.app}
|
||||
@@ -165,6 +169,14 @@ export const IntegrationsSection = ({
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{integration.integration === "terraform-cloud" && integration.targetService && (
|
||||
<div className="ml-2">
|
||||
<FormLabel label="Category" />
|
||||
<div className="rounded-md border border-mineshaft-700 bg-mineshaft-900 px-3 py-2 font-inter text-sm text-bunker-200">
|
||||
{integration.targetService}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{(integration.integration === "checkly" ||
|
||||
integration.integration === "github") && (
|
||||
<div className="ml-2">
|
||||
|
||||
+363
@@ -0,0 +1,363 @@
|
||||
import { Controller, useForm } from "react-hook-form";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import ms from "ms";
|
||||
import { z } from "zod";
|
||||
|
||||
import { TtlFormLabel } from "@app/components/features";
|
||||
import { createNotification } from "@app/components/notifications";
|
||||
import {
|
||||
Accordion,
|
||||
AccordionContent,
|
||||
AccordionItem,
|
||||
AccordionTrigger,
|
||||
Button,
|
||||
FormControl,
|
||||
Input,
|
||||
SecretInput,
|
||||
TextArea
|
||||
} from "@app/components/v2";
|
||||
import { useCreateDynamicSecret } from "@app/hooks/api";
|
||||
import { DynamicSecretProviders } from "@app/hooks/api/dynamicSecret/types";
|
||||
|
||||
const formSchema = z.object({
|
||||
provider: z.object({
|
||||
host: z.string().toLowerCase().min(1),
|
||||
port: z.coerce.number(),
|
||||
keyspace: z.string().optional(),
|
||||
localDataCenter: z.string().min(1),
|
||||
username: z.string().min(1),
|
||||
password: z.string().min(1),
|
||||
creationStatement: z.string().min(1),
|
||||
revocationStatement: z.string().min(1),
|
||||
renewStatement: z.string().optional(),
|
||||
ca: z.string().optional()
|
||||
}),
|
||||
defaultTTL: z.string().superRefine((val, ctx) => {
|
||||
const valMs = ms(val);
|
||||
if (valMs < 60 * 1000)
|
||||
ctx.addIssue({ code: z.ZodIssueCode.custom, message: "TTL must be a greater than 1min" });
|
||||
// a day
|
||||
if (valMs > 24 * 60 * 60 * 1000)
|
||||
ctx.addIssue({ code: z.ZodIssueCode.custom, message: "TTL must be less than a day" });
|
||||
}),
|
||||
maxTTL: z
|
||||
.string()
|
||||
.optional()
|
||||
.superRefine((val, ctx) => {
|
||||
if (!val) return;
|
||||
const valMs = ms(val);
|
||||
if (valMs < 60 * 1000)
|
||||
ctx.addIssue({ code: z.ZodIssueCode.custom, message: "TTL must be a greater than 1min" });
|
||||
// a day
|
||||
if (valMs > 24 * 60 * 60 * 1000)
|
||||
ctx.addIssue({ code: z.ZodIssueCode.custom, message: "TTL must be less than a day" });
|
||||
}),
|
||||
name: z.string().refine((val) => val.toLowerCase() === val, "Must be lowercase")
|
||||
});
|
||||
type TForm = z.infer<typeof formSchema>;
|
||||
|
||||
type Props = {
|
||||
onCompleted: () => void;
|
||||
onCancel: () => void;
|
||||
secretPath: string;
|
||||
projectSlug: string;
|
||||
environment: string;
|
||||
};
|
||||
|
||||
const getSqlStatements = () => {
|
||||
return {
|
||||
creationStatement:
|
||||
"CREATE ROLE '{{username}}' WITH PASSWORD = '{{password}}' AND LOGIN=true;\nGRANT ALL PERMISSIONS ON ALL KEYSPACES TO '{{username}}';",
|
||||
renewStatement: "",
|
||||
revocationStatement: 'DROP ROLE "{{username}}";'
|
||||
};
|
||||
};
|
||||
|
||||
export const CassandraInputForm = ({
|
||||
onCompleted,
|
||||
onCancel,
|
||||
environment,
|
||||
secretPath,
|
||||
projectSlug
|
||||
}: Props) => {
|
||||
const {
|
||||
control,
|
||||
formState: { isSubmitting },
|
||||
handleSubmit
|
||||
} = useForm<TForm>({
|
||||
resolver: zodResolver(formSchema),
|
||||
defaultValues: {
|
||||
provider: getSqlStatements()
|
||||
}
|
||||
});
|
||||
|
||||
const createDynamicSecret = useCreateDynamicSecret();
|
||||
|
||||
const handleCreateDynamicSecret = async ({ name, maxTTL, provider, defaultTTL }: TForm) => {
|
||||
// wait till previous request is finished
|
||||
if (createDynamicSecret.isLoading) return;
|
||||
try {
|
||||
await createDynamicSecret.mutateAsync({
|
||||
provider: { type: DynamicSecretProviders.Cassandra, inputs: provider },
|
||||
maxTTL,
|
||||
name,
|
||||
path: secretPath,
|
||||
defaultTTL,
|
||||
projectSlug,
|
||||
environmentSlug: environment
|
||||
});
|
||||
onCompleted();
|
||||
} catch (err) {
|
||||
createNotification({
|
||||
type: "error",
|
||||
text: "Failed to create dynamic secret"
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div>
|
||||
<form onSubmit={handleSubmit(handleCreateDynamicSecret)} autoComplete="off">
|
||||
<div>
|
||||
<div className="flex items-center space-x-2">
|
||||
<div className="flex-grow">
|
||||
<Controller
|
||||
control={control}
|
||||
defaultValue=""
|
||||
name="name"
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label="Secret Name"
|
||||
isError={Boolean(error)}
|
||||
errorText={error?.message}
|
||||
>
|
||||
<Input {...field} placeholder="dynamic-postgres" />
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
<div className="w-32">
|
||||
<Controller
|
||||
control={control}
|
||||
name="defaultTTL"
|
||||
defaultValue="1h"
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label={<TtlFormLabel label="Default TTL" />}
|
||||
isError={Boolean(error?.message)}
|
||||
errorText={error?.message}
|
||||
>
|
||||
<Input {...field} />
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
<div className="w-32">
|
||||
<Controller
|
||||
control={control}
|
||||
name="maxTTL"
|
||||
defaultValue="24h"
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label={<TtlFormLabel label="Max TTL" />}
|
||||
isError={Boolean(error?.message)}
|
||||
errorText={error?.message}
|
||||
>
|
||||
<Input {...field} />
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<div className="mb-4 mt-4 border-b border-mineshaft-500 pb-2 pl-1 font-medium text-mineshaft-200">
|
||||
Configuration
|
||||
</div>
|
||||
<div className="flex flex-col">
|
||||
<div className="flex items-center space-x-2">
|
||||
<Controller
|
||||
control={control}
|
||||
name="provider.host"
|
||||
defaultValue=""
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label="Host"
|
||||
className="flex-grow"
|
||||
isError={Boolean(error?.message)}
|
||||
errorText={error?.message}
|
||||
>
|
||||
<Input {...field} placeholder="host1,host2" />
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
<Controller
|
||||
control={control}
|
||||
name="provider.port"
|
||||
defaultValue={9042}
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label="Port"
|
||||
isError={Boolean(error?.message)}
|
||||
errorText={error?.message}
|
||||
>
|
||||
<Input {...field} type="number" />
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
<Controller
|
||||
control={control}
|
||||
name="provider.localDataCenter"
|
||||
defaultValue="datacenter1"
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label="Local Data Center"
|
||||
isError={Boolean(error?.message)}
|
||||
errorText={error?.message}
|
||||
>
|
||||
<Input {...field} />
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
<div className="flex items-center space-x-2">
|
||||
<Controller
|
||||
control={control}
|
||||
name="provider.username"
|
||||
defaultValue=""
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label="User"
|
||||
isError={Boolean(error?.message)}
|
||||
errorText={error?.message}
|
||||
>
|
||||
<Input {...field} autoComplete="off" />
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
<Controller
|
||||
control={control}
|
||||
name="provider.password"
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label="Password"
|
||||
isError={Boolean(error?.message)}
|
||||
errorText={error?.message}
|
||||
>
|
||||
<Input {...field} type="password" autoComplete="new-password" />
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
<Controller
|
||||
control={control}
|
||||
name="provider.keyspace"
|
||||
defaultValue=""
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label="Keyspace"
|
||||
isError={Boolean(error?.message)}
|
||||
isOptional
|
||||
errorText={error?.message}
|
||||
>
|
||||
<Input {...field} />
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<Controller
|
||||
control={control}
|
||||
name="provider.ca"
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
isOptional
|
||||
label="CA(SSL)"
|
||||
isError={Boolean(error?.message)}
|
||||
errorText={error?.message}
|
||||
>
|
||||
<SecretInput
|
||||
{...field}
|
||||
containerClassName="text-bunker-300 hover:border-primary-400/50 border border-mineshaft-600 bg-mineshaft-900 px-2 py-1.5"
|
||||
/>
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
<Accordion type="single" collapsible className="mb-2 w-full bg-mineshaft-700">
|
||||
<AccordionItem value="advance-statements">
|
||||
<AccordionTrigger>Modify CQL Statements</AccordionTrigger>
|
||||
<AccordionContent>
|
||||
<Controller
|
||||
control={control}
|
||||
name="provider.creationStatement"
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label="Creation Statement"
|
||||
isError={Boolean(error?.message)}
|
||||
errorText={error?.message}
|
||||
helperText="variables: keyspace. username, password and expiration are dynamically provisioned"
|
||||
>
|
||||
<TextArea
|
||||
{...field}
|
||||
reSize="none"
|
||||
rows={3}
|
||||
className="border-mineshaft-600 bg-mineshaft-900 text-sm"
|
||||
/>
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
<Controller
|
||||
control={control}
|
||||
name="provider.revocationStatement"
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label="Revocation Statement"
|
||||
isError={Boolean(error?.message)}
|
||||
errorText={error?.message}
|
||||
helperText="variables: keyspace, username is dynamically provisioned"
|
||||
>
|
||||
<TextArea
|
||||
{...field}
|
||||
reSize="none"
|
||||
rows={3}
|
||||
className="border-mineshaft-600 bg-mineshaft-900 text-sm"
|
||||
/>
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
<Controller
|
||||
control={control}
|
||||
name="provider.renewStatement"
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label="Renew Statement"
|
||||
helperText="variables: keyspace, username and expiration are dynamically provisioned"
|
||||
isError={Boolean(error?.message)}
|
||||
errorText={error?.message}
|
||||
>
|
||||
<TextArea
|
||||
{...field}
|
||||
reSize="none"
|
||||
rows={3}
|
||||
className="border-mineshaft-600 bg-mineshaft-900 text-sm"
|
||||
/>
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
</AccordionContent>
|
||||
</AccordionItem>
|
||||
</Accordion>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="mt-4 flex items-center space-x-4">
|
||||
<Button type="submit" isLoading={isSubmitting}>
|
||||
Submit
|
||||
</Button>
|
||||
<Button variant="outline_bg" onClick={onCancel}>
|
||||
Cancel
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
+52
-21
@@ -6,6 +6,7 @@ import { AnimatePresence, motion } from "framer-motion";
|
||||
import { Modal, ModalContent } from "@app/components/v2";
|
||||
import { DynamicSecretProviders } from "@app/hooks/api/dynamicSecret/types";
|
||||
|
||||
import { CassandraInputForm } from "./CassandraInputForm";
|
||||
import { SqlDatabaseInputForm } from "./SqlDatabaseInputForm";
|
||||
|
||||
type Props = {
|
||||
@@ -21,6 +22,19 @@ enum WizardSteps {
|
||||
ProviderInputs = "provider-inputs"
|
||||
}
|
||||
|
||||
const DYNAMIC_SECRET_LIST = [
|
||||
{
|
||||
icon: faDatabase,
|
||||
provider: DynamicSecretProviders.SqlDatabase,
|
||||
title: "SQL\nDatabase"
|
||||
},
|
||||
{
|
||||
icon: faDatabase,
|
||||
provider: DynamicSecretProviders.Cassandra,
|
||||
title: "Cassandra"
|
||||
}
|
||||
];
|
||||
|
||||
export const CreateDynamicSecretForm = ({
|
||||
isOpen,
|
||||
onToggle,
|
||||
@@ -55,35 +69,34 @@ export const CreateDynamicSecretForm = ({
|
||||
>
|
||||
<div className="mb-4 text-mineshaft-300">Select a service to connect to:</div>
|
||||
<div className="flex items-center space-x-4">
|
||||
<div
|
||||
className="flex h-32 w-32 cursor-pointer flex-col items-center space-y-4 rounded border border-mineshaft-500 bg-bunker-600 p-6 transition-all hover:border-primary/70 hover:bg-primary/10 hover:text-white"
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
onClick={() => {
|
||||
setSelectedProvider(DynamicSecretProviders.SqlDatabase);
|
||||
setWizardStep(WizardSteps.ProviderInputs);
|
||||
}}
|
||||
onKeyDown={(evt) => {
|
||||
if (evt.key === "Enter") {
|
||||
setSelectedProvider(DynamicSecretProviders.SqlDatabase);
|
||||
{DYNAMIC_SECRET_LIST.map(({ icon, provider, title }) => (
|
||||
<div
|
||||
key={`dynamic-secret-provider-${provider}`}
|
||||
className="flex h-32 w-32 cursor-pointer flex-col items-center space-y-4 rounded border border-mineshaft-500 bg-bunker-600 p-6 transition-all hover:border-primary/70 hover:bg-primary/10 hover:text-white"
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
onClick={() => {
|
||||
setSelectedProvider(provider);
|
||||
setWizardStep(WizardSteps.ProviderInputs);
|
||||
}
|
||||
}}
|
||||
>
|
||||
<FontAwesomeIcon icon={faDatabase} size="lg" />
|
||||
<div className="text-center text-sm">
|
||||
SQL
|
||||
<br />
|
||||
Database
|
||||
}}
|
||||
onKeyDown={(evt) => {
|
||||
if (evt.key === "Enter") {
|
||||
setSelectedProvider(provider);
|
||||
setWizardStep(WizardSteps.ProviderInputs);
|
||||
}
|
||||
}}
|
||||
>
|
||||
<FontAwesomeIcon icon={icon} size="lg" />
|
||||
<div className="whitespace-pre-wrap text-center text-sm">{title}</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</motion.div>
|
||||
)}
|
||||
{wizardStep === WizardSteps.ProviderInputs &&
|
||||
selectedProvider === DynamicSecretProviders.SqlDatabase && (
|
||||
<motion.div
|
||||
key="select-input-step"
|
||||
key="dynamic-sql-step"
|
||||
transition={{ duration: 0.1 }}
|
||||
initial={{ opacity: 0, translateX: 30 }}
|
||||
animate={{ opacity: 1, translateX: 0 }}
|
||||
@@ -98,6 +111,24 @@ export const CreateDynamicSecretForm = ({
|
||||
/>
|
||||
</motion.div>
|
||||
)}
|
||||
{wizardStep === WizardSteps.ProviderInputs &&
|
||||
selectedProvider === DynamicSecretProviders.Cassandra && (
|
||||
<motion.div
|
||||
key="dynamic-cassandra-step"
|
||||
transition={{ duration: 0.1 }}
|
||||
initial={{ opacity: 0, translateX: 30 }}
|
||||
animate={{ opacity: 1, translateX: 0 }}
|
||||
exit={{ opacity: 0, translateX: -30 }}
|
||||
>
|
||||
<CassandraInputForm
|
||||
onCompleted={handleFormReset}
|
||||
onCancel={handleFormReset}
|
||||
projectSlug={projectSlug}
|
||||
secretPath={secretPath}
|
||||
environment={environment}
|
||||
/>
|
||||
</motion.div>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
</ModalContent>
|
||||
</Modal>
|
||||
|
||||
+3
-3
@@ -141,7 +141,7 @@ export const SqlDatabaseInputForm = ({
|
||||
|
||||
return (
|
||||
<div>
|
||||
<form onSubmit={handleSubmit(handleCreateDynamicSecret)}>
|
||||
<form onSubmit={handleSubmit(handleCreateDynamicSecret)} autoComplete="off">
|
||||
<div>
|
||||
<div className="flex items-center space-x-2">
|
||||
<div className="flex-grow">
|
||||
@@ -265,7 +265,7 @@ export const SqlDatabaseInputForm = ({
|
||||
isError={Boolean(error?.message)}
|
||||
errorText={error?.message}
|
||||
>
|
||||
<Input {...field} />
|
||||
<Input {...field} autoComplete="off" />
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
@@ -278,7 +278,7 @@ export const SqlDatabaseInputForm = ({
|
||||
isError={Boolean(error?.message)}
|
||||
errorText={error?.message}
|
||||
>
|
||||
<Input {...field} type="password" />
|
||||
<Input {...field} type="password" autoComplete="new-password" />
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
|
||||
+4
-2
@@ -55,7 +55,10 @@ const OutputDisplay = ({
|
||||
|
||||
const renderOutputForm = (provider: DynamicSecretProviders, data: unknown) => {
|
||||
const { DB_PASSWORD, DB_USERNAME } = data as { DB_USERNAME: string; DB_PASSWORD: string };
|
||||
if (provider === DynamicSecretProviders.SqlDatabase) {
|
||||
if (
|
||||
provider === DynamicSecretProviders.SqlDatabase ||
|
||||
provider === DynamicSecretProviders.Cassandra
|
||||
) {
|
||||
return (
|
||||
<div>
|
||||
<OutputDisplay label="Database User" value={DB_USERNAME} />
|
||||
@@ -102,7 +105,6 @@ export const CreateDynamicSecretLease = ({
|
||||
ttl: "1h"
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
const createDynamicSecretLease = useCreateDynamicSecretLease();
|
||||
|
||||
|
||||
+374
@@ -0,0 +1,374 @@
|
||||
import { Controller, useForm } from "react-hook-form";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import ms from "ms";
|
||||
import { z } from "zod";
|
||||
|
||||
import { TtlFormLabel } from "@app/components/features";
|
||||
import { createNotification } from "@app/components/notifications";
|
||||
import {
|
||||
Accordion,
|
||||
AccordionContent,
|
||||
AccordionItem,
|
||||
AccordionTrigger,
|
||||
Button,
|
||||
FormControl,
|
||||
Input,
|
||||
SecretInput,
|
||||
TextArea
|
||||
} from "@app/components/v2";
|
||||
import { useUpdateDynamicSecret } from "@app/hooks/api";
|
||||
import { TDynamicSecret } from "@app/hooks/api/dynamicSecret/types";
|
||||
|
||||
const formSchema = z.object({
|
||||
inputs: z
|
||||
.object({
|
||||
host: z.string().toLowerCase().min(1),
|
||||
port: z.coerce.number(),
|
||||
keyspace: z.string().optional(),
|
||||
localDataCenter: z.string().min(1),
|
||||
username: z.string().min(1),
|
||||
password: z.string().min(1),
|
||||
creationStatement: z.string().min(1),
|
||||
revocationStatement: z.string().min(1),
|
||||
renewStatement: z.string().optional(),
|
||||
ca: z.string().optional()
|
||||
})
|
||||
.partial(),
|
||||
defaultTTL: z.string().superRefine((val, ctx) => {
|
||||
const valMs = ms(val);
|
||||
if (valMs < 60 * 1000)
|
||||
ctx.addIssue({ code: z.ZodIssueCode.custom, message: "TTL must be a greater than 1min" });
|
||||
// a day
|
||||
if (valMs > 24 * 60 * 60 * 1000)
|
||||
ctx.addIssue({ code: z.ZodIssueCode.custom, message: "TTL must be less than a day" });
|
||||
}),
|
||||
maxTTL: z
|
||||
.string()
|
||||
.optional()
|
||||
.superRefine((val, ctx) => {
|
||||
if (!val) return;
|
||||
const valMs = ms(val);
|
||||
if (valMs < 60 * 1000)
|
||||
ctx.addIssue({ code: z.ZodIssueCode.custom, message: "TTL must be a greater than 1min" });
|
||||
// a day
|
||||
if (valMs > 24 * 60 * 60 * 1000)
|
||||
ctx.addIssue({ code: z.ZodIssueCode.custom, message: "TTL must be less than a day" });
|
||||
})
|
||||
.nullable(),
|
||||
newName: z
|
||||
.string()
|
||||
.refine((val) => val.toLowerCase() === val, "Must be lowercase")
|
||||
.optional()
|
||||
});
|
||||
type TForm = z.infer<typeof formSchema>;
|
||||
|
||||
type Props = {
|
||||
onClose: () => void;
|
||||
dynamicSecret: TDynamicSecret & { inputs: unknown };
|
||||
secretPath: string;
|
||||
environment: string;
|
||||
projectSlug: string;
|
||||
};
|
||||
|
||||
export const EditDynamicSecretCassandraForm = ({
|
||||
onClose,
|
||||
dynamicSecret,
|
||||
environment,
|
||||
secretPath,
|
||||
projectSlug
|
||||
}: Props) => {
|
||||
const {
|
||||
control,
|
||||
formState: { isSubmitting },
|
||||
handleSubmit
|
||||
} = useForm<TForm>({
|
||||
resolver: zodResolver(formSchema),
|
||||
values: {
|
||||
defaultTTL: dynamicSecret.defaultTTL,
|
||||
maxTTL: dynamicSecret.maxTTL,
|
||||
newName: dynamicSecret.name,
|
||||
inputs: {
|
||||
...(dynamicSecret.inputs as TForm["inputs"])
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
const updateDynamicSecret = useUpdateDynamicSecret();
|
||||
|
||||
const handleUpdateDynamicSecret = async ({ inputs, maxTTL, defaultTTL, 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: {
|
||||
maxTTL: maxTTL || undefined,
|
||||
defaultTTL,
|
||||
inputs,
|
||||
newName: newName === dynamicSecret.name ? undefined : newName
|
||||
}
|
||||
});
|
||||
onClose();
|
||||
createNotification({
|
||||
type: "success",
|
||||
text: "Successfully updated dynamic secret"
|
||||
});
|
||||
} catch (err) {
|
||||
createNotification({
|
||||
type: "error",
|
||||
text: "Failed to update dynamic secret"
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div>
|
||||
<form onSubmit={handleSubmit(handleUpdateDynamicSecret)} autoComplete="off">
|
||||
<div className="flex items-center space-x-2">
|
||||
<div className="flex-grow">
|
||||
<Controller
|
||||
control={control}
|
||||
name="newName"
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label="Secret Name"
|
||||
isError={Boolean(error)}
|
||||
errorText={error?.message}
|
||||
>
|
||||
<Input {...field} placeholder="DYN-1" />
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
<div className="w-32">
|
||||
<Controller
|
||||
control={control}
|
||||
name="defaultTTL"
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label={<TtlFormLabel label="Default TTL" />}
|
||||
isError={Boolean(error?.message)}
|
||||
errorText={error?.message}
|
||||
>
|
||||
<Input {...field} />
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
<div className="w-32">
|
||||
<Controller
|
||||
control={control}
|
||||
name="maxTTL"
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label={<TtlFormLabel label="Max TTL" />}
|
||||
isError={Boolean(error?.message)}
|
||||
errorText={error?.message}
|
||||
>
|
||||
<Input {...field} value={field.value || ""} />
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<div className="mb-4 border-b border-b-mineshaft-600 pb-2">Configuration</div>
|
||||
<div className="flex flex-col">
|
||||
<div className="flex items-center space-x-2">
|
||||
<Controller
|
||||
control={control}
|
||||
name="inputs.host"
|
||||
defaultValue=""
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label="Host"
|
||||
className="flex-grow"
|
||||
isError={Boolean(error?.message)}
|
||||
errorText={error?.message}
|
||||
>
|
||||
<Input {...field} />
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
<Controller
|
||||
control={control}
|
||||
name="inputs.port"
|
||||
defaultValue={9042}
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label="Port"
|
||||
isError={Boolean(error?.message)}
|
||||
errorText={error?.message}
|
||||
>
|
||||
<Input
|
||||
{...field}
|
||||
type="number"
|
||||
onChange={(el) => field.onChange(parseInt(el.target.value, 10))}
|
||||
/>
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
<Controller
|
||||
control={control}
|
||||
name="inputs.localDataCenter"
|
||||
defaultValue="datacenter1"
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label="Local Data Center"
|
||||
isError={Boolean(error?.message)}
|
||||
errorText={error?.message}
|
||||
>
|
||||
<Input {...field} />
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
<div className="flex items-center space-x-2">
|
||||
<Controller
|
||||
control={control}
|
||||
name="inputs.username"
|
||||
defaultValue=""
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label="User"
|
||||
isError={Boolean(error?.message)}
|
||||
errorText={error?.message}
|
||||
>
|
||||
<Input {...field} autoComplete="off" />
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
<Controller
|
||||
control={control}
|
||||
name="inputs.password"
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label="Password"
|
||||
isError={Boolean(error?.message)}
|
||||
errorText={error?.message}
|
||||
>
|
||||
<Input {...field} type="password" autoComplete="new-password" />
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
<Controller
|
||||
control={control}
|
||||
name="inputs.keyspace"
|
||||
defaultValue="default"
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label="Keyspace"
|
||||
isOptional
|
||||
isError={Boolean(error?.message)}
|
||||
errorText={error?.message}
|
||||
>
|
||||
<Input {...field} />
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<Controller
|
||||
control={control}
|
||||
name="inputs.ca"
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
isOptional
|
||||
label="CA(SSL)"
|
||||
isError={Boolean(error?.message)}
|
||||
errorText={error?.message}
|
||||
>
|
||||
<SecretInput
|
||||
{...field}
|
||||
containerClassName="text-bunker-300 hover:border-primary-400/50 border border-mineshaft-600 bg-mineshaft-900 px-2 py-1.5"
|
||||
/>
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
<Accordion type="multiple" className="w-full bg-mineshaft-700">
|
||||
<AccordionItem value="modify-sql-statement">
|
||||
<AccordionTrigger>Modify CQL Statements</AccordionTrigger>
|
||||
<AccordionContent>
|
||||
<Controller
|
||||
control={control}
|
||||
name="inputs.creationStatement"
|
||||
defaultValue={
|
||||
"CREATE ROLE '{{username}}' WITH PASSWORD '{{password}}' AND LOGIN=true;\nGRANT ALL PERMISSIONS ON ALL KEYSPACES TO '{{username}}';"
|
||||
}
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label="Creation Statement"
|
||||
isError={Boolean(error?.message)}
|
||||
errorText={error?.message}
|
||||
helperText="variables: keyspace. username, password and expiration are dynamically provisioned"
|
||||
>
|
||||
<TextArea
|
||||
{...field}
|
||||
reSize="none"
|
||||
rows={3}
|
||||
className="border-mineshaft-600 bg-mineshaft-900 text-sm"
|
||||
/>
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
<Controller
|
||||
control={control}
|
||||
name="inputs.revocationStatement"
|
||||
defaultValue='DROP ROLE "{{username}}";'
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label="Revocation Statement"
|
||||
isError={Boolean(error?.message)}
|
||||
errorText={error?.message}
|
||||
helperText="variables: keyspace, username is dynamically provisioned"
|
||||
>
|
||||
<TextArea
|
||||
{...field}
|
||||
reSize="none"
|
||||
rows={3}
|
||||
className="border-mineshaft-600 bg-mineshaft-900 text-sm"
|
||||
/>
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
<Controller
|
||||
control={control}
|
||||
name="inputs.renewStatement"
|
||||
defaultValue=""
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label="Renew Statement"
|
||||
helperText="variables: keyspace, username and expiration are dynamically provisioned"
|
||||
isError={Boolean(error?.message)}
|
||||
errorText={error?.message}
|
||||
>
|
||||
<TextArea
|
||||
{...field}
|
||||
reSize="none"
|
||||
rows={3}
|
||||
className="border-mineshaft-600 bg-mineshaft-900 text-sm"
|
||||
/>
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
</AccordionContent>
|
||||
</AccordionItem>
|
||||
</Accordion>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="mt-4 flex items-center space-x-4">
|
||||
<Button type="submit" isLoading={isSubmitting}>
|
||||
Save
|
||||
</Button>
|
||||
<Button variant="outline_bg" onClick={onClose}>
|
||||
Cancel
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
+18
@@ -4,6 +4,7 @@ import { Spinner } from "@app/components/v2";
|
||||
import { useGetDynamicSecretDetails } from "@app/hooks/api";
|
||||
import { DynamicSecretProviders } from "@app/hooks/api/dynamicSecret/types";
|
||||
|
||||
import { EditDynamicSecretCassandraForm } from "./EditDynamicSecretCassandraForm";
|
||||
import { EditDynamicSecretSqlProviderForm } from "./EditDynamicSecretSqlProviderForm";
|
||||
|
||||
type Props = {
|
||||
@@ -56,6 +57,23 @@ export const EditDynamicSecretForm = ({
|
||||
/>
|
||||
</motion.div>
|
||||
)}
|
||||
{dynamicSecretDetails?.type === DynamicSecretProviders.Cassandra && (
|
||||
<motion.div
|
||||
key="cassandra-provider-edit"
|
||||
transition={{ duration: 0.1 }}
|
||||
initial={{ opacity: 0, translateX: 30 }}
|
||||
animate={{ opacity: 1, translateX: 0 }}
|
||||
exit={{ opacity: 0, translateX: -30 }}
|
||||
>
|
||||
<EditDynamicSecretCassandraForm
|
||||
onClose={onClose}
|
||||
projectSlug={projectSlug}
|
||||
secretPath={secretPath}
|
||||
dynamicSecret={dynamicSecretDetails}
|
||||
environment={environment}
|
||||
/>
|
||||
</motion.div>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
);
|
||||
};
|
||||
|
||||
+3
-3
@@ -128,7 +128,7 @@ export const EditDynamicSecretSqlProviderForm = ({
|
||||
|
||||
return (
|
||||
<div>
|
||||
<form onSubmit={handleSubmit(handleUpdateDynamicSecret)}>
|
||||
<form onSubmit={handleSubmit(handleUpdateDynamicSecret)} autoComplete="off">
|
||||
<div className="flex items-center space-x-2">
|
||||
<div className="flex-grow">
|
||||
<Controller
|
||||
@@ -244,7 +244,7 @@ export const EditDynamicSecretSqlProviderForm = ({
|
||||
isError={Boolean(error?.message)}
|
||||
errorText={error?.message}
|
||||
>
|
||||
<Input {...field} />
|
||||
<Input {...field} autoComplete="off" />
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
@@ -257,7 +257,7 @@ export const EditDynamicSecretSqlProviderForm = ({
|
||||
isError={Boolean(error?.message)}
|
||||
errorText={error?.message}
|
||||
>
|
||||
<Input {...field} type="password" />
|
||||
<Input {...field} type="password" autoComplete="new-password" />
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
|
||||
Reference in New Issue
Block a user