mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-10-12 17:29:13 +00:00
feat: completed migration of all integrations pages
This commit is contained in:
@@ -67,7 +67,189 @@ export const ROUTE_PATHS = Object.freeze({
|
|||||||
IntegrationDetailsByIDPage: setRoute(
|
IntegrationDetailsByIDPage: setRoute(
|
||||||
"/secret-manager/$projectId/integrations/$integrationId",
|
"/secret-manager/$projectId/integrations/$integrationId",
|
||||||
"/_authenticate/_inject-org-details/secret-manager/$projectId/_secret-manager-layout/integrations/$integrationId"
|
"/_authenticate/_inject-org-details/secret-manager/$projectId/_secret-manager-layout/integrations/$integrationId"
|
||||||
|
),
|
||||||
|
Integratons: {
|
||||||
|
SelectIntegrationAuth: setRoute(
|
||||||
|
"/secret-manager/$projectId/integrations/select-integration-auth",
|
||||||
|
"/_authenticate/_inject-org-details/secret-manager/$projectId/_secret-manager-layout/integrations/select-integration-auth"
|
||||||
|
),
|
||||||
|
HerokuOauthCallbackPage: setRoute(
|
||||||
|
"/secret-manager/$projectId/integrations/heroku/oauth2/callback",
|
||||||
|
"/_authenticate/_inject-org-details/secret-manager/$projectId/_secret-manager-layout/integrations/heroku/oauth2/callback"
|
||||||
|
),
|
||||||
|
HerokuConfigurePage: setRoute(
|
||||||
|
"/secret-manager/$projectId/integrations/heroku/create",
|
||||||
|
"/_authenticate/_inject-org-details/secret-manager/$projectId/_secret-manager-layout/integrations/heroku/create"
|
||||||
|
),
|
||||||
|
AwsParameterStoreConfigurePage: setRoute(
|
||||||
|
"/secret-manager/$projectId/integrations/aws-parameter-store/create",
|
||||||
|
"/_authenticate/_inject-org-details/secret-manager/$projectId/_secret-manager-layout/integrations/aws-parameter-store/create"
|
||||||
|
),
|
||||||
|
AwsSecretManagerConfigurePage: setRoute(
|
||||||
|
"/secret-manager/$projectId/integrations/aws-secret-manager/create",
|
||||||
|
"/_authenticate/_inject-org-details/secret-manager/$projectId/_secret-manager-layout/integrations/aws-secret-manager/create"
|
||||||
|
),
|
||||||
|
AzureAppConfigurationsOauthCallbackPage: setRoute(
|
||||||
|
"/secret-manager/$projectId/integrations/azure-app-configuration/oauth2/callback",
|
||||||
|
"/_authenticate/_inject-org-details/secret-manager/$projectId/_secret-manager-layout/integrations/azure-app-configuration/oauth2/callback"
|
||||||
|
),
|
||||||
|
AzureAppConfigurationsConfigurePage: setRoute(
|
||||||
|
"/secret-manager/$projectId/integrations/azure-app-configuration/create",
|
||||||
|
"/_authenticate/_inject-org-details/secret-manager/$projectId/_secret-manager-layout/integrations/azure-app-configuration/create"
|
||||||
|
),
|
||||||
|
AzureDevopsConfigurePage: setRoute(
|
||||||
|
"/secret-manager/$projectId/integrations/azure-devops/create",
|
||||||
|
"/_authenticate/_inject-org-details/secret-manager/$projectId/_secret-manager-layout/integrations/azure-devops/create"
|
||||||
|
),
|
||||||
|
AzureKeyVaultOauthCallbackPage: setRoute(
|
||||||
|
"/secret-manager/$projectId/integrations/azure-key-vault/oauth2/callback",
|
||||||
|
"/_authenticate/_inject-org-details/secret-manager/$projectId/_secret-manager-layout/integrations/azure-key-vault/oauth2/callback"
|
||||||
|
),
|
||||||
|
AzureKeyVaultConfigurePage: setRoute(
|
||||||
|
"/secret-manager/$projectId/integrations/azure-key-vault/create",
|
||||||
|
"/_authenticate/_inject-org-details/secret-manager/$projectId/_secret-manager-layout/integrations/azure-key-vault/create"
|
||||||
|
),
|
||||||
|
BitbucketOauthCallbackPage: setRoute(
|
||||||
|
"/secret-manager/$projectId/integrations/bitbucket/oauth2/callback",
|
||||||
|
"/_authenticate/_inject-org-details/secret-manager/$projectId/_secret-manager-layout/integrations/bitbucket/oauth2/callback"
|
||||||
|
),
|
||||||
|
BitbucketConfigurePage: setRoute(
|
||||||
|
"/secret-manager/$projectId/integrations/bitbucket/create",
|
||||||
|
"/_authenticate/_inject-org-details/secret-manager/$projectId/_secret-manager-layout/integrations/bitbucket/create"
|
||||||
|
),
|
||||||
|
ChecklyConfigurePage: setRoute(
|
||||||
|
"/secret-manager/$projectId/integrations/checkly/create",
|
||||||
|
"/_authenticate/_inject-org-details/secret-manager/$projectId/_secret-manager-layout/integrations/checkly/create"
|
||||||
|
),
|
||||||
|
CircleConfigurePage: setRoute(
|
||||||
|
"/secret-manager/$projectId/integrations/circleci/create",
|
||||||
|
"/_authenticate/_inject-org-details/secret-manager/$projectId/_secret-manager-layout/integrations/circleci/create"
|
||||||
|
),
|
||||||
|
CloudflarePagesConfigurePage: setRoute(
|
||||||
|
"/secret-manager/$projectId/integrations/cloudflare-pages/create",
|
||||||
|
"/_authenticate/_inject-org-details/secret-manager/$projectId/_secret-manager-layout/integrations/cloudflare-pages/create"
|
||||||
|
),
|
||||||
|
DigitalOceanAppPlatformConfigurePage: setRoute(
|
||||||
|
"/secret-manager/$projectId/integrations/digital-ocean-app-platform/create",
|
||||||
|
"/_authenticate/_inject-org-details/secret-manager/$projectId/_secret-manager-layout/integrations/digital-ocean-app-platform/create"
|
||||||
|
),
|
||||||
|
CloudflareWorkersConfigurePage: setRoute(
|
||||||
|
"/secret-manager/$projectId/integrations/cloudflare-workers/create",
|
||||||
|
"/_authenticate/_inject-org-details/secret-manager/$projectId/_secret-manager-layout/integrations/cloudflare-workers/create"
|
||||||
|
),
|
||||||
|
CodefreshConfigurePage: setRoute(
|
||||||
|
"/secret-manager/$projectId/integrations/codefresh/create",
|
||||||
|
"/_authenticate/_inject-org-details/secret-manager/$projectId/_secret-manager-layout/integrations/codefresh/create"
|
||||||
|
),
|
||||||
|
GcpSecretManagerConfigurePage: setRoute(
|
||||||
|
"/secret-manager/$projectId/integrations/gcp-secret-manager/create",
|
||||||
|
"/_authenticate/_inject-org-details/secret-manager/$projectId/_secret-manager-layout/integrations/gcp-secret-manager/create"
|
||||||
|
),
|
||||||
|
GcpSecretManagerOauthCallbackPage: setRoute(
|
||||||
|
"/secret-manager/$projectId/integrations/gcp-secret-manager/oauth2/callback",
|
||||||
|
"/_authenticate/_inject-org-details/secret-manager/$projectId/_secret-manager-layout/integrations/gcp-secret-manager/oauth2/callback"
|
||||||
|
),
|
||||||
|
GithubConfigurePage: setRoute(
|
||||||
|
"/secret-manager/$projectId/integrations/github/create",
|
||||||
|
"/_authenticate/_inject-org-details/secret-manager/$projectId/_secret-manager-layout/integrations/github/create"
|
||||||
|
),
|
||||||
|
GithubOauthCallbackPage: setRoute(
|
||||||
|
"/secret-manager/$projectId/integrations/github/oauth2/callback",
|
||||||
|
"/_authenticate/_inject-org-details/secret-manager/$projectId/_secret-manager-layout/integrations/github/oauth2/callback"
|
||||||
|
),
|
||||||
|
GitlabConfigurePage: setRoute(
|
||||||
|
"/secret-manager/$projectId/integrations/gitlab/create",
|
||||||
|
"/_authenticate/_inject-org-details/secret-manager/$projectId/_secret-manager-layout/integrations/gitlab/create"
|
||||||
|
),
|
||||||
|
GitlabOauthCallbackPage: setRoute(
|
||||||
|
"/secret-manager/$projectId/integrations/gitlab/oauth2/callback",
|
||||||
|
"/_authenticate/_inject-org-details/secret-manager/$projectId/_secret-manager-layout/integrations/gitlab/oauth2/callback"
|
||||||
|
),
|
||||||
|
VercelOauthCallbackPage: setRoute(
|
||||||
|
"/secret-manager/$projectId/integrations/vercel/oauth2/callback",
|
||||||
|
"/_authenticate/_inject-org-details/secret-manager/$projectId/_secret-manager-layout/integrations/vercel/oauth2/callback"
|
||||||
|
),
|
||||||
|
VercelConfigurePage: setRoute(
|
||||||
|
"/secret-manager/$projectId/integrations/vercel/create",
|
||||||
|
"/_authenticate/_inject-org-details/secret-manager/$projectId/_secret-manager-layout/integrations/vercel/create"
|
||||||
|
),
|
||||||
|
FlyioConfigurePage: setRoute(
|
||||||
|
"/secret-manager/$projectId/integrations/flyio/create",
|
||||||
|
"/_authenticate/_inject-org-details/secret-manager/$projectId/_secret-manager-layout/integrations/flyio/create"
|
||||||
|
),
|
||||||
|
HashicorpVaultConfigurePage: setRoute(
|
||||||
|
"/secret-manager/$projectId/integrations/hashicorp-vault/create",
|
||||||
|
"/_authenticate/_inject-org-details/secret-manager/$projectId/_secret-manager-layout/integrations/hashicorp-vault/create"
|
||||||
|
),
|
||||||
|
HasuraCloudConfigurePage: setRoute(
|
||||||
|
"/secret-manager/$projectId/integrations/hasura-cloud/create",
|
||||||
|
"/_authenticate/_inject-org-details/secret-manager/$projectId/_secret-manager-layout/integrations/hasura-cloud/create"
|
||||||
|
),
|
||||||
|
LaravelForgeConfigurePage: setRoute(
|
||||||
|
"/secret-manager/$projectId/integrations/laravel-forge/create",
|
||||||
|
"/_authenticate/_inject-org-details/secret-manager/$projectId/_secret-manager-layout/integrations/laravel-forge/create"
|
||||||
|
),
|
||||||
|
NorthflankConfigurePage: setRoute(
|
||||||
|
"/secret-manager/$projectId/integrations/northflank/create",
|
||||||
|
"/_authenticate/_inject-org-details/secret-manager/$projectId/_secret-manager-layout/integrations/northflank/create"
|
||||||
|
),
|
||||||
|
RailwayConfigurePage: setRoute(
|
||||||
|
"/secret-manager/$projectId/integrations/railway/create",
|
||||||
|
"/_authenticate/_inject-org-details/secret-manager/$projectId/_secret-manager-layout/integrations/railway/create"
|
||||||
|
),
|
||||||
|
RenderConfigurePage: setRoute(
|
||||||
|
"/secret-manager/$projectId/integrations/render/create",
|
||||||
|
"/_authenticate/_inject-org-details/secret-manager/$projectId/_secret-manager-layout/integrations/render/create"
|
||||||
|
),
|
||||||
|
RundeckConfigurePage: setRoute(
|
||||||
|
"/secret-manager/$projectId/integrations/rundeck/create",
|
||||||
|
"/_authenticate/_inject-org-details/secret-manager/$projectId/_secret-manager-layout/integrations/rundeck/create"
|
||||||
|
),
|
||||||
|
WindmillConfigurePage: setRoute(
|
||||||
|
"/secret-manager/$projectId/integrations/windmill/create",
|
||||||
|
"/_authenticate/_inject-org-details/secret-manager/$projectId/_secret-manager-layout/integrations/windmill/create"
|
||||||
|
),
|
||||||
|
TravisCIConfigurePage: setRoute(
|
||||||
|
"/secret-manager/$projectId/integrations/travisci/create",
|
||||||
|
"/_authenticate/_inject-org-details/secret-manager/$projectId/_secret-manager-layout/integrations/travisci/create"
|
||||||
|
),
|
||||||
|
TerraformCloudConfigurePage: setRoute(
|
||||||
|
"/secret-manager/$projectId/integrations/terraform-cloud/create",
|
||||||
|
"/_authenticate/_inject-org-details/secret-manager/$projectId/_secret-manager-layout/integrations/terraform-cloud/create"
|
||||||
|
),
|
||||||
|
TeamcityConfigurePage: setRoute(
|
||||||
|
"/secret-manager/$projectId/integrations/teamcity/create",
|
||||||
|
"/_authenticate/_inject-org-details/secret-manager/$projectId/_secret-manager-layout/integrations/teamcity/create"
|
||||||
|
),
|
||||||
|
SupabaseConfigurePage: setRoute(
|
||||||
|
"/secret-manager/$projectId/integrations/supabase/create",
|
||||||
|
"/_authenticate/_inject-org-details/secret-manager/$projectId/_secret-manager-layout/integrations/supabase/create"
|
||||||
|
),
|
||||||
|
OctopusDeployCloudConfigurePage: setRoute(
|
||||||
|
"/secret-manager/$projectId/integrations/octopus-deploy/create",
|
||||||
|
"/_authenticate/_inject-org-details/secret-manager/$projectId/_secret-manager-layout/integrations/octopus-deploy/create"
|
||||||
|
),
|
||||||
|
DatabricksConfigurePage: setRoute(
|
||||||
|
"/secret-manager/$projectId/integrations/databricks/create",
|
||||||
|
"/_authenticate/_inject-org-details/secret-manager/$projectId/_secret-manager-layout/integrations/databricks/create"
|
||||||
|
),
|
||||||
|
QoveryConfigurePage: setRoute(
|
||||||
|
"/secret-manager/$projectId/integrations/qovery/create",
|
||||||
|
"/_authenticate/_inject-org-details/secret-manager/$projectId/_secret-manager-layout/integrations/qovery/create"
|
||||||
|
),
|
||||||
|
Cloud66ConfigurePage: setRoute(
|
||||||
|
"/secret-manager/$projectId/integrations/cloud-66/create",
|
||||||
|
"/_authenticate/_inject-org-details/secret-manager/$projectId/_secret-manager-layout/integrations/cloud-66/create"
|
||||||
|
),
|
||||||
|
NetlifyConfigurePage: setRoute(
|
||||||
|
"/secret-manager/$projectId/integrations/netlify/create",
|
||||||
|
"/_authenticate/_inject-org-details/secret-manager/$projectId/_secret-manager-layout/integrations/netlify/create"
|
||||||
|
),
|
||||||
|
NetlifyOuathCallbackPage: setRoute(
|
||||||
|
"/secret-manager/$projectId/integrations/netlify/oauth2/callback",
|
||||||
|
"/_authenticate/_inject-org-details/secret-manager/$projectId/_secret-manager-layout/integrations/netlify/oauth2/callback"
|
||||||
)
|
)
|
||||||
|
}
|
||||||
},
|
},
|
||||||
CertManager: {
|
CertManager: {
|
||||||
CertAuthDetailsByIDPage: setRoute(
|
CertAuthDetailsByIDPage: setRoute(
|
||||||
|
|||||||
@@ -0,0 +1,9 @@
|
|||||||
|
const INTEGRATION_PROJECT_ID = "integration_project_id";
|
||||||
|
export const localStorageService = {
|
||||||
|
getIintegrationProjectId() {
|
||||||
|
return localStorage.getItem(INTEGRATION_PROJECT_ID);
|
||||||
|
},
|
||||||
|
setIntegrationProjectId(projectId: string) {
|
||||||
|
return localStorage.setItem(INTEGRATION_PROJECT_ID, projectId);
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -7,6 +7,7 @@ export {
|
|||||||
useGetIntegrationAuthBitBucketWorkspaces,
|
useGetIntegrationAuthBitBucketWorkspaces,
|
||||||
useGetIntegrationAuthById,
|
useGetIntegrationAuthById,
|
||||||
useGetIntegrationAuthChecklyGroups,
|
useGetIntegrationAuthChecklyGroups,
|
||||||
|
useGetIntegrationAuthCircleCIOrganizations,
|
||||||
useGetIntegrationAuthGithubEnvs,
|
useGetIntegrationAuthGithubEnvs,
|
||||||
useGetIntegrationAuthGithubOrgs,
|
useGetIntegrationAuthGithubOrgs,
|
||||||
useGetIntegrationAuthNorthflankSecretGroups,
|
useGetIntegrationAuthNorthflankSecretGroups,
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import {
|
|||||||
BitBucketEnvironment,
|
BitBucketEnvironment,
|
||||||
BitBucketWorkspace,
|
BitBucketWorkspace,
|
||||||
ChecklyGroup,
|
ChecklyGroup,
|
||||||
|
CircleCIOrganization,
|
||||||
Environment,
|
Environment,
|
||||||
HerokuPipelineCoupling,
|
HerokuPipelineCoupling,
|
||||||
IntegrationAuth,
|
IntegrationAuth,
|
||||||
@@ -129,7 +130,9 @@ const integrationAuthKeys = {
|
|||||||
integrationAuthId,
|
integrationAuthId,
|
||||||
...params
|
...params
|
||||||
}: TGetIntegrationAuthOctopusDeployScopeValuesDTO) =>
|
}: TGetIntegrationAuthOctopusDeployScopeValuesDTO) =>
|
||||||
[{ integrationAuthId }, "getIntegrationAuthOctopusDeployScopeValues", params] as const
|
[{ integrationAuthId }, "getIntegrationAuthOctopusDeployScopeValues", params] as const,
|
||||||
|
getIntegrationAuthCircleCIOrganizations: (integrationAuthId: string) =>
|
||||||
|
[{ integrationAuthId }, "getIntegrationAuthCircleCIOrganizations"] as const
|
||||||
};
|
};
|
||||||
|
|
||||||
const fetchIntegrationAuthById = async (integrationAuthId: string) => {
|
const fetchIntegrationAuthById = async (integrationAuthId: string) => {
|
||||||
@@ -835,6 +838,21 @@ export const useGetIntegrationAuthBitBucketEnvironments = (
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const fetchIntegrationAuthCircleCIOrganizations = async (integrationAuthId: string) => {
|
||||||
|
const {
|
||||||
|
data: { organizations }
|
||||||
|
} = await apiRequest.get<{
|
||||||
|
organizations: CircleCIOrganization[];
|
||||||
|
}>(`/api/v1/integration-auth/${integrationAuthId}/circleci/organizations`);
|
||||||
|
return organizations;
|
||||||
|
};
|
||||||
|
export const useGetIntegrationAuthCircleCIOrganizations = (integrationAuthId: string) => {
|
||||||
|
return useQuery({
|
||||||
|
queryKey: integrationAuthKeys.getIntegrationAuthCircleCIOrganizations(integrationAuthId),
|
||||||
|
queryFn: () => fetchIntegrationAuthCircleCIOrganizations(integrationAuthId)
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
export const useGetIntegrationAuthNorthflankSecretGroups = ({
|
export const useGetIntegrationAuthNorthflankSecretGroups = ({
|
||||||
integrationAuthId,
|
integrationAuthId,
|
||||||
appId
|
appId
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
import { createFileRoute } from '@tanstack/react-router'
|
import { createFileRoute } from "@tanstack/react-router";
|
||||||
|
|
||||||
import { SshOverviewPage } from './SshOverviewPage'
|
import { SshOverviewPage } from "./SshOverviewPage";
|
||||||
|
|
||||||
export const Route = createFileRoute(
|
export const Route = createFileRoute(
|
||||||
'/_authenticate/_inject-org-details/organization/_layout/ssh/overview',
|
"/_authenticate/_inject-org-details/organization/_layout/ssh/overview"
|
||||||
)({
|
)({
|
||||||
component: SshOverviewPage,
|
component: SshOverviewPage
|
||||||
})
|
});
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { useCallback, useEffect, useState } from "react";
|
import { useCallback, useEffect, useState } from "react";
|
||||||
import { Helmet } from "react-helmet";
|
import { Helmet } from "react-helmet";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
|
import { useNavigate } from "@tanstack/react-router";
|
||||||
import { motion } from "framer-motion";
|
import { motion } from "framer-motion";
|
||||||
|
|
||||||
import { createNotification } from "@app/components/notifications";
|
import { createNotification } from "@app/components/notifications";
|
||||||
@@ -29,6 +30,7 @@ enum IntegrationView {
|
|||||||
|
|
||||||
const Page = () => {
|
const Page = () => {
|
||||||
const { currentWorkspace } = useWorkspace();
|
const { currentWorkspace } = useWorkspace();
|
||||||
|
const navigate = useNavigate();
|
||||||
const { environments, id: workspaceId } = currentWorkspace;
|
const { environments, id: workspaceId } = currentWorkspace;
|
||||||
const [view, setView] = useState<IntegrationView>(IntegrationView.New);
|
const [view, setView] = useState<IntegrationView>(IntegrationView.New);
|
||||||
|
|
||||||
@@ -97,7 +99,7 @@ const Page = () => {
|
|||||||
if (!selectedCloudIntegration) return;
|
if (!selectedCloudIntegration) return;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
redirectForProviderAuth(selectedCloudIntegration);
|
redirectForProviderAuth(currentWorkspace.id, navigate, selectedCloudIntegration);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(error);
|
console.error(error);
|
||||||
}
|
}
|
||||||
|
|||||||
+208
-50
@@ -1,10 +1,13 @@
|
|||||||
import crypto from "crypto";
|
import crypto from "crypto";
|
||||||
|
|
||||||
|
import { NavigateFn } from "@tanstack/react-router";
|
||||||
|
|
||||||
import { createNotification } from "@app/components/notifications";
|
import { createNotification } from "@app/components/notifications";
|
||||||
import {
|
import {
|
||||||
decryptAssymmetric,
|
decryptAssymmetric,
|
||||||
encryptAssymmetric
|
encryptAssymmetric
|
||||||
} from "@app/components/utilities/cryptography/crypto";
|
} from "@app/components/utilities/cryptography/crypto";
|
||||||
|
import { localStorageService } from "@app/helpers/localStorage";
|
||||||
import { TCloudIntegration, UserWsKeyPair } from "@app/hooks/api/types";
|
import { TCloudIntegration, UserWsKeyPair } from "@app/hooks/api/types";
|
||||||
|
|
||||||
export const generateBotKey = (botPublicKey: string, latestKey: UserWsKeyPair) => {
|
export const generateBotKey = (botPublicKey: string, latestKey: UserWsKeyPair) => {
|
||||||
@@ -52,154 +55,309 @@ export const createIntegrationMissingEnvVarsNotification = (
|
|||||||
title: "Missing Environment Variables"
|
title: "Missing Environment Variables"
|
||||||
});
|
});
|
||||||
|
|
||||||
export const redirectForProviderAuth = (integrationOption: TCloudIntegration) => {
|
export const redirectForProviderAuth = (
|
||||||
|
projectId: string,
|
||||||
|
navigate: NavigateFn,
|
||||||
|
integrationOption: TCloudIntegration
|
||||||
|
) => {
|
||||||
try {
|
try {
|
||||||
// generate CSRF token for OAuth2 code-token exchange integrations
|
// generate CSRF token for OAuth2 code-token exchange integrations
|
||||||
const state = crypto.randomBytes(16).toString("hex");
|
const state = crypto.randomBytes(16).toString("hex");
|
||||||
localStorage.setItem("latestCSRFToken", state);
|
localStorage.setItem("latestCSRFToken", state);
|
||||||
|
localStorageService.setIntegrationProjectId(projectId);
|
||||||
|
|
||||||
let link = "";
|
|
||||||
switch (integrationOption.slug) {
|
switch (integrationOption.slug) {
|
||||||
case "gcp-secret-manager":
|
case "gcp-secret-manager":
|
||||||
link = `${window.location.origin}/integrations/gcp-secret-manager/authorize`;
|
navigate({
|
||||||
|
to: "/secret-manager/$projectId/integrations/gcp-secret-manager/authorize",
|
||||||
|
params: {
|
||||||
|
projectId
|
||||||
|
}
|
||||||
|
});
|
||||||
break;
|
break;
|
||||||
case "azure-key-vault":
|
case "azure-key-vault": {
|
||||||
if (!integrationOption.clientId) {
|
if (!integrationOption.clientId) {
|
||||||
createIntegrationMissingEnvVarsNotification(integrationOption.slug);
|
createIntegrationMissingEnvVarsNotification(integrationOption.slug);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
link = `https://login.microsoftonline.com/common/oauth2/v2.0/authorize?client_id=${integrationOption.clientId}&response_type=code&redirect_uri=${window.location.origin}/integrations/azure-key-vault/oauth2/callback&response_mode=query&scope=https://vault.azure.net/.default openid offline_access&state=${state}`;
|
const link = `https://login.microsoftonline.com/common/oauth2/v2.0/authorize?client_id=${integrationOption.clientId}&response_type=code&redirect_uri=${window.location.origin}/integrations/azure-key-vault/oauth2/callback&response_mode=query&scope=https://vault.azure.net/.default openid offline_access&state=${state}`;
|
||||||
|
window.location.assign(link);
|
||||||
break;
|
break;
|
||||||
case "azure-app-configuration":
|
}
|
||||||
|
case "azure-app-configuration": {
|
||||||
if (!integrationOption.clientId) {
|
if (!integrationOption.clientId) {
|
||||||
createIntegrationMissingEnvVarsNotification(integrationOption.slug);
|
createIntegrationMissingEnvVarsNotification(integrationOption.slug);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
link = `https://login.microsoftonline.com/common/oauth2/v2.0/authorize?client_id=${integrationOption.clientId}&response_type=code&redirect_uri=${window.location.origin}/integrations/azure-app-configuration/oauth2/callback&response_mode=query&scope=https://azconfig.io/.default openid offline_access&state=${state}`;
|
const link = `https://login.microsoftonline.com/common/oauth2/v2.0/authorize?client_id=${integrationOption.clientId}&response_type=code&redirect_uri=${window.location.origin}/integrations/azure-app-configuration/oauth2/callback&response_mode=query&scope=https://azconfig.io/.default openid offline_access&state=${state}`;
|
||||||
|
window.location.assign(link);
|
||||||
break;
|
break;
|
||||||
|
}
|
||||||
case "aws-parameter-store":
|
case "aws-parameter-store":
|
||||||
link = `${window.location.origin}/integrations/aws-parameter-store/authorize`;
|
navigate({
|
||||||
|
to: "/secret-manager/$projectId/integrations/aws-parameter-store/authorize",
|
||||||
|
params: {
|
||||||
|
projectId
|
||||||
|
}
|
||||||
|
});
|
||||||
break;
|
break;
|
||||||
case "aws-secret-manager":
|
case "aws-secret-manager":
|
||||||
link = `${window.location.origin}/integrations/aws-secret-manager/authorize`;
|
navigate({
|
||||||
|
to: "/secret-manager/$projectId/integrations/aws-secret-manager/authorize",
|
||||||
|
params: {
|
||||||
|
projectId
|
||||||
|
}
|
||||||
|
});
|
||||||
break;
|
break;
|
||||||
case "heroku":
|
case "heroku": {
|
||||||
if (!integrationOption.clientId) {
|
if (!integrationOption.clientId) {
|
||||||
createIntegrationMissingEnvVarsNotification(integrationOption.slug);
|
createIntegrationMissingEnvVarsNotification(integrationOption.slug);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
link = `https://id.heroku.com/oauth/authorize?client_id=${integrationOption.clientId}&response_type=code&scope=write-protected&state=${state}`;
|
const link = `https://id.heroku.com/oauth/authorize?client_id=${integrationOption.clientId}&response_type=code&scope=write-protected&state=${state}`;
|
||||||
|
window.location.assign(link);
|
||||||
break;
|
break;
|
||||||
case "vercel":
|
}
|
||||||
|
case "vercel": {
|
||||||
if (!integrationOption.clientSlug) {
|
if (!integrationOption.clientSlug) {
|
||||||
createIntegrationMissingEnvVarsNotification(integrationOption.slug);
|
createIntegrationMissingEnvVarsNotification(integrationOption.slug);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
link = `https://vercel.com/integrations/${integrationOption.clientSlug}/new?state=${state}`;
|
const link = `https://vercel.com/integrations/${integrationOption.clientSlug}/new?state=${state}`;
|
||||||
|
window.location.assign(link);
|
||||||
break;
|
break;
|
||||||
case "netlify":
|
}
|
||||||
|
case "netlify": {
|
||||||
if (!integrationOption.clientId) {
|
if (!integrationOption.clientId) {
|
||||||
createIntegrationMissingEnvVarsNotification(integrationOption.slug);
|
createIntegrationMissingEnvVarsNotification(integrationOption.slug);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
link = `https://app.netlify.com/authorize?client_id=${integrationOption.clientId}&response_type=code&state=${state}&redirect_uri=${window.location.origin}/integrations/netlify/oauth2/callback`;
|
const link = `https://app.netlify.com/authorize?client_id=${integrationOption.clientId}&response_type=code&state=${state}&redirect_uri=${window.location.origin}/integrations/netlify/oauth2/callback`;
|
||||||
|
|
||||||
|
window.location.assign(link);
|
||||||
break;
|
break;
|
||||||
|
}
|
||||||
case "github":
|
case "github":
|
||||||
link = `${window.location.origin}/integrations/github/auth-mode-selection`;
|
navigate({
|
||||||
|
to: "/secret-manager/$projectId/integrations/github/auth-mode-selection",
|
||||||
|
params: {
|
||||||
|
projectId
|
||||||
|
}
|
||||||
|
});
|
||||||
break;
|
break;
|
||||||
case "gitlab":
|
case "gitlab":
|
||||||
link = `${window.location.origin}/integrations/gitlab/authorize`;
|
navigate({
|
||||||
|
to: "/secret-manager/$projectId/integrations/gitlab/authorize",
|
||||||
|
params: {
|
||||||
|
projectId
|
||||||
|
}
|
||||||
|
});
|
||||||
break;
|
break;
|
||||||
case "render":
|
case "render":
|
||||||
link = `${window.location.origin}/integrations/render/authorize`;
|
navigate({
|
||||||
|
to: "/secret-manager/$projectId/integrations/render/authorize",
|
||||||
|
params: {
|
||||||
|
projectId
|
||||||
|
}
|
||||||
|
});
|
||||||
break;
|
break;
|
||||||
case "flyio":
|
case "flyio":
|
||||||
link = `${window.location.origin}/integrations/flyio/authorize`;
|
navigate({
|
||||||
|
to: "/secret-manager/$projectId/integrations/flyio/authorize",
|
||||||
|
params: {
|
||||||
|
projectId
|
||||||
|
}
|
||||||
|
});
|
||||||
break;
|
break;
|
||||||
case "circleci":
|
case "circleci":
|
||||||
link = `${window.location.origin}/integrations/circleci/authorize`;
|
navigate({
|
||||||
|
to: "/secret-manager/$projectId/integrations/circleci/authorize",
|
||||||
|
params: {
|
||||||
|
projectId
|
||||||
|
}
|
||||||
|
});
|
||||||
break;
|
break;
|
||||||
case "databricks":
|
case "databricks":
|
||||||
link = `${window.location.origin}/integrations/databricks/authorize`;
|
navigate({
|
||||||
|
to: "/secret-manager/$projectId/integrations/databricks/authorize",
|
||||||
|
params: {
|
||||||
|
projectId
|
||||||
|
}
|
||||||
|
});
|
||||||
break;
|
break;
|
||||||
case "laravel-forge":
|
case "laravel-forge":
|
||||||
link = `${window.location.origin}/integrations/laravel-forge/authorize`;
|
navigate({
|
||||||
|
to: "/secret-manager/$projectId/integrations/laravel-forge/authorize",
|
||||||
|
params: {
|
||||||
|
projectId
|
||||||
|
}
|
||||||
|
});
|
||||||
break;
|
break;
|
||||||
case "travisci":
|
case "travisci":
|
||||||
link = `${window.location.origin}/integrations/travisci/authorize`;
|
navigate({
|
||||||
|
to: "/secret-manager/$projectId/integrations/travisci/authorize",
|
||||||
|
params: {
|
||||||
|
projectId
|
||||||
|
}
|
||||||
|
});
|
||||||
break;
|
break;
|
||||||
case "supabase":
|
case "supabase":
|
||||||
link = `${window.location.origin}/integrations/supabase/authorize`;
|
navigate({
|
||||||
|
to: "/secret-manager/$projectId/integrations/supabase/authorize",
|
||||||
|
params: {
|
||||||
|
projectId
|
||||||
|
}
|
||||||
|
});
|
||||||
break;
|
break;
|
||||||
case "checkly":
|
case "checkly":
|
||||||
link = `${window.location.origin}/integrations/checkly/authorize`;
|
navigate({
|
||||||
|
to: "/secret-manager/$projectId/integrations/checkly/authorize",
|
||||||
|
params: {
|
||||||
|
projectId
|
||||||
|
}
|
||||||
|
});
|
||||||
break;
|
break;
|
||||||
case "qovery":
|
case "qovery":
|
||||||
link = `${window.location.origin}/integrations/qovery/authorize`;
|
navigate({
|
||||||
|
to: "/secret-manager/$projectId/integrations/qovery/authorize",
|
||||||
|
params: {
|
||||||
|
projectId
|
||||||
|
}
|
||||||
|
});
|
||||||
break;
|
break;
|
||||||
case "railway":
|
case "railway":
|
||||||
link = `${window.location.origin}/integrations/railway/authorize`;
|
navigate({
|
||||||
|
to: "/secret-manager/$projectId/integrations/railway/authorize",
|
||||||
|
params: {
|
||||||
|
projectId
|
||||||
|
}
|
||||||
|
});
|
||||||
break;
|
break;
|
||||||
case "terraform-cloud":
|
case "terraform-cloud":
|
||||||
link = `${window.location.origin}/integrations/terraform-cloud/authorize`;
|
navigate({
|
||||||
|
to: "/secret-manager/$projectId/integrations/terraform-cloud/authorize",
|
||||||
|
params: {
|
||||||
|
projectId
|
||||||
|
}
|
||||||
|
});
|
||||||
break;
|
break;
|
||||||
case "hashicorp-vault":
|
case "hashicorp-vault":
|
||||||
link = `${window.location.origin}/integrations/hashicorp-vault/authorize`;
|
navigate({
|
||||||
|
to: "/secret-manager/$projectId/integrations/hashicorp-vault/authorize",
|
||||||
|
params: {
|
||||||
|
projectId
|
||||||
|
}
|
||||||
|
});
|
||||||
break;
|
break;
|
||||||
case "cloudflare-pages":
|
case "cloudflare-pages":
|
||||||
link = `${window.location.origin}/integrations/cloudflare-pages/authorize`;
|
navigate({
|
||||||
|
to: "/secret-manager/$projectId/integrations/cloudflare-pages/authorize",
|
||||||
|
params: {
|
||||||
|
projectId
|
||||||
|
}
|
||||||
|
});
|
||||||
break;
|
break;
|
||||||
case "cloudflare-workers":
|
case "cloudflare-workers":
|
||||||
link = `${window.location.origin}/integrations/cloudflare-workers/authorize`;
|
navigate({
|
||||||
|
to: "/secret-manager/$projectId/integrations/cloudflare-workers/authorize",
|
||||||
|
params: {
|
||||||
|
projectId
|
||||||
|
}
|
||||||
|
});
|
||||||
break;
|
break;
|
||||||
case "bitbucket":
|
case "bitbucket": {
|
||||||
if (!integrationOption.clientId) {
|
if (!integrationOption.clientId) {
|
||||||
createIntegrationMissingEnvVarsNotification(integrationOption.slug, "cicd");
|
createIntegrationMissingEnvVarsNotification(integrationOption.slug, "cicd");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
link = `https://bitbucket.org/site/oauth2/authorize?client_id=${integrationOption.clientId}&response_type=code&redirect_uri=${window.location.origin}/integrations/bitbucket/oauth2/callback&state=${state}`;
|
const link = `https://bitbucket.org/site/oauth2/authorize?client_id=${integrationOption.clientId}&response_type=code&redirect_uri=${window.location.origin}/integrations/bitbucket/oauth2/callback&state=${state}`;
|
||||||
|
window.location.assign(link);
|
||||||
break;
|
break;
|
||||||
|
}
|
||||||
case "codefresh":
|
case "codefresh":
|
||||||
link = `${window.location.origin}/integrations/codefresh/authorize`;
|
navigate({
|
||||||
|
to: "/secret-manager/$projectId/integrations/codefresh/authorize",
|
||||||
|
params: {
|
||||||
|
projectId
|
||||||
|
}
|
||||||
|
});
|
||||||
break;
|
break;
|
||||||
case "digital-ocean-app-platform":
|
case "digital-ocean-app-platform":
|
||||||
link = `${window.location.origin}/integrations/digital-ocean-app-platform/authorize`;
|
navigate({
|
||||||
|
to: "/secret-manager/$projectId/integrations/digital-ocean-app-platform/authorize",
|
||||||
|
params: {
|
||||||
|
projectId
|
||||||
|
}
|
||||||
|
});
|
||||||
break;
|
break;
|
||||||
case "cloud-66":
|
case "cloud-66":
|
||||||
link = `${window.location.origin}/integrations/cloud-66/authorize`;
|
navigate({
|
||||||
|
to: "/secret-manager/$projectId/integrations/cloud-66/authorize",
|
||||||
|
params: {
|
||||||
|
projectId
|
||||||
|
}
|
||||||
|
});
|
||||||
break;
|
break;
|
||||||
case "northflank":
|
case "northflank":
|
||||||
link = `${window.location.origin}/integrations/northflank/authorize`;
|
navigate({
|
||||||
|
to: "/secret-manager/$projectId/integrations/northflank/authorize",
|
||||||
|
params: {
|
||||||
|
projectId
|
||||||
|
}
|
||||||
|
});
|
||||||
break;
|
break;
|
||||||
case "windmill":
|
case "windmill":
|
||||||
link = `${window.location.origin}/integrations/windmill/authorize`;
|
navigate({
|
||||||
|
to: "/secret-manager/$projectId/integrations/windmill/authorize",
|
||||||
|
params: {
|
||||||
|
projectId
|
||||||
|
}
|
||||||
|
});
|
||||||
break;
|
break;
|
||||||
case "teamcity":
|
case "teamcity":
|
||||||
link = `${window.location.origin}/integrations/teamcity/authorize`;
|
navigate({
|
||||||
|
to: "/secret-manager/$projectId/integrations/teamcity/authorize",
|
||||||
|
params: {
|
||||||
|
projectId
|
||||||
|
}
|
||||||
|
});
|
||||||
break;
|
break;
|
||||||
case "hasura-cloud":
|
case "hasura-cloud":
|
||||||
link = `${window.location.origin}/integrations/hasura-cloud/authorize`;
|
navigate({
|
||||||
|
to: "/secret-manager/$projectId/integrations/hasura-cloud/authorize",
|
||||||
|
params: {
|
||||||
|
projectId
|
||||||
|
}
|
||||||
|
});
|
||||||
break;
|
break;
|
||||||
case "rundeck":
|
case "rundeck":
|
||||||
link = `${window.location.origin}/integrations/rundeck/authorize`;
|
navigate({
|
||||||
|
to: "/secret-manager/$projectId/integrations/rundeck/authorize",
|
||||||
|
params: {
|
||||||
|
projectId
|
||||||
|
}
|
||||||
|
});
|
||||||
break;
|
break;
|
||||||
case "azure-devops":
|
case "azure-devops":
|
||||||
link = `${window.location.origin}/integrations/azure-devops/authorize`;
|
navigate({
|
||||||
|
to: "/secret-manager/$projectId/integrations/azure-devops/authorize",
|
||||||
|
params: {
|
||||||
|
projectId
|
||||||
|
}
|
||||||
|
});
|
||||||
break;
|
break;
|
||||||
case "octopus-deploy":
|
case "octopus-deploy":
|
||||||
link = `${window.location.origin}/integrations/octopus-deploy/authorize`;
|
navigate({
|
||||||
|
to: "/secret-manager/$projectId/integrations/octopus-deploy/authorize",
|
||||||
|
params: {
|
||||||
|
projectId
|
||||||
|
}
|
||||||
|
});
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (link !== "") {
|
|
||||||
window.location.assign(link);
|
|
||||||
}
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error(err);
|
console.error(err);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
export const redirectToIntegrationAppConfigScreen = (provider: string, integrationAuthId: string) =>
|
|
||||||
`/integrations/${provider}/create?integrationAuthId=${integrationAuthId}`;
|
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
import { createFileRoute } from '@tanstack/react-router'
|
import { createFileRoute } from "@tanstack/react-router";
|
||||||
|
|
||||||
import { IntegrationsListPage } from './IntegrationsListPage'
|
import { IntegrationsListPage } from "./IntegrationsListPage";
|
||||||
|
|
||||||
export const Route = createFileRoute(
|
export const Route = createFileRoute(
|
||||||
'/_authenticate/_inject-org-details/secret-manager/$projectId/_secret-manager-layout/integrations/',
|
"/_authenticate/_inject-org-details/secret-manager/$projectId/_secret-manager-layout/integrations/"
|
||||||
)({
|
)({
|
||||||
component: IntegrationsListPage,
|
component: IntegrationsListPage
|
||||||
})
|
});
|
||||||
|
|||||||
+207
@@ -0,0 +1,207 @@
|
|||||||
|
import { Helmet } from "react-helmet";
|
||||||
|
import { Controller, useForm } from "react-hook-form";
|
||||||
|
import { faArrowUpRightFromSquare, faBookOpen } from "@fortawesome/free-solid-svg-icons";
|
||||||
|
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||||
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
|
import { useNavigate } from "@tanstack/react-router";
|
||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
import {
|
||||||
|
Button,
|
||||||
|
Card,
|
||||||
|
CardBody,
|
||||||
|
CardTitle,
|
||||||
|
FormControl,
|
||||||
|
Input,
|
||||||
|
Select,
|
||||||
|
SelectItem
|
||||||
|
} from "@app/components/v2";
|
||||||
|
import { useWorkspace } from "@app/context";
|
||||||
|
import { useSaveIntegrationAccessToken } from "@app/hooks/api";
|
||||||
|
|
||||||
|
enum AwsAuthType {
|
||||||
|
AccessKey = "access-key",
|
||||||
|
AssumeRole = "assume-role"
|
||||||
|
}
|
||||||
|
|
||||||
|
const formSchema = z.discriminatedUnion("type", [
|
||||||
|
z.object({
|
||||||
|
type: z.literal(AwsAuthType.AccessKey),
|
||||||
|
accessKey: z.string().min(1),
|
||||||
|
accessSecretKey: z.string().min(1)
|
||||||
|
}),
|
||||||
|
z.object({
|
||||||
|
type: z.literal(AwsAuthType.AssumeRole),
|
||||||
|
iamRoleArn: z.string().min(1)
|
||||||
|
})
|
||||||
|
]);
|
||||||
|
|
||||||
|
type TForm = z.infer<typeof formSchema>;
|
||||||
|
|
||||||
|
export const AWSParameterStoreAuthorizeIntegrationPage = () => {
|
||||||
|
const navigate = useNavigate();
|
||||||
|
const { currentWorkspace } = useWorkspace();
|
||||||
|
const { mutateAsync } = useSaveIntegrationAccessToken();
|
||||||
|
|
||||||
|
const { control, handleSubmit, formState, watch } = useForm<TForm>({
|
||||||
|
resolver: zodResolver(formSchema),
|
||||||
|
defaultValues: {
|
||||||
|
type: AwsAuthType.AccessKey
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const formAwsAuthTypeField = watch("type");
|
||||||
|
|
||||||
|
const handleFormSubmit = async (data: TForm) => {
|
||||||
|
try {
|
||||||
|
const integrationAuth = await mutateAsync({
|
||||||
|
workspaceId: localStorage.getItem("projectData.id"),
|
||||||
|
integration: "aws-parameter-store",
|
||||||
|
...(data.type === AwsAuthType.AssumeRole
|
||||||
|
? {
|
||||||
|
awsAssumeIamRoleArn: data.iamRoleArn
|
||||||
|
}
|
||||||
|
: {
|
||||||
|
accessId: data.accessKey,
|
||||||
|
accessToken: data.accessSecretKey
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
navigate({
|
||||||
|
to: "/secret-manager/$projectId/integrations/aws-parameter-store/create",
|
||||||
|
params: {
|
||||||
|
projectId: currentWorkspace.id
|
||||||
|
},
|
||||||
|
search: {
|
||||||
|
integrationAuthId: integrationAuth.id
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex h-full w-full items-center justify-center">
|
||||||
|
<Helmet>
|
||||||
|
<title>Authorize AWS Parameter Integration</title>
|
||||||
|
</Helmet>
|
||||||
|
<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="flex items-center">
|
||||||
|
<img
|
||||||
|
src="/images/integrations/Amazon Web Services.png"
|
||||||
|
height={35}
|
||||||
|
width={35}
|
||||||
|
alt="AWS logo"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<span className="ml-1.5">AWS Parameter Store Integration </span>
|
||||||
|
<a
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
href="https://infisical.com/docs/integrations/cloud/aws-parameter-store"
|
||||||
|
>
|
||||||
|
<div className="mb-1 ml-2 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="mb-[0.07rem] ml-1.5 text-xxs"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</CardTitle>
|
||||||
|
<CardBody>
|
||||||
|
<form onSubmit={handleSubmit(handleFormSubmit)}>
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
|
name="type"
|
||||||
|
defaultValue={AwsAuthType.AccessKey}
|
||||||
|
render={({ field: { onChange, ...field }, fieldState: { error } }) => (
|
||||||
|
<FormControl
|
||||||
|
label="Authentication Mode"
|
||||||
|
errorText={error?.message}
|
||||||
|
isError={Boolean(error)}
|
||||||
|
>
|
||||||
|
<Select
|
||||||
|
defaultValue={field.value}
|
||||||
|
{...field}
|
||||||
|
onValueChange={(e) => onChange(e)}
|
||||||
|
className="w-full"
|
||||||
|
>
|
||||||
|
<SelectItem value={AwsAuthType.AccessKey}>Access Key</SelectItem>
|
||||||
|
<SelectItem value={AwsAuthType.AssumeRole}>AWS Assume Role</SelectItem>
|
||||||
|
</Select>
|
||||||
|
</FormControl>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
{formAwsAuthTypeField === AwsAuthType.AccessKey ? (
|
||||||
|
<>
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
|
name="accessKey"
|
||||||
|
render={({ field, fieldState: { error } }) => (
|
||||||
|
<FormControl
|
||||||
|
label="Access Key ID"
|
||||||
|
errorText={error?.message}
|
||||||
|
isError={Boolean(error)}
|
||||||
|
>
|
||||||
|
<Input placeholder="" {...field} />
|
||||||
|
</FormControl>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
|
name="accessSecretKey"
|
||||||
|
render={({ field, fieldState: { error } }) => (
|
||||||
|
<FormControl
|
||||||
|
label="Secret Access Key"
|
||||||
|
errorText={error?.message}
|
||||||
|
isError={Boolean(error)}
|
||||||
|
>
|
||||||
|
<Input
|
||||||
|
type="password"
|
||||||
|
autoComplete="new-password"
|
||||||
|
placeholder=""
|
||||||
|
{...field}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
|
name="iamRoleArn"
|
||||||
|
render={({ field, fieldState: { error } }) => (
|
||||||
|
<FormControl
|
||||||
|
label="IAM Role ARN For Role Assumption"
|
||||||
|
errorText={error?.message}
|
||||||
|
isError={Boolean(error)}
|
||||||
|
>
|
||||||
|
<Input placeholder="" {...field} />
|
||||||
|
</FormControl>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
<Button
|
||||||
|
type="submit"
|
||||||
|
colorSchema="primary"
|
||||||
|
variant="outline_bg"
|
||||||
|
className="mb-6 ml-auto mr-6 mt-2 w-min"
|
||||||
|
isLoading={formState.isSubmitting}
|
||||||
|
>
|
||||||
|
Connect to AWS Parameter Store
|
||||||
|
</Button>
|
||||||
|
</form>
|
||||||
|
</CardBody>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
+9
@@ -0,0 +1,9 @@
|
|||||||
|
import { createFileRoute } from "@tanstack/react-router";
|
||||||
|
|
||||||
|
import { AWSParameterStoreAuthorizeIntegrationPage } from "./AwsParameterStoreAuthorizePage";
|
||||||
|
|
||||||
|
export const Route = createFileRoute(
|
||||||
|
"/_authenticate/_inject-org-details/secret-manager/$projectId/_secret-manager-layout/integrations/aws-parameter-store/authorize"
|
||||||
|
)({
|
||||||
|
component: AWSParameterStoreAuthorizeIntegrationPage
|
||||||
|
});
|
||||||
+391
@@ -0,0 +1,391 @@
|
|||||||
|
import { useEffect, useState } from "react";
|
||||||
|
import { Helmet } from "react-helmet";
|
||||||
|
import {
|
||||||
|
faArrowUpRightFromSquare,
|
||||||
|
faBookOpen,
|
||||||
|
faBugs,
|
||||||
|
faCircleInfo
|
||||||
|
} from "@fortawesome/free-solid-svg-icons";
|
||||||
|
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||||
|
import { useNavigate, useSearch } from "@tanstack/react-router";
|
||||||
|
import { motion } from "framer-motion";
|
||||||
|
|
||||||
|
import {
|
||||||
|
Badge,
|
||||||
|
Button,
|
||||||
|
Card,
|
||||||
|
CardTitle,
|
||||||
|
FormControl,
|
||||||
|
Input,
|
||||||
|
Select,
|
||||||
|
SelectItem,
|
||||||
|
Switch,
|
||||||
|
Tab,
|
||||||
|
TabList,
|
||||||
|
TabPanel,
|
||||||
|
Tabs
|
||||||
|
} from "@app/components/v2";
|
||||||
|
import { ROUTE_PATHS } from "@app/const/routes";
|
||||||
|
import { useWorkspace } from "@app/context";
|
||||||
|
import { useCreateIntegration } from "@app/hooks/api";
|
||||||
|
import { useGetIntegrationAuthById } from "@app/hooks/api/integrationAuth";
|
||||||
|
import { useGetIntegrationAuthAwsKmsKeys } from "@app/hooks/api/integrationAuth/queries";
|
||||||
|
|
||||||
|
enum TabSections {
|
||||||
|
Connection = "connection",
|
||||||
|
Options = "options"
|
||||||
|
}
|
||||||
|
|
||||||
|
const awsRegions = [
|
||||||
|
{ name: "US East (Ohio)", slug: "us-east-2" },
|
||||||
|
{ name: "US East (N. Virginia)", slug: "us-east-1" },
|
||||||
|
{ name: "US West (N. California)", slug: "us-west-1" },
|
||||||
|
{ name: "US West (Oregon)", slug: "us-west-2" },
|
||||||
|
{ name: "Africa (Cape Town)", slug: "af-south-1" },
|
||||||
|
{ name: "Asia Pacific (Hong Kong)", slug: "ap-east-1" },
|
||||||
|
{ name: "Asia Pacific (Hyderabad)", slug: "ap-south-2" },
|
||||||
|
{ name: "Asia Pacific (Jakarta)", slug: "ap-southeast-3" },
|
||||||
|
{ name: "Asia Pacific (Melbourne)", slug: "ap-southeast-4" },
|
||||||
|
{ name: "Asia Pacific (Mumbai)", slug: "ap-south-1" },
|
||||||
|
{ name: "Asia Pacific (Osaka)", slug: "ap-northeast-3" },
|
||||||
|
{ name: "Asia Pacific (Seoul)", slug: "ap-northeast-2" },
|
||||||
|
{ name: "Asia Pacific (Singapore)", slug: "ap-southeast-1" },
|
||||||
|
{ name: "Asia Pacific (Sydney)", slug: "ap-southeast-2" },
|
||||||
|
{ name: "Asia Pacific (Tokyo)", slug: "ap-northeast-1" },
|
||||||
|
{ name: "Canada (Central)", slug: "ca-central-1" },
|
||||||
|
{ name: "Europe (Frankfurt)", slug: "eu-central-1" },
|
||||||
|
{ name: "Europe (Ireland)", slug: "eu-west-1" },
|
||||||
|
{ name: "Europe (London)", slug: "eu-west-2" },
|
||||||
|
{ name: "Europe (Milan)", slug: "eu-south-1" },
|
||||||
|
{ name: "Europe (Paris)", slug: "eu-west-3" },
|
||||||
|
{ name: "Europe (Spain)", slug: "eu-south-2" },
|
||||||
|
{ name: "Europe (Stockholm)", slug: "eu-north-1" },
|
||||||
|
{ name: "Europe (Zurich)", slug: "eu-central-2" },
|
||||||
|
{ name: "Middle East (Bahrain)", slug: "me-south-1" },
|
||||||
|
{ name: "Middle East (UAE)", slug: "me-central-1" },
|
||||||
|
{ name: "South America (Sao Paulo)", slug: "sa-east-1" },
|
||||||
|
{ name: "AWS GovCloud (US-East)", slug: "us-gov-east-1" },
|
||||||
|
{ name: "AWS GovCloud (US-West)", slug: "us-gov-west-1" }
|
||||||
|
];
|
||||||
|
|
||||||
|
export const AWSParameterStoreConfigurePage = () => {
|
||||||
|
const navigate = useNavigate();
|
||||||
|
const { mutateAsync } = useCreateIntegration();
|
||||||
|
|
||||||
|
const integrationAuthId = useSearch({
|
||||||
|
from: ROUTE_PATHS.SecretManager.Integratons.AwsParameterStoreConfigurePage.id,
|
||||||
|
select: (el) => el.integrationAuthId
|
||||||
|
});
|
||||||
|
const { currentWorkspace } = useWorkspace();
|
||||||
|
|
||||||
|
const { data: integrationAuth, isPending: isintegrationAuthLoading } = useGetIntegrationAuthById(
|
||||||
|
(integrationAuthId as string) ?? ""
|
||||||
|
);
|
||||||
|
|
||||||
|
const [selectedSourceEnvironment, setSelectedSourceEnvironment] = useState("");
|
||||||
|
const [secretPath, setSecretPath] = useState("/");
|
||||||
|
const [selectedAWSRegion, setSelectedAWSRegion] = useState("");
|
||||||
|
const [path, setPath] = useState("");
|
||||||
|
const [pathErrorText, setPathErrorText] = useState("");
|
||||||
|
|
||||||
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
|
const [shouldTag, setShouldTag] = useState(false);
|
||||||
|
const [shouldDisableDelete, setShouldDisableDelete] = useState(false);
|
||||||
|
const [tagKey, setTagKey] = useState("");
|
||||||
|
const [tagValue, setTagValue] = useState("");
|
||||||
|
const [kmsKeyId, setKmsKeyId] = useState("");
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (currentWorkspace) {
|
||||||
|
setSelectedSourceEnvironment(currentWorkspace.environments[0].slug);
|
||||||
|
setSelectedAWSRegion(awsRegions[0].slug);
|
||||||
|
}
|
||||||
|
}, [currentWorkspace]);
|
||||||
|
|
||||||
|
const { data: integrationAuthAwsKmsKeys, isPending: isIntegrationAuthAwsKmsKeysLoading } =
|
||||||
|
useGetIntegrationAuthAwsKmsKeys({
|
||||||
|
integrationAuthId: String(integrationAuthId),
|
||||||
|
region: selectedAWSRegion
|
||||||
|
});
|
||||||
|
|
||||||
|
const isValidAWSParameterStorePath = (awsStorePath: string) => {
|
||||||
|
const pattern = /^\/([\w-]+\/)*[\w-]+\/$/;
|
||||||
|
return pattern.test(awsStorePath) && awsStorePath.length <= 2048;
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleButtonClick = async () => {
|
||||||
|
try {
|
||||||
|
if (path !== "") {
|
||||||
|
// case: path is not empty
|
||||||
|
if (!isValidAWSParameterStorePath(path)) {
|
||||||
|
// case: path is not valid for aws parameter store
|
||||||
|
setPathErrorText("Path must be a valid path for SSM like /project/env/");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!integrationAuth?.id) return;
|
||||||
|
|
||||||
|
setIsLoading(true);
|
||||||
|
|
||||||
|
await mutateAsync({
|
||||||
|
integrationAuthId: integrationAuth?.id,
|
||||||
|
isActive: true,
|
||||||
|
sourceEnvironment: selectedSourceEnvironment,
|
||||||
|
path,
|
||||||
|
region: selectedAWSRegion,
|
||||||
|
secretPath,
|
||||||
|
metadata: {
|
||||||
|
...(shouldTag
|
||||||
|
? {
|
||||||
|
secretAWSTag: [
|
||||||
|
{
|
||||||
|
key: tagKey,
|
||||||
|
value: tagValue
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
: {}),
|
||||||
|
...(kmsKeyId && { kmsKeyId }),
|
||||||
|
...(shouldDisableDelete && { shouldDisableDelete })
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
setIsLoading(false);
|
||||||
|
setPathErrorText("");
|
||||||
|
|
||||||
|
navigate({
|
||||||
|
to: "/secret-manager/$projectId/integrations",
|
||||||
|
params: {
|
||||||
|
projectId: currentWorkspace.id
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return integrationAuth && selectedSourceEnvironment && !isIntegrationAuthAwsKmsKeysLoading ? (
|
||||||
|
<div className="flex h-full w-full flex-col items-center justify-center">
|
||||||
|
<Helmet>
|
||||||
|
<title>Set Up AWS Parameter Integration</title>
|
||||||
|
</Helmet>
|
||||||
|
<Card className="max-w-lg rounded-md border border-mineshaft-600">
|
||||||
|
<CardTitle
|
||||||
|
className="px-6 text-left text-xl"
|
||||||
|
subTitle="Choose which environment in Infisical you want to sync to secerts in AWS Parameter Store."
|
||||||
|
>
|
||||||
|
<div className="flex flex-row items-center">
|
||||||
|
<div className="flex items-center">
|
||||||
|
<img
|
||||||
|
src="/images/integrations/Amazon Web Services.png"
|
||||||
|
height={35}
|
||||||
|
width={35}
|
||||||
|
alt="AWS logo"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<span className="ml-1.5">AWS Parameter Store Integration </span>
|
||||||
|
<a
|
||||||
|
href="https://infisical.com/docs/integrations/cloud/aws-parameter-store"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
>
|
||||||
|
<div className="mb-1 ml-2 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="mb-[0.07rem] ml-1.5 text-xxs"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</CardTitle>
|
||||||
|
<Tabs defaultValue={TabSections.Connection} className="px-6">
|
||||||
|
<TabList>
|
||||||
|
<div className="flex w-full flex-row border-b border-mineshaft-600">
|
||||||
|
<Tab value={TabSections.Connection}>Connection</Tab>
|
||||||
|
<Tab value={TabSections.Options}>Options</Tab>
|
||||||
|
</div>
|
||||||
|
</TabList>
|
||||||
|
<TabPanel value={TabSections.Connection}>
|
||||||
|
<motion.div
|
||||||
|
key="panel-1"
|
||||||
|
transition={{ duration: 0.15 }}
|
||||||
|
initial={{ opacity: 0, translateX: 30 }}
|
||||||
|
animate={{ opacity: 1, translateX: 0 }}
|
||||||
|
exit={{ opacity: 0, translateX: 30 }}
|
||||||
|
>
|
||||||
|
<FormControl label="Project Environment">
|
||||||
|
<Select
|
||||||
|
value={selectedSourceEnvironment}
|
||||||
|
onValueChange={(val) => setSelectedSourceEnvironment(val)}
|
||||||
|
className="w-full border border-mineshaft-500"
|
||||||
|
>
|
||||||
|
{currentWorkspace?.environments.map((sourceEnvironment) => (
|
||||||
|
<SelectItem
|
||||||
|
value={sourceEnvironment.slug}
|
||||||
|
key={`flyio-environment-${sourceEnvironment.slug}`}
|
||||||
|
>
|
||||||
|
{sourceEnvironment.name}
|
||||||
|
</SelectItem>
|
||||||
|
))}
|
||||||
|
</Select>
|
||||||
|
</FormControl>
|
||||||
|
<FormControl label="Secrets Path">
|
||||||
|
<Input
|
||||||
|
value={secretPath}
|
||||||
|
onChange={(evt) => setSecretPath(evt.target.value)}
|
||||||
|
placeholder="Provide a path, default is /"
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
<FormControl label="AWS Region">
|
||||||
|
<Select
|
||||||
|
value={selectedAWSRegion}
|
||||||
|
onValueChange={(val) => {
|
||||||
|
setSelectedAWSRegion(val);
|
||||||
|
setKmsKeyId("");
|
||||||
|
}}
|
||||||
|
className="w-full border border-mineshaft-500"
|
||||||
|
>
|
||||||
|
{awsRegions.map((awsRegion) => (
|
||||||
|
<SelectItem value={awsRegion.slug} key={`aws-environment-${awsRegion.slug}`}>
|
||||||
|
{awsRegion.name} <Badge variant="success">{awsRegion.slug}</Badge>
|
||||||
|
</SelectItem>
|
||||||
|
))}
|
||||||
|
</Select>
|
||||||
|
</FormControl>
|
||||||
|
<FormControl label="Path" errorText={pathErrorText} isError={pathErrorText !== ""}>
|
||||||
|
<Input
|
||||||
|
placeholder={`/${currentWorkspace.name
|
||||||
|
.toLowerCase()
|
||||||
|
.replace(/ /g, "-")}/${selectedSourceEnvironment}/`}
|
||||||
|
value={path}
|
||||||
|
onChange={(e) => setPath(e.target.value)}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
</motion.div>
|
||||||
|
</TabPanel>
|
||||||
|
<TabPanel value={TabSections.Options}>
|
||||||
|
<motion.div
|
||||||
|
key="panel-1"
|
||||||
|
transition={{ duration: 0.15 }}
|
||||||
|
initial={{ opacity: 0, translateX: -30 }}
|
||||||
|
animate={{ opacity: 1, translateX: 0 }}
|
||||||
|
exit={{ opacity: 0, translateX: 30 }}
|
||||||
|
>
|
||||||
|
<div className="ml-1 mt-2">
|
||||||
|
<Switch
|
||||||
|
id="delete-aws"
|
||||||
|
onCheckedChange={setShouldDisableDelete}
|
||||||
|
isChecked={shouldDisableDelete}
|
||||||
|
>
|
||||||
|
Disable deleting secrets in AWS Parameter Store
|
||||||
|
</Switch>
|
||||||
|
</div>
|
||||||
|
<div className="ml-1 mt-4">
|
||||||
|
<Switch id="tag-aws" onCheckedChange={setShouldTag} isChecked={shouldTag}>
|
||||||
|
Tag in AWS Parameter Store
|
||||||
|
</Switch>
|
||||||
|
</div>
|
||||||
|
{shouldTag && (
|
||||||
|
<div className="mt-4">
|
||||||
|
<FormControl label="Tag Key">
|
||||||
|
<Input
|
||||||
|
placeholder="managed-by"
|
||||||
|
value={tagKey}
|
||||||
|
onChange={(e) => setTagKey(e.target.value)}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
<FormControl label="Tag Value">
|
||||||
|
<Input
|
||||||
|
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>
|
||||||
|
<Button
|
||||||
|
onClick={handleButtonClick}
|
||||||
|
color="mineshaft"
|
||||||
|
variant="outline_bg"
|
||||||
|
className="mb-6 ml-auto mr-6 mt-2"
|
||||||
|
isLoading={isLoading}
|
||||||
|
>
|
||||||
|
Create Integration
|
||||||
|
</Button>
|
||||||
|
</Card>
|
||||||
|
<div className="mt-6 w-full max-w-md border-t border-mineshaft-800" />
|
||||||
|
<div className="mt-6 flex w-full max-w-lg flex-col rounded-md border border-mineshaft-600 bg-mineshaft-800 p-4">
|
||||||
|
<div className="flex flex-row items-center">
|
||||||
|
<FontAwesomeIcon icon={faCircleInfo} className="text-xl text-mineshaft-200" />{" "}
|
||||||
|
<span className="text-md ml-3 text-mineshaft-100">Pro Tip</span>
|
||||||
|
</div>
|
||||||
|
<span className="mt-4 text-sm text-mineshaft-300">
|
||||||
|
After creating an integration, your secrets will start syncing immediately. This might
|
||||||
|
cause an unexpected override of current secrets in AWS Parameter Store with secrets from
|
||||||
|
Infisical.
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="flex h-full w-full items-center justify-center">
|
||||||
|
<Helmet>
|
||||||
|
<title>Set Up AWS Parameter Store Integration</title>
|
||||||
|
</Helmet>
|
||||||
|
{isintegrationAuthLoading || isIntegrationAuthAwsKmsKeysLoading ? (
|
||||||
|
<img
|
||||||
|
src="/images/loading/loading.gif"
|
||||||
|
height={70}
|
||||||
|
width={120}
|
||||||
|
alt="infisical loading indicator"
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<div className="flex h-max max-w-md flex-col rounded-md border border-mineshaft-600 bg-mineshaft-800 p-6 text-center text-mineshaft-200">
|
||||||
|
<FontAwesomeIcon icon={faBugs} className="inlineli my-2 text-6xl" />
|
||||||
|
<p>
|
||||||
|
Something went wrong. Please contact{" "}
|
||||||
|
<a
|
||||||
|
className="inline cursor-pointer text-mineshaft-100 underline decoration-primary-500 underline-offset-4 opacity-80 duration-200 hover:opacity-100"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
href="mailto:[email protected]"
|
||||||
|
>
|
||||||
|
support@infisical.com
|
||||||
|
</a>{" "}
|
||||||
|
if the issue persists.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
+16
@@ -0,0 +1,16 @@
|
|||||||
|
import { createFileRoute } from "@tanstack/react-router";
|
||||||
|
import { zodValidator } from "@tanstack/zod-adapter";
|
||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
import { AWSParameterStoreConfigurePage } from "./AwsParamterStoreConfigurePage";
|
||||||
|
|
||||||
|
const AwsParameterStoreConfigurePageQueryParamsSchema = z.object({
|
||||||
|
integrationAuthId: z.string()
|
||||||
|
});
|
||||||
|
|
||||||
|
export const Route = createFileRoute(
|
||||||
|
"/_authenticate/_inject-org-details/secret-manager/$projectId/_secret-manager-layout/integrations/aws-parameter-store/create"
|
||||||
|
)({
|
||||||
|
component: AWSParameterStoreConfigurePage,
|
||||||
|
validateSearch: zodValidator(AwsParameterStoreConfigurePageQueryParamsSchema)
|
||||||
|
});
|
||||||
+205
@@ -0,0 +1,205 @@
|
|||||||
|
import { Helmet } from "react-helmet";
|
||||||
|
import { Controller, useForm } from "react-hook-form";
|
||||||
|
import { faArrowUpRightFromSquare, faBookOpen } from "@fortawesome/free-solid-svg-icons";
|
||||||
|
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||||
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
|
import { useNavigate } from "@tanstack/react-router";
|
||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
import {
|
||||||
|
Button,
|
||||||
|
Card,
|
||||||
|
CardBody,
|
||||||
|
CardTitle,
|
||||||
|
FormControl,
|
||||||
|
Input,
|
||||||
|
Select,
|
||||||
|
SelectItem
|
||||||
|
} from "@app/components/v2";
|
||||||
|
import { useWorkspace } from "@app/context";
|
||||||
|
import { useSaveIntegrationAccessToken } from "@app/hooks/api";
|
||||||
|
|
||||||
|
enum AwsAuthType {
|
||||||
|
AccessKey = "access-key",
|
||||||
|
AssumeRole = "assume-role"
|
||||||
|
}
|
||||||
|
|
||||||
|
const formSchema = z.discriminatedUnion("type", [
|
||||||
|
z.object({
|
||||||
|
type: z.literal(AwsAuthType.AccessKey),
|
||||||
|
accessKey: z.string().min(1),
|
||||||
|
accessSecretKey: z.string().min(1)
|
||||||
|
}),
|
||||||
|
z.object({
|
||||||
|
type: z.literal(AwsAuthType.AssumeRole),
|
||||||
|
iamRoleArn: z.string().min(1)
|
||||||
|
})
|
||||||
|
]);
|
||||||
|
|
||||||
|
type TForm = z.infer<typeof formSchema>;
|
||||||
|
|
||||||
|
export const AWSSecretManagerAuthorizePage = () => {
|
||||||
|
const navigate = useNavigate();
|
||||||
|
const { mutateAsync } = useSaveIntegrationAccessToken();
|
||||||
|
const { currentWorkspace } = useWorkspace();
|
||||||
|
|
||||||
|
const { control, handleSubmit, formState, watch } = useForm<TForm>({
|
||||||
|
resolver: zodResolver(formSchema),
|
||||||
|
defaultValues: {
|
||||||
|
type: AwsAuthType.AccessKey
|
||||||
|
}
|
||||||
|
});
|
||||||
|
const formAwsAuthTypeField = watch("type");
|
||||||
|
|
||||||
|
const handleFormSubmit = async (data: TForm) => {
|
||||||
|
try {
|
||||||
|
const integrationAuth = await mutateAsync({
|
||||||
|
workspaceId: currentWorkspace.id,
|
||||||
|
integration: "aws-secret-manager",
|
||||||
|
...(data.type === AwsAuthType.AssumeRole
|
||||||
|
? {
|
||||||
|
awsAssumeIamRoleArn: data.iamRoleArn
|
||||||
|
}
|
||||||
|
: {
|
||||||
|
accessId: data.accessKey,
|
||||||
|
accessToken: data.accessSecretKey
|
||||||
|
})
|
||||||
|
});
|
||||||
|
navigate({
|
||||||
|
to: "/secret-manager/$projectId/integrations/aws-secret-manager/create",
|
||||||
|
params: {
|
||||||
|
projectId: currentWorkspace.id
|
||||||
|
},
|
||||||
|
search: {
|
||||||
|
integrationAuthId: integrationAuth.id
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex h-full w-full items-center justify-center">
|
||||||
|
<Helmet>
|
||||||
|
<title>Authorize AWS Secrets Manager Integration</title>
|
||||||
|
</Helmet>
|
||||||
|
<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="flex items-center">
|
||||||
|
<img
|
||||||
|
src="/images/integrations/Amazon Web Services.png"
|
||||||
|
height={35}
|
||||||
|
width={35}
|
||||||
|
alt="AWS logo"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<span className="ml-1.5">AWS Secrets Manager Integration </span>
|
||||||
|
<a
|
||||||
|
href="https://infisical.com/docs/integrations/cloud/aws-secret-manager"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
>
|
||||||
|
<div className="mb-1 ml-2 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="mb-[0.07rem] ml-1.5 text-xxs"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</CardTitle>
|
||||||
|
<CardBody>
|
||||||
|
<form onSubmit={handleSubmit(handleFormSubmit)}>
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
|
name="type"
|
||||||
|
defaultValue={AwsAuthType.AccessKey}
|
||||||
|
render={({ field: { onChange, ...field }, fieldState: { error } }) => (
|
||||||
|
<FormControl
|
||||||
|
label="Authentication Mode"
|
||||||
|
errorText={error?.message}
|
||||||
|
isError={Boolean(error)}
|
||||||
|
>
|
||||||
|
<Select
|
||||||
|
defaultValue={field.value}
|
||||||
|
{...field}
|
||||||
|
onValueChange={(e) => onChange(e)}
|
||||||
|
className="w-full"
|
||||||
|
>
|
||||||
|
<SelectItem value={AwsAuthType.AccessKey}>Access Key</SelectItem>
|
||||||
|
<SelectItem value={AwsAuthType.AssumeRole}>AWS Assume Role</SelectItem>
|
||||||
|
</Select>
|
||||||
|
</FormControl>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
{formAwsAuthTypeField === AwsAuthType.AccessKey ? (
|
||||||
|
<>
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
|
name="accessKey"
|
||||||
|
render={({ field, fieldState: { error } }) => (
|
||||||
|
<FormControl
|
||||||
|
label="Access Key ID"
|
||||||
|
errorText={error?.message}
|
||||||
|
isError={Boolean(error)}
|
||||||
|
>
|
||||||
|
<Input placeholder="" {...field} />
|
||||||
|
</FormControl>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
|
name="accessSecretKey"
|
||||||
|
render={({ field, fieldState: { error } }) => (
|
||||||
|
<FormControl
|
||||||
|
label="Secret Access Key"
|
||||||
|
errorText={error?.message}
|
||||||
|
isError={Boolean(error)}
|
||||||
|
>
|
||||||
|
<Input
|
||||||
|
type="password"
|
||||||
|
autoComplete="new-password"
|
||||||
|
placeholder=""
|
||||||
|
{...field}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
|
name="iamRoleArn"
|
||||||
|
render={({ field, fieldState: { error } }) => (
|
||||||
|
<FormControl
|
||||||
|
label="IAM Role ARN For Role Assumption"
|
||||||
|
errorText={error?.message}
|
||||||
|
isError={Boolean(error)}
|
||||||
|
>
|
||||||
|
<Input placeholder="" {...field} />
|
||||||
|
</FormControl>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
<Button
|
||||||
|
type="submit"
|
||||||
|
colorSchema="primary"
|
||||||
|
variant="outline_bg"
|
||||||
|
className="mb-6 ml-auto mr-6 mt-2 w-min"
|
||||||
|
isLoading={formState.isSubmitting}
|
||||||
|
>
|
||||||
|
Connect to AWS Secrets Manager
|
||||||
|
</Button>
|
||||||
|
</form>
|
||||||
|
</CardBody>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
+9
@@ -0,0 +1,9 @@
|
|||||||
|
import { createFileRoute } from "@tanstack/react-router";
|
||||||
|
|
||||||
|
import { AWSSecretManagerAuthorizePage } from "./AwsSecretManagerAuthorizePage";
|
||||||
|
|
||||||
|
export const Route = createFileRoute(
|
||||||
|
"/_authenticate/_inject-org-details/secret-manager/$projectId/_secret-manager-layout/integrations/aws-secret-manager/authorize"
|
||||||
|
)({
|
||||||
|
component: AWSSecretManagerAuthorizePage
|
||||||
|
});
|
||||||
+544
@@ -0,0 +1,544 @@
|
|||||||
|
import { useEffect } from "react";
|
||||||
|
import { Helmet } from "react-helmet";
|
||||||
|
import { Controller, useForm } from "react-hook-form";
|
||||||
|
import {
|
||||||
|
faArrowUpRightFromSquare,
|
||||||
|
faBookOpen,
|
||||||
|
faBugs,
|
||||||
|
faCircleInfo
|
||||||
|
} from "@fortawesome/free-solid-svg-icons";
|
||||||
|
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||||
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
|
import { useNavigate, useSearch } from "@tanstack/react-router";
|
||||||
|
import { motion } from "framer-motion";
|
||||||
|
import z from "zod";
|
||||||
|
|
||||||
|
import {
|
||||||
|
Badge,
|
||||||
|
Button,
|
||||||
|
Card,
|
||||||
|
CardTitle,
|
||||||
|
FormControl,
|
||||||
|
Input,
|
||||||
|
Select,
|
||||||
|
SelectItem,
|
||||||
|
Switch,
|
||||||
|
Tab,
|
||||||
|
TabList,
|
||||||
|
TabPanel,
|
||||||
|
Tabs
|
||||||
|
} from "@app/components/v2";
|
||||||
|
import { SecretPathInput } from "@app/components/v2/SecretPathInput";
|
||||||
|
import { ROUTE_PATHS } from "@app/const/routes";
|
||||||
|
import { useWorkspace } from "@app/context";
|
||||||
|
import { useCreateIntegration } from "@app/hooks/api";
|
||||||
|
import { useGetIntegrationAuthById } from "@app/hooks/api/integrationAuth";
|
||||||
|
import { useGetIntegrationAuthAwsKmsKeys } from "@app/hooks/api/integrationAuth/queries";
|
||||||
|
import { IntegrationMappingBehavior } from "@app/hooks/api/integrations/types";
|
||||||
|
|
||||||
|
enum TabSections {
|
||||||
|
Connection = "connection",
|
||||||
|
Options = "options"
|
||||||
|
}
|
||||||
|
|
||||||
|
const awsRegions = [
|
||||||
|
{ name: "US East (Ohio)", slug: "us-east-2" },
|
||||||
|
{ name: "US East (N. Virginia)", slug: "us-east-1" },
|
||||||
|
{ name: "US West (N. California)", slug: "us-west-1" },
|
||||||
|
{ name: "US West (Oregon)", slug: "us-west-2" },
|
||||||
|
{ name: "Africa (Cape Town)", slug: "af-south-1" },
|
||||||
|
{ name: "Asia Pacific (Hong Kong)", slug: "ap-east-1" },
|
||||||
|
{ name: "Asia Pacific (Hyderabad)", slug: "ap-south-2" },
|
||||||
|
{ name: "Asia Pacific (Jakarta)", slug: "ap-southeast-3" },
|
||||||
|
{ name: "Asia Pacific (Melbourne)", slug: "ap-southeast-4" },
|
||||||
|
{ name: "Asia Pacific (Mumbai)", slug: "ap-south-1" },
|
||||||
|
{ name: "Asia Pacific (Osaka)", slug: "ap-northeast-3" },
|
||||||
|
{ name: "Asia Pacific (Seoul)", slug: "ap-northeast-2" },
|
||||||
|
{ name: "Asia Pacific (Singapore)", slug: "ap-southeast-1" },
|
||||||
|
{ name: "Asia Pacific (Sydney)", slug: "ap-southeast-2" },
|
||||||
|
{ name: "Asia Pacific (Tokyo)", slug: "ap-northeast-1" },
|
||||||
|
{ name: "Canada (Central)", slug: "ca-central-1" },
|
||||||
|
{ name: "Europe (Frankfurt)", slug: "eu-central-1" },
|
||||||
|
{ name: "Europe (Ireland)", slug: "eu-west-1" },
|
||||||
|
{ name: "Europe (London)", slug: "eu-west-2" },
|
||||||
|
{ name: "Europe (Milan)", slug: "eu-south-1" },
|
||||||
|
{ name: "Europe (Paris)", slug: "eu-west-3" },
|
||||||
|
{ name: "Europe (Spain)", slug: "eu-south-2" },
|
||||||
|
{ name: "Europe (Stockholm)", slug: "eu-north-1" },
|
||||||
|
{ name: "Europe (Zurich)", slug: "eu-central-2" },
|
||||||
|
{ name: "Middle East (Bahrain)", slug: "me-south-1" },
|
||||||
|
{ name: "Middle East (UAE)", slug: "me-central-1" },
|
||||||
|
{ name: "South America (Sao Paulo)", slug: "sa-east-1" },
|
||||||
|
{ name: "AWS GovCloud (US-East)", slug: "us-gov-east-1" },
|
||||||
|
{ name: "AWS GovCloud (US-West)", slug: "us-gov-west-1" }
|
||||||
|
];
|
||||||
|
|
||||||
|
const mappingBehaviors = [
|
||||||
|
{
|
||||||
|
label: "Many to One (All Infisical secrets will be mapped to a single AWS secret)",
|
||||||
|
value: IntegrationMappingBehavior.MANY_TO_ONE
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "One to One - (Each Infisical secret will be mapped to its own AWS secret)",
|
||||||
|
value: IntegrationMappingBehavior.ONE_TO_ONE
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
const schema = z
|
||||||
|
.object({
|
||||||
|
awsRegion: z.string().trim().min(1, { message: "AWS region is required" }),
|
||||||
|
secretPath: z.string().trim().min(1, { message: "Secret path is required" }),
|
||||||
|
sourceEnvironment: z.string().trim().min(1, { message: "Source environment is required" }),
|
||||||
|
secretPrefix: z.string().default(""),
|
||||||
|
secretName: z.string().trim().min(1).optional(),
|
||||||
|
mappingBehavior: z.nativeEnum(IntegrationMappingBehavior),
|
||||||
|
kmsKeyId: z.string().optional(),
|
||||||
|
shouldTag: z.boolean().optional(),
|
||||||
|
tags: z
|
||||||
|
.object({
|
||||||
|
key: z.string(),
|
||||||
|
value: z.string()
|
||||||
|
})
|
||||||
|
.array()
|
||||||
|
})
|
||||||
|
.refine(
|
||||||
|
(val) =>
|
||||||
|
val.mappingBehavior === IntegrationMappingBehavior.ONE_TO_ONE ||
|
||||||
|
(val.mappingBehavior === IntegrationMappingBehavior.MANY_TO_ONE &&
|
||||||
|
val.secretName &&
|
||||||
|
val.secretName !== ""),
|
||||||
|
{
|
||||||
|
message: "Secret name must be defined for many-to-one integrations",
|
||||||
|
path: ["secretName"]
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
type TFormSchema = z.infer<typeof schema>;
|
||||||
|
|
||||||
|
export const AwsSecretManagerConfigurePage = () => {
|
||||||
|
const navigate = useNavigate();
|
||||||
|
const { mutateAsync } = useCreateIntegration();
|
||||||
|
const {
|
||||||
|
control,
|
||||||
|
setValue,
|
||||||
|
handleSubmit,
|
||||||
|
watch,
|
||||||
|
formState: { isSubmitting }
|
||||||
|
} = useForm<TFormSchema>({
|
||||||
|
resolver: zodResolver(schema),
|
||||||
|
defaultValues: {
|
||||||
|
shouldTag: false,
|
||||||
|
secretPath: "/",
|
||||||
|
secretPrefix: "",
|
||||||
|
mappingBehavior: IntegrationMappingBehavior.MANY_TO_ONE,
|
||||||
|
tags: []
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const shouldTagState = watch("shouldTag");
|
||||||
|
const selectedSourceEnvironment = watch("sourceEnvironment");
|
||||||
|
const selectedAWSRegion = watch("awsRegion");
|
||||||
|
const selectedMappingBehavior = watch("mappingBehavior");
|
||||||
|
const integrationAuthId = useSearch({
|
||||||
|
from: ROUTE_PATHS.SecretManager.Integratons.AwsSecretManagerConfigurePage.id,
|
||||||
|
select: (el) => el.integrationAuthId
|
||||||
|
});
|
||||||
|
|
||||||
|
const { currentWorkspace } = useWorkspace();
|
||||||
|
const { data: integrationAuth, isLoading: isintegrationAuthLoading } = useGetIntegrationAuthById(
|
||||||
|
(integrationAuthId as string) ?? ""
|
||||||
|
);
|
||||||
|
|
||||||
|
const { data: integrationAuthAwsKmsKeys, isLoading: isIntegrationAuthAwsKmsKeysLoading } =
|
||||||
|
useGetIntegrationAuthAwsKmsKeys({
|
||||||
|
integrationAuthId: String(integrationAuthId),
|
||||||
|
region: selectedAWSRegion
|
||||||
|
});
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (currentWorkspace) {
|
||||||
|
setValue("sourceEnvironment", currentWorkspace.environments[0].slug);
|
||||||
|
setValue("awsRegion", awsRegions[0].slug);
|
||||||
|
}
|
||||||
|
}, [currentWorkspace]);
|
||||||
|
|
||||||
|
const handleButtonClick = async ({
|
||||||
|
secretName,
|
||||||
|
sourceEnvironment,
|
||||||
|
awsRegion,
|
||||||
|
secretPath,
|
||||||
|
shouldTag,
|
||||||
|
tags,
|
||||||
|
secretPrefix,
|
||||||
|
kmsKeyId,
|
||||||
|
mappingBehavior
|
||||||
|
}: TFormSchema) => {
|
||||||
|
try {
|
||||||
|
if (!integrationAuth?.id) return;
|
||||||
|
|
||||||
|
await mutateAsync({
|
||||||
|
integrationAuthId: integrationAuth?.id,
|
||||||
|
isActive: true,
|
||||||
|
app: secretName,
|
||||||
|
sourceEnvironment,
|
||||||
|
region: awsRegion,
|
||||||
|
secretPath,
|
||||||
|
metadata: {
|
||||||
|
...(shouldTag
|
||||||
|
? {
|
||||||
|
secretAWSTag: tags
|
||||||
|
}
|
||||||
|
: {}),
|
||||||
|
...(secretPrefix && { secretPrefix }),
|
||||||
|
...(kmsKeyId && { kmsKeyId }),
|
||||||
|
mappingBehavior
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
navigate({
|
||||||
|
to: "/secret-manager/$projectId/integrations",
|
||||||
|
params: {
|
||||||
|
projectId: currentWorkspace.id
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return integrationAuth && selectedSourceEnvironment && !isIntegrationAuthAwsKmsKeysLoading ? (
|
||||||
|
<div className="flex h-full w-full flex-col items-center justify-center">
|
||||||
|
<Helmet>
|
||||||
|
<title>Set Up AWS Secrets Manager Integration</title>
|
||||||
|
</Helmet>
|
||||||
|
<form onSubmit={handleSubmit(handleButtonClick)}>
|
||||||
|
<Card className="max-w-lg rounded-md border border-mineshaft-600">
|
||||||
|
<CardTitle
|
||||||
|
className="px-6 text-left text-xl"
|
||||||
|
subTitle="Choose which environment in Infisical you want to sync to secerts in AWS Secrets Manager."
|
||||||
|
>
|
||||||
|
<div className="flex flex-row items-center">
|
||||||
|
<div className="flex items-center">
|
||||||
|
<img
|
||||||
|
src="/images/integrations/Amazon Web Services.png"
|
||||||
|
height={35}
|
||||||
|
width={35}
|
||||||
|
alt="AWS logo"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<span className="ml-1.5">AWS Secrets Manager Integration </span>
|
||||||
|
<a
|
||||||
|
href="https://infisical.com/docs/integrations/cloud/aws-secret-manager"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
>
|
||||||
|
<div className="mb-1 ml-2 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="mb-[0.07rem] ml-1.5 text-xxs"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</CardTitle>
|
||||||
|
<Tabs defaultValue={TabSections.Connection} className="px-6">
|
||||||
|
<TabList>
|
||||||
|
<div className="flex w-full flex-row border-b border-mineshaft-600">
|
||||||
|
<Tab value={TabSections.Connection}>Connection</Tab>
|
||||||
|
<Tab value={TabSections.Options}>Options</Tab>
|
||||||
|
</div>
|
||||||
|
</TabList>
|
||||||
|
<TabPanel value={TabSections.Connection}>
|
||||||
|
<motion.div
|
||||||
|
key="panel-1"
|
||||||
|
transition={{ duration: 0.15 }}
|
||||||
|
initial={{ opacity: 0, translateX: 30 }}
|
||||||
|
animate={{ opacity: 1, translateX: 0 }}
|
||||||
|
exit={{ opacity: 0, translateX: 30 }}
|
||||||
|
>
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
|
name="sourceEnvironment"
|
||||||
|
render={({ field, fieldState: { error } }) => (
|
||||||
|
<FormControl
|
||||||
|
label="Project Environment"
|
||||||
|
errorText={error?.message}
|
||||||
|
isError={Boolean(error)}
|
||||||
|
>
|
||||||
|
<Select
|
||||||
|
className="w-full border border-mineshaft-500"
|
||||||
|
dropdownContainerClassName="max-w-full"
|
||||||
|
value={field.value}
|
||||||
|
onValueChange={(val) => {
|
||||||
|
field.onChange(val);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{currentWorkspace?.environments.map((sourceEnvironment) => (
|
||||||
|
<SelectItem
|
||||||
|
value={sourceEnvironment.slug}
|
||||||
|
key={`source-environment-${sourceEnvironment.slug}`}
|
||||||
|
>
|
||||||
|
{sourceEnvironment.name}
|
||||||
|
</SelectItem>
|
||||||
|
))}
|
||||||
|
</Select>
|
||||||
|
</FormControl>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
|
name="secretPath"
|
||||||
|
render={({ field, fieldState: { error } }) => (
|
||||||
|
<FormControl
|
||||||
|
label="Secrets Path"
|
||||||
|
errorText={error?.message}
|
||||||
|
isError={Boolean(error)}
|
||||||
|
>
|
||||||
|
<SecretPathInput {...field} />
|
||||||
|
</FormControl>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
|
name="awsRegion"
|
||||||
|
render={({ field: { onChange, ...field }, fieldState: { error } }) => (
|
||||||
|
<FormControl
|
||||||
|
label="AWS region"
|
||||||
|
errorText={error?.message}
|
||||||
|
isError={Boolean(error)}
|
||||||
|
>
|
||||||
|
<Select
|
||||||
|
defaultValue={field.value}
|
||||||
|
onValueChange={(e) => onChange(e)}
|
||||||
|
className="w-full border border-mineshaft-500"
|
||||||
|
dropdownContainerClassName="max-w-full"
|
||||||
|
>
|
||||||
|
{awsRegions.map((awsRegion) => (
|
||||||
|
<SelectItem
|
||||||
|
value={awsRegion.slug}
|
||||||
|
className="flex w-full justify-between"
|
||||||
|
key={`aws-environment-${awsRegion.slug}`}
|
||||||
|
>
|
||||||
|
{awsRegion.name} <Badge variant="success">{awsRegion.slug}</Badge>
|
||||||
|
</SelectItem>
|
||||||
|
))}
|
||||||
|
</Select>
|
||||||
|
</FormControl>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
|
name="mappingBehavior"
|
||||||
|
render={({ field: { onChange, ...field }, fieldState: { error } }) => (
|
||||||
|
<FormControl
|
||||||
|
label="Mapping Behavior"
|
||||||
|
errorText={error?.message}
|
||||||
|
isError={Boolean(error)}
|
||||||
|
>
|
||||||
|
<Select
|
||||||
|
defaultValue={field.value}
|
||||||
|
onValueChange={(e) => onChange(e)}
|
||||||
|
className="w-full border border-mineshaft-500"
|
||||||
|
dropdownContainerClassName="max-w-full"
|
||||||
|
>
|
||||||
|
{mappingBehaviors.map((option) => (
|
||||||
|
<SelectItem
|
||||||
|
value={option.value}
|
||||||
|
className="text-left"
|
||||||
|
key={`mapping-behavior-${option.value}`}
|
||||||
|
>
|
||||||
|
{option.label}
|
||||||
|
</SelectItem>
|
||||||
|
))}
|
||||||
|
</Select>
|
||||||
|
</FormControl>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
{selectedMappingBehavior === IntegrationMappingBehavior.MANY_TO_ONE && (
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
|
name="secretName"
|
||||||
|
render={({ field, fieldState: { error } }) => (
|
||||||
|
<FormControl
|
||||||
|
label="AWS SM Secret Name"
|
||||||
|
errorText={error?.message}
|
||||||
|
isError={Boolean(error)}
|
||||||
|
>
|
||||||
|
<Input
|
||||||
|
placeholder={`${currentWorkspace.name
|
||||||
|
.toLowerCase()
|
||||||
|
.replace(/ /g, "-")}/${selectedSourceEnvironment}`}
|
||||||
|
{...field}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</motion.div>
|
||||||
|
</TabPanel>
|
||||||
|
<TabPanel value={TabSections.Options}>
|
||||||
|
<motion.div
|
||||||
|
key="panel-1"
|
||||||
|
transition={{ duration: 0.15 }}
|
||||||
|
initial={{ opacity: 0, translateX: -30 }}
|
||||||
|
animate={{ opacity: 1, translateX: 0 }}
|
||||||
|
exit={{ opacity: 0, translateX: 30 }}
|
||||||
|
>
|
||||||
|
<div className="ml-1 mt-2">
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
|
name="shouldTag"
|
||||||
|
render={({ field: { onChange, value } }) => (
|
||||||
|
<Switch
|
||||||
|
id="tag-aws"
|
||||||
|
onCheckedChange={(isChecked) => onChange(isChecked)}
|
||||||
|
isChecked={value}
|
||||||
|
>
|
||||||
|
Tag in AWS Secrets Manager
|
||||||
|
</Switch>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
{shouldTagState && (
|
||||||
|
<div className="mt-4 flex justify-between">
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
|
name="tags.0.key"
|
||||||
|
render={({ field, fieldState: { error } }) => (
|
||||||
|
<FormControl
|
||||||
|
label="Tag Key"
|
||||||
|
errorText={error?.message}
|
||||||
|
isError={Boolean(error)}
|
||||||
|
>
|
||||||
|
<Input placeholder="managed-by" {...field} />
|
||||||
|
</FormControl>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
|
name="tags.0.value"
|
||||||
|
render={({ field, fieldState: { error } }) => (
|
||||||
|
<FormControl
|
||||||
|
label="Tag Value"
|
||||||
|
errorText={error?.message}
|
||||||
|
isError={Boolean(error)}
|
||||||
|
>
|
||||||
|
<Input placeholder="infisical" {...field} />
|
||||||
|
</FormControl>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
|
name="secretPrefix"
|
||||||
|
render={({ field, fieldState: { error } }) => (
|
||||||
|
<FormControl
|
||||||
|
label="Secret Prefix"
|
||||||
|
errorText={error?.message}
|
||||||
|
isError={Boolean(error)}
|
||||||
|
className="mt-4"
|
||||||
|
>
|
||||||
|
<Input placeholder="INFISICAL_" {...field} />
|
||||||
|
</FormControl>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
|
name="kmsKeyId"
|
||||||
|
render={({ field: { onChange, ...field }, fieldState: { error } }) => (
|
||||||
|
<FormControl
|
||||||
|
label="Encryption Key"
|
||||||
|
errorText={error?.message}
|
||||||
|
isError={Boolean(error)}
|
||||||
|
>
|
||||||
|
<Select
|
||||||
|
defaultValue={field.value}
|
||||||
|
onValueChange={(e) => onChange(e)}
|
||||||
|
className="w-full border border-mineshaft-500"
|
||||||
|
dropdownContainerClassName="max-w-full"
|
||||||
|
>
|
||||||
|
{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>
|
||||||
|
);
|
||||||
|
})
|
||||||
|
) : (
|
||||||
|
<SelectItem isDisabled value="no-keys" key="no-keys">
|
||||||
|
No KMS keys available
|
||||||
|
</SelectItem>
|
||||||
|
)}
|
||||||
|
</Select>
|
||||||
|
</FormControl>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</motion.div>
|
||||||
|
</TabPanel>
|
||||||
|
</Tabs>
|
||||||
|
<Button
|
||||||
|
color="mineshaft"
|
||||||
|
variant="outline_bg"
|
||||||
|
type="submit"
|
||||||
|
className="mb-6 ml-auto mr-6 mt-2"
|
||||||
|
isLoading={isSubmitting}
|
||||||
|
>
|
||||||
|
Create Integration
|
||||||
|
</Button>
|
||||||
|
</Card>
|
||||||
|
<div className="mt-6 w-full max-w-md border-t border-mineshaft-800" />
|
||||||
|
<div className="mt-6 flex w-full max-w-lg flex-col rounded-md border border-mineshaft-600 bg-mineshaft-800 p-4">
|
||||||
|
<div className="flex flex-row items-center">
|
||||||
|
<FontAwesomeIcon icon={faCircleInfo} className="text-xl text-mineshaft-200" />{" "}
|
||||||
|
<span className="text-md ml-3 text-mineshaft-100">Pro Tip</span>
|
||||||
|
</div>
|
||||||
|
<span className="mt-4 text-sm text-mineshaft-300">
|
||||||
|
After creating an integration, your secrets will start syncing immediately. This might
|
||||||
|
cause an unexpected override of current secrets in AWS Secrets Manager with secrets from
|
||||||
|
Infisical.
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="flex h-full w-full items-center justify-center">
|
||||||
|
<Helmet>
|
||||||
|
<title>Set Up AWS Secrets Manager Integration</title>
|
||||||
|
</Helmet>
|
||||||
|
{isintegrationAuthLoading || isIntegrationAuthAwsKmsKeysLoading ? (
|
||||||
|
<img
|
||||||
|
src="/images/loading/loading.gif"
|
||||||
|
height={70}
|
||||||
|
width={120}
|
||||||
|
alt="infisical loading indicator"
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<div className="flex h-max max-w-md flex-col rounded-md border border-mineshaft-600 bg-mineshaft-800 p-6 text-center text-mineshaft-200">
|
||||||
|
<FontAwesomeIcon icon={faBugs} className="inlineli my-2 text-6xl" />
|
||||||
|
<p>
|
||||||
|
Something went wrong. Please contact{" "}
|
||||||
|
<a
|
||||||
|
className="inline cursor-pointer text-mineshaft-100 underline decoration-primary-500 underline-offset-4 opacity-80 duration-200 hover:opacity-100"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
href="mailto:[email protected]"
|
||||||
|
>
|
||||||
|
support@infisical.com
|
||||||
|
</a>{" "}
|
||||||
|
if the issue persists.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
+16
@@ -0,0 +1,16 @@
|
|||||||
|
import { createFileRoute } from "@tanstack/react-router";
|
||||||
|
import { zodValidator } from "@tanstack/zod-adapter";
|
||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
import { AwsSecretManagerConfigurePage } from "./AwsSecretManagerConfigurePage";
|
||||||
|
|
||||||
|
const AwsSecretManagerConfigurePageQueryParamsSchema = z.object({
|
||||||
|
integrationAuthId: z.string()
|
||||||
|
});
|
||||||
|
|
||||||
|
export const Route = createFileRoute(
|
||||||
|
"/_authenticate/_inject-org-details/secret-manager/$projectId/_secret-manager-layout/integrations/aws-secret-manager/create"
|
||||||
|
)({
|
||||||
|
component: AwsSecretManagerConfigurePage,
|
||||||
|
validateSearch: zodValidator(AwsSecretManagerConfigurePageQueryParamsSchema)
|
||||||
|
});
|
||||||
+325
@@ -0,0 +1,325 @@
|
|||||||
|
import { useEffect } from "react";
|
||||||
|
import { Helmet } from "react-helmet";
|
||||||
|
import { Controller, useForm } from "react-hook-form";
|
||||||
|
import { faArrowUpRightFromSquare, faBookOpen } from "@fortawesome/free-solid-svg-icons";
|
||||||
|
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||||
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
|
import { useNavigate, useSearch } from "@tanstack/react-router";
|
||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
import { createNotification } from "@app/components/notifications";
|
||||||
|
import {
|
||||||
|
Button,
|
||||||
|
Card,
|
||||||
|
CardTitle,
|
||||||
|
FormControl,
|
||||||
|
FormLabel,
|
||||||
|
Input,
|
||||||
|
Select,
|
||||||
|
SelectItem,
|
||||||
|
Switch
|
||||||
|
} from "@app/components/v2";
|
||||||
|
import { SecretPathInput } from "@app/components/v2/SecretPathInput";
|
||||||
|
import { ROUTE_PATHS } from "@app/const/routes";
|
||||||
|
import { useWorkspace } from "@app/context";
|
||||||
|
import { useCreateIntegration } from "@app/hooks/api";
|
||||||
|
import { useGetIntegrationAuthById } from "@app/hooks/api/integrationAuth";
|
||||||
|
import { IntegrationSyncBehavior } from "@app/hooks/api/integrations/types";
|
||||||
|
import { useGetWorkspaceById } from "@app/hooks/api/workspace";
|
||||||
|
|
||||||
|
const schema = z.object({
|
||||||
|
baseUrl: z
|
||||||
|
.string()
|
||||||
|
.trim()
|
||||||
|
.min(1, { message: "Azure App Configuration URL is required" })
|
||||||
|
.url()
|
||||||
|
.refine(
|
||||||
|
(val) => val.endsWith(".azconfig.io"),
|
||||||
|
"URL should have the following format: https://resource-name-here.azconfig.io"
|
||||||
|
),
|
||||||
|
secretPath: z.string().trim().min(1, { message: "Secret path is required" }),
|
||||||
|
sourceEnvironment: z.string().trim().min(1, { message: "Source environment is required" }),
|
||||||
|
initialSyncBehavior: z.nativeEnum(IntegrationSyncBehavior),
|
||||||
|
secretPrefix: z.string().default(""),
|
||||||
|
useLabels: z.boolean().default(false),
|
||||||
|
azureLabel: z.string().min(1).optional()
|
||||||
|
});
|
||||||
|
|
||||||
|
type TFormSchema = z.infer<typeof schema>;
|
||||||
|
|
||||||
|
const initialSyncBehaviors = [
|
||||||
|
{
|
||||||
|
label: "No Import - Overwrite all values in Azure App Configuration",
|
||||||
|
value: IntegrationSyncBehavior.OVERWRITE_TARGET
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Import - Prefer values from Azure App Configuration",
|
||||||
|
value: IntegrationSyncBehavior.PREFER_TARGET
|
||||||
|
},
|
||||||
|
{ label: "Import - Prefer values from Infisical", value: IntegrationSyncBehavior.PREFER_SOURCE }
|
||||||
|
];
|
||||||
|
|
||||||
|
export const AzureAppConfigurationConfigurePage = () => {
|
||||||
|
const navigate = useNavigate();
|
||||||
|
const {
|
||||||
|
control,
|
||||||
|
watch,
|
||||||
|
setValue,
|
||||||
|
handleSubmit,
|
||||||
|
formState: { isSubmitting }
|
||||||
|
} = useForm<TFormSchema>({
|
||||||
|
resolver: zodResolver(schema),
|
||||||
|
defaultValues: {
|
||||||
|
secretPath: "/",
|
||||||
|
secretPrefix: "",
|
||||||
|
initialSyncBehavior: IntegrationSyncBehavior.PREFER_SOURCE
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const { mutateAsync } = useCreateIntegration();
|
||||||
|
|
||||||
|
const integrationAuthId = useSearch({
|
||||||
|
from: ROUTE_PATHS.SecretManager.Integratons.AzureAppConfigurationsConfigurePage.id,
|
||||||
|
select: (el) => el.integrationAuthId
|
||||||
|
});
|
||||||
|
const { currentWorkspace } = useWorkspace();
|
||||||
|
|
||||||
|
const { data: workspace } = useGetWorkspaceById(currentWorkspace.id);
|
||||||
|
const { data: integrationAuth } = useGetIntegrationAuthById((integrationAuthId as string) ?? "");
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (workspace) {
|
||||||
|
setValue("sourceEnvironment", workspace.environments[0].slug);
|
||||||
|
}
|
||||||
|
}, [workspace]);
|
||||||
|
|
||||||
|
const shouldUseLabels = watch("useLabels");
|
||||||
|
|
||||||
|
const handleIntegrationSubmit = async ({
|
||||||
|
secretPath,
|
||||||
|
useLabels,
|
||||||
|
sourceEnvironment,
|
||||||
|
baseUrl,
|
||||||
|
initialSyncBehavior,
|
||||||
|
secretPrefix,
|
||||||
|
azureLabel
|
||||||
|
}: TFormSchema) => {
|
||||||
|
try {
|
||||||
|
if (!integrationAuth?.id) return;
|
||||||
|
|
||||||
|
if (useLabels && !azureLabel) {
|
||||||
|
createNotification({
|
||||||
|
type: "error",
|
||||||
|
text: "Label must be provided when 'Use Labels' is enabled"
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await mutateAsync({
|
||||||
|
integrationAuthId: integrationAuth?.id,
|
||||||
|
isActive: true,
|
||||||
|
app: baseUrl,
|
||||||
|
sourceEnvironment,
|
||||||
|
secretPath,
|
||||||
|
metadata: {
|
||||||
|
initialSyncBehavior,
|
||||||
|
secretPrefix,
|
||||||
|
...(useLabels && { azureLabel })
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
navigate({
|
||||||
|
to: "/secret-manager/$projectId/integrations",
|
||||||
|
params: {
|
||||||
|
projectId: currentWorkspace.id
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return integrationAuth && workspace ? (
|
||||||
|
<form
|
||||||
|
onSubmit={handleSubmit(handleIntegrationSubmit)}
|
||||||
|
className="flex h-full w-full flex-col items-center justify-center"
|
||||||
|
>
|
||||||
|
<Helmet>
|
||||||
|
<title>Set Up Azure App Configuration Integration</title>
|
||||||
|
</Helmet>
|
||||||
|
<Card className="max-w-lg rounded-md border border-mineshaft-600">
|
||||||
|
<CardTitle
|
||||||
|
className="text-left text-xl"
|
||||||
|
subTitle="Choose which environment in Infisical you want to sync to your Azure App Configuration."
|
||||||
|
>
|
||||||
|
<div className="flex flex-row items-center">
|
||||||
|
<div className="flex items-center">
|
||||||
|
<img
|
||||||
|
src="/images/integrations/Microsoft Azure.png"
|
||||||
|
height={35}
|
||||||
|
width={35}
|
||||||
|
alt="Azure logo"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<span className="ml-1.5">Azure App Configuration</span>
|
||||||
|
<a
|
||||||
|
href="https://infisical.com/docs/integrations/cloud/azure-app-configuration"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
>
|
||||||
|
<div className="mb-1 ml-2 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="mb-[0.07rem] ml-1.5 text-xxs"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</CardTitle>
|
||||||
|
<div className="px-6">
|
||||||
|
<div className="">
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
|
name="sourceEnvironment"
|
||||||
|
render={({ field, fieldState: { error } }) => (
|
||||||
|
<FormControl
|
||||||
|
label="Project Environment"
|
||||||
|
errorText={error?.message}
|
||||||
|
isError={Boolean(error)}
|
||||||
|
>
|
||||||
|
<Select
|
||||||
|
className="w-full border border-mineshaft-500"
|
||||||
|
dropdownContainerClassName="max-w-full"
|
||||||
|
value={field.value}
|
||||||
|
onValueChange={(val) => {
|
||||||
|
field.onChange(val);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{workspace?.environments.map((sourceEnvironment) => (
|
||||||
|
<SelectItem
|
||||||
|
value={sourceEnvironment.slug}
|
||||||
|
key={`source-environment-${sourceEnvironment.slug}`}
|
||||||
|
>
|
||||||
|
{sourceEnvironment.name}
|
||||||
|
</SelectItem>
|
||||||
|
))}
|
||||||
|
</Select>
|
||||||
|
</FormControl>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div className="mb-2 flex w-full flex-col gap-1">
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
|
name="useLabels"
|
||||||
|
render={({ field: { onChange, value } }) => (
|
||||||
|
<Switch
|
||||||
|
id="use-environment-labels"
|
||||||
|
onCheckedChange={(isChecked) => onChange(isChecked)}
|
||||||
|
isChecked={value}
|
||||||
|
>
|
||||||
|
<FormLabel label="Use Labels" />
|
||||||
|
</Switch>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{shouldUseLabels && (
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
|
name="azureLabel"
|
||||||
|
render={({ field, fieldState: { error } }) => (
|
||||||
|
<FormControl
|
||||||
|
className=""
|
||||||
|
// label="Label"
|
||||||
|
errorText={error?.message}
|
||||||
|
isError={Boolean(error)}
|
||||||
|
>
|
||||||
|
<Input {...field} placeholder="pre-prod" />
|
||||||
|
</FormControl>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
|
name="secretPath"
|
||||||
|
render={({ field, fieldState: { error } }) => (
|
||||||
|
<FormControl label="Secrets Path" errorText={error?.message} isError={Boolean(error)}>
|
||||||
|
<SecretPathInput {...field} />
|
||||||
|
</FormControl>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
|
name="baseUrl"
|
||||||
|
render={({ field, fieldState: { error } }) => (
|
||||||
|
<FormControl
|
||||||
|
label="Azure App Configuration URL"
|
||||||
|
errorText={error?.message}
|
||||||
|
isError={Boolean(error)}
|
||||||
|
>
|
||||||
|
<Input
|
||||||
|
placeholder="https://infisical-configuration-integration-test.azconfig.io"
|
||||||
|
{...field}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
|
name="secretPrefix"
|
||||||
|
render={({ field, fieldState: { error } }) => (
|
||||||
|
<FormControl label="Key Prefix" errorText={error?.message} isError={Boolean(error)}>
|
||||||
|
<Input {...field} />
|
||||||
|
</FormControl>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
|
name="initialSyncBehavior"
|
||||||
|
render={({ field: { onChange, ...field }, fieldState: { error } }) => (
|
||||||
|
<FormControl
|
||||||
|
label="Initial Sync Behavior"
|
||||||
|
errorText={error?.message}
|
||||||
|
isError={Boolean(error)}
|
||||||
|
>
|
||||||
|
<Select
|
||||||
|
defaultValue={field.value}
|
||||||
|
onValueChange={(e) => onChange(e)}
|
||||||
|
className="w-full border border-mineshaft-500"
|
||||||
|
dropdownContainerClassName="max-w-full"
|
||||||
|
>
|
||||||
|
{initialSyncBehaviors.map((b) => {
|
||||||
|
return (
|
||||||
|
<SelectItem
|
||||||
|
value={b.value}
|
||||||
|
key={`sync-behavior-${b.value}`}
|
||||||
|
className="w-full"
|
||||||
|
>
|
||||||
|
{b.label}
|
||||||
|
</SelectItem>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</Select>
|
||||||
|
</FormControl>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<Button
|
||||||
|
type="submit"
|
||||||
|
color="mineshaft"
|
||||||
|
variant="outline_bg"
|
||||||
|
className="mb-6 ml-auto mt-4"
|
||||||
|
isLoading={isSubmitting}
|
||||||
|
>
|
||||||
|
Create Integration
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
</form>
|
||||||
|
) : (
|
||||||
|
<div />
|
||||||
|
);
|
||||||
|
};
|
||||||
+16
@@ -0,0 +1,16 @@
|
|||||||
|
import { createFileRoute } from "@tanstack/react-router";
|
||||||
|
import { zodValidator } from "@tanstack/zod-adapter";
|
||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
import { AzureAppConfigurationConfigurePage } from "./AzureAppConfigurationConfigurePage";
|
||||||
|
|
||||||
|
const AzureAppConfigurationPageQueryParamsSchema = z.object({
|
||||||
|
integrationAuthId: z.string()
|
||||||
|
});
|
||||||
|
|
||||||
|
export const Route = createFileRoute(
|
||||||
|
"/_authenticate/_inject-org-details/secret-manager/$projectId/_secret-manager-layout/integrations/azure-app-configuration/create"
|
||||||
|
)({
|
||||||
|
component: AzureAppConfigurationConfigurePage,
|
||||||
|
validateSearch: zodValidator(AzureAppConfigurationPageQueryParamsSchema)
|
||||||
|
});
|
||||||
+46
@@ -0,0 +1,46 @@
|
|||||||
|
import { useEffect } from "react";
|
||||||
|
import { useNavigate, useSearch } from "@tanstack/react-router";
|
||||||
|
|
||||||
|
import { ROUTE_PATHS } from "@app/const/routes";
|
||||||
|
import { useWorkspace } from "@app/context";
|
||||||
|
import { useAuthorizeIntegration } from "@app/hooks/api";
|
||||||
|
|
||||||
|
export const AzureAppConfigurationOauthCallbackPage = () => {
|
||||||
|
const navigate = useNavigate();
|
||||||
|
const { mutateAsync } = useAuthorizeIntegration();
|
||||||
|
|
||||||
|
const { code, state } = useSearch({
|
||||||
|
from: ROUTE_PATHS.SecretManager.Integratons.HerokuOauthCallbackPage.id
|
||||||
|
});
|
||||||
|
const { currentWorkspace } = useWorkspace();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
(async () => {
|
||||||
|
try {
|
||||||
|
// validate state
|
||||||
|
if (state !== localStorage.getItem("latestCSRFToken")) return;
|
||||||
|
localStorage.removeItem("latestCSRFToken");
|
||||||
|
|
||||||
|
const integrationAuth = await mutateAsync({
|
||||||
|
workspaceId: currentWorkspace.id,
|
||||||
|
code: code as string,
|
||||||
|
integration: "azure-app-configuration"
|
||||||
|
});
|
||||||
|
|
||||||
|
navigate({
|
||||||
|
to: "/secret-manager/$projectId/integrations/azure-app-configuration/create",
|
||||||
|
params: {
|
||||||
|
projectId: currentWorkspace.id
|
||||||
|
},
|
||||||
|
search: {
|
||||||
|
integrationAuthId: integrationAuth.id
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err);
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return <div />;
|
||||||
|
};
|
||||||
+17
@@ -0,0 +1,17 @@
|
|||||||
|
import { createFileRoute } from "@tanstack/react-router";
|
||||||
|
import { zodValidator } from "@tanstack/zod-adapter";
|
||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
import { AzureAppConfigurationOauthCallbackPage } from "./AzureAppConfigurationOauthCallbackPage";
|
||||||
|
|
||||||
|
export const AzureAppConfigurationOauthCallbackPageQueryParamsSchema = z.object({
|
||||||
|
state: z.string().catch(""),
|
||||||
|
code: z.string()
|
||||||
|
});
|
||||||
|
|
||||||
|
export const Route = createFileRoute(
|
||||||
|
"/_authenticate/_inject-org-details/secret-manager/$projectId/_secret-manager-layout/integrations/azure-app-configuration/oauth2/callback"
|
||||||
|
)({
|
||||||
|
component: AzureAppConfigurationOauthCallbackPage,
|
||||||
|
validateSearch: zodValidator(AzureAppConfigurationOauthCallbackPageQueryParamsSchema)
|
||||||
|
});
|
||||||
+124
@@ -0,0 +1,124 @@
|
|||||||
|
import { useState } from "react";
|
||||||
|
import { Helmet } from "react-helmet";
|
||||||
|
import { faArrowUpRightFromSquare, faBookOpen } from "@fortawesome/free-solid-svg-icons";
|
||||||
|
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||||
|
import { useNavigate } from "@tanstack/react-router";
|
||||||
|
|
||||||
|
import { Button, Card, CardTitle, FormControl, Input } from "@app/components/v2";
|
||||||
|
import { useWorkspace } from "@app/context";
|
||||||
|
import { useSaveIntegrationAccessToken } from "@app/hooks/api";
|
||||||
|
|
||||||
|
export const AzureDevopsAuthorizePage = () => {
|
||||||
|
const navigate = useNavigate();
|
||||||
|
const { mutateAsync } = useSaveIntegrationAccessToken();
|
||||||
|
|
||||||
|
const [apiKey, setApiKey] = useState("");
|
||||||
|
const [devopsOrgName, setDevopsOrgName] = useState("");
|
||||||
|
const [apiKeyErrorText, setApiKeyErrorText] = useState("");
|
||||||
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
|
const { currentWorkspace } = useWorkspace();
|
||||||
|
|
||||||
|
const handleButtonClick = async () => {
|
||||||
|
try {
|
||||||
|
setApiKeyErrorText("");
|
||||||
|
if (apiKey.length === 0) {
|
||||||
|
setApiKeyErrorText("API Key cannot be blank");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setIsLoading(true);
|
||||||
|
|
||||||
|
localStorage.setItem("azure-devops-org-name", devopsOrgName);
|
||||||
|
|
||||||
|
const integrationAuth = await mutateAsync({
|
||||||
|
workspaceId: currentWorkspace.id,
|
||||||
|
integration: "azure-devops",
|
||||||
|
accessToken: btoa(`:${apiKey}`) // This is a base64 encoding of the API key without any username
|
||||||
|
});
|
||||||
|
|
||||||
|
setIsLoading(false);
|
||||||
|
|
||||||
|
navigate({
|
||||||
|
to: "/secret-manager/$projectId/integrations/azure-devops/create",
|
||||||
|
params: {
|
||||||
|
projectId: currentWorkspace.id
|
||||||
|
},
|
||||||
|
search: {
|
||||||
|
integrationAuthId: integrationAuth.id
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex h-full w-full items-center justify-center">
|
||||||
|
<Helmet>
|
||||||
|
<title>Authorize Azure DevOps Integration</title>
|
||||||
|
</Helmet>
|
||||||
|
<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="flex items-center">
|
||||||
|
<img
|
||||||
|
src="/images/integrations/Amazon Web Services.png"
|
||||||
|
height={35}
|
||||||
|
width={35}
|
||||||
|
alt="Azure DevOps logo"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<span className="ml-1.5">Azure DevOps Integration </span>
|
||||||
|
<a
|
||||||
|
target="_blank"
|
||||||
|
href="https://infisical.com/docs/integrations/cloud/azure-devops"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
>
|
||||||
|
<div className="mb-1 ml-2 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="mb-[0.07rem] ml-1.5 text-xxs"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</CardTitle>{" "}
|
||||||
|
<FormControl
|
||||||
|
className="px-6"
|
||||||
|
label="Azure DevOps API Token"
|
||||||
|
errorText={apiKeyErrorText}
|
||||||
|
isError={apiKeyErrorText !== ""}
|
||||||
|
>
|
||||||
|
<Input placeholder="" value={apiKey} onChange={(e) => setApiKey(e.target.value)} />
|
||||||
|
</FormControl>
|
||||||
|
<FormControl
|
||||||
|
className="px-6"
|
||||||
|
label="Azure DevOps Organization Name"
|
||||||
|
tooltipText="This is the slug of the organization. An example would be 'my-acme-org'"
|
||||||
|
errorText={apiKeyErrorText}
|
||||||
|
isError={apiKeyErrorText !== ""}
|
||||||
|
>
|
||||||
|
<Input
|
||||||
|
placeholder=""
|
||||||
|
value={devopsOrgName}
|
||||||
|
onChange={(e) => setDevopsOrgName(e.target.value)}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
<Button
|
||||||
|
onClick={handleButtonClick}
|
||||||
|
colorSchema="primary"
|
||||||
|
variant="outline_bg"
|
||||||
|
className="mb-6 ml-auto mr-6 mt-2 w-min"
|
||||||
|
isLoading={isLoading}
|
||||||
|
>
|
||||||
|
Connect to Azure DevOps
|
||||||
|
</Button>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
import { createFileRoute } from "@tanstack/react-router";
|
||||||
|
|
||||||
|
import { AzureDevopsAuthorizePage } from "./AzureDevopsAuthorizePage";
|
||||||
|
|
||||||
|
export const Route = createFileRoute(
|
||||||
|
"/_authenticate/_inject-org-details/secret-manager/$projectId/_secret-manager-layout/integrations/azure-devops/authorize"
|
||||||
|
)({
|
||||||
|
component: AzureDevopsAuthorizePage
|
||||||
|
});
|
||||||
+192
@@ -0,0 +1,192 @@
|
|||||||
|
import { useEffect, useState } from "react";
|
||||||
|
import { Helmet } from "react-helmet";
|
||||||
|
import { faArrowUpRightFromSquare, faBookOpen } from "@fortawesome/free-solid-svg-icons";
|
||||||
|
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||||
|
import { useNavigate, useSearch } from "@tanstack/react-router";
|
||||||
|
|
||||||
|
import {
|
||||||
|
Button,
|
||||||
|
Card,
|
||||||
|
CardTitle,
|
||||||
|
FormControl,
|
||||||
|
Input,
|
||||||
|
Select,
|
||||||
|
SelectItem
|
||||||
|
} from "@app/components/v2";
|
||||||
|
import { ROUTE_PATHS } from "@app/const/routes";
|
||||||
|
import { useWorkspace } from "@app/context";
|
||||||
|
import { useCreateIntegration } from "@app/hooks/api";
|
||||||
|
import {
|
||||||
|
useGetIntegrationAuthApps,
|
||||||
|
useGetIntegrationAuthById
|
||||||
|
} from "@app/hooks/api/integrationAuth";
|
||||||
|
import { useGetWorkspaceById } from "@app/hooks/api/workspace";
|
||||||
|
|
||||||
|
export const AzureDevopsConfigurePage = () => {
|
||||||
|
const navigate = useNavigate();
|
||||||
|
const { mutateAsync } = useCreateIntegration();
|
||||||
|
|
||||||
|
const integrationAuthId = useSearch({
|
||||||
|
from: ROUTE_PATHS.SecretManager.Integratons.AzureDevopsConfigurePage.id,
|
||||||
|
select: (el) => el.integrationAuthId
|
||||||
|
});
|
||||||
|
const { currentWorkspace } = useWorkspace();
|
||||||
|
|
||||||
|
const { data: workspace } = useGetWorkspaceById(currentWorkspace.id);
|
||||||
|
const { data: integrationAuth } = useGetIntegrationAuthById((integrationAuthId as string) ?? "");
|
||||||
|
const { data: integrationAuthApps } = useGetIntegrationAuthApps({
|
||||||
|
integrationAuthId: (integrationAuthId as string) ?? "",
|
||||||
|
azureDevOpsOrgName: localStorage.getItem("azure-devops-org-name") ?? ""
|
||||||
|
});
|
||||||
|
|
||||||
|
const [selectedSourceEnvironment, setSelectedSourceEnvironment] = useState("");
|
||||||
|
const [secretPath, setSecretPath] = useState("/");
|
||||||
|
const [targetApp, setTargetApp] = useState("");
|
||||||
|
|
||||||
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (workspace) {
|
||||||
|
setSelectedSourceEnvironment(workspace.environments[0].slug);
|
||||||
|
}
|
||||||
|
}, [workspace]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (integrationAuthApps) {
|
||||||
|
if (integrationAuthApps.length > 0) {
|
||||||
|
setTargetApp(integrationAuthApps[0].name);
|
||||||
|
} else {
|
||||||
|
setTargetApp("none");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, [integrationAuthApps]);
|
||||||
|
|
||||||
|
const handleButtonClick = async () => {
|
||||||
|
try {
|
||||||
|
if (!integrationAuth?.id) return;
|
||||||
|
|
||||||
|
setIsLoading(true);
|
||||||
|
|
||||||
|
await mutateAsync({
|
||||||
|
integrationAuthId: integrationAuth?.id,
|
||||||
|
isActive: true,
|
||||||
|
app: localStorage.getItem("azure-devops-org-name") || "",
|
||||||
|
appId: targetApp,
|
||||||
|
sourceEnvironment: selectedSourceEnvironment,
|
||||||
|
secretPath
|
||||||
|
});
|
||||||
|
|
||||||
|
setIsLoading(false);
|
||||||
|
|
||||||
|
navigate({
|
||||||
|
to: "/secret-manager/$projectId/integrations",
|
||||||
|
params: {
|
||||||
|
projectId: currentWorkspace.id
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return integrationAuth &&
|
||||||
|
workspace &&
|
||||||
|
selectedSourceEnvironment &&
|
||||||
|
integrationAuthApps &&
|
||||||
|
targetApp ? (
|
||||||
|
<div className="flex h-full w-full items-center justify-center">
|
||||||
|
<Helmet>
|
||||||
|
<title>Set Up Azure DevOps Integration</title>
|
||||||
|
</Helmet>
|
||||||
|
<Card className="max-w-lg rounded-md border border-mineshaft-600">
|
||||||
|
<CardTitle
|
||||||
|
className="px-6 text-left text-xl"
|
||||||
|
subTitle="Choose which environment in Infisical you want to sync to secrets in Azure DevOps."
|
||||||
|
>
|
||||||
|
<div className="flex flex-row items-center">
|
||||||
|
<div className="flex items-center">
|
||||||
|
<img
|
||||||
|
src="/images/integrations/Amazon Web Services.png"
|
||||||
|
height={35}
|
||||||
|
width={35}
|
||||||
|
alt="Azure DevOps logo"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<span className="ml-1.5">Azure DevOps Integration </span>
|
||||||
|
<a
|
||||||
|
href="https://infisical.com/docs/integrations/cloud/azure-devops"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
>
|
||||||
|
<div className="mb-1 ml-2 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="mb-[0.07rem] ml-1.5 text-xxs"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</CardTitle>
|
||||||
|
<FormControl label="Project Environment" className="mt-4 px-6">
|
||||||
|
<Select
|
||||||
|
value={selectedSourceEnvironment}
|
||||||
|
onValueChange={(val) => setSelectedSourceEnvironment(val)}
|
||||||
|
className="w-full border border-mineshaft-500"
|
||||||
|
>
|
||||||
|
{workspace?.environments.map((sourceEnvironment) => (
|
||||||
|
<SelectItem
|
||||||
|
value={sourceEnvironment.slug}
|
||||||
|
key={`source-environment-${sourceEnvironment.slug}`}
|
||||||
|
>
|
||||||
|
{sourceEnvironment.name}
|
||||||
|
</SelectItem>
|
||||||
|
))}
|
||||||
|
</Select>
|
||||||
|
</FormControl>
|
||||||
|
<FormControl className="px-6" label="Secrets Path">
|
||||||
|
<Input
|
||||||
|
value={secretPath}
|
||||||
|
onChange={(evt) => setSecretPath(evt.target.value)}
|
||||||
|
placeholder="Provide a path, default is /"
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
<FormControl label="Azure DevOps Project" className="px-6">
|
||||||
|
<Select
|
||||||
|
value={targetApp}
|
||||||
|
onValueChange={(val) => setTargetApp(val)}
|
||||||
|
className="w-full border border-mineshaft-500"
|
||||||
|
isDisabled={integrationAuthApps.length === 0}
|
||||||
|
>
|
||||||
|
{integrationAuthApps.length > 0 ? (
|
||||||
|
integrationAuthApps.map((integrationAuthApp) => (
|
||||||
|
<SelectItem
|
||||||
|
value={integrationAuthApp.name}
|
||||||
|
key={`target-environment-${integrationAuthApp.name}`}
|
||||||
|
>
|
||||||
|
{integrationAuthApp.name}
|
||||||
|
</SelectItem>
|
||||||
|
))
|
||||||
|
) : (
|
||||||
|
<SelectItem value="none" key="target-app-none">
|
||||||
|
No projects found
|
||||||
|
</SelectItem>
|
||||||
|
)}
|
||||||
|
</Select>
|
||||||
|
</FormControl>
|
||||||
|
<Button
|
||||||
|
onClick={handleButtonClick}
|
||||||
|
colorSchema="primary"
|
||||||
|
variant="outline_bg"
|
||||||
|
className="mb-6 ml-auto mr-6 mt-2 w-min"
|
||||||
|
isLoading={isLoading}
|
||||||
|
>
|
||||||
|
Create Integration
|
||||||
|
</Button>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div />
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
import { createFileRoute } from "@tanstack/react-router";
|
||||||
|
import { zodValidator } from "@tanstack/zod-adapter";
|
||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
import { AzureDevopsConfigurePage } from "./AzureDevopsConfigurePage";
|
||||||
|
|
||||||
|
const AzureDevopsConfigurePageQueryParamsSchema = z.object({
|
||||||
|
integrationAuthId: z.string()
|
||||||
|
});
|
||||||
|
|
||||||
|
export const Route = createFileRoute(
|
||||||
|
"/_authenticate/_inject-org-details/secret-manager/$projectId/_secret-manager-layout/integrations/azure-devops/create"
|
||||||
|
)({
|
||||||
|
component: AzureDevopsConfigurePage,
|
||||||
|
validateSearch: zodValidator(AzureDevopsConfigurePageQueryParamsSchema)
|
||||||
|
});
|
||||||
+166
@@ -0,0 +1,166 @@
|
|||||||
|
import { useEffect, useState } from "react";
|
||||||
|
import { useNavigate, useSearch } from "@tanstack/react-router";
|
||||||
|
|
||||||
|
import {
|
||||||
|
Button,
|
||||||
|
Card,
|
||||||
|
CardTitle,
|
||||||
|
FormControl,
|
||||||
|
Input,
|
||||||
|
Select,
|
||||||
|
SelectItem
|
||||||
|
} from "@app/components/v2";
|
||||||
|
import { ROUTE_PATHS } from "@app/const/routes";
|
||||||
|
import { useWorkspace } from "@app/context";
|
||||||
|
import { useCreateIntegration } from "@app/hooks/api";
|
||||||
|
import { useGetIntegrationAuthById } from "@app/hooks/api/integrationAuth";
|
||||||
|
import { IntegrationSyncBehavior } from "@app/hooks/api/integrations/types";
|
||||||
|
import { useGetWorkspaceById } from "@app/hooks/api/workspace";
|
||||||
|
|
||||||
|
const initialSyncBehaviors = [
|
||||||
|
{
|
||||||
|
label: "No Import - Overwrite all values in Azure Vault",
|
||||||
|
value: IntegrationSyncBehavior.OVERWRITE_TARGET
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Import - Prefer values from Azure Vault",
|
||||||
|
value: IntegrationSyncBehavior.PREFER_TARGET
|
||||||
|
},
|
||||||
|
{ label: "Import - Prefer values from Infisical", value: IntegrationSyncBehavior.PREFER_SOURCE }
|
||||||
|
];
|
||||||
|
|
||||||
|
export const AzureKeyVaultConfigurePage = () => {
|
||||||
|
const navigate = useNavigate();
|
||||||
|
const { mutateAsync } = useCreateIntegration();
|
||||||
|
|
||||||
|
const integrationAuthId = useSearch({
|
||||||
|
from: ROUTE_PATHS.SecretManager.Integratons.AzureKeyVaultConfigurePage.id,
|
||||||
|
select: (el) => el.integrationAuthId
|
||||||
|
});
|
||||||
|
const { currentWorkspace } = useWorkspace();
|
||||||
|
|
||||||
|
const { data: workspace } = useGetWorkspaceById(currentWorkspace.id);
|
||||||
|
const { data: integrationAuth } = useGetIntegrationAuthById((integrationAuthId as string) ?? "");
|
||||||
|
|
||||||
|
const [selectedSourceEnvironment, setSelectedSourceEnvironment] = useState("");
|
||||||
|
const [secretPath, setSecretPath] = useState("/");
|
||||||
|
|
||||||
|
const [vaultBaseUrl, setVaultBaseUrl] = useState("");
|
||||||
|
const [vaultBaseUrlErrorText, setVaultBaseUrlErrorText] = useState("");
|
||||||
|
const [initialSyncBehavior, setInitialSyncBehavior] = useState(
|
||||||
|
IntegrationSyncBehavior.PREFER_SOURCE
|
||||||
|
);
|
||||||
|
|
||||||
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (workspace) {
|
||||||
|
setSelectedSourceEnvironment(workspace.environments[0].slug);
|
||||||
|
}
|
||||||
|
}, [workspace]);
|
||||||
|
|
||||||
|
const handleButtonClick = async () => {
|
||||||
|
try {
|
||||||
|
if (vaultBaseUrl.length === 0) {
|
||||||
|
setVaultBaseUrlErrorText("Vault URI cannot be blank");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!vaultBaseUrl.startsWith("https://") || !vaultBaseUrl.endsWith("vault.azure.net")) {
|
||||||
|
setVaultBaseUrlErrorText("Vault URI must be like https://<vault_name>.vault.azure.net");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!integrationAuth?.id) return;
|
||||||
|
|
||||||
|
setIsLoading(true);
|
||||||
|
await mutateAsync({
|
||||||
|
integrationAuthId: integrationAuth?.id,
|
||||||
|
isActive: true,
|
||||||
|
app: vaultBaseUrl,
|
||||||
|
sourceEnvironment: selectedSourceEnvironment,
|
||||||
|
secretPath,
|
||||||
|
metadata: {
|
||||||
|
initialSyncBehavior
|
||||||
|
}
|
||||||
|
});
|
||||||
|
setIsLoading(false);
|
||||||
|
|
||||||
|
navigate({
|
||||||
|
to: "/secret-manager/$projectId/integrations",
|
||||||
|
params: {
|
||||||
|
projectId: currentWorkspace.id
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return integrationAuth && workspace && selectedSourceEnvironment ? (
|
||||||
|
<div className="flex h-full w-full items-center justify-center">
|
||||||
|
<Card className="max-w-md rounded-md p-8">
|
||||||
|
<CardTitle className="text-center">Azure Key Vault Integration</CardTitle>
|
||||||
|
<FormControl label="Project Environment" className="mt-4">
|
||||||
|
<Select
|
||||||
|
value={selectedSourceEnvironment}
|
||||||
|
onValueChange={(val) => setSelectedSourceEnvironment(val)}
|
||||||
|
className="w-full border border-mineshaft-500"
|
||||||
|
>
|
||||||
|
{workspace?.environments.map((sourceEnvironment) => (
|
||||||
|
<SelectItem
|
||||||
|
value={sourceEnvironment.slug}
|
||||||
|
key={`azure-key-vault-environment-${sourceEnvironment.slug}`}
|
||||||
|
>
|
||||||
|
{sourceEnvironment.name}
|
||||||
|
</SelectItem>
|
||||||
|
))}
|
||||||
|
</Select>
|
||||||
|
</FormControl>
|
||||||
|
<FormControl label="Secrets Path">
|
||||||
|
<Input
|
||||||
|
value={secretPath}
|
||||||
|
onChange={(evt) => setSecretPath(evt.target.value)}
|
||||||
|
placeholder="Provide a path, default is /"
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
<FormControl
|
||||||
|
label="Vault URI"
|
||||||
|
errorText={vaultBaseUrlErrorText}
|
||||||
|
isError={vaultBaseUrlErrorText !== ""}
|
||||||
|
>
|
||||||
|
<Input
|
||||||
|
placeholder="https://example.vault.azure.net"
|
||||||
|
value={vaultBaseUrl}
|
||||||
|
onChange={(e) => setVaultBaseUrl(e.target.value)}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
<FormControl label="Initial Sync Behavior">
|
||||||
|
<Select
|
||||||
|
value={initialSyncBehavior}
|
||||||
|
onValueChange={(e) => setInitialSyncBehavior(e as IntegrationSyncBehavior)}
|
||||||
|
className="w-full"
|
||||||
|
>
|
||||||
|
{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"
|
||||||
|
isLoading={isLoading}
|
||||||
|
>
|
||||||
|
Create Integration
|
||||||
|
</Button>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div />
|
||||||
|
);
|
||||||
|
};
|
||||||
+16
@@ -0,0 +1,16 @@
|
|||||||
|
import { createFileRoute } from "@tanstack/react-router";
|
||||||
|
import { zodValidator } from "@tanstack/zod-adapter";
|
||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
import { AzureKeyVaultConfigurePage } from "./AzureKeyVaultConfigurePage";
|
||||||
|
|
||||||
|
const AzureKeyVaultConfigurePageQueryParamsSchema = z.object({
|
||||||
|
integrationAuthId: z.string()
|
||||||
|
});
|
||||||
|
|
||||||
|
export const Route = createFileRoute(
|
||||||
|
"/_authenticate/_inject-org-details/secret-manager/$projectId/_secret-manager-layout/integrations/azure-key-vault/create"
|
||||||
|
)({
|
||||||
|
component: AzureKeyVaultConfigurePage,
|
||||||
|
validateSearch: zodValidator(AzureKeyVaultConfigurePageQueryParamsSchema)
|
||||||
|
});
|
||||||
+46
@@ -0,0 +1,46 @@
|
|||||||
|
import { useEffect } from "react";
|
||||||
|
import { useNavigate, useSearch } from "@tanstack/react-router";
|
||||||
|
|
||||||
|
import { ROUTE_PATHS } from "@app/const/routes";
|
||||||
|
import { useWorkspace } from "@app/context";
|
||||||
|
import { useAuthorizeIntegration } from "@app/hooks/api";
|
||||||
|
|
||||||
|
export const AzureKeyVaultOauthCallbackPage = () => {
|
||||||
|
const navigate = useNavigate();
|
||||||
|
const { mutateAsync } = useAuthorizeIntegration();
|
||||||
|
|
||||||
|
const { code, state } = useSearch({
|
||||||
|
from: ROUTE_PATHS.SecretManager.Integratons.AzureKeyVaultOauthCallbackPage.id
|
||||||
|
});
|
||||||
|
const { currentWorkspace } = useWorkspace();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
(async () => {
|
||||||
|
try {
|
||||||
|
// validate state
|
||||||
|
if (state !== localStorage.getItem("latestCSRFToken")) return;
|
||||||
|
localStorage.removeItem("latestCSRFToken");
|
||||||
|
|
||||||
|
const integrationAuth = await mutateAsync({
|
||||||
|
workspaceId: currentWorkspace.id,
|
||||||
|
code: code as string,
|
||||||
|
integration: "azure-key-vault"
|
||||||
|
});
|
||||||
|
|
||||||
|
navigate({
|
||||||
|
to: "/secret-manager/$projectId/integrations/heroku/create",
|
||||||
|
params: {
|
||||||
|
projectId: currentWorkspace.id
|
||||||
|
},
|
||||||
|
search: {
|
||||||
|
integrationAuthId: integrationAuth.id
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err);
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return <div />;
|
||||||
|
};
|
||||||
+17
@@ -0,0 +1,17 @@
|
|||||||
|
import { createFileRoute } from "@tanstack/react-router";
|
||||||
|
import { zodValidator } from "@tanstack/zod-adapter";
|
||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
import { AzureKeyVaultOauthCallbackPage } from "./AzureKeyVaultOauthCallback";
|
||||||
|
|
||||||
|
export const AzureKeyVaultOauthCallbackQueryParamsSchema = z.object({
|
||||||
|
state: z.string().catch(""),
|
||||||
|
code: z.string()
|
||||||
|
});
|
||||||
|
|
||||||
|
export const Route = createFileRoute(
|
||||||
|
"/_authenticate/_inject-org-details/secret-manager/$projectId/_secret-manager-layout/integrations/azure-key-vault/oauth2/callback"
|
||||||
|
)({
|
||||||
|
component: AzureKeyVaultOauthCallbackPage,
|
||||||
|
validateSearch: zodValidator(AzureKeyVaultOauthCallbackQueryParamsSchema)
|
||||||
|
});
|
||||||
+307
@@ -0,0 +1,307 @@
|
|||||||
|
import { useEffect } from "react";
|
||||||
|
import { Controller, useForm } from "react-hook-form";
|
||||||
|
import { SiBitbucket } from "react-icons/si";
|
||||||
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
|
import { useNavigate, useSearch } from "@tanstack/react-router";
|
||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
import { createNotification } from "@app/components/notifications";
|
||||||
|
import {
|
||||||
|
Button,
|
||||||
|
Card,
|
||||||
|
CardTitle,
|
||||||
|
FilterableSelect,
|
||||||
|
FormControl,
|
||||||
|
Input,
|
||||||
|
Spinner
|
||||||
|
} from "@app/components/v2";
|
||||||
|
import { ROUTE_PATHS } from "@app/const/routes";
|
||||||
|
import { useWorkspace } from "@app/context";
|
||||||
|
import {
|
||||||
|
useCreateIntegration,
|
||||||
|
useGetIntegrationAuthApps,
|
||||||
|
useGetIntegrationAuthBitBucketWorkspaces
|
||||||
|
} from "@app/hooks/api";
|
||||||
|
import { useGetIntegrationAuthBitBucketEnvironments } from "@app/hooks/api/integrationAuth/queries";
|
||||||
|
|
||||||
|
enum BitbucketScope {
|
||||||
|
Repo = "repo",
|
||||||
|
Env = "environment"
|
||||||
|
}
|
||||||
|
|
||||||
|
const ScopeOptions = [
|
||||||
|
{
|
||||||
|
label: "Repository",
|
||||||
|
value: BitbucketScope.Repo
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Deployment Environment",
|
||||||
|
value: BitbucketScope.Env
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
const formSchema = z
|
||||||
|
.object({
|
||||||
|
secretPath: z.string().default("/"),
|
||||||
|
sourceEnvironment: z.object({ name: z.string(), slug: z.string() }),
|
||||||
|
targetRepo: z.object({ name: z.string(), appId: z.string() }),
|
||||||
|
targetWorkspace: z.object({ name: z.string(), slug: z.string() }),
|
||||||
|
targetEnvironment: z.object({ name: z.string(), uuid: z.string() }).optional(),
|
||||||
|
scope: z.object({ label: z.string(), value: z.nativeEnum(BitbucketScope) })
|
||||||
|
})
|
||||||
|
.superRefine((val, ctx) => {
|
||||||
|
if (val.scope.value === BitbucketScope.Env && !val.targetEnvironment) {
|
||||||
|
ctx.addIssue({
|
||||||
|
code: z.ZodIssueCode.custom,
|
||||||
|
path: ["targetEnvironment"],
|
||||||
|
message: "Required"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
type TFormData = z.infer<typeof formSchema>;
|
||||||
|
|
||||||
|
export const BitbucketConfigurePage = () => {
|
||||||
|
const navigate = useNavigate();
|
||||||
|
const createIntegration = useCreateIntegration();
|
||||||
|
|
||||||
|
const { watch, control, reset, handleSubmit } = useForm<TFormData>({
|
||||||
|
resolver: zodResolver(formSchema),
|
||||||
|
defaultValues: {
|
||||||
|
secretPath: "/"
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const bitBucketWorkspace = watch("targetWorkspace");
|
||||||
|
const bitBucketRepo = watch("targetRepo");
|
||||||
|
|
||||||
|
const integrationAuthId = useSearch({
|
||||||
|
from: ROUTE_PATHS.SecretManager.Integratons.BitbucketConfigurePage.id,
|
||||||
|
select: (el) => el.integrationAuthId
|
||||||
|
});
|
||||||
|
const { currentWorkspace } = useWorkspace();
|
||||||
|
|
||||||
|
const { data: bitbucketWorkspaces, isPending: isBitbucketWorkspacesLoading } =
|
||||||
|
useGetIntegrationAuthBitBucketWorkspaces((integrationAuthId as string) ?? "");
|
||||||
|
|
||||||
|
const { data: bitbucketRepos, isPending: isBitbucketReposLoading } = useGetIntegrationAuthApps({
|
||||||
|
integrationAuthId: (integrationAuthId as string) ?? "",
|
||||||
|
workspaceSlug: bitBucketWorkspace?.slug
|
||||||
|
});
|
||||||
|
|
||||||
|
const { data: bitbucketEnvironments } = useGetIntegrationAuthBitBucketEnvironments(
|
||||||
|
{
|
||||||
|
integrationAuthId: (integrationAuthId as string) ?? "",
|
||||||
|
workspaceSlug: bitBucketWorkspace?.slug,
|
||||||
|
repoSlug: bitBucketRepo?.appId
|
||||||
|
},
|
||||||
|
{ enabled: Boolean(bitBucketWorkspace?.slug && bitBucketRepo?.appId) }
|
||||||
|
);
|
||||||
|
|
||||||
|
const onSubmit = async ({
|
||||||
|
targetRepo,
|
||||||
|
sourceEnvironment,
|
||||||
|
targetWorkspace,
|
||||||
|
secretPath,
|
||||||
|
targetEnvironment,
|
||||||
|
scope
|
||||||
|
}: TFormData) => {
|
||||||
|
try {
|
||||||
|
await createIntegration.mutateAsync({
|
||||||
|
integrationAuthId,
|
||||||
|
isActive: true,
|
||||||
|
app: targetRepo.name,
|
||||||
|
appId: targetRepo.appId,
|
||||||
|
sourceEnvironment: sourceEnvironment.slug,
|
||||||
|
targetEnvironment: targetWorkspace.name,
|
||||||
|
targetEnvironmentId: targetWorkspace.slug,
|
||||||
|
...(scope.value === BitbucketScope.Env &&
|
||||||
|
targetEnvironment && {
|
||||||
|
targetService: targetEnvironment.name,
|
||||||
|
targetServiceId: targetEnvironment.uuid
|
||||||
|
}),
|
||||||
|
secretPath
|
||||||
|
});
|
||||||
|
|
||||||
|
createNotification({
|
||||||
|
type: "success",
|
||||||
|
text: "Successfully created integration"
|
||||||
|
});
|
||||||
|
navigate({
|
||||||
|
to: "/secret-manager/$projectId/integrations",
|
||||||
|
params: {
|
||||||
|
projectId: currentWorkspace.id
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
createNotification({
|
||||||
|
type: "error",
|
||||||
|
text: "Failed to create integration"
|
||||||
|
});
|
||||||
|
console.error(err);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!bitbucketRepos || !bitbucketWorkspaces || !currentWorkspace) return;
|
||||||
|
|
||||||
|
reset({
|
||||||
|
targetRepo: bitbucketRepos[0],
|
||||||
|
targetWorkspace: bitbucketWorkspaces[0],
|
||||||
|
sourceEnvironment: currentWorkspace.environments[0],
|
||||||
|
secretPath: "/",
|
||||||
|
scope: ScopeOptions[0]
|
||||||
|
});
|
||||||
|
}, [bitbucketWorkspaces, bitbucketRepos, currentWorkspace]);
|
||||||
|
|
||||||
|
if (isBitbucketWorkspacesLoading || isBitbucketReposLoading)
|
||||||
|
return (
|
||||||
|
<div className="flex h-full w-full items-center justify-center p-24">
|
||||||
|
<Spinner />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
const scope = watch("scope");
|
||||||
|
|
||||||
|
return (
|
||||||
|
<form
|
||||||
|
onSubmit={handleSubmit(onSubmit)}
|
||||||
|
className="flex h-full w-full items-center justify-center"
|
||||||
|
>
|
||||||
|
<Card className="max-w-md rounded-md p-8 pt-4">
|
||||||
|
<CardTitle className="text-center">
|
||||||
|
<SiBitbucket size="1.2rem" className="mb-1 mr-2 inline-block" />
|
||||||
|
Bitbucket Integration
|
||||||
|
</CardTitle>
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
|
name="sourceEnvironment"
|
||||||
|
render={({ field: { value, onChange }, fieldState: { error } }) => (
|
||||||
|
<FormControl
|
||||||
|
errorText={error?.message}
|
||||||
|
isError={Boolean(error)}
|
||||||
|
label="Project Environment"
|
||||||
|
>
|
||||||
|
<FilterableSelect
|
||||||
|
getOptionValue={(option) => option.slug}
|
||||||
|
value={value}
|
||||||
|
getOptionLabel={(option) => option.name}
|
||||||
|
onChange={onChange}
|
||||||
|
options={currentWorkspace?.environments}
|
||||||
|
placeholder="Select a project environment"
|
||||||
|
isDisabled={!currentWorkspace?.environments.length}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
|
name="secretPath"
|
||||||
|
render={({ field: { value, onChange }, fieldState: { error } }) => (
|
||||||
|
<FormControl isError={Boolean(error)} label="Secrets Path">
|
||||||
|
<Input
|
||||||
|
value={value}
|
||||||
|
onChange={onChange}
|
||||||
|
placeholder='Provide a path (defaults to "/")'
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
|
name="targetWorkspace"
|
||||||
|
render={({ field: { value, onChange }, fieldState: { error } }) => (
|
||||||
|
<FormControl
|
||||||
|
errorText={error?.message}
|
||||||
|
isError={Boolean(error)}
|
||||||
|
label="Bitbucket Workspace"
|
||||||
|
>
|
||||||
|
<FilterableSelect
|
||||||
|
getOptionValue={(option) => option.slug}
|
||||||
|
value={value}
|
||||||
|
getOptionLabel={(option) => option.name}
|
||||||
|
onChange={onChange}
|
||||||
|
options={bitbucketWorkspaces}
|
||||||
|
placeholder={
|
||||||
|
bitbucketWorkspaces?.length ? "Select a workspace..." : "No workspaces found..."
|
||||||
|
}
|
||||||
|
isDisabled={!bitbucketWorkspaces?.length}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
|
name="targetRepo"
|
||||||
|
render={({ field: { value, onChange }, fieldState: { error } }) => (
|
||||||
|
<FormControl errorText={error?.message} isError={Boolean(error)} label="Bitbucket Repo">
|
||||||
|
<FilterableSelect
|
||||||
|
getOptionValue={(option) => option.appId!}
|
||||||
|
value={value}
|
||||||
|
getOptionLabel={(option) => option.name}
|
||||||
|
onChange={onChange}
|
||||||
|
options={bitbucketRepos}
|
||||||
|
placeholder={
|
||||||
|
bitbucketRepos?.length ? "Select a repository..." : "No repositories found..."
|
||||||
|
}
|
||||||
|
isDisabled={!bitbucketRepos?.length}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
|
name="scope"
|
||||||
|
render={({ field: { value, onChange }, fieldState: { error } }) => (
|
||||||
|
<FormControl errorText={error?.message} isError={Boolean(error)} label="Scope">
|
||||||
|
<FilterableSelect
|
||||||
|
value={value}
|
||||||
|
getOptionValue={(option) => option.value}
|
||||||
|
getOptionLabel={(option) => option.label}
|
||||||
|
onChange={onChange}
|
||||||
|
options={ScopeOptions}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{scope?.value === BitbucketScope.Env && (
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
|
name="targetEnvironment"
|
||||||
|
render={({ field: { value, onChange }, fieldState: { error } }) => (
|
||||||
|
<FormControl
|
||||||
|
errorText={error?.message}
|
||||||
|
isError={Boolean(error)}
|
||||||
|
label="Bitbucket Deployment Environment"
|
||||||
|
>
|
||||||
|
<FilterableSelect
|
||||||
|
getOptionValue={(option) => option.uuid}
|
||||||
|
value={value}
|
||||||
|
getOptionLabel={(option) => option.name}
|
||||||
|
onChange={onChange}
|
||||||
|
options={bitbucketEnvironments}
|
||||||
|
placeholder={
|
||||||
|
bitbucketEnvironments?.length
|
||||||
|
? "Select an environment..."
|
||||||
|
: "No environments found..."
|
||||||
|
}
|
||||||
|
isDisabled={!bitbucketEnvironments?.length}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
<Button
|
||||||
|
type="submit"
|
||||||
|
colorSchema="primary"
|
||||||
|
className="mt-4"
|
||||||
|
isLoading={createIntegration.isPending}
|
||||||
|
isDisabled={createIntegration.isPending || !bitbucketRepos?.length}
|
||||||
|
>
|
||||||
|
Create Integration
|
||||||
|
</Button>
|
||||||
|
</Card>
|
||||||
|
</form>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
import { createFileRoute } from "@tanstack/react-router";
|
||||||
|
import { zodValidator } from "@tanstack/zod-adapter";
|
||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
import { BitbucketConfigurePage } from "./BitbucketConfigurePage";
|
||||||
|
|
||||||
|
const BitbucketConfigurePageQueryParamsSchema = z.object({
|
||||||
|
integrationAuthId: z.string()
|
||||||
|
});
|
||||||
|
|
||||||
|
export const Route = createFileRoute(
|
||||||
|
"/_authenticate/_inject-org-details/secret-manager/$projectId/_secret-manager-layout/integrations/bitbucket/create"
|
||||||
|
)({
|
||||||
|
component: BitbucketConfigurePage,
|
||||||
|
validateSearch: zodValidator(BitbucketConfigurePageQueryParamsSchema)
|
||||||
|
});
|
||||||
+45
@@ -0,0 +1,45 @@
|
|||||||
|
import { useEffect } from "react";
|
||||||
|
import { useNavigate, useSearch } from "@tanstack/react-router";
|
||||||
|
|
||||||
|
import { ROUTE_PATHS } from "@app/const/routes";
|
||||||
|
import { useWorkspace } from "@app/context";
|
||||||
|
import { useAuthorizeIntegration } from "@app/hooks/api";
|
||||||
|
|
||||||
|
export const BitbucketOauthCallbackPage = () => {
|
||||||
|
const navigate = useNavigate();
|
||||||
|
const { mutateAsync } = useAuthorizeIntegration();
|
||||||
|
const { code, state } = useSearch({
|
||||||
|
from: ROUTE_PATHS.SecretManager.Integratons.BitbucketOauthCallbackPage.id
|
||||||
|
});
|
||||||
|
const { currentWorkspace } = useWorkspace();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
(async () => {
|
||||||
|
try {
|
||||||
|
// validate state
|
||||||
|
if (state !== localStorage.getItem("latestCSRFToken")) return;
|
||||||
|
localStorage.removeItem("latestCSRFToken");
|
||||||
|
|
||||||
|
const integrationAuth = await mutateAsync({
|
||||||
|
workspaceId: currentWorkspace.id,
|
||||||
|
code: code as string,
|
||||||
|
integration: "bitbucket"
|
||||||
|
});
|
||||||
|
|
||||||
|
navigate({
|
||||||
|
to: "/secret-manager/$projectId/integrations/heroku/create",
|
||||||
|
params: {
|
||||||
|
projectId: currentWorkspace.id
|
||||||
|
},
|
||||||
|
search: {
|
||||||
|
integrationAuthId: integrationAuth.id
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err);
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return <div />;
|
||||||
|
};
|
||||||
+17
@@ -0,0 +1,17 @@
|
|||||||
|
import { createFileRoute } from "@tanstack/react-router";
|
||||||
|
import { zodValidator } from "@tanstack/zod-adapter";
|
||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
import { BitbucketOauthCallbackPage } from "./BitbucketOauthCallbackPage";
|
||||||
|
|
||||||
|
export const BitbucketOauthCallbackQueryParamsSchema = z.object({
|
||||||
|
state: z.string().catch(""),
|
||||||
|
code: z.string()
|
||||||
|
});
|
||||||
|
|
||||||
|
export const Route = createFileRoute(
|
||||||
|
"/_authenticate/_inject-org-details/secret-manager/$projectId/_secret-manager-layout/integrations/bitbucket/oauth2/callback"
|
||||||
|
)({
|
||||||
|
component: BitbucketOauthCallbackPage,
|
||||||
|
validateSearch: zodValidator(BitbucketOauthCallbackQueryParamsSchema)
|
||||||
|
});
|
||||||
+115
@@ -0,0 +1,115 @@
|
|||||||
|
import { useState } from "react";
|
||||||
|
import { Helmet } from "react-helmet";
|
||||||
|
import { faArrowUpRightFromSquare, faBookOpen } from "@fortawesome/free-solid-svg-icons";
|
||||||
|
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||||
|
import { useNavigate } from "@tanstack/react-router";
|
||||||
|
|
||||||
|
import { Button, Card, CardTitle, FormControl, Input } from "@app/components/v2";
|
||||||
|
import { useWorkspace } from "@app/context";
|
||||||
|
import { useSaveIntegrationAccessToken } from "@app/hooks/api";
|
||||||
|
|
||||||
|
export const ChecklyAuthorizePage = () => {
|
||||||
|
const navigate = useNavigate();
|
||||||
|
const { mutateAsync } = useSaveIntegrationAccessToken();
|
||||||
|
const { currentWorkspace } = useWorkspace();
|
||||||
|
|
||||||
|
const [accessToken, setAccessToken] = useState("");
|
||||||
|
const [accessTokenErrorText, setAccessTokenErrorText] = useState("");
|
||||||
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
|
|
||||||
|
const handleButtonClick = async () => {
|
||||||
|
try {
|
||||||
|
setAccessTokenErrorText("");
|
||||||
|
if (accessToken.length === 0) {
|
||||||
|
setAccessTokenErrorText("Access token cannot be blank");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setIsLoading(true);
|
||||||
|
|
||||||
|
const integrationAuth = await mutateAsync({
|
||||||
|
workspaceId: currentWorkspace.id,
|
||||||
|
integration: "checkly",
|
||||||
|
accessToken
|
||||||
|
});
|
||||||
|
|
||||||
|
setIsLoading(false);
|
||||||
|
|
||||||
|
navigate({
|
||||||
|
to: "/secret-manager/$projectId/integrations/checkly/create",
|
||||||
|
params: {
|
||||||
|
projectId: currentWorkspace.id
|
||||||
|
},
|
||||||
|
search: {
|
||||||
|
integrationAuthId: integrationAuth.id
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex h-full w-full items-center justify-center">
|
||||||
|
<Helmet>
|
||||||
|
<title>Authorize Checkly Integration</title>
|
||||||
|
</Helmet>
|
||||||
|
<Card className="mb-12 max-w-lg rounded-md border border-mineshaft-600">
|
||||||
|
<CardTitle
|
||||||
|
className="px-6 text-left text-xl"
|
||||||
|
subTitle="After adding your API key, 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="flex items-center pb-0.5">
|
||||||
|
<img
|
||||||
|
src="/images/integrations/Checkly.png"
|
||||||
|
height={30}
|
||||||
|
width={30}
|
||||||
|
alt="Checkly logo"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<span className="ml-2.5">Checkly Integration </span>
|
||||||
|
<a
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
href="https://infisical.com/docs/integrations/cloud/checkly"
|
||||||
|
>
|
||||||
|
<div className="mb-1 ml-2 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="mb-[0.07rem] ml-1.5 text-xxs"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</CardTitle>
|
||||||
|
<FormControl
|
||||||
|
label="Checkly API key"
|
||||||
|
errorText={accessTokenErrorText}
|
||||||
|
isError={accessTokenErrorText !== ""}
|
||||||
|
className="mx-6"
|
||||||
|
>
|
||||||
|
<Input
|
||||||
|
placeholder=""
|
||||||
|
value={accessToken}
|
||||||
|
type="password"
|
||||||
|
autoComplete="new-password"
|
||||||
|
onChange={(e) => setAccessToken(e.target.value)}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
<Button
|
||||||
|
onClick={handleButtonClick}
|
||||||
|
colorSchema="primary"
|
||||||
|
variant="outline_bg"
|
||||||
|
className="mb-6 ml-auto mr-6 mt-2 w-min"
|
||||||
|
isFullWidth={false}
|
||||||
|
isLoading={isLoading}
|
||||||
|
>
|
||||||
|
Connect to Checkly
|
||||||
|
</Button>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
import { createFileRoute } from "@tanstack/react-router";
|
||||||
|
|
||||||
|
import { ChecklyAuthorizePage } from "./ChecklyAuthorizePage";
|
||||||
|
|
||||||
|
export const Route = createFileRoute(
|
||||||
|
"/_authenticate/_inject-org-details/secret-manager/$projectId/_secret-manager-layout/integrations/checkly/authorize"
|
||||||
|
)({
|
||||||
|
component: ChecklyAuthorizePage
|
||||||
|
});
|
||||||
+312
@@ -0,0 +1,312 @@
|
|||||||
|
import { useEffect, useState } from "react";
|
||||||
|
import { Helmet } from "react-helmet";
|
||||||
|
import { faArrowUpRightFromSquare, faBookOpen, faBugs } from "@fortawesome/free-solid-svg-icons";
|
||||||
|
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||||
|
import { useNavigate, useSearch } from "@tanstack/react-router";
|
||||||
|
import { motion } from "framer-motion";
|
||||||
|
|
||||||
|
import {
|
||||||
|
Button,
|
||||||
|
Card,
|
||||||
|
CardTitle,
|
||||||
|
FormControl,
|
||||||
|
Input,
|
||||||
|
Select,
|
||||||
|
SelectItem,
|
||||||
|
Tab,
|
||||||
|
TabList,
|
||||||
|
TabPanel,
|
||||||
|
Tabs
|
||||||
|
} from "@app/components/v2";
|
||||||
|
import { ROUTE_PATHS } from "@app/const/routes";
|
||||||
|
import { useWorkspace } from "@app/context";
|
||||||
|
import { useCreateIntegration } from "@app/hooks/api";
|
||||||
|
import {
|
||||||
|
useGetIntegrationAuthApps,
|
||||||
|
useGetIntegrationAuthById,
|
||||||
|
useGetIntegrationAuthChecklyGroups
|
||||||
|
} from "@app/hooks/api/integrationAuth";
|
||||||
|
import { useGetWorkspaceById } from "@app/hooks/api/workspace";
|
||||||
|
|
||||||
|
enum TabSections {
|
||||||
|
Connection = "connection",
|
||||||
|
Options = "options"
|
||||||
|
}
|
||||||
|
|
||||||
|
export const ChecklyConfigurePage = () => {
|
||||||
|
const navigate = useNavigate();
|
||||||
|
const { mutateAsync } = useCreateIntegration();
|
||||||
|
|
||||||
|
const integrationAuthId = useSearch({
|
||||||
|
from: ROUTE_PATHS.SecretManager.Integratons.ChecklyConfigurePage.id,
|
||||||
|
select: (el) => el.integrationAuthId
|
||||||
|
});
|
||||||
|
const { currentWorkspace } = useWorkspace();
|
||||||
|
|
||||||
|
const [selectedSourceEnvironment, setSelectedSourceEnvironment] = useState("");
|
||||||
|
const [secretPath, setSecretPath] = useState("/");
|
||||||
|
const [secretSuffix, setSecretSuffix] = useState("");
|
||||||
|
|
||||||
|
const [targetAppId, setTargetAppId] = useState("");
|
||||||
|
const [targetGroupId, setTargetGroupId] = useState("");
|
||||||
|
|
||||||
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
|
|
||||||
|
const { data: workspace } = useGetWorkspaceById(currentWorkspace.id);
|
||||||
|
const { data: integrationAuth } = useGetIntegrationAuthById((integrationAuthId as string) ?? "");
|
||||||
|
const { data: integrationAuthApps, isPending: isIntegrationAuthAppsLoading } =
|
||||||
|
useGetIntegrationAuthApps({
|
||||||
|
integrationAuthId: (integrationAuthId as string) ?? ""
|
||||||
|
});
|
||||||
|
const { data: integrationAuthGroups, isPending: isintegrationAuthGroupsLoading } =
|
||||||
|
useGetIntegrationAuthChecklyGroups({
|
||||||
|
integrationAuthId: (integrationAuthId as string) ?? "",
|
||||||
|
accountId: targetAppId
|
||||||
|
});
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (workspace) {
|
||||||
|
setSelectedSourceEnvironment(workspace.environments[0].slug);
|
||||||
|
}
|
||||||
|
}, [workspace]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (integrationAuthApps) {
|
||||||
|
if (integrationAuthApps.length > 0) {
|
||||||
|
setTargetAppId(integrationAuthApps[0].appId as string);
|
||||||
|
} else {
|
||||||
|
setTargetAppId("none");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, [integrationAuthApps]);
|
||||||
|
|
||||||
|
const handleButtonClick = async () => {
|
||||||
|
try {
|
||||||
|
if (!integrationAuth?.id) return;
|
||||||
|
|
||||||
|
setIsLoading(true);
|
||||||
|
|
||||||
|
const targetApp = integrationAuthApps?.find(
|
||||||
|
(integrationAuthApp) => integrationAuthApp.appId === targetAppId
|
||||||
|
);
|
||||||
|
const targetGroup = integrationAuthGroups?.find(
|
||||||
|
(group) => group.groupId === Number(targetGroupId)
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!targetApp) return;
|
||||||
|
|
||||||
|
await mutateAsync({
|
||||||
|
integrationAuthId: integrationAuth?.id,
|
||||||
|
isActive: true,
|
||||||
|
app: targetApp?.name,
|
||||||
|
appId: targetApp?.appId,
|
||||||
|
sourceEnvironment: selectedSourceEnvironment,
|
||||||
|
targetService: targetGroup?.name,
|
||||||
|
targetServiceId: targetGroup?.groupId ? String(targetGroup?.groupId) : undefined,
|
||||||
|
secretPath,
|
||||||
|
metadata: {
|
||||||
|
secretSuffix
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
setIsLoading(false);
|
||||||
|
|
||||||
|
navigate({
|
||||||
|
to: "/secret-manager/$projectId/integrations",
|
||||||
|
params: {
|
||||||
|
projectId: currentWorkspace.id
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return integrationAuth &&
|
||||||
|
workspace &&
|
||||||
|
selectedSourceEnvironment &&
|
||||||
|
integrationAuthApps &&
|
||||||
|
integrationAuthGroups &&
|
||||||
|
targetAppId ? (
|
||||||
|
<div className="flex h-full w-full flex-col items-center justify-center bg-gradient-to-tr from-mineshaft-900 to-bunker-900 py-6">
|
||||||
|
<Helmet>
|
||||||
|
<title>Set Up Checkly Integration</title>
|
||||||
|
</Helmet>
|
||||||
|
<Card className="max-w-lg rounded-md border border-mineshaft-600 p-0">
|
||||||
|
<CardTitle
|
||||||
|
className="px-6 text-left text-xl"
|
||||||
|
subTitle="Choose which environment in Infisical you want to sync to Checkly environment variables."
|
||||||
|
>
|
||||||
|
<div className="flex flex-row items-center">
|
||||||
|
<div className="flex items-center pb-0.5">
|
||||||
|
<img
|
||||||
|
src="/images/integrations/Checkly.png"
|
||||||
|
height={30}
|
||||||
|
width={30}
|
||||||
|
alt="Checkly logo"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<span className="ml-2.5">Checkly Integration </span>
|
||||||
|
<a
|
||||||
|
href="https://infisical.com/docs/integrations/cloud/checkly"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
>
|
||||||
|
<div className="mb-1 ml-2 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="mb-[0.07rem] ml-1.5 text-xxs"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</CardTitle>
|
||||||
|
<Tabs defaultValue={TabSections.Connection} className="px-6">
|
||||||
|
<TabList>
|
||||||
|
<div className="flex w-full flex-row border-b border-mineshaft-600">
|
||||||
|
<Tab value={TabSections.Connection}>Connection</Tab>
|
||||||
|
<Tab value={TabSections.Options}>Options</Tab>
|
||||||
|
</div>
|
||||||
|
</TabList>
|
||||||
|
<TabPanel value={TabSections.Connection}>
|
||||||
|
<motion.div
|
||||||
|
key="panel-1"
|
||||||
|
transition={{ duration: 0.15 }}
|
||||||
|
initial={{ opacity: 0, translateX: 30 }}
|
||||||
|
animate={{ opacity: 1, translateX: 0 }}
|
||||||
|
exit={{ opacity: 0, translateX: 30 }}
|
||||||
|
>
|
||||||
|
<FormControl label="Infisical Project Environment">
|
||||||
|
<Select
|
||||||
|
value={selectedSourceEnvironment}
|
||||||
|
onValueChange={(val) => setSelectedSourceEnvironment(val)}
|
||||||
|
className="w-full border border-mineshaft-500"
|
||||||
|
>
|
||||||
|
{workspace?.environments.map((sourceEnvironment) => (
|
||||||
|
<SelectItem
|
||||||
|
value={sourceEnvironment.slug}
|
||||||
|
key={`source-environment-${sourceEnvironment.slug}`}
|
||||||
|
>
|
||||||
|
{sourceEnvironment.name}
|
||||||
|
</SelectItem>
|
||||||
|
))}
|
||||||
|
</Select>
|
||||||
|
</FormControl>
|
||||||
|
<FormControl label="Secrets Path">
|
||||||
|
<Input
|
||||||
|
value={secretPath}
|
||||||
|
onChange={(evt) => setSecretPath(evt.target.value)}
|
||||||
|
placeholder="Provide a path, default is /"
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
<FormControl label="Checkly Account">
|
||||||
|
<Select
|
||||||
|
value={targetAppId}
|
||||||
|
onValueChange={(val) => setTargetAppId(val)}
|
||||||
|
className="w-full border border-mineshaft-500"
|
||||||
|
isDisabled={integrationAuthApps.length === 0}
|
||||||
|
>
|
||||||
|
{integrationAuthApps.length > 0 ? (
|
||||||
|
integrationAuthApps.map((integrationAuthApp) => (
|
||||||
|
<SelectItem
|
||||||
|
value={integrationAuthApp.appId as string}
|
||||||
|
key={`target-app-${integrationAuthApp.appId as string}`}
|
||||||
|
>
|
||||||
|
{integrationAuthApp.name}
|
||||||
|
</SelectItem>
|
||||||
|
))
|
||||||
|
) : (
|
||||||
|
<SelectItem value="none" key="target-app-none">
|
||||||
|
No apps found
|
||||||
|
</SelectItem>
|
||||||
|
)}
|
||||||
|
</Select>
|
||||||
|
</FormControl>
|
||||||
|
<FormControl label="Checkly Group (Optional)">
|
||||||
|
<Select
|
||||||
|
value={targetGroupId}
|
||||||
|
onValueChange={(val) => setTargetGroupId(val)}
|
||||||
|
className="w-full border border-mineshaft-500"
|
||||||
|
>
|
||||||
|
{integrationAuthGroups.length > 0 ? (
|
||||||
|
integrationAuthGroups.map((integrationAuthGroup) => (
|
||||||
|
<SelectItem
|
||||||
|
value={String(integrationAuthGroup.groupId)}
|
||||||
|
key={`target-group-${String(integrationAuthGroup.groupId)}`}
|
||||||
|
>
|
||||||
|
{integrationAuthGroup.name}
|
||||||
|
</SelectItem>
|
||||||
|
))
|
||||||
|
) : (
|
||||||
|
<SelectItem value="none" key="target-group-none">
|
||||||
|
No groups found
|
||||||
|
</SelectItem>
|
||||||
|
)}
|
||||||
|
</Select>
|
||||||
|
</FormControl>
|
||||||
|
</motion.div>
|
||||||
|
</TabPanel>
|
||||||
|
<TabPanel value={TabSections.Options}>
|
||||||
|
<motion.div
|
||||||
|
key="panel-1"
|
||||||
|
transition={{ duration: 0.15 }}
|
||||||
|
initial={{ opacity: 0, translateX: -30 }}
|
||||||
|
animate={{ opacity: 1, translateX: 0 }}
|
||||||
|
exit={{ opacity: 0, translateX: 30 }}
|
||||||
|
>
|
||||||
|
<FormControl label="Append Secret Names with..." className="pb-[9.75rem]">
|
||||||
|
<Input
|
||||||
|
value={secretSuffix}
|
||||||
|
onChange={(evt) => setSecretSuffix(evt.target.value)}
|
||||||
|
placeholder="Provide a suffix for secret names, default is no suffix"
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
</motion.div>
|
||||||
|
</TabPanel>
|
||||||
|
</Tabs>
|
||||||
|
<Button
|
||||||
|
onClick={handleButtonClick}
|
||||||
|
color="mineshaft"
|
||||||
|
variant="outline_bg"
|
||||||
|
className="mb-6 ml-auto mr-6"
|
||||||
|
isFullWidth={false}
|
||||||
|
isLoading={isLoading}
|
||||||
|
>
|
||||||
|
Create Integration
|
||||||
|
</Button>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="flex h-full w-full items-center justify-center">
|
||||||
|
<Helmet>
|
||||||
|
<title>Set Up Checkly Integration</title>
|
||||||
|
</Helmet>
|
||||||
|
{isIntegrationAuthAppsLoading || isintegrationAuthGroupsLoading ? (
|
||||||
|
<img
|
||||||
|
src="/images/loading/loading.gif"
|
||||||
|
height={70}
|
||||||
|
width={120}
|
||||||
|
alt="infisical loading indicator"
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<div className="flex h-max max-w-md flex-col rounded-md border border-mineshaft-600 bg-mineshaft-800 p-6 text-center text-mineshaft-200">
|
||||||
|
<FontAwesomeIcon icon={faBugs} className="inlineli my-2 text-6xl" />
|
||||||
|
<p>
|
||||||
|
Something went wrong. Please contact{" "}
|
||||||
|
<a
|
||||||
|
className="inline cursor-pointer text-mineshaft-100 underline decoration-primary-500 underline-offset-4 opacity-80 duration-200 hover:opacity-100"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
href="mailto:[email protected]"
|
||||||
|
>
|
||||||
|
support@infisical.com
|
||||||
|
</a>{" "}
|
||||||
|
if the issue persists.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
import { createFileRoute } from "@tanstack/react-router";
|
||||||
|
import { zodValidator } from "@tanstack/zod-adapter";
|
||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
import { ChecklyConfigurePage } from "./ChecklyConfigurePage";
|
||||||
|
|
||||||
|
const ChecklyConfigurePageQueryParamsSchema = z.object({
|
||||||
|
integrationAuthId: z.string()
|
||||||
|
});
|
||||||
|
|
||||||
|
export const Route = createFileRoute(
|
||||||
|
"/_authenticate/_inject-org-details/secret-manager/$projectId/_secret-manager-layout/integrations/checkly/create"
|
||||||
|
)({
|
||||||
|
component: ChecklyConfigurePage,
|
||||||
|
validateSearch: zodValidator(ChecklyConfigurePageQueryParamsSchema)
|
||||||
|
});
|
||||||
+108
@@ -0,0 +1,108 @@
|
|||||||
|
import { useState } from "react";
|
||||||
|
import { Helmet } from "react-helmet";
|
||||||
|
import { faArrowUpRightFromSquare, faBookOpen } from "@fortawesome/free-solid-svg-icons";
|
||||||
|
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||||
|
import { useNavigate } from "@tanstack/react-router";
|
||||||
|
|
||||||
|
import { Button, Card, CardTitle, FormControl, Input } from "@app/components/v2";
|
||||||
|
import { useWorkspace } from "@app/context";
|
||||||
|
import { useSaveIntegrationAccessToken } from "@app/hooks/api";
|
||||||
|
|
||||||
|
export const CircleCIAuthorizePage = () => {
|
||||||
|
const navigate = useNavigate();
|
||||||
|
const { mutateAsync } = useSaveIntegrationAccessToken();
|
||||||
|
|
||||||
|
const [apiKey, setApiKey] = useState("");
|
||||||
|
const [apiKeyErrorText, setApiKeyErrorText] = useState("");
|
||||||
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
|
const { currentWorkspace } = useWorkspace();
|
||||||
|
|
||||||
|
const handleButtonClick = async () => {
|
||||||
|
try {
|
||||||
|
setApiKeyErrorText("");
|
||||||
|
if (apiKey.length === 0) {
|
||||||
|
setApiKeyErrorText("API Key cannot be blank");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setIsLoading(true);
|
||||||
|
|
||||||
|
const integrationAuth = await mutateAsync({
|
||||||
|
workspaceId: currentWorkspace.id,
|
||||||
|
integration: "circleci",
|
||||||
|
accessToken: apiKey
|
||||||
|
});
|
||||||
|
|
||||||
|
setIsLoading(false);
|
||||||
|
|
||||||
|
navigate({
|
||||||
|
to: "/secret-manager/$projectId/integrations/circleci/create",
|
||||||
|
params: {
|
||||||
|
projectId: currentWorkspace.id
|
||||||
|
},
|
||||||
|
search: {
|
||||||
|
integrationAuthId: integrationAuth.id
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex h-full w-full items-center justify-center">
|
||||||
|
<Helmet>
|
||||||
|
<title>Authorize CircleCI Integration</title>
|
||||||
|
</Helmet>
|
||||||
|
<Card className="mb-12 max-w-lg rounded-md border border-mineshaft-600">
|
||||||
|
<CardTitle
|
||||||
|
className="px-6 text-left text-xl"
|
||||||
|
subTitle="After adding your API Token, 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="flex items-center pb-0.5">
|
||||||
|
<img
|
||||||
|
src="/images/integrations/CircleCI.png"
|
||||||
|
height={30}
|
||||||
|
width={30}
|
||||||
|
alt="CircleCI logo"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<span className="ml-1.5">CircleCI Integration </span>
|
||||||
|
<a
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
href="https://infisical.com/docs/integrations/cicd/circleci"
|
||||||
|
>
|
||||||
|
<div className="mb-1 ml-2 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="mb-[0.07rem] ml-1.5 text-xxs"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</CardTitle>
|
||||||
|
<FormControl
|
||||||
|
label="CircleCI API Token"
|
||||||
|
errorText={apiKeyErrorText}
|
||||||
|
isError={apiKeyErrorText !== ""}
|
||||||
|
className="px-6"
|
||||||
|
>
|
||||||
|
<Input placeholder="" value={apiKey} onChange={(e) => setApiKey(e.target.value)} />
|
||||||
|
</FormControl>
|
||||||
|
<Button
|
||||||
|
onClick={handleButtonClick}
|
||||||
|
colorSchema="primary"
|
||||||
|
variant="outline_bg"
|
||||||
|
className="mb-6 ml-auto mr-6 mt-2 w-min"
|
||||||
|
isLoading={isLoading}
|
||||||
|
>
|
||||||
|
Connect to CircleCI
|
||||||
|
</Button>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
import { createFileRoute } from "@tanstack/react-router";
|
||||||
|
|
||||||
|
import { CircleCIAuthorizePage } from "./CircleCIAuthorizePage";
|
||||||
|
|
||||||
|
export const Route = createFileRoute(
|
||||||
|
"/_authenticate/_inject-org-details/secret-manager/$projectId/_secret-manager-layout/integrations/circleci/authorize"
|
||||||
|
)({
|
||||||
|
component: CircleCIAuthorizePage
|
||||||
|
});
|
||||||
+313
@@ -0,0 +1,313 @@
|
|||||||
|
import { Controller, useForm } from "react-hook-form";
|
||||||
|
import { faArrowUpRightFromSquare, faBookOpen } from "@fortawesome/free-solid-svg-icons";
|
||||||
|
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||||
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
|
import { useNavigate, useSearch } from "@tanstack/react-router";
|
||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
import { createNotification } from "@app/components/notifications";
|
||||||
|
import {
|
||||||
|
Button,
|
||||||
|
Card,
|
||||||
|
CardTitle,
|
||||||
|
FilterableSelect,
|
||||||
|
FormControl,
|
||||||
|
Select,
|
||||||
|
SelectItem,
|
||||||
|
Spinner
|
||||||
|
} from "@app/components/v2";
|
||||||
|
import { SecretPathInput } from "@app/components/v2/SecretPathInput";
|
||||||
|
import { ROUTE_PATHS } from "@app/const/routes";
|
||||||
|
import { useWorkspace } from "@app/context";
|
||||||
|
import { useCreateIntegration, useGetIntegrationAuthCircleCIOrganizations } from "@app/hooks/api";
|
||||||
|
import { CircleCiScope } from "@app/hooks/api/integrationAuth/types";
|
||||||
|
|
||||||
|
const formSchema = z.discriminatedUnion("scope", [
|
||||||
|
z.object({
|
||||||
|
scope: z.literal(CircleCiScope.Context),
|
||||||
|
secretPath: z.string().default("/"),
|
||||||
|
sourceEnvironment: z.object({ name: z.string(), slug: z.string() }),
|
||||||
|
targetOrg: z.object({ name: z.string().min(1), slug: z.string().min(1) }),
|
||||||
|
targetContext: z.object({ name: z.string().min(1), id: z.string().min(1) })
|
||||||
|
}),
|
||||||
|
z.object({
|
||||||
|
scope: z.literal(CircleCiScope.Project),
|
||||||
|
secretPath: z.string().default("/"),
|
||||||
|
sourceEnvironment: z.object({ name: z.string(), slug: z.string() }),
|
||||||
|
targetOrg: z.object({ name: z.string().min(1), slug: z.string().min(1) }),
|
||||||
|
targetProject: z.object({ name: z.string().min(1), id: z.string().min(1) })
|
||||||
|
})
|
||||||
|
]);
|
||||||
|
|
||||||
|
type TFormData = z.infer<typeof formSchema>;
|
||||||
|
|
||||||
|
export const CircleCIConfigurePage = () => {
|
||||||
|
const navigate = useNavigate();
|
||||||
|
const { mutateAsync, isPending: isCreatingIntegration } = useCreateIntegration();
|
||||||
|
const { currentWorkspace } = useWorkspace();
|
||||||
|
|
||||||
|
const integrationAuthId = useSearch({
|
||||||
|
from: ROUTE_PATHS.SecretManager.Integratons.CircleConfigurePage.id,
|
||||||
|
select: (el) => el.integrationAuthId
|
||||||
|
});
|
||||||
|
|
||||||
|
const { control, watch, handleSubmit, setValue } = useForm<TFormData>({
|
||||||
|
resolver: zodResolver(formSchema),
|
||||||
|
defaultValues: {
|
||||||
|
secretPath: "/",
|
||||||
|
sourceEnvironment: currentWorkspace?.environments[0],
|
||||||
|
scope: CircleCiScope.Project
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const selectedScope = watch("scope");
|
||||||
|
const selectedOrg = watch("targetOrg");
|
||||||
|
|
||||||
|
const { data: circleCIOrganizations, isPending: isCircleCIOrganizationsLoading } =
|
||||||
|
useGetIntegrationAuthCircleCIOrganizations(integrationAuthId);
|
||||||
|
|
||||||
|
const selectedOrganizationEntry = selectedOrg
|
||||||
|
? circleCIOrganizations?.find((org) => org.slug === selectedOrg.slug)
|
||||||
|
: undefined;
|
||||||
|
|
||||||
|
const onSubmit = async (data: TFormData) => {
|
||||||
|
try {
|
||||||
|
if (data.scope === CircleCiScope.Context) {
|
||||||
|
await mutateAsync({
|
||||||
|
scope: data.scope,
|
||||||
|
integrationAuthId,
|
||||||
|
isActive: true,
|
||||||
|
sourceEnvironment: data.sourceEnvironment.slug,
|
||||||
|
app: data.targetContext.name,
|
||||||
|
appId: data.targetContext.id,
|
||||||
|
owner: data.targetOrg.name,
|
||||||
|
secretPath: data.secretPath
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
await mutateAsync({
|
||||||
|
scope: data.scope,
|
||||||
|
integrationAuthId,
|
||||||
|
isActive: true,
|
||||||
|
app: data.targetProject.name, // project name
|
||||||
|
owner: data.targetOrg.name, // organization name
|
||||||
|
appId: data.targetProject.id, // project id (used for syncing)
|
||||||
|
sourceEnvironment: data.sourceEnvironment.slug,
|
||||||
|
secretPath: data.secretPath
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
createNotification({
|
||||||
|
type: "success",
|
||||||
|
text: "Successfully created integration"
|
||||||
|
});
|
||||||
|
navigate({
|
||||||
|
to: "/secret-manager/$projectId/integrations",
|
||||||
|
params: {
|
||||||
|
projectId: currentWorkspace.id
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
createNotification({
|
||||||
|
type: "error",
|
||||||
|
text: "Failed to create integration"
|
||||||
|
});
|
||||||
|
console.error(err);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if (isCircleCIOrganizationsLoading)
|
||||||
|
return (
|
||||||
|
<div className="flex h-full w-full items-center justify-center p-24">
|
||||||
|
<Spinner />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<form
|
||||||
|
onSubmit={handleSubmit(onSubmit)}
|
||||||
|
className="flex h-full w-full items-center justify-center"
|
||||||
|
>
|
||||||
|
<Card className="max-w-lg rounded-md p-8 pt-4">
|
||||||
|
<CardTitle
|
||||||
|
className="w-full px-0 text-left text-xl"
|
||||||
|
subTitle="Choose which environment or folder in Infisical you want to sync to CircleCI."
|
||||||
|
>
|
||||||
|
<div className="flex w-full flex-row items-center justify-between">
|
||||||
|
<div className="flex flex-row items-center gap-1.5">
|
||||||
|
<img
|
||||||
|
src="/images/integrations/CircleCI.png"
|
||||||
|
height={30}
|
||||||
|
width={30}
|
||||||
|
alt="CircleCI logo"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<span className="">CircleCI Context Integration </span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<a
|
||||||
|
href="https://infisical.com/docs/integrations/cicd/circleci"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
>
|
||||||
|
<div className="mb-1 ml-2 flex cursor-pointer flex-row items-center gap-0.5 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="mb-[0.07rem] ml-1.5 text-xxs"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</CardTitle>
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
|
name="sourceEnvironment"
|
||||||
|
render={({ field: { value, onChange }, fieldState: { error } }) => (
|
||||||
|
<FormControl
|
||||||
|
errorText={error?.message}
|
||||||
|
isError={Boolean(error)}
|
||||||
|
label="Project Environment"
|
||||||
|
>
|
||||||
|
<FilterableSelect
|
||||||
|
getOptionValue={(option) => option.slug}
|
||||||
|
value={value}
|
||||||
|
getOptionLabel={(option) => option.name}
|
||||||
|
onChange={onChange}
|
||||||
|
options={currentWorkspace?.environments}
|
||||||
|
placeholder="Select a project environment"
|
||||||
|
isDisabled={!currentWorkspace?.environments.length}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
|
name="secretPath"
|
||||||
|
render={({ field, fieldState: { error } }) => (
|
||||||
|
<FormControl label="Secrets Path" errorText={error?.message} isError={Boolean(error)}>
|
||||||
|
<SecretPathInput {...field} />
|
||||||
|
</FormControl>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
|
name="targetOrg"
|
||||||
|
render={({ field: { value, onChange }, fieldState: { error } }) => (
|
||||||
|
<FormControl
|
||||||
|
errorText={error?.message}
|
||||||
|
isError={Boolean(error)}
|
||||||
|
label="CircleCI Organization"
|
||||||
|
>
|
||||||
|
<FilterableSelect
|
||||||
|
getOptionValue={(option) => option.slug}
|
||||||
|
value={value}
|
||||||
|
getOptionLabel={(option) => option.name}
|
||||||
|
onChange={(e) => {
|
||||||
|
setValue("targetProject", {
|
||||||
|
name: "",
|
||||||
|
id: ""
|
||||||
|
});
|
||||||
|
setValue("targetContext", {
|
||||||
|
name: "",
|
||||||
|
id: ""
|
||||||
|
});
|
||||||
|
|
||||||
|
onChange(e);
|
||||||
|
}}
|
||||||
|
options={circleCIOrganizations}
|
||||||
|
placeholder={
|
||||||
|
circleCIOrganizations?.length
|
||||||
|
? "Select an organization..."
|
||||||
|
: "No organizations found..."
|
||||||
|
}
|
||||||
|
isDisabled={!circleCIOrganizations?.length}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
|
name="scope"
|
||||||
|
render={({ field: { onChange, ...field }, fieldState: { error } }) => (
|
||||||
|
<FormControl label="Scope" errorText={error?.message} isError={Boolean(error)}>
|
||||||
|
<Select
|
||||||
|
defaultValue={field.value}
|
||||||
|
onValueChange={(e) => {
|
||||||
|
onChange(e);
|
||||||
|
}}
|
||||||
|
className="w-full border border-mineshaft-500"
|
||||||
|
>
|
||||||
|
<SelectItem value={CircleCiScope.Project}>Project</SelectItem>
|
||||||
|
<SelectItem value={CircleCiScope.Context}>Context</SelectItem>
|
||||||
|
</Select>
|
||||||
|
</FormControl>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
{selectedScope === CircleCiScope.Context && selectedOrganizationEntry && (
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
|
name="targetContext"
|
||||||
|
render={({ field: { value, onChange }, fieldState: { error } }) => (
|
||||||
|
<FormControl
|
||||||
|
errorText={error?.message}
|
||||||
|
isError={Boolean(error)}
|
||||||
|
label="CircleCI Context"
|
||||||
|
>
|
||||||
|
<FilterableSelect
|
||||||
|
value={value}
|
||||||
|
getOptionValue={(option) => option.id!}
|
||||||
|
getOptionLabel={(option) => option.name}
|
||||||
|
onChange={onChange}
|
||||||
|
options={selectedOrganizationEntry?.contexts}
|
||||||
|
placeholder={
|
||||||
|
selectedOrganizationEntry.contexts?.length
|
||||||
|
? "Select a context..."
|
||||||
|
: "No contexts found..."
|
||||||
|
}
|
||||||
|
isDisabled={!selectedOrganizationEntry.contexts?.length}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{selectedScope === CircleCiScope.Project && selectedOrganizationEntry && (
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
|
name="targetProject"
|
||||||
|
render={({ field: { value, onChange }, fieldState: { error } }) => (
|
||||||
|
<FormControl
|
||||||
|
errorText={error?.message}
|
||||||
|
isError={Boolean(error)}
|
||||||
|
label="CircleCI Project"
|
||||||
|
>
|
||||||
|
<FilterableSelect
|
||||||
|
value={value}
|
||||||
|
getOptionValue={(option) => option.id!}
|
||||||
|
getOptionLabel={(option) => option.name}
|
||||||
|
onChange={onChange}
|
||||||
|
options={selectedOrganizationEntry?.projects}
|
||||||
|
placeholder={
|
||||||
|
selectedOrganizationEntry.projects?.length
|
||||||
|
? "Select a project..."
|
||||||
|
: "No projects found..."
|
||||||
|
}
|
||||||
|
isDisabled={!selectedOrganizationEntry.projects?.length}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
<Button
|
||||||
|
type="submit"
|
||||||
|
colorSchema="primary"
|
||||||
|
className="mt-4"
|
||||||
|
isLoading={isCreatingIntegration}
|
||||||
|
isDisabled={isCreatingIntegration}
|
||||||
|
>
|
||||||
|
Create Integration
|
||||||
|
</Button>
|
||||||
|
</Card>
|
||||||
|
</form>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
import { createFileRoute } from "@tanstack/react-router";
|
||||||
|
import { zodValidator } from "@tanstack/zod-adapter";
|
||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
import { CircleCIConfigurePage } from "./CircleCIConfigurePage";
|
||||||
|
|
||||||
|
const CircleCIConfigurePageQueryParamsSchema = z.object({
|
||||||
|
integrationAuthId: z.string()
|
||||||
|
});
|
||||||
|
|
||||||
|
export const Route = createFileRoute(
|
||||||
|
"/_authenticate/_inject-org-details/secret-manager/$projectId/_secret-manager-layout/integrations/circleci/create"
|
||||||
|
)({
|
||||||
|
component: CircleCIConfigurePage,
|
||||||
|
validateSearch: zodValidator(CircleCIConfigurePageQueryParamsSchema)
|
||||||
|
});
|
||||||
+72
@@ -0,0 +1,72 @@
|
|||||||
|
import { useState } from "react";
|
||||||
|
import { useNavigate } from "@tanstack/react-router";
|
||||||
|
|
||||||
|
import { Button, Card, CardTitle, FormControl, Input } from "@app/components/v2";
|
||||||
|
import { useWorkspace } from "@app/context";
|
||||||
|
import { useSaveIntegrationAccessToken } from "@app/hooks/api";
|
||||||
|
|
||||||
|
export const Cloud66AuthorizePage = () => {
|
||||||
|
const navigate = useNavigate();
|
||||||
|
const { mutateAsync } = useSaveIntegrationAccessToken();
|
||||||
|
|
||||||
|
const { currentWorkspace } = useWorkspace();
|
||||||
|
|
||||||
|
const [apiKey, setApiKey] = useState("");
|
||||||
|
const [apiKeyErrorText, setApiKeyErrorText] = useState("");
|
||||||
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
|
|
||||||
|
const handleButtonClick = async () => {
|
||||||
|
try {
|
||||||
|
setApiKeyErrorText("");
|
||||||
|
if (apiKey.length === 0) {
|
||||||
|
setApiKeyErrorText("Access token cannot be blank");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setIsLoading(true);
|
||||||
|
|
||||||
|
const integrationAuth = await mutateAsync({
|
||||||
|
workspaceId: currentWorkspace.id,
|
||||||
|
integration: "cloud-66",
|
||||||
|
accessToken: apiKey
|
||||||
|
});
|
||||||
|
|
||||||
|
setIsLoading(false);
|
||||||
|
|
||||||
|
navigate({
|
||||||
|
to: "/secret-manager/$projectId/integrations/cloud-66/create",
|
||||||
|
params: {
|
||||||
|
projectId: currentWorkspace.id
|
||||||
|
},
|
||||||
|
search: {
|
||||||
|
integrationAuthId: integrationAuth.id
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
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">Cloud 66 Integration</CardTitle>
|
||||||
|
<FormControl
|
||||||
|
label="Cloud 66 Personal Access Token"
|
||||||
|
errorText={apiKeyErrorText}
|
||||||
|
isError={apiKeyErrorText !== ""}
|
||||||
|
>
|
||||||
|
<Input placeholder="" value={apiKey} onChange={(e) => setApiKey(e.target.value)} />
|
||||||
|
</FormControl>
|
||||||
|
<Button
|
||||||
|
onClick={handleButtonClick}
|
||||||
|
color="mineshaft"
|
||||||
|
className="mt-4"
|
||||||
|
isLoading={isLoading}
|
||||||
|
>
|
||||||
|
Connect to Cloud 66
|
||||||
|
</Button>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
import { createFileRoute } from "@tanstack/react-router";
|
||||||
|
|
||||||
|
import { Cloud66AuthorizePage } from "./Cloud66AuthorizePage";
|
||||||
|
|
||||||
|
export const Route = createFileRoute(
|
||||||
|
"/_authenticate/_inject-org-details/secret-manager/$projectId/_secret-manager-layout/integrations/cloud-66/authorize"
|
||||||
|
)({
|
||||||
|
component: Cloud66AuthorizePage
|
||||||
|
});
|
||||||
+151
@@ -0,0 +1,151 @@
|
|||||||
|
import { useEffect, useState } from "react";
|
||||||
|
import { useNavigate, useSearch } from "@tanstack/react-router";
|
||||||
|
|
||||||
|
import {
|
||||||
|
Button,
|
||||||
|
Card,
|
||||||
|
CardTitle,
|
||||||
|
FormControl,
|
||||||
|
Input,
|
||||||
|
Select,
|
||||||
|
SelectItem
|
||||||
|
} from "@app/components/v2";
|
||||||
|
import { ROUTE_PATHS } from "@app/const/routes";
|
||||||
|
import { useWorkspace } from "@app/context";
|
||||||
|
import { useCreateIntegration } from "@app/hooks/api";
|
||||||
|
import {
|
||||||
|
useGetIntegrationAuthApps,
|
||||||
|
useGetIntegrationAuthById
|
||||||
|
} from "@app/hooks/api/integrationAuth";
|
||||||
|
|
||||||
|
export const Cloud66ConfigurePage = () => {
|
||||||
|
const navigate = useNavigate();
|
||||||
|
const { mutateAsync } = useCreateIntegration();
|
||||||
|
|
||||||
|
const integrationAuthId = useSearch({
|
||||||
|
from: ROUTE_PATHS.SecretManager.Integratons.Cloud66ConfigurePage.id,
|
||||||
|
select: (el) => el.integrationAuthId
|
||||||
|
});
|
||||||
|
|
||||||
|
const { currentWorkspace } = useWorkspace();
|
||||||
|
const { data: integrationAuth } = useGetIntegrationAuthById((integrationAuthId as string) ?? "");
|
||||||
|
const { data: integrationAuthApps } = useGetIntegrationAuthApps({
|
||||||
|
integrationAuthId: (integrationAuthId as string) ?? ""
|
||||||
|
});
|
||||||
|
|
||||||
|
const [selectedSourceEnvironment, setSelectedSourceEnvironment] = useState("");
|
||||||
|
const [targetApp, setTargetApp] = useState("");
|
||||||
|
const [secretPath, setSecretPath] = useState("/");
|
||||||
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (currentWorkspace) {
|
||||||
|
setSelectedSourceEnvironment(currentWorkspace.environments[0].slug);
|
||||||
|
}
|
||||||
|
}, [currentWorkspace]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (integrationAuthApps) {
|
||||||
|
if (integrationAuthApps.length > 0) {
|
||||||
|
setTargetApp(integrationAuthApps[0].name);
|
||||||
|
} else {
|
||||||
|
setTargetApp("none");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, [integrationAuthApps]);
|
||||||
|
|
||||||
|
const handleButtonClick = async () => {
|
||||||
|
try {
|
||||||
|
if (!integrationAuth?.id) return;
|
||||||
|
|
||||||
|
setIsLoading(true);
|
||||||
|
|
||||||
|
await mutateAsync({
|
||||||
|
integrationAuthId: integrationAuth?.id,
|
||||||
|
isActive: true,
|
||||||
|
app: targetApp,
|
||||||
|
appId: integrationAuthApps?.find(
|
||||||
|
(integrationAuthApp) => integrationAuthApp.name === targetApp
|
||||||
|
)?.appId,
|
||||||
|
sourceEnvironment: selectedSourceEnvironment,
|
||||||
|
secretPath
|
||||||
|
});
|
||||||
|
|
||||||
|
setIsLoading(false);
|
||||||
|
|
||||||
|
navigate({
|
||||||
|
to: "/secret-manager/$projectId/integrations",
|
||||||
|
params: {
|
||||||
|
projectId: currentWorkspace.id
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return integrationAuth && selectedSourceEnvironment && 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">Cloud 66 Integration</CardTitle>
|
||||||
|
<FormControl label="Project Environment" className="mt-4">
|
||||||
|
<Select
|
||||||
|
value={selectedSourceEnvironment}
|
||||||
|
onValueChange={(val) => setSelectedSourceEnvironment(val)}
|
||||||
|
className="w-full border border-mineshaft-500"
|
||||||
|
>
|
||||||
|
{currentWorkspace?.environments.map((sourceEnvironment) => (
|
||||||
|
<SelectItem
|
||||||
|
value={sourceEnvironment.slug}
|
||||||
|
key={`source-environment-${sourceEnvironment.slug}`}
|
||||||
|
>
|
||||||
|
{sourceEnvironment.name}
|
||||||
|
</SelectItem>
|
||||||
|
))}
|
||||||
|
</Select>
|
||||||
|
</FormControl>
|
||||||
|
<FormControl label="Secrets Path">
|
||||||
|
<Input
|
||||||
|
value={secretPath}
|
||||||
|
onChange={(evt) => setSecretPath(evt.target.value)}
|
||||||
|
placeholder="Provide a path, default is /"
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
<FormControl label="Cloud 66 Application" className="mt-4">
|
||||||
|
<Select
|
||||||
|
value={targetApp}
|
||||||
|
onValueChange={(val) => setTargetApp(val)}
|
||||||
|
className="w-full border border-mineshaft-500"
|
||||||
|
isDisabled={integrationAuthApps.length === 0}
|
||||||
|
>
|
||||||
|
{integrationAuthApps.length > 0 ? (
|
||||||
|
integrationAuthApps.map((integrationAuthApp) => (
|
||||||
|
<SelectItem
|
||||||
|
value={integrationAuthApp.name}
|
||||||
|
key={`target-app-${integrationAuthApp.name}`}
|
||||||
|
>
|
||||||
|
{integrationAuthApp.name}
|
||||||
|
</SelectItem>
|
||||||
|
))
|
||||||
|
) : (
|
||||||
|
<SelectItem value="none" key="target-app-none">
|
||||||
|
No applications found
|
||||||
|
</SelectItem>
|
||||||
|
)}
|
||||||
|
</Select>
|
||||||
|
</FormControl>
|
||||||
|
<Button
|
||||||
|
onClick={handleButtonClick}
|
||||||
|
color="mineshaft"
|
||||||
|
className="mt-4"
|
||||||
|
isLoading={isLoading}
|
||||||
|
isDisabled={integrationAuthApps.length === 0}
|
||||||
|
>
|
||||||
|
Create Integration
|
||||||
|
</Button>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div />
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
import { createFileRoute } from "@tanstack/react-router";
|
||||||
|
import { zodValidator } from "@tanstack/zod-adapter";
|
||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
import { Cloud66ConfigurePage } from "./Cloud66ConfigurePage";
|
||||||
|
|
||||||
|
const Cloud66ConfigurePageQueryParamsSchema = z.object({
|
||||||
|
integrationAuthId: z.string()
|
||||||
|
});
|
||||||
|
|
||||||
|
export const Route = createFileRoute(
|
||||||
|
"/_authenticate/_inject-org-details/secret-manager/$projectId/_secret-manager-layout/integrations/cloud-66/create"
|
||||||
|
)({
|
||||||
|
component: Cloud66ConfigurePage,
|
||||||
|
validateSearch: zodValidator(Cloud66ConfigurePageQueryParamsSchema)
|
||||||
|
});
|
||||||
+101
@@ -0,0 +1,101 @@
|
|||||||
|
import { useState } from "react";
|
||||||
|
import { useNavigate } from "@tanstack/react-router";
|
||||||
|
|
||||||
|
import { Button, Card, CardTitle, FormControl, Input } from "@app/components/v2";
|
||||||
|
import { useWorkspace } from "@app/context";
|
||||||
|
import { useSaveIntegrationAccessToken } from "@app/hooks/api";
|
||||||
|
|
||||||
|
export const CloudflarePagesAuthorizePage = () => {
|
||||||
|
const { mutateAsync } = useSaveIntegrationAccessToken();
|
||||||
|
|
||||||
|
const [accessKey, setAccessKey] = useState("");
|
||||||
|
const [accessKeyErrorText, setAccessKeyErrorText] = useState("");
|
||||||
|
const [accountId, setAccountId] = useState("");
|
||||||
|
const [accountIdErrorText, setAccountIdErrorText] = useState("");
|
||||||
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
|
|
||||||
|
const { currentWorkspace } = useWorkspace();
|
||||||
|
const navigate = useNavigate();
|
||||||
|
|
||||||
|
const handleButtonClick = async () => {
|
||||||
|
try {
|
||||||
|
setAccessKeyErrorText("");
|
||||||
|
setAccountIdErrorText("");
|
||||||
|
if (accessKey.length === 0 || accountId.length === 0) {
|
||||||
|
if (accessKey.length === 0) setAccessKeyErrorText("API token cannot be blank!");
|
||||||
|
if (accountId.length === 0) setAccountIdErrorText("Account ID cannot be blank!");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setIsLoading(true);
|
||||||
|
|
||||||
|
const integrationAuth = await mutateAsync({
|
||||||
|
workspaceId: currentWorkspace.id,
|
||||||
|
integration: "cloudflare-pages",
|
||||||
|
accessId: accountId,
|
||||||
|
accessToken: accessKey
|
||||||
|
});
|
||||||
|
|
||||||
|
setAccessKey("");
|
||||||
|
setAccountId("");
|
||||||
|
setIsLoading(false);
|
||||||
|
|
||||||
|
navigate({
|
||||||
|
to: "/secret-manager/$projectId/integrations/cloudflare-pages/create",
|
||||||
|
params: {
|
||||||
|
projectId: currentWorkspace.id
|
||||||
|
},
|
||||||
|
search: {
|
||||||
|
integrationAuthId: integrationAuth.id
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex h-full w-full items-center justify-center">
|
||||||
|
<Card className="mb-12 max-w-lg rounded-md border border-mineshaft-600">
|
||||||
|
<CardTitle
|
||||||
|
className="px-6 text-left"
|
||||||
|
subTitle="After adding your API-key, you will be prompted to set up an integration for a particular Infisical project and environment."
|
||||||
|
>
|
||||||
|
Cloudflare Pages Integration
|
||||||
|
</CardTitle>
|
||||||
|
<FormControl
|
||||||
|
label="Cloudflare Pages API token"
|
||||||
|
errorText={accessKeyErrorText}
|
||||||
|
isError={accessKeyErrorText !== ""}
|
||||||
|
className="mx-6"
|
||||||
|
>
|
||||||
|
<Input
|
||||||
|
placeholder=""
|
||||||
|
value={accessKey}
|
||||||
|
type="password"
|
||||||
|
autoComplete="new-password"
|
||||||
|
onChange={(e) => setAccessKey(e.target.value)}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
<FormControl
|
||||||
|
label="Cloudflare Pages Account ID"
|
||||||
|
errorText={accountIdErrorText}
|
||||||
|
isError={accountIdErrorText !== ""}
|
||||||
|
className="mx-6"
|
||||||
|
>
|
||||||
|
<Input placeholder="" value={accountId} onChange={(e) => setAccountId(e.target.value)} />
|
||||||
|
</FormControl>
|
||||||
|
<Button
|
||||||
|
onClick={handleButtonClick}
|
||||||
|
color="mineshaft"
|
||||||
|
variant="outline_bg"
|
||||||
|
className="mb-6 ml-auto mr-6 mt-2 w-min"
|
||||||
|
isFullWidth={false}
|
||||||
|
isLoading={isLoading}
|
||||||
|
>
|
||||||
|
Connect to Cloudflare Pages
|
||||||
|
</Button>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
+9
@@ -0,0 +1,9 @@
|
|||||||
|
import { createFileRoute } from "@tanstack/react-router";
|
||||||
|
|
||||||
|
import { CloudflarePagesAuthorizePage } from "./CloudflarePagesAuthorizePage";
|
||||||
|
|
||||||
|
export const Route = createFileRoute(
|
||||||
|
"/_authenticate/_inject-org-details/secret-manager/$projectId/_secret-manager-layout/integrations/cloudflare-pages/authorize"
|
||||||
|
)({
|
||||||
|
component: CloudflarePagesAuthorizePage
|
||||||
|
});
|
||||||
+219
@@ -0,0 +1,219 @@
|
|||||||
|
import { useEffect, useState } from "react";
|
||||||
|
import { useNavigate, useSearch } from "@tanstack/react-router";
|
||||||
|
import axios from "axios";
|
||||||
|
|
||||||
|
import { createNotification } from "@app/components/notifications";
|
||||||
|
import {
|
||||||
|
Button,
|
||||||
|
Card,
|
||||||
|
CardTitle,
|
||||||
|
FormControl,
|
||||||
|
Select,
|
||||||
|
SelectItem,
|
||||||
|
Switch
|
||||||
|
} from "@app/components/v2";
|
||||||
|
import { SecretPathInput } from "@app/components/v2/SecretPathInput";
|
||||||
|
import { ROUTE_PATHS } from "@app/const/routes";
|
||||||
|
import { useWorkspace } from "@app/context";
|
||||||
|
import { useCreateIntegration, useGetWorkspaceById } from "@app/hooks/api";
|
||||||
|
import {
|
||||||
|
useGetIntegrationAuthApps,
|
||||||
|
useGetIntegrationAuthById
|
||||||
|
} from "@app/hooks/api/integrationAuth";
|
||||||
|
|
||||||
|
const cloudflareEnvironments = [
|
||||||
|
{ name: "Production", slug: "production" },
|
||||||
|
{ name: "Preview", slug: "preview" }
|
||||||
|
];
|
||||||
|
|
||||||
|
export const CloudflarePagesConfigurePage = () => {
|
||||||
|
const navigate = useNavigate();
|
||||||
|
const { mutateAsync } = useCreateIntegration();
|
||||||
|
|
||||||
|
const { currentWorkspace } = useWorkspace();
|
||||||
|
|
||||||
|
const integrationAuthId = useSearch({
|
||||||
|
from: ROUTE_PATHS.SecretManager.Integratons.CloudflarePagesConfigurePage.id,
|
||||||
|
select: (el) => el.integrationAuthId
|
||||||
|
});
|
||||||
|
|
||||||
|
const [secretPath, setSecretPath] = useState("/");
|
||||||
|
const { data: workspace } = useGetWorkspaceById(currentWorkspace.id);
|
||||||
|
const { data: integrationAuth } = useGetIntegrationAuthById((integrationAuthId as string) ?? "");
|
||||||
|
const { data: integrationAuthApps } = useGetIntegrationAuthApps({
|
||||||
|
integrationAuthId: (integrationAuthId as string) ?? ""
|
||||||
|
});
|
||||||
|
|
||||||
|
const [selectedSourceEnvironment, setSelectedSourceEnvironment] = useState("");
|
||||||
|
const [targetApp, setTargetApp] = useState("");
|
||||||
|
const [targetAppId, setTargetAppId] = useState("");
|
||||||
|
const [targetEnvironment, setTargetEnvironment] = useState("");
|
||||||
|
const [shouldAutoRedeploy, setShouldAutoRedeploy] = useState(false);
|
||||||
|
|
||||||
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (workspace) {
|
||||||
|
setSelectedSourceEnvironment(workspace.environments[0].slug);
|
||||||
|
}
|
||||||
|
}, [workspace]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (integrationAuthApps) {
|
||||||
|
if (integrationAuthApps.length > 0) {
|
||||||
|
setTargetApp(integrationAuthApps[0].name);
|
||||||
|
setTargetAppId(String(integrationAuthApps[0].appId));
|
||||||
|
setTargetEnvironment(cloudflareEnvironments[0].slug);
|
||||||
|
} else {
|
||||||
|
setTargetApp("none");
|
||||||
|
setTargetEnvironment(cloudflareEnvironments[0].slug);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, [integrationAuthApps]);
|
||||||
|
|
||||||
|
const handleButtonClick = async () => {
|
||||||
|
try {
|
||||||
|
if (!integrationAuth?.id) return;
|
||||||
|
|
||||||
|
setIsLoading(true);
|
||||||
|
|
||||||
|
await mutateAsync({
|
||||||
|
integrationAuthId: integrationAuth?.id,
|
||||||
|
isActive: true,
|
||||||
|
app: targetApp,
|
||||||
|
appId: targetAppId,
|
||||||
|
sourceEnvironment: selectedSourceEnvironment,
|
||||||
|
targetEnvironment,
|
||||||
|
secretPath,
|
||||||
|
metadata: {
|
||||||
|
shouldAutoRedeploy
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
setIsLoading(false);
|
||||||
|
|
||||||
|
navigate({
|
||||||
|
to: "/secret-manager/$projectId/integrations",
|
||||||
|
params: {
|
||||||
|
projectId: currentWorkspace.id
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err);
|
||||||
|
|
||||||
|
let errorMessage: string = "Something went wrong!";
|
||||||
|
if (axios.isAxiosError(err)) {
|
||||||
|
const { message } = err?.response?.data as { message: string };
|
||||||
|
errorMessage = message;
|
||||||
|
}
|
||||||
|
|
||||||
|
createNotification({
|
||||||
|
text: errorMessage,
|
||||||
|
type: "error"
|
||||||
|
});
|
||||||
|
setIsLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return integrationAuth &&
|
||||||
|
workspace &&
|
||||||
|
selectedSourceEnvironment &&
|
||||||
|
integrationAuthApps &&
|
||||||
|
targetEnvironment &&
|
||||||
|
targetApp ? (
|
||||||
|
<div className="flex h-full w-full items-center justify-center bg-gradient-to-tr from-mineshaft-900 to-bunker-900">
|
||||||
|
<Card className="max-w-lg rounded-md border border-mineshaft-600 p-0">
|
||||||
|
<CardTitle
|
||||||
|
className="px-6 text-left"
|
||||||
|
subTitle="Choose which environment in Infisical you want to sync with your Cloudflare Pages project."
|
||||||
|
>
|
||||||
|
Cloudflare Pages Integration
|
||||||
|
</CardTitle>
|
||||||
|
<FormControl label="Infisical Project Environment" className="mt-2 px-6">
|
||||||
|
<Select
|
||||||
|
value={selectedSourceEnvironment}
|
||||||
|
onValueChange={(val) => setSelectedSourceEnvironment(val)}
|
||||||
|
className="w-full border border-mineshaft-500"
|
||||||
|
>
|
||||||
|
{workspace?.environments.map((sourceEnvironment) => (
|
||||||
|
<SelectItem
|
||||||
|
value={sourceEnvironment.slug}
|
||||||
|
key={`source-environment-${sourceEnvironment.slug}`}
|
||||||
|
>
|
||||||
|
{sourceEnvironment.name}
|
||||||
|
</SelectItem>
|
||||||
|
))}
|
||||||
|
</Select>
|
||||||
|
</FormControl>
|
||||||
|
<FormControl label="Infisical Secret Path" className="mt-2 px-6">
|
||||||
|
<SecretPathInput
|
||||||
|
value={secretPath}
|
||||||
|
onChange={(value) => setSecretPath(value)}
|
||||||
|
environment={selectedSourceEnvironment}
|
||||||
|
placeholder="Provide a path, default is /"
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
<FormControl label="Cloudflare Pages Project" className="mt-4 px-6">
|
||||||
|
<Select
|
||||||
|
value={targetApp}
|
||||||
|
onValueChange={(val) => setTargetApp(val)}
|
||||||
|
className="w-full border border-mineshaft-500"
|
||||||
|
isDisabled={integrationAuthApps.length === 0}
|
||||||
|
>
|
||||||
|
{integrationAuthApps.length > 0 ? (
|
||||||
|
integrationAuthApps.map((integrationAuthApp) => (
|
||||||
|
<SelectItem
|
||||||
|
value={integrationAuthApp.name}
|
||||||
|
key={`target-app-${integrationAuthApp.name}`}
|
||||||
|
>
|
||||||
|
{integrationAuthApp.name}
|
||||||
|
</SelectItem>
|
||||||
|
))
|
||||||
|
) : (
|
||||||
|
<SelectItem value="none" key="target-app-none">
|
||||||
|
No apps found
|
||||||
|
</SelectItem>
|
||||||
|
)}
|
||||||
|
</Select>
|
||||||
|
</FormControl>
|
||||||
|
<FormControl label="Cloudflare Pages Environment" className="mt-4 px-6">
|
||||||
|
<Select
|
||||||
|
value={targetEnvironment}
|
||||||
|
onValueChange={(val) => setTargetEnvironment(val)}
|
||||||
|
className="w-full border border-mineshaft-500"
|
||||||
|
>
|
||||||
|
{cloudflareEnvironments.map((cloudflareEnvironment) => (
|
||||||
|
<SelectItem
|
||||||
|
value={cloudflareEnvironment.slug}
|
||||||
|
key={`target-environment-${cloudflareEnvironment.slug}`}
|
||||||
|
>
|
||||||
|
{cloudflareEnvironment.name}
|
||||||
|
</SelectItem>
|
||||||
|
))}
|
||||||
|
</Select>
|
||||||
|
</FormControl>
|
||||||
|
<div className="mb-[2.36rem] ml-1 px-6">
|
||||||
|
<Switch
|
||||||
|
id="redeploy-cloudflare-pages"
|
||||||
|
onCheckedChange={(isChecked: boolean) => setShouldAutoRedeploy(isChecked)}
|
||||||
|
isChecked={shouldAutoRedeploy}
|
||||||
|
>
|
||||||
|
Auto-redeploy service upon secret change
|
||||||
|
</Switch>
|
||||||
|
</div>
|
||||||
|
<Button
|
||||||
|
onClick={handleButtonClick}
|
||||||
|
color="mineshaft"
|
||||||
|
variant="outline_bg"
|
||||||
|
className="mb-6 ml-auto mr-6 mt-2"
|
||||||
|
isFullWidth={false}
|
||||||
|
isLoading={isLoading}
|
||||||
|
>
|
||||||
|
Create Integration
|
||||||
|
</Button>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div />
|
||||||
|
);
|
||||||
|
};
|
||||||
+16
@@ -0,0 +1,16 @@
|
|||||||
|
import { createFileRoute } from "@tanstack/react-router";
|
||||||
|
import { zodValidator } from "@tanstack/zod-adapter";
|
||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
import { CloudflarePagesConfigurePage } from "./CloudflarePagesConfigurePage";
|
||||||
|
|
||||||
|
const CloudflarePagesConfigurePageQueryParamsSchema = z.object({
|
||||||
|
integrationAuthId: z.string()
|
||||||
|
});
|
||||||
|
|
||||||
|
export const Route = createFileRoute(
|
||||||
|
"/_authenticate/_inject-org-details/secret-manager/$projectId/_secret-manager-layout/integrations/cloudflare-pages/create"
|
||||||
|
)({
|
||||||
|
component: CloudflarePagesConfigurePage,
|
||||||
|
validateSearch: zodValidator(CloudflarePagesConfigurePageQueryParamsSchema)
|
||||||
|
});
|
||||||
+101
@@ -0,0 +1,101 @@
|
|||||||
|
import { useState } from "react";
|
||||||
|
import { useNavigate } from "@tanstack/react-router";
|
||||||
|
|
||||||
|
import { Button, Card, CardTitle, FormControl, Input } from "@app/components/v2";
|
||||||
|
import { useWorkspace } from "@app/context";
|
||||||
|
import { useSaveIntegrationAccessToken } from "@app/hooks/api";
|
||||||
|
|
||||||
|
export const CloudflareWorkersAuthorizePage = () => {
|
||||||
|
const navigate = useNavigate();
|
||||||
|
|
||||||
|
const { currentWorkspace } = useWorkspace();
|
||||||
|
const { mutateAsync } = useSaveIntegrationAccessToken();
|
||||||
|
|
||||||
|
const [accessKey, setAccessKey] = useState("");
|
||||||
|
const [accessKeyErrorText, setAccessKeyErrorText] = useState("");
|
||||||
|
const [accountId, setAccountId] = useState("");
|
||||||
|
const [accountIdErrorText, setAccountIdErrorText] = useState("");
|
||||||
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
|
|
||||||
|
const handleButtonClick = async () => {
|
||||||
|
try {
|
||||||
|
setAccessKeyErrorText("");
|
||||||
|
setAccountIdErrorText("");
|
||||||
|
if (accessKey.length === 0 || accountId.length === 0) {
|
||||||
|
if (accessKey.length === 0) setAccessKeyErrorText("API token cannot be blank!");
|
||||||
|
if (accountId.length === 0) setAccountIdErrorText("Account ID cannot be blank!");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setIsLoading(true);
|
||||||
|
|
||||||
|
const integrationAuth = await mutateAsync({
|
||||||
|
workspaceId: currentWorkspace.id,
|
||||||
|
integration: "cloudflare-workers",
|
||||||
|
accessId: accountId,
|
||||||
|
accessToken: accessKey
|
||||||
|
});
|
||||||
|
|
||||||
|
setAccessKey("");
|
||||||
|
setAccountId("");
|
||||||
|
setIsLoading(false);
|
||||||
|
|
||||||
|
navigate({
|
||||||
|
to: "/secret-manager/$projectId/integrations/cloudflare-workers/create",
|
||||||
|
params: {
|
||||||
|
projectId: currentWorkspace.id
|
||||||
|
},
|
||||||
|
search: {
|
||||||
|
integrationAuthId: integrationAuth.id
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex h-full w-full items-center justify-center">
|
||||||
|
<Card className="mb-12 max-w-lg rounded-md border border-mineshaft-600">
|
||||||
|
<CardTitle
|
||||||
|
className="px-6 text-left"
|
||||||
|
subTitle="After adding your API-key, you will be prompted to set up an integration for a particular Infisical project and environment."
|
||||||
|
>
|
||||||
|
Cloudflare Workers Integration
|
||||||
|
</CardTitle>
|
||||||
|
<FormControl
|
||||||
|
label="Cloudflare Workers API token"
|
||||||
|
errorText={accessKeyErrorText}
|
||||||
|
isError={accessKeyErrorText !== ""}
|
||||||
|
className="mx-6"
|
||||||
|
>
|
||||||
|
<Input
|
||||||
|
placeholder=""
|
||||||
|
value={accessKey}
|
||||||
|
type="password"
|
||||||
|
autoComplete="new-password"
|
||||||
|
onChange={(e) => setAccessKey(e.target.value)}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
<FormControl
|
||||||
|
label="Cloudflare Workers Account ID"
|
||||||
|
errorText={accountIdErrorText}
|
||||||
|
isError={accountIdErrorText !== ""}
|
||||||
|
className="mx-6"
|
||||||
|
>
|
||||||
|
<Input placeholder="" value={accountId} onChange={(e) => setAccountId(e.target.value)} />
|
||||||
|
</FormControl>
|
||||||
|
<Button
|
||||||
|
onClick={handleButtonClick}
|
||||||
|
color="mineshaft"
|
||||||
|
variant="outline_bg"
|
||||||
|
className="mb-6 ml-auto mr-6 mt-2 w-min"
|
||||||
|
isFullWidth={false}
|
||||||
|
isLoading={isLoading}
|
||||||
|
>
|
||||||
|
Connect to Cloudflare Workers
|
||||||
|
</Button>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
+9
@@ -0,0 +1,9 @@
|
|||||||
|
import { createFileRoute } from "@tanstack/react-router";
|
||||||
|
|
||||||
|
import { CloudflareWorkersAuthorizePage } from "./CloudflareWorkersAuthorizePage";
|
||||||
|
|
||||||
|
export const Route = createFileRoute(
|
||||||
|
"/_authenticate/_inject-org-details/secret-manager/$projectId/_secret-manager-layout/integrations/cloudflare-workers/authorize"
|
||||||
|
)({
|
||||||
|
component: CloudflareWorkersAuthorizePage
|
||||||
|
});
|
||||||
+162
@@ -0,0 +1,162 @@
|
|||||||
|
import { useEffect, useState } from "react";
|
||||||
|
import { useNavigate, useSearch } from "@tanstack/react-router";
|
||||||
|
import axios from "axios";
|
||||||
|
|
||||||
|
import { createNotification } from "@app/components/notifications";
|
||||||
|
import { Button, Card, CardTitle, FormControl, Select, SelectItem } from "@app/components/v2";
|
||||||
|
import { SecretPathInput } from "@app/components/v2/SecretPathInput";
|
||||||
|
import { ROUTE_PATHS } from "@app/const/routes";
|
||||||
|
import { useWorkspace } from "@app/context";
|
||||||
|
import { useCreateIntegration } from "@app/hooks/api";
|
||||||
|
import {
|
||||||
|
useGetIntegrationAuthApps,
|
||||||
|
useGetIntegrationAuthById
|
||||||
|
} from "@app/hooks/api/integrationAuth";
|
||||||
|
|
||||||
|
export const CloudflareWorkersConfigurePage = () => {
|
||||||
|
const navigate = useNavigate();
|
||||||
|
const { mutateAsync } = useCreateIntegration();
|
||||||
|
const { currentWorkspace } = useWorkspace();
|
||||||
|
|
||||||
|
const integrationAuthId = useSearch({
|
||||||
|
from: ROUTE_PATHS.SecretManager.Integratons.CloudflareWorkersConfigurePage.id,
|
||||||
|
select: (el) => el.integrationAuthId
|
||||||
|
});
|
||||||
|
const { data: integrationAuth } = useGetIntegrationAuthById((integrationAuthId as string) ?? "");
|
||||||
|
const { data: integrationAuthApps } = useGetIntegrationAuthApps({
|
||||||
|
integrationAuthId: (integrationAuthId as string) ?? ""
|
||||||
|
});
|
||||||
|
|
||||||
|
const [selectedSourceEnvironment, setSelectedSourceEnvironment] = useState(
|
||||||
|
currentWorkspace.environments[0].slug
|
||||||
|
);
|
||||||
|
const [secretPath, setSecretPath] = useState("/");
|
||||||
|
|
||||||
|
const [targetApp, setTargetApp] = useState("");
|
||||||
|
const [targetAppId, setTargetAppId] = useState("");
|
||||||
|
|
||||||
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (integrationAuthApps) {
|
||||||
|
if (integrationAuthApps.length > 0) {
|
||||||
|
setTargetApp(integrationAuthApps[0].name);
|
||||||
|
setTargetAppId(String(integrationAuthApps[0].appId));
|
||||||
|
} else {
|
||||||
|
setTargetApp("none");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, [integrationAuthApps]);
|
||||||
|
|
||||||
|
const handleButtonClick = async () => {
|
||||||
|
try {
|
||||||
|
if (!integrationAuth?.id) return;
|
||||||
|
|
||||||
|
setIsLoading(true);
|
||||||
|
|
||||||
|
await mutateAsync({
|
||||||
|
integrationAuthId: integrationAuth?.id,
|
||||||
|
isActive: true,
|
||||||
|
app: targetApp,
|
||||||
|
appId: targetAppId,
|
||||||
|
sourceEnvironment: selectedSourceEnvironment,
|
||||||
|
secretPath
|
||||||
|
});
|
||||||
|
|
||||||
|
setIsLoading(false);
|
||||||
|
|
||||||
|
navigate({
|
||||||
|
to: "/secret-manager/$projectId/integrations",
|
||||||
|
params: {
|
||||||
|
projectId: currentWorkspace.id
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err);
|
||||||
|
|
||||||
|
let errorMessage: string = "Something went wrong!";
|
||||||
|
if (axios.isAxiosError(err)) {
|
||||||
|
const { message } = err?.response?.data as { message: string };
|
||||||
|
errorMessage = message;
|
||||||
|
}
|
||||||
|
|
||||||
|
createNotification({
|
||||||
|
text: errorMessage,
|
||||||
|
type: "error"
|
||||||
|
});
|
||||||
|
setIsLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return integrationAuth && selectedSourceEnvironment && integrationAuthApps && targetApp ? (
|
||||||
|
<div className="flex h-full w-full items-center justify-center bg-gradient-to-tr from-mineshaft-900 to-bunker-900">
|
||||||
|
<Card className="max-w-lg rounded-md border border-mineshaft-600 p-0">
|
||||||
|
<CardTitle
|
||||||
|
className="px-6 text-left"
|
||||||
|
subTitle="Choose which environment in Infisical you want to sync with your Cloudflare Workers project."
|
||||||
|
>
|
||||||
|
Cloudflare Workers Integration
|
||||||
|
</CardTitle>
|
||||||
|
<FormControl label="Infisical Project Environment" className="mt-2 px-6">
|
||||||
|
<Select
|
||||||
|
value={selectedSourceEnvironment}
|
||||||
|
onValueChange={(val) => setSelectedSourceEnvironment(val)}
|
||||||
|
className="w-full border border-mineshaft-500"
|
||||||
|
>
|
||||||
|
{currentWorkspace?.environments.map((sourceEnvironment) => (
|
||||||
|
<SelectItem
|
||||||
|
value={sourceEnvironment.slug}
|
||||||
|
key={`source-environment-${sourceEnvironment.slug}`}
|
||||||
|
>
|
||||||
|
{sourceEnvironment.name}
|
||||||
|
</SelectItem>
|
||||||
|
))}
|
||||||
|
</Select>
|
||||||
|
</FormControl>
|
||||||
|
<FormControl label="Infisical Secret Path" className="mt-2 px-6">
|
||||||
|
<SecretPathInput
|
||||||
|
value={secretPath}
|
||||||
|
onChange={(value) => setSecretPath(value)}
|
||||||
|
environment={selectedSourceEnvironment}
|
||||||
|
placeholder="Provide a path, default is /"
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
<FormControl label="Cloudflare Workers Project" className="mt-4 px-6">
|
||||||
|
<Select
|
||||||
|
value={targetApp}
|
||||||
|
onValueChange={(val) => setTargetApp(val)}
|
||||||
|
className="w-full border border-mineshaft-500"
|
||||||
|
isDisabled={integrationAuthApps.length === 0}
|
||||||
|
>
|
||||||
|
{integrationAuthApps.length > 0 ? (
|
||||||
|
integrationAuthApps.map((integrationAuthApp) => (
|
||||||
|
<SelectItem
|
||||||
|
value={integrationAuthApp.name}
|
||||||
|
key={`target-app-${integrationAuthApp.name}`}
|
||||||
|
>
|
||||||
|
{integrationAuthApp.name}
|
||||||
|
</SelectItem>
|
||||||
|
))
|
||||||
|
) : (
|
||||||
|
<SelectItem value="none" key="target-app-none">
|
||||||
|
No apps found
|
||||||
|
</SelectItem>
|
||||||
|
)}
|
||||||
|
</Select>
|
||||||
|
</FormControl>
|
||||||
|
<Button
|
||||||
|
onClick={handleButtonClick}
|
||||||
|
color="mineshaft"
|
||||||
|
variant="outline_bg"
|
||||||
|
className="mb-6 ml-auto mr-6 mt-2"
|
||||||
|
isFullWidth={false}
|
||||||
|
isLoading={isLoading}
|
||||||
|
>
|
||||||
|
Create Integration
|
||||||
|
</Button>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div />
|
||||||
|
);
|
||||||
|
};
|
||||||
+16
@@ -0,0 +1,16 @@
|
|||||||
|
import { createFileRoute } from "@tanstack/react-router";
|
||||||
|
import { zodValidator } from "@tanstack/zod-adapter";
|
||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
import { CloudflareWorkersConfigurePage } from "./CloudflareWorkersConfigurePage";
|
||||||
|
|
||||||
|
const CloudflareWorkersConfigurePageQueryParamsSchema = z.object({
|
||||||
|
integrationAuthId: z.string()
|
||||||
|
});
|
||||||
|
|
||||||
|
export const Route = createFileRoute(
|
||||||
|
"/_authenticate/_inject-org-details/secret-manager/$projectId/_secret-manager-layout/integrations/cloudflare-workers/create"
|
||||||
|
)({
|
||||||
|
component: CloudflareWorkersConfigurePage,
|
||||||
|
validateSearch: zodValidator(CloudflareWorkersConfigurePageQueryParamsSchema)
|
||||||
|
});
|
||||||
+71
@@ -0,0 +1,71 @@
|
|||||||
|
import { useState } from "react";
|
||||||
|
import { useNavigate } from "@tanstack/react-router";
|
||||||
|
|
||||||
|
import { Button, Card, CardTitle, FormControl, Input } from "@app/components/v2";
|
||||||
|
import { useWorkspace } from "@app/context";
|
||||||
|
import { useSaveIntegrationAccessToken } from "@app/hooks/api";
|
||||||
|
|
||||||
|
export const CodefreshAuthorizePage = () => {
|
||||||
|
const navigate = useNavigate();
|
||||||
|
const { mutateAsync } = useSaveIntegrationAccessToken();
|
||||||
|
|
||||||
|
const [apiKey, setApiKey] = useState("");
|
||||||
|
const [apiKeyErrorText, setApiKeyErrorText] = useState("");
|
||||||
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
|
const { currentWorkspace } = useWorkspace();
|
||||||
|
|
||||||
|
const handleButtonClick = async () => {
|
||||||
|
try {
|
||||||
|
setApiKeyErrorText("");
|
||||||
|
if (apiKey.length === 0) {
|
||||||
|
setApiKeyErrorText("API Key cannot be blank");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setIsLoading(true);
|
||||||
|
|
||||||
|
const integrationAuth = await mutateAsync({
|
||||||
|
workspaceId: currentWorkspace.id,
|
||||||
|
integration: "codefresh",
|
||||||
|
accessToken: apiKey
|
||||||
|
});
|
||||||
|
|
||||||
|
setIsLoading(false);
|
||||||
|
|
||||||
|
navigate({
|
||||||
|
to: "/secret-manager/$projectId/integrations/codefresh/create",
|
||||||
|
params: {
|
||||||
|
projectId: currentWorkspace.id
|
||||||
|
},
|
||||||
|
search: {
|
||||||
|
integrationAuthId: integrationAuth.id
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
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">Codefresh Integration</CardTitle>
|
||||||
|
<FormControl
|
||||||
|
label="Codefresh API Key"
|
||||||
|
errorText={apiKeyErrorText}
|
||||||
|
isError={apiKeyErrorText !== ""}
|
||||||
|
>
|
||||||
|
<Input placeholder="" value={apiKey} onChange={(e) => setApiKey(e.target.value)} />
|
||||||
|
</FormControl>
|
||||||
|
<Button
|
||||||
|
onClick={handleButtonClick}
|
||||||
|
color="mineshaft"
|
||||||
|
className="mt-4"
|
||||||
|
isLoading={isLoading}
|
||||||
|
>
|
||||||
|
Connect to Codefresh
|
||||||
|
</Button>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
import { createFileRoute } from "@tanstack/react-router";
|
||||||
|
|
||||||
|
import { CodefreshAuthorizePage } from "./CodefreshAuthorizePage";
|
||||||
|
|
||||||
|
export const Route = createFileRoute(
|
||||||
|
"/_authenticate/_inject-org-details/secret-manager/$projectId/_secret-manager-layout/integrations/codefresh/authorize"
|
||||||
|
)({
|
||||||
|
component: CodefreshAuthorizePage
|
||||||
|
});
|
||||||
+147
@@ -0,0 +1,147 @@
|
|||||||
|
import { useEffect, useState } from "react";
|
||||||
|
import { useNavigate, useSearch } from "@tanstack/react-router";
|
||||||
|
|
||||||
|
import {
|
||||||
|
Button,
|
||||||
|
Card,
|
||||||
|
CardTitle,
|
||||||
|
FormControl,
|
||||||
|
Input,
|
||||||
|
Select,
|
||||||
|
SelectItem
|
||||||
|
} from "@app/components/v2";
|
||||||
|
import { ROUTE_PATHS } from "@app/const/routes";
|
||||||
|
import { useWorkspace } from "@app/context";
|
||||||
|
import { useCreateIntegration } from "@app/hooks/api";
|
||||||
|
import {
|
||||||
|
useGetIntegrationAuthApps,
|
||||||
|
useGetIntegrationAuthById
|
||||||
|
} from "@app/hooks/api/integrationAuth";
|
||||||
|
|
||||||
|
export const CodefreshConfigurePage = () => {
|
||||||
|
const navigate = useNavigate();
|
||||||
|
const { mutateAsync } = useCreateIntegration();
|
||||||
|
|
||||||
|
const { currentWorkspace } = useWorkspace();
|
||||||
|
const integrationAuthId = useSearch({
|
||||||
|
from: ROUTE_PATHS.SecretManager.Integratons.CodefreshConfigurePage.id,
|
||||||
|
select: (el) => el.integrationAuthId
|
||||||
|
});
|
||||||
|
|
||||||
|
const { data: integrationAuth } = useGetIntegrationAuthById((integrationAuthId as string) ?? "");
|
||||||
|
const { data: integrationAuthApps } = useGetIntegrationAuthApps({
|
||||||
|
integrationAuthId: (integrationAuthId as string) ?? ""
|
||||||
|
});
|
||||||
|
|
||||||
|
const [selectedSourceEnvironment, setSelectedSourceEnvironment] = useState(
|
||||||
|
currentWorkspace.environments[0].slug
|
||||||
|
);
|
||||||
|
const [targetApp, setTargetApp] = useState("");
|
||||||
|
const [secretPath, setSecretPath] = useState("/");
|
||||||
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (integrationAuthApps) {
|
||||||
|
if (integrationAuthApps.length > 0) {
|
||||||
|
setTargetApp(integrationAuthApps[0].name);
|
||||||
|
} else {
|
||||||
|
setTargetApp("none");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, [integrationAuthApps]);
|
||||||
|
|
||||||
|
const handleButtonClick = async () => {
|
||||||
|
try {
|
||||||
|
if (!integrationAuth?.id) return;
|
||||||
|
|
||||||
|
setIsLoading(true);
|
||||||
|
|
||||||
|
await mutateAsync({
|
||||||
|
integrationAuthId: integrationAuth?.id,
|
||||||
|
isActive: true,
|
||||||
|
app: targetApp,
|
||||||
|
appId: integrationAuthApps?.find(
|
||||||
|
(integrationAuthApp) => integrationAuthApp.name === targetApp
|
||||||
|
)?.appId,
|
||||||
|
sourceEnvironment: selectedSourceEnvironment,
|
||||||
|
secretPath
|
||||||
|
});
|
||||||
|
|
||||||
|
setIsLoading(false);
|
||||||
|
|
||||||
|
navigate({
|
||||||
|
to: "/secret-manager/$projectId/integrations",
|
||||||
|
params: {
|
||||||
|
projectId: currentWorkspace.id
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return integrationAuth && selectedSourceEnvironment && 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">Codefresh Integration</CardTitle>
|
||||||
|
<FormControl label="Project Environment" className="mt-4">
|
||||||
|
<Select
|
||||||
|
value={selectedSourceEnvironment}
|
||||||
|
onValueChange={(val) => setSelectedSourceEnvironment(val)}
|
||||||
|
className="w-full border border-mineshaft-500"
|
||||||
|
>
|
||||||
|
{currentWorkspace?.environments.map((sourceEnvironment) => (
|
||||||
|
<SelectItem
|
||||||
|
value={sourceEnvironment.slug}
|
||||||
|
key={`source-environment-${sourceEnvironment.slug}`}
|
||||||
|
>
|
||||||
|
{sourceEnvironment.name}
|
||||||
|
</SelectItem>
|
||||||
|
))}
|
||||||
|
</Select>
|
||||||
|
</FormControl>
|
||||||
|
<FormControl label="Secrets Path">
|
||||||
|
<Input
|
||||||
|
value={secretPath}
|
||||||
|
onChange={(evt) => setSecretPath(evt.target.value)}
|
||||||
|
placeholder="Provide a path, default is /"
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
<FormControl label="Codefresh Service" className="mt-4">
|
||||||
|
<Select
|
||||||
|
value={targetApp}
|
||||||
|
onValueChange={(val) => setTargetApp(val)}
|
||||||
|
className="w-full border border-mineshaft-500"
|
||||||
|
isDisabled={integrationAuthApps.length === 0}
|
||||||
|
>
|
||||||
|
{integrationAuthApps.length > 0 ? (
|
||||||
|
integrationAuthApps.map((integrationAuthApp) => (
|
||||||
|
<SelectItem
|
||||||
|
value={integrationAuthApp.name}
|
||||||
|
key={`target-app-${integrationAuthApp.name}`}
|
||||||
|
>
|
||||||
|
{integrationAuthApp.name}
|
||||||
|
</SelectItem>
|
||||||
|
))
|
||||||
|
) : (
|
||||||
|
<SelectItem value="none" key="target-app-none">
|
||||||
|
No services found
|
||||||
|
</SelectItem>
|
||||||
|
)}
|
||||||
|
</Select>
|
||||||
|
</FormControl>
|
||||||
|
<Button
|
||||||
|
onClick={handleButtonClick}
|
||||||
|
color="mineshaft"
|
||||||
|
className="mt-4"
|
||||||
|
isLoading={isLoading}
|
||||||
|
isDisabled={integrationAuthApps.length === 0}
|
||||||
|
>
|
||||||
|
Create Integration
|
||||||
|
</Button>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div />
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
import { createFileRoute } from "@tanstack/react-router";
|
||||||
|
import { zodValidator } from "@tanstack/zod-adapter";
|
||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
import { CodefreshConfigurePage } from "./CodefreshConfigurePage";
|
||||||
|
|
||||||
|
const CodefreshConfigurePageQueryParamsSchema = z.object({
|
||||||
|
integrationAuthId: z.string()
|
||||||
|
});
|
||||||
|
|
||||||
|
export const Route = createFileRoute(
|
||||||
|
"/_authenticate/_inject-org-details/secret-manager/$projectId/_secret-manager-layout/integrations/codefresh/create"
|
||||||
|
)({
|
||||||
|
component: CodefreshConfigurePage,
|
||||||
|
validateSearch: zodValidator(CodefreshConfigurePageQueryParamsSchema)
|
||||||
|
});
|
||||||
+130
@@ -0,0 +1,130 @@
|
|||||||
|
import { useState } from "react";
|
||||||
|
import { Helmet } from "react-helmet";
|
||||||
|
import { faArrowUpRightFromSquare, faBookOpen } from "@fortawesome/free-solid-svg-icons";
|
||||||
|
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||||
|
import { useNavigate } from "@tanstack/react-router";
|
||||||
|
|
||||||
|
import { Button, Card, CardTitle, FormControl, Input } from "@app/components/v2";
|
||||||
|
import { useWorkspace } from "@app/context";
|
||||||
|
import { useSaveIntegrationAccessToken } from "@app/hooks/api";
|
||||||
|
|
||||||
|
export const DatabricksAuthorizePage = () => {
|
||||||
|
const navigate = useNavigate();
|
||||||
|
const { mutateAsync, isPending } = useSaveIntegrationAccessToken();
|
||||||
|
|
||||||
|
const [apiKey, setApiKey] = useState("");
|
||||||
|
const [instanceURL, setInstanceURL] = useState("");
|
||||||
|
const [apiKeyErrorText, setApiKeyErrorText] = useState("");
|
||||||
|
const [instanceURLErrorText, setInstanceURLErrorText] = useState("");
|
||||||
|
|
||||||
|
const { currentWorkspace } = useWorkspace();
|
||||||
|
|
||||||
|
const handleButtonClick = async () => {
|
||||||
|
try {
|
||||||
|
setApiKeyErrorText("");
|
||||||
|
setInstanceURLErrorText("");
|
||||||
|
if (apiKey.length === 0) {
|
||||||
|
setApiKeyErrorText("API Key cannot be blank");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (instanceURL.length === 0) {
|
||||||
|
setInstanceURLErrorText("Instance URL cannot be blank");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const integrationAuth = await mutateAsync({
|
||||||
|
workspaceId: currentWorkspace.id,
|
||||||
|
integration: "databricks",
|
||||||
|
url: instanceURL.replace(/\/$/, ""),
|
||||||
|
accessToken: apiKey
|
||||||
|
});
|
||||||
|
|
||||||
|
navigate({
|
||||||
|
to: "/secret-manager/$projectId/integrations/databricks/create",
|
||||||
|
params: {
|
||||||
|
projectId: currentWorkspace.id
|
||||||
|
},
|
||||||
|
search: {
|
||||||
|
integrationAuthId: integrationAuth.id
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex h-full w-full items-center justify-center">
|
||||||
|
<Helmet>
|
||||||
|
<title>Authorize Databricks Integration</title>
|
||||||
|
</Helmet>
|
||||||
|
<Card className="mb-12 max-w-lg rounded-md border border-mineshaft-600">
|
||||||
|
<CardTitle
|
||||||
|
className="px-6 text-left text-xl"
|
||||||
|
subTitle="After adding your Access Token, 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="flex items-center pb-0.5">
|
||||||
|
<img
|
||||||
|
src="/images/integrations/Databricks.png"
|
||||||
|
height={30}
|
||||||
|
width={30}
|
||||||
|
alt="Databricks logo"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<span className="ml-1.5">Databricks Integration </span>
|
||||||
|
<a
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
href="https://infisical.com/docs/integrations/cloud/databricks"
|
||||||
|
>
|
||||||
|
<div className="mb-1 ml-2 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="mb-[0.07rem] ml-1.5 text-xxs"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</CardTitle>
|
||||||
|
<FormControl
|
||||||
|
label="Databricks Instance URL"
|
||||||
|
errorText={instanceURLErrorText}
|
||||||
|
isError={instanceURLErrorText !== ""}
|
||||||
|
className="px-6"
|
||||||
|
>
|
||||||
|
<Input
|
||||||
|
value={instanceURL}
|
||||||
|
onChange={(e) => setInstanceURL(e.target.value)}
|
||||||
|
placeholder="https://xxxx.cloud.databricks.com"
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
<FormControl
|
||||||
|
label="Access Token"
|
||||||
|
errorText={apiKeyErrorText}
|
||||||
|
isError={apiKeyErrorText !== ""}
|
||||||
|
className="px-6"
|
||||||
|
>
|
||||||
|
<Input
|
||||||
|
placeholder=""
|
||||||
|
value={apiKey}
|
||||||
|
onChange={(e) => setApiKey(e.target.value)}
|
||||||
|
type="password"
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
<Button
|
||||||
|
onClick={handleButtonClick}
|
||||||
|
colorSchema="primary"
|
||||||
|
variant="outline_bg"
|
||||||
|
className="mb-6 ml-auto mr-6 mt-2 w-min"
|
||||||
|
isLoading={isPending}
|
||||||
|
isDisabled={isPending}
|
||||||
|
>
|
||||||
|
Connect to Databricks
|
||||||
|
</Button>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
import { createFileRoute } from "@tanstack/react-router";
|
||||||
|
|
||||||
|
import { DatabricksAuthorizePage } from "./DatabricksAuthorizePage";
|
||||||
|
|
||||||
|
export const Route = createFileRoute(
|
||||||
|
"/_authenticate/_inject-org-details/secret-manager/$projectId/_secret-manager-layout/integrations/databricks/authorize"
|
||||||
|
)({
|
||||||
|
component: DatabricksAuthorizePage
|
||||||
|
});
|
||||||
+241
@@ -0,0 +1,241 @@
|
|||||||
|
import { useState } from "react";
|
||||||
|
import { Helmet } from "react-helmet";
|
||||||
|
import {
|
||||||
|
faArrowUpRightFromSquare,
|
||||||
|
faBookOpen,
|
||||||
|
faBugs,
|
||||||
|
faCircleInfo
|
||||||
|
} from "@fortawesome/free-solid-svg-icons";
|
||||||
|
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||||
|
import { useNavigate, useSearch } from "@tanstack/react-router";
|
||||||
|
|
||||||
|
import { createNotification } from "@app/components/notifications";
|
||||||
|
import {
|
||||||
|
Button,
|
||||||
|
Card,
|
||||||
|
CardTitle,
|
||||||
|
FormControl,
|
||||||
|
Input,
|
||||||
|
Select,
|
||||||
|
SelectItem
|
||||||
|
} from "@app/components/v2";
|
||||||
|
import { ROUTE_PATHS } from "@app/const/routes";
|
||||||
|
import { useWorkspace } from "@app/context";
|
||||||
|
import { useCreateIntegration } from "@app/hooks/api";
|
||||||
|
import {
|
||||||
|
useGetIntegrationAuthApps,
|
||||||
|
useGetIntegrationAuthById
|
||||||
|
} from "@app/hooks/api/integrationAuth";
|
||||||
|
|
||||||
|
export const DatabricksConfigurePage = () => {
|
||||||
|
const navigate = useNavigate();
|
||||||
|
const { mutateAsync, isPending } = useCreateIntegration();
|
||||||
|
|
||||||
|
const { currentWorkspace } = useWorkspace();
|
||||||
|
|
||||||
|
const integrationAuthId = useSearch({
|
||||||
|
from: ROUTE_PATHS.SecretManager.Integratons.DatabricksConfigurePage.id,
|
||||||
|
select: (el) => el.integrationAuthId
|
||||||
|
});
|
||||||
|
|
||||||
|
const { data: integrationAuth, isLoading: isintegrationAuthLoading } = useGetIntegrationAuthById(
|
||||||
|
(integrationAuthId as string) ?? ""
|
||||||
|
);
|
||||||
|
|
||||||
|
const { data: integrationAuthScopes, isLoading: isIntegrationAuthScopesLoading } =
|
||||||
|
useGetIntegrationAuthApps({
|
||||||
|
integrationAuthId: (integrationAuthId as string) ?? ""
|
||||||
|
});
|
||||||
|
|
||||||
|
const [selectedSourceEnvironment, setSelectedSourceEnvironment] = useState(
|
||||||
|
currentWorkspace.environments[0].slug
|
||||||
|
);
|
||||||
|
const [targetScope, setTargetScope] = useState("");
|
||||||
|
const [secretPath, setSecretPath] = useState("/");
|
||||||
|
|
||||||
|
const handleButtonClick = async () => {
|
||||||
|
try {
|
||||||
|
if (!integrationAuth?.id) return;
|
||||||
|
|
||||||
|
if (!targetScope) {
|
||||||
|
createNotification({
|
||||||
|
type: "error",
|
||||||
|
text: "Please select a scope"
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const selectedScope = integrationAuthScopes?.find(
|
||||||
|
(integrationAuthScope) => integrationAuthScope.name === targetScope
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!selectedScope) {
|
||||||
|
createNotification({
|
||||||
|
type: "error",
|
||||||
|
text: "Invalid scope selected"
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await mutateAsync({
|
||||||
|
integrationAuthId: integrationAuth?.id,
|
||||||
|
isActive: true,
|
||||||
|
app: selectedScope.name, // scope name
|
||||||
|
sourceEnvironment: selectedSourceEnvironment,
|
||||||
|
secretPath
|
||||||
|
});
|
||||||
|
|
||||||
|
navigate({
|
||||||
|
to: "/secret-manager/$projectId/integrations",
|
||||||
|
params: {
|
||||||
|
projectId: currentWorkspace.id
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return integrationAuth && selectedSourceEnvironment && integrationAuthScopes ? (
|
||||||
|
<div className="flex h-full w-full flex-col items-center justify-center">
|
||||||
|
<Helmet>
|
||||||
|
<title>Set Up Databricks Integration</title>
|
||||||
|
</Helmet>
|
||||||
|
<Card className="max-w-lg rounded-md border border-mineshaft-600">
|
||||||
|
<CardTitle
|
||||||
|
className="px-6 text-left text-xl"
|
||||||
|
subTitle="Choose which environment or folder in Infisical you want to sync to which Databricks secrets scope."
|
||||||
|
>
|
||||||
|
<div className="flex flex-row items-center">
|
||||||
|
<div className="flex items-center pb-0.5">
|
||||||
|
<img
|
||||||
|
src="/images/integrations/Databricks.png"
|
||||||
|
height={30}
|
||||||
|
width={30}
|
||||||
|
alt="Databricks logo"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<span className="ml-1.5">Databricks Integration </span>
|
||||||
|
<a
|
||||||
|
href="https://infisical.com/docs/integrations/cloud/databricks"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
>
|
||||||
|
<div className="mb-1 ml-2 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="mb-[0.07rem] ml-1.5 text-xxs"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</CardTitle>
|
||||||
|
|
||||||
|
<FormControl label="Project Environment" className="px-6">
|
||||||
|
<Select
|
||||||
|
value={selectedSourceEnvironment}
|
||||||
|
onValueChange={(val) => setSelectedSourceEnvironment(val)}
|
||||||
|
className="w-full border border-mineshaft-500"
|
||||||
|
>
|
||||||
|
{currentWorkspace?.environments.map((sourceEnvironment) => (
|
||||||
|
<SelectItem
|
||||||
|
value={sourceEnvironment.slug}
|
||||||
|
key={`source-environment-${sourceEnvironment.slug}`}
|
||||||
|
>
|
||||||
|
{sourceEnvironment.name}
|
||||||
|
</SelectItem>
|
||||||
|
))}
|
||||||
|
</Select>
|
||||||
|
</FormControl>
|
||||||
|
<FormControl label="Secrets Path" className="px-6">
|
||||||
|
<Input
|
||||||
|
value={secretPath}
|
||||||
|
onChange={(evt) => setSecretPath(evt.target.value)}
|
||||||
|
placeholder="Provide a path, default is /"
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
|
||||||
|
{integrationAuthScopes && (
|
||||||
|
<FormControl label="Databricks Scope" className="px-6">
|
||||||
|
<Select
|
||||||
|
value={targetScope}
|
||||||
|
onValueChange={(val) => {
|
||||||
|
setTargetScope(val);
|
||||||
|
}}
|
||||||
|
className="w-full border border-mineshaft-500"
|
||||||
|
placeholder={
|
||||||
|
integrationAuthScopes.length === 0 ? "No scopes found." : "Select scope..."
|
||||||
|
}
|
||||||
|
isDisabled={integrationAuthScopes.length === 0}
|
||||||
|
>
|
||||||
|
{integrationAuthScopes.length > 0 ? (
|
||||||
|
integrationAuthScopes.map((scope) => (
|
||||||
|
<SelectItem value={scope.name!} key={`target-scope-${scope.name}`}>
|
||||||
|
{scope.name}
|
||||||
|
</SelectItem>
|
||||||
|
))
|
||||||
|
) : (
|
||||||
|
<SelectItem value="none" key="target-app-none">
|
||||||
|
No scopes found
|
||||||
|
</SelectItem>
|
||||||
|
)}
|
||||||
|
</Select>
|
||||||
|
</FormControl>
|
||||||
|
)}
|
||||||
|
<Button
|
||||||
|
onClick={handleButtonClick}
|
||||||
|
colorSchema="primary"
|
||||||
|
variant="outline_bg"
|
||||||
|
className="mb-6 ml-auto mr-6 mt-2 w-min"
|
||||||
|
isLoading={isPending}
|
||||||
|
isDisabled={integrationAuthScopes.length === 0 || isPending}
|
||||||
|
>
|
||||||
|
Create Integration
|
||||||
|
</Button>
|
||||||
|
</Card>
|
||||||
|
<div className="mt-6 w-full max-w-md border-t border-mineshaft-800" />
|
||||||
|
<div className="mt-6 flex w-full max-w-lg flex-col rounded-md border border-mineshaft-600 bg-mineshaft-800 p-4">
|
||||||
|
<div className="flex flex-row items-center">
|
||||||
|
<FontAwesomeIcon icon={faCircleInfo} className="text-xl text-mineshaft-200" />{" "}
|
||||||
|
<span className="text-md ml-3 text-mineshaft-100">Pro Tip</span>
|
||||||
|
</div>
|
||||||
|
<span className="mt-4 text-sm text-mineshaft-300">
|
||||||
|
After creating an integration, your secrets will start syncing immediately. This might
|
||||||
|
cause an unexpected override of current secrets in Databricks with secrets from Infisical.
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="flex h-full w-full items-center justify-center">
|
||||||
|
<Helmet>
|
||||||
|
<title>Set Up Databricks Integration</title>
|
||||||
|
</Helmet>
|
||||||
|
{isIntegrationAuthScopesLoading || isintegrationAuthLoading ? (
|
||||||
|
<img
|
||||||
|
src="/images/loading/loading.gif"
|
||||||
|
height={70}
|
||||||
|
width={120}
|
||||||
|
alt="infisical loading indicator"
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<div className="flex h-max max-w-md flex-col rounded-md border border-mineshaft-600 bg-mineshaft-800 p-6 text-center text-mineshaft-200">
|
||||||
|
<FontAwesomeIcon icon={faBugs} className="inlineli my-2 text-6xl" />
|
||||||
|
<p>
|
||||||
|
Something went wrong. Please contact{" "}
|
||||||
|
<a
|
||||||
|
className="inline cursor-pointer text-mineshaft-100 underline decoration-primary-500 underline-offset-4 opacity-80 duration-200 hover:opacity-100"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
href="mailto:[email protected]"
|
||||||
|
>
|
||||||
|
support@infisical.com
|
||||||
|
</a>{" "}
|
||||||
|
if the issue persists.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
import { createFileRoute } from "@tanstack/react-router";
|
||||||
|
import { zodValidator } from "@tanstack/zod-adapter";
|
||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
import { DatabricksConfigurePage } from "./DatabricksConfigurePage";
|
||||||
|
|
||||||
|
const DatabricksConfigurePageQueryParamsSchema = z.object({
|
||||||
|
integrationAuthId: z.string()
|
||||||
|
});
|
||||||
|
|
||||||
|
export const Route = createFileRoute(
|
||||||
|
"/_authenticate/_inject-org-details/secret-manager/$projectId/_secret-manager-layout/integrations/databricks/create"
|
||||||
|
)({
|
||||||
|
component: DatabricksConfigurePage,
|
||||||
|
validateSearch: zodValidator(DatabricksConfigurePageQueryParamsSchema)
|
||||||
|
});
|
||||||
+71
@@ -0,0 +1,71 @@
|
|||||||
|
import { useState } from "react";
|
||||||
|
import { useNavigate } from "@tanstack/react-router";
|
||||||
|
|
||||||
|
import { Button, Card, CardTitle, FormControl, Input } from "@app/components/v2";
|
||||||
|
import { useWorkspace } from "@app/context";
|
||||||
|
import { useSaveIntegrationAccessToken } from "@app/hooks/api";
|
||||||
|
|
||||||
|
export const DigitalOceanAppPlatformAuthorizePage = () => {
|
||||||
|
const navigate = useNavigate();
|
||||||
|
const { mutateAsync } = useSaveIntegrationAccessToken();
|
||||||
|
|
||||||
|
const [apiKey, setApiKey] = useState("");
|
||||||
|
const [apiKeyErrorText, setApiKeyErrorText] = useState("");
|
||||||
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
|
const { currentWorkspace } = useWorkspace();
|
||||||
|
|
||||||
|
const handleButtonClick = async () => {
|
||||||
|
try {
|
||||||
|
setApiKeyErrorText("");
|
||||||
|
if (apiKey.length === 0) {
|
||||||
|
setApiKeyErrorText("API Key cannot be blank");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setIsLoading(true);
|
||||||
|
|
||||||
|
const integrationAuth = await mutateAsync({
|
||||||
|
workspaceId: currentWorkspace.id,
|
||||||
|
integration: "digital-ocean-app-platform",
|
||||||
|
accessToken: apiKey
|
||||||
|
});
|
||||||
|
|
||||||
|
setIsLoading(false);
|
||||||
|
|
||||||
|
navigate({
|
||||||
|
to: "/secret-manager/$projectId/integrations/digital-ocean-app-platform/create",
|
||||||
|
params: {
|
||||||
|
projectId: currentWorkspace.id
|
||||||
|
},
|
||||||
|
search: {
|
||||||
|
integrationAuthId: integrationAuth.id
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
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">Digital Ocean App Platform Integration</CardTitle>
|
||||||
|
<FormControl
|
||||||
|
label="Digital Ocean API Key"
|
||||||
|
errorText={apiKeyErrorText}
|
||||||
|
isError={apiKeyErrorText !== ""}
|
||||||
|
>
|
||||||
|
<Input placeholder="" value={apiKey} onChange={(e) => setApiKey(e.target.value)} />
|
||||||
|
</FormControl>
|
||||||
|
<Button
|
||||||
|
onClick={handleButtonClick}
|
||||||
|
color="mineshaft"
|
||||||
|
className="mt-4"
|
||||||
|
isLoading={isLoading}
|
||||||
|
>
|
||||||
|
Connect to Digital Ocean App Platform
|
||||||
|
</Button>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
+9
@@ -0,0 +1,9 @@
|
|||||||
|
import { createFileRoute } from "@tanstack/react-router";
|
||||||
|
|
||||||
|
import { DigitalOceanAppPlatformAuthorizePage } from "./DigitalOceanAppPlatformAuthorizePage";
|
||||||
|
|
||||||
|
export const Route = createFileRoute(
|
||||||
|
"/_authenticate/_inject-org-details/secret-manager/$projectId/_secret-manager-layout/integrations/digital-ocean-app-platform/authorize"
|
||||||
|
)({
|
||||||
|
component: DigitalOceanAppPlatformAuthorizePage
|
||||||
|
});
|
||||||
+148
@@ -0,0 +1,148 @@
|
|||||||
|
import { useEffect, useState } from "react";
|
||||||
|
import { useNavigate, useSearch } from "@tanstack/react-router";
|
||||||
|
|
||||||
|
import {
|
||||||
|
Button,
|
||||||
|
Card,
|
||||||
|
CardTitle,
|
||||||
|
FormControl,
|
||||||
|
Input,
|
||||||
|
Select,
|
||||||
|
SelectItem
|
||||||
|
} from "@app/components/v2";
|
||||||
|
import { ROUTE_PATHS } from "@app/const/routes";
|
||||||
|
import { useWorkspace } from "@app/context";
|
||||||
|
import { useCreateIntegration } from "@app/hooks/api";
|
||||||
|
import {
|
||||||
|
useGetIntegrationAuthApps,
|
||||||
|
useGetIntegrationAuthById
|
||||||
|
} from "@app/hooks/api/integrationAuth";
|
||||||
|
|
||||||
|
export const DigitalOceanAppPlatformConfigurePage = () => {
|
||||||
|
const navigate = useNavigate();
|
||||||
|
const { mutateAsync } = useCreateIntegration();
|
||||||
|
|
||||||
|
const { currentWorkspace } = useWorkspace();
|
||||||
|
|
||||||
|
const integrationAuthId = useSearch({
|
||||||
|
from: ROUTE_PATHS.SecretManager.Integratons.DigitalOceanAppPlatformConfigurePage.id,
|
||||||
|
select: (el) => el.integrationAuthId
|
||||||
|
});
|
||||||
|
|
||||||
|
const { data: integrationAuth } = useGetIntegrationAuthById((integrationAuthId as string) ?? "");
|
||||||
|
const { data: integrationAuthApps } = useGetIntegrationAuthApps({
|
||||||
|
integrationAuthId: (integrationAuthId as string) ?? ""
|
||||||
|
});
|
||||||
|
|
||||||
|
const [selectedSourceEnvironment, setSelectedSourceEnvironment] = useState(
|
||||||
|
currentWorkspace.environments[0].slug
|
||||||
|
);
|
||||||
|
const [targetApp, setTargetApp] = useState("");
|
||||||
|
const [secretPath, setSecretPath] = useState("/");
|
||||||
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (integrationAuthApps) {
|
||||||
|
if (integrationAuthApps.length > 0) {
|
||||||
|
setTargetApp(integrationAuthApps[0].name);
|
||||||
|
} else {
|
||||||
|
setTargetApp("none");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, [integrationAuthApps]);
|
||||||
|
|
||||||
|
const handleButtonClick = async () => {
|
||||||
|
try {
|
||||||
|
if (!integrationAuth?.id) return;
|
||||||
|
|
||||||
|
setIsLoading(true);
|
||||||
|
|
||||||
|
await mutateAsync({
|
||||||
|
integrationAuthId: integrationAuth?.id,
|
||||||
|
isActive: true,
|
||||||
|
app: targetApp,
|
||||||
|
appId: integrationAuthApps?.find(
|
||||||
|
(integrationAuthApp) => integrationAuthApp.name === targetApp
|
||||||
|
)?.appId,
|
||||||
|
sourceEnvironment: selectedSourceEnvironment,
|
||||||
|
secretPath
|
||||||
|
});
|
||||||
|
|
||||||
|
setIsLoading(false);
|
||||||
|
|
||||||
|
navigate({
|
||||||
|
to: "/secret-manager/$projectId/integrations",
|
||||||
|
params: {
|
||||||
|
projectId: currentWorkspace.id
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return integrationAuth && selectedSourceEnvironment && 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">Digital Ocean App Platform Integration</CardTitle>
|
||||||
|
<FormControl label="Project Environment" className="mt-4">
|
||||||
|
<Select
|
||||||
|
value={selectedSourceEnvironment}
|
||||||
|
onValueChange={(val) => setSelectedSourceEnvironment(val)}
|
||||||
|
className="w-full border border-mineshaft-500"
|
||||||
|
>
|
||||||
|
{currentWorkspace?.environments.map((sourceEnvironment) => (
|
||||||
|
<SelectItem
|
||||||
|
value={sourceEnvironment.slug}
|
||||||
|
key={`source-environment-${sourceEnvironment.slug}`}
|
||||||
|
>
|
||||||
|
{sourceEnvironment.name}
|
||||||
|
</SelectItem>
|
||||||
|
))}
|
||||||
|
</Select>
|
||||||
|
</FormControl>
|
||||||
|
<FormControl label="Secrets Path">
|
||||||
|
<Input
|
||||||
|
value={secretPath}
|
||||||
|
onChange={(evt) => setSecretPath(evt.target.value)}
|
||||||
|
placeholder="Provide a path, default is /"
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
<FormControl label="Digital Ocean App Platform Service" className="mt-4">
|
||||||
|
<Select
|
||||||
|
value={targetApp}
|
||||||
|
onValueChange={(val) => setTargetApp(val)}
|
||||||
|
className="w-full border border-mineshaft-500"
|
||||||
|
isDisabled={integrationAuthApps.length === 0}
|
||||||
|
>
|
||||||
|
{integrationAuthApps.length > 0 ? (
|
||||||
|
integrationAuthApps.map((integrationAuthApp) => (
|
||||||
|
<SelectItem
|
||||||
|
value={integrationAuthApp.name}
|
||||||
|
key={`target-app-${integrationAuthApp.name}`}
|
||||||
|
>
|
||||||
|
{integrationAuthApp.name}
|
||||||
|
</SelectItem>
|
||||||
|
))
|
||||||
|
) : (
|
||||||
|
<SelectItem value="none" key="target-app-none">
|
||||||
|
No services found
|
||||||
|
</SelectItem>
|
||||||
|
)}
|
||||||
|
</Select>
|
||||||
|
</FormControl>
|
||||||
|
<Button
|
||||||
|
onClick={handleButtonClick}
|
||||||
|
color="mineshaft"
|
||||||
|
className="mt-4"
|
||||||
|
isLoading={isLoading}
|
||||||
|
isDisabled={integrationAuthApps.length === 0}
|
||||||
|
>
|
||||||
|
Create Integration
|
||||||
|
</Button>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div />
|
||||||
|
);
|
||||||
|
};
|
||||||
+16
@@ -0,0 +1,16 @@
|
|||||||
|
import { createFileRoute } from "@tanstack/react-router";
|
||||||
|
import { zodValidator } from "@tanstack/zod-adapter";
|
||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
import { DigitalOceanAppPlatformConfigurePage } from "./DigitalOceanAppPlatformConfigurePage";
|
||||||
|
|
||||||
|
const DigitalOceanAppPlatformConfigurePageQueryParamsSchema = z.object({
|
||||||
|
integrationAuthId: z.string()
|
||||||
|
});
|
||||||
|
|
||||||
|
export const Route = createFileRoute(
|
||||||
|
"/_authenticate/_inject-org-details/secret-manager/$projectId/_secret-manager-layout/integrations/digital-ocean-app-platform/create"
|
||||||
|
)({
|
||||||
|
component: DigitalOceanAppPlatformConfigurePage,
|
||||||
|
validateSearch: zodValidator(DigitalOceanAppPlatformConfigurePageQueryParamsSchema)
|
||||||
|
});
|
||||||
+120
@@ -0,0 +1,120 @@
|
|||||||
|
import { useState } from "react";
|
||||||
|
import { Helmet } from "react-helmet";
|
||||||
|
import { Controller, useForm } from "react-hook-form";
|
||||||
|
import { faArrowUpRightFromSquare, faBookOpen } from "@fortawesome/free-solid-svg-icons";
|
||||||
|
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||||
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
|
import { useNavigate } from "@tanstack/react-router";
|
||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
import { Button, Card, CardTitle, FormControl, Input } from "@app/components/v2";
|
||||||
|
import { useWorkspace } from "@app/context";
|
||||||
|
import { useSaveIntegrationAccessToken } from "@app/hooks/api";
|
||||||
|
|
||||||
|
const schema = z.object({
|
||||||
|
accessToken: z.string().trim()
|
||||||
|
});
|
||||||
|
|
||||||
|
type FormData = z.infer<typeof schema>;
|
||||||
|
|
||||||
|
export const FlyioAuthorizePage = () => {
|
||||||
|
const navigate = useNavigate();
|
||||||
|
const { currentWorkspace } = useWorkspace();
|
||||||
|
|
||||||
|
const { control, handleSubmit } = useForm<FormData>({
|
||||||
|
resolver: zodResolver(schema),
|
||||||
|
defaultValues: {
|
||||||
|
accessToken: ""
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const { mutateAsync } = useSaveIntegrationAccessToken();
|
||||||
|
|
||||||
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
|
|
||||||
|
const onFormSubmit = async ({ accessToken }: FormData) => {
|
||||||
|
try {
|
||||||
|
setIsLoading(true);
|
||||||
|
|
||||||
|
const integrationAuth = await mutateAsync({
|
||||||
|
workspaceId: currentWorkspace.id,
|
||||||
|
integration: "flyio",
|
||||||
|
accessToken
|
||||||
|
});
|
||||||
|
|
||||||
|
setIsLoading(false);
|
||||||
|
navigate({
|
||||||
|
to: "/secret-manager/$projectId/integrations/flyio/create",
|
||||||
|
params: {
|
||||||
|
projectId: currentWorkspace.id
|
||||||
|
},
|
||||||
|
search: {
|
||||||
|
integrationAuthId: integrationAuth.id
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
setIsLoading(false);
|
||||||
|
console.error(err);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex h-full w-full items-center justify-center">
|
||||||
|
<Helmet>
|
||||||
|
<title>Authorize Fly.io Integration</title>
|
||||||
|
</Helmet>
|
||||||
|
<Card className="mb-12 max-w-lg rounded-md border border-mineshaft-600">
|
||||||
|
<CardTitle
|
||||||
|
className="px-6 text-left text-xl"
|
||||||
|
subTitle="After adding your access token, 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 pb-0.5">
|
||||||
|
<img src="/images/integrations/Flyio.svg" height={30} width={30} alt="Fly.io logo" />
|
||||||
|
</div>
|
||||||
|
<span className="ml-2.5">Fly.io Integration </span>
|
||||||
|
<a
|
||||||
|
href="https://infisical.com/docs/integrations/cloud/flyio"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
>
|
||||||
|
<div className="mb-1 ml-2 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="mb-[0.07rem] ml-1.5 text-xxs"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</CardTitle>
|
||||||
|
<form onSubmit={handleSubmit(onFormSubmit)} className="px-6 pb-8 text-right">
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
|
name="accessToken"
|
||||||
|
render={({ field, fieldState: { error } }) => (
|
||||||
|
<FormControl
|
||||||
|
label="Fly.io Access Token"
|
||||||
|
errorText={error?.message}
|
||||||
|
isError={Boolean(error)}
|
||||||
|
>
|
||||||
|
<Input {...field} placeholder="" type="password" autoComplete="new-password" />
|
||||||
|
</FormControl>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<Button
|
||||||
|
colorSchema="primary"
|
||||||
|
variant="outline_bg"
|
||||||
|
className="mt-2 w-min"
|
||||||
|
size="sm"
|
||||||
|
type="submit"
|
||||||
|
isLoading={isLoading}
|
||||||
|
>
|
||||||
|
Connect to Fly.io
|
||||||
|
</Button>
|
||||||
|
</form>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
import { createFileRoute } from "@tanstack/react-router";
|
||||||
|
|
||||||
|
import { FlyioAuthorizePage } from "./FlyioAuthorizePage";
|
||||||
|
|
||||||
|
export const Route = createFileRoute(
|
||||||
|
"/_authenticate/_inject-org-details/secret-manager/$projectId/_secret-manager-layout/integrations/flyio/authorize"
|
||||||
|
)({
|
||||||
|
component: FlyioAuthorizePage
|
||||||
|
});
|
||||||
+237
@@ -0,0 +1,237 @@
|
|||||||
|
import { useEffect, useState } from "react";
|
||||||
|
import { Helmet } from "react-helmet";
|
||||||
|
import {
|
||||||
|
faArrowUpRightFromSquare,
|
||||||
|
faBookOpen,
|
||||||
|
faBugs,
|
||||||
|
faCircleInfo
|
||||||
|
} from "@fortawesome/free-solid-svg-icons";
|
||||||
|
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||||
|
import { useNavigate, useSearch } from "@tanstack/react-router";
|
||||||
|
|
||||||
|
import {
|
||||||
|
Alert,
|
||||||
|
AlertDescription,
|
||||||
|
Button,
|
||||||
|
Card,
|
||||||
|
CardTitle,
|
||||||
|
FormControl,
|
||||||
|
Input,
|
||||||
|
Select,
|
||||||
|
SelectItem
|
||||||
|
} from "@app/components/v2";
|
||||||
|
import { ROUTE_PATHS } from "@app/const/routes";
|
||||||
|
import { useWorkspace } from "@app/context";
|
||||||
|
import { useCreateIntegration } from "@app/hooks/api";
|
||||||
|
import {
|
||||||
|
useGetIntegrationAuthApps,
|
||||||
|
useGetIntegrationAuthById
|
||||||
|
} from "@app/hooks/api/integrationAuth";
|
||||||
|
|
||||||
|
export const FlyioConfigurePage = () => {
|
||||||
|
const navigate = useNavigate();
|
||||||
|
|
||||||
|
const { mutateAsync } = useCreateIntegration();
|
||||||
|
|
||||||
|
const { currentWorkspace } = useWorkspace();
|
||||||
|
|
||||||
|
const integrationAuthId = useSearch({
|
||||||
|
from: ROUTE_PATHS.SecretManager.Integratons.FlyioConfigurePage.id,
|
||||||
|
select: (el) => el.integrationAuthId
|
||||||
|
});
|
||||||
|
|
||||||
|
const { data: integrationAuth, isLoading: isIntegrationAuthLoading } = useGetIntegrationAuthById(
|
||||||
|
(integrationAuthId as string) ?? ""
|
||||||
|
);
|
||||||
|
const { data: integrationAuthApps = [], isLoading: isIntegrationAuthAppsLoading } =
|
||||||
|
useGetIntegrationAuthApps({
|
||||||
|
integrationAuthId: (integrationAuthId as string) ?? ""
|
||||||
|
});
|
||||||
|
|
||||||
|
const [selectedSourceEnvironment, setSelectedSourceEnvironment] = useState(
|
||||||
|
currentWorkspace.environments[0].slug
|
||||||
|
);
|
||||||
|
const [secretPath, setSecretPath] = useState("/");
|
||||||
|
|
||||||
|
const [targetApp, setTargetApp] = useState("");
|
||||||
|
|
||||||
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
// TODO: handle case where apps can be empty
|
||||||
|
if (integrationAuthApps) {
|
||||||
|
if (integrationAuthApps.length > 0) {
|
||||||
|
setTargetApp(integrationAuthApps[0].name);
|
||||||
|
} else {
|
||||||
|
setTargetApp("none");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, [integrationAuthApps]);
|
||||||
|
|
||||||
|
const handleButtonClick = async () => {
|
||||||
|
try {
|
||||||
|
if (!integrationAuth?.id) return;
|
||||||
|
|
||||||
|
setIsLoading(true);
|
||||||
|
|
||||||
|
await mutateAsync({
|
||||||
|
integrationAuthId: integrationAuth?.id,
|
||||||
|
isActive: true,
|
||||||
|
app: targetApp,
|
||||||
|
sourceEnvironment: selectedSourceEnvironment,
|
||||||
|
secretPath
|
||||||
|
});
|
||||||
|
|
||||||
|
setIsLoading(false);
|
||||||
|
|
||||||
|
navigate({
|
||||||
|
to: "/secret-manager/$projectId/integrations",
|
||||||
|
params: {
|
||||||
|
projectId: currentWorkspace.id
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return integrationAuth && selectedSourceEnvironment && integrationAuthApps && targetApp ? (
|
||||||
|
<div className="flex h-full w-full flex-col items-center justify-center">
|
||||||
|
<Helmet>
|
||||||
|
<title>Set Up Fly.io Integration</title>
|
||||||
|
</Helmet>
|
||||||
|
<Card className="max-w-lg rounded-md border border-mineshaft-600">
|
||||||
|
<CardTitle
|
||||||
|
className="px-6 text-left text-xl"
|
||||||
|
subTitle="Choose which environment or folder in Infisical you want to sync to Fly.io environment variables."
|
||||||
|
>
|
||||||
|
<div className="flex flex-row items-center">
|
||||||
|
<div className="flex items-center pb-0.5">
|
||||||
|
<img src="/images/integrations/Flyio.svg" height={30} width={30} alt="Fly.io logo" />
|
||||||
|
</div>
|
||||||
|
<span className="ml-2.5">Fly.io Integration </span>
|
||||||
|
<a
|
||||||
|
href="https://infisical.com/docs/integrations/cloud/flyio"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
>
|
||||||
|
<div className="mb-1 ml-2 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="mb-[0.07rem] ml-1.5 text-xxs"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</CardTitle>
|
||||||
|
<div className="px-6 pb-4">
|
||||||
|
<Alert hideTitle variant="warning">
|
||||||
|
<AlertDescription>
|
||||||
|
All current secrets linked to the related Fly.io project will be deleted before
|
||||||
|
Infisical secrets are pushed to your Fly.io project.
|
||||||
|
</AlertDescription>
|
||||||
|
</Alert>
|
||||||
|
</div>
|
||||||
|
<FormControl label="Project Environment" className="px-6">
|
||||||
|
<Select
|
||||||
|
value={selectedSourceEnvironment}
|
||||||
|
onValueChange={(val) => setSelectedSourceEnvironment(val)}
|
||||||
|
className="w-full border border-mineshaft-500"
|
||||||
|
>
|
||||||
|
{currentWorkspace?.environments.map((sourceEnvironment) => (
|
||||||
|
<SelectItem
|
||||||
|
value={sourceEnvironment.slug}
|
||||||
|
key={`source-environment-${sourceEnvironment.slug}`}
|
||||||
|
>
|
||||||
|
{sourceEnvironment.name}
|
||||||
|
</SelectItem>
|
||||||
|
))}
|
||||||
|
</Select>
|
||||||
|
</FormControl>
|
||||||
|
<FormControl label="Secrets Path" className="px-6">
|
||||||
|
<Input
|
||||||
|
value={secretPath}
|
||||||
|
onChange={(evt) => setSecretPath(evt.target.value)}
|
||||||
|
placeholder="Provide a path, default is /"
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
<FormControl label="Fly.io App" className="px-6">
|
||||||
|
<Select
|
||||||
|
value={targetApp}
|
||||||
|
onValueChange={(val) => setTargetApp(val)}
|
||||||
|
className="w-full border border-mineshaft-500"
|
||||||
|
isDisabled={integrationAuthApps.length === 0}
|
||||||
|
>
|
||||||
|
{integrationAuthApps.length > 0 ? (
|
||||||
|
integrationAuthApps.map((integrationAuthApp) => (
|
||||||
|
<SelectItem
|
||||||
|
value={integrationAuthApp.name}
|
||||||
|
key={`target-app-${integrationAuthApp.name}`}
|
||||||
|
>
|
||||||
|
{integrationAuthApp.name}
|
||||||
|
</SelectItem>
|
||||||
|
))
|
||||||
|
) : (
|
||||||
|
<SelectItem value="none" key="target-app-none">
|
||||||
|
No apps found
|
||||||
|
</SelectItem>
|
||||||
|
)}
|
||||||
|
</Select>
|
||||||
|
</FormControl>
|
||||||
|
<Button
|
||||||
|
onClick={handleButtonClick}
|
||||||
|
color="mineshaft"
|
||||||
|
variant="outline_bg"
|
||||||
|
className="mb-6 ml-auto mr-6 mt-2"
|
||||||
|
isLoading={isLoading}
|
||||||
|
isDisabled={integrationAuthApps.length === 0}
|
||||||
|
>
|
||||||
|
Create Integration
|
||||||
|
</Button>
|
||||||
|
</Card>
|
||||||
|
<div className="mt-6 w-full max-w-md border-t border-mineshaft-800" />
|
||||||
|
<div className="mt-6 flex w-full max-w-lg flex-col rounded-md border border-mineshaft-600 bg-mineshaft-800 p-4">
|
||||||
|
<div className="flex flex-row items-center">
|
||||||
|
<FontAwesomeIcon icon={faCircleInfo} className="text-xl text-mineshaft-200" />{" "}
|
||||||
|
<span className="text-md ml-3 text-mineshaft-100">Pro Tip</span>
|
||||||
|
</div>
|
||||||
|
<span className="mt-4 text-sm text-mineshaft-300">
|
||||||
|
After creating an integration, your secrets will start syncing immediately. This might
|
||||||
|
cause an unexpected override of current secrets in Fly.io with secrets from Infisical.
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="flex h-full w-full items-center justify-center">
|
||||||
|
<Helmet>
|
||||||
|
<title>Set Up Fly.io Integration</title>
|
||||||
|
</Helmet>
|
||||||
|
{isIntegrationAuthLoading || isIntegrationAuthAppsLoading ? (
|
||||||
|
<img
|
||||||
|
src="/images/loading/loading.gif"
|
||||||
|
height={70}
|
||||||
|
width={120}
|
||||||
|
alt="infisical loading indicator"
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<div className="flex h-max max-w-md flex-col rounded-md border border-mineshaft-600 bg-mineshaft-800 p-6 text-center text-mineshaft-200">
|
||||||
|
<FontAwesomeIcon icon={faBugs} className="inlineli my-2 text-6xl" />
|
||||||
|
<p>
|
||||||
|
Something went wrong. Please contact{" "}
|
||||||
|
<a
|
||||||
|
className="inline cursor-pointer text-mineshaft-100 underline decoration-primary-500 underline-offset-4 opacity-80 duration-200 hover:opacity-100"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
href="mailto:[email protected]"
|
||||||
|
>
|
||||||
|
support@infisical.com
|
||||||
|
</a>{" "}
|
||||||
|
if the issue persists.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
import { createFileRoute } from "@tanstack/react-router";
|
||||||
|
import { zodValidator } from "@tanstack/zod-adapter";
|
||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
import { FlyioConfigurePage } from "./FlyioConfigurePage";
|
||||||
|
|
||||||
|
const FlyioConfigurePageQueryParamsSchema = z.object({
|
||||||
|
integrationAuthId: z.string()
|
||||||
|
});
|
||||||
|
|
||||||
|
export const Route = createFileRoute(
|
||||||
|
"/_authenticate/_inject-org-details/secret-manager/$projectId/_secret-manager-layout/integrations/flyio/create"
|
||||||
|
)({
|
||||||
|
component: FlyioConfigurePage,
|
||||||
|
validateSearch: zodValidator(FlyioConfigurePageQueryParamsSchema)
|
||||||
|
});
|
||||||
+170
@@ -0,0 +1,170 @@
|
|||||||
|
import crypto from "crypto";
|
||||||
|
|
||||||
|
import { useState } from "react";
|
||||||
|
import { Helmet } from "react-helmet";
|
||||||
|
import { Controller, useForm } from "react-hook-form";
|
||||||
|
import { faGoogle } from "@fortawesome/free-brands-svg-icons";
|
||||||
|
import { faArrowUpRightFromSquare, faBookOpen } from "@fortawesome/free-solid-svg-icons";
|
||||||
|
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||||
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
|
import { useNavigate } from "@tanstack/react-router";
|
||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
import { Button, Card, CardTitle, FormControl, TextArea } from "@app/components/v2";
|
||||||
|
import { useWorkspace } from "@app/context";
|
||||||
|
import { localStorageService } from "@app/helpers/localStorage";
|
||||||
|
import { useGetCloudIntegrations, useSaveIntegrationAccessToken } from "@app/hooks/api";
|
||||||
|
|
||||||
|
import { createIntegrationMissingEnvVarsNotification } from "../../IntegrationsListPage/IntegrationsListPage.utils";
|
||||||
|
|
||||||
|
const schema = z.object({
|
||||||
|
accessToken: z.string()
|
||||||
|
});
|
||||||
|
|
||||||
|
type FormData = z.infer<typeof schema>;
|
||||||
|
|
||||||
|
export const GcpSecretManagerAuthorizePage = () => {
|
||||||
|
const navigate = useNavigate();
|
||||||
|
|
||||||
|
const { control, handleSubmit } = useForm<FormData>({
|
||||||
|
resolver: zodResolver(schema)
|
||||||
|
});
|
||||||
|
|
||||||
|
const { data: cloudIntegrations } = useGetCloudIntegrations();
|
||||||
|
const { currentWorkspace } = useWorkspace();
|
||||||
|
|
||||||
|
const { mutateAsync } = useSaveIntegrationAccessToken();
|
||||||
|
|
||||||
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
|
|
||||||
|
const handleIntegrateWithOAuth = () => {
|
||||||
|
if (!cloudIntegrations) return;
|
||||||
|
const integrationOption = cloudIntegrations.find(
|
||||||
|
(integration) => integration.slug === "gcp-secret-manager"
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!integrationOption) return;
|
||||||
|
|
||||||
|
const state = crypto.randomBytes(16).toString("hex");
|
||||||
|
localStorage.setItem("latestCSRFToken", state);
|
||||||
|
localStorageService.setIntegrationProjectId(currentWorkspace.id);
|
||||||
|
|
||||||
|
if (!integrationOption.clientId) {
|
||||||
|
createIntegrationMissingEnvVarsNotification(integrationOption.slug);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const link = `https://accounts.google.com/o/oauth2/auth?scope=https://www.googleapis.com/auth/cloud-platform&response_type=code&access_type=offline&state=${state}&redirect_uri=${window.location.origin}/integrations/gcp-secret-manager/oauth2/callback&client_id=${integrationOption.clientId}`;
|
||||||
|
window.location.assign(link);
|
||||||
|
};
|
||||||
|
|
||||||
|
const onFormSubmit = async ({ accessToken }: FormData) => {
|
||||||
|
try {
|
||||||
|
setIsLoading(true);
|
||||||
|
|
||||||
|
const integrationAuth = await mutateAsync({
|
||||||
|
workspaceId: currentWorkspace.id,
|
||||||
|
integration: "gcp-secret-manager",
|
||||||
|
refreshToken: accessToken
|
||||||
|
});
|
||||||
|
|
||||||
|
setIsLoading(false);
|
||||||
|
navigate({
|
||||||
|
to: "/secret-manager/$projectId/integrations/gcp-secret-manager/create",
|
||||||
|
params: {
|
||||||
|
projectId: currentWorkspace.id
|
||||||
|
},
|
||||||
|
search: {
|
||||||
|
integrationAuthId: integrationAuth.id
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err);
|
||||||
|
setIsLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex h-full w-full items-center justify-center">
|
||||||
|
<Helmet>
|
||||||
|
<title>Authorize GCP Secret Manager Integration</title>
|
||||||
|
</Helmet>
|
||||||
|
<Card className="mb-12 max-w-lg rounded-md border border-mineshaft-600">
|
||||||
|
<CardTitle
|
||||||
|
className="px-6 text-left text-xl"
|
||||||
|
subTitle="Connect Infisical to GCP Secret Manager to sync secrets."
|
||||||
|
>
|
||||||
|
<div className="flex flex-row items-center">
|
||||||
|
<div className="flex items-center pb-0.5">
|
||||||
|
<img
|
||||||
|
src="/images/integrations/Google Cloud Platform.png"
|
||||||
|
height={30}
|
||||||
|
width={30}
|
||||||
|
alt="GCP logo"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<span className="ml-1.5">GCP Secret Manager Integration </span>
|
||||||
|
<a
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
href="https://infisical.com/docs/integrations/cloud/gcp-secret-manager"
|
||||||
|
>
|
||||||
|
<div className="mb-1 ml-2 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="mb-[0.07rem] ml-1.5 text-xxs"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</CardTitle>
|
||||||
|
<div className="px-6">
|
||||||
|
<Button
|
||||||
|
colorSchema="primary"
|
||||||
|
variant="outline_bg"
|
||||||
|
onClick={handleIntegrateWithOAuth}
|
||||||
|
leftIcon={<FontAwesomeIcon icon={faGoogle} className="mr-2" />}
|
||||||
|
className="mx-0 mt-4 h-11 w-full"
|
||||||
|
>
|
||||||
|
Continue with OAuth
|
||||||
|
</Button>
|
||||||
|
<div className="my-4 flex w-full flex-row items-center py-2">
|
||||||
|
<div className="w-full border-t border-mineshaft-400/40" />
|
||||||
|
<span className="mx-2 text-xs text-mineshaft-400">or</span>
|
||||||
|
<div className="w-full border-t border-mineshaft-400/40" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<form onSubmit={handleSubmit(onFormSubmit)} className="px-6 pb-8 text-right">
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
|
name="accessToken"
|
||||||
|
render={({ field, fieldState: { error } }) => (
|
||||||
|
<FormControl
|
||||||
|
label="GCP Service Account JSON"
|
||||||
|
errorText={error?.message}
|
||||||
|
isError={Boolean(error)}
|
||||||
|
>
|
||||||
|
<TextArea
|
||||||
|
{...field}
|
||||||
|
className="h-48 border border-mineshaft-600 bg-bunker-900/80"
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<Button
|
||||||
|
colorSchema="primary"
|
||||||
|
variant="outline_bg"
|
||||||
|
className="mt-2 w-min"
|
||||||
|
size="sm"
|
||||||
|
type="submit"
|
||||||
|
isLoading={isLoading}
|
||||||
|
>
|
||||||
|
Connect to GCP Secret Manager
|
||||||
|
</Button>
|
||||||
|
</form>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
+9
@@ -0,0 +1,9 @@
|
|||||||
|
import { createFileRoute } from "@tanstack/react-router";
|
||||||
|
|
||||||
|
import { GcpSecretManagerAuthorizePage } from "./GcpSecretManagerAuthorizePage";
|
||||||
|
|
||||||
|
export const Route = createFileRoute(
|
||||||
|
"/_authenticate/_inject-org-details/secret-manager/$projectId/_secret-manager-layout/integrations/gcp-secret-manager/authorize"
|
||||||
|
)({
|
||||||
|
component: GcpSecretManagerAuthorizePage
|
||||||
|
});
|
||||||
+475
@@ -0,0 +1,475 @@
|
|||||||
|
import { useEffect, useState } from "react";
|
||||||
|
import { Helmet } from "react-helmet";
|
||||||
|
import { Controller, useForm } from "react-hook-form";
|
||||||
|
import { faArrowUpRightFromSquare, faBookOpen, faBugs } from "@fortawesome/free-solid-svg-icons";
|
||||||
|
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||||
|
import { yupResolver } from "@hookform/resolvers/yup";
|
||||||
|
import { useNavigate, useSearch } from "@tanstack/react-router";
|
||||||
|
import { motion } from "framer-motion";
|
||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
import {
|
||||||
|
Button,
|
||||||
|
Card,
|
||||||
|
CardTitle,
|
||||||
|
FormControl,
|
||||||
|
Input,
|
||||||
|
Modal,
|
||||||
|
ModalContent,
|
||||||
|
Select,
|
||||||
|
SelectItem,
|
||||||
|
Switch,
|
||||||
|
Tab,
|
||||||
|
TabList,
|
||||||
|
TabPanel,
|
||||||
|
Tabs
|
||||||
|
} from "@app/components/v2";
|
||||||
|
import { SecretPathInput } from "@app/components/v2/SecretPathInput";
|
||||||
|
import { ROUTE_PATHS } from "@app/const/routes";
|
||||||
|
import { useWorkspace } from "@app/context";
|
||||||
|
import { usePopUp } from "@app/hooks";
|
||||||
|
import { useCreateIntegration } from "@app/hooks/api";
|
||||||
|
import {
|
||||||
|
useGetIntegrationAuthApps,
|
||||||
|
useGetIntegrationAuthById
|
||||||
|
} from "@app/hooks/api/integrationAuth";
|
||||||
|
|
||||||
|
enum TabSections {
|
||||||
|
Connection = "connection",
|
||||||
|
Options = "options"
|
||||||
|
}
|
||||||
|
|
||||||
|
const schema = z.object({
|
||||||
|
selectedSourceEnvironment: z.string(),
|
||||||
|
secretPath: z.string(),
|
||||||
|
targetAppId: z.string(),
|
||||||
|
secretPrefix: z.string().trim().optional(),
|
||||||
|
secretSuffix: z.string().trim().optional(),
|
||||||
|
shouldLabel: z.boolean().optional(),
|
||||||
|
labelName: z.string().trim().optional(),
|
||||||
|
labelValue: z.string().trim().optional()
|
||||||
|
});
|
||||||
|
|
||||||
|
type FormData = z.infer<typeof schema>;
|
||||||
|
|
||||||
|
export const GcpSecretManagerConfigurePage = () => {
|
||||||
|
const navigate = useNavigate();
|
||||||
|
const { popUp, handlePopUpOpen, handlePopUpToggle, handlePopUpClose } = usePopUp([
|
||||||
|
"confirmIntegration"
|
||||||
|
] as const);
|
||||||
|
|
||||||
|
const { currentWorkspace } = useWorkspace();
|
||||||
|
const { control, handleSubmit, setValue, watch } = useForm<FormData>({
|
||||||
|
resolver: yupResolver(schema),
|
||||||
|
defaultValues: {
|
||||||
|
secretPath: "/",
|
||||||
|
secretPrefix: "",
|
||||||
|
secretSuffix: "",
|
||||||
|
shouldLabel: false,
|
||||||
|
labelName: "managed-by",
|
||||||
|
labelValue: "infisical",
|
||||||
|
selectedSourceEnvironment: currentWorkspace.environments[0].slug
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const shouldLabel = watch("shouldLabel");
|
||||||
|
const selectedSourceEnvironment = watch("selectedSourceEnvironment");
|
||||||
|
|
||||||
|
const { mutateAsync } = useCreateIntegration();
|
||||||
|
|
||||||
|
const integrationAuthId = useSearch({
|
||||||
|
from: ROUTE_PATHS.SecretManager.Integratons.GcpSecretManagerConfigurePage.id,
|
||||||
|
select: (el) => el.integrationAuthId
|
||||||
|
});
|
||||||
|
const { data: integrationAuth } = useGetIntegrationAuthById((integrationAuthId as string) ?? "");
|
||||||
|
const { data: integrationAuthApps, isLoading: isIntegrationAuthAppsLoading } =
|
||||||
|
useGetIntegrationAuthApps({
|
||||||
|
integrationAuthId: (integrationAuthId as string) ?? ""
|
||||||
|
});
|
||||||
|
|
||||||
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (shouldLabel) {
|
||||||
|
setValue("labelName", "managed-by");
|
||||||
|
setValue("labelValue", "infisical");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setValue("labelName", "");
|
||||||
|
setValue("labelValue", "");
|
||||||
|
}, [shouldLabel]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (integrationAuthApps) {
|
||||||
|
if (integrationAuthApps.length > 0) {
|
||||||
|
setValue("targetAppId", integrationAuthApps[0].appId as string);
|
||||||
|
} else {
|
||||||
|
setValue("targetAppId", "none");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, [integrationAuthApps]);
|
||||||
|
|
||||||
|
const onFormSubmit = async ({
|
||||||
|
selectedSourceEnvironment: sce,
|
||||||
|
secretPath,
|
||||||
|
targetAppId,
|
||||||
|
secretPrefix,
|
||||||
|
secretSuffix,
|
||||||
|
shouldLabel: sl,
|
||||||
|
labelName,
|
||||||
|
labelValue
|
||||||
|
}: FormData) => {
|
||||||
|
try {
|
||||||
|
setIsLoading(true);
|
||||||
|
|
||||||
|
if (!integrationAuth?.id) return;
|
||||||
|
|
||||||
|
await mutateAsync({
|
||||||
|
integrationAuthId: integrationAuth?.id,
|
||||||
|
isActive: true,
|
||||||
|
app: integrationAuthApps?.find(
|
||||||
|
(integrationAuthApp) => integrationAuthApp.appId === targetAppId
|
||||||
|
)?.name,
|
||||||
|
appId: targetAppId,
|
||||||
|
sourceEnvironment: sce,
|
||||||
|
secretPath,
|
||||||
|
metadata: {
|
||||||
|
...(secretPrefix ? { secretPrefix } : {}),
|
||||||
|
...(secretSuffix ? { secretSuffix } : {}),
|
||||||
|
...(sl
|
||||||
|
? {
|
||||||
|
secretGCPLabel: {
|
||||||
|
labelName,
|
||||||
|
labelValue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
: {})
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
setIsLoading(false);
|
||||||
|
navigate({
|
||||||
|
to: "/secret-manager/$projectId/integrations",
|
||||||
|
params: {
|
||||||
|
projectId: currentWorkspace.id
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err);
|
||||||
|
setIsLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return integrationAuth && selectedSourceEnvironment && integrationAuthApps ? (
|
||||||
|
<form
|
||||||
|
onSubmit={handleSubmit((data: FormData) => {
|
||||||
|
if (!data.secretPrefix && !data.secretSuffix && !data.shouldLabel) {
|
||||||
|
handlePopUpOpen("confirmIntegration", data);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
onFormSubmit(data);
|
||||||
|
})}
|
||||||
|
className="flex h-full w-full flex-col items-center justify-center"
|
||||||
|
>
|
||||||
|
<Card className="max-w-lg rounded-md border border-mineshaft-600">
|
||||||
|
<Helmet>
|
||||||
|
<title>Set Up GCP Secret Manager Integration</title>
|
||||||
|
</Helmet>
|
||||||
|
<CardTitle
|
||||||
|
className="mb-2 px-6 text-left text-xl"
|
||||||
|
subTitle="Select which environment or folder in Infisical you want to sync to GCP Secret Manager."
|
||||||
|
>
|
||||||
|
<div className="flex flex-row items-center">
|
||||||
|
<div className="flex items-center pb-0.5">
|
||||||
|
<img
|
||||||
|
src="/images/integrations/Google Cloud Platform.png"
|
||||||
|
height={30}
|
||||||
|
width={30}
|
||||||
|
alt="GCP logo"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<span className="ml-1.5">GCP Secret Manager Integration </span>
|
||||||
|
<a
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
href="https://infisical.com/docs/integrations/cloud/gcp-secret-manager"
|
||||||
|
>
|
||||||
|
<div className="mb-1 ml-2 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="mb-[0.07rem] ml-1.5 text-xxs"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</CardTitle>
|
||||||
|
<Tabs defaultValue={TabSections.Connection} className="px-6">
|
||||||
|
<TabList>
|
||||||
|
<div className="flex w-full flex-row border-b border-mineshaft-600">
|
||||||
|
<Tab value={TabSections.Connection}>Connection</Tab>
|
||||||
|
<Tab value={TabSections.Options}>Options</Tab>
|
||||||
|
</div>
|
||||||
|
</TabList>
|
||||||
|
<TabPanel value={TabSections.Connection}>
|
||||||
|
<motion.div
|
||||||
|
key="panel-1"
|
||||||
|
transition={{ duration: 0.15 }}
|
||||||
|
initial={{ opacity: 0, translateX: 30 }}
|
||||||
|
animate={{ opacity: 1, translateX: 0 }}
|
||||||
|
exit={{ opacity: 0, translateX: 30 }}
|
||||||
|
>
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
|
name="selectedSourceEnvironment"
|
||||||
|
render={({ field: { onChange, ...field }, fieldState: { error } }) => (
|
||||||
|
<FormControl
|
||||||
|
label="Project Environment"
|
||||||
|
errorText={error?.message}
|
||||||
|
isError={Boolean(error)}
|
||||||
|
>
|
||||||
|
<Select
|
||||||
|
defaultValue={field.value}
|
||||||
|
{...field}
|
||||||
|
onValueChange={(e) => onChange(e)}
|
||||||
|
className="w-full"
|
||||||
|
>
|
||||||
|
{currentWorkspace?.environments.map((sourceEnvironment) => (
|
||||||
|
<SelectItem
|
||||||
|
value={sourceEnvironment.slug}
|
||||||
|
key={`source-environment-${sourceEnvironment.slug}`}
|
||||||
|
>
|
||||||
|
{sourceEnvironment.name}
|
||||||
|
</SelectItem>
|
||||||
|
))}
|
||||||
|
</Select>
|
||||||
|
</FormControl>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
|
defaultValue=""
|
||||||
|
name="secretPath"
|
||||||
|
render={({ field, fieldState: { error } }) => (
|
||||||
|
<FormControl
|
||||||
|
label="Secrets Path"
|
||||||
|
isError={Boolean(error)}
|
||||||
|
errorText={error?.message}
|
||||||
|
>
|
||||||
|
<SecretPathInput
|
||||||
|
{...field}
|
||||||
|
environment={selectedSourceEnvironment}
|
||||||
|
placeholder="/"
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
|
name="targetAppId"
|
||||||
|
render={({ field: { onChange, ...field }, fieldState: { error } }) => {
|
||||||
|
return (
|
||||||
|
<FormControl
|
||||||
|
label="GCP Project"
|
||||||
|
errorText={error?.message}
|
||||||
|
isError={Boolean(error)}
|
||||||
|
>
|
||||||
|
<Select
|
||||||
|
{...field}
|
||||||
|
onValueChange={(e) => {
|
||||||
|
if (e === "") return;
|
||||||
|
onChange(e);
|
||||||
|
}}
|
||||||
|
className="w-full"
|
||||||
|
>
|
||||||
|
{integrationAuthApps.length > 0 ? (
|
||||||
|
integrationAuthApps.map((integrationAuthApp) => (
|
||||||
|
<SelectItem
|
||||||
|
value={String(integrationAuthApp.appId as string)}
|
||||||
|
key={`target-app-${String(integrationAuthApp.appId)}`}
|
||||||
|
>
|
||||||
|
{integrationAuthApp.name}
|
||||||
|
</SelectItem>
|
||||||
|
))
|
||||||
|
) : (
|
||||||
|
<SelectItem value="none" key="target-app-none">
|
||||||
|
No projects found
|
||||||
|
</SelectItem>
|
||||||
|
)}
|
||||||
|
</Select>
|
||||||
|
</FormControl>
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</motion.div>
|
||||||
|
</TabPanel>
|
||||||
|
<TabPanel value={TabSections.Options}>
|
||||||
|
<motion.div
|
||||||
|
key="panel-1"
|
||||||
|
transition={{ duration: 0.15 }}
|
||||||
|
initial={{ opacity: 0, translateX: -30 }}
|
||||||
|
animate={{ opacity: 1, translateX: 0 }}
|
||||||
|
exit={{ opacity: 0, translateX: 30 }}
|
||||||
|
>
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
|
name="secretPrefix"
|
||||||
|
render={({ field, fieldState: { error } }) => (
|
||||||
|
<FormControl
|
||||||
|
label="Secret Prefix"
|
||||||
|
isError={Boolean(error)}
|
||||||
|
errorText={error?.message}
|
||||||
|
>
|
||||||
|
<Input {...field} placeholder="INFISICAL_" />
|
||||||
|
</FormControl>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
|
name="secretSuffix"
|
||||||
|
render={({ field, fieldState: { error } }) => (
|
||||||
|
<FormControl
|
||||||
|
label="Secret Suffix"
|
||||||
|
isError={Boolean(error)}
|
||||||
|
errorText={error?.message}
|
||||||
|
>
|
||||||
|
<Input {...field} placeholder="_INFISICAL" />
|
||||||
|
</FormControl>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<div className="mb-[2.36rem] mt-8">
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
|
name="shouldLabel"
|
||||||
|
render={({ field: { onChange, value } }) => (
|
||||||
|
<Switch
|
||||||
|
id="label-gcp"
|
||||||
|
onCheckedChange={(isChecked) => onChange(isChecked)}
|
||||||
|
isChecked={value}
|
||||||
|
>
|
||||||
|
Label in GCP Secret Manager
|
||||||
|
</Switch>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
{shouldLabel && (
|
||||||
|
<div className="mt-8">
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
|
name="labelName"
|
||||||
|
render={({ field, fieldState: { error } }) => (
|
||||||
|
<FormControl
|
||||||
|
label="Label Name"
|
||||||
|
isError={Boolean(error)}
|
||||||
|
errorText={error?.message}
|
||||||
|
>
|
||||||
|
<Input {...field} placeholder="managed-by" />
|
||||||
|
</FormControl>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
|
name="labelValue"
|
||||||
|
render={({ field, fieldState: { error } }) => (
|
||||||
|
<FormControl
|
||||||
|
label="Label Name"
|
||||||
|
isError={Boolean(error)}
|
||||||
|
errorText={error?.message}
|
||||||
|
>
|
||||||
|
<Input {...field} placeholder="infisical" />
|
||||||
|
</FormControl>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</motion.div>
|
||||||
|
</TabPanel>
|
||||||
|
</Tabs>
|
||||||
|
<Button
|
||||||
|
colorSchema="primary"
|
||||||
|
variant="outline_bg"
|
||||||
|
className="mb-8 ml-auto mr-6 w-min"
|
||||||
|
size="sm"
|
||||||
|
type="submit"
|
||||||
|
isLoading={isLoading}
|
||||||
|
isDisabled={integrationAuthApps.length === 0}
|
||||||
|
>
|
||||||
|
Create Integration
|
||||||
|
</Button>
|
||||||
|
</Card>
|
||||||
|
{/* <div className="border-t border-mineshaft-800 w-full max-w-md mt-6"/>
|
||||||
|
<div className="flex flex-col bg-mineshaft-800 border border-mineshaft-600 w-full p-4 max-w-lg mt-6 rounded-md">
|
||||||
|
<div className="flex flex-row items-center">
|
||||||
|
<FontAwesomeIcon icon={faCircleInfo} className="text-mineshaft-200 text-xl"/>
|
||||||
|
<span className="ml-3 text-md text-mineshaft-100">Pro Tip</span>
|
||||||
|
</div>
|
||||||
|
<span className="text-mineshaft-300 text-sm mt-4">
|
||||||
|
After creating an integration, your secrets will start syncing immediately.
|
||||||
|
|
||||||
|
To avoid overwriting existing secrets in GCP Secret Manager, you may consider adding a secret prefix/suffix and/or enabling labeling in the options tab.
|
||||||
|
</span>
|
||||||
|
</div> */}
|
||||||
|
<Modal
|
||||||
|
isOpen={popUp.confirmIntegration?.isOpen}
|
||||||
|
onOpenChange={(isOpen) => handlePopUpToggle("confirmIntegration", isOpen)}
|
||||||
|
>
|
||||||
|
<ModalContent
|
||||||
|
title="Heads Up"
|
||||||
|
footerContent={
|
||||||
|
<div className="flex items-center space-x-2">
|
||||||
|
<Button onClick={() => onFormSubmit(popUp.confirmIntegration?.data as FormData)}>
|
||||||
|
Continue Anyway
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
onClick={() => handlePopUpClose("confirmIntegration")}
|
||||||
|
variant="outline_bg"
|
||||||
|
colorSchema="secondary"
|
||||||
|
>
|
||||||
|
Cancel
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<p>You're about to overwrite any existing secrets in GCP Secret Manager.</p>
|
||||||
|
<p className="mt-4">
|
||||||
|
To avoid this behavior, you may consider adding a secret prefix/suffix or enabling
|
||||||
|
labeling in the options tab.
|
||||||
|
</p>
|
||||||
|
</ModalContent>
|
||||||
|
</Modal>
|
||||||
|
</form>
|
||||||
|
) : (
|
||||||
|
<div className="flex h-full w-full items-center justify-center">
|
||||||
|
<Helmet>
|
||||||
|
<title>Set Up GCP Secret Manager Integration</title>
|
||||||
|
</Helmet>
|
||||||
|
{isIntegrationAuthAppsLoading ? (
|
||||||
|
<img
|
||||||
|
src="/images/loading/loading.gif"
|
||||||
|
height={70}
|
||||||
|
width={120}
|
||||||
|
alt="infisical loading indicator"
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<div className="flex h-max max-w-md flex-col rounded-md border border-mineshaft-600 bg-mineshaft-800 p-6 text-center text-mineshaft-200">
|
||||||
|
<FontAwesomeIcon icon={faBugs} className="inlineli my-2 text-6xl" />
|
||||||
|
<p>
|
||||||
|
Something went wrong. Please contact{" "}
|
||||||
|
<a
|
||||||
|
className="inline cursor-pointer text-mineshaft-100 underline decoration-primary-500 underline-offset-4 opacity-80 duration-200 hover:opacity-100"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
href="mailto:[email protected]"
|
||||||
|
>
|
||||||
|
support@infisical.com
|
||||||
|
</a>{" "}
|
||||||
|
if the issue persists.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
+16
@@ -0,0 +1,16 @@
|
|||||||
|
import { createFileRoute } from "@tanstack/react-router";
|
||||||
|
import { zodValidator } from "@tanstack/zod-adapter";
|
||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
import { GcpSecretManagerConfigurePage } from "./GcpSecretManagerConfigurePage";
|
||||||
|
|
||||||
|
const GcpConfigurePageQueryParamsSchema = z.object({
|
||||||
|
integrationAuthId: z.string()
|
||||||
|
});
|
||||||
|
|
||||||
|
export const Route = createFileRoute(
|
||||||
|
"/_authenticate/_inject-org-details/secret-manager/$projectId/_secret-manager-layout/integrations/gcp-secret-manager/create"
|
||||||
|
)({
|
||||||
|
component: GcpSecretManagerConfigurePage,
|
||||||
|
validateSearch: zodValidator(GcpConfigurePageQueryParamsSchema)
|
||||||
|
});
|
||||||
+46
@@ -0,0 +1,46 @@
|
|||||||
|
import { useEffect } from "react";
|
||||||
|
import { useNavigate, useSearch } from "@tanstack/react-router";
|
||||||
|
|
||||||
|
import { ROUTE_PATHS } from "@app/const/routes";
|
||||||
|
import { useWorkspace } from "@app/context";
|
||||||
|
import { useAuthorizeIntegration } from "@app/hooks/api";
|
||||||
|
|
||||||
|
export const GcpSecretManagerOauthCallbackPage = () => {
|
||||||
|
const navigate = useNavigate();
|
||||||
|
const { mutateAsync } = useAuthorizeIntegration();
|
||||||
|
|
||||||
|
const { code, state } = useSearch({
|
||||||
|
from: ROUTE_PATHS.SecretManager.Integratons.GcpSecretManagerOauthCallbackPage.id
|
||||||
|
});
|
||||||
|
const { currentWorkspace } = useWorkspace();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
(async () => {
|
||||||
|
try {
|
||||||
|
// validate state
|
||||||
|
|
||||||
|
if (state !== localStorage.getItem("latestCSRFToken")) return;
|
||||||
|
localStorage.removeItem("latestCSRFToken");
|
||||||
|
const integrationAuth = await mutateAsync({
|
||||||
|
workspaceId: currentWorkspace.id,
|
||||||
|
code: code as string,
|
||||||
|
integration: "gcp-secret-manager"
|
||||||
|
});
|
||||||
|
|
||||||
|
navigate({
|
||||||
|
to: "/secret-manager/$projectId/integrations/gcp-secret-manager/create",
|
||||||
|
params: {
|
||||||
|
projectId: currentWorkspace.id
|
||||||
|
},
|
||||||
|
search: {
|
||||||
|
integrationAuthId: integrationAuth.id
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err);
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return <div />;
|
||||||
|
};
|
||||||
+17
@@ -0,0 +1,17 @@
|
|||||||
|
import { createFileRoute } from "@tanstack/react-router";
|
||||||
|
import { zodValidator } from "@tanstack/zod-adapter";
|
||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
import { GcpSecretManagerOauthCallbackPage } from "./GcpSecretManagerOauthCallbackPage";
|
||||||
|
|
||||||
|
export const GcpSecretManagerOAuthCallbackPageQueryParamsSchema = z.object({
|
||||||
|
state: z.string().catch(""),
|
||||||
|
code: z.string()
|
||||||
|
});
|
||||||
|
|
||||||
|
export const Route = createFileRoute(
|
||||||
|
"/_authenticate/_inject-org-details/secret-manager/$projectId/_secret-manager-layout/integrations/gcp-secret-manager/oauth2/callback"
|
||||||
|
)({
|
||||||
|
component: GcpSecretManagerOauthCallbackPage,
|
||||||
|
validateSearch: zodValidator(GcpSecretManagerOAuthCallbackPageQueryParamsSchema)
|
||||||
|
});
|
||||||
+122
@@ -0,0 +1,122 @@
|
|||||||
|
import crypto from "crypto";
|
||||||
|
|
||||||
|
import { useState } from "react";
|
||||||
|
import { Helmet } from "react-helmet";
|
||||||
|
import { faArrowUpRightFromSquare, faBookOpen } from "@fortawesome/free-solid-svg-icons";
|
||||||
|
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||||
|
import { useNavigate } from "@tanstack/react-router";
|
||||||
|
|
||||||
|
import {
|
||||||
|
Button,
|
||||||
|
Card,
|
||||||
|
CardBody,
|
||||||
|
CardTitle,
|
||||||
|
FormControl,
|
||||||
|
Select,
|
||||||
|
SelectItem
|
||||||
|
} from "@app/components/v2";
|
||||||
|
import { useWorkspace } from "@app/context";
|
||||||
|
import { localStorageService } from "@app/helpers/localStorage";
|
||||||
|
import { useGetCloudIntegrations } from "@app/hooks/api";
|
||||||
|
|
||||||
|
import { createIntegrationMissingEnvVarsNotification } from "../../IntegrationsListPage/IntegrationsListPage.utils";
|
||||||
|
|
||||||
|
enum AuthMethod {
|
||||||
|
APP = "APP",
|
||||||
|
OAUTH = "OAUTH"
|
||||||
|
}
|
||||||
|
|
||||||
|
export const GithubAuthorizePage = () => {
|
||||||
|
const navigate = useNavigate();
|
||||||
|
const { data: cloudIntegrations } = useGetCloudIntegrations();
|
||||||
|
const githubIntegration = cloudIntegrations?.find((integration) => integration.slug === "github");
|
||||||
|
const [selectedAuthMethod, setSelectedAuthMethod] = useState<AuthMethod>(AuthMethod.APP);
|
||||||
|
const { currentWorkspace } = useWorkspace();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex h-full w-full items-center justify-center">
|
||||||
|
<Helmet>
|
||||||
|
<title>Select GitHub Integration Auth</title>
|
||||||
|
</Helmet>
|
||||||
|
<Card className="mb-12 max-w-lg rounded-md border border-mineshaft-600">
|
||||||
|
<CardTitle
|
||||||
|
className="px-6 text-left text-xl"
|
||||||
|
subTitle="Select how you'd like to integrate with GitHub. We recommend using the GitHub App method for fine-grained access."
|
||||||
|
>
|
||||||
|
<div className="flex flex-row items-center">
|
||||||
|
<div className="flex items-center pb-0.5">
|
||||||
|
<img src="/images/integrations/GitHub.png" height={30} width={30} alt="Github logo" />
|
||||||
|
</div>
|
||||||
|
<span className="ml-2.5">GitHub Integration </span>
|
||||||
|
<a
|
||||||
|
href="https://infisical.com/docs/integrations/cicd/githubactions"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
>
|
||||||
|
<div className="mb-1 ml-2 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="mb-[0.07rem] ml-1.5 text-xxs"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</CardTitle>
|
||||||
|
<CardBody>
|
||||||
|
<FormControl label="Select authentication method">
|
||||||
|
<Select
|
||||||
|
value={selectedAuthMethod}
|
||||||
|
onValueChange={(val) => {
|
||||||
|
setSelectedAuthMethod(val as AuthMethod);
|
||||||
|
}}
|
||||||
|
className="w-full border border-mineshaft-500"
|
||||||
|
>
|
||||||
|
<SelectItem value={AuthMethod.APP}>GitHub App (Recommended)</SelectItem>
|
||||||
|
<SelectItem value={AuthMethod.OAUTH}>OAuth</SelectItem>
|
||||||
|
</Select>
|
||||||
|
</FormControl>
|
||||||
|
<div className="flex items-end">
|
||||||
|
<Button
|
||||||
|
onClick={() => {
|
||||||
|
if (selectedAuthMethod === AuthMethod.APP) {
|
||||||
|
navigate({
|
||||||
|
to: "/secret-manager/$projectId/integrations/select-integration-auth",
|
||||||
|
params: {
|
||||||
|
projectId: currentWorkspace.id
|
||||||
|
},
|
||||||
|
search: {
|
||||||
|
integrationSlug: "github"
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
if (!githubIntegration?.clientId) {
|
||||||
|
createIntegrationMissingEnvVarsNotification(
|
||||||
|
"githubactions",
|
||||||
|
"cicd",
|
||||||
|
"connecting-with-github-oauth"
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const state = crypto.randomBytes(16).toString("hex");
|
||||||
|
localStorage.setItem("latestCSRFToken", state);
|
||||||
|
localStorageService.setIntegrationProjectId(currentWorkspace.id);
|
||||||
|
window.location.assign(
|
||||||
|
`https://github.com/login/oauth/authorize?client_id=${githubIntegration?.clientId}&response_type=code&scope=repo,admin:org&redirect_uri=${window.location.origin}/integrations/github/oauth2/callback&state=${state}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
colorSchema="primary"
|
||||||
|
variant="outline_bg"
|
||||||
|
className="ml-auto mt-4 w-min"
|
||||||
|
>
|
||||||
|
Connect to GitHub
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</CardBody>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
import { createFileRoute } from "@tanstack/react-router";
|
||||||
|
|
||||||
|
import { GithubAuthorizePage } from "./GithubAuthorizePage";
|
||||||
|
|
||||||
|
export const Route = createFileRoute(
|
||||||
|
"/_authenticate/_inject-org-details/secret-manager/$projectId/_secret-manager-layout/integrations/github/auth-mode-selection"
|
||||||
|
)({
|
||||||
|
component: GithubAuthorizePage
|
||||||
|
});
|
||||||
+817
@@ -0,0 +1,817 @@
|
|||||||
|
import { useEffect, useMemo, useState } from "react";
|
||||||
|
import { Helmet } from "react-helmet";
|
||||||
|
import { Controller, useForm } from "react-hook-form";
|
||||||
|
import {
|
||||||
|
faAngleDown,
|
||||||
|
faArrowUpRightFromSquare,
|
||||||
|
faBookOpen,
|
||||||
|
faBugs,
|
||||||
|
faCheckCircle,
|
||||||
|
faCircleInfo
|
||||||
|
} from "@fortawesome/free-solid-svg-icons";
|
||||||
|
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||||
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
|
import { useNavigate, useSearch } from "@tanstack/react-router";
|
||||||
|
import axios from "axios";
|
||||||
|
import { motion } from "framer-motion";
|
||||||
|
import { twMerge } from "tailwind-merge";
|
||||||
|
import { z, ZodIssueCode } from "zod";
|
||||||
|
|
||||||
|
import { createNotification } from "@app/components/notifications";
|
||||||
|
import {
|
||||||
|
Button,
|
||||||
|
Card,
|
||||||
|
CardTitle,
|
||||||
|
DropdownMenu,
|
||||||
|
DropdownMenuContent,
|
||||||
|
DropdownMenuItem,
|
||||||
|
DropdownMenuTrigger,
|
||||||
|
FormControl,
|
||||||
|
Input,
|
||||||
|
Select,
|
||||||
|
SelectItem,
|
||||||
|
Switch,
|
||||||
|
Tab,
|
||||||
|
TabList,
|
||||||
|
TabPanel,
|
||||||
|
Tabs
|
||||||
|
} from "@app/components/v2";
|
||||||
|
import { ROUTE_PATHS } from "@app/const/routes";
|
||||||
|
import { useWorkspace } from "@app/context";
|
||||||
|
import {
|
||||||
|
useCreateIntegration,
|
||||||
|
useGetIntegrationAuthApps,
|
||||||
|
useGetIntegrationAuthById,
|
||||||
|
useGetIntegrationAuthGithubEnvs,
|
||||||
|
useGetIntegrationAuthGithubOrgs
|
||||||
|
} from "@app/hooks/api";
|
||||||
|
|
||||||
|
enum TabSections {
|
||||||
|
Connection = "connection",
|
||||||
|
Options = "options"
|
||||||
|
}
|
||||||
|
|
||||||
|
const secretsVisibility = [
|
||||||
|
{
|
||||||
|
value: "selected",
|
||||||
|
label: "Select repositories"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: "all",
|
||||||
|
label: "All public repositories"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: "private",
|
||||||
|
label: "All private repositories"
|
||||||
|
}
|
||||||
|
] as const;
|
||||||
|
|
||||||
|
const targetEnv = ["github-repo", "github-org", "github-env"] as const;
|
||||||
|
|
||||||
|
const schema = z
|
||||||
|
.object({
|
||||||
|
selectedSourceEnvironment: z.string().trim(),
|
||||||
|
secretPath: z.string().trim(),
|
||||||
|
secretSuffix: z.string().trim().optional(),
|
||||||
|
shouldEnableDelete: z.boolean().optional(),
|
||||||
|
scope: z.enum(targetEnv),
|
||||||
|
|
||||||
|
// Explanation: If scope is (github-repo) OR (github-org AND visibility is set to selected), then repoIds is required
|
||||||
|
repoId: z.string().optional(),
|
||||||
|
repoIds: z.string().array().optional(),
|
||||||
|
repoName: z.string().optional(),
|
||||||
|
repoOwner: z.string().optional(),
|
||||||
|
envId: z.string().optional(),
|
||||||
|
orgId: z.string().optional(),
|
||||||
|
visibility: z.string().optional()
|
||||||
|
})
|
||||||
|
.superRefine((data, ctx) => {
|
||||||
|
if (data.scope === "github-env") {
|
||||||
|
if (!data.repoId) {
|
||||||
|
ctx.addIssue({
|
||||||
|
code: ZodIssueCode.custom,
|
||||||
|
path: ["repoId"],
|
||||||
|
message: "Repository is required"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (!data.repoName) {
|
||||||
|
ctx.addIssue({
|
||||||
|
code: ZodIssueCode.custom,
|
||||||
|
path: ["repoName"],
|
||||||
|
message: "Repository is required"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (!data.repoOwner) {
|
||||||
|
ctx.addIssue({
|
||||||
|
code: ZodIssueCode.custom,
|
||||||
|
path: ["repoOwner"],
|
||||||
|
message: "Repository is required"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (!data.envId) {
|
||||||
|
ctx.addIssue({
|
||||||
|
code: ZodIssueCode.custom,
|
||||||
|
path: ["envId"],
|
||||||
|
message: "Environment is required"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (data.scope === "github-org") {
|
||||||
|
if (!data.orgId) {
|
||||||
|
ctx.addIssue({
|
||||||
|
code: ZodIssueCode.custom,
|
||||||
|
path: ["orgId"],
|
||||||
|
message: "Organization is required"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (!data.visibility) {
|
||||||
|
ctx.addIssue({
|
||||||
|
code: ZodIssueCode.custom,
|
||||||
|
path: ["visibility"],
|
||||||
|
message: "Visibility is required"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
type FormData = z.infer<typeof schema>;
|
||||||
|
|
||||||
|
export const GithubConfigurePage = () => {
|
||||||
|
const navigate = useNavigate();
|
||||||
|
const { mutateAsync } = useCreateIntegration();
|
||||||
|
|
||||||
|
const { currentWorkspace } = useWorkspace();
|
||||||
|
const integrationAuthId = useSearch({
|
||||||
|
from: ROUTE_PATHS.SecretManager.Integratons.GithubConfigurePage.id,
|
||||||
|
select: (el) => el.integrationAuthId
|
||||||
|
});
|
||||||
|
const { data: integrationAuth } = useGetIntegrationAuthById(integrationAuthId);
|
||||||
|
|
||||||
|
const { data: integrationAuthApps, isLoading: isIntegrationAuthAppsLoading } =
|
||||||
|
useGetIntegrationAuthApps({
|
||||||
|
integrationAuthId
|
||||||
|
});
|
||||||
|
|
||||||
|
const { data: integrationAuthOrgs } = useGetIntegrationAuthGithubOrgs(
|
||||||
|
integrationAuthId as string
|
||||||
|
);
|
||||||
|
|
||||||
|
const { control, handleSubmit, watch, setValue } = useForm<FormData>({
|
||||||
|
resolver: zodResolver(schema),
|
||||||
|
defaultValues: {
|
||||||
|
secretPath: "/",
|
||||||
|
scope: "github-repo",
|
||||||
|
repoIds: [],
|
||||||
|
visibility: "all",
|
||||||
|
shouldEnableDelete: false,
|
||||||
|
selectedSourceEnvironment: currentWorkspace.environments[0].slug
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const scope = watch("scope");
|
||||||
|
const repoId = watch("repoId");
|
||||||
|
const repoIds = watch("repoIds") || [];
|
||||||
|
const repoName = watch("repoName");
|
||||||
|
const repoOwner = watch("repoOwner");
|
||||||
|
const selectedOrgId = watch("orgId");
|
||||||
|
|
||||||
|
const { data: integrationAuthGithubEnvs } = useGetIntegrationAuthGithubEnvs(
|
||||||
|
integrationAuthId as string,
|
||||||
|
repoName || "",
|
||||||
|
repoOwner || ""
|
||||||
|
);
|
||||||
|
|
||||||
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (integrationAuthGithubEnvs && integrationAuthGithubEnvs?.length > 0) {
|
||||||
|
setValue("envId", integrationAuthGithubEnvs[0].envId);
|
||||||
|
} else {
|
||||||
|
setValue("envId", undefined);
|
||||||
|
}
|
||||||
|
}, [integrationAuthGithubEnvs]);
|
||||||
|
|
||||||
|
const onFormSubmit = async (data: FormData) => {
|
||||||
|
try {
|
||||||
|
setIsLoading(true);
|
||||||
|
|
||||||
|
if (!integrationAuth?.id) return;
|
||||||
|
|
||||||
|
switch (data.scope) {
|
||||||
|
case "github-repo": {
|
||||||
|
const targetApps = integrationAuthApps?.filter((integrationAuthApp) =>
|
||||||
|
data.repoIds?.includes(String(integrationAuthApp.appId))
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!targetApps) return;
|
||||||
|
|
||||||
|
await Promise.all(
|
||||||
|
targetApps.map(async (targetApp) => {
|
||||||
|
await mutateAsync({
|
||||||
|
integrationAuthId: integrationAuth?.id,
|
||||||
|
isActive: true,
|
||||||
|
scope: data.scope,
|
||||||
|
secretPath: data.secretPath,
|
||||||
|
sourceEnvironment: data.selectedSourceEnvironment,
|
||||||
|
app: targetApp.name, // repo name
|
||||||
|
owner: targetApp.owner, // repo owner
|
||||||
|
metadata: {
|
||||||
|
secretSuffix: data.secretSuffix,
|
||||||
|
shouldEnableDelete: data.shouldEnableDelete
|
||||||
|
}
|
||||||
|
});
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case "github-org":
|
||||||
|
await mutateAsync({
|
||||||
|
integrationAuthId: integrationAuth?.id,
|
||||||
|
isActive: true,
|
||||||
|
secretPath: data.secretPath,
|
||||||
|
sourceEnvironment: data.selectedSourceEnvironment,
|
||||||
|
scope: data.scope,
|
||||||
|
owner: integrationAuthOrgs?.find((e) => e.orgId === data.orgId)?.name,
|
||||||
|
metadata: {
|
||||||
|
githubVisibility: data.visibility,
|
||||||
|
githubVisibilityRepoIds: data.repoIds,
|
||||||
|
secretSuffix: data.secretSuffix,
|
||||||
|
shouldEnableDelete: data.shouldEnableDelete
|
||||||
|
}
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
|
||||||
|
case "github-env":
|
||||||
|
await mutateAsync({
|
||||||
|
integrationAuthId: integrationAuth?.id,
|
||||||
|
isActive: true,
|
||||||
|
secretPath: data.secretPath,
|
||||||
|
sourceEnvironment: data.selectedSourceEnvironment,
|
||||||
|
scope: data.scope,
|
||||||
|
app: repoName,
|
||||||
|
appId: data.repoId,
|
||||||
|
owner: repoOwner,
|
||||||
|
targetEnvironmentId: data.envId,
|
||||||
|
metadata: {
|
||||||
|
secretSuffix: data.secretSuffix,
|
||||||
|
shouldEnableDelete: data.shouldEnableDelete
|
||||||
|
}
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
throw new Error("Invalid scope");
|
||||||
|
}
|
||||||
|
|
||||||
|
setIsLoading(false);
|
||||||
|
navigate({
|
||||||
|
to: "/secret-manager/$projectId/integrations",
|
||||||
|
params: {
|
||||||
|
projectId: currentWorkspace.id
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err);
|
||||||
|
|
||||||
|
let errorMessage: string = "Something went wrong!";
|
||||||
|
if (axios.isAxiosError(err)) {
|
||||||
|
const { message } = err?.response?.data as { message: string };
|
||||||
|
errorMessage = message;
|
||||||
|
}
|
||||||
|
|
||||||
|
createNotification({
|
||||||
|
text: errorMessage,
|
||||||
|
type: "error"
|
||||||
|
});
|
||||||
|
setIsLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const selectedOrganization = useMemo(() => {
|
||||||
|
if (!integrationAuthApps) return null;
|
||||||
|
|
||||||
|
return integrationAuthApps.filter(
|
||||||
|
(authApp) =>
|
||||||
|
integrationAuthOrgs?.find((e) => e.orgId === selectedOrgId)?.name === authApp.owner
|
||||||
|
);
|
||||||
|
}, [selectedOrgId, integrationAuthApps]);
|
||||||
|
|
||||||
|
return integrationAuth && integrationAuthApps ? (
|
||||||
|
<div className="flex h-full w-full flex-col items-center justify-center py-4">
|
||||||
|
<Helmet>
|
||||||
|
<title>Set Up GitHub Integration</title>
|
||||||
|
</Helmet>
|
||||||
|
<Card className="max-w-lg rounded-md border border-mineshaft-600 p-0">
|
||||||
|
<form onSubmit={handleSubmit(onFormSubmit)} className="px-6">
|
||||||
|
<CardTitle
|
||||||
|
className="px-0 text-left text-xl"
|
||||||
|
subTitle="Choose which environment in Infisical you want to sync to environment variables in GitHub."
|
||||||
|
>
|
||||||
|
<div className="flex flex-row items-center">
|
||||||
|
<div className="flex items-center rounded-full bg-mineshaft-200">
|
||||||
|
<img
|
||||||
|
src="/images/integrations/GitHub.png"
|
||||||
|
height={30}
|
||||||
|
width={30}
|
||||||
|
alt="GitHub logo"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<span className="ml-2.5">GitHub Integration </span>
|
||||||
|
<a
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
href="https://infisical.com/docs/integrations/cicd/githubactions"
|
||||||
|
>
|
||||||
|
<div className="mb-1 ml-2 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="mb-[0.07rem] ml-1.5 text-xxs"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</CardTitle>
|
||||||
|
<Tabs defaultValue={TabSections.Connection}>
|
||||||
|
<TabList>
|
||||||
|
<div className="flex w-full flex-row border-b border-mineshaft-600">
|
||||||
|
<Tab value={TabSections.Connection}>Connection</Tab>
|
||||||
|
<Tab value={TabSections.Options}>Options</Tab>
|
||||||
|
</div>
|
||||||
|
</TabList>
|
||||||
|
<TabPanel value={TabSections.Connection}>
|
||||||
|
<motion.div
|
||||||
|
key="panel-1"
|
||||||
|
transition={{ duration: 0.15 }}
|
||||||
|
initial={{ opacity: 0, translateX: 30 }}
|
||||||
|
animate={{ opacity: 1, translateX: 0 }}
|
||||||
|
exit={{ opacity: 0, translateX: 30 }}
|
||||||
|
>
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
|
name="selectedSourceEnvironment"
|
||||||
|
render={({ field: { onChange, ...field }, fieldState: { error } }) => (
|
||||||
|
<FormControl
|
||||||
|
label="Project Environment"
|
||||||
|
errorText={error?.message}
|
||||||
|
isError={Boolean(error)}
|
||||||
|
>
|
||||||
|
<Select
|
||||||
|
defaultValue={field.value}
|
||||||
|
onValueChange={onChange}
|
||||||
|
className="w-full border border-mineshaft-500"
|
||||||
|
>
|
||||||
|
{currentWorkspace?.environments.map((sourceEnvironment) => (
|
||||||
|
<SelectItem
|
||||||
|
value={sourceEnvironment.slug}
|
||||||
|
key={`source-environment-${sourceEnvironment.slug}`}
|
||||||
|
>
|
||||||
|
{sourceEnvironment.name}
|
||||||
|
</SelectItem>
|
||||||
|
))}
|
||||||
|
</Select>
|
||||||
|
</FormControl>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
|
name="secretPath"
|
||||||
|
render={({ field, fieldState: { error } }) => (
|
||||||
|
<FormControl
|
||||||
|
label="Secrets Path"
|
||||||
|
errorText={error?.message}
|
||||||
|
isError={Boolean(error)}
|
||||||
|
>
|
||||||
|
<Input {...field} placeholder="Provide a path, default is /" />
|
||||||
|
</FormControl>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
|
name="scope"
|
||||||
|
render={({ field: { onChange, ...field }, fieldState: { error } }) => (
|
||||||
|
<FormControl label="Scope" errorText={error?.message} isError={Boolean(error)}>
|
||||||
|
<Select
|
||||||
|
defaultValue={field.value}
|
||||||
|
onValueChange={(e) => {
|
||||||
|
setValue("repoIds", []);
|
||||||
|
onChange(e);
|
||||||
|
}}
|
||||||
|
className="w-full border border-mineshaft-500"
|
||||||
|
>
|
||||||
|
<SelectItem value="github-org">Organization</SelectItem>
|
||||||
|
<SelectItem value="github-repo">Repository</SelectItem>
|
||||||
|
<SelectItem value="github-env">Repository Environment</SelectItem>
|
||||||
|
</Select>
|
||||||
|
</FormControl>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
{scope === "github-repo" && (
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
|
name="repoIds"
|
||||||
|
render={({ field: { onChange }, fieldState: { error } }) => (
|
||||||
|
<FormControl
|
||||||
|
label="Repositories"
|
||||||
|
isError={Boolean(error?.message)}
|
||||||
|
errorText={error?.message}
|
||||||
|
>
|
||||||
|
<DropdownMenu>
|
||||||
|
<DropdownMenuTrigger asChild>
|
||||||
|
{integrationAuthApps.length > 0 ? (
|
||||||
|
<div className="inline-flex w-full cursor-pointer items-center justify-between rounded-md border border-mineshaft-600 bg-mineshaft-900 px-3 py-2 font-inter text-sm font-normal text-bunker-200 outline-none data-[placeholder]:text-mineshaft-200">
|
||||||
|
{repoIds?.length === 1
|
||||||
|
? integrationAuthApps?.reduce(
|
||||||
|
(acc, { appId, name, owner }) =>
|
||||||
|
repoIds?.[0] === appId ? `${owner}/${name}` : acc,
|
||||||
|
""
|
||||||
|
)
|
||||||
|
: `${repoIds?.length} repositories selected`}
|
||||||
|
<FontAwesomeIcon icon={faAngleDown} className="text-xs" />
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="inline-flex w-full cursor-default items-center justify-between rounded-md border border-mineshaft-600 bg-mineshaft-900 px-3 py-2 font-inter text-sm font-normal text-bunker-200 outline-none data-[placeholder]:text-mineshaft-200">
|
||||||
|
No repositories found
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</DropdownMenuTrigger>
|
||||||
|
<DropdownMenuContent
|
||||||
|
align="start"
|
||||||
|
className="thin-scrollbar z-[100] max-h-80 overflow-y-scroll"
|
||||||
|
>
|
||||||
|
{integrationAuthApps.length > 0 ? (
|
||||||
|
integrationAuthApps.map((integrationAuthApp) => {
|
||||||
|
const isSelected = repoIds?.includes(
|
||||||
|
String(integrationAuthApp.appId)
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<DropdownMenuItem
|
||||||
|
onClick={() => {
|
||||||
|
if (repoIds?.includes(String(integrationAuthApp.appId))) {
|
||||||
|
onChange(
|
||||||
|
repoIds.filter(
|
||||||
|
(appId: string) =>
|
||||||
|
appId !== String(integrationAuthApp.appId)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
onChange([
|
||||||
|
...(repoIds || []),
|
||||||
|
String(integrationAuthApp.appId)
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
key={`repos-id-${integrationAuthApp.appId}`}
|
||||||
|
icon={
|
||||||
|
isSelected ? (
|
||||||
|
<FontAwesomeIcon
|
||||||
|
icon={faCheckCircle}
|
||||||
|
className="pr-0.5 text-primary"
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<div className="pl-[1.01rem]" />
|
||||||
|
)
|
||||||
|
}
|
||||||
|
iconPos="left"
|
||||||
|
className="w-[28.4rem] text-sm"
|
||||||
|
>
|
||||||
|
{integrationAuthApp.owner}/{integrationAuthApp.name}
|
||||||
|
</DropdownMenuItem>
|
||||||
|
);
|
||||||
|
})
|
||||||
|
) : (
|
||||||
|
<div />
|
||||||
|
)}
|
||||||
|
</DropdownMenuContent>
|
||||||
|
</DropdownMenu>
|
||||||
|
</FormControl>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{scope === "github-org" && (
|
||||||
|
<>
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
|
name="orgId"
|
||||||
|
render={({ field: { onChange, ...field }, fieldState: { error } }) => (
|
||||||
|
<FormControl
|
||||||
|
label="Organization"
|
||||||
|
errorText={
|
||||||
|
integrationAuthOrgs?.length ? error?.message : "No organizations found"
|
||||||
|
}
|
||||||
|
isError={Boolean(integrationAuthOrgs?.length || error?.message)}
|
||||||
|
>
|
||||||
|
<Select
|
||||||
|
value={field.value}
|
||||||
|
onValueChange={onChange}
|
||||||
|
className="w-full border border-mineshaft-500"
|
||||||
|
>
|
||||||
|
{integrationAuthOrgs &&
|
||||||
|
integrationAuthOrgs.map(({ name, orgId }) => (
|
||||||
|
<SelectItem key={`github-organization-${orgId}`} value={orgId}>
|
||||||
|
{name}
|
||||||
|
</SelectItem>
|
||||||
|
))}
|
||||||
|
</Select>
|
||||||
|
</FormControl>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
|
name="visibility"
|
||||||
|
render={({ field: { onChange, ...field }, fieldState: { error } }) => (
|
||||||
|
<FormControl
|
||||||
|
tooltipText="Select which type of repositories that the secrets should be synced to."
|
||||||
|
label="Visibility"
|
||||||
|
errorText={error?.message}
|
||||||
|
isError={Boolean(error?.message)}
|
||||||
|
>
|
||||||
|
<Select
|
||||||
|
value={field.value}
|
||||||
|
onValueChange={onChange}
|
||||||
|
className="w-full border border-mineshaft-500"
|
||||||
|
>
|
||||||
|
{secretsVisibility.map(({ label, value }) => (
|
||||||
|
<SelectItem key={`github-visibility-${value}`} value={value}>
|
||||||
|
{label}
|
||||||
|
</SelectItem>
|
||||||
|
))}
|
||||||
|
</Select>
|
||||||
|
</FormControl>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{watch("visibility") === "selected" && (
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
|
name="repoIds"
|
||||||
|
render={({ field: { onChange }, fieldState: { error } }) => (
|
||||||
|
<FormControl
|
||||||
|
label="Selected Repositories"
|
||||||
|
isError={Boolean(error?.message)}
|
||||||
|
errorText={error?.message}
|
||||||
|
>
|
||||||
|
<DropdownMenu>
|
||||||
|
<DropdownMenuTrigger asChild>
|
||||||
|
{integrationAuthApps.length > 0 ? (
|
||||||
|
<div className="inline-flex w-full cursor-pointer items-center justify-between rounded-md border border-mineshaft-600 bg-mineshaft-900 px-3 py-2 font-inter text-sm font-normal text-bunker-200 outline-none data-[placeholder]:text-mineshaft-200">
|
||||||
|
{repoIds?.length === 1
|
||||||
|
? integrationAuthApps?.reduce(
|
||||||
|
(acc, { appId, name, owner }) =>
|
||||||
|
repoIds?.[0] === appId ? `${owner}/${name}` : acc,
|
||||||
|
""
|
||||||
|
)
|
||||||
|
: `${repoIds?.length} repositories selected`}
|
||||||
|
<FontAwesomeIcon icon={faAngleDown} className="text-xs" />
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="inline-flex w-full cursor-default items-center justify-between rounded-md border border-mineshaft-600 bg-mineshaft-900 px-3 py-2 font-inter text-sm font-normal text-bunker-200 outline-none data-[placeholder]:text-mineshaft-200">
|
||||||
|
No repositories found
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</DropdownMenuTrigger>
|
||||||
|
<DropdownMenuContent
|
||||||
|
align="start"
|
||||||
|
className="thin-scrollbar z-[100] max-h-80 overflow-y-scroll"
|
||||||
|
>
|
||||||
|
{selectedOrganization ? (
|
||||||
|
selectedOrganization.map((integrationAuthApp) => {
|
||||||
|
const isSelected = repoIds?.includes(
|
||||||
|
String(integrationAuthApp.appId)
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<DropdownMenuItem
|
||||||
|
onClick={() => {
|
||||||
|
if (repoIds?.includes(String(integrationAuthApp.appId))) {
|
||||||
|
onChange(
|
||||||
|
repoIds?.filter(
|
||||||
|
(appId: string) =>
|
||||||
|
appId !== String(integrationAuthApp.appId)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
onChange([
|
||||||
|
...repoIds,
|
||||||
|
String(integrationAuthApp.appId)
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
key={`repos-id-${integrationAuthApp.appId}`}
|
||||||
|
icon={
|
||||||
|
isSelected ? (
|
||||||
|
<FontAwesomeIcon
|
||||||
|
icon={faCheckCircle}
|
||||||
|
className="pr-0.5 text-primary"
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<div className="pl-[1.01rem]" />
|
||||||
|
)
|
||||||
|
}
|
||||||
|
iconPos="left"
|
||||||
|
className="w-[28.4rem] text-sm"
|
||||||
|
>
|
||||||
|
{integrationAuthApp.owner}/{integrationAuthApp.name}
|
||||||
|
</DropdownMenuItem>
|
||||||
|
);
|
||||||
|
})
|
||||||
|
) : (
|
||||||
|
<div />
|
||||||
|
)}
|
||||||
|
</DropdownMenuContent>
|
||||||
|
</DropdownMenu>
|
||||||
|
</FormControl>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
{scope === "github-env" && (
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
|
name="repoId"
|
||||||
|
render={({ field: { onChange, ...field }, fieldState: { error } }) => (
|
||||||
|
<FormControl
|
||||||
|
label="Repository"
|
||||||
|
errorText={error?.message}
|
||||||
|
isError={Boolean(error)}
|
||||||
|
>
|
||||||
|
<Select
|
||||||
|
value={field.value}
|
||||||
|
onValueChange={(e) => {
|
||||||
|
const selectedRepo = integrationAuthApps.find((app) => app.appId === e);
|
||||||
|
setValue("repoName", selectedRepo?.name);
|
||||||
|
setValue("repoOwner", selectedRepo?.owner);
|
||||||
|
onChange(e);
|
||||||
|
}}
|
||||||
|
className="w-full border border-mineshaft-500"
|
||||||
|
>
|
||||||
|
{integrationAuthApps?.length ? (
|
||||||
|
integrationAuthApps.map((app) => {
|
||||||
|
return (
|
||||||
|
<SelectItem
|
||||||
|
value={app.appId as string}
|
||||||
|
key={`repo-id-${app.appId}`}
|
||||||
|
className="w-[28.4rem] text-sm"
|
||||||
|
>
|
||||||
|
{app.owner}/{app.name}
|
||||||
|
</SelectItem>
|
||||||
|
);
|
||||||
|
})
|
||||||
|
) : (
|
||||||
|
<div />
|
||||||
|
)}
|
||||||
|
</Select>
|
||||||
|
</FormControl>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{scope === "github-env" && (
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
|
name="envId"
|
||||||
|
render={({ field: { onChange, ...field }, fieldState: { error } }) => (
|
||||||
|
<FormControl
|
||||||
|
label="Environment"
|
||||||
|
errorText={
|
||||||
|
integrationAuthGithubEnvs?.length
|
||||||
|
? error?.message
|
||||||
|
: "No Environment found"
|
||||||
|
}
|
||||||
|
isError={Boolean(integrationAuthGithubEnvs?.length || error?.message)}
|
||||||
|
>
|
||||||
|
<Select
|
||||||
|
value={field.value}
|
||||||
|
onValueChange={onChange}
|
||||||
|
isDisabled={!repoId}
|
||||||
|
className={twMerge(
|
||||||
|
"w-full border border-mineshaft-500",
|
||||||
|
!repoId && "h-10 cursor-not-allowed"
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{integrationAuthGithubEnvs?.length ? (
|
||||||
|
integrationAuthGithubEnvs.map((githubEnv) => {
|
||||||
|
return (
|
||||||
|
<SelectItem
|
||||||
|
value={githubEnv.name as string}
|
||||||
|
key={`env-id-${githubEnv.envId}`}
|
||||||
|
className="w-[28.4rem] text-sm"
|
||||||
|
>
|
||||||
|
{githubEnv.name}
|
||||||
|
</SelectItem>
|
||||||
|
);
|
||||||
|
})
|
||||||
|
) : (
|
||||||
|
<div />
|
||||||
|
)}
|
||||||
|
</Select>
|
||||||
|
</FormControl>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</motion.div>
|
||||||
|
</TabPanel>
|
||||||
|
<TabPanel value={TabSections.Options}>
|
||||||
|
<motion.div
|
||||||
|
key="panel-1"
|
||||||
|
transition={{ duration: 0.15 }}
|
||||||
|
initial={{ opacity: 0, translateX: -30 }}
|
||||||
|
animate={{ opacity: 1, translateX: 0 }}
|
||||||
|
exit={{ opacity: 0, translateX: 30 }}
|
||||||
|
>
|
||||||
|
<div className="mb-5 ml-1">
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
|
name="shouldEnableDelete"
|
||||||
|
render={({ field: { onChange, value } }) => (
|
||||||
|
<Switch
|
||||||
|
id="delete-github-option"
|
||||||
|
onCheckedChange={(isChecked) => onChange(isChecked)}
|
||||||
|
isChecked={value}
|
||||||
|
>
|
||||||
|
Delete secrets in Github that are not in Infisical
|
||||||
|
</Switch>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
|
name="secretSuffix"
|
||||||
|
render={({ field, fieldState: { error } }) => (
|
||||||
|
<FormControl
|
||||||
|
label="Append Secret Names with..."
|
||||||
|
className="pb-[9.75rem]"
|
||||||
|
errorText={error?.message}
|
||||||
|
isError={Boolean(error)}
|
||||||
|
>
|
||||||
|
<Input
|
||||||
|
{...field}
|
||||||
|
placeholder="Provide a suffix for secret names, default is no suffix"
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</motion.div>
|
||||||
|
</TabPanel>
|
||||||
|
</Tabs>
|
||||||
|
<div className="flex w-full justify-end">
|
||||||
|
<Button
|
||||||
|
type="submit"
|
||||||
|
color="mineshaft"
|
||||||
|
variant="outline_bg"
|
||||||
|
className="mb-6"
|
||||||
|
isLoading={isLoading}
|
||||||
|
>
|
||||||
|
Create Integration
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</Card>
|
||||||
|
<div className="mt-6 w-full max-w-md border-t border-mineshaft-800" />
|
||||||
|
<div className="mt-6 flex w-full max-w-lg flex-col rounded-md border border-mineshaft-600 bg-mineshaft-800 p-4">
|
||||||
|
<div className="flex flex-row items-center">
|
||||||
|
<FontAwesomeIcon icon={faCircleInfo} className="text-xl text-mineshaft-200" />{" "}
|
||||||
|
<span className="text-md ml-3 text-mineshaft-100">Pro Tip</span>
|
||||||
|
</div>
|
||||||
|
<span className="mt-4 text-sm text-mineshaft-300">
|
||||||
|
After creating an integration, your secrets will start syncing immediately. This might
|
||||||
|
cause an unexpected override of current secrets in GitHub with secrets from Infisical.
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="flex h-full w-full items-center justify-center">
|
||||||
|
<Helmet>
|
||||||
|
<title>Set Up GitHub Integration</title>
|
||||||
|
</Helmet>
|
||||||
|
{isIntegrationAuthAppsLoading ? (
|
||||||
|
<img
|
||||||
|
src="/images/loading/loading.gif"
|
||||||
|
height={70}
|
||||||
|
width={120}
|
||||||
|
alt="infisical loading indicator"
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<div className="flex h-max max-w-md flex-col rounded-md border border-mineshaft-600 bg-mineshaft-800 p-6 text-center text-mineshaft-200">
|
||||||
|
<FontAwesomeIcon icon={faBugs} className="li my-2 inline text-6xl" />
|
||||||
|
<p>
|
||||||
|
Something went wrong. Please contact{" "}
|
||||||
|
<a
|
||||||
|
className="inline cursor-pointer text-mineshaft-100 underline decoration-primary-500 underline-offset-4 opacity-80 duration-200 hover:opacity-100"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
href="mailto:[email protected]"
|
||||||
|
>
|
||||||
|
support@infisical.com
|
||||||
|
</a>{" "}
|
||||||
|
if the issue persists.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
import { createFileRoute } from "@tanstack/react-router";
|
||||||
|
import { zodValidator } from "@tanstack/zod-adapter";
|
||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
import { GithubConfigurePage } from "./GithubConfigurePage";
|
||||||
|
|
||||||
|
const GithubConfigurePageQueryParamsSchema = z.object({
|
||||||
|
integrationAuthId: z.string()
|
||||||
|
});
|
||||||
|
|
||||||
|
export const Route = createFileRoute(
|
||||||
|
"/_authenticate/_inject-org-details/secret-manager/$projectId/_secret-manager-layout/integrations/github/create"
|
||||||
|
)({
|
||||||
|
component: GithubConfigurePage,
|
||||||
|
validateSearch: zodValidator(GithubConfigurePageQueryParamsSchema)
|
||||||
|
});
|
||||||
+54
@@ -0,0 +1,54 @@
|
|||||||
|
import { useEffect } from "react";
|
||||||
|
import { useNavigate, useSearch } from "@tanstack/react-router";
|
||||||
|
|
||||||
|
import { ROUTE_PATHS } from "@app/const/routes";
|
||||||
|
import { useWorkspace } from "@app/context";
|
||||||
|
import { useAuthorizeIntegration } from "@app/hooks/api";
|
||||||
|
|
||||||
|
export const GithubOauthCallbackPage = () => {
|
||||||
|
const navigate = useNavigate();
|
||||||
|
const { mutateAsync } = useAuthorizeIntegration();
|
||||||
|
|
||||||
|
const {
|
||||||
|
code,
|
||||||
|
state,
|
||||||
|
installation_id: installationId
|
||||||
|
} = useSearch({
|
||||||
|
from: ROUTE_PATHS.SecretManager.Integratons.GithubOauthCallbackPage.id
|
||||||
|
});
|
||||||
|
const { currentWorkspace } = useWorkspace();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
(async () => {
|
||||||
|
try {
|
||||||
|
// validate state
|
||||||
|
if (state !== localStorage.getItem("latestCSRFToken")) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
localStorage.removeItem("latestCSRFToken");
|
||||||
|
|
||||||
|
const integrationAuth = await mutateAsync({
|
||||||
|
workspaceId: currentWorkspace.id,
|
||||||
|
code: code as string,
|
||||||
|
installationId,
|
||||||
|
integration: "github"
|
||||||
|
});
|
||||||
|
|
||||||
|
navigate({
|
||||||
|
to: "/secret-manager/$projectId/integrations/github/create",
|
||||||
|
params: {
|
||||||
|
projectId: currentWorkspace.id
|
||||||
|
},
|
||||||
|
search: {
|
||||||
|
integrationAuthId: integrationAuth.id
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err);
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return <div />;
|
||||||
|
};
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
import { createFileRoute } from "@tanstack/react-router";
|
||||||
|
import { zodValidator } from "@tanstack/zod-adapter";
|
||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
import { GithubOauthCallbackPage } from "./GithubOauthCallbackPage";
|
||||||
|
|
||||||
|
export const GithubOAuthCallbackPageQueryParamsSchema = z.object({
|
||||||
|
state: z.string().catch(""),
|
||||||
|
installation_id: z.string(),
|
||||||
|
code: z.string()
|
||||||
|
});
|
||||||
|
|
||||||
|
export const Route = createFileRoute(
|
||||||
|
"/_authenticate/_inject-org-details/secret-manager/$projectId/_secret-manager-layout/integrations/github/oauth2/callback"
|
||||||
|
)({
|
||||||
|
component: GithubOauthCallbackPage,
|
||||||
|
validateSearch: zodValidator(GithubOAuthCallbackPageQueryParamsSchema)
|
||||||
|
});
|
||||||
+121
@@ -0,0 +1,121 @@
|
|||||||
|
import crypto from "crypto";
|
||||||
|
|
||||||
|
import { Helmet } from "react-helmet";
|
||||||
|
import { Controller, useForm } from "react-hook-form";
|
||||||
|
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 { Button, Card, CardTitle, FormControl, Input } from "@app/components/v2";
|
||||||
|
import { useWorkspace } from "@app/context";
|
||||||
|
import { localStorageService } from "@app/helpers/localStorage";
|
||||||
|
import { useGetCloudIntegrations } from "@app/hooks/api";
|
||||||
|
|
||||||
|
import { createIntegrationMissingEnvVarsNotification } from "../../IntegrationsListPage/IntegrationsListPage.utils";
|
||||||
|
|
||||||
|
const schema = z.object({
|
||||||
|
gitLabURL: z.string()
|
||||||
|
});
|
||||||
|
|
||||||
|
type FormData = z.infer<typeof schema>;
|
||||||
|
|
||||||
|
export const GitlabAuthorizePage = () => {
|
||||||
|
const { control, handleSubmit } = useForm<FormData>({
|
||||||
|
resolver: zodResolver(schema),
|
||||||
|
defaultValues: {
|
||||||
|
gitLabURL: ""
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const { data: cloudIntegrations } = useGetCloudIntegrations();
|
||||||
|
const { currentWorkspace } = useWorkspace();
|
||||||
|
|
||||||
|
const onFormSubmit = ({ gitLabURL }: FormData) => {
|
||||||
|
if (!cloudIntegrations) return;
|
||||||
|
const integrationOption = cloudIntegrations.find(
|
||||||
|
(integration) => integration.slug === "gitlab"
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!integrationOption) return;
|
||||||
|
|
||||||
|
if (!integrationOption.clientId) {
|
||||||
|
createIntegrationMissingEnvVarsNotification(integrationOption.slug, "cicd");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const baseURL =
|
||||||
|
(gitLabURL as string).trim() === "" ? "https://gitlab.com" : (gitLabURL as string).trim();
|
||||||
|
|
||||||
|
const csrfToken = crypto.randomBytes(16).toString("hex");
|
||||||
|
localStorage.setItem("latestCSRFToken", csrfToken);
|
||||||
|
localStorageService.setIntegrationProjectId(currentWorkspace.id);
|
||||||
|
|
||||||
|
const state = `${csrfToken}|${
|
||||||
|
(gitLabURL as string).trim() === "" ? "" : (gitLabURL as string).trim()
|
||||||
|
}`;
|
||||||
|
localStorageService.setIntegrationProjectId(currentWorkspace.id);
|
||||||
|
const link = `${baseURL}/oauth/authorize?client_id=${integrationOption.clientId}&redirect_uri=${window.location.origin}/integrations/gitlab/oauth2/callback&response_type=code&state=${state}`;
|
||||||
|
|
||||||
|
window.location.assign(link);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex h-full w-full items-center justify-center">
|
||||||
|
<Helmet>
|
||||||
|
<title>Authorize GitLab Integration</title>
|
||||||
|
</Helmet>
|
||||||
|
<Card className="mb-12 max-w-lg rounded-md border border-mineshaft-600">
|
||||||
|
<CardTitle
|
||||||
|
className="px-6 text-left text-xl"
|
||||||
|
subTitle="Authorize this integration to sync secrets from Infisical to GitLab. If no self-hosted GitLab URL is specified, then Infisical will connect you to GitLab Cloud."
|
||||||
|
>
|
||||||
|
<div className="flex flex-row items-center">
|
||||||
|
<div className="flex items-center pb-0.5">
|
||||||
|
<img src="/images/integrations/Gitlab.png" height={28} width={28} alt="Gitlab logo" />
|
||||||
|
</div>
|
||||||
|
<span className="ml-2.5">GitLab Integration </span>
|
||||||
|
<a
|
||||||
|
href="https://infisical.com/docs/integrations/cicd/gitlab"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
>
|
||||||
|
<div className="mb-1 ml-2 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="mb-[0.07rem] ml-1.5 text-xxs"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</CardTitle>
|
||||||
|
<form onSubmit={handleSubmit(onFormSubmit)} className="px-6 pb-8 text-right">
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
|
name="gitLabURL"
|
||||||
|
render={({ field, fieldState: { error } }) => (
|
||||||
|
<FormControl
|
||||||
|
label="Self-hosted URL (optional)"
|
||||||
|
errorText={error?.message}
|
||||||
|
isError={Boolean(error)}
|
||||||
|
>
|
||||||
|
<Input {...field} placeholder="https://self-hosted-gitlab.com" />
|
||||||
|
</FormControl>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<Button
|
||||||
|
colorSchema="primary"
|
||||||
|
variant="outline_bg"
|
||||||
|
className="mt-2 w-min"
|
||||||
|
size="sm"
|
||||||
|
type="submit"
|
||||||
|
>
|
||||||
|
Continue with OAuth
|
||||||
|
</Button>
|
||||||
|
</form>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
import { createFileRoute } from "@tanstack/react-router";
|
||||||
|
|
||||||
|
import { GitlabAuthorizePage } from "./GitlabAuthorizePage";
|
||||||
|
|
||||||
|
export const Route = createFileRoute(
|
||||||
|
"/_authenticate/_inject-org-details/secret-manager/$projectId/_secret-manager-layout/integrations/gitlab/authorize"
|
||||||
|
)({
|
||||||
|
component: GitlabAuthorizePage
|
||||||
|
});
|
||||||
+530
@@ -0,0 +1,530 @@
|
|||||||
|
import { useEffect, useState } from "react";
|
||||||
|
import { Helmet } from "react-helmet";
|
||||||
|
import { Controller, useForm } from "react-hook-form";
|
||||||
|
import { faArrowUpRightFromSquare, faBookOpen, faBugs } from "@fortawesome/free-solid-svg-icons";
|
||||||
|
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||||
|
import { yupResolver } from "@hookform/resolvers/yup";
|
||||||
|
import { useNavigate, useSearch } from "@tanstack/react-router";
|
||||||
|
import { motion } from "framer-motion";
|
||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
import {
|
||||||
|
Button,
|
||||||
|
Card,
|
||||||
|
CardTitle,
|
||||||
|
FormControl,
|
||||||
|
Input,
|
||||||
|
Modal,
|
||||||
|
ModalContent,
|
||||||
|
Select,
|
||||||
|
SelectItem,
|
||||||
|
Switch,
|
||||||
|
Tab,
|
||||||
|
TabList,
|
||||||
|
TabPanel,
|
||||||
|
Tabs
|
||||||
|
} from "@app/components/v2";
|
||||||
|
import { SecretPathInput } from "@app/components/v2/SecretPathInput";
|
||||||
|
import { ROUTE_PATHS } from "@app/const/routes";
|
||||||
|
import { useWorkspace } from "@app/context";
|
||||||
|
import { usePopUp } from "@app/hooks";
|
||||||
|
import { useCreateIntegration } from "@app/hooks/api";
|
||||||
|
import {
|
||||||
|
useGetIntegrationAuthApps,
|
||||||
|
useGetIntegrationAuthById,
|
||||||
|
useGetIntegrationAuthTeams
|
||||||
|
} from "@app/hooks/api/integrationAuth";
|
||||||
|
|
||||||
|
const gitLabEntities = [
|
||||||
|
{ name: "Individual", value: "individual" },
|
||||||
|
{ name: "Group", value: "group" }
|
||||||
|
] as const;
|
||||||
|
|
||||||
|
enum TabSections {
|
||||||
|
Connection = "connection",
|
||||||
|
Options = "options"
|
||||||
|
}
|
||||||
|
|
||||||
|
const schema = z.object({
|
||||||
|
targetEntity: z.enum([gitLabEntities[0].value, gitLabEntities[1].value]),
|
||||||
|
targetTeamId: z.string().optional(),
|
||||||
|
selectedSourceEnvironment: z.string(),
|
||||||
|
secretPath: z.string(),
|
||||||
|
targetAppId: z.string(),
|
||||||
|
targetEnvironment: z.string().optional(),
|
||||||
|
secretPrefix: z.string().optional(),
|
||||||
|
secretSuffix: z.string().optional(),
|
||||||
|
shouldMaskSecrets: z.boolean().optional(),
|
||||||
|
shouldProtectSecrets: z.boolean()
|
||||||
|
});
|
||||||
|
|
||||||
|
type FormData = z.infer<typeof schema>;
|
||||||
|
|
||||||
|
export const GitlabConfigurePage = () => {
|
||||||
|
const navigate = useNavigate();
|
||||||
|
const { popUp, handlePopUpOpen, handlePopUpToggle, handlePopUpClose } = usePopUp([
|
||||||
|
"confirmIntegration"
|
||||||
|
] as const);
|
||||||
|
const { currentWorkspace } = useWorkspace();
|
||||||
|
|
||||||
|
const { control, handleSubmit, setValue, watch } = useForm<FormData>({
|
||||||
|
resolver: yupResolver(schema),
|
||||||
|
defaultValues: {
|
||||||
|
targetEntity: "individual",
|
||||||
|
secretPath: "/",
|
||||||
|
secretPrefix: "",
|
||||||
|
secretSuffix: "",
|
||||||
|
selectedSourceEnvironment: currentWorkspace.environments[0].slug
|
||||||
|
}
|
||||||
|
});
|
||||||
|
const selectedSourceEnvironment = watch("selectedSourceEnvironment");
|
||||||
|
const targetEntity = watch("targetEntity");
|
||||||
|
const targetTeamId = watch("targetTeamId");
|
||||||
|
const targetAppIdValue = watch("targetAppId");
|
||||||
|
|
||||||
|
const { mutateAsync } = useCreateIntegration();
|
||||||
|
|
||||||
|
const integrationAuthId = useSearch({
|
||||||
|
from: ROUTE_PATHS.SecretManager.Integratons.GitlabConfigurePage.id,
|
||||||
|
select: (el) => el.integrationAuthId
|
||||||
|
});
|
||||||
|
|
||||||
|
const { data: integrationAuth } = useGetIntegrationAuthById((integrationAuthId as string) ?? "");
|
||||||
|
|
||||||
|
const { data: integrationAuthApps, isLoading: isIntegrationAuthAppsLoading } =
|
||||||
|
useGetIntegrationAuthApps({
|
||||||
|
integrationAuthId: (integrationAuthId as string) ?? "",
|
||||||
|
...(targetTeamId ? { teamId: targetTeamId } : {})
|
||||||
|
});
|
||||||
|
const { data: integrationAuthTeams, isLoading: isintegrationAuthTeamsLoading } =
|
||||||
|
useGetIntegrationAuthTeams((integrationAuthId as string) ?? "");
|
||||||
|
|
||||||
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (integrationAuthApps) {
|
||||||
|
if (integrationAuthApps.length > 0) {
|
||||||
|
setValue("targetAppId", String(integrationAuthApps[0].appId as string));
|
||||||
|
} else {
|
||||||
|
setValue("targetAppId", "none");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, [integrationAuthApps]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (targetEntity === "group" && integrationAuthTeams && integrationAuthTeams.length > 0) {
|
||||||
|
if (integrationAuthTeams) {
|
||||||
|
if (integrationAuthTeams.length > 0) {
|
||||||
|
// case: user is part of at least 1 group in GitLab
|
||||||
|
setValue("targetTeamId", String(integrationAuthTeams[0].id));
|
||||||
|
} else {
|
||||||
|
// case: user is not part of any groups in GitLab
|
||||||
|
setValue("targetTeamId", "none");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if (targetEntity === "individual") {
|
||||||
|
setValue("targetTeamId", undefined);
|
||||||
|
}
|
||||||
|
}, [targetEntity, integrationAuthTeams]);
|
||||||
|
|
||||||
|
const onFormSubmit = async ({
|
||||||
|
selectedSourceEnvironment: sse,
|
||||||
|
secretPath,
|
||||||
|
targetAppId,
|
||||||
|
targetEnvironment,
|
||||||
|
secretPrefix,
|
||||||
|
secretSuffix,
|
||||||
|
shouldMaskSecrets,
|
||||||
|
shouldProtectSecrets
|
||||||
|
}: FormData) => {
|
||||||
|
try {
|
||||||
|
setIsLoading(true);
|
||||||
|
if (!integrationAuth?.id) return;
|
||||||
|
|
||||||
|
await mutateAsync({
|
||||||
|
integrationAuthId: integrationAuth?.id,
|
||||||
|
isActive: true,
|
||||||
|
app: integrationAuthApps?.find(
|
||||||
|
(integrationAuthApp) => String(integrationAuthApp.appId) === targetAppId
|
||||||
|
)?.name,
|
||||||
|
appId: String(targetAppId),
|
||||||
|
sourceEnvironment: sse,
|
||||||
|
targetEnvironment: targetEnvironment === "" ? "*" : targetEnvironment,
|
||||||
|
secretPath,
|
||||||
|
metadata: {
|
||||||
|
secretPrefix,
|
||||||
|
secretSuffix,
|
||||||
|
shouldMaskSecrets,
|
||||||
|
shouldProtectSecrets
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
setIsLoading(false);
|
||||||
|
navigate({
|
||||||
|
to: "/secret-manager/$projectId/integrations",
|
||||||
|
params: {
|
||||||
|
projectId: currentWorkspace.id
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err);
|
||||||
|
setIsLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return integrationAuth &&
|
||||||
|
selectedSourceEnvironment &&
|
||||||
|
integrationAuthApps &&
|
||||||
|
integrationAuthTeams ? (
|
||||||
|
<form
|
||||||
|
onSubmit={handleSubmit((data: FormData) => {
|
||||||
|
if (!data.secretPrefix && !data.secretSuffix) {
|
||||||
|
handlePopUpOpen("confirmIntegration", data);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
onFormSubmit(data);
|
||||||
|
})}
|
||||||
|
className="flex h-full w-full flex-col items-center justify-center"
|
||||||
|
>
|
||||||
|
<Helmet>
|
||||||
|
<title>Set Up GitLab Integration</title>
|
||||||
|
</Helmet>
|
||||||
|
<Card className="max-w-lg rounded-md border border-mineshaft-600">
|
||||||
|
<CardTitle
|
||||||
|
className="px-6 text-left text-xl"
|
||||||
|
subTitle="Select which environment or folder in Infisical you want to sync to GitLab's environment variables."
|
||||||
|
>
|
||||||
|
<div className="flex flex-row items-center">
|
||||||
|
<div className="flex items-center pb-0.5">
|
||||||
|
<img src="/images/integrations/Gitlab.png" height={28} width={28} alt="Gitlab logo" />
|
||||||
|
</div>
|
||||||
|
<span className="ml-2.5">GitLab Integration </span>
|
||||||
|
<a
|
||||||
|
href="https://infisical.com/docs/integrations/cicd/gitlab"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
>
|
||||||
|
<div className="mb-1 ml-2 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="mb-[0.07rem] ml-1.5 text-xxs"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</CardTitle>
|
||||||
|
<Tabs defaultValue={TabSections.Connection} className="px-6">
|
||||||
|
<TabList>
|
||||||
|
<div className="flex w-full flex-row border-b border-mineshaft-600">
|
||||||
|
<Tab value={TabSections.Connection}>Connection</Tab>
|
||||||
|
<Tab value={TabSections.Options}>Options</Tab>
|
||||||
|
</div>
|
||||||
|
</TabList>
|
||||||
|
<TabPanel value={TabSections.Connection}>
|
||||||
|
<motion.div
|
||||||
|
key="panel-1"
|
||||||
|
transition={{ duration: 0.15 }}
|
||||||
|
initial={{ opacity: 0, translateX: 30 }}
|
||||||
|
animate={{ opacity: 1, translateX: 0 }}
|
||||||
|
exit={{ opacity: 0, translateX: 30 }}
|
||||||
|
>
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
|
name="selectedSourceEnvironment"
|
||||||
|
render={({ field: { onChange, ...field }, fieldState: { error } }) => (
|
||||||
|
<FormControl
|
||||||
|
label="Project Environment"
|
||||||
|
errorText={error?.message}
|
||||||
|
isError={Boolean(error)}
|
||||||
|
>
|
||||||
|
<Select
|
||||||
|
defaultValue={field.value}
|
||||||
|
{...field}
|
||||||
|
onValueChange={(e) => onChange(e)}
|
||||||
|
className="w-full"
|
||||||
|
>
|
||||||
|
{currentWorkspace?.environments.map((sourceEnvironment) => (
|
||||||
|
<SelectItem
|
||||||
|
value={sourceEnvironment.slug}
|
||||||
|
key={`source-environment-${sourceEnvironment.slug}`}
|
||||||
|
>
|
||||||
|
{sourceEnvironment.name}
|
||||||
|
</SelectItem>
|
||||||
|
))}
|
||||||
|
</Select>
|
||||||
|
</FormControl>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
|
defaultValue=""
|
||||||
|
name="secretPath"
|
||||||
|
render={({ field, fieldState: { error } }) => (
|
||||||
|
<FormControl
|
||||||
|
label="Secrets Path"
|
||||||
|
isError={Boolean(error)}
|
||||||
|
errorText={error?.message}
|
||||||
|
>
|
||||||
|
<SecretPathInput
|
||||||
|
{...field}
|
||||||
|
placeholder="/"
|
||||||
|
environment={selectedSourceEnvironment}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
|
name="targetEntity"
|
||||||
|
render={({ field: { onChange, ...field }, fieldState: { error } }) => (
|
||||||
|
<FormControl
|
||||||
|
label="GitLab Integration Type"
|
||||||
|
errorText={error?.message}
|
||||||
|
isError={Boolean(error)}
|
||||||
|
>
|
||||||
|
<Select {...field} onValueChange={(e) => onChange(e)} className="w-full">
|
||||||
|
{gitLabEntities.map((entity) => {
|
||||||
|
return (
|
||||||
|
<SelectItem value={entity.value} key={`target-entity-${entity.value}`}>
|
||||||
|
{entity.name}
|
||||||
|
</SelectItem>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</Select>
|
||||||
|
</FormControl>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
{targetEntity === "group" && targetTeamId && (
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
|
name="targetTeamId"
|
||||||
|
render={({ field: { onChange, ...field }, fieldState: { error } }) => (
|
||||||
|
<FormControl
|
||||||
|
label="GitLab Group"
|
||||||
|
errorText={error?.message}
|
||||||
|
isError={Boolean(error)}
|
||||||
|
>
|
||||||
|
<Select {...field} onValueChange={(e) => onChange(e)} className="w-full">
|
||||||
|
{integrationAuthTeams.length > 0 ? (
|
||||||
|
integrationAuthTeams.map((integrationAuthTeam) => (
|
||||||
|
<SelectItem
|
||||||
|
value={String(integrationAuthTeam.id as string)}
|
||||||
|
key={`target-team-${String(integrationAuthTeam.id)}`}
|
||||||
|
>
|
||||||
|
{integrationAuthTeam.name}
|
||||||
|
</SelectItem>
|
||||||
|
))
|
||||||
|
) : (
|
||||||
|
<SelectItem value="none" key="target-team-none">
|
||||||
|
No groups found
|
||||||
|
</SelectItem>
|
||||||
|
)}
|
||||||
|
</Select>
|
||||||
|
</FormControl>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
|
name="targetAppId"
|
||||||
|
render={({ field: { onChange, ...field }, fieldState: { error } }) => {
|
||||||
|
return (
|
||||||
|
<FormControl
|
||||||
|
label="GitLab Project"
|
||||||
|
errorText={error?.message}
|
||||||
|
isError={Boolean(error)}
|
||||||
|
>
|
||||||
|
<Select
|
||||||
|
{...field}
|
||||||
|
onValueChange={(e) => {
|
||||||
|
if (e === "") return;
|
||||||
|
onChange(e);
|
||||||
|
}}
|
||||||
|
className="w-full"
|
||||||
|
>
|
||||||
|
{integrationAuthApps.length > 0 ? (
|
||||||
|
integrationAuthApps.map((integrationAuthApp) => (
|
||||||
|
<SelectItem
|
||||||
|
value={String(integrationAuthApp.appId as string)}
|
||||||
|
key={`target-app-${String(integrationAuthApp.appId)}`}
|
||||||
|
>
|
||||||
|
{integrationAuthApp.name}
|
||||||
|
</SelectItem>
|
||||||
|
))
|
||||||
|
) : (
|
||||||
|
<SelectItem value="none" key="target-app-none">
|
||||||
|
No projects found
|
||||||
|
</SelectItem>
|
||||||
|
)}
|
||||||
|
</Select>
|
||||||
|
</FormControl>
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
|
defaultValue=""
|
||||||
|
name="targetEnvironment"
|
||||||
|
render={({ field, fieldState: { error } }) => (
|
||||||
|
<FormControl
|
||||||
|
label="GitLab Environment Scope (Optional)"
|
||||||
|
isError={Boolean(error)}
|
||||||
|
errorText={error?.message}
|
||||||
|
>
|
||||||
|
<Input {...field} placeholder="*" />
|
||||||
|
</FormControl>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</motion.div>
|
||||||
|
</TabPanel>
|
||||||
|
<TabPanel value={TabSections.Options}>
|
||||||
|
<motion.div
|
||||||
|
key="panel-1"
|
||||||
|
transition={{ duration: 0.15 }}
|
||||||
|
initial={{ opacity: 0, translateX: -30 }}
|
||||||
|
animate={{ opacity: 1, translateX: 0 }}
|
||||||
|
exit={{ opacity: 0, translateX: 30 }}
|
||||||
|
className="pb-[14.25rem]"
|
||||||
|
>
|
||||||
|
<div className="ml-1">
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
|
name="shouldMaskSecrets"
|
||||||
|
render={({ field: { onChange, value } }) => (
|
||||||
|
<Switch
|
||||||
|
id="should-mask-secrets"
|
||||||
|
onCheckedChange={(isChecked) => onChange(isChecked)}
|
||||||
|
isChecked={value}
|
||||||
|
>
|
||||||
|
<div className="max-w-md">
|
||||||
|
Mark Infisical secrets in Gitlab as 'Masked' secrets
|
||||||
|
</div>
|
||||||
|
</Switch>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="mb-5 ml-1 mt-4">
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
|
name="shouldProtectSecrets"
|
||||||
|
render={({ field: { onChange, value } }) => (
|
||||||
|
<Switch
|
||||||
|
id="should-protect-secrets"
|
||||||
|
onCheckedChange={(isChecked) => onChange(isChecked)}
|
||||||
|
isChecked={value}
|
||||||
|
>
|
||||||
|
Mark Infisical secrets in Gitlab as 'Protected' secrets
|
||||||
|
</Switch>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
|
name="secretPrefix"
|
||||||
|
render={({ field, fieldState: { error } }) => (
|
||||||
|
<FormControl
|
||||||
|
label="Secret Prefix"
|
||||||
|
isError={Boolean(error)}
|
||||||
|
errorText={error?.message}
|
||||||
|
>
|
||||||
|
<Input {...field} placeholder="INFISICAL_" />
|
||||||
|
</FormControl>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
|
name="secretSuffix"
|
||||||
|
render={({ field, fieldState: { error } }) => (
|
||||||
|
<FormControl
|
||||||
|
label="Secret Suffix"
|
||||||
|
isError={Boolean(error)}
|
||||||
|
errorText={error?.message}
|
||||||
|
>
|
||||||
|
<Input {...field} placeholder="_INFISICAL" />
|
||||||
|
</FormControl>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</motion.div>
|
||||||
|
</TabPanel>
|
||||||
|
</Tabs>
|
||||||
|
<Button
|
||||||
|
colorSchema="primary"
|
||||||
|
variant="outline_bg"
|
||||||
|
className="mb-8 ml-auto mr-6 w-min"
|
||||||
|
size="sm"
|
||||||
|
type="submit"
|
||||||
|
isLoading={isLoading}
|
||||||
|
isDisabled={targetAppIdValue === "none"}
|
||||||
|
>
|
||||||
|
Create Integration
|
||||||
|
</Button>
|
||||||
|
</Card>
|
||||||
|
{/* <div className="border-t border-mineshaft-800 w-full max-w-md mt-6"/>
|
||||||
|
<div className="flex flex-col bg-mineshaft-800 border border-mineshaft-600 w-full p-4 max-w-lg mt-6 rounded-md">
|
||||||
|
<div className="flex flex-row items-center"><FontAwesomeIcon icon={faCircleInfo} className="text-mineshaft-200 text-xl"/> <span className="ml-3 text-md text-mineshaft-100">Pro Tip</span></div>
|
||||||
|
<span className="text-mineshaft-300 text-sm mt-4">After creating an integration, your secrets will start syncing immediately. This might cause an unexpected override of current secrets in GitLab with secrets from Infisical.</span>
|
||||||
|
</div> */}
|
||||||
|
<Modal
|
||||||
|
isOpen={popUp.confirmIntegration?.isOpen}
|
||||||
|
onOpenChange={(isOpen) => handlePopUpToggle("confirmIntegration", isOpen)}
|
||||||
|
>
|
||||||
|
<ModalContent
|
||||||
|
title="Heads Up"
|
||||||
|
footerContent={
|
||||||
|
<div className="flex items-center space-x-2">
|
||||||
|
<Button onClick={() => onFormSubmit(popUp.confirmIntegration?.data as FormData)}>
|
||||||
|
Continue Anyway
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
onClick={() => handlePopUpClose("confirmIntegration")}
|
||||||
|
variant="outline_bg"
|
||||||
|
colorSchema="secondary"
|
||||||
|
>
|
||||||
|
Cancel
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<p>You're about to overwrite any existing secrets in GitLab.</p>
|
||||||
|
<p className="mt-4">
|
||||||
|
To avoid this behavior, you may consider adding a secret prefix/suffix in the options
|
||||||
|
tab.
|
||||||
|
</p>
|
||||||
|
</ModalContent>
|
||||||
|
</Modal>
|
||||||
|
</form>
|
||||||
|
) : (
|
||||||
|
<div className="flex h-full w-full items-center justify-center">
|
||||||
|
<Helmet>
|
||||||
|
<title>Set Up GitLab Integration</title>
|
||||||
|
</Helmet>
|
||||||
|
{isIntegrationAuthAppsLoading || isintegrationAuthTeamsLoading ? (
|
||||||
|
<img
|
||||||
|
src="/images/loading/loading.gif"
|
||||||
|
height={70}
|
||||||
|
width={120}
|
||||||
|
alt="infisical loading indicator"
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<div className="flex h-max max-w-md flex-col rounded-md border border-mineshaft-600 bg-mineshaft-800 p-6 text-center text-mineshaft-200">
|
||||||
|
<FontAwesomeIcon icon={faBugs} className="inlineli my-2 text-6xl" />
|
||||||
|
<p>
|
||||||
|
Something went wrong. Please contact{" "}
|
||||||
|
<a
|
||||||
|
className="inline cursor-pointer text-mineshaft-100 underline decoration-primary-500 underline-offset-4 opacity-80 duration-200 hover:opacity-100"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
href="mailto:[email protected]"
|
||||||
|
>
|
||||||
|
support@infisical.com
|
||||||
|
</a>{" "}
|
||||||
|
if the issue persists.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
import { createFileRoute } from "@tanstack/react-router";
|
||||||
|
import { zodValidator } from "@tanstack/zod-adapter";
|
||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
import { GitlabConfigurePage } from "./GitlabConfigurePage";
|
||||||
|
|
||||||
|
const GitlabConfigurePageQueryParamsSchema = z.object({
|
||||||
|
integrationAuthId: z.string()
|
||||||
|
});
|
||||||
|
|
||||||
|
export const Route = createFileRoute(
|
||||||
|
"/_authenticate/_inject-org-details/secret-manager/$projectId/_secret-manager-layout/integrations/gitlab/create"
|
||||||
|
)({
|
||||||
|
component: GitlabConfigurePage,
|
||||||
|
validateSearch: zodValidator(GitlabConfigurePageQueryParamsSchema)
|
||||||
|
});
|
||||||
+53
@@ -0,0 +1,53 @@
|
|||||||
|
import { useEffect } from "react";
|
||||||
|
import { useNavigate, useSearch } from "@tanstack/react-router";
|
||||||
|
|
||||||
|
import { ROUTE_PATHS } from "@app/const/routes";
|
||||||
|
import { useWorkspace } from "@app/context";
|
||||||
|
import { useAuthorizeIntegration } from "@app/hooks/api";
|
||||||
|
|
||||||
|
export const GitLabOAuthCallbackPage = () => {
|
||||||
|
const navigate = useNavigate();
|
||||||
|
const { mutateAsync } = useAuthorizeIntegration();
|
||||||
|
|
||||||
|
const { code, state } = useSearch({
|
||||||
|
from: ROUTE_PATHS.SecretManager.Integratons.GitlabOauthCallbackPage.id
|
||||||
|
});
|
||||||
|
const { currentWorkspace } = useWorkspace();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
(async () => {
|
||||||
|
try {
|
||||||
|
// validate state
|
||||||
|
const [csrfToken, url] = (state as string).split("|", 2);
|
||||||
|
|
||||||
|
if (csrfToken !== localStorage.getItem("latestCSRFToken")) return;
|
||||||
|
localStorage.removeItem("latestCSRFToken");
|
||||||
|
|
||||||
|
const integrationAuth = await mutateAsync({
|
||||||
|
workspaceId: currentWorkspace.id,
|
||||||
|
code: code as string,
|
||||||
|
integration: "gitlab",
|
||||||
|
...(url === ""
|
||||||
|
? {}
|
||||||
|
: {
|
||||||
|
url
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
navigate({
|
||||||
|
to: "/secret-manager/$projectId/integrations/gitlab/create",
|
||||||
|
params: {
|
||||||
|
projectId: currentWorkspace.id
|
||||||
|
},
|
||||||
|
search: {
|
||||||
|
integrationAuthId: integrationAuth.id
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err);
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return <div />;
|
||||||
|
};
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
import { createFileRoute } from "@tanstack/react-router";
|
||||||
|
import { zodValidator } from "@tanstack/zod-adapter";
|
||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
import { GitLabOAuthCallbackPage } from "./GitlabOauthCallbackPage";
|
||||||
|
|
||||||
|
export const GitlabOAuthCallbackPageQueryParamsSchema = z.object({
|
||||||
|
state: z.string().catch(""),
|
||||||
|
code: z.string()
|
||||||
|
});
|
||||||
|
|
||||||
|
export const Route = createFileRoute(
|
||||||
|
"/_authenticate/_inject-org-details/secret-manager/$projectId/_secret-manager-layout/integrations/gitlab/oauth2/callback"
|
||||||
|
)({
|
||||||
|
component: GitLabOAuthCallbackPage,
|
||||||
|
validateSearch: zodValidator(GitlabOAuthCallbackPageQueryParamsSchema)
|
||||||
|
});
|
||||||
+194
@@ -0,0 +1,194 @@
|
|||||||
|
import { Helmet } from "react-helmet";
|
||||||
|
import { Controller, useForm } from "react-hook-form";
|
||||||
|
import { faArrowUpRightFromSquare, faBookOpen } from "@fortawesome/free-solid-svg-icons";
|
||||||
|
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||||
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
|
import { useNavigate } from "@tanstack/react-router";
|
||||||
|
import axios from "axios";
|
||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
import { createNotification } from "@app/components/notifications";
|
||||||
|
import { Button, Card, CardBody, CardTitle, FormControl, Input } from "@app/components/v2";
|
||||||
|
import { useWorkspace } from "@app/context";
|
||||||
|
import { useSaveIntegrationAccessToken } from "@app/hooks/api";
|
||||||
|
|
||||||
|
const formSchema = z.object({
|
||||||
|
vaultURL: z.string().url({ message: "Invalid Hashicorp Vault URL" }),
|
||||||
|
vaultNamespace: z.string().optional(),
|
||||||
|
vaultRoleID: z.string().uuid({ message: "Role ID has be a valid UUID" }),
|
||||||
|
vaultSecretID: z.string().uuid({ message: "Role ID has be a valid UUID" })
|
||||||
|
});
|
||||||
|
|
||||||
|
type TForm = z.infer<typeof formSchema>;
|
||||||
|
|
||||||
|
export const HashicorpVaultAuthorizePage = () => {
|
||||||
|
const navigate = useNavigate();
|
||||||
|
const { mutateAsync } = useSaveIntegrationAccessToken();
|
||||||
|
|
||||||
|
const { currentWorkspace } = useWorkspace();
|
||||||
|
const {
|
||||||
|
control,
|
||||||
|
handleSubmit,
|
||||||
|
formState: { isSubmitting }
|
||||||
|
} = useForm<TForm>({
|
||||||
|
resolver: zodResolver(formSchema),
|
||||||
|
defaultValues: {
|
||||||
|
vaultURL: "",
|
||||||
|
vaultNamespace: "",
|
||||||
|
vaultRoleID: "",
|
||||||
|
vaultSecretID: ""
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const handleFormSubmit = async (formData: TForm) => {
|
||||||
|
try {
|
||||||
|
const integrationAuth = await mutateAsync({
|
||||||
|
workspaceId: currentWorkspace.id,
|
||||||
|
integration: "hashicorp-vault",
|
||||||
|
accessId: formData.vaultRoleID,
|
||||||
|
accessToken: formData.vaultSecretID,
|
||||||
|
url: formData.vaultURL,
|
||||||
|
namespace: formData.vaultNamespace
|
||||||
|
});
|
||||||
|
navigate({
|
||||||
|
to: "/secret-manager/$projectId/integrations/hashicorp-vault/create",
|
||||||
|
params: {
|
||||||
|
projectId: currentWorkspace.id
|
||||||
|
},
|
||||||
|
search: {
|
||||||
|
integrationAuthId: integrationAuth.id
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err);
|
||||||
|
let errorMessage: string = "Something went wrong!";
|
||||||
|
if (axios.isAxiosError(err)) {
|
||||||
|
const { message } = err?.response?.data as { message: string };
|
||||||
|
errorMessage = message;
|
||||||
|
}
|
||||||
|
|
||||||
|
createNotification({
|
||||||
|
text: errorMessage,
|
||||||
|
type: "error"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex h-full w-full items-center justify-center">
|
||||||
|
<Helmet>
|
||||||
|
<title>Authorize Vault Integration</title>
|
||||||
|
</Helmet>
|
||||||
|
<Card className="mb-12 max-w-lg rounded-md border border-mineshaft-600">
|
||||||
|
<CardTitle
|
||||||
|
className="px-6 text-left text-xl"
|
||||||
|
subTitle="After connecting to Vault, 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">
|
||||||
|
<img
|
||||||
|
src="/images/integrations/Vault.png"
|
||||||
|
height={30}
|
||||||
|
width={30}
|
||||||
|
alt="HCP Vault logo"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<span className="ml-2.5">HashiCorp Vault Integration</span>
|
||||||
|
<a
|
||||||
|
href="https://infisical.com/docs/integrations/cloud/hashicorp-vault"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
>
|
||||||
|
<div className="mb-1 ml-2 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="mb-[0.07rem] ml-1.5 text-xxs"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</CardTitle>
|
||||||
|
<CardBody className="px-6 pb-6 pt-0">
|
||||||
|
<form onSubmit={handleSubmit(handleFormSubmit)} noValidate>
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
|
name="vaultURL"
|
||||||
|
render={({ field, fieldState: { error } }) => (
|
||||||
|
<FormControl
|
||||||
|
label="Vault Cluster URL"
|
||||||
|
errorText={error?.message}
|
||||||
|
isError={Boolean(error)}
|
||||||
|
isRequired
|
||||||
|
>
|
||||||
|
<Input autoCorrect="off" spellCheck={false} placeholder="" {...field} />
|
||||||
|
</FormControl>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
|
name="vaultNamespace"
|
||||||
|
render={({ field, fieldState: { error } }) => (
|
||||||
|
<FormControl
|
||||||
|
label="Vault Namespace"
|
||||||
|
errorText={error?.message}
|
||||||
|
isError={Boolean(error)}
|
||||||
|
isRequired={false}
|
||||||
|
>
|
||||||
|
<Input
|
||||||
|
autoCorrect="off"
|
||||||
|
spellCheck={false}
|
||||||
|
placeholder="admin/education"
|
||||||
|
{...field}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
|
name="vaultRoleID"
|
||||||
|
render={({ field, fieldState: { error } }) => (
|
||||||
|
<FormControl
|
||||||
|
label="Vault RoleID"
|
||||||
|
errorText={error?.message}
|
||||||
|
isError={Boolean(error)}
|
||||||
|
isRequired
|
||||||
|
>
|
||||||
|
<Input
|
||||||
|
autoCorrect="off"
|
||||||
|
spellCheck={false}
|
||||||
|
placeholder="aaaaaaa-bbbb-cccc-dddd-aaaaaaaaaaa"
|
||||||
|
{...field}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
|
name="vaultSecretID"
|
||||||
|
render={({ field, fieldState: { error } }) => (
|
||||||
|
<FormControl
|
||||||
|
label="Vault SecretID"
|
||||||
|
errorText={error?.message}
|
||||||
|
isError={Boolean(error)}
|
||||||
|
isRequired
|
||||||
|
>
|
||||||
|
<Input
|
||||||
|
autoCorrect="off"
|
||||||
|
spellCheck={false}
|
||||||
|
placeholder="aaaaaaa-bbbb-cccc-dddd-aaaaaaaaaaa"
|
||||||
|
{...field}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<Button type="submit" isLoading={isSubmitting}>
|
||||||
|
Connect to Vault
|
||||||
|
</Button>
|
||||||
|
</form>
|
||||||
|
</CardBody>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
+9
@@ -0,0 +1,9 @@
|
|||||||
|
import { createFileRoute } from "@tanstack/react-router";
|
||||||
|
|
||||||
|
import { HashicorpVaultAuthorizePage } from "./HashicorpVaultAuthorizePage";
|
||||||
|
|
||||||
|
export const Route = createFileRoute(
|
||||||
|
"/_authenticate/_inject-org-details/secret-manager/$projectId/_secret-manager-layout/integrations/hashicorp-vault/authorize"
|
||||||
|
)({
|
||||||
|
component: HashicorpVaultAuthorizePage
|
||||||
|
});
|
||||||
+291
@@ -0,0 +1,291 @@
|
|||||||
|
import { useMemo } from "react";
|
||||||
|
import { Helmet } from "react-helmet";
|
||||||
|
import { Controller, useForm } from "react-hook-form";
|
||||||
|
import {
|
||||||
|
faArrowUpRightFromSquare,
|
||||||
|
faBookOpen,
|
||||||
|
faBugs,
|
||||||
|
faCircleInfo
|
||||||
|
} from "@fortawesome/free-solid-svg-icons";
|
||||||
|
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||||
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
|
import { useNavigate, useSearch } from "@tanstack/react-router";
|
||||||
|
import axios from "axios";
|
||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
import { createNotification } from "@app/components/notifications";
|
||||||
|
import {
|
||||||
|
Button,
|
||||||
|
Card,
|
||||||
|
CardBody,
|
||||||
|
CardTitle,
|
||||||
|
FormControl,
|
||||||
|
Input,
|
||||||
|
Select,
|
||||||
|
SelectItem
|
||||||
|
} from "@app/components/v2";
|
||||||
|
import { ROUTE_PATHS } from "@app/const/routes";
|
||||||
|
import { useWorkspace } from "@app/context";
|
||||||
|
import { isValidPath } from "@app/helpers/string";
|
||||||
|
import { useCreateIntegration } from "@app/hooks/api";
|
||||||
|
import { useGetIntegrationAuthById } from "@app/hooks/api/integrationAuth";
|
||||||
|
|
||||||
|
const generateFormSchema = (availableEnvironmentNames: string[]) => {
|
||||||
|
return z.object({
|
||||||
|
secretPath: z
|
||||||
|
.string()
|
||||||
|
.min(1)
|
||||||
|
.refine((val) => isValidPath(val), {
|
||||||
|
message: "Vault secret path has to be a valid path"
|
||||||
|
}),
|
||||||
|
vaultEnginePath: z
|
||||||
|
.string()
|
||||||
|
.min(1)
|
||||||
|
.refine((val) => isValidPath(val), {
|
||||||
|
message: "Vault engine path has to be a valid path"
|
||||||
|
}),
|
||||||
|
vaultSecretPath: z
|
||||||
|
.string()
|
||||||
|
.min(1)
|
||||||
|
.refine((val) => isValidPath(val), {
|
||||||
|
message: "Vault secret path has to be a valid path"
|
||||||
|
}),
|
||||||
|
selectedSourceEnvironment: z.enum(availableEnvironmentNames as any as [string, ...string[]])
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
type TForm = z.infer<ReturnType<typeof generateFormSchema>>;
|
||||||
|
|
||||||
|
export const HashicorpVaultConfigurePage = () => {
|
||||||
|
const navigate = useNavigate();
|
||||||
|
const { mutateAsync } = useCreateIntegration();
|
||||||
|
|
||||||
|
const { currentWorkspace } = useWorkspace();
|
||||||
|
const integrationAuthId = useSearch({
|
||||||
|
from: ROUTE_PATHS.SecretManager.Integratons.HashicorpVaultConfigurePage.id,
|
||||||
|
select: (el) => el.integrationAuthId
|
||||||
|
});
|
||||||
|
|
||||||
|
const { data: integrationAuth, isLoading: isintegrationAuthLoading } = useGetIntegrationAuthById(
|
||||||
|
(integrationAuthId as string) ?? ""
|
||||||
|
);
|
||||||
|
|
||||||
|
const formSchema = useMemo(() => {
|
||||||
|
return generateFormSchema(currentWorkspace?.environments.map((env) => env.slug) ?? []);
|
||||||
|
}, [currentWorkspace?.environments]);
|
||||||
|
|
||||||
|
const {
|
||||||
|
control,
|
||||||
|
handleSubmit,
|
||||||
|
formState: { isSubmitting }
|
||||||
|
} = useForm<TForm>({
|
||||||
|
resolver: zodResolver(formSchema),
|
||||||
|
defaultValues: {
|
||||||
|
secretPath: "/",
|
||||||
|
vaultEnginePath: "",
|
||||||
|
vaultSecretPath: "",
|
||||||
|
selectedSourceEnvironment: ""
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const handleFormSubmit = async (formData: TForm) => {
|
||||||
|
try {
|
||||||
|
if (!integrationAuth?.id) return;
|
||||||
|
await mutateAsync({
|
||||||
|
integrationAuthId: integrationAuth?.id,
|
||||||
|
isActive: true,
|
||||||
|
app: formData.vaultEnginePath,
|
||||||
|
sourceEnvironment: formData.selectedSourceEnvironment,
|
||||||
|
path: formData.vaultSecretPath,
|
||||||
|
secretPath: formData.secretPath
|
||||||
|
});
|
||||||
|
navigate({
|
||||||
|
to: "/secret-manager/$projectId/integrations",
|
||||||
|
params: {
|
||||||
|
projectId: currentWorkspace.id
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err);
|
||||||
|
let errorMessage: string = "Something went wrong!";
|
||||||
|
if (axios.isAxiosError(err)) {
|
||||||
|
const { message } = err?.response?.data as { message: string };
|
||||||
|
errorMessage = message;
|
||||||
|
}
|
||||||
|
|
||||||
|
createNotification({
|
||||||
|
text: errorMessage,
|
||||||
|
type: "error"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return integrationAuth ? (
|
||||||
|
<div className="flex h-full w-full flex-col items-center justify-center">
|
||||||
|
<Helmet>
|
||||||
|
<title>Set Up Vault Integration</title>
|
||||||
|
</Helmet>
|
||||||
|
<Card className="max-w-lg rounded-md border border-mineshaft-600">
|
||||||
|
<CardTitle
|
||||||
|
className="px-6 text-left text-xl"
|
||||||
|
subTitle="Select which environment or folder in Infisical you want to sync to which path in HashiCorp Vault."
|
||||||
|
>
|
||||||
|
<div className="flex flex-row items-center">
|
||||||
|
<div className="inline-flex items-center">
|
||||||
|
<img
|
||||||
|
src="/images/integrations/Vault.png"
|
||||||
|
height={30}
|
||||||
|
width={30}
|
||||||
|
alt="HCP Vault logo"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<span className="ml-2.5">HashiCorp Vault Integration</span>
|
||||||
|
<a
|
||||||
|
href="https://infisical.com/docs/integrations/cloud/hashicorp-vault"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
>
|
||||||
|
<div className="mb-1 ml-2 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="mb-[0.07rem] ml-1.5 text-xxs"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</CardTitle>
|
||||||
|
<CardBody>
|
||||||
|
<form onSubmit={handleSubmit(handleFormSubmit)} noValidate>
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
|
name="selectedSourceEnvironment"
|
||||||
|
render={({ field, fieldState: { error } }) => (
|
||||||
|
<FormControl
|
||||||
|
errorText={error?.message}
|
||||||
|
isError={Boolean(error)}
|
||||||
|
isRequired
|
||||||
|
label="Project Environment"
|
||||||
|
>
|
||||||
|
<Select
|
||||||
|
value={field.value}
|
||||||
|
onValueChange={field.onChange}
|
||||||
|
className="w-full border border-mineshaft-500"
|
||||||
|
>
|
||||||
|
{currentWorkspace?.environments.map((sourceEnvironment) => (
|
||||||
|
<SelectItem
|
||||||
|
value={sourceEnvironment.slug}
|
||||||
|
key={`vault-environment-${sourceEnvironment.slug}`}
|
||||||
|
>
|
||||||
|
{sourceEnvironment.name}
|
||||||
|
</SelectItem>
|
||||||
|
))}
|
||||||
|
</Select>
|
||||||
|
</FormControl>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
|
name="secretPath"
|
||||||
|
render={({ field, fieldState: { error } }) => (
|
||||||
|
<FormControl
|
||||||
|
errorText={error?.message}
|
||||||
|
isError={Boolean(error)}
|
||||||
|
isRequired
|
||||||
|
label="Secrets Path"
|
||||||
|
helperText="A path to your secrets in Infisical."
|
||||||
|
>
|
||||||
|
<Input {...field} autoCorrect="off" spellCheck={false} placeholder="/" />
|
||||||
|
</FormControl>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
|
name="vaultEnginePath"
|
||||||
|
render={({ field, fieldState: { error } }) => (
|
||||||
|
<FormControl
|
||||||
|
label="Vault KV Secrets Engine Path"
|
||||||
|
errorText={error?.message}
|
||||||
|
isError={Boolean(error)}
|
||||||
|
helperText="A path where your KV Secrets Engine is enabled."
|
||||||
|
isRequired
|
||||||
|
>
|
||||||
|
<Input autoCorrect="off" spellCheck={false} {...field} placeholder="kv" />
|
||||||
|
</FormControl>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
|
name="vaultSecretPath"
|
||||||
|
render={({ field, fieldState: { error } }) => (
|
||||||
|
<FormControl
|
||||||
|
label="Vault Secret(s) Path"
|
||||||
|
errorText={error?.message}
|
||||||
|
isError={Boolean(error)}
|
||||||
|
helperText="A path for storing secrets within the KV Secrets Engine."
|
||||||
|
isRequired
|
||||||
|
>
|
||||||
|
<Input
|
||||||
|
autoCorrect="off"
|
||||||
|
spellCheck={false}
|
||||||
|
{...field}
|
||||||
|
placeholder="machine/dev"
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Button type="submit" isLoading={isSubmitting}>
|
||||||
|
Create Integration
|
||||||
|
</Button>
|
||||||
|
</form>
|
||||||
|
</CardBody>
|
||||||
|
</Card>
|
||||||
|
<div className="mt-6 w-full max-w-md border-t border-mineshaft-800" />
|
||||||
|
<div className="mt-6 flex w-full max-w-lg flex-col rounded-md border border-mineshaft-600 bg-mineshaft-800 p-4">
|
||||||
|
<div className="flex flex-row items-center">
|
||||||
|
<FontAwesomeIcon icon={faCircleInfo} className="text-xl text-mineshaft-200" />{" "}
|
||||||
|
<span className="text-md ml-3 text-mineshaft-100">Pro Tip</span>
|
||||||
|
</div>
|
||||||
|
<span className="mt-4 text-sm text-mineshaft-300">
|
||||||
|
After creating an integration, your secrets will start syncing immediately. This might
|
||||||
|
cause an unexpected override of current secrets in Vault with secrets from Infisical.
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="flex h-full w-full items-center justify-center">
|
||||||
|
<Helmet>
|
||||||
|
<title>Set Up Vault Integration</title>
|
||||||
|
</Helmet>
|
||||||
|
{isintegrationAuthLoading ? (
|
||||||
|
<img
|
||||||
|
src="/images/loading/loading.gif"
|
||||||
|
height={70}
|
||||||
|
width={120}
|
||||||
|
alt="infisical loading indicator"
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<div className="flex h-max max-w-md flex-col rounded-md border border-mineshaft-600 bg-mineshaft-800 p-6 text-center text-mineshaft-200">
|
||||||
|
<FontAwesomeIcon icon={faBugs} className="inlineli my-2 text-6xl" />
|
||||||
|
<p>
|
||||||
|
Something went wrong. Please contact{" "}
|
||||||
|
<a
|
||||||
|
className="inline cursor-pointer text-mineshaft-100 underline decoration-primary-500 underline-offset-4 opacity-80 duration-200 hover:opacity-100"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
href="mailto:[email protected]"
|
||||||
|
>
|
||||||
|
support@infisical.com
|
||||||
|
</a>{" "}
|
||||||
|
if the issue persists.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
+16
@@ -0,0 +1,16 @@
|
|||||||
|
import { createFileRoute } from "@tanstack/react-router";
|
||||||
|
import { zodValidator } from "@tanstack/zod-adapter";
|
||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
import { HashicorpVaultConfigurePage } from "./HashicorpVaultConfigurePage";
|
||||||
|
|
||||||
|
const HashicorpVaultConfigurePageQueryParamsSchema = z.object({
|
||||||
|
integrationAuthId: z.string()
|
||||||
|
});
|
||||||
|
|
||||||
|
export const Route = createFileRoute(
|
||||||
|
"/_authenticate/_inject-org-details/secret-manager/$projectId/_secret-manager-layout/integrations/hashicorp-vault/create"
|
||||||
|
)({
|
||||||
|
component: HashicorpVaultConfigurePage,
|
||||||
|
validateSearch: zodValidator(HashicorpVaultConfigurePageQueryParamsSchema)
|
||||||
|
});
|
||||||
+123
@@ -0,0 +1,123 @@
|
|||||||
|
import { useState } from "react";
|
||||||
|
import { Helmet } from "react-helmet";
|
||||||
|
import { Controller, useForm } from "react-hook-form";
|
||||||
|
import { faArrowUpRightFromSquare, faBookOpen } from "@fortawesome/free-solid-svg-icons";
|
||||||
|
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||||
|
import { yupResolver } from "@hookform/resolvers/yup";
|
||||||
|
import { useNavigate } from "@tanstack/react-router";
|
||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
import { Button, Card, CardTitle, FormControl, Input } from "@app/components/v2";
|
||||||
|
import { useWorkspace } from "@app/context";
|
||||||
|
import { useSaveIntegrationAccessToken } from "@app/hooks/api";
|
||||||
|
|
||||||
|
const schema = z.object({
|
||||||
|
accessToken: z.string().trim()
|
||||||
|
});
|
||||||
|
|
||||||
|
type FormData = z.infer<typeof schema>;
|
||||||
|
|
||||||
|
const APP_NAME = "Hasura Cloud";
|
||||||
|
export const HasuraCloudAuthorizePage = () => {
|
||||||
|
const navigate = useNavigate();
|
||||||
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
|
const { mutateAsync } = useSaveIntegrationAccessToken();
|
||||||
|
|
||||||
|
const { currentWorkspace } = useWorkspace();
|
||||||
|
const { control, handleSubmit } = useForm<FormData>({
|
||||||
|
resolver: yupResolver(schema),
|
||||||
|
defaultValues: {
|
||||||
|
accessToken: ""
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const onFormSubmit = async ({ accessToken }: FormData) => {
|
||||||
|
try {
|
||||||
|
setIsLoading(true);
|
||||||
|
|
||||||
|
const integrationAuth = await mutateAsync({
|
||||||
|
workspaceId: currentWorkspace.id,
|
||||||
|
integration: "hasura-cloud",
|
||||||
|
accessToken
|
||||||
|
});
|
||||||
|
|
||||||
|
setIsLoading(false);
|
||||||
|
navigate({
|
||||||
|
to: "/secret-manager/$projectId/integrations/hasura-cloud/create",
|
||||||
|
params: {
|
||||||
|
projectId: currentWorkspace.id
|
||||||
|
},
|
||||||
|
search: {
|
||||||
|
integrationAuthId: integrationAuth.id
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
setIsLoading(false);
|
||||||
|
console.error(err);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
return (
|
||||||
|
<div className="flex h-full w-full items-center justify-center">
|
||||||
|
<Helmet>
|
||||||
|
<title>Authorize {APP_NAME} Integration</title>
|
||||||
|
</Helmet>
|
||||||
|
<Card className="mb-12 max-w-lg rounded-md border border-mineshaft-600">
|
||||||
|
<CardTitle
|
||||||
|
className="px-6 text-left text-xl"
|
||||||
|
subTitle="After adding your access token, 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="flex items-center pb-0.5">
|
||||||
|
<img
|
||||||
|
src="/images/integrations/Hasura.svg"
|
||||||
|
height={30}
|
||||||
|
width={30}
|
||||||
|
alt={`${APP_NAME} logo`}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<span className="ml-2.5">{APP_NAME} Integration </span>
|
||||||
|
<a
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
href="https://infisical.com/docs/integrations/cloud/hasura-cloud"
|
||||||
|
>
|
||||||
|
<div className="mb-1 ml-2 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="mb-[0.07rem] ml-1.5 text-xxs"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</CardTitle>
|
||||||
|
<form onSubmit={handleSubmit(onFormSubmit)} className="px-6 pb-8 text-right">
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
|
name="accessToken"
|
||||||
|
render={({ field, fieldState: { error } }) => (
|
||||||
|
<FormControl
|
||||||
|
label={`${APP_NAME} Access Token`}
|
||||||
|
errorText={error?.message}
|
||||||
|
isError={Boolean(error)}
|
||||||
|
>
|
||||||
|
<Input {...field} placeholder="" />
|
||||||
|
</FormControl>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<Button
|
||||||
|
colorSchema="primary"
|
||||||
|
variant="outline_bg"
|
||||||
|
className="mt-2 w-min"
|
||||||
|
size="sm"
|
||||||
|
type="submit"
|
||||||
|
isLoading={isLoading}
|
||||||
|
>
|
||||||
|
Connect to {APP_NAME}
|
||||||
|
</Button>
|
||||||
|
</form>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
import { createFileRoute } from "@tanstack/react-router";
|
||||||
|
|
||||||
|
import { HasuraCloudAuthorizePage } from "./HasuraCloudAuthorizePage";
|
||||||
|
|
||||||
|
export const Route = createFileRoute(
|
||||||
|
"/_authenticate/_inject-org-details/secret-manager/$projectId/_secret-manager-layout/integrations/hasura-cloud/authorize"
|
||||||
|
)({
|
||||||
|
component: HasuraCloudAuthorizePage
|
||||||
|
});
|
||||||
+229
@@ -0,0 +1,229 @@
|
|||||||
|
import { Helmet } from "react-helmet";
|
||||||
|
import { Controller, useForm } from "react-hook-form";
|
||||||
|
import { faArrowUpRightFromSquare, faBookOpen, faBugs } from "@fortawesome/free-solid-svg-icons";
|
||||||
|
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||||
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
|
import { useNavigate, useSearch } from "@tanstack/react-router";
|
||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
import { Button, Card, CardTitle, FormControl, Select, SelectItem } from "@app/components/v2";
|
||||||
|
import { SecretPathInput } from "@app/components/v2/SecretPathInput";
|
||||||
|
import { ROUTE_PATHS } from "@app/const/routes";
|
||||||
|
import { useWorkspace } from "@app/context";
|
||||||
|
import { useCreateIntegration } from "@app/hooks/api";
|
||||||
|
import {
|
||||||
|
useGetIntegrationAuthApps,
|
||||||
|
useGetIntegrationAuthById
|
||||||
|
} from "@app/hooks/api/integrationAuth";
|
||||||
|
|
||||||
|
const schema = z.object({
|
||||||
|
secretPath: z.string().trim(),
|
||||||
|
sourceEnvironment: z.string().trim(),
|
||||||
|
appId: z.string().trim()
|
||||||
|
});
|
||||||
|
|
||||||
|
type FormData = z.infer<typeof schema>;
|
||||||
|
|
||||||
|
const APP_NAME = "Hasura Cloud";
|
||||||
|
export const HasuraCloudConfigurePage = () => {
|
||||||
|
const {
|
||||||
|
control,
|
||||||
|
handleSubmit,
|
||||||
|
watch,
|
||||||
|
formState: { isSubmitting }
|
||||||
|
} = useForm<FormData>({
|
||||||
|
resolver: zodResolver(schema)
|
||||||
|
});
|
||||||
|
const navigate = useNavigate();
|
||||||
|
const { mutateAsync } = useCreateIntegration();
|
||||||
|
|
||||||
|
const { currentWorkspace } = useWorkspace();
|
||||||
|
const integrationAuthId = useSearch({
|
||||||
|
from: ROUTE_PATHS.SecretManager.Integratons.HasuraCloudConfigurePage.id,
|
||||||
|
select: (el) => el.integrationAuthId
|
||||||
|
});
|
||||||
|
|
||||||
|
const { data: integrationAuth, isLoading: isIntegrationAuthLoading } = useGetIntegrationAuthById(
|
||||||
|
(integrationAuthId as string) ?? ""
|
||||||
|
);
|
||||||
|
|
||||||
|
const selectedSourceEnvironment = watch("sourceEnvironment");
|
||||||
|
|
||||||
|
const { data: integrationAuthApps, isLoading: isIntegrationAuthAppsLoading } =
|
||||||
|
useGetIntegrationAuthApps({
|
||||||
|
integrationAuthId: (integrationAuthId as string) ?? ""
|
||||||
|
});
|
||||||
|
|
||||||
|
const onFormSubmit = async ({ secretPath, sourceEnvironment, appId }: FormData) => {
|
||||||
|
try {
|
||||||
|
if (!integrationAuth?.id) return;
|
||||||
|
|
||||||
|
const app = integrationAuthApps?.find((data) => data.appId === appId);
|
||||||
|
await mutateAsync({
|
||||||
|
integrationAuthId: integrationAuth?.id,
|
||||||
|
isActive: true,
|
||||||
|
sourceEnvironment,
|
||||||
|
secretPath,
|
||||||
|
appId,
|
||||||
|
app: app?.name
|
||||||
|
});
|
||||||
|
|
||||||
|
navigate({
|
||||||
|
to: "/secret-manager/$projectId/integrations",
|
||||||
|
params: {
|
||||||
|
projectId: currentWorkspace.id
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return integrationAuth && integrationAuthApps ? (
|
||||||
|
<div className="flex h-full w-full flex-col items-center justify-center">
|
||||||
|
<Helmet>
|
||||||
|
<title>Set Up {APP_NAME} Integration</title>
|
||||||
|
</Helmet>
|
||||||
|
<Card className="max-w-lg rounded-md border border-mineshaft-600">
|
||||||
|
<CardTitle
|
||||||
|
className="px-6 text-left text-xl"
|
||||||
|
subTitle={`Choose which environment or folder in Infisical you want to sync to ${APP_NAME} environment variables.`}
|
||||||
|
>
|
||||||
|
<div className="flex flex-row items-center">
|
||||||
|
<div className="flex items-center pb-0.5">
|
||||||
|
<img
|
||||||
|
src="/images/integrations/Hasura.svg"
|
||||||
|
height={30}
|
||||||
|
width={30}
|
||||||
|
alt={`${APP_NAME} logo`}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<span className="ml-2.5">{APP_NAME} Integration </span>
|
||||||
|
<a
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
href="https://infisical.com/docs/integrations/cloud/hasura-cloud"
|
||||||
|
>
|
||||||
|
<div className="mb-1 ml-2 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="mb-[0.07rem] ml-1.5 text-xxs"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</CardTitle>
|
||||||
|
|
||||||
|
<form onSubmit={handleSubmit(onFormSubmit)} className="flex w-full flex-col px-6">
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
|
name="sourceEnvironment"
|
||||||
|
render={({ field, fieldState: { error } }) => (
|
||||||
|
<FormControl
|
||||||
|
label="Project Environment"
|
||||||
|
errorText={error?.message}
|
||||||
|
isError={Boolean(error)}
|
||||||
|
>
|
||||||
|
<Select
|
||||||
|
className="w-full border border-mineshaft-500"
|
||||||
|
value={field.value}
|
||||||
|
onValueChange={(val) => {
|
||||||
|
field.onChange(val);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{currentWorkspace?.environments.map((sourceEnvironment) => (
|
||||||
|
<SelectItem
|
||||||
|
value={sourceEnvironment.slug}
|
||||||
|
key={`source-environment-${sourceEnvironment.slug}`}
|
||||||
|
>
|
||||||
|
{sourceEnvironment.name}
|
||||||
|
</SelectItem>
|
||||||
|
))}
|
||||||
|
</Select>
|
||||||
|
</FormControl>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
|
name="secretPath"
|
||||||
|
render={({ field, fieldState: { error } }) => (
|
||||||
|
<FormControl label="Secrets Path" errorText={error?.message} isError={Boolean(error)}>
|
||||||
|
<SecretPathInput {...field} environment={selectedSourceEnvironment} />
|
||||||
|
</FormControl>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
|
name="appId"
|
||||||
|
render={({ field, fieldState: { error } }) => (
|
||||||
|
<FormControl
|
||||||
|
label="Hasura Cloud Project"
|
||||||
|
errorText={error?.message}
|
||||||
|
isError={Boolean(error)}
|
||||||
|
>
|
||||||
|
<Select
|
||||||
|
className="w-full border border-mineshaft-500"
|
||||||
|
value={field.value}
|
||||||
|
isDisabled={integrationAuthApps?.length === 0}
|
||||||
|
onValueChange={(val) => {
|
||||||
|
field.onChange(val);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{integrationAuthApps?.map((project) => (
|
||||||
|
<SelectItem value={project.appId ?? ""} key={`project-id-${project.appId}`}>
|
||||||
|
{project.name}
|
||||||
|
</SelectItem>
|
||||||
|
))}
|
||||||
|
</Select>
|
||||||
|
</FormControl>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
type="submit"
|
||||||
|
color="mineshaft"
|
||||||
|
variant="outline_bg"
|
||||||
|
className="mb-6 ml-auto mt-2"
|
||||||
|
isLoading={isSubmitting}
|
||||||
|
>
|
||||||
|
Create Integration
|
||||||
|
</Button>
|
||||||
|
</form>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="flex h-full w-full items-center justify-center">
|
||||||
|
<Helmet>
|
||||||
|
<title>Set Up {APP_NAME} Integration</title>
|
||||||
|
</Helmet>
|
||||||
|
{isIntegrationAuthLoading || isIntegrationAuthAppsLoading ? (
|
||||||
|
<img
|
||||||
|
src="/images/loading/loading.gif"
|
||||||
|
height={70}
|
||||||
|
width={120}
|
||||||
|
alt="infisical loading indicator"
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<div className="flex h-max max-w-md flex-col rounded-md border border-mineshaft-600 bg-mineshaft-800 p-6 text-center text-mineshaft-200">
|
||||||
|
<FontAwesomeIcon icon={faBugs} className="inlineli my-2 text-6xl" />
|
||||||
|
<p>
|
||||||
|
Something went wrong. Please contact{" "}
|
||||||
|
<a
|
||||||
|
className="inline cursor-pointer text-mineshaft-100 underline decoration-primary-500 underline-offset-4 opacity-80 duration-200 hover:opacity-100"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
href="mailto:[email protected]"
|
||||||
|
>
|
||||||
|
support@infisical.com
|
||||||
|
</a>{" "}
|
||||||
|
if the issue persists.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
import { createFileRoute } from "@tanstack/react-router";
|
||||||
|
import { zodValidator } from "@tanstack/zod-adapter";
|
||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
import { HasuraCloudConfigurePage } from "./HasuraCloudConfigurePage";
|
||||||
|
|
||||||
|
const HasuraCloudConfigurePageQueryParamsSchema = z.object({
|
||||||
|
integrationAuthId: z.string()
|
||||||
|
});
|
||||||
|
|
||||||
|
export const Route = createFileRoute(
|
||||||
|
"/_authenticate/_inject-org-details/secret-manager/$projectId/_secret-manager-layout/integrations/hasura-cloud/create"
|
||||||
|
)({
|
||||||
|
component: HasuraCloudConfigurePage,
|
||||||
|
validateSearch: zodValidator(HasuraCloudConfigurePageQueryParamsSchema)
|
||||||
|
});
|
||||||
+294
@@ -0,0 +1,294 @@
|
|||||||
|
import { useEffect, useState } from "react";
|
||||||
|
import { Helmet } from "react-helmet";
|
||||||
|
import { Controller, useForm } from "react-hook-form";
|
||||||
|
import { faArrowUpRightFromSquare, faBookOpen, faBugs } from "@fortawesome/free-solid-svg-icons";
|
||||||
|
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||||
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
|
import { useNavigate, useSearch } from "@tanstack/react-router";
|
||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
import { Button, Card, CardTitle, FormControl, Select, SelectItem } from "@app/components/v2";
|
||||||
|
import { SecretPathInput } from "@app/components/v2/SecretPathInput";
|
||||||
|
import { ROUTE_PATHS } from "@app/const/routes";
|
||||||
|
import { useWorkspace } from "@app/context";
|
||||||
|
// import { RadioGroup } from "@app/components/v2/RadioGroup";
|
||||||
|
import { useCreateIntegration } from "@app/hooks/api";
|
||||||
|
import {
|
||||||
|
useGetIntegrationAuthApps,
|
||||||
|
useGetIntegrationAuthById
|
||||||
|
} from "@app/hooks/api/integrationAuth";
|
||||||
|
import { IntegrationSyncBehavior } from "@app/hooks/api/integrations/types";
|
||||||
|
|
||||||
|
const initialSyncBehaviors = [
|
||||||
|
{
|
||||||
|
label: "No Import - Overwrite all values in Heroku",
|
||||||
|
value: IntegrationSyncBehavior.OVERWRITE_TARGET
|
||||||
|
},
|
||||||
|
{ label: "Import - Prefer values from Heroku", value: IntegrationSyncBehavior.PREFER_TARGET },
|
||||||
|
{ label: "Import - Prefer values from Infisical", value: IntegrationSyncBehavior.PREFER_SOURCE }
|
||||||
|
];
|
||||||
|
|
||||||
|
const schema = z.object({
|
||||||
|
selectedSourceEnvironment: z.string(),
|
||||||
|
secretPath: z.string(),
|
||||||
|
targetApp: z.string(),
|
||||||
|
initialSyncBehavior: z.nativeEnum(IntegrationSyncBehavior)
|
||||||
|
});
|
||||||
|
|
||||||
|
type FormData = z.infer<typeof schema>;
|
||||||
|
|
||||||
|
export const HerokuConfigurePage = () => {
|
||||||
|
const navigate = useNavigate();
|
||||||
|
|
||||||
|
const { currentWorkspace } = useWorkspace();
|
||||||
|
const { control, handleSubmit, setValue, watch } = useForm<FormData>({
|
||||||
|
resolver: zodResolver(schema),
|
||||||
|
defaultValues: {
|
||||||
|
secretPath: "/",
|
||||||
|
initialSyncBehavior: IntegrationSyncBehavior.PREFER_SOURCE,
|
||||||
|
selectedSourceEnvironment: currentWorkspace.environments[0].slug
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const selectedSourceEnvironment = watch("selectedSourceEnvironment");
|
||||||
|
|
||||||
|
const { mutateAsync } = useCreateIntegration();
|
||||||
|
// const { mutateAsync: mutateAsyncEnv } = useCreateWsEnvironment();
|
||||||
|
|
||||||
|
const integrationAuthId = useSearch({
|
||||||
|
from: ROUTE_PATHS.SecretManager.Integratons.HerokuConfigurePage.id,
|
||||||
|
select: (el) => el.integrationAuthId
|
||||||
|
});
|
||||||
|
|
||||||
|
const { data: integrationAuth } = useGetIntegrationAuthById((integrationAuthId as string) ?? "");
|
||||||
|
const { data: integrationAuthApps, isLoading: isIntegrationAuthAppsLoading } =
|
||||||
|
useGetIntegrationAuthApps({
|
||||||
|
integrationAuthId: (integrationAuthId as string) ?? ""
|
||||||
|
});
|
||||||
|
|
||||||
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (integrationAuthApps) {
|
||||||
|
if (integrationAuthApps.length > 0) {
|
||||||
|
setValue("targetApp", integrationAuthApps[0].name);
|
||||||
|
} else {
|
||||||
|
setValue("targetApp", "none");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, [integrationAuthApps]);
|
||||||
|
|
||||||
|
const onFormSubmit = async ({ secretPath, targetApp, initialSyncBehavior }: FormData) => {
|
||||||
|
try {
|
||||||
|
if (!integrationAuth?.id) return;
|
||||||
|
|
||||||
|
setIsLoading(true);
|
||||||
|
|
||||||
|
await mutateAsync({
|
||||||
|
integrationAuthId: integrationAuth?.id,
|
||||||
|
isActive: true,
|
||||||
|
app: targetApp,
|
||||||
|
sourceEnvironment: selectedSourceEnvironment,
|
||||||
|
secretPath,
|
||||||
|
metadata: {
|
||||||
|
initialSyncBehavior
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
setIsLoading(false);
|
||||||
|
navigate({
|
||||||
|
to: "/secret-manager/$projectId/integrations",
|
||||||
|
params: {
|
||||||
|
projectId: currentWorkspace.id
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return integrationAuth && selectedSourceEnvironment && integrationAuthApps ? (
|
||||||
|
<div className="flex h-full w-full flex-col items-center justify-center">
|
||||||
|
<Helmet>
|
||||||
|
<title>Set Up Heroku Integration</title>
|
||||||
|
</Helmet>
|
||||||
|
<Card className="max-w-lg rounded-md border border-mineshaft-600 p-0">
|
||||||
|
<CardTitle
|
||||||
|
className="px-6 text-left text-xl"
|
||||||
|
subTitle="Select which environment or folder in Infisical you want to sync to Heroku's environment variables."
|
||||||
|
>
|
||||||
|
<div className="flex flex-row items-center">
|
||||||
|
<div className="flex items-center">
|
||||||
|
<img src="/images/integrations/Heroku.png" height={30} width={30} alt="Heroku logo" />
|
||||||
|
</div>
|
||||||
|
<span className="ml-2">Heroku Integration </span>
|
||||||
|
<a
|
||||||
|
href="https://infisical.com/docs/integrations/cloud/heroku"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
>
|
||||||
|
<div className="mb-1 ml-2 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="mb-[0.07rem] ml-1.5 text-xxs"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</CardTitle>
|
||||||
|
<form onSubmit={handleSubmit(onFormSubmit)} className="px-6">
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
|
name="selectedSourceEnvironment"
|
||||||
|
render={({ field: { onChange, ...field }, fieldState: { error } }) => (
|
||||||
|
<FormControl
|
||||||
|
label="Project Environment"
|
||||||
|
errorText={error?.message}
|
||||||
|
isError={Boolean(error)}
|
||||||
|
>
|
||||||
|
<Select
|
||||||
|
defaultValue={field.value}
|
||||||
|
{...field}
|
||||||
|
onValueChange={(e) => onChange(e)}
|
||||||
|
className="w-full"
|
||||||
|
>
|
||||||
|
{currentWorkspace?.environments.map((sourceEnvironment) => (
|
||||||
|
<SelectItem
|
||||||
|
value={sourceEnvironment.slug}
|
||||||
|
key={`source-environment-${sourceEnvironment.slug}`}
|
||||||
|
>
|
||||||
|
{sourceEnvironment.name}
|
||||||
|
</SelectItem>
|
||||||
|
))}
|
||||||
|
</Select>
|
||||||
|
</FormControl>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
|
defaultValue=""
|
||||||
|
name="secretPath"
|
||||||
|
render={({ field, fieldState: { error } }) => (
|
||||||
|
<FormControl label="Secrets Path" isError={Boolean(error)} errorText={error?.message}>
|
||||||
|
<SecretPathInput
|
||||||
|
{...field}
|
||||||
|
placeholder="/"
|
||||||
|
environment={selectedSourceEnvironment}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
|
name="targetApp"
|
||||||
|
render={({ field: { onChange, ...field }, fieldState: { error } }) => {
|
||||||
|
return (
|
||||||
|
<FormControl label="Heroku App" errorText={error?.message} isError={Boolean(error)}>
|
||||||
|
<Select
|
||||||
|
{...field}
|
||||||
|
onValueChange={(e) => {
|
||||||
|
if (e === "") return;
|
||||||
|
onChange(e);
|
||||||
|
}}
|
||||||
|
className="w-full"
|
||||||
|
>
|
||||||
|
{integrationAuthApps.length > 0 ? (
|
||||||
|
integrationAuthApps.map((integrationAuthApp) => (
|
||||||
|
<SelectItem
|
||||||
|
value={String(integrationAuthApp.name as string)}
|
||||||
|
key={`target-app-${String(integrationAuthApp.appId)}`}
|
||||||
|
>
|
||||||
|
{integrationAuthApp.name}
|
||||||
|
</SelectItem>
|
||||||
|
))
|
||||||
|
) : (
|
||||||
|
<SelectItem value="none" key="target-app-none">
|
||||||
|
No apps found
|
||||||
|
</SelectItem>
|
||||||
|
)}
|
||||||
|
</Select>
|
||||||
|
</FormControl>
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
|
name="initialSyncBehavior"
|
||||||
|
render={({ field: { onChange, ...field }, fieldState: { error } }) => (
|
||||||
|
<FormControl
|
||||||
|
label="Initial Sync Behavior"
|
||||||
|
errorText={error?.message}
|
||||||
|
isError={Boolean(error)}
|
||||||
|
>
|
||||||
|
<Select {...field} onValueChange={(e) => onChange(e)} className="w-full">
|
||||||
|
{initialSyncBehaviors.map((b) => {
|
||||||
|
return (
|
||||||
|
<SelectItem value={b.value} key={`sync-behavior-${b.value}`}>
|
||||||
|
{b.label}
|
||||||
|
</SelectItem>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</Select>
|
||||||
|
</FormControl>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<Button
|
||||||
|
colorSchema="primary"
|
||||||
|
variant="outline_bg"
|
||||||
|
className="mb-6 ml-auto mt-2"
|
||||||
|
size="sm"
|
||||||
|
type="submit"
|
||||||
|
isLoading={isLoading}
|
||||||
|
isDisabled={integrationAuthApps.length === 0}
|
||||||
|
>
|
||||||
|
Create Integration
|
||||||
|
</Button>
|
||||||
|
</form>
|
||||||
|
</Card>
|
||||||
|
{/* {integrationType === "App" && <>
|
||||||
|
<div className="mt-6 w-full max-w-md border-t border-mineshaft-800" />
|
||||||
|
<div className="mt-6 flex w-full max-w-lg flex-col rounded-md border border-mineshaft-600 bg-mineshaft-800 p-4">
|
||||||
|
<div className="flex flex-row items-center">
|
||||||
|
<FontAwesomeIcon icon={faCircleInfo} className="text-xl text-mineshaft-200" />{" "}
|
||||||
|
<span className="text-md ml-3 text-mineshaft-100">Pro Tip</span>
|
||||||
|
</div>
|
||||||
|
<span className="mt-4 text-sm text-mineshaft-300">
|
||||||
|
After creating an integration, your secrets will start syncing immediately. This might
|
||||||
|
cause an unexpected override of current secrets in Heroku with secrets from Infisical.
|
||||||
|
</span>
|
||||||
|
</div></>} */}
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="flex h-full w-full items-center justify-center">
|
||||||
|
<Helmet>
|
||||||
|
<title>Set Up Heroku Integration</title>
|
||||||
|
</Helmet>
|
||||||
|
{isIntegrationAuthAppsLoading ? (
|
||||||
|
<img
|
||||||
|
src="/images/loading/loading.gif"
|
||||||
|
height={70}
|
||||||
|
width={120}
|
||||||
|
alt="infisical loading indicator"
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<div className="flex h-max max-w-md flex-col rounded-md border border-mineshaft-600 bg-mineshaft-800 p-6 text-center text-mineshaft-200">
|
||||||
|
<FontAwesomeIcon icon={faBugs} className="inlineli my-2 text-6xl" />
|
||||||
|
<p>
|
||||||
|
Something went wrong. Please contact{" "}
|
||||||
|
<a
|
||||||
|
className="inline cursor-pointer text-mineshaft-100 underline decoration-primary-500 underline-offset-4 opacity-80 duration-200 hover:opacity-100"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
href="mailto:[email protected]"
|
||||||
|
>
|
||||||
|
support@infisical.com
|
||||||
|
</a>{" "}
|
||||||
|
if the issue persists.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
import { createFileRoute } from "@tanstack/react-router";
|
||||||
|
import { zodValidator } from "@tanstack/zod-adapter";
|
||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
import { HerokuConfigurePage } from "./HerokuConfigurePage";
|
||||||
|
|
||||||
|
const HerokuConfigurePageQueryParamsSchema = z.object({
|
||||||
|
integrationAuthId: z.string()
|
||||||
|
});
|
||||||
|
|
||||||
|
export const Route = createFileRoute(
|
||||||
|
"/_authenticate/_inject-org-details/secret-manager/$projectId/_secret-manager-layout/integrations/heroku/create"
|
||||||
|
)({
|
||||||
|
component: HerokuConfigurePage,
|
||||||
|
validateSearch: zodValidator(HerokuConfigurePageQueryParamsSchema)
|
||||||
|
});
|
||||||
+45
@@ -0,0 +1,45 @@
|
|||||||
|
import { useEffect } from "react";
|
||||||
|
import { useNavigate, useSearch } from "@tanstack/react-router";
|
||||||
|
|
||||||
|
import { ROUTE_PATHS } from "@app/const/routes";
|
||||||
|
import { useWorkspace } from "@app/context";
|
||||||
|
import { useAuthorizeIntegration } from "@app/hooks/api";
|
||||||
|
|
||||||
|
export const HerokuOAuthCallbackPage = () => {
|
||||||
|
const navigate = useNavigate();
|
||||||
|
const { mutateAsync } = useAuthorizeIntegration();
|
||||||
|
|
||||||
|
const { code, state } = useSearch({
|
||||||
|
from: ROUTE_PATHS.SecretManager.Integratons.HerokuOauthCallbackPage.id
|
||||||
|
});
|
||||||
|
const { currentWorkspace } = useWorkspace();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
(async () => {
|
||||||
|
try {
|
||||||
|
// validate state
|
||||||
|
if (state !== localStorage.getItem("latestCSRFToken")) return;
|
||||||
|
localStorage.removeItem("latestCSRFToken");
|
||||||
|
const integrationAuth = await mutateAsync({
|
||||||
|
workspaceId: currentWorkspace.id,
|
||||||
|
code: code as string,
|
||||||
|
integration: "heroku"
|
||||||
|
});
|
||||||
|
|
||||||
|
navigate({
|
||||||
|
to: "/secret-manager/$projectId/integrations/heroku/create",
|
||||||
|
params: {
|
||||||
|
projectId: currentWorkspace.id
|
||||||
|
},
|
||||||
|
search: {
|
||||||
|
integrationAuthId: integrationAuth.id
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err);
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return <div />;
|
||||||
|
};
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
import { createFileRoute } from "@tanstack/react-router";
|
||||||
|
import { zodValidator } from "@tanstack/zod-adapter";
|
||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
import { HerokuOAuthCallbackPage } from "./HerokuOauthCallbackPage";
|
||||||
|
|
||||||
|
export const HerokuOAuthCallbackPageQueryParamsSchema = z.object({
|
||||||
|
state: z.string().catch(""),
|
||||||
|
code: z.string()
|
||||||
|
});
|
||||||
|
|
||||||
|
export const Route = createFileRoute(
|
||||||
|
"/_authenticate/_inject-org-details/secret-manager/$projectId/_secret-manager-layout/integrations/heroku/oauth2/callback"
|
||||||
|
)({
|
||||||
|
component: HerokuOAuthCallbackPage,
|
||||||
|
validateSearch: zodValidator(HerokuOAuthCallbackPageQueryParamsSchema)
|
||||||
|
});
|
||||||
+95
@@ -0,0 +1,95 @@
|
|||||||
|
import { useState } from "react";
|
||||||
|
import { useNavigate } from "@tanstack/react-router";
|
||||||
|
|
||||||
|
import { Button, Card, CardTitle, FormControl, Input } from "@app/components/v2";
|
||||||
|
import { useWorkspace } from "@app/context";
|
||||||
|
import { useSaveIntegrationAccessToken } from "@app/hooks/api";
|
||||||
|
|
||||||
|
export const LaravelForgeAuthorizePage = () => {
|
||||||
|
const navigate = useNavigate();
|
||||||
|
const { mutateAsync } = useSaveIntegrationAccessToken();
|
||||||
|
const { currentWorkspace } = useWorkspace();
|
||||||
|
const [apiKey, setApiKey] = useState("");
|
||||||
|
const [apiKeyErrorText, setApiKeyErrorText] = useState("");
|
||||||
|
const [serverId, setServerId] = useState("");
|
||||||
|
const [serverIdErrorText, setServerIdErrorText] = useState("");
|
||||||
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
|
|
||||||
|
const handleButtonClick = async () => {
|
||||||
|
try {
|
||||||
|
setApiKeyErrorText("");
|
||||||
|
setServerIdErrorText("");
|
||||||
|
|
||||||
|
if (apiKey.length === 0) {
|
||||||
|
setApiKeyErrorText("Access Token cannot be blank");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (serverId.length === 0) {
|
||||||
|
setServerIdErrorText("Server Id cannot be blank");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setIsLoading(true);
|
||||||
|
|
||||||
|
const integrationAuth = await mutateAsync({
|
||||||
|
workspaceId: currentWorkspace.id,
|
||||||
|
integration: "laravel-forge",
|
||||||
|
accessId: serverId,
|
||||||
|
accessToken: apiKey
|
||||||
|
});
|
||||||
|
|
||||||
|
setIsLoading(false);
|
||||||
|
|
||||||
|
navigate({
|
||||||
|
to: "/secret-manager/$projectId/integrations/laravel-forge/create",
|
||||||
|
params: {
|
||||||
|
projectId: currentWorkspace.id
|
||||||
|
},
|
||||||
|
search: {
|
||||||
|
integrationAuthId: integrationAuth.id
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
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">Laravel Forge Integration</CardTitle>
|
||||||
|
<FormControl
|
||||||
|
label="Laravel Forge Access Token"
|
||||||
|
errorText={apiKeyErrorText}
|
||||||
|
isError={apiKeyErrorText !== ""}
|
||||||
|
>
|
||||||
|
<Input
|
||||||
|
placeholder="Access Token"
|
||||||
|
value={apiKey}
|
||||||
|
onChange={(e) => setApiKey(e.target.value)}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
<FormControl
|
||||||
|
label="Laravel Forge Server ID"
|
||||||
|
errorText={serverIdErrorText}
|
||||||
|
isError={serverIdErrorText !== ""}
|
||||||
|
>
|
||||||
|
<Input
|
||||||
|
placeholder="123456"
|
||||||
|
value={serverId}
|
||||||
|
onChange={(e) => setServerId(e.target.value)}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
<Button
|
||||||
|
onClick={handleButtonClick}
|
||||||
|
color="mineshaft"
|
||||||
|
className="mt-4"
|
||||||
|
isLoading={isLoading}
|
||||||
|
>
|
||||||
|
Connect to Laravel Forge
|
||||||
|
</Button>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
import { createFileRoute } from "@tanstack/react-router";
|
||||||
|
|
||||||
|
import { LaravelForgeAuthorizePage } from "./LaravelForgeAuthorizePage";
|
||||||
|
|
||||||
|
export const Route = createFileRoute(
|
||||||
|
"/_authenticate/_inject-org-details/secret-manager/$projectId/_secret-manager-layout/integrations/laravel-forge/authorize"
|
||||||
|
)({
|
||||||
|
component: LaravelForgeAuthorizePage
|
||||||
|
});
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user