diff --git a/backend/package-lock.json b/backend/package-lock.json index 3d9a148a6..98e3fc25b 100644 --- a/backend/package-lock.json +++ b/backend/package-lock.json @@ -34,7 +34,7 @@ "express-validator": "^6.14.2", "handlebars": "^4.7.7", "helmet": "^5.1.1", - "infisical-node": "^1.0.37", + "infisical-node": "^1.1.3", "js-yaml": "^4.1.0", "jsonwebtoken": "^9.0.0", "jsrp": "^0.2.4", @@ -5345,6 +5345,14 @@ "node": ">=12" } }, + "node_modules/clone": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/clone/-/clone-2.1.2.tgz", + "integrity": "sha512-3Pe/CF1Nn94hyhIYpjtiLhdCoEoz0DqQ+988E9gmeEdQZlojxnOb74wctFyuwWQHzqyf9X7C7MG8juUpqBJT8w==", + "engines": { + "node": ">=0.8" + } + }, "node_modules/co": { "version": "4.6.0", "resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz", @@ -6941,11 +6949,13 @@ } }, "node_modules/infisical-node": { - "version": "1.0.37", - "resolved": "https://registry.npmjs.org/infisical-node/-/infisical-node-1.0.37.tgz", - "integrity": "sha512-9ZswN5UovZq46a7Qv/4KmfaAu9pO/TmxxdcEY2PosDIAlXbpfC651hcKr7T8q1iMlRnHLA/gpYkcZMeS0es0rg==", + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/infisical-node/-/infisical-node-1.1.3.tgz", + "integrity": "sha512-MLcZQ/zdpCYFRbj50Tn4Qm58wSKPQfKc3xX4I0c3NnFZvMGd50wnoG1jkkNKjKiYU5h7QDpOg0XZSvlU7yuG6g==", "dependencies": { "axios": "^1.3.3", + "dotenv": "^16.0.3", + "node-cache": "^5.1.2", "tweetnacl": "^1.0.3", "tweetnacl-util": "^0.15.1" } @@ -8439,6 +8449,17 @@ "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-5.1.0.tgz", "integrity": "sha512-eh0GgfEkpnoWDq+VY8OyvYhFEzBk6jIYbRKdIlyTiAXIVJ8PyBaKb0rp7oDtoddbdoHWhq8wwr+XZ81F1rpNdA==" }, + "node_modules/node-cache": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/node-cache/-/node-cache-5.1.2.tgz", + "integrity": "sha512-t1QzWwnk4sjLWaQAS8CHgOJ+RAfmHpxFWmc36IWTiWHQfs0w5JDMBS1b1ZxQteo0vVVuWJvIUKHDkkeK7vIGCg==", + "dependencies": { + "clone": "2.x" + }, + "engines": { + "node": ">= 8.0.0" + } + }, "node_modules/node-fetch": { "version": "2.6.9", "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.9.tgz", @@ -17402,6 +17423,11 @@ "wrap-ansi": "^7.0.0" } }, + "clone": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/clone/-/clone-2.1.2.tgz", + "integrity": "sha512-3Pe/CF1Nn94hyhIYpjtiLhdCoEoz0DqQ+988E9gmeEdQZlojxnOb74wctFyuwWQHzqyf9X7C7MG8juUpqBJT8w==" + }, "co": { "version": "4.6.0", "resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz", @@ -18622,11 +18648,13 @@ "dev": true }, "infisical-node": { - "version": "1.0.37", - "resolved": "https://registry.npmjs.org/infisical-node/-/infisical-node-1.0.37.tgz", - "integrity": "sha512-9ZswN5UovZq46a7Qv/4KmfaAu9pO/TmxxdcEY2PosDIAlXbpfC651hcKr7T8q1iMlRnHLA/gpYkcZMeS0es0rg==", + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/infisical-node/-/infisical-node-1.1.3.tgz", + "integrity": "sha512-MLcZQ/zdpCYFRbj50Tn4Qm58wSKPQfKc3xX4I0c3NnFZvMGd50wnoG1jkkNKjKiYU5h7QDpOg0XZSvlU7yuG6g==", "requires": { "axios": "^1.3.3", + "dotenv": "^16.0.3", + "node-cache": "^5.1.2", "tweetnacl": "^1.0.3", "tweetnacl-util": "^0.15.1" } @@ -19774,6 +19802,14 @@ "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-5.1.0.tgz", "integrity": "sha512-eh0GgfEkpnoWDq+VY8OyvYhFEzBk6jIYbRKdIlyTiAXIVJ8PyBaKb0rp7oDtoddbdoHWhq8wwr+XZ81F1rpNdA==" }, + "node-cache": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/node-cache/-/node-cache-5.1.2.tgz", + "integrity": "sha512-t1QzWwnk4sjLWaQAS8CHgOJ+RAfmHpxFWmc36IWTiWHQfs0w5JDMBS1b1ZxQteo0vVVuWJvIUKHDkkeK7vIGCg==", + "requires": { + "clone": "2.x" + } + }, "node-fetch": { "version": "2.6.9", "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.9.tgz", diff --git a/backend/package.json b/backend/package.json index 1f4aa7f49..aef504b68 100644 --- a/backend/package.json +++ b/backend/package.json @@ -3,7 +3,7 @@ "@aws-sdk/client-secrets-manager": "^3.303.0", "@godaddy/terminus": "^4.11.2", "@octokit/rest": "^19.0.5", - "@sentry/node": "^7.45.0", + "@sentry/node": "^7.41.0", "@sentry/tracing": "^7.46.0", "@types/crypto-js": "^4.1.1", "@types/libsodium-wrappers": "^0.7.10", @@ -25,7 +25,7 @@ "express-validator": "^6.14.2", "handlebars": "^4.7.7", "helmet": "^5.1.1", - "infisical-node": "^1.0.37", + "infisical-node": "^1.1.3", "js-yaml": "^4.1.0", "jsonwebtoken": "^9.0.0", "jsrp": "^0.2.4", diff --git a/backend/src/config/index.ts b/backend/src/config/index.ts index 870c7b2bb..efbc90df8 100644 --- a/backend/src/config/index.ts +++ b/backend/src/config/index.ts @@ -1,67 +1,72 @@ -import infisical from 'infisical-node'; -export const getPort = () => infisical.get('PORT')! || 4000; -export const getInviteOnlySignup = () => infisical.get('INVITE_ONLY_SIGNUP')! == undefined ? false : infisical.get('INVITE_ONLY_SIGNUP'); -export const getEncryptionKey = () => infisical.get('ENCRYPTION_KEY')!; -export const getSaltRounds = () => parseInt(infisical.get('SALT_ROUNDS')!) || 10; -export const getJwtAuthLifetime = () => infisical.get('JWT_AUTH_LIFETIME')! || '10d'; -export const getJwtAuthSecret = () => infisical.get('JWT_AUTH_SECRET')!; -export const getJwtMfaLifetime = () => infisical.get('JWT_MFA_LIFETIME')! || '5m'; -export const getJwtMfaSecret = () => infisical.get('JWT_MFA_LIFETIME')! || '5m'; -export const getJwtRefreshLifetime = () => infisical.get('JWT_REFRESH_LIFETIME')! || '90d'; -export const getJwtRefreshSecret = () => infisical.get('JWT_REFRESH_SECRET')!; -export const getJwtServiceSecret = () => infisical.get('JWT_SERVICE_SECRET')!; -export const getJwtSignupLifetime = () => infisical.get('JWT_SIGNUP_LIFETIME')! || '15m'; -export const getJwtSignupSecret = () => infisical.get('JWT_SIGNUP_SECRET')!; -export const getJwtProviderAuthSecret = () => infisical.get('JWT_PROVIDER_AUTH_SECRET')!; -export const getJwtProviderAuthLifetime = () => infisical.get('JWT_PROVIDER_AUTH_LIFETIME')! || '15m'; -export const getMongoURL = () => infisical.get('MONGO_URL')!; -export const getNodeEnv = () => infisical.get('NODE_ENV')! || 'production'; -export const getVerboseErrorOutput = () => infisical.get('VERBOSE_ERROR_OUTPUT')! === 'true' && true; -export const getLokiHost = () => infisical.get('LOKI_HOST')!; -export const getClientIdAzure = () => infisical.get('CLIENT_ID_AZURE')!; -export const getClientIdHeroku = () => infisical.get('CLIENT_ID_HEROKU')!; -export const getClientIdVercel = () => infisical.get('CLIENT_ID_VERCEL')!; -export const getClientIdNetlify = () => infisical.get('CLIENT_ID_NETLIFY')!; -export const getClientIdGitHub = () => infisical.get('CLIENT_ID_GITHUB')!; -export const getClientIdGitLab = () => infisical.get('CLIENT_ID_GITLAB')!; -export const getClientSecretAzure = () => infisical.get('CLIENT_SECRET_AZURE')!; -export const getClientSecretHeroku = () => infisical.get('CLIENT_SECRET_HEROKU')!; -export const getClientSecretVercel = () => infisical.get('CLIENT_SECRET_VERCEL')!; -export const getClientSecretNetlify = () => infisical.get('CLIENT_SECRET_NETLIFY')!; -export const getClientSecretGitHub = () => infisical.get('CLIENT_SECRET_GITHUB')!; -export const getClientSecretGitLab = () => infisical.get('CLIENT_SECRET_GITLAB')!; -export const getClientSlugVercel = () => infisical.get('CLIENT_SLUG_VERCEL')!; -export const getPostHogHost = () => infisical.get('POSTHOG_HOST')! || 'https://app.posthog.com'; -export const getPostHogProjectApiKey = () => infisical.get('POSTHOG_PROJECT_API_KEY')! || 'phc_nSin8j5q2zdhpFDI1ETmFNUIuTG4DwKVyIigrY10XiE'; -export const getSentryDSN = () => infisical.get('SENTRY_DSN')!; -export const getSessionSecret = () => infisical.get('SESSION_SECRET')!; -export const getSiteURL = () => infisical.get('SITE_URL')!; -export const getSmtpHost = () => infisical.get('SMTP_HOST')!; -export const getSmtpSecure = () => infisical.get('SMTP_SECURE')! === 'true' || false; -export const getSmtpPort = () => parseInt(infisical.get('SMTP_PORT')!) || 587; -export const getSmtpUsername = () => infisical.get('SMTP_USERNAME')!; -export const getSmtpPassword = () => infisical.get('SMTP_PASSWORD')!; -export const getSmtpFromAddress = () => infisical.get('SMTP_FROM_ADDRESS')!; -export const getSmtpFromName = () => infisical.get('SMTP_FROM_NAME')! || 'Infisical'; -export const getStripeProductStarter = () => infisical.get('STRIPE_PRODUCT_STARTER')!; -export const getStripeProductPro = () => infisical.get('STRIPE_PRODUCT_PRO')!; -export const getStripeProductTeam = () => infisical.get('STRIPE_PRODUCT_TEAM')!; -export const getStripePublishableKey = () => infisical.get('STRIPE_PUBLISHABLE_KEY')!; -export const getStripeSecretKey = () => infisical.get('STRIPE_SECRET_KEY')!; -export const getStripeWebhookSecret = () => infisical.get('STRIPE_WEBHOOK_SECRET')!; -export const getTelemetryEnabled = () => infisical.get('TELEMETRY_ENABLED')! !== 'false' && true; -export const getLoopsApiKey = () => infisical.get('LOOPS_API_KEY')!; -export const getSmtpConfigured = () => infisical.get('SMTP_HOST') == '' || infisical.get('SMTP_HOST') == undefined ? false : true -export const getHttpsEnabled = () => { - if (getNodeEnv() != "production") { +import InfisicalClient from 'infisical-node'; + +const client = new InfisicalClient({ + token: process.env.INFISICAL_TOKEN! +}); + +export const getPort = async () => (await client.getSecret('PORT')).secretValue || 4000; +export const getInviteOnlySignup = async () => (await client.getSecret('INVITE_ONLY_SIGNUP')).secretValue == undefined ? false : await client.getSecret('INVITE_ONLY_SIGNUP'); +export const getEncryptionKey = async () => (await client.getSecret('ENCRYPTION_KEY')).secretValue; +export const getSaltRounds = async () => parseInt((await client.getSecret('SALT_ROUNDS')).secretValue) || 10; +export const getJwtAuthLifetime = async () => (await client.getSecret('JWT_AUTH_LIFETIME')).secretValue || '10d'; +export const getJwtAuthSecret = async () => (await client.getSecret('JWT_AUTH_SECRET')).secretValue; +export const getJwtMfaLifetime = async () => (await client.getSecret('JWT_MFA_LIFETIME')).secretValue || '5m'; +export const getJwtMfaSecret = async () => (await client.getSecret('JWT_MFA_LIFETIME')).secretValue || '5m'; +export const getJwtRefreshLifetime = async () => (await client.getSecret('JWT_REFRESH_LIFETIME')).secretValue || '90d'; +export const getJwtRefreshSecret = async () => (await client.getSecret('JWT_REFRESH_SECRET')).secretValue; +export const getJwtServiceSecret = async () => (await client.getSecret('JWT_SERVICE_SECRET')).secretValue; +export const getJwtSignupLifetime = async () => (await client.getSecret('JWT_SIGNUP_LIFETIME')).secretValue || '15m'; +export const getJwtProviderAuthSecret = async () => (await client.getSecret('JWT_PROVIDER_AUTH_SECRET')).secretValue; +export const getJwtProviderAuthLifetime = async () => (await client.getSecret('JWT_PROVIDER_AUTH_LIFETIME')).secretValue || '15m'; +export const getJwtSignupSecret = async () => (await client.getSecret('JWT_SIGNUP_SECRET')).secretValue; +export const getMongoURL = async () => (await client.getSecret('MONGO_URL')).secretValue; +export const getNodeEnv = async () => (await client.getSecret('NODE_ENV')).secretValue || 'production'; +export const getVerboseErrorOutput = async () => (await client.getSecret('VERBOSE_ERROR_OUTPUT')).secretValue === 'true' && true; +export const getLokiHost = async () => (await client.getSecret('LOKI_HOST')).secretValue; +export const getClientIdAzure = async () => (await client.getSecret('CLIENT_ID_AZURE')).secretValue; +export const getClientIdHeroku = async () => (await client.getSecret('CLIENT_ID_HEROKU')).secretValue; +export const getClientIdVercel = async () => (await client.getSecret('CLIENT_ID_VERCEL')).secretValue; +export const getClientIdNetlify = async () => (await client.getSecret('CLIENT_ID_NETLIFY')).secretValue; +export const getClientIdGitHub = async () => (await client.getSecret('CLIENT_ID_GITHUB')).secretValue; +export const getClientIdGitLab = async () => (await client.getSecret('CLIENT_ID_GITLAB')).secretValue; +export const getClientSecretAzure = async () => (await client.getSecret('CLIENT_SECRET_AZURE')).secretValue; +export const getClientSecretHeroku = async () => (await client.getSecret('CLIENT_SECRET_HEROKU')).secretValue; +export const getClientSecretVercel = async () => (await client.getSecret('CLIENT_SECRET_VERCEL')).secretValue; +export const getClientSecretNetlify = async () => (await client.getSecret('CLIENT_SECRET_NETLIFY')).secretValue; +export const getClientSecretGitHub = async () => (await client.getSecret('CLIENT_SECRET_GITHUB')).secretValue; +export const getClientSecretGitLab = async () => (await client.getSecret('CLIENT_SECRET_GITLAB')).secretValue; +export const getClientSlugVercel = async () => (await client.getSecret('CLIENT_SLUG_VERCEL')).secretValue; +export const getPostHogHost = async () => (await client.getSecret('POSTHOG_HOST')).secretValue || 'https://app.posthog.com'; +export const getPostHogProjectApiKey = async () => (await client.getSecret('POSTHOG_PROJECT_API_KEY')).secretValue || 'phc_nSin8j5q2zdhpFDI1ETmFNUIuTG4DwKVyIigrY10XiE'; +export const getSentryDSN = async () => (await client.getSecret('SENTRY_DSN')).secretValue; +export const getSessionSecret = async () => (await client.getSecret('SESSION_SECRET')).secretValue; +export const getSiteURL = async () => (await client.getSecret('SITE_URL')).secretValue; +export const getSmtpHost = async () => (await client.getSecret('SMTP_HOST')).secretValue; +export const getSmtpSecure = async () => (await client.getSecret('SMTP_SECURE')).secretValue === 'true' || false; +export const getSmtpPort = async () => parseInt((await client.getSecret('SMTP_PORT')).secretValue) || 587; +export const getSmtpUsername = async () => (await client.getSecret('SMTP_USERNAME')).secretValue; +export const getSmtpPassword = async () => (await client.getSecret('SMTP_PASSWORD')).secretValue; +export const getSmtpFromAddress = async () => (await client.getSecret('SMTP_FROM_ADDRESS')).secretValue; +export const getSmtpFromName = async () => (await client.getSecret('SMTP_FROM_NAME')).secretValue || 'Infisical'; +export const getStripeProductStarter = async () => (await client.getSecret('STRIPE_PRODUCT_STARTER')).secretValue; +export const getStripeProductPro = async () => (await client.getSecret('STRIPE_PRODUCT_PRO')).secretValue; +export const getStripeProductTeam = async () => (await client.getSecret('STRIPE_PRODUCT_TEAM')).secretValue; +export const getStripePublishableKey = async () => (await client.getSecret('STRIPE_PUBLISHABLE_KEY')).secretValue; +export const getStripeSecretKey = async () => (await client.getSecret('STRIPE_SECRET_KEY')).secretValue; +export const getStripeWebhookSecret = async () => (await client.getSecret('STRIPE_WEBHOOK_SECRET')).secretValue; +export const getTelemetryEnabled = async () => (await client.getSecret('TELEMETRY_ENABLED')).secretValue !== 'false' && true; +export const getLoopsApiKey = async () => (await client.getSecret('LOOPS_API_KEY')).secretValue; +export const getSmtpConfigured = async () => (await client.getSecret('SMTP_HOST')).secretValue == '' || (await client.getSecret('SMTP_HOST')).secretValue == undefined ? false : true +export const getHttpsEnabled = async () => { + if ((await getNodeEnv()) != "production") { // no https for anything other than prod return false } - if (infisical.get('HTTPS_ENABLED') == undefined || infisical.get('HTTPS_ENABLED') == "") { + if ((await client.getSecret('HTTPS_ENABLED')).secretValue == undefined || (await client.getSecret('HTTPS_ENABLED')).secretValue == "") { // default when no value present return true } - return infisical.get('HTTPS_ENABLED') === 'true' && true + return (await client.getSecret('HTTPS_ENABLED')).secretValue === 'true' && true } \ No newline at end of file diff --git a/backend/src/controllers/v1/authController.ts b/backend/src/controllers/v1/authController.ts index c186c9cc0..15341341f 100644 --- a/backend/src/controllers/v1/authController.ts +++ b/backend/src/controllers/v1/authController.ts @@ -126,7 +126,7 @@ export const login2 = async (req: Request, res: Response) => { httpOnly: true, path: '/', sameSite: 'strict', - secure: getHttpsEnabled() + secure: await getHttpsEnabled() }); const loginAction = await EELogService.createAction({ @@ -182,7 +182,7 @@ export const logout = async (req: Request, res: Response) => { httpOnly: true, path: '/', sameSite: 'strict', - secure: getHttpsEnabled() as boolean + secure: (await getHttpsEnabled()) as boolean }); const logoutAction = await EELogService.createAction({ @@ -237,7 +237,7 @@ export const getNewToken = async (req: Request, res: Response) => { } const decodedToken = ( - jwt.verify(refreshToken, getJwtRefreshSecret()) + jwt.verify(refreshToken, await getJwtRefreshSecret()) ); const user = await User.findOne({ @@ -252,8 +252,8 @@ export const getNewToken = async (req: Request, res: Response) => { payload: { userId: decodedToken.userId }, - expiresIn: getJwtAuthLifetime(), - secret: getJwtAuthSecret() + expiresIn: await getJwtAuthLifetime(), + secret: await getJwtAuthSecret() }); return res.status(200).send({ diff --git a/backend/src/controllers/v1/integrationAuthController.ts b/backend/src/controllers/v1/integrationAuthController.ts index 12a39a389..b21a0cd42 100644 --- a/backend/src/controllers/v1/integrationAuthController.ts +++ b/backend/src/controllers/v1/integrationAuthController.ts @@ -44,7 +44,7 @@ export const getIntegrationAuth = async (req: Request, res: Response) => { } export const getIntegrationOptions = async (req: Request, res: Response) => { - const INTEGRATION_OPTIONS = getIntegrationOptionsFunc(); + const INTEGRATION_OPTIONS = await getIntegrationOptionsFunc(); return res.status(200).send({ integrationOptions: INTEGRATION_OPTIONS, diff --git a/backend/src/controllers/v1/membershipController.ts b/backend/src/controllers/v1/membershipController.ts index 436be9dc4..67dd41a0d 100644 --- a/backend/src/controllers/v1/membershipController.ts +++ b/backend/src/controllers/v1/membershipController.ts @@ -215,7 +215,7 @@ export const inviteUserToWorkspace = async (req: Request, res: Response) => { inviterFirstName: req.user.firstName, inviterEmail: req.user.email, workspaceName: req.membership.workspace.name, - callback_url: getSiteURL() + '/login' + callback_url: (await getSiteURL()) + '/login' } }); } catch (err) { diff --git a/backend/src/controllers/v1/membershipOrgController.ts b/backend/src/controllers/v1/membershipOrgController.ts index 5a2b41b2b..b25a9b9a7 100644 --- a/backend/src/controllers/v1/membershipOrgController.ts +++ b/backend/src/controllers/v1/membershipOrgController.ts @@ -180,11 +180,11 @@ export const inviteUserToOrganization = async (req: Request, res: Response) => { organizationName: organization.name, email: inviteeEmail, token, - callback_url: getSiteURL() + '/signupinvite' + callback_url: (await getSiteURL()) + '/signupinvite' } }); - if (!getSmtpConfigured()) { + if (!(await getSmtpConfigured())) { completeInviteLink = `${siteUrl + '/signupinvite'}?token=${token}&to=${inviteeEmail}` } } @@ -257,8 +257,8 @@ export const verifyUserToOrganization = async (req: Request, res: Response) => { payload: { userId: user._id.toString() }, - expiresIn: getJwtSignupLifetime(), - secret: getJwtSignupSecret() + expiresIn: await getJwtSignupLifetime(), + secret: await getJwtSignupSecret() }); } catch (err) { Sentry.setUser(null); diff --git a/backend/src/controllers/v1/organizationController.ts b/backend/src/controllers/v1/organizationController.ts index 00ad87b82..6b082dac5 100644 --- a/backend/src/controllers/v1/organizationController.ts +++ b/backend/src/controllers/v1/organizationController.ts @@ -317,7 +317,7 @@ export const createOrganizationPortalSession = async ( ) => { let session; try { - const stripe = new Stripe(getStripeSecretKey(), { + const stripe = new Stripe(await getStripeSecretKey(), { apiVersion: '2022-08-01' }); @@ -333,13 +333,13 @@ export const createOrganizationPortalSession = async ( customer: req.membershipOrg.organization.customerId, mode: 'setup', payment_method_types: ['card'], - success_url: getSiteURL() + '/dashboard', - cancel_url: getSiteURL() + '/dashboard' + success_url: (await getSiteURL()) + '/dashboard', + cancel_url: (await getSiteURL()) + '/dashboard' }); } else { session = await stripe.billingPortal.sessions.create({ customer: req.membershipOrg.organization.customerId, - return_url: getSiteURL() + '/dashboard' + return_url: (await getSiteURL()) + '/dashboard' }); } @@ -365,7 +365,7 @@ export const getOrganizationSubscriptions = async ( ) => { let subscriptions; try { - const stripe = new Stripe(getStripeSecretKey(), { + const stripe = new Stripe(await getStripeSecretKey(), { apiVersion: '2022-08-01' }); diff --git a/backend/src/controllers/v1/passwordController.ts b/backend/src/controllers/v1/passwordController.ts index fed24419c..85c244505 100644 --- a/backend/src/controllers/v1/passwordController.ts +++ b/backend/src/controllers/v1/passwordController.ts @@ -44,7 +44,7 @@ export const emailPasswordReset = async (req: Request, res: Response) => { substitutions: { email, token, - callback_url: getSiteURL() + '/password-reset' + callback_url: (await getSiteURL()) + '/password-reset' } }); } catch (err) { @@ -91,8 +91,8 @@ export const emailPasswordResetVerify = async (req: Request, res: Response) => { payload: { userId: user._id.toString() }, - expiresIn: getJwtSignupLifetime(), - secret: getJwtSignupSecret() + expiresIn: await getJwtSignupLifetime(), + secret: await getJwtSignupSecret() }); } catch (err) { Sentry.setUser(null); diff --git a/backend/src/controllers/v1/secretController.ts b/backend/src/controllers/v1/secretController.ts index 3ec69122a..316f6cc3e 100644 --- a/backend/src/controllers/v1/secretController.ts +++ b/backend/src/controllers/v1/secretController.ts @@ -39,7 +39,7 @@ export const pushSecrets = async (req: Request, res: Response) => { // upload (encrypted) secrets to workspace with id [workspaceId] try { - const postHogClient = TelemetryService.getPostHogClient(); + const postHogClient = await TelemetryService.getPostHogClient(); let { secrets }: { secrets: PushSecret[] } = req.body; const { keys, environment, channel } = req.body; const { workspaceId } = req.params; @@ -114,7 +114,7 @@ export const pullSecrets = async (req: Request, res: Response) => { let secrets; let key; try { - const postHogClient = TelemetryService.getPostHogClient(); + const postHogClient = await TelemetryService.getPostHogClient(); const environment: string = req.query.environment as string; const channel: string = req.query.channel as string; const { workspaceId } = req.params; @@ -183,7 +183,7 @@ export const pullSecretsServiceToken = async (req: Request, res: Response) => { let secrets; let key; try { - const postHogClient = TelemetryService.getPostHogClient(); + const postHogClient = await TelemetryService.getPostHogClient(); const environment: string = req.query.environment as string; const channel: string = req.query.channel as string; const { workspaceId } = req.params; diff --git a/backend/src/controllers/v1/secretsFolderController.ts b/backend/src/controllers/v1/secretsFolderController.ts new file mode 100644 index 000000000..2e856c2a4 --- /dev/null +++ b/backend/src/controllers/v1/secretsFolderController.ts @@ -0,0 +1,89 @@ +import { Request, Response } from 'express'; +import { Secret } from '../../models'; +import Folder from '../../models/folder'; +import { BadRequestError } from '../../utils/errors'; +import { ROOT_FOLDER_PATH, getFolderPath, getParentPath, normalizePath, validateFolderName } from '../../utils/folder'; +import { ADMIN, MEMBER } from '../../variables'; +import { validateMembership } from '../../helpers/membership'; + +// TODO +// verify workspace id/environment +export const createFolder = async (req: Request, res: Response) => { + const { workspaceId, environment, folderName, parentFolderId } = req.body + if (!validateFolderName(folderName)) { + throw BadRequestError({ message: "Folder name cannot contain spaces. Only underscore and dashes" }) + } + + if (parentFolderId) { + const parentFolder = await Folder.find({ environment: environment, workspace: workspaceId, id: parentFolderId }); + if (!parentFolder) { + throw BadRequestError({ message: "The parent folder doesn't exist" }) + } + } + + let completePath = await getFolderPath(parentFolderId) + if (completePath == ROOT_FOLDER_PATH) { + completePath = "" + } + + const currentFolderPath = completePath + "/" + folderName // construct new path with current folder to be created + const normalizedCurrentPath = normalizePath(currentFolderPath) + const normalizedParentPath = getParentPath(normalizedCurrentPath) + + const existingFolder = await Folder.findOne({ + name: folderName, + workspace: workspaceId, + environment: environment, + parent: parentFolderId, + path: normalizedCurrentPath + }); + + if (existingFolder) { + return res.json(existingFolder) + } + + const newFolder = new Folder({ + name: folderName, + workspace: workspaceId, + environment: environment, + parent: parentFolderId, + path: normalizedCurrentPath, + parentPath: normalizedParentPath + }); + + await newFolder.save(); + + return res.json(newFolder) +} + +export const deleteFolder = async (req: Request, res: Response) => { + const { folderId } = req.params + const queue: any[] = [folderId]; + + const folder = await Folder.findById(folderId); + if (!folder) { + throw BadRequestError({ message: "The folder doesn't exist" }) + } + + // check that user is a member of the workspace + await validateMembership({ + userId: req.user._id.toString(), + workspaceId: folder.workspace as any, + acceptedRoles: [ADMIN, MEMBER] + }); + + while (queue.length > 0) { + const currentFolderId = queue.shift(); + + const childFolders = await Folder.find({ parent: currentFolderId }); + for (const childFolder of childFolders) { + queue.push(childFolder._id); + } + + await Secret.deleteMany({ folder: currentFolderId }); + + await Folder.deleteOne({ _id: currentFolderId }); + } + + res.send() +} \ No newline at end of file diff --git a/backend/src/controllers/v1/serviceTokenController.ts b/backend/src/controllers/v1/serviceTokenController.ts index 9f241349a..86a87f372 100644 --- a/backend/src/controllers/v1/serviceTokenController.ts +++ b/backend/src/controllers/v1/serviceTokenController.ts @@ -61,7 +61,7 @@ export const createServiceToken = async (req: Request, res: Response) => { workspaceId }, expiresIn: expiresIn, - secret: getJwtServiceSecret() + secret: await getJwtServiceSecret() }); } catch (err) { return res.status(400).send({ diff --git a/backend/src/controllers/v1/signupController.ts b/backend/src/controllers/v1/signupController.ts index 6adb0e7a8..193699c15 100644 --- a/backend/src/controllers/v1/signupController.ts +++ b/backend/src/controllers/v1/signupController.ts @@ -21,7 +21,7 @@ export const beginEmailSignup = async (req: Request, res: Response) => { try { email = req.body.email; - if (getInviteOnlySignup()) { + if (await 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) { @@ -75,7 +75,7 @@ export const verifyEmailSignup = async (req: Request, res: Response) => { } // verify email - if (getSmtpConfigured()) { + if (await getSmtpConfigured()) { await checkEmailVerification({ email, code @@ -93,8 +93,8 @@ export const verifyEmailSignup = async (req: Request, res: Response) => { payload: { userId: user._id.toString() }, - expiresIn: getJwtSignupLifetime(), - secret: getJwtSignupSecret() + expiresIn: await getJwtSignupLifetime(), + secret: await getJwtSignupSecret() }); } catch (err) { Sentry.setUser(null); diff --git a/backend/src/controllers/v1/stripeController.ts b/backend/src/controllers/v1/stripeController.ts index 1a981c088..809107ad9 100644 --- a/backend/src/controllers/v1/stripeController.ts +++ b/backend/src/controllers/v1/stripeController.ts @@ -13,7 +13,7 @@ export const handleWebhook = async (req: Request, res: Response) => { let event; try { // check request for valid stripe signature - const stripe = new Stripe(getStripeSecretKey(), { + const stripe = new Stripe(await getStripeSecretKey(), { apiVersion: '2022-08-01' }); @@ -21,7 +21,7 @@ export const handleWebhook = async (req: Request, res: Response) => { event = stripe.webhooks.constructEvent( req.body, sig, - getStripeWebhookSecret() + await getStripeWebhookSecret() ); } catch (err) { Sentry.setUser({ email: req.user.email }); diff --git a/backend/src/controllers/v2/apiKeyDataController.ts b/backend/src/controllers/v2/apiKeyDataController.ts index e4450ae90..86533c733 100644 --- a/backend/src/controllers/v2/apiKeyDataController.ts +++ b/backend/src/controllers/v2/apiKeyDataController.ts @@ -43,7 +43,7 @@ export const createAPIKeyData = async (req: Request, res: Response) => { const { name, expiresIn } = req.body; const secret = crypto.randomBytes(16).toString('hex'); - const secretHash = await bcrypt.hash(secret, getSaltRounds()); + const secretHash = await bcrypt.hash(secret, await getSaltRounds()); const expiresAt = new Date(); expiresAt.setSeconds(expiresAt.getSeconds() + expiresIn); diff --git a/backend/src/controllers/v2/authController.ts b/backend/src/controllers/v2/authController.ts index 92b4a159a..15350f6e5 100644 --- a/backend/src/controllers/v2/authController.ts +++ b/backend/src/controllers/v2/authController.ts @@ -124,8 +124,8 @@ export const login2 = async (req: Request, res: Response) => { payload: { userId: user._id.toString() }, - expiresIn: getJwtMfaLifetime(), - secret: getJwtMfaSecret() + expiresIn: await getJwtMfaLifetime(), + secret: await getJwtMfaSecret() }); const code = await TokenService.createToken({ @@ -163,7 +163,7 @@ export const login2 = async (req: Request, res: Response) => { httpOnly: true, path: '/', sameSite: 'strict', - secure: getHttpsEnabled() + secure: await getHttpsEnabled() }); // case: user does not have MFA enablgged @@ -302,7 +302,7 @@ export const verifyMfaToken = async (req: Request, res: Response) => { httpOnly: true, path: '/', sameSite: 'strict', - secure: getHttpsEnabled() + secure: await getHttpsEnabled() }); interface VerifyMfaTokenRes { diff --git a/backend/src/controllers/v2/secretController.ts b/backend/src/controllers/v2/secretController.ts index d551e8aa4..75eff3127 100644 --- a/backend/src/controllers/v2/secretController.ts +++ b/backend/src/controllers/v2/secretController.ts @@ -17,7 +17,7 @@ import { AccountNotFoundError } from '../../utils/errors'; * @param res */ export const createSecret = async (req: Request, res: Response) => { - const postHogClient = TelemetryService.getPostHogClient(); + const postHogClient = await TelemetryService.getPostHogClient(); const secretToCreate: CreateSecretRequestBody = req.body.secret; const { workspaceId, environment } = req.params const sanitizedSecret: SanitizedSecretForCreate = { @@ -70,7 +70,7 @@ export const createSecret = async (req: Request, res: Response) => { * @param res */ export const createSecrets = async (req: Request, res: Response) => { - const postHogClient = TelemetryService.getPostHogClient(); + const postHogClient = await TelemetryService.getPostHogClient(); const secretsToCreate: CreateSecretRequestBody[] = req.body.secrets; const { workspaceId, environment } = req.params const sanitizedSecretesToCreate: SanitizedSecretForCreate[] = [] @@ -132,7 +132,7 @@ export const createSecrets = async (req: Request, res: Response) => { * @param res */ export const deleteSecrets = async (req: Request, res: Response) => { - const postHogClient = TelemetryService.getPostHogClient(); + const postHogClient = await TelemetryService.getPostHogClient(); const { workspaceId, environmentName } = req.params const secretIdsToDelete: string[] = req.body.secretIds @@ -186,7 +186,7 @@ export const deleteSecrets = async (req: Request, res: Response) => { * @param res */ export const deleteSecret = async (req: Request, res: Response) => { - const postHogClient = TelemetryService.getPostHogClient(); + const postHogClient = await TelemetryService.getPostHogClient(); await Secret.findByIdAndDelete(req._secret._id) if (postHogClient) { @@ -215,7 +215,7 @@ export const deleteSecret = async (req: Request, res: Response) => { * @returns */ export const updateSecrets = async (req: Request, res: Response) => { - const postHogClient = TelemetryService.getPostHogClient(); + const postHogClient = await TelemetryService.getPostHogClient(); const { workspaceId, environmentName } = req.params const secretsModificationsRequested: ModifySecretRequestBody[] = req.body.secrets; const [secretIdsUserCanModifyError, secretIdsUserCanModify] = await to(Secret.find({ workspace: workspaceId, environment: environmentName }, { _id: 1 }).then()) @@ -283,7 +283,7 @@ export const updateSecrets = async (req: Request, res: Response) => { * @returns */ export const updateSecret = async (req: Request, res: Response) => { - const postHogClient = TelemetryService.getPostHogClient(); + const postHogClient = await TelemetryService.getPostHogClient(); const { workspaceId, environmentName } = req.params const secretModificationsRequested: ModifySecretRequestBody = req.body.secret; @@ -337,7 +337,7 @@ export const updateSecret = async (req: Request, res: Response) => { * @returns */ export const getSecrets = async (req: Request, res: Response) => { - const postHogClient = TelemetryService.getPostHogClient(); + const postHogClient = await TelemetryService.getPostHogClient(); const { environment } = req.query; const { workspaceId } = req.params; diff --git a/backend/src/controllers/v2/secretsController.ts b/backend/src/controllers/v2/secretsController.ts index f7b8349ff..51c93182b 100644 --- a/backend/src/controllers/v2/secretsController.ts +++ b/backend/src/controllers/v2/secretsController.ts @@ -25,6 +25,8 @@ import { BatchSecretRequest, BatchSecret } from '../../types/secret'; +import { getFolderPath, getFoldersInDirectory, normalizePath } from '../../utils/folder'; +import { ROOT_FOLDER_PATH } from '../../utils/folder'; /** * Peform a batch of any specified CUD secret operations @@ -35,7 +37,7 @@ import { export const batchSecrets = async (req: Request, res: Response) => { const channel = getChannelFromUserAgent(req.headers['user-agent']); - const postHogClient = TelemetryService.getPostHogClient(); + const postHogClient = await TelemetryService.getPostHogClient(); const { workspaceId, @@ -51,13 +53,18 @@ export const batchSecrets = async (req: Request, res: Response) => { const updateSecrets: BatchSecret[] = []; const deleteSecrets: Types.ObjectId[] = []; const actions: IAction[] = []; - + // get secret blind index salt const salt = await SecretService.getSecretBlindIndexSalt({ workspaceId: new Types.ObjectId(workspaceId) }); for await (const request of requests) { + const folderId = request.secret.folderId + + // TODO: need to auth folder + const fullFolderPath = await getFolderPath(folderId) + let secretBlindIndex = ''; switch (request.method) { case 'POST': @@ -72,19 +79,23 @@ export const batchSecrets = async (req: Request, res: Response) => { user: request.secret.type === SECRET_PERSONAL ? req.user : undefined, environment, workspace: new Types.ObjectId(workspaceId), + path: fullFolderPath, + folder: folderId, secretBlindIndex }); break; case 'PATCH': secretBlindIndex = await SecretService.generateSecretBlindIndexWithSalt({ secretName: request.secret.secretName, - salt + salt, }); updateSecrets.push({ ...request.secret, _id: new Types.ObjectId(request.secret._id), - secretBlindIndex + secretBlindIndex, + folder: folderId, + path: fullFolderPath, }); break; case 'DELETE': @@ -437,9 +448,9 @@ export const createSecrets = async (req: Request, res: Response) => { }); }) ); - + const newlyCreatedSecrets: ISecret[] = (await Secret.insertMany(secretsToInsert)).map((insertedSecret) => insertedSecret.toObject()); - + setTimeout(async () => { // trigger event - push secrets await EventService.handleEvent({ @@ -508,7 +519,7 @@ export const createSecrets = async (req: Request, res: Response) => { workspaceId: new Types.ObjectId(workspaceId) }); - const postHogClient = TelemetryService.getPostHogClient(); + const postHogClient = await TelemetryService.getPostHogClient(); if (postHogClient) { postHogClient.capture({ event: 'secrets added', @@ -578,9 +589,11 @@ export const getSecrets = async (req: Request, res: Response) => { } */ - const { tagSlugs } = req.query; + const { tagSlugs, secretsPath } = req.query; const workspaceId = req.query.workspaceId as string; const environment = req.query.environment as string; + const normalizedPath = normalizePath(secretsPath as string) + const folders = await getFoldersInDirectory(workspaceId as string, environment as string, normalizedPath) // secrets to return let secrets: ISecret[] = []; @@ -613,6 +626,12 @@ export const getSecrets = async (req: Request, res: Response) => { ] } + if (normalizedPath == ROOT_FOLDER_PATH) { + secretQuery.path = { $in: [ROOT_FOLDER_PATH, null, undefined] } + } else if (normalizedPath) { + secretQuery.path = normalizedPath + } + if (tagIds.length > 0) { secretQuery.tags = { $in: tagIds }; } @@ -638,6 +657,13 @@ export const getSecrets = async (req: Request, res: Response) => { ] } + // TODO: check if user can query for given path + if (normalizedPath == ROOT_FOLDER_PATH) { + secretQuery.path = { $in: [ROOT_FOLDER_PATH, null, undefined] } + } else if (normalizedPath) { + secretQuery.path = normalizedPath + } + if (tagIds.length > 0) { secretQuery.tags = { $in: tagIds }; } @@ -655,6 +681,12 @@ export const getSecrets = async (req: Request, res: Response) => { user: { $exists: false } // shared secrets only from workspace } + if (normalizedPath == ROOT_FOLDER_PATH) { + secretQuery.path = { $in: [ROOT_FOLDER_PATH, null, undefined] } + } else if (normalizedPath) { + secretQuery.path = normalizedPath + } + if (tagIds.length > 0) { secretQuery.tags = { $in: tagIds }; } @@ -683,7 +715,7 @@ export const getSecrets = async (req: Request, res: Response) => { ipAddress: req.ip }); - const postHogClient = TelemetryService.getPostHogClient(); + const postHogClient = await TelemetryService.getPostHogClient(); if (postHogClient) { postHogClient.capture({ event: 'secrets pulled', @@ -701,7 +733,8 @@ export const getSecrets = async (req: Request, res: Response) => { } return res.status(200).send({ - secrets + secrets, + folders }); } @@ -905,7 +938,7 @@ export const updateSecrets = async (req: Request, res: Response) => { workspaceId: new Types.ObjectId(key) }) - const postHogClient = TelemetryService.getPostHogClient(); + const postHogClient = await TelemetryService.getPostHogClient(); if (postHogClient) { postHogClient.capture({ event: 'secrets modified', @@ -1039,7 +1072,7 @@ export const deleteSecrets = async (req: Request, res: Response) => { workspaceId: new Types.ObjectId(key) }); - const postHogClient = TelemetryService.getPostHogClient(); + const postHogClient = await TelemetryService.getPostHogClient(); if (postHogClient) { postHogClient.capture({ event: 'secrets deleted', diff --git a/backend/src/controllers/v2/serviceAccountsController.ts b/backend/src/controllers/v2/serviceAccountsController.ts index 7eaf73cea..d0ec62e1b 100644 --- a/backend/src/controllers/v2/serviceAccountsController.ts +++ b/backend/src/controllers/v2/serviceAccountsController.ts @@ -72,7 +72,7 @@ export const createServiceAccount = async (req: Request, res: Response) => { } const secret = crypto.randomBytes(16).toString('base64'); - const secretHash = await bcrypt.hash(secret, getSaltRounds()); + const secretHash = await bcrypt.hash(secret, await getSaltRounds()); // create service account const serviceAccount = await new ServiceAccount({ diff --git a/backend/src/controllers/v2/serviceTokenDataController.ts b/backend/src/controllers/v2/serviceTokenDataController.ts index 332e7e34a..597548f2f 100644 --- a/backend/src/controllers/v2/serviceTokenDataController.ts +++ b/backend/src/controllers/v2/serviceTokenDataController.ts @@ -84,7 +84,7 @@ export const createServiceTokenData = async (req: Request, res: Response) => { } = req.body; const secret = crypto.randomBytes(16).toString('hex'); - const secretHash = await bcrypt.hash(secret, getSaltRounds()); + const secretHash = await bcrypt.hash(secret, await getSaltRounds()); let expiresAt; if (expiresIn) { diff --git a/backend/src/controllers/v2/signupController.ts b/backend/src/controllers/v2/signupController.ts index ff41aa017..7cfb3e454 100644 --- a/backend/src/controllers/v2/signupController.ts +++ b/backend/src/controllers/v2/signupController.ts @@ -108,7 +108,7 @@ export const completeAccountSignup = async (req: Request, res: Response) => { token = tokens.token; // sending a welcome email to new users - if (getLoopsApiKey()) { + if (await getLoopsApiKey()) { await request.post("https://app.loops.so/api/v1/events/send", { "email": email, "eventName": "Sign Up", @@ -117,7 +117,7 @@ export const completeAccountSignup = async (req: Request, res: Response) => { }, { headers: { "Accept": "application/json", - "Authorization": "Bearer " + getLoopsApiKey() + "Authorization": "Bearer " + (await getLoopsApiKey()) }, }); } @@ -127,7 +127,7 @@ export const completeAccountSignup = async (req: Request, res: Response) => { httpOnly: true, path: '/', sameSite: 'strict', - secure: getHttpsEnabled() + secure: await getHttpsEnabled() }); } catch (err) { Sentry.setUser(null); @@ -232,7 +232,7 @@ export const completeAccountInvite = async (req: Request, res: Response) => { httpOnly: true, path: '/', sameSite: 'strict', - secure: getHttpsEnabled() + secure: await getHttpsEnabled() }); } catch (err) { Sentry.setUser(null); diff --git a/backend/src/controllers/v2/workspaceController.ts b/backend/src/controllers/v2/workspaceController.ts index dd0efe91f..ea673d428 100644 --- a/backend/src/controllers/v2/workspaceController.ts +++ b/backend/src/controllers/v2/workspaceController.ts @@ -48,7 +48,7 @@ interface V2PushSecret { export const pushWorkspaceSecrets = async (req: Request, res: Response) => { // upload (encrypted) secrets to workspace with id [workspaceId] try { - const postHogClient = TelemetryService.getPostHogClient(); + const postHogClient = await TelemetryService.getPostHogClient(); let { secrets }: { secrets: V2PushSecret[] } = req.body; const { keys, environment, channel } = req.body; const { workspaceId } = req.params; @@ -123,7 +123,7 @@ export const pushWorkspaceSecrets = async (req: Request, res: Response) => { export const pullSecrets = async (req: Request, res: Response) => { let secrets; try { - const postHogClient = TelemetryService.getPostHogClient(); + const postHogClient = await TelemetryService.getPostHogClient(); const environment: string = req.query.environment as string; const channel: string = req.query.channel as string; const { workspaceId } = req.params; diff --git a/backend/src/ee/controllers/v1/stripeController.ts b/backend/src/ee/controllers/v1/stripeController.ts index 3caa0f395..69858c94f 100644 --- a/backend/src/ee/controllers/v1/stripeController.ts +++ b/backend/src/ee/controllers/v1/stripeController.ts @@ -12,7 +12,7 @@ import { getStripeSecretKey, getStripeWebhookSecret } from '../../../config'; export const handleWebhook = async (req: Request, res: Response) => { let event; try { - const stripe = new Stripe(getStripeSecretKey(), { + const stripe = new Stripe(await getStripeSecretKey(), { apiVersion: '2022-08-01' }); @@ -21,7 +21,7 @@ export const handleWebhook = async (req: Request, res: Response) => { event = stripe.webhooks.constructEvent( req.body, sig, - getStripeWebhookSecret() + await getStripeWebhookSecret() ); } catch (err) { Sentry.setUser({ email: req.user.email }); diff --git a/backend/src/helpers/auth.ts b/backend/src/helpers/auth.ts index 47b1ef9b1..fcc3ba8ff 100644 --- a/backend/src/helpers/auth.ts +++ b/backend/src/helpers/auth.ts @@ -104,7 +104,7 @@ const getAuthUserPayload = async ({ authTokenValue: string; }) => { const decodedToken = ( - jwt.verify(authTokenValue, getJwtAuthSecret()) + jwt.verify(authTokenValue, await getJwtAuthSecret()) ); const user = await User.findOne({ @@ -263,16 +263,16 @@ const issueAuthTokens = async ({ userId }: { userId: string }) => { payload: { userId }, - expiresIn: getJwtAuthLifetime(), - secret: getJwtAuthSecret() + expiresIn: await getJwtAuthLifetime(), + secret: await getJwtAuthSecret() }); const refreshToken = createToken({ payload: { userId }, - expiresIn: getJwtRefreshLifetime(), - secret: getJwtRefreshSecret() + expiresIn: await getJwtRefreshLifetime(), + secret: await getJwtRefreshSecret() }); return { diff --git a/backend/src/helpers/bot.ts b/backend/src/helpers/bot.ts index ff55a5e2c..db022c42f 100644 --- a/backend/src/helpers/bot.ts +++ b/backend/src/helpers/bot.ts @@ -119,7 +119,7 @@ const createBot = async ({ const { publicKey, privateKey } = generateKeyPair(); const { ciphertext, iv, tag } = encryptSymmetric({ plaintext: privateKey, - key: getEncryptionKey() + key: await getEncryptionKey() }); bot = await new Bot({ @@ -216,7 +216,7 @@ const getKey = async ({ workspaceId }: { workspaceId: Types.ObjectId }) => { ciphertext: bot.encryptedPrivateKey, iv: bot.iv, tag: bot.tag, - key: getEncryptionKey() + key: await getEncryptionKey() }); key = decryptAsymmetric({ diff --git a/backend/src/helpers/database.ts b/backend/src/helpers/database.ts index aa29f7e46..4bfaf1305 100644 --- a/backend/src/helpers/database.ts +++ b/backend/src/helpers/database.ts @@ -20,12 +20,12 @@ const initDatabaseHelper = async ({ // allow empty strings to pass the required validator mongoose.Schema.Types.String.checkRequired(v => typeof v === 'string'); - getLogger("database").info("Database connection established"); + (await getLogger("database")).info("Database connection established"); await EESecretService.initSecretVersioning(); await SecretService.initSecretBlindIndexDataHelper(); } catch (err) { - getLogger("database").error(`Unable to establish Database connection due to the error.\n${err}`); + (await getLogger("database")).error(`Unable to establish Database connection due to the error.\n${err}`); } return mongoose.connection; diff --git a/backend/src/helpers/nodemailer.ts b/backend/src/helpers/nodemailer.ts index fe7c05044..386db2c39 100644 --- a/backend/src/helpers/nodemailer.ts +++ b/backend/src/helpers/nodemailer.ts @@ -25,7 +25,7 @@ const sendMail = async ({ recipients: string[]; substitutions: any; }) => { - if (getSmtpConfigured()) { + if (await getSmtpConfigured()) { try { const html = fs.readFileSync( path.resolve(__dirname, '../templates/' + template), @@ -35,7 +35,7 @@ const sendMail = async ({ const htmlToSend = temp(substitutions); await smtpTransporter.sendMail({ - from: `"${getSmtpFromName()}" <${getSmtpFromAddress()}>`, + from: `"${await getSmtpFromName()}" <${await getSmtpFromAddress()}>`, to: recipients.join(', '), subject: subjectLine, html: htmlToSend diff --git a/backend/src/helpers/organization.ts b/backend/src/helpers/organization.ts index 9840c9075..ee52bac8e 100644 --- a/backend/src/helpers/organization.ts +++ b/backend/src/helpers/organization.ts @@ -123,11 +123,11 @@ const createOrganization = async ({ let organization; try { // register stripe account - const stripe = new Stripe(getStripeSecretKey(), { + const stripe = new Stripe(await getStripeSecretKey(), { apiVersion: '2022-08-01' }); - if (getStripeSecretKey()) { + if (await getStripeSecretKey()) { const customer = await stripe.customers.create({ email, description: name @@ -177,14 +177,14 @@ const initSubscriptionOrg = async ({ if (organization) { if (organization.customerId) { // initialize starter subscription with quantity of 0 - const stripe = new Stripe(getStripeSecretKey(), { + const stripe = new Stripe(await getStripeSecretKey(), { apiVersion: '2022-08-01' }); const productToPriceMap = { - starter: getStripeProductStarter(), - team: getStripeProductTeam(), - pro: getStripeProductPro() + starter: await getStripeProductStarter(), + team: await getStripeProductTeam(), + pro: await getStripeProductPro() }; stripeSubscription = await stripe.subscriptions.create({ @@ -239,7 +239,7 @@ const updateSubscriptionOrgQuantity = async ({ status: ACCEPTED }); - const stripe = new Stripe(getStripeSecretKey(), { + const stripe = new Stripe(await getStripeSecretKey(), { apiVersion: '2022-08-01' }); diff --git a/backend/src/helpers/secrets.ts b/backend/src/helpers/secrets.ts index 6e33b187d..c184bad9c 100644 --- a/backend/src/helpers/secrets.ts +++ b/backend/src/helpers/secrets.ts @@ -233,27 +233,29 @@ const initSecretBlindIndexDataHelper = async () => { } }); - const secretBlindIndexDataToInsert = workspaceIdsToBlindIndex.map((workspaceToBlindIndex) => { - const salt = crypto.randomBytes(16).toString('base64'); + const secretBlindIndexDataToInsert = await Promise.all( + workspaceIdsToBlindIndex.map(async (workspaceToBlindIndex) => { + const salt = crypto.randomBytes(16).toString('base64'); - const { - ciphertext: encryptedSaltCiphertext, - iv: saltIV, - tag: saltTag - } = encryptSymmetric({ - plaintext: salt, - key: getEncryptionKey() - }); + const { + ciphertext: encryptedSaltCiphertext, + iv: saltIV, + tag: saltTag + } = encryptSymmetric({ + plaintext: salt, + key: await getEncryptionKey() + }); - const secretBlindIndexData = new SecretBlindIndexData({ - workspace: workspaceToBlindIndex, - encryptedSaltCiphertext, - saltIV, - saltTag + const secretBlindIndexData = new SecretBlindIndexData({ + workspace: workspaceToBlindIndex, + encryptedSaltCiphertext, + saltIV, + saltTag + }) + + return secretBlindIndexData; }) - - return secretBlindIndexData; - }); + ); if (secretBlindIndexDataToInsert.length > 0) { await SecretBlindIndexData.insertMany(secretBlindIndexDataToInsert); @@ -280,7 +282,7 @@ const createSecretBlindIndexDataHelper = async ({ tag: saltTag } = encryptSymmetric({ plaintext: salt, - key: getEncryptionKey() + key: await getEncryptionKey() }); const secretBlindIndexData = await new SecretBlindIndexData({ @@ -316,7 +318,7 @@ const getSecretBlindIndexSaltHelper = async ({ ciphertext: secretBlindIndexData.encryptedSaltCiphertext, iv: secretBlindIndexData.saltIV, tag: secretBlindIndexData.saltTag, - key: getEncryptionKey() + key: await getEncryptionKey() }); return salt; @@ -378,7 +380,7 @@ const generateSecretBlindIndexHelper = async ({ ciphertext: secretBlindIndexData.encryptedSaltCiphertext, iv: secretBlindIndexData.saltIV, tag: secretBlindIndexData.saltTag, - key: getEncryptionKey() + key: await getEncryptionKey() }); const secretBlindIndex = await generateSecretBlindIndexWithSaltHelper({ @@ -508,7 +510,7 @@ const createSecretHelper = async ({ workspaceId }); - const postHogClient = TelemetryService.getPostHogClient(); + const postHogClient = await TelemetryService.getPostHogClient(); if (postHogClient) { postHogClient.capture({ @@ -578,7 +580,7 @@ const getSecretsHelper = async ({ ipAddress: authData.authIP }); - const postHogClient = TelemetryService.getPostHogClient(); + const postHogClient = await TelemetryService.getPostHogClient(); if (postHogClient) { postHogClient.capture({ @@ -660,7 +662,7 @@ const getSecretHelper = async ({ ipAddress: authData.authIP }); - const postHogClient = TelemetryService.getPostHogClient(); + const postHogClient = await TelemetryService.getPostHogClient(); if (postHogClient) { postHogClient.capture({ @@ -798,7 +800,7 @@ const updateSecretHelper = async ({ workspaceId }); - const postHogClient = TelemetryService.getPostHogClient(); + const postHogClient = await TelemetryService.getPostHogClient(); if (postHogClient) { postHogClient.capture({ @@ -905,7 +907,7 @@ const deleteSecretHelper = async ({ workspaceId }); - const postHogClient = TelemetryService.getPostHogClient(); + const postHogClient = await TelemetryService.getPostHogClient(); if (postHogClient) { postHogClient.capture({ diff --git a/backend/src/helpers/token.ts b/backend/src/helpers/token.ts index 0f6a88cc9..8fcbb1dd6 100644 --- a/backend/src/helpers/token.ts +++ b/backend/src/helpers/token.ts @@ -84,7 +84,7 @@ const createTokenHelper = async ({ const query: TokenDataQuery = { type }; const update: TokenDataUpdate = { type, - tokenHash: await bcrypt.hash(token, getSaltRounds()), + tokenHash: await bcrypt.hash(token, await getSaltRounds()), expiresAt } diff --git a/backend/src/helpers/workspace.ts b/backend/src/helpers/workspace.ts index bb002176b..5047a1967 100644 --- a/backend/src/helpers/workspace.ts +++ b/backend/src/helpers/workspace.ts @@ -28,7 +28,6 @@ import { AUTH_MODE_SERVICE_TOKEN, AUTH_MODE_API_KEY } from '../variables'; -import { getEncryptionKey } from '../config'; import { encryptSymmetric } from '../utils/crypto'; import { SecretService } from '../services'; diff --git a/backend/src/index.ts b/backend/src/index.ts index 3b3091f92..a250fc6bc 100644 --- a/backend/src/index.ts +++ b/backend/src/index.ts @@ -1,6 +1,5 @@ import dotenv from 'dotenv'; dotenv.config(); -import infisical from 'infisical-node'; import express from 'express'; import helmet from 'helmet'; import cors from 'cors'; @@ -45,7 +44,8 @@ import { password as v1PasswordRouter, stripe as v1StripeRouter, integration as v1IntegrationRouter, - integrationAuth as v1IntegrationAuthRouter + integrationAuth as v1IntegrationAuthRouter, + secretsFolder as v1SecretsFolder } from './routes/v1'; import { signup as v2SignupRouter, @@ -80,22 +80,16 @@ import { } from './config'; const main = async () => { - if (process.env.INFISICAL_TOKEN != "" || process.env.INFISICAL_TOKEN != undefined) { - await infisical.connect({ - token: process.env.INFISICAL_TOKEN! - }); - } - TelemetryService.logTelemetryMessage(); - setTransporter(initSmtp()); + setTransporter(await initSmtp()); - await DatabaseService.initDatabase(getMongoURL()); - if (getNodeEnv() !== 'test') { + await DatabaseService.initDatabase(await getMongoURL()); + if ((await getNodeEnv()) !== 'test') { Sentry.init({ - dsn: getSentryDSN(), + dsn: await getSentryDSN(), tracesSampleRate: 1.0, - debug: getNodeEnv() === 'production' ? false : true, - environment: getNodeEnv() + debug: await getNodeEnv() === 'production' ? false : true, + environment: await getNodeEnv() }); } @@ -107,7 +101,7 @@ const main = async () => { app.use( cors({ credentials: true, - origin: getSiteURL() + origin: await getSiteURL() }) ); @@ -118,7 +112,7 @@ const main = async () => { saveUninitialized: false, // don't create session until something stored })); - if (getNodeEnv() === 'production') { + if ((await getNodeEnv()) === 'production') { // enable app-wide rate-limiting + helmet security // in production app.disable('x-powered-by'); @@ -150,6 +144,7 @@ const main = async () => { app.use('/api/v1/stripe', v1StripeRouter); app.use('/api/v1/integration', v1IntegrationRouter); app.use('/api/v1/integration-auth', v1IntegrationAuthRouter); + app.use('/api/v1/folder', v1SecretsFolder) // v2 routes (improvements) app.use('/api/v2/signup', v2SignupRouter); @@ -184,8 +179,8 @@ const main = async () => { app.use(requestErrorHandler) - const server = app.listen(getPort(), () => { - getLogger("backend-main").info(`Server started listening at port ${getPort()}`) + const server = app.listen(await getPort(), async () => { + (await getLogger("backend-main")).info(`Server started listening at port ${await getPort()}`) }); await createTestUserForDevelopment(); diff --git a/backend/src/integrations/exchange.ts b/backend/src/integrations/exchange.ts index 1dccb03d0..04dc96ca0 100644 --- a/backend/src/integrations/exchange.ts +++ b/backend/src/integrations/exchange.ts @@ -159,9 +159,9 @@ const exchangeCodeAzure = async ({ grant_type: 'authorization_code', code: code, scope: 'https://vault.azure.net/.default openid offline_access', - client_id: getClientIdAzure(), - client_secret: getClientSecretAzure(), - redirect_uri: `${getSiteURL()}/integrations/azure-key-vault/oauth2/callback` + client_id: await getClientIdAzure(), + client_secret: await getClientSecretAzure(), + redirect_uri: `${await getSiteURL()}/integrations/azure-key-vault/oauth2/callback` } as any) )).data; @@ -204,7 +204,7 @@ const exchangeCodeHeroku = async ({ new URLSearchParams({ grant_type: 'authorization_code', code: code, - client_secret: getClientSecretHeroku() + client_secret: await getClientSecretHeroku() } as any) )).data; @@ -242,9 +242,9 @@ const exchangeCodeVercel = async ({ code }: { code: string }) => { INTEGRATION_VERCEL_TOKEN_URL, new URLSearchParams({ code: code, - client_id: getClientIdVercel(), - client_secret: getClientSecretVercel(), - redirect_uri: `${getSiteURL()}/integrations/vercel/oauth2/callback` + client_id: await getClientIdVercel(), + client_secret: await getClientSecretVercel(), + redirect_uri: `${await getSiteURL()}/integrations/vercel/oauth2/callback` } as any) ) ).data; @@ -282,9 +282,9 @@ const exchangeCodeNetlify = async ({ code }: { code: string }) => { new URLSearchParams({ grant_type: 'authorization_code', code: code, - client_id: getClientIdNetlify(), - client_secret: getClientSecretNetlify(), - redirect_uri: `${getSiteURL()}/integrations/netlify/oauth2/callback` + client_id: await getClientIdNetlify(), + client_secret: await getClientSecretNetlify(), + redirect_uri: `${await getSiteURL()}/integrations/netlify/oauth2/callback` } as any) ) ).data; @@ -333,10 +333,10 @@ const exchangeCodeGithub = async ({ code }: { code: string }) => { res = ( await request.get(INTEGRATION_GITHUB_TOKEN_URL, { params: { - client_id: getClientIdGitHub(), - client_secret: getClientSecretGitHub(), + client_id: await getClientIdGitHub(), + client_secret: await getClientSecretGitHub(), code: code, - redirect_uri: `${getSiteURL()}/integrations/github/oauth2/callback` + redirect_uri: `${await getSiteURL()}/integrations/github/oauth2/callback` }, headers: { 'Accept': 'application/json', @@ -379,9 +379,9 @@ const exchangeCodeGitlab = async ({ code }: { code: string }) => { new URLSearchParams({ grant_type: 'authorization_code', code: code, - client_id: getClientIdGitLab(), - client_secret: getClientSecretGitLab(), - redirect_uri: `${getSiteURL()}/integrations/gitlab/oauth2/callback` + client_id: await getClientIdGitLab(), + client_secret: await getClientSecretGitLab(), + redirect_uri: `${await getSiteURL()}/integrations/gitlab/oauth2/callback` } as any), { headers: { diff --git a/backend/src/integrations/refresh.ts b/backend/src/integrations/refresh.ts index a0aea080e..79b89ecfb 100644 --- a/backend/src/integrations/refresh.ts +++ b/backend/src/integrations/refresh.ts @@ -133,11 +133,11 @@ const exchangeRefreshAzure = async ({ const { data }: { data: RefreshTokenAzureResponse } = await request.post( INTEGRATION_AZURE_TOKEN_URL, new URLSearchParams({ - client_id: getClientIdAzure(), + client_id: await getClientIdAzure(), scope: 'openid offline_access', refresh_token: refreshToken, grant_type: 'refresh_token', - client_secret: getClientSecretAzure() + client_secret: await getClientSecretAzure() } as any) ); @@ -180,7 +180,7 @@ const exchangeRefreshHeroku = async ({ new URLSearchParams({ grant_type: 'refresh_token', refresh_token: refreshToken, - client_secret: getClientSecretHeroku() + client_secret: await getClientSecretHeroku() } as any) ); @@ -223,9 +223,9 @@ const exchangeRefreshGitLab = async ({ new URLSearchParams({ grant_type: 'refresh_token', refresh_token: refreshToken, - client_id: getClientIdGitLab, - client_secret: getClientSecretGitLab(), - redirect_uri: `${getSiteURL()}/integrations/gitlab/oauth2/callback` + client_id: await getClientIdGitLab, + client_secret: await getClientSecretGitLab(), + redirect_uri: `${await getSiteURL()}/integrations/gitlab/oauth2/callback` } as any), { headers: { diff --git a/backend/src/middleware/requestErrorHandler.ts b/backend/src/middleware/requestErrorHandler.ts index f42b2dd74..1f1e9f9b2 100644 --- a/backend/src/middleware/requestErrorHandler.ts +++ b/backend/src/middleware/requestErrorHandler.ts @@ -5,9 +5,9 @@ import { getLogger } from "../utils/logger"; import RequestError, { LogLevel } from "../utils/requestError"; import { getNodeEnv } from '../config'; -export const requestErrorHandler: ErrorRequestHandler = (error: RequestError | Error, req, res, next) => { +export const requestErrorHandler: ErrorRequestHandler = async (error: RequestError | Error, req, res, next) => { if (res.headersSent) return next(); - if (getNodeEnv() !== "production") { + if ((await getNodeEnv()) !== "production") { /* eslint-disable no-console */ console.log(error) /* eslint-enable no-console */ @@ -15,8 +15,8 @@ export const requestErrorHandler: ErrorRequestHandler = (error: RequestError | E //TODO: Find better way to type check for error. In current setting you need to cast type to get the functions and variables from RequestError if (!(error instanceof RequestError)) { - error = InternalServerError({ context: { exception: error.message }, stack: error.stack }) - getLogger('backend-main').log((error).levelName.toLowerCase(), (error).message) + error = InternalServerError({ context: { exception: error.message }, stack: error.stack }); + (await getLogger('backend-main')).log((error).levelName.toLowerCase(), (error).message) } //* Set Sentry user identification if req.user is populated diff --git a/backend/src/middleware/requireMfaAuth.ts b/backend/src/middleware/requireMfaAuth.ts index 7fb38ca25..ca0b3434e 100644 --- a/backend/src/middleware/requireMfaAuth.ts +++ b/backend/src/middleware/requireMfaAuth.ts @@ -26,7 +26,7 @@ const requireMfaAuth = async ( if(AUTH_TOKEN_VALUE === null) return next(BadRequestError({message: 'Missing Authorization Body in the request header'})) const decodedToken = ( - jwt.verify(AUTH_TOKEN_VALUE, getJwtMfaSecret()) + jwt.verify(AUTH_TOKEN_VALUE, await getJwtMfaSecret()) ); const user = await User.findOne({ diff --git a/backend/src/middleware/requireServiceTokenAuth.ts b/backend/src/middleware/requireServiceTokenAuth.ts index 106ca9bbb..5db0dcda5 100644 --- a/backend/src/middleware/requireServiceTokenAuth.ts +++ b/backend/src/middleware/requireServiceTokenAuth.ts @@ -33,7 +33,7 @@ const requireServiceTokenAuth = async ( if(AUTH_TOKEN_VALUE === null) return next(BadRequestError({message: 'Missing Authorization Body in the request header'})) const decodedToken = ( - jwt.verify(AUTH_TOKEN_VALUE, getJwtServiceSecret()) + jwt.verify(AUTH_TOKEN_VALUE, await getJwtServiceSecret()) ); const serviceToken = await ServiceToken.findOne({ diff --git a/backend/src/middleware/requireSignupAuth.ts b/backend/src/middleware/requireSignupAuth.ts index 19e6b3146..6a0fd0b6e 100644 --- a/backend/src/middleware/requireSignupAuth.ts +++ b/backend/src/middleware/requireSignupAuth.ts @@ -27,7 +27,7 @@ const requireSignupAuth = async ( if(AUTH_TOKEN_VALUE === null) return next(BadRequestError({message: 'Missing Authorization Body in the request header'})) const decodedToken = ( - jwt.verify(AUTH_TOKEN_VALUE, getJwtSignupSecret()) + jwt.verify(AUTH_TOKEN_VALUE, await getJwtSignupSecret()) ); const user = await User.findOne({ diff --git a/backend/src/models/folder.ts b/backend/src/models/folder.ts new file mode 100644 index 000000000..885e320a8 --- /dev/null +++ b/backend/src/models/folder.ts @@ -0,0 +1,36 @@ +import { Schema, Types, model } from 'mongoose'; + +const folderSchema = new Schema({ + name: { + type: String, + required: true, + }, + workspace: { + type: Schema.Types.ObjectId, + ref: 'Workspace', + required: true, + }, + environment: { + type: String, + required: true, + }, + parent: { + type: Schema.Types.ObjectId, + ref: 'Folder', + required: false, // optional for root folders + }, + path: { + type: String, + required: true + }, + parentPath: { + type: String, + required: true, + }, +}, { + timestamps: true +}); + +const Folder = model('Folder', folderSchema); + +export default Folder; \ No newline at end of file diff --git a/backend/src/models/secret.ts b/backend/src/models/secret.ts index 725741ccf..7670124bf 100644 --- a/backend/src/models/secret.ts +++ b/backend/src/models/secret.ts @@ -3,6 +3,7 @@ import { SECRET_SHARED, SECRET_PERSONAL, } from '../variables'; +import { ROOT_FOLDER_PATH } from '../utils/folder'; export interface ISecret { _id: Types.ObjectId; @@ -25,6 +26,8 @@ export interface ISecret { secretCommentTag?: string; secretCommentHash?: string; tags?: string[]; + path?: string; + folder?: Types.ObjectId; } const secretSchema = new Schema( @@ -107,7 +110,18 @@ const secretSchema = new Schema( secretCommentHash: { type: String, required: false - } + }, + // the full path to the secret in relation to folders + path: { + type: String, + required: false, + default: ROOT_FOLDER_PATH + }, + folder: { + type: Schema.Types.ObjectId, + ref: 'Folder', + required: false, + }, }, { timestamps: true diff --git a/backend/src/routes/status/status.ts b/backend/src/routes/status/status.ts index f793128be..91d0c9e85 100644 --- a/backend/src/routes/status/status.ts +++ b/backend/src/routes/status/status.ts @@ -5,11 +5,11 @@ const router = express.Router(); router.get( '/status', - (req: Request, res: Response) => { + async (req: Request, res: Response) => { res.status(200).json({ date: new Date(), message: 'Ok', - emailConfigured: getSmtpConfigured() + emailConfigured: await getSmtpConfigured() }) } ); diff --git a/backend/src/routes/v1/index.ts b/backend/src/routes/v1/index.ts index 2dfe58baa..4b9dcafca 100644 --- a/backend/src/routes/v1/index.ts +++ b/backend/src/routes/v1/index.ts @@ -15,6 +15,7 @@ import password from './password'; import stripe from './stripe'; import integration from './integration'; import integrationAuth from './integrationAuth'; +import secretsFolder from './secretsFolder' export { signup, @@ -33,5 +34,6 @@ export { password, stripe, integration, - integrationAuth + integrationAuth, + secretsFolder }; diff --git a/backend/src/routes/v1/secretsFolder.ts b/backend/src/routes/v1/secretsFolder.ts new file mode 100644 index 000000000..7f2cd1bc8 --- /dev/null +++ b/backend/src/routes/v1/secretsFolder.ts @@ -0,0 +1,40 @@ +import express, { Request, Response } from 'express'; +const router = express.Router(); +import { + requireAuth, + requireWorkspaceAuth, + validateRequest +} from '../../middleware'; +import { body, param } from 'express-validator'; +import { createFolder, deleteFolder } from '../../controllers/v1/secretsFolderController'; +import { ADMIN, MEMBER } from '../../variables'; + +router.post( + '/', + requireAuth({ + acceptedAuthModes: ['jwt'] + }), + requireWorkspaceAuth({ + acceptedRoles: [ADMIN, MEMBER], + locationWorkspaceId: 'body' + }), + body('workspaceId').exists(), + body('environment').exists(), + body('folderName').exists(), + body('parentFolderId'), + validateRequest, + createFolder +); + +router.delete( + '/:folderId', + requireAuth({ + acceptedAuthModes: ['jwt'] + }), + param('folderId').exists(), + validateRequest, + deleteFolder +); + + +export default router; \ No newline at end of file diff --git a/backend/src/services/DatabaseService.ts b/backend/src/services/DatabaseService.ts index fdfd7660a..616f56c47 100644 --- a/backend/src/services/DatabaseService.ts +++ b/backend/src/services/DatabaseService.ts @@ -1,5 +1,3 @@ -import mongoose from 'mongoose'; -import { getLogger } from '../utils/logger'; import { initDatabaseHelper, closeDatabaseHelper diff --git a/backend/src/services/TelemetryService.ts b/backend/src/services/TelemetryService.ts index 578b5292d..da90732eb 100644 --- a/backend/src/services/TelemetryService.ts +++ b/backend/src/services/TelemetryService.ts @@ -24,9 +24,9 @@ class Telemetry { /** * Logs telemetry enable/disable notice. */ - static logTelemetryMessage = () => { - if(!getTelemetryEnabled()){ - getLogger("backend-main").info([ + static logTelemetryMessage = async () => { + if(!(await getTelemetryEnabled())){ + (await getLogger("backend-main")).info([ "", "To improve, Infisical collects telemetry data about general usage.", "This helps us understand how the product is doing and guide our product development to create the best possible platform; it also helps us demonstrate growth as we support Infisical as open-source software.", @@ -39,12 +39,12 @@ class Telemetry { * Return an instance of the PostHog client initialized. * @returns */ - static getPostHogClient = () => { + static getPostHogClient = async () => { let postHogClient: any; - if (getNodeEnv() === 'production' && getTelemetryEnabled()) { + if ((await getNodeEnv()) === 'production' && (await getTelemetryEnabled())) { // case: enable opt-out telemetry in production - postHogClient = new PostHog(getPostHogProjectApiKey(), { - host: getPostHogHost() + postHogClient = new PostHog(await getPostHogProjectApiKey(), { + host: await getPostHogHost() }); } diff --git a/backend/src/services/health.ts b/backend/src/services/health.ts index 9c441ba9d..daf3bf962 100644 --- a/backend/src/services/health.ts +++ b/backend/src/services/health.ts @@ -3,8 +3,8 @@ import { createTerminus } from '@godaddy/terminus'; import { getLogger } from '../utils/logger'; export const setUpHealthEndpoint = (server: T) => { - const onSignal = () => { - getLogger('backend-main').info('Server is starting clean-up'); + const onSignal = async () => { + (await getLogger('backend-main')).info('Server is starting clean-up'); return Promise.all([ new Promise((resolve) => { if (mongoose.connection && mongoose.connection.readyState == 1) { diff --git a/backend/src/services/smtp.ts b/backend/src/services/smtp.ts index b30a43447..6bae626be 100644 --- a/backend/src/services/smtp.ts +++ b/backend/src/services/smtp.ts @@ -15,21 +15,21 @@ import { getSmtpPort } from '../config'; -export const initSmtp = () => { +export const initSmtp = async () => { const mailOpts: SMTPConnection.Options = { - host: getSmtpHost(), - port: getSmtpPort() + host: await getSmtpHost(), + port: await getSmtpPort() }; - if (getSmtpUsername() && getSmtpPassword()) { + if ((await getSmtpUsername()) && (await getSmtpPassword())) { mailOpts.auth = { - user: getSmtpUsername(), - pass: getSmtpPassword() + user: await getSmtpUsername(), + pass: await getSmtpPassword() }; } - if (getSmtpSecure() ? getSmtpSecure() : false) { - switch (getSmtpHost()) { + if ((await getSmtpSecure()) ? (await getSmtpSecure()) : false) { + switch (await getSmtpHost()) { case SMTP_HOST_SENDGRID: mailOpts.requireTLS = true; break; @@ -52,7 +52,7 @@ export const initSmtp = () => { } break; default: - if (getSmtpHost().includes('amazonaws.com')) { + if ((await getSmtpHost()).includes('amazonaws.com')) { mailOpts.tls = { ciphers: 'TLSv1.2' } @@ -70,10 +70,10 @@ export const initSmtp = () => { Sentry.setUser(null); Sentry.captureMessage('SMTP - Successfully connected'); }) - .catch((err) => { + .catch(async (err) => { Sentry.setUser(null); Sentry.captureException( - `SMTP - Failed to connect to ${getSmtpHost()}:${getSmtpPort()} \n\t${err}` + `SMTP - Failed to connect to ${await getSmtpHost()}:${await getSmtpPort()} \n\t${err}` ); }); diff --git a/backend/src/utils/addDevelopmentUser.ts b/backend/src/utils/addDevelopmentUser.ts index 136d91a98..52aedcdd2 100644 --- a/backend/src/utils/addDevelopmentUser.ts +++ b/backend/src/utils/addDevelopmentUser.ts @@ -19,7 +19,7 @@ export const testWorkspaceKeyId = "63cf48f0225e6955acec5eff" export const plainTextWorkspaceKey = "543fef8224813a46230b0a50a46c5fb2" export const createTestUserForDevelopment = async () => { - if (getNodeEnv() === "development" || getNodeEnv() === "test") { + if ((await getNodeEnv()) === "development" || (await getNodeEnv()) === "test") { const testUser = { _id: testUserId, email: testUserEmail, diff --git a/backend/src/utils/folder.ts b/backend/src/utils/folder.ts new file mode 100644 index 000000000..f12845339 --- /dev/null +++ b/backend/src/utils/folder.ts @@ -0,0 +1,87 @@ +import Folder from "../models/folder"; + +export const ROOT_FOLDER_PATH = "/" + +export const getFolderPath = async (folderId: string) => { + let currentFolder = await Folder.findById(folderId); + const pathSegments = []; + + while (currentFolder) { + pathSegments.unshift(currentFolder.name); + currentFolder = currentFolder.parent ? await Folder.findById(currentFolder.parent) : null; + } + + return '/' + pathSegments.join('/'); +}; + +/** + Returns the folder ID associated with the specified secret path in the given workspace and environment. + @param workspaceId - The ID of the workspace to search in. + @param environment - The environment to search in. + @param secretPath - The secret path to search for. + @returns The folder ID associated with the specified secret path, or undefined if the path is at the root folder level. + @throws Error if the specified secret path is not found. +*/ +export const getFolderIdFromPath = async (workspaceId: string, environment: string, secretPath: string) => { + const secretPathParts = secretPath.split("/").filter(path => path != "") + if (secretPathParts.length <= 1) { + return undefined // root folder, so no folder id + } + + const folderId = await Folder.find({ path: secretPath, workspace: workspaceId, environment: environment }) + if (!folderId) { + throw Error("Secret path not found") + } + + return folderId +} + +/** + * Cleans up a path by removing empty parts, duplicate slashes, + * and ensuring it starts with ROOT_FOLDER_PATH. + * @param path - The input path to clean up. + * @returns The cleaned-up path string. + */ +export const normalizePath = (path: string) => { + if (path == undefined || path == "" || path == ROOT_FOLDER_PATH) { + return ROOT_FOLDER_PATH + } + + const pathParts = path.split("/").filter(part => part != "") + const cleanPathString = ROOT_FOLDER_PATH + pathParts.join("/") + + return cleanPathString +} + +export const getFoldersInDirectory = async (workspaceId: string, environment: string, pathString: string) => { + const normalizedPath = normalizePath(pathString) + const foldersInDirectory = await Folder.find({ + workspace: workspaceId, + environment: environment, + parentPath: normalizedPath, + }); + + return foldersInDirectory; +} + +/** + * Returns the parent path of the given path. + * @param path - The input path. + * @returns The parent path string. + */ +export const getParentPath = (path: string) => { + const normalizedPath = normalizePath(path); + const folderParts = normalizedPath.split('/').filter(part => part !== ''); + + let folderParent = ROOT_FOLDER_PATH; + if (folderParts.length > 1) { + folderParent = ROOT_FOLDER_PATH + folderParts.slice(0, folderParts.length - 1).join('/'); + } + + return folderParent; +} + +export const validateFolderName = (folderName: string) => { + const validNameRegex = /^[a-zA-Z0-9-_]+$/; + return validNameRegex.test(folderName); +} \ No newline at end of file diff --git a/backend/src/utils/logger.ts b/backend/src/utils/logger.ts index ed29c97ca..15335a619 100644 --- a/backend/src/utils/logger.ts +++ b/backend/src/utils/logger.ts @@ -12,7 +12,7 @@ const logFormat = (prefix: string) => combine( printf((info) => `${info.timestamp} ${info.label} ${info.level}: ${info.message}`) ); -const createLoggerWithLabel = (level: string, label: string) => { +const createLoggerWithLabel = async (level: string, label: string) => { const _level = level.toLowerCase() || 'info' //* Always add Console output to transports const _transports: any[] = [ @@ -25,10 +25,10 @@ const createLoggerWithLabel = (level: string, label: string) => { }) ] //* Add LokiTransport if it's enabled - if(getLokiHost() !== undefined){ + if((await getLokiHost()) !== undefined){ _transports.push( new LokiTransport({ - host: getLokiHost(), + host: await getLokiHost(), handleExceptions: true, handleRejections: true, batching: true, @@ -40,7 +40,7 @@ const createLoggerWithLabel = (level: string, label: string) => { labels: { app: process.env.npm_package_name, version: process.env.npm_package_version, - environment: getNodeEnv() + environment: await getNodeEnv() }, onConnectionError: (err: Error)=> console.error('Connection error while connecting to Loki Server.\n', err) }) @@ -58,12 +58,10 @@ const createLoggerWithLabel = (level: string, label: string) => { }); } -const DEFAULT_LOGGERS = { - "backend-main": createLoggerWithLabel('info', '[IFSC:backend-main]'), - "database": createLoggerWithLabel('info', '[IFSC:database]'), -} -type LoggerNames = keyof typeof DEFAULT_LOGGERS - -export const getLogger = (loggerName: LoggerNames) => { - return DEFAULT_LOGGERS[loggerName] +export const getLogger = async (loggerName: 'backend-main' | 'database') => { + const logger = { + "backend-main": await createLoggerWithLabel('info', '[IFSC:backend-main]'), + "database": await createLoggerWithLabel('info', '[IFSC:database]'), + } + return logger[loggerName] } diff --git a/backend/src/utils/requestError.ts b/backend/src/utils/requestError.ts index 4b5635bac..570ed132e 100644 --- a/backend/src/utils/requestError.ts +++ b/backend/src/utils/requestError.ts @@ -81,13 +81,13 @@ export default class RequestError extends Error{ return obj } - public format(req: Request){ + public async format(req: Request){ let _context = Object.assign({ stacktrace: this.stacktrace }, this.context) //* Omit sensitive information from context that can leak internal workings of this program if user is not developer - if(!getVerboseErrorOutput()){ + if(!(await getVerboseErrorOutput())){ _context = this._omit(_context, [ 'stacktrace', 'exception', diff --git a/backend/src/variables/integration.ts b/backend/src/variables/integration.ts index bbfd7f107..aac78968d 100644 --- a/backend/src/variables/integration.ts +++ b/backend/src/variables/integration.ts @@ -61,7 +61,7 @@ const INTEGRATION_CIRCLECI_API_URL = "https://circleci.com/api"; const INTEGRATION_TRAVISCI_API_URL = "https://api.travis-ci.com"; const INTEGRATION_SUPABASE_API_URL = 'https://api.supabase.com'; -const getIntegrationOptions = () => { +const getIntegrationOptions = async () => { const INTEGRATION_OPTIONS = [ { name: 'Heroku', @@ -69,7 +69,7 @@ const getIntegrationOptions = () => { image: 'Heroku.png', isAvailable: true, type: 'oauth', - clientId: getClientIdHeroku(), + clientId: await getClientIdHeroku(), docsLink: '' }, { @@ -79,7 +79,7 @@ const getIntegrationOptions = () => { isAvailable: true, type: 'oauth', clientId: '', - clientSlug: getClientSlugVercel(), + clientSlug: await getClientSlugVercel(), docsLink: '' }, { @@ -88,7 +88,7 @@ const getIntegrationOptions = () => { image: 'Netlify.png', isAvailable: true, type: 'oauth', - clientId: getClientIdNetlify(), + clientId: await getClientIdNetlify(), docsLink: '' }, { @@ -97,7 +97,7 @@ const getIntegrationOptions = () => { image: 'GitHub.png', isAvailable: true, type: 'oauth', - clientId: getClientIdGitHub(), + clientId: await getClientIdGitHub(), docsLink: '' }, { @@ -151,7 +151,7 @@ const getIntegrationOptions = () => { image: 'Microsoft Azure.png', isAvailable: true, type: 'oauth', - clientId: getClientIdAzure(), + clientId: await getClientIdAzure(), docsLink: '' }, { @@ -169,7 +169,7 @@ const getIntegrationOptions = () => { image: 'GitLab.png', isAvailable: true, type: 'custom', - clientId: getClientIdGitLab(), + clientId: await getClientIdGitLab(), docsLink: '' }, { diff --git a/docs/getting-started/quickstart.mdx b/docs/getting-started/quickstart.mdx index f19bd1120..7fca86d4d 100644 --- a/docs/getting-started/quickstart.mdx +++ b/docs/getting-started/quickstart.mdx @@ -64,7 +64,9 @@ These examples demonstrate how to store and fetch environment variables from [In ### Initialize the Infisical client ```js - await infisical.connect({ + import InfisicalClient from "infisical-node"; + + const client = new InfisicalClient({ token: "your_infisical_token", }); ``` @@ -72,31 +74,31 @@ These examples demonstrate how to store and fetch environment variables from [In ### Get a value ```js - const value = infisical.get("SOME_KEY"); + const value = await client.getSecret("SOME_KEY"); ``` ### Example with Express ```js - const express = require("express"); - const port = 3000; - const infisical = require("infisical-node"); + import InfisicalClient from "infisical-node"; + import express from "express"; + const app = express(); + const PORT = 3000; - const main = async () => { - await infisical.connect({ - token: "st.xxx.xxx", - }); + const client = InfisicalClient({ + token: "st.xxx.xxx", + }); - // your application logic + // your application logic - app.get("/", (req, res) => { - res.send(`Howdy, ${infisical.get("NAME")}!`); - }); + app.get("/", async (req, res) => { + const name = await client.getSecret("NAME"); + res.send(`Hello! My name is: ${name.secretValue}`); + }); - app.listen(port, async () => { - console.log(`App listening on port ${port}`); - }); - }; + app.listen(PORT, async () => { + console.log(`App listening on port ${port}`); + }); ``` diff --git a/docs/sdks/languages/node.mdx b/docs/sdks/languages/node.mdx index 58002392f..4d75168bc 100644 --- a/docs/sdks/languages/node.mdx +++ b/docs/sdks/languages/node.mdx @@ -2,7 +2,39 @@ title: "Node" --- -If you're working with Node.js, the official [infisical-node](https://github.com/Infisical/infisical-node) package is the easiest way to fetch secrets for your application. +If you're working with Node.js, the official [infisical-node](https://github.com/Infisical/infisical-node) package is the easiest way to fetch and work with secrets for your application. + +## Basic Usage + +```js +import InfisicalClient from "infisical-node"; +import express from "express"; +const app = express(); +const PORT = 3000; + +const client = new InfisicalClient({ + token: "YOUR_INFISICAL_TOKEN" +}); + +app.get("/", async (req, res) => { + // access value + const name = await client.getSecret("NAME"); + res.send(`Hello! My name is: ${name.secretValue}`); +}); + +app.listen(PORT, async () => { + // initialize client + console.log(`App listening on port ${port}`); +}); +``` + +This example demonstrates how to use the Infisical SDK with an Express application. The application retrieves a secret named "NAME" and responds to requests with a greeting that includes the secret value. + + + We do not recommend hardcoding your [Infisical + Token](/getting-started/dashboard/token). Setting it as an environment + variable would be best. + ## Installation @@ -12,143 +44,150 @@ Run `npm` to add `infisical-node` to your project. npm install infisical-node --save ``` -## Initialization +## Configuration -Set up the Infisical client asynchronously as early as possible in your application by importing and initializing the global instance with `infisical.connect(options)`. - -This methods fetches back all the secrets in the project and environment accessible by the token passed in `options`. - -### infisical.connect(options) - -Updates the global instance of the Infisical client with a connection to an Infisical project and fetches back secrets if supplied with an [Infisical Token](/getting-started/dashboard/token). - - - - - An [Infisical Token](/getting-started/dashboard/token) scoped to a project - and environment - - - Your self-hosted absolute site URL including the protocol (e.g. - `https://app.infisical.com`) - - - Whether or not debug mode is on - - - Whether or not to attach fetched secrets to `process.env` - - - - -### infisical.createConnection(options) - -Returns a local instance of the Infisical client with a connection to an Infisical project and fetches back secrets if supplied with an [Infisical Token](/getting-started/dashboard/token). - -This method is useful if you wish to connect to two or more Infisical projects within your app. - - - - - An [Infisical Token](/getting-started/dashboard/token) scoped to a project - and environment - - - Your self-hosted absolute site URL including the protocol (e.g. - `https://app.infisical.com`) - - - Whether or not debug mode is on - - - +Import the SDK and create a client instance with your Infisical token. ```js - import infisical from "infisical-node"; + import InfisicalClient from "infisical-node"; + + const client = new InfisicalClient({ + token: "your_infisical_token" + }); - const main = async () => { - await infisical.connect({ - token: "your_infisical_token", - }); - - // your app logic - } - - main(); + // your app logic ``` ```js - const infisical = require("infisical-node"); + const InfisicalClient = require("infisical-node"); - infisical.connect({ - token: "your_infisical_token" - }) - .then(() => { - // your application logic - }) - .catch(err => { - console.error('Error: ', err); - }) + const client = new InfisicalClient({ + token: "your_infisical_token" + }); + + // your app logic ```` -## Usage - -To get the value of a secret, use `infisical.get(key)`. - -### infisical.get(key) - -Return the value of the secret with the specified `key`. Note that the Infisical client falls back to `process.env` if `token` is `undefined` during the -initialization step or if a value for the secret is not found in the fetched secrets. - - - The key of the secret + + + + An [Infisical Token](/getting-started/dashboard/token) scoped to a project + and environment + + + Your self-hosted absolute site URL including the protocol (e.g. + `https://app.infisical.com`) + + + Time-to-live (in seconds) for refreshing cached secrets. Default: `300`. + + + Whether or not debug mode is on + + -```js -const value = infisical.get("SOME_KEY"); -``` +## Caching -## Example with Express +The SDK caches every secret and updates it periodically based on the provided `cacheTTL`. For example, if `cacheTTL` of `300` is provided, then a secret will be refetched 5 minutes after the first fetch; if the fetch fails, the cached secret is returned. + +## Working with Secrets + +### infisical.getSecret(secretName, options) ```js -const express = require("express"); -const port = 3000; -const infisical = require("infisical-node"); - -const main = async () => { - await infisical.connect({ - token: "st.xxx.xxx", - }); - - // your application logic - - app.get("/", (req, res) => { - res.send(`Howdy, ${infisical.get("NAME")}!`); - }); - - app.listen(port, async () => { - console.log(`App listening on port ${port}`); - }); -}; +const secret = await infisical.getSecret("API_KEY"); +const value = secret.secretValue; // get its value ``` - - We do not recommend hardcoding your [Infisical - Token](/getting-started/dashboard/token). Setting it as an environment - variable would be best. - +Retrieve a secret from Infisical. + +By default, `getSecret()` fetches and returns a personal secret. If not found, it returns a shared secret, or tries to retrieve the value from `process.env`. If a secret is fetched, `getSecret()` caches it to reduce excessive calls and re-fetches periodically based on the `cacheTTL` option (default is `300` seconds) when initializing the client — for more information, see the caching section. + + + + The key of the secret to retrieve + + + + + "personal" (default) or "shared". + + + + +### infisical.createSecret(secretName, secretValue, options) + +```js +const newApiKey = await infisical.createSecret("API_KEY", "FOO"); +``` + +Create a new secret in Infisical. + + + The key of the secret to create + + + The value of the secret to create + + + + + "shared" (default) or "personal". A personal secret can only be created if a shared secret with the same name exists. + + + + +### infisical.updateSecret(secretName, secretValue, options) + +```js +const updatedApiKey = await infisical.updateSecret("API_KEY", "BAR"); +``` + +Update an existing secret in Infisical. + + + The key of the secret to update + + + The new value of the secret + + + + + "shared" (default) or "personal". + + + + +### infisical.deleteSecret(secretName, options) + +```js +const deletedSecret = await infisical.deleteSecret("API_KEY"); +``` + +Delete a secret in Infisical. + + + The key of the secret to delete + + + + + "shared" (default) or "personal". Note that deleting a shared secret also deletes all associated personal secrets. + + + + + diff --git a/frontend/src/pages/dashboard/[id].tsx b/frontend/src/pages/dashboard/[id].tsx index 418c038f5..449182ed8 100644 --- a/frontend/src/pages/dashboard/[id].tsx +++ b/frontend/src/pages/dashboard/[id].tsx @@ -803,8 +803,6 @@ export default function Dashboard() { isReadDenied: false }; - console.log(124, envSlug, selectedWorkspaceEnv) - if (selectedWorkspaceEnv) { if (snapshotData) setSelectedSnapshotEnv(selectedWorkspaceEnv); else setSelectedEnv(selectedWorkspaceEnv); diff --git a/frontend/src/views/DashboardPage/DashboardEnvOverview.tsx b/frontend/src/views/DashboardPage/DashboardEnvOverview.tsx index b71e0118f..f4efa8583 100644 --- a/frontend/src/views/DashboardPage/DashboardEnvOverview.tsx +++ b/frontend/src/views/DashboardPage/DashboardEnvOverview.tsx @@ -125,7 +125,7 @@ export const DashboardEnvOverview = ({onEnvChange}: {onEnvChange: any;}) => { if (isSecretsLoading || isEnvListLoading) { return ( -
+
loading animation
); @@ -234,14 +234,14 @@ export const DashboardEnvOverview = ({onEnvChange}: {onEnvChange: any;}) => {
*/} -
+
0
0
{userAvailableEnvs?.map(env => { - return
+ return