feat: completed migration of all integrations pages

This commit is contained in:
=
2025-01-03 19:15:39 +05:30
parent 6c5db3a187
commit 322536d738
167 changed files with 18362 additions and 65 deletions

View File

@ -67,7 +67,189 @@ export const ROUTE_PATHS = Object.freeze({
IntegrationDetailsByIDPage: setRoute(
"/secret-manager/$projectId/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: {
CertAuthDetailsByIDPage: setRoute(

View File

@ -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);
}
};

View File

@ -7,6 +7,7 @@ export {
useGetIntegrationAuthBitBucketWorkspaces,
useGetIntegrationAuthById,
useGetIntegrationAuthChecklyGroups,
useGetIntegrationAuthCircleCIOrganizations,
useGetIntegrationAuthGithubEnvs,
useGetIntegrationAuthGithubOrgs,
useGetIntegrationAuthNorthflankSecretGroups,

View File

@ -9,6 +9,7 @@ import {
BitBucketEnvironment,
BitBucketWorkspace,
ChecklyGroup,
CircleCIOrganization,
Environment,
HerokuPipelineCoupling,
IntegrationAuth,
@ -129,7 +130,9 @@ const integrationAuthKeys = {
integrationAuthId,
...params
}: TGetIntegrationAuthOctopusDeployScopeValuesDTO) =>
[{ integrationAuthId }, "getIntegrationAuthOctopusDeployScopeValues", params] as const
[{ integrationAuthId }, "getIntegrationAuthOctopusDeployScopeValues", params] as const,
getIntegrationAuthCircleCIOrganizations: (integrationAuthId: string) =>
[{ integrationAuthId }, "getIntegrationAuthCircleCIOrganizations"] as const
};
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 = ({
integrationAuthId,
appId

View File

@ -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(
'/_authenticate/_inject-org-details/organization/_layout/ssh/overview',
"/_authenticate/_inject-org-details/organization/_layout/ssh/overview"
)({
component: SshOverviewPage,
})
component: SshOverviewPage
});

View File

@ -1,6 +1,7 @@
import { useCallback, useEffect, useState } from "react";
import { Helmet } from "react-helmet";
import { useTranslation } from "react-i18next";
import { useNavigate } from "@tanstack/react-router";
import { motion } from "framer-motion";
import { createNotification } from "@app/components/notifications";
@ -29,6 +30,7 @@ enum IntegrationView {
const Page = () => {
const { currentWorkspace } = useWorkspace();
const navigate = useNavigate();
const { environments, id: workspaceId } = currentWorkspace;
const [view, setView] = useState<IntegrationView>(IntegrationView.New);
@ -97,7 +99,7 @@ const Page = () => {
if (!selectedCloudIntegration) return;
try {
redirectForProviderAuth(selectedCloudIntegration);
redirectForProviderAuth(currentWorkspace.id, navigate, selectedCloudIntegration);
} catch (error) {
console.error(error);
}

View File

@ -1,10 +1,13 @@
import crypto from "crypto";
import { NavigateFn } from "@tanstack/react-router";
import { createNotification } from "@app/components/notifications";
import {
decryptAssymmetric,
encryptAssymmetric
} from "@app/components/utilities/cryptography/crypto";
import { localStorageService } from "@app/helpers/localStorage";
import { TCloudIntegration, UserWsKeyPair } from "@app/hooks/api/types";
export const generateBotKey = (botPublicKey: string, latestKey: UserWsKeyPair) => {
@ -52,154 +55,309 @@ export const createIntegrationMissingEnvVarsNotification = (
title: "Missing Environment Variables"
});
export const redirectForProviderAuth = (integrationOption: TCloudIntegration) => {
export const redirectForProviderAuth = (
projectId: string,
navigate: NavigateFn,
integrationOption: TCloudIntegration
) => {
try {
// generate CSRF token for OAuth2 code-token exchange integrations
const state = crypto.randomBytes(16).toString("hex");
localStorage.setItem("latestCSRFToken", state);
localStorageService.setIntegrationProjectId(projectId);
let link = "";
switch (integrationOption.slug) {
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;
case "azure-key-vault":
case "azure-key-vault": {
if (!integrationOption.clientId) {
createIntegrationMissingEnvVarsNotification(integrationOption.slug);
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;
case "azure-app-configuration":
}
case "azure-app-configuration": {
if (!integrationOption.clientId) {
createIntegrationMissingEnvVarsNotification(integrationOption.slug);
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;
}
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;
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;
case "heroku":
case "heroku": {
if (!integrationOption.clientId) {
createIntegrationMissingEnvVarsNotification(integrationOption.slug);
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;
case "vercel":
}
case "vercel": {
if (!integrationOption.clientSlug) {
createIntegrationMissingEnvVarsNotification(integrationOption.slug);
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;
case "netlify":
}
case "netlify": {
if (!integrationOption.clientId) {
createIntegrationMissingEnvVarsNotification(integrationOption.slug);
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;
}
case "github":
link = `${window.location.origin}/integrations/github/auth-mode-selection`;
navigate({
to: "/secret-manager/$projectId/integrations/github/auth-mode-selection",
params: {
projectId
}
});
break;
case "gitlab":
link = `${window.location.origin}/integrations/gitlab/authorize`;
navigate({
to: "/secret-manager/$projectId/integrations/gitlab/authorize",
params: {
projectId
}
});
break;
case "render":
link = `${window.location.origin}/integrations/render/authorize`;
navigate({
to: "/secret-manager/$projectId/integrations/render/authorize",
params: {
projectId
}
});
break;
case "flyio":
link = `${window.location.origin}/integrations/flyio/authorize`;
navigate({
to: "/secret-manager/$projectId/integrations/flyio/authorize",
params: {
projectId
}
});
break;
case "circleci":
link = `${window.location.origin}/integrations/circleci/authorize`;
navigate({
to: "/secret-manager/$projectId/integrations/circleci/authorize",
params: {
projectId
}
});
break;
case "databricks":
link = `${window.location.origin}/integrations/databricks/authorize`;
navigate({
to: "/secret-manager/$projectId/integrations/databricks/authorize",
params: {
projectId
}
});
break;
case "laravel-forge":
link = `${window.location.origin}/integrations/laravel-forge/authorize`;
navigate({
to: "/secret-manager/$projectId/integrations/laravel-forge/authorize",
params: {
projectId
}
});
break;
case "travisci":
link = `${window.location.origin}/integrations/travisci/authorize`;
navigate({
to: "/secret-manager/$projectId/integrations/travisci/authorize",
params: {
projectId
}
});
break;
case "supabase":
link = `${window.location.origin}/integrations/supabase/authorize`;
navigate({
to: "/secret-manager/$projectId/integrations/supabase/authorize",
params: {
projectId
}
});
break;
case "checkly":
link = `${window.location.origin}/integrations/checkly/authorize`;
navigate({
to: "/secret-manager/$projectId/integrations/checkly/authorize",
params: {
projectId
}
});
break;
case "qovery":
link = `${window.location.origin}/integrations/qovery/authorize`;
navigate({
to: "/secret-manager/$projectId/integrations/qovery/authorize",
params: {
projectId
}
});
break;
case "railway":
link = `${window.location.origin}/integrations/railway/authorize`;
navigate({
to: "/secret-manager/$projectId/integrations/railway/authorize",
params: {
projectId
}
});
break;
case "terraform-cloud":
link = `${window.location.origin}/integrations/terraform-cloud/authorize`;
navigate({
to: "/secret-manager/$projectId/integrations/terraform-cloud/authorize",
params: {
projectId
}
});
break;
case "hashicorp-vault":
link = `${window.location.origin}/integrations/hashicorp-vault/authorize`;
navigate({
to: "/secret-manager/$projectId/integrations/hashicorp-vault/authorize",
params: {
projectId
}
});
break;
case "cloudflare-pages":
link = `${window.location.origin}/integrations/cloudflare-pages/authorize`;
navigate({
to: "/secret-manager/$projectId/integrations/cloudflare-pages/authorize",
params: {
projectId
}
});
break;
case "cloudflare-workers":
link = `${window.location.origin}/integrations/cloudflare-workers/authorize`;
navigate({
to: "/secret-manager/$projectId/integrations/cloudflare-workers/authorize",
params: {
projectId
}
});
break;
case "bitbucket":
case "bitbucket": {
if (!integrationOption.clientId) {
createIntegrationMissingEnvVarsNotification(integrationOption.slug, "cicd");
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;
}
case "codefresh":
link = `${window.location.origin}/integrations/codefresh/authorize`;
navigate({
to: "/secret-manager/$projectId/integrations/codefresh/authorize",
params: {
projectId
}
});
break;
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;
case "cloud-66":
link = `${window.location.origin}/integrations/cloud-66/authorize`;
navigate({
to: "/secret-manager/$projectId/integrations/cloud-66/authorize",
params: {
projectId
}
});
break;
case "northflank":
link = `${window.location.origin}/integrations/northflank/authorize`;
navigate({
to: "/secret-manager/$projectId/integrations/northflank/authorize",
params: {
projectId
}
});
break;
case "windmill":
link = `${window.location.origin}/integrations/windmill/authorize`;
navigate({
to: "/secret-manager/$projectId/integrations/windmill/authorize",
params: {
projectId
}
});
break;
case "teamcity":
link = `${window.location.origin}/integrations/teamcity/authorize`;
navigate({
to: "/secret-manager/$projectId/integrations/teamcity/authorize",
params: {
projectId
}
});
break;
case "hasura-cloud":
link = `${window.location.origin}/integrations/hasura-cloud/authorize`;
navigate({
to: "/secret-manager/$projectId/integrations/hasura-cloud/authorize",
params: {
projectId
}
});
break;
case "rundeck":
link = `${window.location.origin}/integrations/rundeck/authorize`;
navigate({
to: "/secret-manager/$projectId/integrations/rundeck/authorize",
params: {
projectId
}
});
break;
case "azure-devops":
link = `${window.location.origin}/integrations/azure-devops/authorize`;
navigate({
to: "/secret-manager/$projectId/integrations/azure-devops/authorize",
params: {
projectId
}
});
break;
case "octopus-deploy":
link = `${window.location.origin}/integrations/octopus-deploy/authorize`;
navigate({
to: "/secret-manager/$projectId/integrations/octopus-deploy/authorize",
params: {
projectId
}
});
break;
default:
break;
}
if (link !== "") {
window.location.assign(link);
}
} catch (err) {
console.error(err);
}
};
export const redirectToIntegrationAppConfigScreen = (provider: string, integrationAuthId: string) =>
`/integrations/${provider}/create?integrationAuthId=${integrationAuthId}`;

View File

@ -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(
'/_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
});

View File

@ -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>
);
};

View File

@ -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
});

View File

@ -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:support@infisical.com"
>
support@infisical.com
</a>{" "}
if the issue persists.
</p>
</div>
)}
</div>
);
};

View File

@ -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)
});

View File

@ -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>
);
};

View File

@ -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
});

View File

@ -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:support@infisical.com"
>
support@infisical.com
</a>{" "}
if the issue persists.
</p>
</div>
)}
</div>
);
};

View File

@ -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)
});

View File

@ -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 />
);
};

View File

@ -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)
});

View File

@ -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 />;
};

View File

@ -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)
});

View File

@ -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>
);
};

View File

@ -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
});

View File

@ -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 />
);
};

View File

@ -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)
});

View File

@ -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 />
);
};

View File

@ -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)
});

View File

@ -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 />;
};

View File

@ -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)
});

View File

@ -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>
);
};

View File

@ -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)
});

View File

@ -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 />;
};

View File

@ -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)
});

View File

@ -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>
);
};

View File

@ -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
});

View File

@ -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:support@infisical.com"
>
support@infisical.com
</a>{" "}
if the issue persists.
</p>
</div>
)}
</div>
);
};

View File

@ -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)
});

View File

@ -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>
);
};

View File

@ -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
});

View File

@ -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>
);
};

View File

@ -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)
});

View File

@ -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>
);
};

View File

@ -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
});

View File

@ -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 />
);
};

View File

@ -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)
});

View File

@ -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>
);
};

View File

@ -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
});

View File

@ -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 />
);
};

View File

@ -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)
});

View File

@ -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>
);
};

View File

@ -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
});

View File

@ -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 />
);
};

View File

@ -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)
});

View File

@ -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>
);
};

View File

@ -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
});

View File

@ -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 />
);
};

View File

@ -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)
});

View File

@ -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>
);
};

View File

@ -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
});

View File

@ -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:support@infisical.com"
>
support@infisical.com
</a>{" "}
if the issue persists.
</p>
</div>
)}
</div>
);
};

View File

@ -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)
});

View File

@ -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>
);
};

View File

@ -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
});

View File

@ -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 />
);
};

View File

@ -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)
});

View File

@ -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>
);
};

View File

@ -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
});

View File

@ -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:support@infisical.com"
>
support@infisical.com
</a>{" "}
if the issue persists.
</p>
</div>
)}
</div>
);
};

View File

@ -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)
});

View File

@ -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>
);
};

View File

@ -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
});

View File

@ -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&apos;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:support@infisical.com"
>
support@infisical.com
</a>{" "}
if the issue persists.
</p>
</div>
)}
</div>
);
};

View File

@ -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)
});

View File

@ -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 />;
};

View File

@ -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)
});

View File

@ -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>
);
};

View File

@ -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
});

View File

@ -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:support@infisical.com"
>
support@infisical.com
</a>{" "}
if the issue persists.
</p>
</div>
)}
</div>
);
};

View File

@ -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)
});

View File

@ -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 />;
};

View File

@ -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)
});

View File

@ -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>
);
};

View File

@ -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
});

View File

@ -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 &apos;Masked&apos; 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 &apos;Protected&apos; 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&apos;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:support@infisical.com"
>
support@infisical.com
</a>{" "}
if the issue persists.
</p>
</div>
)}
</div>
);
};

View File

@ -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)
});

View File

@ -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 />;
};

View File

@ -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)
});

View File

@ -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>
);
};

View File

@ -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
});

View File

@ -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:support@infisical.com"
>
support@infisical.com
</a>{" "}
if the issue persists.
</p>
</div>
)}
</div>
);
};

View File

@ -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)
});

View File

@ -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>
);
};

View File

@ -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
});

View File

@ -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:support@infisical.com"
>
support@infisical.com
</a>{" "}
if the issue persists.
</p>
</div>
)}
</div>
);
};

View File

@ -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)
});

View File

@ -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:support@infisical.com"
>
support@infisical.com
</a>{" "}
if the issue persists.
</p>
</div>
)}
</div>
);
};

View File

@ -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)
});

View File

@ -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 />;
};

View File

@ -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)
});

View File

@ -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>
);
};

View File

@ -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