From 5ba851adffb034049e774e883e67a045a5ef077a Mon Sep 17 00:00:00 2001 From: Aashish-Upadhyay-101 Date: Sat, 4 Feb 2023 15:28:04 +0545 Subject: [PATCH 01/21] circleci-integration-setup --- backend/src/integrations/apps.ts | 199 +++++++++-------- backend/src/models/integration.ts | 55 +++-- backend/src/models/integrationAuth.ts | 49 +++-- backend/src/variables/index.ts | 30 ++- backend/src/variables/integration.ts | 296 +++++++++++++------------- 5 files changed, 321 insertions(+), 308 deletions(-) diff --git a/backend/src/integrations/apps.ts b/backend/src/integrations/apps.ts index d1252954e..d36c9c297 100644 --- a/backend/src/integrations/apps.ts +++ b/backend/src/integrations/apps.ts @@ -1,7 +1,7 @@ -import axios from 'axios'; -import * as Sentry from '@sentry/node'; -import { Octokit } from '@octokit/rest'; -import { IIntegrationAuth } from '../models'; +import axios from "axios"; +import * as Sentry from "@sentry/node"; +import { Octokit } from "@octokit/rest"; +import { IIntegrationAuth } from "../models"; import { INTEGRATION_HEROKU, INTEGRATION_VERCEL, @@ -9,12 +9,13 @@ import { INTEGRATION_GITHUB, INTEGRATION_RENDER, INTEGRATION_FLYIO, + INTEGRATION_CIRCLECI, INTEGRATION_HEROKU_API_URL, INTEGRATION_VERCEL_API_URL, INTEGRATION_NETLIFY_API_URL, INTEGRATION_RENDER_API_URL, - INTEGRATION_FLYIO_API_URL -} from '../variables'; + INTEGRATION_FLYIO_API_URL, +} from "../variables"; /** * Return list of names of apps for integration named [integration] @@ -26,7 +27,7 @@ import { */ const getApps = async ({ integrationAuth, - accessToken + accessToken, }: { integrationAuth: IIntegrationAuth; accessToken: string; @@ -42,40 +43,45 @@ const getApps = async ({ switch (integrationAuth.integration) { case INTEGRATION_HEROKU: apps = await getAppsHeroku({ - accessToken + accessToken, }); break; case INTEGRATION_VERCEL: apps = await getAppsVercel({ integrationAuth, - accessToken + accessToken, }); break; case INTEGRATION_NETLIFY: apps = await getAppsNetlify({ - accessToken + accessToken, }); break; case INTEGRATION_GITHUB: apps = await getAppsGithub({ - accessToken + accessToken, }); break; case INTEGRATION_RENDER: apps = await getAppsRender({ - accessToken + accessToken, }); break; case INTEGRATION_FLYIO: apps = await getAppsFlyio({ - accessToken + accessToken, + }); + break; + case INTEGRATION_CIRCLECI: + apps = await getAppsCircleci({ + accessToken, }); break; } } catch (err) { Sentry.setUser(null); Sentry.captureException(err); - throw new Error('Failed to get integration apps'); + throw new Error("Failed to get integration apps"); } return apps; @@ -94,19 +100,19 @@ const getAppsHeroku = async ({ accessToken }: { accessToken: string }) => { const res = ( await axios.get(`${INTEGRATION_HEROKU_API_URL}/apps`, { headers: { - Accept: 'application/vnd.heroku+json; version=3', - Authorization: `Bearer ${accessToken}` - } + Accept: "application/vnd.heroku+json; version=3", + Authorization: `Bearer ${accessToken}`, + }, }) ).data; apps = res.map((a: any) => ({ - name: a.name + name: a.name, })); } catch (err) { Sentry.setUser(null); Sentry.captureException(err); - throw new Error('Failed to get Heroku integration apps'); + throw new Error("Failed to get Heroku integration apps"); } return apps; @@ -119,10 +125,10 @@ const getAppsHeroku = async ({ accessToken }: { accessToken: string }) => { * @returns {Object[]} apps - names of Vercel apps * @returns {String} apps.name - name of Vercel app */ -const getAppsVercel = async ({ +const getAppsVercel = async ({ integrationAuth, - accessToken -}: { + accessToken, +}: { integrationAuth: IIntegrationAuth; accessToken: string; }) => { @@ -131,23 +137,25 @@ const getAppsVercel = async ({ const res = ( await axios.get(`${INTEGRATION_VERCEL_API_URL}/v9/projects`, { headers: { - Authorization: `Bearer ${accessToken}` + Authorization: `Bearer ${accessToken}`, }, - ...( integrationAuth?.teamId ? { - params: { - teamId: integrationAuth.teamId - } - } : {}) + ...(integrationAuth?.teamId + ? { + params: { + teamId: integrationAuth.teamId, + }, + } + : {}), }) ).data; - + apps = res.projects.map((a: any) => ({ - name: a.name + name: a.name, })); } catch (err) { Sentry.setUser(null); Sentry.captureException(err); - throw new Error('Failed to get Vercel integration apps'); + throw new Error("Failed to get Vercel integration apps"); } return apps; @@ -160,29 +168,25 @@ const getAppsVercel = async ({ * @returns {Object[]} apps - names of Netlify sites * @returns {String} apps.name - name of Netlify site */ -const getAppsNetlify = async ({ - accessToken -}: { - accessToken: string; -}) => { +const getAppsNetlify = async ({ accessToken }: { accessToken: string }) => { let apps; try { const res = ( await axios.get(`${INTEGRATION_NETLIFY_API_URL}/api/v1/sites`, { headers: { - Authorization: `Bearer ${accessToken}` - } + Authorization: `Bearer ${accessToken}`, + }, }) ).data; apps = res.map((a: any) => ({ name: a.name, - appId: a.site_id + appId: a.site_id, })); } catch (err) { Sentry.setUser(null); Sentry.captureException(err); - throw new Error('Failed to get Netlify integration apps'); + throw new Error("Failed to get Netlify integration apps"); } return apps; @@ -195,35 +199,32 @@ const getAppsNetlify = async ({ * @returns {Object[]} apps - names of Netlify sites * @returns {String} apps.name - name of Netlify site */ -const getAppsGithub = async ({ - accessToken -}: { - accessToken: string; -}) => { +const getAppsGithub = async ({ accessToken }: { accessToken: string }) => { let apps; try { const octokit = new Octokit({ - auth: accessToken + auth: accessToken, }); - const repos = (await octokit.request( - 'GET /user/repos{?visibility,affiliation,type,sort,direction,per_page,page,since,before}', - { - per_page: 100 - } - )).data; + const repos = ( + await octokit.request( + "GET /user/repos{?visibility,affiliation,type,sort,direction,per_page,page,since,before}", + { + per_page: 100, + } + ) + ).data; apps = repos - .filter((a:any) => a.permissions.admin === true) + .filter((a: any) => a.permissions.admin === true) .map((a: any) => ({ - name: a.name, - owner: a.owner.login - }) - ); + name: a.name, + owner: a.owner.login, + })); } catch (err) { Sentry.setUser(null); Sentry.captureException(err); - throw new Error('Failed to get Github repos'); + throw new Error("Failed to get Github repos"); } return apps; @@ -237,34 +238,29 @@ const getAppsGithub = async ({ * @returns {String} apps.name - name of Render service * @returns {String} apps.appId - id of Render service */ -const getAppsRender = async ({ - accessToken -}: { - accessToken: string; -}) => { +const getAppsRender = async ({ accessToken }: { accessToken: string }) => { let apps: any; try { const res = ( await axios.get(`${INTEGRATION_RENDER_API_URL}/v1/services`, { headers: { - Authorization: `Bearer ${accessToken}` - } + Authorization: `Bearer ${accessToken}`, + }, }) ).data; - - apps = res - .map((a: any) => ({ - name: a.service.name, - appId: a.service.id - })); + + apps = res.map((a: any) => ({ + name: a.service.name, + appId: a.service.id, + })); } catch (err) { Sentry.setUser(null); Sentry.captureException(err); - throw new Error('Failed to get Render services'); + throw new Error("Failed to get Render services"); } - + return apps; -} +}; /** * Return list of apps for Fly.io integration @@ -273,11 +269,7 @@ const getAppsRender = async ({ * @returns {Object[]} apps - names and ids of Fly.io apps * @returns {String} apps.name - name of Fly.io apps */ -const getAppsFlyio = async ({ - accessToken -}: { - accessToken: string; -}) => { +const getAppsFlyio = async ({ accessToken }: { accessToken: string }) => { let apps; try { const query = ` @@ -291,32 +283,37 @@ const getAppsFlyio = async ({ } } `; - - const res = (await axios({ - url: INTEGRATION_FLYIO_API_URL, - method: 'post', - headers: { - 'Authorization': 'Bearer ' + accessToken - }, - data: { - query, - variables: { - role: null - } - } - })).data.data.apps.nodes; - - apps = res - .map((a: any) => ({ - name: a.name - })); + + const res = ( + await axios({ + url: INTEGRATION_FLYIO_API_URL, + method: "post", + headers: { + Authorization: "Bearer " + accessToken, + }, + data: { + query, + variables: { + role: null, + }, + }, + }) + ).data.data.apps.nodes; + + apps = res.map((a: any) => ({ + name: a.name, + })); } catch (err) { Sentry.setUser(null); Sentry.captureException(err); - throw new Error('Failed to get Fly.io apps'); + throw new Error("Failed to get Fly.io apps"); } - + return apps; -} +}; + +const getAppsCircleci = async ({ accessToken }: { accessToken: string }) => { + return []; +}; export { getApps }; diff --git a/backend/src/models/integration.ts b/backend/src/models/integration.ts index 01e1d7ee3..d95d490d3 100644 --- a/backend/src/models/integration.ts +++ b/backend/src/models/integration.ts @@ -1,12 +1,12 @@ -import { Schema, model, Types } from 'mongoose'; +import { Schema, model, Types } from "mongoose"; import { INTEGRATION_HEROKU, INTEGRATION_VERCEL, INTEGRATION_NETLIFY, INTEGRATION_GITHUB, INTEGRATION_RENDER, - INTEGRATION_FLYIO -} from '../variables'; + INTEGRATION_FLYIO, +} from "../variables"; export interface IIntegration { _id: Types.ObjectId; @@ -17,44 +17,53 @@ export interface IIntegration { owner: string; targetEnvironment: string; appId: string; - integration: 'heroku' | 'vercel' | 'netlify' | 'github' | 'render' | 'flyio'; + integration: + | "heroku" + | "vercel" + | "netlify" + | "github" + | "render" + | "flyio" + | "circleci"; integrationAuth: Types.ObjectId; } const integrationSchema = new Schema( { workspace: { - type: Schema.Types.ObjectId, - ref: 'Workspace', - required: true + type: Schema.Types.ObjectId, + ref: "Workspace", + required: true, }, environment: { type: String, - required: true + required: true, }, isActive: { type: Boolean, - required: true + required: true, }, app: { // name of app in provider type: String, - default: null + default: null, }, - appId: { // (new) + appId: { + // (new) // id of app in provider type: String, - default: null + default: null, }, - targetEnvironment: { // (new) - // target environment + targetEnvironment: { + // (new) + // target environment type: String, - default: null + default: null, }, owner: { // github-specific repo owner-login type: String, - default: null + default: null, }, integration: { type: String, @@ -64,21 +73,21 @@ const integrationSchema = new Schema( INTEGRATION_NETLIFY, INTEGRATION_GITHUB, INTEGRATION_RENDER, - INTEGRATION_FLYIO + INTEGRATION_FLYIO, ], - required: true + required: true, }, integrationAuth: { type: Schema.Types.ObjectId, - ref: 'IntegrationAuth', - required: true - } + ref: "IntegrationAuth", + required: true, + }, }, { - timestamps: true + timestamps: true, } ); -const Integration = model('Integration', integrationSchema); +const Integration = model("Integration", integrationSchema); export default Integration; diff --git a/backend/src/models/integrationAuth.ts b/backend/src/models/integrationAuth.ts index bff56f09a..694ce6a67 100644 --- a/backend/src/models/integrationAuth.ts +++ b/backend/src/models/integrationAuth.ts @@ -1,15 +1,22 @@ -import { Schema, model, Types } from 'mongoose'; +import { Schema, model, Types } from "mongoose"; import { INTEGRATION_HEROKU, INTEGRATION_VERCEL, INTEGRATION_NETLIFY, - INTEGRATION_GITHUB -} from '../variables'; + INTEGRATION_GITHUB, +} from "../variables"; export interface IIntegrationAuth { _id: Types.ObjectId; workspace: Types.ObjectId; - integration: 'heroku' | 'vercel' | 'netlify' | 'github' | 'render' | 'flyio'; + integration: + | "heroku" + | "vercel" + | "netlify" + | "github" + | "render" + | "flyio" + | "circleci"; teamId: string; accountId: string; refreshCiphertext?: string; @@ -24,9 +31,9 @@ export interface IIntegrationAuth { const integrationAuthSchema = new Schema( { workspace: { - type: Schema.Types.ObjectId, - ref: 'Workspace', - required: true + type: Schema.Types.ObjectId, + ref: "Workspace", + required: true, }, integration: { type: String, @@ -34,54 +41,54 @@ const integrationAuthSchema = new Schema( INTEGRATION_HEROKU, INTEGRATION_VERCEL, INTEGRATION_NETLIFY, - INTEGRATION_GITHUB + INTEGRATION_GITHUB, ], - required: true + required: true, }, teamId: { // vercel-specific integration param - type: String + type: String, }, accountId: { // netlify-specific integration param - type: String + type: String, }, refreshCiphertext: { type: String, - select: false + select: false, }, refreshIV: { type: String, - select: false + select: false, }, refreshTag: { type: String, - select: false + select: false, }, accessCiphertext: { type: String, - select: false + select: false, }, accessIV: { type: String, - select: false + select: false, }, accessTag: { type: String, - select: false + select: false, }, accessExpiresAt: { type: Date, - select: false - } + select: false, + }, }, { - timestamps: true + timestamps: true, } ); const IntegrationAuth = model( - 'IntegrationAuth', + "IntegrationAuth", integrationAuthSchema ); diff --git a/backend/src/variables/index.ts b/backend/src/variables/index.ts index dc1ce6f78..ed4be5d16 100644 --- a/backend/src/variables/index.ts +++ b/backend/src/variables/index.ts @@ -3,8 +3,8 @@ import { ENV_TESTING, ENV_STAGING, ENV_PROD, - ENV_SET -} from './environment'; + ENV_SET, +} from "./environment"; import { INTEGRATION_HEROKU, INTEGRATION_VERCEL, @@ -12,6 +12,7 @@ import { INTEGRATION_GITHUB, INTEGRATION_RENDER, INTEGRATION_FLYIO, + INTEGRATION_CIRCLECI, INTEGRATION_SET, INTEGRATION_OAUTH2, INTEGRATION_HEROKU_TOKEN_URL, @@ -23,25 +24,19 @@ import { INTEGRATION_NETLIFY_API_URL, INTEGRATION_RENDER_API_URL, INTEGRATION_FLYIO_API_URL, - INTEGRATION_OPTIONS -} from './integration'; -import { - OWNER, - ADMIN, - MEMBER, - INVITED, - ACCEPTED, -} from './organization'; -import { SECRET_SHARED, SECRET_PERSONAL } from './secret'; -import { EVENT_PUSH_SECRETS, EVENT_PULL_SECRETS } from './event'; + INTEGRATION_OPTIONS, +} from "./integration"; +import { OWNER, ADMIN, MEMBER, INVITED, ACCEPTED } from "./organization"; +import { SECRET_SHARED, SECRET_PERSONAL } from "./secret"; +import { EVENT_PUSH_SECRETS, EVENT_PULL_SECRETS } from "./event"; import { ACTION_ADD_SECRETS, ACTION_UPDATE_SECRETS, ACTION_DELETE_SECRETS, - ACTION_READ_SECRETS -} from './action'; -import { SMTP_HOST_SENDGRID, SMTP_HOST_MAILGUN } from './smtp'; -import { PLAN_STARTER, PLAN_PRO } from './stripe'; + ACTION_READ_SECRETS, +} from "./action"; +import { SMTP_HOST_SENDGRID, SMTP_HOST_MAILGUN } from "./smtp"; +import { PLAN_STARTER, PLAN_PRO } from "./stripe"; export { OWNER, @@ -62,6 +57,7 @@ export { INTEGRATION_GITHUB, INTEGRATION_RENDER, INTEGRATION_FLYIO, + INTEGRATION_CIRCLECI, INTEGRATION_SET, INTEGRATION_OAUTH2, INTEGRATION_HEROKU_TOKEN_URL, diff --git a/backend/src/variables/integration.ts b/backend/src/variables/integration.ts index 7cecb54c2..bcb161f81 100644 --- a/backend/src/variables/integration.ts +++ b/backend/src/variables/integration.ts @@ -1,164 +1,168 @@ import { - CLIENT_ID_HEROKU, - CLIENT_ID_NETLIFY, - CLIENT_ID_GITHUB, - CLIENT_SLUG_VERCEL -} from '../config'; + CLIENT_ID_HEROKU, + CLIENT_ID_NETLIFY, + CLIENT_ID_GITHUB, + CLIENT_SLUG_VERCEL, +} from "../config"; // integrations -const INTEGRATION_HEROKU = 'heroku'; -const INTEGRATION_VERCEL = 'vercel'; -const INTEGRATION_NETLIFY = 'netlify'; -const INTEGRATION_GITHUB = 'github'; -const INTEGRATION_RENDER = 'render'; -const INTEGRATION_FLYIO = 'flyio'; +const INTEGRATION_HEROKU = "heroku"; +const INTEGRATION_VERCEL = "vercel"; +const INTEGRATION_NETLIFY = "netlify"; +const INTEGRATION_GITHUB = "github"; +const INTEGRATION_RENDER = "render"; +const INTEGRATION_FLYIO = "flyio"; +const INTEGRATION_CIRCLECI = "circleci"; const INTEGRATION_SET = new Set([ - INTEGRATION_HEROKU, - INTEGRATION_VERCEL, - INTEGRATION_NETLIFY, - INTEGRATION_GITHUB, - INTEGRATION_RENDER, - INTEGRATION_FLYIO + INTEGRATION_HEROKU, + INTEGRATION_VERCEL, + INTEGRATION_NETLIFY, + INTEGRATION_GITHUB, + INTEGRATION_RENDER, + INTEGRATION_FLYIO, + INTEGRATION_CIRCLECI, ]); // integration types -const INTEGRATION_OAUTH2 = 'oauth2'; +const INTEGRATION_OAUTH2 = "oauth2"; // integration oauth endpoints -const INTEGRATION_HEROKU_TOKEN_URL = 'https://id.heroku.com/oauth/token'; +const INTEGRATION_HEROKU_TOKEN_URL = "https://id.heroku.com/oauth/token"; const INTEGRATION_VERCEL_TOKEN_URL = - 'https://api.vercel.com/v2/oauth/access_token'; -const INTEGRATION_NETLIFY_TOKEN_URL = 'https://api.netlify.com/oauth/token'; + "https://api.vercel.com/v2/oauth/access_token"; +const INTEGRATION_NETLIFY_TOKEN_URL = "https://api.netlify.com/oauth/token"; const INTEGRATION_GITHUB_TOKEN_URL = - 'https://github.com/login/oauth/access_token'; + "https://github.com/login/oauth/access_token"; // integration apps endpoints -const INTEGRATION_HEROKU_API_URL = 'https://api.heroku.com'; -const INTEGRATION_VERCEL_API_URL = 'https://api.vercel.com'; -const INTEGRATION_NETLIFY_API_URL = 'https://api.netlify.com'; -const INTEGRATION_RENDER_API_URL = 'https://api.render.com'; -const INTEGRATION_FLYIO_API_URL = 'https://api.fly.io/graphql'; +const INTEGRATION_HEROKU_API_URL = "https://api.heroku.com"; +const INTEGRATION_VERCEL_API_URL = "https://api.vercel.com"; +const INTEGRATION_NETLIFY_API_URL = "https://api.netlify.com"; +const INTEGRATION_RENDER_API_URL = "https://api.render.com"; +const INTEGRATION_FLYIO_API_URL = "https://api.fly.io/graphql"; +const INTEGRATION_CIRCLECI_API_URL = "https://circleci.com/api/v2"; const INTEGRATION_OPTIONS = [ - { - name: 'Heroku', - slug: 'heroku', - image: 'Heroku.png', - isAvailable: true, - type: 'oauth', - clientId: CLIENT_ID_HEROKU, - docsLink: '' - }, - { - name: 'Vercel', - slug: 'vercel', - image: 'Vercel.png', - isAvailable: true, - type: 'oauth', - clientId: '', - clientSlug: CLIENT_SLUG_VERCEL, - docsLink: '' - }, - { - name: 'Netlify', - slug: 'netlify', - image: 'Netlify.png', - isAvailable: true, - type: 'oauth', - clientId: CLIENT_ID_NETLIFY, - docsLink: '' - }, - { - name: 'GitHub', - slug: 'github', - image: 'GitHub.png', - isAvailable: true, - type: 'oauth', - clientId: CLIENT_ID_GITHUB, - docsLink: '' - }, - { - name: 'Render', - slug: 'render', - image: 'Render.png', - isAvailable: true, - type: 'pat', - clientId: '', - docsLink: '' - }, - { - name: 'Fly.io', - slug: 'flyio', - image: 'Flyio.svg', - isAvailable: true, - type: 'pat', - clientId: '', - docsLink: '' - }, - { - name: 'Google Cloud Platform', - slug: 'gcp', - image: 'Google Cloud Platform.png', - isAvailable: false, - type: '', - clientId: '', - docsLink: '' - }, - { - name: 'Amazon Web Services', - slug: 'aws', - image: 'Amazon Web Services.png', - isAvailable: false, - type: '', - clientId: '', - docsLink: '' - }, - { - name: 'Microsoft Azure', - slug: 'azure', - image: 'Microsoft Azure.png', - isAvailable: false, - type: '', - clientId: '', - docsLink: '' - }, - { - name: 'Travis CI', - slug: 'travisci', - image: 'Travis CI.png', - isAvailable: false, - type: '', - clientId: '', - docsLink: '' - }, - { - name: 'Circle CI', - slug: 'circleci', - image: 'Circle CI.png', - isAvailable: false, - type: '', - clientId: '', - docsLink: '' - } -] + { + name: "Heroku", + slug: "heroku", + image: "Heroku.png", + isAvailable: true, + type: "oauth", + clientId: CLIENT_ID_HEROKU, + docsLink: "", + }, + { + name: "Vercel", + slug: "vercel", + image: "Vercel.png", + isAvailable: true, + type: "oauth", + clientId: "", + clientSlug: CLIENT_SLUG_VERCEL, + docsLink: "", + }, + { + name: "Netlify", + slug: "netlify", + image: "Netlify.png", + isAvailable: true, + type: "oauth", + clientId: CLIENT_ID_NETLIFY, + docsLink: "", + }, + { + name: "GitHub", + slug: "github", + image: "GitHub.png", + isAvailable: true, + type: "oauth", + clientId: CLIENT_ID_GITHUB, + docsLink: "", + }, + { + name: "Render", + slug: "render", + image: "Render.png", + isAvailable: true, + type: "pat", + clientId: "", + docsLink: "", + }, + { + name: "Fly.io", + slug: "flyio", + image: "Flyio.svg", + isAvailable: true, + type: "pat", + clientId: "", + docsLink: "", + }, + { + name: "Google Cloud Platform", + slug: "gcp", + image: "Google Cloud Platform.png", + isAvailable: false, + type: "", + clientId: "", + docsLink: "", + }, + { + name: "Amazon Web Services", + slug: "aws", + image: "Amazon Web Services.png", + isAvailable: false, + type: "", + clientId: "", + docsLink: "", + }, + { + name: "Microsoft Azure", + slug: "azure", + image: "Microsoft Azure.png", + isAvailable: false, + type: "", + clientId: "", + docsLink: "", + }, + { + name: "Travis CI", + slug: "travisci", + image: "Travis CI.png", + isAvailable: false, + type: "", + clientId: "", + docsLink: "", + }, + { + name: "Circle CI", + slug: "circleci", + image: "Circle CI.png", + isAvailable: false, + type: "", + clientId: "", + docsLink: "", + }, +]; export { - INTEGRATION_HEROKU, - INTEGRATION_VERCEL, - INTEGRATION_NETLIFY, - INTEGRATION_GITHUB, - INTEGRATION_RENDER, - INTEGRATION_FLYIO, - INTEGRATION_SET, - INTEGRATION_OAUTH2, - INTEGRATION_HEROKU_TOKEN_URL, - INTEGRATION_VERCEL_TOKEN_URL, - INTEGRATION_NETLIFY_TOKEN_URL, - INTEGRATION_GITHUB_TOKEN_URL, - INTEGRATION_HEROKU_API_URL, - INTEGRATION_VERCEL_API_URL, - INTEGRATION_NETLIFY_API_URL, - INTEGRATION_RENDER_API_URL, - INTEGRATION_FLYIO_API_URL, - INTEGRATION_OPTIONS + INTEGRATION_HEROKU, + INTEGRATION_VERCEL, + INTEGRATION_NETLIFY, + INTEGRATION_GITHUB, + INTEGRATION_RENDER, + INTEGRATION_FLYIO, + INTEGRATION_CIRCLECI, + INTEGRATION_SET, + INTEGRATION_OAUTH2, + INTEGRATION_HEROKU_TOKEN_URL, + INTEGRATION_VERCEL_TOKEN_URL, + INTEGRATION_NETLIFY_TOKEN_URL, + INTEGRATION_GITHUB_TOKEN_URL, + INTEGRATION_HEROKU_API_URL, + INTEGRATION_VERCEL_API_URL, + INTEGRATION_NETLIFY_API_URL, + INTEGRATION_RENDER_API_URL, + INTEGRATION_FLYIO_API_URL, + INTEGRATION_OPTIONS, }; From b0ffac2f0095b7b2ca9e1d14146adec23c7359e7 Mon Sep 17 00:00:00 2001 From: Aashish-Upadhyay-101 Date: Sat, 4 Feb 2023 16:50:34 +0545 Subject: [PATCH 02/21] fetch apps from circleci --- backend/src/integrations/apps.ts | 36 +- backend/src/integrations/sync.ts | 952 ++++++++++++++------------- backend/src/variables/index.ts | 2 + backend/src/variables/integration.ts | 3 +- 4 files changed, 538 insertions(+), 455 deletions(-) diff --git a/backend/src/integrations/apps.ts b/backend/src/integrations/apps.ts index d36c9c297..26d606f87 100644 --- a/backend/src/integrations/apps.ts +++ b/backend/src/integrations/apps.ts @@ -15,6 +15,7 @@ import { INTEGRATION_NETLIFY_API_URL, INTEGRATION_RENDER_API_URL, INTEGRATION_FLYIO_API_URL, + INTEGRATION_CIRCLECI_API_URL, } from "../variables"; /** @@ -313,7 +314,40 @@ const getAppsFlyio = async ({ accessToken }: { accessToken: string }) => { }; const getAppsCircleci = async ({ accessToken }: { accessToken: string }) => { - return []; + // in place of accessToken we have to send Circle-Token i.e. Personal API token from CircleCi + let apps: any; + try { + const circleciOrganizationDetail = ( + await axios.get(`${INTEGRATION_CIRCLECI_API_URL}/v2/me/collaborations`, { + headers: { + "Circle-Token": accessToken, + }, + }) + ).data; + + const { slug } = circleciOrganizationDetail; + + const res = ( + await axios.get( + `${INTEGRATION_CIRCLECI_API_URL}/v2/pipeline/?org-slug=${slug}`, + { + headers: { + "Circle-Token": accessToken, + }, + } + ) + ).data.items; + + apps = res.map((a: any) => ({ + name: a?.project_slug?.split("/")[2], + })); + } catch (err) { + Sentry.setUser(null); + Sentry.captureException(err); + throw new Error("Failed to get Render services"); + } + + return apps; }; export { getApps }; diff --git a/backend/src/integrations/sync.ts b/backend/src/integrations/sync.ts index 9954943b8..ddc3329ec 100644 --- a/backend/src/integrations/sync.ts +++ b/backend/src/integrations/sync.ts @@ -1,10 +1,10 @@ -import axios from 'axios'; -import * as Sentry from '@sentry/node'; -import { Octokit } from '@octokit/rest'; +import axios from "axios"; +import * as Sentry from "@sentry/node"; +import { Octokit } from "@octokit/rest"; // import * as sodium from 'libsodium-wrappers'; -import sodium from 'libsodium-wrappers'; +import sodium from "libsodium-wrappers"; // const sodium = require('libsodium-wrappers'); -import { IIntegration, IIntegrationAuth } from '../models'; +import { IIntegration, IIntegrationAuth } from "../models"; import { INTEGRATION_HEROKU, INTEGRATION_VERCEL, @@ -12,13 +12,15 @@ import { INTEGRATION_GITHUB, INTEGRATION_RENDER, INTEGRATION_FLYIO, + INTEGRATION_CIRCLECI, INTEGRATION_HEROKU_API_URL, INTEGRATION_VERCEL_API_URL, INTEGRATION_NETLIFY_API_URL, INTEGRATION_RENDER_API_URL, - INTEGRATION_FLYIO_API_URL -} from '../variables'; -import { access, appendFile } from 'fs'; + INTEGRATION_FLYIO_API_URL, + INTEGRATION_CIRCLECI_API_URL, +} from "../variables"; +import { access, appendFile } from "fs"; /** * Sync/push [secrets] to [app] in integration named [integration] @@ -32,7 +34,7 @@ const syncSecrets = async ({ integration, integrationAuth, secrets, - accessToken + accessToken, }: { integration: IIntegration; integrationAuth: IIntegrationAuth; @@ -45,7 +47,7 @@ const syncSecrets = async ({ await syncSecretsHeroku({ integration, secrets, - accessToken + accessToken, }); break; case INTEGRATION_VERCEL: @@ -53,7 +55,7 @@ const syncSecrets = async ({ integration, integrationAuth, secrets, - accessToken + accessToken, }); break; case INTEGRATION_NETLIFY: @@ -61,35 +63,41 @@ const syncSecrets = async ({ integration, integrationAuth, secrets, - accessToken + accessToken, }); break; case INTEGRATION_GITHUB: await syncSecretsGitHub({ integration, secrets, - accessToken + accessToken, }); break; case INTEGRATION_RENDER: await syncSecretsRender({ integration, secrets, - accessToken + accessToken, }); break; case INTEGRATION_FLYIO: await syncSecretsFlyio({ integration, secrets, - accessToken + accessToken, }); break; + // case INTEGRATION_CIRCLECI: + // await syncSecretsCircleci({ + // integration, + // secrets, + // accessToken, + // }); } } catch (err) { Sentry.setUser(null); Sentry.captureException(err); - throw new Error('Failed to sync secrets to integration'); + throw new Error("Failed to sync secrets to integration"); } }; @@ -103,7 +111,7 @@ const syncSecrets = async ({ const syncSecretsHeroku = async ({ integration, secrets, - accessToken + accessToken, }: { integration: IIntegration; secrets: any; @@ -115,9 +123,9 @@ const syncSecretsHeroku = async ({ `${INTEGRATION_HEROKU_API_URL}/apps/${integration.app}/config-vars`, { headers: { - Accept: 'application/vnd.heroku+json; version=3', - Authorization: `Bearer ${accessToken}` - } + Accept: "application/vnd.heroku+json; version=3", + Authorization: `Bearer ${accessToken}`, + }, } ) ).data; @@ -133,15 +141,15 @@ const syncSecretsHeroku = async ({ secrets, { headers: { - Accept: 'application/vnd.heroku+json; version=3', - Authorization: `Bearer ${accessToken}` - } + Accept: "application/vnd.heroku+json; version=3", + Authorization: `Bearer ${accessToken}`, + }, } ); } catch (err) { Sentry.setUser(null); Sentry.captureException(err); - throw new Error('Failed to sync secrets to Heroku'); + throw new Error("Failed to sync secrets to Heroku"); } }; @@ -152,156 +160,168 @@ const syncSecretsHeroku = async ({ * @param {Object} obj.secrets - secrets to push to integration (object where keys are secret keys and values are secret values) */ const syncSecretsVercel = async ({ - integration, - integrationAuth, - secrets, - accessToken + integration, + integrationAuth, + secrets, + accessToken, }: { - integration: IIntegration, - integrationAuth: IIntegrationAuth, - secrets: any; - accessToken: string; + integration: IIntegration; + integrationAuth: IIntegrationAuth; + secrets: any; + accessToken: string; }) => { - interface VercelSecret { - id?: string; - type: string; - key: string; - value: string; - target: string[]; - } - - try { - // Get all (decrypted) secrets back from Vercel in - // decrypted format - const params: { [key: string]: string } = { - decrypt: 'true', - ...( integrationAuth?.teamId ? { - teamId: integrationAuth.teamId - } : {}) - } - - const res = (await Promise.all((await axios.get( - `${INTEGRATION_VERCEL_API_URL}/v9/projects/${integration.app}/env`, - { + interface VercelSecret { + id?: string; + type: string; + key: string; + value: string; + target: string[]; + } + + try { + // Get all (decrypted) secrets back from Vercel in + // decrypted format + const params: { [key: string]: string } = { + decrypt: "true", + ...(integrationAuth?.teamId + ? { + teamId: integrationAuth.teamId, + } + : {}), + }; + + const res = ( + await Promise.all( + ( + await axios.get( + `${INTEGRATION_VERCEL_API_URL}/v9/projects/${integration.app}/env`, + { params, headers: { - Authorization: `Bearer ${accessToken}` - } - } - )) - .data - .envs - .filter((secret: VercelSecret) => secret.target.includes(integration.targetEnvironment)) - .map(async (secret: VercelSecret) => (await axios.get( - `${INTEGRATION_VERCEL_API_URL}/v9/projects/${integration.app}/env/${secret.id}`, - { - params, - headers: { - Authorization: `Bearer ${accessToken}` - } - } - )).data) - )).reduce((obj: any, secret: any) => ({ - ...obj, - [secret.key]: secret - }), {}); - - const updateSecrets: VercelSecret[] = []; - const deleteSecrets: VercelSecret[] = []; - const newSecrets: VercelSecret[] = []; - - // Identify secrets to create - Object.keys(secrets).map((key) => { - if (!(key in res)) { - // case: secret has been created - newSecrets.push({ - key: key, - value: secrets[key], - type: 'encrypted', - target: [integration.targetEnvironment] - }); - } - }); - - // Identify secrets to update and delete - Object.keys(res).map((key) => { - if (key in secrets) { - if (res[key].value !== secrets[key]) { - // case: secret value has changed - updateSecrets.push({ - id: res[key].id, - key: key, - value: secrets[key], - type: 'encrypted', - target: [integration.targetEnvironment] - }); - } - } else { - // case: secret has been deleted - deleteSecrets.push({ - id: res[key].id, - key: key, - value: res[key].value, - type: 'encrypted', - target: [integration.targetEnvironment], - }); - } - }); - - // Sync/push new secrets - if (newSecrets.length > 0) { - await axios.post( - `${INTEGRATION_VERCEL_API_URL}/v10/projects/${integration.app}/env`, - newSecrets, - { - params, - headers: { - Authorization: `Bearer ${accessToken}` - } - } - ); - } - - // Sync/push updated secrets - if (updateSecrets.length > 0) { - updateSecrets.forEach(async (secret: VercelSecret) => { - const { - id, - ...updatedSecret - } = secret; - await axios.patch( - `${INTEGRATION_VERCEL_API_URL}/v9/projects/${integration.app}/env/${secret.id}`, - updatedSecret, - { - params, - headers: { - Authorization: `Bearer ${accessToken}` - } - } - ); - }); - } - - // Delete secrets - if (deleteSecrets.length > 0) { - deleteSecrets.forEach(async (secret: VercelSecret) => { - await axios.delete( + Authorization: `Bearer ${accessToken}`, + }, + } + ) + ).data.envs + .filter((secret: VercelSecret) => + secret.target.includes(integration.targetEnvironment) + ) + .map( + async (secret: VercelSecret) => + ( + await axios.get( `${INTEGRATION_VERCEL_API_URL}/v9/projects/${integration.app}/env/${secret.id}`, { params, headers: { - Authorization: `Bearer ${accessToken}` - } + Authorization: `Bearer ${accessToken}`, + }, } - ); - }); + ) + ).data + ) + ) + ).reduce( + (obj: any, secret: any) => ({ + ...obj, + [secret.key]: secret, + }), + {} + ); + + const updateSecrets: VercelSecret[] = []; + const deleteSecrets: VercelSecret[] = []; + const newSecrets: VercelSecret[] = []; + + // Identify secrets to create + Object.keys(secrets).map((key) => { + if (!(key in res)) { + // case: secret has been created + newSecrets.push({ + key: key, + value: secrets[key], + type: "encrypted", + target: [integration.targetEnvironment], + }); } - } catch (err) { - Sentry.setUser(null); - Sentry.captureException(err); - throw new Error('Failed to sync secrets to Vercel'); + }); + + // Identify secrets to update and delete + Object.keys(res).map((key) => { + if (key in secrets) { + if (res[key].value !== secrets[key]) { + // case: secret value has changed + updateSecrets.push({ + id: res[key].id, + key: key, + value: secrets[key], + type: "encrypted", + target: [integration.targetEnvironment], + }); + } + } else { + // case: secret has been deleted + deleteSecrets.push({ + id: res[key].id, + key: key, + value: res[key].value, + type: "encrypted", + target: [integration.targetEnvironment], + }); + } + }); + + // Sync/push new secrets + if (newSecrets.length > 0) { + await axios.post( + `${INTEGRATION_VERCEL_API_URL}/v10/projects/${integration.app}/env`, + newSecrets, + { + params, + headers: { + Authorization: `Bearer ${accessToken}`, + }, + } + ); } -} + + // Sync/push updated secrets + if (updateSecrets.length > 0) { + updateSecrets.forEach(async (secret: VercelSecret) => { + const { id, ...updatedSecret } = secret; + await axios.patch( + `${INTEGRATION_VERCEL_API_URL}/v9/projects/${integration.app}/env/${secret.id}`, + updatedSecret, + { + params, + headers: { + Authorization: `Bearer ${accessToken}`, + }, + } + ); + }); + } + + // Delete secrets + if (deleteSecrets.length > 0) { + deleteSecrets.forEach(async (secret: VercelSecret) => { + await axios.delete( + `${INTEGRATION_VERCEL_API_URL}/v9/projects/${integration.app}/env/${secret.id}`, + { + params, + headers: { + Authorization: `Bearer ${accessToken}`, + }, + } + ); + }); + } + } catch (err) { + Sentry.setUser(null); + Sentry.captureException(err); + throw new Error("Failed to sync secrets to Vercel"); + } +}; /** * Sync/push [secrets] to Netlify site with id [integration.appId] @@ -312,202 +332,214 @@ const syncSecretsVercel = async ({ * @param {Object} obj.accessToken - access token for Netlify integration */ const syncSecretsNetlify = async ({ - integration, - integrationAuth, - secrets, - accessToken + integration, + integrationAuth, + secrets, + accessToken, }: { - integration: IIntegration; - integrationAuth: IIntegrationAuth; - secrets: any; - accessToken: string; + integration: IIntegration; + integrationAuth: IIntegrationAuth; + secrets: any; + accessToken: string; }) => { - try { - - interface NetlifyValue { - id?: string; - context: string; // 'dev' | 'branch-deploy' | 'deploy-preview' | 'production', - value: string; - } - - interface NetlifySecret { - key: string; - values: NetlifyValue[]; - } - - interface NetlifySecretsRes { - [index: string]: NetlifySecret; - } - - const getParams = new URLSearchParams({ - context_name: 'all', // integration.context or all - site_id: integration.appId - }); - - const res = (await axios.get( - `${INTEGRATION_NETLIFY_API_URL}/api/v1/accounts/${integrationAuth.accountId}/env`, - { - params: getParams, - headers: { - Authorization: `Bearer ${accessToken}` - } - } - )) - .data - .reduce((obj: any, secret: any) => ({ - ...obj, - [secret.key]: secret - }), {}); - - const newSecrets: NetlifySecret[] = []; // createEnvVars - const deleteSecrets: string[] = []; // deleteEnvVar - const deleteSecretValues: NetlifySecret[] = []; // deleteEnvVarValue - const updateSecrets: NetlifySecret[] = []; // setEnvVarValue - - // identify secrets to create and update - Object.keys(secrets).map((key) => { - if (!(key in res)) { - // case: Infisical secret does not exist in Netlify -> create secret - newSecrets.push({ - key, - values: [{ - value: secrets[key], - context: integration.targetEnvironment - }] - }); - } else { - // case: Infisical secret exists in Netlify - const contexts = res[key].values - .reduce((obj: any, value: NetlifyValue) => ({ - ...obj, - [value.context]: value - }), {}); - - if (integration.targetEnvironment in contexts) { - // case: Netlify secret value exists in integration context - if (secrets[key] !== contexts[integration.targetEnvironment].value) { - // case: Infisical and Netlify secret values are different - // -> update Netlify secret context and value - updateSecrets.push({ - key, - values: [{ - context: integration.targetEnvironment, - value: secrets[key] - }] - }); - } - } else { - // case: Netlify secret value does not exist in integration context - // -> add the new Netlify secret context and value - updateSecrets.push({ - key, - values: [{ - context: integration.targetEnvironment, - value: secrets[key] - }] - }); - } - } - }) - - // identify secrets to delete - // TODO: revise (patch case where 1 context was deleted but others still there - Object.keys(res).map((key) => { - // loop through each key's context - if (!(key in secrets)) { - // case: Netlify secret does not exist in Infisical - - const numberOfValues = res[key].values.length; - - res[key].values.forEach((value: NetlifyValue) => { - if (value.context === integration.targetEnvironment) { - if (numberOfValues <= 1) { - // case: Netlify secret value has less than 1 context -> delete secret - deleteSecrets.push(key); - } else { - // case: Netlify secret value has more than 1 context -> delete secret value context - deleteSecretValues.push({ - key, - values: [{ - id: value.id, - context: integration.targetEnvironment, - value: value.value - }] - }); - } - } - }); - } - }); - - const syncParams = new URLSearchParams({ - site_id: integration.appId - }); - - if (newSecrets.length > 0) { - await axios.post( - `${INTEGRATION_NETLIFY_API_URL}/api/v1/accounts/${integrationAuth.accountId}/env`, - newSecrets, - { - params: syncParams, - headers: { - Authorization: `Bearer ${accessToken}` - } - } - ); - } - - if (updateSecrets.length > 0) { - updateSecrets.forEach(async (secret: NetlifySecret) => { - await axios.patch( - `${INTEGRATION_NETLIFY_API_URL}/api/v1/accounts/${integrationAuth.accountId}/env/${secret.key}`, - { - context: secret.values[0].context, - value: secret.values[0].value - }, - { - params: syncParams, - headers: { - Authorization: `Bearer ${accessToken}` - } - } - ); - }); - } - - if (deleteSecrets.length > 0) { - deleteSecrets.forEach(async (key: string) => { - await axios.delete( - `${INTEGRATION_NETLIFY_API_URL}/api/v1/accounts/${integrationAuth.accountId}/env/${key}`, - { - params: syncParams, - headers: { - Authorization: `Bearer ${accessToken}` - } - } - ); - }); - } - - if (deleteSecretValues.length > 0) { - deleteSecretValues.forEach(async (secret: NetlifySecret) => { - await axios.delete( - `${INTEGRATION_NETLIFY_API_URL}/api/v1/accounts/${integrationAuth.accountId}/env/${secret.key}/value/${secret.values[0].id}`, - { - params: syncParams, - headers: { - Authorization: `Bearer ${accessToken}` - } - } - ); - }); - } - } catch (err) { - Sentry.setUser(null); - Sentry.captureException(err); - throw new Error('Failed to sync secrets to Heroku'); + try { + interface NetlifyValue { + id?: string; + context: string; // 'dev' | 'branch-deploy' | 'deploy-preview' | 'production', + value: string; } -} + + interface NetlifySecret { + key: string; + values: NetlifyValue[]; + } + + interface NetlifySecretsRes { + [index: string]: NetlifySecret; + } + + const getParams = new URLSearchParams({ + context_name: "all", // integration.context or all + site_id: integration.appId, + }); + + const res = ( + await axios.get( + `${INTEGRATION_NETLIFY_API_URL}/api/v1/accounts/${integrationAuth.accountId}/env`, + { + params: getParams, + headers: { + Authorization: `Bearer ${accessToken}`, + }, + } + ) + ).data.reduce( + (obj: any, secret: any) => ({ + ...obj, + [secret.key]: secret, + }), + {} + ); + + const newSecrets: NetlifySecret[] = []; // createEnvVars + const deleteSecrets: string[] = []; // deleteEnvVar + const deleteSecretValues: NetlifySecret[] = []; // deleteEnvVarValue + const updateSecrets: NetlifySecret[] = []; // setEnvVarValue + + // identify secrets to create and update + Object.keys(secrets).map((key) => { + if (!(key in res)) { + // case: Infisical secret does not exist in Netlify -> create secret + newSecrets.push({ + key, + values: [ + { + value: secrets[key], + context: integration.targetEnvironment, + }, + ], + }); + } else { + // case: Infisical secret exists in Netlify + const contexts = res[key].values.reduce( + (obj: any, value: NetlifyValue) => ({ + ...obj, + [value.context]: value, + }), + {} + ); + + if (integration.targetEnvironment in contexts) { + // case: Netlify secret value exists in integration context + if (secrets[key] !== contexts[integration.targetEnvironment].value) { + // case: Infisical and Netlify secret values are different + // -> update Netlify secret context and value + updateSecrets.push({ + key, + values: [ + { + context: integration.targetEnvironment, + value: secrets[key], + }, + ], + }); + } + } else { + // case: Netlify secret value does not exist in integration context + // -> add the new Netlify secret context and value + updateSecrets.push({ + key, + values: [ + { + context: integration.targetEnvironment, + value: secrets[key], + }, + ], + }); + } + } + }); + + // identify secrets to delete + // TODO: revise (patch case where 1 context was deleted but others still there + Object.keys(res).map((key) => { + // loop through each key's context + if (!(key in secrets)) { + // case: Netlify secret does not exist in Infisical + + const numberOfValues = res[key].values.length; + + res[key].values.forEach((value: NetlifyValue) => { + if (value.context === integration.targetEnvironment) { + if (numberOfValues <= 1) { + // case: Netlify secret value has less than 1 context -> delete secret + deleteSecrets.push(key); + } else { + // case: Netlify secret value has more than 1 context -> delete secret value context + deleteSecretValues.push({ + key, + values: [ + { + id: value.id, + context: integration.targetEnvironment, + value: value.value, + }, + ], + }); + } + } + }); + } + }); + + const syncParams = new URLSearchParams({ + site_id: integration.appId, + }); + + if (newSecrets.length > 0) { + await axios.post( + `${INTEGRATION_NETLIFY_API_URL}/api/v1/accounts/${integrationAuth.accountId}/env`, + newSecrets, + { + params: syncParams, + headers: { + Authorization: `Bearer ${accessToken}`, + }, + } + ); + } + + if (updateSecrets.length > 0) { + updateSecrets.forEach(async (secret: NetlifySecret) => { + await axios.patch( + `${INTEGRATION_NETLIFY_API_URL}/api/v1/accounts/${integrationAuth.accountId}/env/${secret.key}`, + { + context: secret.values[0].context, + value: secret.values[0].value, + }, + { + params: syncParams, + headers: { + Authorization: `Bearer ${accessToken}`, + }, + } + ); + }); + } + + if (deleteSecrets.length > 0) { + deleteSecrets.forEach(async (key: string) => { + await axios.delete( + `${INTEGRATION_NETLIFY_API_URL}/api/v1/accounts/${integrationAuth.accountId}/env/${key}`, + { + params: syncParams, + headers: { + Authorization: `Bearer ${accessToken}`, + }, + } + ); + }); + } + + if (deleteSecretValues.length > 0) { + deleteSecretValues.forEach(async (secret: NetlifySecret) => { + await axios.delete( + `${INTEGRATION_NETLIFY_API_URL}/api/v1/accounts/${integrationAuth.accountId}/env/${secret.key}/value/${secret.values[0].id}`, + { + params: syncParams, + headers: { + Authorization: `Bearer ${accessToken}`, + }, + } + ); + }); + } + } catch (err) { + Sentry.setUser(null); + Sentry.captureException(err); + throw new Error("Failed to sync secrets to Heroku"); + } +}; /** * Sync/push [secrets] to GitHub repo with name [integration.app] @@ -515,24 +547,23 @@ const syncSecretsNetlify = async ({ * @param {IIntegration} obj.integration - integration details * @param {IIntegrationAuth} obj.integrationAuth - integration auth details * @param {Object} obj.secrets - secrets to push to integration (object where keys are secret keys and values are secret values) - * @param {String} obj.accessToken - access token for GitHub integration + * @param {String} obj.accessToken - access token for GitHub integration */ const syncSecretsGitHub = async ({ integration, secrets, - accessToken + accessToken, }: { integration: IIntegration; secrets: any; accessToken: string; }) => { try { - interface GitHubRepoKey { key_id: string; key: string; } - + interface GitHubSecret { name: string; created_at: string; @@ -540,87 +571,88 @@ const syncSecretsGitHub = async ({ } interface GitHubSecretRes { - [index: string]: GitHubSecret; + [index: string]: GitHubSecret; } const deleteSecrets: GitHubSecret[] = []; const octokit = new Octokit({ - auth: accessToken + auth: accessToken, }); // const user = (await octokit.request('GET /user', {})).data; - const repoPublicKey: GitHubRepoKey = (await octokit.request( - 'GET /repos/{owner}/{repo}/actions/secrets/public-key', - { - owner: integration.owner, - repo: integration.app - } - )).data; + const repoPublicKey: GitHubRepoKey = ( + await octokit.request( + "GET /repos/{owner}/{repo}/actions/secrets/public-key", + { + owner: integration.owner, + repo: integration.app, + } + ) + ).data; // Get local copy of decrypted secrets. We cannot decrypt them as we dont have access to GH private key - const encryptedSecrets: GitHubSecretRes = (await octokit.request( - 'GET /repos/{owner}/{repo}/actions/secrets', - { + const encryptedSecrets: GitHubSecretRes = ( + await octokit.request("GET /repos/{owner}/{repo}/actions/secrets", { owner: integration.owner, - repo: integration.app - } - )) - .data - .secrets - .reduce((obj: any, secret: any) => ({ - ...obj, - [secret.name]: secret - }), {}); - + repo: integration.app, + }) + ).data.secrets.reduce( + (obj: any, secret: any) => ({ + ...obj, + [secret.name]: secret, + }), + {} + ); + Object.keys(encryptedSecrets).map(async (key) => { if (!(key in secrets)) { await octokit.request( - 'DELETE /repos/{owner}/{repo}/actions/secrets/{secret_name}', + "DELETE /repos/{owner}/{repo}/actions/secrets/{secret_name}", { owner: integration.owner, repo: integration.app, - secret_name: key + secret_name: key, } ); } }); - + Object.keys(secrets).map((key) => { // let encryptedSecret; sodium.ready.then(async () => { - // convert secret & base64 key to Uint8Array. - const binkey = sodium.from_base64( - repoPublicKey.key, - sodium.base64_variants.ORIGINAL - ); - const binsec = sodium.from_string(secrets[key]); + // convert secret & base64 key to Uint8Array. + const binkey = sodium.from_base64( + repoPublicKey.key, + sodium.base64_variants.ORIGINAL + ); + const binsec = sodium.from_string(secrets[key]); - // encrypt secret using libsodium - const encBytes = sodium.crypto_box_seal(binsec, binkey); + // encrypt secret using libsodium + const encBytes = sodium.crypto_box_seal(binsec, binkey); - // convert encrypted Uint8Array to base64 - const encryptedSecret = sodium.to_base64( - encBytes, - sodium.base64_variants.ORIGINAL - ); - - await octokit.request( - 'PUT /repos/{owner}/{repo}/actions/secrets/{secret_name}', - { - owner: integration.owner, - repo: integration.app, - secret_name: key, - encrypted_value: encryptedSecret, - key_id: repoPublicKey.key_id - } - ); + // convert encrypted Uint8Array to base64 + const encryptedSecret = sodium.to_base64( + encBytes, + sodium.base64_variants.ORIGINAL + ); + + await octokit.request( + "PUT /repos/{owner}/{repo}/actions/secrets/{secret_name}", + { + owner: integration.owner, + repo: integration.app, + secret_name: key, + encrypted_value: encryptedSecret, + key_id: repoPublicKey.key_id, + } + ); }); }); } catch (err) { Sentry.setUser(null); Sentry.captureException(err); - throw new Error('Failed to sync secrets to GitHub'); + throw new Error("Failed to sync secrets to GitHub"); } }; @@ -634,7 +666,7 @@ const syncSecretsGitHub = async ({ const syncSecretsRender = async ({ integration, secrets, - accessToken + accessToken, }: { integration: IIntegration; secrets: any; @@ -645,20 +677,20 @@ const syncSecretsRender = async ({ `${INTEGRATION_RENDER_API_URL}/v1/services/${integration.appId}/env-vars`, Object.keys(secrets).map((key) => ({ key, - value: secrets[key] + value: secrets[key], })), { headers: { - Authorization: `Bearer ${accessToken}` - } + Authorization: `Bearer ${accessToken}`, + }, } ); } catch (err) { Sentry.setUser(null); Sentry.captureException(err); - throw new Error('Failed to sync secrets to Render'); + throw new Error("Failed to sync secrets to Render"); } -} +}; /** * Sync/push [secrets] to Fly.io app @@ -670,7 +702,7 @@ const syncSecretsRender = async ({ const syncSecretsFlyio = async ({ integration, secrets, - accessToken + accessToken, }: { integration: IIntegration; secrets: any; @@ -700,28 +732,31 @@ const syncSecretsFlyio = async ({ await axios({ url: INTEGRATION_FLYIO_API_URL, - method: 'post', + method: "post", headers: { - 'Authorization': 'Bearer ' + accessToken + Authorization: "Bearer " + accessToken, }, data: { query: SetSecrets, variables: { input: { appId: integration.app, - secrets: Object.entries(secrets).map(([key, value]) => ({ key, value })) - } - } - } + secrets: Object.entries(secrets).map(([key, value]) => ({ + key, + value, + })), + }, + }, + }, }); - + // get secrets interface FlyioSecret { name: string; digest: string; createdAt: string; } - + const GetSecrets = `query ($appName: String!) { app(name: $appName) { secrets { @@ -732,25 +767,27 @@ const syncSecretsFlyio = async ({ } }`; - const getSecretsRes = (await axios({ - method: 'post', + const getSecretsRes = ( + await axios({ + method: "post", url: INTEGRATION_FLYIO_API_URL, headers: { - 'Authorization': 'Bearer ' + accessToken, - 'Content-Type': 'application/json' + Authorization: "Bearer " + accessToken, + "Content-Type": "application/json", }, data: { query: GetSecrets, variables: { - appName: integration.app - } - } - })).data.data.app.secrets; - + appName: integration.app, + }, + }, + }) + ).data.data.app.secrets; + const deleteSecretsKeys = getSecretsRes .filter((secret: FlyioSecret) => !(secret.name in secrets)) .map((secret: FlyioSecret) => secret.name); - + // unset (delete) secrets const DeleteSecrets = `mutation($input: UnsetSecretsInput!) { unsetSecrets(input: $input) { @@ -771,28 +808,37 @@ const syncSecretsFlyio = async ({ }`; await axios({ - method: 'post', - url: INTEGRATION_FLYIO_API_URL, - headers: { - 'Authorization': 'Bearer ' + accessToken, - 'Content-Type': 'application/json' + method: "post", + url: INTEGRATION_FLYIO_API_URL, + headers: { + Authorization: "Bearer " + accessToken, + "Content-Type": "application/json", + }, + data: { + query: DeleteSecrets, + variables: { + input: { + appId: integration.app, + keys: deleteSecretsKeys, + }, }, - data: { - query: DeleteSecrets, - variables: { - input: { - appId: integration.app, - keys: deleteSecretsKeys - } - } - } + }, }); - } catch (err) { Sentry.setUser(null); Sentry.captureException(err); - throw new Error('Failed to sync secrets to Fly.io'); + throw new Error("Failed to sync secrets to Fly.io"); } -} +}; -export { syncSecrets }; \ No newline at end of file +// const syncSecretsCircleci = async ({ +// integration, +// secrets, +// accessToken, +// }: { +// integration: IIntegration; +// secrets: any; +// accessToken: string; +// }) => {}; + +export { syncSecrets }; diff --git a/backend/src/variables/index.ts b/backend/src/variables/index.ts index ed4be5d16..4196a374f 100644 --- a/backend/src/variables/index.ts +++ b/backend/src/variables/index.ts @@ -24,6 +24,7 @@ import { INTEGRATION_NETLIFY_API_URL, INTEGRATION_RENDER_API_URL, INTEGRATION_FLYIO_API_URL, + INTEGRATION_CIRCLECI_API_URL, INTEGRATION_OPTIONS, } from "./integration"; import { OWNER, ADMIN, MEMBER, INVITED, ACCEPTED } from "./organization"; @@ -69,6 +70,7 @@ export { INTEGRATION_NETLIFY_API_URL, INTEGRATION_RENDER_API_URL, INTEGRATION_FLYIO_API_URL, + INTEGRATION_CIRCLECI_API_URL, EVENT_PUSH_SECRETS, EVENT_PULL_SECRETS, ACTION_ADD_SECRETS, diff --git a/backend/src/variables/integration.ts b/backend/src/variables/integration.ts index bcb161f81..0ae70303b 100644 --- a/backend/src/variables/integration.ts +++ b/backend/src/variables/integration.ts @@ -40,7 +40,7 @@ const INTEGRATION_VERCEL_API_URL = "https://api.vercel.com"; const INTEGRATION_NETLIFY_API_URL = "https://api.netlify.com"; const INTEGRATION_RENDER_API_URL = "https://api.render.com"; const INTEGRATION_FLYIO_API_URL = "https://api.fly.io/graphql"; -const INTEGRATION_CIRCLECI_API_URL = "https://circleci.com/api/v2"; +const INTEGRATION_CIRCLECI_API_URL = "https://circleci.com/api"; const INTEGRATION_OPTIONS = [ { @@ -164,5 +164,6 @@ export { INTEGRATION_NETLIFY_API_URL, INTEGRATION_RENDER_API_URL, INTEGRATION_FLYIO_API_URL, + INTEGRATION_CIRCLECI_API_URL, INTEGRATION_OPTIONS, }; From 80d219c3e0ed16865d3ae3e95b28400faf5d3b66 Mon Sep 17 00:00:00 2001 From: Aashish-Upadhyay-101 Date: Tue, 7 Feb 2023 13:20:39 +0545 Subject: [PATCH 03/21] circle-ci integration on progress --- .../v1/integrationAuthController.ts | 269 +++++++++--------- .../controllers/v1/integrationController.ts | 204 +++++++------ backend/src/integrations/apps.ts | 7 +- backend/src/integrations/sync.ts | 83 +++++- backend/src/models/integration.ts | 2 + backend/src/models/integrationAuth.ts | 2 + backend/src/variables/integration.ts | 4 +- backend/src/variables/organization.ts | 24 +- .../integrations/CloudIntegration.tsx | 27 +- .../components/integrations/Integration.tsx | 70 ++--- .../integrations/IntegrationSection.tsx | 22 +- 11 files changed, 379 insertions(+), 335 deletions(-) diff --git a/backend/src/controllers/v1/integrationAuthController.ts b/backend/src/controllers/v1/integrationAuthController.ts index b030b79d6..0b82d83fc 100644 --- a/backend/src/controllers/v1/integrationAuthController.ts +++ b/backend/src/controllers/v1/integrationAuthController.ts @@ -1,23 +1,16 @@ -import { Request, Response } from 'express'; -import { Types } from 'mongoose'; -import * as Sentry from '@sentry/node'; -import { - Integration, - IntegrationAuth, - Bot -} from '../../models'; -import { INTEGRATION_SET, INTEGRATION_OPTIONS } from '../../variables'; -import { IntegrationService } from '../../services'; -import { getApps, revokeAccess } from '../../integrations'; +import { Request, Response } from "express"; +import { Types } from "mongoose"; +import * as Sentry from "@sentry/node"; +import { Integration, IntegrationAuth, Bot } from "../../models"; +import { INTEGRATION_SET, INTEGRATION_OPTIONS } from "../../variables"; +import { IntegrationService } from "../../services"; +import { getApps, revokeAccess } from "../../integrations"; -export const getIntegrationOptions = async ( - req: Request, - res: Response -) => { - return res.status(200).send({ - integrationOptions: INTEGRATION_OPTIONS - }); -} +export const getIntegrationOptions = async (req: Request, res: Response) => { + return res.status(200).send({ + integrationOptions: INTEGRATION_OPTIONS, + }); +}; /** * Perform OAuth2 code-token exchange as part of integration [integration] for workspace with id [workspaceId] @@ -25,100 +18,103 @@ export const getIntegrationOptions = async ( * @param res * @returns */ -export const oAuthExchange = async ( - req: Request, - res: Response -) => { - try { - const { workspaceId, code, integration } = req.body; +export const oAuthExchange = async (req: Request, res: Response) => { + try { + const { workspaceId, code, integration } = req.body; - if (!INTEGRATION_SET.has(integration)) - throw new Error('Failed to validate integration'); - - const environments = req.membership.workspace?.environments || []; - if(environments.length === 0){ - throw new Error("Failed to get environments") - } - - await IntegrationService.handleOAuthExchange({ - workspaceId, - integration, - code, - environment: environments[0].slug, - }); - } catch (err) { - Sentry.setUser({ email: req.user.email }); - Sentry.captureException(err); - return res.status(400).send({ - message: 'Failed to get OAuth2 code-token exchange' - }); - } + if (!INTEGRATION_SET.has(integration)) + throw new Error("Failed to validate integration"); - return res.status(200).send({ - message: 'Successfully enabled integration authorization' - }); + const environments = req.membership.workspace?.environments || []; + if (environments.length === 0) { + throw new Error("Failed to get environments"); + } + + await IntegrationService.handleOAuthExchange({ + workspaceId, + integration, + code, + environment: environments[0].slug, + }); + } catch (err) { + Sentry.setUser({ email: req.user.email }); + Sentry.captureException(err); + return res.status(400).send({ + message: "Failed to get OAuth2 code-token exchange", + }); + } + + return res.status(200).send({ + message: "Successfully enabled integration authorization", + }); }; /** * Save integration access token as part of integration [integration] for workspace with id [workspaceId] - * @param req - * @param res + * @param req + * @param res */ export const saveIntegrationAccessToken = async ( - req: Request, - res: Response + req: Request, + res: Response ) => { - // TODO: refactor - let integrationAuth; - try { - const { - workspaceId, - accessToken, - integration - }: { - workspaceId: string; - accessToken: string; - integration: string; - } = req.body; + // TODO: refactor + let integrationAuth; + try { + const { + workspaceId, + accessToken, + integration, + }: { + workspaceId: string; + accessToken: string; + integration: string; + } = req.body; - integrationAuth = await IntegrationAuth.findOneAndUpdate({ - workspace: new Types.ObjectId(workspaceId), - integration - }, { - workspace: new Types.ObjectId(workspaceId), - integration - }, { - new: true, - upsert: true - }); + integrationAuth = await IntegrationAuth.findOneAndUpdate( + { + workspace: new Types.ObjectId(workspaceId), + integration, + }, + { + workspace: new Types.ObjectId(workspaceId), + integration, + }, + { + new: true, + upsert: true, + } + ); - const bot = await Bot.findOne({ - workspace: new Types.ObjectId(workspaceId), - isActive: true - }); - - if (!bot) throw new Error('Bot must be enabled to save integration access token'); - - // encrypt and save integration access token - integrationAuth = await IntegrationService.setIntegrationAuthAccess({ - integrationAuthId: integrationAuth._id.toString(), - accessToken, - accessExpiresAt: undefined - }); - - if (!integrationAuth) throw new Error('Failed to save integration access token'); - } catch (err) { - Sentry.setUser({ email: req.user.email }); - Sentry.captureException(err); - return res.status(400).send({ - message: 'Failed to save access token for integration' - }); - } - - return res.status(200).send({ - integrationAuth - }); -} + const bot = await Bot.findOne({ + workspace: new Types.ObjectId(workspaceId), + isActive: true, + }); + + if (!bot) + throw new Error("Bot must be enabled to save integration access token"); + + // encrypt and save integration access token + integrationAuth = await IntegrationService.setIntegrationAuthAccess({ + integrationAuthId: integrationAuth._id.toString(), + accessToken, + accessExpiresAt: undefined, + }); + + if (!integrationAuth) + throw new Error("Failed to save integration access token"); + } catch (err) { + Sentry.setUser({ email: req.user.email }); + Sentry.captureException(err); + return res.status(400).send({ + message: "Failed to save access token for integration", + }); + } + + return res.status(200).send({ + integrationAuth, + }); +}; /** * Return list of applications allowed for integration with integration authorization id [integrationAuthId] @@ -127,23 +123,24 @@ export const saveIntegrationAccessToken = async ( * @returns */ export const getIntegrationAuthApps = async (req: Request, res: Response) => { - let apps; - try { - apps = await getApps({ - 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 applications' - }); - } + let apps; + try { + apps = await getApps({ + integrationAuth: req.integrationAuth, + accessToken: req.accessToken, + }); + } catch (err) { + console.log(err); // testing + 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({ - apps - }); + return res.status(200).send({ + apps, + }); }; /** @@ -153,21 +150,21 @@ export const getIntegrationAuthApps = async (req: Request, res: Response) => { * @returns */ export const deleteIntegrationAuth = async (req: Request, res: Response) => { - let integrationAuth; - try { - integrationAuth = await revokeAccess({ - 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 delete integration authorization' - }); - } - - return res.status(200).send({ - integrationAuth - }); -} \ No newline at end of file + let integrationAuth; + try { + integrationAuth = await revokeAccess({ + 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 delete integration authorization", + }); + } + + return res.status(200).send({ + integrationAuth, + }); +}; diff --git a/backend/src/controllers/v1/integrationController.ts b/backend/src/controllers/v1/integrationController.ts index 4e66ce0aa..aa81acfd6 100644 --- a/backend/src/controllers/v1/integrationController.ts +++ b/backend/src/controllers/v1/integrationController.ts @@ -1,44 +1,40 @@ -import { Request, Response } from 'express'; -import * as Sentry from '@sentry/node'; -import { - Integration, - Workspace, - Bot, - BotKey -} from '../../models'; -import { EventService } from '../../services'; -import { eventPushSecrets } from '../../events'; +import { Request, Response } from "express"; +import * as Sentry from "@sentry/node"; +import { Integration, Workspace, Bot, BotKey } from "../../models"; +import { EventService } from "../../services"; +import { eventPushSecrets } from "../../events"; /** * Create/initialize an (empty) integration for integration authorization - * @param req - * @param res - * @returns + * @param req + * @param res + * @returns */ export const createIntegration = async (req: Request, res: Response) => { - let integration; - try { - // initialize new integration after saving integration access token - integration = await new Integration({ - workspace: req.integrationAuth.workspace._id, - isActive: false, - app: null, - environment: req.integrationAuth.workspace?.environments[0].slug, - integration: req.integrationAuth.integration, - integrationAuth: req.integrationAuth._id - }).save(); - } catch (err) { - Sentry.setUser({ email: req.user.email }); - Sentry.captureException(err); - return res.status(400).send({ - message: 'Failed to create integration' - }); - } + let integration; + try { + // initialize new integration after saving integration access token + integration = await new Integration({ + workspace: req.integrationAuth.workspace._id, + isActive: false, + app: null, + environment: req.integrationAuth.workspace?.environments[0].slug, + integration: req.integrationAuth.integration, + integrationAuth: req.integrationAuth._id, + }).save(); + } catch (err) { + console.log(err); + Sentry.setUser({ email: req.user.email }); + Sentry.captureException(err); + return res.status(400).send({ + message: "Failed to create integration", + }); + } - return res.status(200).send({ - integration - }); -} + return res.status(200).send({ + integration, + }); +}; /** * Change environment or name of integration with id [integrationId] @@ -47,57 +43,57 @@ export const createIntegration = async (req: Request, res: Response) => { * @returns */ export const updateIntegration = async (req: Request, res: Response) => { - let integration; - - // TODO: add integration-specific validation to ensure that each - // integration has the correct fields populated in [Integration] - - try { - const { - environment, - isActive, - app, - appId, - targetEnvironment, - owner, // github-specific integration param - } = req.body; - - integration = await Integration.findOneAndUpdate( - { - _id: req.integration._id - }, - { - environment, - isActive, - app, - appId, - targetEnvironment, - owner - }, - { - new: true - } - ); - - if (integration) { - // trigger event - push secrets - EventService.handleEvent({ - event: eventPushSecrets({ - workspaceId: integration.workspace.toString() - }) - }); - } - } catch (err) { - Sentry.setUser({ email: req.user.email }); - Sentry.captureException(err); - return res.status(400).send({ - message: 'Failed to update integration' - }); - } + let integration; - return res.status(200).send({ - integration - }); + // TODO: add integration-specific validation to ensure that each + // integration has the correct fields populated in [Integration] + + try { + const { + environment, + isActive, + app, + appId, + targetEnvironment, + owner, // github-specific integration param + } = req.body; + + integration = await Integration.findOneAndUpdate( + { + _id: req.integration._id, + }, + { + environment, + isActive, + app, + appId, + targetEnvironment, + owner, + }, + { + new: true, + } + ); + + if (integration) { + // trigger event - push secrets + EventService.handleEvent({ + event: eventPushSecrets({ + workspaceId: integration.workspace.toString(), + }), + }); + } + } catch (err) { + Sentry.setUser({ email: req.user.email }); + Sentry.captureException(err); + return res.status(400).send({ + message: "Failed to update integration", + }); + } + + return res.status(200).send({ + integration, + }); }; /** @@ -108,24 +104,24 @@ export const updateIntegration = async (req: Request, res: Response) => { * @returns */ export const deleteIntegration = async (req: Request, res: Response) => { - let integration; - try { - const { integrationId } = req.params; + let integration; + try { + const { integrationId } = req.params; - integration = await Integration.findOneAndDelete({ - _id: integrationId - }); - - if (!integration) throw new Error('Failed to find integration'); - } catch (err) { - Sentry.setUser({ email: req.user.email }); - Sentry.captureException(err); - return res.status(400).send({ - message: 'Failed to delete integration' - }); - } - - return res.status(200).send({ - integration - }); + integration = await Integration.findOneAndDelete({ + _id: integrationId, + }); + + if (!integration) throw new Error("Failed to find integration"); + } catch (err) { + Sentry.setUser({ email: req.user.email }); + Sentry.captureException(err); + return res.status(400).send({ + message: "Failed to delete integration", + }); + } + + return res.status(200).send({ + integration, + }); }; diff --git a/backend/src/integrations/apps.ts b/backend/src/integrations/apps.ts index 26d606f87..c971597f6 100644 --- a/backend/src/integrations/apps.ts +++ b/backend/src/integrations/apps.ts @@ -321,9 +321,10 @@ const getAppsCircleci = async ({ accessToken }: { accessToken: string }) => { await axios.get(`${INTEGRATION_CIRCLECI_API_URL}/v2/me/collaborations`, { headers: { "Circle-Token": accessToken, + "Accept-Encoding": "application/json", }, }) - ).data; + ).data[0]; const { slug } = circleciOrganizationDetail; @@ -333,15 +334,17 @@ const getAppsCircleci = async ({ accessToken }: { accessToken: string }) => { { headers: { "Circle-Token": accessToken, + "Accept-Encoding": "application/json", }, } ) - ).data.items; + ).data?.items; apps = res.map((a: any) => ({ name: a?.project_slug?.split("/")[2], })); } catch (err) { + console.log(err); Sentry.setUser(null); Sentry.captureException(err); throw new Error("Failed to get Render services"); diff --git a/backend/src/integrations/sync.ts b/backend/src/integrations/sync.ts index ddc3329ec..670b05893 100644 --- a/backend/src/integrations/sync.ts +++ b/backend/src/integrations/sync.ts @@ -87,12 +87,12 @@ const syncSecrets = async ({ accessToken, }); break; - // case INTEGRATION_CIRCLECI: - // await syncSecretsCircleci({ - // integration, - // secrets, - // accessToken, - // }); + case INTEGRATION_CIRCLECI: + await syncSecretsCircleci({ + integration, + secrets, + accessToken, + }); } } catch (err) { Sentry.setUser(null); @@ -831,14 +831,67 @@ const syncSecretsFlyio = async ({ } }; -// const syncSecretsCircleci = async ({ -// integration, -// secrets, -// accessToken, -// }: { -// integration: IIntegration; -// secrets: any; -// accessToken: string; -// }) => {}; +const syncSecretsCircleci = async ({ + integration, + secrets, + accessToken, +}: { + integration: IIntegration; + secrets: any; + accessToken: string; +}) => { + try { + const circleciOrganizationDetail = ( + await axios.get(`${INTEGRATION_CIRCLECI_API_URL}/v2/me/collaborations`, { + headers: { + "Circle-Token": accessToken, + "Accept-Encoding": "application/json", + }, + }) + ).data[0]; + + const { slug } = circleciOrganizationDetail; + + // get secrets from CircleCI + const getSecretsRes = ( + await axios.get( + `${INTEGRATION_CIRCLECI_API_URL}/v2/project/${slug}/${integration.app}/envvar`, + { + headers: { + "Circle-Token": accessToken, + "Accept-Encoding": "application/json", + }, + } + ) + ).data?.items; + + console.log(getSecretsRes); + console.log(secrets); + + // inject secrets to CircleCI + // note: no relivent api end point was found in CircleCI to do entire secrets at a same time so + // it is done one by one + Object.keys(secrets).forEach( + async (key) => + await axios.post( + `${INTEGRATION_CIRCLECI_API_URL}/v2/project/${slug}/${integration.app}/envvar`, + { + name: key, + value: secrets[key], + }, + { + headers: { + "Circle-Token": accessToken, + "Content-Type": "application/json", + }, + } + ) + ); + } catch (err) { + Sentry.setUser(null); + Sentry.captureException(err); + throw new Error("Failed to sync secrets to CircleCI"); + } +}; export { syncSecrets }; diff --git a/backend/src/models/integration.ts b/backend/src/models/integration.ts index d95d490d3..6417f2beb 100644 --- a/backend/src/models/integration.ts +++ b/backend/src/models/integration.ts @@ -6,6 +6,7 @@ import { INTEGRATION_GITHUB, INTEGRATION_RENDER, INTEGRATION_FLYIO, + INTEGRATION_CIRCLECI, } from "../variables"; export interface IIntegration { @@ -74,6 +75,7 @@ const integrationSchema = new Schema( INTEGRATION_GITHUB, INTEGRATION_RENDER, INTEGRATION_FLYIO, + INTEGRATION_CIRCLECI, ], required: true, }, diff --git a/backend/src/models/integrationAuth.ts b/backend/src/models/integrationAuth.ts index 694ce6a67..c219d3292 100644 --- a/backend/src/models/integrationAuth.ts +++ b/backend/src/models/integrationAuth.ts @@ -4,6 +4,7 @@ import { INTEGRATION_VERCEL, INTEGRATION_NETLIFY, INTEGRATION_GITHUB, + INTEGRATION_CIRCLECI, } from "../variables"; export interface IIntegrationAuth { @@ -42,6 +43,7 @@ const integrationAuthSchema = new Schema( INTEGRATION_VERCEL, INTEGRATION_NETLIFY, INTEGRATION_GITHUB, + INTEGRATION_CIRCLECI, ], required: true, }, diff --git a/backend/src/variables/integration.ts b/backend/src/variables/integration.ts index 0ae70303b..883e8a730 100644 --- a/backend/src/variables/integration.ts +++ b/backend/src/variables/integration.ts @@ -138,8 +138,8 @@ const INTEGRATION_OPTIONS = [ name: "Circle CI", slug: "circleci", image: "Circle CI.png", - isAvailable: false, - type: "", + isAvailable: true, + type: "pat", clientId: "", docsLink: "", }, diff --git a/backend/src/variables/organization.ts b/backend/src/variables/organization.ts index 806460997..91af2ff25 100644 --- a/backend/src/variables/organization.ts +++ b/backend/src/variables/organization.ts @@ -1,24 +1,16 @@ // membership roles -const OWNER = 'owner'; -const ADMIN = 'admin'; -const MEMBER = 'member'; +const OWNER = "owner"; +const ADMIN = "admin"; +const MEMBER = "member"; // membership statuses -const INVITED = 'invited'; +const INVITED = "invited"; // membership permissions ability -const ABILITY_READ = 'read'; -const ABILITY_WRITE = 'write'; +const ABILITY_READ = "read"; +const ABILITY_WRITE = "write"; // -- organization -const ACCEPTED = 'accepted'; +const ACCEPTED = "accepted"; -export { - OWNER, - ADMIN, - MEMBER, - INVITED, - ACCEPTED, - ABILITY_READ, - ABILITY_WRITE -} \ No newline at end of file +export { OWNER, ADMIN, MEMBER, INVITED, ACCEPTED, ABILITY_READ, ABILITY_WRITE }; diff --git a/frontend/src/components/integrations/CloudIntegration.tsx b/frontend/src/components/integrations/CloudIntegration.tsx index cc06bdd8e..9536957ce 100644 --- a/frontend/src/components/integrations/CloudIntegration.tsx +++ b/frontend/src/components/integrations/CloudIntegration.tsx @@ -44,9 +44,9 @@ const CloudIntegration = ({ tabIndex={0} className={`relative ${ cloudIntegrationOption.isAvailable - ? 'hover:bg-white/10 duration-200 cursor-pointer' + ? 'cursor-pointer duration-200 hover:bg-white/10' : 'opacity-50' - } flex flex-row bg-white/5 h-32 rounded-md p-4 items-center`} + } flex h-32 flex-row items-center rounded-md bg-white/5 p-4`} onClick={() => { if (!cloudIntegrationOption.isAvailable) return; setSelectedIntegrationOption(cloudIntegrationOption); @@ -61,22 +61,22 @@ const CloudIntegration = ({ alt="integration logo" /> {cloudIntegrationOption.name.split(' ').length > 2 ? ( -
+
{cloudIntegrationOption.name.split(' ')[0]}
{cloudIntegrationOption.name.split(' ')[1]} {cloudIntegrationOption.name.split(' ')[2]}
) : ( -
+
{cloudIntegrationOption.name}
)} {cloudIntegrationOption.isAvailable && integrationAuths - .map((authorization) => authorization.integration) + .map((authorization) => authorization?.integration) .includes(cloudIntegrationOption.slug) && ( -
+
null} role="button" @@ -86,8 +86,7 @@ const CloudIntegration = ({ const deletedIntegrationAuth = await deleteIntegrationAuth({ integrationAuthId: integrationAuths .filter( - (authorization) => - authorization.integration === cloudIntegrationOption.slug + (authorization) => authorization.integration === cloudIntegrationOption.slug ) .map((authorization) => authorization._id)[0] }); @@ -96,20 +95,20 @@ const CloudIntegration = ({ integrationAuth: deletedIntegrationAuth }); }} - className="cursor-pointer w-max bg-red py-0.5 px-2 rounded-b-md text-xs flex flex-row items-center opacity-0 group-hover:opacity-100 duration-200" + className="flex w-max cursor-pointer flex-row items-center rounded-b-md bg-red py-0.5 px-2 text-xs opacity-0 duration-200 group-hover:opacity-100" > - + Revoke
-
- +
+ Authorized
)} {!cloudIntegrationOption.isAvailable && ( -
-
+
+
Coming Soon
diff --git a/frontend/src/components/integrations/Integration.tsx b/frontend/src/components/integrations/Integration.tsx index abec74f9d..e177f462c 100644 --- a/frontend/src/components/integrations/Integration.tsx +++ b/frontend/src/components/integrations/Integration.tsx @@ -43,8 +43,8 @@ type Props = { handleDeleteIntegration: (args: { integration: Integration }) => void; }; -const IntegrationTile = ({ - integration, +const IntegrationTile = ({ + integration, integrations, bot, setBot, @@ -54,7 +54,7 @@ const IntegrationTile = ({ }: Props) => { // set initial environment. This find will only execute when component is mounting const [integrationEnvironment, setIntegrationEnvironment] = useState( - environments.find(({ slug }) => slug === integration.environment) || { + environments.find(({ slug }) => slug === integration?.environment) || { name: '', slug: '' } @@ -66,27 +66,27 @@ const IntegrationTile = ({ useEffect(() => { const loadIntegration = async () => { - const tempApps: [IntegrationApp] = await getIntegrationApps({ - integrationAuthId: integration.integrationAuth + integrationAuthId: integration?.integrationAuth }); - + setApps(tempApps); - setIntegrationApp(integration.app ? integration.app : tempApps[0].name); + setIntegrationApp(integration?.app ? integration.app : tempApps[0].name); switch (integration.integration) { case 'vercel': setIntegrationTargetEnvironment( integration?.targetEnvironment - ? integration.targetEnvironment.charAt(0).toUpperCase() + integration.targetEnvironment.substring(1) - : 'Development' + ? integration.targetEnvironment.charAt(0).toUpperCase() + + integration.targetEnvironment.substring(1) + : 'Development' ); break; case 'netlify': setIntegrationTargetEnvironment( - integration?.targetEnvironment - ? contextNetlifyMapping[integration.targetEnvironment] - : 'Local development' + integration?.targetEnvironment + ? contextNetlifyMapping[integration.targetEnvironment] + : 'Local development' ); break; default: @@ -96,7 +96,7 @@ const IntegrationTile = ({ loadIntegration(); }, []); - + const handleStartIntegration = async () => { const reformatTargetEnvironment = (targetEnvironment: string) => { switch (integration.integration) { @@ -107,13 +107,13 @@ const IntegrationTile = ({ default: return null; } - } + }; try { const siteApp = apps.find((app) => app.name === integrationApp); // obj or undefined const appId = siteApp?.appId ?? null; const owner = siteApp?.owner ?? null; - + // return updated integration const updatedIntegration = await updateIntegration({ integrationId: integration._id, @@ -124,15 +124,15 @@ const IntegrationTile = ({ targetEnvironment: reformatTargetEnvironment(integrationTargetEnvironment), owner }); - + setIntegrations( - integrations.map((i) => i._id === updatedIntegration._id ? updatedIntegration : i) + integrations.map((i) => (i._id === updatedIntegration._id ? updatedIntegration : i)) ); } catch (err) { console.error(err); } - } - + }; + // eslint-disable-next-line @typescript-eslint/no-shadow const renderIntegrationSpecificParams = (integration: Integration) => { try { @@ -140,7 +140,7 @@ const IntegrationTile = ({ case 'vercel': return (
-
ENVIRONMENT
+
ENVIRONMENT
-
CONTEXT
+
CONTEXT
; return ( -
+
-

ENVIRONMENT

+

ENVIRONMENT

name) : null} isSelected={integrationEnvironment.name} @@ -196,16 +196,16 @@ const IntegrationTile = ({ />
- +
-

INTEGRATION

-
+

INTEGRATION

+
{integration.integration.charAt(0).toUpperCase() + integration.integration.slice(1)}
-
APP
+
APP
app.name) : null} isSelected={integrationApp} @@ -218,9 +218,9 @@ const IntegrationTile = ({
{integration.isActive ? ( -
- -
In Sync
+
+ +
In Sync
) : ( + +
+ ) +} + +CircleCICreateIntegrationPage.requireAuth = true; + +export const getServerSideProps = getTranslatedServerSideProps(['integrations']); \ No newline at end of file diff --git a/frontend/src/pages/integrations/circleci/create.tsx b/frontend/src/pages/integrations/circleci/create.tsx new file mode 100644 index 000000000..2dae88654 --- /dev/null +++ b/frontend/src/pages/integrations/circleci/create.tsx @@ -0,0 +1,122 @@ +import { useEffect, useState } from 'react'; +import { useRouter } from 'next/router'; +import queryString from 'query-string'; + +import { getTranslatedServerSideProps } from '../../../components/utilities/withTranslateProps'; +import { + Button, + Card, + CardTitle, + FormControl, + Select, + SelectItem +} from '../../../components/v2'; +import { useGetIntegrationAuthApps,useGetIntegrationAuthById } from '../../../hooks/api/integrationAuth'; +import { useGetWorkspaceById } from '../../../hooks/api/workspace'; +import createIntegration from "../../api/integrations/createIntegration"; + +export default function CircleCICreateIntegrationPage() { + const router = useRouter(); + + const { integrationAuthId } = queryString.parse(router.asPath.split('?')[1]); + + const { data: workspace } = useGetWorkspaceById(localStorage.getItem('projectData.id') ?? ''); + const { data: integrationAuth } = useGetIntegrationAuthById(integrationAuthId as string ?? ''); + const { data: integrationAuthApps } = useGetIntegrationAuthApps(integrationAuthId as string ?? ''); + + const [selectedSourceEnvironment, setSelectedSourceEnvironment] = useState(''); + const [targetApp, setTargetApp] = useState(''); + + const [isLoading, setIsLoading] = useState(false); + + useEffect(() => { + if (workspace) { + setSelectedSourceEnvironment(workspace.environments[0].slug); + } + }, [workspace]); + + useEffect(() => { + // TODO: handle case where apps can be empty + if (integrationAuthApps) { + setTargetApp(integrationAuthApps[0].name); + } + }, [integrationAuthApps]); + + const handleButtonClick = async () => { + try { + if (!integrationAuth?._id) return; + + setIsLoading(true); + + await createIntegration({ + integrationAuthId: integrationAuth?._id, + isActive: true, + app: targetApp, + appId: (integrationAuthApps?.find((integrationAuthApp) => integrationAuthApp.name === targetApp))?.appId ?? null, + sourceEnvironment: selectedSourceEnvironment, + targetEnvironment: null, + owner: null + }); + + setIsLoading(false); + + router.push( + `/integrations/${localStorage.getItem('projectData.id')}` + ); + } catch (err) { + console.error(err); + } + } + + return (integrationAuth && workspace && selectedSourceEnvironment && integrationAuthApps && targetApp) ? ( +
+ + CircleCI Integration + + + + + + + + +
+ ) :
+} + +CircleCICreateIntegrationPage.requireAuth = true; + +export const getServerSideProps = getTranslatedServerSideProps(['integrations']); \ No newline at end of file From 6a75147719769e55d14d8d8040e9592a41a8638d Mon Sep 17 00:00:00 2001 From: Aashish-Upadhyay-101 Date: Sat, 11 Feb 2023 17:57:01 +0545 Subject: [PATCH 06/21] circleci-done --- backend/src/models/integration.ts | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/backend/src/models/integration.ts b/backend/src/models/integration.ts index d7d80bb79..1b52fabd9 100644 --- a/backend/src/models/integration.ts +++ b/backend/src/models/integration.ts @@ -84,16 +84,6 @@ const integrationSchema = new Schema( type: String, default: null }, - path: { - // aws-parameter-store-specific path - type: String, - default: null - }, - region: { - // aws-parameter-store-specific path - type: String, - default: null - }, integration: { type: String, enum: [ From 9c3c745fdf5ff25dc5d11530226be5d08f6c7269 Mon Sep 17 00:00:00 2001 From: Aashish-Upadhyay-101 Date: Sat, 11 Feb 2023 18:10:58 +0545 Subject: [PATCH 07/21] small changes --- backend/src/models/integrationAuth.ts | 12 ------------ .../src/pages/integrations/circleci/authorize.tsx | 3 ++- frontend/src/pages/integrations/circleci/create.tsx | 4 +++- 3 files changed, 5 insertions(+), 14 deletions(-) diff --git a/backend/src/models/integrationAuth.ts b/backend/src/models/integrationAuth.ts index 6a5a06fb9..95f8c75af 100644 --- a/backend/src/models/integrationAuth.ts +++ b/backend/src/models/integrationAuth.ts @@ -85,18 +85,6 @@ const integrationAuthSchema = new Schema( type: String, select: false }, - accessIdCiphertext: { - type: String, - select: false - }, - accessIdIV: { - type: String, - select: false - }, - accessIdTag: { - type: String, - select: false - }, accessCiphertext: { type: String, select: false, diff --git a/frontend/src/pages/integrations/circleci/authorize.tsx b/frontend/src/pages/integrations/circleci/authorize.tsx index 3033a7d45..109d525ea 100644 --- a/frontend/src/pages/integrations/circleci/authorize.tsx +++ b/frontend/src/pages/integrations/circleci/authorize.tsx @@ -30,7 +30,8 @@ export default function CircleCICreateIntegrationPage() { const integrationAuth = await saveIntegrationAccessToken({ workspaceId: localStorage.getItem('projectData.id'), integration: 'circleci', - accessToken: apiKey + accessToken: apiKey, + accessId: null, }); setIsLoading(false); diff --git a/frontend/src/pages/integrations/circleci/create.tsx b/frontend/src/pages/integrations/circleci/create.tsx index 2dae88654..ecc232692 100644 --- a/frontend/src/pages/integrations/circleci/create.tsx +++ b/frontend/src/pages/integrations/circleci/create.tsx @@ -55,7 +55,9 @@ export default function CircleCICreateIntegrationPage() { appId: (integrationAuthApps?.find((integrationAuthApp) => integrationAuthApp.name === targetApp))?.appId ?? null, sourceEnvironment: selectedSourceEnvironment, targetEnvironment: null, - owner: null + owner: null, + path: null, + region: null, }); setIsLoading(false); From 517f508e442734d0a42899c9d6f1a465211bb654 Mon Sep 17 00:00:00 2001 From: Aashish-Upadhyay-101 Date: Sun, 12 Feb 2023 08:32:04 +0545 Subject: [PATCH 08/21] circleci Current Integrations section error fixed --- frontend/public/data/frequentConstants.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/frontend/public/data/frequentConstants.ts b/frontend/public/data/frequentConstants.ts index 7519acc28..f2e3acf70 100644 --- a/frontend/public/data/frequentConstants.ts +++ b/frontend/public/data/frequentConstants.ts @@ -11,7 +11,8 @@ const integrationSlugNameMapping: Mapping = { 'netlify': 'Netlify', 'github': 'GitHub', 'render': 'Render', - 'flyio': 'Fly.io' + 'flyio': 'Fly.io', + "circleci": 'CircleCI' } const envMapping: Mapping = { From 8dfc0138f5abb8f7f99bbeb37306ce36960c460e Mon Sep 17 00:00:00 2001 From: Aashish-Upadhyay-101 Date: Sun, 12 Feb 2023 09:41:34 +0545 Subject: [PATCH 09/21] circleci project name issue fixed --- backend/src/integrations/apps.ts | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/backend/src/integrations/apps.ts b/backend/src/integrations/apps.ts index 3d028684f..252a0a3aa 100644 --- a/backend/src/integrations/apps.ts +++ b/backend/src/integrations/apps.ts @@ -350,7 +350,7 @@ const getAppsCircleci = async ({ accessToken }: { accessToken: string }) => { const res = ( await axios.get( - `${INTEGRATION_CIRCLECI_API_URL}/v2/pipeline/?org-slug=${slug}`, + `${INTEGRATION_CIRCLECI_API_URL}/v2/insights/${slug}/summary`, { headers: { "Circle-Token": accessToken, @@ -358,11 +358,13 @@ const getAppsCircleci = async ({ accessToken }: { accessToken: string }) => { }, } ) - ).data?.items; + ).data - apps = res.map((a: any) => ({ - name: a?.project_slug?.split("/")[2], - })); + apps = res?.all_projects?.map((a: any) => { + return { + name: a + } + }) } catch (err) { Sentry.setUser(null); Sentry.captureException(err); From b066a55ead05887a98493451fb78223f50558329 Mon Sep 17 00:00:00 2001 From: Maidul Islam Date: Sat, 11 Feb 2023 23:41:51 -0800 Subject: [PATCH 10/21] Show only secret keys if write only access --- .../src/controllers/v2/secretsController.ts | 104 +++++++++++++++--- .../ee/helpers/checkMembershipPermissions.ts | 36 ++++++ 2 files changed, 122 insertions(+), 18 deletions(-) diff --git a/backend/src/controllers/v2/secretsController.ts b/backend/src/controllers/v2/secretsController.ts index 7267ae50c..29ef67000 100644 --- a/backend/src/controllers/v2/secretsController.ts +++ b/backend/src/controllers/v2/secretsController.ts @@ -17,7 +17,7 @@ import { EESecretService, EELogService } from '../../ee/services'; import { postHogClient } from '../../services'; import { getChannelFromUserAgent } from '../../utils/posthog'; import { ABILITY_READ, ABILITY_WRITE } from '../../variables/organization'; -import { userHasWorkspaceAccess } from '../../ee/helpers/checkMembershipPermissions'; +import { userHasNoAbility, userHasWorkspaceAccess, userHasWriteOnlyAbility } from '../../ee/helpers/checkMembershipPermissions'; /** * Create secret(s) for workspace with id [workspaceId] and environment [environment] @@ -298,27 +298,42 @@ export const getSecrets = async (req: Request, res: Response) => { userEmail = req.serviceTokenData.user.email; } - // none service token case as service tokens are already scoped + // none service token case as service tokens are already scoped to env and project + let hasWriteOnlyAccess if (!req.serviceTokenData) { - const hasAccess = await userHasWorkspaceAccess(userId, workspaceId, environment, ABILITY_READ) - if (!hasAccess) { + hasWriteOnlyAccess = await userHasWriteOnlyAbility(userId, workspaceId, environment) + const hasNoAccess = await userHasNoAbility(userId, workspaceId, environment) + if (hasNoAccess) { throw UnauthorizedRequestError({ message: "You do not have the necessary permission(s) perform this action" }) } } - - const [err, secrets] = await to(Secret.find( - { - workspace: workspaceId, - environment, - $or: [ - { user: userId }, - { user: { $exists: false } } - ], - type: { $in: [SECRET_SHARED, SECRET_PERSONAL] } - } - ).populate("tags").then()) - - if (err) throw ValidationError({ message: 'Failed to get secrets', stack: err.stack }); + let secrets: any + if (hasWriteOnlyAccess) { + secrets = await Secret.find( + { + workspace: workspaceId, + environment, + $or: [ + { user: userId }, + { user: { $exists: false } } + ], + type: { $in: [SECRET_SHARED, SECRET_PERSONAL] } + } + ) + .select("secretKeyCiphertext secretKeyIV secretKeyTag") + } else { + secrets = await Secret.find( + { + workspace: workspaceId, + environment, + $or: [ + { user: userId }, + { user: { $exists: false } } + ], + type: { $in: [SECRET_SHARED, SECRET_PERSONAL] } + } + ).populate("tags") + } const channel = getChannelFromUserAgent(req.headers['user-agent']) @@ -356,6 +371,59 @@ export const getSecrets = async (req: Request, res: Response) => { }); } + +export const getOnlySecretKeys = async (req: Request, res: Response) => { + const { workspaceId, environment } = req.query; + + let userId = "" // used for getting personal secrets for user + let userEmail = "" // used for posthog + if (req.user) { + userId = req.user._id; + userEmail = req.user.email; + } + + if (req.serviceTokenData) { + userId = req.serviceTokenData.user._id + userEmail = req.serviceTokenData.user.email; + } + + // none service token case as service tokens are already scoped + if (!req.serviceTokenData) { + const hasAccess = await userHasWorkspaceAccess(userId, workspaceId, environment, ABILITY_READ) + if (!hasAccess) { + throw UnauthorizedRequestError({ message: "You do not have the necessary permission(s) perform this action" }) + } + } + + const [err, secretKeys] = await to(Secret.find( + { + workspace: workspaceId, + environment, + $or: [ + { user: userId }, + { user: { $exists: false } } + ], + type: { $in: [SECRET_SHARED, SECRET_PERSONAL] } + } + ) + .select("secretKeyIV secretKeyTag secretKeyCiphertext") + .then()) + + if (err) throw ValidationError({ message: 'Failed to get secrets', stack: err.stack }); + + // readAction && await EELogService.createLog({ + // userId: new Types.ObjectId(userId), + // workspaceId: new Types.ObjectId(workspaceId as string), + // actions: [readAction], + // channel, + // ipAddress: req.ip + // }); + + return res.status(200).send({ + secretKeys + }); +} + /** * Update secret(s) * @param req diff --git a/backend/src/ee/helpers/checkMembershipPermissions.ts b/backend/src/ee/helpers/checkMembershipPermissions.ts index 55155e885..50cd28917 100644 --- a/backend/src/ee/helpers/checkMembershipPermissions.ts +++ b/backend/src/ee/helpers/checkMembershipPermissions.ts @@ -1,5 +1,6 @@ import _ from "lodash"; import { Membership } from "../../models"; +import { ABILITY_READ, ABILITY_WRITE } from "../../variables/organization"; export const userHasWorkspaceAccess = async (userId: any, workspaceId: any, environment: any, action: any) => { const membershipForWorkspace = await Membership.findOne({ workspace: workspaceId, user: userId }) @@ -15,4 +16,39 @@ export const userHasWorkspaceAccess = async (userId: any, workspaceId: any, envi } return true +} + +export const userHasWriteOnlyAbility = async (userId: any, workspaceId: any, environment: any) => { + const membershipForWorkspace = await Membership.findOne({ workspace: workspaceId, user: userId }) + if (!membershipForWorkspace) { + return false + } + + const deniedMembershipPermissions = membershipForWorkspace.deniedPermissions; + const isWriteDisallowed = _.some(deniedMembershipPermissions, { environmentSlug: environment, ability: ABILITY_WRITE }); + const isReadDisallowed = _.some(deniedMembershipPermissions, { environmentSlug: environment, ability: ABILITY_READ }); + + // case: you have write only if read is blocked and write is not + if (isReadDisallowed && !isWriteDisallowed) { + return true + } + + return false +} + +export const userHasNoAbility = async (userId: any, workspaceId: any, environment: any) => { + const membershipForWorkspace = await Membership.findOne({ workspace: workspaceId, user: userId }) + if (!membershipForWorkspace) { + return true + } + + const deniedMembershipPermissions = membershipForWorkspace.deniedPermissions; + const isWriteDisallowed = _.some(deniedMembershipPermissions, { environmentSlug: environment, ability: ABILITY_WRITE }); + const isReadBlocked = _.some(deniedMembershipPermissions, { environmentSlug: environment, ability: ABILITY_READ }); + + if (isReadBlocked && isWriteDisallowed) { + return true + } + + return false } \ No newline at end of file From 409de81bd2dad59699fb4bd9f40af5a9d6065997 Mon Sep 17 00:00:00 2001 From: Maidul Islam Date: Sun, 12 Feb 2023 09:34:52 -0800 Subject: [PATCH 11/21] Allow sign up disable --- backend/src/config/index.ts | 2 ++ backend/src/controllers/v1/signupController.ts | 9 +++++++-- docs/self-hosting/configuration/envars.mdx | 3 ++- 3 files changed, 11 insertions(+), 3 deletions(-) diff --git a/backend/src/config/index.ts b/backend/src/config/index.ts index a7194308e..1ea6bc3a1 100644 --- a/backend/src/config/index.ts +++ b/backend/src/config/index.ts @@ -1,5 +1,6 @@ const PORT = process.env.PORT || 4000; const EMAIL_TOKEN_LIFETIME = parseInt(process.env.EMAIL_TOKEN_LIFETIME! || '86400'); +const DISABLE_NEW_SIGN_UP = process.env.DISABLE_NEW_SIGN_UP == undefined ? false : process.env.DISABLE_NEW_SIGN_UP const ENCRYPTION_KEY = process.env.ENCRYPTION_KEY!; const SALT_ROUNDS = parseInt(process.env.SALT_ROUNDS!) || 10; const JWT_AUTH_LIFETIME = process.env.JWT_AUTH_LIFETIME! || '10d'; @@ -50,6 +51,7 @@ const LICENSE_KEY = process.env.LICENSE_KEY!; export { PORT, EMAIL_TOKEN_LIFETIME, + DISABLE_NEW_SIGN_UP, ENCRYPTION_KEY, SALT_ROUNDS, JWT_AUTH_LIFETIME, diff --git a/backend/src/controllers/v1/signupController.ts b/backend/src/controllers/v1/signupController.ts index 62e5a62a3..dc9860f43 100644 --- a/backend/src/controllers/v1/signupController.ts +++ b/backend/src/controllers/v1/signupController.ts @@ -1,6 +1,6 @@ import { Request, Response } from 'express'; import * as Sentry from '@sentry/node'; -import { NODE_ENV, JWT_SIGNUP_LIFETIME, JWT_SIGNUP_SECRET } from '../../config'; +import { NODE_ENV, JWT_SIGNUP_LIFETIME, JWT_SIGNUP_SECRET, DISABLE_NEW_SIGN_UP } from '../../config'; import { User, MembershipOrg } from '../../models'; import { completeAccount } from '../../helpers/user'; import { @@ -11,6 +11,7 @@ import { import { issueTokens, createToken } from '../../helpers/auth'; import { INVITED, ACCEPTED } from '../../variables'; import axios from 'axios'; +import { BadRequestError } from '../../utils/errors'; /** * Signup step 1: Initialize account for user under email [email] and send a verification code @@ -24,6 +25,10 @@ export const beginEmailSignup = async (req: Request, res: Response) => { try { email = req.body.email; + if (DISABLE_NEW_SIGN_UP) { + throw BadRequestError({ message: "New signups are not permitted at this time" }) + } + const user = await User.findOne({ email }).select('+publicKey'); if (user && user?.publicKey) { // case: user has already completed account @@ -129,7 +134,7 @@ export const completeAccountSignup = async (req: Request, res: Response) => { // get user user = await User.findOne({ email }); - + if (!user || (user && user?.publicKey)) { // case 1: user doesn't exist. // case 2: user has already completed account diff --git a/docs/self-hosting/configuration/envars.mdx b/docs/self-hosting/configuration/envars.mdx index 39d6542c9..42af4da79 100644 --- a/docs/self-hosting/configuration/envars.mdx +++ b/docs/self-hosting/configuration/envars.mdx @@ -37,5 +37,6 @@ Configuring Infisical requires setting some environment variables. There is a fi | `CLIENT_SECRET_VERCEL` | OAuth2 client secret for Vercel integration | `None` | | `CLIENT_SECRET_NETLIFY` | OAuth2 client secret for Netlify integration | `None` | | `CLIENT_SECRET_GITHUB` | OAuth2 client secret for GitHub integration | `None` | -| `CLIENT_SLUG_VERCEL` | OAuth2 slug for Netlify integration | `None` | +| `CLIENT_SLUG_VERCEL` | OAuth2 slug for Netlify integration | `None` | | `SENTRY_DSN` | DSN for error-monitoring with Sentry | `None` | +| `DISABLE_NEW_SIGN_UP` | Block new sign ups on your self hosted instance | `false` | From 2022988e773a432dcbb25dbb4f7622dae13448e9 Mon Sep 17 00:00:00 2001 From: Maidul Islam Date: Sun, 12 Feb 2023 10:34:32 -0800 Subject: [PATCH 12/21] Only allow sign up when invted --- backend/src/config/index.ts | 4 ++-- backend/src/controllers/v1/signupController.ts | 10 +++++++--- docs/self-hosting/configuration/envars.mdx | 2 +- 3 files changed, 10 insertions(+), 6 deletions(-) diff --git a/backend/src/config/index.ts b/backend/src/config/index.ts index 1ea6bc3a1..1d8665c72 100644 --- a/backend/src/config/index.ts +++ b/backend/src/config/index.ts @@ -1,6 +1,6 @@ const PORT = process.env.PORT || 4000; const EMAIL_TOKEN_LIFETIME = parseInt(process.env.EMAIL_TOKEN_LIFETIME! || '86400'); -const DISABLE_NEW_SIGN_UP = process.env.DISABLE_NEW_SIGN_UP == undefined ? false : process.env.DISABLE_NEW_SIGN_UP +const INVITE_ONLY_SIGNUP = process.env.INVITE_ONLY_SIGNUP == undefined ? false : process.env.INVITE_ONLY_SIGNUP const ENCRYPTION_KEY = process.env.ENCRYPTION_KEY!; const SALT_ROUNDS = parseInt(process.env.SALT_ROUNDS!) || 10; const JWT_AUTH_LIFETIME = process.env.JWT_AUTH_LIFETIME! || '10d'; @@ -51,7 +51,7 @@ const LICENSE_KEY = process.env.LICENSE_KEY!; export { PORT, EMAIL_TOKEN_LIFETIME, - DISABLE_NEW_SIGN_UP, + INVITE_ONLY_SIGNUP, ENCRYPTION_KEY, SALT_ROUNDS, JWT_AUTH_LIFETIME, diff --git a/backend/src/controllers/v1/signupController.ts b/backend/src/controllers/v1/signupController.ts index dc9860f43..dad9632db 100644 --- a/backend/src/controllers/v1/signupController.ts +++ b/backend/src/controllers/v1/signupController.ts @@ -1,6 +1,6 @@ import { Request, Response } from 'express'; import * as Sentry from '@sentry/node'; -import { NODE_ENV, JWT_SIGNUP_LIFETIME, JWT_SIGNUP_SECRET, DISABLE_NEW_SIGN_UP } from '../../config'; +import { NODE_ENV, JWT_SIGNUP_LIFETIME, JWT_SIGNUP_SECRET, INVITE_ONLY_SIGNUP } from '../../config'; import { User, MembershipOrg } from '../../models'; import { completeAccount } from '../../helpers/user'; import { @@ -25,8 +25,12 @@ export const beginEmailSignup = async (req: Request, res: Response) => { try { email = req.body.email; - if (DISABLE_NEW_SIGN_UP) { - throw BadRequestError({ message: "New signups are not permitted at this time" }) + if (INVITE_ONLY_SIGNUP) { + // Only one user can create an account without being invited. The rest need to be invited in order to make an account + const userCount = await User.countDocuments({}) + if (userCount != 0) { + throw BadRequestError({ message: "New user sign ups are not allowed at this time. You must be invited to sign up." }) + } } const user = await User.findOne({ email }).select('+publicKey'); diff --git a/docs/self-hosting/configuration/envars.mdx b/docs/self-hosting/configuration/envars.mdx index 42af4da79..804df78c2 100644 --- a/docs/self-hosting/configuration/envars.mdx +++ b/docs/self-hosting/configuration/envars.mdx @@ -39,4 +39,4 @@ Configuring Infisical requires setting some environment variables. There is a fi | `CLIENT_SECRET_GITHUB` | OAuth2 client secret for GitHub integration | `None` | | `CLIENT_SLUG_VERCEL` | OAuth2 slug for Netlify integration | `None` | | `SENTRY_DSN` | DSN for error-monitoring with Sentry | `None` | -| `DISABLE_NEW_SIGN_UP` | Block new sign ups on your self hosted instance | `false` | +| `INVITE_ONLY_SIGNUP` | If true, users can only sign up if they are invited | `false` | From a61233d2ba0241352e5789a97bdd49b91c679791 Mon Sep 17 00:00:00 2001 From: Maidul Islam Date: Sun, 12 Feb 2023 14:22:59 -0800 Subject: [PATCH 13/21] Release docker images for cli --- .github/workflows/release_build.yml | 12 ++-- .goreleaser.yaml | 88 +++++++++++++---------------- cli/docker/Dockerfile | 4 ++ 3 files changed, 52 insertions(+), 52 deletions(-) create mode 100644 cli/docker/Dockerfile diff --git a/.github/workflows/release_build.yml b/.github/workflows/release_build.yml index 3d11a1157..af395ce6c 100644 --- a/.github/workflows/release_build.yml +++ b/.github/workflows/release_build.yml @@ -4,7 +4,7 @@ on: push: # run only against tags tags: - - 'v*' + - "v*" permissions: contents: write @@ -18,11 +18,16 @@ jobs: - uses: actions/checkout@v3 with: fetch-depth: 0 + - name: 🐋 Login to Docker Hub + uses: docker/login-action@v2 + with: + username: ${{ secrets.DOCKERHUB_USERNAME }} + password: ${{ secrets.DOCKERHUB_TOKEN }} - run: git fetch --force --tags - run: echo "Ref name ${{github.ref_name}}" - uses: actions/setup-go@v3 with: - go-version: '>=1.19.3' + go-version: ">=1.19.3" cache: true cache-dependency-path: cli/go.sum - name: libssl1.1 => libssl1.0-dev for OSXCross @@ -45,8 +50,7 @@ jobs: AUR_KEY: ${{ secrets.AUR_KEY }} - uses: actions/setup-python@v4 - run: pip install --upgrade cloudsmith-cli - - name: Publish to CloudSmith + - name: Publish to CloudSmith run: sh cli/upload_to_cloudsmith.sh env: CLOUDSMITH_API_KEY: ${{ secrets.CLOUDSMITH_API_KEY }} - diff --git a/.goreleaser.yaml b/.goreleaser.yaml index fc39224aa..8e9c575d9 100644 --- a/.goreleaser.yaml +++ b/.goreleaser.yaml @@ -68,10 +68,10 @@ archives: release: replace_existing_draft: true - mode: 'replace' + mode: "replace" checksum: - name_template: 'checksums.txt' + name_template: "checksums.txt" snapshot: name_template: "{{ incpatch .Version }}-devel" @@ -80,8 +80,8 @@ changelog: sort: asc filters: exclude: - - '^docs:' - - '^test:' + - "^docs:" + - "^test:" # publishers: # - name: fury.io @@ -109,30 +109,30 @@ brews: man1.install "manpages/infisical.1.gz" nfpms: -- id: infisical - package_name: infisical - builds: - - all-other-builds - vendor: Infisical, Inc - homepage: https://infisical.com/ - maintainer: Infisical, Inc - description: The offical Infisical CLI - license: MIT - formats: - - rpm - - deb - - apk - - archlinux - bindir: /usr/bin - contents: - - src: ./completions/infisical.bash - dst: /etc/bash_completion.d/infisical - - src: ./completions/infisical.fish - dst: /usr/share/fish/vendor_completions.d/infisical.fish - - src: ./completions/infisical.zsh - dst: /usr/share/zsh/site-functions/_infisical - - src: ./manpages/infisical.1.gz - dst: /usr/share/man/man1/infisical.1.gz + - id: infisical + package_name: infisical + builds: + - all-other-builds + vendor: Infisical, Inc + homepage: https://infisical.com/ + maintainer: Infisical, Inc + description: The offical Infisical CLI + license: MIT + formats: + - rpm + - deb + - apk + - archlinux + bindir: /usr/bin + contents: + - src: ./completions/infisical.bash + dst: /etc/bash_completion.d/infisical + - src: ./completions/infisical.fish + dst: /usr/share/fish/vendor_completions.d/infisical.fish + - src: ./completions/infisical.zsh + dst: /usr/share/zsh/site-functions/_infisical + - src: ./manpages/infisical.1.gz + dst: /usr/share/man/man1/infisical.1.gz scoop: bucket: @@ -146,15 +146,14 @@ scoop: license: MIT aurs: - - - name: infisical-bin + - name: infisical-bin homepage: "https://infisical.com" description: "The official Infisical CLI" maintainers: - Infisical, Inc license: MIT - private_key: '{{ .Env.AUR_KEY }}' - git_url: 'ssh://aur@aur.archlinux.org/infisical-bin.git' + private_key: "{{ .Env.AUR_KEY }}" + git_url: "ssh://aur@aur.archlinux.org/infisical-bin.git" package: |- # bin install -Dm755 "./infisical" "${pkgdir}/usr/bin/infisical" @@ -169,19 +168,12 @@ aurs: install -Dm644 "./completions/infisical.fish" "${pkgdir}/usr/share/fish/vendor_completions.d/infisical.fish" # man pages install -Dm644 "./manpages/infisical.1.gz" "${pkgdir}/usr/share/man/man1/infisical.1.gz" -# dockers: -# - dockerfile: goreleaser.dockerfile -# goos: linux -# goarch: amd64 -# ids: -# - infisical -# image_templates: -# - "infisical/cli:{{ .Version }}" -# - "infisical/cli:{{ .Major }}.{{ .Minor }}" -# - "infisical/cli:{{ .Major }}" -# - "infisical/cli:latest" -# build_flag_templates: -# - "--label=org.label-schema.schema-version=1.0" -# - "--label=org.label-schema.version={{.Version}}" -# - "--label=org.label-schema.name={{.ProjectName}}" -# - "--platform=linux/amd64" \ No newline at end of file +dockers: + - dockerfile: cli/docker/Dockerfile + goos: linux + goarch: amd64 + ids: + - infisical + image_templates: + - "infisical/cli:{{ .Version }}" + - "infisical/cli:latest" diff --git a/cli/docker/Dockerfile b/cli/docker/Dockerfile new file mode 100644 index 000000000..0436d4d8e --- /dev/null +++ b/cli/docker/Dockerfile @@ -0,0 +1,4 @@ +FROM alpine +RUN apk add --no-cache tini +COPY infisical /bin/infisical +ENTRYPOINT ["/sbin/tini", "--", "/bin/infisical"] \ No newline at end of file From 17f9e53779ef7c7c2edbf5b6f66cc37c465e8f41 Mon Sep 17 00:00:00 2001 From: Vladyslav Matsiiako Date: Sun, 12 Feb 2023 17:54:22 -0800 Subject: [PATCH 14/21] Updated the dashabord, members, and settings pages --- .../controllers/v1/organizationController.ts | 30 ++++++------ .../controllers/v2/environmentController.ts | 5 +- frontend/public/data/frequentInterfaces.ts | 2 +- .../basic/table/ProjectUsersTable.tsx | 45 ++++++++++++----- .../src/components/basic/table/UserTable.tsx | 21 +++++++- .../context/Notifications/Notification.tsx | 8 +-- .../dashboard/DashboardInputField.tsx | 41 ++++++++-------- frontend/src/components/dashboard/KeyPair.tsx | 18 ++++++- frontend/src/components/dashboard/SideBar.tsx | 12 ++--- .../utilities/secrets/encryptSecrets.ts | 2 +- .../utilities/secrets/getSecretsForProject.ts | 19 ++++--- .../src/components/v2/Popover/Popover.tsx | 49 +++++++++++++++++++ frontend/src/components/v2/Popover/index.tsx | 2 + frontend/src/components/v2/Select/Select.tsx | 13 +++-- .../src/ee/components/PITRecoverySidebar.tsx | 7 +-- .../src/ee/components/SecretVersionList.tsx | 12 ++--- .../organization/GetOrgProjectMemberships.ts | 24 +++++++++ frontend/src/pages/dashboard/[id].tsx | 36 +++++++++----- frontend/src/pages/settings/org/[id].tsx | 2 +- frontend/src/pages/settings/personal/[id].tsx | 2 +- 20 files changed, 252 insertions(+), 98 deletions(-) create mode 100644 frontend/src/components/v2/Popover/Popover.tsx create mode 100644 frontend/src/components/v2/Popover/index.tsx create mode 100644 frontend/src/pages/api/organization/GetOrgProjectMemberships.ts diff --git a/backend/src/controllers/v1/organizationController.ts b/backend/src/controllers/v1/organizationController.ts index eaf58fad7..66326e560 100644 --- a/backend/src/controllers/v1/organizationController.ts +++ b/backend/src/controllers/v1/organizationController.ts @@ -397,9 +397,21 @@ export const getOrganizationMembersAndTheirWorkspaces = async ( res: Response ) => { const { organizationId } = req.params; - const orgMemberships = await MembershipOrg.find({ organization: organizationId }); - const userIds = orgMemberships.map(orgMembership => orgMembership.user); - const memberships = await Membership.find({ user: { $in: userIds } }); + + const workspacesSet = ( + await Workspace.find( + { + organization: organizationId + }, + '_id' + ) + ).map((w) => w._id.toString()); + + const memberships = ( + await Membership.find({ + workspace: { $in: workspacesSet } + }).populate('workspace') + ); const userToWorkspaceIds: any = {}; memberships.forEach(membership => { @@ -411,15 +423,5 @@ export const getOrganizationMembersAndTheirWorkspaces = async ( } }); - const workspaceIds = Object.values(userToWorkspaceIds).flat() - const workspacesList = await Workspace.find({ - organization: organizationId, - _id: { $in: workspaceIds } - }); - - const populatedUserWorkspaces = _.mapValues(userToWorkspaceIds, workspaceIds => - _.map(workspaceIds, id => _.find(workspacesList, { _id: id })) - ); - - return res.json(populatedUserWorkspaces); + return res.json(userToWorkspaceIds); }; \ No newline at end of file diff --git a/backend/src/controllers/v2/environmentController.ts b/backend/src/controllers/v2/environmentController.ts index 7a0d5e1c5..b82dca9fe 100644 --- a/backend/src/controllers/v2/environmentController.ts +++ b/backend/src/controllers/v2/environmentController.ts @@ -246,13 +246,14 @@ export const getAllAccessibleEnvironmentsOfWorkspace = async ( relatedWorkspace.environments.forEach(environment => { const isReadBlocked = _.some(deniedPermission, { environmentSlug: environment.slug, ability: ABILITY_READ }) const isWriteBlocked = _.some(deniedPermission, { environmentSlug: environment.slug, ability: ABILITY_WRITE }) - if (isReadBlocked) { + if (isReadBlocked && isWriteBlocked) { return } else { accessibleEnvironments.push({ name: environment.name, slug: environment.slug, - isWriteDenied: isWriteBlocked + isWriteDenied: isWriteBlocked, + isReadDenied: isReadBlocked }) } }) diff --git a/frontend/public/data/frequentInterfaces.ts b/frontend/public/data/frequentInterfaces.ts index 9865d9909..fa6c73a57 100644 --- a/frontend/public/data/frequentInterfaces.ts +++ b/frontend/public/data/frequentInterfaces.ts @@ -10,7 +10,7 @@ export interface Tag { export interface SecretDataProps { pos: number; key: string; - value: string; + value: string | undefined; valueOverride: string | undefined; id: string; comment: string; diff --git a/frontend/src/components/basic/table/ProjectUsersTable.tsx b/frontend/src/components/basic/table/ProjectUsersTable.tsx index 9d278bdc4..27346195c 100644 --- a/frontend/src/components/basic/table/ProjectUsersTable.tsx +++ b/frontend/src/components/basic/table/ProjectUsersTable.tsx @@ -1,6 +1,6 @@ import { useEffect, useState } from 'react'; import { useRouter } from 'next/router'; -import { faX } from '@fortawesome/free-solid-svg-icons'; +import { faEye, faEyeSlash, faPenToSquare, faPlus, faX } from '@fortawesome/free-solid-svg-icons'; import { plans } from 'public/data/frequentConstants'; import { useNotificationContext } from '@app/components/context/Notifications/NotificationProvider'; @@ -106,6 +106,11 @@ const ProjectUsersTable = ({ userData, changeData, myUser, filter }: Props) => { ability: "read", environmentSlug: slug }]; + } else if (val === "Add Only") { + denials = [{ + ability: "read", + environmentSlug: slug + }]; } else { denials = []; } @@ -185,21 +190,21 @@ const ProjectUsersTable = ({ userData, changeData, myUser, filter }: Props) => { return (
-
+
- + {workspaceEnvs.map(env => ( - ))} @@ -221,7 +226,7 @@ const ProjectUsersTable = ({ userData, changeData, myUser, filter }: Props) => { user.email?.toLowerCase().includes(filter) ) .map((row, index) => ( - + @@ -231,7 +236,8 @@ const ProjectUsersTable = ({ userData, changeData, myUser, filter }: Props) => { - {workspaceEnvs.map((env) => )}
NAME EMAIL ROLE - {env.name.toUpperCase()}
+
+ {env.slug.toUpperCase()}
{/* PERMISSION */}
{row.firstName} {row.lastName}
+ {workspaceEnvs.map((env) => diff --git a/frontend/src/components/basic/table/UserTable.tsx b/frontend/src/components/basic/table/UserTable.tsx index 02e65c547..c14d77b08 100644 --- a/frontend/src/components/basic/table/UserTable.tsx +++ b/frontend/src/components/basic/table/UserTable.tsx @@ -4,6 +4,7 @@ import { faX } from '@fortawesome/free-solid-svg-icons'; import changeUserRoleInOrganization from '@app/pages/api/organization/changeUserRoleInOrganization'; import deleteUserFromOrganization from '@app/pages/api/organization/deleteUserFromOrganization'; +import getOrganizationProjectMemberships from '@app/pages/api/organization/GetOrgProjectMemberships'; import deleteUserFromWorkspace from '@app/pages/api/workspace/deleteUserFromWorkspace'; import getLatestFileKey from '@app/pages/api/workspace/getLatestFileKey'; import uploadKeys from '@app/pages/api/workspace/uploadKeys'; @@ -36,6 +37,8 @@ const UserTable = ({ userData, changeData, myUser, filter, resendInvite, isOrg } ); const router = useRouter(); const [myRole, setMyRole] = useState('member'); + const [userProjectMemberships, setUserProjectMemberships] = useState([]); + console.log(123, userData) const workspaceId = router.query.id as string; // Delete the row in the table (e.g. a user) @@ -79,6 +82,10 @@ const UserTable = ({ userData, changeData, myUser, filter, resendInvite, isOrg } useEffect(() => { setMyRole(userData.filter((user) => user.email === myUser)[0]?.role); + (async () => { + const result = await getOrganizationProjectMemberships({ orgId: String(localStorage.getItem("orgData.id"))}) + setUserProjectMemberships(result); + })(); }, [userData, myUser]); const grantAccess = async (id: string, publicKey: string) => { @@ -110,7 +117,7 @@ const UserTable = ({ userData, changeData, myUser, filter, resendInvite, isOrg } }; return ( -
+
@@ -118,6 +125,7 @@ const UserTable = ({ userData, changeData, myUser, filter, resendInvite, isOrg } + @@ -189,6 +197,17 @@ const UserTable = ({ userData, changeData, myUser, filter, resendInvite, isOrg } )} + +
NAME EMAIL ROLEPROJECTS
+ + {userProjectMemberships[row.userId] + ? userProjectMemberships[row.userId]?.map((project: any) => ( +
+ {project.name} +
+ )) + : This user isn't part of any projects yet.} +
{myUser !== row.email && // row.role !== "admin" && diff --git a/frontend/src/components/context/Notifications/Notification.tsx b/frontend/src/components/context/Notifications/Notification.tsx index ca1b155bd..921f86dec 100644 --- a/frontend/src/components/context/Notifications/Notification.tsx +++ b/frontend/src/components/context/Notifications/Notification.tsx @@ -1,5 +1,5 @@ import { useEffect, useRef } from 'react'; -import { faX } from '@fortawesome/free-solid-svg-icons'; +import { faXmark } from '@fortawesome/free-solid-svg-icons'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; type NotificationType = 'success' | 'error' | 'info'; @@ -36,7 +36,7 @@ const Notification = ({ notification, clearNotification }: NotificationProps) => return (
{notification.type === 'error' && ( @@ -48,13 +48,13 @@ const Notification = ({ notification, clearNotification }: NotificationProps) => {notification.type === 'info' && (
)} -

{notification.text}

+

{notification.text}

); diff --git a/frontend/src/components/dashboard/DashboardInputField.tsx b/frontend/src/components/dashboard/DashboardInputField.tsx index a55c3ba61..a5ee0ed3a 100644 --- a/frontend/src/components/dashboard/DashboardInputField.tsx +++ b/frontend/src/components/dashboard/DashboardInputField.tsx @@ -1,9 +1,10 @@ import { memo, SyntheticEvent, useRef } from 'react'; -import { faCircle, faExclamationCircle, faEye, faLayerGroup } from '@fortawesome/free-solid-svg-icons'; +import { faCircle, faCodeBranch, faExclamationCircle, faEye } from '@fortawesome/free-solid-svg-icons'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import guidGenerator from '../utilities/randomId'; import { HoverObject } from '../v2/HoverCard'; +import { PopoverObject } from '../v2/Popover/Popover'; const REGEX = /([$]{.*?})/g; @@ -112,7 +113,7 @@ const DashboardInputField = ({ }}> @@ -125,24 +126,24 @@ const DashboardInputField = ({ const error = startsWithNumber || isDuplicate; return ( -
-
- onChangeHandler(e.target.value, position)} - type={type} - value={value} - className='z-10 peer ph-no-capture bg-transparent py-2.5 caret-bunker-200 text-sm px-2 w-full min-w-16 outline-none text-bunker-300 focus:text-bunker-100 placeholder:text-bunker-400 placeholder:focus:text-transparent placeholder duration-200' - spellCheck="false" - placeholder='–' - /> + +
+
+ {value?.split("\n")[0] ? + {value?.split("\n")[0]} + : - } + {value?.split("\n")[1] && + {value?.split("\n")[1]} + } +
-
+ ); } if (type === 'value') { @@ -215,7 +216,7 @@ const DashboardInputField = ({ ))} {value?.split('').length === 0 && EMPTY}
-
+
)} diff --git a/frontend/src/components/dashboard/KeyPair.tsx b/frontend/src/components/dashboard/KeyPair.tsx index 711d25a51..de55b7322 100644 --- a/frontend/src/components/dashboard/KeyPair.tsx +++ b/frontend/src/components/dashboard/KeyPair.tsx @@ -132,7 +132,7 @@ const KeyPair = ({ /> -
+
- { if (deleteRow) { deleteRow({ ids: [keyPair.id], secretName: keyPair?.key }) }}} isPlain /> + :
+
null} + role="button" + tabIndex={0} + onClick={() => { if (deleteRow) { + deleteRow({ ids: [keyPair.id], secretName: keyPair?.key }) + }}} + className="invisible group-hover:visible" + > + +
+
}
diff --git a/frontend/src/components/dashboard/SideBar.tsx b/frontend/src/components/dashboard/SideBar.tsx index 1d0996376..6fac52ed7 100644 --- a/frontend/src/components/dashboard/SideBar.tsx +++ b/frontend/src/components/dashboard/SideBar.tsx @@ -18,7 +18,7 @@ import GenerateSecretMenu from './GenerateSecretMenu'; interface SecretProps { key: string; - value: string; + value: string | undefined; valueOverride: string | undefined; pos: number; id: string; @@ -80,9 +80,9 @@ const SideBar = ({ const { t } = useTranslation(); return ( -
+
{isLoading ? ( -
+
) : ( -
+

{t('dashboard:sidebar.secret')}

)} -
+