feat: changed backend-pg to backend

This commit is contained in:
Akhil Mohan
2024-01-27 19:14:33 +05:30
parent d13eafcef7
commit 66d258f02b
792 changed files with 40298 additions and 40607 deletions
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,10 @@
import { TDbClient } from "@app/db";
import { TableName } from "@app/db/schemas";
import { ormify } from "@app/lib/knex";
export type TIntegrationAuthDALFactory = ReturnType<typeof integrationAuthDALFactory>;
export const integrationAuthDALFactory = (db: TDbClient) => {
const integrationAuthOrm = ormify(db, TableName.IntegrationAuth);
return integrationAuthOrm;
};
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,139 @@
import { TProjectPermission } from "@app/lib/types";
export type TGetIntegrationAuthDTO = {
id: string;
} & Omit<TProjectPermission, "projectId">;
export type TOauthExchangeDTO = {
integration: string;
code: string;
url?: string;
} & TProjectPermission;
export type TSaveIntegrationAccessTokenDTO = {
integration: string;
accessId?: string;
accessToken?: string;
url?: string;
namespace?: string;
refreshToken?: string;
} & TProjectPermission;
export type TDeleteIntegrationAuthsDTO = TProjectPermission & {
integration: string;
projectId: string;
};
export type TIntegrationAuthAppsDTO = {
id: string;
teamId?: string;
workspaceSlug?: string;
} & Omit<TProjectPermission, "projectId">;
export type TIntegrationAuthTeamsDTO = {
id: string;
} & Omit<TProjectPermission, "projectId">;
export type TIntegrationAuthVercelBranchesDTO = {
id: string;
appId: string;
} & Omit<TProjectPermission, "projectId">;
export type TIntegrationAuthChecklyGroupsDTO = {
id: string;
accountId: string;
} & Omit<TProjectPermission, "projectId">;
export type TIntegrationAuthQoveryOrgsDTO = {
id: string;
} & Omit<TProjectPermission, "projectId">;
export type TIntegrationAuthQoveryProjectDTO = {
id: string;
orgId: string;
} & Omit<TProjectPermission, "projectId">;
export type TIntegrationAuthQoveryEnvironmentsDTO = {
id: string;
} & TProjectPermission;
export type TIntegrationAuthQoveryScopesDTO = {
id: string;
environmentId: string;
} & Omit<TProjectPermission, "projectId">;
export type TIntegrationAuthRailwayEnvDTO = {
id: string;
appId: string;
} & Omit<TProjectPermission, "projectId">;
export type TIntegrationAuthRailwayServicesDTO = {
id: string;
appId: string;
} & Omit<TProjectPermission, "projectId">;
export type TIntegrationAuthBitbucketWorkspaceDTO = {
id: string;
} & Omit<TProjectPermission, "projectId">;
export type TIntegrationAuthNorthflankSecretGroupDTO = {
id: string;
appId: string;
} & Omit<TProjectPermission, "projectId">;
export type TDeleteIntegrationAuthByIdDTO = {
id: string;
} & Omit<TProjectPermission, "projectId">;
export type TGetIntegrationAuthTeamCityBuildConfigDTO = {
id: string;
appId: string;
} & Omit<TProjectPermission, "projectId">;
export type TVercelBranches = {
ref: string;
lastCommit: string;
isProtected: boolean;
};
export type TChecklyGroups = {
id: number;
name: string;
};
export type TQoveryProjects = {
id: string;
name: string;
};
export type TQoveryEnvironments = {
id: string;
name: string;
};
export type TBitbucketWorkspace = {
type: string;
uuid: string;
name: string;
slug: string;
is_private: boolean;
created_on: string;
updated_on: string;
};
export type TNorthflankSecretGroup = {
id: string;
name: string;
description: string;
priority: number;
projectId: string;
};
export type TTeamCityBuildConfig = {
id: string;
name: string;
projectName: string;
projectId: string;
href: string;
webUrl: string;
};
@@ -0,0 +1,363 @@
import { getConfig } from "@app/lib/config/env";
export enum Integrations {
AZURE_KEY_VAULT = "azure-key-vault",
AWS_PARAMETER_STORE = "aws-parameter-store",
AWS_SECRET_MANAGER = "aws-secret-manager",
GCP_SECRET_MANAGER = "gcp-secret-manager",
HEROKU = "heroku",
VERCEL = "vercel",
NETLIFY = "netlify",
GITHUB = "github",
GITLAB = "gitlab",
RENDER = "render",
RAILWAY = "railway",
FLYIO = "flyio",
LARAVELFORGE = "laravel-forge",
CIRCLECI = "circleci",
TRAVISCI = "travisci",
TEAMCITY = "teamcity",
SUPABASE = "supabase",
CHECKLY = "checkly",
QOVERY = "qovery",
TERRAFORM_CLOUD = "terraform-cloud",
HASHICORP_VAULT = "hashicorp-vault",
CLOUDFLARE_PAGES = "cloudflare-pages",
CLOUDFLARE_WORKERS = "cloudflare-workers",
BITBUCKET = "bitbucket",
CODEFRESH = "codefresh",
WINDMILL = "windmill",
DIGITAL_OCEAN_APP_PLATFORM = "digital-ocean-app-platform",
CLOUD_66 = "cloud-66",
NORTHFLANK = "northflank",
HASURA_CLOUD = "hasura-cloud"
}
export enum IntegrationType {
OAUTH2 = "oauth2"
}
export enum IntegrationUrls {
// integration oauth endpoints
GCP_TOKEN_URL = "https://oauth2.googleapis.com/token",
AZURE_TOKEN_URL = "https://login.microsoftonline.com/common/oauth2/v2.0/token",
HEROKU_TOKEN_URL = "https://id.heroku.com/oauth/token",
VERCEL_TOKEN_URL = "https://api.vercel.com/v2/oauth/access_token",
NETLIFY_TOKEN_URL = "https://api.netlify.com/oauth/token",
GITHUB_TOKEN_URL = "https://github.com/login/oauth/access_token",
GITLAB_TOKEN_URL = "https://gitlab.com/oauth/token",
BITBUCKET_TOKEN_URL = "https://bitbucket.org/site/oauth2/access_token",
// integration apps endpoints
GCP_API_URL = "https://cloudresourcemanager.googleapis.com",
HEROKU_API_URL = "https://api.heroku.com",
GITLAB_URL = "https://gitlab.com",
GITLAB_API_URL = `${GITLAB_URL}/api`,
GITHUB_API_URL = "https://api.github.com",
VERCEL_API_URL = "https://api.vercel.com",
NETLIFY_API_URL = "https://api.netlify.com",
RENDER_API_URL = "https://api.render.com",
RAILWAY_API_URL = "https://backboard.railway.app/graphql/v2",
FLYIO_API_URL = "https://api.fly.io/graphql",
CIRCLECI_API_URL = "https://circleci.com/api",
TRAVISCI_API_URL = "https://api.travis-ci.com",
SUPABASE_API_URL = "https://api.supabase.com",
LARAVELFORGE_API_URL = "https://forge.laravel.com",
CHECKLY_API_URL = "https://api.checklyhq.com",
QOVERY_API_URL = "https://api.qovery.com",
TERRAFORM_CLOUD_API_URL = "https://app.terraform.io",
CLOUDFLARE_PAGES_API_URL = "https://api.cloudflare.com",
CLOUDFLARE_WORKERS_API_URL = "https://api.cloudflare.com",
BITBUCKET_API_URL = "https://api.bitbucket.org",
CODEFRESH_API_URL = "https://g.codefresh.io/api",
WINDMILL_API_URL = "https://app.windmill.dev/api",
DIGITAL_OCEAN_API_URL = "https://api.digitalocean.com",
CLOUD_66_API_URL = "https://app.cloud66.com/api",
NORTHFLANK_API_URL = "https://api.northflank.com",
HASURA_CLOUD_API_URL = "https://data.pro.hasura.io/v1/graphql",
GCP_SECRET_MANAGER_SERVICE_NAME = "secretmanager.googleapis.com",
GCP_SECRET_MANAGER_URL = `https://${GCP_SECRET_MANAGER_SERVICE_NAME}`,
GCP_SERVICE_USAGE_URL = "https://serviceusage.googleapis.com",
GCP_CLOUD_PLATFORM_SCOPE = "https://www.googleapis.com/auth/cloud-platform"
}
export const getIntegrationOptions = async () => {
const appCfg = getConfig();
const INTEGRATION_OPTIONS = [
{
name: "Heroku",
slug: "heroku",
image: "Heroku.png",
isAvailable: true,
type: "oauth",
clientId: appCfg.CLIENT_ID_HEROKU,
docsLink: ""
},
{
name: "Vercel",
slug: "vercel",
image: "Vercel.png",
isAvailable: true,
type: "oauth",
clientId: "",
clientSlug: appCfg.CLIENT_SLUG_VERCEL,
docsLink: ""
},
{
name: "Netlify",
slug: "netlify",
image: "Netlify.png",
isAvailable: true,
type: "oauth",
clientId: appCfg.CLIENT_ID_NETLIFY,
docsLink: ""
},
{
name: "GitHub",
slug: "github",
image: "GitHub.png",
isAvailable: true,
type: "oauth",
clientId: appCfg.CLIENT_ID_GITHUB,
docsLink: ""
},
{
name: "Render",
slug: "render",
image: "Render.png",
isAvailable: true,
type: "pat",
clientId: "",
docsLink: ""
},
{
name: "Railway",
slug: "railway",
image: "Railway.png",
isAvailable: true,
type: "pat",
clientId: "",
docsLink: ""
},
{
name: "Fly.io",
slug: "flyio",
image: "Flyio.svg",
isAvailable: true,
type: "pat",
clientId: "",
docsLink: ""
},
{
name: "AWS Parameter Store",
slug: "aws-parameter-store",
image: "Amazon Web Services.png",
isAvailable: true,
type: "custom",
clientId: "",
docsLink: ""
},
{
name: "Laravel Forge",
slug: "laravel-forge",
image: "Laravel Forge.png",
isAvailable: true,
type: "pat",
clientId: "",
docsLink: ""
},
{
name: "AWS Secrets Manager",
slug: "aws-secret-manager",
image: "Amazon Web Services.png",
isAvailable: true,
type: "custom",
clientId: "",
docsLink: ""
},
{
name: "Azure Key Vault",
slug: "azure-key-vault",
image: "Microsoft Azure.png",
isAvailable: true,
type: "oauth",
clientId: appCfg.CLIENT_ID_AZURE,
docsLink: ""
},
{
name: "Circle CI",
slug: "circleci",
image: "Circle CI.png",
isAvailable: true,
type: "pat",
clientId: "",
docsLink: ""
},
{
name: "GitLab",
slug: "gitlab",
image: "GitLab.png",
isAvailable: true,
type: "custom",
clientId: appCfg.CLIENT_ID_GITLAB,
docsLink: ""
},
{
name: "Terraform Cloud",
slug: "terraform-cloud",
image: "Terraform Cloud.png",
isAvailable: true,
type: "pat",
cliendId: "",
docsLink: ""
},
{
name: "Travis CI",
slug: "travisci",
image: "Travis CI.png",
isAvailable: true,
type: "pat",
clientId: "",
docsLink: ""
},
{
name: "TeamCity",
slug: "teamcity",
image: "TeamCity.png",
isAvailable: true,
type: "pat",
clientId: "",
docsLink: ""
},
{
name: "Supabase",
slug: "supabase",
image: "Supabase.png",
isAvailable: true,
type: "pat",
clientId: "",
docsLink: ""
},
{
name: "Checkly",
slug: "checkly",
image: "Checkly.png",
isAvailable: true,
type: "pat",
clientId: "",
docsLink: ""
},
{
name: "Qovery",
slug: "qovery",
image: "Qovery.png",
isAvailable: true,
type: "pat",
clientId: "",
docsLink: ""
},
{
name: "HashiCorp Vault",
slug: "hashicorp-vault",
image: "Vault.png",
isAvailable: true,
type: "pat",
clientId: "",
docsLink: ""
},
{
name: "GCP Secret Manager",
slug: "gcp-secret-manager",
image: "Google Cloud Platform.png",
isAvailable: true,
type: "oauth",
clientId: appCfg.CLIENT_ID_GCP_SECRET_MANAGER,
docsLink: ""
},
{
name: "Cloudflare Pages",
slug: "cloudflare-pages",
image: "Cloudflare.png",
isAvailable: true,
type: "pat",
clientId: "",
docsLink: ""
},
{
name: "Cloudflare Workers",
slug: "cloudflare-workers",
image: "Cloudflare.png",
isAvailable: true,
type: "pat",
clientId: "",
docsLink: ""
},
{
name: "BitBucket",
slug: "bitbucket",
image: "BitBucket.png",
isAvailable: true,
type: "oauth",
clientId: appCfg.CLIENT_ID_BITBUCKET,
docsLink: ""
},
{
name: "Codefresh",
slug: "codefresh",
image: "Codefresh.png",
isAvailable: true,
type: "pat",
clientId: "",
docsLink: ""
},
{
name: "Windmill",
slug: "windmill",
image: "Windmill.png",
isAvailable: true,
type: "pat",
clientId: "",
docsLink: ""
},
{
name: "Digital Ocean App Platform",
slug: "digital-ocean-app-platform",
image: "Digital Ocean.png",
isAvailable: true,
type: "pat",
clientId: "",
docsLink: ""
},
{
name: "Cloud 66",
slug: "cloud-66",
image: "Cloud 66.png",
isAvailable: true,
type: "pat",
clientId: "",
docsLink: ""
},
{
name: "Northflank",
slug: "northflank",
image: "Northflank.png",
isAvailable: true,
type: "pat",
clientId: "",
docsLink: ""
},
{
name: "Hasura Cloud",
slug: "hasura-cloud",
image: "Hasura.svg",
isAvailable: true,
type: "pat",
clientId: "",
docsLink: ""
}
];
return INTEGRATION_OPTIONS;
};
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,49 @@
import { request } from "@app/lib/config/request";
import { BadRequestError } from "@app/lib/errors";
import { Integrations,IntegrationUrls } from "./integration-list";
type Team = {
name: string;
teamId: string;
};
const getTeamsGitLab = async ({ url, accessToken }: { url: string; accessToken: string }) => {
const gitLabApiUrl = url ? `${url}/api` : IntegrationUrls.GITLAB_API_URL;
let teams: Team[] = [];
const res = (
await request.get(`${gitLabApiUrl}/v4/groups`, {
headers: {
Authorization: `Bearer ${accessToken}`,
"Accept-Encoding": "application/json"
}
})
).data;
teams = res.map((t: any) => ({
name: t.name,
teamId: t.id
}));
return teams;
};
export const getTeams = async ({
accessToken,
url,
integration
}: {
accessToken: string;
url?: string;
integration: string;
}) => {
switch (integration) {
case Integrations.GITLAB:
return getTeamsGitLab({
url: url as string,
accessToken
});
default:
throw new BadRequestError({ message: "Integration doesn't have team support" });
}
};
@@ -0,0 +1,724 @@
import jwt from "jsonwebtoken";
import { getConfig } from "@app/lib/config/env";
import { request } from "@app/lib/config/request";
import { BadRequestError } from "@app/lib/errors";
import { Integrations, IntegrationUrls } from "./integration-list";
type ExchangeCodeAzureResponse = {
token_type: string;
scope: string;
expires_in: number;
ext_expires_in: number;
access_token: string;
refresh_token: string;
id_token: string;
};
type ExchangeCodeGCPResponse = {
access_token: string;
expires_in: number;
refresh_token: string;
scope: string;
token_type: string;
};
type ExchangeCodeHerokuResponse = {
token_type: string;
access_token: string;
expires_in: number;
refresh_token: string;
user_id: string;
session_nonce?: string;
};
type ExchangeCodeVercelResponse = {
token_type: string;
access_token: string;
installation_id: string;
user_id: string;
team_id?: string;
};
type ExchangeCodeNetlifyResponse = {
access_token: string;
token_type: string;
refresh_token: string;
scope: string;
created_at: number;
};
type ExchangeCodeGithubResponse = {
access_token: string;
scope: string;
token_type: string;
};
type ExchangeCodeGitlabResponse = {
access_token: string;
token_type: string;
expires_in: number;
refresh_token: string;
scope: string;
created_at: number;
};
type ExchangeCodeBitBucketResponse = {
access_token: string;
token_type: string;
expires_in: number;
refresh_token: string;
scopes: string;
state: string;
};
const exchangeCodeGCP = async ({ code }: { code: string }) => {
const accessExpiresAt = new Date();
const appCfg = getConfig();
if (!appCfg.CLIENT_SECRET_GCP_SECRET_MANAGER || !appCfg.CLIENT_ID_GCP_SECRET_MANAGER) {
throw new BadRequestError({ message: "Missing client id and client secret" });
}
const res: ExchangeCodeGCPResponse = (
await request.post(
IntegrationUrls.GCP_TOKEN_URL,
new URLSearchParams({
grant_type: "authorization_code",
code,
client_id: appCfg.CLIENT_ID_GCP_SECRET_MANAGER,
client_secret: appCfg.CLIENT_SECRET_GCP_SECRET_MANAGER,
redirect_uri: `${appCfg.SITE_URL}/integrations/gcp-secret-manager/oauth2/callback`
})
)
).data;
accessExpiresAt.setSeconds(accessExpiresAt.getSeconds() + res.expires_in);
return {
accessToken: res.access_token,
refreshToken: res.refresh_token,
accessExpiresAt
};
};
const exchangeCodeAzure = async ({ code }: { code: string }) => {
const accessExpiresAt = new Date();
const appCfg = getConfig();
if (!appCfg.CLIENT_ID_AZURE || !appCfg.CLIENT_SECRET_AZURE) {
throw new BadRequestError({ message: "Missing client id and client secret" });
}
const res: ExchangeCodeAzureResponse = (
await request.post(
IntegrationUrls.AZURE_TOKEN_URL,
new URLSearchParams({
grant_type: "authorization_code",
code,
scope: "https://vault.azure.net/.default openid offline_access",
client_id: appCfg.CLIENT_ID_AZURE,
client_secret: appCfg.CLIENT_SECRET_AZURE,
redirect_uri: `${appCfg.SITE_URL}/integrations/azure-key-vault/oauth2/callback`
} as any)
)
).data;
accessExpiresAt.setSeconds(accessExpiresAt.getSeconds() + res.expires_in);
return {
accessToken: res.access_token,
refreshToken: res.refresh_token,
accessExpiresAt
};
};
const exchangeCodeHeroku = async ({ code }: { code: string }) => {
const accessExpiresAt = new Date();
const appCfg = getConfig();
if (!appCfg.CLIENT_SECRET_HEROKU) {
throw new BadRequestError({ message: "Missing client id and client secret" });
}
const res: ExchangeCodeHerokuResponse = (
await request.post(
IntegrationUrls.HEROKU_TOKEN_URL,
new URLSearchParams({
grant_type: "authorization_code",
code,
client_secret: appCfg.CLIENT_SECRET_HEROKU
})
)
).data;
accessExpiresAt.setSeconds(accessExpiresAt.getSeconds() + res.expires_in);
return {
accessToken: res.access_token,
refreshToken: res.refresh_token,
accessExpiresAt
};
};
/**
* Return [accessToken], [accessExpiresAt], and [refreshToken] for Vercel
* 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 Heroku API
* @returns {String} obj2.refreshToken - refresh token for Heroku API
* @returns {Date} obj2.accessExpiresAt - date of expiration for access token
*/
const exchangeCodeVercel = async ({ code }: { code: string }) => {
const appCfg = getConfig();
if (!appCfg.CLIENT_ID_VERCEL || !appCfg.CLIENT_SECRET_VERCEL) {
throw new BadRequestError({ message: "Missing client id and client secret" });
}
const res: ExchangeCodeVercelResponse = (
await request.post(
IntegrationUrls.VERCEL_TOKEN_URL,
new URLSearchParams({
code,
client_id: appCfg.CLIENT_ID_VERCEL,
client_secret: appCfg.CLIENT_SECRET_VERCEL,
redirect_uri: `${appCfg.SITE_URL}/integrations/vercel/oauth2/callback`
} as any)
)
).data;
return {
accessToken: res.access_token,
refreshToken: null,
accessExpiresAt: null,
teamId: res.team_id
};
};
/**
* Return [accessToken], [accessExpiresAt], and [refreshToken] for Vercel
* 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 Heroku API
* @returns {String} obj2.refreshToken - refresh token for Heroku API
* @returns {Date} obj2.accessExpiresAt - date of expiration for access token
*/
const exchangeCodeNetlify = async ({ code }: { code: string }) => {
const appCfg = getConfig();
if (!appCfg.CLIENT_ID_NETLIFY || !appCfg.CLIENT_SECRET_NETLIFY) {
throw new BadRequestError({ message: "Missing client id and client secret" });
}
const res: ExchangeCodeNetlifyResponse = (
await request.post(
IntegrationUrls.NETLIFY_TOKEN_URL,
new URLSearchParams({
grant_type: "authorization_code",
code,
client_id: appCfg.CLIENT_ID_NETLIFY,
client_secret: appCfg.CLIENT_SECRET_NETLIFY,
redirect_uri: `${appCfg.SITE_URL}/integrations/netlify/oauth2/callback`
} as any)
)
).data;
// akhilmhdh: commented out by me. Not sure why its being called but never used in prev codebase
// const res2 = await request.get("https://api.netlify.com/api/v1/sites", {
// headers: {
// Authorization: `Bearer ${res.access_token}`
// }
// });
const res3 = (
await request.get("https://api.netlify.com/api/v1/accounts", {
headers: {
Authorization: `Bearer ${res.access_token}`
}
})
).data;
const accountId = res3[0].id;
return {
accessToken: res.access_token,
refreshToken: res.refresh_token,
accountId
};
};
const exchangeCodeGithub = async ({ code }: { code: string }) => {
const appCfg = getConfig();
if (!appCfg.CLIENT_ID_GITHUB || !appCfg.CLIENT_SECRET_GITHUB) {
throw new BadRequestError({ message: "Missing client id and client secret" });
}
const res: ExchangeCodeGithubResponse = (
await request.get(IntegrationUrls.GITHUB_TOKEN_URL, {
params: {
client_id: appCfg.CLIENT_ID_GITHUB,
client_secret: appCfg.CLIENT_SECRET_GITHUB,
code,
redirect_uri: `${appCfg.SITE_URL}/integrations/github/oauth2/callback`
},
headers: {
Accept: "application/json",
"Accept-Encoding": "application/json"
}
})
).data;
return {
accessToken: res.access_token,
refreshToken: null,
accessExpiresAt: null
};
};
/**
* Return [accessToken], [accessExpiresAt], and [refreshToken] for Gitlab
* code-token exchange
*/
const exchangeCodeGitlab = async ({ code, url }: { code: string; url?: string }) => {
const accessExpiresAt = new Date();
const appCfg = getConfig();
if (!appCfg.CLIENT_ID_GITLAB || !appCfg.CLIENT_SECRET_GITLAB) {
throw new BadRequestError({ message: "Missing client id and client secret" });
}
const res: ExchangeCodeGitlabResponse = (
await request.post(
url ? `${url}/oauth/token` : IntegrationUrls.GITLAB_TOKEN_URL,
new URLSearchParams({
grant_type: "authorization_code",
code,
client_id: appCfg.CLIENT_ID_GITLAB,
client_secret: appCfg.CLIENT_SECRET_GITLAB,
redirect_uri: `${appCfg.SITE_URL}/integrations/gitlab/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,
url
};
};
/**
* Return [accessToken], [accessExpiresAt], and [refreshToken] for BitBucket
* code-token exchange
*/
const exchangeCodeBitBucket = async ({ code }: { code: string }) => {
const accessExpiresAt = new Date();
const appCfg = getConfig();
if (!appCfg.CLIENT_SECRET_BITBUCKET || !appCfg.CLIENT_ID_BITBUCKET) {
throw new BadRequestError({ message: "Missing client id and client secret" });
}
const res: ExchangeCodeBitBucketResponse = (
await request.post(
IntegrationUrls.BITBUCKET_TOKEN_URL,
new URLSearchParams({
grant_type: "authorization_code",
code,
client_id: appCfg.CLIENT_ID_BITBUCKET,
client_secret: appCfg.CLIENT_SECRET_BITBUCKET,
redirect_uri: `${appCfg.SITE_URL}/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
};
};
type TExchangeReturn = {
accessToken: string;
refreshToken?: string | null;
accessExpiresAt?: Date | null;
url?: string;
teamId?: string;
accountId?: string;
};
/**
* Return [accessToken], [accessExpiresAt], and [refreshToken] for OAuth2
* code-token exchange for integration named [integration]
*/
export const exchangeCode = async ({
integration,
code,
url
}: {
integration: string;
code: string;
url?: string;
}): Promise<TExchangeReturn> => {
switch (integration) {
case Integrations.GCP_SECRET_MANAGER:
return exchangeCodeGCP({
code
});
case Integrations.AZURE_KEY_VAULT:
return exchangeCodeAzure({
code
});
case Integrations.HEROKU:
return exchangeCodeHeroku({
code
});
case Integrations.VERCEL:
return exchangeCodeVercel({
code
});
case Integrations.NETLIFY:
return exchangeCodeNetlify({
code
});
case Integrations.GITHUB:
return exchangeCodeGithub({
code
});
case Integrations.GITLAB:
return exchangeCodeGitlab({
code,
url
});
case Integrations.BITBUCKET:
return exchangeCodeBitBucket({
code
});
default:
throw new BadRequestError({ message: "Unknown integration" });
}
};
type RefreshTokenAzureResponse = {
token_type: string;
scope: string;
expires_in: number;
ext_expires_in: 4871;
access_token: string;
refresh_token: string;
};
type RefreshTokenHerokuResponse = {
access_token: string;
expires_in: number;
refresh_token: string;
token_type: string;
user_id: string;
};
type RefreshTokenGitLabResponse = {
token_type: string;
scope: string;
expires_in: number;
access_token: string;
refresh_token: string;
created_at: number;
};
type RefreshTokenBitBucketResponse = {
access_token: string;
token_type: string;
expires_in: number;
refresh_token: string;
scopes: string;
state: string;
};
type ServiceAccountAccessTokenGCPSecretManagerResponse = {
access_token: string;
expires_in: number;
token_type: string;
};
type RefreshTokenGCPSecretManagerResponse = {
access_token: string;
expires_in: number;
scope: string;
token_type: string;
};
/**
* Return new access token by exchanging refresh token [refreshToken] for the
* Azure integration
*/
const exchangeRefreshAzure = async ({ refreshToken }: { refreshToken: string }) => {
const accessExpiresAt = new Date();
const appCfg = getConfig();
if (!appCfg.CLIENT_ID_AZURE || !appCfg.CLIENT_SECRET_AZURE) {
throw new BadRequestError({ message: "Missing client id and client secret" });
}
const { data }: { data: RefreshTokenAzureResponse } = await request.post(
IntegrationUrls.AZURE_TOKEN_URL,
new URLSearchParams({
client_id: appCfg.CLIENT_ID_AZURE,
scope: "openid offline_access",
refresh_token: refreshToken,
grant_type: "refresh_token",
client_secret: appCfg.CLIENT_SECRET_AZURE
})
);
accessExpiresAt.setSeconds(accessExpiresAt.getSeconds() + data.expires_in);
return {
accessToken: data.access_token,
refreshToken: data.refresh_token,
accessExpiresAt
};
};
/**
* Return new access token by exchanging refresh token [refreshToken] for the
* Heroku integration
*/
const exchangeRefreshHeroku = async ({ refreshToken }: { refreshToken: string }) => {
const accessExpiresAt = new Date();
const appCfg = getConfig();
if (!appCfg.CLIENT_SECRET_HEROKU) {
throw new BadRequestError({ message: "Missing client id and client secret" });
}
const {
data
}: {
data: RefreshTokenHerokuResponse;
} = await request.post(
IntegrationUrls.HEROKU_TOKEN_URL,
new URLSearchParams({
grant_type: "refresh_token",
refresh_token: refreshToken,
client_secret: appCfg.CLIENT_SECRET_HEROKU
})
);
accessExpiresAt.setSeconds(accessExpiresAt.getSeconds() + data.expires_in);
return {
accessToken: data.access_token,
refreshToken: data.refresh_token,
accessExpiresAt
};
};
/**
* Return new access token by exchanging refresh token [refreshToken] for the
* GitLab integration
* @param {Object} obj
* @param {String} obj.refreshToken - refresh token to use to get new access token for GitLab
* @returns
*/
const exchangeRefreshGitLab = async ({
refreshToken,
url
}: {
url?: string | null;
refreshToken: string;
}) => {
const accessExpiresAt = new Date();
const appCfg = getConfig();
if (!appCfg.CLIENT_ID_GITLAB || !appCfg.CLIENT_SECRET_GITLAB) {
throw new BadRequestError({ message: "Missing client id and client secret" });
}
const {
data
}: {
data: RefreshTokenGitLabResponse;
} = await request.post(
url ? `${url}/oauth/token` : IntegrationUrls.GITLAB_TOKEN_URL,
new URLSearchParams({
grant_type: "refresh_token",
refresh_token: refreshToken,
client_id: appCfg.CLIENT_ID_GITLAB,
client_secret: appCfg.CLIENT_SECRET_GITLAB,
redirect_uri: `${appCfg.SITE_URL}/integrations/gitlab/oauth2/callback`
}),
{
headers: {
"Accept-Encoding": "application/json"
}
}
);
accessExpiresAt.setSeconds(accessExpiresAt.getSeconds() + data.expires_in);
return {
accessToken: data.access_token,
refreshToken: data.refresh_token,
accessExpiresAt
};
};
/**
* Return new access token by exchanging refresh token [refreshToken] for the
* BitBucket integration
*/
const exchangeRefreshBitBucket = async ({ refreshToken }: { refreshToken: string }) => {
const accessExpiresAt = new Date();
const appCfg = getConfig();
if (!appCfg.CLIENT_SECRET_BITBUCKET || !appCfg.CLIENT_ID_BITBUCKET) {
throw new BadRequestError({ message: "Missing client id and client secret" });
}
const {
data
}: {
data: RefreshTokenBitBucketResponse;
} = await request.post(
IntegrationUrls.BITBUCKET_TOKEN_URL,
new URLSearchParams({
grant_type: "refresh_token",
refresh_token: refreshToken,
client_id: appCfg.CLIENT_ID_BITBUCKET,
client_secret: appCfg.CLIENT_SECRET_BITBUCKET,
redirect_uri: `${appCfg.SITE_URL}/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
};
};
/**
* Return new access token by exchanging refresh token [refreshToken] for the
* GCP Secret Manager integration
*/
const exchangeRefreshGCPSecretManager = async ({
refreshToken,
metadata = {}
}: {
metadata?: Record<string, string>;
refreshToken: string;
}) => {
const accessExpiresAt = new Date();
if (metadata?.authMethod === "serviceAccount") {
const serviceAccount = JSON.parse(refreshToken);
const payload = {
iss: serviceAccount.client_email,
aud: serviceAccount.token_uri,
scope: IntegrationUrls.GCP_CLOUD_PLATFORM_SCOPE,
iat: Math.floor(Date.now() / 1000),
exp: Math.floor(Date.now() / 1000) + 3600
};
const token = jwt.sign(payload, serviceAccount.private_key, { algorithm: "RS256" });
const { data }: { data: ServiceAccountAccessTokenGCPSecretManagerResponse } =
await request.post(
IntegrationUrls.GCP_TOKEN_URL,
new URLSearchParams({
grant_type: "urn:ietf:params:oauth:grant-type:jwt-bearer",
assertion: token
}).toString(),
{
headers: {
"Content-Type": "application/x-www-form-urlencoded"
}
}
);
accessExpiresAt.setSeconds(accessExpiresAt.getSeconds() + data.expires_in);
return {
accessToken: data.access_token,
refreshToken,
accessExpiresAt
};
}
const appCfg = getConfig();
if (!appCfg.CLIENT_SECRET_GCP_SECRET_MANAGER || !appCfg.CLIENT_ID_GCP_SECRET_MANAGER) {
throw new BadRequestError({ message: "Missing client id and client secret" });
}
const { data }: { data: RefreshTokenGCPSecretManagerResponse } = await request.post(
IntegrationUrls.GCP_TOKEN_URL,
new URLSearchParams({
client_id: appCfg.CLIENT_ID_GCP_SECRET_MANAGER,
client_secret: appCfg.CLIENT_SECRET_GCP_SECRET_MANAGER,
refresh_token: refreshToken,
grant_type: "refresh_token"
} as any)
);
accessExpiresAt.setSeconds(accessExpiresAt.getSeconds() + data.expires_in);
return {
accessToken: data.access_token,
refreshToken,
accessExpiresAt
};
};
/**
* Return new access token by exchanging refresh token [refreshToken] for integration
*/
export const exchangeRefresh = async (
integration: string,
refreshToken: string,
url?: string | null,
metadata?: Record<string, string>
): Promise<{
accessToken: string;
refreshToken: string;
accessExpiresAt: Date;
}> => {
switch (integration) {
case Integrations.AZURE_KEY_VAULT:
return exchangeRefreshAzure({
refreshToken
});
case Integrations.HEROKU:
return exchangeRefreshHeroku({
refreshToken
});
case Integrations.GITLAB:
return exchangeRefreshGitLab({
refreshToken,
url
});
case Integrations.BITBUCKET:
return exchangeRefreshBitBucket({
refreshToken
});
case Integrations.GCP_SECRET_MANAGER:
return exchangeRefreshGCPSecretManager({
refreshToken,
metadata
});
default:
throw new Error("Failed to exchange token for incompatible integration");
}
};