Resolve merge conflicts

This commit is contained in:
Tuan Dang
2023-03-28 14:43:56 +07:00
43 changed files with 1088 additions and 325 deletions

View File

@ -25,7 +25,7 @@
<img src="https://img.shields.io/github/commit-activity/m/infisical/infisical" alt="git commit activity" />
</a>
<a href="https://cloudsmith.io/~infisical/repos/">
<img src="https://img.shields.io/badge/Downloads-45.7k-orange" alt="Cloudsmith downloads" />
<img src="https://img.shields.io/badge/Downloads-55.7k-orange" alt="Cloudsmith downloads" />
</a>
<a href="https://join.slack.com/t/infisical-users/shared_invite/zt-1kdbk07ro-RtoyEt_9E~fyzGo_xQYP6g">
<img src="https://img.shields.io/badge/chat-on%20Slack-blueviolet" alt="Slack community channel" />

9
backend/jest.config.ts Normal file
View File

@ -0,0 +1,9 @@
export default {
preset: 'ts-jest',
testEnvironment: 'node',
collectCoverageFrom: ['src/*.{js,ts}', '!**/node_modules/**'],
modulePaths: ['<rootDir>/src'],
testMatch: ['<rootDir>/tests/**/*.test.ts'],
setupFiles: ['<rootDir>/test-resources/env-vars.js'],
setupFilesAfterEnv: ['<rootDir>/tests/setupTests.ts']
};

View File

@ -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"
}

View File

@ -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": [
"<rootDir>/test-resources/env-vars.js"
]
},
"jest-junit": {
"outputDirectory": "reports",
"outputName": "jest-junit.xml",

View File

@ -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')!;
export const getLoopsApiKey = () => infisical.get('LOOPS_API_KEY')!;
export const getSmtpConfigured = () => infisical.get('SMTP_HOST') == '' || infisical.get('SMTP_HOST') == undefined ? false : true

View File

@ -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,

View File

@ -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({

View File

@ -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);
}
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);
}

View File

@ -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);
}
}
};

View File

@ -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();
})

View File

@ -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()
})
}
);

View File

@ -0,0 +1,5 @@
import { it, expect } from '@jest/globals';
it('should return true', () => {
expect(true).toBeTruthy();
});

View File

@ -18,4 +18,4 @@ describe('Healthcheck endpoint', () => {
const res = await request(server).get('/healthcheck');
expect(res.status).toEqual(200);
});
});
});

View File

@ -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

View File

@ -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=

View File

@ -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)

View File

@ -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)
}

View File

@ -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
}

View File

@ -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: []

View File

@ -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.
<Accordion title="I'm getting a Keyring related error message when trying to login" defaultOpen="true">
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
<Accordion title="Can I upload the .infisical.json file that was generated?">
Yes. This is simply a configuration file and contains no sensitive data.
</Accordion>
</Accordion>

View File

@ -30,6 +30,8 @@ Start syncing environment variables with [Infisical Cloud](https://app.infisical
>
Learn how to configure and deploy Infisical.
</Card>
</CardGroup>
<Card
href="/integrations/overview"
title="Integrations"
@ -38,16 +40,3 @@ Start syncing environment variables with [Infisical Cloud](https://app.infisical
>
Explore integrations for Docker, AWS, Heroku, etc.
</Card>
</CardGroup>
<Card
title="Set up a 1x1 with an Infisical Engineer"
iconType="duotone"
color="#ca8b04"
href="https://calendly.com/maidull/30min"
>
Our team is happy to help you get started with Infisical. If you have any
questions or want to learn how you can leverage Infisical within your
infrastructure, **[set up a 1-on-1 with an Infisical
engineer](https://cal.com/maidul/15min)**.
</Card>

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

View File

@ -39,10 +39,10 @@ Prerequisites:
Starting your service with the Infisical CLI pulls your secrets from Infisical and injects them into your service.
```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]...
```
<Info>

View File

@ -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": [

View File

@ -1,5 +1,5 @@
---
title: "Email"
title: "Configure email service"
description: "How to configure your email when self-hosting Infisical."
---

View File

@ -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.
<Tabs>
<Tab title="Required">
<ParamField query="ENCRYPTION_KEY" type="string" default="none" required>
Must be a random 32 character length hex string
</ParamField>
<ParamField query="JWT_SIGNUP_SECRET" type="string" default="none" required>
Must be a random 32 character length hex string
</ParamField>
<ParamField query="JWT_REFRESH_SECRET" type="string" default="none" required>
Must be a random 32 character length hex string
</ParamField>
<ParamField query="JWT_AUTH_SECRET" type="string" default="none" required>
Must be a random 32 character length hex string
</ParamField>
<ParamField query="JWT_MFA_SECRET" type="string" default="none" required>
Must be a random 32 character length hex string
</ParamField>
<ParamField query="JWT_SERVICE_SECRET" type="string" default="none" required>
Must be a random 32 character length hex string
</ParamField>
<ParamField query="MONGO_URL" type="string" default="none" required>
*TLS based connection string is not yet supported
</ParamField>
</Tab>
<Tab title="Email service">
<Info>When email service is not configured, Infisical will have limited functionality</Info>
<ParamField query="SMTP_HOST" type="string" default="none" optional>
Hostname to connect to for establishing SMTP connections
</ParamField>
<ParamField query="SMTP_USERNAME" type="string" default="none" optional>
Credential to connect to host (e.g. team@infisical.com)
</ParamField>
<ParamField query="SMTP_PASSWORD" type="string" default="587" optional>
Credential to connect to host
</ParamField>
<ParamField query="SMTP_PORT" type="string" default="587" optional>
Port to connect to for establishing SMTP connections
</ParamField>
<ParamField query="SMTP_SECURE" type="string" default="none" optional>
If true, use TLS when connecting to host. If false, TLS will be used if STARTTLS is supported
</ParamField>
<ParamField query="SMTP_FROM_ADDRESS" type="string" default="none" optional>
Email address to be used for sending emails
</ParamField>
<ParamField query="SMTP_FROM_NAME" type="string" default="none" optional>
Name label to be used in From field (e.g. Team)
</ParamField>
</Tab>
<Tab title="Integrations">
To sync secret to third party services, provide value for the related services
<ParamField query="CLIENT_ID_HEROKU" type="string" default="none" optional>
OAuth2 client ID for Heroku integration
</ParamField>
<ParamField query="CLIENT_SECRET_HEROKU" type="string" default="none" optional>
OAuth2 client secret for Heroku integration
</ParamField>
<ParamField query="CLIENT_ID_VERCEL" type="string" default="none" optional>
OAuth2 client ID for Vercel integration
</ParamField>
<ParamField query="CLIENT_SECRET_VERCEL" type="string" default="none" optional>
OAuth2 client secret for Vercel integration
</ParamField>
<ParamField query="CLIENT_ID_NETLIFY" type="string" default="none" optional>
OAuth2 client ID for Netlify integration
</ParamField>
<ParamField query="CLIENT_SECRET_NETLIFY" type="string" default="none" optional>
OAuth2 client secret for Netlify integration
</ParamField>
<ParamField query="CLIENT_ID_GITHUB" type="string" default="none" optional>
OAuth2 client ID for GitHub integration
</ParamField>
<ParamField query="CLIENT_SECRET_GITHUB" type="string" default="none" optional>
OAuth2 client secret for GitHub integration
</ParamField>
<ParamField query="CLIENT_SLUG_VERCEL" type="string" default="none" optional>
OAuth2 slug for Netlify integration
</ParamField>
</Tab>
<Tab title="Others">
#### JWT
<ParamField query="JWT_SIGNUP_LIFETIME" type="string" default="15m" optional>
JWT token lifetime expressed in seconds or a string describing a time span
</ParamField>
<ParamField query="JWT_REFRESH_LIFETIME" type="string" default="90d" optional>
JWT token lifetime expressed in seconds or a string describing a time span
</ParamField>
<ParamField query="JWT_AUTH_LIFETIME" type="string" default="10d" optional>
JWT token lifetime expressed in seconds or a string describing a time span
</ParamField>
<ParamField query="JWT_MFA_LIFETIME" type="string" default="5m" optional>
JWT token lifetime expressed in seconds or a string describing a time span
</ParamField>
<ParamField query="MONGO_USERNAME" type="string" default="none" optional></ParamField>
<ParamField query="MONGO_PASSWORD" type="string" default="none" optional></ParamField>
#### Error logging
Infisical uses Sentry to report error logs
<ParamField query="SENTRY_DSN" type="string" default="none" optional></ParamField>
#### Settings
<ParamField query="INVITE_ONLY_SIGNUP" type="string" default="false" optional>
Only allow users who are invited to sign up
</ParamField>
<ParamField query="SITE_URL" type="string" default="none" optional>
Site URL - should be an absolute URL including the protocol (e.g. https://app.infisical.com)
</ParamField>
<ParamField query="TELEMETRY_ENABLED" type="string" default="true" optional></ParamField>
</Tab>
</Tabs>
## Frontend environment variables
<ParamField query="TELEMETRY_ENABLED" type="string" default="true" optional></ParamField>

View File

@ -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"
---
<Info>
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.
<Tabs>
<Tab title="Quick deploy AWS">
<iframe width="560" height="315" src="https://www.youtube.com/embed/jR-gM7vIY2c" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" allowfullscreen></iframe>
This deployment option will use AWS Cloudformation to auto deploy an instance of Infisical on a single EC2 via Docker Compose.
If you're less technical and looking for a hands-free experience with minimal overhead then we recommend Infisical Cloud.
**Resources that will be provisioned**
- 1 EC2 instance
- 1 DocumentDB cluster
- 1 DocumentDB instance
- Security groups
Infisical Cloud also comes with some extra features unavailable in the self-hosted edition. You can find more information about Infisical Cloud's offering on the pricing page.
<a href="https://us-east-1.console.aws.amazon.com/cloudformation/home?region=us-east-1#/stacks/create/review?templateURL=https://ec2-instance-cloudformation.s3.amazonaws.com/cloudformation.template&stackName=infisical">
<img width="200" src="../images/deploy-aws-button.png" />
</a>
</Info>
</Tab>
<Tab title="Quick deploy Digital Ocean">
<Note>This deployment option is highly available</Note>
Coming soon
</Tab>
<Tab title="Helm Kubernetes">
<Note>This deployment option is highly available</Note>
**Prerequisites**
- You have understanding of [Kubernetes](https://kubernetes.io/)
- You have understanding of [Helm package manager](https://helm.sh/)
- You have [kubectl](https://kubernetes.io/docs/reference/kubectl/kubectl/) installed and connected to your kubernetes cluster
## Deployment options
Infisical can be deployed on a Linux VM with docker-compose and Kubernetes. We're rolling out more specific deployment options for DigitalOcean, AWS, GCP, and Azure soon.
#### 1. Fill our environment variables
<CardGroup cols={2}>
<Card title="Any Linux" icon="square-1" color="#ea5a0c" href="/self-hosting/deployments/linux">
Deploy to any Linux with Docker
</Card>
<Card title="Kubernetes" icon="square-2" color="#0285c7" href="/self-hosting/deployments/kubernetes">
Deploy to your Kubernetes cluster
</Card>
</CardGroup>
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
<Accordion title="values.yaml">
[View all available Helm chart values parameters](https://github.com/Infisical/infisical/tree/main/helm-charts/infisical)
```yaml
frontend:
enabled: true
name: frontend
podAnnotations: {}
deploymentAnnotations: {}
replicaCount: 2
image:
repository: infisical/frontend
tag: "latest"
pullPolicy: IfNotPresent
kubeSecretRef: ""
service:
annotations: {}
type: ClusterIP
nodePort: ""
Infisical collects telemetry data about general usage.
frontendEnvironmentVariables:
SITE_URL: infisical.local
The data helps us understand how the product is doing and guide our product development to create the best possible platform; it also helps us demonstrate growth for investors as we support Infisical as open-source software.
backend:
enabled: true
name: backend
podAnnotations: {}
deploymentAnnotations: {}
replicaCount: 2
image:
repository: infisical/backend
tag: "latest"
pullPolicy: IfNotPresent
kubeSecretRef: ""
service:
annotations: {}
type: ClusterIP
nodePort: ""
To opt out of telemetry, you can set `TELEMETRY_ENABLED=false` within the [environment variables](./configuration/envars).
backendEnvironmentVariables:
ENCRYPTION_KEY: MUST_REPLACE
JWT_SIGNUP_SECRET: MUST_REPLACE
JWT_REFRESH_SECRET: MUST_REPLACE
JWT_AUTH_SECRET: MUST_REPLACE
JWT_SERVICE_SECRET: MUST_REPLACE
SMTP_HOST: MUST_REPLACE
SMTP_PORT: 587
SMTP_SECURE: false
SMTP_FROM_NAME: Infisical
SMTP_FROM_ADDRESS: MUST_REPLACE
SMTP_USERNAME: MUST_REPLACE
SMTP_PASSWORD: MUST_REPLACE
SITE_URL: infisical.local
## Mongo DB persistence
mongodb:
enabled: true
## By default the backend will be connected to a Mongo instance within the cluster
## However, it is recommended to add a managed document DB connection string for production-use (DBaaS)
## Learn about connection string type here https://www.mongodb.com/docs/manual/reference/connection-string/
## e.g. "mongodb://<user>:<pass>@<host>:<port>/<database-name>"
mongodbConnection:
externalMongoDBConnectionString: ""
ingress:
enabled: true
annotations:
kubernetes.io/ingress.class: "nginx"
# cert-manager.io/issuer: letsencrypt-nginx
hostName: infisical.local ## <- Replace with your own domain
frontend:
path: /
pathType: Prefix
backend:
path: /api
pathType: Prefix
tls: []
# - secretName: letsencrypt-nginx
# hosts:
# - infisical.local
mailhog:
enabled: false
```
</Accordion>
Once you have a local copy of the values file, fill our the required environment variables and save the file.
#### 2. Install Infisical Helm repository
```bash
helm repo add infisical-helm-charts 'https://dl.cloudsmith.io/public/infisical/helm-charts/helm/charts/'
helm repo update
```
#### 3. Install the Helm chart
By default, the helm chart will be installed on your default namespace. If you wish to install the Chart on a different namespace, you may specify
that by adding the `--namespace <namespace-to-install-to>` to your `helm install` command.
```bash
## Installs to default namespace
helm install infisical-helm-charts/infisical --generate-name --values <path to the values.yaml you downloaded/created in step 2>
```
<Note>
If you have not filled out all of the required environment variables, you will see an error message prompting you to
do so.
</Note>
#### 4. Your Infisical installation is complete and should be running on the host name you specified in Ingress in `values.yaml`.
</Tab>
<Tab title="Bare Docker Compose">
1. Install Docker on your VM
```bash
# Example in ubuntu
apt-get update
apt-get upgrade
apt install docker-compose
```
2. Download the required files
```bash
# Download env file template
wget -O .env https://raw.githubusercontent.com/Infisical/infisical/main/.env.example
# Download docker compose template
wget -O docker-compose.yml https://raw.githubusercontent.com/Infisical/infisical/main/docker-compose.yml
# Download nginx config
mkdir nginx && wget -O ./nginx/default.conf https://raw.githubusercontent.com/Infisical/infisical/main/nginx/default.dev.conf
```
3. Tweak the `.env` according to your preferences. Refer to the available [environment variables](/self-hosting/configuration/envars)
```bash
# update environment variables like mongo login
nano .env
```
4. Get the service up and running.
```bash
# Start up services in detached mode
docker-compose -f docker-compose.yml up -d
```
5. Your Infisical installation is complete and should be running on [http://localhost:80](http://localhost:80). Please note that the containers are not exposed to the internet and only bind to the localhost. It's up to you to configure a firewall, SSL certificates, and implement any additional security measures.
</Tab>
</Tabs>

View File

@ -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 {
</div>
<Button
text={t('signup:step5-send-invites') ?? ''}
onButtonPressed={() => inviteUsers({ emails })}
onButtonPressed={() => {
if(serverDetails?.emailConfigured){
inviteUsers({ emails })
}else{
handlePopUpOpen('setUpEmail');
}
}}
size="lg"
/>
</div>
<EmailServiceSetupModal
isOpen={popUp.setUpEmail?.isOpen}
onOpenChange={(isOpen) => handlePopUpToggle('setUpEmail', isOpen)}
/>
</div>
);
}

View File

@ -4,13 +4,11 @@ const POSTHOG_HOST =
process.env.NEXT_PUBLIC_POSTHOG_HOST! || "https://app.posthog.com";
const STRIPE_PRODUCT_PRO = process.env.NEXT_PUBLIC_STRIPE_PRODUCT_PRO!;
const STRIPE_PRODUCT_STARTER = process.env.NEXT_PUBLIC_STRIPE_PRODUCT_STARTER!;
const SITE_URL = "https://app.infisical.com";
export {
ENV,
POSTHOG_API_KEY,
POSTHOG_HOST,
SITE_URL,
STRIPE_PRODUCT_PRO,
STRIPE_PRODUCT_STARTER
};

View File

@ -1,7 +1,5 @@
import { jsPDF } from 'jspdf';
import { SITE_URL } from './config';
interface PDFProps {
personalName: string;
personalEmail: string;
@ -19,7 +17,7 @@ const yyyy = today.getFullYear();
const todayFormatted = `${mm}/${dd}/${yyyy}`;
function createPdfHeader(doc: jsPDF, personalName : string) {
function createPdfHeader(doc: jsPDF, personalName: string) {
doc.setFillColor(255, 255, 255);
doc.rect(0, 0, 600, 900, 'F');
doc.setTextColor(23, 23, 23);
@ -30,6 +28,10 @@ function createPdfHeader(doc: jsPDF, personalName : string) {
}
function createPdfContent(doc: jsPDF, personalEmail: string, generatedKey: string) {
const { protocol, hostname, port } = window.location;
const portSuffix = port && port !== '80' ? `:${port}` : '';
const siteURL = `${protocol}//${hostname}${portSuffix}`;
doc.setFontSize(14);
doc.text(
'In case you get locked out of you Infisical account, you`ll need these account details',
@ -73,7 +75,7 @@ function createPdfContent(doc: jsPDF, personalEmail: string, generatedKey: strin
doc.roundedRect(170, 488, 375, 35, 5, 5, 'F');
doc.setTextColor(23, 23, 23);
doc.setFontSize(14);
doc.text(`${SITE_URL}/login`, 180, 420);
doc.text(`${siteURL}/login`, 180, 420);
doc.text(personalEmail, 180, 465);
doc.text(generatedKey, 180, 510);
doc.text('Need help? Contact us at support@infisical.com', 32, 575);

View File

@ -0,0 +1,21 @@
import { Button } from '../Button';
import { Modal, ModalContent } from '../Modal';
type Props = {
isOpen?: boolean;
onOpenChange?: (isOpen: boolean) => void;
};
export const EmailServiceSetupModal = ({ isOpen, onOpenChange }: Props): JSX.Element => (
<Modal isOpen={isOpen} onOpenChange={onOpenChange}>
<ModalContent title="Email service not configured">
<p className="mb-4 text-bunker-300">
The administrators of this Infisical instance have not yet set up an email service provider required to perform this action
</p>
<a href="https://infisical.com/docs/self-hosting/configuration/email">
<Button className="mr-4">Learn more</Button>
</a>
</ModalContent>
</Modal>
);

View File

@ -0,0 +1 @@
export { EmailServiceSetupModal } from './EmailServiceSetupModal';

View File

@ -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';

View File

@ -0,0 +1 @@
export { useFetchServerStatus } from './queries'

View File

@ -0,0 +1,19 @@
import {useQuery } from '@tanstack/react-query';
import { apiRequest } from '@app/config/request';
import { ServerStatus } from './types';
// cache key
const serverStatusKeys = {
serverStatus: ['serverStatus'] as const
};
const fetchServerStatus = async () => {
const {data} = await apiRequest.get<ServerStatus>('/api/status');
return data;
};
export const useFetchServerStatus= () => {
return useQuery({ queryKey: serverStatusKeys.serverStatus, queryFn: fetchServerStatus });
}

View File

@ -0,0 +1,5 @@
export type ServerStatus = {
date: string;
message: string;
emailConfigured: boolean;
};

View File

@ -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<Response, {}, AddUserToOrgDTO>({
mutationFn: (dto) => apiRequest.post(`/api/v1/invite-org/signup`, dto),
onSuccess: (_, { organizationId }) => {
queryClient.invalidateQueries(userKeys.getOrgUsers(organizationId));

View File

@ -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 (
<div className="bg-bunker-800 h-screen flex flex-col items-center justify-center">
<Head>
@ -111,9 +127,7 @@ export default function SignUp() {
password={password}
name={`${firstName} ${lastName}`}
/>
) : (
<TeamInviteStep />
)}
) : (serverDetails?.emailConfigured ? <TeamInviteStep /> : "")}
</form>
</div>
</div>

View File

@ -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() {
</div>
<div className="flex flex-col items-center justify-center w-full md:p-2 max-h-20 max-w-md mt-4 mx-auto text-sm">
<div className="text-l mt-6 m-8 px-8 py-3 text-lg">
<Button text="Continue" onButtonPressed={sendVerificationEmail} size="lg" />
<Button text="Continue" onButtonPressed={()=>{
if (serverDetails?.emailConfigured){
sendVerificationEmail()
} else {
handlePopUpOpen('setUpEmail');
}
}} size="lg" />
</div>
</div>
</div>
@ -80,6 +93,11 @@ export default function VerifyEmail() {
</div>
</div>
)}
<EmailServiceSetupModal
isOpen={popUp.setUpEmail?.isOpen}
onOpenChange={(isOpen) => handlePopUpToggle('setUpEmail', isOpen)}
/>
</div>
);
}

View File

@ -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<string|undefined>("")
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}
/>
</div>
<div className="mb-6 mt-2 w-full rounded-md bg-white/5 p-6">

View File

@ -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 = ({
<div>
<Button
leftIcon={<FontAwesomeIcon icon={faPlus} />}
onClick={() => handlePopUpOpen('addContact')}
onClick={() => {
if (serverDetails?.emailConfigured){
handlePopUpOpen('addContact');
} else {
handlePopUpOpen('setUpEmail');
}
}}
>
Add Contact
</Button>
@ -181,6 +191,10 @@ export const OrgIncidentContactsTable = ({
onChange={(isOpen) => handlePopUpToggle('removeContact', isOpen)}
onDeleteApproved={onRemoveIncidentContact}
/>
<EmailServiceSetupModal
isOpen={popUp.setUpEmail?.isOpen}
onOpenChange={(isOpen) => handlePopUpToggle('setUpEmail', isOpen)}
/>
</div>
);
};

View File

@ -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<void>;
// the current user id to block remove org button
userId: string;
completeInviteLink: string | undefined,
setCompleteInviteLink: Dispatch<SetStateAction<string | undefined>>
};
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 (
<div className="w-full">
<div className="mb-4 flex">
@ -171,7 +186,7 @@ export const OrgMembersTable = ({
<SelectItem value="member">member</SelectItem>
</Select>
)}
{(status === 'invited' || status === 'verified') && (
{((status === 'invited' || status === 'verified') && serverDetails?.emailConfigured) && (
<Button className='w-40' colorSchema="secondary" onClick={() => onInviteMember(email)}>
Resend Invite
</Button>
@ -232,20 +247,23 @@ export const OrgMembersTable = ({
isOpen={popUp?.addMember?.isOpen}
onOpenChange={(isOpen) => {
handlePopUpToggle('addMember', isOpen);
reset();
setCompleteInviteLink(undefined)
}}
>
<ModalContent
title={`Invite others to ${orgName}`}
subTitle={
<>
An invite is specific to an email address and expires after 1 day.
<br />
For security reasons, you will need to separately add members to projects.
</>
<div>
{!completeInviteLink && <div>
An invite is specific to an email address and expires after 1 day.
<br />
For security reasons, you will need to separately add members to projects.
</div>}
{completeInviteLink && "This Infisical instance does not have a email provider setup. Please share this invite link with the invitee manually"}
</div>
}
>
<form onSubmit={handleSubmit(onAddMember)}>
{!completeInviteLink && <form onSubmit={handleSubmit(onAddMember)} >
<Controller
control={control}
defaultValue=""
@ -274,7 +292,22 @@ export const OrgMembersTable = ({
Cancel
</Button>
</div>
</form>
</form>}
{
completeInviteLink &&
<div className="mt-2 mb-3 mr-2 flex items-center justify-end rounded-md bg-white/[0.07] p-2 text-base text-gray-400">
<p className="mr-4 break-all">{completeInviteLink}</p>
<IconButton
ariaLabel="copy icon"
colorSchema="secondary"
className="group relative"
onClick={copyTokenToClipboard}
>
<FontAwesomeIcon icon={false ? faCheck : faCopy} />
<span className="absolute -left-8 -top-20 hidden w-28 translate-y-full rounded-md bg-bunker-800 py-2 pl-3 text-center text-sm text-gray-400 group-hover:flex group-hover:animate-fadeIn">click to copy</span>
</IconButton>
</div>
}
</ModalContent>
</Modal>
<DeleteActionModal
@ -289,6 +322,10 @@ export const OrgMembersTable = ({
onOpenChange={(isOpen) => handlePopUpToggle('upgradePlan', isOpen)}
text="You can add custom environments if you switch to Infisical's Team plan."
/>
<EmailServiceSetupModal
isOpen={popUp.setUpEmail?.isOpen}
onOpenChange={(isOpen) => handlePopUpToggle('setUpEmail', isOpen)}
/>
</div>
);
};

View File

@ -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 (
<>
<form>
<div className="mb-6 mt-2 flex w-full flex-col items-start rounded-md bg-white/5 px-6 pb-6 pt-2">
<p className="mb-4 mt-2 text-xl font-semibold">
@ -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.
</Checkbox>
</div>
</form>
<EmailServiceSetupModal
isOpen={popUp.setUpEmail?.isOpen}
onOpenChange={(isOpen) => handlePopUpToggle('setUpEmail', isOpen)}
/>
</>
);
};