diff --git a/.env.example b/.env.example index c893713f4..79fca5686 100644 --- a/.env.example +++ b/.env.example @@ -16,9 +16,6 @@ JWT_AUTH_LIFETIME= JWT_REFRESH_LIFETIME= JWT_SIGNUP_LIFETIME= -# Optional lifetimes for OTP expressed in seconds -EMAIL_TOKEN_LIFETIME= - # MongoDB # 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 diff --git a/README.md b/README.md index 5014906c1..449b8cce6 100644 --- a/README.md +++ b/README.md @@ -25,7 +25,7 @@ git commit activity - Cloudsmith downloads + Cloudsmith downloads Slack community channel diff --git a/backend/__tests__/healthcheck.test.ts b/backend/__tests__/healthcheck.test.ts deleted file mode 100644 index 234d2d8eb..000000000 --- a/backend/__tests__/healthcheck.test.ts +++ /dev/null @@ -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); - }); -}); diff --git a/backend/environment.d.ts b/backend/environment.d.ts index 497902def..3793552b1 100644 --- a/backend/environment.d.ts +++ b/backend/environment.d.ts @@ -4,7 +4,6 @@ declare global { namespace NodeJS { interface ProcessEnv { PORT: string; - EMAIL_TOKEN_LIFETIME: string; ENCRYPTION_KEY: string; SALT_ROUNDS: string; JWT_AUTH_LIFETIME: string; diff --git a/backend/jest.config.ts b/backend/jest.config.ts new file mode 100644 index 000000000..8a72ad84c --- /dev/null +++ b/backend/jest.config.ts @@ -0,0 +1,9 @@ +export default { + preset: 'ts-jest', + testEnvironment: 'node', + collectCoverageFrom: ['src/*.{js,ts}', '!**/node_modules/**'], + modulePaths: ['/src'], + testMatch: ['/tests/**/*.test.ts'], + setupFiles: ['/test-resources/env-vars.js'], + setupFilesAfterEnv: ['/tests/setupTests.ts'] +}; diff --git a/backend/package-lock.json b/backend/package-lock.json index cef782117..b575c65c8 100644 --- a/backend/package-lock.json +++ b/backend/package-lock.json @@ -12,7 +12,7 @@ "@aws-sdk/client-secrets-manager": "^3.267.0", "@godaddy/terminus": "^4.11.2", "@octokit/rest": "^19.0.5", - "@sentry/node": "^7.14.0", + "@sentry/node": "^7.39.0", "@sentry/tracing": "^7.19.0", "@types/crypto-js": "^4.1.1", "@types/libsodium-wrappers": "^0.7.10", @@ -32,6 +32,7 @@ "express-validator": "^6.14.2", "handlebars": "^4.7.7", "helmet": "^5.1.1", + "infisical-node": "^1.0.37", "js-yaml": "^4.1.0", "jsonwebtoken": "^9.0.0", "jsrp": "^0.2.4", @@ -39,13 +40,13 @@ "lodash": "^4.17.21", "mongoose": "^6.7.2", "nodemailer": "^6.8.0", - "posthog-node": "^2.2.2", + "posthog-node": "^2.5.4", "query-string": "^7.1.3", "request-ip": "^3.3.0", "rimraf": "^3.0.2", "stripe": "^10.7.0", "swagger-autogen": "^2.22.0", - "swagger-ui-express": "^4.6.0", + "swagger-ui-express": "^4.6.2", "tweetnacl": "^1.0.3", "tweetnacl-util": "^0.15.1", "typescript": "^4.9.3", @@ -2799,13 +2800,13 @@ "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==" }, "node_modules/@sentry/node": { - "version": "7.38.0", - "resolved": "https://registry.npmjs.org/@sentry/node/-/node-7.38.0.tgz", - "integrity": "sha512-jNIN6NZvgzn/oms8RQzffjX8Z0LQDTN6N28nnhzqGCvnfmS1QtTt0FlU+pTuFXZNNSjfGy4XMXMYvLlbvhm2bg==", + "version": "7.39.0", + "resolved": "https://registry.npmjs.org/@sentry/node/-/node-7.39.0.tgz", + "integrity": "sha512-oe1OBxgs6t/FizjxkSPtuvJv5wJMO+mLENZkiE0PpBD56JyZrWK48kYIt2ccWAfk6Vh235/oIpmqET150xB4lQ==", "dependencies": { - "@sentry/core": "7.38.0", - "@sentry/types": "7.38.0", - "@sentry/utils": "7.38.0", + "@sentry/core": "7.39.0", + "@sentry/types": "7.39.0", + "@sentry/utils": "7.39.0", "cookie": "^0.4.1", "https-proxy-agent": "^5.0.0", "lru_map": "^0.3.3", @@ -2815,6 +2816,39 @@ "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": { "version": "1.14.1", "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", @@ -5973,6 +6007,16 @@ "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": { "version": "1.0.6", "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", @@ -10489,9 +10533,9 @@ } }, "node_modules/posthog-node": { - "version": "2.5.3", - "resolved": "https://registry.npmjs.org/posthog-node/-/posthog-node-2.5.3.tgz", - "integrity": "sha512-kDmBjQHguPrh/rUTKmB0+Hj7C3fq2t+/fcfQkDBGz0f0fEF2WxV5yyxRmd2IF/hFmHxMrGLDkEVjKr78B+judg==", + "version": "2.5.4", + "resolved": "https://registry.npmjs.org/posthog-node/-/posthog-node-2.5.4.tgz", + "integrity": "sha512-CdywlVh0CZU05/3MrBc0qY/zsLdU2X9XSz/yL1qMRhbyZhD8lrnuGlI69G2cpzZtli6S/nu64wcmULz/mFFA5w==", "dependencies": { "axios": "^0.27.0" }, @@ -11472,9 +11516,9 @@ "integrity": "sha512-4J4XekQG0ol4/TyUzMfksrWsMTbw/7JYlT+SFaX7H0xamd1OeuVlUSb/Cbq4qdDx1lc+uLZQW7u2mlImcE8c+w==" }, "node_modules/swagger-ui-express": { - "version": "4.6.1", - "resolved": "https://registry.npmjs.org/swagger-ui-express/-/swagger-ui-express-4.6.1.tgz", - "integrity": "sha512-Pss7YNFKNdq66XKNjRe4IRXKKYNx/LvOSml9TdrZ8/78UpxUHIp9JoXpXWA5Z4L+SCmX63DZ9IPlQ8nnRuncvA==", + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/swagger-ui-express/-/swagger-ui-express-4.6.2.tgz", + "integrity": "sha512-MHIOaq9JrTTB3ygUJD+08PbjM5Tt/q7x80yz9VTFIatw8j5uIWKcr90S0h5NLMzFEDC6+eVprtoeA5MDZXCUKQ==", "dependencies": { "swagger-ui-dist": ">=4.11.0" }, @@ -14350,19 +14394,43 @@ } }, "@sentry/node": { - "version": "7.38.0", - "resolved": "https://registry.npmjs.org/@sentry/node/-/node-7.38.0.tgz", - "integrity": "sha512-jNIN6NZvgzn/oms8RQzffjX8Z0LQDTN6N28nnhzqGCvnfmS1QtTt0FlU+pTuFXZNNSjfGy4XMXMYvLlbvhm2bg==", + "version": "7.39.0", + "resolved": "https://registry.npmjs.org/@sentry/node/-/node-7.39.0.tgz", + "integrity": "sha512-oe1OBxgs6t/FizjxkSPtuvJv5wJMO+mLENZkiE0PpBD56JyZrWK48kYIt2ccWAfk6Vh235/oIpmqET150xB4lQ==", "requires": { - "@sentry/core": "7.38.0", - "@sentry/types": "7.38.0", - "@sentry/utils": "7.38.0", + "@sentry/core": "7.39.0", + "@sentry/types": "7.39.0", + "@sentry/utils": "7.39.0", "cookie": "^0.4.1", "https-proxy-agent": "^5.0.0", "lru_map": "^0.3.3", "tslib": "^1.9.3" }, "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": { "version": "1.14.1", "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", @@ -16758,6 +16826,16 @@ "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", "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": { "version": "1.0.6", "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", @@ -20028,9 +20106,9 @@ } }, "posthog-node": { - "version": "2.5.3", - "resolved": "https://registry.npmjs.org/posthog-node/-/posthog-node-2.5.3.tgz", - "integrity": "sha512-kDmBjQHguPrh/rUTKmB0+Hj7C3fq2t+/fcfQkDBGz0f0fEF2WxV5yyxRmd2IF/hFmHxMrGLDkEVjKr78B+judg==", + "version": "2.5.4", + "resolved": "https://registry.npmjs.org/posthog-node/-/posthog-node-2.5.4.tgz", + "integrity": "sha512-CdywlVh0CZU05/3MrBc0qY/zsLdU2X9XSz/yL1qMRhbyZhD8lrnuGlI69G2cpzZtli6S/nu64wcmULz/mFFA5w==", "requires": { "axios": "^0.27.0" }, @@ -20758,9 +20836,9 @@ "integrity": "sha512-4J4XekQG0ol4/TyUzMfksrWsMTbw/7JYlT+SFaX7H0xamd1OeuVlUSb/Cbq4qdDx1lc+uLZQW7u2mlImcE8c+w==" }, "swagger-ui-express": { - "version": "4.6.1", - "resolved": "https://registry.npmjs.org/swagger-ui-express/-/swagger-ui-express-4.6.1.tgz", - "integrity": "sha512-Pss7YNFKNdq66XKNjRe4IRXKKYNx/LvOSml9TdrZ8/78UpxUHIp9JoXpXWA5Z4L+SCmX63DZ9IPlQ8nnRuncvA==", + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/swagger-ui-express/-/swagger-ui-express-4.6.2.tgz", + "integrity": "sha512-MHIOaq9JrTTB3ygUJD+08PbjM5Tt/q7x80yz9VTFIatw8j5uIWKcr90S0h5NLMzFEDC6+eVprtoeA5MDZXCUKQ==", "requires": { "swagger-ui-dist": ">=4.11.0" } diff --git a/backend/package.json b/backend/package.json index 90db51ece..3d36797a5 100644 --- a/backend/package.json +++ b/backend/package.json @@ -3,7 +3,7 @@ "@aws-sdk/client-secrets-manager": "^3.267.0", "@godaddy/terminus": "^4.11.2", "@octokit/rest": "^19.0.5", - "@sentry/node": "^7.14.0", + "@sentry/node": "^7.39.0", "@sentry/tracing": "^7.19.0", "@types/crypto-js": "^4.1.1", "@types/libsodium-wrappers": "^0.7.10", @@ -23,6 +23,7 @@ "express-validator": "^6.14.2", "handlebars": "^4.7.7", "helmet": "^5.1.1", + "infisical-node": "^1.0.37", "js-yaml": "^4.1.0", "jsonwebtoken": "^9.0.0", "jsrp": "^0.2.4", @@ -30,13 +31,13 @@ "lodash": "^4.17.21", "mongoose": "^6.7.2", "nodemailer": "^6.8.0", - "posthog-node": "^2.2.2", + "posthog-node": "^2.5.4", "query-string": "^7.1.3", "request-ip": "^3.3.0", "rimraf": "^3.0.2", "stripe": "^10.7.0", "swagger-autogen": "^2.22.0", - "swagger-ui-express": "^4.6.0", + "swagger-ui-express": "^4.6.2", "tweetnacl": "^1.0.3", "tweetnacl-util": "^0.15.1", "typescript": "^4.9.3", @@ -100,17 +101,6 @@ "ts-jest": "^29.0.3", "ts-node": "^10.9.1" }, - "jest": { - "preset": "ts-jest", - "testEnvironment": "node", - "collectCoverageFrom": [ - "src/*.{js,ts}", - "!**/node_modules/**" - ], - "setupFiles": [ - "/test-resources/env-vars.js" - ] - }, "jest-junit": { "outputDirectory": "reports", "outputName": "jest-junit.xml", diff --git a/backend/src/app.ts b/backend/src/app.ts deleted file mode 100644 index 550cf08f6..000000000 --- a/backend/src/app.ts +++ /dev/null @@ -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}`) -}); diff --git a/backend/src/config/index.ts b/backend/src/config/index.ts index e3c242130..33066f613 100644 --- a/backend/src/config/index.ts +++ b/backend/src/config/index.ts @@ -1,105 +1,51 @@ -const PORT = process.env.PORT || 4000; -const EMAIL_TOKEN_LIFETIME = parseInt(process.env.EMAIL_TOKEN_LIFETIME! || '86400'); -const INVITE_ONLY_SIGNUP = process.env.INVITE_ONLY_SIGNUP == undefined ? false : process.env.INVITE_ONLY_SIGNUP -const ENCRYPTION_KEY = process.env.ENCRYPTION_KEY!; -const SALT_ROUNDS = parseInt(process.env.SALT_ROUNDS!) || 10; -const JWT_AUTH_LIFETIME = process.env.JWT_AUTH_LIFETIME! || '10d'; -const JWT_AUTH_SECRET = process.env.JWT_AUTH_SECRET!; -const JWT_MFA_LIFETIME = process.env.JWT_MFA_LIFETIME! || '5m'; -const JWT_MFA_SECRET = process.env.JWT_MFA_SECRET!; -const JWT_REFRESH_LIFETIME = process.env.JWT_REFRESH_LIFETIME! || '90d'; -const JWT_REFRESH_SECRET = process.env.JWT_REFRESH_SECRET!; -const JWT_SERVICE_SECRET = process.env.JWT_SERVICE_SECRET!; -const JWT_SIGNUP_LIFETIME = process.env.JWT_SIGNUP_LIFETIME! || '15m'; -const JWT_SIGNUP_SECRET = process.env.JWT_SIGNUP_SECRET!; -const MONGO_URL = process.env.MONGO_URL!; -const NODE_ENV = process.env.NODE_ENV! || 'production'; -const VERBOSE_ERROR_OUTPUT = process.env.VERBOSE_ERROR_OUTPUT! === 'true' && true; -const LOKI_HOST = process.env.LOKI_HOST || undefined; -const CLIENT_ID_AZURE = process.env.CLIENT_ID_AZURE!; -const CLIENT_ID_HEROKU = process.env.CLIENT_ID_HEROKU!; -const CLIENT_ID_VERCEL = process.env.CLIENT_ID_VERCEL!; -const CLIENT_ID_NETLIFY = process.env.CLIENT_ID_NETLIFY!; -const CLIENT_ID_GITHUB = process.env.CLIENT_ID_GITHUB!; -const CLIENT_ID_GITLAB = process.env.CLIENT_ID_GITLAB!; -const CLIENT_SECRET_AZURE = process.env.CLIENT_SECRET_AZURE!; -const CLIENT_SECRET_HEROKU = process.env.CLIENT_SECRET_HEROKU!; -const CLIENT_SECRET_VERCEL = process.env.CLIENT_SECRET_VERCEL!; -const CLIENT_SECRET_NETLIFY = process.env.CLIENT_SECRET_NETLIFY!; -const CLIENT_SECRET_GITHUB = process.env.CLIENT_SECRET_GITHUB!; -const CLIENT_SECRET_GITLAB = process.env.CLIENT_SECRET_GITLAB; -const CLIENT_SLUG_VERCEL = process.env.CLIENT_SLUG_VERCEL!; -const POSTHOG_HOST = process.env.POSTHOG_HOST! || 'https://app.posthog.com'; -const POSTHOG_PROJECT_API_KEY = - process.env.POSTHOG_PROJECT_API_KEY! || - 'phc_nSin8j5q2zdhpFDI1ETmFNUIuTG4DwKVyIigrY10XiE'; -const SENTRY_DSN = process.env.SENTRY_DSN!; -const SITE_URL = process.env.SITE_URL!; -const SMTP_HOST = process.env.SMTP_HOST!; -const SMTP_SECURE = process.env.SMTP_SECURE! === 'true' || false; -const SMTP_PORT = parseInt(process.env.SMTP_PORT!) || 587; -const SMTP_USERNAME = process.env.SMTP_USERNAME!; -const SMTP_PASSWORD = process.env.SMTP_PASSWORD!; -const SMTP_FROM_ADDRESS = process.env.SMTP_FROM_ADDRESS!; -const SMTP_FROM_NAME = process.env.SMTP_FROM_NAME! || 'Infisical'; -const STRIPE_PRODUCT_STARTER = process.env.STRIPE_PRODUCT_STARTER!; -const STRIPE_PRODUCT_PRO = process.env.STRIPE_PRODUCT_PRO!; -const STRIPE_PRODUCT_TEAM = process.env.STRIPE_PRODUCT_TEAM!; -const STRIPE_PUBLISHABLE_KEY = process.env.STRIPE_PUBLISHABLE_KEY!; -const STRIPE_SECRET_KEY = process.env.STRIPE_SECRET_KEY!; -const STRIPE_WEBHOOK_SECRET = process.env.STRIPE_WEBHOOK_SECRET!; -const TELEMETRY_ENABLED = process.env.TELEMETRY_ENABLED! !== '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 -}; +import infisical from 'infisical-node'; +export const getPort = () => infisical.get('PORT')! || 4000; +export const getInviteOnlySignup = () => infisical.get('INVITE_ONLY_SIGNUP')! == undefined ? false : infisical.get('INVITE_ONLY_SIGNUP'); +export const getEncryptionKey = () => infisical.get('ENCRYPTION_KEY')!; +export const getSaltRounds = () => parseInt(infisical.get('SALT_ROUNDS')!) || 10; +export const getJwtAuthLifetime = () => infisical.get('JWT_AUTH_LIFETIME')! || '10d'; +export const getJwtAuthSecret = () => infisical.get('JWT_AUTH_SECRET')!; +export const getJwtMfaLifetime = () => infisical.get('JWT_MFA_LIFETIME')! || '5m'; +export const getJwtMfaSecret = () => infisical.get('JWT_MFA_LIFETIME')! || '5m'; +export const getJwtRefreshLifetime = () => infisical.get('JWT_REFRESH_LIFETIME')! || '90d'; +export const getJwtRefreshSecret = () => infisical.get('JWT_REFRESH_SECRET')!; +export const getJwtServiceSecret = () => infisical.get('JWT_SERVICE_SECRET')!; +export const getJwtSignupLifetime = () => infisical.get('JWT_SIGNUP_LIFETIME')! || '15m'; +export const getJwtSignupSecret = () => infisical.get('JWT_SIGNUP_SECRET')!; +export const getMongoURL = () => infisical.get('MONGO_URL')!; +export const getNodeEnv = () => infisical.get('NODE_ENV')!; +export const getVerboseErrorOutput = () => infisical.get('VERBOSE_ERROR_OUTPUT')! === 'true' && true; +export const getLokiHost = () => infisical.get('LOKI_HOST')!; +export const getClientIdAzure = () => infisical.get('CLIENT_ID_AZURE')!; +export const getClientIdHeroku = () => infisical.get('CLIENT_ID_HEROKU')!; +export const getClientIdVercel = () => infisical.get('CLIENT_ID_VERCEL')!; +export const getClientIdNetlify = () => infisical.get('CLIENT_ID_NETLIFY')!; +export const getClientIdGitHub = () => infisical.get('CLIENT_ID_GITHUB')!; +export const getClientIdGitLab = () => infisical.get('CLIENT_ID_GITLAB')!; +export const getClientSecretAzure = () => infisical.get('CLIENT_SECRET_AZURE')!; +export const getClientSecretHeroku = () => infisical.get('CLIENT_SECRET_HEROKU')!; +export const getClientSecretVercel = () => infisical.get('CLIENT_SECRET_VERCEL')!; +export const getClientSecretNetlify = () => infisical.get('CLIENT_SECRET_NETLIFY')!; +export const getClientSecretGitHub = () => infisical.get('CLIENT_SECRET_GITHUB')!; +export const getClientSecretGitLab = () => infisical.get('CLIENT_SECRET_GITLAB')!; +export const getClientSlugVercel = () => infisical.get('CLIENT_SLUG_VERCEL')!; +export const getPostHogHost = () => infisical.get('POSTHOG_HOST')! || 'https://app.posthog.com'; +export const getPostHogProjectApiKey = () => infisical.get('POSTHOG_PROJECT_API_KEY')! || 'phc_nSin8j5q2zdhpFDI1ETmFNUIuTG4DwKVyIigrY10XiE'; +export const getSentryDSN = () => infisical.get('SENTRY_DSN')!; +export const getSiteURL = () => infisical.get('SITE_URL')!; +export const getSmtpHost = () => infisical.get('SMTP_HOST')!; +export const getSmtpSecure = () => infisical.get('SMTP_SECURE')! === 'true' || false; +export const getSmtpPort = () => parseInt(infisical.get('SMTP_PORT')!) || 587; +export const getSmtpUsername = () => infisical.get('SMTP_USERNAME')!; +export const getSmtpPassword = () => infisical.get('SMTP_PASSWORD')!; +export const getSmtpFromAddress = () => infisical.get('SMTP_FROM_ADDRESS')!; +export const getSmtpFromName = () => infisical.get('SMTP_FROM_NAME')! || 'Infisical'; +export const getStripeProductStarter = () => infisical.get('STRIPE_PRODUCT_STARTER')!; +export const getStripeProductPro = () => infisical.get('STRIPE_PRODUCT_PRO')!; +export const getStripeProductTeam = () => infisical.get('STRIPE_PRODUCT_TEAM')!; +export const getStripePublishableKey = () => infisical.get('STRIPE_PUBLISHABLE_KEY')!; +export const getStripeSecretKey = () => infisical.get('STRIPE_SECRET_KEY')!; +export const getStripeWebhookSecret = () => infisical.get('STRIPE_WEBHOOK_SECRET')!; +export const getTelemetryEnabled = () => infisical.get('TELEMETRY_ENABLED')! !== 'false' && true; +export const getLoopsApiKey = () => infisical.get('LOOPS_API_KEY')!; +export const getSmtpConfigured = () => infisical.get('SMTP_HOST') == '' || infisical.get('SMTP_HOST') == undefined ? false : true \ No newline at end of file diff --git a/backend/src/controllers/v1/authController.ts b/backend/src/controllers/v1/authController.ts index e329b15c7..3c6ec523a 100644 --- a/backend/src/controllers/v1/authController.ts +++ b/backend/src/controllers/v1/authController.ts @@ -1,8 +1,8 @@ -/* eslint-disable @typescript-eslint/no-var-requires */ +import * as Sentry from '@sentry/node'; import { Request, Response } from 'express'; import jwt from 'jsonwebtoken'; -import * as Sentry from '@sentry/node'; import * as bigintConversion from 'bigint-conversion'; +// eslint-disable-next-line @typescript-eslint/no-var-requires const jsrp = require('jsrp'); import { User, LoginSRPDetail } from '../../models'; import { createToken, issueAuthTokens, clearTokens } from '../../helpers/auth'; @@ -11,15 +11,15 @@ import { ACTION_LOGIN, ACTION_LOGOUT } from '../../variables'; -import { - NODE_ENV, - JWT_AUTH_LIFETIME, - JWT_AUTH_SECRET, - JWT_REFRESH_SECRET -} from '../../config'; import { BadRequestError } from '../../utils/errors'; import { EELogService } from '../../ee/services'; import { getChannelFromUserAgent } from '../../utils/posthog'; // TODO: move this +import { + getNodeEnv, + getJwtRefreshSecret, + getJwtAuthLifetime, + getJwtAuthSecret +} from '../../config'; declare module 'jsonwebtoken' { export interface UserIDJwtPayload extends jwt.JwtPayload { @@ -126,7 +126,7 @@ export const login2 = async (req: Request, res: Response) => { httpOnly: true, path: '/', sameSite: 'strict', - secure: NODE_ENV === 'production' ? true : false + secure: getNodeEnv() === 'production' ? true : false }); const loginAction = await EELogService.createAction({ @@ -182,7 +182,7 @@ export const logout = async (req: Request, res: Response) => { httpOnly: true, path: '/', sameSite: 'strict', - secure: NODE_ENV === 'production' ? true : false + secure: getNodeEnv() === 'production' ? true : false }); const logoutAction = await EELogService.createAction({ @@ -237,7 +237,7 @@ export const getNewToken = async (req: Request, res: Response) => { } const decodedToken = ( - jwt.verify(refreshToken, JWT_REFRESH_SECRET) + jwt.verify(refreshToken, getJwtRefreshSecret()) ); const user = await User.findOne({ @@ -252,8 +252,8 @@ export const getNewToken = async (req: Request, res: Response) => { payload: { userId: decodedToken.userId }, - expiresIn: JWT_AUTH_LIFETIME, - secret: JWT_AUTH_SECRET + expiresIn: getJwtAuthLifetime(), + secret: getJwtAuthSecret() }); return res.status(200).send({ diff --git a/backend/src/controllers/v1/integrationAuthController.ts b/backend/src/controllers/v1/integrationAuthController.ts index d26bab1cc..da8fc5570 100644 --- a/backend/src/controllers/v1/integrationAuthController.ts +++ b/backend/src/controllers/v1/integrationAuthController.ts @@ -5,7 +5,7 @@ import { IntegrationAuth, Bot } from '../../models'; -import { INTEGRATION_SET, INTEGRATION_OPTIONS } from '../../variables'; +import { INTEGRATION_SET, getIntegrationOptions as getIntegrationOptionsFunc } from '../../variables'; import { IntegrationService } from '../../services'; import { getApps, @@ -39,9 +39,11 @@ export const getIntegrationAuth = async (req: Request, res: Response) => { } export const getIntegrationOptions = async (req: Request, res: Response) => { - return res.status(200).send({ - integrationOptions: INTEGRATION_OPTIONS, - }); + const INTEGRATION_OPTIONS = getIntegrationOptionsFunc(); + + return res.status(200).send({ + integrationOptions: INTEGRATION_OPTIONS, + }); }; /** diff --git a/backend/src/controllers/v1/membershipController.ts b/backend/src/controllers/v1/membershipController.ts index 3627ce3fb..436be9dc4 100644 --- a/backend/src/controllers/v1/membershipController.ts +++ b/backend/src/controllers/v1/membershipController.ts @@ -1,13 +1,13 @@ -import { Request, Response } from 'express'; 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 { findMembership, deleteMembership as deleteMember } from '../../helpers/membership'; import { sendMail } from '../../helpers/nodemailer'; -import { SITE_URL } from '../../config'; import { ADMIN, MEMBER, ACCEPTED } from '../../variables'; +import { getSiteURL } from '../../config'; /** * 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, inviterEmail: req.user.email, workspaceName: req.membership.workspace.name, - callback_url: SITE_URL + '/login' + callback_url: getSiteURL() + '/login' } }); } catch (err) { diff --git a/backend/src/controllers/v1/membershipOrgController.ts b/backend/src/controllers/v1/membershipOrgController.ts index aa3022eac..5a2b41b2b 100644 --- a/backend/src/controllers/v1/membershipOrgController.ts +++ b/backend/src/controllers/v1/membershipOrgController.ts @@ -1,6 +1,5 @@ import { Request, Response } from 'express'; import * as Sentry from '@sentry/node'; -import { SITE_URL, JWT_SIGNUP_LIFETIME, JWT_SIGNUP_SECRET } from '../../config'; import { MembershipOrg, Organization, User } from '../../models'; import { deleteMembershipOrg as deleteMemberFromOrg } from '../../helpers/membershipOrg'; import { createToken } from '../../helpers/auth'; @@ -8,6 +7,7 @@ import { updateSubscriptionOrgQuantity } from '../../helpers/organization'; import { sendMail } from '../../helpers/nodemailer'; import { TokenService } from '../../services'; 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 @@ -99,9 +99,11 @@ export const changeMembershipOrgRole = async (req: Request, res: Response) => { * @returns */ export const inviteUserToOrganization = async (req: Request, res: Response) => { - let invitee, inviteeMembershipOrg; + let invitee, inviteeMembershipOrg, completeInviteLink; try { const { organizationId, inviteeEmail } = req.body; + const host = req.headers.host; + const siteUrl = `${req.protocol}://${host}`; // validate membership const membershipOrg = await MembershipOrg.findOne({ @@ -178,9 +180,13 @@ export const inviteUserToOrganization = async (req: Request, res: Response) => { organizationName: organization.name, email: inviteeEmail, token, - callback_url: SITE_URL + '/signupinvite' + callback_url: getSiteURL() + '/signupinvite' } }); + + if (!getSmtpConfigured()) { + completeInviteLink = `${siteUrl + '/signupinvite'}?token=${token}&to=${inviteeEmail}` + } } await updateSubscriptionOrgQuantity({ organizationId }); @@ -193,7 +199,8 @@ export const inviteUserToOrganization = async (req: Request, res: Response) => { } 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) throw new Error('Failed to find any invitations for email'); - + await TokenService.validateToken({ type: TOKEN_EMAIL_ORG_INVITATION, email, @@ -250,8 +257,8 @@ export const verifyUserToOrganization = async (req: Request, res: Response) => { payload: { userId: user._id.toString() }, - expiresIn: JWT_SIGNUP_LIFETIME, - secret: JWT_SIGNUP_SECRET + expiresIn: getJwtSignupLifetime(), + secret: getJwtSignupSecret() }); } catch (err) { Sentry.setUser(null); diff --git a/backend/src/controllers/v1/organizationController.ts b/backend/src/controllers/v1/organizationController.ts index 66326e560..00ad87b82 100644 --- a/backend/src/controllers/v1/organizationController.ts +++ b/backend/src/controllers/v1/organizationController.ts @@ -1,26 +1,18 @@ -import { Request, Response } from 'express'; import * as Sentry from '@sentry/node'; -import { - SITE_URL, - STRIPE_SECRET_KEY -} from '../../config'; +import { Request, Response } from 'express'; import Stripe from 'stripe'; - -const stripe = new Stripe(STRIPE_SECRET_KEY, { - apiVersion: '2022-08-01' -}); import { Membership, MembershipOrg, Organization, Workspace, - IncidentContactOrg, - IMembershipOrg + IncidentContactOrg } from '../../models'; import { createOrganization as create } from '../../helpers/organization'; import { addMembershipsOrg } from '../../helpers/membershipOrg'; import { OWNER, ACCEPTED } from '../../variables'; import _ from 'lodash'; +import { getStripeSecretKey, getSiteURL } from '../../config'; export const getOrganizations = async (req: Request, res: Response) => { let organizations; @@ -325,6 +317,10 @@ export const createOrganizationPortalSession = async ( ) => { let session; try { + const stripe = new Stripe(getStripeSecretKey(), { + apiVersion: '2022-08-01' + }); + // check if there is a payment method on file const paymentMethods = await stripe.paymentMethods.list({ customer: req.membershipOrg.organization.customerId, @@ -337,13 +333,13 @@ export const createOrganizationPortalSession = async ( customer: req.membershipOrg.organization.customerId, mode: 'setup', payment_method_types: ['card'], - success_url: SITE_URL + '/dashboard', - cancel_url: SITE_URL + '/dashboard' + success_url: getSiteURL() + '/dashboard', + cancel_url: getSiteURL() + '/dashboard' }); } else { session = await stripe.billingPortal.sessions.create({ customer: req.membershipOrg.organization.customerId, - return_url: SITE_URL + '/dashboard' + return_url: getSiteURL() + '/dashboard' }); } @@ -369,6 +365,10 @@ export const getOrganizationSubscriptions = async ( ) => { let subscriptions; try { + const stripe = new Stripe(getStripeSecretKey(), { + apiVersion: '2022-08-01' + }); + subscriptions = await stripe.subscriptions.list({ customer: req.membershipOrg.organization.customerId }); diff --git a/backend/src/controllers/v1/passwordController.ts b/backend/src/controllers/v1/passwordController.ts index 63d2b9184..fed24419c 100644 --- a/backend/src/controllers/v1/passwordController.ts +++ b/backend/src/controllers/v1/passwordController.ts @@ -7,9 +7,9 @@ import { User, BackupPrivateKey, LoginSRPDetail } from '../../models'; import { createToken } from '../../helpers/auth'; import { sendMail } from '../../helpers/nodemailer'; import { TokenService } from '../../services'; -import { JWT_SIGNUP_LIFETIME, JWT_SIGNUP_SECRET, SITE_URL } from '../../config'; import { TOKEN_EMAIL_PASSWORD_RESET } from '../../variables'; import { BadRequestError } from '../../utils/errors'; +import { getSiteURL, getJwtSignupLifetime, getJwtSignupSecret } from '../../config'; /** * Password reset step 1: Send email verification link to email [email] @@ -44,7 +44,7 @@ export const emailPasswordReset = async (req: Request, res: Response) => { substitutions: { email, token, - callback_url: SITE_URL + '/password-reset' + callback_url: getSiteURL() + '/password-reset' } }); } catch (err) { @@ -91,8 +91,8 @@ export const emailPasswordResetVerify = async (req: Request, res: Response) => { payload: { userId: user._id.toString() }, - expiresIn: JWT_SIGNUP_LIFETIME, - secret: JWT_SIGNUP_SECRET + expiresIn: getJwtSignupLifetime(), + secret: getJwtSignupSecret() }); } catch (err) { Sentry.setUser(null); diff --git a/backend/src/controllers/v1/secretController.ts b/backend/src/controllers/v1/secretController.ts index c76e5e883..1d1f8981c 100644 --- a/backend/src/controllers/v1/secretController.ts +++ b/backend/src/controllers/v1/secretController.ts @@ -9,7 +9,7 @@ import { import { pushKeys } from '../../helpers/key'; import { eventPushSecrets } from '../../events'; import { EventService } from '../../services'; -import { postHogClient } from '../../services'; +import { getPostHogClient } from '../../services'; interface PushSecret { ciphertextKey: string; @@ -38,6 +38,7 @@ export const pushSecrets = async (req: Request, res: Response) => { // upload (encrypted) secrets to workspace with id [workspaceId] try { + const postHogClient = getPostHogClient(); let { secrets }: { secrets: PushSecret[] } = req.body; const { keys, environment, channel } = req.body; const { workspaceId } = req.params; @@ -111,6 +112,7 @@ export const pullSecrets = async (req: Request, res: Response) => { let secrets; let key; try { + const postHogClient = getPostHogClient(); const environment: string = req.query.environment as string; const channel: string = req.query.channel as string; const { workspaceId } = req.params; @@ -179,6 +181,7 @@ export const pullSecretsServiceToken = async (req: Request, res: Response) => { let secrets; let key; try { + const postHogClient = getPostHogClient(); const environment: string = req.query.environment as string; const channel: string = req.query.channel as string; const { workspaceId } = req.params; diff --git a/backend/src/controllers/v1/serviceTokenController.ts b/backend/src/controllers/v1/serviceTokenController.ts index 3fafb9043..9f241349a 100644 --- a/backend/src/controllers/v1/serviceTokenController.ts +++ b/backend/src/controllers/v1/serviceTokenController.ts @@ -1,7 +1,7 @@ import { Request, Response } from 'express'; import { ServiceToken } from '../../models'; import { createToken } from '../../helpers/auth'; -import { JWT_SERVICE_SECRET } from '../../config'; +import { getJwtServiceSecret } from '../../config'; /** * Return service token on request @@ -61,7 +61,7 @@ export const createServiceToken = async (req: Request, res: Response) => { workspaceId }, expiresIn: expiresIn, - secret: JWT_SERVICE_SECRET + secret: getJwtServiceSecret() }); } catch (err) { return res.status(400).send({ diff --git a/backend/src/controllers/v1/signupController.ts b/backend/src/controllers/v1/signupController.ts index 1c5c9e298..6adb0e7a8 100644 --- a/backend/src/controllers/v1/signupController.ts +++ b/backend/src/controllers/v1/signupController.ts @@ -1,13 +1,13 @@ import { Request, Response } from 'express'; import * as Sentry from '@sentry/node'; import { User } from '../../models'; -import { JWT_SIGNUP_LIFETIME, JWT_SIGNUP_SECRET, INVITE_ONLY_SIGNUP } from '../../config'; import { sendEmailVerification, checkEmailVerification, } from '../../helpers/signup'; import { createToken } from '../../helpers/auth'; 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 @@ -21,7 +21,7 @@ export const beginEmailSignup = async (req: Request, res: Response) => { try { 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 const userCount = await User.countDocuments({}) if (userCount != 0) { @@ -66,7 +66,7 @@ export const verifyEmailSignup = async (req: Request, res: Response) => { const { email, code } = req.body; // initialize user account - user = await User.findOne({ email }); + user = await User.findOne({ email }).select('+publicKey'); if (user && user?.publicKey) { // case: user has already completed account return res.status(403).send({ @@ -75,10 +75,12 @@ export const verifyEmailSignup = async (req: Request, res: Response) => { } // verify email - await checkEmailVerification({ - email, - code - }); + if (getSmtpConfigured()) { + await checkEmailVerification({ + email, + code + }); + } if (!user) { user = await new User({ @@ -91,8 +93,8 @@ export const verifyEmailSignup = async (req: Request, res: Response) => { payload: { userId: user._id.toString() }, - expiresIn: JWT_SIGNUP_LIFETIME, - secret: JWT_SIGNUP_SECRET + expiresIn: getJwtSignupLifetime(), + secret: getJwtSignupSecret() }); } catch (err) { Sentry.setUser(null); diff --git a/backend/src/controllers/v1/stripeController.ts b/backend/src/controllers/v1/stripeController.ts index 15f62a056..1a981c088 100644 --- a/backend/src/controllers/v1/stripeController.ts +++ b/backend/src/controllers/v1/stripeController.ts @@ -1,10 +1,7 @@ import { Request, Response } from 'express'; import * as Sentry from '@sentry/node'; import Stripe from 'stripe'; -import { STRIPE_SECRET_KEY, STRIPE_WEBHOOK_SECRET } from '../../config'; -const stripe = new Stripe(STRIPE_SECRET_KEY, { - apiVersion: '2022-08-01' -}); +import { getStripeSecretKey, getStripeWebhookSecret } from '../../config'; /** * Handle service provisioning/un-provisioning via Stripe @@ -16,11 +13,15 @@ export const handleWebhook = async (req: Request, res: Response) => { let event; try { // check request for valid stripe signature + const stripe = new Stripe(getStripeSecretKey(), { + apiVersion: '2022-08-01' + }); + const sig = req.headers['stripe-signature'] as string; event = stripe.webhooks.constructEvent( req.body, sig, - STRIPE_WEBHOOK_SECRET // ? + getStripeWebhookSecret() ); } catch (err) { Sentry.setUser({ email: req.user.email }); diff --git a/backend/src/controllers/v2/apiKeyDataController.ts b/backend/src/controllers/v2/apiKeyDataController.ts index cafbacb5b..fd87f7306 100644 --- a/backend/src/controllers/v2/apiKeyDataController.ts +++ b/backend/src/controllers/v2/apiKeyDataController.ts @@ -1,13 +1,11 @@ -import { Request, Response } from 'express'; import * as Sentry from '@sentry/node'; +import { Request, Response } from 'express'; import crypto from 'crypto'; import bcrypt from 'bcrypt'; import { APIKeyData } from '../../models'; -import { - SALT_ROUNDS -} from '../../config'; +import { getSaltRounds } from '../../config'; /** * 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 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(); expiresAt.setSeconds(expiresAt.getSeconds() + expiresIn); diff --git a/backend/src/controllers/v2/authController.ts b/backend/src/controllers/v2/authController.ts index 95a1613d2..3a36d559c 100644 --- a/backend/src/controllers/v2/authController.ts +++ b/backend/src/controllers/v2/authController.ts @@ -10,17 +10,17 @@ import { checkUserDevice } from '../../helpers/user'; import { sendMail } from '../../helpers/nodemailer'; import { TokenService } from '../../services'; import { EELogService } from '../../ee/services'; -import { - NODE_ENV, - JWT_MFA_LIFETIME, - JWT_MFA_SECRET -} from '../../config'; import { BadRequestError, InternalServerError } from '../../utils/errors'; import { TOKEN_EMAIL_MFA, ACTION_LOGIN } from '../../variables'; import { getChannelFromUserAgent } from '../../utils/posthog'; // TODO: move this +import { + getNodeEnv, + getJwtMfaLifetime, + getJwtMfaSecret +} from '../../config'; declare module 'jsonwebtoken' { 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 * @param req @@ -89,7 +87,7 @@ export const login1 = async (req: Request, res: Response) => { */ export const login2 = async (req: Request, res: Response) => { try { - + if (!req.headers['user-agent']) throw InternalServerError({ message: 'User-Agent header is required' }); const { email, clientProof } = req.body; @@ -126,15 +124,15 @@ export const login2 = async (req: Request, res: Response) => { payload: { userId: user._id.toString() }, - expiresIn: JWT_MFA_LIFETIME, - secret: JWT_MFA_SECRET + expiresIn: getJwtMfaLifetime(), + secret: getJwtMfaSecret() }); - + const code = await TokenService.createToken({ type: TOKEN_EMAIL_MFA, email }); - + // send MFA code [code] to [email] await sendMail({ template: 'emailMfa.handlebars', @@ -144,13 +142,13 @@ export const login2 = async (req: Request, res: Response) => { code } }); - + return res.status(200).send({ mfaEnabled: true, token }); } - + await checkUserDevice({ user, ip: req.ip, @@ -165,7 +163,7 @@ export const login2 = async (req: Request, res: Response) => { httpOnly: true, path: '/', sameSite: 'strict', - secure: NODE_ENV === 'production' ? true : false + secure: getNodeEnv() === 'production' ? true : false }); // case: user does not have MFA enablgged @@ -183,7 +181,7 @@ export const login2 = async (req: Request, res: Response) => { iv?: string; tag?: string; } - + const response: ResponseData = { mfaEnabled: false, encryptionVersion: user.encryptionVersion, @@ -193,7 +191,7 @@ export const login2 = async (req: Request, res: Response) => { iv: user.iv, tag: user.tag } - + if ( user?.protectedKey && user?.protectedKeyIV && @@ -208,14 +206,14 @@ export const login2 = async (req: Request, res: Response) => { 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(response); } @@ -246,7 +244,7 @@ export const sendMfaToken = async (req: Request, res: Response) => { type: TOKEN_EMAIL_MFA, email }); - + // send MFA code [code] to [email] await sendMail({ template: 'emailMfa.handlebars', @@ -261,9 +259,9 @@ export const sendMfaToken = async (req: Request, res: Response) => { Sentry.captureException(err); return res.status(400).send({ message: 'Failed to send MFA code' - }); + }); } - + return res.status(200).send({ message: 'Successfully sent new MFA code' }); @@ -276,76 +274,87 @@ export const sendMfaToken = async (req: Request, res: Response) => { * @param res */ export const verifyMfaToken = async (req: Request, res: Response) => { - const { email, mfaToken } = req.body; + const { email, mfaToken } = req.body; - await TokenService.validateToken({ - type: TOKEN_EMAIL_MFA, - email, - token: mfaToken - }); + await TokenService.validateToken({ + type: TOKEN_EMAIL_MFA, + email, + token: mfaToken + }); - const user = await User.findOne({ - email - }).select('+salt +verifier +encryptionVersion +protectedKey +protectedKeyIV +protectedKeyTag +publicKey +encryptedPrivateKey +iv +tag'); + const user = await User.findOne({ + email + }).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({ - user, - ip: req.ip, - userAgent: req.headers['user-agent'] ?? '' - }); + await checkUserDevice({ + user, + ip: req.ip, + userAgent: req.headers['user-agent'] ?? '' + }); - // issue tokens - const tokens = await issueAuthTokens({ userId: user._id.toString() }); + // issue tokens + const tokens = await issueAuthTokens({ userId: user._id.toString() }); - // store (refresh) token in httpOnly cookie - res.cookie('jid', tokens.refreshToken, { - httpOnly: true, - path: '/', - sameSite: 'strict', - secure: NODE_ENV === 'production' ? true : false - }); - - interface VerifyMfaTokenRes { - encryptionVersion: number; - protectedKey?: string; - protectedKeyIV?: string; - protectedKeyTag?: string; - token: string; - publicKey: string; - encryptedPrivateKey: string; - iv: string; - tag: string; - } + // store (refresh) token in httpOnly cookie + res.cookie('jid', tokens.refreshToken, { + httpOnly: true, + path: '/', + sameSite: 'strict', + secure: getNodeEnv() === 'production' ? true : false + }); - 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; - } + interface VerifyMfaTokenRes { + encryptionVersion: number; + protectedKey?: string; + protectedKeyIV?: string; + protectedKeyTag?: string; + token: string; + publicKey: string; + encryptedPrivateKey: string; + iv: string; + tag: string; + } - 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 - }); + interface VerifyMfaTokenRes { + encryptionVersion: number; + protectedKey?: string; + protectedKeyIV?: string; + protectedKeyTag?: string; + token: string; + publicKey: string; + encryptedPrivateKey: string; + iv: string; + 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); } - diff --git a/backend/src/controllers/v2/secretController.ts b/backend/src/controllers/v2/secretController.ts index 89567e616..cd91dca67 100644 --- a/backend/src/controllers/v2/secretController.ts +++ b/backend/src/controllers/v2/secretController.ts @@ -7,7 +7,7 @@ const { ValidationError } = mongoose.Error; import { BadRequestError, InternalServerError, UnauthorizedRequestError, ValidationError as RouteValidationError } from '../../utils/errors'; import { AnyBulkWriteOperation } from 'mongodb'; 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] @@ -15,6 +15,7 @@ import { postHogClient } from '../../services'; * @param res */ export const createSecret = async (req: Request, res: Response) => { + const postHogClient = getPostHogClient(); const secretToCreate: CreateSecretRequestBody = req.body.secret; const { workspaceId, environment } = req.params const sanitizedSecret: SanitizedSecretForCreate = { @@ -67,6 +68,7 @@ export const createSecret = async (req: Request, res: Response) => { * @param res */ export const createSecrets = async (req: Request, res: Response) => { + const postHogClient = getPostHogClient(); const secretsToCreate: CreateSecretRequestBody[] = req.body.secrets; const { workspaceId, environment } = req.params const sanitizedSecretesToCreate: SanitizedSecretForCreate[] = [] @@ -128,6 +130,7 @@ export const createSecrets = async (req: Request, res: Response) => { * @param res */ export const deleteSecrets = async (req: Request, res: Response) => { + const postHogClient = getPostHogClient(); const { workspaceId, environmentName } = req.params const secretIdsToDelete: string[] = req.body.secretIds @@ -181,6 +184,7 @@ export const deleteSecrets = async (req: Request, res: Response) => { * @param res */ export const deleteSecret = async (req: Request, res: Response) => { + const postHogClient = getPostHogClient(); await Secret.findByIdAndDelete(req._secret._id) if (postHogClient) { @@ -209,6 +213,7 @@ export const deleteSecret = async (req: Request, res: Response) => { * @returns */ export const updateSecrets = async (req: Request, res: Response) => { + const postHogClient = getPostHogClient(); const { workspaceId, environmentName } = req.params const secretsModificationsRequested: ModifySecretRequestBody[] = req.body.secrets; 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 */ export const updateSecret = async (req: Request, res: Response) => { + const postHogClient = getPostHogClient(); const { workspaceId, environmentName } = req.params const secretModificationsRequested: ModifySecretRequestBody = req.body.secret; @@ -329,6 +335,7 @@ export const updateSecret = async (req: Request, res: Response) => { * @returns */ export const getSecrets = async (req: Request, res: Response) => { + const postHogClient = getPostHogClient(); const { environment } = req.query; const { workspaceId } = req.params; diff --git a/backend/src/controllers/v2/secretsController.ts b/backend/src/controllers/v2/secretsController.ts index 486bf017e..66aecd423 100644 --- a/backend/src/controllers/v2/secretsController.ts +++ b/backend/src/controllers/v2/secretsController.ts @@ -15,7 +15,7 @@ import { UnauthorizedRequestError, ValidationError } from '../../utils/errors'; import { EventService } from '../../services'; import { eventPushSecrets } from '../../events'; import { EESecretService, EELogService } from '../../ee/services'; -import { postHogClient } from '../../services'; +import { getPostHogClient } from '../../services'; import { getChannelFromUserAgent } from '../../utils/posthog'; import { ABILITY_READ, ABILITY_WRITE } from '../../variables/organization'; import { userHasNoAbility, userHasWorkspaceAccess, userHasWriteOnlyAbility } from '../../ee/helpers/checkMembershipPermissions'; @@ -33,6 +33,8 @@ import { */ export const batchSecrets = async (req: Request, res: Response) => { const channel = getChannelFromUserAgent(req.headers['user-agent']); + const postHogClient = getPostHogClient(); + const { workspaceId, environment, @@ -326,6 +328,7 @@ export const createSecrets = async (req: Request, res: Response) => { } } */ + const postHogClient = getPostHogClient(); const channel = getChannelFromUserAgent(req.headers['user-agent']) 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 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'; // 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 toDelete = req.secrets.map((s: any) => s._id); diff --git a/backend/src/controllers/v2/serviceTokenDataController.ts b/backend/src/controllers/v2/serviceTokenDataController.ts index cabedabea..a4e06f8e4 100644 --- a/backend/src/controllers/v2/serviceTokenDataController.ts +++ b/backend/src/controllers/v2/serviceTokenDataController.ts @@ -1,15 +1,13 @@ -import { Request, Response } from 'express'; import * as Sentry from '@sentry/node'; +import { Request, Response } from 'express'; import crypto from 'crypto'; import bcrypt from 'bcrypt'; import { ServiceTokenData } from '../../models'; -import { - SALT_ROUNDS -} from '../../config'; import { userHasWorkspaceAccess } from '../../ee/helpers/checkMembershipPermissions'; import { ABILITY_READ } from '../../variables/organization'; +import { getSaltRounds } from '../../config'; /** * 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 secretHash = await bcrypt.hash(secret, SALT_ROUNDS); + const secretHash = await bcrypt.hash(secret, getSaltRounds()); const expiresAt = new Date(); expiresAt.setSeconds(expiresAt.getSeconds() + expiresIn); @@ -142,4 +140,4 @@ export const deleteServiceTokenData = async (req: Request, res: Response) => { function UnauthorizedRequestError(arg0: { message: string; }) { throw new Error('Function not implemented.'); -} +} \ No newline at end of file diff --git a/backend/src/controllers/v2/signupController.ts b/backend/src/controllers/v2/signupController.ts index 79cb6730d..aaf055b78 100644 --- a/backend/src/controllers/v2/signupController.ts +++ b/backend/src/controllers/v2/signupController.ts @@ -7,8 +7,8 @@ import { } from '../../helpers/signup'; import { issueAuthTokens } from '../../helpers/auth'; import { INVITED, ACCEPTED } from '../../variables'; -import { NODE_ENV } from '../../config'; 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 @@ -108,7 +108,7 @@ export const completeAccountSignup = async (req: Request, res: Response) => { token = tokens.token; // 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", { "email": email, "eventName": "Sign Up", @@ -117,7 +117,7 @@ export const completeAccountSignup = async (req: Request, res: Response) => { }, { headers: { "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, path: '/', sameSite: 'strict', - secure: NODE_ENV === 'production' ? true : false + secure: getNodeEnv() === 'production' ? true : false }); } catch (err) { Sentry.setUser(null); @@ -232,7 +232,7 @@ export const completeAccountInvite = async (req: Request, res: Response) => { httpOnly: true, path: '/', sameSite: 'strict', - secure: NODE_ENV === 'production' ? true : false + secure: getNodeEnv() === 'production' ? true : false }); } catch (err) { Sentry.setUser(null); diff --git a/backend/src/controllers/v2/workspaceController.ts b/backend/src/controllers/v2/workspaceController.ts index 55cd02fff..650c70610 100644 --- a/backend/src/controllers/v2/workspaceController.ts +++ b/backend/src/controllers/v2/workspaceController.ts @@ -19,7 +19,7 @@ import { reformatPullSecrets } from '../../helpers/secret'; import { pushKeys } from '../../helpers/key'; -import { postHogClient, EventService } from '../../services'; +import { getPostHogClient, EventService } from '../../services'; import { eventPushSecrets } from '../../events'; interface V2PushSecret { @@ -48,6 +48,7 @@ interface V2PushSecret { export const pushWorkspaceSecrets = async (req: Request, res: Response) => { // upload (encrypted) secrets to workspace with id [workspaceId] try { + const postHogClient = getPostHogClient(); let { secrets }: { secrets: V2PushSecret[] } = req.body; const { keys, environment, channel } = req.body; const { workspaceId } = req.params; @@ -121,6 +122,7 @@ export const pushWorkspaceSecrets = async (req: Request, res: Response) => { export const pullSecrets = async (req: Request, res: Response) => { let secrets; try { + const postHogClient = getPostHogClient(); const environment: string = req.query.environment as string; const channel: string = req.query.channel as string; const { workspaceId } = req.params; diff --git a/backend/src/ee/controllers/v1/stripeController.ts b/backend/src/ee/controllers/v1/stripeController.ts index faef14cea..3caa0f395 100644 --- a/backend/src/ee/controllers/v1/stripeController.ts +++ b/backend/src/ee/controllers/v1/stripeController.ts @@ -1,10 +1,7 @@ -import { Request, Response } from 'express'; import * as Sentry from '@sentry/node'; +import { Request, Response } from 'express'; import Stripe from 'stripe'; -import { STRIPE_SECRET_KEY, STRIPE_WEBHOOK_SECRET } from '../../../config'; -const stripe = new Stripe(STRIPE_SECRET_KEY, { - apiVersion: '2022-08-01' -}); +import { getStripeSecretKey, getStripeWebhookSecret } from '../../../config'; /** * 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) => { let event; try { + const stripe = new Stripe(getStripeSecretKey(), { + apiVersion: '2022-08-01' + }); + // check request for valid stripe signature const sig = req.headers['stripe-signature'] as string; event = stripe.webhooks.constructEvent( req.body, sig, - STRIPE_WEBHOOK_SECRET // ? + getStripeWebhookSecret() ); } catch (err) { Sentry.setUser({ email: req.user.email }); diff --git a/backend/src/ee/services/EELicenseService.ts b/backend/src/ee/services/EELicenseService.ts index f31482dde..4bd811340 100644 --- a/backend/src/ee/services/EELicenseService.ts +++ b/backend/src/ee/services/EELicenseService.ts @@ -1,5 +1,3 @@ -import { LICENSE_KEY } from '../../config'; - /** * Class to handle Enterprise Edition license actions */ @@ -16,4 +14,4 @@ class EELicenseService { } } -export default new EELicenseService(LICENSE_KEY); \ No newline at end of file +export default new EELicenseService('N/A'); \ No newline at end of file diff --git a/backend/src/helpers/auth.ts b/backend/src/helpers/auth.ts index 102a7ac07..a08dcf2cc 100644 --- a/backend/src/helpers/auth.ts +++ b/backend/src/helpers/auth.ts @@ -1,5 +1,5 @@ -import jwt from 'jsonwebtoken'; import * as Sentry from '@sentry/node'; +import jwt from 'jsonwebtoken'; import bcrypt from 'bcrypt'; import { IUser, @@ -7,12 +7,6 @@ import { ServiceTokenData, APIKeyData } from '../models'; -import { - JWT_AUTH_LIFETIME, - JWT_AUTH_SECRET, - JWT_REFRESH_LIFETIME, - JWT_REFRESH_SECRET -} from '../config'; import { AccountNotFoundError, ServiceTokenDataNotFoundError, @@ -20,6 +14,12 @@ import { UnauthorizedRequestError, BadRequestError } from '../utils/errors'; +import { + getJwtAuthLifetime, + getJwtAuthSecret, + getJwtRefreshLifetime, + getJwtRefreshSecret +} from '../config'; /** * @@ -93,7 +93,7 @@ const getAuthUserPayload = async ({ let user; try { const decodedToken = ( - jwt.verify(authTokenValue, JWT_AUTH_SECRET) + jwt.verify(authTokenValue, getJwtAuthSecret()) ); user = await User.findOne({ @@ -224,16 +224,16 @@ const issueAuthTokens = async ({ userId }: { userId: string }) => { payload: { userId }, - expiresIn: JWT_AUTH_LIFETIME, - secret: JWT_AUTH_SECRET + expiresIn: getJwtAuthLifetime(), + secret: getJwtAuthSecret() }); refreshToken = createToken({ payload: { userId }, - expiresIn: JWT_REFRESH_LIFETIME, - secret: JWT_REFRESH_SECRET + expiresIn: getJwtRefreshLifetime(), + secret: getJwtRefreshSecret() }); } catch (err) { Sentry.setUser(null); diff --git a/backend/src/helpers/bot.ts b/backend/src/helpers/bot.ts index 7519ef18b..5cfbeebf5 100644 --- a/backend/src/helpers/bot.ts +++ b/backend/src/helpers/bot.ts @@ -12,8 +12,8 @@ import { decryptSymmetric, decryptAsymmetric } from '../utils/crypto'; -import { ENCRYPTION_KEY } from '../config'; import { SECRET_SHARED } from '../variables'; +import { getEncryptionKey } from '../config'; /** * Create an inactive bot with name [name] for workspace with id [workspaceId] @@ -33,7 +33,7 @@ const createBot = async ({ const { publicKey, privateKey } = generateKeyPair(); const { ciphertext, iv, tag } = encryptSymmetric({ plaintext: privateKey, - key: ENCRYPTION_KEY + key: getEncryptionKey() }); bot = await new Bot({ @@ -130,7 +130,7 @@ const getKey = async ({ workspaceId }: { workspaceId: string }) => { ciphertext: bot.encryptedPrivateKey, iv: bot.iv, tag: bot.tag, - key: ENCRYPTION_KEY + key: getEncryptionKey() }); key = decryptAsymmetric({ diff --git a/backend/src/helpers/database.ts b/backend/src/helpers/database.ts index 9ba592ea3..9d128ea47 100644 --- a/backend/src/helpers/database.ts +++ b/backend/src/helpers/database.ts @@ -29,6 +29,23 @@ const initDatabaseHelper = async ({ 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 { - initDatabaseHelper + initDatabaseHelper, + closeDatabaseHelper } \ No newline at end of file diff --git a/backend/src/helpers/nodemailer.ts b/backend/src/helpers/nodemailer.ts index 958342aae..fe7c05044 100644 --- a/backend/src/helpers/nodemailer.ts +++ b/backend/src/helpers/nodemailer.ts @@ -1,9 +1,9 @@ +import * as Sentry from '@sentry/node'; import fs from 'fs'; import path from 'path'; import handlebars from 'handlebars'; import nodemailer from 'nodemailer'; -import { SMTP_FROM_NAME, SMTP_FROM_ADDRESS } from '../config'; -import * as Sentry from '@sentry/node'; +import { getSmtpFromName, getSmtpFromAddress, getSmtpConfigured } from '../config'; let smtpTransporter: nodemailer.Transporter; @@ -25,23 +25,25 @@ const sendMail = async ({ recipients: string[]; substitutions: any; }) => { - try { - const html = fs.readFileSync( - path.resolve(__dirname, '../templates/' + template), - 'utf8' - ); - const temp = handlebars.compile(html); - const htmlToSend = temp(substitutions); + if (getSmtpConfigured()) { + try { + const html = fs.readFileSync( + path.resolve(__dirname, '../templates/' + template), + 'utf8' + ); + const temp = handlebars.compile(html); + const htmlToSend = temp(substitutions); - await smtpTransporter.sendMail({ - from: `"${SMTP_FROM_NAME}" <${SMTP_FROM_ADDRESS}>`, - to: recipients.join(', '), - subject: subjectLine, - html: htmlToSend - }); - } catch (err) { - Sentry.setUser(null); - Sentry.captureException(err); + await smtpTransporter.sendMail({ + from: `"${getSmtpFromName()}" <${getSmtpFromAddress()}>`, + to: recipients.join(', '), + subject: subjectLine, + html: htmlToSend + }); + } catch (err) { + Sentry.setUser(null); + Sentry.captureException(err); + } } }; diff --git a/backend/src/helpers/organization.ts b/backend/src/helpers/organization.ts index 4ba3592de..fb559df1b 100644 --- a/backend/src/helpers/organization.ts +++ b/backend/src/helpers/organization.ts @@ -1,23 +1,14 @@ import * as Sentry from '@sentry/node'; 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 { ACCEPTED } from '../variables'; import { Organization, MembershipOrg } from '../models'; - -const productToPriceMap = { - starter: STRIPE_PRODUCT_STARTER, - team: STRIPE_PRODUCT_TEAM, - pro: STRIPE_PRODUCT_PRO -}; +import { + getStripeSecretKey, + getStripeProductPro, + getStripeProductTeam, + getStripeProductStarter +} from '../config'; /** * Create an organization with name [name] @@ -36,8 +27,11 @@ const createOrganization = async ({ let organization; try { // register stripe account + const stripe = new Stripe(getStripeSecretKey(), { + apiVersion: '2022-08-01' + }); - if (STRIPE_SECRET_KEY) { + if (getStripeSecretKey()) { const customer = await stripe.customers.create({ email, description: name @@ -87,6 +81,16 @@ const initSubscriptionOrg = async ({ if (organization) { if (organization.customerId) { // 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({ customer: organization.customerId, items: [ @@ -139,6 +143,10 @@ const updateSubscriptionOrgQuantity = async ({ status: ACCEPTED }); + const stripe = new Stripe(getStripeSecretKey(), { + apiVersion: '2022-08-01' + }); + const subscription = ( await stripe.subscriptions.list({ customer: organization.customerId @@ -167,4 +175,4 @@ export { createOrganization, initSubscriptionOrg, updateSubscriptionOrgQuantity -}; +}; \ No newline at end of file diff --git a/backend/src/helpers/token.ts b/backend/src/helpers/token.ts index ca8838151..0f6a88cc9 100644 --- a/backend/src/helpers/token.ts +++ b/backend/src/helpers/token.ts @@ -9,10 +9,8 @@ import { TOKEN_EMAIL_ORG_INVITATION, TOKEN_EMAIL_PASSWORD_RESET } from '../variables'; -import { - SALT_ROUNDS -} from '../config'; import { UnauthorizedRequestError } from '../utils/errors'; +import { getSaltRounds } from '../config'; /** * Create and store a token in the database for purpose [type] @@ -86,7 +84,7 @@ const createTokenHelper = async ({ const query: TokenDataQuery = { type }; const update: TokenDataUpdate = { type, - tokenHash: await bcrypt.hash(token, SALT_ROUNDS), + tokenHash: await bcrypt.hash(token, getSaltRounds()), expiresAt } diff --git a/backend/src/index.ts b/backend/src/index.ts index a97b5b33b..03534a927 100644 --- a/backend/src/index.ts +++ b/backend/src/index.ts @@ -1,28 +1,183 @@ +import mongoose from 'mongoose'; import dotenv from 'dotenv'; 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 { SENTRY_DSN, NODE_ENV, MONGO_URL } from './config'; -import { server } from './app'; import { DatabaseService } from './services'; import { setUpHealthEndpoint } from './services/health'; import { initSmtp } from './services/smtp'; +import { logTelemetryMessage } from './services'; import { setTransporter } from './helpers/nodemailer'; 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') { - Sentry.init({ - dsn: SENTRY_DSN, - tracesSampleRate: 1.0, - debug: NODE_ENV === 'production' ? false : true, - environment: NODE_ENV - }); + await DatabaseService.initDatabase(getMongoURL()); + if (getNodeEnv() !== 'test') { + Sentry.init({ + dsn: getSentryDSN(), + tracesSampleRate: 1.0, + 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(); \ No newline at end of file diff --git a/backend/src/integrations/exchange.ts b/backend/src/integrations/exchange.ts index 224979c88..1dccb03d0 100644 --- a/backend/src/integrations/exchange.ts +++ b/backend/src/integrations/exchange.ts @@ -1,5 +1,5 @@ -import request from '../config/request'; import * as Sentry from '@sentry/node'; +import request from '../config/request'; import { INTEGRATION_AZURE_KEY_VAULT, INTEGRATION_HEROKU, @@ -15,18 +15,18 @@ import { INTEGRATION_GITLAB_TOKEN_URL } from '../variables'; import { - SITE_URL, - CLIENT_ID_AZURE, - 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, + getSiteURL, + getClientIdAzure, + getClientSecretAzure, + getClientSecretHeroku, + getClientIdVercel, + getClientSecretVercel, + getClientIdNetlify, + getClientSecretNetlify, + getClientIdGitHub, + getClientSecretGitHub, + getClientIdGitLab, + getClientSecretGitLab } from '../config'; interface ExchangeCodeAzureResponse { @@ -159,9 +159,9 @@ const exchangeCodeAzure = async ({ grant_type: 'authorization_code', code: code, scope: 'https://vault.azure.net/.default openid offline_access', - client_id: CLIENT_ID_AZURE, - client_secret: CLIENT_SECRET_AZURE, - redirect_uri: `${SITE_URL}/integrations/azure-key-vault/oauth2/callback` + client_id: getClientIdAzure(), + client_secret: getClientSecretAzure(), + redirect_uri: `${getSiteURL()}/integrations/azure-key-vault/oauth2/callback` } as any) )).data; @@ -204,7 +204,7 @@ const exchangeCodeHeroku = async ({ new URLSearchParams({ grant_type: 'authorization_code', code: code, - client_secret: CLIENT_SECRET_HEROKU + client_secret: getClientSecretHeroku() } as any) )).data; @@ -242,9 +242,9 @@ const exchangeCodeVercel = async ({ code }: { code: string }) => { INTEGRATION_VERCEL_TOKEN_URL, new URLSearchParams({ code: code, - client_id: CLIENT_ID_VERCEL, - client_secret: CLIENT_SECRET_VERCEL, - redirect_uri: `${SITE_URL}/integrations/vercel/oauth2/callback` + client_id: getClientIdVercel(), + client_secret: getClientSecretVercel(), + redirect_uri: `${getSiteURL()}/integrations/vercel/oauth2/callback` } as any) ) ).data; @@ -282,9 +282,9 @@ const exchangeCodeNetlify = async ({ code }: { code: string }) => { new URLSearchParams({ grant_type: 'authorization_code', code: code, - client_id: CLIENT_ID_NETLIFY, - client_secret: CLIENT_SECRET_NETLIFY, - redirect_uri: `${SITE_URL}/integrations/netlify/oauth2/callback` + client_id: getClientIdNetlify(), + client_secret: getClientSecretNetlify(), + redirect_uri: `${getSiteURL()}/integrations/netlify/oauth2/callback` } as any) ) ).data; @@ -333,10 +333,10 @@ const exchangeCodeGithub = async ({ code }: { code: string }) => { res = ( await request.get(INTEGRATION_GITHUB_TOKEN_URL, { params: { - client_id: CLIENT_ID_GITHUB, - client_secret: CLIENT_SECRET_GITHUB, + client_id: getClientIdGitHub(), + client_secret: getClientSecretGitHub(), code: code, - redirect_uri: `${SITE_URL}/integrations/github/oauth2/callback` + redirect_uri: `${getSiteURL()}/integrations/github/oauth2/callback` }, headers: { 'Accept': 'application/json', @@ -379,9 +379,9 @@ const exchangeCodeGitlab = async ({ code }: { code: string }) => { new URLSearchParams({ grant_type: 'authorization_code', code: code, - client_id: CLIENT_ID_GITLAB, - client_secret: CLIENT_SECRET_GITLAB, - redirect_uri: `${SITE_URL}/integrations/gitlab/oauth2/callback` + client_id: getClientIdGitLab(), + client_secret: getClientSecretGitLab(), + redirect_uri: `${getSiteURL()}/integrations/gitlab/oauth2/callback` } as any), { headers: { diff --git a/backend/src/integrations/refresh.ts b/backend/src/integrations/refresh.ts index a3aec1ebd..a0aea080e 100644 --- a/backend/src/integrations/refresh.ts +++ b/backend/src/integrations/refresh.ts @@ -1,5 +1,5 @@ -import request from '../config/request'; import * as Sentry from '@sentry/node'; +import request from '../config/request'; import { IIntegrationAuth } from '../models'; @@ -8,14 +8,6 @@ import { INTEGRATION_HEROKU, INTEGRATION_GITLAB, } from '../variables'; -import { - SITE_URL, - CLIENT_ID_AZURE, - CLIENT_ID_GITLAB, - CLIENT_SECRET_AZURE, - CLIENT_SECRET_HEROKU, - CLIENT_SECRET_GITLAB -} from '../config'; import { INTEGRATION_AZURE_TOKEN_URL, INTEGRATION_HEROKU_TOKEN_URL, @@ -24,6 +16,14 @@ import { import { IntegrationService } from '../services'; +import { + getSiteURL, + getClientIdAzure, + getClientSecretAzure, + getClientSecretHeroku, + getClientIdGitLab, + getClientSecretGitLab +} from '../config'; interface RefreshTokenAzureResponse { token_type: string; @@ -133,11 +133,11 @@ const exchangeRefreshAzure = async ({ const { data }: { data: RefreshTokenAzureResponse } = await request.post( INTEGRATION_AZURE_TOKEN_URL, new URLSearchParams({ - client_id: CLIENT_ID_AZURE, + client_id: getClientIdAzure(), scope: 'openid offline_access', refresh_token: refreshToken, grant_type: 'refresh_token', - client_secret: CLIENT_SECRET_AZURE + client_secret: getClientSecretAzure() } as any) ); @@ -180,7 +180,7 @@ const exchangeRefreshHeroku = async ({ new URLSearchParams({ grant_type: 'refresh_token', refresh_token: refreshToken, - client_secret: CLIENT_SECRET_HEROKU + client_secret: getClientSecretHeroku() } as any) ); @@ -223,9 +223,9 @@ const exchangeRefreshGitLab = async ({ new URLSearchParams({ grant_type: 'refresh_token', refresh_token: refreshToken, - client_id: CLIENT_ID_GITLAB, - client_secret: CLIENT_SECRET_GITLAB, - redirect_uri: `${SITE_URL}/integrations/gitlab/oauth2/callback` + client_id: getClientIdGitLab, + client_secret: getClientSecretGitLab(), + redirect_uri: `${getSiteURL()}/integrations/gitlab/oauth2/callback` } as any), { headers: { diff --git a/backend/src/middleware/requestErrorHandler.ts b/backend/src/middleware/requestErrorHandler.ts index 653c0df2e..202a38e83 100644 --- a/backend/src/middleware/requestErrorHandler.ts +++ b/backend/src/middleware/requestErrorHandler.ts @@ -1,16 +1,13 @@ -import { ErrorRequestHandler } from "express"; - 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 RequestError, { LogLevel } from "../utils/requestError"; -import { NODE_ENV } from "../config"; - -import { TokenExpiredError } from 'jsonwebtoken'; +import { getNodeEnv } from '../config'; export const requestErrorHandler: ErrorRequestHandler = (error: RequestError | Error, req, res, next) => { if (res.headersSent) return next(); - if (NODE_ENV !== "production") { + if (getNodeEnv() !== "production") { /* eslint-disable no-console */ console.log(error) /* eslint-enable no-console */ diff --git a/backend/src/middleware/requireMfaAuth.ts b/backend/src/middleware/requireMfaAuth.ts index 8fb914258..7fb38ca25 100644 --- a/backend/src/middleware/requireMfaAuth.ts +++ b/backend/src/middleware/requireMfaAuth.ts @@ -1,8 +1,8 @@ import jwt from 'jsonwebtoken'; import { Request, Response, NextFunction } from 'express'; import { User } from '../models'; -import { JWT_MFA_SECRET } from '../config'; import { BadRequestError, UnauthorizedRequestError } from '../utils/errors'; +import { getJwtMfaSecret } from '../config'; declare module 'jsonwebtoken' { 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'})) const decodedToken = ( - jwt.verify(AUTH_TOKEN_VALUE, JWT_MFA_SECRET) + jwt.verify(AUTH_TOKEN_VALUE, getJwtMfaSecret()) ); const user = await User.findOne({ diff --git a/backend/src/middleware/requireServiceTokenAuth.ts b/backend/src/middleware/requireServiceTokenAuth.ts index 904f4d38e..106ca9bbb 100644 --- a/backend/src/middleware/requireServiceTokenAuth.ts +++ b/backend/src/middleware/requireServiceTokenAuth.ts @@ -1,8 +1,8 @@ import jwt from 'jsonwebtoken'; import { Request, Response, NextFunction } from 'express'; import { ServiceToken } from '../models'; -import { JWT_SERVICE_SECRET } from '../config'; import { BadRequestError, UnauthorizedRequestError } from '../utils/errors'; +import { getJwtServiceSecret } from '../config'; // TODO: deprecate 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'})) const decodedToken = ( - jwt.verify(AUTH_TOKEN_VALUE, JWT_SERVICE_SECRET) + jwt.verify(AUTH_TOKEN_VALUE, getJwtServiceSecret()) ); const serviceToken = await ServiceToken.findOne({ diff --git a/backend/src/middleware/requireSignupAuth.ts b/backend/src/middleware/requireSignupAuth.ts index 3318bd8d3..19e6b3146 100644 --- a/backend/src/middleware/requireSignupAuth.ts +++ b/backend/src/middleware/requireSignupAuth.ts @@ -1,8 +1,8 @@ import jwt from 'jsonwebtoken'; import { Request, Response, NextFunction } from 'express'; import { User } from '../models'; -import { JWT_SIGNUP_SECRET } from '../config'; import { BadRequestError, UnauthorizedRequestError } from '../utils/errors'; +import { getJwtSignupSecret } from '../config'; declare module 'jsonwebtoken' { 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'})) const decodedToken = ( - jwt.verify(AUTH_TOKEN_VALUE, JWT_SIGNUP_SECRET) + jwt.verify(AUTH_TOKEN_VALUE, getJwtSignupSecret()) ); const user = await User.findOne({ diff --git a/backend/src/models/token.ts b/backend/src/models/token.ts index c003e42a2..e6f485f55 100644 --- a/backend/src/models/token.ts +++ b/backend/src/models/token.ts @@ -1,5 +1,4 @@ import { Schema, model } from 'mongoose'; -import { EMAIL_TOKEN_LIFETIME } from '../config'; export interface IToken { email: string; diff --git a/backend/src/routes/status/status.ts b/backend/src/routes/status/status.ts index 4ab82cca9..f793128be 100644 --- a/backend/src/routes/status/status.ts +++ b/backend/src/routes/status/status.ts @@ -1,4 +1,5 @@ import express, { Request, Response } from 'express'; +import { getSmtpConfigured } from '../../config'; const router = express.Router(); @@ -8,6 +9,7 @@ router.get( res.status(200).json({ date: new Date(), message: 'Ok', + emailConfigured: getSmtpConfigured() }) } ); diff --git a/backend/src/services/DatabaseService.ts b/backend/src/services/DatabaseService.ts index 2e8dc839f..fdfd7660a 100644 --- a/backend/src/services/DatabaseService.ts +++ b/backend/src/services/DatabaseService.ts @@ -1,16 +1,32 @@ import mongoose from 'mongoose'; import { getLogger } from '../utils/logger'; -import { initDatabaseHelper } from '../helpers/database'; +import { + initDatabaseHelper, + closeDatabaseHelper +} from '../helpers/database'; /** * Class to handle database actions */ class DatabaseService { + /** + * Initialize database connection + * @param {Object} obj + * @param {String} obj.mongoURL - mongo connection string + * @returns + */ static async initDatabase(MONGO_URL: string) { return await initDatabaseHelper({ mongoURL: MONGO_URL }); } + + /** + * Close database conection + */ + static async closeDatabase() { + return await closeDatabaseHelper(); + } } export default DatabaseService; \ No newline at end of file diff --git a/backend/src/services/PostHogClient.ts b/backend/src/services/PostHogClient.ts index 0d91a1c13..15ccf0919 100644 --- a/backend/src/services/PostHogClient.ts +++ b/backend/src/services/PostHogClient.ts @@ -1,27 +1,44 @@ import { PostHog } from 'posthog-node'; -import { - NODE_ENV, - POSTHOG_HOST, - POSTHOG_PROJECT_API_KEY, - TELEMETRY_ENABLED -} from '../config'; import { getLogger } from '../utils/logger'; +import { + getNodeEnv, + getTelemetryEnabled, + getPostHogProjectApiKey, + getPostHogHost +} from '../config'; -if(!TELEMETRY_ENABLED){ - getLogger("backend-main").info([ - "", - "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')) +/** + * Logs telemetry enable/disable notice. + */ +const logTelemetryMessage = () => { + if(!getTelemetryEnabled()){ + getLogger("backend-main").info([ + "", + "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) { - // case: enable opt-out telemetry in production - postHogClient = new PostHog(POSTHOG_PROJECT_API_KEY, { - host: POSTHOG_HOST - }); +/** + * Return an instance of the PostHog client initialized. + * @returns + */ +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; diff --git a/backend/src/services/index.ts b/backend/src/services/index.ts index 8ac393cf5..d98b70718 100644 --- a/backend/src/services/index.ts +++ b/backend/src/services/index.ts @@ -1,13 +1,14 @@ import DatabaseService from './DatabaseService'; -import postHogClient from './PostHogClient'; +import { logTelemetryMessage, getPostHogClient } from './PostHogClient'; import BotService from './BotService'; import EventService from './EventService'; import IntegrationService from './IntegrationService'; import TokenService from './TokenService'; export { + logTelemetryMessage, + getPostHogClient, DatabaseService, - postHogClient, BotService, EventService, IntegrationService, diff --git a/backend/src/services/smtp.ts b/backend/src/services/smtp.ts index 1bf809593..7a4ebf00b 100644 --- a/backend/src/services/smtp.ts +++ b/backend/src/services/smtp.ts @@ -1,11 +1,4 @@ import nodemailer from 'nodemailer'; -import { - SMTP_HOST, - SMTP_PORT, - SMTP_USERNAME, - SMTP_PASSWORD, - SMTP_SECURE -} from '../config'; import { SMTP_HOST_SENDGRID, SMTP_HOST_MAILGUN, @@ -14,55 +7,62 @@ import { } from '../variables'; import SMTPConnection from 'nodemailer/lib/smtp-connection'; import * as Sentry from '@sentry/node'; +import { + getSmtpHost, + getSmtpUsername, + getSmtpPassword, + getSmtpSecure, + getSmtpPort +} from '../config'; -const mailOpts: SMTPConnection.Options = { - host: SMTP_HOST, - port: SMTP_PORT as number -}; - -if (SMTP_USERNAME && SMTP_PASSWORD) { - mailOpts.auth = { - user: SMTP_USERNAME, - pass: SMTP_PASSWORD +export const initSmtp = () => { + const mailOpts: SMTPConnection.Options = { + host: getSmtpHost(), + port: getSmtpPort() }; -} -if (SMTP_SECURE) { - switch (SMTP_HOST) { - case SMTP_HOST_SENDGRID: - mailOpts.requireTLS = true; - break; - case SMTP_HOST_MAILGUN: - mailOpts.requireTLS = true; - mailOpts.tls = { - ciphers: 'TLSv1.2' - } - break; - case SMTP_HOST_SOCKETLABS: - mailOpts.requireTLS = true; - mailOpts.tls = { - ciphers: 'TLSv1.2' - } - break; - case SMTP_HOST_ZOHOMAIL: - mailOpts.requireTLS = true; - mailOpts.tls = { - ciphers: 'TLSv1.2' - } - break; - default: - if (SMTP_HOST.includes('amazonaws.com')) { + if (getSmtpUsername() && getSmtpPassword()) { + mailOpts.auth = { + user: getSmtpUsername(), + pass: getSmtpPassword() + }; + } + + if (getSmtpSecure() ? getSmtpSecure() : false) { + switch (getSmtpHost()) { + case SMTP_HOST_SENDGRID: + mailOpts.requireTLS = true; + break; + case SMTP_HOST_MAILGUN: + mailOpts.requireTLS = true; mailOpts.tls = { ciphers: 'TLSv1.2' } - } else { - mailOpts.secure = true; - } - break; + break; + case SMTP_HOST_SOCKETLABS: + mailOpts.requireTLS = true; + 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); transporter .verify() @@ -73,7 +73,7 @@ export const initSmtp = () => { .catch((err) => { Sentry.setUser(null); Sentry.captureException( - `SMTP - Failed to connect to ${SMTP_HOST}:${SMTP_PORT} \n\t${err}` + `SMTP - Failed to connect to ${getSmtpHost()}:${getSmtpPort()} \n\t${err}` ); }); diff --git a/backend/src/utils/addDevelopmentUser.ts b/backend/src/utils/addDevelopmentUser.ts index 2020cf3ad..585740a6b 100644 --- a/backend/src/utils/addDevelopmentUser.ts +++ b/backend/src/utils/addDevelopmentUser.ts @@ -4,12 +4,12 @@ * ************************************************************************************************/ -import { NODE_ENV } from "../config" import { Key, Membership, MembershipOrg, Organization, User, Workspace } from "../models"; import { Types } from 'mongoose'; +import { getNodeEnv } from '../config'; export const createTestUserForDevelopment = async () => { - if (NODE_ENV === "development") { + if (getNodeEnv() === "development") { const testUserEmail = "test@localhost.local" const testUserPassword = "testInfisical1" const testUserId = "63cefa6ec8d3175601cfa980" diff --git a/backend/src/utils/logger.ts b/backend/src/utils/logger.ts index 64c65ea49..ed29c97ca 100644 --- a/backend/src/utils/logger.ts +++ b/backend/src/utils/logger.ts @@ -1,7 +1,7 @@ /* eslint-disable no-console */ import { createLogger, format, transports } from 'winston'; 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; @@ -25,10 +25,10 @@ const createLoggerWithLabel = (level: string, label: string) => { }) ] //* Add LokiTransport if it's enabled - if(LOKI_HOST !== undefined){ + if(getLokiHost() !== undefined){ _transports.push( new LokiTransport({ - host: LOKI_HOST, + host: getLokiHost(), handleExceptions: true, handleRejections: true, batching: true, @@ -37,7 +37,11 @@ const createLoggerWithLabel = (level: string, label: string) => { format: format.combine( 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) }) ) diff --git a/backend/src/utils/requestError.ts b/backend/src/utils/requestError.ts index da2803da7..4b5635bac 100644 --- a/backend/src/utils/requestError.ts +++ b/backend/src/utils/requestError.ts @@ -1,5 +1,5 @@ import { Request } from 'express' -import { VERBOSE_ERROR_OUTPUT } from '../config' +import { getVerboseErrorOutput } from '../config'; export enum LogLevel { DEBUG = 100, @@ -87,7 +87,7 @@ export default class RequestError extends Error{ }, this.context) //* 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, [ 'stacktrace', 'exception', diff --git a/backend/src/variables/index.ts b/backend/src/variables/index.ts index f7c86e0f8..b71044cba 100644 --- a/backend/src/variables/index.ts +++ b/backend/src/variables/index.ts @@ -34,7 +34,7 @@ import { INTEGRATION_FLYIO_API_URL, INTEGRATION_CIRCLECI_API_URL, INTEGRATION_TRAVISCI_API_URL, - INTEGRATION_OPTIONS, + getIntegrationOptions } from "./integration"; import { OWNER, ADMIN, MEMBER, INVITED, ACCEPTED } from "./organization"; import { SECRET_SHARED, SECRET_PERSONAL } from "./secret"; @@ -113,7 +113,7 @@ export { ACTION_UPDATE_SECRETS, ACTION_DELETE_SECRETS, ACTION_READ_SECRETS, - INTEGRATION_OPTIONS, + getIntegrationOptions, SMTP_HOST_SENDGRID, SMTP_HOST_MAILGUN, SMTP_HOST_SOCKETLABS, diff --git a/backend/src/variables/integration.ts b/backend/src/variables/integration.ts index 5ed93a597..52bfcc614 100644 --- a/backend/src/variables/integration.ts +++ b/backend/src/variables/integration.ts @@ -1,13 +1,11 @@ import { - CLIENT_ID_AZURE, - CLIENT_ID_GITLAB + getClientIdHeroku, + getClientSlugVercel, + getClientIdNetlify, + getClientIdAzure, + getClientIdGitLab, + getClientIdGitHub } from '../config'; -import { - CLIENT_ID_HEROKU, - CLIENT_ID_NETLIFY, - CLIENT_ID_GITHUB, - CLIENT_SLUG_VERCEL -} from "../config"; // integrations const INTEGRATION_AZURE_KEY_VAULT = 'azure-key-vault'; @@ -48,7 +46,6 @@ const INTEGRATION_GITHUB_TOKEN_URL = "https://github.com/login/oauth/access_token"; const INTEGRATION_GITLAB_TOKEN_URL = "https://gitlab.com/oauth/token"; - // integration apps endpoints const INTEGRATION_HEROKU_API_URL = "https://api.heroku.com"; 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_TRAVISCI_API_URL = "https://api.travis-ci.com"; -// TODO: deprecate types? -const INTEGRATION_OPTIONS = [ - { - name: 'Heroku', - slug: 'heroku', - image: 'Heroku.png', - isAvailable: true, - type: 'oauth', - clientId: CLIENT_ID_HEROKU, - docsLink: '' - }, - { - name: 'Vercel', - slug: 'vercel', - image: 'Vercel.png', - isAvailable: true, - type: 'oauth', - clientId: '', - clientSlug: CLIENT_SLUG_VERCEL, - docsLink: '' - }, - { - name: 'Netlify', - slug: 'netlify', - image: 'Netlify.png', - isAvailable: true, - type: 'oauth', - clientId: CLIENT_ID_NETLIFY, - docsLink: '' - }, - { - name: 'GitHub', - slug: 'github', - image: 'GitHub.png', - isAvailable: true, - type: 'oauth', - clientId: CLIENT_ID_GITHUB, - docsLink: '' - }, - { - name: 'Render', - slug: 'render', - image: 'Render.png', - isAvailable: true, - type: 'pat', - clientId: '', - docsLink: '' - }, - { - name: 'Fly.io', - slug: 'flyio', - image: 'Flyio.svg', - isAvailable: true, - type: 'pat', - clientId: '', - docsLink: '' - }, - { - name: 'AWS Parameter Store', - slug: 'aws-parameter-store', - image: 'Amazon Web Services.png', - isAvailable: true, - type: 'custom', - clientId: '', - docsLink: '' - }, - { - name: 'AWS Secret Manager', - slug: 'aws-secret-manager', - image: 'Amazon Web Services.png', - isAvailable: true, - type: 'custom', - clientId: '', - docsLink: '' - }, - { - name: 'Azure Key Vault', - slug: 'azure-key-vault', - image: 'Microsoft Azure.png', - isAvailable: true, - type: 'oauth', - clientId: CLIENT_ID_AZURE, - docsLink: '' - }, - { - name: 'Circle CI', - slug: 'circleci', - image: 'Circle CI.png', - isAvailable: true, - type: 'pat', - clientId: '', - docsLink: '' - }, - { - name: 'GitLab', - slug: 'gitlab', - image: 'GitLab.png', - isAvailable: true, - type: 'custom', - clientId: CLIENT_ID_GITLAB, - docsLink: '' - }, - { - name: 'Travis CI', - slug: 'travisci', - image: 'Travis CI.png', - isAvailable: true, - type: 'pat', - clientId: '', - docsLink: '' - }, - { - name: 'Google Cloud Platform', - slug: 'gcp', - image: 'Google Cloud Platform.png', - isAvailable: false, - type: '', - clientId: '', - docsLink: '' - } -] +const getIntegrationOptions = () => { + const INTEGRATION_OPTIONS = [ + { + name: 'Heroku', + slug: 'heroku', + image: 'Heroku.png', + isAvailable: true, + type: 'oauth', + clientId: getClientIdHeroku(), + docsLink: '' + }, + { + name: 'Vercel', + slug: 'vercel', + image: 'Vercel.png', + isAvailable: true, + type: 'oauth', + clientId: '', + clientSlug: getClientSlugVercel(), + docsLink: '' + }, + { + name: 'Netlify', + slug: 'netlify', + image: 'Netlify.png', + isAvailable: true, + type: 'oauth', + clientId: getClientIdNetlify(), + docsLink: '' + }, + { + name: 'GitHub', + slug: 'github', + image: 'GitHub.png', + isAvailable: true, + type: 'oauth', + clientId: getClientIdGitHub(), + docsLink: '' + }, + { + name: 'Render', + slug: 'render', + image: 'Render.png', + isAvailable: true, + type: 'pat', + clientId: '', + docsLink: '' + }, + { + name: 'Fly.io', + slug: 'flyio', + image: 'Flyio.svg', + isAvailable: true, + type: 'pat', + clientId: '', + docsLink: '' + }, + { + name: 'AWS Parameter Store', + slug: 'aws-parameter-store', + image: 'Amazon Web Services.png', + isAvailable: true, + type: 'custom', + clientId: '', + docsLink: '' + }, + { + name: 'AWS Secret Manager', + slug: 'aws-secret-manager', + image: 'Amazon Web Services.png', + isAvailable: true, + type: 'custom', + clientId: '', + docsLink: '' + }, + { + name: 'Azure Key Vault', + slug: 'azure-key-vault', + image: 'Microsoft Azure.png', + isAvailable: true, + type: 'oauth', + clientId: getClientIdAzure(), + docsLink: '' + }, + { + name: 'Circle CI', + slug: 'circleci', + image: 'Circle CI.png', + isAvailable: true, + type: 'pat', + clientId: '', + docsLink: '' + }, + { + name: 'GitLab', + slug: 'gitlab', + image: 'GitLab.png', + isAvailable: true, + type: 'custom', + clientId: getClientIdGitLab(), + docsLink: '' + }, + { + name: 'Travis CI', + slug: 'travisci', + image: 'Travis CI.png', + isAvailable: true, + type: 'pat', + clientId: '', + docsLink: '' + }, + { + name: 'Google Cloud Platform', + slug: 'gcp', + image: 'Google Cloud Platform.png', + isAvailable: false, + type: '', + clientId: '', + docsLink: '' + } + ] + + return INTEGRATION_OPTIONS; +} + export { INTEGRATION_AZURE_KEY_VAULT, INTEGRATION_AWS_PARAMETER_STORE, INTEGRATION_AWS_SECRET_MANAGER, - INTEGRATION_HEROKU, - INTEGRATION_VERCEL, - INTEGRATION_NETLIFY, - INTEGRATION_GITHUB, - INTEGRATION_GITLAB, - INTEGRATION_RENDER, - INTEGRATION_FLYIO, - INTEGRATION_CIRCLECI, - INTEGRATION_TRAVISCI, - INTEGRATION_SET, - INTEGRATION_OAUTH2, + INTEGRATION_HEROKU, + INTEGRATION_VERCEL, + INTEGRATION_NETLIFY, + INTEGRATION_GITHUB, + INTEGRATION_GITLAB, + INTEGRATION_RENDER, + INTEGRATION_FLYIO, + INTEGRATION_CIRCLECI, + INTEGRATION_TRAVISCI, + INTEGRATION_SET, + INTEGRATION_OAUTH2, INTEGRATION_AZURE_TOKEN_URL, - INTEGRATION_HEROKU_TOKEN_URL, - INTEGRATION_VERCEL_TOKEN_URL, - INTEGRATION_NETLIFY_TOKEN_URL, - INTEGRATION_GITHUB_TOKEN_URL, - INTEGRATION_GITLAB_API_URL, - INTEGRATION_HEROKU_API_URL, - INTEGRATION_GITLAB_TOKEN_URL, - INTEGRATION_VERCEL_API_URL, - INTEGRATION_NETLIFY_API_URL, - INTEGRATION_RENDER_API_URL, - INTEGRATION_FLYIO_API_URL, - INTEGRATION_CIRCLECI_API_URL, - INTEGRATION_TRAVISCI_API_URL, - INTEGRATION_OPTIONS, + INTEGRATION_HEROKU_TOKEN_URL, + INTEGRATION_VERCEL_TOKEN_URL, + INTEGRATION_NETLIFY_TOKEN_URL, + INTEGRATION_GITHUB_TOKEN_URL, + INTEGRATION_GITLAB_API_URL, + INTEGRATION_HEROKU_API_URL, + INTEGRATION_GITLAB_TOKEN_URL, + INTEGRATION_VERCEL_API_URL, + INTEGRATION_NETLIFY_API_URL, + INTEGRATION_RENDER_API_URL, + INTEGRATION_FLYIO_API_URL, + INTEGRATION_CIRCLECI_API_URL, + INTEGRATION_TRAVISCI_API_URL, + getIntegrationOptions }; diff --git a/backend/tests/example.test.ts b/backend/tests/example.test.ts new file mode 100644 index 000000000..6d38025a5 --- /dev/null +++ b/backend/tests/example.test.ts @@ -0,0 +1,5 @@ +import { it, expect } from '@jest/globals'; + +it('should return true', () => { + expect(true).toBeTruthy(); +}); diff --git a/backend/tests/setupTests.ts b/backend/tests/setupTests.ts new file mode 100644 index 000000000..903347a2a --- /dev/null +++ b/backend/tests/setupTests.ts @@ -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); + }); +}); diff --git a/cloudformation/ec2-deployment/infisical-ec2-deployment.template b/cloudformation/ec2-deployment/infisical-ec2-deployment.template new file mode 100644 index 000000000..36f0d9dce --- /dev/null +++ b/cloudformation/ec2-deployment/infisical-ec2-deployment.template @@ -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: [] \ No newline at end of file diff --git a/docker-compose.dev.yml b/docker-compose.dev.yml index 007884694..287e80945 100644 --- a/docker-compose.dev.yml +++ b/docker-compose.dev.yml @@ -34,6 +34,7 @@ services: env_file: .env environment: - NODE_ENV=development + - MONGO_URL=mongodb://root:example@mongo:27017/?authSource=admin networks: - infisical-dev @@ -65,8 +66,8 @@ services: restart: always env_file: .env environment: - - MONGO_INITDB_ROOT_USERNAME=${MONGO_USERNAME} - - MONGO_INITDB_ROOT_PASSWORD=${MONGO_PASSWORD} + - MONGO_INITDB_ROOT_USERNAME=root + - MONGO_INITDB_ROOT_PASSWORD=example volumes: - mongo-data:/data/db networks: @@ -80,9 +81,9 @@ services: - mongo env_file: .env environment: - - ME_CONFIG_MONGODB_ADMINUSERNAME=${MONGO_USERNAME} - - ME_CONFIG_MONGODB_ADMINPASSWORD=${MONGO_PASSWORD} - - ME_CONFIG_MONGODB_URL=mongodb://${MONGO_USERNAME}:${MONGO_PASSWORD}@mongo:27017/ + - ME_CONFIG_MONGODB_ADMINUSERNAME=root + - ME_CONFIG_MONGODB_ADMINPASSWORD=example + - ME_CONFIG_MONGODB_URL=mongodb://root:example@mongo:27017/ ports: - 8081:8081 networks: diff --git a/docs/getting-started/dashboard/token.mdx b/docs/getting-started/dashboard/token.mdx index 0caaa81d7..caeb1641d 100644 --- a/docs/getting-started/dashboard/token.mdx +++ b/docs/getting-started/dashboard/token.mdx @@ -1,26 +1,21 @@ --- 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. - ![token add](../../images/project-token-add.png) ## 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. This allows you to use the CLI in environments where you are unable to run the `infisical login` command. - - - 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. - - diff --git a/docs/getting-started/features.mdx b/docs/getting-started/features.mdx index 5b163806b..9eb1d4faf 100644 --- a/docs/getting-started/features.mdx +++ b/docs/getting-started/features.mdx @@ -1,10 +1,8 @@ --- 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 - Provision members access to organizations and projects. @@ -16,29 +14,17 @@ This is a non-exhaustive list of features that Infisical offers: ## 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 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 -We're building the future of secret management, one that's comprehensive and accessible to all. Some high-level features we have in mind: - -| 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 | +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). Interested in contributing? Check out the [guide](/contributing/overview). diff --git a/docs/getting-started/introduction.mdx b/docs/getting-started/introduction.mdx index ff8b72c4d..cd5022083 100644 --- a/docs/getting-started/introduction.mdx +++ b/docs/getting-started/introduction.mdx @@ -19,6 +19,9 @@ Start syncing environment variables with [Infisical Cloud](https://app.infisical Install the CLI to inject secrets into apps and infra. + + Install an SDK into your app to fetch secrets. + Learn how to configure and deploy Infisical. + + Explore integrations for Docker, AWS, Heroku, etc. - - - - - 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)**. - diff --git a/docs/getting-started/quickstart.mdx b/docs/getting-started/quickstart.mdx index e3acd440d..256ae8f85 100644 --- a/docs/getting-started/quickstart.mdx +++ b/docs/getting-started/quickstart.mdx @@ -3,9 +3,7 @@ title: "Quickstart" 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. - -Note that the Infisical CLI is platform-agnostic and can inject environment variables across many tech stacks and frameworks. +These examples demonstrate how to store and fetch environment variables from [Infisical Cloud](https://app.infisical.com) into your application. ## 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) -## Set up the CLI +## Fetch Secrets for Your App -1. Follow the instructions to [install the CLI](/cli/overview). + + + + 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 -# move to your project -cd /path/to/project + 1. Follow the instructions to [install our platform-agnostic CLI](/cli/overview). -# initialize infisical -infisical init -``` + 2. Initialize Infisical for your project. -## Start your app with environment variables injected + ```bash + # move to your project + cd /path/to/project -```bash -# inject environment variables into app -infisical run -- [your application start command] -``` + # initialize infisical + infisical init + ``` - - Check out our [integrations](/integrations/overview) for injecting environment - variables into frameworks and platforms like Docker. - + ### Start your app with environment variables injected -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. + + + + [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}`); + }); + }; + ``` + + + We do not recommend hardcoding your [Infisical + Token](/getting-started/dashboard/token). Setting it as an environment + variable would be best. + + + + Check out our [SDKs](/sdks/overview) for other language SDKs. + + + diff --git a/docs/images/deploy-aws-button.png b/docs/images/deploy-aws-button.png new file mode 100644 index 000000000..71d484c6a Binary files /dev/null and b/docs/images/deploy-aws-button.png differ diff --git a/docs/integrations/platforms/docker.mdx b/docs/integrations/platforms/docker.mdx index 5220833d8..a19562a99 100644 --- a/docs/integrations/platforms/docker.mdx +++ b/docs/integrations/platforms/docker.mdx @@ -39,10 +39,10 @@ Prerequisites: Starting your service with the Infisical CLI pulls your secrets from Infisical and injects them into your service. ```dockerfile -CMD ["infisical", "run", "---", "[your service start command]"] +CMD ["infisical", "run", "--", "[your service start command]"] # example with single single command -CMD ["infisical", "run", "---", "npm", "run", "start"] +CMD ["infisical", "run", "--", "npm", "run", "start"] # example with multiple commands 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 ```bash - docker run --env INFISICAL_TOKEN=[token]... + docker run --env INFISICAL_TOKEN=[token] [DOCKER-IMAGE]... ``` diff --git a/docs/integrations/platforms/kubernetes.mdx b/docs/integrations/platforms/kubernetes.mdx index 069fe1ed4..959a4fce2 100644 --- a/docs/integrations/platforms/kubernetes.mdx +++ b/docs/integrations/platforms/kubernetes.mdx @@ -362,7 +362,7 @@ The managed secret created by the operator will not be deleted when the operator Install Infisical Helm repository ```bash - helm uninstall add + helm uninstall ``` diff --git a/docs/mint.json b/docs/mint.json index 697a7bfeb..48dc1805c 100644 --- a/docs/mint.json +++ b/docs/mint.json @@ -45,14 +45,9 @@ "url": "security" }, { - "name": "Self-hosting", - "icon": "server", - "url": "self-hosting" - }, - { - "name": "SDK", + "name": "SDKs", "icon": "puzzle-piece", - "url": "sdk" + "url": "sdks" }, { "name": "API Reference", @@ -101,6 +96,14 @@ "getting-started/dashboard/token" ] }, + { + "group": "Self-hosting", + "pages": [ + "self-hosting/overview", + "self-hosting/configuration/envars", + "self-hosting/configuration/email" + ] + }, { "group": "Command line", "pages": [ @@ -171,12 +174,6 @@ "integrations/platforms/pm2" ] }, - { - "group": "Self-hosting", - "pages": [ - "self-hosting/overview" - ] - }, { "group": "Deployment options", "pages": [ @@ -185,16 +182,20 @@ ] }, { - "group": "Configuration", + "group": "Overview", "pages": [ - "self-hosting/configuration/envars", - "self-hosting/configuration/email" + "sdks/overview" ] }, { - "group": "SDK", + "group": "SDKs", "pages": [ - "sdk/overview/usage" + "sdks/languages/node", + "sdks/languages/python", + "sdks/languages/java", + "sdks/languages/ruby", + "sdks/languages/go", + "sdks/languages/rust" ] }, { diff --git a/docs/sdk/overview/usage.mdx b/docs/sdk/overview/usage.mdx deleted file mode 100644 index 2708f9a2b..000000000 --- a/docs/sdk/overview/usage.mdx +++ /dev/null @@ -1,104 +0,0 @@ ---- -title: "Usage" ---- - - - 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. - - -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. - - - - -## 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}`); -}); -``` - - - - Coming soon. - - - diff --git a/docs/sdks/languages/go.mdx b/docs/sdks/languages/go.mdx new file mode 100644 index 000000000..0ff2a2dde --- /dev/null +++ b/docs/sdks/languages/go.mdx @@ -0,0 +1,8 @@ +--- +title: "Go" +--- + +Coming soon. + +Follow this GitHub +[issue](https://github.com/Infisical/infisical/issues/436) to stay updated. diff --git a/docs/sdks/languages/java.mdx b/docs/sdks/languages/java.mdx new file mode 100644 index 000000000..2dd2c3c1d --- /dev/null +++ b/docs/sdks/languages/java.mdx @@ -0,0 +1,8 @@ +--- +title: "Java" +--- + +Coming soon. + +Follow this GitHub +[issue](https://github.com/Infisical/infisical/issues/434) to stay updated. diff --git a/docs/sdks/languages/node.mdx b/docs/sdks/languages/node.mdx new file mode 100644 index 000000000..58002392f --- /dev/null +++ b/docs/sdks/languages/node.mdx @@ -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). + + + + + An [Infisical Token](/getting-started/dashboard/token) scoped to a project + and environment + + + Your self-hosted absolute site URL including the protocol (e.g. + `https://app.infisical.com`) + + + Whether or not debug mode is on + + + Whether or not to attach fetched secrets to `process.env` + + + + +### 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. + + + + + An [Infisical Token](/getting-started/dashboard/token) scoped to a project + and environment + + + Your self-hosted absolute site URL including the protocol (e.g. + `https://app.infisical.com`) + + + Whether or not debug mode is on + + + + + + + ```js + import infisical from "infisical-node"; + + const main = async () => { + await infisical.connect({ + token: "your_infisical_token", + }); + + // your app logic + } + + main(); + ``` + + + + ```js + const infisical = require("infisical-node"); + + infisical.connect({ + token: "your_infisical_token" + }) + .then(() => { + // your application logic + }) + .catch(err => { + console.error('Error: ', err); + }) + ```` + + + + +## 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. + + + The key of the secret + + +```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}`); + }); +}; +``` + + + We do not recommend hardcoding your [Infisical + Token](/getting-started/dashboard/token). Setting it as an environment + variable would be best. + diff --git a/docs/sdks/languages/python.mdx b/docs/sdks/languages/python.mdx new file mode 100644 index 000000000..3a9b82152 --- /dev/null +++ b/docs/sdks/languages/python.mdx @@ -0,0 +1,8 @@ +--- +title: "Python" +--- + +Coming soon. + +Follow this GitHub +[issue](https://github.com/Infisical/infisical/issues/433) to stay updated. diff --git a/docs/sdks/languages/ruby.mdx b/docs/sdks/languages/ruby.mdx new file mode 100644 index 000000000..80dab508a --- /dev/null +++ b/docs/sdks/languages/ruby.mdx @@ -0,0 +1,8 @@ +--- +title: "Ruby" +--- + +Coming soon. + +Follow this GitHub +[issue](https://github.com/Infisical/infisical/issues/435) to stay updated. diff --git a/docs/sdks/languages/rust.mdx b/docs/sdks/languages/rust.mdx new file mode 100644 index 000000000..8fa9b3b5b --- /dev/null +++ b/docs/sdks/languages/rust.mdx @@ -0,0 +1,8 @@ +--- +title: "Rust" +--- + +Coming soon. + +Follow this GitHub +[issue](https://github.com/Infisical/infisical/issues/437) to stay updated. diff --git a/docs/sdks/overview.mdx b/docs/sdks/overview.mdx new file mode 100644 index 000000000..fd27ad902 --- /dev/null +++ b/docs/sdks/overview.mdx @@ -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) diff --git a/docs/self-hosting/configuration/email.mdx b/docs/self-hosting/configuration/email.mdx index b35666776..a6b13fa0b 100644 --- a/docs/self-hosting/configuration/email.mdx +++ b/docs/self-hosting/configuration/email.mdx @@ -1,5 +1,5 @@ --- -title: "Email" +title: "Configure email service" description: "How to configure your email when self-hosting Infisical." --- diff --git a/docs/self-hosting/configuration/envars.mdx b/docs/self-hosting/configuration/envars.mdx index 342fa9e96..6786eef08 100644 --- a/docs/self-hosting/configuration/envars.mdx +++ b/docs/self-hosting/configuration/envars.mdx @@ -1,44 +1,151 @@ --- -title: "Environment Variables" -description: "How to configure your environment variables when self-hosting Infisical." +title: "All environment variables" +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 | -| ----------------------- | ----------------------------------------------------------------------------------------------------------- | ------------- | -| `ENCRYPTION_KEY` | ❗️ Strong hex encryption key | `None` | -| `JWT_SIGNUP_SECRET` | ❗️ JWT token secret | `None` | -| `JWT_REFRESH_SECRET` | ❗️ JWT token secret | `None` | -| `JWT_AUTH_SECRET` | ❗️ JWT token secret | `None` | -| `JWT_MFA_SECRET` | ❗️ JWT token secret | `None` | -| `JWT_SERVICE_SECRET` | ❗️ JWT token secret | `None` | -| `JWT_SIGNUP_LIFETIME` | JWT token lifetime expressed in seconds or a string describing a time span (e.g. 60, "2 days", "10h", "7d") | `15m` | -| `JWT_REFRESH_LIFETIME` | JWT token lifetime expressed in seconds or a string describing a time span (e.g. 60, "2 days", "10h", "7d") | `90d` | -| `JWT_AUTH_LIFETIME` | JWT token lifetime expressed in seconds or a string describing a time span (e.g. 60, "2 days", "10h", "7d") | `10d` | -| `JWT_MFA_LIFETIME` | JWT token lifetime expressed in seconds or a string describing a time span (e.g. 60, "2 days", "10h", "7d") | `5m` | -| `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` | -| `MONGO_USERNAME` | MongoDB username if using container | `None` | -| `MONGO_PASSWORD` | MongoDB password if using container | `None` | -| `SITE_URL` | ❗️ Site URL - should be an absolute URL including the protocol (e.g. `https://app.infisical.com`) | `None` | -| `SMTP_HOST` | ❗️ Hostname to connect to for establishing SMTP connections | `None` | -| `SMTP_USERNAME` | ❗️ Credential to connect to host (e.g. `team@infisical.com`) | `None` | -| `SMTP_PASSWORD` | ❗️ Credential to connect to host | `None` | -| `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` | -| `SMTP_FROM_ADDRESS` | ❗️ Email address to be used for sending emails (e.g. `team@infisical.com`) | `None` | -| `SMTP_FROM_NAME` | Name label to be used in From field (e.g. `Team`) | `Infisical` | -| `TELEMETRY_ENABLED` | `true` or `false`. [More](../overview). | `true` | -| `LICENSE_KEY` | License key if using Infisical Enterprise Edition | `true` | -| `CLIENT_ID_HEROKU` | OAuth2 client ID for Heroku integration | `None` | -| `CLIENT_ID_VERCEL` | OAuth2 client ID for Vercel integration | `None` | -| `CLIENT_ID_NETLIFY` | OAuth2 client ID for Netlify integration | `None` | -| `CLIENT_ID_GITHUB` | OAuth2 client ID for GitHub integration | `None` | -| `CLIENT_SECRET_HEROKU` | OAuth2 client secret for Heroku integration | `None` | -| `CLIENT_SECRET_VERCEL` | OAuth2 client secret for Vercel integration | `None` | -| `CLIENT_SECRET_NETLIFY` | OAuth2 client secret for Netlify integration | `None` | -| `CLIENT_SECRET_GITHUB` | OAuth2 client secret for GitHub integration | `None` | -| `CLIENT_SLUG_VERCEL` | OAuth2 slug for Netlify integration | `None` | -| `SENTRY_DSN` | DSN for error-monitoring with Sentry | `None` | -| `INVITE_ONLY_SIGNUP` | If true, users can only sign up if they are invited | `false` | +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. + + + + + Must be a random 32 character length hex string + + + + Must be a random 32 character length hex string + + + + Must be a random 32 character length hex string + + + + Must be a random 32 character length hex string + + + + Must be a random 32 character length hex string + + + + Must be a random 32 character length hex string + + + + *TLS based connection string is not yet supported + + + + When email service is not configured, Infisical will have limited functionality + + + Hostname to connect to for establishing SMTP connections + + + + Credential to connect to host (e.g. team@infisical.com) + + + + Credential to connect to host + + + + Port to connect to for establishing SMTP connections + + + + If true, use TLS when connecting to host. If false, TLS will be used if STARTTLS is supported + + + + Email address to be used for sending emails + + + + Name label to be used in From field (e.g. Team) + + + + To sync secret to third party services, provide value for the related services + + + OAuth2 client ID for Heroku integration + + + + OAuth2 client secret for Heroku integration + + + + OAuth2 client ID for Vercel integration + + + + OAuth2 client secret for Vercel integration + + + + OAuth2 client ID for Netlify integration + + + + OAuth2 client secret for Netlify integration + + + + OAuth2 client ID for GitHub integration + + + + OAuth2 client secret for GitHub integration + + + + OAuth2 slug for Netlify integration + + + + #### JWT + + JWT token lifetime expressed in seconds or a string describing a time span + + + + JWT token lifetime expressed in seconds or a string describing a time span + + + + JWT token lifetime expressed in seconds or a string describing a time span + + + + JWT token lifetime expressed in seconds or a string describing a time span + + + + + + + #### Error logging + Infisical uses Sentry to report error logs + + + #### Settings + + Only allow users who are invited to sign up + + + + Site URL - should be an absolute URL including the protocol (e.g. https://app.infisical.com) + + + + + + +## Frontend environment variables + diff --git a/docs/self-hosting/overview.mdx b/docs/self-hosting/overview.mdx index 7f3bc30bf..216b02d0e 100644 --- a/docs/self-hosting/overview.mdx +++ b/docs/self-hosting/overview.mdx @@ -1,36 +1,198 @@ --- -title: "Overview" -description: "Infisical is an open-source end-to-end encrypted secrets manager that developers can set up within 15 minutes." +title: "Deployment options" +description: "Explore deployment options for self hosting Infisical" --- - -Self-host vs. Infisical Cloud +To meet various compliance requirements, you may want to self-host Infisical instead of using [Infisical Cloud](https://app.infisical.com/). +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. + + + + 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. + + + - + + + This deployment option is highly available + Coming soon + + + This deployment option is highly available + + **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 - - - Deploy to any Linux with Docker - - - Deploy to your Kubernetes cluster - - + 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. + Refer to the available [environment variables](../../self-hosting/configuration/envars) to learn more -## Telemetry + + [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://:@:/" + 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 + ``` + + + 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 ` to your `helm install` command. + + ```bash + ## Installs to default namespace + helm install infisical-helm-charts/infisical --generate-name --values + ``` + + + If you have not filled out all of the required environment variables, you will see an error message prompting you to + do so. + + + #### 4. Your Infisical installation is complete and should be running on the host name you specified in Ingress in `values.yaml`. + + + + 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. + + + \ No newline at end of file diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 5c92b590b..ad099a70d 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -1,5 +1,5 @@ { - "name": "npm-proj-1677883018530-0.7603125731052582NtcmfK", + "name": "frontend", "lockfileVersion": 2, "requires": true, "packages": { @@ -46,6 +46,7 @@ "gray-matter": "^4.0.3", "http-proxy": "^1.18.1", "i18next": "^22.4.9", + "infisical-node": "^1.0.37", "jspdf": "^2.5.1", "jsrp": "^0.2.4", "markdown-it": "^13.0.1", @@ -13367,6 +13368,26 @@ "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": { "version": "1.0.6", "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", @@ -17363,8 +17384,7 @@ "node_modules/proxy-from-env": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", - "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==", - "dev": true + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==" }, "node_modules/pump": { "version": "3.0.0", @@ -21761,9 +21781,9 @@ "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==" }, "node_modules/webpack": { - "version": "5.75.0", - "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.75.0.tgz", - "integrity": "sha512-piaIaoVJlqMsPtX/+3KTTO6jfvrSYgauFVdt8cr9LTHKmcq/AMd4mhzsiP7ZF/PGRNPGA8336jldh9l2Kt2ogQ==", + "version": "5.76.1", + "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.76.1.tgz", + "integrity": "sha512-4+YIK4Abzv8172/SGqObnUjaIHjLEuUasz9EwQj/9xmPPkYJy2Mh03Q/lJfSD3YLzbxy5FeTq5Uw0323Oh6SJQ==", "dev": true, "dependencies": { "@types/eslint-scope": "^3.7.3", @@ -32078,6 +32098,28 @@ "integrity": "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==", "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": { "version": "1.0.6", "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", @@ -34869,8 +34911,7 @@ "proxy-from-env": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", - "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==", - "dev": true + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==" }, "pump": { "version": "3.0.0", @@ -38113,9 +38154,9 @@ "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==" }, "webpack": { - "version": "5.75.0", - "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.75.0.tgz", - "integrity": "sha512-piaIaoVJlqMsPtX/+3KTTO6jfvrSYgauFVdt8cr9LTHKmcq/AMd4mhzsiP7ZF/PGRNPGA8336jldh9l2Kt2ogQ==", + "version": "5.76.1", + "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.76.1.tgz", + "integrity": "sha512-4+YIK4Abzv8172/SGqObnUjaIHjLEuUasz9EwQj/9xmPPkYJy2Mh03Q/lJfSD3YLzbxy5FeTq5Uw0323Oh6SJQ==", "dev": true, "requires": { "@types/eslint-scope": "^3.7.3", diff --git a/frontend/package.json b/frontend/package.json index a1989701a..d53e40ca7 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -39,8 +39,8 @@ "@reduxjs/toolkit": "^1.8.3", "@stripe/react-stripe-js": "^1.16.3", "@stripe/stripe-js": "^1.46.0", - "@types/argon2-browser": "^1.18.1", "@tanstack/react-query": "^4.23.0", + "@types/argon2-browser": "^1.18.1", "add": "^2.0.6", "argon2-browser": "^1.18.0", "axios": "^0.27.2", @@ -53,6 +53,7 @@ "gray-matter": "^4.0.3", "http-proxy": "^1.18.1", "i18next": "^22.4.9", + "infisical-node": "^1.0.37", "jspdf": "^2.5.1", "jsrp": "^0.2.4", "markdown-it": "^13.0.1", diff --git a/frontend/src/components/signup/TeamInviteStep.tsx b/frontend/src/components/signup/TeamInviteStep.tsx index 29cb95076..12d558606 100644 --- a/frontend/src/components/signup/TeamInviteStep.tsx +++ b/frontend/src/components/signup/TeamInviteStep.tsx @@ -2,10 +2,13 @@ import React, { useState } from 'react'; import { useRouter } from 'next/router'; 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 getWorkspaces from '@app/pages/api/workspace/getWorkspaces'; 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. @@ -14,6 +17,10 @@ export default function TeamInviteStep(): JSX.Element { const [emails, setEmails] = useState(''); const { t } = useTranslation(); const router = useRouter(); + const {data: serverDetails } = useFetchServerStatus() + const { handlePopUpToggle, popUp, handlePopUpOpen } = usePopUp([ + 'setUpEmail' + ] as const); // Redirect user to the getting started page const redirectToHome = async () => { @@ -62,10 +69,20 @@ export default function TeamInviteStep(): JSX.Element { + + + +); diff --git a/frontend/src/components/v2/EmailServiceSetupModal/index.tsx b/frontend/src/components/v2/EmailServiceSetupModal/index.tsx new file mode 100644 index 000000000..33a7d1854 --- /dev/null +++ b/frontend/src/components/v2/EmailServiceSetupModal/index.tsx @@ -0,0 +1 @@ +export { EmailServiceSetupModal } from './EmailServiceSetupModal'; diff --git a/frontend/src/components/v2/index.tsx b/frontend/src/components/v2/index.tsx index 52c8fc4f4..130b797ae 100644 --- a/frontend/src/components/v2/index.tsx +++ b/frontend/src/components/v2/index.tsx @@ -3,6 +3,7 @@ export * from './Card'; export * from './Checkbox'; export * from './DeleteActionModal'; export * from './Dropdown'; +export * from './EmailServiceSetupModal' export * from './EmptyState'; export * from './FormControl'; export * from './IconButton'; diff --git a/frontend/src/hooks/api/serverDetails/index.ts b/frontend/src/hooks/api/serverDetails/index.ts new file mode 100644 index 000000000..812293a1d --- /dev/null +++ b/frontend/src/hooks/api/serverDetails/index.ts @@ -0,0 +1 @@ +export { useFetchServerStatus } from './queries' \ No newline at end of file diff --git a/frontend/src/hooks/api/serverDetails/queries.tsx b/frontend/src/hooks/api/serverDetails/queries.tsx new file mode 100644 index 000000000..29abea127 --- /dev/null +++ b/frontend/src/hooks/api/serverDetails/queries.tsx @@ -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('/api/status'); + return data; +}; + +export const useFetchServerStatus= () => { + return useQuery({ queryKey: serverStatusKeys.serverStatus, queryFn: fetchServerStatus }); +} \ No newline at end of file diff --git a/frontend/src/hooks/api/serverDetails/types.ts b/frontend/src/hooks/api/serverDetails/types.ts new file mode 100644 index 000000000..0deb90a38 --- /dev/null +++ b/frontend/src/hooks/api/serverDetails/types.ts @@ -0,0 +1,5 @@ +export type ServerStatus = { + date: string; + message: string; + emailConfigured: boolean; +}; \ No newline at end of file diff --git a/frontend/src/hooks/api/users/queries.tsx b/frontend/src/hooks/api/users/queries.tsx index c6b35960d..5d0759755 100644 --- a/frontend/src/hooks/api/users/queries.tsx +++ b/frontend/src/hooks/api/users/queries.tsx @@ -83,8 +83,14 @@ export const useAddUserToWs = () => { export const useAddUserToOrg = () => { const queryClient = useQueryClient(); + type Response = { + data: { + message: string, + completeInviteLink: string | undefined + } + } - return useMutation<{}, {}, AddUserToOrgDTO>({ + return useMutation({ mutationFn: (dto) => apiRequest.post(`/api/v1/invite-org/signup`, dto), onSuccess: (_, { organizationId }) => { queryClient.invalidateQueries(userKeys.getOrgUsers(organizationId)); diff --git a/frontend/src/pages/dashboard/[id].tsx b/frontend/src/pages/dashboard/[id].tsx index 6c4483d49..799c05c39 100644 --- a/frontend/src/pages/dashboard/[id].tsx +++ b/frontend/src/pages/dashboard/[id].tsx @@ -419,9 +419,20 @@ export default function Dashboard() { const nameErrors = !newData! .map((secret) => !Number.isNaN(Number(secret.key.charAt(0)))) .every((v) => v === false); + const emptyNameError = !newData! + .map((secret) => secret.key.length === 0) + .every((v) => v === false); const duplicatesExist = 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) { setSaveLoading(false); return createNotification({ diff --git a/frontend/src/pages/signup.tsx b/frontend/src/pages/signup.tsx index 62d0f4a47..1f1099c7b 100644 --- a/frontend/src/pages/signup.tsx +++ b/frontend/src/pages/signup.tsx @@ -13,6 +13,7 @@ import TeamInviteStep from '@app/components/signup/TeamInviteStep'; import UserInfoStep from '@app/components/signup/UserInfoStep'; import SecurityClient from '@app/components/utilities/SecurityClient'; import { getTranslatedStaticProps } from '@app/components/utilities/withTranslateProps'; +import { useFetchServerStatus } from '@app/hooks/api/serverDetails'; import checkEmailVerificationCode from './api/auth/CheckEmailVerificationCode'; import getWorkspaces from './api/workspace/getWorkspaces'; @@ -25,10 +26,12 @@ export default function SignUp() { const [password, setPassword] = useState(''); const [firstName, setFirstName] = useState(''); const [lastName, setLastName] = useState(''); - const [code, setCode] = useState(''); + const [code, setCode] = useState('123456'); const [codeError, setCodeError] = useState(false); const [step, setStep] = useState(1); const router = useRouter(); + const {data: serverDetails } = useFetchServerStatus() + 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 (
@@ -111,9 +127,7 @@ export default function SignUp() { password={password} name={`${firstName} ${lastName}`} /> - ) : ( - - )} + ) : (serverDetails?.emailConfigured ? : "")}
diff --git a/frontend/src/pages/verify-email.tsx b/frontend/src/pages/verify-email.tsx index 3b7e1c8d5..203b040bf 100644 --- a/frontend/src/pages/verify-email.tsx +++ b/frontend/src/pages/verify-email.tsx @@ -6,12 +6,19 @@ import Link from 'next/link'; import Button from '@app/components/basic/buttons/Button'; import InputField from '@app/components/basic/InputField'; 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'; export default function VerifyEmail() { const [email, setEmail] = useState(''); 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. @@ -63,7 +70,13 @@ export default function VerifyEmail() {
-
@@ -80,6 +93,11 @@ export default function VerifyEmail() { )} + + handlePopUpToggle('setUpEmail', isOpen)} + /> ); } diff --git a/frontend/src/views/Settings/OrgSettingsPage/OrgSettingsPage.tsx b/frontend/src/views/Settings/OrgSettingsPage/OrgSettingsPage.tsx index 473b6f8c3..f73249196 100644 --- a/frontend/src/views/Settings/OrgSettingsPage/OrgSettingsPage.tsx +++ b/frontend/src/views/Settings/OrgSettingsPage/OrgSettingsPage.tsx @@ -1,4 +1,5 @@ /* eslint-disable @typescript-eslint/no-unused-vars */ +import { useState } from 'react'; import { useTranslation } from 'next-i18next'; import { plans } from 'public/data/frequentConstants'; @@ -51,6 +52,8 @@ export const OrgSettingsPage = () => { const addIncidentContact = useAddIncidentContact(); const removeIncidentContact = useDeleteIncidentContact(); + const [completeInviteLink, setcompleteInviteLink] = useState("") + const isMoreUsersNotAllowed = (orgUsers || []).length >= 5 && subscriptionPlan === plans.starter && @@ -96,11 +99,16 @@ export const OrgSettingsPage = () => { if (!currentOrg?._id) return; try { - await addUserToOrg.mutateAsync({ organizationId: currentOrg?._id, inviteeEmail: email }); - createNotification({ - text: 'Successfully invited user to the organization.', - type: 'success' - }); + const {data} = await addUserToOrg.mutateAsync({ organizationId: currentOrg?._id, inviteeEmail: email }); + setcompleteInviteLink(data?.completeInviteLink) + + // 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) { console.error(error); createNotification({ @@ -247,6 +255,8 @@ export const OrgSettingsPage = () => { onRemoveMember={onRemoveUserOrgMembership} onRoleChange={onUpdateOrgUserRole} onGrantAccess={onGrantUserAccess} + completeInviteLink={completeInviteLink} + setCompleteInviteLink={setcompleteInviteLink} />
diff --git a/frontend/src/views/Settings/OrgSettingsPage/components/OrgIncidentContactsTable/OrgIncidentContactsTable.tsx b/frontend/src/views/Settings/OrgSettingsPage/components/OrgIncidentContactsTable/OrgIncidentContactsTable.tsx index c3bd55f51..e118e455b 100644 --- a/frontend/src/views/Settings/OrgSettingsPage/components/OrgIncidentContactsTable/OrgIncidentContactsTable.tsx +++ b/frontend/src/views/Settings/OrgSettingsPage/components/OrgIncidentContactsTable/OrgIncidentContactsTable.tsx @@ -13,6 +13,7 @@ import * as yup from 'yup'; import { Button, DeleteActionModal, + EmailServiceSetupModal, EmptyState, FormControl, IconButton, @@ -28,6 +29,7 @@ import { THead, Tr} from '@app/components/v2'; import { usePopUp } from '@app/hooks'; +import { useFetchServerStatus } from '@app/hooks/api/serverDetails'; import { IncidentContact } from '@app/hooks/api/types'; type Props = { @@ -50,9 +52,11 @@ export const OrgIncidentContactsTable = ({ isLoading }: Props) => { const [searchContact, setSearchContact] = useState(''); + const {data: serverDetails } = useFetchServerStatus() const { handlePopUpToggle, popUp, handlePopUpOpen, handlePopUpClose } = usePopUp([ 'addContact', - 'removeContact' + 'removeContact', + 'setUpEmail' ] as const); const { @@ -92,7 +96,13 @@ export const OrgIncidentContactsTable = ({
@@ -180,6 +190,10 @@ export const OrgIncidentContactsTable = ({ onChange={(isOpen) => handlePopUpToggle('removeContact', isOpen)} onDeleteApproved={onRemoveIncidentContact} /> + handlePopUpToggle('setUpEmail', isOpen)} + />
); }; diff --git a/frontend/src/views/Settings/OrgSettingsPage/components/OrgMembersTable/OrgMembersTable.tsx b/frontend/src/views/Settings/OrgSettingsPage/components/OrgMembersTable/OrgMembersTable.tsx index 9bed5954b..04ee9da3d 100644 --- a/frontend/src/views/Settings/OrgSettingsPage/components/OrgMembersTable/OrgMembersTable.tsx +++ b/frontend/src/views/Settings/OrgSettingsPage/components/OrgMembersTable/OrgMembersTable.tsx @@ -1,7 +1,7 @@ -import { useMemo, useState } from 'react'; +import { Dispatch, SetStateAction, useMemo, useState } from 'react'; import { Controller, useForm } from 'react-hook-form'; 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 { yupResolver } from '@hookform/resolvers/yup'; import * as yup from 'yup'; @@ -9,7 +9,7 @@ import * as yup from 'yup'; import { Button, DeleteActionModal, - EmptyState, + EmailServiceSetupModal, EmptyState, FormControl, IconButton, Input, @@ -28,6 +28,7 @@ import { Tr, UpgradePlanModal} from '@app/components/v2'; import { usePopUp } from '@app/hooks'; +import { useFetchServerStatus } from '@app/hooks/api/serverDetails'; import { OrgUser, Workspace } from '@app/hooks/api/types'; type Props = { @@ -42,6 +43,8 @@ type Props = { onGrantAccess: (userId: string, publicKey: string) => Promise; // the current user id to block remove org button userId: string; + completeInviteLink: string | undefined, + setCompleteInviteLink: Dispatch> }; const addMemberFormSchema = yup.object({ @@ -60,14 +63,18 @@ export const OrgMembersTable = ({ onGrantAccess, onRoleChange, userId, - isLoading + isLoading, + completeInviteLink, + setCompleteInviteLink }: Props) => { const router = useRouter(); const [searchMemberFilter, setSearchMemberFilter] = useState(''); + const {data: serverDetails } = useFetchServerStatus() const { handlePopUpToggle, popUp, handlePopUpOpen, handlePopUpClose } = usePopUp([ 'addMember', 'removeMember', - 'upgradePlan' + 'upgradePlan', + 'setUpEmail' ] as const); const { @@ -79,8 +86,11 @@ export const OrgMembersTable = ({ const onAddMember = async ({ email }: TAddMemberForm) => { await onInviteMember(email); - handlePopUpClose('addMember'); - reset(); + if (serverDetails?.emailConfigured){ + handlePopUpClose('addMember'); + } + + reset(); }; const onRemoveOrgMemberApproved = async () => { @@ -106,6 +116,11 @@ export const OrgMembersTable = ({ [members, searchMemberFilter] ); + const copyTokenToClipboard = () => { + navigator.clipboard.writeText(completeInviteLink as string); + // setIsTokenCopied.on(); + }; + return (
@@ -121,11 +136,16 @@ export const OrgMembersTable = ({ @@ -234,20 +254,23 @@ export const OrgMembersTable = ({ isOpen={popUp?.addMember?.isOpen} onOpenChange={(isOpen) => { handlePopUpToggle('addMember', isOpen); - reset(); + setCompleteInviteLink(undefined) }} > - 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. - +
+ {!completeInviteLink &&
+ 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. +
} + {completeInviteLink && "This Infisical instance does not have a email provider setup. Please share this invite link with the invitee manually"} +
} > -
+ {!completeInviteLink &&
- + } + { + completeInviteLink && +
+

{completeInviteLink}

+ + + click to copy + +
+ } handlePopUpToggle('upgradePlan', isOpen)} text="You can add custom environments if you switch to Infisical's Team plan." /> + handlePopUpToggle('setUpEmail', isOpen)} + />
); }; diff --git a/frontend/src/views/Settings/PersonalSettingsPage/SecuritySection/SecuritySection.tsx b/frontend/src/views/Settings/PersonalSettingsPage/SecuritySection/SecuritySection.tsx index 0d7902c60..0c64c41e7 100644 --- a/frontend/src/views/Settings/PersonalSettingsPage/SecuritySection/SecuritySection.tsx +++ b/frontend/src/views/Settings/PersonalSettingsPage/SecuritySection/SecuritySection.tsx @@ -1,7 +1,9 @@ import { useEffect, useState } from 'react'; 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 { User } from '../../../../hooks/api/types'; @@ -11,6 +13,11 @@ export const SecuritySection = () => { const [isMfaEnabled, setIsMfaEnabled] = useState(false); const { data: user } = useGetUser(); const { createNotification } = useNotificationContext(); + const { handlePopUpToggle, popUp, handlePopUpOpen } = usePopUp([ + 'setUpEmail' + ] as const); + + const {data: serverDetails } = useFetchServerStatus() useEffect(() => { if (user && typeof user.isMfaEnabled !== 'undefined') { @@ -42,6 +49,7 @@ export const SecuritySection = () => { } return ( + <>

@@ -52,12 +60,21 @@ export const SecuritySection = () => { id="isTwoFAEnabled" isChecked={isMfaEnabled} onCheckedChange={(state) => { - toggleMfa(state as boolean); + if (serverDetails?.emailConfigured){ + toggleMfa(state as boolean); + } else { + handlePopUpOpen('setUpEmail'); + } }} > Enable 2-factor authentication via your personal email.

+ handlePopUpToggle('setUpEmail', isOpen)} + /> + ); };