Merge branch 'heads/main' into fix/helm-charts-improvements

This commit is contained in:
Grraahaam
2023-03-22 00:19:02 +01:00
94 changed files with 2139 additions and 1165 deletions
-3
View File
@@ -16,9 +16,6 @@ JWT_AUTH_LIFETIME=
JWT_REFRESH_LIFETIME= JWT_REFRESH_LIFETIME=
JWT_SIGNUP_LIFETIME= JWT_SIGNUP_LIFETIME=
# Optional lifetimes for OTP expressed in seconds
EMAIL_TOKEN_LIFETIME=
# MongoDB # MongoDB
# Backend will connect to the MongoDB instance at connection string MONGO_URL which can either be a ref # Backend will connect to the MongoDB instance at connection string MONGO_URL which can either be a ref
# to the MongoDB container instance or Mongo Cloud # to the MongoDB container instance or Mongo Cloud
+1 -1
View File
@@ -25,7 +25,7 @@
<img src="https://img.shields.io/github/commit-activity/m/infisical/infisical" alt="git commit activity" /> <img src="https://img.shields.io/github/commit-activity/m/infisical/infisical" alt="git commit activity" />
</a> </a>
<a href="https://cloudsmith.io/~infisical/repos/"> <a href="https://cloudsmith.io/~infisical/repos/">
<img src="https://img.shields.io/badge/Downloads-45.7k-orange" alt="Cloudsmith downloads" /> <img src="https://img.shields.io/badge/Downloads-55.7k-orange" alt="Cloudsmith downloads" />
</a> </a>
<a href="https://join.slack.com/t/infisical-users/shared_invite/zt-1kdbk07ro-RtoyEt_9E~fyzGo_xQYP6g"> <a href="https://join.slack.com/t/infisical-users/shared_invite/zt-1kdbk07ro-RtoyEt_9E~fyzGo_xQYP6g">
<img src="https://img.shields.io/badge/chat-on%20Slack-blueviolet" alt="Slack community channel" /> <img src="https://img.shields.io/badge/chat-on%20Slack-blueviolet" alt="Slack community channel" />
-19
View File
@@ -1,19 +0,0 @@
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);
});
});
-1
View File
@@ -4,7 +4,6 @@ declare global {
namespace NodeJS { namespace NodeJS {
interface ProcessEnv { interface ProcessEnv {
PORT: string; PORT: string;
EMAIL_TOKEN_LIFETIME: string;
ENCRYPTION_KEY: string; ENCRYPTION_KEY: string;
SALT_ROUNDS: string; SALT_ROUNDS: string;
JWT_AUTH_LIFETIME: string; JWT_AUTH_LIFETIME: string;
+9
View File
@@ -0,0 +1,9 @@
export default {
preset: 'ts-jest',
testEnvironment: 'node',
collectCoverageFrom: ['src/*.{js,ts}', '!**/node_modules/**'],
modulePaths: ['<rootDir>/src'],
testMatch: ['<rootDir>/tests/**/*.test.ts'],
setupFiles: ['<rootDir>/test-resources/env-vars.js'],
setupFilesAfterEnv: ['<rootDir>/tests/setupTests.ts']
};
+105 -27
View File
@@ -12,7 +12,7 @@
"@aws-sdk/client-secrets-manager": "^3.267.0", "@aws-sdk/client-secrets-manager": "^3.267.0",
"@godaddy/terminus": "^4.11.2", "@godaddy/terminus": "^4.11.2",
"@octokit/rest": "^19.0.5", "@octokit/rest": "^19.0.5",
"@sentry/node": "^7.14.0", "@sentry/node": "^7.39.0",
"@sentry/tracing": "^7.19.0", "@sentry/tracing": "^7.19.0",
"@types/crypto-js": "^4.1.1", "@types/crypto-js": "^4.1.1",
"@types/libsodium-wrappers": "^0.7.10", "@types/libsodium-wrappers": "^0.7.10",
@@ -32,6 +32,7 @@
"express-validator": "^6.14.2", "express-validator": "^6.14.2",
"handlebars": "^4.7.7", "handlebars": "^4.7.7",
"helmet": "^5.1.1", "helmet": "^5.1.1",
"infisical-node": "^1.0.37",
"js-yaml": "^4.1.0", "js-yaml": "^4.1.0",
"jsonwebtoken": "^9.0.0", "jsonwebtoken": "^9.0.0",
"jsrp": "^0.2.4", "jsrp": "^0.2.4",
@@ -39,13 +40,13 @@
"lodash": "^4.17.21", "lodash": "^4.17.21",
"mongoose": "^6.7.2", "mongoose": "^6.7.2",
"nodemailer": "^6.8.0", "nodemailer": "^6.8.0",
"posthog-node": "^2.2.2", "posthog-node": "^2.5.4",
"query-string": "^7.1.3", "query-string": "^7.1.3",
"request-ip": "^3.3.0", "request-ip": "^3.3.0",
"rimraf": "^3.0.2", "rimraf": "^3.0.2",
"stripe": "^10.7.0", "stripe": "^10.7.0",
"swagger-autogen": "^2.22.0", "swagger-autogen": "^2.22.0",
"swagger-ui-express": "^4.6.0", "swagger-ui-express": "^4.6.2",
"tweetnacl": "^1.0.3", "tweetnacl": "^1.0.3",
"tweetnacl-util": "^0.15.1", "tweetnacl-util": "^0.15.1",
"typescript": "^4.9.3", "typescript": "^4.9.3",
@@ -2799,13 +2800,13 @@
"integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==" "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg=="
}, },
"node_modules/@sentry/node": { "node_modules/@sentry/node": {
"version": "7.38.0", "version": "7.39.0",
"resolved": "https://registry.npmjs.org/@sentry/node/-/node-7.38.0.tgz", "resolved": "https://registry.npmjs.org/@sentry/node/-/node-7.39.0.tgz",
"integrity": "sha512-jNIN6NZvgzn/oms8RQzffjX8Z0LQDTN6N28nnhzqGCvnfmS1QtTt0FlU+pTuFXZNNSjfGy4XMXMYvLlbvhm2bg==", "integrity": "sha512-oe1OBxgs6t/FizjxkSPtuvJv5wJMO+mLENZkiE0PpBD56JyZrWK48kYIt2ccWAfk6Vh235/oIpmqET150xB4lQ==",
"dependencies": { "dependencies": {
"@sentry/core": "7.38.0", "@sentry/core": "7.39.0",
"@sentry/types": "7.38.0", "@sentry/types": "7.39.0",
"@sentry/utils": "7.38.0", "@sentry/utils": "7.39.0",
"cookie": "^0.4.1", "cookie": "^0.4.1",
"https-proxy-agent": "^5.0.0", "https-proxy-agent": "^5.0.0",
"lru_map": "^0.3.3", "lru_map": "^0.3.3",
@@ -2815,6 +2816,39 @@
"node": ">=8" "node": ">=8"
} }
}, },
"node_modules/@sentry/node/node_modules/@sentry/core": {
"version": "7.39.0",
"resolved": "https://registry.npmjs.org/@sentry/core/-/core-7.39.0.tgz",
"integrity": "sha512-45WJIcWWCQnZ8zhHtcrkJjQ4YydmzMWY4pmRuBG7Qp+zrCT6ISoyODcjY+SCHFdvXkiYFi8+bFZa1qG3YQnnYw==",
"dependencies": {
"@sentry/types": "7.39.0",
"@sentry/utils": "7.39.0",
"tslib": "^1.9.3"
},
"engines": {
"node": ">=8"
}
},
"node_modules/@sentry/node/node_modules/@sentry/types": {
"version": "7.39.0",
"resolved": "https://registry.npmjs.org/@sentry/types/-/types-7.39.0.tgz",
"integrity": "sha512-5Y83Y8O3dT5zT2jTKEIPMcpn5lUm05KRMaCXuw0sRsv4r9TbBUKeqiSU1LjowT8rB/XNy8m7DHav8+NmogPaJw==",
"engines": {
"node": ">=8"
}
},
"node_modules/@sentry/node/node_modules/@sentry/utils": {
"version": "7.39.0",
"resolved": "https://registry.npmjs.org/@sentry/utils/-/utils-7.39.0.tgz",
"integrity": "sha512-/ZxlPgm1mGgmuMckCTc9iyqDuFTEYNEoMB53IjVFz8ann+37OiWB7Py/QV1rEEsv3xKrGbA8thhRhV9E1sjTlQ==",
"dependencies": {
"@sentry/types": "7.39.0",
"tslib": "^1.9.3"
},
"engines": {
"node": ">=8"
}
},
"node_modules/@sentry/node/node_modules/tslib": { "node_modules/@sentry/node/node_modules/tslib": {
"version": "1.14.1", "version": "1.14.1",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz",
@@ -5973,6 +6007,16 @@
"node": ">=0.8.19" "node": ">=0.8.19"
} }
}, },
"node_modules/infisical-node": {
"version": "1.0.37",
"resolved": "https://registry.npmjs.org/infisical-node/-/infisical-node-1.0.37.tgz",
"integrity": "sha512-9ZswN5UovZq46a7Qv/4KmfaAu9pO/TmxxdcEY2PosDIAlXbpfC651hcKr7T8q1iMlRnHLA/gpYkcZMeS0es0rg==",
"dependencies": {
"axios": "^1.3.3",
"tweetnacl": "^1.0.3",
"tweetnacl-util": "^0.15.1"
}
},
"node_modules/inflight": { "node_modules/inflight": {
"version": "1.0.6", "version": "1.0.6",
"resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz",
@@ -10489,9 +10533,9 @@
} }
}, },
"node_modules/posthog-node": { "node_modules/posthog-node": {
"version": "2.5.3", "version": "2.5.4",
"resolved": "https://registry.npmjs.org/posthog-node/-/posthog-node-2.5.3.tgz", "resolved": "https://registry.npmjs.org/posthog-node/-/posthog-node-2.5.4.tgz",
"integrity": "sha512-kDmBjQHguPrh/rUTKmB0+Hj7C3fq2t+/fcfQkDBGz0f0fEF2WxV5yyxRmd2IF/hFmHxMrGLDkEVjKr78B+judg==", "integrity": "sha512-CdywlVh0CZU05/3MrBc0qY/zsLdU2X9XSz/yL1qMRhbyZhD8lrnuGlI69G2cpzZtli6S/nu64wcmULz/mFFA5w==",
"dependencies": { "dependencies": {
"axios": "^0.27.0" "axios": "^0.27.0"
}, },
@@ -11472,9 +11516,9 @@
"integrity": "sha512-4J4XekQG0ol4/TyUzMfksrWsMTbw/7JYlT+SFaX7H0xamd1OeuVlUSb/Cbq4qdDx1lc+uLZQW7u2mlImcE8c+w==" "integrity": "sha512-4J4XekQG0ol4/TyUzMfksrWsMTbw/7JYlT+SFaX7H0xamd1OeuVlUSb/Cbq4qdDx1lc+uLZQW7u2mlImcE8c+w=="
}, },
"node_modules/swagger-ui-express": { "node_modules/swagger-ui-express": {
"version": "4.6.1", "version": "4.6.2",
"resolved": "https://registry.npmjs.org/swagger-ui-express/-/swagger-ui-express-4.6.1.tgz", "resolved": "https://registry.npmjs.org/swagger-ui-express/-/swagger-ui-express-4.6.2.tgz",
"integrity": "sha512-Pss7YNFKNdq66XKNjRe4IRXKKYNx/LvOSml9TdrZ8/78UpxUHIp9JoXpXWA5Z4L+SCmX63DZ9IPlQ8nnRuncvA==", "integrity": "sha512-MHIOaq9JrTTB3ygUJD+08PbjM5Tt/q7x80yz9VTFIatw8j5uIWKcr90S0h5NLMzFEDC6+eVprtoeA5MDZXCUKQ==",
"dependencies": { "dependencies": {
"swagger-ui-dist": ">=4.11.0" "swagger-ui-dist": ">=4.11.0"
}, },
@@ -14350,19 +14394,43 @@
} }
}, },
"@sentry/node": { "@sentry/node": {
"version": "7.38.0", "version": "7.39.0",
"resolved": "https://registry.npmjs.org/@sentry/node/-/node-7.38.0.tgz", "resolved": "https://registry.npmjs.org/@sentry/node/-/node-7.39.0.tgz",
"integrity": "sha512-jNIN6NZvgzn/oms8RQzffjX8Z0LQDTN6N28nnhzqGCvnfmS1QtTt0FlU+pTuFXZNNSjfGy4XMXMYvLlbvhm2bg==", "integrity": "sha512-oe1OBxgs6t/FizjxkSPtuvJv5wJMO+mLENZkiE0PpBD56JyZrWK48kYIt2ccWAfk6Vh235/oIpmqET150xB4lQ==",
"requires": { "requires": {
"@sentry/core": "7.38.0", "@sentry/core": "7.39.0",
"@sentry/types": "7.38.0", "@sentry/types": "7.39.0",
"@sentry/utils": "7.38.0", "@sentry/utils": "7.39.0",
"cookie": "^0.4.1", "cookie": "^0.4.1",
"https-proxy-agent": "^5.0.0", "https-proxy-agent": "^5.0.0",
"lru_map": "^0.3.3", "lru_map": "^0.3.3",
"tslib": "^1.9.3" "tslib": "^1.9.3"
}, },
"dependencies": { "dependencies": {
"@sentry/core": {
"version": "7.39.0",
"resolved": "https://registry.npmjs.org/@sentry/core/-/core-7.39.0.tgz",
"integrity": "sha512-45WJIcWWCQnZ8zhHtcrkJjQ4YydmzMWY4pmRuBG7Qp+zrCT6ISoyODcjY+SCHFdvXkiYFi8+bFZa1qG3YQnnYw==",
"requires": {
"@sentry/types": "7.39.0",
"@sentry/utils": "7.39.0",
"tslib": "^1.9.3"
}
},
"@sentry/types": {
"version": "7.39.0",
"resolved": "https://registry.npmjs.org/@sentry/types/-/types-7.39.0.tgz",
"integrity": "sha512-5Y83Y8O3dT5zT2jTKEIPMcpn5lUm05KRMaCXuw0sRsv4r9TbBUKeqiSU1LjowT8rB/XNy8m7DHav8+NmogPaJw=="
},
"@sentry/utils": {
"version": "7.39.0",
"resolved": "https://registry.npmjs.org/@sentry/utils/-/utils-7.39.0.tgz",
"integrity": "sha512-/ZxlPgm1mGgmuMckCTc9iyqDuFTEYNEoMB53IjVFz8ann+37OiWB7Py/QV1rEEsv3xKrGbA8thhRhV9E1sjTlQ==",
"requires": {
"@sentry/types": "7.39.0",
"tslib": "^1.9.3"
}
},
"tslib": { "tslib": {
"version": "1.14.1", "version": "1.14.1",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz",
@@ -16758,6 +16826,16 @@
"integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==",
"dev": true "dev": true
}, },
"infisical-node": {
"version": "1.0.37",
"resolved": "https://registry.npmjs.org/infisical-node/-/infisical-node-1.0.37.tgz",
"integrity": "sha512-9ZswN5UovZq46a7Qv/4KmfaAu9pO/TmxxdcEY2PosDIAlXbpfC651hcKr7T8q1iMlRnHLA/gpYkcZMeS0es0rg==",
"requires": {
"axios": "^1.3.3",
"tweetnacl": "^1.0.3",
"tweetnacl-util": "^0.15.1"
}
},
"inflight": { "inflight": {
"version": "1.0.6", "version": "1.0.6",
"resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz",
@@ -20028,9 +20106,9 @@
} }
}, },
"posthog-node": { "posthog-node": {
"version": "2.5.3", "version": "2.5.4",
"resolved": "https://registry.npmjs.org/posthog-node/-/posthog-node-2.5.3.tgz", "resolved": "https://registry.npmjs.org/posthog-node/-/posthog-node-2.5.4.tgz",
"integrity": "sha512-kDmBjQHguPrh/rUTKmB0+Hj7C3fq2t+/fcfQkDBGz0f0fEF2WxV5yyxRmd2IF/hFmHxMrGLDkEVjKr78B+judg==", "integrity": "sha512-CdywlVh0CZU05/3MrBc0qY/zsLdU2X9XSz/yL1qMRhbyZhD8lrnuGlI69G2cpzZtli6S/nu64wcmULz/mFFA5w==",
"requires": { "requires": {
"axios": "^0.27.0" "axios": "^0.27.0"
}, },
@@ -20758,9 +20836,9 @@
"integrity": "sha512-4J4XekQG0ol4/TyUzMfksrWsMTbw/7JYlT+SFaX7H0xamd1OeuVlUSb/Cbq4qdDx1lc+uLZQW7u2mlImcE8c+w==" "integrity": "sha512-4J4XekQG0ol4/TyUzMfksrWsMTbw/7JYlT+SFaX7H0xamd1OeuVlUSb/Cbq4qdDx1lc+uLZQW7u2mlImcE8c+w=="
}, },
"swagger-ui-express": { "swagger-ui-express": {
"version": "4.6.1", "version": "4.6.2",
"resolved": "https://registry.npmjs.org/swagger-ui-express/-/swagger-ui-express-4.6.1.tgz", "resolved": "https://registry.npmjs.org/swagger-ui-express/-/swagger-ui-express-4.6.2.tgz",
"integrity": "sha512-Pss7YNFKNdq66XKNjRe4IRXKKYNx/LvOSml9TdrZ8/78UpxUHIp9JoXpXWA5Z4L+SCmX63DZ9IPlQ8nnRuncvA==", "integrity": "sha512-MHIOaq9JrTTB3ygUJD+08PbjM5Tt/q7x80yz9VTFIatw8j5uIWKcr90S0h5NLMzFEDC6+eVprtoeA5MDZXCUKQ==",
"requires": { "requires": {
"swagger-ui-dist": ">=4.11.0" "swagger-ui-dist": ">=4.11.0"
} }
+4 -14
View File
@@ -3,7 +3,7 @@
"@aws-sdk/client-secrets-manager": "^3.267.0", "@aws-sdk/client-secrets-manager": "^3.267.0",
"@godaddy/terminus": "^4.11.2", "@godaddy/terminus": "^4.11.2",
"@octokit/rest": "^19.0.5", "@octokit/rest": "^19.0.5",
"@sentry/node": "^7.14.0", "@sentry/node": "^7.39.0",
"@sentry/tracing": "^7.19.0", "@sentry/tracing": "^7.19.0",
"@types/crypto-js": "^4.1.1", "@types/crypto-js": "^4.1.1",
"@types/libsodium-wrappers": "^0.7.10", "@types/libsodium-wrappers": "^0.7.10",
@@ -23,6 +23,7 @@
"express-validator": "^6.14.2", "express-validator": "^6.14.2",
"handlebars": "^4.7.7", "handlebars": "^4.7.7",
"helmet": "^5.1.1", "helmet": "^5.1.1",
"infisical-node": "^1.0.37",
"js-yaml": "^4.1.0", "js-yaml": "^4.1.0",
"jsonwebtoken": "^9.0.0", "jsonwebtoken": "^9.0.0",
"jsrp": "^0.2.4", "jsrp": "^0.2.4",
@@ -30,13 +31,13 @@
"lodash": "^4.17.21", "lodash": "^4.17.21",
"mongoose": "^6.7.2", "mongoose": "^6.7.2",
"nodemailer": "^6.8.0", "nodemailer": "^6.8.0",
"posthog-node": "^2.2.2", "posthog-node": "^2.5.4",
"query-string": "^7.1.3", "query-string": "^7.1.3",
"request-ip": "^3.3.0", "request-ip": "^3.3.0",
"rimraf": "^3.0.2", "rimraf": "^3.0.2",
"stripe": "^10.7.0", "stripe": "^10.7.0",
"swagger-autogen": "^2.22.0", "swagger-autogen": "^2.22.0",
"swagger-ui-express": "^4.6.0", "swagger-ui-express": "^4.6.2",
"tweetnacl": "^1.0.3", "tweetnacl": "^1.0.3",
"tweetnacl-util": "^0.15.1", "tweetnacl-util": "^0.15.1",
"typescript": "^4.9.3", "typescript": "^4.9.3",
@@ -100,17 +101,6 @@
"ts-jest": "^29.0.3", "ts-jest": "^29.0.3",
"ts-node": "^10.9.1" "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": { "jest-junit": {
"outputDirectory": "reports", "outputDirectory": "reports",
"outputName": "jest-junit.xml", "outputName": "jest-junit.xml",
-144
View File
@@ -1,144 +0,0 @@
// eslint-disable-next-line @typescript-eslint/no-var-requires
const { patchRouterParam } = require('./utils/patchAsyncRoutes');
import express from 'express';
import helmet from 'helmet';
import cors from 'cors';
import cookieParser from 'cookie-parser';
import dotenv from 'dotenv';
import swaggerUi = require('swagger-ui-express');
// eslint-disable-next-line @typescript-eslint/no-var-requires
const swaggerFile = require('../spec.json');
// eslint-disable-next-line @typescript-eslint/no-var-requires
const requestIp = require('request-ip');
dotenv.config();
import { PORT, NODE_ENV, SITE_URL } from './config';
import { apiLimiter } from './helpers/rateLimiter';
import {
workspace as eeWorkspaceRouter,
secret as eeSecretRouter,
secretSnapshot as eeSecretSnapshotRouter,
action as eeActionRouter
} from './ee/routes/v1';
import {
signup as v1SignupRouter,
auth as v1AuthRouter,
bot as v1BotRouter,
organization as v1OrganizationRouter,
workspace as v1WorkspaceRouter,
membershipOrg as v1MembershipOrgRouter,
membership as v1MembershipRouter,
key as v1KeyRouter,
inviteOrg as v1InviteOrgRouter,
user as v1UserRouter,
userAction as v1UserActionRouter,
secret as v1SecretRouter,
serviceToken as v1ServiceTokenRouter,
password as v1PasswordRouter,
stripe as v1StripeRouter,
integration as v1IntegrationRouter,
integrationAuth as v1IntegrationAuthRouter
} from './routes/v1';
import {
signup as v2SignupRouter,
auth as v2AuthRouter,
users as v2UsersRouter,
organizations as v2OrganizationsRouter,
workspace as v2WorkspaceRouter,
secret as v2SecretRouter, // begin to phase out
secrets as v2SecretsRouter,
serviceTokenData as v2ServiceTokenDataRouter,
apiKeyData as v2APIKeyDataRouter,
environment as v2EnvironmentRouter,
tags as v2TagsRouter,
} from './routes/v2';
import { healthCheck } from './routes/status';
import { getLogger } from './utils/logger';
import { RouteNotFoundError } from './utils/errors';
import { requestErrorHandler } from './middleware/requestErrorHandler';
// patch async route params to handle Promise Rejections
patchRouterParam();
export const app = express();
app.enable('trust proxy');
app.use(express.json());
app.use(cookieParser());
app.use(
cors({
credentials: true,
origin: SITE_URL
})
);
app.use(requestIp.mw())
if (NODE_ENV === 'production') {
// enable app-wide rate-limiting + helmet security
// in production
app.disable('x-powered-by');
app.use(apiLimiter);
app.use(helmet());
}
// (EE) routes
app.use('/api/v1/secret', eeSecretRouter);
app.use('/api/v1/secret-snapshot', eeSecretSnapshotRouter);
app.use('/api/v1/workspace', eeWorkspaceRouter);
app.use('/api/v1/action', eeActionRouter);
// v1 routes
app.use('/api/v1/signup', v1SignupRouter);
app.use('/api/v1/auth', v1AuthRouter);
app.use('/api/v1/bot', v1BotRouter);
app.use('/api/v1/user', v1UserRouter);
app.use('/api/v1/user-action', v1UserActionRouter);
app.use('/api/v1/organization', v1OrganizationRouter);
app.use('/api/v1/workspace', v1WorkspaceRouter);
app.use('/api/v1/membership-org', v1MembershipOrgRouter);
app.use('/api/v1/membership', v1MembershipRouter);
app.use('/api/v1/key', v1KeyRouter);
app.use('/api/v1/invite-org', v1InviteOrgRouter);
app.use('/api/v1/secret', v1SecretRouter);
app.use('/api/v1/service-token', v1ServiceTokenRouter); // deprecated
app.use('/api/v1/password', v1PasswordRouter);
app.use('/api/v1/stripe', v1StripeRouter);
app.use('/api/v1/integration', v1IntegrationRouter);
app.use('/api/v1/integration-auth', v1IntegrationAuthRouter);
// v2 routes
app.use('/api/v2/signup', v2SignupRouter);
app.use('/api/v2/auth', v2AuthRouter);
app.use('/api/v2/users', v2UsersRouter);
app.use('/api/v2/organizations', v2OrganizationsRouter);
app.use('/api/v2/workspace', v2EnvironmentRouter);
app.use('/api/v2/workspace', v2TagsRouter);
app.use('/api/v2/workspace', v2WorkspaceRouter);
app.use('/api/v2/secret', v2SecretRouter); // deprecated
app.use('/api/v2/secrets', v2SecretsRouter);
app.use('/api/v2/service-token', v2ServiceTokenDataRouter); // TODO: turn into plural route
app.use('/api/v2/api-key', v2APIKeyDataRouter);
// api docs
app.use('/api-docs', swaggerUi.serve, swaggerUi.setup(swaggerFile))
// Server status
app.use('/api', healthCheck)
//* Handle unrouted requests and respond with proper error message as well as status code
app.use((req, res, next) => {
if (res.headersSent) return next();
next(RouteNotFoundError({ message: `The requested source '(${req.method})${req.url}' was not found` }))
})
//* Error Handling Middleware (must be after all routing logic)
app.use(requestErrorHandler)
export const server = app.listen(PORT, () => {
getLogger("backend-main").info(`Server started listening at port ${PORT}`)
});
+51 -105
View File
@@ -1,105 +1,51 @@
const PORT = process.env.PORT || 4000; import infisical from 'infisical-node';
const EMAIL_TOKEN_LIFETIME = parseInt(process.env.EMAIL_TOKEN_LIFETIME! || '86400'); export const getPort = () => infisical.get('PORT')! || 4000;
const INVITE_ONLY_SIGNUP = process.env.INVITE_ONLY_SIGNUP == undefined ? false : process.env.INVITE_ONLY_SIGNUP export const getInviteOnlySignup = () => infisical.get('INVITE_ONLY_SIGNUP')! == undefined ? false : infisical.get('INVITE_ONLY_SIGNUP');
const ENCRYPTION_KEY = process.env.ENCRYPTION_KEY!; export const getEncryptionKey = () => infisical.get('ENCRYPTION_KEY')!;
const SALT_ROUNDS = parseInt(process.env.SALT_ROUNDS!) || 10; export const getSaltRounds = () => parseInt(infisical.get('SALT_ROUNDS')!) || 10;
const JWT_AUTH_LIFETIME = process.env.JWT_AUTH_LIFETIME! || '10d'; export const getJwtAuthLifetime = () => infisical.get('JWT_AUTH_LIFETIME')! || '10d';
const JWT_AUTH_SECRET = process.env.JWT_AUTH_SECRET!; export const getJwtAuthSecret = () => infisical.get('JWT_AUTH_SECRET')!;
const JWT_MFA_LIFETIME = process.env.JWT_MFA_LIFETIME! || '5m'; export const getJwtMfaLifetime = () => infisical.get('JWT_MFA_LIFETIME')! || '5m';
const JWT_MFA_SECRET = process.env.JWT_MFA_SECRET!; export const getJwtMfaSecret = () => infisical.get('JWT_MFA_LIFETIME')! || '5m';
const JWT_REFRESH_LIFETIME = process.env.JWT_REFRESH_LIFETIME! || '90d'; export const getJwtRefreshLifetime = () => infisical.get('JWT_REFRESH_LIFETIME')! || '90d';
const JWT_REFRESH_SECRET = process.env.JWT_REFRESH_SECRET!; export const getJwtRefreshSecret = () => infisical.get('JWT_REFRESH_SECRET')!;
const JWT_SERVICE_SECRET = process.env.JWT_SERVICE_SECRET!; export const getJwtServiceSecret = () => infisical.get('JWT_SERVICE_SECRET')!;
const JWT_SIGNUP_LIFETIME = process.env.JWT_SIGNUP_LIFETIME! || '15m'; export const getJwtSignupLifetime = () => infisical.get('JWT_SIGNUP_LIFETIME')! || '15m';
const JWT_SIGNUP_SECRET = process.env.JWT_SIGNUP_SECRET!; export const getJwtSignupSecret = () => infisical.get('JWT_SIGNUP_SECRET')!;
const MONGO_URL = process.env.MONGO_URL!; export const getMongoURL = () => infisical.get('MONGO_URL')!;
const NODE_ENV = process.env.NODE_ENV! || 'production'; export const getNodeEnv = () => infisical.get('NODE_ENV')!;
const VERBOSE_ERROR_OUTPUT = process.env.VERBOSE_ERROR_OUTPUT! === 'true' && true; export const getVerboseErrorOutput = () => infisical.get('VERBOSE_ERROR_OUTPUT')! === 'true' && true;
const LOKI_HOST = process.env.LOKI_HOST || undefined; export const getLokiHost = () => infisical.get('LOKI_HOST')!;
const CLIENT_ID_AZURE = process.env.CLIENT_ID_AZURE!; export const getClientIdAzure = () => infisical.get('CLIENT_ID_AZURE')!;
const CLIENT_ID_HEROKU = process.env.CLIENT_ID_HEROKU!; export const getClientIdHeroku = () => infisical.get('CLIENT_ID_HEROKU')!;
const CLIENT_ID_VERCEL = process.env.CLIENT_ID_VERCEL!; export const getClientIdVercel = () => infisical.get('CLIENT_ID_VERCEL')!;
const CLIENT_ID_NETLIFY = process.env.CLIENT_ID_NETLIFY!; export const getClientIdNetlify = () => infisical.get('CLIENT_ID_NETLIFY')!;
const CLIENT_ID_GITHUB = process.env.CLIENT_ID_GITHUB!; export const getClientIdGitHub = () => infisical.get('CLIENT_ID_GITHUB')!;
const CLIENT_ID_GITLAB = process.env.CLIENT_ID_GITLAB!; export const getClientIdGitLab = () => infisical.get('CLIENT_ID_GITLAB')!;
const CLIENT_SECRET_AZURE = process.env.CLIENT_SECRET_AZURE!; export const getClientSecretAzure = () => infisical.get('CLIENT_SECRET_AZURE')!;
const CLIENT_SECRET_HEROKU = process.env.CLIENT_SECRET_HEROKU!; export const getClientSecretHeroku = () => infisical.get('CLIENT_SECRET_HEROKU')!;
const CLIENT_SECRET_VERCEL = process.env.CLIENT_SECRET_VERCEL!; export const getClientSecretVercel = () => infisical.get('CLIENT_SECRET_VERCEL')!;
const CLIENT_SECRET_NETLIFY = process.env.CLIENT_SECRET_NETLIFY!; export const getClientSecretNetlify = () => infisical.get('CLIENT_SECRET_NETLIFY')!;
const CLIENT_SECRET_GITHUB = process.env.CLIENT_SECRET_GITHUB!; export const getClientSecretGitHub = () => infisical.get('CLIENT_SECRET_GITHUB')!;
const CLIENT_SECRET_GITLAB = process.env.CLIENT_SECRET_GITLAB; export const getClientSecretGitLab = () => infisical.get('CLIENT_SECRET_GITLAB')!;
const CLIENT_SLUG_VERCEL = process.env.CLIENT_SLUG_VERCEL!; export const getClientSlugVercel = () => infisical.get('CLIENT_SLUG_VERCEL')!;
const POSTHOG_HOST = process.env.POSTHOG_HOST! || 'https://app.posthog.com'; export const getPostHogHost = () => infisical.get('POSTHOG_HOST')! || 'https://app.posthog.com';
const POSTHOG_PROJECT_API_KEY = export const getPostHogProjectApiKey = () => infisical.get('POSTHOG_PROJECT_API_KEY')! || 'phc_nSin8j5q2zdhpFDI1ETmFNUIuTG4DwKVyIigrY10XiE';
process.env.POSTHOG_PROJECT_API_KEY! || export const getSentryDSN = () => infisical.get('SENTRY_DSN')!;
'phc_nSin8j5q2zdhpFDI1ETmFNUIuTG4DwKVyIigrY10XiE'; export const getSiteURL = () => infisical.get('SITE_URL')!;
const SENTRY_DSN = process.env.SENTRY_DSN!; export const getSmtpHost = () => infisical.get('SMTP_HOST')!;
const SITE_URL = process.env.SITE_URL!; export const getSmtpSecure = () => infisical.get('SMTP_SECURE')! === 'true' || false;
const SMTP_HOST = process.env.SMTP_HOST!; export const getSmtpPort = () => parseInt(infisical.get('SMTP_PORT')!) || 587;
const SMTP_SECURE = process.env.SMTP_SECURE! === 'true' || false; export const getSmtpUsername = () => infisical.get('SMTP_USERNAME')!;
const SMTP_PORT = parseInt(process.env.SMTP_PORT!) || 587; export const getSmtpPassword = () => infisical.get('SMTP_PASSWORD')!;
const SMTP_USERNAME = process.env.SMTP_USERNAME!; export const getSmtpFromAddress = () => infisical.get('SMTP_FROM_ADDRESS')!;
const SMTP_PASSWORD = process.env.SMTP_PASSWORD!; export const getSmtpFromName = () => infisical.get('SMTP_FROM_NAME')! || 'Infisical';
const SMTP_FROM_ADDRESS = process.env.SMTP_FROM_ADDRESS!; export const getStripeProductStarter = () => infisical.get('STRIPE_PRODUCT_STARTER')!;
const SMTP_FROM_NAME = process.env.SMTP_FROM_NAME! || 'Infisical'; export const getStripeProductPro = () => infisical.get('STRIPE_PRODUCT_PRO')!;
const STRIPE_PRODUCT_STARTER = process.env.STRIPE_PRODUCT_STARTER!; export const getStripeProductTeam = () => infisical.get('STRIPE_PRODUCT_TEAM')!;
const STRIPE_PRODUCT_PRO = process.env.STRIPE_PRODUCT_PRO!; export const getStripePublishableKey = () => infisical.get('STRIPE_PUBLISHABLE_KEY')!;
const STRIPE_PRODUCT_TEAM = process.env.STRIPE_PRODUCT_TEAM!; export const getStripeSecretKey = () => infisical.get('STRIPE_SECRET_KEY')!;
const STRIPE_PUBLISHABLE_KEY = process.env.STRIPE_PUBLISHABLE_KEY!; export const getStripeWebhookSecret = () => infisical.get('STRIPE_WEBHOOK_SECRET')!;
const STRIPE_SECRET_KEY = process.env.STRIPE_SECRET_KEY!; export const getTelemetryEnabled = () => infisical.get('TELEMETRY_ENABLED')! !== 'false' && true;
const STRIPE_WEBHOOK_SECRET = process.env.STRIPE_WEBHOOK_SECRET!; export const getLoopsApiKey = () => infisical.get('LOOPS_API_KEY')!;
const TELEMETRY_ENABLED = process.env.TELEMETRY_ENABLED! !== 'false' && true; export const getSmtpConfigured = () => infisical.get('SMTP_HOST') == '' || infisical.get('SMTP_HOST') == undefined ? false : true
const LICENSE_KEY = process.env.LICENSE_KEY!;
export {
PORT,
EMAIL_TOKEN_LIFETIME,
INVITE_ONLY_SIGNUP,
ENCRYPTION_KEY,
SALT_ROUNDS,
JWT_AUTH_LIFETIME,
JWT_AUTH_SECRET,
JWT_MFA_LIFETIME,
JWT_MFA_SECRET,
JWT_REFRESH_LIFETIME,
JWT_REFRESH_SECRET,
JWT_SERVICE_SECRET,
JWT_SIGNUP_LIFETIME,
JWT_SIGNUP_SECRET,
MONGO_URL,
NODE_ENV,
VERBOSE_ERROR_OUTPUT,
LOKI_HOST,
CLIENT_ID_AZURE,
CLIENT_ID_HEROKU,
CLIENT_ID_VERCEL,
CLIENT_ID_NETLIFY,
CLIENT_ID_GITHUB,
CLIENT_ID_GITLAB,
CLIENT_SECRET_AZURE,
CLIENT_SECRET_HEROKU,
CLIENT_SECRET_VERCEL,
CLIENT_SECRET_NETLIFY,
CLIENT_SECRET_GITHUB,
CLIENT_SECRET_GITLAB,
CLIENT_SLUG_VERCEL,
POSTHOG_HOST,
POSTHOG_PROJECT_API_KEY,
SENTRY_DSN,
SITE_URL,
SMTP_HOST,
SMTP_PORT,
SMTP_SECURE,
SMTP_USERNAME,
SMTP_PASSWORD,
SMTP_FROM_ADDRESS,
SMTP_FROM_NAME,
STRIPE_PRODUCT_STARTER,
STRIPE_PRODUCT_TEAM,
STRIPE_PRODUCT_PRO,
STRIPE_PUBLISHABLE_KEY,
STRIPE_SECRET_KEY,
STRIPE_WEBHOOK_SECRET,
TELEMETRY_ENABLED,
LICENSE_KEY
};
+13 -13
View File
@@ -1,8 +1,8 @@
/* eslint-disable @typescript-eslint/no-var-requires */ import * as Sentry from '@sentry/node';
import { Request, Response } from 'express'; import { Request, Response } from 'express';
import jwt from 'jsonwebtoken'; import jwt from 'jsonwebtoken';
import * as Sentry from '@sentry/node';
import * as bigintConversion from 'bigint-conversion'; import * as bigintConversion from 'bigint-conversion';
// eslint-disable-next-line @typescript-eslint/no-var-requires
const jsrp = require('jsrp'); const jsrp = require('jsrp');
import { User, LoginSRPDetail } from '../../models'; import { User, LoginSRPDetail } from '../../models';
import { createToken, issueAuthTokens, clearTokens } from '../../helpers/auth'; import { createToken, issueAuthTokens, clearTokens } from '../../helpers/auth';
@@ -11,15 +11,15 @@ import {
ACTION_LOGIN, ACTION_LOGIN,
ACTION_LOGOUT ACTION_LOGOUT
} from '../../variables'; } from '../../variables';
import {
NODE_ENV,
JWT_AUTH_LIFETIME,
JWT_AUTH_SECRET,
JWT_REFRESH_SECRET
} from '../../config';
import { BadRequestError } from '../../utils/errors'; import { BadRequestError } from '../../utils/errors';
import { EELogService } from '../../ee/services'; import { EELogService } from '../../ee/services';
import { getChannelFromUserAgent } from '../../utils/posthog'; // TODO: move this import { getChannelFromUserAgent } from '../../utils/posthog'; // TODO: move this
import {
getNodeEnv,
getJwtRefreshSecret,
getJwtAuthLifetime,
getJwtAuthSecret
} from '../../config';
declare module 'jsonwebtoken' { declare module 'jsonwebtoken' {
export interface UserIDJwtPayload extends jwt.JwtPayload { export interface UserIDJwtPayload extends jwt.JwtPayload {
@@ -126,7 +126,7 @@ export const login2 = async (req: Request, res: Response) => {
httpOnly: true, httpOnly: true,
path: '/', path: '/',
sameSite: 'strict', sameSite: 'strict',
secure: NODE_ENV === 'production' ? true : false secure: getNodeEnv() === 'production' ? true : false
}); });
const loginAction = await EELogService.createAction({ const loginAction = await EELogService.createAction({
@@ -182,7 +182,7 @@ export const logout = async (req: Request, res: Response) => {
httpOnly: true, httpOnly: true,
path: '/', path: '/',
sameSite: 'strict', sameSite: 'strict',
secure: NODE_ENV === 'production' ? true : false secure: getNodeEnv() === 'production' ? true : false
}); });
const logoutAction = await EELogService.createAction({ const logoutAction = await EELogService.createAction({
@@ -237,7 +237,7 @@ export const getNewToken = async (req: Request, res: Response) => {
} }
const decodedToken = <jwt.UserIDJwtPayload>( const decodedToken = <jwt.UserIDJwtPayload>(
jwt.verify(refreshToken, JWT_REFRESH_SECRET) jwt.verify(refreshToken, getJwtRefreshSecret())
); );
const user = await User.findOne({ const user = await User.findOne({
@@ -252,8 +252,8 @@ export const getNewToken = async (req: Request, res: Response) => {
payload: { payload: {
userId: decodedToken.userId userId: decodedToken.userId
}, },
expiresIn: JWT_AUTH_LIFETIME, expiresIn: getJwtAuthLifetime(),
secret: JWT_AUTH_SECRET secret: getJwtAuthSecret()
}); });
return res.status(200).send({ return res.status(200).send({
@@ -5,7 +5,7 @@ import {
IntegrationAuth, IntegrationAuth,
Bot Bot
} from '../../models'; } from '../../models';
import { INTEGRATION_SET, INTEGRATION_OPTIONS } from '../../variables'; import { INTEGRATION_SET, getIntegrationOptions as getIntegrationOptionsFunc } from '../../variables';
import { IntegrationService } from '../../services'; import { IntegrationService } from '../../services';
import { import {
getApps, getApps,
@@ -39,9 +39,11 @@ export const getIntegrationAuth = async (req: Request, res: Response) => {
} }
export const getIntegrationOptions = async (req: Request, res: Response) => { export const getIntegrationOptions = async (req: Request, res: Response) => {
return res.status(200).send({ const INTEGRATION_OPTIONS = getIntegrationOptionsFunc();
integrationOptions: INTEGRATION_OPTIONS,
}); return res.status(200).send({
integrationOptions: INTEGRATION_OPTIONS,
});
}; };
/** /**
@@ -1,13 +1,13 @@
import { Request, Response } from 'express';
import * as Sentry from '@sentry/node'; import * as Sentry from '@sentry/node';
import { Membership, MembershipOrg, User, Key, IMembership, Workspace } from '../../models'; import { Request, Response } from 'express';
import { Membership, MembershipOrg, User, Key } from '../../models';
import { import {
findMembership, findMembership,
deleteMembership as deleteMember deleteMembership as deleteMember
} from '../../helpers/membership'; } from '../../helpers/membership';
import { sendMail } from '../../helpers/nodemailer'; import { sendMail } from '../../helpers/nodemailer';
import { SITE_URL } from '../../config';
import { ADMIN, MEMBER, ACCEPTED } from '../../variables'; import { ADMIN, MEMBER, ACCEPTED } from '../../variables';
import { getSiteURL } from '../../config';
/** /**
* Check that user is a member of workspace with id [workspaceId] * Check that user is a member of workspace with id [workspaceId]
@@ -215,7 +215,7 @@ export const inviteUserToWorkspace = async (req: Request, res: Response) => {
inviterFirstName: req.user.firstName, inviterFirstName: req.user.firstName,
inviterEmail: req.user.email, inviterEmail: req.user.email,
workspaceName: req.membership.workspace.name, workspaceName: req.membership.workspace.name,
callback_url: SITE_URL + '/login' callback_url: getSiteURL() + '/login'
} }
}); });
} catch (err) { } catch (err) {
@@ -1,6 +1,5 @@
import { Request, Response } from 'express'; import { Request, Response } from 'express';
import * as Sentry from '@sentry/node'; import * as Sentry from '@sentry/node';
import { SITE_URL, JWT_SIGNUP_LIFETIME, JWT_SIGNUP_SECRET } from '../../config';
import { MembershipOrg, Organization, User } from '../../models'; import { MembershipOrg, Organization, User } from '../../models';
import { deleteMembershipOrg as deleteMemberFromOrg } from '../../helpers/membershipOrg'; import { deleteMembershipOrg as deleteMemberFromOrg } from '../../helpers/membershipOrg';
import { createToken } from '../../helpers/auth'; import { createToken } from '../../helpers/auth';
@@ -8,6 +7,7 @@ import { updateSubscriptionOrgQuantity } from '../../helpers/organization';
import { sendMail } from '../../helpers/nodemailer'; import { sendMail } from '../../helpers/nodemailer';
import { TokenService } from '../../services'; import { TokenService } from '../../services';
import { OWNER, ADMIN, MEMBER, ACCEPTED, INVITED, TOKEN_EMAIL_ORG_INVITATION } from '../../variables'; import { OWNER, ADMIN, MEMBER, ACCEPTED, INVITED, TOKEN_EMAIL_ORG_INVITATION } from '../../variables';
import { getSiteURL, getJwtSignupLifetime, getJwtSignupSecret, getSmtpConfigured } from '../../config';
/** /**
* Delete organization membership with id [membershipOrgId] from organization * Delete organization membership with id [membershipOrgId] from organization
@@ -99,9 +99,11 @@ export const changeMembershipOrgRole = async (req: Request, res: Response) => {
* @returns * @returns
*/ */
export const inviteUserToOrganization = async (req: Request, res: Response) => { export const inviteUserToOrganization = async (req: Request, res: Response) => {
let invitee, inviteeMembershipOrg; let invitee, inviteeMembershipOrg, completeInviteLink;
try { try {
const { organizationId, inviteeEmail } = req.body; const { organizationId, inviteeEmail } = req.body;
const host = req.headers.host;
const siteUrl = `${req.protocol}://${host}`;
// validate membership // validate membership
const membershipOrg = await MembershipOrg.findOne({ const membershipOrg = await MembershipOrg.findOne({
@@ -178,9 +180,13 @@ export const inviteUserToOrganization = async (req: Request, res: Response) => {
organizationName: organization.name, organizationName: organization.name,
email: inviteeEmail, email: inviteeEmail,
token, token,
callback_url: SITE_URL + '/signupinvite' callback_url: getSiteURL() + '/signupinvite'
} }
}); });
if (!getSmtpConfigured()) {
completeInviteLink = `${siteUrl + '/signupinvite'}?token=${token}&to=${inviteeEmail}`
}
} }
await updateSubscriptionOrgQuantity({ organizationId }); await updateSubscriptionOrgQuantity({ organizationId });
@@ -193,7 +199,8 @@ export const inviteUserToOrganization = async (req: Request, res: Response) => {
} }
return res.status(200).send({ return res.status(200).send({
message: `Sent an invite link to ${req.body.inviteeEmail}` message: `Sent an invite link to ${req.body.inviteeEmail}`,
completeInviteLink
}); });
}; };
@@ -218,7 +225,7 @@ export const verifyUserToOrganization = async (req: Request, res: Response) => {
if (!membershipOrg) if (!membershipOrg)
throw new Error('Failed to find any invitations for email'); throw new Error('Failed to find any invitations for email');
await TokenService.validateToken({ await TokenService.validateToken({
type: TOKEN_EMAIL_ORG_INVITATION, type: TOKEN_EMAIL_ORG_INVITATION,
email, email,
@@ -250,8 +257,8 @@ export const verifyUserToOrganization = async (req: Request, res: Response) => {
payload: { payload: {
userId: user._id.toString() userId: user._id.toString()
}, },
expiresIn: JWT_SIGNUP_LIFETIME, expiresIn: getJwtSignupLifetime(),
secret: JWT_SIGNUP_SECRET secret: getJwtSignupSecret()
}); });
} catch (err) { } catch (err) {
Sentry.setUser(null); Sentry.setUser(null);
@@ -1,26 +1,18 @@
import { Request, Response } from 'express';
import * as Sentry from '@sentry/node'; import * as Sentry from '@sentry/node';
import { import { Request, Response } from 'express';
SITE_URL,
STRIPE_SECRET_KEY
} from '../../config';
import Stripe from 'stripe'; import Stripe from 'stripe';
const stripe = new Stripe(STRIPE_SECRET_KEY, {
apiVersion: '2022-08-01'
});
import { import {
Membership, Membership,
MembershipOrg, MembershipOrg,
Organization, Organization,
Workspace, Workspace,
IncidentContactOrg, IncidentContactOrg
IMembershipOrg
} from '../../models'; } from '../../models';
import { createOrganization as create } from '../../helpers/organization'; import { createOrganization as create } from '../../helpers/organization';
import { addMembershipsOrg } from '../../helpers/membershipOrg'; import { addMembershipsOrg } from '../../helpers/membershipOrg';
import { OWNER, ACCEPTED } from '../../variables'; import { OWNER, ACCEPTED } from '../../variables';
import _ from 'lodash'; import _ from 'lodash';
import { getStripeSecretKey, getSiteURL } from '../../config';
export const getOrganizations = async (req: Request, res: Response) => { export const getOrganizations = async (req: Request, res: Response) => {
let organizations; let organizations;
@@ -325,6 +317,10 @@ export const createOrganizationPortalSession = async (
) => { ) => {
let session; let session;
try { try {
const stripe = new Stripe(getStripeSecretKey(), {
apiVersion: '2022-08-01'
});
// check if there is a payment method on file // check if there is a payment method on file
const paymentMethods = await stripe.paymentMethods.list({ const paymentMethods = await stripe.paymentMethods.list({
customer: req.membershipOrg.organization.customerId, customer: req.membershipOrg.organization.customerId,
@@ -337,13 +333,13 @@ export const createOrganizationPortalSession = async (
customer: req.membershipOrg.organization.customerId, customer: req.membershipOrg.organization.customerId,
mode: 'setup', mode: 'setup',
payment_method_types: ['card'], payment_method_types: ['card'],
success_url: SITE_URL + '/dashboard', success_url: getSiteURL() + '/dashboard',
cancel_url: SITE_URL + '/dashboard' cancel_url: getSiteURL() + '/dashboard'
}); });
} else { } else {
session = await stripe.billingPortal.sessions.create({ session = await stripe.billingPortal.sessions.create({
customer: req.membershipOrg.organization.customerId, customer: req.membershipOrg.organization.customerId,
return_url: SITE_URL + '/dashboard' return_url: getSiteURL() + '/dashboard'
}); });
} }
@@ -369,6 +365,10 @@ export const getOrganizationSubscriptions = async (
) => { ) => {
let subscriptions; let subscriptions;
try { try {
const stripe = new Stripe(getStripeSecretKey(), {
apiVersion: '2022-08-01'
});
subscriptions = await stripe.subscriptions.list({ subscriptions = await stripe.subscriptions.list({
customer: req.membershipOrg.organization.customerId customer: req.membershipOrg.organization.customerId
}); });
@@ -7,9 +7,9 @@ import { User, BackupPrivateKey, LoginSRPDetail } from '../../models';
import { createToken } from '../../helpers/auth'; import { createToken } from '../../helpers/auth';
import { sendMail } from '../../helpers/nodemailer'; import { sendMail } from '../../helpers/nodemailer';
import { TokenService } from '../../services'; import { TokenService } from '../../services';
import { JWT_SIGNUP_LIFETIME, JWT_SIGNUP_SECRET, SITE_URL } from '../../config';
import { TOKEN_EMAIL_PASSWORD_RESET } from '../../variables'; import { TOKEN_EMAIL_PASSWORD_RESET } from '../../variables';
import { BadRequestError } from '../../utils/errors'; import { BadRequestError } from '../../utils/errors';
import { getSiteURL, getJwtSignupLifetime, getJwtSignupSecret } from '../../config';
/** /**
* Password reset step 1: Send email verification link to email [email] * Password reset step 1: Send email verification link to email [email]
@@ -44,7 +44,7 @@ export const emailPasswordReset = async (req: Request, res: Response) => {
substitutions: { substitutions: {
email, email,
token, token,
callback_url: SITE_URL + '/password-reset' callback_url: getSiteURL() + '/password-reset'
} }
}); });
} catch (err) { } catch (err) {
@@ -91,8 +91,8 @@ export const emailPasswordResetVerify = async (req: Request, res: Response) => {
payload: { payload: {
userId: user._id.toString() userId: user._id.toString()
}, },
expiresIn: JWT_SIGNUP_LIFETIME, expiresIn: getJwtSignupLifetime(),
secret: JWT_SIGNUP_SECRET secret: getJwtSignupSecret()
}); });
} catch (err) { } catch (err) {
Sentry.setUser(null); Sentry.setUser(null);
@@ -9,7 +9,7 @@ import {
import { pushKeys } from '../../helpers/key'; import { pushKeys } from '../../helpers/key';
import { eventPushSecrets } from '../../events'; import { eventPushSecrets } from '../../events';
import { EventService } from '../../services'; import { EventService } from '../../services';
import { postHogClient } from '../../services'; import { getPostHogClient } from '../../services';
interface PushSecret { interface PushSecret {
ciphertextKey: string; ciphertextKey: string;
@@ -38,6 +38,7 @@ export const pushSecrets = async (req: Request, res: Response) => {
// upload (encrypted) secrets to workspace with id [workspaceId] // upload (encrypted) secrets to workspace with id [workspaceId]
try { try {
const postHogClient = getPostHogClient();
let { secrets }: { secrets: PushSecret[] } = req.body; let { secrets }: { secrets: PushSecret[] } = req.body;
const { keys, environment, channel } = req.body; const { keys, environment, channel } = req.body;
const { workspaceId } = req.params; const { workspaceId } = req.params;
@@ -111,6 +112,7 @@ export const pullSecrets = async (req: Request, res: Response) => {
let secrets; let secrets;
let key; let key;
try { try {
const postHogClient = getPostHogClient();
const environment: string = req.query.environment as string; const environment: string = req.query.environment as string;
const channel: string = req.query.channel as string; const channel: string = req.query.channel as string;
const { workspaceId } = req.params; const { workspaceId } = req.params;
@@ -179,6 +181,7 @@ export const pullSecretsServiceToken = async (req: Request, res: Response) => {
let secrets; let secrets;
let key; let key;
try { try {
const postHogClient = getPostHogClient();
const environment: string = req.query.environment as string; const environment: string = req.query.environment as string;
const channel: string = req.query.channel as string; const channel: string = req.query.channel as string;
const { workspaceId } = req.params; const { workspaceId } = req.params;
@@ -1,7 +1,7 @@
import { Request, Response } from 'express'; import { Request, Response } from 'express';
import { ServiceToken } from '../../models'; import { ServiceToken } from '../../models';
import { createToken } from '../../helpers/auth'; import { createToken } from '../../helpers/auth';
import { JWT_SERVICE_SECRET } from '../../config'; import { getJwtServiceSecret } from '../../config';
/** /**
* Return service token on request * Return service token on request
@@ -61,7 +61,7 @@ export const createServiceToken = async (req: Request, res: Response) => {
workspaceId workspaceId
}, },
expiresIn: expiresIn, expiresIn: expiresIn,
secret: JWT_SERVICE_SECRET secret: getJwtServiceSecret()
}); });
} catch (err) { } catch (err) {
return res.status(400).send({ return res.status(400).send({
+11 -9
View File
@@ -1,13 +1,13 @@
import { Request, Response } from 'express'; import { Request, Response } from 'express';
import * as Sentry from '@sentry/node'; import * as Sentry from '@sentry/node';
import { User } from '../../models'; import { User } from '../../models';
import { JWT_SIGNUP_LIFETIME, JWT_SIGNUP_SECRET, INVITE_ONLY_SIGNUP } from '../../config';
import { import {
sendEmailVerification, sendEmailVerification,
checkEmailVerification, checkEmailVerification,
} from '../../helpers/signup'; } from '../../helpers/signup';
import { createToken } from '../../helpers/auth'; import { createToken } from '../../helpers/auth';
import { BadRequestError } from '../../utils/errors'; import { BadRequestError } from '../../utils/errors';
import { getInviteOnlySignup, getJwtSignupLifetime, getJwtSignupSecret, getSmtpConfigured } from '../../config';
/** /**
* Signup step 1: Initialize account for user under email [email] and send a verification code * Signup step 1: Initialize account for user under email [email] and send a verification code
@@ -21,7 +21,7 @@ export const beginEmailSignup = async (req: Request, res: Response) => {
try { try {
email = req.body.email; email = req.body.email;
if (INVITE_ONLY_SIGNUP) { if (getInviteOnlySignup()) {
// Only one user can create an account without being invited. The rest need to be invited in order to make an account // Only one user can create an account without being invited. The rest need to be invited in order to make an account
const userCount = await User.countDocuments({}) const userCount = await User.countDocuments({})
if (userCount != 0) { if (userCount != 0) {
@@ -66,7 +66,7 @@ export const verifyEmailSignup = async (req: Request, res: Response) => {
const { email, code } = req.body; const { email, code } = req.body;
// initialize user account // initialize user account
user = await User.findOne({ email }); user = await User.findOne({ email }).select('+publicKey');
if (user && user?.publicKey) { if (user && user?.publicKey) {
// case: user has already completed account // case: user has already completed account
return res.status(403).send({ return res.status(403).send({
@@ -75,10 +75,12 @@ export const verifyEmailSignup = async (req: Request, res: Response) => {
} }
// verify email // verify email
await checkEmailVerification({ if (getSmtpConfigured()) {
email, await checkEmailVerification({
code email,
}); code
});
}
if (!user) { if (!user) {
user = await new User({ user = await new User({
@@ -91,8 +93,8 @@ export const verifyEmailSignup = async (req: Request, res: Response) => {
payload: { payload: {
userId: user._id.toString() userId: user._id.toString()
}, },
expiresIn: JWT_SIGNUP_LIFETIME, expiresIn: getJwtSignupLifetime(),
secret: JWT_SIGNUP_SECRET secret: getJwtSignupSecret()
}); });
} catch (err) { } catch (err) {
Sentry.setUser(null); Sentry.setUser(null);
@@ -1,10 +1,7 @@
import { Request, Response } from 'express'; import { Request, Response } from 'express';
import * as Sentry from '@sentry/node'; import * as Sentry from '@sentry/node';
import Stripe from 'stripe'; import Stripe from 'stripe';
import { STRIPE_SECRET_KEY, STRIPE_WEBHOOK_SECRET } from '../../config'; import { getStripeSecretKey, getStripeWebhookSecret } from '../../config';
const stripe = new Stripe(STRIPE_SECRET_KEY, {
apiVersion: '2022-08-01'
});
/** /**
* Handle service provisioning/un-provisioning via Stripe * Handle service provisioning/un-provisioning via Stripe
@@ -16,11 +13,15 @@ export const handleWebhook = async (req: Request, res: Response) => {
let event; let event;
try { try {
// check request for valid stripe signature // check request for valid stripe signature
const stripe = new Stripe(getStripeSecretKey(), {
apiVersion: '2022-08-01'
});
const sig = req.headers['stripe-signature'] as string; const sig = req.headers['stripe-signature'] as string;
event = stripe.webhooks.constructEvent( event = stripe.webhooks.constructEvent(
req.body, req.body,
sig, sig,
STRIPE_WEBHOOK_SECRET // ? getStripeWebhookSecret()
); );
} catch (err) { } catch (err) {
Sentry.setUser({ email: req.user.email }); Sentry.setUser({ email: req.user.email });
@@ -1,13 +1,11 @@
import { Request, Response } from 'express';
import * as Sentry from '@sentry/node'; import * as Sentry from '@sentry/node';
import { Request, Response } from 'express';
import crypto from 'crypto'; import crypto from 'crypto';
import bcrypt from 'bcrypt'; import bcrypt from 'bcrypt';
import { import {
APIKeyData APIKeyData
} from '../../models'; } from '../../models';
import { import { getSaltRounds } from '../../config';
SALT_ROUNDS
} from '../../config';
/** /**
* Return API key data for user with id [req.user_id] * Return API key data for user with id [req.user_id]
@@ -45,7 +43,7 @@ export const createAPIKeyData = async (req: Request, res: Response) => {
const { name, expiresIn } = req.body; const { name, expiresIn } = req.body;
const secret = crypto.randomBytes(16).toString('hex'); const secret = crypto.randomBytes(16).toString('hex');
const secretHash = await bcrypt.hash(secret, SALT_ROUNDS); const secretHash = await bcrypt.hash(secret, getSaltRounds());
const expiresAt = new Date(); const expiresAt = new Date();
expiresAt.setSeconds(expiresAt.getSeconds() + expiresIn); expiresAt.setSeconds(expiresAt.getSeconds() + expiresIn);
+94 -85
View File
@@ -10,17 +10,17 @@ import { checkUserDevice } from '../../helpers/user';
import { sendMail } from '../../helpers/nodemailer'; import { sendMail } from '../../helpers/nodemailer';
import { TokenService } from '../../services'; import { TokenService } from '../../services';
import { EELogService } from '../../ee/services'; import { EELogService } from '../../ee/services';
import {
NODE_ENV,
JWT_MFA_LIFETIME,
JWT_MFA_SECRET
} from '../../config';
import { BadRequestError, InternalServerError } from '../../utils/errors'; import { BadRequestError, InternalServerError } from '../../utils/errors';
import { import {
TOKEN_EMAIL_MFA, TOKEN_EMAIL_MFA,
ACTION_LOGIN ACTION_LOGIN
} from '../../variables'; } from '../../variables';
import { getChannelFromUserAgent } from '../../utils/posthog'; // TODO: move this import { getChannelFromUserAgent } from '../../utils/posthog'; // TODO: move this
import {
getNodeEnv,
getJwtMfaLifetime,
getJwtMfaSecret
} from '../../config';
declare module 'jsonwebtoken' { declare module 'jsonwebtoken' {
export interface UserIDJwtPayload extends jwt.JwtPayload { export interface UserIDJwtPayload extends jwt.JwtPayload {
@@ -28,8 +28,6 @@ declare module 'jsonwebtoken' {
} }
} }
const clientPublicKeys: any = {};
/** /**
* Log in user step 1: Return [salt] and [serverPublicKey] as part of step 1 of SRP protocol * Log in user step 1: Return [salt] and [serverPublicKey] as part of step 1 of SRP protocol
* @param req * @param req
@@ -89,7 +87,7 @@ export const login1 = async (req: Request, res: Response) => {
*/ */
export const login2 = async (req: Request, res: Response) => { export const login2 = async (req: Request, res: Response) => {
try { try {
if (!req.headers['user-agent']) throw InternalServerError({ message: 'User-Agent header is required' }); if (!req.headers['user-agent']) throw InternalServerError({ message: 'User-Agent header is required' });
const { email, clientProof } = req.body; const { email, clientProof } = req.body;
@@ -126,15 +124,15 @@ export const login2 = async (req: Request, res: Response) => {
payload: { payload: {
userId: user._id.toString() userId: user._id.toString()
}, },
expiresIn: JWT_MFA_LIFETIME, expiresIn: getJwtMfaLifetime(),
secret: JWT_MFA_SECRET secret: getJwtMfaSecret()
}); });
const code = await TokenService.createToken({ const code = await TokenService.createToken({
type: TOKEN_EMAIL_MFA, type: TOKEN_EMAIL_MFA,
email email
}); });
// send MFA code [code] to [email] // send MFA code [code] to [email]
await sendMail({ await sendMail({
template: 'emailMfa.handlebars', template: 'emailMfa.handlebars',
@@ -144,13 +142,13 @@ export const login2 = async (req: Request, res: Response) => {
code code
} }
}); });
return res.status(200).send({ return res.status(200).send({
mfaEnabled: true, mfaEnabled: true,
token token
}); });
} }
await checkUserDevice({ await checkUserDevice({
user, user,
ip: req.ip, ip: req.ip,
@@ -165,7 +163,7 @@ export const login2 = async (req: Request, res: Response) => {
httpOnly: true, httpOnly: true,
path: '/', path: '/',
sameSite: 'strict', sameSite: 'strict',
secure: NODE_ENV === 'production' ? true : false secure: getNodeEnv() === 'production' ? true : false
}); });
// case: user does not have MFA enablgged // case: user does not have MFA enablgged
@@ -183,7 +181,7 @@ export const login2 = async (req: Request, res: Response) => {
iv?: string; iv?: string;
tag?: string; tag?: string;
} }
const response: ResponseData = { const response: ResponseData = {
mfaEnabled: false, mfaEnabled: false,
encryptionVersion: user.encryptionVersion, encryptionVersion: user.encryptionVersion,
@@ -193,7 +191,7 @@ export const login2 = async (req: Request, res: Response) => {
iv: user.iv, iv: user.iv,
tag: user.tag tag: user.tag
} }
if ( if (
user?.protectedKey && user?.protectedKey &&
user?.protectedKeyIV && user?.protectedKeyIV &&
@@ -208,14 +206,14 @@ export const login2 = async (req: Request, res: Response) => {
name: ACTION_LOGIN, name: ACTION_LOGIN,
userId: user._id userId: user._id
}); });
loginAction && await EELogService.createLog({ loginAction && await EELogService.createLog({
userId: user._id, userId: user._id,
actions: [loginAction], actions: [loginAction],
channel: getChannelFromUserAgent(req.headers['user-agent']), channel: getChannelFromUserAgent(req.headers['user-agent']),
ipAddress: req.ip ipAddress: req.ip
}); });
return res.status(200).send(response); return res.status(200).send(response);
} }
@@ -246,7 +244,7 @@ export const sendMfaToken = async (req: Request, res: Response) => {
type: TOKEN_EMAIL_MFA, type: TOKEN_EMAIL_MFA,
email email
}); });
// send MFA code [code] to [email] // send MFA code [code] to [email]
await sendMail({ await sendMail({
template: 'emailMfa.handlebars', template: 'emailMfa.handlebars',
@@ -261,9 +259,9 @@ export const sendMfaToken = async (req: Request, res: Response) => {
Sentry.captureException(err); Sentry.captureException(err);
return res.status(400).send({ return res.status(400).send({
message: 'Failed to send MFA code' message: 'Failed to send MFA code'
}); });
} }
return res.status(200).send({ return res.status(200).send({
message: 'Successfully sent new MFA code' message: 'Successfully sent new MFA code'
}); });
@@ -276,76 +274,87 @@ export const sendMfaToken = async (req: Request, res: Response) => {
* @param res * @param res
*/ */
export const verifyMfaToken = async (req: Request, res: Response) => { export const verifyMfaToken = async (req: Request, res: Response) => {
const { email, mfaToken } = req.body; const { email, mfaToken } = req.body;
await TokenService.validateToken({ await TokenService.validateToken({
type: TOKEN_EMAIL_MFA, type: TOKEN_EMAIL_MFA,
email, email,
token: mfaToken token: mfaToken
}); });
const user = await User.findOne({ const user = await User.findOne({
email email
}).select('+salt +verifier +encryptionVersion +protectedKey +protectedKeyIV +protectedKeyTag +publicKey +encryptedPrivateKey +iv +tag'); }).select('+salt +verifier +encryptionVersion +protectedKey +protectedKeyIV +protectedKeyTag +publicKey +encryptedPrivateKey +iv +tag');
if (!user) throw new Error('Failed to find user'); if (!user) throw new Error('Failed to find user');
await checkUserDevice({ await checkUserDevice({
user, user,
ip: req.ip, ip: req.ip,
userAgent: req.headers['user-agent'] ?? '' userAgent: req.headers['user-agent'] ?? ''
}); });
// issue tokens // issue tokens
const tokens = await issueAuthTokens({ userId: user._id.toString() }); const tokens = await issueAuthTokens({ userId: user._id.toString() });
// store (refresh) token in httpOnly cookie // store (refresh) token in httpOnly cookie
res.cookie('jid', tokens.refreshToken, { res.cookie('jid', tokens.refreshToken, {
httpOnly: true, httpOnly: true,
path: '/', path: '/',
sameSite: 'strict', sameSite: 'strict',
secure: NODE_ENV === 'production' ? true : false secure: getNodeEnv() === 'production' ? true : false
}); });
interface VerifyMfaTokenRes {
encryptionVersion: number;
protectedKey?: string;
protectedKeyIV?: string;
protectedKeyTag?: string;
token: string;
publicKey: string;
encryptedPrivateKey: string;
iv: string;
tag: string;
}
const resObj: VerifyMfaTokenRes = { interface VerifyMfaTokenRes {
encryptionVersion: user.encryptionVersion, encryptionVersion: number;
token: tokens.token, protectedKey?: string;
publicKey: user.publicKey as string, protectedKeyIV?: string;
encryptedPrivateKey: user.encryptedPrivateKey as string, protectedKeyTag?: string;
iv: user.iv as string, token: string;
tag: user.tag as string publicKey: string;
} encryptedPrivateKey: string;
iv: string;
if (user?.protectedKey && user?.protectedKeyIV && user?.protectedKeyTag) { tag: string;
resObj.protectedKey = user.protectedKey; }
resObj.protectedKeyIV = user.protectedKeyIV;
resObj.protectedKeyTag = user.protectedKeyTag;
}
const loginAction = await EELogService.createAction({ interface VerifyMfaTokenRes {
name: ACTION_LOGIN, encryptionVersion: number;
userId: user._id protectedKey?: string;
}); protectedKeyIV?: string;
protectedKeyTag?: string;
loginAction && await EELogService.createLog({ token: string;
userId: user._id, publicKey: string;
actions: [loginAction], encryptedPrivateKey: string;
channel: getChannelFromUserAgent(req.headers['user-agent']), iv: string;
ipAddress: req.ip tag: string;
}); }
return res.status(200).send(resObj); const resObj: VerifyMfaTokenRes = {
encryptionVersion: user.encryptionVersion,
token: tokens.token,
publicKey: user.publicKey as string,
encryptedPrivateKey: user.encryptedPrivateKey as string,
iv: user.iv as string,
tag: user.tag as string
}
if (user?.protectedKey && user?.protectedKeyIV && user?.protectedKeyTag) {
resObj.protectedKey = user.protectedKey;
resObj.protectedKeyIV = user.protectedKeyIV;
resObj.protectedKeyTag = user.protectedKeyTag;
}
const loginAction = await EELogService.createAction({
name: ACTION_LOGIN,
userId: user._id
});
loginAction && await EELogService.createLog({
userId: user._id,
actions: [loginAction],
channel: getChannelFromUserAgent(req.headers['user-agent']),
ipAddress: req.ip
});
return res.status(200).send(resObj);
} }
@@ -7,7 +7,7 @@ const { ValidationError } = mongoose.Error;
import { BadRequestError, InternalServerError, UnauthorizedRequestError, ValidationError as RouteValidationError } from '../../utils/errors'; import { BadRequestError, InternalServerError, UnauthorizedRequestError, ValidationError as RouteValidationError } from '../../utils/errors';
import { AnyBulkWriteOperation } from 'mongodb'; import { AnyBulkWriteOperation } from 'mongodb';
import { SECRET_PERSONAL, SECRET_SHARED } from "../../variables"; import { SECRET_PERSONAL, SECRET_SHARED } from "../../variables";
import { postHogClient } from '../../services'; import { getPostHogClient } from '../../services';
/** /**
* Create secret for workspace with id [workspaceId] and environment [environment] * Create secret for workspace with id [workspaceId] and environment [environment]
@@ -15,6 +15,7 @@ import { postHogClient } from '../../services';
* @param res * @param res
*/ */
export const createSecret = async (req: Request, res: Response) => { export const createSecret = async (req: Request, res: Response) => {
const postHogClient = getPostHogClient();
const secretToCreate: CreateSecretRequestBody = req.body.secret; const secretToCreate: CreateSecretRequestBody = req.body.secret;
const { workspaceId, environment } = req.params const { workspaceId, environment } = req.params
const sanitizedSecret: SanitizedSecretForCreate = { const sanitizedSecret: SanitizedSecretForCreate = {
@@ -67,6 +68,7 @@ export const createSecret = async (req: Request, res: Response) => {
* @param res * @param res
*/ */
export const createSecrets = async (req: Request, res: Response) => { export const createSecrets = async (req: Request, res: Response) => {
const postHogClient = getPostHogClient();
const secretsToCreate: CreateSecretRequestBody[] = req.body.secrets; const secretsToCreate: CreateSecretRequestBody[] = req.body.secrets;
const { workspaceId, environment } = req.params const { workspaceId, environment } = req.params
const sanitizedSecretesToCreate: SanitizedSecretForCreate[] = [] const sanitizedSecretesToCreate: SanitizedSecretForCreate[] = []
@@ -128,6 +130,7 @@ export const createSecrets = async (req: Request, res: Response) => {
* @param res * @param res
*/ */
export const deleteSecrets = async (req: Request, res: Response) => { export const deleteSecrets = async (req: Request, res: Response) => {
const postHogClient = getPostHogClient();
const { workspaceId, environmentName } = req.params const { workspaceId, environmentName } = req.params
const secretIdsToDelete: string[] = req.body.secretIds const secretIdsToDelete: string[] = req.body.secretIds
@@ -181,6 +184,7 @@ export const deleteSecrets = async (req: Request, res: Response) => {
* @param res * @param res
*/ */
export const deleteSecret = async (req: Request, res: Response) => { export const deleteSecret = async (req: Request, res: Response) => {
const postHogClient = getPostHogClient();
await Secret.findByIdAndDelete(req._secret._id) await Secret.findByIdAndDelete(req._secret._id)
if (postHogClient) { if (postHogClient) {
@@ -209,6 +213,7 @@ export const deleteSecret = async (req: Request, res: Response) => {
* @returns * @returns
*/ */
export const updateSecrets = async (req: Request, res: Response) => { export const updateSecrets = async (req: Request, res: Response) => {
const postHogClient = getPostHogClient();
const { workspaceId, environmentName } = req.params const { workspaceId, environmentName } = req.params
const secretsModificationsRequested: ModifySecretRequestBody[] = req.body.secrets; const secretsModificationsRequested: ModifySecretRequestBody[] = req.body.secrets;
const [secretIdsUserCanModifyError, secretIdsUserCanModify] = await to(Secret.find({ workspace: workspaceId, environment: environmentName }, { _id: 1 }).then()) const [secretIdsUserCanModifyError, secretIdsUserCanModify] = await to(Secret.find({ workspace: workspaceId, environment: environmentName }, { _id: 1 }).then())
@@ -276,6 +281,7 @@ export const updateSecrets = async (req: Request, res: Response) => {
* @returns * @returns
*/ */
export const updateSecret = async (req: Request, res: Response) => { export const updateSecret = async (req: Request, res: Response) => {
const postHogClient = getPostHogClient();
const { workspaceId, environmentName } = req.params const { workspaceId, environmentName } = req.params
const secretModificationsRequested: ModifySecretRequestBody = req.body.secret; const secretModificationsRequested: ModifySecretRequestBody = req.body.secret;
@@ -329,6 +335,7 @@ export const updateSecret = async (req: Request, res: Response) => {
* @returns * @returns
*/ */
export const getSecrets = async (req: Request, res: Response) => { export const getSecrets = async (req: Request, res: Response) => {
const postHogClient = getPostHogClient();
const { environment } = req.query; const { environment } = req.query;
const { workspaceId } = req.params; const { workspaceId } = req.params;
@@ -15,7 +15,7 @@ import { UnauthorizedRequestError, ValidationError } from '../../utils/errors';
import { EventService } from '../../services'; import { EventService } from '../../services';
import { eventPushSecrets } from '../../events'; import { eventPushSecrets } from '../../events';
import { EESecretService, EELogService } from '../../ee/services'; import { EESecretService, EELogService } from '../../ee/services';
import { postHogClient } from '../../services'; import { getPostHogClient } from '../../services';
import { getChannelFromUserAgent } from '../../utils/posthog'; import { getChannelFromUserAgent } from '../../utils/posthog';
import { ABILITY_READ, ABILITY_WRITE } from '../../variables/organization'; import { ABILITY_READ, ABILITY_WRITE } from '../../variables/organization';
import { userHasNoAbility, userHasWorkspaceAccess, userHasWriteOnlyAbility } from '../../ee/helpers/checkMembershipPermissions'; import { userHasNoAbility, userHasWorkspaceAccess, userHasWriteOnlyAbility } from '../../ee/helpers/checkMembershipPermissions';
@@ -33,6 +33,8 @@ import {
*/ */
export const batchSecrets = async (req: Request, res: Response) => { export const batchSecrets = async (req: Request, res: Response) => {
const channel = getChannelFromUserAgent(req.headers['user-agent']); const channel = getChannelFromUserAgent(req.headers['user-agent']);
const postHogClient = getPostHogClient();
const { const {
workspaceId, workspaceId,
environment, environment,
@@ -326,6 +328,7 @@ export const createSecrets = async (req: Request, res: Response) => {
} }
} }
*/ */
const postHogClient = getPostHogClient();
const channel = getChannelFromUserAgent(req.headers['user-agent']) const channel = getChannelFromUserAgent(req.headers['user-agent'])
const { workspaceId, environment }: { workspaceId: string, environment: string } = req.body; const { workspaceId, environment }: { workspaceId: string, environment: string } = req.body;
@@ -530,6 +533,7 @@ export const getSecrets = async (req: Request, res: Response) => {
} }
*/ */
const postHogClient = getPostHogClient();
const { workspaceId, environment, tagSlugs } = req.query; const { workspaceId, environment, tagSlugs } = req.query;
const tagNamesList = typeof tagSlugs === 'string' && tagSlugs !== '' ? tagSlugs.split(',') : []; const tagNamesList = typeof tagSlugs === 'string' && tagSlugs !== '' ? tagSlugs.split(',') : [];
@@ -732,6 +736,7 @@ export const updateSecrets = async (req: Request, res: Response) => {
} }
} }
*/ */
const postHogClient = getPostHogClient();
const channel = req.headers?.['user-agent']?.toLowerCase().includes('mozilla') ? 'web' : 'cli'; const channel = req.headers?.['user-agent']?.toLowerCase().includes('mozilla') ? 'web' : 'cli';
// TODO: move type // TODO: move type
@@ -953,7 +958,7 @@ export const deleteSecrets = async (req: Request, res: Response) => {
} }
} }
*/ */
const postHogClient = getPostHogClient();
const channel = getChannelFromUserAgent(req.headers['user-agent']) const channel = getChannelFromUserAgent(req.headers['user-agent'])
const toDelete = req.secrets.map((s: any) => s._id); const toDelete = req.secrets.map((s: any) => s._id);
@@ -1,15 +1,13 @@
import { Request, Response } from 'express';
import * as Sentry from '@sentry/node'; import * as Sentry from '@sentry/node';
import { Request, Response } from 'express';
import crypto from 'crypto'; import crypto from 'crypto';
import bcrypt from 'bcrypt'; import bcrypt from 'bcrypt';
import { import {
ServiceTokenData ServiceTokenData
} from '../../models'; } from '../../models';
import {
SALT_ROUNDS
} from '../../config';
import { userHasWorkspaceAccess } from '../../ee/helpers/checkMembershipPermissions'; import { userHasWorkspaceAccess } from '../../ee/helpers/checkMembershipPermissions';
import { ABILITY_READ } from '../../variables/organization'; import { ABILITY_READ } from '../../variables/organization';
import { getSaltRounds } from '../../config';
/** /**
* Return service token data associated with service token on request * Return service token data associated with service token on request
@@ -75,7 +73,7 @@ export const createServiceTokenData = async (req: Request, res: Response) => {
} }
const secret = crypto.randomBytes(16).toString('hex'); const secret = crypto.randomBytes(16).toString('hex');
const secretHash = await bcrypt.hash(secret, SALT_ROUNDS); const secretHash = await bcrypt.hash(secret, getSaltRounds());
const expiresAt = new Date(); const expiresAt = new Date();
expiresAt.setSeconds(expiresAt.getSeconds() + expiresIn); expiresAt.setSeconds(expiresAt.getSeconds() + expiresIn);
@@ -142,4 +140,4 @@ export const deleteServiceTokenData = async (req: Request, res: Response) => {
function UnauthorizedRequestError(arg0: { message: string; }) { function UnauthorizedRequestError(arg0: { message: string; }) {
throw new Error('Function not implemented.'); throw new Error('Function not implemented.');
} }
@@ -7,8 +7,8 @@ import {
} from '../../helpers/signup'; } from '../../helpers/signup';
import { issueAuthTokens } from '../../helpers/auth'; import { issueAuthTokens } from '../../helpers/auth';
import { INVITED, ACCEPTED } from '../../variables'; import { INVITED, ACCEPTED } from '../../variables';
import { NODE_ENV } from '../../config';
import request from '../../config/request'; import request from '../../config/request';
import { getNodeEnv, getLoopsApiKey } from '../../config';
/** /**
* Complete setting up user by adding their personal and auth information as part of the * Complete setting up user by adding their personal and auth information as part of the
@@ -108,7 +108,7 @@ export const completeAccountSignup = async (req: Request, res: Response) => {
token = tokens.token; token = tokens.token;
// sending a welcome email to new users // sending a welcome email to new users
if (process.env.LOOPS_API_KEY) { if (getLoopsApiKey()) {
await request.post("https://app.loops.so/api/v1/events/send", { await request.post("https://app.loops.so/api/v1/events/send", {
"email": email, "email": email,
"eventName": "Sign Up", "eventName": "Sign Up",
@@ -117,7 +117,7 @@ export const completeAccountSignup = async (req: Request, res: Response) => {
}, { }, {
headers: { headers: {
"Accept": "application/json", "Accept": "application/json",
"Authorization": "Bearer " + process.env.LOOPS_API_KEY "Authorization": "Bearer " + getLoopsApiKey()
}, },
}); });
} }
@@ -127,7 +127,7 @@ export const completeAccountSignup = async (req: Request, res: Response) => {
httpOnly: true, httpOnly: true,
path: '/', path: '/',
sameSite: 'strict', sameSite: 'strict',
secure: NODE_ENV === 'production' ? true : false secure: getNodeEnv() === 'production' ? true : false
}); });
} catch (err) { } catch (err) {
Sentry.setUser(null); Sentry.setUser(null);
@@ -232,7 +232,7 @@ export const completeAccountInvite = async (req: Request, res: Response) => {
httpOnly: true, httpOnly: true,
path: '/', path: '/',
sameSite: 'strict', sameSite: 'strict',
secure: NODE_ENV === 'production' ? true : false secure: getNodeEnv() === 'production' ? true : false
}); });
} catch (err) { } catch (err) {
Sentry.setUser(null); Sentry.setUser(null);
@@ -19,7 +19,7 @@ import {
reformatPullSecrets reformatPullSecrets
} from '../../helpers/secret'; } from '../../helpers/secret';
import { pushKeys } from '../../helpers/key'; import { pushKeys } from '../../helpers/key';
import { postHogClient, EventService } from '../../services'; import { getPostHogClient, EventService } from '../../services';
import { eventPushSecrets } from '../../events'; import { eventPushSecrets } from '../../events';
interface V2PushSecret { interface V2PushSecret {
@@ -48,6 +48,7 @@ interface V2PushSecret {
export const pushWorkspaceSecrets = async (req: Request, res: Response) => { export const pushWorkspaceSecrets = async (req: Request, res: Response) => {
// upload (encrypted) secrets to workspace with id [workspaceId] // upload (encrypted) secrets to workspace with id [workspaceId]
try { try {
const postHogClient = getPostHogClient();
let { secrets }: { secrets: V2PushSecret[] } = req.body; let { secrets }: { secrets: V2PushSecret[] } = req.body;
const { keys, environment, channel } = req.body; const { keys, environment, channel } = req.body;
const { workspaceId } = req.params; const { workspaceId } = req.params;
@@ -121,6 +122,7 @@ export const pushWorkspaceSecrets = async (req: Request, res: Response) => {
export const pullSecrets = async (req: Request, res: Response) => { export const pullSecrets = async (req: Request, res: Response) => {
let secrets; let secrets;
try { try {
const postHogClient = getPostHogClient();
const environment: string = req.query.environment as string; const environment: string = req.query.environment as string;
const channel: string = req.query.channel as string; const channel: string = req.query.channel as string;
const { workspaceId } = req.params; const { workspaceId } = req.params;
@@ -1,10 +1,7 @@
import { Request, Response } from 'express';
import * as Sentry from '@sentry/node'; import * as Sentry from '@sentry/node';
import { Request, Response } from 'express';
import Stripe from 'stripe'; import Stripe from 'stripe';
import { STRIPE_SECRET_KEY, STRIPE_WEBHOOK_SECRET } from '../../../config'; import { getStripeSecretKey, getStripeWebhookSecret } from '../../../config';
const stripe = new Stripe(STRIPE_SECRET_KEY, {
apiVersion: '2022-08-01'
});
/** /**
* Handle service provisioning/un-provisioning via Stripe * Handle service provisioning/un-provisioning via Stripe
@@ -15,12 +12,16 @@ const stripe = new Stripe(STRIPE_SECRET_KEY, {
export const handleWebhook = async (req: Request, res: Response) => { export const handleWebhook = async (req: Request, res: Response) => {
let event; let event;
try { try {
const stripe = new Stripe(getStripeSecretKey(), {
apiVersion: '2022-08-01'
});
// check request for valid stripe signature // check request for valid stripe signature
const sig = req.headers['stripe-signature'] as string; const sig = req.headers['stripe-signature'] as string;
event = stripe.webhooks.constructEvent( event = stripe.webhooks.constructEvent(
req.body, req.body,
sig, sig,
STRIPE_WEBHOOK_SECRET // ? getStripeWebhookSecret()
); );
} catch (err) { } catch (err) {
Sentry.setUser({ email: req.user.email }); Sentry.setUser({ email: req.user.email });
+1 -3
View File
@@ -1,5 +1,3 @@
import { LICENSE_KEY } from '../../config';
/** /**
* Class to handle Enterprise Edition license actions * Class to handle Enterprise Edition license actions
*/ */
@@ -16,4 +14,4 @@ class EELicenseService {
} }
} }
export default new EELicenseService(LICENSE_KEY); export default new EELicenseService('N/A');
+12 -12
View File
@@ -1,5 +1,5 @@
import jwt from 'jsonwebtoken';
import * as Sentry from '@sentry/node'; import * as Sentry from '@sentry/node';
import jwt from 'jsonwebtoken';
import bcrypt from 'bcrypt'; import bcrypt from 'bcrypt';
import { import {
IUser, IUser,
@@ -7,12 +7,6 @@ import {
ServiceTokenData, ServiceTokenData,
APIKeyData APIKeyData
} from '../models'; } from '../models';
import {
JWT_AUTH_LIFETIME,
JWT_AUTH_SECRET,
JWT_REFRESH_LIFETIME,
JWT_REFRESH_SECRET
} from '../config';
import { import {
AccountNotFoundError, AccountNotFoundError,
ServiceTokenDataNotFoundError, ServiceTokenDataNotFoundError,
@@ -20,6 +14,12 @@ import {
UnauthorizedRequestError, UnauthorizedRequestError,
BadRequestError BadRequestError
} from '../utils/errors'; } from '../utils/errors';
import {
getJwtAuthLifetime,
getJwtAuthSecret,
getJwtRefreshLifetime,
getJwtRefreshSecret
} from '../config';
/** /**
* *
@@ -93,7 +93,7 @@ const getAuthUserPayload = async ({
let user; let user;
try { try {
const decodedToken = <jwt.UserIDJwtPayload>( const decodedToken = <jwt.UserIDJwtPayload>(
jwt.verify(authTokenValue, JWT_AUTH_SECRET) jwt.verify(authTokenValue, getJwtAuthSecret())
); );
user = await User.findOne({ user = await User.findOne({
@@ -224,16 +224,16 @@ const issueAuthTokens = async ({ userId }: { userId: string }) => {
payload: { payload: {
userId userId
}, },
expiresIn: JWT_AUTH_LIFETIME, expiresIn: getJwtAuthLifetime(),
secret: JWT_AUTH_SECRET secret: getJwtAuthSecret()
}); });
refreshToken = createToken({ refreshToken = createToken({
payload: { payload: {
userId userId
}, },
expiresIn: JWT_REFRESH_LIFETIME, expiresIn: getJwtRefreshLifetime(),
secret: JWT_REFRESH_SECRET secret: getJwtRefreshSecret()
}); });
} catch (err) { } catch (err) {
Sentry.setUser(null); Sentry.setUser(null);
+3 -3
View File
@@ -12,8 +12,8 @@ import {
decryptSymmetric, decryptSymmetric,
decryptAsymmetric decryptAsymmetric
} from '../utils/crypto'; } from '../utils/crypto';
import { ENCRYPTION_KEY } from '../config';
import { SECRET_SHARED } from '../variables'; import { SECRET_SHARED } from '../variables';
import { getEncryptionKey } from '../config';
/** /**
* Create an inactive bot with name [name] for workspace with id [workspaceId] * Create an inactive bot with name [name] for workspace with id [workspaceId]
@@ -33,7 +33,7 @@ const createBot = async ({
const { publicKey, privateKey } = generateKeyPair(); const { publicKey, privateKey } = generateKeyPair();
const { ciphertext, iv, tag } = encryptSymmetric({ const { ciphertext, iv, tag } = encryptSymmetric({
plaintext: privateKey, plaintext: privateKey,
key: ENCRYPTION_KEY key: getEncryptionKey()
}); });
bot = await new Bot({ bot = await new Bot({
@@ -130,7 +130,7 @@ const getKey = async ({ workspaceId }: { workspaceId: string }) => {
ciphertext: bot.encryptedPrivateKey, ciphertext: bot.encryptedPrivateKey,
iv: bot.iv, iv: bot.iv,
tag: bot.tag, tag: bot.tag,
key: ENCRYPTION_KEY key: getEncryptionKey()
}); });
key = decryptAsymmetric({ key = decryptAsymmetric({
+18 -1
View File
@@ -29,6 +29,23 @@ const initDatabaseHelper = async ({
return mongoose.connection; return mongoose.connection;
} }
/**
* Close database conection
*/
const closeDatabaseHelper = async () => {
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');
}
})
]);
}
export { export {
initDatabaseHelper initDatabaseHelper,
closeDatabaseHelper
} }
+20 -18
View File
@@ -1,9 +1,9 @@
import * as Sentry from '@sentry/node';
import fs from 'fs'; import fs from 'fs';
import path from 'path'; import path from 'path';
import handlebars from 'handlebars'; import handlebars from 'handlebars';
import nodemailer from 'nodemailer'; import nodemailer from 'nodemailer';
import { SMTP_FROM_NAME, SMTP_FROM_ADDRESS } from '../config'; import { getSmtpFromName, getSmtpFromAddress, getSmtpConfigured } from '../config';
import * as Sentry from '@sentry/node';
let smtpTransporter: nodemailer.Transporter; let smtpTransporter: nodemailer.Transporter;
@@ -25,23 +25,25 @@ const sendMail = async ({
recipients: string[]; recipients: string[];
substitutions: any; substitutions: any;
}) => { }) => {
try { if (getSmtpConfigured()) {
const html = fs.readFileSync( try {
path.resolve(__dirname, '../templates/' + template), const html = fs.readFileSync(
'utf8' path.resolve(__dirname, '../templates/' + template),
); 'utf8'
const temp = handlebars.compile(html); );
const htmlToSend = temp(substitutions); const temp = handlebars.compile(html);
const htmlToSend = temp(substitutions);
await smtpTransporter.sendMail({ await smtpTransporter.sendMail({
from: `"${SMTP_FROM_NAME}" <${SMTP_FROM_ADDRESS}>`, from: `"${getSmtpFromName()}" <${getSmtpFromAddress()}>`,
to: recipients.join(', '), to: recipients.join(', '),
subject: subjectLine, subject: subjectLine,
html: htmlToSend html: htmlToSend
}); });
} catch (err) { } catch (err) {
Sentry.setUser(null); Sentry.setUser(null);
Sentry.captureException(err); Sentry.captureException(err);
}
} }
}; };
+25 -17
View File
@@ -1,23 +1,14 @@
import * as Sentry from '@sentry/node'; import * as Sentry from '@sentry/node';
import Stripe from 'stripe'; import Stripe from 'stripe';
import {
STRIPE_SECRET_KEY,
STRIPE_PRODUCT_STARTER,
STRIPE_PRODUCT_TEAM,
STRIPE_PRODUCT_PRO
} from '../config';
const stripe = new Stripe(STRIPE_SECRET_KEY, {
apiVersion: '2022-08-01'
});
import { Types } from 'mongoose'; import { Types } from 'mongoose';
import { ACCEPTED } from '../variables'; import { ACCEPTED } from '../variables';
import { Organization, MembershipOrg } from '../models'; import { Organization, MembershipOrg } from '../models';
import {
const productToPriceMap = { getStripeSecretKey,
starter: STRIPE_PRODUCT_STARTER, getStripeProductPro,
team: STRIPE_PRODUCT_TEAM, getStripeProductTeam,
pro: STRIPE_PRODUCT_PRO getStripeProductStarter
}; } from '../config';
/** /**
* Create an organization with name [name] * Create an organization with name [name]
@@ -36,8 +27,11 @@ const createOrganization = async ({
let organization; let organization;
try { try {
// register stripe account // register stripe account
const stripe = new Stripe(getStripeSecretKey(), {
apiVersion: '2022-08-01'
});
if (STRIPE_SECRET_KEY) { if (getStripeSecretKey()) {
const customer = await stripe.customers.create({ const customer = await stripe.customers.create({
email, email,
description: name description: name
@@ -87,6 +81,16 @@ const initSubscriptionOrg = async ({
if (organization) { if (organization) {
if (organization.customerId) { if (organization.customerId) {
// initialize starter subscription with quantity of 0 // initialize starter subscription with quantity of 0
const stripe = new Stripe(getStripeSecretKey(), {
apiVersion: '2022-08-01'
});
const productToPriceMap = {
starter: getStripeProductStarter(),
team: getStripeProductTeam(),
pro: getStripeProductPro()
};
stripeSubscription = await stripe.subscriptions.create({ stripeSubscription = await stripe.subscriptions.create({
customer: organization.customerId, customer: organization.customerId,
items: [ items: [
@@ -139,6 +143,10 @@ const updateSubscriptionOrgQuantity = async ({
status: ACCEPTED status: ACCEPTED
}); });
const stripe = new Stripe(getStripeSecretKey(), {
apiVersion: '2022-08-01'
});
const subscription = ( const subscription = (
await stripe.subscriptions.list({ await stripe.subscriptions.list({
customer: organization.customerId customer: organization.customerId
@@ -167,4 +175,4 @@ export {
createOrganization, createOrganization,
initSubscriptionOrg, initSubscriptionOrg,
updateSubscriptionOrgQuantity updateSubscriptionOrgQuantity
}; };
+2 -4
View File
@@ -9,10 +9,8 @@ import {
TOKEN_EMAIL_ORG_INVITATION, TOKEN_EMAIL_ORG_INVITATION,
TOKEN_EMAIL_PASSWORD_RESET TOKEN_EMAIL_PASSWORD_RESET
} from '../variables'; } from '../variables';
import {
SALT_ROUNDS
} from '../config';
import { UnauthorizedRequestError } from '../utils/errors'; import { UnauthorizedRequestError } from '../utils/errors';
import { getSaltRounds } from '../config';
/** /**
* Create and store a token in the database for purpose [type] * Create and store a token in the database for purpose [type]
@@ -86,7 +84,7 @@ const createTokenHelper = async ({
const query: TokenDataQuery = { type }; const query: TokenDataQuery = { type };
const update: TokenDataUpdate = { const update: TokenDataUpdate = {
type, type,
tokenHash: await bcrypt.hash(token, SALT_ROUNDS), tokenHash: await bcrypt.hash(token, getSaltRounds()),
expiresAt expiresAt
} }
+169 -14
View File
@@ -1,28 +1,183 @@
import mongoose from 'mongoose';
import dotenv from 'dotenv'; import dotenv from 'dotenv';
dotenv.config(); dotenv.config();
import infisical from 'infisical-node';
import express from 'express';
import helmet from 'helmet';
import cors from 'cors';
import * as Sentry from '@sentry/node'; import * as Sentry from '@sentry/node';
import { SENTRY_DSN, NODE_ENV, MONGO_URL } from './config';
import { server } from './app';
import { DatabaseService } from './services'; import { DatabaseService } from './services';
import { setUpHealthEndpoint } from './services/health'; import { setUpHealthEndpoint } from './services/health';
import { initSmtp } from './services/smtp'; import { initSmtp } from './services/smtp';
import { logTelemetryMessage } from './services';
import { setTransporter } from './helpers/nodemailer'; import { setTransporter } from './helpers/nodemailer';
import { createTestUserForDevelopment } from './utils/addDevelopmentUser'; import { createTestUserForDevelopment } from './utils/addDevelopmentUser';
// eslint-disable-next-line @typescript-eslint/no-var-requires
const { patchRouterParam } = require('./utils/patchAsyncRoutes');
DatabaseService.initDatabase(MONGO_URL); import cookieParser from 'cookie-parser';
import swaggerUi = require('swagger-ui-express');
// eslint-disable-next-line @typescript-eslint/no-var-requires
const swaggerFile = require('../spec.json');
// eslint-disable-next-line @typescript-eslint/no-var-requires
const requestIp = require('request-ip');
import { apiLimiter } from './helpers/rateLimiter';
import {
workspace as eeWorkspaceRouter,
secret as eeSecretRouter,
secretSnapshot as eeSecretSnapshotRouter,
action as eeActionRouter
} from './ee/routes/v1';
import {
signup as v1SignupRouter,
auth as v1AuthRouter,
bot as v1BotRouter,
organization as v1OrganizationRouter,
workspace as v1WorkspaceRouter,
membershipOrg as v1MembershipOrgRouter,
membership as v1MembershipRouter,
key as v1KeyRouter,
inviteOrg as v1InviteOrgRouter,
user as v1UserRouter,
userAction as v1UserActionRouter,
secret as v1SecretRouter,
serviceToken as v1ServiceTokenRouter,
password as v1PasswordRouter,
stripe as v1StripeRouter,
integration as v1IntegrationRouter,
integrationAuth as v1IntegrationAuthRouter
} from './routes/v1';
import {
signup as v2SignupRouter,
auth as v2AuthRouter,
users as v2UsersRouter,
organizations as v2OrganizationsRouter,
workspace as v2WorkspaceRouter,
secret as v2SecretRouter, // begin to phase out
secrets as v2SecretsRouter,
serviceTokenData as v2ServiceTokenDataRouter,
apiKeyData as v2APIKeyDataRouter,
environment as v2EnvironmentRouter,
tags as v2TagsRouter,
} from './routes/v2';
import { healthCheck } from './routes/status';
import { getLogger } from './utils/logger';
import { RouteNotFoundError } from './utils/errors';
import { requestErrorHandler } from './middleware/requestErrorHandler';
import {
getMongoURL,
getNodeEnv,
getPort,
getSentryDSN,
getSiteURL
} from './config';
setUpHealthEndpoint(server); const main = async () => {
if (process.env.INFISICAL_TOKEN != "" || process.env.INFISICAL_TOKEN != undefined) {
await infisical.connect({
token: process.env.INFISICAL_TOKEN!
});
}
setTransporter(initSmtp()); logTelemetryMessage();
setTransporter(initSmtp());
if (NODE_ENV !== 'test') { await DatabaseService.initDatabase(getMongoURL());
Sentry.init({ if (getNodeEnv() !== 'test') {
dsn: SENTRY_DSN, Sentry.init({
tracesSampleRate: 1.0, dsn: getSentryDSN(),
debug: NODE_ENV === 'production' ? false : true, tracesSampleRate: 1.0,
environment: NODE_ENV debug: getNodeEnv() === 'production' ? false : true,
}); environment: getNodeEnv()
});
}
patchRouterParam();
const app = express();
app.enable('trust proxy');
app.use(express.json());
app.use(cookieParser());
app.use(
cors({
credentials: true,
origin: getSiteURL()
})
);
app.use(requestIp.mw());
if (getNodeEnv() === 'production') {
// enable app-wide rate-limiting + helmet security
// in production
app.disable('x-powered-by');
app.use(apiLimiter);
app.use(helmet());
}
// (EE) routes
app.use('/api/v1/secret', eeSecretRouter);
app.use('/api/v1/secret-snapshot', eeSecretSnapshotRouter);
app.use('/api/v1/workspace', eeWorkspaceRouter);
app.use('/api/v1/action', eeActionRouter);
// v1 routes
app.use('/api/v1/signup', v1SignupRouter);
app.use('/api/v1/auth', v1AuthRouter);
app.use('/api/v1/bot', v1BotRouter);
app.use('/api/v1/user', v1UserRouter);
app.use('/api/v1/user-action', v1UserActionRouter);
app.use('/api/v1/organization', v1OrganizationRouter);
app.use('/api/v1/workspace', v1WorkspaceRouter);
app.use('/api/v1/membership-org', v1MembershipOrgRouter);
app.use('/api/v1/membership', v1MembershipRouter);
app.use('/api/v1/key', v1KeyRouter);
app.use('/api/v1/invite-org', v1InviteOrgRouter);
app.use('/api/v1/secret', v1SecretRouter);
app.use('/api/v1/service-token', v1ServiceTokenRouter); // deprecated
app.use('/api/v1/password', v1PasswordRouter);
app.use('/api/v1/stripe', v1StripeRouter);
app.use('/api/v1/integration', v1IntegrationRouter);
app.use('/api/v1/integration-auth', v1IntegrationAuthRouter);
// v2 routes
app.use('/api/v2/signup', v2SignupRouter);
app.use('/api/v2/auth', v2AuthRouter);
app.use('/api/v2/users', v2UsersRouter);
app.use('/api/v2/organizations', v2OrganizationsRouter);
app.use('/api/v2/workspace', v2EnvironmentRouter);
app.use('/api/v2/workspace', v2TagsRouter);
app.use('/api/v2/workspace', v2WorkspaceRouter);
app.use('/api/v2/secret', v2SecretRouter); // deprecated
app.use('/api/v2/secrets', v2SecretsRouter);
app.use('/api/v2/service-token', v2ServiceTokenDataRouter); // TODO: turn into plural route
app.use('/api/v2/api-key', v2APIKeyDataRouter);
// api docs
app.use('/api-docs', swaggerUi.serve, swaggerUi.setup(swaggerFile))
// Server status
app.use('/api', healthCheck)
//* Handle unrouted requests and respond with proper error message as well as status code
app.use((req, res, next) => {
if (res.headersSent) return next();
next(RouteNotFoundError({ message: `The requested source '(${req.method})${req.url}' was not found` }))
})
app.use(requestErrorHandler)
const server = app.listen(getPort(), () => {
getLogger("backend-main").info(`Server started listening at port ${getPort()}`)
});
createTestUserForDevelopment();
setUpHealthEndpoint(server);
server.on('close', async () => {
await DatabaseService.closeDatabase();
})
return server;
} }
createTestUserForDevelopment() export default main();
+29 -29
View File
@@ -1,5 +1,5 @@
import request from '../config/request';
import * as Sentry from '@sentry/node'; import * as Sentry from '@sentry/node';
import request from '../config/request';
import { import {
INTEGRATION_AZURE_KEY_VAULT, INTEGRATION_AZURE_KEY_VAULT,
INTEGRATION_HEROKU, INTEGRATION_HEROKU,
@@ -15,18 +15,18 @@ import {
INTEGRATION_GITLAB_TOKEN_URL INTEGRATION_GITLAB_TOKEN_URL
} from '../variables'; } from '../variables';
import { import {
SITE_URL, getSiteURL,
CLIENT_ID_AZURE, getClientIdAzure,
CLIENT_ID_VERCEL, getClientSecretAzure,
CLIENT_ID_NETLIFY, getClientSecretHeroku,
CLIENT_ID_GITHUB, getClientIdVercel,
CLIENT_ID_GITLAB, getClientSecretVercel,
CLIENT_SECRET_AZURE, getClientIdNetlify,
CLIENT_SECRET_HEROKU, getClientSecretNetlify,
CLIENT_SECRET_VERCEL, getClientIdGitHub,
CLIENT_SECRET_NETLIFY, getClientSecretGitHub,
CLIENT_SECRET_GITHUB, getClientIdGitLab,
CLIENT_SECRET_GITLAB, getClientSecretGitLab
} from '../config'; } from '../config';
interface ExchangeCodeAzureResponse { interface ExchangeCodeAzureResponse {
@@ -159,9 +159,9 @@ const exchangeCodeAzure = async ({
grant_type: 'authorization_code', grant_type: 'authorization_code',
code: code, code: code,
scope: 'https://vault.azure.net/.default openid offline_access', scope: 'https://vault.azure.net/.default openid offline_access',
client_id: CLIENT_ID_AZURE, client_id: getClientIdAzure(),
client_secret: CLIENT_SECRET_AZURE, client_secret: getClientSecretAzure(),
redirect_uri: `${SITE_URL}/integrations/azure-key-vault/oauth2/callback` redirect_uri: `${getSiteURL()}/integrations/azure-key-vault/oauth2/callback`
} as any) } as any)
)).data; )).data;
@@ -204,7 +204,7 @@ const exchangeCodeHeroku = async ({
new URLSearchParams({ new URLSearchParams({
grant_type: 'authorization_code', grant_type: 'authorization_code',
code: code, code: code,
client_secret: CLIENT_SECRET_HEROKU client_secret: getClientSecretHeroku()
} as any) } as any)
)).data; )).data;
@@ -242,9 +242,9 @@ const exchangeCodeVercel = async ({ code }: { code: string }) => {
INTEGRATION_VERCEL_TOKEN_URL, INTEGRATION_VERCEL_TOKEN_URL,
new URLSearchParams({ new URLSearchParams({
code: code, code: code,
client_id: CLIENT_ID_VERCEL, client_id: getClientIdVercel(),
client_secret: CLIENT_SECRET_VERCEL, client_secret: getClientSecretVercel(),
redirect_uri: `${SITE_URL}/integrations/vercel/oauth2/callback` redirect_uri: `${getSiteURL()}/integrations/vercel/oauth2/callback`
} as any) } as any)
) )
).data; ).data;
@@ -282,9 +282,9 @@ const exchangeCodeNetlify = async ({ code }: { code: string }) => {
new URLSearchParams({ new URLSearchParams({
grant_type: 'authorization_code', grant_type: 'authorization_code',
code: code, code: code,
client_id: CLIENT_ID_NETLIFY, client_id: getClientIdNetlify(),
client_secret: CLIENT_SECRET_NETLIFY, client_secret: getClientSecretNetlify(),
redirect_uri: `${SITE_URL}/integrations/netlify/oauth2/callback` redirect_uri: `${getSiteURL()}/integrations/netlify/oauth2/callback`
} as any) } as any)
) )
).data; ).data;
@@ -333,10 +333,10 @@ const exchangeCodeGithub = async ({ code }: { code: string }) => {
res = ( res = (
await request.get(INTEGRATION_GITHUB_TOKEN_URL, { await request.get(INTEGRATION_GITHUB_TOKEN_URL, {
params: { params: {
client_id: CLIENT_ID_GITHUB, client_id: getClientIdGitHub(),
client_secret: CLIENT_SECRET_GITHUB, client_secret: getClientSecretGitHub(),
code: code, code: code,
redirect_uri: `${SITE_URL}/integrations/github/oauth2/callback` redirect_uri: `${getSiteURL()}/integrations/github/oauth2/callback`
}, },
headers: { headers: {
'Accept': 'application/json', 'Accept': 'application/json',
@@ -379,9 +379,9 @@ const exchangeCodeGitlab = async ({ code }: { code: string }) => {
new URLSearchParams({ new URLSearchParams({
grant_type: 'authorization_code', grant_type: 'authorization_code',
code: code, code: code,
client_id: CLIENT_ID_GITLAB, client_id: getClientIdGitLab(),
client_secret: CLIENT_SECRET_GITLAB, client_secret: getClientSecretGitLab(),
redirect_uri: `${SITE_URL}/integrations/gitlab/oauth2/callback` redirect_uri: `${getSiteURL()}/integrations/gitlab/oauth2/callback`
} as any), } as any),
{ {
headers: { headers: {
+15 -15
View File
@@ -1,5 +1,5 @@
import request from '../config/request';
import * as Sentry from '@sentry/node'; import * as Sentry from '@sentry/node';
import request from '../config/request';
import { import {
IIntegrationAuth IIntegrationAuth
} from '../models'; } from '../models';
@@ -8,14 +8,6 @@ import {
INTEGRATION_HEROKU, INTEGRATION_HEROKU,
INTEGRATION_GITLAB, INTEGRATION_GITLAB,
} from '../variables'; } from '../variables';
import {
SITE_URL,
CLIENT_ID_AZURE,
CLIENT_ID_GITLAB,
CLIENT_SECRET_AZURE,
CLIENT_SECRET_HEROKU,
CLIENT_SECRET_GITLAB
} from '../config';
import { import {
INTEGRATION_AZURE_TOKEN_URL, INTEGRATION_AZURE_TOKEN_URL,
INTEGRATION_HEROKU_TOKEN_URL, INTEGRATION_HEROKU_TOKEN_URL,
@@ -24,6 +16,14 @@ import {
import { import {
IntegrationService IntegrationService
} from '../services'; } from '../services';
import {
getSiteURL,
getClientIdAzure,
getClientSecretAzure,
getClientSecretHeroku,
getClientIdGitLab,
getClientSecretGitLab
} from '../config';
interface RefreshTokenAzureResponse { interface RefreshTokenAzureResponse {
token_type: string; token_type: string;
@@ -133,11 +133,11 @@ const exchangeRefreshAzure = async ({
const { data }: { data: RefreshTokenAzureResponse } = await request.post( const { data }: { data: RefreshTokenAzureResponse } = await request.post(
INTEGRATION_AZURE_TOKEN_URL, INTEGRATION_AZURE_TOKEN_URL,
new URLSearchParams({ new URLSearchParams({
client_id: CLIENT_ID_AZURE, client_id: getClientIdAzure(),
scope: 'openid offline_access', scope: 'openid offline_access',
refresh_token: refreshToken, refresh_token: refreshToken,
grant_type: 'refresh_token', grant_type: 'refresh_token',
client_secret: CLIENT_SECRET_AZURE client_secret: getClientSecretAzure()
} as any) } as any)
); );
@@ -180,7 +180,7 @@ const exchangeRefreshHeroku = async ({
new URLSearchParams({ new URLSearchParams({
grant_type: 'refresh_token', grant_type: 'refresh_token',
refresh_token: refreshToken, refresh_token: refreshToken,
client_secret: CLIENT_SECRET_HEROKU client_secret: getClientSecretHeroku()
} as any) } as any)
); );
@@ -223,9 +223,9 @@ const exchangeRefreshGitLab = async ({
new URLSearchParams({ new URLSearchParams({
grant_type: 'refresh_token', grant_type: 'refresh_token',
refresh_token: refreshToken, refresh_token: refreshToken,
client_id: CLIENT_ID_GITLAB, client_id: getClientIdGitLab,
client_secret: CLIENT_SECRET_GITLAB, client_secret: getClientSecretGitLab(),
redirect_uri: `${SITE_URL}/integrations/gitlab/oauth2/callback` redirect_uri: `${getSiteURL()}/integrations/gitlab/oauth2/callback`
} as any), } as any),
{ {
headers: { headers: {
@@ -1,16 +1,13 @@
import { ErrorRequestHandler } from "express";
import * as Sentry from '@sentry/node'; import * as Sentry from '@sentry/node';
import { InternalServerError, UnauthorizedRequestError } from "../utils/errors"; import { ErrorRequestHandler } from "express";
import { InternalServerError } from "../utils/errors";
import { getLogger } from "../utils/logger"; import { getLogger } from "../utils/logger";
import RequestError, { LogLevel } from "../utils/requestError"; import RequestError, { LogLevel } from "../utils/requestError";
import { NODE_ENV } from "../config"; import { getNodeEnv } from '../config';
import { TokenExpiredError } from 'jsonwebtoken';
export const requestErrorHandler: ErrorRequestHandler = (error: RequestError | Error, req, res, next) => { export const requestErrorHandler: ErrorRequestHandler = (error: RequestError | Error, req, res, next) => {
if (res.headersSent) return next(); if (res.headersSent) return next();
if (NODE_ENV !== "production") { if (getNodeEnv() !== "production") {
/* eslint-disable no-console */ /* eslint-disable no-console */
console.log(error) console.log(error)
/* eslint-enable no-console */ /* eslint-enable no-console */
+2 -2
View File
@@ -1,8 +1,8 @@
import jwt from 'jsonwebtoken'; import jwt from 'jsonwebtoken';
import { Request, Response, NextFunction } from 'express'; import { Request, Response, NextFunction } from 'express';
import { User } from '../models'; import { User } from '../models';
import { JWT_MFA_SECRET } from '../config';
import { BadRequestError, UnauthorizedRequestError } from '../utils/errors'; import { BadRequestError, UnauthorizedRequestError } from '../utils/errors';
import { getJwtMfaSecret } from '../config';
declare module 'jsonwebtoken' { declare module 'jsonwebtoken' {
export interface UserIDJwtPayload extends jwt.JwtPayload { export interface UserIDJwtPayload extends jwt.JwtPayload {
@@ -26,7 +26,7 @@ const requireMfaAuth = async (
if(AUTH_TOKEN_VALUE === null) return next(BadRequestError({message: 'Missing Authorization Body in the request header'})) if(AUTH_TOKEN_VALUE === null) return next(BadRequestError({message: 'Missing Authorization Body in the request header'}))
const decodedToken = <jwt.UserIDJwtPayload>( const decodedToken = <jwt.UserIDJwtPayload>(
jwt.verify(AUTH_TOKEN_VALUE, JWT_MFA_SECRET) jwt.verify(AUTH_TOKEN_VALUE, getJwtMfaSecret())
); );
const user = await User.findOne({ const user = await User.findOne({
@@ -1,8 +1,8 @@
import jwt from 'jsonwebtoken'; import jwt from 'jsonwebtoken';
import { Request, Response, NextFunction } from 'express'; import { Request, Response, NextFunction } from 'express';
import { ServiceToken } from '../models'; import { ServiceToken } from '../models';
import { JWT_SERVICE_SECRET } from '../config';
import { BadRequestError, UnauthorizedRequestError } from '../utils/errors'; import { BadRequestError, UnauthorizedRequestError } from '../utils/errors';
import { getJwtServiceSecret } from '../config';
// TODO: deprecate // TODO: deprecate
declare module 'jsonwebtoken' { declare module 'jsonwebtoken' {
@@ -33,7 +33,7 @@ const requireServiceTokenAuth = async (
if(AUTH_TOKEN_VALUE === null) return next(BadRequestError({message: 'Missing Authorization Body in the request header'})) if(AUTH_TOKEN_VALUE === null) return next(BadRequestError({message: 'Missing Authorization Body in the request header'}))
const decodedToken = <jwt.UserIDJwtPayload>( const decodedToken = <jwt.UserIDJwtPayload>(
jwt.verify(AUTH_TOKEN_VALUE, JWT_SERVICE_SECRET) jwt.verify(AUTH_TOKEN_VALUE, getJwtServiceSecret())
); );
const serviceToken = await ServiceToken.findOne({ const serviceToken = await ServiceToken.findOne({
+2 -2
View File
@@ -1,8 +1,8 @@
import jwt from 'jsonwebtoken'; import jwt from 'jsonwebtoken';
import { Request, Response, NextFunction } from 'express'; import { Request, Response, NextFunction } from 'express';
import { User } from '../models'; import { User } from '../models';
import { JWT_SIGNUP_SECRET } from '../config';
import { BadRequestError, UnauthorizedRequestError } from '../utils/errors'; import { BadRequestError, UnauthorizedRequestError } from '../utils/errors';
import { getJwtSignupSecret } from '../config';
declare module 'jsonwebtoken' { declare module 'jsonwebtoken' {
export interface UserIDJwtPayload extends jwt.JwtPayload { export interface UserIDJwtPayload extends jwt.JwtPayload {
@@ -27,7 +27,7 @@ const requireSignupAuth = async (
if(AUTH_TOKEN_VALUE === null) return next(BadRequestError({message: 'Missing Authorization Body in the request header'})) if(AUTH_TOKEN_VALUE === null) return next(BadRequestError({message: 'Missing Authorization Body in the request header'}))
const decodedToken = <jwt.UserIDJwtPayload>( const decodedToken = <jwt.UserIDJwtPayload>(
jwt.verify(AUTH_TOKEN_VALUE, JWT_SIGNUP_SECRET) jwt.verify(AUTH_TOKEN_VALUE, getJwtSignupSecret())
); );
const user = await User.findOne({ const user = await User.findOne({
-1
View File
@@ -1,5 +1,4 @@
import { Schema, model } from 'mongoose'; import { Schema, model } from 'mongoose';
import { EMAIL_TOKEN_LIFETIME } from '../config';
export interface IToken { export interface IToken {
email: string; email: string;
+2
View File
@@ -1,4 +1,5 @@
import express, { Request, Response } from 'express'; import express, { Request, Response } from 'express';
import { getSmtpConfigured } from '../../config';
const router = express.Router(); const router = express.Router();
@@ -8,6 +9,7 @@ router.get(
res.status(200).json({ res.status(200).json({
date: new Date(), date: new Date(),
message: 'Ok', message: 'Ok',
emailConfigured: getSmtpConfigured()
}) })
} }
); );
+17 -1
View File
@@ -1,16 +1,32 @@
import mongoose from 'mongoose'; import mongoose from 'mongoose';
import { getLogger } from '../utils/logger'; import { getLogger } from '../utils/logger';
import { initDatabaseHelper } from '../helpers/database'; import {
initDatabaseHelper,
closeDatabaseHelper
} from '../helpers/database';
/** /**
* Class to handle database actions * Class to handle database actions
*/ */
class DatabaseService { class DatabaseService {
/**
* Initialize database connection
* @param {Object} obj
* @param {String} obj.mongoURL - mongo connection string
* @returns
*/
static async initDatabase(MONGO_URL: string) { static async initDatabase(MONGO_URL: string) {
return await initDatabaseHelper({ return await initDatabaseHelper({
mongoURL: MONGO_URL mongoURL: MONGO_URL
}); });
} }
/**
* Close database conection
*/
static async closeDatabase() {
return await closeDatabaseHelper();
}
} }
export default DatabaseService; export default DatabaseService;
+37 -20
View File
@@ -1,27 +1,44 @@
import { PostHog } from 'posthog-node'; import { PostHog } from 'posthog-node';
import {
NODE_ENV,
POSTHOG_HOST,
POSTHOG_PROJECT_API_KEY,
TELEMETRY_ENABLED
} from '../config';
import { getLogger } from '../utils/logger'; import { getLogger } from '../utils/logger';
import {
getNodeEnv,
getTelemetryEnabled,
getPostHogProjectApiKey,
getPostHogHost
} from '../config';
if(!TELEMETRY_ENABLED){ /**
getLogger("backend-main").info([ * Logs telemetry enable/disable notice.
"", */
"To improve, Infisical collects telemetry data about general usage.", const logTelemetryMessage = () => {
"This helps us understand how the product is doing and guide our product development to create the best possible platform; it also helps us demonstrate growth as we support Infisical as open-source software.", if(!getTelemetryEnabled()){
"To opt into telemetry, you can set `TELEMETRY_ENABLED=true` within the environment variables.", getLogger("backend-main").info([
].join('\n')) "",
"To improve, Infisical collects telemetry data about general usage.",
"This helps us understand how the product is doing and guide our product development to create the best possible platform; it also helps us demonstrate growth as we support Infisical as open-source software.",
"To opt into telemetry, you can set `TELEMETRY_ENABLED=true` within the environment variables.",
].join('\n'))
}
} }
let postHogClient: any; /**
if (NODE_ENV === 'production' && TELEMETRY_ENABLED) { * Return an instance of the PostHog client initialized.
// case: enable opt-out telemetry in production * @returns
postHogClient = new PostHog(POSTHOG_PROJECT_API_KEY, { */
host: POSTHOG_HOST const getPostHogClient = () => {
}); let postHogClient: any;
if (getNodeEnv() === 'production' && getTelemetryEnabled()) {
// case: enable opt-out telemetry in production
postHogClient = new PostHog(getPostHogProjectApiKey(), {
host: getPostHogHost()
});
}
return postHogClient;
}
export {
logTelemetryMessage,
getPostHogClient
} }
export default postHogClient;
+3 -2
View File
@@ -1,13 +1,14 @@
import DatabaseService from './DatabaseService'; import DatabaseService from './DatabaseService';
import postHogClient from './PostHogClient'; import { logTelemetryMessage, getPostHogClient } from './PostHogClient';
import BotService from './BotService'; import BotService from './BotService';
import EventService from './EventService'; import EventService from './EventService';
import IntegrationService from './IntegrationService'; import IntegrationService from './IntegrationService';
import TokenService from './TokenService'; import TokenService from './TokenService';
export { export {
logTelemetryMessage,
getPostHogClient,
DatabaseService, DatabaseService,
postHogClient,
BotService, BotService,
EventService, EventService,
IntegrationService, IntegrationService,
+49 -49
View File
@@ -1,11 +1,4 @@
import nodemailer from 'nodemailer'; import nodemailer from 'nodemailer';
import {
SMTP_HOST,
SMTP_PORT,
SMTP_USERNAME,
SMTP_PASSWORD,
SMTP_SECURE
} from '../config';
import { import {
SMTP_HOST_SENDGRID, SMTP_HOST_SENDGRID,
SMTP_HOST_MAILGUN, SMTP_HOST_MAILGUN,
@@ -14,55 +7,62 @@ import {
} from '../variables'; } from '../variables';
import SMTPConnection from 'nodemailer/lib/smtp-connection'; import SMTPConnection from 'nodemailer/lib/smtp-connection';
import * as Sentry from '@sentry/node'; import * as Sentry from '@sentry/node';
import {
getSmtpHost,
getSmtpUsername,
getSmtpPassword,
getSmtpSecure,
getSmtpPort
} from '../config';
const mailOpts: SMTPConnection.Options = { export const initSmtp = () => {
host: SMTP_HOST, const mailOpts: SMTPConnection.Options = {
port: SMTP_PORT as number host: getSmtpHost(),
}; port: getSmtpPort()
if (SMTP_USERNAME && SMTP_PASSWORD) {
mailOpts.auth = {
user: SMTP_USERNAME,
pass: SMTP_PASSWORD
}; };
}
if (SMTP_SECURE) { if (getSmtpUsername() && getSmtpPassword()) {
switch (SMTP_HOST) { mailOpts.auth = {
case SMTP_HOST_SENDGRID: user: getSmtpUsername(),
mailOpts.requireTLS = true; pass: getSmtpPassword()
break; };
case SMTP_HOST_MAILGUN: }
mailOpts.requireTLS = true;
mailOpts.tls = { if (getSmtpSecure() ? getSmtpSecure() : false) {
ciphers: 'TLSv1.2' switch (getSmtpHost()) {
} case SMTP_HOST_SENDGRID:
break; mailOpts.requireTLS = true;
case SMTP_HOST_SOCKETLABS: break;
mailOpts.requireTLS = true; case SMTP_HOST_MAILGUN:
mailOpts.tls = { mailOpts.requireTLS = true;
ciphers: 'TLSv1.2'
}
break;
case SMTP_HOST_ZOHOMAIL:
mailOpts.requireTLS = true;
mailOpts.tls = {
ciphers: 'TLSv1.2'
}
break;
default:
if (SMTP_HOST.includes('amazonaws.com')) {
mailOpts.tls = { mailOpts.tls = {
ciphers: 'TLSv1.2' ciphers: 'TLSv1.2'
} }
} else { break;
mailOpts.secure = true; case SMTP_HOST_SOCKETLABS:
} mailOpts.requireTLS = true;
break; mailOpts.tls = {
ciphers: 'TLSv1.2'
}
break;
case SMTP_HOST_ZOHOMAIL:
mailOpts.requireTLS = true;
mailOpts.tls = {
ciphers: 'TLSv1.2'
}
break;
default:
if (getSmtpHost().includes('amazonaws.com')) {
mailOpts.tls = {
ciphers: 'TLSv1.2'
}
} else {
mailOpts.secure = true;
}
break;
}
} }
}
export const initSmtp = () => {
const transporter = nodemailer.createTransport(mailOpts); const transporter = nodemailer.createTransport(mailOpts);
transporter transporter
.verify() .verify()
@@ -73,7 +73,7 @@ export const initSmtp = () => {
.catch((err) => { .catch((err) => {
Sentry.setUser(null); Sentry.setUser(null);
Sentry.captureException( Sentry.captureException(
`SMTP - Failed to connect to ${SMTP_HOST}:${SMTP_PORT} \n\t${err}` `SMTP - Failed to connect to ${getSmtpHost()}:${getSmtpPort()} \n\t${err}`
); );
}); });
+2 -2
View File
@@ -4,12 +4,12 @@
* *
************************************************************************************************/ ************************************************************************************************/
import { NODE_ENV } from "../config"
import { Key, Membership, MembershipOrg, Organization, User, Workspace } from "../models"; import { Key, Membership, MembershipOrg, Organization, User, Workspace } from "../models";
import { Types } from 'mongoose'; import { Types } from 'mongoose';
import { getNodeEnv } from '../config';
export const createTestUserForDevelopment = async () => { export const createTestUserForDevelopment = async () => {
if (NODE_ENV === "development") { if (getNodeEnv() === "development") {
const testUserEmail = "[email protected]" const testUserEmail = "[email protected]"
const testUserPassword = "testInfisical1" const testUserPassword = "testInfisical1"
const testUserId = "63cefa6ec8d3175601cfa980" const testUserId = "63cefa6ec8d3175601cfa980"
+8 -4
View File
@@ -1,7 +1,7 @@
/* eslint-disable no-console */ /* eslint-disable no-console */
import { createLogger, format, transports } from 'winston'; import { createLogger, format, transports } from 'winston';
import LokiTransport from 'winston-loki'; import LokiTransport from 'winston-loki';
import { LOKI_HOST, NODE_ENV } from '../config'; import { getLokiHost, getNodeEnv } from '../config';
const { combine, colorize, label, printf, splat, timestamp } = format; const { combine, colorize, label, printf, splat, timestamp } = format;
@@ -25,10 +25,10 @@ const createLoggerWithLabel = (level: string, label: string) => {
}) })
] ]
//* Add LokiTransport if it's enabled //* Add LokiTransport if it's enabled
if(LOKI_HOST !== undefined){ if(getLokiHost() !== undefined){
_transports.push( _transports.push(
new LokiTransport({ new LokiTransport({
host: LOKI_HOST, host: getLokiHost(),
handleExceptions: true, handleExceptions: true,
handleRejections: true, handleRejections: true,
batching: true, batching: true,
@@ -37,7 +37,11 @@ const createLoggerWithLabel = (level: string, label: string) => {
format: format.combine( format: format.combine(
format.json() format.json()
), ),
labels: {app: process.env.npm_package_name, version: process.env.npm_package_version, environment: NODE_ENV}, labels: {
app: process.env.npm_package_name,
version: process.env.npm_package_version,
environment: getNodeEnv()
},
onConnectionError: (err: Error)=> console.error('Connection error while connecting to Loki Server.\n', err) onConnectionError: (err: Error)=> console.error('Connection error while connecting to Loki Server.\n', err)
}) })
) )
+2 -2
View File
@@ -1,5 +1,5 @@
import { Request } from 'express' import { Request } from 'express'
import { VERBOSE_ERROR_OUTPUT } from '../config' import { getVerboseErrorOutput } from '../config';
export enum LogLevel { export enum LogLevel {
DEBUG = 100, DEBUG = 100,
@@ -87,7 +87,7 @@ export default class RequestError extends Error{
}, this.context) }, this.context)
//* Omit sensitive information from context that can leak internal workings of this program if user is not developer //* Omit sensitive information from context that can leak internal workings of this program if user is not developer
if(!VERBOSE_ERROR_OUTPUT){ if(!getVerboseErrorOutput()){
_context = this._omit(_context, [ _context = this._omit(_context, [
'stacktrace', 'stacktrace',
'exception', 'exception',
+2 -2
View File
@@ -34,7 +34,7 @@ import {
INTEGRATION_FLYIO_API_URL, INTEGRATION_FLYIO_API_URL,
INTEGRATION_CIRCLECI_API_URL, INTEGRATION_CIRCLECI_API_URL,
INTEGRATION_TRAVISCI_API_URL, INTEGRATION_TRAVISCI_API_URL,
INTEGRATION_OPTIONS, getIntegrationOptions
} from "./integration"; } from "./integration";
import { OWNER, ADMIN, MEMBER, INVITED, ACCEPTED } from "./organization"; import { OWNER, ADMIN, MEMBER, INVITED, ACCEPTED } from "./organization";
import { SECRET_SHARED, SECRET_PERSONAL } from "./secret"; import { SECRET_SHARED, SECRET_PERSONAL } from "./secret";
@@ -113,7 +113,7 @@ export {
ACTION_UPDATE_SECRETS, ACTION_UPDATE_SECRETS,
ACTION_DELETE_SECRETS, ACTION_DELETE_SECRETS,
ACTION_READ_SECRETS, ACTION_READ_SECRETS,
INTEGRATION_OPTIONS, getIntegrationOptions,
SMTP_HOST_SENDGRID, SMTP_HOST_SENDGRID,
SMTP_HOST_MAILGUN, SMTP_HOST_MAILGUN,
SMTP_HOST_SOCKETLABS, SMTP_HOST_SOCKETLABS,
+156 -155
View File
@@ -1,13 +1,11 @@
import { import {
CLIENT_ID_AZURE, getClientIdHeroku,
CLIENT_ID_GITLAB getClientSlugVercel,
getClientIdNetlify,
getClientIdAzure,
getClientIdGitLab,
getClientIdGitHub
} from '../config'; } from '../config';
import {
CLIENT_ID_HEROKU,
CLIENT_ID_NETLIFY,
CLIENT_ID_GITHUB,
CLIENT_SLUG_VERCEL
} from "../config";
// integrations // integrations
const INTEGRATION_AZURE_KEY_VAULT = 'azure-key-vault'; const INTEGRATION_AZURE_KEY_VAULT = 'azure-key-vault';
@@ -48,7 +46,6 @@ const INTEGRATION_GITHUB_TOKEN_URL =
"https://github.com/login/oauth/access_token"; "https://github.com/login/oauth/access_token";
const INTEGRATION_GITLAB_TOKEN_URL = "https://gitlab.com/oauth/token"; const INTEGRATION_GITLAB_TOKEN_URL = "https://gitlab.com/oauth/token";
// integration apps endpoints // integration apps endpoints
const INTEGRATION_HEROKU_API_URL = "https://api.heroku.com"; const INTEGRATION_HEROKU_API_URL = "https://api.heroku.com";
const INTEGRATION_GITLAB_API_URL = "https://gitlab.com/api"; const INTEGRATION_GITLAB_API_URL = "https://gitlab.com/api";
@@ -59,156 +56,160 @@ const INTEGRATION_FLYIO_API_URL = "https://api.fly.io/graphql";
const INTEGRATION_CIRCLECI_API_URL = "https://circleci.com/api"; const INTEGRATION_CIRCLECI_API_URL = "https://circleci.com/api";
const INTEGRATION_TRAVISCI_API_URL = "https://api.travis-ci.com"; const INTEGRATION_TRAVISCI_API_URL = "https://api.travis-ci.com";
// TODO: deprecate types? const getIntegrationOptions = () => {
const INTEGRATION_OPTIONS = [ const INTEGRATION_OPTIONS = [
{ {
name: 'Heroku', name: 'Heroku',
slug: 'heroku', slug: 'heroku',
image: 'Heroku.png', image: 'Heroku.png',
isAvailable: true, isAvailable: true,
type: 'oauth', type: 'oauth',
clientId: CLIENT_ID_HEROKU, clientId: getClientIdHeroku(),
docsLink: '' docsLink: ''
}, },
{ {
name: 'Vercel', name: 'Vercel',
slug: 'vercel', slug: 'vercel',
image: 'Vercel.png', image: 'Vercel.png',
isAvailable: true, isAvailable: true,
type: 'oauth', type: 'oauth',
clientId: '', clientId: '',
clientSlug: CLIENT_SLUG_VERCEL, clientSlug: getClientSlugVercel(),
docsLink: '' docsLink: ''
}, },
{ {
name: 'Netlify', name: 'Netlify',
slug: 'netlify', slug: 'netlify',
image: 'Netlify.png', image: 'Netlify.png',
isAvailable: true, isAvailable: true,
type: 'oauth', type: 'oauth',
clientId: CLIENT_ID_NETLIFY, clientId: getClientIdNetlify(),
docsLink: '' docsLink: ''
}, },
{ {
name: 'GitHub', name: 'GitHub',
slug: 'github', slug: 'github',
image: 'GitHub.png', image: 'GitHub.png',
isAvailable: true, isAvailable: true,
type: 'oauth', type: 'oauth',
clientId: CLIENT_ID_GITHUB, clientId: getClientIdGitHub(),
docsLink: '' docsLink: ''
}, },
{ {
name: 'Render', name: 'Render',
slug: 'render', slug: 'render',
image: 'Render.png', image: 'Render.png',
isAvailable: true, isAvailable: true,
type: 'pat', type: 'pat',
clientId: '', clientId: '',
docsLink: '' docsLink: ''
}, },
{ {
name: 'Fly.io', name: 'Fly.io',
slug: 'flyio', slug: 'flyio',
image: 'Flyio.svg', image: 'Flyio.svg',
isAvailable: true, isAvailable: true,
type: 'pat', type: 'pat',
clientId: '', clientId: '',
docsLink: '' docsLink: ''
}, },
{ {
name: 'AWS Parameter Store', name: 'AWS Parameter Store',
slug: 'aws-parameter-store', slug: 'aws-parameter-store',
image: 'Amazon Web Services.png', image: 'Amazon Web Services.png',
isAvailable: true, isAvailable: true,
type: 'custom', type: 'custom',
clientId: '', clientId: '',
docsLink: '' docsLink: ''
}, },
{ {
name: 'AWS Secret Manager', name: 'AWS Secret Manager',
slug: 'aws-secret-manager', slug: 'aws-secret-manager',
image: 'Amazon Web Services.png', image: 'Amazon Web Services.png',
isAvailable: true, isAvailable: true,
type: 'custom', type: 'custom',
clientId: '', clientId: '',
docsLink: '' docsLink: ''
}, },
{ {
name: 'Azure Key Vault', name: 'Azure Key Vault',
slug: 'azure-key-vault', slug: 'azure-key-vault',
image: 'Microsoft Azure.png', image: 'Microsoft Azure.png',
isAvailable: true, isAvailable: true,
type: 'oauth', type: 'oauth',
clientId: CLIENT_ID_AZURE, clientId: getClientIdAzure(),
docsLink: '' docsLink: ''
}, },
{ {
name: 'Circle CI', name: 'Circle CI',
slug: 'circleci', slug: 'circleci',
image: 'Circle CI.png', image: 'Circle CI.png',
isAvailable: true, isAvailable: true,
type: 'pat', type: 'pat',
clientId: '', clientId: '',
docsLink: '' docsLink: ''
}, },
{ {
name: 'GitLab', name: 'GitLab',
slug: 'gitlab', slug: 'gitlab',
image: 'GitLab.png', image: 'GitLab.png',
isAvailable: true, isAvailable: true,
type: 'custom', type: 'custom',
clientId: CLIENT_ID_GITLAB, clientId: getClientIdGitLab(),
docsLink: '' docsLink: ''
}, },
{ {
name: 'Travis CI', name: 'Travis CI',
slug: 'travisci', slug: 'travisci',
image: 'Travis CI.png', image: 'Travis CI.png',
isAvailable: true, isAvailable: true,
type: 'pat', type: 'pat',
clientId: '', clientId: '',
docsLink: '' docsLink: ''
}, },
{ {
name: 'Google Cloud Platform', name: 'Google Cloud Platform',
slug: 'gcp', slug: 'gcp',
image: 'Google Cloud Platform.png', image: 'Google Cloud Platform.png',
isAvailable: false, isAvailable: false,
type: '', type: '',
clientId: '', clientId: '',
docsLink: '' docsLink: ''
} }
] ]
return INTEGRATION_OPTIONS;
}
export { export {
INTEGRATION_AZURE_KEY_VAULT, INTEGRATION_AZURE_KEY_VAULT,
INTEGRATION_AWS_PARAMETER_STORE, INTEGRATION_AWS_PARAMETER_STORE,
INTEGRATION_AWS_SECRET_MANAGER, INTEGRATION_AWS_SECRET_MANAGER,
INTEGRATION_HEROKU, INTEGRATION_HEROKU,
INTEGRATION_VERCEL, INTEGRATION_VERCEL,
INTEGRATION_NETLIFY, INTEGRATION_NETLIFY,
INTEGRATION_GITHUB, INTEGRATION_GITHUB,
INTEGRATION_GITLAB, INTEGRATION_GITLAB,
INTEGRATION_RENDER, INTEGRATION_RENDER,
INTEGRATION_FLYIO, INTEGRATION_FLYIO,
INTEGRATION_CIRCLECI, INTEGRATION_CIRCLECI,
INTEGRATION_TRAVISCI, INTEGRATION_TRAVISCI,
INTEGRATION_SET, INTEGRATION_SET,
INTEGRATION_OAUTH2, INTEGRATION_OAUTH2,
INTEGRATION_AZURE_TOKEN_URL, INTEGRATION_AZURE_TOKEN_URL,
INTEGRATION_HEROKU_TOKEN_URL, INTEGRATION_HEROKU_TOKEN_URL,
INTEGRATION_VERCEL_TOKEN_URL, INTEGRATION_VERCEL_TOKEN_URL,
INTEGRATION_NETLIFY_TOKEN_URL, INTEGRATION_NETLIFY_TOKEN_URL,
INTEGRATION_GITHUB_TOKEN_URL, INTEGRATION_GITHUB_TOKEN_URL,
INTEGRATION_GITLAB_API_URL, INTEGRATION_GITLAB_API_URL,
INTEGRATION_HEROKU_API_URL, INTEGRATION_HEROKU_API_URL,
INTEGRATION_GITLAB_TOKEN_URL, INTEGRATION_GITLAB_TOKEN_URL,
INTEGRATION_VERCEL_API_URL, INTEGRATION_VERCEL_API_URL,
INTEGRATION_NETLIFY_API_URL, INTEGRATION_NETLIFY_API_URL,
INTEGRATION_RENDER_API_URL, INTEGRATION_RENDER_API_URL,
INTEGRATION_FLYIO_API_URL, INTEGRATION_FLYIO_API_URL,
INTEGRATION_CIRCLECI_API_URL, INTEGRATION_CIRCLECI_API_URL,
INTEGRATION_TRAVISCI_API_URL, INTEGRATION_TRAVISCI_API_URL,
INTEGRATION_OPTIONS, getIntegrationOptions
}; };
+5
View File
@@ -0,0 +1,5 @@
import { it, expect } from '@jest/globals';
it('should return true', () => {
expect(true).toBeTruthy();
});
+21
View File
@@ -0,0 +1,21 @@
import { Server } from 'http';
import main from '../src';
import { describe, expect, it, beforeAll, afterAll } from '@jest/globals';
import request from 'supertest';
let server: Server;
beforeAll(async () => {
server = await main;
});
afterAll(async () => {
server.close();
});
describe('Healthcheck endpoint', () => {
it('GET /healthcheck should return OK', async () => {
const res = await request(server).get('/healthcheck');
expect(res.status).toEqual(200);
});
});
@@ -0,0 +1,176 @@
AWSTemplateFormatVersion: 2010-09-09
Description: >-
CloudFormation template to deploy Infisical on a EC2 instance with a
DocumentDB instance
Parameters:
KeyPairName:
Description: The name of the EC2 Key Pair to enable SSH access to the instance
Type: "AWS::EC2::KeyPair::KeyName"
VpcId:
Description: The ID of the VPC in which to launch the instance
Type: "AWS::EC2::VPC::Id"
DocumentDBUsername:
Description: The username for the DocumentDB instance
Type: String
MinLength: 5
DocumentDBPassword:
Description: The password for the DocumentDB instance (minimum 8 characters)
Type: String
MinLength: 8
NoEcho: true
Resources:
DocumentDBCluster:
Type: "AWS::DocDB::DBCluster"
Properties:
EngineVersion: 4.0.0
StorageEncrypted: true
MasterUsername: !Ref DocumentDBUsername
MasterUserPassword: !Ref DocumentDBPassword
VpcSecurityGroupIds:
- !Ref DocumentDBClusterSecurityGroup
DBClusterParameterGroupName: !Ref DBClusterParameterGroup
Metadata:
"AWS::CloudFormation::Designer":
id: 73b974cf-eed3-4f7d-8657-6a6746bac169
DependsOn:
- DBClusterParameterGroup
DBClusterParameterGroup:
Type: "AWS::DocDB::DBClusterParameterGroup"
Properties:
Description: "description"
Family: "docdb4.0"
Parameters:
tls: "disabled"
ttl_monitor: "disabled"
Tags:
- Key: "String"
Value: "String"
DocumentDBInstance:
Type: "AWS::DocDB::DBInstance"
Properties:
DBInstanceClass: db.t4g.medium
DBClusterIdentifier: !Ref DocumentDBCluster
Metadata:
"AWS::CloudFormation::Designer":
id: f04cee38-175e-4432-9ad7-62ca28bbf935
DocumentDBClusterSecurityGroup:
Type: AWS::EC2::SecurityGroup
Properties:
GroupDescription: Allow inbound traffic for DocumentDB cluster
VpcId: !Ref VpcId
SecurityGroupIngress:
- IpProtocol: tcp
FromPort: 27017
ToPort: 27017
SourceSecurityGroupId: !Ref InstanceSecurityGroup
EC2Instance:
Type: "AWS::EC2::Instance"
Properties:
ImageId: ami-0557a15b87f6559cf
InstanceType: t2.medium
KeyName: !Ref KeyPairName
UserData:
Fn::Base64: !Sub |
#!/bin/bash
cd /home/ubuntu
curl -fsSL https://get.docker.com -o get-docker.sh
sh get-docker.sh
sudo curl -L "https://github.com/docker/compose/releases/download/1.29.2/docker-compose-$(uname -s)-$(uname -m)" -o /usr/local/bin/docker-compose
sudo chmod +x /usr/local/bin/docker-compose
git clone https://github.com/Infisical/infisical.git
cd infisical
DOCUMENT_DB_CONNECTION_URL="mongodb://${DocumentDBUsername}:${DocumentDBPassword}@${DocumentDBCluster.Endpoint}:${DocumentDBCluster.Port}/infisical?replicaSet=rs0&readPreference=secondaryPreferred&retryWrites=false"
ENCRYPTION_KEY=$(openssl rand -hex 16)
JWT_SIGNUP_SECRET=$(openssl rand -hex 16)
JWT_REFRESH_SECRET=$(openssl rand -hex 16)
JWT_AUTH_SECRET=$(openssl rand -hex 16)
JWT_SERVICE_SECRET=$(openssl rand -hex 16)
touch .env
echo "ENCRYPTION_KEY=${!ENCRYPTION_KEY}" >> .env
echo "JWT_SIGNUP_SECRET=${!JWT_SIGNUP_SECRET}" >> .env
echo "JWT_REFRESH_SECRET=${!JWT_REFRESH_SECRET}" >> .env
echo "JWT_AUTH_SECRET=${!JWT_AUTH_SECRET}" >> .env
echo "JWT_SERVICE_SECRET=${!JWT_SERVICE_SECRET}" >> .env
echo "MONGO_URL=${!DOCUMENT_DB_CONNECTION_URL}" >> .env
docker-compose up -d
SecurityGroupIds:
- !Ref InstanceSecurityGroup
Tags:
- Key: Name
Value: infisical
Metadata:
"AWS::CloudFormation::Designer":
id: 2c0a771c-5002-4785-9848-0377e33cd0e9
DependsOn:
- DocumentDBInstance
InstanceSecurityGroup:
Type: "AWS::EC2::SecurityGroup"
Properties:
GroupDescription: Allow SSH and HTTP traffic
SecurityGroupIngress:
- IpProtocol: tcp
FromPort: 22
ToPort: 22
CidrIp: 0.0.0.0/0
- IpProtocol: tcp
FromPort: 80
ToPort: 80
CidrIp: 0.0.0.0/0
VpcId: !Ref VpcId
Metadata:
"AWS::CloudFormation::Designer":
id: 1fd6856a-11e5-4369-84fa-d18d4011b3de
Outputs:
InstanceIP:
Value: !GetAtt EC2Instance.PublicIp
Metadata:
"AWS::CloudFormation::Designer":
1fd6856a-11e5-4369-84fa-d18d4011b3de:
size:
width: 60
height: 60
position:
x: 60
"y": 90
z: 1
embeds: []
2c0a771c-5002-4785-9848-0377e33cd0e9:
size:
width: 60
height: 60
position:
x: 180
"y": 90
z: 1
embeds: []
isassociatedwith:
- 1fd6856a-11e5-4369-84fa-d18d4011b3de
dependson:
- 2cabaada-fbdb-4945-bf95-a0406704dd5a
- f04cee38-175e-4432-9ad7-62ca28bbf935
73b974cf-eed3-4f7d-8657-6a6746bac169:
size:
width: 60
height: 60
position:
x: 390
"y": 210
z: 1
embeds: []
f04cee38-175e-4432-9ad7-62ca28bbf935:
size:
width: 60
height: 60
position:
x: 270
"y": 90
z: 1
embeds: []
+6 -5
View File
@@ -34,6 +34,7 @@ services:
env_file: .env env_file: .env
environment: environment:
- NODE_ENV=development - NODE_ENV=development
- MONGO_URL=mongodb://root:example@mongo:27017/?authSource=admin
networks: networks:
- infisical-dev - infisical-dev
@@ -65,8 +66,8 @@ services:
restart: always restart: always
env_file: .env env_file: .env
environment: environment:
- MONGO_INITDB_ROOT_USERNAME=${MONGO_USERNAME} - MONGO_INITDB_ROOT_USERNAME=root
- MONGO_INITDB_ROOT_PASSWORD=${MONGO_PASSWORD} - MONGO_INITDB_ROOT_PASSWORD=example
volumes: volumes:
- mongo-data:/data/db - mongo-data:/data/db
networks: networks:
@@ -80,9 +81,9 @@ services:
- mongo - mongo
env_file: .env env_file: .env
environment: environment:
- ME_CONFIG_MONGODB_ADMINUSERNAME=${MONGO_USERNAME} - ME_CONFIG_MONGODB_ADMINUSERNAME=root
- ME_CONFIG_MONGODB_ADMINPASSWORD=${MONGO_PASSWORD} - ME_CONFIG_MONGODB_ADMINPASSWORD=example
- ME_CONFIG_MONGODB_URL=mongodb://${MONGO_USERNAME}:${MONGO_PASSWORD}@mongo:27017/ - ME_CONFIG_MONGODB_URL=mongodb://root:example@mongo:27017/
ports: ports:
- 8081:8081 - 8081:8081
networks: networks:
+7 -12
View File
@@ -1,26 +1,21 @@
--- ---
title: "Infisical Token" title: "Infisical Token"
description: "Use Infisical service token as one of the authentication methods." description: "Use the Infisical Token as one of the authentication methods."
--- ---
An Infisical Token is needed to authenticate the CLI when there isn't an easy way to input your login credentials. An Infisical Token is useful for:
It's useful for your CI/CD environments and integrations such as [Docker](/integrations/platforms/docker) and [Docker Compose](/integrations/platforms/docker-compose). - Authenticating the [Infisical CLI](/cli/overview) when there isn't an easy way to input your login credentials.
- Granting the [Infisical SDKs](/sdks/overview) access to secrets scoped to a project and environment.
It's also useful for CI/CD environments and integrations such as [Docker](/integrations/platforms/docker) and [Docker Compose](/integrations/platforms/docker-compose).
To generate the the token, head over to your project settings as shown below. To generate the the token, head over to your project settings as shown below.
![token add](../../images/project-token-add.png) ![token add](../../images/project-token-add.png)
## Feeding Infisical Token to the CLI ## Feeding Infisical Token to the CLI
The Infisical CLI checks for the presence of an environment variable called `INFISICAL_TOKEN`. The Infisical CLI checks for the presence of an environment variable called `INFISICAL_TOKEN`.
If it detects this variable in the terminal where it is being run, it will use it to authenticate and retrieve the environment variables that the token is authorized to access. If it detects this variable in the terminal where it is being run, it will use it to authenticate and retrieve the environment variables that the token is authorized to access.
This allows you to use the CLI in environments where you are unable to run the `infisical login` command. This allows you to use the CLI in environments where you are unable to run the `infisical login` command.
<Note>
The token grants read-only access to a particular environment and project for
a specified amount of time. Once the token is expired, the CLI using it will no longer be able to make
requests with it.
</Note>
+7 -21
View File
@@ -1,10 +1,8 @@
--- ---
title: "Features" title: "Features"
description: "A list of features that Infisical has to offer." description: "A non-exhaustive list of features that Infisical has to offer."
--- ---
This is a non-exhaustive list of features that Infisical offers:
## Platform ## Platform
- Provision members access to organizations and projects. - Provision members access to organizations and projects.
@@ -16,29 +14,17 @@ This is a non-exhaustive list of features that Infisical offers:
## CLI ## CLI
The CLI is used to inject environment variables into applications and infrastructure. The [CLI](/cli/overview) is used to inject environment variables into applications and infrastructure.
- Inject environment variables. - Inject environment variables.
- Inject environment variables into containers via service tokens for Docker. - Inject environment variables into containers via service tokens for Docker.
## SDKs
[SDKs](/sdks/overview) enable apps to fetch back secrets using an [Infisical Token](/getting-started/dashboard/token) scoped to a project and environment.
## Roadmap ## Roadmap
We're building the future of secret management, one that's comprehensive and accessible to all. Some high-level features we have in mind: We're building the future of secret management, one that's comprehensive and accessible to all. Check out our [roadmap](https://www.notion.so/infisical/be2d2585a6694e40889b03aef96ea36b?v=5b19a8127d1a4060b54769567a8785fa).
| Feature | Status |
| ------------------------------------- | ----------- |
| Account recovery: Backup key | Done |
| 2FA | Done |
| Read/write access controls | Done |
| Comparing secrets across environments | Done |
| Integrations | Ongoing |
| More hosting options | Ongoing |
| 1-Click Deploys | Ongoing |
| Access logs | Ongoing |
| Account recovery: Member-assisted | Coming soon |
| Slack & MS teams integrations | Coming soon |
| Version control for secrets | Coming soon |
| Restricted IPs | Coming soon |
| Secret rotation | Coming soon |
Interested in contributing? Check out the [guide](/contributing/overview). Interested in contributing? Check out the [guide](/contributing/overview).
+5 -6
View File
@@ -19,6 +19,9 @@ Start syncing environment variables with [Infisical Cloud](https://app.infisical
<Card href="/cli/overview" title="CLI" icon="square-terminal" color="#16a34a"> <Card href="/cli/overview" title="CLI" icon="square-terminal" color="#16a34a">
Install the CLI to inject secrets into apps and infra. Install the CLI to inject secrets into apps and infra.
</Card> </Card>
<Card href="/sdks/overview" title="SDKs" icon="puzzle-piece" color="#16a34a">
Install an SDK into your app to fetch secrets.
</Card>
<Card <Card
href="/self-hosting/overview" href="/self-hosting/overview"
title="Self-hosting" title="Self-hosting"
@@ -27,6 +30,8 @@ Start syncing environment variables with [Infisical Cloud](https://app.infisical
> >
Learn how to configure and deploy Infisical. Learn how to configure and deploy Infisical.
</Card> </Card>
</CardGroup>
<Card <Card
href="/integrations/overview" href="/integrations/overview"
title="Integrations" title="Integrations"
@@ -35,9 +40,3 @@ Start syncing environment variables with [Infisical Cloud](https://app.infisical
> >
Explore integrations for Docker, AWS, Heroku, etc. Explore integrations for Docker, AWS, Heroku, etc.
</Card> </Card>
</CardGroup>
<Card title="Set up a 1x1 with an Infisical Engineer" iconType="duotone" color="#ca8b04" href="https://calendly.com/maidull/30min">
Our team is happy to help you get started with Infisical. If you have any questions or want to learn how you can leverage Infisical within your infrastructure, **[set up a 1-on-1 with an Infisical engineer](https://cal.com/maidul/15min)**.
</Card>
+90 -22
View File
@@ -3,9 +3,7 @@ title: "Quickstart"
description: "Start managing your developer secrets and configs with Infisical in 10 minutes." description: "Start managing your developer secrets and configs with Infisical in 10 minutes."
--- ---
This example demonstrates how to store and inject environment variables from [Infisical Cloud](https://app.infisical.com) into your application. These examples demonstrate how to store and fetch environment variables from [Infisical Cloud](https://app.infisical.com) into your application.
Note that the Infisical CLI is platform-agnostic and can inject environment variables across many tech stacks and frameworks.
## Set up Infisical Cloud ## Set up Infisical Cloud
@@ -15,30 +13,100 @@ Note that the Infisical CLI is platform-agnostic and can inject environment vari
![project quickstart](../images/project-quickstart.png) ![project quickstart](../images/project-quickstart.png)
## Set up the CLI ## Fetch Secrets for Your App
1. Follow the instructions to [install the CLI](/cli/overview). <Tabs>
<Tab title="CLI">
The Infisical CLI is platform-agnostic and enables you to inject environment variables into your app across many tech stacks and frameworks.
2. Initialize Infisical for your project. ### Set up the CLI
```bash 1. Follow the instructions to [install our platform-agnostic CLI](/cli/overview).
# move to your project
cd /path/to/project
# initialize infisical 2. Initialize Infisical for your project.
infisical init
```
## Start your app with environment variables injected ```bash
# move to your project
cd /path/to/project
```bash # initialize infisical
# inject environment variables into app infisical init
infisical run -- [your application start command] ```
```
<Info> ### Start your app with environment variables injected
Check out our [integrations](/integrations/overview) for injecting environment
variables into frameworks and platforms like Docker.
</Info>
Your app should be running with the environment variables injected. ```bash
# inject environment variables into app
infisical run -- [your application start command]
```
Your app should now be running with the environment variables injected.
Check out our [integrations](/integrations/overview) for injecting environment variables into frameworks and platforms like Docker.
</Tab>
<Tab title="SDK">
[Infisical SDKs](/sdks/overview) let your app fetch back secrets using an [Infisical Token](/getting-started/dashboard/token) that is scoped to a project and environment in Infisical. In this example, we demonstrate how to use the [Node SDK](/sdks/languages/node).
### Obtain an [Infisical Token](/getting-started/dashboard/token)
Head to your project settings to create a token scoped to the project and environment you wish to fetch secrets from.
![token add](../images/project-token-add.png)
### Install the SDK
```bash
npm install infisical-node --save
```
### Initialize the Infisical client
```js
await infisical.connect({
token: "your_infisical_token",
});
```
### Get a value
```js
const value = infisical.get("SOME_KEY");
```
### Example with Express
```js
const express = require("express");
const port = 3000;
const infisical = require("infisical-node");
const main = async () => {
await infisical.connect({
token: "st.xxx.xxx",
});
// your application logic
app.get("/", (req, res) => {
res.send(`Howdy, ${infisical.get("NAME")}!`);
});
app.listen(port, async () => {
console.log(`App listening on port ${port}`);
});
};
```
<Warning>
We do not recommend hardcoding your [Infisical
Token](/getting-started/dashboard/token). Setting it as an environment
variable would be best.
</Warning>
Check out our [SDKs](/sdks/overview) for other language SDKs.
</Tab>
</Tabs>
Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

+3 -3
View File
@@ -39,10 +39,10 @@ Prerequisites:
Starting your service with the Infisical CLI pulls your secrets from Infisical and injects them into your service. Starting your service with the Infisical CLI pulls your secrets from Infisical and injects them into your service.
```dockerfile ```dockerfile
CMD ["infisical", "run", "---", "[your service start command]"] CMD ["infisical", "run", "--", "[your service start command]"]
# example with single single command # example with single single command
CMD ["infisical", "run", "---", "npm", "run", "start"] CMD ["infisical", "run", "--", "npm", "run", "start"]
# example with multiple commands # example with multiple commands
CMD ["infisical", "run", "--command", "npm run start && ..."] CMD ["infisical", "run", "--command", "npm run start && ..."]
@@ -55,7 +55,7 @@ Head to your project settings in Infisical Cloud to generate an [Infisical Token
## Feed Docker your Infisical Token ## Feed Docker your Infisical Token
```bash ```bash
docker run --env INFISICAL_TOKEN=[token]... docker run --env INFISICAL_TOKEN=[token] [DOCKER-IMAGE]...
``` ```
<Info> <Info>
+1 -1
View File
@@ -362,7 +362,7 @@ The managed secret created by the operator will not be deleted when the operator
<Tab title="Helm"> <Tab title="Helm">
Install Infisical Helm repository Install Infisical Helm repository
```bash ```bash
helm uninstall add <release name> helm uninstall <release name>
``` ```
</Tab> </Tab>
<Tab title="Kubectl"> <Tab title="Kubectl">
+19 -18
View File
@@ -45,14 +45,9 @@
"url": "security" "url": "security"
}, },
{ {
"name": "Self-hosting", "name": "SDKs",
"icon": "server",
"url": "self-hosting"
},
{
"name": "SDK",
"icon": "puzzle-piece", "icon": "puzzle-piece",
"url": "sdk" "url": "sdks"
}, },
{ {
"name": "API Reference", "name": "API Reference",
@@ -101,6 +96,14 @@
"getting-started/dashboard/token" "getting-started/dashboard/token"
] ]
}, },
{
"group": "Self-hosting",
"pages": [
"self-hosting/overview",
"self-hosting/configuration/envars",
"self-hosting/configuration/email"
]
},
{ {
"group": "Command line", "group": "Command line",
"pages": [ "pages": [
@@ -171,12 +174,6 @@
"integrations/platforms/pm2" "integrations/platforms/pm2"
] ]
}, },
{
"group": "Self-hosting",
"pages": [
"self-hosting/overview"
]
},
{ {
"group": "Deployment options", "group": "Deployment options",
"pages": [ "pages": [
@@ -185,16 +182,20 @@
] ]
}, },
{ {
"group": "Configuration", "group": "Overview",
"pages": [ "pages": [
"self-hosting/configuration/envars", "sdks/overview"
"self-hosting/configuration/email"
] ]
}, },
{ {
"group": "SDK", "group": "SDKs",
"pages": [ "pages": [
"sdk/overview/usage" "sdks/languages/node",
"sdks/languages/python",
"sdks/languages/java",
"sdks/languages/ruby",
"sdks/languages/go",
"sdks/languages/rust"
] ]
}, },
{ {
-104
View File
@@ -1,104 +0,0 @@
---
title: "Usage"
---
<Note>
We're currently expanding the functionality of the Javascript SDK and working
on mirror SDKs for other languages like Python as well. Follow this GitHub
[issue](https://github.com/Infisical/infisical/issues/320) to stay updated.
</Note>
Infisical provides a [Node SDK](https://github.com/Infisical/infisical-node) that users can easily install into their applications and use to fetch their secrets.
With the SDK, users can currently fetch back secrets and define default values.
<Tabs>
<Tab title="Javascript">
## Installation
```bash
$ npm install infisical-node
```
## Import
```js
// ES6 syntax
import infisical from "infisical-node";
// ES5 syntax
const infisical = require("infisical-node");
```
## Initialization
If your app only needs to connect to one Infisical project, you should use `infisical.connect`. If you need to connect to multiple Infisical projects, use `infisical.createConnection`.
Both `connect` and `createConnection` take a parameter `token` and pull in the secrets accessible by that Infisical token.
```js
// using async-await (recommended)
await infisical.connect({
token: "your_infisical_token",
});
```
```js
// using promise chaining
infisical.connect({
token: "your_infisical_token"
})
.then(() => {
console.log('Success!)
})
.catch(err => {
console.error('Error: ', err);
})
```
Options:
| Option | Description | Default Value |
| -------------------- | ----------------------------------------------------------- | --------------------------- |
| `token` | ❗️ An Infisical Token to be used to fetch secrets | `None` |
| `siteURL` | Site URL of Infisical to connect to | `https://app.infisical.com` |
| `attachToProcessEnv` | Whether or not to attach fetched secrets to `process.env` | `False` |
| `defaultValues` | Default values for secrets if they aren't fetched/passed in | `{}` |
## Access a Secret Value
```js
const dbURL = infisical.getSecretValue("DB_URL");
```
## Example with Express
```js
const express = require("express");
const port = 3000;
const infisical = require("infisical-node");
app.get("/", (req, res) => {
// access value
const name = infisical.getSecret("NAME");
res.send(`Hello! My name is: ${name}`);
});
app.listen(port, async () => {
// initialize client
await infisical.connect({
token: "YOUR_INFISICAL_TOKEN",
});
console.log(`App listening on port ${port}`);
});
```
</Tab>
<Tab title="Python">
Coming soon.
</Tab>
</Tabs>
+8
View File
@@ -0,0 +1,8 @@
---
title: "Go"
---
Coming soon.
Follow this GitHub
[issue](https://github.com/Infisical/infisical/issues/436) to stay updated.
+8
View File
@@ -0,0 +1,8 @@
---
title: "Java"
---
Coming soon.
Follow this GitHub
[issue](https://github.com/Infisical/infisical/issues/434) to stay updated.
+154
View File
@@ -0,0 +1,154 @@
---
title: "Node"
---
If you're working with Node.js, the official [infisical-node](https://github.com/Infisical/infisical-node) package is the easiest way to fetch secrets for your application.
## Installation
Run `npm` to add `infisical-node` to your project.
```bash
npm install infisical-node --save
```
## Initialization
Set up the Infisical client asynchronously as early as possible in your application by importing and initializing the global instance with `infisical.connect(options)`.
This methods fetches back all the secrets in the project and environment accessible by the token passed in `options`.
### infisical.connect(options)
Updates the global instance of the Infisical client with a connection to an Infisical project and fetches back secrets if supplied with an [Infisical Token](/getting-started/dashboard/token).
<ResponseField name="options" type="object">
<Expandable title="properties">
<ResponseField name="token" type="string">
An [Infisical Token](/getting-started/dashboard/token) scoped to a project
and environment
</ResponseField>
<ResponseField
name="siteURL"
type="string"
default="https://app.infisical.com"
>
Your self-hosted absolute site URL including the protocol (e.g.
`https://app.infisical.com`)
</ResponseField>
<ResponseField name="debug" type="boolean" default="false">
Whether or not debug mode is on
</ResponseField>
<ResponseField name="attachToProcessEnv" type="boolean" default="false">
Whether or not to attach fetched secrets to `process.env`
</ResponseField>
</Expandable>
</ResponseField>
### infisical.createConnection(options)
Returns a local instance of the Infisical client with a connection to an Infisical project and fetches back secrets if supplied with an [Infisical Token](/getting-started/dashboard/token).
This method is useful if you wish to connect to two or more Infisical projects within your app.
<ResponseField name="options" type="object">
<Expandable title="properties">
<ResponseField name="token" type="string">
An [Infisical Token](/getting-started/dashboard/token) scoped to a project
and environment
</ResponseField>
<ResponseField
name="siteURL"
type="string"
default="https://app.infisical.com"
>
Your self-hosted absolute site URL including the protocol (e.g.
`https://app.infisical.com`)
</ResponseField>
<ResponseField name="debug" type="boolean" default="false">
Whether or not debug mode is on
</ResponseField>
</Expandable>
</ResponseField>
<Tabs>
<Tab title="ES6">
```js
import infisical from "infisical-node";
const main = async () => {
await infisical.connect({
token: "your_infisical_token",
});
// your app logic
}
main();
```
</Tab>
<Tab title="ES5">
```js
const infisical = require("infisical-node");
infisical.connect({
token: "your_infisical_token"
})
.then(() => {
// your application logic
})
.catch(err => {
console.error('Error: ', err);
})
````
</Tab>
</Tabs>
## Usage
To get the value of a secret, use `infisical.get(key)`.
### infisical.get(key)
Return the value of the secret with the specified `key`. Note that the Infisical client falls back to `process.env` if `token` is `undefined` during the
initialization step or if a value for the secret is not found in the fetched secrets.
<ResponseField name="key" type="string" required>
The key of the secret
</ResponseField>
```js
const value = infisical.get("SOME_KEY");
```
## Example with Express
```js
const express = require("express");
const port = 3000;
const infisical = require("infisical-node");
const main = async () => {
await infisical.connect({
token: "st.xxx.xxx",
});
// your application logic
app.get("/", (req, res) => {
res.send(`Howdy, ${infisical.get("NAME")}!`);
});
app.listen(port, async () => {
console.log(`App listening on port ${port}`);
});
};
```
<Warning>
We do not recommend hardcoding your [Infisical
Token](/getting-started/dashboard/token). Setting it as an environment
variable would be best.
</Warning>
+8
View File
@@ -0,0 +1,8 @@
---
title: "Python"
---
Coming soon.
Follow this GitHub
[issue](https://github.com/Infisical/infisical/issues/433) to stay updated.
+8
View File
@@ -0,0 +1,8 @@
---
title: "Ruby"
---
Coming soon.
Follow this GitHub
[issue](https://github.com/Infisical/infisical/issues/435) to stay updated.
+8
View File
@@ -0,0 +1,8 @@
---
title: "Rust"
---
Coming soon.
Follow this GitHub
[issue](https://github.com/Infisical/infisical/issues/437) to stay updated.
+18
View File
@@ -0,0 +1,18 @@
---
title: "Overview"
description: "How to use Infisical SDKs to fetch back secrets for your app"
---
Infisical SDKs provide the easiest way for your app to fetch back secrets using an [Infisical Token](/getting-started/dashboard/token) and has a few benefits:
- Local development: Replace 10s of environment variables in your `.env` file with 1 environment variable (the [Infisical Token](/getting-started/dashboard/token)).
- Production: Fetch secrets back to any cloud regardless of if an integration exists between Infisical and the cloud platform.
We currently only have the [Node SDK](/sdks/languages/node) available but more language SDKs are coming out soon:
- [Node](/sdks/languages/node)
- [Python](/sdks/languages/python)
- [Java](/sdks/languages/java)
- [Ruby](/sdks/languages/ruby)
- [Go](/sdks/languages/go)
- [Rust](/sdks/languages/rust)
+1 -1
View File
@@ -1,5 +1,5 @@
--- ---
title: "Email" title: "Configure email service"
description: "How to configure your email when self-hosting Infisical." description: "How to configure your email when self-hosting Infisical."
--- ---
+147 -40
View File
@@ -1,44 +1,151 @@
--- ---
title: "Environment Variables" title: "All environment variables"
description: "How to configure your environment variables when self-hosting Infisical." description: "Configure your environment variables when self-hosting Infisical."
--- ---
Configuring Infisical requires setting some environment variables. There is a file called [`.env.example`](https://github.com/Infisical/infisical/blob/main/.env.example) at the root directory of our main repo that you can use to create a `.env` file before you start the server. ## Backend environment variables
| Variable | Description | Default Value | Depending on your choosen self hosted deployment method, you may need to configured at least the required environment variable listed below.
| ----------------------- | ----------------------------------------------------------------------------------------------------------- | ------------- | Other environment variables are listed below to increase the functionality of your self hosted instance based on your use case.
| `ENCRYPTION_KEY` | ❗️ Strong hex encryption key | `None` |
| `JWT_SIGNUP_SECRET` | ❗️ JWT token secret | `None` | <Tabs>
| `JWT_REFRESH_SECRET` | ❗️ JWT token secret | `None` | <Tab title="Required">
| `JWT_AUTH_SECRET` | ❗️ JWT token secret | `None` | <ParamField query="ENCRYPTION_KEY" type="string" default="none" required>
| `JWT_MFA_SECRET` | ❗️ JWT token secret | `None` | Must be a random 32 character length hex string
| `JWT_SERVICE_SECRET` | ❗️ JWT token secret | `None` | </ParamField>
| `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` | <ParamField query="JWT_SIGNUP_SECRET" type="string" default="none" required>
| `JWT_AUTH_LIFETIME` | JWT token lifetime expressed in seconds or a string describing a time span (e.g. 60, "2 days", "10h", "7d") | `10d` | Must be a random 32 character length hex string
| `JWT_MFA_LIFETIME` | JWT token lifetime expressed in seconds or a string describing a time span (e.g. 60, "2 days", "10h", "7d") | `5m` | </ParamField>
| `EMAIL_TOKEN_LIFETIME` | Email OTP/magic-link lifetime expressed in seconds | `86400` |
| `MONGO_URL` | ❗️ MongoDB instance connection string either to container instance or MongoDB Cloud | `None` | <ParamField query="JWT_REFRESH_SECRET" type="string" default="none" required>
| `MONGO_USERNAME` | MongoDB username if using container | `None` | Must be a random 32 character length hex string
| `MONGO_PASSWORD` | MongoDB password if using container | `None` | </ParamField>
| `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 | `None` | <ParamField query="JWT_AUTH_SECRET" type="string" default="none" required>
| `SMTP_USERNAME` | ❗️ Credential to connect to host (e.g. `[email protected]`) | `None` | Must be a random 32 character length hex string
| `SMTP_PASSWORD` | ❗️ Credential to connect to host | `None` | </ParamField>
| `SMTP_PORT` | Port to connect to for establishing SMTP connections | `587` |
| `SMTP_SECURE` | If true, use TLS when connecting to host. If false, TLS will be used if STARTTLS is supported | `false` | <ParamField query="JWT_MFA_SECRET" type="string" default="none" required>
| `SMTP_FROM_ADDRESS` | ❗️ Email address to be used for sending emails (e.g. `[email protected]`) | `None` | Must be a random 32 character length hex string
| `SMTP_FROM_NAME` | Name label to be used in From field (e.g. `Team`) | `Infisical` | </ParamField>
| `TELEMETRY_ENABLED` | `true` or `false`. [More](../overview). | `true` |
| `LICENSE_KEY` | License key if using Infisical Enterprise Edition | `true` | <ParamField query="JWT_SERVICE_SECRET" type="string" default="none" required>
| `CLIENT_ID_HEROKU` | OAuth2 client ID for Heroku integration | `None` | Must be a random 32 character length hex string
| `CLIENT_ID_VERCEL` | OAuth2 client ID for Vercel integration | `None` | </ParamField>
| `CLIENT_ID_NETLIFY` | OAuth2 client ID for Netlify integration | `None` |
| `CLIENT_ID_GITHUB` | OAuth2 client ID for GitHub integration | `None` | <ParamField query="MONGO_URL" type="string" default="none" required>
| `CLIENT_SECRET_HEROKU` | OAuth2 client secret for Heroku integration | `None` | *TLS based connection string is not yet supported
| `CLIENT_SECRET_VERCEL` | OAuth2 client secret for Vercel integration | `None` | </ParamField>
| `CLIENT_SECRET_NETLIFY` | OAuth2 client secret for Netlify integration | `None` | </Tab>
| `CLIENT_SECRET_GITHUB` | OAuth2 client secret for GitHub integration | `None` | <Tab title="Email service">
| `CLIENT_SLUG_VERCEL` | OAuth2 slug for Netlify integration | `None` | <Info>When email service is not configured, Infisical will have limited functionality</Info>
| `SENTRY_DSN` | DSN for error-monitoring with Sentry | `None` |
| `INVITE_ONLY_SIGNUP` | If true, users can only sign up if they are invited | `false` | <ParamField query="SMTP_HOST" type="string" default="none" optional>
Hostname to connect to for establishing SMTP connections
</ParamField>
<ParamField query="SMTP_USERNAME" type="string" default="none" optional>
Credential to connect to host (e.g. [email protected])
</ParamField>
<ParamField query="SMTP_PASSWORD" type="string" default="587" optional>
Credential to connect to host
</ParamField>
<ParamField query="SMTP_PORT" type="string" default="587" optional>
Port to connect to for establishing SMTP connections
</ParamField>
<ParamField query="SMTP_SECURE" type="string" default="none" optional>
If true, use TLS when connecting to host. If false, TLS will be used if STARTTLS is supported
</ParamField>
<ParamField query="SMTP_FROM_ADDRESS" type="string" default="none" optional>
Email address to be used for sending emails
</ParamField>
<ParamField query="SMTP_FROM_NAME" type="string" default="none" optional>
Name label to be used in From field (e.g. Team)
</ParamField>
</Tab>
<Tab title="Integrations">
To sync secret to third party services, provide value for the related services
<ParamField query="CLIENT_ID_HEROKU" type="string" default="none" optional>
OAuth2 client ID for Heroku integration
</ParamField>
<ParamField query="CLIENT_SECRET_HEROKU" type="string" default="none" optional>
OAuth2 client secret for Heroku integration
</ParamField>
<ParamField query="CLIENT_ID_VERCEL" type="string" default="none" optional>
OAuth2 client ID for Vercel integration
</ParamField>
<ParamField query="CLIENT_SECRET_VERCEL" type="string" default="none" optional>
OAuth2 client secret for Vercel integration
</ParamField>
<ParamField query="CLIENT_ID_NETLIFY" type="string" default="none" optional>
OAuth2 client ID for Netlify integration
</ParamField>
<ParamField query="CLIENT_SECRET_NETLIFY" type="string" default="none" optional>
OAuth2 client secret for Netlify integration
</ParamField>
<ParamField query="CLIENT_ID_GITHUB" type="string" default="none" optional>
OAuth2 client ID for GitHub integration
</ParamField>
<ParamField query="CLIENT_SECRET_GITHUB" type="string" default="none" optional>
OAuth2 client secret for GitHub integration
</ParamField>
<ParamField query="CLIENT_SLUG_VERCEL" type="string" default="none" optional>
OAuth2 slug for Netlify integration
</ParamField>
</Tab>
<Tab title="Others">
#### JWT
<ParamField query="JWT_SIGNUP_LIFETIME" type="string" default="15m" optional>
JWT token lifetime expressed in seconds or a string describing a time span
</ParamField>
<ParamField query="JWT_REFRESH_LIFETIME" type="string" default="90d" optional>
JWT token lifetime expressed in seconds or a string describing a time span
</ParamField>
<ParamField query="JWT_AUTH_LIFETIME" type="string" default="10d" optional>
JWT token lifetime expressed in seconds or a string describing a time span
</ParamField>
<ParamField query="JWT_MFA_LIFETIME" type="string" default="5m" optional>
JWT token lifetime expressed in seconds or a string describing a time span
</ParamField>
<ParamField query="MONGO_USERNAME" type="string" default="none" optional></ParamField>
<ParamField query="MONGO_PASSWORD" type="string" default="none" optional></ParamField>
#### Error logging
Infisical uses Sentry to report error logs
<ParamField query="SENTRY_DSN" type="string" default="none" optional></ParamField>
#### Settings
<ParamField query="INVITE_ONLY_SIGNUP" type="string" default="false" optional>
Only allow users who are invited to sign up
</ParamField>
<ParamField query="SITE_URL" type="string" default="none" optional>
Site URL - should be an absolute URL including the protocol (e.g. https://app.infisical.com)
</ParamField>
<ParamField query="TELEMETRY_ENABLED" type="string" default="true" optional></ParamField>
</Tab>
</Tabs>
## Frontend environment variables
<ParamField query="TELEMETRY_ENABLED" type="string" default="true" optional></ParamField>
+184 -22
View File
@@ -1,36 +1,198 @@
--- ---
title: "Overview" title: "Deployment options"
description: "Infisical is an open-source end-to-end encrypted secrets manager that developers can set up within 15 minutes." description: "Explore deployment options for self hosting Infisical"
--- ---
<Info> To meet various compliance requirements, you may want to self-host Infisical instead of using [Infisical Cloud](https://app.infisical.com/).
Self-host vs. Infisical Cloud Self-hosted Infisical allows you to maintain your sensitive information within your own infrastructure and network, ensuring complete control over your data.
Self-hosting Infisical means managing the service yourself, taking care of upgrades, scaling, security, etc. <Tabs>
<Tab title="Quick deploy AWS">
<iframe width="560" height="315" src="https://www.youtube.com/embed/jR-gM7vIY2c" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" allowfullscreen></iframe>
This deployment option will use AWS Cloudformation to auto deploy an instance of Infisical on a single EC2 via Docker Compose.
If you're less technical and looking for a hands-free experience with minimal overhead then we recommend Infisical Cloud. **Resources that will be provisioned**
- 1 EC2 instance
- 1 DocumentDB cluster
- 1 DocumentDB instance
- Security groups
Infisical Cloud also comes with some extra features unavailable in the self-hosted edition. You can find more information about Infisical Cloud's offering on the pricing page. <a href="https://us-east-1.console.aws.amazon.com/cloudformation/home?region=us-east-1#/stacks/create/review?templateURL=https://ec2-instance-cloudformation.s3.amazonaws.com/cloudformation.template&stackName=infisical">
<img width="200" src="../images/deploy-aws-button.png" />
</a>
</Info> </Tab>
<Tab title="Quick deploy Digital Ocean">
<Note>This deployment option is highly available</Note>
Coming soon
</Tab>
<Tab title="Helm Kubernetes">
<Note>This deployment option is highly available</Note>
**Prerequisites**
- You have understanding of [Kubernetes](https://kubernetes.io/)
- You have understanding of [Helm package manager](https://helm.sh/)
- You have [kubectl](https://kubernetes.io/docs/reference/kubectl/kubectl/) installed and connected to your kubernetes cluster
## Deployment options
Infisical can be deployed on a Linux VM with docker-compose and Kubernetes. We're rolling out more specific deployment options for DigitalOcean, AWS, GCP, and Azure soon. #### 1. Fill our environment variables
<CardGroup cols={2}> Before you can deploy the Helm chart, you must fill out the required environment variables. To do so, please copy the below file to a `.yaml` file.
<Card title="Any Linux" icon="square-1" color="#ea5a0c" href="/self-hosting/deployments/linux"> Refer to the available [environment variables](../../self-hosting/configuration/envars) to learn more
Deploy to any Linux with Docker
</Card>
<Card title="Kubernetes" icon="square-2" color="#0285c7" href="/self-hosting/deployments/kubernetes">
Deploy to your Kubernetes cluster
</Card>
</CardGroup>
## Telemetry <Accordion title="values.yaml">
[View all available Helm chart values parameters](https://github.com/Infisical/infisical/tree/main/helm-charts/infisical)
```yaml
frontend:
enabled: true
name: frontend
podAnnotations: {}
deploymentAnnotations: {}
replicaCount: 2
image:
repository: infisical/frontend
tag: "latest"
pullPolicy: IfNotPresent
kubeSecretRef: ""
service:
annotations: {}
type: ClusterIP
nodePort: ""
Infisical collects telemetry data about general usage. frontendEnvironmentVariables:
SITE_URL: infisical.local
The data helps us understand how the product is doing and guide our product development to create the best possible platform; it also helps us demonstrate growth for investors as we support Infisical as open-source software. backend:
enabled: true
name: backend
podAnnotations: {}
deploymentAnnotations: {}
replicaCount: 2
image:
repository: infisical/backend
tag: "latest"
pullPolicy: IfNotPresent
kubeSecretRef: ""
service:
annotations: {}
type: ClusterIP
nodePort: ""
To opt out of telemetry, you can set `TELEMETRY_ENABLED=false` within the [environment variables](./configuration/envars). backendEnvironmentVariables:
ENCRYPTION_KEY: MUST_REPLACE
JWT_SIGNUP_SECRET: MUST_REPLACE
JWT_REFRESH_SECRET: MUST_REPLACE
JWT_AUTH_SECRET: MUST_REPLACE
JWT_SERVICE_SECRET: MUST_REPLACE
SMTP_HOST: MUST_REPLACE
SMTP_PORT: 587
SMTP_SECURE: false
SMTP_FROM_NAME: Infisical
SMTP_FROM_ADDRESS: MUST_REPLACE
SMTP_USERNAME: MUST_REPLACE
SMTP_PASSWORD: MUST_REPLACE
SITE_URL: infisical.local
## Mongo DB persistence
mongodb:
enabled: true
## By default the backend will be connected to a Mongo instance within the cluster
## However, it is recommended to add a managed document DB connection string for production-use (DBaaS)
## Learn about connection string type here https://www.mongodb.com/docs/manual/reference/connection-string/
## e.g. "mongodb://<user>:<pass>@<host>:<port>/<database-name>"
mongodbConnection:
externalMongoDBConnectionString: ""
ingress:
enabled: true
annotations:
kubernetes.io/ingress.class: "nginx"
# cert-manager.io/issuer: letsencrypt-nginx
hostName: infisical.local ## <- Replace with your own domain
frontend:
path: /
pathType: Prefix
backend:
path: /api
pathType: Prefix
tls: []
# - secretName: letsencrypt-nginx
# hosts:
# - infisical.local
mailhog:
enabled: false
```
</Accordion>
Once you have a local copy of the values file, fill our the required environment variables and save the file.
#### 2. Install Infisical Helm repository
```bash
helm repo add infisical-helm-charts 'https://dl.cloudsmith.io/public/infisical/helm-charts/helm/charts/'
helm repo update
```
#### 3. Install the Helm chart
By default, the helm chart will be installed on your default namespace. If you wish to install the Chart on a different namespace, you may specify
that by adding the `--namespace <namespace-to-install-to>` to your `helm install` command.
```bash
## Installs to default namespace
helm install infisical-helm-charts/infisical --generate-name --values <path to the values.yaml you downloaded/created in step 2>
```
<Note>
If you have not filled out all of the required environment variables, you will see an error message prompting you to
do so.
</Note>
#### 4. Your Infisical installation is complete and should be running on the host name you specified in Ingress in `values.yaml`.
</Tab>
<Tab title="Bare Docker Compose">
1. Install Docker on your VM
```bash
# Example in ubuntu
apt-get update
apt-get upgrade
apt install docker-compose
```
2. Download the required files
```bash
# Download env file template
wget -O .env https://raw.githubusercontent.com/Infisical/infisical/main/.env.example
# Download docker compose template
wget -O docker-compose.yml https://raw.githubusercontent.com/Infisical/infisical/main/docker-compose.yml
# Download nginx config
mkdir nginx && wget -O ./nginx/default.conf https://raw.githubusercontent.com/Infisical/infisical/main/nginx/default.dev.conf
```
3. Tweak the `.env` according to your preferences. Refer to the available [environment variables](../../self-hosting/configuration/envars)
```bash
# update environment variables like mongo login
nano .env
```
4. Get the service up and running.
```bash
# Start up services in detached mode
docker-compose -f docker-compose.yml up -d
```
5. Your Infisical installation is complete and should be running on [http://localhost:80](http://localhost:80). Please note that the containers are not exposed to the internet and only bind to the localhost. It's up to you to configure a firewall, SSL certificates, and implement any additional security measures.
</Tab>
</Tabs>
+52 -11
View File
@@ -1,5 +1,5 @@
{ {
"name": "npm-proj-1677883018530-0.7603125731052582NtcmfK", "name": "frontend",
"lockfileVersion": 2, "lockfileVersion": 2,
"requires": true, "requires": true,
"packages": { "packages": {
@@ -46,6 +46,7 @@
"gray-matter": "^4.0.3", "gray-matter": "^4.0.3",
"http-proxy": "^1.18.1", "http-proxy": "^1.18.1",
"i18next": "^22.4.9", "i18next": "^22.4.9",
"infisical-node": "^1.0.37",
"jspdf": "^2.5.1", "jspdf": "^2.5.1",
"jsrp": "^0.2.4", "jsrp": "^0.2.4",
"markdown-it": "^13.0.1", "markdown-it": "^13.0.1",
@@ -13367,6 +13368,26 @@
"node": ">=8" "node": ">=8"
} }
}, },
"node_modules/infisical-node": {
"version": "1.0.37",
"resolved": "https://registry.npmjs.org/infisical-node/-/infisical-node-1.0.37.tgz",
"integrity": "sha512-9ZswN5UovZq46a7Qv/4KmfaAu9pO/TmxxdcEY2PosDIAlXbpfC651hcKr7T8q1iMlRnHLA/gpYkcZMeS0es0rg==",
"dependencies": {
"axios": "^1.3.3",
"tweetnacl": "^1.0.3",
"tweetnacl-util": "^0.15.1"
}
},
"node_modules/infisical-node/node_modules/axios": {
"version": "1.3.4",
"resolved": "https://registry.npmjs.org/axios/-/axios-1.3.4.tgz",
"integrity": "sha512-toYm+Bsyl6VC5wSkfkbbNB6ROv7KY93PEBBL6xyDczaIHasAiv4wPqQ/c4RjoQzipxRD2W5g21cOqQulZ7rHwQ==",
"dependencies": {
"follow-redirects": "^1.15.0",
"form-data": "^4.0.0",
"proxy-from-env": "^1.1.0"
}
},
"node_modules/inflight": { "node_modules/inflight": {
"version": "1.0.6", "version": "1.0.6",
"resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz",
@@ -17363,8 +17384,7 @@
"node_modules/proxy-from-env": { "node_modules/proxy-from-env": {
"version": "1.1.0", "version": "1.1.0",
"resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz",
"integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==", "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg=="
"dev": true
}, },
"node_modules/pump": { "node_modules/pump": {
"version": "3.0.0", "version": "3.0.0",
@@ -21761,9 +21781,9 @@
"integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==" "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ=="
}, },
"node_modules/webpack": { "node_modules/webpack": {
"version": "5.75.0", "version": "5.76.1",
"resolved": "https://registry.npmjs.org/webpack/-/webpack-5.75.0.tgz", "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.76.1.tgz",
"integrity": "sha512-piaIaoVJlqMsPtX/+3KTTO6jfvrSYgauFVdt8cr9LTHKmcq/AMd4mhzsiP7ZF/PGRNPGA8336jldh9l2Kt2ogQ==", "integrity": "sha512-4+YIK4Abzv8172/SGqObnUjaIHjLEuUasz9EwQj/9xmPPkYJy2Mh03Q/lJfSD3YLzbxy5FeTq5Uw0323Oh6SJQ==",
"dev": true, "dev": true,
"dependencies": { "dependencies": {
"@types/eslint-scope": "^3.7.3", "@types/eslint-scope": "^3.7.3",
@@ -32078,6 +32098,28 @@
"integrity": "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==", "integrity": "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==",
"dev": true "dev": true
}, },
"infisical-node": {
"version": "1.0.37",
"resolved": "https://registry.npmjs.org/infisical-node/-/infisical-node-1.0.37.tgz",
"integrity": "sha512-9ZswN5UovZq46a7Qv/4KmfaAu9pO/TmxxdcEY2PosDIAlXbpfC651hcKr7T8q1iMlRnHLA/gpYkcZMeS0es0rg==",
"requires": {
"axios": "^1.3.3",
"tweetnacl": "^1.0.3",
"tweetnacl-util": "^0.15.1"
},
"dependencies": {
"axios": {
"version": "1.3.4",
"resolved": "https://registry.npmjs.org/axios/-/axios-1.3.4.tgz",
"integrity": "sha512-toYm+Bsyl6VC5wSkfkbbNB6ROv7KY93PEBBL6xyDczaIHasAiv4wPqQ/c4RjoQzipxRD2W5g21cOqQulZ7rHwQ==",
"requires": {
"follow-redirects": "^1.15.0",
"form-data": "^4.0.0",
"proxy-from-env": "^1.1.0"
}
}
}
},
"inflight": { "inflight": {
"version": "1.0.6", "version": "1.0.6",
"resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz",
@@ -34869,8 +34911,7 @@
"proxy-from-env": { "proxy-from-env": {
"version": "1.1.0", "version": "1.1.0",
"resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz",
"integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==", "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg=="
"dev": true
}, },
"pump": { "pump": {
"version": "3.0.0", "version": "3.0.0",
@@ -38113,9 +38154,9 @@
"integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==" "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ=="
}, },
"webpack": { "webpack": {
"version": "5.75.0", "version": "5.76.1",
"resolved": "https://registry.npmjs.org/webpack/-/webpack-5.75.0.tgz", "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.76.1.tgz",
"integrity": "sha512-piaIaoVJlqMsPtX/+3KTTO6jfvrSYgauFVdt8cr9LTHKmcq/AMd4mhzsiP7ZF/PGRNPGA8336jldh9l2Kt2ogQ==", "integrity": "sha512-4+YIK4Abzv8172/SGqObnUjaIHjLEuUasz9EwQj/9xmPPkYJy2Mh03Q/lJfSD3YLzbxy5FeTq5Uw0323Oh6SJQ==",
"dev": true, "dev": true,
"requires": { "requires": {
"@types/eslint-scope": "^3.7.3", "@types/eslint-scope": "^3.7.3",
+2 -1
View File
@@ -39,8 +39,8 @@
"@reduxjs/toolkit": "^1.8.3", "@reduxjs/toolkit": "^1.8.3",
"@stripe/react-stripe-js": "^1.16.3", "@stripe/react-stripe-js": "^1.16.3",
"@stripe/stripe-js": "^1.46.0", "@stripe/stripe-js": "^1.46.0",
"@types/argon2-browser": "^1.18.1",
"@tanstack/react-query": "^4.23.0", "@tanstack/react-query": "^4.23.0",
"@types/argon2-browser": "^1.18.1",
"add": "^2.0.6", "add": "^2.0.6",
"argon2-browser": "^1.18.0", "argon2-browser": "^1.18.0",
"axios": "^0.27.2", "axios": "^0.27.2",
@@ -53,6 +53,7 @@
"gray-matter": "^4.0.3", "gray-matter": "^4.0.3",
"http-proxy": "^1.18.1", "http-proxy": "^1.18.1",
"i18next": "^22.4.9", "i18next": "^22.4.9",
"infisical-node": "^1.0.37",
"jspdf": "^2.5.1", "jspdf": "^2.5.1",
"jsrp": "^0.2.4", "jsrp": "^0.2.4",
"markdown-it": "^13.0.1", "markdown-it": "^13.0.1",
@@ -2,10 +2,13 @@ import React, { useState } from 'react';
import { useRouter } from 'next/router'; import { useRouter } from 'next/router';
import { useTranslation } from 'next-i18next'; import { useTranslation } from 'next-i18next';
import { useFetchServerStatus } from '@app/hooks/api/serverDetails';
import { usePopUp } from '@app/hooks/usePopUp';
import addUserToOrg from '@app/pages/api/organization/addUserToOrg'; import addUserToOrg from '@app/pages/api/organization/addUserToOrg';
import getWorkspaces from '@app/pages/api/workspace/getWorkspaces'; import getWorkspaces from '@app/pages/api/workspace/getWorkspaces';
import Button from '../basic/buttons/Button'; import Button from '../basic/buttons/Button';
import { EmailServiceSetupModal } from '../v2';
/** /**
* This is the last step of the signup flow. People can optionally invite their teammates here. * This is the last step of the signup flow. People can optionally invite their teammates here.
@@ -14,6 +17,10 @@ export default function TeamInviteStep(): JSX.Element {
const [emails, setEmails] = useState(''); const [emails, setEmails] = useState('');
const { t } = useTranslation(); const { t } = useTranslation();
const router = useRouter(); const router = useRouter();
const {data: serverDetails } = useFetchServerStatus()
const { handlePopUpToggle, popUp, handlePopUpOpen } = usePopUp([
'setUpEmail'
] as const);
// Redirect user to the getting started page // Redirect user to the getting started page
const redirectToHome = async () => { const redirectToHome = async () => {
@@ -62,10 +69,20 @@ export default function TeamInviteStep(): JSX.Element {
</div> </div>
<Button <Button
text={t('signup:step5-send-invites') ?? ''} text={t('signup:step5-send-invites') ?? ''}
onButtonPressed={() => inviteUsers({ emails })} onButtonPressed={() => {
if(serverDetails?.emailConfigured){
inviteUsers({ emails })
}else{
handlePopUpOpen('setUpEmail');
}
}}
size="lg" size="lg"
/> />
</div> </div>
<EmailServiceSetupModal
isOpen={popUp.setUpEmail?.isOpen}
onOpenChange={(isOpen) => handlePopUpToggle('setUpEmail', isOpen)}
/>
</div> </div>
); );
} }
@@ -4,13 +4,11 @@ const POSTHOG_HOST =
process.env.NEXT_PUBLIC_POSTHOG_HOST! || "https://app.posthog.com"; process.env.NEXT_PUBLIC_POSTHOG_HOST! || "https://app.posthog.com";
const STRIPE_PRODUCT_PRO = process.env.NEXT_PUBLIC_STRIPE_PRODUCT_PRO!; const STRIPE_PRODUCT_PRO = process.env.NEXT_PUBLIC_STRIPE_PRODUCT_PRO!;
const STRIPE_PRODUCT_STARTER = process.env.NEXT_PUBLIC_STRIPE_PRODUCT_STARTER!; const STRIPE_PRODUCT_STARTER = process.env.NEXT_PUBLIC_STRIPE_PRODUCT_STARTER!;
const SITE_URL = "https://app.infisical.com";
export { export {
ENV, ENV,
POSTHOG_API_KEY, POSTHOG_API_KEY,
POSTHOG_HOST, POSTHOG_HOST,
SITE_URL,
STRIPE_PRODUCT_PRO, STRIPE_PRODUCT_PRO,
STRIPE_PRODUCT_STARTER STRIPE_PRODUCT_STARTER
}; };
@@ -1,7 +1,5 @@
import { jsPDF } from 'jspdf'; import { jsPDF } from 'jspdf';
import { SITE_URL } from './config';
interface PDFProps { interface PDFProps {
personalName: string; personalName: string;
personalEmail: string; personalEmail: string;
@@ -19,7 +17,7 @@ const yyyy = today.getFullYear();
const todayFormatted = `${mm}/${dd}/${yyyy}`; const todayFormatted = `${mm}/${dd}/${yyyy}`;
function createPdfHeader(doc: jsPDF, personalName : string) { function createPdfHeader(doc: jsPDF, personalName: string) {
doc.setFillColor(255, 255, 255); doc.setFillColor(255, 255, 255);
doc.rect(0, 0, 600, 900, 'F'); doc.rect(0, 0, 600, 900, 'F');
doc.setTextColor(23, 23, 23); doc.setTextColor(23, 23, 23);
@@ -30,6 +28,10 @@ function createPdfHeader(doc: jsPDF, personalName : string) {
} }
function createPdfContent(doc: jsPDF, personalEmail: string, generatedKey: string) { function createPdfContent(doc: jsPDF, personalEmail: string, generatedKey: string) {
const { protocol, hostname, port } = window.location;
const portSuffix = port && port !== '80' ? `:${port}` : '';
const siteURL = `${protocol}//${hostname}${portSuffix}`;
doc.setFontSize(14); doc.setFontSize(14);
doc.text( doc.text(
'In case you get locked out of you Infisical account, you`ll need these account details', 'In case you get locked out of you Infisical account, you`ll need these account details',
@@ -73,7 +75,7 @@ function createPdfContent(doc: jsPDF, personalEmail: string, generatedKey: strin
doc.roundedRect(170, 488, 375, 35, 5, 5, 'F'); doc.roundedRect(170, 488, 375, 35, 5, 5, 'F');
doc.setTextColor(23, 23, 23); doc.setTextColor(23, 23, 23);
doc.setFontSize(14); doc.setFontSize(14);
doc.text(`${SITE_URL}/login`, 180, 420); doc.text(`${siteURL}/login`, 180, 420);
doc.text(personalEmail, 180, 465); doc.text(personalEmail, 180, 465);
doc.text(generatedKey, 180, 510); doc.text(generatedKey, 180, 510);
doc.text('Need help? Contact us at [email protected]', 32, 575); doc.text('Need help? Contact us at [email protected]', 32, 575);
@@ -0,0 +1,21 @@
import { Button } from '../Button';
import { Modal, ModalContent } from '../Modal';
type Props = {
isOpen?: boolean;
onOpenChange?: (isOpen: boolean) => void;
};
export const EmailServiceSetupModal = ({ isOpen, onOpenChange }: Props): JSX.Element => (
<Modal isOpen={isOpen} onOpenChange={onOpenChange}>
<ModalContent title="Email service not configured">
<p className="mb-4 text-bunker-300">
The administrators of this Infisical instance have not yet set up an email service provider required to perform this action
</p>
<a href="https://infisical.com/docs/self-hosting/configuration/email">
<Button className="mr-4">Learn more</Button>
</a>
</ModalContent>
</Modal>
);
@@ -0,0 +1 @@
export { EmailServiceSetupModal } from './EmailServiceSetupModal';
+1
View File
@@ -3,6 +3,7 @@ export * from './Card';
export * from './Checkbox'; export * from './Checkbox';
export * from './DeleteActionModal'; export * from './DeleteActionModal';
export * from './Dropdown'; export * from './Dropdown';
export * from './EmailServiceSetupModal'
export * from './EmptyState'; export * from './EmptyState';
export * from './FormControl'; export * from './FormControl';
export * from './IconButton'; export * from './IconButton';
@@ -0,0 +1 @@
export { useFetchServerStatus } from './queries'
@@ -0,0 +1,19 @@
import {useQuery } from '@tanstack/react-query';
import { apiRequest } from '@app/config/request';
import { ServerStatus } from './types';
// cache key
const serverStatusKeys = {
serverStatus: ['serverStatus'] as const
};
const fetchServerStatus = async () => {
const {data} = await apiRequest.get<ServerStatus>('/api/status');
return data;
};
export const useFetchServerStatus= () => {
return useQuery({ queryKey: serverStatusKeys.serverStatus, queryFn: fetchServerStatus });
}
@@ -0,0 +1,5 @@
export type ServerStatus = {
date: string;
message: string;
emailConfigured: boolean;
};
+7 -1
View File
@@ -83,8 +83,14 @@ export const useAddUserToWs = () => {
export const useAddUserToOrg = () => { export const useAddUserToOrg = () => {
const queryClient = useQueryClient(); const queryClient = useQueryClient();
type Response = {
data: {
message: string,
completeInviteLink: string | undefined
}
}
return useMutation<{}, {}, AddUserToOrgDTO>({ return useMutation<Response, {}, AddUserToOrgDTO>({
mutationFn: (dto) => apiRequest.post(`/api/v1/invite-org/signup`, dto), mutationFn: (dto) => apiRequest.post(`/api/v1/invite-org/signup`, dto),
onSuccess: (_, { organizationId }) => { onSuccess: (_, { organizationId }) => {
queryClient.invalidateQueries(userKeys.getOrgUsers(organizationId)); queryClient.invalidateQueries(userKeys.getOrgUsers(organizationId));
+11
View File
@@ -419,9 +419,20 @@ export default function Dashboard() {
const nameErrors = !newData! const nameErrors = !newData!
.map((secret) => !Number.isNaN(Number(secret.key.charAt(0)))) .map((secret) => !Number.isNaN(Number(secret.key.charAt(0))))
.every((v) => v === false); .every((v) => v === false);
const emptyNameError = !newData!
.map((secret) => secret.key.length === 0)
.every((v) => v === false);
const duplicatesExist = const duplicatesExist =
findDuplicates(data!.map((item: SecretDataProps) => item.key)).length > 0; findDuplicates(data!.map((item: SecretDataProps) => item.key)).length > 0;
if (emptyNameError) {
setSaveLoading(false);
return createNotification({
text: 'You can`t have empty secret names.',
type: 'error'
});
}
if (nameErrors) { if (nameErrors) {
setSaveLoading(false); setSaveLoading(false);
return createNotification({ return createNotification({
+18 -4
View File
@@ -13,6 +13,7 @@ import TeamInviteStep from '@app/components/signup/TeamInviteStep';
import UserInfoStep from '@app/components/signup/UserInfoStep'; import UserInfoStep from '@app/components/signup/UserInfoStep';
import SecurityClient from '@app/components/utilities/SecurityClient'; import SecurityClient from '@app/components/utilities/SecurityClient';
import { getTranslatedStaticProps } from '@app/components/utilities/withTranslateProps'; import { getTranslatedStaticProps } from '@app/components/utilities/withTranslateProps';
import { useFetchServerStatus } from '@app/hooks/api/serverDetails';
import checkEmailVerificationCode from './api/auth/CheckEmailVerificationCode'; import checkEmailVerificationCode from './api/auth/CheckEmailVerificationCode';
import getWorkspaces from './api/workspace/getWorkspaces'; import getWorkspaces from './api/workspace/getWorkspaces';
@@ -25,10 +26,12 @@ export default function SignUp() {
const [password, setPassword] = useState(''); const [password, setPassword] = useState('');
const [firstName, setFirstName] = useState(''); const [firstName, setFirstName] = useState('');
const [lastName, setLastName] = useState(''); const [lastName, setLastName] = useState('');
const [code, setCode] = useState(''); const [code, setCode] = useState('123456');
const [codeError, setCodeError] = useState(false); const [codeError, setCodeError] = useState(false);
const [step, setStep] = useState(1); const [step, setStep] = useState(1);
const router = useRouter(); const router = useRouter();
const {data: serverDetails } = useFetchServerStatus()
const { t } = useTranslation(); const { t } = useTranslation();
@@ -68,6 +71,19 @@ export default function SignUp() {
} }
}; };
// when email service is not configured, skip step 2 and 5
useEffect(() => {
if (!serverDetails?.emailConfigured && step === 2){
incrementStep()
}
if (!serverDetails?.emailConfigured && step === 5){
getWorkspaces().then((userWorkspaces)=>{
router.push(`/dashboard/${userWorkspaces[0]._id}`);
});
}
}, [step]);
return ( return (
<div className="bg-bunker-800 h-screen flex flex-col items-center justify-center"> <div className="bg-bunker-800 h-screen flex flex-col items-center justify-center">
<Head> <Head>
@@ -111,9 +127,7 @@ export default function SignUp() {
password={password} password={password}
name={`${firstName} ${lastName}`} name={`${firstName} ${lastName}`}
/> />
) : ( ) : (serverDetails?.emailConfigured ? <TeamInviteStep /> : "")}
<TeamInviteStep />
)}
</form> </form>
</div> </div>
</div> </div>
+19 -1
View File
@@ -6,12 +6,19 @@ import Link from 'next/link';
import Button from '@app/components/basic/buttons/Button'; import Button from '@app/components/basic/buttons/Button';
import InputField from '@app/components/basic/InputField'; import InputField from '@app/components/basic/InputField';
import { getTranslatedStaticProps } from '@app/components/utilities/withTranslateProps'; import { getTranslatedStaticProps } from '@app/components/utilities/withTranslateProps';
import { EmailServiceSetupModal } from '@app/components/v2';
import { usePopUp } from '@app/hooks';
import { useFetchServerStatus } from '@app/hooks/api/serverDetails';
import SendEmailOnPasswordReset from './api/auth/SendEmailOnPasswordReset'; import SendEmailOnPasswordReset from './api/auth/SendEmailOnPasswordReset';
export default function VerifyEmail() { export default function VerifyEmail() {
const [email, setEmail] = useState(''); const [email, setEmail] = useState('');
const [step, setStep] = useState(1); const [step, setStep] = useState(1);
const {data: serverDetails } = useFetchServerStatus()
const { handlePopUpToggle, popUp, handlePopUpOpen } = usePopUp([
'setUpEmail'
] as const);
/** /**
* This function sends the verification email and forwards a user to the next step. * This function sends the verification email and forwards a user to the next step.
@@ -63,7 +70,13 @@ export default function VerifyEmail() {
</div> </div>
<div className="flex flex-col items-center justify-center w-full md:p-2 max-h-20 max-w-md mt-4 mx-auto text-sm"> <div className="flex flex-col items-center justify-center w-full md:p-2 max-h-20 max-w-md mt-4 mx-auto text-sm">
<div className="text-l mt-6 m-8 px-8 py-3 text-lg"> <div className="text-l mt-6 m-8 px-8 py-3 text-lg">
<Button text="Continue" onButtonPressed={sendVerificationEmail} size="lg" /> <Button text="Continue" onButtonPressed={()=>{
if (serverDetails?.emailConfigured){
sendVerificationEmail()
} else {
handlePopUpOpen('setUpEmail');
}
}} size="lg" />
</div> </div>
</div> </div>
</div> </div>
@@ -80,6 +93,11 @@ export default function VerifyEmail() {
</div> </div>
</div> </div>
)} )}
<EmailServiceSetupModal
isOpen={popUp.setUpEmail?.isOpen}
onOpenChange={(isOpen) => handlePopUpToggle('setUpEmail', isOpen)}
/>
</div> </div>
); );
} }
@@ -1,4 +1,5 @@
/* eslint-disable @typescript-eslint/no-unused-vars */ /* eslint-disable @typescript-eslint/no-unused-vars */
import { useState } from 'react';
import { useTranslation } from 'next-i18next'; import { useTranslation } from 'next-i18next';
import { plans } from 'public/data/frequentConstants'; import { plans } from 'public/data/frequentConstants';
@@ -51,6 +52,8 @@ export const OrgSettingsPage = () => {
const addIncidentContact = useAddIncidentContact(); const addIncidentContact = useAddIncidentContact();
const removeIncidentContact = useDeleteIncidentContact(); const removeIncidentContact = useDeleteIncidentContact();
const [completeInviteLink, setcompleteInviteLink] = useState<string|undefined>("")
const isMoreUsersNotAllowed = const isMoreUsersNotAllowed =
(orgUsers || []).length >= 5 && (orgUsers || []).length >= 5 &&
subscriptionPlan === plans.starter && subscriptionPlan === plans.starter &&
@@ -96,11 +99,16 @@ export const OrgSettingsPage = () => {
if (!currentOrg?._id) return; if (!currentOrg?._id) return;
try { try {
await addUserToOrg.mutateAsync({ organizationId: currentOrg?._id, inviteeEmail: email }); const {data} = await addUserToOrg.mutateAsync({ organizationId: currentOrg?._id, inviteeEmail: email });
createNotification({ setcompleteInviteLink(data?.completeInviteLink)
text: 'Successfully invited user to the organization.',
type: 'success' // only show this notification when email is configured. A [completeInviteLink] will not be sent if smtp is configured
}); if (!data.completeInviteLink){
createNotification({
text: 'Successfully invited user to the organization.',
type: 'success'
});
}
} catch (error) { } catch (error) {
console.error(error); console.error(error);
createNotification({ createNotification({
@@ -247,6 +255,8 @@ export const OrgSettingsPage = () => {
onRemoveMember={onRemoveUserOrgMembership} onRemoveMember={onRemoveUserOrgMembership}
onRoleChange={onUpdateOrgUserRole} onRoleChange={onUpdateOrgUserRole}
onGrantAccess={onGrantUserAccess} onGrantAccess={onGrantUserAccess}
completeInviteLink={completeInviteLink}
setCompleteInviteLink={setcompleteInviteLink}
/> />
</div> </div>
<div className="mb-6 mt-2 flex w-full flex-col items-start rounded-md bg-white/5 px-6 pt-6 pb-6"> <div className="mb-6 mt-2 flex w-full flex-col items-start rounded-md bg-white/5 px-6 pt-6 pb-6">
@@ -13,6 +13,7 @@ import * as yup from 'yup';
import { import {
Button, Button,
DeleteActionModal, DeleteActionModal,
EmailServiceSetupModal,
EmptyState, EmptyState,
FormControl, FormControl,
IconButton, IconButton,
@@ -28,6 +29,7 @@ import {
THead, THead,
Tr} from '@app/components/v2'; Tr} from '@app/components/v2';
import { usePopUp } from '@app/hooks'; import { usePopUp } from '@app/hooks';
import { useFetchServerStatus } from '@app/hooks/api/serverDetails';
import { IncidentContact } from '@app/hooks/api/types'; import { IncidentContact } from '@app/hooks/api/types';
type Props = { type Props = {
@@ -50,9 +52,11 @@ export const OrgIncidentContactsTable = ({
isLoading isLoading
}: Props) => { }: Props) => {
const [searchContact, setSearchContact] = useState(''); const [searchContact, setSearchContact] = useState('');
const {data: serverDetails } = useFetchServerStatus()
const { handlePopUpToggle, popUp, handlePopUpOpen, handlePopUpClose } = usePopUp([ const { handlePopUpToggle, popUp, handlePopUpOpen, handlePopUpClose } = usePopUp([
'addContact', 'addContact',
'removeContact' 'removeContact',
'setUpEmail'
] as const); ] as const);
const { const {
@@ -92,7 +96,13 @@ export const OrgIncidentContactsTable = ({
<div> <div>
<Button <Button
leftIcon={<FontAwesomeIcon icon={faPlus} />} leftIcon={<FontAwesomeIcon icon={faPlus} />}
onClick={() => handlePopUpOpen('addContact')} onClick={() => {
if (serverDetails?.emailConfigured){
handlePopUpOpen('addContact');
} else {
handlePopUpOpen('setUpEmail');
}
}}
> >
Add Contact Add Contact
</Button> </Button>
@@ -180,6 +190,10 @@ export const OrgIncidentContactsTable = ({
onChange={(isOpen) => handlePopUpToggle('removeContact', isOpen)} onChange={(isOpen) => handlePopUpToggle('removeContact', isOpen)}
onDeleteApproved={onRemoveIncidentContact} onDeleteApproved={onRemoveIncidentContact}
/> />
<EmailServiceSetupModal
isOpen={popUp.setUpEmail?.isOpen}
onOpenChange={(isOpen) => handlePopUpToggle('setUpEmail', isOpen)}
/>
</div> </div>
); );
}; };
@@ -1,7 +1,7 @@
import { useMemo, useState } from 'react'; import { Dispatch, SetStateAction, useMemo, useState } from 'react';
import { Controller, useForm } from 'react-hook-form'; import { Controller, useForm } from 'react-hook-form';
import { useRouter } from 'next/router'; import { useRouter } from 'next/router';
import { faMagnifyingGlass, faPlus, faTrash, faUsers } from '@fortawesome/free-solid-svg-icons'; import { faCheck, faCopy, faMagnifyingGlass, faPlus, faTrash, faUsers } from '@fortawesome/free-solid-svg-icons';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { yupResolver } from '@hookform/resolvers/yup'; import { yupResolver } from '@hookform/resolvers/yup';
import * as yup from 'yup'; import * as yup from 'yup';
@@ -9,7 +9,7 @@ import * as yup from 'yup';
import { import {
Button, Button,
DeleteActionModal, DeleteActionModal,
EmptyState, EmailServiceSetupModal, EmptyState,
FormControl, FormControl,
IconButton, IconButton,
Input, Input,
@@ -28,6 +28,7 @@ import {
Tr, Tr,
UpgradePlanModal} from '@app/components/v2'; UpgradePlanModal} from '@app/components/v2';
import { usePopUp } from '@app/hooks'; import { usePopUp } from '@app/hooks';
import { useFetchServerStatus } from '@app/hooks/api/serverDetails';
import { OrgUser, Workspace } from '@app/hooks/api/types'; import { OrgUser, Workspace } from '@app/hooks/api/types';
type Props = { type Props = {
@@ -42,6 +43,8 @@ type Props = {
onGrantAccess: (userId: string, publicKey: string) => Promise<void>; onGrantAccess: (userId: string, publicKey: string) => Promise<void>;
// the current user id to block remove org button // the current user id to block remove org button
userId: string; userId: string;
completeInviteLink: string | undefined,
setCompleteInviteLink: Dispatch<SetStateAction<string | undefined>>
}; };
const addMemberFormSchema = yup.object({ const addMemberFormSchema = yup.object({
@@ -60,14 +63,18 @@ export const OrgMembersTable = ({
onGrantAccess, onGrantAccess,
onRoleChange, onRoleChange,
userId, userId,
isLoading isLoading,
completeInviteLink,
setCompleteInviteLink
}: Props) => { }: Props) => {
const router = useRouter(); const router = useRouter();
const [searchMemberFilter, setSearchMemberFilter] = useState(''); const [searchMemberFilter, setSearchMemberFilter] = useState('');
const {data: serverDetails } = useFetchServerStatus()
const { handlePopUpToggle, popUp, handlePopUpOpen, handlePopUpClose } = usePopUp([ const { handlePopUpToggle, popUp, handlePopUpOpen, handlePopUpClose } = usePopUp([
'addMember', 'addMember',
'removeMember', 'removeMember',
'upgradePlan' 'upgradePlan',
'setUpEmail'
] as const); ] as const);
const { const {
@@ -79,8 +86,11 @@ export const OrgMembersTable = ({
const onAddMember = async ({ email }: TAddMemberForm) => { const onAddMember = async ({ email }: TAddMemberForm) => {
await onInviteMember(email); await onInviteMember(email);
handlePopUpClose('addMember'); if (serverDetails?.emailConfigured){
reset(); handlePopUpClose('addMember');
}
reset();
}; };
const onRemoveOrgMemberApproved = async () => { const onRemoveOrgMemberApproved = async () => {
@@ -106,6 +116,11 @@ export const OrgMembersTable = ({
[members, searchMemberFilter] [members, searchMemberFilter]
); );
const copyTokenToClipboard = () => {
navigator.clipboard.writeText(completeInviteLink as string);
// setIsTokenCopied.on();
};
return ( return (
<div className="w-full"> <div className="w-full">
<div className="mb-4 flex"> <div className="mb-4 flex">
@@ -121,11 +136,16 @@ export const OrgMembersTable = ({
<Button <Button
leftIcon={<FontAwesomeIcon icon={faPlus} />} leftIcon={<FontAwesomeIcon icon={faPlus} />}
onClick={() => { onClick={() => {
if (isMoreUserNotAllowed) { // if (serverDetails?.emailConfigured){
handlePopUpOpen('upgradePlan'); if (isMoreUserNotAllowed) {
} else { handlePopUpOpen('upgradePlan');
handlePopUpOpen('addMember'); } else {
} reset();
handlePopUpOpen('addMember');
}
// } else {
// handlePopUpOpen('setUpEmail');
// }
}} }}
> >
Add Member Add Member
@@ -173,7 +193,7 @@ export const OrgMembersTable = ({
<SelectItem value="member">member</SelectItem> <SelectItem value="member">member</SelectItem>
</Select> </Select>
)} )}
{(status === 'invited' || status === 'verified') && ( {((status === 'invited' || status === 'verified') && serverDetails?.emailConfigured) && (
<Button className='w-40' colorSchema="secondary" onClick={() => onInviteMember(email)}> <Button className='w-40' colorSchema="secondary" onClick={() => onInviteMember(email)}>
Resend Invite Resend Invite
</Button> </Button>
@@ -234,20 +254,23 @@ export const OrgMembersTable = ({
isOpen={popUp?.addMember?.isOpen} isOpen={popUp?.addMember?.isOpen}
onOpenChange={(isOpen) => { onOpenChange={(isOpen) => {
handlePopUpToggle('addMember', isOpen); handlePopUpToggle('addMember', isOpen);
reset(); setCompleteInviteLink(undefined)
}} }}
> >
<ModalContent <ModalContent
title={`Invite others to ${orgName}`} title={`Invite others to ${orgName}`}
subTitle={ subTitle={
<> <div>
An invite is specific to an email address and expires after 1 day. {!completeInviteLink && <div>
<br /> An invite is specific to an email address and expires after 1 day.
For security reasons, you will need to separately add members to projects. <br />
</> For security reasons, you will need to separately add members to projects.
</div>}
{completeInviteLink && "This Infisical instance does not have a email provider setup. Please share this invite link with the invitee manually"}
</div>
} }
> >
<form onSubmit={handleSubmit(onAddMember)}> {!completeInviteLink && <form onSubmit={handleSubmit(onAddMember)} >
<Controller <Controller
control={control} control={control}
defaultValue="" defaultValue=""
@@ -276,7 +299,22 @@ export const OrgMembersTable = ({
Cancel Cancel
</Button> </Button>
</div> </div>
</form> </form>}
{
completeInviteLink &&
<div className="mt-2 mb-3 mr-2 flex items-center justify-end rounded-md bg-white/[0.07] p-2 text-base text-gray-400">
<p className="mr-4 break-all">{completeInviteLink}</p>
<IconButton
ariaLabel="copy icon"
colorSchema="secondary"
className="group relative"
onClick={copyTokenToClipboard}
>
<FontAwesomeIcon icon={false ? faCheck : faCopy} />
<span className="absolute -left-8 -top-20 hidden w-28 translate-y-full rounded-md bg-bunker-800 py-2 pl-3 text-center text-sm text-gray-400 group-hover:flex group-hover:animate-fadeIn">click to copy</span>
</IconButton>
</div>
}
</ModalContent> </ModalContent>
</Modal> </Modal>
<DeleteActionModal <DeleteActionModal
@@ -291,6 +329,10 @@ export const OrgMembersTable = ({
onOpenChange={(isOpen) => handlePopUpToggle('upgradePlan', isOpen)} onOpenChange={(isOpen) => handlePopUpToggle('upgradePlan', isOpen)}
text="You can add custom environments if you switch to Infisical's Team plan." text="You can add custom environments if you switch to Infisical's Team plan."
/> />
<EmailServiceSetupModal
isOpen={popUp.setUpEmail?.isOpen}
onOpenChange={(isOpen) => handlePopUpToggle('setUpEmail', isOpen)}
/>
</div> </div>
); );
}; };
@@ -1,7 +1,9 @@
import { useEffect, useState } from 'react'; import { useEffect, useState } from 'react';
import { useNotificationContext } from '@app/components/context/Notifications/NotificationProvider'; import { useNotificationContext } from '@app/components/context/Notifications/NotificationProvider';
import { Checkbox } from '@app/components/v2'; import { Checkbox, EmailServiceSetupModal } from '@app/components/v2';
import { useFetchServerStatus } from '@app/hooks/api/serverDetails';
import { usePopUp } from '@app/hooks/usePopUp';
import { useGetUser } from '../../../../hooks/api'; import { useGetUser } from '../../../../hooks/api';
import { User } from '../../../../hooks/api/types'; import { User } from '../../../../hooks/api/types';
@@ -11,6 +13,11 @@ export const SecuritySection = () => {
const [isMfaEnabled, setIsMfaEnabled] = useState(false); const [isMfaEnabled, setIsMfaEnabled] = useState(false);
const { data: user } = useGetUser(); const { data: user } = useGetUser();
const { createNotification } = useNotificationContext(); const { createNotification } = useNotificationContext();
const { handlePopUpToggle, popUp, handlePopUpOpen } = usePopUp([
'setUpEmail'
] as const);
const {data: serverDetails } = useFetchServerStatus()
useEffect(() => { useEffect(() => {
if (user && typeof user.isMfaEnabled !== 'undefined') { if (user && typeof user.isMfaEnabled !== 'undefined') {
@@ -42,6 +49,7 @@ export const SecuritySection = () => {
} }
return ( return (
<>
<form> <form>
<div className="mb-6 mt-2 flex w-full flex-col items-start rounded-md bg-white/5 px-6 pb-6 pt-2"> <div className="mb-6 mt-2 flex w-full flex-col items-start rounded-md bg-white/5 px-6 pb-6 pt-2">
<p className="mb-4 mt-2 text-xl font-semibold"> <p className="mb-4 mt-2 text-xl font-semibold">
@@ -52,12 +60,21 @@ export const SecuritySection = () => {
id="isTwoFAEnabled" id="isTwoFAEnabled"
isChecked={isMfaEnabled} isChecked={isMfaEnabled}
onCheckedChange={(state) => { onCheckedChange={(state) => {
toggleMfa(state as boolean); if (serverDetails?.emailConfigured){
toggleMfa(state as boolean);
} else {
handlePopUpOpen('setUpEmail');
}
}} }}
> >
Enable 2-factor authentication via your personal email. Enable 2-factor authentication via your personal email.
</Checkbox> </Checkbox>
</div> </div>
</form> </form>
<EmailServiceSetupModal
isOpen={popUp.setUpEmail?.isOpen}
onOpenChange={(isOpen) => handlePopUpToggle('setUpEmail', isOpen)}
/>
</>
); );
}; };