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/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 1eb1dbc66..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", @@ -40,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", @@ -2800,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", @@ -2816,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", @@ -10500,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" }, @@ -11483,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" }, @@ -14361,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", @@ -20049,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" }, @@ -20779,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 cd6cd214a..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", @@ -31,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", @@ -101,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/config/index.ts b/backend/src/config/index.ts index 3fe935097..33066f613 100644 --- a/backend/src/config/index.ts +++ b/backend/src/config/index.ts @@ -5,12 +5,12 @@ 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')!; +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')!; +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')!; @@ -34,12 +34,12 @@ export const getPostHogProjectApiKey = () => infisical.get('POSTHOG_PROJECT_API_ 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 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 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')!; @@ -47,4 +47,5 @@ export const getStripePublishableKey = () => infisical.get('STRIPE_PUBLISHABLE_K 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')!; \ No newline at end of file +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/membershipOrgController.ts b/backend/src/controllers/v1/membershipOrgController.ts index 105be0503..5a2b41b2b 100644 --- a/backend/src/controllers/v1/membershipOrgController.ts +++ b/backend/src/controllers/v1/membershipOrgController.ts @@ -7,7 +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 } from '../../config'; +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({ @@ -181,6 +183,10 @@ export const inviteUserToOrganization = async (req: Request, res: Response) => { 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, diff --git a/backend/src/controllers/v1/signupController.ts b/backend/src/controllers/v1/signupController.ts index cb411a29e..6adb0e7a8 100644 --- a/backend/src/controllers/v1/signupController.ts +++ b/backend/src/controllers/v1/signupController.ts @@ -7,7 +7,7 @@ import { } from '../../helpers/signup'; import { createToken } from '../../helpers/auth'; import { BadRequestError } from '../../utils/errors'; -import { getInviteOnlySignup, getJwtSignupLifetime, getJwtSignupSecret } from '../../config'; +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 (getInviteOnlySignup() || false) { + 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({ diff --git a/backend/src/controllers/v2/authController.ts b/backend/src/controllers/v2/authController.ts index b0204e1b9..3a36d559c 100644 --- a/backend/src/controllers/v2/authController.ts +++ b/backend/src/controllers/v2/authController.ts @@ -87,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; @@ -127,12 +127,12 @@ export const login2 = async (req: Request, res: Response) => { 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', @@ -142,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, @@ -181,7 +181,7 @@ export const login2 = async (req: Request, res: Response) => { iv?: string; tag?: string; } - + const response: ResponseData = { mfaEnabled: false, encryptionVersion: user.encryptionVersion, @@ -191,7 +191,7 @@ export const login2 = async (req: Request, res: Response) => { iv: user.iv, tag: user.tag } - + if ( user?.protectedKey && user?.protectedKeyIV && @@ -206,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); } @@ -244,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', @@ -259,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' }); @@ -274,75 +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: getNodeEnv() === '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); -} \ No newline at end of file + 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/helpers/nodemailer.ts b/backend/src/helpers/nodemailer.ts index d765f3200..fe7c05044 100644 --- a/backend/src/helpers/nodemailer.ts +++ b/backend/src/helpers/nodemailer.ts @@ -3,7 +3,7 @@ import fs from 'fs'; import path from 'path'; import handlebars from 'handlebars'; import nodemailer from 'nodemailer'; -import { getSmtpFromName, getSmtpFromAddress } from '../config'; +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: `"${getSmtpFromName()}" <${getSmtpFromAddress()}>`, - 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/index.ts b/backend/src/index.ts index 2c3060242..955fc96b1 100644 --- a/backend/src/index.ts +++ b/backend/src/index.ts @@ -23,29 +23,29 @@ const swaggerFile = require('../spec.json'); const requestIp = require('request-ip'); import { apiLimiter } from './helpers/rateLimiter'; import { - workspace as eeWorkspaceRouter, - secret as eeSecretRouter, - secretSnapshot as eeSecretSnapshotRouter, - action as eeActionRouter + 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 + 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, @@ -66,7 +66,7 @@ import { getLogger } from './utils/logger'; import { RouteNotFoundError } from './utils/errors'; import { requestErrorHandler } from './middleware/requestErrorHandler'; import { - getMongoURL, + getMongoURL, getNodeEnv, getPort, getSentryDSN, @@ -74,10 +74,12 @@ import { } from './config'; const main = async () => { - await infisical.connect({ - token: process.env.INFISICAL_TOKEN! - }); - + if (process.env.INFISICAL_TOKEN != "" || process.env.INFISICAL_TOKEN != undefined) { + await infisical.connect({ + token: process.env.INFISICAL_TOKEN! + }); + } + logTelemetryMessage(); setTransporter(initSmtp()); @@ -160,7 +162,7 @@ const main = async () => { //* Handle unrouted requests and respond with proper error message as well as status code app.use((req, res, next) => { - if (res.headersSent) return next(); + if (res.headersSent) return next(); next(RouteNotFoundError({ message: `The requested source '(${req.method})${req.url}' was not found` })) }) @@ -172,7 +174,7 @@ const main = async () => { createTestUserForDevelopment(); setUpHealthEndpoint(server); - + server.on('close', async () => { await DatabaseService.closeDatabase(); }) 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/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__/healthcheck.test.ts b/backend/tests/setupTests.ts similarity index 99% rename from backend/__tests__/healthcheck.test.ts rename to backend/tests/setupTests.ts index e054bd180..903347a2a 100644 --- a/backend/__tests__/healthcheck.test.ts +++ b/backend/tests/setupTests.ts @@ -18,4 +18,4 @@ describe('Healthcheck endpoint', () => { const res = await request(server).get('/healthcheck'); expect(res.status).toEqual(200); }); -}); \ No newline at end of file +}); diff --git a/cli/go.mod b/cli/go.mod index cfa1ede11..d1aadd208 100644 --- a/cli/go.mod +++ b/cli/go.mod @@ -4,7 +4,10 @@ go 1.19 require ( github.com/99designs/keyring v1.2.2 + github.com/mattn/go-isatty v0.0.14 + github.com/muesli/ansi v0.0.0-20221106050444-61f0cd9a192a github.com/muesli/mango-cobra v1.2.0 + github.com/muesli/reflow v0.3.0 github.com/muesli/roff v0.1.0 github.com/spf13/cobra v1.6.1 golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d @@ -22,7 +25,6 @@ require ( github.com/godbus/dbus v0.0.0-20190726142602-4481cbc300e2 // indirect github.com/gsterjov/go-libsecret v0.0.0-20161001094733-a6f4afe4910c // indirect github.com/mattn/go-colorable v0.1.9 // indirect - github.com/mattn/go-isatty v0.0.14 // indirect github.com/mattn/go-runewidth v0.0.14 // indirect github.com/mitchellh/mapstructure v1.3.3 // indirect github.com/mtibben/percent v0.2.1 // indirect diff --git a/cli/go.sum b/cli/go.sum index dc97287db..2b514ea6e 100644 --- a/cli/go.sum +++ b/cli/go.sum @@ -56,6 +56,7 @@ github.com/mattn/go-colorable v0.1.9/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= github.com/mattn/go-isatty v0.0.14 h1:yVuAays6BHfxijgZPzw+3Zlu5yQgKGP2/hcQbHb7S9Y= github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94= +github.com/mattn/go-runewidth v0.0.12/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk= github.com/mattn/go-runewidth v0.0.14 h1:+xnbZSEeDbOIg5/mE6JF0w6n9duR1l3/WmbinWVwUuU= github.com/mattn/go-runewidth v0.0.14/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= github.com/mitchellh/mapstructure v1.3.3 h1:SzB1nHZ2Xi+17FP0zVQBHIZqvwRN9408fJO8h+eeNA8= @@ -63,12 +64,16 @@ github.com/mitchellh/mapstructure v1.3.3/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RR github.com/montanaflynn/stats v0.0.0-20171201202039-1bf9dbcd8cbe/go.mod h1:wL8QJuTMNUDYhXwkmfOly8iTdp5TEcJFWZD2D7SIkUc= github.com/mtibben/percent v0.2.1 h1:5gssi8Nqo8QU/r2pynCm+hBQHpkB/uNK7BJCFogWdzs= github.com/mtibben/percent v0.2.1/go.mod h1:KG9uO+SZkUp+VkRHsCdYQV3XSZrrSpR3O9ibNBTZrns= +github.com/muesli/ansi v0.0.0-20221106050444-61f0cd9a192a h1:jlDOeO5TU0pYlbc/y6PFguab5IjANI0Knrpg3u/ton4= +github.com/muesli/ansi v0.0.0-20221106050444-61f0cd9a192a/go.mod h1:CJlz5H+gyd6CUWT45Oy4q24RdLyn7Md9Vj2/ldJBSIo= github.com/muesli/mango v0.1.0 h1:DZQK45d2gGbql1arsYA4vfg4d7I9Hfx5rX/GCmzsAvI= github.com/muesli/mango v0.1.0/go.mod h1:5XFpbC8jY5UUv89YQciiXNlbi+iJgt29VDC5xbzrLL4= github.com/muesli/mango-cobra v1.2.0 h1:DQvjzAM0PMZr85Iv9LIMaYISpTOliMEg+uMFtNbYvWg= github.com/muesli/mango-cobra v1.2.0/go.mod h1:vMJL54QytZAJhCT13LPVDfkvCUJ5/4jNUKF/8NC2UjA= github.com/muesli/mango-pflag v0.1.0 h1:UADqbYgpUyRoBja3g6LUL+3LErjpsOwaC9ywvBWe7Sg= github.com/muesli/mango-pflag v0.1.0/go.mod h1:YEQomTxaCUp8PrbhFh10UfbhbQrM/xJ4i2PB8VTLLW0= +github.com/muesli/reflow v0.3.0 h1:IFsN6K9NfGtjeggFP+68I4chLZV2yIKsXJFNZ+eWh6s= +github.com/muesli/reflow v0.3.0/go.mod h1:pbwTDkVPibjO2kyvBQRBxTWEEGDGq0FlB1BIKtnHY/8= github.com/muesli/roff v0.1.0 h1:YD0lalCotmYuF5HhZliKWlIx7IEhiXeSfq7hNjFqGF8= github.com/muesli/roff v0.1.0/go.mod h1:pjAHQM9hdUUwm/krAfrLGgJkXJ+YuhtsfZ42kieB2Ig= github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e h1:fD57ERR4JtEqsWbfPhv4DMiApHyliiK5xCTNVSPiaAs= @@ -78,6 +83,7 @@ github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/rivo/uniseg v0.1.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY= github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= @@ -106,7 +112,6 @@ go.mongodb.org/mongo-driver v1.10.0/go.mod h1:wsihk0Kdgv8Kqu1Anit4sfK+22vSFbUrAV golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d h1:sK3txAijHtOK88l68nt020reeT1ZdKLIYetKl95FzVY= golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= golang.org/x/net v0.0.0-20211029224645-99673261e6eb/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= -golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2 h1:CIJ76btIcR3eFI5EgSo6k1qKw9KJexJuRLI9G7Hp5wE= golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.7.0 h1:rJrUqqhjsgNp7KqAIc25s9pZnjU7TUcSY7HcVZjdn1g= golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= diff --git a/cli/packages/cmd/secrets.go b/cli/packages/cmd/secrets.go index 95224b24e..69a4539a6 100644 --- a/cli/packages/cmd/secrets.go +++ b/cli/packages/cmd/secrets.go @@ -263,10 +263,10 @@ var secretsSetCmd = &cobra.Command{ } // Print secret operations - headers := []string{"SECRET NAME", "SECRET VALUE", "STATUS"} - rows := [][]string{} + headers := [...]string{"SECRET NAME", "SECRET VALUE", "STATUS"} + rows := [][3]string{} for _, secretOperation := range secretOperations { - rows = append(rows, []string{secretOperation.SecretKey, secretOperation.SecretValue, secretOperation.SecretOperation}) + rows = append(rows, [...]string{secretOperation.SecretKey, secretOperation.SecretValue, secretOperation.SecretOperation}) } visualize.Table(headers, rows) diff --git a/cli/packages/visualize/secrets.go b/cli/packages/visualize/secrets.go index e9ac5d297..7be41020d 100644 --- a/cli/packages/visualize/secrets.go +++ b/cli/packages/visualize/secrets.go @@ -3,12 +3,12 @@ package visualize import "github.com/Infisical/infisical-merge/packages/models" func PrintAllSecretDetails(secrets []models.SingleEnvironmentVariable) { - rows := [][]string{} + rows := [][3]string{} for _, secret := range secrets { - rows = append(rows, []string{secret.Key, secret.Value, secret.Type}) + rows = append(rows, [...]string{secret.Key, secret.Value, secret.Type}) } - headers := []string{"SECRET NAME", "SECRET VALUE", "SECRET TYPE"} + headers := [...]string{"SECRET NAME", "SECRET VALUE", "SECRET TYPE"} Table(headers, rows) } diff --git a/cli/packages/visualize/visualize.go b/cli/packages/visualize/visualize.go index 5658197ee..8599f2405 100644 --- a/cli/packages/visualize/visualize.go +++ b/cli/packages/visualize/visualize.go @@ -2,8 +2,14 @@ package visualize import ( "os" + "strings" "github.com/jedib0t/go-pretty/table" + "github.com/mattn/go-isatty" + "github.com/muesli/ansi" + "github.com/muesli/reflow/truncate" + log "github.com/sirupsen/logrus" + "golang.org/x/term" ) type TableOptions struct { @@ -16,8 +22,35 @@ type TableOptions struct { // } // } +const ( + // combined width of the table borders and padding + borderWidths = 10 + // char to indicate that a string has been truncated + ellipsis = "…" +) + // Given headers and rows, this function will print out a table -func Table(headers []string, rows [][]string) { +func Table(headers [3]string, rows [][3]string) { + // if we're not in a terminal or cygwin terminal, don't truncate the secret value + shouldTruncate := isatty.IsTerminal(os.Stdout.Fd()) + + // This will return an error if we're not in a terminal or + // if the terminal is a cygwin terminal like Git Bash. + width, _, err := term.GetSize(int(os.Stdout.Fd())) + if err != nil { + if shouldTruncate { + log.Errorf("error getting terminal size: %s", err) + } else { + log.Debug(err) + } + } + + longestSecretName, longestSecretType := getLongestValues(append(rows, headers)) + availableWidth := width - longestSecretName - longestSecretType - borderWidths + if availableWidth < 0 { + availableWidth = 0 + } + t := table.NewWriter() t.SetOutputMirror(os.Stdout) t.SetStyle(table.StyleLight) @@ -35,7 +68,11 @@ func Table(headers []string, rows [][]string) { t.AppendHeader(tableHeaders) for _, row := range rows { tableRow := table.Row{} - for _, val := range row { + for i, val := range row { + // only truncate the first column (secret value) + if i == 1 && stringWidth(val) > availableWidth && shouldTruncate { + val = truncate.StringWithTail(val, uint(availableWidth), ellipsis) + } tableRow = append(tableRow, val) } t.AppendRow(tableRow) @@ -43,3 +80,28 @@ func Table(headers []string, rows [][]string) { t.Render() } + +// getLongestValues returns the length of the longest secret name and type from all rows (including the header). +func getLongestValues(rows [][3]string) (longestSecretName, longestSecretType int) { + for _, row := range rows { + if len(row[0]) > longestSecretName { + longestSecretName = stringWidth(row[0]) + } + if len(row[2]) > longestSecretType { + longestSecretType = stringWidth(row[2]) + } + } + return +} + +// stringWidth returns the width of a string. +// ANSI escape sequences are ignored and double-width characters are handled correctly. +func stringWidth(str string) (width int) { + for _, l := range strings.Split(str, "\n") { + w := ansi.PrintableRuneWidth(l) + if w > width { + width = w + } + } + return width +} 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/docs/cli/faq.mdx b/docs/cli/faq.mdx index daeeb6458..80229de01 100644 --- a/docs/cli/faq.mdx +++ b/docs/cli/faq.mdx @@ -4,7 +4,7 @@ description: "Frequently Asked Questions about Infisical" --- Frequently asked questions about the CLI can be found on this page. -If you can't find the answer you're looking for, please create an issue on our GitHub repository or join our Slack channel for additional support. +If you can't find the answer you are looking for, please create an issue on our GitHub repository or join our Slack channel for additional support. By default, the CLI will choose the most suitable store available on your system. @@ -22,4 +22,4 @@ Yes. If you have previously retrieved secrets for a specific project and environ Yes. This is simply a configuration file and contains no sensitive data. - \ No newline at end of file + diff --git a/docs/getting-started/introduction.mdx b/docs/getting-started/introduction.mdx index 38dcffe3a..cd5022083 100644 --- a/docs/getting-started/introduction.mdx +++ b/docs/getting-started/introduction.mdx @@ -30,6 +30,8 @@ Start syncing environment variables with [Infisical Cloud](https://app.infisical > 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/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/mint.json b/docs/mint.json index e257ea0c5..48dc1805c 100644 --- a/docs/mint.json +++ b/docs/mint.json @@ -44,11 +44,6 @@ "icon": "shield-halved", "url": "security" }, - { - "name": "Self-hosting", - "icon": "server", - "url": "self-hosting" - }, { "name": "SDKs", "icon": "puzzle-piece", @@ -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": [ @@ -184,13 +181,6 @@ "self-hosting/deployments/kubernetes" ] }, - { - "group": "Configuration", - "pages": [ - "self-hosting/configuration/envars", - "self-hosting/configuration/email" - ] - }, { "group": "Overview", "pages": [ 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 f674c1bcd..6786eef08 100644 --- a/docs/self-hosting/configuration/envars.mdx +++ b/docs/self-hosting/configuration/envars.mdx @@ -1,43 +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` | -| `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..f149f9663 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. + + + 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/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 e74e1d5d4..93ffccd27 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'; @@ -55,6 +56,8 @@ export const OrgSettingsPage = () => { const addIncidentContact = useAddIncidentContact(); const removeIncidentContact = useDeleteIncidentContact(); + const [completeInviteLink, setcompleteInviteLink] = useState("") + const isMoreUsersNotAllowed = (orgUsers || []).length >= 5 && subscriptionPlan === plans.starter && @@ -100,11 +103,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({ @@ -249,6 +257,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 51829e1df..58de0e288 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, @@ -29,6 +30,7 @@ import { 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 = { @@ -51,9 +53,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 { @@ -93,7 +97,13 @@ export const OrgIncidentContactsTable = ({
@@ -181,6 +191,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 2fcba82b4..5c2a1fc44 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 (
@@ -171,7 +186,7 @@ export const OrgMembersTable = ({ member )} - {(status === 'invited' || status === 'verified') && ( + {((status === 'invited' || status === 'verified') && serverDetails?.emailConfigured) && ( @@ -232,20 +247,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)} + /> + ); };