feat(integration): add integration with BitBucket

This commit is contained in:
Kee, Zhen Wei
2023-07-16 21:40:33 +08:00
parent 9b96daa185
commit df7ad9e645
26 changed files with 794 additions and 36 deletions

View File

@@ -47,11 +47,13 @@ CLIENT_ID_VERCEL=
CLIENT_ID_NETLIFY=
CLIENT_ID_GITHUB=
CLIENT_ID_GITLAB=
CLIENT_ID_BITBUCKET=
CLIENT_SECRET_HEROKU=
CLIENT_SECRET_VERCEL=
CLIENT_SECRET_NETLIFY=
CLIENT_SECRET_GITHUB=
CLIENT_SECRET_GITLAB=
CLIENT_SECRET_BITBUCKET=
CLIENT_SLUG_VERCEL=
# Sentry (optional) for monitoring errors

View File

@@ -37,6 +37,7 @@ export const getClientIdNetlify = async () => (await client.getSecret("CLIENT_ID
export const getClientIdGitHub = async () => (await client.getSecret("CLIENT_ID_GITHUB")).secretValue;
export const getClientIdGitLab = async () => (await client.getSecret("CLIENT_ID_GITLAB")).secretValue;
export const getClientIdGoogle = async () => (await client.getSecret("CLIENT_ID_GOOGLE")).secretValue;
export const getClientIdBitBucket = async () => (await client.getSecret("CLIENT_ID_BITBUCKET")).secretValue;
export const getClientSecretAzure = async () => (await client.getSecret("CLIENT_SECRET_AZURE")).secretValue;
export const getClientSecretHeroku = async () => (await client.getSecret("CLIENT_SECRET_HEROKU")).secretValue;
export const getClientSecretVercel = async () => (await client.getSecret("CLIENT_SECRET_VERCEL")).secretValue;
@@ -44,6 +45,7 @@ export const getClientSecretNetlify = async () => (await client.getSecret("CLIEN
export const getClientSecretGitHub = async () => (await client.getSecret("CLIENT_SECRET_GITHUB")).secretValue;
export const getClientSecretGitLab = async () => (await client.getSecret("CLIENT_SECRET_GITLAB")).secretValue;
export const getClientSecretGoogle = async () => (await client.getSecret("CLIENT_SECRET_GOOGLE")).secretValue;
export const getClientSecretBitBucket = async () => (await client.getSecret("CLIENT_SECRET_BITBUCKET")).secretValue;
export const getClientSlugVercel = async () => (await client.getSecret("CLIENT_SLUG_VERCEL")).secretValue;
export const getPostHogHost = async () => (await client.getSecret("POSTHOG_HOST")).secretValue || "https://app.posthog.com";
export const getPostHogProjectApiKey = async () => (await client.getSecret("POSTHOG_PROJECT_API_KEY")).secretValue || "phc_nSin8j5q2zdhpFDI1ETmFNUIuTG4DwKVyIigrY10XiE";

View File

@@ -7,6 +7,7 @@ import { IntegrationService } from "../../services";
import {
ALGORITHM_AES_256_GCM,
ENCODING_SCHEME_UTF8,
INTEGRATION_BITBUCKET_API_URL,
INTEGRATION_RAILWAY_API_URL,
INTEGRATION_SET,
INTEGRATION_VERCEL_API_URL,
@@ -141,12 +142,14 @@ export const saveIntegrationAccessToken = async (req: Request, res: Response) =>
*/
export const getIntegrationAuthApps = async (req: Request, res: Response) => {
const teamId = req.query.teamId as string;
const workspaceSlug = req.query.workspaceSlug as string;
const apps = await getApps({
integrationAuth: req.integrationAuth,
accessToken: req.accessToken,
accessId: req.accessId,
...(teamId && { teamId })
...(teamId && { teamId }),
...(workspaceSlug && { workspaceSlug })
});
return res.status(200).send({
@@ -382,6 +385,66 @@ export const getIntegrationAuthRailwayServices = async (req: Request, res: Respo
});
};
/**
* Return list of workspaces allowed for integration with integration authorization id [integrationAuthId]
* @param req
* @param res
* @returns
*/
export const getIntegrationAuthBitBucketWorkspaces = async (req: Request, res: Response) => {
interface WorkspaceResponse {
size: number;
page: number;
pageLen: number;
next: string;
previous: string;
values: Array<Workspace>;
}
interface Workspace {
type: string;
uuid: string;
name: string;
slug: string;
is_private: boolean;
created_on: string;
updated_on: string;
}
const workspaces: Workspace[] = [];
let hasNextPage = true;
let workspaceUrl = `${INTEGRATION_BITBUCKET_API_URL}/2.0/workspaces`
while (hasNextPage) {
const { data }: { data: WorkspaceResponse } = await standardRequest.get(
workspaceUrl,
{
headers: {
Authorization: `Bearer ${req.accessToken}`,
"Accept-Encoding": "application/json"
}
}
);
if (data?.values.length > 0) {
data.values.forEach((workspace) => {
workspaces.push(workspace)
})
}
if (data.next) {
workspaceUrl = data.next
} else {
hasNextPage = false
}
}
return res.status(200).send({
workspaces
});
};
/**
* Delete integration authorization with id [integrationAuthId]
* @param req

View File

@@ -5,6 +5,8 @@ import {
INTEGRATION_AWS_PARAMETER_STORE,
INTEGRATION_AWS_SECRET_MANAGER,
INTEGRATION_AZURE_KEY_VAULT,
INTEGRATION_BITBUCKET,
INTEGRATION_BITBUCKET_API_URL,
INTEGRATION_CHECKLY,
INTEGRATION_CHECKLY_API_URL,
INTEGRATION_CIRCLECI,
@@ -54,11 +56,13 @@ const getApps = async ({
accessToken,
accessId,
teamId,
workspaceSlug,
}: {
integrationAuth: IIntegrationAuth;
accessToken: string;
accessId?: string;
teamId?: string;
workspaceSlug?: string;
}) => {
let apps: App[] = [];
switch (integrationAuth.integration) {
@@ -145,6 +149,11 @@ const getApps = async ({
accountId: accessId
})
break;
case INTEGRATION_BITBUCKET:
apps = await getAppsBitBucket({
accessToken,
workspaceSlug
})
}
return apps;
@@ -721,4 +730,78 @@ const getAppsCloudflarePages = async ({
return apps;
}
/**
* Return list of repositories for the BitBucket integration based on provided BitBucket workspace
* @param {Object} obj
* @param {String} obj.accessToken - access token for BitBucket API
* @param {String} obj.workspaceSlug - Workspace identifier for fetching BitBucket repositories
* @returns {Object[]} apps - BitBucket repositories
* @returns {String} apps.name - name of BitBucket repository
*/
const getAppsBitBucket = async ({
accessToken,
workspaceSlug,
}: {
accessToken: string;
workspaceSlug?: string;
}) => {
interface RepositoriesResponse {
size: number;
page: number;
pageLen: number;
next: string;
previous: string;
values: Array<Repository>;
}
interface Repository {
type: string;
uuid: string;
name: string;
is_private: boolean;
created_on: string;
updated_on: string;
}
if (!workspaceSlug) {
return []
}
const repositories: Repository[] = [];
let hasNextPage = true;
let repositoriesUrl = `${INTEGRATION_BITBUCKET_API_URL}/2.0/repositories/${workspaceSlug}`
while (hasNextPage) {
const { data }: { data: RepositoriesResponse } = await standardRequest.get(
repositoriesUrl,
{
headers: {
Authorization: `Bearer ${accessToken}`,
"Accept": "application/json",
},
}
);
if (data?.values.length > 0) {
data.values.forEach((repository) => {
repositories.push(repository)
})
}
if (data.next) {
repositoriesUrl = data.next
} else {
hasNextPage = false
}
}
const apps = repositories.map((repository) => {
return {
name: repository.name,
appId: repository.uuid,
};
});
return apps;
}
export { getApps };

View File

@@ -2,6 +2,8 @@ import { standardRequest } from "../config/request";
import {
INTEGRATION_AZURE_KEY_VAULT,
INTEGRATION_AZURE_TOKEN_URL,
INTEGRATION_BITBUCKET,
INTEGRATION_BITBUCKET_TOKEN_URL,
INTEGRATION_GITHUB,
INTEGRATION_GITHUB_TOKEN_URL,
INTEGRATION_GITLAB,
@@ -15,11 +17,13 @@ import {
} from "../variables";
import {
getClientIdAzure,
getClientIdBitBucket,
getClientIdGitHub,
getClientIdGitLab,
getClientIdNetlify,
getClientIdVercel,
getClientSecretAzure,
getClientSecretBitBucket,
getClientSecretGitHub,
getClientSecretGitLab,
getClientSecretHeroku,
@@ -78,6 +82,15 @@ interface ExchangeCodeGitlabResponse {
created_at: number;
}
interface ExchangeCodeBitBucketResponse {
access_token: string;
token_type: string;
expires_in: number;
refresh_token: string;
scopes: string;
state: string;
}
/**
* Return [accessToken], [accessExpiresAt], and [refreshToken] for OAuth2
* code-token exchange for integration named [integration]
@@ -129,6 +142,11 @@ const exchangeCode = async ({
obj = await exchangeCodeGitlab({
code,
});
break;
case INTEGRATION_BITBUCKET:
obj = await exchangeCodeBitBucket({
code,
})
}
return obj;
@@ -347,4 +365,43 @@ const exchangeCodeGitlab = async ({ code }: { code: string }) => {
};
};
/**
* Return [accessToken], [accessExpiresAt], and [refreshToken] for BitBucket
* code-token exchange
* @param {Object} obj1
* @param {Object} obj1.code - code for code-token exchange
* @returns {Object} obj2
* @returns {String} obj2.accessToken - access token for BitBucket API
* @returns {String} obj2.refreshToken - refresh token for BitBucket API
* @returns {Date} obj2.accessExpiresAt - date of expiration for access token
*/
const exchangeCodeBitBucket = async ({ code }: { code: string }) => {
const accessExpiresAt = new Date();
const res: ExchangeCodeBitBucketResponse = (
await standardRequest.post(
INTEGRATION_BITBUCKET_TOKEN_URL,
new URLSearchParams({
grant_type: "authorization_code",
code: code,
client_id: await getClientIdBitBucket(),
client_secret: await getClientSecretBitBucket(),
redirect_uri: `${await getSiteURL()}/integrations/bitbucket/oauth2/callback`,
} as any),
{
headers: {
"Accept-Encoding": "application/json",
},
}
)
).data;
accessExpiresAt.setSeconds(accessExpiresAt.getSeconds() + res.expires_in);
return {
accessToken: res.access_token,
refreshToken: res.refresh_token,
accessExpiresAt,
};
};
export { exchangeCode };

View File

@@ -2,6 +2,8 @@ import { standardRequest } from "../config/request";
import { IIntegrationAuth } from "../models";
import {
INTEGRATION_AZURE_KEY_VAULT,
INTEGRATION_BITBUCKET,
INTEGRATION_BITBUCKET_TOKEN_URL,
INTEGRATION_GITLAB,
INTEGRATION_HEROKU,
} from "../variables";
@@ -13,8 +15,10 @@ import {
import { IntegrationService } from "../services";
import {
getClientIdAzure,
getClientIdBitBucket,
getClientIdGitLab,
getClientSecretAzure,
getClientSecretBitBucket,
getClientSecretGitLab,
getClientSecretHeroku,
getSiteURL,
@@ -46,6 +50,15 @@ interface RefreshTokenGitLabResponse {
created_at: number;
}
interface RefreshTokenBitBucketResponse {
access_token: string;
token_type: string;
expires_in: number;
refresh_token: string;
scopes: string;
state: string;
}
/**
* Return new access token by exchanging refresh token [refreshToken] for integration
* named [integration]
@@ -83,6 +96,11 @@ const exchangeRefresh = async ({
refreshToken,
});
break;
case INTEGRATION_BITBUCKET:
tokenDetails = await exchangeRefreshBitBucket({
refreshToken,
});
break;
default:
throw new Error("Failed to exchange token for incompatible integration");
}
@@ -218,4 +236,46 @@ const exchangeRefreshGitLab = async ({
};
};
/**
* Return new access token by exchanging refresh token [refreshToken] for the
* BitBucket integration
* @param {Object} obj
* @param {String} obj.refreshToken - refresh token to use to get new access token for BitBucket
* @returns
*/
const exchangeRefreshBitBucket = async ({
refreshToken,
}: {
refreshToken: string;
}) => {
const accessExpiresAt = new Date();
const {
data,
}: {
data: RefreshTokenBitBucketResponse;
} = await standardRequest.post(
INTEGRATION_BITBUCKET_TOKEN_URL,
new URLSearchParams({
grant_type: "refresh_token",
refresh_token: refreshToken,
client_id: await getClientIdBitBucket(),
client_secret: await getClientSecretBitBucket(),
redirect_uri: `${await getSiteURL()}/integrations/bitbucket/oauth2/callback`,
} as any),
{
headers: {
"Accept-Encoding": "application/json",
},
}
);
accessExpiresAt.setSeconds(accessExpiresAt.getSeconds() + data.expires_in);
return {
accessToken: data.access_token,
refreshToken: data.refresh_token,
accessExpiresAt,
};
};
export { exchangeRefresh };

View File

@@ -14,6 +14,8 @@ import {
INTEGRATION_AWS_PARAMETER_STORE,
INTEGRATION_AWS_SECRET_MANAGER,
INTEGRATION_AZURE_KEY_VAULT,
INTEGRATION_BITBUCKET,
INTEGRATION_BITBUCKET_API_URL,
INTEGRATION_CHECKLY,
INTEGRATION_CHECKLY_API_URL,
INTEGRATION_CIRCLECI,
@@ -202,7 +204,14 @@ const syncSecrets = async ({
accessToken
});
break;
}
case INTEGRATION_BITBUCKET:
await syncSecretsBitBucket({
integration,
secrets,
accessToken,
});
break;
}
};
/**
@@ -1937,4 +1946,122 @@ const syncSecretsCloudflarePages = async ({
);
}
/**
* Sync/push [secrets] to BitBucket repo with name [integration.app]
* @param {Object} obj
* @param {IIntegration} obj.integration - integration details
* @param {IIntegrationAuth} obj.integrationAuth - integration auth details
* @param {Object} obj.secrets - secrets to push to integration (object where keys are secret keys and values are secret values)
* @param {String} obj.accessToken - access token for BitBucket integration
*/
const syncSecretsBitBucket = async ({
integration,
secrets,
accessToken,
}: {
integration: IIntegration;
secrets: any;
accessToken: string;
}) => {
interface VariablesResponse {
size: number;
page: number;
pageLen: number;
next: string;
previous: string;
values: Array<Variable>;
}
interface Variable {
type: string;
uuid: string;
key: string;
value: string;
secured: boolean;
}
const existingSecrets: Variable[] = [];
const workspaceSlug = integration.targetEnvironmentId
const repoSlug = integration.appId
let hasNextPage = true;
let variablesUrl = `${INTEGRATION_BITBUCKET_API_URL}/2.0/repositories/${workspaceSlug}/${repoSlug}/pipelines_config/variables`
// Fetch all repository variables
while (hasNextPage) {
const { data }: { data: VariablesResponse } = await standardRequest.get(
variablesUrl,
{
headers: {
Authorization: `Bearer ${accessToken}`,
"Accept": "application/json",
},
}
);
if (data?.values.length > 0) {
data.values.forEach((variable) => {
existingSecrets.push(variable)
})
}
if (data.next) {
variablesUrl = data.next
} else {
hasNextPage = false
}
}
Object.keys(secrets).forEach(async (key) => {
const existingSecret = existingSecrets.find((secret) => secret.key.toUpperCase() === key.toUpperCase());
if (existingSecret) {
// Update existing secrets
await standardRequest.put(
`${variablesUrl}/${existingSecret.uuid}`,
{
key,
value: secrets[key],
secured: true
},
{
headers: {
Authorization: `Bearer ${accessToken}`,
"Accept": "application/json",
},
}
);
} else {
// Create new secrets
await standardRequest.post(
variablesUrl,
{
key,
value: secrets[key],
secured: true
},
{
headers: {
Authorization: `Bearer ${accessToken}`,
"Accept": "application/json",
},
}
);
}
})
// Delete secrets
existingSecrets.forEach(async (existingSecret) => {
if (!(existingSecret.key in secrets) && existingSecret.secured) {
await standardRequest.delete(
`${variablesUrl}/${existingSecret.uuid}`,
{
headers: {
Authorization: `Bearer ${accessToken}`,
"Accept": "application/json",
},
}
);
}
})
}
export { syncSecrets };

View File

@@ -3,6 +3,7 @@ import {
INTEGRATION_AWS_PARAMETER_STORE,
INTEGRATION_AWS_SECRET_MANAGER,
INTEGRATION_AZURE_KEY_VAULT,
INTEGRATION_BITBUCKET,
INTEGRATION_CHECKLY,
INTEGRATION_CIRCLECI,
INTEGRATION_CLOUDFLARE_PAGES,
@@ -54,7 +55,8 @@ export interface IIntegration {
| "supabase"
| "checkly"
| "hashicorp-vault"
| "cloudflare-pages";
| "cloudflare-pages"
| "bitbucket";
integrationAuth: Types.ObjectId;
}
@@ -144,6 +146,7 @@ const integrationSchema = new Schema<IIntegration>(
INTEGRATION_CHECKLY,
INTEGRATION_HASHICORP_VAULT,
INTEGRATION_CLOUDFLARE_PAGES,
INTEGRATION_BITBUCKET,
],
required: true,
},

View File

@@ -6,6 +6,7 @@ import {
INTEGRATION_AWS_PARAMETER_STORE,
INTEGRATION_AWS_SECRET_MANAGER,
INTEGRATION_AZURE_KEY_VAULT,
INTEGRATION_BITBUCKET,
INTEGRATION_CIRCLECI,
INTEGRATION_CLOUDFLARE_PAGES,
INTEGRATION_FLYIO,
@@ -25,7 +26,25 @@ import {
export interface IIntegrationAuth extends Document {
_id: Types.ObjectId;
workspace: Types.ObjectId;
integration: "heroku" | "vercel" | "netlify" | "github" | "gitlab" | "render" | "railway" | "flyio" | "azure-key-vault" | "laravel-forge" | "circleci" | "travisci" | "supabase" | "aws-parameter-store" | "aws-secret-manager" | "checkly" | "cloudflare-pages";
integration:
| "heroku"
| "vercel"
| "netlify"
| "github"
| "gitlab"
| "render"
| "railway"
| "flyio"
| "azure-key-vault"
| "laravel-forge"
| "circleci"
| "travisci"
| "supabase"
| "aws-parameter-store"
| "aws-secret-manager"
| "checkly"
| "cloudflare-pages"
| "bitbucket";
teamId: string;
accountId: string;
url: string;
@@ -71,6 +90,7 @@ const integrationAuthSchema = new Schema<IIntegrationAuth>(
INTEGRATION_SUPABASE,
INTEGRATION_HASHICORP_VAULT,
INTEGRATION_CLOUDFLARE_PAGES,
INTEGRATION_BITBUCKET
],
required: true,
},

View File

@@ -81,6 +81,7 @@ router.get(
}),
param("integrationAuthId"),
query("teamId"),
query("workspaceSlug"),
validateRequest,
integrationAuthController.getIntegrationAuthApps
);
@@ -141,6 +142,19 @@ router.get(
integrationAuthController.getIntegrationAuthRailwayServices
);
router.get(
"/:integrationAuthId/bitbucket/workspaces",
requireAuth({
acceptedAuthModes: [AUTH_MODE_JWT],
}),
requireIntegrationAuthorizationAuth({
acceptedRoles: [ADMIN, MEMBER],
}),
param("integrationAuthId"),
validateRequest,
integrationAuthController.getIntegrationAuthBitBucketWorkspaces
);
router.delete(
"/:integrationAuthId",
requireAuth({

View File

@@ -1,5 +1,6 @@
import {
getClientIdAzure,
getClientIdBitBucket,
getClientIdGitHub,
getClientIdGitLab,
getClientIdHeroku,
@@ -26,6 +27,7 @@ export const INTEGRATION_SUPABASE = "supabase";
export const INTEGRATION_CHECKLY = "checkly";
export const INTEGRATION_HASHICORP_VAULT = "hashicorp-vault";
export const INTEGRATION_CLOUDFLARE_PAGES = "cloudflare-pages";
export const INTEGRATION_BITBUCKET = "bitbucket";
export const INTEGRATION_SET = new Set([
INTEGRATION_AZURE_KEY_VAULT,
INTEGRATION_HEROKU,
@@ -41,7 +43,8 @@ export const INTEGRATION_SET = new Set([
INTEGRATION_SUPABASE,
INTEGRATION_CHECKLY,
INTEGRATION_HASHICORP_VAULT,
INTEGRATION_CLOUDFLARE_PAGES
INTEGRATION_CLOUDFLARE_PAGES,
INTEGRATION_BITBUCKET
]);
// integration types
@@ -56,6 +59,7 @@ export const INTEGRATION_NETLIFY_TOKEN_URL = "https://api.netlify.com/oauth/toke
export const INTEGRATION_GITHUB_TOKEN_URL =
"https://github.com/login/oauth/access_token";
export const INTEGRATION_GITLAB_TOKEN_URL = "https://gitlab.com/oauth/token";
export const INTEGRATION_BITBUCKET_TOKEN_URL = "https://bitbucket.org/site/oauth2/access_token"
// integration apps endpoints
export const INTEGRATION_HEROKU_API_URL = "https://api.heroku.com";
@@ -71,6 +75,7 @@ export const INTEGRATION_SUPABASE_API_URL = "https://api.supabase.com";
export const INTEGRATION_LARAVELFORGE_API_URL = "https://forge.laravel.com";
export const INTEGRATION_CHECKLY_API_URL = "https://api.checklyhq.com";
export const INTEGRATION_CLOUDFLARE_PAGES_API_URL = "https://api.cloudflare.com";
export const INTEGRATION_BITBUCKET_API_URL = "https://api.bitbucket.org/";
export const getIntegrationOptions = async () => {
const INTEGRATION_OPTIONS = [
@@ -245,6 +250,15 @@ export const getIntegrationOptions = async () => {
type: "pat",
clientId: "",
docsLink: ""
},
{
name: "BitBucket",
slug: "bitbucket",
image: "BitBucket.png",
isAvailable: true,
type: "oauth",
clientId: await getClientIdBitBucket(),
docsLink: ""
}
]

Binary file not shown.

After

Width:  |  Height:  |  Size: 32 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 184 KiB

View File

@@ -0,0 +1,29 @@
---
title: "BitBucket"
description: "How to sync secrets from Infisical to BitBucket"
---
Prerequisites:
- Set up and add envars to [Infisical Cloud](https://app.infisical.com)
## Navigate to your project's integrations tab
![integrations](../../images/integrations.png)
## Authorize Infisical for BitBucket
Press on the BitBucket tile and grant Infisical access to your BitBucket account.
![integrations bitbucket authorization](../../images/integrations-bitbucket-auth.png)
<Info>
If this is your project's first cloud integration, then you'll have to grant Infisical access to your project's environment variables.
Although this step breaks E2EE, it's necessary for Infisical to sync the environment variables to the cloud platform.
</Info>
## Start integration
Select which Infisical environment secrets you want to sync to which BitBucket repo and press start integration to start syncing secrets to the repo.
![integrations bitbucket](../../images/integrations-bitbucket.png)

View File

@@ -28,6 +28,7 @@ Missing an integration? [Throw in a request](https://github.com/Infisical/infisi
| [AWS Parameter Store](/integrations/cloud/aws-parameter-store) | Cloud | Available |
| [AWS Secret Manager](/integrations/cloud/aws-secret-manager) | Cloud | Available |
| [Azure Key Vault](/integrations/cloud/azure-key-vault) | Cloud | Available |
| [BitBucket](/integrations/cicd/bitbucket) | CI/CD | Available |
| [GitHub Actions](/integrations/cicd/githubactions) | CI/CD | Available |
| [GitLab](/integrations/cicd/gitlab) | CI/CD | Available |
| [CircleCI](/integrations/cicd/circleci) | CI/CD | Available |

View File

@@ -220,7 +220,8 @@
"integrations/cicd/githubactions",
"integrations/cicd/gitlab",
"integrations/cicd/circleci",
"integrations/cicd/travisci"
"integrations/cicd/travisci",
"integrations/cicd/bitbucket"
]
},
{

View File

@@ -107,6 +107,14 @@ Other environment variables are listed below to increase the functionality of yo
<ParamField query="CLIENT_SLUG_VERCEL" type="string" default="none" optional>
OAuth2 slug for Vercel integration
</ParamField>
<ParamField query="CLIENT_ID_BITBUCKET" type="string" default="none" optional>
OAuth2 client ID for BitBucket integration
</ParamField>
<ParamField query="CLIENT_SECRET_BITBUCKET" type="string" default="none" optional>
OAuth2 client secret for BitBucket integration
</ParamField>
</Tab>
<Tab title="Auth Integrations">
To integrate with external auth providers, provide value for the related keys

View File

@@ -3,25 +3,26 @@ interface Mapping {
}
const integrationSlugNameMapping: Mapping = {
'azure-key-vault': 'Azure Key Vault',
'aws-parameter-store': 'AWS Parameter Store',
'aws-secret-manager': 'AWS Secret Manager',
'heroku': 'Heroku',
'vercel': 'Vercel',
'netlify': 'Netlify',
'github': 'GitHub',
'gitlab': 'GitLab',
'render': 'Render',
'laravel-forge': "Laravel Forge",
'railway': 'Railway',
'flyio': 'Fly.io',
'circleci': 'CircleCI',
'travisci': 'TravisCI',
'supabase': 'Supabase',
'checkly': 'Checkly',
'hashicorp-vault': 'Vault',
'cloudflare-pages': 'Cloudflare Pages'
}
"azure-key-vault": "Azure Key Vault",
"aws-parameter-store": "AWS Parameter Store",
"aws-secret-manager": "AWS Secret Manager",
heroku: "Heroku",
vercel: "Vercel",
netlify: "Netlify",
github: "GitHub",
gitlab: "GitLab",
render: "Render",
"laravel-forge": "Laravel Forge",
railway: "Railway",
flyio: "Fly.io",
circleci: "CircleCI",
travisci: "TravisCI",
supabase: "Supabase",
checkly: "Checkly",
"hashicorp-vault": "Vault",
"cloudflare-pages": "Cloudflare Pages",
bitbucket: "BitBucket"
};
const envMapping: Mapping = {
Development: "dev",

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.4 KiB

View File

@@ -1,9 +1,10 @@
export {
useDeleteIntegrationAuth,
useGetIntegrationAuthApps,
useGetIntegrationAuthBitBucketWorkspaces,
useGetIntegrationAuthById,
useGetIntegrationAuthRailwayEnvironments,
useGetIntegrationAuthRailwayServices,
useGetIntegrationAuthTeams,
useGetIntegrationAuthVercelBranches
useGetIntegrationAuthVercelBranches,
} from "./queries";

View File

@@ -3,13 +3,13 @@ import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
import { apiRequest } from "@app/config/request";
import { workspaceKeys } from "../workspace/queries";
import { App, Environment, IntegrationAuth, Service, Team } from "./types";
import { App, BitBucketWorkspace, Environment, IntegrationAuth, Service, Team } from "./types";
const integrationAuthKeys = {
getIntegrationAuthById: (integrationAuthId: string) =>
[{ integrationAuthId }, "integrationAuth"] as const,
getIntegrationAuthApps: (integrationAuthId: string, teamId?: string) =>
[{ integrationAuthId, teamId }, "integrationAuthApps"] as const,
getIntegrationAuthApps: (integrationAuthId: string, teamId?: string, workspaceSlug?: string) =>
[{ integrationAuthId, teamId, workspaceSlug }, "integrationAuthApps"] as const,
getIntegrationAuthTeams: (integrationAuthId: string) =>
[{ integrationAuthId }, "integrationAuthTeams"] as const,
getIntegrationAuthVercelBranches: ({
@@ -32,7 +32,9 @@ const integrationAuthKeys = {
}: {
integrationAuthId: string;
appId: string;
}) => [{ integrationAuthId, appId }, "integrationAuthRailwayServices"] as const
}) => [{ integrationAuthId, appId }, "integrationAuthRailwayServices"] as const,
getIntegrationAuthBitBucketWorkspaces: (integrationAuthId: string) =>
[{ integrationAuthId }, "integrationAuthTeams"] as const,
};
const fetchIntegrationAuthById = async (integrationAuthId: string) => {
@@ -44,12 +46,22 @@ const fetchIntegrationAuthById = async (integrationAuthId: string) => {
const fetchIntegrationAuthApps = async ({
integrationAuthId,
teamId
teamId,
workspaceSlug
}: {
integrationAuthId: string;
teamId?: string;
workspaceSlug?: string;
}) => {
const searchParams = new URLSearchParams(teamId ? { teamId } : undefined);
const params: Record<string, string> = {}
if (teamId) {
params.teamId = teamId
}
if (workspaceSlug) {
params.workspaceSlug = workspaceSlug
}
const searchParams = new URLSearchParams(params);
const { data } = await apiRequest.get<{ apps: App[] }>(
`/api/v1/integration-auth/${integrationAuthId}/apps`,
{ params: searchParams }
@@ -127,6 +139,13 @@ const fetchIntegrationAuthRailwayServices = async ({
return services;
};
const fetchIntegrationAuthBitBucketWorkspaces = async (integrationAuthId: string) => {
const { data: { workspaces } } = await apiRequest.get<{ workspaces: BitBucketWorkspace[] }>(
`/api/v1/integration-auth/${integrationAuthId}/bitbucket/workspaces`
);
return workspaces;
};
export const useGetIntegrationAuthById = (integrationAuthId: string) => {
return useQuery({
queryKey: integrationAuthKeys.getIntegrationAuthById(integrationAuthId),
@@ -137,17 +156,20 @@ export const useGetIntegrationAuthById = (integrationAuthId: string) => {
export const useGetIntegrationAuthApps = ({
integrationAuthId,
teamId
teamId,
workspaceSlug,
}: {
integrationAuthId: string;
teamId?: string;
workspaceSlug?: string;
}) => {
return useQuery({
queryKey: integrationAuthKeys.getIntegrationAuthApps(integrationAuthId, teamId),
queryKey: integrationAuthKeys.getIntegrationAuthApps(integrationAuthId, teamId, workspaceSlug),
queryFn: () =>
fetchIntegrationAuthApps({
integrationAuthId,
teamId
teamId,
workspaceSlug
}),
enabled: true
});
@@ -224,6 +246,14 @@ export const useGetIntegrationAuthRailwayServices = ({
});
};
export const useGetIntegrationAuthBitBucketWorkspaces = (integrationAuthId: string) => {
return useQuery({
queryKey: integrationAuthKeys.getIntegrationAuthBitBucketWorkspaces(integrationAuthId),
queryFn: () => fetchIntegrationAuthBitBucketWorkspaces(integrationAuthId),
enabled: true
});
};
export const useDeleteIntegrationAuth = () => {
const queryClient = useQueryClient();

View File

@@ -29,3 +29,9 @@ export type Service = {
name: string;
serviceId: string;
};
export type BitBucketWorkspace = {
uuid: string;
name: string;
slug: string;
}

View File

@@ -0,0 +1,198 @@
import { useEffect, useState } from "react";
import { useRouter } from "next/router";
import queryString from "query-string";
import {
Button,
Card,
CardTitle,
FormControl,
Input,
Select,
SelectItem
} from "../../../components/v2";
import {
useGetIntegrationAuthApps,
useGetIntegrationAuthBitBucketWorkspaces,
useGetIntegrationAuthById,
} from "../../../hooks/api/integrationAuth";
import { useGetWorkspaceById } from "../../../hooks/api/workspace";
import createIntegration from "../../api/integrations/createIntegration";
export default function BitBucketCreateIntegrationPage() {
const router = useRouter();
const [targetAppId, setTargetAppId] = useState("");
const [targetEnvironmentId, setTargetEnvironmentId] = useState("");
const [selectedSourceEnvironment, setSelectedSourceEnvironment] = useState("");
const [secretPath, setSecretPath] = useState("/");
const [isLoading, setIsLoading] = useState(false);
const { integrationAuthId } = queryString.parse(router.asPath.split("?")[1]);
const { data: integrationAuth } = useGetIntegrationAuthById((integrationAuthId as string) ?? "");
const { data: workspace } = useGetWorkspaceById(localStorage.getItem("projectData.id") ?? "");
const { data: targetEnvironments } = useGetIntegrationAuthBitBucketWorkspaces((integrationAuthId as string) ?? "");
const { data: integrationAuthApps } = useGetIntegrationAuthApps({
integrationAuthId: (integrationAuthId as string) ?? "",
workspaceSlug: targetEnvironmentId
});
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]);
useEffect(() => {
if (targetEnvironments) {
if (targetEnvironments.length > 0) {
setTargetEnvironmentId(targetEnvironments[0].slug);
} else {
setTargetEnvironmentId("none");
}
}
}, [targetEnvironments]);
const handleButtonClick = async () => {
try {
setIsLoading(true);
if (!integrationAuth?._id) return;
const targetApp = integrationAuthApps?.find(
(integrationAuthApp) => integrationAuthApp.appId === targetAppId
);
const targetEnvironment = targetEnvironments?.find(
(environment) => environment.slug === targetEnvironmentId
);
if (!targetApp || !targetApp.appId || !targetEnvironment) return;
await createIntegration({
integrationAuthId: integrationAuth?._id,
isActive: true,
app: targetApp.name,
appId: targetApp.appId,
sourceEnvironment: selectedSourceEnvironment,
targetEnvironment: targetEnvironment.name,
targetEnvironmentId: targetEnvironment.slug,
targetService: null,
targetServiceId: null,
owner: null,
path: null,
region: null,
secretPath
});
setIsLoading(false);
router.push(`/integrations/${localStorage.getItem("projectData.id")}`);
} catch (err) {
console.error(err);
}
};
return integrationAuth &&
workspace &&
selectedSourceEnvironment &&
integrationAuthApps &&
targetEnvironments ? (
<div className="flex h-full w-full items-center justify-center">
<Card className="max-w-md rounded-md p-8">
<CardTitle className="text-center">BitBucket 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={`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="BitBucket Workspace">
<Select
value={targetEnvironmentId}
onValueChange={(val) => setTargetEnvironmentId(val)}
className="w-full border border-mineshaft-500"
isDisabled={targetEnvironments.length === 0}
>
{targetEnvironments.length > 0 ? (
targetEnvironments.map((targetEnvironment) => (
<SelectItem
value={targetEnvironment.slug as string}
key={`target-environment-${targetEnvironment.slug as string}`}
>
{targetEnvironment.name}
</SelectItem>
))
) : (
<SelectItem value="none" key="target-environment-none">
No workspaces found
</SelectItem>
)}
</Select>
</FormControl>
<FormControl label="BitBucket Repo">
<Select
value={targetAppId}
onValueChange={(val) => setTargetAppId(val)}
className="w-full border border-mineshaft-500"
>
{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 repositories found
</SelectItem>
)}
</Select>
</FormControl>
<Button
onClick={handleButtonClick}
color="mineshaft"
className="mt-4"
isLoading={isLoading}
isDisabled={integrationAuthApps.length === 0}
>
Create Integration
</Button>
</Card>
</div>
) : (
<div />
);
}
BitBucketCreateIntegrationPage.requireAuth = true;

View File

@@ -0,0 +1,34 @@
import { useEffect } from "react";
import { useRouter } from "next/router";
import queryString from "query-string";
import AuthorizeIntegration from "../../../api/integrations/authorizeIntegration";
export default function BitBucketOAuth2CallbackPage() {
const router = useRouter();
const { code, state } = queryString.parse(router.asPath.split("?")[1]);
useEffect(() => {
(async () => {
try {
// validate state
if (state !== localStorage.getItem("latestCSRFToken")) return;
localStorage.removeItem("latestCSRFToken");
const integrationAuth = await AuthorizeIntegration({
workspaceId: localStorage.getItem("projectData.id") as string,
code: code as string,
integration: "bitbucket"
});
router.push(`/integrations/bitbucket/create?integrationAuthId=${integrationAuth._id}`);
} catch (err) {
console.error(err);
}
})();
}, []);
return <div />;
}
BitBucketOAuth2CallbackPage.requireAuth = true;

View File

@@ -92,6 +92,9 @@ export const redirectForProviderAuth = (integrationOption: TCloudIntegration) =>
case "cloudflare-pages":
link = `${window.location.origin}/integrations/cloudflare-pages/authorize`;
break;
case "bitbucket":
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}`;
break;
default:
break;
}

View File

@@ -106,7 +106,8 @@ export const IntegrationsSection = ({
{(integration.integration === "vercel" ||
integration.integration === "netlify" ||
integration.integration === "railway" ||
integration.integration === "gitlab") && (
integration.integration === "gitlab" ||
integration.integration === "bitbucket") && (
<div className="ml-4 flex flex-col">
<FormLabel label="Target Environment" />
<div className="rounded-md border border-mineshaft-700 bg-mineshaft-900 px-3 py-2 font-inter text-sm text-bunker-200">