This commit is contained in:
Vladyslav Matsiiako
2022-12-22 14:31:29 -05:00
71 changed files with 2606 additions and 1333 deletions

View File

@ -1,7 +1,5 @@
# Keys
# Required keys for platform encryption/decryption ops
PRIVATE_KEY=replace_with_nacl_sk
PUBLIC_KEY=replace_with_nacl_pk
# Required key for platform encryption/decryption ops
ENCRYPTION_KEY=replace_with_lengthy_secure_hex
# JWT
@ -9,13 +7,13 @@ ENCRYPTION_KEY=replace_with_lengthy_secure_hex
JWT_SIGNUP_SECRET=replace_with_lengthy_secure_hex
JWT_REFRESH_SECRET=replace_with_lengthy_secure_hex
JWT_AUTH_SECRET=replace_with_lengthy_secure_hex
JWT_SERVICE_SECRET=replace_with_lengthy_secure_hex
# JWT lifetime
# Optional lifetimes for JWT tokens expressed in seconds or a string
# describing a time span (e.g. 60, "2 days", "10h", "7d")
JWT_AUTH_LIFETIME=
JWT_REFRESH_LIFETIME=
JWT_SERVICE_SECRET=
JWT_SIGNUP_LIFETIME=
# Optional lifetimes for OTP expressed in seconds
@ -33,26 +31,31 @@ MONGO_PASSWORD=example
# Website URL
# Required
SITE_URL=http://localhost:8080
# Mail/SMTP
# Required to send emails
# By default, SMTP_HOST is set to smtp.gmail.com
# By default, SMTP_HOST is set to smtp.gmail.com, SMTP_PORT is set to 587, SMTP_TLS is set to false, and SMTP_FROM_NAME is set to Infisical
SMTP_HOST=smtp.gmail.com
# If STARTTLS is supported, the connection will be upgraded to TLS when SMTP_SECURE is set to false
SMTP_SECURE=false
SMTP_PORT=587
SMTP_NAME=Team
SMTP_USERNAME=team@infisical.com
SMTP_USERNAME=
SMTP_PASSWORD=
SMTP_FROM_ADDRESS=
SMTP_FROM_NAME=Infisical
# Integration
# Optional only if integration is used
CLIENT_ID_HEROKU=
CLIENT_ID_VERCEL=
CLIENT_ID_NETLIFY=
CLIENT_ID_GITHUB=
CLIENT_SECRET_HEROKU=
CLIENT_SECRET_VERCEL=
CLIENT_SECRET_NETLIFY=
CLIENT_SECRET_GITHUB=
CLIENT_SLUG_VERCEL=
# Sentry (optional) for monitoring errors
SENTRY_DSN=

41
.github/workflows/be-test-report.yml vendored Normal file
View File

@ -0,0 +1,41 @@
name: "Backend Test Report"
on:
workflow_run:
workflows: ["Check Backend Pull Request"]
types:
- completed
jobs:
be-report:
name: Backend test report
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: 📁 Download test results
id: download-artifact
uses: dawidd6/action-download-artifact@v2
with:
name: be-test-results
path: backend
workflow: check-be-pull-request.yml
workflow_conclusion: success
- name: 📋 Publish test results
uses: dorny/test-reporter@v1
with:
name: Test Results
path: reports/jest-*.xml
reporter: jest-junit
working-directory: backend
- name: 📋 Publish coverage
uses: ArtiomTr/jest-coverage-report-action@v2
id: coverage
with:
output: comment, report-markdown
coverage-file: coverage/report.json
github-token: ${{ secrets.GITHUB_TOKEN }}
working-directory: backend
- uses: marocchino/sticky-pull-request-comment@v2
with:
message: ${{ steps.coverage.outputs.report }}

View File

@ -1,41 +1,42 @@
name: Check Backend Pull Request
name: "Check Backend Pull Request"
on:
pull_request:
types: [ opened, synchronize ]
types: [opened, synchronize]
paths:
- 'backend/**'
- '!backend/README.md'
- '!backend/.*'
- 'backend/.eslintrc.js'
- "backend/**"
- "!backend/README.md"
- "!backend/.*"
- "backend/.eslintrc.js"
jobs:
check-be-pr:
name: Check
runs-on: ubuntu-latest
steps:
-
name: ☁️ Checkout source
- name: ☁️ Checkout source
uses: actions/checkout@v3
-
name: 🔧 Setup Node 16
- name: 🔧 Setup Node 16
uses: actions/setup-node@v3
with:
node-version: '16'
cache: 'npm'
node-version: "16"
cache: "npm"
cache-dependency-path: backend/package-lock.json
-
name: 📦 Install dependencies
- name: 📦 Install dependencies
run: npm ci --only-production --ignore-scripts
working-directory: backend
# -
# name: 🧪 Run tests
# run: npm run test:ci
# working-directory: backend
-
name: 🏗️ Run build
- name: 🧪 Run tests
run: npm run test:ci
working-directory: backend
- name: 📁 Upload test results
uses: actions/upload-artifact@v3
if: always()
with:
name: be-test-results
path: |
./backend/reports
./backend/coverage
- name: 🏗️ Run build
run: npm run build
working-directory: backend

View File

@ -1,22 +0,0 @@
name: Close inactive issues
on:
schedule:
- cron: "30 1 * * *"
jobs:
close-issues:
runs-on: ubuntu-latest
permissions:
issues: write
pull-requests: write
steps:
- uses: actions/stale@v4
with:
days-before-issue-stale: 30
days-before-issue-close: 14
stale-issue-label: "stale"
stale-issue-message: "This issue is stale because it has been open for 30 days with no activity."
close-issue-message: "This issue was closed because it has been inactive for 14 days since being marked as stale."
days-before-pr-stale: -1
days-before-pr-close: -1
repo-token: ${{ secrets.GITHUB_TOKEN }}

4
.gitignore vendored
View File

@ -25,7 +25,9 @@ node_modules
.env
# testing
/coverage
coverage
reports
junit.xml
# next.js
/.next/

View File

@ -81,6 +81,7 @@ nfpms:
- rpm
- deb
- apk
- archlinux
bindir: /usr/bin
scoop:
bucket:

View File

@ -128,7 +128,9 @@ We're currently setting the foundation and building [integrations](https://infis
</tr>
<tr>
<td align="left" valign="middle">
🔜 Vercel (https://github.com/Infisical/infisical/issues/60)
<a href="https://infisical.com/docs/integrations/cloud/vercel?ref=github.com">
✔️ Vercel
</a>
</td>
<td align="left" valign="middle">
<a href="https://infisical.com/docs/integrations/platforms/kubernetes?ref=github.com">
@ -144,7 +146,9 @@ We're currently setting the foundation and building [integrations](https://infis
🔜 AWS
</td>
<td align="left" valign="middle">
🔜 GitHub Actions (https://github.com/Infisical/infisical/issues/54)
<a href="https://infisical.com/docs/integrations/cicd/githubactions">
✔️ GitHub Actions
</a>
</td>
<td align="left" valign="middle">
🔜 Railway
@ -155,10 +159,10 @@ We're currently setting the foundation and building [integrations](https://infis
🔜 GCP
</td>
<td align="left" valign="middle">
🔜 GitLab CI/CD
🔜 GitLab CI/CD (https://github.com/Infisical/infisical/issues/134)
</td>
<td align="left" valign="middle">
🔜 CircleCI
🔜 CircleCI (https://github.com/Infisical/infisical/issues/91)
</td>
</tr>
<tr>
@ -177,7 +181,9 @@ We're currently setting the foundation and building [integrations](https://infis
🔜 TravisCI
</td>
<td align="left" valign="middle">
🔜 Netlify (https://github.com/Infisical/infisical/issues/55)
<a href="https://infisical.com/docs/integrations/cloud/netlify">
✔️ Netlify
</a>
</td>
<td align="left" valign="middle">
🔜 Railway
@ -191,7 +197,7 @@ We're currently setting the foundation and building [integrations](https://infis
🔜 Supabase
</td>
<td align="left" valign="middle">
🔜 Serverless
🔜 Render (https://github.com/Infisical/infisical/issues/132)
</td>
</tr>
</tbody>
@ -315,4 +321,4 @@ Infisical officially launched as v.1.0 on November 21st, 2022. However, a lot of
<!-- prettier-ignore-start -->
<!-- markdownlint-disable -->
<a href="https://github.com/dangtony98"><img src="https://avatars.githubusercontent.com/u/25857006?v=4" width="50" height="50" alt=""/></a> <a href="https://github.com/mv-turtle"><img src="https://avatars.githubusercontent.com/u/78047717?s=96&v=4" width="50" height="50" alt=""/></a> <a href="https://github.com/maidul98"><img src="https://avatars.githubusercontent.com/u/9300960?v=4" width="50" height="50" alt=""/></a> <a href="https://github.com/gangjun06"><img src="https://avatars.githubusercontent.com/u/50910815?v=4" width="50" height="50" alt=""/></a> <a href="https://github.com/reginaldbondoc"><img src="https://avatars.githubusercontent.com/u/7693108?v=4" width="50" height="50" alt=""/></a> <a href="https://github.com/SH5H"><img src="https://avatars.githubusercontent.com/u/25437192?v=4" width="50" height="50" alt=""/></a> <a href="https://github.com/asharonbaltazar"><img src="https://avatars.githubusercontent.com/u/58940073?v=4" width="50" height="50" alt=""/></a> <a href="https://github.com/edgarrmondragon"><img src="https://avatars.githubusercontent.com/u/16805946?v=4" width="50" height="50" alt=""/></a> <a href="https://github.com/arjunyel"><img src="https://avatars.githubusercontent.com/u/11153289?v=4" width="50" height="50" alt=""/></a> <a href="https://github.com/LemmyMwaura"><img src="https://avatars.githubusercontent.com/u/20738858?v=4" width="50" height="50" alt=""/></a> <a href="https://github.com/adrianmarinwork"><img src="https://avatars.githubusercontent.com/u/118568289?v=4" width="50" height="50" alt=""/></a> <a href="https://github.com/arthurzenika"><img src="https://avatars.githubusercontent.com/u/445200?v=4" width="50" height="50" alt=""/></a> <a href="https://github.com/hanywang2"><img src="https://avatars.githubusercontent.com/u/44352119?v=4" width="50" height="50" alt=""/></a> <a href="https://github.com/tobias-mintlify"><img src="https://avatars.githubusercontent.com/u/110702161?v=4" width="50" height="50" alt=""/></a> <a href="https://github.com/0xflotus"><img src="https://avatars.githubusercontent.com/u/26602940?v=4" width="50" height="50" alt=""/></a> <a href="https://github.com/wanjohiryan"><img src="https://avatars.githubusercontent.com/u/71614375?v=4" width="50" height="50" alt=""/></a>
<a href="https://github.com/dangtony98"><img src="https://avatars.githubusercontent.com/u/25857006?v=4" width="50" height="50" alt=""/></a> <a href="https://github.com/mv-turtle"><img src="https://avatars.githubusercontent.com/u/78047717?s=96&v=4" width="50" height="50" alt=""/></a> <a href="https://github.com/maidul98"><img src="https://avatars.githubusercontent.com/u/9300960?v=4" width="50" height="50" alt=""/></a> <a href="https://github.com/gangjun06"><img src="https://avatars.githubusercontent.com/u/50910815?v=4" width="50" height="50" alt=""/></a> <a href="https://github.com/reginaldbondoc"><img src="https://avatars.githubusercontent.com/u/7693108?v=4" width="50" height="50" alt=""/></a> <a href="https://github.com/SH5H"><img src="https://avatars.githubusercontent.com/u/25437192?v=4" width="50" height="50" alt=""/></a> <a href="https://github.com/gmgale"><img src="https://avatars.githubusercontent.com/u/62303146?v=4" width="50" height="50" alt=""/></a> <a href="https://github.com/asharonbaltazar"><img src="https://avatars.githubusercontent.com/u/58940073?v=4" width="50" height="50" alt=""/></a> <a href="https://github.com/edgarrmondragon"><img src="https://avatars.githubusercontent.com/u/16805946?v=4" width="50" height="50" alt=""/></a> <a href="https://github.com/arjunyel"><img src="https://avatars.githubusercontent.com/u/11153289?v=4" width="50" height="50" alt=""/></a> <a href="https://github.com/LemmyMwaura"><img src="https://avatars.githubusercontent.com/u/20738858?v=4" width="50" height="50" alt=""/></a> <a href="https://github.com/Zamion101"><img src="https://avatars.githubusercontent.com/u/8071263?v=4" width="50" height="50" alt=""/></a> <a href="https://github.com/jonerrr"><img src="https://avatars.githubusercontent.com/u/73760377?v=4" width="50" height="50" alt=""/></a> <a href="https://github.com/adrianmarinwork"><img src="https://avatars.githubusercontent.com/u/118568289?v=4" width="50" height="50" alt=""/></a> <a href="https://github.com/hanywang2"><img src="https://avatars.githubusercontent.com/u/44352119?v=4" width="50" height="50" alt=""/></a> <a href="https://github.com/tobias-mintlify"><img src="https://avatars.githubusercontent.com/u/110702161?v=4" width="50" height="50" alt=""/></a> <a href="https://github.com/0xflotus"><img src="https://avatars.githubusercontent.com/u/26602940?v=4" width="50" height="50" alt=""/></a> <a href="https://github.com/wanjohiryan"><img src="https://avatars.githubusercontent.com/u/71614375?v=4" width="50" height="50" alt=""/></a>

View File

@ -0,0 +1,19 @@
import { server } from '../src/app';
import { describe, expect, it, beforeAll, afterAll } from '@jest/globals';
import supertest from 'supertest';
import { setUpHealthEndpoint } from '../src/services/health';
const requestWithSupertest = supertest(server);
describe('Healthcheck endpoint', () => {
beforeAll(async () => {
setUpHealthEndpoint(server);
});
afterAll(async () => {
server.close();
});
it('GET /healthcheck should return OK', async () => {
const res = await requestWithSupertest.get('/healthcheck');
expect(res.status).toEqual(200);
});
});

View File

@ -22,8 +22,6 @@ declare global {
CLIENT_SECRET_NETLIFY: string;
POSTHOG_HOST: string;
POSTHOG_PROJECT_API_KEY: string;
PRIVATE_KEY: string;
PUBLIC_KEY: string;
SENTRY_DSN: string;
SITE_URL: string;
SMTP_HOST: string;

1129
backend/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -1,9 +1,11 @@
{
"dependencies": {
"@godaddy/terminus": "^4.11.2",
"@octokit/rest": "^19.0.5",
"@sentry/node": "^7.14.0",
"@sentry/tracing": "^7.19.0",
"@types/crypto-js": "^4.1.1",
"@types/libsodium-wrappers": "^0.7.10",
"axios": "^1.1.3",
"bigint-conversion": "^2.2.2",
"cookie-parser": "^1.4.6",
@ -15,11 +17,12 @@
"express-validator": "^6.14.2",
"handlebars": "^4.7.7",
"helmet": "^5.1.1",
"jsonwebtoken": "^8.5.1",
"jsonwebtoken": "^9.0.0",
"jsrp": "^0.2.4",
"libsodium-wrappers": "^0.7.10",
"mongoose": "^6.7.2",
"nodemailer": "^6.8.0",
"posthog-node": "^2.1.0",
"posthog-node": "^2.2.0",
"query-string": "^7.1.3",
"rimraf": "^3.0.2",
"stripe": "^10.7.0",
@ -37,7 +40,11 @@
"build": "rimraf ./build && tsc && cp -R ./src/templates ./build",
"lint": "eslint . --ext .ts",
"lint-and-fix": "eslint . --ext .ts --fix",
"lint-staged": "lint-staged"
"lint-staged": "lint-staged",
"pretest": "docker compose -f test-resources/docker-compose.test.yml up -d",
"test": "cross-env NODE_ENV=test jest --testTimeout=10000 --detectOpenHandles",
"test:ci": "npm test -- --watchAll=false --ci --reporters=default --reporters=jest-junit --reporters=github-actions --coverage --testLocationInResults --json --outputFile=coverage/report.json",
"posttest": "docker compose -f test-resources/docker-compose.test.yml down"
},
"repository": {
"type": "git",
@ -51,22 +58,49 @@
"homepage": "https://github.com/Infisical/infisical-api#readme",
"description": "",
"devDependencies": {
"@jest/globals": "^29.3.1",
"@posthog/plugin-scaffold": "^1.3.4",
"@types/cookie-parser": "^1.4.3",
"@types/cors": "^2.8.12",
"@types/express": "^4.17.14",
"@types/jest": "^29.2.4",
"@types/jsonwebtoken": "^8.5.9",
"@types/node": "^18.11.3",
"@types/nodemailer": "^6.4.6",
"@types/supertest": "^2.0.12",
"@types/swagger-jsdoc": "^6.0.1",
"@types/swagger-ui-express": "^4.1.3",
"@typescript-eslint/eslint-plugin": "^5.40.1",
"@typescript-eslint/parser": "^5.40.1",
"cross-env": "^7.0.3",
"eslint": "^8.26.0",
"install": "^0.13.0",
"jest": "^29.3.1",
"jest-junit": "^15.0.0",
"nodemon": "^2.0.19",
"npm": "^8.19.3",
"supertest": "^6.3.3",
"ts-jest": "^29.0.3",
"ts-node": "^10.9.1"
},
"jest": {
"preset": "ts-jest",
"testEnvironment": "node",
"collectCoverageFrom": [
"src/*.{js,ts}",
"!**/node_modules/**"
],
"setupFiles": [
"<rootDir>/test-resources/env-vars.js"
]
},
"jest-junit": {
"outputDirectory": "reports",
"outputName": "jest-junit.xml",
"ancestorSeparator": " ",
"uniqueOutputName": "false",
"suiteNameTemplate": "{filepath}",
"classNameTemplate": "{classname}",
"titleTemplate": "{title}"
}
}

74
backend/src/app.ts Normal file
View File

@ -0,0 +1,74 @@
/* eslint-disable no-console */
import express from 'express';
import helmet from 'helmet';
import cors from 'cors';
import cookieParser from 'cookie-parser';
import dotenv from 'dotenv';
dotenv.config();
import { PORT, NODE_ENV, SITE_URL } from './config';
import { apiLimiter } from './helpers/rateLimiter';
import {
signup as signupRouter,
auth as authRouter,
bot as botRouter,
organization as organizationRouter,
workspace as workspaceRouter,
membershipOrg as membershipOrgRouter,
membership as membershipRouter,
key as keyRouter,
inviteOrg as inviteOrgRouter,
user as userRouter,
userAction as userActionRouter,
secret as secretRouter,
serviceToken as serviceTokenRouter,
password as passwordRouter,
stripe as stripeRouter,
integration as integrationRouter,
integrationAuth as integrationAuthRouter
} from './routes';
export const app = express();
app.enable('trust proxy');
app.use(express.json());
app.use(cookieParser());
app.use(
cors({
credentials: true,
origin: SITE_URL
})
);
if (NODE_ENV === 'production') {
// enable app-wide rate-limiting + helmet security
// in production
app.disable('x-powered-by');
app.use(apiLimiter);
app.use(helmet());
}
// routers
app.use('/api/v1/signup', signupRouter);
app.use('/api/v1/auth', authRouter);
app.use('/api/v1/bot', botRouter);
app.use('/api/v1/user', userRouter);
app.use('/api/v1/user-action', userActionRouter);
app.use('/api/v1/organization', organizationRouter);
app.use('/api/v1/workspace', workspaceRouter);
app.use('/api/v1/membership-org', membershipOrgRouter);
app.use('/api/v1/membership', membershipRouter);
app.use('/api/v1/key', keyRouter);
app.use('/api/v1/invite-org', inviteOrgRouter);
app.use('/api/v1/secret', secretRouter);
app.use('/api/v1/service-token', serviceTokenRouter);
app.use('/api/v1/password', passwordRouter);
app.use('/api/v1/stripe', stripeRouter);
app.use('/api/v1/integration', integrationRouter);
app.use('/api/v1/integration-auth', integrationAuthRouter);
export const server = app.listen(PORT, () => {
console.log(`Listening on PORT ${[PORT]}`);
});

View File

@ -14,21 +14,24 @@ const CLIENT_SECRET_HEROKU = process.env.CLIENT_SECRET_HEROKU!;
const CLIENT_ID_HEROKU = process.env.CLIENT_ID_HEROKU!;
const CLIENT_ID_VERCEL = process.env.CLIENT_ID_VERCEL!;
const CLIENT_ID_NETLIFY = process.env.CLIENT_ID_NETLIFY!;
const CLIENT_ID_GITHUB = process.env.CLIENT_ID_GITHUB!;
const CLIENT_SECRET_VERCEL = process.env.CLIENT_SECRET_VERCEL!;
const CLIENT_SECRET_NETLIFY = process.env.CLIENT_SECRET_NETLIFY!;
const CLIENT_SECRET_GITHUB = process.env.CLIENT_SECRET_GITHUB!;
const CLIENT_SLUG_VERCEL= process.env.CLIENT_SLUG_VERCEL!;
const POSTHOG_HOST = process.env.POSTHOG_HOST! || 'https://app.posthog.com';
const POSTHOG_PROJECT_API_KEY =
process.env.POSTHOG_PROJECT_API_KEY! ||
'phc_nSin8j5q2zdhpFDI1ETmFNUIuTG4DwKVyIigrY10XiE';
const PRIVATE_KEY = process.env.PRIVATE_KEY!;
const PUBLIC_KEY = process.env.PUBLIC_KEY!;
const SENTRY_DSN = process.env.SENTRY_DSN!;
const SITE_URL = process.env.SITE_URL!;
const SMTP_HOST = process.env.SMTP_HOST! || 'smtp.gmail.com';
const SMTP_SECURE = process.env.SMTP_SECURE! || false;
const SMTP_PORT = process.env.SMTP_PORT! || 587;
const SMTP_NAME = process.env.SMTP_NAME!;
const SMTP_USERNAME = process.env.SMTP_USERNAME!;
const SMTP_PASSWORD = process.env.SMTP_PASSWORD!;
const SMTP_FROM_ADDRESS = process.env.SMTP_FROM_ADDRESS!;
const SMTP_FROM_NAME = process.env.SMTP_FROM_NAME! || 'Infisical';
const STRIPE_PRODUCT_CARD_AUTH = process.env.STRIPE_PRODUCT_CARD_AUTH!;
const STRIPE_PRODUCT_PRO = process.env.STRIPE_PRODUCT_PRO!;
const STRIPE_PRODUCT_STARTER = process.env.STRIPE_PRODUCT_STARTER!;
@ -53,20 +56,23 @@ export {
CLIENT_ID_HEROKU,
CLIENT_ID_VERCEL,
CLIENT_ID_NETLIFY,
CLIENT_ID_GITHUB,
CLIENT_SECRET_HEROKU,
CLIENT_SECRET_VERCEL,
CLIENT_SECRET_NETLIFY,
CLIENT_SECRET_GITHUB,
CLIENT_SLUG_VERCEL,
POSTHOG_HOST,
POSTHOG_PROJECT_API_KEY,
PRIVATE_KEY,
PUBLIC_KEY,
SENTRY_DSN,
SITE_URL,
SMTP_HOST,
SMTP_PORT,
SMTP_NAME,
SMTP_SECURE,
SMTP_USERNAME,
SMTP_PASSWORD,
SMTP_FROM_ADDRESS,
SMTP_FROM_NAME,
STRIPE_PRODUCT_CARD_AUTH,
STRIPE_PRODUCT_PRO,
STRIPE_PRODUCT_STARTER,

View File

@ -2,7 +2,6 @@ import { Request, Response } from 'express';
import * as Sentry from '@sentry/node';
import { Key } from '../models';
import { findMembership } from '../helpers/membership';
import { PUBLIC_KEY } from '../config';
import { GRANTED } from '../variables';
/**
@ -84,16 +83,4 @@ export const getLatestKey = async (req: Request, res: Response) => {
}
return res.status(200).send(resObj);
};
/**
* Return public key of Infisical
* @param req
* @param res
* @returns
*/
export const getPublicKeyInfisical = async (req: Request, res: Response) => {
return res.status(200).send({
publicKey: PUBLIC_KEY
});
};
};

View File

@ -69,7 +69,7 @@ const getSecretsHelper = async ({
workspaceId: string;
environment: string;
}) => {
let content = {} as any;
const content = {} as any;
try {
const key = await getKey({ workspaceId });
const secrets = await Secret.find({

View File

@ -53,12 +53,12 @@ const handleOAuthExchangeHelper = async ({
if (!bot) throw new Error('Bot must be enabled for OAuth2 code-token exchange');
// exchange code for access and refresh tokens
let res = await exchangeCode({
const res = await exchangeCode({
integration,
code
});
let update: Update = {
const update: Update = {
workspace: workspaceId,
integration
}
@ -138,7 +138,7 @@ const syncIntegrationsHelper = async ({
// to that integration
for await (const integration of integrations) {
// get workspace, environment (shared) secrets
const secrets = await BotService.getSecrets({
const secrets = await BotService.getSecrets({ // issue here?
workspaceId: integration.workspace.toString(),
environment: integration.environment
});

View File

@ -2,40 +2,10 @@ import fs from 'fs';
import path from 'path';
import handlebars from 'handlebars';
import nodemailer from 'nodemailer';
import {
SMTP_HOST,
SMTP_PORT,
SMTP_NAME,
SMTP_USERNAME,
SMTP_PASSWORD
} from '../config';
import SMTPConnection from 'nodemailer/lib/smtp-connection';
import { SMTP_FROM_NAME, SMTP_FROM_ADDRESS } from '../config';
import * as Sentry from '@sentry/node';
const mailOpts: SMTPConnection.Options = {
host: SMTP_HOST,
port: SMTP_PORT as number
};
if (SMTP_USERNAME && SMTP_PASSWORD) {
mailOpts.auth = {
user: SMTP_USERNAME,
pass: SMTP_PASSWORD
};
}
// create nodemailer transporter
const transporter = nodemailer.createTransport(mailOpts);
transporter
.verify()
.then(() => {
Sentry.setUser(null);
Sentry.captureMessage('SMTP - Successfully connected');
})
.catch((err) => {
Sentry.setUser(null);
Sentry.captureException(
`SMTP - Failed to connect to ${SMTP_HOST}:${SMTP_PORT} \n\t${err}`
);
});
let smtpTransporter: nodemailer.Transporter;
/**
* @param {Object} obj
@ -63,8 +33,8 @@ const sendMail = async ({
const temp = handlebars.compile(html);
const htmlToSend = temp(substitutions);
await transporter.sendMail({
from: `"${SMTP_NAME}" <${SMTP_USERNAME}>`,
await smtpTransporter.sendMail({
from: `"${SMTP_FROM_NAME}" <${SMTP_FROM_ADDRESS}>`,
to: recipients.join(', '),
subject: subjectLine,
html: htmlToSend
@ -75,4 +45,8 @@ const sendMail = async ({
}
};
export { sendMail };
const setTransporter = (transporter: nodemailer.Transporter) => {
smtpTransporter = transporter;
};
export { sendMail, setTransporter };

View File

@ -1,129 +1,25 @@
/* eslint-disable no-console */
import http from 'http';
import express from 'express';
import helmet from 'helmet';
import cors from 'cors';
import cookieParser from 'cookie-parser';
import mongoose from 'mongoose';
import dotenv from 'dotenv';
dotenv.config();
import * as Sentry from '@sentry/node';
import { PORT, SENTRY_DSN, NODE_ENV, MONGO_URL, SITE_URL } from './config';
import { apiLimiter } from './helpers/rateLimiter';
import { createTerminus } from '@godaddy/terminus';
import { SENTRY_DSN, NODE_ENV, MONGO_URL } from './config';
import { server } from './app';
import { initDatabase } from './services/database';
import { setUpHealthEndpoint } from './services/health';
import { initSmtp } from './services/smtp';
import { setTransporter } from './helpers/nodemailer';
const app = express();
initDatabase(MONGO_URL);
Sentry.init({
dsn: SENTRY_DSN,
tracesSampleRate: 1.0,
debug: NODE_ENV === 'production' ? false : true,
environment: NODE_ENV
});
setUpHealthEndpoint(server);
import {
signup as signupRouter,
auth as authRouter,
bot as botRouter,
organization as organizationRouter,
workspace as workspaceRouter,
membershipOrg as membershipOrgRouter,
membership as membershipRouter,
key as keyRouter,
inviteOrg as inviteOrgRouter,
user as userRouter,
userAction as userActionRouter,
secret as secretRouter,
serviceToken as serviceTokenRouter,
password as passwordRouter,
stripe as stripeRouter,
integration as integrationRouter,
integrationAuth as integrationAuthRouter
} from './routes';
setTransporter(initSmtp());
const connectWithRetry = () => {
mongoose
.connect(MONGO_URL)
.then(() => console.log('Successfully connected to DB'))
.catch((e) => {
console.log('Failed to connect to DB ', e);
setTimeout(() => {
console.log(e);
}, 5000);
});
return mongoose.connection;
};
const dbConnection = connectWithRetry();
app.enable('trust proxy');
app.use(cookieParser());
app.use(
cors({
credentials: true,
origin: SITE_URL
})
);
if (NODE_ENV === 'production') {
// enable app-wide rate-limiting + helmet security
// in production
app.disable('x-powered-by');
app.use(apiLimiter);
app.use(helmet());
if (NODE_ENV !== 'test') {
Sentry.init({
dsn: SENTRY_DSN,
tracesSampleRate: 1.0,
debug: NODE_ENV === 'production' ? false : true,
environment: NODE_ENV
});
}
app.use(express.json());
// routers
app.use('/api/v1/signup', signupRouter);
app.use('/api/v1/auth', authRouter);
app.use('/api/v1/bot', botRouter);
app.use('/api/v1/user', userRouter);
app.use('/api/v1/user-action', userActionRouter);
app.use('/api/v1/organization', organizationRouter);
app.use('/api/v1/workspace', workspaceRouter);
app.use('/api/v1/membership-org', membershipOrgRouter);
app.use('/api/v1/membership', membershipRouter);
app.use('/api/v1/key', keyRouter);
app.use('/api/v1/invite-org', inviteOrgRouter);
app.use('/api/v1/secret', secretRouter);
app.use('/api/v1/service-token', serviceTokenRouter);
app.use('/api/v1/password', passwordRouter);
app.use('/api/v1/stripe', stripeRouter);
app.use('/api/v1/integration', integrationRouter);
app.use('/api/v1/integration-auth', integrationAuthRouter);
const server = http.createServer(app);
const onSignal = () => {
console.log('Server is starting clean-up');
return Promise.all([
() => {
dbConnection.close(() => {
console.info('Database connection closed');
});
}
]);
};
const healthCheck = () => {
// `state.isShuttingDown` (boolean) shows whether the server is shutting down or not
return Promise
.resolve
// optionally include a resolve value to be included as
// info in the health check response
();
};
createTerminus(server, {
healthChecks: {
'/healthcheck': healthCheck,
onSignal
}
});
server.listen(PORT, () => {
console.log('Listening on PORT ' + PORT);
});

View File

@ -1,17 +1,22 @@
import axios from 'axios';
import * as Sentry from '@sentry/node';
import { Octokit } from '@octokit/rest';
import { IIntegrationAuth } from '../models';
import {
IIntegrationAuth
} from '../models';
import {
INTEGRATION_HEROKU,
INTEGRATION_VERCEL,
INTEGRATION_NETLIFY,
INTEGRATION_HEROKU_API_URL,
INTEGRATION_VERCEL_API_URL,
INTEGRATION_NETLIFY_API_URL
INTEGRATION_HEROKU,
INTEGRATION_VERCEL,
INTEGRATION_NETLIFY,
INTEGRATION_GITHUB,
INTEGRATION_HEROKU_API_URL,
INTEGRATION_VERCEL_API_URL,
INTEGRATION_NETLIFY_API_URL,
INTEGRATION_GITHUB_API_URL
} from '../variables';
interface GitHubApp {
name: string;
}
/**
* Return list of names of apps for integration named [integration]
* @param {Object} obj
@ -21,47 +26,51 @@ import {
* @returns {String} apps.name - name of integration app
*/
const getApps = async ({
integrationAuth,
accessToken
integrationAuth,
accessToken
}: {
integrationAuth: IIntegrationAuth;
accessToken: string;
integrationAuth: IIntegrationAuth;
accessToken: string;
}) => {
interface App {
name: string;
siteId?: string;
}
interface App {
name: string;
siteId?: string;
}
let apps: App[]; // TODO: add type and define payloads for apps
try {
switch (integrationAuth.integration) {
case INTEGRATION_HEROKU:
apps = await getAppsHeroku({
accessToken
});
break;
case INTEGRATION_VERCEL:
apps = await getAppsVercel({
accessToken
});
break;
case INTEGRATION_NETLIFY:
apps = await getAppsNetlify({
integrationAuth,
accessToken
});
break;
}
} catch (err) {
Sentry.setUser(null);
Sentry.captureException(err);
throw new Error('Failed to get integration apps');
let apps: App[]; // TODO: add type and define payloads for apps
try {
switch (integrationAuth.integration) {
case INTEGRATION_HEROKU:
apps = await getAppsHeroku({
accessToken
});
break;
case INTEGRATION_VERCEL:
apps = await getAppsVercel({
accessToken
});
break;
case INTEGRATION_NETLIFY:
apps = await getAppsNetlify({
integrationAuth,
accessToken
});
break;
case INTEGRATION_GITHUB:
apps = await getAppsGithub({
integrationAuth,
accessToken
});
break;
}
return apps;
}
} catch (err) {
Sentry.setUser(null);
Sentry.captureException(err);
throw new Error('Failed to get integration apps');
}
return apps;
};
/**
* Return list of names of apps for Heroku integration
@ -70,31 +79,29 @@ const getApps = async ({
* @returns {Object[]} apps - names of Heroku apps
* @returns {String} apps.name - name of Heroku app
*/
const getAppsHeroku = async ({
accessToken
}: {
accessToken: string;
}) => {
let apps;
try {
const res = (await axios.get(`${INTEGRATION_HEROKU_API_URL}/apps`, {
headers: {
Accept: 'application/vnd.heroku+json; version=3',
Authorization: `Bearer ${accessToken}`
}
})).data;
apps = res.map((a: any) => ({
name: a.name
}));
} catch (err) {
Sentry.setUser(null);
Sentry.captureException(err);
throw new Error('Failed to get Heroku integration apps');
}
return apps;
}
const getAppsHeroku = async ({ accessToken }: { accessToken: string }) => {
let apps;
try {
const res = (
await axios.get(`${INTEGRATION_HEROKU_API_URL}/apps`, {
headers: {
Accept: 'application/vnd.heroku+json; version=3',
Authorization: `Bearer ${accessToken}`
}
})
).data;
apps = res.map((a: any) => ({
name: a.name
}));
} catch (err) {
Sentry.setUser(null);
Sentry.captureException(err);
throw new Error('Failed to get Heroku integration apps');
}
return apps;
};
/**
* Return list of names of apps for Vercel integration
@ -103,30 +110,28 @@ const getAppsHeroku = async ({
* @returns {Object[]} apps - names of Vercel apps
* @returns {String} apps.name - name of Vercel app
*/
const getAppsVercel = async ({
accessToken
}: {
accessToken: string;
}) => {
let apps;
try {
const res = (await axios.get(`${INTEGRATION_VERCEL_API_URL}/v9/projects`, {
headers: {
Authorization: `Bearer ${accessToken}`
}
})).data;
apps = res.projects.map((a: any) => ({
name: a.name
}));
} catch (err) {
Sentry.setUser(null);
Sentry.captureException(err);
throw new Error('Failed to get Vercel integration apps');
}
return apps;
}
const getAppsVercel = async ({ accessToken }: { accessToken: string }) => {
let apps;
try {
const res = (
await axios.get(`${INTEGRATION_VERCEL_API_URL}/v9/projects`, {
headers: {
Authorization: `Bearer ${accessToken}`
}
})
).data;
apps = res.projects.map((a: any) => ({
name: a.name
}));
} catch (err) {
Sentry.setUser(null);
Sentry.captureException(err);
throw new Error('Failed to get Vercel integration apps');
}
return apps;
};
/**
* Return list of names of sites for Netlify integration
@ -136,34 +141,73 @@ const getAppsVercel = async ({
* @returns {String} apps.name - name of Netlify site
*/
const getAppsNetlify = async ({
integrationAuth,
accessToken
integrationAuth,
accessToken
}: {
integrationAuth: IIntegrationAuth;
accessToken: string;
integrationAuth: IIntegrationAuth;
accessToken: string;
}) => {
let apps;
try {
const res = (await axios.get(`${INTEGRATION_NETLIFY_API_URL}/api/v1/sites`, {
headers: {
Authorization: `Bearer ${accessToken}`
}
})).data;
apps = res.map((a: any) => ({
name: a.name,
siteId: a.site_id
}));
} catch (err) {
Sentry.setUser(null);
Sentry.captureException(err);
throw new Error('Failed to get Netlify integration apps');
}
return apps;
}
let apps;
try {
const res = (
await axios.get(`${INTEGRATION_NETLIFY_API_URL}/api/v1/sites`, {
headers: {
Authorization: `Bearer ${accessToken}`
}
})
).data;
export {
getApps
}
apps = res.map((a: any) => ({
name: a.name,
siteId: a.site_id
}));
} catch (err) {
Sentry.setUser(null);
Sentry.captureException(err);
throw new Error('Failed to get Netlify integration apps');
}
return apps;
};
/**
* Return list of names of repositories for Github integration
* @param {Object} obj
* @param {String} obj.accessToken - access token for Netlify API
* @returns {Object[]} apps - names of Netlify sites
* @returns {String} apps.name - name of Netlify site
*/
const getAppsGithub = async ({
integrationAuth,
accessToken
}: {
integrationAuth: IIntegrationAuth;
accessToken: string;
}) => {
let apps;
try {
const octokit = new Octokit({
auth: accessToken
});
const repos = (await octokit.request(
'GET /user/repos{?visibility,affiliation,type,sort,direction,per_page,page,since,before}',
{}
)).data;
apps = repos
.filter((a:any) => a.permissions.admin === true)
.map((a: any) => ({
name: a.name
})
);
} catch (err) {
Sentry.setUser(null);
Sentry.captureException(err);
throw new Error('Failed to get Github repos');
}
return apps;
};
export { getApps };

View File

@ -1,46 +1,58 @@
import axios from 'axios';
import * as Sentry from '@sentry/node';
import {
INTEGRATION_HEROKU,
INTEGRATION_VERCEL,
INTEGRATION_NETLIFY,
INTEGRATION_HEROKU_TOKEN_URL,
INTEGRATION_VERCEL_TOKEN_URL,
INTEGRATION_NETLIFY_TOKEN_URL,
ACTION_PUSH_TO_HEROKU
INTEGRATION_HEROKU,
INTEGRATION_VERCEL,
INTEGRATION_NETLIFY,
INTEGRATION_GITHUB,
INTEGRATION_HEROKU_TOKEN_URL,
INTEGRATION_VERCEL_TOKEN_URL,
INTEGRATION_NETLIFY_TOKEN_URL,
INTEGRATION_GITHUB_TOKEN_URL,
INTEGRATION_GITHUB_API_URL,
ACTION_PUSH_TO_HEROKU
} from '../variables';
import {
SITE_URL,
CLIENT_SECRET_HEROKU,
CLIENT_ID_VERCEL,
CLIENT_ID_NETLIFY,
CLIENT_SECRET_VERCEL,
CLIENT_SECRET_NETLIFY
import {
SITE_URL,
CLIENT_ID_VERCEL,
CLIENT_ID_NETLIFY,
CLIENT_ID_GITHUB,
CLIENT_SECRET_HEROKU,
CLIENT_SECRET_VERCEL,
CLIENT_SECRET_NETLIFY,
CLIENT_SECRET_GITHUB
} from '../config';
import { user } from '../routes';
interface ExchangeCodeHerokuResponse {
token_type: string;
access_token: string;
expires_in: number;
refresh_token: string;
user_id: string;
session_nonce?: string;
token_type: string;
access_token: string;
expires_in: number;
refresh_token: string;
user_id: string;
session_nonce?: string;
}
interface ExchangeCodeVercelResponse {
token_type: string;
access_token: string;
installation_id: string;
user_id: string;
team_id?: string;
token_type: string;
access_token: string;
installation_id: string;
user_id: string;
team_id?: string;
}
interface ExchangeCodeNetlifyResponse {
access_token: string;
token_type: string;
refresh_token: string;
scope: string;
created_at: number;
access_token: string;
token_type: string;
refresh_token: string;
scope: string;
created_at: number;
}
interface ExchangeCodeGithubResponse {
access_token: string;
scope: string;
token_type: string;
}
/**
@ -56,40 +68,45 @@ interface ExchangeCodeNetlifyResponse {
* @returns {String} obj.action - integration action for bot sequence
*/
const exchangeCode = async ({
integration,
code
}: {
integration: string;
code: string;
integration,
code
}: {
integration: string;
code: string;
}) => {
let obj = {} as any;
try {
switch (integration) {
case INTEGRATION_HEROKU:
obj = await exchangeCodeHeroku({
code
});
break;
case INTEGRATION_VERCEL:
obj = await exchangeCodeVercel({
code
});
break;
case INTEGRATION_NETLIFY:
obj = await exchangeCodeNetlify({
code
});
break;
}
} catch (err) {
Sentry.setUser(null);
Sentry.captureException(err);
throw new Error('Failed OAuth2 code-token exchange');
let obj = {} as any;
try {
switch (integration) {
case INTEGRATION_HEROKU:
obj = await exchangeCodeHeroku({
code
});
break;
case INTEGRATION_VERCEL:
obj = await exchangeCodeVercel({
code
});
break;
case INTEGRATION_NETLIFY:
obj = await exchangeCodeNetlify({
code
});
break;
case INTEGRATION_GITHUB:
obj = await exchangeCodeGithub({
code
});
break;
}
return obj;
}
} catch (err) {
Sentry.setUser(null);
Sentry.captureException(err);
throw new Error('Failed OAuth2 code-token exchange');
}
return obj;
};
/**
* Return [accessToken], [accessExpiresAt], and [refreshToken] for Heroku
@ -107,7 +124,7 @@ const exchangeCodeHeroku = async ({
code: string;
}) => {
let res: ExchangeCodeHerokuResponse;
let accessExpiresAt = new Date();
const accessExpiresAt = new Date();
try {
res = (await axios.post(
INTEGRATION_HEROKU_TOKEN_URL,
@ -144,35 +161,33 @@ const exchangeCodeHeroku = async ({
* @returns {String} obj2.refreshToken - refresh token for Heroku API
* @returns {Date} obj2.accessExpiresAt - date of expiration for access token
*/
const exchangeCodeVercel = async ({
code
}: {
code: string;
}) => {
let res: ExchangeCodeVercelResponse;
try {
res = (await axios.post(
INTEGRATION_VERCEL_TOKEN_URL,
new URLSearchParams({
code: code,
client_id: CLIENT_ID_VERCEL,
client_secret: CLIENT_SECRET_VERCEL,
redirect_uri: `${SITE_URL}/vercel`
} as any)
)).data;
} catch (err) {
Sentry.setUser(null);
Sentry.captureException(err);
throw new Error('Failed OAuth2 code-token exchange with Vercel');
}
return ({
accessToken: res.access_token,
refreshToken: null,
accessExpiresAt: null,
teamId: res.team_id
});
}
const exchangeCodeVercel = async ({ code }: { code: string }) => {
let res: ExchangeCodeVercelResponse;
try {
res = (
await axios.post(
INTEGRATION_VERCEL_TOKEN_URL,
new URLSearchParams({
code: code,
client_id: CLIENT_ID_VERCEL,
client_secret: CLIENT_SECRET_VERCEL,
redirect_uri: `${SITE_URL}/vercel`
} as any)
)
).data;
} catch (err) {
Sentry.setUser(null);
Sentry.captureException(err);
throw new Error('Failed OAuth2 code-token exchange with Vercel');
}
return {
accessToken: res.access_token,
refreshToken: null,
accessExpiresAt: null,
teamId: res.team_id
};
};
/**
* Return [accessToken], [accessExpiresAt], and [refreshToken] for Vercel
@ -184,58 +199,89 @@ const exchangeCodeVercel = async ({
* @returns {String} obj2.refreshToken - refresh token for Heroku API
* @returns {Date} obj2.accessExpiresAt - date of expiration for access token
*/
const exchangeCodeNetlify = async ({
code
}: {
code: string;
}) => {
let res: ExchangeCodeNetlifyResponse;
let accountId;
try {
res = (await axios.post(
INTEGRATION_NETLIFY_TOKEN_URL,
new URLSearchParams({
grant_type: 'authorization_code',
code: code,
client_id: CLIENT_ID_NETLIFY,
client_secret: CLIENT_SECRET_NETLIFY,
redirect_uri: `${SITE_URL}/netlify`
} as any)
)).data;
const exchangeCodeNetlify = async ({ code }: { code: string }) => {
let res: ExchangeCodeNetlifyResponse;
let accountId;
try {
res = (
await axios.post(
INTEGRATION_NETLIFY_TOKEN_URL,
new URLSearchParams({
grant_type: 'authorization_code',
code: code,
client_id: CLIENT_ID_NETLIFY,
client_secret: CLIENT_SECRET_NETLIFY,
redirect_uri: `${SITE_URL}/netlify`
} as any)
)
).data;
const res2 = await axios.get(
'https://api.netlify.com/api/v1/sites',
{
headers: {
Authorization: `Bearer ${res.access_token}`
}
}
);
const res3 = (await axios.get(
'https://api.netlify.com/api/v1/accounts',
{
headers: {
Authorization: `Bearer ${res.access_token}`
}
}
)).data;
accountId = res3[0].id;
} catch (err) {
Sentry.setUser(null);
Sentry.captureException(err);
throw new Error('Failed OAuth2 code-token exchange with Netlify');
}
return ({
accessToken: res.access_token,
refreshToken: res.refresh_token,
accountId
const res2 = await axios.get('https://api.netlify.com/api/v1/sites', {
headers: {
Authorization: `Bearer ${res.access_token}`
}
});
}
export {
exchangeCode
}
const res3 = (
await axios.get('https://api.netlify.com/api/v1/accounts', {
headers: {
Authorization: `Bearer ${res.access_token}`
}
})
).data;
accountId = res3[0].id;
} catch (err) {
Sentry.setUser(null);
Sentry.captureException(err);
throw new Error('Failed OAuth2 code-token exchange with Netlify');
}
return {
accessToken: res.access_token,
refreshToken: res.refresh_token,
accountId
};
};
/**
* Return [accessToken], [accessExpiresAt], and [refreshToken] for Github
* code-token exchange
* @param {Object} obj1
* @param {Object} obj1.code - code for code-token exchange
* @returns {Object} obj2
* @returns {String} obj2.accessToken - access token for Github API
* @returns {String} obj2.refreshToken - refresh token for Github API
* @returns {Date} obj2.accessExpiresAt - date of expiration for access token
*/
const exchangeCodeGithub = async ({ code }: { code: string }) => {
let res: ExchangeCodeGithubResponse;
try {
res = (
await axios.get(INTEGRATION_GITHUB_TOKEN_URL, {
params: {
client_id: CLIENT_ID_GITHUB,
client_secret: CLIENT_SECRET_GITHUB,
code: code,
redirect_uri: `${SITE_URL}/github`
},
headers: {
Accept: 'application/json'
}
})
).data;
} catch (err) {
Sentry.setUser(null);
Sentry.captureException(err);
throw new Error('Failed OAuth2 code-token exchange with Github');
}
return {
accessToken: res.access_token,
refreshToken: null,
accessExpiresAt: null
};
};
export { exchangeCode };

View File

@ -13,44 +13,44 @@ import {
* named [integration]
* @param {Object} obj
* @param {String} obj.integration - name of integration
* @param {String} obj.refreshToken - refresh token to use to get new access token for Heroku
* @param {String} obj.refreshToken - refresh token to use to get new access token for Heroku
*/
const exchangeRefresh = async ({
integration,
refreshToken
integration,
refreshToken
}: {
integration: string;
refreshToken: string;
integration: string;
refreshToken: string;
}) => {
let accessToken;
try {
switch (integration) {
case INTEGRATION_HEROKU:
accessToken = await exchangeRefreshHeroku({
refreshToken
});
break;
}
} catch (err) {
Sentry.setUser(null);
Sentry.captureException(err);
throw new Error('Failed to get new OAuth2 access token');
let accessToken;
try {
switch (integration) {
case INTEGRATION_HEROKU:
accessToken = await exchangeRefreshHeroku({
refreshToken
});
break;
}
return accessToken;
}
} catch (err) {
Sentry.setUser(null);
Sentry.captureException(err);
throw new Error('Failed to get new OAuth2 access token');
}
return accessToken;
};
/**
* Return new access token by exchanging refresh token [refreshToken] for the
* Heroku integration
* @param {Object} obj
* @param {String} obj.refreshToken - refresh token to use to get new access token for Heroku
* @returns
* @returns
*/
const exchangeRefreshHeroku = async ({
refreshToken
refreshToken
}: {
refreshToken: string;
refreshToken: string;
}) => {
let accessToken;
try {
@ -63,16 +63,14 @@ const exchangeRefreshHeroku = async ({
} as any)
);
accessToken = res.data.access_token;
} catch (err) {
Sentry.setUser(null);
Sentry.captureException(err);
throw new Error('Failed to get new OAuth2 access token for Heroku');
}
return accessToken;
}
accessToken = res.data.access_token;
} catch (err) {
Sentry.setUser(null);
Sentry.captureException(err);
throw new Error('Failed to get new OAuth2 access token for Heroku');
}
export {
exchangeRefresh
}
return accessToken;
};
export { exchangeRefresh };

View File

@ -1,50 +1,47 @@
import axios from 'axios';
import * as Sentry from '@sentry/node';
import { IIntegrationAuth, IntegrationAuth, Integration } from '../models';
import {
IIntegrationAuth,
IntegrationAuth,
Integration
} from '../models';
import {
INTEGRATION_HEROKU,
INTEGRATION_VERCEL,
INTEGRATION_NETLIFY
INTEGRATION_HEROKU,
INTEGRATION_VERCEL,
INTEGRATION_NETLIFY,
INTEGRATION_GITHUB
} from '../variables';
const revokeAccess = async ({
integrationAuth,
accessToken
integrationAuth,
accessToken
}: {
integrationAuth: IIntegrationAuth,
accessToken: String
integrationAuth: IIntegrationAuth;
accessToken: string;
}) => {
try {
// add any integration-specific revocation logic
switch (integrationAuth.integration) {
case INTEGRATION_HEROKU:
break;
case INTEGRATION_VERCEL:
break;
case INTEGRATION_NETLIFY:
break;
}
const deletedIntegrationAuth = await IntegrationAuth.findOneAndDelete({
_id: integrationAuth._id
});
if (deletedIntegrationAuth) {
await Integration.deleteMany({
integrationAuth: deletedIntegrationAuth._id
});
}
} catch (err) {
Sentry.setUser(null);
Sentry.captureException(err);
throw new Error('Failed to delete integration authorization');
try {
// add any integration-specific revocation logic
switch (integrationAuth.integration) {
case INTEGRATION_HEROKU:
break;
case INTEGRATION_VERCEL:
break;
case INTEGRATION_NETLIFY:
break;
case INTEGRATION_GITHUB:
break;
}
}
export {
revokeAccess
}
const deletedIntegrationAuth = await IntegrationAuth.findOneAndDelete({
_id: integrationAuth._id
});
if (deletedIntegrationAuth) {
await Integration.deleteMany({
integrationAuth: deletedIntegrationAuth._id
});
}
} catch (err) {
Sentry.setUser(null);
Sentry.captureException(err);
throw new Error('Failed to delete integration authorization');
}
};
export { revokeAccess };

View File

@ -1,16 +1,21 @@
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';
// const sodium = require('libsodium-wrappers');
import { IIntegration, IIntegrationAuth } from '../models';
import {
IIntegration, IIntegrationAuth
} from '../models';
import {
INTEGRATION_HEROKU,
INTEGRATION_VERCEL,
INTEGRATION_NETLIFY,
INTEGRATION_HEROKU_API_URL,
INTEGRATION_VERCEL_API_URL,
INTEGRATION_NETLIFY_API_URL
INTEGRATION_HEROKU,
INTEGRATION_VERCEL,
INTEGRATION_NETLIFY,
INTEGRATION_GITHUB,
INTEGRATION_HEROKU_API_URL,
INTEGRATION_VERCEL_API_URL,
INTEGRATION_NETLIFY_API_URL,
INTEGRATION_GITHUB_API_URL
} from '../variables';
import { access, appendFile } from 'fs';
// TODO: need a helper function in the future to handle integration
// envar priorities (i.e. prioritize secrets within integration or those on Infisical)
@ -26,47 +31,54 @@ import {
* @param {String} obj.accessToken - access token for integration
*/
const syncSecrets = 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 {
switch (integration.integration) {
case INTEGRATION_HEROKU:
await syncSecretsHeroku({
integration,
secrets,
accessToken
});
break;
case INTEGRATION_VERCEL:
await syncSecretsVercel({
integration,
secrets,
accessToken
});
break;
case INTEGRATION_NETLIFY:
await syncSecretsNetlify({
integration,
integrationAuth,
secrets,
accessToken
});
break;
}
} catch (err) {
Sentry.setUser(null);
Sentry.captureException(err);
throw new Error('Failed to sync secrets to integration');
try {
switch (integration.integration) {
case INTEGRATION_HEROKU:
await syncSecretsHeroku({
integration,
secrets,
accessToken
});
break;
case INTEGRATION_VERCEL:
await syncSecretsVercel({
integration,
secrets,
accessToken
});
break;
case INTEGRATION_NETLIFY:
await syncSecretsNetlify({
integration,
integrationAuth,
secrets,
accessToken
});
break;
case INTEGRATION_GITHUB:
await syncSecretsGitHub({
integration,
secrets,
accessToken
});
break;
}
}
} catch (err) {
Sentry.setUser(null);
Sentry.captureException(err);
throw new Error('Failed to sync secrets to integration');
}
};
/**
* Sync/push [secrets] to Heroku [app]
@ -75,47 +87,49 @@ const syncSecrets = async ({
* @param {Object} obj.secrets - secrets to push to integration (object where keys are secret keys and values are secret values)
*/
const syncSecretsHeroku = async ({
integration,
secrets,
accessToken
integration,
secrets,
accessToken
}: {
integration: IIntegration,
secrets: any;
accessToken: string;
integration: IIntegration;
secrets: any;
accessToken: string;
}) => {
try {
const herokuSecrets = (await axios.get(
`${INTEGRATION_HEROKU_API_URL}/apps/${integration.app}/config-vars`,
{
headers: {
Accept: 'application/vnd.heroku+json; version=3',
Authorization: `Bearer ${accessToken}`
}
}
)).data;
Object.keys(herokuSecrets).forEach(key => {
if (!(key in secrets)) {
secrets[key] = null;
}
});
try {
const herokuSecrets = (
await axios.get(
`${INTEGRATION_HEROKU_API_URL}/apps/${integration.app}/config-vars`,
{
headers: {
Accept: 'application/vnd.heroku+json; version=3',
Authorization: `Bearer ${accessToken}`
}
}
)
).data;
await axios.patch(
`${INTEGRATION_HEROKU_API_URL}/apps/${integration.app}/config-vars`,
secrets,
{
headers: {
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');
}
}
Object.keys(herokuSecrets).forEach((key) => {
if (!(key in secrets)) {
secrets[key] = null;
}
});
await axios.patch(
`${INTEGRATION_HEROKU_API_URL}/apps/${integration.app}/config-vars`,
secrets,
{
headers: {
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');
}
};
/**
* Sync/push [secrets] to Heroku [app]
@ -174,9 +188,9 @@ const syncSecretsVercel = async ({
[secret.key]: secret
}), {});
let updateSecrets: VercelSecret[] = [];
let deleteSecrets: VercelSecret[] = [];
let newSecrets: VercelSecret[] = [];
const updateSecrets: VercelSecret[] = [];
const deleteSecrets: VercelSecret[] = [];
const newSecrets: VercelSecret[] = [];
// Identify secrets to create
Object.keys(secrets).map((key) => {
@ -287,8 +301,24 @@ const syncSecretsNetlify = async ({
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: integration.context,
context_name: 'all', // integration.context or all
site_id: integration.siteId
});
@ -304,71 +334,94 @@ const syncSecretsNetlify = async ({
.data
.reduce((obj: any, secret: any) => ({
...obj,
[secret.key]: secret.values[0].value
[secret.key]: secret
}), {});
interface UpdateNetlifySecret {
key: string;
context: string;
value: string;
}
interface DeleteNetlifySecret {
key: string;
}
interface NewNetlifySecretValue {
value: string;
context: string;
}
interface NewNetlifySecret {
key: string;
values: NewNetlifySecretValue[];
}
let updateSecrets: UpdateNetlifySecret[] = [];
let deleteSecrets: DeleteNetlifySecret[] = [];
let newSecrets: NewNetlifySecret[] = [];
const newSecrets: NetlifySecret[] = []; // createEnvVars
const deleteSecrets: string[] = []; // deleteEnvVar
const deleteSecretValues: NetlifySecret[] = []; // deleteEnvVarValue
const updateSecrets: NetlifySecret[] = []; // setEnvVarValue
// Identify secrets to create
// identify secrets to create and update
Object.keys(secrets).map((key) => {
if (!(key in res)) {
// case: secret has been created
// case: Infisical secret does not exist in Netlify -> create secret
newSecrets.push({
key: key,
key,
values: [{
value: secrets[key], // include id?
value: secrets[key],
context: integration.context
}]
});
}
});
// Identify secrets to update and delete
Object.keys(res).map((key) => {
if (key in secrets) {
if (res[key] !== secrets[key]) {
// case: secret value has changed
} else {
// case: Infisical secret exists in Netlify
const contexts = res[key].values
.reduce((obj: any, value: NetlifyValue) => ({
...obj,
[value.context]: value
}), {});
if (integration.context in contexts) {
// case: Netlify secret value exists in integration context
if (secrets[key] !== contexts[integration.context].value) {
// case: Infisical and Netlify secret values are different
// -> update Netlify secret context and value
updateSecrets.push({
key,
values: [{
context: integration.context,
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: key,
context: integration.context,
value: secrets[key]
key,
values: [{
context: integration.context,
value: secrets[key]
}]
});
}
} else {
// case: secret has been deleted
deleteSecrets.push({
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.context) {
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.context,
value: value.value
}]
});
}
}
});
}
});
const syncParams = new URLSearchParams({
site_id: integration.siteId
});
// Sync/push new secrets
if (newSecrets.length > 0) {
await axios.post(
`${INTEGRATION_NETLIFY_API_URL}/api/v1/accounts/${integrationAuth.accountId}/env`,
@ -382,15 +435,13 @@ const syncSecretsNetlify = async ({
);
}
// Sync/push updated secrets
if (updateSecrets.length > 0) {
updateSecrets.forEach(async (secret: UpdateNetlifySecret) => {
updateSecrets.forEach(async (secret: NetlifySecret) => {
await axios.patch(
`${INTEGRATION_NETLIFY_API_URL}/api/v1/accounts/${integrationAuth.accountId}/env/${secret.key}`,
{
context: secret.context,
value: secret.value
context: secret.values[0].context,
value: secret.values[0].value
},
{
params: syncParams,
@ -402,11 +453,24 @@ const syncSecretsNetlify = async ({
});
}
// Delete secrets
if (deleteSecrets.length > 0) {
deleteSecrets.forEach(async (secret: DeleteNetlifySecret) => {
deleteSecrets.forEach(async (key: string) => {
await axios.delete(
`${INTEGRATION_NETLIFY_API_URL}/api/v1/accounts/${integrationAuth.accountId}/env/${secret.key}`,
`${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: {
@ -416,7 +480,6 @@ const syncSecretsNetlify = async ({
);
});
}
} catch (err) {
Sentry.setUser(null);
Sentry.captureException(err);
@ -424,6 +487,119 @@ const syncSecretsNetlify = async ({
}
}
export {
syncSecrets
}
/**
* Sync/push [secrets] to GitHub [repo]
* @param {Object} obj
* @param {IIntegration} obj.integration - integration details
* @param {IIntegrationAuth} obj.integrationAuth - integration auth details
* @param {Object} obj.secrets - secrets to push to integration (object where keys are secret keys and values are secret values)
*/
const syncSecretsGitHub = async ({
integration,
secrets,
accessToken
}: {
integration: IIntegration;
secrets: any;
accessToken: string;
}) => {
try {
interface GitHubRepoKey {
key_id: string;
key: string;
}
interface GitHubSecret {
name: string;
created_at: string;
updated_at: string;
}
interface GitHubSecretRes {
[index: string]: GitHubSecret;
}
const deleteSecrets: GitHubSecret[] = [];
const octokit = new Octokit({
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: user.login,
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',
{
owner: user.login,
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}',
{
owner: user.login,
repo: integration.app,
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]);
// 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: user.login,
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');
}
};
export { syncSecrets };

View File

@ -1,77 +1,83 @@
import { Schema, model, Types } from 'mongoose';
import {
ENV_DEV,
ENV_TESTING,
ENV_STAGING,
ENV_PROD,
INTEGRATION_HEROKU,
INTEGRATION_VERCEL,
INTEGRATION_NETLIFY
ENV_DEV,
ENV_TESTING,
ENV_STAGING,
ENV_PROD,
INTEGRATION_HEROKU,
INTEGRATION_VERCEL,
INTEGRATION_NETLIFY,
INTEGRATION_GITHUB
} from '../variables';
export interface IIntegration {
_id: Types.ObjectId;
workspace: Types.ObjectId;
environment: 'dev' | 'test' | 'staging' | 'prod';
isActive: boolean;
app: string;
target: string;
context: string;
siteId: string;
integration: 'heroku' | 'vercel' | 'netlify';
integrationAuth: Types.ObjectId;
_id: Types.ObjectId;
workspace: Types.ObjectId;
environment: 'dev' | 'test' | 'staging' | 'prod';
isActive: boolean;
app: string;
target: string;
context: string;
siteId: string;
integration: 'heroku' | 'vercel' | 'netlify' | 'github';
integrationAuth: Types.ObjectId;
}
const integrationSchema = new Schema<IIntegration>(
{
workspace: {
type: Schema.Types.ObjectId,
ref: 'Workspace',
required: true
},
environment: {
type: String,
enum: [ENV_DEV, ENV_TESTING, ENV_STAGING, ENV_PROD],
required: true
},
isActive: {
type: Boolean,
required: true
},
app: { // name of app in provider
type: String,
default: null
},
target: { // vercel-specific target (environment)
type: String,
default: null
},
context: { // netlify-specific context (deploy)
type: String,
default: null
},
siteId: { // netlify-specific site (app) id
type: String,
default: null
},
integration: {
type: String,
enum: [
INTEGRATION_HEROKU,
INTEGRATION_VERCEL,
INTEGRATION_NETLIFY
],
required: true
},
integrationAuth: {
type: Schema.Types.ObjectId,
ref: 'IntegrationAuth',
required: true
}
},
{
timestamps: true
}
{
workspace: {
type: Schema.Types.ObjectId,
ref: 'Workspace',
required: true
},
environment: {
type: String,
enum: [ENV_DEV, ENV_TESTING, ENV_STAGING, ENV_PROD],
required: true
},
isActive: {
type: Boolean,
required: true
},
app: {
// name of app in provider
type: String,
default: null
},
target: {
// vercel-specific target (environment)
type: String,
default: null
},
context: {
// netlify-specific context (deploy)
type: String,
default: null
},
siteId: {
// netlify-specific site (app) id
type: String,
default: null
},
integration: {
type: String,
enum: [
INTEGRATION_HEROKU,
INTEGRATION_VERCEL,
INTEGRATION_NETLIFY,
INTEGRATION_GITHUB
],
required: true
},
integrationAuth: {
type: Schema.Types.ObjectId,
ref: 'IntegrationAuth',
required: true
}
},
{
timestamps: true
}
);
const Integration = model<IIntegration>('Integration', integrationSchema);

View File

@ -1,83 +1,87 @@
import { Schema, model, Types } from 'mongoose';
import {
INTEGRATION_HEROKU,
INTEGRATION_VERCEL,
INTEGRATION_NETLIFY
import {
INTEGRATION_HEROKU,
INTEGRATION_VERCEL,
INTEGRATION_NETLIFY,
INTEGRATION_GITHUB
} from '../variables';
export interface IIntegrationAuth {
_id: Types.ObjectId;
workspace: Types.ObjectId;
integration: 'heroku' | 'vercel' | 'netlify';
teamId: string;
accountId: string;
refreshCiphertext?: string;
refreshIV?: string;
refreshTag?: string;
accessCiphertext?: string;
accessIV?: string;
accessTag?: string;
accessExpiresAt?: Date;
_id: Types.ObjectId;
workspace: Types.ObjectId;
integration: 'heroku' | 'vercel' | 'netlify' | 'github';
teamId: string;
accountId: string;
refreshCiphertext?: string;
refreshIV?: string;
refreshTag?: string;
accessCiphertext?: string;
accessIV?: string;
accessTag?: string;
accessExpiresAt?: Date;
}
const integrationAuthSchema = new Schema<IIntegrationAuth>(
{
workspace: {
type: Schema.Types.ObjectId,
required: true
},
integration: {
type: String,
enum: [
INTEGRATION_HEROKU,
INTEGRATION_VERCEL,
INTEGRATION_NETLIFY
],
required: true
},
teamId: { // vercel-specific integration param
type: String
},
accountId: { // netlify-specific integration param
type: String
},
refreshCiphertext: {
type: String,
select: false
},
refreshIV: {
type: String,
select: false
},
refreshTag: {
type: String,
select: false
},
accessCiphertext: {
type: String,
select: false
},
accessIV: {
type: String,
select: false
},
accessTag: {
type: String,
select: false
},
accessExpiresAt: {
type: Date,
select: false
}
},
{
timestamps: true
}
{
workspace: {
type: Schema.Types.ObjectId,
required: true
},
integration: {
type: String,
enum: [
INTEGRATION_HEROKU,
INTEGRATION_VERCEL,
INTEGRATION_NETLIFY,
INTEGRATION_GITHUB
],
required: true
},
teamId: {
// vercel-specific integration param
type: String
},
accountId: {
// netlify-specific integration param
type: String
},
refreshCiphertext: {
type: String,
select: false
},
refreshIV: {
type: String,
select: false
},
refreshTag: {
type: String,
select: false
},
accessCiphertext: {
type: String,
select: false
},
accessIV: {
type: String,
select: false
},
accessTag: {
type: String,
select: false
},
accessExpiresAt: {
type: Date,
select: false
}
},
{
timestamps: true
}
);
const IntegrationAuth = model<IIntegrationAuth>(
'IntegrationAuth',
integrationAuthSchema
'IntegrationAuth',
integrationAuthSchema
);
export default IntegrationAuth;

View File

@ -34,6 +34,4 @@ router.get(
keyController.getLatestKey
);
router.get('/publicKey/infisical', keyController.getPublicKeyInfisical);
export default router;

View File

@ -0,0 +1,10 @@
/* eslint-disable no-console */
import mongoose from 'mongoose';
export const initDatabase = (MONGO_URL: string) => {
mongoose
.connect(MONGO_URL)
.then(() => console.log('Successfully connected to DB'))
.catch((e) => console.log('Failed to connect to DB ', e));
return mongoose.connection;
};

View File

@ -0,0 +1,32 @@
/* eslint-disable no-console */
import mongoose from 'mongoose';
import { createTerminus } from '@godaddy/terminus';
export const setUpHealthEndpoint = <T>(server: T) => {
const onSignal = () => {
console.log('Server is starting clean-up');
return Promise.all([
new Promise((resolve) => {
if (mongoose.connection && mongoose.connection.readyState == 1) {
mongoose.connection.close()
.then(() => resolve('Database connection closed'));
} else {
resolve('Database connection already closed');
}
})
]);
};
const healthCheck = () => {
// `state.isShuttingDown` (boolean) shows whether the server is shutting down or not
// optionally include a resolve value to be included as info in the health check response
return Promise.resolve();
};
createTerminus(server, {
healthChecks: {
'/healthcheck': healthCheck,
onSignal
}
});
};

View File

@ -0,0 +1,34 @@
import nodemailer from 'nodemailer';
import { SMTP_HOST, SMTP_PORT, SMTP_USERNAME, SMTP_PASSWORD, SMTP_SECURE } from '../config';
import SMTPConnection from 'nodemailer/lib/smtp-connection';
import * as Sentry from '@sentry/node';
const mailOpts: SMTPConnection.Options = {
host: SMTP_HOST,
secure: SMTP_SECURE as boolean,
port: SMTP_PORT as number
};
if (SMTP_USERNAME && SMTP_PASSWORD) {
mailOpts.auth = {
user: SMTP_USERNAME,
pass: SMTP_PASSWORD
};
}
export const initSmtp = () => {
const transporter = nodemailer.createTransport(mailOpts);
transporter
.verify()
.then(() => {
Sentry.setUser(null);
Sentry.captureMessage('SMTP - Successfully connected');
})
.catch((err) => {
Sentry.setUser(null);
Sentry.captureException(
`SMTP - Failed to connect to ${SMTP_HOST}:${SMTP_PORT} \n\t${err}`
);
});
return transporter;
};

View File

@ -1,6 +1,7 @@
import nacl from 'tweetnacl';
import util from 'tweetnacl-util';
import AesGCM from './aes-gcm';
import * as Sentry from '@sentry/node';
/**
* Return new base64, NaCl, public-private key pair.
@ -47,6 +48,8 @@ const encryptAsymmetric = ({
util.decodeBase64(privateKey)
);
} catch (err) {
Sentry.setUser(null);
Sentry.captureException(err);
throw new Error('Failed to perform asymmetric encryption');
}
@ -86,6 +89,8 @@ const decryptAsymmetric = ({
util.decodeBase64(privateKey)
);
} catch (err) {
Sentry.setUser(null);
Sentry.captureException(err);
throw new Error('Failed to perform asymmetric decryption');
}
@ -112,6 +117,8 @@ const encryptSymmetric = ({
iv = obj.iv;
tag = obj.tag;
} catch (err) {
Sentry.setUser(null);
Sentry.captureException(err);
throw new Error('Failed to perform symmetric encryption');
}
@ -147,6 +154,8 @@ const decryptSymmetric = ({
try {
plaintext = AesGCM.decrypt(ciphertext, iv, tag, key);
} catch (err) {
Sentry.setUser(null);
Sentry.captureException(err);
throw new Error('Failed to perform symmetric decryption');
}

View File

@ -1,79 +1,74 @@
import {
ENV_DEV,
ENV_TESTING,
ENV_STAGING,
ENV_PROD,
ENV_SET
ENV_DEV,
ENV_TESTING,
ENV_STAGING,
ENV_PROD,
ENV_SET
} from './environment';
import {
INTEGRATION_HEROKU,
INTEGRATION_VERCEL,
INTEGRATION_NETLIFY,
INTEGRATION_SET,
INTEGRATION_OAUTH2,
INTEGRATION_HEROKU_TOKEN_URL,
INTEGRATION_VERCEL_TOKEN_URL,
INTEGRATION_NETLIFY_TOKEN_URL,
INTEGRATION_HEROKU_API_URL,
INTEGRATION_VERCEL_API_URL,
INTEGRATION_NETLIFY_API_URL,
INTEGRATION_OPTIONS
INTEGRATION_HEROKU,
INTEGRATION_VERCEL,
INTEGRATION_NETLIFY,
INTEGRATION_GITHUB,
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_GITHUB_API_URL,
INTEGRATION_OPTIONS
} from './integration';
import {
OWNER,
ADMIN,
MEMBER,
INVITED,
ACCEPTED,
COMPLETED,
GRANTED
OWNER,
ADMIN,
MEMBER,
INVITED,
ACCEPTED,
COMPLETED,
GRANTED
} from './organization';
import {
SECRET_SHARED,
SECRET_PERSONAL
} from './secret';
import {
PLAN_STARTER,
PLAN_PRO
} from './stripe';
import {
EVENT_PUSH_SECRETS,
EVENT_PULL_SECRETS
} from './event';
import {
ACTION_PUSH_TO_HEROKU
} from './action';
import { SECRET_SHARED, SECRET_PERSONAL } from './secret';
import { PLAN_STARTER, PLAN_PRO } from './stripe';
import { EVENT_PUSH_SECRETS, EVENT_PULL_SECRETS } from './event';
import { ACTION_PUSH_TO_HEROKU } from './action';
export {
OWNER,
ADMIN,
MEMBER,
INVITED,
ACCEPTED,
COMPLETED,
GRANTED,
PLAN_STARTER,
PLAN_PRO,
SECRET_SHARED,
SECRET_PERSONAL,
ENV_DEV,
ENV_TESTING,
ENV_STAGING,
ENV_PROD,
ENV_SET,
INTEGRATION_HEROKU,
INTEGRATION_VERCEL,
INTEGRATION_NETLIFY,
INTEGRATION_SET,
INTEGRATION_OAUTH2,
INTEGRATION_HEROKU_TOKEN_URL,
INTEGRATION_VERCEL_TOKEN_URL,
INTEGRATION_NETLIFY_TOKEN_URL,
INTEGRATION_HEROKU_API_URL,
INTEGRATION_VERCEL_API_URL,
INTEGRATION_NETLIFY_API_URL,
EVENT_PUSH_SECRETS,
EVENT_PULL_SECRETS,
ACTION_PUSH_TO_HEROKU,
INTEGRATION_OPTIONS
};
OWNER,
ADMIN,
MEMBER,
INVITED,
ACCEPTED,
COMPLETED,
GRANTED,
PLAN_STARTER,
PLAN_PRO,
SECRET_SHARED,
SECRET_PERSONAL,
ENV_DEV,
ENV_TESTING,
ENV_STAGING,
ENV_PROD,
ENV_SET,
INTEGRATION_HEROKU,
INTEGRATION_VERCEL,
INTEGRATION_NETLIFY,
INTEGRATION_GITHUB,
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_GITHUB_API_URL,
EVENT_PUSH_SECRETS,
EVENT_PULL_SECRETS,
ACTION_PUSH_TO_HEROKU,
INTEGRATION_OPTIONS
};

View File

@ -1,16 +1,20 @@
import {
CLIENT_ID_HEROKU,
CLIENT_ID_NETLIFY
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_SET = new Set([
INTEGRATION_HEROKU,
INTEGRATION_VERCEL,
INTEGRATION_NETLIFY
INTEGRATION_HEROKU,
INTEGRATION_VERCEL,
INTEGRATION_NETLIFY,
INTEGRATION_GITHUB
]);
// integration types
@ -18,13 +22,17 @@ const INTEGRATION_OAUTH2 = 'oauth2';
// integration oauth endpoints
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_VERCEL_TOKEN_URL =
'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';
// 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_GITHUB_API_URL = 'https://api.github.com';
const INTEGRATION_OPTIONS = [
{
@ -43,6 +51,7 @@ const INTEGRATION_OPTIONS = [
isAvailable: true,
type: 'vercel',
clientId: '',
clientSlug: CLIENT_SLUG_VERCEL,
docsLink: ''
},
{
@ -54,6 +63,16 @@ const INTEGRATION_OPTIONS = [
clientId: CLIENT_ID_NETLIFY,
docsLink: ''
},
{
name: 'GitHub',
slug: 'github',
image: 'GitHub',
isAvailable: true,
type: 'oauth2',
clientId: CLIENT_ID_GITHUB,
docsLink: ''
},
{
name: 'Google Cloud Platform',
slug: 'gcp',
@ -102,16 +121,19 @@ const INTEGRATION_OPTIONS = [
]
export {
INTEGRATION_HEROKU,
INTEGRATION_VERCEL,
INTEGRATION_NETLIFY,
INTEGRATION_SET,
INTEGRATION_OAUTH2,
INTEGRATION_HEROKU_TOKEN_URL,
INTEGRATION_VERCEL_TOKEN_URL,
INTEGRATION_NETLIFY_TOKEN_URL,
INTEGRATION_HEROKU_API_URL,
INTEGRATION_VERCEL_API_URL,
INTEGRATION_NETLIFY_API_URL,
INTEGRATION_OPTIONS
}
INTEGRATION_HEROKU,
INTEGRATION_VERCEL,
INTEGRATION_NETLIFY,
INTEGRATION_GITHUB,
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_GITHUB_API_URL,
INTEGRATION_OPTIONS
};

View File

@ -0,0 +1,12 @@
version: '3'
services:
mongo-test:
image: mongo
container_name: infisical-test-mongo
restart: always
ports:
- 27018:27017
environment:
- MONGO_INITDB_ROOT_USERNAME=test
- MONGO_INITDB_ROOT_PASSWORD=test1234

View File

@ -0,0 +1,5 @@
/* eslint-disable no-undef */
process.env.MONGO_URL =
'mongodb://test:test1234@localhost:27018/?authSource=admin';
process.env.MONGO_USERNAME = 'test';
process.env.MONGO_PASSWORD = 'test1234';

View File

@ -8,16 +8,13 @@
"allowJs": true,
"outDir": "build",
"esModuleInterop": true,
"moduleResolution": "node",
"forceConsistentCasingInFileNames": true,
"strict": true,
"noImplicitAny": true,
"skipLibCheck": true,
"typeRoots" : ["./src/types", "./node_modules/@types"]
"typeRoots": ["./src/types", "./node_modules/@types"]
},
"include": [
"src/**/*"
],
"exclude": [
"node_modules"
]
"include": ["src/**/*"],
"exclude": ["node_modules"]
}

View File

@ -8,6 +8,7 @@ import (
"os"
"os/exec"
"os/signal"
"runtime"
"strings"
"syscall"
@ -19,12 +20,38 @@ import (
// runCmd represents the run command
var runCmd = &cobra.Command{
Example: `
infisical run --env=dev -- npm run dev
infisical run --command "first-command && second-command; more-commands..."
`,
Use: "run [any infisical run command flags] -- [your application start command]",
Short: "Used to inject environments variables into your application process",
DisableFlagsInUseLine: true,
Example: "infisical run --env=prod -- npm run dev",
Args: cobra.MinimumNArgs(1),
PreRun: toggleDebug,
Args: func(cmd *cobra.Command, args []string) error {
// Check if the --command flag has been set
commandFlagSet := cmd.Flags().Changed("command")
// If the --command flag has been set, check if a value was provided
if commandFlagSet {
command := cmd.Flag("command").Value.String()
if command == "" {
return fmt.Errorf("you need to provide a command after the flag --command")
}
// If the --command flag has been set, args should not be provided
if len(args) > 0 {
return fmt.Errorf("you cannot set any arguments after --command flag. --command only takes a string command")
}
} else {
// If the --command flag has not been set, at least one arg should be provided
if len(args) == 0 {
return fmt.Errorf("at least one argument is required after the run command, received %d", len(args))
}
}
return nil
},
Run: func(cmd *cobra.Command, args []string) {
envName, err := cmd.Flags().GetString("env")
if err != nil {
@ -54,10 +81,23 @@ var runCmd = &cobra.Command{
}
if shouldExpandSecrets {
secretsWithSubstitutions := util.SubstituteSecrets(secrets)
execCmd(args[0], args[1:], secretsWithSubstitutions)
secrets = util.SubstituteSecrets(secrets)
}
if cmd.Flags().Changed("command") {
command := cmd.Flag("command").Value.String()
err = executeMultipleCommandWithEnvs(command, secrets)
if err != nil {
log.Errorf("Something went wrong when executing your command [error=%s]", err)
return
}
} else {
execCmd(args[0], args[1:], secrets)
err = executeSingleCommandWithEnvs(args, secrets)
if err != nil {
log.Errorf("Something went wrong when executing your command [error=%s]", err)
return
}
return
}
},
@ -68,22 +108,51 @@ func init() {
runCmd.Flags().StringP("env", "e", "dev", "Set the environment (dev, prod, etc.) from which your secrets should be pulled from")
runCmd.Flags().String("projectId", "", "The project ID from which your secrets should be pulled from")
runCmd.Flags().Bool("expand", true, "Parse shell parameter expansions in your secrets")
runCmd.Flags().StringP("command", "c", "", "chained commands to execute (e.g. \"npm install && npm run dev; echo ...\")")
}
// Credit: inspired by AWS Valut
func execCmd(command string, args []string, envs []models.SingleEnvironmentVariable) error {
numberOfSecretsInjected := fmt.Sprintf("\u2713 Injected %v Infisical secrets into your application process successfully", len(envs))
// Will execute a single command and pass in the given secrets into the process
func executeSingleCommandWithEnvs(args []string, secrets []models.SingleEnvironmentVariable) error {
command := args[0]
argsForCommand := args[1:]
numberOfSecretsInjected := fmt.Sprintf("\u2713 Injected %v Infisical secrets into your application process successfully", len(secrets))
log.Infof("\x1b[%dm%s\x1b[0m", 32, numberOfSecretsInjected)
log.Debugf("executing command: %s %s \n", command, strings.Join(args, " "))
log.Debugln("Secrets injected:", envs)
log.Debugf("executing command: %s %s \n", command, strings.Join(argsForCommand, " "))
log.Debugln("Secrets injected:", secrets)
cmd := exec.Command(command, args...)
cmd := exec.Command(command, argsForCommand...)
cmd.Stdin = os.Stdin
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
cmd.Env = getAllEnvs(envs)
cmd.Env = getAllEnvs(secrets)
return execCmd(cmd)
}
func executeMultipleCommandWithEnvs(fullCommand string, secrets []models.SingleEnvironmentVariable) error {
shell := [2]string{"sh", "-c"}
if runtime.GOOS == "windows" {
shell = [2]string{"cmd", "/C"}
} else {
shell[0] = os.Getenv("SHELL")
}
cmd := exec.Command(shell[0], shell[1], fullCommand)
cmd.Stdin = os.Stdin
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
cmd.Env = getAllEnvs(secrets)
numberOfSecretsInjected := fmt.Sprintf("\u2713 Injected %v Infisical secrets into your application process successfully", len(secrets))
log.Infof("\x1b[%dm%s\x1b[0m", 32, numberOfSecretsInjected)
log.Debugf("executing command: %s %s %s \n", shell[0], shell[1], fullCommand)
log.Debugln("Secrets injected:", secrets)
return execCmd(cmd)
}
// Credit: inspired by AWS Valut
func execCmd(cmd *exec.Cmd) error {
sigChannel := make(chan os.Signal, 1)
signal.Notify(sigChannel)
@ -100,7 +169,7 @@ func execCmd(command string, args []string, envs []models.SingleEnvironmentVaria
if err := cmd.Wait(); err != nil {
_ = cmd.Process.Signal(os.Kill)
return fmt.Errorf("Failed to wait for command termination: %v", err)
return fmt.Errorf("failed to wait for command termination: %v", err)
}
waitStatus := cmd.ProcessState.Sys().(syscall.WaitStatus)

View File

@ -9,7 +9,7 @@ services:
- 80:80
- 443:443
volumes:
- ./nginx/default.dev.conf:/etc/nginx/conf.d/default.conf:ro
- ./nginx/default.conf:/etc/nginx/conf.d/default.conf:ro
depends_on:
- frontend
- backend

View File

@ -30,4 +30,7 @@ infisical export --format=csv > secrets.csv
# Export variables to a JSON file
infisical export --format=json > secrets.json
# Export variables to a YAML file
infisical export --format=yaml > secrets.yaml
```

View File

@ -2,9 +2,25 @@
title: "infisical run"
---
```bash
infisical run [options] -- [your application start command]
```
<Tabs>
<Tab title="Single command">
```bash
infisical run [options] -- [your application start command]
# Example
infisical run [options] -- npm run dev
```
</Tab>
<Tab title="Chained commands">
```bash
infisical run [options] --command [string command]
# Example
infisical run [options] --command "npm run bootstrap && npm run dev start; other-bash-command"
```
</Tab>
</Tabs>
## Description
@ -15,5 +31,6 @@ Inject environment variables from the platform into an application process.
| Option | Description | Default value |
| -------------- | ----------------------------------------------------------------------------------------------------------- | ------------- |
| `--env` | Used to set the environment that secrets are pulled from. Accepted values: `dev`, `staging`, `test`, `prod` | `dev` |
| `--projectId` | Used to link a local project to the platform (required only if injecting via the service token method) | `None` |
| `--projectId` | Used to link a local project to the platform (required only if injecting via the service token method) | None |
| `--expand` | Parse shell parameter expansions in your secrets (e.g., `${DOMAIN}`) | `true` |
| `--command` | Pass secrets into chained commands (e.g., `"first-command && second-command; more-commands..."`) | None |

View File

@ -16,59 +16,54 @@ cd infisical
## Set up environment variables
Before running the docker-compose we have to generate the .env file with the environment variables, you can create your own file or start with the
`.env.example` as an example guide.
Start by creating a .env file at the root of the Infisical directory
Mandatory variables in the `.env` file:
<Tip>
Reference the [environment variable list](https://infisical.com/docs/self-hosting/configuration/envars) and provided [`.env.example`](https://raw.githubusercontent.com/Infisical/infisical/main/.env.example) template to fill out your .env file.
</Tip>
1. Keys and JWT variables
### Keys
![image](https://user-images.githubusercontent.com/118568289/206791534-9c9d1431-e83d-49c0-8a54-b373ed0df820.png)
`ENCRYPTION_KEY`, `JWT_SIGNUP_SECRET`, `JWT_REFRESH_SECRET`, `JWT_AUTH_SECRET`, `JWT_SERVICE_SECRET` values can be generated with this [32-byte random hex generator](https://www.browserling.com/tools/random-hex).
The `.env.example` has these variables empty, you can self generate the `JWT and ENCRYPTION_KEY` with this [32-byte random hex strings generator](https://www.browserling.com/tools/random-hex).
### Database
For the `PRIVATE_KEY and PUBLIC_KEY` you can use the ones shown in the screenshot:
Use to the following `MONGO_URL`, `MONGO_USERNAME`, `MONGO_PASSWORD`, `SITE_URL` values:
```
PRIVATE_KEY='oGVv5rThrpZ7WLgQW27chY1cXngr4wLQIZnGfSKgHPk='
PUBLIC_KEY='ldr6JaC7AY+tun3omGLdE4SWpkJbtVBOI54KfUP53Xc='
MONGO_URL=mongodb://root:example@mongo:27017/?authSource=admin
MONGO_USERNAME=root
MONGO_PASSWORD=example
SITE_URL=http://localhost:8080
```
2. Mongo variables and site URL
<Info>
If you decide to use your own `MONGO_USERNAME` and `MONGO_PASSWORD`, you'll have to modify `MONGO_URL` to take the form: `mongodb://[MONGO_USERNAME]:[MONGO_PASSWORD]@mongo:27017/?authSource=admin`.
</Info>
![image](https://user-images.githubusercontent.com/118568289/206792171-3376e3c6-c3ac-4d5d-8776-d78ee089b520.png)
### Mailing
These variables are used to connect the MongoDB and set the URL for the localhost.
Option 1: Bring your own SMTP server and credentials by filling in `SMTP_HOST`, `SMTP_FROM_ADDRESS`, `SMTP_FROM_NAME`, `SMTP_USERNAME`, and `SMTP_PASSWORD`.
<Info>
`SMTP_HOST` is set to `smtp.gmail.com` by default. For `SMTP_USERNAME` and `SMTP_PASSWORD`, you'll need an email with 2-step-verification and an [app password](https://support.google.com/mail/answer/185833?hl=en) for it.
</Info>
For development, you can use `root` for the `MONGO_USERNAME` and `example` for the `MONGO_PASSWORD` as shown in the screenshot.
Take into account that if you use your own `MONGO_USERNAME` and `MONGO_PASSWORD`, you also have to change the `MONGO_URL` with the form of `MONGO_USERNAME:MONGO_PASSWORD` after the `//` part of the URL.
3. Mail SMTP service variables
![image](https://user-images.githubusercontent.com/118568289/206792653-ba3211d1-1071-43f2-93a7-8b408bbd9e0e.png)
If you want to receive actual emails (e.g. you want to test how the email message will look like), take note of the following.
For the `SMTP_USERNAME` variable, you will need an email with 2-steps-verification.
For the `SMTP_PASSWORD` variable, you will need to [generate an app password](https://support.google.com/mail/answer/185833?hl=en) with the email you used in the `SMTP_USERNAME` variable.
Otherwise, a local SMTP server (MailHog) is available for testing purposes. Set the following values to use this:
Option 2: Use the provided (Mailhog) SMTP server and browse emails sent by the backend on `http://localhost:8025`. To use this option, set the following `SMTP_HOST`, `SMTP_PORT`, `SMTP_FROM_NAME`, `SMTP_USERNAME`, `SMTP_PASSWORD` values:
```
SMTP_HOST=smtp-server
SMTP_PORT=1025
SMTP_NAME=<whatever you like>
SMTP_FROM_ADDRESS=team@infisical.com
SMTP_FROM_NAME=[whatever you like]
SMTP_USERNAME=team@infisical.com
SMTP_PASSWORD=
```
Make sure to leave the `SMTP_PASSWORD` blank so the backend will be able to connect to MailHog
You can browse `http://localhost:8025/` to browse email messages sent by the backend.
With these environment variables, you will be ready to run the docker-compose.
<Warning>
Make sure to leave the `SMTP_PASSWORD` blank so the backend can connect to MailHog.
</Warning>
## Docker for development
@ -84,12 +79,4 @@ Then browse http://localhost:8080
docker-compose -f docker-compose.dev.yml down
# start services
docker-compose -f docker-compose.dev.yml up
```
The docker-compose development environment consists of:
- nginx
- frontend
- backend
- mongo
- mongo-express
```

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 842 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 740 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 862 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 MiB

View File

@ -0,0 +1,34 @@
---
title: "GitHub Actions"
---
<Warning>
Infisical can sync secrets to GitHub repo secrets only. If your repo uses environment secrets, then stay tuned with this [issue](https://github.com/Infisical/infisical/issues/54).
</Warning>
Prerequisites:
- Set up and add envars to [Infisical Cloud](https://app.infisical.com)
- Ensure you have admin privileges to the repo you want to sync secrets to.
## Navigate to your project's integrations tab
![integrations](../../images/integrations.png)
## Authorize Infisical for GitHub
Press on the GitHub tile and grant Infisical access to your GitHub account (repo privileges only).
![integrations github authorization](../../images/integrations-github-auth.png)
<Info>
If this is your project's first cloud integration, then you'll have to grant Infisical access to your project's environment variables.
Although this step breaks E2EE, it's necessary for Infisical to sync the environment variables to the cloud platform.
</Info>
## Start integration
Select which Infisical environment secrets you want to sync to which GitHub repo and press start integration to start syncing secrets to the repo.
![integrations github](../../images/integrations-github.png)

View File

@ -1,26 +1,29 @@
---
title: "Heroku"
description: "With this integration, you can automatically sync your secrets to Heroku as soon as you update secrets in Infisical."
---
## Instructions
Prerequisites:
### Step 1: Open the integrations console
- Set up and add envars to [Infisical Cloud](https://app.infisical.com)
Open the Infisical Dashboard. Choose the project in which you want to set up the intergation. Go to the integrations tab in the left sidebar.
## Navigate to your project's integrations tab
### Step 2: Authenticate with Heroku
![integrations](../../images/integrations.png)
Click on "Heroku" tile. Log in if required and provide the necessary permissions to Infisical. You will afterwards be redirected back to the integrations page.
## Authorize Infisical for Heroku
Note: during an integration with Heroku, for security reasons, it is impossible to maintain end-to-end encryption. In theory, this lets Infisical decrypt yor environment variables. In practice, we can assure you that this will never be done, and it allows us to protect your secrets from bad actors online. With any questions, reach out support@infisical.com.
Press on the Heroku tile and grant Infisical access to your Heroku account.
### Step 3: Start integration
![integrations heroku authorization](../../images/integrations-heroku-auth.png)
Choose a Heroku App that you want to sync the secrets to, and the Infisical project environment that you want to sync the secrets from. Start the integration.
The integration should now show status 'In Sync'. Every time you edit secrets, they will be automatically pushed to Heroku.
<Info>
If you need to update your integration, you will have to delete the current one and create a new one.
<Info>
If this is your project's first cloud integration, then you'll have to grant Infisical access to your project's environment variables.
Although this step breaks E2EE, it's necessary for Infisical to sync the environment variables to the cloud platform.
</Info>
## Start integration
Select which Infisical environment secrets you want to sync to which Heroku app and press start integration to start syncing secrets to Heroku.
![integrations heroku](../../images/integrations-heroku.png)

View File

@ -0,0 +1,32 @@
---
title: "Netlify"
---
<Warning>
Infisical integrates with Netlify's new environment variable experience. If your site uses Netlify's old environment variable experience, you'll have to upgrade it to the new one to use this integration.
</Warning>
Prerequisites:
- Set up and add envars to [Infisical Cloud](https://app.infisical.com)
## Navigate to your project's integrations tab
![integrations](../../images/integrations.png)
## Authorize Infisical for Netlify
Press on the Netlify tile and grant Infisical access to your Netlify account.
![integrations netlify authorization](../../images/integrations-netlify-auth.png)
<Info>
If this is your project's first cloud integration, then you'll have to grant Infisical access to your project's environment variables.
Although this step breaks E2EE, it's necessary for Infisical to sync the environment variables to the cloud platform.
</Info>
## Start integration
Select which Infisical environment secrets you want to sync to which Netlify app and context. Lastly, press start integration to start syncing secrets to Netlify.
![integrations netlify](../../images/integrations-netlify.png)

View File

@ -2,4 +2,22 @@
title: "Vercel"
---
Coming soon.
Prerequisites:
- Set up and add envars to [Infisical Cloud](https://app.infisical.com)
## Navigate to your project's integrations tab
![integrations](../../images/integrations.png)
## Authorize Infisical for Vercel
Press on the Vercel tile and grant Infisical access to your Vercel account.
![integrations vercel authorization](../../images/integrations-vercel-auth.png)
## Start integration
Select which Infisical environment secrets you want to sync to which Vercel app and environment. Lastly, press start integration to start syncing secrets to Vercel.
![integrations vercel](../../images/integrations-vercel.png)

View File

@ -12,6 +12,9 @@ Missing an integration? Throw in a [request](https://github.com/Infisical/infisi
| [Docker-Compose](/integrations/platforms/docker-compose) | Platform | Available |
| [Kubernetes](/integrations/platforms/kubernetes) | Platform | Available |
| [Heroku](/integrations/cloud/heroku) | Cloud | Available |
| [Vercel](/integrations/cloud/vercel) | Cloud | Available |
| [Netlify](/integrations/cloud/netlify) | Cloud | Available |
| [GitHub Actions](/integrations/cicd/githubactions) | CI/CD | Available |
| [React](/integrations/frameworks/react) | Framework | Available |
| [Vue](/integrations/frameworks/vue) | Framework | Available |
| [Express](/integrations/frameworks/express) | Framework | Available |
@ -26,7 +29,6 @@ Missing an integration? Throw in a [request](https://github.com/Infisical/infisi
| [Flask](/integrations/frameworks/flask) | Framework | Available |
| [Laravel](/integrations/frameworks/laravel) | Framework | Available |
| [Ruby on Rails](/integrations/frameworks/rails) | Framework | Available |
| [Vercel](/integrations/cloud/vercel) | Cloud | Coming soon |
| [Render](/integrations/cloud/render) | Cloud | Coming soon |
| [Fly.io](/integrations/cloud/flyio) | Cloud | Coming soon |
| AWS | Cloud | Coming soon |

View File

@ -133,13 +133,17 @@
"pages": [
"integrations/cloud/heroku",
"integrations/cloud/vercel",
"integrations/cloud/netlify",
"integrations/cloud/render",
"integrations/cloud/flyio"
]
},
{
"group": "CI/CD",
"pages": ["integrations/cicd/circleci"]
"pages": [
"integrations/cicd/githubactions",
"integrations/cicd/circleci"
]
},
{
"group": "Frameworks",

View File

@ -9,12 +9,11 @@ Configuring Infisical requires setting some environment variables. There is a fi
| Variable | Description | Default Value |
| ---------------------------- | ----------------------------------------------------------------------------------------------------------- | ---------------- |
| `PRIVATE_KEY` | ❗️ NaCl-generated server secret key | `None` |
| `PUBLIC_KEY` | ❗️ NaCl-generated server public key | `None` |
| `ENCRYPTION_KEY` | ❗️ Strong hex encryption key | `None` |
| `JWT_SIGNUP_SECRET` | ❗️ JWT token secret | `None` |
| `JWT_REFRESH_SECRET` | ❗️ JWT token secret | `None` |
| `JWT_AUTH_SECRET` | ❗️ JWT token secret | `None` |
| `JWT_SERVICE_SECRET` | ❗️ JWT token secret | `None` |
| `JWT_SIGNUP_LIFETIME` | JWT token lifetime expressed in seconds or a string describing a time span (e.g. 60, "2 days", "10h", "7d") | `15m` |
| `JWT_REFRESH_LIFETIME` | JWT token lifetime expressed in seconds or a string describing a time span (e.g. 60, "2 days", "10h", "7d") | `90d` |
| `JWT_AUTH_LIFETIME` | JWT token lifetime expressed in seconds or a string describing a time span (e.g. 60, "2 days", "10h", "7d") | `10d` |
@ -24,13 +23,20 @@ Configuring Infisical requires setting some environment variables. There is a fi
| `MONGO_PASSWORD` | MongoDB password if using container | `None` |
| `SITE_URL` | ❗️ Site URL - should be an absolute URL including the protocol (e.g. `https://app.infisical.com`) | `None` |
| `SMTP_HOST` | Hostname to connect to for establishing SMTP connections | `smtp.gmail.com` |
| `SMTP_NAME` | Name label to be used in From field (e.g. `Team`) | `None` |
| `SMTP_SECURE` | Use TLS when connecting to host. If false, TLS will be used if STARTTLS is supported | `false` |
| `SMTP_PORT` | Port to connect to for establishing SMTP connections | `587` |
| `SMTP_FROM_ADDRESS` | ❗️ Email address to be used for sending emails (e.g. `team@infisical.com`) | `None` |
| `SMTP_FROM_NAME` | Name label to be used in From field (e.g. `Team`) | `Infisical` |
| `SMTP_USERNAME` | ❗️ Credential to connect to host (e.g. `team@infisical.com`) | `None` |
| `SMTP_PASSWORD` | ❗️ Credential to connect to host | `None` |
| `TELEMETRY_ENABLED` | `true` or `false`. [More](../overview). | `true` |
| `CLIENT_ID_VERCEL` | OAuth client id for Vercel integration | `None` |
| `CLIENT_ID_NETLIFY` | OAuth client id for Netlify integration | `None` |
| `CLIENT_SECRET_HEROKU` | OAuth client secret for Heroku integration | `None` |
| `CLIENT_SECRET_VERCEL` | OAuth client secret for Vercel integration | `None` |
| `CLIENT_SECRET_NETLIFY` | OAuth client secret for Netlify integration | `None` |
| `CLIENT_ID_HEROKU` | OAuth2 client ID for Heroku integration | `None` |
| `CLIENT_ID_VERCEL` | OAuth2 client ID for Vercel integration | `None` |
| `CLIENT_ID_NETLIFY` | OAuth2 client ID for Netlify integration | `None` |
| `CLIENT_ID_GITHUB` | OAuth2 client ID for GitHub integration | `None` |
| `CLIENT_SECRET_HEROKU` | OAuth2 client secret for Heroku integration | `None` |
| `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` |
| `SENTRY_DSN` | DSN for error-monitoring with Sentry | `None` |

View File

@ -42,7 +42,7 @@ that by adding the `--namespace <namespace-to-install-to>` to your `helm install
```bash
## Installs to default namespace
helm install infisical-helm-charts/infisical --values <path to the values.yaml you downloaded/created in step 2>
helm install infisical-helm-charts/infisical --generate-name --values <path to the values.yaml you downloaded/created in step 2>
```
<Note>
@ -50,5 +50,4 @@ If you have not filled out all of the required environment variables, you will s
do so.
</Note>
4. Your Infisical installation is complete and should be running on the host name you specified in Ingress in `values.yaml`.
Note: Please allow an additional time (2 minutes) for the frontend pods to be fully ready.
#### 4. Your Infisical installation is complete and should be running on the host name you specified in Ingress in `values.yaml`.

View File

@ -11,7 +11,7 @@ import { Listbox, Transition } from "@headlessui/react";
interface ListBoxProps {
selected: string;
onChange: (arg: string) => void;
data: string[];
data: string[] | null;
text?: string;
buttonAction?: () => void;
isFull?: boolean;

View File

@ -1,7 +1,8 @@
import { Fragment } from "react";
import { Dialog, Transition } from "@headlessui/react";
import getLatestFileKey from "../../../pages/api/workspace/getLatestFileKey";
import setBotActiveStatus from "../../../pages/api/bot/setBotActiveStatus";
import getLatestFileKey from "../../../pages/api/workspace/getLatestFileKey";
import {
decryptAssymmetric,
encryptAssymmetric

View File

@ -1,7 +1,8 @@
import { Fragment } from "react";
import { Dialog, Transition } from "@headlessui/react";
import getLatestFileKey from "../../../pages/api/workspace/getLatestFileKey";
import setBotActiveStatus from "../../../pages/api/bot/setBotActiveStatus";
import getLatestFileKey from "../../../pages/api/workspace/getLatestFileKey";
import {
decryptAssymmetric,
encryptAssymmetric

View File

@ -14,9 +14,11 @@ import deleteIntegration from "../../pages/api/integrations/DeleteIntegration"
import getIntegrationApps from "../../pages/api/integrations/GetIntegrationApps";
import updateIntegration from "../../pages/api/integrations/updateIntegration"
import {
contextNetlifyMapping,
envMapping,
reverseContextNetlifyMapping,
reverseEnvMapping} from "../../public/data/frequentConstants";
reverseEnvMapping,
} from "../../public/data/frequentConstants";
interface Integration {
_id: string;
@ -25,6 +27,7 @@ interface Integration {
integration: string;
integrationAuth: string;
isActive: boolean;
context: string;
}
interface IntegrationApp {
@ -69,7 +72,7 @@ const Integration = ({
setIntegrationTarget("Development");
break;
case "netlify":
setIntegrationContext("All");
setIntegrationContext(integration?.context ? contextNetlifyMapping[integration.context] : "Local development");
break;
default:
break;
@ -93,7 +96,7 @@ const Integration = ({
"Production",
"Preview",
"Development"
] : []}
] : null}
selected={"Production"}
onChange={setIntegrationTarget}
/>
@ -107,12 +110,11 @@ const Integration = ({
</div>
<ListBox
data={!integration.isActive ? [
"All",
"Production",
"Deploy previews",
"Branch deploys",
"Local development"
] : []}
] : null}
selected={integrationContext}
onChange={setIntegrationContext}
/>
@ -138,7 +140,7 @@ const Integration = ({
"Staging",
"Testing",
"Production",
] : []}
] : null}
selected={integrationEnvironment}
onChange={(environment) => {
setIntegrationEnvironment(environment);
@ -166,7 +168,7 @@ const Integration = ({
APP
</div>
<ListBox
data={!integration.isActive ? apps.map((app) => app.name) : []}
data={!integration.isActive ? apps.map((app) => app.name) : null}
selected={integrationApp}
onChange={(app) => {
setIntegrationApp(app);
@ -190,7 +192,8 @@ const Integration = ({
onButtonPressed={async () => {
const siteApp = apps.find((app) => app.name === integrationApp); // obj or undefined
const siteId = siteApp ? siteApp.siteId : null;
const siteId = siteApp?.siteId ? siteApp.siteId : null;
const result = await updateIntegration({
integrationId: integration._id,
environment: envMapping[integrationEnvironment],
@ -200,6 +203,7 @@ const Integration = ({
context: integrationContext ? reverseContextNetlifyMapping[integrationContext] : null,
siteId
});
router.reload();
}}
color="mineshaft"

View File

@ -15,6 +15,7 @@ interface IntegrationType {
integration: string;
integrationAuth: string;
isActive: boolean;
context: string;
}
const ProjectIntegrationSection = ({

View File

@ -42,9 +42,9 @@ const attemptLogin = async (
async () => {
const clientPublicKey = client.getPublicKey();
const { serverPublicKey, salt } = await login1(email, clientPublicKey);
try {
const { serverPublicKey, salt } = await login1(email, clientPublicKey);
client.setSalt(salt);
client.setServerPublicKey(serverPublicKey);
const clientProof = client.getProof(); // called M1

37
frontend/pages/github.js Normal file
View File

@ -0,0 +1,37 @@
import React, { useEffect } from "react";
import Head from "next/head";
import { useRouter } from "next/router";
const queryString = require("query-string");
import AuthorizeIntegration from "./api/integrations/authorizeIntegration";
export default function Github() {
const router = useRouter();
const parsedUrl = queryString.parse(router.asPath.split("?")[1]);
const code = parsedUrl.code;
const state = parsedUrl.state;
/**
* Here we forward to the default workspace if a user opens this url
*/
// eslint-disable-next-line react-hooks/exhaustive-deps
useEffect(async () => {
try {
if (state === localStorage.getItem('latestCSRFToken')) {
localStorage.removeItem('latestCSRFToken');
await AuthorizeIntegration({
workspaceId: localStorage.getItem('projectData.id'),
code,
integration: "github",
});
router.push("/integrations/" + localStorage.getItem("projectData.id"));
}
} catch (error) {
console.error('Github integration error: ', error);
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
return <div></div>;
}
Github.requireAuth = true;

View File

@ -41,7 +41,7 @@ export default function Integrations() {
setCloudIntegrationOptions(
await getIntegrationOptions()
);
// get project integration authorizations
setIntegrationAuths(
await getWorkspaceAuthorizations({
@ -123,6 +123,8 @@ export default function Integrations() {
* @returns
*/
const handleIntegrationOption = async ({ integrationOption }) => {
console.log('handleIntegrationOption', integrationOption);
try {
// generate CSRF token for OAuth2 code-token exchange integrations
@ -134,11 +136,14 @@ export default function Integrations() {
window.location = `https://id.heroku.com/oauth/authorize?client_id=${integrationOption.clientId}&response_type=code&scope=write-protected&state=${state}`;
break;
case 'Vercel':
window.location = `https://vercel.com/integrations/infisical-dev/new?state=${state}`;
window.location = `https://vercel.com/integrations/${integrationOption.clientSlug}/new?state=${state}`;
break;
case 'Netlify':
window.location = `https://app.netlify.com/authorize?client_id=${integrationOption.clientId}&response_type=code&state=${state}&redirect_uri=${window.location.origin}/netlify`;
break;
case 'GitHub':
window.location = `https://github.com/login/oauth/authorize?client_id=${integrationOption.clientId}&response_type=code&scope=repo&redirect_uri=${window.location.origin}/github&state=${state}`;
break;
// case 'Fly.io':
// console.log('fly.io');
// setIntegrationAccessTokenDialogOpen(true);

View File

@ -16,8 +16,14 @@ const reverseEnvMapping: Mapping = {
test: "Testing",
};
const contextNetlifyMapping: Mapping = {
"dev": "Local development",
"branch-deploy": "Branch deploys",
"deploy-review": "Deploy Previews",
"production": "Production"
}
const reverseContextNetlifyMapping: Mapping = {
"All": "all",
"Local development": "dev",
"Branch deploys": "branch-deploy",
"Deploy Previews": "deploy-preview",
@ -25,6 +31,7 @@ const reverseContextNetlifyMapping: Mapping = {
}
export {
contextNetlifyMapping,
envMapping,
reverseContextNetlifyMapping,
reverseEnvMapping};
reverseEnvMapping}

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.4 KiB

View File

@ -7,7 +7,7 @@ type: application
# This is the chart version. This version number should be incremented each time you make changes
# to the chart and its templates, including the app version.
# Versions are expected to follow Semantic Versioning (https://semver.org/)
version: 0.1.3
version: 0.1.6
# This is the version number of the application being deployed. This version number should be
# incremented each time you make changes to the application. Versions are not expected to

View File

@ -20,6 +20,11 @@ spec:
imagePullPolicy: {{ .Values.backend.image.pullPolicy }}
ports:
- containerPort: 4000
{{- if .Values.backend.kubeSecretRef }}
envFrom:
- secretRef:
name: {{ .Values.backend.kubeSecretRef }}
{{- end }}
env:
{{- range $key, $value := .Values.backendEnvironmentVariables }}
{{- if $value | quote | eq "MUST_REPLACE" }}

View File

@ -18,6 +18,12 @@ spec:
- name: frontend
image: infisical/frontend
imagePullPolicy: {{ .Values.frontend.image.pullPolicy }}
{{- if .Values.frontend.kubeSecretRef }}
envFrom:
- secretRef:
name: {{ .Values.frontend.kubeSecretRef }}
{{- end }}
{{- if .Values.frontendEnvironmentVariables }}
env:
{{- range $key, $value := .Values.frontendEnvironmentVariables }}
{{- if $value | quote | eq "MUST_REPLACE" }}
@ -26,8 +32,9 @@ spec:
- name: {{ $key }}
value: {{ quote $value }}
{{- end }}
{{- end }}
ports:
- containerPort: 4000
- containerPort: 3000
---
apiVersion: v1
kind: Service

View File

@ -3,14 +3,14 @@
# PLEASE REPLACE VALUES/EDIT AS REQUIRED
#####
namespace: infisical
frontend:
replicaCount: 1
image:
repository:
pullPolicy: IfNotPresent
tag: "latest"
# kubeSecretRef: some-kube-secret-name
backend:
replicaCount: 1
@ -18,10 +18,12 @@ backend:
repository:
pullPolicy: IfNotPresent
tag: "latest"
# kubeSecretRef: some-kube-secret-name
ingress:
enabled: true
annotations: {}
annotations:
kubernetes.io/ingress.class: "nginx"
hostName: example.com
frontend:
path: /
@ -54,8 +56,6 @@ ingress:
###
backendEnvironmentVariables:
# Required keys for platform encryption/decryption ops. Replace with nacl sk keys
PRIVATE_KEY: MUST_REPLACE
PUBLIC_KEY: MUST_REPLACE
ENCRYPTION_KEY: MUST_REPLACE
# JWT
@ -71,9 +71,8 @@ backendEnvironmentVariables:
SMTP_USERNAME: MUST_REPLACE
SMTP_PASSWORD: MUST_REPLACE
# You may replace with Mongo Cloud URI
# Recommended to replace with Mongo Cloud URI as the DB instance in the cluster does not have persistence yet
MONGO_URL: mongodb://root:root@mongodb-service:27017/
# frontendEnvironmentVariables:
# INFISICAL_TELEMETRY_ENABLED: true