Merge pull request #423 from Infisical/check-integrations

Patch create integration page on no integration projects and add support for groups in GitLab integration
This commit is contained in:
BlackMagiq
2023-03-10 21:50:10 +07:00
committed by GitHub
27 changed files with 800 additions and 239 deletions

View File

@@ -2,13 +2,16 @@ import { Request, Response } from 'express';
import { Types } from 'mongoose'; import { Types } from 'mongoose';
import * as Sentry from '@sentry/node'; import * as Sentry from '@sentry/node';
import { import {
Integration,
IntegrationAuth, IntegrationAuth,
Bot Bot
} from '../../models'; } from '../../models';
import { INTEGRATION_SET, INTEGRATION_OPTIONS } from '../../variables'; import { INTEGRATION_SET, INTEGRATION_OPTIONS } from '../../variables';
import { IntegrationService } from '../../services'; import { IntegrationService } from '../../services';
import { getApps, revokeAccess } from '../../integrations'; import {
getApps,
getTeams,
revokeAccess
} from '../../integrations';
/*** /***
* Return integration authorization with id [integrationAuthId] * Return integration authorization with id [integrationAuthId]
@@ -154,25 +157,54 @@ export const saveIntegrationAccessToken = async (
* @returns * @returns
*/ */
export const getIntegrationAuthApps = async (req: Request, res: Response) => { export const getIntegrationAuthApps = async (req: Request, res: Response) => {
let apps; let apps;
try { try {
apps = await getApps({ const teamId = req.query.teamId as string;
integrationAuth: req.integrationAuth,
accessToken: req.accessToken, apps = await getApps({
}); integrationAuth: req.integrationAuth,
} catch (err) { accessToken: req.accessToken,
Sentry.setUser({ email: req.user.email }); ...teamId && { teamId }
Sentry.captureException(err); });
return res.status(400).send({ } catch (err) {
message: "Failed to get integration authorization applications", Sentry.setUser({ email: req.user.email });
}); Sentry.captureException(err);
} return res.status(400).send({
message: "Failed to get integration authorization applications",
});
}
return res.status(200).send({ return res.status(200).send({
apps, apps
}); });
}; };
/**
* Return list of teams allowed for integration with integration authorization id [integrationAuthId]
* @param req
* @param res
* @returns
*/
export const getIntegrationAuthTeams = async (req: Request, res: Response) => {
let teams;
try {
teams = await getTeams({
integrationAuth: req.integrationAuth,
accessToken: req.accessToken
});
} catch (err) {
Sentry.setUser({ email: req.user.email });
Sentry.captureException(err);
return res.status(400).send({
message: "Failed to get integration authorization teams"
});
}
return res.status(200).send({
teams
});
}
/** /**
* Delete integration authorization with id [integrationAuthId] * Delete integration authorization with id [integrationAuthId]
* @param req * @param req

View File

@@ -2,10 +2,7 @@ import { Request, Response } from 'express';
import { Types } from 'mongoose'; import { Types } from 'mongoose';
import * as Sentry from '@sentry/node'; import * as Sentry from '@sentry/node';
import { import {
Integration, Integration
Workspace,
Bot,
BotKey
} from '../../models'; } from '../../models';
import { EventService } from '../../services'; import { EventService } from '../../services';
import { eventPushSecrets } from '../../events'; import { eventPushSecrets } from '../../events';
@@ -18,6 +15,7 @@ import { eventPushSecrets } from '../../events';
*/ */
export const createIntegration = async (req: Request, res: Response) => { export const createIntegration = async (req: Request, res: Response) => {
let integration; let integration;
try { try {
const { const {
integrationAuthId, integrationAuthId,
@@ -34,19 +32,19 @@ export const createIntegration = async (req: Request, res: Response) => {
// TODO: validate [sourceEnvironment] and [targetEnvironment] // TODO: validate [sourceEnvironment] and [targetEnvironment]
// initialize new integration after saving integration access token // initialize new integration after saving integration access token
integration = await new Integration({ integration = await new Integration({
workspace: req.integrationAuth.workspace._id, workspace: req.integrationAuth.workspace._id,
environment: sourceEnvironment, environment: sourceEnvironment,
isActive, isActive,
app, app,
appId, appId,
targetEnvironment, targetEnvironment,
owner, owner,
path, path,
region, region,
integration: req.integrationAuth.integration, integration: req.integrationAuth.integration,
integrationAuth: new Types.ObjectId(integrationAuthId) integrationAuth: new Types.ObjectId(integrationAuthId)
}).save(); }).save();
if (integration) { if (integration) {
// trigger event - push secrets // trigger event - push secrets

View File

@@ -229,7 +229,7 @@ const getIntegrationAuthAccessHelper = async ({ integrationAuthId }: { integrati
// access token is expired // access token is expired
const refreshToken = await getIntegrationAuthRefreshHelper({ integrationAuthId }); const refreshToken = await getIntegrationAuthRefreshHelper({ integrationAuthId });
accessToken = await exchangeRefresh({ accessToken = await exchangeRefresh({
integration: integrationAuth.integration, integrationAuth,
refreshToken refreshToken
}); });
} }

View File

@@ -25,26 +25,30 @@ import {
INTEGRATION_TRAVISCI_API_URL, INTEGRATION_TRAVISCI_API_URL,
} from "../variables"; } from "../variables";
interface App {
name: string;
appId?: string;
owner?: string;
}
/** /**
* Return list of names of apps for integration named [integration] * Return list of names of apps for integration named [integration]
* @param {Object} obj * @param {Object} obj
* @param {String} obj.integration - name of integration * @param {String} obj.integration - name of integration
* @param {String} obj.accessToken - access token for integration * @param {String} obj.accessToken - access token for integration
* @param {String} obj.teamId - (optional) id of team for getting integration apps (used for integrations like GitLab)
* @returns {Object[]} apps - names of integration apps * @returns {Object[]} apps - names of integration apps
* @returns {String} apps.name - name of integration app * @returns {String} apps.name - name of integration app
*/ */
const getApps = async ({ const getApps = async ({
integrationAuth, integrationAuth,
accessToken, accessToken,
teamId
}: { }: {
integrationAuth: IIntegrationAuth; integrationAuth: IIntegrationAuth;
accessToken: string; accessToken: string;
teamId?: string;
}) => { }) => {
interface App {
name: string;
appId?: string;
owner?: string;
}
let apps: App[] = []; let apps: App[] = [];
try { try {
@@ -82,6 +86,7 @@ const getApps = async ({
case INTEGRATION_GITLAB: case INTEGRATION_GITLAB:
apps = await getAppsGitlab({ apps = await getAppsGitlab({
accessToken, accessToken,
teamId
}); });
break; break;
case INTEGRATION_RENDER: case INTEGRATION_RENDER:
@@ -434,44 +439,107 @@ const getAppsTravisCI = async ({ accessToken }: { accessToken: string }) => {
* @returns {Object[]} apps - names of GitLab sites * @returns {Object[]} apps - names of GitLab sites
* @returns {String} apps.name - name of GitLab site * @returns {String} apps.name - name of GitLab site
*/ */
const getAppsGitlab = async ({ accessToken }: {accessToken: string}) => { const getAppsGitlab = async ({
let apps; accessToken,
teamId
}: {
accessToken: string;
teamId?: string;
}) => {
const apps: App[] = [];
let page = 1;
const perPage = 10;
let hasMorePages = true;
try { try {
const { id } = (
await request.get(
`${INTEGRATION_GITLAB_API_URL}/v4/user`,
{
headers: {
"Authorization": `Bearer ${accessToken}`,
"Accept-Encoding": "application/json",
},
}
)
).data;
const res = ( if (teamId) {
await request.get( // case: fetch projects for group with id [teamId] in GitLab
`${INTEGRATION_GITLAB_API_URL}/v4/users/${id}/projects`,
{ while (hasMorePages) {
headers: { const params = new URLSearchParams({
"Authorization": `Bearer ${accessToken}`, page: String(page),
"Accept-Encoding": "application/json", per_page: String(perPage)
}, });
}
)
).data;
apps = res?.map((a: any) => { const { data } = (
return { await request.get(
name: a?.name, `${INTEGRATION_GITLAB_API_URL}/v4/groups/${teamId}/projects`,
appId: `${a?.id}`, {
params,
headers: {
"Authorization": `Bearer ${accessToken}`,
"Accept-Encoding": "application/json",
},
}
)
);
data.map((a: any) => {
apps.push({
name: a.name,
appId: a.id
});
});
if (data.length < perPage) {
hasMorePages = false;
}
page++;
} }
}); } else {
}catch (err) { // case: fetch projects for individual in GitLab
const { id } = (
await request.get(
`${INTEGRATION_GITLAB_API_URL}/v4/user`,
{
headers: {
"Authorization": `Bearer ${accessToken}`,
"Accept-Encoding": "application/json",
},
}
)
).data;
while (hasMorePages) {
const params = new URLSearchParams({
page: String(page),
per_page: String(perPage)
});
const { data } = (
await request.get(
`${INTEGRATION_GITLAB_API_URL}/v4/users/${id}/projects`,
{
params,
headers: {
"Authorization": `Bearer ${accessToken}`,
"Accept-Encoding": "application/json",
},
}
)
);
data.map((a: any) => {
apps.push({
name: a.name,
appId: a.id
});
});
if (data.length < perPage) {
hasMorePages = false;
}
page++;
}
}
} catch (err) {
Sentry.setUser(null); Sentry.setUser(null);
Sentry.captureException(err); Sentry.captureException(err);
throw new Error("Failed to get GitLab repos"); throw new Error("Failed to get GitLab projects");
} }
return apps; return apps;

View File

@@ -12,7 +12,7 @@ import {
INTEGRATION_VERCEL_TOKEN_URL, INTEGRATION_VERCEL_TOKEN_URL,
INTEGRATION_NETLIFY_TOKEN_URL, INTEGRATION_NETLIFY_TOKEN_URL,
INTEGRATION_GITHUB_TOKEN_URL, INTEGRATION_GITHUB_TOKEN_URL,
INTEGRATION_GITLAB_TOKEN_URL, INTEGRATION_GITLAB_TOKEN_URL
} from '../variables'; } from '../variables';
import { import {
SITE_URL, SITE_URL,
@@ -73,7 +73,7 @@ interface ExchangeCodeGithubResponse {
interface ExchangeCodeGitlabResponse { interface ExchangeCodeGitlabResponse {
access_token: string; access_token: string;
token_type: string; token_type: string;
expires_in: string; expires_in: number;
refresh_token: string; refresh_token: string;
scope: string; scope: string;
created_at: number; created_at: number;
@@ -168,7 +168,7 @@ const exchangeCodeAzure = async ({
accessExpiresAt.setSeconds( accessExpiresAt.setSeconds(
accessExpiresAt.getSeconds() + res.expires_in accessExpiresAt.getSeconds() + res.expires_in
); );
} catch (err: any) { } catch (err) {
Sentry.setUser(null); Sentry.setUser(null);
Sentry.captureException(err); Sentry.captureException(err);
throw new Error('Failed OAuth2 code-token exchange with Azure'); throw new Error('Failed OAuth2 code-token exchange with Azure');
@@ -370,6 +370,7 @@ const exchangeCodeGithub = async ({ code }: { code: string }) => {
*/ */
const exchangeCodeGitlab = async ({ code }: { code: string }) => { const exchangeCodeGitlab = async ({ code }: { code: string }) => {
let res: ExchangeCodeGitlabResponse; let res: ExchangeCodeGitlabResponse;
const accessExpiresAt = new Date();
try { try {
res = ( res = (
@@ -389,7 +390,11 @@ const exchangeCodeGitlab = async ({ code }: { code: string }) => {
} }
) )
).data; ).data;
}catch (err) {
accessExpiresAt.setSeconds(
accessExpiresAt.getSeconds() + res.expires_in
);
} catch (err) {
Sentry.setUser(null); Sentry.setUser(null);
Sentry.captureException(err); Sentry.captureException(err);
throw new Error('Failed OAuth2 code-token exchange with Gitlab'); throw new Error('Failed OAuth2 code-token exchange with Gitlab');
@@ -397,8 +402,8 @@ const exchangeCodeGitlab = async ({ code }: { code: string }) => {
return { return {
accessToken: res.access_token, accessToken: res.access_token,
refreshToken: null, refreshToken: res.refresh_token,
accessExpiresAt: null accessExpiresAt
}; };
} }

View File

@@ -1,6 +1,7 @@
import { exchangeCode } from './exchange'; import { exchangeCode } from './exchange';
import { exchangeRefresh } from './refresh'; import { exchangeRefresh } from './refresh';
import { getApps } from './apps'; import { getApps } from './apps';
import { getTeams } from './teams';
import { syncSecrets } from './sync'; import { syncSecrets } from './sync';
import { revokeAccess } from './revoke'; import { revokeAccess } from './revoke';
@@ -8,6 +9,7 @@ export {
exchangeCode, exchangeCode,
exchangeRefresh, exchangeRefresh,
getApps, getApps,
getTeams,
syncSecrets, syncSecrets,
revokeAccess revokeAccess
} }

View File

@@ -1,16 +1,29 @@
import request from '../config/request'; import request from '../config/request';
import * as Sentry from '@sentry/node'; import * as Sentry from '@sentry/node';
import { INTEGRATION_AZURE_KEY_VAULT, INTEGRATION_HEROKU } from '../variables'; import {
IIntegrationAuth
} from '../models';
import {
INTEGRATION_AZURE_KEY_VAULT,
INTEGRATION_HEROKU,
INTEGRATION_GITLAB,
} from '../variables';
import { import {
SITE_URL, SITE_URL,
CLIENT_ID_AZURE, CLIENT_ID_AZURE,
CLIENT_ID_GITLAB,
CLIENT_SECRET_AZURE, CLIENT_SECRET_AZURE,
CLIENT_SECRET_HEROKU CLIENT_SECRET_HEROKU,
CLIENT_SECRET_GITLAB
} from '../config'; } from '../config';
import { import {
INTEGRATION_AZURE_TOKEN_URL, INTEGRATION_AZURE_TOKEN_URL,
INTEGRATION_HEROKU_TOKEN_URL INTEGRATION_HEROKU_TOKEN_URL,
INTEGRATION_GITLAB_TOKEN_URL
} from '../variables'; } from '../variables';
import {
IntegrationService
} from '../services';
interface RefreshTokenAzureResponse { interface RefreshTokenAzureResponse {
token_type: string; token_type: string;
@@ -21,6 +34,23 @@ interface RefreshTokenAzureResponse {
refresh_token: string; refresh_token: string;
} }
interface RefreshTokenHerokuResponse {
access_token: string;
expires_in: number;
refresh_token: string;
token_type: string;
user_id: string;
}
interface RefreshTokenGitLabResponse {
token_type: string;
scope: string;
expires_in: number;
access_token: string;
refresh_token: string;
created_at: number;
}
/** /**
* Return new access token by exchanging refresh token [refreshToken] for integration * Return new access token by exchanging refresh token [refreshToken] for integration
* named [integration] * named [integration]
@@ -29,33 +59,61 @@ interface RefreshTokenAzureResponse {
* @param {String} obj.refreshToken - refresh token to use to get new access token for Heroku * @param {String} obj.refreshToken - refresh token to use to get new access token for Heroku
*/ */
const exchangeRefresh = async ({ const exchangeRefresh = async ({
integration, integrationAuth,
refreshToken refreshToken
}: { }: {
integration: string; integrationAuth: IIntegrationAuth;
refreshToken: string; refreshToken: string;
}) => { }) => {
let accessToken;
interface TokenDetails {
accessToken: string;
refreshToken: string;
accessExpiresAt: Date;
}
let tokenDetails: TokenDetails;
try { try {
switch (integration) { switch (integrationAuth.integration) {
case INTEGRATION_AZURE_KEY_VAULT: case INTEGRATION_AZURE_KEY_VAULT:
accessToken = await exchangeRefreshAzure({ tokenDetails = await exchangeRefreshAzure({
refreshToken refreshToken
}); });
break; break;
case INTEGRATION_HEROKU: case INTEGRATION_HEROKU:
accessToken = await exchangeRefreshHeroku({ tokenDetails = await exchangeRefreshHeroku({
refreshToken refreshToken
}); });
break; break;
case INTEGRATION_GITLAB:
tokenDetails = await exchangeRefreshGitLab({
refreshToken
});
break;
default:
throw new Error('Failed to exchange token for incompatible integration');
} }
if (tokenDetails?.accessToken && tokenDetails?.refreshToken && tokenDetails?.accessExpiresAt) {
await IntegrationService.setIntegrationAuthAccess({
integrationAuthId: integrationAuth._id.toString(),
accessId: null,
accessToken: tokenDetails.accessToken,
accessExpiresAt: tokenDetails.accessExpiresAt
});
await IntegrationService.setIntegrationAuthRefresh({
integrationAuthId: integrationAuth._id.toString(),
refreshToken: tokenDetails.refreshToken
});
}
return tokenDetails.accessToken;
} catch (err) { } catch (err) {
Sentry.setUser(null); Sentry.setUser(null);
Sentry.captureException(err); Sentry.captureException(err);
throw new Error('Failed to get new OAuth2 access token'); throw new Error('Failed to get new OAuth2 access token');
} }
return accessToken;
}; };
/** /**
@@ -71,7 +129,8 @@ const exchangeRefreshAzure = async ({
refreshToken: string; refreshToken: string;
}) => { }) => {
try { try {
const res: RefreshTokenAzureResponse = (await request.post( const accessExpiresAt = new Date();
const { data }: { data: RefreshTokenAzureResponse } = await request.post(
INTEGRATION_AZURE_TOKEN_URL, INTEGRATION_AZURE_TOKEN_URL,
new URLSearchParams({ new URLSearchParams({
client_id: CLIENT_ID_AZURE, client_id: CLIENT_ID_AZURE,
@@ -80,9 +139,17 @@ const exchangeRefreshAzure = async ({
grant_type: 'refresh_token', grant_type: 'refresh_token',
client_secret: CLIENT_SECRET_AZURE client_secret: CLIENT_SECRET_AZURE
} as any) } as any)
)).data; );
return res.access_token; accessExpiresAt.setSeconds(
accessExpiresAt.getSeconds() + data.expires_in
);
return ({
accessToken: data.access_token,
refreshToken: data.refresh_token,
accessExpiresAt
});
} catch (err) { } catch (err) {
Sentry.setUser(null); Sentry.setUser(null);
Sentry.captureException(err); Sentry.captureException(err);
@@ -102,10 +169,13 @@ const exchangeRefreshHeroku = async ({
}: { }: {
refreshToken: string; refreshToken: string;
}) => { }) => {
let accessToken;
try { try {
const res = await request.post( const accessExpiresAt = new Date();
const {
data
}: {
data: RefreshTokenHerokuResponse
} = await request.post(
INTEGRATION_HEROKU_TOKEN_URL, INTEGRATION_HEROKU_TOKEN_URL,
new URLSearchParams({ new URLSearchParams({
grant_type: 'refresh_token', grant_type: 'refresh_token',
@@ -114,14 +184,69 @@ const exchangeRefreshHeroku = async ({
} as any) } as any)
); );
accessToken = res.data.access_token; accessExpiresAt.setSeconds(
accessExpiresAt.getSeconds() + data.expires_in
);
return ({
accessToken: data.access_token,
refreshToken: data.refresh_token,
accessExpiresAt
});
} catch (err) { } catch (err) {
Sentry.setUser(null); Sentry.setUser(null);
Sentry.captureException(err); Sentry.captureException(err);
throw new Error('Failed to refresh OAuth2 access token for Heroku'); throw new Error('Failed to refresh OAuth2 access token for Heroku');
} }
};
return accessToken; /**
* 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
}: {
refreshToken: string;
}) => {
try {
const accessExpiresAt = new Date();
const {
data
}: {
data: RefreshTokenGitLabResponse
} = await request.post(
INTEGRATION_GITLAB_TOKEN_URL,
new URLSearchParams({
grant_type: 'refresh_token',
refresh_token: refreshToken,
client_id: CLIENT_ID_GITLAB,
client_secret: CLIENT_SECRET_GITLAB,
redirect_uri: `${SITE_URL}/integrations/gitlab/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
});
} catch (err) {
Sentry.setUser(null);
Sentry.captureException(err);
throw new Error('Failed to refresh OAuth2 access token for GitLab');
}
}; };
export { exchangeRefresh }; export { exchangeRefresh };

View File

@@ -172,7 +172,6 @@ const syncSecretsAzureKeyVault = async ({
accessToken: string; accessToken: string;
}) => { }) => {
try { try {
interface GetAzureKeyVaultSecret { interface GetAzureKeyVaultSecret {
id: string; // secret URI id: string; // secret URI
attributes: { attributes: {
@@ -1512,7 +1511,7 @@ const syncSecretsGitLab = async ({
) )
).data; ).data;
for (const key of Object.keys(secrets)) { for await (const key of Object.keys(secrets)) {
const existingSecret = getSecretsRes.find((s: any) => s.key == key); const existingSecret = getSecretsRes.find((s: any) => s.key == key);
if (!existingSecret) { if (!existingSecret) {
await request.post( await request.post(
@@ -1533,7 +1532,7 @@ const syncSecretsGitLab = async ({
}, },
} }
) )
}else { } else {
// udpate secret // udpate secret
await request.put( await request.put(
`${INTEGRATION_GITLAB_API_URL}/v4/projects/${integration?.appId}/variables/${existingSecret.key}`, `${INTEGRATION_GITLAB_API_URL}/v4/projects/${integration?.appId}/variables/${existingSecret.key}`,
@@ -1553,7 +1552,7 @@ const syncSecretsGitLab = async ({
} }
// delete secrets // delete secrets
for (const sec of getSecretsRes) { for await (const sec of getSecretsRes) {
if (!(sec.key in secrets)) { if (!(sec.key in secrets)) {
await request.delete( await request.delete(
`${INTEGRATION_GITLAB_API_URL}/v4/projects/${integration?.appId}/variables/${sec.key}`, `${INTEGRATION_GITLAB_API_URL}/v4/projects/${integration?.appId}/variables/${sec.key}`,

View File

@@ -0,0 +1,92 @@
import * as Sentry from "@sentry/node";
import {
IIntegrationAuth
} from '../models';
import {
INTEGRATION_GITLAB,
INTEGRATION_GITLAB_API_URL
} from '../variables';
import request from '../config/request';
interface Team {
name: string;
teamId: string;
}
/**
* Return list of teams for integration authorization [integrationAuth]
* @param {Object} obj
* @param {String} obj.integrationAuth - integration authorization to get teams for
* @param {String} obj.accessToken - access token for integration authorization
* @returns {Object[]} teams - teams of integration authorization
* @returns {String} teams.name - name of team
* @returns {String} teams.teamId - id of team
*/
const getTeams = async ({
integrationAuth,
accessToken
}: {
integrationAuth: IIntegrationAuth;
accessToken: string;
}) => {
let teams: Team[] = [];
try {
switch (integrationAuth.integration) {
case INTEGRATION_GITLAB:
teams = await getTeamsGitLab({
accessToken
});
break;
}
} catch (err) {
Sentry.setUser(null);
Sentry.captureException(err);
throw new Error('Failed to get integration teams');
}
return teams;
}
/**
* Return list of teams for GitLab integration
* @param {Object} obj
* @param {String} obj.accessToken - access token for GitLab API
* @returns {Object[]} teams - teams that user is part of in GitLab
* @returns {String} teams.name - name of team
* @returns {String} teams.teamId - id of team
*/
const getTeamsGitLab = async ({
accessToken
}: {
accessToken: string;
}) => {
let teams: Team[] = [];
try {
const res = (await request.get(
`${INTEGRATION_GITLAB_API_URL}/v4/groups`,
{
headers: {
Authorization: `Bearer ${accessToken}`,
"Accept-Encoding": "application/json"
}
}
)).data;
teams = res.map((t: any) => ({
name: t.name,
teamId: t.id
}));
} catch (err) {
Sentry.setUser(null);
Sentry.captureException(err);
throw new Error("Failed to get GitLab integration teams");
}
return teams;
}
export {
getTeams
}

View File

@@ -62,13 +62,11 @@ const integrationSchema = new Schema<IIntegration>(
default: null, default: null,
}, },
appId: { appId: {
// (new)
// id of app in provider // id of app in provider
type: String, type: String,
default: null, default: null,
}, },
targetEnvironment: { targetEnvironment: {
// (new)
// target environment // target environment
type: String, type: String,
default: null, default: null,

View File

@@ -23,9 +23,9 @@ export interface IIntegrationAuth {
refreshCiphertext?: string; refreshCiphertext?: string;
refreshIV?: string; refreshIV?: string;
refreshTag?: string; refreshTag?: string;
accessIdCiphertext?: string; // new accessIdCiphertext?: string;
accessIdIV?: string; // new accessIdIV?: string;
accessIdTag?: string; // new accessIdTag?: string;
accessCiphertext?: string; accessCiphertext?: string;
accessIV?: string; accessIV?: string;
accessTag?: string; accessTag?: string;

View File

@@ -1,6 +1,6 @@
import express from 'express'; import express from 'express';
const router = express.Router(); const router = express.Router();
import { body, param } from 'express-validator'; import { body, param, query } from 'express-validator';
import { import {
requireAuth, requireAuth,
requireWorkspaceAuth, requireWorkspaceAuth,
@@ -73,10 +73,24 @@ router.get(
acceptedRoles: [ADMIN, MEMBER] acceptedRoles: [ADMIN, MEMBER]
}), }),
param('integrationAuthId'), param('integrationAuthId'),
query('teamId'),
validateRequest, validateRequest,
integrationAuthController.getIntegrationAuthApps integrationAuthController.getIntegrationAuthApps
); );
router.get(
'/:integrationAuthId/teams',
requireAuth({
acceptedAuthModes: ['jwt']
}),
requireIntegrationAuthorizationAuth({
acceptedRoles: [ADMIN, MEMBER]
}),
param('integrationAuthId'),
validateRequest,
integrationAuthController.getIntegrationAuthTeams
);
router.delete( router.delete(
'/:integrationAuthId', '/:integrationAuthId',
requireAuth({ requireAuth({

View File

@@ -7,9 +7,6 @@ import {
setIntegrationAuthAccessHelper, setIntegrationAuthAccessHelper,
} from '../helpers/integration'; } from '../helpers/integration';
// should sync stuff be here too? Probably.
// TODO: move bot functions to IntegrationService.
/** /**
* Class to handle integrations * Class to handle integrations
*/ */

View File

@@ -59,6 +59,7 @@ const INTEGRATION_FLYIO_API_URL = "https://api.fly.io/graphql";
const INTEGRATION_CIRCLECI_API_URL = "https://circleci.com/api"; const INTEGRATION_CIRCLECI_API_URL = "https://circleci.com/api";
const INTEGRATION_TRAVISCI_API_URL = "https://api.travis-ci.com"; const INTEGRATION_TRAVISCI_API_URL = "https://api.travis-ci.com";
// TODO: deprecate types?
const INTEGRATION_OPTIONS = [ const INTEGRATION_OPTIONS = [
{ {
name: 'Heroku', name: 'Heroku',
@@ -156,7 +157,7 @@ const INTEGRATION_OPTIONS = [
slug: 'gitlab', slug: 'gitlab',
image: 'GitLab.png', image: 'GitLab.png',
isAvailable: true, isAvailable: true,
type: 'oauth', type: 'custom',
clientId: CLIENT_ID_GITLAB, clientId: CLIENT_ID_GITLAB,
docsLink: '' docsLink: ''
}, },

View File

@@ -1,3 +1,4 @@
export { export {
useGetIntegrationAuthApps, useGetIntegrationAuthApps,
useGetIntegrationAuthById} from './queries'; useGetIntegrationAuthById,
useGetIntegrationAuthTeams} from './queries';

View File

@@ -4,11 +4,13 @@ import { apiRequest } from "@app/config/request";
import { import {
App, App,
IntegrationAuth} from './types'; IntegrationAuth,
Team} from './types';
const integrationAuthKeys = { const integrationAuthKeys = {
getIntegrationAuthById: (integrationAuthId: string) => [{ integrationAuthId }, 'integrationAuth'] as const, getIntegrationAuthById: (integrationAuthId: string) => [{ integrationAuthId }, 'integrationAuth'] as const,
getIntegrationAuthApps: (integrationAuthId: string) => [{ integrationAuthId }, 'integrationAuthApps'] as const, getIntegrationAuthApps: (integrationAuthId: string, teamId?: string) => [{ integrationAuthId, teamId }, 'integrationAuthApps'] as const,
getIntegrationAuthTeams: (integrationAuthId: string) => [{ integrationAuthId }, 'integrationAuthTeams'] as const
} }
const fetchIntegrationAuthById = async (integrationAuthId: string) => { const fetchIntegrationAuthById = async (integrationAuthId: string) => {
@@ -16,11 +18,26 @@ const fetchIntegrationAuthById = async (integrationAuthId: string) => {
return data.integrationAuth; return data.integrationAuth;
} }
const fetchIntegrationAuthApps = async (integrationAuthId: string) => { const fetchIntegrationAuthApps = async ({
const { data } = await apiRequest.get<{ apps: App[] }>(`/api/v1/integration-auth/${integrationAuthId}/apps`); integrationAuthId,
teamId
}: {
integrationAuthId: string;
teamId?: string;
}) => {
const searchParams = new URLSearchParams(teamId ? { teamId } : undefined);
const { data } = await apiRequest.get<{ apps: App[] }>(
`/api/v1/integration-auth/${integrationAuthId}/apps`,
{ params: searchParams }
);
return data.apps; return data.apps;
} }
const fetchIntegrationAuthTeams = async (integrationAuthId: string) => {
const { data } = await apiRequest.get<{ teams: Team[] }>(`/api/v1/integration-auth/${integrationAuthId}/teams`);
return data.teams;
}
export const useGetIntegrationAuthById = (integrationAuthId: string) => { export const useGetIntegrationAuthById = (integrationAuthId: string) => {
return useQuery({ return useQuery({
queryKey: integrationAuthKeys.getIntegrationAuthById(integrationAuthId), queryKey: integrationAuthKeys.getIntegrationAuthById(integrationAuthId),
@@ -29,10 +46,28 @@ export const useGetIntegrationAuthById = (integrationAuthId: string) => {
}); });
} }
export const useGetIntegrationAuthApps = (integrationAuthId: string) => { // TODO: fix to teamId
export const useGetIntegrationAuthApps = ({
integrationAuthId,
teamId
}: {
integrationAuthId: string;
teamId?: string;
}) => {
return useQuery({ return useQuery({
queryKey: integrationAuthKeys.getIntegrationAuthApps(integrationAuthId), queryKey: integrationAuthKeys.getIntegrationAuthApps(integrationAuthId, teamId),
queryFn: () => fetchIntegrationAuthApps(integrationAuthId), queryFn: () => fetchIntegrationAuthApps({
integrationAuthId,
teamId
}),
enabled: true
});
}
export const useGetIntegrationAuthTeams = (integrationAuthId: string) => {
return useQuery({
queryKey: integrationAuthKeys.getIntegrationAuthTeams(integrationAuthId),
queryFn: () => fetchIntegrationAuthTeams(integrationAuthId),
enabled: true enabled: true
}); });
} }

View File

@@ -10,4 +10,9 @@ export type App = {
name: string; name: string;
appId?: string; appId?: string;
owner?: string; owner?: string;
}
export type Team = {
name: string;
teamId: string;
} }

View File

@@ -196,16 +196,16 @@ export default function Integrations() {
link = `https://gitlab.com/oauth/authorize?client_id=${integrationOption.clientId}&redirect_uri=${window.location.origin}/integrations/gitlab/oauth2/callback&response_type=code&state=${state}`; link = `https://gitlab.com/oauth/authorize?client_id=${integrationOption.clientId}&redirect_uri=${window.location.origin}/integrations/gitlab/oauth2/callback&response_type=code&state=${state}`;
break; break;
case 'render': case 'render':
link = `${window.location.origin}/integrations/render/authorize` link = `${window.location.origin}/integrations/render/authorize`;
break; break;
case 'flyio': case 'flyio':
link = `${window.location.origin}/integrations/flyio/authorize` link = `${window.location.origin}/integrations/flyio/authorize`;
break; break;
case 'circleci': case 'circleci':
link = `${window.location.origin}/integrations/circleci/authorize` link = `${window.location.origin}/integrations/circleci/authorize`;
break; break;
case 'travisci': case 'travisci':
link = `${window.location.origin}/integrations/travisci/authorize` link = `${window.location.origin}/integrations/travisci/authorize`;
break; break;
default: default:
break; break;

View File

@@ -22,7 +22,9 @@ export default function CircleCICreateIntegrationPage() {
const { data: workspace } = useGetWorkspaceById(localStorage.getItem('projectData.id') ?? ''); const { data: workspace } = useGetWorkspaceById(localStorage.getItem('projectData.id') ?? '');
const { data: integrationAuth } = useGetIntegrationAuthById(integrationAuthId as string ?? ''); const { data: integrationAuth } = useGetIntegrationAuthById(integrationAuthId as string ?? '');
const { data: integrationAuthApps } = useGetIntegrationAuthApps(integrationAuthId as string ?? ''); const { data: integrationAuthApps } = useGetIntegrationAuthApps({
integrationAuthId: integrationAuthId as string ?? ''
});
const [selectedSourceEnvironment, setSelectedSourceEnvironment] = useState(''); const [selectedSourceEnvironment, setSelectedSourceEnvironment] = useState('');
const [targetApp, setTargetApp] = useState(''); const [targetApp, setTargetApp] = useState('');
@@ -36,9 +38,12 @@ export default function CircleCICreateIntegrationPage() {
}, [workspace]); }, [workspace]);
useEffect(() => { useEffect(() => {
// TODO: handle case where apps can be empty
if (integrationAuthApps) { if (integrationAuthApps) {
setTargetApp(integrationAuthApps[0]?.name); if (integrationAuthApps.length > 0 ) {
setTargetApp(integrationAuthApps[0]?.name);
} else {
setTargetApp('none');
}
} }
}, [integrationAuthApps]); }, [integrationAuthApps]);
@@ -84,7 +89,7 @@ export default function CircleCICreateIntegrationPage() {
className='w-full border border-mineshaft-500' className='w-full border border-mineshaft-500'
> >
{workspace?.environments.map((sourceEnvironment) => ( {workspace?.environments.map((sourceEnvironment) => (
<SelectItem value={sourceEnvironment.slug} key={`azure-key-vault-environment-${sourceEnvironment.slug}`}> <SelectItem value={sourceEnvironment.slug} key={`source-environment-${sourceEnvironment.slug}`}>
{sourceEnvironment.name} {sourceEnvironment.name}
</SelectItem> </SelectItem>
))} ))}
@@ -98,19 +103,27 @@ export default function CircleCICreateIntegrationPage() {
value={targetApp} value={targetApp}
onValueChange={(val) => setTargetApp(val)} onValueChange={(val) => setTargetApp(val)}
className='w-full border border-mineshaft-500' className='w-full border border-mineshaft-500'
isDisabled={integrationAuthApps.length === 0}
> >
{integrationAuthApps.map((integrationAuthApp) => ( {integrationAuthApps.length > 0 ? (
<SelectItem value={integrationAuthApp.name} key={`render-environment-${integrationAuthApp.name}`}> integrationAuthApps.map((integrationAuthApp) => (
{integrationAuthApp.name} <SelectItem value={integrationAuthApp.name} key={`target-app-${integrationAuthApp.name}`}>
{integrationAuthApp.name}
</SelectItem>
))
) : (
<SelectItem value="none" key="target-app-none">
No projects found
</SelectItem> </SelectItem>
))} )}
</Select> </Select>
</FormControl> </FormControl>
<Button <Button
onClick={handleButtonClick} onClick={handleButtonClick}
color="mineshaft" color="mineshaft"
className='mt-4' className='mt-4'
isLoading={isLoading} isLoading={isLoading}
isDisabled={integrationAuthApps.length === 0}
> >
Create Integration Create Integration
</Button> </Button>

View File

@@ -22,7 +22,9 @@ export default function FlyioCreateIntegrationPage() {
const { data: workspace } = useGetWorkspaceById(localStorage.getItem('projectData.id') ?? ''); const { data: workspace } = useGetWorkspaceById(localStorage.getItem('projectData.id') ?? '');
const { data: integrationAuth } = useGetIntegrationAuthById(integrationAuthId as string ?? ''); const { data: integrationAuth } = useGetIntegrationAuthById(integrationAuthId as string ?? '');
const { data: integrationAuthApps } = useGetIntegrationAuthApps(integrationAuthId as string ?? ''); const { data: integrationAuthApps } = useGetIntegrationAuthApps({
integrationAuthId: integrationAuthId as string ?? ''
});
const [selectedSourceEnvironment, setSelectedSourceEnvironment] = useState(''); const [selectedSourceEnvironment, setSelectedSourceEnvironment] = useState('');
const [targetApp, setTargetApp] = useState(''); const [targetApp, setTargetApp] = useState('');
@@ -31,14 +33,18 @@ export default function FlyioCreateIntegrationPage() {
useEffect(() => { useEffect(() => {
if (workspace) { if (workspace) {
setSelectedSourceEnvironment(workspace.environments[0].slug); setSelectedSourceEnvironment(workspace.environments[0].slug);
} }
}, [workspace]); }, [workspace]);
useEffect(() => { useEffect(() => {
// TODO: handle case where apps can be empty // TODO: handle case where apps can be empty
if (integrationAuthApps) { if (integrationAuthApps) {
if (integrationAuthApps.length > 0) {
setTargetApp(integrationAuthApps[0].name); setTargetApp(integrationAuthApps[0].name);
} else {
setTargetApp('none');
}
} }
}, [integrationAuthApps]); }, [integrationAuthApps]);
@@ -84,7 +90,7 @@ export default function FlyioCreateIntegrationPage() {
className='w-full border border-mineshaft-500' className='w-full border border-mineshaft-500'
> >
{workspace?.environments.map((sourceEnvironment) => ( {workspace?.environments.map((sourceEnvironment) => (
<SelectItem value={sourceEnvironment.slug} key={`flyio-environment-${sourceEnvironment.slug}`}> <SelectItem value={sourceEnvironment.slug} key={`source-environment-${sourceEnvironment.slug}`}>
{sourceEnvironment.name} {sourceEnvironment.name}
</SelectItem> </SelectItem>
))} ))}
@@ -98,12 +104,19 @@ export default function FlyioCreateIntegrationPage() {
value={targetApp} value={targetApp}
onValueChange={(val) => setTargetApp(val)} onValueChange={(val) => setTargetApp(val)}
className='w-full border border-mineshaft-500' className='w-full border border-mineshaft-500'
isDisabled={integrationAuthApps.length === 0}
> >
{integrationAuthApps.map((integrationAuthApp) => ( {integrationAuthApps.length > 0 ? (
<SelectItem value={integrationAuthApp.name} key={`render-environment-${integrationAuthApp.name}`}> integrationAuthApps.map((integrationAuthApp) => (
{integrationAuthApp.name} <SelectItem value={integrationAuthApp.name} key={`target-app-${integrationAuthApp.name}`}>
{integrationAuthApp.name}
</SelectItem>
))
) : (
<SelectItem value="none" key="target-app-none">
No apps found
</SelectItem> </SelectItem>
))} )}
</Select> </Select>
</FormControl> </FormControl>
<Button <Button
@@ -111,6 +124,7 @@ export default function FlyioCreateIntegrationPage() {
color="mineshaft" color="mineshaft"
className='mt-4' className='mt-4'
isLoading={isLoading} isLoading={isLoading}
isDisabled={integrationAuthApps.length === 0}
> >
Create Integration Create Integration
</Button> </Button>

View File

@@ -22,7 +22,9 @@ export default function GitHubCreateIntegrationPage() {
const { data: workspace } = useGetWorkspaceById(localStorage.getItem('projectData.id') ?? ''); const { data: workspace } = useGetWorkspaceById(localStorage.getItem('projectData.id') ?? '');
const { data: integrationAuth } = useGetIntegrationAuthById(integrationAuthId as string ?? ''); const { data: integrationAuth } = useGetIntegrationAuthById(integrationAuthId as string ?? '');
const { data: integrationAuthApps } = useGetIntegrationAuthApps(integrationAuthId as string ?? ''); const { data: integrationAuthApps } = useGetIntegrationAuthApps({
integrationAuthId: integrationAuthId as string ?? ''
});
const [selectedSourceEnvironment, setSelectedSourceEnvironment] = useState(''); const [selectedSourceEnvironment, setSelectedSourceEnvironment] = useState('');
const [owner, setOwner] = useState<string | null>(null); const [owner, setOwner] = useState<string | null>(null);
@@ -37,10 +39,13 @@ export default function GitHubCreateIntegrationPage() {
}, [workspace]); }, [workspace]);
useEffect(() => { useEffect(() => {
// TODO: handle case where apps can be empty
if (integrationAuthApps) { if (integrationAuthApps) {
if (integrationAuthApps.length > 0) {
setTargetApp(integrationAuthApps[0].name); setTargetApp(integrationAuthApps[0].name);
setOwner(integrationAuthApps[0]?.owner ?? null); setOwner(integrationAuthApps[0]?.owner ?? null);
} else {
setTargetApp('none');
}
} }
}, [integrationAuthApps]); }, [integrationAuthApps]);
@@ -99,21 +104,29 @@ export default function GitHubCreateIntegrationPage() {
value={targetApp} value={targetApp}
onValueChange={(val) => setTargetApp(val)} onValueChange={(val) => setTargetApp(val)}
className='w-full border border-mineshaft-500' className='w-full border border-mineshaft-500'
isDisabled={integrationAuthApps.length === 0}
> >
{integrationAuthApps.map((integrationAuthApp) => ( {integrationAuthApps.length > 0 ? (
<SelectItem value={integrationAuthApp.name} key={`github-environment-${integrationAuthApp.name}`}> integrationAuthApps.map((integrationAuthApp) => (
{integrationAuthApp.name} <SelectItem value={integrationAuthApp.name} key={`github-environment-${integrationAuthApp.name}`}>
{integrationAuthApp.name}
</SelectItem>
))
) : (
<SelectItem value="none" key="target-app-none">
No repositories found
</SelectItem> </SelectItem>
))} )}
</Select> </Select>
</FormControl> </FormControl>
<Button <Button
onClick={handleButtonClick} onClick={handleButtonClick}
color="mineshaft" color="mineshaft"
className='mt-4' className='mt-4'
isLoading={isLoading} isLoading={isLoading}
isDisabled={integrationAuthApps.length === 0}
> >
Create Integration Create Integration
</Button> </Button>
</Card> </Card>
</div> </div>

View File

@@ -11,10 +11,18 @@ import {
Select, Select,
SelectItem SelectItem
} from '../../../components/v2'; } from '../../../components/v2';
import { useGetIntegrationAuthApps,useGetIntegrationAuthById } from '../../../hooks/api/integrationAuth'; import {
useGetIntegrationAuthApps,
useGetIntegrationAuthById,
useGetIntegrationAuthTeams} from '../../../hooks/api/integrationAuth';
import { useGetWorkspaceById } from '../../../hooks/api/workspace'; import { useGetWorkspaceById } from '../../../hooks/api/workspace';
import createIntegration from "../../api/integrations/createIntegration"; import createIntegration from "../../api/integrations/createIntegration";
const gitLabEntities = [
{ name: 'Individual', value: 'individual' },
{ name: 'Group', value: 'group' }
]
export default function GitLabCreateIntegrationPage() { export default function GitLabCreateIntegrationPage() {
const router = useRouter(); const router = useRouter();
@@ -22,28 +30,52 @@ export default function GitLabCreateIntegrationPage() {
const { data: workspace } = useGetWorkspaceById(localStorage.getItem('projectData.id') ?? ''); const { data: workspace } = useGetWorkspaceById(localStorage.getItem('projectData.id') ?? '');
const { data: integrationAuth } = useGetIntegrationAuthById(integrationAuthId as string ?? ''); const { data: integrationAuth } = useGetIntegrationAuthById(integrationAuthId as string ?? '');
const { data: integrationAuthApps } = useGetIntegrationAuthApps(integrationAuthId as string ?? '');
const [targetTeamId, setTargetTeamId] = useState<string | null>(null);
const { data: integrationAuthApps } = useGetIntegrationAuthApps({
integrationAuthId: integrationAuthId as string ?? '',
...(targetTeamId ? { teamId: targetTeamId } : {})
});
const { data: integrationAuthTeams } = useGetIntegrationAuthTeams(integrationAuthId as string ?? '');
const [targetEntity, setTargetEntity] = useState(gitLabEntities[0].value);
const [selectedSourceEnvironment, setSelectedSourceEnvironment] = useState(''); const [selectedSourceEnvironment, setSelectedSourceEnvironment] = useState('');
const [owner, setOwner] = useState<string | null>(null); const [targetAppId, setTargetAppId] = useState('');
const [targetApp, setTargetApp] = useState('');
const [isLoading, setIsLoading] = useState(false); const [isLoading, setIsLoading] = useState(false);
useEffect(() => { useEffect(() => {
if (workspace) { if (workspace) {
setSelectedSourceEnvironment(workspace.environments[0].slug); setSelectedSourceEnvironment(workspace.environments[0].slug);
} }
}, [workspace]); }, [workspace]);
useEffect(() => { useEffect(() => {
// TODO: handle case where apps can be empty if (integrationAuthApps) {
if (integrationAuthApps) { if (integrationAuthApps.length > 0) {
setTargetApp(integrationAuthApps[0].name); setTargetAppId(integrationAuthApps[0].appId as string);
setOwner(integrationAuthApps[0]?.owner ?? null); } else {
setTargetAppId('none');
} }
}
}, [integrationAuthApps]); }, [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
setTargetTeamId(integrationAuthTeams[0].teamId);
} else {
// case: user is not part of any groups in GitLab
setTargetTeamId('none');
}
}
} else if (targetEntity === 'individual') {
setTargetTeamId(null);
}
}, [targetEntity, integrationAuthTeams]);
const handleButtonClick = async () => { const handleButtonClick = async () => {
try { try {
@@ -53,11 +85,11 @@ export default function GitLabCreateIntegrationPage() {
await createIntegration({ await createIntegration({
integrationAuthId: integrationAuth?._id, integrationAuthId: integrationAuth?._id,
isActive: true, isActive: true,
app: targetApp, app: (integrationAuthApps?.find((integrationAuthApp) => integrationAuthApp.appId === targetAppId))?.name ?? null,
appId: (integrationAuthApps?.find((integrationAuthApp) => integrationAuthApp.name === targetApp))?.appId ?? null, appId: targetAppId,
sourceEnvironment: selectedSourceEnvironment, sourceEnvironment: selectedSourceEnvironment,
targetEnvironment: null, targetEnvironment: null,
owner, owner: null,
path: null, path: null,
region: null region: null
}); });
@@ -71,7 +103,7 @@ export default function GitLabCreateIntegrationPage() {
} }
} }
return (integrationAuth && workspace && selectedSourceEnvironment && integrationAuthApps && targetApp) ? ( return (integrationAuth && workspace && selectedSourceEnvironment && integrationAuthApps && integrationAuthTeams && targetAppId) ? (
<div className="h-full w-full flex justify-center items-center"> <div className="h-full w-full flex justify-center items-center">
<Card className="max-w-md p-8 rounded-md"> <Card className="max-w-md p-8 rounded-md">
<CardTitle className='text-center'>GitLab Integration</CardTitle> <CardTitle className='text-center'>GitLab Integration</CardTitle>
@@ -85,33 +117,83 @@ export default function GitLabCreateIntegrationPage() {
className='w-full border border-mineshaft-500' className='w-full border border-mineshaft-500'
> >
{workspace?.environments.map((sourceEnvironment) => ( {workspace?.environments.map((sourceEnvironment) => (
<SelectItem value={sourceEnvironment.slug} key={`azure-key-vault-environment-${sourceEnvironment.slug}`}> <SelectItem value={sourceEnvironment.slug} key={`source-environment-${sourceEnvironment.slug}`}>
{sourceEnvironment.name} {sourceEnvironment.name}
</SelectItem> </SelectItem>
))} ))}
</Select> </Select>
</FormControl> </FormControl>
<FormControl <FormControl
label="GitLab Repo" label="GitLab Integration Type"
className='mt-4' className='mt-4'
> >
<Select <Select
value={targetApp} value={targetEntity}
onValueChange={(val) => setTargetApp(val)} onValueChange={(val) => setTargetEntity(val)}
className='w-full border border-mineshaft-500' className='w-full border border-mineshaft-500'
> >
{integrationAuthApps.map((integrationAuthApp) => ( {gitLabEntities.map((entity) => {
<SelectItem value={integrationAuthApp.name} key={`gitlab-environment-${integrationAuthApp.name}`}> return (
{integrationAuthApp.name} <SelectItem value={entity.value} key={`target-entity-${entity.value}`}>
{entity.name}
</SelectItem>
);
})}
</Select>
</FormControl>
{targetEntity === 'group' && targetTeamId && (
<FormControl
label="GitLab Group"
className='mt-4'
>
<Select
value={targetTeamId}
onValueChange={(val) => setTargetTeamId(val)}
className='w-full border border-mineshaft-500'
>
{integrationAuthTeams.length > 0 ? (
integrationAuthTeams.map((integrationAuthTeam) => (
<SelectItem value={integrationAuthTeam.teamId} key={`target-team-${integrationAuthTeam.teamId}`}>
{integrationAuthTeam.name}
</SelectItem>
))
) : (
<SelectItem value="none" key="target-team-none">
No groups found
</SelectItem>
)}
</Select>
</FormControl>
)}
<FormControl
label="GitLab Project"
className='mt-4'
>
<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}`}>
{integrationAuthApp.name}
</SelectItem>
))
) : (
<SelectItem value="none" key="target-app-none">
No projects found
</SelectItem> </SelectItem>
))} )}
</Select> </Select>
</FormControl> </FormControl>
<Button <Button
onClick={handleButtonClick} onClick={handleButtonClick}
color="mineshaft" color="mineshaft"
className='mt-4' className='mt-4'
isLoading={isLoading} isLoading={isLoading}
isDisabled={integrationAuthApps.length === 0}
> >
Create Integration Create Integration
</Button> </Button>

View File

@@ -22,7 +22,9 @@ export default function HerokuCreateIntegrationPage() {
const { data: workspace } = useGetWorkspaceById(localStorage.getItem('projectData.id') ?? ''); const { data: workspace } = useGetWorkspaceById(localStorage.getItem('projectData.id') ?? '');
const { data: integrationAuth } = useGetIntegrationAuthById(integrationAuthId as string ?? ''); const { data: integrationAuth } = useGetIntegrationAuthById(integrationAuthId as string ?? '');
const { data: integrationAuthApps } = useGetIntegrationAuthApps(integrationAuthId as string ?? ''); const { data: integrationAuthApps } = useGetIntegrationAuthApps({
integrationAuthId: integrationAuthId as string ?? ''
});
const [selectedSourceEnvironment, setSelectedSourceEnvironment] = useState(''); const [selectedSourceEnvironment, setSelectedSourceEnvironment] = useState('');
const [targetApp, setTargetApp] = useState(''); const [targetApp, setTargetApp] = useState('');
@@ -36,9 +38,12 @@ export default function HerokuCreateIntegrationPage() {
}, [workspace]); }, [workspace]);
useEffect(() => { useEffect(() => {
// TODO: handle case where apps can be empty
if (integrationAuthApps) { if (integrationAuthApps) {
if (integrationAuthApps.length > 0) {
setTargetApp(integrationAuthApps[0].name); setTargetApp(integrationAuthApps[0].name);
} else {
setTargetApp('none');
}
} }
}, [integrationAuthApps]); }, [integrationAuthApps]);
@@ -83,7 +88,7 @@ export default function HerokuCreateIntegrationPage() {
className='w-full border border-mineshaft-500' className='w-full border border-mineshaft-500'
> >
{workspace?.environments.map((sourceEnvironment) => ( {workspace?.environments.map((sourceEnvironment) => (
<SelectItem value={sourceEnvironment.slug} key={`azure-key-vault-environment-${sourceEnvironment.slug}`}> <SelectItem value={sourceEnvironment.slug} key={`source-environment-${sourceEnvironment.slug}`}>
{sourceEnvironment.name} {sourceEnvironment.name}
</SelectItem> </SelectItem>
))} ))}
@@ -97,19 +102,27 @@ export default function HerokuCreateIntegrationPage() {
value={targetApp} value={targetApp}
onValueChange={(val) => setTargetApp(val)} onValueChange={(val) => setTargetApp(val)}
className='w-full border border-mineshaft-500' className='w-full border border-mineshaft-500'
isDisabled={integrationAuthApps.length === 0}
> >
{integrationAuthApps.map((integrationAuthApp) => ( {integrationAuthApps.length > 0 ? (
<SelectItem value={integrationAuthApp.name} key={`heroku-environment-${integrationAuthApp.name}`}> integrationAuthApps.map((integrationAuthApp) => (
{integrationAuthApp.name} <SelectItem value={integrationAuthApp.name} key={`target-app-${integrationAuthApp.name}`}>
{integrationAuthApp.name}
</SelectItem>
))
) : (
<SelectItem value="none" key="target-app-none">
No apps found
</SelectItem> </SelectItem>
))} )}
</Select> </Select>
</FormControl> </FormControl>
<Button <Button
onClick={handleButtonClick} onClick={handleButtonClick}
color="mineshaft" color="mineshaft"
className='mt-4' className='mt-4'
isLoading={isLoading} isLoading={isLoading}
isDisabled={integrationAuthApps.length === 0}
> >
Create Integration Create Integration
</Button> </Button>

View File

@@ -29,7 +29,9 @@ export default function NetlifyCreateIntegrationPage() {
const { data: workspace } = useGetWorkspaceById(localStorage.getItem('projectData.id') ?? ''); const { data: workspace } = useGetWorkspaceById(localStorage.getItem('projectData.id') ?? '');
const { data: integrationAuth } = useGetIntegrationAuthById(integrationAuthId as string ?? ''); const { data: integrationAuth } = useGetIntegrationAuthById(integrationAuthId as string ?? '');
const { data: integrationAuthApps } = useGetIntegrationAuthApps(integrationAuthId as string ?? ''); const { data: integrationAuthApps } = useGetIntegrationAuthApps({
integrationAuthId: integrationAuthId as string ?? ''
});
const [selectedSourceEnvironment, setSelectedSourceEnvironment] = useState(''); const [selectedSourceEnvironment, setSelectedSourceEnvironment] = useState('');
const [targetApp, setTargetApp] = useState(''); const [targetApp, setTargetApp] = useState('');
@@ -45,10 +47,13 @@ export default function NetlifyCreateIntegrationPage() {
}, [workspace]); }, [workspace]);
useEffect(() => { useEffect(() => {
// TODO: handle case where apps can be empty if (integrationAuthApps) {
if (integrationAuthApps) { if (integrationAuthApps.length > 0) {
setTargetApp(integrationAuthApps[0].name); setTargetApp(integrationAuthApps[0].name);
} else {
setTargetApp('none');
} }
}
}, [integrationAuthApps]); }, [integrationAuthApps]);
const handleButtonClick = async () => { const handleButtonClick = async () => {
@@ -91,7 +96,7 @@ export default function NetlifyCreateIntegrationPage() {
className='w-full border border-mineshaft-500' className='w-full border border-mineshaft-500'
> >
{workspace?.environments.map((sourceEnvironment) => ( {workspace?.environments.map((sourceEnvironment) => (
<SelectItem value={sourceEnvironment.slug} key={`azure-key-vault-environment-${sourceEnvironment.slug}`}> <SelectItem value={sourceEnvironment.slug} key={`source-environment-${sourceEnvironment.slug}`}>
{sourceEnvironment.name} {sourceEnvironment.name}
</SelectItem> </SelectItem>
))} ))}
@@ -104,12 +109,19 @@ export default function NetlifyCreateIntegrationPage() {
value={targetApp} value={targetApp}
onValueChange={(val) => setTargetApp(val)} onValueChange={(val) => setTargetApp(val)}
className='w-full border border-mineshaft-500' className='w-full border border-mineshaft-500'
isDisabled={integrationAuthApps.length === 0}
> >
{integrationAuthApps.map((integrationAuthApp) => ( {integrationAuthApps.length > 0 ? (
<SelectItem value={integrationAuthApp.name} key={`heroku-environment-${integrationAuthApp.name}`}> integrationAuthApps.map((integrationAuthApp) => (
{integrationAuthApp.name} <SelectItem value={integrationAuthApp.name} key={`target-app-${integrationAuthApp.name}`}>
{integrationAuthApp.name}
</SelectItem>
))
) : (
<SelectItem value="none" key="target-app-none">
No sites found
</SelectItem> </SelectItem>
))} )}
</Select> </Select>
</FormControl> </FormControl>
<FormControl <FormControl
@@ -121,17 +133,18 @@ export default function NetlifyCreateIntegrationPage() {
className='w-full border border-mineshaft-500' className='w-full border border-mineshaft-500'
> >
{netlifyEnvironments.map((netlifyEnvironment) => ( {netlifyEnvironments.map((netlifyEnvironment) => (
<SelectItem value={netlifyEnvironment.slug} key={`netlify-environment-${netlifyEnvironment.slug}`}> <SelectItem value={netlifyEnvironment.slug} key={`target-environment-${netlifyEnvironment.slug}`}>
{netlifyEnvironment.name} {netlifyEnvironment.name}
</SelectItem> </SelectItem>
))} ))}
</Select> </Select>
</FormControl> </FormControl>
<Button <Button
onClick={handleButtonClick} onClick={handleButtonClick}
color="mineshaft" color="mineshaft"
className='mt-4' className='mt-4'
isLoading={isLoading} isLoading={isLoading}
isDisabled={integrationAuthApps.length === 0}
> >
Create Integration Create Integration
</Button> </Button>

View File

@@ -22,7 +22,9 @@ export default function RenderCreateIntegrationPage() {
const { data: workspace } = useGetWorkspaceById(localStorage.getItem('projectData.id') ?? ''); const { data: workspace } = useGetWorkspaceById(localStorage.getItem('projectData.id') ?? '');
const { data: integrationAuth } = useGetIntegrationAuthById(integrationAuthId as string ?? ''); const { data: integrationAuth } = useGetIntegrationAuthById(integrationAuthId as string ?? '');
const { data: integrationAuthApps } = useGetIntegrationAuthApps(integrationAuthId as string ?? ''); const { data: integrationAuthApps } = useGetIntegrationAuthApps({
integrationAuthId: integrationAuthId as string ?? ''
});
const [selectedSourceEnvironment, setSelectedSourceEnvironment] = useState(''); const [selectedSourceEnvironment, setSelectedSourceEnvironment] = useState('');
const [targetApp, setTargetApp] = useState(''); const [targetApp, setTargetApp] = useState('');
@@ -38,7 +40,11 @@ export default function RenderCreateIntegrationPage() {
useEffect(() => { useEffect(() => {
// TODO: handle case where apps can be empty // TODO: handle case where apps can be empty
if (integrationAuthApps) { if (integrationAuthApps) {
setTargetApp(integrationAuthApps[0].name); if (integrationAuthApps.length > 0) {
setTargetApp(integrationAuthApps[0].name);
} else {
setTargetApp('none');
}
} }
}, [integrationAuthApps]); }, [integrationAuthApps]);
@@ -84,7 +90,7 @@ export default function RenderCreateIntegrationPage() {
className='w-full border border-mineshaft-500' className='w-full border border-mineshaft-500'
> >
{workspace?.environments.map((sourceEnvironment) => ( {workspace?.environments.map((sourceEnvironment) => (
<SelectItem value={sourceEnvironment.slug} key={`azure-key-vault-environment-${sourceEnvironment.slug}`}> <SelectItem value={sourceEnvironment.slug} key={`source-environment-${sourceEnvironment.slug}`}>
{sourceEnvironment.name} {sourceEnvironment.name}
</SelectItem> </SelectItem>
))} ))}
@@ -98,19 +104,27 @@ export default function RenderCreateIntegrationPage() {
value={targetApp} value={targetApp}
onValueChange={(val) => setTargetApp(val)} onValueChange={(val) => setTargetApp(val)}
className='w-full border border-mineshaft-500' className='w-full border border-mineshaft-500'
isDisabled={integrationAuthApps.length === 0}
> >
{integrationAuthApps.map((integrationAuthApp) => ( {integrationAuthApps.length > 0 ? (
<SelectItem value={integrationAuthApp.name} key={`render-environment-${integrationAuthApp.name}`}> integrationAuthApps.map((integrationAuthApp) => (
{integrationAuthApp.name} <SelectItem value={integrationAuthApp.name} key={`target-app-${integrationAuthApp.name}`}>
{integrationAuthApp.name}
</SelectItem>
))
) : (
<SelectItem value="none" key="target-app-none">
No services found
</SelectItem> </SelectItem>
))} )}
</Select> </Select>
</FormControl> </FormControl>
<Button <Button
onClick={handleButtonClick} onClick={handleButtonClick}
color="mineshaft" color="mineshaft"
className='mt-4' className='mt-4'
isLoading={isLoading} isLoading={isLoading}
isDisabled={integrationAuthApps.length === 0}
> >
Create Integration Create Integration
</Button> </Button>

View File

@@ -22,7 +22,9 @@ export default function TravisCICreateIntegrationPage() {
const { data: workspace } = useGetWorkspaceById(localStorage.getItem('projectData.id') ?? ''); const { data: workspace } = useGetWorkspaceById(localStorage.getItem('projectData.id') ?? '');
const { data: integrationAuth } = useGetIntegrationAuthById(integrationAuthId as string ?? ''); const { data: integrationAuth } = useGetIntegrationAuthById(integrationAuthId as string ?? '');
const { data: integrationAuthApps } = useGetIntegrationAuthApps(integrationAuthId as string ?? ''); const { data: integrationAuthApps } = useGetIntegrationAuthApps({
integrationAuthId: integrationAuthId as string ?? ''
});
const [selectedSourceEnvironment, setSelectedSourceEnvironment] = useState(''); const [selectedSourceEnvironment, setSelectedSourceEnvironment] = useState('');
const [targetApp, setTargetApp] = useState(''); const [targetApp, setTargetApp] = useState('');
@@ -36,10 +38,13 @@ export default function TravisCICreateIntegrationPage() {
}, [workspace]); }, [workspace]);
useEffect(() => { useEffect(() => {
// TODO: handle case where apps can be empty if (integrationAuthApps) {
if (integrationAuthApps) { if (integrationAuthApps.length > 0) {
setTargetApp(integrationAuthApps[0]?.name); setTargetApp(integrationAuthApps[0].name);
} else {
setTargetApp('none');
} }
}
}, [integrationAuthApps]); }, [integrationAuthApps]);
const handleButtonClick = async () => { const handleButtonClick = async () => {
@@ -84,7 +89,7 @@ export default function TravisCICreateIntegrationPage() {
className='w-full border border-mineshaft-500' className='w-full border border-mineshaft-500'
> >
{workspace?.environments.map((sourceEnvironment) => ( {workspace?.environments.map((sourceEnvironment) => (
<SelectItem value={sourceEnvironment.slug} key={`azure-key-vault-environment-${sourceEnvironment.slug}`}> <SelectItem value={sourceEnvironment.slug} key={`source-environment-${sourceEnvironment.slug}`}>
{sourceEnvironment.name} {sourceEnvironment.name}
</SelectItem> </SelectItem>
))} ))}
@@ -98,19 +103,27 @@ export default function TravisCICreateIntegrationPage() {
value={targetApp} value={targetApp}
onValueChange={(val) => setTargetApp(val)} onValueChange={(val) => setTargetApp(val)}
className='w-full border border-mineshaft-500' className='w-full border border-mineshaft-500'
isDisabled={integrationAuthApps.length === 0}
> >
{integrationAuthApps.map((integrationAuthApp) => ( {integrationAuthApps.length > 0 ? (
<SelectItem value={integrationAuthApp.name} key={`render-environment-${integrationAuthApp.name}`}> integrationAuthApps.map((integrationAuthApp) => (
{integrationAuthApp.name} <SelectItem value={integrationAuthApp.name} key={`target-environment-${integrationAuthApp.name}`}>
{integrationAuthApp.name}
</SelectItem>
))
) : (
<SelectItem value="none" key="target-app-none">
No projects found
</SelectItem> </SelectItem>
))} )}
</Select> </Select>
</FormControl> </FormControl>
<Button <Button
onClick={handleButtonClick} onClick={handleButtonClick}
color="mineshaft" color="mineshaft"
className='mt-4' className='mt-4'
isLoading={isLoading} isLoading={isLoading}
isDisabled={integrationAuthApps.length === 0}
> >
Create Integration Create Integration
</Button> </Button>

View File

@@ -28,11 +28,13 @@ export default function VercelCreateIntegrationPage() {
const { data: workspace } = useGetWorkspaceById(localStorage.getItem('projectData.id') ?? ''); const { data: workspace } = useGetWorkspaceById(localStorage.getItem('projectData.id') ?? '');
const { data: integrationAuth } = useGetIntegrationAuthById(integrationAuthId as string ?? ''); const { data: integrationAuth } = useGetIntegrationAuthById(integrationAuthId as string ?? '');
const { data: integrationAuthApps } = useGetIntegrationAuthApps(integrationAuthId as string ?? ''); const { data: integrationAuthApps } = useGetIntegrationAuthApps({
integrationAuthId: integrationAuthId as string ?? ''
});
const [selectedSourceEnvironment, setSelectedSourceEnvironment] = useState(''); const [selectedSourceEnvironment, setSelectedSourceEnvironment] = useState('');
const [targetApp, setTargetApp] = useState(''); const [targetApp, setTargetApp] = useState('');
const [targetEnvironment, setTargetEnvironemnt] = useState(''); const [targetEnvironment, setTargetEnvironment] = useState('');
const [isLoading, setIsLoading] = useState(false); const [isLoading, setIsLoading] = useState(false);
@@ -43,10 +45,14 @@ export default function VercelCreateIntegrationPage() {
}, [workspace]); }, [workspace]);
useEffect(() => { useEffect(() => {
// TODO: handle case where apps can be empty
if (integrationAuthApps) { if (integrationAuthApps) {
if (integrationAuthApps.length > 0) {
setTargetApp(integrationAuthApps[0].name); setTargetApp(integrationAuthApps[0].name);
setTargetEnvironemnt(vercelEnvironments[0].slug); setTargetEnvironment(vercelEnvironments[0].slug);
} else {
setTargetApp('none');
setTargetEnvironment(vercelEnvironments[0].slug);
}
} }
}, [integrationAuthApps]); }, [integrationAuthApps]);
@@ -103,12 +109,19 @@ export default function VercelCreateIntegrationPage() {
value={targetApp} value={targetApp}
onValueChange={(val) => setTargetApp(val)} onValueChange={(val) => setTargetApp(val)}
className='w-full border border-mineshaft-500' className='w-full border border-mineshaft-500'
isDisabled={integrationAuthApps.length === 0}
> >
{integrationAuthApps.map((integrationAuthApp) => ( {integrationAuthApps.length > 0 ? (
<SelectItem value={integrationAuthApp.name} key={`heroku-environment-${integrationAuthApp.name}`}> integrationAuthApps.map((integrationAuthApp) => (
{integrationAuthApp.name} <SelectItem value={integrationAuthApp.name} key={`target-app-${integrationAuthApp.name}`}>
{integrationAuthApp.name}
</SelectItem>
))
) : (
<SelectItem value="none" key="target-app-none">
No projects found
</SelectItem> </SelectItem>
))} )}
</Select> </Select>
</FormControl> </FormControl>
<FormControl <FormControl
@@ -116,21 +129,22 @@ export default function VercelCreateIntegrationPage() {
> >
<Select <Select
value={targetEnvironment} value={targetEnvironment}
onValueChange={(val) => setTargetEnvironemnt(val)} onValueChange={(val) => setTargetEnvironment(val)}
className='w-full border border-mineshaft-500' className='w-full border border-mineshaft-500'
> >
{vercelEnvironments.map((vercelEnvironment) => ( {vercelEnvironments.map((vercelEnvironment) => (
<SelectItem value={vercelEnvironment.slug} key={`vercel-environment-${vercelEnvironment.slug}`}> <SelectItem value={vercelEnvironment.slug} key={`target-environment-${vercelEnvironment.slug}`}>
{vercelEnvironment.name} {vercelEnvironment.name}
</SelectItem> </SelectItem>
))} ))}
</Select> </Select>
</FormControl> </FormControl>
<Button <Button
onClick={handleButtonClick} onClick={handleButtonClick}
color="mineshaft" color="mineshaft"
className='mt-4' className='mt-4'
isLoading={isLoading} isLoading={isLoading}
isDisabled={integrationAuthApps.length === 0}
> >
Create Integration Create Integration
</Button> </Button>