mirror of
https://github.com/Infisical/infisical.git
synced 2025-03-29 22:02:57 +00:00
feat: completed migration of all integrations pages
This commit is contained in:
@ -67,7 +67,189 @@ export const ROUTE_PATHS = Object.freeze({
|
||||
IntegrationDetailsByIDPage: setRoute(
|
||||
"/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(
|
||||
|
9
frontend-v2/src/helpers/localStorage.ts
Normal file
9
frontend-v2/src/helpers/localStorage.ts
Normal 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);
|
||||
}
|
||||
};
|
@ -7,6 +7,7 @@ export {
|
||||
useGetIntegrationAuthBitBucketWorkspaces,
|
||||
useGetIntegrationAuthById,
|
||||
useGetIntegrationAuthChecklyGroups,
|
||||
useGetIntegrationAuthCircleCIOrganizations,
|
||||
useGetIntegrationAuthGithubEnvs,
|
||||
useGetIntegrationAuthGithubOrgs,
|
||||
useGetIntegrationAuthNorthflankSecretGroups,
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
});
|
||||
|
@ -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);
|
||||
}
|
||||
|
@ -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}`;
|
||||
|
@ -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
|
||||
});
|
||||
|
@ -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>
|
||||
);
|
||||
};
|
@ -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
|
||||
});
|
@ -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>
|
||||
);
|
||||
};
|
@ -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)
|
||||
});
|
@ -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>
|
||||
);
|
||||
};
|
@ -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
|
||||
});
|
@ -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>
|
||||
);
|
||||
};
|
@ -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)
|
||||
});
|
@ -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 />
|
||||
);
|
||||
};
|
@ -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)
|
||||
});
|
@ -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 />;
|
||||
};
|
@ -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)
|
||||
});
|
@ -0,0 +1,124 @@
|
||||
import { useState } from "react";
|
||||
import { Helmet } from "react-helmet";
|
||||
import { faArrowUpRightFromSquare, faBookOpen } from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { useNavigate } from "@tanstack/react-router";
|
||||
|
||||
import { Button, Card, CardTitle, FormControl, Input } from "@app/components/v2";
|
||||
import { useWorkspace } from "@app/context";
|
||||
import { useSaveIntegrationAccessToken } from "@app/hooks/api";
|
||||
|
||||
export const AzureDevopsAuthorizePage = () => {
|
||||
const navigate = useNavigate();
|
||||
const { mutateAsync } = useSaveIntegrationAccessToken();
|
||||
|
||||
const [apiKey, setApiKey] = useState("");
|
||||
const [devopsOrgName, setDevopsOrgName] = useState("");
|
||||
const [apiKeyErrorText, setApiKeyErrorText] = useState("");
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const { currentWorkspace } = useWorkspace();
|
||||
|
||||
const handleButtonClick = async () => {
|
||||
try {
|
||||
setApiKeyErrorText("");
|
||||
if (apiKey.length === 0) {
|
||||
setApiKeyErrorText("API Key cannot be blank");
|
||||
return;
|
||||
}
|
||||
|
||||
setIsLoading(true);
|
||||
|
||||
localStorage.setItem("azure-devops-org-name", devopsOrgName);
|
||||
|
||||
const integrationAuth = await mutateAsync({
|
||||
workspaceId: currentWorkspace.id,
|
||||
integration: "azure-devops",
|
||||
accessToken: btoa(`:${apiKey}`) // This is a base64 encoding of the API key without any username
|
||||
});
|
||||
|
||||
setIsLoading(false);
|
||||
|
||||
navigate({
|
||||
to: "/secret-manager/$projectId/integrations/azure-devops/create",
|
||||
params: {
|
||||
projectId: currentWorkspace.id
|
||||
},
|
||||
search: {
|
||||
integrationAuthId: integrationAuth.id
|
||||
}
|
||||
});
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex h-full w-full items-center justify-center">
|
||||
<Helmet>
|
||||
<title>Authorize Azure DevOps Integration</title>
|
||||
</Helmet>
|
||||
<Card className="max-w-lg rounded-md border border-mineshaft-600">
|
||||
<CardTitle
|
||||
className="px-6 text-left text-xl"
|
||||
subTitle="After adding the details below, you will be prompted to set up an integration for a particular Infisical project and environment."
|
||||
>
|
||||
<div className="flex flex-row items-center">
|
||||
<div className="flex items-center">
|
||||
<img
|
||||
src="/images/integrations/Amazon Web Services.png"
|
||||
height={35}
|
||||
width={35}
|
||||
alt="Azure DevOps logo"
|
||||
/>
|
||||
</div>
|
||||
<span className="ml-1.5">Azure DevOps Integration </span>
|
||||
<a
|
||||
target="_blank"
|
||||
href="https://infisical.com/docs/integrations/cloud/azure-devops"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
<div className="mb-1 ml-2 inline-block cursor-default rounded-md bg-yellow/20 px-1.5 pb-[0.03rem] pt-[0.04rem] text-sm text-yellow opacity-80 hover:opacity-100">
|
||||
<FontAwesomeIcon icon={faBookOpen} className="mr-1.5" />
|
||||
Docs
|
||||
<FontAwesomeIcon
|
||||
icon={faArrowUpRightFromSquare}
|
||||
className="mb-[0.07rem] ml-1.5 text-xxs"
|
||||
/>
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
</CardTitle>{" "}
|
||||
<FormControl
|
||||
className="px-6"
|
||||
label="Azure DevOps API Token"
|
||||
errorText={apiKeyErrorText}
|
||||
isError={apiKeyErrorText !== ""}
|
||||
>
|
||||
<Input placeholder="" value={apiKey} onChange={(e) => setApiKey(e.target.value)} />
|
||||
</FormControl>
|
||||
<FormControl
|
||||
className="px-6"
|
||||
label="Azure DevOps Organization Name"
|
||||
tooltipText="This is the slug of the organization. An example would be 'my-acme-org'"
|
||||
errorText={apiKeyErrorText}
|
||||
isError={apiKeyErrorText !== ""}
|
||||
>
|
||||
<Input
|
||||
placeholder=""
|
||||
value={devopsOrgName}
|
||||
onChange={(e) => setDevopsOrgName(e.target.value)}
|
||||
/>
|
||||
</FormControl>
|
||||
<Button
|
||||
onClick={handleButtonClick}
|
||||
colorSchema="primary"
|
||||
variant="outline_bg"
|
||||
className="mb-6 ml-auto mr-6 mt-2 w-min"
|
||||
isLoading={isLoading}
|
||||
>
|
||||
Connect to Azure DevOps
|
||||
</Button>
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
};
|
@ -0,0 +1,9 @@
|
||||
import { createFileRoute } from "@tanstack/react-router";
|
||||
|
||||
import { AzureDevopsAuthorizePage } from "./AzureDevopsAuthorizePage";
|
||||
|
||||
export const Route = createFileRoute(
|
||||
"/_authenticate/_inject-org-details/secret-manager/$projectId/_secret-manager-layout/integrations/azure-devops/authorize"
|
||||
)({
|
||||
component: AzureDevopsAuthorizePage
|
||||
});
|
@ -0,0 +1,192 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import { Helmet } from "react-helmet";
|
||||
import { faArrowUpRightFromSquare, faBookOpen } from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { useNavigate, useSearch } from "@tanstack/react-router";
|
||||
|
||||
import {
|
||||
Button,
|
||||
Card,
|
||||
CardTitle,
|
||||
FormControl,
|
||||
Input,
|
||||
Select,
|
||||
SelectItem
|
||||
} from "@app/components/v2";
|
||||
import { ROUTE_PATHS } from "@app/const/routes";
|
||||
import { useWorkspace } from "@app/context";
|
||||
import { useCreateIntegration } from "@app/hooks/api";
|
||||
import {
|
||||
useGetIntegrationAuthApps,
|
||||
useGetIntegrationAuthById
|
||||
} from "@app/hooks/api/integrationAuth";
|
||||
import { useGetWorkspaceById } from "@app/hooks/api/workspace";
|
||||
|
||||
export const AzureDevopsConfigurePage = () => {
|
||||
const navigate = useNavigate();
|
||||
const { mutateAsync } = useCreateIntegration();
|
||||
|
||||
const integrationAuthId = useSearch({
|
||||
from: ROUTE_PATHS.SecretManager.Integratons.AzureDevopsConfigurePage.id,
|
||||
select: (el) => el.integrationAuthId
|
||||
});
|
||||
const { currentWorkspace } = useWorkspace();
|
||||
|
||||
const { data: workspace } = useGetWorkspaceById(currentWorkspace.id);
|
||||
const { data: integrationAuth } = useGetIntegrationAuthById((integrationAuthId as string) ?? "");
|
||||
const { data: integrationAuthApps } = useGetIntegrationAuthApps({
|
||||
integrationAuthId: (integrationAuthId as string) ?? "",
|
||||
azureDevOpsOrgName: localStorage.getItem("azure-devops-org-name") ?? ""
|
||||
});
|
||||
|
||||
const [selectedSourceEnvironment, setSelectedSourceEnvironment] = useState("");
|
||||
const [secretPath, setSecretPath] = useState("/");
|
||||
const [targetApp, setTargetApp] = useState("");
|
||||
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (workspace) {
|
||||
setSelectedSourceEnvironment(workspace.environments[0].slug);
|
||||
}
|
||||
}, [workspace]);
|
||||
|
||||
useEffect(() => {
|
||||
if (integrationAuthApps) {
|
||||
if (integrationAuthApps.length > 0) {
|
||||
setTargetApp(integrationAuthApps[0].name);
|
||||
} else {
|
||||
setTargetApp("none");
|
||||
}
|
||||
}
|
||||
}, [integrationAuthApps]);
|
||||
|
||||
const handleButtonClick = async () => {
|
||||
try {
|
||||
if (!integrationAuth?.id) return;
|
||||
|
||||
setIsLoading(true);
|
||||
|
||||
await mutateAsync({
|
||||
integrationAuthId: integrationAuth?.id,
|
||||
isActive: true,
|
||||
app: localStorage.getItem("azure-devops-org-name") || "",
|
||||
appId: targetApp,
|
||||
sourceEnvironment: selectedSourceEnvironment,
|
||||
secretPath
|
||||
});
|
||||
|
||||
setIsLoading(false);
|
||||
|
||||
navigate({
|
||||
to: "/secret-manager/$projectId/integrations",
|
||||
params: {
|
||||
projectId: currentWorkspace.id
|
||||
}
|
||||
});
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
}
|
||||
};
|
||||
|
||||
return integrationAuth &&
|
||||
workspace &&
|
||||
selectedSourceEnvironment &&
|
||||
integrationAuthApps &&
|
||||
targetApp ? (
|
||||
<div className="flex h-full w-full items-center justify-center">
|
||||
<Helmet>
|
||||
<title>Set Up Azure DevOps Integration</title>
|
||||
</Helmet>
|
||||
<Card className="max-w-lg rounded-md border border-mineshaft-600">
|
||||
<CardTitle
|
||||
className="px-6 text-left text-xl"
|
||||
subTitle="Choose which environment in Infisical you want to sync to secrets in Azure DevOps."
|
||||
>
|
||||
<div className="flex flex-row items-center">
|
||||
<div className="flex items-center">
|
||||
<img
|
||||
src="/images/integrations/Amazon Web Services.png"
|
||||
height={35}
|
||||
width={35}
|
||||
alt="Azure DevOps logo"
|
||||
/>
|
||||
</div>
|
||||
<span className="ml-1.5">Azure DevOps Integration </span>
|
||||
<a
|
||||
href="https://infisical.com/docs/integrations/cloud/azure-devops"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
<div className="mb-1 ml-2 inline-block cursor-default rounded-md bg-yellow/20 px-1.5 pb-[0.03rem] pt-[0.04rem] text-sm text-yellow opacity-80 hover:opacity-100">
|
||||
<FontAwesomeIcon icon={faBookOpen} className="mr-1.5" />
|
||||
Docs
|
||||
<FontAwesomeIcon
|
||||
icon={faArrowUpRightFromSquare}
|
||||
className="mb-[0.07rem] ml-1.5 text-xxs"
|
||||
/>
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
</CardTitle>
|
||||
<FormControl label="Project Environment" className="mt-4 px-6">
|
||||
<Select
|
||||
value={selectedSourceEnvironment}
|
||||
onValueChange={(val) => setSelectedSourceEnvironment(val)}
|
||||
className="w-full border border-mineshaft-500"
|
||||
>
|
||||
{workspace?.environments.map((sourceEnvironment) => (
|
||||
<SelectItem
|
||||
value={sourceEnvironment.slug}
|
||||
key={`source-environment-${sourceEnvironment.slug}`}
|
||||
>
|
||||
{sourceEnvironment.name}
|
||||
</SelectItem>
|
||||
))}
|
||||
</Select>
|
||||
</FormControl>
|
||||
<FormControl className="px-6" label="Secrets Path">
|
||||
<Input
|
||||
value={secretPath}
|
||||
onChange={(evt) => setSecretPath(evt.target.value)}
|
||||
placeholder="Provide a path, default is /"
|
||||
/>
|
||||
</FormControl>
|
||||
<FormControl label="Azure DevOps Project" className="px-6">
|
||||
<Select
|
||||
value={targetApp}
|
||||
onValueChange={(val) => setTargetApp(val)}
|
||||
className="w-full border border-mineshaft-500"
|
||||
isDisabled={integrationAuthApps.length === 0}
|
||||
>
|
||||
{integrationAuthApps.length > 0 ? (
|
||||
integrationAuthApps.map((integrationAuthApp) => (
|
||||
<SelectItem
|
||||
value={integrationAuthApp.name}
|
||||
key={`target-environment-${integrationAuthApp.name}`}
|
||||
>
|
||||
{integrationAuthApp.name}
|
||||
</SelectItem>
|
||||
))
|
||||
) : (
|
||||
<SelectItem value="none" key="target-app-none">
|
||||
No projects found
|
||||
</SelectItem>
|
||||
)}
|
||||
</Select>
|
||||
</FormControl>
|
||||
<Button
|
||||
onClick={handleButtonClick}
|
||||
colorSchema="primary"
|
||||
variant="outline_bg"
|
||||
className="mb-6 ml-auto mr-6 mt-2 w-min"
|
||||
isLoading={isLoading}
|
||||
>
|
||||
Create Integration
|
||||
</Button>
|
||||
</Card>
|
||||
</div>
|
||||
) : (
|
||||
<div />
|
||||
);
|
||||
};
|
@ -0,0 +1,16 @@
|
||||
import { createFileRoute } from "@tanstack/react-router";
|
||||
import { zodValidator } from "@tanstack/zod-adapter";
|
||||
import { z } from "zod";
|
||||
|
||||
import { AzureDevopsConfigurePage } from "./AzureDevopsConfigurePage";
|
||||
|
||||
const AzureDevopsConfigurePageQueryParamsSchema = z.object({
|
||||
integrationAuthId: z.string()
|
||||
});
|
||||
|
||||
export const Route = createFileRoute(
|
||||
"/_authenticate/_inject-org-details/secret-manager/$projectId/_secret-manager-layout/integrations/azure-devops/create"
|
||||
)({
|
||||
component: AzureDevopsConfigurePage,
|
||||
validateSearch: zodValidator(AzureDevopsConfigurePageQueryParamsSchema)
|
||||
});
|
@ -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 />
|
||||
);
|
||||
};
|
@ -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)
|
||||
});
|
@ -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 />;
|
||||
};
|
@ -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)
|
||||
});
|
@ -0,0 +1,307 @@
|
||||
import { useEffect } from "react";
|
||||
import { Controller, useForm } from "react-hook-form";
|
||||
import { SiBitbucket } from "react-icons/si";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { useNavigate, useSearch } from "@tanstack/react-router";
|
||||
import { z } from "zod";
|
||||
|
||||
import { createNotification } from "@app/components/notifications";
|
||||
import {
|
||||
Button,
|
||||
Card,
|
||||
CardTitle,
|
||||
FilterableSelect,
|
||||
FormControl,
|
||||
Input,
|
||||
Spinner
|
||||
} from "@app/components/v2";
|
||||
import { ROUTE_PATHS } from "@app/const/routes";
|
||||
import { useWorkspace } from "@app/context";
|
||||
import {
|
||||
useCreateIntegration,
|
||||
useGetIntegrationAuthApps,
|
||||
useGetIntegrationAuthBitBucketWorkspaces
|
||||
} from "@app/hooks/api";
|
||||
import { useGetIntegrationAuthBitBucketEnvironments } from "@app/hooks/api/integrationAuth/queries";
|
||||
|
||||
enum BitbucketScope {
|
||||
Repo = "repo",
|
||||
Env = "environment"
|
||||
}
|
||||
|
||||
const ScopeOptions = [
|
||||
{
|
||||
label: "Repository",
|
||||
value: BitbucketScope.Repo
|
||||
},
|
||||
{
|
||||
label: "Deployment Environment",
|
||||
value: BitbucketScope.Env
|
||||
}
|
||||
];
|
||||
|
||||
const formSchema = z
|
||||
.object({
|
||||
secretPath: z.string().default("/"),
|
||||
sourceEnvironment: z.object({ name: z.string(), slug: z.string() }),
|
||||
targetRepo: z.object({ name: z.string(), appId: z.string() }),
|
||||
targetWorkspace: z.object({ name: z.string(), slug: z.string() }),
|
||||
targetEnvironment: z.object({ name: z.string(), uuid: z.string() }).optional(),
|
||||
scope: z.object({ label: z.string(), value: z.nativeEnum(BitbucketScope) })
|
||||
})
|
||||
.superRefine((val, ctx) => {
|
||||
if (val.scope.value === BitbucketScope.Env && !val.targetEnvironment) {
|
||||
ctx.addIssue({
|
||||
code: z.ZodIssueCode.custom,
|
||||
path: ["targetEnvironment"],
|
||||
message: "Required"
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
type TFormData = z.infer<typeof formSchema>;
|
||||
|
||||
export const BitbucketConfigurePage = () => {
|
||||
const navigate = useNavigate();
|
||||
const createIntegration = useCreateIntegration();
|
||||
|
||||
const { watch, control, reset, handleSubmit } = useForm<TFormData>({
|
||||
resolver: zodResolver(formSchema),
|
||||
defaultValues: {
|
||||
secretPath: "/"
|
||||
}
|
||||
});
|
||||
|
||||
const bitBucketWorkspace = watch("targetWorkspace");
|
||||
const bitBucketRepo = watch("targetRepo");
|
||||
|
||||
const integrationAuthId = useSearch({
|
||||
from: ROUTE_PATHS.SecretManager.Integratons.BitbucketConfigurePage.id,
|
||||
select: (el) => el.integrationAuthId
|
||||
});
|
||||
const { currentWorkspace } = useWorkspace();
|
||||
|
||||
const { data: bitbucketWorkspaces, isPending: isBitbucketWorkspacesLoading } =
|
||||
useGetIntegrationAuthBitBucketWorkspaces((integrationAuthId as string) ?? "");
|
||||
|
||||
const { data: bitbucketRepos, isPending: isBitbucketReposLoading } = useGetIntegrationAuthApps({
|
||||
integrationAuthId: (integrationAuthId as string) ?? "",
|
||||
workspaceSlug: bitBucketWorkspace?.slug
|
||||
});
|
||||
|
||||
const { data: bitbucketEnvironments } = useGetIntegrationAuthBitBucketEnvironments(
|
||||
{
|
||||
integrationAuthId: (integrationAuthId as string) ?? "",
|
||||
workspaceSlug: bitBucketWorkspace?.slug,
|
||||
repoSlug: bitBucketRepo?.appId
|
||||
},
|
||||
{ enabled: Boolean(bitBucketWorkspace?.slug && bitBucketRepo?.appId) }
|
||||
);
|
||||
|
||||
const onSubmit = async ({
|
||||
targetRepo,
|
||||
sourceEnvironment,
|
||||
targetWorkspace,
|
||||
secretPath,
|
||||
targetEnvironment,
|
||||
scope
|
||||
}: TFormData) => {
|
||||
try {
|
||||
await createIntegration.mutateAsync({
|
||||
integrationAuthId,
|
||||
isActive: true,
|
||||
app: targetRepo.name,
|
||||
appId: targetRepo.appId,
|
||||
sourceEnvironment: sourceEnvironment.slug,
|
||||
targetEnvironment: targetWorkspace.name,
|
||||
targetEnvironmentId: targetWorkspace.slug,
|
||||
...(scope.value === BitbucketScope.Env &&
|
||||
targetEnvironment && {
|
||||
targetService: targetEnvironment.name,
|
||||
targetServiceId: targetEnvironment.uuid
|
||||
}),
|
||||
secretPath
|
||||
});
|
||||
|
||||
createNotification({
|
||||
type: "success",
|
||||
text: "Successfully created integration"
|
||||
});
|
||||
navigate({
|
||||
to: "/secret-manager/$projectId/integrations",
|
||||
params: {
|
||||
projectId: currentWorkspace.id
|
||||
}
|
||||
});
|
||||
} catch (err) {
|
||||
createNotification({
|
||||
type: "error",
|
||||
text: "Failed to create integration"
|
||||
});
|
||||
console.error(err);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (!bitbucketRepos || !bitbucketWorkspaces || !currentWorkspace) return;
|
||||
|
||||
reset({
|
||||
targetRepo: bitbucketRepos[0],
|
||||
targetWorkspace: bitbucketWorkspaces[0],
|
||||
sourceEnvironment: currentWorkspace.environments[0],
|
||||
secretPath: "/",
|
||||
scope: ScopeOptions[0]
|
||||
});
|
||||
}, [bitbucketWorkspaces, bitbucketRepos, currentWorkspace]);
|
||||
|
||||
if (isBitbucketWorkspacesLoading || isBitbucketReposLoading)
|
||||
return (
|
||||
<div className="flex h-full w-full items-center justify-center p-24">
|
||||
<Spinner />
|
||||
</div>
|
||||
);
|
||||
|
||||
const scope = watch("scope");
|
||||
|
||||
return (
|
||||
<form
|
||||
onSubmit={handleSubmit(onSubmit)}
|
||||
className="flex h-full w-full items-center justify-center"
|
||||
>
|
||||
<Card className="max-w-md rounded-md p-8 pt-4">
|
||||
<CardTitle className="text-center">
|
||||
<SiBitbucket size="1.2rem" className="mb-1 mr-2 inline-block" />
|
||||
Bitbucket Integration
|
||||
</CardTitle>
|
||||
<Controller
|
||||
control={control}
|
||||
name="sourceEnvironment"
|
||||
render={({ field: { value, onChange }, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
errorText={error?.message}
|
||||
isError={Boolean(error)}
|
||||
label="Project Environment"
|
||||
>
|
||||
<FilterableSelect
|
||||
getOptionValue={(option) => option.slug}
|
||||
value={value}
|
||||
getOptionLabel={(option) => option.name}
|
||||
onChange={onChange}
|
||||
options={currentWorkspace?.environments}
|
||||
placeholder="Select a project environment"
|
||||
isDisabled={!currentWorkspace?.environments.length}
|
||||
/>
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
<Controller
|
||||
control={control}
|
||||
name="secretPath"
|
||||
render={({ field: { value, onChange }, fieldState: { error } }) => (
|
||||
<FormControl isError={Boolean(error)} label="Secrets Path">
|
||||
<Input
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
placeholder='Provide a path (defaults to "/")'
|
||||
/>
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
<Controller
|
||||
control={control}
|
||||
name="targetWorkspace"
|
||||
render={({ field: { value, onChange }, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
errorText={error?.message}
|
||||
isError={Boolean(error)}
|
||||
label="Bitbucket Workspace"
|
||||
>
|
||||
<FilterableSelect
|
||||
getOptionValue={(option) => option.slug}
|
||||
value={value}
|
||||
getOptionLabel={(option) => option.name}
|
||||
onChange={onChange}
|
||||
options={bitbucketWorkspaces}
|
||||
placeholder={
|
||||
bitbucketWorkspaces?.length ? "Select a workspace..." : "No workspaces found..."
|
||||
}
|
||||
isDisabled={!bitbucketWorkspaces?.length}
|
||||
/>
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
<Controller
|
||||
control={control}
|
||||
name="targetRepo"
|
||||
render={({ field: { value, onChange }, fieldState: { error } }) => (
|
||||
<FormControl errorText={error?.message} isError={Boolean(error)} label="Bitbucket Repo">
|
||||
<FilterableSelect
|
||||
getOptionValue={(option) => option.appId!}
|
||||
value={value}
|
||||
getOptionLabel={(option) => option.name}
|
||||
onChange={onChange}
|
||||
options={bitbucketRepos}
|
||||
placeholder={
|
||||
bitbucketRepos?.length ? "Select a repository..." : "No repositories found..."
|
||||
}
|
||||
isDisabled={!bitbucketRepos?.length}
|
||||
/>
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
<Controller
|
||||
control={control}
|
||||
name="scope"
|
||||
render={({ field: { value, onChange }, fieldState: { error } }) => (
|
||||
<FormControl errorText={error?.message} isError={Boolean(error)} label="Scope">
|
||||
<FilterableSelect
|
||||
value={value}
|
||||
getOptionValue={(option) => option.value}
|
||||
getOptionLabel={(option) => option.label}
|
||||
onChange={onChange}
|
||||
options={ScopeOptions}
|
||||
/>
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
|
||||
{scope?.value === BitbucketScope.Env && (
|
||||
<Controller
|
||||
control={control}
|
||||
name="targetEnvironment"
|
||||
render={({ field: { value, onChange }, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
errorText={error?.message}
|
||||
isError={Boolean(error)}
|
||||
label="Bitbucket Deployment Environment"
|
||||
>
|
||||
<FilterableSelect
|
||||
getOptionValue={(option) => option.uuid}
|
||||
value={value}
|
||||
getOptionLabel={(option) => option.name}
|
||||
onChange={onChange}
|
||||
options={bitbucketEnvironments}
|
||||
placeholder={
|
||||
bitbucketEnvironments?.length
|
||||
? "Select an environment..."
|
||||
: "No environments found..."
|
||||
}
|
||||
isDisabled={!bitbucketEnvironments?.length}
|
||||
/>
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
<Button
|
||||
type="submit"
|
||||
colorSchema="primary"
|
||||
className="mt-4"
|
||||
isLoading={createIntegration.isPending}
|
||||
isDisabled={createIntegration.isPending || !bitbucketRepos?.length}
|
||||
>
|
||||
Create Integration
|
||||
</Button>
|
||||
</Card>
|
||||
</form>
|
||||
);
|
||||
};
|
@ -0,0 +1,16 @@
|
||||
import { createFileRoute } from "@tanstack/react-router";
|
||||
import { zodValidator } from "@tanstack/zod-adapter";
|
||||
import { z } from "zod";
|
||||
|
||||
import { BitbucketConfigurePage } from "./BitbucketConfigurePage";
|
||||
|
||||
const BitbucketConfigurePageQueryParamsSchema = z.object({
|
||||
integrationAuthId: z.string()
|
||||
});
|
||||
|
||||
export const Route = createFileRoute(
|
||||
"/_authenticate/_inject-org-details/secret-manager/$projectId/_secret-manager-layout/integrations/bitbucket/create"
|
||||
)({
|
||||
component: BitbucketConfigurePage,
|
||||
validateSearch: zodValidator(BitbucketConfigurePageQueryParamsSchema)
|
||||
});
|
@ -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 />;
|
||||
};
|
@ -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)
|
||||
});
|
@ -0,0 +1,115 @@
|
||||
import { useState } from "react";
|
||||
import { Helmet } from "react-helmet";
|
||||
import { faArrowUpRightFromSquare, faBookOpen } from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { useNavigate } from "@tanstack/react-router";
|
||||
|
||||
import { Button, Card, CardTitle, FormControl, Input } from "@app/components/v2";
|
||||
import { useWorkspace } from "@app/context";
|
||||
import { useSaveIntegrationAccessToken } from "@app/hooks/api";
|
||||
|
||||
export const ChecklyAuthorizePage = () => {
|
||||
const navigate = useNavigate();
|
||||
const { mutateAsync } = useSaveIntegrationAccessToken();
|
||||
const { currentWorkspace } = useWorkspace();
|
||||
|
||||
const [accessToken, setAccessToken] = useState("");
|
||||
const [accessTokenErrorText, setAccessTokenErrorText] = useState("");
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
|
||||
const handleButtonClick = async () => {
|
||||
try {
|
||||
setAccessTokenErrorText("");
|
||||
if (accessToken.length === 0) {
|
||||
setAccessTokenErrorText("Access token cannot be blank");
|
||||
return;
|
||||
}
|
||||
|
||||
setIsLoading(true);
|
||||
|
||||
const integrationAuth = await mutateAsync({
|
||||
workspaceId: currentWorkspace.id,
|
||||
integration: "checkly",
|
||||
accessToken
|
||||
});
|
||||
|
||||
setIsLoading(false);
|
||||
|
||||
navigate({
|
||||
to: "/secret-manager/$projectId/integrations/checkly/create",
|
||||
params: {
|
||||
projectId: currentWorkspace.id
|
||||
},
|
||||
search: {
|
||||
integrationAuthId: integrationAuth.id
|
||||
}
|
||||
});
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex h-full w-full items-center justify-center">
|
||||
<Helmet>
|
||||
<title>Authorize Checkly Integration</title>
|
||||
</Helmet>
|
||||
<Card className="mb-12 max-w-lg rounded-md border border-mineshaft-600">
|
||||
<CardTitle
|
||||
className="px-6 text-left text-xl"
|
||||
subTitle="After adding your API key, you will be prompted to set up an integration for a particular Infisical project and environment."
|
||||
>
|
||||
<div className="flex flex-row items-center">
|
||||
<div className="flex items-center pb-0.5">
|
||||
<img
|
||||
src="/images/integrations/Checkly.png"
|
||||
height={30}
|
||||
width={30}
|
||||
alt="Checkly logo"
|
||||
/>
|
||||
</div>
|
||||
<span className="ml-2.5">Checkly Integration </span>
|
||||
<a
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
href="https://infisical.com/docs/integrations/cloud/checkly"
|
||||
>
|
||||
<div className="mb-1 ml-2 inline-block cursor-default rounded-md bg-yellow/20 px-1.5 pb-[0.03rem] pt-[0.04rem] text-sm text-yellow opacity-80 hover:opacity-100">
|
||||
<FontAwesomeIcon icon={faBookOpen} className="mr-1.5" />
|
||||
Docs
|
||||
<FontAwesomeIcon
|
||||
icon={faArrowUpRightFromSquare}
|
||||
className="mb-[0.07rem] ml-1.5 text-xxs"
|
||||
/>
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
</CardTitle>
|
||||
<FormControl
|
||||
label="Checkly API key"
|
||||
errorText={accessTokenErrorText}
|
||||
isError={accessTokenErrorText !== ""}
|
||||
className="mx-6"
|
||||
>
|
||||
<Input
|
||||
placeholder=""
|
||||
value={accessToken}
|
||||
type="password"
|
||||
autoComplete="new-password"
|
||||
onChange={(e) => setAccessToken(e.target.value)}
|
||||
/>
|
||||
</FormControl>
|
||||
<Button
|
||||
onClick={handleButtonClick}
|
||||
colorSchema="primary"
|
||||
variant="outline_bg"
|
||||
className="mb-6 ml-auto mr-6 mt-2 w-min"
|
||||
isFullWidth={false}
|
||||
isLoading={isLoading}
|
||||
>
|
||||
Connect to Checkly
|
||||
</Button>
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
};
|
@ -0,0 +1,9 @@
|
||||
import { createFileRoute } from "@tanstack/react-router";
|
||||
|
||||
import { ChecklyAuthorizePage } from "./ChecklyAuthorizePage";
|
||||
|
||||
export const Route = createFileRoute(
|
||||
"/_authenticate/_inject-org-details/secret-manager/$projectId/_secret-manager-layout/integrations/checkly/authorize"
|
||||
)({
|
||||
component: ChecklyAuthorizePage
|
||||
});
|
@ -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>
|
||||
);
|
||||
};
|
@ -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)
|
||||
});
|
@ -0,0 +1,108 @@
|
||||
import { useState } from "react";
|
||||
import { Helmet } from "react-helmet";
|
||||
import { faArrowUpRightFromSquare, faBookOpen } from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { useNavigate } from "@tanstack/react-router";
|
||||
|
||||
import { Button, Card, CardTitle, FormControl, Input } from "@app/components/v2";
|
||||
import { useWorkspace } from "@app/context";
|
||||
import { useSaveIntegrationAccessToken } from "@app/hooks/api";
|
||||
|
||||
export const CircleCIAuthorizePage = () => {
|
||||
const navigate = useNavigate();
|
||||
const { mutateAsync } = useSaveIntegrationAccessToken();
|
||||
|
||||
const [apiKey, setApiKey] = useState("");
|
||||
const [apiKeyErrorText, setApiKeyErrorText] = useState("");
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const { currentWorkspace } = useWorkspace();
|
||||
|
||||
const handleButtonClick = async () => {
|
||||
try {
|
||||
setApiKeyErrorText("");
|
||||
if (apiKey.length === 0) {
|
||||
setApiKeyErrorText("API Key cannot be blank");
|
||||
return;
|
||||
}
|
||||
|
||||
setIsLoading(true);
|
||||
|
||||
const integrationAuth = await mutateAsync({
|
||||
workspaceId: currentWorkspace.id,
|
||||
integration: "circleci",
|
||||
accessToken: apiKey
|
||||
});
|
||||
|
||||
setIsLoading(false);
|
||||
|
||||
navigate({
|
||||
to: "/secret-manager/$projectId/integrations/circleci/create",
|
||||
params: {
|
||||
projectId: currentWorkspace.id
|
||||
},
|
||||
search: {
|
||||
integrationAuthId: integrationAuth.id
|
||||
}
|
||||
});
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex h-full w-full items-center justify-center">
|
||||
<Helmet>
|
||||
<title>Authorize CircleCI Integration</title>
|
||||
</Helmet>
|
||||
<Card className="mb-12 max-w-lg rounded-md border border-mineshaft-600">
|
||||
<CardTitle
|
||||
className="px-6 text-left text-xl"
|
||||
subTitle="After adding your API Token, you will be prompted to set up an integration for a particular Infisical project and environment."
|
||||
>
|
||||
<div className="flex flex-row items-center">
|
||||
<div className="flex items-center pb-0.5">
|
||||
<img
|
||||
src="/images/integrations/CircleCI.png"
|
||||
height={30}
|
||||
width={30}
|
||||
alt="CircleCI logo"
|
||||
/>
|
||||
</div>
|
||||
<span className="ml-1.5">CircleCI Integration </span>
|
||||
<a
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
href="https://infisical.com/docs/integrations/cicd/circleci"
|
||||
>
|
||||
<div className="mb-1 ml-2 inline-block cursor-default rounded-md bg-yellow/20 px-1.5 pb-[0.03rem] pt-[0.04rem] text-sm text-yellow opacity-80 hover:opacity-100">
|
||||
<FontAwesomeIcon icon={faBookOpen} className="mr-1.5" />
|
||||
Docs
|
||||
<FontAwesomeIcon
|
||||
icon={faArrowUpRightFromSquare}
|
||||
className="mb-[0.07rem] ml-1.5 text-xxs"
|
||||
/>
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
</CardTitle>
|
||||
<FormControl
|
||||
label="CircleCI API Token"
|
||||
errorText={apiKeyErrorText}
|
||||
isError={apiKeyErrorText !== ""}
|
||||
className="px-6"
|
||||
>
|
||||
<Input placeholder="" value={apiKey} onChange={(e) => setApiKey(e.target.value)} />
|
||||
</FormControl>
|
||||
<Button
|
||||
onClick={handleButtonClick}
|
||||
colorSchema="primary"
|
||||
variant="outline_bg"
|
||||
className="mb-6 ml-auto mr-6 mt-2 w-min"
|
||||
isLoading={isLoading}
|
||||
>
|
||||
Connect to CircleCI
|
||||
</Button>
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
};
|
@ -0,0 +1,9 @@
|
||||
import { createFileRoute } from "@tanstack/react-router";
|
||||
|
||||
import { CircleCIAuthorizePage } from "./CircleCIAuthorizePage";
|
||||
|
||||
export const Route = createFileRoute(
|
||||
"/_authenticate/_inject-org-details/secret-manager/$projectId/_secret-manager-layout/integrations/circleci/authorize"
|
||||
)({
|
||||
component: CircleCIAuthorizePage
|
||||
});
|
@ -0,0 +1,313 @@
|
||||
import { Controller, useForm } from "react-hook-form";
|
||||
import { faArrowUpRightFromSquare, faBookOpen } from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { useNavigate, useSearch } from "@tanstack/react-router";
|
||||
import { z } from "zod";
|
||||
|
||||
import { createNotification } from "@app/components/notifications";
|
||||
import {
|
||||
Button,
|
||||
Card,
|
||||
CardTitle,
|
||||
FilterableSelect,
|
||||
FormControl,
|
||||
Select,
|
||||
SelectItem,
|
||||
Spinner
|
||||
} from "@app/components/v2";
|
||||
import { SecretPathInput } from "@app/components/v2/SecretPathInput";
|
||||
import { ROUTE_PATHS } from "@app/const/routes";
|
||||
import { useWorkspace } from "@app/context";
|
||||
import { useCreateIntegration, useGetIntegrationAuthCircleCIOrganizations } from "@app/hooks/api";
|
||||
import { CircleCiScope } from "@app/hooks/api/integrationAuth/types";
|
||||
|
||||
const formSchema = z.discriminatedUnion("scope", [
|
||||
z.object({
|
||||
scope: z.literal(CircleCiScope.Context),
|
||||
secretPath: z.string().default("/"),
|
||||
sourceEnvironment: z.object({ name: z.string(), slug: z.string() }),
|
||||
targetOrg: z.object({ name: z.string().min(1), slug: z.string().min(1) }),
|
||||
targetContext: z.object({ name: z.string().min(1), id: z.string().min(1) })
|
||||
}),
|
||||
z.object({
|
||||
scope: z.literal(CircleCiScope.Project),
|
||||
secretPath: z.string().default("/"),
|
||||
sourceEnvironment: z.object({ name: z.string(), slug: z.string() }),
|
||||
targetOrg: z.object({ name: z.string().min(1), slug: z.string().min(1) }),
|
||||
targetProject: z.object({ name: z.string().min(1), id: z.string().min(1) })
|
||||
})
|
||||
]);
|
||||
|
||||
type TFormData = z.infer<typeof formSchema>;
|
||||
|
||||
export const CircleCIConfigurePage = () => {
|
||||
const navigate = useNavigate();
|
||||
const { mutateAsync, isPending: isCreatingIntegration } = useCreateIntegration();
|
||||
const { currentWorkspace } = useWorkspace();
|
||||
|
||||
const integrationAuthId = useSearch({
|
||||
from: ROUTE_PATHS.SecretManager.Integratons.CircleConfigurePage.id,
|
||||
select: (el) => el.integrationAuthId
|
||||
});
|
||||
|
||||
const { control, watch, handleSubmit, setValue } = useForm<TFormData>({
|
||||
resolver: zodResolver(formSchema),
|
||||
defaultValues: {
|
||||
secretPath: "/",
|
||||
sourceEnvironment: currentWorkspace?.environments[0],
|
||||
scope: CircleCiScope.Project
|
||||
}
|
||||
});
|
||||
|
||||
const selectedScope = watch("scope");
|
||||
const selectedOrg = watch("targetOrg");
|
||||
|
||||
const { data: circleCIOrganizations, isPending: isCircleCIOrganizationsLoading } =
|
||||
useGetIntegrationAuthCircleCIOrganizations(integrationAuthId);
|
||||
|
||||
const selectedOrganizationEntry = selectedOrg
|
||||
? circleCIOrganizations?.find((org) => org.slug === selectedOrg.slug)
|
||||
: undefined;
|
||||
|
||||
const onSubmit = async (data: TFormData) => {
|
||||
try {
|
||||
if (data.scope === CircleCiScope.Context) {
|
||||
await mutateAsync({
|
||||
scope: data.scope,
|
||||
integrationAuthId,
|
||||
isActive: true,
|
||||
sourceEnvironment: data.sourceEnvironment.slug,
|
||||
app: data.targetContext.name,
|
||||
appId: data.targetContext.id,
|
||||
owner: data.targetOrg.name,
|
||||
secretPath: data.secretPath
|
||||
});
|
||||
} else {
|
||||
await mutateAsync({
|
||||
scope: data.scope,
|
||||
integrationAuthId,
|
||||
isActive: true,
|
||||
app: data.targetProject.name, // project name
|
||||
owner: data.targetOrg.name, // organization name
|
||||
appId: data.targetProject.id, // project id (used for syncing)
|
||||
sourceEnvironment: data.sourceEnvironment.slug,
|
||||
secretPath: data.secretPath
|
||||
});
|
||||
}
|
||||
|
||||
createNotification({
|
||||
type: "success",
|
||||
text: "Successfully created integration"
|
||||
});
|
||||
navigate({
|
||||
to: "/secret-manager/$projectId/integrations",
|
||||
params: {
|
||||
projectId: currentWorkspace.id
|
||||
}
|
||||
});
|
||||
} catch (err) {
|
||||
createNotification({
|
||||
type: "error",
|
||||
text: "Failed to create integration"
|
||||
});
|
||||
console.error(err);
|
||||
}
|
||||
};
|
||||
|
||||
if (isCircleCIOrganizationsLoading)
|
||||
return (
|
||||
<div className="flex h-full w-full items-center justify-center p-24">
|
||||
<Spinner />
|
||||
</div>
|
||||
);
|
||||
|
||||
return (
|
||||
<form
|
||||
onSubmit={handleSubmit(onSubmit)}
|
||||
className="flex h-full w-full items-center justify-center"
|
||||
>
|
||||
<Card className="max-w-lg rounded-md p-8 pt-4">
|
||||
<CardTitle
|
||||
className="w-full px-0 text-left text-xl"
|
||||
subTitle="Choose which environment or folder in Infisical you want to sync to CircleCI."
|
||||
>
|
||||
<div className="flex w-full flex-row items-center justify-between">
|
||||
<div className="flex flex-row items-center gap-1.5">
|
||||
<img
|
||||
src="/images/integrations/CircleCI.png"
|
||||
height={30}
|
||||
width={30}
|
||||
alt="CircleCI logo"
|
||||
/>
|
||||
|
||||
<span className="">CircleCI Context Integration </span>
|
||||
</div>
|
||||
|
||||
<a
|
||||
href="https://infisical.com/docs/integrations/cicd/circleci"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
<div className="mb-1 ml-2 flex cursor-pointer flex-row items-center gap-0.5 rounded-md bg-yellow/20 px-1.5 pb-[0.03rem] pt-[0.04rem] text-sm text-yellow opacity-80 hover:opacity-100">
|
||||
<FontAwesomeIcon icon={faBookOpen} className="mr-1.5" />
|
||||
Docs
|
||||
<FontAwesomeIcon
|
||||
icon={faArrowUpRightFromSquare}
|
||||
className="mb-[0.07rem] ml-1.5 text-xxs"
|
||||
/>
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
</CardTitle>
|
||||
<Controller
|
||||
control={control}
|
||||
name="sourceEnvironment"
|
||||
render={({ field: { value, onChange }, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
errorText={error?.message}
|
||||
isError={Boolean(error)}
|
||||
label="Project Environment"
|
||||
>
|
||||
<FilterableSelect
|
||||
getOptionValue={(option) => option.slug}
|
||||
value={value}
|
||||
getOptionLabel={(option) => option.name}
|
||||
onChange={onChange}
|
||||
options={currentWorkspace?.environments}
|
||||
placeholder="Select a project environment"
|
||||
isDisabled={!currentWorkspace?.environments.length}
|
||||
/>
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
<Controller
|
||||
control={control}
|
||||
name="secretPath"
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl label="Secrets Path" errorText={error?.message} isError={Boolean(error)}>
|
||||
<SecretPathInput {...field} />
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
<Controller
|
||||
control={control}
|
||||
name="targetOrg"
|
||||
render={({ field: { value, onChange }, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
errorText={error?.message}
|
||||
isError={Boolean(error)}
|
||||
label="CircleCI Organization"
|
||||
>
|
||||
<FilterableSelect
|
||||
getOptionValue={(option) => option.slug}
|
||||
value={value}
|
||||
getOptionLabel={(option) => option.name}
|
||||
onChange={(e) => {
|
||||
setValue("targetProject", {
|
||||
name: "",
|
||||
id: ""
|
||||
});
|
||||
setValue("targetContext", {
|
||||
name: "",
|
||||
id: ""
|
||||
});
|
||||
|
||||
onChange(e);
|
||||
}}
|
||||
options={circleCIOrganizations}
|
||||
placeholder={
|
||||
circleCIOrganizations?.length
|
||||
? "Select an organization..."
|
||||
: "No organizations found..."
|
||||
}
|
||||
isDisabled={!circleCIOrganizations?.length}
|
||||
/>
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
<Controller
|
||||
control={control}
|
||||
name="scope"
|
||||
render={({ field: { onChange, ...field }, fieldState: { error } }) => (
|
||||
<FormControl label="Scope" errorText={error?.message} isError={Boolean(error)}>
|
||||
<Select
|
||||
defaultValue={field.value}
|
||||
onValueChange={(e) => {
|
||||
onChange(e);
|
||||
}}
|
||||
className="w-full border border-mineshaft-500"
|
||||
>
|
||||
<SelectItem value={CircleCiScope.Project}>Project</SelectItem>
|
||||
<SelectItem value={CircleCiScope.Context}>Context</SelectItem>
|
||||
</Select>
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
{selectedScope === CircleCiScope.Context && selectedOrganizationEntry && (
|
||||
<Controller
|
||||
control={control}
|
||||
name="targetContext"
|
||||
render={({ field: { value, onChange }, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
errorText={error?.message}
|
||||
isError={Boolean(error)}
|
||||
label="CircleCI Context"
|
||||
>
|
||||
<FilterableSelect
|
||||
value={value}
|
||||
getOptionValue={(option) => option.id!}
|
||||
getOptionLabel={(option) => option.name}
|
||||
onChange={onChange}
|
||||
options={selectedOrganizationEntry?.contexts}
|
||||
placeholder={
|
||||
selectedOrganizationEntry.contexts?.length
|
||||
? "Select a context..."
|
||||
: "No contexts found..."
|
||||
}
|
||||
isDisabled={!selectedOrganizationEntry.contexts?.length}
|
||||
/>
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
{selectedScope === CircleCiScope.Project && selectedOrganizationEntry && (
|
||||
<Controller
|
||||
control={control}
|
||||
name="targetProject"
|
||||
render={({ field: { value, onChange }, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
errorText={error?.message}
|
||||
isError={Boolean(error)}
|
||||
label="CircleCI Project"
|
||||
>
|
||||
<FilterableSelect
|
||||
value={value}
|
||||
getOptionValue={(option) => option.id!}
|
||||
getOptionLabel={(option) => option.name}
|
||||
onChange={onChange}
|
||||
options={selectedOrganizationEntry?.projects}
|
||||
placeholder={
|
||||
selectedOrganizationEntry.projects?.length
|
||||
? "Select a project..."
|
||||
: "No projects found..."
|
||||
}
|
||||
isDisabled={!selectedOrganizationEntry.projects?.length}
|
||||
/>
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
<Button
|
||||
type="submit"
|
||||
colorSchema="primary"
|
||||
className="mt-4"
|
||||
isLoading={isCreatingIntegration}
|
||||
isDisabled={isCreatingIntegration}
|
||||
>
|
||||
Create Integration
|
||||
</Button>
|
||||
</Card>
|
||||
</form>
|
||||
);
|
||||
};
|
@ -0,0 +1,16 @@
|
||||
import { createFileRoute } from "@tanstack/react-router";
|
||||
import { zodValidator } from "@tanstack/zod-adapter";
|
||||
import { z } from "zod";
|
||||
|
||||
import { CircleCIConfigurePage } from "./CircleCIConfigurePage";
|
||||
|
||||
const CircleCIConfigurePageQueryParamsSchema = z.object({
|
||||
integrationAuthId: z.string()
|
||||
});
|
||||
|
||||
export const Route = createFileRoute(
|
||||
"/_authenticate/_inject-org-details/secret-manager/$projectId/_secret-manager-layout/integrations/circleci/create"
|
||||
)({
|
||||
component: CircleCIConfigurePage,
|
||||
validateSearch: zodValidator(CircleCIConfigurePageQueryParamsSchema)
|
||||
});
|
@ -0,0 +1,72 @@
|
||||
import { useState } from "react";
|
||||
import { useNavigate } from "@tanstack/react-router";
|
||||
|
||||
import { Button, Card, CardTitle, FormControl, Input } from "@app/components/v2";
|
||||
import { useWorkspace } from "@app/context";
|
||||
import { useSaveIntegrationAccessToken } from "@app/hooks/api";
|
||||
|
||||
export const Cloud66AuthorizePage = () => {
|
||||
const navigate = useNavigate();
|
||||
const { mutateAsync } = useSaveIntegrationAccessToken();
|
||||
|
||||
const { currentWorkspace } = useWorkspace();
|
||||
|
||||
const [apiKey, setApiKey] = useState("");
|
||||
const [apiKeyErrorText, setApiKeyErrorText] = useState("");
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
|
||||
const handleButtonClick = async () => {
|
||||
try {
|
||||
setApiKeyErrorText("");
|
||||
if (apiKey.length === 0) {
|
||||
setApiKeyErrorText("Access token cannot be blank");
|
||||
return;
|
||||
}
|
||||
|
||||
setIsLoading(true);
|
||||
|
||||
const integrationAuth = await mutateAsync({
|
||||
workspaceId: currentWorkspace.id,
|
||||
integration: "cloud-66",
|
||||
accessToken: apiKey
|
||||
});
|
||||
|
||||
setIsLoading(false);
|
||||
|
||||
navigate({
|
||||
to: "/secret-manager/$projectId/integrations/cloud-66/create",
|
||||
params: {
|
||||
projectId: currentWorkspace.id
|
||||
},
|
||||
search: {
|
||||
integrationAuthId: integrationAuth.id
|
||||
}
|
||||
});
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex h-full w-full items-center justify-center">
|
||||
<Card className="max-w-md rounded-md p-8">
|
||||
<CardTitle className="text-center">Cloud 66 Integration</CardTitle>
|
||||
<FormControl
|
||||
label="Cloud 66 Personal Access Token"
|
||||
errorText={apiKeyErrorText}
|
||||
isError={apiKeyErrorText !== ""}
|
||||
>
|
||||
<Input placeholder="" value={apiKey} onChange={(e) => setApiKey(e.target.value)} />
|
||||
</FormControl>
|
||||
<Button
|
||||
onClick={handleButtonClick}
|
||||
color="mineshaft"
|
||||
className="mt-4"
|
||||
isLoading={isLoading}
|
||||
>
|
||||
Connect to Cloud 66
|
||||
</Button>
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
};
|
@ -0,0 +1,9 @@
|
||||
import { createFileRoute } from "@tanstack/react-router";
|
||||
|
||||
import { Cloud66AuthorizePage } from "./Cloud66AuthorizePage";
|
||||
|
||||
export const Route = createFileRoute(
|
||||
"/_authenticate/_inject-org-details/secret-manager/$projectId/_secret-manager-layout/integrations/cloud-66/authorize"
|
||||
)({
|
||||
component: Cloud66AuthorizePage
|
||||
});
|
@ -0,0 +1,151 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import { useNavigate, useSearch } from "@tanstack/react-router";
|
||||
|
||||
import {
|
||||
Button,
|
||||
Card,
|
||||
CardTitle,
|
||||
FormControl,
|
||||
Input,
|
||||
Select,
|
||||
SelectItem
|
||||
} from "@app/components/v2";
|
||||
import { ROUTE_PATHS } from "@app/const/routes";
|
||||
import { useWorkspace } from "@app/context";
|
||||
import { useCreateIntegration } from "@app/hooks/api";
|
||||
import {
|
||||
useGetIntegrationAuthApps,
|
||||
useGetIntegrationAuthById
|
||||
} from "@app/hooks/api/integrationAuth";
|
||||
|
||||
export const Cloud66ConfigurePage = () => {
|
||||
const navigate = useNavigate();
|
||||
const { mutateAsync } = useCreateIntegration();
|
||||
|
||||
const integrationAuthId = useSearch({
|
||||
from: ROUTE_PATHS.SecretManager.Integratons.Cloud66ConfigurePage.id,
|
||||
select: (el) => el.integrationAuthId
|
||||
});
|
||||
|
||||
const { currentWorkspace } = useWorkspace();
|
||||
const { data: integrationAuth } = useGetIntegrationAuthById((integrationAuthId as string) ?? "");
|
||||
const { data: integrationAuthApps } = useGetIntegrationAuthApps({
|
||||
integrationAuthId: (integrationAuthId as string) ?? ""
|
||||
});
|
||||
|
||||
const [selectedSourceEnvironment, setSelectedSourceEnvironment] = useState("");
|
||||
const [targetApp, setTargetApp] = useState("");
|
||||
const [secretPath, setSecretPath] = useState("/");
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (currentWorkspace) {
|
||||
setSelectedSourceEnvironment(currentWorkspace.environments[0].slug);
|
||||
}
|
||||
}, [currentWorkspace]);
|
||||
|
||||
useEffect(() => {
|
||||
if (integrationAuthApps) {
|
||||
if (integrationAuthApps.length > 0) {
|
||||
setTargetApp(integrationAuthApps[0].name);
|
||||
} else {
|
||||
setTargetApp("none");
|
||||
}
|
||||
}
|
||||
}, [integrationAuthApps]);
|
||||
|
||||
const handleButtonClick = async () => {
|
||||
try {
|
||||
if (!integrationAuth?.id) return;
|
||||
|
||||
setIsLoading(true);
|
||||
|
||||
await mutateAsync({
|
||||
integrationAuthId: integrationAuth?.id,
|
||||
isActive: true,
|
||||
app: targetApp,
|
||||
appId: integrationAuthApps?.find(
|
||||
(integrationAuthApp) => integrationAuthApp.name === targetApp
|
||||
)?.appId,
|
||||
sourceEnvironment: selectedSourceEnvironment,
|
||||
secretPath
|
||||
});
|
||||
|
||||
setIsLoading(false);
|
||||
|
||||
navigate({
|
||||
to: "/secret-manager/$projectId/integrations",
|
||||
params: {
|
||||
projectId: currentWorkspace.id
|
||||
}
|
||||
});
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
}
|
||||
};
|
||||
|
||||
return integrationAuth && selectedSourceEnvironment && integrationAuthApps && targetApp ? (
|
||||
<div className="flex h-full w-full items-center justify-center">
|
||||
<Card className="max-w-md rounded-md p-8">
|
||||
<CardTitle className="text-center">Cloud 66 Integration</CardTitle>
|
||||
<FormControl label="Project Environment" className="mt-4">
|
||||
<Select
|
||||
value={selectedSourceEnvironment}
|
||||
onValueChange={(val) => setSelectedSourceEnvironment(val)}
|
||||
className="w-full border border-mineshaft-500"
|
||||
>
|
||||
{currentWorkspace?.environments.map((sourceEnvironment) => (
|
||||
<SelectItem
|
||||
value={sourceEnvironment.slug}
|
||||
key={`source-environment-${sourceEnvironment.slug}`}
|
||||
>
|
||||
{sourceEnvironment.name}
|
||||
</SelectItem>
|
||||
))}
|
||||
</Select>
|
||||
</FormControl>
|
||||
<FormControl label="Secrets Path">
|
||||
<Input
|
||||
value={secretPath}
|
||||
onChange={(evt) => setSecretPath(evt.target.value)}
|
||||
placeholder="Provide a path, default is /"
|
||||
/>
|
||||
</FormControl>
|
||||
<FormControl label="Cloud 66 Application" className="mt-4">
|
||||
<Select
|
||||
value={targetApp}
|
||||
onValueChange={(val) => setTargetApp(val)}
|
||||
className="w-full border border-mineshaft-500"
|
||||
isDisabled={integrationAuthApps.length === 0}
|
||||
>
|
||||
{integrationAuthApps.length > 0 ? (
|
||||
integrationAuthApps.map((integrationAuthApp) => (
|
||||
<SelectItem
|
||||
value={integrationAuthApp.name}
|
||||
key={`target-app-${integrationAuthApp.name}`}
|
||||
>
|
||||
{integrationAuthApp.name}
|
||||
</SelectItem>
|
||||
))
|
||||
) : (
|
||||
<SelectItem value="none" key="target-app-none">
|
||||
No applications found
|
||||
</SelectItem>
|
||||
)}
|
||||
</Select>
|
||||
</FormControl>
|
||||
<Button
|
||||
onClick={handleButtonClick}
|
||||
color="mineshaft"
|
||||
className="mt-4"
|
||||
isLoading={isLoading}
|
||||
isDisabled={integrationAuthApps.length === 0}
|
||||
>
|
||||
Create Integration
|
||||
</Button>
|
||||
</Card>
|
||||
</div>
|
||||
) : (
|
||||
<div />
|
||||
);
|
||||
};
|
@ -0,0 +1,16 @@
|
||||
import { createFileRoute } from "@tanstack/react-router";
|
||||
import { zodValidator } from "@tanstack/zod-adapter";
|
||||
import { z } from "zod";
|
||||
|
||||
import { Cloud66ConfigurePage } from "./Cloud66ConfigurePage";
|
||||
|
||||
const Cloud66ConfigurePageQueryParamsSchema = z.object({
|
||||
integrationAuthId: z.string()
|
||||
});
|
||||
|
||||
export const Route = createFileRoute(
|
||||
"/_authenticate/_inject-org-details/secret-manager/$projectId/_secret-manager-layout/integrations/cloud-66/create"
|
||||
)({
|
||||
component: Cloud66ConfigurePage,
|
||||
validateSearch: zodValidator(Cloud66ConfigurePageQueryParamsSchema)
|
||||
});
|
@ -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>
|
||||
);
|
||||
};
|
@ -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
|
||||
});
|
@ -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 />
|
||||
);
|
||||
};
|
@ -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)
|
||||
});
|
@ -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>
|
||||
);
|
||||
};
|
@ -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
|
||||
});
|
@ -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 />
|
||||
);
|
||||
};
|
@ -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)
|
||||
});
|
@ -0,0 +1,71 @@
|
||||
import { useState } from "react";
|
||||
import { useNavigate } from "@tanstack/react-router";
|
||||
|
||||
import { Button, Card, CardTitle, FormControl, Input } from "@app/components/v2";
|
||||
import { useWorkspace } from "@app/context";
|
||||
import { useSaveIntegrationAccessToken } from "@app/hooks/api";
|
||||
|
||||
export const CodefreshAuthorizePage = () => {
|
||||
const navigate = useNavigate();
|
||||
const { mutateAsync } = useSaveIntegrationAccessToken();
|
||||
|
||||
const [apiKey, setApiKey] = useState("");
|
||||
const [apiKeyErrorText, setApiKeyErrorText] = useState("");
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const { currentWorkspace } = useWorkspace();
|
||||
|
||||
const handleButtonClick = async () => {
|
||||
try {
|
||||
setApiKeyErrorText("");
|
||||
if (apiKey.length === 0) {
|
||||
setApiKeyErrorText("API Key cannot be blank");
|
||||
return;
|
||||
}
|
||||
|
||||
setIsLoading(true);
|
||||
|
||||
const integrationAuth = await mutateAsync({
|
||||
workspaceId: currentWorkspace.id,
|
||||
integration: "codefresh",
|
||||
accessToken: apiKey
|
||||
});
|
||||
|
||||
setIsLoading(false);
|
||||
|
||||
navigate({
|
||||
to: "/secret-manager/$projectId/integrations/codefresh/create",
|
||||
params: {
|
||||
projectId: currentWorkspace.id
|
||||
},
|
||||
search: {
|
||||
integrationAuthId: integrationAuth.id
|
||||
}
|
||||
});
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex h-full w-full items-center justify-center">
|
||||
<Card className="max-w-md rounded-md p-8">
|
||||
<CardTitle className="text-center">Codefresh Integration</CardTitle>
|
||||
<FormControl
|
||||
label="Codefresh API Key"
|
||||
errorText={apiKeyErrorText}
|
||||
isError={apiKeyErrorText !== ""}
|
||||
>
|
||||
<Input placeholder="" value={apiKey} onChange={(e) => setApiKey(e.target.value)} />
|
||||
</FormControl>
|
||||
<Button
|
||||
onClick={handleButtonClick}
|
||||
color="mineshaft"
|
||||
className="mt-4"
|
||||
isLoading={isLoading}
|
||||
>
|
||||
Connect to Codefresh
|
||||
</Button>
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
};
|
@ -0,0 +1,9 @@
|
||||
import { createFileRoute } from "@tanstack/react-router";
|
||||
|
||||
import { CodefreshAuthorizePage } from "./CodefreshAuthorizePage";
|
||||
|
||||
export const Route = createFileRoute(
|
||||
"/_authenticate/_inject-org-details/secret-manager/$projectId/_secret-manager-layout/integrations/codefresh/authorize"
|
||||
)({
|
||||
component: CodefreshAuthorizePage
|
||||
});
|
@ -0,0 +1,147 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import { useNavigate, useSearch } from "@tanstack/react-router";
|
||||
|
||||
import {
|
||||
Button,
|
||||
Card,
|
||||
CardTitle,
|
||||
FormControl,
|
||||
Input,
|
||||
Select,
|
||||
SelectItem
|
||||
} from "@app/components/v2";
|
||||
import { ROUTE_PATHS } from "@app/const/routes";
|
||||
import { useWorkspace } from "@app/context";
|
||||
import { useCreateIntegration } from "@app/hooks/api";
|
||||
import {
|
||||
useGetIntegrationAuthApps,
|
||||
useGetIntegrationAuthById
|
||||
} from "@app/hooks/api/integrationAuth";
|
||||
|
||||
export const CodefreshConfigurePage = () => {
|
||||
const navigate = useNavigate();
|
||||
const { mutateAsync } = useCreateIntegration();
|
||||
|
||||
const { currentWorkspace } = useWorkspace();
|
||||
const integrationAuthId = useSearch({
|
||||
from: ROUTE_PATHS.SecretManager.Integratons.CodefreshConfigurePage.id,
|
||||
select: (el) => el.integrationAuthId
|
||||
});
|
||||
|
||||
const { data: integrationAuth } = useGetIntegrationAuthById((integrationAuthId as string) ?? "");
|
||||
const { data: integrationAuthApps } = useGetIntegrationAuthApps({
|
||||
integrationAuthId: (integrationAuthId as string) ?? ""
|
||||
});
|
||||
|
||||
const [selectedSourceEnvironment, setSelectedSourceEnvironment] = useState(
|
||||
currentWorkspace.environments[0].slug
|
||||
);
|
||||
const [targetApp, setTargetApp] = useState("");
|
||||
const [secretPath, setSecretPath] = useState("/");
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (integrationAuthApps) {
|
||||
if (integrationAuthApps.length > 0) {
|
||||
setTargetApp(integrationAuthApps[0].name);
|
||||
} else {
|
||||
setTargetApp("none");
|
||||
}
|
||||
}
|
||||
}, [integrationAuthApps]);
|
||||
|
||||
const handleButtonClick = async () => {
|
||||
try {
|
||||
if (!integrationAuth?.id) return;
|
||||
|
||||
setIsLoading(true);
|
||||
|
||||
await mutateAsync({
|
||||
integrationAuthId: integrationAuth?.id,
|
||||
isActive: true,
|
||||
app: targetApp,
|
||||
appId: integrationAuthApps?.find(
|
||||
(integrationAuthApp) => integrationAuthApp.name === targetApp
|
||||
)?.appId,
|
||||
sourceEnvironment: selectedSourceEnvironment,
|
||||
secretPath
|
||||
});
|
||||
|
||||
setIsLoading(false);
|
||||
|
||||
navigate({
|
||||
to: "/secret-manager/$projectId/integrations",
|
||||
params: {
|
||||
projectId: currentWorkspace.id
|
||||
}
|
||||
});
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
}
|
||||
};
|
||||
|
||||
return integrationAuth && selectedSourceEnvironment && integrationAuthApps && targetApp ? (
|
||||
<div className="flex h-full w-full items-center justify-center">
|
||||
<Card className="max-w-md rounded-md p-8">
|
||||
<CardTitle className="text-center">Codefresh Integration</CardTitle>
|
||||
<FormControl label="Project Environment" className="mt-4">
|
||||
<Select
|
||||
value={selectedSourceEnvironment}
|
||||
onValueChange={(val) => setSelectedSourceEnvironment(val)}
|
||||
className="w-full border border-mineshaft-500"
|
||||
>
|
||||
{currentWorkspace?.environments.map((sourceEnvironment) => (
|
||||
<SelectItem
|
||||
value={sourceEnvironment.slug}
|
||||
key={`source-environment-${sourceEnvironment.slug}`}
|
||||
>
|
||||
{sourceEnvironment.name}
|
||||
</SelectItem>
|
||||
))}
|
||||
</Select>
|
||||
</FormControl>
|
||||
<FormControl label="Secrets Path">
|
||||
<Input
|
||||
value={secretPath}
|
||||
onChange={(evt) => setSecretPath(evt.target.value)}
|
||||
placeholder="Provide a path, default is /"
|
||||
/>
|
||||
</FormControl>
|
||||
<FormControl label="Codefresh Service" className="mt-4">
|
||||
<Select
|
||||
value={targetApp}
|
||||
onValueChange={(val) => setTargetApp(val)}
|
||||
className="w-full border border-mineshaft-500"
|
||||
isDisabled={integrationAuthApps.length === 0}
|
||||
>
|
||||
{integrationAuthApps.length > 0 ? (
|
||||
integrationAuthApps.map((integrationAuthApp) => (
|
||||
<SelectItem
|
||||
value={integrationAuthApp.name}
|
||||
key={`target-app-${integrationAuthApp.name}`}
|
||||
>
|
||||
{integrationAuthApp.name}
|
||||
</SelectItem>
|
||||
))
|
||||
) : (
|
||||
<SelectItem value="none" key="target-app-none">
|
||||
No services found
|
||||
</SelectItem>
|
||||
)}
|
||||
</Select>
|
||||
</FormControl>
|
||||
<Button
|
||||
onClick={handleButtonClick}
|
||||
color="mineshaft"
|
||||
className="mt-4"
|
||||
isLoading={isLoading}
|
||||
isDisabled={integrationAuthApps.length === 0}
|
||||
>
|
||||
Create Integration
|
||||
</Button>
|
||||
</Card>
|
||||
</div>
|
||||
) : (
|
||||
<div />
|
||||
);
|
||||
};
|
@ -0,0 +1,16 @@
|
||||
import { createFileRoute } from "@tanstack/react-router";
|
||||
import { zodValidator } from "@tanstack/zod-adapter";
|
||||
import { z } from "zod";
|
||||
|
||||
import { CodefreshConfigurePage } from "./CodefreshConfigurePage";
|
||||
|
||||
const CodefreshConfigurePageQueryParamsSchema = z.object({
|
||||
integrationAuthId: z.string()
|
||||
});
|
||||
|
||||
export const Route = createFileRoute(
|
||||
"/_authenticate/_inject-org-details/secret-manager/$projectId/_secret-manager-layout/integrations/codefresh/create"
|
||||
)({
|
||||
component: CodefreshConfigurePage,
|
||||
validateSearch: zodValidator(CodefreshConfigurePageQueryParamsSchema)
|
||||
});
|
@ -0,0 +1,130 @@
|
||||
import { useState } from "react";
|
||||
import { Helmet } from "react-helmet";
|
||||
import { faArrowUpRightFromSquare, faBookOpen } from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { useNavigate } from "@tanstack/react-router";
|
||||
|
||||
import { Button, Card, CardTitle, FormControl, Input } from "@app/components/v2";
|
||||
import { useWorkspace } from "@app/context";
|
||||
import { useSaveIntegrationAccessToken } from "@app/hooks/api";
|
||||
|
||||
export const DatabricksAuthorizePage = () => {
|
||||
const navigate = useNavigate();
|
||||
const { mutateAsync, isPending } = useSaveIntegrationAccessToken();
|
||||
|
||||
const [apiKey, setApiKey] = useState("");
|
||||
const [instanceURL, setInstanceURL] = useState("");
|
||||
const [apiKeyErrorText, setApiKeyErrorText] = useState("");
|
||||
const [instanceURLErrorText, setInstanceURLErrorText] = useState("");
|
||||
|
||||
const { currentWorkspace } = useWorkspace();
|
||||
|
||||
const handleButtonClick = async () => {
|
||||
try {
|
||||
setApiKeyErrorText("");
|
||||
setInstanceURLErrorText("");
|
||||
if (apiKey.length === 0) {
|
||||
setApiKeyErrorText("API Key cannot be blank");
|
||||
return;
|
||||
}
|
||||
if (instanceURL.length === 0) {
|
||||
setInstanceURLErrorText("Instance URL cannot be blank");
|
||||
return;
|
||||
}
|
||||
|
||||
const integrationAuth = await mutateAsync({
|
||||
workspaceId: currentWorkspace.id,
|
||||
integration: "databricks",
|
||||
url: instanceURL.replace(/\/$/, ""),
|
||||
accessToken: apiKey
|
||||
});
|
||||
|
||||
navigate({
|
||||
to: "/secret-manager/$projectId/integrations/databricks/create",
|
||||
params: {
|
||||
projectId: currentWorkspace.id
|
||||
},
|
||||
search: {
|
||||
integrationAuthId: integrationAuth.id
|
||||
}
|
||||
});
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex h-full w-full items-center justify-center">
|
||||
<Helmet>
|
||||
<title>Authorize Databricks Integration</title>
|
||||
</Helmet>
|
||||
<Card className="mb-12 max-w-lg rounded-md border border-mineshaft-600">
|
||||
<CardTitle
|
||||
className="px-6 text-left text-xl"
|
||||
subTitle="After adding your Access Token, you will be prompted to set up an integration for a particular Infisical project and environment."
|
||||
>
|
||||
<div className="flex flex-row items-center">
|
||||
<div className="flex items-center pb-0.5">
|
||||
<img
|
||||
src="/images/integrations/Databricks.png"
|
||||
height={30}
|
||||
width={30}
|
||||
alt="Databricks logo"
|
||||
/>
|
||||
</div>
|
||||
<span className="ml-1.5">Databricks Integration </span>
|
||||
<a
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
href="https://infisical.com/docs/integrations/cloud/databricks"
|
||||
>
|
||||
<div className="mb-1 ml-2 inline-block cursor-default rounded-md bg-yellow/20 px-1.5 pb-[0.03rem] pt-[0.04rem] text-sm text-yellow opacity-80 hover:opacity-100">
|
||||
<FontAwesomeIcon icon={faBookOpen} className="mr-1.5" />
|
||||
Docs
|
||||
<FontAwesomeIcon
|
||||
icon={faArrowUpRightFromSquare}
|
||||
className="mb-[0.07rem] ml-1.5 text-xxs"
|
||||
/>
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
</CardTitle>
|
||||
<FormControl
|
||||
label="Databricks Instance URL"
|
||||
errorText={instanceURLErrorText}
|
||||
isError={instanceURLErrorText !== ""}
|
||||
className="px-6"
|
||||
>
|
||||
<Input
|
||||
value={instanceURL}
|
||||
onChange={(e) => setInstanceURL(e.target.value)}
|
||||
placeholder="https://xxxx.cloud.databricks.com"
|
||||
/>
|
||||
</FormControl>
|
||||
<FormControl
|
||||
label="Access Token"
|
||||
errorText={apiKeyErrorText}
|
||||
isError={apiKeyErrorText !== ""}
|
||||
className="px-6"
|
||||
>
|
||||
<Input
|
||||
placeholder=""
|
||||
value={apiKey}
|
||||
onChange={(e) => setApiKey(e.target.value)}
|
||||
type="password"
|
||||
/>
|
||||
</FormControl>
|
||||
<Button
|
||||
onClick={handleButtonClick}
|
||||
colorSchema="primary"
|
||||
variant="outline_bg"
|
||||
className="mb-6 ml-auto mr-6 mt-2 w-min"
|
||||
isLoading={isPending}
|
||||
isDisabled={isPending}
|
||||
>
|
||||
Connect to Databricks
|
||||
</Button>
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
};
|
@ -0,0 +1,9 @@
|
||||
import { createFileRoute } from "@tanstack/react-router";
|
||||
|
||||
import { DatabricksAuthorizePage } from "./DatabricksAuthorizePage";
|
||||
|
||||
export const Route = createFileRoute(
|
||||
"/_authenticate/_inject-org-details/secret-manager/$projectId/_secret-manager-layout/integrations/databricks/authorize"
|
||||
)({
|
||||
component: DatabricksAuthorizePage
|
||||
});
|
@ -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>
|
||||
);
|
||||
};
|
@ -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)
|
||||
});
|
@ -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>
|
||||
);
|
||||
};
|
@ -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
|
||||
});
|
@ -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 />
|
||||
);
|
||||
};
|
@ -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)
|
||||
});
|
@ -0,0 +1,120 @@
|
||||
import { useState } from "react";
|
||||
import { Helmet } from "react-helmet";
|
||||
import { Controller, useForm } from "react-hook-form";
|
||||
import { faArrowUpRightFromSquare, faBookOpen } from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { useNavigate } from "@tanstack/react-router";
|
||||
import { z } from "zod";
|
||||
|
||||
import { Button, Card, CardTitle, FormControl, Input } from "@app/components/v2";
|
||||
import { useWorkspace } from "@app/context";
|
||||
import { useSaveIntegrationAccessToken } from "@app/hooks/api";
|
||||
|
||||
const schema = z.object({
|
||||
accessToken: z.string().trim()
|
||||
});
|
||||
|
||||
type FormData = z.infer<typeof schema>;
|
||||
|
||||
export const FlyioAuthorizePage = () => {
|
||||
const navigate = useNavigate();
|
||||
const { currentWorkspace } = useWorkspace();
|
||||
|
||||
const { control, handleSubmit } = useForm<FormData>({
|
||||
resolver: zodResolver(schema),
|
||||
defaultValues: {
|
||||
accessToken: ""
|
||||
}
|
||||
});
|
||||
|
||||
const { mutateAsync } = useSaveIntegrationAccessToken();
|
||||
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
|
||||
const onFormSubmit = async ({ accessToken }: FormData) => {
|
||||
try {
|
||||
setIsLoading(true);
|
||||
|
||||
const integrationAuth = await mutateAsync({
|
||||
workspaceId: currentWorkspace.id,
|
||||
integration: "flyio",
|
||||
accessToken
|
||||
});
|
||||
|
||||
setIsLoading(false);
|
||||
navigate({
|
||||
to: "/secret-manager/$projectId/integrations/flyio/create",
|
||||
params: {
|
||||
projectId: currentWorkspace.id
|
||||
},
|
||||
search: {
|
||||
integrationAuthId: integrationAuth.id
|
||||
}
|
||||
});
|
||||
} catch (err) {
|
||||
setIsLoading(false);
|
||||
console.error(err);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex h-full w-full items-center justify-center">
|
||||
<Helmet>
|
||||
<title>Authorize Fly.io Integration</title>
|
||||
</Helmet>
|
||||
<Card className="mb-12 max-w-lg rounded-md border border-mineshaft-600">
|
||||
<CardTitle
|
||||
className="px-6 text-left text-xl"
|
||||
subTitle="After adding your access token, you will be prompted to set up an integration for a particular Infisical project and environment."
|
||||
>
|
||||
<div className="flex flex-row items-center">
|
||||
<div className="inline flex items-center pb-0.5">
|
||||
<img src="/images/integrations/Flyio.svg" height={30} width={30} alt="Fly.io logo" />
|
||||
</div>
|
||||
<span className="ml-2.5">Fly.io Integration </span>
|
||||
<a
|
||||
href="https://infisical.com/docs/integrations/cloud/flyio"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
<div className="mb-1 ml-2 inline-block cursor-default rounded-md bg-yellow/20 px-1.5 pb-[0.03rem] pt-[0.04rem] text-sm text-yellow opacity-80 hover:opacity-100">
|
||||
<FontAwesomeIcon icon={faBookOpen} className="mr-1.5" />
|
||||
Docs
|
||||
<FontAwesomeIcon
|
||||
icon={faArrowUpRightFromSquare}
|
||||
className="mb-[0.07rem] ml-1.5 text-xxs"
|
||||
/>
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
</CardTitle>
|
||||
<form onSubmit={handleSubmit(onFormSubmit)} className="px-6 pb-8 text-right">
|
||||
<Controller
|
||||
control={control}
|
||||
name="accessToken"
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label="Fly.io Access Token"
|
||||
errorText={error?.message}
|
||||
isError={Boolean(error)}
|
||||
>
|
||||
<Input {...field} placeholder="" type="password" autoComplete="new-password" />
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
<Button
|
||||
colorSchema="primary"
|
||||
variant="outline_bg"
|
||||
className="mt-2 w-min"
|
||||
size="sm"
|
||||
type="submit"
|
||||
isLoading={isLoading}
|
||||
>
|
||||
Connect to Fly.io
|
||||
</Button>
|
||||
</form>
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
};
|
@ -0,0 +1,9 @@
|
||||
import { createFileRoute } from "@tanstack/react-router";
|
||||
|
||||
import { FlyioAuthorizePage } from "./FlyioAuthorizePage";
|
||||
|
||||
export const Route = createFileRoute(
|
||||
"/_authenticate/_inject-org-details/secret-manager/$projectId/_secret-manager-layout/integrations/flyio/authorize"
|
||||
)({
|
||||
component: FlyioAuthorizePage
|
||||
});
|
@ -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>
|
||||
);
|
||||
};
|
@ -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)
|
||||
});
|
@ -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>
|
||||
);
|
||||
};
|
@ -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
|
||||
});
|
@ -0,0 +1,475 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import { Helmet } from "react-helmet";
|
||||
import { Controller, useForm } from "react-hook-form";
|
||||
import { faArrowUpRightFromSquare, faBookOpen, faBugs } from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { yupResolver } from "@hookform/resolvers/yup";
|
||||
import { useNavigate, useSearch } from "@tanstack/react-router";
|
||||
import { motion } from "framer-motion";
|
||||
import { z } from "zod";
|
||||
|
||||
import {
|
||||
Button,
|
||||
Card,
|
||||
CardTitle,
|
||||
FormControl,
|
||||
Input,
|
||||
Modal,
|
||||
ModalContent,
|
||||
Select,
|
||||
SelectItem,
|
||||
Switch,
|
||||
Tab,
|
||||
TabList,
|
||||
TabPanel,
|
||||
Tabs
|
||||
} from "@app/components/v2";
|
||||
import { SecretPathInput } from "@app/components/v2/SecretPathInput";
|
||||
import { ROUTE_PATHS } from "@app/const/routes";
|
||||
import { useWorkspace } from "@app/context";
|
||||
import { usePopUp } from "@app/hooks";
|
||||
import { useCreateIntegration } from "@app/hooks/api";
|
||||
import {
|
||||
useGetIntegrationAuthApps,
|
||||
useGetIntegrationAuthById
|
||||
} from "@app/hooks/api/integrationAuth";
|
||||
|
||||
enum TabSections {
|
||||
Connection = "connection",
|
||||
Options = "options"
|
||||
}
|
||||
|
||||
const schema = z.object({
|
||||
selectedSourceEnvironment: z.string(),
|
||||
secretPath: z.string(),
|
||||
targetAppId: z.string(),
|
||||
secretPrefix: z.string().trim().optional(),
|
||||
secretSuffix: z.string().trim().optional(),
|
||||
shouldLabel: z.boolean().optional(),
|
||||
labelName: z.string().trim().optional(),
|
||||
labelValue: z.string().trim().optional()
|
||||
});
|
||||
|
||||
type FormData = z.infer<typeof schema>;
|
||||
|
||||
export const GcpSecretManagerConfigurePage = () => {
|
||||
const navigate = useNavigate();
|
||||
const { popUp, handlePopUpOpen, handlePopUpToggle, handlePopUpClose } = usePopUp([
|
||||
"confirmIntegration"
|
||||
] as const);
|
||||
|
||||
const { currentWorkspace } = useWorkspace();
|
||||
const { control, handleSubmit, setValue, watch } = useForm<FormData>({
|
||||
resolver: yupResolver(schema),
|
||||
defaultValues: {
|
||||
secretPath: "/",
|
||||
secretPrefix: "",
|
||||
secretSuffix: "",
|
||||
shouldLabel: false,
|
||||
labelName: "managed-by",
|
||||
labelValue: "infisical",
|
||||
selectedSourceEnvironment: currentWorkspace.environments[0].slug
|
||||
}
|
||||
});
|
||||
|
||||
const shouldLabel = watch("shouldLabel");
|
||||
const selectedSourceEnvironment = watch("selectedSourceEnvironment");
|
||||
|
||||
const { mutateAsync } = useCreateIntegration();
|
||||
|
||||
const integrationAuthId = useSearch({
|
||||
from: ROUTE_PATHS.SecretManager.Integratons.GcpSecretManagerConfigurePage.id,
|
||||
select: (el) => el.integrationAuthId
|
||||
});
|
||||
const { data: integrationAuth } = useGetIntegrationAuthById((integrationAuthId as string) ?? "");
|
||||
const { data: integrationAuthApps, isLoading: isIntegrationAuthAppsLoading } =
|
||||
useGetIntegrationAuthApps({
|
||||
integrationAuthId: (integrationAuthId as string) ?? ""
|
||||
});
|
||||
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (shouldLabel) {
|
||||
setValue("labelName", "managed-by");
|
||||
setValue("labelValue", "infisical");
|
||||
return;
|
||||
}
|
||||
|
||||
setValue("labelName", "");
|
||||
setValue("labelValue", "");
|
||||
}, [shouldLabel]);
|
||||
|
||||
useEffect(() => {
|
||||
if (integrationAuthApps) {
|
||||
if (integrationAuthApps.length > 0) {
|
||||
setValue("targetAppId", integrationAuthApps[0].appId as string);
|
||||
} else {
|
||||
setValue("targetAppId", "none");
|
||||
}
|
||||
}
|
||||
}, [integrationAuthApps]);
|
||||
|
||||
const onFormSubmit = async ({
|
||||
selectedSourceEnvironment: sce,
|
||||
secretPath,
|
||||
targetAppId,
|
||||
secretPrefix,
|
||||
secretSuffix,
|
||||
shouldLabel: sl,
|
||||
labelName,
|
||||
labelValue
|
||||
}: FormData) => {
|
||||
try {
|
||||
setIsLoading(true);
|
||||
|
||||
if (!integrationAuth?.id) return;
|
||||
|
||||
await mutateAsync({
|
||||
integrationAuthId: integrationAuth?.id,
|
||||
isActive: true,
|
||||
app: integrationAuthApps?.find(
|
||||
(integrationAuthApp) => integrationAuthApp.appId === targetAppId
|
||||
)?.name,
|
||||
appId: targetAppId,
|
||||
sourceEnvironment: sce,
|
||||
secretPath,
|
||||
metadata: {
|
||||
...(secretPrefix ? { secretPrefix } : {}),
|
||||
...(secretSuffix ? { secretSuffix } : {}),
|
||||
...(sl
|
||||
? {
|
||||
secretGCPLabel: {
|
||||
labelName,
|
||||
labelValue
|
||||
}
|
||||
}
|
||||
: {})
|
||||
}
|
||||
});
|
||||
|
||||
setIsLoading(false);
|
||||
navigate({
|
||||
to: "/secret-manager/$projectId/integrations",
|
||||
params: {
|
||||
projectId: currentWorkspace.id
|
||||
}
|
||||
});
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
return integrationAuth && selectedSourceEnvironment && integrationAuthApps ? (
|
||||
<form
|
||||
onSubmit={handleSubmit((data: FormData) => {
|
||||
if (!data.secretPrefix && !data.secretSuffix && !data.shouldLabel) {
|
||||
handlePopUpOpen("confirmIntegration", data);
|
||||
return;
|
||||
}
|
||||
|
||||
onFormSubmit(data);
|
||||
})}
|
||||
className="flex h-full w-full flex-col items-center justify-center"
|
||||
>
|
||||
<Card className="max-w-lg rounded-md border border-mineshaft-600">
|
||||
<Helmet>
|
||||
<title>Set Up GCP Secret Manager Integration</title>
|
||||
</Helmet>
|
||||
<CardTitle
|
||||
className="mb-2 px-6 text-left text-xl"
|
||||
subTitle="Select which environment or folder in Infisical you want to sync to GCP Secret Manager."
|
||||
>
|
||||
<div className="flex flex-row items-center">
|
||||
<div className="flex items-center pb-0.5">
|
||||
<img
|
||||
src="/images/integrations/Google Cloud Platform.png"
|
||||
height={30}
|
||||
width={30}
|
||||
alt="GCP logo"
|
||||
/>
|
||||
</div>
|
||||
<span className="ml-1.5">GCP Secret Manager Integration </span>
|
||||
<a
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
href="https://infisical.com/docs/integrations/cloud/gcp-secret-manager"
|
||||
>
|
||||
<div className="mb-1 ml-2 inline-block cursor-default rounded-md bg-yellow/20 px-1.5 pb-[0.03rem] pt-[0.04rem] text-sm text-yellow opacity-80 hover:opacity-100">
|
||||
<FontAwesomeIcon icon={faBookOpen} className="mr-1.5" />
|
||||
Docs
|
||||
<FontAwesomeIcon
|
||||
icon={faArrowUpRightFromSquare}
|
||||
className="mb-[0.07rem] ml-1.5 text-xxs"
|
||||
/>
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
</CardTitle>
|
||||
<Tabs defaultValue={TabSections.Connection} className="px-6">
|
||||
<TabList>
|
||||
<div className="flex w-full flex-row border-b border-mineshaft-600">
|
||||
<Tab value={TabSections.Connection}>Connection</Tab>
|
||||
<Tab value={TabSections.Options}>Options</Tab>
|
||||
</div>
|
||||
</TabList>
|
||||
<TabPanel value={TabSections.Connection}>
|
||||
<motion.div
|
||||
key="panel-1"
|
||||
transition={{ duration: 0.15 }}
|
||||
initial={{ opacity: 0, translateX: 30 }}
|
||||
animate={{ opacity: 1, translateX: 0 }}
|
||||
exit={{ opacity: 0, translateX: 30 }}
|
||||
>
|
||||
<Controller
|
||||
control={control}
|
||||
name="selectedSourceEnvironment"
|
||||
render={({ field: { onChange, ...field }, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label="Project Environment"
|
||||
errorText={error?.message}
|
||||
isError={Boolean(error)}
|
||||
>
|
||||
<Select
|
||||
defaultValue={field.value}
|
||||
{...field}
|
||||
onValueChange={(e) => onChange(e)}
|
||||
className="w-full"
|
||||
>
|
||||
{currentWorkspace?.environments.map((sourceEnvironment) => (
|
||||
<SelectItem
|
||||
value={sourceEnvironment.slug}
|
||||
key={`source-environment-${sourceEnvironment.slug}`}
|
||||
>
|
||||
{sourceEnvironment.name}
|
||||
</SelectItem>
|
||||
))}
|
||||
</Select>
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
<Controller
|
||||
control={control}
|
||||
defaultValue=""
|
||||
name="secretPath"
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label="Secrets Path"
|
||||
isError={Boolean(error)}
|
||||
errorText={error?.message}
|
||||
>
|
||||
<SecretPathInput
|
||||
{...field}
|
||||
environment={selectedSourceEnvironment}
|
||||
placeholder="/"
|
||||
/>
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
<Controller
|
||||
control={control}
|
||||
name="targetAppId"
|
||||
render={({ field: { onChange, ...field }, fieldState: { error } }) => {
|
||||
return (
|
||||
<FormControl
|
||||
label="GCP Project"
|
||||
errorText={error?.message}
|
||||
isError={Boolean(error)}
|
||||
>
|
||||
<Select
|
||||
{...field}
|
||||
onValueChange={(e) => {
|
||||
if (e === "") return;
|
||||
onChange(e);
|
||||
}}
|
||||
className="w-full"
|
||||
>
|
||||
{integrationAuthApps.length > 0 ? (
|
||||
integrationAuthApps.map((integrationAuthApp) => (
|
||||
<SelectItem
|
||||
value={String(integrationAuthApp.appId as string)}
|
||||
key={`target-app-${String(integrationAuthApp.appId)}`}
|
||||
>
|
||||
{integrationAuthApp.name}
|
||||
</SelectItem>
|
||||
))
|
||||
) : (
|
||||
<SelectItem value="none" key="target-app-none">
|
||||
No projects found
|
||||
</SelectItem>
|
||||
)}
|
||||
</Select>
|
||||
</FormControl>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
</motion.div>
|
||||
</TabPanel>
|
||||
<TabPanel value={TabSections.Options}>
|
||||
<motion.div
|
||||
key="panel-1"
|
||||
transition={{ duration: 0.15 }}
|
||||
initial={{ opacity: 0, translateX: -30 }}
|
||||
animate={{ opacity: 1, translateX: 0 }}
|
||||
exit={{ opacity: 0, translateX: 30 }}
|
||||
>
|
||||
<Controller
|
||||
control={control}
|
||||
name="secretPrefix"
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label="Secret Prefix"
|
||||
isError={Boolean(error)}
|
||||
errorText={error?.message}
|
||||
>
|
||||
<Input {...field} placeholder="INFISICAL_" />
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
<Controller
|
||||
control={control}
|
||||
name="secretSuffix"
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label="Secret Suffix"
|
||||
isError={Boolean(error)}
|
||||
errorText={error?.message}
|
||||
>
|
||||
<Input {...field} placeholder="_INFISICAL" />
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
<div className="mb-[2.36rem] mt-8">
|
||||
<Controller
|
||||
control={control}
|
||||
name="shouldLabel"
|
||||
render={({ field: { onChange, value } }) => (
|
||||
<Switch
|
||||
id="label-gcp"
|
||||
onCheckedChange={(isChecked) => onChange(isChecked)}
|
||||
isChecked={value}
|
||||
>
|
||||
Label in GCP Secret Manager
|
||||
</Switch>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
{shouldLabel && (
|
||||
<div className="mt-8">
|
||||
<Controller
|
||||
control={control}
|
||||
name="labelName"
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label="Label Name"
|
||||
isError={Boolean(error)}
|
||||
errorText={error?.message}
|
||||
>
|
||||
<Input {...field} placeholder="managed-by" />
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
<Controller
|
||||
control={control}
|
||||
name="labelValue"
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label="Label Name"
|
||||
isError={Boolean(error)}
|
||||
errorText={error?.message}
|
||||
>
|
||||
<Input {...field} placeholder="infisical" />
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</motion.div>
|
||||
</TabPanel>
|
||||
</Tabs>
|
||||
<Button
|
||||
colorSchema="primary"
|
||||
variant="outline_bg"
|
||||
className="mb-8 ml-auto mr-6 w-min"
|
||||
size="sm"
|
||||
type="submit"
|
||||
isLoading={isLoading}
|
||||
isDisabled={integrationAuthApps.length === 0}
|
||||
>
|
||||
Create Integration
|
||||
</Button>
|
||||
</Card>
|
||||
{/* <div className="border-t border-mineshaft-800 w-full max-w-md mt-6"/>
|
||||
<div className="flex flex-col bg-mineshaft-800 border border-mineshaft-600 w-full p-4 max-w-lg mt-6 rounded-md">
|
||||
<div className="flex flex-row items-center">
|
||||
<FontAwesomeIcon icon={faCircleInfo} className="text-mineshaft-200 text-xl"/>
|
||||
<span className="ml-3 text-md text-mineshaft-100">Pro Tip</span>
|
||||
</div>
|
||||
<span className="text-mineshaft-300 text-sm mt-4">
|
||||
After creating an integration, your secrets will start syncing immediately.
|
||||
|
||||
To avoid overwriting existing secrets in GCP Secret Manager, you may consider adding a secret prefix/suffix and/or enabling labeling in the options tab.
|
||||
</span>
|
||||
</div> */}
|
||||
<Modal
|
||||
isOpen={popUp.confirmIntegration?.isOpen}
|
||||
onOpenChange={(isOpen) => handlePopUpToggle("confirmIntegration", isOpen)}
|
||||
>
|
||||
<ModalContent
|
||||
title="Heads Up"
|
||||
footerContent={
|
||||
<div className="flex items-center space-x-2">
|
||||
<Button onClick={() => onFormSubmit(popUp.confirmIntegration?.data as FormData)}>
|
||||
Continue Anyway
|
||||
</Button>
|
||||
<Button
|
||||
onClick={() => handlePopUpClose("confirmIntegration")}
|
||||
variant="outline_bg"
|
||||
colorSchema="secondary"
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<p>You're about to overwrite any existing secrets in GCP Secret Manager.</p>
|
||||
<p className="mt-4">
|
||||
To avoid this behavior, you may consider adding a secret prefix/suffix or enabling
|
||||
labeling in the options tab.
|
||||
</p>
|
||||
</ModalContent>
|
||||
</Modal>
|
||||
</form>
|
||||
) : (
|
||||
<div className="flex h-full w-full items-center justify-center">
|
||||
<Helmet>
|
||||
<title>Set Up GCP Secret Manager Integration</title>
|
||||
</Helmet>
|
||||
{isIntegrationAuthAppsLoading ? (
|
||||
<img
|
||||
src="/images/loading/loading.gif"
|
||||
height={70}
|
||||
width={120}
|
||||
alt="infisical loading indicator"
|
||||
/>
|
||||
) : (
|
||||
<div className="flex h-max max-w-md flex-col rounded-md border border-mineshaft-600 bg-mineshaft-800 p-6 text-center text-mineshaft-200">
|
||||
<FontAwesomeIcon icon={faBugs} className="inlineli my-2 text-6xl" />
|
||||
<p>
|
||||
Something went wrong. Please contact{" "}
|
||||
<a
|
||||
className="inline cursor-pointer text-mineshaft-100 underline decoration-primary-500 underline-offset-4 opacity-80 duration-200 hover:opacity-100"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
href="mailto:support@infisical.com"
|
||||
>
|
||||
support@infisical.com
|
||||
</a>{" "}
|
||||
if the issue persists.
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
@ -0,0 +1,16 @@
|
||||
import { createFileRoute } from "@tanstack/react-router";
|
||||
import { zodValidator } from "@tanstack/zod-adapter";
|
||||
import { z } from "zod";
|
||||
|
||||
import { 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)
|
||||
});
|
@ -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 />;
|
||||
};
|
@ -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)
|
||||
});
|
@ -0,0 +1,122 @@
|
||||
import crypto from "crypto";
|
||||
|
||||
import { useState } from "react";
|
||||
import { Helmet } from "react-helmet";
|
||||
import { faArrowUpRightFromSquare, faBookOpen } from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { useNavigate } from "@tanstack/react-router";
|
||||
|
||||
import {
|
||||
Button,
|
||||
Card,
|
||||
CardBody,
|
||||
CardTitle,
|
||||
FormControl,
|
||||
Select,
|
||||
SelectItem
|
||||
} from "@app/components/v2";
|
||||
import { useWorkspace } from "@app/context";
|
||||
import { localStorageService } from "@app/helpers/localStorage";
|
||||
import { useGetCloudIntegrations } from "@app/hooks/api";
|
||||
|
||||
import { createIntegrationMissingEnvVarsNotification } from "../../IntegrationsListPage/IntegrationsListPage.utils";
|
||||
|
||||
enum AuthMethod {
|
||||
APP = "APP",
|
||||
OAUTH = "OAUTH"
|
||||
}
|
||||
|
||||
export const GithubAuthorizePage = () => {
|
||||
const navigate = useNavigate();
|
||||
const { data: cloudIntegrations } = useGetCloudIntegrations();
|
||||
const githubIntegration = cloudIntegrations?.find((integration) => integration.slug === "github");
|
||||
const [selectedAuthMethod, setSelectedAuthMethod] = useState<AuthMethod>(AuthMethod.APP);
|
||||
const { currentWorkspace } = useWorkspace();
|
||||
|
||||
return (
|
||||
<div className="flex h-full w-full items-center justify-center">
|
||||
<Helmet>
|
||||
<title>Select GitHub Integration Auth</title>
|
||||
</Helmet>
|
||||
<Card className="mb-12 max-w-lg rounded-md border border-mineshaft-600">
|
||||
<CardTitle
|
||||
className="px-6 text-left text-xl"
|
||||
subTitle="Select how you'd like to integrate with GitHub. We recommend using the GitHub App method for fine-grained access."
|
||||
>
|
||||
<div className="flex flex-row items-center">
|
||||
<div className="flex items-center pb-0.5">
|
||||
<img src="/images/integrations/GitHub.png" height={30} width={30} alt="Github logo" />
|
||||
</div>
|
||||
<span className="ml-2.5">GitHub Integration </span>
|
||||
<a
|
||||
href="https://infisical.com/docs/integrations/cicd/githubactions"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
<div className="mb-1 ml-2 inline-block cursor-default rounded-md bg-yellow/20 px-1.5 pb-[0.03rem] pt-[0.04rem] text-sm text-yellow opacity-80 hover:opacity-100">
|
||||
<FontAwesomeIcon icon={faBookOpen} className="mr-1.5" />
|
||||
Docs
|
||||
<FontAwesomeIcon
|
||||
icon={faArrowUpRightFromSquare}
|
||||
className="mb-[0.07rem] ml-1.5 text-xxs"
|
||||
/>
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
</CardTitle>
|
||||
<CardBody>
|
||||
<FormControl label="Select authentication method">
|
||||
<Select
|
||||
value={selectedAuthMethod}
|
||||
onValueChange={(val) => {
|
||||
setSelectedAuthMethod(val as AuthMethod);
|
||||
}}
|
||||
className="w-full border border-mineshaft-500"
|
||||
>
|
||||
<SelectItem value={AuthMethod.APP}>GitHub App (Recommended)</SelectItem>
|
||||
<SelectItem value={AuthMethod.OAUTH}>OAuth</SelectItem>
|
||||
</Select>
|
||||
</FormControl>
|
||||
<div className="flex items-end">
|
||||
<Button
|
||||
onClick={() => {
|
||||
if (selectedAuthMethod === AuthMethod.APP) {
|
||||
navigate({
|
||||
to: "/secret-manager/$projectId/integrations/select-integration-auth",
|
||||
params: {
|
||||
projectId: currentWorkspace.id
|
||||
},
|
||||
search: {
|
||||
integrationSlug: "github"
|
||||
}
|
||||
});
|
||||
} else {
|
||||
if (!githubIntegration?.clientId) {
|
||||
createIntegrationMissingEnvVarsNotification(
|
||||
"githubactions",
|
||||
"cicd",
|
||||
"connecting-with-github-oauth"
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
const state = crypto.randomBytes(16).toString("hex");
|
||||
localStorage.setItem("latestCSRFToken", state);
|
||||
localStorageService.setIntegrationProjectId(currentWorkspace.id);
|
||||
window.location.assign(
|
||||
`https://github.com/login/oauth/authorize?client_id=${githubIntegration?.clientId}&response_type=code&scope=repo,admin:org&redirect_uri=${window.location.origin}/integrations/github/oauth2/callback&state=${state}`
|
||||
);
|
||||
}
|
||||
}}
|
||||
colorSchema="primary"
|
||||
variant="outline_bg"
|
||||
className="ml-auto mt-4 w-min"
|
||||
>
|
||||
Connect to GitHub
|
||||
</Button>
|
||||
</div>
|
||||
</CardBody>
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
};
|
@ -0,0 +1,9 @@
|
||||
import { createFileRoute } from "@tanstack/react-router";
|
||||
|
||||
import { GithubAuthorizePage } from "./GithubAuthorizePage";
|
||||
|
||||
export const Route = createFileRoute(
|
||||
"/_authenticate/_inject-org-details/secret-manager/$projectId/_secret-manager-layout/integrations/github/auth-mode-selection"
|
||||
)({
|
||||
component: GithubAuthorizePage
|
||||
});
|
@ -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>
|
||||
);
|
||||
};
|
@ -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)
|
||||
});
|
@ -0,0 +1,54 @@
|
||||
import { useEffect } from "react";
|
||||
import { useNavigate, useSearch } from "@tanstack/react-router";
|
||||
|
||||
import { ROUTE_PATHS } from "@app/const/routes";
|
||||
import { useWorkspace } from "@app/context";
|
||||
import { useAuthorizeIntegration } from "@app/hooks/api";
|
||||
|
||||
export const GithubOauthCallbackPage = () => {
|
||||
const navigate = useNavigate();
|
||||
const { mutateAsync } = useAuthorizeIntegration();
|
||||
|
||||
const {
|
||||
code,
|
||||
state,
|
||||
installation_id: installationId
|
||||
} = useSearch({
|
||||
from: ROUTE_PATHS.SecretManager.Integratons.GithubOauthCallbackPage.id
|
||||
});
|
||||
const { currentWorkspace } = useWorkspace();
|
||||
|
||||
useEffect(() => {
|
||||
(async () => {
|
||||
try {
|
||||
// validate state
|
||||
if (state !== localStorage.getItem("latestCSRFToken")) {
|
||||
return;
|
||||
}
|
||||
|
||||
localStorage.removeItem("latestCSRFToken");
|
||||
|
||||
const integrationAuth = await mutateAsync({
|
||||
workspaceId: currentWorkspace.id,
|
||||
code: code as string,
|
||||
installationId,
|
||||
integration: "github"
|
||||
});
|
||||
|
||||
navigate({
|
||||
to: "/secret-manager/$projectId/integrations/github/create",
|
||||
params: {
|
||||
projectId: currentWorkspace.id
|
||||
},
|
||||
search: {
|
||||
integrationAuthId: integrationAuth.id
|
||||
}
|
||||
});
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
}
|
||||
})();
|
||||
}, []);
|
||||
|
||||
return <div />;
|
||||
};
|
@ -0,0 +1,18 @@
|
||||
import { createFileRoute } from "@tanstack/react-router";
|
||||
import { zodValidator } from "@tanstack/zod-adapter";
|
||||
import { z } from "zod";
|
||||
|
||||
import { GithubOauthCallbackPage } from "./GithubOauthCallbackPage";
|
||||
|
||||
export const GithubOAuthCallbackPageQueryParamsSchema = z.object({
|
||||
state: z.string().catch(""),
|
||||
installation_id: z.string(),
|
||||
code: z.string()
|
||||
});
|
||||
|
||||
export const Route = createFileRoute(
|
||||
"/_authenticate/_inject-org-details/secret-manager/$projectId/_secret-manager-layout/integrations/github/oauth2/callback"
|
||||
)({
|
||||
component: GithubOauthCallbackPage,
|
||||
validateSearch: zodValidator(GithubOAuthCallbackPageQueryParamsSchema)
|
||||
});
|
@ -0,0 +1,121 @@
|
||||
import crypto from "crypto";
|
||||
|
||||
import { Helmet } from "react-helmet";
|
||||
import { Controller, useForm } from "react-hook-form";
|
||||
import { faArrowUpRightFromSquare, faBookOpen } from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { z } from "zod";
|
||||
|
||||
import { Button, Card, CardTitle, FormControl, Input } from "@app/components/v2";
|
||||
import { useWorkspace } from "@app/context";
|
||||
import { localStorageService } from "@app/helpers/localStorage";
|
||||
import { useGetCloudIntegrations } from "@app/hooks/api";
|
||||
|
||||
import { createIntegrationMissingEnvVarsNotification } from "../../IntegrationsListPage/IntegrationsListPage.utils";
|
||||
|
||||
const schema = z.object({
|
||||
gitLabURL: z.string()
|
||||
});
|
||||
|
||||
type FormData = z.infer<typeof schema>;
|
||||
|
||||
export const GitlabAuthorizePage = () => {
|
||||
const { control, handleSubmit } = useForm<FormData>({
|
||||
resolver: zodResolver(schema),
|
||||
defaultValues: {
|
||||
gitLabURL: ""
|
||||
}
|
||||
});
|
||||
|
||||
const { data: cloudIntegrations } = useGetCloudIntegrations();
|
||||
const { currentWorkspace } = useWorkspace();
|
||||
|
||||
const onFormSubmit = ({ gitLabURL }: FormData) => {
|
||||
if (!cloudIntegrations) return;
|
||||
const integrationOption = cloudIntegrations.find(
|
||||
(integration) => integration.slug === "gitlab"
|
||||
);
|
||||
|
||||
if (!integrationOption) return;
|
||||
|
||||
if (!integrationOption.clientId) {
|
||||
createIntegrationMissingEnvVarsNotification(integrationOption.slug, "cicd");
|
||||
return;
|
||||
}
|
||||
|
||||
const baseURL =
|
||||
(gitLabURL as string).trim() === "" ? "https://gitlab.com" : (gitLabURL as string).trim();
|
||||
|
||||
const csrfToken = crypto.randomBytes(16).toString("hex");
|
||||
localStorage.setItem("latestCSRFToken", csrfToken);
|
||||
localStorageService.setIntegrationProjectId(currentWorkspace.id);
|
||||
|
||||
const state = `${csrfToken}|${
|
||||
(gitLabURL as string).trim() === "" ? "" : (gitLabURL as string).trim()
|
||||
}`;
|
||||
localStorageService.setIntegrationProjectId(currentWorkspace.id);
|
||||
const link = `${baseURL}/oauth/authorize?client_id=${integrationOption.clientId}&redirect_uri=${window.location.origin}/integrations/gitlab/oauth2/callback&response_type=code&state=${state}`;
|
||||
|
||||
window.location.assign(link);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex h-full w-full items-center justify-center">
|
||||
<Helmet>
|
||||
<title>Authorize GitLab Integration</title>
|
||||
</Helmet>
|
||||
<Card className="mb-12 max-w-lg rounded-md border border-mineshaft-600">
|
||||
<CardTitle
|
||||
className="px-6 text-left text-xl"
|
||||
subTitle="Authorize this integration to sync secrets from Infisical to GitLab. If no self-hosted GitLab URL is specified, then Infisical will connect you to GitLab Cloud."
|
||||
>
|
||||
<div className="flex flex-row items-center">
|
||||
<div className="flex items-center pb-0.5">
|
||||
<img src="/images/integrations/Gitlab.png" height={28} width={28} alt="Gitlab logo" />
|
||||
</div>
|
||||
<span className="ml-2.5">GitLab Integration </span>
|
||||
<a
|
||||
href="https://infisical.com/docs/integrations/cicd/gitlab"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
<div className="mb-1 ml-2 inline-block cursor-default rounded-md bg-yellow/20 px-1.5 pb-[0.03rem] pt-[0.04rem] text-sm text-yellow opacity-80 hover:opacity-100">
|
||||
<FontAwesomeIcon icon={faBookOpen} className="mr-1.5" />
|
||||
Docs
|
||||
<FontAwesomeIcon
|
||||
icon={faArrowUpRightFromSquare}
|
||||
className="mb-[0.07rem] ml-1.5 text-xxs"
|
||||
/>
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
</CardTitle>
|
||||
<form onSubmit={handleSubmit(onFormSubmit)} className="px-6 pb-8 text-right">
|
||||
<Controller
|
||||
control={control}
|
||||
name="gitLabURL"
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label="Self-hosted URL (optional)"
|
||||
errorText={error?.message}
|
||||
isError={Boolean(error)}
|
||||
>
|
||||
<Input {...field} placeholder="https://self-hosted-gitlab.com" />
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
<Button
|
||||
colorSchema="primary"
|
||||
variant="outline_bg"
|
||||
className="mt-2 w-min"
|
||||
size="sm"
|
||||
type="submit"
|
||||
>
|
||||
Continue with OAuth
|
||||
</Button>
|
||||
</form>
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
};
|
@ -0,0 +1,9 @@
|
||||
import { createFileRoute } from "@tanstack/react-router";
|
||||
|
||||
import { GitlabAuthorizePage } from "./GitlabAuthorizePage";
|
||||
|
||||
export const Route = createFileRoute(
|
||||
"/_authenticate/_inject-org-details/secret-manager/$projectId/_secret-manager-layout/integrations/gitlab/authorize"
|
||||
)({
|
||||
component: GitlabAuthorizePage
|
||||
});
|
@ -0,0 +1,530 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import { Helmet } from "react-helmet";
|
||||
import { Controller, useForm } from "react-hook-form";
|
||||
import { faArrowUpRightFromSquare, faBookOpen, faBugs } from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { yupResolver } from "@hookform/resolvers/yup";
|
||||
import { useNavigate, useSearch } from "@tanstack/react-router";
|
||||
import { motion } from "framer-motion";
|
||||
import { z } from "zod";
|
||||
|
||||
import {
|
||||
Button,
|
||||
Card,
|
||||
CardTitle,
|
||||
FormControl,
|
||||
Input,
|
||||
Modal,
|
||||
ModalContent,
|
||||
Select,
|
||||
SelectItem,
|
||||
Switch,
|
||||
Tab,
|
||||
TabList,
|
||||
TabPanel,
|
||||
Tabs
|
||||
} from "@app/components/v2";
|
||||
import { SecretPathInput } from "@app/components/v2/SecretPathInput";
|
||||
import { ROUTE_PATHS } from "@app/const/routes";
|
||||
import { useWorkspace } from "@app/context";
|
||||
import { usePopUp } from "@app/hooks";
|
||||
import { useCreateIntegration } from "@app/hooks/api";
|
||||
import {
|
||||
useGetIntegrationAuthApps,
|
||||
useGetIntegrationAuthById,
|
||||
useGetIntegrationAuthTeams
|
||||
} from "@app/hooks/api/integrationAuth";
|
||||
|
||||
const gitLabEntities = [
|
||||
{ name: "Individual", value: "individual" },
|
||||
{ name: "Group", value: "group" }
|
||||
] as const;
|
||||
|
||||
enum TabSections {
|
||||
Connection = "connection",
|
||||
Options = "options"
|
||||
}
|
||||
|
||||
const schema = z.object({
|
||||
targetEntity: z.enum([gitLabEntities[0].value, gitLabEntities[1].value]),
|
||||
targetTeamId: z.string().optional(),
|
||||
selectedSourceEnvironment: z.string(),
|
||||
secretPath: z.string(),
|
||||
targetAppId: z.string(),
|
||||
targetEnvironment: z.string().optional(),
|
||||
secretPrefix: z.string().optional(),
|
||||
secretSuffix: z.string().optional(),
|
||||
shouldMaskSecrets: z.boolean().optional(),
|
||||
shouldProtectSecrets: z.boolean()
|
||||
});
|
||||
|
||||
type FormData = z.infer<typeof schema>;
|
||||
|
||||
export const GitlabConfigurePage = () => {
|
||||
const navigate = useNavigate();
|
||||
const { popUp, handlePopUpOpen, handlePopUpToggle, handlePopUpClose } = usePopUp([
|
||||
"confirmIntegration"
|
||||
] as const);
|
||||
const { currentWorkspace } = useWorkspace();
|
||||
|
||||
const { control, handleSubmit, setValue, watch } = useForm<FormData>({
|
||||
resolver: yupResolver(schema),
|
||||
defaultValues: {
|
||||
targetEntity: "individual",
|
||||
secretPath: "/",
|
||||
secretPrefix: "",
|
||||
secretSuffix: "",
|
||||
selectedSourceEnvironment: currentWorkspace.environments[0].slug
|
||||
}
|
||||
});
|
||||
const selectedSourceEnvironment = watch("selectedSourceEnvironment");
|
||||
const targetEntity = watch("targetEntity");
|
||||
const targetTeamId = watch("targetTeamId");
|
||||
const targetAppIdValue = watch("targetAppId");
|
||||
|
||||
const { mutateAsync } = useCreateIntegration();
|
||||
|
||||
const integrationAuthId = useSearch({
|
||||
from: ROUTE_PATHS.SecretManager.Integratons.GitlabConfigurePage.id,
|
||||
select: (el) => el.integrationAuthId
|
||||
});
|
||||
|
||||
const { data: integrationAuth } = useGetIntegrationAuthById((integrationAuthId as string) ?? "");
|
||||
|
||||
const { data: integrationAuthApps, isLoading: isIntegrationAuthAppsLoading } =
|
||||
useGetIntegrationAuthApps({
|
||||
integrationAuthId: (integrationAuthId as string) ?? "",
|
||||
...(targetTeamId ? { teamId: targetTeamId } : {})
|
||||
});
|
||||
const { data: integrationAuthTeams, isLoading: isintegrationAuthTeamsLoading } =
|
||||
useGetIntegrationAuthTeams((integrationAuthId as string) ?? "");
|
||||
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (integrationAuthApps) {
|
||||
if (integrationAuthApps.length > 0) {
|
||||
setValue("targetAppId", String(integrationAuthApps[0].appId as string));
|
||||
} else {
|
||||
setValue("targetAppId", "none");
|
||||
}
|
||||
}
|
||||
}, [integrationAuthApps]);
|
||||
|
||||
useEffect(() => {
|
||||
if (targetEntity === "group" && integrationAuthTeams && integrationAuthTeams.length > 0) {
|
||||
if (integrationAuthTeams) {
|
||||
if (integrationAuthTeams.length > 0) {
|
||||
// case: user is part of at least 1 group in GitLab
|
||||
setValue("targetTeamId", String(integrationAuthTeams[0].id));
|
||||
} else {
|
||||
// case: user is not part of any groups in GitLab
|
||||
setValue("targetTeamId", "none");
|
||||
}
|
||||
}
|
||||
} else if (targetEntity === "individual") {
|
||||
setValue("targetTeamId", undefined);
|
||||
}
|
||||
}, [targetEntity, integrationAuthTeams]);
|
||||
|
||||
const onFormSubmit = async ({
|
||||
selectedSourceEnvironment: sse,
|
||||
secretPath,
|
||||
targetAppId,
|
||||
targetEnvironment,
|
||||
secretPrefix,
|
||||
secretSuffix,
|
||||
shouldMaskSecrets,
|
||||
shouldProtectSecrets
|
||||
}: FormData) => {
|
||||
try {
|
||||
setIsLoading(true);
|
||||
if (!integrationAuth?.id) return;
|
||||
|
||||
await mutateAsync({
|
||||
integrationAuthId: integrationAuth?.id,
|
||||
isActive: true,
|
||||
app: integrationAuthApps?.find(
|
||||
(integrationAuthApp) => String(integrationAuthApp.appId) === targetAppId
|
||||
)?.name,
|
||||
appId: String(targetAppId),
|
||||
sourceEnvironment: sse,
|
||||
targetEnvironment: targetEnvironment === "" ? "*" : targetEnvironment,
|
||||
secretPath,
|
||||
metadata: {
|
||||
secretPrefix,
|
||||
secretSuffix,
|
||||
shouldMaskSecrets,
|
||||
shouldProtectSecrets
|
||||
}
|
||||
});
|
||||
|
||||
setIsLoading(false);
|
||||
navigate({
|
||||
to: "/secret-manager/$projectId/integrations",
|
||||
params: {
|
||||
projectId: currentWorkspace.id
|
||||
}
|
||||
});
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
return integrationAuth &&
|
||||
selectedSourceEnvironment &&
|
||||
integrationAuthApps &&
|
||||
integrationAuthTeams ? (
|
||||
<form
|
||||
onSubmit={handleSubmit((data: FormData) => {
|
||||
if (!data.secretPrefix && !data.secretSuffix) {
|
||||
handlePopUpOpen("confirmIntegration", data);
|
||||
return;
|
||||
}
|
||||
|
||||
onFormSubmit(data);
|
||||
})}
|
||||
className="flex h-full w-full flex-col items-center justify-center"
|
||||
>
|
||||
<Helmet>
|
||||
<title>Set Up GitLab Integration</title>
|
||||
</Helmet>
|
||||
<Card className="max-w-lg rounded-md border border-mineshaft-600">
|
||||
<CardTitle
|
||||
className="px-6 text-left text-xl"
|
||||
subTitle="Select which environment or folder in Infisical you want to sync to GitLab's environment variables."
|
||||
>
|
||||
<div className="flex flex-row items-center">
|
||||
<div className="flex items-center pb-0.5">
|
||||
<img src="/images/integrations/Gitlab.png" height={28} width={28} alt="Gitlab logo" />
|
||||
</div>
|
||||
<span className="ml-2.5">GitLab Integration </span>
|
||||
<a
|
||||
href="https://infisical.com/docs/integrations/cicd/gitlab"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
<div className="mb-1 ml-2 inline-block cursor-default rounded-md bg-yellow/20 px-1.5 pb-[0.03rem] pt-[0.04rem] text-sm text-yellow opacity-80 hover:opacity-100">
|
||||
<FontAwesomeIcon icon={faBookOpen} className="mr-1.5" />
|
||||
Docs
|
||||
<FontAwesomeIcon
|
||||
icon={faArrowUpRightFromSquare}
|
||||
className="mb-[0.07rem] ml-1.5 text-xxs"
|
||||
/>
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
</CardTitle>
|
||||
<Tabs defaultValue={TabSections.Connection} className="px-6">
|
||||
<TabList>
|
||||
<div className="flex w-full flex-row border-b border-mineshaft-600">
|
||||
<Tab value={TabSections.Connection}>Connection</Tab>
|
||||
<Tab value={TabSections.Options}>Options</Tab>
|
||||
</div>
|
||||
</TabList>
|
||||
<TabPanel value={TabSections.Connection}>
|
||||
<motion.div
|
||||
key="panel-1"
|
||||
transition={{ duration: 0.15 }}
|
||||
initial={{ opacity: 0, translateX: 30 }}
|
||||
animate={{ opacity: 1, translateX: 0 }}
|
||||
exit={{ opacity: 0, translateX: 30 }}
|
||||
>
|
||||
<Controller
|
||||
control={control}
|
||||
name="selectedSourceEnvironment"
|
||||
render={({ field: { onChange, ...field }, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label="Project Environment"
|
||||
errorText={error?.message}
|
||||
isError={Boolean(error)}
|
||||
>
|
||||
<Select
|
||||
defaultValue={field.value}
|
||||
{...field}
|
||||
onValueChange={(e) => onChange(e)}
|
||||
className="w-full"
|
||||
>
|
||||
{currentWorkspace?.environments.map((sourceEnvironment) => (
|
||||
<SelectItem
|
||||
value={sourceEnvironment.slug}
|
||||
key={`source-environment-${sourceEnvironment.slug}`}
|
||||
>
|
||||
{sourceEnvironment.name}
|
||||
</SelectItem>
|
||||
))}
|
||||
</Select>
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
<Controller
|
||||
control={control}
|
||||
defaultValue=""
|
||||
name="secretPath"
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label="Secrets Path"
|
||||
isError={Boolean(error)}
|
||||
errorText={error?.message}
|
||||
>
|
||||
<SecretPathInput
|
||||
{...field}
|
||||
placeholder="/"
|
||||
environment={selectedSourceEnvironment}
|
||||
/>
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
<Controller
|
||||
control={control}
|
||||
name="targetEntity"
|
||||
render={({ field: { onChange, ...field }, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label="GitLab Integration Type"
|
||||
errorText={error?.message}
|
||||
isError={Boolean(error)}
|
||||
>
|
||||
<Select {...field} onValueChange={(e) => onChange(e)} className="w-full">
|
||||
{gitLabEntities.map((entity) => {
|
||||
return (
|
||||
<SelectItem value={entity.value} key={`target-entity-${entity.value}`}>
|
||||
{entity.name}
|
||||
</SelectItem>
|
||||
);
|
||||
})}
|
||||
</Select>
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
{targetEntity === "group" && targetTeamId && (
|
||||
<Controller
|
||||
control={control}
|
||||
name="targetTeamId"
|
||||
render={({ field: { onChange, ...field }, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label="GitLab Group"
|
||||
errorText={error?.message}
|
||||
isError={Boolean(error)}
|
||||
>
|
||||
<Select {...field} onValueChange={(e) => onChange(e)} className="w-full">
|
||||
{integrationAuthTeams.length > 0 ? (
|
||||
integrationAuthTeams.map((integrationAuthTeam) => (
|
||||
<SelectItem
|
||||
value={String(integrationAuthTeam.id as string)}
|
||||
key={`target-team-${String(integrationAuthTeam.id)}`}
|
||||
>
|
||||
{integrationAuthTeam.name}
|
||||
</SelectItem>
|
||||
))
|
||||
) : (
|
||||
<SelectItem value="none" key="target-team-none">
|
||||
No groups found
|
||||
</SelectItem>
|
||||
)}
|
||||
</Select>
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
<Controller
|
||||
control={control}
|
||||
name="targetAppId"
|
||||
render={({ field: { onChange, ...field }, fieldState: { error } }) => {
|
||||
return (
|
||||
<FormControl
|
||||
label="GitLab Project"
|
||||
errorText={error?.message}
|
||||
isError={Boolean(error)}
|
||||
>
|
||||
<Select
|
||||
{...field}
|
||||
onValueChange={(e) => {
|
||||
if (e === "") return;
|
||||
onChange(e);
|
||||
}}
|
||||
className="w-full"
|
||||
>
|
||||
{integrationAuthApps.length > 0 ? (
|
||||
integrationAuthApps.map((integrationAuthApp) => (
|
||||
<SelectItem
|
||||
value={String(integrationAuthApp.appId as string)}
|
||||
key={`target-app-${String(integrationAuthApp.appId)}`}
|
||||
>
|
||||
{integrationAuthApp.name}
|
||||
</SelectItem>
|
||||
))
|
||||
) : (
|
||||
<SelectItem value="none" key="target-app-none">
|
||||
No projects found
|
||||
</SelectItem>
|
||||
)}
|
||||
</Select>
|
||||
</FormControl>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
<Controller
|
||||
control={control}
|
||||
defaultValue=""
|
||||
name="targetEnvironment"
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label="GitLab Environment Scope (Optional)"
|
||||
isError={Boolean(error)}
|
||||
errorText={error?.message}
|
||||
>
|
||||
<Input {...field} placeholder="*" />
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
</motion.div>
|
||||
</TabPanel>
|
||||
<TabPanel value={TabSections.Options}>
|
||||
<motion.div
|
||||
key="panel-1"
|
||||
transition={{ duration: 0.15 }}
|
||||
initial={{ opacity: 0, translateX: -30 }}
|
||||
animate={{ opacity: 1, translateX: 0 }}
|
||||
exit={{ opacity: 0, translateX: 30 }}
|
||||
className="pb-[14.25rem]"
|
||||
>
|
||||
<div className="ml-1">
|
||||
<Controller
|
||||
control={control}
|
||||
name="shouldMaskSecrets"
|
||||
render={({ field: { onChange, value } }) => (
|
||||
<Switch
|
||||
id="should-mask-secrets"
|
||||
onCheckedChange={(isChecked) => onChange(isChecked)}
|
||||
isChecked={value}
|
||||
>
|
||||
<div className="max-w-md">
|
||||
Mark Infisical secrets in Gitlab as 'Masked' secrets
|
||||
</div>
|
||||
</Switch>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
<div className="mb-5 ml-1 mt-4">
|
||||
<Controller
|
||||
control={control}
|
||||
name="shouldProtectSecrets"
|
||||
render={({ field: { onChange, value } }) => (
|
||||
<Switch
|
||||
id="should-protect-secrets"
|
||||
onCheckedChange={(isChecked) => onChange(isChecked)}
|
||||
isChecked={value}
|
||||
>
|
||||
Mark Infisical secrets in Gitlab as 'Protected' secrets
|
||||
</Switch>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
<Controller
|
||||
control={control}
|
||||
name="secretPrefix"
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label="Secret Prefix"
|
||||
isError={Boolean(error)}
|
||||
errorText={error?.message}
|
||||
>
|
||||
<Input {...field} placeholder="INFISICAL_" />
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
<Controller
|
||||
control={control}
|
||||
name="secretSuffix"
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label="Secret Suffix"
|
||||
isError={Boolean(error)}
|
||||
errorText={error?.message}
|
||||
>
|
||||
<Input {...field} placeholder="_INFISICAL" />
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
</motion.div>
|
||||
</TabPanel>
|
||||
</Tabs>
|
||||
<Button
|
||||
colorSchema="primary"
|
||||
variant="outline_bg"
|
||||
className="mb-8 ml-auto mr-6 w-min"
|
||||
size="sm"
|
||||
type="submit"
|
||||
isLoading={isLoading}
|
||||
isDisabled={targetAppIdValue === "none"}
|
||||
>
|
||||
Create Integration
|
||||
</Button>
|
||||
</Card>
|
||||
{/* <div className="border-t border-mineshaft-800 w-full max-w-md mt-6"/>
|
||||
<div className="flex flex-col bg-mineshaft-800 border border-mineshaft-600 w-full p-4 max-w-lg mt-6 rounded-md">
|
||||
<div className="flex flex-row items-center"><FontAwesomeIcon icon={faCircleInfo} className="text-mineshaft-200 text-xl"/> <span className="ml-3 text-md text-mineshaft-100">Pro Tip</span></div>
|
||||
<span className="text-mineshaft-300 text-sm mt-4">After creating an integration, your secrets will start syncing immediately. This might cause an unexpected override of current secrets in GitLab with secrets from Infisical.</span>
|
||||
</div> */}
|
||||
<Modal
|
||||
isOpen={popUp.confirmIntegration?.isOpen}
|
||||
onOpenChange={(isOpen) => handlePopUpToggle("confirmIntegration", isOpen)}
|
||||
>
|
||||
<ModalContent
|
||||
title="Heads Up"
|
||||
footerContent={
|
||||
<div className="flex items-center space-x-2">
|
||||
<Button onClick={() => onFormSubmit(popUp.confirmIntegration?.data as FormData)}>
|
||||
Continue Anyway
|
||||
</Button>
|
||||
<Button
|
||||
onClick={() => handlePopUpClose("confirmIntegration")}
|
||||
variant="outline_bg"
|
||||
colorSchema="secondary"
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<p>You're about to overwrite any existing secrets in GitLab.</p>
|
||||
<p className="mt-4">
|
||||
To avoid this behavior, you may consider adding a secret prefix/suffix in the options
|
||||
tab.
|
||||
</p>
|
||||
</ModalContent>
|
||||
</Modal>
|
||||
</form>
|
||||
) : (
|
||||
<div className="flex h-full w-full items-center justify-center">
|
||||
<Helmet>
|
||||
<title>Set Up GitLab Integration</title>
|
||||
</Helmet>
|
||||
{isIntegrationAuthAppsLoading || isintegrationAuthTeamsLoading ? (
|
||||
<img
|
||||
src="/images/loading/loading.gif"
|
||||
height={70}
|
||||
width={120}
|
||||
alt="infisical loading indicator"
|
||||
/>
|
||||
) : (
|
||||
<div className="flex h-max max-w-md flex-col rounded-md border border-mineshaft-600 bg-mineshaft-800 p-6 text-center text-mineshaft-200">
|
||||
<FontAwesomeIcon icon={faBugs} className="inlineli my-2 text-6xl" />
|
||||
<p>
|
||||
Something went wrong. Please contact{" "}
|
||||
<a
|
||||
className="inline cursor-pointer text-mineshaft-100 underline decoration-primary-500 underline-offset-4 opacity-80 duration-200 hover:opacity-100"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
href="mailto:support@infisical.com"
|
||||
>
|
||||
support@infisical.com
|
||||
</a>{" "}
|
||||
if the issue persists.
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
@ -0,0 +1,16 @@
|
||||
import { createFileRoute } from "@tanstack/react-router";
|
||||
import { zodValidator } from "@tanstack/zod-adapter";
|
||||
import { z } from "zod";
|
||||
|
||||
import { GitlabConfigurePage } from "./GitlabConfigurePage";
|
||||
|
||||
const GitlabConfigurePageQueryParamsSchema = z.object({
|
||||
integrationAuthId: z.string()
|
||||
});
|
||||
|
||||
export const Route = createFileRoute(
|
||||
"/_authenticate/_inject-org-details/secret-manager/$projectId/_secret-manager-layout/integrations/gitlab/create"
|
||||
)({
|
||||
component: GitlabConfigurePage,
|
||||
validateSearch: zodValidator(GitlabConfigurePageQueryParamsSchema)
|
||||
});
|
@ -0,0 +1,53 @@
|
||||
import { useEffect } from "react";
|
||||
import { useNavigate, useSearch } from "@tanstack/react-router";
|
||||
|
||||
import { ROUTE_PATHS } from "@app/const/routes";
|
||||
import { useWorkspace } from "@app/context";
|
||||
import { useAuthorizeIntegration } from "@app/hooks/api";
|
||||
|
||||
export const GitLabOAuthCallbackPage = () => {
|
||||
const navigate = useNavigate();
|
||||
const { mutateAsync } = useAuthorizeIntegration();
|
||||
|
||||
const { code, state } = useSearch({
|
||||
from: ROUTE_PATHS.SecretManager.Integratons.GitlabOauthCallbackPage.id
|
||||
});
|
||||
const { currentWorkspace } = useWorkspace();
|
||||
|
||||
useEffect(() => {
|
||||
(async () => {
|
||||
try {
|
||||
// validate state
|
||||
const [csrfToken, url] = (state as string).split("|", 2);
|
||||
|
||||
if (csrfToken !== localStorage.getItem("latestCSRFToken")) return;
|
||||
localStorage.removeItem("latestCSRFToken");
|
||||
|
||||
const integrationAuth = await mutateAsync({
|
||||
workspaceId: currentWorkspace.id,
|
||||
code: code as string,
|
||||
integration: "gitlab",
|
||||
...(url === ""
|
||||
? {}
|
||||
: {
|
||||
url
|
||||
})
|
||||
});
|
||||
|
||||
navigate({
|
||||
to: "/secret-manager/$projectId/integrations/gitlab/create",
|
||||
params: {
|
||||
projectId: currentWorkspace.id
|
||||
},
|
||||
search: {
|
||||
integrationAuthId: integrationAuth.id
|
||||
}
|
||||
});
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
}
|
||||
})();
|
||||
}, []);
|
||||
|
||||
return <div />;
|
||||
};
|
@ -0,0 +1,17 @@
|
||||
import { createFileRoute } from "@tanstack/react-router";
|
||||
import { zodValidator } from "@tanstack/zod-adapter";
|
||||
import { z } from "zod";
|
||||
|
||||
import { GitLabOAuthCallbackPage } from "./GitlabOauthCallbackPage";
|
||||
|
||||
export const GitlabOAuthCallbackPageQueryParamsSchema = z.object({
|
||||
state: z.string().catch(""),
|
||||
code: z.string()
|
||||
});
|
||||
|
||||
export const Route = createFileRoute(
|
||||
"/_authenticate/_inject-org-details/secret-manager/$projectId/_secret-manager-layout/integrations/gitlab/oauth2/callback"
|
||||
)({
|
||||
component: GitLabOAuthCallbackPage,
|
||||
validateSearch: zodValidator(GitlabOAuthCallbackPageQueryParamsSchema)
|
||||
});
|
@ -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>
|
||||
);
|
||||
};
|
@ -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
|
||||
});
|
@ -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>
|
||||
);
|
||||
};
|
@ -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)
|
||||
});
|
@ -0,0 +1,123 @@
|
||||
import { useState } from "react";
|
||||
import { Helmet } from "react-helmet";
|
||||
import { Controller, useForm } from "react-hook-form";
|
||||
import { faArrowUpRightFromSquare, faBookOpen } from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { yupResolver } from "@hookform/resolvers/yup";
|
||||
import { useNavigate } from "@tanstack/react-router";
|
||||
import { z } from "zod";
|
||||
|
||||
import { Button, Card, CardTitle, FormControl, Input } from "@app/components/v2";
|
||||
import { useWorkspace } from "@app/context";
|
||||
import { useSaveIntegrationAccessToken } from "@app/hooks/api";
|
||||
|
||||
const schema = z.object({
|
||||
accessToken: z.string().trim()
|
||||
});
|
||||
|
||||
type FormData = z.infer<typeof schema>;
|
||||
|
||||
const APP_NAME = "Hasura Cloud";
|
||||
export const HasuraCloudAuthorizePage = () => {
|
||||
const navigate = useNavigate();
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const { mutateAsync } = useSaveIntegrationAccessToken();
|
||||
|
||||
const { currentWorkspace } = useWorkspace();
|
||||
const { control, handleSubmit } = useForm<FormData>({
|
||||
resolver: yupResolver(schema),
|
||||
defaultValues: {
|
||||
accessToken: ""
|
||||
}
|
||||
});
|
||||
|
||||
const onFormSubmit = async ({ accessToken }: FormData) => {
|
||||
try {
|
||||
setIsLoading(true);
|
||||
|
||||
const integrationAuth = await mutateAsync({
|
||||
workspaceId: currentWorkspace.id,
|
||||
integration: "hasura-cloud",
|
||||
accessToken
|
||||
});
|
||||
|
||||
setIsLoading(false);
|
||||
navigate({
|
||||
to: "/secret-manager/$projectId/integrations/hasura-cloud/create",
|
||||
params: {
|
||||
projectId: currentWorkspace.id
|
||||
},
|
||||
search: {
|
||||
integrationAuthId: integrationAuth.id
|
||||
}
|
||||
});
|
||||
} catch (err) {
|
||||
setIsLoading(false);
|
||||
console.error(err);
|
||||
}
|
||||
};
|
||||
return (
|
||||
<div className="flex h-full w-full items-center justify-center">
|
||||
<Helmet>
|
||||
<title>Authorize {APP_NAME} Integration</title>
|
||||
</Helmet>
|
||||
<Card className="mb-12 max-w-lg rounded-md border border-mineshaft-600">
|
||||
<CardTitle
|
||||
className="px-6 text-left text-xl"
|
||||
subTitle="After adding your access token, you will be prompted to set up an integration for a particular Infisical project and environment."
|
||||
>
|
||||
<div className="flex flex-row items-center">
|
||||
<div className="flex items-center pb-0.5">
|
||||
<img
|
||||
src="/images/integrations/Hasura.svg"
|
||||
height={30}
|
||||
width={30}
|
||||
alt={`${APP_NAME} logo`}
|
||||
/>
|
||||
</div>
|
||||
<span className="ml-2.5">{APP_NAME} Integration </span>
|
||||
<a
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
href="https://infisical.com/docs/integrations/cloud/hasura-cloud"
|
||||
>
|
||||
<div className="mb-1 ml-2 inline-block cursor-default rounded-md bg-yellow/20 px-1.5 pb-[0.03rem] pt-[0.04rem] text-sm text-yellow opacity-80 hover:opacity-100">
|
||||
<FontAwesomeIcon icon={faBookOpen} className="mr-1.5" />
|
||||
Docs
|
||||
<FontAwesomeIcon
|
||||
icon={faArrowUpRightFromSquare}
|
||||
className="mb-[0.07rem] ml-1.5 text-xxs"
|
||||
/>
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
</CardTitle>
|
||||
<form onSubmit={handleSubmit(onFormSubmit)} className="px-6 pb-8 text-right">
|
||||
<Controller
|
||||
control={control}
|
||||
name="accessToken"
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label={`${APP_NAME} Access Token`}
|
||||
errorText={error?.message}
|
||||
isError={Boolean(error)}
|
||||
>
|
||||
<Input {...field} placeholder="" />
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
<Button
|
||||
colorSchema="primary"
|
||||
variant="outline_bg"
|
||||
className="mt-2 w-min"
|
||||
size="sm"
|
||||
type="submit"
|
||||
isLoading={isLoading}
|
||||
>
|
||||
Connect to {APP_NAME}
|
||||
</Button>
|
||||
</form>
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
};
|
@ -0,0 +1,9 @@
|
||||
import { createFileRoute } from "@tanstack/react-router";
|
||||
|
||||
import { HasuraCloudAuthorizePage } from "./HasuraCloudAuthorizePage";
|
||||
|
||||
export const Route = createFileRoute(
|
||||
"/_authenticate/_inject-org-details/secret-manager/$projectId/_secret-manager-layout/integrations/hasura-cloud/authorize"
|
||||
)({
|
||||
component: HasuraCloudAuthorizePage
|
||||
});
|
@ -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>
|
||||
);
|
||||
};
|
@ -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)
|
||||
});
|
@ -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>
|
||||
);
|
||||
};
|
@ -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)
|
||||
});
|
@ -0,0 +1,45 @@
|
||||
import { useEffect } from "react";
|
||||
import { useNavigate, useSearch } from "@tanstack/react-router";
|
||||
|
||||
import { ROUTE_PATHS } from "@app/const/routes";
|
||||
import { useWorkspace } from "@app/context";
|
||||
import { useAuthorizeIntegration } from "@app/hooks/api";
|
||||
|
||||
export const HerokuOAuthCallbackPage = () => {
|
||||
const navigate = useNavigate();
|
||||
const { mutateAsync } = useAuthorizeIntegration();
|
||||
|
||||
const { code, state } = useSearch({
|
||||
from: ROUTE_PATHS.SecretManager.Integratons.HerokuOauthCallbackPage.id
|
||||
});
|
||||
const { currentWorkspace } = useWorkspace();
|
||||
|
||||
useEffect(() => {
|
||||
(async () => {
|
||||
try {
|
||||
// validate state
|
||||
if (state !== localStorage.getItem("latestCSRFToken")) return;
|
||||
localStorage.removeItem("latestCSRFToken");
|
||||
const integrationAuth = await mutateAsync({
|
||||
workspaceId: currentWorkspace.id,
|
||||
code: code as string,
|
||||
integration: "heroku"
|
||||
});
|
||||
|
||||
navigate({
|
||||
to: "/secret-manager/$projectId/integrations/heroku/create",
|
||||
params: {
|
||||
projectId: currentWorkspace.id
|
||||
},
|
||||
search: {
|
||||
integrationAuthId: integrationAuth.id
|
||||
}
|
||||
});
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
}
|
||||
})();
|
||||
}, []);
|
||||
|
||||
return <div />;
|
||||
};
|
@ -0,0 +1,17 @@
|
||||
import { createFileRoute } from "@tanstack/react-router";
|
||||
import { zodValidator } from "@tanstack/zod-adapter";
|
||||
import { z } from "zod";
|
||||
|
||||
import { HerokuOAuthCallbackPage } from "./HerokuOauthCallbackPage";
|
||||
|
||||
export const HerokuOAuthCallbackPageQueryParamsSchema = z.object({
|
||||
state: z.string().catch(""),
|
||||
code: z.string()
|
||||
});
|
||||
|
||||
export const Route = createFileRoute(
|
||||
"/_authenticate/_inject-org-details/secret-manager/$projectId/_secret-manager-layout/integrations/heroku/oauth2/callback"
|
||||
)({
|
||||
component: HerokuOAuthCallbackPage,
|
||||
validateSearch: zodValidator(HerokuOAuthCallbackPageQueryParamsSchema)
|
||||
});
|
@ -0,0 +1,95 @@
|
||||
import { useState } from "react";
|
||||
import { useNavigate } from "@tanstack/react-router";
|
||||
|
||||
import { Button, Card, CardTitle, FormControl, Input } from "@app/components/v2";
|
||||
import { useWorkspace } from "@app/context";
|
||||
import { useSaveIntegrationAccessToken } from "@app/hooks/api";
|
||||
|
||||
export const LaravelForgeAuthorizePage = () => {
|
||||
const navigate = useNavigate();
|
||||
const { mutateAsync } = useSaveIntegrationAccessToken();
|
||||
const { currentWorkspace } = useWorkspace();
|
||||
const [apiKey, setApiKey] = useState("");
|
||||
const [apiKeyErrorText, setApiKeyErrorText] = useState("");
|
||||
const [serverId, setServerId] = useState("");
|
||||
const [serverIdErrorText, setServerIdErrorText] = useState("");
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
|
||||
const handleButtonClick = async () => {
|
||||
try {
|
||||
setApiKeyErrorText("");
|
||||
setServerIdErrorText("");
|
||||
|
||||
if (apiKey.length === 0) {
|
||||
setApiKeyErrorText("Access Token cannot be blank");
|
||||
return;
|
||||
}
|
||||
|
||||
if (serverId.length === 0) {
|
||||
setServerIdErrorText("Server Id cannot be blank");
|
||||
return;
|
||||
}
|
||||
|
||||
setIsLoading(true);
|
||||
|
||||
const integrationAuth = await mutateAsync({
|
||||
workspaceId: currentWorkspace.id,
|
||||
integration: "laravel-forge",
|
||||
accessId: serverId,
|
||||
accessToken: apiKey
|
||||
});
|
||||
|
||||
setIsLoading(false);
|
||||
|
||||
navigate({
|
||||
to: "/secret-manager/$projectId/integrations/laravel-forge/create",
|
||||
params: {
|
||||
projectId: currentWorkspace.id
|
||||
},
|
||||
search: {
|
||||
integrationAuthId: integrationAuth.id
|
||||
}
|
||||
});
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex h-full w-full items-center justify-center">
|
||||
<Card className="max-w-md rounded-md p-8">
|
||||
<CardTitle className="text-center">Laravel Forge Integration</CardTitle>
|
||||
<FormControl
|
||||
label="Laravel Forge Access Token"
|
||||
errorText={apiKeyErrorText}
|
||||
isError={apiKeyErrorText !== ""}
|
||||
>
|
||||
<Input
|
||||
placeholder="Access Token"
|
||||
value={apiKey}
|
||||
onChange={(e) => setApiKey(e.target.value)}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormControl
|
||||
label="Laravel Forge Server ID"
|
||||
errorText={serverIdErrorText}
|
||||
isError={serverIdErrorText !== ""}
|
||||
>
|
||||
<Input
|
||||
placeholder="123456"
|
||||
value={serverId}
|
||||
onChange={(e) => setServerId(e.target.value)}
|
||||
/>
|
||||
</FormControl>
|
||||
<Button
|
||||
onClick={handleButtonClick}
|
||||
color="mineshaft"
|
||||
className="mt-4"
|
||||
isLoading={isLoading}
|
||||
>
|
||||
Connect to Laravel Forge
|
||||
</Button>
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
};
|
@ -0,0 +1,9 @@
|
||||
import { createFileRoute } from "@tanstack/react-router";
|
||||
|
||||
import { LaravelForgeAuthorizePage } from "./LaravelForgeAuthorizePage";
|
||||
|
||||
export const Route = createFileRoute(
|
||||
"/_authenticate/_inject-org-details/secret-manager/$projectId/_secret-manager-layout/integrations/laravel-forge/authorize"
|
||||
)({
|
||||
component: LaravelForgeAuthorizePage
|
||||
});
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user